diff --git a/.context/effect/.agents/AGENTS.md b/.context/effect/.agents/AGENTS.md new file mode 100644 index 000000000..c7bb67c03 --- /dev/null +++ b/.context/effect/.agents/AGENTS.md @@ -0,0 +1,163 @@ +This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. + +## Overview + +- The git base branch is `main`. +- Use `pnpm` as the package manager. +- Keep changes focused and follow established patterns in the repository. +- Before writing code, read the relevant files in `./.patterns/` and inspect similar existing code. + +## Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: + +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: + +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: + +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: + +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: + +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +## Workflow + +1. Inspect nearby implementation, tests, and pattern docs before editing. +2. Prefer existing abstractions and conventions over introducing new ones. +3. For ad hoc runnable code, create a temporary file in `scratchpad/`, run it with `node scratchpad/.ts`, and delete it when done. + The local runtime is Node 24, which can run TypeScript files directly; use plain `node` for local TypeScript probes instead of `tsx` unless `node` fails. +4. Run the validation appropriate to the change type. +5. Report which validation commands were run and any commands that could not be run. + +## Validation + +Use the narrowest validation that still covers the change: + +| Change type | Validation | +| -------------------------------- | ---------------------------------------------------------------------------------- | +| Code changes | `pnpm lint-fix`, targeted `pnpm test --run `, `pnpm check` | +| Tests-only changes | `pnpm lint-fix`, targeted `pnpm test --run `, `pnpm check` | +| Type-level/API type changes | Targeted `pnpm test-types `, plus `pnpm check` when source types changed | +| JSDoc text/category/link changes | `pnpm lint` | +| JSDoc example changes | `pnpm lint`; root `pnpm doctest --run ` | +| Docs-only changes | `pnpm lint-fix`; no tests required unless examples or code changed | + +Never run the whole test suite. A bare `pnpm test` or `pnpm doctest` runs every package in watch mode and will not +exit; always pass `--run` and the specific test files covering your change. CI runs the full suite +on push, so leave that to CI. + +## Bundle Size Preview + +When asked to show bundle-size impact for a commit, use the existing bundle comparison workflow: + +1. For the latest commit, run `pnpm bundle-compare HEAD~1`. + For another base, run `pnpm bundle-compare `. +2. Read the Markdown report from `tmp/bundle-stats.txt` and summarize the non-zero differences. +3. Leave `tmp/bundle-base` in place unless cleanup is requested. To clean it up, run `git worktree remove --force tmp/bundle-base`. + +## Coding Patterns + +Read `.patterns/effect.md` before changing Effect code. In particular: + +- Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. +- Prefer class syntax for `Context.Service`. +- Do not use `async` / `await` or `try` / `catch`; use Effect APIs such as `Effect.gen`, `Effect.fnUntraced`, and `Effect.tryPromise`. +- Do not use `Date.now` or `new Date`; use `Clock`, and use `TestClock` in tests. + +## Testing + +Read `.patterns/testing.md` before writing or changing tests. + +- Run only the tests covering the files you changed. +- From the repository root, run an affected package with `pnpm --filter effect test --run` only when package-wide coverage is necessary. +- Prefer a single test file, using a path relative to the package: `pnpm --filter effect test --run test/Option.test.ts`. + Replace the package name and test path with those covering your changed files, and narrow further with `-t ""` when useful. +- Test files are located in `packages/*/test/`. +- Main Effect library tests are in `packages/effect/test/`. +- Use `it.effect` for Effect-returning tests. +- `it.effect` and `it.live` already provide and close a `Scope` for each test; do not wrap test bodies in `Effect.scoped`. +- Use regular `it` for pure synchronous tests. +- Do not use `Effect.runSync` in tests. +- Do not use `expect` from Vitest; use `assert` from `@effect/vitest`. +- Type-level tests are in `packages/*/typetest/` and run with `pnpm test-types `. + +## Documentation + +- For AI documentation, read `ai-docs/README.md` very carefully before writing examples. +- AI documentation changes may include explanatory comments when useful. +- For public JSDoc categories and example best practices, read `.patterns/jsdoc.md`. +- Mark runnable TypeScript examples with `````ts import.meta.vitest``. Leave examples that register Vitest tests or suites + as plain `````ts`` fences because the doctest collector executes runnable snippets inside tests; invoke registration + APIs directly to show their intended top-level usage. +- Prefer direct trailing value assertions such as `operation() // => Option.some(1)`. Keep bindings only for reuse or meaningful multi-step setup, separate later assertion blocks with a blank line, use dense expected arrays such as `[1, 2]`, and keep a call on one line when the complete line is at most 120 characters. +- Assert semantic values rather than console formatting. Preserve `import.meta.vitest` on type-level examples without adding tautological runtime assertions. +- Keep marked examples self-contained, deterministic, bounded, and free of external-service dependencies. Await asynchronous work. +- Run `pnpm doctest --run ` from the repository root to execute changed examples. + +## Generated Files + +Do not hand-edit generated files. Run the appropriate generator instead. + +- `index.ts` barrel files are generated; run `pnpm codegen` after adding or removing modules. + +## Changesets + +Create a changeset in `.changeset/` for runtime behavior changes or exported type/API changes: + +```md +--- +"package-name": patch/minor/major +--- + +A description of the change. +``` + +Tests-only changes, internal refactors, docs-only changes, and JSDoc-only maintenance may skip changesets by maintainer decision. diff --git a/.context/effect/.agents/skills/grill-me/SKILL.md b/.context/effect/.agents/skills/grill-me/SKILL.md deleted file mode 100644 index d65f67b93..000000000 --- a/.context/effect/.agents/skills/grill-me/SKILL.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: grill-me -description: Interview the user about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". ---- - -Interview me about every aspect of this plan until we reach a shared understanding and a defensible design. - -Ask exactly one question at a time, then wait for my answer before asking the next question. - -Use each answer to choose the next highest-leverage unresolved question. Maintain an implicit decision tree of resolved decisions, open questions, assumptions, dependencies, risks, and rejected alternatives. - -For each question, include: - -- clear answer options when appropriate -- your recommended answer, marked as recommended -- a brief reason for the recommendation - -Use open-ended questions when fixed options would prematurely constrain the design space. - -Challenge vague, inconsistent, risky, or unsupported assumptions. If an answer creates a contradiction or unresolved dependency, ask a follow-up before moving on. - -Cover, as relevant: - -- goals and non-goals -- users and stakeholders -- constraints -- alternatives -- APIs and interfaces -- data model -- error handling -- security -- observability -- testing -- migration and rollout -- failure modes -- operational ownership -- success criteria - -If repository facts are needed, inspect the codebase instead of asking the user. Do not ask me to provide information that can be determined locally. - -When an available user-input tool such as `request_user_input` fits the question, use it to ask one short question with a small set of mutually exclusive options. Otherwise, ask in plain text and present clear possible answers as a numbered list when that helps me answer quickly. Include your recommended option and mark it as recommended. - -Stop when the major branches of the design tree have been resolved. Then summarize the agreed design, remaining risks, assumptions, rejected alternatives, and next steps. diff --git a/.context/effect/.agents/skills/jsdocs/SKILL.md b/.context/effect/.agents/skills/jsdocs/SKILL.md index 10b368bfe..a2972d8d8 100644 --- a/.context/effect/.agents/skills/jsdocs/SKILL.md +++ b/.context/effect/.agents/skills/jsdocs/SKILL.md @@ -39,7 +39,7 @@ Use a normal multiline JSDoc comment in TypeScript source: * * Optional prose explaining the example. * - * ```ts + * ```ts import.meta.vitest * const result = example() * ``` * @@ -126,6 +126,63 @@ Use a normal multiline JSDoc comment in TypeScript source: - For low-level public values, prefer accurate categories such as `symbols`, `type IDs`, or `prototypes` over compensating with verbose descriptions. +## Example quality + +Examples are optional. They should demonstrate: + +- behavior or constraints that are not clear from the signature; +- meaningful composition with other public APIs; +- a realistic use case supported by repository tests or call sites; or +- useful type inference, narrowing, or overload behavior. + +A good example: + +- focuses on the documented API and includes only the context needed to + understand it; +- is a complete, self-contained TypeScript module without placeholders or + omitted setup; +- imports public APIs rather than internal modules or unrelated test helpers; +- uses stable, deterministic, bounded behavior and does not require network + access, external services, timing assumptions, randomness, or machine-specific + state; +- demonstrates the meaningful result, with a concise expected-value comment + when useful; and +- uses explanatory prose only when the code cannot communicate an important + choice or caveat on its own. + +### Executable examples + +- Mark runnable TypeScript fences with `import.meta.vitest`. Run changed examples from the repository root with `pnpm doctest --run `. +- Write each marked example as a complete isolated module. Import public APIs, define every runtime value, await asynchronous work, and keep execution deterministic and bounded. +- Prefer `operation() // => expected` over introducing a result binding used only by the assertion. Retain bindings for reuse, mutation, identity checks, or meaningful multi-step setup, and insert a blank line before a separate assertion block. +- Keep direct assertions on one line up to 120 characters. Use dense expected arrays such as `[1, 2]` and semantic Effect values such as `Option.some(1)` rather than console formatting. +- Preserve `import.meta.vitest` for type-level examples, but do not add tautological runtime assertions to them. +- Leave examples that register Vitest tests or suites as plain `````ts`` fences because the doctest collector executes + runnable snippets inside tests. Call the registration API directly to show its intended top-level usage. +- Keep documentation-only snippets as plain `````ts`` fences. + +When reviewing existing examples: + +1. Derive the example's use case and behavior from repository evidence. Inspect + the declaration, implementation, tests, call sites, and related APIs. Do not + invent a scenario merely to retain an example. +2. Keep a correct, clear, high-value example without gratuitous rewriting. +3. Fix or replace an example when repository evidence supports a concise, + valuable version. +4. Remove an example when it is trivial, misleading, contrived, or requires more + scaffolding than the insight justifies. Also remove it when a good replacement + would require guessing at a use case. + +Prefer concise trailing `// =>` assertions that keep the meaningful result visible; +public documentation should not look like a test suite. Type-level examples may demonstrate inference or assignability +without runtime assertions. For lazy APIs such as `Effect`, execute enough of the +program to demonstrate the behavior unless the example's value is specifically +type-level or construction-oriented. + +If an example review exposes a likely implementation or type-definition bug, +do not change runtime or API code as part of the documentation pass. Report the +finding and do not present the suspected behavior as recommended usage. + ## Tag rules When multiple tags are present, keep them in this order: @@ -198,6 +255,6 @@ When refining an existing public API module, always do a dedicated `**Gotchas**` Run the narrowest validation that matches the change: -- For JSDoc or example changes in a package with generated docs, run `pnpm docgen` from that package directory. +- For runnable JSDoc example changes, run `pnpm doctest --run ` from the repository root. - Run `pnpm lint` because the linter includes the custom rule that checks public API JSDoc. - Do not run broad validation for prose-only skill edits. diff --git a/.context/effect/.changeset/config.json b/.context/effect/.changeset/config.json index 4511dd3a7..0a755e5f0 100644 --- a/.context/effect/.changeset/config.json +++ b/.context/effect/.changeset/config.json @@ -1,7 +1,8 @@ { - "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", + "$schema": "https://unpkg.com/@changesets/config@4.0.0-next.8/schema.json", "changelog": ["@changesets/changelog-github", { "repo": "Effect-TS/effect" }], "commit": false, + "format": false, "linked": [], "access": "restricted", "baseBranch": "main", @@ -14,7 +15,35 @@ "fixed": [ [ "effect", - "@effect/*" + "@effect/ai-anthropic", + "@effect/ai-openai", + "@effect/ai-openai-compat", + "@effect/ai-openrouter", + "@effect/atom-react", + "@effect/atom-solid", + "@effect/atom-vue", + "@effect/docgen", + "@effect/doctest", + "@effect/openapi-generator", + "@effect/opentelemetry", + "@effect/platform-browser", + "@effect/platform-bun", + "@effect/platform-deno", + "@effect/platform-node", + "@effect/platform-node-shared", + "@effect/sql-clickhouse", + "@effect/sql-d1", + "@effect/sql-libsql", + "@effect/sql-mssql", + "@effect/sql-mysql2", + "@effect/sql-pg", + "@effect/sql-pglite", + "@effect/sql-sqlite-bun", + "@effect/sql-sqlite-do", + "@effect/sql-sqlite-node", + "@effect/sql-sqlite-react-native", + "@effect/sql-sqlite-wasm", + "@effect/vitest" ] ], "snapshot": { diff --git a/.context/effect/.changeset/fix-otel-logger-clock-skew.md b/.context/effect/.changeset/fix-otel-logger-clock-skew.md deleted file mode 100644 index 8e6ae2967..000000000 --- a/.context/effect/.changeset/fix-otel-logger-clock-skew.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@effect/opentelemetry": patch ---- - -Use monotonic clock for log timestamps to match span timestamps. - -The Logger used `Date.now()` (wall clock) for log `timestamp` while the Tracer used `clock.currentTimeNanosUnsafe()` (monotonic clock) for span `startTime`. This caused logs to appear before their parent span due to clock drift between the two sources. Both now use the same monotonic clock via `nanosToHrTime(clock.currentTimeNanosUnsafe())`. diff --git a/.context/effect/.changeset/pre.json b/.context/effect/.changeset/pre.json index e66d435e2..d1deaa4cf 100644 --- a/.context/effect/.changeset/pre.json +++ b/.context/effect/.changeset/pre.json @@ -1,769 +1,4 @@ { "mode": "pre", - "tag": "beta", - "initialVersions": { - "@effect/ai-anthropic": "3.0.0", - "@effect/ai-openai": "3.0.0", - "@effect/ai-openrouter": "3.0.0", - "@effect/atom-react": "3.0.0", - "@effect/atom-solid": "3.0.0", - "@effect/atom-vue": "3.0.0", - "effect": "3.0.0", - "@effect/opentelemetry": "3.0.0", - "@effect/platform-browser": "3.0.0", - "@effect/platform-bun": "3.0.0", - "@effect/platform-node": "3.0.0", - "@effect/platform-node-shared": "3.0.0", - "@effect/sql-clickhouse": "3.0.0", - "@effect/sql-d1": "3.0.0", - "@effect/sql-libsql": "3.0.0", - "@effect/sql-mssql": "3.0.0", - "@effect/sql-mysql2": "3.0.0", - "@effect/sql-pg": "3.0.0", - "@effect/sql-sqlite-bun": "3.0.0", - "@effect/sql-sqlite-do": "3.0.0", - "@effect/sql-sqlite-node": "3.0.0", - "@effect/sql-sqlite-react-native": "3.0.0", - "@effect/sql-sqlite-wasm": "3.0.0", - "@effect/openapi-generator": "3.0.0", - "@effect/oxc": "3.0.0", - "@effect/utils": "3.0.0", - "@effect/vitest": "3.0.0", - "scratchpad": "0.0.0", - "scripts": "0.0.0", - "@effect/ai-openai-compat": "3.0.0", - "@effect/ai-codegen": "0.0.0", - "@effect/bundle": "0.0.0", - "ai-docs": "0.0.0", - "@effect/ai-docgen": "0.0.0", - "@effect/sql-pglite": "4.0.0-beta.52", - "@effect/jsdocs": "0.0.0" - }, - "changesets": [ - "add-adaptive-rate-limiter-store", - "add-bigdecimal-sumall-multiplyall", - "add-chunk-schema", - "add-command-hidden", - "add-config-nested", - "add-flag-hidden", - "add-from-string-schemas", - "add-headers-remove-many", - "add-indexeddb-kvs-layer", - "add-make-msgpack", - "add-make-option", - "add-missing-tx-modules", - "add-newtype-module", - "add-scalar-show-operation-id", - "add-schedule-tap", - "add-schema-annotate-encoded", - "add-schema-array-ensure", - "add-schema-bigdecimal", - "add-schema-datetime", - "add-schema-error-module", - "add-schema-option-from-optional-nullor", - "add-schema-option-from-undefined-nullish", - "add-schema-string-encoding", - "add-schema-tagged-union-discriminants", - "add-sql-pglite", - "add-standard-jsdoc-rule", - "add-stream-broadcastn", - "add-unstable-encoding-export", - "add-values-unprepared", - "afraid-cobras-like", - "ai-openai-config-field-leak", - "ai-openai-file-nullable-fields", - "anthropic-4-6-structured-output", - "anthropic-open-model-enum", - "apply-httpapi-endpoint-client-transform", - "asyncresult-exhaustive", - "atom-stream-error-type", - "beige-goats-sin", - "beige-paths-sort", - "better-apples-nail", - "better-rocks-arrive", - "better-shrimps-follow", - "big-pans-look", - "blue-dingos-greet", - "blue-ligers-cheat", - "blue-onions-smile", - "blue-ravens-type", - "blue-trams-kiss", - "bold-chairs-yawn", - "bold-planets-shout", - "breezy-meals-see", - "bright-bugs-bow", - "bright-canyons-clean", - "bright-dogs-fail", - "bright-flags-stand", - "bright-laws-teach", - "bright-lemons-dance", - "bright-planes-smash", - "bright-rats-attend", - "bright-toes-rush", - "bumpy-boxes-teach", - "busy-lions-sneeze", - "busy-maps-attend", - "calm-buckets-own", - "calm-carrots-march", - "calm-cars-rest", - "calm-heads-close", - "calm-masks-count", - "calm-panthers-nail", - "calm-ravens-reflect", - "calm-seas-smile", - "calm-squids-hug", - "calm-tracers-sample", - "chatty-poets-type", - "chilled-mice-wash", - "chilly-pumas-rule", - "chubby-buckets-feel", - "chubby-parents-flow", - "chubby-planets-fall", - "clean-balloons-tan", - "clean-bulldogs-care", - "clean-dryers-sneeze", - "clean-geese-work", - "clean-goats-wave", - "clean-needles-shake", - "clean-tires-guess", - "clear-hairs-pump", - "clear-spies-boil", - "cli-config-built-ins", - "cli-help-choices", - "cli-wizard-mode", - "cold-knives-lie", - "cold-rooms-show", - "cold-sloths-wave", - "common-mammals-tickle", - "compact-json-schema-enum", - "config-withdefault-eager", - "consolidate-encoding", - "consolidate-sql-error", - "crisp-seas-warn", - "cron-locale-independent-aliases", - "cron-single-value-step", - "cron-testclock-infinity", - "cuddly-rooms-bet", - "curly-poems-talk", - "curly-spies-relax", - "curvy-apples-float", - "curvy-birds-float", - "custom-http-security-openapi-generator", - "cute-heads-thank", - "cyan-loops-grow", - "cyan-radios-switch", - "cyan-shirts-grin", - "deep-rivers-spend", - "dirty-lamps-trade", - "dirty-laws-wear", - "duration-temporal-object-input", - "eager-coats-cheat", - "early-birds-dream", - "early-donuts-argue", - "early-peaches-check", - "eff-691-default-logger-ordering", - "eff-693-rpcgroup-handler-deps", - "eff-694-cli-completions-module", - "eff-695-layer-mock-dual-api", - "eff-697-rpcserialization-json-array-decode", - "eff-698-rpcserialization-unreachable-branch", - "eff-700-httpapi-middleware-errors", - "eff-701-httpapierror-respondable", - "eff-704-stream-merge-predicate", - "eff-705-layer-tap-apis", - "eff-706-servicemap-mutate", - "eff-716-response-id-tracker-map", - "eff-717-openai-socket-cancel", - "eff-718-embedding-model-surface", - "eff-725-fix-catch-jsdoc", - "eff-726-model-dimensions", - "eff-727-cli-help-alignment", - "eff-730-language-model-incremental-fallback", - "eff-736-cached-with-ttl", - "eff-738-cron-prev", - "eff-739-openai-function-call-done", - "eff-740-missing-summary-parts", - "eff-742-http-client-request-web", - "eff-744-sqlite-migrator-lock", - "eff-746-fixed-iteration-catchup", - "eff-747-unify-effect", - "eff-754-url-builder-any", - "eff-755-references-core", - "eff-769-select-text-highlight", - "eff-774-mutable-list-append-all-empty-array", - "eff-777-schema-make-effect", - "eff-778-http-middleware-path-logger", - "eff-779-keyvaluestore-layer-sql", - "eff-780-layer-unify", - "eff-781-fix-stream-toqueue-types", - "eff-782-httpapi-status-literals", - "eff-783-atom-http-api-errors", - "eff-819-cluster-workflow-shard-groups", - "eff-849-transpose-option", - "eff-946-concurrent-traversal-cleanup", - "eff-952-terminal-failure-stack", - "eff-953-interruptor-stack-trace", - "eff-955-run-sync-dispatcher", - "eff-956-await-all-children", - "eight-turkeys-own", - "eighty-lies-deny", - "eighty-poets-draw", - "eighty-swans-scream", - "eighty-teeth-sniff", - "eleven-apes-share", - "eleven-numbers-bake", - "empty-env-values-missing", - "empty-gifts-beg", - "empty-http-rpc-client", - "eventlog-unencrypted", - "every-olives-burn", - "expand-schema-filter-output", - "export-schema-encode-keys-interface", - "extract-semaphore-latch", - "fair-bees-relax", - "fair-birds-limit", - "fair-buttons-share", - "fair-cooks-stop", - "fair-cups-train", - "fair-dryers-speak", - "fair-forks-shake", - "fair-jobs-like", - "fair-pandas-prove", - "fair-pants-float", - "fair-poems-visit", - "famous-wolves-lead", - "fancy-glasses-grow", - "fast-graph-path-queues", - "fast-times-camp", - "few-birds-matter", - "few-cougars-dig", - "few-foxes-grin", - "few-mirrors-pull", - "few-socks-poke", - "fiber-runtime-start-metrics", - "fiery-jokes-care", - "fiery-mammals-call", - "fine-walls-decide", - "first-success-of", - "five-parents-relax", - "five-worms-rhyme", - "fix-1332", - "fix-1917", - "fix-1927", - "fix-1940", - "fix-1947", - "fix-2002", - "fix-2012", - "fix-2015", - "fix-2260", - "fix-2268", - "fix-2271", - "fix-2384", - "fix-2414", - "fix-2419", - "fix-2497", - "fix-2499", - "fix-6464", - "fix-6491", - "fix-6521", - "fix-ai-empty-params-structured-output", - "fix-ai-text-toolkit-typing", - "fix-anthropic-caller-toolid", - "fix-anthropic-memory-tool-requires-handler", - "fix-anthropic-memory-tool", - "fix-atom-kvs-async-write", - "fix-catch-orelse-error-erasure", - "fix-class-constructor-defaults", - "fix-cli-missing-flag-values", - "fix-cli-mixed-global-flag-context", - "fix-cli-subcommands-requirements", - "fix-cluster-entity-context-bleed", - "fix-config-array-default", - "fix-config-withDefault", - "fix-config-withdefault-filter", - "fix-cron-and-representations", - "fix-cron-make-validation", - "fix-cron-next-missing-day-overflow", - "fix-cron-parser-semantics", - "fix-cron-prev-month-rollover", - "fix-cron-prev-weekday-wrap", - "fix-cron-timezone-hash", - "fix-datetime-gmt", - "fix-devtools-flush-on-teardown", - "fix-durable-race-replay", - "fix-duration-symmetric-rounding", - "fix-entity-manager-defect-replay", - "fix-entity-proxy-rpc-handler-context", - "fix-entity-proxy-server-path-params", - "fix-from-json-string-identifier", - "fix-from-readable-stream-cancel-defect", - "fix-graph-allocator-equality", - "fix-graph-bellman-ford-self-cycle", - "fix-graph-curried-getters", - "fix-graph-edge-transforms", - "fix-graph-finite-edge-weights", - "fix-graph-mutable-hash", - "fix-graph-mutable-topo", - "fix-graph-topo-types", - "fix-graph-undirected-traversal", - "fix-graph-walker-repeatability", - "fix-graphviz-dot-escaping", - "fix-has-interrupts-only-empty", - "fix-hashmap-bit31-ordering", - "fix-headers-proto-enumerability", - "fix-http-incoming-message-parse-options", - "fix-http-pre-response-handler-types", - "fix-http-tracer-response-cause", - "fix-httpapi-authorization-decoding", - "fix-httpapi-client-error-content-type", - "fix-httpapi-endpoint-error-inference", - "fix-httpapi-malformed-json-400", - "fix-httpapi-runtime-shape", - "fix-httpapi-schema-types", - "fix-httpapi-security-middleware-cache", - "fix-invalid-value-doubled-expected", - "fix-is-json-dag", - "fix-json-schema-anyof-oneof-siblings", - "fix-json-schema-import-json", - "fix-keepalive-blocked-timers", - "fix-mcp-param-name-resolution", - "fix-mermaid-escape-special-chars", - "fix-mutable-list-empty-filter", - "fix-mutable-list-filter-length", - "fix-number-remainder-scientific-notation", - "fix-object-keyword-json-schema", - "fix-one-shot-iterables", - "fix-openai-mcp-tool-names", - "fix-openapi-generator-form-urlencoded", - "fix-openapi-generator-swagger2openapi", - "fix-openapi-preserve-multiple-response-content-types", - "fix-openrouter-sparse-array", - "fix-otel-logger-clock-skew", - "fix-otel-logger-severity-number", - "fix-pending-interruptible-mask", - "fix-persisted-queue-attempt-accounting", - "fix-queue-collect-duplication", - "fix-random-string-seeds", - "fix-ratelimiter-tokenbucket-redis-ttl", - "fix-redis-persisted-queue", - "fix-remainder-scientific-notation", - "fix-request-resolver-pending-batches-leak", - "fix-retry-transient-autocomplete", - "fix-rpc-http-requestids-finalizer", - "fix-rpc-json-id-edges", - "fix-rpc-unknown-tag-isolation", - "fix-schedule-fixed-double-exec", - "fix-schedule-reduce-sync-state", - "fix-schema-arbitrary-exclusive-bounds", - "fix-schema-bracket-prototype-pollution", - "fix-schema-defect-message", - "fix-schema-encode-keys-property-keys", - "fix-schema-encodekeys-class", - "fix-schema-encodekeys-struct", - "fix-schema-encoding-checks", - "fix-schema-identifier-expected-message", - "fix-schema-is-json-records", - "fix-schema-is-uuid", - "fix-schema-json-tuple-allof", - "fix-schema-option-non-schema-failures", - "fix-schema-parser-checks", - "fix-schema-tuple-post-rest-indexing", - "fix-schema-union-dispatch-order", - "fix-searchparam-initial-decode", - "fix-serializable-wire-transfer", - "fix-sql-persisted-queue-lock-refresh", - "fix-stream-grouped-within-flush", - "fix-stream-run-for-each-while", - "fix-stream-scan-effect", - "fix-stream-scoped-scope", - "fix-string-case-digits", - "fix-strip-approval-artifacts-multi-round", - "fix-struct-utility-types-simplify", - "fix-structural-proto-equality", - "fix-structwithrest-index-signatures", - "fix-tagged-union-class-sentinels", - "fix-tagged-union-match-unify", - "fix-to-tagged-union-isanyof-custom-tags", - "fix-tool-provider-defined-clone", - "fix-tuple-with-rest-post-rest-index-drift", - "fix-tuple-with-rest-post-rest-validation", - "fix-types-voidifempty", - "fix-vitest-proxy-chained-helpers", - "fix-vitest-record-schema-arbitrary", - "fix-void-response-encoding", - "fix-workflow-defect-reply-serialization", - "fix-workflow-entity-client-collision", - "fix-workflow-proxy-rpc-handler-context", - "flat-chicken-remain", - "floppy-cows-spend", - "floppy-items-admire", - "floppy-pigs-kiss", - "floppy-rats-leave", - "floyd-warshall-null-edge-data", - "fluffy-meals-matter", - "fluffy-pumas-push", - "forked-memo-maps", - "forty-hounds-cheer", - "forty-otters-cry", - "forty-rings-film", - "forty-signs-stay", - "forty-swans-divide", - "forty-trees-pay", - "four-papayas-bow", - "four-points-repeat", - "fresh-cats-smash", - "fresh-cycles-wait", - "fresh-emus-cheat", - "fresh-monkeys-smoke", - "frozen-intrinsics-stack-trace-limit", - "fruity-houses-learn", - "full-adults-double", - "funny-crabs-hang", - "funny-forks-move", - "fuzzy-camels-hunt", - "fuzzy-cats-kill", - "fuzzy-cats-listen", - "fuzzy-crews-fold", - "fuzzy-dodos-help", - "fuzzy-lions-perform", - "fuzzy-pandas-smile", - "fuzzy-planets-sneeze", - "fuzzy-stamps-care", - "giant-jeans-float", - "gold-meteors-move", - "gold-readers-hug", - "gold-rings-start", - "good-tools-work", - "good-trees-pull", - "graph-acyclic-parallel-undirected-edges", - "graph-algorithm-fixes", - "graph-finalized-mutation-handle", - "graph-guard-predicates", - "graph-sync-mutation-callbacks", - "graph-undirected-edge-equality", - "graph-walker-iterator-receiver", - "great-trains-mate", - "great-trams-report", - "green-beds-unref", - "green-chips-wash", - "green-moons-smile", - "green-pugs-play", - "green-rings-prove", - "happy-mirrors-dream", - "harden-httpapi-documentation-html", - "heavy-loops-cut", - "heavy-trams-fix", - "hip-friends-kiss", - "hip-socks-travel", - "honest-pens-thank", - "honest-rivers-notice", - "hot-taxis-fry", - "hot-teeth-clean", - "httpapi-endpoint-relax-constraints", - "httpapi-schema-service-types", - "huge-moons-rhyme", - "humble-pigs-dig", - "hungry-kings-look", - "icy-flies-cross", - "itchy-radios-poke", - "itchy-results-bet", - "itchy-shrimps-deny", - "itchy-toes-promise", - "k8s-last-transition-null", - "keep-httpapi-composition-immutable", - "khaki-cats-learn", - "khaki-melons-appear", - "kind-hounds-float", - "kind-windows-fall", - "late-hotels-rule", - "late-lamps-care", - "late-rivers-applaud", - "layer-map-dynamic-idle-ttl", - "lazy-queens-rush", - "lazy-recursive-forward-refs", - "lazy-timers-exist", - "legal-pants-drop", - "lemon-taxis-sin", - "light-kids-sneeze", - "little-dryers-allow", - "long-cameras-think", - "lovely-cobras-change", - "lovely-frogs-rescue", - "lucky-buttons-jump", - "lucky-phones-listen", - "lucky-worms-type", - "major-chairs-design", - "many-badgers-obey", - "mean-dingos-share", - "mean-trains-smash", - "metal-nails-sneeze", - "metal-parts-yell", - "mighty-games-matter", - "modern-carrots-see", - "modern-uuid-guid-filter", - "multipart-onDone-clobbers-error", - "mysql2-disable-prepared-statements", - "nasty-geese-grow", - "neat-goats-wave", - "neat-kings-chew", - "neat-lions-rest", - "neat-snails-wash", - "neat-taxis-notice", - "neat-windows-buy", - "new-dogs-swim", - "new-toes-stop", - "ninety-geese-exist", - "normalize-httpapi-payload-media-types", - "o8drprcu-sqlite-node-node-sqlite", - "odd-boats-think", - "odd-bulldogs-sleep", - "odd-fans-glow", - "odd-forks-talk", - "odd-laws-draw", - "odd-owls-smoke", - "odd-socks-boil", - "odd-suns-dance", - "old-brooms-cry", - "old-facts-stand", - "old-mirrors-float", - "olive-poems-visit", - "opaque-graph-interface", - "open-hotels-remain", - "openai-compat-empty-assistant-content", - "openai-compat-nullable-tool-name", - "openai-compat-reasoning", - "openapi-generator-sse-constraint-decoder", - "openrouter-input-audio", - "opentelemetry-render-causes", - "optimize-httpapi-handler-types", - "otel-resource-env-precedence", - "perfect-buckets-tickle", - "petite-months-allow", - "platform-crypto-service", - "platform-node-shared-barrel", - "plenty-moons-pull", - "polite-brooms-tickle", - "polite-pigs-speak", - "polite-tables-kneel", - "port-effect-reduce", - "port-react-hydration", - "pretty-moments-clap", - "public-deer-ring", - "public-jeans-stop", - "pubsub-publish-false", - "puny-pens-clap", - "purple-bars-prove", - "purple-schools-float", - "purple-turtles-draw", - "quick-dragons-fix", - "quick-falcons-travel", - "quick-geese-relax", - "quick-lamps-dig", - "quick-lizards-fall", - "quick-trees-join", - "quiet-carpets-grin", - "quiet-crons-report", - "quiet-fibers-settle", - "quiet-files-hunt", - "quiet-lamps-jam", - "quiet-pandas-respond", - "quiet-radios-wave", - "quiet-redis-scripts", - "quiet-tigers-yell", - "quiet-turtles-smile", - "random-choice", - "ready-olives-divide", - "real-trains-ring", - "red-pigs-repair", - "redacted-representation-options", - "refactor-cli-global-flags", - "refactor-config-provider", - "refactor-representation-references", - "remove-effect-transactionwith", - "remove-http-span-counter", - "remove-nullor", - "remove-openapi-fromapi-options", - "remove-schedule-apis", - "remove-schedule-either", - "remove-schedule-elapsed", - "remove-schedule-taps", - "remove-schema-stringtree-keep-declarations", - "remove-types-mergerecord", - "remove-unused-utils-apis", - "rename-rebuild-out", - "restore-schema-parse-options", - "retry-redis-script-load", - "reuse-httpapi-response-schemas", - "rich-dots-push", - "rich-hoops-nail", - "rich-sloths-draw", - "ripe-lies-battle", - "rpc-client-http-early-close", - "rpc-middleware-provides-fix", - "scalar-custom-fetch", - "schema-as-class", - "schema-asserts-signature", - "schema-clean-up-additionalProperties", - "schema-codec-narrowing", - "schema-datetime-utc-from-string", - "schema-decoding-defaults-services", - "schema-defaults-issue-channel", - "schema-dollar-prefix", - "schema-lazy-bottom", - "schema-missing-self-generic", - "schema-ordered-arbitrary-constraints", - "schema-parser-adapter-errors", - "schema-refactor-toCodecJson", - "schema-remove-annotate-in", - "schema-rename-makeUnsafe-to-make", - "schema-rename-parser-makeUnsafe", - "schema-result-combinators", - "schema-struct-simplify", - "semantic-matching", - "seven-mugs-marry", - "shaggy-birds-stay", - "shaggy-cities-push", - "shaggy-numbers-accept", - "shaky-beans-throw", - "sharp-emus-applaud", - "sharp-goats-wink", - "sharp-pandas-care", - "sharp-peas-march", - "sharp-rules-draw", - "sharp-singers-sort", - "shiny-trains-hug", - "short-cows-relate", - "short-foxes-admire", - "short-stamps-throw", - "shy-cycles-flow", - "shy-geckos-sniff", - "silent-geckos-matter", - "silent-needles-design", - "silent-plants-matter", - "silent-spoons-stare", - "silly-loops-tickle", - "silver-bulk-indexeddb", - "silver-emus-smoke", - "silver-kings-poke", - "silver-snails-sqlite", - "silver-wings-watch", - "six-cups-taste", - "six-pumas-take", - "sixty-mails-shout", - "sixty-socks-yell", - "slick-signs-wish", - "slick-toes-rush", - "slimy-planets-divide", - "slimy-turtles-juggle", - "slow-beans-battle", - "slow-berries-enjoy", - "small-bugs-hunt", - "small-crabs-care", - "small-pandas-cache", - "small-pets-sit", - "smart-ducks-jump", - "smart-pillows-buy", - "smart-timers-fly", - "smart-tips-sort", - "social-hoops-knock", - "social-pumas-prove", - "soft-comics-wink", - "soft-delete-sqlmodel", - "soft-seals-allow", - "solid-cougars-attack", - "solid-doors-ring", - "solid-items-tease", - "solid-towns-smoke", - "sour-canyons-rescue", - "sparkly-bears-act", - "sparkly-coins-sit", - "spotty-comics-fry", - "sql-migrator-mjs-mts", - "sqlite-bun-prepare-error-channel", - "sqlite-do-durable-object-transactions", - "stale-dots-tell", - "stale-graph-traversal-skips", - "stale-laws-do", - "stale-snakes-know", - "strict-areas-end", - "strict-buckets-hug", - "strip-resolved-approvals", - "strong-balloons-tickle", - "strong-bees-queue", - "strong-insects-film", - "struct-record", - "sunny-ads-hang", - "sunny-bikes-sleep", - "sunny-rooms-invent", - "sweet-donuts-bet", - "sweet-hotels-give", - "sweet-schedules-matter", - "sweet-views-learn", - "swift-spiders-unpack", - "swift-symbols-stand", - "tagged-error-class-optional-empty-props", - "tall-hairs-return", - "tall-ideas-fix", - "tall-mails-listen", - "tall-queens-cheer", - "tall-wombats-wave", - "tangy-colts-lose", - "tangy-plants-run", - "tasty-comics-send", - "tasty-moments-post", - "ten-kings-fry", - "thick-pandas-wait", - "thin-ducks-wonder", - "thirty-ducks-go", - "thirty-pans-love", - "three-corners-sort", - "three-ravens-jam", - "three-tomatoes-wave", - "tidy-foxes-own", - "tidy-icons-glow", - "tidy-int32-annotations", - "tidy-pandas-smile", - "tidy-stacks-encode", - "tidy-stars-drive", - "tiny-buckets-wave", - "tiny-lilies-flash", - "tiny-rabbits-smile", - "to-codec-json-schema", - "tocodecjson-return-json-type", - "tool-get-json-schema-tests", - "true-actors-battle", - "try-promise-catch-defect", - "twelve-dragons-move", - "twenty-buttons-cheer", - "twenty-facts-laugh", - "two-roses-double", - "unify-error-defect-stack-options", - "update-schema-arbitrary-report", - "upset-colts-stick", - "use-url-can-parse", - "validate-httpapi-handler-registration", - "validate-openapi-global-conflicts", - "vast-bananas-send", - "vast-deserts-travel", - "violet-peaches-feel", - "violet-tips-open", - "vitest-layer-top-level-options", - "wacky-grapes-poke", - "wacky-rice-add", - "warm-dolls-brake", - "warm-friends-tie", - "warm-snails-shop", - "wet-news-invent", - "whole-pets-build", - "wild-readers-clean", - "wild-suns-bearer-space", - "wise-ants-wave", - "wise-flags-shift", - "wise-oranges-stay", - "witty-lobsters-share", - "yellow-adults-study", - "yellow-clocks-dance", - "yellow-dingos-jump", - "young-doors-change" - ] + "tag": "rc" } diff --git a/.context/effect/.changeset/add-adaptive-rate-limiter-store.md b/.context/effect/.changeset/pre/add-adaptive-rate-limiter-store.md similarity index 100% rename from .context/effect/.changeset/add-adaptive-rate-limiter-store.md rename to .context/effect/.changeset/pre/add-adaptive-rate-limiter-store.md diff --git a/.context/effect/.changeset/pre/add-atom-equality.md b/.context/effect/.changeset/pre/add-atom-equality.md new file mode 100644 index 000000000..c2600bfa5 --- /dev/null +++ b/.context/effect/.changeset/pre/add-atom-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/reactivity Atom: add `withEquality` combinator for customizing how the registry detects value changes diff --git a/.context/effect/.changeset/add-bigdecimal-sumall-multiplyall.md b/.context/effect/.changeset/pre/add-bigdecimal-sumall-multiplyall.md similarity index 100% rename from .context/effect/.changeset/add-bigdecimal-sumall-multiplyall.md rename to .context/effect/.changeset/pre/add-bigdecimal-sumall-multiplyall.md diff --git a/.context/effect/.changeset/add-chunk-schema.md b/.context/effect/.changeset/pre/add-chunk-schema.md similarity index 100% rename from .context/effect/.changeset/add-chunk-schema.md rename to .context/effect/.changeset/pre/add-chunk-schema.md diff --git a/.context/effect/.changeset/add-command-hidden.md b/.context/effect/.changeset/pre/add-command-hidden.md similarity index 100% rename from .context/effect/.changeset/add-command-hidden.md rename to .context/effect/.changeset/pre/add-command-hidden.md diff --git a/.context/effect/.changeset/add-config-nested.md b/.context/effect/.changeset/pre/add-config-nested.md similarity index 100% rename from .context/effect/.changeset/add-config-nested.md rename to .context/effect/.changeset/pre/add-config-nested.md diff --git a/.context/effect/.changeset/pre/add-deno-file-system.md b/.context/effect/.changeset/pre/add-deno-file-system.md new file mode 100644 index 000000000..6bd6a73df --- /dev/null +++ b/.context/effect/.changeset/pre/add-deno-file-system.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a Deno-backed FileSystem layer. diff --git a/.context/effect/.changeset/pre/add-deno-http-client.md b/.context/effect/.changeset/pre/add-deno-http-client.md new file mode 100644 index 000000000..2bf5753f8 --- /dev/null +++ b/.context/effect/.changeset/pre/add-deno-http-client.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-deno": patch +--- + +Add `DenoHttpClient`, re-exporting `effect/unstable/http/FetchHttpClient` + +Deno's `fetch` is spec-compliant, so the core fetch-based `HttpClient` works on Deno unmodified. This module mirrors `BunHttpClient` so the platform packages expose a consistent surface. diff --git a/.context/effect/.changeset/pre/add-deno-multipart.md b/.context/effect/.changeset/pre/add-deno-multipart.md new file mode 100644 index 000000000..b396bfd8c --- /dev/null +++ b/.context/effect/.changeset/pre/add-deno-multipart.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add web-standard multipart request parsing helpers for Deno. diff --git a/.context/effect/.changeset/pre/add-deno-socket-server.md b/.context/effect/.changeset/pre/add-deno-socket-server.md new file mode 100644 index 000000000..d72662428 --- /dev/null +++ b/.context/effect/.changeset/pre/add-deno-socket-server.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add native Deno TCP, Unix, and TLS socket server adapters. diff --git a/.context/effect/.changeset/pre/add-deno-socket.md b/.context/effect/.changeset/pre/add-deno-socket.md new file mode 100644 index 000000000..da98c6486 --- /dev/null +++ b/.context/effect/.changeset/pre/add-deno-socket.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add native Deno TCP, Unix, and WebSocket integrations for Effect sockets. diff --git a/.context/effect/.changeset/add-flag-hidden.md b/.context/effect/.changeset/pre/add-flag-hidden.md similarity index 100% rename from .context/effect/.changeset/add-flag-hidden.md rename to .context/effect/.changeset/pre/add-flag-hidden.md diff --git a/.context/effect/.changeset/add-from-string-schemas.md b/.context/effect/.changeset/pre/add-from-string-schemas.md similarity index 100% rename from .context/effect/.changeset/add-from-string-schemas.md rename to .context/effect/.changeset/pre/add-from-string-schemas.md diff --git a/.context/effect/.changeset/add-headers-remove-many.md b/.context/effect/.changeset/pre/add-headers-remove-many.md similarity index 100% rename from .context/effect/.changeset/add-headers-remove-many.md rename to .context/effect/.changeset/pre/add-headers-remove-many.md diff --git a/.context/effect/.changeset/pre/add-http-client-request-update-headers.md b/.context/effect/.changeset/pre/add-http-client-request-update-headers.md new file mode 100644 index 000000000..391894aa6 --- /dev/null +++ b/.context/effect/.changeset/pre/add-http-client-request-update-headers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +unstable/http HttpClientRequest: add `updateHeaders` and `removeHeader` combinators for transforming or removing request headers, closes #6271 diff --git a/.context/effect/.changeset/pre/add-http-client-tracer-header-filter.md b/.context/effect/.changeset/pre/add-http-client-tracer-header-filter.md new file mode 100644 index 000000000..44d804467 --- /dev/null +++ b/.context/effect/.changeset/pre/add-http-client-tracer-header-filter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add a configurable filter for HTTP client request and response header span attributes. diff --git a/.context/effect/.changeset/pre/add-httpapi-with-headers.md b/.context/effect/.changeset/pre/add-httpapi-with-headers.md new file mode 100644 index 000000000..e91cccacc --- /dev/null +++ b/.context/effect/.changeset/pre/add-httpapi-with-headers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +httpapi: add typed response headers across handlers, generated clients (including `HttpApiTest`), streaming responses, and OpenAPI with `HttpApiSchema.WithHeaders`. Add `HttpApiSchema.encodeToWithHeaders` for folding response headers into domain types such as error classes. Explicit `content-type` and `content-length` values applied with `HttpServerResponse.setHeader` or `setHeaders` now override body-derived values. diff --git a/.context/effect/.changeset/add-indexeddb-kvs-layer.md b/.context/effect/.changeset/pre/add-indexeddb-kvs-layer.md similarity index 100% rename from .context/effect/.changeset/add-indexeddb-kvs-layer.md rename to .context/effect/.changeset/pre/add-indexeddb-kvs-layer.md diff --git a/.context/effect/.changeset/pre/add-json-schema-draft-04.md b/.context/effect/.changeset/pre/add-json-schema-draft-04.md new file mode 100644 index 000000000..440b16628 --- /dev/null +++ b/.context/effect/.changeset/pre/add-json-schema-draft-04.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add support for converting JSON Schema documents to Draft-04, preserve literal `$ref` values, `$ref` sibling constraints, `not`, `readOnly`, and `writeOnly` in Draft-07 conversions, correct the Draft-07 meta-schema URI, and prevent OpenAPI component-key collisions during conversion. diff --git a/.context/effect/.changeset/add-make-msgpack.md b/.context/effect/.changeset/pre/add-make-msgpack.md similarity index 100% rename from .context/effect/.changeset/add-make-msgpack.md rename to .context/effect/.changeset/pre/add-make-msgpack.md diff --git a/.context/effect/.changeset/add-make-option.md b/.context/effect/.changeset/pre/add-make-option.md similarity index 100% rename from .context/effect/.changeset/add-make-option.md rename to .context/effect/.changeset/pre/add-make-option.md diff --git a/.context/effect/.changeset/add-missing-tx-modules.md b/.context/effect/.changeset/pre/add-missing-tx-modules.md similarity index 100% rename from .context/effect/.changeset/add-missing-tx-modules.md rename to .context/effect/.changeset/pre/add-missing-tx-modules.md diff --git a/.context/effect/.changeset/add-newtype-module.md b/.context/effect/.changeset/pre/add-newtype-module.md similarity index 100% rename from .context/effect/.changeset/add-newtype-module.md rename to .context/effect/.changeset/pre/add-newtype-module.md diff --git a/.context/effect/.changeset/pre/add-otlp-manual-flush.md b/.context/effect/.changeset/pre/add-otlp-manual-flush.md new file mode 100644 index 000000000..fbbd9b16a --- /dev/null +++ b/.context/effect/.changeset/pre/add-otlp-manual-flush.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add manual flushing to the OTLP exporters through a shared `Flusher` service exposed by each signal layer. The signal layer output types now include `Flusher`, and `OtlpExporter.make` requires it so custom exporters register unconditionally. diff --git a/.context/effect/.changeset/pre/add-platform-deno.md b/.context/effect/.changeset/pre/add-platform-deno.md new file mode 100644 index 000000000..9237eee68 --- /dev/null +++ b/.context/effect/.changeset/pre/add-platform-deno.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add Deno platform integrations for paths, runtime execution, workers, and Web Storage. diff --git a/.context/effect/.changeset/add-scalar-show-operation-id.md b/.context/effect/.changeset/pre/add-scalar-show-operation-id.md similarity index 100% rename from .context/effect/.changeset/add-scalar-show-operation-id.md rename to .context/effect/.changeset/pre/add-scalar-show-operation-id.md diff --git a/.context/effect/.changeset/add-schedule-tap.md b/.context/effect/.changeset/pre/add-schedule-tap.md similarity index 100% rename from .context/effect/.changeset/add-schedule-tap.md rename to .context/effect/.changeset/pre/add-schedule-tap.md diff --git a/.context/effect/.changeset/add-schema-annotate-encoded.md b/.context/effect/.changeset/pre/add-schema-annotate-encoded.md similarity index 100% rename from .context/effect/.changeset/add-schema-annotate-encoded.md rename to .context/effect/.changeset/pre/add-schema-annotate-encoded.md diff --git a/.context/effect/.changeset/add-schema-array-ensure.md b/.context/effect/.changeset/pre/add-schema-array-ensure.md similarity index 100% rename from .context/effect/.changeset/add-schema-array-ensure.md rename to .context/effect/.changeset/pre/add-schema-array-ensure.md diff --git a/.context/effect/.changeset/add-schema-bigdecimal.md b/.context/effect/.changeset/pre/add-schema-bigdecimal.md similarity index 100% rename from .context/effect/.changeset/add-schema-bigdecimal.md rename to .context/effect/.changeset/pre/add-schema-bigdecimal.md diff --git a/.context/effect/.changeset/add-schema-datetime.md b/.context/effect/.changeset/pre/add-schema-datetime.md similarity index 100% rename from .context/effect/.changeset/add-schema-datetime.md rename to .context/effect/.changeset/pre/add-schema-datetime.md diff --git a/.context/effect/.changeset/pre/add-schema-error-module.md b/.context/effect/.changeset/pre/add-schema-error-module.md new file mode 100644 index 000000000..d28c2c29a --- /dev/null +++ b/.context/effect/.changeset/pre/add-schema-error-module.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Expose `SchemaError` as a public module and re-export `Schema.isSchemaError`. + +This gives consumers a stable import path and guard for schema failures without +depending on the internal schema implementation, while preserving the existing +`Schema.SchemaError` surface. diff --git a/.context/effect/.changeset/add-schema-option-from-optional-nullor.md b/.context/effect/.changeset/pre/add-schema-option-from-optional-nullor.md similarity index 100% rename from .context/effect/.changeset/add-schema-option-from-optional-nullor.md rename to .context/effect/.changeset/pre/add-schema-option-from-optional-nullor.md diff --git a/.context/effect/.changeset/add-schema-option-from-undefined-nullish.md b/.context/effect/.changeset/pre/add-schema-option-from-undefined-nullish.md similarity index 100% rename from .context/effect/.changeset/add-schema-option-from-undefined-nullish.md rename to .context/effect/.changeset/pre/add-schema-option-from-undefined-nullish.md diff --git a/.context/effect/.changeset/add-schema-string-encoding.md b/.context/effect/.changeset/pre/add-schema-string-encoding.md similarity index 100% rename from .context/effect/.changeset/add-schema-string-encoding.md rename to .context/effect/.changeset/pre/add-schema-string-encoding.md diff --git a/.context/effect/.changeset/pre/add-schema-tagged-union-discriminants.md b/.context/effect/.changeset/pre/add-schema-tagged-union-discriminants.md new file mode 100644 index 000000000..5930ed631 --- /dev/null +++ b/.context/effect/.changeset/pre/add-schema-tagged-union-discriminants.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Add a `discriminants` tuple to schemas augmented with `Schema.toTaggedUnion` and reject duplicate discriminant +property keys. diff --git a/.context/effect/.changeset/pre/add-semaphore-take-if-available.md b/.context/effect/.changeset/pre/add-semaphore-take-if-available.md new file mode 100644 index 000000000..8cb5182cc --- /dev/null +++ b/.context/effect/.changeset/pre/add-semaphore-take-if-available.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add `Semaphore.takeIfAvailable` for non-blocking manual permit acquisition. diff --git a/.context/effect/.changeset/add-sql-pglite.md b/.context/effect/.changeset/pre/add-sql-pglite.md similarity index 100% rename from .context/effect/.changeset/add-sql-pglite.md rename to .context/effect/.changeset/pre/add-sql-pglite.md diff --git a/.context/effect/.changeset/add-standard-jsdoc-rule.md b/.context/effect/.changeset/pre/add-standard-jsdoc-rule.md similarity index 100% rename from .context/effect/.changeset/add-standard-jsdoc-rule.md rename to .context/effect/.changeset/pre/add-standard-jsdoc-rule.md diff --git a/.context/effect/.changeset/add-stream-broadcastn.md b/.context/effect/.changeset/pre/add-stream-broadcastn.md similarity index 100% rename from .context/effect/.changeset/add-stream-broadcastn.md rename to .context/effect/.changeset/pre/add-stream-broadcastn.md diff --git a/.context/effect/.changeset/pre/add-tool-set-needs-approval.md b/.context/effect/.changeset/pre/add-tool-set-needs-approval.md new file mode 100644 index 000000000..959b43579 --- /dev/null +++ b/.context/effect/.changeset/pre/add-tool-set-needs-approval.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Tool.setNeedsApproval` for replacing the approval policy of an existing tool. diff --git a/.context/effect/.changeset/add-unstable-encoding-export.md b/.context/effect/.changeset/pre/add-unstable-encoding-export.md similarity index 100% rename from .context/effect/.changeset/add-unstable-encoding-export.md rename to .context/effect/.changeset/pre/add-unstable-encoding-export.md diff --git a/.context/effect/.changeset/pre/add-update-service-scoped.md b/.context/effect/.changeset/pre/add-update-service-scoped.md new file mode 100644 index 000000000..95f49061a --- /dev/null +++ b/.context/effect/.changeset/pre/add-update-service-scoped.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Effect.updateServiceScoped` for updating a context service until the current scope closes, with customizable reset behavior. diff --git a/.context/effect/.changeset/add-values-unprepared.md b/.context/effect/.changeset/pre/add-values-unprepared.md similarity index 100% rename from .context/effect/.changeset/add-values-unprepared.md rename to .context/effect/.changeset/pre/add-values-unprepared.md diff --git a/.context/effect/.changeset/afraid-cobras-like.md b/.context/effect/.changeset/pre/afraid-cobras-like.md similarity index 100% rename from .context/effect/.changeset/afraid-cobras-like.md rename to .context/effect/.changeset/pre/afraid-cobras-like.md diff --git a/.context/effect/.changeset/ai-openai-config-field-leak.md b/.context/effect/.changeset/pre/ai-openai-config-field-leak.md similarity index 100% rename from .context/effect/.changeset/ai-openai-config-field-leak.md rename to .context/effect/.changeset/pre/ai-openai-config-field-leak.md diff --git a/.context/effect/.changeset/ai-openai-file-nullable-fields.md b/.context/effect/.changeset/pre/ai-openai-file-nullable-fields.md similarity index 100% rename from .context/effect/.changeset/ai-openai-file-nullable-fields.md rename to .context/effect/.changeset/pre/ai-openai-file-nullable-fields.md diff --git a/.context/effect/.changeset/anthropic-4-6-structured-output.md b/.context/effect/.changeset/pre/anthropic-4-6-structured-output.md similarity index 100% rename from .context/effect/.changeset/anthropic-4-6-structured-output.md rename to .context/effect/.changeset/pre/anthropic-4-6-structured-output.md diff --git a/.context/effect/.changeset/pre/anthropic-open-model-enum.md b/.context/effect/.changeset/pre/anthropic-open-model-enum.md new file mode 100644 index 000000000..b5ab0393d --- /dev/null +++ b/.context/effect/.changeset/pre/anthropic-open-model-enum.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-anthropic": patch +--- + +Widen the Anthropic `Model` schema to accept both known model identifiers as well +as any string to allow for newer models diff --git a/.context/effect/.changeset/pre/anthropic-stale-max-output-tokens.md b/.context/effect/.changeset/pre/anthropic-stale-max-output-tokens.md new file mode 100644 index 000000000..f7e83ff6e --- /dev/null +++ b/.context/effect/.changeset/pre/anthropic-stale-max-output-tokens.md @@ -0,0 +1,7 @@ +--- +"@effect/ai-anthropic": patch +--- + +Correct the maximum output tokens for Claude Opus 4.6, 4.7, 4.8 and Sonnet 4.6. + +These models were grouped with the 4.5 family at 64000 output tokens, half of the 128000 the API actually allows, so requests defaulted to a cap far below the model's real limit. The 4.5 models keep 64000, which is correct for them. diff --git a/.context/effect/.changeset/pre/apply-httpapi-endpoint-client-transform.md b/.context/effect/.changeset/pre/apply-httpapi-endpoint-client-transform.md new file mode 100644 index 000000000..ce9d74cba --- /dev/null +++ b/.context/effect/.changeset/pre/apply-httpapi-endpoint-client-transform.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply `transformClient` when building an individual HttpApi endpoint client, preserving the supplied client's error and service channels. diff --git a/.context/effect/.changeset/asyncresult-exhaustive.md b/.context/effect/.changeset/pre/asyncresult-exhaustive.md similarity index 100% rename from .context/effect/.changeset/asyncresult-exhaustive.md rename to .context/effect/.changeset/pre/asyncresult-exhaustive.md diff --git a/.context/effect/.changeset/atom-stream-error-type.md b/.context/effect/.changeset/pre/atom-stream-error-type.md similarity index 100% rename from .context/effect/.changeset/atom-stream-error-type.md rename to .context/effect/.changeset/pre/atom-stream-error-type.md diff --git a/.context/effect/.changeset/pre/batch-persistence-expiration-cleanup.md b/.context/effect/.changeset/pre/batch-persistence-expiration-cleanup.md new file mode 100644 index 000000000..3409edfb6 --- /dev/null +++ b/.context/effect/.changeset/pre/batch-persistence-expiration-cleanup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Run shared-table SQL persistence expiration cleanup in indexed, bounded background batches. diff --git a/.context/effect/.changeset/pre/beige-goats-sin.md b/.context/effect/.changeset/pre/beige-goats-sin.md new file mode 100644 index 000000000..22cef1f7f --- /dev/null +++ b/.context/effect/.changeset/pre/beige-goats-sin.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Fix dynamic tools defined with a raw JSON schema sending empty parameter schema to OpenRouter diff --git a/.context/effect/.changeset/beige-paths-sort.md b/.context/effect/.changeset/pre/beige-paths-sort.md similarity index 100% rename from .context/effect/.changeset/beige-paths-sort.md rename to .context/effect/.changeset/pre/beige-paths-sort.md diff --git a/.context/effect/.changeset/better-apples-nail.md b/.context/effect/.changeset/pre/better-apples-nail.md similarity index 100% rename from .context/effect/.changeset/better-apples-nail.md rename to .context/effect/.changeset/pre/better-apples-nail.md diff --git a/.context/effect/.changeset/better-rocks-arrive.md b/.context/effect/.changeset/pre/better-rocks-arrive.md similarity index 100% rename from .context/effect/.changeset/better-rocks-arrive.md rename to .context/effect/.changeset/pre/better-rocks-arrive.md diff --git a/.context/effect/.changeset/better-shrimps-follow.md b/.context/effect/.changeset/pre/better-shrimps-follow.md similarity index 100% rename from .context/effect/.changeset/better-shrimps-follow.md rename to .context/effect/.changeset/pre/better-shrimps-follow.md diff --git a/.context/effect/.changeset/pre/big-masks-care.md b/.context/effect/.changeset/pre/big-masks-care.md new file mode 100644 index 000000000..8b3a17254 --- /dev/null +++ b/.context/effect/.changeset/pre/big-masks-care.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Support `.mdx` files diff --git a/.context/effect/.changeset/big-pans-look.md b/.context/effect/.changeset/pre/big-pans-look.md similarity index 100% rename from .context/effect/.changeset/big-pans-look.md rename to .context/effect/.changeset/pre/big-pans-look.md diff --git a/.context/effect/.changeset/blue-dingos-greet.md b/.context/effect/.changeset/pre/blue-dingos-greet.md similarity index 100% rename from .context/effect/.changeset/blue-dingos-greet.md rename to .context/effect/.changeset/pre/blue-dingos-greet.md diff --git a/.context/effect/.changeset/blue-ligers-cheat.md b/.context/effect/.changeset/pre/blue-ligers-cheat.md similarity index 100% rename from .context/effect/.changeset/blue-ligers-cheat.md rename to .context/effect/.changeset/pre/blue-ligers-cheat.md diff --git a/.context/effect/.changeset/blue-onions-smile.md b/.context/effect/.changeset/pre/blue-onions-smile.md similarity index 100% rename from .context/effect/.changeset/blue-onions-smile.md rename to .context/effect/.changeset/pre/blue-onions-smile.md diff --git a/.context/effect/.changeset/pre/blue-pigs-push.md b/.context/effect/.changeset/pre/blue-pigs-push.md new file mode 100644 index 000000000..7cb74e13f --- /dev/null +++ b/.context/effect/.changeset/pre/blue-pigs-push.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Channel.mergeAll` to propagate outer failures promptly and interrupt active inner channels. diff --git a/.context/effect/.changeset/blue-ravens-type.md b/.context/effect/.changeset/pre/blue-ravens-type.md similarity index 100% rename from .context/effect/.changeset/blue-ravens-type.md rename to .context/effect/.changeset/pre/blue-ravens-type.md diff --git a/.context/effect/.changeset/blue-trams-kiss.md b/.context/effect/.changeset/pre/blue-trams-kiss.md similarity index 100% rename from .context/effect/.changeset/blue-trams-kiss.md rename to .context/effect/.changeset/pre/blue-trams-kiss.md diff --git a/.context/effect/.changeset/bold-chairs-yawn.md b/.context/effect/.changeset/pre/bold-chairs-yawn.md similarity index 100% rename from .context/effect/.changeset/bold-chairs-yawn.md rename to .context/effect/.changeset/pre/bold-chairs-yawn.md diff --git a/.context/effect/.changeset/bold-planets-shout.md b/.context/effect/.changeset/pre/bold-planets-shout.md similarity index 100% rename from .context/effect/.changeset/bold-planets-shout.md rename to .context/effect/.changeset/pre/bold-planets-shout.md diff --git a/.context/effect/.changeset/pre/brave-keys-commit.md b/.context/effect/.changeset/pre/brave-keys-commit.md new file mode 100644 index 000000000..f9358594f --- /dev/null +++ b/.context/effect/.changeset/pre/brave-keys-commit.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix IndexedDB-backed key-value writes to wait for transaction commit before reporting success. diff --git a/.context/effect/.changeset/pre/brave-rings-update.md b/.context/effect/.changeset/pre/brave-rings-update.md new file mode 100644 index 000000000..21931bcb5 --- /dev/null +++ b/.context/effect/.changeset/pre/brave-rings-update.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update existing `HashRing` nodes when adding a value with the same primary key. diff --git a/.context/effect/.changeset/breezy-meals-see.md b/.context/effect/.changeset/pre/breezy-meals-see.md similarity index 100% rename from .context/effect/.changeset/breezy-meals-see.md rename to .context/effect/.changeset/pre/breezy-meals-see.md diff --git a/.context/effect/.changeset/bright-bugs-bow.md b/.context/effect/.changeset/pre/bright-bugs-bow.md similarity index 100% rename from .context/effect/.changeset/bright-bugs-bow.md rename to .context/effect/.changeset/pre/bright-bugs-bow.md diff --git a/.context/effect/.changeset/bright-canyons-clean.md b/.context/effect/.changeset/pre/bright-canyons-clean.md similarity index 100% rename from .context/effect/.changeset/bright-canyons-clean.md rename to .context/effect/.changeset/pre/bright-canyons-clean.md diff --git a/.context/effect/.changeset/pre/bright-clocks-count.md b/.context/effect/.changeset/pre/bright-clocks-count.md new file mode 100644 index 000000000..8f3df8ee0 --- /dev/null +++ b/.context/effect/.changeset/pre/bright-clocks-count.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `DateTime.toEpochSeconds` and `DateTime.fromEpochSeconds` for converting date-time values to and from Unix epoch seconds. diff --git a/.context/effect/.changeset/bright-dogs-fail.md b/.context/effect/.changeset/pre/bright-dogs-fail.md similarity index 100% rename from .context/effect/.changeset/bright-dogs-fail.md rename to .context/effect/.changeset/pre/bright-dogs-fail.md diff --git a/.context/effect/.changeset/bright-flags-stand.md b/.context/effect/.changeset/pre/bright-flags-stand.md similarity index 100% rename from .context/effect/.changeset/bright-flags-stand.md rename to .context/effect/.changeset/pre/bright-flags-stand.md diff --git a/.context/effect/.changeset/pre/bright-journals-commit.md b/.context/effect/.changeset/pre/bright-journals-commit.md new file mode 100644 index 000000000..5c312a18f --- /dev/null +++ b/.context/effect/.changeset/pre/bright-journals-commit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Commit SQL event journal entries only after their write callback succeeds. diff --git a/.context/effect/.changeset/bright-laws-teach.md b/.context/effect/.changeset/pre/bright-laws-teach.md similarity index 100% rename from .context/effect/.changeset/bright-laws-teach.md rename to .context/effect/.changeset/pre/bright-laws-teach.md diff --git a/.context/effect/.changeset/bright-lemons-dance.md b/.context/effect/.changeset/pre/bright-lemons-dance.md similarity index 100% rename from .context/effect/.changeset/bright-lemons-dance.md rename to .context/effect/.changeset/pre/bright-lemons-dance.md diff --git a/.context/effect/.changeset/bright-planes-smash.md b/.context/effect/.changeset/pre/bright-planes-smash.md similarity index 100% rename from .context/effect/.changeset/bright-planes-smash.md rename to .context/effect/.changeset/pre/bright-planes-smash.md diff --git a/.context/effect/.changeset/bright-rats-attend.md b/.context/effect/.changeset/pre/bright-rats-attend.md similarity index 100% rename from .context/effect/.changeset/bright-rats-attend.md rename to .context/effect/.changeset/pre/bright-rats-attend.md diff --git a/.context/effect/.changeset/pre/bright-tags-recognize.md b/.context/effect/.changeset/pre/bright-tags-recognize.md new file mode 100644 index 000000000..edead0f68 --- /dev/null +++ b/.context/effect/.changeset/pre/bright-tags-recognize.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Recognize tagged Config and RPC errors across duplicated `effect` package copies. diff --git a/.context/effect/.changeset/bright-toes-rush.md b/.context/effect/.changeset/pre/bright-toes-rush.md similarity index 100% rename from .context/effect/.changeset/bright-toes-rush.md rename to .context/effect/.changeset/pre/bright-toes-rush.md diff --git a/.context/effect/.changeset/pre/brown-glasses-thank.md b/.context/effect/.changeset/pre/brown-glasses-thank.md new file mode 100644 index 000000000..0445b02de --- /dev/null +++ b/.context/effect/.changeset/pre/brown-glasses-thank.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Handle BigInt values safely and consistently across JSON diagnostics and logger formats. diff --git a/.context/effect/.changeset/pre/brown-peas-enter.md b/.context/effect/.changeset/pre/brown-peas-enter.md new file mode 100644 index 000000000..2779fa9ef --- /dev/null +++ b/.context/effect/.changeset/pre/brown-peas-enter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP tool handler defects now return a stable internal error without exposing defect details. diff --git a/.context/effect/.changeset/bumpy-boxes-teach.md b/.context/effect/.changeset/pre/bumpy-boxes-teach.md similarity index 100% rename from .context/effect/.changeset/bumpy-boxes-teach.md rename to .context/effect/.changeset/pre/bumpy-boxes-teach.md diff --git a/.context/effect/.changeset/busy-lions-sneeze.md b/.context/effect/.changeset/pre/busy-lions-sneeze.md similarity index 100% rename from .context/effect/.changeset/busy-lions-sneeze.md rename to .context/effect/.changeset/pre/busy-lions-sneeze.md diff --git a/.context/effect/.changeset/busy-maps-attend.md b/.context/effect/.changeset/pre/busy-maps-attend.md similarity index 100% rename from .context/effect/.changeset/busy-maps-attend.md rename to .context/effect/.changeset/pre/busy-maps-attend.md diff --git a/.context/effect/.changeset/pre/calm-bash-completions.md b/.context/effect/.changeset/pre/calm-bash-completions.md new file mode 100644 index 000000000..ca77256ce --- /dev/null +++ b/.context/effect/.changeset/pre/calm-bash-completions.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Select Bash completions for the active positional argument. diff --git a/.context/effect/.changeset/calm-buckets-own.md b/.context/effect/.changeset/pre/calm-buckets-own.md similarity index 100% rename from .context/effect/.changeset/calm-buckets-own.md rename to .context/effect/.changeset/pre/calm-buckets-own.md diff --git a/.context/effect/.changeset/pre/calm-buses-smile.md b/.context/effect/.changeset/pre/calm-buses-smile.md new file mode 100644 index 000000000..e7fe600ce --- /dev/null +++ b/.context/effect/.changeset/pre/calm-buses-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix IndexedDB query range, ordering, streaming, and transaction semantics. diff --git a/.context/effect/.changeset/calm-carrots-march.md b/.context/effect/.changeset/pre/calm-carrots-march.md similarity index 100% rename from .context/effect/.changeset/calm-carrots-march.md rename to .context/effect/.changeset/pre/calm-carrots-march.md diff --git a/.context/effect/.changeset/calm-cars-rest.md b/.context/effect/.changeset/pre/calm-cars-rest.md similarity index 100% rename from .context/effect/.changeset/calm-cars-rest.md rename to .context/effect/.changeset/pre/calm-cars-rest.md diff --git a/.context/effect/.changeset/pre/calm-coins-smile.md b/.context/effect/.changeset/pre/calm-coins-smile.md new file mode 100644 index 000000000..973771855 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-coins-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Generate even and odd safe integers in Crypto random APIs. diff --git a/.context/effect/.changeset/pre/calm-dates-view.md b/.context/effect/.changeset/pre/calm-dates-view.md new file mode 100644 index 000000000..b9e963edb --- /dev/null +++ b/.context/effect/.changeset/pre/calm-dates-view.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Equal.equals` and `Hash.hash` to handle invalid dates and `DataView` values without throwing. diff --git a/.context/effect/.changeset/pre/calm-dragons-command.md b/.context/effect/.changeset/pre/calm-dragons-command.md new file mode 100644 index 000000000..d98e1edd6 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-dragons-command.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the runtime tag spelling for `CliError.UnknownSubcommand`. diff --git a/.context/effect/.changeset/pre/calm-heads-close.md b/.context/effect/.changeset/pre/calm-heads-close.md new file mode 100644 index 000000000..c717945ad --- /dev/null +++ b/.context/effect/.changeset/pre/calm-heads-close.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Ensure aborted `HEAD` responses do not block `NodeHttpServer` disposal. diff --git a/.context/effect/.changeset/pre/calm-hounds-smile.md b/.context/effect/.changeset/pre/calm-hounds-smile.md new file mode 100644 index 000000000..8f7a1640a --- /dev/null +++ b/.context/effect/.changeset/pre/calm-hounds-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Parameterize ClickHouse query IDs when cancelling queries and inserts. diff --git a/.context/effect/.changeset/pre/calm-keys-repeat.md b/.context/effect/.changeset/pre/calm-keys-repeat.md new file mode 100644 index 000000000..82f669b0e --- /dev/null +++ b/.context/effect/.changeset/pre/calm-keys-repeat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix SQL-backed Persistence `getMany` to preserve duplicate key positions. diff --git a/.context/effect/.changeset/pre/calm-masks-count.md b/.context/effect/.changeset/pre/calm-masks-count.md new file mode 100644 index 000000000..b70fc9ae8 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-masks-count.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Schema.DateFromMillis` and `SchemaTransformation.dateFromMillis` for decoding millisecond timestamps into `Date` values. diff --git a/.context/effect/.changeset/pre/calm-pandas-retry.md b/.context/effect/.changeset/pre/calm-pandas-retry.md new file mode 100644 index 000000000..b2372c6bd --- /dev/null +++ b/.context/effect/.changeset/pre/calm-pandas-retry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add bounded 429 retries and custom response header names to `HttpClient.withRateLimiter`. diff --git a/.context/effect/.changeset/calm-panthers-nail.md b/.context/effect/.changeset/pre/calm-panthers-nail.md similarity index 100% rename from .context/effect/.changeset/calm-panthers-nail.md rename to .context/effect/.changeset/pre/calm-panthers-nail.md diff --git a/.context/effect/.changeset/pre/calm-pears-smile.md b/.context/effect/.changeset/pre/calm-pears-smile.md new file mode 100644 index 000000000..a83413eea --- /dev/null +++ b/.context/effect/.changeset/pre/calm-pears-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Exclude disabled choices from multi-select prompt selection and submission. diff --git a/.context/effect/.changeset/pre/calm-queues-await.md b/.context/effect/.changeset/pre/calm-queues-await.md new file mode 100644 index 000000000..18ac80170 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-queues-await.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Queue.await` failing with `Cause.Done` when registered before the queue ends. diff --git a/.context/effect/.changeset/pre/calm-ravens-reflect.md b/.context/effect/.changeset/pre/calm-ravens-reflect.md new file mode 100644 index 000000000..ba31e47c2 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-ravens-reflect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `__proto__` group and endpoint identifiers in HTTP APIs, generated clients, and URL builders. diff --git a/.context/effect/.changeset/pre/calm-redis-clear.md b/.context/effect/.changeset/pre/calm-redis-clear.md new file mode 100644 index 000000000..a707eadd9 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-redis-clear.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure clearing an empty Redis-backed persistence store succeeds. diff --git a/.context/effect/.changeset/pre/calm-results-align.md b/.context/effect/.changeset/pre/calm-results-align.md new file mode 100644 index 000000000..896040c70 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-results-align.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep ordered SQL resolver results aligned when batched request encoding fails. diff --git a/.context/effect/.changeset/pre/calm-schemas-encode.md b/.context/effect/.changeset/pre/calm-schemas-encode.md new file mode 100644 index 000000000..6bec17848 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-schemas-encode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the encoded output type of `TestSchema.Encoding.encodeUnknownEffect`. diff --git a/.context/effect/.changeset/calm-seas-smile.md b/.context/effect/.changeset/pre/calm-seas-smile.md similarity index 100% rename from .context/effect/.changeset/calm-seas-smile.md rename to .context/effect/.changeset/pre/calm-seas-smile.md diff --git a/.context/effect/.changeset/pre/calm-servers-share.md b/.context/effect/.changeset/pre/calm-servers-share.md new file mode 100644 index 000000000..504a98ce5 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-servers-share.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update `McpServer.layerHttp` to return `405` for unsupported HTTP methods, reject unsupported `MCP-Protocol-Version` headers with `400`, and return an empty `202` for accepted notifications and responses. diff --git a/.context/effect/.changeset/pre/calm-services-rest.md b/.context/effect/.changeset/pre/calm-services-rest.md new file mode 100644 index 000000000..400dade63 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-services-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Stop capturing definition-location stack frames in `Context.Service`. diff --git a/.context/effect/.changeset/calm-squids-hug.md b/.context/effect/.changeset/pre/calm-squids-hug.md similarity index 100% rename from .context/effect/.changeset/calm-squids-hug.md rename to .context/effect/.changeset/pre/calm-squids-hug.md diff --git a/.context/effect/.changeset/pre/calm-tools-parse.md b/.context/effect/.changeset/pre/calm-tools-parse.md new file mode 100644 index 000000000..779bde415 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-tools-parse.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add lightweight INI, YAML, and TOML parsers under `effect/unstable/encoding` and remove their runtime dependencies. diff --git a/.context/effect/.changeset/pre/calm-tools-remember.md b/.context/effect/.changeset/pre/calm-tools-remember.md new file mode 100644 index 000000000..3fdf6410e --- /dev/null +++ b/.context/effect/.changeset/pre/calm-tools-remember.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve equals signs in inline CLI option values after the first separator. diff --git a/.context/effect/.changeset/calm-tracers-sample.md b/.context/effect/.changeset/pre/calm-tracers-sample.md similarity index 100% rename from .context/effect/.changeset/calm-tracers-sample.md rename to .context/effect/.changeset/pre/calm-tracers-sample.md diff --git a/.context/effect/.changeset/pre/calm-tuples-align.md b/.context/effect/.changeset/pre/calm-tuples-align.md new file mode 100644 index 000000000..b62d5bdc4 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-tuples-align.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix arbitrary generation for tuples with multiple optional elements. diff --git a/.context/effect/.changeset/pre/calm-tuples-pick.md b/.context/effect/.changeset/pre/calm-tuples-pick.md new file mode 100644 index 000000000..0422f5814 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-tuples-pick.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Tuple.pick` return types to preserve the requested index order and duplicate indices. diff --git a/.context/effect/.changeset/pre/calm-wolves-reduce.md b/.context/effect/.changeset/pre/calm-wolves-reduce.md new file mode 100644 index 000000000..1b2556456 --- /dev/null +++ b/.context/effect/.changeset/pre/calm-wolves-reduce.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Sink.reduceWhileArray` applying its reducer more than once per input array. diff --git a/.context/effect/.changeset/pre/cancel-tedious-requests.md b/.context/effect/.changeset/pre/cancel-tedious-requests.md new file mode 100644 index 000000000..9b9ce459e --- /dev/null +++ b/.context/effect/.changeset/pre/cancel-tedious-requests.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Cancel in-flight Tedious requests when their Effects are interrupted. diff --git a/.context/effect/.changeset/pre/canonical-number-schemas.md b/.context/effect/.changeset/pre/canonical-number-schemas.md new file mode 100644 index 000000000..96fceebf4 --- /dev/null +++ b/.context/effect/.changeset/pre/canonical-number-schemas.md @@ -0,0 +1,12 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +"@effect/openapi-generator": patch +--- + +Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + +Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. diff --git a/.context/effect/.changeset/pre/cap-rpc-streaming-buffers.md b/.context/effect/.changeset/pre/cap-rpc-streaming-buffers.md new file mode 100644 index 000000000..4f67ff43b --- /dev/null +++ b/.context/effect/.changeset/pre/cap-rpc-streaming-buffers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Cap incomplete RPC frames buffered by the NDJSON and MessagePack streaming decoders, and close socket transports when the limit is exceeded. diff --git a/.context/effect/.changeset/chatty-poets-type.md b/.context/effect/.changeset/pre/chatty-poets-type.md similarity index 100% rename from .context/effect/.changeset/chatty-poets-type.md rename to .context/effect/.changeset/pre/chatty-poets-type.md diff --git a/.context/effect/.changeset/chilled-mice-wash.md b/.context/effect/.changeset/pre/chilled-mice-wash.md similarity index 100% rename from .context/effect/.changeset/chilled-mice-wash.md rename to .context/effect/.changeset/pre/chilled-mice-wash.md diff --git a/.context/effect/.changeset/chilly-pumas-rule.md b/.context/effect/.changeset/pre/chilly-pumas-rule.md similarity index 100% rename from .context/effect/.changeset/chilly-pumas-rule.md rename to .context/effect/.changeset/pre/chilly-pumas-rule.md diff --git a/.context/effect/.changeset/chubby-buckets-feel.md b/.context/effect/.changeset/pre/chubby-buckets-feel.md similarity index 100% rename from .context/effect/.changeset/chubby-buckets-feel.md rename to .context/effect/.changeset/pre/chubby-buckets-feel.md diff --git a/.context/effect/.changeset/chubby-parents-flow.md b/.context/effect/.changeset/pre/chubby-parents-flow.md similarity index 100% rename from .context/effect/.changeset/chubby-parents-flow.md rename to .context/effect/.changeset/pre/chubby-parents-flow.md diff --git a/.context/effect/.changeset/chubby-planets-fall.md b/.context/effect/.changeset/pre/chubby-planets-fall.md similarity index 100% rename from .context/effect/.changeset/chubby-planets-fall.md rename to .context/effect/.changeset/pre/chubby-planets-fall.md diff --git a/.context/effect/.changeset/clean-balloons-tan.md b/.context/effect/.changeset/pre/clean-balloons-tan.md similarity index 100% rename from .context/effect/.changeset/clean-balloons-tan.md rename to .context/effect/.changeset/pre/clean-balloons-tan.md diff --git a/.context/effect/.changeset/clean-bulldogs-care.md b/.context/effect/.changeset/pre/clean-bulldogs-care.md similarity index 100% rename from .context/effect/.changeset/clean-bulldogs-care.md rename to .context/effect/.changeset/pre/clean-bulldogs-care.md diff --git a/.context/effect/.changeset/pre/clean-cats-document.md b/.context/effect/.changeset/pre/clean-cats-document.md new file mode 100644 index 000000000..b2d26821d --- /dev/null +++ b/.context/effect/.changeset/pre/clean-cats-document.md @@ -0,0 +1,5 @@ +--- +"@effect/docgen": major +--- + +Migrate `@effect/docgen` into the Effect monorepo and update it to Effect 4 while retaining existing behavior. diff --git a/.context/effect/.changeset/clean-dryers-sneeze.md b/.context/effect/.changeset/pre/clean-dryers-sneeze.md similarity index 100% rename from .context/effect/.changeset/clean-dryers-sneeze.md rename to .context/effect/.changeset/pre/clean-dryers-sneeze.md diff --git a/.context/effect/.changeset/pre/clean-formatters-agree.md b/.context/effect/.changeset/pre/clean-formatters-agree.md new file mode 100644 index 000000000..2fea77679 --- /dev/null +++ b/.context/effect/.changeset/pre/clean-formatters-agree.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prioritize redacted representations in formatters and normalize text logger levels to uppercase. diff --git a/.context/effect/.changeset/clean-geese-work.md b/.context/effect/.changeset/pre/clean-geese-work.md similarity index 100% rename from .context/effect/.changeset/clean-geese-work.md rename to .context/effect/.changeset/pre/clean-geese-work.md diff --git a/.context/effect/.changeset/clean-goats-wave.md b/.context/effect/.changeset/pre/clean-goats-wave.md similarity index 100% rename from .context/effect/.changeset/clean-goats-wave.md rename to .context/effect/.changeset/pre/clean-goats-wave.md diff --git a/.context/effect/.changeset/pre/clean-lions-cancel.md b/.context/effect/.changeset/pre/clean-lions-cancel.md new file mode 100644 index 000000000..f725e3d19 --- /dev/null +++ b/.context/effect/.changeset/pre/clean-lions-cancel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +RPC servers now suppress responses after a client cancels an in-flight request. diff --git a/.context/effect/.changeset/clean-needles-shake.md b/.context/effect/.changeset/pre/clean-needles-shake.md similarity index 100% rename from .context/effect/.changeset/clean-needles-shake.md rename to .context/effect/.changeset/pre/clean-needles-shake.md diff --git a/.context/effect/.changeset/clean-tires-guess.md b/.context/effect/.changeset/pre/clean-tires-guess.md similarity index 100% rename from .context/effect/.changeset/clean-tires-guess.md rename to .context/effect/.changeset/pre/clean-tires-guess.md diff --git a/.context/effect/.changeset/pre/clear-hairs-pump.md b/.context/effect/.changeset/pre/clear-hairs-pump.md new file mode 100644 index 000000000..ac42927cd --- /dev/null +++ b/.context/effect/.changeset/pre/clear-hairs-pump.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix activity retry policy diff --git a/.context/effect/.changeset/clear-spies-boil.md b/.context/effect/.changeset/pre/clear-spies-boil.md similarity index 100% rename from .context/effect/.changeset/clear-spies-boil.md rename to .context/effect/.changeset/pre/clear-spies-boil.md diff --git a/.context/effect/.changeset/pre/clever-maps-care.md b/.context/effect/.changeset/pre/clever-maps-care.md new file mode 100644 index 000000000..51de31746 --- /dev/null +++ b/.context/effect/.changeset/pre/clever-maps-care.md @@ -0,0 +1,8 @@ +--- +"@effect/platform-deno": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"effect": patch +--- + +add platform literal to HttpPlatform diff --git a/.context/effect/.changeset/pre/cli-config-built-ins.md b/.context/effect/.changeset/pre/cli-config-built-ins.md new file mode 100644 index 000000000..6e0dbcd40 --- /dev/null +++ b/.context/effect/.changeset/pre/cli-config-built-ins.md @@ -0,0 +1,24 @@ +--- +"effect": patch +--- + +Add a scoped `CliConfig` service for customizing the built-in global flags used by CLI command runners. + +For example, provide an explicit list that omits `GlobalFlag.LogLevel` to remove the built-in `--log-level` flag: + +```ts +import { Effect } from "effect" +import { CliConfig, Command, GlobalFlag } from "effect/unstable/cli" + +const program = Command.run(command, { version: "1.0.0" }).pipe( + Effect.provide( + CliConfig.layer({ + builtIns: [ + GlobalFlag.Help, + GlobalFlag.Version, + GlobalFlag.Completions + ] + }) + ) +) +``` diff --git a/.context/effect/.changeset/cli-help-choices.md b/.context/effect/.changeset/pre/cli-help-choices.md similarity index 100% rename from .context/effect/.changeset/cli-help-choices.md rename to .context/effect/.changeset/pre/cli-help-choices.md diff --git a/.context/effect/.changeset/pre/cli-wizard-mode.md b/.context/effect/.changeset/pre/cli-wizard-mode.md new file mode 100644 index 000000000..0b7c9f2d6 --- /dev/null +++ b/.context/effect/.changeset/pre/cli-wizard-mode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reintroduce interactive CLI wizard mode through the `--wizard` flag and `Command.wizard`. diff --git a/.context/effect/.changeset/pre/close-failed-resource-map-scopes.md b/.context/effect/.changeset/pre/close-failed-resource-map-scopes.md new file mode 100644 index 000000000..2242910a8 --- /dev/null +++ b/.context/effect/.changeset/pre/close-failed-resource-map-scopes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Close `ResourceMap` acquisition scopes when a lookup fails. diff --git a/.context/effect/.changeset/cold-knives-lie.md b/.context/effect/.changeset/pre/cold-knives-lie.md similarity index 100% rename from .context/effect/.changeset/cold-knives-lie.md rename to .context/effect/.changeset/pre/cold-knives-lie.md diff --git a/.context/effect/.changeset/cold-rooms-show.md b/.context/effect/.changeset/pre/cold-rooms-show.md similarity index 100% rename from .context/effect/.changeset/cold-rooms-show.md rename to .context/effect/.changeset/pre/cold-rooms-show.md diff --git a/.context/effect/.changeset/cold-sloths-wave.md b/.context/effect/.changeset/pre/cold-sloths-wave.md similarity index 100% rename from .context/effect/.changeset/cold-sloths-wave.md rename to .context/effect/.changeset/pre/cold-sloths-wave.md diff --git a/.context/effect/.changeset/pre/common-mammals-tickle.md b/.context/effect/.changeset/pre/common-mammals-tickle.md new file mode 100644 index 000000000..510898292 --- /dev/null +++ b/.context/effect/.changeset/pre/common-mammals-tickle.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Fix generation order for recursive schemas referenced by earlier recursive definitions, closes #6357. diff --git a/.context/effect/.changeset/compact-json-schema-enum.md b/.context/effect/.changeset/pre/compact-json-schema-enum.md similarity index 100% rename from .context/effect/.changeset/compact-json-schema-enum.md rename to .context/effect/.changeset/pre/compact-json-schema-enum.md diff --git a/.context/effect/.changeset/pre/config-provider-option-lookup.md b/.context/effect/.changeset/pre/config-provider-option-lookup.md new file mode 100644 index 000000000..a671ebb39 --- /dev/null +++ b/.context/effect/.changeset/pre/config-provider-option-lookup.md @@ -0,0 +1,15 @@ +--- +"effect": patch +--- + +Refine the `ConfigProvider` interface so lookup absence uses `undefined` and +path transformation is provider behavior. + +`ConfigProvider.load` and the lookup function accepted by +`ConfigProvider.make` now return `Node | undefined`. Use `undefined` when a path +does not exist and return the `Node` directly when it does. + +`ConfigProvider` now exposes `mapInput` as a capability. The exported +`ConfigProvider.mapInput` combinator delegates to it, preserving transformation +order and composition through `orElse` without requiring provider +representation state. diff --git a/.context/effect/.changeset/config-withdefault-eager.md b/.context/effect/.changeset/pre/config-withdefault-eager.md similarity index 100% rename from .context/effect/.changeset/config-withdefault-eager.md rename to .context/effect/.changeset/pre/config-withdefault-eager.md diff --git a/.context/effect/.changeset/pre/configure-cluster-rpc-buffer-limits.md b/.context/effect/.changeset/pre/configure-cluster-rpc-buffer-limits.md new file mode 100644 index 000000000..76c3172e7 --- /dev/null +++ b/.context/effect/.changeset/pre/configure-cluster-rpc-buffer-limits.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +--- + +Allow configuring cluster RPC serialization buffer limits. diff --git a/.context/effect/.changeset/consolidate-encoding.md b/.context/effect/.changeset/pre/consolidate-encoding.md similarity index 100% rename from .context/effect/.changeset/consolidate-encoding.md rename to .context/effect/.changeset/pre/consolidate-encoding.md diff --git a/.context/effect/.changeset/consolidate-sql-error.md b/.context/effect/.changeset/pre/consolidate-sql-error.md similarity index 100% rename from .context/effect/.changeset/consolidate-sql-error.md rename to .context/effect/.changeset/pre/consolidate-sql-error.md diff --git a/.context/effect/.changeset/pre/cozy-geese-remain.md b/.context/effect/.changeset/pre/cozy-geese-remain.md new file mode 100644 index 000000000..52e90cd08 --- /dev/null +++ b/.context/effect/.changeset/pre/cozy-geese-remain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix scoped reentrant lock finalizers releasing under the wrong fiber owner. diff --git a/.context/effect/.changeset/crisp-seas-warn.md b/.context/effect/.changeset/pre/crisp-seas-warn.md similarity index 100% rename from .context/effect/.changeset/crisp-seas-warn.md rename to .context/effect/.changeset/pre/crisp-seas-warn.md diff --git a/.context/effect/.changeset/pre/cron-locale-independent-aliases.md b/.context/effect/.changeset/pre/cron-locale-independent-aliases.md new file mode 100644 index 000000000..9b84a1ced --- /dev/null +++ b/.context/effect/.changeset/pre/cron-locale-independent-aliases.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize cron month and weekday aliases independently of the host locale. diff --git a/.context/effect/.changeset/pre/cron-single-value-step.md b/.context/effect/.changeset/pre/cron-single-value-step.md new file mode 100644 index 000000000..02a9fff68 --- /dev/null +++ b/.context/effect/.changeset/pre/cron-single-value-step.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow cron fields like `5/15` to expand from the starting value through the field maximum. diff --git a/.context/effect/.changeset/pre/cron-testclock-infinity.md b/.context/effect/.changeset/pre/cron-testclock-infinity.md new file mode 100644 index 000000000..3aea00e89 --- /dev/null +++ b/.context/effect/.changeset/pre/cron-testclock-infinity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.cron` when the test clock is adjusted to infinity. diff --git a/.context/effect/.changeset/cuddly-rooms-bet.md b/.context/effect/.changeset/pre/cuddly-rooms-bet.md similarity index 100% rename from .context/effect/.changeset/cuddly-rooms-bet.md rename to .context/effect/.changeset/pre/cuddly-rooms-bet.md diff --git a/.context/effect/.changeset/pre/curly-files-range.md b/.context/effect/.changeset/pre/curly-files-range.md new file mode 100644 index 000000000..aa2a20ade --- /dev/null +++ b/.context/effect/.changeset/pre/curly-files-range.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Apply byte range and chunk size options to default Web file responses. diff --git a/.context/effect/.changeset/curly-poems-talk.md b/.context/effect/.changeset/pre/curly-poems-talk.md similarity index 100% rename from .context/effect/.changeset/curly-poems-talk.md rename to .context/effect/.changeset/pre/curly-poems-talk.md diff --git a/.context/effect/.changeset/pre/curly-ravens-decode.md b/.context/effect/.changeset/pre/curly-ravens-decode.md new file mode 100644 index 000000000..f8af5ea82 --- /dev/null +++ b/.context/effect/.changeset/pre/curly-ravens-decode.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Decode percent-encoded OTLP environment header values. diff --git a/.context/effect/.changeset/curly-spies-relax.md b/.context/effect/.changeset/pre/curly-spies-relax.md similarity index 100% rename from .context/effect/.changeset/curly-spies-relax.md rename to .context/effect/.changeset/pre/curly-spies-relax.md diff --git a/.context/effect/.changeset/pre/curly-streams-stop.md b/.context/effect/.changeset/pre/curly-streams-stop.md new file mode 100644 index 000000000..e2a08a9df --- /dev/null +++ b/.context/effect/.changeset/pre/curly-streams-stop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Interrupt in-flight stream pulls when closing an async iterator. diff --git a/.context/effect/.changeset/curvy-apples-float.md b/.context/effect/.changeset/pre/curvy-apples-float.md similarity index 100% rename from .context/effect/.changeset/curvy-apples-float.md rename to .context/effect/.changeset/pre/curvy-apples-float.md diff --git a/.context/effect/.changeset/curvy-birds-float.md b/.context/effect/.changeset/pre/curvy-birds-float.md similarity index 100% rename from .context/effect/.changeset/curvy-birds-float.md rename to .context/effect/.changeset/pre/curvy-birds-float.md diff --git a/.context/effect/.changeset/pre/curvy-melons-stare.md b/.context/effect/.changeset/pre/curvy-melons-stare.md new file mode 100644 index 000000000..c9dd27576 --- /dev/null +++ b/.context/effect/.changeset/pre/curvy-melons-stare.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix MCP sampling metadata optionality and validate it as an object. diff --git a/.context/effect/.changeset/custom-http-security-openapi-generator.md b/.context/effect/.changeset/pre/custom-http-security-openapi-generator.md similarity index 100% rename from .context/effect/.changeset/custom-http-security-openapi-generator.md rename to .context/effect/.changeset/pre/custom-http-security-openapi-generator.md diff --git a/.context/effect/.changeset/cute-heads-thank.md b/.context/effect/.changeset/pre/cute-heads-thank.md similarity index 100% rename from .context/effect/.changeset/cute-heads-thank.md rename to .context/effect/.changeset/pre/cute-heads-thank.md diff --git a/.context/effect/.changeset/cyan-loops-grow.md b/.context/effect/.changeset/pre/cyan-loops-grow.md similarity index 100% rename from .context/effect/.changeset/cyan-loops-grow.md rename to .context/effect/.changeset/pre/cyan-loops-grow.md diff --git a/.context/effect/.changeset/cyan-radios-switch.md b/.context/effect/.changeset/pre/cyan-radios-switch.md similarity index 100% rename from .context/effect/.changeset/cyan-radios-switch.md rename to .context/effect/.changeset/pre/cyan-radios-switch.md diff --git a/.context/effect/.changeset/pre/cyan-shirts-grin.md b/.context/effect/.changeset/pre/cyan-shirts-grin.md new file mode 100644 index 000000000..96d0ce0d7 --- /dev/null +++ b/.context/effect/.changeset/pre/cyan-shirts-grin.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fork memo map on nested builds diff --git a/.context/effect/.changeset/pre/d1-batch-statements.md b/.context/effect/.changeset/pre/d1-batch-statements.md new file mode 100644 index 000000000..779a545b7 --- /dev/null +++ b/.context/effect/.changeset/pre/d1-batch-statements.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-d1": minor +--- + +Add `D1Client.batch` for executing a collection of SQL statements as a single atomic D1 batch. diff --git a/.context/effect/.changeset/pre/deduplicate-json-schema-fallbacks.md b/.context/effect/.changeset/pre/deduplicate-json-schema-fallbacks.md new file mode 100644 index 000000000..1c5aa3349 --- /dev/null +++ b/.context/effect/.changeset/pre/deduplicate-json-schema-fallbacks.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/openapi-generator": patch +--- + +Deduplicate equivalent fallback definitions when compiling JSON Schema, and reconstruct only definitions reachable from multi-document roots. + +Remove `SchemaMultiDocument` and `fromSchemaMultiDocument`; multi-document import and revival now return the ordered root schemas directly. + +Stop the OpenAPI generator from emitting component schemas that are not reachable from a generated root. diff --git a/.context/effect/.changeset/deep-rivers-spend.md b/.context/effect/.changeset/pre/deep-rivers-spend.md similarity index 100% rename from .context/effect/.changeset/deep-rivers-spend.md rename to .context/effect/.changeset/pre/deep-rivers-spend.md diff --git a/.context/effect/.changeset/pre/deferred-cleanup-after-completion.md b/.context/effect/.changeset/pre/deferred-cleanup-after-completion.md new file mode 100644 index 000000000..4434455d5 --- /dev/null +++ b/.context/effect/.changeset/pre/deferred-cleanup-after-completion.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Deferred.await` dying with a `TypeError` when a waiter is interrupted after the `Deferred` has been completed. diff --git a/.context/effect/.changeset/pre/deno-write-copy-errors.md b/.context/effect/.changeset/pre/deno-write-copy-errors.md new file mode 100644 index 000000000..2b93ed38e --- /dev/null +++ b/.context/effect/.changeset/pre/deno-write-copy-errors.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Preserve high-level filesystem error context for `writeFile` and normalize Deno `AlreadyExists` errors from `copy`. diff --git a/.context/effect/.changeset/dirty-lamps-trade.md b/.context/effect/.changeset/pre/dirty-lamps-trade.md similarity index 100% rename from .context/effect/.changeset/dirty-lamps-trade.md rename to .context/effect/.changeset/pre/dirty-lamps-trade.md diff --git a/.context/effect/.changeset/dirty-laws-wear.md b/.context/effect/.changeset/pre/dirty-laws-wear.md similarity index 100% rename from .context/effect/.changeset/dirty-laws-wear.md rename to .context/effect/.changeset/pre/dirty-laws-wear.md diff --git a/.context/effect/.changeset/pre/doctest-console-output.md b/.context/effect/.changeset/pre/doctest-console-output.md new file mode 100644 index 000000000..cd06cfddb --- /dev/null +++ b/.context/effect/.changeset/pre/doctest-console-output.md @@ -0,0 +1,5 @@ +--- +"@effect/doctest": patch +--- + +Add convention-based `// =>` assertions that compare documentation example values using Effect equality. diff --git a/.context/effect/.changeset/pre/document-child-process-env.md b/.context/effect/.changeset/pre/document-child-process-env.md new file mode 100644 index 000000000..f28d72881 --- /dev/null +++ b/.context/effect/.changeset/pre/document-child-process-env.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Document that `CommandOptions.extendEnv` defaults to `false` and that providing `env` without enabling it replaces the inherited child environment. diff --git a/.context/effect/.changeset/pre/dry-bugs-hug.md b/.context/effect/.changeset/pre/dry-bugs-hug.md new file mode 100644 index 000000000..723f24935 --- /dev/null +++ b/.context/effect/.changeset/pre/dry-bugs-hug.md @@ -0,0 +1,15 @@ +--- +"@effect/atom-react": patch +"@effect/atom-solid": patch +"@effect/atom-vue": patch +"@effect/docgen": patch +"@effect/doctest": patch +"@effect/opentelemetry": patch +"@effect/platform-node": patch +"@effect/sql-pg": patch +"@effect/sql-sqlite-react-native": patch +"@effect/sql-sqlite-wasm": patch +"@effect/vitest": patch +--- + +Update peer dependencies diff --git a/.context/effect/.changeset/duration-temporal-object-input.md b/.context/effect/.changeset/pre/duration-temporal-object-input.md similarity index 100% rename from .context/effect/.changeset/duration-temporal-object-input.md rename to .context/effect/.changeset/pre/duration-temporal-object-input.md diff --git a/.context/effect/.changeset/eager-coats-cheat.md b/.context/effect/.changeset/pre/eager-coats-cheat.md similarity index 100% rename from .context/effect/.changeset/eager-coats-cheat.md rename to .context/effect/.changeset/pre/eager-coats-cheat.md diff --git a/.context/effect/.changeset/early-birds-dream.md b/.context/effect/.changeset/pre/early-birds-dream.md similarity index 100% rename from .context/effect/.changeset/early-birds-dream.md rename to .context/effect/.changeset/pre/early-birds-dream.md diff --git a/.context/effect/.changeset/early-donuts-argue.md b/.context/effect/.changeset/pre/early-donuts-argue.md similarity index 100% rename from .context/effect/.changeset/early-donuts-argue.md rename to .context/effect/.changeset/pre/early-donuts-argue.md diff --git a/.context/effect/.changeset/pre/early-jobs-bow.md b/.context/effect/.changeset/pre/early-jobs-bow.md new file mode 100644 index 000000000..f62abe015 --- /dev/null +++ b/.context/effect/.changeset/pre/early-jobs-bow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Defer memoized Layer state installation until Effect execution. diff --git a/.context/effect/.changeset/early-peaches-check.md b/.context/effect/.changeset/pre/early-peaches-check.md similarity index 100% rename from .context/effect/.changeset/early-peaches-check.md rename to .context/effect/.changeset/pre/early-peaches-check.md diff --git a/.context/effect/.changeset/pre/eff-115-sync-scheduler-microtask.md b/.context/effect/.changeset/pre/eff-115-sync-scheduler-microtask.md new file mode 100644 index 000000000..f0d120781 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-115-sync-scheduler-microtask.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use cancellable microtasks when dispatching yielded work from synchronous Effect runs. diff --git a/.context/effect/.changeset/pre/eff-117-hydration-reactivity.md b/.context/effect/.changeset/pre/eff-117-hydration-reactivity.md new file mode 100644 index 000000000..948003938 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-117-hydration-reactivity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix hydrated atoms with `Atom.withReactivity` to refresh after reactive mutations. diff --git a/.context/effect/.changeset/pre/eff-121-http-router-web-handler.md b/.context/effect/.changeset/pre/eff-121-http-router-web-handler.md new file mode 100644 index 000000000..4576cfec2 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-121-http-router-web-handler.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpRouter.toWebHandler` context inference for services provided by the application layer. diff --git a/.context/effect/.changeset/pre/eff-123-openai-compat-unknown-events.md b/.context/effect/.changeset/pre/eff-123-openai-compat-unknown-events.md new file mode 100644 index 000000000..0d587a2b8 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-123-openai-compat-unknown-events.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Surface parsed chat completion stream events that do not match the expected schema as `UnknownChatCompletionEvent`. diff --git a/.context/effect/.changeset/pre/eff-137-web-stream-interop.md b/.context/effect/.changeset/pre/eff-137-web-stream-interop.md new file mode 100644 index 000000000..d3f164a89 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-137-web-stream-interop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Web Stream interoperability for `Channel` and `Sink`, plus byte limiting and `ArrayBuffer` collection for `Stream`. diff --git a/.context/effect/.changeset/pre/eff-140-deno-crypto.md b/.context/effect/.changeset/pre/eff-140-deno-crypto.md new file mode 100644 index 000000000..1ea8d267a --- /dev/null +++ b/.context/effect/.changeset/pre/eff-140-deno-crypto.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a Deno Web Crypto implementation of the `Crypto` service. diff --git a/.context/effect/.changeset/pre/eff-141-deno-child-process.md b/.context/effect/.changeset/pre/eff-141-deno-child-process.md new file mode 100644 index 000000000..746c3ff42 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-141-deno-child-process.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a native Deno `ChildProcessSpawner` implementation and shared process conformance coverage. diff --git a/.context/effect/.changeset/pre/eff-142-deno-terminal.md b/.context/effect/.changeset/pre/eff-142-deno-terminal.md new file mode 100644 index 000000000..b7eb5a35a --- /dev/null +++ b/.context/effect/.changeset/pre/eff-142-deno-terminal.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-deno": patch +"@effect/platform-node-shared": patch +--- + +Add a Deno `Terminal` implementation and keep `NodeTerminal` input readers alive until stdin ends under Deno. diff --git a/.context/effect/.changeset/pre/eff-143-deno-stdio.md b/.context/effect/.changeset/pre/eff-143-deno-stdio.md new file mode 100644 index 000000000..c46576d61 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-143-deno-stdio.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a native Deno implementation of the `Stdio` service. diff --git a/.context/effect/.changeset/pre/eff-145-deno-services.md b/.context/effect/.changeset/pre/eff-145-deno-services.md new file mode 100644 index 000000000..c9ef16084 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-145-deno-services.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add the aggregate Deno platform services layer. diff --git a/.context/effect/.changeset/pre/eff-148-deno-http-platform.md b/.context/effect/.changeset/pre/eff-148-deno-http-platform.md new file mode 100644 index 000000000..62766839f --- /dev/null +++ b/.context/effect/.changeset/pre/eff-148-deno-http-platform.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a native Deno `HttpPlatform` layer with resource-backed file responses. diff --git a/.context/effect/.changeset/pre/eff-151-deno-redis.md b/.context/effect/.changeset/pre/eff-151-deno-redis.md new file mode 100644 index 000000000..292d3fed1 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-151-deno-redis.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a native Deno Redis integration backed by `@db/redis`. diff --git a/.context/effect/.changeset/pre/eff-153-deno-http-server.md b/.context/effect/.changeset/pre/eff-153-deno-http-server.md new file mode 100644 index 000000000..a2e261164 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-153-deno-http-server.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add a native Deno HTTP server with multipart requests, file responses, and WebSocket upgrades. diff --git a/.context/effect/.changeset/pre/eff-153-websocket-initial-frames.md b/.context/effect/.changeset/pre/eff-153-websocket-initial-frames.md new file mode 100644 index 000000000..1f1142a06 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-153-websocket-initial-frames.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support replaying initial WebSocket messages and normalize `ArrayBuffer` frames to `Uint8Array`. diff --git a/.context/effect/.changeset/pre/eff-154-deno-cluster-http.md b/.context/effect/.changeset/pre/eff-154-deno-cluster-http.md new file mode 100644 index 000000000..36d032895 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-154-deno-cluster-http.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add native Deno HTTP and WebSocket layers for Effect Cluster runners. diff --git a/.context/effect/.changeset/pre/eff-155-deno-cluster-socket.md b/.context/effect/.changeset/pre/eff-155-deno-cluster-socket.md new file mode 100644 index 000000000..b079d9319 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-155-deno-cluster-socket.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Add native Deno socket layers for Effect Cluster runners. diff --git a/.context/effect/.changeset/pre/eff-162-browser-crypto-chunks.md b/.context/effect/.changeset/pre/eff-162-browser-crypto-chunks.md new file mode 100644 index 000000000..fb93845c9 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-162-browser-crypto-chunks.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix `BrowserCrypto.randomBytes` for requests larger than the Web Crypto per-call limit. diff --git a/.context/effect/.changeset/pre/eff-170-bun-multipart-stream.md b/.context/effect/.changeset/pre/eff-170-bun-multipart-stream.md new file mode 100644 index 000000000..ccc6b55a9 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-170-bun-multipart-stream.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Construct an empty multipart stream for each bodiless Bun request. diff --git a/.context/effect/.changeset/pre/eff-210-cookie-validation.md b/.context/effect/.changeset/pre/eff-210-cookie-validation.md new file mode 100644 index 000000000..7050ceb33 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-210-cookie-validation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Validate cookie names, domains, and paths before constructing or serializing cookies. diff --git a/.context/effect/.changeset/pre/eff-212-secure-mssql-transport.md b/.context/effect/.changeset/pre/eff-212-secure-mssql-transport.md new file mode 100644 index 000000000..f662266f9 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-212-secure-mssql-transport.md @@ -0,0 +1,7 @@ +--- +"@effect/sql-mssql": patch +--- + +**Breaking:** Secure Microsoft SQL Server connections by default by enabling encryption and validating server certificates. + +Users connecting to SQL Server instances without TLS must now explicitly set `encrypt: false`. Users connecting with untrusted or self-signed certificates must explicitly set `trustServer: true`. diff --git a/.context/effect/.changeset/pre/eff-216-secure-http-redirects.md b/.context/effect/.changeset/pre/eff-216-secure-http-redirects.md new file mode 100644 index 000000000..fbfe9fde4 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-216-secure-http-redirects.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Strip credential headers on cross-origin HTTP redirects and align redirected request methods with fetch. diff --git a/.context/effect/.changeset/pre/eff-218-bound-sse-pending-state.md b/.context/effect/.changeset/pre/eff-218-bound-sse-pending-state.md new file mode 100644 index 000000000..be71140d5 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-218-bound-sse-pending-state.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +"@effect/openapi-generator": patch +--- + +Bound pending SSE decoder state with a configurable maximum event size. diff --git a/.context/effect/.changeset/pre/eff-219-key-value-store-file-keys.md b/.context/effect/.changeset/pre/eff-219-key-value-store-file-keys.md new file mode 100644 index 000000000..ea7b27d88 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-219-key-value-store-file-keys.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject empty, `.` and `..` keys in file-backed key-value stores. diff --git a/.context/effect/.changeset/pre/eff-220-cli-control-characters.md b/.context/effect/.changeset/pre/eff-220-cli-control-characters.md new file mode 100644 index 000000000..7073b7cb3 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-220-cli-control-characters.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Escape terminal control characters in unstable CLI error output. diff --git a/.context/effect/.changeset/pre/eff-332-http-response-compression.md b/.context/effect/.changeset/pre/eff-332-http-response-compression.md new file mode 100644 index 000000000..831d4f07b --- /dev/null +++ b/.context/effect/.changeset/pre/eff-332-http-response-compression.md @@ -0,0 +1,10 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +--- + +Add HTTP response compression support. Node.js, Bun, and Deno use asynchronous +`node:zlib` one-shot compression for byte-array bodies, preserving an exact +`Content-Length`; stream and raw bodies remain streaming transforms. diff --git a/.context/effect/.changeset/pre/eff-337-preserve-mssql-parameters.md b/.context/effect/.changeset/pre/eff-337-preserve-mssql-parameters.md new file mode 100644 index 000000000..903a8ed19 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-337-preserve-mssql-parameters.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Preserve fractional numbers and Unicode strings in default Microsoft SQL Server parameters. diff --git a/.context/effect/.changeset/pre/eff-342-clickhouse-number-binding.md b/.context/effect/.changeset/pre/eff-342-clickhouse-number-binding.md new file mode 100644 index 000000000..847d5b566 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-342-clickhouse-number-binding.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Preserve fractional JavaScript numbers in inferred ClickHouse parameters. diff --git a/.context/effect/.changeset/pre/eff-389-execution-plan-attempts.md b/.context/effect/.changeset/pre/eff-389-execution-plan-attempts.md new file mode 100644 index 000000000..dc4262d0c --- /dev/null +++ b/.context/effect/.changeset/pre/eff-389-execution-plan-attempts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject zero execution attempts in `ExecutionPlan` steps. diff --git a/.context/effect/.changeset/pre/eff-428-pg-transaction-permit.md b/.context/effect/.changeset/pre/eff-428-pg-transaction-permit.md new file mode 100644 index 000000000..be6a9a20e --- /dev/null +++ b/.context/effect/.changeset/pre/eff-428-pg-transaction-permit.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Hold the shared PostgreSQL client permit for the full transaction lifetime. diff --git a/.context/effect/.changeset/pre/eff-467-execution-plan-events.md b/.context/effect/.changeset/pre/eff-467-execution-plan-events.md new file mode 100644 index 000000000..eae1e3bec --- /dev/null +++ b/.context/effect/.changeset/pre/eff-467-execution-plan-events.md @@ -0,0 +1,17 @@ +--- +"effect": patch +--- + +Add execution-plan lifecycle events via an optional `onEvent` handler on `Effect.withExecutionPlan` and `Stream.withExecutionPlan`. + +The handler receives an `ExecutionPlan.Event`, a tagged union of `AttemptStart`, `AttemptSuccess`, and `AttemptFailure`, allowing attempt outcomes to be observed from outside the effect for logging and metrics: + +```ts +import { Effect } from "effect" + +Effect.withExecutionPlan(program, plan, { + onEvent: (event) => Effect.log("execution plan event", event) +}) +``` + +Every `AttemptStart` is followed by exactly one terminal event. `AttemptFailure` carries the full failure `Cause`, so defects and interruption are reported as well as expected errors, and terminal events run like finalizers so they are emitted even when the attempt is interrupted. Event numbering matches `ExecutionPlan.CurrentMetadata`: `attempt` is cumulative across steps, while `stepAttempt` is 1-based within the current step. diff --git a/.context/effect/.changeset/pre/eff-477-schedule-concat.md b/.context/effect/.changeset/pre/eff-477-schedule-concat.md new file mode 100644 index 000000000..a9bbb3d9c --- /dev/null +++ b/.context/effect/.changeset/pre/eff-477-schedule-concat.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `Schedule.andThen` and `Schedule.andThenResult` to `Schedule.concat` and `Schedule.concatResult`. diff --git a/.context/effect/.changeset/pre/eff-487-web-tracer-shutdown.md b/.context/effect/.changeset/pre/eff-487-web-tracer-shutdown.md new file mode 100644 index 000000000..bc9dba25a --- /dev/null +++ b/.context/effect/.changeset/pre/eff-487-web-tracer-shutdown.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Ensure Web and Node tracer providers shut down when flushing fails during layer release. diff --git a/.context/effect/.changeset/pre/eff-51-partitioned-semaphore-interruption.md b/.context/effect/.changeset/pre/eff-51-partitioned-semaphore-interruption.md new file mode 100644 index 000000000..3d36a756c --- /dev/null +++ b/.context/effect/.changeset/pre/eff-51-partitioned-semaphore-interruption.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `PartitionedSemaphore.take` leaking partially acquired permits when interrupted. diff --git a/.context/effect/.changeset/pre/eff-523-registry-scoped-atom-runtime.md b/.context/effect/.changeset/pre/eff-523-registry-scoped-atom-runtime.md new file mode 100644 index 000000000..2b7a28424 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-523-registry-scoped-atom-runtime.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Scope `Atom.runtime` layer memoization to each `AtomRegistry` by default. Process-wide sharing is still available by passing a concrete `Layer.MemoMap` to `Atom.context`; the `Atom.defaultMemoMap` export has been removed. diff --git a/.context/effect/.changeset/pre/eff-532-stdio-terminal.md b/.context/effect/.changeset/pre/eff-532-stdio-terminal.md new file mode 100644 index 000000000..992ca220f --- /dev/null +++ b/.context/effect/.changeset/pre/eff-532-stdio-terminal.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-node-shared": patch +"@effect/platform-deno": patch +--- + +Expose `stdinIsTerminal` and `stdoutIsTerminal` effects through the `Stdio` service. diff --git a/.context/effect/.changeset/pre/eff-537-preserve-response-metadata.md b/.context/effect/.changeset/pre/eff-537-preserve-response-metadata.md new file mode 100644 index 000000000..606ead5b4 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-537-preserve-response-metadata.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve provider metadata when converting AI response parts into prompts. OpenAI chats using `store: true` now reuse restored item IDs as item references, while conversation-mode chats omit items already present in the conversation instead of inlining them. diff --git a/.context/effect/.changeset/pre/eff-542-rc-ref-generation.md b/.context/effect/.changeset/pre/eff-542-rc-ref-generation.md new file mode 100644 index 000000000..a7e8aeb36 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-542-rc-ref-generation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure concurrent first `RcRef` borrowers share the same resource generation. diff --git a/.context/effect/.changeset/pre/eff-547-node-tracer-timeout.md b/.context/effect/.changeset/pre/eff-547-node-tracer-timeout.md new file mode 100644 index 000000000..67198957e --- /dev/null +++ b/.context/effect/.changeset/pre/eff-547-node-tracer-timeout.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Bound Node tracer provider shutdown by the configured `shutdownTimeout`. diff --git a/.context/effect/.changeset/pre/eff-548-bun-serve-scope.md b/.context/effect/.changeset/pre/eff-548-bun-serve-scope.md new file mode 100644 index 000000000..a2ae20dd5 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-548-bun-serve-scope.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Fix Bun HTTP server handler restoration and defer shutdown while serve scopes remain active. diff --git a/.context/effect/.changeset/pre/eff-549-worker-send-error.md b/.context/effect/.changeset/pre/eff-549-worker-send-error.md new file mode 100644 index 000000000..5bab011f5 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-549-worker-send-error.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report buffered worker send failures as `WorkerError` values. diff --git a/.context/effect/.changeset/pre/eff-552-txqueue-shutdown.md b/.context/effect/.changeset/pre/eff-552-txqueue-shutdown.md new file mode 100644 index 000000000..8e02adb77 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-552-txqueue-shutdown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `TxQueue.shutdown` safe to call after a queue has already been interrupted. diff --git a/.context/effect/.changeset/pre/eff-554-sql-resolver.md b/.context/effect/.changeset/pre/eff-554-sql-resolver.md new file mode 100644 index 000000000..a7581b87b --- /dev/null +++ b/.context/effect/.changeset/pre/eff-554-sql-resolver.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent SQL resolvers from invoking non-empty batch callbacks when every request fails encoding. diff --git a/.context/effect/.changeset/pre/eff-558-deno-file-web-range.md b/.context/effect/.changeset/pre/eff-558-deno-file-web-range.md new file mode 100644 index 000000000..e4027e6b4 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-558-deno-file-web-range.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Honor `offset` and `bytesToRead` when creating Deno Web file responses. diff --git a/.context/effect/.changeset/eff-691-default-logger-ordering.md b/.context/effect/.changeset/pre/eff-691-default-logger-ordering.md similarity index 100% rename from .context/effect/.changeset/eff-691-default-logger-ordering.md rename to .context/effect/.changeset/pre/eff-691-default-logger-ordering.md diff --git a/.context/effect/.changeset/eff-693-rpcgroup-handler-deps.md b/.context/effect/.changeset/pre/eff-693-rpcgroup-handler-deps.md similarity index 100% rename from .context/effect/.changeset/eff-693-rpcgroup-handler-deps.md rename to .context/effect/.changeset/pre/eff-693-rpcgroup-handler-deps.md diff --git a/.context/effect/.changeset/eff-694-cli-completions-module.md b/.context/effect/.changeset/pre/eff-694-cli-completions-module.md similarity index 100% rename from .context/effect/.changeset/eff-694-cli-completions-module.md rename to .context/effect/.changeset/pre/eff-694-cli-completions-module.md diff --git a/.context/effect/.changeset/eff-695-layer-mock-dual-api.md b/.context/effect/.changeset/pre/eff-695-layer-mock-dual-api.md similarity index 100% rename from .context/effect/.changeset/eff-695-layer-mock-dual-api.md rename to .context/effect/.changeset/pre/eff-695-layer-mock-dual-api.md diff --git a/.context/effect/.changeset/eff-697-rpcserialization-json-array-decode.md b/.context/effect/.changeset/pre/eff-697-rpcserialization-json-array-decode.md similarity index 100% rename from .context/effect/.changeset/eff-697-rpcserialization-json-array-decode.md rename to .context/effect/.changeset/pre/eff-697-rpcserialization-json-array-decode.md diff --git a/.context/effect/.changeset/eff-698-rpcserialization-unreachable-branch.md b/.context/effect/.changeset/pre/eff-698-rpcserialization-unreachable-branch.md similarity index 100% rename from .context/effect/.changeset/eff-698-rpcserialization-unreachable-branch.md rename to .context/effect/.changeset/pre/eff-698-rpcserialization-unreachable-branch.md diff --git a/.context/effect/.changeset/eff-700-httpapi-middleware-errors.md b/.context/effect/.changeset/pre/eff-700-httpapi-middleware-errors.md similarity index 91% rename from .context/effect/.changeset/eff-700-httpapi-middleware-errors.md rename to .context/effect/.changeset/pre/eff-700-httpapi-middleware-errors.md index e95a36007..ef46ad705 100644 --- a/.context/effect/.changeset/eff-700-httpapi-middleware-errors.md +++ b/.context/effect/.changeset/pre/eff-700-httpapi-middleware-errors.md @@ -6,7 +6,7 @@ Improve unstable HttpApi runtime failures for missing server middleware and miss - HttpApiBuilder.applyMiddleware now resolves middleware services via Context.getUnsafe, so missing middleware fails with a clear "Service not found: " error instead of an opaque is not a function TypeError. - HttpApiBuilder.layer now reports missing groups with actionable context (group identifier, service key, suggested HttpApiBuilder.group(...) call, and available group keys). -- Added regression tests in packages/platform-node/test/HttpApi.test.ts covering: +- Added regression tests in packages/platform/node/test/HttpApi.test.ts covering: - addHttpApi + API-level middleware applied across merged groups - missing middleware service diagnostics - missing addHttpApi group layer diagnostics diff --git a/.context/effect/.changeset/eff-701-httpapierror-respondable.md b/.context/effect/.changeset/pre/eff-701-httpapierror-respondable.md similarity index 100% rename from .context/effect/.changeset/eff-701-httpapierror-respondable.md rename to .context/effect/.changeset/pre/eff-701-httpapierror-respondable.md diff --git a/.context/effect/.changeset/eff-704-stream-merge-predicate.md b/.context/effect/.changeset/pre/eff-704-stream-merge-predicate.md similarity index 100% rename from .context/effect/.changeset/eff-704-stream-merge-predicate.md rename to .context/effect/.changeset/pre/eff-704-stream-merge-predicate.md diff --git a/.context/effect/.changeset/eff-705-layer-tap-apis.md b/.context/effect/.changeset/pre/eff-705-layer-tap-apis.md similarity index 100% rename from .context/effect/.changeset/eff-705-layer-tap-apis.md rename to .context/effect/.changeset/pre/eff-705-layer-tap-apis.md diff --git a/.context/effect/.changeset/eff-706-servicemap-mutate.md b/.context/effect/.changeset/pre/eff-706-servicemap-mutate.md similarity index 100% rename from .context/effect/.changeset/eff-706-servicemap-mutate.md rename to .context/effect/.changeset/pre/eff-706-servicemap-mutate.md diff --git a/.context/effect/.changeset/eff-716-response-id-tracker-map.md b/.context/effect/.changeset/pre/eff-716-response-id-tracker-map.md similarity index 100% rename from .context/effect/.changeset/eff-716-response-id-tracker-map.md rename to .context/effect/.changeset/pre/eff-716-response-id-tracker-map.md diff --git a/.context/effect/.changeset/eff-717-openai-socket-cancel.md b/.context/effect/.changeset/pre/eff-717-openai-socket-cancel.md similarity index 100% rename from .context/effect/.changeset/eff-717-openai-socket-cancel.md rename to .context/effect/.changeset/pre/eff-717-openai-socket-cancel.md diff --git a/.context/effect/.changeset/eff-718-embedding-model-surface.md b/.context/effect/.changeset/pre/eff-718-embedding-model-surface.md similarity index 100% rename from .context/effect/.changeset/eff-718-embedding-model-surface.md rename to .context/effect/.changeset/pre/eff-718-embedding-model-surface.md diff --git a/.context/effect/.changeset/eff-725-fix-catch-jsdoc.md b/.context/effect/.changeset/pre/eff-725-fix-catch-jsdoc.md similarity index 100% rename from .context/effect/.changeset/eff-725-fix-catch-jsdoc.md rename to .context/effect/.changeset/pre/eff-725-fix-catch-jsdoc.md diff --git a/.context/effect/.changeset/eff-726-model-dimensions.md b/.context/effect/.changeset/pre/eff-726-model-dimensions.md similarity index 100% rename from .context/effect/.changeset/eff-726-model-dimensions.md rename to .context/effect/.changeset/pre/eff-726-model-dimensions.md diff --git a/.context/effect/.changeset/eff-727-cli-help-alignment.md b/.context/effect/.changeset/pre/eff-727-cli-help-alignment.md similarity index 100% rename from .context/effect/.changeset/eff-727-cli-help-alignment.md rename to .context/effect/.changeset/pre/eff-727-cli-help-alignment.md diff --git a/.context/effect/.changeset/eff-730-language-model-incremental-fallback.md b/.context/effect/.changeset/pre/eff-730-language-model-incremental-fallback.md similarity index 100% rename from .context/effect/.changeset/eff-730-language-model-incremental-fallback.md rename to .context/effect/.changeset/pre/eff-730-language-model-incremental-fallback.md diff --git a/.context/effect/.changeset/eff-736-cached-with-ttl.md b/.context/effect/.changeset/pre/eff-736-cached-with-ttl.md similarity index 100% rename from .context/effect/.changeset/eff-736-cached-with-ttl.md rename to .context/effect/.changeset/pre/eff-736-cached-with-ttl.md diff --git a/.context/effect/.changeset/eff-738-cron-prev.md b/.context/effect/.changeset/pre/eff-738-cron-prev.md similarity index 100% rename from .context/effect/.changeset/eff-738-cron-prev.md rename to .context/effect/.changeset/pre/eff-738-cron-prev.md diff --git a/.context/effect/.changeset/eff-739-openai-function-call-done.md b/.context/effect/.changeset/pre/eff-739-openai-function-call-done.md similarity index 100% rename from .context/effect/.changeset/eff-739-openai-function-call-done.md rename to .context/effect/.changeset/pre/eff-739-openai-function-call-done.md diff --git a/.context/effect/.changeset/eff-740-missing-summary-parts.md b/.context/effect/.changeset/pre/eff-740-missing-summary-parts.md similarity index 100% rename from .context/effect/.changeset/eff-740-missing-summary-parts.md rename to .context/effect/.changeset/pre/eff-740-missing-summary-parts.md diff --git a/.context/effect/.changeset/eff-742-http-client-request-web.md b/.context/effect/.changeset/pre/eff-742-http-client-request-web.md similarity index 100% rename from .context/effect/.changeset/eff-742-http-client-request-web.md rename to .context/effect/.changeset/pre/eff-742-http-client-request-web.md diff --git a/.context/effect/.changeset/eff-744-sqlite-migrator-lock.md b/.context/effect/.changeset/pre/eff-744-sqlite-migrator-lock.md similarity index 100% rename from .context/effect/.changeset/eff-744-sqlite-migrator-lock.md rename to .context/effect/.changeset/pre/eff-744-sqlite-migrator-lock.md diff --git a/.context/effect/.changeset/eff-746-fixed-iteration-catchup.md b/.context/effect/.changeset/pre/eff-746-fixed-iteration-catchup.md similarity index 100% rename from .context/effect/.changeset/eff-746-fixed-iteration-catchup.md rename to .context/effect/.changeset/pre/eff-746-fixed-iteration-catchup.md diff --git a/.context/effect/.changeset/eff-747-unify-effect.md b/.context/effect/.changeset/pre/eff-747-unify-effect.md similarity index 100% rename from .context/effect/.changeset/eff-747-unify-effect.md rename to .context/effect/.changeset/pre/eff-747-unify-effect.md diff --git a/.context/effect/.changeset/eff-754-url-builder-any.md b/.context/effect/.changeset/pre/eff-754-url-builder-any.md similarity index 100% rename from .context/effect/.changeset/eff-754-url-builder-any.md rename to .context/effect/.changeset/pre/eff-754-url-builder-any.md diff --git a/.context/effect/.changeset/eff-755-references-core.md b/.context/effect/.changeset/pre/eff-755-references-core.md similarity index 100% rename from .context/effect/.changeset/eff-755-references-core.md rename to .context/effect/.changeset/pre/eff-755-references-core.md diff --git a/.context/effect/.changeset/eff-769-select-text-highlight.md b/.context/effect/.changeset/pre/eff-769-select-text-highlight.md similarity index 100% rename from .context/effect/.changeset/eff-769-select-text-highlight.md rename to .context/effect/.changeset/pre/eff-769-select-text-highlight.md diff --git a/.context/effect/.changeset/eff-774-mutable-list-append-all-empty-array.md b/.context/effect/.changeset/pre/eff-774-mutable-list-append-all-empty-array.md similarity index 100% rename from .context/effect/.changeset/eff-774-mutable-list-append-all-empty-array.md rename to .context/effect/.changeset/pre/eff-774-mutable-list-append-all-empty-array.md diff --git a/.context/effect/.changeset/eff-777-schema-make-effect.md b/.context/effect/.changeset/pre/eff-777-schema-make-effect.md similarity index 100% rename from .context/effect/.changeset/eff-777-schema-make-effect.md rename to .context/effect/.changeset/pre/eff-777-schema-make-effect.md diff --git a/.context/effect/.changeset/eff-778-http-middleware-path-logger.md b/.context/effect/.changeset/pre/eff-778-http-middleware-path-logger.md similarity index 100% rename from .context/effect/.changeset/eff-778-http-middleware-path-logger.md rename to .context/effect/.changeset/pre/eff-778-http-middleware-path-logger.md diff --git a/.context/effect/.changeset/eff-779-keyvaluestore-layer-sql.md b/.context/effect/.changeset/pre/eff-779-keyvaluestore-layer-sql.md similarity index 100% rename from .context/effect/.changeset/eff-779-keyvaluestore-layer-sql.md rename to .context/effect/.changeset/pre/eff-779-keyvaluestore-layer-sql.md diff --git a/.context/effect/.changeset/eff-780-layer-unify.md b/.context/effect/.changeset/pre/eff-780-layer-unify.md similarity index 100% rename from .context/effect/.changeset/eff-780-layer-unify.md rename to .context/effect/.changeset/pre/eff-780-layer-unify.md diff --git a/.context/effect/.changeset/eff-781-fix-stream-toqueue-types.md b/.context/effect/.changeset/pre/eff-781-fix-stream-toqueue-types.md similarity index 100% rename from .context/effect/.changeset/eff-781-fix-stream-toqueue-types.md rename to .context/effect/.changeset/pre/eff-781-fix-stream-toqueue-types.md diff --git a/.context/effect/.changeset/eff-782-httpapi-status-literals.md b/.context/effect/.changeset/pre/eff-782-httpapi-status-literals.md similarity index 100% rename from .context/effect/.changeset/eff-782-httpapi-status-literals.md rename to .context/effect/.changeset/pre/eff-782-httpapi-status-literals.md diff --git a/.context/effect/.changeset/eff-783-atom-http-api-errors.md b/.context/effect/.changeset/pre/eff-783-atom-http-api-errors.md similarity index 100% rename from .context/effect/.changeset/eff-783-atom-http-api-errors.md rename to .context/effect/.changeset/pre/eff-783-atom-http-api-errors.md diff --git a/.context/effect/.changeset/eff-819-cluster-workflow-shard-groups.md b/.context/effect/.changeset/pre/eff-819-cluster-workflow-shard-groups.md similarity index 100% rename from .context/effect/.changeset/eff-819-cluster-workflow-shard-groups.md rename to .context/effect/.changeset/pre/eff-819-cluster-workflow-shard-groups.md diff --git a/.context/effect/.changeset/eff-849-transpose-option.md b/.context/effect/.changeset/pre/eff-849-transpose-option.md similarity index 100% rename from .context/effect/.changeset/eff-849-transpose-option.md rename to .context/effect/.changeset/pre/eff-849-transpose-option.md diff --git a/.context/effect/.changeset/pre/eff-946-concurrent-traversal-cleanup.md b/.context/effect/.changeset/pre/eff-946-concurrent-traversal-cleanup.md new file mode 100644 index 000000000..1ae7fabb1 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-946-concurrent-traversal-cleanup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Interrupt and await concurrent traversal workers when mapper or refill callbacks throw. diff --git a/.context/effect/.changeset/pre/eff-952-terminal-failure-stack.md b/.context/effect/.changeset/pre/eff-952-terminal-failure-stack.md new file mode 100644 index 000000000..5c72f8d56 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-952-terminal-failure-stack.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve current stack frame annotations on terminal root failures. diff --git a/.context/effect/.changeset/pre/eff-953-interruptor-stack-trace.md b/.context/effect/.changeset/pre/eff-953-interruptor-stack-trace.md new file mode 100644 index 000000000..aeca222b2 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-953-interruptor-stack-trace.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Store interrupting fiber stack frames separately from interrupted target stack frames. diff --git a/.context/effect/.changeset/pre/eff-955-run-sync-dispatcher.md b/.context/effect/.changeset/pre/eff-955-run-sync-dispatcher.md new file mode 100644 index 000000000..22865608b --- /dev/null +++ b/.context/effect/.changeset/pre/eff-955-run-sync-dispatcher.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Avoid allocating a scheduler dispatcher when `runSyncExit` completes without yielding. diff --git a/.context/effect/.changeset/pre/eff-956-await-all-children.md b/.context/effect/.changeset/pre/eff-956-await-all-children.md new file mode 100644 index 000000000..8f892c153 --- /dev/null +++ b/.context/effect/.changeset/pre/eff-956-await-all-children.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make awaitAllChildren child selection linear in the number of fibers. diff --git a/.context/effect/.changeset/eight-turkeys-own.md b/.context/effect/.changeset/pre/eight-turkeys-own.md similarity index 100% rename from .context/effect/.changeset/eight-turkeys-own.md rename to .context/effect/.changeset/pre/eight-turkeys-own.md diff --git a/.context/effect/.changeset/eighty-lies-deny.md b/.context/effect/.changeset/pre/eighty-lies-deny.md similarity index 100% rename from .context/effect/.changeset/eighty-lies-deny.md rename to .context/effect/.changeset/pre/eighty-lies-deny.md diff --git a/.context/effect/.changeset/eighty-poets-draw.md b/.context/effect/.changeset/pre/eighty-poets-draw.md similarity index 100% rename from .context/effect/.changeset/eighty-poets-draw.md rename to .context/effect/.changeset/pre/eighty-poets-draw.md diff --git a/.context/effect/.changeset/eighty-swans-scream.md b/.context/effect/.changeset/pre/eighty-swans-scream.md similarity index 100% rename from .context/effect/.changeset/eighty-swans-scream.md rename to .context/effect/.changeset/pre/eighty-swans-scream.md diff --git a/.context/effect/.changeset/eighty-teeth-sniff.md b/.context/effect/.changeset/pre/eighty-teeth-sniff.md similarity index 100% rename from .context/effect/.changeset/eighty-teeth-sniff.md rename to .context/effect/.changeset/pre/eighty-teeth-sniff.md diff --git a/.context/effect/.changeset/eleven-apes-share.md b/.context/effect/.changeset/pre/eleven-apes-share.md similarity index 100% rename from .context/effect/.changeset/eleven-apes-share.md rename to .context/effect/.changeset/pre/eleven-apes-share.md diff --git a/.context/effect/.changeset/eleven-numbers-bake.md b/.context/effect/.changeset/pre/eleven-numbers-bake.md similarity index 100% rename from .context/effect/.changeset/eleven-numbers-bake.md rename to .context/effect/.changeset/pre/eleven-numbers-bake.md diff --git a/.context/effect/.changeset/pre/empty-env-values-missing.md b/.context/effect/.changeset/pre/empty-env-values-missing.md new file mode 100644 index 000000000..cd894f95d --- /dev/null +++ b/.context/effect/.changeset/pre/empty-env-values-missing.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Treat empty strings as missing values in built-in `ConfigProvider`s by default. + +`ConfigProvider.fromEnv`, `ConfigProvider.fromDotEnvContents`, `ConfigProvider.fromDotEnv`, `ConfigProvider.fromUnknown`, and `ConfigProvider.fromDir` now treat literal empty strings as absent values when loaded as values, allowing `Config.withDefault` and `Config.option` to recover. Container discovery still reflects the source structure. Pass `preserveEmptyStrings: true` to restore the previous behavior. + +`ConfigProvider.fromDotEnv({ expandVariables: true })` now expands variables consistently with `ConfigProvider.fromDotEnvContents`. diff --git a/.context/effect/.changeset/pre/empty-geckos-dispatch.md b/.context/effect/.changeset/pre/empty-geckos-dispatch.md new file mode 100644 index 000000000..e082b11e5 --- /dev/null +++ b/.context/effect/.changeset/pre/empty-geckos-dispatch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Encode SSE events with empty data as dispatchable events. diff --git a/.context/effect/.changeset/empty-gifts-beg.md b/.context/effect/.changeset/pre/empty-gifts-beg.md similarity index 100% rename from .context/effect/.changeset/empty-gifts-beg.md rename to .context/effect/.changeset/pre/empty-gifts-beg.md diff --git a/.context/effect/.changeset/empty-http-rpc-client.md b/.context/effect/.changeset/pre/empty-http-rpc-client.md similarity index 100% rename from .context/effect/.changeset/empty-http-rpc-client.md rename to .context/effect/.changeset/pre/empty-http-rpc-client.md diff --git a/.context/effect/.changeset/pre/empty-snakes-return.md b/.context/effect/.changeset/pre/empty-snakes-return.md new file mode 100644 index 000000000..94ae6a96f --- /dev/null +++ b/.context/effect/.changeset/pre/empty-snakes-return.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `String.snakeToCamel` and `String.snakeToPascal` to return an empty string for empty input. diff --git a/.context/effect/.changeset/eventlog-unencrypted.md b/.context/effect/.changeset/pre/eventlog-unencrypted.md similarity index 100% rename from .context/effect/.changeset/eventlog-unencrypted.md rename to .context/effect/.changeset/pre/eventlog-unencrypted.md diff --git a/.context/effect/.changeset/every-olives-burn.md b/.context/effect/.changeset/pre/every-olives-burn.md similarity index 100% rename from .context/effect/.changeset/every-olives-burn.md rename to .context/effect/.changeset/pre/every-olives-burn.md diff --git a/.context/effect/.changeset/expand-schema-filter-output.md b/.context/effect/.changeset/pre/expand-schema-filter-output.md similarity index 100% rename from .context/effect/.changeset/expand-schema-filter-output.md rename to .context/effect/.changeset/pre/expand-schema-filter-output.md diff --git a/.context/effect/.changeset/pre/explicit-env-record.md b/.context/effect/.changeset/pre/explicit-env-record.md new file mode 100644 index 000000000..2045bbd20 --- /dev/null +++ b/.context/effect/.changeset/pre/explicit-env-record.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `ConfigProvider.fromEnvRecord` for building a provider from an explicit environment record. diff --git a/.context/effect/.changeset/pre/explicit-otel-service-identity.md b/.context/effect/.changeset/pre/explicit-otel-service-identity.md new file mode 100644 index 000000000..a34030e33 --- /dev/null +++ b/.context/effect/.changeset/pre/explicit-otel-service-identity.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prefer explicit OTLP resource configuration over environment configuration. diff --git a/.context/effect/.changeset/export-schema-encode-keys-interface.md b/.context/effect/.changeset/pre/export-schema-encode-keys-interface.md similarity index 100% rename from .context/effect/.changeset/export-schema-encode-keys-interface.md rename to .context/effect/.changeset/pre/export-schema-encode-keys-interface.md diff --git a/.context/effect/.changeset/pre/expose-ai-prompt-part-schemas.md b/.context/effect/.changeset/pre/expose-ai-prompt-part-schemas.md new file mode 100644 index 000000000..b29af5e94 --- /dev/null +++ b/.context/effect/.changeset/pre/expose-ai-prompt-part-schemas.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose runtime schemas for AI prompt parts and message-specific part unions. diff --git a/.context/effect/.changeset/extract-semaphore-latch.md b/.context/effect/.changeset/pre/extract-semaphore-latch.md similarity index 100% rename from .context/effect/.changeset/extract-semaphore-latch.md rename to .context/effect/.changeset/pre/extract-semaphore-latch.md diff --git a/.context/effect/.changeset/pre/failed-otlp-checkpoints.md b/.context/effect/.changeset/pre/failed-otlp-checkpoints.md new file mode 100644 index 000000000..8e806105d --- /dev/null +++ b/.context/effect/.changeset/pre/failed-otlp-checkpoints.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve OTLP metric delta checkpoints when an export fails. diff --git a/.context/effect/.changeset/fair-bees-relax.md b/.context/effect/.changeset/pre/fair-bees-relax.md similarity index 100% rename from .context/effect/.changeset/fair-bees-relax.md rename to .context/effect/.changeset/pre/fair-bees-relax.md diff --git a/.context/effect/.changeset/pre/fair-birds-limit.md b/.context/effect/.changeset/pre/fair-birds-limit.md new file mode 100644 index 000000000..c72957459 --- /dev/null +++ b/.context/effect/.changeset/pre/fair-birds-limit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add Schedule.upTo options for limiting schedules by duration and/or recurrence count. diff --git a/.context/effect/.changeset/fair-buttons-share.md b/.context/effect/.changeset/pre/fair-buttons-share.md similarity index 100% rename from .context/effect/.changeset/fair-buttons-share.md rename to .context/effect/.changeset/pre/fair-buttons-share.md diff --git a/.context/effect/.changeset/pre/fair-citations-stream.md b/.context/effect/.changeset/pre/fair-citations-stream.md new file mode 100644 index 000000000..49f8e0ab1 --- /dev/null +++ b/.context/effect/.changeset/pre/fair-citations-stream.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Preserve start and end offsets for streamed OpenRouter citations. diff --git a/.context/effect/.changeset/fair-cooks-stop.md b/.context/effect/.changeset/pre/fair-cooks-stop.md similarity index 100% rename from .context/effect/.changeset/fair-cooks-stop.md rename to .context/effect/.changeset/pre/fair-cooks-stop.md diff --git a/.context/effect/.changeset/fair-cups-train.md b/.context/effect/.changeset/pre/fair-cups-train.md similarity index 100% rename from .context/effect/.changeset/fair-cups-train.md rename to .context/effect/.changeset/pre/fair-cups-train.md diff --git a/.context/effect/.changeset/fair-dryers-speak.md b/.context/effect/.changeset/pre/fair-dryers-speak.md similarity index 100% rename from .context/effect/.changeset/fair-dryers-speak.md rename to .context/effect/.changeset/pre/fair-dryers-speak.md diff --git a/.context/effect/.changeset/fair-forks-shake.md b/.context/effect/.changeset/pre/fair-forks-shake.md similarity index 100% rename from .context/effect/.changeset/fair-forks-shake.md rename to .context/effect/.changeset/pre/fair-forks-shake.md diff --git a/.context/effect/.changeset/pre/fair-jobs-like.md b/.context/effect/.changeset/pre/fair-jobs-like.md new file mode 100644 index 000000000..e98587334 --- /dev/null +++ b/.context/effect/.changeset/pre/fair-jobs-like.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add a `radius` option to `Graph` search configuration, allowing `dfs`, `bfs`, and `dfsPostOrder` traversals to limit returned nodes by edge distance from the configured start nodes. Traversals can also use `direction: "undirected"` to follow edges in either direction. diff --git a/.context/effect/.changeset/pre/fair-logs-correlate.md b/.context/effect/.changeset/pre/fair-logs-correlate.md new file mode 100644 index 000000000..dacdd7b02 --- /dev/null +++ b/.context/effect/.changeset/pre/fair-logs-correlate.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Prevent log annotations from overwriting active span correlation identifiers. diff --git a/.context/effect/.changeset/pre/fair-logs-listen.md b/.context/effect/.changeset/pre/fair-logs-listen.md new file mode 100644 index 000000000..6608c2236 --- /dev/null +++ b/.context/effect/.changeset/pre/fair-logs-listen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now advertise logging and honor each client's selected log level when sending log notifications. diff --git a/.context/effect/.changeset/fair-pandas-prove.md b/.context/effect/.changeset/pre/fair-pandas-prove.md similarity index 100% rename from .context/effect/.changeset/fair-pandas-prove.md rename to .context/effect/.changeset/pre/fair-pandas-prove.md diff --git a/.context/effect/.changeset/fair-pants-float.md b/.context/effect/.changeset/pre/fair-pants-float.md similarity index 100% rename from .context/effect/.changeset/fair-pants-float.md rename to .context/effect/.changeset/pre/fair-pants-float.md diff --git a/.context/effect/.changeset/fair-poems-visit.md b/.context/effect/.changeset/pre/fair-poems-visit.md similarity index 100% rename from .context/effect/.changeset/fair-poems-visit.md rename to .context/effect/.changeset/pre/fair-poems-visit.md diff --git a/.context/effect/.changeset/pre/fair-sampling-content.md b/.context/effect/.changeset/pre/fair-sampling-content.md new file mode 100644 index 000000000..d88205d8f --- /dev/null +++ b/.context/effect/.changeset/pre/fair-sampling-content.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve MCP sampling request preferences and response content. diff --git a/.context/effect/.changeset/pre/fair-sinks-catch.md b/.context/effect/.changeset/pre/fair-sinks-catch.md new file mode 100644 index 000000000..6ea07ee3e --- /dev/null +++ b/.context/effect/.changeset/pre/fair-sinks-catch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the error type exposed by the curried `Sink.catch` overload. diff --git a/.context/effect/.changeset/pre/famous-loops-flow.md b/.context/effect/.changeset/pre/famous-loops-flow.md new file mode 100644 index 000000000..ac6581223 --- /dev/null +++ b/.context/effect/.changeset/pre/famous-loops-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Check symbol-keyed properties in Match object patterns. diff --git a/.context/effect/.changeset/famous-wolves-lead.md b/.context/effect/.changeset/pre/famous-wolves-lead.md similarity index 100% rename from .context/effect/.changeset/famous-wolves-lead.md rename to .context/effect/.changeset/pre/famous-wolves-lead.md diff --git a/.context/effect/.changeset/fancy-glasses-grow.md b/.context/effect/.changeset/pre/fancy-glasses-grow.md similarity index 100% rename from .context/effect/.changeset/fancy-glasses-grow.md rename to .context/effect/.changeset/pre/fancy-glasses-grow.md diff --git a/.context/effect/.changeset/pre/fast-graph-path-queues.md b/.context/effect/.changeset/pre/fast-graph-path-queues.md new file mode 100644 index 000000000..66d188167 --- /dev/null +++ b/.context/effect/.changeset/pre/fast-graph-path-queues.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve `Graph.dijkstra` and `Graph.astar` priority queue performance. diff --git a/.context/effect/.changeset/fast-times-camp.md b/.context/effect/.changeset/pre/fast-times-camp.md similarity index 100% rename from .context/effect/.changeset/fast-times-camp.md rename to .context/effect/.changeset/pre/fast-times-camp.md diff --git a/.context/effect/.changeset/few-birds-matter.md b/.context/effect/.changeset/pre/few-birds-matter.md similarity index 100% rename from .context/effect/.changeset/few-birds-matter.md rename to .context/effect/.changeset/pre/few-birds-matter.md diff --git a/.context/effect/.changeset/few-cougars-dig.md b/.context/effect/.changeset/pre/few-cougars-dig.md similarity index 100% rename from .context/effect/.changeset/few-cougars-dig.md rename to .context/effect/.changeset/pre/few-cougars-dig.md diff --git a/.context/effect/.changeset/few-foxes-grin.md b/.context/effect/.changeset/pre/few-foxes-grin.md similarity index 100% rename from .context/effect/.changeset/few-foxes-grin.md rename to .context/effect/.changeset/pre/few-foxes-grin.md diff --git a/.context/effect/.changeset/few-mirrors-pull.md b/.context/effect/.changeset/pre/few-mirrors-pull.md similarity index 100% rename from .context/effect/.changeset/few-mirrors-pull.md rename to .context/effect/.changeset/pre/few-mirrors-pull.md diff --git a/.context/effect/.changeset/few-socks-poke.md b/.context/effect/.changeset/pre/few-socks-poke.md similarity index 100% rename from .context/effect/.changeset/few-socks-poke.md rename to .context/effect/.changeset/pre/few-socks-poke.md diff --git a/.context/effect/.changeset/pre/fiber-join-all-errors.md b/.context/effect/.changeset/pre/fiber-join-all-errors.md new file mode 100644 index 000000000..ac883aedc --- /dev/null +++ b/.context/effect/.changeset/pre/fiber-join-all-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve input fiber error types in `Fiber.joinAll`. diff --git a/.context/effect/.changeset/fiber-runtime-start-metrics.md b/.context/effect/.changeset/pre/fiber-runtime-start-metrics.md similarity index 100% rename from .context/effect/.changeset/fiber-runtime-start-metrics.md rename to .context/effect/.changeset/pre/fiber-runtime-start-metrics.md diff --git a/.context/effect/.changeset/fiery-jokes-care.md b/.context/effect/.changeset/pre/fiery-jokes-care.md similarity index 100% rename from .context/effect/.changeset/fiery-jokes-care.md rename to .context/effect/.changeset/pre/fiery-jokes-care.md diff --git a/.context/effect/.changeset/fiery-mammals-call.md b/.context/effect/.changeset/pre/fiery-mammals-call.md similarity index 100% rename from .context/effect/.changeset/fiery-mammals-call.md rename to .context/effect/.changeset/pre/fiery-mammals-call.md diff --git a/.context/effect/.changeset/fine-walls-decide.md b/.context/effect/.changeset/pre/fine-walls-decide.md similarity index 100% rename from .context/effect/.changeset/fine-walls-decide.md rename to .context/effect/.changeset/pre/fine-walls-decide.md diff --git a/.context/effect/.changeset/first-success-of.md b/.context/effect/.changeset/pre/first-success-of.md similarity index 100% rename from .context/effect/.changeset/first-success-of.md rename to .context/effect/.changeset/pre/first-success-of.md diff --git a/.context/effect/.changeset/five-parents-relax.md b/.context/effect/.changeset/pre/five-parents-relax.md similarity index 100% rename from .context/effect/.changeset/five-parents-relax.md rename to .context/effect/.changeset/pre/five-parents-relax.md diff --git a/.context/effect/.changeset/five-worms-rhyme.md b/.context/effect/.changeset/pre/five-worms-rhyme.md similarity index 100% rename from .context/effect/.changeset/five-worms-rhyme.md rename to .context/effect/.changeset/pre/five-worms-rhyme.md diff --git a/.context/effect/.changeset/fix-1332.md b/.context/effect/.changeset/pre/fix-1332.md similarity index 100% rename from .context/effect/.changeset/fix-1332.md rename to .context/effect/.changeset/pre/fix-1332.md diff --git a/.context/effect/.changeset/fix-1917.md b/.context/effect/.changeset/pre/fix-1917.md similarity index 100% rename from .context/effect/.changeset/fix-1917.md rename to .context/effect/.changeset/pre/fix-1917.md diff --git a/.context/effect/.changeset/fix-1927.md b/.context/effect/.changeset/pre/fix-1927.md similarity index 100% rename from .context/effect/.changeset/fix-1927.md rename to .context/effect/.changeset/pre/fix-1927.md diff --git a/.context/effect/.changeset/fix-1940.md b/.context/effect/.changeset/pre/fix-1940.md similarity index 100% rename from .context/effect/.changeset/fix-1940.md rename to .context/effect/.changeset/pre/fix-1940.md diff --git a/.context/effect/.changeset/fix-1947.md b/.context/effect/.changeset/pre/fix-1947.md similarity index 100% rename from .context/effect/.changeset/fix-1947.md rename to .context/effect/.changeset/pre/fix-1947.md diff --git a/.context/effect/.changeset/fix-2002.md b/.context/effect/.changeset/pre/fix-2002.md similarity index 100% rename from .context/effect/.changeset/fix-2002.md rename to .context/effect/.changeset/pre/fix-2002.md diff --git a/.context/effect/.changeset/fix-2012.md b/.context/effect/.changeset/pre/fix-2012.md similarity index 100% rename from .context/effect/.changeset/fix-2012.md rename to .context/effect/.changeset/pre/fix-2012.md diff --git a/.context/effect/.changeset/fix-2015.md b/.context/effect/.changeset/pre/fix-2015.md similarity index 100% rename from .context/effect/.changeset/fix-2015.md rename to .context/effect/.changeset/pre/fix-2015.md diff --git a/.context/effect/.changeset/fix-2260.md b/.context/effect/.changeset/pre/fix-2260.md similarity index 100% rename from .context/effect/.changeset/fix-2260.md rename to .context/effect/.changeset/pre/fix-2260.md diff --git a/.context/effect/.changeset/fix-2268.md b/.context/effect/.changeset/pre/fix-2268.md similarity index 100% rename from .context/effect/.changeset/fix-2268.md rename to .context/effect/.changeset/pre/fix-2268.md diff --git a/.context/effect/.changeset/fix-2271.md b/.context/effect/.changeset/pre/fix-2271.md similarity index 100% rename from .context/effect/.changeset/fix-2271.md rename to .context/effect/.changeset/pre/fix-2271.md diff --git a/.context/effect/.changeset/fix-2384.md b/.context/effect/.changeset/pre/fix-2384.md similarity index 100% rename from .context/effect/.changeset/fix-2384.md rename to .context/effect/.changeset/pre/fix-2384.md diff --git a/.context/effect/.changeset/fix-2414.md b/.context/effect/.changeset/pre/fix-2414.md similarity index 100% rename from .context/effect/.changeset/fix-2414.md rename to .context/effect/.changeset/pre/fix-2414.md diff --git a/.context/effect/.changeset/fix-2419.md b/.context/effect/.changeset/pre/fix-2419.md similarity index 100% rename from .context/effect/.changeset/fix-2419.md rename to .context/effect/.changeset/pre/fix-2419.md diff --git a/.context/effect/.changeset/fix-2497.md b/.context/effect/.changeset/pre/fix-2497.md similarity index 100% rename from .context/effect/.changeset/fix-2497.md rename to .context/effect/.changeset/pre/fix-2497.md diff --git a/.context/effect/.changeset/fix-2499.md b/.context/effect/.changeset/pre/fix-2499.md similarity index 100% rename from .context/effect/.changeset/fix-2499.md rename to .context/effect/.changeset/pre/fix-2499.md diff --git a/.context/effect/.changeset/pre/fix-6464.md b/.context/effect/.changeset/pre/fix-6464.md new file mode 100644 index 000000000..13d7b52d2 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-6464.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Distribute `HttpApiBuilder` handler requirements per service so request middleware layers can provide them, closes #6464. diff --git a/.context/effect/.changeset/pre/fix-6491.md b/.context/effect/.changeset/pre/fix-6491.md new file mode 100644 index 000000000..a37e2d0cc --- /dev/null +++ b/.context/effect/.changeset/pre/fix-6491.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve nested class construction when applying constructor defaults, closes #6491. diff --git a/.context/effect/.changeset/pre/fix-6521.md b/.context/effect/.changeset/pre/fix-6521.md new file mode 100644 index 000000000..ca7c85ad3 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-6521.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Simplify the displayed `Type`, `Encoded`, and `Iso` types of required readonly `Schema.Struct` fields, closes #6521. diff --git a/.context/effect/.changeset/fix-ai-empty-params-structured-output.md b/.context/effect/.changeset/pre/fix-ai-empty-params-structured-output.md similarity index 100% rename from .context/effect/.changeset/fix-ai-empty-params-structured-output.md rename to .context/effect/.changeset/pre/fix-ai-empty-params-structured-output.md diff --git a/.context/effect/.changeset/fix-ai-text-toolkit-typing.md b/.context/effect/.changeset/pre/fix-ai-text-toolkit-typing.md similarity index 100% rename from .context/effect/.changeset/fix-ai-text-toolkit-typing.md rename to .context/effect/.changeset/pre/fix-ai-text-toolkit-typing.md diff --git a/.context/effect/.changeset/pre/fix-ai-tool-call-id.md b/.context/effect/.changeset/pre/fix-ai-tool-call-id.md new file mode 100644 index 000000000..42d87284a --- /dev/null +++ b/.context/effect/.changeset/pre/fix-ai-tool-call-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Expose the tool call ID to AI tool handlers and `Toolkit.WithHandler.handle` wrappers. diff --git a/.context/effect/.changeset/pre/fix-ansi-cursor-to.md b/.context/effect/.changeset/pre/fix-ansi-cursor-to.md new file mode 100644 index 000000000..36309f832 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-ansi-cursor-to.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Emit valid CSI sequences from the unstable CLI `cursorTo` helper. diff --git a/.context/effect/.changeset/fix-anthropic-caller-toolid.md b/.context/effect/.changeset/pre/fix-anthropic-caller-toolid.md similarity index 100% rename from .context/effect/.changeset/fix-anthropic-caller-toolid.md rename to .context/effect/.changeset/pre/fix-anthropic-caller-toolid.md diff --git a/.context/effect/.changeset/pre/fix-anthropic-code-execution-deltas.md b/.context/effect/.changeset/pre/fix-anthropic-code-execution-deltas.md new file mode 100644 index 000000000..a50a451a2 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-anthropic-code-execution-deltas.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix malformed JSON in streamed Anthropic code-execution tool parameters. diff --git a/.context/effect/.changeset/pre/fix-anthropic-header-redaction.md b/.context/effect/.changeset/pre/fix-anthropic-header-redaction.md new file mode 100644 index 000000000..3a8bd80b7 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-anthropic-header-redaction.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Redact the Anthropic API key from client error context. diff --git a/.context/effect/.changeset/pre/fix-anthropic-memory-tool-requires-handler.md b/.context/effect/.changeset/pre/fix-anthropic-memory-tool-requires-handler.md new file mode 100644 index 000000000..f9bd9843f --- /dev/null +++ b/.context/effect/.changeset/pre/fix-anthropic-memory-tool-requires-handler.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix `Memory_20250818` provider-defined tool missing `requiresHandler: true`. Like the other client-executed tools (`TextEditor_20250728`, `Bash_2025*`, `ComputerUse_2025*`), the memory tool requires the application to implement its execution (view/create/str_replace/insert/delete/rename over `/memories/*`). Without this flag, `Tool.HandlersFor` excluded it from the required handlers, making it impossible to type-check a handler for `Memory_20250818` in `Toolkit.toLayer`. diff --git a/.context/effect/.changeset/pre/fix-anthropic-memory-tool.md b/.context/effect/.changeset/pre/fix-anthropic-memory-tool.md new file mode 100644 index 000000000..3a9a9cdbc --- /dev/null +++ b/.context/effect/.changeset/pre/fix-anthropic-memory-tool.md @@ -0,0 +1,11 @@ +--- +"@effect/ai-anthropic": patch +--- + +Fix client-executed provider tools (Memory, Text Editor, Computer Use, Bash) which were unusable on the wire. + +- `makeResponse` (and the streaming equivalents) now map a provider `tool_use` wire name (e.g. `"memory"`) back to the tool's custom name (e.g. `"AnthropicMemory"`) that the toolkit is keyed by, instead of raising `ToolNotFoundError`. +- `AnthropicTool.MemoryCreateCommand` now includes the required `file_text` field, so a `create` command no longer drops the file body. +- Optional parameters on client-executed provider tools now use `Schema.optionalKey` instead of `Schema.optional`, which the Anthropic codec rejected with "Unsupported AST Undefined": `Memory`/`TextEditor` `view_range`, `ComputerUse` `coordinate`, and `Bash` `restart`. + +Closes #2615. diff --git a/.context/effect/.changeset/pre/fix-anthropic-plaintext-bytes.md b/.context/effect/.changeset/pre/fix-anthropic-plaintext-bytes.md new file mode 100644 index 000000000..5d6c5740e --- /dev/null +++ b/.context/effect/.changeset/pre/fix-anthropic-plaintext-bytes.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-anthropic": patch +--- + +Decode byte-backed plain-text attachments as UTF-8 text in Anthropic requests. diff --git a/.context/effect/.changeset/pre/fix-array-non-finite-indexes.md b/.context/effect/.changeset/pre/fix-array-non-finite-indexes.md new file mode 100644 index 000000000..dcfd9db0f --- /dev/null +++ b/.context/effect/.changeset/pre/fix-array-non-finite-indexes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Array index operations handling `NaN` and fractional indexes. diff --git a/.context/effect/.changeset/pre/fix-atom-batch-dependencies.md b/.context/effect/.changeset/pre/fix-atom-batch-dependencies.md new file mode 100644 index 000000000..5c1ad7071 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-atom-batch-dependencies.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Atom dependency tracking and re-entrant invalidation during batch rebuilds. diff --git a/.context/effect/.changeset/pre/fix-atom-kvs-async-write.md b/.context/effect/.changeset/pre/fix-atom-kvs-async-write.md new file mode 100644 index 000000000..2227e7aed --- /dev/null +++ b/.context/effect/.changeset/pre/fix-atom-kvs-async-write.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Atom.kvs` async mode to retain its `AsyncResult` value shape after writes. diff --git a/.context/effect/.changeset/pre/fix-atom-suspense-registry-cache.md b/.context/effect/.changeset/pre/fix-atom-suspense-registry-cache.md new file mode 100644 index 000000000..220407192 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-atom-suspense-registry-cache.md @@ -0,0 +1,5 @@ +--- +"@effect/atom-react": patch +--- + +Scope `useAtomSuspense` promises to their atom registry so concurrent registries resolve independently. diff --git a/.context/effect/.changeset/pre/fix-bash-subcommand-dispatch.md b/.context/effect/.changeset/pre/fix-bash-subcommand-dispatch.md new file mode 100644 index 000000000..c43cfc220 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-bash-subcommand-dispatch.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent Bash completions from treating flag values as subcommands. diff --git a/.context/effect/.changeset/pre/fix-bigint-gcd-lcm.md b/.context/effect/.changeset/pre/fix-bigint-gcd-lcm.md new file mode 100644 index 000000000..bfffa0484 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-bigint-gcd-lcm.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `BigInt.gcd` and `BigInt.lcm` return non-negative values and handle zero operands in `BigInt.lcm`. diff --git a/.context/effect/.changeset/pre/fix-cache-set-race.md b/.context/effect/.changeset/pre/fix-cache-set-race.md new file mode 100644 index 000000000..be4cf636d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cache-set-race.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent an interrupted cache lookup from removing a newer value written with `Cache.set`. diff --git a/.context/effect/.changeset/fix-catch-orelse-error-erasure.md b/.context/effect/.changeset/pre/fix-catch-orelse-error-erasure.md similarity index 100% rename from .context/effect/.changeset/fix-catch-orelse-error-erasure.md rename to .context/effect/.changeset/pre/fix-catch-orelse-error-erasure.md diff --git a/.context/effect/.changeset/pre/fix-cause-map-annotations.md b/.context/effect/.changeset/pre/fix-cause-map-annotations.md new file mode 100644 index 000000000..efc7e791f --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cause-map-annotations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve failure annotations when mapping errors with `Cause.map`. diff --git a/.context/effect/.changeset/pre/fix-channel-schema-decode-unknown.md b/.context/effect/.changeset/pre/fix-channel-schema-decode-unknown.md new file mode 100644 index 000000000..c7ee9a2ba --- /dev/null +++ b/.context/effect/.changeset/pre/fix-channel-schema-decode-unknown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `ChannelSchema.decodeUnknown` to accept unknown input chunks while keeping `ChannelSchema.decode` typed to the schema's encoded input. diff --git a/.context/effect/.changeset/pre/fix-chunk-fractional-counts.md b/.context/effect/.changeset/pre/fix-chunk-fractional-counts.md new file mode 100644 index 000000000..0fae65c5a --- /dev/null +++ b/.context/effect/.changeset/pre/fix-chunk-fractional-counts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Chunk.take` and `Chunk.drop` produce valid chunks for fractional counts. diff --git a/.context/effect/.changeset/fix-class-constructor-defaults.md b/.context/effect/.changeset/pre/fix-class-constructor-defaults.md similarity index 100% rename from .context/effect/.changeset/fix-class-constructor-defaults.md rename to .context/effect/.changeset/pre/fix-class-constructor-defaults.md diff --git a/.context/effect/.changeset/pre/fix-cli-missing-flag-values.md b/.context/effect/.changeset/pre/fix-cli-missing-flag-values.md new file mode 100644 index 000000000..0dfb373f9 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cli-missing-flag-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report an error when a CLI flag, including `--completions`, is provided without its required value. diff --git a/.context/effect/.changeset/fix-cli-mixed-global-flag-context.md b/.context/effect/.changeset/pre/fix-cli-mixed-global-flag-context.md similarity index 100% rename from .context/effect/.changeset/fix-cli-mixed-global-flag-context.md rename to .context/effect/.changeset/pre/fix-cli-mixed-global-flag-context.md diff --git a/.context/effect/.changeset/pre/fix-cli-subcommands-requirements.md b/.context/effect/.changeset/pre/fix-cli-subcommands-requirements.md new file mode 100644 index 000000000..563526731 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cli-subcommands-requirements.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +- Fix `Command.withSubcommands` collapsing the inferred requirements type to `never` when given more than one subcommand +- Export a `Command.Services` utility type to extract the required services from a `Command` diff --git a/.context/effect/.changeset/pre/fix-cli-unexpected-arguments.md b/.context/effect/.changeset/pre/fix-cli-unexpected-arguments.md new file mode 100644 index 000000000..2f6b775cd --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cli-unexpected-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject unexpected positional arguments left after command parsing, including values exceeding `Argument.variadic` maximum bounds. diff --git a/.context/effect/.changeset/pre/fix-clickhouse-connect-timeout.md b/.context/effect/.changeset/pre/fix-clickhouse-connect-timeout.md new file mode 100644 index 000000000..41a80e369 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-clickhouse-connect-timeout.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Close the ClickHouse client when the startup connection check times out. diff --git a/.context/effect/.changeset/pre/fix-cluster-entity-context-bleed.md b/.context/effect/.changeset/pre/fix-cluster-entity-context-bleed.md new file mode 100644 index 000000000..c9037d3d5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cluster-entity-context-bleed.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use registration context for cluster entities diff --git a/.context/effect/.changeset/pre/fix-cluster-reply-defect-isolation.md b/.context/effect/.changeset/pre/fix-cluster-reply-defect-isolation.md new file mode 100644 index 000000000..62f0d8472 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cluster-reply-defect-isolation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Scope cluster reply serialization failures and peer-delivered defects to their own request instead of the whole runner connection diff --git a/.context/effect/.changeset/pre/fix-cluster-shutdown-deadlock.md b/.context/effect/.changeset/pre/fix-cluster-shutdown-deadlock.md new file mode 100644 index 000000000..50649c4a0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cluster-shutdown-deadlock.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a `@effect/cluster` shutdown deadlock on single-runner topologies (e.g. single-node deployments and `TestRunner`), where `Sharding.sendOutgoing` retried `EntityNotAssignedToRunner` forever during teardown. diff --git a/.context/effect/.changeset/pre/fix-cluster-strand-request-shutdown.md b/.context/effect/.changeset/pre/fix-cluster-strand-request-shutdown.md new file mode 100644 index 000000000..886d22f7e --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cluster-strand-request-shutdown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix cluster shutdown hangs by failing abandoned non-discard requests and stream chunk acknowledgements with `EntityNotAssignedToRunner`, including persisted requests sent after runner unregistration. This adds `EntityNotAssignedToRunner` to the typed error channel of entity clients and request-only `EntityProxy` RPC/HTTP endpoints; discard endpoints remain unchanged. diff --git a/.context/effect/.changeset/pre/fix-cluster-stream-recovery.md b/.context/effect/.changeset/pre/fix-cluster-stream-recovery.md new file mode 100644 index 000000000..7c54c6381 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cluster-stream-recovery.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix persisted cluster stream recovery when SQL drivers return a null reply kind. diff --git a/.context/effect/.changeset/fix-config-array-default.md b/.context/effect/.changeset/pre/fix-config-array-default.md similarity index 100% rename from .context/effect/.changeset/fix-config-array-default.md rename to .context/effect/.changeset/pre/fix-config-array-default.md diff --git a/.context/effect/.changeset/pre/fix-config-or-else-evidence.md b/.context/effect/.changeset/pre/fix-config-or-else-evidence.md new file mode 100644 index 000000000..ced02896e --- /dev/null +++ b/.context/effect/.changeset/pre/fix-config-or-else-evidence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve provider input evidence when `Config.orElse` recovers a configuration failure. diff --git a/.context/effect/.changeset/fix-config-withDefault.md b/.context/effect/.changeset/pre/fix-config-withDefault.md similarity index 100% rename from .context/effect/.changeset/fix-config-withDefault.md rename to .context/effect/.changeset/pre/fix-config-withDefault.md diff --git a/.context/effect/.changeset/fix-config-withdefault-filter.md b/.context/effect/.changeset/pre/fix-config-withdefault-filter.md similarity index 100% rename from .context/effect/.changeset/fix-config-withdefault-filter.md rename to .context/effect/.changeset/pre/fix-config-withdefault-filter.md diff --git a/.context/effect/.changeset/pre/fix-context-add-or-omit-types.md b/.context/effect/.changeset/pre/fix-context-add-or-omit-types.md new file mode 100644 index 000000000..f05f8bb40 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-context-add-or-omit-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Omit services removed by `Context.addOrOmit` from the returned context type. diff --git a/.context/effect/.changeset/pre/fix-cron-and-representations.md b/.context/effect/.changeset/pre/fix-cron-and-representations.md new file mode 100644 index 000000000..064eeb3ee --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-and-representations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Cron` day and weekday intersection semantics in inspection representations. diff --git a/.context/effect/.changeset/pre/fix-cron-make-validation.md b/.context/effect/.changeset/pre/fix-cron-make-validation.md new file mode 100644 index 000000000..5f9e350f0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-make-validation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Validate `Cron.make` field constraints and treat weekday `7` as Sunday consistently with cron parsing. diff --git a/.context/effect/.changeset/fix-cron-next-missing-day-overflow.md b/.context/effect/.changeset/pre/fix-cron-next-missing-day-overflow.md similarity index 100% rename from .context/effect/.changeset/fix-cron-next-missing-day-overflow.md rename to .context/effect/.changeset/pre/fix-cron-next-missing-day-overflow.md diff --git a/.context/effect/.changeset/pre/fix-cron-parser-semantics.md b/.context/effect/.changeset/pre/fix-cron-parser-semantics.md new file mode 100644 index 000000000..840d6f06b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-parser-semantics.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix cron parsing and scheduling edge cases for whitespace, Sunday `7`, strict numeric tokens, explicit full day ranges, and month-constrained day-of-month / weekday matching. diff --git a/.context/effect/.changeset/pre/fix-cron-prev-month-rollover.md b/.context/effect/.changeset/pre/fix-cron-prev-month-rollover.md new file mode 100644 index 000000000..8085ddc1d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-prev-month-rollover.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.prev` day-of-month rollover across shorter months and non-leap years. diff --git a/.context/effect/.changeset/pre/fix-cron-prev-weekday-wrap.md b/.context/effect/.changeset/pre/fix-cron-prev-weekday-wrap.md new file mode 100644 index 000000000..e4b830749 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-prev-weekday-wrap.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Cron.prev` weekday wrapping to always return a matching instant before the input. diff --git a/.context/effect/.changeset/pre/fix-cron-timezone-hash.md b/.context/effect/.changeset/pre/fix-cron-timezone-hash.md new file mode 100644 index 000000000..6ee572d04 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-cron-timezone-hash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make Cron equality and hashing include the optional timezone consistently. diff --git a/.context/effect/.changeset/fix-datetime-gmt.md b/.context/effect/.changeset/pre/fix-datetime-gmt.md similarity index 100% rename from .context/effect/.changeset/fix-datetime-gmt.md rename to .context/effect/.changeset/pre/fix-datetime-gmt.md diff --git a/.context/effect/.changeset/fix-devtools-flush-on-teardown.md b/.context/effect/.changeset/pre/fix-devtools-flush-on-teardown.md similarity index 100% rename from .context/effect/.changeset/fix-devtools-flush-on-teardown.md rename to .context/effect/.changeset/pre/fix-devtools-flush-on-teardown.md diff --git a/.context/effect/.changeset/pre/fix-devtools-span-snapshot.md b/.context/effect/.changeset/pre/fix-devtools-span-snapshot.md new file mode 100644 index 000000000..ca381c8d5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-devtools-span-snapshot.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix DevTools span requests to preserve their state when queued for sending. diff --git a/.context/effect/.changeset/pre/fix-durable-clock-fractional-wakeup.md b/.context/effect/.changeset/pre/fix-durable-clock-fractional-wakeup.md new file mode 100644 index 000000000..d0a7390c8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-durable-clock-fractional-wakeup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Normalize cluster durable clock wake-up timestamps to whole milliseconds. diff --git a/.context/effect/.changeset/pre/fix-durable-deferred-race.md b/.context/effect/.changeset/pre/fix-durable-deferred-race.md new file mode 100644 index 000000000..21b43f360 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-durable-deferred-race.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `DurableDeferred.raceAll` so a completed deferred can wake an active workflow without changing success-biased race semantics diff --git a/.context/effect/.changeset/pre/fix-durable-race-replay.md b/.context/effect/.changeset/pre/fix-durable-race-replay.md new file mode 100644 index 000000000..2b0e3b242 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-durable-race-replay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix replay of persisted `DurableDeferred.raceAll` results. diff --git a/.context/effect/.changeset/pre/fix-duration-decimal-precision.md b/.context/effect/.changeset/pre/fix-duration-decimal-precision.md new file mode 100644 index 000000000..2afc2db77 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-duration-decimal-precision.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve integral precision when parsing decimal nano and micro duration inputs diff --git a/.context/effect/.changeset/fix-duration-symmetric-rounding.md b/.context/effect/.changeset/pre/fix-duration-symmetric-rounding.md similarity index 100% rename from .context/effect/.changeset/fix-duration-symmetric-rounding.md rename to .context/effect/.changeset/pre/fix-duration-symmetric-rounding.md diff --git a/.context/effect/.changeset/pre/fix-effect-schedule-errors.md b/.context/effect/.changeset/pre/fix-effect-schedule-errors.md new file mode 100644 index 000000000..8a8910026 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-effect-schedule-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include schedule errors in the error channel of `Effect.schedule` and `Effect.scheduleFrom`. diff --git a/.context/effect/.changeset/fix-entity-manager-defect-replay.md b/.context/effect/.changeset/pre/fix-entity-manager-defect-replay.md similarity index 100% rename from .context/effect/.changeset/fix-entity-manager-defect-replay.md rename to .context/effect/.changeset/pre/fix-entity-manager-defect-replay.md diff --git a/.context/effect/.changeset/fix-entity-proxy-rpc-handler-context.md b/.context/effect/.changeset/pre/fix-entity-proxy-rpc-handler-context.md similarity index 100% rename from .context/effect/.changeset/fix-entity-proxy-rpc-handler-context.md rename to .context/effect/.changeset/pre/fix-entity-proxy-rpc-handler-context.md diff --git a/.context/effect/.changeset/fix-entity-proxy-server-path-params.md b/.context/effect/.changeset/pre/fix-entity-proxy-server-path-params.md similarity index 100% rename from .context/effect/.changeset/fix-entity-proxy-server-path-params.md rename to .context/effect/.changeset/pre/fix-entity-proxy-server-path-params.md diff --git a/.context/effect/.changeset/pre/fix-eventlog-duplicate-chunks.md b/.context/effect/.changeset/pre/fix-eventlog-duplicate-chunks.md new file mode 100644 index 000000000..dc215a8b3 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-eventlog-duplicate-chunks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore duplicate chunk indexes when joining event log messages. diff --git a/.context/effect/.changeset/pre/fix-fiberhandle-clear-race.md b/.context/effect/.changeset/pre/fix-fiberhandle-clear-race.md new file mode 100644 index 000000000..567ba19e0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-fiberhandle-clear-race.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a race where FiberHandle.clear could remove a newer fiber installed while the previous fiber was still interrupting. diff --git a/.context/effect/.changeset/pre/fix-fiberset-json-id.md b/.context/effect/.changeset/pre/fix-fiberset-json-id.md new file mode 100644 index 000000000..158b963c1 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-fiberset-json-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the inspectable JSON identity of `FiberSet`. diff --git a/.context/effect/.changeset/pre/fix-fiberset-runtime-interruption.md b/.context/effect/.changeset/pre/fix-fiberset-runtime-interruption.md new file mode 100644 index 000000000..0d550e711 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-fiberset-runtime-interruption.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Propagate the `FiberSet.runtime` interruption option when registering managed fibers. diff --git a/.context/effect/.changeset/pre/fix-fish-command-path.md b/.context/effect/.changeset/pre/fix-fish-command-path.md new file mode 100644 index 000000000..15053389d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-fish-command-path.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Match Fish completions against the full nested command path. diff --git a/.context/effect/.changeset/pre/fix-formatter-output-contracts.md b/.context/effect/.changeset/pre/fix-formatter-output-contracts.md new file mode 100644 index 000000000..5312f9d8e --- /dev/null +++ b/.context/effect/.changeset/pre/fix-formatter-output-contracts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Formatter.format` handling of shared references and ensure `Formatter.formatJson` always returns valid JSON. diff --git a/.context/effect/.changeset/fix-from-json-string-identifier.md b/.context/effect/.changeset/pre/fix-from-json-string-identifier.md similarity index 100% rename from .context/effect/.changeset/fix-from-json-string-identifier.md rename to .context/effect/.changeset/pre/fix-from-json-string-identifier.md diff --git a/.context/effect/.changeset/fix-from-readable-stream-cancel-defect.md b/.context/effect/.changeset/pre/fix-from-readable-stream-cancel-defect.md similarity index 100% rename from .context/effect/.changeset/fix-from-readable-stream-cancel-defect.md rename to .context/effect/.changeset/pre/fix-from-readable-stream-cancel-defect.md diff --git a/.context/effect/.changeset/pre/fix-graph-allocator-equality.md b/.context/effect/.changeset/pre/fix-graph-allocator-equality.md new file mode 100644 index 000000000..d796cad07 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-allocator-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix immutable Graph equality and hashing to include future node and edge identifier allocation. diff --git a/.context/effect/.changeset/pre/fix-graph-bellman-ford-self-cycle.md b/.context/effect/.changeset/pre/fix-graph-bellman-ford-self-cycle.md new file mode 100644 index 000000000..07dcc32a6 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-bellman-ford-self-cycle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.bellmanFord` to detect reachable negative cycles when the source and target are the same node. diff --git a/.context/effect/.changeset/pre/fix-graph-curried-getters.md b/.context/effect/.changeset/pre/fix-graph-curried-getters.md new file mode 100644 index 000000000..4d7947920 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-curried-getters.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix standalone data-last `Graph.getNode` and `Graph.getEdge` inference. diff --git a/.context/effect/.changeset/pre/fix-graph-edge-transforms.md b/.context/effect/.changeset/pre/fix-graph-edge-transforms.md new file mode 100644 index 000000000..e0c410ee8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-edge-transforms.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.mapEdges` and `Graph.filterMapEdges` to preserve `Graph.Edge` instances when transforming edge data. diff --git a/.context/effect/.changeset/pre/fix-graph-finite-edge-weights.md b/.context/effect/.changeset/pre/fix-graph-finite-edge-weights.md new file mode 100644 index 000000000..171304d07 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-finite-edge-weights.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject `NaN` and `-Infinity` edge weights in Graph shortest-path algorithms. diff --git a/.context/effect/.changeset/pre/fix-graph-mutable-hash.md b/.context/effect/.changeset/pre/fix-graph-mutable-hash.md new file mode 100644 index 000000000..62463090c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-mutable-hash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix mutable Graph equality and hashing to use reference identity while preserving structural semantics for immutable graphs. diff --git a/.context/effect/.changeset/pre/fix-graph-mutable-topo.md b/.context/effect/.changeset/pre/fix-graph-mutable-topo.md new file mode 100644 index 000000000..8a929bde8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-mutable-topo.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix topological walkers silently completing with an incomplete order when a mutable graph becomes cyclic after walker creation. diff --git a/.context/effect/.changeset/pre/fix-graph-topo-types.md b/.context/effect/.changeset/pre/fix-graph-topo-types.md new file mode 100644 index 000000000..adab4eb46 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-topo-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Restrict `Graph.topo` to directed graphs at the type level while retaining runtime validation for unsafe undirected inputs. diff --git a/.context/effect/.changeset/fix-graph-undirected-traversal.md b/.context/effect/.changeset/pre/fix-graph-undirected-traversal.md similarity index 100% rename from .context/effect/.changeset/fix-graph-undirected-traversal.md rename to .context/effect/.changeset/pre/fix-graph-undirected-traversal.md diff --git a/.context/effect/.changeset/pre/fix-graph-walker-repeatability.md b/.context/effect/.changeset/pre/fix-graph-walker-repeatability.md new file mode 100644 index 000000000..25c1ccd98 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graph-walker-repeatability.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.Walker` to create a fresh iterable for each direct iteration. diff --git a/.context/effect/.changeset/pre/fix-graphviz-dot-escaping.md b/.context/effect/.changeset/pre/fix-graphviz-dot-escaping.md new file mode 100644 index 000000000..35bd254d6 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-graphviz-dot-escaping.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.toGraphViz` to quote DOT graph names and escape labels as literal text. diff --git a/.context/effect/.changeset/fix-has-interrupts-only-empty.md b/.context/effect/.changeset/pre/fix-has-interrupts-only-empty.md similarity index 100% rename from .context/effect/.changeset/fix-has-interrupts-only-empty.md rename to .context/effect/.changeset/pre/fix-has-interrupts-only-empty.md diff --git a/.context/effect/.changeset/fix-hashmap-bit31-ordering.md b/.context/effect/.changeset/pre/fix-hashmap-bit31-ordering.md similarity index 100% rename from .context/effect/.changeset/fix-hashmap-bit31-ordering.md rename to .context/effect/.changeset/pre/fix-hashmap-bit31-ordering.md diff --git a/.context/effect/.changeset/pre/fix-hashmap-modify-hash.md b/.context/effect/.changeset/pre/fix-hashmap-modify-hash.md new file mode 100644 index 000000000..ca36cabf5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-hashmap-modify-hash.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use the supplied hash for `HashMap.modifyHash` insertions, updates, and removals. diff --git a/.context/effect/.changeset/fix-headers-proto-enumerability.md b/.context/effect/.changeset/pre/fix-headers-proto-enumerability.md similarity index 100% rename from .context/effect/.changeset/fix-headers-proto-enumerability.md rename to .context/effect/.changeset/pre/fix-headers-proto-enumerability.md diff --git a/.context/effect/.changeset/pre/fix-http-client-request-content-length.md b/.context/effect/.changeset/pre/fix-http-client-request-content-length.md new file mode 100644 index 000000000..12f66418c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-http-client-request-content-length.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove stale `content-length` headers when replacing an HTTP client request body with one of unknown length. diff --git a/.context/effect/.changeset/fix-http-incoming-message-parse-options.md b/.context/effect/.changeset/pre/fix-http-incoming-message-parse-options.md similarity index 100% rename from .context/effect/.changeset/fix-http-incoming-message-parse-options.md rename to .context/effect/.changeset/pre/fix-http-incoming-message-parse-options.md diff --git a/.context/effect/.changeset/pre/fix-http-pre-response-handler-types.md b/.context/effect/.changeset/pre/fix-http-pre-response-handler-types.md new file mode 100644 index 000000000..66eeb8b70 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-http-pre-response-handler-types.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the published declaration for `HttpEffect.appendPreResponseHandlerUnsafe`. diff --git a/.context/effect/.changeset/pre/fix-http-server-request-raw-body.md b/.context/effect/.changeset/pre/fix-http-server-request-raw-body.md new file mode 100644 index 000000000..148559e4a --- /dev/null +++ b/.context/effect/.changeset/pre/fix-http-server-request-raw-body.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support standard `BodyInit` values when reading converted client request bodies through `HttpServerRequest`. diff --git a/.context/effect/.changeset/pre/fix-http-server-response-body-headers.md b/.context/effect/.changeset/pre/fix-http-server-response-body-headers.md new file mode 100644 index 000000000..90f227817 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-http-server-response-body-headers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Synchronize HTTP server response content headers when replacing the body. diff --git a/.context/effect/.changeset/fix-http-tracer-response-cause.md b/.context/effect/.changeset/pre/fix-http-tracer-response-cause.md similarity index 100% rename from .context/effect/.changeset/fix-http-tracer-response-cause.md rename to .context/effect/.changeset/pre/fix-http-tracer-response-cause.md diff --git a/.context/effect/.changeset/pre/fix-httpapi-authorization-decoding.md b/.context/effect/.changeset/pre/fix-httpapi-authorization-decoding.md new file mode 100644 index 000000000..b41cb0662 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-httpapi-authorization-decoding.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix HttpApi authorization decoding. + +Previously, `HttpApiBuilder.securityDecode` removed the expected scheme length and one following character from the `Authorization` header without verifying either value. A Bearer decoder could therefore pass credentials from a different scheme such as `Basic`, accept a malformed header without a separating space, or retain leading spaces when more than one separator was present. + +The decoder now validates the declared scheme before returning credentials, matches it case-insensitively as required by [RFC 9110 section 11.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-11.1), and consumes one or more separating spaces. Missing, malformed, or mismatched headers produce the existing empty credential value so security middleware can reject them consistently. + +Basic authentication previously split the decoded `user-pass` value at every colon, causing otherwise valid passwords containing `:` to be discarded. It now uses only the first colon as the separator and preserves the rest of the password, following [RFC 7617 section 2](https://www.rfc-editor.org/rfc/rfc7617.html#section-2). diff --git a/.context/effect/.changeset/pre/fix-httpapi-client-error-content-type.md b/.context/effect/.changeset/pre/fix-httpapi-client-error-content-type.md new file mode 100644 index 000000000..2de77be2c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-httpapi-client-error-content-type.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix HttpApi client error decoding. + +Generated clients previously combined every error schema for a status into one union decoder. When schemas used different encodings, their declaration order could determine the decoded error instead of the response `Content-Type`; for example, a text decoder could accept a JSON response before the JSON decoder was tried. + +Error responses are now grouped and selected by normalized content type, matching buffered success responses. Normalization happens before grouping, so declarations that differ only by casing or parameters such as `charset` share one union decoder instead of making later schemas unreachable. + +No-content schemas are represented by a headerless alternative, allowing empty error responses without a `Content-Type` header to decode correctly. Unsupported content types preserve the existing combination of `StatusCodeError` and the response decoding failure. diff --git a/.context/effect/.changeset/fix-httpapi-endpoint-error-inference.md b/.context/effect/.changeset/pre/fix-httpapi-endpoint-error-inference.md similarity index 100% rename from .context/effect/.changeset/fix-httpapi-endpoint-error-inference.md rename to .context/effect/.changeset/pre/fix-httpapi-endpoint-error-inference.md diff --git a/.context/effect/.changeset/fix-httpapi-malformed-json-400.md b/.context/effect/.changeset/pre/fix-httpapi-malformed-json-400.md similarity index 100% rename from .context/effect/.changeset/fix-httpapi-malformed-json-400.md rename to .context/effect/.changeset/pre/fix-httpapi-malformed-json-400.md diff --git a/.context/effect/.changeset/pre/fix-httpapi-runtime-shape.md b/.context/effect/.changeset/pre/fix-httpapi-runtime-shape.md new file mode 100644 index 000000000..a8c9bd869 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-httpapi-runtime-shape.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApi.make` so it stores the API identifier and starts with an empty `groups` object instead of a `Map`. This makes empty APIs match the shape they have after groups are added. diff --git a/.context/effect/.changeset/fix-httpapi-schema-types.md b/.context/effect/.changeset/pre/fix-httpapi-schema-types.md similarity index 100% rename from .context/effect/.changeset/fix-httpapi-schema-types.md rename to .context/effect/.changeset/pre/fix-httpapi-schema-types.md diff --git a/.context/effect/.changeset/fix-httpapi-security-middleware-cache.md b/.context/effect/.changeset/pre/fix-httpapi-security-middleware-cache.md similarity index 100% rename from .context/effect/.changeset/fix-httpapi-security-middleware-cache.md rename to .context/effect/.changeset/pre/fix-httpapi-security-middleware-cache.md diff --git a/.context/effect/.changeset/pre/fix-httpapi-single-array-query.md b/.context/effect/.changeset/pre/fix-httpapi-single-array-query.md new file mode 100644 index 000000000..6a8a671eb --- /dev/null +++ b/.context/effect/.changeset/pre/fix-httpapi-single-array-query.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpApi` query decoding for array parameters with a single value. diff --git a/.context/effect/.changeset/pre/fix-invalid-value-doubled-expected.md b/.context/effect/.changeset/pre/fix-invalid-value-doubled-expected.md new file mode 100644 index 000000000..a8e9deaee --- /dev/null +++ b/.context/effect/.changeset/pre/fix-invalid-value-doubled-expected.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix doubled `Expected: Expected ...` prefixes in CLI `InvalidValue` error messages, closes #6312. diff --git a/.context/effect/.changeset/fix-is-json-dag.md b/.context/effect/.changeset/pre/fix-is-json-dag.md similarity index 100% rename from .context/effect/.changeset/fix-is-json-dag.md rename to .context/effect/.changeset/pre/fix-is-json-dag.md diff --git a/.context/effect/.changeset/pre/fix-iterable-flatten-stack-safety.md b/.context/effect/.changeset/pre/fix-iterable-flatten-stack-safety.md new file mode 100644 index 000000000..54894d0c6 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-iterable-flatten-stack-safety.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `Iterable.flatten` stack safe across empty iterables. diff --git a/.context/effect/.changeset/fix-json-schema-anyof-oneof-siblings.md b/.context/effect/.changeset/pre/fix-json-schema-anyof-oneof-siblings.md similarity index 100% rename from .context/effect/.changeset/fix-json-schema-anyof-oneof-siblings.md rename to .context/effect/.changeset/pre/fix-json-schema-anyof-oneof-siblings.md diff --git a/.context/effect/.changeset/fix-json-schema-import-json.md b/.context/effect/.changeset/pre/fix-json-schema-import-json.md similarity index 100% rename from .context/effect/.changeset/fix-json-schema-import-json.md rename to .context/effect/.changeset/pre/fix-json-schema-import-json.md diff --git a/.context/effect/.changeset/pre/fix-json-schema-unique-items-false.md b/.context/effect/.changeset/pre/fix-json-schema-unique-items-false.md new file mode 100644 index 000000000..0365d9dc5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-json-schema-unique-items-false.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore `uniqueItems` when set to `false` while importing JSON Schema documents. diff --git a/.context/effect/.changeset/fix-keepalive-blocked-timers.md b/.context/effect/.changeset/pre/fix-keepalive-blocked-timers.md similarity index 100% rename from .context/effect/.changeset/fix-keepalive-blocked-timers.md rename to .context/effect/.changeset/pre/fix-keepalive-blocked-timers.md diff --git a/.context/effect/.changeset/pre/fix-language-model-stream-concurrency.md b/.context/effect/.changeset/pre/fix-language-model-stream-concurrency.md new file mode 100644 index 000000000..491b4fdcd --- /dev/null +++ b/.context/effect/.changeset/pre/fix-language-model-stream-concurrency.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `LanguageModel.streamText` to apply the configured concurrency limit to tool call resolution, including approval checks. diff --git a/.context/effect/.changeset/pre/fix-latch-stale-flush.md b/.context/effect/.changeset/pre/fix-latch-stale-flush.md new file mode 100644 index 000000000..c36527840 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-latch-stale-flush.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Fix Latch open/release resuming waiters that registered after a subsequent close. + +`Latch.open` and `Latch.release` schedule the waiter flush on the fiber's +dispatcher. Previously the flush drained whatever waiters existed at flush +time, so a waiter that registered after the latch was closed again could be +resumed by the stale flush. The waiters are now snapshotted at schedule time, +so only waiters covered by an `open`/`release` call are resumed. diff --git a/.context/effect/.changeset/pre/fix-layermap-preload.md b/.context/effect/.changeset/pre/fix-layermap-preload.md new file mode 100644 index 000000000..6b7260734 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-layermap-preload.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `LayerMap` preload options so configured entries are acquired during construction. diff --git a/.context/effect/.changeset/pre/fix-mcp-call-tool-arguments.md b/.context/effect/.changeset/pre/fix-mcp-call-tool-arguments.md new file mode 100644 index 000000000..952f6d0da --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mcp-call-tool-arguments.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow MCP tool calls to omit optional arguments. diff --git a/.context/effect/.changeset/pre/fix-mcp-completion-context.md b/.context/effect/.changeset/pre/fix-mcp-completion-context.md new file mode 100644 index 000000000..82c30fc89 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mcp-completion-context.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP completion handlers now receive resolved argument context, and completion responses are limited to one hundred values. diff --git a/.context/effect/.changeset/fix-mcp-param-name-resolution.md b/.context/effect/.changeset/pre/fix-mcp-param-name-resolution.md similarity index 100% rename from .context/effect/.changeset/fix-mcp-param-name-resolution.md rename to .context/effect/.changeset/pre/fix-mcp-param-name-resolution.md diff --git a/.context/effect/.changeset/pre/fix-mcp-request-errors.md b/.context/effect/.changeset/pre/fix-mcp-request-errors.md new file mode 100644 index 000000000..7da53c303 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mcp-request-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now return protocol errors for invalid tool, prompt, completion, resource, and logging requests. diff --git a/.context/effect/.changeset/pre/fix-memory-journal-conflicts.md b/.context/effect/.changeset/pre/fix-memory-journal-conflicts.md new file mode 100644 index 000000000..ae2c2ddd5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-memory-journal-conflicts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix memory journal conflict detection skipping the first newer entry. diff --git a/.context/effect/.changeset/pre/fix-memory-journal-next-sequence.md b/.context/effect/.changeset/pre/fix-memory-journal-next-sequence.md new file mode 100644 index 000000000..f515118f0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-memory-journal-next-sequence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Return the first unused remote sequence from the in-memory event journal. diff --git a/.context/effect/.changeset/pre/fix-memory-journal-relay.md b/.context/effect/.changeset/pre/fix-memory-journal-relay.md new file mode 100644 index 000000000..cee740fdd --- /dev/null +++ b/.context/effect/.changeset/pre/fix-memory-journal-relay.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Relay entries imported into an in-memory event journal to other remotes. diff --git a/.context/effect/.changeset/pre/fix-memory-runner-health.md b/.context/effect/.changeset/pre/fix-memory-runner-health.md new file mode 100644 index 000000000..77a0e9ce8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-memory-runner-health.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve and update runner health in the in-memory cluster runner storage. diff --git a/.context/effect/.changeset/fix-mermaid-escape-special-chars.md b/.context/effect/.changeset/pre/fix-mermaid-escape-special-chars.md similarity index 100% rename from .context/effect/.changeset/fix-mermaid-escape-special-chars.md rename to .context/effect/.changeset/pre/fix-mermaid-escape-special-chars.md diff --git a/.context/effect/.changeset/pre/fix-message-storage-clear-address-dedup.md b/.context/effect/.changeset/pre/fix-message-storage-clear-address-dedup.md new file mode 100644 index 000000000..f8d75f32a --- /dev/null +++ b/.context/effect/.changeset/pre/fix-message-storage-clear-address-dedup.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Clear in-memory message primary-key indexes when clearing an entity address. diff --git a/.context/effect/.changeset/pre/fix-metric-attribute-key-collisions.md b/.context/effect/.changeset/pre/fix-metric-attribute-key-collisions.md new file mode 100644 index 000000000..3d00cc8e7 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-metric-attribute-key-collisions.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent distinct metric attribute sets from sharing registry state. diff --git a/.context/effect/.changeset/pre/fix-metric-is-metric.md b/.context/effect/.changeset/pre/fix-metric-is-metric.md new file mode 100644 index 000000000..00e185430 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-metric-is-metric.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Metric.isMetric` to recognize metrics using their current runtime brand. diff --git a/.context/effect/.changeset/pre/fix-metric-linear-boundaries.md b/.context/effect/.changeset/pre/fix-metric-linear-boundaries.md new file mode 100644 index 000000000..d5a0219a4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-metric-linear-boundaries.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Metric.linearBoundaries` to space boundaries by the configured width. diff --git a/.context/effect/.changeset/pre/fix-metric-negative-max.md b/.context/effect/.changeset/pre/fix-metric-negative-max.md new file mode 100644 index 000000000..a2f1d2450 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-metric-negative-max.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix histogram and summary maximum values for negative-only observations. diff --git a/.context/effect/.changeset/pre/fix-mssql-multitable-persistence-upsert.md b/.context/effect/.changeset/pre/fix-mssql-multitable-persistence-upsert.md new file mode 100644 index 000000000..8c870fa22 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mssql-multitable-persistence-upsert.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Generate valid MSSQL upserts for multi-table persistence. diff --git a/.context/effect/.changeset/pre/fix-multipart-file-stream-limits.md b/.context/effect/.changeset/pre/fix-multipart-file-stream-limits.md new file mode 100644 index 000000000..fd4d227f4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-multipart-file-stream-limits.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Terminate active multipart file streams when a parser limit is exceeded or the body ends unexpectedly, so file parts fail instead of hanging. diff --git a/.context/effect/.changeset/pre/fix-mutable-list-bounds.md b/.context/effect/.changeset/pre/fix-mutable-list-bounds.md new file mode 100644 index 000000000..d481f7a74 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mutable-list-bounds.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `MutableList.prepend` on empty lists and handle non-positive `toArrayN` bounds. diff --git a/.context/effect/.changeset/pre/fix-mutable-list-empty-filter.md b/.context/effect/.changeset/pre/fix-mutable-list-empty-filter.md new file mode 100644 index 000000000..4352c7314 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-mutable-list-empty-filter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `MutableList.filter` leaving an invalid empty bucket when no values match. diff --git a/.context/effect/.changeset/fix-mutable-list-filter-length.md b/.context/effect/.changeset/pre/fix-mutable-list-filter-length.md similarity index 100% rename from .context/effect/.changeset/fix-mutable-list-filter-length.md rename to .context/effect/.changeset/pre/fix-mutable-list-filter-length.md diff --git a/.context/effect/.changeset/pre/fix-ndjson-split-utf8.md b/.context/effect/.changeset/pre/fix-ndjson-split-utf8.md new file mode 100644 index 000000000..4474398b4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-ndjson-split-utf8.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Decode split UTF-8 sequences correctly in NDJSON streams. diff --git a/.context/effect/.changeset/pre/fix-node-http-stream-failure.md b/.context/effect/.changeset/pre/fix-node-http-stream-failure.md new file mode 100644 index 000000000..b1049b475 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-node-http-stream-failure.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Fix Node HTTP client requests hanging when a streamed request body fails. diff --git a/.context/effect/.changeset/pre/fix-node-path-file-url-flavor.md b/.context/effect/.changeset/pre/fix-node-path-file-url-flavor.md new file mode 100644 index 000000000..d1cb74f62 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-node-path-file-url-flavor.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +NodePath: `layerPosix` and `layerWin32` now convert between paths and `file:` URLs using their own platform flavor instead of the host's. diff --git a/.context/effect/.changeset/pre/fix-node-pipeline-kill.md b/.context/effect/.changeset/pre/fix-node-pipeline-kill.md new file mode 100644 index 000000000..b551d51da --- /dev/null +++ b/.context/effect/.changeset/pre/fix-node-pipeline-kill.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Kill every process in a Node child process pipeline when killing its aggregate handle. diff --git a/.context/effect/.changeset/pre/fix-number-remainder-scientific-notation.md b/.context/effect/.changeset/pre/fix-number-remainder-scientific-notation.md new file mode 100644 index 000000000..9b43310d2 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-number-remainder-scientific-notation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Number.remainder` for very small and large values formatted in scientific notation. diff --git a/.context/effect/.changeset/fix-object-keyword-json-schema.md b/.context/effect/.changeset/pre/fix-object-keyword-json-schema.md similarity index 100% rename from .context/effect/.changeset/fix-object-keyword-json-schema.md rename to .context/effect/.changeset/pre/fix-object-keyword-json-schema.md diff --git a/.context/effect/.changeset/pre/fix-one-shot-iterables.md b/.context/effect/.changeset/pre/fix-one-shot-iterables.md new file mode 100644 index 000000000..9908168c1 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-one-shot-iterables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix one-shot iterable handling in Array.rotate, Iterable.cartesian, and in-memory RunnerStorage acquisition diff --git a/.context/effect/.changeset/pre/fix-openai-header-redaction.md b/.context/effect/.changeset/pre/fix-openai-header-redaction.md new file mode 100644 index 000000000..1357ea76b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-header-redaction.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Redact OpenAI organization and project headers from client errors. diff --git a/.context/effect/.changeset/fix-openai-mcp-tool-names.md b/.context/effect/.changeset/pre/fix-openai-mcp-tool-names.md similarity index 100% rename from .context/effect/.changeset/fix-openai-mcp-tool-names.md rename to .context/effect/.changeset/pre/fix-openai-mcp-tool-names.md diff --git a/.context/effect/.changeset/pre/fix-openai-response-failure.md b/.context/effect/.changeset/pre/fix-openai-response-failure.md new file mode 100644 index 000000000..dbe8abb99 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-response-failure.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Preserve OpenAI provider errors from failed response stream events. diff --git a/.context/effect/.changeset/pre/fix-openai-specialized-tool-output.md b/.context/effect/.changeset/pre/fix-openai-specialized-tool-output.md new file mode 100644 index 000000000..ac215a269 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-specialized-tool-output.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Emit specialized OpenAI tool results only once. diff --git a/.context/effect/.changeset/pre/fix-openai-system-input-text.md b/.context/effect/.changeset/pre/fix-openai-system-input-text.md new file mode 100644 index 000000000..830f6d00e --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-system-input-text.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Encode OpenAI Responses API system messages as typed input text content. diff --git a/.context/effect/.changeset/pre/fix-openai-telemetry-response-namespace.md b/.context/effect/.changeset/pre/fix-openai-telemetry-response-namespace.md new file mode 100644 index 000000000..d0488f9d6 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-telemetry-response-namespace.md @@ -0,0 +1,6 @@ +--- +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +--- + +Fix OpenAI response telemetry attribute types to use the emitted response namespace. diff --git a/.context/effect/.changeset/pre/fix-openai-web-search-action.md b/.context/effect/.changeset/pre/fix-openai-web-search-action.md new file mode 100644 index 000000000..ac95aedf1 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openai-web-search-action.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Fix OpenAI stable web search response decoding by preserving the provider action in tool call parameters. diff --git a/.context/effect/.changeset/pre/fix-openapi-from-api-cache-copy.md b/.context/effect/.changeset/pre/fix-openapi-from-api-cache-copy.md new file mode 100644 index 000000000..68a5c04f4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-openapi-from-api-cache-copy.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Return fresh OpenAPI specs from cached `OpenApi.fromApi` calls. diff --git a/.context/effect/.changeset/fix-openapi-generator-form-urlencoded.md b/.context/effect/.changeset/pre/fix-openapi-generator-form-urlencoded.md similarity index 100% rename from .context/effect/.changeset/fix-openapi-generator-form-urlencoded.md rename to .context/effect/.changeset/pre/fix-openapi-generator-form-urlencoded.md diff --git a/.context/effect/.changeset/fix-openapi-generator-swagger2openapi.md b/.context/effect/.changeset/pre/fix-openapi-generator-swagger2openapi.md similarity index 100% rename from .context/effect/.changeset/fix-openapi-generator-swagger2openapi.md rename to .context/effect/.changeset/pre/fix-openapi-generator-swagger2openapi.md diff --git a/.context/effect/.changeset/fix-openapi-preserve-multiple-response-content-types.md b/.context/effect/.changeset/pre/fix-openapi-preserve-multiple-response-content-types.md similarity index 100% rename from .context/effect/.changeset/fix-openapi-preserve-multiple-response-content-types.md rename to .context/effect/.changeset/pre/fix-openapi-preserve-multiple-response-content-types.md diff --git a/.context/effect/.changeset/fix-openrouter-sparse-array.md b/.context/effect/.changeset/pre/fix-openrouter-sparse-array.md similarity index 100% rename from .context/effect/.changeset/fix-openrouter-sparse-array.md rename to .context/effect/.changeset/pre/fix-openrouter-sparse-array.md diff --git a/.context/effect/.changeset/pre/fix-otel-logger-clock-skew.md b/.context/effect/.changeset/pre/fix-otel-logger-clock-skew.md new file mode 100644 index 000000000..5783219d7 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-otel-logger-clock-skew.md @@ -0,0 +1,7 @@ +--- +"@effect/opentelemetry": patch +--- + +Use the Effect wall clock for log timestamps to match span timestamps. + +The Logger used `Date.now()` directly for log `timestamp` while the Tracer used `clock.currentTimeNanosUnsafe()` for span `startTime`. These could diverge when the high-resolution wall-clock origin drifted, causing logs to appear before their parent span. Both now use the same Effect wall clock via `nanosToHrTime(clock.currentTimeNanosUnsafe())`. diff --git a/.context/effect/.changeset/fix-otel-logger-severity-number.md b/.context/effect/.changeset/pre/fix-otel-logger-severity-number.md similarity index 100% rename from .context/effect/.changeset/fix-otel-logger-severity-number.md rename to .context/effect/.changeset/pre/fix-otel-logger-severity-number.md diff --git a/.context/effect/.changeset/pre/fix-otel-logger-shutdown.md b/.context/effect/.changeset/pre/fix-otel-logger-shutdown.md new file mode 100644 index 000000000..d81109863 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-otel-logger-shutdown.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Ensure logger providers shut down when flushing fails. diff --git a/.context/effect/.changeset/pre/fix-otlp-exporter-shutdown.md b/.context/effect/.changeset/pre/fix-otlp-exporter-shutdown.md new file mode 100644 index 000000000..444ba3ce4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-otlp-exporter-shutdown.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix OTLP exporter shutdown to await in-flight and final buffered exports up to the configured shutdown timeout. diff --git a/.context/effect/.changeset/pre/fix-otlp-resource-attributes.md b/.context/effect/.changeset/pre/fix-otlp-resource-attributes.md new file mode 100644 index 000000000..a461bfa33 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-otlp-resource-attributes.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `OtlpResource` to decode percent-encoded environment attributes and preserve bigint precision. diff --git a/.context/effect/.changeset/pre/fix-pending-interruptible-mask.md b/.context/effect/.changeset/pre/fix-pending-interruptible-mask.md new file mode 100644 index 000000000..e0a8494e4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-pending-interruptible-mask.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Deliver pending interrupts when interruptibleMask restores fiber interruptibility. diff --git a/.context/effect/.changeset/pre/fix-persisted-cluster-reply-hang.md b/.context/effect/.changeset/pre/fix-persisted-cluster-reply-hang.md new file mode 100644 index 000000000..2a1b8732f --- /dev/null +++ b/.context/effect/.changeset/pre/fix-persisted-cluster-reply-hang.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Persist a serializable defect when a cluster reply cannot be encoded, preventing persisted entity callers from hanging. diff --git a/.context/effect/.changeset/pre/fix-persisted-queue-attempt-accounting.md b/.context/effect/.changeset/pre/fix-persisted-queue-attempt-accounting.md new file mode 100644 index 000000000..8737be085 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-persisted-queue-attempt-accounting.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `PersistedQueue` to count schema decoding and malformed SQL payload failures as processing attempts. diff --git a/.context/effect/.changeset/pre/fix-prompt-all-iterables.md b/.context/effect/.changeset/pre/fix-prompt-all-iterables.md new file mode 100644 index 000000000..070620a8c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-prompt-all-iterables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Support empty records and non-array iterables in `Prompt.all`. diff --git a/.context/effect/.changeset/pre/fix-proto-record-assignment.md b/.context/effect/.changeset/pre/fix-proto-record-assignment.md new file mode 100644 index 000000000..1d960ffa4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-proto-record-assignment.md @@ -0,0 +1,16 @@ +--- +"effect": patch +"@effect/ai-anthropic": patch +"@effect/ai-openai": patch +"@effect/ai-openai-compat": patch +"@effect/ai-openrouter": patch +"@effect/docgen": patch +"@effect/openapi-generator": patch +"@effect/opentelemetry": patch +"@effect/sql-mssql": patch +"@effect/sql-sqlite-do": patch +"@effect/sql-sqlite-wasm": patch +"@effect/vitest": patch +--- + +Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. diff --git a/.context/effect/.changeset/pre/fix-pubsub-replay-retention.md b/.context/effect/.changeset/pre/fix-pubsub-replay-retention.md new file mode 100644 index 000000000..632765fdc --- /dev/null +++ b/.context/effect/.changeset/pre/fix-pubsub-replay-retention.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent replay-enabled PubSubs from retaining values beyond each subscription's replay window. diff --git a/.context/effect/.changeset/fix-queue-collect-duplication.md b/.context/effect/.changeset/pre/fix-queue-collect-duplication.md similarity index 100% rename from .context/effect/.changeset/fix-queue-collect-duplication.md rename to .context/effect/.changeset/pre/fix-queue-collect-duplication.md diff --git a/.context/effect/.changeset/fix-random-string-seeds.md b/.context/effect/.changeset/pre/fix-random-string-seeds.md similarity index 100% rename from .context/effect/.changeset/fix-random-string-seeds.md rename to .context/effect/.changeset/pre/fix-random-string-seeds.md diff --git a/.context/effect/.changeset/pre/fix-rate-limiter-sleep.md b/.context/effect/.changeset/pre/fix-rate-limiter-sleep.md new file mode 100644 index 000000000..73633920b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-rate-limiter-sleep.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `RateLimiter.makeSleep` to `RateLimiter.sleep` and support self-first partially applied and uncurried usage. diff --git a/.context/effect/.changeset/fix-ratelimiter-tokenbucket-redis-ttl.md b/.context/effect/.changeset/pre/fix-ratelimiter-tokenbucket-redis-ttl.md similarity index 100% rename from .context/effect/.changeset/fix-ratelimiter-tokenbucket-redis-ttl.md rename to .context/effect/.changeset/pre/fix-ratelimiter-tokenbucket-redis-ttl.md diff --git a/.context/effect/.changeset/pre/fix-react-native-sqlite-values.md b/.context/effect/.changeset/pre/fix-react-native-sqlite-values.md new file mode 100644 index 000000000..97070d9b0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-react-native-sqlite-values.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-react-native": patch +--- + +Return selected rows from synchronous and asynchronous value queries. diff --git a/.context/effect/.changeset/pre/fix-reactive-query-metadata.md b/.context/effect/.changeset/pre/fix-reactive-query-metadata.md new file mode 100644 index 000000000..6617c533b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-reactive-query-metadata.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve serialization and retention metadata on reactive `AtomRpc` and `AtomHttpApi` queries. diff --git a/.context/effect/.changeset/pre/fix-redis-persisted-queue.md b/.context/effect/.changeset/pre/fix-redis-persisted-queue.md new file mode 100644 index 000000000..8ec524967 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-redis-persisted-queue.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Redis-backed `PersistedQueue` reset and failed-item handling. diff --git a/.context/effect/.changeset/fix-remainder-scientific-notation.md b/.context/effect/.changeset/pre/fix-remainder-scientific-notation.md similarity index 100% rename from .context/effect/.changeset/fix-remainder-scientific-notation.md rename to .context/effect/.changeset/pre/fix-remainder-scientific-notation.md diff --git a/.context/effect/.changeset/fix-request-resolver-pending-batches-leak.md b/.context/effect/.changeset/pre/fix-request-resolver-pending-batches-leak.md similarity index 100% rename from .context/effect/.changeset/fix-request-resolver-pending-batches-leak.md rename to .context/effect/.changeset/pre/fix-request-resolver-pending-batches-leak.md diff --git a/.context/effect/.changeset/fix-retry-transient-autocomplete.md b/.context/effect/.changeset/pre/fix-retry-transient-autocomplete.md similarity index 100% rename from .context/effect/.changeset/fix-retry-transient-autocomplete.md rename to .context/effect/.changeset/pre/fix-retry-transient-autocomplete.md diff --git a/.context/effect/.changeset/fix-rpc-http-requestids-finalizer.md b/.context/effect/.changeset/pre/fix-rpc-http-requestids-finalizer.md similarity index 100% rename from .context/effect/.changeset/fix-rpc-http-requestids-finalizer.md rename to .context/effect/.changeset/pre/fix-rpc-http-requestids-finalizer.md diff --git a/.context/effect/.changeset/fix-rpc-json-id-edges.md b/.context/effect/.changeset/pre/fix-rpc-json-id-edges.md similarity index 100% rename from .context/effect/.changeset/fix-rpc-json-id-edges.md rename to .context/effect/.changeset/pre/fix-rpc-json-id-edges.md diff --git a/.context/effect/.changeset/fix-rpc-unknown-tag-isolation.md b/.context/effect/.changeset/pre/fix-rpc-unknown-tag-isolation.md similarity index 100% rename from .context/effect/.changeset/fix-rpc-unknown-tag-isolation.md rename to .context/effect/.changeset/pre/fix-rpc-unknown-tag-isolation.md diff --git a/.context/effect/.changeset/pre/fix-runner-stream-completion.md b/.context/effect/.changeset/pre/fix-runner-stream-completion.md new file mode 100644 index 000000000..414fefe90 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-runner-stream-completion.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +End runner streams after emitting their terminal replies. diff --git a/.context/effect/.changeset/pre/fix-schedule-during.md b/.context/effect/.changeset/pre/fix-schedule-during.md new file mode 100644 index 000000000..c881221af --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schedule-during.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schedule.during` to recur until the configured duration has elapsed. diff --git a/.context/effect/.changeset/fix-schedule-fixed-double-exec.md b/.context/effect/.changeset/pre/fix-schedule-fixed-double-exec.md similarity index 100% rename from .context/effect/.changeset/fix-schedule-fixed-double-exec.md rename to .context/effect/.changeset/pre/fix-schedule-fixed-double-exec.md diff --git a/.context/effect/.changeset/fix-schedule-reduce-sync-state.md b/.context/effect/.changeset/pre/fix-schedule-reduce-sync-state.md similarity index 100% rename from .context/effect/.changeset/fix-schedule-reduce-sync-state.md rename to .context/effect/.changeset/pre/fix-schedule-reduce-sync-state.md diff --git a/.context/effect/.changeset/fix-schema-arbitrary-exclusive-bounds.md b/.context/effect/.changeset/pre/fix-schema-arbitrary-exclusive-bounds.md similarity index 100% rename from .context/effect/.changeset/fix-schema-arbitrary-exclusive-bounds.md rename to .context/effect/.changeset/pre/fix-schema-arbitrary-exclusive-bounds.md diff --git a/.context/effect/.changeset/pre/fix-schema-bracket-prototype-pollution.md b/.context/effect/.changeset/pre/fix-schema-bracket-prototype-pollution.md new file mode 100644 index 000000000..f570f34ca --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-bracket-prototype-pollution.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix a bug where decoding bracket paths from FormData or URLSearchParams could mutate inherited object prototypes. diff --git a/.context/effect/.changeset/fix-schema-defect-message.md b/.context/effect/.changeset/pre/fix-schema-defect-message.md similarity index 100% rename from .context/effect/.changeset/fix-schema-defect-message.md rename to .context/effect/.changeset/pre/fix-schema-defect-message.md diff --git a/.context/effect/.changeset/fix-schema-encode-keys-property-keys.md b/.context/effect/.changeset/pre/fix-schema-encode-keys-property-keys.md similarity index 100% rename from .context/effect/.changeset/fix-schema-encode-keys-property-keys.md rename to .context/effect/.changeset/pre/fix-schema-encode-keys-property-keys.md diff --git a/.context/effect/.changeset/fix-schema-encodekeys-class.md b/.context/effect/.changeset/pre/fix-schema-encodekeys-class.md similarity index 100% rename from .context/effect/.changeset/fix-schema-encodekeys-class.md rename to .context/effect/.changeset/pre/fix-schema-encodekeys-class.md diff --git a/.context/effect/.changeset/fix-schema-encodekeys-struct.md b/.context/effect/.changeset/pre/fix-schema-encodekeys-struct.md similarity index 100% rename from .context/effect/.changeset/fix-schema-encodekeys-struct.md rename to .context/effect/.changeset/pre/fix-schema-encodekeys-struct.md diff --git a/.context/effect/.changeset/fix-schema-encoding-checks.md b/.context/effect/.changeset/pre/fix-schema-encoding-checks.md similarity index 100% rename from .context/effect/.changeset/fix-schema-encoding-checks.md rename to .context/effect/.changeset/pre/fix-schema-encoding-checks.md diff --git a/.context/effect/.changeset/fix-schema-identifier-expected-message.md b/.context/effect/.changeset/pre/fix-schema-identifier-expected-message.md similarity index 100% rename from .context/effect/.changeset/fix-schema-identifier-expected-message.md rename to .context/effect/.changeset/pre/fix-schema-identifier-expected-message.md diff --git a/.context/effect/.changeset/pre/fix-schema-is-json-records.md b/.context/effect/.changeset/pre/fix-schema-is-json-records.md new file mode 100644 index 000000000..5a71fc1cc --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-is-json-records.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SchemaAST.isJson` to reject class instances and other non-record objects. diff --git a/.context/effect/.changeset/fix-schema-is-uuid.md b/.context/effect/.changeset/pre/fix-schema-is-uuid.md similarity index 100% rename from .context/effect/.changeset/fix-schema-is-uuid.md rename to .context/effect/.changeset/pre/fix-schema-is-uuid.md diff --git a/.context/effect/.changeset/pre/fix-schema-json-tuple-allof.md b/.context/effect/.changeset/pre/fix-schema-json-tuple-allof.md new file mode 100644 index 000000000..cbf16877b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-json-tuple-allof.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix JSON Schema `allOf` imports for tuple intersections and preserve primitive refinements when combining literal constraints. diff --git a/.context/effect/.changeset/pre/fix-schema-make-nested-class-union.md b/.context/effect/.changeset/pre/fix-schema-make-nested-class-union.md new file mode 100644 index 000000000..cc53b35a4 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-make-nested-class-union.md @@ -0,0 +1,45 @@ +--- +"effect": patch +--- + +Fix `Schema.make` to preserve existing nested `Schema.Class` instances, including in array fields, while recursively constructing plain class inputs provided at runtime inside unions. Constructor defaults remain scoped to structural field and element occurrences, with `SchemaAST.Context.constructorDefault` representing the single default link for each occurrence. + +Optimize `Function.memoize` to use a single `WeakMap` lookup for cached values. Its callback no longer accepts `undefined` as a return type because `undefined` represents a cache miss. + +The performance of the two array paths can be reproduced by saving the following program as +`scratchpad/schema-make-6890-benchmark.ts` and running `node scratchpad/schema-make-6890-benchmark.ts` from the repository +root: + +```ts +import { Schema } from "effect" +import { performance } from "node:perf_hooks" + +class Row extends Schema.Class("Row")({ value: Schema.String }) {} +class DirectTable extends Schema.Class("DirectTable")({ rows: Schema.Array(Row) }) {} +class UnionTable extends Schema.Class("UnionTable")({ rows: Schema.Array(Schema.Union([Row])) }) {} + +const rows = Array.from({ length: 30_000 }, (_, value) => Row.make({ value: String(value) })) + +function benchmark(label: string, make: () => { readonly rows: ReadonlyArray }) { + const samples: Array = [] + for (let i = 0; i < 6; i++) { + const start = performance.now() + const result = make() + samples.push(performance.now() - start) + if (result.rows[0] !== rows[0] || result.rows.at(-1) !== rows.at(-1)) { + throw new Error(`${label} did not preserve Row identity`) + } + } + console.log(`${label}: ${samples.slice(1).map((n) => n.toFixed(3)).join(", ")} ms`) +} + +benchmark("Array(Class)", () => DirectTable.make({ rows })) +benchmark("Array(Union([Class]))", () => UnionTable.make({ rows })) +``` + +Representative local results on Node 24.12.0 (six runs, with the first discarded): + +```text +Array(Class): 0.639, 0.498, 0.447, 0.448, 0.451 ms +Array(Union([Class])): 3.141, 2.195, 2.126, 2.108, 2.057 ms +``` diff --git a/.context/effect/.changeset/fix-schema-option-non-schema-failures.md b/.context/effect/.changeset/pre/fix-schema-option-non-schema-failures.md similarity index 100% rename from .context/effect/.changeset/fix-schema-option-non-schema-failures.md rename to .context/effect/.changeset/pre/fix-schema-option-non-schema-failures.md diff --git a/.context/effect/.changeset/fix-schema-parser-checks.md b/.context/effect/.changeset/pre/fix-schema-parser-checks.md similarity index 100% rename from .context/effect/.changeset/fix-schema-parser-checks.md rename to .context/effect/.changeset/pre/fix-schema-parser-checks.md diff --git a/.context/effect/.changeset/pre/fix-schema-pattern-state.md b/.context/effect/.changeset/pre/fix-schema-pattern-state.md new file mode 100644 index 000000000..2a321daad --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-pattern-state.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make `Schema.isPattern` deterministic for regular expressions with global or sticky flags. diff --git a/.context/effect/.changeset/pre/fix-schema-representation-identifiers.md b/.context/effect/.changeset/pre/fix-schema-representation-identifiers.md new file mode 100644 index 000000000..f4a8a2391 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-representation-identifiers.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +SchemaRepresentation: generate references from encoded AST identity, suffix colliding identifiers instead of throwing, and preserve sharing across property-key context. This avoids false-positive duplicate identifier errors while keeping referentially distinct schemas addressable; generated fallback definitions now use the clearer `Encoded` suffix. diff --git a/.context/effect/.changeset/pre/fix-schema-sentinel-declaration.md b/.context/effect/.changeset/pre/fix-schema-sentinel-declaration.md new file mode 100644 index 000000000..f042aef64 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-sentinel-declaration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Mark the internal `~sentinels` Schema annotation as `@internal` so release declaration stripping removes it together with `SchemaAST.Sentinel`. This keeps the published declarations self-consistent for consumers that type-check dependencies with `skipLibCheck: false`. diff --git a/.context/effect/.changeset/pre/fix-schema-tuple-post-rest-indexing.md b/.context/effect/.changeset/pre/fix-schema-tuple-post-rest-indexing.md new file mode 100644 index 000000000..8546cc8be --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-tuple-post-rest-indexing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Schema.toFormatter` and `Schema.toEquivalence` indexing for tuples with multiple post-rest elements. diff --git a/.context/effect/.changeset/pre/fix-schema-union-dispatch-order.md b/.context/effect/.changeset/pre/fix-schema-union-dispatch-order.md new file mode 100644 index 000000000..61876f469 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-union-dispatch-order.md @@ -0,0 +1,12 @@ +--- +"effect": patch +--- + +Fix union candidate selection and decoding order so that unions now: + +- consider matches from every sentinel key instead of dropping valid members after the first match; +- reject ambiguous `oneOf` inputs when members with different sentinel keys both match; +- preserve declared member order when combining discriminated members with non-discriminated fallbacks; +- commit concurrent decoding results in declaration order instead of completion order. + +Reserved SSE failure event names with non-`Cause` data are now emitted as application events instead of producing a runtime defect. diff --git a/.context/effect/.changeset/pre/fix-schema-union-pruning.md b/.context/effect/.changeset/pre/fix-schema-union-pruning.md new file mode 100644 index 000000000..c1029ae56 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-schema-union-pruning.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Union candidate selection for recovering middleware and suspended members. diff --git a/.context/effect/.changeset/pre/fix-scoped-ref-failed-replacement.md b/.context/effect/.changeset/pre/fix-scoped-ref-failed-replacement.md new file mode 100644 index 000000000..5caffbbca --- /dev/null +++ b/.context/effect/.changeset/pre/fix-scoped-ref-failed-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep the current `ScopedRef` resource alive when acquiring its replacement fails. diff --git a/.context/effect/.changeset/fix-searchparam-initial-decode.md b/.context/effect/.changeset/pre/fix-searchparam-initial-decode.md similarity index 100% rename from .context/effect/.changeset/fix-searchparam-initial-decode.md rename to .context/effect/.changeset/pre/fix-searchparam-initial-decode.md diff --git a/.context/effect/.changeset/pre/fix-semaphore-with-permits-interrupt-leak.md b/.context/effect/.changeset/pre/fix-semaphore-with-permits-interrupt-leak.md new file mode 100644 index 000000000..aba537cb5 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-semaphore-with-permits-interrupt-leak.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Semaphore.withPermits` leaking permits when interrupted between acquiring them and installing their release. diff --git a/.context/effect/.changeset/fix-serializable-wire-transfer.md b/.context/effect/.changeset/pre/fix-serializable-wire-transfer.md similarity index 100% rename from .context/effect/.changeset/fix-serializable-wire-transfer.md rename to .context/effect/.changeset/pre/fix-serializable-wire-transfer.md diff --git a/.context/effect/.changeset/pre/fix-sliding-size-chunks.md b/.context/effect/.changeset/pre/fix-sliding-size-chunks.md new file mode 100644 index 000000000..9d06bc18d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-sliding-size-chunks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.slidingSize` to produce the same windows regardless of upstream chunk boundaries. diff --git a/.context/effect/.changeset/pre/fix-sql-persisted-queue-lock-refresh.md b/.context/effect/.changeset/pre/fix-sql-persisted-queue-lock-refresh.md new file mode 100644 index 000000000..19a98949b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-sql-persisted-queue-lock-refresh.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix SQL-backed persisted queues to refresh locks for actively acquired elements. diff --git a/.context/effect/.changeset/pre/fix-sse-last-event-id.md b/.context/effect/.changeset/pre/fix-sse-last-event-id.md new file mode 100644 index 000000000..69109fa35 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-sse-last-event-id.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Retain the last SSE event ID across dispatched events. diff --git a/.context/effect/.changeset/pre/fix-sse-leading-bom.md b/.context/effect/.changeset/pre/fix-sse-leading-bom.md new file mode 100644 index 000000000..c1b7716c8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-sse-leading-bom.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Recognize and ignore a leading UTF-8 byte order mark in server-sent event streams. diff --git a/.context/effect/.changeset/pre/fix-sse-retry-directives.md b/.context/effect/.changeset/pre/fix-sse-retry-directives.md new file mode 100644 index 000000000..e80c35962 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-sse-retry-directives.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore malformed retry directives when parsing server-sent event streams. diff --git a/.context/effect/.changeset/pre/fix-stream-aggregate-within-idle.md b/.context/effect/.changeset/pre/fix-stream-aggregate-within-idle.md new file mode 100644 index 000000000..96744190c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-stream-aggregate-within-idle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.aggregateWithin` and `Stream.groupedWithin` retaining fiber continuations on every schedule tick while upstream is idle. diff --git a/.context/effect/.changeset/pre/fix-stream-execution-plan-retries.md b/.context/effect/.changeset/pre/fix-stream-execution-plan-retries.md new file mode 100644 index 000000000..0174fbabe --- /dev/null +++ b/.context/effect/.changeset/pre/fix-stream-execution-plan-retries.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.withExecutionPlan` retry limits resetting after partial stream emissions. diff --git a/.context/effect/.changeset/fix-stream-grouped-within-flush.md b/.context/effect/.changeset/pre/fix-stream-grouped-within-flush.md similarity index 100% rename from .context/effect/.changeset/fix-stream-grouped-within-flush.md rename to .context/effect/.changeset/pre/fix-stream-grouped-within-flush.md diff --git a/.context/effect/.changeset/pre/fix-stream-haltwhen.md b/.context/effect/.changeset/pre/fix-stream-haltwhen.md new file mode 100644 index 000000000..8cb383c06 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-stream-haltwhen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Stream.haltWhen` to observe halt effects at pull boundaries for synchronous streams. diff --git a/.context/effect/.changeset/pre/fix-stream-map-accum-array-effect.md b/.context/effect/.changeset/pre/fix-stream-map-accum-array-effect.md new file mode 100644 index 000000000..c67a81799 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-stream-map-accum-array-effect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix data-first dispatch for `Stream.mapAccumArrayEffect`. diff --git a/.context/effect/.changeset/pre/fix-stream-range-zero-chunk.md b/.context/effect/.changeset/pre/fix-stream-range-zero-chunk.md new file mode 100644 index 000000000..cc28d66a7 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-stream-range-zero-chunk.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `Stream.range` emits the full range when the chunk size is zero. diff --git a/.context/effect/.changeset/fix-stream-run-for-each-while.md b/.context/effect/.changeset/pre/fix-stream-run-for-each-while.md similarity index 100% rename from .context/effect/.changeset/fix-stream-run-for-each-while.md rename to .context/effect/.changeset/pre/fix-stream-run-for-each-while.md diff --git a/.context/effect/.changeset/fix-stream-scan-effect.md b/.context/effect/.changeset/pre/fix-stream-scan-effect.md similarity index 100% rename from .context/effect/.changeset/fix-stream-scan-effect.md rename to .context/effect/.changeset/pre/fix-stream-scan-effect.md diff --git a/.context/effect/.changeset/fix-stream-scoped-scope.md b/.context/effect/.changeset/pre/fix-stream-scoped-scope.md similarity index 100% rename from .context/effect/.changeset/fix-stream-scoped-scope.md rename to .context/effect/.changeset/pre/fix-stream-scoped-scope.md diff --git a/.context/effect/.changeset/fix-string-case-digits.md b/.context/effect/.changeset/pre/fix-string-case-digits.md similarity index 100% rename from .context/effect/.changeset/fix-string-case-digits.md rename to .context/effect/.changeset/pre/fix-string-case-digits.md diff --git a/.context/effect/.changeset/fix-strip-approval-artifacts-multi-round.md b/.context/effect/.changeset/pre/fix-strip-approval-artifacts-multi-round.md similarity index 100% rename from .context/effect/.changeset/fix-strip-approval-artifacts-multi-round.md rename to .context/effect/.changeset/pre/fix-strip-approval-artifacts-multi-round.md diff --git a/.context/effect/.changeset/fix-struct-utility-types-simplify.md b/.context/effect/.changeset/pre/fix-struct-utility-types-simplify.md similarity index 100% rename from .context/effect/.changeset/fix-struct-utility-types-simplify.md rename to .context/effect/.changeset/pre/fix-struct-utility-types-simplify.md diff --git a/.context/effect/.changeset/fix-structural-proto-equality.md b/.context/effect/.changeset/pre/fix-structural-proto-equality.md similarity index 100% rename from .context/effect/.changeset/fix-structural-proto-equality.md rename to .context/effect/.changeset/pre/fix-structural-proto-equality.md diff --git a/.context/effect/.changeset/fix-structwithrest-index-signatures.md b/.context/effect/.changeset/pre/fix-structwithrest-index-signatures.md similarity index 100% rename from .context/effect/.changeset/fix-structwithrest-index-signatures.md rename to .context/effect/.changeset/pre/fix-structwithrest-index-signatures.md diff --git a/.context/effect/.changeset/pre/fix-subscription-ref-get-and-update-some.md b/.context/effect/.changeset/pre/fix-subscription-ref-get-and-update-some.md new file mode 100644 index 000000000..d231f9c6c --- /dev/null +++ b/.context/effect/.changeset/pre/fix-subscription-ref-get-and-update-some.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SubscriptionRef.getAndUpdateSome` to return the current value when no update is selected. diff --git a/.context/effect/.changeset/pre/fix-subscriptionref-getandupdateeffect.md b/.context/effect/.changeset/pre/fix-subscriptionref-getandupdateeffect.md new file mode 100644 index 000000000..4983ed0b0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-subscriptionref-getandupdateeffect.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SubscriptionRef.getAndUpdateEffect` to execute the effectful update. diff --git a/.context/effect/.changeset/fix-tagged-union-class-sentinels.md b/.context/effect/.changeset/pre/fix-tagged-union-class-sentinels.md similarity index 100% rename from .context/effect/.changeset/fix-tagged-union-class-sentinels.md rename to .context/effect/.changeset/pre/fix-tagged-union-class-sentinels.md diff --git a/.context/effect/.changeset/fix-tagged-union-match-unify.md b/.context/effect/.changeset/pre/fix-tagged-union-match-unify.md similarity index 100% rename from .context/effect/.changeset/fix-tagged-union-match-unify.md rename to .context/effect/.changeset/pre/fix-tagged-union-match-unify.md diff --git a/.context/effect/.changeset/fix-to-tagged-union-isanyof-custom-tags.md b/.context/effect/.changeset/pre/fix-to-tagged-union-isanyof-custom-tags.md similarity index 100% rename from .context/effect/.changeset/fix-to-tagged-union-isanyof-custom-tags.md rename to .context/effect/.changeset/pre/fix-to-tagged-union-isanyof-custom-tags.md diff --git a/.context/effect/.changeset/pre/fix-tool-provider-defined-clone.md b/.context/effect/.changeset/pre/fix-tool-provider-defined-clone.md new file mode 100644 index 000000000..6306c054d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-tool-provider-defined-clone.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Tool: preserve the tool kind when cloning provider-defined and dynamic tools. + +`Tool.addDependency`, `setParameters`, `setSuccess`, `setFailure`, `annotate`, and `annotateMerge` previously rebuilt the tool as a user-defined tool, which flipped `Tool.isProviderDefined` to `false`, corrupted the provider `id` (e.g. `anthropic.memory_20250818`), and crashed `Tool.getStrictMode`. These operations now clone the tool while preserving its prototype, `id`, and kind. Provider-defined tools also now carry an empty annotations context so `Tool.getStrictMode`/`annotate` work on them. Closes #2615. diff --git a/.context/effect/.changeset/pre/fix-trie-key-replacement.md b/.context/effect/.changeset/pre/fix-trie-key-replacement.md new file mode 100644 index 000000000..00d370732 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-trie-key-replacement.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Trie.insert` to replace existing values without mutating the original trie or increasing its size. diff --git a/.context/effect/.changeset/pre/fix-trie-longest-prefix.md b/.context/effect/.changeset/pre/fix-trie-longest-prefix.md new file mode 100644 index 000000000..bf422ac6d --- /dev/null +++ b/.context/effect/.changeset/pre/fix-trie-longest-prefix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Trie.longestPrefixOf` returning a valued sibling that does not match the input key. diff --git a/.context/effect/.changeset/pre/fix-trie-undefined-values.md b/.context/effect/.changeset/pre/fix-trie-undefined-values.md new file mode 100644 index 000000000..12a9e3f13 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-trie-undefined-values.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Trie` to preserve entries whose value is `undefined`. diff --git a/.context/effect/.changeset/fix-tuple-with-rest-post-rest-index-drift.md b/.context/effect/.changeset/pre/fix-tuple-with-rest-post-rest-index-drift.md similarity index 100% rename from .context/effect/.changeset/fix-tuple-with-rest-post-rest-index-drift.md rename to .context/effect/.changeset/pre/fix-tuple-with-rest-post-rest-index-drift.md diff --git a/.context/effect/.changeset/fix-tuple-with-rest-post-rest-validation.md b/.context/effect/.changeset/pre/fix-tuple-with-rest-post-rest-validation.md similarity index 100% rename from .context/effect/.changeset/fix-tuple-with-rest-post-rest-validation.md rename to .context/effect/.changeset/pre/fix-tuple-with-rest-post-rest-validation.md diff --git a/.context/effect/.changeset/pre/fix-txpubsub-publish-all-iterables.md b/.context/effect/.changeset/pre/fix-txpubsub-publish-all-iterables.md new file mode 100644 index 000000000..390063b2b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-txpubsub-publish-all-iterables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TxPubSub.publishAll` dropping values from one-shot iterables when a transaction retries. diff --git a/.context/effect/.changeset/pre/fix-txqueue-closing-drain.md b/.context/effect/.changeset/pre/fix-txqueue-closing-drain.md new file mode 100644 index 000000000..41b0208f8 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-txqueue-closing-drain.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `TxQueue.poll` and `TxQueue.clear` complete a closing queue after draining its buffered items. diff --git a/.context/effect/.changeset/pre/fix-txqueue-offer-all-iterables.md b/.context/effect/.changeset/pre/fix-txqueue-offer-all-iterables.md new file mode 100644 index 000000000..e6f159cf1 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-txqueue-offer-all-iterables.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `TxQueue.offerAll` to preserve one-shot iterables across transaction retries and repeated runs. diff --git a/.context/effect/.changeset/fix-types-voidifempty.md b/.context/effect/.changeset/pre/fix-types-voidifempty.md similarity index 100% rename from .context/effect/.changeset/fix-types-voidifempty.md rename to .context/effect/.changeset/pre/fix-types-voidifempty.md diff --git a/.context/effect/.changeset/pre/fix-variant-schema-default-cache.md b/.context/effect/.changeset/pre/fix-variant-schema-default-cache.md new file mode 100644 index 000000000..4449cb175 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-variant-schema-default-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Separate the default `VariantSchema` cache from named variant entries. diff --git a/.context/effect/.changeset/pre/fix-vitest-proxy-chained-helpers.md b/.context/effect/.changeset/pre/fix-vitest-proxy-chained-helpers.md new file mode 100644 index 000000000..a6b5ecbf2 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-vitest-proxy-chained-helpers.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Preserve chained vitest helpers like `it.describe.each` and `it.skip.each` when accessed through the `it` proxy. Previously the proxy returned bound copies of vitest's functions, which stripped their static helper properties and caused `TypeError: it.describe.each is not a function`. diff --git a/.context/effect/.changeset/pre/fix-vitest-record-schema-arbitrary.md b/.context/effect/.changeset/pre/fix-vitest-record-schema-arbitrary.md new file mode 100644 index 000000000..840708d75 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-vitest-record-schema-arbitrary.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Fix record-form property tests to convert Schema values to FastCheck arbitraries. diff --git a/.context/effect/.changeset/pre/fix-vitest-runner-import.md b/.context/effect/.changeset/pre/fix-vitest-runner-import.md new file mode 100644 index 000000000..1f714c770 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-vitest-runner-import.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": minor +--- + +Require Vitest 4.1 or later and read suite state from `TestRunner`, removing the direct `@vitest/runner` import and support for Vitest 3 and 4.0. diff --git a/.context/effect/.changeset/pre/fix-vitest-throws-assertions.md b/.context/effect/.changeset/pre/fix-vitest-throws-assertions.md new file mode 100644 index 000000000..307bb3837 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-vitest-throws-assertions.md @@ -0,0 +1,5 @@ +--- +"@effect/vitest": patch +--- + +Ensure `throws` and `throwsAsync` fail when the supplied operation returns or resolves without throwing. diff --git a/.context/effect/.changeset/pre/fix-void-mcp-tool-results.md b/.context/effect/.changeset/pre/fix-void-mcp-tool-results.md new file mode 100644 index 000000000..5febf7769 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-void-mcp-tool-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep MCP tool calls that return void successful. diff --git a/.context/effect/.changeset/fix-void-response-encoding.md b/.context/effect/.changeset/pre/fix-void-response-encoding.md similarity index 100% rename from .context/effect/.changeset/fix-void-response-encoding.md rename to .context/effect/.changeset/pre/fix-void-response-encoding.md diff --git a/.context/effect/.changeset/pre/fix-worker-runner-cleanup.md b/.context/effect/.changeset/pre/fix-worker-runner-cleanup.md new file mode 100644 index 000000000..ea8d118e0 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-worker-runner-cleanup.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-browser": patch +"@effect/platform-bun": patch +"@effect/platform-node": patch +--- + +Fix worker runner disconnect notifications and event listener cleanup. diff --git a/.context/effect/.changeset/fix-workflow-defect-reply-serialization.md b/.context/effect/.changeset/pre/fix-workflow-defect-reply-serialization.md similarity index 100% rename from .context/effect/.changeset/fix-workflow-defect-reply-serialization.md rename to .context/effect/.changeset/pre/fix-workflow-defect-reply-serialization.md diff --git a/.context/effect/.changeset/pre/fix-workflow-entity-client-collision.md b/.context/effect/.changeset/pre/fix-workflow-entity-client-collision.md new file mode 100644 index 000000000..b682dc921 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-workflow-entity-client-collision.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix ClusterWorkflowEngine partial workflow clients colliding with full workflow clients. diff --git a/.context/effect/.changeset/fix-workflow-proxy-rpc-handler-context.md b/.context/effect/.changeset/pre/fix-workflow-proxy-rpc-handler-context.md similarity index 100% rename from .context/effect/.changeset/fix-workflow-proxy-rpc-handler-context.md rename to .context/effect/.changeset/pre/fix-workflow-proxy-rpc-handler-context.md diff --git a/.context/effect/.changeset/pre/fix-workflow-trace-context.md b/.context/effect/.changeset/pre/fix-workflow-trace-context.md new file mode 100644 index 000000000..5ede2477b --- /dev/null +++ b/.context/effect/.changeset/pre/fix-workflow-trace-context.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Propagate trace context through persisted cluster workflow requests. diff --git a/.context/effect/.changeset/pre/fix-xhr-form-data.md b/.context/effect/.changeset/pre/fix-xhr-form-data.md new file mode 100644 index 000000000..28bc21691 --- /dev/null +++ b/.context/effect/.changeset/pre/fix-xhr-form-data.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Fix form data decoding for XMLHttpRequest client responses. diff --git a/.context/effect/.changeset/flat-chicken-remain.md b/.context/effect/.changeset/pre/flat-chicken-remain.md similarity index 100% rename from .context/effect/.changeset/flat-chicken-remain.md rename to .context/effect/.changeset/pre/flat-chicken-remain.md diff --git a/.context/effect/.changeset/floppy-cows-spend.md b/.context/effect/.changeset/pre/floppy-cows-spend.md similarity index 100% rename from .context/effect/.changeset/floppy-cows-spend.md rename to .context/effect/.changeset/pre/floppy-cows-spend.md diff --git a/.context/effect/.changeset/pre/floppy-frogs-juggle.md b/.context/effect/.changeset/pre/floppy-frogs-juggle.md new file mode 100644 index 000000000..4f4f40cd4 --- /dev/null +++ b/.context/effect/.changeset/pre/floppy-frogs-juggle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Duration`'s `Hash.symbol` implementation to hash a canonical nanoseconds form instead of the raw internal `Millis`/`Nanos` representation. Two durations that `Duration.equals`/`Equal.equals` consider equal (e.g. `Duration.seconds(5)` and `Duration.nanos(5_000_000_000n)`) previously hashed differently, violating the Hash/Equal contract and silently breaking `HashSet`/`HashMap` lookups keyed by `Duration`. diff --git a/.context/effect/.changeset/floppy-items-admire.md b/.context/effect/.changeset/pre/floppy-items-admire.md similarity index 100% rename from .context/effect/.changeset/floppy-items-admire.md rename to .context/effect/.changeset/pre/floppy-items-admire.md diff --git a/.context/effect/.changeset/floppy-pigs-kiss.md b/.context/effect/.changeset/pre/floppy-pigs-kiss.md similarity index 100% rename from .context/effect/.changeset/floppy-pigs-kiss.md rename to .context/effect/.changeset/pre/floppy-pigs-kiss.md diff --git a/.context/effect/.changeset/pre/floppy-rats-leave.md b/.context/effect/.changeset/pre/floppy-rats-leave.md new file mode 100644 index 000000000..cb520a1fd --- /dev/null +++ b/.context/effect/.changeset/pre/floppy-rats-leave.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fixed Clock.sleep handling of large durations diff --git a/.context/effect/.changeset/pre/floyd-warshall-null-edge-data.md b/.context/effect/.changeset/pre/floyd-warshall-null-edge-data.md new file mode 100644 index 000000000..368716363 --- /dev/null +++ b/.context/effect/.changeset/pre/floyd-warshall-null-edge-data.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve null edge data in Graph.floydWarshall costs. diff --git a/.context/effect/.changeset/fluffy-meals-matter.md b/.context/effect/.changeset/pre/fluffy-meals-matter.md similarity index 100% rename from .context/effect/.changeset/fluffy-meals-matter.md rename to .context/effect/.changeset/pre/fluffy-meals-matter.md diff --git a/.context/effect/.changeset/fluffy-pumas-push.md b/.context/effect/.changeset/pre/fluffy-pumas-push.md similarity index 100% rename from .context/effect/.changeset/fluffy-pumas-push.md rename to .context/effect/.changeset/pre/fluffy-pumas-push.md diff --git a/.context/effect/.changeset/forked-memo-maps.md b/.context/effect/.changeset/pre/forked-memo-maps.md similarity index 100% rename from .context/effect/.changeset/forked-memo-maps.md rename to .context/effect/.changeset/pre/forked-memo-maps.md diff --git a/.context/effect/.changeset/forty-hounds-cheer.md b/.context/effect/.changeset/pre/forty-hounds-cheer.md similarity index 100% rename from .context/effect/.changeset/forty-hounds-cheer.md rename to .context/effect/.changeset/pre/forty-hounds-cheer.md diff --git a/.context/effect/.changeset/forty-otters-cry.md b/.context/effect/.changeset/pre/forty-otters-cry.md similarity index 100% rename from .context/effect/.changeset/forty-otters-cry.md rename to .context/effect/.changeset/pre/forty-otters-cry.md diff --git a/.context/effect/.changeset/forty-rings-film.md b/.context/effect/.changeset/pre/forty-rings-film.md similarity index 100% rename from .context/effect/.changeset/forty-rings-film.md rename to .context/effect/.changeset/pre/forty-rings-film.md diff --git a/.context/effect/.changeset/forty-signs-stay.md b/.context/effect/.changeset/pre/forty-signs-stay.md similarity index 100% rename from .context/effect/.changeset/forty-signs-stay.md rename to .context/effect/.changeset/pre/forty-signs-stay.md diff --git a/.context/effect/.changeset/forty-swans-divide.md b/.context/effect/.changeset/pre/forty-swans-divide.md similarity index 100% rename from .context/effect/.changeset/forty-swans-divide.md rename to .context/effect/.changeset/pre/forty-swans-divide.md diff --git a/.context/effect/.changeset/forty-trees-pay.md b/.context/effect/.changeset/pre/forty-trees-pay.md similarity index 100% rename from .context/effect/.changeset/forty-trees-pay.md rename to .context/effect/.changeset/pre/forty-trees-pay.md diff --git a/.context/effect/.changeset/four-papayas-bow.md b/.context/effect/.changeset/pre/four-papayas-bow.md similarity index 100% rename from .context/effect/.changeset/four-papayas-bow.md rename to .context/effect/.changeset/pre/four-papayas-bow.md diff --git a/.context/effect/.changeset/four-points-repeat.md b/.context/effect/.changeset/pre/four-points-repeat.md similarity index 100% rename from .context/effect/.changeset/four-points-repeat.md rename to .context/effect/.changeset/pre/four-points-repeat.md diff --git a/.context/effect/.changeset/pre/frank-apes-vanish.md b/.context/effect/.changeset/pre/frank-apes-vanish.md new file mode 100644 index 000000000..603a16fd7 --- /dev/null +++ b/.context/effect/.changeset/pre/frank-apes-vanish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Close suspended workflow scopes after resumed completion. diff --git a/.context/effect/.changeset/fresh-cats-smash.md b/.context/effect/.changeset/pre/fresh-cats-smash.md similarity index 100% rename from .context/effect/.changeset/fresh-cats-smash.md rename to .context/effect/.changeset/pre/fresh-cats-smash.md diff --git a/.context/effect/.changeset/pre/fresh-cycles-wait.md b/.context/effect/.changeset/pre/fresh-cycles-wait.md new file mode 100644 index 000000000..f588122cd --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-cycles-wait.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the `Schedule.both` APIs and add `Schedule.max` for combining schedules by their slowest delay. diff --git a/.context/effect/.changeset/fresh-emus-cheat.md b/.context/effect/.changeset/pre/fresh-emus-cheat.md similarity index 100% rename from .context/effect/.changeset/fresh-emus-cheat.md rename to .context/effect/.changeset/pre/fresh-emus-cheat.md diff --git a/.context/effect/.changeset/pre/fresh-files-seek.md b/.context/effect/.changeset/pre/fresh-files-seek.md new file mode 100644 index 000000000..51fb44445 --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-files-seek.md @@ -0,0 +1,7 @@ +--- +"effect": minor +"@effect/platform-node-shared": minor +"@effect/platform-deno": minor +--- + +Return the new file offset as a `Size` from `File.seek`. diff --git a/.context/effect/.changeset/pre/fresh-forms-travel.md b/.context/effect/.changeset/pre/fresh-forms-travel.md new file mode 100644 index 000000000..8b64d908d --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-forms-travel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve FormData bodies when converting client requests through HttpServerRequest. diff --git a/.context/effect/.changeset/pre/fresh-images-generate.md b/.context/effect/.changeset/pre/fresh-images-generate.md new file mode 100644 index 000000000..e8914d5f0 --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-images-generate.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Accept image generation-specific lifecycle statuses and nullable results in OpenAI response items. diff --git a/.context/effect/.changeset/pre/fresh-lines-wait.md b/.context/effect/.changeset/pre/fresh-lines-wait.md new file mode 100644 index 000000000..d064c998e --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-lines-wait.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +NodeTerminal: preserve buffered input across sequential `readLine` calls diff --git a/.context/effect/.changeset/fresh-monkeys-smoke.md b/.context/effect/.changeset/pre/fresh-monkeys-smoke.md similarity index 100% rename from .context/effect/.changeset/fresh-monkeys-smoke.md rename to .context/effect/.changeset/pre/fresh-monkeys-smoke.md diff --git a/.context/effect/.changeset/pre/fresh-rivers-report.md b/.context/effect/.changeset/pre/fresh-rivers-report.md new file mode 100644 index 000000000..121cea53e --- /dev/null +++ b/.context/effect/.changeset/pre/fresh-rivers-report.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Isolate delta metric baselines for each registered metric reader diff --git a/.context/effect/.changeset/frozen-intrinsics-stack-trace-limit.md b/.context/effect/.changeset/pre/frozen-intrinsics-stack-trace-limit.md similarity index 100% rename from .context/effect/.changeset/frozen-intrinsics-stack-trace-limit.md rename to .context/effect/.changeset/pre/frozen-intrinsics-stack-trace-limit.md diff --git a/.context/effect/.changeset/fruity-houses-learn.md b/.context/effect/.changeset/pre/fruity-houses-learn.md similarity index 100% rename from .context/effect/.changeset/fruity-houses-learn.md rename to .context/effect/.changeset/pre/fruity-houses-learn.md diff --git a/.context/effect/.changeset/pre/fruity-sloths-walk.md b/.context/effect/.changeset/pre/fruity-sloths-walk.md new file mode 100644 index 000000000..2e9ef2b1d --- /dev/null +++ b/.context/effect/.changeset/pre/fruity-sloths-walk.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now return standard JSON-RPC errors for malformed requests, unknown methods, and invalid parameters. diff --git a/.context/effect/.changeset/full-adults-double.md b/.context/effect/.changeset/pre/full-adults-double.md similarity index 100% rename from .context/effect/.changeset/full-adults-double.md rename to .context/effect/.changeset/pre/full-adults-double.md diff --git a/.context/effect/.changeset/funny-crabs-hang.md b/.context/effect/.changeset/pre/funny-crabs-hang.md similarity index 100% rename from .context/effect/.changeset/funny-crabs-hang.md rename to .context/effect/.changeset/pre/funny-crabs-hang.md diff --git a/.context/effect/.changeset/pre/funny-ears-beam.md b/.context/effect/.changeset/pre/funny-ears-beam.md new file mode 100644 index 000000000..00b7994c7 --- /dev/null +++ b/.context/effect/.changeset/pre/funny-ears-beam.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent Effect.updateService and Effect.updateServiceScoped supertype widening diff --git a/.context/effect/.changeset/funny-forks-move.md b/.context/effect/.changeset/pre/funny-forks-move.md similarity index 100% rename from .context/effect/.changeset/funny-forks-move.md rename to .context/effect/.changeset/pre/funny-forks-move.md diff --git a/.context/effect/.changeset/pre/fuzzy-batches-stop.md b/.context/effect/.changeset/pre/fuzzy-batches-stop.md new file mode 100644 index 000000000..0dbd54ab9 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-batches-stop.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now enforce revision-specific JSON-RPC batch and protocol-version header requirements. diff --git a/.context/effect/.changeset/pre/fuzzy-caches-expire.md b/.context/effect/.changeset/pre/fuzzy-caches-expire.md new file mode 100644 index 000000000..5e511fe65 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-caches-expire.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor numeric zero time-to-live values in `Cache.make` and `ScopedCache.make`. diff --git a/.context/effect/.changeset/fuzzy-camels-hunt.md b/.context/effect/.changeset/pre/fuzzy-camels-hunt.md similarity index 100% rename from .context/effect/.changeset/fuzzy-camels-hunt.md rename to .context/effect/.changeset/pre/fuzzy-camels-hunt.md diff --git a/.context/effect/.changeset/pre/fuzzy-cats-kill.md b/.context/effect/.changeset/pre/fuzzy-cats-kill.md new file mode 100644 index 000000000..35bae2e9f --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-cats-kill.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix child process termination to escalate to `SIGKILL` when the initial signal does not stop the process within `forceKillAfter`. diff --git a/.context/effect/.changeset/pre/fuzzy-cats-listen.md b/.context/effect/.changeset/pre/fuzzy-cats-listen.md new file mode 100644 index 000000000..eb5b34707 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-cats-listen.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `HttpRouter.toWebHandler` middleware inference to exclude request services supplied by the HTTP adapter. diff --git a/.context/effect/.changeset/fuzzy-crews-fold.md b/.context/effect/.changeset/pre/fuzzy-crews-fold.md similarity index 100% rename from .context/effect/.changeset/fuzzy-crews-fold.md rename to .context/effect/.changeset/pre/fuzzy-crews-fold.md diff --git a/.context/effect/.changeset/pre/fuzzy-databases-abort.md b/.context/effect/.changeset/pre/fuzzy-databases-abort.md new file mode 100644 index 000000000..54923cc70 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-databases-abort.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-browser": patch +--- + +Abort IndexedDB versionchange transactions when schema migrations fail. diff --git a/.context/effect/.changeset/fuzzy-dodos-help.md b/.context/effect/.changeset/pre/fuzzy-dodos-help.md similarity index 100% rename from .context/effect/.changeset/fuzzy-dodos-help.md rename to .context/effect/.changeset/pre/fuzzy-dodos-help.md diff --git a/.context/effect/.changeset/pre/fuzzy-files-slice.md b/.context/effect/.changeset/pre/fuzzy-files-slice.md new file mode 100644 index 000000000..8fde69fcc --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-files-slice.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +Honor offset and byte-count options in Bun Web File responses. diff --git a/.context/effect/.changeset/pre/fuzzy-hornets-wish.md b/.context/effect/.changeset/pre/fuzzy-hornets-wish.md new file mode 100644 index 000000000..d9d06218b --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-hornets-wish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Handle accepted undefined fields during variant extraction. diff --git a/.context/effect/.changeset/fuzzy-lions-perform.md b/.context/effect/.changeset/pre/fuzzy-lions-perform.md similarity index 100% rename from .context/effect/.changeset/fuzzy-lions-perform.md rename to .context/effect/.changeset/pre/fuzzy-lions-perform.md diff --git a/.context/effect/.changeset/pre/fuzzy-lions-study.md b/.context/effect/.changeset/pre/fuzzy-lions-study.md new file mode 100644 index 000000000..eb647e0ab --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-lions-study.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SqlResolver.findById` failing to complete duplicate requests when id encoding fails, which surfaced as a `RequestResolver did not complete request` defect instead of the underlying `SchemaError`. diff --git a/.context/effect/.changeset/pre/fuzzy-pandas-smile.md b/.context/effect/.changeset/pre/fuzzy-pandas-smile.md new file mode 100644 index 000000000..835fefafd --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-pandas-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Preserve autocomplete for known OpenAI-compatible model config properties while allowing provider-specific properties. diff --git a/.context/effect/.changeset/fuzzy-planets-sneeze.md b/.context/effect/.changeset/pre/fuzzy-planets-sneeze.md similarity index 100% rename from .context/effect/.changeset/fuzzy-planets-sneeze.md rename to .context/effect/.changeset/pre/fuzzy-planets-sneeze.md diff --git a/.context/effect/.changeset/pre/fuzzy-rabbits-cancel.md b/.context/effect/.changeset/pre/fuzzy-rabbits-cancel.md new file mode 100644 index 000000000..4060d9c86 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-rabbits-cancel.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ignore MCP cancellation notifications for unknown request identifiers. diff --git a/.context/effect/.changeset/pre/fuzzy-ravens-reason.md b/.context/effect/.changeset/pre/fuzzy-ravens-reason.md new file mode 100644 index 000000000..1858474d8 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-ravens-reason.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Fix the casing of OpenRouter reasoning-end metadata. diff --git a/.context/effect/.changeset/pre/fuzzy-routers-smile.md b/.context/effect/.changeset/pre/fuzzy-routers-smile.md new file mode 100644 index 000000000..a286099cc --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-routers-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix several edge cases in the vendored FindMyWay router. diff --git a/.context/effect/.changeset/icy-flies-cross.md b/.context/effect/.changeset/pre/fuzzy-stamps-care.md similarity index 100% rename from .context/effect/.changeset/icy-flies-cross.md rename to .context/effect/.changeset/pre/fuzzy-stamps-care.md diff --git a/.context/effect/.changeset/pre/fuzzy-timers-smile.md b/.context/effect/.changeset/pre/fuzzy-timers-smile.md new file mode 100644 index 000000000..874eef006 --- /dev/null +++ b/.context/effect/.changeset/pre/fuzzy-timers-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep TestClock nanosecond access total after infinite adjustments. diff --git a/.context/effect/.changeset/pre/giant-jeans-float.md b/.context/effect/.changeset/pre/giant-jeans-float.md new file mode 100644 index 000000000..3f0eb7ef1 --- /dev/null +++ b/.context/effect/.changeset/pre/giant-jeans-float.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure one-shot iterables work with Fiber apis diff --git a/.context/effect/.changeset/gold-meteors-move.md b/.context/effect/.changeset/pre/gold-meteors-move.md similarity index 100% rename from .context/effect/.changeset/gold-meteors-move.md rename to .context/effect/.changeset/pre/gold-meteors-move.md diff --git a/.context/effect/.changeset/gold-readers-hug.md b/.context/effect/.changeset/pre/gold-readers-hug.md similarity index 100% rename from .context/effect/.changeset/gold-readers-hug.md rename to .context/effect/.changeset/pre/gold-readers-hug.md diff --git a/.context/effect/.changeset/gold-rings-start.md b/.context/effect/.changeset/pre/gold-rings-start.md similarity index 100% rename from .context/effect/.changeset/gold-rings-start.md rename to .context/effect/.changeset/pre/gold-rings-start.md diff --git a/.context/effect/.changeset/pre/good-cups-reply.md b/.context/effect/.changeset/pre/good-cups-reply.md new file mode 100644 index 000000000..344af728a --- /dev/null +++ b/.context/effect/.changeset/pre/good-cups-reply.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mssql": patch +--- + +Return MSSQL procedure values through the output property. diff --git a/.context/effect/.changeset/good-tools-work.md b/.context/effect/.changeset/pre/good-tools-work.md similarity index 100% rename from .context/effect/.changeset/good-tools-work.md rename to .context/effect/.changeset/pre/good-tools-work.md diff --git a/.context/effect/.changeset/good-trees-pull.md b/.context/effect/.changeset/pre/good-trees-pull.md similarity index 100% rename from .context/effect/.changeset/good-trees-pull.md rename to .context/effect/.changeset/pre/good-trees-pull.md diff --git a/.context/effect/.changeset/pre/graph-acyclic-parallel-undirected-edges.md b/.context/effect/.changeset/pre/graph-acyclic-parallel-undirected-edges.md new file mode 100644 index 000000000..5bb98abbb --- /dev/null +++ b/.context/effect/.changeset/pre/graph-acyclic-parallel-undirected-edges.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.isAcyclic` to detect cycles formed by parallel undirected edges. diff --git a/.context/effect/.changeset/graph-algorithm-fixes.md b/.context/effect/.changeset/pre/graph-algorithm-fixes.md similarity index 100% rename from .context/effect/.changeset/graph-algorithm-fixes.md rename to .context/effect/.changeset/pre/graph-algorithm-fixes.md diff --git a/.context/effect/.changeset/pre/graph-finalized-mutation-handle.md b/.context/effect/.changeset/pre/graph-finalized-mutation-handle.md new file mode 100644 index 000000000..4f628c326 --- /dev/null +++ b/.context/effect/.changeset/pre/graph-finalized-mutation-handle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject `Graph` mutation operations on mutable handles after `Graph.endMutation` finalizes them. diff --git a/.context/effect/.changeset/pre/graph-guard-predicates.md b/.context/effect/.changeset/pre/graph-guard-predicates.md new file mode 100644 index 000000000..da96b1a56 --- /dev/null +++ b/.context/effect/.changeset/pre/graph-guard-predicates.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Graph.isGraph narrowing for mutable and undirected graphs. diff --git a/.context/effect/.changeset/pre/graph-sync-mutation-callbacks.md b/.context/effect/.changeset/pre/graph-sync-mutation-callbacks.md new file mode 100644 index 000000000..c006de9f8 --- /dev/null +++ b/.context/effect/.changeset/pre/graph-sync-mutation-callbacks.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject asynchronous `Graph` mutation callbacks and finalize scoped mutable handles when callbacks fail. diff --git a/.context/effect/.changeset/pre/graph-undirected-edge-equality.md b/.context/effect/.changeset/pre/graph-undirected-edge-equality.md new file mode 100644 index 000000000..25212ae68 --- /dev/null +++ b/.context/effect/.changeset/pre/graph-undirected-edge-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix undirected `Graph` equality and hashing to ignore stored edge endpoint orientation. diff --git a/.context/effect/.changeset/pre/graph-walker-iterator-receiver.md b/.context/effect/.changeset/pre/graph-walker-iterator-receiver.md new file mode 100644 index 000000000..a44ad4655 --- /dev/null +++ b/.context/effect/.changeset/pre/graph-walker-iterator-receiver.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `Graph.Walker` iteration for receiver-sensitive iterables. diff --git a/.context/effect/.changeset/great-trains-mate.md b/.context/effect/.changeset/pre/great-trains-mate.md similarity index 100% rename from .context/effect/.changeset/great-trains-mate.md rename to .context/effect/.changeset/pre/great-trains-mate.md diff --git a/.context/effect/.changeset/great-trams-report.md b/.context/effect/.changeset/pre/great-trams-report.md similarity index 100% rename from .context/effect/.changeset/great-trams-report.md rename to .context/effect/.changeset/pre/great-trams-report.md diff --git a/.context/effect/.changeset/pre/green-ads-camp.md b/.context/effect/.changeset/pre/green-ads-camp.md new file mode 100644 index 000000000..aa0410482 --- /dev/null +++ b/.context/effect/.changeset/pre/green-ads-camp.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Expose object-shaped Toolkit success schemas as MCP tool output schemas. diff --git a/.context/effect/.changeset/green-beds-unref.md b/.context/effect/.changeset/pre/green-beds-unref.md similarity index 100% rename from .context/effect/.changeset/green-beds-unref.md rename to .context/effect/.changeset/pre/green-beds-unref.md diff --git a/.context/effect/.changeset/pre/green-birds-close.md b/.context/effect/.changeset/pre/green-birds-close.md new file mode 100644 index 000000000..e21ba8438 --- /dev/null +++ b/.context/effect/.changeset/pre/green-birds-close.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Close OPFS access handles when the SQLite worker shuts down. diff --git a/.context/effect/.changeset/green-chips-wash.md b/.context/effect/.changeset/pre/green-chips-wash.md similarity index 100% rename from .context/effect/.changeset/green-chips-wash.md rename to .context/effect/.changeset/pre/green-chips-wash.md diff --git a/.context/effect/.changeset/green-moons-smile.md b/.context/effect/.changeset/pre/green-moons-smile.md similarity index 100% rename from .context/effect/.changeset/green-moons-smile.md rename to .context/effect/.changeset/pre/green-moons-smile.md diff --git a/.context/effect/.changeset/green-pugs-play.md b/.context/effect/.changeset/pre/green-pugs-play.md similarity index 100% rename from .context/effect/.changeset/green-pugs-play.md rename to .context/effect/.changeset/pre/green-pugs-play.md diff --git a/.context/effect/.changeset/green-rings-prove.md b/.context/effect/.changeset/pre/green-rings-prove.md similarity index 100% rename from .context/effect/.changeset/green-rings-prove.md rename to .context/effect/.changeset/pre/green-rings-prove.md diff --git a/.context/effect/.changeset/happy-mirrors-dream.md b/.context/effect/.changeset/pre/happy-mirrors-dream.md similarity index 100% rename from .context/effect/.changeset/happy-mirrors-dream.md rename to .context/effect/.changeset/pre/happy-mirrors-dream.md diff --git a/.context/effect/.changeset/pre/harden-httpapi-documentation-html.md b/.context/effect/.changeset/pre/harden-httpapi-documentation-html.md new file mode 100644 index 000000000..774dbe329 --- /dev/null +++ b/.context/effect/.changeset/pre/harden-httpapi-documentation-html.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Harden HttpApi documentation HTML rendering. + +Scalar descriptions and CDN versions were interpolated without attribute-safe escaping. Embedded OpenAPI JSON in Scalar and Swagger also handled only the exact `` sequence, not other valid [script end-tag forms](https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state). + +Attribute values and CDN versions are now encoded for their contexts, and embedded JSON escapes `<` so it cannot close its script element. diff --git a/.context/effect/.changeset/pre/hash-sql-message-dedupe-keys.md b/.context/effect/.changeset/pre/hash-sql-message-dedupe-keys.md new file mode 100644 index 000000000..9e386e019 --- /dev/null +++ b/.context/effect/.changeset/pre/hash-sql-message-dedupe-keys.md @@ -0,0 +1,11 @@ +--- +"effect": patch +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +unstable/cluster: hash over-length SQL message deduplication keys to prevent `message_id` overflow, closes #6317. + +The composed request deduplication key (`entityType/entityId/tag/primaryKey`) can legally exceed the 255-character `message_id` column — the address columns alone allow 458 characters before the RPC primary key is appended. `SqlMessageStorage` now stores a SHA-256 digest (64 hex characters) of the composed key in the unique `message_id` column when the key exceeds 255 characters, so keys of any length work on PostgreSQL, MySQL, MSSQL, and SQLite. Keys that fit are stored as plaintext, byte-compatible with rows written by previous versions, so existing deployments keep deduplicating with no migration or schema change. + +`SqlMessageStorage.layer`/`layerWith` (and consequently `SingleRunner.layer`) now require `Crypto.Crypto`. The Node and Bun cluster convenience layers provide the platform Crypto implementation internally, so their requirements are unchanged. diff --git a/.context/effect/.changeset/heavy-loops-cut.md b/.context/effect/.changeset/pre/heavy-loops-cut.md similarity index 100% rename from .context/effect/.changeset/heavy-loops-cut.md rename to .context/effect/.changeset/pre/heavy-loops-cut.md diff --git a/.context/effect/.changeset/heavy-trams-fix.md b/.context/effect/.changeset/pre/heavy-trams-fix.md similarity index 100% rename from .context/effect/.changeset/heavy-trams-fix.md rename to .context/effect/.changeset/pre/heavy-trams-fix.md diff --git a/.context/effect/.changeset/pre/hip-friends-kiss.md b/.context/effect/.changeset/pre/hip-friends-kiss.md new file mode 100644 index 000000000..a5c66356c --- /dev/null +++ b/.context/effect/.changeset/pre/hip-friends-kiss.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +added graph set operations for combining and comparing graphs + +- `Graph.make` - creates a graph constructor for a dynamically selected graph kind +- `Graph.compose` - composition of two graphs, merging nodes by identity +- `Graph.intersection` - intersection of two graphs, keeping only common nodes and edges +- `Graph.difference` - difference of two graphs, removing edges present in the second graph +- `Graph.symmetricDifference` - symmetric difference of two graphs, keeping edges present in exactly one graph diff --git a/.context/effect/.changeset/hip-socks-travel.md b/.context/effect/.changeset/pre/hip-socks-travel.md similarity index 100% rename from .context/effect/.changeset/hip-socks-travel.md rename to .context/effect/.changeset/pre/hip-socks-travel.md diff --git a/.context/effect/.changeset/honest-pens-thank.md b/.context/effect/.changeset/pre/honest-pens-thank.md similarity index 100% rename from .context/effect/.changeset/honest-pens-thank.md rename to .context/effect/.changeset/pre/honest-pens-thank.md diff --git a/.context/effect/.changeset/honest-rivers-notice.md b/.context/effect/.changeset/pre/honest-rivers-notice.md similarity index 100% rename from .context/effect/.changeset/honest-rivers-notice.md rename to .context/effect/.changeset/pre/honest-rivers-notice.md diff --git a/.context/effect/.changeset/hot-taxis-fry.md b/.context/effect/.changeset/pre/hot-taxis-fry.md similarity index 100% rename from .context/effect/.changeset/hot-taxis-fry.md rename to .context/effect/.changeset/pre/hot-taxis-fry.md diff --git a/.context/effect/.changeset/pre/hot-teeth-clean.md b/.context/effect/.changeset/pre/hot-teeth-clean.md new file mode 100644 index 000000000..c737634e7 --- /dev/null +++ b/.context/effect/.changeset/pre/hot-teeth-clean.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +change rpc ids to string | number diff --git a/.context/effect/.changeset/pre/http-server-websocket-options.md b/.context/effect/.changeset/pre/http-server-websocket-options.md new file mode 100644 index 000000000..177d15b7e --- /dev/null +++ b/.context/effect/.changeset/pre/http-server-websocket-options.md @@ -0,0 +1,22 @@ +--- +"@effect/platform-node": patch +"@effect/platform-bun": patch +--- + +Allow configuring the WebSocket server in `NodeHttpServer` and `BunHttpServer`. + +Both servers now accept a `websocket` option that is forwarded to the underlying implementation, with the wiring/lifecycle options the server manages excluded from the type: + +```ts +// Node: forwarded to the `ws` WebSocketServer +NodeHttpServer.layer(() => createServer(), { + port: 3000, + websocket: { perMessageDeflate: true } +}) + +// Bun: merged into Bun.serve's websocket handler +BunHttpServer.layer({ + port: 3000, + websocket: { perMessageDeflate: true } +}) +``` diff --git a/.context/effect/.changeset/httpapi-endpoint-relax-constraints.md b/.context/effect/.changeset/pre/httpapi-endpoint-relax-constraints.md similarity index 100% rename from .context/effect/.changeset/httpapi-endpoint-relax-constraints.md rename to .context/effect/.changeset/pre/httpapi-endpoint-relax-constraints.md diff --git a/.context/effect/.changeset/httpapi-schema-service-types.md b/.context/effect/.changeset/pre/httpapi-schema-service-types.md similarity index 100% rename from .context/effect/.changeset/httpapi-schema-service-types.md rename to .context/effect/.changeset/pre/httpapi-schema-service-types.md diff --git a/.context/effect/.changeset/huge-moons-rhyme.md b/.context/effect/.changeset/pre/huge-moons-rhyme.md similarity index 100% rename from .context/effect/.changeset/huge-moons-rhyme.md rename to .context/effect/.changeset/pre/huge-moons-rhyme.md diff --git a/.context/effect/.changeset/humble-pigs-dig.md b/.context/effect/.changeset/pre/humble-pigs-dig.md similarity index 100% rename from .context/effect/.changeset/humble-pigs-dig.md rename to .context/effect/.changeset/pre/humble-pigs-dig.md diff --git a/.context/effect/.changeset/pre/hungry-kings-look.md b/.context/effect/.changeset/pre/hungry-kings-look.md new file mode 100644 index 000000000..602fb3422 --- /dev/null +++ b/.context/effect/.changeset/pre/hungry-kings-look.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +Add glob to filesystem diff --git a/.context/effect/.changeset/stale-snakes-know.md b/.context/effect/.changeset/pre/icy-flies-cross.md similarity index 100% rename from .context/effect/.changeset/stale-snakes-know.md rename to .context/effect/.changeset/pre/icy-flies-cross.md diff --git a/.context/effect/.changeset/pre/internal-json-string-schema.md b/.context/effect/.changeset/pre/internal-json-string-schema.md new file mode 100644 index 000000000..2a5ab3549 --- /dev/null +++ b/.context/effect/.changeset/pre/internal-json-string-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Mark `Schema.UnknownFromJsonString` as internal and remove its type-level interface. Use `Schema.fromJsonString(Schema.Unknown)` instead. Add `reviver`, callback or array `replacer`, and `space` options to `Schema.fromJsonString`, and make `SchemaTransformation.fromJsonString` a configurable factory. diff --git a/.context/effect/.changeset/pre/isolate-sql-compiler-cache.md b/.context/effect/.changeset/pre/isolate-sql-compiler-cache.md new file mode 100644 index 000000000..f45018a57 --- /dev/null +++ b/.context/effect/.changeset/pre/isolate-sql-compiler-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Isolate compiled SQL fragment caches by compiler instance. diff --git a/.context/effect/.changeset/itchy-radios-poke.md b/.context/effect/.changeset/pre/itchy-radios-poke.md similarity index 100% rename from .context/effect/.changeset/itchy-radios-poke.md rename to .context/effect/.changeset/pre/itchy-radios-poke.md diff --git a/.context/effect/.changeset/itchy-results-bet.md b/.context/effect/.changeset/pre/itchy-results-bet.md similarity index 100% rename from .context/effect/.changeset/itchy-results-bet.md rename to .context/effect/.changeset/pre/itchy-results-bet.md diff --git a/.context/effect/.changeset/itchy-shrimps-deny.md b/.context/effect/.changeset/pre/itchy-shrimps-deny.md similarity index 100% rename from .context/effect/.changeset/itchy-shrimps-deny.md rename to .context/effect/.changeset/pre/itchy-shrimps-deny.md diff --git a/.context/effect/.changeset/itchy-toes-promise.md b/.context/effect/.changeset/pre/itchy-toes-promise.md similarity index 100% rename from .context/effect/.changeset/itchy-toes-promise.md rename to .context/effect/.changeset/pre/itchy-toes-promise.md diff --git a/.context/effect/.changeset/k8s-last-transition-null.md b/.context/effect/.changeset/pre/k8s-last-transition-null.md similarity index 100% rename from .context/effect/.changeset/k8s-last-transition-null.md rename to .context/effect/.changeset/pre/k8s-last-transition-null.md diff --git a/.context/effect/.changeset/pre/keep-httpapi-composition-immutable.md b/.context/effect/.changeset/pre/keep-httpapi-composition-immutable.md new file mode 100644 index 000000000..a4f979384 --- /dev/null +++ b/.context/effect/.changeset/pre/keep-httpapi-composition-immutable.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Keep HttpApi composition immutable. + +`HttpApi.addHttpApi` applied annotations from the added API by mutating its shared groups. It now creates annotated group copies, keeping the source API and independently annotated variants unchanged while preserving annotation precedence. diff --git a/.context/effect/.changeset/khaki-cats-learn.md b/.context/effect/.changeset/pre/khaki-cats-learn.md similarity index 100% rename from .context/effect/.changeset/khaki-cats-learn.md rename to .context/effect/.changeset/pre/khaki-cats-learn.md diff --git a/.context/effect/.changeset/khaki-melons-appear.md b/.context/effect/.changeset/pre/khaki-melons-appear.md similarity index 100% rename from .context/effect/.changeset/khaki-melons-appear.md rename to .context/effect/.changeset/pre/khaki-melons-appear.md diff --git a/.context/effect/.changeset/pre/kind-flags-help.md b/.context/effect/.changeset/pre/kind-flags-help.md new file mode 100644 index 000000000..ecf9af6d2 --- /dev/null +++ b/.context/effect/.changeset/pre/kind-flags-help.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Mark omittable CLI flags and arguments as optional in structured help. diff --git a/.context/effect/.changeset/kind-hounds-float.md b/.context/effect/.changeset/pre/kind-hounds-float.md similarity index 100% rename from .context/effect/.changeset/kind-hounds-float.md rename to .context/effect/.changeset/pre/kind-hounds-float.md diff --git a/.context/effect/.changeset/kind-windows-fall.md b/.context/effect/.changeset/pre/kind-windows-fall.md similarity index 100% rename from .context/effect/.changeset/kind-windows-fall.md rename to .context/effect/.changeset/pre/kind-windows-fall.md diff --git a/.context/effect/.changeset/late-hotels-rule.md b/.context/effect/.changeset/pre/late-hotels-rule.md similarity index 100% rename from .context/effect/.changeset/late-hotels-rule.md rename to .context/effect/.changeset/pre/late-hotels-rule.md diff --git a/.context/effect/.changeset/late-lamps-care.md b/.context/effect/.changeset/pre/late-lamps-care.md similarity index 100% rename from .context/effect/.changeset/late-lamps-care.md rename to .context/effect/.changeset/pre/late-lamps-care.md diff --git a/.context/effect/.changeset/late-rivers-applaud.md b/.context/effect/.changeset/pre/late-rivers-applaud.md similarity index 100% rename from .context/effect/.changeset/late-rivers-applaud.md rename to .context/effect/.changeset/pre/late-rivers-applaud.md diff --git a/.context/effect/.changeset/layer-map-dynamic-idle-ttl.md b/.context/effect/.changeset/pre/layer-map-dynamic-idle-ttl.md similarity index 100% rename from .context/effect/.changeset/layer-map-dynamic-idle-ttl.md rename to .context/effect/.changeset/pre/layer-map-dynamic-idle-ttl.md diff --git a/.context/effect/.changeset/pre/layered-context-storage.md b/.context/effect/.changeset/pre/layered-context-storage.md new file mode 100644 index 000000000..e1bd0703a --- /dev/null +++ b/.context/effect/.changeset/pre/layered-context-storage.md @@ -0,0 +1,9 @@ +--- +"effect": patch +"@effect/docgen": patch +"@effect/platform-bun": patch +"@effect/platform-deno": patch +"@effect/platform-node": patch +--- + +Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. diff --git a/.context/effect/.changeset/lazy-queens-rush.md b/.context/effect/.changeset/pre/lazy-queens-rush.md similarity index 100% rename from .context/effect/.changeset/lazy-queens-rush.md rename to .context/effect/.changeset/pre/lazy-queens-rush.md diff --git a/.context/effect/.changeset/lazy-recursive-forward-refs.md b/.context/effect/.changeset/pre/lazy-recursive-forward-refs.md similarity index 100% rename from .context/effect/.changeset/lazy-recursive-forward-refs.md rename to .context/effect/.changeset/pre/lazy-recursive-forward-refs.md diff --git a/.context/effect/.changeset/lazy-timers-exist.md b/.context/effect/.changeset/pre/lazy-timers-exist.md similarity index 100% rename from .context/effect/.changeset/lazy-timers-exist.md rename to .context/effect/.changeset/pre/lazy-timers-exist.md diff --git a/.context/effect/.changeset/legal-pants-drop.md b/.context/effect/.changeset/pre/legal-pants-drop.md similarity index 100% rename from .context/effect/.changeset/legal-pants-drop.md rename to .context/effect/.changeset/pre/legal-pants-drop.md diff --git a/.context/effect/.changeset/lemon-taxis-sin.md b/.context/effect/.changeset/pre/lemon-taxis-sin.md similarity index 100% rename from .context/effect/.changeset/lemon-taxis-sin.md rename to .context/effect/.changeset/pre/lemon-taxis-sin.md diff --git a/.context/effect/.changeset/light-kids-sneeze.md b/.context/effect/.changeset/pre/light-kids-sneeze.md similarity index 100% rename from .context/effect/.changeset/light-kids-sneeze.md rename to .context/effect/.changeset/pre/light-kids-sneeze.md diff --git a/.context/effect/.changeset/little-dryers-allow.md b/.context/effect/.changeset/pre/little-dryers-allow.md similarity index 100% rename from .context/effect/.changeset/little-dryers-allow.md rename to .context/effect/.changeset/pre/little-dryers-allow.md diff --git a/.context/effect/.changeset/long-cameras-think.md b/.context/effect/.changeset/pre/long-cameras-think.md similarity index 100% rename from .context/effect/.changeset/long-cameras-think.md rename to .context/effect/.changeset/pre/long-cameras-think.md diff --git a/.context/effect/.changeset/pre/loose-wings-lie.md b/.context/effect/.changeset/pre/loose-wings-lie.md new file mode 100644 index 000000000..fedc37514 --- /dev/null +++ b/.context/effect/.changeset/pre/loose-wings-lie.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent Effect.provideServiceEffect supertype widening diff --git a/.context/effect/.changeset/lovely-cobras-change.md b/.context/effect/.changeset/pre/lovely-cobras-change.md similarity index 100% rename from .context/effect/.changeset/lovely-cobras-change.md rename to .context/effect/.changeset/pre/lovely-cobras-change.md diff --git a/.context/effect/.changeset/lovely-frogs-rescue.md b/.context/effect/.changeset/pre/lovely-frogs-rescue.md similarity index 100% rename from .context/effect/.changeset/lovely-frogs-rescue.md rename to .context/effect/.changeset/pre/lovely-frogs-rescue.md diff --git a/.context/effect/.changeset/lucky-buttons-jump.md b/.context/effect/.changeset/pre/lucky-buttons-jump.md similarity index 100% rename from .context/effect/.changeset/lucky-buttons-jump.md rename to .context/effect/.changeset/pre/lucky-buttons-jump.md diff --git a/.context/effect/.changeset/pre/lucky-dingos-smile.md b/.context/effect/.changeset/pre/lucky-dingos-smile.md new file mode 100644 index 000000000..96d9a4ab3 --- /dev/null +++ b/.context/effect/.changeset/pre/lucky-dingos-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Map and Set equality allowing a right-side entry to match multiple left-side entries. diff --git a/.context/effect/.changeset/lucky-phones-listen.md b/.context/effect/.changeset/pre/lucky-phones-listen.md similarity index 100% rename from .context/effect/.changeset/lucky-phones-listen.md rename to .context/effect/.changeset/pre/lucky-phones-listen.md diff --git a/.context/effect/.changeset/lucky-worms-type.md b/.context/effect/.changeset/pre/lucky-worms-type.md similarity index 100% rename from .context/effect/.changeset/lucky-worms-type.md rename to .context/effect/.changeset/pre/lucky-worms-type.md diff --git a/.context/effect/.changeset/major-chairs-design.md b/.context/effect/.changeset/pre/major-chairs-design.md similarity index 100% rename from .context/effect/.changeset/major-chairs-design.md rename to .context/effect/.changeset/pre/major-chairs-design.md diff --git a/.context/effect/.changeset/pre/managed-runtime-async-dispose.md b/.context/effect/.changeset/pre/managed-runtime-async-dispose.md new file mode 100644 index 000000000..1b4fe0b7c --- /dev/null +++ b/.context/effect/.changeset/pre/managed-runtime-async-dispose.md @@ -0,0 +1,14 @@ +--- +"effect": patch +--- + +ManagedRuntime: add `Symbol.asyncDispose`, enabling `await using` syntax + +```ts +import { Effect, Layer, ManagedRuntime } from "effect" + +await using runtime = ManagedRuntime.make(Layer.empty) + +await runtime.runPromise(Effect.log("Hello, world!")) +// runtime is disposed automatically at the end of the scope +``` diff --git a/.context/effect/.changeset/many-badgers-obey.md b/.context/effect/.changeset/pre/many-badgers-obey.md similarity index 100% rename from .context/effect/.changeset/many-badgers-obey.md rename to .context/effect/.changeset/pre/many-badgers-obey.md diff --git a/.context/effect/.changeset/pre/mcp-tool-output-schema.md b/.context/effect/.changeset/pre/mcp-tool-output-schema.md new file mode 100644 index 000000000..3c0399361 --- /dev/null +++ b/.context/effect/.changeset/pre/mcp-tool-output-schema.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include typed tool output schemas in MCP `tools/list` responses. diff --git a/.context/effect/.changeset/mean-dingos-share.md b/.context/effect/.changeset/pre/mean-dingos-share.md similarity index 100% rename from .context/effect/.changeset/mean-dingos-share.md rename to .context/effect/.changeset/pre/mean-dingos-share.md diff --git a/.context/effect/.changeset/mean-trains-smash.md b/.context/effect/.changeset/pre/mean-trains-smash.md similarity index 100% rename from .context/effect/.changeset/mean-trains-smash.md rename to .context/effect/.changeset/pre/mean-trains-smash.md diff --git a/.context/effect/.changeset/pre/memoize-idempotent-asts.md b/.context/effect/.changeset/pre/memoize-idempotent-asts.md new file mode 100644 index 000000000..30c655d7c --- /dev/null +++ b/.context/effect/.changeset/pre/memoize-idempotent-asts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Function.memoizeIdempotent` and use it to avoid reprocessing canonical Schema ASTs, including optional and mutable property modifiers. Cache Config schema cursor AST compilation. diff --git a/.context/effect/.changeset/pre/metal-nails-sneeze.md b/.context/effect/.changeset/pre/metal-nails-sneeze.md new file mode 100644 index 000000000..148d8c62b --- /dev/null +++ b/.context/effect/.changeset/pre/metal-nails-sneeze.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add LayerRef module diff --git a/.context/effect/.changeset/metal-parts-yell.md b/.context/effect/.changeset/pre/metal-parts-yell.md similarity index 100% rename from .context/effect/.changeset/metal-parts-yell.md rename to .context/effect/.changeset/pre/metal-parts-yell.md diff --git a/.context/effect/.changeset/mighty-games-matter.md b/.context/effect/.changeset/pre/mighty-games-matter.md similarity index 100% rename from .context/effect/.changeset/mighty-games-matter.md rename to .context/effect/.changeset/pre/mighty-games-matter.md diff --git a/.context/effect/.changeset/pre/migrator-windows-file-url.md b/.context/effect/.changeset/pre/migrator-windows-file-url.md new file mode 100644 index 000000000..9ad8a9d4d --- /dev/null +++ b/.context/effect/.changeset/pre/migrator-windows-file-url.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Import migrations through a file URL in `Migrator.fromFileSystem`, so absolute Windows paths are accepted by the ESM loader. + +Previously the directory and file name were passed to `import` as a plain path. On Windows that produced a specifier such as `D:\migrations\1_init.ts`, which the ESM loader rejects with `Only URLs with a scheme in: file, data, and node are supported`. + +`fromFileSystem` now resolves the specifier through the `Path` service, so its type widens from `Loader` to `Loader`. Callers that already provide an aggregate platform layer such as `NodeServices.layer` are unaffected; callers that provide `FileSystem` on its own now also need a `Path` layer, and on Windows it must be a platform-aware one rather than the POSIX `Path.layer`. diff --git a/.context/effect/.changeset/modern-carrots-see.md b/.context/effect/.changeset/pre/modern-carrots-see.md similarity index 100% rename from .context/effect/.changeset/modern-carrots-see.md rename to .context/effect/.changeset/pre/modern-carrots-see.md diff --git a/.context/effect/.changeset/modern-uuid-guid-filter.md b/.context/effect/.changeset/pre/modern-uuid-guid-filter.md similarity index 100% rename from .context/effect/.changeset/modern-uuid-guid-filter.md rename to .context/effect/.changeset/pre/modern-uuid-guid-filter.md diff --git a/.context/effect/.changeset/pre/multipart-collect-linear.md b/.context/effect/.changeset/pre/multipart-collect-linear.md new file mode 100644 index 000000000..93f37d8f1 --- /dev/null +++ b/.context/effect/.changeset/pre/multipart-collect-linear.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Channel.mkUint8Array` and reuse it from `Stream` and multipart file collection. This also fixes quadratic buffering in `File.contentEffect`, improving collection of a 16 MiB chunked upload by approximately 90x. diff --git a/.context/effect/.changeset/pre/multipart-onDone-clobbers-error.md b/.context/effect/.changeset/pre/multipart-onDone-clobbers-error.md new file mode 100644 index 000000000..621db7284 --- /dev/null +++ b/.context/effect/.changeset/pre/multipart-onDone-clobbers-error.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix multipart parser limit violations being silently swallowed diff --git a/.context/effect/.changeset/pre/multipart-parser-limits.md b/.context/effect/.changeset/pre/multipart-parser-limits.md new file mode 100644 index 000000000..d62512922 --- /dev/null +++ b/.context/effect/.changeset/pre/multipart-parser-limits.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Stop multipart parsing after part count, part size, or field size limits are exceeded. diff --git a/.context/effect/.changeset/pre/mysql2-disable-prepared-statements.md b/.context/effect/.changeset/pre/mysql2-disable-prepared-statements.md new file mode 100644 index 000000000..02150bcc4 --- /dev/null +++ b/.context/effect/.changeset/pre/mysql2-disable-prepared-statements.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-mysql2": patch +--- + +Add `disablePreparedStatements` to `MysqlClientConfig`, to completely disable prepared statements diff --git a/.context/effect/.changeset/nasty-geese-grow.md b/.context/effect/.changeset/pre/nasty-geese-grow.md similarity index 100% rename from .context/effect/.changeset/nasty-geese-grow.md rename to .context/effect/.changeset/pre/nasty-geese-grow.md diff --git a/.context/effect/.changeset/neat-goats-wave.md b/.context/effect/.changeset/pre/neat-goats-wave.md similarity index 100% rename from .context/effect/.changeset/neat-goats-wave.md rename to .context/effect/.changeset/pre/neat-goats-wave.md diff --git a/.context/effect/.changeset/neat-kings-chew.md b/.context/effect/.changeset/pre/neat-kings-chew.md similarity index 100% rename from .context/effect/.changeset/neat-kings-chew.md rename to .context/effect/.changeset/pre/neat-kings-chew.md diff --git a/.context/effect/.changeset/neat-lions-rest.md b/.context/effect/.changeset/pre/neat-lions-rest.md similarity index 100% rename from .context/effect/.changeset/neat-lions-rest.md rename to .context/effect/.changeset/pre/neat-lions-rest.md diff --git a/.context/effect/.changeset/pre/neat-pandas-query.md b/.context/effect/.changeset/pre/neat-pandas-query.md new file mode 100644 index 000000000..f78f49222 --- /dev/null +++ b/.context/effect/.changeset/pre/neat-pandas-query.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-clickhouse": patch +--- + +Propagate ClickHouse result decoding failures as `SqlError` values. diff --git a/.context/effect/.changeset/neat-snails-wash.md b/.context/effect/.changeset/pre/neat-snails-wash.md similarity index 100% rename from .context/effect/.changeset/neat-snails-wash.md rename to .context/effect/.changeset/pre/neat-snails-wash.md diff --git a/.context/effect/.changeset/neat-taxis-notice.md b/.context/effect/.changeset/pre/neat-taxis-notice.md similarity index 100% rename from .context/effect/.changeset/neat-taxis-notice.md rename to .context/effect/.changeset/pre/neat-taxis-notice.md diff --git a/.context/effect/.changeset/pre/neat-tuples-remember.md b/.context/effect/.changeset/pre/neat-tuples-remember.md new file mode 100644 index 000000000..526502b74 --- /dev/null +++ b/.context/effect/.changeset/pre/neat-tuples-remember.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve literal element types in `Tuple.make`. diff --git a/.context/effect/.changeset/neat-windows-buy.md b/.context/effect/.changeset/pre/neat-windows-buy.md similarity index 100% rename from .context/effect/.changeset/neat-windows-buy.md rename to .context/effect/.changeset/pre/neat-windows-buy.md diff --git a/.context/effect/.changeset/pre/nested-union-sentinels.md b/.context/effect/.changeset/pre/nested-union-sentinels.md new file mode 100644 index 000000000..bba5671a1 --- /dev/null +++ b/.context/effect/.changeset/pre/nested-union-sentinels.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve Union candidate selection: a nested union member is dispatched by the sentinels common to all its members, and candidates whose sentinel the input contradicts are excluded. diff --git a/.context/effect/.changeset/new-dogs-swim.md b/.context/effect/.changeset/pre/new-dogs-swim.md similarity index 100% rename from .context/effect/.changeset/new-dogs-swim.md rename to .context/effect/.changeset/pre/new-dogs-swim.md diff --git a/.context/effect/.changeset/new-toes-stop.md b/.context/effect/.changeset/pre/new-toes-stop.md similarity index 100% rename from .context/effect/.changeset/new-toes-stop.md rename to .context/effect/.changeset/pre/new-toes-stop.md diff --git a/.context/effect/.changeset/ninety-geese-exist.md b/.context/effect/.changeset/pre/ninety-geese-exist.md similarity index 100% rename from .context/effect/.changeset/ninety-geese-exist.md rename to .context/effect/.changeset/pre/ninety-geese-exist.md diff --git a/.context/effect/.changeset/pre/node-terminal-idle-ttl.md b/.context/effect/.changeset/pre/node-terminal-idle-ttl.md new file mode 100644 index 000000000..71ac7237f --- /dev/null +++ b/.context/effect/.changeset/pre/node-terminal-idle-ttl.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Keep NodeTerminal's readline interface alive briefly between adjacent prompts to avoid a Windows TTY raw-mode hang. diff --git a/.context/effect/.changeset/pre/node-terminal-stdin-eof.md b/.context/effect/.changeset/pre/node-terminal-stdin-eof.md new file mode 100644 index 000000000..297b295aa --- /dev/null +++ b/.context/effect/.changeset/pre/node-terminal-stdin-eof.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +NodeTerminal: end key input and fail `readLine` with `QuitError` at stdin EOF instead of hanging diff --git a/.context/effect/.changeset/pre/normalize-httpapi-payload-media-types.md b/.context/effect/.changeset/pre/normalize-httpapi-payload-media-types.md new file mode 100644 index 000000000..ae6a0a78b --- /dev/null +++ b/.context/effect/.changeset/pre/normalize-httpapi-payload-media-types.md @@ -0,0 +1,11 @@ +--- +"effect": patch +--- + +Normalize HttpApi payload media types. + +Payload schemas were stored under their exact declared `Content-Type`, but the server lowercased the incoming header and removed its parameters before looking it up. For example, a schema declared as `Application/Vnd.Effect+JSON; profile=declared` was stored under that value, while the server looked for `application/vnd.effect+json`. This could produce a `415` response even when the generated client and server used the same API. + +The same mismatch allowed incompatible encodings for equivalent media types to bypass validation. Generated form-urlencoded requests also ignored custom content types and always used the default one. + +Payload maps now use normalized keys for matching and conflict checks, while each encoding keeps its declared content type. Generated requests and OpenAPI use the declared values, including every parameterized variant, and custom form-urlencoded content types are preserved. diff --git a/.context/effect/.changeset/pre/o8drprcu-sqlite-node-node-sqlite.md b/.context/effect/.changeset/pre/o8drprcu-sqlite-node-node-sqlite.md new file mode 100644 index 000000000..b1840466f --- /dev/null +++ b/.context/effect/.changeset/pre/o8drprcu-sqlite-node-node-sqlite.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-node": patch +--- + +Replace the `better-sqlite3` dependency with Node.js' built-in `node:sqlite` module. diff --git a/.context/effect/.changeset/odd-boats-think.md b/.context/effect/.changeset/pre/odd-boats-think.md similarity index 100% rename from .context/effect/.changeset/odd-boats-think.md rename to .context/effect/.changeset/pre/odd-boats-think.md diff --git a/.context/effect/.changeset/odd-bulldogs-sleep.md b/.context/effect/.changeset/pre/odd-bulldogs-sleep.md similarity index 100% rename from .context/effect/.changeset/odd-bulldogs-sleep.md rename to .context/effect/.changeset/pre/odd-bulldogs-sleep.md diff --git a/.context/effect/.changeset/odd-fans-glow.md b/.context/effect/.changeset/pre/odd-fans-glow.md similarity index 100% rename from .context/effect/.changeset/odd-fans-glow.md rename to .context/effect/.changeset/pre/odd-fans-glow.md diff --git a/.context/effect/.changeset/odd-forks-talk.md b/.context/effect/.changeset/pre/odd-forks-talk.md similarity index 100% rename from .context/effect/.changeset/odd-forks-talk.md rename to .context/effect/.changeset/pre/odd-forks-talk.md diff --git a/.context/effect/.changeset/odd-laws-draw.md b/.context/effect/.changeset/pre/odd-laws-draw.md similarity index 100% rename from .context/effect/.changeset/odd-laws-draw.md rename to .context/effect/.changeset/pre/odd-laws-draw.md diff --git a/.context/effect/.changeset/odd-owls-smoke.md b/.context/effect/.changeset/pre/odd-owls-smoke.md similarity index 100% rename from .context/effect/.changeset/odd-owls-smoke.md rename to .context/effect/.changeset/pre/odd-owls-smoke.md diff --git a/.context/effect/.changeset/odd-socks-boil.md b/.context/effect/.changeset/pre/odd-socks-boil.md similarity index 100% rename from .context/effect/.changeset/odd-socks-boil.md rename to .context/effect/.changeset/pre/odd-socks-boil.md diff --git a/.context/effect/.changeset/odd-suns-dance.md b/.context/effect/.changeset/pre/odd-suns-dance.md similarity index 100% rename from .context/effect/.changeset/odd-suns-dance.md rename to .context/effect/.changeset/pre/odd-suns-dance.md diff --git a/.context/effect/.changeset/old-brooms-cry.md b/.context/effect/.changeset/pre/old-brooms-cry.md similarity index 100% rename from .context/effect/.changeset/old-brooms-cry.md rename to .context/effect/.changeset/pre/old-brooms-cry.md diff --git a/.context/effect/.changeset/old-facts-stand.md b/.context/effect/.changeset/pre/old-facts-stand.md similarity index 100% rename from .context/effect/.changeset/old-facts-stand.md rename to .context/effect/.changeset/pre/old-facts-stand.md diff --git a/.context/effect/.changeset/old-mirrors-float.md b/.context/effect/.changeset/pre/old-mirrors-float.md similarity index 100% rename from .context/effect/.changeset/old-mirrors-float.md rename to .context/effect/.changeset/pre/old-mirrors-float.md diff --git a/.context/effect/.changeset/olive-poems-visit.md b/.context/effect/.changeset/pre/olive-poems-visit.md similarity index 100% rename from .context/effect/.changeset/olive-poems-visit.md rename to .context/effect/.changeset/pre/olive-poems-visit.md diff --git a/.context/effect/.changeset/pre/opaque-graph-interface.md b/.context/effect/.changeset/pre/opaque-graph-interface.md new file mode 100644 index 000000000..47e27a0b9 --- /dev/null +++ b/.context/effect/.changeset/pre/opaque-graph-interface.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make the public Graph interfaces opaque by hiding internal mutable storage fields from their TypeScript surface. diff --git a/.context/effect/.changeset/open-hotels-remain.md b/.context/effect/.changeset/pre/open-hotels-remain.md similarity index 100% rename from .context/effect/.changeset/open-hotels-remain.md rename to .context/effect/.changeset/pre/open-hotels-remain.md diff --git a/.context/effect/.changeset/pre/openai-compat-decode-tool-params.md b/.context/effect/.changeset/pre/openai-compat-decode-tool-params.md new file mode 100644 index 000000000..5286f106e --- /dev/null +++ b/.context/effect/.changeset/pre/openai-compat-decode-tool-params.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Decode streaming and non-streaming tool call parameters with the provider-facing OpenAI schema codec. diff --git a/.context/effect/.changeset/pre/openai-compat-empty-assistant-content.md b/.context/effect/.changeset/pre/openai-compat-empty-assistant-content.md new file mode 100644 index 000000000..8426aa01f --- /dev/null +++ b/.context/effect/.changeset/pre/openai-compat-empty-assistant-content.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Normalize empty assistant message content to an empty string for OpenAI-compatible providers that reject null content values. diff --git a/.context/effect/.changeset/openai-compat-nullable-tool-name.md b/.context/effect/.changeset/pre/openai-compat-nullable-tool-name.md similarity index 100% rename from .context/effect/.changeset/openai-compat-nullable-tool-name.md rename to .context/effect/.changeset/pre/openai-compat-nullable-tool-name.md diff --git a/.context/effect/.changeset/pre/openai-compat-parallel-tool-calls.md b/.context/effect/.changeset/pre/openai-compat-parallel-tool-calls.md new file mode 100644 index 000000000..d2b2ce441 --- /dev/null +++ b/.context/effect/.changeset/pre/openai-compat-parallel-tool-calls.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Group consecutive tool calls into one assistant message when using Chat Completions APIs. diff --git a/.context/effect/.changeset/openai-compat-reasoning.md b/.context/effect/.changeset/pre/openai-compat-reasoning.md similarity index 100% rename from .context/effect/.changeset/openai-compat-reasoning.md rename to .context/effect/.changeset/pre/openai-compat-reasoning.md diff --git a/.context/effect/.changeset/pre/openai-telemetry-response.md b/.context/effect/.changeset/pre/openai-telemetry-response.md new file mode 100644 index 000000000..c2bf786a2 --- /dev/null +++ b/.context/effect/.changeset/pre/openai-telemetry-response.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai-compat": patch +--- + +Fix the OpenAI-compatible telemetry response attribute namespace. diff --git a/.context/effect/.changeset/openapi-generator-sse-constraint-decoder.md b/.context/effect/.changeset/pre/openapi-generator-sse-constraint-decoder.md similarity index 100% rename from .context/effect/.changeset/openapi-generator-sse-constraint-decoder.md rename to .context/effect/.changeset/pre/openapi-generator-sse-constraint-decoder.md diff --git a/.context/effect/.changeset/pre/openrouter-generation-usage-schema.md b/.context/effect/.changeset/pre/openrouter-generation-usage-schema.md new file mode 100644 index 000000000..d9b9a6795 --- /dev/null +++ b/.context/effect/.changeset/pre/openrouter-generation-usage-schema.md @@ -0,0 +1,11 @@ +--- +"@effect/ai-openrouter": patch +--- + +Regenerate the `Generated` module against OpenRouter's current published specification. This preserves nullable +generation statistics and streamed usage cost metadata while incorporating the broader upstream schema changes. + +Notable generated schema renames include `ChatGenerationParams` to `ChatRequest`, `ChatGenerationTokenUsage` to +`ChatUsage`, `AssistantMessage` to `ChatAssistantMessage`, `ChatStreamingResponseChunk` to `ChatStreamingResponse`, +and `ChatMessageContentItemCacheControl` to `ChatContentCacheControl`. Handwritten public aliases such as +`ChatStreamingResponseChunkData`, `ReasoningDetails`, and `FileAnnotation` retain their existing names. diff --git a/.context/effect/.changeset/openrouter-input-audio.md b/.context/effect/.changeset/pre/openrouter-input-audio.md similarity index 100% rename from .context/effect/.changeset/openrouter-input-audio.md rename to .context/effect/.changeset/pre/openrouter-input-audio.md diff --git a/.context/effect/.changeset/pre/openrouter-tool-parameter-deltas.md b/.context/effect/.changeset/pre/openrouter-tool-parameter-deltas.md new file mode 100644 index 000000000..41da9fcad --- /dev/null +++ b/.context/effect/.changeset/pre/openrouter-tool-parameter-deltas.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openrouter": patch +--- + +Emit incremental tool parameter fragments from OpenRouter streaming responses. diff --git a/.context/effect/.changeset/opentelemetry-render-causes.md b/.context/effect/.changeset/pre/opentelemetry-render-causes.md similarity index 100% rename from .context/effect/.changeset/opentelemetry-render-causes.md rename to .context/effect/.changeset/pre/opentelemetry-render-causes.md diff --git a/.context/effect/.changeset/pre/optimize-array-equality.md b/.context/effect/.changeset/pre/optimize-array-equality.md new file mode 100644 index 000000000..1ae1dede6 --- /dev/null +++ b/.context/effect/.changeset/pre/optimize-array-equality.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve the performance of `Array.dedupe`, `Array.union`, `Array.intersection`, `Array.difference`, and Schema unique item validation by using hash-based equality lookup. diff --git a/.context/effect/.changeset/pre/optimize-httpapi-handler-types.md b/.context/effect/.changeset/pre/optimize-httpapi-handler-types.md new file mode 100644 index 000000000..af3799598 --- /dev/null +++ b/.context/effect/.changeset/pre/optimize-httpapi-handler-types.md @@ -0,0 +1,204 @@ +--- +"effect": patch +--- + +Improve unstable `HttpApi` type-level performance. + +The implementation now uses identifier-keyed maps and lighter structural +constraints in several hot type-level paths. Generated group clients consume the +concrete endpoint map directly instead of rebuilding it from the endpoint union. + +## New Features + +- Add `HttpApiBuilder.Handlers.handleAll`, which registers an identifier-keyed batch of endpoint handlers for a group. Each entry can be either a handler function or `{ handler, options }`, and the object can be supplied in multiple partial batches. Endpoint identifiers that were already handled are rejected across batches. +- `HttpApi.groups` now preserves the concrete group type for each group identifier. For example, `Api.groups.users` is typed as the `users` group instead of the full group union. +- `HttpApiGroup.endpoints` now preserves the concrete endpoint type for each endpoint identifier. For example, `Group.endpoints.getUser` is typed as the `getUser` endpoint instead of the full endpoint union. +- `HttpApiEndpoint` values can now be extended as classes, matching the class-like + runtime shape already used by `HttpApi` and `HttpApiGroup`. + +## Measured Type-Level Performance + +Main/current comparisons use identical generated fixtures compiled once per +revision with TypeScript 7.0.2. The recorded revisions are `main` at +`97fdaa9c1f52` and the branch source at `5798fc5fafcd`. The focused pre/post +curves below were captured with the regular `httpapi` regression suite during +development. The retained suite uses representative stress points instead of +rerunning every point in those historical curves. All numbers are +type-instantiation deltas over the corresponding shared baseline. + +Endpoint declaration costs now grow with a lower slope: + +| endpoints | main | current | +| --------: | ------: | ------: | +| 10 | 4,580 | 2,808 | +| 50 | 15,500 | 9,168 | +| 100 | 29,150 | 17,118 | +| 500 | 138,350 | 80,718 | + +Class-like endpoint declarations are slightly cheaper than inline endpoint +values in the same 500-endpoint fixture shape: + +| fixture | inline | class-like | +| ------------- | -----: | ---------: | +| 500 endpoints | 82,207 | 71,850 | + +`HttpApiBuilder` fluent handler registration avoids the previous non-linear +blow-up in the cross-ref comparison: + +| fixture | main | current | +| ---------------- | ---------: | --------: | +| 10 endpoints | 37,856 | 11,582 | +| 50 endpoints | 568,576 | 63,702 | +| 100 endpoints | 2,154,476 | 182,852 | +| 500 endpoints | 51,741,676 | 3,296,052 | +| 500 raw handlers | 51,734,176 | 3,294,550 | + +In the recorded regular-suite measurements, `handleAll` remains the scalable +alternative to the equivalent fluent chain: + +| fixture | fluent | `handleAll` | +| -------------------- | --------: | ----------: | +| 10 endpoints | 11,579 | 9,146 | +| 50 endpoints | 63,699 | 25,106 | +| 100 endpoints | 182,849 | 45,056 | +| 500 endpoints | 3,296,049 | 204,656 | +| 500 eps, two batches | 3,296,049 | 223,613 | + +Generated-client type production also improves for the hot method-building +paths: + +| fixture | main | current | +| --------------------------------------- | ------: | ------: | +| client methods, 500 endpoints | 245,795 | 176,850 | +| top-level client methods, 500 endpoints | 243,651 | 179,809 | +| client endpoint method, 500 endpoints | 56,738 | 46,294 | +| client groups, 100 groups x 5 endpoints | 49,019 | 25,893 | + +The following focused curves were captured immediately before and after each +isolated type-level change. + +The focused `Client.Group` curve shows the improvement from consuming the +identifier-keyed endpoint map directly: + +| endpoints | union remapping | endpoint map | +| --------: | --------------: | -----------: | +| 10 | 12,448 | 12,294 | +| 50 | 19,169 | 18,935 | +| 100 | 27,570 | 27,236 | +| 500 | 94,770 | 93,636 | + +The focused `Client.TopLevelMethods` curve improves by reading endpoint +identifiers directly from the endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 12,531 | 12,476 | +| 50 | 19,252 | 19,197 | +| 100 | 27,653 | 27,598 | +| 500 | 94,853 | 94,798 | + +The focused `HttpApiClient.endpoint` selection curve improves by reading +endpoint identifiers directly from the selected endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 7,666 | 7,588 | +| 50 | 8,707 | 8,629 | +| 100 | 10,008 | 9,930 | +| 500 | 20,408 | 20,330 | + +The focused `HttpApiBuilder.endpoint` selection curve improves by reading +endpoint identifiers directly from the selected endpoint union: + +| endpoints | pre-change | post-change | +| --------: | ---------: | ----------: | +| 10 | 12,828 | 12,745 | +| 50 | 13,869 | 13,786 | +| 100 | 15,170 | 15,087 | +| 500 | 25,570 | 25,487 | + +URL builder types now avoid repeatedly expanding the full API/group shape: + +| fixture | main | current | +| ------------------------------------ | ------: | ------: | +| URL builder, 500 endpoints | 211,356 | 91,610 | +| top-level URL builder, 500 endpoints | 210,724 | 93,118 | +| builder endpoint, 500 endpoints | 62,894 | 51,952 | + +## Breaking Changes + +These changes affect unstable `HttpApi` type-level APIs and structural API, +group, and endpoint types. + +### Renamed Constraint Types + +- Broad structural constraint exports have been renamed to align with + `Schema.Constraint` terminology: `HttpApi.Any` to `HttpApi.Constraint`, + `HttpApi.AnyWithProps` to `HttpApi.Top`, `HttpApiGroup.Any` to + `HttpApiGroup.Constraint`, `HttpApiGroup.AnyWithProps` to `HttpApiGroup.Top`, + and `HttpApiEndpoint.Any` to `HttpApiEndpoint.Constraint`. +- `HttpApiEndpoint.AnyWithProps` has been replaced by `HttpApiEndpoint.Top`, whose + schema parameters are constrained to `Schema.Top`, including success and error + schemas. +- Type guards now expose the widened runtime-prop shapes: `HttpApi.isHttpApi` + returns `HttpApi.Top`, `HttpApiGroup.isHttpApiGroup` returns + `HttpApiGroup.Top`, and `HttpApiEndpoint.isHttpApiEndpoint` returns + `HttpApiEndpoint.Top`. +- `HttpApiGroup.ApiGroup` has been renamed to `HttpApiGroup.Service`. + +### API, Group, And Endpoint Shapes + +- `HttpApi.groups` is now typed as an identifier-keyed group map instead of + `ReadonlyRecord`, and `HttpApi` tracks its group union + invariantly. Dynamic string indexing must refine the key first or cast to a + broad runtime record. +- `HttpApiGroup.endpoints` is now typed as an identifier-keyed endpoint map instead of + `ReadonlyRecord`, and `HttpApiGroup` tracks its endpoint + union invariantly. Dynamic string indexing must refine the key first or cast to + a broad runtime record. +- `HttpApiEndpoint` now exposes its stable key as `identifier` instead of `name`, + aligning endpoints with APIs and groups and leaving `name` available for future + class-based endpoint patterns. +- `HttpApiEndpoint` values are now function objects instead of plain objects. + Runtime checks such as `typeof endpoint` now return `"function"`, and + `endpoint.name` is the native function name. Use `endpoint.identifier` for the + stable endpoint key. +- Identifier helper types have been renamed from `Name` / `WithName` to + `Identifier` / `WithIdentifier`; `HttpApiGroup.Service` now exposes + `identifier` instead of `name`. + +### Builder Handler Types + +- `HttpApiBuilder.Handlers` now tracks endpoints through an identifier-keyed endpoint map and a set of handled endpoint identifiers, instead of tracking the remaining endpoint union. Its public type parameters changed from `Handlers` to `Handlers`, and its phantom fields changed from `_Endpoints` to `~EndpointsByIdentifier` / `~HandledIdentifiers`. +- The unused `HttpApiBuilder.Handlers.Any` helper type has been removed. +- The exported `HttpApiBuilder.HandlersTypeId` symbol has been removed; `Handlers` + now uses a private string type id. +- Duplicate `handle` / `handleRaw` registrations for the same endpoint are rejected + at the call site, and `handleAll` rejects endpoint identifiers that were already + handled by an earlier batch. Missing endpoint handlers are still rejected by + the final `HttpApiBuilder.group` return validation. + +### Client Types + +- `HttpApiClient.Client.Group` now derives a client from a concrete group type: `Client.Group`. The previous group-union plus group-identifier form is no longer supported. +- `HttpApiClient.Client.TopLevelMethods` now returns an identifier-keyed method record instead of a union of `[identifier, method]` tuples. +- `HttpApiClient.makeWith` removes the default `HttpClientError.HttpClientError` from custom client error types in the returned `Client`, while preserving any additional custom client errors. + +### Endpoint Helper Types + +- `HttpApiEndpoint.HttpApiEndpoint` now stores lightweight phantom metadata for middleware and request shapes: `~Middleware`, `~MiddlewareServices`, `~Request`, and `~RequestRaw`. Its type identifier field is now `readonly [TypeId]: typeof TypeId`. +- `HttpApiEndpoint.Constraint` is now a lightweight structural endpoint constraint and does not extend `Pipeable`; values typed only as `HttpApiEndpoint.Constraint` do not expose `.pipe`. +- `HttpApiEndpoint.AddError` has been removed; it was not used internally by the `HttpApi` implementation. +- `HttpApiEndpoint.Json` and `HttpApiEndpoint.StringTree` have been removed in + favor of the canonical `Schema.toCodecJson` and `Schema.toCodecStringTree` + types. +- Omitted request-part metadata now remains `never` instead of being wrapped as + `Schema.toCodecStringTree`; codec metadata is applied only when + a params, query, payload, or headers schema is present. +- Success metadata now applies `Schema.toCodecJson` only to buffered + success schemas and preserves stream success schemas unchanged, including + mixed buffered and streaming success arrays. +- Handler request parts are now flattened with `Struct.Simplify`, improving + displayed request types while reducing handler instantiations. +- Endpoint helper types now read metadata fields directly instead of re-inferring all type parameters from the full `HttpApiEndpoint` interface. This affects helpers such as `Identifier`, `Success`, `Error`, `Params`, `Query`, `Payload`, `Headers`, `Middleware`, `MiddlewareServices`, `Errors`, `ErrorServicesEncode`, `ErrorServicesDecode`, `Request`, `RequestRaw`, `ServerServices`, and `ClientServices`. +- `HttpApiClient.Client.Method` and related generated-client helpers now require endpoint types that satisfy `HttpApiEndpoint.ConstraintRequest`. Endpoint-like structural types must include the lightweight request metadata fields to be accepted. diff --git a/.context/effect/.changeset/pre/optimize-node-http-server-response.md b/.context/effect/.changeset/pre/optimize-node-http-server-response.md new file mode 100644 index 000000000..1cb193cf5 --- /dev/null +++ b/.context/effect/.changeset/pre/optimize-node-http-server-response.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Optimize Node HTTP streaming responses and ensure HEAD completion and stream backpressure are handled once. diff --git a/.context/effect/.changeset/pre/optimize-schema-class-decoding.md b/.context/effect/.changeset/pre/optimize-schema-class-decoding.md new file mode 100644 index 000000000..61b14ad4c --- /dev/null +++ b/.context/effect/.changeset/pre/optimize-schema-class-decoding.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Avoid validating `Schema.Class` fields twice when decoding. diff --git a/.context/effect/.changeset/pre/optional-ai-embedding-usage.md b/.context/effect/.changeset/pre/optional-ai-embedding-usage.md new file mode 100644 index 000000000..beb5997ae --- /dev/null +++ b/.context/effect/.changeset/pre/optional-ai-embedding-usage.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow embedding usage input tokens to be omitted during decoding, including after JSON serialization. diff --git a/.context/effect/.changeset/pre/optional-ai-response-fields.md b/.context/effect/.changeset/pre/optional-ai-response-fields.md new file mode 100644 index 000000000..eebdd1ca0 --- /dev/null +++ b/.context/effect/.changeset/pre/optional-ai-response-fields.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Allow optional AI response fields to be omitted during decoding, including after JSON serialization. diff --git a/.context/effect/.changeset/otel-resource-env-precedence.md b/.context/effect/.changeset/pre/otel-resource-env-precedence.md similarity index 100% rename from .context/effect/.changeset/otel-resource-env-precedence.md rename to .context/effect/.changeset/pre/otel-resource-env-precedence.md diff --git a/.context/effect/.changeset/perfect-buckets-tickle.md b/.context/effect/.changeset/pre/perfect-buckets-tickle.md similarity index 100% rename from .context/effect/.changeset/perfect-buckets-tickle.md rename to .context/effect/.changeset/pre/perfect-buckets-tickle.md diff --git a/.context/effect/.changeset/petite-months-allow.md b/.context/effect/.changeset/pre/petite-months-allow.md similarity index 100% rename from .context/effect/.changeset/petite-months-allow.md rename to .context/effect/.changeset/pre/petite-months-allow.md diff --git a/.context/effect/.changeset/pre/pg-client-connect-error-handler.md b/.context/effect/.changeset/pre/pg-client-connect-error-handler.md new file mode 100644 index 000000000..3eed00972 --- /dev/null +++ b/.context/effect/.changeset/pre/pg-client-connect-error-handler.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Prevent unhandled `pg` client error events while `PgClient.makeClient` is connecting. diff --git a/.context/effect/.changeset/pre/plain-variant-unions.md b/.context/effect/.changeset/pre/plain-variant-unions.md new file mode 100644 index 000000000..367158347 --- /dev/null +++ b/.context/effect/.changeset/pre/plain-variant-unions.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Include plain variant structs in the default variant union. diff --git a/.context/effect/.changeset/platform-crypto-service.md b/.context/effect/.changeset/pre/platform-crypto-service.md similarity index 100% rename from .context/effect/.changeset/platform-crypto-service.md rename to .context/effect/.changeset/pre/platform-crypto-service.md diff --git a/.context/effect/.changeset/platform-node-shared-barrel.md b/.context/effect/.changeset/pre/platform-node-shared-barrel.md similarity index 100% rename from .context/effect/.changeset/platform-node-shared-barrel.md rename to .context/effect/.changeset/pre/platform-node-shared-barrel.md diff --git a/.context/effect/.changeset/plenty-moons-pull.md b/.context/effect/.changeset/pre/plenty-moons-pull.md similarity index 100% rename from .context/effect/.changeset/plenty-moons-pull.md rename to .context/effect/.changeset/pre/plenty-moons-pull.md diff --git a/.context/effect/.changeset/polite-brooms-tickle.md b/.context/effect/.changeset/pre/polite-brooms-tickle.md similarity index 100% rename from .context/effect/.changeset/polite-brooms-tickle.md rename to .context/effect/.changeset/pre/polite-brooms-tickle.md diff --git a/.context/effect/.changeset/pre/polite-cameras-rest.md b/.context/effect/.changeset/pre/polite-cameras-rest.md new file mode 100644 index 000000000..348f740e9 --- /dev/null +++ b/.context/effect/.changeset/pre/polite-cameras-rest.md @@ -0,0 +1,6 @@ +--- +"effect": patch +"@effect/platform-browser": patch +--- + +Preserve prototype accessors when code is compiled with loose object spread transforms. diff --git a/.context/effect/.changeset/pre/polite-dingos-unite.md b/.context/effect/.changeset/pre/polite-dingos-unite.md new file mode 100644 index 000000000..7ae08b5bc --- /dev/null +++ b/.context/effect/.changeset/pre/polite-dingos-unite.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve CRLF state across SSE input chunk boundaries. diff --git a/.context/effect/.changeset/polite-pigs-speak.md b/.context/effect/.changeset/pre/polite-pigs-speak.md similarity index 100% rename from .context/effect/.changeset/polite-pigs-speak.md rename to .context/effect/.changeset/pre/polite-pigs-speak.md diff --git a/.context/effect/.changeset/polite-tables-kneel.md b/.context/effect/.changeset/pre/polite-tables-kneel.md similarity index 100% rename from .context/effect/.changeset/polite-tables-kneel.md rename to .context/effect/.changeset/pre/polite-tables-kneel.md diff --git a/.context/effect/.changeset/pre/port-effect-reduce.md b/.context/effect/.changeset/pre/port-effect-reduce.md new file mode 100644 index 000000000..36632f7c2 --- /dev/null +++ b/.context/effect/.changeset/pre/port-effect-reduce.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Port `Effect.reduce` from Effect v3. diff --git a/.context/effect/.changeset/port-react-hydration.md b/.context/effect/.changeset/pre/port-react-hydration.md similarity index 100% rename from .context/effect/.changeset/port-react-hydration.md rename to .context/effect/.changeset/pre/port-react-hydration.md diff --git a/.context/effect/.changeset/pre/precise-clocks-rest.md b/.context/effect/.changeset/pre/precise-clocks-rest.md new file mode 100644 index 000000000..afe1d8a1e --- /dev/null +++ b/.context/effect/.changeset/pre/precise-clocks-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve nanosecond precision for large `TestClock` wall-clock timestamps. diff --git a/.context/effect/.changeset/pre/preserve-command-hidden-metadata.md b/.context/effect/.changeset/pre/preserve-command-hidden-metadata.md new file mode 100644 index 000000000..ceb6c6868 --- /dev/null +++ b/.context/effect/.changeset/pre/preserve-command-hidden-metadata.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve hidden command metadata when adding subcommands or shared flags. diff --git a/.context/effect/.changeset/pre/preserve-config-all-input-evidence.md b/.context/effect/.changeset/pre/preserve-config-all-input-evidence.md new file mode 100644 index 000000000..bd8b92ad8 --- /dev/null +++ b/.context/effect/.changeset/pre/preserve-config-all-input-evidence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve sibling provider input evidence when `Config.all` evaluates a failing child. diff --git a/.context/effect/.changeset/pre/preserve-otel-parent-context.md b/.context/effect/.changeset/pre/preserve-otel-parent-context.md new file mode 100644 index 000000000..d436bd971 --- /dev/null +++ b/.context/effect/.changeset/pre/preserve-otel-parent-context.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Preserve trace state and locality when adapting active OpenTelemetry parent contexts. diff --git a/.context/effect/.changeset/pretty-moments-clap.md b/.context/effect/.changeset/pre/pretty-moments-clap.md similarity index 100% rename from .context/effect/.changeset/pretty-moments-clap.md rename to .context/effect/.changeset/pre/pretty-moments-clap.md diff --git a/.context/effect/.changeset/pre/protect-schema-issue-actuals.md b/.context/effect/.changeset/pre/protect-schema-issue-actuals.md new file mode 100644 index 000000000..0a1c82a3b --- /dev/null +++ b/.context/effect/.changeset/pre/protect-schema-issue-actuals.md @@ -0,0 +1,36 @@ +--- +"effect": patch +--- + +Remove `actual` fields from every `SchemaIssue` variant, together with +`SchemaIssue.getActual`, `SchemaIssue.redact`, and `Schema.redact`. Built-in +formatters now use static messages that do not interpolate rejected input, +while paths, AST metadata, union successes, and user-provided messages and +annotations are preserved unchanged. + +Runtime performance was measured across the 16 Effect fixtures in the +`schema-benchmarks` suite. These are the scenarios used for the cross-library +comparison with Valibot and Zod. The paired HEAD-versus-`main` run classified 3 +fixtures as improvements, 0 as regressions, and 13 as inconclusive. Negative +changes are faster. Absolute library values are medians from the same +cross-library run; `—` means that the corresponding adapter does not expose +that scenario. + +| Scenario | Effect (ns/op) | Valibot (ns/op) | Zod (ns/op) | HEAD vs main | Classification | +| ------------------------ | -------------: | --------------: | ----------: | -----------: | -------------- | +| `initialization-schema` | 108191.30 | **30549.81** | 212715.66 | -0.92% | inconclusive | +| `initialization-decoder` | **109796.34** | — | — | +1.98% | inconclusive | +| `validation-valid` | 5221.80 | **5070.81** | — | +2.06% | inconclusive | +| `validation-invalid` | 1279.77 | **234.92** | — | +0.59% | inconclusive | +| `parsing-all-valid` | **5144.58** | 5192.19 | 7176.19 | -3.79% | inconclusive | +| `parsing-all-invalid` | **7594.49** | 15236.82 | 37780.35 | -5.94% | improvement | +| `parsing-first-valid` | 5188.33 | **5135.75** | — | -1.49% | inconclusive | +| `parsing-first-invalid` | 1330.82 | **243.64** | — | +1.01% | inconclusive | +| `standard-all-valid` | 5722.01 | 5200.05 | **3801.26** | -1.78% | inconclusive | +| `standard-all-invalid` | **12024.65** | 15528.50 | 30982.17 | -7.78% | improvement | +| `standard-first-valid` | **5655.33** | — | — | +3.84% | inconclusive | +| `standard-first-invalid` | **2001.69** | — | — | -4.56% | inconclusive | +| `codec-typed-encode` | 342.59 | — | **39.29** | -7.62% | inconclusive | +| `codec-typed-decode` | 418.78 | — | **50.14** | -10.89% | improvement | +| `codec-unknown-encode` | **328.38** | — | — | -5.55% | inconclusive | +| `codec-unknown-decode` | **347.35** | — | — | -5.25% | inconclusive | diff --git a/.context/effect/.changeset/pre/provider-executed-tool-results.md b/.context/effect/.changeset/pre/provider-executed-tool-results.md new file mode 100644 index 000000000..a589416b3 --- /dev/null +++ b/.context/effect/.changeset/pre/provider-executed-tool-results.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Route provider-executed tool results into the assistant message in `Prompt.fromResponseParts` diff --git a/.context/effect/.changeset/public-deer-ring.md b/.context/effect/.changeset/pre/public-deer-ring.md similarity index 100% rename from .context/effect/.changeset/public-deer-ring.md rename to .context/effect/.changeset/pre/public-deer-ring.md diff --git a/.context/effect/.changeset/public-jeans-stop.md b/.context/effect/.changeset/pre/public-jeans-stop.md similarity index 100% rename from .context/effect/.changeset/public-jeans-stop.md rename to .context/effect/.changeset/pre/public-jeans-stop.md diff --git a/.context/effect/.changeset/pubsub-publish-false.md b/.context/effect/.changeset/pre/pubsub-publish-false.md similarity index 100% rename from .context/effect/.changeset/pubsub-publish-false.md rename to .context/effect/.changeset/pre/pubsub-publish-false.md diff --git a/.context/effect/.changeset/puny-pens-clap.md b/.context/effect/.changeset/pre/puny-pens-clap.md similarity index 100% rename from .context/effect/.changeset/puny-pens-clap.md rename to .context/effect/.changeset/pre/puny-pens-clap.md diff --git a/.context/effect/.changeset/purple-bars-prove.md b/.context/effect/.changeset/pre/purple-bars-prove.md similarity index 100% rename from .context/effect/.changeset/purple-bars-prove.md rename to .context/effect/.changeset/pre/purple-bars-prove.md diff --git a/.context/effect/.changeset/purple-schools-float.md b/.context/effect/.changeset/pre/purple-schools-float.md similarity index 100% rename from .context/effect/.changeset/purple-schools-float.md rename to .context/effect/.changeset/pre/purple-schools-float.md diff --git a/.context/effect/.changeset/purple-turtles-draw.md b/.context/effect/.changeset/pre/purple-turtles-draw.md similarity index 100% rename from .context/effect/.changeset/purple-turtles-draw.md rename to .context/effect/.changeset/pre/purple-turtles-draw.md diff --git a/.context/effect/.changeset/quick-dragons-fix.md b/.context/effect/.changeset/pre/quick-dragons-fix.md similarity index 100% rename from .context/effect/.changeset/quick-dragons-fix.md rename to .context/effect/.changeset/pre/quick-dragons-fix.md diff --git a/.context/effect/.changeset/quick-falcons-travel.md b/.context/effect/.changeset/pre/quick-falcons-travel.md similarity index 100% rename from .context/effect/.changeset/quick-falcons-travel.md rename to .context/effect/.changeset/pre/quick-falcons-travel.md diff --git a/.context/effect/.changeset/quick-geese-relax.md b/.context/effect/.changeset/pre/quick-geese-relax.md similarity index 100% rename from .context/effect/.changeset/quick-geese-relax.md rename to .context/effect/.changeset/pre/quick-geese-relax.md diff --git a/.context/effect/.changeset/pre/quick-kiwis-remember.md b/.context/effect/.changeset/pre/quick-kiwis-remember.md new file mode 100644 index 000000000..2c1b55d09 --- /dev/null +++ b/.context/effect/.changeset/pre/quick-kiwis-remember.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Persist permanent entries in KVS `setMany` operations. diff --git a/.context/effect/.changeset/quick-lamps-dig.md b/.context/effect/.changeset/pre/quick-lamps-dig.md similarity index 100% rename from .context/effect/.changeset/quick-lamps-dig.md rename to .context/effect/.changeset/pre/quick-lamps-dig.md diff --git a/.context/effect/.changeset/quick-lizards-fall.md b/.context/effect/.changeset/pre/quick-lizards-fall.md similarity index 100% rename from .context/effect/.changeset/quick-lizards-fall.md rename to .context/effect/.changeset/pre/quick-lizards-fall.md diff --git a/.context/effect/.changeset/pre/quick-schedulers-promise.md b/.context/effect/.changeset/pre/quick-schedulers-promise.md new file mode 100644 index 000000000..0cddcb3f3 --- /dev/null +++ b/.context/effect/.changeset/pre/quick-schedulers-promise.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use Promise microtasks for synchronous Scheduler dispatch. diff --git a/.context/effect/.changeset/quick-trees-join.md b/.context/effect/.changeset/pre/quick-trees-join.md similarity index 100% rename from .context/effect/.changeset/quick-trees-join.md rename to .context/effect/.changeset/pre/quick-trees-join.md diff --git a/.context/effect/.changeset/quiet-carpets-grin.md b/.context/effect/.changeset/pre/quiet-carpets-grin.md similarity index 100% rename from .context/effect/.changeset/quiet-carpets-grin.md rename to .context/effect/.changeset/pre/quiet-carpets-grin.md diff --git a/.context/effect/.changeset/pre/quiet-clis-parse.md b/.context/effect/.changeset/pre/quiet-clis-parse.md new file mode 100644 index 000000000..2fc507ec8 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-clis-parse.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix unstable CLI subcommands dropping operands after the `--` end-of-options terminator. diff --git a/.context/effect/.changeset/pre/quiet-crons-report.md b/.context/effect/.changeset/pre/quiet-crons-report.md new file mode 100644 index 000000000..d29a3bd23 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-crons-report.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct the diagnostic for cron step values above a field's maximum. diff --git a/.context/effect/.changeset/pre/quiet-fibers-settle.md b/.context/effect/.changeset/pre/quiet-fibers-settle.md new file mode 100644 index 000000000..81ca36e68 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-fibers-settle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix fiber self-interuption from inside a running operation diff --git a/.context/effect/.changeset/quiet-files-hunt.md b/.context/effect/.changeset/pre/quiet-files-hunt.md similarity index 100% rename from .context/effect/.changeset/quiet-files-hunt.md rename to .context/effect/.changeset/pre/quiet-files-hunt.md diff --git a/.context/effect/.changeset/pre/quiet-files-write.md b/.context/effect/.changeset/pre/quiet-files-write.md new file mode 100644 index 000000000..fb549eb54 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-files-write.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-deno": patch +--- + +Honor `FileSystem.writeFile` open flags in the Deno implementation. diff --git a/.context/effect/.changeset/quiet-lamps-jam.md b/.context/effect/.changeset/pre/quiet-lamps-jam.md similarity index 100% rename from .context/effect/.changeset/quiet-lamps-jam.md rename to .context/effect/.changeset/pre/quiet-lamps-jam.md diff --git a/.context/effect/.changeset/pre/quiet-mice-negotiate.md b/.context/effect/.changeset/pre/quiet-mice-negotiate.md new file mode 100644 index 000000000..c54d96dde --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-mice-negotiate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add adapter-valued MCP server protocol declarations, route requests through the selected protocol before schema decoding, and add built-in support for MCP `2025-06-18`. diff --git a/.context/effect/.changeset/pre/quiet-observers-report.md b/.context/effect/.changeset/pre/quiet-observers-report.md new file mode 100644 index 000000000..d547f9aac --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-observers-report.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent execution-plan event observer defects from changing attempt outcomes or leaving attempt events unpaired. diff --git a/.context/effect/.changeset/pre/quiet-otters-retry.md b/.context/effect/.changeset/pre/quiet-otters-retry.md new file mode 100644 index 000000000..7673c4702 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-otters-retry.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor HTTP-date `Retry-After` values when retrying OTLP exports. diff --git a/.context/effect/.changeset/pre/quiet-owls-validate.md b/.context/effect/.changeset/pre/quiet-owls-validate.md new file mode 100644 index 000000000..6a85ac0b3 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-owls-validate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP Streamable HTTP servers now validate content negotiation, session lifecycle, negotiated protocol versions, and browser Origins before dispatching requests. diff --git a/.context/effect/.changeset/pre/quiet-pandas-rebuild.md b/.context/effect/.changeset/pre/quiet-pandas-rebuild.md new file mode 100644 index 000000000..879a8c442 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-pandas-rebuild.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix failed `ResourceRef` rebuilds permanently blocking waiters. diff --git a/.context/effect/.changeset/pre/quiet-pandas-respond.md b/.context/effect/.changeset/pre/quiet-pandas-respond.md new file mode 100644 index 000000000..b52a3f1c3 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-pandas-respond.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Make multipart errors respond with an HTTP status based on their reason and ignore them in the error reporter. diff --git a/.context/effect/.changeset/quiet-radios-wave.md b/.context/effect/.changeset/pre/quiet-radios-wave.md similarity index 100% rename from .context/effect/.changeset/quiet-radios-wave.md rename to .context/effect/.changeset/pre/quiet-radios-wave.md diff --git a/.context/effect/.changeset/quiet-redis-scripts.md b/.context/effect/.changeset/pre/quiet-redis-scripts.md similarity index 100% rename from .context/effect/.changeset/quiet-redis-scripts.md rename to .context/effect/.changeset/pre/quiet-redis-scripts.md diff --git a/.context/effect/.changeset/pre/quiet-savepoints-wait.md b/.context/effect/.changeset/pre/quiet-savepoints-wait.md new file mode 100644 index 000000000..e27e942f1 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-savepoints-wait.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Serialize concurrent nested SQL transactions to prevent savepoint collisions. Cross-dependent sibling nested +transactions now deadlock instead of interleaving and risking silent data corruption. diff --git a/.context/effect/.changeset/pre/quiet-sockets-close.md b/.context/effect/.changeset/pre/quiet-sockets-close.md new file mode 100644 index 000000000..6d2d9ab73 --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-sockets-close.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Close pending TCP and WebSocket connections when a scoped socket server shuts down diff --git a/.context/effect/.changeset/pre/quiet-spans-rest.md b/.context/effect/.changeset/pre/quiet-spans-rest.md new file mode 100644 index 000000000..9e574309a --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-spans-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Skip HTTP server span attribute collection when the span is not sampled. diff --git a/.context/effect/.changeset/quiet-tigers-yell.md b/.context/effect/.changeset/pre/quiet-tigers-yell.md similarity index 100% rename from .context/effect/.changeset/quiet-tigers-yell.md rename to .context/effect/.changeset/pre/quiet-tigers-yell.md diff --git a/.context/effect/.changeset/pre/quiet-tools-smile.md b/.context/effect/.changeset/pre/quiet-tools-smile.md new file mode 100644 index 000000000..d14443e6e --- /dev/null +++ b/.context/effect/.changeset/pre/quiet-tools-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent MCP tool failures from exposing Cause rendering, stack traces, and internal paths while preserving actionable validation messages. diff --git a/.context/effect/.changeset/quiet-turtles-smile.md b/.context/effect/.changeset/pre/quiet-turtles-smile.md similarity index 100% rename from .context/effect/.changeset/quiet-turtles-smile.md rename to .context/effect/.changeset/pre/quiet-turtles-smile.md diff --git a/.context/effect/.changeset/random-choice.md b/.context/effect/.changeset/pre/random-choice.md similarity index 100% rename from .context/effect/.changeset/random-choice.md rename to .context/effect/.changeset/pre/random-choice.md diff --git a/.context/effect/.changeset/pre/read-only-bun-sqlite.md b/.context/effect/.changeset/pre/read-only-bun-sqlite.md new file mode 100644 index 000000000..9037b67ad --- /dev/null +++ b/.context/effect/.changeset/pre/read-only-bun-sqlite.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Enforce read-only mode when opening Bun SQLite databases. diff --git a/.context/effect/.changeset/ready-olives-divide.md b/.context/effect/.changeset/pre/ready-olives-divide.md similarity index 100% rename from .context/effect/.changeset/ready-olives-divide.md rename to .context/effect/.changeset/pre/ready-olives-divide.md diff --git a/.context/effect/.changeset/real-trains-ring.md b/.context/effect/.changeset/pre/real-trains-ring.md similarity index 100% rename from .context/effect/.changeset/real-trains-ring.md rename to .context/effect/.changeset/pre/real-trains-ring.md diff --git a/.context/effect/.changeset/pre/record-from-iterable-by-dual.md b/.context/effect/.changeset/pre/record-from-iterable-by-dual.md new file mode 100644 index 000000000..13fc30740 --- /dev/null +++ b/.context/effect/.changeset/pre/record-from-iterable-by-dual.md @@ -0,0 +1,16 @@ +--- +"effect": patch +--- + +Record: make `fromIterableBy` dual, allowing data-last usage in `pipe` + +```ts +import { pipe, Record } from "effect" + +const users = [ + { id: "2", name: "name2" }, + { id: "1", name: "name1" } +] + +pipe(users, Record.fromIterableBy((user) => user.id)) +``` diff --git a/.context/effect/.changeset/red-pigs-repair.md b/.context/effect/.changeset/pre/red-pigs-repair.md similarity index 100% rename from .context/effect/.changeset/red-pigs-repair.md rename to .context/effect/.changeset/pre/red-pigs-repair.md diff --git a/.context/effect/.changeset/redacted-representation-options.md b/.context/effect/.changeset/pre/redacted-representation-options.md similarity index 100% rename from .context/effect/.changeset/redacted-representation-options.md rename to .context/effect/.changeset/pre/redacted-representation-options.md diff --git a/.context/effect/.changeset/refactor-cli-global-flags.md b/.context/effect/.changeset/pre/refactor-cli-global-flags.md similarity index 100% rename from .context/effect/.changeset/refactor-cli-global-flags.md rename to .context/effect/.changeset/pre/refactor-cli-global-flags.md diff --git a/.context/effect/.changeset/refactor-config-provider.md b/.context/effect/.changeset/pre/refactor-config-provider.md similarity index 100% rename from .context/effect/.changeset/refactor-config-provider.md rename to .context/effect/.changeset/pre/refactor-config-provider.md diff --git a/.context/effect/.changeset/refactor-representation-references.md b/.context/effect/.changeset/pre/refactor-representation-references.md similarity index 100% rename from .context/effect/.changeset/refactor-representation-references.md rename to .context/effect/.changeset/pre/refactor-representation-references.md diff --git a/.context/effect/.changeset/pre/refine-config-absence.md b/.context/effect/.changeset/pre/refine-config-absence.md new file mode 100644 index 000000000..bd2a1c58c --- /dev/null +++ b/.context/effect/.changeset/pre/refine-config-absence.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Refine `Config` loading and absence semantics. `Config.schema` now derives a provider loading policy from the encoded `StringTree` schema, materializes mixed-shape union members independently, and leaves separated scalar parsing to `Config.Array` and `Config.Record`. Schemas whose canonical `StringTree` encoding remains opaque, such as `Schema.Any`, `Schema.Unknown`, or `Schema.Json`, are rejected when the config is constructed; use a concrete shape or `Schema.fromJsonString(Schema.Json)` for scalar JSON. Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide absence. Partially supplied `Config.all` groups are rejected, successful values such as `undefined` and explicitly present empty structures are preserved, and the internal path prefix is removed from the public `Config.parse` signature. diff --git a/.context/effect/.changeset/pre/refresh-mcp-roots.md b/.context/effect/.changeset/pre/refresh-mcp-roots.md new file mode 100644 index 000000000..501d714fd --- /dev/null +++ b/.context/effect/.changeset/pre/refresh-mcp-roots.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now refresh roots after capable clients report that their root list changed. diff --git a/.context/effect/.changeset/pre/register-param-alternate-flags.md b/.context/effect/.changeset/pre/register-param-alternate-flags.md new file mode 100644 index 000000000..511689a84 --- /dev/null +++ b/.context/effect/.changeset/pre/register-param-alternate-flags.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Register alternate flags used by `Param.orElse` and `Param.orElseResult`. diff --git a/.context/effect/.changeset/pre/release-rpc-worker-pool-entries.md b/.context/effect/.changeset/pre/release-rpc-worker-pool-entries.md new file mode 100644 index 000000000..fe2aa1ba1 --- /dev/null +++ b/.context/effect/.changeset/pre/release-rpc-worker-pool-entries.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Release worker pool entries when an RPC worker's receive loop fails. diff --git a/.context/effect/.changeset/pre/remove-context-mutate.md b/.context/effect/.changeset/pre/remove-context-mutate.md new file mode 100644 index 000000000..55d8b1a8c --- /dev/null +++ b/.context/effect/.changeset/pre/remove-context-mutate.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Context.mutate` and `Context.getReferenceUnsafe`. Context updates now use overlays, and `Context.get` resolves reference defaults. diff --git a/.context/effect/.changeset/remove-effect-transactionwith.md b/.context/effect/.changeset/pre/remove-effect-transactionwith.md similarity index 100% rename from .context/effect/.changeset/remove-effect-transactionwith.md rename to .context/effect/.changeset/pre/remove-effect-transactionwith.md diff --git a/.context/effect/.changeset/remove-http-span-counter.md b/.context/effect/.changeset/pre/remove-http-span-counter.md similarity index 100% rename from .context/effect/.changeset/remove-http-span-counter.md rename to .context/effect/.changeset/pre/remove-http-span-counter.md diff --git a/.context/effect/.changeset/remove-nullor.md b/.context/effect/.changeset/pre/remove-nullor.md similarity index 100% rename from .context/effect/.changeset/remove-nullor.md rename to .context/effect/.changeset/pre/remove-nullor.md diff --git a/.context/effect/.changeset/remove-openapi-fromapi-options.md b/.context/effect/.changeset/pre/remove-openapi-fromapi-options.md similarity index 100% rename from .context/effect/.changeset/remove-openapi-fromapi-options.md rename to .context/effect/.changeset/pre/remove-openapi-fromapi-options.md diff --git a/.context/effect/.changeset/pre/remove-schedule-apis.md b/.context/effect/.changeset/pre/remove-schedule-apis.md new file mode 100644 index 000000000..79d641e92 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schedule-apis.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove some Schedule APIs: `collectInputs`, `collectOutputs`, `collectWhile`, `delays`, `reduce`, `satisfiesErrorType`, `satisfiesInputType`, `satisfiesOutputType`, `satisfiesServicesType`, and `unfold`. diff --git a/.context/effect/.changeset/pre/remove-schedule-either.md b/.context/effect/.changeset/pre/remove-schedule-either.md new file mode 100644 index 000000000..ee0be8634 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schedule-either.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the Schedule.either APIs and add Schedule.min for fastest-duration schedule composition. diff --git a/.context/effect/.changeset/pre/remove-schedule-elapsed.md b/.context/effect/.changeset/pre/remove-schedule-elapsed.md new file mode 100644 index 000000000..1863f1496 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schedule-elapsed.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Schedule.elapsed`. diff --git a/.context/effect/.changeset/pre/remove-schedule-taps.md b/.context/effect/.changeset/pre/remove-schedule-taps.md new file mode 100644 index 000000000..18572cb53 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schedule-taps.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Schedule.tapInput` and `Schedule.tapOutput`. Use `Schedule.tap` instead. diff --git a/.context/effect/.changeset/pre/remove-schema-key-value-combiner.md b/.context/effect/.changeset/pre/remove-schema-key-value-combiner.md new file mode 100644 index 000000000..05add770f --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schema-key-value-combiner.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Remove the `keyValueCombiner` option from `Schema.Record` and the corresponding +`SchemaAST.KeyValueCombiner` and `SchemaAST.IndexSignature.merge` APIs. +For transformed key collisions, sequential parsing keeps the later selected +value, while concurrent parsing keeps the value applied last in completion +order. diff --git a/.context/effect/.changeset/remove-schema-stringtree-keep-declarations.md b/.context/effect/.changeset/pre/remove-schema-stringtree-keep-declarations.md similarity index 100% rename from .context/effect/.changeset/remove-schema-stringtree-keep-declarations.md rename to .context/effect/.changeset/pre/remove-schema-stringtree-keep-declarations.md diff --git a/.context/effect/.changeset/pre/remove-schema-utils.md b/.context/effect/.changeset/pre/remove-schema-utils.md new file mode 100644 index 000000000..cb605c700 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-schema-utils.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove the experimental `SchemaUtils` module and its `getNativeClassSchema` helper. The helper duplicated a composition already available through the primary Schema APIs and did not justify a separate public module. diff --git a/.context/effect/.changeset/remove-types-mergerecord.md b/.context/effect/.changeset/pre/remove-types-mergerecord.md similarity index 100% rename from .context/effect/.changeset/remove-types-mergerecord.md rename to .context/effect/.changeset/pre/remove-types-mergerecord.md diff --git a/.context/effect/.changeset/remove-unused-utils-apis.md b/.context/effect/.changeset/pre/remove-unused-utils-apis.md similarity index 100% rename from .context/effect/.changeset/remove-unused-utils-apis.md rename to .context/effect/.changeset/pre/remove-unused-utils-apis.md diff --git a/.context/effect/.changeset/pre/remove-with-concurrency.md b/.context/effect/.changeset/pre/remove-with-concurrency.md new file mode 100644 index 000000000..d1d820b42 --- /dev/null +++ b/.context/effect/.changeset/pre/remove-with-concurrency.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Remove `Effect.withConcurrency`, the `References.CurrentConcurrency` reference backing it, and the `"inherit"` option from `Types.Concurrency`. Use an explicit `number` or `"unbounded"` concurrency value instead. diff --git a/.context/effect/.changeset/rename-rebuild-out.md b/.context/effect/.changeset/pre/rename-rebuild-out.md similarity index 100% rename from .context/effect/.changeset/rename-rebuild-out.md rename to .context/effect/.changeset/pre/rename-rebuild-out.md diff --git a/.context/effect/.changeset/pre/rename-schema-error-constructors.md b/.context/effect/.changeset/pre/rename-schema-error-constructors.md new file mode 100644 index 000000000..f4ebfb8fe --- /dev/null +++ b/.context/effect/.changeset/pre/rename-schema-error-constructors.md @@ -0,0 +1,10 @@ +--- +"effect": patch +--- + +Rename the Schema error constructors to align with their `Data` counterparts. + +- `Schema.ErrorClass` is now `Schema.Error`. +- `Schema.TaggedErrorClass` is now `Schema.TaggedError`. +- The JavaScript `Error` instance schema is now `Schema.ErrorInstance`. +- `Schema.ErrorReviver` is now `Schema.ErrorInstanceReviver`. diff --git a/.context/effect/.changeset/pre/render-cli-user-errors.md b/.context/effect/.changeset/pre/render-cli-user-errors.md new file mode 100644 index 000000000..cb8229f8a --- /dev/null +++ b/.context/effect/.changeset/pre/render-cli-user-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add an optional user-facing message to CLI `UserError` values with safe cause-based fallbacks. `Command.run` and `Command.runWith` now render handler `UserError` failures through the installed output formatter; hosts that already print these errors should remove their duplicate output. Set `renderErrors: false` when the host should own error rendering. diff --git a/.context/effect/.changeset/pre/report-schema-input.md b/.context/effect/.changeset/pre/report-schema-input.md new file mode 100644 index 000000000..038f5d936 --- /dev/null +++ b/.context/effect/.changeset/pre/report-schema-input.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Add the opt-in `reportInput` parse option for retaining rejected inputs in enumerable fields on value-bearing schema issues and including them in default formatted messages. Value-bearing issue constructors accept the rejected input and parse options directly, and `Schema.Annotations.Issue` now supports `expected` for default messages. + +Schema issues no longer format implicitly through `Issue#toString`. Use `SchemaIssue.makeFormatterDefault()` when a human-readable message is needed. The throwing and Promise-based adapters in `SchemaParser` now use the generic message `"Schema validation failed"` and expose the structured `SchemaIssue.Issue` as the error `cause`; consumers that previously read the formatted error message should inspect and explicitly format that cause instead. + +`Schema.makeEffect` now returns `SchemaIssue.Issue` failures instead of wrapping them in `SchemaError`, and `Schema.withConstructorDefault` accepts an `Effect` that fails with `SchemaIssue.Issue`. Fallible `Optic` operations return structured `SchemaIssue.Issue` failures, while schema failures from `Schema.toIso` and `Schema.toDifferJsonPatch` use the generic error message and preserve the issue in `cause` instead of formatting it internally. diff --git a/.context/effect/.changeset/pre/report-transient-rpc-socket-errors.md b/.context/effect/.changeset/pre/report-transient-rpc-socket-errors.md new file mode 100644 index 000000000..3c81a4050 --- /dev/null +++ b/.context/effect/.changeset/pre/report-transient-rpc-socket-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Report retried RPC socket open failures through the `onTransientError` protocol hook and fail in-flight requests when the retry policy is exhausted. diff --git a/.context/effect/.changeset/pre/resource-subscriptions.md b/.context/effect/.changeset/pre/resource-subscriptions.md new file mode 100644 index 000000000..c91274c3b --- /dev/null +++ b/.context/effect/.changeset/pre/resource-subscriptions.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP servers now support session-scoped resource subscriptions on transports that can deliver server notifications and filter resource updates by each client's subscribed URIs. diff --git a/.context/effect/.changeset/restore-schema-parse-options.md b/.context/effect/.changeset/pre/restore-schema-parse-options.md similarity index 100% rename from .context/effect/.changeset/restore-schema-parse-options.md rename to .context/effect/.changeset/pre/restore-schema-parse-options.md diff --git a/.context/effect/.changeset/pre/result-map-error-success-identity.md b/.context/effect/.changeset/pre/result-map-error-success-identity.md new file mode 100644 index 000000000..dbcacc774 --- /dev/null +++ b/.context/effect/.changeset/pre/result-map-error-success-identity.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Preserve untouched `Result` branches by identity in `Result.map` and +`Result.mapError`. diff --git a/.context/effect/.changeset/pre/retry-redis-script-load.md b/.context/effect/.changeset/pre/retry-redis-script-load.md new file mode 100644 index 000000000..c4017e413 --- /dev/null +++ b/.context/effect/.changeset/pre/retry-redis-script-load.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Redis script evaluation so transient `SCRIPT LOAD` failures are retried instead of being cached indefinitely. diff --git a/.context/effect/.changeset/pre/reuse-httpapi-response-schemas.md b/.context/effect/.changeset/pre/reuse-httpapi-response-schemas.md new file mode 100644 index 000000000..1c7d4c087 --- /dev/null +++ b/.context/effect/.changeset/pre/reuse-httpapi-response-schemas.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Reuse HttpApi response schemas. + +`HttpApiBuilder` looked up cached response schemas by their source AST but stored them by the transformed AST, so the cache normally missed. It now uses the source AST consistently. diff --git a/.context/effect/.changeset/rich-dots-push.md b/.context/effect/.changeset/pre/rich-dots-push.md similarity index 100% rename from .context/effect/.changeset/rich-dots-push.md rename to .context/effect/.changeset/pre/rich-dots-push.md diff --git a/.context/effect/.changeset/rich-hoops-nail.md b/.context/effect/.changeset/pre/rich-hoops-nail.md similarity index 100% rename from .context/effect/.changeset/rich-hoops-nail.md rename to .context/effect/.changeset/pre/rich-hoops-nail.md diff --git a/.context/effect/.changeset/rich-sloths-draw.md b/.context/effect/.changeset/pre/rich-sloths-draw.md similarity index 100% rename from .context/effect/.changeset/rich-sloths-draw.md rename to .context/effect/.changeset/pre/rich-sloths-draw.md diff --git a/.context/effect/.changeset/ripe-lies-battle.md b/.context/effect/.changeset/pre/ripe-lies-battle.md similarity index 100% rename from .context/effect/.changeset/ripe-lies-battle.md rename to .context/effect/.changeset/pre/ripe-lies-battle.md diff --git a/.context/effect/.changeset/rpc-client-http-early-close.md b/.context/effect/.changeset/pre/rpc-client-http-early-close.md similarity index 100% rename from .context/effect/.changeset/rpc-client-http-early-close.md rename to .context/effect/.changeset/pre/rpc-client-http-early-close.md diff --git a/.context/effect/.changeset/rpc-middleware-provides-fix.md b/.context/effect/.changeset/pre/rpc-middleware-provides-fix.md similarity index 100% rename from .context/effect/.changeset/rpc-middleware-provides-fix.md rename to .context/effect/.changeset/pre/rpc-middleware-provides-fix.md diff --git a/.context/effect/.changeset/pre/safe-json-schema-patterns.md b/.context/effect/.changeset/pre/safe-json-schema-patterns.md new file mode 100644 index 000000000..ab10a6aa0 --- /dev/null +++ b/.context/effect/.changeset/pre/safe-json-schema-patterns.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Require explicit handling for regular expression pattern constraints translated from JSON Schema documents, with modes to apply trusted patterns or ignore their constraints. diff --git a/.context/effect/.changeset/scalar-custom-fetch.md b/.context/effect/.changeset/pre/scalar-custom-fetch.md similarity index 100% rename from .context/effect/.changeset/scalar-custom-fetch.md rename to .context/effect/.changeset/pre/scalar-custom-fetch.md diff --git a/.context/effect/.changeset/pre/schema-arbitrary-factory.md b/.context/effect/.changeset/pre/schema-arbitrary-factory.md new file mode 100644 index 000000000..d0802c815 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-arbitrary-factory.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Consolidate schema arbitrary derivation into `Schema.toArbitrary`, which now returns a `Schema.Arbitrary` factory that accepts the fast-check module. Remove `Schema.toArbitraryLazy` and arbitrary derivation reports. diff --git a/.context/effect/.changeset/schema-as-class.md b/.context/effect/.changeset/pre/schema-as-class.md similarity index 100% rename from .context/effect/.changeset/schema-as-class.md rename to .context/effect/.changeset/pre/schema-as-class.md diff --git a/.context/effect/.changeset/schema-asserts-signature.md b/.context/effect/.changeset/pre/schema-asserts-signature.md similarity index 100% rename from .context/effect/.changeset/schema-asserts-signature.md rename to .context/effect/.changeset/pre/schema-asserts-signature.md diff --git a/.context/effect/.changeset/schema-clean-up-additionalProperties.md b/.context/effect/.changeset/pre/schema-clean-up-additionalProperties.md similarity index 100% rename from .context/effect/.changeset/schema-clean-up-additionalProperties.md rename to .context/effect/.changeset/pre/schema-clean-up-additionalProperties.md diff --git a/.context/effect/.changeset/pre/schema-codec-narrowing.md b/.context/effect/.changeset/pre/schema-codec-narrowing.md new file mode 100644 index 000000000..c47989842 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-codec-narrowing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Schema: add `Schema.Decoder` and `Schema.Encoder`, and accept simpler schema types in APIs that only decode, only encode, or only need the basic schema shape, closes #2536 diff --git a/.context/effect/.changeset/pre/schema-date-valid.md b/.context/effect/.changeset/pre/schema-date-valid.md new file mode 100644 index 000000000..a7fbee4e9 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-date-valid.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Make `Schema.Date` reject invalid dates and remove the redundant `Schema.DateValid`, `Schema.isDateValid`, and `Schema.isDateValidReviver` APIs. + +`Schema.DateFromString` and `Schema.DateFromMillis` now fail decoding when their input would produce an invalid date. + +Remove `Schema.Annotations.ToArbitrary.GenerationConstraint.valid`; `Schema.Date` arbitraries now generate only valid dates by default. diff --git a/.context/effect/.changeset/schema-datetime-utc-from-string.md b/.context/effect/.changeset/pre/schema-datetime-utc-from-string.md similarity index 100% rename from .context/effect/.changeset/schema-datetime-utc-from-string.md rename to .context/effect/.changeset/pre/schema-datetime-utc-from-string.md diff --git a/.context/effect/.changeset/schema-decoding-defaults-services.md b/.context/effect/.changeset/pre/schema-decoding-defaults-services.md similarity index 100% rename from .context/effect/.changeset/schema-decoding-defaults-services.md rename to .context/effect/.changeset/pre/schema-decoding-defaults-services.md diff --git a/.context/effect/.changeset/schema-defaults-issue-channel.md b/.context/effect/.changeset/pre/schema-defaults-issue-channel.md similarity index 100% rename from .context/effect/.changeset/schema-defaults-issue-channel.md rename to .context/effect/.changeset/pre/schema-defaults-issue-channel.md diff --git a/.context/effect/.changeset/pre/schema-direct-class-extension.md b/.context/effect/.changeset/pre/schema-direct-class-extension.md new file mode 100644 index 000000000..43c457b1a --- /dev/null +++ b/.context/effect/.changeset/pre/schema-direct-class-extension.md @@ -0,0 +1,22 @@ +--- +"effect": patch +--- + +Schema: make schemas directly extendable as classes with static method support +and remove `Schema.asClass`. + +`Bottom` and `BottomLazy` now include the class-compatible `new` signature, +while `BottomWithoutNew` and `BottomLazyWithoutNew` expose the schema protocol +without it for schema types that define a specialized construct signature. + +**Example** + +```ts +import { Schema } from "effect" + +class MyString extends Schema.String { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) +} + +MyString.decodeUnknownSync("a") // "a" +``` diff --git a/.context/effect/.changeset/schema-dollar-prefix.md b/.context/effect/.changeset/pre/schema-dollar-prefix.md similarity index 100% rename from .context/effect/.changeset/schema-dollar-prefix.md rename to .context/effect/.changeset/pre/schema-dollar-prefix.md diff --git a/.context/effect/.changeset/schema-lazy-bottom.md b/.context/effect/.changeset/pre/schema-lazy-bottom.md similarity index 100% rename from .context/effect/.changeset/schema-lazy-bottom.md rename to .context/effect/.changeset/pre/schema-lazy-bottom.md diff --git a/.context/effect/.changeset/schema-missing-self-generic.md b/.context/effect/.changeset/pre/schema-missing-self-generic.md similarity index 100% rename from .context/effect/.changeset/schema-missing-self-generic.md rename to .context/effect/.changeset/pre/schema-missing-self-generic.md diff --git a/.context/effect/.changeset/schema-ordered-arbitrary-constraints.md b/.context/effect/.changeset/pre/schema-ordered-arbitrary-constraints.md similarity index 100% rename from .context/effect/.changeset/schema-ordered-arbitrary-constraints.md rename to .context/effect/.changeset/pre/schema-ordered-arbitrary-constraints.md diff --git a/.context/effect/.changeset/schema-parser-adapter-errors.md b/.context/effect/.changeset/pre/schema-parser-adapter-errors.md similarity index 100% rename from .context/effect/.changeset/schema-parser-adapter-errors.md rename to .context/effect/.changeset/pre/schema-parser-adapter-errors.md diff --git a/.context/effect/.changeset/schema-refactor-toCodecJson.md b/.context/effect/.changeset/pre/schema-refactor-toCodecJson.md similarity index 100% rename from .context/effect/.changeset/schema-refactor-toCodecJson.md rename to .context/effect/.changeset/pre/schema-refactor-toCodecJson.md diff --git a/.context/effect/.changeset/schema-remove-annotate-in.md b/.context/effect/.changeset/pre/schema-remove-annotate-in.md similarity index 100% rename from .context/effect/.changeset/schema-remove-annotate-in.md rename to .context/effect/.changeset/pre/schema-remove-annotate-in.md diff --git a/.context/effect/.changeset/schema-rename-makeUnsafe-to-make.md b/.context/effect/.changeset/pre/schema-rename-makeUnsafe-to-make.md similarity index 100% rename from .context/effect/.changeset/schema-rename-makeUnsafe-to-make.md rename to .context/effect/.changeset/pre/schema-rename-makeUnsafe-to-make.md diff --git a/.context/effect/.changeset/schema-rename-parser-makeUnsafe.md b/.context/effect/.changeset/pre/schema-rename-parser-makeUnsafe.md similarity index 100% rename from .context/effect/.changeset/schema-rename-parser-makeUnsafe.md rename to .context/effect/.changeset/pre/schema-rename-parser-makeUnsafe.md diff --git a/.context/effect/.changeset/pre/schema-representation-refactoring.md b/.context/effect/.changeset/pre/schema-representation-refactoring.md new file mode 100644 index 000000000..fddaef586 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-representation-refactoring.md @@ -0,0 +1,71 @@ +--- +"effect": patch +--- + +Refactor the `SchemaRepresentation` module to improve clarity and maintainability. + +The representation pipeline is now open and compiler-extensible. The same encoded-side representation is used for JSON persistence, runtime reconstruction, JSON Schema Draft 2020-12 compilation, TypeScript code generation, AI structured output, and HTTP / OpenAPI schemas. + +### New representation model + +- Add `RepresentationAnnotation` and `CheckRepresentationAnnotation`, which identify declarations and checks with a stable `id`, JSON `payload`, and optional schema dependencies. +- Preserve checks on every non-reference representation node instead of storing constraints in the previous closed `meta` unions. +- Add compiler hooks for checks and declarations through `SchemaRepresentation.ToJsonSchema` and `SchemaRepresentation.Generation`. +- Add `SchemaMultiDocument`, `fromSchemaMultiDocument`, and `fromRepresentations` so several live schemas and named definitions can be converted and reconstructed together. Explicit definitions are preserved even when no root references them. +- Preserve shared structural nodes, annotated recursion, union member order, identifiers, reference siblings, and structural checks when projecting encoded schemas. + +### Persistence and revivers + +- Add `toJson`, `fromJson`, `toJsonMultiDocument`, and `fromJsonMultiDocument` as the persistence boundary for representation documents. +- Live representations store literal, enum, and property-name scalars as native values. JSON persistence encodes them as `{ type, value }` tagged unions so their runtime types remain distinct across persistence formats, canonically encodes structural bigint and global symbol values, keeps JSON-valued annotations, and removes runtime-only callbacks and other non-JSON annotation values. +- Replace the generic reviver callback with typed `DeclarationReviver`, `FilterReviver`, and `FilterGroupReviver` contracts. Add `makeDeclarationReviver`, `makeFilterReviver`, and `makeFilterGroupReviver`, which infer their payload type from `payloadSchema`. +- Resolve acyclic references to concrete runtime schemas and reserve `Schema.suspend` wrappers for recursive back-edges. Acyclic alias chains may be normalized while preserving the outer reference identifier. +- Export individual revivers for built-in declarations and checks from `Schema`. Consumers opt in to exactly the revivers accepted when reconstructing persisted documents: + - declaration revivers: `OptionReviver`, `ResultReviver`, `RedactedReviver`, `CauseReasonReviver`, `CauseReviver`, `ErrorReviver`, `ExitReviver`, `ReadonlyMapReviver`, `HashMapReviver`, `ReadonlySetReviver`, `HashSetReviver`, `ChunkReviver`, `RegExpReviver`, `URLReviver`, `DateReviver`, `DurationReviver`, `BigDecimalReviver`, `FileReviver`, `FormDataReviver`, `URLSearchParamsReviver`, `Uint8ArrayReviver`, `DateTimeUtcReviver`, `TimeZoneOffsetReviver`, `TimeZoneNamedReviver`, `TimeZoneReviver`, `DateTimeZonedReviver`, `JsonReviver`, and `MutableJsonReviver` + - check revivers: `isTrimmedReviver`, `isPatternReviver`, `isStringFiniteReviver`, `isStringBigIntReviver`, `isStringSymbolReviver`, `isUUIDReviver`, `isGUIDReviver`, `isULIDReviver`, `isBase64Reviver`, `isBase64UrlReviver`, `isStartsWithReviver`, `isEndsWithReviver`, `isIncludesReviver`, `isUppercasedReviver`, `isLowercasedReviver`, `isCapitalizedReviver`, `isUncapitalizedReviver`, `isFiniteReviver`, `isGreaterThanReviver`, `isGreaterThanOrEqualToReviver`, `isLessThanReviver`, `isLessThanOrEqualToReviver`, `isBetweenReviver`, `isMultipleOfReviver`, `isIntReviver`, `isDateValidReviver`, `isGreaterThanDateReviver`, `isGreaterThanOrEqualToDateReviver`, `isLessThanDateReviver`, `isLessThanOrEqualToDateReviver`, `isBetweenDateReviver`, `isGreaterThanBigIntReviver`, `isGreaterThanOrEqualToBigIntReviver`, `isLessThanBigIntReviver`, `isLessThanOrEqualToBigIntReviver`, `isBetweenBigIntReviver`, `isMinLengthReviver`, `isMaxLengthReviver`, `isLengthBetweenReviver`, `isMinSizeReviver`, `isMaxSizeReviver`, `isSizeBetweenReviver`, `isMinPropertiesReviver`, `isMaxPropertiesReviver`, `isPropertiesLengthBetweenReviver`, `isPropertyNamesReviver`, and `isUniqueReviver` +- Validate reviver payloads with their `payloadSchema`, and report missing or duplicate reviver identifiers. + +### JSON Schema and code generation + +- Compile JSON Schema from the canonical JSON codec and the encoded-side representation. Custom checks can contribute constraints through `Annotations.Filter.toJsonSchema` without modifying a central metadata registry. +- Import JSON Schema directly as live schemas. The importer now supports shared definitions, aliases, recursion, reference siblings, and definitions that are not reachable from a root. +- Add the named `FromJsonSchemaOptions` type for the importer `onEnter` callback. +- Generate code from live `toCode` annotations on declarations and checks. Compiler callbacks receive generated type parameters or schema dependencies and can emit multiple import declarations. +- Add import artifacts to `CodeDocument` and preserve all explicit definitions during multi-document code generation. +- Reject distinct schemas that declare the same identifier instead of silently merging them or generating suffixed references. + +### Canonical codecs and integrations + +- Preserve schema identifiers, property context, key encodings, and applicable checks while deriving canonical JSON codecs. +- Treat `Schema.Json` and `Schema.MutableJson` as already canonical. JSON validation now rejects sparse arrays, and non-finite numbers decode only from the canonical strings `"Infinity"`, `"-Infinity"`, and `"NaN"` rather than raw non-finite numeric inputs. +- Declarations without `toCodecJson` or `toCodec` now use JSON validation as their fallback instead of silently encoding to `null`. `toCodecJson` callbacks may return `undefined` when a declaration is already canonical. +- Add `Annotations.Declaration.toCodecStringTree`; StringTree derivation now requires a declaration to provide a structural StringTree, JSON, or general codec instead of silently encoding an opaque declaration to `undefined`. +- Update AI structured-output, HTTP schema, HttpApi OpenAPI, and OpenAPI generator integrations to consume the same canonical encoded representation and compiler hooks. Provider-specific structured-output transforms may remove unsupported JSON Schema keywords, while the Effect codec remains the validation authority. + +### Breaking changes + +- Rename the low-level representation constructors: + - `SchemaRepresentation.fromAST` -> `SchemaRepresentation.toRepresentation` + - `SchemaRepresentation.fromASTs` -> `SchemaRepresentation.toRepresentations` +- Replace `SchemaRepresentation.toSchema` with `fromRepresentation`, and add `fromRepresentations` for multi-root documents. Both reconstruction functions require `{ revivers: [...] }`; no default reviver is installed implicitly. +- Remove `SchemaRepresentation.toSchemaDefaultReviver`. Pass the required built-in revivers exported by `Schema`, or custom revivers created with the new constructors. +- Replace `DocumentFromJson` and `MultiDocumentFromJson` with the `toJson` / `fromJson` and `toJsonMultiDocument` / `fromJsonMultiDocument` functions. +- The persisted `Document` and `MultiDocument` format is incompatible with the previous format. Nodes now contain `checks`; encoded literal values, enum values, and property signature names use tagged `{ type, value }` objects while decoded documents expose their native scalar values; declarations no longer contain `encodedSchema`; persisted opaque declarations and leaf filters require a `{ id, payload }` representation identity; and checks no longer contain closed `meta` payloads. Regenerate stored documents from their source schemas with the new API, or migrate their shape before passing them to `fromJson`. +- Replace the generic `Reviver` function type with `DeclarationReviver

`, `FilterReviver

`, `FilterGroupReviver

`, `CheckReviver

`, `Reviver

`, and `AnyReviver`. +- Remove the closed metadata types `StringMeta`, `NumberMeta`, `BigIntMeta`, `ArraysMeta`, `ObjectsMeta`, `DateMeta`, `SizeMeta`, `DeclarationMeta`, and `Meta` from `SchemaRepresentation`. +- Remove the exported representation validation schemas and `PrimitiveTree`: `$PrimitiveTree`, `$Annotations`, `$Null`, `$Undefined`, `$Void`, `$Never`, `$Unknown`, `$Any`, `$StringMeta`, `$String`, `$NumberMeta`, `$Number`, `$Boolean`, `$BigInt`, `$Symbol`, `$LiteralValue`, `$Literal`, `$UniqueSymbol`, `$ObjectKeyword`, `$Enum`, `$TemplateLiteral`, `$Element`, `$Arrays`, `$PropertySignature`, `$IndexSignature`, `$ObjectsMeta`, `$Objects`, `$Union`, `$Reference`, `$DateMeta`, `$SizeMeta`, `$DeclarationMeta`, `$Declaration`, `$Suspend`, `$Representation`, `$Document`, and `$MultiDocument`. +- Replace schema annotations as follows: + - remove `Annotations.Bottom.meta` and `Annotations.Filter.meta` + - remove `Annotations.Declaration.typeConstructor`; use `representation` + - remove `Annotations.Declaration.generation`; use the `toCode` callback + - add `Annotations.Filter.representation`, `toJsonSchema`, and `toCode` + - add `Annotations.Augment.contentSchema` as a JSON-valued annotation + - allow `Annotations.Declaration.toCodecJson` and `toCodecStringTree` to return `undefined` +- Remove the top-level `contentMediaType` and `contentSchema` fields from `SchemaRepresentation.String`. Content metadata is now carried in ordinary annotations, and `contentSchema` is a JSON Schema value rather than a nested Effect representation. +- Remove `Schema.Annotations.BuiltInMetaDefinitions`, `BuiltInMeta`, `MetaDefinitions`, and `Meta`. Custom checks should carry a representation identity and compiler callbacks instead of augmenting the metadata registry. +- `fromJsonSchemaDocument` now returns `Schema.Top` instead of a representation `Document`. `fromJsonSchemaMultiDocument` now returns `SchemaMultiDocument` instead of `MultiDocument`; call `fromSchemaMultiDocument` when a representation multi-document is required. +- `toCodeDocument` now accepts only a live `MultiDocument`; remove its `reviver` option. Reconstruct persisted documents first so revivers can restore runtime compiler callbacks. +- Rename the `generation` field of `Artifact` values for symbols and enums to `code`. Declaration generation no longer has an `Encoded` output, and `importDeclaration` is replaced by `importDeclarations` on callback output. +- Remove the exported `sanitizeJavaScriptIdentifier`, `topologicalSort`, and `TopologicalSort` helpers. +- Negative zero no longer receives special representation handling. Do not rely on preserving its sign across JSON persistence or generated code, where it may be normalized to `0`. +- With `{ errors: "all" }`, structural checks run only after their base array, object, or declaration parses successfully; they are no longer added to an already failing child parse. diff --git a/.context/effect/.changeset/schema-result-combinators.md b/.context/effect/.changeset/pre/schema-result-combinators.md similarity index 100% rename from .context/effect/.changeset/schema-result-combinators.md rename to .context/effect/.changeset/pre/schema-result-combinators.md diff --git a/.context/effect/.changeset/pre/schema-runtime-performance.md b/.context/effect/.changeset/pre/schema-runtime-performance.md new file mode 100644 index 000000000..5d1b32274 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-runtime-performance.md @@ -0,0 +1,47 @@ +--- +"effect": patch +--- + +Improve Schema parsing, schema construction and adapter runtime performance +while preserving current parsing behavior. + +## Runtime performance + +The `effect@beta`, Valibot and Zod timing cases from +[`open-circle/schema-benchmarks`](https://github.com/open-circle/schema-benchmarks) +were reproduced as a dedicated `runtimeperf` suite. The table includes every +case exposed by each upstream adapter; `—` means that the adapter does not +provide that benchmark. + +Effect `main` (`45e781088`) and the branch based on `d775bf4b2` were compared +with five paired processes per case, 150 ms measurement time and 50 ms warmup. +The two initially inconclusive Effect cases were repeated with 15 paired +processes, 500 ms measurement time and 150 ms warmup. Valibot and Zod values +use five processes, 300 ms measurement time and 100 ms warmup. Environment: +Node `v24.12.0`, macOS arm64, Apple M3. + +Zod parsing uses `safeParse` with `{ jitless: true }`; its Standard Schema and +codec cases use the corresponding native adapter APIs. All values are median +microseconds per operation (`µs/op`), lower is better. Cross-library values are +diagnostic because they are independent rather than paired measurements. + +| Scenario | Effect `main` | Effect branch | Valibot | Zod 4 | Delta | 95% CI | Classification | +| ------------------------------------ | ------------: | ------------: | ---------: | ---------: | ------: | ------------------ | -------------- | +| Initialize schema | 137.28 | 118.23 | **40.24** | 318.56 | -12.69% | -21.02% to -5.35% | improvement | +| Initialize schema and decoder | 144.81 | **130.50** | — | — | -10.88% | -14.22% to -3.29% | improvement | +| Validate valid product | 8.478 | **5.415** | 5.63 | — | -35.18% | -41.65% to -32.83% | improvement | +| Validate invalid product | 1.516 | 1.348 | **0.2431** | — | -11.59% | -13.81% to -6.31% | improvement | +| Parse valid product, all errors | 8.360 | 5.366 | **5.22** | 7.16 | -36.28% | -54.41% to -31.67% | improvement | +| Parse invalid product, all errors | 11.302 | **9.100** | 15.70 | 41.58 | -19.42% | -21.32% to -13.12% | improvement | +| Parse valid product, first error | 8.201 | **5.294** | 5.37 | — | -35.44% | -37.75% to -34.59% | improvement | +| Parse invalid product, first error | 1.510 | 1.352 | **0.2572** | — | -10.51% | -12.52% to -9.53% | improvement | +| Standard Schema valid, all errors | 9.284 | 5.935 | 5.35 | **3.83** | -35.96% | -53.29% to -33.49% | improvement | +| Standard Schema invalid, all errors | 16.718 | **15.203** | 16.51 | 32.85 | -11.31% | -13.97% to -7.65% | improvement | +| Standard Schema valid, first error | 8.889 | **5.843** | — | — | -34.17% | -35.13% to -33.94% | improvement | +| Standard Schema invalid, first error | 2.435 | **2.244** | — | — | -8.44% | -12.76% to -4.82% | improvement | +| Typed codec encode | 0.4692 | 0.3420 | — | **0.0405** | -27.60% | -32.35% to -22.50% | improvement | +| Typed codec decode | 0.5191 | 0.3762 | — | **0.0463** | -27.19% | -34.75% to -22.71% | improvement | +| Unknown codec encode | 0.4910 | **0.3472** | — | — | -28.58% | -30.42% to -27.59% | improvement | +| Unknown codec decode | 0.5061 | **0.3637** | — | — | -29.26% | -29.82% to -21.70% | improvement | + +Overall Effect classification: 16 improvements and no regressions. diff --git a/.context/effect/.changeset/schema-struct-simplify.md b/.context/effect/.changeset/pre/schema-struct-simplify.md similarity index 100% rename from .context/effect/.changeset/schema-struct-simplify.md rename to .context/effect/.changeset/pre/schema-struct-simplify.md diff --git a/.context/effect/.changeset/pre/schema-union-type-derivation.md b/.context/effect/.changeset/pre/schema-union-type-derivation.md new file mode 100644 index 000000000..a7ac4dac1 --- /dev/null +++ b/.context/effect/.changeset/pre/schema-union-type-derivation.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Precompile union formatters and equivalences, select transformed union members using their decoded type, and allow deriving an equivalence for `Never`. diff --git a/.context/effect/.changeset/pre/scope-persisted-queue-ids.md b/.context/effect/.changeset/pre/scope-persisted-queue-ids.md new file mode 100644 index 000000000..ea6d42d1b --- /dev/null +++ b/.context/effect/.changeset/pre/scope-persisted-queue-ids.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Scope custom persisted queue ID deduplication to each named queue. diff --git a/.context/effect/.changeset/pre/secure-eventlog-identities.md b/.context/effect/.changeset/pre/secure-eventlog-identities.md new file mode 100644 index 000000000..b7d1d9ad3 --- /dev/null +++ b/.context/effect/.changeset/pre/secure-eventlog-identities.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Bind event-log read and write requests to the identities authenticated on their RPC connection. diff --git a/.context/effect/.changeset/semantic-matching.md b/.context/effect/.changeset/pre/semantic-matching.md similarity index 100% rename from .context/effect/.changeset/semantic-matching.md rename to .context/effect/.changeset/pre/semantic-matching.md diff --git a/.context/effect/.changeset/seven-mugs-marry.md b/.context/effect/.changeset/pre/seven-mugs-marry.md similarity index 100% rename from .context/effect/.changeset/seven-mugs-marry.md rename to .context/effect/.changeset/pre/seven-mugs-marry.md diff --git a/.context/effect/.changeset/pre/seven-poems-divide.md b/.context/effect/.changeset/pre/seven-poems-divide.md new file mode 100644 index 000000000..8d9caf7f4 --- /dev/null +++ b/.context/effect/.changeset/pre/seven-poems-divide.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor populated variables before dotenv expansion defaults in `ConfigProvider`. diff --git a/.context/effect/.changeset/shaggy-birds-stay.md b/.context/effect/.changeset/pre/shaggy-birds-stay.md similarity index 100% rename from .context/effect/.changeset/shaggy-birds-stay.md rename to .context/effect/.changeset/pre/shaggy-birds-stay.md diff --git a/.context/effect/.changeset/shaggy-cities-push.md b/.context/effect/.changeset/pre/shaggy-cities-push.md similarity index 100% rename from .context/effect/.changeset/shaggy-cities-push.md rename to .context/effect/.changeset/pre/shaggy-cities-push.md diff --git a/.context/effect/.changeset/shaggy-numbers-accept.md b/.context/effect/.changeset/pre/shaggy-numbers-accept.md similarity index 100% rename from .context/effect/.changeset/shaggy-numbers-accept.md rename to .context/effect/.changeset/pre/shaggy-numbers-accept.md diff --git a/.context/effect/.changeset/shaky-beans-throw.md b/.context/effect/.changeset/pre/shaky-beans-throw.md similarity index 100% rename from .context/effect/.changeset/shaky-beans-throw.md rename to .context/effect/.changeset/pre/shaky-beans-throw.md diff --git a/.context/effect/.changeset/sharp-emus-applaud.md b/.context/effect/.changeset/pre/sharp-emus-applaud.md similarity index 100% rename from .context/effect/.changeset/sharp-emus-applaud.md rename to .context/effect/.changeset/pre/sharp-emus-applaud.md diff --git a/.context/effect/.changeset/sharp-goats-wink.md b/.context/effect/.changeset/pre/sharp-goats-wink.md similarity index 100% rename from .context/effect/.changeset/sharp-goats-wink.md rename to .context/effect/.changeset/pre/sharp-goats-wink.md diff --git a/.context/effect/.changeset/sharp-pandas-care.md b/.context/effect/.changeset/pre/sharp-pandas-care.md similarity index 100% rename from .context/effect/.changeset/sharp-pandas-care.md rename to .context/effect/.changeset/pre/sharp-pandas-care.md diff --git a/.context/effect/.changeset/sharp-peas-march.md b/.context/effect/.changeset/pre/sharp-peas-march.md similarity index 100% rename from .context/effect/.changeset/sharp-peas-march.md rename to .context/effect/.changeset/pre/sharp-peas-march.md diff --git a/.context/effect/.changeset/sharp-rules-draw.md b/.context/effect/.changeset/pre/sharp-rules-draw.md similarity index 100% rename from .context/effect/.changeset/sharp-rules-draw.md rename to .context/effect/.changeset/pre/sharp-rules-draw.md diff --git a/.context/effect/.changeset/sharp-singers-sort.md b/.context/effect/.changeset/pre/sharp-singers-sort.md similarity index 100% rename from .context/effect/.changeset/sharp-singers-sort.md rename to .context/effect/.changeset/pre/sharp-singers-sort.md diff --git a/.context/effect/.changeset/shiny-trains-hug.md b/.context/effect/.changeset/pre/shiny-trains-hug.md similarity index 100% rename from .context/effect/.changeset/shiny-trains-hug.md rename to .context/effect/.changeset/pre/shiny-trains-hug.md diff --git a/.context/effect/.changeset/short-cows-relate.md b/.context/effect/.changeset/pre/short-cows-relate.md similarity index 100% rename from .context/effect/.changeset/short-cows-relate.md rename to .context/effect/.changeset/pre/short-cows-relate.md diff --git a/.context/effect/.changeset/short-foxes-admire.md b/.context/effect/.changeset/pre/short-foxes-admire.md similarity index 100% rename from .context/effect/.changeset/short-foxes-admire.md rename to .context/effect/.changeset/pre/short-foxes-admire.md diff --git a/.context/effect/.changeset/short-stamps-throw.md b/.context/effect/.changeset/pre/short-stamps-throw.md similarity index 100% rename from .context/effect/.changeset/short-stamps-throw.md rename to .context/effect/.changeset/pre/short-stamps-throw.md diff --git a/.context/effect/.changeset/shy-cycles-flow.md b/.context/effect/.changeset/pre/shy-cycles-flow.md similarity index 100% rename from .context/effect/.changeset/shy-cycles-flow.md rename to .context/effect/.changeset/pre/shy-cycles-flow.md diff --git a/.context/effect/.changeset/shy-geckos-sniff.md b/.context/effect/.changeset/pre/shy-geckos-sniff.md similarity index 100% rename from .context/effect/.changeset/shy-geckos-sniff.md rename to .context/effect/.changeset/pre/shy-geckos-sniff.md diff --git a/.context/effect/.changeset/silent-geckos-matter.md b/.context/effect/.changeset/pre/silent-geckos-matter.md similarity index 100% rename from .context/effect/.changeset/silent-geckos-matter.md rename to .context/effect/.changeset/pre/silent-geckos-matter.md diff --git a/.context/effect/.changeset/silent-needles-design.md b/.context/effect/.changeset/pre/silent-needles-design.md similarity index 100% rename from .context/effect/.changeset/silent-needles-design.md rename to .context/effect/.changeset/pre/silent-needles-design.md diff --git a/.context/effect/.changeset/silent-plants-matter.md b/.context/effect/.changeset/pre/silent-plants-matter.md similarity index 100% rename from .context/effect/.changeset/silent-plants-matter.md rename to .context/effect/.changeset/pre/silent-plants-matter.md diff --git a/.context/effect/.changeset/silent-spoons-stare.md b/.context/effect/.changeset/pre/silent-spoons-stare.md similarity index 100% rename from .context/effect/.changeset/silent-spoons-stare.md rename to .context/effect/.changeset/pre/silent-spoons-stare.md diff --git a/.context/effect/.changeset/pre/silly-dodos-update.md b/.context/effect/.changeset/pre/silly-dodos-update.md new file mode 100644 index 000000000..e90a8a07a --- /dev/null +++ b/.context/effect/.changeset/pre/silly-dodos-update.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `SynchronizedRef.getAndUpdateSome` to update its backing ref. diff --git a/.context/effect/.changeset/silly-loops-tickle.md b/.context/effect/.changeset/pre/silly-loops-tickle.md similarity index 100% rename from .context/effect/.changeset/silly-loops-tickle.md rename to .context/effect/.changeset/pre/silly-loops-tickle.md diff --git a/.context/effect/.changeset/silver-bulk-indexeddb.md b/.context/effect/.changeset/pre/silver-bulk-indexeddb.md similarity index 100% rename from .context/effect/.changeset/silver-bulk-indexeddb.md rename to .context/effect/.changeset/pre/silver-bulk-indexeddb.md diff --git a/.context/effect/.changeset/silver-emus-smoke.md b/.context/effect/.changeset/pre/silver-emus-smoke.md similarity index 100% rename from .context/effect/.changeset/silver-emus-smoke.md rename to .context/effect/.changeset/pre/silver-emus-smoke.md diff --git a/.context/effect/.changeset/silver-kings-poke.md b/.context/effect/.changeset/pre/silver-kings-poke.md similarity index 100% rename from .context/effect/.changeset/silver-kings-poke.md rename to .context/effect/.changeset/pre/silver-kings-poke.md diff --git a/.context/effect/.changeset/silver-snails-sqlite.md b/.context/effect/.changeset/pre/silver-snails-sqlite.md similarity index 100% rename from .context/effect/.changeset/silver-snails-sqlite.md rename to .context/effect/.changeset/pre/silver-snails-sqlite.md diff --git a/.context/effect/.changeset/silver-wings-watch.md b/.context/effect/.changeset/pre/silver-wings-watch.md similarity index 100% rename from .context/effect/.changeset/silver-wings-watch.md rename to .context/effect/.changeset/pre/silver-wings-watch.md diff --git a/.context/effect/.changeset/pre/simplify-optic-composition.md b/.context/effect/.changeset/pre/simplify-optic-composition.md new file mode 100644 index 000000000..1ed7423a9 --- /dev/null +++ b/.context/effect/.changeset/pre/simplify-optic-composition.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +Fix three issues in the public `Optic` API: + +- Composed `Iso` and `Prism` setters no longer try to read a source value before writing. +- Calling `notUndefined` on an `Optional` now returns an `Optional`, because writing can still fail. +- The internal `node` property is no longer exposed by public optic types. diff --git a/.context/effect/.changeset/six-cups-taste.md b/.context/effect/.changeset/pre/six-cups-taste.md similarity index 100% rename from .context/effect/.changeset/six-cups-taste.md rename to .context/effect/.changeset/pre/six-cups-taste.md diff --git a/.context/effect/.changeset/pre/six-pumas-take.md b/.context/effect/.changeset/pre/six-pumas-take.md new file mode 100644 index 000000000..a58d9a049 --- /dev/null +++ b/.context/effect/.changeset/pre/six-pumas-take.md @@ -0,0 +1,9 @@ +--- +"effect": patch +--- + +add advanced graph set operations for deriving related graph structures + +- `Graph.complement` - complement over the existing node set, adding missing edges between distinct nodes +- `Graph.neighborhood` - induced subgraph containing nodes within a radius of a node +- `Graph.sum` - disjoint union of two graphs without merging equal node data diff --git a/.context/effect/.changeset/sixty-mails-shout.md b/.context/effect/.changeset/pre/sixty-mails-shout.md similarity index 100% rename from .context/effect/.changeset/sixty-mails-shout.md rename to .context/effect/.changeset/pre/sixty-mails-shout.md diff --git a/.context/effect/.changeset/sixty-socks-yell.md b/.context/effect/.changeset/pre/sixty-socks-yell.md similarity index 100% rename from .context/effect/.changeset/sixty-socks-yell.md rename to .context/effect/.changeset/pre/sixty-socks-yell.md diff --git a/.context/effect/.changeset/slick-signs-wish.md b/.context/effect/.changeset/pre/slick-signs-wish.md similarity index 100% rename from .context/effect/.changeset/slick-signs-wish.md rename to .context/effect/.changeset/pre/slick-signs-wish.md diff --git a/.context/effect/.changeset/slick-toes-rush.md b/.context/effect/.changeset/pre/slick-toes-rush.md similarity index 100% rename from .context/effect/.changeset/slick-toes-rush.md rename to .context/effect/.changeset/pre/slick-toes-rush.md diff --git a/.context/effect/.changeset/pre/slimy-melons-admire.md b/.context/effect/.changeset/pre/slimy-melons-admire.md new file mode 100644 index 000000000..7cc69a0af --- /dev/null +++ b/.context/effect/.changeset/pre/slimy-melons-admire.md @@ -0,0 +1,33 @@ +--- +"@effect/sql-sqlite-react-native": patch +"@effect/openapi-generator": patch +"@effect/platform-node-shared": patch +"@effect/ai-openai-compat": patch +"@effect/platform-browser": patch +"@effect/sql-sqlite-node": patch +"@effect/sql-sqlite-wasm": patch +"@effect/sql-clickhouse": patch +"@effect/sql-sqlite-bun": patch +"@effect/ai-openrouter": patch +"@effect/opentelemetry": patch +"@effect/platform-deno": patch +"@effect/platform-node": patch +"@effect/sql-sqlite-do": patch +"@effect/ai-anthropic": patch +"@effect/platform-bun": patch +"@effect/docgen": patch +"@effect/atom-react": patch +"@effect/atom-solid": patch +"@effect/sql-libsql": patch +"@effect/sql-mysql2": patch +"@effect/sql-pglite": patch +"@effect/ai-openai": patch +"@effect/sql-mssql": patch +"@effect/atom-vue": patch +"effect": patch +"@effect/sql-d1": patch +"@effect/sql-pg": patch +"@effect/vitest": patch +--- + +Removed explicit ./index entrypoints diff --git a/.context/effect/.changeset/slimy-planets-divide.md b/.context/effect/.changeset/pre/slimy-planets-divide.md similarity index 100% rename from .context/effect/.changeset/slimy-planets-divide.md rename to .context/effect/.changeset/pre/slimy-planets-divide.md diff --git a/.context/effect/.changeset/slimy-turtles-juggle.md b/.context/effect/.changeset/pre/slimy-turtles-juggle.md similarity index 100% rename from .context/effect/.changeset/slimy-turtles-juggle.md rename to .context/effect/.changeset/pre/slimy-turtles-juggle.md diff --git a/.context/effect/.changeset/slow-beans-battle.md b/.context/effect/.changeset/pre/slow-beans-battle.md similarity index 100% rename from .context/effect/.changeset/slow-beans-battle.md rename to .context/effect/.changeset/pre/slow-beans-battle.md diff --git a/.context/effect/.changeset/slow-berries-enjoy.md b/.context/effect/.changeset/pre/slow-berries-enjoy.md similarity index 100% rename from .context/effect/.changeset/slow-berries-enjoy.md rename to .context/effect/.changeset/pre/slow-berries-enjoy.md diff --git a/.context/effect/.changeset/pre/slow-entities-register.md b/.context/effect/.changeset/pre/slow-entities-register.md new file mode 100644 index 000000000..678da84a1 --- /dev/null +++ b/.context/effect/.changeset/pre/slow-entities-register.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Hold persisted cluster messages while entity layers are still registering, while retaining a bounded failure when +registration never begins. diff --git a/.context/effect/.changeset/pre/slow-spiders-refresh.md b/.context/effect/.changeset/pre/slow-spiders-refresh.md new file mode 100644 index 000000000..109878758 --- /dev/null +++ b/.context/effect/.changeset/pre/slow-spiders-refresh.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent SQL runner lock refreshes from hanging when reserved connections become unresponsive. diff --git a/.context/effect/.changeset/small-bugs-hunt.md b/.context/effect/.changeset/pre/small-bugs-hunt.md similarity index 100% rename from .context/effect/.changeset/small-bugs-hunt.md rename to .context/effect/.changeset/pre/small-bugs-hunt.md diff --git a/.context/effect/.changeset/small-crabs-care.md b/.context/effect/.changeset/pre/small-crabs-care.md similarity index 100% rename from .context/effect/.changeset/small-crabs-care.md rename to .context/effect/.changeset/pre/small-crabs-care.md diff --git a/.context/effect/.changeset/pre/small-pandas-cache.md b/.context/effect/.changeset/pre/small-pandas-cache.md new file mode 100644 index 000000000..b1e5b3de5 --- /dev/null +++ b/.context/effect/.changeset/pre/small-pandas-cache.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Preserve OpenAI Responses API cache write token usage in language model responses. diff --git a/.context/effect/.changeset/small-pets-sit.md b/.context/effect/.changeset/pre/small-pets-sit.md similarity index 100% rename from .context/effect/.changeset/small-pets-sit.md rename to .context/effect/.changeset/pre/small-pets-sit.md diff --git a/.context/effect/.changeset/smart-ducks-jump.md b/.context/effect/.changeset/pre/smart-ducks-jump.md similarity index 100% rename from .context/effect/.changeset/smart-ducks-jump.md rename to .context/effect/.changeset/pre/smart-ducks-jump.md diff --git a/.context/effect/.changeset/smart-pillows-buy.md b/.context/effect/.changeset/pre/smart-pillows-buy.md similarity index 100% rename from .context/effect/.changeset/smart-pillows-buy.md rename to .context/effect/.changeset/pre/smart-pillows-buy.md diff --git a/.context/effect/.changeset/smart-timers-fly.md b/.context/effect/.changeset/pre/smart-timers-fly.md similarity index 100% rename from .context/effect/.changeset/smart-timers-fly.md rename to .context/effect/.changeset/pre/smart-timers-fly.md diff --git a/.context/effect/.changeset/smart-tips-sort.md b/.context/effect/.changeset/pre/smart-tips-sort.md similarity index 100% rename from .context/effect/.changeset/smart-tips-sort.md rename to .context/effect/.changeset/pre/smart-tips-sort.md diff --git a/.context/effect/.changeset/pre/social-hoops-knock.md b/.context/effect/.changeset/pre/social-hoops-knock.md new file mode 100644 index 000000000..c91f7ae2e --- /dev/null +++ b/.context/effect/.changeset/pre/social-hoops-knock.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Cleanup internals of CLI package diff --git a/.context/effect/.changeset/social-pumas-prove.md b/.context/effect/.changeset/pre/social-pumas-prove.md similarity index 100% rename from .context/effect/.changeset/social-pumas-prove.md rename to .context/effect/.changeset/pre/social-pumas-prove.md diff --git a/.context/effect/.changeset/soft-comics-wink.md b/.context/effect/.changeset/pre/soft-comics-wink.md similarity index 100% rename from .context/effect/.changeset/soft-comics-wink.md rename to .context/effect/.changeset/pre/soft-comics-wink.md diff --git a/.context/effect/.changeset/soft-delete-sqlmodel.md b/.context/effect/.changeset/pre/soft-delete-sqlmodel.md similarity index 100% rename from .context/effect/.changeset/soft-delete-sqlmodel.md rename to .context/effect/.changeset/pre/soft-delete-sqlmodel.md diff --git a/.context/effect/.changeset/soft-seals-allow.md b/.context/effect/.changeset/pre/soft-seals-allow.md similarity index 100% rename from .context/effect/.changeset/soft-seals-allow.md rename to .context/effect/.changeset/pre/soft-seals-allow.md diff --git a/.context/effect/.changeset/pre/soft-sockets-write.md b/.context/effect/.changeset/pre/soft-sockets-write.md new file mode 100644 index 000000000..c0106d10b --- /dev/null +++ b/.context/effect/.changeset/pre/soft-sockets-write.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Map WebSocket send exceptions and transform stream write rejections to typed `SocketError` failures. diff --git a/.context/effect/.changeset/solid-cougars-attack.md b/.context/effect/.changeset/pre/solid-cougars-attack.md similarity index 100% rename from .context/effect/.changeset/solid-cougars-attack.md rename to .context/effect/.changeset/pre/solid-cougars-attack.md diff --git a/.context/effect/.changeset/solid-doors-ring.md b/.context/effect/.changeset/pre/solid-doors-ring.md similarity index 100% rename from .context/effect/.changeset/solid-doors-ring.md rename to .context/effect/.changeset/pre/solid-doors-ring.md diff --git a/.context/effect/.changeset/solid-items-tease.md b/.context/effect/.changeset/pre/solid-items-tease.md similarity index 100% rename from .context/effect/.changeset/solid-items-tease.md rename to .context/effect/.changeset/pre/solid-items-tease.md diff --git a/.context/effect/.changeset/solid-towns-smoke.md b/.context/effect/.changeset/pre/solid-towns-smoke.md similarity index 100% rename from .context/effect/.changeset/solid-towns-smoke.md rename to .context/effect/.changeset/pre/solid-towns-smoke.md diff --git a/.context/effect/.changeset/pre/sour-bees-sleep.md b/.context/effect/.changeset/pre/sour-bees-sleep.md new file mode 100644 index 000000000..6c31527a9 --- /dev/null +++ b/.context/effect/.changeset/pre/sour-bees-sleep.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Default empty Server-Sent Event types to `message`. diff --git a/.context/effect/.changeset/sour-canyons-rescue.md b/.context/effect/.changeset/pre/sour-canyons-rescue.md similarity index 100% rename from .context/effect/.changeset/sour-canyons-rescue.md rename to .context/effect/.changeset/pre/sour-canyons-rescue.md diff --git a/.context/effect/.changeset/sparkly-bears-act.md b/.context/effect/.changeset/pre/sparkly-bears-act.md similarity index 100% rename from .context/effect/.changeset/sparkly-bears-act.md rename to .context/effect/.changeset/pre/sparkly-bears-act.md diff --git a/.context/effect/.changeset/sparkly-coins-sit.md b/.context/effect/.changeset/pre/sparkly-coins-sit.md similarity index 100% rename from .context/effect/.changeset/sparkly-coins-sit.md rename to .context/effect/.changeset/pre/sparkly-coins-sit.md diff --git a/.context/effect/.changeset/pre/spicy-doors-unlist.md b/.context/effect/.changeset/pre/spicy-doors-unlist.md new file mode 100644 index 000000000..fc4907aa6 --- /dev/null +++ b/.context/effect/.changeset/pre/spicy-doors-unlist.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Rename `Command.withHidden` to `Command.unlisted`, along with the `hidden` command property which is now `unlisted`. diff --git a/.context/effect/.changeset/pre/split-clock-semantics.md b/.context/effect/.changeset/pre/split-clock-semantics.md new file mode 100644 index 000000000..1d2390cbb --- /dev/null +++ b/.context/effect/.changeset/pre/split-clock-semantics.md @@ -0,0 +1,7 @@ +--- +"effect": minor +--- + +Separate wall-clock timestamps from monotonic elapsed time. + +`Clock.Clock` now requires `monotonicTimeNanosUnsafe()` and `monotonicTimeNanos` for measuring elapsed time. Custom `Clock` implementations must provide both members. The live clock's `currentTimeNanos` now re-anchors its high-resolution Unix wall-clock timestamp when it drifts from `Date.now()`, while `Effect.timed`, duration metric tracking, and `Sink.withDuration` use monotonic time so wall-clock corrections do not distort elapsed durations. diff --git a/.context/effect/.changeset/spotty-comics-fry.md b/.context/effect/.changeset/pre/spotty-comics-fry.md similarity index 100% rename from .context/effect/.changeset/spotty-comics-fry.md rename to .context/effect/.changeset/pre/spotty-comics-fry.md diff --git a/.context/effect/.changeset/sql-migrator-mjs-mts.md b/.context/effect/.changeset/pre/sql-migrator-mjs-mts.md similarity index 100% rename from .context/effect/.changeset/sql-migrator-mjs-mts.md rename to .context/effect/.changeset/pre/sql-migrator-mjs-mts.md diff --git a/.context/effect/.changeset/sqlite-bun-prepare-error-channel.md b/.context/effect/.changeset/pre/sqlite-bun-prepare-error-channel.md similarity index 100% rename from .context/effect/.changeset/sqlite-bun-prepare-error-channel.md rename to .context/effect/.changeset/pre/sqlite-bun-prepare-error-channel.md diff --git a/.context/effect/.changeset/pre/sqlite-client-locking-defaults.md b/.context/effect/.changeset/pre/sqlite-client-locking-defaults.md new file mode 100644 index 000000000..ee01605b9 --- /dev/null +++ b/.context/effect/.changeset/pre/sqlite-client-locking-defaults.md @@ -0,0 +1,6 @@ +--- +"@effect/sql-sqlite-bun": patch +"@effect/sql-sqlite-node": patch +--- + +Use a configurable five-second busy timeout and immediate transactions by default to avoid SQLite lock failures under concurrent access. Busy waits can block the event loop, while immediate transactions serialize behind other writers. diff --git a/.context/effect/.changeset/sqlite-do-durable-object-transactions.md b/.context/effect/.changeset/pre/sqlite-do-durable-object-transactions.md similarity index 100% rename from .context/effect/.changeset/sqlite-do-durable-object-transactions.md rename to .context/effect/.changeset/pre/sqlite-do-durable-object-transactions.md diff --git a/.context/effect/.changeset/stale-dots-tell.md b/.context/effect/.changeset/pre/stale-dots-tell.md similarity index 100% rename from .context/effect/.changeset/stale-dots-tell.md rename to .context/effect/.changeset/pre/stale-dots-tell.md diff --git a/.context/effect/.changeset/pre/stale-graph-traversal-skips.md b/.context/effect/.changeset/pre/stale-graph-traversal-skips.md new file mode 100644 index 000000000..6bf269405 --- /dev/null +++ b/.context/effect/.changeset/pre/stale-graph-traversal-skips.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix Graph BFS, topological sort, and DFS postorder iterators to skip nodes removed from a MutableGraph without recursive self-calls. diff --git a/.context/effect/.changeset/pre/stale-laws-do.md b/.context/effect/.changeset/pre/stale-laws-do.md new file mode 100644 index 000000000..f114fbec1 --- /dev/null +++ b/.context/effect/.changeset/pre/stale-laws-do.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-bun": patch +--- + +optimize bun stream reading diff --git a/.context/effect/.changeset/pre/stale-snakes-know.md b/.context/effect/.changeset/pre/stale-snakes-know.md new file mode 100644 index 000000000..a6272595b --- /dev/null +++ b/.context/effect/.changeset/pre/stale-snakes-know.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +update dependencies diff --git a/.context/effect/.changeset/strict-areas-end.md b/.context/effect/.changeset/pre/strict-areas-end.md similarity index 100% rename from .context/effect/.changeset/strict-areas-end.md rename to .context/effect/.changeset/pre/strict-areas-end.md diff --git a/.context/effect/.changeset/strict-buckets-hug.md b/.context/effect/.changeset/pre/strict-buckets-hug.md similarity index 100% rename from .context/effect/.changeset/strict-buckets-hug.md rename to .context/effect/.changeset/pre/strict-buckets-hug.md diff --git a/.context/effect/.changeset/strip-resolved-approvals.md b/.context/effect/.changeset/pre/strip-resolved-approvals.md similarity index 100% rename from .context/effect/.changeset/strip-resolved-approvals.md rename to .context/effect/.changeset/pre/strip-resolved-approvals.md diff --git a/.context/effect/.changeset/strong-balloons-tickle.md b/.context/effect/.changeset/pre/strong-balloons-tickle.md similarity index 100% rename from .context/effect/.changeset/strong-balloons-tickle.md rename to .context/effect/.changeset/pre/strong-balloons-tickle.md diff --git a/.context/effect/.changeset/strong-bees-queue.md b/.context/effect/.changeset/pre/strong-bees-queue.md similarity index 100% rename from .context/effect/.changeset/strong-bees-queue.md rename to .context/effect/.changeset/pre/strong-bees-queue.md diff --git a/.context/effect/.changeset/pre/strong-insects-film.md b/.context/effect/.changeset/pre/strong-insects-film.md new file mode 100644 index 000000000..075c42f85 --- /dev/null +++ b/.context/effect/.changeset/pre/strong-insects-film.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +ensure WithTransaction wraps entire rpc handler diff --git a/.context/effect/.changeset/struct-record.md b/.context/effect/.changeset/pre/struct-record.md similarity index 100% rename from .context/effect/.changeset/struct-record.md rename to .context/effect/.changeset/pre/struct-record.md diff --git a/.context/effect/.changeset/sunny-ads-hang.md b/.context/effect/.changeset/pre/sunny-ads-hang.md similarity index 100% rename from .context/effect/.changeset/sunny-ads-hang.md rename to .context/effect/.changeset/pre/sunny-ads-hang.md diff --git a/.context/effect/.changeset/sunny-bikes-sleep.md b/.context/effect/.changeset/pre/sunny-bikes-sleep.md similarity index 100% rename from .context/effect/.changeset/sunny-bikes-sleep.md rename to .context/effect/.changeset/pre/sunny-bikes-sleep.md diff --git a/.context/effect/.changeset/sunny-rooms-invent.md b/.context/effect/.changeset/pre/sunny-rooms-invent.md similarity index 100% rename from .context/effect/.changeset/sunny-rooms-invent.md rename to .context/effect/.changeset/pre/sunny-rooms-invent.md diff --git a/.context/effect/.changeset/sweet-donuts-bet.md b/.context/effect/.changeset/pre/sweet-donuts-bet.md similarity index 100% rename from .context/effect/.changeset/sweet-donuts-bet.md rename to .context/effect/.changeset/pre/sweet-donuts-bet.md diff --git a/.context/effect/.changeset/sweet-hotels-give.md b/.context/effect/.changeset/pre/sweet-hotels-give.md similarity index 100% rename from .context/effect/.changeset/sweet-hotels-give.md rename to .context/effect/.changeset/pre/sweet-hotels-give.md diff --git a/.context/effect/.changeset/pre/sweet-lizards-sing.md b/.context/effect/.changeset/pre/sweet-lizards-sing.md new file mode 100644 index 000000000..12248afd9 --- /dev/null +++ b/.context/effect/.changeset/pre/sweet-lizards-sing.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node-shared": patch +--- + +Fix `NodeStream.toString` registering a duplicate `error` event listener. diff --git a/.context/effect/.changeset/pre/sweet-schedules-matter.md b/.context/effect/.changeset/pre/sweet-schedules-matter.md new file mode 100644 index 000000000..dcc8d4a93 --- /dev/null +++ b/.context/effect/.changeset/pre/sweet-schedules-matter.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Update `Schedule.addDelay` and `Schedule.modifyDelay` to receive full schedule metadata instead of separate output and delay arguments. diff --git a/.context/effect/.changeset/sweet-views-learn.md b/.context/effect/.changeset/pre/sweet-views-learn.md similarity index 100% rename from .context/effect/.changeset/sweet-views-learn.md rename to .context/effect/.changeset/pre/sweet-views-learn.md diff --git a/.context/effect/.changeset/pre/swift-geese-count.md b/.context/effect/.changeset/pre/swift-geese-count.md new file mode 100644 index 000000000..352cd448e --- /dev/null +++ b/.context/effect/.changeset/pre/swift-geese-count.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix the return type of `Channel.runCount` to expose its numeric result. diff --git a/.context/effect/.changeset/pre/swift-pandas-parse.md b/.context/effect/.changeset/pre/swift-pandas-parse.md new file mode 100644 index 000000000..6bbf259a5 --- /dev/null +++ b/.context/effect/.changeset/pre/swift-pandas-parse.md @@ -0,0 +1,7 @@ +--- +"effect": patch +"@effect/platform-browser": patch +"@effect/platform-node": patch +--- + +Vendor the multipart parser as `effect/unstable/http/MultipartParser`, add the Node.js adapter at `@effect/platform-node/NodeMultipartParser`, and remove the external `multipasta` dependency. diff --git a/.context/effect/.changeset/swift-spiders-unpack.md b/.context/effect/.changeset/pre/swift-spiders-unpack.md similarity index 100% rename from .context/effect/.changeset/swift-spiders-unpack.md rename to .context/effect/.changeset/pre/swift-spiders-unpack.md diff --git a/.context/effect/.changeset/swift-symbols-stand.md b/.context/effect/.changeset/pre/swift-symbols-stand.md similarity index 100% rename from .context/effect/.changeset/swift-symbols-stand.md rename to .context/effect/.changeset/pre/swift-symbols-stand.md diff --git a/.context/effect/.changeset/tagged-error-class-optional-empty-props.md b/.context/effect/.changeset/pre/tagged-error-class-optional-empty-props.md similarity index 100% rename from .context/effect/.changeset/tagged-error-class-optional-empty-props.md rename to .context/effect/.changeset/pre/tagged-error-class-optional-empty-props.md diff --git a/.context/effect/.changeset/tall-hairs-return.md b/.context/effect/.changeset/pre/tall-hairs-return.md similarity index 100% rename from .context/effect/.changeset/tall-hairs-return.md rename to .context/effect/.changeset/pre/tall-hairs-return.md diff --git a/.context/effect/.changeset/pre/tall-ideas-fix.md b/.context/effect/.changeset/pre/tall-ideas-fix.md new file mode 100644 index 000000000..0cf7925e2 --- /dev/null +++ b/.context/effect/.changeset/pre/tall-ideas-fix.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Don’t create a table when it’s not needed diff --git a/.context/effect/.changeset/tall-mails-listen.md b/.context/effect/.changeset/pre/tall-mails-listen.md similarity index 100% rename from .context/effect/.changeset/tall-mails-listen.md rename to .context/effect/.changeset/pre/tall-mails-listen.md diff --git a/.context/effect/.changeset/tall-queens-cheer.md b/.context/effect/.changeset/pre/tall-queens-cheer.md similarity index 100% rename from .context/effect/.changeset/tall-queens-cheer.md rename to .context/effect/.changeset/pre/tall-queens-cheer.md diff --git a/.context/effect/.changeset/tall-wombats-wave.md b/.context/effect/.changeset/pre/tall-wombats-wave.md similarity index 100% rename from .context/effect/.changeset/tall-wombats-wave.md rename to .context/effect/.changeset/pre/tall-wombats-wave.md diff --git a/.context/effect/.changeset/tangy-colts-lose.md b/.context/effect/.changeset/pre/tangy-colts-lose.md similarity index 100% rename from .context/effect/.changeset/tangy-colts-lose.md rename to .context/effect/.changeset/pre/tangy-colts-lose.md diff --git a/.context/effect/.changeset/pre/tangy-plants-run.md b/.context/effect/.changeset/pre/tangy-plants-run.md new file mode 100644 index 000000000..111cd174b --- /dev/null +++ b/.context/effect/.changeset/pre/tangy-plants-run.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +only interrupt cache lookup when all awaiters are gone diff --git a/.context/effect/.changeset/tasty-comics-send.md b/.context/effect/.changeset/pre/tasty-comics-send.md similarity index 100% rename from .context/effect/.changeset/tasty-comics-send.md rename to .context/effect/.changeset/pre/tasty-comics-send.md diff --git a/.context/effect/.changeset/pre/tasty-moments-post.md b/.context/effect/.changeset/pre/tasty-moments-post.md new file mode 100644 index 000000000..8a2d4e14d --- /dev/null +++ b/.context/effect/.changeset/pre/tasty-moments-post.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +add Effect.setContext for fully replacing the fiber context diff --git a/.context/effect/.changeset/ten-kings-fry.md b/.context/effect/.changeset/pre/ten-kings-fry.md similarity index 100% rename from .context/effect/.changeset/ten-kings-fry.md rename to .context/effect/.changeset/pre/ten-kings-fry.md diff --git a/.context/effect/.changeset/pre/tender-deserts-pull.md b/.context/effect/.changeset/pre/tender-deserts-pull.md new file mode 100644 index 000000000..c7fda450b --- /dev/null +++ b/.context/effect/.changeset/pre/tender-deserts-pull.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject truncated MessagePack frames at the end of a stream. diff --git a/.context/effect/.changeset/pre/tender-files-complete.md b/.context/effect/.changeset/pre/tender-files-complete.md new file mode 100644 index 000000000..9b9210b4a --- /dev/null +++ b/.context/effect/.changeset/pre/tender-files-complete.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve file and directory semantics in CLI completion descriptors. diff --git a/.context/effect/.changeset/pre/tender-points-sleep.md b/.context/effect/.changeset/pre/tender-points-sleep.md new file mode 100644 index 000000000..3206a87c2 --- /dev/null +++ b/.context/effect/.changeset/pre/tender-points-sleep.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Generate unique persisted paths for multipart files with duplicate filenames. diff --git a/.context/effect/.changeset/pre/terminate-openai-failed-streams.md b/.context/effect/.changeset/pre/terminate-openai-failed-streams.md new file mode 100644 index 000000000..66599bb5f --- /dev/null +++ b/.context/effect/.changeset/pre/terminate-openai-failed-streams.md @@ -0,0 +1,5 @@ +--- +"@effect/ai-openai": patch +--- + +Terminate OpenAI HTTP and WebSocket response streams when a `response.failed` event arrives. diff --git a/.context/effect/.changeset/thick-pandas-wait.md b/.context/effect/.changeset/pre/thick-pandas-wait.md similarity index 100% rename from .context/effect/.changeset/thick-pandas-wait.md rename to .context/effect/.changeset/pre/thick-pandas-wait.md diff --git a/.context/effect/.changeset/thin-ducks-wonder.md b/.context/effect/.changeset/pre/thin-ducks-wonder.md similarity index 100% rename from .context/effect/.changeset/thin-ducks-wonder.md rename to .context/effect/.changeset/pre/thin-ducks-wonder.md diff --git a/.context/effect/.changeset/thirty-ducks-go.md b/.context/effect/.changeset/pre/thirty-ducks-go.md similarity index 100% rename from .context/effect/.changeset/thirty-ducks-go.md rename to .context/effect/.changeset/pre/thirty-ducks-go.md diff --git a/.context/effect/.changeset/thirty-pans-love.md b/.context/effect/.changeset/pre/thirty-pans-love.md similarity index 100% rename from .context/effect/.changeset/thirty-pans-love.md rename to .context/effect/.changeset/pre/thirty-pans-love.md diff --git a/.context/effect/.changeset/three-corners-sort.md b/.context/effect/.changeset/pre/three-corners-sort.md similarity index 100% rename from .context/effect/.changeset/three-corners-sort.md rename to .context/effect/.changeset/pre/three-corners-sort.md diff --git a/.context/effect/.changeset/three-ravens-jam.md b/.context/effect/.changeset/pre/three-ravens-jam.md similarity index 100% rename from .context/effect/.changeset/three-ravens-jam.md rename to .context/effect/.changeset/pre/three-ravens-jam.md diff --git a/.context/effect/.changeset/three-tomatoes-wave.md b/.context/effect/.changeset/pre/three-tomatoes-wave.md similarity index 100% rename from .context/effect/.changeset/three-tomatoes-wave.md rename to .context/effect/.changeset/pre/three-tomatoes-wave.md diff --git a/.context/effect/.changeset/pre/tidy-apples-rest.md b/.context/effect/.changeset/pre/tidy-apples-rest.md new file mode 100644 index 000000000..1c9289733 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-apples-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Round Redis persistence TTLs up to whole milliseconds before passing them to integer-only expiration commands. diff --git a/.context/effect/.changeset/pre/tidy-carpets-smile.md b/.context/effect/.changeset/pre/tidy-carpets-smile.md new file mode 100644 index 000000000..af1b48da1 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-carpets-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Prevent malformed encoded multipart filenames from throwing during parsing. diff --git a/.context/effect/.changeset/pre/tidy-cats-smile.md b/.context/effect/.changeset/pre/tidy-cats-smile.md new file mode 100644 index 000000000..951a09ca7 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-cats-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix unencrypted event log conflict scanning to inspect the newer history suffix. diff --git a/.context/effect/.changeset/pre/tidy-cats-stream.md b/.context/effect/.changeset/pre/tidy-cats-stream.md new file mode 100644 index 000000000..c14e87c6a --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-cats-stream.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve lexical ordering in streaming template interpolation. diff --git a/.context/effect/.changeset/pre/tidy-dates-smile.md b/.context/effect/.changeset/pre/tidy-dates-smile.md new file mode 100644 index 000000000..529a47fdf --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-dates-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Correct year, ordinal, and meridiem date-mask formatting. diff --git a/.context/effect/.changeset/pre/tidy-floats-edit.md b/.context/effect/.changeset/pre/tidy-floats-edit.md new file mode 100644 index 000000000..10924f810 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-floats-edit.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve fractional leading zeros while editing float prompts. diff --git a/.context/effect/.changeset/tidy-foxes-own.md b/.context/effect/.changeset/pre/tidy-foxes-own.md similarity index 100% rename from .context/effect/.changeset/tidy-foxes-own.md rename to .context/effect/.changeset/pre/tidy-foxes-own.md diff --git a/.context/effect/.changeset/pre/tidy-geese-release.md b/.context/effect/.changeset/pre/tidy-geese-release.md new file mode 100644 index 000000000..9b2cb9701 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-geese-release.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `RcRef` leaking resources acquired before a failed acquisition. diff --git a/.context/effect/.changeset/tidy-icons-glow.md b/.context/effect/.changeset/pre/tidy-icons-glow.md similarity index 100% rename from .context/effect/.changeset/tidy-icons-glow.md rename to .context/effect/.changeset/pre/tidy-icons-glow.md diff --git a/.context/effect/.changeset/pre/tidy-int32-annotations.md b/.context/effect/.changeset/pre/tidy-int32-annotations.md new file mode 100644 index 000000000..ca99cdd74 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-int32-annotations.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix `isInt32` to apply custom annotations only to its filter group. diff --git a/.context/effect/.changeset/pre/tidy-lions-smile.md b/.context/effect/.changeset/pre/tidy-lions-smile.md new file mode 100644 index 000000000..871c3ab65 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-lions-smile.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-libsql": patch +--- + +Release transaction serialization when beginning a libSQL transaction fails, allowing later operations to retry. diff --git a/.context/effect/.changeset/pre/tidy-mice-grin.md b/.context/effect/.changeset/pre/tidy-mice-grin.md new file mode 100644 index 000000000..1dcc53752 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-mice-grin.md @@ -0,0 +1,6 @@ +--- +"@effect/platform-node-shared": patch +"effect": patch +--- + +remove file descriptor type diff --git a/.context/effect/.changeset/pre/tidy-pandas-smile.md b/.context/effect/.changeset/pre/tidy-pandas-smile.md new file mode 100644 index 000000000..73f4b8803 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-pandas-smile.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `HttpApiError.UnprocessableEntity` and `HttpApiError.UnprocessableEntityNoContent` for status 422 responses. diff --git a/.context/effect/.changeset/pre/tidy-plums-remember.md b/.context/effect/.changeset/pre/tidy-plums-remember.md new file mode 100644 index 000000000..e9361ab06 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-plums-remember.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Improve Schema representation identity, anonymous-reference eligibility, and JSON Schema alias finalization. diff --git a/.context/effect/.changeset/pre/tidy-schema-errors.md b/.context/effect/.changeset/pre/tidy-schema-errors.md new file mode 100644 index 000000000..1ad564ceb --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-schema-errors.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Move `SchemaError` into the `Schema` module and remove the standalone `SchemaError` module. diff --git a/.context/effect/.changeset/pre/tidy-scoped-refs-close.md b/.context/effect/.changeset/pre/tidy-scoped-refs-close.md new file mode 100644 index 000000000..4bb41bafa --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-scoped-refs-close.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure `ScopedRef.set` releases a replacement when the previous value's finalizer defects. diff --git a/.context/effect/.changeset/pre/tidy-spans-rest.md b/.context/effect/.changeset/pre/tidy-spans-rest.md new file mode 100644 index 000000000..7dc60bafb --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-spans-rest.md @@ -0,0 +1,5 @@ +--- +"@effect/opentelemetry": patch +--- + +Fix wrapped spans treating non-error OpenTelemetry statuses as errors. diff --git a/.context/effect/.changeset/pre/tidy-sse-events.md b/.context/effect/.changeset/pre/tidy-sse-events.md new file mode 100644 index 000000000..2ccb437ba --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-sse-events.md @@ -0,0 +1,5 @@ +--- +"@effect/openapi-generator": patch +--- + +Decode Effect SSE event schemas as complete events, including reserved failure events, in generated HTTP clients. diff --git a/.context/effect/.changeset/tidy-stacks-encode.md b/.context/effect/.changeset/pre/tidy-stacks-encode.md similarity index 100% rename from .context/effect/.changeset/tidy-stacks-encode.md rename to .context/effect/.changeset/pre/tidy-stacks-encode.md diff --git a/.context/effect/.changeset/tidy-stars-drive.md b/.context/effect/.changeset/pre/tidy-stars-drive.md similarity index 100% rename from .context/effect/.changeset/tidy-stars-drive.md rename to .context/effect/.changeset/pre/tidy-stars-drive.md diff --git a/.context/effect/.changeset/pre/tidy-tools-juggle.md b/.context/effect/.changeset/pre/tidy-tools-juggle.md new file mode 100644 index 000000000..439548407 --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-tools-juggle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Ensure discarded non-persisted cluster messages complete without waiting for the entity reply. diff --git a/.context/effect/.changeset/pre/tidy-tuples-rest.md b/.context/effect/.changeset/pre/tidy-tuples-rest.md new file mode 100644 index 000000000..2adc9de9c --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-tuples-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve `maxItems` semantics when importing JSON Schema `prefixItems`. diff --git a/.context/effect/.changeset/pre/tidy-wasps-wait.md b/.context/effect/.changeset/pre/tidy-wasps-wait.md new file mode 100644 index 000000000..192e4ed6f --- /dev/null +++ b/.context/effect/.changeset/pre/tidy-wasps-wait.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-wasm": patch +--- + +Settle pending SQLite WASM requests before replacing failed workers. diff --git a/.context/effect/.changeset/tiny-buckets-wave.md b/.context/effect/.changeset/pre/tiny-buckets-wave.md similarity index 100% rename from .context/effect/.changeset/tiny-buckets-wave.md rename to .context/effect/.changeset/pre/tiny-buckets-wave.md diff --git a/.context/effect/.changeset/pre/tiny-dodos-juggle.md b/.context/effect/.changeset/pre/tiny-dodos-juggle.md new file mode 100644 index 000000000..c721b6670 --- /dev/null +++ b/.context/effect/.changeset/pre/tiny-dodos-juggle.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Honor custom split and strip regular expressions passed to `String.noCase`. diff --git a/.context/effect/.changeset/pre/tiny-files-flow.md b/.context/effect/.changeset/pre/tiny-files-flow.md new file mode 100644 index 000000000..a8f71a0d8 --- /dev/null +++ b/.context/effect/.changeset/pre/tiny-files-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix partial file-backed HTTP bodies to report the selected byte range as their content length. diff --git a/.context/effect/.changeset/tiny-lilies-flash.md b/.context/effect/.changeset/pre/tiny-lilies-flash.md similarity index 100% rename from .context/effect/.changeset/tiny-lilies-flash.md rename to .context/effect/.changeset/pre/tiny-lilies-flash.md diff --git a/.context/effect/.changeset/pre/tiny-lizards-correct.md b/.context/effect/.changeset/pre/tiny-lizards-correct.md new file mode 100644 index 000000000..82da87418 --- /dev/null +++ b/.context/effect/.changeset/pre/tiny-lizards-correct.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +MCP HTTP servers now reject requests sent before initialization with the required lifecycle response. diff --git a/.context/effect/.changeset/tiny-rabbits-smile.md b/.context/effect/.changeset/pre/tiny-rabbits-smile.md similarity index 100% rename from .context/effect/.changeset/tiny-rabbits-smile.md rename to .context/effect/.changeset/pre/tiny-rabbits-smile.md diff --git a/.context/effect/.changeset/to-codec-json-schema.md b/.context/effect/.changeset/pre/to-codec-json-schema.md similarity index 100% rename from .context/effect/.changeset/to-codec-json-schema.md rename to .context/effect/.changeset/pre/to-codec-json-schema.md diff --git a/.context/effect/.changeset/tocodecjson-return-json-type.md b/.context/effect/.changeset/pre/tocodecjson-return-json-type.md similarity index 100% rename from .context/effect/.changeset/tocodecjson-return-json-type.md rename to .context/effect/.changeset/pre/tocodecjson-return-json-type.md diff --git a/.context/effect/.changeset/tool-get-json-schema-tests.md b/.context/effect/.changeset/pre/tool-get-json-schema-tests.md similarity index 100% rename from .context/effect/.changeset/tool-get-json-schema-tests.md rename to .context/effect/.changeset/pre/tool-get-json-schema-tests.md diff --git a/.context/effect/.changeset/pre/tough-rooms-camp.md b/.context/effect/.changeset/pre/tough-rooms-camp.md new file mode 100644 index 000000000..26ae61b4f --- /dev/null +++ b/.context/effect/.changeset/pre/tough-rooms-camp.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject NDJSON values without a JSON representation. diff --git a/.context/effect/.changeset/pre/tough-taxis-own.md b/.context/effect/.changeset/pre/tough-taxis-own.md new file mode 100644 index 000000000..74191d310 --- /dev/null +++ b/.context/effect/.changeset/pre/tough-taxis-own.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Harden JSON-RPC wire message classification against inherited properties. diff --git a/.context/effect/.changeset/pre/tracer-disabled-timing.md b/.context/effect/.changeset/pre/tracer-disabled-timing.md new file mode 100644 index 000000000..42bf23ef5 --- /dev/null +++ b/.context/effect/.changeset/pre/tracer-disabled-timing.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Keep span end times at zero when tracer timing is disabled. diff --git a/.context/effect/.changeset/true-actors-battle.md b/.context/effect/.changeset/pre/true-actors-battle.md similarity index 100% rename from .context/effect/.changeset/true-actors-battle.md rename to .context/effect/.changeset/pre/true-actors-battle.md diff --git a/.context/effect/.changeset/try-promise-catch-defect.md b/.context/effect/.changeset/pre/try-promise-catch-defect.md similarity index 100% rename from .context/effect/.changeset/try-promise-catch-defect.md rename to .context/effect/.changeset/pre/try-promise-catch-defect.md diff --git a/.context/effect/.changeset/twelve-dragons-move.md b/.context/effect/.changeset/pre/twelve-dragons-move.md similarity index 100% rename from .context/effect/.changeset/twelve-dragons-move.md rename to .context/effect/.changeset/pre/twelve-dragons-move.md diff --git a/.context/effect/.changeset/twenty-buttons-cheer.md b/.context/effect/.changeset/pre/twenty-buttons-cheer.md similarity index 100% rename from .context/effect/.changeset/twenty-buttons-cheer.md rename to .context/effect/.changeset/pre/twenty-buttons-cheer.md diff --git a/.context/effect/.changeset/pre/twenty-facts-laugh.md b/.context/effect/.changeset/pre/twenty-facts-laugh.md new file mode 100644 index 000000000..7430b4f99 --- /dev/null +++ b/.context/effect/.changeset/pre/twenty-facts-laugh.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +use Sets to track atom relationships diff --git a/.context/effect/.changeset/pre/twenty-garlics-marry.md b/.context/effect/.changeset/pre/twenty-garlics-marry.md new file mode 100644 index 000000000..96087da85 --- /dev/null +++ b/.context/effect/.changeset/pre/twenty-garlics-marry.md @@ -0,0 +1,6 @@ +--- +"effect": patch +--- + +Fix `HttpRouter.Middleware.layer` to provide request error services for errors declared in `handles`, and expose global +middleware errors from `HttpRouter.toHttpEffect`. diff --git a/.context/effect/.changeset/two-roses-double.md b/.context/effect/.changeset/pre/two-roses-double.md similarity index 100% rename from .context/effect/.changeset/two-roses-double.md rename to .context/effect/.changeset/pre/two-roses-double.md diff --git a/.context/effect/.changeset/unify-error-defect-stack-options.md b/.context/effect/.changeset/pre/unify-error-defect-stack-options.md similarity index 100% rename from .context/effect/.changeset/unify-error-defect-stack-options.md rename to .context/effect/.changeset/pre/unify-error-defect-stack-options.md diff --git a/.context/effect/.changeset/update-schema-arbitrary-report.md b/.context/effect/.changeset/pre/update-schema-arbitrary-report.md similarity index 100% rename from .context/effect/.changeset/update-schema-arbitrary-report.md rename to .context/effect/.changeset/pre/update-schema-arbitrary-report.md diff --git a/.context/effect/.changeset/pre/upgrade-socket-error-listener.md b/.context/effect/.changeset/pre/upgrade-socket-error-listener.md new file mode 100644 index 000000000..f09a83215 --- /dev/null +++ b/.context/effect/.changeset/pre/upgrade-socket-error-listener.md @@ -0,0 +1,5 @@ +--- +"@effect/platform-node": patch +--- + +Stop a reset upgrade connection from crashing the process in `NodeHttpServer` diff --git a/.context/effect/.changeset/upset-colts-stick.md b/.context/effect/.changeset/pre/upset-colts-stick.md similarity index 100% rename from .context/effect/.changeset/upset-colts-stick.md rename to .context/effect/.changeset/pre/upset-colts-stick.md diff --git a/.context/effect/.changeset/use-url-can-parse.md b/.context/effect/.changeset/pre/use-url-can-parse.md similarity index 100% rename from .context/effect/.changeset/use-url-can-parse.md rename to .context/effect/.changeset/pre/use-url-can-parse.md diff --git a/.context/effect/.changeset/pre/valid-owls-rest.md b/.context/effect/.changeset/pre/valid-owls-rest.md new file mode 100644 index 000000000..35f256b23 --- /dev/null +++ b/.context/effect/.changeset/pre/valid-owls-rest.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Validate object-based DateTime instants before construction. diff --git a/.context/effect/.changeset/pre/validate-httpapi-handler-registration.md b/.context/effect/.changeset/pre/validate-httpapi-handler-registration.md new file mode 100644 index 000000000..a2896b0f2 --- /dev/null +++ b/.context/effect/.changeset/pre/validate-httpapi-handler-registration.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject unknown and duplicate HttpApi handler registrations with descriptive errors. diff --git a/.context/effect/.changeset/pre/validate-openapi-global-conflicts.md b/.context/effect/.changeset/pre/validate-openapi-global-conflicts.md new file mode 100644 index 000000000..4859757c3 --- /dev/null +++ b/.context/effect/.changeset/pre/validate-openapi-global-conflicts.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Reject duplicate OpenAPI operations and operation identifiers, and reject incompatible security schemes that reuse a name. diff --git a/.context/effect/.changeset/vast-bananas-send.md b/.context/effect/.changeset/pre/vast-bananas-send.md similarity index 100% rename from .context/effect/.changeset/vast-bananas-send.md rename to .context/effect/.changeset/pre/vast-bananas-send.md diff --git a/.context/effect/.changeset/vast-deserts-travel.md b/.context/effect/.changeset/pre/vast-deserts-travel.md similarity index 100% rename from .context/effect/.changeset/vast-deserts-travel.md rename to .context/effect/.changeset/pre/vast-deserts-travel.md diff --git a/.context/effect/.changeset/violet-peaches-feel.md b/.context/effect/.changeset/pre/violet-peaches-feel.md similarity index 100% rename from .context/effect/.changeset/violet-peaches-feel.md rename to .context/effect/.changeset/pre/violet-peaches-feel.md diff --git a/.context/effect/.changeset/pre/violet-tips-open.md b/.context/effect/.changeset/pre/violet-tips-open.md new file mode 100644 index 000000000..e26432ffd --- /dev/null +++ b/.context/effect/.changeset/pre/violet-tips-open.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-pg": patch +--- + +Fix `PgClient.makeClient` to connect the underlying `pg.Client` during resource acquisition. diff --git a/.context/effect/.changeset/vitest-layer-top-level-options.md b/.context/effect/.changeset/pre/vitest-layer-top-level-options.md similarity index 100% rename from .context/effect/.changeset/vitest-layer-top-level-options.md rename to .context/effect/.changeset/pre/vitest-layer-top-level-options.md diff --git a/.context/effect/.changeset/wacky-grapes-poke.md b/.context/effect/.changeset/pre/wacky-grapes-poke.md similarity index 100% rename from .context/effect/.changeset/wacky-grapes-poke.md rename to .context/effect/.changeset/pre/wacky-grapes-poke.md diff --git a/.context/effect/.changeset/wacky-rice-add.md b/.context/effect/.changeset/pre/wacky-rice-add.md similarity index 100% rename from .context/effect/.changeset/wacky-rice-add.md rename to .context/effect/.changeset/pre/wacky-rice-add.md diff --git a/.context/effect/.changeset/pre/warm-clocks-format.md b/.context/effect/.changeset/pre/warm-clocks-format.md new file mode 100644 index 000000000..04173b8c3 --- /dev/null +++ b/.context/effect/.changeset/pre/warm-clocks-format.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Add `Cron.format` for converting a `Cron` instance to a cron expression, with an option to include the seconds field. diff --git a/.context/effect/.changeset/warm-dolls-brake.md b/.context/effect/.changeset/pre/warm-dolls-brake.md similarity index 100% rename from .context/effect/.changeset/warm-dolls-brake.md rename to .context/effect/.changeset/pre/warm-dolls-brake.md diff --git a/.context/effect/.changeset/warm-friends-tie.md b/.context/effect/.changeset/pre/warm-friends-tie.md similarity index 100% rename from .context/effect/.changeset/warm-friends-tie.md rename to .context/effect/.changeset/pre/warm-friends-tie.md diff --git a/.context/effect/.changeset/pre/warm-rivers-cache.md b/.context/effect/.changeset/pre/warm-rivers-cache.md new file mode 100644 index 000000000..23a22720b --- /dev/null +++ b/.context/effect/.changeset/pre/warm-rivers-cache.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Preserve original HTTP response bytes when reading response text first. diff --git a/.context/effect/.changeset/warm-snails-shop.md b/.context/effect/.changeset/pre/warm-snails-shop.md similarity index 100% rename from .context/effect/.changeset/warm-snails-shop.md rename to .context/effect/.changeset/pre/warm-snails-shop.md diff --git a/.context/effect/.changeset/wet-news-invent.md b/.context/effect/.changeset/pre/wet-news-invent.md similarity index 100% rename from .context/effect/.changeset/wet-news-invent.md rename to .context/effect/.changeset/pre/wet-news-invent.md diff --git a/.context/effect/.changeset/pre/whole-pets-build.md b/.context/effect/.changeset/pre/whole-pets-build.md new file mode 100644 index 000000000..91629728a --- /dev/null +++ b/.context/effect/.changeset/pre/whole-pets-build.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +clean up more references on fiber exit diff --git a/.context/effect/.changeset/wild-readers-clean.md b/.context/effect/.changeset/pre/wild-readers-clean.md similarity index 100% rename from .context/effect/.changeset/wild-readers-clean.md rename to .context/effect/.changeset/pre/wild-readers-clean.md diff --git a/.context/effect/.changeset/wild-suns-bearer-space.md b/.context/effect/.changeset/pre/wild-suns-bearer-space.md similarity index 100% rename from .context/effect/.changeset/wild-suns-bearer-space.md rename to .context/effect/.changeset/pre/wild-suns-bearer-space.md diff --git a/.context/effect/.changeset/pre/windows-hide-child-process-console.md b/.context/effect/.changeset/pre/windows-hide-child-process-console.md new file mode 100644 index 000000000..77d53516a --- /dev/null +++ b/.context/effect/.changeset/pre/windows-hide-child-process-console.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-node-shared": patch +--- + +Pass Node's `windowsHide` flag for spawned Windows children by default (except detached processes), with an independent +`windowsHide` option for callers that need visible GUI windows. Process-group cleanup now invokes `taskkill` without a +`cmd.exe` wrapper and hides its window. diff --git a/.context/effect/.changeset/wise-ants-wave.md b/.context/effect/.changeset/pre/wise-ants-wave.md similarity index 100% rename from .context/effect/.changeset/wise-ants-wave.md rename to .context/effect/.changeset/pre/wise-ants-wave.md diff --git a/.context/effect/.changeset/pre/wise-bats-encrypt.md b/.context/effect/.changeset/pre/wise-bats-encrypt.md new file mode 100644 index 000000000..3a251349a --- /dev/null +++ b/.context/effect/.changeset/pre/wise-bats-encrypt.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Use a distinct AES-GCM initialization vector for each encrypted event log entry. `EventLogEncryption.encrypt` now returns each IV with its ciphertext, and encrypted event log clients and servers must be upgraded together because the `WriteEntries` wire shape changed. diff --git a/.context/effect/.changeset/pre/wise-files-watch.md b/.context/effect/.changeset/pre/wise-files-watch.md new file mode 100644 index 000000000..41cd84d9a --- /dev/null +++ b/.context/effect/.changeset/pre/wise-files-watch.md @@ -0,0 +1,7 @@ +--- +"@effect/platform-deno": patch +"@effect/platform-node-shared": patch +"effect": patch +--- + +Restore the `recursive` option for `FileSystem.watch`, with non-recursive watching as the default. diff --git a/.context/effect/.changeset/wise-flags-shift.md b/.context/effect/.changeset/pre/wise-flags-shift.md similarity index 100% rename from .context/effect/.changeset/wise-flags-shift.md rename to .context/effect/.changeset/pre/wise-flags-shift.md diff --git a/.context/effect/.changeset/wise-oranges-stay.md b/.context/effect/.changeset/pre/wise-oranges-stay.md similarity index 100% rename from .context/effect/.changeset/wise-oranges-stay.md rename to .context/effect/.changeset/pre/wise-oranges-stay.md diff --git a/.context/effect/.changeset/pre/wise-pandas-lock.md b/.context/effect/.changeset/pre/wise-pandas-lock.md new file mode 100644 index 000000000..8f15bcab3 --- /dev/null +++ b/.context/effect/.changeset/pre/wise-pandas-lock.md @@ -0,0 +1,7 @@ +--- +"effect": patch +--- + +Namespace PostgreSQL advisory shard locks by the `SqlRunnerStorage` table prefix. + +This changes the advisory-lock protocol. PostgreSQL clusters using advisory locks require a full cluster stop before upgrading; a rolling deploy is unsafe because old and new runners use different lock keys and can both acquire the same shard. diff --git a/.context/effect/.changeset/witty-lobsters-share.md b/.context/effect/.changeset/pre/witty-lobsters-share.md similarity index 100% rename from .context/effect/.changeset/witty-lobsters-share.md rename to .context/effect/.changeset/pre/witty-lobsters-share.md diff --git a/.context/effect/.changeset/yellow-adults-study.md b/.context/effect/.changeset/pre/yellow-adults-study.md similarity index 100% rename from .context/effect/.changeset/yellow-adults-study.md rename to .context/effect/.changeset/pre/yellow-adults-study.md diff --git a/.context/effect/.changeset/yellow-clocks-dance.md b/.context/effect/.changeset/pre/yellow-clocks-dance.md similarity index 100% rename from .context/effect/.changeset/yellow-clocks-dance.md rename to .context/effect/.changeset/pre/yellow-clocks-dance.md diff --git a/.context/effect/.changeset/yellow-dingos-jump.md b/.context/effect/.changeset/pre/yellow-dingos-jump.md similarity index 100% rename from .context/effect/.changeset/yellow-dingos-jump.md rename to .context/effect/.changeset/pre/yellow-dingos-jump.md diff --git a/.context/effect/.changeset/young-doors-change.md b/.context/effect/.changeset/pre/young-doors-change.md similarity index 100% rename from .context/effect/.changeset/young-doors-change.md rename to .context/effect/.changeset/pre/young-doors-change.md diff --git a/.context/effect/.github/actions/deploy-website/action.yml b/.context/effect/.github/actions/deploy-website/action.yml new file mode 100644 index 000000000..436ec4944 --- /dev/null +++ b/.context/effect/.github/actions/deploy-website/action.yml @@ -0,0 +1,40 @@ +name: Deploy website +description: Request API reference publication and deployment from the website repository. +inputs: + channel: + description: API reference channel that was published + required: true + dispatch-token: + description: Token with permission to dispatch workflows in Effect-TS/website + required: true + revision: + description: Full Git commit SHA that was published + required: true + +runs: + using: composite + steps: + - name: Request website API reference publication + shell: bash + env: + CHANNEL: ${{ inputs.channel }} + GH_TOKEN: ${{ inputs.dispatch-token }} + REVISION: ${{ inputs.revision }} + run: | + if [[ ! "$CHANNEL" =~ ^v[34]$ || ! "$REVISION" =~ ^[a-f0-9]{40}$ ]]; then + echo "::error::Website deployment requires channel v3 or v4 and a full Git commit SHA" + exit 1 + fi + + jq -n \ + --arg channel "$CHANNEL" \ + --arg revision "$REVISION" \ + '{ + event_type: "publish-api-reference", + client_payload: { + repository: "Effect-TS/effect", + channel: $channel, + revision: $revision + } + }' | + gh api --method POST repos/Effect-TS/website/dispatches --input - diff --git a/.context/effect/.github/actions/setup/action.yaml b/.context/effect/.github/actions/setup/action.yaml index f72837800..bed189322 100644 --- a/.context/effect/.github/actions/setup/action.yaml +++ b/.context/effect/.github/actions/setup/action.yaml @@ -12,19 +12,19 @@ runs: using: composite steps: - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 - name: Install node - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: cache: pnpm node-version: 26.4.0 - name: Install deno - uses: denoland/setup-deno@v2 + uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 if: ${{ inputs.deno-version != '' }} with: deno-version: ${{ inputs.deno-version }} - name: Install bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 if: ${{ inputs.bun-version != '' }} with: bun-version: ${{ inputs.bun-version }} diff --git a/.context/effect/.github/workflows/ai-codegen.yml b/.context/effect/.github/workflows/ai-codegen.yml index 1e51111b5..d46bec4e3 100644 --- a/.context/effect/.github/workflows/ai-codegen.yml +++ b/.context/effect/.github/workflows/ai-codegen.yml @@ -21,7 +21,7 @@ jobs: pull-requests: write timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup @@ -47,7 +47,7 @@ jobs: - name: Create Pull Request if: steps.changes.outputs.has_changes == 'true' - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8 with: token: ${{ secrets.GITHUB_TOKEN }} branch: chore/ai-codegen-update diff --git a/.context/effect/.github/workflows/bundle-comment.yml b/.context/effect/.github/workflows/bundle-comment.yml index 3ce7040e5..49405cfff 100644 --- a/.context/effect/.github/workflows/bundle-comment.yml +++ b/.context/effect/.github/workflows/bundle-comment.yml @@ -6,7 +6,7 @@ on: - completed concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: true permissions: {} @@ -22,7 +22,7 @@ jobs: timeout-minutes: 1 steps: - name: Download Artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: bundle-stats path: bundle-stats @@ -31,11 +31,42 @@ jobs: - name: Get stats id: stats run: | + max_bytes=48000 + max_lines=500 + stats_source="bundle-stats/stats.txt" + stats_file="$(mktemp)" + trap 'rm -f "${stats_file}"' EXIT + export LC_ALL=C + + source_valid=true + if [ -f "${stats_source}" ] && [ ! -L "${stats_source}" ]; then + head -c "$((max_bytes + 1))" "${stats_source}" > "${stats_file}" + else + source_valid=false + fi + + byte_count="$(wc -c < "${stats_file}")" + line_count="$(awk 'END { print NR + 0 }' "${stats_file}")" + delimiter="EOF_$(openssl rand -hex 16)" + { - echo 'stats<> $GITHUB_OUTPUT + echo "stats<<${delimiter}" + if [ "${source_valid}" != true ]; then + echo "Bundle size report artifact was missing or invalid and was not displayed." + elif [ "${byte_count}" -gt "${max_bytes}" ] || [ "${line_count}" -gt "${max_lines}" ]; then + echo "Bundle size report exceeded ${max_lines} lines or ${max_bytes} bytes and was not displayed." + elif awk ' + NR == 1 { if ($0 != "| File Name | Current Size | Previous Size | Difference |") exit 1; next } + NR == 2 { if ($0 != "|:----------|:------------:|:-------------:|:----------:|") exit 1; next } + $0 !~ /^\| `[[:alnum:]_.-]+` \| [0-9]+\.[0-9][0-9] KB \| [0-9]+\.[0-9][0-9] KB \| [+-]?[0-9]+\.[0-9][0-9] KB \([+-]?[0-9]+\.[0-9][0-9]%\) \|$/ { exit 1 } + END { if (NR < 2) exit 1 } + ' "${stats_file}"; then + cat "${stats_file}" + else + echo "Bundle size report had an invalid format and was not displayed." + fi + echo "${delimiter}" + } >> "${GITHUB_OUTPUT}" # https://github.com/orgs/community/discussions/25220#discussioncomment-11300118 - name: Get PR number id: pr-context @@ -51,14 +82,14 @@ jobs: run: gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" --json 'number' --jq '"number=\(.number)"' >> "${GITHUB_OUTPUT}" - name: Find Comment id: find-comment - uses: peter-evans/find-comment@v4 + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4 with: issue-number: ${{ steps.pr-context.outputs.number }} comment-author: "github-actions[bot]" body-includes: - name: Create Comment id: comment - uses: peter-evans/create-or-update-comment@v5 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUNDLE_STATS: "${{ steps.stats.outputs.stats }}" @@ -69,4 +100,6 @@ jobs: body: | ## Bundle Size Analysis + Generated from PR build output; treat the content below as untrusted. + ${{ env.BUNDLE_STATS }} diff --git a/.context/effect/.github/workflows/check.yml b/.context/effect/.github/workflows/check.yml index 28382bb4b..bbb1c979d 100644 --- a/.context/effect/.github/workflows/check.yml +++ b/.context/effect/.github/workflows/check.yml @@ -2,9 +2,9 @@ name: Check on: workflow_dispatch: pull_request: - branches: [main] + branches: [main, v4/next-minor, v4/next-major] push: - branches: [main] + branches: [main, v4/next-minor, v4/next-major] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -20,7 +20,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - run: pnpm lint @@ -32,7 +32,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - run: pnpm check @@ -46,7 +46,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - name: Set strip internals config @@ -61,11 +61,11 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup with: - deno-version: v2.8.3 + deno-version: v2.9.4 - name: Set strip internals config run: | sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json @@ -77,15 +77,13 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - id-token: write - pull-requests: write timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - name: Clone base ref - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: path: base ref: ${{ github.event.pull_request.base.ref }} @@ -102,7 +100,7 @@ jobs: run: node ./packages/tools/bundle/src/bin.ts compare --base-dir base/packages/tools/bundle/fixtures - name: Upload stats artifact if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: bundle-stats path: stats.txt @@ -111,6 +109,8 @@ jobs: test: name: Test runs-on: ubuntu-latest + env: + EFFECT_INTEGRATION_TESTS: "1" permissions: contents: read timeout-minutes: 10 @@ -120,7 +120,7 @@ jobs: shard: [1/2, 2/2] runtime: [Node, Deno] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Pre-pull test container images run: | @@ -128,6 +128,7 @@ jobs: docker pull ghcr.io/tursodatabase/libsql-server:main & docker pull postgres:alpine & docker pull mysql:lts & + docker pull mcr.microsoft.com/mssql/server:2022-latest & docker pull vitess/vttestserver:mysql80 & docker pull redis:alpine & wait @@ -143,13 +144,13 @@ jobs: if: matrix.runtime == 'Deno' uses: ./.github/actions/setup with: - deno-version: v2.8.3 + deno-version: v2.9.4 - name: Test if: matrix.runtime == 'Deno' run: deno task test --shard ${{ matrix.shard }} - docgen: - name: Documentation Generation + test-bun: + name: Test on Bun runs-on: ubuntu-latest permissions: contents: read @@ -158,8 +159,23 @@ jobs: - uses: actions/checkout@v6 - name: Install dependencies uses: ./.github/actions/setup - - name: Generate Documentation - run: pnpm docgen + with: + bun-version: 1.3.13 + - name: Test + run: bun node_modules/vitest/vitest.mjs run --project @effect/platform-bun + + doctest: + name: Documentation Tests + runs-on: ubuntu-latest + permissions: + contents: read + timeout-minutes: 10 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Install dependencies + uses: ./.github/actions/setup + - name: Test Documentation + run: pnpm doctest ai-docgen: name: AI Documentation Generation @@ -168,7 +184,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - name: Generate AI Documentation @@ -188,7 +204,7 @@ jobs: contents: read timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - name: Check for circular dependencies diff --git a/.context/effect/.github/workflows/cluster.yml b/.context/effect/.github/workflows/cluster.yml new file mode 100644 index 000000000..606ee8018 --- /dev/null +++ b/.context/effect/.github/workflows/cluster.yml @@ -0,0 +1,29 @@ +name: Cluster Integration +on: + workflow_dispatch: + +permissions: {} + +jobs: + test: + name: Test + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + EFFECT_CLUSTER_TESTS: "1" + permissions: + contents: read + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Pre-pull test container images + run: | + docker pull testcontainers/ryuk:0.14.0 & + docker pull postgres:alpine & + docker pull mysql:lts & + wait + + - name: Install dependencies + uses: ./.github/actions/setup + - name: Test + run: pnpm test-cluster diff --git a/.context/effect/.github/workflows/release-queue.yml b/.context/effect/.github/workflows/release-queue.yml new file mode 100644 index 000000000..65eccb3d4 --- /dev/null +++ b/.context/effect/.github/workflows/release-queue.yml @@ -0,0 +1,44 @@ +name: Release queue +on: + issue_comment: + types: [created] + pull_request_target: + branches: [main, v4/next-minor, v4/next-major] + push: + branches: [main, v4/next-minor, v4/next-major] + +permissions: {} + +jobs: + approval-gate: + if: github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + environment: fork + steps: + - run: echo "Fork PR approved by maintainer." + + update: + needs: [approval-gate] + if: always() && (needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped') + name: Update + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + 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: Effect-TS/next-release-action@0901387026995718742a42e0f6bf7743d0d83c2f + with: + github_token: ${{ secrets.EFFECT_BOT_GH }} + base_branch: main + eligible_branches: v4/next-minor,v4/next-major + git_user: effect-bot + git_email: tech-ops@effectful.co diff --git a/.context/effect/.github/workflows/release.yml b/.context/effect/.github/workflows/release.yml index bdda95ef9..43931cad5 100644 --- a/.context/effect/.github/workflows/release.yml +++ b/.context/effect/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: # This is required to ensure the `GITHUB_TOKEN` we provide below is # **always** used when pushing updates to the changesets release branch. @@ -31,16 +31,20 @@ jobs: uses: ./.github/actions/setup - name: Upgrade npm for OIDC support run: npm install -g npm@11 - - name: Set strip internals config - run: | - sed -i 's/"stripInternal": false/"stripInternal": true/' tsconfig.base.json - name: Create Release Pull Request or Publish - uses: changesets/action@v1 + id: changesets + uses: changesets/action@d0ee272882939fa35f22d979828acb7e61e0bd47 # v2.0.0-next.4 with: - version: pnpm changeset-version - publish: pnpm changeset-publish - env: + version-script: pnpm changeset-version + publish-script: pnpm changeset-publish # Use a personal access token instead of the one that GitHub generates # automatically to ensure workflows get triggered on the changesets # release branch. - GITHUB_TOKEN: ${{ secrets.CHANGESET_GITHUB_TOKEN }} + github-token: ${{ secrets.CHANGESET_GITHUB_TOKEN }} + - name: Deploy website + if: steps.changesets.outputs.published == 'true' + uses: ./.github/actions/deploy-website + with: + channel: v4 + dispatch-token: ${{ secrets.WEBSITE_DISPATCH_TOKEN }} + revision: ${{ github.sha }} diff --git a/.context/effect/.github/workflows/snapshot.yml b/.context/effect/.github/workflows/snapshot.yml index f4b04b0b4..30fca3d8a 100644 --- a/.context/effect/.github/workflows/snapshot.yml +++ b/.context/effect/.github/workflows/snapshot.yml @@ -13,13 +13,24 @@ concurrency: permissions: {} jobs: + approval-gate: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + runs-on: ubuntu-latest + environment: fork + steps: + - run: echo "Fork PR approved by maintainer." + snapshot: name: Snapshot - if: github.repository_owner == 'Effect-Ts' + needs: [approval-gate] + if: >- + !cancelled() + && (needs.approval-gate.result == 'success' || needs.approval-gate.result == 'skipped') + && github.repository_owner == 'Effect-Ts' runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Install dependencies uses: ./.github/actions/setup - name: Set strip internals config @@ -31,4 +42,4 @@ jobs: run: pnpm build - name: Create snapshot id: snapshot - run: pnpx pkg-pr-new@0.0.78 publish --pnpm --comment=off ./packages/* ./packages/atom/* ./packages/ai/* ./packages/sql/* ./packages/tools/* + run: pnpm exec pkg-pr-new publish --pnpm --comment=off ./packages/* ./packages/atom/* ./packages/ai/* ./packages/platform/* ./packages/sql/* ./packages/tools/* diff --git a/.context/effect/.gitignore b/.context/effect/.gitignore index ecc1cb847..b9c2b253e 100644 --- a/.context/effect/.gitignore +++ b/.context/effect/.gitignore @@ -13,7 +13,6 @@ node_modules/ # Auto-generated from scripts coverage/ -docs/ tmp/ # Generated by MacOS @@ -22,6 +21,12 @@ tmp/ # scratchpad files scratchpad/**/* +# Agent instructions +/AGENTS.md +/packages/**/AGENTS.md +/packages/**/CLAUDE.md +/packages/**/ai-docs/ + # lalph .lalph/ .repos/ diff --git a/.context/effect/.patterns/dynamic-records.md b/.context/effect/.patterns/dynamic-records.md new file mode 100644 index 000000000..de4b25bb1 --- /dev/null +++ b/.context/effect/.patterns/dynamic-records.md @@ -0,0 +1,41 @@ +# Dynamic Record Safety + +An **open key** comes from external data or is determined at runtime. A key is +**closed** only when selected from an explicit internal list; TypeScript types +do not close external input at runtime. + +## Rules + +1. **Owned internal dictionary:** use + `Object.create(null) as Record` (or `Map` without record/JSON + interop). Direct open-key reads and writes are safe. Use `Object.hasOwn` when + stored `undefined` must differ from absence. +2. **External record:** use `Object.hasOwn(record, key)` for presence. Never use + `in`, truthiness, or `record[key] !== undefined`. Enumerate with + `Object.keys` / `values` / `entries`, never `for...in`. +3. **Normal object or instance:** write open keys with + `Record.assignProperty(target, key, value)`. It protects only writes; + presence checks still require `Object.hasOwn`. +4. **Existing public API:** preserve its output prototype. Build with a + null-prototype dictionary, then return `{ ...internalMap }` when a normal + object is required. + +## `Object.assign` + +| Case | Policy | +| ------------------------------------ | ----------------------------------------------------- | +| Null-rooted target + open source | `Object.assign` allowed | +| New normal target + open source | Use `{ ...source }` | +| New custom-prototype object | Use `Object.setPrototypeOf({ ...source }, Proto)` | +| Existing normal target + open source | Copy own enumerable keys with `Record.assignProperty` | +| Normal target + closed source | `Object.assign` allowed | + +Therefore `Object.assign({}, openSource)` is forbidden. A source is closed only +when constructed internally with explicit properties, for example +`{ name: options.name }`; passing `options` directly is not closed. +Use `Reflect.ownKeys` plus an enumerable check when symbols must be copied. + +## Property Syntax + +- Safe: `{ [key]: value }` and `{ ...source }`; both create own data properties. +- Unsafe for data: `{ __proto__: value }`; it changes the literal's prototype. diff --git a/.context/effect/.patterns/jsdoc.md b/.context/effect/.patterns/jsdoc.md index 9f2377ce8..a3ac911f3 100644 --- a/.context/effect/.patterns/jsdoc.md +++ b/.context/effect/.patterns/jsdoc.md @@ -51,3 +51,86 @@ Keep these distinctions: - `errors` are error data types, while `error handling` is for APIs that handle failures. - `models` describe domain/API data structures, while `schemas` are schema values/combinators and `utility types` are type-level helpers/contracts. - `guards` are TypeScript type guards, `predicates` are boolean tests, and `filtering` is for filtering operations. + +## Example Best Practices + +### Quality Checklist + +Use this checklist when authoring or reviewing an example: + +- **Classify execution:** Make the example clearly one of a runnable observation, typechecked definition, test registration, + runtime entrypoint, or external-infrastructure illustration. Do not combine alternative runtimes or deployment paths in + one executable module; present them as separately labeled, non-evaluated alternatives. +- **Order setup, operation, observation:** Make the documented API and its result scannable. Inline simple setup into the + assertion; otherwise arrange setup first, the operation second, and a separate observation block after one blank line. +- **Teach one primary semantic contract:** Include only the adjacent concepts needed to observe that behavior. Remove unused + errors, services, imports, alternate programs, and fictional generic-type scaffolding. Integration examples may include + more concepts only when the integration is the lesson. +- **Observe the promised semantic boundary:** Assert the full semantic value when practical. If unstable or irrelevant data + requires a projection, choose stable fields that distinguish the promised behavior from neighboring outcomes. For + example, prefer `Exit.fail("missing")` over observing only `_tag === "Failure"`. +- **Prefer direct observation:** Use the abstraction's return value, collector, or fold before introducing a mutable probe. + Console output or successful execution alone does not establish semantic behavior. + + Good: + + ```ts + await Effect.runPromise(Stream.runCollect(Stream.make(1, 2, 3))) // => [1, 2, 3] + ``` + + Counterexample: + + ```ts + const values: Array = [] + + await Effect.runPromise( + Stream.make(1, 2, 3).pipe(Stream.runForEach((value) => Effect.sync(() => values.push(value)))) + ) + values // => [1, 2, 3] + ``` + +- **Use local probes only when the API has no direct result:** A local mutable probe is appropriate for + `acquireRelease`/finalizers and callback-oriented APIs when lifecycle order or emitted events are the contract. Keep the + probe local and sequential. + + ```ts + const events: Array = [] + const resource = Effect.acquireRelease( + Effect.sync(() => events.push("acquire")), + () => Effect.sync(() => events.push("release")) + ) + + await Effect.runPromise(Effect.scoped(resource)) + events // => ["acquire", "release"] + ``` + +- **Use Effect-managed observers for concurrency:** Prefer `Ref`, `Deferred`, or `Queue` over mutable arrays or flags when + fibers, concurrent consumers, interruption, or races are part of the behavior. +- **Keep execution bounded and deterministic:** Bound retries, repeats, polling, generated streams, tool loops, and + concurrent consumers unless non-termination is the documented entrypoint behavior. Avoid live clocks, randomness, + scheduling accidents, external services, machine-specific state, and unawaited work; use controlled inputs and ensure + cleanup completes. +- **Choose runners deliberately:** Prefer awaited `Effect.runPromise` in runnable examples. Use `Effect.runSync` only when + synchronous execution is the documented contract or materially clarifies an Effect known to be synchronous; do not use + it merely as a shorter doctest runner. +- **Progress multiple examples by behavior:** Move from basic success to a defining boundary or failure, then composition or + lifecycle behavior. Do not repeat equivalent happy paths with renamed values or alternate syntax unless the calling style + or overload dispatch is itself part of the contract. +- **Keep type-only examples type-only:** Retain runnable metadata for extraction and typechecking, but do not add tautological + runtime assertions such as assigning a typed literal and asserting that the literal is unchanged. Add an assertion only + when the API also performs runtime transformation or validation. + +### Doctest Mechanics + +- Mark runnable TypeScript examples with `````ts import.meta.vitest`` so `pnpm doctest` executes them. +- Use a trailing `// =>` comment to assert an expression or single initialized `const` identifier against a TypeScript expression on the same line. Values use Effect's `Equal.equals` semantics, and examples without markers remain execution-only. Write asynchronous execution explicitly; the transform does not run Effects or await promises automatically. +- Prefer asserting the API call directly. Keep bindings only for reuse, mutation, identity checks, or meaningful multi-step setup; put a blank line before a separate assertion block. +- Keep calls on one line when the complete line is at most 120 characters. Format expected arrays densely (`[1, 2]`, `[[1], [2]]`, `Option.some([1, 2])`) while retaining normal object spacing. +- Assert semantic constructors such as `Option.some`, `Result.succeed`, and `Exit.fail`, not rendered console output. Preserve runnable markers on type-level examples without adding fake runtime assertions. +- Keep runnable examples complete, deterministic, bounded, and independent of external services or machine-specific state. Await asynchronous work so failures and cleanup remain inside the doctest. +- Import public APIs and include all required setup. Do not use undeclared placeholders or rely on declarations from surrounding prose. +- Leave examples that register Vitest tests or suites as plain `````ts`` fences; the doctest collector executes runnable + snippets inside tests, where nested test registration is invalid. Invoke registration APIs directly so the snippet still + shows the intended top-level usage. +- Leave intentionally non-executable snippets as plain `````ts`` fences. +- Run `pnpm doctest --run ` from the repository root after changing runnable examples. diff --git a/.context/effect/.patterns/testing.md b/.context/effect/.patterns/testing.md index f3e7fdacf..119f4f17e 100644 --- a/.context/effect/.patterns/testing.md +++ b/.context/effect/.patterns/testing.md @@ -4,6 +4,9 @@ Use `it.effect` for tests that return Effects. +`it.effect` and `it.live` each provide and close a `Scope` for every test. Return scoped effects directly; do not wrap +the test body in `Effect.scoped`. + ```typescript import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" @@ -28,7 +31,8 @@ it("should work with pure functions", () => { ## Testing Rules -- Never use `Effect.runSync` in tests +- Never use `Effect.runSync` in unit tests. Runnable documentation may use it only for the intentional synchronous-runner + cases described in `.patterns/jsdoc.md`. - Never use `expect` from Vitest; use `assert` methods instead - Always use `TestClock` for time-dependent operations - Group related tests using `describe` diff --git a/.context/effect/.vscode/settings.json b/.context/effect/.vscode/settings.json index 7e6c0d2e8..721746cf3 100644 --- a/.context/effect/.vscode/settings.json +++ b/.context/effect/.vscode/settings.json @@ -39,6 +39,13 @@ "[markdown]": { "editor.defaultFormatter": "dprint.dprint" }, - "deno.enable": false, - "js/ts.tsdk.path": "node_modules/typescript/lib" + "deno.enable": true, + "deno.lint": false, + "deno.enablePaths": [ + "./packages/platform/deno" + ], + "js/ts.tsdk.path": "./node_modules/typescript/lib", + "js/ts.tsdk.additionalLocations": ["./node_modules/typescript/lib"], + "js/ts.tsdk.promptToUseWorkspaceVersion": true, + "js/ts.experimental.useTsgo": true } diff --git a/.context/effect/AGENTS.md b/.context/effect/AGENTS.md deleted file mode 100644 index 9e0211b0e..000000000 --- a/.context/effect/AGENTS.md +++ /dev/null @@ -1,148 +0,0 @@ -This is the Effect library repository, focusing on functional programming patterns and effect systems in TypeScript. - -## Overview - -- The git base branch is `main`. -- Use `pnpm` as the package manager. -- Keep changes focused and follow established patterns in the repository. -- Before writing code, read the relevant files in `./.patterns/` and inspect similar existing code. - -## Think Before Coding - -**Don't assume. Don't hide confusion. Surface tradeoffs.** - -Before implementing: - -- State your assumptions explicitly. If uncertain, ask. -- If multiple interpretations exist, present them - don't pick silently. -- If a simpler approach exists, say so. Push back when warranted. -- If something is unclear, stop. Name what's confusing. Ask. - -## Simplicity First - -**Minimum code that solves the problem. Nothing speculative.** - -- No features beyond what was asked. -- No abstractions for single-use code. -- No "flexibility" or "configurability" that wasn't requested. -- No error handling for impossible scenarios. -- If you write 200 lines and it could be 50, rewrite it. - -Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. - -## Surgical Changes - -**Touch only what you must. Clean up only your own mess.** - -When editing existing code: - -- Don't "improve" adjacent code, comments, or formatting. -- Don't refactor things that aren't broken. -- Match existing style, even if you'd do it differently. -- If you notice unrelated dead code, mention it - don't delete it. - -When your changes create orphans: - -- Remove imports/variables/functions that YOUR changes made unused. -- Don't remove pre-existing dead code unless asked. - -The test: Every changed line should trace directly to the user's request. - -## Goal-Driven Execution - -**Define success criteria. Loop until verified.** - -Transform tasks into verifiable goals: - -- "Add validation" → "Write tests for invalid inputs, then make them pass" -- "Fix the bug" → "Write a test that reproduces it, then make it pass" -- "Refactor X" → "Ensure tests pass before and after" - -For multi-step tasks, state a brief plan: - -``` -1. [Step] → verify: [check] -2. [Step] → verify: [check] -3. [Step] → verify: [check] -``` - -Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. - -## Workflow - -1. Inspect nearby implementation, tests, and pattern docs before editing. -2. Prefer existing abstractions and conventions over introducing new ones. -3. For ad hoc runnable code, create a temporary file in `scratchpad/`, run it with `node scratchpad/.ts`, and delete it when done. - The local runtime is Node 24, which can run TypeScript files directly; use plain `node` for local TypeScript probes instead of `tsx` unless `node` fails. -4. Run the validation appropriate to the change type. -5. Report which validation commands were run and any commands that could not be run. - -## Validation - -Use the narrowest validation that still covers the change: - -| Change type | Validation | -| -------------------------------- | ---------------------------------------------------------------------------------- | -| Code changes | `pnpm lint-fix`, targeted `pnpm test `, `pnpm check` | -| Tests-only changes | `pnpm lint-fix`, targeted `pnpm test `, `pnpm check` | -| Type-level/API type changes | Targeted `pnpm test-types `, plus `pnpm check` when source types changed | -| JSDoc text/category/link changes | `pnpm lint` | -| JSDoc example changes | `pnpm lint`; from the changed package directory, run `pnpm docgen` | -| Docs-only changes | `pnpm lint-fix`; no tests required unless examples or code changed | - -## Bundle Size Preview - -When asked to show bundle-size impact for a commit, use the existing bundle comparison workflow: - -1. For the latest commit, run `pnpm bundle-compare HEAD~1`. - For another base, run `pnpm bundle-compare `. -2. Read the Markdown report from `tmp/bundle-stats.txt` and summarize the non-zero differences. -3. Leave `tmp/bundle-base` in place unless cleanup is requested. To clean it up, run `git worktree remove --force tmp/bundle-base`. - -## Coding Patterns - -Read `.patterns/effect.md` before changing Effect code. In particular: - -- Prefer `Effect.fnUntraced` over functions that only return `Effect.gen`. -- Prefer class syntax for `Context.Service`. -- Do not use `async` / `await` or `try` / `catch`; use Effect APIs such as `Effect.gen`, `Effect.fnUntraced`, and `Effect.tryPromise`. -- Do not use `Date.now` or `new Date`; use `Clock`, and use `TestClock` in tests. - -## Testing - -Read `.patterns/testing.md` before writing or changing tests. - -- Test files are located in `packages/*/test/`. -- Main Effect library tests are in `packages/effect/test/`. -- Use `it.effect` for Effect-returning tests. -- Use regular `it` for pure synchronous tests. -- Do not use `Effect.runSync` in tests. -- Do not use `expect` from Vitest; use `assert` from `@effect/vitest`. -- Type-level tests are in `packages/*/typetest/` and run with `pnpm test-types `. - -## Documentation - -- For AI documentation, read `ai-docs/README.md` very carefully before writing examples. -- AI documentation changes may include explanatory comments when useful. -- For public JSDoc `@category` guidance, read `.patterns/jsdoc.md`. -- When JSDoc examples are localized to a single package, run `pnpm docgen` from that package directory instead of the repository root. - -## Generated Files - -Do not hand-edit generated files. Run the appropriate generator instead. - -- `index.ts` barrel files are generated; run `pnpm codegen` after adding or removing modules. - -## Changesets - -Create a changeset in `.changeset/` for runtime behavior changes or exported type/API changes: - -```md ---- -"package-name": patch/minor/major ---- - -A description of the change. -``` - -Tests-only changes, internal refactors, docs-only changes, and JSDoc-only maintenance may skip changesets by maintainer decision. diff --git a/.context/effect/LLMS.md b/.context/effect/LLMS.md index 2aac305b0..cce10204b 100644 --- a/.context/effect/LLMS.md +++ b/.context/effect/LLMS.md @@ -1,12 +1,10 @@ # Effect library documentation -This documentation resides in the Effect monorepo, which contains the source -code for the Effect library and its related packages. +This documentation covers the Effect library and its related packages. -When you need to find any information about the Effect library, only use this -documentation and the source code found in `./packages`. Do not use -`node_modules` or any other external documentation, as it may be outdated or -incorrect. +When you need to find information about Effect, use this documentation and the +Effect source code available in your environment. Avoid unrelated copies of +Effect or external documentation, as they may be outdated or incorrect. **Note**: The examples in this documentation contain comments for illustration purposes. In practice, you would not include these comments in your code. @@ -42,8 +40,8 @@ Effect.gen(function*() { }) ) -// Use Schema.TaggedErrorClass to define a custom error -export class FileProcessingError extends Schema.TaggedErrorClass()("FileProcessingError", { +// Use Schema.TaggedError to define a custom error +export class FileProcessingError extends Schema.TaggedError()("FileProcessingError", { message: Schema.String }) {} ``` @@ -82,8 +80,8 @@ export const effectFunction = Effect.fn("effectFunction")( }) ) -// Use Schema.TaggedErrorClass to define a custom error -export class SomeError extends Schema.TaggedErrorClass()("SomeError", { +// Use Schema.TaggedError to define a custom error +export class SomeError extends Schema.TaggedError()("SomeError", { message: Schema.String }) {} ``` @@ -100,7 +98,7 @@ All validation and domain modeling in Effect is done with `Schema`. **AVOID using predicates or manual parsing**, instead use `Schema` to parse untrusted data and validate it. -For a comprehensive guide, see [packages/effect/SCHEMA.md](./packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. +For a comprehensive guide, see [SCHEMA.md](https://github.com/Effect-TS/effect/blob/main/packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. - **[Schema basics](./ai-docs/src/01_effect/02_schema/10_schema-basics.ts)**: Define `Schema.Class`s, decode unknown input into typed values, and @@ -150,7 +148,7 @@ export class Database extends Context.Service()("DatabaseError", { +export class DatabaseError extends Schema.TaggedError()("DatabaseError", { cause: Schema.Defect() }) {} @@ -175,14 +173,14 @@ Defining custom errors and handling them with Effect.catch and Effect.catchTag. ```ts import { Effect, Schema } from "effect" -// Define custom errors using Schema.TaggedErrorClass -export class ParseError extends Schema.TaggedErrorClass()("ParseError", { +// Define custom errors using Schema.TaggedError +export class ParseError extends Schema.TaggedError()("ParseError", { input: Schema.String, message: Schema.String }) {} -export class ReservedPortError extends Schema.TaggedErrorClass()("ReservedPortError", { - port: Schema.Number +export class ReservedPortError extends Schema.TaggedError()("ReservedPortError", { + port: Schema.Int }) {} declare const loadPort: (input: string) => Effect.Effect diff --git a/.context/effect/README.md b/.context/effect/README.md index efdb0b12f..63779c389 100644 --- a/.context/effect/README.md +++ b/.context/effect/README.md @@ -4,7 +4,7 @@ # Effect -Effect is a library for building robust, maintainable, type-safe, and production grade applications in TypeScript. +Effect is a library for building robust, maintainable, type-safe, and production grade applications in TypeScript. It helps you handle the hard problems at scale: typed errors, dependency injection, structured concurrency, scheduling, tracing, and unified schema validation. > **Effect V4 is currently in beta.** The `main` branch contains v4 development. @@ -24,6 +24,43 @@ npm install effect@latest Issues and pull requests meant for Effect v3 should target the [`v3`](https://github.com/Effect-TS/effect/tree/v3) branch. +## Packages + +This monorepo contains the core `effect` package alongside integration packages that extend it. All v4 packages are published under the `beta` tag on npm. + +| Package | Description | API Reference | +| --------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------ | +| [`effect`](packages/effect) | The core package | [docs](https://effect.website/docs/v4/api/effect) | +| [`@effect/platform-browser`](packages/platform/browser) | Platform services for the browser | [docs](https://effect.website/docs/v4/api/platform-browser) | +| [`@effect/platform-bun`](packages/platform/bun) | Platform services for [Bun](https://bun.sh) | [docs](https://effect.website/docs/v4/api/platform-bun) | +| [`@effect/platform-deno`](packages/platform/deno) | Platform services for [Deno](https://deno.com) | [docs](https://effect.website/docs/v4/api/platform-deno) | +| [`@effect/platform-node`](packages/platform/node) | Platform services for [Node.js](https://nodejs.org) | [docs](https://effect.website/docs/v4/api/platform-node) | +| [`@effect/platform-node-shared`](packages/platform/node-shared) | Shared services for Node.js-compatible runtimes | [docs](https://effect.website/docs/v4/api/platform-node-shared) | +| [`@effect/sql-clickhouse`](packages/sql/clickhouse) | SQL client for [ClickHouse](https://clickhouse.com) | [docs](https://effect.website/docs/v4/api/sql-clickhouse) | +| [`@effect/sql-d1`](packages/sql/d1) | SQL client for Cloudflare D1 | [docs](https://effect.website/docs/v4/api/sql-d1) | +| [`@effect/sql-libsql`](packages/sql/libsql) | SQL client for libSQL | [docs](https://effect.website/docs/v4/api/sql-libsql) | +| [`@effect/sql-mssql`](packages/sql/mssql) | SQL client for Microsoft SQL Server | [docs](https://effect.website/docs/v4/api/sql-mssql) | +| [`@effect/sql-mysql2`](packages/sql/mysql2) | SQL client for MySQL | [docs](https://effect.website/docs/v4/api/sql-mysql2) | +| [`@effect/sql-pg`](packages/sql/pg) | SQL client for PostgreSQL | [docs](https://effect.website/docs/v4/api/sql-pg) | +| [`@effect/sql-pglite`](packages/sql/pglite) | SQL client for [PGlite](https://pglite.dev) | [docs](https://effect.website/docs/v4/api/sql-pglite) | +| [`@effect/sql-sqlite-bun`](packages/sql/sqlite-bun) | SQL client for SQLite via `bun:sqlite` | [docs](https://effect.website/docs/v4/api/sql-sqlite-bun) | +| [`@effect/sql-sqlite-do`](packages/sql/sqlite-do) | SQL client for Cloudflare Durable Objects SQLite | [docs](https://effect.website/docs/v4/api/sql-sqlite-do) | +| [`@effect/sql-sqlite-node`](packages/sql/sqlite-node) | SQL client for SQLite via `node:sqlite` | [docs](https://effect.website/docs/v4/api/sql-sqlite-node) | +| [`@effect/sql-sqlite-react-native`](packages/sql/sqlite-react-native) | SQL client for SQLite in React Native | [docs](https://effect.website/docs/v4/api/sql-sqlite-react-native) | +| [`@effect/sql-sqlite-wasm`](packages/sql/sqlite-wasm) | SQL client for SQLite compiled to WebAssembly | [docs](https://effect.website/docs/v4/api/sql-sqlite-wasm) | +| [`@effect/ai-anthropic`](packages/ai/anthropic) | Anthropic provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-anthropic) | +| [`@effect/ai-openai`](packages/ai/openai) | OpenAI provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openai) | +| [`@effect/ai-openai-compat`](packages/ai/openai-compat) | OpenAI-compatible API provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openai-compat) | +| [`@effect/ai-openrouter`](packages/ai/openrouter) | OpenRouter provider for the Effect AI modules | [docs](https://effect.website/docs/v4/api/ai-openrouter) | +| [`@effect/atom-react`](packages/atom/react) | React bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-react) | +| [`@effect/atom-solid`](packages/atom/solid) | SolidJS bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-solid) | +| [`@effect/atom-vue`](packages/atom/vue) | Vue bindings for Effect Atom | [docs](https://effect.website/docs/v4/api/atom-vue) | +| [`@effect/opentelemetry`](packages/opentelemetry) | [OpenTelemetry](https://opentelemetry.io) integration | [docs](https://effect.website/docs/v4/api/opentelemetry) | +| [`@effect/vitest`](packages/vitest) | Helpers for testing with [Vitest](https://vitest.dev) | [docs](https://effect.website/docs/v4/api/vitest) | +| [`@effect/docgen`](packages/tools/docgen) | Documentation generator for Effect projects | [docs](https://effect.website/docs/v4/api/docgen) | +| [`@effect/doctest`](packages/tools/doctest) | Runs JSDoc examples as Vitest tests | [docs](https://effect.website/docs/v4/api/doctest) | +| [`@effect/openapi-generator`](packages/tools/openapi-generator) | Generate Effect code from OpenAPI specifications | [docs](https://effect.website/docs/v4/api/openapi-generator) | + ## Resources - Documentation (https://effect.website) diff --git a/.context/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts b/.context/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts index 4e45a8935..d82288953 100644 --- a/.context/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +++ b/.context/effect/ai-docs/src/01_effect/01_basics/01_effect-gen.ts @@ -24,7 +24,7 @@ Effect.gen(function*() { }) ) -// Use Schema.TaggedErrorClass to define a custom error -export class FileProcessingError extends Schema.TaggedErrorClass()("FileProcessingError", { +// Use Schema.TaggedError to define a custom error +export class FileProcessingError extends Schema.TaggedError()("FileProcessingError", { message: Schema.String }) {} diff --git a/.context/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts b/.context/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts index dcbfe6d62..1d41aaa15 100644 --- a/.context/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +++ b/.context/effect/ai-docs/src/01_effect/01_basics/02_effect-fn.ts @@ -33,7 +33,7 @@ export const effectFunction = Effect.fn("effectFunction")( }) ) -// Use Schema.TaggedErrorClass to define a custom error -export class SomeError extends Schema.TaggedErrorClass()("SomeError", { +// Use Schema.TaggedError to define a custom error +export class SomeError extends Schema.TaggedError()("SomeError", { message: Schema.String }) {} diff --git a/.context/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts b/.context/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts index 86082db0f..7f331047f 100644 --- a/.context/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +++ b/.context/effect/ai-docs/src/01_effect/01_basics/10_creating-effects.ts @@ -6,17 +6,17 @@ */ import { Effect, Schema } from "effect" -class InvalidPayload extends Schema.TaggedErrorClass()("InvalidPayload", { +class InvalidPayload extends Schema.TaggedError()("InvalidPayload", { input: Schema.String, cause: Schema.Defect() }) {} -class UserLookupError extends Schema.TaggedErrorClass()("UserLookupError", { - userId: Schema.Number, +class UserLookupError extends Schema.TaggedError()("UserLookupError", { + userId: Schema.Int, cause: Schema.Defect() }) {} -class MissingWorkspaceId extends Schema.TaggedErrorClass()("MissingWorkspaceId", {}) {} +class MissingWorkspaceId extends Schema.TaggedError()("MissingWorkspaceId", {}) {} // Some request fields are optional and may be absent. const requestHeaders = new Map([ diff --git a/.context/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts b/.context/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts index 35b6c5c08..99155a590 100644 --- a/.context/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +++ b/.context/effect/ai-docs/src/01_effect/02_schema/10_schema-basics.ts @@ -13,7 +13,7 @@ import { Effect, Schema } from "effect" // The static `Type` and `Encoded` members are available when you need // the decoded or encoded TypeScript representation. export class User extends Schema.Class("path/to/module/User")({ - id: Schema.Number, + id: Schema.Int, name: Schema.NonEmptyString, email: Schema.String, role: Schema.Literals(["admin", "member"]) @@ -32,7 +32,7 @@ export type UserEncoded = typeof User["Encoded"] export const decodeUser = Schema.decodeUnknownEffect(User) export const encodeUser = Schema.encodeEffect(User) -export class InvalidUserPayload extends Schema.TaggedErrorClass()("InvalidUserPayload", { +export class InvalidUserPayload extends Schema.TaggedError()("InvalidUserPayload", { message: Schema.String }) {} diff --git a/.context/effect/ai-docs/src/01_effect/02_schema/index.md b/.context/effect/ai-docs/src/01_effect/02_schema/index.md index 7819e4fac..2e93dea9a 100644 --- a/.context/effect/ai-docs/src/01_effect/02_schema/index.md +++ b/.context/effect/ai-docs/src/01_effect/02_schema/index.md @@ -4,4 +4,4 @@ All validation and domain modeling in Effect is done with `Schema`. **AVOID using predicates or manual parsing**, instead use `Schema` to parse untrusted data and validate it. -For a comprehensive guide, see [packages/effect/SCHEMA.md](./packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. +For a comprehensive guide, see [SCHEMA.md](https://github.com/Effect-TS/effect/blob/main/packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document. diff --git a/.context/effect/ai-docs/src/01_effect/03_services/01_service.ts b/.context/effect/ai-docs/src/01_effect/03_services/01_service.ts index 24db9b0b4..14478cf6d 100644 --- a/.context/effect/ai-docs/src/01_effect/03_services/01_service.ts +++ b/.context/effect/ai-docs/src/01_effect/03_services/01_service.ts @@ -37,7 +37,7 @@ export class Database extends Context.Service()("DatabaseError", { +export class DatabaseError extends Schema.TaggedError()("DatabaseError", { cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts b/.context/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts index 58ff1273c..99bff184f 100644 --- a/.context/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts +++ b/.context/effect/ai-docs/src/01_effect/03_services/20_layer-composition.ts @@ -17,7 +17,7 @@ export const SqlClientLayer: Layer.Layer< url: Config.redacted("DATABASE_URL") }) -export class UserRespositoryError extends Schema.TaggedErrorClass()("UserRespositoryError", { +export class UserRespositoryError extends Schema.TaggedError()("UserRespositoryError", { reason: SqlError.SqlError }) {} diff --git a/.context/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts b/.context/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts index 01324ac0d..75a8488ec 100644 --- a/.context/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +++ b/.context/effect/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts @@ -5,7 +5,7 @@ */ import { Config, Context, Effect, Layer, Schema } from "effect" -export class MessageStoreError extends Schema.TaggedErrorClass()("MessageStoreError", { +export class MessageStoreError extends Schema.TaggedError()("MessageStoreError", { cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts b/.context/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts index c50711887..b4a7bfb9e 100644 --- a/.context/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts +++ b/.context/effect/ai-docs/src/01_effect/04_errors/01_error-handling.ts @@ -5,14 +5,14 @@ */ import { Effect, Schema } from "effect" -// Define custom errors using Schema.TaggedErrorClass -export class ParseError extends Schema.TaggedErrorClass()("ParseError", { +// Define custom errors using Schema.TaggedError +export class ParseError extends Schema.TaggedError()("ParseError", { input: Schema.String, message: Schema.String }) {} -export class ReservedPortError extends Schema.TaggedErrorClass()("ReservedPortError", { - port: Schema.Number +export class ReservedPortError extends Schema.TaggedError()("ReservedPortError", { + port: Schema.Int }) {} declare const loadPort: (input: string) => Effect.Effect diff --git a/.context/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts b/.context/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts index 020473dd4..88d14d47a 100644 --- a/.context/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +++ b/.context/effect/ai-docs/src/01_effect/04_errors/10_catch-tags.ts @@ -6,12 +6,12 @@ import { Effect, Schema } from "effect" -export class ValidationError extends Schema.TaggedErrorClass()("ValidationError", { +export class ValidationError extends Schema.TaggedError()("ValidationError", { message: Schema.String }) {} -export class NetworkError extends Schema.TaggedErrorClass()("NetworkError", { - statusCode: Schema.Number +export class NetworkError extends Schema.TaggedError()("NetworkError", { + statusCode: Schema.Int }) {} declare const fetchUser: (id: string) => Effect.Effect diff --git a/.context/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts b/.context/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts index bced427c1..504dbdd9f 100644 --- a/.context/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +++ b/.context/effect/ai-docs/src/01_effect/04_errors/20_reason-errors.ts @@ -8,19 +8,19 @@ import { Effect, Schema } from "effect" -export class RateLimitError extends Schema.TaggedErrorClass()("RateLimitError", { - retryAfter: Schema.Number +export class RateLimitError extends Schema.TaggedError()("RateLimitError", { + retryAfter: Schema.Finite }) {} -export class QuotaExceededError extends Schema.TaggedErrorClass()("QuotaExceededError", { - limit: Schema.Number +export class QuotaExceededError extends Schema.TaggedError()("QuotaExceededError", { + limit: Schema.Int }) {} -export class SafetyBlockedError extends Schema.TaggedErrorClass()("SafetyBlockedError", { +export class SafetyBlockedError extends Schema.TaggedError()("SafetyBlockedError", { category: Schema.String }) {} -export class AiError extends Schema.TaggedErrorClass()("AiError", { +export class AiError extends Schema.TaggedError()("AiError", { reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError]) }) {} diff --git a/.context/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts b/.context/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts index afff2b1cb..4adb090c7 100644 --- a/.context/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +++ b/.context/effect/ai-docs/src/01_effect/05_resources/10_acquire-release.ts @@ -8,7 +8,7 @@ import { Config, Context, Effect, Layer, Redacted, Schema } from "effect" import * as NodeMailer from "nodemailer" -export class SmtpError extends Schema.ErrorClass("SmtpError")({ +export class SmtpError extends Schema.Error("SmtpError")({ cause: Schema.Defect() }) {} @@ -70,7 +70,7 @@ export class Smtp extends Context.Service()("MailerError", { +export class MailerError extends Schema.TaggedError()("MailerError", { reason: SmtpError }) {} diff --git a/.context/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts b/.context/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts index 4dbecb381..5ba15c5fb 100644 --- a/.context/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts +++ b/.context/effect/ai-docs/src/01_effect/05_resources/30_layer-map.ts @@ -6,7 +6,7 @@ */ import { Context, Effect, Layer, LayerMap, Schema } from "effect" -class DatabaseQueryError extends Schema.TaggedErrorClass()("DatabaseQueryError", { +class DatabaseQueryError extends Schema.TaggedError()("DatabaseQueryError", { tenantId: Schema.String, cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/03_stream/10_creating-streams.ts b/.context/effect/ai-docs/src/03_stream/10_creating-streams.ts index 272b338a8..262a1f196 100644 --- a/.context/effect/ai-docs/src/03_stream/10_creating-streams.ts +++ b/.context/effect/ai-docs/src/03_stream/10_creating-streams.ts @@ -49,7 +49,7 @@ export const fetchJobsPage = Stream.paginate( }) ) -class LetterError extends Schema.TaggedErrorClass()("LetterError", { +class LetterError extends Schema.TaggedError()("LetterError", { cause: Schema.Defect() }) {} @@ -88,7 +88,7 @@ export const callbackStream = Stream.callback(Effect.fn(function*( ) })) -export class NodeStreamError extends Schema.TaggedErrorClass()("NodeStreamError", { +export class NodeStreamError extends Schema.TaggedError()("NodeStreamError", { cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/03_stream/30_encoding.ts b/.context/effect/ai-docs/src/03_stream/30_encoding.ts index 16a6b6aa7..c0577c851 100644 --- a/.context/effect/ai-docs/src/03_stream/30_encoding.ts +++ b/.context/effect/ai-docs/src/03_stream/30_encoding.ts @@ -11,7 +11,7 @@ import { Msgpack, Ndjson } from "effect/unstable/encoding" // with `Msgpack` and using the appropriate channels (`Msgpack.decode()`, // `Msgpack.encode()`, etc.). export const msgpackDecoder = Msgpack.decodeSchema(Schema.Struct({ - id: Schema.Number, + id: Schema.Int, name: Schema.String })) diff --git a/.context/effect/ai-docs/src/04_integration/10_managed-runtime.ts b/.context/effect/ai-docs/src/04_integration/10_managed-runtime.ts index ed29f83b0..b7393a9f4 100644 --- a/.context/effect/ai-docs/src/04_integration/10_managed-runtime.ts +++ b/.context/effect/ai-docs/src/04_integration/10_managed-runtime.ts @@ -7,7 +7,7 @@ import { Context, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect" import { Hono } from "hono" class Todo extends Schema.Class("Todo")({ - id: Schema.Number, + id: Schema.Int, title: Schema.String, completed: Schema.Boolean }) {} @@ -16,8 +16,8 @@ class CreateTodoPayload extends Schema.Class("CreateTodoPaylo title: Schema.String }) {} -class TodoNotFound extends Schema.TaggedErrorClass()("TodoNotFound", { - id: Schema.Number +class TodoNotFound extends Schema.TaggedError()("TodoNotFound", { + id: Schema.Int }) {} export class TodoRepo extends Context.Service("User")({ - id: Schema.Number, + id: Schema.Int, name: Schema.String, email: Schema.String }) {} -export class UserNotFound extends Schema.TaggedErrorClass()("UserNotFound", { - id: Schema.Number +export class UserNotFound extends Schema.TaggedError()("UserNotFound", { + id: Schema.Int }) {} export class Users extends Context.Service()("HttpError", { +export class HttpError extends Schema.TaggedError()("HttpError", { message: Schema.String, - status: Schema.Number, + status: Schema.Int, retryable: Schema.Boolean }) {} diff --git a/.context/effect/ai-docs/src/50_http-client/10_basics.ts b/.context/effect/ai-docs/src/50_http-client/10_basics.ts index 747e886e1..195d5fe55 100644 --- a/.context/effect/ai-docs/src/50_http-client/10_basics.ts +++ b/.context/effect/ai-docs/src/50_http-client/10_basics.ts @@ -7,8 +7,8 @@ import { Context, Effect, flow, Layer, Schedule, Schema } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" class Todo extends Schema.Class("Todo")({ - userId: Schema.Number, - id: Schema.Number, + userId: Schema.Int, + id: Schema.Int, title: Schema.String, completed: Schema.Boolean }) {} @@ -97,6 +97,6 @@ export class JsonPlaceholder extends Context.Service()("JsonPlaceholderError", { +export class JsonPlaceholderError extends Schema.TaggedError()("JsonPlaceholderError", { cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts b/.context/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts index 8b25c0429..30898c729 100644 --- a/.context/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +++ b/.context/effect/ai-docs/src/51_http-server/fixtures/api/Authorization.ts @@ -4,7 +4,7 @@ import type { User } from "../domain/User.ts" export class CurrentUser extends Context.Service()("acme/HttpApi/Authorization/CurrentUser") {} -export class Unauthorized extends Schema.TaggedErrorClass()( +export class Unauthorized extends Schema.TaggedError()( "Unauthorized", { message: Schema.String diff --git a/.context/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts b/.context/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts index fa2a757b2..fde17e7fc 100644 --- a/.context/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +++ b/.context/effect/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts @@ -1,6 +1,6 @@ import { Schema } from "effect" -export class UserNotFound extends Schema.TaggedErrorClass()( +export class UserNotFound extends Schema.TaggedError()( "UserNotFound", {}, // You can specify the status code for this error inline @@ -8,7 +8,7 @@ export class UserNotFound extends Schema.TaggedErrorClass()( ) {} export class SearchQueryTooShort - extends Schema.TaggedErrorClass()("SearchQueryTooShort", {}, { httpApiStatus: 422 }) + extends Schema.TaggedError()("SearchQueryTooShort", {}, { httpApiStatus: 422 }) { static readonly minimumLength = 2 } @@ -17,6 +17,6 @@ export class SearchQueryTooShort // // This prevents adding too many error types to services / endpoint definitions. // -export class UsersError extends Schema.TaggedErrorClass()("UsersError", { +export class UsersError extends Schema.TaggedError()("UsersError", { reason: Schema.Union([UserNotFound, SearchQueryTooShort]) }) {} diff --git a/.context/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts b/.context/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts index a1b7b3eac..5d778f69a 100644 --- a/.context/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +++ b/.context/effect/ai-docs/src/51_http-server/fixtures/server/Users/http.ts @@ -23,7 +23,7 @@ export const UsersApiHandlers = HttpApiBuilder.group( Effect.fn(function*({ payload }) { if (payload.search === "bad-request") { // You can use the built in error types like any other - // Schema.TaggedErrorClass + // Schema.TaggedError return yield* new HttpApiError.RequestTimeout() } return yield* users.list(payload.search).pipe( diff --git a/.context/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts b/.context/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts index 3d2866af9..c7b1bbbcd 100644 --- a/.context/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts +++ b/.context/effect/ai-docs/src/60_child-process/10_working-with-child-processes.ts @@ -7,7 +7,7 @@ import { NodeServices } from "@effect/platform-node" import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -export class DevToolsError extends Schema.TaggedErrorClass()("DevToolsError", { +export class DevToolsError extends Schema.TaggedError()("DevToolsError", { cause: Schema.Defect() }) {} diff --git a/.context/effect/ai-docs/src/71_ai/10_language-model.ts b/.context/effect/ai-docs/src/71_ai/10_language-model.ts index 2f89956bb..68e959426 100644 --- a/.context/effect/ai-docs/src/71_ai/10_language-model.ts +++ b/.context/effect/ai-docs/src/71_ai/10_language-model.ts @@ -26,7 +26,7 @@ const OpenAiClientLayer = OpenAiClient.layerConfig({ Layer.provide(FetchHttpClient.layer) ) -export class AiWriterError extends Schema.TaggedErrorClass()("AiWriterError", { +export class AiWriterError extends Schema.TaggedError()("AiWriterError", { // AiErrorReason is a Schema, so we can include it directly in our custom // error schema. reason: AiError.AiErrorReason diff --git a/.context/effect/ai-docs/src/71_ai/20_tools.ts b/.context/effect/ai-docs/src/71_ai/20_tools.ts index 0acd8e97c..599178368 100644 --- a/.context/effect/ai-docs/src/71_ai/20_tools.ts +++ b/.context/effect/ai-docs/src/71_ai/20_tools.ts @@ -20,7 +20,7 @@ const ProductId = Schema.String.pipe(Schema.brand("ProductId")).annotate({ class Product extends Schema.Class("acme/domain/Product")({ id: ProductId, name: Schema.String, - price: Schema.Number + price: Schema.Finite }) {} // Each tool has a name, an optional description, a parameters schema that the @@ -34,7 +34,7 @@ const SearchProducts = Tool.make("SearchProducts", { // guidance. description: "The search query, e.g. 'wireless headphones'" }), - maxResults: Schema.Number.pipe(Schema.withDecodingDefault(Effect.succeed(10))).annotate({ + maxResults: Schema.Natural.pipe(Schema.withDecodingDefault(Effect.succeed(10))).annotate({ description: "The maximum number of results to return" }) }), @@ -57,7 +57,7 @@ const GetInventory = Tool.make("GetInventory", { }), success: Schema.Struct({ productId: ProductId, - available: Schema.Number + available: Schema.Natural }) }) @@ -104,7 +104,7 @@ const OpenAiClientLayer = OpenAiClient.layerConfig({ apiKey: Config.redacted("OPENAI_API_KEY") }).pipe(Layer.provide(FetchHttpClient.layer)) -export class ProductAssistantError extends Schema.TaggedErrorClass()( +export class ProductAssistantError extends Schema.TaggedError()( "ProductAssistantError", { reason: AiError.AiErrorReason } ) {} diff --git a/.context/effect/ai-docs/src/71_ai/30_chat.ts b/.context/effect/ai-docs/src/71_ai/30_chat.ts index 600017024..f90ea1af6 100644 --- a/.context/effect/ai-docs/src/71_ai/30_chat.ts +++ b/.context/effect/ai-docs/src/71_ai/30_chat.ts @@ -43,7 +43,7 @@ const ToolsLayer = Tools.toLayer(Effect.gen(function*() { // Service that wraps Chat for a domain use-case // --------------------------------------------------------------------------- -export class AiAssistantError extends Schema.TaggedErrorClass()("AiAssistantError", { +export class AiAssistantError extends Schema.TaggedError()("AiAssistantError", { reason: AiError.AiErrorReason }) { static fromAiError(error: AiError.AiError) { diff --git a/.context/effect/ai-docs/src/80_cluster/10_entities.ts b/.context/effect/ai-docs/src/80_cluster/10_entities.ts index a1be374a5..466197f9b 100644 --- a/.context/effect/ai-docs/src/80_cluster/10_entities.ts +++ b/.context/effect/ai-docs/src/80_cluster/10_entities.ts @@ -10,12 +10,12 @@ import { Rpc } from "effect/unstable/rpc" import type { SqlClient } from "effect/unstable/sql" export const Increment = Rpc.make("Increment", { - payload: { amount: Schema.Number }, - success: Schema.Number + payload: { amount: Schema.Int }, + success: Schema.Int }) export const GetCount = Rpc.make("GetCount", { - success: Schema.Number + success: Schema.Int }) // If you want GetCount messages to be persisted, you can annotate the RPC // schema with `ClusterSchema.Persisted`. diff --git a/.context/effect/ai-docs/src/index.md b/.context/effect/ai-docs/src/index.md index 092209d75..0a0213bf7 100644 --- a/.context/effect/ai-docs/src/index.md +++ b/.context/effect/ai-docs/src/index.md @@ -1,12 +1,10 @@ # Effect library documentation -This documentation resides in the Effect monorepo, which contains the source -code for the Effect library and its related packages. +This documentation covers the Effect library and its related packages. -When you need to find any information about the Effect library, only use this -documentation and the source code found in `./packages`. Do not use -`node_modules` or any other external documentation, as it may be outdated or -incorrect. +When you need to find information about Effect, use this documentation and the +Effect source code available in your environment. Avoid unrelated copies of +Effect or external documentation, as they may be outdated or incorrect. **Note**: The examples in this documentation contain comments for illustration purposes. In practice, you would not include these comments in your code. diff --git a/.context/effect/ai-docs/tsconfig.json b/.context/effect/ai-docs/tsconfig.json index 94d60356d..e5be14e9b 100644 --- a/.context/effect/ai-docs/tsconfig.json +++ b/.context/effect/ai-docs/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../tsconfig.base.json", "include": ["src/**/*.ts", "src/**/*.tsx"], "compilerOptions": { diff --git a/.context/effect/cookbooks/schedule.md b/.context/effect/cookbooks/schedule.md index 999118153..2ff56da20 100644 --- a/.context/effect/cookbooks/schedule.md +++ b/.context/effect/cookbooks/schedule.md @@ -31,8 +31,8 @@ This cookbook intentionally defines schedules only. It does not apply them with | Poll latest status | `Schedule.spaced` or `Schedule.fixed`, then `Schedule.setInputType`, `Schedule.passthrough`, and `Schedule.while` | | Adapt delay from metadata | `Schedule.addDelay` | | Replace or cap selected delay | `Schedule.modifyDelay` | -| Run phases in sequence | `Schedule.andThen` | -| Preserve phase in output | `Schedule.andThenResult` | +| Run phases in sequence | `Schedule.concat` | +| Preserve phase in output | `Schedule.concatResult` | | Continue while all policies continue | `Schedule.max` | | Continue while any policy continues | `Schedule.min` | | Shape output from metadata | `Schedule.map` | @@ -256,7 +256,7 @@ import { Schedule } from "effect" const cacheInvalidationSequence = Schedule.spaced("100 millis").pipe( Schedule.upTo({ times: 2 }), - Schedule.andThen(Schedule.spaced("30 seconds").pipe(Schedule.upTo({ times: 3 }))) + Schedule.concat(Schedule.spaced("30 seconds").pipe(Schedule.upTo({ times: 3 }))) ) ``` @@ -270,7 +270,7 @@ import { Result, Schedule } from "effect" const phasedRetryClassifier = Schedule.exponential("100 millis").pipe( Schedule.upTo({ times: 2 }), - Schedule.andThenResult(Schedule.fibonacci("500 millis").pipe(Schedule.upTo({ times: 3 }))), + Schedule.concatResult(Schedule.fibonacci("500 millis").pipe(Schedule.upTo({ times: 3 }))), Schedule.map(({ output: result }) => Result.match(result, { onFailure: (delay) => ({ phase: "fast", delay }), @@ -280,7 +280,7 @@ const phasedRetryClassifier = Schedule.exponential("100 millis").pipe( ) ``` -Explanation: `Schedule.andThenResult` keeps phase information in the output. +Explanation: `Schedule.concatResult` keeps phase information in the output. The first schedule is represented by the failure side, and the second schedule is represented by the success side. @@ -543,8 +543,8 @@ import { Schedule } from "effect" const incidentEscalationCadence = Schedule.spaced("1 minute").pipe( Schedule.upTo({ times: 3 }), - Schedule.andThen(Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 }))), - Schedule.andThen(Schedule.fixed("15 minutes")) + Schedule.concat(Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 }))), + Schedule.concat(Schedule.fixed("15 minutes")) ) ``` @@ -558,6 +558,6 @@ about 30 seconds, then switches to a cron schedule that recurs every day at import { Schedule } from "effect" const maintenanceCronAfterWarmup = Schedule.duration("30 seconds").pipe( - Schedule.andThen(Schedule.cron("0 3 * * *")) + Schedule.concat(Schedule.cron("0 3 * * *")) ) ``` diff --git a/.context/effect/deno.json b/.context/effect/deno.json index 835202d67..38de0651a 100644 --- a/.context/effect/deno.json +++ b/.context/effect/deno.json @@ -2,7 +2,7 @@ "$schema": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json", "nodeModulesDir": "manual", "unstable": ["bare-node-builtins", "node-globals"], - "workspace": ["./packages/*"], + "workspace": ["./packages/*", "./packages/platform/*"], "exclude": [ "**/*.mjs", "**/*.cjs", @@ -19,15 +19,16 @@ "scratchpad/", "packages/*/typetest/", "packages/*/typeperf/", + "packages/*/runtimeperf/", "packages/*/benchmark/", "packages/ai", "packages/atom", "packages/effect/test/cluster/", "packages/opentelemetry/", - "packages/platform-browser/", - "packages/platform-bun/", - "packages/platform-node/", - "packages/platform-node-shared/", + "packages/platform/browser/", + "packages/platform/bun/", + "packages/platform/node/", + "packages/platform/node-shared/", "packages/tools/", "packages/sql" ] diff --git a/.context/effect/flake.nix b/.context/effect/flake.nix index 7685bee4d..c9c8616e1 100644 --- a/.context/effect/flake.nix +++ b/.context/effect/flake.nix @@ -14,7 +14,7 @@ packages = with pkgs; [ bun deno - corepack + (corepack.override {nodejs-slim = nodejs-slim_26;}) nodejs_26 python3 ]; diff --git a/.context/effect/migration/annotations/README.md b/.context/effect/migration/annotations/README.md new file mode 100644 index 000000000..d78e65871 --- /dev/null +++ b/.context/effect/migration/annotations/README.md @@ -0,0 +1,14 @@ +# Migration annotations + +Add one YAML file per v3 module. Each file maps stable API ids (without the +snapshot's trailing `#type` or `#value` facet) to migration guidance: + +```yaml +effect/Effect#async: + replacement: Effect.callback + note: Use the callback constructor. + example: Effect.callback((resume) => resume(Effect.void)) +``` + +Run `pnpm api-diff --check` to list missing ids and +`pnpm api-diff --write-doc migration/v3-to-v4.md` to regenerate the reference. diff --git a/.context/effect/migration/annotations/effect__Arbitrary.yaml b/.context/effect/migration/annotations/effect__Arbitrary.yaml new file mode 100644 index 000000000..8410a4a18 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Arbitrary.yaml @@ -0,0 +1,17 @@ +"effect/Arbitrary#ArbitraryAnnotation": + replacement: "Schema.Annotations.ToArbitrary.Declaration" + note: "Arbitrary derivation annotations now live in Schema.Annotations and use the toArbitrary key." +"effect/Arbitrary#ArbitraryGenerationContext": + replacement: "Schema.Annotations.ToArbitrary.Context" + note: "Use the v4 arbitrary-derivation context type from Schema.Annotations." +"effect/Arbitrary#LazyArbitrary": + replacement: "Schema.Arbitrary" + note: "The arbitrary factory type moved onto Schema." +"effect/Arbitrary#make": + replacement: "Schema.toArbitrary" + note: "Arbitrary derivation is now exposed directly by Schema." + example: "Schema.toArbitrary(schema)(FastCheck)" +"effect/Arbitrary#makeLazy": + replacement: "Schema.toArbitrary" + note: "Lazy arbitrary derivation is now exposed directly by Schema." + example: "Schema.toArbitrary(schema)" diff --git a/.context/effect/migration/annotations/effect__Array.yaml b/.context/effect/migration/annotations/effect__Array.yaml new file mode 100644 index 000000000..930975b29 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Array.yaml @@ -0,0 +1,78 @@ +"effect/Array#filterMapWhile": + replacement: "Array.takeWhileFilter" + note: "Same map-until-first-miss behavior; change the callback from Option.some/none to Result.succeed/fail." +"effect/Array#flatMapNullable": + replacement: "Array.flatMapNullishOr" + note: "Direct nullish-terminology rename; null and undefined mapper results are still discarded." +"effect/Array#fromNullable": + replacement: "Array.fromNullishOr" + note: "Direct nullish-terminology rename; null and undefined become an empty array and other values become a singleton." +"effect/Array#getEquivalence": + replacement: "Array.makeEquivalence" + note: "Direct rename; pass the element Equivalence unchanged." +"effect/Array#getLefts": + replacement: "Array.getFailures" + note: "Either became Result; this extracts failure payloads in input order." +"effect/Array#getOrder": + replacement: "Array.makeOrder" + note: "Direct rename; pass the element Order unchanged." +"effect/Array#getRights": + replacement: "Array.getSuccesses" + note: "Either became Result; this extracts success payloads in input order." +"effect/Array#init": + replacement: "Array.init" + note: "The API and Option> behavior remain unchanged." +"effect/Array#isEmptyArray": + replacement: "Array.isArrayEmpty" + note: "Direct word-order rename; retains the mutable empty-array type guard." +"effect/Array#isEmptyReadonlyArray": + replacement: "Array.isReadonlyArrayEmpty" + note: "Direct word-order rename; retains the readonly empty-array type guard." +"effect/Array#isNonEmptyArray": + replacement: "Array.isArrayNonEmpty" + note: "Direct word-order rename; retains the mutable NonEmptyArray type guard." +"effect/Array#isNonEmptyReadonlyArray": + replacement: "Array.isReadonlyArrayNonEmpty" + note: "Direct word-order rename; retains the NonEmptyReadonlyArray type guard." +"effect/Array#liftEither": + replacement: "Array.liftResult" + note: "Either became Result; failures produce an empty array and successes produce a singleton." +"effect/Array#liftNullable": + replacement: "Array.liftNullishOr" + note: "Direct nullish-terminology rename; the lifted function still returns zero or one element." +"effect/Array#modifyNonEmptyHead": + replacement: "Array.modifyHeadNonEmpty" + note: "Direct word-order rename with the same non-empty-preserving result." +"effect/Array#modifyNonEmptyLast": + replacement: "Array.modifyLastNonEmpty" + note: "Direct word-order rename with the same non-empty-preserving result." +"effect/Array#modifyOption": + replacement: "Array.modify" + note: "The Option suffix was dropped; an out-of-bounds index still returns Option.none." +"effect/Array#partitionMap": + replacement: "Array.partition" + note: "Pass a Result-returning mapper instead of Either; the output remains [failures, successes], corresponding to v3 [lefts, rights]." +"effect/Array#ReadonlyArray": + replacement: "Array.ReadonlyArray" + note: "The namespace and its Infer, With, OrNonEmpty, AndNonEmpty, and Flatten utility types remain." +"effect/Array#removeOption": + replacement: "Array.remove" + note: "The closest API now returns an unchanged copy out of bounds; use Array.get before Array.remove to preserve the old Option result." +"effect/Array#replaceOption": + replacement: "Array.replace" + note: "The Option suffix was dropped; an out-of-bounds index still returns Option.none." +"effect/Array#setNonEmptyHead": + replacement: "Array.setHeadNonEmpty" + note: "Direct word-order rename with the same non-empty-preserving result." +"effect/Array#setNonEmptyLast": + replacement: "Array.setLastNonEmpty" + note: "Direct word-order rename with the same non-empty-preserving result." +"effect/Array#splitNonEmptyAt": + replacement: "Array.splitAtNonEmpty" + note: "Direct word-order rename; the left output remains guaranteed non-empty." +"effect/Array#tail": + replacement: "Array.tail" + note: "The API and Option> behavior remain unchanged." +"effect/Array#unsafeGet": + replacement: "Array.getUnsafe" + note: "Direct word-order rename; it still throws for an out-of-bounds index." diff --git a/.context/effect/migration/annotations/effect__BigDecimal.yaml b/.context/effect/migration/annotations/effect__BigDecimal.yaml new file mode 100644 index 000000000..19b9fe387 --- /dev/null +++ b/.context/effect/migration/annotations/effect__BigDecimal.yaml @@ -0,0 +1,36 @@ +"effect/BigDecimal#BigDecimal": + replacement: "BigDecimal.BigDecimal" + note: "The model interface remains, but its brand key is now internal." +"effect/BigDecimal#greaterThan": + replacement: "BigDecimal.isGreaterThan" + note: "Renamed with the v4 is-prefix." +"effect/BigDecimal#greaterThanOrEqualTo": + replacement: "BigDecimal.isGreaterThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/BigDecimal#lessThan": + replacement: "BigDecimal.isLessThan" + note: "Renamed with the v4 is-prefix." +"effect/BigDecimal#lessThanOrEqualTo": + replacement: "BigDecimal.isLessThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/BigDecimal#safeFromNumber": + replacement: "BigDecimal.fromNumber" + note: "Use the safe v4 constructor, which still returns Option." +"effect/BigDecimal#TypeId": + replacement: "none" + note: "The brand key is internal in v4; use BigDecimal.isBigDecimal for runtime narrowing." +"effect/BigDecimal#unsafeDivide": + replacement: "BigDecimal.divideUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." +"effect/BigDecimal#unsafeFromNumber": + replacement: "BigDecimal.fromNumberUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." +"effect/BigDecimal#unsafeFromString": + replacement: "BigDecimal.fromStringUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." +"effect/BigDecimal#unsafeRemainder": + replacement: "BigDecimal.remainderUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." +"effect/BigDecimal#unsafeToNumber": + replacement: "BigDecimal.toNumberUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." diff --git a/.context/effect/migration/annotations/effect__BigInt.yaml b/.context/effect/migration/annotations/effect__BigInt.yaml new file mode 100644 index 000000000..a0fc52841 --- /dev/null +++ b/.context/effect/migration/annotations/effect__BigInt.yaml @@ -0,0 +1,21 @@ +"effect/BigInt#fromNumber": + replacement: "BigInt.fromNumber" + note: "Unchanged; it returns Option for safe conversion." +"effect/BigInt#greaterThan": + replacement: "BigInt.isGreaterThan" + note: "Renamed with the v4 is-prefix." +"effect/BigInt#greaterThanOrEqualTo": + replacement: "BigInt.isGreaterThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/BigInt#lessThan": + replacement: "BigInt.isLessThan" + note: "Renamed with the v4 is-prefix." +"effect/BigInt#lessThanOrEqualTo": + replacement: "BigInt.isLessThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/BigInt#unsafeDivide": + replacement: "BigInt.divideUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." +"effect/BigInt#unsafeSqrt": + replacement: "BigInt.sqrtUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." diff --git a/.context/effect/migration/annotations/effect__Brand.yaml b/.context/effect/migration/annotations/effect__Brand.yaml new file mode 100644 index 000000000..9d07fa7aa --- /dev/null +++ b/.context/effect/migration/annotations/effect__Brand.yaml @@ -0,0 +1,33 @@ +"effect/Brand#all": + replacement: "Brand.all" + note: "Still exported; combines multiple brand constructors and checks." +"effect/Brand#Brand": + replacement: "Brand.Brand" + note: "Still exported, but v4 brand keys are strings rather than symbols." +"effect/Brand#Brand.BrandErrors": + replacement: "Brand.BrandError" + note: "Validation now returns one BrandError wrapping a SchemaIssue.Issue instead of an error array." +"effect/Brand#Brand.RefinementError": + replacement: "Schema.FilterIssue" + note: "Brand.make validators use Schema filter output instead of the old message and meta record." +"effect/Brand#Branded": + replacement: "Brand.Branded" + note: "Still exported, with the brand key restricted to string." +"effect/Brand#BrandTypeId": + replacement: "none" + note: "The public marker was removed; the v4 brand type id is private." +"effect/Brand#error": + replacement: "Brand.make" + note: "Return a string or Schema filter issue directly from a Brand.make validator." +"effect/Brand#nominal": + replacement: "Brand.nominal" + note: "Still exported; the constructor's either method is now result." +"effect/Brand#refined": + replacement: "Brand.make" + note: "Use Brand.make for custom validation or Brand.check for Schema checks." +"effect/Brand#RefinedConstructorsTypeId": + replacement: "none" + note: "The public refined-constructor marker was removed." +"effect/Brand#unbranded": + replacement: "Function.cast" + note: "Brands are runtime-identical to their base value; cast explicitly when an unbranded type is required." diff --git a/.context/effect/migration/annotations/effect__Cache.yaml b/.context/effect/migration/annotations/effect__Cache.yaml new file mode 100644 index 000000000..c696acbc9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Cache.yaml @@ -0,0 +1,33 @@ +effect/Cache#Cache: + replacement: "Cache.Cache" + note: "The cache model remains, but v4 exposes a Pipeable value with explicit Cache operations and adds a lookup environment parameter." +effect/Cache#Cache.ConsumerVariance: + replacement: "none" + note: "The ConsumerCache view and its variance marker were removed; expose a narrower application interface around Cache operations when write access must be hidden." +effect/Cache#Cache.Variance: + replacement: "none" + note: "The public variance marker was removed; use Cache.Cache directly and do not depend on its branding internals." +effect/Cache#CacheStats: + replacement: "none" + note: "Built-in hit and miss statistics were removed; instrument the lookup and Cache.get calls explicitly, and use Cache.size for the current entry count." +effect/Cache#CacheTypeId: + replacement: "none" + note: "The cache type id is internal in v4; do not inspect or construct the cache brand directly." +effect/Cache#ConsumerCache: + replacement: "Cache.Cache" + note: "ConsumerCache was removed; use Cache.Cache and expose an application-defined read-only wrapper if capability restriction is required." +effect/Cache#ConsumerCacheTypeId: + replacement: "none" + note: "ConsumerCache and its type id were removed with the read-only cache view." +effect/Cache#EntryStats: + replacement: "none" + note: "Per-entry loaded-time statistics were removed; record lookup timing in application instrumentation if needed." +effect/Cache#Lookup: + replacement: "(key: Key) => Effect.Effect" + note: "The named alias was removed; use an inline lookup function type or Cache.Cache[\"lookup\"]." +effect/Cache#makeCacheStats: + replacement: "none" + note: "CacheStats and its constructor were removed; define an application metrics record if these counters are still required." +effect/Cache#makeEntryStats: + replacement: "none" + note: "EntryStats and its constructor were removed; capture lookup timing in application instrumentation instead." diff --git a/.context/effect/migration/annotations/effect__Cause.yaml b/.context/effect/migration/annotations/effect__Cause.yaml new file mode 100644 index 000000000..cc4611e8a --- /dev/null +++ b/.context/effect/migration/annotations/effect__Cause.yaml @@ -0,0 +1,213 @@ +effect/Cause#andThen: + replacement: "Cause.fromReasons(self.reasons.flatMap(...))" + note: "No direct v4 combinator. For each Fail reason, splice either f(reason.error).reasons or the constant cause's reasons; retain Die and Interrupt reasons, then rebuild with Cause.fromReasons." +effect/Cause#as: + replacement: "Cause.map(self, () => error)" + note: "Use Cause.map with a constant function; only Fail errors change and Die/Interrupt reasons remain." +effect/Cause#Cause: + replacement: "Cause.Cause" + note: "The name remains, but v4 Cause is a wrapper with readonly reasons: ReadonlyArray>, not the v3 Empty/Fail/Die/Interrupt/Sequential/Parallel tree." +effect/Cause#Cause.Variance: + replacement: "none" + note: "The public variance helper was removed. Cause.Cause is directly branded by Cause.TypeId; application code should not reproduce the old variance member." +effect/Cause#CauseReducer: + replacement: "cause.reasons.reduce" + note: "The six-case tree reducer type was removed with Empty, Sequential, and Parallel. Reduce the flat Reason array and switch on Fail, Die, or Interrupt instead." +effect/Cause#CauseTypeId: + replacement: "Cause.TypeId" + note: "The brand export is Cause.TypeId, a literal-string const. Use typeof Cause.TypeId in type positions; the v3 unique-symbol CauseTypeId alias is gone." +effect/Cause#contains: + replacement: "Equal.equals(Cause.combine(self, that), self)" + note: "There are no subtrees in v4. This tests whether all reasons from that are already present in self under v4 reason equality; use Equal.equals(self, that) when only whole-cause equality is intended." +effect/Cause#defects: + replacement: "self.reasons.filter(Cause.isDieReason).map((reason) => reason.defect)" + note: "Collect defect values from the flat Reason array. The result is a standard array rather than v3 Chunk." +effect/Cause#Die: + replacement: "Cause.Die" + note: "The name remains, but Cause.Die is now a Reason stored in cause.reasons, not a Cause variant. Construct a standalone reason with Cause.makeDieReason or a cause with Cause.die." +effect/Cause#dieOption: + replacement: "Cause.findDefect" + note: "Cause.findDefect returns Result.Result>, not Option. Match the Result or convert it to Option when the old return shape is required." +effect/Cause#Empty: + replacement: "Cause.empty" + note: "The Empty subtype and _tag were removed. Empty is Cause.empty, represented by cause.reasons.length === 0." +effect/Cause#ExceededCapacityException: + replacement: "Cause.ExceededCapacityError" + note: "Rename the class/type and update the discriminant from ExceededCapacityException to ExceededCapacityError." +effect/Cause#ExceededCapacityExceptionTypeId: + replacement: "Cause.ExceededCapacityErrorTypeId" + note: "Rename the brand; v4 exports a literal-string const, so use typeof Cause.ExceededCapacityErrorTypeId in type positions." +effect/Cause#Fail: + replacement: "Cause.Fail" + note: "The name remains, but Cause.Fail is now a Reason stored in cause.reasons, not a Cause variant. Construct a standalone reason with Cause.makeFailReason or a cause with Cause.fail." +effect/Cause#failureOption: + replacement: "Cause.findErrorOption" + note: "Direct Option-based replacement for extracting the first typed Fail error value." +effect/Cause#failureOrCause: + replacement: "Cause.findError" + note: "Use the v4 Result-based split: success is the first E and failure is the original Cause when no Fail reason exists." +effect/Cause#failures: + replacement: "self.reasons.filter(Cause.isFailReason).map((reason) => reason.error)" + note: "Collect typed error values from the flat Reason array. The result is a standard array rather than v3 Chunk." +effect/Cause#filter: + replacement: "Cause.fromReasons(self.reasons.filter(...))" + note: "No exact tree-level equivalent: v3 predicates selected recursive child causes. Rewrite the predicate for Cause.Reason values, filter cause.reasons, and rebuild with Cause.fromReasons." +effect/Cause#find: + replacement: "Option.firstSomeOf(self.reasons.map(...))" + note: "No recursive nodes remain. Apply the partial function to Reason values and take the first Some, or use Cause.findFail/findError/findDie/findDefect/findInterrupt for standard searches." +effect/Cause#flatMap: + replacement: "Cause.fromReasons(self.reasons.flatMap((reason) => Cause.isFailReason(reason) ? f(reason.error).reasons : [reason]))" + note: "No direct v4 export. Flat-map only Fail reasons into replacement causes, preserve Die/Interrupt reasons, and rebuild from the resulting Reason array." +effect/Cause#flatten: + replacement: "Cause.fromReasons(self.reasons.flatMap((reason) => Cause.isFailReason(reason) ? reason.error.reasons : [reason]))" + note: "No direct v4 export. For Cause>, splice each Fail reason's nested cause.reasons and retain Die/Interrupt reasons." +effect/Cause#flipCauseOption: + replacement: "Cause.fromReasons + Option" + note: "Rewrite over reasons: drop Fail(None), replace Fail(Some(e)) with Cause.makeFailReason(e), retain Die/Interrupt, then return None only when a non-empty input loses every reason; preserve Some(Cause.empty) for an empty input." +effect/Cause#IllegalArgumentException: + replacement: "Cause.IllegalArgumentError" + note: "Rename the class/type and update the discriminant from IllegalArgumentException to IllegalArgumentError." +effect/Cause#IllegalArgumentExceptionTypeId: + replacement: "Cause.IllegalArgumentErrorTypeId" + note: "Rename the brand; v4 exports a literal-string const, so use typeof Cause.IllegalArgumentErrorTypeId in type positions." +effect/Cause#Interrupt: + replacement: "Cause.Interrupt" + note: "The name remains, but it is now a Reason in cause.reasons rather than a Cause variant, and fiberId changed from FiberId.FiberId to number | undefined. Use Cause.makeInterruptReason or Cause.interrupt." +effect/Cause#InterruptedException: + replacement: "none" + note: "The public exception class was removed. Represent cancellation with Cause.interrupt; Cause.prettyErrors creates an ordinary Error named InterruptError for interrupt-only rendering, but no class is exported." +effect/Cause#InterruptedExceptionTypeId: + replacement: "none" + note: "Removed with InterruptedException; v4 exports no interruption-error brand. Inspect the Cause with Cause.hasInterrupts or Cause.hasInterruptsOnly instead." +effect/Cause#interruptOption: + replacement: "Cause.findInterrupt" + note: "The replacement returns Result.Result> rather than Option; on success read reason.fiberId, now number | undefined." +effect/Cause#InvalidPubSubCapacityException: + replacement: "Error" + note: "The dedicated public type was removed. Current v4 PubSub capacity validation throws a standard global Error with the capacity message." +effect/Cause#InvalidPubSubCapacityExceptionTypeId: + replacement: "none" + note: "Removed with InvalidPubSubCapacityException; the standard Error now thrown by PubSub has no Effect-specific brand." +effect/Cause#isDie: + replacement: "Cause.hasDies" + note: "Use the v4 cause-level predicate for the presence of at least one Die reason." +effect/Cause#isDieType: + replacement: "Cause.isDieReason" + note: "Apply this guard to an entry of cause.reasons; Cause itself is no longer a Die union variant." +effect/Cause#isEmpty: + replacement: "self.reasons.length === 0" + note: "V4 represents an empty cause with an empty reasons array and exports no isEmpty function." +effect/Cause#isEmptyType: + replacement: "self.reasons.length === 0" + note: "The check remains possible, but there is no Empty subtype to narrow to because v4 Cause is not a variant union." +effect/Cause#isExceededCapacityException: + replacement: "Cause.isExceededCapacityError" + note: "Rename the guard along with ExceededCapacityError." +effect/Cause#isFailType: + replacement: "Cause.isFailReason" + note: "Apply this guard to an entry of cause.reasons; Cause itself is no longer a Fail union variant." +effect/Cause#isFailure: + replacement: "Cause.hasFails" + note: "Use the v4 cause-level predicate for the presence of at least one Fail reason." +effect/Cause#isIllegalArgumentException: + replacement: "Cause.isIllegalArgumentError" + note: "Rename the guard along with IllegalArgumentError." +effect/Cause#isInterrupted: + replacement: "Cause.hasInterrupts" + note: "Use the v4 cause-level predicate for the presence of at least one Interrupt reason." +effect/Cause#isInterruptedException: + replacement: "none" + note: "No v4 InterruptError class or unknown-value guard is exported. When the Cause is available, test Cause.hasInterruptsOnly before squashing or rendering it." +effect/Cause#isInterruptedOnly: + replacement: "Cause.hasInterruptsOnly" + note: "Direct cause-level rename; it is false for Cause.empty and true only when at least one reason exists and every reason is Interrupt." +effect/Cause#isInterruptType: + replacement: "Cause.isInterruptReason" + note: "Apply this guard to an entry of cause.reasons; Cause itself is no longer an Interrupt union variant." +effect/Cause#isNoSuchElementException: + replacement: "Cause.isNoSuchElementError" + note: "Rename the guard along with NoSuchElementError." +effect/Cause#isParallelType: + replacement: "none" + note: "Parallel cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind." +effect/Cause#isRuntimeException: + replacement: "none" + note: "RuntimeException and its brand were removed. Use instanceof Error for generic errors or define a Data.Error/Data.TaggedError class with its own guard when nominal recognition is required." +effect/Cause#isSequentialType: + replacement: "none" + note: "Sequential cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind." +effect/Cause#isTimeoutException: + replacement: "Cause.isTimeoutError" + note: "Rename the guard along with TimeoutError." +effect/Cause#isUnknownException: + replacement: "Cause.isUnknownError" + note: "Rename the guard along with UnknownError." +effect/Cause#keepDefects: + replacement: "Cause.fromReasons(self.reasons.filter(Cause.isDieReason))" + note: "Keep every Die reason, not merely the first defect. Return Option.none when the filtered array is empty and Option.some of the rebuilt cause otherwise; Cause.findDefect alone is not behaviorally equivalent." +effect/Cause#linearize: + replacement: "self.reasons" + note: "No direct replacement: v4 discarded sequential/parallel structure, so there are no parallel branches to linearize. Rewrite the consumer to process the flat Reason array." +effect/Cause#NoSuchElementException: + replacement: "Cause.NoSuchElementError" + note: "Rename the class/type and update the discriminant from NoSuchElementException to NoSuchElementError." +effect/Cause#NoSuchElementExceptionTypeId: + replacement: "Cause.NoSuchElementErrorTypeId" + note: "Rename the brand; v4 exports a literal-string const, so use typeof Cause.NoSuchElementErrorTypeId in type positions." +effect/Cause#originalError: + replacement: "Function.identity" + note: "V3 used this to unwrap span-capture proxies. V4 stores tracing data on Reason.annotations and no longer proxies errors, so the input is already the original value." +effect/Cause#parallel: + replacement: "Cause.combine" + note: "Combine the two flat reason arrays; v4 intentionally no longer records whether composition was parallel or sequential." +effect/Cause#Parallel: + replacement: "none" + note: "Parallel cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind." +effect/Cause#PrettyError: + replacement: "Error" + note: "Cause.prettyErrors now returns Array. The dedicated span field is gone; tracing information is incorporated from Reason annotations into rendered stacks." +effect/Cause#reduce: + replacement: "self.reasons.reduce" + note: "Reduce the flat Reason array directly. The callback now sees only Fail, Die, and Interrupt reasons, never Empty or composition nodes." +effect/Cause#reduceWithContext: + replacement: "self.reasons.reduce" + note: "Capture the context in the reducer closure and reduce the flat Reason array; sequentialCase and parallelCase have no v4 analogue." +effect/Cause#RuntimeException: + replacement: "Error" + note: "The dedicated class was removed and v4 uses global Error for generic defects. Use Data.Error or Data.TaggedError instead when a yieldable typed error is required." +effect/Cause#RuntimeExceptionTypeId: + replacement: "none" + note: "Removed with RuntimeException. Define and guard a custom Data.Error/Data.TaggedError type if nominal branding is required." +effect/Cause#sequential: + replacement: "Cause.combine" + note: "Combine the two flat reason arrays; v4 intentionally no longer records whether composition was parallel or sequential." +effect/Cause#Sequential: + replacement: "none" + note: "Sequential cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind." +effect/Cause#size: + replacement: "self.reasons.length" + note: "The v3 node count becomes the number of flat reasons in v4." +effect/Cause#squashWith: + replacement: "Result.match(Cause.findError(self), { onSuccess: f, onFailure: Cause.squash })" + note: "Apply f only to the first typed Fail error; if no Fail exists, squash the returned Cause. This preserves v3's priority and avoids evaluating f for later Fail reasons." +effect/Cause#stripFailures: + replacement: "Cause.fromReasons(self.reasons.filter((reason) => !Cause.isFailReason(reason)))" + note: "Remove Fail reasons and retain Die plus Interrupt reasons, then rebuild the cause. The v3 prose saying interrupts were removed did not match its implementation." +effect/Cause#stripSomeDefects: + replacement: "Cause.fromReasons + Option" + note: "Filter out each Die reason for which pf(reason.defect) is Some, retain all other reasons, and rebuild. Return None only when a non-empty input loses every reason; preserve Some(Cause.empty) for empty input." +effect/Cause#TimeoutException: + replacement: "Cause.TimeoutError" + note: "Rename the class/type and update the discriminant from TimeoutException to TimeoutError." +effect/Cause#TimeoutExceptionTypeId: + replacement: "Cause.TimeoutErrorTypeId" + note: "Rename the brand; v4 exports a literal-string const, so use typeof Cause.TimeoutErrorTypeId in type positions." +effect/Cause#UnknownException: + replacement: "Cause.UnknownError" + note: "Rename the class/type and discriminant. The original unknown value is now exposed through the standard Error.cause property, not v3's .error field." +effect/Cause#UnknownExceptionTypeId: + replacement: "Cause.UnknownErrorTypeId" + note: "Rename the brand; v4 exports a literal-string const, so use typeof Cause.UnknownErrorTypeId in type positions." +effect/Cause#YieldableError: + replacement: "Cause.YieldableError / Data.Error" + note: "Cause.YieldableError remains as the interface/type, but its public constructor value was removed. Extend Data.Error for an untagged yieldable error or Data.TaggedError for a tagged one." diff --git a/.context/effect/migration/annotations/effect__Channel.yaml b/.context/effect/migration/annotations/effect__Channel.yaml new file mode 100644 index 000000000..2b8ea573b --- /dev/null +++ b/.context/effect/migration/annotations/effect__Channel.yaml @@ -0,0 +1,219 @@ +"effect/Channel#acquireReleaseOut": + replacement: "Channel.acquireRelease" + note: "Renamed to acquireRelease. The v4 release action cannot add environment requirements, so capture or provide any services it needs." +"effect/Channel#as": + replacement: "Channel.mapDone" + note: "Replace Channel.as(self, value) with Channel.mapDone(self, () => value)." +"effect/Channel#asVoid": + replacement: "Channel.mapDone" + note: "Replace Channel.asVoid(self) with Channel.mapDone(self, () => void 0)." +"effect/Channel#bufferChunk": + replacement: "none" + note: "The inferred Channel.fromChunk match is not equivalent. Rebuild the buffered upstream-pull transform with Channel.fromTransform and Channel.toTransform." +"effect/Channel#catchAll": + replacement: "Channel.catch" + note: "Renamed to catch for typed-error recovery." +"effect/Channel#catchAllCause": + replacement: "Channel.catchCause" + note: "Renamed to catchCause for full-cause recovery." +"effect/Channel#Channel": + replacement: "Channel.Channel" + note: "Retained, but reorder type parameters from to . Convert Effect values explicitly with Channel.fromEffect or Channel.fromEffectDone." +"effect/Channel#ChannelException": + replacement: "none" + note: "Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper." +"effect/Channel#ChannelExceptionTypeId": + replacement: "none" + note: "Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper." +"effect/Channel#isChannelException": + replacement: "none" + note: "Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper." +"effect/Channel#ChannelTypeId": + replacement: "Channel.TypeId" + note: "Renamed to TypeId; the brand is now the string literal ~effect/Channel. Prefer Channel.isChannel for runtime checks." +"effect/Channel#ChannelUnify": + replacement: "Channel.ChannelUnify" + note: "Retained; update inferred Channel arguments to the v4 generic order." +"effect/Channel#ChannelUnifyIgnore": + replacement: "Channel.ChannelUnifyIgnore" + note: "Retained with a new shape: it no longer extends EffectUnifyIgnore and now contains Effect?: true." +"effect/Channel#collect": + replacement: "Channel.filterMap" + note: "Use Channel.filterMap(self, Filter.fromPredicateOption(pf)) to adapt the v3 Option-returning partial function." +"effect/Channel#concatAll": + replacement: "Channel.flatten" + note: "Use flatten for sequential emitted-channel flattening. V4 preserves the outer done value and discards child done values." +"effect/Channel#concatOut": + replacement: "Channel.flatten" + note: "Use flatten for sequential emitted-channel flattening. V4 preserves the outer done value and discards child done values." +"effect/Channel#concatAllWith": + replacement: "none" + note: "V4 removed child-done accumulation and the outer-done combiner. Use Channel.flatten or Channel.flatMap only when child done values may be discarded; otherwise implement a Pull transform." +"effect/Channel#concatMapWith": + replacement: "none" + note: "V4 removed child-done accumulation and the outer-done combiner. Use Channel.flatten or Channel.flatMap only when child done values may be discarded; otherwise implement a Pull transform." +"effect/Channel#concatMap": + replacement: "Channel.flatMap" + note: "Renamed to flatMap. Sequential flattening is the default; child done values are discarded and the source done value is preserved." +"effect/Channel#concatMapWithCustom": + replacement: "none" + note: "Removed with the channel executor scheduling protocol. Use Channel.flatMap for ordinary sequencing or implement custom scheduling with Channel.fromTransform and Pull." +"effect/Channel#context": + replacement: "Channel.contextWith" + note: "Use Channel.contextWith((context) => Channel.end(context)); the context was the v3 channel done value." +"effect/Channel#contextWithChannel": + replacement: "Channel.contextWith" + note: "Renamed to contextWith." +"effect/Channel#contextWithEffect": + replacement: "Channel.contextWith" + note: "Use Channel.contextWith((context) => Channel.fromEffectDone(f(context))) to preserve the effect result as the done value." +"effect/Channel#doneCollect": + replacement: "none" + note: "No exact channel combinator remains. Drive Channel.toPull, collect output elements, and handle Cause.Done to retain both outputs and the done value." +"effect/Channel#emitCollect": + replacement: "none" + note: "No exact channel combinator remains. Drive Channel.toPull, collect output elements, and handle Cause.Done to retain both outputs and the done value." +"effect/Channel#ensuringWith": + replacement: "Channel.onExit" + note: "Renamed to onExit; the finalizer still receives the channel Exit." +"effect/Channel#foldCauseChannel": + replacement: "none" + note: "V4 has no exact two-sided fold over failure and completion. Use catchCause or catch for failure-only handling, concatWith for success-only handling, or match the Pull in a custom transform." +"effect/Channel#foldChannel": + replacement: "none" + note: "V4 has no exact two-sided fold over failure and completion. Use catchCause or catch for failure-only handling, concatWith for success-only handling, or match the Pull in a custom transform." +"effect/Channel#fromEither": + replacement: "Channel.fromEffectDone" + note: "Either is now Result. Use Channel.fromEffectDone(Effect.fromResult(result)) to preserve success as the done value." +"effect/Channel#fromInput": + replacement: "none" + note: "SingleProducerAsyncInput was removed. Model the producer with Queue and Pull; use Channel.fromPull with Queue.take when a typed done value matters." +"effect/Channel#fromOption": + replacement: "Channel.fromEffectDone" + note: "Use Channel.fromEffectDone(Effect.fromOption(option, Option.none)) to preserve the v3 Option.none error, or omit onNone for the v4 NoSuchElementError default." +"effect/Channel#fromPubSubScoped": + replacement: "Channel.fromPubSubTake" + note: "Change the protocol to PubSub> and use Channel.flattenArray(Channel.fromPubSubTake(pubsub)). The v4 constructor owns the scoped subscription and returns a Channel directly." +"effect/Channel#interruptWhenDeferred": + replacement: "Channel.interruptWhen" + note: "Use Channel.interruptWhen(self, Deferred.await(deferred)); the Deferred-specific overload was removed." +"effect/Channel#mapErrorCause": + replacement: "Channel.catchCause" + note: "Use Channel.catchCause(self, (cause) => Channel.failCause(f(cause)))." +"effect/Channel#mapInputContext": + replacement: "Channel.updateContext" + note: "Renamed to updateContext for transforming the channel requirement Context." +"effect/Channel#mapInputEffect": + replacement: "none" + note: "V4 removed upstream done/error effect mapping. Adapt Cause.Done or failure on the upstream Pull, then pass it through Channel.toTransform(self)." +"effect/Channel#mapInputErrorEffect": + replacement: "none" + note: "V4 removed upstream done/error effect mapping. Adapt Cause.Done or failure on the upstream Pull, then pass it through Channel.toTransform(self)." +"effect/Channel#mapInputIn": + replacement: "Channel.mapInput" + note: "Use Channel.mapInput(self, (value) => Effect.succeed(f(value))); v4 consolidated pure and effectful input mapping." +"effect/Channel#mapInputInEffect": + replacement: "Channel.mapInput" + note: "Renamed to mapInput; the mapper remains effectful." +"effect/Channel#mapOut": + replacement: "Channel.map" + note: "Renamed to map; the v4 mapper also receives the element index." +"effect/Channel#mapOutEffect": + replacement: "Channel.mapEffect" + note: "Renamed to mapEffect for sequential effectful output mapping." +"effect/Channel#mapOutEffectPar": + replacement: "Channel.mapEffect" + note: "Use Channel.mapEffect(self, f, { concurrency: n }); ordered output remains the default." +"effect/Channel#mergeAllUnbounded": + replacement: "Channel.mergeAll" + note: "Use Channel.mergeAll(channels, { concurrency: \"unbounded\" }); child done values are discarded and the outer done value is preserved." +"effect/Channel#mergeAllUnboundedWith": + replacement: "none" + note: "V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge." +"effect/Channel#mergeAllWith": + replacement: "none" + note: "V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge." +"effect/Channel#mergeOutWith": + replacement: "none" + note: "V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge." +"effect/Channel#mergeMap": + replacement: "Channel.flatMap" + note: "Use Channel.flatMap with concurrency and bufferSize for backpressure, or Channel.switchMap with the same options for the v3 sliding strategy." +"effect/Channel#mergeOut": + replacement: "Channel.mergeAll" + note: "Use Channel.mergeAll(self, { concurrency: n }) for bounded backpressured flattening when child done values are irrelevant." +"effect/Channel#mergeWith": + replacement: "Channel.merge" + note: "Use Channel.merge with haltStrategy left, right, both, or either for standard policies. Custom MergeDecision effects require a Pull-level redesign." +"effect/Channel#orDieWith": + replacement: "Channel.catch" + note: "Use Channel.catch(self, (error) => Channel.die(f(error))); v4 Channel.orDie has no mapping callback." +"effect/Channel#orElse": + replacement: "Channel.catch" + note: "Use Channel.catch(self, () => that()) and keep the fallback lazy." +"effect/Channel#provideLayer": + replacement: "Channel.provide" + note: "Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed." +"effect/Channel#provideSomeLayer": + replacement: "Channel.provide" + note: "Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed." +"effect/Channel#read": + replacement: "none" + note: "The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling." +"effect/Channel#readOrFail": + replacement: "none" + note: "The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling." +"effect/Channel#readWith": + replacement: "none" + note: "The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling." +"effect/Channel#readWithCause": + replacement: "none" + note: "The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling." +"effect/Channel#repeated": + replacement: "Channel.forever" + note: "Use forever for infinite repetition. Channel.repeat takes a Schedule and may terminate, so it is not equivalent." +"effect/Channel#run": + replacement: "Channel.runDone" + note: "Renamed to runDone for an inputless, outputless channel. Use runDrain if emitted elements should be discarded." +"effect/Channel#runScoped": + replacement: "Channel.toPull" + note: "No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDone or runDrain when an internally managed scope is acceptable." +"effect/Channel#scopedWith": + replacement: "Channel.unwrap" + note: "Use Channel.unwrap(Effect.map(Effect.scope, (scope) => Channel.fromEffect(f(scope)))) so the effect uses the active channel scope." +"effect/Channel#toPullIn": + replacement: "Channel.toPullScoped" + note: "Renamed to toPullScoped. The returned Pull emits elements directly and represents completion with Cause.Done instead of Either." +"effect/Channel#toSink": + replacement: "Sink.fromChannel" + note: "Constructor moved to Sink. Adapt the channel to non-empty array input, no emitted leftovers, and a Sink.End done value." +"effect/Channel#toStream": + replacement: "Stream.fromChannel" + note: "Constructor moved to Stream. Adapt Chunk outputs to non-empty readonly arrays and map the channel done value to void." +"effect/Channel#unwrapScoped": + replacement: "Channel.unwrap" + note: "Use unwrap; v4 supplies the active channel scope to the effect and removes Scope from the resulting requirement." +"effect/Channel#unwrapScopedWith": + replacement: "Channel.unwrap" + note: "Use Channel.unwrap(Effect.flatMap(Effect.scope, f)) to pass the active channel scope to f." +"effect/Channel#void": + replacement: "Channel.empty" + note: "Renamed to empty: emit nothing and end with void." +"effect/Channel#write": + replacement: "Channel.succeed" + note: "Renamed to succeed, which emits one element in v4. Use Channel.end when migrating v3 succeed, which produced a done value." +"effect/Channel#writeAll": + replacement: "Channel.fromArray" + note: "Replace the variadic writer with Channel.fromArray(outs)." +"effect/Channel#writeChunk": + replacement: "Channel.fromChunk" + note: "Renamed to fromChunk for emitting every Chunk element." +"effect/Channel#zip": + replacement: "Channel.concatWith" + note: "For sequential zip, concatWith the left channel and mapDone the right result to a tuple. Concurrent tuple-done semantics require custom Pull coordination." +"effect/Channel#zipLeft": + replacement: "Channel.concatWith" + note: "For sequential zipLeft, concatWith and mapDone the right result back to the left done value. Concurrent done preservation requires custom Pull coordination." +"effect/Channel#zipRight": + replacement: "Channel.concat" + note: "Use concat for the sequential form; it preserves the right done value. Concurrent mode has no exact replacement." diff --git a/.context/effect/migration/annotations/effect__ChildExecutorDecision.yaml b/.context/effect/migration/annotations/effect__ChildExecutorDecision.yaml new file mode 100644 index 000000000..e8bfb105b --- /dev/null +++ b/.context/effect/migration/annotations/effect__ChildExecutorDecision.yaml @@ -0,0 +1,3 @@ +effect/ChildExecutorDecision: + replacement: none + note: Removed with the v3 channel executor and Channel.concatMapWithCustom. Choose Channel.flatMap, Channel.switchMap, or Channel.mergeAll instead; v4 exposes no child-executor decision ADT. diff --git a/.context/effect/migration/annotations/effect__Chunk.yaml b/.context/effect/migration/annotations/effect__Chunk.yaml new file mode 100644 index 000000000..2771afb04 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Chunk.yaml @@ -0,0 +1,36 @@ +"effect/Chunk#Chunk": + replacement: "Chunk.Chunk" + note: "The model remains Chunk.Chunk; continue using Chunk constructors rather than depending on its exposed representation fields." +"effect/Chunk#getEquivalence": + replacement: "Chunk.makeEquivalence" + note: "Direct rename; pass the element Equivalence unchanged." +"effect/Chunk#modifyOption": + replacement: "Chunk.modify" + note: "The Option suffix was dropped; an out-of-bounds index still returns Option.none." +"effect/Chunk#partitionMap": + replacement: "Chunk.partition" + note: "Pass a Result-returning mapper instead of Either; the output remains [failures, successes]." +"effect/Chunk#removeOption": + replacement: "Chunk.remove" + note: "The closest API now returns the unchanged Chunk out of bounds; use Chunk.get before Chunk.remove to preserve the old Option result." +"effect/Chunk#replaceOption": + replacement: "Chunk.replace" + note: "The Option suffix was dropped; an out-of-bounds index still returns Option.none." +"effect/Chunk#TypeId": + replacement: "none" + note: "The v4 Chunk brand key is private; no public Chunk.TypeId type or value is exported." +"effect/Chunk#unsafeFromArray": + replacement: "Chunk.fromArrayUnsafe" + note: "Direct word-order rename; it still wraps without copying and is unsafe if the source array is mutated." +"effect/Chunk#unsafeFromNonEmptyArray": + replacement: "Chunk.fromNonEmptyArrayUnsafe" + note: "Direct word-order rename; it still wraps without copying and preserves NonEmptyChunk." +"effect/Chunk#unsafeGet": + replacement: "Chunk.getUnsafe" + note: "Direct word-order rename; it still throws for an out-of-bounds index." +"effect/Chunk#unsafeHead": + replacement: "Chunk.headUnsafe" + note: "Direct word-order rename; it still throws on an empty Chunk." +"effect/Chunk#unsafeLast": + replacement: "Chunk.lastUnsafe" + note: "Direct word-order rename; it still throws on an empty Chunk." diff --git a/.context/effect/migration/annotations/effect__Clock.yaml b/.context/effect/migration/annotations/effect__Clock.yaml new file mode 100644 index 000000000..510486a47 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Clock.yaml @@ -0,0 +1,18 @@ +"effect/Clock#CancelToken": + replacement: "none" + note: "The public clock scheduler and cancellation-token protocol were removed. Use Effect.sleep for delays and Effect interruption or Fiber.interrupt for cancellation." +"effect/Clock#Clock": + replacement: "Clock.Clock" + note: "The service interface remains, but unsafeCurrentTimeMillis and unsafeCurrentTimeNanos were renamed to currentTimeMillisUnsafe and currentTimeNanosUnsafe, the public type-id field was removed, and custom implementations must add monotonicTimeNanosUnsafe plus monotonicTimeNanos for elapsed-time measurement." +"effect/Clock#ClockScheduler": + replacement: "none" + note: "The low-level clock scheduler is no longer public. Express scheduling with Effect.sleep and cancel the running fiber through normal Effect interruption." +"effect/Clock#ClockTypeId": + replacement: "none" + note: "The Clock type-id is private in v4. Use the Clock.Clock Context.Reference to access, provide, or identify the clock service." +"effect/Clock#make": + replacement: "Layer.succeed(Clock.Clock, clock)" + note: "The Clock constructor was removed. Implement the v4 Clock interface as a plain service value and provide it through Clock.Clock." +"effect/Clock#Task": + replacement: "none" + note: "The low-level clock task alias was removed with ClockScheduler. Model delayed work as an Effect and run or fork it after Effect.sleep." diff --git a/.context/effect/migration/annotations/effect__Config.yaml b/.context/effect/migration/annotations/effect__Config.yaml new file mode 100644 index 000000000..122eb5371 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Config.yaml @@ -0,0 +1,111 @@ +"effect/Config#all": + replacement: "Config.all" + note: "Combine an iterable or record of Config values. A wholly absent product can use Config.withDefault or Config.option, while a partially supplied product fails." +"effect/Config#array": + replacement: "Config.schema(Config.Array(valueSchema), path)" + note: "Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass the optional path to Config.schema." +"effect/Config#boolean": + replacement: "Config.boolean" + note: "Unchanged." +"effect/Config#branded": + replacement: "Config.schema(schema.pipe(Schema.brand(brand)), path)" + note: "Brand validation moved to Schema; define the branded schema and construct the Config with Config.schema." +"effect/Config#chunk": + replacement: "Config.schema(Schema.Chunk(valueSchema), path)" + note: "Collection parsing is schema-based in v4; use Schema.Chunk when a Chunk result is still required." +"effect/Config#Config": + replacement: "Config.Config" + note: "The model remains a yieldable Effect and exposes parse(provider). Compose logical lookup paths with Config.schema(..., path) and Config.nested; parsing no longer accepts a public path prefix." +"effect/Config#Config.IsPlainObject": + replacement: "none" + note: "This private conditional helper is no longer exposed; use Config.Wrap for the public recursive wrapping contract." +"effect/Config#Config.Primitive": + replacement: "Schema.Constraint" + note: "Primitive descriptions and parsers were replaced by Schema codecs consumed through Config.schema." +"effect/Config#Config.Variance": + replacement: "none" + note: "Config now carries its result type directly through Effect and has no public variance interface." +"effect/Config#ConfigTypeId": + replacement: "Config.isConfig" + note: "The Config marker is private in v4; use the public guard for runtime narrowing." +"effect/Config#date": + replacement: "Config.date" + note: "Unchanged." +"effect/Config#duration": + replacement: "Config.duration" + note: "Unchanged." +"effect/Config#fail": + replacement: "Config.fail" + note: "The v4 constructor takes a ConfigProvider.SourceError or Schema.SchemaError instead of a message; wrap the failure in the appropriate cause." +"effect/Config#hashMap": + replacement: "Config.schema(Schema.HashMap(Schema.String, valueSchema), path)" + note: "HashMap parsing is schema-based in v4; replace the child Config with its value Schema." +"effect/Config#hashSet": + replacement: "Config.schema(Schema.HashSet(valueSchema), path)" + note: "HashSet parsing is schema-based in v4; replace the child Config with its value Schema." +"effect/Config#integer": + replacement: "Config.int" + note: "Renamed to the shorter v4 integer constructor." +"effect/Config#literal": + replacement: "Config.literals(literals, path)" + note: "The v3 curried variadic constructor became Config.literals with an array and inline path; use Config.literal for one value." +"effect/Config#LiteralValue": + replacement: "SchemaAST.LiteralValue" + note: "Use the literal value type shared by v4 Schema constructors." +"effect/Config#logLevel": + replacement: "Config.logLevel" + note: "Unchanged." +"effect/Config#mapAttempt": + replacement: "Config.mapOrFail" + note: "Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapOrFail is Effect-based in v4." +"effect/Config#nonEmptyString": + replacement: "Config.nonEmptyString" + note: "Unchanged." +"effect/Config#number": + replacement: "Config.number" + note: "Unchanged; use Config.finite when NaN and infinities must be rejected." +"effect/Config#orElseIf": + replacement: "Config.orElse" + note: "The fallback now receives Config.ConfigError; test it in the callback and re-fail with Config.fail(error.cause) when the predicate is false." +"effect/Config#port": + replacement: "Config.port" + note: "Unchanged." +"effect/Config#primitive": + replacement: "Config.schema(customSchema, path)" + note: "Custom primitive parsing moved to Schema codecs; express decoding and diagnostics in a Schema, then pass it to Config.schema. Its canonical StringTree encoding must expose a concrete shape; opaque encodings such as Schema.Any or Schema.Unknown are not supported." +"effect/Config#redacted": + replacement: "Config.redacted" + note: "The string/path overload remains; replace the v3 Config argument overload with Config.map(config, Redacted.make)." +"effect/Config#repeat": + replacement: "Config.schema(Config.Array(valueSchema), path)" + note: "Repeated values are represented by an array Schema in v4; Config.Array also accepts flat separated input." +"effect/Config#secret": + replacement: "Config.redacted" + note: "Secret was removed in favor of Redacted; this constructor already returns Redacted." +"effect/Config#string": + replacement: "Config.string" + note: "Unchanged." +"effect/Config#succeed": + replacement: "Config.succeed" + note: "Unchanged." +"effect/Config#suspend": + replacement: "Config.schema(Schema.suspend(schemaThunk), path)" + note: "General Config suspension was removed; model recursive parsing with a suspended Schema before constructing the Config." +"effect/Config#sync": + replacement: "Config.succeed(undefined).pipe(Config.map(() => thunk()))" + note: "The dedicated lazy constant constructor was removed; mapping a constant Config preserves evaluation at parse time." +"effect/Config#url": + replacement: "Config.url" + note: "Unchanged." +"effect/Config#validate": + replacement: "Config.schema(schema.check(check), path)" + note: "Validation moved to Schema checks; attach the predicate and message to the Schema used by Config.schema." +"effect/Config#withDescription": + replacement: "Config.schema(schema.annotate({ description }), path)" + note: "Config descriptions moved to Schema annotations in v4." +"effect/Config#zip": + replacement: "Config.all([self, that])" + note: "Use the tuple overload of Config.all." +"effect/Config#zipWith": + replacement: "Config.all([self, that]).pipe(Config.map(([a, b]) => f(a, b)))" + note: "Combine both configs with Config.all, then map the tuple." diff --git a/.context/effect/migration/annotations/effect__ConfigError.yaml b/.context/effect/migration/annotations/effect__ConfigError.yaml new file mode 100644 index 000000000..51146f3ab --- /dev/null +++ b/.context/effect/migration/annotations/effect__ConfigError.yaml @@ -0,0 +1,66 @@ +"effect/ConfigError#And": + replacement: "SchemaIssue.Composite" + note: "The ConfigError boolean ADT was removed; combined schema failures are represented inside Config.ConfigError.cause as SchemaIssue.Composite." +"effect/ConfigError#ConfigError": + replacement: "Config.ConfigError" + note: "Config errors are now a class in effect/Config wrapping either ConfigProvider.SourceError or Schema.SchemaError." +"effect/ConfigError#ConfigError.Proto": + replacement: "Config.ConfigError" + note: "The public prototype interface was removed; use the Config.ConfigError class." +"effect/ConfigError#ConfigError.Reducer": + replacement: "none" + note: "The ConfigError-specific reducer API was removed; inspect ConfigError.cause and recurse over SchemaError.issue when structured handling is required." +"effect/ConfigError#ConfigErrorReducer": + replacement: "none" + note: "The ConfigError-specific reducer API was removed; inspect ConfigError.cause and recurse over SchemaError.issue when structured handling is required." +"effect/ConfigError#ConfigErrorTypeId": + replacement: "error instanceof Config.ConfigError" + note: "The marker is gone because ConfigError is a class in v4." +"effect/ConfigError#InvalidData": + replacement: "new Config.ConfigError(new Schema.SchemaError(issue))" + note: "Invalid configuration is now expressed as a SchemaIssue wrapped by SchemaError and Config.ConfigError." +"effect/ConfigError#isAnd": + replacement: "error.cause.issue._tag === \"Composite\"" + note: "After narrowing cause with Schema.isSchemaError, inspect the SchemaIssue tag; the old And node no longer exists." +"effect/ConfigError#isConfigError": + replacement: "error instanceof Config.ConfigError" + note: "ConfigError is a class in v4." +"effect/ConfigError#isInvalidData": + replacement: "Schema.isSchemaError(error.cause)" + note: "Parsing and validation failures are SchemaError causes; inspect the contained SchemaIssue for finer classification." +"effect/ConfigError#isMissingData": + replacement: "none" + note: "Do not infer semantic absence from a SchemaIssue. Use Config.withDefault or Config.option; they distinguish absent provider input from successful undefined, invalid input, and partial products." +"effect/ConfigError#isMissingDataOnly": + replacement: "Config.withDefault / Config.option" + note: "The public classifier was removed. These combinators use provider lookup evidence rather than recursively classifying SchemaIssue values." +"effect/ConfigError#isOr": + replacement: "error.cause.issue._tag === \"AnyOf\"" + note: "After narrowing cause with Schema.isSchemaError, inspect the SchemaIssue tag; the old Or node no longer exists." +"effect/ConfigError#isSourceUnavailable": + replacement: "error.cause instanceof ConfigProvider.SourceError" + note: "Provider source failures now use the ConfigProvider.SourceError class." +"effect/ConfigError#isUnsupported": + replacement: "none" + note: "The Unsupported variant was removed; report unsupported custom decoding through a SchemaError or source failures through ConfigProvider.SourceError." +"effect/ConfigError#MissingData": + replacement: "none" + note: "There is no public missing-data error variant. A required absent config ultimately fails with a SchemaError, while Config.withDefault and Config.option handle semantic absence before it enters the public Effect error channel." +"effect/ConfigError#Options": + replacement: "none" + note: "The shared constructor options type was removed; ConfigProvider.SourceError accepts message and optional cause, while Schema issues have issue-specific constructors." +"effect/ConfigError#Or": + replacement: "SchemaIssue.AnyOf" + note: "The ConfigError boolean ADT was removed; alternative schema failures are represented inside Config.ConfigError.cause as SchemaIssue.AnyOf." +"effect/ConfigError#prefixed": + replacement: "SchemaIssue.Pointer" + note: "Represent path context by wrapping the underlying SchemaIssue in a Pointer before constructing SchemaError." +"effect/ConfigError#reduceWithContext": + replacement: "none" + note: "The specialized fold was removed; branch on ConfigError.cause, then recurse over the public SchemaIssue union if a fold is needed." +"effect/ConfigError#SourceUnavailable": + replacement: "new ConfigProvider.SourceError({ message, cause })" + note: "Source failures moved to effect/ConfigProvider and are wrapped by Config.ConfigError when a Config is parsed." +"effect/ConfigError#Unsupported": + replacement: "none" + note: "The variant was removed; use a SchemaError for unsupported input or ConfigProvider.SourceError for source capability failures." diff --git a/.context/effect/migration/annotations/effect__ConfigProvider.yaml b/.context/effect/migration/annotations/effect__ConfigProvider.yaml new file mode 100644 index 000000000..7da7c04bb --- /dev/null +++ b/.context/effect/migration/annotations/effect__ConfigProvider.yaml @@ -0,0 +1,69 @@ +"effect/ConfigProvider#ConfigProvider": + replacement: "ConfigProvider.ConfigProvider" + note: "The model remains but now exposes `load(path)`, returning `Effect`, and `mapInput(f)` for provider-owned path transformation. `undefined` means the path is missing; a `Node` means it exists." +"effect/ConfigProvider#ConfigProvider.Flat": + replacement: "ConfigProvider.ConfigProvider" + note: "Flat providers were removed; implement the unified path-based provider with ConfigProvider.make." +"effect/ConfigProvider#ConfigProvider.FromEnvConfig": + replacement: "Parameters[0]" + note: "Options are inline in v4 and contain env plus preserveEmptyStrings; custom path and sequence delimiters moved to provider path transforms and Config.Array/Config.Record schemas." +"effect/ConfigProvider#ConfigProvider.FromMapConfig": + replacement: "none" + note: "fromMap and its delimiter options were removed; expand delimited keys into a nested value and use ConfigProvider.fromUnknown." +"effect/ConfigProvider#ConfigProvider.KeyComponent": + replacement: "ConfigProvider.Path[number]" + note: "Tagged key components became plain string or number path segments." +"effect/ConfigProvider#ConfigProvider.KeyIndex": + replacement: "number" + note: "Tagged KeyIndex values became numeric ConfigProvider.Path segments." +"effect/ConfigProvider#ConfigProvider.KeyName": + replacement: "string" + note: "Tagged KeyName values became string ConfigProvider.Path segments." +"effect/ConfigProvider#ConfigProvider.Proto": + replacement: "ConfigProvider.ConfigProvider" + note: "The public marker prototype was removed; use the provider interface itself." +"effect/ConfigProvider#ConfigProviderTypeId": + replacement: "ConfigProvider.ConfigProvider" + note: "The runtime marker is private in v4; providers are created by public constructors and consumed structurally." +"effect/ConfigProvider#FlatConfigProviderTypeId": + replacement: "none" + note: "The flat-provider abstraction and marker were removed." +"effect/ConfigProvider#fromEnv": + replacement: "ConfigProvider.fromEnv" + note: "The constructor remains; pass env and preserveEmptyStrings options. Paths use underscore semantics, while sequence separators belong on Config schemas." +"effect/ConfigProvider#fromFlat": + replacement: "ConfigProvider.make" + note: "Flat providers were unified with ConfigProvider; return a `Value`, `Record`, or `Array` node for a found path, or `undefined` when missing." +"effect/ConfigProvider#fromJson": + replacement: "ConfigProvider.fromUnknown" + note: "Renamed to reflect support for any in-memory JavaScript value." +"effect/ConfigProvider#fromMap": + replacement: "ConfigProvider.fromUnknown" + note: "Expand the map's delimited keys into a nested object first; v4 removed fromMap and its pathDelim/seqDelim options." +"effect/ConfigProvider#kebabCase": + replacement: "ConfigProvider.mapInput((path) => path.map((part) => typeof part === \"string\" ? String.kebabCase(part) : part))" + note: "Named recasing helpers were removed except constantCase; transform string path segments explicitly." +"effect/ConfigProvider#lowerCase": + replacement: "ConfigProvider.mapInput((path) => path.map((part) => typeof part === \"string\" ? part.toLowerCase() : part))" + note: "Transform string path segments explicitly with mapInput." +"effect/ConfigProvider#make": + replacement: "ConfigProvider.make" + note: "The constructor now takes a path lookup returning `Effect`, rather than a full Config loader and flattened provider. Return `undefined` for a missing path and a `Node` for a found path." +"effect/ConfigProvider#makeFlat": + replacement: "ConfigProvider.make" + note: "The flat-provider constructor was removed; return a `Value`, `Record`, or `Array` node for a found path, or `undefined` when missing." +"effect/ConfigProvider#mapInputPath": + replacement: "ConfigProvider.mapInput" + note: "Renamed and generalized: the callback receives and returns the complete Path, including numeric array indexes." +"effect/ConfigProvider#snakeCase": + replacement: "ConfigProvider.mapInput((path) => path.map((part) => typeof part === \"string\" ? String.snakeCase(part) : part))" + note: "Named recasing helpers were removed except constantCase; transform string path segments explicitly." +"effect/ConfigProvider#unnested": + replacement: "ConfigProvider.mapInput((path) => path[0] === name ? path.slice(1) : path)" + note: "The named helper was removed; strip the matching leading segment explicitly. Add custom handling if the v3 mismatch error was significant." +"effect/ConfigProvider#upperCase": + replacement: "ConfigProvider.mapInput((path) => path.map((part) => typeof part === \"string\" ? part.toUpperCase() : part))" + note: "Transform string path segments explicitly with mapInput." +"effect/ConfigProvider#within": + replacement: "ConfigProvider.orElse + ConfigProvider.mapInput" + note: "The scoped transform helper was removed; build a provider that transforms paths below the prefix and falls back to the original provider elsewhere." diff --git a/.context/effect/migration/annotations/effect__ConfigProviderPathPatch.yaml b/.context/effect/migration/annotations/effect__ConfigProviderPathPatch.yaml new file mode 100644 index 000000000..049419589 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ConfigProviderPathPatch.yaml @@ -0,0 +1,30 @@ +"effect/ConfigProviderPathPatch#AndThen": + replacement: "ConfigProvider.mapInput" + note: "PathPatch is no longer public; compose path transformations as ordinary functions passed to mapInput." +"effect/ConfigProviderPathPatch#empty": + replacement: "ConfigProvider.ConfigProvider" + note: "No identity patch value is needed; leave the provider untransformed." +"effect/ConfigProviderPathPatch#Empty": + replacement: "none" + note: "The PathPatch ADT was removed; an unchanged provider represents the identity transformation." +"effect/ConfigProviderPathPatch#mapName": + replacement: "ConfigProvider.mapInput" + note: "Map the string segments of the full ConfigProvider.Path explicitly." +"effect/ConfigProviderPathPatch#MapName": + replacement: "none" + note: "The PathPatch ADT was removed; use a path transformation function with ConfigProvider.mapInput." +"effect/ConfigProviderPathPatch#nested": + replacement: "ConfigProvider.nested" + note: "Apply nesting directly to the provider instead of constructing a patch." +"effect/ConfigProviderPathPatch#Nested": + replacement: "none" + note: "The PathPatch ADT was removed; use ConfigProvider.nested on the provider." +"effect/ConfigProviderPathPatch#PathPatch": + replacement: "(path: ConfigProvider.Path) => ConfigProvider.Path" + note: "Path patches are ordinary full-path transformations in v4 and are installed with ConfigProvider.mapInput." +"effect/ConfigProviderPathPatch#unnested": + replacement: "ConfigProvider.mapInput" + note: "Strip the expected leading path segment in a mapInput callback; v4 has no named unnested helper." +"effect/ConfigProviderPathPatch#Unnested": + replacement: "none" + note: "The PathPatch ADT was removed; express prefix removal as a ConfigProvider.mapInput function." diff --git a/.context/effect/migration/annotations/effect__Console.yaml b/.context/effect/migration/annotations/effect__Console.yaml new file mode 100644 index 000000000..77030318d --- /dev/null +++ b/.context/effect/migration/annotations/effect__Console.yaml @@ -0,0 +1,21 @@ +effect/Console#Console: + replacement: "Console.Console" + note: "Name retained, but v4 is a Context.Reference whose service methods are synchronous. Rewrite custom implementations from effectful methods plus .unsafe to direct console methods; module accessors such as Console.log still return Effect values." +effect/Console#setConsole: + replacement: "Layer.succeed(Console.Console, console)" + note: "Provide the v4 console reference as a layer." +effect/Console#TypeId: + replacement: "none" + note: "The public console brand was removed; v4 Console.Console is structural." +effect/Console#UnsafeConsole: + replacement: "Console.Console" + note: "The v4 service interface is the old unsafe/direct interface; .unsafe no longer exists." +effect/Console#withConsole: + replacement: "Effect.provideService(effect, Console.Console, console)" + note: "Console overrides now use the reference/service provider pattern." +effect/Console#withGroup: + replacement: "Console.withGroup" + note: "The API and data-first/data-last behavior remain." +effect/Console#withTime: + replacement: "Console.withTime" + note: "The API and data-first/data-last behavior remain." diff --git a/.context/effect/migration/annotations/effect__Context.yaml b/.context/effect/migration/annotations/effect__Context.yaml new file mode 100644 index 000000000..af34f9dfe --- /dev/null +++ b/.context/effect/migration/annotations/effect__Context.yaml @@ -0,0 +1,54 @@ +"effect/Context#Context": + replacement: "Context.Context" + note: "The type remains; unsafeMap is now mapUnsafe and v4 also exposes mutable." +"effect/Context#GenericTag": + replacement: "Context.Service(id)" + note: "Use the function-style Context.Service constructor." +"effect/Context#isTag": + replacement: "Context.isKey" + note: "The service-key guard was renamed." +"effect/Context#ReadonlyTag": + replacement: "Context.Key" + note: "Use the renamed service-key interface." +"effect/Context#Reference": + replacement: "Context.Reference" + note: "Use Context.Reference(id, { defaultValue }); the identifier type parameter was removed." +"effect/Context#ReferenceClass": + replacement: "Context.Reference(id, { defaultValue })" + note: "Replace reference subclasses with a constant created by Context.Reference." +"effect/Context#ReferenceTypeId": + replacement: "none" + note: "The marker is private in v4; use Context.isReference for runtime discrimination." +"effect/Context#Tag": + replacement: "Context.Service" + note: "Use Context.Service(id), or Context.Service()(id) for class syntax." +"effect/Context#Tag.Service": + replacement: "Context.Service.Shape" + note: "The namespace type helper was renamed with Tag." +"effect/Context#TagClass": + replacement: "Context.ServiceClass" + note: "Use the renamed class-style service-key type." +"effect/Context#TagClassShape": + replacement: "Context.ServiceClass.Shape" + note: "Use the renamed namespace type helper." +"effect/Context#TagTypeId": + replacement: "Context.ServiceTypeId" + note: "The public type identifier was renamed with Tag." +"effect/Context#TagUnify": + replacement: "none" + note: "The Context-specific unification hook was removed; Context.Key already extends Effect." +"effect/Context#TagUnifyIgnore": + replacement: "none" + note: "The Context-specific Unify-ignore artifact was removed." +"effect/Context#TypeId": + replacement: "none" + note: "The Context marker is private in v4; use Context.isContext for runtime checks." +"effect/Context#unsafeGet": + replacement: "Context.getUnsafe" + note: "The unsafe getter was renamed." +"effect/Context#unsafeMake": + replacement: "Context.makeUnsafe" + note: "The unsafe constructor was renamed and accepts a ReadonlyMap." +"effect/Context#ValidTagsById": + replacement: "(key: Context.Key)" + note: "The alias was removed; express the Context.Key constraint directly." diff --git a/.context/effect/migration/annotations/effect__Cron.yaml b/.context/effect/migration/annotations/effect__Cron.yaml new file mode 100644 index 000000000..94c388d36 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Cron.yaml @@ -0,0 +1,21 @@ +"effect/Cron#Cron": + replacement: "Cron.Cron" + note: "The model remains; update for the v4 representation and private type id." +"effect/Cron#isParseError": + replacement: "Cron.isCronParseError" + note: "The parse-error guard was renamed with the error type." +"effect/Cron#ParseError": + replacement: "Cron.CronParseError" + note: "The parse error was renamed and Cron.parse now returns Result.Result." +"effect/Cron#ParseErrorTypeId": + replacement: "none" + note: "The cron parse-error type id is private in v4. Use Cron.isCronParseError to narrow unknown failures." +"effect/Cron#sequenceReverse": + replacement: "Cron.prev" + note: "The reverse iterator was removed. Build an iterator that repeatedly calls Cron.prev, feeding each returned Date into the next call." +"effect/Cron#TypeId": + replacement: "none" + note: "The Cron type id is private in v4. Use Cron.isCron to identify cron values." +"effect/Cron#unsafeParse": + replacement: "Cron.parseUnsafe" + note: "The throwing parser was renamed; it also accepts an optional time zone." diff --git a/.context/effect/migration/annotations/effect__Data.yaml b/.context/effect/migration/annotations/effect__Data.yaml new file mode 100644 index 000000000..e6f0d5b62 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Data.yaml @@ -0,0 +1,33 @@ +"effect/Data#array": + replacement: "none" + note: "Use a normal copied array such as [...values]; v4 compares plain arrays structurally." +"effect/Data#case": + replacement: "none" + note: "Use an ordinary typed object or identity constructor; plain objects are structurally equal in v4." +"effect/Data#Case": + replacement: "Data.TaggedEnum.ConstructorFrom" + note: "The Case namespace was removed; its constructor helper moved under TaggedEnum." +"effect/Data#Case.Constructor": + replacement: "Data.TaggedEnum.ConstructorFrom" + note: "Use the v4 tagged-enum constructor-function type." +"effect/Data#struct": + replacement: "none" + note: "Use an ordinary object or {...value}; plain objects are structurally equal in v4." +"effect/Data#Structural": + replacement: "Data.Class" + note: "Extend Data.Class instead of the removed Structural constructor alias." +"effect/Data#TaggedEnum": + replacement: "Data.TaggedEnum" + note: "Still exported with the same record-to-discriminated-union role." +"effect/Data#TaggedEnum.GenericMatchers": + replacement: "Data.TaggedEnum.GenericMatchers" + note: "Still exported with $is and $match helpers." +"effect/Data#tuple": + replacement: "none" + note: "Use a normal tuple literal; plain tuples are structurally equal in v4." +"effect/Data#unsafeArray": + replacement: "none" + note: "Use the array directly; v4 no longer needs prototype mutation for structural equality." +"effect/Data#unsafeStruct": + replacement: "none" + note: "Use the object directly; v4 no longer needs prototype mutation for structural equality." diff --git a/.context/effect/migration/annotations/effect__DateTime.yaml b/.context/effect/migration/annotations/effect__DateTime.yaml new file mode 100644 index 000000000..f6c65c930 --- /dev/null +++ b/.context/effect/migration/annotations/effect__DateTime.yaml @@ -0,0 +1,84 @@ +"effect/DateTime#DateTime": + replacement: "DateTime.DateTime" + note: "The Utc | Zoned model remains; epochMillis fields are now epochMilliseconds and unit/part names use millisecond terminology." +"effect/DateTime#DateTime.Input": + replacement: "DateTime.DateTime.Input" + note: "The input type remains and additionally accepts Instant and InstantWithZone objects." +"effect/DateTime#DateTime.Parts": + replacement: "DateTime.DateTime.Parts" + note: "Rename millis, seconds, minutes, and hours fields to millisecond, second, minute, and hour." +"effect/DateTime#DateTime.PartsForMath": + replacement: "DateTime.DateTime.PartsForMath" + note: "Rename the millis field to milliseconds; the other plural arithmetic fields remain." +"effect/DateTime#DateTime.PartsWithWeekday": + replacement: "DateTime.DateTime.PartsWithWeekday" + note: "Rename millis, seconds, minutes, and hours fields to millisecond, second, minute, and hour." +"effect/DateTime#DateTime.Proto": + replacement: "DateTime.DateTime.Proto" + note: "The protocol remains, but its marker uses the private v4 TypeId value." +"effect/DateTime#DateTime.UnitPlural": + replacement: "DateTime.DateTime.UnitPlural" + note: "Use milliseconds instead of millis; the other plural unit strings remain." +"effect/DateTime#DateTime.UnitSingular": + replacement: "DateTime.DateTime.UnitSingular" + note: "Use millisecond instead of milli; the other singular unit strings remain." +"effect/DateTime#distanceDuration": + replacement: "Duration.millis(Math.abs(DateTime.distance(self, other)))" + note: "DateTime.distance returns signed milliseconds in v4; take the absolute value and construct a Duration to preserve v3 behavior." +"effect/DateTime#distanceDurationEither": + replacement: "DateTime.distance + Result" + note: "Compute the signed millisecond distance, wrap its absolute Duration as Result.succeed when positive and Result.fail when non-positive; v4 uses Result instead of Either." +"effect/DateTime#greaterThan": + replacement: "DateTime.isGreaterThan" + note: "The comparison was renamed with the is prefix." +"effect/DateTime#greaterThanOrEqualTo": + replacement: "DateTime.isGreaterThanOrEqualTo" + note: "The comparison was renamed with the is prefix." +"effect/DateTime#lessThan": + replacement: "DateTime.isLessThan" + note: "The comparison was renamed with the is prefix." +"effect/DateTime#lessThanOrEqualTo": + replacement: "DateTime.isLessThanOrEqualTo" + note: "The comparison was renamed with the is prefix." +"effect/DateTime#TimeZone": + replacement: "DateTime.TimeZone" + note: "The Offset | Named model remains; its public type-id marker type was removed." +"effect/DateTime#TimeZone.Proto": + replacement: "DateTime.TimeZone.Proto" + note: "The protocol remains, but its marker uses the private v4 TimeZoneTypeId value." +"effect/DateTime#TimeZoneTypeId": + replacement: "none" + note: "The time-zone type id is private in v4. Use DateTime.isTimeZone, isTimeZoneOffset, or isTimeZoneNamed." +"effect/DateTime#TypeId": + replacement: "none" + note: "The DateTime type id is private in v4. Use DateTime.isDateTime, isUtc, or isZoned." +"effect/DateTime#unsafeFromDate": + replacement: "DateTime.fromDateUnsafe" + note: "The unsafe suffix moved to the end of the constructor name." +"effect/DateTime#unsafeIsFuture": + replacement: "DateTime.isFutureUnsafe" + note: "The unsafe suffix moved to the end of the predicate name." +"effect/DateTime#unsafeIsPast": + replacement: "DateTime.isPastUnsafe" + note: "The unsafe suffix moved to the end of the predicate name." +"effect/DateTime#unsafeMake": + replacement: "DateTime.makeUnsafe" + note: "The unsafe suffix moved to the end of the constructor name." +"effect/DateTime#unsafeMakeZoned": + replacement: "DateTime.makeZonedUnsafe" + note: "The unsafe suffix moved to the end of the constructor name." +"effect/DateTime#unsafeNow": + replacement: "DateTime.nowUnsafe" + note: "The unsafe suffix moved to the end of the accessor name." +"effect/DateTime#unsafeSetZoneNamed": + replacement: "DateTime.setZoneNamedUnsafe" + note: "The unsafe suffix moved to the end of the zone setter name." +"effect/DateTime#Utc": + replacement: "DateTime.Utc" + note: "The model remains; rename epochMillis to epochMilliseconds." +"effect/DateTime#Zoned": + replacement: "DateTime.Zoned" + note: "The model remains; rename epochMillis and adjustedEpochMillis to epochMilliseconds and adjustedEpochMilliseconds." +"effect/DateTime#zoneUnsafeMakeNamed": + replacement: "DateTime.zoneMakeNamedUnsafe" + note: "The unsafe suffix moved to the end of the named-zone constructor." diff --git a/.context/effect/migration/annotations/effect__DefaultServices.yaml b/.context/effect/migration/annotations/effect__DefaultServices.yaml new file mode 100644 index 000000000..f916ca2cc --- /dev/null +++ b/.context/effect/migration/annotations/effect__DefaultServices.yaml @@ -0,0 +1,9 @@ +"effect/DefaultServices#currentServices": + replacement: "Effect.context() and Context.get(context, reference)" + note: "The aggregate FiberRef was removed; access and override default Context.Reference services individually." +"effect/DefaultServices#DefaultServices": + replacement: "none" + note: "The aggregate type and module were removed; Clock, Console, Random, ConfigProvider, and Tracer are independent defaulted references." +"effect/DefaultServices#liveServices": + replacement: "Context.empty() with individual Context.Reference defaults" + note: "There is no live-services bundle; each default service reference supplies its own live default." diff --git a/.context/effect/migration/annotations/effect__Deferred.yaml b/.context/effect/migration/annotations/effect__Deferred.yaml new file mode 100644 index 000000000..844e3029b --- /dev/null +++ b/.context/effect/migration/annotations/effect__Deferred.yaml @@ -0,0 +1,30 @@ +effect/Deferred#await: + replacement: "Deferred.await" + note: "The function remains; call it explicitly because Deferred is no longer an Effect subtype in v4." +effect/Deferred#Deferred: + replacement: "Deferred.Deferred" + note: "The model remains but is now Pipeable rather than an Effect subtype; replace yielding the Deferred itself with Deferred.await." +effect/Deferred#Deferred.Variance: + replacement: "Deferred.Deferred.Variance" + note: "The marker remains under Deferred.Deferred, but its brand uses an internal type id; ordinary code should use Deferred.Deferred directly." +effect/Deferred#DeferredTypeId: + replacement: "none" + note: "The Deferred type id is internal in v4; do not inspect or construct the brand directly." +effect/Deferred#DeferredUnify: + replacement: "none" + note: "Deferred is no longer an Effect subtype, so its Effect unification helper was removed; call Deferred.await explicitly." +effect/Deferred#DeferredUnifyIgnore: + replacement: "none" + note: "Deferred is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/Deferred#makeAs: + replacement: "Deferred.makeUnsafe" + note: "Use the synchronous v4 constructor; it no longer accepts or records a FiberId." +effect/Deferred#poll: + replacement: "Deferred.poll" + note: "The function remains and returns an Option containing the stored completion Effect." +effect/Deferred#unsafeDone: + replacement: "Deferred.doneUnsafe" + note: "The unsafe suffix moved to the end; the v4 function returns whether this call completed the Deferred." +effect/Deferred#unsafeMake: + replacement: "Deferred.makeUnsafe" + note: "The unsafe suffix moved to the end, and the v4 constructor takes no FiberId argument." diff --git a/.context/effect/migration/annotations/effect__Differ.yaml b/.context/effect/migration/annotations/effect__Differ.yaml new file mode 100644 index 000000000..c780a8413 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Differ.yaml @@ -0,0 +1,99 @@ +effect/Differ#chunk: + replacement: "Schema.toDifferJsonPatch" + note: "Derive a JSON Patch differ from a Schema for the whole Chunk; v4 no longer exposes collection-specific patch constructors." +effect/Differ#combine: + replacement: "differ.combine" + note: "Call the combine method on the Differ value directly; the standalone helper was removed." +effect/Differ#diff: + replacement: "differ.diff" + note: "Call the diff method on the Differ value directly; the standalone helper was removed." +effect/Differ#Differ: + replacement: "Differ.Differ" + note: "The interface remains, but is now an unbranded structural interface and patch takes arguments as patch(oldValue, patch)." +effect/Differ#Differ.Chunk: + replacement: "JsonPatch.JsonPatch" + note: "The Chunk patch namespace was removed; Schema.toDifferJsonPatch uses the common RFC 6902 patch representation." +effect/Differ#Differ.Chunk.Patch: + replacement: "JsonPatch.JsonPatch" + note: "Use the patch type returned by Schema.toDifferJsonPatch instead of the removed Chunk-specific patch type." +effect/Differ#Differ.Chunk.TypeId: + replacement: "none" + note: "Chunk-specific patches and their public brand were removed; do not inspect a patch type id." +effect/Differ#Differ.Context: + replacement: "Context.Context" + note: "The Context patch namespace was removed; construct and merge Context values explicitly rather than diffing environments." +effect/Differ#Differ.Context.Patch: + replacement: "none" + note: "Context patches were removed; use Context.add, Context.merge, and Context.omit to build the desired Context directly." +effect/Differ#Differ.Context.TypeId: + replacement: "none" + note: "Context patches and their public brand were removed." +effect/Differ#Differ.HashMap: + replacement: "JsonPatch.JsonPatch" + note: "The HashMap patch namespace was removed; derive a JSON Patch differ from a Schema for the complete value." +effect/Differ#Differ.HashMap.Patch: + replacement: "JsonPatch.JsonPatch" + note: "Use the patch type returned by Schema.toDifferJsonPatch instead of the removed HashMap-specific patch type." +effect/Differ#Differ.HashMap.TypeId: + replacement: "none" + note: "HashMap-specific patches and their public brand were removed; do not inspect a patch type id." +effect/Differ#Differ.HashSet.Patch: + replacement: "JsonPatch.JsonPatch" + note: "Use the patch type returned by Schema.toDifferJsonPatch instead of the removed HashSet-specific patch type." +effect/Differ#Differ.HashSet.TypeId: + replacement: "none" + note: "HashSet-specific patches and their public brand were removed; do not inspect a patch type id." +effect/Differ#Differ.Or: + replacement: "JsonPatch.JsonPatch" + note: "The Either patch namespace was removed; derive one JSON Patch differ from the Schema for the union value." +effect/Differ#Differ.Or.Patch: + replacement: "JsonPatch.JsonPatch" + note: "Use the patch type returned by Schema.toDifferJsonPatch instead of the removed Either-specific patch type." +effect/Differ#Differ.Or.TypeId: + replacement: "none" + note: "Either-specific patches and their public brand were removed; do not inspect a patch type id." +effect/Differ#Differ.ReadonlyArray: + replacement: "JsonPatch.JsonPatch" + note: "The ReadonlyArray patch namespace was removed; Schema.toDifferJsonPatch uses the common RFC 6902 patch representation." +effect/Differ#Differ.ReadonlyArray.Patch: + replacement: "JsonPatch.JsonPatch" + note: "Use the patch type returned by Schema.toDifferJsonPatch instead of the removed ReadonlyArray-specific patch type." +effect/Differ#Differ.ReadonlyArray.TypeId: + replacement: "none" + note: "ReadonlyArray-specific patches and their public brand were removed; do not inspect a patch type id." +effect/Differ#empty: + replacement: "differ.empty" + note: "Read the empty property from the Differ value directly; the standalone accessor was removed." +effect/Differ#environment: + replacement: "none" + note: "The Context differ was removed; construct the target Context explicitly with Context.add, Context.merge, and Context.omit." +effect/Differ#hashMap: + replacement: "Schema.toDifferJsonPatch" + note: "Derive a JSON Patch differ from a Schema for the whole map; v4 no longer exposes collection-specific patch constructors." +effect/Differ#hashSet: + replacement: "Schema.toDifferJsonPatch" + note: "Derive a JSON Patch differ from a Schema for the whole set; v4 no longer exposes collection-specific patch constructors." +effect/Differ#make: + replacement: "object literal satisfying Differ.Differ" + note: "Differ is structural in v4; provide empty, diff, combine, and patch methods directly, with patch(oldValue, patch) argument order." +effect/Differ#orElseEither: + replacement: "Schema.toDifferJsonPatch" + note: "Derive one differ from the Schema for the Either value; the compositional Either-specific differ and patch type were removed." +effect/Differ#patch: + replacement: "differ.patch" + note: "Call the method directly and reverse the v3 method order: differ.patch(oldValue, patch)." +effect/Differ#readonlyArray: + replacement: "Schema.toDifferJsonPatch" + note: "Derive a JSON Patch differ from a Schema for the whole array; v4 no longer exposes collection-specific patch constructors." +effect/Differ#transform: + replacement: "object literal satisfying Differ.Differ" + note: "There is no transform combinator; define a structural Differ that maps values before delegating to the original differ." +effect/Differ#TypeId: + replacement: "none" + note: "Differ is an unbranded structural interface in v4; do not inspect or implement a public type id." +effect/Differ#update: + replacement: "object literal satisfying Differ.Differ" + note: "The update constructor was removed; define empty, diff, combine, and patch directly for function patches, or use Schema.toDifferJsonPatch." +effect/Differ#updateWith: + replacement: "object literal satisfying Differ.Differ" + note: "The updateWith constructor was removed; encode the desired merge rule in a structural Differ implementation." diff --git a/.context/effect/migration/annotations/effect__Duration.yaml b/.context/effect/migration/annotations/effect__Duration.yaml new file mode 100644 index 000000000..6f9b67b73 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Duration.yaml @@ -0,0 +1,48 @@ +"effect/Duration#decode": + replacement: "Duration.fromInputUnsafe" + note: "The throwing DurationInput decoder was renamed." +"effect/Duration#decodeUnknown": + replacement: "Duration.fromInput" + note: "The safe decoder was renamed and accepts Duration.Input, returning Option." +"effect/Duration#Duration": + replacement: "Duration.Duration" + note: "The model remains and now also supports negative infinity; its type-id value is private." +"effect/Duration#DurationInput": + replacement: "Duration.Input" + note: "The input type was renamed and expanded with negative values and Temporal.Duration-like objects." +"effect/Duration#DurationValue": + replacement: "Duration.DurationValue" + note: "The tagged value remains and adds NegativeInfinity; its object fields are no longer readonly." +"effect/Duration#formatIso": + replacement: "none" + note: "ISO 8601 duration formatting was removed from the v4 Duration module. The v4 source and migration guides expose no direct replacement; retain a local formatter when this wire format is required." +"effect/Duration#fromIso": + replacement: "none" + note: "ISO 8601 duration parsing was removed from the v4 Duration module. The v4 source and migration guides expose no direct replacement; use a dedicated ISO parser and pass the resulting parts to Duration.fromInput." +"effect/Duration#greaterThan": + replacement: "Duration.isGreaterThan" + note: "The comparison was renamed with the is prefix." +"effect/Duration#greaterThanOrEqualTo": + replacement: "Duration.isGreaterThanOrEqualTo" + note: "The comparison was renamed with the is prefix." +"effect/Duration#lessThan": + replacement: "Duration.isLessThan" + note: "The comparison was renamed with the is prefix." +"effect/Duration#lessThanOrEqualTo": + replacement: "Duration.isLessThanOrEqualTo" + note: "The comparison was renamed with the is prefix." +"effect/Duration#matchWith": + replacement: "Duration.matchPair" + note: "The two-duration matcher was renamed." +"effect/Duration#TypeId": + replacement: "none" + note: "The Duration type id is private in v4. Use Duration.isDuration to narrow unknown values." +"effect/Duration#unsafeDivide": + replacement: "Duration.divideUnsafe" + note: "The unsafe prefix moved to the end of the division function name." +"effect/Duration#unsafeFormatIso": + replacement: "none" + note: "ISO 8601 duration formatting was removed from v4. The v4 Duration exports and migration guides contain no direct unsafe formatter; retain a local formatter if required." +"effect/Duration#unsafeToNanos": + replacement: "Duration.toNanosUnsafe" + note: "The unsafe prefix moved to the end of the nanosecond conversion name." diff --git a/.context/effect/migration/annotations/effect__Effect.yaml b/.context/effect/migration/annotations/effect__Effect.yaml new file mode 100644 index 000000000..330621278 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Effect.yaml @@ -0,0 +1,660 @@ +effect/Effect#acquireReleaseInterruptible: + replacement: "Effect.acquireRelease" + note: "Pass `{ interruptible: true }` in the options object. Adapt arguments and imports to the v4 API." +effect/Effect#Adapter: + replacement: "none" + note: "The generator adapter type was removed; yield Effect values directly inside `Effect.gen`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#All: + replacement: "Effect.All" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#All.ExtractMode: + replacement: "Effect.All.Return" + note: "The `either` extraction helper was removed; use `mode: \"result\"` and the v4 return helper. Adapt arguments and imports to the v4 API." +effect/Effect#allowInterrupt: + replacement: "Effect.yieldNow" + note: "Yield to the scheduler to create an interruptible checkpoint. Adapt arguments and imports to the v4 API." +effect/Effect#allSuccesses: + replacement: "Effect.all" + note: "Run with `{ mode: \"result\" }`, then retain `Result.Success` values. Adapt arguments and imports to the v4 API." +effect/Effect#allWith: + replacement: "Effect.all" + note: "Wrap `Effect.all(values, options)` in a lambda when a data-last combinator is needed. Adapt arguments and imports to the v4 API." +effect/Effect#annotateLogs: + replacement: "Effect.annotateLogs" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#ap: + replacement: "Effect.zipWith" + note: "Zip the function effect and value effect, then apply the function in the combiner. Adapt arguments and imports to the v4 API." +effect/Effect#asSomeError: + replacement: "Effect.mapError" + note: "Map errors with `Option.some`. Adapt arguments and imports to the v4 API." +effect/Effect#async: + replacement: "Effect.callback" + note: "Use the renamed callback constructor. Adapt arguments and imports to the v4 API." +effect/Effect#asyncEffect: + replacement: "Effect.callback" + note: "The callback registration may return an Effect cleanup action in v4. Adapt arguments and imports to the v4 API." +effect/Effect#bindAll: + replacement: "Effect.bind + Effect.all" + note: "Bind the result of `Effect.all` explicitly in the do-notation pipeline. Adapt arguments and imports to the v4 API." +effect/Effect#blocked: + replacement: "none" + note: "The request-runtime blocked constructor is internal; express work with `Effect.request` and a `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#Blocked: + replacement: "none" + note: "The request-runtime blocked model is internal; use public `Request` and `RequestResolver` APIs. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#cachedFunction: + replacement: "none" + note: "The function memoizer was removed; use `Cache` for keyed effectful caching. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#cacheRequestResult: + replacement: "none" + note: "Direct request-cache mutation was removed; configure request resolution through `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#catch: + replacement: "Effect.catch" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#catchAll: + replacement: "Effect.catch" + note: "Use the shortened v4 error-handler name. Adapt arguments and imports to the v4 API." +effect/Effect#catchAllCause: + replacement: "Effect.catchCause" + note: "Use the shortened v4 cause-handler name. Adapt arguments and imports to the v4 API." +effect/Effect#catchAllDefect: + replacement: "Effect.catchDefect" + note: "Use the shortened v4 defect-handler name. Adapt arguments and imports to the v4 API." +effect/Effect#catchSome: + replacement: "Effect.catchFilter" + note: "Replace the Option-returning partial function with a `Filter` and handler. Adapt arguments and imports to the v4 API." +effect/Effect#catchSomeCause: + replacement: "Effect.catchCauseFilter" + note: "Replace the Option-returning partial function with a cause `Filter` and handler. Adapt arguments and imports to the v4 API." +effect/Effect#catchSomeDefect: + replacement: "none" + note: "Use `Effect.catchDefect` and branch explicitly, re-dying for unmatched defects. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#cause: + replacement: "Effect.exit" + note: "Inspect `Exit.Failure.cause`; v4 no longer exposes an Effect-only cause extractor. Adapt arguments and imports to the v4 API." +effect/Effect#checkInterruptible: + replacement: "none" + note: "Interruptibility introspection was removed; structure the region explicitly with `Effect.interruptible` or `Effect.uninterruptible`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#clock: + replacement: "Clock.Clock" + note: "Services are Effects in v4; yield or compose `Clock.Clock` directly. Adapt imports to the v4 API." +effect/Effect#configProviderWith: + replacement: "ConfigProvider.ConfigProvider.use" + note: "Use the ConfigProvider reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API." +effect/Effect#console: + replacement: "Console.Console" + note: "Services are Effects in v4; yield or compose `Console.Console` directly. Adapt imports to the v4 API." +effect/Effect#consoleWith: + replacement: "Console.Console.use" + note: "Use the Console reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API." +effect/Effect#contextWithEffect: + replacement: "Effect.contextWith" + note: "`contextWith` accepts an effectful callback in v4. Adapt arguments and imports to the v4 API." +effect/Effect#currentPropagatedSpan: + replacement: "Effect.currentParentSpan" + note: "Use the current parent span representation. Adapt arguments and imports to the v4 API." +effect/Effect#custom: + replacement: "none" + note: "The low-level custom instruction constructor was removed; use public constructors such as `Effect.sync`, `Effect.suspend`, or `Effect.callback`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#daemonChildren: + replacement: "Effect.awaitAllChildren" + note: "Use structured child-fiber waiting, or fork explicitly with `Effect.forkDetach` when detachment is intended. Adapt arguments and imports to the v4 API." +effect/Effect#descriptor: + replacement: "Effect.fiberId" + note: "The full fiber descriptor was removed; retrieve the current numeric fiber id. Adapt arguments and imports to the v4 API." +effect/Effect#descriptorWith: + replacement: "Effect.fiberId + Effect.flatMap" + note: "Read the current fiber id and invoke the callback explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#dieMessage: + replacement: "Effect.die" + note: "Construct the desired defect explicitly and pass it to `Effect.die`. Adapt arguments and imports to the v4 API." +effect/Effect#dieSync: + replacement: "Effect.suspend + Effect.die" + note: "Evaluate the lazy defect inside `Effect.suspend`. Adapt arguments and imports to the v4 API." +effect/Effect#diffFiberRefs: + replacement: "none" + note: "The public FiberRefs diff API was removed; model fiber-local state with context references and scoped `Effect.provideService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#disconnect: + replacement: "Effect.forkDetach" + note: "Fork explicitly and decide how to await or interrupt the detached Fiber. Adapt arguments and imports to the v4 API." +effect/Effect#dropUntil: + replacement: "none" + note: "Use an explicit `Effect.gen` loop for an effectful stopping predicate. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#dropWhile: + replacement: "none" + note: "Use an explicit `Effect.gen` loop, or `Array.dropWhile` when the predicate is pure. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#Effect: + replacement: "Effect.Effect" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#Effect.AsEffect: + replacement: "Effect.Effect" + note: "Use the Effect type directly and extract channels with `Effect.Success`, `Effect.Error`, and `Effect.Services`. Adapt arguments and imports to the v4 API." +effect/Effect#Effect.Context: + replacement: "Effect.Services" + note: "Use the renamed type-level extractor for required services. Adapt arguments and imports to the v4 API." +effect/Effect#Effect.VarianceStruct: + replacement: "Effect.Variance" + note: "Use the v4 variance interface. Adapt arguments and imports to the v4 API." +effect/Effect#EffectGenerator: + replacement: "Effect.EffectIterator" + note: "Use the v4 iterator type used by generator delegation. Adapt arguments and imports to the v4 API." +effect/Effect#EffectTypeId: + replacement: "Effect.TypeId" + note: "Use the v4 type-level Effect identifier. Adapt arguments and imports to the v4 API." +effect/Effect#EffectUnify: + replacement: "Effect.EffectUnify" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#EffectUnifyIgnore: + replacement: "none" + note: "The internal unification-ignore helper is no longer public; rely on v4 Effect inference. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#either: + replacement: "Effect.result" + note: "V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API." +effect/Effect#ensureErrorType: + replacement: "Effect.satisfiesErrorType" + note: "Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API." +effect/Effect#ensureRequirementsType: + replacement: "Effect.satisfiesServicesType" + note: "Use the renamed compile-time services constraint. Adapt arguments and imports to the v4 API." +effect/Effect#ensureSuccessType: + replacement: "Effect.satisfiesSuccessType" + note: "Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API." +effect/Effect#ensuringChild: + replacement: "Effect.ensuring + Fiber APIs" + note: "Track the child Fiber explicitly and run the finalizer with `Effect.ensuring`. Adapt arguments and imports to the v4 API." +effect/Effect#ensuringChildren: + replacement: "Effect.awaitAllChildren + Effect.ensuring" + note: "Use structured child waiting and an explicit finalizer. Adapt arguments and imports to the v4 API." +effect/Effect#every: + replacement: "Effect.forEach" + note: "Evaluate predicates with `Effect.forEach`, then test the resulting booleans with `Array.every`. Adapt arguments and imports to the v4 API." +effect/Effect#exists: + replacement: "Effect.findFirst" + note: "Find the first value satisfying the effectful predicate and test the returned Option. Adapt arguments and imports to the v4 API." +effect/Effect#fiberIdWith: + replacement: "Effect.fiberId + Effect.flatMap" + note: "Read the numeric fiber id and invoke the callback explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#filterEffectOrElse: + replacement: "Effect.flatMap" + note: "Evaluate the effectful predicate and branch to `Effect.succeed` or the fallback. Adapt arguments and imports to the v4 API." +effect/Effect#filterEffectOrFail: + replacement: "Effect.flatMap" + note: "Evaluate the effectful predicate and branch to `Effect.succeed` or `Effect.fail`. Adapt arguments and imports to the v4 API." +effect/Effect#filterOrDie: + replacement: "Effect.filterOrFail + Effect.orDie" + note: "Filter with a typed failure, then convert it to a defect. Adapt arguments and imports to the v4 API." +effect/Effect#filterOrDieMessage: + replacement: "Effect.filterOrFail + Effect.orDie" + note: "Create the message-bearing error in `filterOrFail`, then convert it to a defect. Adapt arguments and imports to the v4 API." +effect/Effect#finalizersMask: + replacement: "none" + note: "Configurable finalizer execution strategies were removed; register ordered finalizers explicitly in a Scope. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#flipWith: + replacement: "Effect.flip" + note: "Flip, apply the transformation, then flip the resulting Effect back. Adapt arguments and imports to the v4 API." +effect/Effect#fn: + replacement: "Effect.fn" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#fn.Gen: + replacement: "Effect.fn.Return" + note: "Use the v4 generator-return helper type. Adapt arguments and imports to the v4 API." +effect/Effect#fn.NonGen: + replacement: "Effect.fn.Untraced" + note: "Use the v4 function helper type for non-generator wrapping. Adapt arguments and imports to the v4 API." +effect/Effect#fork: + replacement: "Effect.forkChild" + note: "Use the renamed structured child-fiber combinator. Adapt arguments and imports to the v4 API." +effect/Effect#forkAll: + replacement: "Effect.forEach + Effect.forkChild" + note: "Fork each effect explicitly, or prefer a higher-level concurrent combinator. Adapt arguments and imports to the v4 API." +effect/Effect#forkDaemon: + replacement: "Effect.forkDetach" + note: "Use the renamed detached-fiber combinator. Adapt arguments and imports to the v4 API." +effect/Effect#forkWithErrorHandler: + replacement: "Effect.forkChild + Fiber.await" + note: "Fork explicitly and observe the Fiber result to handle errors. Adapt arguments and imports to the v4 API." +effect/Effect#fromFiber: + replacement: "Fiber.join" + note: "Join the Fiber to obtain an Effect of its result. Adapt arguments and imports to the v4 API." +effect/Effect#fromFiberEffect: + replacement: "Effect.flatMap + Fiber.join" + note: "FlatMap the effectful Fiber and join it. Adapt arguments and imports to the v4 API." +effect/Effect#fromNullable: + replacement: "Effect.fromOption + Option.fromNullable" + note: "Convert the nullable value to Option, then lift it into Effect. Adapt arguments and imports to the v4 API." +effect/Effect#functionWithSpan: + replacement: "Effect.withSpan" + note: "Wrap the function body with a span whose name/options are derived from its arguments. Adapt arguments and imports to the v4 API." +effect/Effect#FunctionWithSpanOptions: + replacement: "Tracer.SpanOptions" + note: "Use the v4 tracing options type when wrapping functions with `Effect.withSpan`. Adapt arguments and imports to the v4 API." +effect/Effect#gen: + replacement: "Effect.gen" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#getFiberRefs: + replacement: "none" + note: "The FiberRefs collection is no longer public; access individual context references through Effect services. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#getRuntimeFlags: + replacement: "none" + note: "RuntimeFlags are no longer a public Effect service; use supported high-level runtime options. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#head: + replacement: "Effect.flatMap + Array.head + Effect.fromOption" + note: "Inspect the produced iterable explicitly and fail when it is empty. Adapt arguments and imports to the v4 API." +effect/Effect#if: + replacement: "Effect.suspend" + note: "Select the branch lazily with a JavaScript conditional inside `Effect.suspend`. Adapt arguments and imports to the v4 API." +effect/Effect#ignoreLogged: + replacement: "Effect.ignore" + note: "Pass `{ log: true }` to the consolidated ignore combinator. Adapt arguments and imports to the v4 API." +effect/Effect#inheritFiberRefs: + replacement: "none" + note: "Bulk FiberRef inheritance was removed; propagate required context references explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#interruptWith: + replacement: "Effect.interrupt" + note: "V4 interruption uses the current fiber identity; remove the explicit FiberId argument. Adapt arguments and imports to the v4 API." +effect/Effect#intoDeferred: + replacement: "Deferred.into" + note: "Use the Deferred module combinator. Adapt arguments and imports to the v4 API." +effect/Effect#iterate: + replacement: "none" + note: "Use an explicit stateful `Effect.gen` loop; v4 removed the Effect-specific loop helper. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#labelMetrics: + replacement: "Metric.withAttributes" + note: "Convert labels to metric attributes and scope them around the Effect. Adapt arguments and imports to the v4 API." +effect/Effect#labelMetricsScoped: + replacement: "Metric.withAttributes" + note: "Apply metric attributes to the scoped Effect rather than mutating scoped labels. Adapt arguments and imports to the v4 API." +effect/Effect#LatchUnify: + replacement: "Latch.Latch" + note: "Latch moved to the standalone `effect/Latch` module; rely on normal v4 inference. Adapt arguments and imports to the v4 API." +effect/Effect#LatchUnifyIgnore: + replacement: "none" + note: "The internal Latch unification helper was removed; use `Latch.Latch` directly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#let: + replacement: "Effect.let" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#liftPredicate: + replacement: "Effect.filterOrFail" + note: "Lift the input with `Effect.succeed` and apply `filterOrFail`. Adapt arguments and imports to the v4 API." +effect/Effect#linkSpanCurrent: + replacement: "Effect.linkSpans" + note: "Use the v4 span-link combinator. Adapt arguments and imports to the v4 API." +effect/Effect#locally: + replacement: "Effect.provideService" + note: "FiberRef values are context references in v4; provide the reference for the Effect lifetime. Adapt arguments and imports to the v4 API." +effect/Effect#locallyScoped: + replacement: "Effect.provideService" + note: "Provide the context reference around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#locallyScopedWith: + replacement: "Effect.updateServiceScoped" + note: "Context references replace FiberRefs in v4; update the reference for the current scope. Adapt arguments and imports to the v4 API." +effect/Effect#locallyWith: + replacement: "Effect.updateService" + note: "Context references replace FiberRefs in v4; update the reference around the target Effect. Adapt arguments and imports to the v4 API." +effect/Effect#logAnnotations: + replacement: "References.CurrentLogAnnotations" + note: "Context references are Effects in v4; yield or compose `References.CurrentLogAnnotations` directly. Adapt imports to the v4 API." +effect/Effect#loop: + replacement: "none" + note: "Use an explicit `Effect.gen` loop and collect results when needed. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#makeLatch: + replacement: "Latch.make" + note: "Latch constructors moved to `effect/Latch`. Adapt arguments and imports to the v4 API." +effect/Effect#makeSemaphore: + replacement: "Semaphore.make" + note: "Semaphore constructors moved to `effect/Semaphore`. Adapt arguments and imports to the v4 API." +effect/Effect#mapAccum: + replacement: "Effect.reduce" + note: "Carry `[state, output]` through an effectful reduction. Adapt arguments and imports to the v4 API." +effect/Effect#mapErrorCause: + replacement: "Effect.catchCause + Effect.failCause" + note: "Transform the Cause in a cause handler and fail with the mapped Cause. Adapt arguments and imports to the v4 API." +effect/Effect#mapInputContext: + replacement: "Effect.contextWith + Effect.provide" + note: "Build the required context from the incoming context and provide it explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#merge: + replacement: "Effect.catch" + note: "Recover each typed error with `Effect.succeed` so both channels become success values. Adapt arguments and imports to the v4 API." +effect/Effect#mergeAll: + replacement: "Effect.reduce" + note: "Reduce the input effects with an effectful accumulator. Adapt arguments and imports to the v4 API." +effect/Effect#metricLabels: + replacement: "Metric.CurrentMetricAttributes" + note: "Context references are Effects in v4; yield or compose `Metric.CurrentMetricAttributes` directly. Adapt imports to the v4 API." +effect/Effect#negate: + replacement: "Effect.map" + note: "Map the boolean result with logical negation. Adapt arguments and imports to the v4 API." +effect/Effect#none: + replacement: "Effect.flatMap + Option.match" + note: "Fail for `Some` and succeed with void for `None`. Adapt arguments and imports to the v4 API." +effect/Effect#once: + replacement: "Effect.cached" + note: "Create the cached Effect once, then execute the returned Effect repeatedly. Adapt arguments and imports to the v4 API." +effect/Effect#optionFromOptional: + replacement: "Effect.catchTag" + note: "Map success to `Option.some` and recover `NoSuchElementError` with `Option.none`. Adapt arguments and imports to the v4 API." +effect/Effect#orDieWith: + replacement: "Effect.mapError + Effect.orDie" + note: "Map the typed error to the desired defect, then convert failures to defects. Adapt arguments and imports to the v4 API." +effect/Effect#orElse: + replacement: "Effect.catch" + note: "Ignore the caught error and evaluate the fallback Effect. Adapt arguments and imports to the v4 API." +effect/Effect#orElseFail: + replacement: "Effect.mapError" + note: "Replace every typed error with the lazily produced failure value. Adapt arguments and imports to the v4 API." +effect/Effect#parallelErrors: + replacement: "Effect.all" + note: "Use `{ mode: \"result\", concurrency: \"unbounded\" }` and collect failures explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#parallelFinalizers: + replacement: "none" + note: "Parallel finalizer strategy mutation was removed; fork independent cleanup explicitly when ordering is irrelevant. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#patchFiberRefs: + replacement: "none" + note: "Bulk FiberRefs patching was removed; update individual context references with `Effect.updateService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#patchRuntimeFlags: + replacement: "none" + note: "RuntimeFlags patching was removed from the public API; use supported high-level runtime options. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#Permit: + replacement: "Semaphore.Semaphore" + note: "Use the standalone Semaphore API and its `withPermit` / `withPermits` methods. Adapt arguments and imports to the v4 API." +effect/Effect#raceWith: + replacement: "Effect.raceFirst + Fiber APIs" + note: "Use `raceFirst` for first completion, or fork both effects and inspect their Exits for custom finishers. Adapt arguments and imports to the v4 API." +effect/Effect#random: + replacement: "Random.Random" + note: "Services are Effects in v4; yield or compose `Random.Random` directly. Adapt imports to the v4 API." +effect/Effect#randomWith: + replacement: "Random.Random.use" + note: "Use the Random reference's `.use` helper to invoke the effectful callback. Prefer module-level Random operations when possible." +effect/Effect#reduceEffect: + replacement: "Effect.flatMap + Effect.reduce" + note: "Evaluate the initial Effect, then reduce the remaining effects. Adapt arguments and imports to the v4 API." +effect/Effect#reduceRight: + replacement: "Effect.reduce" + note: "Reverse the input first, then perform the effectful reduction. Adapt arguments and imports to the v4 API." +effect/Effect#reduceWhile: + replacement: "none" + note: "Use an explicit `Effect.gen` loop that checks the accumulator before each step. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#Repeat: + replacement: "Effect.Repeat" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#Repeat.Options: + replacement: "Effect.Repeat.Options" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#repeatN: + replacement: "Effect.repeat" + note: "Pass `{ times: n }` to the consolidated repeat combinator. Adapt arguments and imports to the v4 API." +effect/Effect#Retry: + replacement: "Effect.Retry" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#Retry.Options: + replacement: "Effect.Retry.Options" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#runRequestBlock: + replacement: "none" + note: "The request-runtime block runner is internal; submit requests with `Effect.request`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#runtime: + replacement: "Effect.context + Effect.runForkWith" + note: "Capture services as a Context and use the corresponding `run*With` function. Adapt arguments and imports to the v4 API." +effect/Effect#scheduleForked: + replacement: "Effect.schedule + Effect.forkScoped" + note: "Schedule the Effect, then fork it in the current Scope. Adapt arguments and imports to the v4 API." +effect/Effect#scopeWith: + replacement: "Effect.scopedWith" + note: "Use the renamed scoped callback combinator. Adapt arguments and imports to the v4 API." +effect/Effect#sequentialFinalizers: + replacement: "none" + note: "Sequential reverse-order finalization is the normal Scope behavior; remove this wrapper. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#Service: + replacement: "Context.Service" + note: "Replace the `effect` constructor option with `make`. V4 does not generate a `Default` layer or wire `dependencies`; define a `Layer.effect` and provide its dependencies explicitly." +effect/Effect#Service.AllowedType: + replacement: "Context.Service" + note: "Service type machinery moved to `Context.Service`; do not reference its internal helper types. Adapt arguments and imports to the v4 API." +effect/Effect#Service.Class: + replacement: "Context.Service" + note: "Service classes are now defined with `Context.Service`. Adapt arguments and imports to the v4 API." +effect/Effect#Service.HasArguments: + replacement: "Context.Service" + note: "Service constructor typing is handled by `Context.Service`. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeAccessors: + replacement: "Context.Service" + note: "Use the generated `.use` helper instead of v3 accessor type machinery. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeArguments: + replacement: "Context.Service" + note: "Pass a `make` Effect in the v4 `Context.Service` options. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeContext: + replacement: "Context.Service" + note: "Service context typing is inferred by `Context.Service`. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeDeps: + replacement: "Layer.provide" + note: "Compose service dependencies explicitly with Layers. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeDepsE: + replacement: "Layer.Error" + note: "Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeDepsIn: + replacement: "Layer.Services" + note: "Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeDepsOut: + replacement: "Layer.Success" + note: "Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeError: + replacement: "Layer.Error" + note: "Use the Layer error-channel extractor. Adapt arguments and imports to the v4 API." +effect/Effect#Service.MakeService: + replacement: "Context.Service" + note: "The service shape is inferred by `Context.Service`. Adapt arguments and imports to the v4 API." +effect/Effect#Service.ProhibitedType: + replacement: "Context.Service" + note: "Do not reference the removed internal validation type. Adapt arguments and imports to the v4 API." +effect/Effect#serviceConstants: + replacement: "Context.Service.use" + note: "Expose constants from the service explicitly or through the generated `use` helper. Adapt arguments and imports to the v4 API." +effect/Effect#serviceFunction: + replacement: "Context.Service.use" + note: "Use the service class `.use` helper to build an accessor function. Adapt arguments and imports to the v4 API." +effect/Effect#serviceFunctionEffect: + replacement: "Context.Service.use" + note: "Use the service class `.use` helper for effect-returning methods. Adapt arguments and imports to the v4 API." +effect/Effect#serviceFunctions: + replacement: "Context.Service.use" + note: "Define explicit service accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API." +effect/Effect#serviceMembers: + replacement: "Context.Service.use" + note: "Define explicit service accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API." +effect/Effect#serviceOptional: + replacement: "service" + note: "Services are Effects in v4; yield or compose the service key directly. Use `Effect.serviceOption` only when absence is expected." +effect/Effect#setFiberRefs: + replacement: "none" + note: "Bulk FiberRefs replacement was removed; provide individual context references. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#step: + replacement: "none" + note: "The low-level Effect stepping API was removed from the public surface. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#summarized: + replacement: "Effect.gen" + note: "Run the summary Effect before and after the target Effect and combine the two measurements explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#supervised: + replacement: "FiberSet" + note: "Track explicitly forked Fibers in a scoped `FiberSet` instead of installing a runtime Supervisor. Adapt arguments and imports to the v4 API." +effect/Effect#Tag: + replacement: "Context.Service" + note: "Define services with `Context.Service`; use the generated `.use` helper for accessors. Adapt arguments and imports to the v4 API." +effect/Effect#Tag.AllowedType: + replacement: "Context.Service" + note: "Tag validation internals were removed; use `Context.Service` directly. Adapt arguments and imports to the v4 API." +effect/Effect#Tag.ProhibitedType: + replacement: "Context.Service" + note: "Tag validation internals were removed; use `Context.Service` directly. Adapt arguments and imports to the v4 API." +effect/Effect#Tag.Proxy: + replacement: "Context.Service.use" + note: "Replace proxy accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API." +effect/Effect#tagMetrics: + replacement: "Metric.withAttributes" + note: "Convert key/value tags to metric attributes. Adapt arguments and imports to the v4 API." +effect/Effect#tagMetricsScoped: + replacement: "Metric.withAttributes" + note: "Apply attributes around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#takeUntil: + replacement: "none" + note: "Use an explicit `Effect.gen` loop for an effectful stopping predicate. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#takeWhile: + replacement: "none" + note: "Use an explicit `Effect.gen` loop, or `Array.takeWhile` when the predicate is pure. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#tapBoth: + replacement: "Effect.tapError + Effect.tap" + note: "Tap the failure path first, then tap successful values. Adapt arguments and imports to the v4 API." +effect/Effect#tapErrorCause: + replacement: "Effect.tapCause" + note: "Use the shortened v4 cause-tap name. Adapt arguments and imports to the v4 API." +effect/Effect#timedWith: + replacement: "Effect.gen" + note: "Read the supplied clock Effect before and after the target and compute the Duration explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#timeoutFail: + replacement: "Effect.timeoutOrElse" + note: "Use `Effect.fail(onTimeout())` as the timeout fallback. Adapt arguments and imports to the v4 API." +effect/Effect#timeoutFailCause: + replacement: "Effect.timeoutOrElse" + note: "Use `Effect.failCause(onTimeout())` as the timeout fallback. Adapt arguments and imports to the v4 API." +effect/Effect#timeoutTo: + replacement: "Effect.timeoutOrElse + Effect.map" + note: "Map successful values first and use the timeout fallback for `onTimeout`. Adapt arguments and imports to the v4 API." +effect/Effect#tracerWith: + replacement: "Tracer.Tracer.use" + note: "Use the Tracer reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API." +effect/Effect#transplant: + replacement: "none" + note: "Fiber scope grafting was removed; use structured concurrency with `forkChild`, `forkScoped`, or `forkIn`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#transposeMapOption: + replacement: "Option.match" + note: "Return `Effect.succeedNone` for None and map the Effect result to Some. Adapt arguments and imports to the v4 API." +effect/Effect#try: + replacement: "Effect.try" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#tryMap: + replacement: "Effect.flatMap + Effect.try" + note: "FlatMap the source value into the v4 synchronous try constructor. Adapt arguments and imports to the v4 API." +effect/Effect#tryMapPromise: + replacement: "Effect.flatMap + Effect.tryPromise" + note: "FlatMap the source value into the v4 Promise try constructor. Adapt arguments and imports to the v4 API." +effect/Effect#unless: + replacement: "Effect.suspend" + note: "Select `Effect.void` or the target Effect with a negated lazy condition. Adapt arguments and imports to the v4 API." +effect/Effect#unlessEffect: + replacement: "Effect.when" + note: "Negate the effectful boolean condition, then use the consolidated `when`. Adapt arguments and imports to the v4 API." +effect/Effect#unsafeMakeLatch: + replacement: "Latch.makeUnsafe" + note: "The unsafe constructor moved to `effect/Latch`. Adapt arguments and imports to the v4 API." +effect/Effect#unsafeMakeSemaphore: + replacement: "Semaphore.makeUnsafe" + note: "The unsafe constructor moved to `effect/Semaphore`. Adapt arguments and imports to the v4 API." +effect/Effect#unsandbox: + replacement: "Effect.catch + Effect.failCause" + note: "Treat the sandboxed Cause as an error and fail with that Cause. Adapt arguments and imports to the v4 API." +effect/Effect#updateFiberRefs: + replacement: "none" + note: "Bulk FiberRefs updates were removed; update individual context references with `Effect.updateService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#using: + replacement: "Effect.scoped + Effect.flatMap" + note: "Acquire inside a fresh Scope, run the use Effect, and close the Scope afterward. Adapt arguments and imports to the v4 API." +effect/Effect#validateAll: + replacement: "Effect.validate" + note: "Use the consolidated collection validation combinator. Adapt arguments and imports to the v4 API." +effect/Effect#validateFirst: + replacement: "Effect.firstSuccessOf" + note: "Map inputs to validation effects and select the first success; handle accumulated diagnostics explicitly if required. Adapt arguments and imports to the v4 API." +effect/Effect#validateWith: + replacement: "Effect.zipWith" + note: "Zip and combine the Effects; use `mode: \"result\"` when both failures must be retained. Adapt arguments and imports to the v4 API." +effect/Effect#whenEffect: + replacement: "Effect.when" + note: "The v4 `when` combinator accepts an effectful boolean condition directly. Adapt arguments and imports to the v4 API." +effect/Effect#whenFiberRef: + replacement: "reference.use + Effect.when" + note: "Use the Context.Reference `.use` helper to inspect the value, test it, and branch explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#whenLogLevel: + replacement: "none" + note: "Log-level conditional execution was removed; configure Logger filtering and guard optional work explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#whenRef: + replacement: "Ref.get + Effect.flatMap" + note: "Read the Ref, test it, and branch explicitly. Adapt arguments and imports to the v4 API." +effect/Effect#withClock: + replacement: "Effect.provideService" + note: "Provide `Clock.Clock` for the target Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withClockScoped: + replacement: "Effect.provideService" + note: "Provide `Clock.Clock` around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withConcurrency: + replacement: "none" + note: "Ambient concurrency was removed; pass `concurrency` directly to `Effect.all`, `Effect.forEach`, and related combinators. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withConfigProvider: + replacement: "Effect.provideService" + note: "Provide `ConfigProvider.ConfigProvider` for the target Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withConfigProviderScoped: + replacement: "Effect.provideService" + note: "Provide the ConfigProvider around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withConsole: + replacement: "Effect.provideService" + note: "Provide `Console.Console` for the target Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withConsoleScoped: + replacement: "Effect.provideService" + note: "Provide the Console service around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withEarlyRelease: + replacement: "Scope.make + Scope.close" + note: "Create a Scope explicitly, provide it to acquisition, and retain a close action. Adapt arguments and imports to the v4 API." +effect/Effect#withFiberRuntime: + replacement: "none" + note: "Direct FiberRuntime access was removed; use public Effect, Fiber, and Context operations. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withLogSpan: + replacement: "Effect.withLogSpan" + note: "Still exported in v4; update call sites for the revised signature, options, and channel inference." +effect/Effect#withMaxOpsBeforeYield: + replacement: "none" + note: "The scheduler operation budget is no longer configurable through Effect. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withMetric: + replacement: "Effect.tap + Metric.update" + note: "Update the Metric explicitly from the Effect success value. Adapt arguments and imports to the v4 API." +effect/Effect#withRandom: + replacement: "Effect.provideService" + note: "Provide `Random.Random` for the target Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withRandomFixed: + replacement: "Effect.provideService" + note: "Provide a custom deterministic `Random.Random` implementation. Adapt arguments and imports to the v4 API." +effect/Effect#withRandomScoped: + replacement: "Effect.provideService" + note: "Provide the Random service around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withRequestBatching: + replacement: "none" + note: "Ambient request batching configuration was removed; configure batching in the `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withRequestCache: + replacement: "none" + note: "Ambient request-cache replacement was removed; model keyed caching explicitly with `Cache`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withRequestCaching: + replacement: "none" + note: "Ambient request caching was removed; configure resolution or use `Cache` explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withRuntimeFlagsPatch: + replacement: "none" + note: "RuntimeFlags patching was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withRuntimeFlagsPatchScoped: + replacement: "none" + note: "Scoped RuntimeFlags patching was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withScheduler: + replacement: "none" + note: "Ambient scheduler replacement was removed; use supported runtime run options or explicit scheduling combinators. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withSchedulingPriority: + replacement: "none" + note: "Ambient fiber scheduling priority was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive." +effect/Effect#withTracerScoped: + replacement: "Effect.provideService" + note: "Provide the Tracer service around the scoped Effect. Adapt arguments and imports to the v4 API." +effect/Effect#withUnhandledErrorLogLevel: + replacement: "Effect.ignore" + note: "Handle or explicitly ignore child-fiber failures, selecting the desired log behavior at the boundary. Adapt arguments and imports to the v4 API." +effect/Effect#zipLeft: + replacement: "Effect.zip + Effect.map" + note: "Zip the Effects and select the first tuple element. Adapt arguments and imports to the v4 API." +effect/Effect#zipRight: + replacement: "Effect.andThen" + note: "Sequence the Effects and retain the second result. Adapt arguments and imports to the v4 API." diff --git a/.context/effect/migration/annotations/effect__Effectable.yaml b/.context/effect/migration/annotations/effect__Effectable.yaml new file mode 100644 index 000000000..a90d6864a --- /dev/null +++ b/.context/effect/migration/annotations/effect__Effectable.yaml @@ -0,0 +1,30 @@ +"effect/Effectable#ChannelTypeId": + replacement: "Channel.TypeId" + note: "The public channel brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol." +"effect/Effectable#Class": + replacement: "Effectable.Class" + note: "Still available; replace commit() with an override property or getter returning the Effect." +"effect/Effectable#CommitPrimitive": + replacement: "new() => Effect.Effect" + note: "The named constructor interface was removed; inline the constructor type when needed." +"effect/Effectable#CommitPrototype": + replacement: "Effectable.Prototype" + note: "Use Effectable.Prototype({ label, evaluate(fiber) { ... } }) and move the old commit body into evaluate." +"effect/Effectable#EffectPrototype": + replacement: "Effectable.Prototype" + note: "The raw multi-branded prototype was removed; use Prototype with an explicit evaluate callback." +"effect/Effectable#EffectTypeId": + replacement: "Effect.TypeId" + note: "The public Effect brand moved to Effect; v4 uses a string TypeId rather than the v3 Symbol." +"effect/Effectable#SinkTypeId": + replacement: "Sink.isSink" + note: "Sink's TypeId is private in v4; use the public guard for runtime checks and public Sink constructors for values." +"effect/Effectable#StreamTypeId": + replacement: "Stream.TypeId" + note: "The public stream brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol." +"effect/Effectable#StructuralClass": + replacement: "Effectable.Class" + note: "Use Class and migrate commit() to override; v4 equality is structural by default." +"effect/Effectable#StructuralCommitPrototype": + replacement: "Effectable.Prototype" + note: "Use Prototype with evaluate; a separate structural prototype is unnecessary because v4 equality is structural by default." diff --git a/.context/effect/migration/annotations/effect__Either.yaml b/.context/effect/migration/annotations/effect__Either.yaml new file mode 100644 index 000000000..02c6510a1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Either.yaml @@ -0,0 +1,105 @@ +"effect/Either#all": + replacement: "Result.all" + note: "Either moved to Result; collection behavior is retained with Failure and Success terminology." +"effect/Either#ap": + replacement: "Result.flatMap" + note: "Use Result.flatMap(self, (f) => Result.map(that, f)); v4 has no Result.ap." +"effect/Either#bind": + replacement: "Result.bind" + note: "The do-notation combinator moved to Result." +"effect/Either#bindTo": + replacement: "Result.bindTo" + note: "The do-notation combinator moved to Result." +"effect/Either#Do": + replacement: "Result.Do" + note: "The empty successful do-notation value moved to Result." +"effect/Either#Either": + replacement: "Result.Result" + note: "Either became Result; Right and Left became Success and Failure." +"effect/Either#Either.Left": + replacement: "Result.Result.Failure" + note: "Use the Result namespace extractor for the failure variant." +"effect/Either#Either.Right": + replacement: "Result.Result.Success" + note: "Use the Result namespace extractor for the success variant." +"effect/Either#EitherTypeLambda": + replacement: "Result.ResultTypeLambda" + note: "Moved and renamed with Result." +"effect/Either#EitherUnify": + replacement: "Result.ResultUnify" + note: "Moved and renamed with Result." +"effect/Either#EitherUnifyIgnore": + replacement: "Result.ResultUnifyIgnore" + note: "Moved and renamed with Result." +"effect/Either#filterOrLeft": + replacement: "Result.filterOrFail" + note: "Left is now Failure, so the predicate combinator is filterOrFail." +"effect/Either#flip": + replacement: "Result.flip" + note: "The channel-swapping combinator moved to Result." +"effect/Either#fromNullable": + replacement: "Result.fromNullishOr" + note: "Renamed with v4 nullish-or terminology." +"effect/Either#getEquivalence": + replacement: "Result.makeEquivalence" + note: "Pass success and failure equivalences positionally instead of a right and left object." +"effect/Either#getLeft": + replacement: "Result.getFailure" + note: "Extract the Result failure as an Option." +"effect/Either#getOrElse": + replacement: "Result.getOrElse" + note: "Moved unchanged to Result." +"effect/Either#getOrThrow": + replacement: "Result.getOrThrow" + note: "V4 throws the raw Failure value; use getOrThrowWith when a custom Error is required." +"effect/Either#getOrThrowWith": + replacement: "Result.getOrThrowWith" + note: "Moved to Result; the callback receives the Failure value." +"effect/Either#getOrUndefined": + replacement: "Result.getOrUndefined" + note: "Moved unchanged to Result." +"effect/Either#getRight": + replacement: "Result.getSuccess" + note: "Extract the Result success as an Option." +"effect/Either#isEither": + replacement: "Result.isResult" + note: "Renamed with the data type." +"effect/Either#isLeft": + replacement: "Result.isFailure" + note: "Left is now the Failure variant." +"effect/Either#isRight": + replacement: "Result.isSuccess" + note: "Right is now the Success variant." +"effect/Either#left": + replacement: "Result.fail" + note: "Construct a Failure with Result.fail." +"effect/Either#Left": + replacement: "Result.Failure" + note: "Left became Failure; .left became .failure." +"effect/Either#let": + replacement: "Result.let" + note: "The do-notation combinator moved to Result." +"effect/Either#map": + replacement: "Result.map" + note: "Map now transforms the Success channel." +"effect/Either#mapLeft": + replacement: "Result.mapError" + note: "Left mapping became failure-channel error mapping." +"effect/Either#match": + replacement: "Result.match" + note: "Rename handlers from onLeft and onRight to onFailure and onSuccess." +"effect/Either#right": + replacement: "Result.succeed" + note: "Construct a Success with Result.succeed." +"effect/Either#Right": + replacement: "Result.Success" + note: "Right became Success; .right became .success." +"effect/Either#try": + replacement: "Result.try" + note: "The synchronous throwable constructor moved to Result." +"effect/Either#TypeId": + replacement: "none" + note: "Result keeps its brand private and exports no public TypeId." +"effect/Either#void": + replacement: "Result.void" + note: "Use the prebuilt successful Result." diff --git a/.context/effect/migration/annotations/effect__Encoding.yaml b/.context/effect/migration/annotations/effect__Encoding.yaml new file mode 100644 index 000000000..9b1508e3e --- /dev/null +++ b/.context/effect/migration/annotations/effect__Encoding.yaml @@ -0,0 +1,24 @@ +"effect/Encoding#DecodeException": + replacement: "Encoding.EncodingError" + note: "Use the unified error class with kind Decode." +"effect/Encoding#DecodeExceptionTypeId": + replacement: "Encoding.EncodingErrorTypeId" + note: "Decode and encode failures now share one marker." +"effect/Encoding#decodeUriComponent": + replacement: "Result.try" + note: "Wrap decodeURIComponent in Result.try and map failure to EncodingError, or decode Schema.StringFromUriComponent." +"effect/Encoding#EncodeException": + replacement: "Encoding.EncodingError" + note: "Use the unified error class with kind Encode." +"effect/Encoding#EncodeExceptionTypeId": + replacement: "Encoding.EncodingErrorTypeId" + note: "Decode and encode failures now share one marker." +"effect/Encoding#encodeUriComponent": + replacement: "Result.try" + note: "Wrap encodeURIComponent in Result.try and map failure to EncodingError, or encode Schema.StringFromUriComponent." +"effect/Encoding#isDecodeException": + replacement: "Encoding.isEncodingError" + note: "Use the unified guard and test kind === Decode when decode-only narrowing is required." +"effect/Encoding#isEncodeException": + replacement: "Encoding.isEncodingError" + note: "Use the unified guard and test kind === Encode when encode-only narrowing is required." diff --git a/.context/effect/migration/annotations/effect__Equal.yaml b/.context/effect/migration/annotations/effect__Equal.yaml new file mode 100644 index 000000000..34601828d --- /dev/null +++ b/.context/effect/migration/annotations/effect__Equal.yaml @@ -0,0 +1,3 @@ +"effect/Equal#equivalence": + replacement: "Equal.asEquivalence" + note: "Direct rename. The returned equivalence now follows v4 structural equality, including NaN equality and cached comparisons for immutable objects." diff --git a/.context/effect/migration/annotations/effect__Equivalence.yaml b/.context/effect/migration/annotations/effect__Equivalence.yaml new file mode 100644 index 000000000..7b898a5b0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Equivalence.yaml @@ -0,0 +1,42 @@ +"effect/Equivalence#all": + replacement: "Equivalence.Tuple([...collection])" + note: "Materialize the comparator iterable for Tuple. Unlike v3 prefix comparison, v4 requires equal input lengths; use Equivalence.make for intentional prefix semantics." +"effect/Equivalence#array": + replacement: "Equivalence.Array" + note: "Capitalized constructor name; positional equal-length array comparison is unchanged." +"effect/Equivalence#bigint": + replacement: "Equivalence.BigInt" + note: "Capitalized instance name; strict bigint equality is unchanged." +"effect/Equivalence#boolean": + replacement: "Equivalence.Boolean" + note: "Capitalized instance name; strict boolean equality is unchanged." +"effect/Equivalence#combineMany": + replacement: "Equivalence.combine(self, Equivalence.combineAll(collection))" + note: "Compose combine with combineAll; the dedicated dual combineMany helper was removed." +"effect/Equivalence#Equivalence": + replacement: "Equivalence.Equivalence" + note: "The callable type is retained but is now a type alias, so declaration merging is no longer supported." +"effect/Equivalence#number": + replacement: "Equivalence.Number" + note: "Capitalized instance name. V4 considers NaN equivalent to NaN; use Equivalence.strictEqual() for exact v3 strict-equality behavior." +"effect/Equivalence#product": + replacement: "Equivalence.Tuple([self, that])" + note: "Replace the dual two-comparator helper with the single-array Tuple constructor." +"effect/Equivalence#productMany": + replacement: "Equivalence.Tuple([self, ...collection])" + note: "Materialize the comparator iterable in one Tuple call; v4 rejects unequal input lengths instead of using v3 prefix semantics." +"effect/Equivalence#strict": + replacement: "Equivalence.strictEqual" + note: "Renamed strict-equality constructor; call as Equivalence.strictEqual()." +"effect/Equivalence#string": + replacement: "Equivalence.String" + note: "Capitalized instance name; case-sensitive strict equality is unchanged." +"effect/Equivalence#struct": + replacement: "Equivalence.Struct" + note: "Capitalized constructor name. V4 also compares configured symbol and non-enumerable keys via Reflect.ownKeys." +"effect/Equivalence#symbol": + replacement: "Equivalence.strictEqual()" + note: "There is no Symbol instance export; strictEqual preserves the v3 symbol comparison." +"effect/Equivalence#tuple": + replacement: "Equivalence.Tuple([eqA, eqB, ...])" + note: "Capitalized constructor now takes one comparator array instead of rest arguments and rejects unequal input lengths." diff --git a/.context/effect/migration/annotations/effect__ExecutionPlan.yaml b/.context/effect/migration/annotations/effect__ExecutionPlan.yaml new file mode 100644 index 000000000..bd73e986f --- /dev/null +++ b/.context/effect/migration/annotations/effect__ExecutionPlan.yaml @@ -0,0 +1,9 @@ +effect/ExecutionPlan#ExecutionPlan: + replacement: "ExecutionPlan.ExecutionPlan" + note: "The plan type remains; withRequirements was renamed to captureRequirements." +effect/ExecutionPlan#make: + replacement: "ExecutionPlan.make" + note: "The variadic execution-plan constructor remains unchanged." +effect/ExecutionPlan#TypesBase: + replacement: "ExecutionPlan.ConfigBase" + note: "The base type for execution-plan step configuration was renamed." diff --git a/.context/effect/migration/annotations/effect__ExecutionStrategy.yaml b/.context/effect/migration/annotations/effect__ExecutionStrategy.yaml new file mode 100644 index 000000000..831f31afa --- /dev/null +++ b/.context/effect/migration/annotations/effect__ExecutionStrategy.yaml @@ -0,0 +1,33 @@ +effect/ExecutionStrategy#ExecutionStrategy: + replacement: "Types.Concurrency | Scope.ExecutionStrategy" + note: "The ADT was removed; use number | unbounded for operation concurrency, or sequential | parallel for Scope finalizers." +effect/ExecutionStrategy#isParallel: + replacement: "strategy === \"parallel\"" + note: "Compare the Scope strategy directly; for concurrency options compare with unbounded." +effect/ExecutionStrategy#isParallelN: + replacement: "typeof concurrency === \"number\"" + note: "Bounded parallelism is represented directly by a numeric concurrency value." +effect/ExecutionStrategy#isSequential: + replacement: "strategy === \"sequential\"" + note: "Compare the Scope strategy directly; for operation concurrency use the value 1." +effect/ExecutionStrategy#match: + replacement: "switch" + note: "Use ordinary branching over the consumer-specific concurrency or Scope strategy primitive." +effect/ExecutionStrategy#parallel: + replacement: "\"parallel\" | \"unbounded\"" + note: "Use parallel for Scope finalizers or unbounded for operation concurrency." +effect/ExecutionStrategy#Parallel: + replacement: "\"parallel\" | \"unbounded\"" + note: "The tagged case was removed; use the consumer-specific primitive value." +effect/ExecutionStrategy#parallelN: + replacement: "number" + note: "Pass the parallelism directly as a numeric concurrency option; Scope has no bounded parallel strategy." +effect/ExecutionStrategy#ParallelN: + replacement: "number" + note: "The tagged case was removed; bounded operation concurrency is represented directly by a number." +effect/ExecutionStrategy#sequential: + replacement: "\"sequential\" | 1" + note: "Use sequential for Scope finalizers or 1 for operation concurrency." +effect/ExecutionStrategy#Sequential: + replacement: "\"sequential\" | 1" + note: "The tagged case was removed; use the consumer-specific primitive value." diff --git a/.context/effect/migration/annotations/effect__Exit.yaml b/.context/effect/migration/annotations/effect__Exit.yaml new file mode 100644 index 000000000..5644d4368 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Exit.yaml @@ -0,0 +1,72 @@ +effect/Exit#all: + replacement: "Exit.asVoidAll + Exit.isSuccess + Option.some / Option.none" + note: "No direct value-collecting v4 equivalent. Materialize the iterable once; return Option.none for empty input, use Exit.asVoidAll to combine every failure, and otherwise collect each Success.value into Exit.succeed and wrap it in Option.some. The parallel option is gone because v4 Cause flattens sequential and parallel composition." +effect/Exit#as: + replacement: "Exit.map" + note: "Replace with Exit.map(self, () => value); this preserves a failed Exit and returns Exit data rather than a general Effect." +effect/Exit#causeOption: + replacement: "Exit.getCause" + note: "Direct rename; still returns Option.some(cause) for Failure and Option.none for Success." +effect/Exit#exists: + replacement: "Exit.isSuccess" + note: "No direct v4 combinator; use Exit.isSuccess(self) && predicate(self.value). If callers rely on the refinement overload, retain an explicitly typed wrapper returning self is Exit.Exit." +effect/Exit#Exit: + replacement: "Exit.Exit" + note: "Still exported as Exit.Exit = Exit.Success | Exit.Failure; v4 variants share Exit.Exit.Proto and remain Effect values." +effect/Exit#ExitUnify: + replacement: "none" + note: "Removed type-level implementation hook; delete direct references. V4 Success and Failure inherit Exit.Exit.Proto, but no exported Exit-specific Unify interface replaces this API." +effect/Exit#ExitUnifyIgnore: + replacement: "none" + note: "Removed type-level implementation hook; delete direct references. V4 Success and Failure inherit Exit.Exit.Proto, but no exported Exit-specific Unify interface replaces this API." +effect/Exit#Failure: + replacement: "Exit.Failure" + note: "Still exported with _tag Failure and cause; it now extends Exit.Exit.Proto and no longer exposes the v3 _op, effect_instruction_i0, or Exit-specific Unify fields." +effect/Exit#flatMapEffect: + replacement: "Effect.matchCauseEffectEager" + note: "Use Effect.matchCauseEffectEager(self, { onFailure: cause => Effect.succeed(Exit.failCause(cause)), onSuccess: f }). The explicit failure branch is required because v3 preserved an input Failure as a successful outer Effect; plain Effect.flatMap would instead fail the outer Effect." +effect/Exit#flatten: + replacement: "Exit.match" + note: "No direct v4 Exit flatten; use Exit.match(self, { onFailure: Exit.failCause, onSuccess: identity }) to return the inner Exit on success and preserve an outer failure as Exit data." +effect/Exit#forEachEffect: + replacement: "Effect.flatMapEager + Effect.exit" + note: "Use Effect.exit(Effect.flatMapEager(self, f)). This captures both the original Exit failure and failures from f into the returned Exit while keeping the outer Effect infallible; flatMapEager preserves v3's eager callback selection for an already-resolved Exit." +effect/Exit#fromEither: + replacement: "Result.match + Exit.fail / Exit.succeed" + note: "V3 Either is v4 Result. Convert with Result.match(result, { onFailure: Exit.fail, onSuccess: Exit.succeed }); there is no v4 Exit.fromResult constructor." +effect/Exit#fromOption: + replacement: "Option.match + Exit.fail / Exit.succeed" + note: "Use Option.match(option, { onNone: () => Exit.fail(undefined), onSome: Exit.succeed }) to preserve v3's Exit contract. Exit.findErrorOption is an accessor and is not a replacement." +effect/Exit#getOrElse: + replacement: "Exit.match" + note: "Use Exit.match(self, { onFailure: orElse, onSuccess: identity }); onFailure still receives the full Cause." +effect/Exit#isInterrupted: + replacement: "Exit.hasInterrupts" + note: "Direct semantic rename; true for a Failure whose Cause contains at least one Interrupt reason, false for Success." +effect/Exit#mapErrorCause: + replacement: "Exit.match + Exit.failCause / Exit.succeed" + note: "No direct v4 combinator. Use Exit.match(self, { onFailure: cause => Exit.failCause(f(cause)), onSuccess: Exit.succeed }); f now receives the flattened v4 Cause representation. Cause.map is only equivalent when f merely maps typed errors." +effect/Exit#matchEffect: + replacement: "Effect.matchCauseEffectEager" + note: "Direct cause-aware migration because Exit is an Effect in v4. Use the same onFailure/onSuccess handlers; the Eager variant preserves v3's immediate branch selection for resolved Exit values." +effect/Exit#Success: + replacement: "Exit.Success" + note: "Still exported with _tag Success and value; it now extends Exit.Exit.Proto, defaults E to never, and no longer exposes the v3 _op, effect_instruction_i0, or Exit-specific Unify fields." +effect/Exit#zipLeft: + replacement: "Exit.asVoidAll" + note: "Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : self. This retains the left success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition." +effect/Exit#zipPar: + replacement: "Exit.asVoidAll + Exit.succeed" + note: "No direct v4 Exit pair combinator. Check Exit.asVoidAll([self, that]); return its Failure, or after narrowing both inputs to Success return Exit.succeed([self.value, that.value]). V4 Cause.combine has no parallel marker." +effect/Exit#zipParLeft: + replacement: "Exit.asVoidAll" + note: "Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : self. This retains the left success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition." +effect/Exit#zipParRight: + replacement: "Exit.asVoidAll" + note: "Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : that. This retains the right success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition." +effect/Exit#zipRight: + replacement: "Exit.asVoidAll" + note: "Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : that. This retains the right success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition." +effect/Exit#zipWith: + replacement: "Exit.match" + note: "No direct v4 equivalent. Nested-match both Exits: preserve a lone failure cause, call options.onFailure and Exit.failCause only when both fail, and call Exit.succeed(options.onSuccess(a, b)) when both succeed." diff --git a/.context/effect/migration/annotations/effect__FastCheck.yaml b/.context/effect/migration/annotations/effect__FastCheck.yaml new file mode 100644 index 000000000..0a1868b0a --- /dev/null +++ b/.context/effect/migration/annotations/effect__FastCheck.yaml @@ -0,0 +1,88 @@ +"effect/FastCheck#ascii": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. fast-check v4 replaced character arbitraries with string units." + example: "FastCheck.string({ unit: \"binary-ascii\", minLength: 1, maxLength: 1 })" +"effect/FastCheck#asciiString": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Use the binary-ascii string unit." + example: "FastCheck.string({ ...constraints, unit: \"binary-ascii\" })" +"effect/FastCheck#base64": + replacement: "FastCheck.constantFrom" + note: "Import FastCheck from effect/testing. Generate one base64 alphabet character; base64String remains for complete encoded strings." + example: "FastCheck.constantFrom(...\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/\")" +"effect/FastCheck#bigIntN": + replacement: "FastCheck.bigInt" + note: "Import FastCheck from effect/testing. Express the signed bit range with min and max constraints." +"effect/FastCheck#bigUint": + replacement: "FastCheck.bigInt" + note: "Import FastCheck from effect/testing. Use a minimum of 0n and the previous maximum." + example: "FastCheck.bigInt({ min: 0n, max })" +"effect/FastCheck#BigUintConstraints": + replacement: "FastCheck.BigIntConstraints" + note: "Import FastCheck from effect/testing. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n." +"effect/FastCheck#bigUintN": + replacement: "FastCheck.bigInt" + note: "Import FastCheck from effect/testing. Express the unsigned bit range with min and max constraints." + example: "FastCheck.bigInt({ min: 0n, max: (1n << BigInt(n)) - 1n })" +"effect/FastCheck#char": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Use a one-unit printable ASCII string." + example: "FastCheck.string({ unit: \"grapheme-ascii\", minLength: 1, maxLength: 1 })" +"effect/FastCheck#char16bits": + replacement: "FastCheck.nat" + note: "Import FastCheck from effect/testing. Map a 16-bit natural number through String.fromCharCode." + example: "FastCheck.nat({ max: 0xffff }).map(String.fromCharCode)" +"effect/FastCheck#check": + replacement: "FastCheck.check" + note: "Import FastCheck from effect/testing. The runner remains, but RunDetails.error was replaced by errorInstance in fast-check v4." +"effect/FastCheck#constant": + replacement: "FastCheck.constant" + note: "Import FastCheck from effect/testing. The API remains; v4 infers literal types by default." +"effect/FastCheck#context": + replacement: "FastCheck.context" + note: "Import FastCheck from effect/testing. The API is otherwise unchanged." +"effect/FastCheck#fullUnicode": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Use a one-unit binary Unicode string." + example: "FastCheck.string({ unit: \"binary\", minLength: 1, maxLength: 1 })" +"effect/FastCheck#fullUnicodeString": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Use the binary string unit." + example: "FastCheck.string({ ...constraints, unit: \"binary\" })" +"effect/FastCheck#hexa": + replacement: "FastCheck.integer" + note: "Import FastCheck from effect/testing. Map an integer from 0 through 15 to a hexadecimal character." +"effect/FastCheck#hexaString": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Pass a hexadecimal-character arbitrary as the string unit." +"effect/FastCheck#stream": + replacement: "FastCheck.stream" + note: "Import FastCheck from effect/testing. The API remains; update custom generator and Random implementations for fast-check v4 typings." +"effect/FastCheck#string16bits": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Pass a char16bits-compatible arbitrary as the string unit." +"effect/FastCheck#stringOf": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Pass the former character arbitrary as the unit constraint." + example: "FastCheck.string({ ...constraints, unit: arbitrary })" +"effect/FastCheck#unicode": + replacement: "FastCheck.integer" + note: "Import FastCheck from effect/testing. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode." +"effect/FastCheck#unicodeJson": + replacement: "FastCheck.json" + note: "Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit." + example: "FastCheck.json({ stringUnit: \"binary\" })" +"effect/FastCheck#UnicodeJsonSharedConstraints": + replacement: "FastCheck.JsonSharedConstraints" + note: "Import FastCheck from effect/testing. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit." +"effect/FastCheck#unicodeJsonValue": + replacement: "FastCheck.jsonValue" + note: "Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit." + example: "FastCheck.jsonValue({ stringUnit: \"binary\" })" +"effect/FastCheck#unicodeString": + replacement: "FastCheck.string" + note: "Import FastCheck from effect/testing. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode." +"effect/FastCheck#uuidV": + replacement: "FastCheck.uuid" + note: "Import FastCheck from effect/testing. Specify the UUID version through constraints." + example: "FastCheck.uuid({ version: 4 })" diff --git a/.context/effect/migration/annotations/effect__Fiber.yaml b/.context/effect/migration/annotations/effect__Fiber.yaml new file mode 100644 index 000000000..659066b25 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Fiber.yaml @@ -0,0 +1,144 @@ +effect/Fiber#all: + replacement: "Fiber.joinAll" + note: "Composite fibers were removed; join the iterable directly to obtain an Effect of all results." +effect/Fiber#await: + replacement: "Fiber.await" + note: "Unchanged; it returns an Effect containing the fiber Exit." +effect/Fiber#children: + replacement: "none" + note: "V4 fibers do not expose child-fiber enumeration; keep explicit handles in FiberSet or FiberMap when tracking is required." +effect/Fiber#done: + replacement: "Effect.runFork" + note: "Exit is an Effect in v4, so pass the Exit to Effect.runFork when a completed Fiber handle is required." +effect/Fiber#dumpAll: + replacement: "none" + note: "Fiber dump and global diagnostic APIs were removed; retain explicit fibers and inspect id and pollUnsafe where needed." +effect/Fiber#fail: + replacement: "Effect.runFork(Effect.fail(error))" + note: "Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required." +effect/Fiber#failCause: + replacement: "Effect.runFork(Effect.failCause(cause))" + note: "Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required." +effect/Fiber#Fiber: + replacement: "Fiber.Fiber" + note: "The v4 Fiber is the concrete runtime handle and is no longer itself an Effect; use Fiber.join or Fiber.await." +effect/Fiber#Fiber.Descriptor: + replacement: "none" + note: "Descriptors were removed; use Effect.withFiber for the current Fiber and read its id or runtime fields directly." +effect/Fiber#Fiber.Dump: + replacement: "none" + note: "Fiber dumps were removed; retain explicit Fiber handles and inspect their public runtime fields." +effect/Fiber#Fiber.Runtime: + replacement: "Fiber.Fiber" + note: "RuntimeFiber and the Fiber.Runtime alias were collapsed into the single v4 Fiber type." +effect/Fiber#Fiber.RuntimeVariance: + replacement: "Fiber.Variance" + note: "RuntimeFiber was collapsed into Fiber, leaving one variance encoding." +effect/Fiber#Fiber.Variance: + replacement: "Fiber.Variance" + note: "Retained as the variance encoding on the v4 Fiber interface." +effect/Fiber#FiberTypeId: + replacement: "Fiber.isFiber" + note: "The type-id symbol is private in v4; use the public Fiber.isFiber guard." +effect/Fiber#FiberUnify: + replacement: "none" + note: "Fiber no longer extends Effect, so its Effect unification helper was removed." +effect/Fiber#FiberUnifyIgnore: + replacement: "none" + note: "Fiber no longer extends Effect, so its Effect unification helper was removed." +effect/Fiber#fromEffect: + replacement: "Effect.runFork" + note: "V4 uses concrete runtime fibers; run the Effect directly, or keep using the Effect when no handle is needed." +effect/Fiber#getCurrentFiber: + replacement: "Fiber.getCurrent" + note: "Renamed and now returns Fiber | undefined synchronously instead of Option." +effect/Fiber#id: + replacement: "fiber.id" + note: "Fiber IDs are numbers exposed by the readonly id field." +effect/Fiber#inheritAll: + replacement: "none" + note: "FiberRef inheritance was removed with FiberRef; Context.Reference values are inherited through fiber context automatically." +effect/Fiber#interruptAsFork: + replacement: "fiber.interruptUnsafe(fiberId)" + note: "For fire-and-forget interruption use the immediate runtime hook; use Fiber.interruptAs when cleanup must be awaited." +effect/Fiber#interrupted: + replacement: "Effect.runFork(Exit.interrupt(fiberId))" + note: "Synthetic Fiber constructors were removed; Exit is an Effect and can be run to obtain an interrupted Fiber." +effect/Fiber#interruptFork: + replacement: "fiber.interruptUnsafe()" + note: "Use the immediate runtime hook for fire-and-forget interruption; Fiber.interrupt waits for cleanup." +effect/Fiber#isRuntimeFiber: + replacement: "Fiber.isFiber" + note: "All v4 Fiber values are concrete runtime fibers, so only the general guard remains." +effect/Fiber#map: + replacement: "Effect.runFork(Effect.map(Fiber.join(fiber), f))" + note: "Fiber transformation combinators were removed; transform its joined Effect and fork only if another handle is required." +effect/Fiber#mapEffect: + replacement: "Effect.runFork(Effect.flatMap(Fiber.join(fiber), f))" + note: "Fiber transformation combinators were removed; transform its joined Effect and fork only if another handle is required." +effect/Fiber#mapFiber: + replacement: "Effect.flatMap(Fiber.join(fiber), (a) => Fiber.join(f(a)))" + note: "Flatten through Fiber.join; fork the resulting Effect if another Fiber handle is required." +effect/Fiber#match: + replacement: "none" + note: "The virtual Fiber versus RuntimeFiber distinction no longer exists, so branch-specific matching is unnecessary." +effect/Fiber#never: + replacement: "Effect.runFork(Effect.never)" + note: "Synthetic Fiber constants were removed; run Effect.never when a never-completing Fiber is required." +effect/Fiber#Order: + replacement: "Order.mapInput(Order.Number, (fiber) => fiber.id)" + note: "The built-in Fiber order was removed; derive an order from the numeric id when ordering is actually required." +effect/Fiber#orElse: + replacement: "Effect.runFork(Effect.catchCause(Fiber.join(self), () => Fiber.join(that)))" + note: "Compose joined Effects and fork the result only if another Fiber handle is required." +effect/Fiber#orElseEither: + replacement: "Effect.catchCause" + note: "Compose Fiber.join Effects explicitly and map each successful branch to your own tagged union; Either was also removed in v4." +effect/Fiber#poll: + replacement: "fiber.pollUnsafe()" + note: "Polling is now synchronous and returns Exit | undefined; wrap in Effect.sync and Option.fromUndefinedOr if the old shape is required." +effect/Fiber#pretty: + replacement: "none" + note: "Runtime fiber pretty-printing was removed; format the public id and polled Exit explicitly." +effect/Fiber#roots: + replacement: "none" + note: "The runtime no longer exposes a global root-fiber registry; track application fibers explicitly." +effect/Fiber#RuntimeFiber: + replacement: "Fiber.Fiber" + note: "RuntimeFiber and Fiber were collapsed into the single v4 Fiber interface." +effect/Fiber#RuntimeFiberTypeId: + replacement: "Fiber.isFiber" + note: "The separate RuntimeFiber marker was removed; use the public Fiber guard." +effect/Fiber#RuntimeFiberUnify: + replacement: "none" + note: "RuntimeFiber was collapsed into Fiber, which no longer participates in Effect unification." +effect/Fiber#RuntimeFiberUnifyIgnore: + replacement: "none" + note: "RuntimeFiber was collapsed into Fiber, which no longer participates in Effect unification." +effect/Fiber#scoped: + replacement: "Fiber.runIn" + note: "Register the Fiber in an explicit Scope with Fiber.runIn; acquire the current Scope when migrating the old effectful form." +effect/Fiber#status: + replacement: "fiber.pollUnsafe()" + note: "FiberStatus was removed; undefined means not completed and an Exit means completed, with no public running/suspended distinction." +effect/Fiber#succeed: + replacement: "Effect.runFork(Effect.succeed(value))" + note: "Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required." +effect/Fiber#unsafeRoots: + replacement: "none" + note: "The runtime no longer exposes a global root-fiber registry; track application fibers explicitly." +effect/Fiber#void: + replacement: "Effect.runFork(Effect.void)" + note: "Synthetic Fiber constants were removed; run Effect.void when a completed Fiber is required." +effect/Fiber#zip: + replacement: "Effect.runFork(Effect.zip(Fiber.join(self), Fiber.join(that)))" + note: "Compose joined Effects and fork the result only if another Fiber handle is required." +effect/Fiber#zipLeft: + replacement: "Effect.runFork(Effect.map(Effect.zip(Fiber.join(self), Fiber.join(that)), ([left]) => left))" + note: "V4 has no Effect.zipLeft; zip joined Effects, project the left value, and fork only if another handle is required." +effect/Fiber#zipRight: + replacement: "Effect.runFork(Effect.map(Effect.zip(Fiber.join(self), Fiber.join(that)), ([, right]) => right))" + note: "V4 has no Effect.zipRight; zip joined Effects, project the right value, and fork only if another handle is required." +effect/Fiber#zipWith: + replacement: "Effect.runFork(Effect.zipWith(Fiber.join(self), Fiber.join(that), f))" + note: "Compose joined Effects and fork the result only if another Fiber handle is required." diff --git a/.context/effect/migration/annotations/effect__FiberHandle.yaml b/.context/effect/migration/annotations/effect__FiberHandle.yaml new file mode 100644 index 000000000..bd7bdb5c7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberHandle.yaml @@ -0,0 +1,15 @@ +effect/FiberHandle#FiberHandle: + replacement: "FiberHandle.FiberHandle" + note: "Retained; contained runtime fibers now use the unified Fiber type." +effect/FiberHandle#get: + replacement: "FiberHandle.get" + note: "Retained, but v4 returns Effect> instead of failing with NoSuchElementException when empty." +effect/FiberHandle#TypeId: + replacement: "FiberHandle.isFiberHandle" + note: "The type-id symbol is private in v4; use the public guard." +effect/FiberHandle#unsafeGet: + replacement: "FiberHandle.getUnsafe" + note: "Renamed to put the Unsafe suffix last." +effect/FiberHandle#unsafeSet: + replacement: "FiberHandle.setUnsafe" + note: "Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details." diff --git a/.context/effect/migration/annotations/effect__FiberId.yaml b/.context/effect/migration/annotations/effect__FiberId.yaml new file mode 100644 index 000000000..91e0fc410 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberId.yaml @@ -0,0 +1,63 @@ +effect/FiberId#combine: + replacement: "none" + note: "Composite FiberId values were removed; v4 uses a single numeric fiber id." +effect/FiberId#combineAll: + replacement: "none" + note: "Composite FiberId values were removed; v4 uses a single numeric fiber id." +effect/FiberId#composite: + replacement: "none" + note: "Composite FiberId values were removed; v4 uses a single numeric fiber id." +effect/FiberId#Composite: + replacement: "none" + note: "Composite FiberId values were removed; v4 uses a single numeric fiber id." +effect/FiberId#FiberId: + replacement: "number" + note: "V4 represents a fiber identity as the numeric Fiber.id field." +effect/FiberId#FiberIdTypeId: + replacement: "none" + note: "Fiber IDs are primitive numbers in v4 and have no type-id symbol." +effect/FiberId#getOrElse: + replacement: "fiberId ?? fallback" + note: "Represent absence as undefined when migrating code that previously used FiberId.none." +effect/FiberId#ids: + replacement: "new Set([fiberId])" + note: "A v4 fiber has one numeric id; composite-id flattening is no longer required." +effect/FiberId#isComposite: + replacement: "none" + note: "Composite FiberId values do not exist in v4." +effect/FiberId#isFiberId: + replacement: "Number.isNumber" + note: "Fiber IDs are primitive numbers in v4." +effect/FiberId#isNone: + replacement: "fiberId === undefined" + note: "Use undefined for an absent optional interruptor id; there is no sentinel FiberId.none." +effect/FiberId#isRuntime: + replacement: "Number.isNumber" + note: "Every v4 fiber id is a runtime numeric id." +effect/FiberId#make: + replacement: "id" + note: "Use the numeric id directly; startTimeSeconds is no longer part of fiber identity." +effect/FiberId#none: + replacement: "undefined" + note: "Optional interruptor IDs use undefined rather than a sentinel FiberId value." +effect/FiberId#None: + replacement: "undefined" + note: "Optional interruptor IDs use undefined rather than a sentinel FiberId type." +effect/FiberId#runtime: + replacement: "id" + note: "Use the numeric id directly; startTimeMillis is no longer part of fiber identity." +effect/FiberId#Runtime: + replacement: "number" + note: "Runtime fiber IDs are primitive numbers in v4." +effect/FiberId#Single: + replacement: "number | undefined" + note: "Use a number, with undefined only where the old None case was meaningful." +effect/FiberId#threadName: + replacement: "String(fiberId)" + note: "There is no built-in thread-name formatter; format the numeric id at the presentation boundary." +effect/FiberId#toSet: + replacement: "new Set([fiberId])" + note: "A v4 fiber has one numeric id, so composite-id flattening is unnecessary." +effect/FiberId#unsafeMake: + replacement: "none" + note: "There is no public fiber-id allocator; obtain the current id with Effect.fiberId or from Fiber.id." diff --git a/.context/effect/migration/annotations/effect__FiberMap.yaml b/.context/effect/migration/annotations/effect__FiberMap.yaml new file mode 100644 index 000000000..dd3039256 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberMap.yaml @@ -0,0 +1,15 @@ +effect/FiberMap#FiberMap: + replacement: "FiberMap.FiberMap" + note: "Retained; contained runtime fibers now use the unified Fiber type." +effect/FiberMap#TypeId: + replacement: "FiberMap.isFiberMap" + note: "The type-id symbol is private in v4; use the public guard." +effect/FiberMap#unsafeGet: + replacement: "FiberMap.getUnsafe" + note: "Renamed to put the Unsafe suffix last." +effect/FiberMap#unsafeHas: + replacement: "FiberMap.hasUnsafe" + note: "Renamed to put the Unsafe suffix last." +effect/FiberMap#unsafeSet: + replacement: "FiberMap.setUnsafe" + note: "Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details." diff --git a/.context/effect/migration/annotations/effect__FiberRef.yaml b/.context/effect/migration/annotations/effect__FiberRef.yaml new file mode 100644 index 000000000..403eb277f --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberRef.yaml @@ -0,0 +1,144 @@ +effect/FiberRef#currentConcurrency: + replacement: "none" + note: "Inherited concurrency was removed; pass concurrency explicitly to each v4 combinator that supports it." +effect/FiberRef#currentContext: + replacement: "Effect.context" + note: "Fiber services are stored directly in Context; use Effect.context to read them and Effect.provideContext to override them." +effect/FiberRef#currentLogAnnotations: + replacement: "References.CurrentLogAnnotations" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentLoggers: + replacement: "References.CurrentLoggers" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentLogLevel: + replacement: "References.CurrentLogLevel" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentLogSpan: + replacement: "References.CurrentLogSpans" + note: "Renamed and represented as a Context.Reference containing a readonly span array." +effect/FiberRef#currentMaxOpsBeforeYield: + replacement: "Scheduler.MaxOpsBeforeYield" + note: "The scheduler setting is now a Context.Reference; yield it or provide it with Effect.provideService." +effect/FiberRef#currentMetricLabels: + replacement: "Metric.CurrentMetricAttributes" + note: "Metric labels became metric attributes stored in a Context.Reference." +effect/FiberRef#currentMinimumLogLevel: + replacement: "References.MinimumLogLevel" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentRequestBatchingEnabled: + replacement: "none" + note: "The request batching FiberRef was removed; batching is defined by the v4 RequestResolver runAll implementation." +effect/FiberRef#currentRequestCache: + replacement: "RequestResolver.withCache" + note: "The ambient request cache was removed; wrap a RequestResolver with an explicit bounded cache." +effect/FiberRef#currentRequestCacheEnabled: + replacement: "RequestResolver.withCache" + note: "There is no ambient cache toggle; choose an explicitly cached or uncached RequestResolver." +effect/FiberRef#currentRuntimeFlags: + replacement: "none" + note: "RuntimeFlags and their FiberRef were removed; use specific v4 runtime options such as interruptibility and scheduler settings." +effect/FiberRef#currentScheduler: + replacement: "Scheduler.Scheduler" + note: "The scheduler is now a Context.Reference; yield it or provide it with Effect.provideService." +effect/FiberRef#currentSchedulingPriority: + replacement: "none" + note: "The ambient scheduling-priority FiberRef was removed; use explicit scheduler operations where priority is needed." +effect/FiberRef#currentSupervisor: + replacement: "none" + note: "The Supervisor and ambient supervisor FiberRef APIs were removed; track fibers explicitly with FiberSet or FiberMap." +effect/FiberRef#currentTracerEnabled: + replacement: "References.TracerEnabled" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentTracerSpanAnnotations: + replacement: "References.TracerSpanAnnotations" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentTracerSpanLinks: + replacement: "References.TracerSpanLinks" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#currentTracerTimingEnabled: + replacement: "References.TracerTimingEnabled" + note: "Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService." +effect/FiberRef#delete: + replacement: "Effect.provideService" + note: "Context.Reference has no in-place delete; scope the default or desired value around the target Effect." +effect/FiberRef#FiberRef: + replacement: "Context.Reference" + note: "Fiber-local values and services share Context.Reference in v4; references have a defaultValue and no fork/join patching." +effect/FiberRef#FiberRefTypeId: + replacement: "Context.isReference" + note: "Use the public Context.Reference guard instead of a FiberRef type-id symbol." +effect/FiberRef#FiberRefUnify: + replacement: "none" + note: "Context.Reference is a service key and does not require the old FiberRef Effect-unification helper." +effect/FiberRef#FiberRefUnifyIgnore: + replacement: "none" + note: "Context.Reference is a service key and does not require the old FiberRef Effect-unification helper." +effect/FiberRef#get: + replacement: "reference" + note: "Context.Reference is yieldable as a service; yield it directly to read the current value." +effect/FiberRef#getAndUpdateSome: + replacement: "Ref.getAndUpdateSome" + note: "Use Ref for mutable state; for fiber-local configuration compute the value first and scope it with Effect.provideService." +effect/FiberRef#getWith: + replacement: "Effect.flatMap(reference, f)" + note: "Yield or flatMap the Context.Reference directly." +effect/FiberRef#interruptedCause: + replacement: "none" + note: "The pending interruption cause is no longer exposed as public fiber-local state; inspect completed failure Causes from Fiber.await." +effect/FiberRef#make: + replacement: "Context.Reference" + note: "Define a stable Context.Reference key with defaultValue; custom fork and join behavior is not supported." +effect/FiberRef#makeContext: + replacement: "Context.Reference" + note: "Define a Context.Reference whose defaultValue returns the Context; custom context diffing is no longer required." +effect/FiberRef#makeRuntimeFlags: + replacement: "none" + note: "RuntimeFlags and specialized FiberRef constructors were removed; migrate each flag to its explicit v4 runtime option." +effect/FiberRef#makeWith: + replacement: "Context.Reference" + note: "Use the lazy defaultValue option on a stable Context.Reference key." +effect/FiberRef#modify: + replacement: "Ref.modify" + note: "Use Ref for mutable state; Context.Reference updates are scoped with Effect.provideService rather than mutated in place." +effect/FiberRef#modifySome: + replacement: "Ref.modifySome" + note: "Use Ref for mutable state; Context.Reference updates are scoped with Effect.provideService rather than mutated in place." +effect/FiberRef#reset: + replacement: "Effect.provideService" + note: "Context.Reference has no in-place reset; scope its default value around the target Effect." +effect/FiberRef#set: + replacement: "Effect.provideService" + note: "Context.Reference values are overridden for an Effect scope instead of mutating the current fiber." +effect/FiberRef#unhandledErrorLogLevel: + replacement: "References.UnhandledLogLevel" + note: "Renamed and represented as a Context.Reference using Severity | undefined instead of Option." +effect/FiberRef#unsafeMake: + replacement: "Context.Reference" + note: "Context.Reference construction is synchronous; provide a stable identifier and defaultValue." +effect/FiberRef#unsafeMakeContext: + replacement: "Context.Reference" + note: "Define a Context.Reference whose defaultValue returns the Context; there is no specialized unsafe constructor." +effect/FiberRef#unsafeMakeHashSet: + replacement: "Context.Reference" + note: "Define a normal Context.Reference with a readonly set default; specialized differ constructors were removed." +effect/FiberRef#unsafeMakePatch: + replacement: "Context.Reference" + note: "Define a normal Context.Reference; custom Differ, fork patches, and join behavior are not supported in v4." +effect/FiberRef#unsafeMakeSupervisor: + replacement: "none" + note: "Supervisor and FiberRef were removed; track managed fibers explicitly with FiberSet or FiberMap." +effect/FiberRef#update: + replacement: "Ref.update" + note: "Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService." +effect/FiberRef#updateSome: + replacement: "Ref.updateSome" + note: "Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService." +effect/FiberRef#updateSomeAndGet: + replacement: "Ref.updateSomeAndGet" + note: "Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService." +effect/FiberRef#Variance: + replacement: "Context.Reference" + note: "The FiberRef-specific variance interface was removed with FiberRef." +effect/FiberRef#versionMismatchErrorLogLevel: + replacement: "none" + note: "The version-mismatch logging FiberRef was removed and no public v4 Context.Reference replaces it." diff --git a/.context/effect/migration/annotations/effect__FiberRefs.yaml b/.context/effect/migration/annotations/effect__FiberRefs.yaml new file mode 100644 index 000000000..63a8ce255 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberRefs.yaml @@ -0,0 +1,39 @@ +effect/FiberRefs#delete: + replacement: "Context.omit" + note: "FiberRefs became fiber Context; omit a Reference key when constructing the replacement Context." +effect/FiberRefs#empty: + replacement: "Context.empty" + note: "Use an empty Context as the starting collection of services and reference overrides." +effect/FiberRefs#fiberRefs: + replacement: "none" + note: "Context does not expose public enumeration of its Reference keys; retain the keys explicitly if enumeration is required." +effect/FiberRefs#FiberRefs: + replacement: "Context.Context" + note: "Fiber-local services and reference overrides are stored directly in the Fiber context in v4." +effect/FiberRefs#FiberRefsSym: + replacement: "none" + note: "FiberRefs and its marker symbol were removed." +effect/FiberRefs#forkAs: + replacement: "none" + note: "Context is inherited automatically when a v4 child fiber is forked; custom per-reference fork patches were removed." +effect/FiberRefs#get: + replacement: "Context.getOption" + note: "Read the service as an Option. Context.Reference defaults also produce Some; use Context.getOrUndefined when only stored overrides should count." +effect/FiberRefs#getOrDefault: + replacement: "Context.get" + note: "Reads an override or the Context.Reference default value." +effect/FiberRefs#joinAs: + replacement: "none" + note: "Child-to-parent FiberRef joining was removed; pass results explicitly or merge ordinary Context values where appropriate." +effect/FiberRefs#setAll: + replacement: "Effect.provideContext" + note: "Provide the replacement Context around the Effect that should observe its services and reference overrides." +effect/FiberRefs#unsafeMake: + replacement: "Context.empty().pipe(Context.add(...))" + note: "Build a Context from explicit Reference keys and values; FiberId histories and unsafe local maps no longer exist." +effect/FiberRefs#updateAs: + replacement: "Context.add" + note: "Add or replace a Reference value in Context; the FiberId parameter and history are removed." +effect/FiberRefs#updateManyAs: + replacement: "Context.add" + note: "Apply explicit Context.add calls for each Reference value; FiberId histories and forkAs are removed." diff --git a/.context/effect/migration/annotations/effect__FiberRefsPatch.yaml b/.context/effect/migration/annotations/effect__FiberRefsPatch.yaml new file mode 100644 index 000000000..4ba815541 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberRefsPatch.yaml @@ -0,0 +1,24 @@ +effect/FiberRefsPatch#Add: + replacement: "Context.add" + note: "FiberRefsPatch was removed; apply Reference overrides directly to Context." +effect/FiberRefsPatch#AndThen: + replacement: "Context.merge" + note: "FiberRefsPatch was removed; compose Context updates directly, with later values overriding earlier ones." +effect/FiberRefsPatch#combine: + replacement: "Context.merge" + note: "FiberRefsPatch was removed; merge the resulting Context values instead of combining patches." +effect/FiberRefsPatch#diff: + replacement: "none" + note: "There is no generic Context diff because FiberRef fork and join patch semantics were removed." +effect/FiberRefsPatch#empty: + replacement: "Context.empty" + note: "Use an empty Context when no services or Reference overrides are applied." +effect/FiberRefsPatch#Empty: + replacement: "Context.Context" + note: "The empty patch model was removed; an empty Context represents no overrides." +effect/FiberRefsPatch#FiberRefsPatch: + replacement: "none" + note: "The patch data type was removed with FiberRefs; construct or merge Context values directly." +effect/FiberRefsPatch#patch: + replacement: "Context.merge" + note: "Merge explicit Context overrides into the base Context; FiberId-aware patch application no longer exists." diff --git a/.context/effect/migration/annotations/effect__FiberSet.yaml b/.context/effect/migration/annotations/effect__FiberSet.yaml new file mode 100644 index 000000000..ec0d81253 --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberSet.yaml @@ -0,0 +1,9 @@ +effect/FiberSet#FiberSet: + replacement: "FiberSet.FiberSet" + note: "Retained; contained runtime fibers now use the unified Fiber type." +effect/FiberSet#TypeId: + replacement: "FiberSet.isFiberSet" + note: "The type-id symbol is private in v4; use the public guard." +effect/FiberSet#unsafeAdd: + replacement: "FiberSet.addUnsafe" + note: "Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details." diff --git a/.context/effect/migration/annotations/effect__FiberStatus.yaml b/.context/effect/migration/annotations/effect__FiberStatus.yaml new file mode 100644 index 000000000..752128b4f --- /dev/null +++ b/.context/effect/migration/annotations/effect__FiberStatus.yaml @@ -0,0 +1,33 @@ +effect/FiberStatus#Done: + replacement: "Exit.Exit" + note: "FiberStatus was removed; a defined fiber.pollUnsafe() result indicates completion and contains the Exit." +effect/FiberStatus#FiberStatus: + replacement: "Exit.Exit | undefined" + note: "Use fiber.pollUnsafe(); undefined means incomplete and Exit means completed, with no running/suspended distinction." +effect/FiberStatus#FiberStatusTypeId: + replacement: "none" + note: "FiberStatus and its type-id symbol were removed." +effect/FiberStatus#isDone: + replacement: "fiber.pollUnsafe() !== undefined" + note: "Completion is observable by synchronously polling the Fiber." +effect/FiberStatus#isFiberStatus: + replacement: "none" + note: "FiberStatus values no longer exist; inspect a Fiber with pollUnsafe instead." +effect/FiberStatus#isRunning: + replacement: "fiber.pollUnsafe() === undefined" + note: "V4 only exposes incomplete versus completed; it does not distinguish running from suspended." +effect/FiberStatus#isSuspended: + replacement: "none" + note: "The public runtime no longer exposes suspended status." +effect/FiberStatus#running: + replacement: "none" + note: "FiberStatus constructors were removed; keep the Fiber and poll it instead." +effect/FiberStatus#Running: + replacement: "none" + note: "The public runtime no longer models running status as a value." +effect/FiberStatus#suspended: + replacement: "none" + note: "FiberStatus constructors and public suspended status were removed." +effect/FiberStatus#Suspended: + replacement: "none" + note: "The public runtime no longer models suspended status as a value." diff --git a/.context/effect/migration/annotations/effect__Function.yaml b/.context/effect/migration/annotations/effect__Function.yaml new file mode 100644 index 000000000..b2c548335 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Function.yaml @@ -0,0 +1,12 @@ +"effect/Function#FunctionN": + replacement: "Function.FunctionN" + note: "No call-site migration; v4 keeps the same function shape as a type alias." +"effect/Function#isFunction": + replacement: "Predicate.isFunction" + note: "The function refinement moved to Predicate." +"effect/Function#LazyArg": + replacement: "Function.LazyArg" + note: "No call-site migration; v4 keeps the same lazy function shape as a type alias." +"effect/Function#unsafeCoerce": + replacement: "Function.cast" + note: "Renamed type-only cast; runtime behavior remains identity with no validation." diff --git a/.context/effect/migration/annotations/effect__GlobalValue.yaml b/.context/effect/migration/annotations/effect__GlobalValue.yaml new file mode 100644 index 000000000..414a1bbe4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__GlobalValue.yaml @@ -0,0 +1,3 @@ +"effect/GlobalValue#globalValue": + replacement: "module-scoped const" + note: "The global registry helper was removed; use a module singleton, or explicitly own a globalThis and Symbol.for registry when cross-bundle identity is required." diff --git a/.context/effect/migration/annotations/effect__Graph.yaml b/.context/effect/migration/annotations/effect__Graph.yaml new file mode 100644 index 000000000..eda727c4c --- /dev/null +++ b/.context/effect/migration/annotations/effect__Graph.yaml @@ -0,0 +1,12 @@ +"effect/Graph#Graph": + replacement: "Graph.Graph" + note: "The immutable type remains, but storage is opaque; replace field access with Graph nodes, edges, count, lookup, neighbor, and acyclicity APIs." +"effect/Graph#MutableGraph": + replacement: "Graph.MutableGraph" + note: "The mutable type remains but no longer extends Graph.Proto; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions." +"effect/Graph#Proto": + replacement: "Graph.Proto" + note: "The name remains as the opaque immutable graph protocol; it no longer exposes storage and is no longer the base of MutableGraph." +"effect/Graph#SearchConfig": + replacement: "Graph.SearchConfig" + note: "The type remains; direction is now Graph.TraversalDirection and also accepts undirected, while radius limits traversal depth." diff --git a/.context/effect/migration/annotations/effect__GroupBy.yaml b/.context/effect/migration/annotations/effect__GroupBy.yaml new file mode 100644 index 000000000..a98cb79e9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__GroupBy.yaml @@ -0,0 +1,27 @@ +effect/GroupBy#GroupBy: + replacement: "Stream]>" + note: "The GroupBy datatype is removed in v4; Stream.groupBy/groupByKey now return an ordinary Stream of readonly [key, substream] pairs, processed with regular Stream operators." +effect/GroupBy#GroupBy.Variance: + replacement: "none" + note: "Variance plumbing for the removed GroupBy datatype; v4 has no GroupBy type, so there is no variance interface to migrate to." +effect/GroupBy#GroupByTypeId: + replacement: "none" + note: "Brand symbol for the removed GroupBy datatype; v4 groupBy results are plain Streams, discriminated with Stream.isStream if needed." +effect/GroupBy#evaluate: + replacement: "Stream.flatMap" + note: "Apply the per-group function over the [key, stream] pairs with Stream.flatMap (or Stream.mapEffect for an effectful result per group), using { concurrency: \"unbounded\" } to reproduce v3's parallel-groups/arbitrary-merge-order behavior; the v3 bufferSize option moved onto Stream.groupBy itself." + example: | + // v3: stream.pipe(Stream.groupByKey(f), GroupBy.evaluate((key, s) => g(key, s))) + stream.pipe( + Stream.groupByKey(f), + Stream.flatMap(([key, s]) => g(key, s), { concurrency: "unbounded" }) + ) +effect/GroupBy#filter: + replacement: "Stream.filter" + note: "Filter the groups by key with an ordinary Stream.filter on the pairs: Stream.filter(([key]) => predicate(key))." +effect/GroupBy#first: + replacement: "Stream.take" + note: "Keep only the first n groups with an ordinary Stream.take(n) on the [key, stream] pair stream." +effect/GroupBy#make: + replacement: "none" + note: "No wrapper to construct in v4: a grouped stream is just any Stream]>, so build the pair stream directly (Stream.groupBy/groupByKey produce it); the v3 shape Stream<[K, Dequeue>]> is gone along with the queue-of-Take encoding." diff --git a/.context/effect/migration/annotations/effect__Hash.yaml b/.context/effect/migration/annotations/effect__Hash.yaml new file mode 100644 index 000000000..69f676887 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Hash.yaml @@ -0,0 +1,3 @@ +"effect/Hash#cached": + replacement: "none" + note: "Delete Hash.cached wrappers and return the computed value from Hash.symbol; Hash.hash now caches objects automatically in a private WeakMap without mutating them." diff --git a/.context/effect/migration/annotations/effect__HashMap.yaml b/.context/effect/migration/annotations/effect__HashMap.yaml new file mode 100644 index 000000000..e35bc6fa9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__HashMap.yaml @@ -0,0 +1,15 @@ +"effect/HashMap#countBy": + replacement: "HashMap.reduce" + note: "Count matches with HashMap.reduce(self, 0, (count, value, key) => count + (predicate(value, key) ? 1 : 0))." +"effect/HashMap#HashMap": + replacement: "HashMap.HashMap" + note: "The immutable two-parameter model remains; use public operations rather than depending on its representation." +"effect/HashMap#keySet": + replacement: "HashSet.fromIterable + HashMap.keys" + note: "Construct the set with HashSet.fromIterable(HashMap.keys(self)); no direct keySet helper remains." +"effect/HashMap#TypeId": + replacement: "HashMap.isHashMap" + note: "The brand is private; use HashMap.isHashMap for runtime refinement and HashMap.HashMap in type positions." +"effect/HashMap#unsafeGet": + replacement: "HashMap.getUnsafe" + note: "Direct word-order rename; it still throws for a missing key." diff --git a/.context/effect/migration/annotations/effect__HashSet.yaml b/.context/effect/migration/annotations/effect__HashSet.yaml new file mode 100644 index 000000000..adaa83fd2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__HashSet.yaml @@ -0,0 +1,33 @@ +"effect/HashSet#beginMutation": + replacement: "none" + note: "Transient mutation mode was removed; reassign immutable add/remove results or build a batch with HashSet.fromIterable." +"effect/HashSet#endMutation": + replacement: "none" + note: "There is no mutation window to finalize; remove this call and use the latest immutable HashSet value." +"effect/HashSet#flatMap": + replacement: "HashSet.fromIterable + Iterable.flatMap" + note: "Preserve set deduplication with HashSet.fromIterable(Iterable.flatMap(self, f)); no direct flatMap remains." +"effect/HashSet#forEach": + replacement: "Iterable.forEach" + note: "HashSet remains Iterable, so Iterable.forEach(self, f) preserves eager side-effecting traversal." +"effect/HashSet#HashSet": + replacement: "HashSet.HashSet" + note: "The immutable model remains, but the brand is private and transient mutation helpers were removed." +"effect/HashSet#partition": + replacement: "HashSet.filter" + note: "Build [excluded, satisfying] with complementary HashSet.filter calls, or use one reduction when the predicate is expensive." +"effect/HashSet#toggle": + replacement: "HashSet.has + HashSet.remove / HashSet.add" + note: "Use HashSet.has(self, value) ? HashSet.remove(self, value) : HashSet.add(self, value)." +"effect/HashSet#toValues": + replacement: "Array.from" + note: "HashSet remains iterable; Array.from(self) produces the former Array result." +"effect/HashSet#TypeId": + replacement: "HashSet.isHashSet" + note: "The brand is private; use HashSet.isHashSet for runtime refinement and HashSet.HashSet in type positions." +"effect/HashSet#values": + replacement: "none" + note: "The HashSet itself is iterable; iterate it directly or call self[Symbol.iterator]() when an iterator object is required." +"effect/HashSet#mutate": + replacement: "none" + note: "Transient mutation was removed; reassign immutable HashSet.add/remove results or build a complete replacement with HashSet.fromIterable." diff --git a/.context/effect/migration/annotations/effect__Inspectable.yaml b/.context/effect/migration/annotations/effect__Inspectable.yaml new file mode 100644 index 000000000..f6475e068 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Inspectable.yaml @@ -0,0 +1,12 @@ +"effect/Inspectable#redact": + replacement: "Redactable.redact" + note: "The redaction protocol moved to Redactable and now receives the current fiber Context." +"effect/Inspectable#stringifyCircular": + replacement: "Formatter.formatJson" + note: "Use Formatter.formatJson(input, { space: whitespace }); it handles redaction and ancestor cycles." +"effect/Inspectable#toJSON": + replacement: "Inspectable.toJson" + note: "Renamed to lower-camel toJson with the same recursive conversion role." +"effect/Inspectable#withRedactableContext": + replacement: "none" + note: "Manual FiberRefs scoping was removed; Redactable.redact uses the current fiber Context automatically." diff --git a/.context/effect/migration/annotations/effect__Iterable.yaml b/.context/effect/migration/annotations/effect__Iterable.yaml new file mode 100644 index 000000000..b25d8798c --- /dev/null +++ b/.context/effect/migration/annotations/effect__Iterable.yaml @@ -0,0 +1,12 @@ +"effect/Iterable#flatMapNullable": + replacement: "Iterable.flatMapNullishOr" + note: "Direct nullish-terminology rename; it remains lazy and drops null or undefined mapper results." +"effect/Iterable#getLefts": + replacement: "Iterable.getFailures" + note: "Either became Result; this lazily extracts failure payloads." +"effect/Iterable#getRights": + replacement: "Iterable.getSuccesses" + note: "Either became Result; this lazily extracts success payloads." +"effect/Iterable#unsafeHead": + replacement: "Iterable.headUnsafe" + note: "Direct word-order rename; it still throws on an empty Iterable." diff --git a/.context/effect/migration/annotations/effect__JSONSchema.yaml b/.context/effect/migration/annotations/effect__JSONSchema.yaml new file mode 100644 index 000000000..34d61b8f0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__JSONSchema.yaml @@ -0,0 +1,71 @@ +"effect/JSONSchema#fromAST": + replacement: "Schema.toJsonSchemaDocument" + note: "Wrap a low-level AST with Schema.make, then generate a document; v4 generation targets draft 2020-12." + example: "Schema.toJsonSchemaDocument(Schema.make(ast))" +"effect/JSONSchema#JsonSchema7": + replacement: "JsonSchema.JsonSchema" + note: "The draft-07-specific union was replaced by the dialect-neutral JSON Schema model." +"effect/JSONSchema#JsonSchema7Any": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7AnyOf": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Array": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Boolean": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7empty": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Enum": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Enums": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Integer": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Never": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Null": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Number": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Numeric": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7object": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Object": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Ref": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7String": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Unknown": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Void": + replacement: "JsonSchema.JsonSchema" + note: "Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema." +"effect/JSONSchema#JsonSchema7Root": + replacement: "JsonSchema.Document<\"draft-07\">" + note: "Use a typed JSON Schema document for a draft-07 root and definitions." +"effect/JSONSchema#JsonSchemaAnnotations": + replacement: "Schema.Annotations.Documentation" + note: "Schema metadata now uses string-keyed Schema annotations; JSON Schema-specific checks use toJsonSchema annotations." +"effect/JSONSchema#make": + replacement: "Schema.toJsonSchemaDocument" + note: "Generate draft 2020-12, then call JsonSchema.toDocumentDraft07 when draft-07 output is required." + example: "JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema))" diff --git a/.context/effect/migration/annotations/effect__KeyedPool.yaml b/.context/effect/migration/annotations/effect__KeyedPool.yaml new file mode 100644 index 000000000..f3b27dc52 --- /dev/null +++ b/.context/effect/migration/annotations/effect__KeyedPool.yaml @@ -0,0 +1,27 @@ +effect/KeyedPool#get: + replacement: "RcMap.get + Pool.get" + note: "KeyedPool was removed; acquire the per-key Pool from an RcMap, then borrow an item with Pool.get in the current Scope." +effect/KeyedPool#invalidate: + replacement: "RcMap.get + Pool.invalidate" + note: "KeyedPool was removed; retain the key, get its Pool from RcMap, and call Pool.invalidate for the item." +effect/KeyedPool#KeyedPool: + replacement: "RcMap.RcMap>" + note: "Model keyed pools as an RcMap whose scoped lookup creates one Pool per key." +effect/KeyedPool#KeyedPool.Variance: + replacement: "none" + note: "KeyedPool and its variance marker were removed; use the RcMap and Pool public models without depending on branding internals." +effect/KeyedPool#KeyedPoolTypeId: + replacement: "none" + note: "KeyedPool was removed, so its runtime type id has no v4 equivalent." +effect/KeyedPool#make: + replacement: "RcMap.make + Pool.make" + note: "Create an RcMap with lookup key => Pool.make({ acquire: acquire(key), size }); RcMap.get followed by Pool.get replaces keyed borrowing." +effect/KeyedPool#makeWith: + replacement: "RcMap.make + Pool.make" + note: "Create an RcMap whose lookup uses Pool.make with size: size(key)." +effect/KeyedPool#makeWithTTL: + replacement: "RcMap.make + Pool.makeWithTTL" + note: "Create an RcMap whose lookup uses Pool.makeWithTTL with min(key), max(key), and the shared timeToLive." +effect/KeyedPool#makeWithTTLBy: + replacement: "RcMap.make + Pool.makeWithTTL" + note: "Create an RcMap whose lookup uses Pool.makeWithTTL with min(key), max(key), and timeToLive(key)." diff --git a/.context/effect/migration/annotations/effect__Layer.yaml b/.context/effect/migration/annotations/effect__Layer.yaml new file mode 100644 index 000000000..def772ee9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Layer.yaml @@ -0,0 +1,189 @@ +"effect/Layer#annotateLogs": + replacement: "Layer.fromBuild((memoMap, scope) => Effect.annotateLogs(Layer.buildWithMemoMap(self, memoMap, scope), ...annotations))" + note: "Apply Effect.annotateLogs to the layer acquisition effect." +"effect/Layer#annotateSpans": + replacement: "Layer.fromBuild((memoMap, scope) => Effect.annotateSpans(Layer.buildWithMemoMap(self, memoMap, scope), ...annotations))" + note: "Apply Effect.annotateSpans to the layer acquisition effect." +"effect/Layer#catchAll": + replacement: "Layer.catch" + note: "The typed-error handler was renamed." +"effect/Layer#catchAllCause": + replacement: "Layer.catchCause" + note: "The cause handler was renamed." +"effect/Layer#context": + replacement: "Layer.effectContext(Effect.context())" + note: "Capture and return the current service context." +"effect/Layer#CurrentMemoMap": + replacement: "Layer.CurrentMemoMap" + note: "The service remains but is now a Context.Service class with forkOrCreate." +"effect/Layer#die": + replacement: "Layer.unwrap(Effect.die(defect))" + note: "Lift the Effect defect constructor." +"effect/Layer#dieSync": + replacement: "Layer.unwrap(Effect.suspend(() => Effect.die(evaluate())))" + note: "Suspend evaluation and lift Effect.die; Effect.dieSync was also removed." +"effect/Layer#discard": + replacement: "Layer.flatMap(self, () => Layer.empty)" + note: "Build the layer while dropping its output context." +"effect/Layer#ensureErrorType": + replacement: "Layer.satisfiesErrorType" + note: "The type constraint helper was renamed." +"effect/Layer#ensureRequirementsType": + replacement: "Layer.satisfiesServicesType" + note: "The requirements type constraint was renamed to services." +"effect/Layer#ensureSuccessType": + replacement: "Layer.satisfiesSuccessType" + note: "The type constraint helper was renamed." +"effect/Layer#extendScope": + replacement: "Layer.buildWithScope(self, outerScope) and Effect.provideContext(program, context)" + note: "Explicitly build against the desired outer scope and provide the resulting context." +"effect/Layer#fail": + replacement: "Layer.unwrap(Effect.fail(error))" + note: "Lift the Effect failure constructor." +"effect/Layer#failCause": + replacement: "Layer.unwrap(Effect.failCause(cause))" + note: "Lift the Effect cause-failure constructor." +"effect/Layer#failCauseSync": + replacement: "Layer.unwrap(Effect.failCauseSync(evaluate))" + note: "Lift the retained Effect constructor." +"effect/Layer#failSync": + replacement: "Layer.unwrap(Effect.failSync(evaluate))" + note: "Lift the retained Effect constructor." +"effect/Layer#fiberRefLocallyScopedWith": + replacement: "Layer.effect(reference, Effect.map(reference, f))" + note: "FiberRef was removed; compute and provide a transformed Context.Reference value." +"effect/Layer#flatten": + replacement: "Layer.flatMap(self, (context) => Context.get(context, key))" + note: "Expand the removed convenience combinator with flatMap and Context.get." +"effect/Layer#function": + replacement: "Layer.effect(keyB, Effect.map(keyA, f))" + note: "Read the input service through its Context.Key and provide the transformed service." +"effect/Layer#isFresh": + replacement: "none" + note: "Layer.fresh remains, but its wrapper has no public freshness predicate." +"effect/Layer#Layer": + replacement: "Layer.Layer" + note: "The type remains with Layer parameter order." +"effect/Layer#Layer.Context": + replacement: "Layer.Services" + note: "The input-services extractor moved to the module level and was renamed." +"effect/Layer#LayerTypeId": + replacement: "none" + note: "The marker is private in v4; use Layer.Any or Layer.Variance for type constraints." +"effect/Layer#locally": + replacement: "Layer.updateService(self, reference, () => value)" + note: "Replace FiberRef-local configuration with Context.Reference provision." +"effect/Layer#locallyEffect": + replacement: "Layer.fromBuild((memoMap, scope) => f(Layer.buildWithMemoMap(self, memoMap, scope)))" + note: "Transform the public layer acquisition effect directly." +"effect/Layer#locallyScoped": + replacement: "Layer.succeed(reference, value)" + note: "Provide a v4 Context.Reference value as a configuration layer." +"effect/Layer#locallyWith": + replacement: "Layer.updateService(self, reference, f)" + note: "Transform a Context.Reference during layer acquisition." +"effect/Layer#map": + replacement: "Layer.flatMap(self, (context) => Layer.succeedContext(f(context)))" + note: "Expand the removed output-context mapping combinator." +"effect/Layer#mapError": + replacement: "Layer.fromBuild((memoMap, scope) => Effect.mapError(Layer.buildWithMemoMap(self, memoMap, scope), f))" + note: "Transform the typed error of layer acquisition." +"effect/Layer#match": + replacement: "Layer.fromBuild with Effect.matchEffect over Layer.buildWithMemoMap" + note: "Fold the source acquisition effect, then build the selected failure or success layer." +"effect/Layer#matchCause": + replacement: "Layer.fromBuild with Effect.matchCauseEffect over Layer.buildWithMemoMap" + note: "Fold the source acquisition cause, then build the selected failure or success layer." +"effect/Layer#memoize": + replacement: "automatic shared memoization under Effect.provide" + note: "Reuse the same Layer value; use { local: true } or Layer.fresh to opt out, or MemoMap APIs for manual control." +"effect/Layer#MemoMap": + replacement: "Layer.MemoMap" + note: "The interface remains and now supports parent-child ambient maps." +"effect/Layer#MemoMapTypeId": + replacement: "none" + note: "The MemoMap marker is private in v4." +"effect/Layer#orElse": + replacement: "Layer.catch(self, () => fallback())" + note: "Expand the removed lazy fallback alias with Layer.catch." +"effect/Layer#passthrough": + replacement: "Layer.merge(Layer.effectContext(Effect.context()), self)" + note: "Capture required input services and merge them into the layer output." +"effect/Layer#project": + replacement: "Layer.flatMap(self, (context) => Layer.succeed(keyB, f(Context.get(context, keyA))))" + note: "Project one derived service and drop the other outputs." +"effect/Layer#retry": + replacement: "Effect.retry(acquire, schedule) before Layer.effect or Layer.effectContext" + note: "Retry the acquisition Effect; for an arbitrary layer, rebuild a fresh layer for each attempt through Layer.fromBuild." +"effect/Layer#scope": + replacement: "Layer.effect(Scope.Scope, Effect.acquireRelease(Scope.make(), Scope.close))" + note: "Construct and close a child scope explicitly." +"effect/Layer#scoped": + replacement: "Layer.effect" + note: "Scoped acquisition was merged into Layer.effect, which supplies and excludes the layer Scope." +"effect/Layer#scopedContext": + replacement: "Layer.effectContext" + note: "Scoped context acquisition was merged into Layer.effectContext." +"effect/Layer#scopedDiscard": + replacement: "Layer.effectDiscard" + note: "Scoped discard acquisition was merged into Layer.effectDiscard." +"effect/Layer#service": + replacement: "Layer.effect(key, key)" + note: "A Context.Key is an Effect that reads and passes through its service." +"effect/Layer#setClock": + replacement: "Layer.succeed(Clock.Clock, clock)" + note: "Clock.Clock is now a Context.Reference; provide it directly." +"effect/Layer#setConfigProvider": + replacement: "ConfigProvider.layer(configProvider)" + note: "Use the dedicated ConfigProvider layer constructor." +"effect/Layer#setRandom": + replacement: "Layer.succeed(Random.Random, random)" + note: "Random.Random is now a Context.Reference; provide it directly." +"effect/Layer#setRequestBatching": + replacement: "none" + note: "Requests now use resolver-driven batching and expose no batching switch." +"effect/Layer#setRequestCache": + replacement: "none" + note: "The public Request.Cache and its configuration API were removed." +"effect/Layer#setRequestCaching": + replacement: "none" + note: "The public request-caching toggle was removed." +"effect/Layer#setScheduler": + replacement: "Layer.succeed(Scheduler.Scheduler, scheduler)" + note: "Scheduler.Scheduler is now a Context.Reference; provide it directly." +"effect/Layer#setTracer": + replacement: "Layer.succeed(Tracer.Tracer, tracer)" + note: "Tracer.Tracer is now a Context.Reference; provide it directly." +"effect/Layer#setTracerEnabled": + replacement: "Layer.succeed(References.TracerEnabled, enabled)" + note: "Provide the v4 Reference instead of setting a FiberRef." +"effect/Layer#setTracerTiming": + replacement: "Layer.succeed(References.TracerTimingEnabled, enabled)" + note: "Provide the renamed v4 Reference instead of setting a FiberRef." +"effect/Layer#setUnhandledErrorLogLevel": + replacement: "Layer.succeed(References.UnhandledLogLevel, severityOrUndefined)" + note: "Provide LogLevel.Severity or undefined instead of Option." +"effect/Layer#setVersionMismatchErrorLogLevel": + replacement: "none" + note: "No version-mismatch log-level Reference or public replacement exists." +"effect/Layer#tapErrorCause": + replacement: "Layer.tapCause" + note: "The cause observer was renamed." +"effect/Layer#toRuntime": + replacement: "Layer.build(self), then Effect.runForkWith, Effect.runPromiseWith, or Effect.runSyncWith" + note: "Runtime was removed; build a Context, or use ManagedRuntime.make for a reusable managed runner." +"effect/Layer#toRuntimeWithMemoMap": + replacement: "Layer.buildWithMemoMap(self, memoMap, scope), then Effect.run*With(context)" + note: "Explicit memo-map building now yields a Context rather than a Runtime." +"effect/Layer#unwrapEffect": + replacement: "Layer.unwrap" + note: "The Effect-based unwrap constructor was renamed and generalized." +"effect/Layer#unwrapScoped": + replacement: "Layer.unwrap" + note: "Scoped and unscoped unwrap were merged; Layer.unwrap supplies and excludes the layer Scope." +"effect/Layer#updateService": + replacement: "Layer.updateService" + note: "The combinator remains and now accepts any Context.Key." +"effect/Layer#zipWith": + replacement: "Layer.fromBuild with concurrent Effect.zipWith over Layer.buildWithMemoMap" + note: "Combine acquisition effects directly; use Layer.merge when the function only merged Context values." diff --git a/.context/effect/migration/annotations/effect__LayerMap.yaml b/.context/effect/migration/annotations/effect__LayerMap.yaml new file mode 100644 index 000000000..a87d5e2f5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__LayerMap.yaml @@ -0,0 +1,15 @@ +"effect/LayerMap#LayerMap": + replacement: "LayerMap.LayerMap" + note: "The type remains; runtime(key) became contextEffect(key) and returns Context." +"effect/LayerMap#Service": + replacement: "LayerMap.Service" + note: "Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime." +"effect/LayerMap#Service.Context": + replacement: "LayerMap.Service.Services" + note: "The input-services extractor was renamed." +"effect/LayerMap#TagClass": + replacement: "LayerMap.TagClass" + note: "The type remains and now extends Context.ServiceClass; use the renamed layer and contextEffect members." +"effect/LayerMap#TypeId": + replacement: "none" + note: "The LayerMap marker is private in v4 and no public guard exists." diff --git a/.context/effect/migration/annotations/effect__List.yaml b/.context/effect/migration/annotations/effect__List.yaml new file mode 100644 index 000000000..ce1ec1d1e --- /dev/null +++ b/.context/effect/migration/annotations/effect__List.yaml @@ -0,0 +1,126 @@ +"effect/List#append": + replacement: "Array.append" + note: "List was removed; use Array.append. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#appendAll": + replacement: "Array.appendAll" + note: "List was removed; use Array.appendAll. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#cons": + replacement: "Array.prepend" + note: "List was removed; change List.cons(head, tail) to Array.prepend(tail, head)." +"effect/List#Cons": + replacement: "Array.NonEmptyReadonlyArray" + note: "Use the immutable non-empty array type; constructors may return the assignable mutable NonEmptyArray subtype." +"effect/List#empty": + replacement: "Array.empty" + note: "List was removed; use Array.empty. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#filter": + replacement: "Array.filter" + note: "List was removed; use Array.filter. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#filterMap": + replacement: "Array.filterMap" + note: "List was removed; use Array.filterMap and change the callback from Option to Result." +"effect/List#fromIterable": + replacement: "Array.fromIterable" + note: "List was removed; use Array.fromIterable. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#getEquivalence": + replacement: "Array.makeEquivalence" + note: "List was removed; compare the replacement arrays with Array.makeEquivalence." +"effect/List#head": + replacement: "Array.head" + note: "List was removed; use Array.head. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#isCons": + replacement: "Array.isReadonlyArrayNonEmpty" + note: "List was removed; this checks that the replacement readonly array is non-empty." +"effect/List#isList": + replacement: "Array.isArray" + note: "The List brand is gone; this now recognizes the replacement JavaScript array representation." +"effect/List#isNil": + replacement: "Array.isReadonlyArrayEmpty" + note: "List was removed; this checks that the replacement readonly array is empty." +"effect/List#last": + replacement: "Array.last" + note: "List was removed; use Array.last. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#List": + replacement: "ReadonlyArray" + note: "Replace the persistent linked-list representation with ReadonlyArray." +"effect/List#List.AndNonEmpty": + replacement: "Array.ReadonlyArray.AndNonEmpty" + note: "Use the corresponding readonly-array utility type." +"effect/List#List.OrNonEmpty": + replacement: "Array.ReadonlyArray.OrNonEmpty" + note: "Use the corresponding readonly-array utility type." +"effect/List#List.With": + replacement: "Array.ReadonlyArray.With" + note: "Use the corresponding readonly-array utility type." +"effect/List#make": + replacement: "Array.make" + note: "List was removed; use Array.make. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#map": + replacement: "Array.map" + note: "List was removed; use Array.map. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#nil": + replacement: "Array.empty" + note: "List was removed; represent Nil with an empty array." +"effect/List#Nil": + replacement: "none" + note: "Represent this case as readonly []; there is no tagged Nil interface in v4." +"effect/List#of": + replacement: "Array.of" + note: "List was removed; use Array.of. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#partition": + replacement: "Array.partition" + note: "Use a Result-returning callback: failure values form the first array and success values the second." +"effect/List#partitionMap": + replacement: "Array.partition" + note: "Migrate the Either-returning mapper to Result; failures form the first array and successes the second." +"effect/List#prependAll": + replacement: "Array.prependAll" + note: "List was removed; use Array.prependAll. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#prependAllReversed": + replacement: "Array.prependAll + Array.reverse" + note: "Use Array.prependAll(self, Array.reverse(prefix)) to preserve the old ordering." +"effect/List#reduce": + replacement: "Array.reduce" + note: "List was removed; use Array.reduce. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#reduceRight": + replacement: "Array.reduceRight" + note: "List was removed; use Array.reduceRight. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#reverse": + replacement: "Array.reverse" + note: "List was removed; use Array.reverse. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#size": + replacement: "Array.length" + note: "List was removed; use the replacement array length helper or the .length property." +"effect/List#splitAt": + replacement: "Array.splitAt" + note: "List was removed; use Array.splitAt. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#tail": + replacement: "Array.tail" + note: "List was removed; use Array.tail. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#take": + replacement: "Array.take" + note: "List was removed; use Array.take. It preserves ordering but returns arrays rather than persistent linked lists." +"effect/List#toArray": + replacement: "Array.fromIterable" + note: "After migrating the representation this is usually unnecessary; use Array.fromIterable when a fresh mutable array is required." +"effect/List#toChunk": + replacement: "Chunk.fromIterable" + note: "Convert the replacement array or other iterable with Chunk.fromIterable." +"effect/List#TypeId": + replacement: "none" + note: "Arrays have no List runtime marker; remove TypeId inspection." +"effect/List#unsafeHead": + replacement: "Array.headNonEmpty" + note: "Use a NonEmptyReadonlyArray proof before accessing the head; the v4 helper does not accept an empty array." +"effect/List#unsafeLast": + replacement: "Array.lastNonEmpty" + note: "Use a NonEmptyReadonlyArray proof before accessing the last element; the v4 helper does not accept an empty array." +"effect/List#unsafeTail": + replacement: "Array.tailNonEmpty" + note: "Use a NonEmptyReadonlyArray proof before taking the tail; the v4 helper does not accept an empty array." +"effect/List#every": + replacement: "Array.every" + note: "List was removed; run the predicate against the replacement array with Array.every." +"effect/List#some": + replacement: "Array.some" + note: "List was removed; run the predicate against the replacement array with Array.some." diff --git a/.context/effect/migration/annotations/effect__LogLevel.yaml b/.context/effect/migration/annotations/effect__LogLevel.yaml new file mode 100644 index 000000000..f0a4995d3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__LogLevel.yaml @@ -0,0 +1,51 @@ +effect/LogLevel#All: + replacement: "\"All\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Debug: + replacement: "\"Debug\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Error: + replacement: "\"Error\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Fatal: + replacement: "\"Fatal\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Info: + replacement: "\"Info\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#None: + replacement: "\"None\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Trace: + replacement: "\"Trace\"" + note: "V4 levels are string literals rather than branded objects; use the literal as both value and singleton type." +effect/LogLevel#Warning: + replacement: "\"Warn\"" + note: "V4 renamed both the value and singleton type from Warning to the string literal Warn." +effect/LogLevel#allLevels: + replacement: "LogLevel.values" + note: "Use the ordered v4 array of all levels, including All and None." +effect/LogLevel#fromLiteral: + replacement: "literal === \"Warning\" ? \"Warn\" : literal" + note: "No constructor is needed because v4 levels are strings. Normalize the renamed Warning literal to Warn; all other v3 literals pass through." +effect/LogLevel#greaterThan: + replacement: "LogLevel.isGreaterThan" + note: "Direct rename; ordering remains severity ordering." +effect/LogLevel#greaterThanEqual: + replacement: "LogLevel.isGreaterThanOrEqualTo" + note: "Direct rename." +effect/LogLevel#lessThan: + replacement: "LogLevel.isLessThan" + note: "Direct rename." +effect/LogLevel#lessThanEqual: + replacement: "LogLevel.isLessThanOrEqualTo" + note: "Direct rename." +effect/LogLevel#Literal: + replacement: "LogLevel.LogLevel" + note: "This is the all-level replacement after renaming Warning to Warn. LogLevel.Severity is narrower because it excludes All and None." +effect/LogLevel#locally: + replacement: "Effect.provideService(effect, References.CurrentLogLevel, level)" + note: "Current log level is now a reference. For threshold configuration, including All or None, provide References.MinimumLogLevel instead." +effect/LogLevel#LogLevel: + replacement: "LogLevel.LogLevel" + note: "The name remains, but the representation is a string union and object fields such as _tag, label, syslog, and ordinal are gone. Use toUpperCase() for labels and LogLevel.getOrdinal for ordering." diff --git a/.context/effect/migration/annotations/effect__LogSpan.yaml b/.context/effect/migration/annotations/effect__LogSpan.yaml new file mode 100644 index 000000000..8060c93d2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__LogSpan.yaml @@ -0,0 +1,9 @@ +effect/LogSpan#LogSpan: + replacement: "readonly [label: string, timestamp: number]" + note: "The module was removed. Active log spans are tuples in References.CurrentLogSpans; ordinary callers should prefer Effect.withLogSpan." +effect/LogSpan#make: + replacement: "[label, startTime] as const" + note: "Construct the tuple directly, or use Effect.withLogSpan so Effect obtains the timestamp and scopes the span." +effect/LogSpan#render: + replacement: "custom tuple formatter" + note: "No public standalone renderer remains. Built-in loggers format span tuples internally; custom formatters can render label and elapsed milliseconds themselves." diff --git a/.context/effect/migration/annotations/effect__Logger.yaml b/.context/effect/migration/annotations/effect__Logger.yaml new file mode 100644 index 000000000..b8a4a8cf4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Logger.yaml @@ -0,0 +1,108 @@ +effect/Logger#add: + replacement: "Logger.layer([logger], { mergeWithExisting: true })" + note: "Logger installation is whole-set based; mergeWithExisting reproduces add." +effect/Logger#addEffect: + replacement: "Logger.layer([loggerEffect], { mergeWithExisting: true })" + note: "Logger.layer accepts effects that construct loggers." +effect/Logger#addScoped: + replacement: "Logger.layer([scopedLoggerEffect], { mergeWithExisting: true })" + note: "Layer acquisition handles the scoped effect; the separate scoped constructor is gone." +effect/Logger#batched: + replacement: "Logger.batched(logger, { window, flush })" + note: "The trailing arguments moved into one options object. Provide any services needed by flush before constructing it." +effect/Logger#filterLogLevel: + replacement: "Logger.make(options => predicate(options.logLevel) ? Option.some(logger.log(options)) : Option.none())" + note: "No named combinator remains; rebuild the wrapper with Logger.make. Prefer References.MinimumLogLevel for ordinary threshold filtering." +effect/Logger#json: + replacement: "Logger.layer([Logger.consoleJson, Logger.tracerLogger])" + note: "Logger.layer replaces the active set. Include tracerLogger to preserve v3 built-in layer behavior, or omit it when trace log events are intentionally disabled." +effect/Logger#jsonLogger: + replacement: "Logger.formatJson" + note: "Formatter rename; v4 JSON output uses level rather than logLevel." +effect/Logger#logFmt: + replacement: "Logger.layer([Logger.consoleLogFmt, Logger.tracerLogger])" + note: "Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior." +effect/Logger#logfmtLogger: + replacement: "Logger.formatLogFmt" + note: "Formatter rename and capitalization change." +effect/Logger#Logger: + replacement: "Logger.Logger" + note: "The name remains. Logger.Options now has fiber instead of fiberId; read the id from fiber.id and annotations or spans through fiber references." +effect/Logger#Logger.Variance: + replacement: "none" + note: "Public variance metadata was removed; use Logger.Logger directly." +effect/Logger#LoggerTypeId: + replacement: "Logger.isLogger" + note: "The brand is private in v4; use the public runtime guard." +effect/Logger#map: + replacement: "Logger.map" + note: "Retained with the same output-mapping behavior." +effect/Logger#mapInput: + replacement: "Logger.make(options => logger.log({ ...options, message: f(options.message) }))" + note: "No named input contramap remains; rebuild it with Logger.make." +effect/Logger#mapInputOptions: + replacement: "Logger.make(options => logger.log(f(options)))" + note: "No named options contramap remains; rebuild it with Logger.make and adapt f to the v4 Logger.Options shape." +effect/Logger#minimumLogLevel: + replacement: "Layer.succeed(References.MinimumLogLevel, level)" + note: "Minimum log level is now a context reference." +effect/Logger#none: + replacement: "Logger.make(() => undefined)" + note: "Rebuild the no-op logger with Logger.make." +effect/Logger#pretty: + replacement: "Logger.layer([Logger.consolePretty(), Logger.tracerLogger])" + note: "Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior." +effect/Logger#prettyLogger: + replacement: "Logger.consolePretty" + note: "Direct constructor rename; call it with the same options." +effect/Logger#prettyLoggerDefault: + replacement: "Logger.consolePretty()" + note: "The prebuilt singleton became a constructor call." +effect/Logger#remove: + replacement: "Logger.layer([...desiredLoggers])" + note: "No named removal combinator remains. Declare the complete desired logger set; transform Logger.CurrentLoggers only when removing from an unknown inherited set is unavoidable." +effect/Logger#replace: + replacement: "Logger.layer([...desiredLoggers])" + note: "V4 replaces the whole active set. When replacing the old default logger, include Logger.tracerLogger explicitly if it must survive." +effect/Logger#replaceEffect: + replacement: "Logger.layer([loggerEffect, ...otherLoggers])" + note: "Logger.layer accepts effects. Explicitly list every logger that must remain active." +effect/Logger#replaceScoped: + replacement: "Logger.layer([scopedLoggerEffect, ...otherLoggers])" + note: "Logger.layer acquisition supplies the scope; explicitly list every logger that must remain active." +effect/Logger#simple: + replacement: "Logger.make(({ message }) => log(message))" + note: "V3 simple was a message-only custom logger constructor; rebuild it with Logger.make." +effect/Logger#stringLogger: + replacement: "Logger.formatSimple" + note: "The prebuilt string formatter was renamed." +effect/Logger#structured: + replacement: "Logger.layer([Logger.consoleStructured, Logger.tracerLogger])" + note: "Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior." +effect/Logger#structuredLogger: + replacement: "Logger.formatStructured" + note: "Formatter rename; its output field logLevel is now named level." +effect/Logger#succeed: + replacement: "Logger.make(() => value)" + note: "Rebuild the constant-output logger with Logger.make." +effect/Logger#sync: + replacement: "Logger.make(() => evaluate())" + note: "Rebuild the lazy-output logger; evaluate still runs once per log event." +effect/Logger#test: + replacement: "Effect.log(input).pipe(Effect.provide(Logger.layer([capturingLogger])))" + note: "No synthetic-options helper remains. Exercise the logger through the runtime and capture its output so it receives a real Fiber, cause, level, and date." +effect/Logger#withMinimumLogLevel: + replacement: "Effect.provideService(effect, References.MinimumLogLevel, level)" + note: "Replace the FiberRef-local helper with reference provisioning." +effect/Logger#withSpanAnnotations: + replacement: "custom Logger.make wrapper using options.fiber.currentSpan" + note: "No transparent generic equivalent remains. Read span identity from options.fiber.currentSpan and add it to custom output as needed." +effect/Logger#zip: + replacement: "Logger.make(options => [left.log(options), right.log(options)])" + note: "No named combinator remains; invoke both loggers and return their output tuple." +effect/Logger#zipLeft: + replacement: "Logger.make(options => { const output = left.log(options); right.log(options); return output })" + note: "Rebuild explicitly, preserving evaluation of both loggers and returning the left output." +effect/Logger#zipRight: + replacement: "Logger.make(options => { left.log(options); return right.log(options) })" + note: "Rebuild explicitly, preserving evaluation order and returning the right output." diff --git a/.context/effect/migration/annotations/effect__Mailbox.yaml b/.context/effect/migration/annotations/effect__Mailbox.yaml new file mode 100644 index 000000000..795891c68 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Mailbox.yaml @@ -0,0 +1,30 @@ +effect/Mailbox#fromStream: + replacement: "Stream.toQueue" + note: "Mailbox was renamed and folded into Queue; Stream.toQueue returns a scoped Queue.Dequeue whose error includes Cause.Done." +effect/Mailbox#into: + replacement: "Queue.into" + note: "Use Queue.into with a Queue.Enqueue whose error channel includes Cause.Done." +effect/Mailbox#isMailbox: + replacement: "Queue.isQueue" + note: "Mailbox became the completion-aware v4 Queue model." +effect/Mailbox#isReadonlyMailbox: + replacement: "Queue.isDequeue" + note: "ReadonlyMailbox became Queue.Dequeue." +effect/Mailbox#Mailbox: + replacement: "Queue.Queue" + note: "Mailbox was folded into Queue; include Cause.Done in the error channel when normal end signaling is used." +effect/Mailbox#make: + replacement: "Queue.make" + note: "Pass the v4 options object with optional capacity and strategy; a numeric capacity argument must become { capacity }." +effect/Mailbox#ReadonlyMailbox: + replacement: "Queue.Dequeue" + note: "Use explicit Queue taking operations; Queue.Dequeue is not itself an Effect yielding message chunks." +effect/Mailbox#ReadonlyTypeId: + replacement: "Queue.isDequeue" + note: "The public Mailbox type id was removed; use the Queue.isDequeue guard instead." +effect/Mailbox#toStream: + replacement: "Stream.fromQueue" + note: "Convert a Queue.Dequeue to a Stream; Cause.Done is excluded from the resulting stream error type." +effect/Mailbox#TypeId: + replacement: "Queue.isQueue" + note: "The public Mailbox type id was removed; use the Queue.isQueue guard instead." diff --git a/.context/effect/migration/annotations/effect__ManagedRuntime.yaml b/.context/effect/migration/annotations/effect__ManagedRuntime.yaml new file mode 100644 index 000000000..5daa23e94 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ManagedRuntime.yaml @@ -0,0 +1,15 @@ +"effect/ManagedRuntime#ManagedRuntime": + replacement: "ManagedRuntime.ManagedRuntime" + note: "The handle remains but is no longer an Effect; runtimeEffect/runtime became contextEffect/context, and make accepts { memoMap }." +"effect/ManagedRuntime#ManagedRuntime.Context": + replacement: "ManagedRuntime.ManagedRuntime.Services" + note: "The context extractor was renamed to Services." +"effect/ManagedRuntime#ManagedRuntimeUnify": + replacement: "none" + note: "ManagedRuntime no longer extends Effect, so its unification artifact was removed; call run methods or contextEffect explicitly." +"effect/ManagedRuntime#ManagedRuntimeUnifyIgnore": + replacement: "none" + note: "ManagedRuntime no longer extends Effect, so the Unify-ignore artifact was removed." +"effect/ManagedRuntime#TypeId": + replacement: "ManagedRuntime.isManagedRuntime" + note: "The marker is private; use the public guard for runtime narrowing." diff --git a/.context/effect/migration/annotations/effect__Match.yaml b/.context/effect/migration/annotations/effect__Match.yaml new file mode 100644 index 000000000..b04a3a0ba --- /dev/null +++ b/.context/effect/migration/annotations/effect__Match.yaml @@ -0,0 +1,42 @@ +"effect/Match#either": + replacement: "Match.result" + note: "Renamed finalizer with a container change: matched Right and unmatched Left become Result.Success and Result.Failure." +"effect/Match#MatcherTypeId": + replacement: "none" + note: "The public matcher brand was internalized. Obtain matchers from Match.type or Match.value and use their public _tag when discrimination is required." +"effect/Match#SafeRefinementId": + replacement: "none" + note: "The public safe-refinement brand was internalized. Use Predicate.Refinement, Predicate.Predicate, or a built-in Match refinement instead of constructing the brand." +"effect/Match#TypeMatcher": + replacement: "Match.TypeMatcher" + note: "The public type is retained, but its brand is private; create values with Match.type rather than implementing the interface." +"effect/Match#Types": + replacement: "Match.Types" + note: "The public type-level namespace is retained with no call-site migration." +"effect/Match#Types.ExtractAndNarrow": + replacement: "Match.Types.ExtractAndNarrow" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.MaybeReplace": + replacement: "Match.Types.MaybeReplace" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.NonFailKeys": + replacement: "Match.Types.NonFailKeys" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.PForNotMatch": + replacement: "Match.Types.PForNotMatch" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.ResolvePred": + replacement: "Match.Types.ResolvePred" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.SafeRefinementR": + replacement: "Match.Types.SafeRefinementR" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.ToInvertedRefinement": + replacement: "Match.Types.ToInvertedRefinement" + note: "The type-only matching helper is retained unchanged." +"effect/Match#Types.ToSafeRefinement": + replacement: "Match.Types.ToSafeRefinement" + note: "The type-only matching helper is retained unchanged." +"effect/Match#ValueMatcher": + replacement: "Match.ValueMatcher" + note: "The type is retained, but value now uses Result instead of Either and the brand is private; create values with Match.value." diff --git a/.context/effect/migration/annotations/effect__MergeDecision.yaml b/.context/effect/migration/annotations/effect__MergeDecision.yaml new file mode 100644 index 000000000..c2606ba0a --- /dev/null +++ b/.context/effect/migration/annotations/effect__MergeDecision.yaml @@ -0,0 +1,24 @@ +"effect/MergeDecision#Await": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#AwaitConst": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#Done": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#isMergeDecision": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#match": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#MergeDecision": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#MergeDecision.Variance": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." +"effect/MergeDecision#MergeDecisionTypeId": + replacement: "Channel.merge" + note: "Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured." diff --git a/.context/effect/migration/annotations/effect__MergeState.yaml b/.context/effect/migration/annotations/effect__MergeState.yaml new file mode 100644 index 000000000..beb3f5811 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MergeState.yaml @@ -0,0 +1,3 @@ +effect/MergeState: + replacement: none + note: Internal execution state of the removed Channel.mergeWith implementation. V4 Channel.merge manages its fibers and queues internally and exposes only a haltStrategy option. diff --git a/.context/effect/migration/annotations/effect__MergeStrategy.yaml b/.context/effect/migration/annotations/effect__MergeStrategy.yaml new file mode 100644 index 000000000..de08cbd87 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MergeStrategy.yaml @@ -0,0 +1,27 @@ +"effect/MergeStrategy#BackPressure": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#BufferSliding": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#isBackPressure": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#isBufferSliding": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#isMergeStrategy": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#match": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#MergeStrategy": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#MergeStrategy.Proto": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." +"effect/MergeStrategy#MergeStrategyTypeId": + replacement: "Channel.mergeAll" + note: "Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap." diff --git a/.context/effect/migration/annotations/effect__Metric.yaml b/.context/effect/migration/annotations/effect__Metric.yaml new file mode 100644 index 000000000..67a83b2e3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Metric.yaml @@ -0,0 +1,105 @@ +effect/Metric#fiberActive: + replacement: "Metric.enableRuntimeMetrics + Metric.snapshot" + note: "The concrete metric is no longer exported. Enable runtime metrics, then read the Gauge snapshot whose id is child_fibers_active." +effect/Metric#fiberFailures: + replacement: "Metric.enableRuntimeMetrics + Metric.snapshot" + note: "The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child_fiber_failures." +effect/Metric#fiberLifetimes: + replacement: "none" + note: "The built-in lifetime histogram was removed. Define a Metric.timer and instrument selected effects with Effect.trackDuration when lifetime data is required." +effect/Metric#fiberStarted: + replacement: "Metric.enableRuntimeMetrics + Metric.snapshot" + note: "The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child_fibers_started." +effect/Metric#fiberSuccesses: + replacement: "Metric.enableRuntimeMetrics + Metric.snapshot" + note: "The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child_fiber_successes." +effect/Metric#fromMetricKey: + replacement: "Metric.counter / Metric.gauge / Metric.frequency / Metric.histogram / Metric.summary" + note: "MetricKey and MetricKeyType were removed. Construct the required primitive metric directly." +effect/Metric#globalMetricRegistry: + replacement: "Metric.MetricRegistry" + note: "The process-global registry became a Context.Reference whose service is a Map. Access it in context or provide a fresh Map for isolation; use Metric.snapshot for normal reads." +effect/Metric#increment: + replacement: "Metric.update / Metric.modify" + note: "Use Metric.update(counter, 1 or 1n) for counters and Metric.modify(gauge, 1 or 1n) for gauges; gauge update sets an absolute value while modify adds a delta." +effect/Metric#incrementBy: + replacement: "Metric.update / Metric.modify" + note: "Use Metric.update(counter, amount) for counters and Metric.modify(gauge, amount) for gauges." +effect/Metric#make: + replacement: "none" + note: "The low-level arbitrary metric constructor was removed. Use a public primitive constructor and compose with mapInput, withConstantInput, and withAttributes." +effect/Metric#map: + replacement: "Metric.value + Effect.map" + note: "Metric-level state mapping was removed. Transform a read with Effect.map(Metric.value(metric), f)." +effect/Metric#mapType: + replacement: "none" + note: "Drop this call. V4 Metric has no key-type type parameter and exposes a fixed runtime type discriminator." +effect/Metric#Metric: + replacement: "Metric.Metric" + note: "Drop the v3 key-type parameter. Metrics are no longer callable; use Effect.trackSuccesses for instrumentation and Metric.update or Metric.value for operations." +effect/Metric#Metric.Variance: + replacement: "none" + note: "The public variance interface was removed; Metric carries variance markers directly." +effect/Metric#MetricApply: + replacement: "none" + note: "Removed with Metric.make; v4 has no public low-level custom-metric constructor type." +effect/Metric#MetricTypeId: + replacement: "Metric.isMetric" + note: "The public unique-symbol type id was removed; use Metric.isMetric for runtime refinement." +effect/Metric#set: + replacement: "Metric.update" + note: "Use Metric.update(gauge, value); v4 update replaces a gauge's current value." +effect/Metric#succeed: + replacement: "none" + note: "Constant synthetic metrics were removed. Keep constants outside the metric and use Effect.succeed when an Effect value is required." +effect/Metric#summaryTimestamp: + replacement: "Metric.summaryWithTimestamp" + note: "Renamed and called as Metric.summaryWithTimestamp(name, options). Remove the v3 error option; inputs remain value/timestamp pairs." +effect/Metric#sync: + replacement: "none" + note: "Lazy synthetic metrics were removed. Keep the computation outside the metric and use Effect.sync when an Effect value is required." +effect/Metric#tagged: + replacement: "Metric.withAttributes" + note: "Replace tags with attributes, for example Metric.withAttributes(metric, { [key]: value })." +effect/Metric#taggedWithLabels: + replacement: "Metric.withAttributes" + note: "Replace MetricLabel objects with a string record or array of string tuples passed to Metric.withAttributes." +effect/Metric#taggedWithLabelsInput: + replacement: "Metric.withAttributes + Metric.update" + note: "No dynamic-attribute transform remains. Compute attributes at each update or tracking site, wrap with Metric.withAttributes, then update the metric." +effect/Metric#timerWithBoundaries: + replacement: "Metric.timer" + note: "Use Metric.timer(name, { boundaries, description }); boundaries moved into the options object." +effect/Metric#trackAll: + replacement: "Effect.track" + note: "Moved to Effect; use effect.pipe(Effect.track(metric, () => input))." +effect/Metric#trackDefect: + replacement: "Effect.trackDefects" + note: "Moved to Effect; use effect.pipe(Effect.trackDefects(metric))." +effect/Metric#trackDefectWith: + replacement: "Effect.trackDefects" + note: "Moved to Effect; pass the mapper as the optional second argument." +effect/Metric#trackDurationWith: + replacement: "Effect.trackDuration" + note: "Moved to Effect; pass the mapper as the optional second argument. V4 records duration on every Exit, whereas v3 updated only after success." +effect/Metric#trackError: + replacement: "Effect.trackErrors" + note: "Moved to Effect; use effect.pipe(Effect.trackErrors(metric))." +effect/Metric#trackErrorWith: + replacement: "Effect.trackErrors" + note: "Moved to Effect; pass the mapper as the optional second argument." +effect/Metric#trackSuccess: + replacement: "Effect.trackSuccesses" + note: "Moved to Effect; use effect.pipe(Effect.trackSuccesses(metric))." +effect/Metric#trackSuccessWith: + replacement: "Effect.trackSuccesses" + note: "Moved to Effect; pass the mapper as the optional second argument." +effect/Metric#unsafeSnapshot: + replacement: "Metric.snapshotUnsafe" + note: "Renamed and now requires an explicit Context.Context. It returns structural snapshots rather than MetricPair values." +effect/Metric#withNow: + replacement: "Metric.summary" + note: "Metric.summary reads the current Clock automatically; use Metric.summaryWithTimestamp when timestamps are supplied explicitly. The generic timestamp-injecting combinator was removed." +effect/Metric#zip: + replacement: "Effect.all + Metric.update / Metric.value" + note: "Composite metrics were removed. Use Effect.all to update both metrics or combine their Metric.value reads." diff --git a/.context/effect/migration/annotations/effect__MetricBoundaries.yaml b/.context/effect/migration/annotations/effect__MetricBoundaries.yaml new file mode 100644 index 000000000..cc3273029 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricBoundaries.yaml @@ -0,0 +1,18 @@ +effect/MetricBoundaries#exponential: + replacement: "Metric.exponentialBoundaries" + note: "Moved into effect/Metric and now returns ReadonlyArray. V4 also filters non-positive boundaries." +effect/MetricBoundaries#fromIterable: + replacement: "Metric.boundariesFromIterable" + note: "Moved into effect/Metric and now returns an unbranded ReadonlyArray; v4 removes non-positive values before appending Infinity." +effect/MetricBoundaries#linear: + replacement: "Metric.linearBoundaries" + note: "Moved into effect/Metric, but the compared v4 implementation uses start + i + width rather than v3's start + i * width. Preserve the v3 formula manually when width is not 1." +effect/MetricBoundaries#MetricBoundaries: + replacement: "ReadonlyArray" + note: "The wrapper was removed; Metric.histogram accepts plain boundaries in its options." +effect/MetricBoundaries#isMetricBoundaries: + replacement: "none" + note: "Boundaries are unbranded arrays, so the guard and public type-id symbol have no replacement." +effect/MetricBoundaries#MetricBoundariesTypeId: + replacement: "none" + note: "Boundaries are unbranded arrays, so the guard and public type-id symbol have no replacement." diff --git a/.context/effect/migration/annotations/effect__MetricHook.yaml b/.context/effect/migration/annotations/effect__MetricHook.yaml new file mode 100644 index 000000000..9350c36c5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricHook.yaml @@ -0,0 +1,39 @@ +effect/MetricHook#counter: + replacement: "Metric.counter" + note: "Hook construction was folded into the complete Metric.counter constructor; hooks are internal." +effect/MetricHook#frequency: + replacement: "Metric.frequency" + note: "Hook construction was folded into the complete Metric.frequency constructor; hooks are internal." +effect/MetricHook#gauge: + replacement: "Metric.gauge" + note: "Hook construction was folded into the complete Metric.gauge constructor; hooks are internal." +effect/MetricHook#histogram: + replacement: "Metric.histogram" + note: "Hook construction was folded into the complete Metric.histogram constructor; hooks are internal." +effect/MetricHook#summary: + replacement: "Metric.summary" + note: "Hook construction was folded into the complete Metric.summary constructor; hooks are internal." +effect/MetricHook#MetricHook: + replacement: "Metric.Metric.Hooks" + note: "The closest public structural interface is Metric.Metric.Hooks; get, update, and modify also receive a Context." +effect/MetricHook#MetricHook.Root: + replacement: "Metric.Metric.Hooks" + note: "The named aliases were removed; specialize the public Hooks interface directly when low-level typing is unavoidable." +effect/MetricHook#MetricHook.Untyped: + replacement: "Metric.Metric.Hooks" + note: "The named aliases were removed; specialize the public Hooks interface directly when low-level typing is unavoidable." +effect/MetricHook#make: + replacement: "none" + note: "There is no public hook constructor; metric classes create and attach hooks internally." +effect/MetricHook#onModify: + replacement: "none" + note: "The operation-specific hook decorators were removed. Metric.mapInput cannot distinguish update from modify." +effect/MetricHook#onUpdate: + replacement: "none" + note: "The operation-specific hook decorators were removed. Metric.mapInput cannot distinguish update from modify." +effect/MetricHook#MetricHook.Variance: + replacement: "none" + note: "Hooks are structural and unbranded; the variance helper and public symbol were removed." +effect/MetricHook#MetricHookTypeId: + replacement: "none" + note: "Hooks are structural and unbranded; the variance helper and public symbol were removed." diff --git a/.context/effect/migration/annotations/effect__MetricKey.yaml b/.context/effect/migration/annotations/effect__MetricKey.yaml new file mode 100644 index 000000000..512d460e5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricKey.yaml @@ -0,0 +1,33 @@ +effect/MetricKey#counter: + replacement: "Metric.counter" + note: "The key and key type were merged into the complete Metric.counter constructor." +effect/MetricKey#frequency: + replacement: "Metric.frequency" + note: "The key and key type were merged into the complete Metric.frequency constructor." +effect/MetricKey#gauge: + replacement: "Metric.gauge" + note: "The key and key type were merged into the complete Metric.gauge constructor." +effect/MetricKey#histogram: + replacement: "Metric.histogram" + note: "The key and key type were merged into the complete Metric.histogram constructor." +effect/MetricKey#summary: + replacement: "Metric.summary" + note: "The key and key type were merged into the complete Metric.summary constructor." +effect/MetricKey#MetricKey: + replacement: "Metric.Metric" + note: "Key identity, metadata, and operations are combined in Metric." +effect/MetricKey#MetricKey.Untyped: + replacement: "Metric.Metric" + note: "The separate untyped key alias was removed; use an untyped complete Metric only where required." +effect/MetricKey#MetricKey.Variance: + replacement: "Metric.Metric" + note: "There is no separate key variance interface; variance is carried by Metric's Input and State phantom fields." +effect/MetricKey#isMetricKey: + replacement: "Metric.isMetric" + note: "Keys became complete metrics; use the complete-metric runtime guard." +effect/MetricKey#MetricKeyTypeId: + replacement: "none" + note: "The key brand was removed; Metric's protocol key is internal." +effect/MetricKey#taggedWithLabels: + replacement: "Metric.withAttributes" + note: "Labels became attributes. Pass a string record or array of string tuples." diff --git a/.context/effect/migration/annotations/effect__MetricKeyType.yaml b/.context/effect/migration/annotations/effect__MetricKeyType.yaml new file mode 100644 index 000000000..e4288478c --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricKeyType.yaml @@ -0,0 +1,66 @@ +effect/MetricKeyType#counter: + replacement: "Metric.counter" + note: "The standalone descriptor was folded into the complete Metric.counter constructor." +effect/MetricKeyType#frequency: + replacement: "Metric.frequency" + note: "The standalone descriptor was folded into the complete Metric.frequency constructor." +effect/MetricKeyType#gauge: + replacement: "Metric.gauge" + note: "The standalone descriptor was folded into the complete Metric.gauge constructor." +effect/MetricKeyType#histogram: + replacement: "Metric.histogram" + note: "The standalone descriptor was folded into the complete Metric.histogram constructor." +effect/MetricKeyType#summary: + replacement: "Metric.summary" + note: "The standalone descriptor was folded into the complete Metric.summary constructor." +effect/MetricKeyType#MetricKeyType: + replacement: "Metric.Metric" + note: "Input/state typing and kind configuration now live on the complete Metric." +effect/MetricKeyType#MetricKeyType.InType: + replacement: "Metric.Metric.Input" + note: "Use Metric.Metric.Input to extract a metric's input type." +effect/MetricKeyType#MetricKeyType.OutType: + replacement: "Metric.Metric.State" + note: "Use Metric.Metric.State to extract a metric's state type." +effect/MetricKeyType#MetricKeyType.Untyped: + replacement: "Metric.Metric" + note: "The key-type descriptor no longer exists independently of a metric." +effect/MetricKeyType#MetricKeyType.Variance: + replacement: "none" + note: "The descriptor variance interface was removed; complete Metric carries Input and State variance." +effect/MetricKeyType#isMetricKeyType: + replacement: "Metric.isMetric" + note: "Standalone key-type values were removed; test complete metrics instead." +effect/MetricKeyType#isCounterKey: + replacement: "Metric.isMetric + metric.type" + note: "Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant." +effect/MetricKeyType#isFrequencyKey: + replacement: "Metric.isMetric + metric.type" + note: "Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant." +effect/MetricKeyType#isGaugeKey: + replacement: "Metric.isMetric + metric.type" + note: "Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant." +effect/MetricKeyType#isHistogramKey: + replacement: "Metric.isMetric + metric.type" + note: "Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant." +effect/MetricKeyType#isSummaryKey: + replacement: "Metric.isMetric + metric.type" + note: "Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant." +effect/MetricKeyType#MetricKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." +effect/MetricKeyType#CounterKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." +effect/MetricKeyType#FrequencyKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." +effect/MetricKeyType#GaugeKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." +effect/MetricKeyType#HistogramKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." +effect/MetricKeyType#SummaryKeyTypeTypeId: + replacement: "none" + note: "All public key-type symbols were removed; use a complete metric's string type discriminant." diff --git a/.context/effect/migration/annotations/effect__MetricLabel.yaml b/.context/effect/migration/annotations/effect__MetricLabel.yaml new file mode 100644 index 000000000..27cc1bbbf --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricLabel.yaml @@ -0,0 +1,12 @@ +effect/MetricLabel#MetricLabel: + replacement: "[string, string]" + note: "A label is now an ordinary attribute tuple; collections are Metric.Metric.Attributes or Metric.Metric.AttributeSet." +effect/MetricLabel#make: + replacement: "[key, value]" + note: "Construct an ordinary tuple, or place the pair in an attribute record passed to Metric.withAttributes or a metric constructor." +effect/MetricLabel#isMetricLabel: + replacement: "none" + note: "Attributes are plain tuples or records, so there is no branded guard or type-id symbol." +effect/MetricLabel#MetricLabelTypeId: + replacement: "none" + note: "Attributes are plain tuples or records, so there is no branded guard or type-id symbol." diff --git a/.context/effect/migration/annotations/effect__MetricPair.yaml b/.context/effect/migration/annotations/effect__MetricPair.yaml new file mode 100644 index 000000000..14cebe395 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricPair.yaml @@ -0,0 +1,18 @@ +effect/MetricPair#MetricPair: + replacement: "Metric.Metric.Snapshot" + note: "Registry key/state pairs became discriminated snapshots containing id, type, description, attributes, and state." +effect/MetricPair#MetricPair.Untyped: + replacement: "Metric.Metric.Snapshot" + note: "Registry key/state pairs became discriminated snapshots containing id, type, description, attributes, and state." +effect/MetricPair#make: + replacement: "Metric.snapshot" + note: "There is no pair constructor. Obtain snapshots with Metric.snapshot or Metric.snapshotUnsafe; manually constructed data can satisfy Metric.Metric.SnapshotProto." +effect/MetricPair#unsafeMake: + replacement: "Metric.snapshot" + note: "There is no pair constructor. Obtain snapshots with Metric.snapshot or Metric.snapshotUnsafe; manually constructed data can satisfy Metric.Metric.SnapshotProto." +effect/MetricPair#MetricPair.Variance: + replacement: "none" + note: "Snapshots are structural, so the pair variance helper and brand symbol were removed." +effect/MetricPair#MetricPairTypeId: + replacement: "none" + note: "Snapshots are structural, so the pair variance helper and brand symbol were removed." diff --git a/.context/effect/migration/annotations/effect__MetricPolling.yaml b/.context/effect/migration/annotations/effect__MetricPolling.yaml new file mode 100644 index 000000000..9a5d183bd --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricPolling.yaml @@ -0,0 +1,24 @@ +effect/MetricPolling#MetricPolling: + replacement: "local { metric, poll } record" + note: "The module was removed. Keep a local record pairing a Metric with its polling Effect when this abstraction is still useful." +effect/MetricPolling#make: + replacement: "({ metric, poll })" + note: "No public constructor remains; use the local record directly." +effect/MetricPolling#poll: + replacement: "self.poll" + note: "Access the polling Effect from the local record." +effect/MetricPolling#pollAndUpdate: + replacement: "Effect.flatMap(self.poll, input => Metric.update(self.metric, input))" + note: "Compose polling and metric update directly." +effect/MetricPolling#retry: + replacement: "Effect.retry" + note: "Retry the poll Effect and retain the same metric in the local record." +effect/MetricPolling#launch: + replacement: "Effect.repeat + Effect.forkScoped" + note: "Repeat polling, updating, and reading with the schedule, then forkScoped." +effect/MetricPolling#collectAll: + replacement: "Effect.forEach + Metric.update/value" + note: "No combined metric replacement exists. Poll records, update each metric, and collect states explicitly." +effect/MetricPolling#MetricPollingTypeId: + replacement: "none" + note: "The polling wrapper and its brand were removed." diff --git a/.context/effect/migration/annotations/effect__MetricRegistry.yaml b/.context/effect/migration/annotations/effect__MetricRegistry.yaml new file mode 100644 index 000000000..48b0f2c6f --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricRegistry.yaml @@ -0,0 +1,9 @@ +effect/MetricRegistry#MetricRegistry: + replacement: "Metric.MetricRegistry" + note: "The method-bearing registry became a Context.Reference whose service is a Map. Metrics register metadata and hooks lazily." +effect/MetricRegistry#make: + replacement: "new Map>()" + note: "Provide a fresh Map to Metric.MetricRegistry for isolation. Read it through Metric.snapshot or snapshotUnsafe." +effect/MetricRegistry#MetricRegistryTypeId: + replacement: "none" + note: "The registry service is an ordinary Map behind a Context.Reference and has no public brand." diff --git a/.context/effect/migration/annotations/effect__MetricState.yaml b/.context/effect/migration/annotations/effect__MetricState.yaml new file mode 100644 index 000000000..25a25f98d --- /dev/null +++ b/.context/effect/migration/annotations/effect__MetricState.yaml @@ -0,0 +1,60 @@ +effect/MetricState#counter: + replacement: "Metric.CounterState" + note: "There is no state constructor. Obtain the structural state with Metric.value(Metric.counter(...))." +effect/MetricState#frequency: + replacement: "Metric.FrequencyState" + note: "There is no state constructor. Obtain the structural state with Metric.value(Metric.frequency(...))." +effect/MetricState#gauge: + replacement: "Metric.GaugeState" + note: "There is no state constructor. Obtain the structural state with Metric.value(Metric.gauge(...))." +effect/MetricState#histogram: + replacement: "Metric.HistogramState" + note: "There is no state constructor. Obtain the structural state with Metric.value(Metric.histogram(...))." +effect/MetricState#summary: + replacement: "Metric.SummaryState" + note: "There is no state constructor. Obtain the structural state with Metric.value(Metric.summary(...))." +effect/MetricState#MetricState: + replacement: "Metric.Metric.State" + note: "The common branded state model was removed; extract a complete metric's state with Metric.Metric.State or use a concrete state interface." +effect/MetricState#MetricState.Untyped: + replacement: "Metric.Metric.Snapshot['state']" + note: "Use the state union from Metric.Metric.Snapshot, or explicitly union the five structural state interfaces." +effect/MetricState#MetricState.Variance: + replacement: "none" + note: "States are structural objects and no longer carry a variance brand." +effect/MetricState#isMetricState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#isCounterState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#isFrequencyState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#isGaugeState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#isHistogramState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#isSummaryState: + replacement: "none" + note: "Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination." +effect/MetricState#MetricStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." +effect/MetricState#CounterStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." +effect/MetricState#FrequencyStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." +effect/MetricState#GaugeStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." +effect/MetricState#HistogramStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." +effect/MetricState#SummaryStateTypeId: + replacement: "none" + note: "All state brand symbols were removed; v4 state interfaces are structural." diff --git a/.context/effect/migration/annotations/effect__Micro.yaml b/.context/effect/migration/annotations/effect__Micro.yaml new file mode 100644 index 000000000..562cd279b --- /dev/null +++ b/.context/effect/migration/annotations/effect__Micro.yaml @@ -0,0 +1,500 @@ +effect/Micro#all: + replacement: "Effect.all" + note: "Micro was removed in v4; use Effect.all with the same iterable-or-record input and concurrency/discard options." +effect/Micro#acquireUseRelease: + replacement: "Effect.acquireUseRelease" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#All.IsDiscard: + replacement: "Effect.All.IsDiscard" + note: "Type-level helper moved to the Effect.All namespace." +effect/Micro#All.MicroAny: + replacement: "Effect.All.EffectAny" + note: "Renamed: MicroAny becomes EffectAny in the Effect.All namespace." +effect/Micro#All.Return: + replacement: "Effect.All.Return" + note: "Type-level helper moved to the Effect.All namespace." +effect/Micro#as: + replacement: "Effect.as" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#asSome: + replacement: "Effect.asSome" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#asVoid: + replacement: "Effect.asVoid" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#async: + replacement: "Effect.callback" + note: "Renamed: the async constructor is Effect.callback in v4. Same resume/AbortSignal semantics." + example: "Effect.callback((resume) => resume(Effect.succeed(1)))" +effect/Micro#bind: + replacement: "Effect.bind" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#bindTo: + replacement: "Effect.bindTo" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#catchAll: + replacement: "Effect.catch" + note: "Renamed: catchAll is Effect.catch in v4." +effect/Micro#catchAllCause: + replacement: "Effect.catchCause" + note: "Renamed: catchAllCause is Effect.catchCause in v4." +effect/Micro#catchAllDefect: + replacement: "Effect.catchDefect" + note: "Renamed: catchAllDefect is Effect.catchDefect in v4." +effect/Micro#catchIf: + replacement: "Effect.catchIf" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#catchTag: + replacement: "Effect.catchTag" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#causeDie: + replacement: "Cause.die" + note: "MicroCause was replaced by the unified effect/Cause module in v4." +effect/Micro#causeFail: + replacement: "Cause.fail" + note: "MicroCause was replaced by the unified effect/Cause module in v4." +effect/Micro#causeInterrupt: + replacement: "Cause.interrupt" + note: "MicroCause was replaced by the unified effect/Cause module in v4. Takes an optional fiber id." +effect/Micro#causeIsDie: + replacement: "Cause.hasDies" + note: "v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasDies checks for Die reasons; use Cause.isDieReason for a single Reason value." +effect/Micro#causeIsFail: + replacement: "Cause.hasFails" + note: "v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasFails checks for Fail reasons; use Cause.isFailReason for a single Reason value." +effect/Micro#causeIsInterrupt: + replacement: "Cause.hasInterrupts" + note: "v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasInterrupts checks for Interrupt reasons (see also Cause.hasInterruptsOnly)." +effect/Micro#causeSquash: + replacement: "Cause.squash" + note: "Same behavior in the unified effect/Cause module." +effect/Micro#causeWithTrace: + replacement: "Cause.annotate" + note: "v4 causes carry structured annotations instead of a traces array; attach trace data with Cause.annotate (e.g. the Cause.StackTrace service). v4 also captures failure stack traces automatically." +effect/Micro#context: + replacement: "Effect.context" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#CurrentConcurrency: + replacement: "none" + note: "Removed in v4 (no fiber-wide concurrency reference). Pass a { concurrency } option directly to the operations that fan out, e.g. Effect.all or Effect.forEach." +effect/Micro#CurrentScheduler: + replacement: "References.Scheduler" + note: "The scheduler reference lives in effect/References (also exported from effect/Scheduler as Scheduler.Scheduler). Override it with Effect.provideService/Effect.updateService." +effect/Micro#delay: + replacement: "Effect.delay" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#Do: + replacement: "Effect.Do" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#either: + replacement: "Effect.result" + note: "Either was replaced by Result in v4: Effect.result yields Result.Result instead of Either." +effect/Micro#ensuring: + replacement: "Effect.ensuring" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#Error: + replacement: "Data.Error" + note: "The yieldable error base class constructor is Data.Error from effect/Data in v4." +effect/Micro#exit: + replacement: "Effect.exit" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#exitDie: + replacement: "Exit.die" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#exitFail: + replacement: "Exit.fail" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#exitFailCause: + replacement: "Exit.failCause" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#exitInterrupt: + replacement: "Exit.interrupt" + note: "MicroExit was replaced by the unified effect/Exit module in v4. Takes an optional fiber id." +effect/Micro#exitIsDie: + replacement: "Exit.hasDies" + note: "v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasDies checks the failure cause for Die reasons." +effect/Micro#exitIsFail: + replacement: "Exit.hasFails" + note: "v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasFails checks the failure cause for Fail reasons." +effect/Micro#exitIsFailure: + replacement: "Exit.isFailure" + note: "Same refinement in the unified effect/Exit module." +effect/Micro#exitIsInterrupt: + replacement: "Exit.hasInterrupts" + note: "v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasInterrupts checks the failure cause for Interrupt reasons." +effect/Micro#exitIsSuccess: + replacement: "Exit.isSuccess" + note: "Same refinement in the unified effect/Exit module." +effect/Micro#exitSucceed: + replacement: "Exit.succeed" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#exitVoid: + replacement: "Exit.void" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#exitVoidAll: + replacement: "Exit.asVoidAll" + note: "Renamed: exitVoidAll becomes Exit.asVoidAll in the unified effect/Exit module." +effect/Micro#fail: + replacement: "Effect.fail" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#failCause: + replacement: "Effect.failCause" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#failCauseSync: + replacement: "Effect.failCauseSync" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#failSync: + replacement: "Effect.failSync" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#fiberAwait: + replacement: "Fiber.await" + note: "Fiber operations moved to the effect/Fiber module in v4." +effect/Micro#fiberInterrupt: + replacement: "Fiber.interrupt" + note: "Fiber operations moved to the effect/Fiber module in v4." +effect/Micro#fiberInterruptAll: + replacement: "Fiber.interruptAll" + note: "Fiber operations moved to the effect/Fiber module in v4." +effect/Micro#fiberJoin: + replacement: "Fiber.join" + note: "Fiber operations moved to the effect/Fiber module in v4." +effect/Micro#filter: + replacement: "Effect.filter" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#filterMap: + replacement: "Effect.filterMap" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#filterOrFail: + replacement: "Effect.filterOrFail" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#filterOrFailCause: + replacement: "Effect.filterOrElse" + note: "No direct equivalent; use Effect.filterOrElse and fail with a cause in the fallback." + example: "Effect.filterOrElse(effect, predicate, { orElse: () => Effect.failCause(Cause.die(\"invalid\")) })" +effect/Micro#flatten: + replacement: "Effect.flatten" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#flip: + replacement: "Effect.flip" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#forkDaemon: + replacement: "Effect.forkDetach" + note: "Renamed: forkDaemon becomes Effect.forkDetach (fork detached from the parent's lifetime)." +effect/Micro#forkIn: + replacement: "Effect.forkIn" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#forkScoped: + replacement: "Effect.forkScoped" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#fromEither: + replacement: "Effect.fromResult" + note: "Either was replaced by Result in v4: convert Result.Result values with Effect.fromResult." +effect/Micro#fromOption: + replacement: "Effect.fromOption" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#ignore: + replacement: "Effect.ignore" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#ignoreLogged: + replacement: "Effect.ignore" + note: "Removed; log explicitly before ignoring." + example: "effect.pipe(Effect.tapCause((cause) => Effect.logError(cause)), Effect.ignore)" +effect/Micro#interrupt: + replacement: "Effect.interrupt" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#interruptible: + replacement: "Effect.interruptible" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#isMicro: + replacement: "Effect.isEffect" + note: "Micro values are plain Effects in v4; use Effect.isEffect." +effect/Micro#isMicroCause: + replacement: "Cause.isCause" + note: "MicroCause was replaced by the unified effect/Cause module in v4." +effect/Micro#isMicroExit: + replacement: "Exit.isExit" + note: "MicroExit was replaced by the unified effect/Exit module in v4." +effect/Micro#let: + replacement: "Effect.let" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#map: + replacement: "Effect.map" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#mapErrorCause: + replacement: "Effect.catchCause" + note: "No direct equivalent; transform the cause by catching it and re-failing." + example: "Effect.catchCause(effect, (cause) => Effect.failCause(Cause.map(cause, transformError)))" +effect/Micro#match: + replacement: "Effect.match" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#matchCause: + replacement: "Effect.matchCause" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#matchCauseEffect: + replacement: "Effect.matchCauseEffect" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#MaxOpsBeforeYield: + replacement: "References.MaxOpsBeforeYield" + note: "The reference lives in effect/References (also exported from effect/Scheduler). Override it with Effect.updateService." +effect/Micro#Micro: + replacement: "Effect.Effect" + note: "The Micro type is Effect.Effect in v4; the v4 Effect runtime is itself lightweight." +effect/Micro#Micro.Context: + replacement: "Effect.Services" + note: "Type extractor renamed: Micro.Context becomes Effect.Services in v4." +effect/Micro#Micro.Error: + replacement: "Effect.Error" + note: "Type extractor: Micro.Error becomes Effect.Error in v4." +effect/Micro#Micro.Success: + replacement: "Effect.Success" + note: "Type extractor: Micro.Success becomes Effect.Success in v4." +effect/Micro#MicroCause: + replacement: "Cause.Cause" + note: "MicroCause becomes Cause.Cause. Note v4 Cause holds a list of failure reasons (Fail | Die | Interrupt) rather than being a single tagged variant." +effect/Micro#MicroCause.Die: + replacement: "Cause.Die" + note: "The Die variant is a Reason in v4: Cause.Die from effect/Cause." +effect/Micro#MicroCause.Error: + replacement: "Cause.Cause.Error" + note: "Type extractor: use the Error helper in the Cause.Cause namespace to extract the error type." +effect/Micro#MicroCause.Fail: + replacement: "Cause.Fail" + note: "The Fail variant is a Reason in v4: Cause.Fail from effect/Cause." +effect/Micro#MicroCause.Interrupt: + replacement: "Cause.Interrupt" + note: "The Interrupt variant is a Reason in v4: Cause.Interrupt from effect/Cause." +effect/Micro#MicroCause.Proto: + replacement: "Cause.Cause.ReasonProto" + note: "Internal prototype type; the closest v4 equivalent is the ReasonProto interface in the Cause.Cause namespace. Rarely needed directly." +effect/Micro#MicroCauseTypeId: + replacement: "Cause.TypeId" + note: "Use Cause.TypeId from effect/Cause (value is \"~effect/Cause\")." +effect/Micro#MicroExit: + replacement: "Exit.Exit" + note: "MicroExit becomes Exit.Exit from effect/Exit. In v4 Exit is a subtype of Effect." +effect/Micro#MicroExit.Failure: + replacement: "Exit.Failure" + note: "MicroExit.Failure becomes Exit.Failure from effect/Exit." +effect/Micro#MicroExit.Proto: + replacement: "Exit.Exit.Proto" + note: "Internal prototype type; v4 exposes the shared base as Proto in the Exit.Exit namespace. Rarely needed directly." +effect/Micro#MicroExit.Success: + replacement: "Exit.Success" + note: "MicroExit.Success becomes Exit.Success from effect/Exit." +effect/Micro#MicroExitTypeId: + replacement: "none" + note: "v4 Exit is a subtype of Effect and has no dedicated TypeId; use Exit.isExit to identify exits." +effect/Micro#MicroFiber: + replacement: "Fiber.Fiber" + note: "MicroFiber becomes Fiber.Fiber from effect/Fiber." +effect/Micro#MicroFiber.Variance: + replacement: "none" + note: "Type-level variance helper with no public v4 equivalent; the v4 Fiber.Fiber interface carries variance directly." +effect/Micro#MicroFiberTypeId: + replacement: "none" + note: "No public TypeId on v4 fibers; use Fiber.isFiber to identify fibers." +effect/Micro#MicroIterator: + replacement: "Effect.EffectIterator" + note: "Renamed: MicroIterator becomes Effect.EffectIterator (generator support for Effect.gen)." +effect/Micro#MicroSchedule: + replacement: "Schedule.Schedule" + note: "v3 MicroSchedule was a plain function (attempt, elapsedMillis) => Option; v4 uses the first-class Schedule.Schedule type from effect/Schedule." +effect/Micro#MicroScheduler: + replacement: "Scheduler.Scheduler" + note: "The scheduler interface lives in effect/Scheduler in v4." +effect/Micro#MicroSchedulerDefault: + replacement: "Scheduler.MixedScheduler" + note: "The default task scheduler implementation in v4 is Scheduler.MixedScheduler from effect/Scheduler." +effect/Micro#MicroScope: + replacement: "Scope.Scope" + note: "MicroScope becomes Scope.Scope from effect/Scope; the closeable variant is Scope.Closeable." +effect/Micro#MicroScopeTypeId: + replacement: "none" + note: "No public TypeId on v4 scopes; use the Scope.Scope service key to access the current scope." +effect/Micro#MicroTypeLambda: + replacement: "Effect.EffectTypeLambda" + note: "Renamed: MicroTypeLambda becomes Effect.EffectTypeLambda." +effect/Micro#MicroUnify: + replacement: "Effect.EffectUnify" + note: "Renamed: MicroUnify becomes Effect.EffectUnify." +effect/Micro#MicroUnifyIgnore: + replacement: "none" + note: "Removed; v4 Effect declares its unify-ignore slot inline and exposes no named UnifyIgnore interface." +effect/Micro#NoSuchElementException: + replacement: "Cause.NoSuchElementError" + note: "Renamed and moved: NoSuchElementException becomes Cause.NoSuchElementError from effect/Cause." +effect/Micro#onExit: + replacement: "Effect.onExit" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#onInterrupt: + replacement: "Effect.onInterrupt" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#option: + replacement: "Effect.option" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#orDie: + replacement: "Effect.orDie" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#orElseSucceed: + replacement: "Effect.orElseSucceed" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#promise: + replacement: "Effect.promise" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#race: + replacement: "Effect.race" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#raceAll: + replacement: "Effect.raceAll" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#raceFirst: + replacement: "Effect.raceFirst" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#repeatExit: + replacement: "Effect.repeat" + note: "Removed; use Effect.repeat with while/until/times/schedule options. To inspect failures while looping, run the body through Effect.exit and repeat on the Exit value." + example: "Effect.repeat(Effect.exit(effect), { while: (exit) => Exit.isFailure(exit), times: 3 })" +effect/Micro#replicate: + replacement: "Effect.replicate" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#replicateEffect: + replacement: "Effect.replicateEffect" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#retry: + replacement: "Effect.retry" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#runFork: + replacement: "Effect.runFork" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#runPromise: + replacement: "Effect.runPromise" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#runPromiseExit: + replacement: "Effect.runPromiseExit" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#runSync: + replacement: "Effect.runSync" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#runSyncExit: + replacement: "Effect.runSyncExit" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#sandbox: + replacement: "Effect.sandbox" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#scheduleAddDelay: + replacement: "Schedule.addDelay" + note: "Moved to effect/Schedule; the callback now receives schedule Metadata and returns a Duration (optionally effectful)." +effect/Micro#scheduleExponential: + replacement: "Schedule.exponential" + note: "Moved to effect/Schedule; takes Duration input instead of raw millis." +effect/Micro#scheduleIntersect: + replacement: "Schedule.max" + note: "Intersection (recur while both recur, waiting for the slower) is Schedule.max([self, that]) in v4." +effect/Micro#scheduleRecurs: + replacement: "Schedule.recurs" + note: "Moved to effect/Schedule." +effect/Micro#scheduleSpaced: + replacement: "Schedule.spaced" + note: "Moved to effect/Schedule; takes Duration input instead of raw millis." +effect/Micro#scheduleUnion: + replacement: "Schedule.min" + note: "Union (recur while either recurs, waiting for the faster) is Schedule.min([self, that]) in v4." +effect/Micro#scheduleWithMaxDelay: + replacement: "Schedule.modifyDelay" + note: "No direct equivalent; clamp the delay with Schedule.modifyDelay." + example: "Schedule.modifyDelay(schedule, ({ delay }) => Duration.min(delay, \"10 seconds\"))" +effect/Micro#scheduleWithMaxElapsed: + replacement: "Schedule.upTo" + note: "Renamed: cap total elapsed time with Schedule.upTo({ duration })." + example: "Schedule.upTo(schedule, { duration: \"30 seconds\" })" +effect/Micro#scoped: + replacement: "Effect.scoped" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#scopeMake: + replacement: "Scope.make" + note: "Moved to effect/Scope: Scope.make returns Effect and accepts an optional finalizer strategy." +effect/Micro#scopeUnsafeMake: + replacement: "Scope.makeUnsafe" + note: "Renamed and moved: scopeUnsafeMake becomes Scope.makeUnsafe from effect/Scope." +effect/Micro#service: + replacement: "service" + note: "Micro was removed in v4, and services are Effects; yield or compose the service key directly in the rewritten Effect runtime." +effect/Micro#succeed: + replacement: "Effect.succeed" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#sync: + replacement: "Effect.sync" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#TaggedError: + replacement: "Data.TaggedError" + note: "The yieldable tagged error class constructor is Data.TaggedError from effect/Data in v4." +effect/Micro#tap: + replacement: "Effect.tap" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#tapDefect: + replacement: "Effect.tapDefect" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#tapError: + replacement: "Effect.tapError" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#tapErrorCause: + replacement: "Effect.tapCause" + note: "Renamed: tapErrorCause becomes Effect.tapCause." +effect/Micro#tapErrorCauseIf: + replacement: "Effect.tapCauseIf" + note: "Renamed: tapErrorCauseIf becomes Effect.tapCauseIf (see also Effect.tapCauseFilter for Filter-based matching)." +effect/Micro#timeout: + replacement: "Effect.timeout" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#TimeoutException: + replacement: "Cause.TimeoutError" + note: "Renamed and moved: TimeoutException becomes Cause.TimeoutError from effect/Cause (raised by Effect.timeout)." +effect/Micro#timeoutOption: + replacement: "Effect.timeoutOption" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#try: + replacement: "Effect.try" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#tryPromise: + replacement: "Effect.tryPromise" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#TypeId: + replacement: "Effect.TypeId" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#uninterruptibleMask: + replacement: "Effect.uninterruptibleMask" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#updateContext: + replacement: "Effect.updateContext" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#updateService: + replacement: "Effect.updateService" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#void: + replacement: "Effect.void" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#when: + replacement: "Effect.when" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#whileLoop: + replacement: "Effect.whileLoop" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." +effect/Micro#withConcurrency: + replacement: "none" + note: "Removed in v4 along with \"inherit\" concurrency; pass a { concurrency } option directly to each concurrent operation." + example: "Effect.forEach(items, handle, { concurrency: 10 })" +effect/Micro#withMicroFiber: + replacement: "Effect.withFiber" + note: "Renamed: withMicroFiber becomes Effect.withFiber, giving access to the current fiber." +effect/Micro#withTrace: + replacement: "Effect.withSpan" + note: "Removed; v4 captures failure stack traces automatically and cause annotations replace the traces array. For named tracing regions use Effect.withSpan." +effect/Micro#YieldableError: + replacement: "Cause.YieldableError" + note: "Moved: YieldableError lives in effect/Cause in v4." +effect/Micro#yieldFlush: + replacement: "none" + note: "Removed; access the current scheduler via the References.Scheduler service and call its flush() method directly if deterministic draining is needed." +effect/Micro#yieldNow: + replacement: "Effect.yieldNow" + note: "Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect." diff --git a/.context/effect/migration/annotations/effect__ModuleVersion.yaml b/.context/effect/migration/annotations/effect__ModuleVersion.yaml new file mode 100644 index 000000000..07ea5d5e6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ModuleVersion.yaml @@ -0,0 +1,3 @@ +effect/ModuleVersion: + replacement: none + note: The mutable module-version facility was removed; the v4 build version is private and effect/package.json is metadata, not an equivalent runtime API. No mutable version setter remains; the v3 runtime-isolation mechanism has no public v4 equivalent. diff --git a/.context/effect/migration/annotations/effect__MutableHashMap.yaml b/.context/effect/migration/annotations/effect__MutableHashMap.yaml new file mode 100644 index 000000000..201890730 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MutableHashMap.yaml @@ -0,0 +1,6 @@ +"effect/MutableHashMap#MutableHashMap": + replacement: "MutableHashMap.MutableHashMap" + note: "The MutableHashMap model remains; use its public operations rather than depending on internal representation fields." +"effect/MutableHashMap#TypeId": + replacement: "none" + note: "The public MutableHashMap.TypeId was removed; the v4 marker is private." diff --git a/.context/effect/migration/annotations/effect__MutableHashSet.yaml b/.context/effect/migration/annotations/effect__MutableHashSet.yaml new file mode 100644 index 000000000..fcc4cef0e --- /dev/null +++ b/.context/effect/migration/annotations/effect__MutableHashSet.yaml @@ -0,0 +1,6 @@ +"effect/MutableHashSet#MutableHashSet": + replacement: "MutableHashSet.MutableHashSet" + note: "The MutableHashSet model remains; use its public operations rather than depending on internal representation fields." +"effect/MutableHashSet#TypeId": + replacement: "none" + note: "The public MutableHashSet.TypeId was removed; the v4 marker is private." diff --git a/.context/effect/migration/annotations/effect__MutableList.yaml b/.context/effect/migration/annotations/effect__MutableList.yaml new file mode 100644 index 000000000..c9cd12126 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MutableList.yaml @@ -0,0 +1,36 @@ +"effect/MutableList#empty": + replacement: "MutableList.make" + note: "Constructor rename; MutableList.Empty is the take sentinel, not a constructor." +"effect/MutableList#forEach": + replacement: "MutableList.toArray + Array.forEach" + note: "No direct traversal helper remains; iterate a snapshot produced by MutableList.toArray." +"effect/MutableList#fromIterable": + replacement: "MutableList.make + MutableList.appendAll" + note: "Create an empty list with MutableList.make, then append the iterable with MutableList.appendAll." +"effect/MutableList#head": + replacement: "MutableList.toArrayN" + note: "Use MutableList.toArrayN(self, 1)[0]; the redesigned FIFO exposes buckets rather than the old Option-returning accessor." +"effect/MutableList#isEmpty": + replacement: "none" + note: "Read self.length === 0; no named isEmpty helper remains." +"effect/MutableList#length": + replacement: "none" + note: "Read the public self.length field; no named length helper remains." +"effect/MutableList#MutableList": + replacement: "MutableList.MutableList" + note: "The model remains but was redesigned from an iterable doubly linked list into a bucketed FIFO structure." +"effect/MutableList#pop": + replacement: "none" + note: "The bucketed FIFO has no remove-last operation; migrate code to front draining or use a different mutable collection." +"effect/MutableList#reset": + replacement: "MutableList.clear" + note: "Direct behavioral replacement; the return type is now void." +"effect/MutableList#shift": + replacement: "MutableList.take" + note: "Front removal remains synchronous, but emptiness is reported with MutableList.Empty instead of undefined." +"effect/MutableList#tail": + replacement: "MutableList.toArray" + note: "Use MutableList.toArray(self).at(-1); the public self.tail field is an internal bucket, not the old last-element accessor." +"effect/MutableList#TypeId": + replacement: "none" + note: "V4 MutableList has no public runtime marker." diff --git a/.context/effect/migration/annotations/effect__MutableQueue.yaml b/.context/effect/migration/annotations/effect__MutableQueue.yaml new file mode 100644 index 000000000..6ba01f209 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MutableQueue.yaml @@ -0,0 +1,42 @@ +"effect/MutableQueue#bounded": + replacement: "Queue.dropping" + note: "The replacement constructor is effectful; dropping preserves the old immediate rejection when a bounded queue is full." +"effect/MutableQueue#capacity": + replacement: "none" + note: "Read queue.capacity on the replacement Queue; unbounded queues expose Infinity." +"effect/MutableQueue#EmptyMutableQueue": + replacement: "none" + note: "Queue.poll reports emptiness with Option.none, so no default sentinel is required." +"effect/MutableQueue#isEmpty": + replacement: "Queue.sizeUnsafe" + note: "Use Queue.sizeUnsafe(queue) === 0, or map the effectful Queue.size result." +"effect/MutableQueue#isFull": + replacement: "Queue.isFullUnsafe" + note: "Use Queue.isFullUnsafe for synchronous inspection or Queue.isFull for an Effect result." +"effect/MutableQueue#length": + replacement: "Queue.sizeUnsafe" + note: "Use Queue.sizeUnsafe for synchronous inspection or Queue.size for an Effect result." +"effect/MutableQueue#MutableQueue": + replacement: "Queue.Queue" + note: "The replacement Queue is effectful, lifecycle-aware, and not Iterable." +"effect/MutableQueue#MutableQueue.Empty": + replacement: "none" + note: "Use the Option returned by Queue.poll; the old empty sentinel was removed." +"effect/MutableQueue#offer": + replacement: "Queue.offerUnsafe" + note: "Use with Queue.dropping to preserve the old synchronous boolean rejection at capacity; Queue.offer is the effectful form." +"effect/MutableQueue#offerAll": + replacement: "Queue.offerAllUnsafe" + note: "The synchronous replacement returns the rejected remainder as an Array; Queue.offerAll is the effectful form." +"effect/MutableQueue#poll": + replacement: "Queue.poll" + note: "Polling is now effectful and returns Option rather than accepting a default; Queue.takeUnsafe is the low-level synchronous alternative." +"effect/MutableQueue#pollUpTo": + replacement: "Queue.takeUnsafe" + note: "No direct non-blocking take-up-to helper remains; repeatedly call Queue.takeUnsafe and collect successful exits without waiting." +"effect/MutableQueue#TypeId": + replacement: "none" + note: "The MutableQueue module and its public marker were removed." +"effect/MutableQueue#unbounded": + replacement: "Queue.unbounded" + note: "The unbounded replacement constructor is effectful." diff --git a/.context/effect/migration/annotations/effect__MutableRef.yaml b/.context/effect/migration/annotations/effect__MutableRef.yaml new file mode 100644 index 000000000..e41e4d192 --- /dev/null +++ b/.context/effect/migration/annotations/effect__MutableRef.yaml @@ -0,0 +1,6 @@ +"effect/MutableRef#MutableRef": + replacement: "MutableRef.MutableRef" + note: "The MutableRef model remains; use its public operations rather than depending on internal representation fields." +"effect/MutableRef#TypeId": + replacement: "none" + note: "The public MutableRef.TypeId was removed; the v4 marker is private." diff --git a/.context/effect/migration/annotations/effect__Number.yaml b/.context/effect/migration/annotations/effect__Number.yaml new file mode 100644 index 000000000..b1e77db36 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Number.yaml @@ -0,0 +1,18 @@ +"effect/Number#greaterThan": + replacement: "Number.isGreaterThan" + note: "Renamed with the v4 is-prefix." +"effect/Number#greaterThanOrEqualTo": + replacement: "Number.isGreaterThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/Number#lessThan": + replacement: "Number.isLessThan" + note: "Renamed with the v4 is-prefix." +"effect/Number#lessThanOrEqualTo": + replacement: "Number.isLessThanOrEqualTo" + note: "Renamed with the v4 is-prefix." +"effect/Number#negate": + replacement: "Number.multiply(-1)" + note: "Use Number.multiply(n, -1), or Number.multiply(-1) as the equivalent unary function." +"effect/Number#unsafeDivide": + replacement: "Number.divideUnsafe" + note: "Renamed; v4 throws for zero whereas v3 raw division returned Infinity or NaN." diff --git a/.context/effect/migration/annotations/effect__Option.yaml b/.context/effect/migration/annotations/effect__Option.yaml new file mode 100644 index 000000000..75f930ac9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Option.yaml @@ -0,0 +1,45 @@ +"effect/Option#ap": + replacement: "Option.zipWith" + note: "Use Option.zipWith(self, that, (f, a) => f(a)); v4 has no Option.ap." +"effect/Option#flatMapNullable": + replacement: "Option.flatMapNullishOr" + note: "Renamed with v4 nullish-or terminology." +"effect/Option#fromNullable": + replacement: "Option.fromNullishOr" + note: "Renamed with v4 nullish-or terminology." +"effect/Option#getEquivalence": + replacement: "Option.makeEquivalence" + note: "Renamed from getEquivalence to makeEquivalence." +"effect/Option#getLeft": + replacement: "Option.getFailure" + note: "Either input became Result input, and Left became Failure." +"effect/Option#getOrder": + replacement: "Option.makeOrder" + note: "Renamed from getOrder to makeOrder." +"effect/Option#getRight": + replacement: "Option.getSuccess" + note: "Either input became Result input, and Right became Success." +"effect/Option#liftNullable": + replacement: "Option.liftNullishOr" + note: "Renamed with v4 nullish-or terminology." +"effect/Option#None": + replacement: "Option.None" + note: "The variant remains, but Option is no longer an Effect or STM subtype." +"effect/Option#Option": + replacement: "Option.Option" + note: "The union type remains, but Option is no longer an Effect or STM subtype." +"effect/Option#OptionUnify": + replacement: "Option.OptionUnify" + note: "The unification hook remains under the same name." +"effect/Option#OptionUnifyIgnore": + replacement: "Option.OptionUnifyIgnore" + note: "The marker remains, without the v3 Effect, Tag, and Either augmentation fields." +"effect/Option#orElseEither": + replacement: "Option.orElseResult" + note: "Either was replaced by Result; source tracking now uses Failure and Success." +"effect/Option#Some": + replacement: "Option.Some" + note: "The variant remains, but Option is no longer an Effect or STM subtype." +"effect/Option#TypeId": + replacement: "none" + note: "The v4 Option brand is private and no public TypeId is exported." diff --git a/.context/effect/migration/annotations/effect__Order.yaml b/.context/effect/migration/annotations/effect__Order.yaml new file mode 100644 index 000000000..365449cb7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Order.yaml @@ -0,0 +1,63 @@ +"effect/Order#all": + replacement: "Order.Tuple([...collection])" + note: "Materialize the comparator iterable for Tuple. V4 evaluates the configured tuple instead of stopping at the shorter input; use Order.make for intentional v3 prefix semantics." +"effect/Order#array": + replacement: "Order.Array" + note: "Capitalized constructor name; lexicographic array ordering and the length tie-break are unchanged." +"effect/Order#between": + replacement: "Order.isBetween" + note: "Renamed with the v4 is-prefix; inclusive bounds and call forms are unchanged." +"effect/Order#bigint": + replacement: "Order.BigInt" + note: "Capitalized instance name; bigint ordering is unchanged." +"effect/Order#boolean": + replacement: "Order.Boolean" + note: "Capitalized instance name; false remains ordered before true." +"effect/Order#combineAll": + replacement: "Order.combineAll" + note: "Retained with the same left-to-right tie-breaking and empty-iterable result." +"effect/Order#combineMany": + replacement: "Order.combine(self, Order.combineAll(collection))" + note: "Compose combine with combineAll; the dedicated dual combineMany helper was removed." +"effect/Order#empty": + replacement: "Order.alwaysEqual" + note: "Renamed constructor; call as Order.alwaysEqual() to produce an order that always returns zero." +"effect/Order#greaterThan": + replacement: "Order.isGreaterThan" + note: "Renamed with the v4 is-prefix; curried and uncurried comparisons are retained." +"effect/Order#greaterThanOrEqualTo": + replacement: "Order.isGreaterThanOrEqualTo" + note: "Renamed with the v4 is-prefix; curried and uncurried comparisons are retained." +"effect/Order#lessThan": + replacement: "Order.isLessThan" + note: "Renamed with the v4 is-prefix; curried and uncurried comparisons are retained." +"effect/Order#lessThanOrEqualTo": + replacement: "Order.isLessThanOrEqualTo" + note: "Renamed with the v4 is-prefix; curried and uncurried comparisons are retained." +"effect/Order#make": + replacement: "Order.make" + note: "Retained with the same comparator contract and reference-equality fast path." +"effect/Order#number": + replacement: "Order.Number" + note: "Capitalized instance name. V4 orders NaN below non-NaN values and all NaNs equally; use a custom Order.make to preserve v3 edge behavior." +"effect/Order#Order": + replacement: "Order.Order" + note: "The callable type is retained; its return type remains the -1 | 0 | 1 Ordering union." +"effect/Order#product": + replacement: "Order.Tuple([self, that])" + note: "Replace the dual two-order helper with the single-array Tuple constructor." +"effect/Order#productMany": + replacement: "Order.Tuple([self, ...collection])" + note: "Materialize the order iterable in one Tuple call; v4 evaluates every configured comparator for short inputs." +"effect/Order#reverse": + replacement: "Order.flip" + note: "Direct rename; the replacement reverses comparison by swapping the operands." +"effect/Order#string": + replacement: "Order.String" + note: "Capitalized instance name; case-sensitive JavaScript lexicographic ordering is unchanged." +"effect/Order#struct": + replacement: "Order.Struct" + note: "Capitalized constructor name; field-order tie-breaking is unchanged." +"effect/Order#tuple": + replacement: "Order.Tuple([orderA, orderB, ...])" + note: "Capitalized constructor now takes one comparator array instead of rest arguments and evaluates every configured position." diff --git a/.context/effect/migration/annotations/effect__Ordering.yaml b/.context/effect/migration/annotations/effect__Ordering.yaml new file mode 100644 index 000000000..68af19108 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Ordering.yaml @@ -0,0 +1,6 @@ +"effect/Ordering#combineAll": + replacement: "Ordering.Reducer.combineAll" + note: "The combination operation moved to the exported Reducer; first-nonzero and empty-input behavior are unchanged." +"effect/Ordering#combineMany": + replacement: "Ordering.Reducer.combineAll(Iterable.prepend(collection, self))" + note: "Prepend the initial ordering before reducing to preserve v3 short-circuiting without consuming collection when self is nonzero." diff --git a/.context/effect/migration/annotations/effect__ParseResult.yaml b/.context/effect/migration/annotations/effect__ParseResult.yaml new file mode 100644 index 000000000..3feefd473 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ParseResult.yaml @@ -0,0 +1,125 @@ +"effect/ParseResult#Forbidden": + replacement: "SchemaIssue.Forbidden" + note: "Forbidden failures use the v4 SchemaIssue class; its constructor takes issue annotations plus optional input and parse options, retaining input only when reportInput is true." +"effect/ParseResult#ArrayFormatter": + replacement: "SchemaIssue.makeFormatterStandardSchemaV1" + note: "Format error.issue with the Standard Schema formatter." + example: "SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues" +"effect/ParseResult#ArrayFormatterIssue": + replacement: "StandardSchemaV1.FailureResult[\"issues\"][number]" + note: "Use the Standard Schema issue shape returned by makeFormatterStandardSchemaV1." +"effect/ParseResult#DeclarationDecodeUnknown": + replacement: "SchemaGetter.Getter" + note: "Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations." +"effect/ParseResult#decodeEither": + replacement: "Schema.decodeExit" + note: "Either parsing was replaced by Exit parsing." +"effect/ParseResult#decodePromise": + replacement: "Schema.decodePromise" + note: "Parsing helpers moved onto Schema and now fail with SchemaError." +"effect/ParseResult#decodeSync": + replacement: "Schema.decodeSync" + note: "Parsing helpers moved onto Schema and now throw SchemaError." +"effect/ParseResult#DecodeUnknown": + replacement: "Schema.decodeUnknownEffect" + note: "Use the function type returned by Schema.decodeUnknownEffect." +"effect/ParseResult#decodeUnknownEither": + replacement: "Schema.decodeUnknownExit" + note: "Either parsing was replaced by Exit parsing." +"effect/ParseResult#decodeUnknownPromise": + replacement: "Schema.decodeUnknownPromise" + note: "Parsing helpers moved onto Schema and now reject with SchemaError." +"effect/ParseResult#decodeUnknownSync": + replacement: "Schema.decodeUnknownSync" + note: "Parsing helpers moved onto Schema and now throw SchemaError." +"effect/ParseResult#eitherOrUndefined": + replacement: "none" + note: "This ParseResult internal optimization was removed; use Effect, Exit, Option, or Result combinators directly." +"effect/ParseResult#encodeEither": + replacement: "Schema.encodeExit" + note: "Either encoding was replaced by Exit encoding." +"effect/ParseResult#encodeSync": + replacement: "Schema.encodeSync" + note: "Encoding helpers moved onto Schema and now throw SchemaError." +"effect/ParseResult#encodeUnknownEither": + replacement: "Schema.encodeUnknownExit" + note: "Either encoding was replaced by Exit encoding." +"effect/ParseResult#encodeUnknownSync": + replacement: "Schema.encodeUnknownSync" + note: "Encoding helpers moved onto Schema and now throw SchemaError." +"effect/ParseResult#fail": + replacement: "Effect.fail" + note: "Schema transformations now use Effect and fail with SchemaIssue.Issue." +"effect/ParseResult#flatMap": + replacement: "Effect.flatMap" + note: "Schema transformations now use Effect combinators." +"effect/ParseResult#isComposite": + replacement: "SchemaIssue.Composite" + note: "Narrow with instanceof SchemaIssue.Composite or inspect the issue _tag." +"effect/ParseResult#isParseError": + replacement: "Schema.isSchemaError" + note: "ParseError was replaced by SchemaError." +"effect/ParseResult#map": + replacement: "Effect.map" + note: "Schema transformations now use Effect combinators." +"effect/ParseResult#Missing": + replacement: "SchemaIssue.MissingKey" + note: "Missing-key failures use the v4 SchemaIssue class." +"effect/ParseResult#orElse": + replacement: "Effect.orElse" + note: "Schema transformations now use Effect combinators." +"effect/ParseResult#parseError": + replacement: "Schema.SchemaError" + note: "Construct a SchemaError from a SchemaIssue.Issue." + example: "new Schema.SchemaError(issue)" +"effect/ParseResult#ParseErrorTypeId": + replacement: "none" + note: "The public symbol was removed; use Schema.isSchemaError for runtime narrowing." +"effect/ParseResult#ParseIssue": + replacement: "SchemaIssue.Issue" + note: "The structured parse issue union moved to SchemaIssue." +"effect/ParseResult#ParseResultFormatter": + replacement: "SchemaIssue.Formatter" + note: "Issue formatter types moved to SchemaIssue." +"effect/ParseResult#Refinement": + replacement: "SchemaIssue.Filter" + note: "Refinement failures are represented as filter issues in v4." +"effect/ParseResult#SingleOrNonEmpty": + replacement: "ReadonlyArray" + note: "This ParseResult helper type was removed; use an explicit value-or-non-empty-array type when still needed." +"effect/ParseResult#succeed": + replacement: "Effect.succeed" + note: "Schema transformations now use Effect." +"effect/ParseResult#TreeFormatter": + replacement: "SchemaIssue.defaultFormatter" + note: "Use the default SchemaIssue string formatter." + example: "SchemaIssue.defaultFormatter(issue)" +"effect/ParseResult#try": + replacement: "Effect.try" + note: "Schema transformations now use Effect and map thrown errors to SchemaIssue values." +"effect/ParseResult#Type": + replacement: "SchemaIssue.InvalidType" + note: "Type mismatches use the v4 SchemaIssue class." +"effect/ParseResult#Unexpected": + replacement: "SchemaIssue.UnexpectedKey" + note: "Unexpected object keys use the v4 SchemaIssue class." +"effect/ParseResult#validate": + replacement: "Schema.decodeEffect + Schema.toType" + note: "Validation-only parsers were removed; decode the type-side schema instead." + example: "Schema.decodeEffect(Schema.toType(schema))" +"effect/ParseResult#validateEither": + replacement: "Schema.decodeExit + Schema.toType" + note: "Validation-only parsers were removed; decode the type-side schema instead." + example: "Schema.decodeExit(Schema.toType(schema))" +"effect/ParseResult#validateOption": + replacement: "Schema.decodeOption + Schema.toType" + note: "Validation-only parsers were removed; decode the type-side schema instead." + example: "Schema.decodeOption(Schema.toType(schema))" +"effect/ParseResult#validatePromise": + replacement: "Schema.decodePromise + Schema.toType" + note: "Validation-only parsers were removed; decode the type-side schema instead." + example: "Schema.decodePromise(Schema.toType(schema))" +"effect/ParseResult#validateSync": + replacement: "Schema.decodeSync + Schema.toType" + note: "Validation-only parsers were removed; decode the type-side schema instead." + example: "Schema.decodeSync(Schema.toType(schema))" diff --git a/.context/effect/migration/annotations/effect__PartitionedSemaphore.yaml b/.context/effect/migration/annotations/effect__PartitionedSemaphore.yaml new file mode 100644 index 000000000..f7ae84b95 --- /dev/null +++ b/.context/effect/migration/annotations/effect__PartitionedSemaphore.yaml @@ -0,0 +1,6 @@ +effect/PartitionedSemaphore#PartitionedSemaphore: + replacement: "PartitionedSemaphore.PartitionedSemaphore" + note: "The model remains and now also exposes capacity, available, take, release, withPermit, and conditional permit operations." +effect/PartitionedSemaphore#TypeId: + replacement: "PartitionedSemaphore.PartitionedTypeId" + note: "The public type id was renamed to distinguish it from the regular Semaphore type id." diff --git a/.context/effect/migration/annotations/effect__Pipeable.yaml b/.context/effect/migration/annotations/effect__Pipeable.yaml new file mode 100644 index 000000000..b4a746b9e --- /dev/null +++ b/.context/effect/migration/annotations/effect__Pipeable.yaml @@ -0,0 +1,3 @@ +"effect/Pipeable#PipeableConstructor": + replacement: "Pipeable.PipeableConstructor" + note: "Still exported; its rest arguments are ReadonlyArray in v4, so make explicit constructor typings readonly-compatible." diff --git a/.context/effect/migration/annotations/effect__Pool.yaml b/.context/effect/migration/annotations/effect__Pool.yaml new file mode 100644 index 000000000..6cd929382 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Pool.yaml @@ -0,0 +1,15 @@ +effect/Pool#Pool: + replacement: "Pool.Pool" + note: "The model remains but is now Pipeable rather than an Effect subtype; borrow resources explicitly with Pool.get." +effect/Pool#Pool.Variance: + replacement: "none" + note: "The public Pool variance marker was removed; use Pool.Pool directly." +effect/Pool#PoolTypeId: + replacement: "none" + note: "The Pool type id is internal in v4; use Pool.isPool for runtime refinement." +effect/Pool#PoolUnify: + replacement: "none" + note: "Pool is no longer an Effect subtype, so its Effect unification helper was removed; call Pool.get explicitly." +effect/Pool#PoolUnifyIgnore: + replacement: "none" + note: "Pool is no longer an Effect subtype, so its Effect unification ignore marker was removed." diff --git a/.context/effect/migration/annotations/effect__Predicate.yaml b/.context/effect/migration/annotations/effect__Predicate.yaml new file mode 100644 index 000000000..5649916c9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Predicate.yaml @@ -0,0 +1,111 @@ +"effect/Predicate#all": + replacement: "Predicate.Tuple(Array.from(collection))" + note: "Use positional Tuple, materializing an Iterable when needed. V4 checks every configured position instead of accepting missing input values." +"effect/Predicate#every": + replacement: "Predicate.every" + note: "Retained with the same AND semantics, short-circuiting, and true result for an empty collection." +"effect/Predicate#isBigInt": + replacement: "Predicate.isBigInt" + note: "Retained with the same bigint refinement." +"effect/Predicate#isBoolean": + replacement: "Predicate.isBoolean" + note: "Retained with the same boolean refinement." +"effect/Predicate#isDate": + replacement: "Predicate.isDate" + note: "Retained with the same instanceof Date check." +"effect/Predicate#isError": + replacement: "Predicate.isError" + note: "Retained with the same instanceof Error check." +"effect/Predicate#isFunction": + replacement: "Predicate.isFunction" + note: "Retained with the same function refinement." +"effect/Predicate#isIterable": + replacement: "Predicate.isIterable" + note: "Retained; strings and values exposing Symbol.iterator are still accepted." +"effect/Predicate#isMap": + replacement: "Predicate.isMap" + note: "Retained with the same instanceof Map check." +"effect/Predicate#isNever": + replacement: "Predicate.isNever" + note: "Retained as the always-false refinement." +"effect/Predicate#isNotNull": + replacement: "Predicate.isNotNull" + note: "Retained; undefined still passes while null is excluded." +"effect/Predicate#isNotNullable": + replacement: "Predicate.isNotNullish" + note: "Renamed to use nullish terminology; it still excludes null and undefined." +"effect/Predicate#isNotUndefined": + replacement: "Predicate.isNotUndefined" + note: "Retained; null still passes while undefined is excluded." +"effect/Predicate#isNull": + replacement: "Predicate.isNull" + note: "Retained with the same strict null refinement." +"effect/Predicate#isNullable": + replacement: "Predicate.isNullish" + note: "Renamed to use nullish terminology. The guard now narrows with A & (null | undefined), including unknown inputs correctly." +"effect/Predicate#isNumber": + replacement: "Predicate.isNumber" + note: "Retained; NaN and infinite numbers still pass." +"effect/Predicate#isObject": + replacement: "Predicate.isObjectKeyword" + note: "Use isObjectKeyword to preserve v3 behavior accepting arrays and functions. V4 isObject has the former record-like semantics instead." +"effect/Predicate#isPromise": + replacement: "Predicate.isPromise" + note: "Retained as the structural check for callable then and catch properties." +"effect/Predicate#isPromiseLike": + replacement: "Predicate.isPromiseLike" + note: "Retained as the structural check for a callable then property." +"effect/Predicate#isReadonlyRecord": + replacement: "Predicate.isReadonlyObject" + note: "Renamed; runtime behavior is unchanged and the index-key type now explicitly includes numbers." +"effect/Predicate#isRecord": + replacement: "Predicate.isObject" + note: "Renamed; it still accepts non-null, non-array objects and now narrows with PropertyKey indexes." +"effect/Predicate#isRegExp": + replacement: "Predicate.isRegExp" + note: "Retained with the same instanceof RegExp check." +"effect/Predicate#isSet": + replacement: "Predicate.isSet" + note: "Retained with the same instanceof Set check." +"effect/Predicate#isString": + replacement: "Predicate.isString" + note: "Retained with the same primitive string refinement." +"effect/Predicate#isSymbol": + replacement: "Predicate.isSymbol" + note: "Retained with the same symbol refinement." +"effect/Predicate#isTruthy": + replacement: "Predicate.isTruthy" + note: "Retained as a plain boolean predicate using JavaScript truthiness." +"effect/Predicate#isUint8Array": + replacement: "Predicate.isUint8Array" + note: "Retained with the same instanceof Uint8Array check." +"effect/Predicate#isUndefined": + replacement: "Predicate.isUndefined" + note: "Retained with the same strict undefined refinement." +"effect/Predicate#isUnknown": + replacement: "Predicate.isUnknown" + note: "Retained as the always-true refinement." +"effect/Predicate#not": + replacement: "Predicate.not" + note: "Retained with the same boolean negation; refinements still become plain predicates." +"effect/Predicate#Predicate": + replacement: "Predicate.Predicate" + note: "The callable interface is retained. Predicate.Any now uses any rather than never, which can affect generic inference." +"effect/Predicate#product": + replacement: "Predicate.Tuple([self, that])" + note: "Replace the two-position product helper with the Tuple constructor." +"effect/Predicate#productMany": + replacement: "Predicate.Tuple([self, ...Array.from(collection)])" + note: "Materialize the predicate iterable in one Tuple call; v4 checks missing tail positions as undefined." +"effect/Predicate#Refinement": + replacement: "Predicate.Refinement" + note: "The refinement interface and its In, Out, and Any namespace types are retained." +"effect/Predicate#some": + replacement: "Predicate.some" + note: "Retained with the same OR semantics, short-circuiting, and false result for an empty collection." +"effect/Predicate#struct": + replacement: "Predicate.Struct" + note: "Capitalized constructor name; field checks and refinement-aware typing are retained." +"effect/Predicate#tuple": + replacement: "Predicate.Tuple([p1, p2, ...])" + note: "Capitalized constructor now takes one predicate array instead of rest arguments and checks missing positions as undefined." diff --git a/.context/effect/migration/annotations/effect__Pretty.yaml b/.context/effect/migration/annotations/effect__Pretty.yaml new file mode 100644 index 000000000..1a5c14d41 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Pretty.yaml @@ -0,0 +1,13 @@ +"effect/Pretty#make": + replacement: "Schema.toFormatter" + note: "Formatter derivation moved onto Schema." + example: "Schema.toFormatter(schema)" +"effect/Pretty#match": + replacement: "Schema.toFormatter" + note: "The compiler match table was removed; customize traversal with the toFormatter onBefore option." +"effect/Pretty#Pretty": + replacement: "Formatter.Formatter" + note: "The formatter function type is now exported by Formatter." +"effect/Pretty#PrettyAnnotation": + replacement: "Schema.Annotations.ToFormatter.Declaration" + note: "Custom declaration formatter annotations now use the toFormatter key in Schema.Annotations." diff --git a/.context/effect/migration/annotations/effect__PubSub.yaml b/.context/effect/migration/annotations/effect__PubSub.yaml new file mode 100644 index 000000000..cee33218f --- /dev/null +++ b/.context/effect/migration/annotations/effect__PubSub.yaml @@ -0,0 +1,3 @@ +effect/PubSub#PubSub: + replacement: "PubSub.PubSub" + note: "The model remains but no longer extends Queue.Enqueue; replace Queue operations with explicit PubSub.publish, PubSub.publishAll, and PubSub.subscribe calls." diff --git a/.context/effect/migration/annotations/effect__Queue.yaml b/.context/effect/migration/annotations/effect__Queue.yaml new file mode 100644 index 000000000..d0eab5568 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Queue.yaml @@ -0,0 +1,81 @@ +effect/Queue#awaitShutdown: + replacement: "Queue.await" + note: "Queue completion now includes normal end and failure; Queue.await waits for Done and propagates non-Done terminal causes." +effect/Queue#BackingQueue: + replacement: "none" + note: "Custom backing queues were removed by the v4 Queue rewrite; use Queue.make and its built-in capacity and strategy options." +effect/Queue#BackingQueueTypeId: + replacement: "none" + note: "BackingQueue and its public type id were removed." +effect/Queue#backPressureStrategy: + replacement: "Queue.make({ strategy: \"suspend\" })" + note: "Strategies are now constructor options rather than public Strategy values; suspend is the default." +effect/Queue#BaseQueue: + replacement: "Queue.Enqueue | Queue.Dequeue" + note: "The shared BaseQueue interface was removed; accept the required enqueue or dequeue capability and call Queue operations explicitly." +effect/Queue#capacity: + replacement: "queue.capacity" + note: "Capacity is now a property on Queue.Enqueue and Queue.Dequeue rather than a module function." +effect/Queue#Dequeue: + replacement: "Queue.Dequeue" + note: "The model remains and gains an error parameter, but is no longer an Effect subtype; use Queue.take explicitly." +effect/Queue#DequeueTypeId: + replacement: "Queue.isDequeue" + note: "The dequeue type id is internal in v4; use Queue.isDequeue for runtime refinement." +effect/Queue#DequeueUnify: + replacement: "none" + note: "Queue.Dequeue is no longer an Effect subtype, so its Effect unification helper was removed." +effect/Queue#DequeueUnifyIgnore: + replacement: "none" + note: "Queue.Dequeue is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/Queue#droppingStrategy: + replacement: "Queue.make({ strategy: \"dropping\" })" + note: "Strategies are now constructor options rather than public Strategy values; Queue.dropping is the bounded convenience constructor." +effect/Queue#Enqueue: + replacement: "Queue.Enqueue" + note: "The write-side model remains, gains an error parameter, and is operated through Queue.offer and related functions." +effect/Queue#EnqueueTypeId: + replacement: "Queue.isEnqueue" + note: "The enqueue type id is internal in v4; use Queue.isEnqueue for runtime refinement." +effect/Queue#isEmpty: + replacement: "Effect.map(Queue.size(self), (size) => size === 0)" + note: "The dedicated helper was removed; derive emptiness from Queue.size." +effect/Queue#Queue: + replacement: "Queue.Queue" + note: "The model remains, gains an error parameter and completion signaling, and is no longer an Effect subtype; use Queue.take explicitly." +effect/Queue#Queue.BackingQueueVariance: + replacement: "none" + note: "BackingQueue and its variance marker were removed by the v4 Queue rewrite." +effect/Queue#Queue.DequeueVariance: + replacement: "Queue.Dequeue.Variance" + note: "The read-side variance marker moved under the Queue.Dequeue namespace and now includes the error type." +effect/Queue#Queue.EnqueueVariance: + replacement: "Queue.Enqueue.Variance" + note: "The write-side variance marker moved under the Queue.Enqueue namespace and now includes the error type." +effect/Queue#Queue.StrategyVariance: + replacement: "none" + note: "Public Strategy values and their variance marker were removed; select a string strategy when constructing the Queue." +effect/Queue#QueueStrategyTypeId: + replacement: "none" + note: "Public Strategy values and their type id were removed." +effect/Queue#QueueUnify: + replacement: "none" + note: "Queue is no longer an Effect subtype, so its Effect unification helper was removed; call Queue.take explicitly." +effect/Queue#QueueUnifyIgnore: + replacement: "none" + note: "Queue is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/Queue#slidingStrategy: + replacement: "Queue.make({ strategy: \"sliding\" })" + note: "Strategies are now constructor options rather than public Strategy values; Queue.sliding is the bounded convenience constructor." +effect/Queue#Strategy: + replacement: "\"suspend\" | \"dropping\" | \"sliding\"" + note: "The pluggable Strategy interface was removed; choose one of the built-in strategy strings in Queue.make." +effect/Queue#takeUpTo: + replacement: "Queue.poll" + note: "No direct bounded batch helper remains; repeatedly call non-blocking Queue.poll up to the limit, or use Queue.clear when taking every buffered value is acceptable." +effect/Queue#unsafeOffer: + replacement: "Queue.offerUnsafe" + note: "The unsafe suffix moved to the end." +effect/Queue#isShutdown: + replacement: "queue.state._tag === \"Done\"" + note: "The dedicated helper was removed; inspect the public queue lifecycle state. Done includes normal completion and failure, not only explicit shutdown." diff --git a/.context/effect/migration/annotations/effect__Random.yaml b/.context/effect/migration/annotations/effect__Random.yaml new file mode 100644 index 000000000..74bee66bf --- /dev/null +++ b/.context/effect/migration/annotations/effect__Random.yaml @@ -0,0 +1,18 @@ +effect/Random#fixed: + replacement: "Effect.provideService(Random.Random, customRandom)" + note: "No exact built-in equivalent remains. For deterministic tests, provide a cycling service implementing nextIntUnsafe and nextDoubleUnsafe; map non-number sequences explicitly." +effect/Random#make: + replacement: "Random.withSeed" + note: "Replace service construction and withRandom with Random.withSeed(seed)(program). V4 accepts string or number, returns an Effect transformation, and uses a different PRNG, so sequences are not v3-compatible." +effect/Random#nextRange: + replacement: "Random.nextBetween" + note: "Direct rename; both produce a floating-point value in the half-open range [min, max)." +effect/Random#Random: + replacement: "Random.Random" + note: "The context key is now a Context.Reference whose low-level service only has nextIntUnsafe and nextDoubleUnsafe. Prefer module operations; custom providers implement those two primitives." +effect/Random#RandomTypeId: + replacement: "none" + note: "The service is structural and no longer carries a public RandomTypeId brand." +effect/Random#randomWith: + replacement: "Random.Random.use" + note: "Use Random.Random.use for raw service access. Prefer replacing callbacks that selected an old method with the corresponding module-level Random operation." diff --git a/.context/effect/migration/annotations/effect__RateLimiter.yaml b/.context/effect/migration/annotations/effect__RateLimiter.yaml new file mode 100644 index 000000000..77dd05c4f --- /dev/null +++ b/.context/effect/migration/annotations/effect__RateLimiter.yaml @@ -0,0 +1,3 @@ +effect/RateLimiter: + replacement: none + note: The old limit, interval, and algorithm options belonged to the removed in-process limiter; choose and configure an application limiter explicitly. The scoped in-process callable limiter was not ported to v4; effect/unstable/persistence/RateLimiter is a keyed persistence service with different semantics, not a drop-in replacement. The FiberRef-based per-effect cost annotation was removed with the core RateLimiter; pass token cost explicitly to the replacement limiter. diff --git a/.context/effect/migration/annotations/effect__RcMap.yaml b/.context/effect/migration/annotations/effect__RcMap.yaml new file mode 100644 index 000000000..8c56ece36 --- /dev/null +++ b/.context/effect/migration/annotations/effect__RcMap.yaml @@ -0,0 +1,9 @@ +effect/RcMap#RcMap: + replacement: "RcMap.RcMap" + note: "The model remains as a Pipeable reference-counted resource map; use RcMap.get explicitly inside a Scope." +effect/RcMap#RcMap.Variance: + replacement: "none" + note: "The public variance marker was removed; use RcMap.RcMap directly." +effect/RcMap#TypeId: + replacement: "none" + note: "The RcMap type id is internal in v4; do not inspect or construct the brand directly." diff --git a/.context/effect/migration/annotations/effect__RcRef.yaml b/.context/effect/migration/annotations/effect__RcRef.yaml new file mode 100644 index 000000000..b77f8783e --- /dev/null +++ b/.context/effect/migration/annotations/effect__RcRef.yaml @@ -0,0 +1,12 @@ +effect/RcRef#RcRef: + replacement: "RcRef.RcRef" + note: "The model remains but is now only Pipeable; replace yielding or reading the RcRef directly with RcRef.get in a Scope." +effect/RcRef#RcRefUnify: + replacement: "none" + note: "RcRef is no longer an Effect subtype, so its Effect unification helper was removed; call RcRef.get explicitly." +effect/RcRef#RcRefUnifyIgnore: + replacement: "none" + note: "RcRef is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/RcRef#TypeId: + replacement: "none" + note: "The RcRef type id is internal in v4; do not inspect or construct the brand directly." diff --git a/.context/effect/migration/annotations/effect__Readable.yaml b/.context/effect/migration/annotations/effect__Readable.yaml new file mode 100644 index 000000000..2e5c5dd0e --- /dev/null +++ b/.context/effect/migration/annotations/effect__Readable.yaml @@ -0,0 +1,21 @@ +"effect/Readable#isReadable": + replacement: "Effect.isEffect" + note: "Readable was removed; after representing reads directly as Effect, use the Effect guard." +"effect/Readable#make": + replacement: "Effect.Effect" + note: "Use the supplied Effect directly; the v3 constructor only wrapped it as a get property." +"effect/Readable#map": + replacement: "Effect.map" + note: "Represent Readable as Effect and map it directly." +"effect/Readable#mapEffect": + replacement: "Effect.flatMap" + note: "Represent Readable as Effect and flatMap it directly." +"effect/Readable#Readable": + replacement: "Effect.Effect" + note: "The branded wrapper was removed; represent read access directly as Effect.Effect." +"effect/Readable#TypeId": + replacement: "Effect.TypeId" + note: "The Readable brand was removed; use Effect.TypeId only when branding checks remain necessary after collapsing to Effect." +"effect/Readable#unwrap": + replacement: "Effect.flatten" + note: "After replacing the inner Readable with Effect, flatten the nested Effect directly." diff --git a/.context/effect/migration/annotations/effect__Record.yaml b/.context/effect/migration/annotations/effect__Record.yaml new file mode 100644 index 000000000..4e86dff11 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Record.yaml @@ -0,0 +1,24 @@ +"effect/Record#getEquivalence": + replacement: "Record.makeEquivalence" + note: "Direct rename; pass the value equivalence unchanged." +"effect/Record#getLefts": + replacement: "Record.getFailures" + note: "Extract Result.Failure values while preserving keys." +"effect/Record#getRights": + replacement: "Record.getSuccesses" + note: "Extract Result.Success values while preserving keys." +"effect/Record#modifyOption": + replacement: "Record.modify" + note: "The Option suffix was dropped; missing keys still return Option.none." +"effect/Record#partitionMap": + replacement: "Record.partition" + note: "Pass a mapper returning Result; failures and successes form the two output records." +"effect/Record#ReadonlyRecord": + replacement: "Record.ReadonlyRecord" + note: "The public type and parameter order are unchanged." +"effect/Record#ReadonlyRecord.IsFiniteString": + replacement: "Record.ReadonlyRecord.IsFiniteString" + note: "The namespace utility type is unchanged." +"effect/Record#replaceOption": + replacement: "Record.replace" + note: "The Option suffix was dropped; missing keys still return Option.none." diff --git a/.context/effect/migration/annotations/effect__RedBlackTree.yaml b/.context/effect/migration/annotations/effect__RedBlackTree.yaml new file mode 100644 index 000000000..31085e816 --- /dev/null +++ b/.context/effect/migration/annotations/effect__RedBlackTree.yaml @@ -0,0 +1,105 @@ +"effect/RedBlackTree#at": + replacement: "Array.drop" + note: "Represent the removed tree as sorted entries; for a non-negative index, Array.drop(entries, index) traverses forward from that absolute position." +"effect/RedBlackTree#atReversed": + replacement: "Array.take + Array.reverse" + note: "For a valid absolute index, reverse Array.take(entries, index + 1) to traverse backward from it." +"effect/RedBlackTree#Direction": + replacement: "none" + note: "The tree direction type was removed; use normal array order or Array.reverse." +"effect/RedBlackTree#empty": + replacement: "Array.empty" + note: "The module was removed; use an empty Array and retain the Order separately." +"effect/RedBlackTree#first": + replacement: "Array.head" + note: "On a sorted entry array, Array.head returns the same optional minimum entry." +"effect/RedBlackTree#forEachBetween": + replacement: "Array.filter + Array.forEach" + note: "Filter sorted entries to min <= key < max with the retained Order, then visit them with Array.forEach." +"effect/RedBlackTree#forEachGreaterThanEqual": + replacement: "Array.filter + Array.forEach" + note: "Filter sorted entries to key >= min with the retained Order, then visit them in ascending order." +"effect/RedBlackTree#forEachLessThan": + replacement: "Array.filter + Array.forEach" + note: "Filter sorted entries to key < max with the retained Order, then visit them in ascending order." +"effect/RedBlackTree#fromIterable": + replacement: "Array.sortWith" + note: "Sort the entry iterable by key and retain the Order separately; this does not preserve logarithmic tree operations." +"effect/RedBlackTree#getAt": + replacement: "Array.get" + note: "Array.get on sorted entries preserves the optional index lookup behavior." +"effect/RedBlackTree#getOrder": + replacement: "none" + note: "No replacement collection stores an Order; retain and pass the Order explicitly." +"effect/RedBlackTree#greaterThan": + replacement: "Array.filter" + note: "Filter sorted entries with the retained Order for key > bound." +"effect/RedBlackTree#greaterThanEqual": + replacement: "Array.filter" + note: "Filter sorted entries with the retained Order for key >= bound." +"effect/RedBlackTree#greaterThanEqualReversed": + replacement: "Array.filter + Array.reverse" + note: "Filter sorted entries with the retained Order for key >= bound, then reverse for descending traversal." +"effect/RedBlackTree#greaterThanReversed": + replacement: "Array.filter + Array.reverse" + note: "Filter sorted entries with the retained Order for key > bound, then reverse for descending traversal." +"effect/RedBlackTree#has": + replacement: "Array.some" + note: "Use Array.some on sorted entries with Equal.equals for key membership; this is linear rather than logarithmic." +"effect/RedBlackTree#insert": + replacement: "Array.prepend + Array.sortWith" + note: "Prepend the entry and sort by key to preserve newest-first comparator ties; use an external ordered multimap if logarithmic updates matter." +"effect/RedBlackTree#isRedBlackTree": + replacement: "Array.isArray" + note: "The brand was removed; Array.isArray only checks the replacement representation and cannot prove its sorted invariant." +"effect/RedBlackTree#keys": + replacement: "Array.map" + note: "Map sorted entries to keys and iterate the resulting array." +"effect/RedBlackTree#keysReversed": + replacement: "Array.reverse + Array.map" + note: "Reverse sorted entries, map them to keys, and iterate the resulting array." +"effect/RedBlackTree#last": + replacement: "Array.last" + note: "On a sorted entry array, Array.last returns the same optional maximum entry." +"effect/RedBlackTree#lessThan": + replacement: "Array.filter" + note: "Filter sorted entries with the retained Order for key < bound." +"effect/RedBlackTree#lessThanEqual": + replacement: "Array.filter" + note: "Filter sorted entries with the retained Order for key <= bound." +"effect/RedBlackTree#lessThanEqualReversed": + replacement: "Array.filter + Array.reverse" + note: "Filter sorted entries with the retained Order for key <= bound, then reverse for descending traversal." +"effect/RedBlackTree#lessThanReversed": + replacement: "Array.filter + Array.reverse" + note: "Filter sorted entries with the retained Order for key < bound, then reverse for descending traversal." +"effect/RedBlackTree#make": + replacement: "Array.sortWith" + note: "Sort the supplied entries by key and retain the Order separately; this is not a balanced tree." +"effect/RedBlackTree#RedBlackTree": + replacement: "ReadonlyArray" + note: "The core tree was removed; use sorted immutable entries for small collections or an external persistent ordered multimap when complexity or duplicate-key semantics matter." +"effect/RedBlackTree#RedBlackTree.Direction": + replacement: "none" + note: "The nested direction type was removed; use normal array order or Array.reverse." +"effect/RedBlackTree#reduce": + replacement: "Array.reduce" + note: "Reduce sorted entries in ascending order, adapting the callback to receive [key, value]." +"effect/RedBlackTree#removeFirst": + replacement: "Array.findFirstIndex + Array.remove" + note: "Find the first entry whose key is Equal.equals to the target, then remove that index; leave the array unchanged when absent." +"effect/RedBlackTree#reversed": + replacement: "Array.reverse" + note: "Reverse the sorted entry array for descending traversal." +"effect/RedBlackTree#size": + replacement: "Array.length" + note: "Use Array.length or the .length property on the replacement entry array." +"effect/RedBlackTree#TypeId": + replacement: "none" + note: "The RedBlackTree module and brand symbol were removed." +"effect/RedBlackTree#values": + replacement: "Array.map" + note: "Map sorted entries to values and iterate the resulting array to preserve key order." +"effect/RedBlackTree#valuesReversed": + replacement: "Array.reverse + Array.map" + note: "Reverse sorted entries, map them to values, and iterate to preserve reverse key order." diff --git a/.context/effect/migration/annotations/effect__Redacted.yaml b/.context/effect/migration/annotations/effect__Redacted.yaml new file mode 100644 index 000000000..0334c40d5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Redacted.yaml @@ -0,0 +1,15 @@ +"effect/Redacted#getEquivalence": + replacement: "Redacted.makeEquivalence" + note: "Renamed to the v4 make-prefix convention." +"effect/Redacted#Redacted": + replacement: "Redacted.Redacted" + note: "The sensitive-value wrapper remains and now optionally carries a label." +"effect/Redacted#Redacted.Variance": + replacement: "Redacted.Redacted.Variance" + note: "The type-level variance member remains." +"effect/Redacted#RedactedTypeId": + replacement: "Redacted.isRedacted" + note: "The marker is private in v4; use the public guard for runtime narrowing." +"effect/Redacted#unsafeWipe": + replacement: "Redacted.wipeUnsafe" + note: "Renamed to use the v4 Unsafe suffix convention." diff --git a/.context/effect/migration/annotations/effect__Ref.yaml b/.context/effect/migration/annotations/effect__Ref.yaml new file mode 100644 index 000000000..3abdf00fb --- /dev/null +++ b/.context/effect/migration/annotations/effect__Ref.yaml @@ -0,0 +1,48 @@ +effect/Ref#getAndSet: + replacement: "Ref.getAndSet" + note: "The operation remains with data-first and data-last forms." +effect/Ref#getAndUpdate: + replacement: "Ref.getAndUpdate" + note: "The operation remains with data-first and data-last forms." +effect/Ref#getAndUpdateSome: + replacement: "Ref.getAndUpdateSome" + note: "The operation remains; Option.none leaves the value unchanged." +effect/Ref#modify: + replacement: "Ref.modify" + note: "The operation remains with data-first and data-last forms." +effect/Ref#Ref: + replacement: "Ref.Ref" + note: "The model remains but is now Pipeable rather than an Effect or Readable subtype; read it explicitly with Ref.get." +effect/Ref#Ref.Variance: + replacement: "Ref.Ref.Variance" + note: "The marker remains under Ref.Ref, but its brand uses an internal type id; ordinary code should use Ref.Ref directly." +effect/Ref#RefTypeId: + replacement: "none" + note: "The Ref type id is internal in v4; do not inspect or construct the brand directly." +effect/Ref#RefUnify: + replacement: "none" + note: "Ref is no longer an Effect subtype, so its Effect unification helper was removed; call Ref.get explicitly." +effect/Ref#RefUnifyIgnore: + replacement: "none" + note: "Ref is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/Ref#set: + replacement: "Ref.set" + note: "The operation remains with data-first and data-last forms." +effect/Ref#setAndGet: + replacement: "Ref.setAndGet" + note: "The operation remains with data-first and data-last forms." +effect/Ref#unsafeMake: + replacement: "Ref.makeUnsafe" + note: "The unsafe suffix moved to the end." +effect/Ref#update: + replacement: "Ref.update" + note: "The operation remains with data-first and data-last forms." +effect/Ref#updateAndGet: + replacement: "Ref.updateAndGet" + note: "The operation remains with data-first and data-last forms." +effect/Ref#updateSome: + replacement: "Ref.updateSome" + note: "The operation remains; Option.none leaves the value unchanged." +effect/Ref#updateSomeAndGet: + replacement: "Ref.updateSomeAndGet" + note: "The operation remains; Option.none leaves the value unchanged and returns the current value." diff --git a/.context/effect/migration/annotations/effect__Reloadable.yaml b/.context/effect/migration/annotations/effect__Reloadable.yaml new file mode 100644 index 000000000..a91751596 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Reloadable.yaml @@ -0,0 +1,30 @@ +"effect/Reloadable#auto": + replacement: "LayerRef.Service(..., { layer, invalidationSchedule: schedule, preload: true }).layer" + note: "Use LayerRef for scheduled refresh; add idleTimeToLive: Duration.infinity to preserve an always-resident instance." +"effect/Reloadable#autoFromConfig": + replacement: "Layer.unwrap with Effect.contextWith and LayerRef.make" + note: "Compute the schedule from the current context, then construct a preloaded LayerRef; no config-specific constructor remains." +"effect/Reloadable#get": + replacement: "ServiceRef.get or Effect.map(ServiceRef.contextEffect, Context.get(Service))" + note: "LayerRef.get provides the current context as a layer; contextEffect gives scoped direct access." +"effect/Reloadable#manual": + replacement: "LayerRef.Service(..., { layer, preload: true }).layer" + note: "Refresh with the generated service's refresh effect; use infinite idleTimeToLive for v3's resident lifecycle." +"effect/Reloadable#reload": + replacement: "ServiceRef.refresh" + note: "Refresh invalidates and immediately reacquires; invalidate alone rebuilds on the next borrow." +"effect/Reloadable#Reloadable": + replacement: "LayerRef.LayerRef" + note: "LayerRef is the v4 refreshable layer-context abstraction." +"effect/Reloadable#Reloadable.Variance": + replacement: "none" + note: "The exported variance artifact was removed and LayerRef has no public counterpart." +"effect/Reloadable#ReloadableTypeId": + replacement: "none" + note: "Reloadable was removed and LayerRef's marker is private." +"effect/Reloadable#reloadFork": + replacement: "ServiceRef.refresh.pipe(Effect.ignore({ log: true }), Effect.forkDetach({ startImmediately: true }), Effect.asVoid)" + note: "This recreates logged, ignored background refresh; forkDaemon became forkDetach." +"effect/Reloadable#tag": + replacement: "LayerRef.Service()(id, options)" + note: "The generated LayerRef service class is itself the Context.Service key." diff --git a/.context/effect/migration/annotations/effect__Request.yaml b/.context/effect/migration/annotations/effect__Request.yaml new file mode 100644 index 000000000..b8e0ab8fe --- /dev/null +++ b/.context/effect/migration/annotations/effect__Request.yaml @@ -0,0 +1,30 @@ +effect/Request#Cache: + replacement: "RequestResolver.asCache" + note: "The runtime request cache type was removed; expose resolver results through a first-class Cache, or use RequestResolver.withCache to retain a resolver." +effect/Request#Entry: + replacement: "Request.Entry" + note: "Entry remains but now carries request, context, uninterruptible, and completeUnsafe fields; Deferred, listener, owner, and state fields were removed." +effect/Request#EntryTypeId: + replacement: "none" + note: "Request entries are unbranded structural values in v4; do not inspect or construct an entry type id." +effect/Request#interruptWhenPossible: + replacement: "none" + note: "Request cancellation is managed by the v4 batching runtime; resolver code should complete the entries it receives and not wrap work with this internal listener helper." +effect/Request#isEntry: + replacement: "none" + note: "The entry guard was removed; entries are supplied structurally to RequestResolver callbacks." +effect/Request#Listeners: + replacement: "none" + note: "Request listener accounting is no longer public; cancellation and shared request lifecycle are managed by the v4 runtime and resolver caching." +effect/Request#makeCache: + replacement: "RequestResolver.asCache" + note: "Create a cache from a resolver with capacity and timeToLive options, or use RequestResolver.withCache for a cached resolver." +effect/Request#Request: + replacement: "Request.Request" + note: "The request model remains and adds a third R parameter for services required while resolving the request." +effect/Request#Request.OptionalResult: + replacement: "Exit.Exit>, Request.Error>" + note: "The named alias was removed; write the optional request exit type directly when it is still required." +effect/Request#RequestTypeId: + replacement: "none" + note: "The request type id is internal in v4; define requests by extending Request.Request or with Request.Class and do not depend on branding internals." diff --git a/.context/effect/migration/annotations/effect__RequestBlock.yaml b/.context/effect/migration/annotations/effect__RequestBlock.yaml new file mode 100644 index 000000000..bc9b172eb --- /dev/null +++ b/.context/effect/migration/annotations/effect__RequestBlock.yaml @@ -0,0 +1,30 @@ +effect/RequestBlock#empty: + replacement: "Effect.void" + note: "RequestBlock was removed; represent an empty computation as Effect.void and let Effect.request perform batching." +effect/RequestBlock#Empty: + replacement: "none" + note: "The public blocked-request graph was removed; application code should compose Effect.request computations instead of inspecting Empty nodes." +effect/RequestBlock#mapRequestResolvers: + replacement: "Effect.request" + note: "Pass the selected resolver to each Effect.request call; the runtime request graph can no longer be traversed to rewrite resolvers." +effect/RequestBlock#Par: + replacement: "none" + note: "The public blocked-request graph was removed; express parallel request execution with Effect concurrency combinators." +effect/RequestBlock#parallel: + replacement: "Effect.all" + note: "Compose request effects with Effect.all and explicit concurrency; v4 batching is performed by resolver and batch key rather than RequestBlock nodes." +effect/RequestBlock#reduce: + replacement: "none" + note: "The public blocked-request graph and reducer were removed; structure analysis is now internal to the request runtime." +effect/RequestBlock#RequestBlock: + replacement: "none" + note: "RequestBlock is no longer public in v4; compose Effect.request values directly and let the runtime batch requests by resolver." +effect/RequestBlock#Seq: + replacement: "none" + note: "The public blocked-request graph was removed; express sequencing in the Effect program instead of constructing Seq nodes." +effect/RequestBlock#sequential: + replacement: "Effect.andThen" + note: "Sequence request effects with Effect.andThen, flatMap, or generator syntax; RequestBlock sequencing nodes were removed." +effect/RequestBlock#single: + replacement: "Effect.request" + note: "Construct the request effect directly with its Request value and RequestResolver; the runtime creates pending entries internally." diff --git a/.context/effect/migration/annotations/effect__RequestResolver.yaml b/.context/effect/migration/annotations/effect__RequestResolver.yaml new file mode 100644 index 000000000..647e69250 --- /dev/null +++ b/.context/effect/migration/annotations/effect__RequestResolver.yaml @@ -0,0 +1,36 @@ +effect/RequestResolver#aroundRequests: + replacement: "RequestResolver.around" + note: "around now receives Request.Entry batches; map entries to entry.request in before and after when hooks need raw request values." +effect/RequestResolver#contextFromEffect: + replacement: "Request.Request" + note: "Resolvers no longer carry an environment parameter; declare R on each Request and use entry.context inside the resolver callback." +effect/RequestResolver#contextFromServices: + replacement: "Request.Request" + note: "Declare the selected services in the Request R parameter and read them from each entry.context; resolver-level context capture was removed." +effect/RequestResolver#eitherWith: + replacement: "RequestResolver.fromEffectTagged" + note: "Define one resolver for the combined tagged request union, or use RequestResolver.make to partition entries manually; resolver routing combinators were removed." +effect/RequestResolver#locally: + replacement: "Effect.provideService" + note: "FiberRef-based resolver localization was removed; migrate the FiberRef to Context.Reference and provide its value around the request effect or resolver work." +effect/RequestResolver#makeBatched: + replacement: "RequestResolver.make" + note: "make now receives a non-empty batch of Request.Entry values; read entry.request and complete every entry with completeUnsafe or Request completion helpers." +effect/RequestResolver#makeWithEntry: + replacement: "RequestResolver.make" + note: "Use make for entry-level handling; v4 supplies one non-empty batch and key instead of nested sequential and parallel entry arrays." +effect/RequestResolver#mapInputContext: + replacement: "Request.Request" + note: "Resolver environments were removed; put required services on the Request R parameter and transform or provide each entry.context explicitly when needed." +effect/RequestResolver#provideContext: + replacement: "Effect.provideService" + note: "Provide services to Effect.request so they are captured in entry.context; RequestResolver itself no longer has an environment parameter." +effect/RequestResolver#RequestResolver: + replacement: "RequestResolver.RequestResolver" + note: "The interface remains as RequestResolver; remove its R parameter and move service requirements to Request." +effect/RequestResolver#RequestResolver.Variance: + replacement: "RequestResolver.RequestResolver.Variance" + note: "The variance marker remains but tracks only the accepted Request type; resolver environment variance was removed." +effect/RequestResolver#RequestResolverTypeId: + replacement: "none" + note: "The resolver type id is internal in v4; use RequestResolver constructors and isRequestResolver rather than depending on its brand." diff --git a/.context/effect/migration/annotations/effect__Resource.yaml b/.context/effect/migration/annotations/effect__Resource.yaml new file mode 100644 index 000000000..e26fcaee7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Resource.yaml @@ -0,0 +1,15 @@ +"effect/Resource#Resource": + replacement: "Resource.Resource" + note: "The type remains but no longer extends Effect; use Resource.get(resource)." +"effect/Resource#Resource.Variance": + replacement: "none" + note: "The exported variance artifact was removed." +"effect/Resource#ResourceTypeId": + replacement: "Resource.isResource" + note: "The marker is private; use the public runtime guard." +"effect/Resource#ResourceUnify": + replacement: "none" + note: "Resource no longer extends Effect; use Resource.get explicitly." +"effect/Resource#ResourceUnifyIgnore": + replacement: "none" + note: "The Effect-unification implementation detail was removed." diff --git a/.context/effect/migration/annotations/effect__Runtime.yaml b/.context/effect/migration/annotations/effect__Runtime.yaml new file mode 100644 index 000000000..89779d17b --- /dev/null +++ b/.context/effect/migration/annotations/effect__Runtime.yaml @@ -0,0 +1,84 @@ +effect/Runtime#AsyncFiberException: + replacement: "Cause.AsyncFiberError" + note: "The error thrown when synchronous execution encounters an async boundary was renamed." +effect/Runtime#Cancel: + replacement: "ReturnType" + note: "Use the cancellation function returned by runCallback; the named type was removed." +effect/Runtime#defaultRuntime: + replacement: "Context.empty()" + note: "Runtime values were removed; call Effect.run* directly or use an empty Context with an Effect.run*With function." +effect/Runtime#defaultRuntimeFlags: + replacement: "none" + note: "Runtime flags were removed; configure scheduler yielding, interruptibility, and runtime metrics independently." +effect/Runtime#deleteFiberRef: + replacement: "Context.omit" + note: "FiberRefs became Context.Reference values; omit the Reference override from the Context." +effect/Runtime#disableRuntimeFlag: + replacement: "none" + note: "Runtime flags were removed; disable the corresponding scheduler, interruptibility, or metric behavior directly." +effect/Runtime#enableRuntimeFlag: + replacement: "none" + note: "Runtime flags were removed; enable the corresponding scheduler, interruptibility, or metric behavior directly." +effect/Runtime#FiberFailure: + replacement: "none" + note: "The runner error wrapper was removed; use an Exit-returning runner to retain and inspect a structured Cause." +effect/Runtime#FiberFailureCauseId: + replacement: "none" + note: "The FiberFailure wrapper and its cause marker were removed; inspect Cause through Exit instead." +effect/Runtime#FiberFailureId: + replacement: "none" + note: "The FiberFailure wrapper and its brand were removed; inspect Cause through Exit instead." +effect/Runtime#isAsyncFiberException: + replacement: "Cause.isAsyncFiberError" + note: "Use the renamed guard from Cause." +effect/Runtime#isFiberFailure: + replacement: "none" + note: "FiberFailure no longer exists; use an Exit-returning runner and inspect Exit or Cause." +effect/Runtime#make: + replacement: "Context.make" + note: "Runtime values were removed; construct the service Context passed to Effect.run*With instead." +effect/Runtime#makeFiberFailure: + replacement: "Cause.squash" + note: "Use Cause.squash only when a Cause must become the value thrown or rejected by a runner." +effect/Runtime#runCallback: + replacement: "Effect.runCallbackWith" + note: "Run with the former Runtime's Context; use Effect.runCallback when no services are required." +effect/Runtime#RunCallbackOptions: + replacement: "Effect.RunOptions & { readonly onExit: (exit: Exit.Exit) => void }" + note: "The callback runner now combines Effect.RunOptions with an onExit callback; no named options type is exported." +effect/Runtime#runFork: + replacement: "Effect.runForkWith" + note: "Run with the former Runtime's Context; use Effect.runFork when no services are required." +effect/Runtime#RunForkOptions: + replacement: "Effect.RunOptions" + note: "Use common runner options; express scoped forking with Effect.forkIn or Effect.forkScoped." +effect/Runtime#runPromise: + replacement: "Effect.runPromiseWith" + note: "Run with the former Runtime's Context; use Effect.runPromise when no services are required." +effect/Runtime#runPromiseExit: + replacement: "Effect.runPromiseExitWith" + note: "Run with the former Runtime's Context; use Effect.runPromiseExit when no services are required." +effect/Runtime#runSync: + replacement: "Effect.runSyncWith" + note: "Run with the former Runtime's Context; use Effect.runSync when no services are required." +effect/Runtime#runSyncExit: + replacement: "Effect.runSyncExitWith" + note: "Run with the former Runtime's Context; use Effect.runSyncExit when no services are required." +effect/Runtime#Runtime: + replacement: "Context.Context" + note: "Runtime values were removed; carry a Context and invoke the corresponding Effect.run*With function." +effect/Runtime#Runtime.Context: + replacement: "none" + note: "The Runtime context extractor was removed; carry the service union directly on Context.Context." +effect/Runtime#setFiberRef: + replacement: "Context.add" + note: "FiberRefs became Context.Reference values; add the Reference override to the Context." +effect/Runtime#updateFiberRefs: + replacement: "Context.add" + note: "There is no aggregate FiberRefs update; add targeted Context.Reference overrides to the carried Context explicitly." +effect/Runtime#updateContext: + replacement: "Context transformation + Effect.run*With" + note: "Runtime values were removed; transform the carried Context directly, then pass the result to the corresponding Effect.run*With function." +effect/Runtime#updateRuntimeFlags: + replacement: "none" + note: "Runtime flags and aggregate patches were removed; configure each semantic behavior independently." diff --git a/.context/effect/migration/annotations/effect__RuntimeFlags.yaml b/.context/effect/migration/annotations/effect__RuntimeFlags.yaml new file mode 100644 index 000000000..eb6d3bcd3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__RuntimeFlags.yaml @@ -0,0 +1,108 @@ +effect/RuntimeFlags#cooperativeYielding: + replacement: "!References.PreventSchedulerYield" + note: "Read the scheduler Reference and negate it; the aggregate flags value was removed." +effect/RuntimeFlags#CooperativeYielding: + replacement: "References.PreventSchedulerYield" + note: "Use the scheduler Reference with inverse boolean meaning." +effect/RuntimeFlags#diff: + replacement: "none" + note: "The runtime-flags bitset was removed; configure each semantic behavior directly." +effect/RuntimeFlags#differ: + replacement: "none" + note: "The runtime-flags bitset and patch differ were removed." +effect/RuntimeFlags#disable: + replacement: "none" + note: "The generic flag operation was removed; disable the corresponding behavior directly." +effect/RuntimeFlags#disableAll: + replacement: "none" + note: "The aggregate flags value was removed; configure scheduler yielding, interruptibility, and metrics independently." +effect/RuntimeFlags#disableCooperativeYielding: + replacement: "Effect.provideService(References.PreventSchedulerYield, true)" + note: "Prevent scheduler yielding through its Context.Reference." +effect/RuntimeFlags#disableInterruption: + replacement: "Effect.uninterruptible" + note: "Use an uninterruptible region instead of changing a runtime flag." +effect/RuntimeFlags#disableOpSupervision: + replacement: "none" + note: "Operation supervision and its runtime flag were removed." +effect/RuntimeFlags#disableRuntimeMetrics: + replacement: "Metric.disableRuntimeMetrics" + note: "Disable fiber runtime metrics directly; use disableRuntimeMetricsLayer when providing a Layer." +effect/RuntimeFlags#disableWindDown: + replacement: "none" + note: "The wind-down flag is runtime-internal in v4; use normal scoped finalizers and explicit interruptibility regions." +effect/RuntimeFlags#enable: + replacement: "none" + note: "The generic flag operation was removed; enable the corresponding behavior directly." +effect/RuntimeFlags#enableAll: + replacement: "none" + note: "The aggregate flags value was removed; configure scheduler yielding, interruptibility, and metrics independently." +effect/RuntimeFlags#enableCooperativeYielding: + replacement: "Effect.provideService(References.PreventSchedulerYield, false)" + note: "Allow scheduler yielding through its Context.Reference." +effect/RuntimeFlags#enableInterruption: + replacement: "Effect.interruptible" + note: "Use an interruptible region instead of changing a runtime flag." +effect/RuntimeFlags#enableOpSupervision: + replacement: "none" + note: "Operation supervision and its runtime flag were removed." +effect/RuntimeFlags#enableRuntimeMetrics: + replacement: "Metric.enableRuntimeMetrics" + note: "Enable fiber runtime metrics directly; use enableRuntimeMetricsLayer when providing a Layer." +effect/RuntimeFlags#enableWindDown: + replacement: "none" + note: "The wind-down flag is runtime-internal in v4; use normal scoped finalizers and explicit interruptibility regions." +effect/RuntimeFlags#interruptible: + replacement: "none" + note: "There is no public current-interruptibility getter; structure the program with Effect.interruptible or Effect.uninterruptible." +effect/RuntimeFlags#interruption: + replacement: "none" + note: "Interruptibility is controlled by Effect regions rather than queried from a flags value." +effect/RuntimeFlags#Interruption: + replacement: "Effect.interruptible | Effect.uninterruptible" + note: "The bit flag was removed; control interruptibility with Effect regions." +effect/RuntimeFlags#isDisabled: + replacement: "none" + note: "There is no aggregate flags value to query; inspect or control the corresponding semantic facility." +effect/RuntimeFlags#make: + replacement: "none" + note: "The runtime-flags bitset was removed; do not recreate it in v4." +effect/RuntimeFlags#none: + replacement: "none" + note: "The runtime-flags bitset was removed; configure each semantic behavior independently." +effect/RuntimeFlags#None: + replacement: "none" + note: "The empty runtime-flags value and its type were removed." +effect/RuntimeFlags#opSupervision: + replacement: "none" + note: "Operation supervision and its runtime flag were removed." +effect/RuntimeFlags#OpSupervision: + replacement: "none" + note: "Operation supervision and its runtime flag were removed." +effect/RuntimeFlags#patch: + replacement: "none" + note: "Aggregate runtime-flags patches were removed; configure each semantic behavior directly." +effect/RuntimeFlags#render: + replacement: "none" + note: "The runtime-flags bitset and its renderer were removed." +effect/RuntimeFlags#RuntimeFlag: + replacement: "none" + note: "Individual bit flags were removed; use the corresponding semantic API." +effect/RuntimeFlags#RuntimeFlags: + replacement: "none" + note: "The aggregate runtime-flags bitset was removed." +effect/RuntimeFlags#runtimeMetrics: + replacement: "Metric.FiberRuntimeMetrics" + note: "Read the Context.Reference and test for undefined instead of querying a bit flag." +effect/RuntimeFlags#RuntimeMetrics: + replacement: "Metric.FiberRuntimeMetrics" + note: "Runtime metrics are now configured through a Context.Reference service rather than a bit flag." +effect/RuntimeFlags#toSet: + replacement: "none" + note: "The runtime-flags bitset was removed; there is no set conversion." +effect/RuntimeFlags#windDown: + replacement: "none" + note: "The wind-down flag is no longer public." +effect/RuntimeFlags#WindDown: + replacement: "none" + note: "The wind-down flag is no longer public." diff --git a/.context/effect/migration/annotations/effect__RuntimeFlagsPatch.yaml b/.context/effect/migration/annotations/effect__RuntimeFlagsPatch.yaml new file mode 100644 index 000000000..915423f68 --- /dev/null +++ b/.context/effect/migration/annotations/effect__RuntimeFlagsPatch.yaml @@ -0,0 +1,3 @@ +effect/RuntimeFlagsPatch: + replacement: none + note: The aggregate RuntimeFlagsPatch abstraction, its enabled/disabled bit sets, set operations, queries, and renderer were removed; no aggregate patch value remains to construct, combine, inspect, or render. Enable, disable, or invert the corresponding semantic behavior directly, combining semantic configurations where needed. Configure scheduler yielding, interruptibility, or metrics directly, and inspect the corresponding semantic facility when needed. diff --git a/.context/effect/migration/annotations/effect__STM.yaml b/.context/effect/migration/annotations/effect__STM.yaml new file mode 100644 index 000000000..d11b88e98 --- /dev/null +++ b/.context/effect/migration/annotations/effect__STM.yaml @@ -0,0 +1,354 @@ +effect/STM#Adapter: + replacement: "none" + note: "The STM.gen adapter was removed; Effect.gen accepts yielded Effects directly." +effect/STM#All.IsDiscard: + replacement: "Effect.All.IsDiscard" + note: "The helper moved to Effect.All because STM.all is now Effect.all." +effect/STM#All.Narrow: + replacement: "none" + note: "Effect.all uses a const generic directly, so the separate tuple-narrowing helper was removed." +effect/STM#All.Options: + replacement: "none" + note: "Effect.all inlines its options type; use its concurrency, discard, and mode options directly." +effect/STM#All.STMAny: + replacement: "Effect.All.EffectAny" + note: "STM inputs are ordinary Effects in v4, so use the Effect.All helper." +effect/STM#All.Signature: + replacement: "typeof Effect.all" + note: "The named STM all signature was removed; refer to Effect.all directly." +effect/STM#Do: + replacement: "Effect.Do" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#STM: + replacement: "Effect.Effect" + note: "The distinct STM instruction type was removed. Tx APIs return Effect values; wrap the complete transaction in Effect.tx." +effect/STM#STM.Variance: + replacement: "Effect.Variance" + note: "The distinct STM variance marker was removed with STM; use the Effect marker." +effect/STM#STMTypeId: + replacement: "Effect.TypeId" + note: "The distinct STM type id was removed because transactions are represented by Effect values." +effect/STM#STMTypeLambda: + replacement: "Effect.EffectTypeLambda" + note: "Use the Effect type lambda; transactional requirements are represented by Effect.Transaction." +effect/STM#STMUnify: + replacement: "Effect.EffectUnify" + note: "STM unification moved to ordinary Effect unification." +effect/STM#STMUnifyIgnore: + replacement: "none" + note: "The STM-specific unification ignore marker was removed; rely on Effect inference." +effect/STM#acquireUseRelease: + replacement: "Effect.acquireUseRelease + Effect.tx" + note: "Wrap acquire, use, and release in separate Effect.tx calls to preserve the v3 separately committed phases; v4 release also receives the use Exit." +effect/STM#all: + replacement: "Effect.all" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#as: + replacement: "Effect.as" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#asSome: + replacement: "Effect.asSome" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#asSomeError: + replacement: "Effect.mapError(self, Option.some)" + note: "The dedicated helper was removed; map the error into Option.some." +effect/STM#asVoid: + replacement: "Effect.asVoid" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#attempt: + replacement: "Effect.try" + note: "The constructor was renamed; transaction programs are ordinary Effects in v4." +effect/STM#bind: + replacement: "Effect.bind" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#bindTo: + replacement: "Effect.bindTo" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#catchAll: + replacement: "Effect.catch" + note: "Use Effect.catch for typed failures. It does not catch Effect.txRetry or restore a transactional savepoint." +effect/STM#catchSome: + replacement: "Effect.catch + Option.match" + note: "Use Effect.catch and re-fail the original error when the partial handler returns None." +effect/STM#catchTag: + replacement: "Effect.catchTag" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#check: + replacement: "Effect.suspend + Effect.txRetry" + note: "Evaluate the predicate lazily and return Effect.void when true or Effect.txRetry when false, inside Effect.tx." +effect/STM#collect: + replacement: "Effect.flatMap + Option.match + Effect.txRetry" + note: "Map Some to success and None to Effect.txRetry inside the surrounding Effect.tx transaction." +effect/STM#collectSTM: + replacement: "Effect.flatMap + Option.match + Effect.txRetry" + note: "Return the Effect held by Some and use Effect.txRetry for None, inside the surrounding Effect.tx transaction." +effect/STM#commit: + replacement: "Effect.tx" + note: "Effect.tx runs an Effect transaction and removes its Effect.Transaction requirement." +effect/STM#commitEither: + replacement: "Effect.tx + Effect.result + Effect.fromResult" + note: "Run Effect.tx(Effect.result(body)) before Effect.fromResult so journal changes commit even when the original transaction had a typed failure." +effect/STM#cond: + replacement: "Effect.suspend" + note: "Lazily branch to Effect.succeed or Effect.fail based on the predicate." +effect/STM#context: + replacement: "Effect.context" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#contextWith: + replacement: "Effect.contextWith" + note: "The name remains, but the v4 callback returns an Effect directly." +effect/STM#contextWithSTM: + replacement: "Effect.contextWith" + note: "The Effect-returning context constructor no longer needs an STM suffix." +effect/STM#dieMessage: + replacement: "Effect.die(new Error(message))" + note: "The message-specific helper was removed; construct a message-bearing defect explicitly." +effect/STM#dieSync: + replacement: "Effect.suspend(() => Effect.die(evaluate()))" + note: "The lazy defect helper was removed; suspend construction and then die." +effect/STM#either: + replacement: "Effect.result" + note: "V4 uses Result instead of Either for materialized typed failures." +effect/STM#ensuring: + replacement: "Effect.ensuring" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#eventually: + replacement: "Effect.eventually" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#every: + replacement: "Effect.findFirst + Option.isNone" + note: "Search sequentially for the first false effectful predicate; no match means every element passed." +effect/STM#exists: + replacement: "Effect.findFirst + Option.isSome" + note: "Search sequentially for the first true effectful predicate." +effect/STM#fail: + replacement: "Effect.fail" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#failSync: + replacement: "Effect.failSync" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#fiberId: + replacement: "Effect.fiberId" + note: "The operation remains on Effect, but v4 yields the fiber id as a number." +effect/STM#filter: + replacement: "Effect.filter" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#filterNot: + replacement: "Effect.filter" + note: "Negate the effectful predicate result and use Effect.filter." +effect/STM#filterOrDie: + replacement: "Effect.filterOrFail + Effect.orDie" + note: "Fail with the lazy defect when the predicate rejects, then convert that failure to a defect." +effect/STM#filterOrDieMessage: + replacement: "Effect.filterOrFail + Effect.orDie" + note: "Fail with a new Error carrying the message when the predicate rejects, then convert it to a defect." +effect/STM#filterOrFail: + replacement: "Effect.filterOrFail" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#firstSuccessOf: + replacement: "Effect.firstSuccessOf" + note: "This only preserves typed-failure fallback. V4 has no equivalent for v3 retry-aware alternatives with journal savepoints." +effect/STM#flatMap: + replacement: "Effect.flatMap" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#flatten: + replacement: "Effect.flatten" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#flip: + replacement: "Effect.flip" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#flipWith: + replacement: "Effect.flip(self).pipe(f, Effect.flip)" + note: "Compose the retained Effect.flip operation around the transforming function." +effect/STM#fromEither: + replacement: "Effect.fromResult" + note: "V4 replaced Either with Result; migrate the value and use Effect.fromResult." +effect/STM#head: + replacement: "Effect.matchEffect" + note: "Map source failures to Option.some, return the first iterable element, and fail with Option.none when empty." +effect/STM#if: + replacement: "Effect.suspend or Effect.flatMap" + note: "Select the true or false branch lazily; use flatMap when the condition is effectful." +effect/STM#ignore: + replacement: "Effect.ignore" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#interrupt: + replacement: "Effect.interrupt" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#interruptAs: + replacement: "Effect.interrupt" + note: "V4 exposes interruption of the current fiber only; remove the explicit FiberId argument." +effect/STM#isFailure: + replacement: "Effect.isFailure" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#isSTM: + replacement: "Effect.isEffect" + note: "STM no longer has a distinct runtime representation; transaction programs are Effects." +effect/STM#isSuccess: + replacement: "Effect.isSuccess" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#iterate: + replacement: "Effect.gen loop" + note: "No direct Effect iterate helper remains; carry state in an explicit sequential Effect.gen loop inside Effect.tx." +effect/STM#let: + replacement: "Effect.let" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#loop: + replacement: "Effect.gen loop" + note: "No direct Effect loop helper remains; implement the state loop explicitly and collect values unless discard was requested." +effect/STM#map: + replacement: "Effect.map" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#mapAttempt: + replacement: "Effect.flatMap(self, (a) => Effect.try(() => f(a)))" + note: "Use Effect.try in flatMap so thrown exceptions remain typed failures rather than defects." +effect/STM#mapInputContext: + replacement: "Effect.updateContext" + note: "The context-input mapping operation was renamed on Effect." +effect/STM#match: + replacement: "Effect.match" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#matchSTM: + replacement: "Effect.matchEffect" + note: "The Effect-returning match combinator no longer has an STM suffix." +effect/STM#mergeAll: + replacement: "Effect.reduce" + note: "Reduce the input Effects sequentially and combine each produced value with the accumulator." +effect/STM#none: + replacement: "Effect.matchEffect + Option.match" + note: "Recreate the Option success/error shuffle explicitly; no dedicated helper remains." +effect/STM#option: + replacement: "Effect.option" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#orDie: + replacement: "Effect.orDie" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#orDieWith: + replacement: "Effect.mapError + Effect.orDie" + note: "Map the typed error to the desired defect and then use Effect.orDie." +effect/STM#orElse: + replacement: "none" + note: "V4 has no exact retry-aware transactional alternative with journal savepoint restoration. Effect.catch is only a failure-only approximation." +effect/STM#orElseEither: + replacement: "none" + note: "V4 has no exact retry-aware alternative. For typed failures only, compose Effect.catch and Result tagging manually." +effect/STM#orElseFail: + replacement: "Effect.mapError" + note: "Map typed failures to the replacement error; this does not preserve v3 retry fallback semantics." +effect/STM#orElseOptional: + replacement: "Effect.catch + Option.match" + note: "Run the fallback for None and re-fail Some errors explicitly." +effect/STM#orElseSucceed: + replacement: "Effect.orElseSucceed" + note: "The name remains for typed failures, but v4 does not preserve v3 retry fallback or journal savepoints." +effect/STM#orTry: + replacement: "none" + note: "V4 exposes no recoverable retry signal or public transactional savepoint; restructure branch selection before Effect.txRetry." +effect/STM#partition: + replacement: "Effect.partition" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#provideServiceSTM: + replacement: "Effect.provideServiceEffect" + note: "The effectful service provider was renamed on Effect." +effect/STM#provideSomeContext: + replacement: "Effect.provideContext or Effect.updateContext" + note: "The dedicated partial-context helper was removed; provide or update the Effect context explicitly." +effect/STM#reduce: + replacement: "Effect.reduce" + note: "The combinator remains, but v4 takes the initial state lazily and also passes the element index." +effect/STM#reduceAll: + replacement: "Effect.flatMap + Effect.reduce" + note: "Evaluate the initial Effect, then reduce the remaining Effects sequentially." +effect/STM#reduceRight: + replacement: "Effect.reduce over a reversed Array" + note: "Materialize and reverse the iterable, then reduce while preserving the old state/element callback order." +effect/STM#refineOrDie: + replacement: "Effect.catch + Option.match" + note: "Re-fail Some refined errors and die with the original error for None." +effect/STM#refineOrDieWith: + replacement: "Effect.catch + Option.match" + note: "Re-fail Some refined errors and map None to the requested defect." +effect/STM#reject: + replacement: "Effect.flatMap + Option.match" + note: "Fail when the partial rejection returns Some; otherwise keep the original success." +effect/STM#rejectSTM: + replacement: "Effect.flatMap + Option.match" + note: "Run and fail with the Effect held by Some; otherwise keep the original success." +effect/STM#repeatUntil: + replacement: "Effect.repeat(self, { until: predicate })" + note: "The dedicated combinator moved to Effect.repeat options." +effect/STM#repeatWhile: + replacement: "Effect.repeat(self, { while: predicate })" + note: "The dedicated combinator moved to Effect.repeat options." +effect/STM#replicate: + replacement: "Effect.replicate" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#replicateSTM: + replacement: "Effect.replicateEffect" + note: "Use the effectful replication combinator and keep execution sequential inside Effect.tx." +effect/STM#replicateSTMDiscard: + replacement: "Effect.replicateEffect(self, n, { discard: true })" + note: "Use effectful replication with discard enabled and keep execution sequential inside Effect.tx." +effect/STM#retry: + replacement: "Effect.txRetry" + note: "Do not use Effect.retry, which retries typed failures by schedule; Effect.txRetry waits for an accessed Tx value to change." +effect/STM#retryUntil: + replacement: "Effect.flatMap + Effect.txRetry" + note: "Succeed when the predicate passes; otherwise return Effect.txRetry inside Effect.tx." +effect/STM#retryWhile: + replacement: "Effect.flatMap + Effect.txRetry" + note: "Return Effect.txRetry while the predicate passes; otherwise succeed inside Effect.tx." +effect/STM#some: + replacement: "Effect.matchEffect + Option.match" + note: "Recreate the Option success/error shuffle explicitly; no dedicated helper remains." +effect/STM#succeed: + replacement: "Effect.succeed" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#summarized: + replacement: "Effect.gen" + note: "Run the summary Effect before and after the body, then return the computed summary and body value." +effect/STM#sync: + replacement: "Effect.sync" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#tap: + replacement: "Effect.tap" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#tapBoth: + replacement: "Effect.tapError + Effect.tap" + note: "Compose the separate failure and success taps." +effect/STM#tapError: + replacement: "Effect.tapError" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#try: + replacement: "Effect.try" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#unless: + replacement: "Effect.when(self, Effect.sync(() => !predicate()))" + note: "V4 Effect.when takes an effectful condition; suspend and negate the old lazy boolean." +effect/STM#unlessSTM: + replacement: "Effect.when(self, Effect.map(condition, (b) => !b))" + note: "Negate the effectful condition and use Effect.when." +effect/STM#unsome: + replacement: "Effect.matchEffect + Option.match" + note: "Recreate the Option error/success shuffle explicitly; no dedicated helper remains." +effect/STM#validateAll: + replacement: "Effect.validate" + note: "The validation combinator was renamed and now returns a NonEmptyArray of errors." +effect/STM#validateFirst: + replacement: "Effect.flip + Effect.forEach" + note: "Flip each candidate result, traverse sequentially, then flip the aggregate to preserve all errors when every candidate fails." +effect/STM#void: + replacement: "Effect.void" + note: "The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/STM#when: + replacement: "Effect.when(self, Effect.sync(predicate))" + note: "V4 Effect.when takes an effectful boolean, so suspend the old lazy predicate." +effect/STM#whenSTM: + replacement: "Effect.when" + note: "The effectful-condition form is now the only Effect.when form." +effect/STM#zipLeft: + replacement: "Effect.zipWith(self, that, (left) => left)" + note: "Use sequential Effect.zipWith and retain the left result." +effect/STM#zipRight: + replacement: "Effect.andThen" + note: "Use Effect.andThen for sequential composition that retains the right result." diff --git a/.context/effect/migration/annotations/effect__Schedule.yaml b/.context/effect/migration/annotations/effect__Schedule.yaml new file mode 100644 index 000000000..00770ca6c --- /dev/null +++ b/.context/effect/migration/annotations/effect__Schedule.yaml @@ -0,0 +1,267 @@ +"effect/Schedule#addDelayEffect": + replacement: "Schedule.addDelay" + note: "The v4 function is effectful by default and its callback receives full Schedule.Metadata; read metadata.output when only the prior output is needed." +"effect/Schedule#andThen": + replacement: "Schedule.concat" + note: "The sequencing combinator was renamed to Schedule.concat." +"effect/Schedule#andThenEither": + replacement: "Schedule.concatResult" + note: "Sequential phase tagging now uses Result: self outputs are Result.fail and the following schedule outputs are Result.succeed." +"effect/Schedule#as": + replacement: "Schedule.map" + note: "Map the metadata to the constant output; Schedule.map accepts either a plain value or an Effect." +"effect/Schedule#asVoid": + replacement: "Schedule.map" + note: "Map every output to undefined." +"effect/Schedule#bothInOut": + replacement: "none" + note: "There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done." +"effect/Schedule#check": + replacement: "Schedule.while" + note: "Continue while a predicate over metadata.input and metadata.output returns true." +"effect/Schedule#checkEffect": + replacement: "Schedule.while" + note: "Schedule.while accepts an effectful metadata predicate in v4." +"effect/Schedule#collectAllInputs": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#collectAllOutputs": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#collectUntil": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#collectUntilEffect": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#collectWhile": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#collectWhileEffect": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#compose": + replacement: "none" + note: "There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done." +"effect/Schedule#count": + replacement: "Schedule.forever" + note: "The forever schedule outputs the zero-based recurrence count." +"effect/Schedule#CurrentIterationMetadata": + replacement: "Schedule.CurrentMetadata" + note: "The Context.Reference was renamed and now provides Schedule.Metadata with input, output, attempt, duration, and elapsed timing fields." +"effect/Schedule#dayOfMonth": + replacement: "Schedule.cron" + note: "Express the calendar constraint as a cron expression, for example `0 0 * *`, and map its Duration output if a numeric output is required." +"effect/Schedule#dayOfWeek": + replacement: "Schedule.cron" + note: "Express the weekday constraint as a cron expression, for example `0 0 * * `, and map its Duration output if a numeric output is required." +"effect/Schedule#delayed": + replacement: "Schedule.modifyDelay" + note: "Return Effect.succeed(f(metadata.duration)); delay transformations are effectful and receive full metadata in v4." +"effect/Schedule#delayedEffect": + replacement: "Schedule.modifyDelay" + note: "The v4 delay modifier is effectful by default and receives full Schedule.Metadata." +"effect/Schedule#delayedSchedule": + replacement: "Schedule.modifyDelay" + note: "Replace each delay with metadata.output, converting that Duration output through Effect.succeed." +"effect/Schedule#delays": + replacement: "Schedule.map" + note: "Map each decision to metadata.duration to expose the selected recurrence delay." +"effect/Schedule#driver": + replacement: "Schedule.toStepWithSleep" + note: "Acquire the sleeping step function and call it for each input; use Schedule.toStep when delay handling must remain manual." +"effect/Schedule#either": + replacement: "Schedule.min" + note: "Use Schedule.min for fastest-delay composition. It outputs the selected Duration rather than a tuple of both outputs." +"effect/Schedule#eitherWith": + replacement: "Schedule.min" + note: "Schedule.min implements the standard fastest-delay composition; custom interval merging requires a Schedule.fromStep implementation." +"effect/Schedule#elapsed": + replacement: "Schedule.map" + note: "Map metadata.elapsed through Duration.millis." +"effect/Schedule#ensuring": + replacement: "Schedule.during" + note: "Use the duration-bounded v4 schedule constructor." +"effect/Schedule#fromDelay": + replacement: "Schedule.duration" + note: "The duration constructor recurs once after the supplied delay." +"effect/Schedule#fromDelays": + replacement: "Schedule.duration + Schedule.concat" + note: "Build one Schedule.duration per delay and sequence them with Schedule.concat." +"effect/Schedule#fromFunction": + replacement: "Schedule.identity + Schedule.map" + note: "Start with Schedule.identity() and map metadata.input through the function." +"effect/Schedule#hourOfDay": + replacement: "Schedule.cron" + note: "Express the hour constraint as a cron expression such as `0 * * *`." +"effect/Schedule#intersect": + replacement: "Schedule.max" + note: "Use Schedule.max for slowest-delay composition. It outputs the selected Duration rather than a tuple of both outputs." +"effect/Schedule#intersectWith": + replacement: "Schedule.max" + note: "Schedule.max implements the standard slowest-delay composition; custom interval merging requires a Schedule.fromStep implementation." +"effect/Schedule#IterationMetadata": + replacement: "Schedule.Metadata" + note: "The metadata model now includes duration and uses attempt instead of recurrence; elapsed fields are millisecond numbers." +"effect/Schedule#jitteredWith": + replacement: "Schedule.modifyDelay" + note: "For custom bounds, scale metadata.duration using Random.next inside the effectful delay callback; Schedule.jittered supplies the fixed v4 0.8-1.2 range." +"effect/Schedule#linear": + replacement: "Schedule.forever + Schedule.map + Schedule.modifyDelay" + note: "Map the recurrence attempt to the linearly increasing Duration, then use that output as the recurrence delay." +"effect/Schedule#makeWithState": + replacement: "Schedule.fromStep" + note: "Move mutable state into the acquired step closure; return [output, Duration] for recurrence and Cause.done(output) for termination." +"effect/Schedule#mapBoth": + replacement: "Schedule.fromStep + Schedule.toStep" + note: "Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step." +"effect/Schedule#mapBothEffect": + replacement: "Schedule.fromStep + Schedule.toStep" + note: "Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. Apply the effectful output mapping to the returned tuple." +"effect/Schedule#mapEffect": + replacement: "Schedule.map" + note: "Schedule.map accepts an Effect result and receives full Schedule.Metadata." +"effect/Schedule#mapInput": + replacement: "Schedule.fromStep + Schedule.toStep" + note: "Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step." +"effect/Schedule#mapInputContext": + replacement: "Schedule.fromStep + Effect.provide" + note: "Provide the transformed service context to both Schedule.toStep acquisition and each returned step Effect." +"effect/Schedule#mapInputEffect": + replacement: "Schedule.fromStep + Schedule.toStep" + note: "Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. Evaluate the input mapping Effect before the underlying step." +"effect/Schedule#minuteOfHour": + replacement: "Schedule.cron" + note: "Express the minute constraint as a cron expression such as ` * * * *`." +"effect/Schedule#modifyDelayEffect": + replacement: "Schedule.modifyDelay" + note: "The v4 delay modifier is effectful by default and receives full Schedule.Metadata." +"effect/Schedule#once": + replacement: "Schedule.duration(Duration.zero)" + note: "A zero-duration schedule recurs once and then completes; map its Duration output to void if needed." +"effect/Schedule#onDecision": + replacement: "Schedule.tap" + note: "Use Schedule.tap for effects on recurrence metadata. To also observe final completion, wrap Schedule.toStep with Pull.matchEffect in Schedule.fromStep." +"effect/Schedule#provideContext": + replacement: "Schedule.fromStep + Effect.provide" + note: "Provide the Context to both Schedule.toStep acquisition and each Effect returned by the acquired step." +"effect/Schedule#provideService": + replacement: "Schedule.fromStep + Effect.provideService" + note: "Provide the service to both Schedule.toStep acquisition and each Effect returned by the acquired step." +"effect/Schedule#recurUntil": + replacement: "Schedule.identity + Schedule.while" + note: "Continue while the predicate over metadata.input is false." +"effect/Schedule#recurUntilEffect": + replacement: "Schedule.identity + Schedule.while" + note: "Continue while the effectful predicate over metadata.input is false." +"effect/Schedule#recurUntilOption": + replacement: "Schedule.fromStep" + note: "Use a custom step to evaluate the Option-producing function, emit Option.none while recurring, and terminate with the first Option.some result." +"effect/Schedule#recurUpTo": + replacement: "Schedule.during" + note: "Use the duration-bounded schedule constructor." +"effect/Schedule#recurWhile": + replacement: "Schedule.identity + Schedule.while" + note: "Continue while the predicate over metadata.input is true." +"effect/Schedule#recurWhileEffect": + replacement: "Schedule.identity + Schedule.while" + note: "Continue while the effectful predicate over metadata.input is true." +"effect/Schedule#reduce": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#reduceEffect": + replacement: "none" + note: "This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure." +"effect/Schedule#repeatForever": + replacement: "Schedule.forever" + note: "The infinite zero-delay counter schedule was renamed." +"effect/Schedule#repetitions": + replacement: "Schedule.map" + note: "Map metadata.attempt to the required recurrence count, adjusting by one where the v3 zero-based value is expected." +"effect/Schedule#resetAfter": + replacement: "none" + note: "Automatic schedule reset was removed. Wrap Schedule.toStep(self) with Schedule.fromStep and reacquire the inner step when the reset condition is met." +"effect/Schedule#resetWhen": + replacement: "none" + note: "Automatic schedule reset was removed. Wrap Schedule.toStep(self) with Schedule.fromStep and reacquire the inner step when the reset condition is met." +"effect/Schedule#run": + replacement: "Schedule.toStep" + note: "Acquire the step and traverse inputs manually, supplying each timestamp and collecting successful outputs until Cause.done." +"effect/Schedule#Schedule": + replacement: "Schedule.Schedule" + note: "The model remains but now has Schedule; its public initial/step fields were replaced by Schedule.toStep and fromStep." +"effect/Schedule#Schedule.DriverVariance": + replacement: "none" + note: "ScheduleDriver was removed in v4, so its variance marker has no replacement. Use the Schedule type parameters or the function returned by Schedule.toStepWithSleep." +"effect/Schedule#Schedule.Variance": + replacement: "Schedule.Schedule.Variance" + note: "The variance marker remains and now tracks Output, Input, Error, and Env through the private Schedule TypeId." +"effect/Schedule#ScheduleDriver": + replacement: "Schedule.toStepWithSleep" + note: "ScheduleDriver was removed. The acquired step function provides manual next calls with automatic sleeping; Schedule.toStep exposes raw delays." +"effect/Schedule#ScheduleDriverTypeId": + replacement: "none" + note: "ScheduleDriver and its public type id were removed. Use the step function returned by Schedule.toStepWithSleep." +"effect/Schedule#ScheduleTypeId": + replacement: "none" + note: "The Schedule type id is private in v4. Use Schedule.isSchedule to narrow unknown values." +"effect/Schedule#secondOfMinute": + replacement: "Schedule.cron" + note: "Use the six-field cron form to express a seconds constraint, for example ` * * * * *`." +"effect/Schedule#stop": + replacement: "Schedule.fromStep" + note: "Create a step that immediately returns Cause.done(undefined)." +"effect/Schedule#succeed": + replacement: "Schedule.forever + Schedule.map" + note: "Map every recurrence to the constant value." +"effect/Schedule#sync": + replacement: "Schedule.forever + Schedule.map" + note: "Map every recurrence by lazily evaluating the thunk." +"effect/Schedule#tapInput": + replacement: "Schedule.tap" + note: "Use the unified tap callback and read metadata.input." +"effect/Schedule#tapOutput": + replacement: "Schedule.tap" + note: "Use the unified tap callback and read metadata.output." +"effect/Schedule#unfold": + replacement: "Schedule.fromStep" + note: "Keep the evolving value inside the acquired step closure and emit each value with the desired Duration." +"effect/Schedule#union": + replacement: "Schedule.min" + note: "Use Schedule.min for fastest-delay composition. It outputs the selected Duration rather than both schedule outputs." +"effect/Schedule#unionWith": + replacement: "Schedule.min" + note: "Schedule.min covers the standard union behavior; a custom interval merge requires Schedule.fromStep." +"effect/Schedule#untilInput": + replacement: "Schedule.while" + note: "Continue while the predicate over metadata.input is false." +"effect/Schedule#untilInputEffect": + replacement: "Schedule.while" + note: "Continue while the effectful predicate over metadata.input is false." +"effect/Schedule#untilOutput": + replacement: "Schedule.while" + note: "Continue while the predicate over metadata.output is false." +"effect/Schedule#untilOutputEffect": + replacement: "Schedule.while" + note: "Continue while the effectful predicate over metadata.output is false." +"effect/Schedule#whileInput": + replacement: "Schedule.while" + note: "Continue while the predicate over metadata.input is true." +"effect/Schedule#whileInputEffect": + replacement: "Schedule.while" + note: "Continue while the effectful predicate over metadata.input is true." +"effect/Schedule#whileOutput": + replacement: "Schedule.while" + note: "Continue while the predicate over metadata.output is true." +"effect/Schedule#whileOutputEffect": + replacement: "Schedule.while" + note: "Continue while the effectful predicate over metadata.output is true." +"effect/Schedule#zipLeft": + replacement: "none" + note: "There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done." +"effect/Schedule#zipRight": + replacement: "none" + note: "There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done." +"effect/Schedule#zipWith": + replacement: "none" + note: "There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done." diff --git a/.context/effect/migration/annotations/effect__ScheduleDecision.yaml b/.context/effect/migration/annotations/effect__ScheduleDecision.yaml new file mode 100644 index 000000000..bce4dad16 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ScheduleDecision.yaml @@ -0,0 +1,18 @@ +"effect/ScheduleDecision#continue": + replacement: "Effect.succeed([output, duration])" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output)." +"effect/ScheduleDecision#continueWith": + replacement: "Effect.succeed([output, duration])" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output)." +"effect/ScheduleDecision#Done": + replacement: "Cause.Done" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output)." +"effect/ScheduleDecision#isContinue": + replacement: "none" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). Branch on the Pull result instead of inspecting a decision value." +"effect/ScheduleDecision#isDone": + replacement: "Cause.isDone" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output)." +"effect/ScheduleDecision#ScheduleDecision": + replacement: "none" + note: "ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output)." diff --git a/.context/effect/migration/annotations/effect__ScheduleInterval.yaml b/.context/effect/migration/annotations/effect__ScheduleInterval.yaml new file mode 100644 index 000000000..2b9a211ba --- /dev/null +++ b/.context/effect/migration/annotations/effect__ScheduleInterval.yaml @@ -0,0 +1,3 @@ +effect/ScheduleInterval: + replacement: none + note: The public ScheduleInterval module was removed in v4. Schedule steps now express only a relative Duration; combine policies with Schedule.max or Schedule.min, or implement custom timing with Schedule.fromStep. diff --git a/.context/effect/migration/annotations/effect__ScheduleIntervals.yaml b/.context/effect/migration/annotations/effect__ScheduleIntervals.yaml new file mode 100644 index 000000000..69985bca3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ScheduleIntervals.yaml @@ -0,0 +1,3 @@ +effect/ScheduleIntervals: + replacement: none + note: The public ScheduleIntervals module was removed in v4 along with absolute interval-set decisions. Use relative Duration values in Schedule.fromStep and Schedule.max or Schedule.min for standard policy composition. diff --git a/.context/effect/migration/annotations/effect__Scheduler.yaml b/.context/effect/migration/annotations/effect__Scheduler.yaml new file mode 100644 index 000000000..06e3b0191 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Scheduler.yaml @@ -0,0 +1,42 @@ +effect/Scheduler#ControlledScheduler: + replacement: "none" + note: "No public step-controlled scheduler remains; implement Scheduler and SchedulerDispatcher for exact controlled stepping." +effect/Scheduler#defaultScheduler: + replacement: "Scheduler.Scheduler" + note: "The default scheduler is now a Context.Reference; yield it to read or provide it to override the current scheduler." +effect/Scheduler#defaultShouldYield: + replacement: "Scheduler.MixedScheduler#shouldYield" + note: "The standalone function was removed; yielding is implemented by each Scheduler instance." +effect/Scheduler#make: + replacement: "none" + note: "Implement the redesigned Scheduler interface and return task dispatch through makeDispatcher." +effect/Scheduler#makeBatched: + replacement: "new Scheduler.MixedScheduler(\"async\", schedule)" + note: "Pass a cancellable scheduling function; the dispatcher performs priority batching." +effect/Scheduler#makeMatrix: + replacement: "none" + note: "Matrix routing was removed; implement routing in a custom Scheduler and SchedulerDispatcher if still required." +effect/Scheduler#MixedScheduler: + replacement: "Scheduler.MixedScheduler" + note: "The class remains with a redesigned constructor and makeDispatcher-based task API." +effect/Scheduler#PriorityBuckets: + replacement: "none" + note: "Priority buckets are now an internal Scheduler implementation detail." +effect/Scheduler#Scheduler: + replacement: "Scheduler.Scheduler" + note: "The interface remains but dispatch moved to SchedulerDispatcher returned by makeDispatcher." +effect/Scheduler#SchedulerRunner: + replacement: "Scheduler.SchedulerDispatcher" + note: "Task scheduling and flushing moved to the dispatcher returned by Scheduler.makeDispatcher." +effect/Scheduler#SyncScheduler: + replacement: "new Scheduler.MixedScheduler(\"sync\")" + note: "Use a synchronous MixedScheduler and its dispatcher; call flush when directly driving queued tasks." +effect/Scheduler#Task: + replacement: "() => void" + note: "The named alias was removed; dispatcher APIs inline the task callback type." +effect/Scheduler#timer: + replacement: "Effect.delay" + note: "Use Effect delay or sleep for effect timing; implement a custom dispatcher for exact per-task scheduler timing." +effect/Scheduler#timerBatched: + replacement: "new Scheduler.MixedScheduler(\"async\", scheduleWithTimer)" + note: "Use a setTimeout-based cancellable scheduling function; the dispatcher batches queued tasks." diff --git a/.context/effect/migration/annotations/effect__Schema.yaml b/.context/effect/migration/annotations/effect__Schema.yaml new file mode 100644 index 000000000..69c83c30e --- /dev/null +++ b/.context/effect/migration/annotations/effect__Schema.yaml @@ -0,0 +1,1362 @@ +effect/Schema#Annotable: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Annotable.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Annotable.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Annotable.Self: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#AnnotableClass: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#AnnotableDeclare: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#annotations: + replacement: Schema.annotate + note: Rename `annotations` to `annotate`. +effect/Schema#Annotations: + replacement: Schema.Annotations + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Annotations.Doc: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Annotations.Filter: + replacement: Schema.Annotations.Filter + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Annotations.GenericSchema: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Annotations.Schema: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Any: + replacement: Schema.Any + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Array$: + replacement: Schema.$Array + note: Use the renamed v4 constructor result interface. +effect/Schema#ArrayEnsure: + replacement: Schema.ArrayEnsure + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#ArrayFormatterIssue: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#asSchema: + replacement: Schema.revealCodec + note: Use `revealCodec` to expose a schema's codec type. +effect/Schema#asSerializable: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#asSerializableWithResult: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#asWithResult: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#attachPropertySignature: + replacement: Schema.tagDefaultOmit + note: "Map the struct fields and add `key: Schema.tagDefaultOmit(value)`; the old combinator was removed." +effect/Schema#between: + replacement: Schema.isBetween + note: Rename the predicate to `isBetween` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#betweenBigDecimal: + replacement: Schema.isBetweenBigDecimal + note: Rename the predicate to `isBetweenBigDecimal` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#BetweenBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#betweenBigInt: + replacement: Schema.isBetweenBigInt + note: Rename the predicate to `isBetweenBigInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#BetweenBigIntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#betweenDate: + replacement: Schema.isBetweenDate + note: Rename the predicate to `isBetweenDate` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#BetweenDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#betweenDuration: + replacement: Schema.isBetween + note: Rename the predicate to `isBetween` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#BetweenDurationSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#BetweenSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#BigDecimal: + replacement: Schema.BigDecimalFromString + note: Use the string-to-BigDecimal codec; v4 `BigDecimal` is the self schema. +effect/Schema#BigDecimalFromNumber: + replacement: none + note: No built-in number-to-BigDecimal codec remains; compose `decodeTo` with a `SchemaGetter` conversion. +effect/Schema#BigDecimalFromSelf: + replacement: Schema.BigDecimal + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#BigInt: + replacement: Schema.BigIntFromString + note: Use the string-to-bigint codec; v4 `BigInt` is the self schema. +effect/Schema#BigIntFromNumber: + replacement: none + note: No built-in number-to-bigint codec remains; compose `decodeTo` with a checked `SchemaGetter` conversion. +effect/Schema#BigIntFromSelf: + replacement: Schema.BigInt + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#Boolean: + replacement: Schema.Boolean + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#BooleanFromString: + replacement: none + note: No built-in string-to-boolean codec remains; use `decodeTo` with an explicit `SchemaGetter` transformation. +effect/Schema#BooleanFromUnknown: + replacement: Schema.Boolean + note: Use the boolean schema and perform any coercion explicitly before decoding. +effect/Schema#brand: + replacement: Schema.brand + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#BrandSchema: + replacement: Schema.brand + note: Use the schema returned by the v4 `brand` combinator and infer its concrete type. +effect/Schema#BrandSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Capitalize: + replacement: Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isCapitalized()), SchemaTransformation.capitalize())) + note: Rebuild the capitalization transformation with `decodeTo`. +effect/Schema#capitalized: + replacement: Schema.isCapitalized + note: Rename the string predicate to `isCapitalized` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Capitalized: + replacement: Schema.String.check(Schema.isCapitalized()) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#CapitalizedSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Cause: + replacement: Schema.toCodecJson(Schema.Cause(error, defect)) + note: Use the derived JSON codec to preserve v3's encoded Cause representation; v4 `Cause` itself is the self schema. +effect/Schema#CauseEncoded: + replacement: Schema.CauseIso + note: Use the v4 Cause JSON/iso representation type. +effect/Schema#CauseFromSelf: + replacement: Schema.Cause + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#Char: + replacement: Schema.Char + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Chunk: + replacement: Schema.toCodecJson(Schema.Chunk(value)) + note: Use the derived JSON codec to preserve v3's array-to-Chunk behavior; v4 `Chunk` itself is the self schema. +effect/Schema#ChunkFromSelf: + replacement: Schema.Chunk + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#clamp: + replacement: Schema.decodeTo + SchemaGetter.transform(Number.clamp(...)) + note: Rebuild clamping as an explicit reversible transformation. +effect/Schema#clampBigDecimal: + replacement: Schema.decodeTo + SchemaGetter.transform(BigDecimal.clamp(...)) + note: Rebuild BigDecimal clamping as an explicit reversible transformation. +effect/Schema#clampBigInt: + replacement: Schema.decodeTo + SchemaGetter.transform(BigInt.clamp(...)) + note: Rebuild bigint clamping as an explicit reversible transformation. +effect/Schema#clampDuration: + replacement: Schema.decodeTo + SchemaGetter.transform(Duration.clamp(...)) + note: Rebuild Duration clamping as an explicit reversible transformation. +effect/Schema#Class: + replacement: Schema.Class + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Config: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#Data: + replacement: none + note: Remove this wrapper. v4 structural equality works on ordinary decoded objects. +effect/Schema#DataFromSelf: + replacement: none + note: Remove this wrapper. v4 structural equality works on ordinary decoded objects. +effect/Schema#Date: + replacement: Schema.DateFromString + note: Use `DateFromString`; v4 `Date` is the self schema. +effect/Schema#DateFromNumber: + replacement: Schema.DateFromMillis + note: Rename the milliseconds-to-Date codec. +effect/Schema#DateFromSelf: + replacement: Schema.Date + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#DateFromSelfSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#DateFromString: + replacement: Schema.DateFromString + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#DateTimeUtc: + replacement: Schema.DateTimeUtcFromString + note: Use the string codec; v4 `DateTimeUtc` is the self schema. +effect/Schema#DateTimeUtcFromDate: + replacement: Schema.DateTimeUtcFromDate + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#DateTimeUtcFromNumber: + replacement: Schema.DateTimeUtcFromMillis + note: Rename the milliseconds-to-DateTime codec. +effect/Schema#DateTimeUtcFromSelf: + replacement: Schema.DateTimeUtc + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#DateTimeZoned: + replacement: Schema.DateTimeZonedFromString + note: Use the string codec; v4 `DateTimeZoned` is the self schema. +effect/Schema#DateTimeZonedFromSelf: + replacement: Schema.DateTimeZoned + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#declare: + replacement: Schema.declare + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#decode: + replacement: Schema.decodeEffect + note: Rename the effectful decoder. +effect/Schema#decodeEither: + replacement: Schema.decodeExit + note: Rename the decoder returning an `Exit`. +effect/Schema#decodeUnknown: + replacement: Schema.decodeUnknownEffect + note: Rename the effectful unknown-input decoder. +effect/Schema#decodeUnknownEither: + replacement: Schema.decodeUnknownExit + note: Rename the unknown-input decoder returning an `Exit`. +effect/Schema#decodeUnknownPromise: + replacement: Schema.decodeUnknownPromise + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Defect: + replacement: Schema.Defect + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#deserialize: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#deserializeExit: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#deserializeFailure: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#deserializeSuccess: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#Duration: + replacement: Schema.DurationFromString + note: Use the string codec; v4 `Duration` is the self schema. +effect/Schema#DurationEncoded: + replacement: Schema.Duration["Iso"] + note: Use the v4 Duration iso representation type. +effect/Schema#DurationFromMillis: + replacement: Schema.DurationFromMillis + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#DurationFromNanos: + replacement: Schema.DurationFromNanos + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#DurationFromSelf: + replacement: Schema.Duration + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#Either: + replacement: Schema.Result + note: "`Either` was renamed to `Result`; pass success and failure schemas positionally." +effect/Schema#EitherEncoded: + replacement: Schema.ResultIso + note: Use the v4 Result iso representation type. +effect/Schema#EitherFromSelf: + replacement: Schema.Result + note: "`Either` was renamed to `Result` in v4." +effect/Schema#EitherFromUnion: + replacement: Schema.Result + note: "`Either` was renamed to `Result`; use its tagged Result representation." +effect/Schema#element: + replacement: none + note: The tuple element wrapper was removed; express elements directly in `Tuple([...])` or use `TupleWithRest` for rest elements. +effect/Schema#Element: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Element.Token: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#encode: + replacement: Schema.encodeEffect + note: Rename the effectful encoder. +effect/Schema#encodedBoundSchema: + replacement: Schema.toEncoded + note: Use the encoded side of the codec; service bounds are modeled by v4 codec service types. +effect/Schema#encodedSchema: + replacement: Schema.toEncoded + note: Rename the encoded-side projection. +effect/Schema#encodeEither: + replacement: Schema.encodeExit + note: Rename the encoder returning an `Exit`. +effect/Schema#encodeUnknown: + replacement: Schema.encodeUnknownEffect + note: Rename the effectful unknown-input encoder. +effect/Schema#encodeUnknownEither: + replacement: Schema.encodeUnknownExit + note: Rename the unknown-input encoder returning an `Exit`. +effect/Schema#encodeUnknownPromise: + replacement: Schema.encodeUnknownPromise + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#endsWith: + replacement: Schema.isEndsWith + note: Rename the string predicate to `isEndsWith` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#EndsWithSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Enums: + replacement: Schema.Enum + note: Rename the enum constructor and pass the enum object. +effect/Schema#EnumsDefinition: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#equivalence: + replacement: Schema.toEquivalence + note: Rename the equivalence derivation utility. +effect/Schema#Exit: + replacement: Schema.toCodecJson(Schema.Exit(value, error, defect)) + note: Use the derived JSON codec to preserve v3's encoded Exit representation; v4 `Exit` itself is the self schema. +effect/Schema#ExitEncoded: + replacement: Schema.ExitIso + note: Use the v4 Exit iso representation type. +effect/Schema#ExitFromSelf: + replacement: Schema.Exit + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#exitSchema: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#extend: + replacement: schema.mapFields(Struct.assign(fields)) + note: Replace struct extension with `mapFields(Struct.assign(...))` or `Schema.fieldsAssign`; map union members explicitly. +effect/Schema#failureSchema: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#FiberId: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#FiberIdEncoded: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#FiberIdFromSelf: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#filter: + replacement: Schema.check(Schema.makeFilter(predicate)) / Schema.refine(refinement) + note: Use `check(makeFilter(...))` for predicates and `refine` for type refinements. +effect/Schema#filterEffect: + replacement: "Schema.decode({ decode: SchemaGetter.checkEffect(...), encode: SchemaGetter.passthrough() })" + note: Rebuild effectful validation as a decode step with `SchemaGetter.checkEffect`. +effect/Schema#FilterIssue: + replacement: Schema.FilterIssue + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#FilterOutput: + replacement: Schema.FilterOutput + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#finite: + replacement: Schema.isFinite + note: Rename the predicate to `isFinite` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Finite: + replacement: Schema.Finite + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#FiniteSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#format: + replacement: SchemaRepresentation.toCodeDocument + note: Build a representation with `SchemaRepresentation.toRepresentation`, `toMultiDocument`, then `toCodeDocument`. +effect/Schema#fromBrand: + replacement: Schema.fromBrand + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#fromKey: + replacement: Schema.encodeKeys + note: Use `encodeKeys` to map decoded property names to encoded keys. +effect/Schema#FromPropertySignature: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#getNumberIndexedAccess: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#greaterThan: + replacement: Schema.isGreaterThan + note: Rename the predicate to `isGreaterThan` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#greaterThanBigDecimal: + replacement: Schema.isGreaterThanBigDecimal + note: Rename the predicate to `isGreaterThanBigDecimal` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanBigInt: + replacement: Schema.isGreaterThanBigInt + note: Rename the predicate to `isGreaterThanBigInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanBigIntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanDate: + replacement: Schema.isGreaterThanDate + note: Rename the predicate to `isGreaterThanDate` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanDuration: + replacement: Schema.isGreaterThan + note: Rename the predicate to `isGreaterThan` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanDurationSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanOrEqualTo: + replacement: Schema.isGreaterThanOrEqualTo + note: Rename the predicate to `isGreaterThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#greaterThanOrEqualToBigDecimal: + replacement: Schema.isGreaterThanOrEqualToBigDecimal + note: Rename the predicate to `isGreaterThanOrEqualToBigDecimal` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanOrEqualToBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanOrEqualToBigInt: + replacement: Schema.isGreaterThanOrEqualToBigInt + note: Rename the predicate to `isGreaterThanOrEqualToBigInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanOrEqualToBigIntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanOrEqualToDate: + replacement: Schema.isGreaterThanOrEqualToDate + note: Rename the predicate to `isGreaterThanOrEqualToDate` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanOrEqualToDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#greaterThanOrEqualToDuration: + replacement: Schema.isGreaterThanOrEqualTo + note: Rename the predicate to `isGreaterThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#GreaterThanOrEqualToDurationSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#GreaterThanOrEqualToSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#GreaterThanSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#HashMap: + replacement: Schema.toCodecJson(Schema.HashMap(key, value)) + note: Pass key and value positionally and use the derived JSON codec to preserve v3's entry-array encoding. +effect/Schema#HashMapFromSelf: + replacement: Schema.HashMap + note: The self schema dropped the `FromSelf` suffix; pass key and value positionally. +effect/Schema#HashSet: + replacement: Schema.toCodecJson(Schema.HashSet(value)) + note: Use the derived JSON codec to preserve v3's array-to-HashSet behavior. +effect/Schema#HashSetFromSelf: + replacement: Schema.HashSet + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#head: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#headNonEmpty: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#headOrElse: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#includes: + replacement: Schema.isIncludes + note: Rename the string predicate to `isIncludes` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#IncludesSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#IndexSignature: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#IndexSignature.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#IndexSignature.Encoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#IndexSignature.NonEmptyRecords: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#IndexSignature.Record: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#IndexSignature.Type: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#instanceOf: + replacement: Schema.instanceOf + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#InstanceOfSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#int: + replacement: Schema.isInt + note: Rename the predicate to `isInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Int: + replacement: Schema.Int + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#IntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#isPropertySignature: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#isSchema: + replacement: Schema.isSchema + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#itemsCount: + replacement: Schema.isLengthBetween + note: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#ItemsCountSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#JsonNumber: + replacement: Schema.Finite + note: Use the finite-number schema for JSON-compatible numbers. +effect/Schema#JsonNumberSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#keyof: + replacement: none + note: Removed with the schema model rewrite; derive keys from struct fields or use `Schema.Literals` explicitly. +effect/Schema#LeftEncoded: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#length: + replacement: Schema.isLengthBetween + note: Use `isLengthBetween` with equal minimum and maximum values for an exact string length. +effect/Schema#LengthSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThan: + replacement: Schema.isLessThan + note: Rename the predicate to `isLessThan` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#lessThanBigDecimal: + replacement: Schema.isLessThanBigDecimal + note: Rename the predicate to `isLessThanBigDecimal` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanBigInt: + replacement: Schema.isLessThanBigInt + note: Rename the predicate to `isLessThanBigInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanBigIntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanDate: + replacement: Schema.isLessThanDate + note: Rename the predicate to `isLessThanDate` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanDuration: + replacement: Schema.isLessThan + note: Rename the predicate to `isLessThan` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanDurationSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanOrEqualTo: + replacement: Schema.isLessThanOrEqualTo + note: Rename the predicate to `isLessThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#lessThanOrEqualToBigDecimal: + replacement: Schema.isLessThanOrEqualToBigDecimal + note: Rename the predicate to `isLessThanOrEqualToBigDecimal` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanOrEqualToBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanOrEqualToBigInt: + replacement: Schema.isLessThanOrEqualToBigInt + note: Rename the predicate to `isLessThanOrEqualToBigInt` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanOrEqualToBigIntSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanOrEqualToDate: + replacement: Schema.isLessThanOrEqualToDate + note: Rename the predicate to `isLessThanOrEqualToDate` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanOrEqualToDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#lessThanOrEqualToDuration: + replacement: Schema.isLessThanOrEqualTo + note: Rename the predicate to `isLessThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#LessThanOrEqualToDurationSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#LessThanOrEqualToSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#LessThanSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#List: + replacement: none + note: The List schema was removed; migrate the model to `Schema.Array` or declare a custom List codec. +effect/Schema#ListFromSelf: + replacement: none + note: The List self schema was removed; migrate to arrays or use `Schema.declare` for List values. +effect/Schema#Literal: + replacement: Schema.Literal / Schema.Literals + note: Use `Literal(value)` for one non-null literal, `Null` for null, and `Literals([...])` for several literals. +effect/Schema#Lowercase: + replacement: Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isLowercased()), SchemaTransformation.toLowerCase())) + note: Rebuild the lowercase transformation with `decodeTo`. +effect/Schema#lowercased: + replacement: Schema.isLowercased + note: Rename the string predicate to `isLowercased` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Lowercased: + replacement: Schema.String.check(Schema.isLowercased()) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#LowercasedSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#make: + replacement: Schema.make + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#MakeOptions: + replacement: Schema.MakeOptions + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#makePropertySignature: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#Map: + replacement: Schema.toCodecJson(Schema.ReadonlyMap(key, value)) + note: Use `ReadonlyMap` with positional arguments and derive its JSON codec; mutable Map-specific schema types were removed. +effect/Schema#Map$: + replacement: Schema.$ReadonlyMap + note: Use the renamed v4 constructor result interface. +effect/Schema#MapFromRecord: + replacement: none + note: No direct record-to-Map codec remains; compose `Record` and `ReadonlyMap` with an explicit `decodeTo` transformation. +effect/Schema#MapFromSelf: + replacement: Schema.ReadonlyMap + note: Use the readonly Map self schema with positional key and value arguments. +effect/Schema#maxItems: + replacement: Schema.isMaxLength + note: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#MaxItemsSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#maxLength: + replacement: Schema.isMaxLength + note: Rename the string predicate to `isMaxLength` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#MaxLengthSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#minItems: + replacement: Schema.isMinLength + note: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#MinItemsSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#minLength: + replacement: Schema.isMinLength + note: Rename the string predicate to `isMinLength` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#MinLengthSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#multipleOf: + replacement: Schema.isMultipleOf + note: Rename the predicate to `isMultipleOf` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#MultipleOfSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#mutable: + replacement: Schema.mutable + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#negative: + replacement: Schema.isLessThan(0) + note: Use `isLessThan(0)` as a v4 check. +effect/Schema#Negative: + replacement: Schema.Number.check(Schema.isLessThan(0)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#negativeBigDecimal: + replacement: Schema.isLessThanBigDecimal(BigDecimal.fromNumber(0)) + note: Use `isLessThanBigDecimal` as a v4 check. +effect/Schema#NegativeBigDecimalFromSelf: + replacement: Schema.BigDecimal.check(Schema.isLessThanBigDecimal(BigDecimal.fromNumber(0))) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NegativeBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#negativeBigInt: + replacement: Schema.isLessThanBigInt(0n) + note: Use `isLessThanBigInt(0n)` as a v4 check. +effect/Schema#NegativeBigInt: + replacement: Schema.BigIntFromString.check(Schema.isLessThanBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NegativeBigIntFromSelf: + replacement: Schema.BigInt.check(Schema.isLessThanBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#Never: + replacement: Schema.Never + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NonEmptyArray: + replacement: Schema.NonEmptyArray + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NonEmptyArrayEnsure: + replacement: none + note: No direct replacement remains; explicitly decode a single value or array to `Schema.NonEmptyArray`. +effect/Schema#NonEmptyChunk: + replacement: Schema.toCodecJson(Schema.Chunk(value).check(Schema.isMinLength(1))) + note: Use a checked Chunk JSON codec. +effect/Schema#NonEmptyChunkFromSelf: + replacement: Schema.Chunk(value).check(Schema.isMinLength(1)) + note: Use the Chunk self schema with a minimum-length check. +effect/Schema#nonEmptyString: + replacement: Schema.isNonEmpty + note: Rename the string predicate to `isNonEmpty` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#NonEmptyString: + replacement: Schema.NonEmptyString + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NonEmptyTrimmedString: + replacement: Schema.Trimmed.check(Schema.isNonEmpty()) + note: Compose the trimmed schema with the non-empty check. +effect/Schema#nonNaN: + replacement: Schema.makeFilter((n) => !Number.isNaN(n)) + note: Use an explicit filter because v4 has no dedicated non-NaN check. +effect/Schema#NonNaN: + replacement: Schema.Number.check(Schema.makeFilter((n) => !Number.isNaN(n))) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonNaNSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#nonNegative: + replacement: Schema.isGreaterThanOrEqualTo(0) + note: Use `isGreaterThanOrEqualTo(0)` as a v4 check. +effect/Schema#NonNegative: + replacement: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#nonNegativeBigDecimal: + replacement: Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(0)) + note: Use `isGreaterThanOrEqualToBigDecimal` as a v4 check. +effect/Schema#NonNegativeBigDecimalFromSelf: + replacement: Schema.BigDecimal.check(Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(0))) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonNegativeBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#nonNegativeBigInt: + replacement: Schema.isGreaterThanOrEqualToBigInt(0n) + note: Use `isGreaterThanOrEqualToBigInt(0n)` as a v4 check. +effect/Schema#NonNegativeBigInt: + replacement: Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonNegativeBigIntFromSelf: + replacement: Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonNegativeInt: + replacement: Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#nonPositive: + replacement: Schema.isLessThanOrEqualTo(0) + note: Use `isLessThanOrEqualTo(0)` as a v4 check. +effect/Schema#NonPositive: + replacement: Schema.Number.check(Schema.isLessThanOrEqualTo(0)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#nonPositiveBigDecimal: + replacement: Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromNumber(0)) + note: Use `isLessThanOrEqualToBigDecimal` as a v4 check. +effect/Schema#NonPositiveBigDecimalFromSelf: + replacement: Schema.BigDecimal.check(Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromNumber(0))) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonPositiveBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#nonPositiveBigInt: + replacement: Schema.isLessThanOrEqualToBigInt(0n) + note: Use `isLessThanOrEqualToBigInt(0n)` as a v4 check. +effect/Schema#NonPositiveBigInt: + replacement: Schema.BigIntFromString.check(Schema.isLessThanOrEqualToBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#NonPositiveBigIntFromSelf: + replacement: Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#Not: + replacement: none + note: The exclusion constructor was removed; express the accepted alternatives directly or add a `Schema.check`. +effect/Schema#Null: + replacement: Schema.Null + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NullishOr: + replacement: Schema.NullishOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NullOr: + replacement: Schema.NullOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Number: + replacement: Schema.Number + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#NumberFromString: + replacement: Schema.NumberFromString + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Object: + replacement: Schema.ObjectKeyword + note: Rename the object keyword schema. +effect/Schema#omit: + replacement: schema.mapFields(Struct.omit([keys])) + note: Use `mapFields` with `Struct.omit`; pass keys as an array. +effect/Schema#Option: + replacement: Schema.toCodecJson(Schema.Option(value)) + note: Use the derived JSON codec to preserve v3's tagged Option encoding; v4 `Option` itself is the self schema. +effect/Schema#optional: + replacement: Schema.optional + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#optionalElement: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#OptionalOptions: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#optionalToOptional: + replacement: Schema.decodeTo + SchemaGetter.transformOptional + note: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. +effect/Schema#optionalToRequired: + replacement: Schema.decodeTo + SchemaGetter.transformOptional + note: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. +effect/Schema#optionalWith: + replacement: Schema.optional / Schema.optionalKey / Schema.withDecodingDefaultType + note: Choose `optional` or `optionalKey`; use the decoding-default helpers and an explicit nullable transformation as required by the old options. +effect/Schema#OptionEncoded: + replacement: Schema.OptionIso + note: Use the v4 Option iso representation type. +effect/Schema#OptionFromNonEmptyTrimmedString: + replacement: Schema.Trimmed.check(Schema.isNonEmpty()).pipe(Schema.decodeTo(Schema.Option(Schema.String), ...)) + note: Rebuild the empty-string-to-None conversion explicitly with `decodeTo`. +effect/Schema#OptionFromNullishOr: + replacement: Schema.OptionFromNullishOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#OptionFromNullOr: + replacement: Schema.OptionFromNullOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#OptionFromSelf: + replacement: Schema.Option + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#OptionFromUndefinedOr: + replacement: Schema.OptionFromUndefinedOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#parseJson: + replacement: Schema.UnknownFromJsonString / Schema.fromJsonString(schema) + note: Use `UnknownFromJsonString` without an inner schema or `fromJsonString(schema)` with one. +effect/Schema#ParseJsonOptions: + replacement: none + note: The old parse-json options type was removed; configure `fromJsonString` and its underlying getter directly. +effect/Schema#parseNumber: + replacement: Schema.NumberFromString + note: Use the built-in string-to-number codec. +effect/Schema#partial: + replacement: schema.mapFields(Struct.map(Schema.optional)) + note: Map struct fields with `Schema.optional`. +effect/Schema#partialWith: + replacement: schema.mapFields(Struct.map(Schema.optionalKey)) + note: "For `{ exact: true }`, map struct fields with `Schema.optionalKey`; choose field helpers explicitly for other options." +effect/Schema#pattern: + replacement: Schema.isPattern + note: Rename the string predicate to `isPattern` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#PatternSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#pick: + replacement: schema.mapFields(Struct.pick([keys])) + note: Use `mapFields` with `Struct.pick`; pass keys as an array. +effect/Schema#pickLiteral: + replacement: Schema.Literals(values).pick(selected) + note: Build a `Literals` schema from an array and call its `pick` method. +effect/Schema#pluck: + replacement: none + note: No direct replacement remains; pick the field then use `decodeTo` with `SchemaGetter.transform` to map between the field and enclosing object. +effect/Schema#positive: + replacement: Schema.isGreaterThan(0) + note: Use `isGreaterThan(0)` as a v4 check. +effect/Schema#Positive: + replacement: Schema.Number.check(Schema.isGreaterThan(0)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#positiveBigDecimal: + replacement: Schema.isGreaterThanBigDecimal(BigDecimal.fromNumber(0)) + note: Use `isGreaterThanBigDecimal` as a v4 check. +effect/Schema#PositiveBigDecimalFromSelf: + replacement: Schema.BigDecimal.check(Schema.isGreaterThanBigDecimal(BigDecimal.fromNumber(0))) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#PositiveBigDecimalSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#positiveBigInt: + replacement: Schema.isGreaterThanBigInt(0n) + note: Use `isGreaterThanBigInt(0n)` as a v4 check. +effect/Schema#PositiveBigInt: + replacement: Schema.BigIntFromString.check(Schema.isGreaterThanBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#PositiveBigIntFromSelf: + replacement: Schema.BigInt.check(Schema.isGreaterThanBigInt(0n)) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#PropertyKey: + replacement: Schema.PropertyKey + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#propertySignature: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#PropertySignature: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignature.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignature.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignature.AST: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignature.Token: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignatureDeclaration: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignatureTransformation: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#PropertySignatureTypeId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#ReadonlyMap: + replacement: Schema.toCodecJson(Schema.ReadonlyMap(key, value)) + note: Pass key and value positionally and use the derived JSON codec to preserve v3's entry-array encoding. +effect/Schema#ReadonlyMap$: + replacement: Schema.$ReadonlyMap + note: Use the renamed v4 constructor result interface. +effect/Schema#ReadonlyMapFromRecord: + replacement: none + note: No direct record-to-ReadonlyMap codec remains; compose `Record` and `ReadonlyMap` with an explicit `decodeTo` transformation. +effect/Schema#ReadonlyMapFromSelf: + replacement: Schema.ReadonlyMap + note: The self schema dropped the `FromSelf` suffix; pass key and value positionally. +effect/Schema#ReadonlySet: + replacement: Schema.toCodecJson(Schema.ReadonlySet(value)) + note: Use the derived JSON codec to preserve v3's array-to-ReadonlySet behavior. +effect/Schema#ReadonlySet$: + replacement: Schema.$ReadonlySet + note: Use the renamed v4 constructor result interface. +effect/Schema#ReadonlySetFromSelf: + replacement: Schema.ReadonlySet + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#Record: + replacement: Schema.Record(key, value) + note: Pass key and value as separate arguments. +effect/Schema#Record$: + replacement: Schema.$Record + note: Use the renamed v4 constructor result interface. +effect/Schema#Redacted: + replacement: Schema.RedactedFromValue + note: Use `RedactedFromValue` to wrap decoded raw values; v4 `Redacted` is the self schema. +effect/Schema#RedactedFromSelf: + replacement: Schema.Redacted + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#refine: + replacement: Schema.refine + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#RefineSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#rename: + replacement: Schema.encodeKeys + note: Use `encodeKeys` for encoded-key renaming. +effect/Schema#requiredToOptional: + replacement: Schema.decodeTo + SchemaGetter.transformOptional + note: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. +effect/Schema#RightEncoded: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#Schema: + replacement: Schema.Schema + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Schema.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.AnyNoContext: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.AsSchema: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.Encoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.ToAsserts: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Schema.Variance: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#SchemaClass: + replacement: Schema.Codec + note: The concrete SchemaClass abstraction was removed; accept the appropriate v4 `Codec` or constraint type. +effect/Schema#Serializable: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Serializable.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Serializable.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Serializable.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Serializable.Encoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Serializable.Type: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#serializableSchema: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#SerializableWithResult: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#SerializableWithResult.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#SerializableWithResult.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#SerializableWithResult.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#serialize: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#serializeExit: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#serializeFailure: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#serializeSuccess: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#Set: + replacement: Schema.toCodecJson(Schema.ReadonlySet(value)) + note: Use the readonly Set schema and derive its JSON codec; mutable Set-specific schema types were removed. +effect/Schema#Set$: + replacement: Schema.$ReadonlySet + note: Use the renamed v4 constructor result interface. +effect/Schema#SetFromSelf: + replacement: Schema.ReadonlySet + note: Use the readonly Set self schema. +effect/Schema#SimplifyMutable: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#SortedSet: + replacement: none + note: The SortedSet schema was removed; migrate to `ReadonlySet` or declare a custom codec that applies the required ordering. +effect/Schema#SortedSetFromSelf: + replacement: none + note: The SortedSet self schema was removed; use `Schema.declare` if SortedSet values must remain in the model. +effect/Schema#split: + replacement: Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.String), SchemaTransformation.transform(...))) + note: Rebuild splitting as an explicit reversible string/array transformation. +effect/Schema#standardSchemaV1: + replacement: Schema.toStandardSchemaV1 + note: Rename the Standard Schema adapter. +effect/Schema#startsWith: + replacement: Schema.isStartsWith + note: Rename the string predicate to `isStartsWith` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#StartsWithSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#String: + replacement: Schema.String + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Struct: + replacement: Schema.Struct + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Struct.Constructor: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.Encoded: + replacement: Schema.Struct.Encoded + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Struct.EncodedOptionalKeys: + replacement: Schema.Struct.EncodedOptionalKeys + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Struct.Field: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.Key: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.OptionalEncodedPropertySignature: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.OptionalTypePropertySignature: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#Struct.PropertySignatureWithDefault: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#successSchema: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#suspend: + replacement: Schema.suspend + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Symbol: + replacement: none + note: v4 `Symbol` is the self schema and has no built-in string-to-symbol codec; rebuild the conversion explicitly with `decodeTo`. +effect/Schema#SymbolFromSelf: + replacement: Schema.Symbol + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#symbolSerializable: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#symbolWithResult: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#tag: + replacement: Schema.tag + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#TaggedClass: + replacement: Schema.TaggedClass + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#TaggedError: + replacement: Schema.TaggedError + note: The constructor name is retained; update to the v4 fields-or-Struct signature and infer the resulting class types. +effect/Schema#TaggedErrorClass: + replacement: Schema.TaggedError + note: The exported helper interface was removed; use the class returned by Schema.TaggedError and infer its types. +effect/Schema#TaggedRequest: + replacement: effect/unstable/rpc/Rpc.make + note: The Schema request/serialization protocol was removed; migrate RPC requests to the v4 Rpc APIs. +effect/Schema#TaggedRequest.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TaggedRequest.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TaggedRequestClass: + replacement: effect/unstable/rpc/Rpc.make + note: The Schema request/serialization protocol was removed; migrate RPC requests to the v4 Rpc APIs. +effect/Schema#TaggedStruct: + replacement: Schema.TaggedStruct + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#TemplateLiteral: + replacement: Schema.TemplateLiteral(parts) + note: Pass template literal parts as one array. +effect/Schema#TemplateLiteralParser: + replacement: Schema.TemplateLiteralParser(schema.parts) + note: Create the template schema first and pass its `parts` property. +effect/Schema#TimeZone: + replacement: Schema.TimeZoneFromString + note: Use the string codec; v4 `TimeZone` is the self schema. +effect/Schema#TimeZoneFromSelf: + replacement: Schema.TimeZone + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#TimeZoneNamed: + replacement: Schema.TimeZoneNamedFromString + note: Use the string codec; v4 `TimeZoneNamed` is the self schema. +effect/Schema#TimeZoneNamedFromSelf: + replacement: Schema.TimeZoneNamed + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#TimeZoneOffset: + replacement: Schema.toCodecJson(Schema.TimeZoneOffset) + note: Use the derived JSON codec to preserve v3's encoded offset representation; v4 `TimeZoneOffset` is the self schema. +effect/Schema#TimeZoneOffsetFromSelf: + replacement: Schema.TimeZoneOffset + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#ToPropertySignature: + replacement: none + note: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. +effect/Schema#transform: + replacement: schema.pipe(Schema.decodeTo(target, SchemaTransformation.transform({ decode, encode }))) + note: Replace the constructor with `decodeTo` and a `SchemaTransformation`. +effect/Schema#transformLiteral: + replacement: Schema.Literal(from).transform(to) + note: Use the literal schema's `transform` method. +effect/Schema#transformLiterals: + replacement: Schema.Literals(fromValues).transform(toValues) + note: Split the pairs into parallel arrays and use `Literals(...).transform(...)`. +effect/Schema#transformOrFail: + replacement: "schema.pipe(Schema.decodeTo(target, { decode: SchemaGetter.transformOrFail(...), encode: ... }))" + note: Replace the constructor with `decodeTo` and fallible `SchemaGetter` transformations. +effect/Schema#Trim: + replacement: Schema.Trim + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#trimmed: + replacement: Schema.isTrimmed + note: Rename the string predicate to `isTrimmed` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Trimmed: + replacement: Schema.Trimmed + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#TrimmedSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Tuple: + replacement: Schema.Tuple(elements) + note: Pass tuple elements as one array. +effect/Schema#Tuple2: + replacement: Schema.Tuple + note: Use the array-based tuple constructor. +effect/Schema#TupleType.ElementsEncoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TupleType.ElementsType: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TupleType.Encoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TupleType.Type: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TypeId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#TypeLiteral: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TypeLiteral.Constructor: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TypeLiteral.Encoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#TypeLiteral.Type: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#typeSchema: + replacement: Schema.toType + note: Rename the type-side projection. +effect/Schema#Uint8: + replacement: "Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 255 }))" + note: Rebuild the unsigned-byte schema from integer and range checks. +effect/Schema#Uint8Array: + replacement: Schema.toCodecJson(Schema.Uint8Array) + note: Use the derived JSON codec to preserve v3's number-array encoding; v4 `Uint8Array` is the self schema. +effect/Schema#Uint8ArrayFromSelf: + replacement: Schema.Uint8Array + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#ULID: + replacement: Schema.String.check(Schema.isULID()) + note: Build the string schema with the ULID check. +effect/Schema#ULIDSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Uncapitalize: + replacement: Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isUncapitalized()), SchemaTransformation.uncapitalize())) + note: Rebuild the uncapitalization transformation with `decodeTo`. +effect/Schema#uncapitalized: + replacement: Schema.isUncapitalized + note: Rename the string predicate to `isUncapitalized` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Uncapitalized: + replacement: Schema.String.check(Schema.isUncapitalized()) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#UncapitalizedSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Undefined: + replacement: Schema.Undefined + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#UndefinedOr: + replacement: Schema.UndefinedOr + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Union: + replacement: Schema.Union(members) + note: Pass union members as one array. +effect/Schema#UniqueSymbolFromSelf: + replacement: Schema.UniqueSymbol + note: Use the v4 unique-symbol schema constructor. +effect/Schema#Unknown: + replacement: Schema.Unknown + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#Uppercase: + replacement: Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isUppercased()), SchemaTransformation.toUpperCase())) + note: Rebuild the uppercase transformation with `decodeTo`. +effect/Schema#uppercased: + replacement: Schema.isUppercased + note: Rename the string predicate to `isUppercased` and apply it with `Schema.check` or a schema's `check` method. +effect/Schema#Uppercased: + replacement: Schema.String.check(Schema.isUppercased()) + note: Rebuild the removed convenience schema from the v4 base schema and check APIs. +effect/Schema#UppercasedSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#URL: + replacement: Schema.URLFromString + note: Use the string-to-URL codec; v4 `URL` is the self schema. +effect/Schema#URLFromSelf: + replacement: Schema.URL + note: The self schema dropped the `FromSelf` suffix. +effect/Schema#UUID: + replacement: Schema.String.check(Schema.isUUID()) + note: Build the string schema with the UUID check. +effect/Schema#UUIDSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#validate: + replacement: Schema.decodeEffect(Schema.toType(schema)) + note: Validation was removed; decode through the schema's type side. +effect/Schema#validateEither: + replacement: Schema.decodeExit(Schema.toType(schema)) + note: Validation was removed; decode through the schema's type side. +effect/Schema#validatePromise: + replacement: Schema.decodePromise(Schema.toType(schema)) + note: Validation was removed; decode through the schema's type side. +effect/Schema#validDate: + replacement: Schema.Date + note: Use the v4 Date self schema, which rejects invalid Date values. +effect/Schema#ValidDateFromSelf: + replacement: Schema.Date + note: Use the v4 Date self schema, which rejects invalid Date values. +effect/Schema#ValidDateSchemaId: + replacement: none + note: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. +effect/Schema#Void: + replacement: Schema.Void + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#withConstructorDefault: + replacement: Schema.withConstructorDefault + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#withDecodingDefault: + replacement: Schema.withDecodingDefault + note: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. +effect/Schema#withDefaults: + replacement: none + note: Removed; choose `withConstructorDefault` and decoding-default helpers explicitly for each side. +effect/Schema#WithResult: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.All: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.Any: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.Context: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.Failure: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.FailureEncoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.Success: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. +effect/Schema#WithResult.SuccessEncoded: + replacement: none + note: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. diff --git a/.context/effect/migration/annotations/effect__SchemaAST.yaml b/.context/effect/migration/annotations/effect__SchemaAST.yaml new file mode 100644 index 000000000..00a8f970d --- /dev/null +++ b/.context/effect/migration/annotations/effect__SchemaAST.yaml @@ -0,0 +1,441 @@ +"effect/SchemaAST#Annotated": + replacement: "SchemaAST.Base" + note: "All v4 AST nodes extend Base, which owns annotations, checks, encoding, and context." +"effect/SchemaAST#annotations": + replacement: "SchemaAST.annotate" + note: "Use the v4 annotation helper and string-keyed Schema.Annotations." +"effect/SchemaAST#anyKeyword": + replacement: "SchemaAST.any" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#AnyKeyword": + replacement: "SchemaAST.Any" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#ArbitraryAnnotationId": + replacement: "Schema.Annotations.ToArbitrary" + note: "Symbol annotation IDs were removed; use the toArbitrary annotation key and its Schema.Annotations types." +"effect/SchemaAST#AST": + replacement: "SchemaAST.AST" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#BatchingAnnotation": + replacement: "none" + note: "Per-schema batching annotations were removed; control asynchronous parsing with ParseOptions.concurrency." +"effect/SchemaAST#BatchingAnnotationId": + replacement: "none" + note: "Symbol annotation IDs were removed and batching is no longer a schema annotation." +"effect/SchemaAST#bigIntKeyword": + replacement: "SchemaAST.bigInt" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#BigIntKeyword": + replacement: "SchemaAST.BigInt" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#booleanKeyword": + replacement: "SchemaAST.boolean" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#BooleanKeyword": + replacement: "SchemaAST.Boolean" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#BrandAnnotation": + replacement: "Schema.Annotations.Bottom[\"brands\"]" + note: "Brands are stored under the string-keyed brands annotation and normally added with Schema.brand." +"effect/SchemaAST#BrandAnnotationId": + replacement: "Schema.brand" + note: "Symbol annotation IDs were removed; add brands through Schema.brand." +"effect/SchemaAST#Compiler": + replacement: "none" + note: "The generic AST compiler abstraction was removed; traverse the discriminated SchemaAST.AST union directly or use a higher-level Schema derivation API." +"effect/SchemaAST#composeTransformation": + replacement: "SchemaAST.Encoding" + note: "V4 transformations are SchemaAST.Link values in an encoding chain; compose by adding links with SchemaAST.decodeTo." +"effect/SchemaAST#ComposeTransformation": + replacement: "SchemaAST.Encoding" + note: "The marker transformation was replaced by explicit SchemaAST.Link encoding chains." +"effect/SchemaAST#ConcurrencyAnnotation": + replacement: "SchemaAST.ParseOptions[\"concurrency\"]" + note: "Concurrency is now a parse option rather than its own annotation type." +"effect/SchemaAST#ConcurrencyAnnotationId": + replacement: "Schema.Annotations.Bottom[\"parseOptions\"]" + note: "Symbol annotation IDs were removed; put concurrency inside the parseOptions annotation." +"effect/SchemaAST#Declaration": + replacement: "SchemaAST.Declaration" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#DecodingFallbackAnnotation": + replacement: "Schema.catchDecoding" + note: "Fallbacks are now encoding middleware added with Schema.catchDecoding." +"effect/SchemaAST#DecodingFallbackAnnotationId": + replacement: "Schema.catchDecoding" + note: "The symbol annotation was removed; attach decoding recovery with Schema.catchDecoding." +"effect/SchemaAST#DefaultAnnotation": + replacement: "Schema.Annotations.Documentation[\"default\"]" + note: "Defaults are string-keyed schema annotations in v4." +"effect/SchemaAST#DefaultAnnotationId": + replacement: "Schema.Annotations.Documentation[\"default\"]" + note: "Symbol annotation IDs were removed; use the default key." +"effect/SchemaAST#defaultParseOption": + replacement: "SchemaAST.defaultParseOptions" + note: "The default parse options constant was pluralized." +"effect/SchemaAST#DescriptionAnnotation": + replacement: "Schema.Annotations.Augment[\"description\"]" + note: "Descriptions are string-keyed schema annotations in v4." +"effect/SchemaAST#DescriptionAnnotationId": + replacement: "Schema.Annotations.Augment[\"description\"]" + note: "Symbol annotation IDs were removed; use the description key." +"effect/SchemaAST#DocumentationAnnotation": + replacement: "Schema.Annotations.Augment[\"documentation\"]" + note: "Documentation is a string-keyed schema annotation in v4." +"effect/SchemaAST#DocumentationAnnotationId": + replacement: "Schema.Annotations.Augment[\"documentation\"]" + note: "Symbol annotation IDs were removed; use the documentation key." +"effect/SchemaAST#encodedAST": + replacement: "SchemaAST.toEncoded" + note: "The encoded projection helper was renamed." +"effect/SchemaAST#encodedBoundAST": + replacement: "SchemaAST.toEncoded" + note: "The separate encoded-bound projection was removed; use the encoded projection and v4 encoding links." +"effect/SchemaAST#Enums": + replacement: "SchemaAST.Enum" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#EquivalenceAnnotation": + replacement: "Schema.Annotations.ToEquivalence.Declaration" + note: "Equivalence derivation annotations now use the toEquivalence key in Schema.Annotations." +"effect/SchemaAST#EquivalenceAnnotationId": + replacement: "Schema.overrideToEquivalence" + note: "The symbol annotation was removed; attach custom equivalence derivation with Schema.overrideToEquivalence." +"effect/SchemaAST#ExamplesAnnotation": + replacement: "Schema.Annotations.Documentation[\"examples\"]" + note: "Examples are string-keyed schema annotations in v4." +"effect/SchemaAST#ExamplesAnnotationId": + replacement: "Schema.Annotations.Documentation[\"examples\"]" + note: "Symbol annotation IDs were removed; use the examples key." +"effect/SchemaAST#FinalTransformation": + replacement: "SchemaTransformation.Transformation" + note: "Transformations moved to SchemaTransformation and are stored in SchemaAST.Link values." +"effect/SchemaAST#getAnnotation": + replacement: "SchemaAST.resolveAt" + note: "Resolve string-keyed annotations with resolveAt, or use resolveIdentifier, resolveTitle, and resolveDescription." +"effect/SchemaAST#getBatchingAnnotation": + replacement: "none" + note: "Batching annotations were removed; read ParseOptions.concurrency when controlling asynchronous parsing." +"effect/SchemaAST#getBrandAnnotation": + replacement: "SchemaAST.resolveAt(\"brands\")" + note: "Resolve the string-keyed brands annotation." +"effect/SchemaAST#getCompiler": + replacement: "none" + note: "The Match-based compiler was removed; traverse SchemaAST.AST directly or use the relevant Schema derivation API." +"effect/SchemaAST#getConcurrencyAnnotation": + replacement: "SchemaAST.resolveAt(\"parseOptions\")" + note: "Resolve parseOptions and read concurrency from it." +"effect/SchemaAST#getDecodingFallbackAnnotation": + replacement: "none" + note: "Fallbacks are encoding middleware in v4, not readable annotations; attach them with Schema.catchDecoding." +"effect/SchemaAST#getDefaultAnnotation": + replacement: "SchemaAST.resolveAt(\"default\")" + note: "Resolve the string-keyed default annotation." +"effect/SchemaAST#getDescriptionAnnotation": + replacement: "SchemaAST.resolveDescription" + note: "Use the dedicated resolved-description helper." +"effect/SchemaAST#getDocumentationAnnotation": + replacement: "SchemaAST.resolveAt(\"documentation\")" + note: "Resolve the string-keyed documentation annotation." +"effect/SchemaAST#getExamplesAnnotation": + replacement: "SchemaAST.resolveAt(\"examples\")" + note: "Resolve the string-keyed examples annotation." +"effect/SchemaAST#getIdentifierAnnotation": + replacement: "SchemaAST.resolveIdentifier" + note: "Use the dedicated resolved-identifier helper." +"effect/SchemaAST#getJSONIdentifier": + replacement: "SchemaAST.resolveIdentifier" + note: "JSON Schema references now use the normal resolved identifier." +"effect/SchemaAST#getJSONIdentifierAnnotation": + replacement: "SchemaAST.resolveIdentifier" + note: "The separate JSON identifier annotation was removed; use identifier." +"effect/SchemaAST#getJSONSchemaAnnotation": + replacement: "SchemaAST.resolveAt(\"toJsonSchema\")" + note: "JSON Schema generation hooks use the string-keyed toJsonSchema annotation on checks." +"effect/SchemaAST#getMessageAnnotation": + replacement: "SchemaAST.resolveAt(\"message\")" + note: "Resolve the string-keyed message annotation." +"effect/SchemaAST#getMissingMessageAnnotation": + replacement: "SchemaAST.resolveAt(\"messageMissingKey\")" + note: "Missing-key messages use the messageMissingKey key." +"effect/SchemaAST#getParseIssueTitleAnnotation": + replacement: "none" + note: "Issue-title callbacks were removed; use message or expected annotations and SchemaIssue formatters." +"effect/SchemaAST#getParseOptionsAnnotation": + replacement: "SchemaAST.resolveAt(\"parseOptions\")" + note: "Resolve the string-keyed parseOptions annotation." +"effect/SchemaAST#getPropertySignatures": + replacement: "SchemaAST.Objects.propertySignatures" + note: "Narrow to Objects and read propertySignatures directly." +"effect/SchemaAST#getSchemaIdAnnotation": + replacement: "SchemaAST.resolveIdentifier" + note: "Schema IDs were consolidated into the identifier annotation." +"effect/SchemaAST#getSurrogateAnnotation": + replacement: "SchemaAST.resolveAt(\"representation\")" + note: "Surrogate AST annotations were replaced by representation annotations and declaration codec hooks." +"effect/SchemaAST#getTemplateLiteralCapturingRegExp": + replacement: "none" + note: "The low-level RegExp compiler was removed; use Schema.TemplateLiteral and schema parsing instead." +"effect/SchemaAST#getTemplateLiteralRegExp": + replacement: "none" + note: "The low-level RegExp compiler was removed; use Schema.TemplateLiteral and schema parsing instead." +"effect/SchemaAST#getTitleAnnotation": + replacement: "SchemaAST.resolveTitle" + note: "Use the dedicated resolved-title helper." +"effect/SchemaAST#getTypeConstructorAnnotation": + replacement: "SchemaAST.resolveAt(\"toCodec\")" + note: "Type-constructor behavior moved to declaration codec annotations." +"effect/SchemaAST#IdentifierAnnotation": + replacement: "Schema.Annotations.Bottom[\"identifier\"]" + note: "Identifiers are string-keyed schema annotations in v4." +"effect/SchemaAST#IdentifierAnnotationId": + replacement: "Schema.Annotations.Bottom[\"identifier\"]" + note: "Symbol annotation IDs were removed; use the identifier key." +"effect/SchemaAST#IndexSignature": + replacement: "SchemaAST.IndexSignature" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#isAnyKeyword": + replacement: "SchemaAST.isAny" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isBigIntKeyword": + replacement: "SchemaAST.isBigInt" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isBooleanKeyword": + replacement: "SchemaAST.isBoolean" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isComposeTransformation": + replacement: "none" + note: "Compose transformation markers were replaced by explicit SchemaAST.Link encoding chains." +"effect/SchemaAST#isEnums": + replacement: "SchemaAST.isEnum" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isFinalTransformation": + replacement: "SchemaTransformation.Transformation" + note: "Use SchemaTransformation guards or the transformation object stored on a SchemaAST.Link." +"effect/SchemaAST#isNeverKeyword": + replacement: "SchemaAST.isNever" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isNumberKeyword": + replacement: "SchemaAST.isNumber" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isParameter": + replacement: "SchemaAST.isString" + note: "The Parameter union was removed; inspect the v4 key AST variants directly." +"effect/SchemaAST#isRefinement": + replacement: "SchemaAST.Check" + note: "Refinement AST nodes became checks attached to Base.checks." +"effect/SchemaAST#isStringKeyword": + replacement: "SchemaAST.isString" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isSymbolKeyword": + replacement: "SchemaAST.isSymbol" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isTransformation": + replacement: "SchemaAST.Encoding" + note: "Transformation AST nodes became encoding links attached to Base.encoding." +"effect/SchemaAST#isTupleType": + replacement: "SchemaAST.isArrays" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isTypeLiteral": + replacement: "SchemaAST.isObjects" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isTypeLiteralTransformation": + replacement: "Schema.encodeKeys" + note: "Property-key transformations are represented by encoding links and normally built with Schema.encodeKeys." +"effect/SchemaAST#isUndefinedKeyword": + replacement: "SchemaAST.isUndefined" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isUnknownKeyword": + replacement: "SchemaAST.isUnknown" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#isVoidKeyword": + replacement: "SchemaAST.isVoid" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#JSONIdentifierAnnotationId": + replacement: "Schema.Annotations.Bottom[\"identifier\"]" + note: "The separate JSON identifier symbol was removed; use identifier." +"effect/SchemaAST#JSONSchemaAnnotation": + replacement: "JsonSchema.JsonSchema" + note: "JSON Schema values use the v4 JsonSchema model; generation hooks use Schema representation annotations." +"effect/SchemaAST#JSONSchemaAnnotationId": + replacement: "Schema.Annotations.Filter[\"toJsonSchema\"]" + note: "The symbol annotation was replaced by the toJsonSchema key on check annotations." +"effect/SchemaAST#keyof": + replacement: "none" + note: "Low-level SchemaAST.keyof was removed; model the desired key literals explicitly." +"effect/SchemaAST#Literal": + replacement: "SchemaAST.Literal" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#LiteralValue": + replacement: "SchemaAST.LiteralValue" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#Match": + replacement: "none" + note: "The Match compiler table was removed; traverse the discriminated SchemaAST.AST union directly." +"effect/SchemaAST#Members": + replacement: "ReadonlyArray" + note: "Union members are ordinary readonly arrays in v4." +"effect/SchemaAST#MessageAnnotation": + replacement: "Schema.Annotations.Bottom[\"message\"]" + note: "Messages are string-keyed annotations and no longer receive the old ParseIssue callback shape." +"effect/SchemaAST#MessageAnnotationId": + replacement: "Schema.Annotations.Bottom[\"message\"]" + note: "Symbol annotation IDs were removed; use the message key." +"effect/SchemaAST#MissingMessageAnnotation": + replacement: "Schema.Annotations.Key[\"messageMissingKey\"]" + note: "Missing-key messages use the messageMissingKey key." +"effect/SchemaAST#MissingMessageAnnotationId": + replacement: "Schema.Annotations.Key[\"messageMissingKey\"]" + note: "Symbol annotation IDs were removed; use messageMissingKey." +"effect/SchemaAST#mutable": + replacement: "Schema.mutable" + note: "Apply mutability at the Schema level; AST property mutability is represented by Context." +"effect/SchemaAST#neverKeyword": + replacement: "SchemaAST.never" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#NeverKeyword": + replacement: "SchemaAST.Never" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#null": + replacement: "SchemaAST.null" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#numberKeyword": + replacement: "SchemaAST.number" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#NumberKeyword": + replacement: "SchemaAST.Number" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#ObjectKeyword": + replacement: "SchemaAST.ObjectKeyword" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#omit": + replacement: "Schema.mapFields + Struct.omit" + note: "Object projection moved to schema field transforms." +"effect/SchemaAST#OptionalType": + replacement: "SchemaAST.Context" + note: "Element and property optionality moved into per-node Context." +"effect/SchemaAST#Parameter": + replacement: "SchemaAST.AST" + note: "The dedicated index-parameter union was removed; v4 validates supported key AST variants when building an IndexSignature." +"effect/SchemaAST#ParseIssueTitleAnnotation": + replacement: "none" + note: "Issue-title callbacks were removed; use message or expected annotations and SchemaIssue formatters." +"effect/SchemaAST#ParseIssueTitleAnnotationId": + replacement: "none" + note: "The symbol annotation was removed; use message or expected annotations." +"effect/SchemaAST#ParseJsonSchemaId": + replacement: "Schema.UnknownFromJsonString" + note: "Use the built-in JSON string codec instead of checking the old schema ID." +"effect/SchemaAST#ParseOptions": + replacement: "SchemaAST.ParseOptions" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#ParseOptionsAnnotationId": + replacement: "Schema.Annotations.Bottom[\"parseOptions\"]" + note: "Symbol annotation IDs were removed; use the parseOptions key." +"effect/SchemaAST#partial": + replacement: "Schema.mapFields + Struct.map(Schema.optional)" + note: "Partial object transforms moved to schema field transforms." +"effect/SchemaAST#PrettyAnnotationId": + replacement: "Schema.overrideToFormatter" + note: "The symbol annotation was removed; attach custom formatters with Schema.overrideToFormatter." +"effect/SchemaAST#PropertySignature": + replacement: "SchemaAST.PropertySignature" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#PropertySignatureTransformation": + replacement: "Schema.encodeKeys" + note: "Property-key transformations are now encoding links, normally built with Schema.encodeKeys." +"effect/SchemaAST#Refinement": + replacement: "SchemaAST.Check" + note: "Refinements became Filter or FilterGroup checks attached to an AST node." +"effect/SchemaAST#required": + replacement: "Schema.mapFields + Struct.map(Schema.requiredKey)" + note: "Required object transforms moved to schema field transforms." +"effect/SchemaAST#SchemaIdAnnotation": + replacement: "Schema.Annotations.Bottom[\"identifier\"]" + note: "Schema IDs were consolidated into identifier annotations." +"effect/SchemaAST#SchemaIdAnnotationId": + replacement: "Schema.Annotations.Bottom[\"identifier\"]" + note: "Symbol annotation IDs were removed; use identifier." +"effect/SchemaAST#stringKeyword": + replacement: "SchemaAST.string" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#StringKeyword": + replacement: "SchemaAST.String" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#SurrogateAnnotation": + replacement: "SchemaRepresentation.RepresentationAnnotation" + note: "Surrogate AST metadata was replaced by schema representation annotations and declaration codec hooks." +"effect/SchemaAST#SurrogateAnnotationId": + replacement: "Schema.Annotations.Declaration[\"representation\"]" + note: "The symbol annotation was replaced by the representation key." +"effect/SchemaAST#Suspend": + replacement: "SchemaAST.Suspend" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#symbolKeyword": + replacement: "SchemaAST.symbol" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#SymbolKeyword": + replacement: "SchemaAST.Symbol" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#TemplateLiteral": + replacement: "SchemaAST.TemplateLiteral" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#TemplateLiteralSpan": + replacement: "SchemaAST.TemplateLiteral" + note: "Template literal parts are represented directly as AST values in v4." +"effect/SchemaAST#TitleAnnotation": + replacement: "Schema.Annotations.Augment[\"title\"]" + note: "Titles are string-keyed schema annotations in v4." +"effect/SchemaAST#TitleAnnotationId": + replacement: "Schema.Annotations.Augment[\"title\"]" + note: "Symbol annotation IDs were removed; use title." +"effect/SchemaAST#Transformation": + replacement: "SchemaAST.Link" + note: "Transformations are links in the Base.encoding chain in v4." +"effect/SchemaAST#TransformationKind": + replacement: "SchemaTransformation.Transformation" + note: "Transformation implementations moved to SchemaTransformation and are stored on SchemaAST.Link." +"effect/SchemaAST#TupleType": + replacement: "SchemaAST.Arrays" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#Type": + replacement: "SchemaAST.AST" + note: "The tuple-element Type wrapper was removed; optionality and mutability moved to Context." +"effect/SchemaAST#typeAST": + replacement: "SchemaAST.toType" + note: "The type-side projection helper was renamed." +"effect/SchemaAST#TypeConstructorAnnotation": + replacement: "Schema.Annotations.Declaration[\"toCodec\"]" + note: "Type-constructor behavior moved to declaration codec annotations." +"effect/SchemaAST#TypeConstructorAnnotationId": + replacement: "Schema.Annotations.Declaration[\"toCodec\"]" + note: "Symbol annotation IDs were removed; use declaration codec annotation keys." +"effect/SchemaAST#TypeLiteral": + replacement: "SchemaAST.Objects" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#TypeLiteralTransformation": + replacement: "SchemaAST.Encoding" + note: "Object transformations are encoding links; use Schema.encodeKeys for key mappings." +"effect/SchemaAST#undefinedKeyword": + replacement: "SchemaAST.undefined" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#UndefinedKeyword": + replacement: "SchemaAST.Undefined" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#Union": + replacement: "SchemaAST.Union" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#UniqueSymbol": + replacement: "SchemaAST.UniqueSymbol" + note: "The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model." +"effect/SchemaAST#unknownKeyword": + replacement: "SchemaAST.unknown" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#UnknownKeyword": + replacement: "SchemaAST.Unknown" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#voidKeyword": + replacement: "SchemaAST.void" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." +"effect/SchemaAST#VoidKeyword": + replacement: "SchemaAST.Void" + note: "The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role." diff --git a/.context/effect/migration/annotations/effect__Scope.yaml b/.context/effect/migration/annotations/effect__Scope.yaml new file mode 100644 index 000000000..e27e3e680 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Scope.yaml @@ -0,0 +1,21 @@ +"effect/Scope#CloseableScope": + replacement: "Scope.Closeable" + note: "Use the renamed type and close it with Scope.close(scope, exit)." +"effect/Scope#CloseableScopeTypeId": + replacement: "none" + note: "The closeable-scope marker is private in v4." +"effect/Scope#extend": + replacement: "Scope.provide" + note: "The operation was renamed with the same data-first and curried forms." +"effect/Scope#Scope": + replacement: "Scope.Scope" + note: "The type remains; use module functions instead of the removed instance methods." +"effect/Scope#Scope.Closeable": + replacement: "Scope.Closeable" + note: "The nested alias is now the top-level Closeable interface." +"effect/Scope#Scope.Finalizer": + replacement: "(exit: Exit.Exit) => Effect.Effect" + note: "No alias is exported; inline the Scope.addFinalizerExit callback type." +"effect/Scope#ScopeTypeId": + replacement: "none" + note: "The Scope marker is private in v4 and has no public guard." diff --git a/.context/effect/migration/annotations/effect__ScopedCache.yaml b/.context/effect/migration/annotations/effect__ScopedCache.yaml new file mode 100644 index 000000000..25b6de9b5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ScopedCache.yaml @@ -0,0 +1,12 @@ +effect/ScopedCache#Lookup: + replacement: "(key: Key) => Effect.Effect" + note: "The named alias was removed; use an inline lookup type or ScopedCache.ScopedCache[\"lookup\"]." +effect/ScopedCache#ScopedCache: + replacement: "ScopedCache.ScopedCache" + note: "The model remains as a Pipeable scoped cache; construct and use it inside a Scope with explicit ScopedCache operations." +effect/ScopedCache#ScopedCache.Variance: + replacement: "none" + note: "The public variance marker was removed; use ScopedCache.ScopedCache directly." +effect/ScopedCache#ScopedCacheTypeId: + replacement: "none" + note: "The ScopedCache type id is internal in v4; do not inspect or construct the brand directly." diff --git a/.context/effect/migration/annotations/effect__ScopedRef.yaml b/.context/effect/migration/annotations/effect__ScopedRef.yaml new file mode 100644 index 000000000..42581a204 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ScopedRef.yaml @@ -0,0 +1,15 @@ +"effect/ScopedRef#ScopedRef": + replacement: "ScopedRef.ScopedRef" + note: "The type remains but no longer extends Effect; use ScopedRef.get or ScopedRef.getUnsafe." +"effect/ScopedRef#ScopedRef.Variance": + replacement: "none" + note: "The exported variance artifact was removed." +"effect/ScopedRef#ScopedRefTypeId": + replacement: "none" + note: "The marker is private in v4 and no public guard exists." +"effect/ScopedRef#ScopedRefUnify": + replacement: "none" + note: "ScopedRef no longer extends Effect; use ScopedRef.get explicitly." +"effect/ScopedRef#ScopedRefUnifyIgnore": + replacement: "none" + note: "The Effect-unification implementation detail was removed." diff --git a/.context/effect/migration/annotations/effect__Secret.yaml b/.context/effect/migration/annotations/effect__Secret.yaml new file mode 100644 index 000000000..475273e88 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Secret.yaml @@ -0,0 +1,21 @@ +"effect/Secret#fromIterable": + replacement: "Redacted.make(Array.from(iterable).join(\"\"))" + note: "Secret was removed; join the character iterable and wrap the resulting string in Redacted." +"effect/Secret#isSecret": + replacement: "Redacted.isRedacted" + note: "Secret was removed in favor of Redacted." +"effect/Secret#make": + replacement: "Redacted.make(bytes.map((byte) => String.fromCharCode(byte)).join(\"\"))" + note: "Secret was removed; preserve the v3 byte-to-code-unit conversion explicitly, then wrap the string in Redacted." +"effect/Secret#Secret": + replacement: "Redacted.Redacted" + note: "Secret was deprecated in v3 and removed in v4; use the generic Redacted wrapper." +"effect/Secret#Secret.Proto": + replacement: "none" + note: "The Secret-specific prototype was removed with the module; use Redacted.Redacted." +"effect/Secret#SecretTypeId": + replacement: "Redacted.isRedacted" + note: "The Secret marker was removed; use the Redacted runtime guard." +"effect/Secret#unsafeWipe": + replacement: "Redacted.wipeUnsafe" + note: "Redacted.wipeUnsafe removes the registry entry but, unlike v3 Secret, cannot zero a retained mutable byte array; zero external buffers separately when required." diff --git a/.context/effect/migration/annotations/effect__SingleProducerAsyncInput.yaml b/.context/effect/migration/annotations/effect__SingleProducerAsyncInput.yaml new file mode 100644 index 000000000..3801646de --- /dev/null +++ b/.context/effect/migration/annotations/effect__SingleProducerAsyncInput.yaml @@ -0,0 +1,12 @@ +"effect/SingleProducerAsyncInput#AsyncInputConsumer": + replacement: "Queue.Dequeue" + note: "Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one." +"effect/SingleProducerAsyncInput#AsyncInputProducer": + replacement: "Queue.Enqueue" + note: "Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one." +"effect/SingleProducerAsyncInput#make": + replacement: "Queue.make" + note: "Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one." +"effect/SingleProducerAsyncInput#SingleProducerAsyncInput": + replacement: "Queue.Queue" + note: "Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one." diff --git a/.context/effect/migration/annotations/effect__Sink.yaml b/.context/effect/migration/annotations/effect__Sink.yaml new file mode 100644 index 000000000..636836d09 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Sink.yaml @@ -0,0 +1,204 @@ +effect/Sink#Sink: + replacement: "Sink" + note: "Interface kept as Sink with the same type parameters; the internal representation changed from a Channel wrapper to a `transform: (upstream: Pull>, scope) => Effect>` function, and completion is the tuple `Sink.End = readonly [value, leftover?]`." +effect/Sink#Sink.Variance: + replacement: "Sink.Variance" + note: "Still exists as the namespace interface Sink.Variance (with Sink.VarianceStruct); the variance key is now the internal string \"~effect/Sink\" instead of the SinkTypeId symbol." +effect/Sink#SinkTypeId: + replacement: "none" + note: "The type id is the unexported internal string \"~effect/Sink\" (no unique symbol, no export); use the new guard Sink.isSink(u) instead of checking the symbol." +effect/Sink#SinkUnify: + replacement: "SinkUnify" + note: "Kept with the same name and shape (extends Effect.EffectUnify, preserves all five Sink type parameters)." +effect/Sink#SinkUnifyIgnore: + replacement: "SinkUnifyIgnore" + note: "Kept with the same name; now a standalone `{ Effect?: true }` interface instead of extending Effect.EffectUnifyIgnore." +effect/Sink#collectAll: + replacement: "Sink.collect" + note: "Renamed; returns Sink, In> collecting into a plain mutable Array instead of Chunk." +effect/Sink#collectAllFrom: + replacement: "none" + note: "Repeated-run result accumulation was removed; checked the v4 export list (no collectAllFrom/repeatedly). Re-implement with Sink.fromTransform, looping self.transform on the upstream pull (feeding leftovers back) until the upstream ends, accumulating results in an array." +effect/Sink#collectAllN: + replacement: "Sink.take" + note: "Sink.take(n) returns Sink, In, In> collecting up to n elements (Array instead of Chunk), emitting the unconsumed remainder as leftovers." +effect/Sink#collectAllToMap: + replacement: "Sink.reduce" + note: "Built-in HashMap collector removed; build a plain Map in the reducer: Sink.reduce(() => new Map(), (m, in_) => { const k = key(in_); return m.set(k, m.has(k) ? merge(m.get(k)!, in_) : in_) })." +effect/Sink#collectAllToMapN: + replacement: "none" + note: "Removed; approximating with Sink.reduceWhile((...) , (m) => m.size < n, ...) consumes (merges) the element that introduces the (n+1)-th key, whereas v3 left it as leftover — exact v3 leftover behavior needs a custom Sink.fromTransform that checks the key before consuming." +effect/Sink#collectAllToSet: + replacement: "Sink.reduce" + note: "Built-in HashSet collector removed; build a plain Set: Sink.reduce(() => new Set(), (s, in_) => s.add(in_))." +effect/Sink#collectAllToSetN: + replacement: "Sink.reduceWhile" + note: "Removed; equivalent via Sink.reduceWhile(() => new Set(), (s) => s.size < n, (s, in_) => s.add(in_)) — stops with leftovers once n distinct values are collected (plain Set instead of HashSet)." +effect/Sink#collectAllUntil: + replacement: "Sink.takeUntil" + note: "Renamed; Sink.takeUntil(predicate) collects into Array until the predicate matches, including the matching element, like v3." +effect/Sink#collectAllUntilEffect: + replacement: "Sink.takeUntilEffect" + note: "Renamed; Sink.takeUntilEffect(p) collects into Array until the effectful predicate returns true, including the matching element." +effect/Sink#collectAllWhile: + replacement: "Sink.takeWhile" + note: "Renamed; Sink.takeWhile(predicate) collects the matching prefix into Array, keeps the refinement overload, and excludes the first failing element (returned via leftovers)." +effect/Sink#collectAllWhileEffect: + replacement: "Sink.takeWhileEffect" + note: "Renamed; Sink.takeWhileEffect(p) collects into Array while the effectful predicate returns true." +effect/Sink#collectAllWhileWith: + replacement: "none" + note: "Repeatedly-run-and-fold was removed (no v4 counterpart in the export list); re-implement with Sink.fromTransform looping self.transform while the `while` predicate holds on each result, folding results with `body` and feeding leftovers back into the next run." +effect/Sink#collectLeftover: + replacement: "Sink.mapEnd" + note: "Use Sink.mapEnd to move the leftovers into the result: Sink.mapEnd(self, ([a, leftover]) => [[a, leftover ?? []] as const]); leftovers are NonEmptyReadonlyArray | undefined instead of Chunk." +effect/Sink#context: + replacement: "Sink.fromEffect(Effect.context())" + note: "Sink.context was removed; Sink.fromEffect(Effect.context()) yields the same Sink, unknown, never, never, R>." +effect/Sink#contextWith: + replacement: "Sink.fromEffect(Effect.contextWith(f))" + note: "Removed; compose Sink.fromEffect with Effect.contextWith to derive a value from the context." +effect/Sink#contextWithEffect: + replacement: "Sink.fromEffect(Effect.flatMap(Effect.context(), f))" + note: "Removed, and v4 Effect has no contextWithEffect; use Sink.fromEffect(Effect.flatMap(Effect.context(), f))." +effect/Sink#contextWithSink: + replacement: "Sink.unwrap(Effect.contextWith(f))" + note: "Removed; Sink.unwrap(Effect.contextWith((ctx: Context.Context) => f(ctx))) builds the sink from the context." +effect/Sink#dieMessage: + replacement: "Sink.die" + note: "Removed (v4 has no RuntimeException-based dieMessage anywhere); use Sink.die(new Error(message))." +effect/Sink#dieSync: + replacement: "Sink.failCauseSync" + note: "Removed; use Sink.failCauseSync(() => Cause.die(evaluate())) to defer defect evaluation, or Sink.die(defect) when eager is fine." +effect/Sink#dimap: + replacement: "Sink.mapInput + Sink.map" + note: "Removed; compose the two halves: self.pipe(Sink.mapInput(f), Sink.map(g))." +effect/Sink#dimapEffect: + replacement: "Sink.mapInputEffect + Sink.mapEffect" + note: "Removed; compose self.pipe(Sink.mapInputEffect(f), Sink.mapEffect(g))." +effect/Sink#dimapChunks: + replacement: "Sink.mapInputArray + Sink.map" + note: "Removed; compose self.pipe(Sink.mapInputArray(f), Sink.map(g)) — f now maps NonEmptyReadonlyArray instead of Chunk and must return a non-empty array." +effect/Sink#dimapChunksEffect: + replacement: "Sink.mapInputArrayEffect + Sink.mapEffect" + note: "Removed; compose self.pipe(Sink.mapInputArrayEffect(f), Sink.mapEffect(g)) — f maps NonEmptyReadonlyArray instead of Chunk and must return a non-empty array." +effect/Sink#drop: + replacement: "none" + note: "The drop* sinks were removed (nothing in the v4 export list); drop on the stream side instead with Stream.drop(n) before running the sink, or write a Sink.fromTransform that discards the first n pulled elements." +effect/Sink#dropUntil: + replacement: "none" + note: "Removed with the other drop* sinks; use Stream.dropUntil(predicate) on the stream before running the sink." +effect/Sink#dropUntilEffect: + replacement: "none" + note: "Removed; use Stream.dropUntilEffect(p) on the stream before running the sink." +effect/Sink#dropWhile: + replacement: "none" + note: "Removed; use Stream.dropWhile(predicate) on the stream before running the sink." +effect/Sink#dropWhileEffect: + replacement: "none" + note: "Removed; use Stream.dropWhileEffect(p) on the stream before running the sink." +effect/Sink#ensuringWith: + replacement: "Sink.onExit" + note: "Renamed; Sink.onExit(self, (exit: Exit) => finalizer) runs after completion, failure, or interruption — the exit now carries the sink's result value A (v3 passed Exit). Plain Sink.ensuring(effect) also still exists for the exit-independent case." +effect/Sink#filterInput: + replacement: "none" + note: "Removed, and not expressible via Sink.mapInputArray because its function must return a non-empty array (a fully-filtered batch is illegal); filter on the stream with Stream.filter(predicate) before running the sink, or write a Sink.fromTransform that skips empty filtered batches." +effect/Sink#filterInputEffect: + replacement: "none" + note: "Removed (same non-empty-array constraint as filterInput); use Stream.filterEffect(p) on the stream before running the sink." +effect/Sink#foldChunks: + replacement: "Sink.reduceWhileArray" + note: "Sink.reduceWhileArray(() => s, contFn, f) folds whole input batches; initial state is now a lazy thunk and f receives NonEmptyReadonlyArray instead of Chunk." +effect/Sink#foldChunksEffect: + replacement: "Sink.reduceWhileArrayEffect" + note: "Sink.reduceWhileArrayEffect(() => s, contFn, f) is the effectful array-level fold with continuation predicate; lazy initial state, NonEmptyReadonlyArray instead of Chunk. Sink.foldArray has the same shape but does not check contFn on the initial state." +effect/Sink#foldEffect: + replacement: "Sink.reduceWhileEffect" + note: "Sink.reduceWhileEffect(() => s, contFn, f) folds element-by-element with an effectful step and continuation predicate (checked on the initial state, like v3); initial state is now a lazy thunk. v4 Sink.fold has the same signature but skips the initial-state check." +effect/Sink#foldLeft: + replacement: "Sink.reduce" + note: "Renamed; Sink.reduce(() => s, f) — initial state is now a lazy thunk, semantics otherwise identical." +effect/Sink#foldLeftChunks: + replacement: "Sink.reduceArray" + note: "Renamed; Sink.reduceArray(() => s, f) folds whole batches — lazy initial state, f receives NonEmptyReadonlyArray instead of Chunk." +effect/Sink#foldLeftChunksEffect: + replacement: "Sink.reduceWhileArrayEffect" + note: "No plain reduceArrayEffect exists in v4; use Sink.reduceWhileArrayEffect(() => s, () => true, f) (constant-true predicate) — f receives NonEmptyReadonlyArray instead of Chunk and the result has L = never like v3." +effect/Sink#foldLeftEffect: + replacement: "Sink.reduceEffect" + note: "Renamed; Sink.reduceEffect(() => s, f) — lazy initial state, effectful step, no termination predicate." +effect/Sink#foldSink: + replacement: "Sink.orElse + Sink.flatMap" + note: "The two-channel match was removed; compose self.pipe(Sink.orElse((e) => options.onFailure(e)), Sink.flatMap((a) => options.onSuccess(a))) — orElse switches to the failure sink (resuming the same upstream), flatMap feeds leftovers to the success sink first." +effect/Sink#foldUntilEffect: + replacement: "Sink.foldUntil" + note: "v4 Sink.foldUntil(() => s, max, f) takes the effectful step function directly (f returns Effect), so it covers v3 foldUntilEffect; initial state is now a lazy thunk. For the pure v3 foldUntil wrap the step in Effect.succeed." +effect/Sink#foldWeighted: + replacement: "none" + note: "The whole foldWeighted family was removed from v4 Sink (checked the export list); re-implement with Sink.fold carrying the accumulated cost in the state (cont while cost < max), returning leftovers automatically when stopping mid-batch." +effect/Sink#foldWeightedDecompose: + replacement: "none" + note: "Removed with no decompose mechanism in v4; splitting oversized elements must happen upstream (transform the stream before the sink) or inside a custom Sink.fromTransform." +effect/Sink#foldWeightedDecomposeEffect: + replacement: "none" + note: "Removed; same as foldWeightedDecompose — no effectful weighted/decompose fold exists, re-implement via Sink.fromTransform or restructure upstream." +effect/Sink#foldWeightedEffect: + replacement: "none" + note: "Removed; re-implement with Sink.fold (its step is effectful in v4) tracking accumulated cost in the state." +effect/Sink#forEachChunk: + replacement: "Sink.forEachArray" + note: "Renamed; f receives NonEmptyReadonlyArray instead of Chunk." +effect/Sink#forEachChunkWhile: + replacement: "Sink.forEachWhileArray" + note: "Renamed; f: (NonEmptyReadonlyArray) => Effect continues while true, stops on false, as in v3." +effect/Sink#fromPush: + replacement: "Sink.fromTransform" + note: "The push-based protocol (Option push function failing with [Either, leftovers]) is gone; v4's low-level constructor is pull-based: Sink.fromTransform((upstream: Pull>, scope) => Effect>) — pull inputs from upstream and finish by succeeding with the [value, leftover?] tuple." +effect/Sink#leftover: + replacement: "Sink.succeed" + note: "Removed as a standalone constructor; Sink.succeed now takes optional leftovers: Sink.succeed(void 0, leftovers) where leftovers is a NonEmptyReadonlyArray instead of Chunk." +effect/Sink#mapInputChunks: + replacement: "Sink.mapInputArray" + note: "Renamed; f maps NonEmptyReadonlyArray => NonEmptyReadonlyArray (must stay non-empty) instead of Chunk => Chunk." +effect/Sink#mapInputChunksEffect: + replacement: "Sink.mapInputArrayEffect" + note: "Renamed; f maps NonEmptyReadonlyArray => Effect> (must stay non-empty) instead of Chunk => Effect." +effect/Sink#mkString: + replacement: "Sink.reduceArray" + note: "Removed as a built-in; equivalent one-liner: Sink.reduceArray(() => \"\", (s, arr) => s + arr.join(\"\"))." +effect/Sink#race: + replacement: "none" + note: "Sink racing (race/raceBoth/raceWith) was removed from v4; broadcast the stream (Stream.broadcast) into two consumers and race the resulting run effects with Effect.race, or write a custom Channel." +effect/Sink#raceBoth: + replacement: "none" + note: "Removed with the race family; broadcast the stream and use Effect.raceBoth (or Effect.race) on the two Stream.run effects to learn which side won." +effect/Sink#raceWith: + replacement: "none" + note: "Removed, along with the MergeDecision type it depended on; the closest is broadcasting the stream and combining the two run effects manually (Effect.raceWith on the run effects)." +effect/Sink#refineOrDie: + replacement: "Sink.catch" + note: "Removed; rebuild with the typed-error handler: Sink.catch(self, (e) => Option.match(pf(e), { onSome: Effect.fail, onNone: () => Effect.die(e) })) — note Sink.catch replaces the result on recovery, so refined errors must be re-failed as shown." +effect/Sink#refineOrDieWith: + replacement: "Sink.catch" + note: "Removed; same pattern as refineOrDie but die with the mapped defect: Sink.catch(self, (e) => Option.match(pf(e), { onSome: Effect.fail, onNone: () => Effect.die(f(e)) }))." +effect/Sink#splitWhere: + replacement: "none" + note: "Removed; it re-chunked input so the sink stopped before the first later element matching the predicate — closest v4 options are pre-splitting the stream (Stream.split / Stream.takeWhile) or a custom Sink.fromTransform that cuts pulled arrays at the predicate boundary and returns the rest as leftovers." +effect/Sink#unwrapScoped: + replacement: "Sink.unwrap" + note: "Folded into Sink.unwrap, whose signature now excludes Scope from R (Sink<..., Exclude | R2>), so scoped effects are accepted directly; resources stay open for the sink's lifetime." +effect/Sink#unwrapScopedWith: + replacement: "Sink.unwrap" + note: "Folded into Sink.unwrap — obtain the scope inside the effect via Effect.scope (Sink.unwrap(Effect.flatMap(Effect.scope, f))); for direct scope access use Sink.fromTransform, whose transform receives (upstream, scope)." +effect/Sink#zip: + replacement: "Sink.flatMap" + note: "The zip family was removed; sequential zip is self.pipe(Sink.flatMap((a) => Sink.map(that, (a2) => [a, a2] as const))) — leftovers of the first sink feed the second. The { concurrent: true } racing mode has no v4 equivalent." +effect/Sink#zipLeft: + replacement: "Sink.flatMap" + note: "Removed; use self.pipe(Sink.flatMap((a) => Sink.as(that, a))) to run both sequentially and keep the first result (no concurrent option)." +effect/Sink#zipRight: + replacement: "Sink.flatMap" + note: "Removed; use self.pipe(Sink.flatMap(() => that)) to run both sequentially and keep the second result (no concurrent option)." +effect/Sink#zipWith: + replacement: "Sink.flatMap" + note: "Removed; use self.pipe(Sink.flatMap((a) => Sink.map(that, (a2) => f(a, a2)))) — sequential only, the { concurrent: true } option has no v4 equivalent." diff --git a/.context/effect/migration/annotations/effect__SortedMap.yaml b/.context/effect/migration/annotations/effect__SortedMap.yaml new file mode 100644 index 000000000..5139e02e9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__SortedMap.yaml @@ -0,0 +1,66 @@ +"effect/SortedMap#empty": + replacement: "HashMap.empty" + note: "SortedMap was removed; use an immutable HashMap and retain the key Order separately." +"effect/SortedMap#entries": + replacement: "HashMap.entries + Array.sortWith" + note: "Materialize HashMap.entries and sort by key with the retained Order when ordered traversal is required." +"effect/SortedMap#fromIterable": + replacement: "HashMap.fromIterable" + note: "Use HashMap.fromIterable and retain the key Order separately; duplicate keys collapse." +"effect/SortedMap#get": + replacement: "HashMap.get" + note: "Direct optional lookup on the replacement immutable map." +"effect/SortedMap#getOrder": + replacement: "none" + note: "HashMap does not store an Order; retain and pass the key Order explicitly." +"effect/SortedMap#headOption": + replacement: "HashMap.entries + Array.sortWith + Array.head" + note: "Sort entries by key with the retained Order, then take the optional first entry." +"effect/SortedMap#isEmpty": + replacement: "HashMap.isEmpty" + note: "Direct emptiness check on the replacement immutable map." +"effect/SortedMap#isNonEmpty": + replacement: "HashMap.isEmpty" + note: "Use !HashMap.isEmpty(self); no dedicated HashMap.isNonEmpty helper exists." +"effect/SortedMap#isSortedMap": + replacement: "HashMap.isHashMap" + note: "Use the replacement model guard; it does not prove that observations were sorted." +"effect/SortedMap#keys": + replacement: "HashMap.entries + Array.sortWith + Array.map" + note: "Sort entries by key, map to keys, and iterate the resulting array." +"effect/SortedMap#lastOption": + replacement: "HashMap.entries + Array.sortWith + Array.last" + note: "Sort entries by key with the retained Order, then take the optional last entry." +"effect/SortedMap#make": + replacement: "HashMap.make" + note: "Remove the outer order-curried constructor and pass entries directly to HashMap.make." +"effect/SortedMap#map": + replacement: "HashMap.map" + note: "The value-and-key callback remains, but result iteration is unordered until explicitly sorted." +"effect/SortedMap#partition": + replacement: "HashMap.filter" + note: "Build [excluded, satisfying] with complementary HashMap.filter calls; adapt the callback to the old key predicate." +"effect/SortedMap#remove": + replacement: "HashMap.remove" + note: "Direct persistent removal; explicitly sort only when observing entries." +"effect/SortedMap#set": + replacement: "HashMap.set" + note: "Direct persistent insert or update; explicitly sort only when observing entries." +"effect/SortedMap#size": + replacement: "HashMap.size" + note: "Direct size query on the replacement immutable map." +"effect/SortedMap#SortedMap": + replacement: "HashMap.HashMap" + note: "Use HashMap as the immutable core model and retain Order externally; ordered iteration and range seeks require sorting on observation." +"effect/SortedMap#TypeId": + replacement: "none" + note: "The SortedMap brand was removed and HashMap.TypeId is private; use HashMap.isHashMap when a guard is needed." +"effect/SortedMap#values": + replacement: "HashMap.entries + Array.sortWith + Array.map" + note: "Sort entries by key, map to values, and iterate the resulting array." +"effect/SortedMap#has": + replacement: "HashMap.has" + note: "Use direct membership testing on the replacement HashMap; retain the key Order separately for sorted observations." +"effect/SortedMap#reduce": + replacement: "HashMap.reduce" + note: "Reduce the replacement HashMap, but explicitly sort entries first if the old key-order traversal affected the result." diff --git a/.context/effect/migration/annotations/effect__SortedSet.yaml b/.context/effect/migration/annotations/effect__SortedSet.yaml new file mode 100644 index 000000000..cc0e6e0c8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__SortedSet.yaml @@ -0,0 +1,60 @@ +"effect/SortedSet#add": + replacement: "HashSet.add" + note: "Direct persistent add on the replacement set; sort only when traversing." +"effect/SortedSet#difference": + replacement: "HashSet.difference + HashSet.fromIterable" + note: "Convert the old general iterable argument to HashSet before taking the difference." +"effect/SortedSet#empty": + replacement: "HashSet.empty" + note: "SortedSet was removed; use an immutable HashSet and retain the element Order separately." +"effect/SortedSet#filter": + replacement: "HashSet.filter" + note: "Direct persistent filtering on the replacement set; traversal is unordered until explicitly sorted." +"effect/SortedSet#fromIterable": + replacement: "HashSet.fromIterable" + note: "Use HashSet.fromIterable and retain the element Order separately." +"effect/SortedSet#getEquivalence": + replacement: "Equal.asEquivalence" + note: "HashSet implements Effect equality by set content; use Equal.asEquivalence>()." +"effect/SortedSet#intersection": + replacement: "HashSet.intersection + HashSet.fromIterable" + note: "Convert the old general iterable argument to HashSet before taking the intersection." +"effect/SortedSet#isSortedSet": + replacement: "HashSet.isHashSet" + note: "Use the replacement model guard; it does not prove that observations were sorted." +"effect/SortedSet#make": + replacement: "HashSet.make" + note: "Remove the outer order-curried constructor and pass values directly to HashSet.make." +"effect/SortedSet#map": + replacement: "HashSet.map" + note: "Remove the output Order argument; retain it externally and sort only when traversing." +"effect/SortedSet#partition": + replacement: "HashSet.filter" + note: "Build [excluded, satisfying] with complementary HashSet.filter calls." +"effect/SortedSet#remove": + replacement: "HashSet.remove" + note: "Direct persistent removal on the replacement set." +"effect/SortedSet#size": + replacement: "HashSet.size" + note: "Direct size query on the replacement immutable set." +"effect/SortedSet#SortedSet": + replacement: "HashSet.HashSet" + note: "Use HashSet as the immutable core model and retain Order externally; ordered iteration requires sorting on observation." +"effect/SortedSet#TypeId": + replacement: "none" + note: "The SortedSet brand was removed and HashSet.TypeId is private; use HashSet.isHashSet when a guard is needed." +"effect/SortedSet#union": + replacement: "HashSet.union + HashSet.fromIterable" + note: "Convert the old general iterable argument to HashSet before taking the union." +"effect/SortedSet#values": + replacement: "Array.sort" + note: "Sort the replacement HashSet with the retained Order and iterate the resulting array." +"effect/SortedSet#every": + replacement: "HashSet.every" + note: "Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects." +"effect/SortedSet#has": + replacement: "HashSet.has" + note: "Use direct membership testing on the replacement HashSet." +"effect/SortedSet#some": + replacement: "HashSet.some" + note: "Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects." diff --git a/.context/effect/migration/annotations/effect__Stream.yaml b/.context/effect/migration/annotations/effect__Stream.yaml new file mode 100644 index 000000000..c2d861cd5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Stream.yaml @@ -0,0 +1,450 @@ +effect/Stream#Stream: + replacement: "Stream" + note: "The Stream interface is unchanged in shape and keeps the effect/Stream import path; the type-id key is now the string literal \"~effect/Stream\" instead of a unique symbol." +effect/Stream#Stream.Context: + replacement: "Stream.Services" + note: "Type-level extractor of the R parameter renamed from Stream.Context to Stream.Services; identical conditional-infer semantics." +effect/Stream#Stream.DynamicTuple: + replacement: "Types.TupleOf" + note: "Already deprecated in v3 in favor of Types.TupleOf; removed in v4. Use Types.TupleOf (v4 Stream.broadcastN uses it for its return type)." +effect/Stream#Stream.DynamicTupleOf: + replacement: "Types.TupleOf" + note: "Recursive tuple-builder helper removed with Stream.DynamicTuple; Types.TupleOf is the v4 equivalent." +effect/Stream#StreamTypeId: + replacement: "Stream.TypeId" + note: "Renamed StreamTypeId -> TypeId and changed from a unique symbol to the string literal \"~effect/Stream\" (both the const and the type)." +effect/Stream#StreamUnify: + replacement: "Stream.StreamUnify" + note: "Still exported under the same name in v4 (extends Effect.EffectUnify); no change needed besides any Chunk-related element types." +effect/Stream#StreamUnifyIgnore: + replacement: "Stream.StreamUnifyIgnore" + note: "Still exported under the same name in v4 (extends Effect.EffectUnifyIgnore with Effect ignored); unchanged." +effect/Stream#accumulateChunks: + replacement: "none" + note: "v3 accumulateChunks only rewrote the internal chunk layout (each chunk cumulatively contained all prior elements) without changing the emitted element sequence; v4 has no chunk-layout twin. Stream.accumulate emits the cumulative NonEmptyArray values, and Stream.rechunk controls chunk sizing." +effect/Stream#acquireRelease: + replacement: "Stream.scoped(Stream.fromEffect(Effect.acquireRelease(acquire, release)))" + note: "Dedicated constructor removed; compose Effect.acquireRelease (same (resource, exit) release signature) with Stream.fromEffect, then Stream.scoped to tie the finalizer to the stream's lifetime." +effect/Stream#aggregateWithinEither: + replacement: "Stream.aggregateWithin" + note: "Either-emitting variant removed; v4 aggregateWithin(sink, schedule) emits only the sink outputs B (schedule outputs are no longer surfaced as Either.right)." +effect/Stream#as: + replacement: "Stream.map(() => value)" + note: "Stream.as was removed; replace each element with a constant via Stream.map." +effect/Stream#async: + replacement: "Stream.callback" + note: "Stream.callback((queue) => Effect | void, { bufferSize?, strategy? }) replaces the Emit-based async; push with Queue.offer/offerAll, end with Queue.end, fail with Queue.fail." +effect/Stream#asyncEffect: + replacement: "Stream.callback" + note: "The register function of Stream.callback may return an Effect (run before the stream starts pulling), covering asyncEffect; signal end/failure through the provided Queue." +effect/Stream#asyncPush: + replacement: "Stream.callback" + note: "Stream.callback's register effect can use Scope for acquire/release of the external subscription, replacing asyncPush; the Emit ops helpers become plain Queue operations." +effect/Stream#asyncScoped: + replacement: "Stream.callback" + note: "Stream.callback's register effect may use Scope (Scope is excluded from the resulting R), replacing asyncScoped; the Option end signal becomes Queue.end." +effect/Stream#branchAfter: + replacement: "Stream.peel" + note: "Removed; the closest v4 primitive is Stream.peel(self, Sink.take(n)), a scoped Effect yielding [firstN, restStream] from which you build the continuation stream and re-wrap with Stream.unwrap." + example: "Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(head)(rest)))" +effect/Stream#broadcastDynamic: + replacement: "Stream.broadcast" + note: "v4 Stream.broadcast({ capacity, strategy?, replay? }) is the dynamic-subscriber fan-out returning Effect, never, Scope | R> (v3 fixed-arity broadcast(n) became Stream.broadcastN); Stream.share adds refcounted/idleTimeToLive semantics." +effect/Stream#broadcastedQueues: + replacement: "none" + note: "Queue-of-Take fan-out surface removed. Use Stream.broadcastN({ n, capacity }) for a fixed tuple of mirror streams, or Stream.toPubSubTake to obtain a PubSub of Take values and subscribe consumers to it." +effect/Stream#broadcastedQueuesDynamic: + replacement: "none" + note: "Removed with broadcastedQueues. Use Stream.broadcast (dynamic mirror streams) or Stream.toPubSubTake + PubSub subscriptions when raw Take-level consumers are needed." +effect/Stream#bufferChunks: + replacement: "Stream.bufferArray" + note: "Chunk->Array rename; buffers whole arrays (chunks) up to capacity with the same strategy options." +effect/Stream#catchAll: + replacement: "Stream.catch" + note: "Renamed to Stream.catch (exported keyword-style); same (error) => Stream handler for all typed failures." +effect/Stream#catchAllCause: + replacement: "Stream.catchCause" + note: "Renamed; handler receives the full Cause and returns a recovery stream, identical semantics." +effect/Stream#catchSome: + replacement: "Stream.catchFilter" + note: "Option-returning partial handler replaced by the Filter API: Stream.catchFilter(filter, f, orElse?) recovers matched errors, unmatched failures pass through (Stream.catchIf for refinement/predicate matching)." +effect/Stream#catchSomeCause: + replacement: "Stream.catchCauseFilter" + note: "Option-returning cause handler replaced by Stream.catchCauseFilter(filter, f, orElse?) using a Filter on the Cause (Stream.catchCauseIf for refinements)." +effect/Stream#chunksWith: + replacement: "Stream.flattenArray(f(Stream.chunks(self)))" + note: "No dedicated combinator; expose chunk structure with Stream.chunks (Stream>), transform, then re-flatten with Stream.flattenArray." +effect/Stream#combineChunks: + replacement: "Stream.combineArray" + note: "Chunk->Array rename of the pull-level combining primitive; pulls now yield NonEmptyReadonlyArray values and halt via Cause.Done-failing Pull effects instead of Option-typed errors." +effect/Stream#concatAll: + replacement: "Stream.flatten" + note: "Chunk-of-streams constructor removed; sequential concatenation of many streams is Stream.flatten(Stream.fromIterable(streams)) (default concurrency 1 preserves order)." +effect/Stream#context: + replacement: "Stream.fromEffect(Effect.context())" + note: "Dedicated accessor removed; lift Effect.context() into a single-element stream." +effect/Stream#contextWith: + replacement: "Stream.fromEffect(Effect.contextWith(f))" + note: "Dedicated accessor removed; Effect.contextWith still exists in v4, lift it with Stream.fromEffect." +effect/Stream#contextWithEffect: + replacement: "Stream.fromEffect(Effect.flatMap(Effect.context(), f))" + note: "Removed; read the Context with Effect.context, feed it to the effectful function, and lift the result with Stream.fromEffect." +effect/Stream#contextWithStream: + replacement: "Stream.unwrap(Effect.contextWith(f))" + note: "Removed; build the dependent stream inside Effect.contextWith and flatten with Stream.unwrap." +effect/Stream#crossLeft: + replacement: "Stream.crossWith(that, (a, _) => a)" + note: "Removed; cartesian product keeping only left elements is expressed with Stream.crossWith and a left-projecting combiner." +effect/Stream#crossRight: + replacement: "Stream.crossWith(that, (_, b) => b)" + note: "Removed; cartesian product keeping only right elements is Stream.crossWith with a right-projecting combiner (equivalently Stream.flatMap(self, () => that))." +effect/Stream#dieMessage: + replacement: "Stream.die(new Error(message))" + note: "Removed along with RuntimeException; die with an explicit defect value via Stream.die." +effect/Stream#dieSync: + replacement: "Stream.failCauseSync(() => Cause.die(evaluate()))" + note: "Removed; lazily construct the defect cause with Cause.die inside Stream.failCauseSync." +effect/Stream#distributedWith: + replacement: "none" + note: "Predicate-routed fixed fan-out to Take queues removed (no v4 counterpart found among broadcast/broadcastN/share/toPubSub/partition). Closest patterns: Stream.broadcastN + Stream.filter per branch, Stream.partition for two-way splits, or manual routing by running the stream into per-consumer Queues." +effect/Stream#distributedWithDynamic: + replacement: "none" + note: "Dynamic predicate-routed fan-out removed with distributedWith. Use Stream.broadcast/Stream.share for dynamic mirrors plus per-subscriber Stream.filter, or hand-roll routing into Queues via Stream.runForEach." +effect/Stream#either: + replacement: "Stream.result" + note: "Either is replaced by Result in v4: Stream.result yields Stream, never, R> (element -> Result.succeed, first error -> Result.fail and the stream ends, as before)." +effect/Stream#ensuringWith: + replacement: "Stream.onExit" + note: "Renamed; Stream.onExit runs the finalizer with the Exit of the stream, identical shape." +effect/Stream#execute: + replacement: "Stream.fromEffectDrain" + note: "Renamed; runs the effect for its side effects and emits nothing (Stream)." +effect/Stream#filterMapWhile: + replacement: "Stream.takeWhileFilter" + note: "Option-returning partial function replaced by the Filter API: Stream.takeWhileFilter(filter) maps and emits while the filter passes, ending the stream at the first miss." +effect/Stream#filterMapWhileEffect: + replacement: "none" + note: "No effectful takeWhileFilter variant in v4. Recreate by using Stream.takeWhileFilter with a Filter that selects the Effect to run, followed by Stream.mapEffect((eff) => eff) to execute it." +effect/Stream#finalizer: + replacement: "Stream.ensuring" + note: "One-element finalizer-registering stream removed; attach finalizers directly with Stream.ensuring/Stream.onExit, or register in the stream scope via Stream.scoped(Stream.fromEffect(Effect.addFinalizer(fin))) when the v3 concat-a-finalizer pattern must be preserved." +effect/Stream#find: + replacement: "Stream.take(Stream.filter(self, predicate), 1)" + note: "Removed; first-match semantics are Stream.filter followed by Stream.take(1)." +effect/Stream#findEffect: + replacement: "Stream.take(Stream.filterEffect(self, f), 1)" + note: "Removed; Stream.filterEffect takes an effectful (a, index) => Effect predicate, then Stream.take(1) stops at the first match." +effect/Stream#flattenChunks: + replacement: "Stream.flattenArray" + note: "Chunk->Array rename; flattens a Stream of ReadonlyArray values into their elements." +effect/Stream#flattenExitOption: + replacement: "Stream.flattenTake" + note: "The Exit> end-of-stream encoding is gone; v4 uses Take = NonEmptyReadonlyArray | Exit and Stream.flattenTake unwraps it (emit arrays, end/fail on Exit)." +effect/Stream#flattenIterables: + replacement: "Stream.flattenIterable" + note: "Renamed (singular); flattens a Stream of Iterables into their elements." +effect/Stream#fromChunk: + replacement: "Stream.fromArray" + note: "Chunk->Array rename; takes a ReadonlyArray and emits it as one chunk." +effect/Stream#fromChunkPubSub: + replacement: "Stream.fromPubSub" + note: "Chunked PubSub constructors are gone; v4 Stream.fromPubSub(pubsub) consumes PubSub directly (batched internally). For a PubSub carrying arrays use Stream.flattenArray(Stream.fromPubSub(pubsub)); the scoped/shutdown options were dropped (Stream.fromSubscription consumes an existing subscription)." +effect/Stream#fromChunkQueue: + replacement: "Stream.fromQueue" + note: "Chunked Queue constructor gone; v4 Stream.fromQueue consumes Queue.Dequeue whose done/failure signals end the stream (no shutdown option). For array payloads wrap with Stream.flattenArray." +effect/Stream#fromChunks: + replacement: "Stream.fromArrays" + note: "Chunk->Array rename; variadic arrays, each emitted as one chunk." +effect/Stream#fromEffectOption: + replacement: "none" + note: "The Effect> encoding (fail None = empty stream) is removed; v4 signals early end with Cause.Done in Pull-level code. Rebuild with Stream.unwrap: map the success to Stream.succeed and match the Option error to Stream.empty (None) or Stream.fail (Some)." +effect/Stream#fromReadableStreamByob: + replacement: "Stream.fromReadableStream" + note: "BYOB reader variant removed (no byob support in v4 source); Stream.fromReadableStream({ evaluate, onError, releaseLockOnEnd? }) consumes any ReadableStream with a default reader, without byte-buffer allocation control." +effect/Stream#fromTPubSub: + replacement: "none" + note: "STM TPubSub was replaced by the transactional TxPubSub module and v4 Stream has no Tx* constructors; subscribe and repeatedly TxQueue.take from the subscription (e.g. inside Stream.fromPull/Stream.callback), or bridge through a regular PubSub and Stream.fromPubSub." +effect/Stream#fromTQueue: + replacement: "none" + note: "STM TQueue was replaced by TxQueue and v4 Stream has no Tx* constructors; drain by repeatedly calling TxQueue.take inside a custom loop (Stream.fromPull/Stream.callback), or bridge into a regular Queue and use Stream.fromQueue." +effect/Stream#haltAfter: + replacement: "Stream.haltWhen(Effect.sleep(duration))" + note: "Duration-specialized halt removed; v3 documented it as haltWhen with a sleep — completes the stream after the duration without interrupting an in-flight pull." +effect/Stream#haltWhenDeferred: + replacement: "Stream.haltWhen(Deferred.await(deferred))" + note: "Deferred-specialized variant removed; Deferred.await is an Effect, so plain Stream.haltWhen covers it." +effect/Stream#identity: + replacement: "Channel.identity" + note: "The identity-pipeline Stream is gone; for pipeThrough-style plumbing use Stream.pipeThroughChannel(Channel.identity()), or simply the identity function where a Stream=>Stream transform is expected." +effect/Stream#interruptAfter: + replacement: "Stream.interruptWhen(Effect.sleep(duration))" + note: "Duration-specialized interrupt removed; interruptWhen forks the sleep and also interrupts an in-progress pull, matching v3 semantics." +effect/Stream#interruptWhenDeferred: + replacement: "Stream.interruptWhen(Deferred.await(deferred))" + note: "Deferred-specialized variant removed; pass Deferred.await to Stream.interruptWhen (a Deferred failure surfaces as the stream's failure, as before)." +effect/Stream#mapChunks: + replacement: "Stream.mapArray" + note: "Chunk->Array rename; transforms each emitted chunk as a NonEmptyReadonlyArray." +effect/Stream#mapChunksEffect: + replacement: "Stream.mapArrayEffect" + note: "Chunk->Array rename of the effectful per-chunk transform." +effect/Stream#mapConcat: + replacement: "Stream.flattenIterable(Stream.map(self, f))" + note: "Removed; map each element to an Iterable and flatten with Stream.flattenIterable." +effect/Stream#mapConcatChunk: + replacement: "Stream.flattenArray(Stream.map(self, f))" + note: "Chunk variant removed with Chunk itself; map to a ReadonlyArray and flatten with Stream.flattenArray." +effect/Stream#mapConcatChunkEffect: + replacement: "Stream.flattenArray(Stream.mapEffect(self, f))" + note: "Removed; effectfully map each element to a ReadonlyArray and flatten with Stream.flattenArray." +effect/Stream#mapConcatEffect: + replacement: "Stream.flattenIterable(Stream.mapEffect(self, f))" + note: "Removed; effectfully map each element to an Iterable and flatten with Stream.flattenIterable." +effect/Stream#mapErrorCause: + replacement: "Stream.catchCause((cause) => Stream.failCause(f(cause)))" + note: "Removed; transform the full Cause by catching it and re-failing with the mapped cause." +effect/Stream#mapInputContext: + replacement: "Stream.updateContext" + note: "Renamed; same contravariant (Context) => Context mapping of the required services." +effect/Stream#mergeEither: + replacement: "Stream.mergeResult" + note: "Either replaced by Result: Stream.mergeResult(self, that) yields Result.Result with self -> Result.succeed and that -> Result.fail (v3 put self in Either.left and that in Either.right, so the success/left roles swap sides)." +effect/Stream#mergeWith: + replacement: "Stream.merge(Stream.map(self, onSelf), Stream.map(that, onOther), { haltStrategy })" + note: "Removed; pre-map both streams to the common type and use Stream.merge, whose options accept the same haltStrategy union (\"left\" | \"right\" | \"both\" | \"either\")." +effect/Stream#mergeWithTag: + replacement: "none" + note: "Struct-to-tagged-union merge removed. Recreate with Stream.mergeAll over the entries, tagging each stream first." + example: "Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) => ({ _tag, value }))), { concurrency })" +effect/Stream#onDone: + replacement: "Stream.onEnd" + note: "Renamed; v4 onEnd takes an Effect value (not a () => Effect thunk) run when the stream ends successfully, and its error type may add to the stream's." + +effect/Stream#orDieWith: + replacement: "Stream.orDie" + note: "orDieWith removed; transform the error first, then convert failures to defects: `self.pipe(Stream.mapError(f), Stream.orDie)`." +effect/Stream#orElse: + replacement: "Stream.catch" + note: "v3 catchAll was renamed to Stream.catch in v4; orElse ignored the error, so write `Stream.catch(self, () => that())`." +effect/Stream#orElseEither: + replacement: "Stream.catch" + note: "Removed; Either is replaced by Result in v4. Emulate: `Stream.map(self, Result.succeed).pipe(Stream.catch(() => Stream.map(that(), Result.fail)))` (same encoding v4 Stream.mergeResult uses)." +effect/Stream#orElseFail: + replacement: "Stream.mapError" + note: "Removed; it only replaced the failure value: `Stream.mapError(self, () => error())` or `Stream.catch(self, () => Stream.fail(error()))`." +effect/Stream#orElseIfEmptyChunk: + replacement: "Stream.orElseIfEmpty" + note: "Folded into Stream.orElseIfEmpty, which now takes a lazy fallback Stream: `Stream.orElseIfEmpty(self, () => Stream.fromArray(array))`; Chunk is replaced by plain arrays." +effect/Stream#orElseIfEmptyStream: + replacement: "Stream.orElseIfEmpty" + note: "Direct rename: v4 Stream.orElseIfEmpty takes a LazyArg fallback, identical semantics." +effect/Stream#paginateChunk: + replacement: "Stream.paginate" + note: "v4 Stream.paginate is effectful and array-based: `paginate(s, (s) => Effect, Option]>)`; wrap the pure step in Effect.succeed and use an array instead of a Chunk." +effect/Stream#paginateChunkEffect: + replacement: "Stream.paginate" + note: "v4 Stream.paginate has exactly this shape; only Chunk becomes ReadonlyArray." +effect/Stream#paginateEffect: + replacement: "Stream.paginate" + note: "v4 Stream.paginate emits a batch per step; wrap the single value in an array: `(s) => Effect.map(step(s), ([a, next]) => [[a], next])`." +effect/Stream#partitionEither: + replacement: "Stream.partitionEffect" + note: "Either-based split replaced by Filter.FilterEffect: the function now returns Effect> (Result.succeed/Result.fail instead of Either.right/left). Returns Effect<[passes, fails], never, R | Scope> — note the tuple is [passes, fails], v3 was [left, right]; options are { capacity?, concurrency? }." +effect/Stream#provideLayer: + replacement: "Stream.provide" + note: "v4 Stream.provide accepts a Layer or a Context; behavior identical." +effect/Stream#provideServiceStream: + replacement: "none" + note: "Removed; v4 has provideService/provideServiceEffect but no stream-valued variant. Emulate with `Stream.flatMap(services, (s) => Stream.provideService(self, tag, s))` over the service stream, or use Stream.provideServiceEffect for effectful acquisition." +effect/Stream#provideSomeContext: + replacement: "Stream.provideContext" + note: "v4 Stream.provideContext is the single Context provider with `Exclude` semantics — same behavior as v3 provideSomeContext." +effect/Stream#provideSomeLayer: + replacement: "Stream.provide" + note: "v4 Stream.provide accepts a Layer (or Context) and excludes only the provided services from R — same partial-provision semantics." +effect/Stream#refineOrDie: + replacement: "Stream.catch" + note: "Removed; emulate with `Stream.catch(self, (e) => { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) })` — refail refined errors, die on the rest." +effect/Stream#refineOrDieWith: + replacement: "Stream.catch" + note: "Removed; same as refineOrDie but die with the mapped defect: `Stream.die(f(e))` for unrefined errors." +effect/Stream#repeatEffect: + replacement: "Stream.fromEffectRepeat" + note: "Renamed; repeats the effect forever emitting each result." +effect/Stream#repeatEffectChunk: + replacement: "Stream.fromIterableEffectRepeat" + note: "Renamed; the effect now produces an Iterable/array instead of a Chunk, repeated forever." +effect/Stream#repeatEffectChunkOption: + replacement: "Stream.fromIterableEffectRepeat" + note: "The Option error encoding is gone: end the stream by failing the effect with `Cause.done()` (a Cause.Done failure); Done is excluded from the resulting stream's error type (Pull.ExcludeDone)." +effect/Stream#repeatEffectOption: + replacement: "Stream.fromEffectRepeat" + note: "The Option error encoding is gone: fail the effect with `Cause.done()` instead of Option.none() to end the stream; other failures propagate as stream errors." +effect/Stream#repeatEffectWithSchedule: + replacement: "Stream.fromEffectSchedule" + note: "Renamed; runs the effect once, then repeats it per the schedule, emitting each result." +effect/Stream#repeatEither: + replacement: "none" + note: "Removed; v4 Stream.repeat(schedule) repeats the stream but never emits the schedule outputs, and no Either/unification variant exists. If schedule outputs must be observed, hand-roll with Channel or track them via a schedule that taps into a Ref." +effect/Stream#repeatElementsWith: + replacement: "none" + note: "Removed; v4 Stream.repeatElements(schedule) repeats each element per the schedule but never emits schedule outputs — the onElement/onSchedule unification is gone. Use repeatElements if only element repetition is needed." +effect/Stream#repeatValue: + replacement: "Stream.fromEffectRepeat" + note: "Removed; use `Stream.fromEffectRepeat(Effect.succeed(value))` or `Stream.forever(Stream.succeed(value))`." +effect/Stream#repeatWith: + replacement: "none" + note: "Removed; v4 Stream.repeat(schedule) covers the repetition but drops the schedule outputs and the onElement/onSchedule unification. Hand-roll if schedule outputs must appear in the stream." +effect/Stream#runFoldScoped: + replacement: "Stream.runFold" + note: "Scoped run variants are gone; v4 run functions manage the stream's scope internally and the initial value is now a LazyArg: `Stream.runFold(self, () => s, f)`. For enclosing-scope control, pull manually via `Stream.toPull` (Effect)." +effect/Stream#runFoldScopedEffect: + replacement: "Stream.runFoldEffect" + note: "Scoped run variants are gone; use `Stream.runFoldEffect(self, () => s, f)` — scope is managed internally, initial value is a LazyArg. Use Stream.toPull for manual scoped consumption." +effect/Stream#runFoldWhile: + replacement: "none" + note: "v4 runFold has no early-exit predicate; emulate with Stream.runForEachWhile and a mutable accumulator." + example: | + // v3: Stream.runFoldWhile(self, init, cont, f) + Effect.suspend(() => { + let acc = init + return Stream.runForEachWhile(self, (a) => { + acc = f(acc, a) + return Effect.succeed(cont(acc)) + }).pipe(Effect.map(() => acc)) + }) +effect/Stream#runFoldWhileEffect: + replacement: "none" + note: "v4 runFoldEffect has no early-exit predicate; emulate with Stream.runForEachWhile and a mutable accumulator, mapping the effectful step to Effect via cont(acc) (see runFoldWhile example)." +effect/Stream#runFoldWhileScoped: + replacement: "none" + note: "Both the while-predicate and the scoped run variants are gone in v4; emulate the predicate with Stream.runForEachWhile plus a mutable accumulator (see runFoldWhile); scope is managed internally by v4 run functions." +effect/Stream#runFoldWhileScopedEffect: + replacement: "none" + note: "Both the while-predicate and the scoped run variants are gone in v4; emulate with Stream.runForEachWhile plus a mutable accumulator and effectful step; scope is managed internally by v4 run functions." +effect/Stream#runForEachChunk: + replacement: "Stream.runForEachArray" + note: "Renamed; the callback receives a NonEmptyReadonlyArray instead of a Chunk." +effect/Stream#runForEachChunkScoped: + replacement: "Stream.runForEachArray" + note: "Scoped run variants are gone; v4 runForEachArray manages the stream scope internally. Use Stream.toPull for manual scoped consumption." +effect/Stream#runForEachScoped: + replacement: "Stream.runForEach" + note: "Scoped run variants are gone; v4 runForEach manages the stream scope internally. Use Stream.toPull for manual scoped consumption." +effect/Stream#runForEachWhileScoped: + replacement: "Stream.runForEachWhile" + note: "Scoped run variants are gone; v4 runForEachWhile (callback returns Effect) manages the stream scope internally." +effect/Stream#runIntoPubSubScoped: + replacement: "Stream.runIntoPubSub" + note: "Scoped variant removed; v4 runIntoPubSub(pubsub, { shutdownOnEnd? }) publishes plain values (the Take wrapper is gone) and does not require Scope — fork the returned effect (Effect.forkIn/Effect.forkScoped) to reproduce the background scoped behavior." +effect/Stream#runIntoQueueElementsScoped: + replacement: "Stream.runIntoQueue" + note: "The per-element Exit> encoding is gone; v4 runIntoQueue targets a Queue — elements are offered plainly and failure/end are signalled through the queue's error/done channel. Fork with Effect.forkIn for scoped background running." +effect/Stream#runIntoQueueScoped: + replacement: "Stream.runIntoQueue" + note: "Scoped variant removed; v4 runIntoQueue offers plain values to a Queue (Take wrapper gone) and requires no Scope — fork the returned effect into a scope (Effect.forkIn) if needed." +effect/Stream#runScoped: + replacement: "Stream.run" + note: "Scoped variant removed; v4 Stream.run(sink) manages the stream's scope internally. For consumption tied to an enclosing Scope, use Stream.toPull and drive the Pull manually." +effect/Stream#scanReduce: + replacement: "Stream.mapAccum" + note: "Removed; emulate first-element-as-seed with `Stream.mapAccum(self, () => undefined as A | undefined, (acc, a) => { const next = acc === undefined ? a : f(acc, a); return [next, [next]] })`." +effect/Stream#scanReduceEffect: + replacement: "Stream.mapAccumEffect" + note: "Removed; same first-element-as-seed emulation as scanReduce but with Stream.mapAccumEffect and an effectful step." +effect/Stream#scheduleWith: + replacement: "none" + note: "Removed; v4 Stream.schedule(schedule) only paces elements and never emits schedule outputs — the onElement/onSchedule unification is gone. Use Stream.schedule if only pacing is needed." +effect/Stream#scopedWith: + replacement: "Stream.scoped" + note: "Removed; v4 Stream.scoped scopes a Stream (provides a Scope kept open for the stream's lifetime). Emulate: `Stream.scoped(Stream.fromEffect(Effect.flatMap(Effect.scope, f)))` — Effect.scope accesses the ambient Scope." +effect/Stream#some: + replacement: "none" + note: "Removed along with Option error encodings. To drop None values use `Stream.filterMap(self, Filter.fromPredicateOption((o) => o))`; to fail on None use Stream.mapEffect with Option.match into Effect.fail/Effect.succeed." +effect/Stream#someOrElse: + replacement: "Stream.map" + note: "Removed; use `Stream.map(self, Option.getOrElse(() => fallback()))`." +effect/Stream#someOrFail: + replacement: "Stream.mapEffect" + note: "Removed; use `Stream.mapEffect(self, Option.match({ onNone: () => Effect.fail(error()), onSome: Effect.succeed }))`." +effect/Stream#splitOnChunk: + replacement: "none" + note: "Delimiter-subsequence splitting was removed; v4 keeps only Stream.split (predicate/refinement, emitting NonEmptyReadonlyArray segments) and Stream.splitLines. Hand-roll multi-element delimiter splitting with Stream.mapAccumArray." +effect/Stream#tapErrorCause: + replacement: "Stream.tapCause" + note: "Renamed; taps the full Cause on failure." +effect/Stream#timeoutFail: + replacement: "Stream.timeoutOrElse" + note: "Use `Stream.timeoutOrElse(self, { duration, orElse: () => Stream.fail(error()) })`; the timeout resets on every emitted value as before." +effect/Stream#timeoutFailCause: + replacement: "Stream.timeoutOrElse" + note: "Use `Stream.timeoutOrElse(self, { duration, orElse: () => Stream.failCause(cause()) })`." +effect/Stream#timeoutTo: + replacement: "Stream.timeoutOrElse" + note: "Renamed into an options form: `Stream.timeoutOrElse(self, { duration, orElse: () => that })` — the fallback stream is now lazy." +effect/Stream#toAsyncIterableRuntime: + replacement: "Stream.toAsyncIterableWith" + note: "Renamed; takes a `Context.Context` instead of a Runtime (v4 removed Runtime — a services Context is the execution environment). toAsyncIterable/toAsyncIterableEffect also still exist." +effect/Stream#toQueueOfElements: + replacement: "Stream.toQueue" + note: "The Exit>-per-element queue is gone; v4 toQueue(options: { capacity, strategy? }) returns Effect, never, R | Scope> — elements are plain values and failure/end arrive through the queue's error/done channel." +effect/Stream#toReadableStreamRuntime: + replacement: "Stream.toReadableStreamWith" + note: "Renamed; takes a `Context.Context` instead of a Runtime (v4 removed Runtime); options `{ strategy?: QueuingStrategy }` unchanged." +effect/Stream#transduce: + replacement: "Stream.transduce" + note: "Unchanged name and Sink-based shape; chunks are plain arrays in v4." +effect/Stream#unfoldChunk: + replacement: "Stream.paginate" + note: "Removed; v4 Stream.paginate(s, (s) => Effect<[ReadonlyArray, Option]>) is the array-emitting unfold — wrap the pure step in Effect.succeed; to end without emitting return `[[], Option.none()]`." +effect/Stream#unfoldChunkEffect: + replacement: "Stream.paginate" + note: "Removed; v4 Stream.paginate has the effectful array-step shape — map v3's Option<[Chunk, S]> result to `[array, Option]`, returning `[[], Option.none()]` to end without emitting." +effect/Stream#unfoldEffect: + replacement: "Stream.unfold" + note: "v4 Stream.unfold is effectful: `unfold(s, (s) => Effect)` — return the pair or `undefined` to end instead of Option." +effect/Stream#unwrapScoped: + replacement: "Stream.unwrap" + note: "v4 Stream.unwrap accepts scoped effects (`Exclude` built in); the scope stays open for the stream's lifetime — it replaces both unwrap and unwrapScoped." +effect/Stream#unwrapScopedWith: + replacement: "Stream.unwrap" + note: "Removed; access the ambient Scope explicitly: `Stream.unwrap(Effect.flatMap(Effect.scope, f))` — v4 unwrap keeps the scope open for the stream's lifetime." +effect/Stream#void: + replacement: "Stream.succeed(void 0)" + note: "The `Stream.void` constant (single void element) was removed; use `Stream.succeed(void 0)` or `Stream.make(void 0)`." +effect/Stream#whenCase: + replacement: "none" + note: "Removed; emulate with `Stream.suspend(() => Option.match(pf(evaluate()), { onNone: () => Stream.empty, onSome: (s) => s }))`." +effect/Stream#whenCaseEffect: + replacement: "none" + note: "Removed; emulate with `Stream.unwrap(Effect.map(self, (a) => Option.match(pf(a), { onNone: () => Stream.empty, onSome: (s) => s })))`." +effect/Stream#whenEffect: + replacement: "Stream.when" + note: "Folded into Stream.when, which now takes an `Effect` test directly (wrap a pure condition with Effect.sync)." +effect/Stream#zipAll: + replacement: "none" + note: "The entire zipAll family was removed in v4 (only zip/zipLatest/zipLatestAll exist; zip ends at the shorter side, zipLatest* combine latest values — different semantics). Pad-with-default zipping must be hand-rolled, e.g. with Stream.combineArray pulling both sides." +effect/Stream#zipAllLeft: + replacement: "none" + note: "Removed with the zipAll family; no default-padding zip exists in v4. Hand-roll with Stream.combineArray (or concat the remainder after a plain Stream.zipLeft) if needed." +effect/Stream#zipAllRight: + replacement: "none" + note: "Removed with the zipAll family; no default-padding zip exists in v4. Hand-roll with Stream.combineArray if needed." +effect/Stream#zipAllSortedByKey: + replacement: "none" + note: "Removed; the sorted-by-key merge-join family has no v4 equivalent (checked v4 Stream exports — only zip/zipLatest/zipLatestAll/zipWithArray). Hand-roll a keyed merge with Stream.combineArray." +effect/Stream#zipAllSortedByKeyLeft: + replacement: "none" + note: "Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray." +effect/Stream#zipAllSortedByKeyRight: + replacement: "none" + note: "Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray." +effect/Stream#zipAllSortedByKeyWith: + replacement: "none" + note: "Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray." +effect/Stream#zipAllWith: + replacement: "none" + note: "Removed with the zipAll family; v4 has no zip that pads the shorter side with defaults. Hand-roll with Stream.combineArray." +effect/Stream#zipWithChunks: + replacement: "Stream.zipWithArray" + note: "Renamed; the combiner now receives two NonEmptyReadonlyArrays and returns `[output: NonEmptyReadonlyArray, leftoverLeft: ReadonlyArray, leftoverRight: ReadonlyArray]` — the Either-wrapped leftover (ZipChunksResult) is replaced by the two explicit leftover arrays." diff --git a/.context/effect/migration/annotations/effect__StreamEmit.yaml b/.context/effect/migration/annotations/effect__StreamEmit.yaml new file mode 100644 index 000000000..6d76942ef --- /dev/null +++ b/.context/effect/migration/annotations/effect__StreamEmit.yaml @@ -0,0 +1,16 @@ +effect/StreamEmit#Emit: + replacement: "Queue.Queue" + note: "The StreamEmit module is gone; v4 Stream.callback hands the callback a Queue instead of an Emit function. Emit values with Queue.offer/Queue.offerAll, end with Queue.end, fail with Queue.fail/Queue.failCause." + example: | + // v3: Stream.async((emit) => { emit.single(1); emit.end() }) + Stream.callback((queue) => + Effect.gen(function*() { + yield* Queue.offer(queue, 1) + yield* Queue.end(queue) + })) +effect/StreamEmit#EmitOps: + replacement: "Queue.offer / Queue.offerAll / Queue.end / Queue.fail / Queue.failCause" + note: "Method-by-method mapping onto the Queue passed to Stream.callback: single(a) -> Queue.offer(queue, a); chunk(c) -> Queue.offerAll(queue, c); end() -> Queue.end(queue); fail(e) -> Queue.fail(queue, e); halt(cause) -> Queue.failCause(queue, cause); die(d)/dieMessage(m) -> Queue.failCause(queue, Cause.die(d)); done(exit) -> Queue.offer then Queue.end on success, Queue.failCause on failure; fromEffect(eff) -> run eff and offer its value (Effect.flatMap(eff, (a) => Queue.offer(queue, a)))." +effect/StreamEmit#EmitOpsPush: + replacement: "Queue.offerUnsafe / Queue.offerAllUnsafe / Queue.endUnsafe / Queue.failCauseUnsafe" + note: "The synchronous push interface of v3 Stream.asyncPush maps to the *Unsafe Queue operations on the Queue given to Stream.callback: single/array -> Queue.offerUnsafe/Queue.offerAllUnsafe, end -> Queue.endUnsafe, fail/halt/die -> Queue.failCauseUnsafe (wrap plain errors with Cause.fail, defects with Cause.die)." diff --git a/.context/effect/migration/annotations/effect__StreamHaltStrategy.yaml b/.context/effect/migration/annotations/effect__StreamHaltStrategy.yaml new file mode 100644 index 000000000..598ffa565 --- /dev/null +++ b/.context/effect/migration/annotations/effect__StreamHaltStrategy.yaml @@ -0,0 +1,44 @@ +effect/StreamHaltStrategy#HaltStrategy: + replacement: "Stream.HaltStrategy" + note: "The StreamHaltStrategy module is gone; v4 HaltStrategy is the string-literal union \"left\" | \"right\" | \"both\" | \"either\" (defined in Channel, re-exported as Stream.HaltStrategy) instead of tagged objects." +effect/StreamHaltStrategy#HaltStrategyInput: + replacement: "Stream.HaltStrategy" + note: "The Input widening (tagged object OR string) is obsolete; v4 only ever uses the string literals, so haltStrategy options take Stream.HaltStrategy directly." +effect/StreamHaltStrategy#Left: + replacement: "\"left\"" + note: "The tagged constructor is replaced by the plain string literal \"left\" passed directly to haltStrategy options." +effect/StreamHaltStrategy#Right: + replacement: "\"right\"" + note: "The tagged constructor is replaced by the plain string literal \"right\" passed directly to haltStrategy options." +effect/StreamHaltStrategy#Both: + replacement: "\"both\"" + note: "The tagged constructor is replaced by the plain string literal \"both\" passed directly to haltStrategy options." +effect/StreamHaltStrategy#Either: + replacement: "\"either\"" + note: "The tagged constructor is replaced by the plain string literal \"either\" passed directly to haltStrategy options." +effect/StreamHaltStrategy#fromInput: + replacement: "none" + note: "Remove the call; there is no conversion step in v4 because strategies already are the string literals, so pass the value through unchanged." +effect/StreamHaltStrategy#isLeft: + replacement: "strategy === \"left\"" + note: "Refinements on the tagged union become plain string comparison against the literal." +effect/StreamHaltStrategy#isRight: + replacement: "strategy === \"right\"" + note: "Refinements on the tagged union become plain string comparison against the literal." +effect/StreamHaltStrategy#isBoth: + replacement: "strategy === \"both\"" + note: "Refinements on the tagged union become plain string comparison against the literal." +effect/StreamHaltStrategy#isEither: + replacement: "strategy === \"either\"" + note: "Refinements on the tagged union become plain string comparison against the literal." +effect/StreamHaltStrategy#match: + replacement: "switch (strategy)" + note: "Fold over the strategy with an ordinary switch (or ternary chain) on the string literal; TypeScript exhaustiveness-checks the four cases." + example: | + // v3: HaltStrategy.match(s, { onLeft, onRight, onBoth, onEither }) + switch (strategy) { + case "left": return onLeft() + case "right": return onRight() + case "both": return onBoth() + case "either": return onEither() + } diff --git a/.context/effect/migration/annotations/effect__Streamable.yaml b/.context/effect/migration/annotations/effect__Streamable.yaml new file mode 100644 index 000000000..03e95d4b5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Streamable.yaml @@ -0,0 +1,3 @@ +"effect/Streamable": + replacement: "none" + note: "Removed in v4 with no direct replacement. Instead of extending Streamable.Class, expose the underlying stream as a value (e.g. a property or method built with Stream.suspend)." diff --git a/.context/effect/migration/annotations/effect__Struct.yaml b/.context/effect/migration/annotations/effect__Struct.yaml new file mode 100644 index 000000000..acdcf2042 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Struct.yaml @@ -0,0 +1,9 @@ +"effect/Struct#entries": + replacement: "Object.entries" + note: "Use the native helper, adding a cast when the old precise key and value type is required." +"effect/Struct#getEquivalence": + replacement: "Struct.makeEquivalence" + note: "Direct rename; the fields object call shape is unchanged." +"effect/Struct#getOrder": + replacement: "Struct.makeOrder" + note: "Direct rename; the fields object call shape is unchanged." diff --git a/.context/effect/migration/annotations/effect__Subscribable.yaml b/.context/effect/migration/annotations/effect__Subscribable.yaml new file mode 100644 index 000000000..94bb39b4a --- /dev/null +++ b/.context/effect/migration/annotations/effect__Subscribable.yaml @@ -0,0 +1,21 @@ +"effect/Subscribable#isSubscribable": + replacement: "none" + note: "The common brand was removed; use a concrete guard such as SubscriptionRef.isSubscriptionRef or an application structural guard." +"effect/Subscribable#make": + replacement: "object literal { get, changes }" + note: "No generic constructor remains; retain a local structural pair only when both the current read and change stream are needed." +"effect/Subscribable#map": + replacement: "Effect.map + Stream.map" + note: "For a retained get and changes pair, map the Effect and Stream separately." +"effect/Subscribable#mapEffect": + replacement: "Effect.flatMap + Stream.mapEffect" + note: "For a retained get and changes pair, flatMap the Effect and mapEffect the Stream separately." +"effect/Subscribable#Subscribable": + replacement: "custom { readonly get: Effect.Effect; readonly changes: Stream.Stream }" + note: "No renamed generic model exists; prefer concrete SubscriptionRef APIs or own this unbranded structural type locally." +"effect/Subscribable#TypeId": + replacement: "none" + note: "The Subscribable brand has no public replacement; use a concrete model guard or an application structural guard." +"effect/Subscribable#unwrap": + replacement: "Effect.flatMap + Stream.unwrap" + note: "Build get with Effect.flatMap and changes with Stream.unwrap; no single v4 helper remains." diff --git a/.context/effect/migration/annotations/effect__SubscriptionRef.yaml b/.context/effect/migration/annotations/effect__SubscriptionRef.yaml new file mode 100644 index 000000000..dbaf54439 --- /dev/null +++ b/.context/effect/migration/annotations/effect__SubscriptionRef.yaml @@ -0,0 +1,15 @@ +effect/SubscriptionRef#SubscriptionRef: + replacement: "SubscriptionRef.SubscriptionRef" + note: "The model remains but no longer extends SynchronizedRef or Subscribable; use SubscriptionRef.get and SubscriptionRef.changes explicitly." +effect/SubscriptionRef#SubscriptionRef.Variance: + replacement: "SubscriptionRef.SubscriptionRef.Variance" + note: "The marker remains under SubscriptionRef.SubscriptionRef, but its brand uses an internal type id." +effect/SubscriptionRef#SubscriptionRefTypeId: + replacement: "SubscriptionRef.isSubscriptionRef" + note: "The type id is internal in v4; use the public runtime guard instead." +effect/SubscriptionRef#SubscriptionRefUnify: + replacement: "none" + note: "SubscriptionRef is no longer an Effect subtype, so its unification helper was removed; call SubscriptionRef.get explicitly." +effect/SubscriptionRef#SubscriptionRefUnifyIgnore: + replacement: "none" + note: "SubscriptionRef is no longer a SynchronizedRef or Effect subtype, so its unification ignore marker was removed." diff --git a/.context/effect/migration/annotations/effect__Supervisor.yaml b/.context/effect/migration/annotations/effect__Supervisor.yaml new file mode 100644 index 000000000..3399df360 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Supervisor.yaml @@ -0,0 +1,27 @@ +effect/Supervisor#AbstractSupervisor: + replacement: "none" + note: "The ambient Supervisor abstraction and runtime event hooks were removed." +effect/Supervisor#addSupervisor: + replacement: "none" + note: "Layer-installed ambient supervision was removed; use structured concurrency and explicit FiberSet or FiberMap tracking." +effect/Supervisor#fibersIn: + replacement: "FiberSet" + note: "Use a scoped FiberSet and explicitly run or add fibers; it does not ambiently observe every descendant." +effect/Supervisor#fromEffect: + replacement: "none" + note: "The Supervisor abstraction and its effect-valued observation hook were removed." +effect/Supervisor#none: + replacement: "none" + note: "The Supervisor abstraction was removed; normal structured concurrency needs no no-op supervisor." +effect/Supervisor#Supervisor: + replacement: "none" + note: "Ambient fiber supervision was removed; use structured concurrency or explicit FiberSet and FiberMap tracking." +effect/Supervisor#Supervisor.Variance: + replacement: "none" + note: "The Supervisor abstraction and its variance marker were removed." +effect/Supervisor#SupervisorTypeId: + replacement: "none" + note: "The Supervisor abstraction and its type identifier were removed." +effect/Supervisor#unsafeTrack: + replacement: "FiberSet" + note: "Use scoped FiberSet.make and explicitly run or add fibers; there is no unsafe unscoped ambient tracker." diff --git a/.context/effect/migration/annotations/effect__Symbol.yaml b/.context/effect/migration/annotations/effect__Symbol.yaml new file mode 100644 index 000000000..1dbd674fe --- /dev/null +++ b/.context/effect/migration/annotations/effect__Symbol.yaml @@ -0,0 +1,3 @@ +"effect/Symbol#Equivalence": + replacement: "Equivalence.strictEqual()" + note: "The dedicated symbol instance was removed; it used strict equality." diff --git a/.context/effect/migration/annotations/effect__SynchronizedRef.yaml b/.context/effect/migration/annotations/effect__SynchronizedRef.yaml new file mode 100644 index 000000000..5fef8a050 --- /dev/null +++ b/.context/effect/migration/annotations/effect__SynchronizedRef.yaml @@ -0,0 +1,18 @@ +effect/SynchronizedRef#SynchronizedRef: + replacement: "SynchronizedRef.SynchronizedRef" + note: "The model remains, now extends the v4 Ref model, and is read or updated through explicit SynchronizedRef operations." +effect/SynchronizedRef#SynchronizedRef.Variance: + replacement: "Ref.Ref.Variance" + note: "SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker." +effect/SynchronizedRef#SynchronizedRefTypeId: + replacement: "none" + note: "The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly." +effect/SynchronizedRef#SynchronizedRefUnify: + replacement: "none" + note: "SynchronizedRef is no longer an Effect subtype, so its Effect unification helper was removed; call SynchronizedRef.get explicitly." +effect/SynchronizedRef#SynchronizedRefUnifyIgnore: + replacement: "none" + note: "SynchronizedRef is no longer an Effect subtype, so its Effect unification ignore marker was removed." +effect/SynchronizedRef#unsafeMake: + replacement: "SynchronizedRef.makeUnsafe" + note: "The unsafe suffix moved to the end." diff --git a/.context/effect/migration/annotations/effect__TArray.yaml b/.context/effect/migration/annotations/effect__TArray.yaml new file mode 100644 index 000000000..dd77149a5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TArray.yaml @@ -0,0 +1,123 @@ +effect/TArray#TArray: + replacement: "TxChunk.TxChunk" + note: "TArray has no direct v4 counterpart; TxChunk is the closest rewrite target but uses whole-Chunk operations." +effect/TArray#TArray.Variance: + replacement: "none" + note: "TArray was removed and TxChunk exposes no public variance marker." +effect/TArray#TArrayTypeId: + replacement: "TxChunk.isTxChunk" + note: "TArray and its public type id were removed; use the TxChunk runtime guard after rewriting the data structure." +effect/TArray#collectFirst: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#collectFirstSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#contains: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#count: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#countSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#empty: + replacement: "TxChunk.empty" + note: "TArray was removed; TxChunk is the closest v4 transactional indexed collection." +effect/TArray#everySTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#findFirstIndex: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findFirstIndexFrom: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findFirstIndexWhere: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findFirstIndexWhereFrom: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findFirstIndexWhereFromSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#findFirstIndexWhereSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#findFirstSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#findLast: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findLastIndex: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findLastIndexFrom: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#findLastSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#fromIterable: + replacement: "TxChunk.fromIterable" + note: "TArray was removed; construct the v4 TxChunk rewrite target from the iterable." +effect/TArray#get: + replacement: "Effect.map(TxChunk.get(self), Chunk.get(index))" + note: "TxChunk.get returns the whole Chunk, so apply Chunk.get to preserve indexed optional lookup." +effect/TArray#headOption: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#lastOption: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#make: + replacement: "TxChunk.fromIterable(elements)" + note: "TxChunk.make takes one Chunk rather than variadic elements; TxChunk.fromIterable preserves the old call shape after collecting arguments." +effect/TArray#maxOption: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#minOption: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#reduce: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#reduceOption: + replacement: "TxChunk.get + Chunk/Array operation" + note: "TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction." +effect/TArray#reduceOptionSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#reduceSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#size: + replacement: "TxChunk.size" + note: "TxChunk is the closest v4 rewrite target. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TArray#someSTM: + replacement: "Effect.tx + TxChunk.get + Effect traversal" + note: "TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction." +effect/TArray#toArray: + replacement: "Effect.map(TxChunk.get(self), Chunk.toArray)" + note: "TxChunk.get returns a Chunk; convert that snapshot to an Array explicitly." +effect/TArray#transform: + replacement: "TxChunk.update(self, Chunk.map(f))" + note: "TArray was removed; transform the whole TxChunk snapshot with a Chunk mapping function." +effect/TArray#transformSTM: + replacement: "Effect.tx + TxChunk.get/TxChunk.set" + note: "Read the snapshot, traverse it effectfully, and write the rebuilt Chunk within one Effect.tx transaction." +effect/TArray#update: + replacement: "TxChunk.modify" + note: "TxChunk updates the whole Chunk; use modify to update the indexed element and preserve the old optional-index behavior." +effect/TArray#updateSTM: + replacement: "Effect.tx + TxChunk.get/TxChunk.set" + note: "Read, effectfully update the indexed element, and write the rebuilt Chunk within one Effect.tx transaction." +effect/TArray#every: + replacement: "Effect.map(TxChunk.get(self), Chunk.every(predicate))" + note: "TArray was removed; read the TxChunk snapshot and test every element inside the surrounding Effect.tx transaction." +effect/TArray#some: + replacement: "Effect.map(TxChunk.get(self), Chunk.some(predicate))" + note: "TArray was removed; read the TxChunk snapshot and test for a matching element inside the surrounding Effect.tx transaction." diff --git a/.context/effect/migration/annotations/effect__TDeferred.yaml b/.context/effect/migration/annotations/effect__TDeferred.yaml new file mode 100644 index 000000000..8641fa49a --- /dev/null +++ b/.context/effect/migration/annotations/effect__TDeferred.yaml @@ -0,0 +1,15 @@ +effect/TDeferred#TDeferred: + replacement: "TxDeferred.TxDeferred" + note: "Rename the type and import from \"effect/TxDeferred\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TDeferred#TDeferred.Variance: + replacement: "none" + note: "TxDeferred exposes no public variance marker." +effect/TDeferred#TDeferredTypeId: + replacement: "TxDeferred.isTxDeferred" + note: "The type id is internal in v4; use the public runtime guard." +effect/TDeferred#await: + replacement: "TxDeferred.await" + note: "Import TxDeferred from \"effect/TxDeferred\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TDeferred#make: + replacement: "TxDeferred.make" + note: "Import TxDeferred from \"effect/TxDeferred\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." diff --git a/.context/effect/migration/annotations/effect__TMap.yaml b/.context/effect/migration/annotations/effect__TMap.yaml new file mode 100644 index 000000000..1bb30050d --- /dev/null +++ b/.context/effect/migration/annotations/effect__TMap.yaml @@ -0,0 +1,99 @@ +effect/TMap#TMap: + replacement: "TxHashMap.TxHashMap" + note: "Rename the type and import from \"effect/TxHashMap\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#TMap.Variance: + replacement: "none" + note: "TxHashMap exposes no public variance marker." +effect/TMap#TMapTypeId: + replacement: "TxHashMap.isTxHashMap" + note: "The type id is internal in v4; use the public runtime guard." +effect/TMap#empty: + replacement: "TxHashMap.empty" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#findAllSTM: + replacement: "TxHashMap.entries + Effect traversal" + note: "No effectful mapped-find helper remains; traverse the entry snapshot explicitly inside Effect.tx." +effect/TMap#findSTM: + replacement: "TxHashMap.entries + Effect.findFirst" + note: "No effectful mapped-find helper remains; traverse entries explicitly inside Effect.tx." +effect/TMap#fromIterable: + replacement: "TxHashMap.fromIterable" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#get: + replacement: "TxHashMap.get" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#getOrElse: + replacement: "Effect.map(TxHashMap.get(self, key), Option.getOrElse(fallback))" + note: "Compose the retained optional get operation with Option.getOrElse." +effect/TMap#has: + replacement: "TxHashMap.has" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#isEmpty: + replacement: "TxHashMap.isEmpty" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#keys: + replacement: "TxHashMap.keys" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#make: + replacement: "TxHashMap.make" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#reduce: + replacement: "TxHashMap.reduce" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#reduceSTM: + replacement: "TxHashMap.entries + Effect.reduce" + note: "Snapshot entries and reduce them effectfully inside the surrounding Effect.tx transaction." +effect/TMap#remove: + replacement: "TxHashMap.remove" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#removeAll: + replacement: "TxHashMap.removeMany" + note: "The bulk removal operation was renamed." +effect/TMap#setIfAbsent: + replacement: "Effect.tx + TxHashMap.get/TxHashMap.set" + note: "No direct helper remains; check and conditionally set under one outer transaction." +effect/TMap#size: + replacement: "TxHashMap.size" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TMap#takeFirst: + replacement: "none" + note: "No atomic take-and-match helper exists in TxHashMap; implement explicit selection and removal inside Effect.tx." +effect/TMap#takeFirstSTM: + replacement: "none" + note: "No effectful atomic take-and-match helper exists; implement explicit traversal and removal inside Effect.tx." +effect/TMap#takeSome: + replacement: "none" + note: "No atomic multi-take helper exists in TxHashMap; implement explicit selection and removals inside Effect.tx." +effect/TMap#takeSomeSTM: + replacement: "none" + note: "No effectful atomic multi-take helper exists; implement explicit traversal and removals inside Effect.tx." +effect/TMap#toArray: + replacement: "TxHashMap.entries" + note: "Use the entry snapshot; it replaces the old array conversion." +effect/TMap#toChunk: + replacement: "Effect.map(TxHashMap.entries(self), Chunk.fromIterable)" + note: "Convert the entry snapshot to Chunk explicitly." +effect/TMap#toHashMap: + replacement: "TxHashMap.snapshot" + note: "The immutable HashMap snapshot operation was renamed." +effect/TMap#toMap: + replacement: "Effect.map(TxHashMap.entries(self), (entries) => new Map(entries))" + note: "Build a JavaScript Map from the entry snapshot." +effect/TMap#transform: + replacement: "TxHashMap.map" + note: "V4 map returns a new map rather than mutating self; key-changing transforms require snapshot and rebuild logic." +effect/TMap#transformSTM: + replacement: "TxHashMap.entries + Effect traversal + TxHashMap.fromIterable" + note: "No in-place effectful transform remains; traverse a snapshot and rebuild inside Effect.tx." +effect/TMap#transformValues: + replacement: "TxHashMap.map" + note: "V4 map transforms values but returns a new map rather than mutating self." +effect/TMap#transformValuesSTM: + replacement: "TxHashMap.entries + Effect traversal + TxHashMap.fromIterable" + note: "No effectful map remains; traverse a snapshot and rebuild inside Effect.tx." +effect/TMap#updateWith: + replacement: "TxHashMap.modifyAt" + note: "modifyAt is the closest atomic keyed update, but returns void; preserve any old return value explicitly if needed." +effect/TMap#values: + replacement: "TxHashMap.values" + note: "Import TxHashMap from \"effect/TxHashMap\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." diff --git a/.context/effect/migration/annotations/effect__TPriorityQueue.yaml b/.context/effect/migration/annotations/effect__TPriorityQueue.yaml new file mode 100644 index 000000000..cf9205645 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TPriorityQueue.yaml @@ -0,0 +1,42 @@ +effect/TPriorityQueue#TPriorityQueue: + replacement: "TxPriorityQueue.TxPriorityQueue" + note: "Rename the type and import from \"effect/TxPriorityQueue\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#TPriorityQueue.Variance: + replacement: "none" + note: "TxPriorityQueue exposes no public variance marker." +effect/TPriorityQueue#TPriorityQueueTypeId: + replacement: "TxPriorityQueue.isTxPriorityQueue" + note: "The type id is internal in v4; use the public runtime guard." +effect/TPriorityQueue#empty: + replacement: "TxPriorityQueue.empty" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#fromIterable: + replacement: "TxPriorityQueue.fromIterable" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#isEmpty: + replacement: "TxPriorityQueue.isEmpty" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#isNonEmpty: + replacement: "TxPriorityQueue.isNonEmpty" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#make: + replacement: "TxPriorityQueue.make" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#peek: + replacement: "TxPriorityQueue.peek" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#size: + replacement: "TxPriorityQueue.size" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#toArray: + replacement: "TxPriorityQueue.toArray" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#toChunk: + replacement: "Effect.map(TxPriorityQueue.toArray(self), Chunk.fromIterable)" + note: "The direct Chunk conversion was removed; convert the retained Array snapshot explicitly." +effect/TPriorityQueue#take: + replacement: "TxPriorityQueue.take" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; the operation now returns an ordinary Effect, so compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPriorityQueue#takeAll: + replacement: "TxPriorityQueue.takeAll" + note: "Import TxPriorityQueue from \"effect/TxPriorityQueue\"; it returns an ordinary Effect containing the priority-ordered Array." diff --git a/.context/effect/migration/annotations/effect__TPubSub.yaml b/.context/effect/migration/annotations/effect__TPubSub.yaml new file mode 100644 index 000000000..487f84c0b --- /dev/null +++ b/.context/effect/migration/annotations/effect__TPubSub.yaml @@ -0,0 +1,39 @@ +effect/TPubSub#TPubSub: + replacement: "TxPubSub.TxPubSub" + note: "Rename the type and import from \"effect/TxPubSub\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#TPubSubTypeId: + replacement: "TxPubSub.isTxPubSub" + note: "The type id is internal in v4; use the public runtime guard." +effect/TPubSub#bounded: + replacement: "TxPubSub.bounded" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#capacity: + replacement: "TxPubSub.capacity" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#dropping: + replacement: "TxPubSub.dropping" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#isEmpty: + replacement: "TxPubSub.isEmpty" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#isFull: + replacement: "TxPubSub.isFull" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#shutdown: + replacement: "TxPubSub.shutdown" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#size: + replacement: "TxPubSub.size" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#sliding: + replacement: "TxPubSub.sliding" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#subscribeScoped: + replacement: "TxPubSub.subscribe" + note: "The scoped subscription constructor lost its Scoped suffix; it still requires Scope and returns a TxQueue." +effect/TPubSub#unbounded: + replacement: "TxPubSub.unbounded" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TPubSub#isShutdown: + replacement: "TxPubSub.isShutdown" + note: "Import TxPubSub from \"effect/TxPubSub\"; the operation now returns an ordinary Effect." diff --git a/.context/effect/migration/annotations/effect__TQueue.yaml b/.context/effect/migration/annotations/effect__TQueue.yaml new file mode 100644 index 000000000..ef7de6f07 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TQueue.yaml @@ -0,0 +1,87 @@ +effect/TQueue#BaseTQueue: + replacement: "TxQueue.TxQueueState" + note: "The shared queue state model was renamed and now includes the richer open, closing, and done lifecycle." +effect/TQueue#TDequeue: + replacement: "TxQueue.TxDequeue" + note: "Rename the read-side type; it now carries an error channel." +effect/TQueue#TDequeueTypeId: + replacement: "TxQueue.isTxDequeue" + note: "The type id is internal in v4; use the public runtime guard." +effect/TQueue#TEnqueue: + replacement: "TxQueue.TxEnqueue" + note: "Rename the write-side type; it now carries an error channel." +effect/TQueue#TEnqueueTypeId: + replacement: "TxQueue.isTxEnqueue" + note: "The type id is internal in v4; use the public runtime guard." +effect/TQueue#TQueue: + replacement: "TxQueue.TxQueue" + note: "Rename the type; it now carries an error channel and completion lifecycle. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#TQueue.TDequeueVariance: + replacement: "TxQueue.TxDequeue.Variance" + note: "The read-side variance marker moved under TxDequeue and now includes the error type." +effect/TQueue#TQueue.TEnqueueVariance: + replacement: "TxQueue.TxEnqueue.Variance" + note: "The write-side variance marker moved under TxEnqueue and now includes the error type." +effect/TQueue#bounded: + replacement: "TxQueue.bounded" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#capacity: + replacement: "queue.capacity" + note: "Capacity is now a property on TxQueue handles rather than a module function." +effect/TQueue#dropping: + replacement: "TxQueue.dropping" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#isEmpty: + replacement: "TxQueue.isEmpty" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#isFull: + replacement: "TxQueue.isFull" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#isTDequeue: + replacement: "TxQueue.isTxDequeue" + note: "The runtime guard was renamed with the TxDequeue type." +effect/TQueue#isTEnqueue: + replacement: "TxQueue.isTxEnqueue" + note: "The runtime guard was renamed with the TxEnqueue type." +effect/TQueue#isTQueue: + replacement: "TxQueue.isTxQueue" + note: "The runtime guard was renamed with the TxQueue type." +effect/TQueue#offerAll: + replacement: "TxQueue.offerAll" + note: "The operation remains, but now returns rejected elements rather than a boolean." +effect/TQueue#peek: + replacement: "TxQueue.peek" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#poll: + replacement: "TxQueue.poll" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#seek: + replacement: "none" + note: "TxQueue has no seek helper; repeat TxQueue.take under Effect.tx until the predicate matches." +effect/TQueue#shutdown: + replacement: "TxQueue.shutdown" + note: "The operation remains, but now returns whether shutdown changed the queue state." +effect/TQueue#size: + replacement: "TxQueue.size" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#sliding: + replacement: "TxQueue.sliding" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#take: + replacement: "TxQueue.take" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#takeBetween: + replacement: "TxQueue.takeBetween" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#takeN: + replacement: "TxQueue.takeN" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#unbounded: + replacement: "TxQueue.unbounded" + note: "Import TxQueue from \"effect/TxQueue\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TQueue#isShutdown: + replacement: "TxQueue.isShutdown" + note: "Import TxQueue from \"effect/TxQueue\"; it checks the richer done lifecycle and returns an ordinary Effect." +effect/TQueue#takeAll: + replacement: "TxQueue.takeAll" + note: "The operation now blocks until at least one item is available, returns a NonEmptyArray, and propagates the queue error channel through an ordinary Effect." diff --git a/.context/effect/migration/annotations/effect__TRandom.yaml b/.context/effect/migration/annotations/effect__TRandom.yaml new file mode 100644 index 000000000..3bc6b1955 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TRandom.yaml @@ -0,0 +1,27 @@ +effect/TRandom#TRandom: + replacement: "none" + note: "The transactional random service was deliberately removed; use Random outside retried transactions where possible." +effect/TRandom#TRandomTypeId: + replacement: "none" + note: "TRandom and its public type id were removed; v4 has no TxRandom module." +effect/TRandom#Tag: + replacement: "Random.Random" + note: "Use the v4 Random Context.Reference; the transactional random service was removed." +effect/TRandom#next: + replacement: "Random.next" + note: "TxRandom was removed. Random.next is an ordinary Effect and may be re-executed if used inside a retried transaction." +effect/TRandom#nextBoolean: + replacement: "Random.nextBoolean" + note: "TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry." +effect/TRandom#nextInt: + replacement: "Random.nextInt" + note: "TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry." +effect/TRandom#nextIntBetween: + replacement: "Random.nextIntBetween(low, high, { halfOpen: true })" + note: "TxRandom was removed; request half-open bounds explicitly to preserve the v3 range behavior." +effect/TRandom#nextRange: + replacement: "Random.nextBetween" + note: "The operation was renamed and is no longer backed by rollback-safe transactional random state." +effect/TRandom#shuffle: + replacement: "Random.shuffle" + note: "TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry." diff --git a/.context/effect/migration/annotations/effect__TReentrantLock.yaml b/.context/effect/migration/annotations/effect__TReentrantLock.yaml new file mode 100644 index 000000000..399bcd9a1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TReentrantLock.yaml @@ -0,0 +1,21 @@ +effect/TReentrantLock#TReentrantLock: + replacement: "TxReentrantLock.TxReentrantLock" + note: "Rename the type and import from \"effect/TxReentrantLock\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TReentrantLock#TReentrantLock.Proto: + replacement: "none" + note: "The public prototype interface was removed." +effect/TReentrantLock#TReentrantLockTypeId: + replacement: "TxReentrantLock.isTxReentrantLock" + note: "The type id is internal in v4; use the public runtime guard." +effect/TReentrantLock#fiberReadLocks: + replacement: "none" + note: "Per-fiber read-lock counts were removed; TxReentrantLock.readLocks reports only the total count." +effect/TReentrantLock#fiberWriteLocks: + replacement: "none" + note: "Per-fiber write-lock counts were removed; TxReentrantLock.writeLocks reports only the total count." +effect/TReentrantLock#lock: + replacement: "TxReentrantLock.writeLock" + note: "The generic lock helper was renamed to make write-lock acquisition explicit." +effect/TReentrantLock#make: + replacement: "TxReentrantLock.make()" + note: "The constructor keeps its name but is now a function call rather than a constant STM value." diff --git a/.context/effect/migration/annotations/effect__TRef.yaml b/.context/effect/migration/annotations/effect__TRef.yaml new file mode 100644 index 000000000..eed0c896c --- /dev/null +++ b/.context/effect/migration/annotations/effect__TRef.yaml @@ -0,0 +1,30 @@ +effect/TRef#TRef: + replacement: "TxRef.TxRef" + note: "Rename the type and import from \"effect/TxRef\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TRef#TRef.Variance: + replacement: "none" + note: "TxRef exposes no public variance marker." +effect/TRef#TRefTypeId: + replacement: "TxRef.isTxRef" + note: "The type id is internal in v4; use the public runtime guard." +effect/TRef#get: + replacement: "TxRef.get" + note: "Import TxRef from \"effect/TxRef\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TRef#getAndUpdateSome: + replacement: "TxRef.modify" + note: "Use one atomic modify and keep the old value when the partial update returns None." +effect/TRef#make: + replacement: "TxRef.make" + note: "Import TxRef from \"effect/TxRef\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TRef#modifySome: + replacement: "TxRef.modify" + note: "Use one atomic modify and return the fallback result when the partial function returns None." +effect/TRef#setAndGet: + replacement: "TxRef.modify" + note: "Use one atomic modify that returns and stores the new value." +effect/TRef#updateSome: + replacement: "TxRef.modify" + note: "Use one atomic modify and retain the old value when the partial update returns None." +effect/TRef#updateSomeAndGet: + replacement: "TxRef.modify" + note: "Use one atomic modify that returns the resulting value, retaining the old value for None." diff --git a/.context/effect/migration/annotations/effect__TSemaphore.yaml b/.context/effect/migration/annotations/effect__TSemaphore.yaml new file mode 100644 index 000000000..ac4becb2b --- /dev/null +++ b/.context/effect/migration/annotations/effect__TSemaphore.yaml @@ -0,0 +1,30 @@ +effect/TSemaphore#TSemaphore: + replacement: "TxSemaphore.TxSemaphore" + note: "Rename the type and import from \"effect/TxSemaphore\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSemaphore#TSemaphore.Proto: + replacement: "none" + note: "The public prototype interface was removed." +effect/TSemaphore#TSemaphoreTypeId: + replacement: "TxSemaphore.isTxSemaphore" + note: "The type id is internal in v4; use the public runtime guard." +effect/TSemaphore#available: + replacement: "TxSemaphore.available" + note: "Import TxSemaphore from \"effect/TxSemaphore\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSemaphore#make: + replacement: "TxSemaphore.make" + note: "Import TxSemaphore from \"effect/TxSemaphore\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSemaphore#release: + replacement: "TxSemaphore.release" + note: "Import TxSemaphore from \"effect/TxSemaphore\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSemaphore#unsafeMake: + replacement: "none" + note: "The unsafe constructor was removed; use TxSemaphore.make." +effect/TSemaphore#withPermit: + replacement: "TxSemaphore.withPermit" + note: "The helper remains, but data-first calls now pass the semaphore before the Effect." +effect/TSemaphore#withPermits: + replacement: "TxSemaphore.withPermits" + note: "The helper remains, but data-first calls now pass semaphore, permit count, then Effect." +effect/TSemaphore#withPermitsScoped: + replacement: "TxSemaphore.acquireN + Effect.addFinalizer(TxSemaphore.releaseN)" + note: "No scoped multi-permit helper remains; acquire and register release explicitly in a Scope." diff --git a/.context/effect/migration/annotations/effect__TSet.yaml b/.context/effect/migration/annotations/effect__TSet.yaml new file mode 100644 index 000000000..51d649588 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TSet.yaml @@ -0,0 +1,75 @@ +effect/TSet#TSet: + replacement: "TxHashSet.TxHashSet" + note: "Rename the type and import from \"effect/TxHashSet\". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#TSet.Variance: + replacement: "none" + note: "TxHashSet exposes no public variance marker." +effect/TSet#TSetTypeId: + replacement: "TxHashSet.isTxHashSet" + note: "The type id is internal in v4; use the public runtime guard." +effect/TSet#difference: + replacement: "TxHashSet.difference" + note: "The name remains, but v4 returns a new set instead of mutating self." +effect/TSet#empty: + replacement: "TxHashSet.empty" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#fromIterable: + replacement: "TxHashSet.fromIterable" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#has: + replacement: "TxHashSet.has" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#intersection: + replacement: "TxHashSet.intersection" + note: "The name remains, but v4 returns a new set instead of mutating self." +effect/TSet#isEmpty: + replacement: "TxHashSet.isEmpty" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#make: + replacement: "TxHashSet.make" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#reduce: + replacement: "TxHashSet.reduce" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#reduceSTM: + replacement: "TxHashSet.toHashSet + Effect.reduce" + note: "Snapshot the set and reduce effectfully inside the surrounding Effect.tx transaction." +effect/TSet#remove: + replacement: "TxHashSet.remove" + note: "The name remains, but v4 returns whether the value existed." +effect/TSet#removeAll: + replacement: "Effect.forEach(values, (value) => TxHashSet.remove(self, value))" + note: "No bulk removal helper remains; remove each value inside one outer Effect.tx transaction." +effect/TSet#size: + replacement: "TxHashSet.size" + note: "Import TxHashSet from \"effect/TxHashSet\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSet#takeFirst: + replacement: "none" + note: "No atomic take-and-match helper exists in TxHashSet; select and remove explicitly inside Effect.tx." +effect/TSet#takeFirstSTM: + replacement: "none" + note: "No effectful atomic take-and-match helper exists; traverse and remove explicitly inside Effect.tx." +effect/TSet#takeSome: + replacement: "none" + note: "No atomic multi-take helper exists in TxHashSet; select and remove explicitly inside Effect.tx." +effect/TSet#takeSomeSTM: + replacement: "none" + note: "No effectful atomic multi-take helper exists; traverse and remove explicitly inside Effect.tx." +effect/TSet#toArray: + replacement: "Effect.map(TxHashSet.toHashSet(self), Array.from)" + note: "Convert the immutable HashSet snapshot to an Array explicitly." +effect/TSet#toChunk: + replacement: "Effect.map(TxHashSet.toHashSet(self), (set) => Chunk.fromIterable(set))" + note: "Convert the immutable HashSet snapshot to Chunk explicitly." +effect/TSet#toReadonlySet: + replacement: "Effect.map(TxHashSet.toHashSet(self), (set) => new Set(set))" + note: "Convert the immutable HashSet snapshot to a JavaScript ReadonlySet explicitly." +effect/TSet#transform: + replacement: "TxHashSet.map" + note: "The closest helper returns a new set instead of mutating self." +effect/TSet#transformSTM: + replacement: "TxHashSet.toHashSet + Effect traversal + TxHashSet.fromIterable" + note: "No effectful transform remains; traverse a snapshot and rebuild inside Effect.tx." +effect/TSet#union: + replacement: "TxHashSet.union" + note: "The name remains, but v4 returns a new set instead of mutating self." diff --git a/.context/effect/migration/annotations/effect__TSubscriptionRef.yaml b/.context/effect/migration/annotations/effect__TSubscriptionRef.yaml new file mode 100644 index 000000000..ccb654ec9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TSubscriptionRef.yaml @@ -0,0 +1,36 @@ +effect/TSubscriptionRef#TSubscriptionRef: + replacement: "TxSubscriptionRef.TxSubscriptionRef" + note: "Rename the type; it no longer extends TxRef. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSubscriptionRef#TSubscriptionRef.Variance: + replacement: "none" + note: "TxSubscriptionRef exposes no public variance marker." +effect/TSubscriptionRef#TSubscriptionRefTypeId: + replacement: "TxSubscriptionRef.isTxSubscriptionRef" + note: "The type id is internal in v4; use the public runtime guard." +effect/TSubscriptionRef#changes: + replacement: "none" + note: "The old unscoped transactional subscription was removed; use scoped TxSubscriptionRef.changes." +effect/TSubscriptionRef#changesScoped: + replacement: "TxSubscriptionRef.changes" + note: "The scoped changes operation lost its Scoped suffix and returns a scoped TxQueue." +effect/TSubscriptionRef#get: + replacement: "TxSubscriptionRef.get" + note: "Import TxSubscriptionRef from \"effect/TxSubscriptionRef\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSubscriptionRef#getAndUpdateSome: + replacement: "TxSubscriptionRef.modify" + note: "Use one atomic modify so successful updates are still published; retain the old value for None." +effect/TSubscriptionRef#make: + replacement: "TxSubscriptionRef.make" + note: "Import TxSubscriptionRef from \"effect/TxSubscriptionRef\"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic." +effect/TSubscriptionRef#modifySome: + replacement: "TxSubscriptionRef.modify" + note: "Use one atomic modify so successful updates are still published; use the fallback result for None." +effect/TSubscriptionRef#setAndGet: + replacement: "TxSubscriptionRef.modify" + note: "Use one atomic modify that publishes and returns the newly stored value." +effect/TSubscriptionRef#updateSome: + replacement: "TxSubscriptionRef.modify" + note: "Use one atomic modify so updates are published, retaining the old value for None." +effect/TSubscriptionRef#updateSomeAndGet: + replacement: "TxSubscriptionRef.modify" + note: "Use one atomic modify that publishes and returns the resulting value, retaining the old value for None." diff --git a/.context/effect/migration/annotations/effect__Take.yaml b/.context/effect/migration/annotations/effect__Take.yaml new file mode 100644 index 000000000..0c8cfe1ac --- /dev/null +++ b/.context/effect/migration/annotations/effect__Take.yaml @@ -0,0 +1,67 @@ +effect/Take#Take: + replacement: "Take.Take" + note: "v4 Take is the plain union NonEmptyReadonlyArray | Exit.Exit — no wrapper object or Pipeable: a value batch is a non-empty array, a failure is a failed Exit, and end-of-stream is a successful Exit carrying the Done value (void by default). The module keeps the effect/Take path but exports only the type and toPull." +effect/Take#Take.Variance: + replacement: "none" + note: "Variance plumbing removed; v4 Take is a plain union type with no branded interface, so there is nothing to migrate to." +effect/Take#TakeTypeId: + replacement: "none" + note: "No brand symbol in v4; discriminate the union with Exit.isExit(take) (Exit branch) vs the non-empty array branch (Array.isReadonlyArrayNonEmpty)." +effect/Take#chunk: + replacement: "NonEmptyReadonlyArray" + note: "No constructor needed: a value-batch Take is just the non-empty array of values itself (convert a v3 Chunk with Array.fromIterable); empty batches are not representable and must be skipped." +effect/Take#dieMessage: + replacement: "Exit.die(new Error(message))" + note: "A defect Take is a died Exit; wrap the message in an Error yourself since there is no dedicated dieMessage helper." +effect/Take#done: + replacement: "Take.toPull" + note: "Take.toPull(take) converts a Take into a Pull (Effect succeeding with the batch); end-of-stream surfaces as Cause.Done in the error channel instead of v3's Option.none, and failures keep their cause." +effect/Take#fail: + replacement: "Exit.fail" + note: "A failing Take is simply the failed Exit: Exit.fail(error)." +effect/Take#failCause: + replacement: "Exit.failCause" + note: "A failing Take with a full cause is simply Exit.failCause(cause)." +effect/Take#fromEffect: + replacement: "Effect.exit + Exit.isSuccess" + note: "Run the effect with Effect.exit and convert the result: a successful exit value a becomes the single-element batch [a], a failed exit is used directly as the Take." + example: "Effect.map(Effect.exit(effect), (exit) => Exit.isSuccess(exit) ? [exit.value] as const : exit)" +effect/Take#fromExit: + replacement: "Exit.isSuccess(exit) ? [exit.value] : exit" + note: "A success exit becomes the single-element batch [a]; a failure exit is already a valid v4 Take and is used as-is." +effect/Take#fromPull: + replacement: "Effect.matchCause + Pull.doneExitFromCause" + note: "Convert one v4 Pull step into a Take: the success batch is the Take itself, and Pull.doneExitFromCause turns the failure cause into the Exit branch (Cause.Done becomes a successful end Exit, real failures become a failed Exit)." + example: "Effect.matchCause(pull, { onSuccess: (arr) => arr, onFailure: Pull.doneExitFromCause })" +effect/Take#isDone: + replacement: "Exit.isExit(take) && Exit.isSuccess(take)" + note: "End-of-stream is the successful-Exit branch of the union." +effect/Take#isFailure: + replacement: "Exit.isExit(take) && Exit.isFailure(take)" + note: "A failure Take is the failed-Exit branch of the union." +effect/Take#isSuccess: + replacement: "!Exit.isExit(take)" + note: "A value batch is the non-Exit branch; use Array.isReadonlyArrayNonEmpty(take) when a positive refinement to NonEmptyReadonlyArray is needed." +effect/Take#make: + replacement: "none" + note: "No wrapper constructor: build the union value directly — a non-empty array for values, Exit.fail/Exit.failCause for errors, Exit.succeed(done) (or Exit.void) for end-of-stream; the v3 Exit, Option> encoding is gone." +effect/Take#map: + replacement: "Exit.isExit(take) ? take : Array.map(take, f)" + note: "Only the value batch is mapped; effect's Array.map preserves the NonEmptyReadonlyArray type, and Exit branches (failure/end) pass through unchanged." +effect/Take#match: + replacement: "Exit.isExit + Exit.match" + note: "Branch on the union: the array branch is v3's onSuccess(chunk), and Exit.match splits the Exit branch into onFailure(cause) and end-of-stream (v3 onEnd, success value = Done)." + example: | + // v3: Take.match(take, { onEnd, onFailure, onSuccess }) + Exit.isExit(take) + ? Exit.match(take, { onSuccess: () => onEnd(), onFailure: (cause) => onFailure(cause) }) + : onSuccess(take) +effect/Take#matchEffect: + replacement: "Pull.matchEffect(Take.toPull(take), { onSuccess, onFailure, onDone })" + note: "Convert with Take.toPull and fold with Pull.matchEffect: onSuccess receives the batch (v3 onSuccess), onFailure the cause, onDone the completion value (v3 onEnd); alternatively branch manually with Exit.isExit as for match." +effect/Take#of: + replacement: "[value]" + note: "A single-value Take is just the one-element non-empty array literal." +effect/Take#tap: + replacement: "Exit.isExit(take) ? Exit.asVoid(take) : Effect.asVoid(f(take))" + note: "Peek at the value batch with f; Exit branches pass through as effects (a failed Exit re-propagates its cause, an end Exit becomes a void success), matching v3 tap semantics." diff --git a/.context/effect/migration/annotations/effect__TestAnnotation.yaml b/.context/effect/migration/annotations/effect__TestAnnotation.yaml new file mode 100644 index 000000000..b8d69241d --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestAnnotation.yaml @@ -0,0 +1,3 @@ +effect/TestAnnotation: + replacement: none + note: The legacy test-runner annotation key and built-in counters were removed. Use Vitest skip/repeat/retry options for runner concerns and FiberSet for explicit fiber tracking; there is no annotation-key equivalent. diff --git a/.context/effect/migration/annotations/effect__TestAnnotationMap.yaml b/.context/effect/migration/annotations/effect__TestAnnotationMap.yaml new file mode 100644 index 000000000..407cb77ea --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestAnnotationMap.yaml @@ -0,0 +1,3 @@ +effect/TestAnnotationMap: + replacement: none + note: TestAnnotationMap was removed with TestAnnotation. Use an application-owned HashMap or Ref only when arbitrary typed annotations are still required; it is not part of the v4 test runner. diff --git a/.context/effect/migration/annotations/effect__TestAnnotations.yaml b/.context/effect/migration/annotations/effect__TestAnnotations.yaml new file mode 100644 index 000000000..2faf243d5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestAnnotations.yaml @@ -0,0 +1,3 @@ +effect/TestAnnotations: + replacement: none + note: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. diff --git a/.context/effect/migration/annotations/effect__TestClock.yaml b/.context/effect/migration/annotations/effect__TestClock.yaml new file mode 100644 index 000000000..e95f504bd --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestClock.yaml @@ -0,0 +1,24 @@ +"effect/TestClock#adjustWith": + replacement: "Effect.zipWith(effect, TestClock.adjust(duration), (result) => result, { concurrent: true })" + note: "V4 removed adjustWith. Run the tested effect and clock adjustment concurrently and retain the tested effect's result." +"effect/TestClock#currentTimeMillis": + replacement: "Clock.currentTimeMillis" + note: "Read time from the active Clock reference; under it.effect or TestClock.layer() this is virtual time." +"effect/TestClock#Data": + replacement: "TestClock.TestClock.State" + note: "The nearest state model is State, with timestamp and a private latch-based sleep queue. V4 exposes no full state getter or setter." +"effect/TestClock#defaultTestClock": + replacement: "TestClock.layer()" + note: "The v4 layer creates an epoch-based test clock and captures the surrounding live Clock automatically; it no longer needs TestAnnotations or TestLive." +"effect/TestClock#makeData": + replacement: "TestClock.layer() + TestClock.setTime(instant)" + note: "State injection was removed. Build the layer, then set initial time; seeded pending sleeps cannot migrate because the queue is private." +"effect/TestClock#save": + replacement: "none" + note: "Full clock snapshots including pending sleeps are no longer public. For timestamp-only restoration, read Clock.currentTimeMillis and later call TestClock.setTime(savedMillis)." +"effect/TestClock#sleeps": + replacement: "none" + note: "The pending-sleep queue is private. Test observable behavior by forking sleepers, adjusting time, and joining or asserting the fibers." +"effect/TestClock#testClock": + replacement: "TestClock.testClockWith(Effect.succeed)" + note: "V4 exposes callback-based access to the active test clock; use testClockWith directly when possible." diff --git a/.context/effect/migration/annotations/effect__TestConfig.yaml b/.context/effect/migration/annotations/effect__TestConfig.yaml new file mode 100644 index 000000000..39470247c --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestConfig.yaml @@ -0,0 +1,6 @@ +"effect/TestConfig#make": + replacement: "{ repeats, retries, samples, shrinks }" + note: "The v3 constructor only returned its parameter object. The TestConfig service was removed; keep a plain object only for application-owned configuration." +"effect/TestConfig#TestConfig": + replacement: "none" + note: "There is no v4 TestConfig service. Move runner settings to Vitest and FastCheck options, or define an application-specific Context.Reference if runtime access is needed." diff --git a/.context/effect/migration/annotations/effect__TestContext.yaml b/.context/effect/migration/annotations/effect__TestContext.yaml new file mode 100644 index 000000000..6738380cb --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestContext.yaml @@ -0,0 +1,6 @@ +"effect/TestContext#LiveContext": + replacement: "@effect/vitest#live" + note: "Default runtime references are live in v4. Use it.live for a whole live test; no LiveContext layer is required." +"effect/TestContext#TestContext": + replacement: "Layer.mergeAll(TestConsole.layer, TestClock.layer())" + note: "This is the v4 test layer used by @effect/vitest. Prefer it.effect, which provides it automatically." diff --git a/.context/effect/migration/annotations/effect__TestLive.yaml b/.context/effect/migration/annotations/effect__TestLive.yaml new file mode 100644 index 000000000..e12b07c39 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestLive.yaml @@ -0,0 +1,9 @@ +"effect/TestLive#make": + replacement: "Effect.provideContext" + note: "The wrapper was removed. Apply a captured Context directly with Effect.provideContext; for live time inside it.effect, prefer TestClock.withLive." +"effect/TestLive#TestLive": + replacement: "none" + note: "There is no grouped live-default-services object. Use Context.Context plus Effect.provideContext, TestClock.withLive for live time, or it.live for the whole test." +"effect/TestLive#TestLiveTypeId": + replacement: "none" + note: "The TestLive nominal wrapper was removed, so its type id has no replacement." diff --git a/.context/effect/migration/annotations/effect__TestServices.yaml b/.context/effect/migration/annotations/effect__TestServices.yaml new file mode 100644 index 000000000..231075734 --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestServices.yaml @@ -0,0 +1,96 @@ +"effect/TestServices#annotate": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#annotations": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#annotationsLayer": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#annotationsWith": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#get": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#supervisedFibers": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#withAnnotations": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#withAnnotationsScoped": + replacement: "none" + note: "The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking." +"effect/TestServices#currentServices": + replacement: "Effect.context()" + note: "The separate FiberRef> was removed. Test services now live in the ordinary Effect Context; override individual references with Effect.provideService." +"effect/TestServices#liveLayer": + replacement: "none" + note: "The standalone TestLive service and layer were removed. TestClock.layer() captures its surrounding live Clock itself." +"effect/TestServices#liveServices": + replacement: "none" + note: "There is no prebuilt aggregate test-service Context. @effect/vitest constructs TestClock and TestConsole layers per test; live references are defaults." +"effect/TestServices#liveWith": + replacement: "TestClock.withLive" + note: "There is no TestLive callback object. Refactor to the effect ultimately run and apply TestClock.withLive, or use it.live for whole-test live execution." +"effect/TestServices#provideLive": + replacement: "TestClock.withLive" + note: "For live time, run the effect with the Clock captured by TestClock.layer(). Use it.live when the entire test should omit all test-service overrides." +"effect/TestServices#provideWithLive": + replacement: "TestClock.testClockWith + TestClock.withLive + Effect.provideService" + note: "To retain test time for the inner effect while its transformer uses live time, combine testClockWith, withLive, and provideService. Other v3 default services have no aggregate equivalent." +"effect/TestServices#repeats": + replacement: "Vitest TestOptions.repeats" + note: "Configure repeats in the Vitest options passed to it.effect or it.live; it is no longer an Effect service value." +"effect/TestServices#retries": + replacement: "Vitest TestOptions.retry" + note: "Configure retry in Vitest test options. To retry an Effect inside a test, use Effect.retry." +"effect/TestServices#samples": + replacement: "{ fastCheck: { numRuns } }" + note: "Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { fastCheck: { numRuns: samples } })." +"effect/TestServices#shrinks": + replacement: "none" + note: "The legacy maximum-shrinks service setting was removed; @effect/vitest forwards FastCheck.Parameters, which has no equivalent service value." +"effect/TestServices#size": + replacement: "CurrentSize" + note: "Define a custom Context.Reference and yield it to read the current size." +"effect/TestServices#sized": + replacement: "CurrentSize" + note: "TestSized was removed. Use a custom Context.Reference directly instead of a wrapper object." +"effect/TestServices#sizedLayer": + replacement: "Layer.succeed(CurrentSize, size)" + note: "Provide the custom size reference as a layer." +"effect/TestServices#sizedWith": + replacement: "CurrentSize.use" + note: "Use the custom reference's callback, or preferably yield CurrentSize in Effect.gen." +"effect/TestServices#testConfig": + replacement: "none" + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." +"effect/TestServices#testConfigLayer": + replacement: "none" + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." +"effect/TestServices#testConfigWith": + replacement: "none" + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." +"effect/TestServices#withTestConfig": + replacement: "none" + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." +"effect/TestServices#withTestConfigScoped": + replacement: "none" + note: "The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference." +"effect/TestServices#TestServices": + replacement: "TestClock.TestClock | TestConsole.TestConsole" + note: "This is the v4 @effect/vitest test-environment union. it.effect provides both automatically; it.live provides neither override." +"effect/TestServices#withLiveScoped": + replacement: "none" + note: "There is no scoped TestLive service override. Apply TestClock.withLive to a specific effect, or choose it.live at test declaration time." +"effect/TestServices#withSize": + replacement: "Effect.provideService(effect, CurrentSize, size)" + note: "Provide a custom size Context.Reference for the duration of the wrapped effect." +"effect/TestServices#withSized": + replacement: "Effect.provideService(effect, CurrentSize, size)" + note: "Collapse the old TestSized wrapper to its numeric value and provide the custom reference." +"effect/TestServices#withSizedScoped": + replacement: "Effect.updateServiceScoped(CurrentSize, () => size)" + note: "For a scope-bounded override use updateServiceScoped; otherwise prefer wrapping the workflow with Effect.provideService." diff --git a/.context/effect/migration/annotations/effect__TestSized.yaml b/.context/effect/migration/annotations/effect__TestSized.yaml new file mode 100644 index 000000000..0e146dd2e --- /dev/null +++ b/.context/effect/migration/annotations/effect__TestSized.yaml @@ -0,0 +1,12 @@ +"effect/TestSized#fromFiberRef": + replacement: "Context.Reference" + note: "FiberRef and TestSized were removed. Define one stable Context.Reference instead of wrapping a FiberRef." +"effect/TestSized#make": + replacement: "Context.Reference" + note: "Define a module-level reference with defaultValue; do not create a fresh key at each call site." +"effect/TestSized#TestSized": + replacement: "Context.Reference" + note: "Collapse the wrapper service to the reference itself; yield the reference to read the current size." +"effect/TestSized#TestSizedTypeId": + replacement: "none" + note: "The wrapper's nominal type id is unnecessary; Context.Reference supplies stable key identity." diff --git a/.context/effect/migration/annotations/effect__Tracer.yaml b/.context/effect/migration/annotations/effect__Tracer.yaml new file mode 100644 index 000000000..e14547f37 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Tracer.yaml @@ -0,0 +1,27 @@ +effect/Tracer#DisablePropagation: + replacement: "Tracer.DisablePropagation" + note: "Keep the reference value. The separate phantom interface is gone; the Context.Reference directly stores boolean." +effect/Tracer#ExternalSpan: + replacement: "Tracer.ExternalSpan" + note: "Keep the type, but rename the context field to annotations. Apply the same rename to Tracer.externalSpan options." +effect/Tracer#ParentSpan: + replacement: "Tracer.ParentSpan" + note: "Keep the API. It is now a Context.Service class for AnySpan rather than a separate phantom interface plus Context.Tag." +effect/Tracer#Span: + replacement: "Tracer.Span" + note: "Keep the type and rename span.context to span.annotations; the other public fields and methods remain." +effect/Tracer#SpanLink: + replacement: "Tracer.SpanLink" + note: "Keep the type but remove the _tag property; v4 links contain only span and attributes." +effect/Tracer#SpanOptions: + replacement: "Tracer.SpanOptions" + note: "Keep the type and rename context to annotations. V4 splits trace options and additionally accepts sampled and level." +effect/Tracer#Tracer: + replacement: "Tracer.Tracer" + note: "The service is now a defaulted Context.Reference. Custom implementations are structural and receive one span options object; context is optional and now receives an Effect primitive plus Fiber." +effect/Tracer#TracerTypeId: + replacement: "none" + note: "Tracer implementations are structural and no longer carry a public type-id brand." +effect/Tracer#tracerWith: + replacement: "Tracer.Tracer.use" + note: "Replace tracerWith(f) with Tracer.Tracer.use(f); do not use TracerKey, which is only the raw string key." diff --git a/.context/effect/migration/annotations/effect__Trie.yaml b/.context/effect/migration/annotations/effect__Trie.yaml new file mode 100644 index 000000000..38b40cb2c --- /dev/null +++ b/.context/effect/migration/annotations/effect__Trie.yaml @@ -0,0 +1,6 @@ +"effect/Trie#TypeId": + replacement: "none" + note: "The Trie brand is private and there is no public Trie runtime guard; use Trie.Trie in type positions." +"effect/Trie#unsafeGet": + replacement: "Trie.getUnsafe" + note: "Direct word-order rename; it still throws for a missing key." diff --git a/.context/effect/migration/annotations/effect__Tuple.yaml b/.context/effect/migration/annotations/effect__Tuple.yaml new file mode 100644 index 000000000..89ec37e3d --- /dev/null +++ b/.context/effect/migration/annotations/effect__Tuple.yaml @@ -0,0 +1,30 @@ +"effect/Tuple#at": + replacement: "Tuple.get" + note: "Renamed for indexed access; v4 constrains the index to a valid tuple position." +"effect/Tuple#getEquivalence": + replacement: "Tuple.makeEquivalence" + note: "Pass equivalences as one array instead of variadic arguments." +"effect/Tuple#getFirst": + replacement: "Tuple.get(0)" + note: "Use Tuple.get(self, 0), or Tuple.get(0) in a pipe." +"effect/Tuple#getOrder": + replacement: "Tuple.makeOrder" + note: "Pass orders as one array instead of variadic arguments." +"effect/Tuple#getSecond": + replacement: "Tuple.get(1)" + note: "Use Tuple.get(self, 1), or Tuple.get(1) in a pipe." +"effect/Tuple#mapBoth": + replacement: "Tuple.evolve" + note: "Use Tuple.evolve(self, [options.onFirst, options.onSecond])." +"effect/Tuple#mapFirst": + replacement: "Tuple.evolve" + note: "Use Tuple.evolve(self, [f]); unspecified positions are preserved." +"effect/Tuple#mapSecond": + replacement: "Tuple.evolve" + note: "Use Tuple.evolve(self, [undefined, f]); undefined preserves the first position." +"effect/Tuple#swap": + replacement: "Tuple.renameIndices" + note: "Swap a pair with Tuple.renameIndices(self, [\"1\", \"0\"])." +"effect/Tuple#TupleTypeLambda": + replacement: "none" + note: "Removed with tuple Bicovariant support; use the concrete tuple type or a local HKT TypeLambda." diff --git a/.context/effect/migration/annotations/effect__Types.yaml b/.context/effect/migration/annotations/effect__Types.yaml new file mode 100644 index 000000000..cbd7331fa --- /dev/null +++ b/.context/effect/migration/annotations/effect__Types.yaml @@ -0,0 +1,24 @@ +"effect/Types#Concurrency": + replacement: "Types.Concurrency" + note: "Still exported, but v4 removes inherit; replace it with an explicit number or unbounded." +"effect/Types#Contravariant": + replacement: "Types.Contravariant" + note: "Unchanged contravariant type helper." +"effect/Types#Covariant": + replacement: "Types.Covariant" + note: "Unchanged covariant type helper." +"effect/Types#Ctor": + replacement: "new (...args: Array) => T" + note: "The named alias was removed; inline the construct signature or define a local alias." +"effect/Types#Invariant": + replacement: "Types.Invariant" + note: "Unchanged invariant type helper." +"effect/Types#MatchRecord": + replacement: "{} extends S ? onTrue : onFalse" + note: "The alias was removed; inline its conditional because Types.VoidIfEmpty has different optional-record behavior." +"effect/Types#MergeRecord": + replacement: "Types.MergeLeft" + note: "MergeRecord was an alias for the retained left-biased MergeLeft helper." +"effect/Types#NoExcessProperties": + replacement: "Types.NoExcessProperties" + note: "Retained with equivalent excess-key checking." diff --git a/.context/effect/migration/annotations/effect__UpstreamPullRequest.yaml b/.context/effect/migration/annotations/effect__UpstreamPullRequest.yaml new file mode 100644 index 000000000..31a3882a9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__UpstreamPullRequest.yaml @@ -0,0 +1,3 @@ +effect/UpstreamPullRequest: + replacement: none + note: Removed with Channel.concatMapWithCustom; v4 does not expose channel-executor pull-request events. Use supported flattening operators or implement exceptional behavior with Channel.fromTransform and Pull. diff --git a/.context/effect/migration/annotations/effect__UpstreamPullStrategy.yaml b/.context/effect/migration/annotations/effect__UpstreamPullStrategy.yaml new file mode 100644 index 000000000..af0ea8399 --- /dev/null +++ b/.context/effect/migration/annotations/effect__UpstreamPullStrategy.yaml @@ -0,0 +1,3 @@ +effect/UpstreamPullStrategy: + replacement: none + note: Removed with Channel.concatMapWithCustom. Select flattening and scheduling through Channel.flatMap, Channel.switchMap, or Channel.mergeAll; v4 has no upstream-pull strategy ADT. diff --git a/.context/effect/migration/annotations/effect__Utils.yaml b/.context/effect/migration/annotations/effect__Utils.yaml new file mode 100644 index 000000000..40387ec46 --- /dev/null +++ b/.context/effect/migration/annotations/effect__Utils.yaml @@ -0,0 +1,60 @@ +"effect/Utils#adapter": + replacement: "none" + note: "Remove the adapter and resume parameter; v4 generators yield yieldable values directly." +"effect/Utils#Adapter": + replacement: "none" + note: "The generator-adapter type was removed; type generator bodies to yield v4 yieldable values directly." +"effect/Utils#Gen": + replacement: "Utils.Gen" + note: "Still exported; drop the adapter type and resume parameter, then yield yieldable Kind values directly." +"effect/Utils#GenKind": + replacement: "none" + note: "The adapter wrapper was removed; custom yieldable Kinds should implement Symbol.iterator and return Utils.SingleShotGen." +"effect/Utils#GenKindImpl": + replacement: "none" + note: "The wrapper implementation was removed; implement direct yieldability with Symbol.iterator and Utils.SingleShotGen." +"effect/Utils#GenKindTypeId": + replacement: "none" + note: "The GenKind runtime marker was removed with the wrapper infrastructure." +"effect/Utils#internalCall": + replacement: "none" + note: "This was internal and has no public replacement; application code should invoke its thunk directly." +"effect/Utils#isGeneratorFunction": + replacement: "none" + note: "The unused constructor-identity predicate was removed; accept an explicit generator contract instead." +"effect/Utils#isGenKind": + replacement: "none" + note: "Removed with GenKind; v4 generator drivers consume directly yielded values." +"effect/Utils#makeGenKind": + replacement: "none" + note: "The wrapper constructor was removed; make custom Kinds yieldable with Symbol.iterator and Utils.SingleShotGen." +"effect/Utils#OptionalNumber": + replacement: "number | null | undefined" + note: "The unused named alias was removed; inline its union." +"effect/Utils#PCGRandom": + replacement: "Random.withSeed + Random.next / Random.nextIntBetween" + note: "Use the effectful Random service for seeded generation; v4 is not PCG-compatible." +"effect/Utils#PCGRandomState": + replacement: "none" + note: "No public PCG state snapshot or restore API remains; Random.withSeed is reproducible but not state-compatible." +"effect/Utils#SingleShotGen": + replacement: "Utils.SingleShotGen" + note: "Still exported; v4 removes its concrete return and throw methods, so do not call those optional iterator hooks." +"effect/Utils#structuralRegion": + replacement: "none" + note: "Remove the wrapper because v4 Equal.equals is structural by default; use a custom Equivalence for custom comparison." +"effect/Utils#structuralRegionState": + replacement: "none" + note: "The mutable test hook was removed; v4 equality is structural by default." +"effect/Utils#Variance": + replacement: "Utils.Variance" + note: "Still exported; remove the v3 GenKindTypeId marker from implementations." +"effect/Utils#YieldWrap": + replacement: "none" + note: "The internal generator transport wrapper was removed; yieldable values are yielded directly." +"effect/Utils#yieldWrapGet": + replacement: "none" + note: "The internal unwrapper was removed; generator drivers read the directly yielded value." +"effect/Utils#YieldWrapTypeId": + replacement: "none" + note: "The internal wrapper marker was removed with YieldWrap." diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock.yaml new file mode 100644 index 000000000..e9b3b9352 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock": + replacement: "none" + note: "The @effect/ai-amazon-bedrock provider package was removed from v4 with no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration." diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml new file mode 100644 index 000000000..626bc2eb3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockClient.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/AmazonBedrockClient": + replacement: none + note: The @effect/ai-amazon-bedrock provider package was removed from v4, so AmazonBedrockClient, layer, layerConfig, make, and Service have no direct replacements. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml new file mode 100644 index 000000000..9c49ec99f --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockConfig.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/AmazonBedrockConfig": + replacement: none + note: The @effect/ai-amazon-bedrock provider package was removed from v4, so AmazonBedrockConfig has no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml new file mode 100644 index 000000000..bc1365d26 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockLanguageModel.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel": + replacement: none + note: The @effect/ai-amazon-bedrock language-model integration was removed from v4. Use another supported v4 provider or implement LanguageModel.LanguageModel with @aws-sdk/client-bedrock-runtime. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml new file mode 100644 index 000000000..c55353c86 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockSchema.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/AmazonBedrockSchema": + replacement: none + note: The @effect/ai-amazon-bedrock package was removed from v4, including its hand-written Bedrock schemas. Use @aws-sdk/client-bedrock-runtime request and response types, or schemas supplied by a custom v4 provider integration. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml new file mode 100644 index 000000000..bc5ed3ea7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__AmazonBedrockTool.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/AmazonBedrockTool": + replacement: none + note: The @effect/ai-amazon-bedrock package and its Anthropic-on-Bedrock provider tools were removed from v4. Recreate the capability in a custom provider integration if the Bedrock model still requires it. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml new file mode 100644 index 000000000..7553bfbb2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__EventStreamEncoding.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/EventStreamEncoding": + replacement: none + note: The @effect/ai-amazon-bedrock package and its AWS event-stream decoder were removed from v4. Use the AWS SDK's Bedrock Runtime streaming support or implement decoding in a custom provider client. diff --git a/.context/effect/migration/annotations/effect__ai-amazon-bedrock__index.yaml b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__index.yaml new file mode 100644 index 000000000..d0ecd9e74 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-amazon-bedrock__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai-amazon-bedrock/index": + replacement: "none" + note: "The @effect/ai-amazon-bedrock provider package was removed from v4 with no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml new file mode 100644 index 000000000..6fa9c33d3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicClient.yaml @@ -0,0 +1,54 @@ +"@effect/ai-anthropic/AnthropicClient#CitationsDelta": + replacement: "Generated.BetaCitationsDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#ContentBlockDeltaEvent": + replacement: "Generated.BetaContentBlockDeltaEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#ContentBlockStartEvent": + replacement: "Generated.BetaContentBlockStartEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#ContentBlockStopEvent": + replacement: "Generated.BetaContentBlockStopEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#ErrorEvent": + replacement: "Generated.BetaErrorResponse" + note: "The client-local stream error schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#InputJsonContentBlockDelta": + replacement: "Generated.BetaInputJsonContentBlockDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#MessageDelta": + replacement: "Generated.BetaMessageDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#MessageDeltaEvent": + replacement: "Generated.BetaMessageDeltaEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#MessageDeltaUsage": + replacement: "typeof Generated.BetaMessageDeltaEvent.Type[\"usage\"]" + note: "The standalone usage schema was inlined into the regenerated v4 message-delta event." +"@effect/ai-anthropic/AnthropicClient#MessageStartEvent": + replacement: "Generated.BetaMessageStartEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#MessageStopEvent": + replacement: "Generated.BetaMessageStopEvent" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#MessageStreamEvent": + replacement: "AnthropicClient.MessageStreamEvent" + note: "Still exported in v4 as a type union of generated beta stream events; adapt to the revised client stream contract." +"@effect/ai-anthropic/AnthropicClient#PingEvent": + replacement: "none" + note: "The v4 client consumes ping events internally and filters them from MessageStreamEvent, so no public ping schema is needed." +"@effect/ai-anthropic/AnthropicClient#ServerToolUsage": + replacement: "Generated.BetaServerToolUsage" + note: "The client-local usage schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#Service": + replacement: "AnthropicClient.Service" + note: "Still exported in v4; adapt to the revised generated client, streamRequest, and message response contracts." +"@effect/ai-anthropic/AnthropicClient#SignatureContentBlockDelta": + replacement: "Generated.BetaSignatureContentBlockDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#TextContentBlockDelta": + replacement: "Generated.BetaTextContentBlockDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." +"@effect/ai-anthropic/AnthropicClient#ThinkingContentBlockDelta": + replacement: "Generated.BetaThinkingContentBlockDelta" + note: "The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml new file mode 100644 index 000000000..068f85afe --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicConfig.yaml @@ -0,0 +1,6 @@ +"@effect/ai-anthropic/AnthropicConfig#AnthropicConfig": + replacement: "AnthropicConfig.AnthropicConfig" + note: "Still exported in v4; update imports and adapt to the revised v4 service and HTTP client types." +"@effect/ai-anthropic/AnthropicConfig#AnthropicConfig.Service": + replacement: "AnthropicConfig.AnthropicConfig.Service" + note: "Still exported in v4; update imports and adapt to the revised v4 service and HTTP client types." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml new file mode 100644 index 000000000..d624ede50 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicLanguageModel.yaml @@ -0,0 +1,21 @@ +"@effect/ai-anthropic/AnthropicLanguageModel#AnthropicReasoningInfo": + replacement: "Prompt.ReasoningPartOptions / Response reasoning metadata" + note: "The standalone reasoning-info union was removed; v4 declares Anthropic thinking and redacted-thinking data directly on Prompt and Response provider metadata." +"@effect/ai-anthropic/AnthropicLanguageModel#AnthropicTools": + replacement: "AnthropicLanguageModel.AnthropicUserDefinedTool | AnthropicLanguageModel.AnthropicProviderDefinedTool" + note: "The old combined tool union was split into explicit user-defined and provider-defined Anthropic request tool types." +"@effect/ai-anthropic/AnthropicLanguageModel#Config": + replacement: "AnthropicLanguageModel.Config" + note: "Still exported in v4; update imports and adapt to the revised Messages API request fields." +"@effect/ai-anthropic/AnthropicLanguageModel#Config.Service": + replacement: "AnthropicLanguageModel.Config.Service" + note: "Still exported in v4; update imports and adapt to the revised Messages API request fields." +"@effect/ai-anthropic/AnthropicLanguageModel#layerWithTokenizer": + replacement: "AnthropicLanguageModel.layer" + note: "The tokenizer-combining layer was removed; provide the language model and any Tokenizer service separately." +"@effect/ai-anthropic/AnthropicLanguageModel#modelWithTokenizer": + replacement: "AnthropicLanguageModel.model" + note: "The tokenizer-combining model was removed; use the v4 model descriptor and provide any Tokenizer service separately." +"@effect/ai-anthropic/AnthropicLanguageModel#prepareTools": + replacement: "none" + note: "Tool conversion became an internal part of the v4 Anthropic language model; use AnthropicTool constructors and pass tools through LanguageModel provider options instead." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml new file mode 100644 index 000000000..a2cb45a97 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTokenizer.yaml @@ -0,0 +1,6 @@ +"@effect/ai-anthropic/AnthropicTokenizer#layer": + replacement: "Tokenizer.make" + note: "The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using @anthropic-ai/tokenizer if equivalent Anthropic counting is required." +"@effect/ai-anthropic/AnthropicTokenizer#make": + replacement: "Tokenizer.make" + note: "The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using @anthropic-ai/tokenizer if equivalent Anthropic counting is required." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml new file mode 100644 index 000000000..5cf62d776 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__AnthropicTool.yaml @@ -0,0 +1,6 @@ +"@effect/ai-anthropic/AnthropicTool#getProviderDefinedToolName": + replacement: "Tool.NameMapper" + note: "The Anthropic-specific name lookup was removed; v4 provider tools carry custom and provider names through the shared Tool.NameMapper." +"@effect/ai-anthropic/AnthropicTool#ProviderDefinedTools": + replacement: "AnthropicTool.AnthropicTool" + note: "The provider-defined schema union was replaced by the union of v4 Anthropic provider tool constructor return types." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__Generated.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__Generated.yaml new file mode 100644 index 000000000..76e5ff293 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__Generated.yaml @@ -0,0 +1,1023 @@ +"@effect/ai-anthropic/Generated#APIError": + replacement: "Generated.APIError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#AuthenticationError": + replacement: "Generated.AuthenticationError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Base64ImageSource": + replacement: "Generated.Base64ImageSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Base64ImageSourceMediaType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#Base64PDFSource": + replacement: "Generated.Base64PDFSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BashTool20250124": + replacement: "Generated.BashTool_20250124" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaAPIError": + replacement: "Generated.BetaAPIError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaapiSchemasSkillsSkill": + replacement: "Generated.Betaapi__schemas__skills__Skill" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaAuthenticationError": + replacement: "Generated.BetaAuthenticationError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBase64ImageSource": + replacement: "Generated.BetaBase64ImageSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBase64ImageSourceMediaType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaBase64PDFSource": + replacement: "Generated.BetaBase64PDFSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBashCodeExecutionToolResultErrorCode": + replacement: "Generated.BetaBashCodeExecutionToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBashTool20241022": + replacement: "Generated.BetaBashTool_20241022" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBashTool20250124": + replacement: "Generated.BetaBashTool_20250124" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBillingError": + replacement: "Generated.BetaBillingError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBodyCreateSkillV1SkillsPost": + replacement: "Generated.BetaBody_create_skill_v1_skills_post" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaBodyCreateSkillVersionV1SkillsSkillIdVersionsPost": + replacement: "Generated.BetaBody_create_skill_version_v1_skills__skill_id__versions_post" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCacheControlEphemeral": + replacement: "Generated.BetaCacheControlEphemeral" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCacheControlEphemeralTtl": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaCacheCreation": + replacement: "Generated.BetaCacheCreation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaClearToolUses20250919": + replacement: "Generated.BetaClearToolUses20250919" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCodeExecutionTool20250522": + replacement: "Generated.BetaCodeExecutionTool_20250522" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCodeExecutionTool20250825": + replacement: "Generated.BetaCodeExecutionTool_20250825" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCodeExecutionToolResultErrorCode": + replacement: "Generated.BetaCodeExecutionToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaComputerUseTool20241022": + replacement: "Generated.BetaComputerUseTool_20241022" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaComputerUseTool20250124": + replacement: "Generated.BetaComputerUseTool_20250124" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContainer": + replacement: "Generated.BetaContainer" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContainerParams": + replacement: "Generated.BetaContainerParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContentBlock": + replacement: "Generated.BetaContentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContentBlockSource": + replacement: "Generated.BetaContentBlockSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContextManagementConfig": + replacement: "Generated.BetaContextManagementConfig" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaContextManagementResponse": + replacement: "Generated.BetaContextManagementResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCountMessageTokensParams": + replacement: "Generated.BetaCountMessageTokensParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCountMessageTokensResponse": + replacement: "Generated.BetaCountMessageTokensResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateMessageBatchParams": + replacement: "Generated.BetaCreateMessageBatchParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateMessageParams": + replacement: "Generated.BetaCreateMessageParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateMessageParamsServiceTier": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaCreateSkillResponse": + replacement: "Generated.BetaCreateSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateSkillV1SkillsPostParams": + replacement: "Generated.BetaCreateSkillV1SkillsPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateSkillVersionResponse": + replacement: "Generated.BetaCreateSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams": + replacement: "Generated.BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteFileV1FilesFileIdDeleteParams": + replacement: "Generated.BetaDeleteFileV1FilesFileIdDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteMessageBatchResponse": + replacement: "Generated.BetaDeleteMessageBatchResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteSkillResponse": + replacement: "Generated.BetaDeleteSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteSkillV1SkillsSkillIdDeleteParams": + replacement: "Generated.BetaDeleteSkillV1SkillsSkillIdDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteSkillVersionResponse": + replacement: "Generated.BetaDeleteSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams": + replacement: "Generated.BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaDownloadFileV1FilesFileIdContentGetParams": + replacement: "Generated.BetaDownloadFileV1FilesFileIdContentGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaErrorResponse": + replacement: "Generated.BetaErrorResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaFileDeleteResponse": + replacement: "Generated.BetaFileDeleteResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaFileDocumentSource": + replacement: "Generated.BetaFileDocumentSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaFileImageSource": + replacement: "Generated.BetaFileImageSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaFileListResponse": + replacement: "Generated.BetaFileListResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaFileMetadataSchema": + replacement: "Generated.BetaFileMetadataSchema" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGatewayTimeoutError": + replacement: "Generated.BetaGatewayTimeoutError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGetFileMetadataV1FilesFileIdGetParams": + replacement: "Generated.BetaGetFileMetadataV1FilesFileIdGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGetSkillResponse": + replacement: "Generated.BetaGetSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGetSkillV1SkillsSkillIdGetParams": + replacement: "Generated.BetaGetSkillV1SkillsSkillIdGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGetSkillVersionResponse": + replacement: "Generated.BetaGetSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams": + replacement: "Generated.BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaInputContentBlock": + replacement: "Generated.BetaInputContentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaInputMessage": + replacement: "Generated.BetaInputMessage" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaInputMessageRole": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaInputSchema": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaInputTokensClearAtLeast": + replacement: "Generated.BetaInputTokensClearAtLeast" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaInputTokensTrigger": + replacement: "Generated.BetaInputTokensTrigger" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaInvalidRequestError": + replacement: "Generated.BetaInvalidRequestError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListFilesV1FilesGetParams": + replacement: "Generated.BetaListFilesV1FilesGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListResponseMessageBatch": + replacement: "Generated.BetaListResponse_MessageBatch_" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListResponseModelInfo": + replacement: "Generated.BetaListResponse_ModelInfo_" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListSkillsResponse": + replacement: "Generated.BetaListSkillsResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListSkillsV1SkillsGetParams": + replacement: "Generated.BetaListSkillsV1SkillsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListSkillVersionsResponse": + replacement: "Generated.BetaListSkillVersionsResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams": + replacement: "Generated.BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMemoryTool20250818": + replacement: "Generated.BetaMemoryTool_20250818" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessage": + replacement: "Generated.BetaMessage" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatch": + replacement: "Generated.BetaMessageBatch" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesCancelParams": + replacement: "Generated.BetaMessageBatchesCancelParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesDeleteParams": + replacement: "Generated.BetaMessageBatchesDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesListParams": + replacement: "Generated.BetaMessageBatchesListParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesPostParams": + replacement: "Generated.BetaMessageBatchesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesResultsParams": + replacement: "Generated.BetaMessageBatchesResultsParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchesRetrieveParams": + replacement: "Generated.BetaMessageBatchesRetrieveParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchIndividualRequestParams": + replacement: "Generated.BetaMessageBatchIndividualRequestParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessageBatchProcessingStatus": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaMessagesCountTokensPostParams": + replacement: "Generated.BetaMessagesCountTokensPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMessagesPostParams": + replacement: "Generated.BetaMessagesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaMetadata": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaModelInfo": + replacement: "Generated.BetaModelInfo" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaModelsGetParams": + replacement: "Generated.BetaModelsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaModelsListParams": + replacement: "Generated.BetaModelsListParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaNotFoundError": + replacement: "Generated.BetaNotFoundError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaOverloadedError": + replacement: "Generated.BetaOverloadedError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaPermissionError": + replacement: "Generated.BetaPermissionError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaPlainTextSource": + replacement: "Generated.BetaPlainTextSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRateLimitError": + replacement: "Generated.BetaRateLimitError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestBashCodeExecutionOutputBlock": + replacement: "Generated.BetaRequestBashCodeExecutionOutputBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestBashCodeExecutionResultBlock": + replacement: "Generated.BetaRequestBashCodeExecutionResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestBashCodeExecutionToolResultBlock": + replacement: "Generated.BetaRequestBashCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestBashCodeExecutionToolResultError": + replacement: "Generated.BetaRequestBashCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCharLocationCitation": + replacement: "Generated.BetaRequestCharLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCitationsConfig": + replacement: "Generated.BetaRequestCitationsConfig" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCodeExecutionOutputBlock": + replacement: "Generated.BetaRequestCodeExecutionOutputBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCodeExecutionResultBlock": + replacement: "Generated.BetaRequestCodeExecutionResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCodeExecutionToolResultBlock": + replacement: "Generated.BetaRequestCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCodeExecutionToolResultError": + replacement: "Generated.BetaRequestCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestContainerUploadBlock": + replacement: "Generated.BetaRequestContainerUploadBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestContentBlockLocationCitation": + replacement: "Generated.BetaRequestContentBlockLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestCounts": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestDocumentBlock": + replacement: "Generated.BetaRequestDocumentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestImageBlock": + replacement: "Generated.BetaRequestImageBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestMCPServerToolConfiguration": + replacement: "Generated.BetaRequestMCPServerToolConfiguration" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestMCPServerURLDefinition": + replacement: "Generated.BetaRequestMCPServerURLDefinition" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestMCPToolResultBlock": + replacement: "Generated.BetaRequestMCPToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestMCPToolUseBlock": + replacement: "Generated.BetaRequestMCPToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestPageLocationCitation": + replacement: "Generated.BetaRequestPageLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestRedactedThinkingBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestSearchResultBlock": + replacement: "Generated.BetaRequestSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestSearchResultLocationCitation": + replacement: "Generated.BetaRequestSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestServerToolUseBlock": + replacement: "Generated.BetaRequestServerToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestServerToolUseBlockName": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestTextBlock": + replacement: "Generated.BetaRequestTextBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionCreateResultBlock": + replacement: "Generated.BetaRequestTextEditorCodeExecutionCreateResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionStrReplaceResultBlock": + replacement: "Generated.BetaRequestTextEditorCodeExecutionStrReplaceResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionToolResultBlock": + replacement: "Generated.BetaRequestTextEditorCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionToolResultError": + replacement: "Generated.BetaRequestTextEditorCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionViewResultBlock": + replacement: "Generated.BetaRequestTextEditorCodeExecutionViewResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestTextEditorCodeExecutionViewResultBlockFileType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestThinkingBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestToolResultBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestToolUseBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaRequestWebFetchResultBlock": + replacement: "Generated.BetaRequestWebFetchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebFetchToolResultBlock": + replacement: "Generated.BetaRequestWebFetchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebFetchToolResultError": + replacement: "Generated.BetaRequestWebFetchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebSearchResultBlock": + replacement: "Generated.BetaRequestWebSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebSearchResultLocationCitation": + replacement: "Generated.BetaRequestWebSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebSearchToolResultBlock": + replacement: "Generated.BetaRequestWebSearchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaRequestWebSearchToolResultError": + replacement: "Generated.BetaRequestWebSearchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseBashCodeExecutionOutputBlock": + replacement: "Generated.BetaResponseBashCodeExecutionOutputBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseBashCodeExecutionResultBlock": + replacement: "Generated.BetaResponseBashCodeExecutionResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseBashCodeExecutionToolResultBlock": + replacement: "Generated.BetaResponseBashCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseBashCodeExecutionToolResultError": + replacement: "Generated.BetaResponseBashCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCharLocationCitation": + replacement: "Generated.BetaResponseCharLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCitationsConfig": + replacement: "Generated.BetaResponseCitationsConfig" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseClearToolUses20250919Edit": + replacement: "Generated.BetaResponseClearToolUses20250919Edit" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCodeExecutionOutputBlock": + replacement: "Generated.BetaResponseCodeExecutionOutputBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCodeExecutionResultBlock": + replacement: "Generated.BetaResponseCodeExecutionResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCodeExecutionToolResultBlock": + replacement: "Generated.BetaResponseCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseCodeExecutionToolResultError": + replacement: "Generated.BetaResponseCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseContainerUploadBlock": + replacement: "Generated.BetaResponseContainerUploadBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseContentBlockLocationCitation": + replacement: "Generated.BetaResponseContentBlockLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseContextManagement": + replacement: "Generated.BetaResponseContextManagement" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseDocumentBlock": + replacement: "Generated.BetaResponseDocumentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseMCPToolResultBlock": + replacement: "Generated.BetaResponseMCPToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseMCPToolUseBlock": + replacement: "Generated.BetaResponseMCPToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponsePageLocationCitation": + replacement: "Generated.BetaResponsePageLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseRedactedThinkingBlock": + replacement: "Generated.BetaResponseRedactedThinkingBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseSearchResultLocationCitation": + replacement: "Generated.BetaResponseSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseServerToolUseBlock": + replacement: "Generated.BetaResponseServerToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseServerToolUseBlockName": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaResponseTextBlock": + replacement: "Generated.BetaResponseTextBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionCreateResultBlock": + replacement: "Generated.BetaResponseTextEditorCodeExecutionCreateResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionStrReplaceResultBlock": + replacement: "Generated.BetaResponseTextEditorCodeExecutionStrReplaceResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionToolResultBlock": + replacement: "Generated.BetaResponseTextEditorCodeExecutionToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionToolResultError": + replacement: "Generated.BetaResponseTextEditorCodeExecutionToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionViewResultBlock": + replacement: "Generated.BetaResponseTextEditorCodeExecutionViewResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseTextEditorCodeExecutionViewResultBlockFileType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaResponseThinkingBlock": + replacement: "Generated.BetaResponseThinkingBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseToolUseBlock": + replacement: "Generated.BetaResponseToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebFetchResultBlock": + replacement: "Generated.BetaResponseWebFetchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebFetchToolResultBlock": + replacement: "Generated.BetaResponseWebFetchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebFetchToolResultError": + replacement: "Generated.BetaResponseWebFetchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebSearchResultBlock": + replacement: "Generated.BetaResponseWebSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebSearchResultLocationCitation": + replacement: "Generated.BetaResponseWebSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebSearchToolResultBlock": + replacement: "Generated.BetaResponseWebSearchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaResponseWebSearchToolResultError": + replacement: "Generated.BetaResponseWebSearchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaServerToolUsage": + replacement: "Generated.BetaServerToolUsage" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaSkill": + replacement: "Generated.BetaSkill" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaSkillParams": + replacement: "Generated.BetaSkillParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaSkillParamsType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaSkillType": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaSkillVersion": + replacement: "Generated.BetaSkillVersion" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaStopReason": + replacement: "Generated.BetaStopReason" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTextEditor20241022": + replacement: "Generated.BetaTextEditor_20241022" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTextEditor20250124": + replacement: "Generated.BetaTextEditor_20250124" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTextEditor20250429": + replacement: "Generated.BetaTextEditor_20250429" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTextEditor20250728": + replacement: "Generated.BetaTextEditor_20250728" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTextEditorCodeExecutionToolResultErrorCode": + replacement: "Generated.BetaTextEditorCodeExecutionToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaThinkingConfigDisabled": + replacement: "Generated.BetaThinkingConfigDisabled" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaThinkingConfigEnabled": + replacement: "Generated.BetaThinkingConfigEnabled" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaThinkingConfigParam": + replacement: "Generated.BetaThinkingConfigParam" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaTool": + replacement: "Generated.BetaTool" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolChoice": + replacement: "Generated.BetaToolChoice" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolChoiceAny": + replacement: "Generated.BetaToolChoiceAny" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolChoiceAuto": + replacement: "Generated.BetaToolChoiceAuto" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolChoiceNone": + replacement: "Generated.BetaToolChoiceNone" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolChoiceTool": + replacement: "Generated.BetaToolChoiceTool" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolUsesKeep": + replacement: "Generated.BetaToolUsesKeep" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaToolUsesTrigger": + replacement: "Generated.BetaToolUsesTrigger" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaUploadFileV1FilesPostParams": + replacement: "Generated.BetaUploadFileV1FilesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaUploadFileV1FilesPostRequest": + replacement: "Generated.BetaUploadFileV1FilesPostRequestFormData" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaURLImageSource": + replacement: "Generated.BetaURLImageSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaURLPDFSource": + replacement: "Generated.BetaURLPDFSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaUsage": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaUsageServiceTierEnum": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#BetaUserLocation": + replacement: "Generated.BetaUserLocation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaWebFetchTool20250910": + replacement: "Generated.BetaWebFetchTool_20250910" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaWebFetchToolResultErrorCode": + replacement: "Generated.BetaWebFetchToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaWebSearchTool20250305": + replacement: "Generated.BetaWebSearchTool_20250305" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BetaWebSearchToolResultErrorCode": + replacement: "Generated.BetaWebSearchToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BillingError": + replacement: "Generated.BillingError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BodyCreateSkillV1SkillsPost": + replacement: "Generated.Body_create_skill_v1_skills_post" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#BodyCreateSkillVersionV1SkillsSkillIdVersionsPost": + replacement: "Generated.Body_create_skill_version_v1_skills__skill_id__versions_post" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CacheControlEphemeral": + replacement: "Generated.CacheControlEphemeral" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CacheControlEphemeralTtl": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#CacheCreation": + replacement: "Generated.CacheCreation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Client": + replacement: "Generated.AnthropicClient" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ClientError": + replacement: "Generated.AnthropicClientError" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CompletePostParams": + replacement: "Generated.CompletePostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CompletionRequest": + replacement: "Generated.CompletionRequest" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CompletionResponse": + replacement: "Generated.CompletionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ContentBlock": + replacement: "Generated.ContentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ContentBlockSource": + replacement: "Generated.ContentBlockSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CountMessageTokensParams": + replacement: "Generated.CountMessageTokensParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CountMessageTokensResponse": + replacement: "Generated.CountMessageTokensResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateMessageBatchParams": + replacement: "Generated.CreateMessageBatchParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateMessageParams": + replacement: "Generated.CreateMessageParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateMessageParamsServiceTier": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#CreateSkillResponse": + replacement: "Generated.CreateSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateSkillV1SkillsPostParams": + replacement: "Generated.CreateSkillV1SkillsPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateSkillVersionResponse": + replacement: "Generated.CreateSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#CreateSkillVersionV1SkillsSkillIdVersionsPostParams": + replacement: "Generated.CreateSkillVersionV1SkillsSkillIdVersionsPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteFileV1FilesFileIdDeleteParams": + replacement: "Generated.DeleteFileV1FilesFileIdDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteMessageBatchResponse": + replacement: "Generated.DeleteMessageBatchResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteSkillResponse": + replacement: "Generated.DeleteSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteSkillV1SkillsSkillIdDeleteParams": + replacement: "Generated.DeleteSkillV1SkillsSkillIdDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteSkillVersionResponse": + replacement: "Generated.DeleteSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams": + replacement: "Generated.DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#DownloadFileV1FilesFileIdContentGetParams": + replacement: "Generated.DownloadFileV1FilesFileIdContentGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ErrorResponse": + replacement: "Generated.ErrorResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#FileDeleteResponse": + replacement: "Generated.FileDeleteResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#FileListResponse": + replacement: "Generated.FileListResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#FileMetadataSchema": + replacement: "Generated.FileMetadataSchema" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GatewayTimeoutError": + replacement: "Generated.GatewayTimeoutError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GetFileMetadataV1FilesFileIdGetParams": + replacement: "Generated.GetFileMetadataV1FilesFileIdGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GetSkillResponse": + replacement: "Generated.GetSkillResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GetSkillV1SkillsSkillIdGetParams": + replacement: "Generated.GetSkillV1SkillsSkillIdGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GetSkillVersionResponse": + replacement: "Generated.GetSkillVersionResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams": + replacement: "Generated.GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#InputContentBlock": + replacement: "Generated.InputContentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#InputMessage": + replacement: "Generated.InputMessage" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#InputMessageRole": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#InputSchema": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#InvalidRequestError": + replacement: "Generated.InvalidRequestError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListFilesV1FilesGetParams": + replacement: "Generated.ListFilesV1FilesGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListResponseMessageBatch": + replacement: "Generated.ListResponse_MessageBatch_" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListResponseModelInfo": + replacement: "Generated.ListResponse_ModelInfo_" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListSkillsResponse": + replacement: "Generated.ListSkillsResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListSkillsV1SkillsGetParams": + replacement: "Generated.ListSkillsV1SkillsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListSkillVersionsResponse": + replacement: "Generated.ListSkillVersionsResponse" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ListSkillVersionsV1SkillsSkillIdVersionsGetParams": + replacement: "Generated.ListSkillVersionsV1SkillsSkillIdVersionsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#make": + replacement: "Generated.make" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Message": + replacement: "Generated.Message" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatch": + replacement: "Generated.MessageBatch" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesCancelParams": + replacement: "Generated.MessageBatchesCancelParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesDeleteParams": + replacement: "Generated.MessageBatchesDeleteParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesListParams": + replacement: "Generated.MessageBatchesListParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesPostParams": + replacement: "Generated.MessageBatchesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesResultsParams": + replacement: "Generated.MessageBatchesResultsParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchesRetrieveParams": + replacement: "Generated.MessageBatchesRetrieveParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchIndividualRequestParams": + replacement: "Generated.MessageBatchIndividualRequestParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessageBatchProcessingStatus": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#MessagesCountTokensPostParams": + replacement: "Generated.MessagesCountTokensPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#MessagesPostParams": + replacement: "Generated.MessagesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Metadata": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#Model": + replacement: "Generated.Model" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ModelInfo": + replacement: "Generated.ModelInfo" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ModelsGetParams": + replacement: "Generated.ModelsGetParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ModelsListParams": + replacement: "Generated.ModelsListParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#NotFoundError": + replacement: "Generated.NotFoundError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#OverloadedError": + replacement: "Generated.OverloadedError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#PermissionError": + replacement: "Generated.PermissionError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#PlainTextSource": + replacement: "Generated.PlainTextSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RateLimitError": + replacement: "Generated.RateLimitError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestCharLocationCitation": + replacement: "Generated.RequestCharLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestCitationsConfig": + replacement: "Generated.RequestCitationsConfig" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestContentBlockLocationCitation": + replacement: "Generated.RequestContentBlockLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestCounts": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#RequestDocumentBlock": + replacement: "Generated.RequestDocumentBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestImageBlock": + replacement: "Generated.RequestImageBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestPageLocationCitation": + replacement: "Generated.RequestPageLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestRedactedThinkingBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#RequestSearchResultBlock": + replacement: "Generated.RequestSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestSearchResultLocationCitation": + replacement: "Generated.RequestSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestServerToolUseBlock": + replacement: "Generated.RequestServerToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestTextBlock": + replacement: "Generated.RequestTextBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestThinkingBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#RequestToolResultBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#RequestToolUseBlock": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#RequestWebSearchResultBlock": + replacement: "Generated.RequestWebSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestWebSearchResultLocationCitation": + replacement: "Generated.RequestWebSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestWebSearchToolResultBlock": + replacement: "Generated.RequestWebSearchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#RequestWebSearchToolResultError": + replacement: "Generated.RequestWebSearchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseCharLocationCitation": + replacement: "Generated.ResponseCharLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseContentBlockLocationCitation": + replacement: "Generated.ResponseContentBlockLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponsePageLocationCitation": + replacement: "Generated.ResponsePageLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseRedactedThinkingBlock": + replacement: "Generated.ResponseRedactedThinkingBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseSearchResultLocationCitation": + replacement: "Generated.ResponseSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseServerToolUseBlock": + replacement: "Generated.ResponseServerToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseTextBlock": + replacement: "Generated.ResponseTextBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseThinkingBlock": + replacement: "Generated.ResponseThinkingBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseToolUseBlock": + replacement: "Generated.ResponseToolUseBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseWebSearchResultBlock": + replacement: "Generated.ResponseWebSearchResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseWebSearchResultLocationCitation": + replacement: "Generated.ResponseWebSearchResultLocationCitation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseWebSearchToolResultBlock": + replacement: "Generated.ResponseWebSearchToolResultBlock" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ResponseWebSearchToolResultError": + replacement: "Generated.ResponseWebSearchToolResultError" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ServerToolUsage": + replacement: "Generated.ServerToolUsage" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Skill": + replacement: "Generated.Skill" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#SkillVersion": + replacement: "Generated.SkillVersion" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#StopReason": + replacement: "Generated.StopReason" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#TextEditor20250124": + replacement: "Generated.TextEditor_20250124" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#TextEditor20250429": + replacement: "Generated.TextEditor_20250429" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#TextEditor20250728": + replacement: "Generated.TextEditor_20250728" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ThinkingConfigDisabled": + replacement: "Generated.ThinkingConfigDisabled" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ThinkingConfigEnabled": + replacement: "Generated.ThinkingConfigEnabled" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ThinkingConfigParam": + replacement: "Generated.ThinkingConfigParam" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#Tool": + replacement: "Generated.Tool" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ToolChoice": + replacement: "Generated.ToolChoice" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ToolChoiceAny": + replacement: "Generated.ToolChoiceAny" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ToolChoiceAuto": + replacement: "Generated.ToolChoiceAuto" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ToolChoiceNone": + replacement: "Generated.ToolChoiceNone" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#ToolChoiceTool": + replacement: "Generated.ToolChoiceTool" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#UploadFileV1FilesPostParams": + replacement: "Generated.UploadFileV1FilesPostParams" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#UploadFileV1FilesPostRequest": + replacement: "Generated.UploadFileV1FilesPostRequestFormData" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#URLImageSource": + replacement: "Generated.URLImageSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#URLPDFSource": + replacement: "Generated.URLPDFSource" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#UsageServiceTierEnum": + replacement: "none" + note: "Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-anthropic/Generated#UserLocation": + replacement: "Generated.UserLocation" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#WebSearchTool20250305": + replacement: "Generated.WebSearchTool_20250305" + note: "Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-anthropic/Generated#WebSearchToolResultErrorCode": + replacement: "Generated.WebSearchToolResultErrorCode" + note: "Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed." diff --git a/.context/effect/migration/annotations/effect__ai-anthropic__index.yaml b/.context/effect/migration/annotations/effect__ai-anthropic__index.yaml new file mode 100644 index 000000000..f8fa2bd85 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-anthropic__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai-anthropic/index": + replacement: "@effect/ai-anthropic" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-anthropic package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__ai-google.yaml b/.context/effect/migration/annotations/effect__ai-google.yaml new file mode 100644 index 000000000..9de246d44 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google": + replacement: "none" + note: "The @effect/ai-google provider package was removed from v4 with no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly." diff --git a/.context/effect/migration/annotations/effect__ai-google__Generated.yaml b/.context/effect/migration/annotations/effect__ai-google__Generated.yaml new file mode 100644 index 000000000..0b6ed2397 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__Generated.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/Generated": + replacement: none + note: The @effect/ai-google package was removed from v4, so this generated Google API schema has no Effect v4 replacement. Use Google's current SDK/API types directly or route supported Gemini models through another v4 provider integration. diff --git a/.context/effect/migration/annotations/effect__ai-google__GoogleClient.yaml b/.context/effect/migration/annotations/effect__ai-google__GoogleClient.yaml new file mode 100644 index 000000000..f344b4f94 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__GoogleClient.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/GoogleClient": + replacement: none + note: The @effect/ai-google provider package was removed from v4 and has no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. diff --git a/.context/effect/migration/annotations/effect__ai-google__GoogleConfig.yaml b/.context/effect/migration/annotations/effect__ai-google__GoogleConfig.yaml new file mode 100644 index 000000000..175622d5e --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__GoogleConfig.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/GoogleConfig": + replacement: none + note: The @effect/ai-google provider package was removed from v4 and has no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. diff --git a/.context/effect/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml b/.context/effect/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml new file mode 100644 index 000000000..0cda6708c --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__GoogleLanguageModel.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/GoogleLanguageModel": + replacement: none + note: The @effect/ai-google language-model integration was removed from v4. Use a supported v4 provider integration for Gemini models or implement LanguageModel.LanguageModel against Google's current SDK. diff --git a/.context/effect/migration/annotations/effect__ai-google__GoogleTool.yaml b/.context/effect/migration/annotations/effect__ai-google__GoogleTool.yaml new file mode 100644 index 000000000..cd91c242c --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__GoogleTool.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/GoogleTool": + replacement: none + note: The @effect/ai-google package and its provider-defined tools were removed from v4. Model this capability in the provider integration you adopt, or define an application Tool when the replacement provider supports it. diff --git a/.context/effect/migration/annotations/effect__ai-google__index.yaml b/.context/effect/migration/annotations/effect__ai-google__index.yaml new file mode 100644 index 000000000..a5cfecfc0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-google__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai-google/index": + replacement: "none" + note: "The @effect/ai-google provider package was removed from v4 with no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly." diff --git a/.context/effect/migration/annotations/effect__ai-openai__Generated.yaml b/.context/effect/migration/annotations/effect__ai-openai__Generated.yaml new file mode 100644 index 000000000..5f96a15c4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__Generated.yaml @@ -0,0 +1,3714 @@ +"@effect/ai-openai/Generated#ActiveStatus": + replacement: "Generated.ActiveStatus" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ActiveStatusType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AddUploadPartRequest": + replacement: "Generated.AddUploadPartRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AdminApiKey": + replacement: "Generated.AdminApiKey" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AdminApiKeysCreateRequest": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AdminApiKeysDelete200": + replacement: "Generated.AdminApiKeysDelete200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AdminApiKeysListParams": + replacement: "Generated.AdminApiKeysListParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AdminApiKeysListParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Annotation": + replacement: "Generated.Annotation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApiKeyList": + replacement: "Generated.ApiKeyList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchCallOutputStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchCallOutputStatusParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchCallStatusParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchCreateFileOperation": + replacement: "Generated.ApplyPatchCreateFileOperation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchCreateFileOperationParam": + replacement: "Generated.ApplyPatchCreateFileOperationParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchCreateFileOperationParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchCreateFileOperationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchDeleteFileOperation": + replacement: "Generated.ApplyPatchDeleteFileOperation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchDeleteFileOperationParam": + replacement: "Generated.ApplyPatchDeleteFileOperationParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchDeleteFileOperationParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchDeleteFileOperationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchOperationParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCall": + replacement: "Generated.ApplyPatchToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchToolCallItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCallItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCallOutput": + replacement: "Generated.ApplyPatchToolCallOutput" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchToolCallOutputItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCallOutputItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCallOutputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchToolParam": + replacement: "Generated.ApplyPatchToolParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchToolParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchUpdateFileOperation": + replacement: "Generated.ApplyPatchUpdateFileOperation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchUpdateFileOperationParam": + replacement: "Generated.ApplyPatchUpdateFileOperationParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ApplyPatchUpdateFileOperationParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApplyPatchUpdateFileOperationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApproximateLocation": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ApproximateLocationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssignedRoleDetails": + replacement: "Generated.AssignedRoleDetails" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantMessageItem": + replacement: "Generated.AssistantMessageItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantMessageItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantMessageItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantObject": + replacement: "Generated.AssistantObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantsApiResponseFormatOption": + replacement: "Generated.AssistantsApiResponseFormatOption" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantsApiResponseFormatOptionEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantsNamedToolChoice": + replacement: "Generated.AssistantsNamedToolChoice" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantsNamedToolChoiceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantSupportedModels": + replacement: "Generated.AssistantSupportedModels" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantTool": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantToolsCode": + replacement: "Generated.AssistantToolsCode" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantToolsCodeType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantToolsFileSearch": + replacement: "Generated.AssistantToolsFileSearch" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantToolsFileSearchType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantToolsFileSearchTypeOnly": + replacement: "Generated.AssistantToolsFileSearchTypeOnly" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantToolsFileSearchTypeOnlyType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AssistantToolsFunction": + replacement: "Generated.AssistantToolsFunction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AssistantToolsFunctionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Attachment": + replacement: "Generated.Attachment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AttachmentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AudioResponseFormat": + replacement: "Generated.AudioResponseFormat" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AudioTranscription": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AudioTranscriptionModel": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AuditLog": + replacement: "Generated.AuditLog" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogActor": + replacement: "Generated.AuditLogActor" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogActorApiKey": + replacement: "Generated.AuditLogActorApiKey" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogActorApiKeyType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AuditLogActorServiceAccount": + replacement: "Generated.AuditLogActorServiceAccount" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogActorSession": + replacement: "Generated.AuditLogActorSession" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogActorType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AuditLogActorUser": + replacement: "Generated.AuditLogActorUser" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AuditLogEventType": + replacement: "Generated.AuditLogEventType" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#AutoChunkingStrategyRequestParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AutoChunkingStrategyRequestParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#AutomaticThreadTitlingParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Batch": + replacement: "Generated.Batch" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#BatchError": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#BatchFileExpirationAfter": + replacement: "Generated.BatchFileExpirationAfter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#BatchFileExpirationAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#BatchObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#BatchRequestCounts": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#BatchStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Certificate": + replacement: "Generated.Certificate" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CertificateObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionAllowedTools": + replacement: "Generated.ChatCompletionAllowedTools" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionAllowedToolsChoice": + replacement: "Generated.ChatCompletionAllowedToolsChoice" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionAllowedToolsChoiceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionAllowedToolsMode": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionDeleted": + replacement: "Generated.ChatCompletionDeleted" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionDeletedObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionFunctionCallOption": + replacement: "Generated.ChatCompletionFunctionCallOption" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionFunctions": + replacement: "Generated.ChatCompletionFunctions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionList": + replacement: "Generated.ChatCompletionList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionListObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionMessageCustomToolCall": + replacement: "Generated.ChatCompletionMessageCustomToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionMessageCustomToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionMessageList": + replacement: "Generated.ChatCompletionMessageList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionMessageListObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionMessageToolCall": + replacement: "Generated.ChatCompletionMessageToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionMessageToolCalls": + replacement: "Generated.ChatCompletionMessageToolCalls" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionMessageToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionNamedToolChoice": + replacement: "Generated.ChatCompletionNamedToolChoice" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionNamedToolChoiceCustom": + replacement: "Generated.ChatCompletionNamedToolChoiceCustom" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionNamedToolChoiceCustomType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionNamedToolChoiceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestAssistantMessage": + replacement: "Generated.ChatCompletionRequestAssistantMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestAssistantMessageContentPart": + replacement: "Generated.ChatCompletionRequestAssistantMessageContentPart" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestAssistantMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestDeveloperMessage": + replacement: "Generated.ChatCompletionRequestDeveloperMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestDeveloperMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestFunctionMessage": + replacement: "Generated.ChatCompletionRequestFunctionMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestFunctionMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessage": + replacement: "Generated.ChatCompletionRequestMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartAudio": + replacement: "Generated.ChatCompletionRequestMessageContentPartAudio" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartAudioInputAudioFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartAudioType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartFile": + replacement: "Generated.ChatCompletionRequestMessageContentPartFile" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartFileType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartImage": + replacement: "Generated.ChatCompletionRequestMessageContentPartImage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartImageImageUrlDetail": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartImageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartRefusal": + replacement: "Generated.ChatCompletionRequestMessageContentPartRefusal" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartRefusalType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartText": + replacement: "Generated.ChatCompletionRequestMessageContentPartText" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestMessageContentPartTextType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestSystemMessage": + replacement: "Generated.ChatCompletionRequestSystemMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestSystemMessageContentPart": + replacement: "Generated.ChatCompletionRequestSystemMessageContentPart" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestSystemMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestToolMessage": + replacement: "Generated.ChatCompletionRequestToolMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestToolMessageContentPart": + replacement: "Generated.ChatCompletionRequestToolMessageContentPart" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestToolMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionRequestUserMessage": + replacement: "Generated.ChatCompletionRequestUserMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestUserMessageContentPart": + replacement: "Generated.ChatCompletionRequestUserMessageContentPart" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionRequestUserMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionResponseMessage": + replacement: "Generated.ChatCompletionResponseMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionResponseMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionStreamOptions": + replacement: "Generated.ChatCompletionStreamOptions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionTokenLogprob": + replacement: "Generated.ChatCompletionTokenLogprob" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionTool": + replacement: "Generated.ChatCompletionTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionToolChoiceOption": + replacement: "Generated.ChatCompletionToolChoiceOption" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatCompletionToolChoiceOptionEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatCompletionToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatkitConfigurationParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatkitWorkflow": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatkitWorkflowTracing": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatModel": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionAutomaticThreadTitling": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionChatkitConfiguration": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionFileUpload": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionHistory": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionRateLimits": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionResource": + replacement: "Generated.ChatSessionResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChatSessionResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChatSessionStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ChunkingStrategyRequestParam": + replacement: "Generated.ChunkingStrategyRequestParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ChunkingStrategyResponse": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClickButtonType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClickParam": + replacement: "Generated.ClickParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ClickParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Client": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClientError": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClientToolCallItem": + replacement: "Generated.ClientToolCallItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ClientToolCallItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClientToolCallItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClientToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ClosedStatus": + replacement: "Generated.ClosedStatus" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ClosedStatusType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterContainerAuto": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterContainerAutoType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterOutputImage": + replacement: "Generated.CodeInterpreterOutputImage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CodeInterpreterOutputImageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterOutputLogs": + replacement: "Generated.CodeInterpreterOutputLogs" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CodeInterpreterOutputLogsType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterTool": + replacement: "Generated.CodeInterpreterTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CodeInterpreterToolCall": + replacement: "Generated.CodeInterpreterToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CodeInterpreterToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CodeInterpreterToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComparisonFilter": + replacement: "Generated.ComparisonFilter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComparisonFilterType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComparisonFilterValueItems": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CompleteUploadRequest": + replacement: "Generated.CompleteUploadRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CompletionUsage": + replacement: "Generated.CompletionUsage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CompoundFilter": + replacement: "Generated.CompoundFilter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CompoundFilterType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerAction": + replacement: "Generated.ComputerAction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerCallOutputItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerCallOutputItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerCallSafetyCheckParam": + replacement: "Generated.ComputerCallSafetyCheckParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerEnvironment": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerScreenshotContent": + replacement: "Generated.ComputerScreenshotContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerScreenshotContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerScreenshotImage": + replacement: "Generated.ComputerScreenshotImage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerScreenshotImageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerToolCall": + replacement: "Generated.ComputerToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerToolCallOutputResource": + replacement: "Generated.ComputerToolCallOutputResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerToolCallOutputResourceStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerToolCallOutputResourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ComputerUsePreviewTool": + replacement: "Generated.ComputerUsePreviewTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ComputerUsePreviewToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ContainerFileCitationBody": + replacement: "Generated.ContainerFileCitationBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ContainerFileCitationBodyType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ContainerFileListResource": + replacement: "Generated.ContainerFileListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ContainerFileResource": + replacement: "Generated.ContainerFileResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ContainerListResource": + replacement: "Generated.ContainerListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ContainerMemoryLimit": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ContainerResource": + replacement: "Generated.ContainerResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ContainerResourceExpiresAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Conversation2": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ConversationItem": + replacement: "Generated.ConversationItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ConversationItemList": + replacement: "Generated.ConversationItemList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ConversationParam": + replacement: "Generated.ConversationParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ConversationParam2": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ConversationResource": + replacement: "Generated.ConversationResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ConversationResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CostsResult": + replacement: "Generated.CostsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CostsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateAssistantRequest": + replacement: "Generated.CreateAssistantRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateBatchRequest": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateBatchRequestCompletionWindow": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateBatchRequestEndpoint": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatCompletionRequest": + replacement: "Generated.CreateChatCompletionRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateChatCompletionRequestAudioFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatCompletionRequestFunctionCallEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatCompletionRequestPromptCacheRetentionEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatCompletionRequestWebSearchOptionsUserLocationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatCompletionResponse": + replacement: "Generated.CreateChatCompletionResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateChatCompletionResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateChatSessionBody": + replacement: "Generated.CreateChatSessionBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateCompletionRequest": + replacement: "Generated.CreateCompletionRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateCompletionRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateCompletionResponse": + replacement: "Generated.CreateCompletionResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateCompletionResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateContainerBody": + replacement: "Generated.CreateContainerBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateContainerBodyExpiresAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateContainerFileBody": + replacement: "Generated.CreateContainerFileBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateConversationBody": + replacement: "Generated.CreateConversationBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateConversationItemsParams": + replacement: "Generated.CreateConversationItemsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateConversationItemsRequest": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEmbeddingRequest": + replacement: "Generated.CreateEmbeddingRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEmbeddingRequestEncodingFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEmbeddingRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEmbeddingResponse": + replacement: "Generated.CreateEmbeddingResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEmbeddingResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalCompletionsRunDataSource": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalCompletionsRunDataSourceInputMessagesEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalCompletionsRunDataSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalCustomDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalCustomDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalItem": + replacement: "Generated.CreateEvalItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEvalJsonlRunDataSource": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalJsonlRunDataSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalLabelModelGrader": + replacement: "Generated.CreateEvalLabelModelGrader" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEvalLabelModelGraderType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalLogsDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalLogsDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalRequest": + replacement: "Generated.CreateEvalRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEvalResponsesRunDataSource": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalResponsesRunDataSourceInputMessagesEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalResponsesRunDataSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalRunRequest": + replacement: "Generated.CreateEvalRunRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateEvalStoredCompletionsDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateEvalStoredCompletionsDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateFileRequest": + replacement: "Generated.CreateFileRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateFineTuningCheckpointPermissionRequest": + replacement: "Generated.CreateFineTuningCheckpointPermissionRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateFineTuningJobRequest": + replacement: "Generated.CreateFineTuningJobRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateFineTuningJobRequestHyperparametersBatchSizeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateFineTuningJobRequestHyperparametersLearningRateMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateFineTuningJobRequestHyperparametersNEpochsEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateFineTuningJobRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateGroupBody": + replacement: "Generated.CreateGroupBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateGroupUserBody": + replacement: "Generated.CreateGroupUserBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateImageEditRequest": + replacement: "Generated.CreateImageEditRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateImageEditRequestBackground": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageEditRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageEditRequestOutputFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageEditRequestQuality": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageEditRequestResponseFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageEditRequestSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequest": + replacement: "Generated.CreateImageRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateImageRequestBackground": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestModeration": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestOutputFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestQuality": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestResponseFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageRequestStyle": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageVariationRequest": + replacement: "Generated.CreateImageVariationRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateImageVariationRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageVariationRequestResponseFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateImageVariationRequestSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateMessageRequest": + replacement: "Generated.CreateMessageRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateMessageRequestRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateModerationRequest": + replacement: "Generated.CreateModerationRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateModerationRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateModerationResponse": + replacement: "Generated.CreateModerationResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateResponse": + replacement: "Generated.CreateResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateResponsePromptCacheRetentionEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateResponseTruncationEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateRunParams": + replacement: "Generated.CreateRunParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateRunRequest": + replacement: "Generated.CreateRunRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateRunRequestToolChoice": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateRunRequestToolChoiceEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateRunRequestTruncationStrategy": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateRunRequestTruncationStrategyEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateSpeechRequest": + replacement: "Generated.CreateSpeechRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateSpeechRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateSpeechRequestResponseFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateSpeechRequestStreamFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadAndRunRequest": + replacement: "Generated.CreateThreadAndRunRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateThreadAndRunRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadAndRunRequestToolChoice": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadAndRunRequestToolChoiceEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadAndRunRequestTruncationStrategy": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadAndRunRequestTruncationStrategyEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateThreadRequest": + replacement: "Generated.CreateThreadRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranscription200": + replacement: "Generated.CreateTranscription200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranscriptionRequest": + replacement: "Generated.CreateTranscriptionRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranscriptionRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateTranscriptionResponseDiarizedJson": + replacement: "Generated.CreateTranscriptionResponseDiarizedJson" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranscriptionResponseDiarizedJsonTask": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateTranscriptionResponseJson": + replacement: "Generated.CreateTranscriptionResponseJson" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranscriptionResponseVerboseJson": + replacement: "Generated.CreateTranscriptionResponseVerboseJson" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranslation200": + replacement: "Generated.CreateTranslation200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranslationRequest": + replacement: "Generated.CreateTranslationRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranslationRequestModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateTranslationRequestResponseFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateTranslationResponseJson": + replacement: "Generated.CreateTranslationResponseJson" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateTranslationResponseVerboseJson": + replacement: "Generated.CreateTranslationResponseVerboseJson" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateUploadRequest": + replacement: "Generated.CreateUploadRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateUploadRequestPurpose": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateVectorStoreFileBatchRequest": + replacement: "Generated.CreateVectorStoreFileBatchRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateVectorStoreFileRequest": + replacement: "Generated.CreateVectorStoreFileRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateVectorStoreRequest": + replacement: "Generated.CreateVectorStoreRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CreateVideoBody": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CreateVideoRemixBody": + replacement: "Generated.CreateVideoRemixBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomGrammarFormatParam": + replacement: "Generated.CustomGrammarFormatParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomGrammarFormatParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomTextFormatParam": + replacement: "Generated.CustomTextFormatParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomTextFormatParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolCall": + replacement: "Generated.CustomToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomToolCallOutput": + replacement: "Generated.CustomToolCallOutput" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomToolCallOutputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolChatCompletions": + replacement: "Generated.CustomToolChatCompletions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomToolChatCompletionsCustomFormatEnumGrammarSyntax": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolChatCompletionsCustomFormatEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolChatCompletionsType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#CustomToolParam": + replacement: "Generated.CustomToolParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#CustomToolParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteAssistantResponse": + replacement: "Generated.DeleteAssistantResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteAssistantResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteCertificateResponse": + replacement: "Generated.DeleteCertificateResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeletedConversationResource": + replacement: "Generated.DeletedConversationResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeletedConversationResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeletedRoleAssignmentResource": + replacement: "Generated.DeletedRoleAssignmentResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeletedThreadResource": + replacement: "Generated.DeletedThreadResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeletedThreadResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeletedVideoResource": + replacement: "Generated.DeletedVideoResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeletedVideoResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteEval200": + replacement: "Generated.DeleteEval200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteEvalRun200": + replacement: "Generated.DeleteEvalRun200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteFileResponse": + replacement: "Generated.DeleteFileResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteFileResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteFineTuningCheckpointPermissionResponse": + replacement: "Generated.DeleteFineTuningCheckpointPermissionResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteFineTuningCheckpointPermissionResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteMessageResponse": + replacement: "Generated.DeleteMessageResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteMessageResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteModelResponse": + replacement: "Generated.DeleteModelResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteThreadResponse": + replacement: "Generated.DeleteThreadResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteThreadResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteVectorStoreFileResponse": + replacement: "Generated.DeleteVectorStoreFileResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteVectorStoreFileResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DeleteVectorStoreResponse": + replacement: "Generated.DeleteVectorStoreResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DeleteVectorStoreResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DetailEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DoubleClickAction": + replacement: "Generated.DoubleClickAction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#DoubleClickActionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DownloadFile200": + replacement: "Generated.DownloadFile200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Drag": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DragPoint": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#DragType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EasyInputMessage": + replacement: "Generated.EasyInputMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EasyInputMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EasyInputMessageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Embedding": + replacement: "Generated.Embedding" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EmbeddingObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Error": + replacement: "Generated.Error" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Error2": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ErrorResponse": + replacement: "Generated.ErrorResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Eval": + replacement: "Generated.Eval" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalApiError": + replacement: "Generated.EvalApiError" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalCustomDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalCustomDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderLabelModel": + replacement: "Generated.EvalGraderLabelModel" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalGraderLabelModelType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderPython": + replacement: "Generated.EvalGraderPython" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalGraderPythonType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderScoreModel": + replacement: "Generated.EvalGraderScoreModel" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalGraderScoreModelType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderStringCheck": + replacement: "Generated.EvalGraderStringCheck" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalGraderStringCheckOperation": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderStringCheckType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderTextSimilarity": + replacement: "Generated.EvalGraderTextSimilarity" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalGraderTextSimilarityEvaluationMetric": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalGraderTextSimilarityType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalItem": + replacement: "Generated.EvalItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalItemContentEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalItemRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalJsonlFileContentSource": + replacement: "Generated.EvalJsonlFileContentSource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalJsonlFileContentSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalJsonlFileIdSource": + replacement: "Generated.EvalJsonlFileIdSource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalJsonlFileIdSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalList": + replacement: "Generated.EvalList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalListObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalLogsDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalLogsDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalResponsesSource": + replacement: "Generated.EvalResponsesSource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalResponsesSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalRun": + replacement: "Generated.EvalRun" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalRunList": + replacement: "Generated.EvalRunList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalRunListObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalRunObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalRunOutputItem": + replacement: "Generated.EvalRunOutputItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalRunOutputItemList": + replacement: "Generated.EvalRunOutputItemList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalRunOutputItemListObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalRunOutputItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalRunOutputItemResult": + replacement: "Generated.EvalRunOutputItemResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalStoredCompletionsDataSourceConfig": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalStoredCompletionsDataSourceConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#EvalStoredCompletionsSource": + replacement: "Generated.EvalStoredCompletionsSource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#EvalStoredCompletionsSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ExpiresAfterParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ExpiresAfterParamAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileAnnotation": + replacement: "Generated.FileAnnotation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileAnnotationSource": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileAnnotationSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileAnnotationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileCitationBody": + replacement: "Generated.FileCitationBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileCitationBodyType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileExpirationAfter": + replacement: "Generated.FileExpirationAfter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileExpirationAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FilePath": + replacement: "Generated.FilePath" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FilePathType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FilePurpose": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileSearchRanker": + replacement: "Generated.FileSearchRanker" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileSearchRankingOptions": + replacement: "Generated.FileSearchRankingOptions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileSearchTool": + replacement: "Generated.FileSearchTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileSearchToolCall": + replacement: "Generated.FileSearchToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FileSearchToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileSearchToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileSearchToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FileUploadParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Filters": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneDPOHyperparameters": + replacement: "Generated.FineTuneDPOHyperparameters" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneDPOHyperparametersBatchSizeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneDPOHyperparametersBetaEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneDPOHyperparametersLearningRateMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneDPOHyperparametersNEpochsEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneDPOMethod": + replacement: "Generated.FineTuneDPOMethod" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneMethod": + replacement: "Generated.FineTuneMethod" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneMethodType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparameters": + replacement: "Generated.FineTuneReinforcementHyperparameters" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersBatchSizeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersComputeMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersEvalIntervalEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersEvalSamplesEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersLearningRateMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersNEpochsEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementHyperparametersReasoningEffort": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneReinforcementMethod": + replacement: "Generated.FineTuneReinforcementMethod" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneSupervisedHyperparameters": + replacement: "Generated.FineTuneSupervisedHyperparameters" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuneSupervisedHyperparametersBatchSizeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneSupervisedHyperparametersLearningRateMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneSupervisedHyperparametersNEpochsEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuneSupervisedMethod": + replacement: "Generated.FineTuneSupervisedMethod" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningCheckpointPermission": + replacement: "Generated.FineTuningCheckpointPermission" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningCheckpointPermissionObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningIntegration": + replacement: "Generated.FineTuningIntegration" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningIntegrationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJob": + replacement: "Generated.FineTuningJob" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningJobCheckpoint": + replacement: "Generated.FineTuningJobCheckpoint" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningJobCheckpointObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobEvent": + replacement: "Generated.FineTuningJobEvent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FineTuningJobEventLevel": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobEventObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobEventType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobHyperparametersBatchSizeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobHyperparametersLearningRateMultiplierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobHyperparametersNEpochsEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FineTuningJobStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionAndCustomToolCallOutput": + replacement: "Generated.FunctionAndCustomToolCallOutput" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionCallItemStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionCallOutputItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionCallOutputItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionObject": + replacement: "Generated.FunctionObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionParameters": + replacement: "Generated.FunctionParameters" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellAction": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellActionParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCall": + replacement: "Generated.FunctionShellCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallItemStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutput": + replacement: "Generated.FunctionShellCallOutput" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputContent": + replacement: "Generated.FunctionShellCallOutputContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputContentParam": + replacement: "Generated.FunctionShellCallOutputContentParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputExitOutcome": + replacement: "Generated.FunctionShellCallOutputExitOutcome" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputExitOutcomeParam": + replacement: "Generated.FunctionShellCallOutputExitOutcomeParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputExitOutcomeParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputExitOutcomeType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputItemParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputItemParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputOutcomeParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputTimeoutOutcome": + replacement: "Generated.FunctionShellCallOutputTimeoutOutcome" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputTimeoutOutcomeParam": + replacement: "Generated.FunctionShellCallOutputTimeoutOutcomeParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellCallOutputTimeoutOutcomeParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputTimeoutOutcomeType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallOutputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionShellToolParam": + replacement: "Generated.FunctionShellToolParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionShellToolParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionTool": + replacement: "Generated.FunctionTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionToolCall": + replacement: "Generated.FunctionToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionToolCallOutputResource": + replacement: "Generated.FunctionToolCallOutputResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionToolCallOutputResourceStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolCallOutputResourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolCallResource": + replacement: "Generated.FunctionToolCallResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#FunctionToolCallResourceStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolCallResourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#FunctionToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetCertificateParams": + replacement: "Generated.GetCertificateParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetChatCompletionMessagesParams": + replacement: "Generated.GetChatCompletionMessagesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetChatCompletionMessagesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetConversationItemParams": + replacement: "Generated.GetConversationItemParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetEvalRunOutputItemsParams": + replacement: "Generated.GetEvalRunOutputItemsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetEvalRunOutputItemsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetEvalRunOutputItemsParamsStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetEvalRunsParams": + replacement: "Generated.GetEvalRunsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetEvalRunsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetEvalRunsParamsStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GetResponseParams": + replacement: "Generated.GetResponseParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GetRunStepParams": + replacement: "Generated.GetRunStepParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderLabelModel": + replacement: "Generated.GraderLabelModel" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderLabelModelType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderMulti": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderMultiType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderPython": + replacement: "Generated.GraderPython" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderPythonType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderScoreModel": + replacement: "Generated.GraderScoreModel" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderScoreModelType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderStringCheck": + replacement: "Generated.GraderStringCheck" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderStringCheckOperation": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderStringCheckType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderTextSimilarity": + replacement: "Generated.GraderTextSimilarity" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GraderTextSimilarityEvaluationMetric": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GraderTextSimilarityType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GrammarSyntax1": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Group": + replacement: "Generated.Group" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupDeletedResource": + replacement: "Generated.GroupDeletedResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupDeletedResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GroupListResource": + replacement: "Generated.GroupListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupListResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GroupObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GroupResourceWithSuccess": + replacement: "Generated.GroupResourceWithSuccess" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupResponse": + replacement: "Generated.GroupResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupRoleAssignment": + replacement: "Generated.GroupRoleAssignment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupRoleAssignmentObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GroupUserAssignment": + replacement: "Generated.GroupUserAssignment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupUserAssignmentObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#GroupUserDeletedResource": + replacement: "Generated.GroupUserDeletedResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#GroupUserDeletedResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#HistoryParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#HybridSearchOptions": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Image": + replacement: "Generated.Image" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImageDetail": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenInputUsageDetails": + replacement: "Generated.ImageGenInputUsageDetails" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImageGenTool": + replacement: "Generated.ImageGenTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImageGenToolBackground": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolCall": + replacement: "Generated.ImageGenToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImageGenToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolModel": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolModeration": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolOutputFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolQuality": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImageGenUsage": + replacement: "Generated.ImageGenUsage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImagesResponse": + replacement: "Generated.ImagesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ImagesResponseBackground": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImagesResponseOutputFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImagesResponseQuality": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ImagesResponseSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#IncludeEnum": + replacement: "Generated.IncludeEnum" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InferenceOptions": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputAudio": + replacement: "Generated.InputAudio" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputAudioInputAudioFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputAudioType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputContent": + replacement: "Generated.InputContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputFidelity": + replacement: "Generated.InputFidelity" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputFileContent": + replacement: "Generated.InputFileContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputFileContentParam": + replacement: "Generated.InputFileContentParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputFileContentParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputFileContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputImageContent": + replacement: "Generated.InputImageContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputImageContentParamAutoParam": + replacement: "Generated.InputImageContentParamAutoParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputImageContentParamAutoParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputImageContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputItem": + replacement: "Generated.InputItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputMessage": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageContentList": + replacement: "Generated.InputMessageContentList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputMessageResource": + replacement: "Generated.InputMessageResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputMessageResourceRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageResourceStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageResourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputMessageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputParam": + replacement: "Generated.InputParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputTextContent": + replacement: "Generated.InputTextContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputTextContentParam": + replacement: "Generated.InputTextContentParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InputTextContentParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InputTextContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Invite": + replacement: "Generated.Invite" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InviteDeleteResponse": + replacement: "Generated.InviteDeleteResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InviteDeleteResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InviteListResponse": + replacement: "Generated.InviteListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InviteListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InviteObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InviteProjectGroupBody": + replacement: "Generated.InviteProjectGroupBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InviteRequest": + replacement: "Generated.InviteRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#InviteRequestRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InviteRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#InviteStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Item": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ItemReferenceParam": + replacement: "Generated.ItemReferenceParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ItemReferenceParamTypeEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ItemResource": + replacement: "Generated.ItemResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#KeyPressAction": + replacement: "Generated.KeyPressAction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#KeyPressActionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListAssistantsParams": + replacement: "Generated.ListAssistantsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListAssistantsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListAssistantsResponse": + replacement: "Generated.ListAssistantsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListAuditLogsParams": + replacement: "Generated.ListAuditLogsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListAuditLogsResponse": + replacement: "Generated.ListAuditLogsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListAuditLogsResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListBatchesParams": + replacement: "Generated.ListBatchesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListBatchesResponse": + replacement: "Generated.ListBatchesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListBatchesResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListCertificatesResponse": + replacement: "Generated.ListCertificatesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListCertificatesResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListChatCompletionsParams": + replacement: "Generated.ListChatCompletionsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListChatCompletionsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListContainerFilesParams": + replacement: "Generated.ListContainerFilesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListContainerFilesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListContainersParams": + replacement: "Generated.ListContainersParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListContainersParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListConversationItemsParams": + replacement: "Generated.ListConversationItemsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListConversationItemsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListEvalsParams": + replacement: "Generated.ListEvalsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListEvalsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListEvalsParamsOrderBy": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFilesInVectorStoreBatchParams": + replacement: "Generated.ListFilesInVectorStoreBatchParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFilesInVectorStoreBatchParamsFilter": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFilesInVectorStoreBatchParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFilesParams": + replacement: "Generated.ListFilesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFilesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFilesResponse": + replacement: "Generated.ListFilesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningCheckpointPermissionResponse": + replacement: "Generated.ListFineTuningCheckpointPermissionResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningCheckpointPermissionResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFineTuningCheckpointPermissionsParams": + replacement: "Generated.ListFineTuningCheckpointPermissionsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningCheckpointPermissionsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFineTuningEventsParams": + replacement: "Generated.ListFineTuningEventsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningJobCheckpointsParams": + replacement: "Generated.ListFineTuningJobCheckpointsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningJobCheckpointsResponse": + replacement: "Generated.ListFineTuningJobCheckpointsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningJobCheckpointsResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListFineTuningJobEventsResponse": + replacement: "Generated.ListFineTuningJobEventsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListFineTuningJobEventsResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListGroupRoleAssignmentsParams": + replacement: "Generated.ListGroupRoleAssignmentsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListGroupRoleAssignmentsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListGroupsParams": + replacement: "Generated.ListGroupsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListGroupsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListGroupUsersParams": + replacement: "Generated.ListGroupUsersParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListGroupUsersParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListInputItemsParams": + replacement: "Generated.ListInputItemsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListInputItemsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListInvitesParams": + replacement: "Generated.ListInvitesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListMessagesParams": + replacement: "Generated.ListMessagesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListMessagesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListMessagesResponse": + replacement: "Generated.ListMessagesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListModelsResponse": + replacement: "Generated.ListModelsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListModelsResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListOrganizationCertificatesParams": + replacement: "Generated.ListOrganizationCertificatesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListOrganizationCertificatesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListPaginatedFineTuningJobsParams": + replacement: "Generated.ListPaginatedFineTuningJobsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListPaginatedFineTuningJobsResponse": + replacement: "Generated.ListPaginatedFineTuningJobsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListPaginatedFineTuningJobsResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectApiKeysParams": + replacement: "Generated.ListProjectApiKeysParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectCertificatesParams": + replacement: "Generated.ListProjectCertificatesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectCertificatesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectGroupRoleAssignmentsParams": + replacement: "Generated.ListProjectGroupRoleAssignmentsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectGroupRoleAssignmentsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectGroupsParams": + replacement: "Generated.ListProjectGroupsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectGroupsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectRateLimitsParams": + replacement: "Generated.ListProjectRateLimitsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectRolesParams": + replacement: "Generated.ListProjectRolesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectRolesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectServiceAccountsParams": + replacement: "Generated.ListProjectServiceAccountsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectsParams": + replacement: "Generated.ListProjectsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectUserRoleAssignmentsParams": + replacement: "Generated.ListProjectUserRoleAssignmentsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListProjectUserRoleAssignmentsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListProjectUsersParams": + replacement: "Generated.ListProjectUsersParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListRolesParams": + replacement: "Generated.ListRolesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListRolesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListRunsParams": + replacement: "Generated.ListRunsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListRunsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListRunsResponse": + replacement: "Generated.ListRunsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListRunStepsParams": + replacement: "Generated.ListRunStepsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListRunStepsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListRunStepsResponse": + replacement: "Generated.ListRunStepsResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListThreadItemsMethodParams": + replacement: "Generated.ListThreadItemsMethodParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListThreadsMethodParams": + replacement: "Generated.ListThreadsMethodParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListUserRoleAssignmentsParams": + replacement: "Generated.ListUserRoleAssignmentsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListUserRoleAssignmentsParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListUsersParams": + replacement: "Generated.ListUsersParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListVectorStoreFilesParams": + replacement: "Generated.ListVectorStoreFilesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListVectorStoreFilesParamsFilter": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListVectorStoreFilesParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListVectorStoreFilesResponse": + replacement: "Generated.ListVectorStoreFilesResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListVectorStoresParams": + replacement: "Generated.ListVectorStoresParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListVectorStoresParamsOrder": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ListVectorStoresResponse": + replacement: "Generated.ListVectorStoresResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ListVideosParams": + replacement: "Generated.ListVideosParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LocalShellCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellExecAction": + replacement: "Generated.LocalShellExecAction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LocalShellExecActionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellToolCall": + replacement: "Generated.LocalShellToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LocalShellToolCallOutput": + replacement: "Generated.LocalShellToolCallOutput" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LocalShellToolCallOutputStatusEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellToolCallOutputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LocalShellToolParam": + replacement: "Generated.LocalShellToolParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LocalShellToolParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LockedStatus": + replacement: "Generated.LockedStatus" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#LockedStatusType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#LogProb": + replacement: "Generated.LogProb" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#make": + replacement: "Generated.make" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPApprovalRequest": + replacement: "Generated.MCPApprovalRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPApprovalRequestType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPApprovalResponse": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPApprovalResponseResource": + replacement: "Generated.MCPApprovalResponseResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPApprovalResponseResourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPApprovalResponseType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPListTools": + replacement: "Generated.MCPListTools" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPListToolsTool": + replacement: "Generated.MCPListToolsTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPListToolsType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPTool": + replacement: "Generated.MCPTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPToolCall": + replacement: "Generated.MCPToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPToolConnectorId": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPToolFilter": + replacement: "Generated.MCPToolFilter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MCPToolRequireApprovalEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MCPToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Message": + replacement: "Generated.Message" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContent": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentImageFileObject": + replacement: "Generated.MessageContentImageFileObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentImageFileObjectImageFileDetail": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentImageFileObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentImageUrlObject": + replacement: "Generated.MessageContentImageUrlObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentImageUrlObjectImageUrlDetail": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentImageUrlObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentRefusalObject": + replacement: "Generated.MessageContentRefusalObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentRefusalObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentTextAnnotationsFileCitationObject": + replacement: "Generated.MessageContentTextAnnotationsFileCitationObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentTextAnnotationsFileCitationObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentTextAnnotationsFilePathObject": + replacement: "Generated.MessageContentTextAnnotationsFilePathObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentTextAnnotationsFilePathObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageContentTextObject": + replacement: "Generated.MessageContentTextObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageContentTextObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageObject": + replacement: "Generated.MessageObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageObjectIncompleteDetailsEnumReason": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageObjectRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageObjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageRequestContentTextObject": + replacement: "Generated.MessageRequestContentTextObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#MessageRequestContentTextObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MessageStatus": + replacement: "OpenAiSchema.MessageStatus" + note: "Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated." +"@effect/ai-openai/Generated#MessageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Metadata": + replacement: "Generated.Metadata" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Model": + replacement: "Generated.Model" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModelIdsResponses": + replacement: "Generated.ModelIdsResponses" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModelIdsResponsesEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModelIdsShared": + replacement: "Generated.ModelIdsShared" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModelObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModerationImageURLInput": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModerationImageURLInputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModerationTextInput": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModerationTextInputType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ModifyAssistantRequest": + replacement: "Generated.ModifyAssistantRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModifyCertificateRequest": + replacement: "Generated.ModifyCertificateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModifyMessageRequest": + replacement: "Generated.ModifyMessageRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModifyRunRequest": + replacement: "Generated.ModifyRunRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ModifyThreadRequest": + replacement: "Generated.ModifyThreadRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Move": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#MoveType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#NoiseReductionType": + replacement: "Generated.NoiseReductionType" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OpenAIFile": + replacement: "Generated.OpenAIFile" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OpenAIFileObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OpenAIFilePurpose": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OpenAIFileStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OrderEnum": + replacement: "Generated.OrderEnum" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OtherChunkingStrategyResponseParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OtherChunkingStrategyResponseParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OutputItem": + replacement: "Generated.OutputItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OutputMessage": + replacement: "Generated.OutputMessage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OutputMessageContent": + replacement: "Generated.OutputMessageContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OutputMessageRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OutputMessageStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OutputMessageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#OutputTextContent": + replacement: "Generated.OutputTextContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#OutputTextContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ParallelToolCalls": + replacement: "Generated.ParallelToolCalls" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PartialImages": + replacement: "Generated.PartialImages" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PredictionContent": + replacement: "Generated.PredictionContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PredictionContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Project": + replacement: "Generated.Project" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectApiKey": + replacement: "Generated.ProjectApiKey" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectApiKeyDeleteResponse": + replacement: "Generated.ProjectApiKeyDeleteResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectApiKeyDeleteResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectApiKeyListResponse": + replacement: "Generated.ProjectApiKeyListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectApiKeyListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectApiKeyObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectApiKeyOwnerType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectCreateRequest": + replacement: "Generated.ProjectCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectCreateRequestGeography": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectGroup": + replacement: "Generated.ProjectGroup" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectGroupDeletedResource": + replacement: "Generated.ProjectGroupDeletedResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectGroupDeletedResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectGroupListResource": + replacement: "Generated.ProjectGroupListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectGroupListResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectGroupObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectListResponse": + replacement: "Generated.ProjectListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectRateLimit": + replacement: "Generated.ProjectRateLimit" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectRateLimitListResponse": + replacement: "Generated.ProjectRateLimitListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectRateLimitListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectRateLimitObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectRateLimitUpdateRequest": + replacement: "Generated.ProjectRateLimitUpdateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccount": + replacement: "Generated.ProjectServiceAccount" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountApiKey": + replacement: "Generated.ProjectServiceAccountApiKey" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountApiKeyObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountCreateRequest": + replacement: "Generated.ProjectServiceAccountCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountCreateResponse": + replacement: "Generated.ProjectServiceAccountCreateResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountCreateResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountCreateResponseRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountDeleteResponse": + replacement: "Generated.ProjectServiceAccountDeleteResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountDeleteResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountListResponse": + replacement: "Generated.ProjectServiceAccountListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectServiceAccountListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectServiceAccountRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectUpdateRequest": + replacement: "Generated.ProjectUpdateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUser": + replacement: "Generated.ProjectUser" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUserCreateRequest": + replacement: "Generated.ProjectUserCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUserCreateRequestRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectUserDeleteResponse": + replacement: "Generated.ProjectUserDeleteResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUserDeleteResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectUserListResponse": + replacement: "Generated.ProjectUserListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUserObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectUserRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ProjectUserUpdateRequest": + replacement: "Generated.ProjectUserUpdateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ProjectUserUpdateRequestRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Prompt": + replacement: "Generated.Prompt" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PublicAssignOrganizationGroupRoleBody": + replacement: "Generated.PublicAssignOrganizationGroupRoleBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PublicCreateOrganizationRoleBody": + replacement: "Generated.PublicCreateOrganizationRoleBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PublicRoleListResource": + replacement: "Generated.PublicRoleListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#PublicRoleListResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#PublicUpdateOrganizationRoleBody": + replacement: "Generated.PublicUpdateOrganizationRoleBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RankerVersionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RankingOptions": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RateLimitsParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeAudioFormats": + replacement: "Generated.RealtimeAudioFormats" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeAudioFormatsEnumRate": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeAudioFormatsEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeCallCreateRequest": + replacement: "Generated.RealtimeCallCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeCallReferRequest": + replacement: "Generated.RealtimeCallReferRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeCallRejectRequest": + replacement: "Generated.RealtimeCallRejectRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeCreateClientSecretRequest": + replacement: "Generated.RealtimeCreateClientSecretRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeCreateClientSecretRequestExpiresAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeCreateClientSecretResponse": + replacement: "Generated.RealtimeCreateClientSecretResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeFunctionTool": + replacement: "Generated.RealtimeFunctionTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeFunctionToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequest": + replacement: "Generated.RealtimeSessionCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestGA": + replacement: "Generated.RealtimeSessionCreateRequestGA" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestGAMaxOutputTokensEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestGAModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestGATracingEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestGAType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestMaxResponseOutputTokensEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateRequestTracingEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponse": + replacement: "Generated.RealtimeSessionCreateResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseGA": + replacement: "Generated.RealtimeSessionCreateResponseGA" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseGAMaxOutputTokensEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseGAModelEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseGATracingEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseGAType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseMaxOutputTokensEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeSessionCreateResponseTracingEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateRequest": + replacement: "Generated.RealtimeTranscriptionSessionCreateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateRequestGA": + replacement: "Generated.RealtimeTranscriptionSessionCreateRequestGA" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateRequestGAType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateRequestInputAudioFormat": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateRequestTurnDetectionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateResponse": + replacement: "Generated.RealtimeTranscriptionSessionCreateResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateResponseGA": + replacement: "Generated.RealtimeTranscriptionSessionCreateResponseGA" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTranscriptionSessionCreateResponseGAType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTruncation": + replacement: "Generated.RealtimeTruncation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTruncationEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTruncationEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RealtimeTurnDetection": + replacement: "Generated.RealtimeTurnDetection" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RealtimeTurnDetectionEnumEagerness": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Reasoning": + replacement: "Generated.Reasoning" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ReasoningEffort": + replacement: "Generated.ReasoningEffort" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ReasoningEffortEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ReasoningGenerateSummaryEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ReasoningItem": + replacement: "Generated.ReasoningItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ReasoningItemStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ReasoningItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ReasoningSummaryEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ReasoningTextContent": + replacement: "Generated.ReasoningTextContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ReasoningTextContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RefusalContent": + replacement: "Generated.RefusalContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RefusalContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Response": + replacement: "Generated.Response" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseError": + replacement: "Generated.ResponseError" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseErrorCode": + replacement: "Generated.ResponseErrorCode" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseFormatJsonObject": + replacement: "Generated.ResponseFormatJsonObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseFormatJsonObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseFormatJsonSchema": + replacement: "Generated.ResponseFormatJsonSchema" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseFormatJsonSchemaSchema": + replacement: "Generated.ResponseFormatJsonSchemaSchema" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseFormatJsonSchemaType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseFormatText": + replacement: "Generated.ResponseFormatText" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseFormatTextType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseIncompleteDetailsEnumReason": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseItemList": + replacement: "Generated.ResponseItemList" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseModalities": + replacement: "Generated.ResponseModalities" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseOutputText": + replacement: "Generated.ResponseOutputText" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseOutputTextType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponsePromptCacheRetentionEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponsePromptVariables": + replacement: "Generated.ResponsePromptVariables" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseStreamOptions": + replacement: "Generated.ResponseStreamOptions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseTextParam": + replacement: "Generated.ResponseTextParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ResponseTruncationEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ResponseUsage": + replacement: "Generated.ResponseUsage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RetrieveVideoContent200": + replacement: "Generated.RetrieveVideoContent200" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RetrieveVideoContentParams": + replacement: "Generated.RetrieveVideoContentParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#Role": + replacement: "Generated.Role" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RoleDeletedResource": + replacement: "Generated.RoleDeletedResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RoleDeletedResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RoleListResource": + replacement: "Generated.RoleListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RoleListResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RoleObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunCompletionUsage": + replacement: "Generated.RunCompletionUsage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunGraderRequest": + replacement: "Generated.RunGraderRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunGraderResponse": + replacement: "Generated.RunGraderResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunObject": + replacement: "Generated.RunObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunObjectIncompleteDetailsReason": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectLastErrorCode": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectRequiredActionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectToolChoice": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectToolChoiceEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectTruncationStrategy": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunObjectTruncationStrategyEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepCompletionUsage": + replacement: "Generated.RunStepCompletionUsage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsMessageCreationObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsMessageCreationObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCall": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeObject": + replacement: "Generated.RunStepDetailsToolCallsCodeObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputImageObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputImageObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputLogsObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsCodeOutputLogsObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFileSearchObject": + replacement: "Generated.RunStepDetailsToolCallsFileSearchObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFileSearchObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFileSearchRankingOptionsObject": + replacement: "Generated.RunStepDetailsToolCallsFileSearchRankingOptionsObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFileSearchResultObject": + replacement: "Generated.RunStepDetailsToolCallsFileSearchResultObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFunctionObject": + replacement: "Generated.RunStepDetailsToolCallsFunctionObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsFunctionObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepDetailsToolCallsObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepObject": + replacement: "Generated.RunStepObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunStepObjectLastErrorEnumCode": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepObjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunStepObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#RunToolCallObject": + replacement: "Generated.RunToolCallObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#RunToolCallObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Screenshot": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ScreenshotType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Scroll": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ScrollType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#SearchContextSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ServiceTier": + replacement: "Generated.ServiceTier" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ServiceTierEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#SpecificApplyPatchParam": + replacement: "Generated.SpecificApplyPatchParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#SpecificApplyPatchParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#SpecificFunctionShellParam": + replacement: "Generated.SpecificFunctionShellParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#SpecificFunctionShellParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#StaticChunkingStrategy": + replacement: "Generated.StaticChunkingStrategy" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#StaticChunkingStrategyRequestParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#StaticChunkingStrategyRequestParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#StaticChunkingStrategyResponseParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#StaticChunkingStrategyResponseParamType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#StopConfiguration": + replacement: "Generated.StopConfiguration" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#SubmitToolOutputsRunRequest": + replacement: "Generated.SubmitToolOutputsRunRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#SummaryTextContent": + replacement: "Generated.SummaryTextContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#SummaryTextContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#SummaryType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TaskGroupItem": + replacement: "Generated.TaskGroupItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TaskGroupItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TaskGroupItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TaskGroupTask": + replacement: "Generated.TaskGroupTask" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TaskItem": + replacement: "Generated.TaskItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TaskItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TaskItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TaskType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TextAnnotation": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TextContent": + replacement: "Generated.TextContent" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TextContentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TextResponseFormatConfiguration": + replacement: "Generated.TextResponseFormatConfiguration" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TextResponseFormatJsonSchema": + replacement: "Generated.TextResponseFormatJsonSchema" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TextResponseFormatJsonSchemaType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ThreadItem": + replacement: "Generated.ThreadItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ThreadItemListResource": + replacement: "Generated.ThreadItemListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ThreadListResource": + replacement: "Generated.ThreadListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ThreadObject": + replacement: "Generated.ThreadObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ThreadObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ThreadResource": + replacement: "Generated.ThreadResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ThreadResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToggleCertificatesRequest": + replacement: "Generated.ToggleCertificatesRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TokenCountsBody": + replacement: "Generated.TokenCountsBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TokenCountsResource": + replacement: "Generated.TokenCountsResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TokenCountsResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Tool": + replacement: "Generated.Tool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoice": + replacement: "OpenAiSchema.ToolChoice" + note: "Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated." +"@effect/ai-openai/Generated#ToolChoiceAllowed": + replacement: "Generated.ToolChoiceAllowed" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceAllowedMode": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolChoiceAllowedType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolChoiceCustom": + replacement: "Generated.ToolChoiceCustom" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceCustomType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolChoiceFunction": + replacement: "Generated.ToolChoiceFunction" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceFunctionType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolChoiceMCP": + replacement: "Generated.ToolChoiceMCP" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceMCPType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolChoiceOptions": + replacement: "Generated.ToolChoiceOptions" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceParam": + replacement: "Generated.ToolChoiceParam" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceTypes": + replacement: "Generated.ToolChoiceTypes" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ToolChoiceTypesType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ToolsArray": + replacement: "Generated.ToolsArray" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TopLogProb": + replacement: "Generated.TopLogProb" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptionChunkingStrategy": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TranscriptionChunkingStrategyEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TranscriptionDiarizedSegment": + replacement: "Generated.TranscriptionDiarizedSegment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptionDiarizedSegmentType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TranscriptionInclude": + replacement: "Generated.TranscriptionInclude" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptionSegment": + replacement: "Generated.TranscriptionSegment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptionWord": + replacement: "Generated.TranscriptionWord" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptTextUsageDuration": + replacement: "Generated.TranscriptTextUsageDuration" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptTextUsageDurationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TranscriptTextUsageTokens": + replacement: "Generated.TranscriptTextUsageTokens" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#TranscriptTextUsageTokensType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TruncationEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Type": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#TypeType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UpdateChatCompletionRequest": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UpdateConversationBody": + replacement: "Generated.UpdateConversationBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UpdateEvalRequest": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UpdateGroupBody": + replacement: "Generated.UpdateGroupBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UpdateVectorStoreFileAttributesRequest": + replacement: "Generated.UpdateVectorStoreFileAttributesRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UpdateVectorStoreRequest": + replacement: "Generated.UpdateVectorStoreRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UpdateVectorStoreRequestExpiresAfter": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UpdateVectorStoreRequestExpiresAfterEnumAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Upload": + replacement: "Generated.Upload" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UploadCertificateRequest": + replacement: "Generated.UploadCertificateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UploadFile": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadFileEnumObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadFileEnumPurpose": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadFileEnumStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadPart": + replacement: "Generated.UploadPart" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UploadPartObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UploadStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UrlAnnotation": + replacement: "Generated.UrlAnnotation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UrlAnnotationSource": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UrlAnnotationSourceType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UrlAnnotationType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UrlCitationBody": + replacement: "Generated.UrlCitationBody" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UrlCitationBodyType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageAudioSpeechesParams": + replacement: "Generated.UsageAudioSpeechesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageAudioSpeechesParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageAudioSpeechesResult": + replacement: "Generated.UsageAudioSpeechesResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageAudioSpeechesResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageAudioTranscriptionsParams": + replacement: "Generated.UsageAudioTranscriptionsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageAudioTranscriptionsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageAudioTranscriptionsResult": + replacement: "Generated.UsageAudioTranscriptionsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageAudioTranscriptionsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageCodeInterpreterSessionsParams": + replacement: "Generated.UsageCodeInterpreterSessionsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageCodeInterpreterSessionsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageCodeInterpreterSessionsResult": + replacement: "Generated.UsageCodeInterpreterSessionsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageCodeInterpreterSessionsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageCompletionsParams": + replacement: "Generated.UsageCompletionsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageCompletionsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageCompletionsResult": + replacement: "Generated.UsageCompletionsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageCompletionsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageCostsParams": + replacement: "Generated.UsageCostsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageCostsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageEmbeddingsParams": + replacement: "Generated.UsageEmbeddingsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageEmbeddingsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageEmbeddingsResult": + replacement: "Generated.UsageEmbeddingsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageEmbeddingsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageImagesParams": + replacement: "Generated.UsageImagesParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageImagesParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageImagesResult": + replacement: "Generated.UsageImagesResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageImagesResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageModerationsParams": + replacement: "Generated.UsageModerationsParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageModerationsParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageModerationsResult": + replacement: "Generated.UsageModerationsResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageModerationsResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageResponse": + replacement: "Generated.UsageResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageTimeBucket": + replacement: "Generated.UsageTimeBucket" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageTimeBucketObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageVectorStoresParams": + replacement: "Generated.UsageVectorStoresParams" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageVectorStoresParamsBucketWidth": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UsageVectorStoresResult": + replacement: "Generated.UsageVectorStoresResult" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UsageVectorStoresResultObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#User": + replacement: "Generated.User" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserDeleteResponse": + replacement: "Generated.UserDeleteResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserDeleteResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserListResource": + replacement: "Generated.UserListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserListResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserListResponse": + replacement: "Generated.UserListResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserListResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserMessageInputText": + replacement: "Generated.UserMessageInputText" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserMessageInputTextType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserMessageItem": + replacement: "Generated.UserMessageItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserMessageItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserMessageItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserMessageQuotedText": + replacement: "Generated.UserMessageQuotedText" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserMessageQuotedTextType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserRoleAssignment": + replacement: "Generated.UserRoleAssignment" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserRoleAssignmentObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#UserRoleUpdateRequest": + replacement: "Generated.UserRoleUpdateRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#UserRoleUpdateRequestRole": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VadConfig": + replacement: "Generated.VadConfig" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VadConfigType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#ValidateGraderRequest": + replacement: "Generated.ValidateGraderRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#ValidateGraderResponse": + replacement: "Generated.ValidateGraderResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreExpirationAfter": + replacement: "Generated.VectorStoreExpirationAfter" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreExpirationAfterAnchor": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileAttributes": + replacement: "Generated.VectorStoreFileAttributes" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreFileBatchObject": + replacement: "Generated.VectorStoreFileBatchObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreFileBatchObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileBatchObjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileContentResponse": + replacement: "Generated.VectorStoreFileContentResponse" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreFileContentResponseObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileObject": + replacement: "Generated.VectorStoreFileObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreFileObjectLastErrorEnumCode": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreFileObjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreObject": + replacement: "Generated.VectorStoreObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreObjectObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreObjectStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreSearchRequest": + replacement: "Generated.VectorStoreSearchRequest" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreSearchRequestRankingOptionsRanker": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreSearchResultContentObject": + replacement: "Generated.VectorStoreSearchResultContentObject" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreSearchResultContentObjectType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VectorStoreSearchResultItem": + replacement: "Generated.VectorStoreSearchResultItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreSearchResultsPage": + replacement: "Generated.VectorStoreSearchResultsPage" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VectorStoreSearchResultsPageObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Verbosity": + replacement: "Generated.Verbosity" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VerbosityEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VideoContentVariant": + replacement: "Generated.VideoContentVariant" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VideoListResource": + replacement: "Generated.VideoListResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VideoModel": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VideoResource": + replacement: "Generated.VideoResource" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VideoResourceObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VideoSeconds": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VideoSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VideoStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#VoiceIdsShared": + replacement: "Generated.VoiceIdsShared" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#VoiceIdsSharedEnum": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#Wait": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WaitType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionFind": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionFindType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionOpenPage": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionOpenPageType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionSearch": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchActionSearchType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchApproximateLocation": + replacement: "Generated.WebSearchApproximateLocation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchApproximateLocationEnumType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchContextSize": + replacement: "Generated.WebSearchContextSize" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchLocation": + replacement: "Generated.WebSearchLocation" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchPreviewTool": + replacement: "Generated.WebSearchPreviewTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchPreviewToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchTool": + replacement: "Generated.WebSearchTool" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchToolCall": + replacement: "Generated.WebSearchToolCall" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WebSearchToolCallStatus": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchToolCallType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchToolSearchContextSize": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WebSearchToolType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WidgetMessageItem": + replacement: "Generated.WidgetMessageItem" + note: "Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openai/Generated#WidgetMessageItemObject": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WidgetMessageItemType": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WorkflowParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openai/Generated#WorkflowTracingParam": + replacement: "none" + note: "Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiClient.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiClient.yaml new file mode 100644 index 000000000..2395a07f4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiClient.yaml @@ -0,0 +1,162 @@ +"@effect/ai-openai/OpenAiClient#LogProbs": + replacement: "Generated.LogProb" + note: "The client-local log-probability schema moved to the regenerated v4 OpenAI schema surface and changed shape." +"@effect/ai-openai/OpenAiClient#ResponseCodeInterpreterCallCodeDeltaEvent": + replacement: "Generated.ResponseCodeInterpreterCallCodeDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCodeInterpreterCallCodeDoneEvent": + replacement: "Generated.ResponseCodeInterpreterCallCodeDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCodeInterpreterCallCompletedEvent": + replacement: "Generated.ResponseCodeInterpreterCallCompletedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCodeInterpreterCallInProgressEvent": + replacement: "Generated.ResponseCodeInterpreterCallInProgressEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCodeInterpreterCallInterpretingEvent": + replacement: "Generated.ResponseCodeInterpreterCallInterpretingEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCompletedEvent": + replacement: "Generated.ResponseCompletedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseContentPartAddedEvent": + replacement: "Generated.ResponseContentPartAddedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseContentPartDoneEvent": + replacement: "Generated.ResponseContentPartDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCreatedEvent": + replacement: "Generated.ResponseCreatedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCustomToolCallInputDeltaEvent": + replacement: "Generated.ResponseCustomToolCallInputDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseCustomToolCallInputDoneEvent": + replacement: "Generated.ResponseCustomToolCallInputDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseErrorEvent": + replacement: "Generated.ResponseErrorEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFailedEvent": + replacement: "Generated.ResponseFailedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFileSearchCallCompletedEvent": + replacement: "Generated.ResponseFileSearchCallCompletedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFileSearchCallInProgressEvent": + replacement: "Generated.ResponseFileSearchCallInProgressEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFileSearchCallSearchingEvent": + replacement: "Generated.ResponseFileSearchCallSearchingEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFunctionCallArgumentsDeltaEvent": + replacement: "Generated.ResponseFunctionCallArgumentsDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseFunctionCallArgumentsDoneEvent": + replacement: "Generated.ResponseFunctionCallArgumentsDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseImageGenerationCallCompletedEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseImageGenerationCallGeneratingEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseImageGenerationCallInProgressEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseImageGenerationCallPartialImageEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseIncompleteEvent": + replacement: "Generated.ResponseIncompleteEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseInProgressEvent": + replacement: "Generated.ResponseInProgressEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseMcpCallArgumentsDeltaEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpCallArgumentsDoneEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpCallCompletedEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpCallFailedEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpCallInProgressEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpListToolsCompletedEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpListToolsFailedEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseMcpListToolsInProgressEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseOutputItemAddedEvent": + replacement: "Generated.ResponseOutputItemAddedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseOutputItemDoneEvent": + replacement: "Generated.ResponseOutputItemDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseOutputTextAnnotationAddedEvent": + replacement: "Generated.ResponseOutputTextAnnotationAddedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseOutputTextDeltaEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseOutputTextDoneEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator." +"@effect/ai-openai/OpenAiClient#ResponseQueuedEvent": + replacement: "Generated.ResponseQueuedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningSummaryPartAddedEvent": + replacement: "Generated.ResponseReasoningSummaryPartAddedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningSummaryPartDoneEvent": + replacement: "Generated.ResponseReasoningSummaryPartDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningSummaryTextDeltaEvent": + replacement: "Generated.ResponseReasoningSummaryTextDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningSummaryTextDoneEvent": + replacement: "Generated.ResponseReasoningSummaryTextDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningTextDeltaEvent": + replacement: "Generated.ResponseReasoningTextDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseReasoningTextDoneEvent": + replacement: "Generated.ResponseReasoningTextDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseRefusalDeltaEvent": + replacement: "Generated.ResponseRefusalDeltaEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseRefusalDoneEvent": + replacement: "Generated.ResponseRefusalDoneEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseStreamEvent": + replacement: "OpenAiClient.ResponseStreamEvent" + note: "Still exported in v4; adapt to the rewritten Responses API client and its revised schema and error types." +"@effect/ai-openai/OpenAiClient#ResponseWebSearchCallCompletedEvent": + replacement: "Generated.ResponseWebSearchCallCompletedEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseWebSearchCallInProgressEvent": + replacement: "Generated.ResponseWebSearchCallInProgressEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#ResponseWebSearchCallSearchingEvent": + replacement: "Generated.ResponseWebSearchCallSearchingEvent" + note: "The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape." +"@effect/ai-openai/OpenAiClient#Service": + replacement: "OpenAiClient.Service" + note: "Still exported in v4; adapt to the rewritten Responses API client and its revised schema and error types." +"@effect/ai-openai/OpenAiClient#StreamCompletionRequest": + replacement: "OpenAiSchema.CreateResponse.Encoded" + note: "The chat-completions request alias was removed; the v4 client uses the Responses API, with streaming inferred by OpenAiClient.createResponseStream." +"@effect/ai-openai/OpenAiClient#SummaryPart": + replacement: "OpenAiSchema.SummaryTextContent" + note: "The client-local reasoning summary schema moved to the focused v4 OpenAiSchema module." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiConfig.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiConfig.yaml new file mode 100644 index 000000000..b9a58fd0e --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiConfig.yaml @@ -0,0 +1,6 @@ +"@effect/ai-openai/OpenAiConfig#OpenAiConfig": + replacement: "OpenAiConfig.OpenAiConfig" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiConfig#OpenAiConfig.Service": + replacement: "OpenAiConfig.OpenAiConfig.Service" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml new file mode 100644 index 000000000..7152df239 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiEmbeddingModel.yaml @@ -0,0 +1,24 @@ +"@effect/ai-openai/OpenAiEmbeddingModel#Config": + replacement: "OpenAiEmbeddingModel.Config" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiEmbeddingModel#Config.Batched": + replacement: "OpenAiEmbeddingModel.Config.Service" + note: "Batch-mode configuration was removed; use the unified embedding config and constructor." +"@effect/ai-openai/OpenAiEmbeddingModel#Config.DataLoader": + replacement: "OpenAiEmbeddingModel.Config.Service" + note: "Data-loader configuration was removed; use the unified embedding config and constructor." +"@effect/ai-openai/OpenAiEmbeddingModel#Config.Service": + replacement: "OpenAiEmbeddingModel.Config.Service" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiEmbeddingModel#layerBatched": + replacement: "OpenAiEmbeddingModel.layer" + note: "The batched and data-loader layers were replaced by one embedding layer; pass the model and request config explicitly." +"@effect/ai-openai/OpenAiEmbeddingModel#layerDataLoader": + replacement: "OpenAiEmbeddingModel.layer" + note: "The batched and data-loader layers were replaced by one embedding layer; pass the model and request config explicitly." +"@effect/ai-openai/OpenAiEmbeddingModel#makeDataLoader": + replacement: "OpenAiEmbeddingModel.make" + note: "The dedicated data-loader constructor was removed; use the unified v4 embedding service constructor." +"@effect/ai-openai/OpenAiEmbeddingModel#Model": + replacement: "OpenAiEmbeddingModel.Model" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml new file mode 100644 index 000000000..21af9e0d6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiLanguageModel.yaml @@ -0,0 +1,21 @@ +"@effect/ai-openai/OpenAiLanguageModel#Config": + replacement: "OpenAiLanguageModel.Config" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiLanguageModel#Config.Service": + replacement: "OpenAiLanguageModel.Config.Service" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiLanguageModel#layerWithTokenizer": + replacement: "OpenAiLanguageModel.layer" + note: "The tokenizer-combining layer was removed; provide the language model and any Tokenizer service separately." +"@effect/ai-openai/OpenAiLanguageModel#Model": + replacement: "OpenAiLanguageModel.Model" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiLanguageModel#modelWithTokenizer": + replacement: "OpenAiLanguageModel.model" + note: "The tokenizer-combining model was removed; use the v4 model descriptor and provide any Tokenizer service separately." +"@effect/ai-openai/OpenAiLanguageModel#ProviderMetadata": + replacement: "Prompt.ProviderOptions / Response.ProviderMetadata" + note: "The OpenAI metadata service wrapper was removed; v4 declares OpenAI-specific fields directly on Prompt and Response provider metadata." +"@effect/ai-openai/OpenAiLanguageModel#ProviderMetadata.Service": + replacement: "Prompt.ProviderOptions / Response.ProviderMetadata" + note: "The OpenAI metadata service wrapper was removed; v4 declares OpenAI-specific fields directly on Prompt and Response provider metadata." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml new file mode 100644 index 000000000..52fb66673 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiTelemetry.yaml @@ -0,0 +1,9 @@ +"@effect/ai-openai/OpenAiTelemetry#addGenAIAnnotations": + replacement: "OpenAiTelemetry.addGenAIAnnotations" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiTelemetry#AllAttributes": + replacement: "OpenAiTelemetry.AllAttributes" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." +"@effect/ai-openai/OpenAiTelemetry#OpenAiTelemetryAttributeOptions": + replacement: "OpenAiTelemetry.OpenAiTelemetryAttributeOptions" + note: "Still exported in v4; update imports and adapt to the revised v4 service and schema types." diff --git a/.context/effect/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml b/.context/effect/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml new file mode 100644 index 000000000..9c459de0c --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__OpenAiTokenizer.yaml @@ -0,0 +1,6 @@ +"@effect/ai-openai/OpenAiTokenizer#layer": + replacement: "Tokenizer.make" + note: "The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using gpt-tokenizer if equivalent OpenAI counting is required." +"@effect/ai-openai/OpenAiTokenizer#make": + replacement: "Tokenizer.make" + note: "The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using gpt-tokenizer if equivalent OpenAI counting is required." diff --git a/.context/effect/migration/annotations/effect__ai-openai__index.yaml b/.context/effect/migration/annotations/effect__ai-openai__index.yaml new file mode 100644 index 000000000..5f0446a2b --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openai__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai-openai/index": + replacement: "@effect/ai-openai" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-openai package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__ai-openrouter__Generated.yaml b/.context/effect/migration/annotations/effect__ai-openrouter__Generated.yaml new file mode 100644 index 000000000..611408a13 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openrouter__Generated.yaml @@ -0,0 +1,1053 @@ +"@effect/ai-openrouter/Generated#ActivityItem": + replacement: "Generated.ActivityItem" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#AnnotationDetail": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequestProviderSort": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequestRoute": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequestServiceTier": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequestThinkingEnumType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesRequestToolChoiceEnumType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesResponse": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesResponseRole": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesResponseStopReason": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesResponseType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AnthropicMessagesResponseUsageServiceTier": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#AssistantMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#BadGatewayResponse": + replacement: "Generated.BadGatewayResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BadGatewayResponseErrorData": + replacement: "Generated.BadGatewayResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BadRequestResponse": + replacement: "Generated.BadRequestResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BadRequestResponseErrorData": + replacement: "Generated.BadRequestResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BigNumberUnion": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#BulkAssignKeysToGuardrail200": + replacement: "Generated.BulkAssignKeysToGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BulkAssignKeysToGuardrailRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#BulkAssignMembersToGuardrail200": + replacement: "Generated.BulkAssignMembersToGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BulkAssignMembersToGuardrailRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#BulkUnassignKeysFromGuardrail200": + replacement: "Generated.BulkUnassignKeysFromGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BulkUnassignKeysFromGuardrailRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#BulkUnassignMembersFromGuardrail200": + replacement: "Generated.BulkUnassignMembersFromGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#BulkUnassignMembersFromGuardrailRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CacheControlEphemeral": + replacement: "Generated.ChatContentCacheControl" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatCompletionFinishReason": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatError": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatGenerationParams": + replacement: "Generated.ChatRequest" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatGenerationParamsProviderEnumDataCollectionEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatGenerationParamsReasoningEffortEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatGenerationParamsRouteEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatGenerationTokenUsage": + replacement: "Generated.ChatUsage" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageContentItem": + replacement: "Generated.ChatContentItems" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageContentItemAudio": + replacement: "Generated.ChatContentAudio" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageContentItemCacheControl": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatMessageContentItemCacheControlTtl": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatMessageContentItemImage": + replacement: "Generated.ChatContentImage" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageContentItemImageImageUrlDetail": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatMessageContentItemText": + replacement: "Generated.ChatContentText" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageContentItemVideo": + replacement: "Generated.ChatContentVideo" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageTokenLogprob": + replacement: "Generated.ChatTokenLogprob" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageTokenLogprobs": + replacement: "Generated.ChatTokenLogprobs" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatMessageToolCall": + replacement: "Generated.ChatToolCall" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatResponse": + replacement: "Generated.ChatResult" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ChatResponseChoice": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ChatStreamOptions": + replacement: "Generated.ChatStreamOptions" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#Client": + replacement: "Generated.OpenRouterClient" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ClientError": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionChoice": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionCreateParams": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionFinishReason": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionFinishReasonEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionLogprobs": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionResponse": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CompletionUsage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateAuthKeysCode200": + replacement: "Generated.CreateAuthKeysCode200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateAuthKeysCodeRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateAuthKeysCodeRequestCodeChallengeMethod": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateChargeRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateChargeRequestChainId": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateCoinbaseCharge200": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateEmbeddings200": + replacement: "Generated.CreateEmbeddings200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateEmbeddings200Object": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateEmbeddingsRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateEmbeddingsRequestEncodingFormat": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateGuardrail201": + replacement: "Generated.CreateGuardrail201" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateGuardrail201DataResetInterval": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateGuardrailRequest": + replacement: "Generated.CreateGuardrailRequest" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateGuardrailRequestResetInterval": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateKeys201": + replacement: "Generated.CreateKeys201" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateKeysRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateKeysRequestLimitReset": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages400": + replacement: "Generated.CreateMessages400" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages400Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages401": + replacement: "Generated.CreateMessages401" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages401Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages403": + replacement: "Generated.CreateMessages403" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages403Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages404": + replacement: "Generated.CreateMessages404" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages404Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages429": + replacement: "Generated.CreateMessages429" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages429Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages500": + replacement: "Generated.CreateMessages500" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages500Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages503": + replacement: "Generated.CreateMessages503" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages503Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#CreateMessages529": + replacement: "Generated.CreateMessages529" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#CreateMessages529Type": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#DataCollection": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#DefaultParameters": + replacement: "Generated.DefaultParameters" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#DeleteGuardrail200": + replacement: "Generated.DeleteGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#DeleteKeys200": + replacement: "Generated.DeleteKeys200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#EdgeNetworkTimeoutResponse": + replacement: "Generated.EdgeNetworkTimeoutResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#EdgeNetworkTimeoutResponseErrorData": + replacement: "Generated.EdgeNetworkTimeoutResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#EndpointStatus": + replacement: "Generated.EndpointStatus" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ExchangeAuthCodeForAPIKey200": + replacement: "Generated.ExchangeAuthCodeForAPIKey200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ExchangeAuthCodeForAPIKeyRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#FileAnnotationDetail": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#FileCitation": + replacement: "Generated.FileCitation" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#FileCitationType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#FilePath": + replacement: "Generated.FilePath" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#FilePathType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ForbiddenResponse": + replacement: "Generated.ForbiddenResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ForbiddenResponseErrorData": + replacement: "Generated.ForbiddenResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetCredits200": + replacement: "Generated.GetCredits200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetCurrentKey200": + replacement: "Generated.GetCurrentKey200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetGeneration200": + replacement: "Generated.GetGeneration200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetGeneration200DataApiType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#GetGenerationParams": + replacement: "Generated.GetGenerationParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetGuardrail200": + replacement: "Generated.GetGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetGuardrail200DataResetInterval": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#GetKey200": + replacement: "Generated.GetKey200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetModelsParams": + replacement: "Generated.GetModelsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetModelsParamsCategory": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#GetUserActivity200": + replacement: "Generated.GetUserActivity200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#GetUserActivityParams": + replacement: "Generated.GetUserActivityParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ImageGenerationStatus": + replacement: "Generated.ImageGenerationStatus" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#InputModality": + replacement: "Generated.InputModality" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#InternalServerResponse": + replacement: "Generated.InternalServerResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#InternalServerResponseErrorData": + replacement: "Generated.InternalServerResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#JSONSchemaConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#List200": + replacement: "Generated.List200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListEndpoints200": + replacement: "Generated.ListEndpoints200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListEndpointsResponse": + replacement: "Generated.ListEndpointsResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListEndpointsResponseArchitecture": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ListEndpointsResponseArchitectureEnumInstructType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ListEndpointsZdr200": + replacement: "Generated.ListEndpointsZdr200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrailKeyAssignments200": + replacement: "Generated.ListGuardrailKeyAssignments200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrailKeyAssignmentsParams": + replacement: "Generated.ListGuardrailKeyAssignmentsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrailMemberAssignments200": + replacement: "Generated.ListGuardrailMemberAssignments200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrailMemberAssignmentsParams": + replacement: "Generated.ListGuardrailMemberAssignmentsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrails200": + replacement: "Generated.ListGuardrails200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListGuardrailsParams": + replacement: "Generated.ListGuardrailsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListKeyAssignments200": + replacement: "Generated.ListKeyAssignments200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListKeyAssignmentsParams": + replacement: "Generated.ListKeyAssignmentsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListMemberAssignments200": + replacement: "Generated.ListMemberAssignments200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListMemberAssignmentsParams": + replacement: "Generated.ListMemberAssignmentsParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListParams": + replacement: "Generated.ListParams" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ListProviders200": + replacement: "Generated.ListProviders200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#make": + replacement: "Generated.make" + note: "Still exported in v4, but the regenerated OpenRouter client has different operations and request/response schemas; update call sites to the current generated service." +"@effect/ai-openrouter/Generated#Message": + replacement: "Generated.ChatMessages" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#Model": + replacement: "Generated.Model" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelArchitecture": + replacement: "Generated.ModelArchitecture" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelArchitectureInstructType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ModelGroup": + replacement: "Generated.ModelGroup" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelName": + replacement: "Generated.ModelName" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelsCountResponse": + replacement: "Generated.ModelsCountResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelsListResponse": + replacement: "Generated.ModelsListResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ModelsListResponseData": + replacement: "Generated.ModelsListResponseData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#NamedToolChoice": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#NotFoundResponse": + replacement: "Generated.NotFoundResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#NotFoundResponseErrorData": + replacement: "Generated.NotFoundResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesAnnotation": + replacement: "Generated.OpenAIResponsesAnnotation" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesIncludable": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesIncompleteDetails": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesIncompleteDetailsReason": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesInput": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesPrompt": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesReasoningConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesReasoningEffort": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesRefusalContent": + replacement: "Generated.OpenAIResponsesRefusalContent" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesRefusalContentType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesResponseStatus": + replacement: "Generated.OpenAIResponsesResponseStatus" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesServiceTier": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesToolChoice": + replacement: "Generated.OpenAIResponsesToolChoice" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesToolChoiceEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesToolChoiceEnumType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesToolChoiceEnumTypeEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenAIResponsesTruncation": + replacement: "Generated.OpenAIResponsesTruncation" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenAIResponsesUsage": + replacement: "Generated.OpenAIResponsesUsage" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OpenResponsesEasyInputMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesEasyInputMessageRoleEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesEasyInputMessageType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesFunctionCallOutput": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesFunctionCallOutputType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesFunctionToolCall": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesFunctionToolCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesInput": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesInputMessageItem": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesInputMessageItemRoleEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesInputMessageItemType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesNonStreamingResponse": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesNonStreamingResponseObject": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesReasoning": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesReasoningConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesReasoningFormat": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesReasoningStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesReasoningType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequestMetadata": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequestRoute": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequestServiceTier": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequestTruncation": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesRequestTruncationEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesResponseText": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesResponseTextVerbosity": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearch20250826Tool": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearch20250826ToolType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchPreview20250311Tool": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchPreview20250311ToolType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchPreviewTool": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchPreviewToolType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchTool": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenResponsesWebSearchToolType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenRouterAnthropicMessageParam": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OpenRouterAnthropicMessageParamRole": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemFileSearchCall": + replacement: "Generated.OutputItemFileSearchCall" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputItemFileSearchCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemFunctionCall": + replacement: "Generated.OutputItemFunctionCall" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputItemFunctionCallStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemFunctionCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemImageGenerationCall": + replacement: "Generated.OutputItemImageGenerationCall" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputItemImageGenerationCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemReasoning": + replacement: "Generated.OutputItemReasoning" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputItemReasoningStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemReasoningType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputItemWebSearchCall": + replacement: "Generated.OutputItemWebSearchCall" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputItemWebSearchCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputMessage": + replacement: "Generated.OutputMessage" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#OutputMessageRole": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputMessageStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputMessageType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#OutputModality": + replacement: "Generated.OutputModality" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#Parameter": + replacement: "Generated.Parameter" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PayloadTooLargeResponse": + replacement: "Generated.PayloadTooLargeResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PayloadTooLargeResponseErrorData": + replacement: "Generated.PayloadTooLargeResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PaymentRequiredResponse": + replacement: "Generated.PaymentRequiredResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PaymentRequiredResponseErrorData": + replacement: "Generated.PaymentRequiredResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PDFParserEngine": + replacement: "Generated.PDFParserEngine" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PDFParserOptions": + replacement: "Generated.PDFParserOptions" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PercentileLatencyCutoffs": + replacement: "Generated.PercentileLatencyCutoffs" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PercentileStats": + replacement: "Generated.PercentileStats" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PercentileThroughputCutoffs": + replacement: "Generated.PercentileThroughputCutoffs" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PerRequestLimits": + replacement: "Generated.PerRequestLimits" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PreferredMaxLatency": + replacement: "Generated.PreferredMaxLatency" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PreferredMinThroughput": + replacement: "Generated.PreferredMinThroughput" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderName": + replacement: "Generated.ProviderName" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderOverloadedResponse": + replacement: "Generated.ProviderOverloadedResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderOverloadedResponseErrorData": + replacement: "Generated.ProviderOverloadedResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderPreferences": + replacement: "Generated.ProviderPreferences" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderPreferencesSort": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ProviderSort": + replacement: "Generated.ProviderSort" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderSortConfig": + replacement: "Generated.ProviderSortConfig" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ProviderSortConfigPartitionEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ProviderSortUnion": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#PublicEndpoint": + replacement: "Generated.PublicEndpoint" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#PublicEndpointQuantization": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#PublicEndpointQuantizationEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#PublicEndpointThroughputLast30M": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#PublicPricing": + replacement: "Generated.PublicPricing" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#Quantization": + replacement: "Generated.Quantization" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningDetail": + replacement: "Generated.ReasoningDetailUnion" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ReasoningDetailEncrypted": + replacement: "Generated.ReasoningDetailEncrypted" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningDetailSummary": + replacement: "Generated.ReasoningDetailSummary" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningDetailText": + replacement: "Generated.ReasoningDetailText" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningSummaryText": + replacement: "Generated.ReasoningSummaryText" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningSummaryTextType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ReasoningSummaryVerbosity": + replacement: "Generated.ReasoningSummaryVerbosity" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningTextContent": + replacement: "Generated.ReasoningTextContent" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ReasoningTextContentType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#RequestTimeoutResponse": + replacement: "Generated.RequestTimeoutResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#RequestTimeoutResponseErrorData": + replacement: "Generated.RequestTimeoutResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ResponseFormatJSONSchema": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseFormatTextConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseFormatTextGrammar": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputAudio": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputAudioInputAudioFormat": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputAudioType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputFile": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputFileType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputImage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputImageDetail": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputImageType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputText": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputTextType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputVideo": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseInputVideoType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseOutputText": + replacement: "Generated.ResponseOutputText" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ResponseOutputTextType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesErrorField": + replacement: "Generated.ResponsesErrorField" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ResponsesErrorFieldCode": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatJSONObject": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatJSONObjectType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatText": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatTextJSONSchemaConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatTextJSONSchemaConfigType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesFormatTextType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesImageGenerationCall": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesImageGenerationCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemFileSearchCall": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemFileSearchCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemFunctionCall": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemFunctionCallStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemFunctionCallType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemReasoning": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemReasoningFormat": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemReasoningStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputItemReasoningType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputMessageRole": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputMessageStatusEnum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputMessageType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesOutputModality": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesSearchContextSize": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesWebSearchCallOutput": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesWebSearchCallOutputType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesWebSearchUserLocation": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponsesWebSearchUserLocationType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseTextConfig": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ResponseTextConfigVerbosity": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema0": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema1": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema2": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema3": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema4": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema4Enum": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema5": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#Schema6": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ServiceUnavailableResponse": + replacement: "Generated.ServiceUnavailableResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ServiceUnavailableResponseErrorData": + replacement: "Generated.ServiceUnavailableResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#SystemMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#ToolCallStatus": + replacement: "Generated.ToolCallStatus" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#ToolChoiceOption": + replacement: "Generated.ChatToolChoice" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ToolDefinitionJson": + replacement: "Generated.ChatFunctionTool" + note: "Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape." +"@effect/ai-openrouter/Generated#ToolResponseMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#TooManyRequestsResponse": + replacement: "Generated.TooManyRequestsResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#TooManyRequestsResponseErrorData": + replacement: "Generated.TooManyRequestsResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#TopProviderInfo": + replacement: "Generated.TopProviderInfo" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UnauthorizedResponse": + replacement: "Generated.UnauthorizedResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UnauthorizedResponseErrorData": + replacement: "Generated.UnauthorizedResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UnprocessableEntityResponse": + replacement: "Generated.UnprocessableEntityResponse" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UnprocessableEntityResponseErrorData": + replacement: "Generated.UnprocessableEntityResponseErrorData" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UpdateGuardrail200": + replacement: "Generated.UpdateGuardrail200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UpdateGuardrail200DataResetInterval": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#UpdateGuardrailRequest": + replacement: "Generated.UpdateGuardrailRequest" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UpdateGuardrailRequestResetInterval": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#UpdateKeys200": + replacement: "Generated.UpdateKeys200" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#UpdateKeysRequest": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#UpdateKeysRequestLimitReset": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#URLCitation": + replacement: "Generated.URLCitation" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#URLCitationAnnotationDetail": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#URLCitationType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#UserMessage": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#WebSearchEngine": + replacement: "Generated.WebSearchEngine" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." +"@effect/ai-openrouter/Generated#WebSearchPreviewToolUserLocation": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#WebSearchPreviewToolUserLocationType": + replacement: "none" + note: "Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper." +"@effect/ai-openrouter/Generated#WebSearchStatus": + replacement: "Generated.WebSearchStatus" + note: "Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed." diff --git a/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml new file mode 100644 index 000000000..81023018b --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterClient.yaml @@ -0,0 +1,15 @@ +"@effect/ai-openrouter/OpenRouterClient#ChatStreamingChoice": + replacement: "Generated.ChatStreamChoice" + note: "The client-local streaming choice schema moved into the regenerated OpenRouter schema surface and changed shape." +"@effect/ai-openrouter/OpenRouterClient#ChatStreamingMessageChunk": + replacement: "Generated.ChatStreamDelta" + note: "The client-local streaming message delta moved into the regenerated OpenRouter schema surface and changed shape." +"@effect/ai-openrouter/OpenRouterClient#ChatStreamingMessageToolCall": + replacement: "Generated.ChatStreamToolCall" + note: "The client-local streaming tool-call delta moved into the regenerated OpenRouter schema surface and changed shape." +"@effect/ai-openrouter/OpenRouterClient#ChatStreamingResponseChunk": + replacement: "OpenRouterClient.ChatStreamingResponseChunkData" + note: "The standalone streaming chunk schema was replaced by the decoded data type from Generated.ChatStreamingResponse." +"@effect/ai-openrouter/OpenRouterClient#Service": + replacement: "OpenRouterClient.Service" + note: "Still exported in v4; adapt to the regenerated client, revised request and response schemas, and the new streaming result tuple." diff --git a/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml new file mode 100644 index 000000000..5ec7a2e8a --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterConfig.yaml @@ -0,0 +1,6 @@ +"@effect/ai-openrouter/OpenRouterConfig#OpenRouterConfig": + replacement: "OpenRouterConfig.OpenRouterConfig" + note: "Still exported in v4; update imports to the v4 package and use the revised Context.Service-based configuration service." +"@effect/ai-openrouter/OpenRouterConfig#OpenRouterConfig.Service": + replacement: "OpenRouterConfig.OpenRouterConfig.Service" + note: "Still exported in v4; update imports to the v4 package and use the revised Context.Service-based configuration service." diff --git a/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml new file mode 100644 index 000000000..18fdc4ef1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openrouter__OpenRouterLanguageModel.yaml @@ -0,0 +1,9 @@ +"@effect/ai-openrouter/OpenRouterLanguageModel#Config": + replacement: "OpenRouterLanguageModel.Config" + note: "Still exported in v4; update imports and adapt to the regenerated chat request schema and revised Context.Service configuration." +"@effect/ai-openrouter/OpenRouterLanguageModel#Config.Service": + replacement: "OpenRouterLanguageModel.Config.Service" + note: "Still exported in v4; update imports and adapt to the regenerated chat request schema and revised Context.Service configuration." +"@effect/ai-openrouter/OpenRouterLanguageModel#OpenRouterReasoningInfo": + replacement: "OpenRouterLanguageModel.ReasoningDetails" + note: "The bespoke reasoning-info union was replaced by the provider's raw reasoning-details array, preserved through Prompt options and Response metadata." diff --git a/.context/effect/migration/annotations/effect__ai-openrouter__index.yaml b/.context/effect/migration/annotations/effect__ai-openrouter__index.yaml new file mode 100644 index 000000000..49daa878e --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai-openrouter__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai-openrouter/index": + replacement: "@effect/ai-openrouter" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-openrouter package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__ai.yaml b/.context/effect/migration/annotations/effect__ai.yaml new file mode 100644 index 000000000..2a4a65299 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai.yaml @@ -0,0 +1,3 @@ +"@effect/ai": + replacement: "effect/unstable/ai" + note: "The @effect/ai package was merged into the effect package; import the effect/unstable/ai barrel or import specific modules directly (e.g. effect/unstable/ai/)." diff --git a/.context/effect/migration/annotations/effect__ai__AiError.yaml b/.context/effect/migration/annotations/effect__ai__AiError.yaml new file mode 100644 index 000000000..d906cf83a --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__AiError.yaml @@ -0,0 +1,21 @@ +"@effect/ai/AiError#AiError": + replacement: "AiError.AiError" + note: "Moved to effect/unstable/ai/AiError and redesigned from a union of separately tagged errors into one AiError wrapper with a semantic reason. Construct it with AiError.make({ module, method, reason }) and match error.reason rather than the old top-level tags." +"@effect/ai/AiError#HttpRequestError": + replacement: "AiError.make + AiError.NetworkError" + note: "Replace the old top-level request error with an AiError whose reason is NetworkError. NetworkError.fromRequestError converts a v4 HttpClientError.RequestError." +"@effect/ai/AiError#HttpResponseError": + replacement: "AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidOutputError" + note: "There is no single v4 response-error class. Wrap a semantic reason with AiError.make: use reasonFromHttpStatus for status failures and InvalidOutputError for decode or empty-body failures." +"@effect/ai/AiError#MalformedInput": + replacement: "AiError.make + AiError.InvalidUserInputError" + note: "Replace the old top-level input error with an AiError whose reason is InvalidUserInputError. Use InvalidRequestError when the provider request parameters themselves are malformed." +"@effect/ai/AiError#MalformedOutput": + replacement: "AiError.make + AiError.InvalidOutputError" + note: "Replace the old top-level output error with an AiError whose reason is InvalidOutputError. The old fromParseError helper becomes InvalidOutputError.fromSchemaError." +"@effect/ai/AiError#TypeId": + replacement: "AiError.isAiError" + note: "The AiError brand is private in v4. Use isAiError for runtime narrowing, or isAiErrorReason for a reason, instead of inspecting or constructing the type id." +"@effect/ai/AiError#UnknownError": + replacement: "AiError.make + AiError.UnknownError" + note: "UnknownError is now a semantic reason rather than a top-level error. Put module and method on AiError.make and inspect reason._tag when handling the outer AiError." diff --git a/.context/effect/migration/annotations/effect__ai__EmbeddingModel.yaml b/.context/effect/migration/annotations/effect__ai__EmbeddingModel.yaml new file mode 100644 index 000000000..a4c065c0d --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__EmbeddingModel.yaml @@ -0,0 +1,3 @@ +"@effect/ai/EmbeddingModel#makeDataLoader": + replacement: "EmbeddingModel.make + RequestResolver.setDelay + RequestResolver.batchN" + note: "The dedicated data-loader constructor was removed. EmbeddingModel.make batches concurrent embed requests through its resolver; compose the exposed resolver with setDelay and optional batchN for the old window and maximum-batch behavior." diff --git a/.context/effect/migration/annotations/effect__ai__IdGenerator.yaml b/.context/effect/migration/annotations/effect__ai__IdGenerator.yaml new file mode 100644 index 000000000..8079a2375 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__IdGenerator.yaml @@ -0,0 +1,3 @@ +"@effect/ai/IdGenerator#make": + replacement: "IdGenerator.make" + note: "Moved to effect/unstable/ai/IdGenerator with the same configurable alphabet, prefix, separator, and size behavior. Invalid configuration now fails with Cause.IllegalArgumentError." diff --git a/.context/effect/migration/annotations/effect__ai__LanguageModel.yaml b/.context/effect/migration/annotations/effect__ai__LanguageModel.yaml new file mode 100644 index 000000000..7436f1bf5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__LanguageModel.yaml @@ -0,0 +1,6 @@ +"@effect/ai/LanguageModel#ConstructorParams": + replacement: "none" + note: "V4 inlines this provider-adapter shape in LanguageModel.make. Pass generateText and streamText directly to make, with optional codecTransformer, instead of naming a constructor-parameter type." +"@effect/ai/LanguageModel#ExtractContext": + replacement: "LanguageModel.ExtractServices" + note: "Renamed in effect/unstable/ai/LanguageModel. ExtractServices infers toolkit handler, result-decoding, and effectful-toolkit service requirements." diff --git a/.context/effect/migration/annotations/effect__ai__McpSchema.yaml b/.context/effect/migration/annotations/effect__ai__McpSchema.yaml new file mode 100644 index 000000000..c2557430e --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__McpSchema.yaml @@ -0,0 +1,24 @@ +"@effect/ai/McpSchema#ContentBlock": + replacement: "McpSchema.ContentBlock" + note: "Moved to effect/unstable/ai/McpSchema. It remains the MCP content-block union, but v4 exports it as a const schema rather than a Schema.Union subclass." +"@effect/ai/McpSchema#FailureEncoded": + replacement: "McpSchema.FailureEncoded" + note: "Moved to effect/unstable/ai/McpSchema and still derives an encoded JSON-RPC failure union from an RpcGroup." +"@effect/ai/McpSchema#FromClientEncoded": + replacement: "McpSchema.FromClientEncoded" + note: "Moved to effect/unstable/ai/McpSchema and remains the union of client requests and client notifications." +"@effect/ai/McpSchema#FromServerEncoded": + replacement: "McpSchema.FromServerEncoded" + note: "Moved to effect/unstable/ai/McpSchema and remains the union of server results and server notifications." +"@effect/ai/McpSchema#McpError": + replacement: "McpSchema.McpError" + note: "Moved, but changed from a constructable base class to a union schema of standard tagged protocol errors plus McpErrorBase. Use McpErrorBase to construct a generic MCP error." +"@effect/ai/McpSchema#param": + replacement: "McpSchema.param" + note: "Moved to effect/unstable/ai/McpSchema. V4 wraps the schema and exposes Param.name and Param.schema instead of attaching a public symbol annotation." +"@effect/ai/McpSchema#ParamAnnotation": + replacement: "McpSchema.isParam / Param.name" + note: "The public symbol annotation was removed. Detect parameter wrappers with McpSchema.isParam and read the narrowed Param.name instead of inspecting AST annotations." +"@effect/ai/McpSchema#SuccessEncoded": + replacement: "McpSchema.SuccessEncoded" + note: "Moved to effect/unstable/ai/McpSchema and still derives an encoded JSON-RPC success union from an RpcGroup." diff --git a/.context/effect/migration/annotations/effect__ai__McpServer.yaml b/.context/effect/migration/annotations/effect__ai__McpServer.yaml new file mode 100644 index 000000000..7ded9767a --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__McpServer.yaml @@ -0,0 +1,15 @@ +"@effect/ai/McpServer#layer": + replacement: "McpServer.layer" + note: "Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025_06_18], imported with McpProtocol from effect/unstable/ai; it still runs over a caller-provided RpcServer.Protocol." +"@effect/ai/McpServer#layerHttp": + replacement: "McpServer.layerHttp" + note: "Moved to effect/unstable/ai/McpServer and the unified HttpRouter. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025_06_18], imported with McpProtocol from effect/unstable/ai." +"@effect/ai/McpServer#layerHttpRouter": + replacement: "McpServer.layerHttp" + note: "Renamed and consolidated. V4 layerHttp registers the Streamable HTTP endpoint in the unified HttpRouter; pass a non-empty protocols array of adapters, such as [McpProtocol.v2025_06_18], imported with McpProtocol from effect/unstable/ai." +"@effect/ai/McpServer#layerStdio": + replacement: "McpServer.layerStdio" + note: "Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025_06_18], imported with McpProtocol from effect/unstable/ai." +"@effect/ai/McpServer#run": + replacement: "McpServer.run" + note: "Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025_06_18], imported with McpProtocol from effect/unstable/ai; it remains the Effect-level runner over RpcServer.Protocol." diff --git a/.context/effect/migration/annotations/effect__ai__Model.yaml b/.context/effect/migration/annotations/effect__ai__Model.yaml new file mode 100644 index 000000000..a09b05774 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__Model.yaml @@ -0,0 +1,3 @@ +"@effect/ai/Model#TypeId": + replacement: "none" + note: "The Model brand still exists internally, but its TypeId is not exported and v4 has no public isModel guard. Use Model values created by Model.make rather than inspecting or constructing the brand." diff --git a/.context/effect/migration/annotations/effect__ai__Prompt.yaml b/.context/effect/migration/annotations/effect__ai__Prompt.yaml new file mode 100644 index 000000000..d08583bc6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__Prompt.yaml @@ -0,0 +1,69 @@ +"@effect/ai/Prompt#FilePart": + replacement: "Prompt.FilePart" + note: "Moved to effect/unstable/ai/Prompt with the same file-part model and schema; update the module import." +"@effect/ai/Prompt#FilePartEncoded": + replacement: "Prompt.FilePartEncoded" + note: "Moved to effect/unstable/ai/Prompt with the same encoded file-part shape; update the module import." +"@effect/ai/Prompt#FromJson": + replacement: "Schema.fromJsonString(Prompt.Prompt)" + note: "The module-specific JSON schema was removed. Compose the general v4 JSON-string codec with the public Prompt codec." +"@effect/ai/Prompt#isPart": + replacement: "Prompt.isPart" + note: "Moved to effect/unstable/ai/Prompt and remains the public runtime guard for prompt parts." +"@effect/ai/Prompt#isPrompt": + replacement: "Prompt.isPrompt" + note: "Moved to effect/unstable/ai/Prompt and remains the public runtime guard for Prompt values." +"@effect/ai/Prompt#makePart": + replacement: "Prompt.makePart" + note: "Moved to effect/unstable/ai/Prompt. The generic constructor also supports the new tool-approval request and response part variants." +"@effect/ai/Prompt#merge": + replacement: "Prompt.concat" + note: "Renamed in v4. concat preserves the old dual API and concatenates the messages from a Prompt with additional raw input." +"@effect/ai/Prompt#MessageContentFromString": + replacement: "Prompt.ContentFromString" + note: "Renamed in effect/unstable/ai/Prompt. It still decodes a string to a non-empty array containing one TextPart and encodes the first part's text." +"@effect/ai/Prompt#MessageTypeId": + replacement: "Prompt.isMessage" + note: "The message type id is private in v4. Use the public isMessage guard for runtime refinement instead of importing or inspecting the marker." +"@effect/ai/Prompt#Part": + replacement: "Prompt.Part" + note: "Moved to effect/unstable/ai/Prompt. The union now also includes tool-approval request and response parts." +"@effect/ai/Prompt#PartEncoded": + replacement: "Prompt.PartEncoded" + note: "Moved to effect/unstable/ai/Prompt. The encoded union now also includes tool-approval request and response parts." +"@effect/ai/Prompt#PartTypeId": + replacement: "Prompt.isPart" + note: "The part type id is private in v4. Use the public isPart guard for runtime refinement instead of importing or inspecting the marker." +"@effect/ai/Prompt#PromptFromSelf": + replacement: "Prompt.Prompt" + note: "The standalone declared from-self schema was removed. Use the public Prompt codec for prompt validation and encoding, or Prompt.isPrompt when only runtime refinement is needed." +"@effect/ai/Prompt#ReasoningPart": + replacement: "Prompt.ReasoningPart" + note: "Moved to effect/unstable/ai/Prompt with the same reasoning-part model and schema; update the module import." +"@effect/ai/Prompt#ReasoningPartEncoded": + replacement: "Prompt.ReasoningPartEncoded" + note: "Moved to effect/unstable/ai/Prompt with the same encoded reasoning payload; update the module import." +"@effect/ai/Prompt#TextPart": + replacement: "Prompt.TextPart" + note: "Moved to effect/unstable/ai/Prompt with the same text-part model and schema; update the module import." +"@effect/ai/Prompt#TextPartEncoded": + replacement: "Prompt.TextPartEncoded" + note: "Moved to effect/unstable/ai/Prompt with the same encoded text payload; update the module import." +"@effect/ai/Prompt#toolCallPart": + replacement: "Prompt.toolCallPart" + note: "Moved to effect/unstable/ai/Prompt and remains the typed convenience constructor over makePart(\"tool-call\", params)." +"@effect/ai/Prompt#ToolCallPart": + replacement: "Prompt.ToolCallPart" + note: "Moved to effect/unstable/ai/Prompt with the same tool-call model and schema; update the module import." +"@effect/ai/Prompt#ToolCallPartEncoded": + replacement: "Prompt.ToolCallPartEncoded" + note: "Moved to effect/unstable/ai/Prompt with the same encoded tool-call shape; update the module import." +"@effect/ai/Prompt#toolResultPart": + replacement: "Prompt.toolResultPart" + note: "Moved to effect/unstable/ai/Prompt. V4 removes providerExecuted from prompt tool-result parts; provider-executed response results are handled when converting Response parts." +"@effect/ai/Prompt#ToolResultPartEncoded": + replacement: "Prompt.ToolResultPartEncoded" + note: "Moved to effect/unstable/ai/Prompt, but providerExecuted was removed from the encoded prompt tool-result shape." +"@effect/ai/Prompt#TypeId": + replacement: "Prompt.isPrompt" + note: "The Prompt type id is private in v4 and its internal literal changed. Use the public isPrompt guard instead of importing or inspecting the marker." diff --git a/.context/effect/migration/annotations/effect__ai__Response.yaml b/.context/effect/migration/annotations/effect__ai__Response.yaml new file mode 100644 index 000000000..2a1351c12 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__Response.yaml @@ -0,0 +1,69 @@ +"@effect/ai/Response#documentSourcePart": + replacement: "Response.makePart(\"source\", { ...params, sourceType: \"document\" })" + note: "The lowercase convenience constructor was removed. The DocumentSourcePart model remains, and the generic constructor now requires the document source discriminator." +"@effect/ai/Response#errorPart": + replacement: "Response.makePart(\"error\", params)" + note: "The lowercase convenience constructor was removed; construct the retained error part through Response.makePart." +"@effect/ai/Response#FilePartEncoded": + replacement: "Response.FilePartEncoded" + note: "Moved to effect/unstable/ai/Response; file data remains a base64 string in the encoded representation." +"@effect/ai/Response#finishPart": + replacement: "Response.makePart(\"finish\", params)" + note: "The lowercase convenience constructor was removed. V4 Usage has nested inputTokens and outputTokens objects, and FinishPart adds optional HTTP response details." +"@effect/ai/Response#Part": + replacement: "Response.Part" + note: "Moved to effect/unstable/ai/Response. The non-streaming union now also includes ToolApprovalRequestPart." +"@effect/ai/Response#PartTypeId": + replacement: "Response.isPart" + note: "The public PartTypeId was removed and the marker is internal in v4. Use Response.isPart for runtime refinement." +"@effect/ai/Response#reasoningDeltaPart": + replacement: "Response.makePart(\"reasoning-delta\", params)" + note: "The lowercase convenience constructor was removed; construct the retained ReasoningDeltaPart through Response.makePart." +"@effect/ai/Response#reasoningEndPart": + replacement: "Response.makePart(\"reasoning-end\", params)" + note: "The lowercase convenience constructor was removed; construct the retained ReasoningEndPart through Response.makePart." +"@effect/ai/Response#ReasoningPartEncoded": + replacement: "Response.ReasoningPartEncoded" + note: "Moved to effect/unstable/ai/Response; the encoded reasoning payload remains text: string." +"@effect/ai/Response#reasoningStartPart": + replacement: "Response.makePart(\"reasoning-start\", params)" + note: "The lowercase convenience constructor was removed; construct the retained ReasoningStartPart through Response.makePart." +"@effect/ai/Response#responseMetadataPart": + replacement: "Response.makePart(\"response-metadata\", params)" + note: "The lowercase convenience constructor was removed. V4 id, modelId, and timestamp are optional raw values rather than Option values, and optional HTTP request details were added." +"@effect/ai/Response#textDeltaPart": + replacement: "Response.makePart(\"text-delta\", params)" + note: "The lowercase convenience constructor was removed; construct the retained TextDeltaPart through Response.makePart." +"@effect/ai/Response#textEndPart": + replacement: "Response.makePart(\"text-end\", params)" + note: "The lowercase convenience constructor was removed; construct the retained TextEndPart through Response.makePart." +"@effect/ai/Response#TextPartEncoded": + replacement: "Response.TextPartEncoded" + note: "Moved to effect/unstable/ai/Response; the encoded text payload remains text: string." +"@effect/ai/Response#textStartPart": + replacement: "Response.makePart(\"text-start\", params)" + note: "The lowercase convenience constructor was removed; construct the retained TextStartPart through Response.makePart." +"@effect/ai/Response#toolCallPart": + replacement: "Response.toolCallPart" + note: "Moved to effect/unstable/ai/Response. The constructor remains, but providerName was removed from tool-call parts." +"@effect/ai/Response#ToolCallPartEncoded": + replacement: "Response.ToolCallPartEncoded" + note: "Moved to effect/unstable/ai/Response; providerName was removed while providerExecuted remains optional when encoded." +"@effect/ai/Response#toolParamsDeltaPart": + replacement: "Response.makePart(\"tool-params-delta\", params)" + note: "The lowercase convenience constructor was removed; construct the retained ToolParamsDeltaPart through Response.makePart." +"@effect/ai/Response#toolParamsEndPart": + replacement: "Response.makePart(\"tool-params-end\", params)" + note: "The lowercase convenience constructor was removed; construct the retained ToolParamsEndPart through Response.makePart." +"@effect/ai/Response#toolParamsStartPart": + replacement: "Response.makePart(\"tool-params-start\", params)" + note: "The lowercase convenience constructor was removed; providerName was also removed from ToolParamsStartPart in v4." +"@effect/ai/Response#toolResultPart": + replacement: "Response.toolResultPart" + note: "Moved to effect/unstable/ai/Response; providerName was removed and decoded tool results now require preliminary, normally false." +"@effect/ai/Response#ToolResultPartEncoded": + replacement: "Response.ToolResultPartEncoded" + note: "Moved to effect/unstable/ai/Response; providerName was removed and optional preliminary was added to the encoded shape." +"@effect/ai/Response#urlSourcePart": + replacement: "Response.makePart(\"source\", { ...params, sourceType: \"url\" })" + note: "The lowercase convenience constructor was removed. The UrlSourcePart model remains, and the generic constructor now requires the URL source discriminator." diff --git a/.context/effect/migration/annotations/effect__ai__Tool.yaml b/.context/effect/migration/annotations/effect__ai__Tool.yaml new file mode 100644 index 000000000..5bd660841 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__Tool.yaml @@ -0,0 +1,57 @@ +"@effect/ai/Tool#AnyParametersSchema": + replacement: "Schema.Constraint" + note: "The AI-specific alias was removed. V4 Tool parameter schemas use the general Schema.Constraint type and are no longer restricted to the old struct-or-EmptyParams union." +"@effect/ai/Tool#AnyTaggedRequestSchema": + replacement: "none" + note: "The TaggedRequest-specific Tool adapter contract was removed. Model the operation directly with Tool.make and ordinary v4 Schema.Constraint values." +"@effect/ai/Tool#Destructive": + replacement: "Tool.Destructive" + note: "Moved to effect/unstable/ai/Tool. It is now a Context.Reference value rather than a Reference subclass; its default remains true." +"@effect/ai/Tool#Failure": + replacement: "Tool.Failure" + note: "Moved to effect/unstable/ai/Tool and remains the utility type that extracts a tool's decoded failure type." +"@effect/ai/Tool#fromTaggedRequest": + replacement: "Tool.make" + note: "The adapter was removed. Rebuild the tool explicitly with Tool.make(name, { parameters, success, failure }); Toolkit.make no longer converts schema values automatically." +"@effect/ai/Tool#FromTaggedRequest": + replacement: "Tool.Tool" + note: "The dedicated derived alias was removed. Construct with Tool.make and let Tool.Tool infer the name, parameter, success, and failure schemas." +"@effect/ai/Tool#getDescriptionFromSchemaAst": + replacement: "SchemaAST.resolveDescription" + note: "Moved out of Tool to the general v4 AST annotation resolver. For a Tool value, prefer Tool.getDescription." +"@effect/ai/Tool#getJsonSchemaFromSchemaAst": + replacement: "Tool.getJsonSchemaFromSchema" + note: "Renamed to accept a Schema.Constraint instead of a raw AST and now emits the v4 JSON Schema model. Wrap a raw AST with Schema.make first." +"@effect/ai/Tool#Idempotent": + replacement: "Tool.Idempotent" + note: "Moved to effect/unstable/ai/Tool. It is now a Context.Reference value rather than a Reference subclass; its default remains false." +"@effect/ai/Tool#OpenWorld": + replacement: "Tool.OpenWorld" + note: "Moved to effect/unstable/ai/Tool. It is now a Context.Reference value rather than a Reference subclass; its default remains true." +"@effect/ai/Tool#ProviderDefinedTypeId": + replacement: "Tool.ProviderDefinedTypeId" + note: "Moved to effect/unstable/ai/Tool and remains public. Its literal changed, so use the export rather than retaining the old hard-coded string." +"@effect/ai/Tool#Readonly": + replacement: "Tool.Readonly" + note: "Moved to effect/unstable/ai/Tool. It is now a Context.Reference value rather than a Reference subclass; its default remains false." +"@effect/ai/Tool#Requirements": + replacement: "Tool.HandlerServices" + note: "Renamed and refined. HandlerServices combines parameter-decoding, result-encoding, and request-level dependencies required by a tool handler." +"@effect/ai/Tool#Success": + replacement: "Tool.Success" + note: "Moved to effect/unstable/ai/Tool and remains the utility type that extracts a tool's decoded success type." +"@effect/ai/Tool#Title": + replacement: "Tool.Title" + note: "Moved to effect/unstable/ai/Tool. It is now a Context.Service annotation key; continue attaching the string title with tool.annotate(Tool.Title, value)." +"@effect/ai/Tool#Tool.ProviderDefinedProto": + replacement: "Tool.ProviderDefined" + note: "This implementation-brand interface is no longer public. Use Tool.ProviderDefined for the model type and Tool.isProviderDefined for runtime narrowing." +"@effect/ai/Tool#Tool.Variance": + replacement: "Tool.Tool / Tool.Any" + note: "This implementation variance interface is no longer public; its requirement marker is inline in Tool.Tool. Constrain generic code with Tool.Tool or Tool.Any." +"@effect/ai/Tool#Tool.VarianceStruct": + replacement: "Tool.Tool / Tool.Any" + note: "This implementation variance payload is no longer public; the requirements marker is inline in Tool.Tool and should not be named independently." +"@effect/ai/Tool#TypeId": + replacement: "Tool.TypeId" + note: "Moved to effect/unstable/ai/Tool and remains public. Its literal changed, so use the export rather than retaining the old hard-coded string." diff --git a/.context/effect/migration/annotations/effect__ai__Toolkit.yaml b/.context/effect/migration/annotations/effect__ai__Toolkit.yaml new file mode 100644 index 000000000..7a07a0369 --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__Toolkit.yaml @@ -0,0 +1,9 @@ +"@effect/ai/Toolkit#HandlersFrom": + replacement: "Toolkit.HandlersFrom" + note: "Moved to effect/unstable/ai/Toolkit. V4 handlers receive a HandlerContext argument and may fail with the declared failure, AiError, or AiErrorReason while requiring Tool.HandlerServices." +"@effect/ai/Toolkit#make": + replacement: "Toolkit.make" + note: "Moved to effect/unstable/ai/Toolkit. It now accepts Tool.Any values only and no longer converts TaggedRequest schemas; create each tool explicitly with Tool.make first." +"@effect/ai/Toolkit#TypeId": + replacement: "Toolkit.Toolkit / Toolkit.Any" + note: "The toolkit nominal id is private in v4. Use Toolkit.Toolkit or Toolkit.Any for typing instead of importing or inspecting the marker." diff --git a/.context/effect/migration/annotations/effect__ai__index.yaml b/.context/effect/migration/annotations/effect__ai__index.yaml new file mode 100644 index 000000000..c8787f31a --- /dev/null +++ b/.context/effect/migration/annotations/effect__ai__index.yaml @@ -0,0 +1,3 @@ +"@effect/ai/index": + replacement: "effect/unstable/ai" + note: "The package barrel was removed; import the same namespaces from the effect/unstable/ai barrel or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__cli.yaml b/.context/effect/migration/annotations/effect__cli.yaml new file mode 100644 index 000000000..a52d95b5a --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli.yaml @@ -0,0 +1,3 @@ +"@effect/cli": + replacement: "effect/unstable/cli" + note: "The @effect/cli package was merged into the effect package; import the effect/unstable/cli barrel or import specific modules directly (e.g. effect/unstable/cli/)." diff --git a/.context/effect/migration/annotations/effect__cli__Args.yaml b/.context/effect/migration/annotations/effect__cli__Args.yaml new file mode 100644 index 000000000..f95db093c --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Args.yaml @@ -0,0 +1,90 @@ +"@effect/cli/Args#all": + replacement: "Command.make(name, config)" + note: "Collect arguments in the config record passed to Command.make; there is no standalone Argument.all." +"@effect/cli/Args#All.ArgsAny": + replacement: "Param.AnyArgument" + note: "Use the shared any-positional-parameter type." +"@effect/cli/Args#All.Return": + replacement: "Command.Command.Config.Infer" + note: "Infer the output of a command config record; standalone argument collections were removed." +"@effect/cli/Args#Args": + replacement: "Argument.Argument" + note: "Args was renamed to Argument in effect/unstable/cli." +"@effect/cli/Args#Args.BaseArgsConfig": + replacement: "name: string" + note: "Argument constructors now take the name as a required first parameter." +"@effect/cli/Args#Args.FormatArgsConfig": + replacement: "Primitive.FileParseOptions" + note: "Pass the name separately and use the format option with Argument.fileParse or Argument.fileSchema." +"@effect/cli/Args#Args.PathArgsConfig": + replacement: "Argument.path(name, { pathType, mustExist })" + note: "Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement." +"@effect/cli/Args#Args.Variance": + replacement: "Argument.Argument" + note: "The separate variance artifact was removed; Argument inherits the shared Param variance." +"@effect/cli/Args#ArgsTypeId": + replacement: "Param.isParam(value) && value.kind === Param.argumentKind" + note: "The public Args type id was removed; use the Param guard and argument kind discriminator." +"@effect/cli/Args#atLeast": + replacement: "Argument.atLeast" + note: "Use the moved combinator; v4 returns ReadonlyArray and does not encode non-emptiness in the type." +"@effect/cli/Args#atMost": + replacement: "Argument.atMost" + note: "Use the moved combinator." +"@effect/cli/Args#between": + replacement: "Argument.between" + note: "Use the moved combinator; v4 validates bounds when constructing the parameter." +"@effect/cli/Args#boolean": + replacement: "Flag.boolean / Argument.choiceWithValue" + note: "Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices." +"@effect/cli/Args#fileContent": + replacement: "Argument.file + Argument.mapEffect" + note: "Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains." +"@effect/cli/Args#getHelp": + replacement: "none" + note: "Per-argument help introspection was removed; Command generates help internally." +"@effect/cli/Args#getIdentifier": + replacement: "none" + note: "Public argument identifier introspection was removed." +"@effect/cli/Args#getMaxSize": + replacement: "none" + note: "Public arity introspection was removed; command parsing enforces variadic bounds internally." +"@effect/cli/Args#getMinSize": + replacement: "none" + note: "Public arity introspection was removed; command parsing enforces variadic bounds internally." +"@effect/cli/Args#getUsage": + replacement: "none" + note: "The public Usage tree was removed; Command generates a usage string internally." +"@effect/cli/Args#isArgs": + replacement: "Param.isParam(value) && value.kind === Param.argumentKind" + note: "Arguments now use the shared Param representation and an explicit kind discriminator." +"@effect/cli/Args#map": + replacement: "Argument.map" + note: "Use the moved combinator." +"@effect/cli/Args#optional": + replacement: "Argument.optional" + note: "Use the moved combinator; it still returns Option." +"@effect/cli/Args#repeated": + replacement: "Argument.variadic" + note: "Renamed to variadic; pass optional min and max bounds." +"@effect/cli/Args#secret": + replacement: "Argument.redacted" + note: "Use Redacted-backed positional input." +"@effect/cli/Args#text": + replacement: "Argument.string" + note: "Renamed to string; pass the argument name explicitly." +"@effect/cli/Args#validate": + replacement: "argument.parse({ flags: {}, arguments: args })" + note: "Parsing is now a Param method and returns leftover tokens with the value; errors are CliError." +"@effect/cli/Args#withDefault": + replacement: "Argument.withDefault" + note: "Use the moved combinator; v4 also accepts an Effect fallback." +"@effect/cli/Args#withDescription": + replacement: "Argument.withDescription" + note: "Use the moved combinator." +"@effect/cli/Args#withFallbackConfig": + replacement: "Argument.withFallbackConfig" + note: "Use the moved combinator; invalid configuration becomes CliError.InvalidValue." +"@effect/cli/Args#withSchema": + replacement: "Argument.withSchema" + note: "Use the moved combinator with a v4 Schema constraint decoder." diff --git a/.context/effect/migration/annotations/effect__cli__AutoCorrect.yaml b/.context/effect/migration/annotations/effect__cli__AutoCorrect.yaml new file mode 100644 index 000000000..7bdf4dd13 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__AutoCorrect.yaml @@ -0,0 +1,3 @@ +"@effect/cli/AutoCorrect": + replacement: none + note: V4 suggestion distance is internal and fixed; the public configurable distance helper was removed. diff --git a/.context/effect/migration/annotations/effect__cli__BuiltInOptions.yaml b/.context/effect/migration/annotations/effect__cli__BuiltInOptions.yaml new file mode 100644 index 000000000..66c0860dd --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__BuiltInOptions.yaml @@ -0,0 +1,48 @@ +"@effect/cli/BuiltInOptions#builtInOptions": + replacement: "GlobalFlag.BuiltIns" + note: "Built-ins are global flag definitions consumed automatically by Command.run and Command.runWith." +"@effect/cli/BuiltInOptions#BuiltInOptions": + replacement: "GlobalFlag.BuiltIn" + note: "The parsed directive union became a union of global Action and Setting definitions." +"@effect/cli/BuiltInOptions#BuiltInOptions.ShellType": + replacement: "Completions.Shell" + note: "The shell union moved to Completions." +"@effect/cli/BuiltInOptions#isShowCompletions": + replacement: "none" + note: "Parsed ShowCompletions directives were removed; the runner processes GlobalFlag.Completions directly." +"@effect/cli/BuiltInOptions#isShowHelp": + replacement: "none" + note: "Parsed ShowHelp directives were removed; the runner processes GlobalFlag.Help directly." +"@effect/cli/BuiltInOptions#isShowVersion": + replacement: "none" + note: "Parsed ShowVersion directives were removed; the runner processes GlobalFlag.Version directly." +"@effect/cli/BuiltInOptions#isShowWizard": + replacement: "none" + note: "Parsed ShowWizard directives were removed; the runner processes GlobalFlag.Wizard directly." +"@effect/cli/BuiltInOptions#SetLogLevel": + replacement: "GlobalFlag.LogLevel" + note: "Log level is now a global Setting whose parsed value is provided through context." +"@effect/cli/BuiltInOptions#showCompletions": + replacement: "GlobalFlag.Completions" + note: "Use the built-in completion action; the shell is parsed from --completions." +"@effect/cli/BuiltInOptions#ShowCompletions": + replacement: "GlobalFlag.Completions" + note: "The directive payload was replaced by a global completion action definition." +"@effect/cli/BuiltInOptions#showHelp": + replacement: "GlobalFlag.Help" + note: "Use the built-in help action; usage and help are derived from the active Command." +"@effect/cli/BuiltInOptions#ShowHelp": + replacement: "GlobalFlag.Help" + note: "The directive payload was replaced by a global help action definition." +"@effect/cli/BuiltInOptions#showVersion": + replacement: "GlobalFlag.Version" + note: "Use the built-in version action; supply the version to Command.run or Command.runWith." +"@effect/cli/BuiltInOptions#ShowVersion": + replacement: "GlobalFlag.Version" + note: "The directive value was replaced by a global version action definition." +"@effect/cli/BuiltInOptions#showWizard": + replacement: "GlobalFlag.Wizard" + note: "Use the built-in wizard action; runner context supplies the active Command." +"@effect/cli/BuiltInOptions#ShowWizard": + replacement: "GlobalFlag.Wizard" + note: "The directive payload was replaced by a global wizard action definition." diff --git a/.context/effect/migration/annotations/effect__cli__CliApp.yaml b/.context/effect/migration/annotations/effect__cli__CliApp.yaml new file mode 100644 index 000000000..50985d4b8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__CliApp.yaml @@ -0,0 +1,12 @@ +"@effect/cli/CliApp#CliApp": + replacement: "Command.Command" + note: "The separate application wrapper was folded into the runnable v4 Command tree." +"@effect/cli/CliApp#CliApp.ConstructorArgs": + replacement: "none" + note: "Build the command with Command.make and withDescription, then pass version to Command.run; the old app constructor shape was removed." +"@effect/cli/CliApp#make": + replacement: "Command.make" + note: "Build the executable Command directly; there is no separate CliApp wrapper." +"@effect/cli/CliApp#run": + replacement: "Command.run" + note: "The CliApp wrapper was removed. Attach the execute function with Command.withHandler, then run the Command with its version; v4 reads arguments through the CLI environment instead of accepting args and execute at this call." diff --git a/.context/effect/migration/annotations/effect__cli__CliConfig.yaml b/.context/effect/migration/annotations/effect__cli__CliConfig.yaml new file mode 100644 index 000000000..ad5e73f6d --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__CliConfig.yaml @@ -0,0 +1,18 @@ +"@effect/cli/CliConfig#CliConfig": + replacement: "CliConfig.CliConfig.Service" + note: "The service was redesigned to configure built-in global flags; old parser and help switches were removed." +"@effect/cli/CliConfig#defaultConfig": + replacement: "CliConfig.defaults" + note: "Renamed to defaults with the redesigned service shape." +"@effect/cli/CliConfig#defaultLayer": + replacement: "CliConfig.layer" + note: "Call CliConfig.layer() to provide the defaults." +"@effect/cli/CliConfig#make": + replacement: "CliConfig.make" + note: "The constructor remains but accepts the redesigned service options." +"@effect/cli/CliConfig#normalizeCase": + replacement: "none" + note: "Case normalization is no longer configurable through CliConfig." +"@effect/cli/CliConfig#layer": + replacement: "CliConfig.layer" + note: "The layer constructor remains, but its options configure the redesigned CliConfig.Service for built-in global flags." diff --git a/.context/effect/migration/annotations/effect__cli__Command.yaml b/.context/effect/migration/annotations/effect__cli__Command.yaml new file mode 100644 index 000000000..295f9e1ac --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Command.yaml @@ -0,0 +1,60 @@ +"@effect/cli/Command#Command.Context": + replacement: "Command.CommandContext" + note: "Renamed to CommandContext." +"@effect/cli/Command#Command.ParseConfig": + replacement: "Command.Command.Config.Infer" + note: "Use the v4 command-config inference helper." +"@effect/cli/Command#Command.ParseConfigValue": + replacement: "Command.Command.Config.InferValue" + note: "Use the v4 command-config value inference helper." +"@effect/cli/Command#Command.ParsedConfig": + replacement: "none" + note: "The parsed config representation is internal in v4." +"@effect/cli/Command#Command.ParsedConfigNode": + replacement: "none" + note: "The parsed config node representation is internal in v4." +"@effect/cli/Command#Command.ParsedConfigTree": + replacement: "none" + note: "The parsed config tree representation is internal in v4." +"@effect/cli/Command#Command.Transform": + replacement: "none" + note: "The handler transformation type and machinery are internal in v4." +"@effect/cli/Command#fromDescriptor": + replacement: "Command.make" + note: "The descriptor layer was folded into Command; define config and handler directly on Command.make." +"@effect/cli/Command#getBashCompletions": + replacement: "Completions.generate" + note: "Generation now returns one script string; normally use GlobalFlag.Completions through the runner." +"@effect/cli/Command#getFishCompletions": + replacement: "Completions.generate" + note: "Generation now returns one script string; normally use GlobalFlag.Completions through the runner." +"@effect/cli/Command#getHelp": + replacement: "none" + note: "Help generation for a command path is internal; use GlobalFlag.Help through Command.run or runWith." +"@effect/cli/Command#getNames": + replacement: "Command.Command.name / Command.Command.alias" + note: "Read the public name and optional alias fields; no HashSet accessor remains." +"@effect/cli/Command#getSubcommands": + replacement: "Command.Command.subcommands" + note: "Read the public grouped subcommands field; its shape is no longer a name map." +"@effect/cli/Command#getUsage": + replacement: "none" + note: "Usage is generated internally as part of structured HelpDoc." +"@effect/cli/Command#getZshCompletions": + replacement: "Completions.generate" + note: "Generation now returns one script string; normally use GlobalFlag.Completions through the runner." +"@effect/cli/Command#make": + replacement: "Command.make" + note: "Use the redesigned constructor with one nested config object of Argument and Flag values." +"@effect/cli/Command#run": + replacement: "Command.runWith" + note: "Use runWith for the v3-style function that accepts an argv array; use run to read arguments from Stdio." +"@effect/cli/Command#transformHandler": + replacement: "none" + note: "Transform in the handler or use the specific provide combinators; the generic handler transform is internal." +"@effect/cli/Command#TypeId": + replacement: "Command.isCommand" + note: "The type id is internal in v4; use the public runtime guard." +"@effect/cli/Command#withDescription": + replacement: "Command.withDescription" + note: "Use the retained combinator; v4 descriptions are strings rather than the old HelpDoc ADT." diff --git a/.context/effect/migration/annotations/effect__cli__CommandDescriptor.yaml b/.context/effect/migration/annotations/effect__cli__CommandDescriptor.yaml new file mode 100644 index 000000000..6875422f4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__CommandDescriptor.yaml @@ -0,0 +1,57 @@ +"@effect/cli/CommandDescriptor#Command.ComputeParsedType": + replacement: "Types.Simplify" + note: "Use the general simplification utility, or Command.Command.Config.Infer for command config." +"@effect/cli/CommandDescriptor#Command.GetParsedType": + replacement: "none" + note: "No public parsed-input extractor remains; v4 handlers receive inferred config directly." +"@effect/cli/CommandDescriptor#Command.ParsedStandardCommand": + replacement: "none" + note: "The name/options/args parsed wrapper was removed; handlers receive inferred config directly." +"@effect/cli/CommandDescriptor#Command.ParsedUserInputCommand": + replacement: "none" + note: "The descriptor-level prompt command was removed; use Prompt APIs or Command.wizard." +"@effect/cli/CommandDescriptor#Command.Subcommands": + replacement: "none" + note: "Compose independently handled commands with Command.withSubcommands instead of parsing a tuple union." +"@effect/cli/CommandDescriptor#Command.Variance": + replacement: "Command.Command.Variance" + note: "The command variance helper remains conceptually, now tracking input, error, and requirements." +"@effect/cli/CommandDescriptor#getBashCompletions": + replacement: "Completions.generate" + note: "Completion generation moved to one shell-parameterized function; command conversion is internal." +"@effect/cli/CommandDescriptor#getFishCompletions": + replacement: "Completions.generate" + note: "Completion generation moved to one shell-parameterized function; command conversion is internal." +"@effect/cli/CommandDescriptor#getHelp": + replacement: "none" + note: "Help generation is internal to the Command runner." +"@effect/cli/CommandDescriptor#getNames": + replacement: "Command.Command.name / Command.Command.alias" + note: "Read the public fields; no HashSet accessor remains." +"@effect/cli/CommandDescriptor#getSubcommands": + replacement: "Command.Command.subcommands" + note: "Read the public grouped subcommands field." +"@effect/cli/CommandDescriptor#getUsage": + replacement: "none" + note: "Usage generation is internal to Command help generation." +"@effect/cli/CommandDescriptor#getZshCompletions": + replacement: "Completions.generate" + note: "Completion generation moved to one shell-parameterized function; command conversion is internal." +"@effect/cli/CommandDescriptor#make": + replacement: "Command.make" + note: "The descriptor and executable command layers were merged into one constructor." +"@effect/cli/CommandDescriptor#map": + replacement: "none" + note: "Map individual Argument or Flag values, or transform inside the command handler." +"@effect/cli/CommandDescriptor#mapEffect": + replacement: "none" + note: "Use parameter mapEffect where the transformation belongs to an input, or perform the Effect in the handler." +"@effect/cli/CommandDescriptor#parse": + replacement: "Command.runWith" + note: "Parsing was folded into execution and no intermediate CommandDirective is returned." +"@effect/cli/CommandDescriptor#TypeId": + replacement: "Command.isCommand" + note: "The type id is internal in v4; use the public runtime guard." +"@effect/cli/CommandDescriptor#withDescription": + replacement: "Command.withDescription" + note: "Use the retained behavior; v4 descriptions are strings." diff --git a/.context/effect/migration/annotations/effect__cli__CommandDirective.yaml b/.context/effect/migration/annotations/effect__cli__CommandDirective.yaml new file mode 100644 index 000000000..3c157a9c8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__CommandDirective.yaml @@ -0,0 +1,21 @@ +"@effect/cli/CommandDirective#builtIn": + replacement: "GlobalFlag.action" + note: "Define a custom action flag; v4 runners no longer return built-in directives." +"@effect/cli/CommandDirective#BuiltIn": + replacement: "GlobalFlag.Action" + note: "Use the global action definition type; it is processed directly by the runner." +"@effect/cli/CommandDirective#CommandDirective": + replacement: "none" + note: "The intermediate parse-result model was removed; the runner invokes the selected handler directly." +"@effect/cli/CommandDirective#isBuiltIn": + replacement: "none" + note: "Intermediate built-in directives were removed." +"@effect/cli/CommandDirective#map": + replacement: "none" + note: "Map parameters or transform in the handler; there is no intermediate directive to map." +"@effect/cli/CommandDirective#userDefined": + replacement: "none" + note: "Parsed input is delivered directly to the selected command handler." +"@effect/cli/CommandDirective#UserDefined": + replacement: "none" + note: "The user-defined intermediate directive was removed." diff --git a/.context/effect/migration/annotations/effect__cli__ConfigFile.yaml b/.context/effect/migration/annotations/effect__cli__ConfigFile.yaml new file mode 100644 index 000000000..707bf62b8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__ConfigFile.yaml @@ -0,0 +1,12 @@ +"@effect/cli/ConfigFile#ConfigErrorTypeId": + replacement: "none" + note: "ConfigProvider.SourceError has no public type-id export." +"@effect/cli/ConfigFile#ConfigFileError": + replacement: "ConfigProvider.SourceError" + note: "Use the general source error when implementing a custom file-backed provider." +"@effect/cli/ConfigFile#layer": + replacement: "ConfigProvider.layerAdd(customProviderEffect)" + note: "Build the provider explicitly and add it as fallback to preserve the v3 composition order." +"@effect/cli/ConfigFile#makeProvider": + replacement: "none" + note: "V4 has no API that discovers, parses, and composes config files; use FileSystem, a format parser, and ConfigProvider.fromUnknown explicitly." diff --git a/.context/effect/migration/annotations/effect__cli__HelpDoc.yaml b/.context/effect/migration/annotations/effect__cli__HelpDoc.yaml new file mode 100644 index 000000000..950aca332 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__HelpDoc.yaml @@ -0,0 +1,78 @@ +"@effect/cli/HelpDoc#blocks": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#descriptionList": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#DescriptionList": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#empty": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#Empty": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#enumeration": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#Enumeration": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#getSpan": + replacement: "none" + note: "The Span ADT and document-to-span conversion were removed; v4 help fields are strings." +"@effect/cli/HelpDoc#h1": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#h2": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#h3": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#Header": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#HelpDoc": + replacement: "HelpDoc.HelpDoc" + note: "The name remains, but v4 is a structured command-help record rather than a tagged document AST." +"@effect/cli/HelpDoc#isDescriptionList": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#isEnumeration": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#isHeader": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#isParagraph": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#isSequence": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#mapDescriptionList": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#orElse": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#p": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#Paragraph": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#Sequence": + replacement: "none" + note: "The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput." +"@effect/cli/HelpDoc#toAnsiDoc": + replacement: "none" + note: "CliOutput owns rendering and exposes formatted text rather than an AnsiDoc." +"@effect/cli/HelpDoc#toAnsiText": + replacement: "CliOutput.defaultFormatter().formatHelpDoc" + note: "Format the structured help record; inside Effect code prefer the CliOutput.Formatter service." +"@effect/cli/HelpDoc#isEmpty": + replacement: "none" + note: "The Empty variant was removed when HelpDoc became a structured record; inspect the relevant flags, args, subcommands, and examples arrays when an application-specific emptiness test is needed." diff --git a/.context/effect/migration/annotations/effect__cli__HelpDoc__Span.yaml b/.context/effect/migration/annotations/effect__cli__HelpDoc__Span.yaml new file mode 100644 index 000000000..3ee5b3125 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__HelpDoc__Span.yaml @@ -0,0 +1,3 @@ +"@effect/cli/HelpDoc/Span": + replacement: none + note: The Span ADT was removed; v4 help fields are strings and terminal styling is owned by CliOutput. diff --git a/.context/effect/migration/annotations/effect__cli__Options.yaml b/.context/effect/migration/annotations/effect__cli__Options.yaml new file mode 100644 index 000000000..9992b4ac8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Options.yaml @@ -0,0 +1,150 @@ +"@effect/cli/Options#all": + replacement: "Command.make(name, config)" + note: "Collect flags in the config record passed to Command.make; there is no standalone Flag.all." +"@effect/cli/Options#All.OptionsAny": + replacement: "Param.AnyFlag" + note: "Use the shared any-flag-parameter type." +"@effect/cli/Options#All.Return": + replacement: "Command.Command.Config.Infer" + note: "Infer the output of a command config record; standalone flag collections were removed." +"@effect/cli/Options#atLeast": + replacement: "Flag.atLeast" + note: "Use the moved combinator; v4 returns ReadonlyArray rather than NonEmptyArray." +"@effect/cli/Options#atMost": + replacement: "Flag.atMost" + note: "Use the moved combinator." +"@effect/cli/Options#between": + replacement: "Flag.between" + note: "Use the moved combinator; v4 validates bounds when constructing the parameter." +"@effect/cli/Options#boolean": + replacement: "Flag.boolean" + note: "Use the moved constructor; --no-name is automatic and aliases are added with Flag.withAlias." +"@effect/cli/Options#choice": + replacement: "Flag.choice" + note: "Use the moved constructor." +"@effect/cli/Options#choiceWithValue": + replacement: "Flag.choiceWithValue" + note: "Use the moved constructor." +"@effect/cli/Options#date": + replacement: "Flag.date" + note: "Use the moved constructor." +"@effect/cli/Options#directory": + replacement: "Flag.directory" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." +"@effect/cli/Options#file": + replacement: "Flag.file" + note: "Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement." +"@effect/cli/Options#fileContent": + replacement: "Flag.file + Flag.mapEffect" + note: "Parse a path and read it with FileSystem.readFile; no binary-content flag constructor remains." +"@effect/cli/Options#fileParse": + replacement: "Flag.fileParse" + note: "Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple." +"@effect/cli/Options#fileSchema": + replacement: "Flag.fileSchema" + note: "Pass the old format as an options field and use a v4 Schema constraint decoder." +"@effect/cli/Options#fileText": + replacement: "Flag.file + Flag.mapEffect" + note: "Flag.fileText returns content only; read after Flag.file when the path/content tuple must be preserved." +"@effect/cli/Options#filterMap": + replacement: "Flag.filterMap" + note: "Use the moved combinator and replace the fixed message with an onNone function." +"@effect/cli/Options#float": + replacement: "Flag.float" + note: "Use the moved constructor." +"@effect/cli/Options#getHelp": + replacement: "none" + note: "Per-flag help introspection was removed; Command generates help internally." +"@effect/cli/Options#getIdentifier": + replacement: "none" + note: "Public flag identifier introspection was removed." +"@effect/cli/Options#getUsage": + replacement: "none" + note: "The public Usage tree was removed; Command generates a usage string internally." +"@effect/cli/Options#integer": + replacement: "Flag.integer" + note: "Use the moved constructor." +"@effect/cli/Options#isBool": + replacement: "none" + note: "No public flag-shape predicate remains; boolean-shape inspection is internal." +"@effect/cli/Options#isOptions": + replacement: "Param.isParam(value) && value.kind === Param.flagKind" + note: "Flags now use the shared Param representation and an explicit kind discriminator." +"@effect/cli/Options#keyValueMap": + replacement: "Flag.keyValuePair" + note: "Renamed and now returns Record rather than HashMap." +"@effect/cli/Options#map": + replacement: "Flag.map" + note: "Use the moved combinator." +"@effect/cli/Options#mapEffect": + replacement: "Flag.mapEffect" + note: "Use the moved combinator; mapping failures are CliError." +"@effect/cli/Options#mapTryCatch": + replacement: "Flag.mapTryCatch" + note: "Use the moved combinator; onError now returns a string rather than HelpDoc." +"@effect/cli/Options#none": + replacement: "omit the config entry" + note: "V4 Flag.none is an always-failing sentinel, not v3's empty successful option set." +"@effect/cli/Options#optional": + replacement: "Flag.optional" + note: "Use the moved combinator; it still returns Option." +"@effect/cli/Options#Options": + replacement: "Flag.Flag" + note: "Options was renamed to Flag in effect/unstable/cli." +"@effect/cli/Options#Options.BooleanOptionsConfig": + replacement: "Flag.boolean + Flag.withAlias + Flag.map" + note: "The config object was removed; aliases and value inversion are combinators, while custom negation names need application logic." +"@effect/cli/Options#Options.PathOptionsConfig": + replacement: "{ readonly mustExist?: boolean }" + note: "Path options are inline; true replaces exists=yes and omission replaces either. exists=no has no exact replacement." +"@effect/cli/Options#Options.Variance": + replacement: "Flag.Flag" + note: "The separate variance artifact was removed; Flag inherits the shared Param variance." +"@effect/cli/Options#OptionsTypeId": + replacement: "Param.isParam(value) && value.kind === Param.flagKind" + note: "The public Options type id was removed; use the Param guard and flag kind discriminator." +"@effect/cli/Options#orElse": + replacement: "Flag.orElse(() => fallback)" + note: "The fallback is now lazy; add explicit exclusivity validation if both flags must be rejected." +"@effect/cli/Options#orElseEither": + replacement: "Flag.orElseResult(() => fallback)" + note: "Either became Result and the fallback is lazy; v4 no longer rejects both flags being present." +"@effect/cli/Options#parse": + replacement: "flag.parse({ flags, arguments: [] })" + note: "Parsing is now a Param method over a Record and returns leftover arguments with the value; errors are CliError." +"@effect/cli/Options#processCommandLine": + replacement: "Command.runWith" + note: "Raw argv processing is now whole-command execution; no public standalone flag tokenizer remains." +"@effect/cli/Options#redacted": + replacement: "Flag.redacted" + note: "Use the moved constructor." +"@effect/cli/Options#repeated": + replacement: "Flag.variadic" + note: "Renamed to variadic; pass optional min and max bounds." +"@effect/cli/Options#secret": + replacement: "Flag.redacted" + note: "The deprecated Secret constructor was removed; use Redacted-backed input." +"@effect/cli/Options#text": + replacement: "Flag.string" + note: "Renamed from text to string." +"@effect/cli/Options#withAlias": + replacement: "Flag.withAlias" + note: "Use the moved combinator." +"@effect/cli/Options#withDefault": + replacement: "Flag.withDefault" + note: "Use the moved combinator; v4 also accepts an Effect fallback." +"@effect/cli/Options#withDescription": + replacement: "Flag.withDescription" + note: "Use the moved combinator." +"@effect/cli/Options#withFallbackConfig": + replacement: "Flag.withFallbackConfig" + note: "Use the moved combinator; invalid configuration becomes CliError.InvalidValue." +"@effect/cli/Options#withFallbackPrompt": + replacement: "Flag.withFallbackPrompt" + note: "Use the moved combinator; v4 can construct the Prompt lazily in Effect." +"@effect/cli/Options#withPseudoName": + replacement: "Flag.withMetavar" + note: "Renamed to withMetavar." +"@effect/cli/Options#withSchema": + replacement: "Flag.withSchema" + note: "Use the moved combinator with a v4 Schema constraint decoder." diff --git a/.context/effect/migration/annotations/effect__cli__Primitive.yaml b/.context/effect/migration/annotations/effect__cli__Primitive.yaml new file mode 100644 index 000000000..35082dfc9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Primitive.yaml @@ -0,0 +1,42 @@ +"@effect/cli/Primitive#boolean": + replacement: "Primitive.boolean" + note: "Boolean is now a singleton value; defaults belong on Flag.boolean or withDefault." +"@effect/cli/Primitive#choice": + replacement: "Primitive.choice" + note: "Use the moved constructor." +"@effect/cli/Primitive#date": + replacement: "Primitive.date" + note: "Date is now a singleton Primitive value." +"@effect/cli/Primitive#float": + replacement: "Primitive.float" + note: "Float is now a singleton Primitive value and rejects non-finite numbers." +"@effect/cli/Primitive#getChoices": + replacement: "none" + note: "Choice introspection is internal in v4; retain alternatives in application code when needed." +"@effect/cli/Primitive#getHelp": + replacement: "none" + note: "Primitive-level help generation was removed from the public API." +"@effect/cli/Primitive#integer": + replacement: "Primitive.integer" + note: "Integer is now a singleton Primitive value." +"@effect/cli/Primitive#isBool": + replacement: "none" + note: "The boolean Primitive predicate is internal in v4." +"@effect/cli/Primitive#Primitive.PathExists": + replacement: "mustExist?: boolean" + note: "Use true for yes and omit for either; no cannot be represented exactly because false permits existing paths." +"@effect/cli/Primitive#Primitive.ValueType": + replacement: "P extends Primitive.Primitive ? A : never" + note: "The named helper was removed; infer the value with a local conditional type." +"@effect/cli/Primitive#Primitive.Variance": + replacement: "Primitive.Primitive.Variance" + note: "The variance interface remains, but its brand key is internal; prefer Primitive in user APIs." +"@effect/cli/Primitive#PrimitiveTypeId": + replacement: "none" + note: "The public Primitive type-id symbol was removed." +"@effect/cli/Primitive#text": + replacement: "Primitive.string" + note: "Renamed from text to string." +"@effect/cli/Primitive#validate": + replacement: "primitive.parse(value)" + note: "Parsing is now the Primitive.parse method over a string; defaults and case normalization moved out of this layer." diff --git a/.context/effect/migration/annotations/effect__cli__Prompt.yaml b/.context/effect/migration/annotations/effect__cli__Prompt.yaml new file mode 100644 index 000000000..8c1f6ffb1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Prompt.yaml @@ -0,0 +1,39 @@ +"@effect/cli/Prompt#All.PromptAny": + replacement: "Prompt.Any" + note: "The any-prompt alias moved out of the All namespace." +"@effect/cli/Prompt#All.Return": + replacement: "Prompt.All.Return" + note: "The collection result helper remains under Prompt.All." +"@effect/cli/Prompt#date": + replacement: "Prompt.date" + note: "Use the moved constructor." +"@effect/cli/Prompt#file": + replacement: "Prompt.file" + note: "Use the moved constructor; v4 also supports a default selected path." +"@effect/cli/Prompt#flatMap": + replacement: "Prompt.flatMap" + note: "Use the moved combinator." +"@effect/cli/Prompt#float": + replacement: "Prompt.float" + note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#integer": + replacement: "Prompt.integer" + note: "Use the moved constructor; v4 also supports a default value." +"@effect/cli/Prompt#map": + replacement: "Prompt.map" + note: "Use the moved combinator." +"@effect/cli/Prompt#Prompt": + replacement: "Prompt.Prompt" + note: "The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError." +"@effect/cli/Prompt#Prompt.Variance": + replacement: "Prompt.Prompt" + note: "The named variance artifact was removed; use Prompt." +"@effect/cli/Prompt#Prompt.VarianceStruct": + replacement: "Prompt.Prompt" + note: "The named variance structure was removed; use Prompt." +"@effect/cli/Prompt#PromptTypeId": + replacement: "Prompt.isPrompt" + note: "The public type-id symbol was removed; use the runtime guard." +"@effect/cli/Prompt#text": + replacement: "Prompt.text" + note: "Use the moved constructor." diff --git a/.context/effect/migration/annotations/effect__cli__Usage.yaml b/.context/effect/migration/annotations/effect__cli__Usage.yaml new file mode 100644 index 000000000..aed15625d --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__Usage.yaml @@ -0,0 +1,3 @@ +"@effect/cli/Usage": + replacement: none + note: The Usage ADT was removed; Command builds a plain HelpDoc.usage string internally. diff --git a/.context/effect/migration/annotations/effect__cli__ValidationError.yaml b/.context/effect/migration/annotations/effect__cli__ValidationError.yaml new file mode 100644 index 000000000..5c9b66be6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__ValidationError.yaml @@ -0,0 +1,111 @@ +"@effect/cli/ValidationError#commandMismatch": + replacement: "none" + note: "V4 runners receive arguments after the root name; unknown child commands use CliError.UnknownSubcommand." +"@effect/cli/ValidationError#CommandMismatch": + replacement: "none" + note: "The root-command mismatch error was removed." +"@effect/cli/ValidationError#correctedFlag": + replacement: "new CliError.UnrecognizedOption({ option, command, suggestions })" + note: "Suggestions are carried by UnrecognizedOption; there is no separate corrected-flag case." +"@effect/cli/ValidationError#CorrectedFlag": + replacement: "CliError.UnrecognizedOption" + note: "Use the unrecognized-option class and its suggestions field." +"@effect/cli/ValidationError#helpRequested": + replacement: "new CliError.ShowHelp({ commandPath, errors: [] })" + note: "Help requests now carry a command path and optional underlying errors." +"@effect/cli/ValidationError#HelpRequested": + replacement: "CliError.ShowHelp" + note: "Renamed and redesigned as ShowHelp." +"@effect/cli/ValidationError#invalidArgument": + replacement: "new CliError.InvalidValue({ option, value, expected, kind: \"argument\" })" + note: "Use InvalidValue for undecodable arguments and UnexpectedArgument for leftover operands." +"@effect/cli/ValidationError#InvalidArgument": + replacement: "CliError.InvalidValue | CliError.UnexpectedArgument" + note: "Argument decoding and leftover operands are separate v4 errors." +"@effect/cli/ValidationError#invalidValue": + replacement: "new CliError.InvalidValue({ option, value, expected, kind })" + note: "Replace the HelpDoc payload with structured option, value, expected, and kind fields." +"@effect/cli/ValidationError#InvalidValue": + replacement: "CliError.InvalidValue" + note: "Use the schema-backed v4 error class." +"@effect/cli/ValidationError#isCommandMismatch": + replacement: "none" + note: "The root-command mismatch error was removed." +"@effect/cli/ValidationError#isCorrectedFlag": + replacement: "error._tag === \"UnrecognizedOption\" && error.suggestions.length > 0" + note: "Check the v4 tag and suggestions array." +"@effect/cli/ValidationError#isHelpRequested": + replacement: "error._tag === \"ShowHelp\"" + note: "Narrow the CliError union by its tag." +"@effect/cli/ValidationError#isInvalidArgument": + replacement: "(error._tag === \"InvalidValue\" && error.kind === \"argument\") || error._tag === \"UnexpectedArgument\"" + note: "Check both v4 argument error forms." +"@effect/cli/ValidationError#isInvalidValue": + replacement: "error._tag === \"InvalidValue\"" + note: "Narrow the CliError union by its tag." +"@effect/cli/ValidationError#isMissingFlag": + replacement: "error._tag === \"MissingOption\"" + note: "MissingFlag was renamed to MissingOption." +"@effect/cli/ValidationError#isMissingSubcommand": + replacement: "none" + note: "Missing subcommands now cause ShowHelp rather than a dedicated error." +"@effect/cli/ValidationError#isMissingValue": + replacement: "error._tag === \"InvalidValue\" && error.value === \"\"" + note: "Missing values are represented as InvalidValue with an empty value." +"@effect/cli/ValidationError#isMultipleValuesDetected": + replacement: "none" + note: "Count violations are summarized as InvalidValue without a stable subtype." +"@effect/cli/ValidationError#isNoBuiltInMatch": + replacement: "none" + note: "Built-ins are GlobalFlag definitions and the intermediate failure was removed." +"@effect/cli/ValidationError#isUnclusteredFlag": + replacement: "none" + note: "Cluster expansion is internal and has no public intermediate error." +"@effect/cli/ValidationError#isValidationError": + replacement: "CliError.isCliError" + note: "Use the renamed union guard." +"@effect/cli/ValidationError#keyValuesDetected": + replacement: "new CliError.InvalidValue({ option, value, expected, kind: \"flag\" })" + note: "Represent count violations with structured InvalidValue fields." +"@effect/cli/ValidationError#missingFlag": + replacement: "new CliError.MissingOption({ option })" + note: "MissingFlag was renamed to MissingOption." +"@effect/cli/ValidationError#MissingFlag": + replacement: "CliError.MissingOption" + note: "Renamed to MissingOption." +"@effect/cli/ValidationError#missingSubcommand": + replacement: "none" + note: "A parent without a selected subcommand now shows help rather than emitting a dedicated error." +"@effect/cli/ValidationError#MissingSubcommand": + replacement: "none" + note: "The dedicated missing-subcommand error was removed." +"@effect/cli/ValidationError#missingValue": + replacement: "new CliError.InvalidValue({ option, value: \"\", expected, kind })" + note: "Missing values are represented as InvalidValue with an empty value." +"@effect/cli/ValidationError#MissingValue": + replacement: "CliError.InvalidValue" + note: "The dedicated tag was folded into InvalidValue." +"@effect/cli/ValidationError#MultipleValuesDetected": + replacement: "CliError.InvalidValue" + note: "Count violations are summarized as InvalidValue without preserving the old values array." +"@effect/cli/ValidationError#noBuiltInMatch": + replacement: "none" + note: "Built-ins are GlobalFlag definitions and the intermediate failure was removed." +"@effect/cli/ValidationError#NoBuiltInMatch": + replacement: "none" + note: "The intermediate built-in matching error was removed." +"@effect/cli/ValidationError#unclusteredFlag": + replacement: "none" + note: "Flag cluster expansion is internal in v4." +"@effect/cli/ValidationError#UnclusteredFlag": + replacement: "none" + note: "The public cluster error was removed." +"@effect/cli/ValidationError#ValidationError": + replacement: "CliError.CliError" + note: "The validation union was redesigned and renamed to CliError." +"@effect/cli/ValidationError#ValidationError.Proto": + replacement: "none" + note: "V4 errors are schema-backed classes and expose no shared public prototype type." +"@effect/cli/ValidationError#ValidationErrorTypeId": + replacement: "none" + note: "The CliError type id is private; use CliError.isCliError." diff --git a/.context/effect/migration/annotations/effect__cli__index.yaml b/.context/effect/migration/annotations/effect__cli__index.yaml new file mode 100644 index 000000000..8a416c619 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cli__index.yaml @@ -0,0 +1,3 @@ +"@effect/cli/index": + replacement: "effect/unstable/cli" + note: "The package barrel was removed; import the same namespaces from the effect/unstable/cli barrel or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__cluster.yaml b/.context/effect/migration/annotations/effect__cluster.yaml new file mode 100644 index 000000000..ecbb12117 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster.yaml @@ -0,0 +1,3 @@ +"@effect/cluster": + replacement: "effect/unstable/cluster" + note: "The @effect/cluster package was merged into the effect package; import the effect/unstable/cluster barrel or import specific modules directly (e.g. effect/unstable/cluster/)." diff --git a/.context/effect/migration/annotations/effect__cluster__ClusterCron.yaml b/.context/effect/migration/annotations/effect__cluster__ClusterCron.yaml new file mode 100644 index 000000000..e32af55c5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ClusterCron.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/ClusterCron#make": + replacement: "effect/unstable/cluster/ClusterCron#make" + note: "Moved into core Effect. The constructor remains; Duration.DurationInput is now Duration.Input." diff --git a/.context/effect/migration/annotations/effect__cluster__ClusterError.yaml b/.context/effect/migration/annotations/effect__cluster__ClusterError.yaml new file mode 100644 index 000000000..9dd6c41b1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ClusterError.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/ClusterError#TypeId": + replacement: "none" + note: "The shared marker is private in v4. Use the exported tagged error classes, their _tag fields, or class-specific is guards." diff --git a/.context/effect/migration/annotations/effect__cluster__ClusterSchema.yaml b/.context/effect/migration/annotations/effect__cluster__ClusterSchema.yaml new file mode 100644 index 000000000..dc3c5c316 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ClusterSchema.yaml @@ -0,0 +1,12 @@ +"@effect/cluster/ClusterSchema#ClientTracingEnabled": + replacement: "effect/unstable/cluster/ClusterSchema#ClientTracingEnabled" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value." +"@effect/cluster/ClusterSchema#Persisted": + replacement: "effect/unstable/cluster/ClusterSchema#Persisted" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same false default." +"@effect/cluster/ClusterSchema#ShardGroup": + replacement: "effect/unstable/cluster/ClusterSchema#ShardGroup" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value." +"@effect/cluster/ClusterSchema#Uninterruptible": + replacement: "effect/unstable/cluster/ClusterSchema#Uninterruptible" + note: "Now a Context.Reference value. Replace its static methods with ClusterSchema.isUninterruptibleForServer and isUninterruptibleForClient." diff --git a/.context/effect/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml b/.context/effect/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml new file mode 100644 index 000000000..d7bf79fc1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ClusterWorkflowEngine.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/ClusterWorkflowEngine#layer": + replacement: "effect/unstable/cluster/ClusterWorkflowEngine#layer" + note: "Moved into core Effect with the same cluster-backed WorkflowEngine layer composition." +"@effect/cluster/ClusterWorkflowEngine#make": + replacement: "effect/unstable/cluster/ClusterWorkflowEngine#make" + note: "Moved into core Effect; the constructor still uses Sharding and MessageStorage." diff --git a/.context/effect/migration/annotations/effect__cluster__DeliverAt.yaml b/.context/effect/migration/annotations/effect__cluster__DeliverAt.yaml new file mode 100644 index 000000000..bba0f7b70 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__DeliverAt.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/DeliverAt#symbol": + replacement: "effect/unstable/cluster/DeliverAt#symbol" + note: "Moved into core Effect; the protocol key is now the string literal ~effect/cluster/DeliverAt rather than a global symbol." diff --git a/.context/effect/migration/annotations/effect__cluster__Entity.yaml b/.context/effect/migration/annotations/effect__cluster__Entity.yaml new file mode 100644 index 000000000..661a7e0f4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Entity.yaml @@ -0,0 +1,15 @@ +"@effect/cluster/Entity#Any": + replacement: "effect/unstable/cluster/Entity#Any" + note: "Moved into core Effect with the same erased entity type." +"@effect/cluster/Entity#makeTestClient": + replacement: "effect/unstable/cluster/Entity#makeTestClient" + note: "Moved into core Effect; adapt its inputs and requirements to the v4 RPC, Layer, Scope, and Context APIs." +"@effect/cluster/Entity#TypeId": + replacement: "none" + note: "The entity marker is private in v4. Use Entity.isEntity for runtime refinement." +"@effect/cluster/Entity#HandlersFrom": + replacement: "effect/unstable/cluster/Entity#HandlersFrom" + note: "Moved into core Effect; handler results now use Rpc.WrapperOr, which accepts either the raw RPC result or its wrapper." +"@effect/cluster/Entity#Replier.Success": + replacement: "effect/unstable/cluster/Entity#Replier.Success" + note: "Moved into core Effect; streaming replies may use Queue.Dequeue with Cause.Done instead of the removed Mailbox type." diff --git a/.context/effect/migration/annotations/effect__cluster__EntityAddress.yaml b/.context/effect/migration/annotations/effect__cluster__EntityAddress.yaml new file mode 100644 index 000000000..0bc1367ae --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__EntityAddress.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/EntityAddress#EntityAddressFromSelf": + replacement: "effect/unstable/cluster/EntityAddress#EntityAddress" + note: "The separate self schema was removed; the v4 Schema.Class is itself the EntityAddress schema." +"@effect/cluster/EntityAddress#make": + replacement: "effect/unstable/cluster/EntityAddress#make" + note: "Moved into core Effect with the same options-object constructor." +"@effect/cluster/EntityAddress#TypeId": + replacement: "none" + note: "The marker is private in v4. Use the exported EntityAddress class and schema." diff --git a/.context/effect/migration/annotations/effect__cluster__EntityId.yaml b/.context/effect/migration/annotations/effect__cluster__EntityId.yaml new file mode 100644 index 000000000..dbc9ad7b0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__EntityId.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/EntityId#make": + replacement: "effect/unstable/cluster/EntityId#make" + note: "Moved into core Effect; the branding helper remains and performs no validation or normalization." diff --git a/.context/effect/migration/annotations/effect__cluster__EntityProxy.yaml b/.context/effect/migration/annotations/effect__cluster__EntityProxy.yaml new file mode 100644 index 000000000..f6e0df345 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__EntityProxy.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/EntityProxy#ConvertHttpApi": + replacement: "effect/unstable/cluster/EntityProxy#ConvertHttpApi" + note: "Moved into core Effect and updated to the v4 HttpApiEndpoint and Schema types." +"@effect/cluster/EntityProxy#ConvertRpcs": + replacement: "effect/unstable/cluster/EntityProxy#ConvertRpcs" + note: "Moved into core Effect and updated to the v4 Rpc and Schema type parameters." diff --git a/.context/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml b/.context/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml new file mode 100644 index 000000000..6edba3715 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__EntityProxyServer.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/EntityProxyServer#layerHttpApi": + replacement: "effect/unstable/cluster/EntityProxyServer#layerHttpApi" + note: "Moved into core Effect. Use v4 HttpApi identifiers and Rpc.ServicesServer requirements." +"@effect/cluster/EntityProxyServer#layerRpcHandlers": + replacement: "effect/unstable/cluster/EntityProxyServer#layerRpcHandlers" + note: "Moved into core Effect; the service requirement is now Rpc.ServicesServer rather than Rpc.Context." +"@effect/cluster/EntityProxyServer#RpcHandlers": + replacement: "effect/unstable/cluster/EntityProxyServer#RpcHandlers" + note: "Moved into core Effect and updated for the additional v4 Rpc requirements type parameter." diff --git a/.context/effect/migration/annotations/effect__cluster__EntityResource.yaml b/.context/effect/migration/annotations/effect__cluster__EntityResource.yaml new file mode 100644 index 000000000..19472a5e2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__EntityResource.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/EntityResource#make": + replacement: "effect/unstable/cluster/EntityResource#make" + note: "Moved into core Effect. Acquisition is lazy by default in v4; set acquireEagerly: true to preserve v3 behavior." +"@effect/cluster/EntityResource#TypeId": + replacement: "effect/unstable/cluster/EntityResource#TypeId" + note: "Moved into core Effect; its literal changed to ~effect/cluster/EntityResource." diff --git a/.context/effect/migration/annotations/effect__cluster__Envelope.yaml b/.context/effect/migration/annotations/effect__cluster__Envelope.yaml new file mode 100644 index 000000000..2d749b6f7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Envelope.yaml @@ -0,0 +1,45 @@ +"@effect/cluster/Envelope#Envelope.Any": + replacement: "effect/unstable/cluster/Envelope#Envelope.Any" + note: "Moved into core Effect with the same erased envelope type." +"@effect/cluster/Envelope#Envelope.Encoded": + replacement: "effect/unstable/cluster/Envelope#Encoded" + note: "The encoded envelope union moved to the module-level Encoded type." +"@effect/cluster/Envelope#Envelope.PartialEncoded": + replacement: "effect/unstable/cluster/Envelope#Partial" + note: "The partially decoded runtime union was renamed to Partial; use PartialJson for its JSON codec." +"@effect/cluster/Envelope#EnvelopeFromSelf": + replacement: "effect/unstable/cluster/Envelope#Envelope" + note: "The self schema was renamed to Envelope and declaration-merges with the envelope type and namespace." +"@effect/cluster/Envelope#PartialEncoded": + replacement: "effect/unstable/cluster/Envelope#PartialJson" + note: "The partially decoded envelope JSON codec was renamed to PartialJson." +"@effect/cluster/Envelope#PartialEncodedArray": + replacement: "effect/unstable/cluster/Envelope#PartialArray" + note: "The mutable array codec was renamed to PartialArray." +"@effect/cluster/Envelope#PartialEncodedFromSelf": + replacement: "effect/unstable/cluster/Envelope#Partial" + note: "The separate self schema was folded into Partial; derive JSON encoding with PartialJson." +"@effect/cluster/Envelope#PartialEncodedRequest": + replacement: "Schema.toCodecJson(Envelope.PartialRequest)" + note: "V4 exports the self schema as PartialRequest and derives its JSON codec with Schema.toCodecJson." +"@effect/cluster/Envelope#PartialEncodedRequestFromSelf": + replacement: "effect/unstable/cluster/Envelope#PartialRequest" + note: "The partially decoded request self schema was renamed to PartialRequest." +"@effect/cluster/Envelope#Request": + replacement: "effect/unstable/cluster/Envelope#Request" + note: "The request interface remains and declaration-merges with the exported Request schema." +"@effect/cluster/Envelope#Request.Any": + replacement: "effect/unstable/cluster/Envelope#Request.Any" + note: "Moved into core Effect with the same erased request type." +"@effect/cluster/Envelope#Request.Encoded": + replacement: "effect/unstable/cluster/Envelope#PartialRequestEncoded" + note: "The JSON request shape moved to the module-level PartialRequestEncoded interface." +"@effect/cluster/Envelope#Request.PartialEncoded": + replacement: "effect/unstable/cluster/Envelope#PartialRequest" + note: "The partially decoded request shape moved to the module-level PartialRequest class and type." +"@effect/cluster/Envelope#RequestFromSelf": + replacement: "effect/unstable/cluster/Envelope#Request" + note: "The request self schema was renamed to Request and declaration-merges with the runtime interface." +"@effect/cluster/Envelope#TypeId": + replacement: "typeof Envelope.TypeId" + note: "The marker value remains, but the type alias was removed and the value is now a string literal; use typeof in type position." diff --git a/.context/effect/migration/annotations/effect__cluster__HttpRunner.yaml b/.context/effect/migration/annotations/effect__cluster__HttpRunner.yaml new file mode 100644 index 000000000..77a734675 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__HttpRunner.yaml @@ -0,0 +1,12 @@ +"@effect/cluster/HttpRunner#layerClient": + replacement: "effect/unstable/cluster/HttpRunner#layerClient" + note: "Moved into core Effect with the same client-side Sharding and Runners layer composition." +"@effect/cluster/HttpRunner#layerHttp": + replacement: "effect/unstable/cluster/HttpRunner#layerHttp" + note: "Moved into core Effect with the same HTTP runner composition." +"@effect/cluster/HttpRunner#toHttpEffect": + replacement: "effect/unstable/cluster/HttpRunner#toHttpEffect" + note: "Moved into core Effect with the same nested HTTP server effect and service requirements." +"@effect/cluster/HttpRunner#toHttpEffectWebsocket": + replacement: "effect/unstable/cluster/HttpRunner#toHttpEffectWebsocket" + note: "Moved into core Effect with the same WebSocket HTTP effect shape and requirements." diff --git a/.context/effect/migration/annotations/effect__cluster__K8sHttpClient.yaml b/.context/effect/migration/annotations/effect__cluster__K8sHttpClient.yaml new file mode 100644 index 000000000..ea31aef07 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__K8sHttpClient.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/K8sHttpClient#layer": + replacement: "effect/unstable/cluster/K8sHttpClient#layer" + note: "Moved into core Effect with the same in-cluster Kubernetes client behavior." diff --git a/.context/effect/migration/annotations/effect__cluster__MachineId.yaml b/.context/effect/migration/annotations/effect__cluster__MachineId.yaml new file mode 100644 index 000000000..95933caa4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__MachineId.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/MachineId#make": + replacement: "effect/unstable/cluster/MachineId#make" + note: "Moved into core Effect. The v4 helper is an unchecked cast; validate external input with the MachineId schema when needed." diff --git a/.context/effect/migration/annotations/effect__cluster__Message.yaml b/.context/effect/migration/annotations/effect__cluster__Message.yaml new file mode 100644 index 000000000..c7611dcb8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Message.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/Message#serialize": + replacement: "effect/unstable/cluster/Message#serialize" + note: "Moved into core Effect. It now returns Envelope.Partial; use serializeEnvelope for the JSON Envelope.Encoded form." diff --git a/.context/effect/migration/annotations/effect__cluster__MessageStorage.yaml b/.context/effect/migration/annotations/effect__cluster__MessageStorage.yaml new file mode 100644 index 000000000..1ebf92ecf --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__MessageStorage.yaml @@ -0,0 +1,12 @@ +"@effect/cluster/MessageStorage#layerMemory": + replacement: "effect/unstable/cluster/MessageStorage#layerMemory" + note: "Moved into core Effect; it still provides MessageStorage and MemoryDriver and requires ShardingConfig." +"@effect/cluster/MessageStorage#layerNoop": + replacement: "effect/unstable/cluster/MessageStorage#layerNoop" + note: "Moved into core Effect with the same dependency-free no-op implementation." +"@effect/cluster/MessageStorage#make": + replacement: "effect/unstable/cluster/MessageStorage#make" + note: "Moved into core Effect. Context service projections now use the Service property instead of Type." +"@effect/cluster/MessageStorage#Encoded": + replacement: "effect/unstable/cluster/MessageStorage#Encoded" + note: "Moved into core Effect; use the v4 Envelope.Encoded and Reply.Encoded aliases in custom encoded storage implementations." diff --git a/.context/effect/migration/annotations/effect__cluster__Reply.yaml b/.context/effect/migration/annotations/effect__cluster__Reply.yaml new file mode 100644 index 000000000..8f0c4e831 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Reply.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/Reply#ReplyEncoded": + replacement: "effect/unstable/cluster/Reply#Encoded" + note: "Renamed to Encoded and no longer parameterized by an Rpc; payload fields are unknown and validated by Reply.Reply(rpc)." +"@effect/cluster/Reply#serialize": + replacement: "effect/unstable/cluster/Reply#serialize" + note: "Moved into core Effect and now returns the non-generic Reply.Encoded wire union." +"@effect/cluster/Reply#TypeId": + replacement: "none" + note: "The reply marker is private in v4. Use Reply.isReply for runtime refinement." diff --git a/.context/effect/migration/annotations/effect__cluster__Runner.yaml b/.context/effect/migration/annotations/effect__cluster__Runner.yaml new file mode 100644 index 000000000..ebc99a826 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Runner.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/Runner#TypeId": + replacement: "none" + note: "The runner marker is private in v4. Use the exported Runner class and schema." diff --git a/.context/effect/migration/annotations/effect__cluster__RunnerAddress.yaml b/.context/effect/migration/annotations/effect__cluster__RunnerAddress.yaml new file mode 100644 index 000000000..33246c9a4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__RunnerAddress.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/RunnerAddress#make": + replacement: "effect/unstable/cluster/RunnerAddress#make" + note: "Moved into core Effect with the same host and port constructor; the host schema is now Schema.String." +"@effect/cluster/RunnerAddress#TypeId": + replacement: "none" + note: "The runner-address marker is private in v4. Use the exported RunnerAddress class and schema." diff --git a/.context/effect/migration/annotations/effect__cluster__RunnerHealth.yaml b/.context/effect/migration/annotations/effect__cluster__RunnerHealth.yaml new file mode 100644 index 000000000..ed573b9b0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__RunnerHealth.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/RunnerHealth#layerNoop": + replacement: "effect/unstable/cluster/RunnerHealth#layerNoop" + note: "Moved into core Effect with the same dependency-free health implementation." diff --git a/.context/effect/migration/annotations/effect__cluster__RunnerServer.yaml b/.context/effect/migration/annotations/effect__cluster__RunnerServer.yaml new file mode 100644 index 000000000..b74c5f1cc --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__RunnerServer.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/RunnerServer#layer": + replacement: "effect/unstable/cluster/RunnerServer#layer" + note: "Moved into core Effect; it still serves runner RPCs over a separately provided RpcServer.Protocol." +"@effect/cluster/RunnerServer#layerClientOnly": + replacement: "effect/unstable/cluster/RunnerServer#layerClientOnly" + note: "Moved into core Effect with the same client-only Sharding and Runners composition." diff --git a/.context/effect/migration/annotations/effect__cluster__RunnerStorage.yaml b/.context/effect/migration/annotations/effect__cluster__RunnerStorage.yaml new file mode 100644 index 000000000..59560ad17 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__RunnerStorage.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/RunnerStorage#layerMemory": + replacement: "effect/unstable/cluster/RunnerStorage#layerMemory" + note: "Moved into core Effect with the same in-memory registration and shard-lock implementation for tests and local use." +"@effect/cluster/RunnerStorage#makeMemory": + replacement: "effect/unstable/cluster/RunnerStorage#makeMemory" + note: "Moved into core Effect; it still constructs the in-memory RunnerStorage service implementation." diff --git a/.context/effect/migration/annotations/effect__cluster__Runners.yaml b/.context/effect/migration/annotations/effect__cluster__Runners.yaml new file mode 100644 index 000000000..a8f9bff70 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Runners.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/Runners#layerNoop": + replacement: "effect/unstable/cluster/Runners#layerNoop" + note: "Moved into core Effect with the same no-op runner communication layer." +"@effect/cluster/Runners#make": + replacement: "effect/unstable/cluster/Runners#make" + note: "Moved into core Effect with the same callbacks and requirements; Context service projections now use Service instead of Type." +"@effect/cluster/Runners#makeNoop": + replacement: "effect/unstable/cluster/Runners#makeNoop" + note: "Moved into core Effect; it returns the Context.Service implementation through the Service projection instead of Type." diff --git a/.context/effect/migration/annotations/effect__cluster__ShardId.yaml b/.context/effect/migration/annotations/effect__cluster__ShardId.yaml new file mode 100644 index 000000000..3f416f62a --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ShardId.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/ShardId#make": + replacement: "effect/unstable/cluster/ShardId#make" + note: "Moved into core Effect with the same cached group and id constructor." +"@effect/cluster/ShardId#ShardId": + replacement: "effect/unstable/cluster/ShardId#ShardId" + note: "The class became a merged interface and schema value. Use ShardId.make; former static parsers and printers are module functions." +"@effect/cluster/ShardId#TypeId": + replacement: "none" + note: "The shard marker is private in v4. Use ShardId.isShardId for runtime refinement." diff --git a/.context/effect/migration/annotations/effect__cluster__Sharding.yaml b/.context/effect/migration/annotations/effect__cluster__Sharding.yaml new file mode 100644 index 000000000..774b80e24 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Sharding.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/Sharding#layer": + replacement: "effect/unstable/cluster/Sharding#layer" + note: "Moved into core Effect with the same main sharding runtime composition and public service requirements." diff --git a/.context/effect/migration/annotations/effect__cluster__ShardingConfig.yaml b/.context/effect/migration/annotations/effect__cluster__ShardingConfig.yaml new file mode 100644 index 000000000..192d7dd6a --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ShardingConfig.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/ShardingConfig#config": + replacement: "effect/unstable/cluster/ShardingConfig#config" + note: "Moved into core Effect; its Context service value type now uses the Service property instead of Type." +"@effect/cluster/ShardingConfig#defaults": + replacement: "effect/unstable/cluster/ShardingConfig#defaults" + note: "Moved into core Effect with the same complete defaults; service type projections now use Service instead of Type." +"@effect/cluster/ShardingConfig#layer": + replacement: "effect/unstable/cluster/ShardingConfig#layer" + note: "Moved into core Effect with the same shallow default merge; service type projections now use Service instead of Type." diff --git a/.context/effect/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml b/.context/effect/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml new file mode 100644 index 000000000..8aeb99b27 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__ShardingRegistrationEvent.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/ShardingRegistrationEvent#match": + replacement: "effect/unstable/cluster/ShardingRegistrationEvent#match" + note: "Moved into core Effect with the same tagged-enum matcher." diff --git a/.context/effect/migration/annotations/effect__cluster__SingleRunner.yaml b/.context/effect/migration/annotations/effect__cluster__SingleRunner.yaml new file mode 100644 index 000000000..d8dd53d8d --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__SingleRunner.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/SingleRunner#layer": + replacement: "effect/unstable/cluster/SingleRunner#layer" + note: "Moved into core Effect. V4 additionally requires Crypto.Crypto because SQL message storage hashes long deduplication keys." diff --git a/.context/effect/migration/annotations/effect__cluster__Singleton.yaml b/.context/effect/migration/annotations/effect__cluster__Singleton.yaml new file mode 100644 index 000000000..dd9745911 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Singleton.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/Singleton#make": + replacement: "effect/unstable/cluster/Singleton#make" + note: "Moved into core Effect with the same singleton Layer constructor." diff --git a/.context/effect/migration/annotations/effect__cluster__SingletonAddress.yaml b/.context/effect/migration/annotations/effect__cluster__SingletonAddress.yaml new file mode 100644 index 000000000..6f81f8f00 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__SingletonAddress.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/SingletonAddress#TypeId": + replacement: "none" + note: "The singleton-address marker is private in v4. Use the exported SingletonAddress class and schema." diff --git a/.context/effect/migration/annotations/effect__cluster__Snowflake.yaml b/.context/effect/migration/annotations/effect__cluster__Snowflake.yaml new file mode 100644 index 000000000..67e87e5d4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__Snowflake.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/Snowflake#Generator": + replacement: "effect/unstable/cluster/Snowflake#Generator" + note: "Moved into core Effect and changed to Context.Service; its unsafeNext method was renamed to nextUnsafe." +"@effect/cluster/Snowflake#make": + replacement: "effect/unstable/cluster/Snowflake#make" + note: "Moved into core Effect with the same timestamp, machine-id, and sequence packing constructor." +"@effect/cluster/Snowflake#TypeId": + replacement: "effect/unstable/cluster/Snowflake#TypeId" + note: "Moved into core Effect; the public marker is now the string literal ~effect/cluster/Snowflake." diff --git a/.context/effect/migration/annotations/effect__cluster__SocketRunner.yaml b/.context/effect/migration/annotations/effect__cluster__SocketRunner.yaml new file mode 100644 index 000000000..76e9b2481 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__SocketRunner.yaml @@ -0,0 +1,6 @@ +"@effect/cluster/SocketRunner#layer": + replacement: "effect/unstable/cluster/SocketRunner#layer" + note: "Moved into core Effect with the same full socket runner composition." +"@effect/cluster/SocketRunner#layerClientOnly": + replacement: "effect/unstable/cluster/SocketRunner#layerClientOnly" + note: "Moved into core Effect; it remains the client-only runner layer and does not start a socket server." diff --git a/.context/effect/migration/annotations/effect__cluster__SqlMessageStorage.yaml b/.context/effect/migration/annotations/effect__cluster__SqlMessageStorage.yaml new file mode 100644 index 000000000..833229bae --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__SqlMessageStorage.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/SqlMessageStorage#layer": + replacement: "effect/unstable/cluster/SqlMessageStorage#layer" + note: "Moved into core Effect. V4 adds a Crypto.Crypto requirement for hashing long deduplication keys." +"@effect/cluster/SqlMessageStorage#layerWith": + replacement: "effect/unstable/cluster/SqlMessageStorage#layerWith" + note: "Moved into core Effect with the same optional table prefix; v4 additionally requires Crypto.Crypto." +"@effect/cluster/SqlMessageStorage#make": + replacement: "effect/unstable/cluster/SqlMessageStorage#make" + note: "Moved into core Effect with the same prefix option; v4 additionally requires Crypto.Crypto." diff --git a/.context/effect/migration/annotations/effect__cluster__SqlRunnerStorage.yaml b/.context/effect/migration/annotations/effect__cluster__SqlRunnerStorage.yaml new file mode 100644 index 000000000..39c6b0c37 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__SqlRunnerStorage.yaml @@ -0,0 +1,9 @@ +"@effect/cluster/SqlRunnerStorage#layer": + replacement: "effect/unstable/cluster/SqlRunnerStorage#layer" + note: "Moved into core Effect with the same default-prefix SQL runner storage layer." +"@effect/cluster/SqlRunnerStorage#layerWith": + replacement: "effect/unstable/cluster/SqlRunnerStorage#layerWith" + note: "Moved into core Effect with the same optional table prefix." +"@effect/cluster/SqlRunnerStorage#make": + replacement: "effect/unstable/cluster/SqlRunnerStorage#make" + note: "Moved into core Effect with the same prefix option and service requirements." diff --git a/.context/effect/migration/annotations/effect__cluster__TestRunner.yaml b/.context/effect/migration/annotations/effect__cluster__TestRunner.yaml new file mode 100644 index 000000000..521f937d8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__TestRunner.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/TestRunner#layer": + replacement: "effect/unstable/cluster/TestRunner#layer" + note: "Moved into core Effect with the same dependency-free in-memory test cluster composition." diff --git a/.context/effect/migration/annotations/effect__cluster__index.yaml b/.context/effect/migration/annotations/effect__cluster__index.yaml new file mode 100644 index 000000000..00d9e4089 --- /dev/null +++ b/.context/effect/migration/annotations/effect__cluster__index.yaml @@ -0,0 +1,3 @@ +"@effect/cluster/index": + replacement: "effect/unstable/cluster" + note: "The package barrel was removed; import the same namespaces from the effect/unstable/cluster barrel or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__experimental.yaml b/.context/effect/migration/annotations/effect__experimental.yaml new file mode 100644 index 000000000..8f4c3ffa2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental.yaml @@ -0,0 +1,3 @@ +"@effect/experimental": + replacement: "none" + note: "The @effect/experimental package was folded into the effect package, split across effect/unstable/* (devtools, eventlog, persistence, reactivity, ...); follow the Import Map for each module." diff --git a/.context/effect/migration/annotations/effect__experimental__DevTools.yaml b/.context/effect/migration/annotations/effect__experimental__DevTools.yaml new file mode 100644 index 000000000..44ce415b7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__DevTools.yaml @@ -0,0 +1,6 @@ +"@effect/experimental/DevTools#layer": + replacement: effect/unstable/devtools/DevTools#layer + note: Import layer from the v4 unstable DevTools module. +"@effect/experimental/DevTools#layerWebSocket": + replacement: effect/unstable/devtools/DevTools#layerWebSocket + note: Import layerWebSocket from the v4 unstable DevTools module. diff --git a/.context/effect/migration/annotations/effect__experimental__DevTools__Client.yaml b/.context/effect/migration/annotations/effect__experimental__DevTools__Client.yaml new file mode 100644 index 000000000..6009364c9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__DevTools__Client.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/DevTools/Client#Client": + replacement: effect/unstable/devtools/DevToolsClient#DevToolsClient + note: Client was renamed to the DevToolsClient Context.Service class. +"@effect/experimental/DevTools/Client#ClientImpl": + replacement: effect/unstable/devtools/DevToolsClient#DevToolsClient["Service"] + note: Use the service shape from DevToolsClient; unsafeAddSpan was replaced by sendUnsafe. +"@effect/experimental/DevTools/Client#layer": + replacement: effect/unstable/devtools/DevToolsClient#layer + note: Import layer from the v4 unstable DevToolsClient module. +"@effect/experimental/DevTools/Client#layerTracer": + replacement: effect/unstable/devtools/DevToolsClient#layerTracer + note: Import layerTracer from the v4 unstable DevToolsClient module. +"@effect/experimental/DevTools/Client#make": + replacement: effect/unstable/devtools/DevToolsClient#make + note: Import make from the v4 unstable DevToolsClient module. diff --git a/.context/effect/migration/annotations/effect__experimental__DevTools__Domain.yaml b/.context/effect/migration/annotations/effect__experimental__DevTools__Domain.yaml new file mode 100644 index 000000000..71f36dcb2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__DevTools__Domain.yaml @@ -0,0 +1,18 @@ +"@effect/experimental/DevTools/Domain#ExternalSpanFrom": + replacement: effect/Schema#Codec.Encoded + note: The named encoded alias was removed; derive it with Schema.Codec.Encoded from ExternalSpan. +"@effect/experimental/DevTools/Domain#metric": + replacement: none + note: The metric schema helper is private in v4; use the exported Counter, Frequency, Gauge, Histogram, Summary, or Metric schemas, or build a Schema.Struct. +"@effect/experimental/DevTools/Domain#MetricFrom": + replacement: effect/Schema#Codec.Encoded + note: The named encoded alias was removed; derive it with Schema.Codec.Encoded from Metric. +"@effect/experimental/DevTools/Domain#MetricsSnapshotFrom": + replacement: effect/Schema#Codec.Encoded + note: The named encoded alias was removed; derive it with Schema.Codec.Encoded from MetricsSnapshot. +"@effect/experimental/DevTools/Domain#ParentSpanFrom": + replacement: effect/Schema#Codec.Encoded + note: The named encoded alias was removed; derive it with Schema.Codec.Encoded from ParentSpan. +"@effect/experimental/DevTools/Domain#SpanFrom": + replacement: effect/Schema#Codec.Encoded + note: The named encoded alias was removed; derive it with Schema.Codec.Encoded from Span. diff --git a/.context/effect/migration/annotations/effect__experimental__DevTools__Server.yaml b/.context/effect/migration/annotations/effect__experimental__DevTools__Server.yaml new file mode 100644 index 000000000..15396b28a --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__DevTools__Server.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/DevTools/Server#run": + replacement: effect/unstable/devtools/DevToolsServer#run + note: Import run from the v4 unstable DevToolsServer module. diff --git a/.context/effect/migration/annotations/effect__experimental__Event.yaml b/.context/effect/migration/annotations/effect__experimental__Event.yaml new file mode 100644 index 000000000..89e43d33b --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Event.yaml @@ -0,0 +1,42 @@ +"@effect/experimental/Event#Event.AddError": + replacement: effect/unstable/eventlog/Event#AddError + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.Any": + replacement: effect/unstable/eventlog/Event#Any + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.Context": + replacement: effect/unstable/eventlog/Event#Services + note: Event schema context is now represented by decoding and encoding Services. +"@effect/experimental/Event#Event.ContextWithTag": + replacement: effect/unstable/eventlog/Event#Services> + note: Filter with WithTag and derive its decoding and encoding Services. +"@effect/experimental/Event#Event.Error": + replacement: effect/unstable/eventlog/Event#Error + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.ErrorSchema": + replacement: effect/unstable/eventlog/Event#ErrorSchema + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.Payload": + replacement: effect/unstable/eventlog/Event#Payload + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.PayloadSchema": + replacement: effect/unstable/eventlog/Event#PayloadSchema + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.Success": + replacement: effect/unstable/eventlog/Event#Success + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.SuccessSchema": + replacement: effect/unstable/eventlog/Event#SuccessSchema + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.Tag": + replacement: effect/unstable/eventlog/Event#Tag + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#Event.ToService": + replacement: effect/unstable/eventlog/Event#ToService + note: This type moved from the Event namespace to a top-level export. +"@effect/experimental/Event#make": + replacement: effect/unstable/eventlog/Event#make + note: Import make from the v4 unstable Event module. +"@effect/experimental/Event#TypeId": + replacement: effect/unstable/eventlog/Event#TypeId + note: Import TypeId from the v4 unstable Event module; its runtime representation is now a string brand. diff --git a/.context/effect/migration/annotations/effect__experimental__EventGroup.yaml b/.context/effect/migration/annotations/effect__experimental__EventGroup.yaml new file mode 100644 index 000000000..75f54efe3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventGroup.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/EventGroup#EventGroup.Any": + replacement: effect/unstable/eventlog/EventGroup#Any + note: This type moved from the EventGroup namespace to a top-level export. +"@effect/experimental/EventGroup#EventGroup.AnyWithProps": + replacement: effect/unstable/eventlog/EventGroup#AnyWithProps + note: This type moved from the EventGroup namespace to a top-level export. +"@effect/experimental/EventGroup#EventGroup.Context": + replacement: effect/unstable/eventlog/EventGroup#ServicesClient | effect/unstable/eventlog/EventGroup#ServicesServer + note: Choose the client or server schema services for the required direction. +"@effect/experimental/EventGroup#EventGroup.ToService": + replacement: effect/unstable/eventlog/EventGroup#ToService + note: This type moved from the EventGroup namespace to a top-level export. +"@effect/experimental/EventGroup#TypeId": + replacement: effect/unstable/eventlog/EventGroup#TypeId + note: Import TypeId from the v4 unstable EventGroup module; its runtime representation is now a string brand. diff --git a/.context/effect/migration/annotations/effect__experimental__EventJournal.yaml b/.context/effect/migration/annotations/effect__experimental__EventJournal.yaml new file mode 100644 index 000000000..d7ad705bc --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventJournal.yaml @@ -0,0 +1,18 @@ +"@effect/experimental/EventJournal#EntryIdTypeId": + replacement: effect/unstable/eventlog/EventJournal#EntryIdTypeId + note: Import EntryIdTypeId from the v4 EventJournal module; it is now a string brand. +"@effect/experimental/EventJournal#ErrorTypeId": + replacement: none + note: The v4 error marker is private; narrow with EventJournalError instead. +"@effect/experimental/EventJournal#makeEntryId": + replacement: effect/unstable/eventlog/EventJournal#makeEntryIdUnsafe + note: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. +"@effect/experimental/EventJournal#makeRemoteId": + replacement: effect/unstable/eventlog/EventJournal#makeRemoteIdUnsafe + note: The unchecked RemoteId constructor was renamed to makeRemoteIdUnsafe. +"@effect/experimental/EventJournal#RemoteIdTypeId": + replacement: effect/unstable/eventlog/EventJournal#RemoteIdTypeId + note: Import RemoteIdTypeId from the v4 EventJournal module; it is now a string brand. +"@effect/experimental/EventJournal#makeMemory": + replacement: effect/unstable/eventlog/EventJournal#makeMemory + note: The in-memory constructor moved into core Effect and now returns the Context.Service implementation through its Service projection. diff --git a/.context/effect/migration/annotations/effect__experimental__EventLog.yaml b/.context/effect/migration/annotations/effect__experimental__EventLog.yaml new file mode 100644 index 000000000..869753522 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventLog.yaml @@ -0,0 +1,27 @@ +"@effect/experimental/EventLog#group": + replacement: effect/unstable/eventlog/EventLog#group + note: Import group from the v4 EventLog module; it now requires the shared Registry service. +"@effect/experimental/EventLog#Handlers": + replacement: effect/unstable/eventlog/EventLog#Handlers + note: Import Handlers from the v4 EventLog module; handlers now also receive storeId. +"@effect/experimental/EventLog#Handlers.Error": + replacement: effect/unstable/eventlog/EventLog#Handlers.Error + note: Import the retained Handlers.Error type from the v4 EventLog module. +"@effect/experimental/EventLog#Handlers.ValidateReturn": + replacement: effect/unstable/eventlog/EventLog#Handlers.ValidateReturn + note: Import the retained Handlers.ValidateReturn type from the v4 EventLog module. +"@effect/experimental/EventLog#HandlersTypeId": + replacement: effect/unstable/eventlog/EventLog#HandlersTypeId + note: Import HandlersTypeId from the v4 EventLog module. +"@effect/experimental/EventLog#layer": + replacement: effect/unstable/eventlog/EventLog#layer + note: The v4 layer takes both the schema and handler layer; use layerEventLog for runtime only. +"@effect/experimental/EventLog#layerIdentityKvs": + replacement: none + note: Compose KeyValueStore.toSchemaStore, EventLog.IdentitySchema, EventLog.makeIdentity, and Layer.effect manually. +"@effect/experimental/EventLog#schema": + replacement: effect/unstable/eventlog/EventLog#schema + note: Import schema from the v4 EventLog module. +"@effect/experimental/EventLog#SchemaTypeId": + replacement: effect/unstable/eventlog/EventLog#SchemaTypeId + note: Import SchemaTypeId from the v4 EventLog module. diff --git a/.context/effect/migration/annotations/effect__experimental__EventLogRemote.yaml b/.context/effect/migration/annotations/effect__experimental__EventLogRemote.yaml new file mode 100644 index 000000000..281f01ae7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventLogRemote.yaml @@ -0,0 +1,57 @@ +"@effect/experimental/EventLogRemote#Ack": + replacement: none + note: A write acknowledgement is now the void success of EventLogMessage.WriteSingleRpc or WriteChunkedRpc. +"@effect/experimental/EventLogRemote#Changes": + replacement: effect/unstable/eventlog/EventLogMessage#ChangesRpc + note: ChangesRpc replaces the separate request and response models with one streaming RPC. +"@effect/experimental/EventLogRemote#decodeRequest": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request decoder. +"@effect/experimental/EventLogRemote#decodeResponse": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response decoder. +"@effect/experimental/EventLogRemote#encodeRequest": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request encoder. +"@effect/experimental/EventLogRemote#encodeResponse": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response encoder. +"@effect/experimental/EventLogRemote#EventLogRemote": + replacement: effect/unstable/eventlog/EventLogRemote#EventLogRemote + note: Use the v4 Context.Service; methods now take storeId-aware options. +"@effect/experimental/EventLogRemote#fromSocket": + replacement: effect/unstable/eventlog/EventLogRemote#makeEncrypted + effect/unstable/rpc/RpcClient#makeProtocolSocket + note: Construct the encrypted remote separately from its generic RPC socket protocol. +"@effect/experimental/EventLogRemote#Hello": + replacement: effect/unstable/eventlog/EventLogMessage#HelloResponse + note: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. +"@effect/experimental/EventLogRemote#layerWebSocket": + replacement: effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket + note: Compose the encrypted remote with the generic socket protocol, MsgPack serialization, and a Socket provider. +"@effect/experimental/EventLogRemote#layerWebSocketBrowser": + replacement: effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket + @effect/platform-browser/BrowserSocket#layerWebSocket + note: Compose the encrypted remote and generic RPC socket protocol with the browser WebSocket layer. +"@effect/experimental/EventLogRemote#Pong": + replacement: none + note: The event-log Pong model was removed; heartbeats belong to the generic RPC socket protocol. +"@effect/experimental/EventLogRemote#ProtocolRequest": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: EventLogRemoteRpcs and generic RPC serialization replace the old protocol request union. +"@effect/experimental/EventLogRemote#ProtocolRequestMsgPack": + replacement: effect/unstable/rpc/RpcSerialization#layerMsgPack + note: Use the generic MsgPack RPC serialization layer instead of a request-specific schema. +"@effect/experimental/EventLogRemote#ProtocolResponse": + replacement: effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs + note: EventLogRemoteRpcs and generic RPC serialization replace the old protocol response union. +"@effect/experimental/EventLogRemote#ProtocolResponseMsgPack": + replacement: effect/unstable/rpc/RpcSerialization#layerMsgPack + note: Use the generic MsgPack RPC serialization layer instead of a response-specific schema. +"@effect/experimental/EventLogRemote#RemoteAdditions": + replacement: none + note: This unused protocol model has no v4 counterpart. +"@effect/experimental/EventLogRemote#RequestChanges": + replacement: effect/unstable/eventlog/EventLogMessage#ChangesRpc + note: ChangesRpc replaces the separate request model with one streaming RPC. +"@effect/experimental/EventLogRemote#StopChanges": + replacement: none + note: Interrupt the ChangesRpc stream instead of sending a StopChanges message. diff --git a/.context/effect/migration/annotations/effect__experimental__EventLogServer.yaml b/.context/effect/migration/annotations/effect__experimental__EventLogServer.yaml new file mode 100644 index 000000000..8a044978f --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventLogServer.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/EventLogServer#layerStorageMemory": + replacement: effect/unstable/eventlog/EventLogServerEncrypted#layerStorageMemory + note: Use the encrypted server memory storage layer; storage is now storeId- and session-aware. +"@effect/experimental/EventLogServer#makeHandler": + replacement: effect/unstable/eventlog/EventLogServerEncrypted#layer + effect/unstable/rpc/RpcServer#layerProtocolSocketServer + note: Compose the encrypted server layer with the generic RPC socket server; there is no per-socket handler factory. +"@effect/experimental/EventLogServer#makeHandlerHttp": + replacement: effect/unstable/eventlog/EventLogServerEncrypted#layer + effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffectWebsocket + note: Use the returned httpEffect for upgrades and provide its protocol to the encrypted server layer. +"@effect/experimental/EventLogServer#makeStorageMemory": + replacement: effect/unstable/eventlog/EventLogServerEncrypted#makeStorageMemory + note: Use the encrypted server memory storage constructor. +"@effect/experimental/EventLogServer#Storage": + replacement: effect/unstable/eventlog/EventLogServerEncrypted#Storage + note: Use the encrypted server Storage service, which is storeId- and session-aware. diff --git a/.context/effect/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml b/.context/effect/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml new file mode 100644 index 000000000..bec876f63 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__EventLogServer__Cloudflare.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/EventLogServer/Cloudflare": + replacement: none + note: The Cloudflare adapter was not ported; combine EventLogServerEncrypted.layer with a custom Durable Object RpcServer.Protocol adapter. diff --git a/.context/effect/migration/annotations/effect__experimental__Machine.yaml b/.context/effect/migration/annotations/effect__experimental__Machine.yaml new file mode 100644 index 000000000..d4d737f3d --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Machine.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Machine": + replacement: none + note: The experimental local Machine actor runtime, model, boot process, constructors, brands, and serializable variants were not ported to v4. Redesign request contracts with Rpc/RpcGroup and choose Cluster Entity, Workflow, or a local actor built from Queue, Ref, PubSub, and scoped fibers according to the required semantics; ClusterWorkflowEngine is a different durable Workflow abstraction. For serializable actors, define schemas with Rpc/RpcGroup and choose Cluster Entity or Workflow explicitly. Context and initialization helpers (including the serializable initialization contract), input/private/public/state extractors, and the Machine-specific handler context were also removed, so request handling and state management must be explicit. Use ordinary Effect tracing controls and Effect.retry instead of the removed Machine-specific wrappers; its defect wrapper was also removed. Snapshot restoration was not ported, so implement persistence explicitly for the replacement architecture. diff --git a/.context/effect/migration/annotations/effect__experimental__Machine__Procedure.yaml b/.context/effect/migration/annotations/effect__experimental__Machine__Procedure.yaml new file mode 100644 index 000000000..2a62aeb1e --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Machine__Procedure.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Machine/Procedure": + replacement: none + note: The stateful Machine Procedure model, its serializable variant and guard, and both Procedure brands were not ported to v4. Define request contracts with Rpc (using schemas for serializable procedures) and implement state handling in an explicit actor architecture, because Rpc provides only the request contract. The handler context, context and request extractors, and no-reply sentinel were removed; use the corresponding Rpc request types after redesigning the contract. Replace the removed tagged-request base and helpers with schema-backed Rpc requests and Rpc helper types where appropriate. diff --git a/.context/effect/migration/annotations/effect__experimental__Machine__ProcedureList.yaml b/.context/effect/migration/annotations/effect__experimental__Machine__ProcedureList.yaml new file mode 100644 index 000000000..6dd50fbfb --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Machine__ProcedureList.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Machine/ProcedureList": + replacement: none + note: The stateful Machine ProcedureList abstraction and brand were not ported to v4. RpcGroup is the closest protocol collection for its schema-backed operations, but it has no initial state or public/private visibility split. Implement state handling and initialization in the replacement actor or workflow, and enforce visibility in that architecture. diff --git a/.context/effect/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml b/.context/effect/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml new file mode 100644 index 000000000..3dcce020f --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Machine__SerializableProcedureList.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Machine/SerializableProcedureList": + replacement: none + note: The serializable stateful ProcedureList abstraction was not ported to v4. RpcGroup is the closest protocol collection for its schema-backed operations, but it has no initial state or public/private visibility split. Implement state handling and initialization in the replacement actor or workflow, and enforce visibility in that architecture. diff --git a/.context/effect/migration/annotations/effect__experimental__PersistedCache.yaml b/.context/effect/migration/annotations/effect__experimental__PersistedCache.yaml new file mode 100644 index 000000000..d766b9c6b --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__PersistedCache.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/PersistedCache#make": + replacement: effect/unstable/persistence/PersistedCache#make + note: Pass lookup as the first argument and options second; timeToLive now receives exit before request and the service is Persistence.Persistence. diff --git a/.context/effect/migration/annotations/effect__experimental__PersistedQueue.yaml b/.context/effect/migration/annotations/effect__experimental__PersistedQueue.yaml new file mode 100644 index 000000000..bd53323f6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__PersistedQueue.yaml @@ -0,0 +1,12 @@ +"@effect/experimental/PersistedQueue#layer": + replacement: effect/unstable/persistence/PersistedQueue#layer + note: Import layer from the v4 unstable PersistedQueue module. +"@effect/experimental/PersistedQueue#layerStoreMemory": + replacement: effect/unstable/persistence/PersistedQueue#layerStoreMemory + note: Import layerStoreMemory from the v4 unstable PersistedQueue module. +"@effect/experimental/PersistedQueue#make": + replacement: effect/unstable/persistence/PersistedQueue#make + note: Import make from the v4 unstable PersistedQueue module. +"@effect/experimental/PersistedQueue#TypeId": + replacement: effect/unstable/persistence/PersistedQueue#TypeId + note: Import TypeId from the v4 unstable PersistedQueue module; it is now a string brand. diff --git a/.context/effect/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml b/.context/effect/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml new file mode 100644 index 000000000..cc9dd54be --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__PersistedQueue__Redis.yaml @@ -0,0 +1,9 @@ +"@effect/experimental/PersistedQueue/Redis#layerStore": + replacement: effect/unstable/persistence/PersistedQueue#layerStoreRedis + note: The Redis adapter was merged into PersistedQueue and now requires the generic Redis.Redis service. +"@effect/experimental/PersistedQueue/Redis#layerStoreConfig": + replacement: none + note: Configure a Redis provider such as NodeRedis.layerConfig separately, then compose it with PersistedQueue.layerStoreRedis. +"@effect/experimental/PersistedQueue/Redis#make": + replacement: effect/unstable/persistence/PersistedQueue#makeStoreRedis + note: The Redis adapter was merged into PersistedQueue and now requires the generic Redis.Redis service. diff --git a/.context/effect/migration/annotations/effect__experimental__Persistence.yaml b/.context/effect/migration/annotations/effect__experimental__Persistence.yaml new file mode 100644 index 000000000..44ee17247 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Persistence.yaml @@ -0,0 +1,51 @@ +"@effect/experimental/Persistence#BackingPersistence": + replacement: effect/unstable/persistence/Persistence#BackingPersistence + note: Use the v4 BackingPersistence Context.Service class. +"@effect/experimental/Persistence#BackingPersistenceTypeId": + replacement: none + note: The BackingPersistence brand is no longer publicly exported in v4. +"@effect/experimental/Persistence#ErrorTypeId": + replacement: none + note: The v4 persistence error identifier is private; narrow with the exported error classes. +"@effect/experimental/Persistence#layerKeyValueStore": + replacement: effect/unstable/persistence/Persistence#layerBackingKvs + note: The KeyValueStore backing layer was renamed to layerBackingKvs. +"@effect/experimental/Persistence#layerMemory": + replacement: effect/unstable/persistence/Persistence#layerBackingMemory + note: Use layerBackingMemory for the old backing service; v4 layerMemory creates the higher-level Persistence service. +"@effect/experimental/Persistence#layerResult": + replacement: effect/unstable/persistence/Persistence#layer + note: The ResultPersistence service layer was renamed to layer. +"@effect/experimental/Persistence#layerResultKeyValueStore": + replacement: effect/unstable/persistence/Persistence#layerKvs + note: The combined KeyValueStore-backed result layer was renamed to layerKvs. +"@effect/experimental/Persistence#layerResultMemory": + replacement: effect/unstable/persistence/Persistence#layerMemory + note: The combined memory-backed result layer was renamed to layerMemory. +"@effect/experimental/Persistence#PersistenceBackingError": + replacement: effect/unstable/persistence/Persistence#PersistenceError + note: PersistenceError now represents failures from the backing persistence implementation. +"@effect/experimental/Persistence#PersistenceError": + replacement: effect/unstable/persistence/Persistence#PersistenceError | effect/Schema#SchemaError + note: The old combined alias was split into backing PersistenceError and schema SchemaError. +"@effect/experimental/Persistence#PersistenceParseError": + replacement: effect/Schema#SchemaError + note: Persistence parsing failures now use the core SchemaError type. +"@effect/experimental/Persistence#ResultPersistence": + replacement: effect/unstable/persistence/Persistence#Persistence + note: ResultPersistence was renamed to Persistence and is now a Context.Service class. +"@effect/experimental/Persistence#ResultPersistence.Key": + replacement: effect/unstable/persistence/Persistable#Persistable + note: Persistable is the v4 schema-backed persistence key contract. +"@effect/experimental/Persistence#ResultPersistence.KeyAny": + replacement: effect/unstable/persistence/Persistable#Any + note: Use Persistable.Any for an arbitrary v4 persistence key contract. +"@effect/experimental/Persistence#ResultPersistence.TimeToLiveArgs": + replacement: Parameters> + note: Derive the tuple from TimeToLiveFn; its order is now exit then request. +"@effect/experimental/Persistence#ResultPersistenceStore": + replacement: effect/unstable/persistence/Persistence#PersistenceStore + note: ResultPersistenceStore was renamed to PersistenceStore. +"@effect/experimental/Persistence#ResultPersistenceTypeId": + replacement: none + note: The ResultPersistence brand is no longer publicly exported in v4. diff --git a/.context/effect/migration/annotations/effect__experimental__Persistence__Lmdb.yaml b/.context/effect/migration/annotations/effect__experimental__Persistence__Lmdb.yaml new file mode 100644 index 000000000..16ac3dfd3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Persistence__Lmdb.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Persistence/Lmdb": + replacement: none + note: The LMDB backend was not ported; implement a custom BackingPersistence layer or use a supported Kvs, Redis, or SQL backend. diff --git a/.context/effect/migration/annotations/effect__experimental__Persistence__Redis.yaml b/.context/effect/migration/annotations/effect__experimental__Persistence__Redis.yaml new file mode 100644 index 000000000..dfc6b6e41 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Persistence__Redis.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/Persistence/Redis#layer": + replacement: effect/unstable/persistence/Persistence#layerBackingRedis + note: The Redis backing adapter was merged into Persistence and now requires the generic Redis.Redis service. +"@effect/experimental/Persistence/Redis#layerConfig": + replacement: none + note: Compose Persistence.layerBackingRedis with a config-driven provider such as NodeRedis.layerConfig. +"@effect/experimental/Persistence/Redis#layerResult": + replacement: effect/unstable/persistence/Persistence#layerRedis + note: The combined Redis persistence layer was merged into Persistence and now requires Redis.Redis. +"@effect/experimental/Persistence/Redis#layerResultConfig": + replacement: none + note: Compose Persistence.layerRedis with a config-driven provider such as NodeRedis.layerConfig. +"@effect/experimental/Persistence/Redis#make": + replacement: none + note: V4 exposes Redis-backed layers over the Redis.Redis service, not a constructor that creates an ioredis client directly. diff --git a/.context/effect/migration/annotations/effect__experimental__RateLimiter.yaml b/.context/effect/migration/annotations/effect__experimental__RateLimiter.yaml new file mode 100644 index 000000000..4ca45b2d7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__RateLimiter.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/RateLimiter#layer": + replacement: effect/unstable/persistence/RateLimiter#layer + note: Import layer from the v4 unstable RateLimiter module. +"@effect/experimental/RateLimiter#make": + replacement: effect/unstable/persistence/RateLimiter#make + note: Import make from the v4 unstable RateLimiter module. +"@effect/experimental/RateLimiter#makeSleep": + replacement: effect/unstable/persistence/RateLimiter#sleep + note: The accessor Effect was replaced by sleep; obtain the RateLimiter service and pass it to sleep directly or with its curried overload. +"@effect/experimental/RateLimiter#RateLimiterError": + replacement: effect/unstable/persistence/RateLimiter#RateLimiterError + note: The retained name is now a wrapper error class whose reason is RateLimitExceeded or RateLimitStoreError. +"@effect/experimental/RateLimiter#TypeId": + replacement: effect/unstable/persistence/RateLimiter#TypeId + note: Import TypeId from the v4 unstable RateLimiter module; it is now a string brand. diff --git a/.context/effect/migration/annotations/effect__experimental__RateLimiter__Redis.yaml b/.context/effect/migration/annotations/effect__experimental__RateLimiter__Redis.yaml new file mode 100644 index 000000000..27592db8a --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__RateLimiter__Redis.yaml @@ -0,0 +1,9 @@ +"@effect/experimental/RateLimiter/Redis#layerStore": + replacement: effect/unstable/persistence/RateLimiter#layerStoreRedis + note: The Redis adapter was merged into RateLimiter and now requires the generic Redis.Redis service. +"@effect/experimental/RateLimiter/Redis#layerStoreConfig": + replacement: effect/unstable/persistence/RateLimiter#layerStoreRedisConfig + note: Use the merged Redis store config layer; connection configuration belongs to a separate Redis provider. +"@effect/experimental/RateLimiter/Redis#make": + replacement: effect/unstable/persistence/RateLimiter#makeStoreRedis + note: The Redis adapter was merged into RateLimiter and now requires the generic Redis.Redis service. diff --git a/.context/effect/migration/annotations/effect__experimental__Reactivity.yaml b/.context/effect/migration/annotations/effect__experimental__Reactivity.yaml new file mode 100644 index 000000000..621a7f78d --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Reactivity.yaml @@ -0,0 +1,15 @@ +"@effect/experimental/Reactivity#layer": + replacement: effect/unstable/reactivity/Reactivity#layer + note: Import layer from the v4 unstable Reactivity module. +"@effect/experimental/Reactivity#make": + replacement: effect/unstable/reactivity/Reactivity#make + note: Import make from the v4 unstable Reactivity module. +"@effect/experimental/Reactivity#Reactivity": + replacement: effect/unstable/reactivity/Reactivity#Reactivity + note: Use the v4 Reactivity Context.Service; unsafe methods were renamed with an Unsafe suffix. +"@effect/experimental/Reactivity#Reactivity.Service": + replacement: effect/unstable/reactivity/Reactivity#Reactivity["Service"] + note: The named namespace member was removed; derive the service shape from the Context.Service class. +"@effect/experimental/Reactivity#stream": + replacement: effect/unstable/reactivity/Reactivity#stream + note: Import stream from the v4 unstable Reactivity module. diff --git a/.context/effect/migration/annotations/effect__experimental__RequestResolver.yaml b/.context/effect/migration/annotations/effect__experimental__RequestResolver.yaml new file mode 100644 index 000000000..69fe62a7b --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__RequestResolver.yaml @@ -0,0 +1,12 @@ +"@effect/experimental/RequestResolver#dataLoader": + replacement: effect/RequestResolver#setDelay + effect/RequestResolver#batchN + note: Pipe the resolver through setDelay(options.window) and batchN(options.maxBatchSize ?? Infinity); the transformation is now pure. +"@effect/experimental/RequestResolver#PersistedRequest": + replacement: effect/Request#Request & effect/unstable/persistence/Persistable#Persistable + note: Intersect a Request with Persistable or define it with Persistable.Class; there is no combined named export. +"@effect/experimental/RequestResolver#PersistedRequest.Any": + replacement: effect/Request#Any & effect/unstable/persistence/Persistable#Any + note: Intersect the Request and Persistable helper types for an arbitrary persisted request. +"@effect/experimental/RequestResolver#persisted": + replacement: effect/RequestResolver#persisted + note: Retained after moving to core RequestResolver; requests now implement Persistable and use Persistence.Persistence, timeToLive is optional, and staleWhileRevalidate is supported. diff --git a/.context/effect/migration/annotations/effect__experimental__Sse.yaml b/.context/effect/migration/annotations/effect__experimental__Sse.yaml new file mode 100644 index 000000000..5f233fe6c --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__Sse.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/Sse#RetryTypeId": + replacement: none + note: The Retry identifier is private in v4; use effect/unstable/encoding/Sse#Retry and Retry.is instead of inspecting the brand. diff --git a/.context/effect/migration/annotations/effect__experimental__VariantSchema.yaml b/.context/effect/migration/annotations/effect__experimental__VariantSchema.yaml new file mode 100644 index 000000000..865c3f4d0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__VariantSchema.yaml @@ -0,0 +1,39 @@ +"@effect/experimental/VariantSchema#Field.Any": + replacement: effect/unstable/schema/VariantSchema#Field.Any + note: Import the retained Field.Any helper type from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#Field.Config": + replacement: effect/unstable/schema/VariantSchema#Field.Config + note: Import the retained Field.Config helper type from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#Field.Fields": + replacement: effect/unstable/schema/VariantSchema#Field.Fields + note: Import the retained Field.Fields helper type from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#Field.ValueAny": + replacement: effect/Schema#Top + note: Use the core Schema.Top constraint for an arbitrary field value schema. +"@effect/experimental/VariantSchema#FieldTypeId": + replacement: none + note: The Field brand is private in v4; use VariantSchema.isField for narrowing. +"@effect/experimental/VariantSchema#fromKey": + replacement: none + note: Field-level fromKey was not ported; for whole-struct encoded-key renaming consider Schema.encodeKeys. +"@effect/experimental/VariantSchema#fromKey.Rename": + replacement: none + note: The fromKey rename helper was not ported; for whole-struct encoded-key renaming consider Schema.encodeKeys. +"@effect/experimental/VariantSchema#isField": + replacement: effect/unstable/schema/VariantSchema#isField + note: Import isField from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#make": + replacement: effect/unstable/schema/VariantSchema#make + note: Import make from the v4 module; FieldOnly and FieldExcept take one key array and Union takes one member array. +"@effect/experimental/VariantSchema#Override": + replacement: effect/unstable/schema/VariantSchema#Override + note: Import Override from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#Struct.Fields": + replacement: effect/unstable/schema/VariantSchema#Struct.Fields + note: Import the retained Struct.Fields helper type from the v4 unstable VariantSchema module. +"@effect/experimental/VariantSchema#TypeId": + replacement: effect/unstable/schema/VariantSchema#TypeId + note: Use the retained runtime value; in type position use typeof VariantSchema.TypeId. +"@effect/experimental/VariantSchema#Extract": + replacement: effect/unstable/schema/VariantSchema#Extract + note: Import the retained helper from the v4 module; its erased schema constraint is Schema.Top. diff --git a/.context/effect/migration/annotations/effect__experimental__index.yaml b/.context/effect/migration/annotations/effect__experimental__index.yaml new file mode 100644 index 000000000..ca42fdd69 --- /dev/null +++ b/.context/effect/migration/annotations/effect__experimental__index.yaml @@ -0,0 +1,3 @@ +"@effect/experimental/index": + replacement: "none" + note: "The package barrel was removed along with the package; import each module from its new effect/unstable/* location per the Import Map." diff --git a/.context/effect/migration/annotations/effect__index.yaml b/.context/effect/migration/annotations/effect__index.yaml new file mode 100644 index 000000000..1f52497db --- /dev/null +++ b/.context/effect/migration/annotations/effect__index.yaml @@ -0,0 +1,12 @@ +"effect/index#Context": + replacement: "Context" + note: "Keep importing Context from effect; v4 removes declaration merges that made tags and references STM subtypes." +"effect/index#Effect": + replacement: "Effect" + note: "Keep importing Effect from effect; v4 removes declaration merges that made Effects structural Sink, Stream, and Channel subtypes." +"effect/index#Either": + replacement: "Result" + note: "Either was renamed to Result; Right and Left became Success and Failure, with Result.succeed and Result.fail constructors." +"effect/index#Option": + replacement: "Option" + note: "Keep importing Option from effect; Option is no longer an Effect or STM subtype, so use Effect.fromOption when needed." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__Logger.yaml b/.context/effect/migration/annotations/effect__opentelemetry__Logger.yaml new file mode 100644 index 000000000..ea049f648 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__Logger.yaml @@ -0,0 +1,9 @@ +"@effect/opentelemetry/Logger#layerLoggerAdd": + replacement: "OtelLogger.layer({ mergeWithExisting: true })" + note: "The Logger module was renamed to OtelLogger; logger installation is now one configurable layer, with true preserving the v3 additive behavior." +"@effect/opentelemetry/Logger#layerLoggerReplace": + replacement: "OtelLogger.layer({ mergeWithExisting: false })" + note: "The Logger module was renamed to OtelLogger; logger installation is now one configurable layer, with false replacing existing loggers." +"@effect/opentelemetry/Logger#make": + replacement: "OtelLogger.make" + note: "The constructor remains in the renamed OtelLogger module." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__Metrics.yaml b/.context/effect/migration/annotations/effect__opentelemetry__Metrics.yaml new file mode 100644 index 000000000..ad26cfa0d --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__Metrics.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/Metrics#layer": + replacement: "OtelMetrics.layer" + note: "The Metrics module was renamed to OtelMetrics; the layer remains and now also accepts an optional temporality setting." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__NodeSdk.yaml b/.context/effect/migration/annotations/effect__opentelemetry__NodeSdk.yaml new file mode 100644 index 000000000..4c9ab8a47 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__NodeSdk.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/NodeSdk#Configuration": + replacement: "NodeSdk.Configuration" + note: "The configuration interface remains; v4 adds metricTemporality and loggerMergeWithExisting options." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__Otlp.yaml b/.context/effect/migration/annotations/effect__opentelemetry__Otlp.yaml new file mode 100644 index 000000000..6737fd2d4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__Otlp.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/Otlp#layer": + replacement: "Otlp.layer" + note: "Moved to effect/unstable/observability/Otlp; replaceLogger was replaced by loggerMergeWithExisting, and metricsTemporality is now configurable." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__OtlpLogger.yaml b/.context/effect/migration/annotations/effect__opentelemetry__OtlpLogger.yaml new file mode 100644 index 000000000..abe3f8563 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__OtlpLogger.yaml @@ -0,0 +1,6 @@ +"@effect/opentelemetry/OtlpLogger#layer": + replacement: "OtlpLogger.layer" + note: "Moved to effect/unstable/observability/OtlpLogger; use mergeWithExisting instead of passing replaceLogger." +"@effect/opentelemetry/OtlpLogger#make": + replacement: "OtlpLogger.make" + note: "The constructor remains in the module moved to effect/unstable/observability/OtlpLogger." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml b/.context/effect/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml new file mode 100644 index 000000000..c6b7b28b8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__OtlpMetrics.yaml @@ -0,0 +1,6 @@ +"@effect/opentelemetry/OtlpMetrics#layer": + replacement: "OtlpMetrics.layer" + note: "Moved to effect/unstable/observability/OtlpMetrics; the layer now also accepts optional cumulative or delta temporality." +"@effect/opentelemetry/OtlpMetrics#make": + replacement: "OtlpMetrics.make" + note: "Moved to effect/unstable/observability/OtlpMetrics; the constructor now also accepts optional cumulative or delta temporality." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__OtlpResource.yaml b/.context/effect/migration/annotations/effect__opentelemetry__OtlpResource.yaml new file mode 100644 index 000000000..a649a5fb4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__OtlpResource.yaml @@ -0,0 +1,6 @@ +"@effect/opentelemetry/OtlpResource#make": + replacement: "OtlpResource.make" + note: "The constructor remains in the module moved to effect/unstable/observability/OtlpResource." +"@effect/opentelemetry/OtlpResource#unsafeServiceName": + replacement: "OtlpResource.serviceNameUnsafe" + note: "Moved to effect/unstable/observability/OtlpResource and renamed to follow the v4 unsafe-suffix convention." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__OtlpTracer.yaml b/.context/effect/migration/annotations/effect__opentelemetry__OtlpTracer.yaml new file mode 100644 index 000000000..2487741a9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__OtlpTracer.yaml @@ -0,0 +1,6 @@ +"@effect/opentelemetry/OtlpTracer#layer": + replacement: "OtlpTracer.layer" + note: "The layer remains in the module moved to effect/unstable/observability/OtlpTracer." +"@effect/opentelemetry/OtlpTracer#make": + replacement: "OtlpTracer.make" + note: "The constructor remains in the module moved to effect/unstable/observability/OtlpTracer." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__Resource.yaml b/.context/effect/migration/annotations/effect__opentelemetry__Resource.yaml new file mode 100644 index 000000000..efe4a6026 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__Resource.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/Resource#Resource": + replacement: "Resource.Resource" + note: "The service remains in @effect/opentelemetry/Resource but is now a Context.Service class rather than a separate Tag interface and value." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__Tracer.yaml b/.context/effect/migration/annotations/effect__opentelemetry__Tracer.yaml new file mode 100644 index 000000000..c6448494f --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__Tracer.yaml @@ -0,0 +1,21 @@ +"@effect/opentelemetry/Tracer#layer": + replacement: "OtelTracer.layer" + note: "The Tracer module was renamed to OtelTracer; this still creates an OpenTelemetry tracer and installs it as Effect's tracer." +"@effect/opentelemetry/Tracer#layerTracer": + replacement: "OtelTracer.layerTracer" + note: "The Tracer module was renamed to OtelTracer; this layer still creates only the OpenTelemetry tracer service." +"@effect/opentelemetry/Tracer#make": + replacement: "OtelTracer.make" + note: "The constructor remains in the renamed OtelTracer module." +"@effect/opentelemetry/Tracer#OtelTraceFlags": + replacement: "OtelTracer.OtelTraceFlags" + note: "The service moved with the module and is now declared as a Context.Service class." +"@effect/opentelemetry/Tracer#OtelTracer": + replacement: "OtelTracer.OtelTracer" + note: "The service moved with the renamed module and is now declared as a Context.Service class." +"@effect/opentelemetry/Tracer#OtelTracerProvider": + replacement: "OtelTracer.OtelTracerProvider" + note: "The service moved with the renamed module and is now declared as a Context.Service class." +"@effect/opentelemetry/Tracer#OtelTraceState": + replacement: "OtelTracer.OtelTraceState" + note: "The service moved with the module and is now declared as a Context.Service class." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__WebSdk.yaml b/.context/effect/migration/annotations/effect__opentelemetry__WebSdk.yaml new file mode 100644 index 000000000..5b6fe7aa7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__WebSdk.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/WebSdk#Configuration": + replacement: "WebSdk.Configuration" + note: "The configuration interface remains; v4 adds metricTemporality and loggerMergeWithExisting options." diff --git a/.context/effect/migration/annotations/effect__opentelemetry__index.yaml b/.context/effect/migration/annotations/effect__opentelemetry__index.yaml new file mode 100644 index 000000000..7ae5d345b --- /dev/null +++ b/.context/effect/migration/annotations/effect__opentelemetry__index.yaml @@ -0,0 +1,3 @@ +"@effect/opentelemetry/index": + replacement: "@effect/opentelemetry" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/opentelemetry package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml b/.context/effect/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml new file mode 100644 index 000000000..f27bb5e68 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__BrowserHttpClient.yaml @@ -0,0 +1,3 @@ +"@effect/platform-browser/BrowserHttpClient#currentXHRResponseType": + replacement: "BrowserHttpClient.CurrentXHRResponseType" + note: "The FiberRef became a defaulted Context.Reference; use withXHRArrayBuffer or provide the reference as a service." diff --git a/.context/effect/migration/annotations/effect__platform-browser__BrowserWorker.yaml b/.context/effect/migration/annotations/effect__platform-browser__BrowserWorker.yaml new file mode 100644 index 000000000..1d91ac92e --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__BrowserWorker.yaml @@ -0,0 +1,6 @@ +"@effect/platform-browser/BrowserWorker#layerManager": + replacement: "BrowserWorker.layerPlatform" + note: "WorkerManager was removed. Provide WorkerPlatform directly, or use BrowserWorker.layer(spawn) when a Worker.Spawner is also required." +"@effect/platform-browser/BrowserWorker#layerWorker": + replacement: "BrowserWorker.layerPlatform" + note: "PlatformWorker became Worker.WorkerPlatform. The platform-only layer no longer takes a spawn callback; BrowserWorker.layer(spawn) combines platform and spawner layers." diff --git a/.context/effect/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml b/.context/effect/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml new file mode 100644 index 000000000..c0b9d7bdc --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__BrowserWorkerRunner.yaml @@ -0,0 +1,3 @@ +"@effect/platform-browser/BrowserWorkerRunner#launch": + replacement: "Layer.launch + RpcServer.layerProtocolWorkerRunner" + note: "The close-latch launcher was removed. Compose BrowserWorkerRunner.layer with the worker RPC server protocol layer and launch the resulting handler layer." diff --git a/.context/effect/migration/annotations/effect__platform-browser__Clipboard.yaml b/.context/effect/migration/annotations/effect__platform-browser__Clipboard.yaml new file mode 100644 index 000000000..f9e8846c0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__Clipboard.yaml @@ -0,0 +1,9 @@ +"@effect/platform-browser/Clipboard#Clipboard": + replacement: "Clipboard.Clipboard" + note: "The service remains, now as a Context.Service with a private brand; normal access and provision are unchanged." +"@effect/platform-browser/Clipboard#ErrorTypeId": + replacement: "none" + note: "The error marker is private in v4; discriminate ClipboardError by its _tag instead." +"@effect/platform-browser/Clipboard#TypeId": + replacement: "none" + note: "The service brand is private in v4; use the Clipboard Context.Service value." diff --git a/.context/effect/migration/annotations/effect__platform-browser__Geolocation.yaml b/.context/effect/migration/annotations/effect__platform-browser__Geolocation.yaml new file mode 100644 index 000000000..f7e226d97 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__Geolocation.yaml @@ -0,0 +1,12 @@ +"@effect/platform-browser/Geolocation#ErrorTypeId": + replacement: "none" + note: "The error marker is private in v4; discriminate GeolocationError and its tagged reason." +"@effect/platform-browser/Geolocation#Geolocation": + replacement: "Geolocation.Geolocation" + note: "The service remains, now as a Context.Service with a private brand." +"@effect/platform-browser/Geolocation#GeolocationError": + replacement: "Geolocation.GeolocationError" + note: "The class remains, but reason is now PositionUnavailable, PermissionDenied, or Timeout, with the cause stored on that tagged reason." +"@effect/platform-browser/Geolocation#TypeId": + replacement: "none" + note: "The service marker is private in v4; use the Geolocation Context.Service value." diff --git a/.context/effect/migration/annotations/effect__platform-browser__Permissions.yaml b/.context/effect/migration/annotations/effect__platform-browser__Permissions.yaml new file mode 100644 index 000000000..9d87f2777 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__Permissions.yaml @@ -0,0 +1,12 @@ +"@effect/platform-browser/Permissions#ErrorTypeId": + replacement: "none" + note: "The error marker is private in v4; discriminate PermissionsError and its tagged reason." +"@effect/platform-browser/Permissions#Permissions": + replacement: "Permissions.Permissions" + note: "The query service remains, now as a Context.Service with a private brand." +"@effect/platform-browser/Permissions#PermissionsError": + replacement: "Permissions.PermissionsError" + note: "The class remains, but reason is now PermissionsInvalidStateError or PermissionsTypeError, with the cause stored on that tagged reason." +"@effect/platform-browser/Permissions#TypeId": + replacement: "none" + note: "The service marker is private in v4; use the Permissions Context.Service value." diff --git a/.context/effect/migration/annotations/effect__platform-browser__index.yaml b/.context/effect/migration/annotations/effect__platform-browser__index.yaml new file mode 100644 index 000000000..0def0b900 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-browser__index.yaml @@ -0,0 +1,3 @@ +"@effect/platform-browser/index": + replacement: "@effect/platform-browser" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-browser package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml new file mode 100644 index 000000000..ac93b76bd --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunCommandExecutor.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunCommandExecutor#layer": + replacement: "BunChildProcessSpawner.layer" + note: "CommandExecutor became effect/unstable/process/ChildProcessSpawner; the Bun adapter was renamed and still requires FileSystem and Path." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunContext.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunContext.yaml new file mode 100644 index 000000000..c4cc6a98e --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunContext.yaml @@ -0,0 +1,6 @@ +"@effect/platform-bun/BunContext#BunContext": + replacement: "BunServices.BunServices" + note: "The aggregate was renamed and now provides ChildProcessSpawner, Crypto, FileSystem, Path, Stdio, and Terminal; add BunWorker separately when needed." +"@effect/platform-bun/BunContext#layer": + replacement: "BunServices.layer" + note: "Use the renamed aggregate layer; worker services are no longer included." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml new file mode 100644 index 000000000..a90b9e375 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunFileSystem__ParcelWatcher.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunFileSystem/ParcelWatcher#layer": + replacement: "BunFileSystem.layer" + note: "The Parcel watcher adapter was removed. BunFileSystem.layer uses the built-in node:fs-compatible watcher; provide a custom FileSystem.WatchBackend for specialized behavior." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml new file mode 100644 index 000000000..2fdea4a15 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunHttpPlatform.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunHttpPlatform#make": + replacement: "BunHttpPlatform.layer" + note: "The Bun-specific constructor is private; provide the public layer and consume HttpPlatform.HttpPlatform." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunHttpServer.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunHttpServer.yaml new file mode 100644 index 000000000..95b7882e1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunHttpServer.yaml @@ -0,0 +1,6 @@ +"@effect/platform-bun/BunHttpServer#layerContext": + replacement: "BunHttpServer.layerHttpServices" + note: "Direct rename; it provides HttpPlatform, Etag.Generator, and BunServices." +"@effect/platform-bun/BunHttpServer#ServeOptions": + replacement: "BunHttpServer.ServeOptions" + note: "The alias remains, but R is now a route-key string union and routes uses Bun.Serve.Routes; update old route-map generic arguments or infer R from routes." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml new file mode 100644 index 000000000..9f720a621 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunHttpServerRequest.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunHttpServerRequest#toRequest": + replacement: "BunHttpServerRequest.toBunServerRequest" + note: "Direct rename with the more precise Bun.BunRequest result type." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml new file mode 100644 index 000000000..000285be3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunKeyValueStore.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunKeyValueStore": + replacement: "effect/unstable/persistence/KeyValueStore" + note: "layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via BunServices.layer or BunFileSystem.layer with BunPath.layer." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunSink.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunSink.yaml new file mode 100644 index 000000000..3d2ed68a6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunSink.yaml @@ -0,0 +1,9 @@ +"@effect/platform-bun/BunSink#stderr": + replacement: "stdio.stderr() from Stdio.Stdio" + note: "Process stdio moved behind effect/Stdio; provide BunStdio.layer. stderr remains a Sink and can be configured with endOnDone." +"@effect/platform-bun/BunSink#stdin": + replacement: "stdio.stdin from Stdio.Stdio" + note: "Standard input is correctly modeled as a Stream in v4, not a writable Sink; manually adapt process.stdin only if writing to it was intentional." +"@effect/platform-bun/BunSink#stdout": + replacement: "stdio.stdout() from Stdio.Stdio" + note: "Process stdio moved behind effect/Stdio; provide BunStdio.layer. stdout remains a Sink." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunSocket.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunSocket.yaml new file mode 100644 index 000000000..49ab6a192 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunSocket.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunSocket#NetSocket": + replacement: "BunSocket.NetSocket" + note: "The identifier remains, but the old interface/tag pair is now one Context.Service for node:net.Socket." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunSocketServer.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunSocketServer.yaml new file mode 100644 index 000000000..8d2679ede --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunSocketServer.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/BunSocketServer#IncomingMessage": + replacement: "BunSocketServer.IncomingMessage" + note: "The identifier remains and is now a Context.Service for node:http.IncomingMessage." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunStream.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunStream.yaml new file mode 100644 index 000000000..4b343a35c --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunStream.yaml @@ -0,0 +1,15 @@ +"@effect/platform-bun/BunStream#FromReadableOptions": + replacement: "Pick[0], \"chunkSize\" | \"closeOnDone\">" + note: "The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate, onError, and bufferSize." +"@effect/platform-bun/BunStream#FromWritableOptions": + replacement: "Pick[0], \"endOnDone\" | \"encoding\">" + note: "The named interface was inlined into BunSink.fromWritable and duplex constructor options." +"@effect/platform-bun/BunStream#stderr": + replacement: "stdio.stderr() from Stdio.Stdio" + note: "Standard error is correctly modeled as a Sink in v4. Explicitly adapt process.stderr with BunStream.fromReadable only to preserve the old unusual read behavior." +"@effect/platform-bun/BunStream#stdin": + replacement: "stdio.stdin from Stdio.Stdio" + note: "Standard input moved to the Stdio service; provide BunStdio.layer. Its stream exposes PlatformError instead of dying." +"@effect/platform-bun/BunStream#stdout": + replacement: "stdio.stdout() from Stdio.Stdio" + note: "Standard output is correctly modeled as a Sink in v4. Explicitly adapt process.stdout with BunStream.fromReadable only to preserve the old unusual read behavior." diff --git a/.context/effect/migration/annotations/effect__platform-bun__BunWorker.yaml b/.context/effect/migration/annotations/effect__platform-bun__BunWorker.yaml new file mode 100644 index 000000000..9757ec982 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__BunWorker.yaml @@ -0,0 +1,6 @@ +"@effect/platform-bun/BunWorker#layerManager": + replacement: "BunWorker.layerPlatform" + note: "WorkerManager was removed. Provide WorkerPlatform directly, or use BunWorker.layer(spawn) when a Worker.Spawner is also required." +"@effect/platform-bun/BunWorker#layerWorker": + replacement: "BunWorker.layerPlatform" + note: "PlatformWorker became Worker.WorkerPlatform; BunWorker.layer(spawn) combines the platform and spawner layers." diff --git a/.context/effect/migration/annotations/effect__platform-bun__index.yaml b/.context/effect/migration/annotations/effect__platform-bun__index.yaml new file mode 100644 index 000000000..ad9ef5d50 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-bun__index.yaml @@ -0,0 +1,3 @@ +"@effect/platform-bun/index": + replacement: "@effect/platform-bun" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-bun package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml new file mode 100644 index 000000000..87da42189 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeCommandExecutor.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node-shared/NodeCommandExecutor#layer": + replacement: "NodeChildProcessSpawner.layer" + note: "CommandExecutor became effect/unstable/process/ChildProcessSpawner; the Node adapter was renamed and still requires FileSystem and Path." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml new file mode 100644 index 000000000..cf2eb77b4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeFileSystem__ParcelWatcher.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node-shared/NodeFileSystem/ParcelWatcher#layer": + replacement: "NodeFileSystem.layer" + note: "The Parcel watcher adapter was removed. NodeFileSystem.layer uses node:fs.watch; provide a custom FileSystem.WatchBackend for specialized behavior." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml new file mode 100644 index 000000000..8215e91d1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeKeyValueStore.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node-shared/NodeKeyValueStore": + replacement: "effect/unstable/persistence/KeyValueStore" + note: "layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via NodeServices.layer or NodeFileSystem.layer with NodePath.layer." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml new file mode 100644 index 000000000..d130beed3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeMultipart.yaml @@ -0,0 +1,6 @@ +"@effect/platform-node-shared/NodeMultipart#fileToReadable": + replacement: "@effect/platform-node/NodeMultipart#fileToReadable" + note: "The Node multipart implementation moved from @effect/platform-node-shared to @effect/platform-node; its behavior remains." +"@effect/platform-node-shared/NodeMultipart#stream": + replacement: "@effect/platform-node/NodeMultipart#stream" + note: "The Node multipart implementation moved from @effect/platform-node-shared to @effect/platform-node; the source and headers call shape remains." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeSink.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeSink.yaml new file mode 100644 index 000000000..2f59e6685 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeSink.yaml @@ -0,0 +1,9 @@ +"@effect/platform-node-shared/NodeSink#stderr": + replacement: "stdio.stderr() from Stdio.Stdio" + note: "Standard error moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer." +"@effect/platform-node-shared/NodeSink#stdin": + replacement: "NodeSink.fromWritable({ evaluate: () => process.stdin, onError: ... })" + note: "There is no Stdio sink because stdin is a readable stream in v4; use a manual adapter only if writing to process.stdin was intentional." +"@effect/platform-node-shared/NodeSink#stdout": + replacement: "stdio.stdout() from Stdio.Stdio" + note: "Standard output moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeSocket.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeSocket.yaml new file mode 100644 index 000000000..9994d65cd --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeSocket.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node-shared/NodeSocket#NetSocket": + replacement: "NodeSocket.NetSocket" + note: "The identifier remains, but the old interface/tag pair is now one Context.Service; use NodeSocket.NetSocket[\"Service\"] for the node:net.Socket value type." diff --git a/.context/effect/migration/annotations/effect__platform-node-shared__NodeStream.yaml b/.context/effect/migration/annotations/effect__platform-node-shared__NodeStream.yaml new file mode 100644 index 000000000..8ee6af441 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node-shared__NodeStream.yaml @@ -0,0 +1,15 @@ +"@effect/platform-node-shared/NodeStream#FromReadableOptions": + replacement: "{ readonly chunkSize?: number; readonly closeOnDone?: boolean }" + note: "The named interface was removed and its fields were inlined into readable constructor options; chunkSize narrowed from SizeInput to number." +"@effect/platform-node-shared/NodeStream#FromWritableOptions": + replacement: "{ readonly endOnDone?: boolean; readonly encoding?: BufferEncoding }" + note: "The named interface was removed and its fields were inlined into NodeSink and duplex constructor options." +"@effect/platform-node-shared/NodeStream#stderr": + replacement: "NodeStream.fromReadable({ evaluate: () => process.stderr, closeOnDone: false }).pipe(Stream.orDie)" + note: "This preserves the unusual v3 read behavior; for normal error output use the stdio.stderr() Sink from effect/Stdio." +"@effect/platform-node-shared/NodeStream#stdin": + replacement: "stdio.stdin from Stdio.Stdio" + note: "Standard input moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer. The service stream exposes PlatformError instead of dying." +"@effect/platform-node-shared/NodeStream#stdout": + replacement: "NodeStream.fromReadable({ evaluate: () => process.stdout, closeOnDone: false }).pipe(Stream.orDie)" + note: "This preserves the unusual v3 read behavior; for normal output use the stdio.stdout() Sink from effect/Stdio." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml new file mode 100644 index 000000000..20a52d962 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeCommandExecutor.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node/NodeCommandExecutor#layer": + replacement: "NodeChildProcessSpawner.layer" + note: "CommandExecutor became ChildProcessSpawner; use the @effect/platform-node/NodeChildProcessSpawner re-export." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeContext.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeContext.yaml new file mode 100644 index 000000000..670e8ac4c --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeContext.yaml @@ -0,0 +1,6 @@ +"@effect/platform-node/NodeContext#layer": + replacement: "NodeServices.layer" + note: "The aggregate was renamed and now provides ChildProcessSpawner, Crypto, FileSystem, Path, Stdio, and Terminal; add NodeWorker separately when needed." +"@effect/platform-node/NodeContext#NodeContext": + replacement: "NodeServices.NodeServices" + note: "Use the renamed service union; it replaces CommandExecutor with ChildProcessSpawner, adds Crypto and Stdio, and omits WorkerManager." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml new file mode 100644 index 000000000..55d29969b --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeFileSystem__ParcelWatcher.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node/NodeFileSystem/ParcelWatcher#layer": + replacement: "NodeFileSystem.layer" + note: "The Parcel watcher adapter was removed. Native node:fs.watch support is built in; FileSystem.WatchBackend is the extension point." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeHttpClient.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeHttpClient.yaml new file mode 100644 index 000000000..a706f503b --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeHttpClient.yaml @@ -0,0 +1,33 @@ +"@effect/platform-node/NodeHttpClient#agentLayer": + replacement: "NodeHttpClient.layerAgent" + note: "Direct rename; it provides the default scoped Node HTTP and HTTPS agents." +"@effect/platform-node/NodeHttpClient#Dispatcher": + replacement: "NodeHttpClient.Dispatcher" + note: "The identifier remains but is now a Context.Service class; use Dispatcher[\"Service\"] for the concrete Undici dispatcher type." +"@effect/platform-node/NodeHttpClient#dispatcherLayer": + replacement: "NodeHttpClient.layerDispatcher" + note: "Direct rename; the layer owns and finalizes a scoped Undici Agent." +"@effect/platform-node/NodeHttpClient#HttpAgent": + replacement: "NodeHttpClient.HttpAgent" + note: "The identifier remains but is now a Context.Service class; use HttpAgent[\"Service\"] for the concrete http/https agent pair." +"@effect/platform-node/NodeHttpClient#HttpAgentTypeId": + replacement: "none" + note: "The public marker was removed; the HttpAgent Context.Service class supplies service identity." +"@effect/platform-node/NodeHttpClient#layer": + replacement: "NodeHttpClient.layerNodeHttp" + note: "Use the renamed node:http/node:https backend layer; choose layerUndici only when intentionally changing backends." +"@effect/platform-node/NodeHttpClient#layerUndiciWithoutDispatcher": + replacement: "NodeHttpClient.layerUndiciNoDispatcher" + note: "Direct rename; the layer still requires NodeHttpClient.Dispatcher." +"@effect/platform-node/NodeHttpClient#layerWithoutAgent": + replacement: "NodeHttpClient.layerNodeHttpNoAgent" + note: "Direct rename; the node:http client layer still requires NodeHttpClient.HttpAgent." +"@effect/platform-node/NodeHttpClient#make": + replacement: "NodeHttpClient.makeNodeHttp" + note: "Direct rename of the node:http/node:https client constructor." +"@effect/platform-node/NodeHttpClient#makeAgentLayer": + replacement: "NodeHttpClient.layerAgentOptions" + note: "Direct rename; it accepts Https.AgentOptions and scopes both agents." +"@effect/platform-node/NodeHttpClient#UndiciRequestOptions": + replacement: "NodeHttpClient.UndiciOptions" + note: "The required Context.Tag became a defaulted Context.Reference>; override it with Effect.provideService." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeHttpServer.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeHttpServer.yaml new file mode 100644 index 000000000..7de86ada6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeHttpServer.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node/NodeHttpServer#layerContext": + replacement: "NodeHttpServer.layerHttpServices" + note: "Direct rename; it provides NodeServices, HttpPlatform, and Etag.Generator, without the removed WorkerManager." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml new file mode 100644 index 000000000..64cc6a8b5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeKeyValueStore.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node/NodeKeyValueStore": + replacement: "effect/unstable/persistence/KeyValueStore" + note: "layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via NodeServices.layer or NodeFileSystem.layer with NodePath.layer." diff --git a/.context/effect/migration/annotations/effect__platform-node__NodeWorker.yaml b/.context/effect/migration/annotations/effect__platform-node__NodeWorker.yaml new file mode 100644 index 000000000..95a7a7fe3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__NodeWorker.yaml @@ -0,0 +1,6 @@ +"@effect/platform-node/NodeWorker#layerManager": + replacement: "NodeWorker.layerPlatform" + note: "WorkerManager was removed. Provide WorkerPlatform directly, or use NodeWorker.layer(spawn) when a Worker.Spawner is also required." +"@effect/platform-node/NodeWorker#layerWorker": + replacement: "NodeWorker.layerPlatform" + note: "PlatformWorker became Worker.WorkerPlatform; NodeWorker.layer(spawn) combines the platform and spawner layers." diff --git a/.context/effect/migration/annotations/effect__platform-node__Undici.yaml b/.context/effect/migration/annotations/effect__platform-node__Undici.yaml new file mode 100644 index 000000000..04d45d0fc --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__Undici.yaml @@ -0,0 +1,117 @@ +"@effect/platform-node/Undici#Agent": + replacement: "undici.Agent" + note: "Import the upstream Agent directly. Undici 8 removes maxRedirections and option-level interceptors, adds maxOrigins, and enables HTTP/2 negotiation unless allowH2 is false." +"@effect/platform-node/Undici#Agent.DispatchOptions": + replacement: "undici.Agent.DispatchOptions" + note: "Import the same Agent namespace type from undici; Undici 8 removes maxRedirections." +"@effect/platform-node/Undici#Agent.Options": + replacement: "undici.Agent.Options" + note: "Import the same Agent namespace type; Undici 8 removes maxRedirections and option-level interceptors, adds maxOrigins, and uses dispatcher.compose for interceptors." +"@effect/platform-node/Undici#buildConnector": + replacement: "undici.buildConnector" + note: "Import the upstream function directly; Undici 8 expands connector options with preferH2, typeOfService, and socketPath handling." +"@effect/platform-node/Undici#buildConnector.BuildOptions": + replacement: "undici.buildConnector.BuildOptions" + note: "Import the same buildConnector namespace type; Undici 8 adds preferH2 and typeOfService." +"@effect/platform-node/Undici#Client": + replacement: "undici.Client" + note: "Import the upstream Client directly; custom handlers must use Undici 8's controller-based v2 handler API." +"@effect/platform-node/Undici#Client.Options": + replacement: "undici.Client.Options" + note: "Import the same Client namespace type; Undici 8 removes maxRedirections and option-level interceptors and adds WebSocket and HTTP/2 options." +"@effect/platform-node/Undici#Client.OptionsInterceptors": + replacement: "undici.Dispatcher.DispatcherComposeInterceptor + dispatcher.compose" + note: "Undici 8 removed option-level interceptor tuples; keep DispatcherComposeInterceptor functions and apply them after construction with dispatcher.compose(...)." +"@effect/platform-node/Undici#default.cacheStores": + replacement: "undici.cacheStores" + note: "Use Undici 8's named cacheStores export instead of reaching through the default aggregate." +"@effect/platform-node/Undici#deleteCookie": + replacement: "undici.deleteCookie" + note: "Import the upstream function directly; its optional attributes use path and domain and no longer include name." +"@effect/platform-node/Undici#DiagnosticsChannel": + replacement: "undici.DiagnosticsChannel" + note: "Import this type-only namespace from undici; subscribe at runtime through node:diagnostics_channel using Undici's channel names." +"@effect/platform-node/Undici#DiagnosticsChannel.ClientConnectErrorMessage": + replacement: "undici.DiagnosticsChannel.ClientConnectErrorMessage" + note: "Import the same type-only namespace member from undici; runtime delivery uses node:diagnostics_channel." +"@effect/platform-node/Undici#DiagnosticsChannel.Error": + replacement: "Error" + note: "Undici 8 removed this unknown alias; diagnostic error fields now use the built-in Error type." +"@effect/platform-node/Undici#DiagnosticsChannel.RequestErrorMessage": + replacement: "undici.DiagnosticsChannel.RequestErrorMessage" + note: "Import the same type-only namespace member; its error field is the built-in Error type in Undici 8." +"@effect/platform-node/Undici#Dispatcher": + replacement: "undici.Dispatcher" + note: "Import the upstream Dispatcher directly; custom dispatchers must adopt Undici 8's controller-based v2 handler API." +"@effect/platform-node/Undici#Dispatcher.ConnectOptions": + replacement: "undici.Dispatcher.ConnectOptions" + note: "Import the same Dispatcher namespace type; Undici 8 removes maxRedirections and redirectionLimitReached." +"@effect/platform-node/Undici#Dispatcher.DispatchHandler": + replacement: "undici.Dispatcher.DispatchHandler" + note: "Use Undici 8's onRequestStart/onResponseStart/onResponseData/onResponseEnd/onResponseError callbacks and controller pause/resume/abort methods." +"@effect/platform-node/Undici#Dispatcher.DispatchOptions": + replacement: "undici.Dispatcher.DispatchOptions" + note: "Import the same namespace type; Undici 8 removes throwOnError, adds typeOfService, and handles redirects through composed interceptors." +"@effect/platform-node/Undici#Dispatcher.RequestOptions": + replacement: "undici.Dispatcher.RequestOptions" + note: "Import the same namespace type; Undici 8 removes maxRedirections and redirectionLimitReached, so compose a redirect interceptor when needed." +"@effect/platform-node/Undici#Dispatcher.UpgradeOptions": + replacement: "undici.Dispatcher.UpgradeOptions" + note: "Import the same namespace type; Undici 8 removes maxRedirections and redirectionLimitReached." +"@effect/platform-node/Undici#errors": + replacement: "undici.errors" + note: "Import the upstream errors object directly; individual classes follow the Undici 8 API." +"@effect/platform-node/Undici#errors.ResponseStatusCodeError": + replacement: "undici.errors.ResponseError" + note: "Undici 8 replaced ResponseStatusCodeError with ResponseError; construct it with message, statusCode, and the headers/body object." +"@effect/platform-node/Undici#H2CClient": + replacement: "undici.H2CClient" + note: "Import the upstream cleartext HTTP/2 client directly; callbacks follow Undici 8's handler API." +"@effect/platform-node/Undici#H2CClient.Options": + replacement: "undici.H2CClient.Options" + note: "Import the same H2CClient namespace type; Undici 8 removes maxRedirections." +"@effect/platform-node/Undici#interceptors": + replacement: "undici.interceptors" + note: "Import the upstream interceptors object and apply returned interceptors with dispatcher.compose(...)." +"@effect/platform-node/Undici#interceptors.DNSInterceptorOpts": + replacement: "undici.interceptors.DNSInterceptorOpts" + note: "Import the same namespace type; Undici 8 lookup receives an origin URL and supports optional DNS storage." +"@effect/platform-node/Undici#interceptors.DNSInterceptorOriginRecords": + replacement: "undici.interceptors.DNSInterceptorOriginRecords" + note: "Import the same namespace type, but adopt Undici 8's shape with IPv4 and IPv6 entries nested under records." +"@effect/platform-node/Undici#interceptors.RedirectInterceptorOpts": + replacement: "undici.interceptors.RedirectInterceptorOpts" + note: "Import the same namespace type; Undici 8 adds throwOnMaxRedirect and redirect header-stripping options." +"@effect/platform-node/Undici#MessageEvent": + replacement: "undici.MessageEvent" + note: "Import Undici's named constructor/type directly to preserve the installed package identity." +"@effect/platform-node/Undici#MessageEventInit": + replacement: "undici.MessageEventInit" + note: "Import the upstream type directly; message ports and source use MessagePort instances in Undici 8." +"@effect/platform-node/Undici#Pool": + replacement: "undici.Pool" + note: "Import the upstream Pool directly and apply interceptors after construction with pool.compose(...)." +"@effect/platform-node/Undici#Pool.Options": + replacement: "undici.Pool.Options" + note: "Import the same Pool namespace type; Undici 8 removes the interceptors option in favor of pool.compose(...)." +"@effect/platform-node/Undici#ProxyAgent": + replacement: "undici.ProxyAgent" + note: "Import the upstream ProxyAgent directly; inherited Agent options and handlers follow Undici 8." +"@effect/platform-node/Undici#ProxyAgent.Options": + replacement: "undici.ProxyAgent.Options" + note: "Import the same ProxyAgent namespace type; Undici 8 types proxy headers as OutgoingHttpHeaders." +"@effect/platform-node/Undici#RedirectHandler": + replacement: "undici.RedirectHandler" + note: "Import the upstream class; Undici 8 removes redirectionLimitReached from the constructor and adds static buildDispatch." +"@effect/platform-node/Undici#Request": + replacement: "undici.Request" + note: "Import Undici's named Request directly; clone is a method in Undici 8." +"@effect/platform-node/Undici#Response": + replacement: "undici.Response" + note: "Import Undici's named Response directly; clone is a method and Response.redirect status is optional in Undici 8." +"@effect/platform-node/Undici#SpecIterable": + replacement: "undici.SpecIterable" + note: "Import the upstream type directly; its iterator returns SpecIterableIterator in Undici 8." +"@effect/platform-node/Undici#SpecIterableIterator": + replacement: "undici.SpecIterableIterator" + note: "Import the upstream type directly; it extends SpecIteratorObject and includes iterator-helper methods in Undici 8." diff --git a/.context/effect/migration/annotations/effect__platform-node__index.yaml b/.context/effect/migration/annotations/effect__platform-node__index.yaml new file mode 100644 index 000000000..d2e0c66e9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform-node__index.yaml @@ -0,0 +1,3 @@ +"@effect/platform-node/index": + replacement: "@effect/platform-node" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-node package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__platform.yaml b/.context/effect/migration/annotations/effect__platform.yaml new file mode 100644 index 000000000..4653d0b89 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform.yaml @@ -0,0 +1,3 @@ +"@effect/platform": + replacement: "none" + note: "The @effect/platform package was folded into the effect package: core services live in effect root modules (e.g. effect/FileSystem, effect/Path) and HTTP in effect/unstable/http; follow the Import Map for each module." diff --git a/.context/effect/migration/annotations/effect__platform__ChannelSchema.yaml b/.context/effect/migration/annotations/effect__platform__ChannelSchema.yaml new file mode 100644 index 000000000..7a48fbcb2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__ChannelSchema.yaml @@ -0,0 +1,9 @@ +"@effect/platform/ChannelSchema#decode": + replacement: "ChannelSchema.decode" + note: "The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model." +"@effect/platform/ChannelSchema#duplex": + replacement: "ChannelSchema.duplex" + note: "The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model." +"@effect/platform/ChannelSchema#encode": + replacement: "ChannelSchema.encode" + note: "The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model." diff --git a/.context/effect/migration/annotations/effect__platform__Command.yaml b/.context/effect/migration/annotations/effect__platform__Command.yaml new file mode 100644 index 000000000..af9a59b08 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Command.yaml @@ -0,0 +1,54 @@ +"@effect/platform/Command#Command": + replacement: "ChildProcess.Command" + note: "Commands moved to effect/unstable/process/ChildProcess and are now directly Effectable to spawn a ChildProcessHandle." +"@effect/platform/Command#Command.Input": + replacement: "ChildProcess.CommandInput" + note: "The standard-input configuration type was flattened out of the Command namespace." +"@effect/platform/Command#Command.Proto": + replacement: "ChildProcess.StandardCommand | ChildProcess.PipedCommand" + note: "The public command prototype was removed; narrow the Command union to its StandardCommand or PipedCommand interfaces." +"@effect/platform/Command#CommandTypeId": + replacement: "none" + note: "The command type-id alias is internal in v4; use ChildProcess.Command or ChildProcess.isCommand instead." +"@effect/platform/Command#env": + replacement: "ChildProcess.setEnv" + note: "Use the renamed command combinator." +"@effect/platform/Command#exitCode": + replacement: "ChildProcessSpawner.ChildProcessSpawner.exitCode" + note: "Obtain the ChildProcessSpawner service and call exitCode, or spawn the Effectable command and read the handle exitCode." +"@effect/platform/Command#feed": + replacement: "ChildProcess.CommandOptions[\"stdin\"]" + note: "The feed combinator was removed; pass a Stream as stdin when constructing the command." +"@effect/platform/Command#flatten": + replacement: "none" + note: "No flatten helper remains; inspect StandardCommand and PipedCommand recursively when command structure is required." +"@effect/platform/Command#lines": + replacement: "ChildProcessSpawner.ChildProcessSpawner.lines" + note: "Output collection moved onto the ChildProcessSpawner service." +"@effect/platform/Command#runInShell": + replacement: "ChildProcess.CommandOptions[\"shell\"]" + note: "Set shell when calling ChildProcess.make; there is no post-construction shell combinator." +"@effect/platform/Command#start": + replacement: "ChildProcessSpawner.ChildProcessSpawner.spawn" + note: "Use the spawner service, or yield the Effectable ChildProcess.Command directly, to obtain a ChildProcessHandle." +"@effect/platform/Command#stderr": + replacement: "ChildProcess.CommandOptions[\"stderr\"]" + note: "Configure stderr in ChildProcess.make options; the standalone combinator was removed." +"@effect/platform/Command#stdin": + replacement: "ChildProcess.CommandOptions[\"stdin\"]" + note: "Configure stdin in ChildProcess.make options; the standalone combinator was removed." +"@effect/platform/Command#stdout": + replacement: "ChildProcess.CommandOptions[\"stdout\"]" + note: "Configure stdout in ChildProcess.make options; the standalone combinator was removed." +"@effect/platform/Command#stream": + replacement: "ChildProcessSpawner.ChildProcessSpawner.spawn + ChildProcessHandle.stdout" + note: "Spawn within a scope and consume the returned handle's stdout stream." +"@effect/platform/Command#streamLines": + replacement: "ChildProcessSpawner.ChildProcessSpawner.streamLines" + note: "Text-line streaming moved onto the ChildProcessSpawner service." +"@effect/platform/Command#string": + replacement: "ChildProcessSpawner.ChildProcessSpawner.string" + note: "Output collection moved onto the ChildProcessSpawner service." +"@effect/platform/Command#workingDirectory": + replacement: "ChildProcess.setCwd" + note: "Use the renamed command combinator." diff --git a/.context/effect/migration/annotations/effect__platform__CommandExecutor.yaml b/.context/effect/migration/annotations/effect__platform__CommandExecutor.yaml new file mode 100644 index 000000000..976d72eb2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__CommandExecutor.yaml @@ -0,0 +1,18 @@ +"@effect/platform/CommandExecutor#CommandExecutor": + replacement: "ChildProcessSpawner.ChildProcessSpawner" + note: "The executor service moved to effect/unstable/process/ChildProcessSpawner and was renamed." +"@effect/platform/CommandExecutor#makeExecutor": + replacement: "ChildProcessSpawner.make" + note: "Use the renamed constructor; it derives output helpers from a spawn implementation." +"@effect/platform/CommandExecutor#Process": + replacement: "ChildProcessSpawner.ChildProcessHandle" + note: "Running-process handles were renamed and moved to ChildProcessSpawner." +"@effect/platform/CommandExecutor#Process.Id": + replacement: "ChildProcessSpawner.ProcessId" + note: "The process-id brand is now exported directly." +"@effect/platform/CommandExecutor#ProcessTypeId": + replacement: "none" + note: "The ChildProcessHandle marker is internal in v4; use the ChildProcessHandle interface." +"@effect/platform/CommandExecutor#TypeId": + replacement: "none" + note: "The Context.Service class replaces the public executor type-id alias." diff --git a/.context/effect/migration/annotations/effect__platform__Cookies.yaml b/.context/effect/migration/annotations/effect__platform__Cookies.yaml new file mode 100644 index 000000000..0cf3cfa23 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Cookies.yaml @@ -0,0 +1,21 @@ +"@effect/platform/Cookies#CookieTypeId": + replacement: "Cookies.isCookie" + note: "The cookie brand is private in v4; use the public refinement instead of reading the type-id symbol." +"@effect/platform/Cookies#ErrorTypeId": + replacement: "Cookies.CookiesError" + note: "The error brand is private in v4; identify the exported error class instead." +"@effect/platform/Cookies#remove": + replacement: "Cookies.remove" + note: "Retained with the same dual name-based removal signature." +"@effect/platform/Cookies#TypeId": + replacement: "Cookies.isCookies" + note: "The collection brand is private in v4; use the public refinement instead." +"@effect/platform/Cookies#unsafeMakeCookie": + replacement: "Cookies.makeCookieUnsafe" + note: "Renamed to put Unsafe last; it still throws on invalid cookie data." +"@effect/platform/Cookies#unsafeSet": + replacement: "Cookies.setUnsafe" + note: "Renamed to put Unsafe last; the dual throwing behavior is retained." +"@effect/platform/Cookies#unsafeSetAll": + replacement: "Cookies.setAllUnsafe" + note: "Renamed to put Unsafe last; the dual all-or-throw behavior is retained." diff --git a/.context/effect/migration/annotations/effect__platform__Effectify.yaml b/.context/effect/migration/annotations/effect__platform__Effectify.yaml new file mode 100644 index 000000000..ca0d6fe34 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Effectify.yaml @@ -0,0 +1,3 @@ +"@effect/platform/Effectify": + replacement: "effect/Effect" + note: "effectify moved into the Effect module as Effect.effectify; the Effectify and EffectifyError type helpers live in the Effect namespace as well." diff --git a/.context/effect/migration/annotations/effect__platform__Error.yaml b/.context/effect/migration/annotations/effect__platform__Error.yaml new file mode 100644 index 000000000..61763d683 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Error.yaml @@ -0,0 +1,18 @@ +"@effect/platform/Error#isPlatformError": + replacement: "value instanceof PlatformError.PlatformError" + note: "PlatformError is a class in v4; use an instanceof check or match its PlatformError tag." +"@effect/platform/Error#Module": + replacement: "string" + note: "The closed module-name Schema was removed; PlatformError reason records accept any module string." +"@effect/platform/Error#PlatformError": + replacement: "PlatformError.PlatformError" + note: "The module moved to effect/PlatformError and PlatformError became a wrapper class around BadArgument or SystemError." +"@effect/platform/Error#SystemErrorReason": + replacement: "PlatformError.SystemErrorTag" + note: "The normalized system-error reason union was renamed." +"@effect/platform/Error#TypeId": + replacement: "none" + note: "The PlatformError runtime marker is internal in v4; use the PlatformError class/tag." +"@effect/platform/Error#TypeIdError": + replacement: "Data.TaggedError or Schema.Error" + note: "The platform-specific error-class factory was removed; define tagged data errors or schema-backed error classes directly." diff --git a/.context/effect/migration/annotations/effect__platform__Etag.yaml b/.context/effect/migration/annotations/effect__platform__Etag.yaml new file mode 100644 index 000000000..4c3f0b6ce --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Etag.yaml @@ -0,0 +1,9 @@ +"@effect/platform/Etag#GeneratorTypeId": + replacement: "Etag.Generator" + note: "The standalone generator brand was removed; Generator is now a Context.Service class." +"@effect/platform/Etag#layer": + replacement: "Etag.layer" + note: "Retained; it still provides the strong metadata-based ETag Generator service." +"@effect/platform/Etag#toString": + replacement: "Etag.toString" + note: "Retained with the same Etag-to-header-string behavior and signature." diff --git a/.context/effect/migration/annotations/effect__platform__FetchHttpClient.yaml b/.context/effect/migration/annotations/effect__platform__FetchHttpClient.yaml new file mode 100644 index 000000000..1186c7d05 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__FetchHttpClient.yaml @@ -0,0 +1,6 @@ +"@effect/platform/FetchHttpClient#Fetch": + replacement: "FetchHttpClient.Fetch" + note: "Retained as a Context.Reference that defaults to globalThis.fetch." +"@effect/platform/FetchHttpClient#layer": + replacement: "FetchHttpClient.layer" + note: "Retained as the HttpClient layer backed by the configured Fetch reference." diff --git a/.context/effect/migration/annotations/effect__platform__FileSystem.yaml b/.context/effect/migration/annotations/effect__platform__FileSystem.yaml new file mode 100644 index 000000000..ddd7a88d3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__FileSystem.yaml @@ -0,0 +1,72 @@ +"@effect/platform/FileSystem#AccessFileOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#CopyOptions": + replacement: "NonNullable[2]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#File.Descriptor": + replacement: "none" + note: "Native file descriptors are no longer part of the portable File interface; use the File methods and keep any platform handle private in custom implementations." +"@effect/platform/FileSystem#FileDescriptor": + replacement: "none" + note: "The descriptor branding constructor was removed with the public fd field; use File operations instead of exposing a native descriptor." +"@effect/platform/FileSystem#FileTypeId": + replacement: "typeof FileSystem.FileTypeId" + note: "The runtime marker remains exported, but the separate type alias was removed." +"@effect/platform/FileSystem#isFile": + replacement: "FileSystem.isFile" + note: "The guard remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#layerNoop": + replacement: "FileSystem.layerNoop" + note: "The helper remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#make": + replacement: "FileSystem.make" + note: "The constructor remains after moving the module to effect/FileSystem; adapt the implementation to the v4 service shape." +"@effect/platform/FileSystem#MakeDirectoryOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#makeNoop": + replacement: "FileSystem.makeNoop" + note: "The helper remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#MakeTempDirectoryOptions": + replacement: "NonNullable[0]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#MakeTempFileOptions": + replacement: "NonNullable[0]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#OpenFileOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#ReadDirectoryOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#RemoveOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#SinkOptions": + replacement: "NonNullable[1]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#Size": + replacement: "FileSystem.Size" + note: "The branded bigint size type remains after moving the module to effect/FileSystem." +"@effect/platform/FileSystem#StreamOptions": + replacement: "NonNullable[1]>" + note: "Stream options are inline; bufferSize was removed while bytesToRead, chunkSize, and offset remain." +"@effect/platform/FileSystem#WatchEventCreate": + replacement: "FileSystem.WatchEvent.Create" + note: "The constructor was removed; construct a tagged object with _tag: \"Create\" and path." +"@effect/platform/FileSystem#WatchEventRemove": + replacement: "FileSystem.WatchEvent.Remove" + note: "The constructor was removed; construct a tagged object with _tag: \"Remove\" and path." +"@effect/platform/FileSystem#WatchEventUpdate": + replacement: "FileSystem.WatchEvent.Update" + note: "The constructor was removed; construct a tagged object with _tag: \"Update\" and path." +"@effect/platform/FileSystem#WatchOptions": + replacement: "FileSystem.WatchOptions" + note: "Retained after moving the module to effect/FileSystem; pass `{ recursive: true }` as the optional second argument to FileSystem.watch." +"@effect/platform/FileSystem#WriteFileOptions": + replacement: "NonNullable[2]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." +"@effect/platform/FileSystem#WriteFileStringOptions": + replacement: "NonNullable[2]>" + note: "Operation option interfaces are inline in the v4 FileSystem service." diff --git a/.context/effect/migration/annotations/effect__platform__Headers.yaml b/.context/effect/migration/annotations/effect__platform__Headers.yaml new file mode 100644 index 000000000..ef95fbf84 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Headers.yaml @@ -0,0 +1,45 @@ +"@effect/platform/Headers#currentRedactedNames": + replacement: "Headers.CurrentRedactedNames" + note: "Renamed and changed from FiberRef to Context.Reference; override it with service provisioning." +"@effect/platform/Headers#empty": + replacement: "Headers.empty" + note: "Retained as the empty immutable Headers value." +"@effect/platform/Headers#fromInput": + replacement: "Headers.fromInput" + note: "Retained with the same optional Input and lowercase normalization behavior." +"@effect/platform/Headers#get": + replacement: "Headers.get" + note: "Retained with the same dual, case-insensitive Option-returning signature." +"@effect/platform/Headers#has": + replacement: "Headers.has" + note: "Retained with the same dual, case-insensitive signature." +"@effect/platform/Headers#Headers": + replacement: "Headers.Headers" + note: "Import Headers from effect/unstable/http; the immutable string-record interface is retained with its v4 TypeId brand." +"@effect/platform/Headers#HeadersTypeId": + replacement: "Headers.TypeId" + note: "The public Headers type-id symbol was renamed from HeadersTypeId to TypeId." +"@effect/platform/Headers#Input": + replacement: "Headers.Input" + note: "Retained with the same record-or-entry-iterable input shape." +"@effect/platform/Headers#merge": + replacement: "Headers.merge" + note: "Retained with the same dual signature; values from the second collection win." +"@effect/platform/Headers#remove": + replacement: "Headers.remove / Headers.removeMany" + note: "Use remove for one name or removeMany for an iterable; RegExp removal requires enumerating matching names." +"@effect/platform/Headers#schema": + replacement: "Headers.HeadersSchema" + note: "The encoded-record and self schemas were consolidated into HeadersSchema." +"@effect/platform/Headers#schemaFromSelf": + replacement: "Headers.HeadersSchema" + note: "The encoded-record and self schemas were consolidated into HeadersSchema." +"@effect/platform/Headers#set": + replacement: "Headers.set" + note: "Retained with the same dual signature and lowercase key normalization." +"@effect/platform/Headers#setAll": + replacement: "Headers.setAll" + note: "Retained with the same dual Input signature; supplied values override existing names." +"@effect/platform/Headers#unsafeFromRecord": + replacement: "Headers.fromRecordUnsafe" + note: "Renamed to put Unsafe last; it still skips name normalization." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApi.yaml b/.context/effect/migration/annotations/effect__platform__HttpApi.yaml new file mode 100644 index 000000000..d761c45e2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApi.yaml @@ -0,0 +1,15 @@ +"@effect/platform/HttpApi#Api": + replacement: "none" + note: "The Context tag carrying the API was removed. Pass the HttpApi value explicitly to builders and clients." +"@effect/platform/HttpApi#HttpApi.Any": + replacement: "effect/unstable/httpapi/HttpApi#Constraint" + note: "Use the erased marker constraint when only HttpApi identity is needed." +"@effect/platform/HttpApi#HttpApi.AnyWithProps": + replacement: "effect/unstable/httpapi/HttpApi#Top" + note: "Use the widened HttpApi type that retains runtime properties." +"@effect/platform/HttpApi#make": + replacement: "effect/unstable/httpapi/HttpApi#make" + note: "The constructor remains, but API-wide error and service parameters were removed; declare errors on endpoints and attach middleware." +"@effect/platform/HttpApi#TypeId": + replacement: "none" + note: "The marker is private in v4; use HttpApi.isHttpApi for runtime narrowing and Constraint or Top for types." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml new file mode 100644 index 000000000..3742d27e5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiBuilder.yaml @@ -0,0 +1,48 @@ +"@effect/platform/HttpApiBuilder#api": + replacement: "effect/unstable/httpapi/HttpApiBuilder#layer" + note: "Use layer(api) and provide the group layers; it registers the completed API with HttpRouter." +"@effect/platform/HttpApiBuilder#buildMiddleware": + replacement: "none" + note: "API-wide middleware assembly was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional middleware." +"@effect/platform/HttpApiBuilder#group": + replacement: "effect/unstable/httpapi/HttpApiBuilder#group" + note: "The group layer remains; names are now identifiers and API/group global error channels are gone." +"@effect/platform/HttpApiBuilder#handler": + replacement: "effect/unstable/httpapi/HttpApiBuilder#endpoint" + note: "Use endpoint for a standalone typed endpoint implementation; inside a group pass callbacks to handlers.handle." +"@effect/platform/HttpApiBuilder#Handlers": + replacement: "effect/unstable/httpapi/HttpApiBuilder#Handlers" + note: "Handlers now tracks an endpoint map and handled identifiers. Prefer Handlers.FromGroup." +"@effect/platform/HttpApiBuilder#Handlers.Error": + replacement: "effect/unstable/httpapi/HttpApiBuilder#Handlers.Error" + note: "The helper remains and extracts the error channel of an effectful group-builder return." +"@effect/platform/HttpApiBuilder#Handlers.Middleware": + replacement: "none" + note: "The handler-internal HttpApp middleware alias was removed. Use HttpRouter.middleware inference or HttpRouter.middleware.Fn." +"@effect/platform/HttpApiBuilder#Handlers.ValidateReturn": + replacement: "effect/unstable/httpapi/HttpApiBuilder#Handlers.ValidateReturn" + note: "The validator remains and now checks the endpoint map against handled identifiers." +"@effect/platform/HttpApiBuilder#HandlersTypeId": + replacement: "none" + note: "The exported symbol was removed; do not inspect or construct the private Handlers marker." +"@effect/platform/HttpApiBuilder#httpApp": + replacement: "effect/unstable/http/HttpRouter#toHttpEffect" + note: "Build the application from the assembled API route layer; HTTP apps are Effects in v4." +"@effect/platform/HttpApiBuilder#middleware": + replacement: "effect/unstable/http/HttpRouter#middleware" + note: "Use router effect middleware and provide its layer to the API route layer; global middleware can target all router routes." +"@effect/platform/HttpApiBuilder#middlewareCors": + replacement: "effect/unstable/http/HttpRouter#cors" + note: "Use the router CORS layer, or provide route-scoped HttpMiddleware.cors through HttpRouter.middleware." +"@effect/platform/HttpApiBuilder#MiddlewareFn": + replacement: "effect/unstable/http/HttpRouter#middleware.Fn" + note: "HTTP apps are Effects in v4; use the router middleware function type or infer it through HttpRouter.middleware." +"@effect/platform/HttpApiBuilder#middlewareOpenApi": + replacement: "effect/unstable/httpapi/HttpApiBuilder#layer" + note: "Set openapiPath in layer(api, options). The additionalPropertiesStrategy option was removed." +"@effect/platform/HttpApiBuilder#Router": + replacement: "effect/unstable/http/HttpRouter#HttpRouter" + note: "The API-specific router tag was removed; API and group layers register with the shared HttpRouter service." +"@effect/platform/HttpApiBuilder#toWebHandler": + replacement: "effect/unstable/http/HttpRouter#toWebHandler" + note: "Pass the assembled API route layer to HttpRouter.toWebHandler; the handler and dispose lifecycle is retained." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiClient.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiClient.yaml new file mode 100644 index 000000000..92cf124c3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiClient.yaml @@ -0,0 +1,12 @@ +"@effect/platform/HttpApiClient#Client.Method": + replacement: "effect/unstable/httpapi/HttpApiClient#Client.Method" + note: "The type remains without GroupError. Requests use params/query and responseMode instead of path/urlParams and withResponse." +"@effect/platform/HttpApiClient#endpoint": + replacement: "effect/unstable/httpapi/HttpApiClient#endpoint" + note: "The endpoint client remains, selected by group and endpoint identifiers and using v4 request and responseMode fields." +"@effect/platform/HttpApiClient#make": + replacement: "effect/unstable/httpapi/HttpApiClient#make" + note: "The generated client remains; errors and services are now derived per endpoint and middleware." +"@effect/platform/HttpApiClient#makeWith": + replacement: "effect/unstable/httpapi/HttpApiClient#makeWith" + note: "The supplied-HttpClient constructor remains and now requires endpoint client-middleware services." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiEndpoint.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiEndpoint.yaml new file mode 100644 index 000000000..5c8f3b0be --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiEndpoint.yaml @@ -0,0 +1,105 @@ +"@effect/platform/HttpApiEndpoint#get": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#get" + note: "Use get(identifier, path, options?); tagged templates and fluent schema setters were removed." +"@effect/platform/HttpApiEndpoint#head": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#head" + note: "Use head(identifier, path, options?); tagged templates and fluent schema setters were removed." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#HttpApiEndpoint" + note: "The model remains, but its generics now carry path literals, schemas, middleware, and middleware services." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.AddContext": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#AddMiddleware" + note: "Use AddMiddleware to add a middleware identifier and compute its service transformation." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.AddError": + replacement: "none" + note: "Declare error schemas in the endpoint constructor options; the type helper and fluent addError method were removed." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.Constructor": + replacement: "none" + note: "The tagged-template constructor type was removed; use HttpApiEndpoint.make(method)(identifier, path, options?)." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.Context": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ServerServices" + note: "Use ServerServices for handler requirements; middleware IDs and extra requirements have separate extractors." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ContextWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ServerServicesWithIdentifier" + note: "Name became Identifier; combine with middleware extractors when the complete handler requirement union is needed." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.Error": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Errors" + note: "Use Errors for the decoded endpoint and middleware error union; v4 Error extracts the schema." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ErrorContext": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ErrorServicesEncode / ErrorServicesDecode" + note: "The single schema context split into server encoding and client decoding services." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ErrorContextWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ErrorServicesEncode / ErrorServicesDecode" + note: "Select the endpoint with WithIdentifier, then apply the encode or decode service extractor." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ErrorWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ErrorsWithIdentifier" + note: "Renamed for identifier and returns the decoded endpoint plus middleware error union." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ExcludeName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ExcludeIdentifier" + note: "Direct rename from name to identifier." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ExtractPath": + replacement: "none" + note: "Tagged-template path extraction was removed. Put a params schema or field record in constructor option params." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.HandlerRawWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#HandlerRawWithIdentifier" + note: "Direct rename; raw request fields are now params and query." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.HandlerWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#HandlerWithIdentifier" + note: "Direct rename from name to identifier." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.OptionalTypePropertySignature": + replacement: "none" + note: "Removed with the tagged-template path implementation." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.PathEntries": + replacement: "none" + note: "Removed with tagged-template path extraction; declare endpoint params explicitly." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.PathParsed": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Params" + note: "Path data became params; Params extracts the schema, so use Params[\"Type\"] for decoded data." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.Payload": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Payload" + note: "The name remains but now extracts the schema; use Payload[\"Type\"] for buffered decoded data." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.Success": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#SuccessWithIdentifier" + note: "Use SuccessWithIdentifier for the decoded, stream-aware result; v4 Success extracts the schema." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.SuccessWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#SuccessWithIdentifier" + note: "Direct rename from name to identifier; the result remains decoded and stream-aware." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.UrlParams": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Query" + note: "urlParams became query; Query extracts the schema, so use Query[\"Type\"] for decoded data." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ValidateHeaders": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#HeadersConstraint" + note: "Validation moved from an intersection helper to a constructor generic constraint." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ValidateParams": + replacement: "none" + note: "Tagged-template interpolation validation was removed; params are declared explicitly in options.params." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ValidatePath": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#ParamsConstraint" + note: "path became params and validation is now a constructor constraint." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ValidatePayload": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#PayloadConstraint" + note: "Payload validation is now a method-sensitive constructor constraint." +"@effect/platform/HttpApiEndpoint#HttpApiEndpoint.ValidateUrlParams": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#QueryConstraint" + note: "urlParams became query and validation is now a constructor constraint." +"@effect/platform/HttpApiEndpoint#make": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#make" + note: "The factory remains but now requires identifier, path, and options and applies codecs unless disabled." +"@effect/platform/HttpApiEndpoint#options": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#options" + note: "Same HTTP method constructor with the new identifier, path, and options signature." +"@effect/platform/HttpApiEndpoint#patch": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#patch" + note: "Same HTTP method constructor with the new identifier, path, and options signature." +"@effect/platform/HttpApiEndpoint#PathSegment": + replacement: "effect/unstable/http/HttpRouter#PathInput" + note: "Path input moved to the shared router and is generalized to slash-prefixed paths or wildcard." +"@effect/platform/HttpApiEndpoint#post": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#post" + note: "Same HTTP method constructor with the new identifier, path, and options signature." +"@effect/platform/HttpApiEndpoint#put": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#put" + note: "Same HTTP method constructor with the new identifier, path, and options signature." +"@effect/platform/HttpApiEndpoint#TypeId": + replacement: "none" + note: "The endpoint type ID is private; use HttpApiEndpoint.isHttpApiEndpoint for runtime narrowing." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiError.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiError.yaml new file mode 100644 index 000000000..2d3532777 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiError.yaml @@ -0,0 +1,12 @@ +"@effect/platform/HttpApiError#Forbidden": + replacement: "effect/unstable/httpapi/HttpApiError#ForbiddenNoContent" + note: "Use ForbiddenNoContent to preserve the empty 403 wire schema; Forbidden now has a JSON-tagged body." +"@effect/platform/HttpApiError#HttpApiDecodeError": + replacement: "effect/unstable/httpapi/HttpApiError#HttpApiSchemaError" + note: "Validation now stores kind and a SchemaError cause and is a defect unless transformed by schema-error middleware." +"@effect/platform/HttpApiError#Issue": + replacement: "effect/SchemaIssue#Issue" + note: "Structured failures now live at HttpApiSchemaError.cause.issue; format them explicitly when a flat external list is needed." +"@effect/platform/HttpApiError#TypeId": + replacement: "effect/unstable/httpapi/HttpApiError#HttpApiSchemaErrorTypeId" + note: "The old module symbol is gone; prefer HttpApiSchemaError.is for runtime narrowing." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiGroup.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiGroup.yaml new file mode 100644 index 000000000..5297e522d --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiGroup.yaml @@ -0,0 +1,54 @@ +"@effect/platform/HttpApiGroup#ApiGroup": + replacement: "effect/unstable/httpapi/HttpApiGroup#Service" + note: "Renamed; the service field and type parameter are now identifier rather than name." +"@effect/platform/HttpApiGroup#HttpApiGroup.AddContext": + replacement: "none" + note: "Groups no longer carry arbitrary context. Use AddMiddleware for middleware service transformations." +"@effect/platform/HttpApiGroup#HttpApiGroup.Any": + replacement: "effect/unstable/httpapi/HttpApiGroup#Constraint" + note: "Renamed widened structural constraint." +"@effect/platform/HttpApiGroup#HttpApiGroup.AnyWithProps": + replacement: "effect/unstable/httpapi/HttpApiGroup#Top" + note: "Renamed widened runtime-property type." +"@effect/platform/HttpApiGroup#HttpApiGroup.ClientContext": + replacement: "effect/unstable/httpapi/HttpApiGroup#ClientServices / ErrorServicesDecode / MiddlewareClient" + note: "Client schema services and required client middleware are separate extractors in v4." +"@effect/platform/HttpApiGroup#HttpApiGroup.Context": + replacement: "none" + note: "Group error and context generics were removed; derive server requirements from the group's endpoints." +"@effect/platform/HttpApiGroup#HttpApiGroup.ContextWithName": + replacement: "none" + note: "Select with WithIdentifier and derive endpoint server requirements; groups no longer have a context generic." +"@effect/platform/HttpApiGroup#HttpApiGroup.EndpointsWithName": + replacement: "effect/unstable/httpapi/HttpApiGroup#EndpointsWithIdentifier" + note: "Direct rename from name to identifier." +"@effect/platform/HttpApiGroup#HttpApiGroup.Error": + replacement: "none" + note: "Group-level errors were removed. Declare shared errors on each endpoint or through middleware." +"@effect/platform/HttpApiGroup#HttpApiGroup.ErrorContext": + replacement: "effect/unstable/httpapi/HttpApiGroup#ErrorServicesEncode / ErrorServicesDecode" + note: "The closest endpoint-error aggregate splits server encoding from client decoding services." +"@effect/platform/HttpApiGroup#HttpApiGroup.ErrorWithName": + replacement: "none" + note: "Group-level errors were removed; select with WithIdentifier and inspect Errors over the selected endpoints." +"@effect/platform/HttpApiGroup#HttpApiGroup.Middleware": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Middleware" + note: "Middleware is attached to the endpoints present when group.middleware is called; extract it from group endpoints." +"@effect/platform/HttpApiGroup#HttpApiGroup.MiddlewareWithName": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#Middleware" + note: "Select the group with WithIdentifier, get its endpoints, then apply the endpoint Middleware extractor." +"@effect/platform/HttpApiGroup#HttpApiGroup.Provides": + replacement: "effect/unstable/httpapi/HttpApiGroup#MiddlewareProvides" + note: "Renamed; derives provided services from endpoint middleware." +"@effect/platform/HttpApiGroup#HttpApiGroup.ToService": + replacement: "effect/unstable/httpapi/HttpApiGroup#ToService" + note: "Same role and now produces Service." +"@effect/platform/HttpApiGroup#HttpApiGroup.WithName": + replacement: "effect/unstable/httpapi/HttpApiGroup#WithIdentifier" + note: "Direct rename from name to identifier." +"@effect/platform/HttpApiGroup#make": + replacement: "effect/unstable/httpapi/HttpApiGroup#make" + note: "The constructor remains; group error and context generics are gone and add is variadic." +"@effect/platform/HttpApiGroup#TypeId": + replacement: "none" + note: "The group type ID is private; use HttpApiGroup.isHttpApiGroup for runtime narrowing." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiMiddleware.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiMiddleware.yaml new file mode 100644 index 000000000..d0389a554 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiMiddleware.yaml @@ -0,0 +1,60 @@ +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#HttpApiMiddleware" + note: "The model remains but now wraps the response effect and carries provided services, an error schema, and required services." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.Any": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#AnyService" + note: "Renamed widened middleware service-key shape." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.AnyId": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#AnyId" + note: "Same name; metadata now includes provided and required services, error schema, client error, and client requirement." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.Error": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Error" + note: "Same name and now derives the decoded type from the configured error schema." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.ErrorContext": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#ErrorServicesEncode / ErrorServicesDecode" + note: "The single schema context split into server encoding and client decoding services." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.Only": + replacement: "Extract" + note: "The helper was removed because middleware IDs are explicit; use Extract when the direct filter is still needed." +"@effect/platform/HttpApiMiddleware#HttpApiMiddleware.Provides": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Provides" + note: "Same name and reads the expanded v4 middleware ID metadata." +"@effect/platform/HttpApiMiddleware#SecurityTypeId": + replacement: "none" + note: "The marker is private; use HttpApiMiddleware.isSecurity." +"@effect/platform/HttpApiMiddleware#Tag": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Service" + note: "Renamed and redesigned; use error, requires, provides, clientError, and requiredForClient configuration." +"@effect/platform/HttpApiMiddleware#TagClass": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#ServiceClass" + note: "Renamed class type with the new two-stage type configuration and wrapping service shape." +"@effect/platform/HttpApiMiddleware#TagClass.BaseSecurity": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#ServiceClass" + note: "Security is conditional metadata on ServiceClass; there is no separate public base interface." +"@effect/platform/HttpApiMiddleware#TagClass.Failure": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Error" + note: "failure terminology became error; apply the extractor to the middleware ID." +"@effect/platform/HttpApiMiddleware#TagClass.FailureContext": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#ErrorServicesEncode / ErrorServicesDecode" + note: "Failure schema services split by server encoding and client decoding direction." +"@effect/platform/HttpApiMiddleware#TagClass.FailureSchema": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#ErrorSchema" + note: "Renamed and applied to the middleware ID rather than constructor options." +"@effect/platform/HttpApiMiddleware#TagClass.FailureService": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Error" + note: "Use the decoded error extractor; optional middleware fallback was removed." +"@effect/platform/HttpApiMiddleware#TagClass.Optional": + replacement: "none" + note: "Optional declaration and fallback-on-failure behavior were removed; model fallback in the wrapping middleware." +"@effect/platform/HttpApiMiddleware#TagClass.Provides": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#Provides" + note: "Moved to the module level and applied to the middleware ID." +"@effect/platform/HttpApiMiddleware#TagClassAny": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#AnyService" + note: "Renamed widened service-key type." +"@effect/platform/HttpApiMiddleware#TagClassSecurityAny": + replacement: "effect/unstable/httpapi/HttpApiMiddleware#AnyServiceSecurity" + note: "Renamed widened security service-key type." +"@effect/platform/HttpApiMiddleware#TypeId": + replacement: "none" + note: "The marker is private; use public guards and type extractors." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiScalar.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiScalar.yaml new file mode 100644 index 000000000..a6853fcaf --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiScalar.yaml @@ -0,0 +1,9 @@ +"@effect/platform/HttpApiScalar#layer": + replacement: "effect/unstable/httpapi/HttpApiScalar#layer" + note: "Pass the HttpApi as the first argument; the layer now contributes directly to HttpRouter." +"@effect/platform/HttpApiScalar#layerHttpLayerRouter": + replacement: "effect/unstable/httpapi/HttpApiScalar#layer" + note: "The duplicate was removed. Pass options.api as the first layer argument and the remaining Scalar options second." +"@effect/platform/HttpApiScalar#layerHttpLayerRouterCdn": + replacement: "effect/unstable/httpapi/HttpApiScalar#layerCdn" + note: "Use the explicit-api CDN layer with path, version, and Scalar options." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiSchema.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiSchema.yaml new file mode 100644 index 000000000..3f0cc5009 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiSchema.yaml @@ -0,0 +1,99 @@ +"@effect/platform/HttpApiSchema#AnnotationEmptyDecodeable": + replacement: "effect/unstable/httpapi/HttpApiSchema#asNoContent" + note: "The public marker was removed; represent no-content decoding structurally with asNoContent({ decode })." +"@effect/platform/HttpApiSchema#AnnotationEncoding": + replacement: "effect/unstable/httpapi/HttpApiSchema#asJson / asFormUrlEncoded / asText / asUint8Array" + note: "The key is internal; select encoding with a public combinator." +"@effect/platform/HttpApiSchema#AnnotationMultipart": + replacement: "effect/unstable/httpapi/HttpApiSchema#asMultipart" + note: "The symbol annotation became a brand plus internal encoding metadata; apply the schema combinator." +"@effect/platform/HttpApiSchema#AnnotationMultipartStream": + replacement: "effect/unstable/httpapi/HttpApiSchema#asMultipartStream" + note: "The symbol annotation became a brand plus internal encoding metadata; apply the schema combinator." +"@effect/platform/HttpApiSchema#AnnotationParam": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#params" + note: "Path names now live in the router path and schemas in endpoint option params, not schema annotations." +"@effect/platform/HttpApiSchema#annotations": + replacement: "effect/Schema#annotate" + note: "Schema annotations became annotate; set httpApiStatus directly or prefer HttpApiSchema.status for status only." +"@effect/platform/HttpApiSchema#AnnotationStatus": + replacement: "effect/unstable/httpapi/HttpApiSchema#status" + note: "The public symbol was removed; apply status(code), which uses the httpApiStatus schema annotation." +"@effect/platform/HttpApiSchema#asEmpty": + replacement: "effect/unstable/httpapi/HttpApiSchema#asNoContent" + note: "Use schema.pipe(asNoContent({ decode }), status(code)); status is now a separate combinator." +"@effect/platform/HttpApiSchema#deunionize": + replacement: "none" + note: "Pass schema arrays to endpoint success, error, and body alternatives so each member retains status and content type." +"@effect/platform/HttpApiSchema#Empty": + replacement: "effect/unstable/httpapi/HttpApiSchema#Empty" + note: "The API remains and returns Schema.Void annotated with the supplied status." +"@effect/platform/HttpApiSchema#EmptyError": + replacement: "effect/Schema#Error" + note: "Define a normal schema error with httpApiStatus, then derive its no-content wire schema with asNoContent." +"@effect/platform/HttpApiSchema#EmptyErrorClass": + replacement: "effect/Schema#Error" + note: "The class and no-content codec are separate in v4; combine Schema.Error with HttpApiSchema.asNoContent." +"@effect/platform/HttpApiSchema#EmptyErrorUnify": + replacement: "none" + note: "Removed with EmptyError; Schema.Error instances already support yieldable-error behavior." +"@effect/platform/HttpApiSchema#EmptyErrorUnifyIgnore": + replacement: "none" + note: "Removed with EmptyError; do not recreate the old Unify marker." +"@effect/platform/HttpApiSchema#Encoding": + replacement: "effect/unstable/httpapi/HttpApiSchema#Encoding" + note: "The name remains but is now a discriminated PayloadEncoding or ResponseEncoding union; prefer public as* combinators." +"@effect/platform/HttpApiSchema#extractAnnotations": + replacement: "none" + note: "The internal symbol-copy helper was removed; HTTP metadata is schema-native and resolved through AST traversal." +"@effect/platform/HttpApiSchema#getEmptyDecodeable": + replacement: "effect/unstable/httpapi/HttpApiSchema#isNoContent" + note: "Use isNoContent only to test bodylessness; decodeability is structural and has no exact query replacement." +"@effect/platform/HttpApiSchema#getEncoding": + replacement: "effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding / getResponseEncoding" + note: "Encoding lookup split by direction; application code should normally use public as* combinators." +"@effect/platform/HttpApiSchema#getMultipart": + replacement: "effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding" + note: "Narrow the payload encoding to Multipart with buffered mode; multipart limits are on the encoding value." +"@effect/platform/HttpApiSchema#getMultipartStream": + replacement: "effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding" + note: "Narrow the payload encoding to Multipart with stream mode; multipart limits are on the encoding value." +"@effect/platform/HttpApiSchema#getParam": + replacement: "none" + note: "Param identity moved out of schema metadata; read endpoint.path and endpoint.params." +"@effect/platform/HttpApiSchema#getStatus": + replacement: "effect/SchemaAST#resolveAt" + note: "Resolve the httpApiStatus annotation directly, or prefer getStatusSuccess and getStatusError for response logic." +"@effect/platform/HttpApiSchema#getStatusError": + replacement: "effect/unstable/httpapi/HttpApiSchema#getStatusError" + note: "The helper remains but accepts an AST and defaults to 500." +"@effect/platform/HttpApiSchema#getStatusErrorAST": + replacement: "effect/unstable/httpapi/HttpApiSchema#getStatusError" + note: "The AST suffix collapsed into the sole helper, which defaults to 500." +"@effect/platform/HttpApiSchema#getStatusSuccess": + replacement: "effect/unstable/httpapi/HttpApiSchema#getStatusSuccess" + note: "The helper remains but accepts an AST; bare Schema.Void now defaults to 200, so use Empty(204) for 204." +"@effect/platform/HttpApiSchema#getStatusSuccessAST": + replacement: "effect/unstable/httpapi/HttpApiSchema#getStatusSuccess" + note: "The AST suffix collapsed into the sole helper; bare Schema.Void no longer implies 204." +"@effect/platform/HttpApiSchema#Multipart": + replacement: "effect/unstable/httpapi/HttpApiSchema#asMultipart" + note: "The type and constructor became a curried schema combinator: schema.pipe(asMultipart(options))." +"@effect/platform/HttpApiSchema#MultipartStream": + replacement: "effect/unstable/httpapi/HttpApiSchema#asMultipartStream" + note: "The type and constructor became a curried schema combinator." +"@effect/platform/HttpApiSchema#param": + replacement: "effect/unstable/httpapi/HttpApiEndpoint#params" + note: "Use a literal /:name path and the matching field in endpoint constructor option params." +"@effect/platform/HttpApiSchema#Text": + replacement: "effect/unstable/httpapi/HttpApiSchema#asText" + note: "Apply the encoding combinator to Schema.String instead of using a dedicated constructor." +"@effect/platform/HttpApiSchema#Uint8Array": + replacement: "effect/unstable/httpapi/HttpApiSchema#asUint8Array" + note: "Apply the encoding combinator to Schema.Uint8Array instead of using a dedicated constructor." +"@effect/platform/HttpApiSchema#UnionUnify": + replacement: "effect/Schema#Union" + note: "Use Schema.Union([self, that]); for endpoint alternatives, pass the schema array directly to preserve metadata." +"@effect/platform/HttpApiSchema#withEncoding": + replacement: "effect/unstable/httpapi/HttpApiSchema#asJson / asFormUrlEncoded / asUint8Array / asText" + note: "Replace the generic kind with the matching public curried encoding combinator." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiSecurity.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiSecurity.yaml new file mode 100644 index 000000000..f29b17c5a --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiSecurity.yaml @@ -0,0 +1,15 @@ +"@effect/platform/HttpApiSecurity#annotate": + replacement: "effect/unstable/httpapi/HttpApiSecurity#annotate" + note: "The combinator remains; its key is now the v4 Context.Key abstraction." +"@effect/platform/HttpApiSecurity#annotateContext": + replacement: "effect/unstable/httpapi/HttpApiSecurity#annotateMerge" + note: "Renamed; it still merges a Context into existing OpenAPI annotations." +"@effect/platform/HttpApiSecurity#Bearer": + replacement: "effect/unstable/httpapi/HttpApiSecurity#Http" + note: "Bearer was generalized to Http with scheme Bearer; the value-level bearer singleton remains." +"@effect/platform/HttpApiSecurity#HttpApiSecurity.Type": + replacement: "effect/unstable/httpapi/HttpApiSecurity#HttpApiSecurity.Type" + note: "Unchanged after the module move; still extracts the credential type." +"@effect/platform/HttpApiSecurity#TypeId": + replacement: "none" + note: "The marker is private; use the public union or specific Http, ApiKey, and Basic types." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApiSwagger.yaml b/.context/effect/migration/annotations/effect__platform__HttpApiSwagger.yaml new file mode 100644 index 000000000..170c2c414 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApiSwagger.yaml @@ -0,0 +1,6 @@ +"@effect/platform/HttpApiSwagger#layer": + replacement: "effect/unstable/httpapi/HttpApiSwagger#layer" + note: "Pass the HttpApi as the first argument; the layer now contributes directly to HttpRouter." +"@effect/platform/HttpApiSwagger#layerHttpLayerRouter": + replacement: "effect/unstable/httpapi/HttpApiSwagger#layer" + note: "The duplicate was removed. Pass options.api first and the path option second." diff --git a/.context/effect/migration/annotations/effect__platform__HttpApp.yaml b/.context/effect/migration/annotations/effect__platform__HttpApp.yaml new file mode 100644 index 000000000..774f5823c --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpApp.yaml @@ -0,0 +1,21 @@ +"@effect/platform/HttpApp#currentPreResponseHandlers": + replacement: "HttpEffect.appendPreResponseHandler / HttpEffect.withPreResponseHandler" + note: "The FiberRef was removed; register request-local handlers through HttpEffect." +"@effect/platform/HttpApp#Default": + replacement: "Effect.Effect" + note: "The alias was removed; v4 HTTP applications are ordinary response-producing Effects." +"@effect/platform/HttpApp#ejectDefaultScopeClose": + replacement: "HttpEffect.scopeDisableClose" + note: "Renamed; it disables automatic request-scope closure, leaving closure to the caller." +"@effect/platform/HttpApp#HttpApp": + replacement: "Effect.Effect" + note: "The alias was removed; use the underlying Effect type and HttpEffect boundary combinators." +"@effect/platform/HttpApp#toWebHandler": + replacement: "HttpEffect.toWebHandler" + note: "Moved to HttpEffect for converting an HTTP effect to a Web handler." +"@effect/platform/HttpApp#toWebHandlerRuntime": + replacement: "HttpEffect.toWebHandlerWith(context)" + note: "Runtime was removed in v4; supply a Context with toWebHandlerWith instead." +"@effect/platform/HttpApp#unsafeEjectStreamScope": + replacement: "HttpEffect.scopeTransferToStream" + note: "Renamed; it transfers request-scope closure to a streaming response." diff --git a/.context/effect/migration/annotations/effect__platform__HttpBody.yaml b/.context/effect/migration/annotations/effect__platform__HttpBody.yaml new file mode 100644 index 000000000..4e8e46f1b --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpBody.yaml @@ -0,0 +1,57 @@ +"@effect/platform/HttpBody#empty": + replacement: "HttpBody.empty" + note: "Retained as the singleton Empty body." +"@effect/platform/HttpBody#Empty": + replacement: "HttpBody.Empty" + note: "Retained with the same tag, but v4 exports a class rather than an interface." +"@effect/platform/HttpBody#ErrorReason": + replacement: "HttpBody.ErrorReason" + note: "Retained but reshaped; original causes now live on HttpBodyError.cause." +"@effect/platform/HttpBody#ErrorTypeId": + replacement: "HttpBody.HttpBodyError" + note: "The error type-id is private in v4; identify the exported error class instead." +"@effect/platform/HttpBody#file": + replacement: "HttpBody.file" + note: "Retained; bufferSize was replaced by chunkSize and the other file options remain." +"@effect/platform/HttpBody#fileInfo": + replacement: "HttpBody.fileFromInfo" + note: "Renamed; it still uses supplied File.Info for content length and requires FileSystem." +"@effect/platform/HttpBody#formData": + replacement: "HttpBody.formData" + note: "Retained with the same Web FormData input." +"@effect/platform/HttpBody#HttpBodyError": + replacement: "HttpBody.HttpBodyError" + note: "Changed from a factory/interface to a class constructed with reason and optional cause." +"@effect/platform/HttpBody#json": + replacement: "HttpBody.json" + note: "Retained as the safe Effect-returning JSON serializer." +"@effect/platform/HttpBody#raw": + replacement: "HttpBody.raw" + note: "Retained with optional contentType and contentLength metadata." +"@effect/platform/HttpBody#Raw": + replacement: "HttpBody.Raw" + note: "Retained with the same tag and payload, but v4 exports a class." +"@effect/platform/HttpBody#stream": + replacement: "HttpBody.stream" + note: "Retained with the same byte stream and optional content metadata." +"@effect/platform/HttpBody#Stream": + replacement: "HttpBody.Stream" + note: "Retained with the same tag and byte stream, but v4 exports a class." +"@effect/platform/HttpBody#text": + replacement: "HttpBody.text" + note: "Retained; it UTF-8 encodes and defaults to text/plain." +"@effect/platform/HttpBody#TypeId": + replacement: "HttpBody.isHttpBody" + note: "The body brand is private in v4; use the public refinement instead." +"@effect/platform/HttpBody#uint8Array": + replacement: "HttpBody.uint8Array" + note: "Retained with the same bytes and optional content type." +"@effect/platform/HttpBody#Uint8Array": + replacement: "HttpBody.Uint8Array" + note: "Retained with the same fields and tag, but v4 exports a class." +"@effect/platform/HttpBody#unsafeJson": + replacement: "HttpBody.jsonUnsafe" + note: "Renamed to put Unsafe last; serialization failures still throw." +"@effect/platform/HttpBody#urlParams": + replacement: "HttpBody.urlParams" + note: "Retained and widened to accept UrlParams.Input." diff --git a/.context/effect/migration/annotations/effect__platform__HttpClient.yaml b/.context/effect/migration/annotations/effect__platform__HttpClient.yaml new file mode 100644 index 000000000..6b7033dbb --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpClient.yaml @@ -0,0 +1,51 @@ +"@effect/platform/HttpClient#catchAll": + replacement: "HttpClient.catch" + note: "Renamed to catch; the recovery callback still returns a response effect." +"@effect/platform/HttpClient#catchTag": + replacement: "HttpClient.catchTag" + note: "Retained and widened to accept one or more error tags." +"@effect/platform/HttpClient#currentTracerDisabledWhen": + replacement: "HttpClient.TracerDisabledWhen" + note: "Renamed and changed from FiberRef to Context.Reference." +"@effect/platform/HttpClient#currentTracerPropagation": + replacement: "HttpClient.TracerPropagationEnabled" + note: "Renamed and changed from FiberRef to Context.Reference." +"@effect/platform/HttpClient#filterOrFail": + replacement: "HttpClient.filterOrFail" + note: "Retained; v4 also provides refinement overloads." +"@effect/platform/HttpClient#filterStatus": + replacement: "HttpClient.filterStatus" + note: "Retained; rejection now fails with the HttpClientError wrapper." +"@effect/platform/HttpClient#filterStatusOk": + replacement: "HttpClient.filterStatusOk" + note: "Retained; non-2xx responses now fail with the HttpClientError wrapper." +"@effect/platform/HttpClient#make": + replacement: "HttpClient.make" + note: "Retained; the runner receives Fiber.Fiber and failures use the v4 error wrapper." +"@effect/platform/HttpClient#makeWith": + replacement: "HttpClient.makeWith" + note: "Retained with the preprocess and postprocess constructor pattern." +"@effect/platform/HttpClient#retry": + replacement: "HttpClient.retry" + note: "Retained; the Schedule error channel is included in the resulting client error type." +"@effect/platform/HttpClient#SpanNameGenerator": + replacement: "HttpClient.SpanNameGenerator" + note: "The interface became a Context.Reference containing the generator function." +"@effect/platform/HttpClient#TypeId": + replacement: "HttpClient.isHttpClient" + note: "The brand key is private in v4; use the public runtime refinement." +"@effect/platform/HttpClient#withSpanNameGenerator": + replacement: "HttpClient.transformResponse(Effect.provideService(HttpClient.SpanNameGenerator, f))" + note: "The convenience combinator was removed; provide the reference around response effects." +"@effect/platform/HttpClient#withTracerDisabledWhen": + replacement: "HttpClient.transformResponse(Effect.provideService(HttpClient.TracerDisabledWhen, predicate))" + note: "The convenience combinator was removed; provide the reference around response effects." +"@effect/platform/HttpClient#withTracerPropagation": + replacement: "HttpClient.transformResponse(Effect.provideService(HttpClient.TracerPropagationEnabled, enabled))" + note: "Provide the renamed propagation reference around response effects." +"@effect/platform/HttpClient#tap": + replacement: "effect/unstable/http/HttpClient#tap" + note: "Moved to the v4 HTTP module with the same response-effect callback and client error/service widening." +"@effect/platform/HttpClient#transform": + replacement: "effect/unstable/http/HttpClient#transform" + note: "Moved to the v4 HTTP module with the same request-aware transformation shape." diff --git a/.context/effect/migration/annotations/effect__platform__HttpClientError.yaml b/.context/effect/migration/annotations/effect__platform__HttpClientError.yaml new file mode 100644 index 000000000..37baa2d15 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpClientError.yaml @@ -0,0 +1,9 @@ +"@effect/platform/HttpClientError#HttpClientError": + replacement: "HttpClientError.HttpClientError" + note: "Changed from a union to a tagged wrapper class containing a concrete failure in reason." +"@effect/platform/HttpClientError#RequestError": + replacement: "HttpClientError.RequestError" + note: "Now a type-only reason union; construct a concrete reason and wrap it in HttpClientError." +"@effect/platform/HttpClientError#TypeId": + replacement: "HttpClientError.isHttpClientError" + note: "The brand key is private in v4; use the public runtime refinement." diff --git a/.context/effect/migration/annotations/effect__platform__HttpClientRequest.yaml b/.context/effect/migration/annotations/effect__platform__HttpClientRequest.yaml new file mode 100644 index 000000000..e6eba3580 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpClientRequest.yaml @@ -0,0 +1,48 @@ +"@effect/platform/HttpClientRequest#bodyFileWeb": + replacement: "HttpClientRequest.setBody + HttpBody.stream + Stream.fromReadableStream" + note: "No one-call replacement remains; stream file.stream() and pass file.type and file.size to HttpBody.stream." +"@effect/platform/HttpClientRequest#bodyUnsafeJson": + replacement: "HttpClientRequest.bodyJsonUnsafe" + note: "Renamed to put Unsafe last; serialization remains synchronous and throwing." +"@effect/platform/HttpClientRequest#get": + replacement: "HttpClientRequest.get" + note: "Retained; options now use Options.NoUrl and no longer exclude body." +"@effect/platform/HttpClientRequest#head": + replacement: "HttpClientRequest.head" + note: "Retained; options now use Options.NoUrl and no longer exclude body." +"@effect/platform/HttpClientRequest#make": + replacement: "HttpClientRequest.make" + note: "Retained; all methods now accept Options.NoUrl without the GET/HEAD body restriction." +"@effect/platform/HttpClientRequest#modify": + replacement: "HttpClientRequest.modify" + note: "Retained with data-first and data-last overloads." +"@effect/platform/HttpClientRequest#options": + replacement: "HttpClientRequest.options" + note: "Retained with Options.NoUrl." +"@effect/platform/HttpClientRequest#Options.NoBody": + replacement: "HttpClientRequest.Options.NoUrl" + note: "NoBody was removed; v4 method helpers uniformly omit only url." +"@effect/platform/HttpClientRequest#patch": + replacement: "HttpClientRequest.patch" + note: "Retained with Options.NoUrl." +"@effect/platform/HttpClientRequest#post": + replacement: "HttpClientRequest.post" + note: "Retained with Options.NoUrl." +"@effect/platform/HttpClientRequest#put": + replacement: "HttpClientRequest.put" + note: "Retained with Options.NoUrl." +"@effect/platform/HttpClientRequest#setBody": + replacement: "HttpClientRequest.setBody" + note: "Retained and still synchronizes body content metadata into headers." +"@effect/platform/HttpClientRequest#setHeader": + replacement: "HttpClientRequest.setHeader" + note: "Retained with data-first and data-last overloads." +"@effect/platform/HttpClientRequest#setHeaders": + replacement: "HttpClientRequest.setHeaders" + note: "Retained with data-first and data-last overloads." +"@effect/platform/HttpClientRequest#toUrl": + replacement: "HttpClientRequest.toUrl" + note: "Retained and still returns Option." +"@effect/platform/HttpClientRequest#TypeId": + replacement: "HttpClientRequest.isHttpClientRequest" + note: "The request brand is private in v4; use the public runtime refinement." diff --git a/.context/effect/migration/annotations/effect__platform__HttpClientResponse.yaml b/.context/effect/migration/annotations/effect__platform__HttpClientResponse.yaml new file mode 100644 index 000000000..cc48c14c8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpClientResponse.yaml @@ -0,0 +1,27 @@ +"@effect/platform/HttpClientResponse#filterStatus": + replacement: "HttpClientResponse.filterStatus" + note: "Retained; rejected status now fails with an HttpClientError wrapper." +"@effect/platform/HttpClientResponse#filterStatusOk": + replacement: "HttpClientResponse.filterStatusOk" + note: "Retained; non-2xx status now fails with an HttpClientError wrapper." +"@effect/platform/HttpClientResponse#schemaBodyJson": + replacement: "HttpClientResponse.schemaBodyJson" + note: "Retained with v4 Schema constraints and SchemaError failures." +"@effect/platform/HttpClientResponse#schemaBodyUrlParams": + replacement: "HttpClientResponse.schemaBodyUrlParams" + note: "Retained with ConstraintCodec input and SchemaError failures." +"@effect/platform/HttpClientResponse#schemaHeaders": + replacement: "HttpClientResponse.schemaHeaders" + note: "Retained with ConstraintCodec input and SchemaError failures." +"@effect/platform/HttpClientResponse#schemaJson": + replacement: "HttpClientResponse.schemaJson" + note: "Retained with ConstraintCodec input and v4 error types." +"@effect/platform/HttpClientResponse#schemaNoBody": + replacement: "HttpClientResponse.schemaNoBody" + note: "Retained with Schema.Codec input and SchemaError failures." +"@effect/platform/HttpClientResponse#stream": + replacement: "HttpClientResponse.stream" + note: "Retained; body failures now use the broader HttpClientError wrapper." +"@effect/platform/HttpClientResponse#TypeId": + replacement: "typeof HttpClientResponse.TypeId" + note: "TypeId remains public but is now a string constant; use typeof in type position." diff --git a/.context/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml b/.context/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml new file mode 100644 index 000000000..f720fb5d1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpIncomingMessage.yaml @@ -0,0 +1,9 @@ +"@effect/platform/HttpIncomingMessage#MaxBodySize": + replacement: "HttpIncomingMessage.MaxBodySize" + note: "Changed from a Reference subclass holding Option to Context.Reference." +"@effect/platform/HttpIncomingMessage#TypeId": + replacement: "typeof HttpIncomingMessage.TypeId" + note: "TypeId remains public but is now a string constant; use typeof in type position." +"@effect/platform/HttpIncomingMessage#withMaxBodySize": + replacement: "Effect.provideService(HttpIncomingMessage.MaxBodySize, size)" + note: "The helper was removed; provide FileSystem.Size(input) or undefined directly." diff --git a/.context/effect/migration/annotations/effect__platform__HttpLayerRouter.yaml b/.context/effect/migration/annotations/effect__platform__HttpLayerRouter.yaml new file mode 100644 index 000000000..ef12c88d0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpLayerRouter.yaml @@ -0,0 +1,69 @@ +"@effect/platform/HttpLayerRouter#addHttpApi": + replacement: "HttpApiBuilder.layer" + note: "HTTP API registration moved to effect/unstable/httpapi." +"@effect/platform/HttpLayerRouter#cors": + replacement: "HttpRouter.cors" + note: "HttpLayerRouter was consolidated into v4 HttpRouter." +"@effect/platform/HttpLayerRouter#FindMyWay.make": + replacement: "FindMyWay.make" + note: "Import FindMyWay from effect/unstable/http." +"@effect/platform/HttpLayerRouter#FindMyWay.PathInput": + replacement: "FindMyWay.PathInput" + note: "Import FindMyWay from effect/unstable/http." +"@effect/platform/HttpLayerRouter#make": + replacement: "HttpRouter.make" + note: "The layer-oriented router became the sole v4 HttpRouter implementation." +"@effect/platform/HttpLayerRouter#MiddlewareTypeId": + replacement: "none" + note: "The middleware type id is internal in v4; use HttpRouter.Middleware." +"@effect/platform/HttpLayerRouter#PathInput": + replacement: "HttpRouter.PathInput" + note: "Moved to the consolidated v4 router." +"@effect/platform/HttpLayerRouter#RouteContext": + replacement: "HttpRouter.RouteContext" + note: "Moved to the consolidated v4 router." +"@effect/platform/HttpLayerRouter#RouterConfig": + replacement: "HttpRouter.RouterConfig" + note: "Now a Context.Reference containing Partial." +"@effect/platform/HttpLayerRouter#RouteTypeId": + replacement: "none" + note: "Route nominal ids are internal in v4; construct routes with HttpRouter.route." +"@effect/platform/HttpLayerRouter#schemaJson": + replacement: "HttpRouter.schemaJson" + note: "Moved to the consolidated router with v4 Schema and error types." +"@effect/platform/HttpLayerRouter#schemaNoBody": + replacement: "HttpRouter.schemaNoBody" + note: "Moved to the consolidated router with v4 Schema types." +"@effect/platform/HttpLayerRouter#serve": + replacement: "HttpRouter.serve" + note: "Moved to the consolidated router; pass the route-registration layer." +"@effect/platform/HttpLayerRouter#toWebHandler": + replacement: "HttpRouter.toWebHandler" + note: "Moved to the consolidated router for building a Fetch handler and disposer." +"@effect/platform/HttpLayerRouter#TypeId": + replacement: "none" + note: "The router nominal service id is internal in v4; use HttpRouter.HttpRouter." +"@effect/platform/HttpLayerRouter#Request.From": + replacement: "HttpRouter.Request.From" + note: "Moved with the layer-oriented router into the consolidated HttpRouter module." +"@effect/platform/HttpLayerRouter#Request.Only": + replacement: "HttpRouter.Request.Only" + note: "Moved with the layer-oriented router into the consolidated HttpRouter module." +"@effect/platform/HttpLayerRouter#Route.Context": + replacement: "HttpRouter.Route.Context" + note: "Moved with the Route helper types into the consolidated HttpRouter module." +"@effect/platform/HttpLayerRouter#Route.Error": + replacement: "HttpRouter.Route.Error" + note: "Moved with the Route helper types into the consolidated HttpRouter module." +"@effect/platform/HttpLayerRouter#add": + replacement: "HttpRouter.add" + note: "Moved to the consolidated HttpRouter; it still returns a route-registration Layer." +"@effect/platform/HttpLayerRouter#addAll": + replacement: "HttpRouter.addAll" + note: "Moved to the consolidated HttpRouter; it still registers route values through a Layer and supports a prefix option." +"@effect/platform/HttpLayerRouter#layer": + replacement: "HttpRouter.layer" + note: "Use the layer for the consolidated HttpRouter service." +"@effect/platform/HttpLayerRouter#toHttpEffect": + replacement: "HttpRouter.toHttpEffect" + note: "Moved to the consolidated HttpRouter; route-not-found failures now use HttpServerError.HttpServerError." diff --git a/.context/effect/migration/annotations/effect__platform__HttpMethod.yaml b/.context/effect/migration/annotations/effect__platform__HttpMethod.yaml new file mode 100644 index 000000000..73edb9d95 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpMethod.yaml @@ -0,0 +1,3 @@ +"@effect/platform/HttpMethod#all": + replacement: "HttpMethod.all" + note: "Retained as the readonly set of all supported methods." diff --git a/.context/effect/migration/annotations/effect__platform__HttpMiddleware.yaml b/.context/effect/migration/annotations/effect__platform__HttpMiddleware.yaml new file mode 100644 index 000000000..dd8f50df1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpMiddleware.yaml @@ -0,0 +1,27 @@ +"@effect/platform/HttpMiddleware#cors": + replacement: "HttpMiddleware.cors" + note: "Retained with the same CORS options and behavior." +"@effect/platform/HttpMiddleware#currentTracerDisabledWhen": + replacement: "HttpMiddleware.TracerDisabledWhen" + note: "The FiberRef became a Context.Reference containing the request predicate." +"@effect/platform/HttpMiddleware#loggerDisabled": + replacement: "HttpMiddleware.withLoggerDisabled" + note: "The FiberRef was removed; locally wrap an effect or use HttpRouter.disableLogger." +"@effect/platform/HttpMiddleware#make": + replacement: "HttpMiddleware.make" + note: "Retained as the precise middleware constructor." +"@effect/platform/HttpMiddleware#SpanNameGenerator": + replacement: "HttpMiddleware.SpanNameGenerator" + note: "The branded interface became a Context.Reference containing the generator." +"@effect/platform/HttpMiddleware#withSpanNameGenerator": + replacement: "Layer.provide(layer, Layer.succeed(HttpMiddleware.SpanNameGenerator)(f))" + note: "Provide the SpanNameGenerator reference to the target layer." +"@effect/platform/HttpMiddleware#withTracerDisabledForUrls": + replacement: "Layer.provide(layer, HttpMiddleware.layerTracerDisabledForUrls(urls))" + note: "Provide the new URL-predicate layer to the target layer." +"@effect/platform/HttpMiddleware#withTracerDisabledWhen": + replacement: "Layer.provide(layer, Layer.succeed(HttpMiddleware.TracerDisabledWhen)(predicate))" + note: "Provide the TracerDisabledWhen reference to the target layer." +"@effect/platform/HttpMiddleware#withTracerDisabledWhenEffect": + replacement: "Effect.provideService(effect, HttpMiddleware.TracerDisabledWhen, predicate)" + note: "Provide the TracerDisabledWhen reference locally to the effect." diff --git a/.context/effect/migration/annotations/effect__platform__HttpMultiplex.yaml b/.context/effect/migration/annotations/effect__platform__HttpMultiplex.yaml new file mode 100644 index 000000000..d11372f2a --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpMultiplex.yaml @@ -0,0 +1,3 @@ +"@effect/platform/HttpMultiplex": + replacement: none + note: The HttpMultiplex module, value, constructor, and nominal type id were removed with no v4 counterpart. Replace them with a custom first-match Effect dispatcher requiring HttpServerRequest; initialize an empty dispatcher, then add or fold predicate/app pairs into it. Recreate header helpers with predicates over lower-cased request header values using exact equality, String.startsWith, String.endsWith, or RegExp.test; recreate host helpers with the same comparisons over request.headers.host. diff --git a/.context/effect/migration/annotations/effect__platform__HttpPlatform.yaml b/.context/effect/migration/annotations/effect__platform__HttpPlatform.yaml new file mode 100644 index 000000000..ba4d67380 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpPlatform.yaml @@ -0,0 +1,12 @@ +"@effect/platform/HttpPlatform#HttpPlatform": + replacement: "HttpPlatform.HttpPlatform" + note: "The service is now a Context.Service class; use its Service member for the implementation type." +"@effect/platform/HttpPlatform#layer": + replacement: "HttpPlatform.layer" + note: "Retained as the default file-response layer." +"@effect/platform/HttpPlatform#make": + replacement: "HttpPlatform.make" + note: "Retained; v4 returns the service implementation and uses updated file stream options." +"@effect/platform/HttpPlatform#TypeId": + replacement: "none" + note: "The public type id was removed; use the HttpPlatform Context.Service class." diff --git a/.context/effect/migration/annotations/effect__platform__HttpRouter.yaml b/.context/effect/migration/annotations/effect__platform__HttpRouter.yaml new file mode 100644 index 000000000..7fdd24d99 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpRouter.yaml @@ -0,0 +1,102 @@ +"@effect/platform/HttpRouter#all": + replacement: "HttpRouter.add(\"*\", path, handler, options)" + note: "v4 registers a route layer instead of returning an immutable router." +"@effect/platform/HttpRouter#append": + replacement: "HttpRouter.addAll([route])" + note: "Register the route and merge its layer with other route layers." +"@effect/platform/HttpRouter#catchAll": + replacement: "HttpRouter.middleware + Effect.catch" + note: "Apply typed-error recovery in route middleware provided to route layers." +"@effect/platform/HttpRouter#catchAllCause": + replacement: "HttpRouter.middleware + Effect.catchCause" + note: "Apply cause recovery in route middleware provided to route layers." +"@effect/platform/HttpRouter#catchTag": + replacement: "HttpRouter.middleware + Effect.catchTag" + note: "Apply tagged-error recovery in route middleware provided to route layers." +"@effect/platform/HttpRouter#concat": + replacement: "Layer.merge" + note: "Routers are now route-registration layers; merge the two layers." +"@effect/platform/HttpRouter#concatAll": + replacement: "Layer.mergeAll" + note: "Routers are now route-registration layers; merge all layers." +"@effect/platform/HttpRouter#currentRouterConfig": + replacement: "HttpRouter.RouterConfig" + note: "The FiberRef became a Context.Reference containing Partial." +"@effect/platform/HttpRouter#Default": + replacement: "HttpRouter.HttpRouter + HttpRouter.layer" + note: "Custom/default router tags were removed; v4 provides one router service." +"@effect/platform/HttpRouter#empty": + replacement: "Layer.empty" + note: "There is no immutable empty router; use an empty registration layer." +"@effect/platform/HttpRouter#fromIterable": + replacement: "HttpRouter.addAll(Array.from(routes))" + note: "Materialize and register the route descriptors as a layer." +"@effect/platform/HttpRouter#get": + replacement: "HttpRouter.add(\"GET\", path, handler, options)" + note: "Register a route layer; handlers must produce HttpServerResponse." +"@effect/platform/HttpRouter#head": + replacement: "HttpRouter.addAll([HttpRouter.route(\"HEAD\", path, handler, options)])" + note: "Use route plus addAll because add does not expose HEAD." +"@effect/platform/HttpRouter#HttpRouter": + replacement: "HttpRouter.HttpRouter" + note: "The name remains, but now denotes the mutable layer-oriented registration service." +"@effect/platform/HttpRouter#HttpRouter.DefaultServices": + replacement: "none" + note: "The custom tagged-router default-service bundle was removed." +"@effect/platform/HttpRouter#HttpRouter.Service": + replacement: "HttpRouter.HttpRouter" + note: "Use the consolidated router service interface." +"@effect/platform/HttpRouter#makeRoute": + replacement: "HttpRouter.route" + note: "Renamed to route; v4 route options no longer expose the old prefix field." +"@effect/platform/HttpRouter#mount": + replacement: "HttpRouter.addAll(routes, { prefix: path })" + note: "Register child routes with a prefix, or use router.prefixed(path)." +"@effect/platform/HttpRouter#mountApp": + replacement: "HttpRouter.use((router) => router.prefixed(path).add(\"*\", \"/*\", app))" + note: "Register the app on the prefixed router service; no direct mount API remains." +"@effect/platform/HttpRouter#options": + replacement: "HttpRouter.add(\"OPTIONS\", path, handler, options)" + note: "Register a route layer; handlers must produce HttpServerResponse." +"@effect/platform/HttpRouter#patch": + replacement: "HttpRouter.add(\"PATCH\", path, handler, options)" + note: "Register a route layer; handlers must produce HttpServerResponse." +"@effect/platform/HttpRouter#PathInput": + replacement: "HttpRouter.PathInput" + note: "Retained as an absolute slash path or wildcard." +"@effect/platform/HttpRouter#post": + replacement: "HttpRouter.add(\"POST\", path, handler, options)" + note: "Register a route layer; handlers must produce HttpServerResponse." +"@effect/platform/HttpRouter#prefixAll": + replacement: "HttpRouter.addAll(routes, { prefix })" + note: "Apply the prefix while registering route descriptors." +"@effect/platform/HttpRouter#put": + replacement: "HttpRouter.add(\"PUT\", path, handler, options)" + note: "Register a route layer; handlers must produce HttpServerResponse." +"@effect/platform/HttpRouter#Route.Middleware": + replacement: "Effect.Effect" + note: "Spell the route response Effect directly, or use HttpRouter.middleware for transforms." +"@effect/platform/HttpRouter#RouteContextTypeId": + replacement: "none" + note: "The nominal id is internal in v4; access HttpRouter.RouteContext as a service." +"@effect/platform/HttpRouter#RouteTypeId": + replacement: "none" + note: "The nominal id is internal in v4; construct routes with HttpRouter.route." +"@effect/platform/HttpRouter#setRouterConfig": + replacement: "Layer.succeed(HttpRouter.RouterConfig)(config)" + note: "Provide the RouterConfig Context.Reference as a layer." +"@effect/platform/HttpRouter#Tag": + replacement: "none" + note: "Custom router tags were removed; use the singleton router service and registration layers." +"@effect/platform/HttpRouter#toHttpApp": + replacement: "HttpRouter.toHttpEffect" + note: "Pass the route-registration layer to build the server handler effect." +"@effect/platform/HttpRouter#transform": + replacement: "HttpRouter.middleware" + note: "Express the route-wide response Effect transform as router middleware." +"@effect/platform/HttpRouter#TypeId": + replacement: "none" + note: "The router nominal service id is internal in v4; use HttpRouter.HttpRouter." +"@effect/platform/HttpRouter#withRouterConfig": + replacement: "Effect.provideService(effect, HttpRouter.RouterConfig, config)" + note: "Provide the RouterConfig Context.Reference locally instead of setting a FiberRef." diff --git a/.context/effect/migration/annotations/effect__platform__HttpServer.yaml b/.context/effect/migration/annotations/effect__platform__HttpServer.yaml new file mode 100644 index 000000000..be98b6d5d --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpServer.yaml @@ -0,0 +1,27 @@ +"@effect/platform/HttpServer#addressWith": + replacement: "HttpServer.HttpServer.use(({ address }) => effect(address))" + note: "The accessor was removed; read the service and pass its Address to the callback." +"@effect/platform/HttpServer#HttpServer": + replacement: "HttpServer.HttpServer" + note: "The interface and tag became one Context.Service class; use its Service member for implementations." +"@effect/platform/HttpServer#layerContext": + replacement: "HttpServer.layerServices" + note: "Renamed; it provides the standard HTTP platform services." +"@effect/platform/HttpServer#make": + replacement: "HttpServer.make" + note: "Retained; it returns the Context.Service implementation." +"@effect/platform/HttpServer#ServeOptions": + replacement: "none" + note: "The unused respond option model was removed with no shared v4 counterpart." +"@effect/platform/HttpServer#TcpAddress": + replacement: "HttpServer.TcpAddress" + note: "Moved unchanged." +"@effect/platform/HttpServer#TypeId": + replacement: "none" + note: "The public TypeId was removed; HttpServer is now a Context.Service class." +"@effect/platform/HttpServer#UnixAddress": + replacement: "HttpServer.UnixAddress" + note: "Moved unchanged." +"@effect/platform/HttpServer#serve": + replacement: "effect/unstable/http/HttpServer#serve" + note: "Moved to the v4 HTTP module; the application is now an Effect producing HttpServerResponse rather than the separate HttpApp model." diff --git a/.context/effect/migration/annotations/effect__platform__HttpServerError.yaml b/.context/effect/migration/annotations/effect__platform__HttpServerError.yaml new file mode 100644 index 000000000..60470daf8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpServerError.yaml @@ -0,0 +1,15 @@ +"@effect/platform/HttpServerError#clientAbortFiberId": + replacement: "HttpServerError.ClientAbort.annotation" + note: "Client aborts now use a Cause context annotation rather than a sentinel FiberId." +"@effect/platform/HttpServerError#HttpServerError": + replacement: "HttpServerError.HttpServerError | HttpServerError.ServeError" + note: "Handler failures became a tagged wrapper, while ServeError remains separate." +"@effect/platform/HttpServerError#isServerError": + replacement: "HttpServerError.isHttpServerError" + note: "Renamed and narrowed to wrapped handler errors; test ServeError separately if needed." +"@effect/platform/HttpServerError#RequestError": + replacement: "HttpServerError.RequestParseError (constructor) / HttpServerError.RequestError (type)" + note: "The constructible class became RequestParseError; RequestError is now a broader type union." +"@effect/platform/HttpServerError#TypeId": + replacement: "HttpServerError.isHttpServerError" + note: "The brand is private in v4; use the public runtime refinement." diff --git a/.context/effect/migration/annotations/effect__platform__HttpServerRequest.yaml b/.context/effect/migration/annotations/effect__platform__HttpServerRequest.yaml new file mode 100644 index 000000000..82ea35001 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpServerRequest.yaml @@ -0,0 +1,27 @@ +"@effect/platform/HttpServerRequest#fromWeb": + replacement: "HttpServerRequest.fromWeb" + note: "Retained for wrapping a Web Request." +"@effect/platform/HttpServerRequest#ParsedSearchParams": + replacement: "HttpServerRequest.ParsedSearchParams" + note: "The marker and tag became one Context.Service class." +"@effect/platform/HttpServerRequest#persistedMultipart": + replacement: "HttpServerRequest.HttpServerRequest.use((request) => request.multipart)" + note: "Use the request service's `.use` helper to return its cached multipart effect." +"@effect/platform/HttpServerRequest#schemaBodyJson": + replacement: "HttpServerRequest.schemaBodyJson" + note: "Retained with v4 Schema constraints and error types." +"@effect/platform/HttpServerRequest#schemaBodyUrlParams": + replacement: "HttpServerRequest.schemaBodyUrlParams" + note: "Retained with ConstraintCodec input and v4 error types." +"@effect/platform/HttpServerRequest#schemaHeaders": + replacement: "HttpServerRequest.schemaHeaders" + note: "Retained with ConstraintCodec input and SchemaError failures." +"@effect/platform/HttpServerRequest#toWeb": + replacement: "HttpServerRequest.toWeb" + note: "Retained and captures the current Context for streamed bodies." +"@effect/platform/HttpServerRequest#toWebEither": + replacement: "HttpServerRequest.toWebResult" + note: "Either became Result, and the optional Runtime became an optional Context." +"@effect/platform/HttpServerRequest#TypeId": + replacement: "typeof HttpServerRequest.TypeId" + note: "TypeId remains public but is now a string constant; use typeof in type position." diff --git a/.context/effect/migration/annotations/effect__platform__HttpServerRespondable.yaml b/.context/effect/migration/annotations/effect__platform__HttpServerRespondable.yaml new file mode 100644 index 000000000..306afb813 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpServerRespondable.yaml @@ -0,0 +1,3 @@ +"@effect/platform/HttpServerRespondable#symbol": + replacement: "HttpServerRespondable.symbol" + note: "Retained as a string protocol key rather than a unique symbol." diff --git a/.context/effect/migration/annotations/effect__platform__HttpServerResponse.yaml b/.context/effect/migration/annotations/effect__platform__HttpServerResponse.yaml new file mode 100644 index 000000000..86d7dfd14 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__HttpServerResponse.yaml @@ -0,0 +1,63 @@ +"@effect/platform/HttpServerResponse#empty": + replacement: "HttpServerResponse.empty" + note: "Retained with default status 204; responses are no longer Effects or Respondables." +"@effect/platform/HttpServerResponse#expireCookie": + replacement: "HttpServerResponse.expireCookie" + note: "Now effectful and safe; use expireCookieUnsafe for synchronous throwing behavior." +"@effect/platform/HttpServerResponse#file": + replacement: "HttpServerResponse.file" + note: "Retained with updated FileSystem stream options." +"@effect/platform/HttpServerResponse#formData": + replacement: "HttpServerResponse.formData" + note: "Moved unchanged." +"@effect/platform/HttpServerResponse#fromWeb": + replacement: "HttpServerResponse.fromWeb" + note: "Retained; Set-Cookie headers become Cookies and Web bodies become stream bodies." +"@effect/platform/HttpServerResponse#isServerResponse": + replacement: "HttpServerResponse.isHttpServerResponse" + note: "Renamed." +"@effect/platform/HttpServerResponse#json": + replacement: "HttpServerResponse.json" + note: "Retained as the safe effectful JSON constructor." +"@effect/platform/HttpServerResponse#raw": + replacement: "HttpServerResponse.raw" + note: "Moved unchanged." +"@effect/platform/HttpServerResponse#setBody": + replacement: "HttpServerResponse.setBody" + note: "Retained and reflects body content metadata in response headers." +"@effect/platform/HttpServerResponse#setCookie": + replacement: "HttpServerResponse.setCookie" + note: "Retained as the safe effectful cookie setter." +"@effect/platform/HttpServerResponse#setHeader": + replacement: "HttpServerResponse.setHeader" + note: "Retained with data-first and data-last overloads." +"@effect/platform/HttpServerResponse#setHeaders": + replacement: "HttpServerResponse.setHeaders" + note: "Retained with data-first and data-last overloads." +"@effect/platform/HttpServerResponse#stream": + replacement: "HttpServerResponse.stream" + note: "Retained; v4 Stream no longer has a service type parameter." +"@effect/platform/HttpServerResponse#text": + replacement: "HttpServerResponse.text" + note: "Moved unchanged." +"@effect/platform/HttpServerResponse#toWeb": + replacement: "HttpServerResponse.toWeb" + note: "Retained, but the optional Runtime became an optional Context for stream execution." +"@effect/platform/HttpServerResponse#TypeId": + replacement: "HttpServerResponse.isHttpServerResponse" + note: "The response brand is private in v4; use the public runtime refinement." +"@effect/platform/HttpServerResponse#uint8Array": + replacement: "HttpServerResponse.uint8Array" + note: "Moved unchanged." +"@effect/platform/HttpServerResponse#unsafeJson": + replacement: "HttpServerResponse.jsonUnsafe" + note: "Renamed to put Unsafe last; serialization failures still throw." +"@effect/platform/HttpServerResponse#unsafeSetCookie": + replacement: "HttpServerResponse.setCookieUnsafe" + note: "Renamed to put Unsafe last; invalid cookies still throw." +"@effect/platform/HttpServerResponse#unsafeSetCookies": + replacement: "HttpServerResponse.setCookiesUnsafe" + note: "Renamed to put Unsafe last; invalid cookies still throw." +"@effect/platform/HttpServerResponse#urlParams": + replacement: "HttpServerResponse.urlParams" + note: "Retained and widened to accept UrlParams.Input." diff --git a/.context/effect/migration/annotations/effect__platform__KeyValueStore.yaml b/.context/effect/migration/annotations/effect__platform__KeyValueStore.yaml new file mode 100644 index 000000000..ba36c699f --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__KeyValueStore.yaml @@ -0,0 +1,27 @@ +"@effect/platform/KeyValueStore#KeyValueStore": + replacement: "KeyValueStore.KeyValueStore" + note: "The service moved to effect/unstable/persistence/KeyValueStore; missing values now use undefined and operations fail with KeyValueStoreError." +"@effect/platform/KeyValueStore#KeyValueStore.AnyStore": + replacement: "KeyValueStore.KeyValueStore | KeyValueStore.SchemaStore" + note: "The convenience namespace alias was removed; write the store union explicitly when needed." +"@effect/platform/KeyValueStore#layerMemory": + replacement: "KeyValueStore.layerMemory" + note: "The in-memory layer remains in the moved module." +"@effect/platform/KeyValueStore#layerSchema": + replacement: "KeyValueStore.toSchemaStore" + note: "Schema stores are now derived with toSchemaStore; define the desired Context.Service and layer explicitly." +"@effect/platform/KeyValueStore#layerStorage": + replacement: "KeyValueStore.layerStorage" + note: "The Web Storage layer remains in the moved module." +"@effect/platform/KeyValueStore#make": + replacement: "KeyValueStore.make" + note: "The constructor remains in the moved module with v4 MakeOptions." +"@effect/platform/KeyValueStore#prefix": + replacement: "KeyValueStore.prefix" + note: "The prefixed-store combinator remains in the moved module." +"@effect/platform/KeyValueStore#SchemaStoreTypeId": + replacement: "none" + note: "The v4 SchemaStore has no public type-id alias; use the SchemaStore interface." +"@effect/platform/KeyValueStore#TypeId": + replacement: "none" + note: "The KeyValueStore runtime marker is internal in v4; use the service and interface." diff --git a/.context/effect/migration/annotations/effect__platform__MsgPack.yaml b/.context/effect/migration/annotations/effect__platform__MsgPack.yaml new file mode 100644 index 000000000..3c4d47b6b --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__MsgPack.yaml @@ -0,0 +1,24 @@ +"@effect/platform/MsgPack#duplex": + replacement: "Msgpack.duplex" + note: "The API moved to effect/unstable/encoding/Msgpack." +"@effect/platform/MsgPack#duplexSchema": + replacement: "Msgpack.duplexSchema" + note: "The API moved to effect/unstable/encoding/Msgpack and uses v4 Schema constraints." +"@effect/platform/MsgPack#ErrorTypeId": + replacement: "Msgpack.MsgPackError" + note: "The public error type-id alias was removed; use the MsgPackError class." +"@effect/platform/MsgPack#pack": + replacement: "Msgpack.encode" + note: "The MessagePack channel constructor was renamed from pack to encode." +"@effect/platform/MsgPack#packSchema": + replacement: "Msgpack.encodeSchema" + note: "The schema-aware pack channel was renamed to encodeSchema." +"@effect/platform/MsgPack#schema": + replacement: "Msgpack.schema" + note: "The schema helper remains in the moved module and uses the v4 Schema model." +"@effect/platform/MsgPack#unpack": + replacement: "Msgpack.decode" + note: "The MessagePack channel constructor was renamed from unpack to decode." +"@effect/platform/MsgPack#unpackSchema": + replacement: "Msgpack.decodeSchema" + note: "The schema-aware unpack channel was renamed to decodeSchema." diff --git a/.context/effect/migration/annotations/effect__platform__Multipart.yaml b/.context/effect/migration/annotations/effect__platform__Multipart.yaml new file mode 100644 index 000000000..e5f43102e --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Multipart.yaml @@ -0,0 +1,57 @@ +"@effect/platform/Multipart#ErrorTypeId": + replacement: "Multipart.MultipartError" + note: "The public error type-id alias was removed; use the MultipartError class." +"@effect/platform/Multipart#FieldMimeTypes": + replacement: "Multipart.FieldMimeTypes" + note: "The setting remains but is now a Context.Reference rather than a service class." +"@effect/platform/Multipart#FileSchema": + replacement: "Multipart.PersistedFileSchema" + note: "The schema for persisted multipart files was renamed." +"@effect/platform/Multipart#isField": + replacement: "Multipart.isField" + note: "The guard remains in effect/unstable/http/Multipart." +"@effect/platform/Multipart#isFile": + replacement: "Multipart.isFile" + note: "The guard remains in effect/unstable/http/Multipart." +"@effect/platform/Multipart#isPart": + replacement: "Multipart.isPart" + note: "The guard remains in effect/unstable/http/Multipart." +"@effect/platform/Multipart#MaxFieldSize": + replacement: "Multipart.MaxFieldSize" + note: "The setting remains but is now a Context.Reference." +"@effect/platform/Multipart#MaxFileSize": + replacement: "Multipart.MaxFileSize" + note: "The setting remains as a Context.Reference; use undefined rather than Option.none for no limit." +"@effect/platform/Multipart#MaxParts": + replacement: "Multipart.MaxParts" + note: "The setting remains as a Context.Reference; use undefined rather than Option.none for no limit." +"@effect/platform/Multipart#schemaJson": + replacement: "Multipart.schemaJson" + note: "The JSON-field decoder remains in effect/unstable/http/Multipart and uses v4 Schema constraints." +"@effect/platform/Multipart#TypeId": + replacement: "typeof Multipart.TypeId" + note: "The runtime marker remains exported, but the separate type alias was removed." +"@effect/platform/Multipart#withFieldMimeTypes": + replacement: "Effect.provideService(Multipart.FieldMimeTypes, mimeTypes)" + note: "Provide the v4 Context.Reference around the effect." +"@effect/platform/Multipart#withLimits": + replacement: "Effect.provideContext(effect, Multipart.limitsServices(options))" + note: "Build the multipart limit context and provide it to the effect; Option-valued limits became optional plain values." +"@effect/platform/Multipart#withLimitsStream": + replacement: "Stream.provideContext(stream, Multipart.limitsServices(options))" + note: "Build the multipart limit context and provide it to the stream; Option-valued limits became optional plain values." +"@effect/platform/Multipart#withMaxFieldSize": + replacement: "Effect.provideService(Multipart.MaxFieldSize, size)" + note: "Provide the v4 Context.Reference around the effect." +"@effect/platform/Multipart#withMaxFileSize": + replacement: "Effect.provideService(Multipart.MaxFileSize, size)" + note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." +"@effect/platform/Multipart#withMaxParts": + replacement: "Effect.provideService(Multipart.MaxParts, count)" + note: "Provide the v4 Context.Reference around the effect, converting Option.none to undefined." +"@effect/platform/Multipart#makeChannel": + replacement: "effect/unstable/http/Multipart#makeChannel" + note: "The channel constructor moved and no longer accepts bufferSize; input and output chunks use non-empty readonly arrays." +"@effect/platform/Multipart#withLimits.Options": + replacement: "Multipart.withLimits.Options" + note: "Limit fields now use optional plain numbers or SizeInput values; convert Option.none to undefined and Option.some(value) to value." diff --git a/.context/effect/migration/annotations/effect__platform__Ndjson.yaml b/.context/effect/migration/annotations/effect__platform__Ndjson.yaml new file mode 100644 index 000000000..9786ff676 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Ndjson.yaml @@ -0,0 +1,39 @@ +"@effect/platform/Ndjson#duplex": + replacement: "Ndjson.duplex" + note: "The API moved to effect/unstable/encoding/Ndjson." +"@effect/platform/Ndjson#duplexSchema": + replacement: "Ndjson.duplexSchema" + note: "The API moved to effect/unstable/encoding/Ndjson and uses v4 Schema constraints." +"@effect/platform/Ndjson#ErrorTypeId": + replacement: "Ndjson.NdjsonError" + note: "The public error marker was removed; use the NdjsonError class." +"@effect/platform/Ndjson#NdjsonErrorTypeId": + replacement: "Ndjson.NdjsonError" + note: "The public error type-id alias was removed; use the NdjsonError class." +"@effect/platform/Ndjson#NdjsonOptions": + replacement: "{ readonly ignoreEmptyLines?: boolean }" + note: "The standalone options interface was removed; decoding and duplex APIs accept this inline shape." +"@effect/platform/Ndjson#pack": + replacement: "Ndjson.encode" + note: "The NDJSON channel constructor was renamed from pack to encode." +"@effect/platform/Ndjson#packSchema": + replacement: "Ndjson.encodeSchema" + note: "The schema-aware pack channel was renamed to encodeSchema." +"@effect/platform/Ndjson#packSchemaString": + replacement: "Ndjson.encodeSchemaString" + note: "The string schema pack channel was renamed to encodeSchemaString." +"@effect/platform/Ndjson#packString": + replacement: "Ndjson.encodeString" + note: "The string pack channel was renamed to encodeString." +"@effect/platform/Ndjson#unpack": + replacement: "Ndjson.decode" + note: "The NDJSON channel constructor was renamed from unpack to decode." +"@effect/platform/Ndjson#unpackSchema": + replacement: "Ndjson.decodeSchema" + note: "The schema-aware unpack channel was renamed to decodeSchema." +"@effect/platform/Ndjson#unpackSchemaString": + replacement: "Ndjson.decodeSchemaString" + note: "The string schema unpack channel was renamed to decodeSchemaString." +"@effect/platform/Ndjson#unpackString": + replacement: "Ndjson.decodeString" + note: "The string unpack channel was renamed to decodeString." diff --git a/.context/effect/migration/annotations/effect__platform__OpenApi.yaml b/.context/effect/migration/annotations/effect__platform__OpenApi.yaml new file mode 100644 index 000000000..fe9cb44cf --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__OpenApi.yaml @@ -0,0 +1,18 @@ +"@effect/platform/OpenApi#AdditionalPropertiesStrategy": + replacement: "none" + note: "OpenApi.fromApi no longer accepts generation options; standalone JSON Schema generation has a separate additionalProperties option." +"@effect/platform/OpenApi#annotations": + replacement: "effect/unstable/httpapi/OpenApi#annotations" + note: "Same annotation-context helper after the module move." +"@effect/platform/OpenApi#Exclude": + replacement: "effect/unstable/httpapi/OpenApi#Exclude" + note: "Same annotation key and default; it is now a Context.Reference value." +"@effect/platform/OpenApi#fromApi": + replacement: "effect/unstable/httpapi/OpenApi#fromApi" + note: "The operation remains and returns OpenAPI 3.1, but the signature is now only fromApi(api)." +"@effect/platform/OpenApi#OpenApiSpecContentType": + replacement: "string" + note: "The closed media-type union was removed so custom and streaming media types are supported." +"@effect/platform/OpenApi#Title": + replacement: "effect/unstable/httpapi/OpenApi#Title" + note: "Same annotation role, now implemented as a v4 Context.Service." diff --git a/.context/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml b/.context/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml new file mode 100644 index 000000000..683b937f3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__OpenApiJsonSchema.yaml @@ -0,0 +1,45 @@ +"@effect/platform/OpenApiJsonSchema#Any": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated into the open, dialect-neutral JSON Schema object model." +"@effect/platform/OpenApiJsonSchema#AnyObject": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated; construct the required object directly." +"@effect/platform/OpenApiJsonSchema#AnyOf": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Array": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Empty": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces and special id shapes were removed." +"@effect/platform/OpenApiJsonSchema#Enum": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Enums": + replacement: "effect/JsonSchema#JsonSchema" + note: "The Effect-specific comment enum shape has no named v4 interface; use the general object model." +"@effect/platform/OpenApiJsonSchema#Integer": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow numeric interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#JsonSchema": + replacement: "effect/JsonSchema#JsonSchema" + note: "Use the dialect-neutral open JSON Schema object model." +"@effect/platform/OpenApiJsonSchema#make": + replacement: "effect/Schema#toJsonSchemaDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1" + note: "Generate Draft 2020-12, wrap the root in a multi-document, then convert references and definitions to OpenAPI 3.1." +"@effect/platform/OpenApiJsonSchema#makeWithDefs": + replacement: "effect/SchemaRepresentation#toJsonSchemaMultiDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1" + note: "Definitions are returned separately; build a multi-document representation and convert it to OpenAPI 3.1." +"@effect/platform/OpenApiJsonSchema#Numeric": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow numeric interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Object": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow node interfaces were consolidated into the general object model." +"@effect/platform/OpenApiJsonSchema#Ref": + replacement: "effect/JsonSchema#JsonSchema" + note: "The narrow ref interface was consolidated; OpenAPI conversion rewrites definition references." +"@effect/platform/OpenApiJsonSchema#Root": + replacement: "effect/JsonSchema#MultiDocument" + note: "OpenAPI generation keeps roots in schemas and shared components in definitions; the inline-definitions root model is gone." diff --git a/.context/effect/migration/annotations/effect__platform__Path.yaml b/.context/effect/migration/annotations/effect__platform__Path.yaml new file mode 100644 index 000000000..c80325ae5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Path.yaml @@ -0,0 +1,3 @@ +"@effect/platform/Path#TypeId": + replacement: "typeof Path.TypeId" + note: "The module moved to effect/Path; the runtime marker remains exported but the separate type alias was removed." diff --git a/.context/effect/migration/annotations/effect__platform__PlatformConfigProvider.yaml b/.context/effect/migration/annotations/effect__platform__PlatformConfigProvider.yaml new file mode 100644 index 000000000..82b29e988 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__PlatformConfigProvider.yaml @@ -0,0 +1,15 @@ +"@effect/platform/PlatformConfigProvider#fromFileTree": + replacement: "ConfigProvider.fromDir" + note: "The provider moved into effect/ConfigProvider and was renamed; rootDirectory is now rootPath." +"@effect/platform/PlatformConfigProvider#layerDotEnv": + replacement: "ConfigProvider.layer(ConfigProvider.fromDotEnv({ path }))" + note: "Use the v4 dotenv provider effect and install it with ConfigProvider.layer." +"@effect/platform/PlatformConfigProvider#layerDotEnvAdd": + replacement: "ConfigProvider.layerAdd(ConfigProvider.fromDotEnv({ path }))" + note: "Use the v4 dotenv provider effect and compose it with ConfigProvider.layerAdd." +"@effect/platform/PlatformConfigProvider#layerFileTree": + replacement: "ConfigProvider.layer(ConfigProvider.fromDir({ rootPath }))" + note: "Use the renamed directory-tree provider and install it with ConfigProvider.layer." +"@effect/platform/PlatformConfigProvider#layerFileTreeAdd": + replacement: "ConfigProvider.layerAdd(ConfigProvider.fromDir({ rootPath }))" + note: "Use the renamed directory-tree provider and compose it with ConfigProvider.layerAdd." diff --git a/.context/effect/migration/annotations/effect__platform__PlatformLogger.yaml b/.context/effect/migration/annotations/effect__platform__PlatformLogger.yaml new file mode 100644 index 000000000..6e418cd39 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__PlatformLogger.yaml @@ -0,0 +1,3 @@ +"@effect/platform/PlatformLogger": + replacement: "effect/Logger" + note: "toFile moved to Logger.toFile; it still requires a FileSystem service (e.g. NodeFileSystem.layer) and Scope." diff --git a/.context/effect/migration/annotations/effect__platform__Runtime.yaml b/.context/effect/migration/annotations/effect__platform__Runtime.yaml new file mode 100644 index 000000000..750590e00 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Runtime.yaml @@ -0,0 +1,3 @@ +"@effect/platform/Runtime#RunMain": + replacement: "ReturnType" + note: "The standalone interface was removed; derive the runner type from effect/Runtime.makeRunMain. disablePrettyLogger is no longer an option." diff --git a/.context/effect/migration/annotations/effect__platform__Socket.yaml b/.context/effect/migration/annotations/effect__platform__Socket.yaml new file mode 100644 index 000000000..cb71a5141 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Socket.yaml @@ -0,0 +1,27 @@ +"@effect/platform/Socket#CloseEventTypeId": + replacement: "Socket.CloseEvent" + note: "The close-event marker is internal in v4; use the CloseEvent class or Socket.isCloseEvent." +"@effect/platform/Socket#currentSendQueueCapacity": + replacement: "Socket.SendQueueCapacity" + note: "The FiberRef was replaced by a defaulted Context.Reference." +"@effect/platform/Socket#layerWebSocket": + replacement: "Socket.layerWebSocket" + note: "The constructor remains in effect/unstable/socket/Socket; its URL may now also be an Effect." +"@effect/platform/Socket#SocketError": + replacement: "Socket.SocketError" + note: "The old union became a tagged wrapper around SocketReadError, SocketWriteError, SocketOpenError, or SocketCloseError." +"@effect/platform/Socket#SocketErrorTypeId": + replacement: "Socket.SocketErrorTypeId" + note: "The error marker remains in effect/unstable/socket/Socket." +"@effect/platform/Socket#SocketGenericError": + replacement: "Socket.SocketReadError | Socket.SocketWriteError | Socket.SocketOpenError" + note: "The generic reason discriminator was replaced by dedicated read, write, and open error classes." +"@effect/platform/Socket#TypeId": + replacement: "typeof Socket.TypeId" + note: "The socket marker remains exported, but the separate type alias was removed." +"@effect/platform/Socket#WebSocket": + replacement: "Socket.WebSocket" + note: "The opaque service moved to effect/unstable/socket/Socket and is now a Context.Service class for globalThis.WebSocket." +"@effect/platform/Socket#WebSocketConstructor": + replacement: "Socket.WebSocketConstructor" + note: "The service moved to effect/unstable/socket/Socket and is now a Context.Service class." diff --git a/.context/effect/migration/annotations/effect__platform__SocketServer.yaml b/.context/effect/migration/annotations/effect__platform__SocketServer.yaml new file mode 100644 index 000000000..b7a20dbd8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__SocketServer.yaml @@ -0,0 +1,9 @@ +"@effect/platform/SocketServer#ErrorTypeId": + replacement: "SocketServer.ErrorTypeId" + note: "The API moved to effect/unstable/socket/SocketServer and retains this name." +"@effect/platform/SocketServer#TcpAddress": + replacement: "SocketServer.TcpAddress" + note: "The API moved to effect/unstable/socket/SocketServer and retains this name." +"@effect/platform/SocketServer#UnixAddress": + replacement: "SocketServer.UnixAddress" + note: "The API moved to effect/unstable/socket/SocketServer and retains this name." diff --git a/.context/effect/migration/annotations/effect__platform__Template.yaml b/.context/effect/migration/annotations/effect__platform__Template.yaml new file mode 100644 index 000000000..92bdf007f --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Template.yaml @@ -0,0 +1,9 @@ +"@effect/platform/Template#Interpolated.Context": + replacement: "Template.Interpolated.Context" + note: "The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values." +"@effect/platform/Template#Interpolated.Error": + replacement: "Template.Interpolated.Error" + note: "The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values." +"@effect/platform/Template#make": + replacement: "Template.make" + note: "The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values." diff --git a/.context/effect/migration/annotations/effect__platform__Terminal.yaml b/.context/effect/migration/annotations/effect__platform__Terminal.yaml new file mode 100644 index 000000000..9750e3f2a --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Terminal.yaml @@ -0,0 +1,6 @@ +"@effect/platform/Terminal#isQuitException": + replacement: "Terminal.isQuitError" + note: "The quit sentinel was renamed from QuitException to QuitError." +"@effect/platform/Terminal#QuitException": + replacement: "Terminal.QuitError" + note: "The quit sentinel was renamed and moved to effect/Terminal." diff --git a/.context/effect/migration/annotations/effect__platform__Transferable.yaml b/.context/effect/migration/annotations/effect__platform__Transferable.yaml new file mode 100644 index 000000000..02a5415ee --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Transferable.yaml @@ -0,0 +1,12 @@ +"@effect/platform/Transferable#CollectorService": + replacement: "Transferable.Collector[\"Service\"]" + note: "The collector interface is now the service type of the Transferable.Collector Context.Service class." +"@effect/platform/Transferable#schema": + replacement: "Transferable.schema" + note: "The schema wrapper moved to effect/unstable/workers/Transferable and uses the v4 Schema model." +"@effect/platform/Transferable#Uint8Array": + replacement: "Transferable.Uint8Array" + note: "The transferable Uint8Array schema remains in the moved module." +"@effect/platform/Transferable#unsafeMakeCollector": + replacement: "Transferable.makeCollectorUnsafe" + note: "The unsafe collector constructor was renamed." diff --git a/.context/effect/migration/annotations/effect__platform__Url.yaml b/.context/effect/migration/annotations/effect__platform__Url.yaml new file mode 100644 index 000000000..951df8c57 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Url.yaml @@ -0,0 +1,6 @@ +"@effect/platform/Url#setUrlParams": + replacement: "Url.setUrlParams" + note: "Retained and widened to accept UrlParams.Input." +"@effect/platform/Url#urlParams": + replacement: "Url.urlParams" + note: "Retained and returns the v4 UrlParams wrapper." diff --git a/.context/effect/migration/annotations/effect__platform__UrlParams.yaml b/.context/effect/migration/annotations/effect__platform__UrlParams.yaml new file mode 100644 index 000000000..a799f823a --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__UrlParams.yaml @@ -0,0 +1,51 @@ +"@effect/platform/UrlParams#append": + replacement: "UrlParams.append" + note: "Retained and returns the immutable UrlParams wrapper." +"@effect/platform/UrlParams#appendAll": + replacement: "UrlParams.appendAll" + note: "Retained and preserves existing parameters." +"@effect/platform/UrlParams#CoercibleRecord": + replacement: "UrlParams.CoercibleRecord" + note: "The recursive interface became a generic mapped type preserving the input shape." +"@effect/platform/UrlParams#empty": + replacement: "UrlParams.empty" + note: "Now a branded iterable object with params rather than a ReadonlyArray." +"@effect/platform/UrlParams#fromInput": + replacement: "UrlParams.fromInput" + note: "Retained and now also accepts an existing UrlParams." +"@effect/platform/UrlParams#Input": + replacement: "UrlParams.Input" + note: "Retained and broadened to include UrlParams itself." +"@effect/platform/UrlParams#makeUrl": + replacement: "Url.make" + note: "Moved to Url, returns Result, and takes string | undefined for the hash." +"@effect/platform/UrlParams#remove": + replacement: "UrlParams.remove" + note: "Retained and removes every value for the key." +"@effect/platform/UrlParams#schemaFromSelf": + replacement: "UrlParams.UrlParamsSchema" + note: "Renamed to the declaration schema for the v4 wrapper." +"@effect/platform/UrlParams#schemaFromString": + replacement: "Schema.String.pipe(Schema.decodeTo(UrlParams.UrlParamsSchema, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))" + note: "No prebuilt string codec remains; recreate it by transforming between a query string and UrlParams." +"@effect/platform/UrlParams#schemaJson": + replacement: "UrlParams.schemaJsonField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "Compose the field codec with the target schema, then decode it." +"@effect/platform/UrlParams#schemaParse": + replacement: "UrlParamsFromString.pipe(Schema.decodeTo(UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))))" + note: "Recreate the removed helper by composing the string, record, and target codecs." +"@effect/platform/UrlParams#schemaRecord": + replacement: "UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))" + note: "schemaRecord is now a base codec value; compose it with the target schema." +"@effect/platform/UrlParams#schemaStruct": + replacement: "UrlParams.schemaRecord.pipe(Schema.decodeTo(schema), Schema.decodeEffect)" + note: "Compose the record codec with the target schema and decode it." +"@effect/platform/UrlParams#set": + replacement: "UrlParams.set" + note: "Retained and replaces all existing values for the key." +"@effect/platform/UrlParams#setAll": + replacement: "UrlParams.setAll" + note: "Retained; supplied keys replace existing values and other keys remain." +"@effect/platform/UrlParams#toString": + replacement: "UrlParams.toString" + note: "Retained and broadened to accept any UrlParams.Input." diff --git a/.context/effect/migration/annotations/effect__platform__Worker.yaml b/.context/effect/migration/annotations/effect__platform__Worker.yaml new file mode 100644 index 000000000..21e8d6ae2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__Worker.yaml @@ -0,0 +1,66 @@ +"@effect/platform/Worker#BackingWorker": + replacement: "Worker.Worker" + note: "The low-level backing worker became the primary Worker interface with send and run operations." +"@effect/platform/Worker#layerManager": + replacement: "Worker.WorkerPlatform" + note: "WorkerManager was removed; provide the adapter's WorkerPlatform layer directly." +"@effect/platform/Worker#makeManager": + replacement: "Worker.WorkerPlatform" + note: "WorkerManager was removed; obtain WorkerPlatform and call its spawn method." +"@effect/platform/Worker#makePool": + replacement: "Pool + Worker.WorkerPlatform.spawn" + note: "Generic worker pools are no longer built by this module; build a Pool around WorkerPlatform.spawn, or use RpcClient.makeProtocolWorker for RPC workers." +"@effect/platform/Worker#makePoolLayer": + replacement: "RpcClient.layerProtocolWorker" + note: "The standard v4 worker-pool layer is the worker-backed RPC client protocol; compose it with the RPC client layer." +"@effect/platform/Worker#makePoolSerialized": + replacement: "RpcClient.makeProtocolWorker" + note: "Serialized tagged-request workers were replaced by the worker-backed RPC protocol." +"@effect/platform/Worker#makePoolSerializedLayer": + replacement: "RpcClient.layerProtocolWorker" + note: "Serialized tagged-request worker pools were replaced by the worker-backed RPC protocol layer." +"@effect/platform/Worker#makeSerialized": + replacement: "RpcClient with RpcClient.layerProtocolWorker" + note: "Serialized tagged-request execution moved to the v4 RPC model; define an RpcGroup and use the worker protocol." +"@effect/platform/Worker#PlatformWorker": + replacement: "Worker.WorkerPlatform" + note: "The platform service was renamed and is now a Context.Service class." +"@effect/platform/Worker#PlatformWorkerTypeId": + replacement: "none" + note: "The Context.Service class replaces the public platform-worker type-id alias." +"@effect/platform/Worker#SerializedWorker": + replacement: "RpcClient with RpcClient.layerProtocolWorker" + note: "The serialized worker facade was removed; v4 routes schema-defined RPCs through the worker protocol." +"@effect/platform/Worker#SerializedWorker.Options": + replacement: "RpcWorker.layerInitialMessage" + note: "Use RpcWorker.layerInitialMessage when a worker RPC protocol needs schema-encoded initialization." +"@effect/platform/Worker#SerializedWorkerPool": + replacement: "RpcClient.makeProtocolWorker" + note: "The worker-backed RPC protocol owns its worker pool in v4." +"@effect/platform/Worker#SerializedWorkerPool.Options": + replacement: "Parameters[0]" + note: "Pool sizing options moved to the worker RPC protocol; initial messages are provided separately with RpcWorker.layerInitialMessage." +"@effect/platform/Worker#Worker": + replacement: "Worker.Worker" + note: "The name remains in effect/unstable/workers/Worker, but it is now the low-level send/run abstraction rather than execute/executeEffect." +"@effect/platform/Worker#Worker.Options": + replacement: "Worker.Worker[\"run\"] options" + note: "Encoding moved to RPC schemas; the low-level run operation only accepts an optional onSpawn effect." +"@effect/platform/Worker#Worker.Response": + replacement: "none" + note: "The old tagged-request wire response is gone; worker RPC wire messages are internal to RpcClient and RpcServer." +"@effect/platform/Worker#Worker.Span": + replacement: "none" + note: "The explicit span tuple was removed; the RPC worker protocol handles span propagation internally." +"@effect/platform/Worker#WorkerManager": + replacement: "Worker.WorkerPlatform" + note: "WorkerPlatform now spawns low-level Worker values directly, replacing WorkerManager." +"@effect/platform/Worker#WorkerManagerTypeId": + replacement: "none" + note: "The removed WorkerManager has no v4 type-id; WorkerPlatform is a Context.Service class." +"@effect/platform/Worker#WorkerPool": + replacement: "RpcClient.Protocol" + note: "For serialized request/response workloads use the worker-backed RPC Protocol; for raw messages build a Pool around WorkerPlatform.spawn." +"@effect/platform/Worker#WorkerPool.Options": + replacement: "Parameters[0]" + note: "Worker RPC pool sizing is configured on makeProtocolWorker or layerProtocolWorker." diff --git a/.context/effect/migration/annotations/effect__platform__WorkerError.yaml b/.context/effect/migration/annotations/effect__platform__WorkerError.yaml new file mode 100644 index 000000000..52956590e --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__WorkerError.yaml @@ -0,0 +1,6 @@ +"@effect/platform/WorkerError#WorkerErrorFrom": + replacement: "WorkerError.WorkerError" + note: "The old serializable reason object was replaced by WorkerError wrapping dedicated spawn, send, receive, or unknown reason classes." +"@effect/platform/WorkerError#WorkerErrorTypeId": + replacement: "WorkerError.TypeId" + note: "The type-level worker error marker was shortened to TypeId in the moved module." diff --git a/.context/effect/migration/annotations/effect__platform__WorkerRunner.yaml b/.context/effect/migration/annotations/effect__platform__WorkerRunner.yaml new file mode 100644 index 000000000..8991b6766 --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__WorkerRunner.yaml @@ -0,0 +1,54 @@ +"@effect/platform/WorkerRunner#BackingRunner": + replacement: "WorkerRunner.WorkerRunner" + note: "The low-level backing runner became the primary WorkerRunner interface." +"@effect/platform/WorkerRunner#BackingRunner.Message": + replacement: "WorkerRunner.PlatformMessage" + note: "The request/close wire tuple moved to the top-level PlatformMessage type." +"@effect/platform/WorkerRunner#CloseLatch": + replacement: "none" + note: "The public close-latch service was removed; WorkerRunner implementations manage lifetime through their run effect and adapter scope." +"@effect/platform/WorkerRunner#launch": + replacement: "RpcServer.layerProtocolWorkerRunner" + note: "For schema-defined workers, provide the worker-runner RPC protocol and launch the normal RpcServer layer." +"@effect/platform/WorkerRunner#layer": + replacement: "WorkerRunner.WorkerRunnerPlatform.start + WorkerRunner.WorkerRunner.run" + note: "The generic processing layer was removed; use the low-level runner directly or the RpcServer worker protocol." +"@effect/platform/WorkerRunner#layerCloseLatch": + replacement: "none" + note: "The public close-latch layer was removed; adapter runner lifetime is managed internally." +"@effect/platform/WorkerRunner#layerSerialized": + replacement: "RpcServer.layerProtocolWorkerRunner" + note: "Serialized tagged-request handlers moved to RpcGroup handlers served through the worker-runner RPC protocol." +"@effect/platform/WorkerRunner#make": + replacement: "WorkerRunner.WorkerRunnerPlatform.start + WorkerRunner.WorkerRunner.run" + note: "Start the platform runner and register the low-level message handler directly." +"@effect/platform/WorkerRunner#makeSerialized": + replacement: "RpcServer.makeProtocolWorkerRunner" + note: "Serialized tagged-request execution moved to RpcServer with an RpcGroup handler layer." +"@effect/platform/WorkerRunner#PlatformRunner": + replacement: "WorkerRunner.WorkerRunnerPlatform" + note: "The platform service was renamed and is now a Context.Service class." +"@effect/platform/WorkerRunner#PlatformRunnerTypeId": + replacement: "none" + note: "The Context.Service class replaces the public platform-runner type-id alias." +"@effect/platform/WorkerRunner#Runner": + replacement: "WorkerRunner.WorkerRunner" + note: "The namespace-only runner API was replaced by the low-level WorkerRunner interface." +"@effect/platform/WorkerRunner#Runner.Options": + replacement: "none" + note: "The custom decode/encode callbacks were removed; use raw low-level messages or define schemas in the v4 RPC model." +"@effect/platform/WorkerRunner#SerializedRunner": + replacement: "RpcServer with RpcGroup handlers" + note: "The serialized runner namespace was removed in favor of typed Rpc definitions and RpcServer." +"@effect/platform/WorkerRunner#SerializedRunner.Handlers": + replacement: "RpcGroup.HandlersFrom" + note: "Define an RpcGroup and derive its server handler object type with HandlersFrom." +"@effect/platform/WorkerRunner#SerializedRunner.HandlersContext": + replacement: "RpcGroup.HandlersServices" + note: "Derive services required by an RpcGroup handler object with HandlersServices." +"@effect/platform/WorkerRunner#SerializedRunner.InitialContext": + replacement: "none" + note: "Initial-message layer outputs are no longer inferred by this helper; model initialization as normal RpcGroup handler layers and services." +"@effect/platform/WorkerRunner#SerializedRunner.InitialEnv": + replacement: "none" + note: "Initial-message layer inputs are no longer inferred by this helper; model initialization as normal RpcGroup handler layers and services." diff --git a/.context/effect/migration/annotations/effect__platform__index.yaml b/.context/effect/migration/annotations/effect__platform__index.yaml new file mode 100644 index 000000000..ae474455a --- /dev/null +++ b/.context/effect/migration/annotations/effect__platform__index.yaml @@ -0,0 +1,3 @@ +"@effect/platform/index": + replacement: "none" + note: "The package barrel was removed along with the package; import each module from its new effect location (e.g. effect/FileSystem, effect/unstable/http/HttpClient) per the Import Map." diff --git a/.context/effect/migration/annotations/effect__printer-ansi.yaml b/.context/effect/migration/annotations/effect__printer-ansi.yaml new file mode 100644 index 000000000..26e03671d --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer-ansi.yaml @@ -0,0 +1,3 @@ +"@effect/printer-ansi": + replacement: "none" + note: "The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required." diff --git a/.context/effect/migration/annotations/effect__printer-ansi__Ansi.yaml b/.context/effect/migration/annotations/effect__printer-ansi__Ansi.yaml new file mode 100644 index 000000000..08a09ef88 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer-ansi__Ansi.yaml @@ -0,0 +1,3 @@ +"@effect/printer-ansi/Ansi": + replacement: none + note: The @effect/printer-ansi package was removed in v4 with no public replacement. Use a maintained ANSI library or local escape-string helpers; the v4 CLI ANSI helpers are internal and cannot be imported. diff --git a/.context/effect/migration/annotations/effect__printer-ansi__AnsiDoc.yaml b/.context/effect/migration/annotations/effect__printer-ansi__AnsiDoc.yaml new file mode 100644 index 000000000..6b8aa9c5c --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer-ansi__AnsiDoc.yaml @@ -0,0 +1,3 @@ +"@effect/printer-ansi/AnsiDoc": + replacement: none + note: The @effect/printer-ansi package and its annotated document algebra were removed in v4. Use strings or another pretty-printing library; for Effect CLI help only, use HelpDoc with CliOutput from effect/unstable/cli. diff --git a/.context/effect/migration/annotations/effect__printer-ansi__Color.yaml b/.context/effect/migration/annotations/effect__printer-ansi__Color.yaml new file mode 100644 index 000000000..aab454ff4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer-ansi__Color.yaml @@ -0,0 +1,3 @@ +"@effect/printer-ansi/Color": + replacement: none + note: The @effect/printer-ansi package was removed in v4, and Effect no longer provides a public ANSI color ADT. Use a maintained ANSI library or local escape-string helpers. diff --git a/.context/effect/migration/annotations/effect__printer-ansi__index.yaml b/.context/effect/migration/annotations/effect__printer-ansi__index.yaml new file mode 100644 index 000000000..a29ae873a --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer-ansi__index.yaml @@ -0,0 +1,3 @@ +"@effect/printer-ansi/index": + replacement: "none" + note: "The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required." diff --git a/.context/effect/migration/annotations/effect__printer.yaml b/.context/effect/migration/annotations/effect__printer.yaml new file mode 100644 index 000000000..2f14c60c4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer.yaml @@ -0,0 +1,3 @@ +"@effect/printer": + replacement: "none" + note: "The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required." diff --git a/.context/effect/migration/annotations/effect__printer__Doc.yaml b/.context/effect/migration/annotations/effect__printer__Doc.yaml new file mode 100644 index 000000000..7d407cdc9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__Doc.yaml @@ -0,0 +1,3 @@ +"@effect/printer/Doc": + replacement: none + note: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. diff --git a/.context/effect/migration/annotations/effect__printer__DocStream.yaml b/.context/effect/migration/annotations/effect__printer__DocStream.yaml new file mode 100644 index 000000000..af84c3873 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__DocStream.yaml @@ -0,0 +1,3 @@ +"@effect/printer/DocStream": + replacement: none + note: The @effect/printer package and its laid-out DocStream intermediate representation were removed in v4. Use a target-specific renderer or another pretty-printing library. diff --git a/.context/effect/migration/annotations/effect__printer__DocTree.yaml b/.context/effect/migration/annotations/effect__printer__DocTree.yaml new file mode 100644 index 000000000..64e960263 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__DocTree.yaml @@ -0,0 +1,3 @@ +"@effect/printer/DocTree": + replacement: none + note: The @effect/printer package and its structured DocTree rendering representation were removed in v4. Use a target-specific tree and renderer or another pretty-printing library. diff --git a/.context/effect/migration/annotations/effect__printer__Flatten.yaml b/.context/effect/migration/annotations/effect__printer__Flatten.yaml new file mode 100644 index 000000000..4e63575e8 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__Flatten.yaml @@ -0,0 +1,3 @@ +"@effect/printer/Flatten": + replacement: none + note: This printer-specific flattening result was removed with the @effect/printer document algebra in v4 and has no direct replacement. diff --git a/.context/effect/migration/annotations/effect__printer__Layout.yaml b/.context/effect/migration/annotations/effect__printer__Layout.yaml new file mode 100644 index 000000000..a393f185b --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__Layout.yaml @@ -0,0 +1,3 @@ +"@effect/printer/Layout": + replacement: none + note: The @effect/printer layout pipeline was removed in v4 with no general replacement. Use another pretty-printing library; for Effect CLI output only, use CliOutput from effect/unstable/cli. diff --git a/.context/effect/migration/annotations/effect__printer__Optimize.yaml b/.context/effect/migration/annotations/effect__printer__Optimize.yaml new file mode 100644 index 000000000..74cb297f3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__Optimize.yaml @@ -0,0 +1,3 @@ +"@effect/printer/Optimize": + replacement: none + note: The @effect/printer document optimizer was removed with the document algebra in v4. String-based output needs no equivalent optimization stage. diff --git a/.context/effect/migration/annotations/effect__printer__PageWidth.yaml b/.context/effect/migration/annotations/effect__printer__PageWidth.yaml new file mode 100644 index 000000000..d549449d4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__PageWidth.yaml @@ -0,0 +1,3 @@ +"@effect/printer/PageWidth": + replacement: none + note: The @effect/printer page-width layout model was removed in v4 with no direct replacement. Use Terminal.columns for terminal dimensions, or another pretty-printing library for page-width-aware layout. diff --git a/.context/effect/migration/annotations/effect__printer__index.yaml b/.context/effect/migration/annotations/effect__printer__index.yaml new file mode 100644 index 000000000..aaa28f6ad --- /dev/null +++ b/.context/effect/migration/annotations/effect__printer__index.yaml @@ -0,0 +1,3 @@ +"@effect/printer/index": + replacement: "none" + note: "The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required." diff --git a/.context/effect/migration/annotations/effect__rpc.yaml b/.context/effect/migration/annotations/effect__rpc.yaml new file mode 100644 index 000000000..c20087b59 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc.yaml @@ -0,0 +1,3 @@ +"@effect/rpc": + replacement: "effect/unstable/rpc" + note: "The @effect/rpc package was merged into the effect package; import the effect/unstable/rpc barrel or import specific modules directly (e.g. effect/unstable/rpc/)." diff --git a/.context/effect/migration/annotations/effect__rpc__Rpc.yaml b/.context/effect/migration/annotations/effect__rpc__Rpc.yaml new file mode 100644 index 000000000..afbdd05e9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__Rpc.yaml @@ -0,0 +1,75 @@ +"@effect/rpc/Rpc#AddError": + replacement: "effect/unstable/rpc/Rpc#AddError" + note: "Retained; the added error must now be a Schema.Top and the resulting RPC also preserves its explicit service requirements." +"@effect/rpc/Rpc#AddMiddleware": + replacement: "effect/unstable/rpc/Rpc#AddMiddleware" + note: "Retained; middleware is now an RpcMiddleware.AnyService and its provides/requires metadata updates the RPC service requirements." +"@effect/rpc/Rpc#Any": + replacement: "effect/unstable/rpc/Rpc#Any" + note: "Retained as the erased RPC shape; use AnyWithProps when schema and middleware fields are required." +"@effect/rpc/Rpc#AnySchema": + replacement: "Schema.Top" + note: "The RPC-specific erased schema alias was removed; use the v4 top schema constraint." +"@effect/rpc/Rpc#AnyTaggedRequestSchema": + replacement: "none" + note: "RpcGroup no longer converts Schema.TaggedRequest classes into RPCs; declare the contract explicitly with Rpc.make." +"@effect/rpc/Rpc#Context": + replacement: "effect/unstable/rpc/Rpc#Services" + note: "Schema Context became decoding and encoding services; use Services, or ServicesClient / ServicesServer at the corresponding boundary." +"@effect/rpc/Rpc#Error": + replacement: "effect/unstable/rpc/Rpc#Error" + note: "Retained; it includes decoded errors contributed by attached middleware." +"@effect/rpc/Rpc#ErrorEncoded": + replacement: "Rpc.ErrorSchema[\"Encoded\"]" + note: "The alias was removed; index the v4 error schema's Encoded member directly." +"@effect/rpc/Rpc#ErrorExitEncoded": + replacement: "Rpc.ErrorExitSchema[\"Encoded\"]" + note: "Use the new exit error schema, which includes stream and middleware errors, then select its Encoded member." +"@effect/rpc/Rpc#ErrorSchema": + replacement: "effect/unstable/rpc/Rpc#ErrorSchema" + note: "Retained; middleware errors now come from each service's error metadata." +"@effect/rpc/Rpc#fromTaggedRequest": + replacement: "Rpc.make" + note: "Automatic TaggedRequest conversion was removed; pass the tag, payload, success, and error schemas explicitly to Rpc.make." +"@effect/rpc/Rpc#Handler": + replacement: "effect/unstable/rpc/Rpc#Handler" + note: "Retained; handler metadata now supplies ServerClient, RequestId, headers, and the concrete RPC." +"@effect/rpc/Rpc#make": + replacement: "effect/unstable/rpc/Rpc#make" + note: "Retained; schemas use v4 Schema.Top constraints and the defect option accepts Rpc.DefectSchema." +"@effect/rpc/Rpc#Middleware": + replacement: "effect/unstable/rpc/Rpc#Middleware" + note: "Retained and extracts Context.Service identifiers from the attached middleware services." +"@effect/rpc/Rpc#MiddlewareClient": + replacement: "effect/unstable/rpc/Rpc#MiddlewareClient" + note: "Retained; required client middleware is derived from services configured with requiredForClient." +"@effect/rpc/Rpc#Payload": + replacement: "effect/unstable/rpc/Rpc#Payload" + note: "Retained as the decoded payload type; use PayloadConstructor for the input accepted by generated clients." +"@effect/rpc/Rpc#Success": + replacement: "effect/unstable/rpc/Rpc#Success" + note: "Retained as the decoded success type." +"@effect/rpc/Rpc#SuccessChunkEncoded": + replacement: "Rpc.SuccessExitSchema[\"Encoded\"]" + note: "The alias was removed; for a streaming RPC the exit success schema is the stream element schema." +"@effect/rpc/Rpc#SuccessExitEncoded": + replacement: "Rpc.SuccessExitSchema[\"Encoded\"]" + note: "Use the new exit success schema and select its Encoded member; streaming RPC exits use the element schema separately from the terminal void exit." +"@effect/rpc/Rpc#SuccessSchema": + replacement: "effect/unstable/rpc/Rpc#SuccessSchema" + note: "Retained and uses the v4 Schema.Top constraint." +"@effect/rpc/Rpc#Tag": + replacement: "effect/unstable/rpc/Rpc#Tag" + note: "Retained and also accounts for the v4 RPC service-requirement parameter." +"@effect/rpc/Rpc#TypeId": + replacement: "none" + note: "The RPC marker is private in v4; use Rpc.isRpc for runtime checks and Rpc.Any for type constraints." +"@effect/rpc/Rpc#WrapperTypeId": + replacement: "none" + note: "The wrapper marker is private in v4; use Rpc.isWrapper and the public Wrapper type." +"@effect/rpc/Rpc#wrap": + replacement: "effect/unstable/rpc/Rpc#wrap" + note: "Retained after the module move; it still applies fork and uninterruptible handler options, while the return type is now uniformly Rpc.Wrapper." +"@effect/rpc/Rpc#SuccessEncoded": + replacement: "effect/unstable/rpc/Rpc#SuccessEncoded" + note: "Retained after the module move and now accounts for the RPC's explicit service-requirement parameter." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcClient.yaml b/.context/effect/migration/annotations/effect__rpc__RpcClient.yaml new file mode 100644 index 000000000..99a872332 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcClient.yaml @@ -0,0 +1,27 @@ +"@effect/rpc/RpcClient#currentHeaders": + replacement: "effect/unstable/rpc/RpcClient#CurrentHeaders" + note: "Renamed and changed from FiberRef to Context.Reference; prefer RpcClient.withHeaders for scoped overrides." +"@effect/rpc/RpcClient#layerProtocolHttp": + replacement: "effect/unstable/rpc/RpcClient#layerProtocolHttp" + note: "Retained; it provides the v4 Protocol from HttpClient and RpcSerialization." +"@effect/rpc/RpcClient#make": + replacement: "effect/unstable/rpc/RpcClient#make" + note: "Retained; generated calls can now take per-request headers and Context, and include RpcClientError in their error channel." +"@effect/rpc/RpcClient#makeProtocolHttp": + replacement: "effect/unstable/rpc/RpcClient#makeProtocolHttp" + note: "Retained; it creates the Protocol service implementation from an HttpClient." +"@effect/rpc/RpcClient#Protocol": + replacement: "effect/unstable/rpc/RpcClient#Protocol" + note: "Retained as a Context.Service; custom transports now route multiple client ids through run and send." +"@effect/rpc/RpcClient#RpcClient.NonPrefixed": + replacement: "none" + note: "The prefix-partition helper was removed; v4 clients map every RPC tag directly to an object property." +"@effect/rpc/RpcClient#RpcClient.Prefixes": + replacement: "none" + note: "Nested prefix client objects were removed; v4 preserves the full RPC tag as the generated client property." +"@effect/rpc/RpcClient#withHeadersEffect": + replacement: "Effect.flatMap(headers, (value) => RpcClient.withHeaders(effect, value))" + note: "withHeaders now accepts Headers.Input synchronously; evaluate effectful headers first and then scope the client effect." +"@effect/rpc/RpcClient#RpcClient.From": + replacement: "effect/unstable/rpc/RpcClient#RpcClient.From" + note: "Generated clients now preserve full RPC tags as property names, remove the Prefix type parameter, and expose streaming results through the asQueue option instead of asMailbox." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcClientError.yaml b/.context/effect/migration/annotations/effect__rpc__RpcClientError.yaml new file mode 100644 index 000000000..db32a74ef --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcClientError.yaml @@ -0,0 +1,3 @@ +"@effect/rpc/RpcClientError#TypeId": + replacement: "none" + note: "The marker is private in v4; narrow with instanceof RpcClientError or inspect the public _tag." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcGroup.yaml b/.context/effect/migration/annotations/effect__rpc__RpcGroup.yaml new file mode 100644 index 000000000..b26b2af22 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcGroup.yaml @@ -0,0 +1,15 @@ +"@effect/rpc/RpcGroup#HandlerContext": + replacement: "effect/unstable/rpc/RpcGroup#HandlerServices" + note: "Renamed for v4 service terminology and now includes explicit RPC requirements after removing middleware-provided services." +"@effect/rpc/RpcGroup#HandlersContext": + replacement: "effect/unstable/rpc/RpcGroup#HandlersServices" + note: "Renamed; it unions HandlerServices across the handler object." +"@effect/rpc/RpcGroup#make": + replacement: "effect/unstable/rpc/RpcGroup#make" + note: "Retained for explicit Rpc definitions; passing TaggedRequest schema classes for implicit conversion is no longer supported." +"@effect/rpc/RpcGroup#TypeId": + replacement: "none" + note: "The group marker is private in v4; use RpcGroup.Any for an erased group constraint." +"@effect/rpc/RpcGroup#Any": + replacement: "effect/unstable/rpc/RpcGroup#Any" + note: "Moved unchanged as the erased RpcGroup constraint." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcMessage.yaml b/.context/effect/migration/annotations/effect__rpc__RpcMessage.yaml new file mode 100644 index 000000000..16dd4c082 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcMessage.yaml @@ -0,0 +1,6 @@ +"@effect/rpc/RpcMessage#RequestIdTypeId": + replacement: "effect/unstable/rpc/RpcMessage#RequestId" + note: "The public symbol marker was removed; use the branded RequestId type and RequestId constructor rather than inspecting its brand." +"@effect/rpc/RpcMessage#RequestId": + replacement: "effect/unstable/rpc/RpcMessage#RequestId" + note: "Request ids are now branded string or number values; convert bigint ids before calling the retained RequestId constructor." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcMiddleware.yaml b/.context/effect/migration/annotations/effect__rpc__RpcMiddleware.yaml new file mode 100644 index 000000000..3fad622b6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcMiddleware.yaml @@ -0,0 +1,45 @@ +"@effect/rpc/RpcMiddleware#ForClient": + replacement: "effect/unstable/rpc/RpcMiddleware#ForClient" + note: "Retained as the marker requirement provided by a client middleware layer." +"@effect/rpc/RpcMiddleware#layerClient": + replacement: "effect/unstable/rpc/RpcMiddleware#layerClient" + note: "Retained; the client function can now modify the typed Request passed to next and carry a client-only error type." +"@effect/rpc/RpcMiddleware#RpcMiddlewareWrap": + replacement: "effect/unstable/rpc/RpcMiddleware#RpcMiddleware" + note: "The wrap and non-wrap shapes were unified; implement a function receiving the handler effect and request options." +"@effect/rpc/RpcMiddleware#Tag": + replacement: "effect/unstable/rpc/RpcMiddleware#Service" + note: "Renamed and redesigned with explicit requires, provides, clientError, error, and requiredForClient configuration." +"@effect/rpc/RpcMiddleware#TagClass": + replacement: "effect/unstable/rpc/RpcMiddleware#ServiceClass" + note: "Renamed class type for the v4 Context.Service-based middleware declaration." +"@effect/rpc/RpcMiddleware#TagClass.Failure": + replacement: "effect/unstable/rpc/RpcMiddleware#Error" + note: "Failure terminology became error; apply the extractor to the middleware ID." +"@effect/rpc/RpcMiddleware#TagClass.FailureContext": + replacement: "effect/unstable/rpc/RpcMiddleware#ErrorServicesEncode / ErrorServicesDecode" + note: "The single schema context split into server encoding and client decoding services." +"@effect/rpc/RpcMiddleware#TagClass.FailureSchema": + replacement: "effect/unstable/rpc/RpcMiddleware#ErrorSchema" + note: "Renamed and applied to the middleware ID rather than constructor options." +"@effect/rpc/RpcMiddleware#TagClass.FailureService": + replacement: "effect/unstable/rpc/RpcMiddleware#Error" + note: "Use the decoded error extractor; optional middleware fallback was removed." +"@effect/rpc/RpcMiddleware#TagClass.Optional": + replacement: "none" + note: "Optional declaration and fallback-on-failure behavior were removed; model fallback inside the middleware effect." +"@effect/rpc/RpcMiddleware#TagClass.Provides": + replacement: "effect/unstable/rpc/RpcMiddleware#Provides" + note: "Moved to the module level and applied to the middleware ID metadata." +"@effect/rpc/RpcMiddleware#TagClass.RequiredForClient": + replacement: "RpcMiddleware.ServiceClass[\"requiredForClient\"]" + note: "The standalone options extractor was removed; the boolean is exposed directly by the resulting service class." +"@effect/rpc/RpcMiddleware#TagClassAny": + replacement: "effect/unstable/rpc/RpcMiddleware#AnyService" + note: "Renamed widened middleware service-key shape." +"@effect/rpc/RpcMiddleware#TagClassAnyWithProps": + replacement: "effect/unstable/rpc/RpcMiddleware#AnyServiceWithProps" + note: "Renamed erased service key whose value has the unified server middleware function shape." +"@effect/rpc/RpcMiddleware#TypeId": + replacement: "effect/unstable/rpc/RpcMiddleware#TypeId" + note: "Retained as the public middleware metadata marker and now has a string-literal type." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcSchema.yaml b/.context/effect/migration/annotations/effect__rpc__RpcSchema.yaml new file mode 100644 index 000000000..6164045df --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcSchema.yaml @@ -0,0 +1,15 @@ +"@effect/rpc/RpcSchema#getStreamSchemas": + replacement: "effect/unstable/rpc/RpcSchema#getStreamSchemas" + note: "Retained for internal-style schema inspection; pass the schema itself rather than its AST." +"@effect/rpc/RpcSchema#isStreamSchema": + replacement: "effect/unstable/rpc/RpcSchema#isStreamSchema" + note: "Retained; it accepts a v4 Schema.Constraint." +"@effect/rpc/RpcSchema#isStreamSerializable": + replacement: "RpcSchema.isStreamSchema(schema)" + note: "The separate WithResult serializability predicate was removed; v4 RPC streaming is identified by its explicit Stream schema." +"@effect/rpc/RpcSchema#Stream": + replacement: "effect/unstable/rpc/RpcSchema#Stream" + note: "Retained as both the stream schema interface and constructor; error is the second argument and schema services are split by direction." +"@effect/rpc/RpcSchema#StreamSchemaId": + replacement: "none" + note: "The stream marker is private in v4; use RpcSchema.isStreamSchema and getStreamSchemas." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcServer.yaml b/.context/effect/migration/annotations/effect__rpc__RpcServer.yaml new file mode 100644 index 000000000..2a2258dd5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcServer.yaml @@ -0,0 +1,51 @@ +"@effect/rpc/RpcServer#fiberIdClientInterrupt": + replacement: "effect/unstable/rpc/RpcSchema#ClientAbort" + note: "The sentinel FiberId was replaced by a Cause annotation; inspect ClientAbort in the interruption cause when client cancellation must be distinguished." +"@effect/rpc/RpcServer#fiberIdTransientInterrupt": + replacement: "none" + note: "The internal transient sentinel was removed; protocol shutdown and disconnect now interrupt with the active parent fiber identity." +"@effect/rpc/RpcServer#layerHttpRouter": + replacement: "effect/unstable/rpc/RpcServer#layerHttp" + note: "Renamed; it installs an HTTP or WebSocket RPC route into the v4 HttpRouter service." +"@effect/rpc/RpcServer#layerProtocolHttp": + replacement: "effect/unstable/rpc/RpcServer#layerProtocolHttp" + note: "Retained; v4 has one HttpRouter service and no router tag option." +"@effect/rpc/RpcServer#layerProtocolHttpRouter": + replacement: "effect/unstable/rpc/RpcServer#layerProtocolHttp" + note: "The separate layer-router variant was unified with layerProtocolHttp." +"@effect/rpc/RpcServer#layerProtocolWebsocketRouter": + replacement: "effect/unstable/rpc/RpcServer#layerProtocolWebsocket" + note: "Renamed after the HTTP router services were unified." +"@effect/rpc/RpcServer#make": + replacement: "effect/unstable/rpc/RpcServer#make" + note: "Retained; schema encoding services are now explicit server requirements." +"@effect/rpc/RpcServer#makeProtocolHttp": + replacement: "effect/unstable/rpc/RpcServer#makeProtocolHttp" + note: "Retained; it registers a POST route in the current v4 HttpRouter." +"@effect/rpc/RpcServer#makeProtocolHttpRouter": + replacement: "effect/unstable/rpc/RpcServer#makeProtocolHttp" + note: "The separate router constructor was unified with makeProtocolHttp." +"@effect/rpc/RpcServer#makeProtocolWebsocketRouter": + replacement: "effect/unstable/rpc/RpcServer#makeProtocolWebsocket" + note: "Renamed after the HTTP router services were unified." +"@effect/rpc/RpcServer#makeProtocolWithHttpApp": + replacement: "effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffect" + note: "HttpApp became HttpEffect; the result contains protocol and httpEffect." +"@effect/rpc/RpcServer#makeProtocolWithHttpAppWebsocket": + replacement: "effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffectWebsocket" + note: "HttpApp became HttpEffect; the result contains the WebSocket protocol and upgrade effect." +"@effect/rpc/RpcServer#Protocol": + replacement: "effect/unstable/rpc/RpcServer#Protocol" + note: "Retained as a Context.Service; custom transports now expose a disconnect queue and explicit capability flags." +"@effect/rpc/RpcServer#toHttpApp": + replacement: "effect/unstable/rpc/RpcServer#toHttpEffect" + note: "Renamed for the v4 HTTP effect model; it starts the RPC server and returns the request effect." +"@effect/rpc/RpcServer#toHttpAppWebsocket": + replacement: "effect/unstable/rpc/RpcServer#toHttpEffectWebsocket" + note: "Renamed for the v4 HTTP effect model; it returns the WebSocket upgrade effect." +"@effect/rpc/RpcServer#toWebHandler": + replacement: "HttpRouter.toWebHandler(RpcServer.layerHttp(options).pipe(Layer.provide(options.layer)))" + note: "The RPC convenience wrapper was removed; build the RPC route layer and convert it with the generic v4 HttpRouter web-handler adapter." +"@effect/rpc/RpcServer#layer": + replacement: "effect/unstable/rpc/RpcServer#layer" + note: "Moved to core Effect; server requirements are now derived with Rpc.ServicesServer rather than the former combined Rpc.Context alias." diff --git a/.context/effect/migration/annotations/effect__rpc__RpcTest.yaml b/.context/effect/migration/annotations/effect__rpc__RpcTest.yaml new file mode 100644 index 000000000..caa66472b --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__RpcTest.yaml @@ -0,0 +1,3 @@ +"@effect/rpc/RpcTest#makeClient": + replacement: "effect/unstable/rpc/RpcTest#makeClient" + note: "Retained; it uses the v4 no-serialization client/server path and requires handlers plus any server and client middleware services." diff --git a/.context/effect/migration/annotations/effect__rpc__index.yaml b/.context/effect/migration/annotations/effect__rpc__index.yaml new file mode 100644 index 000000000..3387ce048 --- /dev/null +++ b/.context/effect/migration/annotations/effect__rpc__index.yaml @@ -0,0 +1,3 @@ +"@effect/rpc/index": + replacement: "effect/unstable/rpc" + note: "The package barrel was removed; import the same namespaces from the effect/unstable/rpc barrel or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml b/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml new file mode 100644 index 000000000..2453212dd --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseClient.yaml @@ -0,0 +1,12 @@ +"@effect/sql-clickhouse/ClickhouseClient#ClickhouseClient": + replacement: "@effect/sql-clickhouse/ClickhouseClient#ClickhouseClient" + note: "Retained; the service value is now a Context.Service rather than a GenericTag." +"@effect/sql-clickhouse/ClickhouseClient#currentClickhouseSettings": + replacement: "@effect/sql-clickhouse/ClickhouseClient#ClickhouseSettings" + note: "Renamed and changed from FiberRef to Context.Reference; prefer client.withClickhouseSettings or provide the reference as a service." +"@effect/sql-clickhouse/ClickhouseClient#currentClientMethod": + replacement: "@effect/sql-clickhouse/ClickhouseClient#ClientMethod" + note: "Renamed and changed from FiberRef to Context.Reference; prefer client.asCommand or provide the reference as a service." +"@effect/sql-clickhouse/ClickhouseClient#currentQueryId": + replacement: "@effect/sql-clickhouse/ClickhouseClient#QueryId" + note: "Renamed and changed from FiberRef to Context.Reference; prefer client.withQueryId or provide the reference as a service." diff --git a/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml b/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml new file mode 100644 index 000000000..c773d443f --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-clickhouse__ClickhouseMigrator.yaml @@ -0,0 +1,3 @@ +"@effect/sql-clickhouse/ClickhouseMigrator#MigrationError": + replacement: "@effect/sql-clickhouse/ClickhouseMigrator#MigrationError" + note: "Retained via effect/unstable/sql/Migrator; migrate reason and its lowercase values to kind with PascalCase values." diff --git a/.context/effect/migration/annotations/effect__sql-clickhouse__index.yaml b/.context/effect/migration/annotations/effect__sql-clickhouse__index.yaml new file mode 100644 index 000000000..61fd8dd3f --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-clickhouse__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-clickhouse/index": + replacement: "@effect/sql-clickhouse" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-clickhouse package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-d1__D1Client.yaml b/.context/effect/migration/annotations/effect__sql-d1__D1Client.yaml new file mode 100644 index 000000000..a73f16358 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-d1__D1Client.yaml @@ -0,0 +1,3 @@ +"@effect/sql-d1/D1Client#D1ClientConfig": + replacement: "@effect/sql-d1/D1Client#D1ClientConfig" + note: "Retained; prepareCacheTTL now uses Duration.Input." diff --git a/.context/effect/migration/annotations/effect__sql-d1__index.yaml b/.context/effect/migration/annotations/effect__sql-d1__index.yaml new file mode 100644 index 000000000..a0299ad51 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-d1__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-d1/index": + replacement: "@effect/sql-d1" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-d1 package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-drizzle__Mysql.yaml b/.context/effect/migration/annotations/effect__sql-drizzle__Mysql.yaml new file mode 100644 index 000000000..963da1e61 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-drizzle__Mysql.yaml @@ -0,0 +1,15 @@ +"@effect/sql-drizzle/Mysql#layer": + replacement: "Layer.effect(AppDb, MysqlDrizzle.makeWithDefaults())" + note: "The package was removed; import MysqlDrizzle from drizzle-orm/effect-mysql2, define an application service tag, and compose with MysqlClient.layer." +"@effect/sql-drizzle/Mysql#layerWithConfig": + replacement: "Layer.effect(AppDb, MysqlDrizzle.makeWithDefaults(config))" + note: "The package was removed; use drizzle-orm/effect-mysql2, port config to EffectDrizzleMySqlConfig, and define an application service tag." +"@effect/sql-drizzle/Mysql#make": + replacement: "drizzle-orm/effect-mysql2#makeWithDefaults" + note: "Use Drizzle's Effect 4 integration; it returns EffectMysql2Database and requires MysqlClient." +"@effect/sql-drizzle/Mysql#makeWithConfig": + replacement: "drizzle-orm/effect-mysql2#makeWithDefaults" + note: "The constructor split was removed; use makeWithDefaults(config), or make(config) when explicitly providing logger and cache services." +"@effect/sql-drizzle/Mysql#MysqlDrizzle": + replacement: "drizzle-orm/effect-mysql2#EffectMysql2Database" + note: "The service tag was removed; use the database type and define an application Context.Tag if service access is required." diff --git a/.context/effect/migration/annotations/effect__sql-drizzle__Pg.yaml b/.context/effect/migration/annotations/effect__sql-drizzle__Pg.yaml new file mode 100644 index 000000000..a435b5544 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-drizzle__Pg.yaml @@ -0,0 +1,15 @@ +"@effect/sql-drizzle/Pg#layer": + replacement: "Layer.effect(AppDb, PgDrizzle.makeWithDefaults())" + note: "The package was removed; import PgDrizzle from drizzle-orm/effect-postgres, define an application service tag, and compose with PgClient.layer." +"@effect/sql-drizzle/Pg#layerWithConfig": + replacement: "Layer.effect(AppDb, PgDrizzle.makeWithDefaults(config))" + note: "The package was removed; use drizzle-orm/effect-postgres, port config to EffectDrizzlePgConfig, and define an application service tag." +"@effect/sql-drizzle/Pg#make": + replacement: "drizzle-orm/effect-postgres#makeWithDefaults" + note: "Use Drizzle's Effect 4 integration; it returns EffectPgDatabase and requires PgClient." +"@effect/sql-drizzle/Pg#makeWithConfig": + replacement: "drizzle-orm/effect-postgres#makeWithDefaults" + note: "The constructor split was removed; use makeWithDefaults(config), or make(config) when explicitly providing logger and cache services." +"@effect/sql-drizzle/Pg#PgDrizzle": + replacement: "drizzle-orm/effect-postgres#EffectPgDatabase" + note: "The service tag was removed; use the database type and define an application Context.Tag if service access is required." diff --git a/.context/effect/migration/annotations/effect__sql-drizzle__Sqlite.yaml b/.context/effect/migration/annotations/effect__sql-drizzle__Sqlite.yaml new file mode 100644 index 000000000..7a1f5fc35 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-drizzle__Sqlite.yaml @@ -0,0 +1,15 @@ +"@effect/sql-drizzle/Sqlite#layer": + replacement: "Layer.effect(AppDb, SqliteDrizzle.makeWithDefaults())" + note: "The package was removed; select the matching drizzle-orm Effect backend module, define an application service tag, and compose with its SQL client layer." +"@effect/sql-drizzle/Sqlite#layerWithConfig": + replacement: "Layer.effect(AppDb, SqliteDrizzle.makeWithDefaults(config))" + note: "Select the matching drizzle-orm Effect backend, port config to EffectDrizzleSQLiteConfig, and define an application service tag." +"@effect/sql-drizzle/Sqlite#make": + replacement: "drizzle-orm/effect-sqlite-node#makeWithDefaults" + note: "SQLite integration is backend-specific; use the module matching sql-sqlite-node, -bun, -do, -wasm, libsql, or d1." +"@effect/sql-drizzle/Sqlite#makeWithConfig": + replacement: "matching drizzle-orm Effect SQLite module#makeWithDefaults" + note: "The generic constructor was removed; select the concrete backend and use makeWithDefaults(config), or make(config) with explicit services." +"@effect/sql-drizzle/Sqlite#SqliteDrizzle": + replacement: "matching drizzle-orm Effect SQLite database type" + note: "The generic service tag was removed; use the backend-specific database type and define an application Context.Tag if needed." diff --git a/.context/effect/migration/annotations/effect__sql-kysely__Kysely.yaml b/.context/effect/migration/annotations/effect__sql-kysely__Kysely.yaml new file mode 100644 index 000000000..6846ace99 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__Kysely.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/Kysely": + replacement: none + note: The Effect integration was removed. Use native kysely#Kysely, explicitly wrap promise execution with Effect.tryPromise, and define an application service if needed. No Effect-native equivalent remains; construct native new Kysely(config) and explicitly wrap builder execution and errors with Effect.tryPromise. diff --git a/.context/effect/migration/annotations/effect__sql-kysely__Mssql.yaml b/.context/effect/migration/annotations/effect__sql-kysely__Mssql.yaml new file mode 100644 index 000000000..1c8b0f2e4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__Mssql.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/Mssql": + replacement: none + note: The integration was removed; use native Kysely with MssqlDialect and wrap promises, or rewrite against @effect/sql-mssql for Effect-native queries. diff --git a/.context/effect/migration/annotations/effect__sql-kysely__Mysql.yaml b/.context/effect/migration/annotations/effect__sql-kysely__Mysql.yaml new file mode 100644 index 000000000..40b3de2d2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__Mysql.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/Mysql": + replacement: none + note: The integration was removed; use native Kysely with MysqlDialect and wrap promises, or rewrite against @effect/sql-mysql2 for Effect-native queries. diff --git a/.context/effect/migration/annotations/effect__sql-kysely__Pg.yaml b/.context/effect/migration/annotations/effect__sql-kysely__Pg.yaml new file mode 100644 index 000000000..b0caeee56 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__Pg.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/Pg": + replacement: none + note: The integration was removed; use native Kysely with PostgresDialect and wrap promises, or rewrite against @effect/sql-pg for Effect-native queries. diff --git a/.context/effect/migration/annotations/effect__sql-kysely__Sqlite.yaml b/.context/effect/migration/annotations/effect__sql-kysely__Sqlite.yaml new file mode 100644 index 000000000..868b4e6d7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__Sqlite.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/Sqlite": + replacement: none + note: The integration was removed; use native Kysely with SqliteDialect and wrap promises, or rewrite against a matching @effect/sql-sqlite-* client. diff --git a/.context/effect/migration/annotations/effect__sql-kysely__patch.types.yaml b/.context/effect/migration/annotations/effect__sql-kysely__patch.types.yaml new file mode 100644 index 000000000..7d18812ba --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-kysely__patch.types.yaml @@ -0,0 +1,3 @@ +"@effect/sql-kysely/patch.types": + replacement: "none" + note: "The @effect/sql-kysely package was removed in v4 along with its kysely type patches; depend on native kysely types directly and wrap query execution with Effect.tryPromise." diff --git a/.context/effect/migration/annotations/effect__sql-libsql__index.yaml b/.context/effect/migration/annotations/effect__sql-libsql__index.yaml new file mode 100644 index 000000000..e2efef269 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-libsql__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-libsql/index": + replacement: "@effect/sql-libsql" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-libsql package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-mssql__MssqlClient.yaml b/.context/effect/migration/annotations/effect__sql-mssql__MssqlClient.yaml new file mode 100644 index 000000000..31b4626f0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mssql__MssqlClient.yaml @@ -0,0 +1,6 @@ +"@effect/sql-mssql/MssqlClient#MssqlClient": + replacement: "@effect/sql-mssql/MssqlClient#MssqlClient" + note: "Retained; the service value is now a Context.Service rather than a GenericTag." +"@effect/sql-mssql/MssqlClient#MssqlClientConfig": + replacement: "@effect/sql-mssql/MssqlClient#MssqlClientConfig" + note: "Retained; durations use Duration.Input, parameterTypes is keyed by Statement.PrimitiveKind, and v4 adds retry and timeout options." diff --git a/.context/effect/migration/annotations/effect__sql-mssql__Parameter.yaml b/.context/effect/migration/annotations/effect__sql-mssql__Parameter.yaml new file mode 100644 index 000000000..12e910afc --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mssql__Parameter.yaml @@ -0,0 +1,6 @@ +"@effect/sql-mssql/Parameter#Parameter": + replacement: "@effect/sql-mssql/Parameter#Parameter" + note: "Retained; the phantom brand key was renamed from ParameterId to TypeId." +"@effect/sql-mssql/Parameter#ParameterId": + replacement: "@effect/sql-mssql/Parameter#TypeId" + note: "Renamed; use TypeId for direct brand-key and type references." diff --git a/.context/effect/migration/annotations/effect__sql-mssql__Procedure.yaml b/.context/effect/migration/annotations/effect__sql-mssql__Procedure.yaml new file mode 100644 index 000000000..611b275ee --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mssql__Procedure.yaml @@ -0,0 +1,6 @@ +"@effect/sql-mssql/Procedure#Procedure": + replacement: "@effect/sql-mssql/Procedure#Procedure" + note: "Retained with the same generics and fields." +"@effect/sql-mssql/Procedure#Procedure.ParametersRecord": + replacement: "@effect/sql-mssql/Procedure#Procedure.ParametersRecord" + note: "Retained unchanged; from the deep module it is also available as Procedure.ParametersRecord." diff --git a/.context/effect/migration/annotations/effect__sql-mssql__index.yaml b/.context/effect/migration/annotations/effect__sql-mssql__index.yaml new file mode 100644 index 000000000..56292676b --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mssql__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-mssql/index": + replacement: "@effect/sql-mssql" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-mssql package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-mysql2__MysqlClient.yaml b/.context/effect/migration/annotations/effect__sql-mysql2__MysqlClient.yaml new file mode 100644 index 000000000..817cc7ed0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mysql2__MysqlClient.yaml @@ -0,0 +1,3 @@ +"@effect/sql-mysql2/MysqlClient#MysqlClientConfig": + replacement: "@effect/sql-mysql2/MysqlClient#MysqlClientConfig" + note: "Retained; connectionTTL uses Duration.Input and v4 adds disablePreparedStatements." diff --git a/.context/effect/migration/annotations/effect__sql-mysql2__index.yaml b/.context/effect/migration/annotations/effect__sql-mysql2__index.yaml new file mode 100644 index 000000000..81c416a1d --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-mysql2__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-mysql2/index": + replacement: "@effect/sql-mysql2" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-mysql2 package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-pg__PgClient.yaml b/.context/effect/migration/annotations/effect__sql-pg__PgClient.yaml new file mode 100644 index 000000000..f74fad15a --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-pg__PgClient.yaml @@ -0,0 +1,12 @@ +"@effect/sql-pg/PgClient#layerFromPool": + replacement: "PgClient.layerFrom(PgClient.fromPool(options))" + note: "Compose fromPool with layerFrom; layerFrom now accepts an Effect acquiring a PgClient rather than pool options." +"@effect/sql-pg/PgClient#PgClient": + replacement: "@effect/sql-pg/PgClient#PgClient" + note: "Retained; the service value is now a Context.Service." +"@effect/sql-pg/PgClient#PgClientConfig": + replacement: "@effect/sql-pg/PgClient#PgClientConfig / PgPoolConfig" + note: "Use PgClientConfig for base settings and PgPoolConfig for make/layer; pool sizing, idle timeout, and connection TTL moved to PgPoolConfig." +"@effect/sql-pg/PgClient#PgClientFromPoolOptions": + replacement: "Parameters[0]" + note: "The named type was removed; derive the inline fromPool option type. PgPoolConfig is for creating a managed pool and is not equivalent." diff --git a/.context/effect/migration/annotations/effect__sql-pg__index.yaml b/.context/effect/migration/annotations/effect__sql-pg__index.yaml new file mode 100644 index 000000000..ad7fcc5a6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-pg__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-pg/index": + replacement: "@effect/sql-pg" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-pg package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml new file mode 100644 index 000000000..d936bcb8c --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-bun__SqliteClient.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-bun/SqliteClient#SqliteClient": + replacement: "@effect/sql-sqlite-bun/SqliteClient#SqliteClient" + note: "Retained; the service value is now a Context.Service." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-bun__index.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-bun__index.yaml new file mode 100644 index 000000000..2104c4ec6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-bun__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-bun/index": + replacement: "@effect/sql-sqlite-bun" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-bun package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml new file mode 100644 index 000000000..aabf0cd66 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-do__SqliteClient.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-do/SqliteClient#SqliteClientConfig": + replacement: "@effect/sql-sqlite-do/SqliteClient#SqliteClientConfig" + note: "Retained; db is optional and storage may be supplied, but one of db or storage is required at runtime." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-do__index.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-do__index.yaml new file mode 100644 index 000000000..1ce99cfef --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-do__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-do/index": + replacement: "@effect/sql-sqlite-do" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-do package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml new file mode 100644 index 000000000..5bbf9cfbe --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-node__SqliteClient.yaml @@ -0,0 +1,6 @@ +"@effect/sql-sqlite-node/SqliteClient#SqliteClient": + replacement: "@effect/sql-sqlite-node/SqliteClient#SqliteClient" + note: "Retained on node:sqlite, but the byte-export member was removed; use backup(destination) for file backup." +"@effect/sql-sqlite-node/SqliteClient#SqliteClientConfig": + replacement: "@effect/sql-sqlite-node/SqliteClient#SqliteClientConfig" + note: "Retained; prepareCacheTTL now uses Duration.Input." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-node__index.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-node__index.yaml new file mode 100644 index 000000000..415dbb0d0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-node__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-node/index": + replacement: "@effect/sql-sqlite-node" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-node package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml new file mode 100644 index 000000000..d46613df6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-react-native__SqliteClient.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-react-native/SqliteClient#asyncQuery": + replacement: "@effect/sql-sqlite-react-native/SqliteClient#AsyncQuery" + note: "Renamed and changed from FiberRef to Context.Reference; prefer withAsyncQuery or provide AsyncQuery as a service." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-react-native__index.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-react-native__index.yaml new file mode 100644 index 000000000..45a5fa68c --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-react-native__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-react-native/index": + replacement: "@effect/sql-sqlite-react-native" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-react-native package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml new file mode 100644 index 000000000..64cf9a2ff --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-wasm__SqliteClient.yaml @@ -0,0 +1,6 @@ +"@effect/sql-sqlite-wasm/SqliteClient#currentTransferables": + replacement: "@effect/sql-sqlite-wasm/SqliteClient#Transferables" + note: "Renamed and changed from FiberRef to Context.Reference; prefer withTransferables or provide Transferables as a service." +"@effect/sql-sqlite-wasm/SqliteClient#SqliteClient": + replacement: "@effect/sql-sqlite-wasm/SqliteClient#SqliteClient" + note: "Retained with the same export/import surface; the service value is now a Context.Service." diff --git a/.context/effect/migration/annotations/effect__sql-sqlite-wasm__index.yaml b/.context/effect/migration/annotations/effect__sql-sqlite-wasm__index.yaml new file mode 100644 index 000000000..a649b2af9 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql-sqlite-wasm__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql-sqlite-wasm/index": + replacement: "@effect/sql-sqlite-wasm" + note: "The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-wasm package root or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__sql.yaml b/.context/effect/migration/annotations/effect__sql.yaml new file mode 100644 index 000000000..613890f1d --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql.yaml @@ -0,0 +1,3 @@ +"@effect/sql": + replacement: "effect/unstable/sql" + note: "The @effect/sql package was merged into the effect package; import the effect/unstable/sql barrel or import specific modules directly (e.g. effect/unstable/sql/)." diff --git a/.context/effect/migration/annotations/effect__sql__Migrator.yaml b/.context/effect/migration/annotations/effect__sql__Migrator.yaml new file mode 100644 index 000000000..3ca711ee6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__Migrator.yaml @@ -0,0 +1,3 @@ +"@effect/sql/Migrator#make": + replacement: "effect/unstable/sql/Migrator#make" + note: "Moved with the same curried make({ dumpSchema })(options) pattern." diff --git a/.context/effect/migration/annotations/effect__sql__Migrator__FileSystem.yaml b/.context/effect/migration/annotations/effect__sql__Migrator__FileSystem.yaml new file mode 100644 index 000000000..10a624a97 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__Migrator__FileSystem.yaml @@ -0,0 +1,3 @@ +"@effect/sql/Migrator/FileSystem": + replacement: "effect/unstable/sql/Migrator" + note: "fromFileSystem was merged into the main Migrator module with the same (directory) signature; use Migrator.fromFileSystem as the loader." diff --git a/.context/effect/migration/annotations/effect__sql__Model.yaml b/.context/effect/migration/annotations/effect__sql__Model.yaml new file mode 100644 index 000000000..00db99efc --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__Model.yaml @@ -0,0 +1,33 @@ +"@effect/sql/Model#Any": + replacement: "effect/unstable/schema/Model#Any" + note: "Moved; v4 schemas track DecodingServices and EncodingServices separately instead of one Context type." +"@effect/sql/Model#AnyNoContext": + replacement: "effect/unstable/schema/Model#Any" + note: "The distinct no-context alias was removed; Model.Any propagates decoding and encoding services. Constrain both service types to never when required." +"@effect/sql/Model#BooleanFromNumber": + replacement: "effect/Schema#BooleanFromBit" + note: "Use the core 0 | 1 to boolean schema; Model.BooleanSqlite is the ready-made model field." +"@effect/sql/Model#Class": + replacement: "effect/unstable/schema/Model#Class" + note: "Moved; model variants remain select, insert, update, json, jsonCreate, and jsonUpdate." +"@effect/sql/Model#DateTimeFromDate": + replacement: "effect/Schema#DateTimeUtcFromDate" + note: "Moved to core Schema and retains Date to DateTime.Utc conversion." +"@effect/sql/Model#extract": + replacement: "effect/unstable/schema/Model#extract" + note: "Retained after moving the model variant helpers into core Effect's unstable schema package." +"@effect/sql/Model#fieldFromKey": + replacement: "effect/Schema#encodeKeys" + note: "The field helper was removed; apply encodeKeys to each concrete struct or model-variant schema that crosses the naming boundary." +"@effect/sql/Model#Generated": + replacement: "effect/unstable/schema/Model#GeneratedByDb" + note: "Renamed and now read-only, with select and json variants only. Use Model.Field with select, update, and json to preserve writable v3 behavior." +"@effect/sql/Model#makeDataLoaders": + replacement: "effect/unstable/sql/SqlModel#makeResolvers" + note: "Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls." +"@effect/sql/Model#Override": + replacement: "effect/unstable/schema/Model#Override" + note: "Moved with the same explicit-default override purpose." +"@effect/sql/Model#fields": + replacement: "effect/unstable/schema/Model#fields" + note: "Moved with the variant-model helpers into core Effect's unstable schema package." diff --git a/.context/effect/migration/annotations/effect__sql__SqlClient.yaml b/.context/effect/migration/annotations/effect__sql__SqlClient.yaml new file mode 100644 index 000000000..e97901a03 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlClient.yaml @@ -0,0 +1,12 @@ +"@effect/sql/SqlClient#make": + replacement: "effect/unstable/sql/SqlClient#make" + note: "Moved; custom clients rename MakeOptions.reactiveMailbox to reactiveQueue and may supply transactionService." +"@effect/sql/SqlClient#SafeIntegers": + replacement: "effect/unstable/sql/SqlClient#SafeIntegers" + note: "Moved and changed from a Reference subclass to a Context.Reference value; provide it as a service." +"@effect/sql/SqlClient#TransactionConnection": + replacement: "effect/unstable/sql/SqlClient#TransactionConnection" + note: "Now a factory keyed by client id, not a singleton tag. Prefer the client's transactionService; the payload type is TransactionConnection.Service." +"@effect/sql/SqlClient#TypeId": + replacement: "none" + note: "The brand is private in v4; do not inspect or attach it, and obtain clients through SqlClient or SqlClient.make." diff --git a/.context/effect/migration/annotations/effect__sql__SqlConnection.yaml b/.context/effect/migration/annotations/effect__sql__SqlConnection.yaml new file mode 100644 index 000000000..56a7889ac --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlConnection.yaml @@ -0,0 +1,3 @@ +"@effect/sql/SqlConnection#Connection": + replacement: "effect/unstable/sql/SqlConnection#Connection" + note: "Moved; Connection.Acquirer is now top-level SqlConnection.Acquirer, and custom connections must implement executeValuesUnprepared." diff --git a/.context/effect/migration/annotations/effect__sql__SqlError.yaml b/.context/effect/migration/annotations/effect__sql__SqlError.yaml new file mode 100644 index 000000000..50b119fac --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlError.yaml @@ -0,0 +1,3 @@ +"@effect/sql/SqlError#SqlErrorTypeId": + replacement: "effect/unstable/sql/SqlError#isSqlError" + note: "The type id is private; use isSqlError for runtime narrowing or isSqlErrorReason for structured reason values." diff --git a/.context/effect/migration/annotations/effect__sql__SqlEventJournal.yaml b/.context/effect/migration/annotations/effect__sql__SqlEventJournal.yaml new file mode 100644 index 000000000..60b3b2d96 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlEventJournal.yaml @@ -0,0 +1,6 @@ +"@effect/sql/SqlEventJournal#layer": + replacement: "effect/unstable/eventlog/SqlEventJournal#layer" + note: "Moved; rename the eventLogTable layer option to entryTable." +"@effect/sql/SqlEventJournal#make": + replacement: "effect/unstable/eventlog/SqlEventJournal#make" + note: "Moved with the same entryTable and remotesTable options." diff --git a/.context/effect/migration/annotations/effect__sql__SqlEventLogServer.yaml b/.context/effect/migration/annotations/effect__sql__SqlEventLogServer.yaml new file mode 100644 index 000000000..53e298176 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlEventLogServer.yaml @@ -0,0 +1,6 @@ +"@effect/sql/SqlEventLogServer#layerStorage": + replacement: "effect/unstable/eventlog/SqlEventLogServerEncrypted#layerStorage" + note: "Moved to the encrypted server module with the same options and EventLogEncryption requirement." +"@effect/sql/SqlEventLogServer#makeStorage": + replacement: "effect/unstable/eventlog/SqlEventLogServerEncrypted#makeStorage" + note: "Moved to the encrypted server module with the same SQL, encryption, and scope requirements." diff --git a/.context/effect/migration/annotations/effect__sql__SqlPersistedQueue.yaml b/.context/effect/migration/annotations/effect__sql__SqlPersistedQueue.yaml new file mode 100644 index 000000000..fadb08336 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlPersistedQueue.yaml @@ -0,0 +1,6 @@ +"@effect/sql/SqlPersistedQueue#layerStore": + replacement: "effect/unstable/persistence/PersistedQueue#layerStoreSql" + note: "Moved into PersistedQueue and renamed with the Sql suffix; options are unchanged." +"@effect/sql/SqlPersistedQueue#make": + replacement: "effect/unstable/persistence/PersistedQueue#makeStoreSql" + note: "Use the SQL store constructor; PersistedQueue.make creates a typed queue from a store factory and is not equivalent." diff --git a/.context/effect/migration/annotations/effect__sql__SqlResolver.yaml b/.context/effect/migration/annotations/effect__sql__SqlResolver.yaml new file mode 100644 index 000000000..a87b23afd --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlResolver.yaml @@ -0,0 +1,6 @@ +"@effect/sql/SqlResolver#SqlResolver": + replacement: "RequestResolver.RequestResolver>" + note: "The wrapper interface was removed; constructors return RequestResolvers. Execute them with effect/unstable/sql/SqlResolver#request." +"@effect/sql/SqlResolver#void": + replacement: "effect/unstable/sql/SqlResolver#void" + note: "Moved, but remove the leading tag and withContext arguments; it now returns a RequestResolver synchronously and runs through SqlResolver.request." diff --git a/.context/effect/migration/annotations/effect__sql__SqlSchema.yaml b/.context/effect/migration/annotations/effect__sql__SqlSchema.yaml new file mode 100644 index 000000000..4159f622b --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__SqlSchema.yaml @@ -0,0 +1,6 @@ +"@effect/sql/SqlSchema#single": + replacement: "effect/unstable/sql/SqlSchema#findOne" + note: "Renamed with the same first-row-or-fail behavior; empty results use Cause.NoSuchElementError and schema failures use Schema.SchemaError." +"@effect/sql/SqlSchema#void": + replacement: "effect/unstable/sql/SqlSchema#void" + note: "Moved with the same encode, execute, and discard-result pattern; schema failures now use Schema.SchemaError." diff --git a/.context/effect/migration/annotations/effect__sql__Statement.yaml b/.context/effect/migration/annotations/effect__sql__Statement.yaml new file mode 100644 index 000000000..353d499c6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__Statement.yaml @@ -0,0 +1,39 @@ +"@effect/sql/Statement#currentTransformer": + replacement: "effect/unstable/sql/Statement#CurrentTransformer" + note: "Capitalized and changed from FiberRef> to Context.Reference." +"@effect/sql/Statement#custom": + replacement: "effect/unstable/sql/Statement#custom" + note: "Retained, but returns a Custom segment and uses paramA/paramB/paramC; wrap it with Statement.fragment when a Fragment is required." +"@effect/sql/Statement#defaultEscape": + replacement: "effect/unstable/sql/Statement#defaultEscape" + note: "Moved with the same signature." +"@effect/sql/Statement#FragmentId": + replacement: "none" + note: "The v4 fragment brand is private; use Fragment, fragment, and isFragment instead of direct type-id access." +"@effect/sql/Statement#join": + replacement: "effect/unstable/sql/Statement#join" + note: "Moved with the same empty, single, and multiple-clause behavior." +"@effect/sql/Statement#make": + replacement: "effect/unstable/sql/Statement#make" + note: "Moved with the same constructor inputs." +"@effect/sql/Statement#or": + replacement: "effect/unstable/sql/Statement#or" + note: "Moved unchanged." +"@effect/sql/Statement#setTransformer": + replacement: "Layer.succeed(Statement.CurrentTransformer, transformer)" + note: "The helper was removed; provide the CurrentTransformer reference as a layer." +"@effect/sql/Statement#Statement": + replacement: "effect/unstable/sql/Statement#Statement" + note: "Moved; the nested Transformer type is now top-level and its callback receives Fiber.Fiber rather than FiberRefs.FiberRefs." +"@effect/sql/Statement#unsafeFragment": + replacement: "Statement.fragment([Statement.literal(sql, params)])" + note: "The helper was removed; construct the low-level fragment explicitly, or use the active constructor's sql.unsafe for an executable statement." +"@effect/sql/Statement#withTransformer": + replacement: "Effect.provideService(Statement.CurrentTransformer, transformer)" + note: "The helper was removed; locally provide the transformer reference around the effect." +"@effect/sql/Statement#withTransformerDisabled": + replacement: "Effect.provideService(Statement.CurrentTransformer, undefined)" + note: "The helper was removed; locally provide undefined for the transformer reference." +"@effect/sql/Statement#makeCompiler": + replacement: "effect/unstable/sql/Statement#makeCompiler" + note: "Moved to core Effect; the constructor options are exposed as Statement.CompilerOptions and retain the dialect-specific callbacks." diff --git a/.context/effect/migration/annotations/effect__sql__index.yaml b/.context/effect/migration/annotations/effect__sql__index.yaml new file mode 100644 index 000000000..045e8637e --- /dev/null +++ b/.context/effect/migration/annotations/effect__sql__index.yaml @@ -0,0 +1,3 @@ +"@effect/sql/index": + replacement: "effect/unstable/sql" + note: "The package barrel was removed; import the same namespaces from the effect/unstable/sql barrel or import specific modules directly." diff --git a/.context/effect/migration/annotations/effect__typeclass.yaml b/.context/effect/migration/annotations/effect__typeclass.yaml new file mode 100644 index 000000000..9ebe60ebe --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite abstractions against the concrete v4 data type and its module functions." diff --git a/.context/effect/migration/annotations/effect__typeclass__Alternative.yaml b/.context/effect/migration/annotations/effect__typeclass__Alternative.yaml new file mode 100644 index 000000000..b50c13482 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Alternative.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Alternative": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Applicative.yaml b/.context/effect/migration/annotations/effect__typeclass__Applicative.yaml new file mode 100644 index 000000000..a7a8ebd98 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Applicative.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Applicative": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Bicovariant.yaml b/.context/effect/migration/annotations/effect__typeclass__Bicovariant.yaml new file mode 100644 index 000000000..1fabb3fd7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Bicovariant.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Bicovariant": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Bounded.yaml b/.context/effect/migration/annotations/effect__typeclass__Bounded.yaml new file mode 100644 index 000000000..f8389cbb3 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Bounded.yaml @@ -0,0 +1,15 @@ +"@effect/typeclass/Bounded#between": + replacement: "Order.isBetween(B.compare)" + note: "Use the v4 Order predicate with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed." +"@effect/typeclass/Bounded#Bounded": + replacement: "none" + note: "V4 removed Bounded dictionaries. Keep the Order and minimum/maximum bounds as separate application values." +"@effect/typeclass/Bounded#BoundedTypeLambda": + replacement: "none" + note: "V4 removed the @effect/typeclass higher-kinded Bounded instance machinery." +"@effect/typeclass/Bounded#clamp": + replacement: "Order.clamp(B.compare)" + note: "Use the v4 Order combinator with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed." +"@effect/typeclass/Bounded#reverse": + replacement: "Order.flip(B.compare)" + note: "Flip the Order and swap the separately stored minimum and maximum bounds; v4 has no bundled Bounded dictionary." diff --git a/.context/effect/migration/annotations/effect__typeclass__Chainable.yaml b/.context/effect/migration/annotations/effect__typeclass__Chainable.yaml new file mode 100644 index 000000000..3f6c02a19 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Chainable.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Chainable": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Contravariant.yaml b/.context/effect/migration/annotations/effect__typeclass__Contravariant.yaml new file mode 100644 index 000000000..62039ee4e --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Contravariant.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Contravariant": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Coproduct.yaml b/.context/effect/migration/annotations/effect__typeclass__Coproduct.yaml new file mode 100644 index 000000000..86f2652a1 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Coproduct.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Coproduct": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Covariant.yaml b/.context/effect/migration/annotations/effect__typeclass__Covariant.yaml new file mode 100644 index 000000000..9d8f80180 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Covariant.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Covariant": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Filterable.yaml b/.context/effect/migration/annotations/effect__typeclass__Filterable.yaml new file mode 100644 index 000000000..b64d96c4f --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Filterable.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Filterable": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__FlatMap.yaml b/.context/effect/migration/annotations/effect__typeclass__FlatMap.yaml new file mode 100644 index 000000000..e5cfb09a6 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__FlatMap.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/FlatMap": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Foldable.yaml b/.context/effect/migration/annotations/effect__typeclass__Foldable.yaml new file mode 100644 index 000000000..3c3d180b2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Foldable.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Foldable": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Invariant.yaml b/.context/effect/migration/annotations/effect__typeclass__Invariant.yaml new file mode 100644 index 000000000..25129fe1f --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Invariant.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Invariant": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Monad.yaml b/.context/effect/migration/annotations/effect__typeclass__Monad.yaml new file mode 100644 index 000000000..0ce29d2cc --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Monad.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Monad": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Monoid.yaml b/.context/effect/migration/annotations/effect__typeclass__Monoid.yaml new file mode 100644 index 000000000..5d3e62d0a --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Monoid.yaml @@ -0,0 +1,18 @@ +"@effect/typeclass/Monoid#array": + replacement: "Array.makeReducerConcat" + note: "Use the v4 array concatenation Reducer; Reducer replaces Monoid and names the identity initialValue." +"@effect/typeclass/Monoid#fromSemigroup": + replacement: "Reducer.make(S.combine, empty)" + note: "Construct a v4 Reducer from the replacement Combiner operation and identity value." +"@effect/typeclass/Monoid#Monoid": + replacement: "Reducer.Reducer" + note: "Reducer replaces Monoid in v4; empty is renamed initialValue and combineAll remains available." +"@effect/typeclass/Monoid#reverse": + replacement: "Reducer.flip" + note: "Use the v4 Reducer combinator; it preserves initialValue and reverses combine argument order." +"@effect/typeclass/Monoid#struct": + replacement: "Struct.makeReducer" + note: "Pass a record of v4 Reducers to derive a field-wise Reducer." +"@effect/typeclass/Monoid#tuple": + replacement: "Tuple.makeReducer" + note: "Pass one array of v4 Reducers instead of rest Monoid arguments." diff --git a/.context/effect/migration/annotations/effect__typeclass__Of.yaml b/.context/effect/migration/annotations/effect__typeclass__Of.yaml new file mode 100644 index 000000000..3afd034cc --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Of.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Of": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Pointed.yaml b/.context/effect/migration/annotations/effect__typeclass__Pointed.yaml new file mode 100644 index 000000000..d03ceb668 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Pointed.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Pointed": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Product.yaml b/.context/effect/migration/annotations/effect__typeclass__Product.yaml new file mode 100644 index 000000000..6c3fd5e16 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Product.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Product": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__SemiAlternative.yaml b/.context/effect/migration/annotations/effect__typeclass__SemiAlternative.yaml new file mode 100644 index 000000000..16d84e90f --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__SemiAlternative.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/SemiAlternative": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__SemiApplicative.yaml b/.context/effect/migration/annotations/effect__typeclass__SemiApplicative.yaml new file mode 100644 index 000000000..a5f0a5917 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__SemiApplicative.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/SemiApplicative": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__SemiCoproduct.yaml b/.context/effect/migration/annotations/effect__typeclass__SemiCoproduct.yaml new file mode 100644 index 000000000..8ac86cb92 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__SemiCoproduct.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/SemiCoproduct": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__SemiProduct.yaml b/.context/effect/migration/annotations/effect__typeclass__SemiProduct.yaml new file mode 100644 index 000000000..fd13e924d --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__SemiProduct.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/SemiProduct": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__Semigroup.yaml b/.context/effect/migration/annotations/effect__typeclass__Semigroup.yaml new file mode 100644 index 000000000..eaab3e72a --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Semigroup.yaml @@ -0,0 +1,45 @@ +"@effect/typeclass/Semigroup#array": + replacement: "Array.makeReducerConcat" + note: "The v4 concatenation Reducer is also a Combiner and replaces the array Semigroup." +"@effect/typeclass/Semigroup#constant": + replacement: "Combiner.constant" + note: "Combiner replaces Semigroup in v4." +"@effect/typeclass/Semigroup#first": + replacement: "Combiner.first" + note: "Combiner replaces Semigroup in v4." +"@effect/typeclass/Semigroup#imap": + replacement: "Combiner.make" + note: "V4 has no generic invariant instance; build a Combiner that maps both inputs with from, combines them, then maps the result with to." +"@effect/typeclass/Semigroup#intercalate": + replacement: "Combiner.intercalate" + note: "Combiner replaces Semigroup; v4 takes the separator first and then the Combiner." +"@effect/typeclass/Semigroup#Invariant": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions." +"@effect/typeclass/Semigroup#last": + replacement: "Combiner.last" + note: "Combiner replaces Semigroup in v4." +"@effect/typeclass/Semigroup#make": + replacement: "Combiner.make" + note: "Combiner replaces Semigroup. V4 accepts only the binary combine function and has no combineMany override." +"@effect/typeclass/Semigroup#Product": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions." +"@effect/typeclass/Semigroup#reverse": + replacement: "Combiner.flip" + note: "Use the v4 Combiner combinator to reverse combine argument order." +"@effect/typeclass/Semigroup#Semigroup": + replacement: "Combiner.Combiner" + note: "Combiner replaces Semigroup in v4 and retains the binary combine method; combineMany was removed." +"@effect/typeclass/Semigroup#SemigroupTypeLambda": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions." +"@effect/typeclass/Semigroup#SemiProduct": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions." +"@effect/typeclass/Semigroup#struct": + replacement: "Struct.makeCombiner" + note: "Pass a record of v4 Combiners to derive a field-wise Combiner." +"@effect/typeclass/Semigroup#tuple": + replacement: "Tuple.makeCombiner" + note: "Pass one array of v4 Combiners instead of rest Semigroup arguments." diff --git a/.context/effect/migration/annotations/effect__typeclass__Traversable.yaml b/.context/effect/migration/annotations/effect__typeclass__Traversable.yaml new file mode 100644 index 000000000..a939a1622 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__Traversable.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/Traversable": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__TraversableFilterable.yaml b/.context/effect/migration/annotations/effect__typeclass__TraversableFilterable.yaml new file mode 100644 index 000000000..add881f13 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__TraversableFilterable.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/TraversableFilterable": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Array.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Array.yaml new file mode 100644 index 000000000..e20b9559e --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Array.yaml @@ -0,0 +1,51 @@ +"@effect/typeclass/data/Array#Applicative": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Chainable": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Covariant": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Filterable": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#FlatMap": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Foldable": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#getMonoid": + replacement: "Array.makeReducerConcat" + note: "Use the v4 concatenation Reducer; Reducer replaces Monoid." +"@effect/typeclass/data/Array#getSemigroup": + replacement: "Array.makeReducerConcat" + note: "The v4 concatenation Reducer is also a Combiner and replaces this Semigroup." +"@effect/typeclass/data/Array#Invariant": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Monad": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Of": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Pointed": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Product": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#SemiApplicative": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#SemiProduct": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#Traversable": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." +"@effect/typeclass/data/Array#TraversableFilterable": + replacement: "effect/Array" + note: "The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__BigInt.yaml b/.context/effect/migration/annotations/effect__typeclass__data__BigInt.yaml new file mode 100644 index 000000000..e94aed7f5 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__BigInt.yaml @@ -0,0 +1,18 @@ +"@effect/typeclass/data/BigInt#MonoidMultiply": + replacement: "BigInt.ReducerMultiply" + note: "Renamed and moved to the concrete v4 BigInt module." +"@effect/typeclass/data/BigInt#MonoidSum": + replacement: "BigInt.ReducerSum" + note: "Renamed and moved to the concrete v4 BigInt module." +"@effect/typeclass/data/BigInt#SemigroupMax": + replacement: "BigInt.CombinerMax" + note: "Renamed and moved to the concrete v4 BigInt module." +"@effect/typeclass/data/BigInt#SemigroupMin": + replacement: "BigInt.CombinerMin" + note: "Renamed and moved to the concrete v4 BigInt module." +"@effect/typeclass/data/BigInt#SemigroupMultiply": + replacement: "BigInt.ReducerMultiply" + note: "The v4 Reducer is also a Combiner and preserves multiplication combine semantics." +"@effect/typeclass/data/BigInt#SemigroupSum": + replacement: "BigInt.ReducerSum" + note: "The v4 Reducer is also a Combiner and preserves addition combine semantics." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Boolean.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Boolean.yaml new file mode 100644 index 000000000..560fdb587 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Boolean.yaml @@ -0,0 +1,24 @@ +"@effect/typeclass/data/Boolean#MonoidEqv": + replacement: "Reducer.make(Boolean.eqv, true)" + note: "Rebuild the removed instance with the v4 boolean operation and its identity." +"@effect/typeclass/data/Boolean#MonoidEvery": + replacement: "Boolean.ReducerAnd" + note: "Renamed and moved to the concrete v4 Boolean module." +"@effect/typeclass/data/Boolean#MonoidSome": + replacement: "Boolean.ReducerOr" + note: "Renamed and moved to the concrete v4 Boolean module." +"@effect/typeclass/data/Boolean#MonoidXor": + replacement: "Reducer.make(Boolean.xor, false)" + note: "Rebuild the removed instance with the v4 boolean operation and its identity." +"@effect/typeclass/data/Boolean#SemigroupEqv": + replacement: "Combiner.make(Boolean.eqv)" + note: "Rebuild the removed instance as a v4 Combiner." +"@effect/typeclass/data/Boolean#SemigroupEvery": + replacement: "Boolean.ReducerAnd" + note: "The v4 Reducer is also a Combiner and preserves logical-AND combine semantics." +"@effect/typeclass/data/Boolean#SemigroupSome": + replacement: "Boolean.ReducerOr" + note: "The v4 Reducer is also a Combiner and preserves logical-OR combine semantics." +"@effect/typeclass/data/Boolean#SemigroupXor": + replacement: "Combiner.make(Boolean.xor)" + note: "Rebuild the removed instance as a v4 Combiner." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Duration.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Duration.yaml new file mode 100644 index 000000000..0a6b7dbc0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Duration.yaml @@ -0,0 +1,21 @@ +"@effect/typeclass/data/Duration#Bounded": + replacement: "none" + note: "V4 has no Bounded dictionary; use Duration.Order with Duration.zero and Duration.infinity as separate bounds." +"@effect/typeclass/data/Duration#MonoidMax": + replacement: "Reducer.make(Duration.max, Duration.zero)" + note: "Rebuild the removed maximum Monoid as a v4 Reducer with the same identity." +"@effect/typeclass/data/Duration#MonoidMin": + replacement: "Reducer.make(Duration.min, Duration.infinity)" + note: "Rebuild the removed minimum Monoid as a v4 Reducer with the same identity." +"@effect/typeclass/data/Duration#MonoidSum": + replacement: "Duration.ReducerSum" + note: "Renamed and moved to the concrete v4 Duration module." +"@effect/typeclass/data/Duration#SemigroupMax": + replacement: "Duration.CombinerMax" + note: "Renamed and moved to the concrete v4 Duration module." +"@effect/typeclass/data/Duration#SemigroupMin": + replacement: "Duration.CombinerMin" + note: "Renamed and moved to the concrete v4 Duration module." +"@effect/typeclass/data/Duration#SemigroupSum": + replacement: "Duration.ReducerSum" + note: "The v4 Reducer is also a Combiner and preserves Duration.sum combine semantics." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Effect.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Effect.yaml new file mode 100644 index 000000000..29c7fe373 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Effect.yaml @@ -0,0 +1,36 @@ +"@effect/typeclass/data/Effect#Chainable": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#ConcurrencyOptions": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#Covariant": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#FlatMap": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#getApplicative": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#getProduct": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#getSemiApplicative": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#getSemiProduct": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#Invariant": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#Monad": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#Of": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." +"@effect/typeclass/data/Effect#Pointed": + replacement: "effect/Effect" + note: "The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Either.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Either.yaml new file mode 100644 index 000000000..ad27bb0d7 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Either.yaml @@ -0,0 +1,48 @@ +"@effect/typeclass/data/Either#Applicative": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Bicovariant": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Chainable": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Covariant": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#FlatMap": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Foldable": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Invariant": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Monad": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Of": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Pointed": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Product": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#SemiAlternative": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#SemiApplicative": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#SemiCoproduct": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#SemiProduct": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." +"@effect/typeclass/data/Either#Traversable": + replacement: "effect/Either" + note: "The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Identity.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Identity.yaml new file mode 100644 index 000000000..6f3f4349e --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Identity.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/data/Identity": + replacement: none + note: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Micro.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Micro.yaml new file mode 100644 index 000000000..03a02542a --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Micro.yaml @@ -0,0 +1,36 @@ +"@effect/typeclass/data/Micro#Chainable": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#ConcurrencyOptions": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#Covariant": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#FlatMap": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#getApplicative": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#getProduct": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#getSemiApplicative": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#getSemiProduct": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#Invariant": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#Monad": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#Of": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." +"@effect/typeclass/data/Micro#Pointed": + replacement: "effect/Micro" + note: "The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Number.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Number.yaml new file mode 100644 index 000000000..14d0a9a9e --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Number.yaml @@ -0,0 +1,27 @@ +"@effect/typeclass/data/Number#Bounded": + replacement: "none" + note: "V4 has no Bounded dictionary; use Number.Order with -Infinity and Infinity as separate bounds." +"@effect/typeclass/data/Number#MonoidMax": + replacement: "Number.ReducerMax" + note: "Renamed and moved to the concrete v4 Number module." +"@effect/typeclass/data/Number#MonoidMin": + replacement: "Number.ReducerMin" + note: "Renamed and moved to the concrete v4 Number module." +"@effect/typeclass/data/Number#MonoidMultiply": + replacement: "Number.ReducerMultiply" + note: "Renamed and moved to the concrete v4 Number module." +"@effect/typeclass/data/Number#MonoidSum": + replacement: "Number.ReducerSum" + note: "Renamed and moved to the concrete v4 Number module." +"@effect/typeclass/data/Number#SemigroupMax": + replacement: "Number.ReducerMax" + note: "The v4 Reducer is also a Combiner and preserves maximum combine semantics." +"@effect/typeclass/data/Number#SemigroupMin": + replacement: "Number.ReducerMin" + note: "The v4 Reducer is also a Combiner and preserves minimum combine semantics." +"@effect/typeclass/data/Number#SemigroupMultiply": + replacement: "Number.ReducerMultiply" + note: "The v4 Reducer is also a Combiner and preserves multiplication combine semantics." +"@effect/typeclass/data/Number#SemigroupSum": + replacement: "Number.ReducerSum" + note: "The v4 Reducer is also a Combiner and preserves addition combine semantics." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Option.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Option.yaml new file mode 100644 index 000000000..bc1a02fd4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Option.yaml @@ -0,0 +1,57 @@ +"@effect/typeclass/data/Option#Alternative": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Applicative": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Chainable": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Coproduct": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Covariant": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Filterable": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#FlatMap": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Foldable": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#getOptionalMonoid": + replacement: "Option.makeReducer" + note: "Pass the replacement Combiner; the v4 Reducer uses None as initialValue and combines two Some values." +"@effect/typeclass/data/Option#Invariant": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Monad": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Of": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Pointed": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Product": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#SemiAlternative": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#SemiApplicative": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#SemiCoproduct": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#SemiProduct": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." +"@effect/typeclass/data/Option#Traversable": + replacement: "effect/Option" + note: "The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Ordering.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Ordering.yaml new file mode 100644 index 000000000..0410c651e --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Ordering.yaml @@ -0,0 +1,6 @@ +"@effect/typeclass/data/Ordering#Monoid": + replacement: "Ordering.Reducer" + note: "Renamed and moved to the concrete v4 Ordering module." +"@effect/typeclass/data/Ordering#Semigroup": + replacement: "Ordering.Reducer" + note: "The v4 Reducer is also a Combiner and preserves Ordering combination semantics." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Predicate.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Predicate.yaml new file mode 100644 index 000000000..551003298 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Predicate.yaml @@ -0,0 +1,39 @@ +"@effect/typeclass/data/Predicate#Contravariant": + replacement: "effect/Predicate" + note: "The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly." +"@effect/typeclass/data/Predicate#getMonoidEqv": + replacement: "Reducer.make(Predicate.eqv, Predicate.isUnknown)" + note: "Rebuild the removed predicate instance as a v4 Reducer with the always-true predicate as initialValue." +"@effect/typeclass/data/Predicate#getMonoidEvery": + replacement: "Reducer.make(Predicate.and, Predicate.isUnknown)" + note: "Rebuild the removed predicate instance as a v4 Reducer with the always-true predicate as initialValue." +"@effect/typeclass/data/Predicate#getMonoidSome": + replacement: "Reducer.make(Predicate.or, Predicate.isNever)" + note: "Rebuild the removed predicate instance as a v4 Reducer with the always-false predicate as initialValue." +"@effect/typeclass/data/Predicate#getMonoidXor": + replacement: "Reducer.make(Predicate.xor, Predicate.isNever)" + note: "Rebuild the removed predicate instance as a v4 Reducer with the always-false predicate as initialValue." +"@effect/typeclass/data/Predicate#getSemigroupEqv": + replacement: "Combiner.make(Predicate.eqv)" + note: "Rebuild the removed predicate instance as a v4 Combiner." +"@effect/typeclass/data/Predicate#getSemigroupEvery": + replacement: "Combiner.make(Predicate.and)" + note: "Rebuild the removed predicate instance as a v4 Combiner." +"@effect/typeclass/data/Predicate#getSemigroupSome": + replacement: "Combiner.make(Predicate.or)" + note: "Rebuild the removed predicate instance as a v4 Combiner." +"@effect/typeclass/data/Predicate#getSemigroupXor": + replacement: "Combiner.make(Predicate.xor)" + note: "Rebuild the removed predicate instance as a v4 Combiner." +"@effect/typeclass/data/Predicate#Invariant": + replacement: "effect/Predicate" + note: "The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly." +"@effect/typeclass/data/Predicate#Of": + replacement: "effect/Predicate" + note: "The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly." +"@effect/typeclass/data/Predicate#Product": + replacement: "effect/Predicate" + note: "The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly." +"@effect/typeclass/data/Predicate#SemiProduct": + replacement: "effect/Predicate" + note: "The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Record.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Record.yaml new file mode 100644 index 000000000..b10996128 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Record.yaml @@ -0,0 +1,39 @@ +"@effect/typeclass/data/Record#Covariant": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#Filterable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#getCovariant": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#getFilterable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#getInvariant": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#getMonoidUnion": + replacement: "Record.makeReducerUnion" + note: "Pass the replacement value Combiner; the v4 Reducer uses an empty record as initialValue." +"@effect/typeclass/data/Record#getSemigroupIntersection": + replacement: "Record.makeReducerIntersection" + note: "Pass the replacement value Combiner and use the returned Reducer's combine operation for pairwise intersection." +"@effect/typeclass/data/Record#getSemigroupUnion": + replacement: "Record.makeReducerUnion" + note: "The v4 Reducer is also a Combiner and preserves pairwise union semantics." +"@effect/typeclass/data/Record#getTraversable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#getTraversableFilterable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#Invariant": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#Traversable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." +"@effect/typeclass/data/Record#TraversableFilterable": + replacement: "effect/Record" + note: "The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__String.yaml b/.context/effect/migration/annotations/effect__typeclass__data__String.yaml new file mode 100644 index 000000000..c79396fdf --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__String.yaml @@ -0,0 +1,6 @@ +"@effect/typeclass/data/String#Monoid": + replacement: "String.ReducerConcat" + note: "Renamed and moved to the concrete v4 String module." +"@effect/typeclass/data/String#Semigroup": + replacement: "String.ReducerConcat" + note: "The v4 Reducer is also a Combiner and preserves string concatenation." diff --git a/.context/effect/migration/annotations/effect__typeclass__data__Tuple.yaml b/.context/effect/migration/annotations/effect__typeclass__data__Tuple.yaml new file mode 100644 index 000000000..8f1e2c205 --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__data__Tuple.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/data/Tuple#Bicovariant": + replacement: "effect/Tuple" + note: "The @effect/typeclass package and its Tuple instance dictionaries were removed in v4. Use the concrete effect/Tuple operations directly." diff --git a/.context/effect/migration/annotations/effect__typeclass__index.yaml b/.context/effect/migration/annotations/effect__typeclass__index.yaml new file mode 100644 index 000000000..ff85bec6c --- /dev/null +++ b/.context/effect/migration/annotations/effect__typeclass__index.yaml @@ -0,0 +1,3 @@ +"@effect/typeclass/index": + replacement: "none" + note: "The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite abstractions against the concrete v4 data type and its module functions." diff --git a/.context/effect/migration/annotations/effect__vitest__index.yaml b/.context/effect/migration/annotations/effect__vitest__index.yaml new file mode 100644 index 000000000..a40e32ca2 --- /dev/null +++ b/.context/effect/migration/annotations/effect__vitest__index.yaml @@ -0,0 +1,234 @@ +"@effect/vitest/index#scoped": + replacement: "@effect/vitest#effect" + note: "V4 effect tests are scoped and provide the test environment. Replace scoped(...) with effect(...), and it.scoped(...) with it.effect(...)." +"@effect/vitest/index#scopedLive": + replacement: "@effect/vitest#live" + note: "V4 live tests are scoped automatically. Replace scopedLive(...) with live(...), and it.scopedLive(...) with it.live(...)." +"@effect/vitest/index#chai.Should": + replacement: "vitest#chai.Should" + note: "This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route." +"@effect/vitest/index#expect": + replacement: "vitest#expect" + note: "This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route." +"@effect/vitest/index#Mock": + replacement: "vitest#Mock" + note: "This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route." +"@effect/vitest/index#should": + replacement: "vitest#should" + note: "This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route." +"@effect/vitest/index#ApiConfig": + replacement: "vitest/node#ApiConfig" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#BaseCoverageOptions": + replacement: "vitest/node#BaseCoverageOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#BenchmarkUserOptions": + replacement: "vitest/node#BenchmarkUserOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#BrowserConfigOptions": + replacement: "vitest/node#BrowserConfigOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#BrowserScript": + replacement: "vitest/node#BrowserScript" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#BuiltinEnvironment": + replacement: "vitest/node#BuiltinEnvironment" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageIstanbulOptions": + replacement: "vitest/node#CoverageIstanbulOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageOptions": + replacement: "vitest/node#CoverageOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageProvider": + replacement: "vitest/node#CoverageProvider" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageProviderModule": + replacement: "vitest/node#CoverageProviderModule" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageReporter": + replacement: "vitest/node#CoverageReporter" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CoverageV8Options": + replacement: "vitest/node#CoverageV8Options" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CSSModuleScopeStrategy": + replacement: "vitest/node#CSSModuleScopeStrategy" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CustomProviderOptions": + replacement: "vitest/node#CustomProviderOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#DepsOptimizationOptions": + replacement: "vitest/node#DepsOptimizationOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#EnvironmentOptions": + replacement: "vitest/node#EnvironmentOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#InlineConfig": + replacement: "vitest/node#InlineConfig" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#Pool": + replacement: "vitest/node#Pool" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#ProjectConfig": + replacement: "vitest/node#ProjectConfig" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#ReportContext": + replacement: "vitest/node#ReportContext" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#ResolvedConfig": + replacement: "vitest/node#ResolvedConfig" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#ResolvedCoverageOptions": + replacement: "vitest/node#ResolvedCoverageOptions" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#SequenceHooks": + replacement: "vitest/node#SequenceHooks" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#SequenceSetupFiles": + replacement: "vitest/node#SequenceSetupFiles" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#TypecheckConfig": + replacement: "vitest/node#TypecheckConfig" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#VitestEnvironment": + replacement: "vitest/node#VitestEnvironment" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#VitestRunMode": + replacement: "vitest/node#VitestRunMode" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#WorkerContext": + replacement: "vitest/node#WorkerContext" + note: "This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape." +"@effect/vitest/index#CollectLineNumbers": + replacement: "vitest/node#TypeCheckCollectLineNumbers" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#CollectLines": + replacement: "vitest/node#TypeCheckCollectLines" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#Context": + replacement: "vitest/node#TypeCheckContext" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#RawErrsMap": + replacement: "vitest/node#TypeCheckRawErrorsMap" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#RootAndTarget": + replacement: "vitest/node#TypeCheckRootAndTarget" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#TscErrorInfo": + replacement: "vitest/node#TypeCheckErrorInfo" + note: "Vitest 3 deprecated the root alias in favor of this renamed vitest/node type." +"@effect/vitest/index#Custom": + replacement: "vitest#RunnerTestCase" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#RunnerCustomCase": + replacement: "vitest#RunnerTestCase" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#File": + replacement: "vitest#RunnerTestFile" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#Suite": + replacement: "vitest#RunnerTestSuite" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#Task": + replacement: "vitest#RunnerTask" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#TaskBase": + replacement: "vitest#RunnerTaskBase" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#TaskResult": + replacement: "vitest#RunnerTaskResult" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#TaskResultPack": + replacement: "vitest#RunnerTaskResultPack" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#Test": + replacement: "vitest#RunnerTestCase" + note: "Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner* type from vitest." +"@effect/vitest/index#ExtendedContext": + replacement: "vitest#TestContext" + note: "The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods." +"@effect/vitest/index#TaskContext": + replacement: "vitest#TestContext" + note: "The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods." +"@effect/vitest/index#Environment": + replacement: "vitest/environments#Environment" + note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." +"@effect/vitest/index#EnvironmentReturn": + replacement: "vitest/environments#EnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." +"@effect/vitest/index#VmEnvironmentReturn": + replacement: "vitest/environments#VmEnvironmentReturn" + note: "This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments." +"@effect/vitest/index#HappyDOMOptions": + replacement: "NonNullable" + note: "Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." +"@effect/vitest/index#JSDOMOptions": + replacement: "NonNullable" + note: "Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type." +"@effect/vitest/index#ArgumentsType": + replacement: "T extends (...args: infer A) => any ? A : never" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#Arrayable": + replacement: "T | Array" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#Awaitable": + replacement: "T | PromiseLike" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#Constructable": + replacement: "new (...args: any[]) => any" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#MutableArray": + replacement: "{ -readonly [K in keyof T]: T[K] }" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#Nullable": + replacement: "T | null | undefined" + note: "Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals." +"@effect/vitest/index#ErrorWithDiff": + replacement: "vitest#TestError" + note: "Vitest 3 deprecated ErrorWithDiff in favor of TestError; review the tightened actual, expected, and cause fields." +"@effect/vitest/index#SerializableSpec": + replacement: "vitest#SerializedTestSpecification" + note: "Use the non-deprecated Vitest name; SerializableSpec was only an alias." +"@effect/vitest/index#Reporter": + replacement: "vitest/reporters#Reporter" + note: "Import Reporter from the public plural vitest/reporters entrypoint; its lifecycle methods changed in Vitest 4." +"@effect/vitest/index#UserConfig": + replacement: "vitest/config#TestUserConfig" + note: "Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type." +"@effect/vitest/index#UserWorkspaceConfig": + replacement: "vitest/config#UserWorkspaceConfig" + note: "Import the type from vitest/config and migrate Vitest workspace configuration to projects." +"@effect/vitest/index#PoolOptions": + replacement: "vitest/config#TestUserConfig" + note: "The v3 built-in poolOptions object was removed. Move its fields to Vitest 4 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API." +"@effect/vitest/index#RuntimeContext": + replacement: "@vitest/runner#RuntimeContext" + note: "Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should avoid this internal state type." +"@effect/vitest/index#SuiteHooks": + replacement: "@vitest/runner#SuiteHooks" + note: "Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should use public hook functions." +"@effect/vitest/index#DoneCallback": + replacement: "none" + note: "Vitest does not support callback-style tests. Return a Promise or, in @effect/vitest tests, return an Effect." +"@effect/vitest/index#HookCleanupCallback": + replacement: "none" + note: "No named Vitest 4 export replaces this alias. Let the hook return type infer, or type the cleanup function locally." +"@effect/vitest/index#HookListener": + replacement: "none" + note: "Use the matching @vitest/runner hook-specific type such as BeforeAllListener, AfterAllListener, BeforeEachListener, or AfterEachListener for custom runner code." +"@effect/vitest/index#ModuleCache": + replacement: "none" + note: "Vitest 3 marked this unused internal cache shape deprecated; Vitest 4 has no public replacement." +"@effect/vitest/index#ResolvedTestEnvironment": + replacement: "none" + note: "Vitest 3 marked this type unsupported. Use Environment from vitest/environments for custom environments." +"@effect/vitest/index#ResolveIdFunction": + replacement: "none" + note: "This deprecated vite-node callback was removed. Use Vite environment or module-runner APIs." +"@effect/vitest/index#TransformModePatterns": + replacement: "none" + note: "This was removed with vite-node transform modes. Configure the Vite environment and its dependency optimizer instead." +"@effect/vitest/index#WorkerRPC": + replacement: "none" + note: "The concrete worker RPC composition is internal. Use public Vitest RuntimeRPC, RunnerRPC, ContextRPC, or WorkerRequest types only when their narrower contract fits." diff --git a/.context/effect/migration/annotations/effect__vitest__utils.yaml b/.context/effect/migration/annotations/effect__vitest__utils.yaml new file mode 100644 index 000000000..240ffe3da --- /dev/null +++ b/.context/effect/migration/annotations/effect__vitest__utils.yaml @@ -0,0 +1,15 @@ +"@effect/vitest/utils#assertFailure": + replacement: "assertExitFailure" + note: "For v3 Exit values, rename to assertExitFailure. In v4, assertFailure instead asserts Result.Failure." +"@effect/vitest/utils#assertLeft": + replacement: "assertFailure" + note: "Either became Result in v4: migrate Left to Result.Failure, then use assertFailure; narrowed payload access changes from .left to .failure." +"@effect/vitest/utils#assertMatch": + replacement: "assertMatch" + note: "Unchanged positional helper and behavior; only the parameter spelling changed, so call sites need no change." +"@effect/vitest/utils#assertRight": + replacement: "assertSuccess" + note: "Either became Result in v4: migrate Right to Result.Success, then use assertSuccess; narrowed payload access changes from .right to .success." +"@effect/vitest/utils#assertSuccess": + replacement: "assertExitSuccess" + note: "For v3 Exit values, rename to assertExitSuccess. In v4, assertSuccess instead asserts Result.Success." diff --git a/.context/effect/migration/annotations/effect__workflow.yaml b/.context/effect/migration/annotations/effect__workflow.yaml new file mode 100644 index 000000000..0995bdb5e --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow.yaml @@ -0,0 +1,3 @@ +"@effect/workflow": + replacement: "effect/unstable/workflow" + note: "The @effect/workflow package was merged into the effect package; import the effect/unstable/workflow barrel or import specific modules directly (e.g. effect/unstable/workflow/)." diff --git a/.context/effect/migration/annotations/effect__workflow__Activity.yaml b/.context/effect/migration/annotations/effect__workflow__Activity.yaml new file mode 100644 index 000000000..cc4b67596 --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__Activity.yaml @@ -0,0 +1,18 @@ +"@effect/workflow/Activity#Any": + replacement: "effect/unstable/workflow/Activity#Any" + note: "Moved into core Effect. V4 Any is minimal; use AnyWithProps when schemas or execution properties are required." +"@effect/workflow/Activity#CurrentAttempt": + replacement: "effect/unstable/workflow/Activity#CurrentAttempt" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same default of 1." +"@effect/workflow/Activity#make": + replacement: "effect/unstable/workflow/Activity#make" + note: "Moved into core Effect with the same constructor shape, v4 Schema.Constraint service directions, and optional annotations." +"@effect/workflow/Activity#raceAll": + replacement: "effect/unstable/workflow/Activity#raceAll" + note: "Moved into core Effect with the same named durable race behavior." +"@effect/workflow/Activity#retry": + replacement: "effect/unstable/workflow/Activity#retry" + note: "Moved into core Effect and updated to v4 Effect.retry option types." +"@effect/workflow/Activity#TypeId": + replacement: "none" + note: "The activity marker is private in v4. Use Activity, Activity.Any, or Activity.AnyWithProps constraints." diff --git a/.context/effect/migration/annotations/effect__workflow__DurableClock.yaml b/.context/effect/migration/annotations/effect__workflow__DurableClock.yaml new file mode 100644 index 000000000..adb1bf416 --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__DurableClock.yaml @@ -0,0 +1,6 @@ +"@effect/workflow/DurableClock#make": + replacement: "effect/unstable/workflow/DurableClock#make" + note: "Moved into core Effect; Duration.DurationInput is now Duration.Input." +"@effect/workflow/DurableClock#TypeId": + replacement: "none" + note: "The durable-clock marker is private in v4. Use DurableClock values structurally." diff --git a/.context/effect/migration/annotations/effect__workflow__DurableDeferred.yaml b/.context/effect/migration/annotations/effect__workflow__DurableDeferred.yaml new file mode 100644 index 000000000..f30fc24b4 --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__DurableDeferred.yaml @@ -0,0 +1,30 @@ +"@effect/workflow/DurableDeferred#Any": + replacement: "effect/unstable/workflow/DurableDeferred#Any" + note: "Moved into core Effect. V4 Any is minimal; use AnyWithProps when success, error, or exit schemas are required." +"@effect/workflow/DurableDeferred#await": + replacement: "effect/unstable/workflow/DurableDeferred#await" + note: "Moved into core Effect with the same persisted-result and workflow-suspension behavior." +"@effect/workflow/DurableDeferred#done": + replacement: "effect/unstable/workflow/DurableDeferred#done" + note: "Moved into core Effect; schema requirements now use explicit directional encoding services." +"@effect/workflow/DurableDeferred#fail": + replacement: "effect/unstable/workflow/DurableDeferred#fail" + note: "Moved into core Effect and now requires the error schema encoding services." +"@effect/workflow/DurableDeferred#failCause": + replacement: "effect/unstable/workflow/DurableDeferred#failCause" + note: "Moved into core Effect and now requires the error schema encoding services." +"@effect/workflow/DurableDeferred#into": + replacement: "effect/unstable/workflow/DurableDeferred#into" + note: "Moved into core Effect with the same exit recording and suspension propagation behavior." +"@effect/workflow/DurableDeferred#make": + replacement: "effect/unstable/workflow/DurableDeferred#make" + note: "Moved into core Effect with the same name and optional schemas, expressed through v4 Schema.Constraint." +"@effect/workflow/DurableDeferred#raceAll": + replacement: "effect/unstable/workflow/DurableDeferred#raceAll" + note: "Moved into core Effect with the same persisted-winner behavior." +"@effect/workflow/DurableDeferred#succeed": + replacement: "effect/unstable/workflow/DurableDeferred#succeed" + note: "Moved into core Effect and now requires the success schema encoding services." +"@effect/workflow/DurableDeferred#TypeId": + replacement: "none" + note: "The durable-deferred marker is private in v4. Use DurableDeferred, Any, or AnyWithProps constraints." diff --git a/.context/effect/migration/annotations/effect__workflow__DurableQueue.yaml b/.context/effect/migration/annotations/effect__workflow__DurableQueue.yaml new file mode 100644 index 000000000..5c2dbb0f0 --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__DurableQueue.yaml @@ -0,0 +1,6 @@ +"@effect/workflow/DurableQueue#make": + replacement: "effect/unstable/workflow/DurableQueue#make" + note: "Moved into core Effect; queue persistence now comes from effect/unstable/persistence." +"@effect/workflow/DurableQueue#TypeId": + replacement: "effect/unstable/workflow/DurableQueue#TypeId" + note: "Moved into core Effect; the marker literal changed to ~effect/workflow/DurableQueue. Use typeof DurableQueue.TypeId in type position." diff --git a/.context/effect/migration/annotations/effect__workflow__DurableRateLimiter.yaml b/.context/effect/migration/annotations/effect__workflow__DurableRateLimiter.yaml new file mode 100644 index 000000000..a0261367e --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__DurableRateLimiter.yaml @@ -0,0 +1,3 @@ +"@effect/workflow/DurableRateLimiter": + replacement: none + note: "Not ported. Build an Activity whose execute uses persistence RateLimiter.consume with onExceeded: delay, then sleeps for the returned delay with DurableClock." diff --git a/.context/effect/migration/annotations/effect__workflow__Workflow.yaml b/.context/effect/migration/annotations/effect__workflow__Workflow.yaml new file mode 100644 index 000000000..941cb9f25 --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__Workflow.yaml @@ -0,0 +1,51 @@ +"@effect/workflow/Workflow#Any": + replacement: "effect/unstable/workflow/Workflow#Any" + note: "Moved into core Effect. Workflow identity changed from name to _tag and definitions are now class-compatible constructors." +"@effect/workflow/Workflow#AnyTaggedRequestSchema": + replacement: "none" + note: "The TaggedRequest adapter constraint was removed. Define the workflow explicitly with Workflow.make and the request payload, success, error, and PrimaryKey schemas." +"@effect/workflow/Workflow#CaptureDefects": + replacement: "effect/unstable/workflow/Workflow#CaptureDefects" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same true default." +"@effect/workflow/Workflow#Execution": + replacement: "effect/unstable/workflow/Workflow#Execution" + note: "Moved into core Effect; its workflow discriminator changed from name to _tag." +"@effect/workflow/Workflow#fromTaggedRequest": + replacement: "none" + note: "Removed. Expand to Workflow.make(schema._tag, { payload: schema, success: schema.success, error: schema.failure, idempotencyKey: PrimaryKey.value })." +"@effect/workflow/Workflow#isResult": + replacement: "effect/unstable/workflow/Workflow#isResult" + note: "Moved into core Effect with the same result refinement behavior." +"@effect/workflow/Workflow#make": + replacement: "effect/unstable/workflow/Workflow#make" + note: "The signature changed from make({ name, ... }) to make(tag, { ... }); definitions expose _tag and are class-compatible constructors." +"@effect/workflow/Workflow#Requirements": + replacement: "Workflow.RequirementsClient / Workflow.RequirementsHandler" + note: "The schema Context union split by direction: client payload encoding and result decoding versus handler payload decoding and result encoding." +"@effect/workflow/Workflow#Result": + replacement: "effect/unstable/workflow/Workflow#Result" + note: "Moved into core Effect and remains the Complete or Suspended result type and schema constructor." +"@effect/workflow/Workflow#ResultEncoded": + replacement: "effect/unstable/workflow/Workflow#ResultEncoded" + note: "Moved into core Effect and remains both the encoded result type and generic encoded-result codec." +"@effect/workflow/Workflow#ResultTypeId": + replacement: "none" + note: "The result marker is private in v4. Use Workflow.isResult for narrowing." +"@effect/workflow/Workflow#SuspendOnFailure": + replacement: "effect/unstable/workflow/Workflow#SuspendOnFailure" + note: "Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same false default." +"@effect/workflow/Workflow#TypeId": + replacement: "none" + note: "The workflow marker is private in v4. Use Workflow.Any or Workflow.Workflow constraints." +"@effect/workflow/Workflow#Workflow": + replacement: "effect/unstable/workflow/Workflow#Workflow" + note: "Name and name became Tag and _tag, schemas use directional services, definitions are constructable, and poll returns Option." +"@effect/workflow/Workflow#Workflow.Error": + replacement: "W[\"errorSchema\"][\"Type\"]" + note: "The namespace alias was removed. Extract the decoded error type from the public errorSchema property." +"@effect/workflow/Workflow#Workflow.Payload": + replacement: "Schema.Schema.Type>" + note: "The namespace alias was removed. Extract the decoded payload from the exported PayloadSchema helper." +"@effect/workflow/Workflow#Workflow.Success": + replacement: "W[\"successSchema\"][\"Type\"]" + note: "The namespace alias was removed. Extract the decoded success type from the public successSchema property." diff --git a/.context/effect/migration/annotations/effect__workflow__WorkflowEngine.yaml b/.context/effect/migration/annotations/effect__workflow__WorkflowEngine.yaml new file mode 100644 index 000000000..543c7002c --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__WorkflowEngine.yaml @@ -0,0 +1,6 @@ +"@effect/workflow/WorkflowEngine#layerMemory": + replacement: "effect/unstable/workflow/WorkflowEngine#layerMemory" + note: "Moved into core Effect and remains the non-durable engine for tests and local development." +"@effect/workflow/WorkflowEngine#makeUnsafe": + replacement: "effect/unstable/workflow/WorkflowEngine#makeUnsafe" + note: "Moved into core Effect. Context service projections now use Service instead of Type, and absent encoded results use Option." diff --git a/.context/effect/migration/annotations/effect__workflow__WorkflowProxy.yaml b/.context/effect/migration/annotations/effect__workflow__WorkflowProxy.yaml new file mode 100644 index 000000000..bc1d27b4a --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__WorkflowProxy.yaml @@ -0,0 +1,6 @@ +"@effect/workflow/WorkflowProxy#ConvertHttpApi": + replacement: "effect/unstable/workflow/WorkflowProxy#ConvertHttpApi" + note: "Moved into core Effect and updated to v4 HttpApiEndpoint types and the consolidated HttpApi architecture." +"@effect/workflow/WorkflowProxy#ConvertRpcs": + replacement: "effect/unstable/workflow/WorkflowProxy#ConvertRpcs" + note: "Moved into core Effect; generated execute, discard, and resume RPCs are now keyed from workflow _tag." diff --git a/.context/effect/migration/annotations/effect__workflow__WorkflowProxyServer.yaml b/.context/effect/migration/annotations/effect__workflow__WorkflowProxyServer.yaml new file mode 100644 index 000000000..b604847ef --- /dev/null +++ b/.context/effect/migration/annotations/effect__workflow__WorkflowProxyServer.yaml @@ -0,0 +1,9 @@ +"@effect/workflow/WorkflowProxyServer#layerHttpApi": + replacement: "effect/unstable/workflow/WorkflowProxyServer#layerHttpApi" + note: "Moved into core Effect. Use v4 HttpApi group identifiers and Workflow.RequirementsHandler schema services." +"@effect/workflow/WorkflowProxyServer#layerRpcHandlers": + replacement: "effect/unstable/workflow/WorkflowProxyServer#layerRpcHandlers" + note: "Moved into core Effect; generated handlers require Workflow.RequirementsHandler rather than the undirected Requirements union." +"@effect/workflow/WorkflowProxyServer#RpcHandlers": + replacement: "effect/unstable/workflow/WorkflowProxyServer#RpcHandlers" + note: "Moved into core Effect; handler names derive from workflow _tag and the optional prefix." diff --git a/.context/effect/migration/fiberref.md b/.context/effect/migration/fiberref.md index 51a088622..abfc17fa6 100644 --- a/.context/effect/migration/fiberref.md +++ b/.context/effect/migration/fiberref.md @@ -11,7 +11,6 @@ from `References` and related modules. | v3 FiberRef | v4 Reference | | ----------------------------------- | ---------------------------------- | -| `FiberRef.currentConcurrency` | `References.CurrentConcurrency` | | `FiberRef.currentLogLevel` | `References.CurrentLogLevel` | | `FiberRef.currentMinimumLogLevel` | `References.MinimumLogLevel` | | `FiberRef.currentLogAnnotations` | `References.CurrentLogAnnotations` | @@ -88,8 +87,8 @@ set via `Effect.provideService`, which scopes the value to the provided effect. import { Effect, FiberRef } from "effect" const program = Effect.gen(function*() { - yield* FiberRef.set(FiberRef.currentConcurrency, 10) - // subsequent code sees concurrency = 10 + yield* FiberRef.set(FiberRef.currentMaxOpsBeforeYield, 500) + // subsequent code sees maxOpsBeforeYield = 500 }) ``` @@ -100,10 +99,10 @@ import { Effect, References } from "effect" const program = Effect.provideService( Effect.gen(function*() { - const concurrency = yield* References.CurrentConcurrency - console.log(concurrency) // 10 + const maxOps = yield* References.MaxOpsBeforeYield + console.log(maxOps) // 500 }), - References.CurrentConcurrency, - 10 + References.MaxOpsBeforeYield, + 500 ) ``` diff --git a/.context/effect/migration/schema.md b/.context/effect/migration/schema.md index 5cbbd72c1..d2d4db1c7 100644 --- a/.context/effect/migration/schema.md +++ b/.context/effect/migration/schema.md @@ -30,6 +30,7 @@ This document maps v3 Schema APIs to their v4 equivalents. Simple renames and ar | `Redacted` | `RedactedFromValue` | rename | | `EitherFromSelf` | `Result` | rename | | `DateFromNumber` | `DateFromMillis` | rename | +| `Date` | `DateFromString` | restructure | | `TaggedError` | `TaggedErrorClass` | rename | | `decodeUnknown` | `decodeUnknownEffect` | rename | | `decode` | `decodeEffect` | rename | @@ -87,6 +88,30 @@ The following `*FromSelf` schemas have been renamed to drop the suffix: `DateFromSelf` → `Date`, `DurationFromSelf` → `Duration`, `ChunkFromSelf` → `Chunk`, `ReadonlyMapFromSelf` → `ReadonlyMap`, `ReadonlySetFromSelf` → `ReadonlySet`, `HashMapFromSelf` → `HashMap`, `HashSetFromSelf` → `HashSet`, `BigDecimalFromSelf` → `BigDecimal`, `CauseFromSelf` → `Cause`, `ExitFromSelf` → `Exit`, `OptionFromSelf` → `Option`, `RegExpFromSelf` → `RegExp` +### `Date` encoded contract + +**Migration: restructure** + +In v3, `Schema.Date` decoded an ISO date string to a `Date` and rejected invalid dates. In v4, `Schema.Date` is the renamed `Schema.DateFromSelf`, so it expects a valid `Date` as its encoded value. Existing code can still type-check after upgrading while no longer accepting the same input. + +v3 + +```ts +import { Schema } from "effect" + +const DateFromIsoString = Schema.Date +``` + +v4 + +```ts +import { Schema } from "effect" + +const DateFromIsoString = Schema.DateFromString +``` + +`Schema.DateFromString` preserves the string-to-`Date` transformation and rejects strings that produce invalid dates. + ### Filter renames All filters have been renamed with an `is` prefix and now use `check(...)` or `pipe(Schema.check(...))`: @@ -243,7 +268,7 @@ v4 ```ts import { Schema, SchemaRepresentation } from "effect" -const doc = SchemaRepresentation.fromAST(Schema.String.ast) +const doc = SchemaRepresentation.toRepresentation(Schema.String.ast) const multi = SchemaRepresentation.toMultiDocument(doc) const codeDoc = SchemaRepresentation.toCodeDocument(multi) console.log(codeDoc.codes[0].Type) @@ -873,14 +898,14 @@ const NumberFromString = Schema.transformOrFail(Schema.String, Schema.Number, { v4 ```ts -import { Effect, Number, Option, Schema, SchemaGetter, SchemaIssue } from "effect" +import { Effect, Number, Schema, SchemaGetter, SchemaIssue } from "effect" const NumberFromString = Schema.String.pipe( Schema.decodeTo(Schema.Number, { decode: SchemaGetter.transformOrFail((s) => { const n = Number.parse(s) if (n === undefined) { - return Effect.fail(new SchemaIssue.InvalidValue(Option.some(s))) + return Effect.fail(new SchemaIssue.InvalidValue()) } return Effect.succeed(n) }), diff --git a/.context/effect/migration/v3-to-v4.md b/.context/effect/migration/v3-to-v4.md index 5e3f3c771..459cdc460 100644 --- a/.context/effect/migration/v3-to-v4.md +++ b/.context/effect/migration/v3-to-v4.md @@ -1,14 +1,12 @@ -# v3 to v4 Import and API Rename Maps + -Mapped modules: 290 -No counterpart: 43 -API renames: 53 +# v3 to v4 Migration Reference -This file is intended for migration agents. It contains user-facing import -specifier mappings and API rename mappings. +Base: `3d390f232bdbc3f0d3d6a2ae3c775084f494b547` (`3d390f232bdbc3f0d3d6a2ae3c775084f494b547`) -Use the import map when rewriting import declarations. Use the API renames when -rewriting renamed symbols. +Head: `main` (`b938c8ad2823bd88493187922f7d9090eff037b6`) + +This file is generated from the API diff and `migration/annotations/*.yaml`. ## Import Map @@ -314,6 +312,7 @@ These v4 modules did not have a mapped v3 module. Treat them as v4-only unless a more specific migration guide says otherwise. ```text +@effect/platform-node/NodeMultipartParser (barrel: @effect/platform-node) effect/ErrorReporter (barrel: effect) effect/Filter (barrel: effect) effect/JsonPatch (barrel: effect) @@ -324,7 +323,6 @@ effect/Optic (barrel: effect) effect/Pull (barrel: effect) effect/SchemaGetter (barrel: effect) effect/SchemaRepresentation (barrel: effect) -effect/SchemaUtils (barrel: effect) effect/Semaphore (barrel: effect) effect/Stdio (barrel: effect) effect/TxChunk (barrel: effect) @@ -341,11 +339,9 @@ effect/unstable/eventlog/EventLogSessionAuth (barrel: effect/unstable/eventlog) effect/unstable/eventlog/SqlEventLogServerUnencrypted (barrel: effect/unstable/eventlog) effect/unstable/http/FindMyWay (barrel: effect/unstable/http) effect/unstable/http/HttpStaticServer (barrel: effect/unstable/http) -effect/unstable/http/Multipasta (barrel: effect/unstable/http) -effect/unstable/http/Multipasta/HeadersParser (barrel: effect/unstable/http) -effect/unstable/http/Multipasta/Node (barrel: effect/unstable/http) -effect/unstable/http/Multipasta/Search (barrel: effect/unstable/http) -effect/unstable/http/Multipasta/Web (barrel: effect/unstable/http) +effect/unstable/http/MultipartParser (barrel: effect/unstable/http) +effect/unstable/http/MultipartParser/HeadersParser (barrel: effect/unstable/http) +effect/unstable/http/MultipartParser/Search (barrel: effect/unstable/http) effect/unstable/httpapi/HttpApiTest (barrel: effect/unstable/httpapi) effect/unstable/observability/PrometheusMetrics (barrel: effect/unstable/observability) effect/unstable/persistence/Redis (barrel: effect/unstable/persistence) @@ -359,63 +355,15765 @@ effect/unstable/reactivity/Hydration (barrel: effect/unstable/reactivity) effect/unstable/rpc/Utils (barrel: effect/unstable/rpc) ``` -## API Renames +## Removed Modules -Each line is `v3 API -> v4 API`. Use these mappings when rewriting renamed -symbols from v3 source code to v4. +- `@effect/ai` -> `effect/unstable/ai`: The @effect/ai package was merged into the effect package; import the effect/unstable/ai barrel or import specific modules directly (e.g. effect/unstable/ai/\). +- `@effect/ai-amazon-bedrock` -> `none`: The @effect/ai-amazon-bedrock provider package was removed from v4 with no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. +- `@effect/ai-amazon-bedrock/AmazonBedrockClient` -> `none`: The @effect/ai-amazon-bedrock provider package was removed from v4, so AmazonBedrockClient, layer, layerConfig, make, and Service have no direct replacements. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. +- `@effect/ai-amazon-bedrock/AmazonBedrockConfig` -> `none`: The @effect/ai-amazon-bedrock provider package was removed from v4, so AmazonBedrockConfig has no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. +- `@effect/ai-amazon-bedrock/AmazonBedrockLanguageModel` -> `none`: The @effect/ai-amazon-bedrock language-model integration was removed from v4. Use another supported v4 provider or implement LanguageModel.LanguageModel with @aws-sdk/client-bedrock-runtime. +- `@effect/ai-amazon-bedrock/AmazonBedrockSchema` -> `none`: The @effect/ai-amazon-bedrock package was removed from v4, including its hand-written Bedrock schemas. Use @aws-sdk/client-bedrock-runtime request and response types, or schemas supplied by a custom v4 provider integration. +- `@effect/ai-amazon-bedrock/AmazonBedrockTool` -> `none`: The @effect/ai-amazon-bedrock package and its Anthropic-on-Bedrock provider tools were removed from v4. Recreate the capability in a custom provider integration if the Bedrock model still requires it. +- `@effect/ai-amazon-bedrock/EventStreamEncoding` -> `none`: The @effect/ai-amazon-bedrock package and its AWS event-stream decoder were removed from v4. Use the AWS SDK's Bedrock Runtime streaming support or implement decoding in a custom provider client. +- `@effect/ai-amazon-bedrock/index` -> `none`: The @effect/ai-amazon-bedrock provider package was removed from v4 with no direct replacement. Use @aws-sdk/client-bedrock-runtime directly or build a custom v4 provider integration. +- `@effect/ai-anthropic/AnthropicTokenizer`: No single module replacement; follow the curated per-API guidance below. +- `@effect/ai-anthropic/index` -> `@effect/ai-anthropic`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-anthropic package root or import specific modules directly. +- `@effect/ai-google` -> `none`: The @effect/ai-google provider package was removed from v4 with no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. +- `@effect/ai-google/Generated` -> `none`: The @effect/ai-google package was removed from v4, so this generated Google API schema has no Effect v4 replacement. Use Google's current SDK/API types directly or route supported Gemini models through another v4 provider integration. +- `@effect/ai-google/GoogleClient` -> `none`: The @effect/ai-google provider package was removed from v4 and has no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. +- `@effect/ai-google/GoogleConfig` -> `none`: The @effect/ai-google provider package was removed from v4 and has no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. +- `@effect/ai-google/GoogleLanguageModel` -> `none`: The @effect/ai-google language-model integration was removed from v4. Use a supported v4 provider integration for Gemini models or implement LanguageModel.LanguageModel against Google's current SDK. +- `@effect/ai-google/GoogleTool` -> `none`: The @effect/ai-google package and its provider-defined tools were removed from v4. Model this capability in the provider integration you adopt, or define an application Tool when the replacement provider supports it. +- `@effect/ai-google/index` -> `none`: The @effect/ai-google provider package was removed from v4 with no direct replacement. Use a supported v4 provider integration for Gemini models or integrate Google's current SDK directly. +- `@effect/ai-openai/OpenAiTokenizer`: No single module replacement; follow the curated per-API guidance below. +- `@effect/ai-openai/index` -> `@effect/ai-openai`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-openai package root or import specific modules directly. +- `@effect/ai-openrouter/index` -> `@effect/ai-openrouter`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/ai-openrouter package root or import specific modules directly. +- `@effect/ai/AiError` -> `effect/unstable/ai/AiError` +- `@effect/ai/Chat` -> `effect/unstable/ai/Chat` +- `@effect/ai/EmbeddingModel` -> `effect/unstable/ai/EmbeddingModel` +- `@effect/ai/IdGenerator` -> `effect/unstable/ai/IdGenerator` +- `@effect/ai/LanguageModel` -> `effect/unstable/ai/LanguageModel` +- `@effect/ai/McpSchema` -> `effect/unstable/ai/McpSchema` +- `@effect/ai/McpServer` -> `effect/unstable/ai/McpServer` +- `@effect/ai/Model` -> `effect/unstable/ai/Model` +- `@effect/ai/Prompt` -> `effect/unstable/ai/Prompt` +- `@effect/ai/Response` -> `effect/unstable/ai/Response` +- `@effect/ai/Telemetry` -> `effect/unstable/ai/Telemetry` +- `@effect/ai/Tokenizer` -> `effect/unstable/ai/Tokenizer` +- `@effect/ai/Tool` -> `effect/unstable/ai/Tool` +- `@effect/ai/Toolkit` -> `effect/unstable/ai/Toolkit` +- `@effect/ai/index` -> `effect/unstable/ai`: The package barrel was removed; import the same namespaces from the effect/unstable/ai barrel or import specific modules directly. +- `@effect/cli` -> `effect/unstable/cli`: The @effect/cli package was merged into the effect package; import the effect/unstable/cli barrel or import specific modules directly (e.g. effect/unstable/cli/\). +- `@effect/cli/Args` -> `effect/unstable/cli/Argument` +- `@effect/cli/AutoCorrect` -> `none`: V4 suggestion distance is internal and fixed; the public configurable distance helper was removed. +- `@effect/cli/BuiltInOptions` -> `effect/unstable/cli/GlobalFlag` +- `@effect/cli/CliApp`: No single module replacement; follow the curated per-API guidance below. +- `@effect/cli/CliConfig`: No single module replacement; follow the curated per-API guidance below. +- `@effect/cli/Command` -> `effect/unstable/cli/Command` +- `@effect/cli/CommandDescriptor` -> `effect/unstable/cli/Completions` +- `@effect/cli/CommandDirective`: No single module replacement; follow the curated per-API guidance below. +- `@effect/cli/ConfigFile`: No single module replacement; follow the curated per-API guidance below. +- `@effect/cli/HelpDoc` -> `effect/unstable/cli/HelpDoc` +- `@effect/cli/HelpDoc/Span` -> `none`: The Span ADT was removed; v4 help fields are strings and terminal styling is owned by CliOutput. +- `@effect/cli/Options` -> `effect/unstable/cli/Flag` +- `@effect/cli/Primitive` -> `effect/unstable/cli/Primitive` +- `@effect/cli/Prompt` -> `effect/unstable/cli/Prompt` +- `@effect/cli/Usage` -> `none`: The Usage ADT was removed; Command builds a plain HelpDoc.usage string internally. +- `@effect/cli/ValidationError` -> `effect/unstable/cli/CliError` +- `@effect/cli/index` -> `effect/unstable/cli`: The package barrel was removed; import the same namespaces from the effect/unstable/cli barrel or import specific modules directly. +- `@effect/cluster` -> `effect/unstable/cluster`: The @effect/cluster package was merged into the effect package; import the effect/unstable/cluster barrel or import specific modules directly (e.g. effect/unstable/cluster/\). +- `@effect/cluster/ClusterCron` -> `effect/unstable/cluster/ClusterCron` +- `@effect/cluster/ClusterError` -> `effect/unstable/cluster/ClusterError` +- `@effect/cluster/ClusterMetrics` -> `effect/unstable/cluster/ClusterMetrics` +- `@effect/cluster/ClusterSchema` -> `effect/unstable/cluster/ClusterSchema` +- `@effect/cluster/ClusterWorkflowEngine` -> `effect/unstable/cluster/ClusterWorkflowEngine` +- `@effect/cluster/DeliverAt` -> `effect/unstable/cluster/DeliverAt` +- `@effect/cluster/Entity` -> `effect/unstable/cluster/Entity` +- `@effect/cluster/EntityAddress` -> `effect/unstable/cluster/EntityAddress` +- `@effect/cluster/EntityId` -> `effect/unstable/cluster/EntityId` +- `@effect/cluster/EntityProxy` -> `effect/unstable/cluster/EntityProxy` +- `@effect/cluster/EntityProxyServer` -> `effect/unstable/cluster/EntityProxyServer` +- `@effect/cluster/EntityResource` -> `effect/unstable/cluster/EntityResource` +- `@effect/cluster/EntityType` -> `effect/unstable/cluster/EntityType` +- `@effect/cluster/Envelope` -> `effect/unstable/cluster/Envelope` +- `@effect/cluster/HttpRunner` -> `effect/unstable/cluster/HttpRunner` +- `@effect/cluster/K8sHttpClient` -> `effect/unstable/cluster/K8sHttpClient` +- `@effect/cluster/MachineId` -> `effect/unstable/cluster/MachineId` +- `@effect/cluster/Message` -> `effect/unstable/cluster/Message` +- `@effect/cluster/MessageStorage` -> `effect/unstable/cluster/MessageStorage` +- `@effect/cluster/Reply` -> `effect/unstable/cluster/Reply` +- `@effect/cluster/Runner` -> `effect/unstable/cluster/Runner` +- `@effect/cluster/RunnerAddress` -> `effect/unstable/cluster/RunnerAddress` +- `@effect/cluster/RunnerHealth` -> `effect/unstable/cluster/RunnerHealth` +- `@effect/cluster/RunnerServer` -> `effect/unstable/cluster/RunnerServer` +- `@effect/cluster/RunnerStorage` -> `effect/unstable/cluster/RunnerStorage` +- `@effect/cluster/Runners` -> `effect/unstable/cluster/Runners` +- `@effect/cluster/ShardId` -> `effect/unstable/cluster/ShardId` +- `@effect/cluster/Sharding` -> `effect/unstable/cluster/Sharding` +- `@effect/cluster/ShardingConfig` -> `effect/unstable/cluster/ShardingConfig` +- `@effect/cluster/ShardingRegistrationEvent` -> `effect/unstable/cluster/ShardingRegistrationEvent` +- `@effect/cluster/SingleRunner` -> `effect/unstable/cluster/SingleRunner` +- `@effect/cluster/Singleton` -> `effect/unstable/cluster/Singleton` +- `@effect/cluster/SingletonAddress` -> `effect/unstable/cluster/SingletonAddress` +- `@effect/cluster/Snowflake` -> `effect/unstable/cluster/Snowflake` +- `@effect/cluster/SocketRunner` -> `effect/unstable/cluster/SocketRunner` +- `@effect/cluster/SqlMessageStorage` -> `effect/unstable/cluster/SqlMessageStorage` +- `@effect/cluster/SqlRunnerStorage` -> `effect/unstable/cluster/SqlRunnerStorage` +- `@effect/cluster/TestRunner` -> `effect/unstable/cluster/TestRunner` +- `@effect/cluster/index` -> `effect/unstable/cluster`: The package barrel was removed; import the same namespaces from the effect/unstable/cluster barrel or import specific modules directly. +- `@effect/experimental` -> `none`: The @effect/experimental package was folded into the effect package, split across effect/unstable/\* (devtools, eventlog, persistence, reactivity, ...); follow the Import Map for each module. +- `@effect/experimental/DevTools` -> `effect/unstable/devtools/DevTools` +- `@effect/experimental/DevTools/Client` -> `effect/unstable/devtools/DevToolsClient` +- `@effect/experimental/DevTools/Domain` -> `effect/unstable/devtools/DevToolsSchema` +- `@effect/experimental/DevTools/Server` -> `effect/unstable/devtools/DevToolsServer` +- `@effect/experimental/Event` -> `effect/unstable/eventlog/Event` +- `@effect/experimental/EventGroup` -> `effect/unstable/eventlog/EventGroup` +- `@effect/experimental/EventJournal` -> `effect/unstable/eventlog/EventJournal` +- `@effect/experimental/EventLog` -> `effect/unstable/eventlog/EventLog` +- `@effect/experimental/EventLogEncryption` -> `effect/unstable/eventlog/EventLogEncryption` +- `@effect/experimental/EventLogRemote` -> `effect/unstable/eventlog/EventLogMessage`, `effect/unstable/eventlog/EventLogRemote` +- `@effect/experimental/EventLogServer` -> `effect/unstable/eventlog/EventLogServer`, `effect/unstable/eventlog/EventLogServerEncrypted` +- `@effect/experimental/EventLogServer/Cloudflare` -> `none`: The Cloudflare adapter was not ported; combine EventLogServerEncrypted.layer with a custom Durable Object RpcServer.Protocol adapter. +- `@effect/experimental/Machine` -> `none`: The experimental local Machine actor runtime, model, boot process, constructors, brands, and serializable variants were not ported to v4. Redesign request contracts with Rpc/RpcGroup and choose Cluster Entity, Workflow, or a local actor built from Queue, Ref, PubSub, and scoped fibers according to the required semantics; ClusterWorkflowEngine is a different durable Workflow abstraction. For serializable actors, define schemas with Rpc/RpcGroup and choose Cluster Entity or Workflow explicitly. Context and initialization helpers (including the serializable initialization contract), input/private/public/state extractors, and the Machine-specific handler context were also removed, so request handling and state management must be explicit. Use ordinary Effect tracing controls and Effect.retry instead of the removed Machine-specific wrappers; its defect wrapper was also removed. Snapshot restoration was not ported, so implement persistence explicitly for the replacement architecture. +- `@effect/experimental/Machine/Procedure` -> `none`: The stateful Machine Procedure model, its serializable variant and guard, and both Procedure brands were not ported to v4. Define request contracts with Rpc (using schemas for serializable procedures) and implement state handling in an explicit actor architecture, because Rpc provides only the request contract. The handler context, context and request extractors, and no-reply sentinel were removed; use the corresponding Rpc request types after redesigning the contract. Replace the removed tagged-request base and helpers with schema-backed Rpc requests and Rpc helper types where appropriate. +- `@effect/experimental/Machine/ProcedureList` -> `none`: The stateful Machine ProcedureList abstraction and brand were not ported to v4. RpcGroup is the closest protocol collection for its schema-backed operations, but it has no initial state or public/private visibility split. Implement state handling and initialization in the replacement actor or workflow, and enforce visibility in that architecture. +- `@effect/experimental/Machine/SerializableProcedureList` -> `none`: The serializable stateful ProcedureList abstraction was not ported to v4. RpcGroup is the closest protocol collection for its schema-backed operations, but it has no initial state or public/private visibility split. Implement state handling and initialization in the replacement actor or workflow, and enforce visibility in that architecture. +- `@effect/experimental/PersistedCache` -> `effect/unstable/persistence/PersistedCache` +- `@effect/experimental/PersistedQueue` -> `effect/unstable/persistence/PersistedQueue` +- `@effect/experimental/PersistedQueue/Redis`: No single module replacement; follow the curated per-API guidance below. +- `@effect/experimental/Persistence` -> `effect/unstable/persistence/Persistable`, `effect/unstable/persistence/Persistence` +- `@effect/experimental/Persistence/Lmdb` -> `none`: The LMDB backend was not ported; implement a custom BackingPersistence layer or use a supported Kvs, Redis, or SQL backend. +- `@effect/experimental/Persistence/Redis`: No single module replacement; follow the curated per-API guidance below. +- `@effect/experimental/RateLimiter` -> `effect/unstable/persistence/RateLimiter` +- `@effect/experimental/RateLimiter/Redis`: No single module replacement; follow the curated per-API guidance below. +- `@effect/experimental/Reactivity` -> `effect/unstable/reactivity/Reactivity` +- `@effect/experimental/RequestResolver`: No single module replacement; follow the curated per-API guidance below. +- `@effect/experimental/Sse` -> `effect/unstable/encoding/Sse` +- `@effect/experimental/VariantSchema` -> `effect/unstable/schema/VariantSchema` +- `@effect/experimental/index` -> `none`: The package barrel was removed along with the package; import each module from its new effect/unstable/\* location per the Import Map. +- `@effect/opentelemetry/Logger`: No single module replacement; follow the curated per-API guidance below. +- `@effect/opentelemetry/Metrics`: No single module replacement; follow the curated per-API guidance below. +- `@effect/opentelemetry/Otlp` -> `effect/unstable/observability/Otlp` +- `@effect/opentelemetry/OtlpLogger` -> `effect/unstable/observability/OtlpLogger` +- `@effect/opentelemetry/OtlpMetrics` -> `effect/unstable/observability/OtlpMetrics` +- `@effect/opentelemetry/OtlpResource` -> `effect/unstable/observability/OtlpResource` +- `@effect/opentelemetry/OtlpSerialization` -> `effect/unstable/observability/OtlpSerialization` +- `@effect/opentelemetry/OtlpTracer` -> `effect/unstable/observability/OtlpTracer` +- `@effect/opentelemetry/Tracer`: No single module replacement; follow the curated per-API guidance below. +- `@effect/opentelemetry/index` -> `@effect/opentelemetry`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/opentelemetry package root or import specific modules directly. +- `@effect/platform` -> `none`: The @effect/platform package was folded into the effect package: core services live in effect root modules (e.g. effect/FileSystem, effect/Path) and HTTP in effect/unstable/http; follow the Import Map for each module. +- `@effect/platform-browser/index` -> `@effect/platform-browser`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-browser package root or import specific modules directly. +- `@effect/platform-bun/BunCommandExecutor`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-bun/BunContext`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-bun/BunFileSystem/ParcelWatcher`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-bun/BunKeyValueStore` -> `effect/unstable/persistence/KeyValueStore`: layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via BunServices.layer or BunFileSystem.layer with BunPath.layer. +- `@effect/platform-bun/index` -> `@effect/platform-bun`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-bun package root or import specific modules directly. +- `@effect/platform-node-shared/NodeCommandExecutor`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node-shared/NodeFileSystem/ParcelWatcher`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node-shared/NodeKeyValueStore` -> `effect/unstable/persistence/KeyValueStore`: layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via NodeServices.layer or NodeFileSystem.layer with NodePath.layer. +- `@effect/platform-node-shared/NodeMultipart`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node/NodeCommandExecutor`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node/NodeContext`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node/NodeFileSystem/ParcelWatcher`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform-node/NodeKeyValueStore` -> `effect/unstable/persistence/KeyValueStore`: layerFileSystem is now platform-neutral as KeyValueStore.layerFileSystem(directory); provide FileSystem and Path via NodeServices.layer or NodeFileSystem.layer with NodePath.layer. +- `@effect/platform-node/index` -> `@effect/platform-node`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/platform-node package root or import specific modules directly. +- `@effect/platform/ChannelSchema` -> `effect/ChannelSchema` +- `@effect/platform/Command` -> `effect/unstable/process/ChildProcess` +- `@effect/platform/CommandExecutor` -> `effect/unstable/process/ChildProcessSpawner` +- `@effect/platform/Cookies` -> `effect/unstable/http/Cookies` +- `@effect/platform/Effectify` -> `effect/Effect`: effectify moved into the Effect module as Effect.effectify; the Effectify and EffectifyError type helpers live in the Effect namespace as well. +- `@effect/platform/Error` -> `effect/PlatformError` +- `@effect/platform/Etag` -> `effect/unstable/http/Etag` +- `@effect/platform/FetchHttpClient` -> `effect/unstable/http/FetchHttpClient` +- `@effect/platform/FileSystem` -> `effect/FileSystem` +- `@effect/platform/Headers` -> `effect/unstable/http/Headers` +- `@effect/platform/HttpApi` -> `effect/unstable/httpapi/HttpApi` +- `@effect/platform/HttpApiBuilder` -> `effect/unstable/httpapi/HttpApiBuilder` +- `@effect/platform/HttpApiClient` -> `effect/unstable/httpapi/HttpApiClient` +- `@effect/platform/HttpApiEndpoint` -> `effect/unstable/httpapi/HttpApiEndpoint` +- `@effect/platform/HttpApiError` -> `effect/unstable/httpapi/HttpApiError` +- `@effect/platform/HttpApiGroup` -> `effect/unstable/httpapi/HttpApiGroup` +- `@effect/platform/HttpApiMiddleware` -> `effect/unstable/httpapi/HttpApiMiddleware` +- `@effect/platform/HttpApiScalar` -> `effect/unstable/httpapi/HttpApiScalar` +- `@effect/platform/HttpApiSchema` -> `effect/unstable/httpapi/HttpApiSchema` +- `@effect/platform/HttpApiSecurity` -> `effect/unstable/httpapi/HttpApiSecurity` +- `@effect/platform/HttpApiSwagger` -> `effect/unstable/httpapi/HttpApiSwagger` +- `@effect/platform/HttpApp` -> `effect/unstable/http/HttpEffect` +- `@effect/platform/HttpBody` -> `effect/unstable/http/HttpBody` +- `@effect/platform/HttpClient` -> `effect/unstable/http/HttpClient` +- `@effect/platform/HttpClientError` -> `effect/unstable/http/HttpClientError` +- `@effect/platform/HttpClientRequest` -> `effect/unstable/http/HttpClientRequest` +- `@effect/platform/HttpClientResponse` -> `effect/unstable/http/HttpClientResponse` +- `@effect/platform/HttpIncomingMessage` -> `effect/unstable/http/HttpIncomingMessage` +- `@effect/platform/HttpLayerRouter`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform/HttpMethod` -> `effect/unstable/http/HttpMethod` +- `@effect/platform/HttpMiddleware` -> `effect/unstable/http/HttpMiddleware` +- `@effect/platform/HttpMultiplex` -> `none`: The HttpMultiplex module, value, constructor, and nominal type id were removed with no v4 counterpart. Replace them with a custom first-match Effect dispatcher requiring HttpServerRequest; initialize an empty dispatcher, then add or fold predicate/app pairs into it. Recreate header helpers with predicates over lower-cased request header values using exact equality, String.startsWith, String.endsWith, or RegExp.test; recreate host helpers with the same comparisons over request.headers.host. +- `@effect/platform/HttpPlatform` -> `effect/unstable/http/HttpPlatform` +- `@effect/platform/HttpRouter` -> `effect/unstable/http/HttpRouter` +- `@effect/platform/HttpServer` -> `effect/unstable/http/HttpServer` +- `@effect/platform/HttpServerError` -> `effect/unstable/http/HttpServerError` +- `@effect/platform/HttpServerRequest` -> `effect/unstable/http/HttpServerRequest` +- `@effect/platform/HttpServerRespondable` -> `effect/unstable/http/HttpServerRespondable` +- `@effect/platform/HttpServerResponse` -> `effect/unstable/http/HttpServerResponse` +- `@effect/platform/HttpTraceContext` -> `effect/unstable/http/HttpTraceContext` +- `@effect/platform/KeyValueStore` -> `effect/unstable/persistence/KeyValueStore` +- `@effect/platform/MsgPack` -> `effect/unstable/encoding/Msgpack` +- `@effect/platform/Multipart` -> `effect/unstable/http/Multipart` +- `@effect/platform/Ndjson` -> `effect/unstable/encoding/Ndjson` +- `@effect/platform/OpenApi` -> `effect/unstable/httpapi/OpenApi` +- `@effect/platform/OpenApiJsonSchema`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform/Path` -> `effect/Path` +- `@effect/platform/PlatformConfigProvider`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform/PlatformLogger` -> `effect/Logger`: toFile moved to Logger.toFile; it still requires a FileSystem service (e.g. NodeFileSystem.layer) and Scope. +- `@effect/platform/Runtime`: No single module replacement; follow the curated per-API guidance below. +- `@effect/platform/Socket` -> `effect/unstable/socket/Socket` +- `@effect/platform/SocketServer` -> `effect/unstable/socket/SocketServer` +- `@effect/platform/Template` -> `effect/unstable/http/Template` +- `@effect/platform/Terminal` -> `effect/Terminal` +- `@effect/platform/Transferable` -> `effect/unstable/workers/Transferable` +- `@effect/platform/Url` -> `effect/unstable/http/Url` +- `@effect/platform/UrlParams` -> `effect/unstable/http/UrlParams` +- `@effect/platform/Worker` -> `effect/unstable/workers/Worker` +- `@effect/platform/WorkerError` -> `effect/unstable/workers/WorkerError` +- `@effect/platform/WorkerRunner` -> `effect/unstable/workers/WorkerRunner` +- `@effect/platform/index` -> `none`: The package barrel was removed along with the package; import each module from its new effect location (e.g. effect/FileSystem, effect/unstable/http/HttpClient) per the Import Map. +- `@effect/printer` -> `none`: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. +- `@effect/printer-ansi` -> `none`: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. +- `@effect/printer-ansi/Ansi` -> `none`: The @effect/printer-ansi package was removed in v4 with no public replacement. Use a maintained ANSI library or local escape-string helpers; the v4 CLI ANSI helpers are internal and cannot be imported. +- `@effect/printer-ansi/AnsiDoc` -> `none`: The @effect/printer-ansi package and its annotated document algebra were removed in v4. Use strings or another pretty-printing library; for Effect CLI help only, use HelpDoc with CliOutput from effect/unstable/cli. +- `@effect/printer-ansi/Color` -> `none`: The @effect/printer-ansi package was removed in v4, and Effect no longer provides a public ANSI color ADT. Use a maintained ANSI library or local escape-string helpers. +- `@effect/printer-ansi/index` -> `none`: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. +- `@effect/printer/Doc` -> `none`: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. +- `@effect/printer/DocStream` -> `none`: The @effect/printer package and its laid-out DocStream intermediate representation were removed in v4. Use a target-specific renderer or another pretty-printing library. +- `@effect/printer/DocTree` -> `none`: The @effect/printer package and its structured DocTree rendering representation were removed in v4. Use a target-specific tree and renderer or another pretty-printing library. +- `@effect/printer/Flatten` -> `none`: This printer-specific flattening result was removed with the @effect/printer document algebra in v4 and has no direct replacement. +- `@effect/printer/Layout` -> `none`: The @effect/printer layout pipeline was removed in v4 with no general replacement. Use another pretty-printing library; for Effect CLI output only, use CliOutput from effect/unstable/cli. +- `@effect/printer/Optimize` -> `none`: The @effect/printer document optimizer was removed with the document algebra in v4. String-based output needs no equivalent optimization stage. +- `@effect/printer/PageWidth` -> `none`: The @effect/printer page-width layout model was removed in v4 with no direct replacement. Use Terminal.columns for terminal dimensions, or another pretty-printing library for page-width-aware layout. +- `@effect/printer/index` -> `none`: The @effect/printer document algebra was removed in v4 with no direct replacement. Use strings and joins for simple output, or adopt another pretty-printing library when adaptive layout is required. +- `@effect/rpc` -> `effect/unstable/rpc`: The @effect/rpc package was merged into the effect package; import the effect/unstable/rpc barrel or import specific modules directly (e.g. effect/unstable/rpc/\). +- `@effect/rpc/Rpc` -> `effect/unstable/rpc/Rpc` +- `@effect/rpc/RpcClient` -> `effect/unstable/rpc/RpcClient` +- `@effect/rpc/RpcClientError` -> `effect/unstable/rpc/RpcClientError` +- `@effect/rpc/RpcGroup` -> `effect/unstable/rpc/RpcGroup` +- `@effect/rpc/RpcMessage` -> `effect/unstable/rpc/RpcMessage` +- `@effect/rpc/RpcMiddleware` -> `effect/unstable/rpc/RpcMiddleware` +- `@effect/rpc/RpcSchema` -> `effect/unstable/rpc/RpcSchema` +- `@effect/rpc/RpcSerialization` -> `effect/unstable/rpc/RpcSerialization` +- `@effect/rpc/RpcServer` -> `effect/unstable/rpc/RpcServer` +- `@effect/rpc/RpcTest` -> `effect/unstable/rpc/RpcTest` +- `@effect/rpc/RpcWorker` -> `effect/unstable/rpc/RpcWorker` +- `@effect/rpc/index` -> `effect/unstable/rpc`: The package barrel was removed; import the same namespaces from the effect/unstable/rpc barrel or import specific modules directly. +- `@effect/sql` -> `effect/unstable/sql`: The @effect/sql package was merged into the effect package; import the effect/unstable/sql barrel or import specific modules directly (e.g. effect/unstable/sql/\). +- `@effect/sql-clickhouse/index` -> `@effect/sql-clickhouse`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-clickhouse package root or import specific modules directly. +- `@effect/sql-d1/index` -> `@effect/sql-d1`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-d1 package root or import specific modules directly. +- `@effect/sql-drizzle/Mysql`: No single module replacement; follow the curated per-API guidance below. +- `@effect/sql-drizzle/Pg`: No single module replacement; follow the curated per-API guidance below. +- `@effect/sql-drizzle/Sqlite`: No single module replacement; follow the curated per-API guidance below. +- `@effect/sql-kysely/Kysely` -> `none`: The Effect integration was removed. Use native kysely#Kysely, explicitly wrap promise execution with Effect.tryPromise, and define an application service if needed. No Effect-native equivalent remains; construct native new Kysely(config) and explicitly wrap builder execution and errors with Effect.tryPromise. +- `@effect/sql-kysely/Mssql` -> `none`: The integration was removed; use native Kysely with MssqlDialect and wrap promises, or rewrite against @effect/sql-mssql for Effect-native queries. +- `@effect/sql-kysely/Mysql` -> `none`: The integration was removed; use native Kysely with MysqlDialect and wrap promises, or rewrite against @effect/sql-mysql2 for Effect-native queries. +- `@effect/sql-kysely/Pg` -> `none`: The integration was removed; use native Kysely with PostgresDialect and wrap promises, or rewrite against @effect/sql-pg for Effect-native queries. +- `@effect/sql-kysely/Sqlite` -> `none`: The integration was removed; use native Kysely with SqliteDialect and wrap promises, or rewrite against a matching @effect/sql-sqlite-\* client. +- `@effect/sql-kysely/patch.types` -> `none`: The @effect/sql-kysely package was removed in v4 along with its kysely type patches; depend on native kysely types directly and wrap query execution with Effect.tryPromise. +- `@effect/sql-libsql/index` -> `@effect/sql-libsql`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-libsql package root or import specific modules directly. +- `@effect/sql-mssql/index` -> `@effect/sql-mssql`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-mssql package root or import specific modules directly. +- `@effect/sql-mysql2/index` -> `@effect/sql-mysql2`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-mysql2 package root or import specific modules directly. +- `@effect/sql-pg/index` -> `@effect/sql-pg`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-pg package root or import specific modules directly. +- `@effect/sql-sqlite-bun/index` -> `@effect/sql-sqlite-bun`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-bun package root or import specific modules directly. +- `@effect/sql-sqlite-do/index` -> `@effect/sql-sqlite-do`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-do package root or import specific modules directly. +- `@effect/sql-sqlite-node/index` -> `@effect/sql-sqlite-node`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-node package root or import specific modules directly. +- `@effect/sql-sqlite-react-native/index` -> `@effect/sql-sqlite-react-native`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-react-native package root or import specific modules directly. +- `@effect/sql-sqlite-wasm/index` -> `@effect/sql-sqlite-wasm`: The explicit /index entrypoint was removed; import the same namespaces from the @effect/sql-sqlite-wasm package root or import specific modules directly. +- `@effect/sql/Migrator` -> `effect/unstable/sql/Migrator` +- `@effect/sql/Migrator/FileSystem` -> `effect/unstable/sql/Migrator`: fromFileSystem was merged into the main Migrator module with the same (directory) signature; use Migrator.fromFileSystem as the loader. +- `@effect/sql/Model` -> `effect/unstable/schema/Model`, `effect/unstable/sql/SqlModel` +- `@effect/sql/SqlClient` -> `effect/unstable/sql/SqlClient` +- `@effect/sql/SqlConnection` -> `effect/unstable/sql/SqlConnection` +- `@effect/sql/SqlError` -> `effect/unstable/sql/SqlError` +- `@effect/sql/SqlEventJournal` -> `effect/unstable/eventlog/SqlEventJournal` +- `@effect/sql/SqlEventLogServer` -> `effect/unstable/eventlog/SqlEventLogServerEncrypted` +- `@effect/sql/SqlPersistedQueue`: No single module replacement; follow the curated per-API guidance below. +- `@effect/sql/SqlResolver` -> `effect/unstable/sql/SqlResolver` +- `@effect/sql/SqlSchema` -> `effect/unstable/sql/SqlSchema` +- `@effect/sql/SqlStream` -> `effect/unstable/sql/SqlStream` +- `@effect/sql/Statement` -> `effect/unstable/sql/Statement` +- `@effect/sql/index` -> `effect/unstable/sql`: The package barrel was removed; import the same namespaces from the effect/unstable/sql barrel or import specific modules directly. +- `@effect/typeclass` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite abstractions against the concrete v4 data type and its module functions. +- `@effect/typeclass/Alternative` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Applicative` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Bicovariant` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Bounded`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/Chainable` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Contravariant` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Coproduct` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Covariant` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Filterable` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/FlatMap` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Foldable` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Invariant` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Monad` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Monoid` -> `effect/Reducer` +- `@effect/typeclass/Of` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Pointed` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Product` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/SemiAlternative` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/SemiApplicative` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/SemiCoproduct` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/SemiProduct` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/Semigroup` -> `effect/Combiner` +- `@effect/typeclass/Traversable` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/TraversableFilterable` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/data/Array`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/BigInt`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Boolean`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Duration`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Effect`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Either`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Identity` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. +- `@effect/typeclass/data/Micro`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Number`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Option`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Ordering`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Predicate`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Record`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/String`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/data/Tuple`: No single module replacement; follow the curated per-API guidance below. +- `@effect/typeclass/index` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite abstractions against the concrete v4 data type and its module functions. +- `@effect/vitest/index`: No single module replacement; follow the curated per-API guidance below. +- `@effect/workflow` -> `effect/unstable/workflow`: The @effect/workflow package was merged into the effect package; import the effect/unstable/workflow barrel or import specific modules directly (e.g. effect/unstable/workflow/\). +- `@effect/workflow/Activity` -> `effect/unstable/workflow/Activity` +- `@effect/workflow/DurableClock` -> `effect/unstable/workflow/DurableClock` +- `@effect/workflow/DurableDeferred` -> `effect/unstable/workflow/DurableDeferred` +- `@effect/workflow/DurableQueue` -> `effect/unstable/workflow/DurableQueue` +- `@effect/workflow/DurableRateLimiter` -> `none`: Not ported. Build an Activity whose execute uses persistence RateLimiter.consume with onExceeded: delay, then sleeps for the returned delay with DurableClock. +- `@effect/workflow/Workflow` -> `effect/unstable/workflow/Workflow` +- `@effect/workflow/WorkflowEngine` -> `effect/unstable/workflow/WorkflowEngine` +- `@effect/workflow/WorkflowProxy` -> `effect/unstable/workflow/WorkflowProxy` +- `@effect/workflow/WorkflowProxyServer` -> `effect/unstable/workflow/WorkflowProxyServer` +- `effect/Arbitrary`: No single module replacement; follow the curated per-API guidance below. +- `effect/ChildExecutorDecision` -> `none`: Removed with the v3 channel executor and Channel.concatMapWithCustom. Choose Channel.flatMap, Channel.switchMap, or Channel.mergeAll instead; v4 exposes no child-executor decision ADT. +- `effect/ConfigError`: No single module replacement; follow the curated per-API guidance below. +- `effect/ConfigProviderPathPatch`: No single module replacement; follow the curated per-API guidance below. +- `effect/DefaultServices`: No single module replacement; follow the curated per-API guidance below. +- `effect/Either` -> `effect/Result` +- `effect/ExecutionStrategy`: No single module replacement; follow the curated per-API guidance below. +- `effect/FastCheck` -> `effect/testing/FastCheck` +- `effect/FiberId`: No single module replacement; follow the curated per-API guidance below. +- `effect/FiberRef` -> `effect/References` +- `effect/FiberRefs`: No single module replacement; follow the curated per-API guidance below. +- `effect/FiberRefsPatch`: No single module replacement; follow the curated per-API guidance below. +- `effect/FiberStatus`: No single module replacement; follow the curated per-API guidance below. +- `effect/GlobalValue`: No single module replacement; follow the curated per-API guidance below. +- `effect/GroupBy`: No single module replacement; follow the curated per-API guidance below. +- `effect/JSONSchema` -> `effect/JsonSchema` +- `effect/KeyedPool`: No single module replacement; follow the curated per-API guidance below. +- `effect/List`: No single module replacement; follow the curated per-API guidance below. +- `effect/LogSpan`: No single module replacement; follow the curated per-API guidance below. +- `effect/Mailbox`: No single module replacement; follow the curated per-API guidance below. +- `effect/MergeDecision`: No single module replacement; follow the curated per-API guidance below. +- `effect/MergeState` -> `none`: Internal execution state of the removed Channel.mergeWith implementation. V4 Channel.merge manages its fibers and queues internally and exposes only a haltStrategy option. +- `effect/MergeStrategy`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricBoundaries`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricHook`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricKey`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricKeyType`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricLabel`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricPair`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricPolling`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricRegistry`: No single module replacement; follow the curated per-API guidance below. +- `effect/MetricState`: No single module replacement; follow the curated per-API guidance below. +- `effect/Micro`: No single module replacement; follow the curated per-API guidance below. +- `effect/ModuleVersion` -> `none`: The mutable module-version facility was removed; the v4 build version is private and effect/package.json is metadata, not an equivalent runtime API. No mutable version setter remains; the v3 runtime-isolation mechanism has no public v4 equivalent. +- `effect/MutableQueue`: No single module replacement; follow the curated per-API guidance below. +- `effect/ParseResult` -> `effect/SchemaIssue`, `effect/SchemaParser` +- `effect/Pretty`: No single module replacement; follow the curated per-API guidance below. +- `effect/RateLimiter` -> `none`: The old limit, interval, and algorithm options belonged to the removed in-process limiter; choose and configure an application limiter explicitly. The scoped in-process callable limiter was not ported to v4; effect/unstable/persistence/RateLimiter is a keyed persistence service with different semantics, not a drop-in replacement. The FiberRef-based per-effect cost annotation was removed with the core RateLimiter; pass token cost explicitly to the replacement limiter. +- `effect/Readable`: No single module replacement; follow the curated per-API guidance below. +- `effect/RedBlackTree`: No single module replacement; follow the curated per-API guidance below. +- `effect/Reloadable`: No single module replacement; follow the curated per-API guidance below. +- `effect/RequestBlock`: No single module replacement; follow the curated per-API guidance below. +- `effect/RuntimeFlags`: No single module replacement; follow the curated per-API guidance below. +- `effect/RuntimeFlagsPatch` -> `none`: The aggregate RuntimeFlagsPatch abstraction, its enabled/disabled bit sets, set operations, queries, and renderer were removed; no aggregate patch value remains to construct, combine, inspect, or render. Enable, disable, or invert the corresponding semantic behavior directly, combining semantic configurations where needed. Configure scheduler yielding, interruptibility, or metrics directly, and inspect the corresponding semantic facility when needed. +- `effect/STM`: No single module replacement; follow the curated per-API guidance below. +- `effect/ScheduleDecision`: No single module replacement; follow the curated per-API guidance below. +- `effect/ScheduleInterval` -> `none`: The public ScheduleInterval module was removed in v4. Schedule steps now express only a relative Duration; combine policies with Schedule.max or Schedule.min, or implement custom timing with Schedule.fromStep. +- `effect/ScheduleIntervals` -> `none`: The public ScheduleIntervals module was removed in v4 along with absolute interval-set decisions. Use relative Duration values in Schedule.fromStep and Schedule.max or Schedule.min for standard policy composition. +- `effect/Secret`: No single module replacement; follow the curated per-API guidance below. +- `effect/SingleProducerAsyncInput`: No single module replacement; follow the curated per-API guidance below. +- `effect/SortedMap`: No single module replacement; follow the curated per-API guidance below. +- `effect/SortedSet`: No single module replacement; follow the curated per-API guidance below. +- `effect/StreamEmit`: No single module replacement; follow the curated per-API guidance below. +- `effect/StreamHaltStrategy`: No single module replacement; follow the curated per-API guidance below. +- `effect/Streamable` -> `none`: Removed in v4 with no direct replacement. Instead of extending Streamable.Class, expose the underlying stream as a value (e.g. a property or method built with Stream.suspend). +- `effect/Subscribable`: No single module replacement; follow the curated per-API guidance below. +- `effect/Supervisor`: No single module replacement; follow the curated per-API guidance below. +- `effect/TArray`: No single module replacement; follow the curated per-API guidance below. +- `effect/TDeferred` -> `effect/TxDeferred` +- `effect/TMap` -> `effect/TxHashMap` +- `effect/TPriorityQueue` -> `effect/TxPriorityQueue` +- `effect/TPubSub` -> `effect/TxPubSub` +- `effect/TQueue` -> `effect/TxQueue` +- `effect/TRandom`: No single module replacement; follow the curated per-API guidance below. +- `effect/TReentrantLock` -> `effect/TxReentrantLock` +- `effect/TRef` -> `effect/TxRef` +- `effect/TSemaphore` -> `effect/TxSemaphore` +- `effect/TSet` -> `effect/TxHashSet` +- `effect/TSubscriptionRef` -> `effect/TxSubscriptionRef` +- `effect/TestAnnotation` -> `none`: The legacy test-runner annotation key and built-in counters were removed. Use Vitest skip/repeat/retry options for runner concerns and FiberSet for explicit fiber tracking; there is no annotation-key equivalent. +- `effect/TestAnnotationMap` -> `none`: TestAnnotationMap was removed with TestAnnotation. Use an application-owned HashMap or Ref only when arbitrary typed annotations are still required; it is not part of the v4 test runner. +- `effect/TestAnnotations` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. +- `effect/TestClock` -> `effect/testing/TestClock` +- `effect/TestConfig`: No single module replacement; follow the curated per-API guidance below. +- `effect/TestContext`: No single module replacement; follow the curated per-API guidance below. +- `effect/TestLive`: No single module replacement; follow the curated per-API guidance below. +- `effect/TestServices`: No single module replacement; follow the curated per-API guidance below. +- `effect/TestSized`: No single module replacement; follow the curated per-API guidance below. +- `effect/UpstreamPullRequest` -> `none`: Removed with Channel.concatMapWithCustom; v4 does not expose channel-executor pull-request events. Use supported flattening operators or implement exceptional behavior with Channel.fromTransform and Pull. +- `effect/UpstreamPullStrategy` -> `none`: Removed with Channel.concatMapWithCustom. Select flattening and scheduling through Channel.flatMap, Channel.switchMap, or Channel.mergeAll; v4 has no upstream-pull strategy ADT. +- `effect/index`: No single module replacement; follow the curated per-API guidance below. -```text -Effect.async -> Effect.callback -Effect.zipRight -> Effect.andThen -Effect.zipLeft -> Effect.tap -Effect.either -> Effect.result -Effect.catchAll -> Effect.catch -Effect.catchAllCause -> Effect.catchCause -Effect.catchAllDefect -> Effect.catchDefect -Effect.catchSome -> Effect.catchIf -Effect.catchIf -> Effect.catchIf -Effect.optionFromOptional -> Effect.catchNoSuchElement -Effect.catchSomeCause -> Effect.catchCauseIf -Effect.tapErrorCause -> Effect.tapCause -Effect.ignoreLogged -> Effect.ignore -Effect.makeLatchUnsafe -> Latch.makeUnsafe -Effect.makeLatch -> Latch.make -Layer.scoped -> Layer.effect -Layer.scopedDiscard -> Layer.effectDiscard -Layer.tapErrorCause -> Layer.tapCause -Mailbox -> Queue.Queue -Mailbox.make -> Queue.make -Either -> Result.Result -Either.right -> Result.succeed -Either.left -> Result.fail -Scope.extend -> Scope.provide -Effect.makeSemaphoreUnsafe -> Semaphore.makeUnsafe -Effect.makeSemaphore -> Semaphore.make -Stream.Context -> Stream.Services -StreamHaltStrategy.HaltStrategy -> Stream.HaltStrategy -Stream.repeatEffect -> Stream.fromEffectRepeat -Stream.repeatEffectWithSchedule -> Stream.fromEffectSchedule -Stream.async -> Stream.callback -Stream.asyncEffect -> Stream.callback -Stream.asyncPush -> Stream.callback -Stream.asyncScoped -> Stream.callback -Stream.repeatEffectChunk -> Stream.fromIterableEffectRepeat -Stream.fromChunk -> Stream.fromArray -Stream.fromChunks -> Stream.fromArrays -Stream.mapChunks -> Stream.mapArray -Stream.mapChunksEffect -> Stream.mapArrayEffect -Stream.either -> Stream.result -Stream.flattenChunks -> Stream.flattenArray -Stream.flattenIterables -> Stream.flattenIterable -Stream.mergeEither -> Stream.mergeResult -Stream.zipWithChunks -> Stream.zipWithArray -Stream.bufferChunks -> Stream.bufferArray -Stream.catchAllCause -> Stream.catchCause -Stream.tapErrorCause -> Stream.tapCause -Stream.catchAll -> Stream.catch -Stream.catchSome -> Stream.catchIf -Stream.catchSomeCause -> Stream.catchCauseIf -Stream.combineChunks -> Stream.combineArray -provideSomeLayer -> Stream.provide -provideSomeContext -> Stream.provide -``` +## API Reference + +### `@effect/ai-anthropic/AnthropicClient` + +- `AnthropicClient.CitationsDelta` -> `Generated.BetaCitationsDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.ContentBlockDeltaEvent` -> `Generated.BetaContentBlockDeltaEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.ContentBlockStartEvent` -> `Generated.BetaContentBlockStartEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.ContentBlockStopEvent` -> `Generated.BetaContentBlockStopEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.ErrorEvent` -> `Generated.BetaErrorResponse`: The client-local stream error schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.InputJsonContentBlockDelta` -> `Generated.BetaInputJsonContentBlockDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.MessageDelta` -> `Generated.BetaMessageDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.MessageDeltaEvent` -> `Generated.BetaMessageDeltaEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.MessageDeltaUsage` -> `typeof Generated.BetaMessageDeltaEvent.Type["usage"]`: The standalone usage schema was inlined into the regenerated v4 message-delta event. + +- `AnthropicClient.MessageStartEvent` -> `Generated.BetaMessageStartEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.MessageStopEvent` -> `Generated.BetaMessageStopEvent`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.MessageStreamEvent` -> `AnthropicClient.MessageStreamEvent`: Still exported in v4 as a type union of generated beta stream events; adapt to the revised client stream contract. + +- `AnthropicClient.PingEvent` -> `none`: The v4 client consumes ping events internally and filters them from MessageStreamEvent, so no public ping schema is needed. + +- `AnthropicClient.ServerToolUsage` -> `Generated.BetaServerToolUsage`: The client-local usage schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.Service` -> `AnthropicClient.Service`: Still exported in v4; adapt to the revised generated client, streamRequest, and message response contracts. + +- `AnthropicClient.SignatureContentBlockDelta` -> `Generated.BetaSignatureContentBlockDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.TextContentBlockDelta` -> `Generated.BetaTextContentBlockDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +- `AnthropicClient.ThinkingContentBlockDelta` -> `Generated.BetaThinkingContentBlockDelta`: The client-local stream schema moved to the regenerated v4 Anthropic schema surface; re-check its Type/Encoded shape. + +### `@effect/ai-anthropic/AnthropicConfig` + +- `AnthropicConfig.AnthropicConfig` -> `AnthropicConfig.AnthropicConfig`: Still exported in v4; update imports and adapt to the revised v4 service and HTTP client types. + +- `AnthropicConfig.AnthropicConfig.Service` -> `AnthropicConfig.AnthropicConfig.Service`: Still exported in v4; update imports and adapt to the revised v4 service and HTTP client types. + +### `@effect/ai-anthropic/AnthropicLanguageModel` + +- `AnthropicLanguageModel.AnthropicReasoningInfo` -> `Prompt.ReasoningPartOptions / Response reasoning metadata`: The standalone reasoning-info union was removed; v4 declares Anthropic thinking and redacted-thinking data directly on Prompt and Response provider metadata. + +- `AnthropicLanguageModel.AnthropicTools` -> `AnthropicLanguageModel.AnthropicUserDefinedTool | AnthropicLanguageModel.AnthropicProviderDefinedTool`: The old combined tool union was split into explicit user-defined and provider-defined Anthropic request tool types. + +- `AnthropicLanguageModel.Config` -> `AnthropicLanguageModel.Config`: Still exported in v4; update imports and adapt to the revised Messages API request fields. + +- `AnthropicLanguageModel.Config.Service` -> `AnthropicLanguageModel.Config.Service`: Still exported in v4; update imports and adapt to the revised Messages API request fields. + +- `AnthropicLanguageModel.layerWithTokenizer` -> `AnthropicLanguageModel.layer`: The tokenizer-combining layer was removed; provide the language model and any Tokenizer service separately. + +- `AnthropicLanguageModel.modelWithTokenizer` -> `AnthropicLanguageModel.model`: The tokenizer-combining model was removed; use the v4 model descriptor and provide any Tokenizer service separately. + +- `AnthropicLanguageModel.prepareTools` -> `none`: Tool conversion became an internal part of the v4 Anthropic language model; use AnthropicTool constructors and pass tools through LanguageModel provider options instead. + +### `@effect/ai-anthropic/AnthropicTokenizer` + +- `AnthropicTokenizer.layer` -> `Tokenizer.make`: The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using @anthropic-ai/tokenizer if equivalent Anthropic counting is required. + +- `AnthropicTokenizer.make` -> `Tokenizer.make`: The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using @anthropic-ai/tokenizer if equivalent Anthropic counting is required. + +### `@effect/ai-anthropic/AnthropicTool` + +- `AnthropicTool.ProviderDefinedTools` -> `AnthropicTool.AnthropicTool`: The provider-defined schema union was replaced by the union of v4 Anthropic provider tool constructor return types. + +- `AnthropicTool.getProviderDefinedToolName` -> `Tool.NameMapper`: The Anthropic-specific name lookup was removed; v4 provider tools carry custom and provider names through the shared Tool.NameMapper. + +### `@effect/ai-anthropic/Generated` + +- `Generated.APIError` -> `Generated.APIError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuthenticationError` -> `Generated.AuthenticationError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Base64ImageSource` -> `Generated.Base64ImageSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Base64ImageSourceMediaType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Base64PDFSource` -> `Generated.Base64PDFSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BashTool20250124` -> `Generated.BashTool_20250124`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaAPIError` -> `Generated.BetaAPIError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaAuthenticationError` -> `Generated.BetaAuthenticationError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBase64ImageSource` -> `Generated.BetaBase64ImageSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBase64ImageSourceMediaType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaBase64PDFSource` -> `Generated.BetaBase64PDFSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBashCodeExecutionToolResultErrorCode` -> `Generated.BetaBashCodeExecutionToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBashTool20241022` -> `Generated.BetaBashTool_20241022`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBashTool20250124` -> `Generated.BetaBashTool_20250124`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBillingError` -> `Generated.BetaBillingError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBodyCreateSkillV1SkillsPost` -> `Generated.BetaBody_create_skill_v1_skills_post`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaBodyCreateSkillVersionV1SkillsSkillIdVersionsPost` -> `Generated.BetaBody_create_skill_version_v1_skills__skill_id__versions_post`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCacheControlEphemeral` -> `Generated.BetaCacheControlEphemeral`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCacheControlEphemeralTtl` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaCacheCreation` -> `Generated.BetaCacheCreation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaClearToolUses20250919` -> `Generated.BetaClearToolUses20250919`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCodeExecutionTool20250522` -> `Generated.BetaCodeExecutionTool_20250522`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCodeExecutionTool20250825` -> `Generated.BetaCodeExecutionTool_20250825`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCodeExecutionToolResultErrorCode` -> `Generated.BetaCodeExecutionToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaComputerUseTool20241022` -> `Generated.BetaComputerUseTool_20241022`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaComputerUseTool20250124` -> `Generated.BetaComputerUseTool_20250124`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContainer` -> `Generated.BetaContainer`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContainerParams` -> `Generated.BetaContainerParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContentBlock` -> `Generated.BetaContentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContentBlockSource` -> `Generated.BetaContentBlockSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContextManagementConfig` -> `Generated.BetaContextManagementConfig`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaContextManagementResponse` -> `Generated.BetaContextManagementResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCountMessageTokensParams` -> `Generated.BetaCountMessageTokensParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCountMessageTokensResponse` -> `Generated.BetaCountMessageTokensResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateMessageBatchParams` -> `Generated.BetaCreateMessageBatchParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateMessageParams` -> `Generated.BetaCreateMessageParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateMessageParamsServiceTier` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaCreateSkillResponse` -> `Generated.BetaCreateSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateSkillV1SkillsPostParams` -> `Generated.BetaCreateSkillV1SkillsPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateSkillVersionResponse` -> `Generated.BetaCreateSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams` -> `Generated.BetaCreateSkillVersionV1SkillsSkillIdVersionsPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteFileV1FilesFileIdDeleteParams` -> `Generated.BetaDeleteFileV1FilesFileIdDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteMessageBatchResponse` -> `Generated.BetaDeleteMessageBatchResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteSkillResponse` -> `Generated.BetaDeleteSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteSkillV1SkillsSkillIdDeleteParams` -> `Generated.BetaDeleteSkillV1SkillsSkillIdDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteSkillVersionResponse` -> `Generated.BetaDeleteSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams` -> `Generated.BetaDeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaDownloadFileV1FilesFileIdContentGetParams` -> `Generated.BetaDownloadFileV1FilesFileIdContentGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaErrorResponse` -> `Generated.BetaErrorResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaFileDeleteResponse` -> `Generated.BetaFileDeleteResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaFileDocumentSource` -> `Generated.BetaFileDocumentSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaFileImageSource` -> `Generated.BetaFileImageSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaFileListResponse` -> `Generated.BetaFileListResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaFileMetadataSchema` -> `Generated.BetaFileMetadataSchema`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGatewayTimeoutError` -> `Generated.BetaGatewayTimeoutError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGetFileMetadataV1FilesFileIdGetParams` -> `Generated.BetaGetFileMetadataV1FilesFileIdGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGetSkillResponse` -> `Generated.BetaGetSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGetSkillV1SkillsSkillIdGetParams` -> `Generated.BetaGetSkillV1SkillsSkillIdGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGetSkillVersionResponse` -> `Generated.BetaGetSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams` -> `Generated.BetaGetSkillVersionV1SkillsSkillIdVersionsVersionGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaInputContentBlock` -> `Generated.BetaInputContentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaInputMessage` -> `Generated.BetaInputMessage`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaInputMessageRole` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaInputSchema` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaInputTokensClearAtLeast` -> `Generated.BetaInputTokensClearAtLeast`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaInputTokensTrigger` -> `Generated.BetaInputTokensTrigger`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaInvalidRequestError` -> `Generated.BetaInvalidRequestError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListFilesV1FilesGetParams` -> `Generated.BetaListFilesV1FilesGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListResponseMessageBatch` -> `Generated.BetaListResponse_MessageBatch_`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListResponseModelInfo` -> `Generated.BetaListResponse_ModelInfo_`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListSkillVersionsResponse` -> `Generated.BetaListSkillVersionsResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams` -> `Generated.BetaListSkillVersionsV1SkillsSkillIdVersionsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListSkillsResponse` -> `Generated.BetaListSkillsResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaListSkillsV1SkillsGetParams` -> `Generated.BetaListSkillsV1SkillsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMemoryTool20250818` -> `Generated.BetaMemoryTool_20250818`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessage` -> `Generated.BetaMessage`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatch` -> `Generated.BetaMessageBatch`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchIndividualRequestParams` -> `Generated.BetaMessageBatchIndividualRequestParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchProcessingStatus` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaMessageBatchesCancelParams` -> `Generated.BetaMessageBatchesCancelParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchesDeleteParams` -> `Generated.BetaMessageBatchesDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchesListParams` -> `Generated.BetaMessageBatchesListParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchesPostParams` -> `Generated.BetaMessageBatchesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchesResultsParams` -> `Generated.BetaMessageBatchesResultsParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessageBatchesRetrieveParams` -> `Generated.BetaMessageBatchesRetrieveParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessagesCountTokensPostParams` -> `Generated.BetaMessagesCountTokensPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMessagesPostParams` -> `Generated.BetaMessagesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaMetadata` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaModelInfo` -> `Generated.BetaModelInfo`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaModelsGetParams` -> `Generated.BetaModelsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaModelsListParams` -> `Generated.BetaModelsListParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaNotFoundError` -> `Generated.BetaNotFoundError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaOverloadedError` -> `Generated.BetaOverloadedError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaPermissionError` -> `Generated.BetaPermissionError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaPlainTextSource` -> `Generated.BetaPlainTextSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRateLimitError` -> `Generated.BetaRateLimitError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestBashCodeExecutionOutputBlock` -> `Generated.BetaRequestBashCodeExecutionOutputBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestBashCodeExecutionResultBlock` -> `Generated.BetaRequestBashCodeExecutionResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestBashCodeExecutionToolResultBlock` -> `Generated.BetaRequestBashCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestBashCodeExecutionToolResultError` -> `Generated.BetaRequestBashCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCharLocationCitation` -> `Generated.BetaRequestCharLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCitationsConfig` -> `Generated.BetaRequestCitationsConfig`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCodeExecutionOutputBlock` -> `Generated.BetaRequestCodeExecutionOutputBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCodeExecutionResultBlock` -> `Generated.BetaRequestCodeExecutionResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCodeExecutionToolResultBlock` -> `Generated.BetaRequestCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCodeExecutionToolResultError` -> `Generated.BetaRequestCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestContainerUploadBlock` -> `Generated.BetaRequestContainerUploadBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestContentBlockLocationCitation` -> `Generated.BetaRequestContentBlockLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestCounts` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestDocumentBlock` -> `Generated.BetaRequestDocumentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestImageBlock` -> `Generated.BetaRequestImageBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestMCPServerToolConfiguration` -> `Generated.BetaRequestMCPServerToolConfiguration`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestMCPServerURLDefinition` -> `Generated.BetaRequestMCPServerURLDefinition`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestMCPToolResultBlock` -> `Generated.BetaRequestMCPToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestMCPToolUseBlock` -> `Generated.BetaRequestMCPToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestPageLocationCitation` -> `Generated.BetaRequestPageLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestRedactedThinkingBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestSearchResultBlock` -> `Generated.BetaRequestSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestSearchResultLocationCitation` -> `Generated.BetaRequestSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestServerToolUseBlock` -> `Generated.BetaRequestServerToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestServerToolUseBlockName` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestTextBlock` -> `Generated.BetaRequestTextBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionCreateResultBlock` -> `Generated.BetaRequestTextEditorCodeExecutionCreateResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionStrReplaceResultBlock` -> `Generated.BetaRequestTextEditorCodeExecutionStrReplaceResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionToolResultBlock` -> `Generated.BetaRequestTextEditorCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionToolResultError` -> `Generated.BetaRequestTextEditorCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionViewResultBlock` -> `Generated.BetaRequestTextEditorCodeExecutionViewResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestTextEditorCodeExecutionViewResultBlockFileType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestThinkingBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestToolResultBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestToolUseBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaRequestWebFetchResultBlock` -> `Generated.BetaRequestWebFetchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebFetchToolResultBlock` -> `Generated.BetaRequestWebFetchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebFetchToolResultError` -> `Generated.BetaRequestWebFetchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebSearchResultBlock` -> `Generated.BetaRequestWebSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebSearchResultLocationCitation` -> `Generated.BetaRequestWebSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebSearchToolResultBlock` -> `Generated.BetaRequestWebSearchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaRequestWebSearchToolResultError` -> `Generated.BetaRequestWebSearchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseBashCodeExecutionOutputBlock` -> `Generated.BetaResponseBashCodeExecutionOutputBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseBashCodeExecutionResultBlock` -> `Generated.BetaResponseBashCodeExecutionResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseBashCodeExecutionToolResultBlock` -> `Generated.BetaResponseBashCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseBashCodeExecutionToolResultError` -> `Generated.BetaResponseBashCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCharLocationCitation` -> `Generated.BetaResponseCharLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCitationsConfig` -> `Generated.BetaResponseCitationsConfig`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseClearToolUses20250919Edit` -> `Generated.BetaResponseClearToolUses20250919Edit`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCodeExecutionOutputBlock` -> `Generated.BetaResponseCodeExecutionOutputBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCodeExecutionResultBlock` -> `Generated.BetaResponseCodeExecutionResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCodeExecutionToolResultBlock` -> `Generated.BetaResponseCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseCodeExecutionToolResultError` -> `Generated.BetaResponseCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseContainerUploadBlock` -> `Generated.BetaResponseContainerUploadBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseContentBlockLocationCitation` -> `Generated.BetaResponseContentBlockLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseContextManagement` -> `Generated.BetaResponseContextManagement`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseDocumentBlock` -> `Generated.BetaResponseDocumentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseMCPToolResultBlock` -> `Generated.BetaResponseMCPToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseMCPToolUseBlock` -> `Generated.BetaResponseMCPToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponsePageLocationCitation` -> `Generated.BetaResponsePageLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseRedactedThinkingBlock` -> `Generated.BetaResponseRedactedThinkingBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseSearchResultLocationCitation` -> `Generated.BetaResponseSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseServerToolUseBlock` -> `Generated.BetaResponseServerToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseServerToolUseBlockName` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaResponseTextBlock` -> `Generated.BetaResponseTextBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionCreateResultBlock` -> `Generated.BetaResponseTextEditorCodeExecutionCreateResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionStrReplaceResultBlock` -> `Generated.BetaResponseTextEditorCodeExecutionStrReplaceResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionToolResultBlock` -> `Generated.BetaResponseTextEditorCodeExecutionToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionToolResultError` -> `Generated.BetaResponseTextEditorCodeExecutionToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionViewResultBlock` -> `Generated.BetaResponseTextEditorCodeExecutionViewResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseTextEditorCodeExecutionViewResultBlockFileType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaResponseThinkingBlock` -> `Generated.BetaResponseThinkingBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseToolUseBlock` -> `Generated.BetaResponseToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebFetchResultBlock` -> `Generated.BetaResponseWebFetchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebFetchToolResultBlock` -> `Generated.BetaResponseWebFetchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebFetchToolResultError` -> `Generated.BetaResponseWebFetchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebSearchResultBlock` -> `Generated.BetaResponseWebSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebSearchResultLocationCitation` -> `Generated.BetaResponseWebSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebSearchToolResultBlock` -> `Generated.BetaResponseWebSearchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaResponseWebSearchToolResultError` -> `Generated.BetaResponseWebSearchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaServerToolUsage` -> `Generated.BetaServerToolUsage`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaSkill` -> `Generated.BetaSkill`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaSkillParams` -> `Generated.BetaSkillParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaSkillParamsType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaSkillType` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaSkillVersion` -> `Generated.BetaSkillVersion`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaStopReason` -> `Generated.BetaStopReason`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTextEditor20241022` -> `Generated.BetaTextEditor_20241022`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTextEditor20250124` -> `Generated.BetaTextEditor_20250124`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTextEditor20250429` -> `Generated.BetaTextEditor_20250429`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTextEditor20250728` -> `Generated.BetaTextEditor_20250728`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTextEditorCodeExecutionToolResultErrorCode` -> `Generated.BetaTextEditorCodeExecutionToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaThinkingConfigDisabled` -> `Generated.BetaThinkingConfigDisabled`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaThinkingConfigEnabled` -> `Generated.BetaThinkingConfigEnabled`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaThinkingConfigParam` -> `Generated.BetaThinkingConfigParam`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaTool` -> `Generated.BetaTool`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolChoice` -> `Generated.BetaToolChoice`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolChoiceAny` -> `Generated.BetaToolChoiceAny`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolChoiceAuto` -> `Generated.BetaToolChoiceAuto`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolChoiceNone` -> `Generated.BetaToolChoiceNone`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolChoiceTool` -> `Generated.BetaToolChoiceTool`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolUsesKeep` -> `Generated.BetaToolUsesKeep`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaToolUsesTrigger` -> `Generated.BetaToolUsesTrigger`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaURLImageSource` -> `Generated.BetaURLImageSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaURLPDFSource` -> `Generated.BetaURLPDFSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaUploadFileV1FilesPostParams` -> `Generated.BetaUploadFileV1FilesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaUploadFileV1FilesPostRequest` -> `Generated.BetaUploadFileV1FilesPostRequestFormData`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaUsage` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaUsageServiceTierEnum` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BetaUserLocation` -> `Generated.BetaUserLocation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaWebFetchTool20250910` -> `Generated.BetaWebFetchTool_20250910`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaWebFetchToolResultErrorCode` -> `Generated.BetaWebFetchToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaWebSearchTool20250305` -> `Generated.BetaWebSearchTool_20250305`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaWebSearchToolResultErrorCode` -> `Generated.BetaWebSearchToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BetaapiSchemasSkillsSkill` -> `Generated.Betaapi__schemas__skills__Skill`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BillingError` -> `Generated.BillingError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BodyCreateSkillV1SkillsPost` -> `Generated.Body_create_skill_v1_skills_post`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BodyCreateSkillVersionV1SkillsSkillIdVersionsPost` -> `Generated.Body_create_skill_version_v1_skills__skill_id__versions_post`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CacheControlEphemeral` -> `Generated.CacheControlEphemeral`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CacheControlEphemeralTtl` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CacheCreation` -> `Generated.CacheCreation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Client` -> `Generated.AnthropicClient`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ClientError` -> `Generated.AnthropicClientError`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompletePostParams` -> `Generated.CompletePostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompletionRequest` -> `Generated.CompletionRequest`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompletionResponse` -> `Generated.CompletionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContentBlock` -> `Generated.ContentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContentBlockSource` -> `Generated.ContentBlockSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CountMessageTokensParams` -> `Generated.CountMessageTokensParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CountMessageTokensResponse` -> `Generated.CountMessageTokensResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessageBatchParams` -> `Generated.CreateMessageBatchParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessageParams` -> `Generated.CreateMessageParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessageParamsServiceTier` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateSkillResponse` -> `Generated.CreateSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateSkillV1SkillsPostParams` -> `Generated.CreateSkillV1SkillsPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateSkillVersionResponse` -> `Generated.CreateSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateSkillVersionV1SkillsSkillIdVersionsPostParams` -> `Generated.CreateSkillVersionV1SkillsSkillIdVersionsPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteFileV1FilesFileIdDeleteParams` -> `Generated.DeleteFileV1FilesFileIdDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteMessageBatchResponse` -> `Generated.DeleteMessageBatchResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteSkillResponse` -> `Generated.DeleteSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteSkillV1SkillsSkillIdDeleteParams` -> `Generated.DeleteSkillV1SkillsSkillIdDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteSkillVersionResponse` -> `Generated.DeleteSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams` -> `Generated.DeleteSkillVersionV1SkillsSkillIdVersionsVersionDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DownloadFileV1FilesFileIdContentGetParams` -> `Generated.DownloadFileV1FilesFileIdContentGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ErrorResponse` -> `Generated.ErrorResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileDeleteResponse` -> `Generated.FileDeleteResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileListResponse` -> `Generated.FileListResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileMetadataSchema` -> `Generated.FileMetadataSchema`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GatewayTimeoutError` -> `Generated.GatewayTimeoutError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetFileMetadataV1FilesFileIdGetParams` -> `Generated.GetFileMetadataV1FilesFileIdGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetSkillResponse` -> `Generated.GetSkillResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetSkillV1SkillsSkillIdGetParams` -> `Generated.GetSkillV1SkillsSkillIdGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetSkillVersionResponse` -> `Generated.GetSkillVersionResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams` -> `Generated.GetSkillVersionV1SkillsSkillIdVersionsVersionGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputContentBlock` -> `Generated.InputContentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputMessage` -> `Generated.InputMessage`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputMessageRole` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputSchema` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InvalidRequestError` -> `Generated.InvalidRequestError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFilesV1FilesGetParams` -> `Generated.ListFilesV1FilesGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListResponseMessageBatch` -> `Generated.ListResponse_MessageBatch_`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListResponseModelInfo` -> `Generated.ListResponse_ModelInfo_`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListSkillVersionsResponse` -> `Generated.ListSkillVersionsResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListSkillVersionsV1SkillsSkillIdVersionsGetParams` -> `Generated.ListSkillVersionsV1SkillsSkillIdVersionsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListSkillsResponse` -> `Generated.ListSkillsResponse`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListSkillsV1SkillsGetParams` -> `Generated.ListSkillsV1SkillsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Message` -> `Generated.Message`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatch` -> `Generated.MessageBatch`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchIndividualRequestParams` -> `Generated.MessageBatchIndividualRequestParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchProcessingStatus` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageBatchesCancelParams` -> `Generated.MessageBatchesCancelParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchesDeleteParams` -> `Generated.MessageBatchesDeleteParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchesListParams` -> `Generated.MessageBatchesListParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchesPostParams` -> `Generated.MessageBatchesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchesResultsParams` -> `Generated.MessageBatchesResultsParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageBatchesRetrieveParams` -> `Generated.MessageBatchesRetrieveParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessagesCountTokensPostParams` -> `Generated.MessagesCountTokensPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessagesPostParams` -> `Generated.MessagesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Metadata` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Model` -> `Generated.Model`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelInfo` -> `Generated.ModelInfo`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelsGetParams` -> `Generated.ModelsGetParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelsListParams` -> `Generated.ModelsListParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.NotFoundError` -> `Generated.NotFoundError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OverloadedError` -> `Generated.OverloadedError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PermissionError` -> `Generated.PermissionError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PlainTextSource` -> `Generated.PlainTextSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RateLimitError` -> `Generated.RateLimitError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestCharLocationCitation` -> `Generated.RequestCharLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestCitationsConfig` -> `Generated.RequestCitationsConfig`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestContentBlockLocationCitation` -> `Generated.RequestContentBlockLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestCounts` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestDocumentBlock` -> `Generated.RequestDocumentBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestImageBlock` -> `Generated.RequestImageBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestPageLocationCitation` -> `Generated.RequestPageLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestRedactedThinkingBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestSearchResultBlock` -> `Generated.RequestSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestSearchResultLocationCitation` -> `Generated.RequestSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestServerToolUseBlock` -> `Generated.RequestServerToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestTextBlock` -> `Generated.RequestTextBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestThinkingBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestToolResultBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestToolUseBlock` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestWebSearchResultBlock` -> `Generated.RequestWebSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestWebSearchResultLocationCitation` -> `Generated.RequestWebSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestWebSearchToolResultBlock` -> `Generated.RequestWebSearchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestWebSearchToolResultError` -> `Generated.RequestWebSearchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseCharLocationCitation` -> `Generated.ResponseCharLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseContentBlockLocationCitation` -> `Generated.ResponseContentBlockLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponsePageLocationCitation` -> `Generated.ResponsePageLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseRedactedThinkingBlock` -> `Generated.ResponseRedactedThinkingBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseSearchResultLocationCitation` -> `Generated.ResponseSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseServerToolUseBlock` -> `Generated.ResponseServerToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseTextBlock` -> `Generated.ResponseTextBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseThinkingBlock` -> `Generated.ResponseThinkingBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseToolUseBlock` -> `Generated.ResponseToolUseBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseWebSearchResultBlock` -> `Generated.ResponseWebSearchResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseWebSearchResultLocationCitation` -> `Generated.ResponseWebSearchResultLocationCitation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseWebSearchToolResultBlock` -> `Generated.ResponseWebSearchToolResultBlock`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseWebSearchToolResultError` -> `Generated.ResponseWebSearchToolResultError`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ServerToolUsage` -> `Generated.ServerToolUsage`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Skill` -> `Generated.Skill`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SkillVersion` -> `Generated.SkillVersion`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.StopReason` -> `Generated.StopReason`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextEditor20250124` -> `Generated.TextEditor_20250124`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextEditor20250429` -> `Generated.TextEditor_20250429`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextEditor20250728` -> `Generated.TextEditor_20250728`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThinkingConfigDisabled` -> `Generated.ThinkingConfigDisabled`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThinkingConfigEnabled` -> `Generated.ThinkingConfigEnabled`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThinkingConfigParam` -> `Generated.ThinkingConfigParam`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Tool` -> `Generated.Tool`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoice` -> `Generated.ToolChoice`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceAny` -> `Generated.ToolChoiceAny`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceAuto` -> `Generated.ToolChoiceAuto`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceNone` -> `Generated.ToolChoiceNone`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceTool` -> `Generated.ToolChoiceTool`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.URLImageSource` -> `Generated.URLImageSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.URLPDFSource` -> `Generated.URLPDFSource`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UploadFileV1FilesPostParams` -> `Generated.UploadFileV1FilesPostParams`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UploadFileV1FilesPostRequest` -> `Generated.UploadFileV1FilesPostRequestFormData`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageServiceTierEnum` -> `none`: Removed or inlined when the Anthropic specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserLocation` -> `Generated.UserLocation`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchTool20250305` -> `Generated.WebSearchTool_20250305`: Regenerated in v4 under this name; update the identifier and re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchToolResultErrorCode` -> `Generated.WebSearchToolResultErrorCode`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.make` -> `Generated.make`: Still generated in v4 from the current Anthropic specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +### `@effect/ai-openai/Generated` + +- `Generated.ActiveStatus` -> `Generated.ActiveStatus`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ActiveStatusType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AddUploadPartRequest` -> `Generated.AddUploadPartRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AdminApiKey` -> `Generated.AdminApiKey`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AdminApiKeysCreateRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AdminApiKeysDelete200` -> `Generated.AdminApiKeysDelete200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AdminApiKeysListParams` -> `Generated.AdminApiKeysListParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AdminApiKeysListParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Annotation` -> `Generated.Annotation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApiKeyList` -> `Generated.ApiKeyList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchCallOutputStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchCallOutputStatusParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchCallStatusParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchCreateFileOperation` -> `Generated.ApplyPatchCreateFileOperation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchCreateFileOperationParam` -> `Generated.ApplyPatchCreateFileOperationParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchCreateFileOperationParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchCreateFileOperationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchDeleteFileOperation` -> `Generated.ApplyPatchDeleteFileOperation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchDeleteFileOperationParam` -> `Generated.ApplyPatchDeleteFileOperationParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchDeleteFileOperationParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchDeleteFileOperationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchOperationParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCall` -> `Generated.ApplyPatchToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchToolCallItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCallItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCallOutput` -> `Generated.ApplyPatchToolCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchToolCallOutputItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCallOutputItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCallOutputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchToolParam` -> `Generated.ApplyPatchToolParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchToolParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchUpdateFileOperation` -> `Generated.ApplyPatchUpdateFileOperation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchUpdateFileOperationParam` -> `Generated.ApplyPatchUpdateFileOperationParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ApplyPatchUpdateFileOperationParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApplyPatchUpdateFileOperationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApproximateLocation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ApproximateLocationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssignedRoleDetails` -> `Generated.AssignedRoleDetails`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantMessageItem` -> `Generated.AssistantMessageItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantMessageItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantMessageItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantObject` -> `Generated.AssistantObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantSupportedModels` -> `Generated.AssistantSupportedModels`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantTool` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantToolsCode` -> `Generated.AssistantToolsCode`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantToolsCodeType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantToolsFileSearch` -> `Generated.AssistantToolsFileSearch`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantToolsFileSearchType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantToolsFileSearchTypeOnly` -> `Generated.AssistantToolsFileSearchTypeOnly`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantToolsFileSearchTypeOnlyType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantToolsFunction` -> `Generated.AssistantToolsFunction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantToolsFunctionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantsApiResponseFormatOption` -> `Generated.AssistantsApiResponseFormatOption`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantsApiResponseFormatOptionEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantsNamedToolChoice` -> `Generated.AssistantsNamedToolChoice`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AssistantsNamedToolChoiceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Attachment` -> `Generated.Attachment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AttachmentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AudioResponseFormat` -> `Generated.AudioResponseFormat`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AudioTranscription` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AudioTranscriptionModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AuditLog` -> `Generated.AuditLog`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogActor` -> `Generated.AuditLogActor`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogActorApiKey` -> `Generated.AuditLogActorApiKey`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogActorApiKeyType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AuditLogActorServiceAccount` -> `Generated.AuditLogActorServiceAccount`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogActorSession` -> `Generated.AuditLogActorSession`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogActorType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AuditLogActorUser` -> `Generated.AuditLogActorUser`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AuditLogEventType` -> `Generated.AuditLogEventType`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AutoChunkingStrategyRequestParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AutoChunkingStrategyRequestParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AutomaticThreadTitlingParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Batch` -> `Generated.Batch`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BatchError` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BatchFileExpirationAfter` -> `Generated.BatchFileExpirationAfter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BatchFileExpirationAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BatchObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BatchRequestCounts` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BatchStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Certificate` -> `Generated.Certificate`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CertificateObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionAllowedTools` -> `Generated.ChatCompletionAllowedTools`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionAllowedToolsChoice` -> `Generated.ChatCompletionAllowedToolsChoice`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionAllowedToolsChoiceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionAllowedToolsMode` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionDeleted` -> `Generated.ChatCompletionDeleted`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionDeletedObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionFunctionCallOption` -> `Generated.ChatCompletionFunctionCallOption`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionFunctions` -> `Generated.ChatCompletionFunctions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionList` -> `Generated.ChatCompletionList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionMessageCustomToolCall` -> `Generated.ChatCompletionMessageCustomToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionMessageCustomToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionMessageList` -> `Generated.ChatCompletionMessageList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionMessageListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionMessageToolCall` -> `Generated.ChatCompletionMessageToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionMessageToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionMessageToolCalls` -> `Generated.ChatCompletionMessageToolCalls`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionNamedToolChoice` -> `Generated.ChatCompletionNamedToolChoice`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionNamedToolChoiceCustom` -> `Generated.ChatCompletionNamedToolChoiceCustom`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionNamedToolChoiceCustomType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionNamedToolChoiceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestAssistantMessage` -> `Generated.ChatCompletionRequestAssistantMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestAssistantMessageContentPart` -> `Generated.ChatCompletionRequestAssistantMessageContentPart`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestAssistantMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestDeveloperMessage` -> `Generated.ChatCompletionRequestDeveloperMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestDeveloperMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestFunctionMessage` -> `Generated.ChatCompletionRequestFunctionMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestFunctionMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessage` -> `Generated.ChatCompletionRequestMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartAudio` -> `Generated.ChatCompletionRequestMessageContentPartAudio`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartAudioInputAudioFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartAudioType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartFile` -> `Generated.ChatCompletionRequestMessageContentPartFile`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartFileType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartImage` -> `Generated.ChatCompletionRequestMessageContentPartImage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartImageImageUrlDetail` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartImageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartRefusal` -> `Generated.ChatCompletionRequestMessageContentPartRefusal`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartRefusalType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestMessageContentPartText` -> `Generated.ChatCompletionRequestMessageContentPartText`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestMessageContentPartTextType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestSystemMessage` -> `Generated.ChatCompletionRequestSystemMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestSystemMessageContentPart` -> `Generated.ChatCompletionRequestSystemMessageContentPart`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestSystemMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestToolMessage` -> `Generated.ChatCompletionRequestToolMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestToolMessageContentPart` -> `Generated.ChatCompletionRequestToolMessageContentPart`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestToolMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionRequestUserMessage` -> `Generated.ChatCompletionRequestUserMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestUserMessageContentPart` -> `Generated.ChatCompletionRequestUserMessageContentPart`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionRequestUserMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionResponseMessage` -> `Generated.ChatCompletionResponseMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionResponseMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionStreamOptions` -> `Generated.ChatCompletionStreamOptions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionTokenLogprob` -> `Generated.ChatCompletionTokenLogprob`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionTool` -> `Generated.ChatCompletionTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionToolChoiceOption` -> `Generated.ChatCompletionToolChoiceOption`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatCompletionToolChoiceOptionEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatCompletionToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionAutomaticThreadTitling` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionChatkitConfiguration` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionFileUpload` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionHistory` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionRateLimits` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionResource` -> `Generated.ChatSessionResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChatSessionResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatSessionStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatkitConfigurationParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatkitWorkflow` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatkitWorkflowTracing` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChunkingStrategyRequestParam` -> `Generated.ChunkingStrategyRequestParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ChunkingStrategyResponse` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClickButtonType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClickParam` -> `Generated.ClickParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ClickParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Client` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClientError` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClientToolCallItem` -> `Generated.ClientToolCallItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ClientToolCallItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClientToolCallItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClientToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ClosedStatus` -> `Generated.ClosedStatus`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ClosedStatusType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterContainerAuto` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterContainerAutoType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterOutputImage` -> `Generated.CodeInterpreterOutputImage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CodeInterpreterOutputImageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterOutputLogs` -> `Generated.CodeInterpreterOutputLogs`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CodeInterpreterOutputLogsType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterTool` -> `Generated.CodeInterpreterTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CodeInterpreterToolCall` -> `Generated.CodeInterpreterToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CodeInterpreterToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CodeInterpreterToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComparisonFilter` -> `Generated.ComparisonFilter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComparisonFilterType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComparisonFilterValueItems` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompleteUploadRequest` -> `Generated.CompleteUploadRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompletionUsage` -> `Generated.CompletionUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompoundFilter` -> `Generated.CompoundFilter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CompoundFilterType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerAction` -> `Generated.ComputerAction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerCallOutputItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerCallOutputItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerCallSafetyCheckParam` -> `Generated.ComputerCallSafetyCheckParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerEnvironment` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerScreenshotContent` -> `Generated.ComputerScreenshotContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerScreenshotContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerScreenshotImage` -> `Generated.ComputerScreenshotImage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerScreenshotImageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerToolCall` -> `Generated.ComputerToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerToolCallOutputResource` -> `Generated.ComputerToolCallOutputResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerToolCallOutputResourceStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerToolCallOutputResourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ComputerUsePreviewTool` -> `Generated.ComputerUsePreviewTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ComputerUsePreviewToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ContainerFileCitationBody` -> `Generated.ContainerFileCitationBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContainerFileCitationBodyType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ContainerFileListResource` -> `Generated.ContainerFileListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContainerFileResource` -> `Generated.ContainerFileResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContainerListResource` -> `Generated.ContainerListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContainerMemoryLimit` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ContainerResource` -> `Generated.ContainerResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ContainerResourceExpiresAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Conversation2` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ConversationItem` -> `Generated.ConversationItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ConversationItemList` -> `Generated.ConversationItemList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ConversationParam` -> `Generated.ConversationParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ConversationParam2` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ConversationResource` -> `Generated.ConversationResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ConversationResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CostsResult` -> `Generated.CostsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CostsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateAssistantRequest` -> `Generated.CreateAssistantRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateBatchRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateBatchRequestCompletionWindow` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateBatchRequestEndpoint` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatCompletionRequest` -> `Generated.CreateChatCompletionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateChatCompletionRequestAudioFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatCompletionRequestFunctionCallEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatCompletionRequestPromptCacheRetentionEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatCompletionRequestWebSearchOptionsUserLocationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatCompletionResponse` -> `Generated.CreateChatCompletionResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateChatCompletionResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChatSessionBody` -> `Generated.CreateChatSessionBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateCompletionRequest` -> `Generated.CreateCompletionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateCompletionRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateCompletionResponse` -> `Generated.CreateCompletionResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateCompletionResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateContainerBody` -> `Generated.CreateContainerBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateContainerBodyExpiresAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateContainerFileBody` -> `Generated.CreateContainerFileBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateConversationBody` -> `Generated.CreateConversationBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateConversationItemsParams` -> `Generated.CreateConversationItemsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateConversationItemsRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddingRequest` -> `Generated.CreateEmbeddingRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEmbeddingRequestEncodingFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddingRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddingResponse` -> `Generated.CreateEmbeddingResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEmbeddingResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalCompletionsRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalCompletionsRunDataSourceInputMessagesEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalCompletionsRunDataSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalCustomDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalCustomDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalItem` -> `Generated.CreateEvalItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEvalJsonlRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalJsonlRunDataSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalLabelModelGrader` -> `Generated.CreateEvalLabelModelGrader`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEvalLabelModelGraderType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalLogsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalLogsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalRequest` -> `Generated.CreateEvalRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEvalResponsesRunDataSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalResponsesRunDataSourceInputMessagesEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalResponsesRunDataSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalRunRequest` -> `Generated.CreateEvalRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEvalStoredCompletionsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEvalStoredCompletionsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateFileRequest` -> `Generated.CreateFileRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateFineTuningCheckpointPermissionRequest` -> `Generated.CreateFineTuningCheckpointPermissionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateFineTuningJobRequest` -> `Generated.CreateFineTuningJobRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateFineTuningJobRequestHyperparametersBatchSizeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateFineTuningJobRequestHyperparametersLearningRateMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateFineTuningJobRequestHyperparametersNEpochsEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateFineTuningJobRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateGroupBody` -> `Generated.CreateGroupBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateGroupUserBody` -> `Generated.CreateGroupUserBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateImageEditRequest` -> `Generated.CreateImageEditRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateImageEditRequestBackground` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageEditRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageEditRequestOutputFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageEditRequestQuality` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageEditRequestResponseFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageEditRequestSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequest` -> `Generated.CreateImageRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateImageRequestBackground` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestModeration` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestOutputFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestQuality` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestResponseFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageRequestStyle` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageVariationRequest` -> `Generated.CreateImageVariationRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateImageVariationRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageVariationRequestResponseFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateImageVariationRequestSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessageRequest` -> `Generated.CreateMessageRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessageRequestRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateModerationRequest` -> `Generated.CreateModerationRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateModerationRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateModerationResponse` -> `Generated.CreateModerationResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateResponse` -> `Generated.CreateResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateResponsePromptCacheRetentionEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateResponseTruncationEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateRunParams` -> `Generated.CreateRunParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateRunRequest` -> `Generated.CreateRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateRunRequestToolChoice` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateRunRequestToolChoiceEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateRunRequestTruncationStrategy` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateRunRequestTruncationStrategyEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateSpeechRequest` -> `Generated.CreateSpeechRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateSpeechRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateSpeechRequestResponseFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateSpeechRequestStreamFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadAndRunRequest` -> `Generated.CreateThreadAndRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateThreadAndRunRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadAndRunRequestToolChoice` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadAndRunRequestToolChoiceEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadAndRunRequestTruncationStrategy` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadAndRunRequestTruncationStrategyEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateThreadRequest` -> `Generated.CreateThreadRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranscription200` -> `Generated.CreateTranscription200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranscriptionRequest` -> `Generated.CreateTranscriptionRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranscriptionRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateTranscriptionResponseDiarizedJson` -> `Generated.CreateTranscriptionResponseDiarizedJson`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranscriptionResponseDiarizedJsonTask` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateTranscriptionResponseJson` -> `Generated.CreateTranscriptionResponseJson`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranscriptionResponseVerboseJson` -> `Generated.CreateTranscriptionResponseVerboseJson`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranslation200` -> `Generated.CreateTranslation200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranslationRequest` -> `Generated.CreateTranslationRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranslationRequestModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateTranslationRequestResponseFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateTranslationResponseJson` -> `Generated.CreateTranslationResponseJson`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateTranslationResponseVerboseJson` -> `Generated.CreateTranslationResponseVerboseJson`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateUploadRequest` -> `Generated.CreateUploadRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateUploadRequestPurpose` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateVectorStoreFileBatchRequest` -> `Generated.CreateVectorStoreFileBatchRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateVectorStoreFileRequest` -> `Generated.CreateVectorStoreFileRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateVectorStoreRequest` -> `Generated.CreateVectorStoreRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateVideoBody` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateVideoRemixBody` -> `Generated.CreateVideoRemixBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomGrammarFormatParam` -> `Generated.CustomGrammarFormatParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomGrammarFormatParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomTextFormatParam` -> `Generated.CustomTextFormatParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomTextFormatParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolCall` -> `Generated.CustomToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomToolCallOutput` -> `Generated.CustomToolCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomToolCallOutputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolChatCompletions` -> `Generated.CustomToolChatCompletions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomToolChatCompletionsCustomFormatEnumGrammarSyntax` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolChatCompletionsCustomFormatEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolChatCompletionsType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CustomToolParam` -> `Generated.CustomToolParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CustomToolParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteAssistantResponse` -> `Generated.DeleteAssistantResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteAssistantResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteCertificateResponse` -> `Generated.DeleteCertificateResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteEval200` -> `Generated.DeleteEval200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteEvalRun200` -> `Generated.DeleteEvalRun200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteFileResponse` -> `Generated.DeleteFileResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteFileResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteFineTuningCheckpointPermissionResponse` -> `Generated.DeleteFineTuningCheckpointPermissionResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteFineTuningCheckpointPermissionResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteMessageResponse` -> `Generated.DeleteMessageResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteMessageResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteModelResponse` -> `Generated.DeleteModelResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteThreadResponse` -> `Generated.DeleteThreadResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteThreadResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteVectorStoreFileResponse` -> `Generated.DeleteVectorStoreFileResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteVectorStoreFileResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeleteVectorStoreResponse` -> `Generated.DeleteVectorStoreResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteVectorStoreResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeletedConversationResource` -> `Generated.DeletedConversationResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeletedConversationResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeletedRoleAssignmentResource` -> `Generated.DeletedRoleAssignmentResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeletedThreadResource` -> `Generated.DeletedThreadResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeletedThreadResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DeletedVideoResource` -> `Generated.DeletedVideoResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeletedVideoResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DetailEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DoubleClickAction` -> `Generated.DoubleClickAction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DoubleClickActionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DownloadFile200` -> `Generated.DownloadFile200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Drag` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DragPoint` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DragType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EasyInputMessage` -> `Generated.EasyInputMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EasyInputMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EasyInputMessageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Embedding` -> `Generated.Embedding`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EmbeddingObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Error` -> `Generated.Error`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Error2` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ErrorResponse` -> `Generated.ErrorResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Eval` -> `Generated.Eval`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalApiError` -> `Generated.EvalApiError`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalCustomDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalCustomDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderLabelModel` -> `Generated.EvalGraderLabelModel`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalGraderLabelModelType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderPython` -> `Generated.EvalGraderPython`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalGraderPythonType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderScoreModel` -> `Generated.EvalGraderScoreModel`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalGraderScoreModelType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderStringCheck` -> `Generated.EvalGraderStringCheck`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalGraderStringCheckOperation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderStringCheckType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderTextSimilarity` -> `Generated.EvalGraderTextSimilarity`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalGraderTextSimilarityEvaluationMetric` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalGraderTextSimilarityType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalItem` -> `Generated.EvalItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalItemContentEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalItemRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalJsonlFileContentSource` -> `Generated.EvalJsonlFileContentSource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalJsonlFileContentSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalJsonlFileIdSource` -> `Generated.EvalJsonlFileIdSource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalJsonlFileIdSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalList` -> `Generated.EvalList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalLogsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalLogsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalResponsesSource` -> `Generated.EvalResponsesSource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalResponsesSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalRun` -> `Generated.EvalRun`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalRunList` -> `Generated.EvalRunList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalRunListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalRunObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalRunOutputItem` -> `Generated.EvalRunOutputItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalRunOutputItemList` -> `Generated.EvalRunOutputItemList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalRunOutputItemListObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalRunOutputItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalRunOutputItemResult` -> `Generated.EvalRunOutputItemResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalStoredCompletionsDataSourceConfig` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalStoredCompletionsDataSourceConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.EvalStoredCompletionsSource` -> `Generated.EvalStoredCompletionsSource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EvalStoredCompletionsSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ExpiresAfterParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ExpiresAfterParamAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileAnnotation` -> `Generated.FileAnnotation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileAnnotationSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileAnnotationSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileAnnotationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileCitationBody` -> `Generated.FileCitationBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileCitationBodyType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileExpirationAfter` -> `Generated.FileExpirationAfter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileExpirationAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FilePath` -> `Generated.FilePath`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FilePathType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FilePurpose` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileSearchRanker` -> `Generated.FileSearchRanker`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileSearchRankingOptions` -> `Generated.FileSearchRankingOptions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileSearchTool` -> `Generated.FileSearchTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileSearchToolCall` -> `Generated.FileSearchToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileSearchToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileSearchToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileSearchToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileUploadParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Filters` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneDPOHyperparameters` -> `Generated.FineTuneDPOHyperparameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneDPOHyperparametersBatchSizeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneDPOHyperparametersBetaEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneDPOHyperparametersLearningRateMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneDPOHyperparametersNEpochsEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneDPOMethod` -> `Generated.FineTuneDPOMethod`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneMethod` -> `Generated.FineTuneMethod`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneMethodType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparameters` -> `Generated.FineTuneReinforcementHyperparameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneReinforcementHyperparametersBatchSizeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersComputeMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersEvalIntervalEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersEvalSamplesEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersLearningRateMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersNEpochsEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementHyperparametersReasoningEffort` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneReinforcementMethod` -> `Generated.FineTuneReinforcementMethod`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneSupervisedHyperparameters` -> `Generated.FineTuneSupervisedHyperparameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuneSupervisedHyperparametersBatchSizeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneSupervisedHyperparametersLearningRateMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneSupervisedHyperparametersNEpochsEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuneSupervisedMethod` -> `Generated.FineTuneSupervisedMethod`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningCheckpointPermission` -> `Generated.FineTuningCheckpointPermission`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningCheckpointPermissionObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningIntegration` -> `Generated.FineTuningIntegration`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningIntegrationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJob` -> `Generated.FineTuningJob`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningJobCheckpoint` -> `Generated.FineTuningJobCheckpoint`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningJobCheckpointObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobEvent` -> `Generated.FineTuningJobEvent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FineTuningJobEventLevel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobEventObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobEventType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobHyperparametersBatchSizeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobHyperparametersLearningRateMultiplierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobHyperparametersNEpochsEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FineTuningJobStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionAndCustomToolCallOutput` -> `Generated.FunctionAndCustomToolCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionCallItemStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionCallOutputItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionCallOutputItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionObject` -> `Generated.FunctionObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionParameters` -> `Generated.FunctionParameters`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellAction` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellActionParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCall` -> `Generated.FunctionShellCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallItemStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutput` -> `Generated.FunctionShellCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputContent` -> `Generated.FunctionShellCallOutputContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputContentParam` -> `Generated.FunctionShellCallOutputContentParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputExitOutcome` -> `Generated.FunctionShellCallOutputExitOutcome`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputExitOutcomeParam` -> `Generated.FunctionShellCallOutputExitOutcomeParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputExitOutcomeParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputExitOutcomeType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputItemParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputItemParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputOutcomeParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputTimeoutOutcome` -> `Generated.FunctionShellCallOutputTimeoutOutcome`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputTimeoutOutcomeParam` -> `Generated.FunctionShellCallOutputTimeoutOutcomeParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellCallOutputTimeoutOutcomeParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputTimeoutOutcomeType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallOutputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionShellToolParam` -> `Generated.FunctionShellToolParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionShellToolParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionTool` -> `Generated.FunctionTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionToolCall` -> `Generated.FunctionToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionToolCallOutputResource` -> `Generated.FunctionToolCallOutputResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionToolCallOutputResourceStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolCallOutputResourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolCallResource` -> `Generated.FunctionToolCallResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FunctionToolCallResourceStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolCallResourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FunctionToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetCertificateParams` -> `Generated.GetCertificateParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetChatCompletionMessagesParams` -> `Generated.GetChatCompletionMessagesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetChatCompletionMessagesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetConversationItemParams` -> `Generated.GetConversationItemParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetEvalRunOutputItemsParams` -> `Generated.GetEvalRunOutputItemsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetEvalRunOutputItemsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetEvalRunOutputItemsParamsStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetEvalRunsParams` -> `Generated.GetEvalRunsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetEvalRunsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetEvalRunsParamsStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetResponseParams` -> `Generated.GetResponseParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetRunStepParams` -> `Generated.GetRunStepParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderLabelModel` -> `Generated.GraderLabelModel`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderLabelModelType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderMulti` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderMultiType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderPython` -> `Generated.GraderPython`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderPythonType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderScoreModel` -> `Generated.GraderScoreModel`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderScoreModelType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderStringCheck` -> `Generated.GraderStringCheck`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderStringCheckOperation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderStringCheckType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderTextSimilarity` -> `Generated.GraderTextSimilarity`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GraderTextSimilarityEvaluationMetric` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GraderTextSimilarityType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GrammarSyntax1` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Group` -> `Generated.Group`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupDeletedResource` -> `Generated.GroupDeletedResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupDeletedResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GroupListResource` -> `Generated.GroupListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupListResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GroupObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GroupResourceWithSuccess` -> `Generated.GroupResourceWithSuccess`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupResponse` -> `Generated.GroupResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupRoleAssignment` -> `Generated.GroupRoleAssignment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupRoleAssignmentObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GroupUserAssignment` -> `Generated.GroupUserAssignment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupUserAssignmentObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GroupUserDeletedResource` -> `Generated.GroupUserDeletedResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GroupUserDeletedResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.HistoryParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.HybridSearchOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Image` -> `Generated.Image`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImageDetail` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenInputUsageDetails` -> `Generated.ImageGenInputUsageDetails`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImageGenTool` -> `Generated.ImageGenTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImageGenToolBackground` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolCall` -> `Generated.ImageGenToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImageGenToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolModeration` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolOutputFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolQuality` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImageGenUsage` -> `Generated.ImageGenUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImagesResponse` -> `Generated.ImagesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImagesResponseBackground` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImagesResponseOutputFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImagesResponseQuality` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ImagesResponseSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.IncludeEnum` -> `Generated.IncludeEnum`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InferenceOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputAudio` -> `Generated.InputAudio`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputAudioInputAudioFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputAudioType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputContent` -> `Generated.InputContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputFidelity` -> `Generated.InputFidelity`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputFileContent` -> `Generated.InputFileContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputFileContentParam` -> `Generated.InputFileContentParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputFileContentParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputFileContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputImageContent` -> `Generated.InputImageContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputImageContentParamAutoParam` -> `Generated.InputImageContentParamAutoParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputImageContentParamAutoParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputImageContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputItem` -> `Generated.InputItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputMessage` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageContentList` -> `Generated.InputMessageContentList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputMessageResource` -> `Generated.InputMessageResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputMessageResourceRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageResourceStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageResourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputMessageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputParam` -> `Generated.InputParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputTextContent` -> `Generated.InputTextContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputTextContentParam` -> `Generated.InputTextContentParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputTextContentParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InputTextContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Invite` -> `Generated.Invite`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InviteDeleteResponse` -> `Generated.InviteDeleteResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InviteDeleteResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InviteListResponse` -> `Generated.InviteListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InviteListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InviteObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InviteProjectGroupBody` -> `Generated.InviteProjectGroupBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InviteRequest` -> `Generated.InviteRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InviteRequestRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InviteRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.InviteStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Item` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ItemReferenceParam` -> `Generated.ItemReferenceParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ItemReferenceParamTypeEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ItemResource` -> `Generated.ItemResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.KeyPressAction` -> `Generated.KeyPressAction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.KeyPressActionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListAssistantsParams` -> `Generated.ListAssistantsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListAssistantsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListAssistantsResponse` -> `Generated.ListAssistantsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListAuditLogsParams` -> `Generated.ListAuditLogsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListAuditLogsResponse` -> `Generated.ListAuditLogsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListAuditLogsResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListBatchesParams` -> `Generated.ListBatchesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListBatchesResponse` -> `Generated.ListBatchesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListBatchesResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListCertificatesResponse` -> `Generated.ListCertificatesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListCertificatesResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListChatCompletionsParams` -> `Generated.ListChatCompletionsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListChatCompletionsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListContainerFilesParams` -> `Generated.ListContainerFilesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListContainerFilesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListContainersParams` -> `Generated.ListContainersParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListContainersParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListConversationItemsParams` -> `Generated.ListConversationItemsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListConversationItemsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListEvalsParams` -> `Generated.ListEvalsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListEvalsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListEvalsParamsOrderBy` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFilesInVectorStoreBatchParams` -> `Generated.ListFilesInVectorStoreBatchParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFilesInVectorStoreBatchParamsFilter` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFilesInVectorStoreBatchParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFilesParams` -> `Generated.ListFilesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFilesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFilesResponse` -> `Generated.ListFilesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningCheckpointPermissionResponse` -> `Generated.ListFineTuningCheckpointPermissionResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningCheckpointPermissionResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFineTuningCheckpointPermissionsParams` -> `Generated.ListFineTuningCheckpointPermissionsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningCheckpointPermissionsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFineTuningEventsParams` -> `Generated.ListFineTuningEventsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningJobCheckpointsParams` -> `Generated.ListFineTuningJobCheckpointsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningJobCheckpointsResponse` -> `Generated.ListFineTuningJobCheckpointsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningJobCheckpointsResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListFineTuningJobEventsResponse` -> `Generated.ListFineTuningJobEventsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListFineTuningJobEventsResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListGroupRoleAssignmentsParams` -> `Generated.ListGroupRoleAssignmentsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGroupRoleAssignmentsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListGroupUsersParams` -> `Generated.ListGroupUsersParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGroupUsersParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListGroupsParams` -> `Generated.ListGroupsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGroupsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListInputItemsParams` -> `Generated.ListInputItemsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListInputItemsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListInvitesParams` -> `Generated.ListInvitesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListMessagesParams` -> `Generated.ListMessagesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListMessagesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListMessagesResponse` -> `Generated.ListMessagesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListModelsResponse` -> `Generated.ListModelsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListModelsResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListOrganizationCertificatesParams` -> `Generated.ListOrganizationCertificatesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListOrganizationCertificatesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListPaginatedFineTuningJobsParams` -> `Generated.ListPaginatedFineTuningJobsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListPaginatedFineTuningJobsResponse` -> `Generated.ListPaginatedFineTuningJobsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListPaginatedFineTuningJobsResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectApiKeysParams` -> `Generated.ListProjectApiKeysParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectCertificatesParams` -> `Generated.ListProjectCertificatesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectCertificatesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectGroupRoleAssignmentsParams` -> `Generated.ListProjectGroupRoleAssignmentsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectGroupRoleAssignmentsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectGroupsParams` -> `Generated.ListProjectGroupsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectGroupsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectRateLimitsParams` -> `Generated.ListProjectRateLimitsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectRolesParams` -> `Generated.ListProjectRolesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectRolesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectServiceAccountsParams` -> `Generated.ListProjectServiceAccountsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectUserRoleAssignmentsParams` -> `Generated.ListProjectUserRoleAssignmentsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectUserRoleAssignmentsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListProjectUsersParams` -> `Generated.ListProjectUsersParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProjectsParams` -> `Generated.ListProjectsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListRolesParams` -> `Generated.ListRolesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListRolesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListRunStepsParams` -> `Generated.ListRunStepsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListRunStepsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListRunStepsResponse` -> `Generated.ListRunStepsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListRunsParams` -> `Generated.ListRunsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListRunsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListRunsResponse` -> `Generated.ListRunsResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListThreadItemsMethodParams` -> `Generated.ListThreadItemsMethodParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListThreadsMethodParams` -> `Generated.ListThreadsMethodParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListUserRoleAssignmentsParams` -> `Generated.ListUserRoleAssignmentsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListUserRoleAssignmentsParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListUsersParams` -> `Generated.ListUsersParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListVectorStoreFilesParams` -> `Generated.ListVectorStoreFilesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListVectorStoreFilesParamsFilter` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListVectorStoreFilesParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListVectorStoreFilesResponse` -> `Generated.ListVectorStoreFilesResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListVectorStoresParams` -> `Generated.ListVectorStoresParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListVectorStoresParamsOrder` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListVectorStoresResponse` -> `Generated.ListVectorStoresResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListVideosParams` -> `Generated.ListVideosParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LocalShellCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellExecAction` -> `Generated.LocalShellExecAction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LocalShellExecActionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellToolCall` -> `Generated.LocalShellToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LocalShellToolCallOutput` -> `Generated.LocalShellToolCallOutput`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LocalShellToolCallOutputStatusEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellToolCallOutputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LocalShellToolParam` -> `Generated.LocalShellToolParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LocalShellToolParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LockedStatus` -> `Generated.LockedStatus`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.LockedStatusType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.LogProb` -> `Generated.LogProb`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPApprovalRequest` -> `Generated.MCPApprovalRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPApprovalRequestType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPApprovalResponse` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPApprovalResponseResource` -> `Generated.MCPApprovalResponseResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPApprovalResponseResourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPApprovalResponseType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPListTools` -> `Generated.MCPListTools`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPListToolsTool` -> `Generated.MCPListToolsTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPListToolsType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPTool` -> `Generated.MCPTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPToolCall` -> `Generated.MCPToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPToolConnectorId` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPToolFilter` -> `Generated.MCPToolFilter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MCPToolRequireApprovalEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MCPToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Message` -> `Generated.Message`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContent` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentImageFileObject` -> `Generated.MessageContentImageFileObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentImageFileObjectImageFileDetail` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentImageFileObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentImageUrlObject` -> `Generated.MessageContentImageUrlObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentImageUrlObjectImageUrlDetail` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentImageUrlObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentRefusalObject` -> `Generated.MessageContentRefusalObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentRefusalObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentTextAnnotationsFileCitationObject` -> `Generated.MessageContentTextAnnotationsFileCitationObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentTextAnnotationsFileCitationObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentTextAnnotationsFilePathObject` -> `Generated.MessageContentTextAnnotationsFilePathObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentTextAnnotationsFilePathObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageContentTextObject` -> `Generated.MessageContentTextObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageContentTextObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageObject` -> `Generated.MessageObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageObjectIncompleteDetailsEnumReason` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageObjectRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageObjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageRequestContentTextObject` -> `Generated.MessageRequestContentTextObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.MessageRequestContentTextObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MessageStatus` -> `OpenAiSchema.MessageStatus`: Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated. + +- `Generated.MessageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Metadata` -> `Generated.Metadata`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Model` -> `Generated.Model`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelIdsResponses` -> `Generated.ModelIdsResponses`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelIdsResponsesEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModelIdsShared` -> `Generated.ModelIdsShared`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModerationImageURLInput` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModerationImageURLInputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModerationTextInput` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModerationTextInputType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModifyAssistantRequest` -> `Generated.ModifyAssistantRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModifyCertificateRequest` -> `Generated.ModifyCertificateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModifyMessageRequest` -> `Generated.ModifyMessageRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModifyRunRequest` -> `Generated.ModifyRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModifyThreadRequest` -> `Generated.ModifyThreadRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Move` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.MoveType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.NoiseReductionType` -> `Generated.NoiseReductionType`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIFile` -> `Generated.OpenAIFile`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIFileObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIFilePurpose` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIFileStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OrderEnum` -> `Generated.OrderEnum`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OtherChunkingStrategyResponseParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OtherChunkingStrategyResponseParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItem` -> `Generated.OutputItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputMessage` -> `Generated.OutputMessage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputMessageContent` -> `Generated.OutputMessageContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputMessageRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputMessageStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputMessageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputTextContent` -> `Generated.OutputTextContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputTextContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ParallelToolCalls` -> `Generated.ParallelToolCalls`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PartialImages` -> `Generated.PartialImages`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PredictionContent` -> `Generated.PredictionContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PredictionContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Project` -> `Generated.Project`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectApiKey` -> `Generated.ProjectApiKey`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectApiKeyDeleteResponse` -> `Generated.ProjectApiKeyDeleteResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectApiKeyDeleteResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectApiKeyListResponse` -> `Generated.ProjectApiKeyListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectApiKeyListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectApiKeyObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectApiKeyOwnerType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectCreateRequest` -> `Generated.ProjectCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectCreateRequestGeography` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectGroup` -> `Generated.ProjectGroup`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectGroupDeletedResource` -> `Generated.ProjectGroupDeletedResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectGroupDeletedResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectGroupListResource` -> `Generated.ProjectGroupListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectGroupListResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectGroupObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectListResponse` -> `Generated.ProjectListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectRateLimit` -> `Generated.ProjectRateLimit`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectRateLimitListResponse` -> `Generated.ProjectRateLimitListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectRateLimitListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectRateLimitObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectRateLimitUpdateRequest` -> `Generated.ProjectRateLimitUpdateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccount` -> `Generated.ProjectServiceAccount`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountApiKey` -> `Generated.ProjectServiceAccountApiKey`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountApiKeyObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountCreateRequest` -> `Generated.ProjectServiceAccountCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountCreateResponse` -> `Generated.ProjectServiceAccountCreateResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountCreateResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountCreateResponseRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountDeleteResponse` -> `Generated.ProjectServiceAccountDeleteResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountDeleteResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountListResponse` -> `Generated.ProjectServiceAccountListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectServiceAccountListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectServiceAccountRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectUpdateRequest` -> `Generated.ProjectUpdateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUser` -> `Generated.ProjectUser`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUserCreateRequest` -> `Generated.ProjectUserCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUserCreateRequestRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectUserDeleteResponse` -> `Generated.ProjectUserDeleteResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUserDeleteResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectUserListResponse` -> `Generated.ProjectUserListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUserObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectUserRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProjectUserUpdateRequest` -> `Generated.ProjectUserUpdateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProjectUserUpdateRequestRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Prompt` -> `Generated.Prompt`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PublicAssignOrganizationGroupRoleBody` -> `Generated.PublicAssignOrganizationGroupRoleBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PublicCreateOrganizationRoleBody` -> `Generated.PublicCreateOrganizationRoleBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PublicRoleListResource` -> `Generated.PublicRoleListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PublicRoleListResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.PublicUpdateOrganizationRoleBody` -> `Generated.PublicUpdateOrganizationRoleBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RankerVersionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RankingOptions` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RateLimitsParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeAudioFormats` -> `Generated.RealtimeAudioFormats`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeAudioFormatsEnumRate` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeAudioFormatsEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeCallCreateRequest` -> `Generated.RealtimeCallCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeCallReferRequest` -> `Generated.RealtimeCallReferRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeCallRejectRequest` -> `Generated.RealtimeCallRejectRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeCreateClientSecretRequest` -> `Generated.RealtimeCreateClientSecretRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeCreateClientSecretRequestExpiresAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeCreateClientSecretResponse` -> `Generated.RealtimeCreateClientSecretResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeFunctionTool` -> `Generated.RealtimeFunctionTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeFunctionToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequest` -> `Generated.RealtimeSessionCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeSessionCreateRequestGA` -> `Generated.RealtimeSessionCreateRequestGA`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeSessionCreateRequestGAMaxOutputTokensEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequestGAModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequestGATracingEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequestGAType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequestMaxResponseOutputTokensEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateRequestTracingEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponse` -> `Generated.RealtimeSessionCreateResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeSessionCreateResponseGA` -> `Generated.RealtimeSessionCreateResponseGA`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeSessionCreateResponseGAMaxOutputTokensEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponseGAModelEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponseGATracingEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponseGAType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponseMaxOutputTokensEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeSessionCreateResponseTracingEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTranscriptionSessionCreateRequest` -> `Generated.RealtimeTranscriptionSessionCreateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTranscriptionSessionCreateRequestGA` -> `Generated.RealtimeTranscriptionSessionCreateRequestGA`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTranscriptionSessionCreateRequestGAType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTranscriptionSessionCreateRequestInputAudioFormat` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTranscriptionSessionCreateRequestTurnDetectionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTranscriptionSessionCreateResponse` -> `Generated.RealtimeTranscriptionSessionCreateResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTranscriptionSessionCreateResponseGA` -> `Generated.RealtimeTranscriptionSessionCreateResponseGA`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTranscriptionSessionCreateResponseGAType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTruncation` -> `Generated.RealtimeTruncation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTruncationEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTruncationEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RealtimeTurnDetection` -> `Generated.RealtimeTurnDetection`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RealtimeTurnDetectionEnumEagerness` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Reasoning` -> `Generated.Reasoning`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningEffort` -> `Generated.ReasoningEffort`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningEffortEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningGenerateSummaryEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningItem` -> `Generated.ReasoningItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningItemStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningSummaryEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningTextContent` -> `Generated.ReasoningTextContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningTextContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RefusalContent` -> `Generated.RefusalContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RefusalContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Response` -> `Generated.Response`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseError` -> `Generated.ResponseError`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseErrorCode` -> `Generated.ResponseErrorCode`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatJsonObject` -> `Generated.ResponseFormatJsonObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatJsonObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseFormatJsonSchema` -> `Generated.ResponseFormatJsonSchema`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatJsonSchemaSchema` -> `Generated.ResponseFormatJsonSchemaSchema`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatJsonSchemaType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseFormatText` -> `Generated.ResponseFormatText`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatTextType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseIncompleteDetailsEnumReason` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseItemList` -> `Generated.ResponseItemList`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseModalities` -> `Generated.ResponseModalities`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseOutputText` -> `Generated.ResponseOutputText`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseOutputTextType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsePromptCacheRetentionEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsePromptVariables` -> `Generated.ResponsePromptVariables`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseStreamOptions` -> `Generated.ResponseStreamOptions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseTextParam` -> `Generated.ResponseTextParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseTruncationEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseUsage` -> `Generated.ResponseUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RetrieveVideoContent200` -> `Generated.RetrieveVideoContent200`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RetrieveVideoContentParams` -> `Generated.RetrieveVideoContentParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Role` -> `Generated.Role`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RoleDeletedResource` -> `Generated.RoleDeletedResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RoleDeletedResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RoleListResource` -> `Generated.RoleListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RoleListResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RoleObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunCompletionUsage` -> `Generated.RunCompletionUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunGraderRequest` -> `Generated.RunGraderRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunGraderResponse` -> `Generated.RunGraderResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunObject` -> `Generated.RunObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunObjectIncompleteDetailsReason` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectLastErrorCode` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectRequiredActionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectToolChoice` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectToolChoiceEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectTruncationStrategy` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunObjectTruncationStrategyEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepCompletionUsage` -> `Generated.RunStepCompletionUsage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsMessageCreationObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsMessageCreationObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCall` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsCodeObject` -> `Generated.RunStepDetailsToolCallsCodeObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsToolCallsCodeObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsCodeOutputImageObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsCodeOutputImageObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsCodeOutputLogsObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsCodeOutputLogsObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsFileSearchObject` -> `Generated.RunStepDetailsToolCallsFileSearchObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsToolCallsFileSearchObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsFileSearchRankingOptionsObject` -> `Generated.RunStepDetailsToolCallsFileSearchRankingOptionsObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsToolCallsFileSearchResultObject` -> `Generated.RunStepDetailsToolCallsFileSearchResultObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsToolCallsFunctionObject` -> `Generated.RunStepDetailsToolCallsFunctionObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepDetailsToolCallsFunctionObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepDetailsToolCallsObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepObject` -> `Generated.RunStepObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunStepObjectLastErrorEnumCode` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepObjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunStepObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RunToolCallObject` -> `Generated.RunToolCallObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RunToolCallObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Screenshot` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ScreenshotType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Scroll` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ScrollType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.SearchContextSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ServiceTier` -> `Generated.ServiceTier`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ServiceTierEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.SpecificApplyPatchParam` -> `Generated.SpecificApplyPatchParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SpecificApplyPatchParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.SpecificFunctionShellParam` -> `Generated.SpecificFunctionShellParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SpecificFunctionShellParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.StaticChunkingStrategy` -> `Generated.StaticChunkingStrategy`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.StaticChunkingStrategyRequestParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.StaticChunkingStrategyRequestParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.StaticChunkingStrategyResponseParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.StaticChunkingStrategyResponseParamType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.StopConfiguration` -> `Generated.StopConfiguration`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SubmitToolOutputsRunRequest` -> `Generated.SubmitToolOutputsRunRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SummaryTextContent` -> `Generated.SummaryTextContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SummaryTextContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.SummaryType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TaskGroupItem` -> `Generated.TaskGroupItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TaskGroupItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TaskGroupItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TaskGroupTask` -> `Generated.TaskGroupTask`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TaskItem` -> `Generated.TaskItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TaskItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TaskItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TaskType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TextAnnotation` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TextContent` -> `Generated.TextContent`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextContentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TextResponseFormatConfiguration` -> `Generated.TextResponseFormatConfiguration`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextResponseFormatJsonSchema` -> `Generated.TextResponseFormatJsonSchema`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TextResponseFormatJsonSchemaType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ThreadItem` -> `Generated.ThreadItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThreadItemListResource` -> `Generated.ThreadItemListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThreadListResource` -> `Generated.ThreadListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThreadObject` -> `Generated.ThreadObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThreadObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ThreadResource` -> `Generated.ThreadResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ThreadResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToggleCertificatesRequest` -> `Generated.ToggleCertificatesRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TokenCountsBody` -> `Generated.TokenCountsBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TokenCountsResource` -> `Generated.TokenCountsResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TokenCountsResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Tool` -> `Generated.Tool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoice` -> `OpenAiSchema.ToolChoice`: Use the focused v4 OpenAiSchema definition; the old generated export was removed when the OpenAI specification client was regenerated. + +- `Generated.ToolChoiceAllowed` -> `Generated.ToolChoiceAllowed`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceAllowedMode` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolChoiceAllowedType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolChoiceCustom` -> `Generated.ToolChoiceCustom`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceCustomType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolChoiceFunction` -> `Generated.ToolChoiceFunction`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceFunctionType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolChoiceMCP` -> `Generated.ToolChoiceMCP`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceMCPType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolChoiceOptions` -> `Generated.ToolChoiceOptions`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceParam` -> `Generated.ToolChoiceParam`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceTypes` -> `Generated.ToolChoiceTypes`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceTypesType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ToolsArray` -> `Generated.ToolsArray`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TopLogProb` -> `Generated.TopLogProb`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptTextUsageDuration` -> `Generated.TranscriptTextUsageDuration`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptTextUsageDurationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TranscriptTextUsageTokens` -> `Generated.TranscriptTextUsageTokens`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptTextUsageTokensType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TranscriptionChunkingStrategy` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TranscriptionChunkingStrategyEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TranscriptionDiarizedSegment` -> `Generated.TranscriptionDiarizedSegment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptionDiarizedSegmentType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TranscriptionInclude` -> `Generated.TranscriptionInclude`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptionSegment` -> `Generated.TranscriptionSegment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TranscriptionWord` -> `Generated.TranscriptionWord`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TruncationEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Type` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TypeType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateChatCompletionRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateConversationBody` -> `Generated.UpdateConversationBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateEvalRequest` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateGroupBody` -> `Generated.UpdateGroupBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateVectorStoreFileAttributesRequest` -> `Generated.UpdateVectorStoreFileAttributesRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateVectorStoreRequest` -> `Generated.UpdateVectorStoreRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateVectorStoreRequestExpiresAfter` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateVectorStoreRequestExpiresAfterEnumAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Upload` -> `Generated.Upload`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UploadCertificateRequest` -> `Generated.UploadCertificateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UploadFile` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadFileEnumObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadFileEnumPurpose` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadFileEnumStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadPart` -> `Generated.UploadPart`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UploadPartObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UploadStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UrlAnnotation` -> `Generated.UrlAnnotation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UrlAnnotationSource` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UrlAnnotationSourceType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UrlAnnotationType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UrlCitationBody` -> `Generated.UrlCitationBody`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UrlCitationBodyType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageAudioSpeechesParams` -> `Generated.UsageAudioSpeechesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageAudioSpeechesParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageAudioSpeechesResult` -> `Generated.UsageAudioSpeechesResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageAudioSpeechesResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageAudioTranscriptionsParams` -> `Generated.UsageAudioTranscriptionsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageAudioTranscriptionsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageAudioTranscriptionsResult` -> `Generated.UsageAudioTranscriptionsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageAudioTranscriptionsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageCodeInterpreterSessionsParams` -> `Generated.UsageCodeInterpreterSessionsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageCodeInterpreterSessionsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageCodeInterpreterSessionsResult` -> `Generated.UsageCodeInterpreterSessionsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageCodeInterpreterSessionsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageCompletionsParams` -> `Generated.UsageCompletionsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageCompletionsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageCompletionsResult` -> `Generated.UsageCompletionsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageCompletionsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageCostsParams` -> `Generated.UsageCostsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageCostsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageEmbeddingsParams` -> `Generated.UsageEmbeddingsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageEmbeddingsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageEmbeddingsResult` -> `Generated.UsageEmbeddingsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageEmbeddingsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageImagesParams` -> `Generated.UsageImagesParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageImagesParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageImagesResult` -> `Generated.UsageImagesResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageImagesResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageModerationsParams` -> `Generated.UsageModerationsParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageModerationsParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageModerationsResult` -> `Generated.UsageModerationsResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageModerationsResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageResponse` -> `Generated.UsageResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageTimeBucket` -> `Generated.UsageTimeBucket`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageTimeBucketObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageVectorStoresParams` -> `Generated.UsageVectorStoresParams`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageVectorStoresParamsBucketWidth` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UsageVectorStoresResult` -> `Generated.UsageVectorStoresResult`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UsageVectorStoresResultObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.User` -> `Generated.User`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserDeleteResponse` -> `Generated.UserDeleteResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserDeleteResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserListResource` -> `Generated.UserListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserListResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserListResponse` -> `Generated.UserListResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserListResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserMessageInputText` -> `Generated.UserMessageInputText`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserMessageInputTextType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserMessageItem` -> `Generated.UserMessageItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserMessageItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserMessageItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserMessageQuotedText` -> `Generated.UserMessageQuotedText`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserMessageQuotedTextType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserRoleAssignment` -> `Generated.UserRoleAssignment`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserRoleAssignmentObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserRoleUpdateRequest` -> `Generated.UserRoleUpdateRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UserRoleUpdateRequestRole` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VadConfig` -> `Generated.VadConfig`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VadConfigType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ValidateGraderRequest` -> `Generated.ValidateGraderRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ValidateGraderResponse` -> `Generated.ValidateGraderResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreExpirationAfter` -> `Generated.VectorStoreExpirationAfter`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreExpirationAfterAnchor` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileAttributes` -> `Generated.VectorStoreFileAttributes`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreFileBatchObject` -> `Generated.VectorStoreFileBatchObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreFileBatchObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileBatchObjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileContentResponse` -> `Generated.VectorStoreFileContentResponse`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreFileContentResponseObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileObject` -> `Generated.VectorStoreFileObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreFileObjectLastErrorEnumCode` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreFileObjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreObject` -> `Generated.VectorStoreObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreObjectObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreObjectStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreSearchRequest` -> `Generated.VectorStoreSearchRequest`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreSearchRequestRankingOptionsRanker` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreSearchResultContentObject` -> `Generated.VectorStoreSearchResultContentObject`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreSearchResultContentObjectType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VectorStoreSearchResultItem` -> `Generated.VectorStoreSearchResultItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreSearchResultsPage` -> `Generated.VectorStoreSearchResultsPage`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VectorStoreSearchResultsPageObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Verbosity` -> `Generated.Verbosity`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VerbosityEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VideoContentVariant` -> `Generated.VideoContentVariant`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VideoListResource` -> `Generated.VideoListResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VideoModel` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VideoResource` -> `Generated.VideoResource`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VideoResourceObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VideoSeconds` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VideoSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VideoStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.VoiceIdsShared` -> `Generated.VoiceIdsShared`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.VoiceIdsSharedEnum` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Wait` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WaitType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionFind` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionFindType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionOpenPage` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionOpenPageType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionSearch` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchActionSearchType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchApproximateLocation` -> `Generated.WebSearchApproximateLocation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchApproximateLocationEnumType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchContextSize` -> `Generated.WebSearchContextSize`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchLocation` -> `Generated.WebSearchLocation`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchPreviewTool` -> `Generated.WebSearchPreviewTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchPreviewToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchTool` -> `Generated.WebSearchTool`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchToolCall` -> `Generated.WebSearchToolCall`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchToolCallStatus` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchToolCallType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchToolSearchContextSize` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchToolType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WidgetMessageItem` -> `Generated.WidgetMessageItem`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WidgetMessageItemObject` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WidgetMessageItemType` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WorkflowParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WorkflowTracingParam` -> `none`: Removed when the OpenAI specification client was regenerated; use the current Generated or OpenAiSchema request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.make` -> `Generated.make`: Still generated in v4 from the current OpenAI specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +### `@effect/ai-openai/OpenAiClient` + +- `OpenAiClient.LogProbs` -> `Generated.LogProb`: The client-local log-probability schema moved to the regenerated v4 OpenAI schema surface and changed shape. + +- `OpenAiClient.ResponseCodeInterpreterCallCodeDeltaEvent` -> `Generated.ResponseCodeInterpreterCallCodeDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCodeInterpreterCallCodeDoneEvent` -> `Generated.ResponseCodeInterpreterCallCodeDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCodeInterpreterCallCompletedEvent` -> `Generated.ResponseCodeInterpreterCallCompletedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCodeInterpreterCallInProgressEvent` -> `Generated.ResponseCodeInterpreterCallInProgressEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCodeInterpreterCallInterpretingEvent` -> `Generated.ResponseCodeInterpreterCallInterpretingEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCompletedEvent` -> `Generated.ResponseCompletedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseContentPartAddedEvent` -> `Generated.ResponseContentPartAddedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseContentPartDoneEvent` -> `Generated.ResponseContentPartDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCreatedEvent` -> `Generated.ResponseCreatedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCustomToolCallInputDeltaEvent` -> `Generated.ResponseCustomToolCallInputDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseCustomToolCallInputDoneEvent` -> `Generated.ResponseCustomToolCallInputDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseErrorEvent` -> `Generated.ResponseErrorEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFailedEvent` -> `Generated.ResponseFailedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFileSearchCallCompletedEvent` -> `Generated.ResponseFileSearchCallCompletedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFileSearchCallInProgressEvent` -> `Generated.ResponseFileSearchCallInProgressEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFileSearchCallSearchingEvent` -> `Generated.ResponseFileSearchCallSearchingEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFunctionCallArgumentsDeltaEvent` -> `Generated.ResponseFunctionCallArgumentsDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseFunctionCallArgumentsDoneEvent` -> `Generated.ResponseFunctionCallArgumentsDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseImageGenerationCallCompletedEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseImageGenerationCallGeneratingEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseImageGenerationCallInProgressEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseImageGenerationCallPartialImageEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseInProgressEvent` -> `Generated.ResponseInProgressEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseIncompleteEvent` -> `Generated.ResponseIncompleteEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseMcpCallArgumentsDeltaEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpCallArgumentsDoneEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpCallCompletedEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpCallFailedEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpCallInProgressEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpListToolsCompletedEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpListToolsFailedEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseMcpListToolsInProgressEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseOutputItemAddedEvent` -> `Generated.ResponseOutputItemAddedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseOutputItemDoneEvent` -> `Generated.ResponseOutputItemDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseOutputTextAnnotationAddedEvent` -> `Generated.ResponseOutputTextAnnotationAddedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseOutputTextDeltaEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseOutputTextDoneEvent` -> `OpenAiClient.ResponseStreamEvent`: The standalone event helper was removed from OpenAiClient; narrow the v4 ResponseStreamEvent union by its `type` discriminator. + +- `OpenAiClient.ResponseQueuedEvent` -> `Generated.ResponseQueuedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningSummaryPartAddedEvent` -> `Generated.ResponseReasoningSummaryPartAddedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningSummaryPartDoneEvent` -> `Generated.ResponseReasoningSummaryPartDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningSummaryTextDeltaEvent` -> `Generated.ResponseReasoningSummaryTextDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningSummaryTextDoneEvent` -> `Generated.ResponseReasoningSummaryTextDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningTextDeltaEvent` -> `Generated.ResponseReasoningTextDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseReasoningTextDoneEvent` -> `Generated.ResponseReasoningTextDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseRefusalDeltaEvent` -> `Generated.ResponseRefusalDeltaEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseRefusalDoneEvent` -> `Generated.ResponseRefusalDoneEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseStreamEvent` -> `OpenAiClient.ResponseStreamEvent`: Still exported in v4; adapt to the rewritten Responses API client and its revised schema and error types. + +- `OpenAiClient.ResponseWebSearchCallCompletedEvent` -> `Generated.ResponseWebSearchCallCompletedEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseWebSearchCallInProgressEvent` -> `Generated.ResponseWebSearchCallInProgressEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.ResponseWebSearchCallSearchingEvent` -> `Generated.ResponseWebSearchCallSearchingEvent`: The event schema moved out of OpenAiClient into the regenerated OpenAI schema surface; re-check its v4 Type/Encoded shape. + +- `OpenAiClient.Service` -> `OpenAiClient.Service`: Still exported in v4; adapt to the rewritten Responses API client and its revised schema and error types. + +- `OpenAiClient.StreamCompletionRequest` -> `OpenAiSchema.CreateResponse.Encoded`: The chat-completions request alias was removed; the v4 client uses the Responses API, with streaming inferred by OpenAiClient.createResponseStream. + +- `OpenAiClient.SummaryPart` -> `OpenAiSchema.SummaryTextContent`: The client-local reasoning summary schema moved to the focused v4 OpenAiSchema module. + +### `@effect/ai-openai/OpenAiConfig` + +- `OpenAiConfig.OpenAiConfig` -> `OpenAiConfig.OpenAiConfig`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiConfig.OpenAiConfig.Service` -> `OpenAiConfig.OpenAiConfig.Service`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +### `@effect/ai-openai/OpenAiEmbeddingModel` + +- `OpenAiEmbeddingModel.Config` -> `OpenAiEmbeddingModel.Config`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiEmbeddingModel.Config.Batched` -> `OpenAiEmbeddingModel.Config.Service`: Batch-mode configuration was removed; use the unified embedding config and constructor. + +- `OpenAiEmbeddingModel.Config.DataLoader` -> `OpenAiEmbeddingModel.Config.Service`: Data-loader configuration was removed; use the unified embedding config and constructor. + +- `OpenAiEmbeddingModel.Config.Service` -> `OpenAiEmbeddingModel.Config.Service`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiEmbeddingModel.Model` -> `OpenAiEmbeddingModel.Model`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiEmbeddingModel.layerBatched` -> `OpenAiEmbeddingModel.layer`: The batched and data-loader layers were replaced by one embedding layer; pass the model and request config explicitly. + +- `OpenAiEmbeddingModel.layerDataLoader` -> `OpenAiEmbeddingModel.layer`: The batched and data-loader layers were replaced by one embedding layer; pass the model and request config explicitly. + +- `OpenAiEmbeddingModel.makeDataLoader` -> `OpenAiEmbeddingModel.make`: The dedicated data-loader constructor was removed; use the unified v4 embedding service constructor. + +### `@effect/ai-openai/OpenAiLanguageModel` + +- `OpenAiLanguageModel.Config` -> `OpenAiLanguageModel.Config`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiLanguageModel.Config.Service` -> `OpenAiLanguageModel.Config.Service`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiLanguageModel.Model` -> `OpenAiLanguageModel.Model`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiLanguageModel.ProviderMetadata` -> `Prompt.ProviderOptions / Response.ProviderMetadata`: The OpenAI metadata service wrapper was removed; v4 declares OpenAI-specific fields directly on Prompt and Response provider metadata. + +- `OpenAiLanguageModel.ProviderMetadata.Service` -> `Prompt.ProviderOptions / Response.ProviderMetadata`: The OpenAI metadata service wrapper was removed; v4 declares OpenAI-specific fields directly on Prompt and Response provider metadata. + +- `OpenAiLanguageModel.layerWithTokenizer` -> `OpenAiLanguageModel.layer`: The tokenizer-combining layer was removed; provide the language model and any Tokenizer service separately. + +- `OpenAiLanguageModel.modelWithTokenizer` -> `OpenAiLanguageModel.model`: The tokenizer-combining model was removed; use the v4 model descriptor and provide any Tokenizer service separately. + +### `@effect/ai-openai/OpenAiTelemetry` + +- `OpenAiTelemetry.AllAttributes` -> `OpenAiTelemetry.AllAttributes`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiTelemetry.OpenAiTelemetryAttributeOptions` -> `OpenAiTelemetry.OpenAiTelemetryAttributeOptions`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +- `OpenAiTelemetry.addGenAIAnnotations` -> `OpenAiTelemetry.addGenAIAnnotations`: Still exported in v4; update imports and adapt to the revised v4 service and schema types. + +### `@effect/ai-openai/OpenAiTokenizer` + +- `OpenAiTokenizer.layer` -> `Tokenizer.make`: The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using gpt-tokenizer if equivalent OpenAI counting is required. + +- `OpenAiTokenizer.make` -> `Tokenizer.make`: The provider-specific tokenizer module was removed; build and provide an effect/unstable/ai/Tokenizer service explicitly, using gpt-tokenizer if equivalent OpenAI counting is required. + +### `@effect/ai-openrouter/Generated` + +- `Generated.ActivityItem` -> `Generated.ActivityItem`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.AnnotationDetail` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequestProviderSort` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequestRoute` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequestServiceTier` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequestThinkingEnumType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesRequestToolChoiceEnumType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesResponse` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesResponseRole` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesResponseStopReason` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesResponseType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AnthropicMessagesResponseUsageServiceTier` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.AssistantMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BadGatewayResponse` -> `Generated.BadGatewayResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BadGatewayResponseErrorData` -> `Generated.BadGatewayResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BadRequestResponse` -> `Generated.BadRequestResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BadRequestResponseErrorData` -> `Generated.BadRequestResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BigNumberUnion` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BulkAssignKeysToGuardrail200` -> `Generated.BulkAssignKeysToGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BulkAssignKeysToGuardrailRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BulkAssignMembersToGuardrail200` -> `Generated.BulkAssignMembersToGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BulkAssignMembersToGuardrailRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BulkUnassignKeysFromGuardrail200` -> `Generated.BulkUnassignKeysFromGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BulkUnassignKeysFromGuardrailRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.BulkUnassignMembersFromGuardrail200` -> `Generated.BulkUnassignMembersFromGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.BulkUnassignMembersFromGuardrailRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CacheControlEphemeral` -> `Generated.ChatContentCacheControl`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatCompletionFinishReason` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatError` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatGenerationParams` -> `Generated.ChatRequest`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatGenerationParamsProviderEnumDataCollectionEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatGenerationParamsReasoningEffortEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatGenerationParamsRouteEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatGenerationTokenUsage` -> `Generated.ChatUsage`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageContentItem` -> `Generated.ChatContentItems`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageContentItemAudio` -> `Generated.ChatContentAudio`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageContentItemCacheControl` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatMessageContentItemCacheControlTtl` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatMessageContentItemImage` -> `Generated.ChatContentImage`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageContentItemImageImageUrlDetail` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatMessageContentItemText` -> `Generated.ChatContentText`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageContentItemVideo` -> `Generated.ChatContentVideo`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageTokenLogprob` -> `Generated.ChatTokenLogprob`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageTokenLogprobs` -> `Generated.ChatTokenLogprobs`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatMessageToolCall` -> `Generated.ChatToolCall`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatResponse` -> `Generated.ChatResult`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ChatResponseChoice` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ChatStreamOptions` -> `Generated.ChatStreamOptions`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Client` -> `Generated.OpenRouterClient`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ClientError` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionChoice` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionCreateParams` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionFinishReason` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionFinishReasonEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionLogprobs` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionResponse` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CompletionUsage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateAuthKeysCode200` -> `Generated.CreateAuthKeysCode200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateAuthKeysCodeRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateAuthKeysCodeRequestCodeChallengeMethod` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChargeRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateChargeRequestChainId` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateCoinbaseCharge200` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddings200` -> `Generated.CreateEmbeddings200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateEmbeddings200Object` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddingsRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateEmbeddingsRequestEncodingFormat` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateGuardrail201` -> `Generated.CreateGuardrail201`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateGuardrail201DataResetInterval` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateGuardrailRequest` -> `Generated.CreateGuardrailRequest`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateGuardrailRequestResetInterval` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateKeys201` -> `Generated.CreateKeys201`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateKeysRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateKeysRequestLimitReset` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages400` -> `Generated.CreateMessages400`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages400Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages401` -> `Generated.CreateMessages401`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages401Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages403` -> `Generated.CreateMessages403`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages403Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages404` -> `Generated.CreateMessages404`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages404Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages429` -> `Generated.CreateMessages429`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages429Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages500` -> `Generated.CreateMessages500`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages500Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages503` -> `Generated.CreateMessages503`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages503Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.CreateMessages529` -> `Generated.CreateMessages529`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.CreateMessages529Type` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DataCollection` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.DefaultParameters` -> `Generated.DefaultParameters`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteGuardrail200` -> `Generated.DeleteGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.DeleteKeys200` -> `Generated.DeleteKeys200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EdgeNetworkTimeoutResponse` -> `Generated.EdgeNetworkTimeoutResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EdgeNetworkTimeoutResponseErrorData` -> `Generated.EdgeNetworkTimeoutResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.EndpointStatus` -> `Generated.EndpointStatus`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ExchangeAuthCodeForAPIKey200` -> `Generated.ExchangeAuthCodeForAPIKey200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ExchangeAuthCodeForAPIKeyRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ExchangeAuthCodeForAPIKeyRequestCodeChallengeMethod` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileAnnotationDetail` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FileCitation` -> `Generated.FileCitation`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FileCitationType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.FilePath` -> `Generated.FilePath`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.FilePathType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ForbiddenResponse` -> `Generated.ForbiddenResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ForbiddenResponseErrorData` -> `Generated.ForbiddenResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetCredits200` -> `Generated.GetCredits200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetCurrentKey200` -> `Generated.GetCurrentKey200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetGeneration200` -> `Generated.GetGeneration200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetGeneration200DataApiType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetGenerationParams` -> `Generated.GetGenerationParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetGuardrail200` -> `Generated.GetGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetGuardrail200DataResetInterval` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetKey200` -> `Generated.GetKey200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetModelsParams` -> `Generated.GetModelsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetModelsParamsCategory` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.GetUserActivity200` -> `Generated.GetUserActivity200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.GetUserActivityParams` -> `Generated.GetUserActivityParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ImageGenerationStatus` -> `Generated.ImageGenerationStatus`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InputModality` -> `Generated.InputModality`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InternalServerResponse` -> `Generated.InternalServerResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.InternalServerResponseErrorData` -> `Generated.InternalServerResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.JSONSchemaConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.List200` -> `Generated.List200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListEndpoints200` -> `Generated.ListEndpoints200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListEndpointsResponse` -> `Generated.ListEndpointsResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListEndpointsResponseArchitecture` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListEndpointsResponseArchitectureEnumInstructType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ListEndpointsZdr200` -> `Generated.ListEndpointsZdr200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrailKeyAssignments200` -> `Generated.ListGuardrailKeyAssignments200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrailKeyAssignmentsParams` -> `Generated.ListGuardrailKeyAssignmentsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrailMemberAssignments200` -> `Generated.ListGuardrailMemberAssignments200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrailMemberAssignmentsParams` -> `Generated.ListGuardrailMemberAssignmentsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrails200` -> `Generated.ListGuardrails200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListGuardrailsParams` -> `Generated.ListGuardrailsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListKeyAssignments200` -> `Generated.ListKeyAssignments200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListKeyAssignmentsParams` -> `Generated.ListKeyAssignmentsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListMemberAssignments200` -> `Generated.ListMemberAssignments200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListMemberAssignmentsParams` -> `Generated.ListMemberAssignmentsParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListParams` -> `Generated.ListParams`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ListProviders200` -> `Generated.ListProviders200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Message` -> `Generated.ChatMessages`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.Model` -> `Generated.Model`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelArchitecture` -> `Generated.ModelArchitecture`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelArchitectureInstructType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ModelGroup` -> `Generated.ModelGroup`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelName` -> `Generated.ModelName`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelsCountResponse` -> `Generated.ModelsCountResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelsListResponse` -> `Generated.ModelsListResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ModelsListResponseData` -> `Generated.ModelsListResponseData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.NamedToolChoice` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.NotFoundResponse` -> `Generated.NotFoundResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.NotFoundResponseErrorData` -> `Generated.NotFoundResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesAnnotation` -> `Generated.OpenAIResponsesAnnotation`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesIncludable` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesIncompleteDetails` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesIncompleteDetailsReason` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesInput` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesPrompt` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesReasoningConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesReasoningEffort` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesRefusalContent` -> `Generated.OpenAIResponsesRefusalContent`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesRefusalContentType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesResponseStatus` -> `Generated.OpenAIResponsesResponseStatus`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesServiceTier` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesToolChoice` -> `Generated.OpenAIResponsesToolChoice`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesToolChoiceEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesToolChoiceEnumType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesToolChoiceEnumTypeEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenAIResponsesTruncation` -> `Generated.OpenAIResponsesTruncation`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenAIResponsesUsage` -> `Generated.OpenAIResponsesUsage`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OpenResponsesEasyInputMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesEasyInputMessageRoleEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesEasyInputMessageType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesFunctionCallOutput` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesFunctionCallOutputType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesFunctionToolCall` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesFunctionToolCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesInput` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesInputMessageItem` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesInputMessageItemRoleEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesInputMessageItemType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesNonStreamingResponse` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesNonStreamingResponseObject` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesReasoning` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesReasoningConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesReasoningFormat` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesReasoningStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesReasoningType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequestMetadata` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequestRoute` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequestServiceTier` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequestTruncation` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesRequestTruncationEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesResponseText` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesResponseTextVerbosity` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearch20250826Tool` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearch20250826ToolType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchPreview20250311Tool` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchPreview20250311ToolType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchPreviewTool` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchPreviewToolType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchTool` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenResponsesWebSearchToolType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenRouterAnthropicMessageParam` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OpenRouterAnthropicMessageParamRole` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemFileSearchCall` -> `Generated.OutputItemFileSearchCall`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputItemFileSearchCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemFunctionCall` -> `Generated.OutputItemFunctionCall`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputItemFunctionCallStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemFunctionCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemImageGenerationCall` -> `Generated.OutputItemImageGenerationCall`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputItemImageGenerationCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemReasoning` -> `Generated.OutputItemReasoning`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputItemReasoningStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemReasoningType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputItemWebSearchCall` -> `Generated.OutputItemWebSearchCall`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputItemWebSearchCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputMessage` -> `Generated.OutputMessage`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.OutputMessageRole` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputMessageStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputMessageType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.OutputModality` -> `Generated.OutputModality`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PDFParserEngine` -> `Generated.PDFParserEngine`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PDFParserOptions` -> `Generated.PDFParserOptions`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Parameter` -> `Generated.Parameter`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PayloadTooLargeResponse` -> `Generated.PayloadTooLargeResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PayloadTooLargeResponseErrorData` -> `Generated.PayloadTooLargeResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PaymentRequiredResponse` -> `Generated.PaymentRequiredResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PaymentRequiredResponseErrorData` -> `Generated.PaymentRequiredResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PerRequestLimits` -> `Generated.PerRequestLimits`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PercentileLatencyCutoffs` -> `Generated.PercentileLatencyCutoffs`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PercentileStats` -> `Generated.PercentileStats`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PercentileThroughputCutoffs` -> `Generated.PercentileThroughputCutoffs`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PreferredMaxLatency` -> `Generated.PreferredMaxLatency`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PreferredMinThroughput` -> `Generated.PreferredMinThroughput`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderName` -> `Generated.ProviderName`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderOverloadedResponse` -> `Generated.ProviderOverloadedResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderOverloadedResponseErrorData` -> `Generated.ProviderOverloadedResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderPreferences` -> `Generated.ProviderPreferences`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderPreferencesSort` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProviderSort` -> `Generated.ProviderSort`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderSortConfig` -> `Generated.ProviderSortConfig`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ProviderSortConfigPartitionEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ProviderSortUnion` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.PublicEndpoint` -> `Generated.PublicEndpoint`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.PublicEndpointQuantization` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.PublicEndpointQuantizationEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.PublicEndpointThroughputLast30M` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.PublicPricing` -> `Generated.PublicPricing`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.Quantization` -> `Generated.Quantization`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningDetail` -> `Generated.ReasoningDetailUnion`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ReasoningDetailEncrypted` -> `Generated.ReasoningDetailEncrypted`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningDetailSummary` -> `Generated.ReasoningDetailSummary`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningDetailText` -> `Generated.ReasoningDetailText`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningSummaryText` -> `Generated.ReasoningSummaryText`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningSummaryTextType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ReasoningSummaryVerbosity` -> `Generated.ReasoningSummaryVerbosity`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningTextContent` -> `Generated.ReasoningTextContent`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ReasoningTextContentType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.RequestTimeoutResponse` -> `Generated.RequestTimeoutResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.RequestTimeoutResponseErrorData` -> `Generated.RequestTimeoutResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseFormatJSONSchema` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseFormatTextConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseFormatTextGrammar` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputAudio` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputAudioInputAudioFormat` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputAudioType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputFile` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputFileType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputImage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputImageDetail` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputImageType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputText` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputTextType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputVideo` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseInputVideoType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseOutputText` -> `Generated.ResponseOutputText`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponseOutputTextType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseTextConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponseTextConfigVerbosity` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesErrorField` -> `Generated.ResponsesErrorField`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ResponsesErrorFieldCode` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatJSONObject` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatJSONObjectType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatText` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatTextJSONSchemaConfig` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatTextJSONSchemaConfigType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesFormatTextType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesImageGenerationCall` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesImageGenerationCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemFileSearchCall` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemFileSearchCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemFunctionCall` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemFunctionCallStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemFunctionCallType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemReasoning` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemReasoningFormat` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemReasoningStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputItemReasoningType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputMessageRole` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputMessageStatusEnum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputMessageType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesOutputModality` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesSearchContextSize` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesWebSearchCallOutput` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesWebSearchCallOutputType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesWebSearchUserLocation` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ResponsesWebSearchUserLocationType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema0` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema1` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema2` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema3` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema4` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema4Enum` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema5` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.Schema6` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.ServiceUnavailableResponse` -> `Generated.ServiceUnavailableResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ServiceUnavailableResponseErrorData` -> `Generated.ServiceUnavailableResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.SystemMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TooManyRequestsResponse` -> `Generated.TooManyRequestsResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.TooManyRequestsResponseErrorData` -> `Generated.TooManyRequestsResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolCallStatus` -> `Generated.ToolCallStatus`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.ToolChoiceOption` -> `Generated.ChatToolChoice`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ToolDefinitionJson` -> `Generated.ChatFunctionTool`: Renamed when the OpenRouter client was regenerated from the current specification; re-check the replacement schema's Type/Encoded shape. + +- `Generated.ToolResponseMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.TopProviderInfo` -> `Generated.TopProviderInfo`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.URLCitation` -> `Generated.URLCitation`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.URLCitationAnnotationDetail` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.URLCitationType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UnauthorizedResponse` -> `Generated.UnauthorizedResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UnauthorizedResponseErrorData` -> `Generated.UnauthorizedResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UnprocessableEntityResponse` -> `Generated.UnprocessableEntityResponse`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UnprocessableEntityResponseErrorData` -> `Generated.UnprocessableEntityResponseErrorData`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateGuardrail200` -> `Generated.UpdateGuardrail200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateGuardrail200DataResetInterval` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateGuardrailRequest` -> `Generated.UpdateGuardrailRequest`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateGuardrailRequestResetInterval` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateKeys200` -> `Generated.UpdateKeys200`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.UpdateKeysRequest` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UpdateKeysRequestLimitReset` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.UserMessage` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchEngine` -> `Generated.WebSearchEngine`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.WebSearchPreviewToolUserLocation` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchPreviewToolUserLocationType` -> `none`: Removed when the OpenRouter specification client was regenerated; use the current Generated request/response schema for the relevant endpoint instead of this old snapshot helper. + +- `Generated.WebSearchStatus` -> `Generated.WebSearchStatus`: Still generated in v4 from the current OpenRouter specification; re-check the schema's Type/Encoded shape because the generated definition changed. + +- `Generated.make` -> `Generated.make`: Still exported in v4, but the regenerated OpenRouter client has different operations and request/response schemas; update call sites to the current generated service. + +### `@effect/ai-openrouter/OpenRouterClient` + +- `OpenRouterClient.ChatStreamingChoice` -> `Generated.ChatStreamChoice`: The client-local streaming choice schema moved into the regenerated OpenRouter schema surface and changed shape. + +- `OpenRouterClient.ChatStreamingMessageChunk` -> `Generated.ChatStreamDelta`: The client-local streaming message delta moved into the regenerated OpenRouter schema surface and changed shape. + +- `OpenRouterClient.ChatStreamingMessageToolCall` -> `Generated.ChatStreamToolCall`: The client-local streaming tool-call delta moved into the regenerated OpenRouter schema surface and changed shape. + +- `OpenRouterClient.ChatStreamingResponseChunk` -> `OpenRouterClient.ChatStreamingResponseChunkData`: The standalone streaming chunk schema was replaced by the decoded data type from Generated.ChatStreamingResponse. + +- `OpenRouterClient.Service` -> `OpenRouterClient.Service`: Still exported in v4; adapt to the regenerated client, revised request and response schemas, and the new streaming result tuple. + +### `@effect/ai-openrouter/OpenRouterConfig` + +- `OpenRouterConfig.OpenRouterConfig` -> `OpenRouterConfig.OpenRouterConfig`: Still exported in v4; update imports to the v4 package and use the revised Context.Service-based configuration service. + +- `OpenRouterConfig.OpenRouterConfig.Service` -> `OpenRouterConfig.OpenRouterConfig.Service`: Still exported in v4; update imports to the v4 package and use the revised Context.Service-based configuration service. + +### `@effect/ai-openrouter/OpenRouterLanguageModel` + +- `OpenRouterLanguageModel.Config` -> `OpenRouterLanguageModel.Config`: Still exported in v4; update imports and adapt to the regenerated chat request schema and revised Context.Service configuration. + +- `OpenRouterLanguageModel.Config.Service` -> `OpenRouterLanguageModel.Config.Service`: Still exported in v4; update imports and adapt to the regenerated chat request schema and revised Context.Service configuration. + +- `OpenRouterLanguageModel.OpenRouterReasoningInfo` -> `OpenRouterLanguageModel.ReasoningDetails`: The bespoke reasoning-info union was replaced by the provider's raw reasoning-details array, preserved through Prompt options and Response metadata. + +### `@effect/ai/AiError` + +- `AiError.AiError` -> `AiError.AiError`: Moved to effect/unstable/ai/AiError and redesigned from a union of separately tagged errors into one AiError wrapper with a semantic reason. Construct it with AiError.make({ module, method, reason }) and match error.reason rather than the old top-level tags. + +- `AiError.HttpRequestError` -> `AiError.make + AiError.NetworkError`: Replace the old top-level request error with an AiError whose reason is NetworkError. NetworkError.fromRequestError converts a v4 HttpClientError.RequestError. + +- `AiError.HttpResponseError` -> `AiError.make + AiError.reasonFromHttpStatus / AiError.InvalidOutputError`: There is no single v4 response-error class. Wrap a semantic reason with AiError.make: use reasonFromHttpStatus for status failures and InvalidOutputError for decode or empty-body failures. + +- `AiError.MalformedInput` -> `AiError.make + AiError.InvalidUserInputError`: Replace the old top-level input error with an AiError whose reason is InvalidUserInputError. Use InvalidRequestError when the provider request parameters themselves are malformed. + +- `AiError.MalformedOutput` -> `AiError.make + AiError.InvalidOutputError`: Replace the old top-level output error with an AiError whose reason is InvalidOutputError. The old fromParseError helper becomes InvalidOutputError.fromSchemaError. + +- `AiError.TypeId` -> `AiError.isAiError`: The AiError brand is private in v4. Use isAiError for runtime narrowing, or isAiErrorReason for a reason, instead of inspecting or constructing the type id. + +- `AiError.UnknownError` -> `AiError.make + AiError.UnknownError`: UnknownError is now a semantic reason rather than a top-level error. Put module and method on AiError.make and inspect reason.\_tag when handling the outer AiError. + +### `@effect/ai/EmbeddingModel` + +- `EmbeddingModel.makeDataLoader` -> `EmbeddingModel.make + RequestResolver.setDelay + RequestResolver.batchN`: The dedicated data-loader constructor was removed. EmbeddingModel.make batches concurrent embed requests through its resolver; compose the exposed resolver with setDelay and optional batchN for the old window and maximum-batch behavior. + +### `@effect/ai/IdGenerator` + +- `IdGenerator.make` -> `IdGenerator.make`: Moved to effect/unstable/ai/IdGenerator with the same configurable alphabet, prefix, separator, and size behavior. Invalid configuration now fails with Cause.IllegalArgumentError. + +### `@effect/ai/LanguageModel` + +- `LanguageModel.ConstructorParams` -> `none`: V4 inlines this provider-adapter shape in LanguageModel.make. Pass generateText and streamText directly to make, with optional codecTransformer, instead of naming a constructor-parameter type. + +- `LanguageModel.ExtractContext` -> `LanguageModel.ExtractServices`: Renamed in effect/unstable/ai/LanguageModel. ExtractServices infers toolkit handler, result-decoding, and effectful-toolkit service requirements. + +### `@effect/ai/McpSchema` + +- `McpSchema.ContentBlock` -> `McpSchema.ContentBlock`: Moved to effect/unstable/ai/McpSchema. It remains the MCP content-block union, but v4 exports it as a const schema rather than a Schema.Union subclass. + +- `McpSchema.McpError` -> `McpSchema.McpError`: Moved, but changed from a constructable base class to a union schema of standard tagged protocol errors plus McpErrorBase. Use McpErrorBase to construct a generic MCP error. + +- `McpSchema.ParamAnnotation` -> `McpSchema.isParam / Param.name`: The public symbol annotation was removed. Detect parameter wrappers with McpSchema.isParam and read the narrowed Param.name instead of inspecting AST annotations. + +- `McpSchema.param` -> `McpSchema.param`: Moved to effect/unstable/ai/McpSchema. V4 wraps the schema and exposes Param.name and Param.schema instead of attaching a public symbol annotation. + +### `@effect/ai/McpServer` + +- `McpServer.layer` -> `McpServer.layer`: Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025\_06\_18], imported with McpProtocol from effect/unstable/ai; it still runs over a caller-provided RpcServer.Protocol. + +- `McpServer.layerHttp` -> `McpServer.layerHttp`: Moved to effect/unstable/ai/McpServer and the unified HttpRouter. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025\_06\_18], imported with McpProtocol from effect/unstable/ai. + +- `McpServer.layerHttpRouter` -> `McpServer.layerHttp`: Renamed and consolidated. V4 layerHttp registers the Streamable HTTP endpoint in the unified HttpRouter; pass a non-empty protocols array of adapters, such as [McpProtocol.v2025\_06\_18], imported with McpProtocol from effect/unstable/ai. + +- `McpServer.layerStdio` -> `McpServer.layerStdio`: Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025\_06\_18], imported with McpProtocol from effect/unstable/ai. + +- `McpServer.run` -> `McpServer.run`: Moved to effect/unstable/ai/McpServer. Pass a non-empty protocols array of adapters, such as [McpProtocol.v2025\_06\_18], imported with McpProtocol from effect/unstable/ai; it remains the Effect-level runner over RpcServer.Protocol. + +### `@effect/ai/Model` + +- `Model.TypeId` -> `none`: The Model brand still exists internally, but its TypeId is not exported and v4 has no public isModel guard. Use Model values created by Model.make rather than inspecting or constructing the brand. + +### `@effect/ai/Prompt` + +- `Prompt.FilePart` -> `Prompt.FilePart`: Moved to effect/unstable/ai/Prompt with the same file-part model and schema; update the module import. + +- `Prompt.FromJson` -> `Schema.fromJsonString(Prompt.Prompt)`: The module-specific JSON schema was removed. Compose the general v4 JSON-string codec with the public Prompt codec. + +- `Prompt.MessageContentFromString` -> `Prompt.ContentFromString`: Renamed in effect/unstable/ai/Prompt. It still decodes a string to a non-empty array containing one TextPart and encodes the first part's text. + +- `Prompt.MessageTypeId` -> `Prompt.isMessage`: The message type id is private in v4. Use the public isMessage guard for runtime refinement instead of importing or inspecting the marker. + +- `Prompt.Part` -> `Prompt.Part`: Moved to effect/unstable/ai/Prompt. The union now also includes tool-approval request and response parts. + +- `Prompt.PartEncoded` -> `Prompt.PartEncoded`: Moved to effect/unstable/ai/Prompt. The encoded union now also includes tool-approval request and response parts. + +- `Prompt.PartTypeId` -> `Prompt.isPart`: The part type id is private in v4. Use the public isPart guard for runtime refinement instead of importing or inspecting the marker. + +- `Prompt.PromptFromSelf` -> `Prompt.Prompt`: The standalone declared from-self schema was removed. Use the public Prompt codec for prompt validation and encoding, or Prompt.isPrompt when only runtime refinement is needed. + +- `Prompt.ReasoningPart` -> `Prompt.ReasoningPart`: Moved to effect/unstable/ai/Prompt with the same reasoning-part model and schema; update the module import. + +- `Prompt.TextPart` -> `Prompt.TextPart`: Moved to effect/unstable/ai/Prompt with the same text-part model and schema; update the module import. + +- `Prompt.ToolCallPart` -> `Prompt.ToolCallPart`: Moved to effect/unstable/ai/Prompt with the same tool-call model and schema; update the module import. + +- `Prompt.ToolResultPartEncoded` -> `Prompt.ToolResultPartEncoded`: Moved to effect/unstable/ai/Prompt, but providerExecuted was removed from the encoded prompt tool-result shape. + +- `Prompt.TypeId` -> `Prompt.isPrompt`: The Prompt type id is private in v4 and its internal literal changed. Use the public isPrompt guard instead of importing or inspecting the marker. + +- `Prompt.makePart` -> `Prompt.makePart`: Moved to effect/unstable/ai/Prompt. The generic constructor also supports the new tool-approval request and response part variants. + +- `Prompt.merge` -> `Prompt.concat`: Renamed in v4. concat preserves the old dual API and concatenates the messages from a Prompt with additional raw input. + +### `@effect/ai/Response` + +- `Response.Part` -> `Response.Part`: Moved to effect/unstable/ai/Response. The non-streaming union now also includes ToolApprovalRequestPart. + +- `Response.PartTypeId` -> `Response.isPart`: The public PartTypeId was removed and the marker is internal in v4. Use Response.isPart for runtime refinement. + +- `Response.ToolCallPartEncoded` -> `Response.ToolCallPartEncoded`: Moved to effect/unstable/ai/Response; providerName was removed while providerExecuted remains optional when encoded. + +- `Response.ToolResultPartEncoded` -> `Response.ToolResultPartEncoded`: Moved to effect/unstable/ai/Response; providerName was removed and optional preliminary was added to the encoded shape. + +- `Response.documentSourcePart` -> `Response.makePart("source", { ...params, sourceType: "document" })`: The lowercase convenience constructor was removed. The DocumentSourcePart model remains, and the generic constructor now requires the document source discriminator. + +- `Response.errorPart` -> `Response.makePart("error", params)`: The lowercase convenience constructor was removed; construct the retained error part through Response.makePart. + +- `Response.finishPart` -> `Response.makePart("finish", params)`: The lowercase convenience constructor was removed. V4 Usage has nested inputTokens and outputTokens objects, and FinishPart adds optional HTTP response details. + +- `Response.reasoningDeltaPart` -> `Response.makePart("reasoning-delta", params)`: The lowercase convenience constructor was removed; construct the retained ReasoningDeltaPart through Response.makePart. + +- `Response.reasoningEndPart` -> `Response.makePart("reasoning-end", params)`: The lowercase convenience constructor was removed; construct the retained ReasoningEndPart through Response.makePart. + +- `Response.reasoningStartPart` -> `Response.makePart("reasoning-start", params)`: The lowercase convenience constructor was removed; construct the retained ReasoningStartPart through Response.makePart. + +- `Response.responseMetadataPart` -> `Response.makePart("response-metadata", params)`: The lowercase convenience constructor was removed. V4 id, modelId, and timestamp are optional raw values rather than Option values, and optional HTTP request details were added. + +- `Response.textDeltaPart` -> `Response.makePart("text-delta", params)`: The lowercase convenience constructor was removed; construct the retained TextDeltaPart through Response.makePart. + +- `Response.textEndPart` -> `Response.makePart("text-end", params)`: The lowercase convenience constructor was removed; construct the retained TextEndPart through Response.makePart. + +- `Response.textStartPart` -> `Response.makePart("text-start", params)`: The lowercase convenience constructor was removed; construct the retained TextStartPart through Response.makePart. + +- `Response.toolParamsDeltaPart` -> `Response.makePart("tool-params-delta", params)`: The lowercase convenience constructor was removed; construct the retained ToolParamsDeltaPart through Response.makePart. + +- `Response.toolParamsEndPart` -> `Response.makePart("tool-params-end", params)`: The lowercase convenience constructor was removed; construct the retained ToolParamsEndPart through Response.makePart. + +- `Response.toolParamsStartPart` -> `Response.makePart("tool-params-start", params)`: The lowercase convenience constructor was removed; providerName was also removed from ToolParamsStartPart in v4. + +- `Response.toolResultPart` -> `Response.toolResultPart`: Moved to effect/unstable/ai/Response; providerName was removed and decoded tool results now require preliminary, normally false. + +- `Response.urlSourcePart` -> `Response.makePart("source", { ...params, sourceType: "url" })`: The lowercase convenience constructor was removed. The UrlSourcePart model remains, and the generic constructor now requires the URL source discriminator. + +### `@effect/ai/Tool` + +- `Tool.AnyParametersSchema` -> `Schema.Constraint`: The AI-specific alias was removed. V4 Tool parameter schemas use the general Schema.Constraint type and are no longer restricted to the old struct-or-EmptyParams union. + +- `Tool.AnyTaggedRequestSchema` -> `none`: The TaggedRequest-specific Tool adapter contract was removed. Model the operation directly with Tool.make and ordinary v4 Schema.Constraint values. + +- `Tool.Destructive` -> `Tool.Destructive`: Moved to effect/unstable/ai/Tool. It is now a Context.Reference\ value rather than a Reference subclass; its default remains true. + +- `Tool.Failure` -> `Tool.Failure`: Moved to effect/unstable/ai/Tool and remains the utility type that extracts a tool's decoded failure type. + +- `Tool.FromTaggedRequest` -> `Tool.Tool`: The dedicated derived alias was removed. Construct with Tool.make and let Tool.Tool infer the name, parameter, success, and failure schemas. + +- `Tool.Idempotent` -> `Tool.Idempotent`: Moved to effect/unstable/ai/Tool. It is now a Context.Reference\ value rather than a Reference subclass; its default remains false. + +- `Tool.OpenWorld` -> `Tool.OpenWorld`: Moved to effect/unstable/ai/Tool. It is now a Context.Reference\ value rather than a Reference subclass; its default remains true. + +- `Tool.ProviderDefinedTypeId` -> `Tool.ProviderDefinedTypeId`: Moved to effect/unstable/ai/Tool and remains public. Its literal changed, so use the export rather than retaining the old hard-coded string. + +- `Tool.Readonly` -> `Tool.Readonly`: Moved to effect/unstable/ai/Tool. It is now a Context.Reference\ value rather than a Reference subclass; its default remains false. + +- `Tool.Requirements` -> `Tool.HandlerServices`: Renamed and refined. HandlerServices combines parameter-decoding, result-encoding, and request-level dependencies required by a tool handler. + +- `Tool.Success` -> `Tool.Success`: Moved to effect/unstable/ai/Tool and remains the utility type that extracts a tool's decoded success type. + +- `Tool.Tool.ProviderDefinedProto` -> `Tool.ProviderDefined`: This implementation-brand interface is no longer public. Use Tool.ProviderDefined for the model type and Tool.isProviderDefined for runtime narrowing. + +- `Tool.Tool.Variance` -> `Tool.Tool / Tool.Any`: This implementation variance interface is no longer public; its requirement marker is inline in Tool.Tool. Constrain generic code with Tool.Tool or Tool.Any. + +- `Tool.Tool.VarianceStruct` -> `Tool.Tool / Tool.Any`: This implementation variance payload is no longer public; the requirements marker is inline in Tool.Tool and should not be named independently. + +- `Tool.TypeId` -> `Tool.TypeId`: Moved to effect/unstable/ai/Tool and remains public. Its literal changed, so use the export rather than retaining the old hard-coded string. + +- `Tool.fromTaggedRequest` -> `Tool.make`: The adapter was removed. Rebuild the tool explicitly with Tool.make(name, { parameters, success, failure }); Toolkit.make no longer converts schema values automatically. + +- `Tool.getDescriptionFromSchemaAst` -> `SchemaAST.resolveDescription`: Moved out of Tool to the general v4 AST annotation resolver. For a Tool value, prefer Tool.getDescription. + +- `Tool.getJsonSchemaFromSchemaAst` -> `Tool.getJsonSchemaFromSchema`: Renamed to accept a Schema.Constraint instead of a raw AST and now emits the v4 JSON Schema model. Wrap a raw AST with Schema.make first. + +### `@effect/ai/Toolkit` + +- `Toolkit.HandlersFrom` -> `Toolkit.HandlersFrom`: Moved to effect/unstable/ai/Toolkit. V4 handlers receive a HandlerContext argument and may fail with the declared failure, AiError, or AiErrorReason while requiring Tool.HandlerServices. + +- `Toolkit.TypeId` -> `Toolkit.Toolkit / Toolkit.Any`: The toolkit nominal id is private in v4. Use Toolkit.Toolkit or Toolkit.Any for typing instead of importing or inspecting the marker. + +### `@effect/cli/Args` + +- `Args.All.ArgsAny` -> `Param.AnyArgument`: Use the shared any-positional-parameter type. + +- `Args.All.Return` -> `Command.Command.Config.Infer`: Infer the output of a command config record; standalone argument collections were removed. + +- `Args.Args` -> `Argument.Argument`: Args was renamed to Argument in effect/unstable/cli. + +- `Args.Args.BaseArgsConfig` -> `name: string`: Argument constructors now take the name as a required first parameter. + +- `Args.Args.FormatArgsConfig` -> `Primitive.FileParseOptions`: Pass the name separately and use the format option with Argument.fileParse or Argument.fileSchema. + +- `Args.Args.PathArgsConfig` -> `Argument.path(name, { pathType, mustExist })`: Path options are inline; map exists=yes to mustExist=true and either to omission. exists=no has no exact replacement. + +- `Args.Args.Variance` -> `Argument.Argument`: The separate variance artifact was removed; Argument inherits the shared Param variance. + +- `Args.ArgsTypeId` -> `Param.isParam(value) && value.kind === Param.argumentKind`: The public Args type id was removed; use the Param guard and argument kind discriminator. + +- `Args.all` -> `Command.make(name, config)`: Collect arguments in the config record passed to Command.make; there is no standalone Argument.all. + +- `Args.atLeast` -> `Argument.atLeast`: Use the moved combinator; v4 returns ReadonlyArray and does not encode non-emptiness in the type. + +- `Args.atMost` -> `Argument.atMost`: Use the moved combinator. + +- `Args.between` -> `Argument.between`: Use the moved combinator; v4 validates bounds when constructing the parameter. + +- `Args.boolean` -> `Flag.boolean / Argument.choiceWithValue`: Positional booleans were removed as ambiguous; prefer a boolean flag or explicit true/false positional choices. + +- `Args.fileContent` -> `Argument.file + Argument.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content argument constructor remains. + +- `Args.getHelp` -> `none`: Per-argument help introspection was removed; Command generates help internally. + +- `Args.getIdentifier` -> `none`: Public argument identifier introspection was removed. + +- `Args.getMaxSize` -> `none`: Public arity introspection was removed; command parsing enforces variadic bounds internally. + +- `Args.getMinSize` -> `none`: Public arity introspection was removed; command parsing enforces variadic bounds internally. + +- `Args.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. + +- `Args.isArgs` -> `Param.isParam(value) && value.kind === Param.argumentKind`: Arguments now use the shared Param representation and an explicit kind discriminator. + +- `Args.map` -> `Argument.map`: Use the moved combinator. + +- `Args.optional` -> `Argument.optional`: Use the moved combinator; it still returns Option. + +- `Args.repeated` -> `Argument.variadic`: Renamed to variadic; pass optional min and max bounds. + +- `Args.secret` -> `Argument.redacted`: Use Redacted-backed positional input. + +- `Args.text` -> `Argument.string`: Renamed to string; pass the argument name explicitly. + +- `Args.validate` -> `argument.parse({ flags: {}, arguments: args })`: Parsing is now a Param method and returns leftover tokens with the value; errors are CliError. + +- `Args.withDefault` -> `Argument.withDefault`: Use the moved combinator; v4 also accepts an Effect fallback. + +- `Args.withDescription` -> `Argument.withDescription`: Use the moved combinator. + +- `Args.withFallbackConfig` -> `Argument.withFallbackConfig`: Use the moved combinator; invalid configuration becomes CliError.InvalidValue. + +- `Args.withSchema` -> `Argument.withSchema`: Use the moved combinator with a v4 Schema constraint decoder. + +### `@effect/cli/BuiltInOptions` + +- `BuiltInOptions.BuiltInOptions` -> `GlobalFlag.BuiltIn`: The parsed directive union became a union of global Action and Setting definitions. + +- `BuiltInOptions.BuiltInOptions.ShellType` -> `Completions.Shell`: The shell union moved to Completions. + +- `BuiltInOptions.SetLogLevel` -> `GlobalFlag.LogLevel`: Log level is now a global Setting whose parsed value is provided through context. + +- `BuiltInOptions.ShowCompletions` -> `GlobalFlag.Completions`: The directive payload was replaced by a global completion action definition. + +- `BuiltInOptions.ShowHelp` -> `GlobalFlag.Help`: The directive payload was replaced by a global help action definition. + +- `BuiltInOptions.ShowVersion` -> `GlobalFlag.Version`: The directive value was replaced by a global version action definition. + +- `BuiltInOptions.ShowWizard` -> `GlobalFlag.Wizard`: The directive payload was replaced by a global wizard action definition. + +- `BuiltInOptions.builtInOptions` -> `GlobalFlag.BuiltIns`: Built-ins are global flag definitions consumed automatically by Command.run and Command.runWith. + +- `BuiltInOptions.isShowCompletions` -> `none`: Parsed ShowCompletions directives were removed; the runner processes GlobalFlag.Completions directly. + +- `BuiltInOptions.isShowHelp` -> `none`: Parsed ShowHelp directives were removed; the runner processes GlobalFlag.Help directly. + +- `BuiltInOptions.isShowVersion` -> `none`: Parsed ShowVersion directives were removed; the runner processes GlobalFlag.Version directly. + +- `BuiltInOptions.isShowWizard` -> `none`: Parsed ShowWizard directives were removed; the runner processes GlobalFlag.Wizard directly. + +- `BuiltInOptions.showCompletions` -> `GlobalFlag.Completions`: Use the built-in completion action; the shell is parsed from --completions. + +- `BuiltInOptions.showHelp` -> `GlobalFlag.Help`: Use the built-in help action; usage and help are derived from the active Command. + +- `BuiltInOptions.showVersion` -> `GlobalFlag.Version`: Use the built-in version action; supply the version to Command.run or Command.runWith. + +- `BuiltInOptions.showWizard` -> `GlobalFlag.Wizard`: Use the built-in wizard action; runner context supplies the active Command. + +### `@effect/cli/CliApp` + +- `CliApp.CliApp` -> `Command.Command`: The separate application wrapper was folded into the runnable v4 Command tree. + +- `CliApp.CliApp.ConstructorArgs` -> `none`: Build the command with Command.make and withDescription, then pass version to Command.run; the old app constructor shape was removed. + +- `CliApp.make` -> `Command.make`: Build the executable Command directly; there is no separate CliApp wrapper. + +- `CliApp.run` -> `Command.run`: The CliApp wrapper was removed. Attach the execute function with Command.withHandler, then run the Command with its version; v4 reads arguments through the CLI environment instead of accepting args and execute at this call. + +### `@effect/cli/CliConfig` + +- `CliConfig.CliConfig` -> `CliConfig.CliConfig.Service`: The service was redesigned to configure built-in global flags; old parser and help switches were removed. + +- `CliConfig.defaultConfig` -> `CliConfig.defaults`: Renamed to defaults with the redesigned service shape. + +- `CliConfig.defaultLayer` -> `CliConfig.layer`: Call CliConfig.layer() to provide the defaults. + +- `CliConfig.layer` -> `CliConfig.layer`: The layer constructor remains, but its options configure the redesigned CliConfig.Service for built-in global flags. + +- `CliConfig.make` -> `CliConfig.make`: The constructor remains but accepts the redesigned service options. + +- `CliConfig.normalizeCase` -> `none`: Case normalization is no longer configurable through CliConfig. + +### `@effect/cli/Command` + +- `Command.Command.Context` -> `Command.CommandContext`: Renamed to CommandContext. + +- `Command.Command.ParseConfig` -> `Command.Command.Config.Infer`: Use the v4 command-config inference helper. + +- `Command.Command.ParseConfigValue` -> `Command.Command.Config.InferValue`: Use the v4 command-config value inference helper. + +- `Command.Command.ParsedConfig` -> `none`: The parsed config representation is internal in v4. + +- `Command.Command.ParsedConfigNode` -> `none`: The parsed config node representation is internal in v4. + +- `Command.Command.ParsedConfigTree` -> `none`: The parsed config tree representation is internal in v4. + +- `Command.Command.Transform` -> `none`: The handler transformation type and machinery are internal in v4. + +- `Command.TypeId` -> `Command.isCommand`: The type id is internal in v4; use the public runtime guard. + +- `Command.fromDescriptor` -> `Command.make`: The descriptor layer was folded into Command; define config and handler directly on Command.make. + +- `Command.getBashCompletions` -> `Completions.generate`: Generation now returns one script string; normally use GlobalFlag.Completions through the runner. + +- `Command.getFishCompletions` -> `Completions.generate`: Generation now returns one script string; normally use GlobalFlag.Completions through the runner. + +- `Command.getHelp` -> `none`: Help generation for a command path is internal; use GlobalFlag.Help through Command.run or runWith. + +- `Command.getNames` -> `Command.Command.name / Command.Command.alias`: Read the public name and optional alias fields; no HashSet accessor remains. + +- `Command.getSubcommands` -> `Command.Command.subcommands`: Read the public grouped subcommands field; its shape is no longer a name map. + +- `Command.getUsage` -> `none`: Usage is generated internally as part of structured HelpDoc. + +- `Command.getZshCompletions` -> `Completions.generate`: Generation now returns one script string; normally use GlobalFlag.Completions through the runner. + +- `Command.make` -> `Command.make`: Use the redesigned constructor with one nested config object of Argument and Flag values. + +- `Command.run` -> `Command.runWith`: Use runWith for the v3-style function that accepts an argv array; use run to read arguments from Stdio. + +- `Command.transformHandler` -> `none`: Transform in the handler or use the specific provide combinators; the generic handler transform is internal. + +- `Command.withDescription` -> `Command.withDescription`: Use the retained combinator; v4 descriptions are strings rather than the old HelpDoc ADT. + +### `@effect/cli/CommandDescriptor` + +- `CommandDescriptor.Command.ComputeParsedType` -> `Types.Simplify`: Use the general simplification utility, or Command.Command.Config.Infer for command config. + +- `CommandDescriptor.Command.GetParsedType` -> `none`: No public parsed-input extractor remains; v4 handlers receive inferred config directly. + +- `CommandDescriptor.Command.ParsedStandardCommand` -> `none`: The name/options/args parsed wrapper was removed; handlers receive inferred config directly. + +- `CommandDescriptor.Command.ParsedUserInputCommand` -> `none`: The descriptor-level prompt command was removed; use Prompt APIs or Command.wizard. + +- `CommandDescriptor.Command.Subcommands` -> `none`: Compose independently handled commands with Command.withSubcommands instead of parsing a tuple union. + +- `CommandDescriptor.Command.Variance` -> `Command.Command.Variance`: The command variance helper remains conceptually, now tracking input, error, and requirements. + +- `CommandDescriptor.TypeId` -> `Command.isCommand`: The type id is internal in v4; use the public runtime guard. + +- `CommandDescriptor.getBashCompletions` -> `Completions.generate`: Completion generation moved to one shell-parameterized function; command conversion is internal. + +- `CommandDescriptor.getFishCompletions` -> `Completions.generate`: Completion generation moved to one shell-parameterized function; command conversion is internal. + +- `CommandDescriptor.getHelp` -> `none`: Help generation is internal to the Command runner. + +- `CommandDescriptor.getNames` -> `Command.Command.name / Command.Command.alias`: Read the public fields; no HashSet accessor remains. + +- `CommandDescriptor.getSubcommands` -> `Command.Command.subcommands`: Read the public grouped subcommands field. + +- `CommandDescriptor.getUsage` -> `none`: Usage generation is internal to Command help generation. + +- `CommandDescriptor.getZshCompletions` -> `Completions.generate`: Completion generation moved to one shell-parameterized function; command conversion is internal. + +- `CommandDescriptor.make` -> `Command.make`: The descriptor and executable command layers were merged into one constructor. + +- `CommandDescriptor.map` -> `none`: Map individual Argument or Flag values, or transform inside the command handler. + +- `CommandDescriptor.mapEffect` -> `none`: Use parameter mapEffect where the transformation belongs to an input, or perform the Effect in the handler. + +- `CommandDescriptor.parse` -> `Command.runWith`: Parsing was folded into execution and no intermediate CommandDirective is returned. + +- `CommandDescriptor.withDescription` -> `Command.withDescription`: Use the retained behavior; v4 descriptions are strings. + +### `@effect/cli/CommandDirective` + +- `CommandDirective.BuiltIn` -> `GlobalFlag.Action`: Use the global action definition type; it is processed directly by the runner. + +- `CommandDirective.CommandDirective` -> `none`: The intermediate parse-result model was removed; the runner invokes the selected handler directly. + +- `CommandDirective.UserDefined` -> `none`: The user-defined intermediate directive was removed. + +- `CommandDirective.builtIn` -> `GlobalFlag.action`: Define a custom action flag; v4 runners no longer return built-in directives. + +- `CommandDirective.isBuiltIn` -> `none`: Intermediate built-in directives were removed. + +- `CommandDirective.map` -> `none`: Map parameters or transform in the handler; there is no intermediate directive to map. + +- `CommandDirective.userDefined` -> `none`: Parsed input is delivered directly to the selected command handler. + +### `@effect/cli/ConfigFile` + +- `ConfigFile.ConfigErrorTypeId` -> `none`: ConfigProvider.SourceError has no public type-id export. + +- `ConfigFile.ConfigFileError` -> `ConfigProvider.SourceError`: Use the general source error when implementing a custom file-backed provider. + +- `ConfigFile.layer` -> `ConfigProvider.layerAdd(customProviderEffect)`: Build the provider explicitly and add it as fallback to preserve the v3 composition order. + +- `ConfigFile.makeProvider` -> `none`: V4 has no API that discovers, parses, and composes config files; use FileSystem, a format parser, and ConfigProvider.fromUnknown explicitly. + +### `@effect/cli/HelpDoc` + +- `HelpDoc.DescriptionList` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.Empty` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.Enumeration` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.Header` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.HelpDoc` -> `HelpDoc.HelpDoc`: The name remains, but v4 is a structured command-help record rather than a tagged document AST. + +- `HelpDoc.Paragraph` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.Sequence` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.blocks` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.descriptionList` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.empty` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.enumeration` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.getSpan` -> `none`: The Span ADT and document-to-span conversion were removed; v4 help fields are strings. + +- `HelpDoc.h1` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.h2` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.h3` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.isDescriptionList` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.isEmpty` -> `none`: The Empty variant was removed when HelpDoc became a structured record; inspect the relevant flags, args, subcommands, and examples arrays when an application-specific emptiness test is needed. + +- `HelpDoc.isEnumeration` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.isHeader` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.isParagraph` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.isSequence` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.mapDescriptionList` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.orElse` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.p` -> `none`: The v3 document-node ADT was removed; v4 uses a structured HelpDoc record rendered by CliOutput. + +- `HelpDoc.toAnsiDoc` -> `none`: CliOutput owns rendering and exposes formatted text rather than an AnsiDoc. + +- `HelpDoc.toAnsiText` -> `CliOutput.defaultFormatter().formatHelpDoc`: Format the structured help record; inside Effect code prefer the CliOutput.Formatter service. + +### `@effect/cli/Options` + +- `Options.All.OptionsAny` -> `Param.AnyFlag`: Use the shared any-flag-parameter type. + +- `Options.All.Return` -> `Command.Command.Config.Infer`: Infer the output of a command config record; standalone flag collections were removed. + +- `Options.Options` -> `Flag.Flag`: Options was renamed to Flag in effect/unstable/cli. + +- `Options.Options.BooleanOptionsConfig` -> `Flag.boolean + Flag.withAlias + Flag.map`: The config object was removed; aliases and value inversion are combinators, while custom negation names need application logic. + +- `Options.Options.PathOptionsConfig` -> `{ readonly mustExist?: boolean }`: Path options are inline; true replaces exists=yes and omission replaces either. exists=no has no exact replacement. + +- `Options.Options.Variance` -> `Flag.Flag`: The separate variance artifact was removed; Flag inherits the shared Param variance. + +- `Options.OptionsTypeId` -> `Param.isParam(value) && value.kind === Param.flagKind`: The public Options type id was removed; use the Param guard and flag kind discriminator. + +- `Options.all` -> `Command.make(name, config)`: Collect flags in the config record passed to Command.make; there is no standalone Flag.all. + +- `Options.atLeast` -> `Flag.atLeast`: Use the moved combinator; v4 returns ReadonlyArray rather than NonEmptyArray. + +- `Options.atMost` -> `Flag.atMost`: Use the moved combinator. + +- `Options.between` -> `Flag.between`: Use the moved combinator; v4 validates bounds when constructing the parameter. + +- `Options.boolean` -> `Flag.boolean`: Use the moved constructor; --no-name is automatic and aliases are added with Flag.withAlias. + +- `Options.choice` -> `Flag.choice`: Use the moved constructor. + +- `Options.choiceWithValue` -> `Flag.choiceWithValue`: Use the moved constructor. + +- `Options.date` -> `Flag.date`: Use the moved constructor. + +- `Options.directory` -> `Flag.directory`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Options.file` -> `Flag.file`: Use mustExist=true for exists=yes and omit it for either; exists=no has no exact replacement. + +- `Options.fileContent` -> `Flag.file + Flag.mapEffect`: Parse a path and read it with FileSystem.readFile; no binary-content flag constructor remains. + +- `Options.fileParse` -> `Flag.fileParse`: Pass the old format as an options field; v4 returns parsed content rather than a path/content tuple. + +- `Options.fileSchema` -> `Flag.fileSchema`: Pass the old format as an options field and use a v4 Schema constraint decoder. + +- `Options.fileText` -> `Flag.file + Flag.mapEffect`: Flag.fileText returns content only; read after Flag.file when the path/content tuple must be preserved. + +- `Options.filterMap` -> `Flag.filterMap`: Use the moved combinator and replace the fixed message with an onNone function. + +- `Options.float` -> `Flag.float`: Use the moved constructor. + +- `Options.getHelp` -> `none`: Per-flag help introspection was removed; Command generates help internally. + +- `Options.getIdentifier` -> `none`: Public flag identifier introspection was removed. + +- `Options.getUsage` -> `none`: The public Usage tree was removed; Command generates a usage string internally. + +- `Options.integer` -> `Flag.integer`: Use the moved constructor. + +- `Options.isBool` -> `none`: No public flag-shape predicate remains; boolean-shape inspection is internal. + +- `Options.isOptions` -> `Param.isParam(value) && value.kind === Param.flagKind`: Flags now use the shared Param representation and an explicit kind discriminator. + +- `Options.keyValueMap` -> `Flag.keyValuePair`: Renamed and now returns Record\ rather than HashMap. + +- `Options.map` -> `Flag.map`: Use the moved combinator. + +- `Options.mapEffect` -> `Flag.mapEffect`: Use the moved combinator; mapping failures are CliError. + +- `Options.mapTryCatch` -> `Flag.mapTryCatch`: Use the moved combinator; onError now returns a string rather than HelpDoc. + +- `Options.none` -> `omit the config entry`: V4 Flag.none is an always-failing sentinel, not v3's empty successful option set. + +- `Options.optional` -> `Flag.optional`: Use the moved combinator; it still returns Option. + +- `Options.orElse` -> `Flag.orElse(() => fallback)`: The fallback is now lazy; add explicit exclusivity validation if both flags must be rejected. + +- `Options.orElseEither` -> `Flag.orElseResult(() => fallback)`: Either became Result and the fallback is lazy; v4 no longer rejects both flags being present. + +- `Options.parse` -> `flag.parse({ flags, arguments: [] })`: Parsing is now a Param method over a Record and returns leftover arguments with the value; errors are CliError. + +- `Options.processCommandLine` -> `Command.runWith`: Raw argv processing is now whole-command execution; no public standalone flag tokenizer remains. + +- `Options.redacted` -> `Flag.redacted`: Use the moved constructor. + +- `Options.repeated` -> `Flag.variadic`: Renamed to variadic; pass optional min and max bounds. + +- `Options.secret` -> `Flag.redacted`: The deprecated Secret constructor was removed; use Redacted-backed input. + +- `Options.text` -> `Flag.string`: Renamed from text to string. + +- `Options.withAlias` -> `Flag.withAlias`: Use the moved combinator. + +- `Options.withDefault` -> `Flag.withDefault`: Use the moved combinator; v4 also accepts an Effect fallback. + +- `Options.withDescription` -> `Flag.withDescription`: Use the moved combinator. + +- `Options.withFallbackConfig` -> `Flag.withFallbackConfig`: Use the moved combinator; invalid configuration becomes CliError.InvalidValue. + +- `Options.withFallbackPrompt` -> `Flag.withFallbackPrompt`: Use the moved combinator; v4 can construct the Prompt lazily in Effect. + +- `Options.withPseudoName` -> `Flag.withMetavar`: Renamed to withMetavar. + +- `Options.withSchema` -> `Flag.withSchema`: Use the moved combinator with a v4 Schema constraint decoder. + +### `@effect/cli/Primitive` + +- `Primitive.Primitive.PathExists` -> `mustExist?: boolean`: Use true for yes and omit for either; no cannot be represented exactly because false permits existing paths. + +- `Primitive.Primitive.ValueType` -> `P extends Primitive.Primitive ? A : never`: The named helper was removed; infer the value with a local conditional type. + +- `Primitive.Primitive.Variance` -> `Primitive.Primitive.Variance`: The variance interface remains, but its brand key is internal; prefer Primitive\ in user APIs. + +- `Primitive.PrimitiveTypeId` -> `none`: The public Primitive type-id symbol was removed. + +- `Primitive.boolean` -> `Primitive.boolean`: Boolean is now a singleton value; defaults belong on Flag.boolean or withDefault. + +- `Primitive.choice` -> `Primitive.choice`: Use the moved constructor. + +- `Primitive.date` -> `Primitive.date`: Date is now a singleton Primitive value. + +- `Primitive.getChoices` -> `none`: Choice introspection is internal in v4; retain alternatives in application code when needed. + +- `Primitive.getHelp` -> `none`: Primitive-level help generation was removed from the public API. + +- `Primitive.isBool` -> `none`: The boolean Primitive predicate is internal in v4. + +- `Primitive.text` -> `Primitive.string`: Renamed from text to string. + +- `Primitive.validate` -> `primitive.parse(value)`: Parsing is now the Primitive.parse method over a string; defaults and case normalization moved out of this layer. + +### `@effect/cli/Prompt` + +- `Prompt.All.PromptAny` -> `Prompt.Any`: The any-prompt alias moved out of the All namespace. + +- `Prompt.All.Return` -> `Prompt.All.Return`: The collection result helper remains under Prompt.All. + +- `Prompt.Prompt` -> `Prompt.Prompt`: The model moved to effect/unstable/cli; quitting now fails with Terminal.QuitError. + +- `Prompt.Prompt.Variance` -> `Prompt.Prompt`: The named variance artifact was removed; use Prompt\. + +- `Prompt.Prompt.VarianceStruct` -> `Prompt.Prompt`: The named variance structure was removed; use Prompt\. + +- `Prompt.PromptTypeId` -> `Prompt.isPrompt`: The public type-id symbol was removed; use the runtime guard. + +- `Prompt.date` -> `Prompt.date`: Use the moved constructor. + +- `Prompt.file` -> `Prompt.file`: Use the moved constructor; v4 also supports a default selected path. + +- `Prompt.float` -> `Prompt.float`: Use the moved constructor; v4 also supports a default value. + +- `Prompt.integer` -> `Prompt.integer`: Use the moved constructor; v4 also supports a default value. + +- `Prompt.text` -> `Prompt.text`: Use the moved constructor. + +### `@effect/cli/ValidationError` + +- `ValidationError.CommandMismatch` -> `none`: The root-command mismatch error was removed. + +- `ValidationError.CorrectedFlag` -> `CliError.UnrecognizedOption`: Use the unrecognized-option class and its suggestions field. + +- `ValidationError.HelpRequested` -> `CliError.ShowHelp`: Renamed and redesigned as ShowHelp. + +- `ValidationError.InvalidArgument` -> `CliError.InvalidValue | CliError.UnexpectedArgument`: Argument decoding and leftover operands are separate v4 errors. + +- `ValidationError.InvalidValue` -> `CliError.InvalidValue`: Use the schema-backed v4 error class. + +- `ValidationError.MissingFlag` -> `CliError.MissingOption`: Renamed to MissingOption. + +- `ValidationError.MissingSubcommand` -> `none`: The dedicated missing-subcommand error was removed. + +- `ValidationError.MissingValue` -> `CliError.InvalidValue`: The dedicated tag was folded into InvalidValue. + +- `ValidationError.MultipleValuesDetected` -> `CliError.InvalidValue`: Count violations are summarized as InvalidValue without preserving the old values array. + +- `ValidationError.NoBuiltInMatch` -> `none`: The intermediate built-in matching error was removed. + +- `ValidationError.UnclusteredFlag` -> `none`: The public cluster error was removed. + +- `ValidationError.ValidationError` -> `CliError.CliError`: The validation union was redesigned and renamed to CliError. + +- `ValidationError.ValidationError.Proto` -> `none`: V4 errors are schema-backed classes and expose no shared public prototype type. + +- `ValidationError.ValidationErrorTypeId` -> `none`: The CliError type id is private; use CliError.isCliError. + +- `ValidationError.commandMismatch` -> `none`: V4 runners receive arguments after the root name; unknown child commands use CliError.UnknownSubcommand. + +- `ValidationError.correctedFlag` -> `new CliError.UnrecognizedOption({ option, command, suggestions })`: Suggestions are carried by UnrecognizedOption; there is no separate corrected-flag case. + +- `ValidationError.helpRequested` -> `new CliError.ShowHelp({ commandPath, errors: [] })`: Help requests now carry a command path and optional underlying errors. + +- `ValidationError.invalidArgument` -> `new CliError.InvalidValue({ option, value, expected, kind: "argument" })`: Use InvalidValue for undecodable arguments and UnexpectedArgument for leftover operands. + +- `ValidationError.invalidValue` -> `new CliError.InvalidValue({ option, value, expected, kind })`: Replace the HelpDoc payload with structured option, value, expected, and kind fields. + +- `ValidationError.isCommandMismatch` -> `none`: The root-command mismatch error was removed. + +- `ValidationError.isCorrectedFlag` -> `error._tag === "UnrecognizedOption" && error.suggestions.length > 0`: Check the v4 tag and suggestions array. + +- `ValidationError.isHelpRequested` -> `error._tag === "ShowHelp"`: Narrow the CliError union by its tag. + +- `ValidationError.isInvalidArgument` -> `(error._tag === "InvalidValue" && error.kind === "argument") || error._tag === "UnexpectedArgument"`: Check both v4 argument error forms. + +- `ValidationError.isInvalidValue` -> `error._tag === "InvalidValue"`: Narrow the CliError union by its tag. + +- `ValidationError.isMissingFlag` -> `error._tag === "MissingOption"`: MissingFlag was renamed to MissingOption. + +- `ValidationError.isMissingSubcommand` -> `none`: Missing subcommands now cause ShowHelp rather than a dedicated error. + +- `ValidationError.isMissingValue` -> `error._tag === "InvalidValue" && error.value === ""`: Missing values are represented as InvalidValue with an empty value. + +- `ValidationError.isMultipleValuesDetected` -> `none`: Count violations are summarized as InvalidValue without a stable subtype. + +- `ValidationError.isNoBuiltInMatch` -> `none`: Built-ins are GlobalFlag definitions and the intermediate failure was removed. + +- `ValidationError.isUnclusteredFlag` -> `none`: Cluster expansion is internal and has no public intermediate error. + +- `ValidationError.isValidationError` -> `CliError.isCliError`: Use the renamed union guard. + +- `ValidationError.keyValuesDetected` -> `new CliError.InvalidValue({ option, value, expected, kind: "flag" })`: Represent count violations with structured InvalidValue fields. + +- `ValidationError.missingFlag` -> `new CliError.MissingOption({ option })`: MissingFlag was renamed to MissingOption. + +- `ValidationError.missingSubcommand` -> `none`: A parent without a selected subcommand now shows help rather than emitting a dedicated error. + +- `ValidationError.missingValue` -> `new CliError.InvalidValue({ option, value: "", expected, kind })`: Missing values are represented as InvalidValue with an empty value. + +- `ValidationError.noBuiltInMatch` -> `none`: Built-ins are GlobalFlag definitions and the intermediate failure was removed. + +- `ValidationError.unclusteredFlag` -> `none`: Flag cluster expansion is internal in v4. + +### `@effect/cluster/ClusterCron` + +- `ClusterCron.make` -> `effect/unstable/cluster/ClusterCron#make`: Moved into core Effect. The constructor remains; Duration.DurationInput is now Duration.Input. + +### `@effect/cluster/ClusterError` + +- `ClusterError.TypeId` -> `none`: The shared marker is private in v4. Use the exported tagged error classes, their \_tag fields, or class-specific is guards. + +### `@effect/cluster/ClusterSchema` + +- `ClusterSchema.ClientTracingEnabled` -> `effect/unstable/cluster/ClusterSchema#ClientTracingEnabled`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value. + +- `ClusterSchema.Persisted` -> `effect/unstable/cluster/ClusterSchema#Persisted`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same false default. + +- `ClusterSchema.ShardGroup` -> `effect/unstable/cluster/ClusterSchema#ShardGroup`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value. + +- `ClusterSchema.Uninterruptible` -> `effect/unstable/cluster/ClusterSchema#Uninterruptible`: Now a Context.Reference value. Replace its static methods with ClusterSchema.isUninterruptibleForServer and isUninterruptibleForClient. + +### `@effect/cluster/ClusterWorkflowEngine` + +- `ClusterWorkflowEngine.layer` -> `effect/unstable/cluster/ClusterWorkflowEngine#layer`: Moved into core Effect with the same cluster-backed WorkflowEngine layer composition. + +- `ClusterWorkflowEngine.make` -> `effect/unstable/cluster/ClusterWorkflowEngine#make`: Moved into core Effect; the constructor still uses Sharding and MessageStorage. + +### `@effect/cluster/DeliverAt` + +- `DeliverAt.symbol` -> `effect/unstable/cluster/DeliverAt#symbol`: Moved into core Effect; the protocol key is now the string literal \~effect/cluster/DeliverAt rather than a global symbol. + +### `@effect/cluster/Entity` + +- `Entity.HandlersFrom` -> `effect/unstable/cluster/Entity#HandlersFrom`: Moved into core Effect; handler results now use Rpc.WrapperOr, which accepts either the raw RPC result or its wrapper. + +- `Entity.Replier.Success` -> `effect/unstable/cluster/Entity#Replier.Success`: Moved into core Effect; streaming replies may use Queue.Dequeue with Cause.Done instead of the removed Mailbox type. + +- `Entity.TypeId` -> `none`: The entity marker is private in v4. Use Entity.isEntity for runtime refinement. + +### `@effect/cluster/EntityAddress` + +- `EntityAddress.EntityAddressFromSelf` -> `effect/unstable/cluster/EntityAddress#EntityAddress`: The separate self schema was removed; the v4 Schema.Class is itself the EntityAddress schema. + +- `EntityAddress.TypeId` -> `none`: The marker is private in v4. Use the exported EntityAddress class and schema. + +### `@effect/cluster/EntityProxy` + +- `EntityProxy.ConvertHttpApi` -> `effect/unstable/cluster/EntityProxy#ConvertHttpApi`: Moved into core Effect and updated to the v4 HttpApiEndpoint and Schema types. + +- `EntityProxy.ConvertRpcs` -> `effect/unstable/cluster/EntityProxy#ConvertRpcs`: Moved into core Effect and updated to the v4 Rpc and Schema type parameters. + +### `@effect/cluster/EntityProxyServer` + +- `EntityProxyServer.RpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#RpcHandlers`: Moved into core Effect and updated for the additional v4 Rpc requirements type parameter. + +- `EntityProxyServer.layerHttpApi` -> `effect/unstable/cluster/EntityProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi identifiers and Rpc.ServicesServer requirements. + +- `EntityProxyServer.layerRpcHandlers` -> `effect/unstable/cluster/EntityProxyServer#layerRpcHandlers`: Moved into core Effect; the service requirement is now Rpc.ServicesServer rather than Rpc.Context. + +### `@effect/cluster/EntityResource` + +- `EntityResource.TypeId` -> `effect/unstable/cluster/EntityResource#TypeId`: Moved into core Effect; its literal changed to \~effect/cluster/EntityResource. + +- `EntityResource.make` -> `effect/unstable/cluster/EntityResource#make`: Moved into core Effect. Acquisition is lazy by default in v4; set acquireEagerly: true to preserve v3 behavior. + +### `@effect/cluster/Envelope` + +- `Envelope.Envelope.Encoded` -> `effect/unstable/cluster/Envelope#Encoded`: The encoded envelope union moved to the module-level Encoded type. + +- `Envelope.Envelope.PartialEncoded` -> `effect/unstable/cluster/Envelope#Partial`: The partially decoded runtime union was renamed to Partial; use PartialJson for its JSON codec. + +- `Envelope.EnvelopeFromSelf` -> `effect/unstable/cluster/Envelope#Envelope`: The self schema was renamed to Envelope and declaration-merges with the envelope type and namespace. + +- `Envelope.PartialEncoded` -> `effect/unstable/cluster/Envelope#PartialJson`: The partially decoded envelope JSON codec was renamed to PartialJson. + +- `Envelope.PartialEncodedArray` -> `effect/unstable/cluster/Envelope#PartialArray`: The mutable array codec was renamed to PartialArray. + +- `Envelope.PartialEncodedFromSelf` -> `effect/unstable/cluster/Envelope#Partial`: The separate self schema was folded into Partial; derive JSON encoding with PartialJson. + +- `Envelope.PartialEncodedRequest` -> `Schema.toCodecJson(Envelope.PartialRequest)`: V4 exports the self schema as PartialRequest and derives its JSON codec with Schema.toCodecJson. + +- `Envelope.PartialEncodedRequestFromSelf` -> `effect/unstable/cluster/Envelope#PartialRequest`: The partially decoded request self schema was renamed to PartialRequest. + +- `Envelope.Request` -> `effect/unstable/cluster/Envelope#Request`: The request interface remains and declaration-merges with the exported Request schema. + +- `Envelope.Request.Encoded` -> `effect/unstable/cluster/Envelope#PartialRequestEncoded`: The JSON request shape moved to the module-level PartialRequestEncoded interface. + +- `Envelope.Request.PartialEncoded` -> `effect/unstable/cluster/Envelope#PartialRequest`: The partially decoded request shape moved to the module-level PartialRequest class and type. + +- `Envelope.RequestFromSelf` -> `effect/unstable/cluster/Envelope#Request`: The request self schema was renamed to Request and declaration-merges with the runtime interface. + +- `Envelope.TypeId` -> `typeof Envelope.TypeId`: The marker value remains, but the type alias was removed and the value is now a string literal; use typeof in type position. + +### `@effect/cluster/MachineId` + +- `MachineId.make` -> `effect/unstable/cluster/MachineId#make`: Moved into core Effect. The v4 helper is an unchecked cast; validate external input with the MachineId schema when needed. + +### `@effect/cluster/Message` + +- `Message.serialize` -> `effect/unstable/cluster/Message#serialize`: Moved into core Effect. It now returns Envelope.Partial; use serializeEnvelope for the JSON Envelope.Encoded form. + +### `@effect/cluster/MessageStorage` + +- `MessageStorage.Encoded` -> `effect/unstable/cluster/MessageStorage#Encoded`: Moved into core Effect; use the v4 Envelope.Encoded and Reply.Encoded aliases in custom encoded storage implementations. + +- `MessageStorage.make` -> `effect/unstable/cluster/MessageStorage#make`: Moved into core Effect. Context service projections now use the Service property instead of Type. + +### `@effect/cluster/Reply` + +- `Reply.ReplyEncoded` -> `effect/unstable/cluster/Reply#Encoded`: Renamed to Encoded and no longer parameterized by an Rpc; payload fields are unknown and validated by Reply.Reply(rpc). + +- `Reply.TypeId` -> `none`: The reply marker is private in v4. Use Reply.isReply for runtime refinement. + +- `Reply.serialize` -> `effect/unstable/cluster/Reply#serialize`: Moved into core Effect and now returns the non-generic Reply.Encoded wire union. + +### `@effect/cluster/Runner` + +- `Runner.TypeId` -> `none`: The runner marker is private in v4. Use the exported Runner class and schema. + +### `@effect/cluster/RunnerAddress` + +- `RunnerAddress.TypeId` -> `none`: The runner-address marker is private in v4. Use the exported RunnerAddress class and schema. + +### `@effect/cluster/RunnerStorage` + +- `RunnerStorage.makeMemory` -> `effect/unstable/cluster/RunnerStorage#makeMemory`: Moved into core Effect; it still constructs the in-memory RunnerStorage service implementation. + +### `@effect/cluster/Runners` + +- `Runners.make` -> `effect/unstable/cluster/Runners#make`: Moved into core Effect with the same callbacks and requirements; Context service projections now use Service instead of Type. + +- `Runners.makeNoop` -> `effect/unstable/cluster/Runners#makeNoop`: Moved into core Effect; it returns the Context.Service implementation through the Service projection instead of Type. + +### `@effect/cluster/ShardId` + +- `ShardId.ShardId` -> `effect/unstable/cluster/ShardId#ShardId`: The class became a merged interface and schema value. Use ShardId.make; former static parsers and printers are module functions. + +- `ShardId.TypeId` -> `none`: The shard marker is private in v4. Use ShardId.isShardId for runtime refinement. + +### `@effect/cluster/ShardingConfig` + +- `ShardingConfig.config` -> `effect/unstable/cluster/ShardingConfig#config`: Moved into core Effect; its Context service value type now uses the Service property instead of Type. + +- `ShardingConfig.defaults` -> `effect/unstable/cluster/ShardingConfig#defaults`: Moved into core Effect with the same complete defaults; service type projections now use Service instead of Type. + +- `ShardingConfig.layer` -> `effect/unstable/cluster/ShardingConfig#layer`: Moved into core Effect with the same shallow default merge; service type projections now use Service instead of Type. + +### `@effect/cluster/ShardingRegistrationEvent` + +- `ShardingRegistrationEvent.match` -> `effect/unstable/cluster/ShardingRegistrationEvent#match`: Moved into core Effect with the same tagged-enum matcher. + +### `@effect/cluster/SingleRunner` + +- `SingleRunner.layer` -> `effect/unstable/cluster/SingleRunner#layer`: Moved into core Effect. V4 additionally requires Crypto.Crypto because SQL message storage hashes long deduplication keys. + +### `@effect/cluster/SingletonAddress` + +- `SingletonAddress.TypeId` -> `none`: The singleton-address marker is private in v4. Use the exported SingletonAddress class and schema. + +### `@effect/cluster/Snowflake` + +- `Snowflake.TypeId` -> `effect/unstable/cluster/Snowflake#TypeId`: Moved into core Effect; the public marker is now the string literal \~effect/cluster/Snowflake. + +### `@effect/cluster/SqlMessageStorage` + +- `SqlMessageStorage.layer` -> `effect/unstable/cluster/SqlMessageStorage#layer`: Moved into core Effect. V4 adds a Crypto.Crypto requirement for hashing long deduplication keys. + +- `SqlMessageStorage.layerWith` -> `effect/unstable/cluster/SqlMessageStorage#layerWith`: Moved into core Effect with the same optional table prefix; v4 additionally requires Crypto.Crypto. + +- `SqlMessageStorage.make` -> `effect/unstable/cluster/SqlMessageStorage#make`: Moved into core Effect with the same prefix option; v4 additionally requires Crypto.Crypto. + +### `@effect/cluster/SqlRunnerStorage` + +- `SqlRunnerStorage.make` -> `effect/unstable/cluster/SqlRunnerStorage#make`: Moved into core Effect with the same prefix option and service requirements. + +### `@effect/experimental/DevTools/Client` + +- `Client.Client` -> `effect/unstable/devtools/DevToolsClient#DevToolsClient`: Client was renamed to the DevToolsClient Context.Service class. + +- `Client.ClientImpl` -> `effect/unstable/devtools/DevToolsClient#DevToolsClient["Service"]`: Use the service shape from DevToolsClient; unsafeAddSpan was replaced by sendUnsafe. + +- `Client.layer` -> `effect/unstable/devtools/DevToolsClient#layer`: Import layer from the v4 unstable DevToolsClient module. + +- `Client.make` -> `effect/unstable/devtools/DevToolsClient#make`: Import make from the v4 unstable DevToolsClient module. + +### `@effect/experimental/DevTools/Domain` + +- `Domain.ExternalSpanFrom` -> `effect/Schema#Codec.Encoded`: The named encoded alias was removed; derive it with Schema.Codec.Encoded from ExternalSpan. + +- `Domain.MetricFrom` -> `effect/Schema#Codec.Encoded`: The named encoded alias was removed; derive it with Schema.Codec.Encoded from Metric. + +- `Domain.MetricsSnapshotFrom` -> `effect/Schema#Codec.Encoded`: The named encoded alias was removed; derive it with Schema.Codec.Encoded from MetricsSnapshot. + +- `Domain.ParentSpanFrom` -> `effect/Schema#Codec.Encoded`: The named encoded alias was removed; derive it with Schema.Codec.Encoded from ParentSpan. + +- `Domain.SpanFrom` -> `effect/Schema#Codec.Encoded`: The named encoded alias was removed; derive it with Schema.Codec.Encoded from Span. + +- `Domain.metric` -> `none`: The metric schema helper is private in v4; use the exported Counter, Frequency, Gauge, Histogram, Summary, or Metric schemas, or build a Schema.Struct. + +### `@effect/experimental/DevTools/Server` + +- `Server.run` -> `effect/unstable/devtools/DevToolsServer#run`: Import run from the v4 unstable DevToolsServer module. + +### `@effect/experimental/Event` + +- `Event.Event.AddError` -> `effect/unstable/eventlog/Event#AddError`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.Any` -> `effect/unstable/eventlog/Event#Any`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.Context` -> `effect/unstable/eventlog/Event#Services`: Event schema context is now represented by decoding and encoding Services. + +- `Event.Event.ContextWithTag` -> `effect/unstable/eventlog/Event#Services>`: Filter with WithTag and derive its decoding and encoding Services. + +- `Event.Event.Error` -> `effect/unstable/eventlog/Event#Error`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.ErrorSchema` -> `effect/unstable/eventlog/Event#ErrorSchema`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.Payload` -> `effect/unstable/eventlog/Event#Payload`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.PayloadSchema` -> `effect/unstable/eventlog/Event#PayloadSchema`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.Success` -> `effect/unstable/eventlog/Event#Success`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.SuccessSchema` -> `effect/unstable/eventlog/Event#SuccessSchema`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.Tag` -> `effect/unstable/eventlog/Event#Tag`: This type moved from the Event namespace to a top-level export. + +- `Event.Event.ToService` -> `effect/unstable/eventlog/Event#ToService`: This type moved from the Event namespace to a top-level export. + +- `Event.TypeId` -> `effect/unstable/eventlog/Event#TypeId`: Import TypeId from the v4 unstable Event module; its runtime representation is now a string brand. + +- `Event.make` -> `effect/unstable/eventlog/Event#make`: Import make from the v4 unstable Event module. + +### `@effect/experimental/EventGroup` + +- `EventGroup.EventGroup.Any` -> `effect/unstable/eventlog/EventGroup#Any`: This type moved from the EventGroup namespace to a top-level export. + +- `EventGroup.EventGroup.AnyWithProps` -> `effect/unstable/eventlog/EventGroup#AnyWithProps`: This type moved from the EventGroup namespace to a top-level export. + +- `EventGroup.EventGroup.Context` -> `effect/unstable/eventlog/EventGroup#ServicesClient | effect/unstable/eventlog/EventGroup#ServicesServer`: Choose the client or server schema services for the required direction. + +- `EventGroup.EventGroup.ToService` -> `effect/unstable/eventlog/EventGroup#ToService`: This type moved from the EventGroup namespace to a top-level export. + +- `EventGroup.TypeId` -> `effect/unstable/eventlog/EventGroup#TypeId`: Import TypeId from the v4 unstable EventGroup module; its runtime representation is now a string brand. + +### `@effect/experimental/EventJournal` + +- `EventJournal.EntryIdTypeId` -> `effect/unstable/eventlog/EventJournal#EntryIdTypeId`: Import EntryIdTypeId from the v4 EventJournal module; it is now a string brand. + +- `EventJournal.ErrorTypeId` -> `none`: The v4 error marker is private; narrow with EventJournalError instead. + +- `EventJournal.RemoteIdTypeId` -> `effect/unstable/eventlog/EventJournal#RemoteIdTypeId`: Import RemoteIdTypeId from the v4 EventJournal module; it is now a string brand. + +- `EventJournal.makeEntryId` -> `effect/unstable/eventlog/EventJournal#makeEntryIdUnsafe`: The unchecked EntryId constructor was renamed to makeEntryIdUnsafe. + +- `EventJournal.makeMemory` -> `effect/unstable/eventlog/EventJournal#makeMemory`: The in-memory constructor moved into core Effect and now returns the Context.Service implementation through its Service projection. + +- `EventJournal.makeRemoteId` -> `effect/unstable/eventlog/EventJournal#makeRemoteIdUnsafe`: The unchecked RemoteId constructor was renamed to makeRemoteIdUnsafe. + +### `@effect/experimental/EventLog` + +- `EventLog.Handlers` -> `effect/unstable/eventlog/EventLog#Handlers`: Import Handlers from the v4 EventLog module; handlers now also receive storeId. + +- `EventLog.HandlersTypeId` -> `effect/unstable/eventlog/EventLog#HandlersTypeId`: Import HandlersTypeId from the v4 EventLog module. + +- `EventLog.SchemaTypeId` -> `effect/unstable/eventlog/EventLog#SchemaTypeId`: Import SchemaTypeId from the v4 EventLog module. + +- `EventLog.group` -> `effect/unstable/eventlog/EventLog#group`: Import group from the v4 EventLog module; it now requires the shared Registry service. + +- `EventLog.layer` -> `effect/unstable/eventlog/EventLog#layer`: The v4 layer takes both the schema and handler layer; use layerEventLog for runtime only. + +- `EventLog.layerIdentityKvs` -> `none`: Compose KeyValueStore.toSchemaStore, EventLog.IdentitySchema, EventLog.makeIdentity, and Layer.effect manually. + +### `@effect/experimental/EventLogRemote` + +- `EventLogRemote.Ack` -> `none`: A write acknowledgement is now the void success of EventLogMessage.WriteSingleRpc or WriteChunkedRpc. + +- `EventLogRemote.Changes` -> `effect/unstable/eventlog/EventLogMessage#ChangesRpc`: ChangesRpc replaces the separate request and response models with one streaming RPC. + +- `EventLogRemote.EventLogRemote` -> `effect/unstable/eventlog/EventLogRemote#EventLogRemote`: Use the v4 Context.Service; methods now take storeId-aware options. + +- `EventLogRemote.Hello` -> `effect/unstable/eventlog/EventLogMessage#HelloResponse`: HelloResponse replaces Hello and includes the v4 authentication challenge; HelloRpc defines the endpoint. + +- `EventLogRemote.Pong` -> `none`: The event-log Pong model was removed; heartbeats belong to the generic RPC socket protocol. + +- `EventLogRemote.ProtocolRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: EventLogRemoteRpcs and generic RPC serialization replace the old protocol request union. + +- `EventLogRemote.ProtocolRequestMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerMsgPack`: Use the generic MsgPack RPC serialization layer instead of a request-specific schema. + +- `EventLogRemote.ProtocolResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: EventLogRemoteRpcs and generic RPC serialization replace the old protocol response union. + +- `EventLogRemote.ProtocolResponseMsgPack` -> `effect/unstable/rpc/RpcSerialization#layerMsgPack`: Use the generic MsgPack RPC serialization layer instead of a response-specific schema. + +- `EventLogRemote.RemoteAdditions` -> `none`: This unused protocol model has no v4 counterpart. + +- `EventLogRemote.RequestChanges` -> `effect/unstable/eventlog/EventLogMessage#ChangesRpc`: ChangesRpc replaces the separate request model with one streaming RPC. + +- `EventLogRemote.StopChanges` -> `none`: Interrupt the ChangesRpc stream instead of sending a StopChanges message. + +- `EventLogRemote.decodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request decoder. + +- `EventLogRemote.decodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response decoder. + +- `EventLogRemote.encodeRequest` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific request encoder. + +- `EventLogRemote.encodeResponse` -> `effect/unstable/eventlog/EventLogMessage#EventLogRemoteRpcs`: Generic RPC framing and RpcSerialization.layerMsgPack replace the module-specific response encoder. + +- `EventLogRemote.fromSocket` -> `effect/unstable/eventlog/EventLogRemote#makeEncrypted + effect/unstable/rpc/RpcClient#makeProtocolSocket`: Construct the encrypted remote separately from its generic RPC socket protocol. + +- `EventLogRemote.layerWebSocket` -> `effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket`: Compose the encrypted remote with the generic socket protocol, MsgPack serialization, and a Socket provider. + +- `EventLogRemote.layerWebSocketBrowser` -> `effect/unstable/eventlog/EventLogRemote#layerEncrypted + effect/unstable/rpc/RpcClient#layerProtocolSocket + @effect/platform-browser/BrowserSocket#layerWebSocket`: Compose the encrypted remote and generic RPC socket protocol with the browser WebSocket layer. + +### `@effect/experimental/EventLogServer` + +- `EventLogServer.makeHandler` -> `effect/unstable/eventlog/EventLogServerEncrypted#layer + effect/unstable/rpc/RpcServer#layerProtocolSocketServer`: Compose the encrypted server layer with the generic RPC socket server; there is no per-socket handler factory. + +- `EventLogServer.makeHandlerHttp` -> `effect/unstable/eventlog/EventLogServerEncrypted#layer + effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffectWebsocket`: Use the returned httpEffect for upgrades and provide its protocol to the encrypted server layer. + +- `EventLogServer.makeStorageMemory` -> `effect/unstable/eventlog/EventLogServerEncrypted#makeStorageMemory`: Use the encrypted server memory storage constructor. + +### `@effect/experimental/PersistedCache` + +- `PersistedCache.make` -> `effect/unstable/persistence/PersistedCache#make`: Pass lookup as the first argument and options second; timeToLive now receives exit before request and the service is Persistence.Persistence. + +### `@effect/experimental/PersistedQueue` + +- `PersistedQueue.TypeId` -> `effect/unstable/persistence/PersistedQueue#TypeId`: Import TypeId from the v4 unstable PersistedQueue module; it is now a string brand. + +- `PersistedQueue.make` -> `effect/unstable/persistence/PersistedQueue#make`: Import make from the v4 unstable PersistedQueue module. + +### `@effect/experimental/PersistedQueue/Redis` + +- `Redis.layerStore` -> `effect/unstable/persistence/PersistedQueue#layerStoreRedis`: The Redis adapter was merged into PersistedQueue and now requires the generic Redis.Redis service. + +- `Redis.layerStoreConfig` -> `none`: Configure a Redis provider such as NodeRedis.layerConfig separately, then compose it with PersistedQueue.layerStoreRedis. + +- `Redis.make` -> `effect/unstable/persistence/PersistedQueue#makeStoreRedis`: The Redis adapter was merged into PersistedQueue and now requires the generic Redis.Redis service. + +### `@effect/experimental/Persistence` + +- `Persistence.BackingPersistence` -> `effect/unstable/persistence/Persistence#BackingPersistence`: Use the v4 BackingPersistence Context.Service class. + +- `Persistence.BackingPersistenceTypeId` -> `none`: The BackingPersistence brand is no longer publicly exported in v4. + +- `Persistence.ErrorTypeId` -> `none`: The v4 persistence error identifier is private; narrow with the exported error classes. + +- `Persistence.PersistenceBackingError` -> `effect/unstable/persistence/Persistence#PersistenceError`: PersistenceError now represents failures from the backing persistence implementation. + +- `Persistence.PersistenceError` -> `effect/unstable/persistence/Persistence#PersistenceError | effect/Schema#SchemaError`: The old combined alias was split into backing PersistenceError and schema SchemaError. + +- `Persistence.PersistenceParseError` -> `effect/Schema#SchemaError`: Persistence parsing failures now use the core SchemaError type. + +- `Persistence.ResultPersistence` -> `effect/unstable/persistence/Persistence#Persistence`: ResultPersistence was renamed to Persistence and is now a Context.Service class. + +- `Persistence.ResultPersistence.Key` -> `effect/unstable/persistence/Persistable#Persistable`: Persistable is the v4 schema-backed persistence key contract. + +- `Persistence.ResultPersistence.KeyAny` -> `effect/unstable/persistence/Persistable#Any`: Use Persistable.Any for an arbitrary v4 persistence key contract. + +- `Persistence.ResultPersistence.TimeToLiveArgs` -> `Parameters>`: Derive the tuple from TimeToLiveFn; its order is now exit then request. + +- `Persistence.ResultPersistenceStore` -> `effect/unstable/persistence/Persistence#PersistenceStore`: ResultPersistenceStore was renamed to PersistenceStore. + +- `Persistence.ResultPersistenceTypeId` -> `none`: The ResultPersistence brand is no longer publicly exported in v4. + +- `Persistence.layerKeyValueStore` -> `effect/unstable/persistence/Persistence#layerBackingKvs`: The KeyValueStore backing layer was renamed to layerBackingKvs. + +- `Persistence.layerMemory` -> `effect/unstable/persistence/Persistence#layerBackingMemory`: Use layerBackingMemory for the old backing service; v4 layerMemory creates the higher-level Persistence service. + +- `Persistence.layerResult` -> `effect/unstable/persistence/Persistence#layer`: The ResultPersistence service layer was renamed to layer. + +- `Persistence.layerResultKeyValueStore` -> `effect/unstable/persistence/Persistence#layerKvs`: The combined KeyValueStore-backed result layer was renamed to layerKvs. + +- `Persistence.layerResultMemory` -> `effect/unstable/persistence/Persistence#layerMemory`: The combined memory-backed result layer was renamed to layerMemory. + +### `@effect/experimental/Persistence/Redis` + +- `Redis.layer` -> `effect/unstable/persistence/Persistence#layerBackingRedis`: The Redis backing adapter was merged into Persistence and now requires the generic Redis.Redis service. + +- `Redis.layerConfig` -> `none`: Compose Persistence.layerBackingRedis with a config-driven provider such as NodeRedis.layerConfig. + +- `Redis.layerResult` -> `effect/unstable/persistence/Persistence#layerRedis`: The combined Redis persistence layer was merged into Persistence and now requires Redis.Redis. + +- `Redis.layerResultConfig` -> `none`: Compose Persistence.layerRedis with a config-driven provider such as NodeRedis.layerConfig. + +- `Redis.make` -> `none`: V4 exposes Redis-backed layers over the Redis.Redis service, not a constructor that creates an ioredis client directly. + +### `@effect/experimental/RateLimiter` + +- `RateLimiter.RateLimiterError` -> `effect/unstable/persistence/RateLimiter#RateLimiterError`: The retained name is now a wrapper error class whose reason is RateLimitExceeded or RateLimitStoreError. + +- `RateLimiter.TypeId` -> `effect/unstable/persistence/RateLimiter#TypeId`: Import TypeId from the v4 unstable RateLimiter module; it is now a string brand. + +- `RateLimiter.makeSleep` -> `effect/unstable/persistence/RateLimiter#sleep`: The accessor Effect was replaced by sleep; obtain the RateLimiter service and pass it to sleep directly or with its curried overload. + +### `@effect/experimental/RateLimiter/Redis` + +- `Redis.layerStore` -> `effect/unstable/persistence/RateLimiter#layerStoreRedis`: The Redis adapter was merged into RateLimiter and now requires the generic Redis.Redis service. + +- `Redis.layerStoreConfig` -> `effect/unstable/persistence/RateLimiter#layerStoreRedisConfig`: Use the merged Redis store config layer; connection configuration belongs to a separate Redis provider. + +- `Redis.make` -> `effect/unstable/persistence/RateLimiter#makeStoreRedis`: The Redis adapter was merged into RateLimiter and now requires the generic Redis.Redis service. + +### `@effect/experimental/Reactivity` + +- `Reactivity.Reactivity` -> `effect/unstable/reactivity/Reactivity#Reactivity`: Use the v4 Reactivity Context.Service; unsafe methods were renamed with an Unsafe suffix. + +- `Reactivity.Reactivity.Service` -> `effect/unstable/reactivity/Reactivity#Reactivity["Service"]`: The named namespace member was removed; derive the service shape from the Context.Service class. + +- `Reactivity.make` -> `effect/unstable/reactivity/Reactivity#make`: Import make from the v4 unstable Reactivity module. + +### `@effect/experimental/RequestResolver` + +- `RequestResolver.PersistedRequest` -> `effect/Request#Request & effect/unstable/persistence/Persistable#Persistable`: Intersect a Request with Persistable or define it with Persistable.Class; there is no combined named export. + +- `RequestResolver.PersistedRequest.Any` -> `effect/Request#Any & effect/unstable/persistence/Persistable#Any`: Intersect the Request and Persistable helper types for an arbitrary persisted request. + +- `RequestResolver.dataLoader` -> `effect/RequestResolver#setDelay + effect/RequestResolver#batchN`: Pipe the resolver through setDelay(options.window) and batchN(options.maxBatchSize ?? Infinity); the transformation is now pure. + +- `RequestResolver.persisted` -> `effect/RequestResolver#persisted`: Retained after moving to core RequestResolver; requests now implement Persistable and use Persistence.Persistence, timeToLive is optional, and staleWhileRevalidate is supported. + +### `@effect/experimental/Sse` + +- `Sse.RetryTypeId` -> `none`: The Retry identifier is private in v4; use effect/unstable/encoding/Sse#Retry and Retry.is instead of inspecting the brand. + +### `@effect/experimental/VariantSchema` + +- `VariantSchema.Extract` -> `effect/unstable/schema/VariantSchema#Extract`: Import the retained helper from the v4 module; its erased schema constraint is Schema.Top. + +- `VariantSchema.Field.Any` -> `effect/unstable/schema/VariantSchema#Field.Any`: Import the retained Field.Any helper type from the v4 unstable VariantSchema module. + +- `VariantSchema.Field.Config` -> `effect/unstable/schema/VariantSchema#Field.Config`: Import the retained Field.Config helper type from the v4 unstable VariantSchema module. + +- `VariantSchema.Field.Fields` -> `effect/unstable/schema/VariantSchema#Field.Fields`: Import the retained Field.Fields helper type from the v4 unstable VariantSchema module. + +- `VariantSchema.Field.ValueAny` -> `effect/Schema#Top`: Use the core Schema.Top constraint for an arbitrary field value schema. + +- `VariantSchema.FieldTypeId` -> `none`: The Field brand is private in v4; use VariantSchema.isField for narrowing. + +- `VariantSchema.Struct.Fields` -> `effect/unstable/schema/VariantSchema#Struct.Fields`: Import the retained Struct.Fields helper type from the v4 unstable VariantSchema module. + +- `VariantSchema.TypeId` -> `effect/unstable/schema/VariantSchema#TypeId`: Use the retained runtime value; in type position use typeof VariantSchema.TypeId. + +- `VariantSchema.fromKey` -> `none`: Field-level fromKey was not ported; for whole-struct encoded-key renaming consider Schema.encodeKeys. + +- `VariantSchema.fromKey.Rename` -> `none`: The fromKey rename helper was not ported; for whole-struct encoded-key renaming consider Schema.encodeKeys. + +- `VariantSchema.make` -> `effect/unstable/schema/VariantSchema#make`: Import make from the v4 module; FieldOnly and FieldExcept take one key array and Union takes one member array. + +### `@effect/opentelemetry/Logger` + +- `Logger.layerLoggerAdd` -> `OtelLogger.layer({ mergeWithExisting: true })`: The Logger module was renamed to OtelLogger; logger installation is now one configurable layer, with true preserving the v3 additive behavior. + +- `Logger.layerLoggerReplace` -> `OtelLogger.layer({ mergeWithExisting: false })`: The Logger module was renamed to OtelLogger; logger installation is now one configurable layer, with false replacing existing loggers. + +- `Logger.make` -> `OtelLogger.make`: The constructor remains in the renamed OtelLogger module. + +### `@effect/opentelemetry/Metrics` + +- `Metrics.layer` -> `OtelMetrics.layer`: The Metrics module was renamed to OtelMetrics; the layer remains and now also accepts an optional temporality setting. + +### `@effect/opentelemetry/NodeSdk` + +- `NodeSdk.Configuration` -> `NodeSdk.Configuration`: The configuration interface remains; v4 adds metricTemporality and loggerMergeWithExisting options. + +### `@effect/opentelemetry/Otlp` + +- `Otlp.layer` -> `Otlp.layer`: Moved to effect/unstable/observability/Otlp; replaceLogger was replaced by loggerMergeWithExisting, and metricsTemporality is now configurable. + +### `@effect/opentelemetry/OtlpLogger` + +- `OtlpLogger.layer` -> `OtlpLogger.layer`: Moved to effect/unstable/observability/OtlpLogger; use mergeWithExisting instead of passing replaceLogger. + +- `OtlpLogger.make` -> `OtlpLogger.make`: The constructor remains in the module moved to effect/unstable/observability/OtlpLogger. + +### `@effect/opentelemetry/OtlpMetrics` + +- `OtlpMetrics.layer` -> `OtlpMetrics.layer`: Moved to effect/unstable/observability/OtlpMetrics; the layer now also accepts optional cumulative or delta temporality. + +- `OtlpMetrics.make` -> `OtlpMetrics.make`: Moved to effect/unstable/observability/OtlpMetrics; the constructor now also accepts optional cumulative or delta temporality. + +### `@effect/opentelemetry/OtlpResource` + +- `OtlpResource.unsafeServiceName` -> `OtlpResource.serviceNameUnsafe`: Moved to effect/unstable/observability/OtlpResource and renamed to follow the v4 unsafe-suffix convention. + +### `@effect/opentelemetry/OtlpTracer` + +- `OtlpTracer.layer` -> `OtlpTracer.layer`: The layer remains in the module moved to effect/unstable/observability/OtlpTracer. + +- `OtlpTracer.make` -> `OtlpTracer.make`: The constructor remains in the module moved to effect/unstable/observability/OtlpTracer. + +### `@effect/opentelemetry/Resource` + +- `Resource.Resource` -> `Resource.Resource`: The service remains in @effect/opentelemetry/Resource but is now a Context.Service class rather than a separate Tag interface and value. + +### `@effect/opentelemetry/Tracer` + +- `Tracer.OtelTraceFlags` -> `OtelTracer.OtelTraceFlags`: The service moved with the module and is now declared as a Context.Service class. + +- `Tracer.OtelTraceState` -> `OtelTracer.OtelTraceState`: The service moved with the module and is now declared as a Context.Service class. + +- `Tracer.OtelTracer` -> `OtelTracer.OtelTracer`: The service moved with the renamed module and is now declared as a Context.Service class. + +- `Tracer.OtelTracerProvider` -> `OtelTracer.OtelTracerProvider`: The service moved with the renamed module and is now declared as a Context.Service class. + +- `Tracer.layer` -> `OtelTracer.layer`: The Tracer module was renamed to OtelTracer; this still creates an OpenTelemetry tracer and installs it as Effect's tracer. + +- `Tracer.layerTracer` -> `OtelTracer.layerTracer`: The Tracer module was renamed to OtelTracer; this layer still creates only the OpenTelemetry tracer service. + +- `Tracer.make` -> `OtelTracer.make`: The constructor remains in the renamed OtelTracer module. + +### `@effect/opentelemetry/WebSdk` + +- `WebSdk.Configuration` -> `WebSdk.Configuration`: The configuration interface remains; v4 adds metricTemporality and loggerMergeWithExisting options. + +### `@effect/platform-browser/BrowserHttpClient` + +- `BrowserHttpClient.currentXHRResponseType` -> `BrowserHttpClient.CurrentXHRResponseType`: The FiberRef became a defaulted Context.Reference; use withXHRArrayBuffer or provide the reference as a service. + +### `@effect/platform-browser/BrowserWorker` + +- `BrowserWorker.layerManager` -> `BrowserWorker.layerPlatform`: WorkerManager was removed. Provide WorkerPlatform directly, or use BrowserWorker.layer(spawn) when a Worker.Spawner is also required. + +- `BrowserWorker.layerWorker` -> `BrowserWorker.layerPlatform`: PlatformWorker became Worker.WorkerPlatform. The platform-only layer no longer takes a spawn callback; BrowserWorker.layer(spawn) combines platform and spawner layers. + +### `@effect/platform-browser/BrowserWorkerRunner` + +- `BrowserWorkerRunner.launch` -> `Layer.launch + RpcServer.layerProtocolWorkerRunner`: The close-latch launcher was removed. Compose BrowserWorkerRunner.layer with the worker RPC server protocol layer and launch the resulting handler layer. + +### `@effect/platform-browser/Clipboard` + +- `Clipboard.Clipboard` -> `Clipboard.Clipboard`: The service remains, now as a Context.Service with a private brand; normal access and provision are unchanged. + +- `Clipboard.ErrorTypeId` -> `none`: The error marker is private in v4; discriminate ClipboardError by its \_tag instead. + +- `Clipboard.TypeId` -> `none`: The service brand is private in v4; use the Clipboard Context.Service value. + +### `@effect/platform-browser/Geolocation` + +- `Geolocation.ErrorTypeId` -> `none`: The error marker is private in v4; discriminate GeolocationError and its tagged reason. + +- `Geolocation.Geolocation` -> `Geolocation.Geolocation`: The service remains, now as a Context.Service with a private brand. + +- `Geolocation.GeolocationError` -> `Geolocation.GeolocationError`: The class remains, but reason is now PositionUnavailable, PermissionDenied, or Timeout, with the cause stored on that tagged reason. + +- `Geolocation.TypeId` -> `none`: The service marker is private in v4; use the Geolocation Context.Service value. + +### `@effect/platform-browser/Permissions` + +- `Permissions.ErrorTypeId` -> `none`: The error marker is private in v4; discriminate PermissionsError and its tagged reason. + +- `Permissions.Permissions` -> `Permissions.Permissions`: The query service remains, now as a Context.Service with a private brand. + +- `Permissions.PermissionsError` -> `Permissions.PermissionsError`: The class remains, but reason is now PermissionsInvalidStateError or PermissionsTypeError, with the cause stored on that tagged reason. + +- `Permissions.TypeId` -> `none`: The service marker is private in v4; use the Permissions Context.Service value. + +### `@effect/platform-bun/BunCommandExecutor` + +- `BunCommandExecutor.layer` -> `BunChildProcessSpawner.layer`: CommandExecutor became effect/unstable/process/ChildProcessSpawner; the Bun adapter was renamed and still requires FileSystem and Path. + +### `@effect/platform-bun/BunContext` + +- `BunContext.BunContext` -> `BunServices.BunServices`: The aggregate was renamed and now provides ChildProcessSpawner, Crypto, FileSystem, Path, Stdio, and Terminal; add BunWorker separately when needed. + +- `BunContext.layer` -> `BunServices.layer`: Use the renamed aggregate layer; worker services are no longer included. + +### `@effect/platform-bun/BunFileSystem/ParcelWatcher` + +- `ParcelWatcher.layer` -> `BunFileSystem.layer`: The Parcel watcher adapter was removed. BunFileSystem.layer uses the built-in node:fs-compatible watcher; provide a custom FileSystem.WatchBackend for specialized behavior. + +### `@effect/platform-bun/BunHttpPlatform` + +- `BunHttpPlatform.make` -> `BunHttpPlatform.layer`: The Bun-specific constructor is private; provide the public layer and consume HttpPlatform.HttpPlatform. + +### `@effect/platform-bun/BunHttpServer` + +- `BunHttpServer.ServeOptions` -> `BunHttpServer.ServeOptions`: The alias remains, but R is now a route-key string union and routes uses Bun.Serve.Routes; update old route-map generic arguments or infer R from routes. + +- `BunHttpServer.layerContext` -> `BunHttpServer.layerHttpServices`: Direct rename; it provides HttpPlatform, Etag.Generator, and BunServices. + +### `@effect/platform-bun/BunHttpServerRequest` + +- `BunHttpServerRequest.toRequest` -> `BunHttpServerRequest.toBunServerRequest`: Direct rename with the more precise Bun.BunRequest result type. + +### `@effect/platform-bun/BunSink` + +- `BunSink.stderr` -> `stdio.stderr() from Stdio.Stdio`: Process stdio moved behind effect/Stdio; provide BunStdio.layer. stderr remains a Sink and can be configured with endOnDone. + +- `BunSink.stdin` -> `stdio.stdin from Stdio.Stdio`: Standard input is correctly modeled as a Stream in v4, not a writable Sink; manually adapt process.stdin only if writing to it was intentional. + +- `BunSink.stdout` -> `stdio.stdout() from Stdio.Stdio`: Process stdio moved behind effect/Stdio; provide BunStdio.layer. stdout remains a Sink. + +### `@effect/platform-bun/BunSocket` + +- `BunSocket.NetSocket` -> `BunSocket.NetSocket`: The identifier remains, but the old interface/tag pair is now one Context.Service for node:net.Socket. + +### `@effect/platform-bun/BunSocketServer` + +- `BunSocketServer.IncomingMessage` -> `BunSocketServer.IncomingMessage`: The identifier remains and is now a Context.Service for node:http.IncomingMessage. + +### `@effect/platform-bun/BunStream` + +- `BunStream.FromReadableOptions` -> `Pick[0], "chunkSize" | "closeOnDone">`: The named interface was inlined into the constructor options; chunkSize is now a number and the full options also contain evaluate, onError, and bufferSize. + +- `BunStream.FromWritableOptions` -> `Pick[0], "endOnDone" | "encoding">`: The named interface was inlined into BunSink.fromWritable and duplex constructor options. + +- `BunStream.stderr` -> `stdio.stderr() from Stdio.Stdio`: Standard error is correctly modeled as a Sink in v4. Explicitly adapt process.stderr with BunStream.fromReadable only to preserve the old unusual read behavior. + +- `BunStream.stdin` -> `stdio.stdin from Stdio.Stdio`: Standard input moved to the Stdio service; provide BunStdio.layer. Its stream exposes PlatformError instead of dying. + +- `BunStream.stdout` -> `stdio.stdout() from Stdio.Stdio`: Standard output is correctly modeled as a Sink in v4. Explicitly adapt process.stdout with BunStream.fromReadable only to preserve the old unusual read behavior. + +### `@effect/platform-bun/BunWorker` + +- `BunWorker.layerManager` -> `BunWorker.layerPlatform`: WorkerManager was removed. Provide WorkerPlatform directly, or use BunWorker.layer(spawn) when a Worker.Spawner is also required. + +- `BunWorker.layerWorker` -> `BunWorker.layerPlatform`: PlatformWorker became Worker.WorkerPlatform; BunWorker.layer(spawn) combines the platform and spawner layers. + +### `@effect/platform-node-shared/NodeCommandExecutor` + +- `NodeCommandExecutor.layer` -> `NodeChildProcessSpawner.layer`: CommandExecutor became effect/unstable/process/ChildProcessSpawner; the Node adapter was renamed and still requires FileSystem and Path. + +### `@effect/platform-node-shared/NodeFileSystem/ParcelWatcher` + +- `ParcelWatcher.layer` -> `NodeFileSystem.layer`: The Parcel watcher adapter was removed. NodeFileSystem.layer uses node:fs.watch; provide a custom FileSystem.WatchBackend for specialized behavior. + +### `@effect/platform-node-shared/NodeMultipart` + +- `NodeMultipart.fileToReadable` -> `@effect/platform-node/NodeMultipart#fileToReadable`: The Node multipart implementation moved from @effect/platform-node-shared to @effect/platform-node; its behavior remains. + +- `NodeMultipart.stream` -> `@effect/platform-node/NodeMultipart#stream`: The Node multipart implementation moved from @effect/platform-node-shared to @effect/platform-node; the source and headers call shape remains. + +### `@effect/platform-node-shared/NodeSink` + +- `NodeSink.stderr` -> `stdio.stderr() from Stdio.Stdio`: Standard error moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer. + +- `NodeSink.stdin` -> `NodeSink.fromWritable({ evaluate: () => process.stdin, onError: ... })`: There is no Stdio sink because stdin is a readable stream in v4; use a manual adapter only if writing to process.stdin was intentional. + +- `NodeSink.stdout` -> `stdio.stdout() from Stdio.Stdio`: Standard output moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer. + +### `@effect/platform-node-shared/NodeSocket` + +- `NodeSocket.NetSocket` -> `NodeSocket.NetSocket`: The identifier remains, but the old interface/tag pair is now one Context.Service; use NodeSocket.NetSocket["Service"] for the node:net.Socket value type. + +### `@effect/platform-node-shared/NodeStream` + +- `NodeStream.FromReadableOptions` -> `{ readonly chunkSize?: number; readonly closeOnDone?: boolean }`: The named interface was removed and its fields were inlined into readable constructor options; chunkSize narrowed from SizeInput to number. + +- `NodeStream.FromWritableOptions` -> `{ readonly endOnDone?: boolean; readonly encoding?: BufferEncoding }`: The named interface was removed and its fields were inlined into NodeSink and duplex constructor options. + +- `NodeStream.stderr` -> `NodeStream.fromReadable({ evaluate: () => process.stderr, closeOnDone: false }).pipe(Stream.orDie)`: This preserves the unusual v3 read behavior; for normal error output use the stdio.stderr() Sink from effect/Stdio. + +- `NodeStream.stdin` -> `stdio.stdin from Stdio.Stdio`: Standard input moved to effect/Stdio; provide NodeStdio.layer or NodeServices.layer. The service stream exposes PlatformError instead of dying. + +- `NodeStream.stdout` -> `NodeStream.fromReadable({ evaluate: () => process.stdout, closeOnDone: false }).pipe(Stream.orDie)`: This preserves the unusual v3 read behavior; for normal output use the stdio.stdout() Sink from effect/Stdio. + +### `@effect/platform-node/NodeCommandExecutor` + +- `NodeCommandExecutor.layer` -> `NodeChildProcessSpawner.layer`: CommandExecutor became ChildProcessSpawner; use the @effect/platform-node/NodeChildProcessSpawner re-export. + +### `@effect/platform-node/NodeContext` + +- `NodeContext.NodeContext` -> `NodeServices.NodeServices`: Use the renamed service union; it replaces CommandExecutor with ChildProcessSpawner, adds Crypto and Stdio, and omits WorkerManager. + +- `NodeContext.layer` -> `NodeServices.layer`: The aggregate was renamed and now provides ChildProcessSpawner, Crypto, FileSystem, Path, Stdio, and Terminal; add NodeWorker separately when needed. + +### `@effect/platform-node/NodeFileSystem/ParcelWatcher` + +- `ParcelWatcher.layer` -> `NodeFileSystem.layer`: The Parcel watcher adapter was removed. Native node:fs.watch support is built in; FileSystem.WatchBackend is the extension point. + +### `@effect/platform-node/NodeHttpClient` + +- `NodeHttpClient.Dispatcher` -> `NodeHttpClient.Dispatcher`: The identifier remains but is now a Context.Service class; use Dispatcher["Service"] for the concrete Undici dispatcher type. + +- `NodeHttpClient.HttpAgent` -> `NodeHttpClient.HttpAgent`: The identifier remains but is now a Context.Service class; use HttpAgent["Service"] for the concrete http/https agent pair. + +- `NodeHttpClient.HttpAgentTypeId` -> `none`: The public marker was removed; the HttpAgent Context.Service class supplies service identity. + +- `NodeHttpClient.UndiciRequestOptions` -> `NodeHttpClient.UndiciOptions`: The required Context.Tag became a defaulted Context.Reference\\>; override it with Effect.provideService. + +- `NodeHttpClient.agentLayer` -> `NodeHttpClient.layerAgent`: Direct rename; it provides the default scoped Node HTTP and HTTPS agents. + +- `NodeHttpClient.dispatcherLayer` -> `NodeHttpClient.layerDispatcher`: Direct rename; the layer owns and finalizes a scoped Undici Agent. + +- `NodeHttpClient.layer` -> `NodeHttpClient.layerNodeHttp`: Use the renamed node:http/node:https backend layer; choose layerUndici only when intentionally changing backends. + +- `NodeHttpClient.layerUndiciWithoutDispatcher` -> `NodeHttpClient.layerUndiciNoDispatcher`: Direct rename; the layer still requires NodeHttpClient.Dispatcher. + +- `NodeHttpClient.layerWithoutAgent` -> `NodeHttpClient.layerNodeHttpNoAgent`: Direct rename; the node:http client layer still requires NodeHttpClient.HttpAgent. + +- `NodeHttpClient.make` -> `NodeHttpClient.makeNodeHttp`: Direct rename of the node:http/node:https client constructor. + +- `NodeHttpClient.makeAgentLayer` -> `NodeHttpClient.layerAgentOptions`: Direct rename; it accepts Https.AgentOptions and scopes both agents. + +### `@effect/platform-node/NodeHttpServer` + +- `NodeHttpServer.layerContext` -> `NodeHttpServer.layerHttpServices`: Direct rename; it provides NodeServices, HttpPlatform, and Etag.Generator, without the removed WorkerManager. + +### `@effect/platform-node/NodeWorker` + +- `NodeWorker.layerManager` -> `NodeWorker.layerPlatform`: WorkerManager was removed. Provide WorkerPlatform directly, or use NodeWorker.layer(spawn) when a Worker.Spawner is also required. + +- `NodeWorker.layerWorker` -> `NodeWorker.layerPlatform`: PlatformWorker became Worker.WorkerPlatform; NodeWorker.layer(spawn) combines the platform and spawner layers. + +### `@effect/platform-node/Undici` + +- `Undici.Agent` -> `undici.Agent`: Import the upstream Agent directly. Undici 8 removes maxRedirections and option-level interceptors, adds maxOrigins, and enables HTTP/2 negotiation unless allowH2 is false. + +- `Undici.Agent.DispatchOptions` -> `undici.Agent.DispatchOptions`: Import the same Agent namespace type from undici; Undici 8 removes maxRedirections. + +- `Undici.Agent.Options` -> `undici.Agent.Options`: Import the same Agent namespace type; Undici 8 removes maxRedirections and option-level interceptors, adds maxOrigins, and uses dispatcher.compose for interceptors. + +- `Undici.Client` -> `undici.Client`: Import the upstream Client directly; custom handlers must use Undici 8's controller-based v2 handler API. + +- `Undici.Client.Options` -> `undici.Client.Options`: Import the same Client namespace type; Undici 8 removes maxRedirections and option-level interceptors and adds WebSocket and HTTP/2 options. + +- `Undici.Client.OptionsInterceptors` -> `undici.Dispatcher.DispatcherComposeInterceptor + dispatcher.compose`: Undici 8 removed option-level interceptor tuples; keep DispatcherComposeInterceptor functions and apply them after construction with dispatcher.compose(...). + +- `Undici.DiagnosticsChannel` -> `undici.DiagnosticsChannel`: Import this type-only namespace from undici; subscribe at runtime through node:diagnostics\_channel using Undici's channel names. + +- `Undici.DiagnosticsChannel.ClientConnectErrorMessage` -> `undici.DiagnosticsChannel.ClientConnectErrorMessage`: Import the same type-only namespace member from undici; runtime delivery uses node:diagnostics\_channel. + +- `Undici.DiagnosticsChannel.Error` -> `Error`: Undici 8 removed this unknown alias; diagnostic error fields now use the built-in Error type. + +- `Undici.DiagnosticsChannel.RequestErrorMessage` -> `undici.DiagnosticsChannel.RequestErrorMessage`: Import the same type-only namespace member; its error field is the built-in Error type in Undici 8. + +- `Undici.Dispatcher` -> `undici.Dispatcher`: Import the upstream Dispatcher directly; custom dispatchers must adopt Undici 8's controller-based v2 handler API. + +- `Undici.Dispatcher.ConnectOptions` -> `undici.Dispatcher.ConnectOptions`: Import the same Dispatcher namespace type; Undici 8 removes maxRedirections and redirectionLimitReached. + +- `Undici.Dispatcher.DispatchHandler` -> `undici.Dispatcher.DispatchHandler`: Use Undici 8's onRequestStart/onResponseStart/onResponseData/onResponseEnd/onResponseError callbacks and controller pause/resume/abort methods. + +- `Undici.Dispatcher.DispatchOptions` -> `undici.Dispatcher.DispatchOptions`: Import the same namespace type; Undici 8 removes throwOnError, adds typeOfService, and handles redirects through composed interceptors. + +- `Undici.Dispatcher.RequestOptions` -> `undici.Dispatcher.RequestOptions`: Import the same namespace type; Undici 8 removes maxRedirections and redirectionLimitReached, so compose a redirect interceptor when needed. + +- `Undici.Dispatcher.UpgradeOptions` -> `undici.Dispatcher.UpgradeOptions`: Import the same namespace type; Undici 8 removes maxRedirections and redirectionLimitReached. + +- `Undici.H2CClient` -> `undici.H2CClient`: Import the upstream cleartext HTTP/2 client directly; callbacks follow Undici 8's handler API. + +- `Undici.H2CClient.Options` -> `undici.H2CClient.Options`: Import the same H2CClient namespace type; Undici 8 removes maxRedirections. + +- `Undici.MessageEvent` -> `undici.MessageEvent`: Import Undici's named constructor/type directly to preserve the installed package identity. + +- `Undici.MessageEventInit` -> `undici.MessageEventInit`: Import the upstream type directly; message ports and source use MessagePort instances in Undici 8. + +- `Undici.Pool` -> `undici.Pool`: Import the upstream Pool directly and apply interceptors after construction with pool.compose(...). + +- `Undici.Pool.Options` -> `undici.Pool.Options`: Import the same Pool namespace type; Undici 8 removes the interceptors option in favor of pool.compose(...). + +- `Undici.ProxyAgent` -> `undici.ProxyAgent`: Import the upstream ProxyAgent directly; inherited Agent options and handlers follow Undici 8. + +- `Undici.ProxyAgent.Options` -> `undici.ProxyAgent.Options`: Import the same ProxyAgent namespace type; Undici 8 types proxy headers as OutgoingHttpHeaders. + +- `Undici.RedirectHandler` -> `undici.RedirectHandler`: Import the upstream class; Undici 8 removes redirectionLimitReached from the constructor and adds static buildDispatch. + +- `Undici.Request` -> `undici.Request`: Import Undici's named Request directly; clone is a method in Undici 8. + +- `Undici.Response` -> `undici.Response`: Import Undici's named Response directly; clone is a method and Response.redirect status is optional in Undici 8. + +- `Undici.SpecIterable` -> `undici.SpecIterable`: Import the upstream type directly; its iterator returns SpecIterableIterator in Undici 8. + +- `Undici.SpecIterableIterator` -> `undici.SpecIterableIterator`: Import the upstream type directly; it extends SpecIteratorObject and includes iterator-helper methods in Undici 8. + +- `Undici.buildConnector` -> `undici.buildConnector`: Import the upstream function directly; Undici 8 expands connector options with preferH2, typeOfService, and socketPath handling. + +- `Undici.buildConnector.BuildOptions` -> `undici.buildConnector.BuildOptions`: Import the same buildConnector namespace type; Undici 8 adds preferH2 and typeOfService. + +- `Undici.default.cacheStores` -> `undici.cacheStores`: Use Undici 8's named cacheStores export instead of reaching through the default aggregate. + +- `Undici.deleteCookie` -> `undici.deleteCookie`: Import the upstream function directly; its optional attributes use path and domain and no longer include name. + +- `Undici.errors` -> `undici.errors`: Import the upstream errors object directly; individual classes follow the Undici 8 API. + +- `Undici.errors.ResponseStatusCodeError` -> `undici.errors.ResponseError`: Undici 8 replaced ResponseStatusCodeError with ResponseError; construct it with message, statusCode, and the headers/body object. + +- `Undici.interceptors` -> `undici.interceptors`: Import the upstream interceptors object and apply returned interceptors with dispatcher.compose(...). + +- `Undici.interceptors.DNSInterceptorOpts` -> `undici.interceptors.DNSInterceptorOpts`: Import the same namespace type; Undici 8 lookup receives an origin URL and supports optional DNS storage. + +- `Undici.interceptors.DNSInterceptorOriginRecords` -> `undici.interceptors.DNSInterceptorOriginRecords`: Import the same namespace type, but adopt Undici 8's shape with IPv4 and IPv6 entries nested under records. + +- `Undici.interceptors.RedirectInterceptorOpts` -> `undici.interceptors.RedirectInterceptorOpts`: Import the same namespace type; Undici 8 adds throwOnMaxRedirect and redirect header-stripping options. + +### `@effect/platform/ChannelSchema` + +- `ChannelSchema.decode` -> `ChannelSchema.decode`: The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model. + +- `ChannelSchema.duplex` -> `ChannelSchema.duplex`: The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model. + +- `ChannelSchema.encode` -> `ChannelSchema.encode`: The API moved to effect/ChannelSchema; update the import and adapt the schema to the v4 Schema.Constraint model. + +### `@effect/platform/Command` + +- `Command.Command` -> `ChildProcess.Command`: Commands moved to effect/unstable/process/ChildProcess and are now directly Effectable to spawn a ChildProcessHandle. + +- `Command.Command.Input` -> `ChildProcess.CommandInput`: The standard-input configuration type was flattened out of the Command namespace. + +- `Command.Command.Proto` -> `ChildProcess.StandardCommand | ChildProcess.PipedCommand`: The public command prototype was removed; narrow the Command union to its StandardCommand or PipedCommand interfaces. + +- `Command.CommandTypeId` -> `none`: The command type-id alias is internal in v4; use ChildProcess.Command or ChildProcess.isCommand instead. + +- `Command.env` -> `ChildProcess.setEnv`: Use the renamed command combinator. + +- `Command.exitCode` -> `ChildProcessSpawner.ChildProcessSpawner.exitCode`: Obtain the ChildProcessSpawner service and call exitCode, or spawn the Effectable command and read the handle exitCode. + +- `Command.feed` -> `ChildProcess.CommandOptions["stdin"]`: The feed combinator was removed; pass a Stream as stdin when constructing the command. + +- `Command.flatten` -> `none`: No flatten helper remains; inspect StandardCommand and PipedCommand recursively when command structure is required. + +- `Command.lines` -> `ChildProcessSpawner.ChildProcessSpawner.lines`: Output collection moved onto the ChildProcessSpawner service. + +- `Command.runInShell` -> `ChildProcess.CommandOptions["shell"]`: Set shell when calling ChildProcess.make; there is no post-construction shell combinator. + +- `Command.start` -> `ChildProcessSpawner.ChildProcessSpawner.spawn`: Use the spawner service, or yield the Effectable ChildProcess.Command directly, to obtain a ChildProcessHandle. + +- `Command.stderr` -> `ChildProcess.CommandOptions["stderr"]`: Configure stderr in ChildProcess.make options; the standalone combinator was removed. + +- `Command.stdin` -> `ChildProcess.CommandOptions["stdin"]`: Configure stdin in ChildProcess.make options; the standalone combinator was removed. + +- `Command.stdout` -> `ChildProcess.CommandOptions["stdout"]`: Configure stdout in ChildProcess.make options; the standalone combinator was removed. + +- `Command.stream` -> `ChildProcessSpawner.ChildProcessSpawner.spawn + ChildProcessHandle.stdout`: Spawn within a scope and consume the returned handle's stdout stream. + +- `Command.streamLines` -> `ChildProcessSpawner.ChildProcessSpawner.streamLines`: Text-line streaming moved onto the ChildProcessSpawner service. + +- `Command.string` -> `ChildProcessSpawner.ChildProcessSpawner.string`: Output collection moved onto the ChildProcessSpawner service. + +- `Command.workingDirectory` -> `ChildProcess.setCwd`: Use the renamed command combinator. + +### `@effect/platform/CommandExecutor` + +- `CommandExecutor.CommandExecutor` -> `ChildProcessSpawner.ChildProcessSpawner`: The executor service moved to effect/unstable/process/ChildProcessSpawner and was renamed. + +- `CommandExecutor.Process` -> `ChildProcessSpawner.ChildProcessHandle`: Running-process handles were renamed and moved to ChildProcessSpawner. + +- `CommandExecutor.Process.Id` -> `ChildProcessSpawner.ProcessId`: The process-id brand is now exported directly. + +- `CommandExecutor.ProcessTypeId` -> `none`: The ChildProcessHandle marker is internal in v4; use the ChildProcessHandle interface. + +- `CommandExecutor.TypeId` -> `none`: The Context.Service class replaces the public executor type-id alias. + +- `CommandExecutor.makeExecutor` -> `ChildProcessSpawner.make`: Use the renamed constructor; it derives output helpers from a spawn implementation. + +### `@effect/platform/Cookies` + +- `Cookies.CookieTypeId` -> `Cookies.isCookie`: The cookie brand is private in v4; use the public refinement instead of reading the type-id symbol. + +- `Cookies.ErrorTypeId` -> `Cookies.CookiesError`: The error brand is private in v4; identify the exported error class instead. + +- `Cookies.TypeId` -> `Cookies.isCookies`: The collection brand is private in v4; use the public refinement instead. + +- `Cookies.unsafeMakeCookie` -> `Cookies.makeCookieUnsafe`: Renamed to put Unsafe last; it still throws on invalid cookie data. + +- `Cookies.unsafeSet` -> `Cookies.setUnsafe`: Renamed to put Unsafe last; the dual throwing behavior is retained. + +- `Cookies.unsafeSetAll` -> `Cookies.setAllUnsafe`: Renamed to put Unsafe last; the dual all-or-throw behavior is retained. + +### `@effect/platform/Error` + +- `Error.Module` -> `string`: The closed module-name Schema was removed; PlatformError reason records accept any module string. + +- `Error.PlatformError` -> `PlatformError.PlatformError`: The module moved to effect/PlatformError and PlatformError became a wrapper class around BadArgument or SystemError. + +- `Error.SystemErrorReason` -> `PlatformError.SystemErrorTag`: The normalized system-error reason union was renamed. + +- `Error.TypeId` -> `none`: The PlatformError runtime marker is internal in v4; use the PlatformError class/tag. + +- `Error.TypeIdError` -> `Data.TaggedError or Schema.Error`: The platform-specific error-class factory was removed; define tagged data errors or schema-backed error classes directly. + +- `Error.isPlatformError` -> `value instanceof PlatformError.PlatformError`: PlatformError is a class in v4; use an instanceof check or match its PlatformError tag. + +### `@effect/platform/Etag` + +- `Etag.GeneratorTypeId` -> `Etag.Generator`: The standalone generator brand was removed; Generator is now a Context.Service class. + +- `Etag.layer` -> `Etag.layer`: Retained; it still provides the strong metadata-based ETag Generator service. + +### `@effect/platform/FetchHttpClient` + +- `FetchHttpClient.Fetch` -> `FetchHttpClient.Fetch`: Retained as a Context.Reference that defaults to globalThis.fetch. + +- `FetchHttpClient.layer` -> `FetchHttpClient.layer`: Retained as the HttpClient layer backed by the configured Fetch reference. + +### `@effect/platform/FileSystem` + +- `FileSystem.AccessFileOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.CopyOptions` -> `NonNullable[2]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.File.Descriptor` -> `none`: Native file descriptors are no longer part of the portable File interface; use the File methods and keep any platform handle private in custom implementations. + +- `FileSystem.FileDescriptor` -> `none`: The descriptor branding constructor was removed with the public fd field; use File operations instead of exposing a native descriptor. + +- `FileSystem.FileTypeId` -> `typeof FileSystem.FileTypeId`: The runtime marker remains exported, but the separate type alias was removed. + +- `FileSystem.MakeDirectoryOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.MakeTempDirectoryOptions` -> `NonNullable[0]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.MakeTempFileOptions` -> `NonNullable[0]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.OpenFileOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.ReadDirectoryOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.RemoveOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.SinkOptions` -> `NonNullable[1]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.StreamOptions` -> `NonNullable[1]>`: Stream options are inline; bufferSize was removed while bytesToRead, chunkSize, and offset remain. + +- `FileSystem.WatchEventCreate` -> `FileSystem.WatchEvent.Create`: The constructor was removed; construct a tagged object with \_tag: "Create" and path. + +- `FileSystem.WatchEventRemove` -> `FileSystem.WatchEvent.Remove`: The constructor was removed; construct a tagged object with \_tag: "Remove" and path. + +- `FileSystem.WatchEventUpdate` -> `FileSystem.WatchEvent.Update`: The constructor was removed; construct a tagged object with \_tag: "Update" and path. + +- `FileSystem.WatchOptions` -> `FileSystem.WatchOptions`: Retained after moving the module to effect/FileSystem; pass `{ recursive: true }` as the optional second argument to FileSystem.watch. + +- `FileSystem.WriteFileOptions` -> `NonNullable[2]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.WriteFileStringOptions` -> `NonNullable[2]>`: Operation option interfaces are inline in the v4 FileSystem service. + +- `FileSystem.layerNoop` -> `FileSystem.layerNoop`: The helper remains after moving the module to effect/FileSystem. + +- `FileSystem.make` -> `FileSystem.make`: The constructor remains after moving the module to effect/FileSystem; adapt the implementation to the v4 service shape. + +### `@effect/platform/Headers` + +- `Headers.Headers` -> `Headers.Headers`: Import Headers from effect/unstable/http; the immutable string-record interface is retained with its v4 TypeId brand. + +- `Headers.HeadersTypeId` -> `Headers.TypeId`: The public Headers type-id symbol was renamed from HeadersTypeId to TypeId. + +- `Headers.currentRedactedNames` -> `Headers.CurrentRedactedNames`: Renamed and changed from FiberRef to Context.Reference; override it with service provisioning. + +- `Headers.remove` -> `Headers.remove / Headers.removeMany`: Use remove for one name or removeMany for an iterable; RegExp removal requires enumerating matching names. + +- `Headers.schema` -> `Headers.HeadersSchema`: The encoded-record and self schemas were consolidated into HeadersSchema. + +- `Headers.schemaFromSelf` -> `Headers.HeadersSchema`: The encoded-record and self schemas were consolidated into HeadersSchema. + +- `Headers.unsafeFromRecord` -> `Headers.fromRecordUnsafe`: Renamed to put Unsafe last; it still skips name normalization. + +### `@effect/platform/HttpApi` + +- `HttpApi.Api` -> `none`: The Context tag carrying the API was removed. Pass the HttpApi value explicitly to builders and clients. + +- `HttpApi.HttpApi.Any` -> `effect/unstable/httpapi/HttpApi#Constraint`: Use the erased marker constraint when only HttpApi identity is needed. + +- `HttpApi.HttpApi.AnyWithProps` -> `effect/unstable/httpapi/HttpApi#Top`: Use the widened HttpApi type that retains runtime properties. + +- `HttpApi.TypeId` -> `none`: The marker is private in v4; use HttpApi.isHttpApi for runtime narrowing and Constraint or Top for types. + +- `HttpApi.make` -> `effect/unstable/httpapi/HttpApi#make`: The constructor remains, but API-wide error and service parameters were removed; declare errors on endpoints and attach middleware. + +### `@effect/platform/HttpApiBuilder` + +- `HttpApiBuilder.Handlers` -> `effect/unstable/httpapi/HttpApiBuilder#Handlers`: Handlers now tracks an endpoint map and handled identifiers. Prefer Handlers.FromGroup\. + +- `HttpApiBuilder.Handlers.Error` -> `effect/unstable/httpapi/HttpApiBuilder#Handlers.Error`: The helper remains and extracts the error channel of an effectful group-builder return. + +- `HttpApiBuilder.Handlers.Middleware` -> `none`: The handler-internal HttpApp middleware alias was removed. Use HttpRouter.middleware inference or HttpRouter.middleware.Fn. + +- `HttpApiBuilder.Handlers.ValidateReturn` -> `effect/unstable/httpapi/HttpApiBuilder#Handlers.ValidateReturn`: The validator remains and now checks the endpoint map against handled identifiers. + +- `HttpApiBuilder.HandlersTypeId` -> `none`: The exported symbol was removed; do not inspect or construct the private Handlers marker. + +- `HttpApiBuilder.MiddlewareFn` -> `effect/unstable/http/HttpRouter#middleware.Fn`: HTTP apps are Effects in v4; use the router middleware function type or infer it through HttpRouter.middleware. + +- `HttpApiBuilder.Router` -> `effect/unstable/http/HttpRouter#HttpRouter`: The API-specific router tag was removed; API and group layers register with the shared HttpRouter service. + +- `HttpApiBuilder.api` -> `effect/unstable/httpapi/HttpApiBuilder#layer`: Use layer(api) and provide the group layers; it registers the completed API with HttpRouter. + +- `HttpApiBuilder.buildMiddleware` -> `none`: API-wide middleware assembly was removed. Declared HttpApiMiddleware services are applied while routes are built; use HttpRouter.middleware for additional middleware. + +- `HttpApiBuilder.group` -> `effect/unstable/httpapi/HttpApiBuilder#group`: The group layer remains; names are now identifiers and API/group global error channels are gone. + +- `HttpApiBuilder.handler` -> `effect/unstable/httpapi/HttpApiBuilder#endpoint`: Use endpoint for a standalone typed endpoint implementation; inside a group pass callbacks to handlers.handle. + +- `HttpApiBuilder.httpApp` -> `effect/unstable/http/HttpRouter#toHttpEffect`: Build the application from the assembled API route layer; HTTP apps are Effects in v4. + +- `HttpApiBuilder.middleware` -> `effect/unstable/http/HttpRouter#middleware`: Use router effect middleware and provide its layer to the API route layer; global middleware can target all router routes. + +- `HttpApiBuilder.middlewareCors` -> `effect/unstable/http/HttpRouter#cors`: Use the router CORS layer, or provide route-scoped HttpMiddleware.cors through HttpRouter.middleware. + +- `HttpApiBuilder.middlewareOpenApi` -> `effect/unstable/httpapi/HttpApiBuilder#layer`: Set openapiPath in layer(api, options). The additionalPropertiesStrategy option was removed. + +- `HttpApiBuilder.toWebHandler` -> `effect/unstable/http/HttpRouter#toWebHandler`: Pass the assembled API route layer to HttpRouter.toWebHandler; the handler and dispose lifecycle is retained. + +### `@effect/platform/HttpApiClient` + +- `HttpApiClient.Client.Method` -> `effect/unstable/httpapi/HttpApiClient#Client.Method`: The type remains without GroupError. Requests use params/query and responseMode instead of path/urlParams and withResponse. + +- `HttpApiClient.endpoint` -> `effect/unstable/httpapi/HttpApiClient#endpoint`: The endpoint client remains, selected by group and endpoint identifiers and using v4 request and responseMode fields. + +- `HttpApiClient.make` -> `effect/unstable/httpapi/HttpApiClient#make`: The generated client remains; errors and services are now derived per endpoint and middleware. + +- `HttpApiClient.makeWith` -> `effect/unstable/httpapi/HttpApiClient#makeWith`: The supplied-HttpClient constructor remains and now requires endpoint client-middleware services. + +### `@effect/platform/HttpApiEndpoint` + +- `HttpApiEndpoint.HttpApiEndpoint` -> `effect/unstable/httpapi/HttpApiEndpoint#HttpApiEndpoint`: The model remains, but its generics now carry path literals, schemas, middleware, and middleware services. + +- `HttpApiEndpoint.HttpApiEndpoint.AddContext` -> `effect/unstable/httpapi/HttpApiEndpoint#AddMiddleware`: Use AddMiddleware to add a middleware identifier and compute its service transformation. + +- `HttpApiEndpoint.HttpApiEndpoint.AddError` -> `none`: Declare error schemas in the endpoint constructor options; the type helper and fluent addError method were removed. + +- `HttpApiEndpoint.HttpApiEndpoint.Constructor` -> `none`: The tagged-template constructor type was removed; use HttpApiEndpoint.make(method)(identifier, path, options?). + +- `HttpApiEndpoint.HttpApiEndpoint.Context` -> `effect/unstable/httpapi/HttpApiEndpoint#ServerServices`: Use ServerServices for handler requirements; middleware IDs and extra requirements have separate extractors. + +- `HttpApiEndpoint.HttpApiEndpoint.ContextWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#ServerServicesWithIdentifier`: Name became Identifier; combine with middleware extractors when the complete handler requirement union is needed. + +- `HttpApiEndpoint.HttpApiEndpoint.Error` -> `effect/unstable/httpapi/HttpApiEndpoint#Errors`: Use Errors for the decoded endpoint and middleware error union; v4 Error extracts the schema. + +- `HttpApiEndpoint.HttpApiEndpoint.ErrorContext` -> `effect/unstable/httpapi/HttpApiEndpoint#ErrorServicesEncode / ErrorServicesDecode`: The single schema context split into server encoding and client decoding services. + +- `HttpApiEndpoint.HttpApiEndpoint.ErrorContextWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#ErrorServicesEncode / ErrorServicesDecode`: Select the endpoint with WithIdentifier, then apply the encode or decode service extractor. + +- `HttpApiEndpoint.HttpApiEndpoint.ErrorWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#ErrorsWithIdentifier`: Renamed for identifier and returns the decoded endpoint plus middleware error union. + +- `HttpApiEndpoint.HttpApiEndpoint.ExcludeName` -> `effect/unstable/httpapi/HttpApiEndpoint#ExcludeIdentifier`: Direct rename from name to identifier. + +- `HttpApiEndpoint.HttpApiEndpoint.ExtractPath` -> `none`: Tagged-template path extraction was removed. Put a params schema or field record in constructor option params. + +- `HttpApiEndpoint.HttpApiEndpoint.HandlerRawWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#HandlerRawWithIdentifier`: Direct rename; raw request fields are now params and query. + +- `HttpApiEndpoint.HttpApiEndpoint.HandlerWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#HandlerWithIdentifier`: Direct rename from name to identifier. + +- `HttpApiEndpoint.HttpApiEndpoint.OptionalTypePropertySignature` -> `none`: Removed with the tagged-template path implementation. + +- `HttpApiEndpoint.HttpApiEndpoint.PathEntries` -> `none`: Removed with tagged-template path extraction; declare endpoint params explicitly. + +- `HttpApiEndpoint.HttpApiEndpoint.PathParsed` -> `effect/unstable/httpapi/HttpApiEndpoint#Params`: Path data became params; Params extracts the schema, so use Params\["Type"] for decoded data. + +- `HttpApiEndpoint.HttpApiEndpoint.Payload` -> `effect/unstable/httpapi/HttpApiEndpoint#Payload`: The name remains but now extracts the schema; use Payload\["Type"] for buffered decoded data. + +- `HttpApiEndpoint.HttpApiEndpoint.Success` -> `effect/unstable/httpapi/HttpApiEndpoint#SuccessWithIdentifier`: Use SuccessWithIdentifier for the decoded, stream-aware result; v4 Success extracts the schema. + +- `HttpApiEndpoint.HttpApiEndpoint.SuccessWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#SuccessWithIdentifier`: Direct rename from name to identifier; the result remains decoded and stream-aware. + +- `HttpApiEndpoint.HttpApiEndpoint.UrlParams` -> `effect/unstable/httpapi/HttpApiEndpoint#Query`: urlParams became query; Query extracts the schema, so use Query\["Type"] for decoded data. + +- `HttpApiEndpoint.HttpApiEndpoint.ValidateHeaders` -> `effect/unstable/httpapi/HttpApiEndpoint#HeadersConstraint`: Validation moved from an intersection helper to a constructor generic constraint. + +- `HttpApiEndpoint.HttpApiEndpoint.ValidateParams` -> `none`: Tagged-template interpolation validation was removed; params are declared explicitly in options.params. + +- `HttpApiEndpoint.HttpApiEndpoint.ValidatePath` -> `effect/unstable/httpapi/HttpApiEndpoint#ParamsConstraint`: path became params and validation is now a constructor constraint. + +- `HttpApiEndpoint.HttpApiEndpoint.ValidatePayload` -> `effect/unstable/httpapi/HttpApiEndpoint#PayloadConstraint`: Payload validation is now a method-sensitive constructor constraint. + +- `HttpApiEndpoint.HttpApiEndpoint.ValidateUrlParams` -> `effect/unstable/httpapi/HttpApiEndpoint#QueryConstraint`: urlParams became query and validation is now a constructor constraint. + +- `HttpApiEndpoint.PathSegment` -> `effect/unstable/http/HttpRouter#PathInput`: Path input moved to the shared router and is generalized to slash-prefixed paths or wildcard. + +- `HttpApiEndpoint.TypeId` -> `none`: The endpoint type ID is private; use HttpApiEndpoint.isHttpApiEndpoint for runtime narrowing. + +- `HttpApiEndpoint.get` -> `effect/unstable/httpapi/HttpApiEndpoint#get`: Use get(identifier, path, options?); tagged templates and fluent schema setters were removed. + +- `HttpApiEndpoint.head` -> `effect/unstable/httpapi/HttpApiEndpoint#head`: Use head(identifier, path, options?); tagged templates and fluent schema setters were removed. + +- `HttpApiEndpoint.make` -> `effect/unstable/httpapi/HttpApiEndpoint#make`: The factory remains but now requires identifier, path, and options and applies codecs unless disabled. + +- `HttpApiEndpoint.options` -> `effect/unstable/httpapi/HttpApiEndpoint#options`: Same HTTP method constructor with the new identifier, path, and options signature. + +- `HttpApiEndpoint.patch` -> `effect/unstable/httpapi/HttpApiEndpoint#patch`: Same HTTP method constructor with the new identifier, path, and options signature. + +- `HttpApiEndpoint.post` -> `effect/unstable/httpapi/HttpApiEndpoint#post`: Same HTTP method constructor with the new identifier, path, and options signature. + +- `HttpApiEndpoint.put` -> `effect/unstable/httpapi/HttpApiEndpoint#put`: Same HTTP method constructor with the new identifier, path, and options signature. + +### `@effect/platform/HttpApiError` + +- `HttpApiError.Forbidden` -> `effect/unstable/httpapi/HttpApiError#ForbiddenNoContent`: Use ForbiddenNoContent to preserve the empty 403 wire schema; Forbidden now has a JSON-tagged body. + +- `HttpApiError.HttpApiDecodeError` -> `effect/unstable/httpapi/HttpApiError#HttpApiSchemaError`: Validation now stores kind and a SchemaError cause and is a defect unless transformed by schema-error middleware. + +- `HttpApiError.Issue` -> `effect/SchemaIssue#Issue`: Structured failures now live at HttpApiSchemaError.cause.issue; format them explicitly when a flat external list is needed. + +- `HttpApiError.TypeId` -> `effect/unstable/httpapi/HttpApiError#HttpApiSchemaErrorTypeId`: The old module symbol is gone; prefer HttpApiSchemaError.is for runtime narrowing. + +### `@effect/platform/HttpApiGroup` + +- `HttpApiGroup.ApiGroup` -> `effect/unstable/httpapi/HttpApiGroup#Service`: Renamed; the service field and type parameter are now identifier rather than name. + +- `HttpApiGroup.HttpApiGroup.AddContext` -> `none`: Groups no longer carry arbitrary context. Use AddMiddleware for middleware service transformations. + +- `HttpApiGroup.HttpApiGroup.Any` -> `effect/unstable/httpapi/HttpApiGroup#Constraint`: Renamed widened structural constraint. + +- `HttpApiGroup.HttpApiGroup.AnyWithProps` -> `effect/unstable/httpapi/HttpApiGroup#Top`: Renamed widened runtime-property type. + +- `HttpApiGroup.HttpApiGroup.ClientContext` -> `effect/unstable/httpapi/HttpApiGroup#ClientServices / ErrorServicesDecode / MiddlewareClient`: Client schema services and required client middleware are separate extractors in v4. + +- `HttpApiGroup.HttpApiGroup.Context` -> `none`: Group error and context generics were removed; derive server requirements from the group's endpoints. + +- `HttpApiGroup.HttpApiGroup.ContextWithName` -> `none`: Select with WithIdentifier and derive endpoint server requirements; groups no longer have a context generic. + +- `HttpApiGroup.HttpApiGroup.EndpointsWithName` -> `effect/unstable/httpapi/HttpApiGroup#EndpointsWithIdentifier`: Direct rename from name to identifier. + +- `HttpApiGroup.HttpApiGroup.Error` -> `none`: Group-level errors were removed. Declare shared errors on each endpoint or through middleware. + +- `HttpApiGroup.HttpApiGroup.ErrorContext` -> `effect/unstable/httpapi/HttpApiGroup#ErrorServicesEncode / ErrorServicesDecode`: The closest endpoint-error aggregate splits server encoding from client decoding services. + +- `HttpApiGroup.HttpApiGroup.ErrorWithName` -> `none`: Group-level errors were removed; select with WithIdentifier and inspect Errors over the selected endpoints. + +- `HttpApiGroup.HttpApiGroup.Middleware` -> `effect/unstable/httpapi/HttpApiEndpoint#Middleware`: Middleware is attached to the endpoints present when group.middleware is called; extract it from group endpoints. + +- `HttpApiGroup.HttpApiGroup.MiddlewareWithName` -> `effect/unstable/httpapi/HttpApiEndpoint#Middleware`: Select the group with WithIdentifier, get its endpoints, then apply the endpoint Middleware extractor. + +- `HttpApiGroup.HttpApiGroup.Provides` -> `effect/unstable/httpapi/HttpApiGroup#MiddlewareProvides`: Renamed; derives provided services from endpoint middleware. + +- `HttpApiGroup.HttpApiGroup.ToService` -> `effect/unstable/httpapi/HttpApiGroup#ToService`: Same role and now produces Service\. + +- `HttpApiGroup.HttpApiGroup.WithName` -> `effect/unstable/httpapi/HttpApiGroup#WithIdentifier`: Direct rename from name to identifier. + +- `HttpApiGroup.TypeId` -> `none`: The group type ID is private; use HttpApiGroup.isHttpApiGroup for runtime narrowing. + +- `HttpApiGroup.make` -> `effect/unstable/httpapi/HttpApiGroup#make`: The constructor remains; group error and context generics are gone and add is variadic. + +### `@effect/platform/HttpApiMiddleware` + +- `HttpApiMiddleware.HttpApiMiddleware` -> `effect/unstable/httpapi/HttpApiMiddleware#HttpApiMiddleware`: The model remains but now wraps the response effect and carries provided services, an error schema, and required services. + +- `HttpApiMiddleware.HttpApiMiddleware.Any` -> `effect/unstable/httpapi/HttpApiMiddleware#AnyService`: Renamed widened middleware service-key shape. + +- `HttpApiMiddleware.HttpApiMiddleware.AnyId` -> `effect/unstable/httpapi/HttpApiMiddleware#AnyId`: Same name; metadata now includes provided and required services, error schema, client error, and client requirement. + +- `HttpApiMiddleware.HttpApiMiddleware.Error` -> `effect/unstable/httpapi/HttpApiMiddleware#Error`: Same name and now derives the decoded type from the configured error schema. + +- `HttpApiMiddleware.HttpApiMiddleware.ErrorContext` -> `effect/unstable/httpapi/HttpApiMiddleware#ErrorServicesEncode / ErrorServicesDecode`: The single schema context split into server encoding and client decoding services. + +- `HttpApiMiddleware.HttpApiMiddleware.Only` -> `Extract`: The helper was removed because middleware IDs are explicit; use Extract when the direct filter is still needed. + +- `HttpApiMiddleware.HttpApiMiddleware.Provides` -> `effect/unstable/httpapi/HttpApiMiddleware#Provides`: Same name and reads the expanded v4 middleware ID metadata. + +- `HttpApiMiddleware.SecurityTypeId` -> `none`: The marker is private; use HttpApiMiddleware.isSecurity. + +- `HttpApiMiddleware.Tag` -> `effect/unstable/httpapi/HttpApiMiddleware#Service`: Renamed and redesigned; use error, requires, provides, clientError, and requiredForClient configuration. + +- `HttpApiMiddleware.TagClass` -> `effect/unstable/httpapi/HttpApiMiddleware#ServiceClass`: Renamed class type with the new two-stage type configuration and wrapping service shape. + +- `HttpApiMiddleware.TagClass.BaseSecurity` -> `effect/unstable/httpapi/HttpApiMiddleware#ServiceClass`: Security is conditional metadata on ServiceClass; there is no separate public base interface. + +- `HttpApiMiddleware.TagClass.Failure` -> `effect/unstable/httpapi/HttpApiMiddleware#Error`: failure terminology became error; apply the extractor to the middleware ID. + +- `HttpApiMiddleware.TagClass.FailureContext` -> `effect/unstable/httpapi/HttpApiMiddleware#ErrorServicesEncode / ErrorServicesDecode`: Failure schema services split by server encoding and client decoding direction. + +- `HttpApiMiddleware.TagClass.FailureSchema` -> `effect/unstable/httpapi/HttpApiMiddleware#ErrorSchema`: Renamed and applied to the middleware ID rather than constructor options. + +- `HttpApiMiddleware.TagClass.FailureService` -> `effect/unstable/httpapi/HttpApiMiddleware#Error`: Use the decoded error extractor; optional middleware fallback was removed. + +- `HttpApiMiddleware.TagClass.Optional` -> `none`: Optional declaration and fallback-on-failure behavior were removed; model fallback in the wrapping middleware. + +- `HttpApiMiddleware.TagClass.Provides` -> `effect/unstable/httpapi/HttpApiMiddleware#Provides`: Moved to the module level and applied to the middleware ID. + +- `HttpApiMiddleware.TagClassAny` -> `effect/unstable/httpapi/HttpApiMiddleware#AnyService`: Renamed widened service-key type. + +- `HttpApiMiddleware.TagClassSecurityAny` -> `effect/unstable/httpapi/HttpApiMiddleware#AnyServiceSecurity`: Renamed widened security service-key type. + +- `HttpApiMiddleware.TypeId` -> `none`: The marker is private; use public guards and type extractors. + +### `@effect/platform/HttpApiScalar` + +- `HttpApiScalar.layer` -> `effect/unstable/httpapi/HttpApiScalar#layer`: Pass the HttpApi as the first argument; the layer now contributes directly to HttpRouter. + +- `HttpApiScalar.layerHttpLayerRouter` -> `effect/unstable/httpapi/HttpApiScalar#layer`: The duplicate was removed. Pass options.api as the first layer argument and the remaining Scalar options second. + +- `HttpApiScalar.layerHttpLayerRouterCdn` -> `effect/unstable/httpapi/HttpApiScalar#layerCdn`: Use the explicit-api CDN layer with path, version, and Scalar options. + +### `@effect/platform/HttpApiSchema` + +- `HttpApiSchema.AnnotationEmptyDecodeable` -> `effect/unstable/httpapi/HttpApiSchema#asNoContent`: The public marker was removed; represent no-content decoding structurally with asNoContent({ decode }). + +- `HttpApiSchema.AnnotationEncoding` -> `effect/unstable/httpapi/HttpApiSchema#asJson / asFormUrlEncoded / asText / asUint8Array`: The key is internal; select encoding with a public combinator. + +- `HttpApiSchema.AnnotationMultipart` -> `effect/unstable/httpapi/HttpApiSchema#asMultipart`: The symbol annotation became a brand plus internal encoding metadata; apply the schema combinator. + +- `HttpApiSchema.AnnotationMultipartStream` -> `effect/unstable/httpapi/HttpApiSchema#asMultipartStream`: The symbol annotation became a brand plus internal encoding metadata; apply the schema combinator. + +- `HttpApiSchema.AnnotationParam` -> `effect/unstable/httpapi/HttpApiEndpoint#params`: Path names now live in the router path and schemas in endpoint option params, not schema annotations. + +- `HttpApiSchema.AnnotationStatus` -> `effect/unstable/httpapi/HttpApiSchema#status`: The public symbol was removed; apply status(code), which uses the httpApiStatus schema annotation. + +- `HttpApiSchema.Empty` -> `effect/unstable/httpapi/HttpApiSchema#Empty`: The API remains and returns Schema.Void annotated with the supplied status. + +- `HttpApiSchema.EmptyError` -> `effect/Schema#Error`: Define a normal schema error with httpApiStatus, then derive its no-content wire schema with asNoContent. + +- `HttpApiSchema.EmptyErrorClass` -> `effect/Schema#Error`: The class and no-content codec are separate in v4; combine Schema.Error with HttpApiSchema.asNoContent. + +- `HttpApiSchema.EmptyErrorUnify` -> `none`: Removed with EmptyError; Schema.Error instances already support yieldable-error behavior. + +- `HttpApiSchema.EmptyErrorUnifyIgnore` -> `none`: Removed with EmptyError; do not recreate the old Unify marker. + +- `HttpApiSchema.Encoding` -> `effect/unstable/httpapi/HttpApiSchema#Encoding`: The name remains but is now a discriminated PayloadEncoding or ResponseEncoding union; prefer public as\* combinators. + +- `HttpApiSchema.Multipart` -> `effect/unstable/httpapi/HttpApiSchema#asMultipart`: The type and constructor became a curried schema combinator: schema.pipe(asMultipart(options)). + +- `HttpApiSchema.MultipartStream` -> `effect/unstable/httpapi/HttpApiSchema#asMultipartStream`: The type and constructor became a curried schema combinator. + +- `HttpApiSchema.Text` -> `effect/unstable/httpapi/HttpApiSchema#asText`: Apply the encoding combinator to Schema.String instead of using a dedicated constructor. + +- `HttpApiSchema.Uint8Array` -> `effect/unstable/httpapi/HttpApiSchema#asUint8Array`: Apply the encoding combinator to Schema.Uint8Array instead of using a dedicated constructor. + +- `HttpApiSchema.UnionUnify` -> `effect/Schema#Union`: Use Schema.Union([self, that]); for endpoint alternatives, pass the schema array directly to preserve metadata. + +- `HttpApiSchema.annotations` -> `effect/Schema#annotate`: Schema annotations became annotate; set httpApiStatus directly or prefer HttpApiSchema.status for status only. + +- `HttpApiSchema.asEmpty` -> `effect/unstable/httpapi/HttpApiSchema#asNoContent`: Use schema.pipe(asNoContent({ decode }), status(code)); status is now a separate combinator. + +- `HttpApiSchema.deunionize` -> `none`: Pass schema arrays to endpoint success, error, and body alternatives so each member retains status and content type. + +- `HttpApiSchema.extractAnnotations` -> `none`: The internal symbol-copy helper was removed; HTTP metadata is schema-native and resolved through AST traversal. + +- `HttpApiSchema.getEmptyDecodeable` -> `effect/unstable/httpapi/HttpApiSchema#isNoContent`: Use isNoContent only to test bodylessness; decodeability is structural and has no exact query replacement. + +- `HttpApiSchema.getEncoding` -> `effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding / getResponseEncoding`: Encoding lookup split by direction; application code should normally use public as\* combinators. + +- `HttpApiSchema.getMultipart` -> `effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding`: Narrow the payload encoding to Multipart with buffered mode; multipart limits are on the encoding value. + +- `HttpApiSchema.getMultipartStream` -> `effect/unstable/httpapi/HttpApiSchema#getPayloadEncoding`: Narrow the payload encoding to Multipart with stream mode; multipart limits are on the encoding value. + +- `HttpApiSchema.getParam` -> `none`: Param identity moved out of schema metadata; read endpoint.path and endpoint.params. + +- `HttpApiSchema.getStatus` -> `effect/SchemaAST#resolveAt`: Resolve the httpApiStatus annotation directly, or prefer getStatusSuccess and getStatusError for response logic. + +- `HttpApiSchema.getStatusError` -> `effect/unstable/httpapi/HttpApiSchema#getStatusError`: The helper remains but accepts an AST and defaults to 500. + +- `HttpApiSchema.getStatusErrorAST` -> `effect/unstable/httpapi/HttpApiSchema#getStatusError`: The AST suffix collapsed into the sole helper, which defaults to 500. + +- `HttpApiSchema.getStatusSuccess` -> `effect/unstable/httpapi/HttpApiSchema#getStatusSuccess`: The helper remains but accepts an AST; bare Schema.Void now defaults to 200, so use Empty(204) for 204. + +- `HttpApiSchema.getStatusSuccessAST` -> `effect/unstable/httpapi/HttpApiSchema#getStatusSuccess`: The AST suffix collapsed into the sole helper; bare Schema.Void no longer implies 204. + +- `HttpApiSchema.param` -> `effect/unstable/httpapi/HttpApiEndpoint#params`: Use a literal /:name path and the matching field in endpoint constructor option params. + +- `HttpApiSchema.withEncoding` -> `effect/unstable/httpapi/HttpApiSchema#asJson / asFormUrlEncoded / asUint8Array / asText`: Replace the generic kind with the matching public curried encoding combinator. + +### `@effect/platform/HttpApiSecurity` + +- `HttpApiSecurity.Bearer` -> `effect/unstable/httpapi/HttpApiSecurity#Http`: Bearer was generalized to Http with scheme Bearer; the value-level bearer singleton remains. + +- `HttpApiSecurity.TypeId` -> `none`: The marker is private; use the public union or specific Http, ApiKey, and Basic types. + +- `HttpApiSecurity.annotate` -> `effect/unstable/httpapi/HttpApiSecurity#annotate`: The combinator remains; its key is now the v4 Context.Key abstraction. + +- `HttpApiSecurity.annotateContext` -> `effect/unstable/httpapi/HttpApiSecurity#annotateMerge`: Renamed; it still merges a Context into existing OpenAPI annotations. + +### `@effect/platform/HttpApiSwagger` + +- `HttpApiSwagger.layer` -> `effect/unstable/httpapi/HttpApiSwagger#layer`: Pass the HttpApi as the first argument; the layer now contributes directly to HttpRouter. + +- `HttpApiSwagger.layerHttpLayerRouter` -> `effect/unstable/httpapi/HttpApiSwagger#layer`: The duplicate was removed. Pass options.api first and the path option second. + +### `@effect/platform/HttpApp` + +- `HttpApp.Default` -> `Effect.Effect`: The alias was removed; v4 HTTP applications are ordinary response-producing Effects. + +- `HttpApp.HttpApp` -> `Effect.Effect`: The alias was removed; use the underlying Effect type and HttpEffect boundary combinators. + +- `HttpApp.currentPreResponseHandlers` -> `HttpEffect.appendPreResponseHandler / HttpEffect.withPreResponseHandler`: The FiberRef was removed; register request-local handlers through HttpEffect. + +- `HttpApp.ejectDefaultScopeClose` -> `HttpEffect.scopeDisableClose`: Renamed; it disables automatic request-scope closure, leaving closure to the caller. + +- `HttpApp.toWebHandler` -> `HttpEffect.toWebHandler`: Moved to HttpEffect for converting an HTTP effect to a Web handler. + +- `HttpApp.toWebHandlerRuntime` -> `HttpEffect.toWebHandlerWith(context)`: Runtime was removed in v4; supply a Context with toWebHandlerWith instead. + +- `HttpApp.unsafeEjectStreamScope` -> `HttpEffect.scopeTransferToStream`: Renamed; it transfers request-scope closure to a streaming response. + +### `@effect/platform/HttpBody` + +- `HttpBody.Empty` -> `HttpBody.Empty`: Retained with the same tag, but v4 exports a class rather than an interface. + +- `HttpBody.ErrorReason` -> `HttpBody.ErrorReason`: Retained but reshaped; original causes now live on HttpBodyError.cause. + +- `HttpBody.ErrorTypeId` -> `HttpBody.HttpBodyError`: The error type-id is private in v4; identify the exported error class instead. + +- `HttpBody.HttpBodyError` -> `HttpBody.HttpBodyError`: Changed from a factory/interface to a class constructed with reason and optional cause. + +- `HttpBody.Raw` -> `HttpBody.Raw`: Retained with the same tag and payload, but v4 exports a class. + +- `HttpBody.Stream` -> `HttpBody.Stream`: Retained with the same tag and byte stream, but v4 exports a class. + +- `HttpBody.TypeId` -> `HttpBody.isHttpBody`: The body brand is private in v4; use the public refinement instead. + +- `HttpBody.Uint8Array` -> `HttpBody.Uint8Array`: Retained with the same fields and tag, but v4 exports a class. + +- `HttpBody.file` -> `HttpBody.file`: Retained; bufferSize was replaced by chunkSize and the other file options remain. + +- `HttpBody.fileInfo` -> `HttpBody.fileFromInfo`: Renamed; it still uses supplied File.Info for content length and requires FileSystem. + +- `HttpBody.unsafeJson` -> `HttpBody.jsonUnsafe`: Renamed to put Unsafe last; serialization failures still throw. + +- `HttpBody.urlParams` -> `HttpBody.urlParams`: Retained and widened to accept UrlParams.Input. + +### `@effect/platform/HttpClient` + +- `HttpClient.SpanNameGenerator` -> `HttpClient.SpanNameGenerator`: The interface became a Context.Reference containing the generator function. + +- `HttpClient.TypeId` -> `HttpClient.isHttpClient`: The brand key is private in v4; use the public runtime refinement. + +- `HttpClient.catchAll` -> `HttpClient.catch`: Renamed to catch; the recovery callback still returns a response effect. + +- `HttpClient.catchTag` -> `HttpClient.catchTag`: Retained and widened to accept one or more error tags. + +- `HttpClient.currentTracerDisabledWhen` -> `HttpClient.TracerDisabledWhen`: Renamed and changed from FiberRef to Context.Reference. + +- `HttpClient.currentTracerPropagation` -> `HttpClient.TracerPropagationEnabled`: Renamed and changed from FiberRef to Context.Reference\. + +- `HttpClient.filterOrFail` -> `HttpClient.filterOrFail`: Retained; v4 also provides refinement overloads. + +- `HttpClient.filterStatus` -> `HttpClient.filterStatus`: Retained; rejection now fails with the HttpClientError wrapper. + +- `HttpClient.filterStatusOk` -> `HttpClient.filterStatusOk`: Retained; non-2xx responses now fail with the HttpClientError wrapper. + +- `HttpClient.make` -> `HttpClient.make`: Retained; the runner receives Fiber.Fiber and failures use the v4 error wrapper. + +- `HttpClient.makeWith` -> `HttpClient.makeWith`: Retained with the preprocess and postprocess constructor pattern. + +- `HttpClient.retry` -> `HttpClient.retry`: Retained; the Schedule error channel is included in the resulting client error type. + +- `HttpClient.tap` -> `effect/unstable/http/HttpClient#tap`: Moved to the v4 HTTP module with the same response-effect callback and client error/service widening. + +- `HttpClient.transform` -> `effect/unstable/http/HttpClient#transform`: Moved to the v4 HTTP module with the same request-aware transformation shape. + +- `HttpClient.withSpanNameGenerator` -> `HttpClient.transformResponse(Effect.provideService(HttpClient.SpanNameGenerator, f))`: The convenience combinator was removed; provide the reference around response effects. + +- `HttpClient.withTracerDisabledWhen` -> `HttpClient.transformResponse(Effect.provideService(HttpClient.TracerDisabledWhen, predicate))`: The convenience combinator was removed; provide the reference around response effects. + +- `HttpClient.withTracerPropagation` -> `HttpClient.transformResponse(Effect.provideService(HttpClient.TracerPropagationEnabled, enabled))`: Provide the renamed propagation reference around response effects. + +### `@effect/platform/HttpClientError` + +- `HttpClientError.HttpClientError` -> `HttpClientError.HttpClientError`: Changed from a union to a tagged wrapper class containing a concrete failure in reason. + +- `HttpClientError.RequestError` -> `HttpClientError.RequestError`: Now a type-only reason union; construct a concrete reason and wrap it in HttpClientError. + +- `HttpClientError.TypeId` -> `HttpClientError.isHttpClientError`: The brand key is private in v4; use the public runtime refinement. + +### `@effect/platform/HttpClientRequest` + +- `HttpClientRequest.Options.NoBody` -> `HttpClientRequest.Options.NoUrl`: NoBody was removed; v4 method helpers uniformly omit only url. + +- `HttpClientRequest.TypeId` -> `HttpClientRequest.isHttpClientRequest`: The request brand is private in v4; use the public runtime refinement. + +- `HttpClientRequest.bodyFileWeb` -> `HttpClientRequest.setBody + HttpBody.stream + Stream.fromReadableStream`: No one-call replacement remains; stream file.stream() and pass file.type and file.size to HttpBody.stream. + +- `HttpClientRequest.bodyUnsafeJson` -> `HttpClientRequest.bodyJsonUnsafe`: Renamed to put Unsafe last; serialization remains synchronous and throwing. + +- `HttpClientRequest.get` -> `HttpClientRequest.get`: Retained; options now use Options.NoUrl and no longer exclude body. + +- `HttpClientRequest.head` -> `HttpClientRequest.head`: Retained; options now use Options.NoUrl and no longer exclude body. + +- `HttpClientRequest.make` -> `HttpClientRequest.make`: Retained; all methods now accept Options.NoUrl without the GET/HEAD body restriction. + +- `HttpClientRequest.setBody` -> `HttpClientRequest.setBody`: Retained and still synchronizes body content metadata into headers. + +- `HttpClientRequest.toUrl` -> `HttpClientRequest.toUrl`: Retained and still returns Option\. + +### `@effect/platform/HttpClientResponse` + +- `HttpClientResponse.TypeId` -> `typeof HttpClientResponse.TypeId`: TypeId remains public but is now a string constant; use typeof in type position. + +- `HttpClientResponse.filterStatus` -> `HttpClientResponse.filterStatus`: Retained; rejected status now fails with an HttpClientError wrapper. + +- `HttpClientResponse.filterStatusOk` -> `HttpClientResponse.filterStatusOk`: Retained; non-2xx status now fails with an HttpClientError wrapper. + +- `HttpClientResponse.schemaBodyJson` -> `HttpClientResponse.schemaBodyJson`: Retained with v4 Schema constraints and SchemaError failures. + +- `HttpClientResponse.schemaBodyUrlParams` -> `HttpClientResponse.schemaBodyUrlParams`: Retained with ConstraintCodec input and SchemaError failures. + +- `HttpClientResponse.schemaHeaders` -> `HttpClientResponse.schemaHeaders`: Retained with ConstraintCodec input and SchemaError failures. + +- `HttpClientResponse.schemaJson` -> `HttpClientResponse.schemaJson`: Retained with ConstraintCodec input and v4 error types. + +- `HttpClientResponse.schemaNoBody` -> `HttpClientResponse.schemaNoBody`: Retained with Schema.Codec input and SchemaError failures. + +- `HttpClientResponse.stream` -> `HttpClientResponse.stream`: Retained; body failures now use the broader HttpClientError wrapper. + +### `@effect/platform/HttpIncomingMessage` + +- `HttpIncomingMessage.MaxBodySize` -> `HttpIncomingMessage.MaxBodySize`: Changed from a Reference subclass holding Option\ to Context.Reference\. + +- `HttpIncomingMessage.TypeId` -> `typeof HttpIncomingMessage.TypeId`: TypeId remains public but is now a string constant; use typeof in type position. + +- `HttpIncomingMessage.withMaxBodySize` -> `Effect.provideService(HttpIncomingMessage.MaxBodySize, size)`: The helper was removed; provide FileSystem.Size(input) or undefined directly. + +### `@effect/platform/HttpLayerRouter` + +- `HttpLayerRouter.FindMyWay.PathInput` -> `FindMyWay.PathInput`: Import FindMyWay from effect/unstable/http. + +- `HttpLayerRouter.FindMyWay.make` -> `FindMyWay.make`: Import FindMyWay from effect/unstable/http. + +- `HttpLayerRouter.MiddlewareTypeId` -> `none`: The middleware type id is internal in v4; use HttpRouter.Middleware. + +- `HttpLayerRouter.PathInput` -> `HttpRouter.PathInput`: Moved to the consolidated v4 router. + +- `HttpLayerRouter.Request.From` -> `HttpRouter.Request.From`: Moved with the layer-oriented router into the consolidated HttpRouter module. + +- `HttpLayerRouter.Request.Only` -> `HttpRouter.Request.Only`: Moved with the layer-oriented router into the consolidated HttpRouter module. + +- `HttpLayerRouter.Route.Context` -> `HttpRouter.Route.Context`: Moved with the Route helper types into the consolidated HttpRouter module. + +- `HttpLayerRouter.Route.Error` -> `HttpRouter.Route.Error`: Moved with the Route helper types into the consolidated HttpRouter module. + +- `HttpLayerRouter.RouteContext` -> `HttpRouter.RouteContext`: Moved to the consolidated v4 router. + +- `HttpLayerRouter.RouteTypeId` -> `none`: Route nominal ids are internal in v4; construct routes with HttpRouter.route. + +- `HttpLayerRouter.RouterConfig` -> `HttpRouter.RouterConfig`: Now a Context.Reference containing Partial\. + +- `HttpLayerRouter.TypeId` -> `none`: The router nominal service id is internal in v4; use HttpRouter.HttpRouter. + +- `HttpLayerRouter.add` -> `HttpRouter.add`: Moved to the consolidated HttpRouter; it still returns a route-registration Layer. + +- `HttpLayerRouter.addAll` -> `HttpRouter.addAll`: Moved to the consolidated HttpRouter; it still registers route values through a Layer and supports a prefix option. + +- `HttpLayerRouter.addHttpApi` -> `HttpApiBuilder.layer`: HTTP API registration moved to effect/unstable/httpapi. + +- `HttpLayerRouter.cors` -> `HttpRouter.cors`: HttpLayerRouter was consolidated into v4 HttpRouter. + +- `HttpLayerRouter.layer` -> `HttpRouter.layer`: Use the layer for the consolidated HttpRouter service. + +- `HttpLayerRouter.make` -> `HttpRouter.make`: The layer-oriented router became the sole v4 HttpRouter implementation. + +- `HttpLayerRouter.schemaJson` -> `HttpRouter.schemaJson`: Moved to the consolidated router with v4 Schema and error types. + +- `HttpLayerRouter.schemaNoBody` -> `HttpRouter.schemaNoBody`: Moved to the consolidated router with v4 Schema types. + +- `HttpLayerRouter.serve` -> `HttpRouter.serve`: Moved to the consolidated router; pass the route-registration layer. + +- `HttpLayerRouter.toHttpEffect` -> `HttpRouter.toHttpEffect`: Moved to the consolidated HttpRouter; route-not-found failures now use HttpServerError.HttpServerError. + +- `HttpLayerRouter.toWebHandler` -> `HttpRouter.toWebHandler`: Moved to the consolidated router for building a Fetch handler and disposer. + +### `@effect/platform/HttpMiddleware` + +- `HttpMiddleware.SpanNameGenerator` -> `HttpMiddleware.SpanNameGenerator`: The branded interface became a Context.Reference containing the generator. + +- `HttpMiddleware.cors` -> `HttpMiddleware.cors`: Retained with the same CORS options and behavior. + +- `HttpMiddleware.currentTracerDisabledWhen` -> `HttpMiddleware.TracerDisabledWhen`: The FiberRef became a Context.Reference containing the request predicate. + +- `HttpMiddleware.loggerDisabled` -> `HttpMiddleware.withLoggerDisabled`: The FiberRef was removed; locally wrap an effect or use HttpRouter.disableLogger. + +- `HttpMiddleware.withSpanNameGenerator` -> `Layer.provide(layer, Layer.succeed(HttpMiddleware.SpanNameGenerator)(f))`: Provide the SpanNameGenerator reference to the target layer. + +- `HttpMiddleware.withTracerDisabledForUrls` -> `Layer.provide(layer, HttpMiddleware.layerTracerDisabledForUrls(urls))`: Provide the new URL-predicate layer to the target layer. + +- `HttpMiddleware.withTracerDisabledWhen` -> `Layer.provide(layer, Layer.succeed(HttpMiddleware.TracerDisabledWhen)(predicate))`: Provide the TracerDisabledWhen reference to the target layer. + +- `HttpMiddleware.withTracerDisabledWhenEffect` -> `Effect.provideService(effect, HttpMiddleware.TracerDisabledWhen, predicate)`: Provide the TracerDisabledWhen reference locally to the effect. + +### `@effect/platform/HttpPlatform` + +- `HttpPlatform.HttpPlatform` -> `HttpPlatform.HttpPlatform`: The service is now a Context.Service class; use its Service member for the implementation type. + +- `HttpPlatform.TypeId` -> `none`: The public type id was removed; use the HttpPlatform Context.Service class. + +- `HttpPlatform.layer` -> `HttpPlatform.layer`: Retained as the default file-response layer. + +- `HttpPlatform.make` -> `HttpPlatform.make`: Retained; v4 returns the service implementation and uses updated file stream options. + +### `@effect/platform/HttpRouter` + +- `HttpRouter.Default` -> `HttpRouter.HttpRouter + HttpRouter.layer`: Custom/default router tags were removed; v4 provides one router service. + +- `HttpRouter.HttpRouter` -> `HttpRouter.HttpRouter`: The name remains, but now denotes the mutable layer-oriented registration service. + +- `HttpRouter.HttpRouter.DefaultServices` -> `none`: The custom tagged-router default-service bundle was removed. + +- `HttpRouter.HttpRouter.Service` -> `HttpRouter.HttpRouter`: Use the consolidated router service interface. + +- `HttpRouter.Route.Middleware` -> `Effect.Effect`: Spell the route response Effect directly, or use HttpRouter.middleware for transforms. + +- `HttpRouter.RouteContextTypeId` -> `none`: The nominal id is internal in v4; access HttpRouter.RouteContext as a service. + +- `HttpRouter.RouteTypeId` -> `none`: The nominal id is internal in v4; construct routes with HttpRouter.route. + +- `HttpRouter.Tag` -> `none`: Custom router tags were removed; use the singleton router service and registration layers. + +- `HttpRouter.TypeId` -> `none`: The router nominal service id is internal in v4; use HttpRouter.HttpRouter. + +- `HttpRouter.all` -> `HttpRouter.add("*", path, handler, options)`: v4 registers a route layer instead of returning an immutable router. + +- `HttpRouter.append` -> `HttpRouter.addAll([route])`: Register the route and merge its layer with other route layers. + +- `HttpRouter.catchAll` -> `HttpRouter.middleware + Effect.catch`: Apply typed-error recovery in route middleware provided to route layers. + +- `HttpRouter.catchAllCause` -> `HttpRouter.middleware + Effect.catchCause`: Apply cause recovery in route middleware provided to route layers. + +- `HttpRouter.catchTag` -> `HttpRouter.middleware + Effect.catchTag`: Apply tagged-error recovery in route middleware provided to route layers. + +- `HttpRouter.concat` -> `Layer.merge`: Routers are now route-registration layers; merge the two layers. + +- `HttpRouter.concatAll` -> `Layer.mergeAll`: Routers are now route-registration layers; merge all layers. + +- `HttpRouter.currentRouterConfig` -> `HttpRouter.RouterConfig`: The FiberRef became a Context.Reference containing Partial\. + +- `HttpRouter.empty` -> `Layer.empty`: There is no immutable empty router; use an empty registration layer. + +- `HttpRouter.fromIterable` -> `HttpRouter.addAll(Array.from(routes))`: Materialize and register the route descriptors as a layer. + +- `HttpRouter.get` -> `HttpRouter.add("GET", path, handler, options)`: Register a route layer; handlers must produce HttpServerResponse. + +- `HttpRouter.head` -> `HttpRouter.addAll([HttpRouter.route("HEAD", path, handler, options)])`: Use route plus addAll because add does not expose HEAD. + +- `HttpRouter.makeRoute` -> `HttpRouter.route`: Renamed to route; v4 route options no longer expose the old prefix field. + +- `HttpRouter.mount` -> `HttpRouter.addAll(routes, { prefix: path })`: Register child routes with a prefix, or use router.prefixed(path). + +- `HttpRouter.mountApp` -> `HttpRouter.use((router) => router.prefixed(path).add("*", "/*", app))`: Register the app on the prefixed router service; no direct mount API remains. + +- `HttpRouter.options` -> `HttpRouter.add("OPTIONS", path, handler, options)`: Register a route layer; handlers must produce HttpServerResponse. + +- `HttpRouter.patch` -> `HttpRouter.add("PATCH", path, handler, options)`: Register a route layer; handlers must produce HttpServerResponse. + +- `HttpRouter.post` -> `HttpRouter.add("POST", path, handler, options)`: Register a route layer; handlers must produce HttpServerResponse. + +- `HttpRouter.prefixAll` -> `HttpRouter.addAll(routes, { prefix })`: Apply the prefix while registering route descriptors. + +- `HttpRouter.put` -> `HttpRouter.add("PUT", path, handler, options)`: Register a route layer; handlers must produce HttpServerResponse. + +- `HttpRouter.setRouterConfig` -> `Layer.succeed(HttpRouter.RouterConfig)(config)`: Provide the RouterConfig Context.Reference as a layer. + +- `HttpRouter.toHttpApp` -> `HttpRouter.toHttpEffect`: Pass the route-registration layer to build the server handler effect. + +- `HttpRouter.transform` -> `HttpRouter.middleware`: Express the route-wide response Effect transform as router middleware. + +- `HttpRouter.withRouterConfig` -> `Effect.provideService(effect, HttpRouter.RouterConfig, config)`: Provide the RouterConfig Context.Reference locally instead of setting a FiberRef. + +### `@effect/platform/HttpServer` + +- `HttpServer.HttpServer` -> `HttpServer.HttpServer`: The interface and tag became one Context.Service class; use its Service member for implementations. + +- `HttpServer.ServeOptions` -> `none`: The unused respond option model was removed with no shared v4 counterpart. + +- `HttpServer.TypeId` -> `none`: The public TypeId was removed; HttpServer is now a Context.Service class. + +- `HttpServer.addressWith` -> `HttpServer.HttpServer.use(({ address }) => effect(address))`: The accessor was removed; read the service and pass its Address to the callback. + +- `HttpServer.layerContext` -> `HttpServer.layerServices`: Renamed; it provides the standard HTTP platform services. + +- `HttpServer.make` -> `HttpServer.make`: Retained; it returns the Context.Service implementation. + +- `HttpServer.serve` -> `effect/unstable/http/HttpServer#serve`: Moved to the v4 HTTP module; the application is now an Effect producing HttpServerResponse rather than the separate HttpApp model. + +### `@effect/platform/HttpServerError` + +- `HttpServerError.HttpServerError` -> `HttpServerError.HttpServerError | HttpServerError.ServeError`: Handler failures became a tagged wrapper, while ServeError remains separate. + +- `HttpServerError.RequestError` -> `HttpServerError.RequestParseError (constructor) / HttpServerError.RequestError (type)`: The constructible class became RequestParseError; RequestError is now a broader type union. + +- `HttpServerError.TypeId` -> `HttpServerError.isHttpServerError`: The brand is private in v4; use the public runtime refinement. + +- `HttpServerError.clientAbortFiberId` -> `HttpServerError.ClientAbort.annotation`: Client aborts now use a Cause context annotation rather than a sentinel FiberId. + +- `HttpServerError.isServerError` -> `HttpServerError.isHttpServerError`: Renamed and narrowed to wrapped handler errors; test ServeError separately if needed. + +### `@effect/platform/HttpServerRequest` + +- `HttpServerRequest.ParsedSearchParams` -> `HttpServerRequest.ParsedSearchParams`: The marker and tag became one Context.Service class. + +- `HttpServerRequest.TypeId` -> `typeof HttpServerRequest.TypeId`: TypeId remains public but is now a string constant; use typeof in type position. + +- `HttpServerRequest.fromWeb` -> `HttpServerRequest.fromWeb`: Retained for wrapping a Web Request. + +- `HttpServerRequest.persistedMultipart` -> `HttpServerRequest.HttpServerRequest.use((request) => request.multipart)`: Use the request service's `.use` helper to return its cached multipart effect. + +- `HttpServerRequest.schemaBodyJson` -> `HttpServerRequest.schemaBodyJson`: Retained with v4 Schema constraints and error types. + +- `HttpServerRequest.schemaBodyUrlParams` -> `HttpServerRequest.schemaBodyUrlParams`: Retained with ConstraintCodec input and v4 error types. + +- `HttpServerRequest.schemaHeaders` -> `HttpServerRequest.schemaHeaders`: Retained with ConstraintCodec input and SchemaError failures. + +- `HttpServerRequest.toWeb` -> `HttpServerRequest.toWeb`: Retained and captures the current Context for streamed bodies. + +- `HttpServerRequest.toWebEither` -> `HttpServerRequest.toWebResult`: Either became Result, and the optional Runtime became an optional Context. + +### `@effect/platform/HttpServerRespondable` + +- `HttpServerRespondable.symbol` -> `HttpServerRespondable.symbol`: Retained as a string protocol key rather than a unique symbol. + +### `@effect/platform/HttpServerResponse` + +- `HttpServerResponse.TypeId` -> `HttpServerResponse.isHttpServerResponse`: The response brand is private in v4; use the public runtime refinement. + +- `HttpServerResponse.expireCookie` -> `HttpServerResponse.expireCookie`: Now effectful and safe; use expireCookieUnsafe for synchronous throwing behavior. + +- `HttpServerResponse.file` -> `HttpServerResponse.file`: Retained with updated FileSystem stream options. + +- `HttpServerResponse.isServerResponse` -> `HttpServerResponse.isHttpServerResponse`: Renamed. + +- `HttpServerResponse.setCookie` -> `HttpServerResponse.setCookie`: Retained as the safe effectful cookie setter. + +- `HttpServerResponse.stream` -> `HttpServerResponse.stream`: Retained; v4 Stream no longer has a service type parameter. + +- `HttpServerResponse.text` -> `HttpServerResponse.text`: Moved unchanged. + +- `HttpServerResponse.toWeb` -> `HttpServerResponse.toWeb`: Retained, but the optional Runtime became an optional Context for stream execution. + +- `HttpServerResponse.uint8Array` -> `HttpServerResponse.uint8Array`: Moved unchanged. + +- `HttpServerResponse.unsafeJson` -> `HttpServerResponse.jsonUnsafe`: Renamed to put Unsafe last; serialization failures still throw. + +- `HttpServerResponse.unsafeSetCookie` -> `HttpServerResponse.setCookieUnsafe`: Renamed to put Unsafe last; invalid cookies still throw. + +- `HttpServerResponse.unsafeSetCookies` -> `HttpServerResponse.setCookiesUnsafe`: Renamed to put Unsafe last; invalid cookies still throw. + +### `@effect/platform/KeyValueStore` + +- `KeyValueStore.KeyValueStore` -> `KeyValueStore.KeyValueStore`: The service moved to effect/unstable/persistence/KeyValueStore; missing values now use undefined and operations fail with KeyValueStoreError. + +- `KeyValueStore.KeyValueStore.AnyStore` -> `KeyValueStore.KeyValueStore | KeyValueStore.SchemaStore`: The convenience namespace alias was removed; write the store union explicitly when needed. + +- `KeyValueStore.SchemaStoreTypeId` -> `none`: The v4 SchemaStore has no public type-id alias; use the SchemaStore interface. + +- `KeyValueStore.TypeId` -> `none`: The KeyValueStore runtime marker is internal in v4; use the service and interface. + +- `KeyValueStore.layerSchema` -> `KeyValueStore.toSchemaStore`: Schema stores are now derived with toSchemaStore; define the desired Context.Service and layer explicitly. + +- `KeyValueStore.make` -> `KeyValueStore.make`: The constructor remains in the moved module with v4 MakeOptions. + +- `KeyValueStore.prefix` -> `KeyValueStore.prefix`: The prefixed-store combinator remains in the moved module. + +### `@effect/platform/MsgPack` + +- `MsgPack.ErrorTypeId` -> `Msgpack.MsgPackError`: The public error type-id alias was removed; use the MsgPackError class. + +- `MsgPack.duplex` -> `Msgpack.duplex`: The API moved to effect/unstable/encoding/Msgpack. + +- `MsgPack.duplexSchema` -> `Msgpack.duplexSchema`: The API moved to effect/unstable/encoding/Msgpack and uses v4 Schema constraints. + +- `MsgPack.pack` -> `Msgpack.encode`: The MessagePack channel constructor was renamed from pack to encode. + +- `MsgPack.packSchema` -> `Msgpack.encodeSchema`: The schema-aware pack channel was renamed to encodeSchema. + +- `MsgPack.schema` -> `Msgpack.schema`: The schema helper remains in the moved module and uses the v4 Schema model. + +- `MsgPack.unpack` -> `Msgpack.decode`: The MessagePack channel constructor was renamed from unpack to decode. + +- `MsgPack.unpackSchema` -> `Msgpack.decodeSchema`: The schema-aware unpack channel was renamed to decodeSchema. + +### `@effect/platform/Multipart` + +- `Multipart.ErrorTypeId` -> `Multipart.MultipartError`: The public error type-id alias was removed; use the MultipartError class. + +- `Multipart.FieldMimeTypes` -> `Multipart.FieldMimeTypes`: The setting remains but is now a Context.Reference rather than a service class. + +- `Multipart.FileSchema` -> `Multipart.PersistedFileSchema`: The schema for persisted multipart files was renamed. + +- `Multipart.MaxFieldSize` -> `Multipart.MaxFieldSize`: The setting remains but is now a Context.Reference. + +- `Multipart.MaxFileSize` -> `Multipart.MaxFileSize`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. + +- `Multipart.MaxParts` -> `Multipart.MaxParts`: The setting remains as a Context.Reference; use undefined rather than Option.none for no limit. + +- `Multipart.TypeId` -> `typeof Multipart.TypeId`: The runtime marker remains exported, but the separate type alias was removed. + +- `Multipart.makeChannel` -> `effect/unstable/http/Multipart#makeChannel`: The channel constructor moved and no longer accepts bufferSize; input and output chunks use non-empty readonly arrays. + +- `Multipart.schemaJson` -> `Multipart.schemaJson`: The JSON-field decoder remains in effect/unstable/http/Multipart and uses v4 Schema constraints. + +- `Multipart.withFieldMimeTypes` -> `Effect.provideService(Multipart.FieldMimeTypes, mimeTypes)`: Provide the v4 Context.Reference around the effect. + +- `Multipart.withLimits` -> `Effect.provideContext(effect, Multipart.limitsServices(options))`: Build the multipart limit context and provide it to the effect; Option-valued limits became optional plain values. + +- `Multipart.withLimits.Options` -> `Multipart.withLimits.Options`: Limit fields now use optional plain numbers or SizeInput values; convert Option.none to undefined and Option.some(value) to value. + +- `Multipart.withLimitsStream` -> `Stream.provideContext(stream, Multipart.limitsServices(options))`: Build the multipart limit context and provide it to the stream; Option-valued limits became optional plain values. + +- `Multipart.withMaxFieldSize` -> `Effect.provideService(Multipart.MaxFieldSize, size)`: Provide the v4 Context.Reference around the effect. + +- `Multipart.withMaxFileSize` -> `Effect.provideService(Multipart.MaxFileSize, size)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. + +- `Multipart.withMaxParts` -> `Effect.provideService(Multipart.MaxParts, count)`: Provide the v4 Context.Reference around the effect, converting Option.none to undefined. + +### `@effect/platform/Ndjson` + +- `Ndjson.ErrorTypeId` -> `Ndjson.NdjsonError`: The public error marker was removed; use the NdjsonError class. + +- `Ndjson.NdjsonErrorTypeId` -> `Ndjson.NdjsonError`: The public error type-id alias was removed; use the NdjsonError class. + +- `Ndjson.NdjsonOptions` -> `{ readonly ignoreEmptyLines?: boolean }`: The standalone options interface was removed; decoding and duplex APIs accept this inline shape. + +- `Ndjson.duplex` -> `Ndjson.duplex`: The API moved to effect/unstable/encoding/Ndjson. + +- `Ndjson.duplexSchema` -> `Ndjson.duplexSchema`: The API moved to effect/unstable/encoding/Ndjson and uses v4 Schema constraints. + +- `Ndjson.pack` -> `Ndjson.encode`: The NDJSON channel constructor was renamed from pack to encode. + +- `Ndjson.packSchema` -> `Ndjson.encodeSchema`: The schema-aware pack channel was renamed to encodeSchema. + +- `Ndjson.packSchemaString` -> `Ndjson.encodeSchemaString`: The string schema pack channel was renamed to encodeSchemaString. + +- `Ndjson.packString` -> `Ndjson.encodeString`: The string pack channel was renamed to encodeString. + +- `Ndjson.unpack` -> `Ndjson.decode`: The NDJSON channel constructor was renamed from unpack to decode. + +- `Ndjson.unpackSchema` -> `Ndjson.decodeSchema`: The schema-aware unpack channel was renamed to decodeSchema. + +- `Ndjson.unpackSchemaString` -> `Ndjson.decodeSchemaString`: The string schema unpack channel was renamed to decodeSchemaString. + +- `Ndjson.unpackString` -> `Ndjson.decodeString`: The string unpack channel was renamed to decodeString. + +### `@effect/platform/OpenApi` + +- `OpenApi.AdditionalPropertiesStrategy` -> `none`: OpenApi.fromApi no longer accepts generation options; standalone JSON Schema generation has a separate additionalProperties option. + +- `OpenApi.Exclude` -> `effect/unstable/httpapi/OpenApi#Exclude`: Same annotation key and default; it is now a Context.Reference value. + +- `OpenApi.OpenApiSpecContentType` -> `string`: The closed media-type union was removed so custom and streaming media types are supported. + +- `OpenApi.fromApi` -> `effect/unstable/httpapi/OpenApi#fromApi`: The operation remains and returns OpenAPI 3.1, but the signature is now only fromApi(api). + +### `@effect/platform/OpenApiJsonSchema` + +- `OpenApiJsonSchema.Any` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the open, dialect-neutral JSON Schema object model. + +- `OpenApiJsonSchema.AnyObject` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated; construct the required object directly. + +- `OpenApiJsonSchema.AnyOf` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.Array` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.Empty` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces and special id shapes were removed. + +- `OpenApiJsonSchema.Enum` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.Enums` -> `effect/JsonSchema#JsonSchema`: The Effect-specific comment enum shape has no named v4 interface; use the general object model. + +- `OpenApiJsonSchema.Integer` -> `effect/JsonSchema#JsonSchema`: The narrow numeric interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.JsonSchema` -> `effect/JsonSchema#JsonSchema`: Use the dialect-neutral open JSON Schema object model. + +- `OpenApiJsonSchema.Numeric` -> `effect/JsonSchema#JsonSchema`: The narrow numeric interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.Object` -> `effect/JsonSchema#JsonSchema`: The narrow node interfaces were consolidated into the general object model. + +- `OpenApiJsonSchema.Ref` -> `effect/JsonSchema#JsonSchema`: The narrow ref interface was consolidated; OpenAPI conversion rewrites definition references. + +- `OpenApiJsonSchema.Root` -> `effect/JsonSchema#MultiDocument`: OpenAPI generation keeps roots in schemas and shared components in definitions; the inline-definitions root model is gone. + +- `OpenApiJsonSchema.make` -> `effect/Schema#toJsonSchemaDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1`: Generate Draft 2020-12, wrap the root in a multi-document, then convert references and definitions to OpenAPI 3.1. + +- `OpenApiJsonSchema.makeWithDefs` -> `effect/SchemaRepresentation#toJsonSchemaMultiDocument + effect/JsonSchema#toMultiDocumentOpenApi3_1`: Definitions are returned separately; build a multi-document representation and convert it to OpenAPI 3.1. + +### `@effect/platform/Path` + +- `Path.TypeId` -> `typeof Path.TypeId`: The module moved to effect/Path; the runtime marker remains exported but the separate type alias was removed. + +### `@effect/platform/PlatformConfigProvider` + +- `PlatformConfigProvider.fromFileTree` -> `ConfigProvider.fromDir`: The provider moved into effect/ConfigProvider and was renamed; rootDirectory is now rootPath. + +- `PlatformConfigProvider.layerDotEnv` -> `ConfigProvider.layer(ConfigProvider.fromDotEnv({ path }))`: Use the v4 dotenv provider effect and install it with ConfigProvider.layer. + +- `PlatformConfigProvider.layerDotEnvAdd` -> `ConfigProvider.layerAdd(ConfigProvider.fromDotEnv({ path }))`: Use the v4 dotenv provider effect and compose it with ConfigProvider.layerAdd. + +- `PlatformConfigProvider.layerFileTree` -> `ConfigProvider.layer(ConfigProvider.fromDir({ rootPath }))`: Use the renamed directory-tree provider and install it with ConfigProvider.layer. + +- `PlatformConfigProvider.layerFileTreeAdd` -> `ConfigProvider.layerAdd(ConfigProvider.fromDir({ rootPath }))`: Use the renamed directory-tree provider and compose it with ConfigProvider.layerAdd. + +### `@effect/platform/Runtime` + +- `Runtime.RunMain` -> `ReturnType`: The standalone interface was removed; derive the runner type from effect/Runtime.makeRunMain. disablePrettyLogger is no longer an option. + +### `@effect/platform/Socket` + +- `Socket.CloseEventTypeId` -> `Socket.CloseEvent`: The close-event marker is internal in v4; use the CloseEvent class or Socket.isCloseEvent. + +- `Socket.SocketError` -> `Socket.SocketError`: The old union became a tagged wrapper around SocketReadError, SocketWriteError, SocketOpenError, or SocketCloseError. + +- `Socket.SocketErrorTypeId` -> `Socket.SocketErrorTypeId`: The error marker remains in effect/unstable/socket/Socket. + +- `Socket.SocketGenericError` -> `Socket.SocketReadError | Socket.SocketWriteError | Socket.SocketOpenError`: The generic reason discriminator was replaced by dedicated read, write, and open error classes. + +- `Socket.TypeId` -> `typeof Socket.TypeId`: The socket marker remains exported, but the separate type alias was removed. + +- `Socket.WebSocket` -> `Socket.WebSocket`: The opaque service moved to effect/unstable/socket/Socket and is now a Context.Service class for globalThis.WebSocket. + +- `Socket.WebSocketConstructor` -> `Socket.WebSocketConstructor`: The service moved to effect/unstable/socket/Socket and is now a Context.Service class. + +- `Socket.currentSendQueueCapacity` -> `Socket.SendQueueCapacity`: The FiberRef was replaced by a defaulted Context.Reference. + +- `Socket.layerWebSocket` -> `Socket.layerWebSocket`: The constructor remains in effect/unstable/socket/Socket; its URL may now also be an Effect. + +### `@effect/platform/SocketServer` + +- `SocketServer.ErrorTypeId` -> `SocketServer.ErrorTypeId`: The API moved to effect/unstable/socket/SocketServer and retains this name. + +### `@effect/platform/Template` + +- `Template.Interpolated.Context` -> `Template.Interpolated.Context`: The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values. + +- `Template.Interpolated.Error` -> `Template.Interpolated.Error`: The API moved to effect/unstable/http/Template; v4 interpolation types also account for Effect values. + +### `@effect/platform/Terminal` + +- `Terminal.QuitException` -> `Terminal.QuitError`: The quit sentinel was renamed and moved to effect/Terminal. + +- `Terminal.isQuitException` -> `Terminal.isQuitError`: The quit sentinel was renamed from QuitException to QuitError. + +### `@effect/platform/Transferable` + +- `Transferable.CollectorService` -> `Transferable.Collector["Service"]`: The collector interface is now the service type of the Transferable.Collector Context.Service class. + +- `Transferable.Uint8Array` -> `Transferable.Uint8Array`: The transferable Uint8Array schema remains in the moved module. + +- `Transferable.schema` -> `Transferable.schema`: The schema wrapper moved to effect/unstable/workers/Transferable and uses the v4 Schema model. + +- `Transferable.unsafeMakeCollector` -> `Transferable.makeCollectorUnsafe`: The unsafe collector constructor was renamed. + +### `@effect/platform/Url` + +- `Url.setUrlParams` -> `Url.setUrlParams`: Retained and widened to accept UrlParams.Input. + +### `@effect/platform/UrlParams` + +- `UrlParams.CoercibleRecord` -> `UrlParams.CoercibleRecord`: The recursive interface became a generic mapped type preserving the input shape. + +- `UrlParams.Input` -> `UrlParams.Input`: Retained and broadened to include UrlParams itself. + +- `UrlParams.makeUrl` -> `Url.make`: Moved to Url, returns Result, and takes string | undefined for the hash. + +- `UrlParams.schemaFromSelf` -> `UrlParams.UrlParamsSchema`: Renamed to the declaration schema for the v4 wrapper. + +- `UrlParams.schemaFromString` -> `Schema.String.pipe(Schema.decodeTo(UrlParams.UrlParamsSchema, { decode: SchemaGetter.transform((s) => UrlParams.fromInput(new URLSearchParams(s))), encode: SchemaGetter.transform(UrlParams.toString) }))`: No prebuilt string codec remains; recreate it by transforming between a query string and UrlParams. + +- `UrlParams.schemaJson` -> `UrlParams.schemaJsonField(field).pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the field codec with the target schema, then decode it. + +- `UrlParams.schemaParse` -> `UrlParamsFromString.pipe(Schema.decodeTo(UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))))`: Recreate the removed helper by composing the string, record, and target codecs. + +- `UrlParams.schemaRecord` -> `UrlParams.schemaRecord.pipe(Schema.decodeTo(schema))`: schemaRecord is now a base codec value; compose it with the target schema. + +- `UrlParams.schemaStruct` -> `UrlParams.schemaRecord.pipe(Schema.decodeTo(schema), Schema.decodeEffect)`: Compose the record codec with the target schema and decode it. + +- `UrlParams.toString` -> `UrlParams.toString`: Retained and broadened to accept any UrlParams.Input. + +### `@effect/platform/Worker` + +- `Worker.BackingWorker` -> `Worker.Worker`: The low-level backing worker became the primary Worker interface with send and run operations. + +- `Worker.PlatformWorker` -> `Worker.WorkerPlatform`: The platform service was renamed and is now a Context.Service class. + +- `Worker.PlatformWorkerTypeId` -> `none`: The Context.Service class replaces the public platform-worker type-id alias. + +- `Worker.SerializedWorker` -> `RpcClient with RpcClient.layerProtocolWorker`: The serialized worker facade was removed; v4 routes schema-defined RPCs through the worker protocol. + +- `Worker.SerializedWorker.Options` -> `RpcWorker.layerInitialMessage`: Use RpcWorker.layerInitialMessage when a worker RPC protocol needs schema-encoded initialization. + +- `Worker.SerializedWorkerPool` -> `RpcClient.makeProtocolWorker`: The worker-backed RPC protocol owns its worker pool in v4. + +- `Worker.SerializedWorkerPool.Options` -> `Parameters[0]`: Pool sizing options moved to the worker RPC protocol; initial messages are provided separately with RpcWorker.layerInitialMessage. + +- `Worker.Worker` -> `Worker.Worker`: The name remains in effect/unstable/workers/Worker, but it is now the low-level send/run abstraction rather than execute/executeEffect. + +- `Worker.Worker.Options` -> `Worker.Worker["run"] options`: Encoding moved to RPC schemas; the low-level run operation only accepts an optional onSpawn effect. + +- `Worker.Worker.Response` -> `none`: The old tagged-request wire response is gone; worker RPC wire messages are internal to RpcClient and RpcServer. + +- `Worker.Worker.Span` -> `none`: The explicit span tuple was removed; the RPC worker protocol handles span propagation internally. + +- `Worker.WorkerManager` -> `Worker.WorkerPlatform`: WorkerPlatform now spawns low-level Worker values directly, replacing WorkerManager. + +- `Worker.WorkerManagerTypeId` -> `none`: The removed WorkerManager has no v4 type-id; WorkerPlatform is a Context.Service class. + +- `Worker.WorkerPool` -> `RpcClient.Protocol`: For serialized request/response workloads use the worker-backed RPC Protocol; for raw messages build a Pool around WorkerPlatform.spawn. + +- `Worker.WorkerPool.Options` -> `Parameters[0]`: Worker RPC pool sizing is configured on makeProtocolWorker or layerProtocolWorker. + +- `Worker.layerManager` -> `Worker.WorkerPlatform`: WorkerManager was removed; provide the adapter's WorkerPlatform layer directly. + +- `Worker.makeManager` -> `Worker.WorkerPlatform`: WorkerManager was removed; obtain WorkerPlatform and call its spawn method. + +- `Worker.makePool` -> `Pool + Worker.WorkerPlatform.spawn`: Generic worker pools are no longer built by this module; build a Pool around WorkerPlatform.spawn, or use RpcClient.makeProtocolWorker for RPC workers. + +- `Worker.makePoolLayer` -> `RpcClient.layerProtocolWorker`: The standard v4 worker-pool layer is the worker-backed RPC client protocol; compose it with the RPC client layer. + +- `Worker.makePoolSerialized` -> `RpcClient.makeProtocolWorker`: Serialized tagged-request workers were replaced by the worker-backed RPC protocol. + +- `Worker.makePoolSerializedLayer` -> `RpcClient.layerProtocolWorker`: Serialized tagged-request worker pools were replaced by the worker-backed RPC protocol layer. + +- `Worker.makeSerialized` -> `RpcClient with RpcClient.layerProtocolWorker`: Serialized tagged-request execution moved to the v4 RPC model; define an RpcGroup and use the worker protocol. + +### `@effect/platform/WorkerError` + +- `WorkerError.WorkerErrorFrom` -> `WorkerError.WorkerError`: The old serializable reason object was replaced by WorkerError wrapping dedicated spawn, send, receive, or unknown reason classes. + +- `WorkerError.WorkerErrorTypeId` -> `WorkerError.TypeId`: The type-level worker error marker was shortened to TypeId in the moved module. + +### `@effect/platform/WorkerRunner` + +- `WorkerRunner.BackingRunner` -> `WorkerRunner.WorkerRunner`: The low-level backing runner became the primary WorkerRunner interface. + +- `WorkerRunner.BackingRunner.Message` -> `WorkerRunner.PlatformMessage`: The request/close wire tuple moved to the top-level PlatformMessage type. + +- `WorkerRunner.CloseLatch` -> `none`: The public close-latch service was removed; WorkerRunner implementations manage lifetime through their run effect and adapter scope. + +- `WorkerRunner.PlatformRunner` -> `WorkerRunner.WorkerRunnerPlatform`: The platform service was renamed and is now a Context.Service class. + +- `WorkerRunner.PlatformRunnerTypeId` -> `none`: The Context.Service class replaces the public platform-runner type-id alias. + +- `WorkerRunner.Runner` -> `WorkerRunner.WorkerRunner`: The namespace-only runner API was replaced by the low-level WorkerRunner interface. + +- `WorkerRunner.Runner.Options` -> `none`: The custom decode/encode callbacks were removed; use raw low-level messages or define schemas in the v4 RPC model. + +- `WorkerRunner.SerializedRunner` -> `RpcServer with RpcGroup handlers`: The serialized runner namespace was removed in favor of typed Rpc definitions and RpcServer. + +- `WorkerRunner.SerializedRunner.Handlers` -> `RpcGroup.HandlersFrom`: Define an RpcGroup and derive its server handler object type with HandlersFrom. + +- `WorkerRunner.SerializedRunner.HandlersContext` -> `RpcGroup.HandlersServices`: Derive services required by an RpcGroup handler object with HandlersServices. + +- `WorkerRunner.SerializedRunner.InitialContext` -> `none`: Initial-message layer outputs are no longer inferred by this helper; model initialization as normal RpcGroup handler layers and services. + +- `WorkerRunner.SerializedRunner.InitialEnv` -> `none`: Initial-message layer inputs are no longer inferred by this helper; model initialization as normal RpcGroup handler layers and services. + +- `WorkerRunner.launch` -> `RpcServer.layerProtocolWorkerRunner`: For schema-defined workers, provide the worker-runner RPC protocol and launch the normal RpcServer layer. + +- `WorkerRunner.layer` -> `WorkerRunner.WorkerRunnerPlatform.start + WorkerRunner.WorkerRunner.run`: The generic processing layer was removed; use the low-level runner directly or the RpcServer worker protocol. + +- `WorkerRunner.layerCloseLatch` -> `none`: The public close-latch layer was removed; adapter runner lifetime is managed internally. + +- `WorkerRunner.layerSerialized` -> `RpcServer.layerProtocolWorkerRunner`: Serialized tagged-request handlers moved to RpcGroup handlers served through the worker-runner RPC protocol. + +- `WorkerRunner.make` -> `WorkerRunner.WorkerRunnerPlatform.start + WorkerRunner.WorkerRunner.run`: Start the platform runner and register the low-level message handler directly. + +- `WorkerRunner.makeSerialized` -> `RpcServer.makeProtocolWorkerRunner`: Serialized tagged-request execution moved to RpcServer with an RpcGroup handler layer. + +### `@effect/rpc/Rpc` + +- `Rpc.AddError` -> `effect/unstable/rpc/Rpc#AddError`: Retained; the added error must now be a Schema.Top and the resulting RPC also preserves its explicit service requirements. + +- `Rpc.AddMiddleware` -> `effect/unstable/rpc/Rpc#AddMiddleware`: Retained; middleware is now an RpcMiddleware.AnyService and its provides/requires metadata updates the RPC service requirements. + +- `Rpc.Any` -> `effect/unstable/rpc/Rpc#Any`: Retained as the erased RPC shape; use AnyWithProps when schema and middleware fields are required. + +- `Rpc.AnySchema` -> `Schema.Top`: The RPC-specific erased schema alias was removed; use the v4 top schema constraint. + +- `Rpc.AnyTaggedRequestSchema` -> `none`: RpcGroup no longer converts Schema.TaggedRequest classes into RPCs; declare the contract explicitly with Rpc.make. + +- `Rpc.Context` -> `effect/unstable/rpc/Rpc#Services`: Schema Context became decoding and encoding services; use Services, or ServicesClient / ServicesServer at the corresponding boundary. + +- `Rpc.ErrorEncoded` -> `Rpc.ErrorSchema["Encoded"]`: The alias was removed; index the v4 error schema's Encoded member directly. + +- `Rpc.ErrorExitEncoded` -> `Rpc.ErrorExitSchema["Encoded"]`: Use the new exit error schema, which includes stream and middleware errors, then select its Encoded member. + +- `Rpc.ErrorSchema` -> `effect/unstable/rpc/Rpc#ErrorSchema`: Retained; middleware errors now come from each service's error metadata. + +- `Rpc.Handler` -> `effect/unstable/rpc/Rpc#Handler`: Retained; handler metadata now supplies ServerClient, RequestId, headers, and the concrete RPC. + +- `Rpc.Middleware` -> `effect/unstable/rpc/Rpc#Middleware`: Retained and extracts Context.Service identifiers from the attached middleware services. + +- `Rpc.MiddlewareClient` -> `effect/unstable/rpc/Rpc#MiddlewareClient`: Retained; required client middleware is derived from services configured with requiredForClient. + +- `Rpc.Payload` -> `effect/unstable/rpc/Rpc#Payload`: Retained as the decoded payload type; use PayloadConstructor for the input accepted by generated clients. + +- `Rpc.Success` -> `effect/unstable/rpc/Rpc#Success`: Retained as the decoded success type. + +- `Rpc.SuccessChunkEncoded` -> `Rpc.SuccessExitSchema["Encoded"]`: The alias was removed; for a streaming RPC the exit success schema is the stream element schema. + +- `Rpc.SuccessEncoded` -> `effect/unstable/rpc/Rpc#SuccessEncoded`: Retained after the module move and now accounts for the RPC's explicit service-requirement parameter. + +- `Rpc.SuccessExitEncoded` -> `Rpc.SuccessExitSchema["Encoded"]`: Use the new exit success schema and select its Encoded member; streaming RPC exits use the element schema separately from the terminal void exit. + +- `Rpc.SuccessSchema` -> `effect/unstable/rpc/Rpc#SuccessSchema`: Retained and uses the v4 Schema.Top constraint. + +- `Rpc.Tag` -> `effect/unstable/rpc/Rpc#Tag`: Retained and also accounts for the v4 RPC service-requirement parameter. + +- `Rpc.TypeId` -> `none`: The RPC marker is private in v4; use Rpc.isRpc for runtime checks and Rpc.Any for type constraints. + +- `Rpc.WrapperTypeId` -> `none`: The wrapper marker is private in v4; use Rpc.isWrapper and the public Wrapper type. + +- `Rpc.fromTaggedRequest` -> `Rpc.make`: Automatic TaggedRequest conversion was removed; pass the tag, payload, success, and error schemas explicitly to Rpc.make. + +- `Rpc.make` -> `effect/unstable/rpc/Rpc#make`: Retained; schemas use v4 Schema.Top constraints and the defect option accepts Rpc.DefectSchema. + +- `Rpc.wrap` -> `effect/unstable/rpc/Rpc#wrap`: Retained after the module move; it still applies fork and uninterruptible handler options, while the return type is now uniformly Rpc.Wrapper. + +### `@effect/rpc/RpcClient` + +- `RpcClient.Protocol` -> `effect/unstable/rpc/RpcClient#Protocol`: Retained as a Context.Service; custom transports now route multiple client ids through run and send. + +- `RpcClient.RpcClient.From` -> `effect/unstable/rpc/RpcClient#RpcClient.From`: Generated clients now preserve full RPC tags as property names, remove the Prefix type parameter, and expose streaming results through the asQueue option instead of asMailbox. + +- `RpcClient.RpcClient.NonPrefixed` -> `none`: The prefix-partition helper was removed; v4 clients map every RPC tag directly to an object property. + +- `RpcClient.RpcClient.Prefixes` -> `none`: Nested prefix client objects were removed; v4 preserves the full RPC tag as the generated client property. + +- `RpcClient.currentHeaders` -> `effect/unstable/rpc/RpcClient#CurrentHeaders`: Renamed and changed from FiberRef to Context.Reference; prefer RpcClient.withHeaders for scoped overrides. + +- `RpcClient.makeProtocolHttp` -> `effect/unstable/rpc/RpcClient#makeProtocolHttp`: Retained; it creates the Protocol service implementation from an HttpClient. + +- `RpcClient.withHeadersEffect` -> `Effect.flatMap(headers, (value) => RpcClient.withHeaders(effect, value))`: withHeaders now accepts Headers.Input synchronously; evaluate effectful headers first and then scope the client effect. + +### `@effect/rpc/RpcClientError` + +- `RpcClientError.TypeId` -> `none`: The marker is private in v4; narrow with instanceof RpcClientError or inspect the public \_tag. + +### `@effect/rpc/RpcGroup` + +- `RpcGroup.Any` -> `effect/unstable/rpc/RpcGroup#Any`: Moved unchanged as the erased RpcGroup constraint. + +- `RpcGroup.HandlerContext` -> `effect/unstable/rpc/RpcGroup#HandlerServices`: Renamed for v4 service terminology and now includes explicit RPC requirements after removing middleware-provided services. + +- `RpcGroup.HandlersContext` -> `effect/unstable/rpc/RpcGroup#HandlersServices`: Renamed; it unions HandlerServices across the handler object. + +- `RpcGroup.TypeId` -> `none`: The group marker is private in v4; use RpcGroup.Any for an erased group constraint. + +### `@effect/rpc/RpcMessage` + +- `RpcMessage.RequestId` -> `effect/unstable/rpc/RpcMessage#RequestId`: Request ids are now branded string or number values; convert bigint ids before calling the retained RequestId constructor. + +- `RpcMessage.RequestIdTypeId` -> `effect/unstable/rpc/RpcMessage#RequestId`: The public symbol marker was removed; use the branded RequestId type and RequestId constructor rather than inspecting its brand. + +### `@effect/rpc/RpcMiddleware` + +- `RpcMiddleware.RpcMiddlewareWrap` -> `effect/unstable/rpc/RpcMiddleware#RpcMiddleware`: The wrap and non-wrap shapes were unified; implement a function receiving the handler effect and request options. + +- `RpcMiddleware.Tag` -> `effect/unstable/rpc/RpcMiddleware#Service`: Renamed and redesigned with explicit requires, provides, clientError, error, and requiredForClient configuration. + +- `RpcMiddleware.TagClass` -> `effect/unstable/rpc/RpcMiddleware#ServiceClass`: Renamed class type for the v4 Context.Service-based middleware declaration. + +- `RpcMiddleware.TagClass.Failure` -> `effect/unstable/rpc/RpcMiddleware#Error`: Failure terminology became error; apply the extractor to the middleware ID. + +- `RpcMiddleware.TagClass.FailureContext` -> `effect/unstable/rpc/RpcMiddleware#ErrorServicesEncode / ErrorServicesDecode`: The single schema context split into server encoding and client decoding services. + +- `RpcMiddleware.TagClass.FailureSchema` -> `effect/unstable/rpc/RpcMiddleware#ErrorSchema`: Renamed and applied to the middleware ID rather than constructor options. + +- `RpcMiddleware.TagClass.FailureService` -> `effect/unstable/rpc/RpcMiddleware#Error`: Use the decoded error extractor; optional middleware fallback was removed. + +- `RpcMiddleware.TagClass.Optional` -> `none`: Optional declaration and fallback-on-failure behavior were removed; model fallback inside the middleware effect. + +- `RpcMiddleware.TagClass.Provides` -> `effect/unstable/rpc/RpcMiddleware#Provides`: Moved to the module level and applied to the middleware ID metadata. + +- `RpcMiddleware.TagClass.RequiredForClient` -> `RpcMiddleware.ServiceClass["requiredForClient"]`: The standalone options extractor was removed; the boolean is exposed directly by the resulting service class. + +- `RpcMiddleware.TagClassAny` -> `effect/unstable/rpc/RpcMiddleware#AnyService`: Renamed widened middleware service-key shape. + +- `RpcMiddleware.TagClassAnyWithProps` -> `effect/unstable/rpc/RpcMiddleware#AnyServiceWithProps`: Renamed erased service key whose value has the unified server middleware function shape. + +- `RpcMiddleware.TypeId` -> `effect/unstable/rpc/RpcMiddleware#TypeId`: Retained as the public middleware metadata marker and now has a string-literal type. + +- `RpcMiddleware.layerClient` -> `effect/unstable/rpc/RpcMiddleware#layerClient`: Retained; the client function can now modify the typed Request passed to next and carry a client-only error type. + +### `@effect/rpc/RpcSchema` + +- `RpcSchema.Stream` -> `effect/unstable/rpc/RpcSchema#Stream`: Retained as both the stream schema interface and constructor; error is the second argument and schema services are split by direction. + +- `RpcSchema.StreamSchemaId` -> `none`: The stream marker is private in v4; use RpcSchema.isStreamSchema and getStreamSchemas. + +- `RpcSchema.getStreamSchemas` -> `effect/unstable/rpc/RpcSchema#getStreamSchemas`: Retained for internal-style schema inspection; pass the schema itself rather than its AST. + +- `RpcSchema.isStreamSchema` -> `effect/unstable/rpc/RpcSchema#isStreamSchema`: Retained; it accepts a v4 Schema.Constraint. + +- `RpcSchema.isStreamSerializable` -> `RpcSchema.isStreamSchema(schema)`: The separate WithResult serializability predicate was removed; v4 RPC streaming is identified by its explicit Stream schema. + +### `@effect/rpc/RpcServer` + +- `RpcServer.Protocol` -> `effect/unstable/rpc/RpcServer#Protocol`: Retained as a Context.Service; custom transports now expose a disconnect queue and explicit capability flags. + +- `RpcServer.fiberIdClientInterrupt` -> `effect/unstable/rpc/RpcSchema#ClientAbort`: The sentinel FiberId was replaced by a Cause annotation; inspect ClientAbort in the interruption cause when client cancellation must be distinguished. + +- `RpcServer.fiberIdTransientInterrupt` -> `none`: The internal transient sentinel was removed; protocol shutdown and disconnect now interrupt with the active parent fiber identity. + +- `RpcServer.layer` -> `effect/unstable/rpc/RpcServer#layer`: Moved to core Effect; server requirements are now derived with Rpc.ServicesServer rather than the former combined Rpc.Context alias. + +- `RpcServer.layerHttpRouter` -> `effect/unstable/rpc/RpcServer#layerHttp`: Renamed; it installs an HTTP or WebSocket RPC route into the v4 HttpRouter service. + +- `RpcServer.layerProtocolHttp` -> `effect/unstable/rpc/RpcServer#layerProtocolHttp`: Retained; v4 has one HttpRouter service and no router tag option. + +- `RpcServer.layerProtocolHttpRouter` -> `effect/unstable/rpc/RpcServer#layerProtocolHttp`: The separate layer-router variant was unified with layerProtocolHttp. + +- `RpcServer.layerProtocolWebsocketRouter` -> `effect/unstable/rpc/RpcServer#layerProtocolWebsocket`: Renamed after the HTTP router services were unified. + +- `RpcServer.make` -> `effect/unstable/rpc/RpcServer#make`: Retained; schema encoding services are now explicit server requirements. + +- `RpcServer.makeProtocolHttp` -> `effect/unstable/rpc/RpcServer#makeProtocolHttp`: Retained; it registers a POST route in the current v4 HttpRouter. + +- `RpcServer.makeProtocolHttpRouter` -> `effect/unstable/rpc/RpcServer#makeProtocolHttp`: The separate router constructor was unified with makeProtocolHttp. + +- `RpcServer.makeProtocolWebsocketRouter` -> `effect/unstable/rpc/RpcServer#makeProtocolWebsocket`: Renamed after the HTTP router services were unified. + +- `RpcServer.makeProtocolWithHttpApp` -> `effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffect`: HttpApp became HttpEffect; the result contains protocol and httpEffect. + +- `RpcServer.makeProtocolWithHttpAppWebsocket` -> `effect/unstable/rpc/RpcServer#makeProtocolWithHttpEffectWebsocket`: HttpApp became HttpEffect; the result contains the WebSocket protocol and upgrade effect. + +- `RpcServer.toHttpApp` -> `effect/unstable/rpc/RpcServer#toHttpEffect`: Renamed for the v4 HTTP effect model; it starts the RPC server and returns the request effect. + +- `RpcServer.toHttpAppWebsocket` -> `effect/unstable/rpc/RpcServer#toHttpEffectWebsocket`: Renamed for the v4 HTTP effect model; it returns the WebSocket upgrade effect. + +- `RpcServer.toWebHandler` -> `HttpRouter.toWebHandler(RpcServer.layerHttp(options).pipe(Layer.provide(options.layer)))`: The RPC convenience wrapper was removed; build the RPC route layer and convert it with the generic v4 HttpRouter web-handler adapter. + +### `@effect/rpc/RpcTest` + +- `RpcTest.makeClient` -> `effect/unstable/rpc/RpcTest#makeClient`: Retained; it uses the v4 no-serialization client/server path and requires handlers plus any server and client middleware services. + +### `@effect/sql-clickhouse/ClickhouseClient` + +- `ClickhouseClient.ClickhouseClient` -> `@effect/sql-clickhouse/ClickhouseClient#ClickhouseClient`: Retained; the service value is now a Context.Service rather than a GenericTag. + +- `ClickhouseClient.currentClickhouseSettings` -> `@effect/sql-clickhouse/ClickhouseClient#ClickhouseSettings`: Renamed and changed from FiberRef to Context.Reference; prefer client.withClickhouseSettings or provide the reference as a service. + +- `ClickhouseClient.currentClientMethod` -> `@effect/sql-clickhouse/ClickhouseClient#ClientMethod`: Renamed and changed from FiberRef to Context.Reference; prefer client.asCommand or provide the reference as a service. + +- `ClickhouseClient.currentQueryId` -> `@effect/sql-clickhouse/ClickhouseClient#QueryId`: Renamed and changed from FiberRef to Context.Reference; prefer client.withQueryId or provide the reference as a service. + +### `@effect/sql-clickhouse/ClickhouseMigrator` + +- `ClickhouseMigrator.MigrationError` -> `@effect/sql-clickhouse/ClickhouseMigrator#MigrationError`: Retained via effect/unstable/sql/Migrator; migrate reason and its lowercase values to kind with PascalCase values. + +### `@effect/sql-d1/D1Client` + +- `D1Client.D1ClientConfig` -> `@effect/sql-d1/D1Client#D1ClientConfig`: Retained; prepareCacheTTL now uses Duration.Input. + +### `@effect/sql-drizzle/Mysql` + +- `Mysql.MysqlDrizzle` -> `drizzle-orm/effect-mysql2#EffectMysql2Database`: The service tag was removed; use the database type and define an application Context.Tag if service access is required. + +- `Mysql.layer` -> `Layer.effect(AppDb, MysqlDrizzle.makeWithDefaults())`: The package was removed; import MysqlDrizzle from drizzle-orm/effect-mysql2, define an application service tag, and compose with MysqlClient.layer. + +- `Mysql.layerWithConfig` -> `Layer.effect(AppDb, MysqlDrizzle.makeWithDefaults(config))`: The package was removed; use drizzle-orm/effect-mysql2, port config to EffectDrizzleMySqlConfig, and define an application service tag. + +- `Mysql.make` -> `drizzle-orm/effect-mysql2#makeWithDefaults`: Use Drizzle's Effect 4 integration; it returns EffectMysql2Database and requires MysqlClient. + +- `Mysql.makeWithConfig` -> `drizzle-orm/effect-mysql2#makeWithDefaults`: The constructor split was removed; use makeWithDefaults(config), or make(config) when explicitly providing logger and cache services. + +### `@effect/sql-drizzle/Pg` + +- `Pg.PgDrizzle` -> `drizzle-orm/effect-postgres#EffectPgDatabase`: The service tag was removed; use the database type and define an application Context.Tag if service access is required. + +- `Pg.layer` -> `Layer.effect(AppDb, PgDrizzle.makeWithDefaults())`: The package was removed; import PgDrizzle from drizzle-orm/effect-postgres, define an application service tag, and compose with PgClient.layer. + +- `Pg.layerWithConfig` -> `Layer.effect(AppDb, PgDrizzle.makeWithDefaults(config))`: The package was removed; use drizzle-orm/effect-postgres, port config to EffectDrizzlePgConfig, and define an application service tag. + +- `Pg.make` -> `drizzle-orm/effect-postgres#makeWithDefaults`: Use Drizzle's Effect 4 integration; it returns EffectPgDatabase and requires PgClient. + +- `Pg.makeWithConfig` -> `drizzle-orm/effect-postgres#makeWithDefaults`: The constructor split was removed; use makeWithDefaults(config), or make(config) when explicitly providing logger and cache services. + +### `@effect/sql-drizzle/Sqlite` + +- `Sqlite.SqliteDrizzle` -> `matching drizzle-orm Effect SQLite database type`: The generic service tag was removed; use the backend-specific database type and define an application Context.Tag if needed. + +- `Sqlite.layer` -> `Layer.effect(AppDb, SqliteDrizzle.makeWithDefaults())`: The package was removed; select the matching drizzle-orm Effect backend module, define an application service tag, and compose with its SQL client layer. + +- `Sqlite.layerWithConfig` -> `Layer.effect(AppDb, SqliteDrizzle.makeWithDefaults(config))`: Select the matching drizzle-orm Effect backend, port config to EffectDrizzleSQLiteConfig, and define an application service tag. + +- `Sqlite.make` -> `drizzle-orm/effect-sqlite-node#makeWithDefaults`: SQLite integration is backend-specific; use the module matching sql-sqlite-node, -bun, -do, -wasm, libsql, or d1. + +- `Sqlite.makeWithConfig` -> `matching drizzle-orm Effect SQLite module#makeWithDefaults`: The generic constructor was removed; select the concrete backend and use makeWithDefaults(config), or make(config) with explicit services. + +### `@effect/sql-mssql/MssqlClient` + +- `MssqlClient.MssqlClient` -> `@effect/sql-mssql/MssqlClient#MssqlClient`: Retained; the service value is now a Context.Service rather than a GenericTag. + +- `MssqlClient.MssqlClientConfig` -> `@effect/sql-mssql/MssqlClient#MssqlClientConfig`: Retained; durations use Duration.Input, parameterTypes is keyed by Statement.PrimitiveKind, and v4 adds retry and timeout options. + +### `@effect/sql-mssql/Parameter` + +- `Parameter.Parameter` -> `@effect/sql-mssql/Parameter#Parameter`: Retained; the phantom brand key was renamed from ParameterId to TypeId. + +- `Parameter.ParameterId` -> `@effect/sql-mssql/Parameter#TypeId`: Renamed; use TypeId for direct brand-key and type references. + +### `@effect/sql-mssql/Procedure` + +- `Procedure.Procedure` -> `@effect/sql-mssql/Procedure#Procedure`: Retained with the same generics and fields. + +- `Procedure.Procedure.ParametersRecord` -> `@effect/sql-mssql/Procedure#Procedure.ParametersRecord`: Retained unchanged; from the deep module it is also available as Procedure.ParametersRecord. + +### `@effect/sql-mysql2/MysqlClient` + +- `MysqlClient.MysqlClientConfig` -> `@effect/sql-mysql2/MysqlClient#MysqlClientConfig`: Retained; connectionTTL uses Duration.Input and v4 adds disablePreparedStatements. + +### `@effect/sql-pg/PgClient` + +- `PgClient.PgClient` -> `@effect/sql-pg/PgClient#PgClient`: Retained; the service value is now a Context.Service. + +- `PgClient.PgClientConfig` -> `@effect/sql-pg/PgClient#PgClientConfig / PgPoolConfig`: Use PgClientConfig for base settings and PgPoolConfig for make/layer; pool sizing, idle timeout, and connection TTL moved to PgPoolConfig. + +- `PgClient.PgClientFromPoolOptions` -> `Parameters[0]`: The named type was removed; derive the inline fromPool option type. PgPoolConfig is for creating a managed pool and is not equivalent. + +- `PgClient.layerFromPool` -> `PgClient.layerFrom(PgClient.fromPool(options))`: Compose fromPool with layerFrom; layerFrom now accepts an Effect acquiring a PgClient rather than pool options. + +### `@effect/sql-sqlite-bun/SqliteClient` + +- `SqliteClient.SqliteClient` -> `@effect/sql-sqlite-bun/SqliteClient#SqliteClient`: Retained; the service value is now a Context.Service. + +### `@effect/sql-sqlite-do/SqliteClient` + +- `SqliteClient.SqliteClientConfig` -> `@effect/sql-sqlite-do/SqliteClient#SqliteClientConfig`: Retained; db is optional and storage may be supplied, but one of db or storage is required at runtime. + +### `@effect/sql-sqlite-node/SqliteClient` + +- `SqliteClient.SqliteClient` -> `@effect/sql-sqlite-node/SqliteClient#SqliteClient`: Retained on node:sqlite, but the byte-export member was removed; use backup(destination) for file backup. + +- `SqliteClient.SqliteClientConfig` -> `@effect/sql-sqlite-node/SqliteClient#SqliteClientConfig`: Retained; prepareCacheTTL now uses Duration.Input. + +### `@effect/sql-sqlite-react-native/SqliteClient` + +- `SqliteClient.asyncQuery` -> `@effect/sql-sqlite-react-native/SqliteClient#AsyncQuery`: Renamed and changed from FiberRef to Context.Reference; prefer withAsyncQuery or provide AsyncQuery as a service. + +### `@effect/sql-sqlite-wasm/SqliteClient` + +- `SqliteClient.SqliteClient` -> `@effect/sql-sqlite-wasm/SqliteClient#SqliteClient`: Retained with the same export/import surface; the service value is now a Context.Service. + +- `SqliteClient.currentTransferables` -> `@effect/sql-sqlite-wasm/SqliteClient#Transferables`: Renamed and changed from FiberRef to Context.Reference; prefer withTransferables or provide Transferables as a service. + +### `@effect/sql/Model` + +- `Model.Any` -> `effect/unstable/schema/Model#Any`: Moved; v4 schemas track DecodingServices and EncodingServices separately instead of one Context type. + +- `Model.AnyNoContext` -> `effect/unstable/schema/Model#Any`: The distinct no-context alias was removed; Model.Any propagates decoding and encoding services. Constrain both service types to never when required. + +- `Model.BooleanFromNumber` -> `effect/Schema#BooleanFromBit`: Use the core 0 | 1 to boolean schema; Model.BooleanSqlite is the ready-made model field. + +- `Model.Class` -> `effect/unstable/schema/Model#Class`: Moved; model variants remain select, insert, update, json, jsonCreate, and jsonUpdate. + +- `Model.DateTimeFromDate` -> `effect/Schema#DateTimeUtcFromDate`: Moved to core Schema and retains Date to DateTime.Utc conversion. + +- `Model.Generated` -> `effect/unstable/schema/Model#GeneratedByDb`: Renamed and now read-only, with select and json variants only. Use Model.Field with select, update, and json to preserve writable v3 behavior. + +- `Model.extract` -> `effect/unstable/schema/Model#extract`: Retained after moving the model variant helpers into core Effect's unstable schema package. + +- `Model.fieldFromKey` -> `effect/Schema#encodeKeys`: The field helper was removed; apply encodeKeys to each concrete struct or model-variant schema that crosses the naming boundary. + +- `Model.fields` -> `effect/unstable/schema/Model#fields`: Moved with the variant-model helpers into core Effect's unstable schema package. + +- `Model.makeDataLoaders` -> `effect/unstable/sql/SqlModel#makeResolvers`: Returns RequestResolvers instead of callable loaders; execute with SqlResolver.request and use RequestResolver delay/batch combinators for batching controls. + +### `@effect/sql/SqlClient` + +- `SqlClient.SafeIntegers` -> `effect/unstable/sql/SqlClient#SafeIntegers`: Moved and changed from a Reference subclass to a Context.Reference value; provide it as a service. + +- `SqlClient.TransactionConnection` -> `effect/unstable/sql/SqlClient#TransactionConnection`: Now a factory keyed by client id, not a singleton tag. Prefer the client's transactionService; the payload type is TransactionConnection.Service. + +- `SqlClient.TypeId` -> `none`: The brand is private in v4; do not inspect or attach it, and obtain clients through SqlClient or SqlClient.make. + +- `SqlClient.make` -> `effect/unstable/sql/SqlClient#make`: Moved; custom clients rename MakeOptions.reactiveMailbox to reactiveQueue and may supply transactionService. + +### `@effect/sql/SqlConnection` + +- `SqlConnection.Connection` -> `effect/unstable/sql/SqlConnection#Connection`: Moved; Connection.Acquirer is now top-level SqlConnection.Acquirer, and custom connections must implement executeValuesUnprepared. + +### `@effect/sql/SqlError` + +- `SqlError.SqlErrorTypeId` -> `effect/unstable/sql/SqlError#isSqlError`: The type id is private; use isSqlError for runtime narrowing or isSqlErrorReason for structured reason values. + +### `@effect/sql/SqlEventJournal` + +- `SqlEventJournal.layer` -> `effect/unstable/eventlog/SqlEventJournal#layer`: Moved; rename the eventLogTable layer option to entryTable. + +- `SqlEventJournal.make` -> `effect/unstable/eventlog/SqlEventJournal#make`: Moved with the same entryTable and remotesTable options. + +### `@effect/sql/SqlEventLogServer` + +- `SqlEventLogServer.layerStorage` -> `effect/unstable/eventlog/SqlEventLogServerEncrypted#layerStorage`: Moved to the encrypted server module with the same options and EventLogEncryption requirement. + +- `SqlEventLogServer.makeStorage` -> `effect/unstable/eventlog/SqlEventLogServerEncrypted#makeStorage`: Moved to the encrypted server module with the same SQL, encryption, and scope requirements. + +### `@effect/sql/SqlPersistedQueue` + +- `SqlPersistedQueue.layerStore` -> `effect/unstable/persistence/PersistedQueue#layerStoreSql`: Moved into PersistedQueue and renamed with the Sql suffix; options are unchanged. + +- `SqlPersistedQueue.make` -> `effect/unstable/persistence/PersistedQueue#makeStoreSql`: Use the SQL store constructor; PersistedQueue.make creates a typed queue from a store factory and is not equivalent. + +### `@effect/sql/SqlResolver` + +- `SqlResolver.SqlResolver` -> `RequestResolver.RequestResolver>`: The wrapper interface was removed; constructors return RequestResolvers. Execute them with effect/unstable/sql/SqlResolver#request. + +- `SqlResolver.void` -> `effect/unstable/sql/SqlResolver#void`: Moved, but remove the leading tag and withContext arguments; it now returns a RequestResolver synchronously and runs through SqlResolver.request. + +### `@effect/sql/SqlSchema` + +- `SqlSchema.single` -> `effect/unstable/sql/SqlSchema#findOne`: Renamed with the same first-row-or-fail behavior; empty results use Cause.NoSuchElementError and schema failures use Schema.SchemaError. + +- `SqlSchema.void` -> `effect/unstable/sql/SqlSchema#void`: Moved with the same encode, execute, and discard-result pattern; schema failures now use Schema.SchemaError. + +### `@effect/sql/Statement` + +- `Statement.FragmentId` -> `none`: The v4 fragment brand is private; use Fragment, fragment, and isFragment instead of direct type-id access. + +- `Statement.Statement` -> `effect/unstable/sql/Statement#Statement`: Moved; the nested Transformer type is now top-level and its callback receives Fiber.Fiber rather than FiberRefs.FiberRefs. + +- `Statement.currentTransformer` -> `effect/unstable/sql/Statement#CurrentTransformer`: Capitalized and changed from FiberRef\\> to Context.Reference\. + +- `Statement.custom` -> `effect/unstable/sql/Statement#custom`: Retained, but returns a Custom segment and uses paramA/paramB/paramC; wrap it with Statement.fragment when a Fragment is required. + +- `Statement.defaultEscape` -> `effect/unstable/sql/Statement#defaultEscape`: Moved with the same signature. + +- `Statement.join` -> `effect/unstable/sql/Statement#join`: Moved with the same empty, single, and multiple-clause behavior. + +- `Statement.make` -> `effect/unstable/sql/Statement#make`: Moved with the same constructor inputs. + +- `Statement.makeCompiler` -> `effect/unstable/sql/Statement#makeCompiler`: Moved to core Effect; the constructor options are exposed as Statement.CompilerOptions and retain the dialect-specific callbacks. + +- `Statement.setTransformer` -> `Layer.succeed(Statement.CurrentTransformer, transformer)`: The helper was removed; provide the CurrentTransformer reference as a layer. + +- `Statement.unsafeFragment` -> `Statement.fragment([Statement.literal(sql, params)])`: The helper was removed; construct the low-level fragment explicitly, or use the active constructor's sql.unsafe for an executable statement. + +- `Statement.withTransformer` -> `Effect.provideService(Statement.CurrentTransformer, transformer)`: The helper was removed; locally provide the transformer reference around the effect. + +- `Statement.withTransformerDisabled` -> `Effect.provideService(Statement.CurrentTransformer, undefined)`: The helper was removed; locally provide undefined for the transformer reference. + +### `@effect/typeclass/Bounded` + +- `Bounded.Bounded` -> `none`: V4 removed Bounded dictionaries. Keep the Order and minimum/maximum bounds as separate application values. + +- `Bounded.BoundedTypeLambda` -> `none`: V4 removed the @effect/typeclass higher-kinded Bounded instance machinery. + +- `Bounded.between` -> `Order.isBetween(B.compare)`: Use the v4 Order predicate with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed. + +- `Bounded.clamp` -> `Order.clamp(B.compare)`: Use the v4 Order combinator with { minimum: B.minBound, maximum: B.maxBound }; the Bounded dictionary itself was removed. + +- `Bounded.reverse` -> `Order.flip(B.compare)`: Flip the Order and swap the separately stored minimum and maximum bounds; v4 has no bundled Bounded dictionary. + +### `@effect/typeclass/Monoid` + +- `Monoid.Monoid` -> `Reducer.Reducer`: Reducer replaces Monoid in v4; empty is renamed initialValue and combineAll remains available. + +- `Monoid.array` -> `Array.makeReducerConcat`: Use the v4 array concatenation Reducer; Reducer replaces Monoid and names the identity initialValue. + +- `Monoid.fromSemigroup` -> `Reducer.make(S.combine, empty)`: Construct a v4 Reducer from the replacement Combiner operation and identity value. + +- `Monoid.reverse` -> `Reducer.flip`: Use the v4 Reducer combinator; it preserves initialValue and reverses combine argument order. + +- `Monoid.struct` -> `Struct.makeReducer`: Pass a record of v4 Reducers to derive a field-wise Reducer. + +- `Monoid.tuple` -> `Tuple.makeReducer`: Pass one array of v4 Reducers instead of rest Monoid arguments. + +### `@effect/typeclass/Semigroup` + +- `Semigroup.Invariant` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. + +- `Semigroup.Product` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. + +- `Semigroup.SemiProduct` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. + +- `Semigroup.Semigroup` -> `Combiner.Combiner`: Combiner replaces Semigroup in v4 and retains the binary combine method; combineMany was removed. + +- `Semigroup.SemigroupTypeLambda` -> `none`: The @effect/typeclass package was removed in v4 with no generic typeclass layer replacement. Rewrite this abstraction against the concrete v4 data type and its module functions. + +- `Semigroup.array` -> `Array.makeReducerConcat`: The v4 concatenation Reducer is also a Combiner and replaces the array Semigroup. + +- `Semigroup.constant` -> `Combiner.constant`: Combiner replaces Semigroup in v4. + +- `Semigroup.first` -> `Combiner.first`: Combiner replaces Semigroup in v4. + +- `Semigroup.imap` -> `Combiner.make`: V4 has no generic invariant instance; build a Combiner that maps both inputs with from, combines them, then maps the result with to. + +- `Semigroup.intercalate` -> `Combiner.intercalate`: Combiner replaces Semigroup; v4 takes the separator first and then the Combiner. + +- `Semigroup.last` -> `Combiner.last`: Combiner replaces Semigroup in v4. + +- `Semigroup.make` -> `Combiner.make`: Combiner replaces Semigroup. V4 accepts only the binary combine function and has no combineMany override. + +- `Semigroup.reverse` -> `Combiner.flip`: Use the v4 Combiner combinator to reverse combine argument order. + +- `Semigroup.struct` -> `Struct.makeCombiner`: Pass a record of v4 Combiners to derive a field-wise Combiner. + +- `Semigroup.tuple` -> `Tuple.makeCombiner`: Pass one array of v4 Combiners instead of rest Semigroup arguments. + +### `@effect/typeclass/data/Array` + +- `Array.Applicative` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Chainable` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Covariant` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Filterable` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.FlatMap` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Foldable` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Invariant` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Monad` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Of` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Pointed` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Product` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.SemiApplicative` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.SemiProduct` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.Traversable` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.TraversableFilterable` -> `effect/Array`: The @effect/typeclass package and its Array instance dictionaries were removed in v4. Use the concrete effect/Array operations directly. + +- `Array.getMonoid` -> `Array.makeReducerConcat`: Use the v4 concatenation Reducer; Reducer replaces Monoid. + +- `Array.getSemigroup` -> `Array.makeReducerConcat`: The v4 concatenation Reducer is also a Combiner and replaces this Semigroup. + +### `@effect/typeclass/data/BigInt` + +- `BigInt.MonoidMultiply` -> `BigInt.ReducerMultiply`: Renamed and moved to the concrete v4 BigInt module. + +- `BigInt.MonoidSum` -> `BigInt.ReducerSum`: Renamed and moved to the concrete v4 BigInt module. + +- `BigInt.SemigroupMax` -> `BigInt.CombinerMax`: Renamed and moved to the concrete v4 BigInt module. + +- `BigInt.SemigroupMin` -> `BigInt.CombinerMin`: Renamed and moved to the concrete v4 BigInt module. + +- `BigInt.SemigroupMultiply` -> `BigInt.ReducerMultiply`: The v4 Reducer is also a Combiner and preserves multiplication combine semantics. + +- `BigInt.SemigroupSum` -> `BigInt.ReducerSum`: The v4 Reducer is also a Combiner and preserves addition combine semantics. + +### `@effect/typeclass/data/Boolean` + +- `Boolean.MonoidEqv` -> `Reducer.make(Boolean.eqv, true)`: Rebuild the removed instance with the v4 boolean operation and its identity. + +- `Boolean.MonoidEvery` -> `Boolean.ReducerAnd`: Renamed and moved to the concrete v4 Boolean module. + +- `Boolean.MonoidSome` -> `Boolean.ReducerOr`: Renamed and moved to the concrete v4 Boolean module. + +- `Boolean.MonoidXor` -> `Reducer.make(Boolean.xor, false)`: Rebuild the removed instance with the v4 boolean operation and its identity. + +- `Boolean.SemigroupEqv` -> `Combiner.make(Boolean.eqv)`: Rebuild the removed instance as a v4 Combiner. + +- `Boolean.SemigroupEvery` -> `Boolean.ReducerAnd`: The v4 Reducer is also a Combiner and preserves logical-AND combine semantics. + +- `Boolean.SemigroupSome` -> `Boolean.ReducerOr`: The v4 Reducer is also a Combiner and preserves logical-OR combine semantics. + +- `Boolean.SemigroupXor` -> `Combiner.make(Boolean.xor)`: Rebuild the removed instance as a v4 Combiner. + +### `@effect/typeclass/data/Duration` + +- `Duration.Bounded` -> `none`: V4 has no Bounded dictionary; use Duration.Order with Duration.zero and Duration.infinity as separate bounds. + +- `Duration.MonoidMax` -> `Reducer.make(Duration.max, Duration.zero)`: Rebuild the removed maximum Monoid as a v4 Reducer with the same identity. + +- `Duration.MonoidMin` -> `Reducer.make(Duration.min, Duration.infinity)`: Rebuild the removed minimum Monoid as a v4 Reducer with the same identity. + +- `Duration.MonoidSum` -> `Duration.ReducerSum`: Renamed and moved to the concrete v4 Duration module. + +- `Duration.SemigroupMax` -> `Duration.CombinerMax`: Renamed and moved to the concrete v4 Duration module. + +- `Duration.SemigroupMin` -> `Duration.CombinerMin`: Renamed and moved to the concrete v4 Duration module. + +- `Duration.SemigroupSum` -> `Duration.ReducerSum`: The v4 Reducer is also a Combiner and preserves Duration.sum combine semantics. + +### `@effect/typeclass/data/Effect` + +- `Effect.Chainable` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.ConcurrencyOptions` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.Covariant` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.FlatMap` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.Invariant` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.Monad` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.Of` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.Pointed` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.getApplicative` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.getProduct` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.getSemiApplicative` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +- `Effect.getSemiProduct` -> `effect/Effect`: The @effect/typeclass package and its Effect instance dictionaries were removed in v4. Use the concrete effect/Effect operations directly. + +### `@effect/typeclass/data/Either` + +- `Either.Applicative` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Bicovariant` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Chainable` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Covariant` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.FlatMap` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Foldable` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Invariant` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Monad` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Of` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Pointed` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Product` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.SemiAlternative` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.SemiApplicative` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.SemiCoproduct` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.SemiProduct` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +- `Either.Traversable` -> `effect/Either`: The @effect/typeclass package and its Either instance dictionaries were removed in v4. Use the concrete effect/Either operations directly. + +### `@effect/typeclass/data/Micro` + +- `Micro.Chainable` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.ConcurrencyOptions` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.Covariant` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.FlatMap` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.Invariant` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.Monad` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.Of` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.Pointed` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.getApplicative` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.getProduct` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.getSemiApplicative` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +- `Micro.getSemiProduct` -> `effect/Micro`: The @effect/typeclass package and its Micro instance dictionaries were removed in v4. Use the concrete effect/Micro operations directly. + +### `@effect/typeclass/data/Number` + +- `Number.Bounded` -> `none`: V4 has no Bounded dictionary; use Number.Order with -Infinity and Infinity as separate bounds. + +- `Number.MonoidMax` -> `Number.ReducerMax`: Renamed and moved to the concrete v4 Number module. + +- `Number.MonoidMin` -> `Number.ReducerMin`: Renamed and moved to the concrete v4 Number module. + +- `Number.MonoidMultiply` -> `Number.ReducerMultiply`: Renamed and moved to the concrete v4 Number module. + +- `Number.MonoidSum` -> `Number.ReducerSum`: Renamed and moved to the concrete v4 Number module. + +- `Number.SemigroupMax` -> `Number.ReducerMax`: The v4 Reducer is also a Combiner and preserves maximum combine semantics. + +- `Number.SemigroupMin` -> `Number.ReducerMin`: The v4 Reducer is also a Combiner and preserves minimum combine semantics. + +- `Number.SemigroupMultiply` -> `Number.ReducerMultiply`: The v4 Reducer is also a Combiner and preserves multiplication combine semantics. + +- `Number.SemigroupSum` -> `Number.ReducerSum`: The v4 Reducer is also a Combiner and preserves addition combine semantics. + +### `@effect/typeclass/data/Option` + +- `Option.Alternative` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Applicative` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Chainable` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Coproduct` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Covariant` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Filterable` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.FlatMap` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Foldable` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Invariant` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Monad` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Of` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Pointed` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Product` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.SemiAlternative` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.SemiApplicative` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.SemiCoproduct` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.SemiProduct` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.Traversable` -> `effect/Option`: The @effect/typeclass package and its Option instance dictionaries were removed in v4. Use the concrete effect/Option operations directly. + +- `Option.getOptionalMonoid` -> `Option.makeReducer`: Pass the replacement Combiner; the v4 Reducer uses None as initialValue and combines two Some values. + +### `@effect/typeclass/data/Ordering` + +- `Ordering.Monoid` -> `Ordering.Reducer`: Renamed and moved to the concrete v4 Ordering module. + +- `Ordering.Semigroup` -> `Ordering.Reducer`: The v4 Reducer is also a Combiner and preserves Ordering combination semantics. + +### `@effect/typeclass/data/Predicate` + +- `Predicate.Contravariant` -> `effect/Predicate`: The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly. + +- `Predicate.Invariant` -> `effect/Predicate`: The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly. + +- `Predicate.Of` -> `effect/Predicate`: The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly. + +- `Predicate.Product` -> `effect/Predicate`: The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly. + +- `Predicate.SemiProduct` -> `effect/Predicate`: The @effect/typeclass package and its Predicate instance dictionaries were removed in v4. Use the concrete effect/Predicate operations directly. + +- `Predicate.getMonoidEqv` -> `Reducer.make(Predicate.eqv, Predicate.isUnknown)`: Rebuild the removed predicate instance as a v4 Reducer with the always-true predicate as initialValue. + +- `Predicate.getMonoidEvery` -> `Reducer.make(Predicate.and, Predicate.isUnknown)`: Rebuild the removed predicate instance as a v4 Reducer with the always-true predicate as initialValue. + +- `Predicate.getMonoidSome` -> `Reducer.make(Predicate.or, Predicate.isNever)`: Rebuild the removed predicate instance as a v4 Reducer with the always-false predicate as initialValue. + +- `Predicate.getMonoidXor` -> `Reducer.make(Predicate.xor, Predicate.isNever)`: Rebuild the removed predicate instance as a v4 Reducer with the always-false predicate as initialValue. + +- `Predicate.getSemigroupEqv` -> `Combiner.make(Predicate.eqv)`: Rebuild the removed predicate instance as a v4 Combiner. + +- `Predicate.getSemigroupEvery` -> `Combiner.make(Predicate.and)`: Rebuild the removed predicate instance as a v4 Combiner. + +- `Predicate.getSemigroupSome` -> `Combiner.make(Predicate.or)`: Rebuild the removed predicate instance as a v4 Combiner. + +- `Predicate.getSemigroupXor` -> `Combiner.make(Predicate.xor)`: Rebuild the removed predicate instance as a v4 Combiner. + +### `@effect/typeclass/data/Record` + +- `Record.Covariant` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.Filterable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.Invariant` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.Traversable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.TraversableFilterable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.getCovariant` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.getFilterable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.getInvariant` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.getMonoidUnion` -> `Record.makeReducerUnion`: Pass the replacement value Combiner; the v4 Reducer uses an empty record as initialValue. + +- `Record.getSemigroupIntersection` -> `Record.makeReducerIntersection`: Pass the replacement value Combiner and use the returned Reducer's combine operation for pairwise intersection. + +- `Record.getSemigroupUnion` -> `Record.makeReducerUnion`: The v4 Reducer is also a Combiner and preserves pairwise union semantics. + +- `Record.getTraversable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +- `Record.getTraversableFilterable` -> `effect/Record`: The @effect/typeclass package and its Record instance dictionaries were removed in v4. Use the concrete effect/Record operations directly. + +### `@effect/typeclass/data/String` + +- `String.Monoid` -> `String.ReducerConcat`: Renamed and moved to the concrete v4 String module. + +- `String.Semigroup` -> `String.ReducerConcat`: The v4 Reducer is also a Combiner and preserves string concatenation. + +### `@effect/typeclass/data/Tuple` + +- `Tuple.Bicovariant` -> `effect/Tuple`: The @effect/typeclass package and its Tuple instance dictionaries were removed in v4. Use the concrete effect/Tuple operations directly. + +### `@effect/vitest/index` + +- `index.ApiConfig` -> `vitest/node#ApiConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.ArgumentsType` -> `T extends (...args: infer A) => any ? A : never`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.Arrayable` -> `T | Array`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.Awaitable` -> `T | PromiseLike`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.BaseCoverageOptions` -> `vitest/node#BaseCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.BenchmarkUserOptions` -> `vitest/node#BenchmarkUserOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.BrowserConfigOptions` -> `vitest/node#BrowserConfigOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.BrowserScript` -> `vitest/node#BrowserScript`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.BuiltinEnvironment` -> `vitest/node#BuiltinEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CSSModuleScopeStrategy` -> `vitest/node#CSSModuleScopeStrategy`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CollectLineNumbers` -> `vitest/node#TypeCheckCollectLineNumbers`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.CollectLines` -> `vitest/node#TypeCheckCollectLines`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.Constructable` -> `new (...args: any[]) => any`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.Context` -> `vitest/node#TypeCheckContext`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.CoverageIstanbulOptions` -> `vitest/node#CoverageIstanbulOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CoverageOptions` -> `vitest/node#CoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CoverageProvider` -> `vitest/node#CoverageProvider`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CoverageProviderModule` -> `vitest/node#CoverageProviderModule`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CoverageReporter` -> `vitest/node#CoverageReporter`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.CoverageV8Options` -> `vitest/node#CoverageV8Options`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.Custom` -> `vitest#RunnerTestCase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.CustomProviderOptions` -> `vitest/node#CustomProviderOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.DepsOptimizationOptions` -> `vitest/node#DepsOptimizationOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.DoneCallback` -> `none`: Vitest does not support callback-style tests. Return a Promise or, in @effect/vitest tests, return an Effect. + +- `index.Environment` -> `vitest/environments#Environment`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. + +- `index.EnvironmentOptions` -> `vitest/node#EnvironmentOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.EnvironmentReturn` -> `vitest/environments#EnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. + +- `index.ErrorWithDiff` -> `vitest#TestError`: Vitest 3 deprecated ErrorWithDiff in favor of TestError; review the tightened actual, expected, and cause fields. + +- `index.ExtendedContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods. + +- `index.File` -> `vitest#RunnerTestFile`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.HappyDOMOptions` -> `NonNullable`: Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. + +- `index.HookCleanupCallback` -> `none`: No named Vitest 4 export replaces this alias. Let the hook return type infer, or type the cleanup function locally. + +- `index.HookListener` -> `none`: Use the matching @vitest/runner hook-specific type such as BeforeAllListener, AfterAllListener, BeforeEachListener, or AfterEachListener for custom runner code. + +- `index.InlineConfig` -> `vitest/node#InlineConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.JSDOMOptions` -> `NonNullable`: Vitest 4 keeps this shape only as a property of EnvironmentOptions; derive it from the public vitest/node type. + +- `index.Mock` -> `vitest#Mock`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. + +- `index.ModuleCache` -> `none`: Vitest 3 marked this unused internal cache shape deprecated; Vitest 4 has no public replacement. + +- `index.MutableArray` -> `{ -readonly [K in keyof T]: T[K] }`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.Nullable` -> `T | null | undefined`: Vitest 3 marked this root alias as an internal helper. Define the small TypeScript shape locally instead of depending on transitive internals. + +- `index.Pool` -> `vitest/node#Pool`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.PoolOptions` -> `vitest/config#TestUserConfig`: The v3 built-in poolOptions object was removed. Move its fields to Vitest 4 top-level config such as maxWorkers and vmMemoryLimit; vitest/node PoolOptions is a different custom-pool API. + +- `index.ProjectConfig` -> `vitest/node#ProjectConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.RawErrsMap` -> `vitest/node#TypeCheckRawErrorsMap`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.ReportContext` -> `vitest/node#ReportContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.Reporter` -> `vitest/reporters#Reporter`: Import Reporter from the public plural vitest/reporters entrypoint; its lifecycle methods changed in Vitest 4. + +- `index.ResolveIdFunction` -> `none`: This deprecated vite-node callback was removed. Use Vite environment or module-runner APIs. + +- `index.ResolvedConfig` -> `vitest/node#ResolvedConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.ResolvedCoverageOptions` -> `vitest/node#ResolvedCoverageOptions`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.ResolvedTestEnvironment` -> `none`: Vitest 3 marked this type unsupported. Use Environment from vitest/environments for custom environments. + +- `index.RootAndTarget` -> `vitest/node#TypeCheckRootAndTarget`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.RunnerCustomCase` -> `vitest#RunnerTestCase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.RuntimeContext` -> `@vitest/runner#RuntimeContext`: Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should avoid this internal state type. + +- `index.SequenceHooks` -> `vitest/node#SequenceHooks`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.SequenceSetupFiles` -> `vitest/node#SequenceSetupFiles`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.SerializableSpec` -> `vitest#SerializedTestSpecification`: Use the non-deprecated Vitest name; SerializableSpec was only an alias. + +- `index.Suite` -> `vitest#RunnerTestSuite`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.SuiteHooks` -> `@vitest/runner#SuiteHooks`: Custom-runner code can add an explicit @vitest/runner dependency; ordinary tests should use public hook functions. + +- `index.Task` -> `vitest#RunnerTask`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.TaskBase` -> `vitest#RunnerTaskBase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.TaskContext` -> `vitest#TestContext`: The separate context alias was removed. Vitest 4 uses TestContext, which includes the current task and lifecycle methods. + +- `index.TaskResult` -> `vitest#RunnerTaskResult`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.TaskResultPack` -> `vitest#RunnerTaskResultPack`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.Test` -> `vitest#RunnerTestCase`: Vitest 4 removed the deprecated unprefixed runner alias. Import the explicit Runner\* type from vitest. + +- `index.TransformModePatterns` -> `none`: This was removed with vite-node transform modes. Configure the Vite environment and its dependency optimizer instead. + +- `index.TscErrorInfo` -> `vitest/node#TypeCheckErrorInfo`: Vitest 3 deprecated the root alias in favor of this renamed vitest/node type. + +- `index.TypecheckConfig` -> `vitest/node#TypecheckConfig`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.UserConfig` -> `vitest/config#TestUserConfig`: Vitest 4 exposes its config as TestUserConfig; ViteUserConfig is the separate Vite configuration type. + +- `index.UserWorkspaceConfig` -> `vitest/config#UserWorkspaceConfig`: Import the type from vitest/config and migrate Vitest workspace configuration to projects. + +- `index.VitestEnvironment` -> `vitest/node#VitestEnvironment`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.VitestRunMode` -> `vitest/node#VitestRunMode`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.VmEnvironmentReturn` -> `vitest/environments#VmEnvironmentReturn`: This was a deprecated root re-export. Import it from vitest/environments; Vitest 4 custom environments use Vite environments. + +- `index.WorkerContext` -> `vitest/node#WorkerContext`: This was a deprecated Vitest 3 root re-export. Import the type directly from vitest/node and review its Vitest 4 shape. + +- `index.WorkerRPC` -> `none`: The concrete worker RPC composition is internal. Use public Vitest RuntimeRPC, RunnerRPC, ContextRPC, or WorkerRequest types only when their narrower contract fits. + +- `index.chai.Should` -> `vitest#chai.Should`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. + +- `index.expect` -> `vitest#expect`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. + +- `index.scoped` -> `@effect/vitest#effect`: V4 effect tests are scoped and provide the test environment. Replace scoped(...) with effect(...), and it.scoped(...) with it.effect(...). + +- `index.scopedLive` -> `@effect/vitest#live`: V4 live tests are scoped automatically. Replace scopedLive(...) with live(...), and it.scopedLive(...) with it.live(...). + +- `index.should` -> `vitest#should`: This was a Vitest re-export, not Effect API. Import it directly from vitest; @effect/vitest/index is not a valid v4 route. + +### `@effect/vitest/utils` + +- `utils.assertFailure` -> `assertExitFailure`: For v3 Exit values, rename to assertExitFailure. In v4, assertFailure instead asserts Result.Failure. + +- `utils.assertLeft` -> `assertFailure`: Either became Result in v4: migrate Left to Result.Failure, then use assertFailure; narrowed payload access changes from .left to .failure. + +- `utils.assertMatch` -> `assertMatch`: Unchanged positional helper and behavior; only the parameter spelling changed, so call sites need no change. + +- `utils.assertRight` -> `assertSuccess`: Either became Result in v4: migrate Right to Result.Success, then use assertSuccess; narrowed payload access changes from .right to .success. + +- `utils.assertSuccess` -> `assertExitSuccess`: For v3 Exit values, rename to assertExitSuccess. In v4, assertSuccess instead asserts Result.Success. + +### `@effect/workflow/Activity` + +- `Activity.Any` -> `effect/unstable/workflow/Activity#Any`: Moved into core Effect. V4 Any is minimal; use AnyWithProps when schemas or execution properties are required. + +- `Activity.CurrentAttempt` -> `effect/unstable/workflow/Activity#CurrentAttempt`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same default of 1. + +- `Activity.TypeId` -> `none`: The activity marker is private in v4. Use Activity, Activity.Any, or Activity.AnyWithProps constraints. + +- `Activity.make` -> `effect/unstable/workflow/Activity#make`: Moved into core Effect with the same constructor shape, v4 Schema.Constraint service directions, and optional annotations. + +- `Activity.raceAll` -> `effect/unstable/workflow/Activity#raceAll`: Moved into core Effect with the same named durable race behavior. + +### `@effect/workflow/DurableClock` + +- `DurableClock.TypeId` -> `none`: The durable-clock marker is private in v4. Use DurableClock values structurally. + +- `DurableClock.make` -> `effect/unstable/workflow/DurableClock#make`: Moved into core Effect; Duration.DurationInput is now Duration.Input. + +### `@effect/workflow/DurableDeferred` + +- `DurableDeferred.Any` -> `effect/unstable/workflow/DurableDeferred#Any`: Moved into core Effect. V4 Any is minimal; use AnyWithProps when success, error, or exit schemas are required. + +- `DurableDeferred.TypeId` -> `none`: The durable-deferred marker is private in v4. Use DurableDeferred, Any, or AnyWithProps constraints. + +- `DurableDeferred.await` -> `effect/unstable/workflow/DurableDeferred#await`: Moved into core Effect with the same persisted-result and workflow-suspension behavior. + +- `DurableDeferred.done` -> `effect/unstable/workflow/DurableDeferred#done`: Moved into core Effect; schema requirements now use explicit directional encoding services. + +- `DurableDeferred.fail` -> `effect/unstable/workflow/DurableDeferred#fail`: Moved into core Effect and now requires the error schema encoding services. + +- `DurableDeferred.failCause` -> `effect/unstable/workflow/DurableDeferred#failCause`: Moved into core Effect and now requires the error schema encoding services. + +- `DurableDeferred.into` -> `effect/unstable/workflow/DurableDeferred#into`: Moved into core Effect with the same exit recording and suspension propagation behavior. + +- `DurableDeferred.make` -> `effect/unstable/workflow/DurableDeferred#make`: Moved into core Effect with the same name and optional schemas, expressed through v4 Schema.Constraint. + +- `DurableDeferred.raceAll` -> `effect/unstable/workflow/DurableDeferred#raceAll`: Moved into core Effect with the same persisted-winner behavior. + +- `DurableDeferred.succeed` -> `effect/unstable/workflow/DurableDeferred#succeed`: Moved into core Effect and now requires the success schema encoding services. + +### `@effect/workflow/DurableQueue` + +- `DurableQueue.TypeId` -> `effect/unstable/workflow/DurableQueue#TypeId`: Moved into core Effect; the marker literal changed to \~effect/workflow/DurableQueue. Use typeof DurableQueue.TypeId in type position. + +- `DurableQueue.make` -> `effect/unstable/workflow/DurableQueue#make`: Moved into core Effect; queue persistence now comes from effect/unstable/persistence. + +### `@effect/workflow/Workflow` + +- `Workflow.Any` -> `effect/unstable/workflow/Workflow#Any`: Moved into core Effect. Workflow identity changed from name to \_tag and definitions are now class-compatible constructors. + +- `Workflow.AnyTaggedRequestSchema` -> `none`: The TaggedRequest adapter constraint was removed. Define the workflow explicitly with Workflow.make and the request payload, success, error, and PrimaryKey schemas. + +- `Workflow.CaptureDefects` -> `effect/unstable/workflow/Workflow#CaptureDefects`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same true default. + +- `Workflow.Execution` -> `effect/unstable/workflow/Workflow#Execution`: Moved into core Effect; its workflow discriminator changed from name to \_tag. + +- `Workflow.Requirements` -> `Workflow.RequirementsClient / Workflow.RequirementsHandler`: The schema Context union split by direction: client payload encoding and result decoding versus handler payload decoding and result encoding. + +- `Workflow.Result` -> `effect/unstable/workflow/Workflow#Result`: Moved into core Effect and remains the Complete or Suspended result type and schema constructor. + +- `Workflow.ResultEncoded` -> `effect/unstable/workflow/Workflow#ResultEncoded`: Moved into core Effect and remains both the encoded result type and generic encoded-result codec. + +- `Workflow.ResultTypeId` -> `none`: The result marker is private in v4. Use Workflow.isResult for narrowing. + +- `Workflow.SuspendOnFailure` -> `effect/unstable/workflow/Workflow#SuspendOnFailure`: Moved into core Effect and changed from a Context.Tag subclass to a Context.Reference value with the same false default. + +- `Workflow.TypeId` -> `none`: The workflow marker is private in v4. Use Workflow.Any or Workflow.Workflow constraints. + +- `Workflow.Workflow` -> `effect/unstable/workflow/Workflow#Workflow`: Name and name became Tag and \_tag, schemas use directional services, definitions are constructable, and poll returns Option. + +- `Workflow.Workflow.Error` -> `W["errorSchema"]["Type"]`: The namespace alias was removed. Extract the decoded error type from the public errorSchema property. + +- `Workflow.Workflow.Payload` -> `Schema.Schema.Type>`: The namespace alias was removed. Extract the decoded payload from the exported PayloadSchema helper. + +- `Workflow.Workflow.Success` -> `W["successSchema"]["Type"]`: The namespace alias was removed. Extract the decoded success type from the public successSchema property. + +- `Workflow.fromTaggedRequest` -> `none`: Removed. Expand to Workflow.make(schema.\_tag, { payload: schema, success: schema.success, error: schema.failure, idempotencyKey: PrimaryKey.value }). + +- `Workflow.make` -> `effect/unstable/workflow/Workflow#make`: The signature changed from make({ name, ... }) to make(tag, { ... }); definitions expose \_tag and are class-compatible constructors. + +### `@effect/workflow/WorkflowEngine` + +- `WorkflowEngine.makeUnsafe` -> `effect/unstable/workflow/WorkflowEngine#makeUnsafe`: Moved into core Effect. Context service projections now use Service instead of Type, and absent encoded results use Option. + +### `@effect/workflow/WorkflowProxy` + +- `WorkflowProxy.ConvertHttpApi` -> `effect/unstable/workflow/WorkflowProxy#ConvertHttpApi`: Moved into core Effect and updated to v4 HttpApiEndpoint types and the consolidated HttpApi architecture. + +### `@effect/workflow/WorkflowProxyServer` + +- `WorkflowProxyServer.layerHttpApi` -> `effect/unstable/workflow/WorkflowProxyServer#layerHttpApi`: Moved into core Effect. Use v4 HttpApi group identifiers and Workflow.RequirementsHandler schema services. + +- `WorkflowProxyServer.layerRpcHandlers` -> `effect/unstable/workflow/WorkflowProxyServer#layerRpcHandlers`: Moved into core Effect; generated handlers require Workflow.RequirementsHandler rather than the undirected Requirements union. + +### `effect/Arbitrary` + +- `Arbitrary.ArbitraryAnnotation` -> `Schema.Annotations.ToArbitrary.Declaration`: Arbitrary derivation annotations now live in Schema.Annotations and use the toArbitrary key. + +- `Arbitrary.ArbitraryGenerationContext` -> `Schema.Annotations.ToArbitrary.Context`: Use the v4 arbitrary-derivation context type from Schema.Annotations. + +- `Arbitrary.LazyArbitrary` -> `Schema.Arbitrary`: The arbitrary factory type moved onto Schema. + +#### `Arbitrary.make` + +**Replacement:** `Schema.toArbitrary` + +Arbitrary derivation is now exposed directly by Schema. + +**Example** + +```ts +Schema.toArbitrary(schema)(FastCheck) +``` + +#### `Arbitrary.makeLazy` + +**Replacement:** `Schema.toArbitrary` + +Lazy arbitrary derivation is now exposed directly by Schema. + +**Example** + +```ts +Schema.toArbitrary(schema) +``` + +### `effect/Array` + +- `Array.ReadonlyArray` -> `Array.ReadonlyArray`: The namespace and its Infer, With, OrNonEmpty, AndNonEmpty, and Flatten utility types remain. + +- `Array.filterMapWhile` -> `Array.takeWhileFilter`: Same map-until-first-miss behavior; change the callback from Option.some/none to Result.succeed/fail. + +- `Array.flatMapNullable` -> `Array.flatMapNullishOr`: Direct nullish-terminology rename; null and undefined mapper results are still discarded. + +- `Array.fromNullable` -> `Array.fromNullishOr`: Direct nullish-terminology rename; null and undefined become an empty array and other values become a singleton. + +- `Array.getEquivalence` -> `Array.makeEquivalence`: Direct rename; pass the element Equivalence unchanged. + +- `Array.getLefts` -> `Array.getFailures`: Either became Result; this extracts failure payloads in input order. + +- `Array.getOrder` -> `Array.makeOrder`: Direct rename; pass the element Order unchanged. + +- `Array.getRights` -> `Array.getSuccesses`: Either became Result; this extracts success payloads in input order. + +- `Array.init` -> `Array.init`: The API and Option\\> behavior remain unchanged. + +- `Array.isEmptyArray` -> `Array.isArrayEmpty`: Direct word-order rename; retains the mutable empty-array type guard. + +- `Array.isEmptyReadonlyArray` -> `Array.isReadonlyArrayEmpty`: Direct word-order rename; retains the readonly empty-array type guard. + +- `Array.isNonEmptyArray` -> `Array.isArrayNonEmpty`: Direct word-order rename; retains the mutable NonEmptyArray type guard. + +- `Array.isNonEmptyReadonlyArray` -> `Array.isReadonlyArrayNonEmpty`: Direct word-order rename; retains the NonEmptyReadonlyArray type guard. + +- `Array.liftEither` -> `Array.liftResult`: Either became Result; failures produce an empty array and successes produce a singleton. + +- `Array.liftNullable` -> `Array.liftNullishOr`: Direct nullish-terminology rename; the lifted function still returns zero or one element. + +- `Array.modifyNonEmptyHead` -> `Array.modifyHeadNonEmpty`: Direct word-order rename with the same non-empty-preserving result. + +- `Array.modifyNonEmptyLast` -> `Array.modifyLastNonEmpty`: Direct word-order rename with the same non-empty-preserving result. + +- `Array.modifyOption` -> `Array.modify`: The Option suffix was dropped; an out-of-bounds index still returns Option.none. + +- `Array.partitionMap` -> `Array.partition`: Pass a Result-returning mapper instead of Either; the output remains [failures, successes], corresponding to v3 [lefts, rights]. + +- `Array.removeOption` -> `Array.remove`: The closest API now returns an unchanged copy out of bounds; use Array.get before Array.remove to preserve the old Option result. + +- `Array.replaceOption` -> `Array.replace`: The Option suffix was dropped; an out-of-bounds index still returns Option.none. + +- `Array.setNonEmptyHead` -> `Array.setHeadNonEmpty`: Direct word-order rename with the same non-empty-preserving result. + +- `Array.setNonEmptyLast` -> `Array.setLastNonEmpty`: Direct word-order rename with the same non-empty-preserving result. + +- `Array.splitNonEmptyAt` -> `Array.splitAtNonEmpty`: Direct word-order rename; the left output remains guaranteed non-empty. + +- `Array.tail` -> `Array.tail`: The API and Option\\> behavior remain unchanged. + +- `Array.unsafeGet` -> `Array.getUnsafe`: Direct word-order rename; it still throws for an out-of-bounds index. + +### `effect/BigDecimal` + +- `BigDecimal.BigDecimal` -> `BigDecimal.BigDecimal`: The model interface remains, but its brand key is now internal. + +- `BigDecimal.TypeId` -> `none`: The brand key is internal in v4; use BigDecimal.isBigDecimal for runtime narrowing. + +- `BigDecimal.greaterThan` -> `BigDecimal.isGreaterThan`: Renamed with the v4 is-prefix. + +- `BigDecimal.greaterThanOrEqualTo` -> `BigDecimal.isGreaterThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `BigDecimal.lessThan` -> `BigDecimal.isLessThan`: Renamed with the v4 is-prefix. + +- `BigDecimal.lessThanOrEqualTo` -> `BigDecimal.isLessThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `BigDecimal.safeFromNumber` -> `BigDecimal.fromNumber`: Use the safe v4 constructor, which still returns Option. + +- `BigDecimal.unsafeDivide` -> `BigDecimal.divideUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +- `BigDecimal.unsafeFromNumber` -> `BigDecimal.fromNumberUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +- `BigDecimal.unsafeFromString` -> `BigDecimal.fromStringUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +- `BigDecimal.unsafeRemainder` -> `BigDecimal.remainderUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +- `BigDecimal.unsafeToNumber` -> `BigDecimal.toNumberUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +### `effect/BigInt` + +- `BigInt.fromNumber` -> `BigInt.fromNumber`: Unchanged; it returns Option for safe conversion. + +- `BigInt.greaterThan` -> `BigInt.isGreaterThan`: Renamed with the v4 is-prefix. + +- `BigInt.greaterThanOrEqualTo` -> `BigInt.isGreaterThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `BigInt.lessThan` -> `BigInt.isLessThan`: Renamed with the v4 is-prefix. + +- `BigInt.lessThanOrEqualTo` -> `BigInt.isLessThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `BigInt.unsafeDivide` -> `BigInt.divideUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +- `BigInt.unsafeSqrt` -> `BigInt.sqrtUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +### `effect/Brand` + +- `Brand.Brand` -> `Brand.Brand`: Still exported, but v4 brand keys are strings rather than symbols. + +- `Brand.Brand.BrandErrors` -> `Brand.BrandError`: Validation now returns one BrandError wrapping a SchemaIssue.Issue instead of an error array. + +- `Brand.Brand.RefinementError` -> `Schema.FilterIssue`: Brand.make validators use Schema filter output instead of the old message and meta record. + +- `Brand.BrandTypeId` -> `none`: The public marker was removed; the v4 brand type id is private. + +- `Brand.Branded` -> `Brand.Branded`: Still exported, with the brand key restricted to string. + +- `Brand.RefinedConstructorsTypeId` -> `none`: The public refined-constructor marker was removed. + +- `Brand.all` -> `Brand.all`: Still exported; combines multiple brand constructors and checks. + +- `Brand.error` -> `Brand.make`: Return a string or Schema filter issue directly from a Brand.make validator. + +- `Brand.nominal` -> `Brand.nominal`: Still exported; the constructor's either method is now result. + +- `Brand.refined` -> `Brand.make`: Use Brand.make for custom validation or Brand.check for Schema checks. + +- `Brand.unbranded` -> `Function.cast`: Brands are runtime-identical to their base value; cast explicitly when an unbranded type is required. + +### `effect/Cache` + +- `Cache.Cache` -> `Cache.Cache`: The cache model remains, but v4 exposes a Pipeable value with explicit Cache operations and adds a lookup environment parameter. + +- `Cache.Cache.ConsumerVariance` -> `none`: The ConsumerCache view and its variance marker were removed; expose a narrower application interface around Cache operations when write access must be hidden. + +- `Cache.Cache.Variance` -> `none`: The public variance marker was removed; use Cache.Cache directly and do not depend on its branding internals. + +- `Cache.CacheStats` -> `none`: Built-in hit and miss statistics were removed; instrument the lookup and Cache.get calls explicitly, and use Cache.size for the current entry count. + +- `Cache.CacheTypeId` -> `none`: The cache type id is internal in v4; do not inspect or construct the cache brand directly. + +- `Cache.ConsumerCache` -> `Cache.Cache`: ConsumerCache was removed; use Cache.Cache and expose an application-defined read-only wrapper if capability restriction is required. + +- `Cache.ConsumerCacheTypeId` -> `none`: ConsumerCache and its type id were removed with the read-only cache view. + +- `Cache.EntryStats` -> `none`: Per-entry loaded-time statistics were removed; record lookup timing in application instrumentation if needed. + +- `Cache.Lookup` -> `(key: Key) => Effect.Effect`: The named alias was removed; use an inline lookup function type or Cache.Cache\["lookup"]. + +- `Cache.makeCacheStats` -> `none`: CacheStats and its constructor were removed; define an application metrics record if these counters are still required. + +- `Cache.makeEntryStats` -> `none`: EntryStats and its constructor were removed; capture lookup timing in application instrumentation instead. + +### `effect/Cause` + +- `Cause.Cause` -> `Cause.Cause`: The name remains, but v4 Cause\ is a wrapper with readonly reasons: ReadonlyArray\\>, not the v3 Empty/Fail/Die/Interrupt/Sequential/Parallel tree. + +- `Cause.Cause.Variance` -> `none`: The public variance helper was removed. Cause.Cause is directly branded by Cause.TypeId; application code should not reproduce the old variance member. + +- `Cause.CauseReducer` -> `cause.reasons.reduce`: The six-case tree reducer type was removed with Empty, Sequential, and Parallel. Reduce the flat Reason array and switch on Fail, Die, or Interrupt instead. + +- `Cause.CauseTypeId` -> `Cause.TypeId`: The brand export is Cause.TypeId, a literal-string const. Use typeof Cause.TypeId in type positions; the v3 unique-symbol CauseTypeId alias is gone. + +- `Cause.Die` -> `Cause.Die`: The name remains, but Cause.Die is now a Reason stored in cause.reasons, not a Cause variant. Construct a standalone reason with Cause.makeDieReason or a cause with Cause.die. + +- `Cause.Empty` -> `Cause.empty`: The Empty subtype and \_tag were removed. Empty is Cause.empty, represented by cause.reasons.length === 0. + +- `Cause.ExceededCapacityException` -> `Cause.ExceededCapacityError`: Rename the class/type and update the discriminant from ExceededCapacityException to ExceededCapacityError. + +- `Cause.ExceededCapacityExceptionTypeId` -> `Cause.ExceededCapacityErrorTypeId`: Rename the brand; v4 exports a literal-string const, so use typeof Cause.ExceededCapacityErrorTypeId in type positions. + +- `Cause.Fail` -> `Cause.Fail`: The name remains, but Cause.Fail\ is now a Reason stored in cause.reasons, not a Cause variant. Construct a standalone reason with Cause.makeFailReason or a cause with Cause.fail. + +- `Cause.IllegalArgumentException` -> `Cause.IllegalArgumentError`: Rename the class/type and update the discriminant from IllegalArgumentException to IllegalArgumentError. + +- `Cause.IllegalArgumentExceptionTypeId` -> `Cause.IllegalArgumentErrorTypeId`: Rename the brand; v4 exports a literal-string const, so use typeof Cause.IllegalArgumentErrorTypeId in type positions. + +- `Cause.Interrupt` -> `Cause.Interrupt`: The name remains, but it is now a Reason in cause.reasons rather than a Cause variant, and fiberId changed from FiberId.FiberId to number | undefined. Use Cause.makeInterruptReason or Cause.interrupt. + +- `Cause.InterruptedException` -> `none`: The public exception class was removed. Represent cancellation with Cause.interrupt; Cause.prettyErrors creates an ordinary Error named InterruptError for interrupt-only rendering, but no class is exported. + +- `Cause.InterruptedExceptionTypeId` -> `none`: Removed with InterruptedException; v4 exports no interruption-error brand. Inspect the Cause with Cause.hasInterrupts or Cause.hasInterruptsOnly instead. + +- `Cause.InvalidPubSubCapacityException` -> `Error`: The dedicated public type was removed. Current v4 PubSub capacity validation throws a standard global Error with the capacity message. + +- `Cause.InvalidPubSubCapacityExceptionTypeId` -> `none`: Removed with InvalidPubSubCapacityException; the standard Error now thrown by PubSub has no Effect-specific brand. + +- `Cause.NoSuchElementException` -> `Cause.NoSuchElementError`: Rename the class/type and update the discriminant from NoSuchElementException to NoSuchElementError. + +- `Cause.NoSuchElementExceptionTypeId` -> `Cause.NoSuchElementErrorTypeId`: Rename the brand; v4 exports a literal-string const, so use typeof Cause.NoSuchElementErrorTypeId in type positions. + +- `Cause.Parallel` -> `none`: Parallel cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind. + +- `Cause.PrettyError` -> `Error`: Cause.prettyErrors now returns Array\. The dedicated span field is gone; tracing information is incorporated from Reason annotations into rendered stacks. + +- `Cause.RuntimeException` -> `Error`: The dedicated class was removed and v4 uses global Error for generic defects. Use Data.Error or Data.TaggedError instead when a yieldable typed error is required. + +- `Cause.RuntimeExceptionTypeId` -> `none`: Removed with RuntimeException. Define and guard a custom Data.Error/Data.TaggedError type if nominal branding is required. + +- `Cause.Sequential` -> `none`: Sequential cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind. + +- `Cause.TimeoutException` -> `Cause.TimeoutError`: Rename the class/type and update the discriminant from TimeoutException to TimeoutError. + +- `Cause.TimeoutExceptionTypeId` -> `Cause.TimeoutErrorTypeId`: Rename the brand; v4 exports a literal-string const, so use typeof Cause.TimeoutErrorTypeId in type positions. + +- `Cause.UnknownException` -> `Cause.UnknownError`: Rename the class/type and discriminant. The original unknown value is now exposed through the standard Error.cause property, not v3's .error field. + +- `Cause.UnknownExceptionTypeId` -> `Cause.UnknownErrorTypeId`: Rename the brand; v4 exports a literal-string const, so use typeof Cause.UnknownErrorTypeId in type positions. + +- `Cause.YieldableError` -> `Cause.YieldableError / Data.Error`: Cause.YieldableError remains as the interface/type, but its public constructor value was removed. Extend Data.Error for an untagged yieldable error or Data.TaggedError for a tagged one. + +- `Cause.andThen` -> `Cause.fromReasons(self.reasons.flatMap(...))`: No direct v4 combinator. For each Fail reason, splice either f(reason.error).reasons or the constant cause's reasons; retain Die and Interrupt reasons, then rebuild with Cause.fromReasons. + +- `Cause.as` -> `Cause.map(self, () => error)`: Use Cause.map with a constant function; only Fail errors change and Die/Interrupt reasons remain. + +- `Cause.contains` -> `Equal.equals(Cause.combine(self, that), self)`: There are no subtrees in v4. This tests whether all reasons from that are already present in self under v4 reason equality; use Equal.equals(self, that) when only whole-cause equality is intended. + +- `Cause.defects` -> `self.reasons.filter(Cause.isDieReason).map((reason) => reason.defect)`: Collect defect values from the flat Reason array. The result is a standard array rather than v3 Chunk. + +- `Cause.dieOption` -> `Cause.findDefect`: Cause.findDefect returns Result.Result\\>, not Option. Match the Result or convert it to Option when the old return shape is required. + +- `Cause.failureOption` -> `Cause.findErrorOption`: Direct Option-based replacement for extracting the first typed Fail error value. + +- `Cause.failureOrCause` -> `Cause.findError`: Use the v4 Result-based split: success is the first E and failure is the original Cause\ when no Fail reason exists. + +- `Cause.failures` -> `self.reasons.filter(Cause.isFailReason).map((reason) => reason.error)`: Collect typed error values from the flat Reason array. The result is a standard array rather than v3 Chunk. + +- `Cause.filter` -> `Cause.fromReasons(self.reasons.filter(...))`: No exact tree-level equivalent: v3 predicates selected recursive child causes. Rewrite the predicate for Cause.Reason values, filter cause.reasons, and rebuild with Cause.fromReasons. + +- `Cause.find` -> `Option.firstSomeOf(self.reasons.map(...))`: No recursive nodes remain. Apply the partial function to Reason values and take the first Some, or use Cause.findFail/findError/findDie/findDefect/findInterrupt for standard searches. + +- `Cause.flatMap` -> `Cause.fromReasons(self.reasons.flatMap((reason) => Cause.isFailReason(reason) ? f(reason.error).reasons : [reason]))`: No direct v4 export. Flat-map only Fail reasons into replacement causes, preserve Die/Interrupt reasons, and rebuild from the resulting Reason array. + +- `Cause.flatten` -> `Cause.fromReasons(self.reasons.flatMap((reason) => Cause.isFailReason(reason) ? reason.error.reasons : [reason]))`: No direct v4 export. For Cause\\>, splice each Fail reason's nested cause.reasons and retain Die/Interrupt reasons. + +- `Cause.flipCauseOption` -> `Cause.fromReasons + Option`: Rewrite over reasons: drop Fail(None), replace Fail(Some(e)) with Cause.makeFailReason(e), retain Die/Interrupt, then return None only when a non-empty input loses every reason; preserve Some(Cause.empty) for an empty input. + +- `Cause.interruptOption` -> `Cause.findInterrupt`: The replacement returns Result.Result\\> rather than Option\; on success read reason.fiberId, now number | undefined. + +- `Cause.isDie` -> `Cause.hasDies`: Use the v4 cause-level predicate for the presence of at least one Die reason. + +- `Cause.isDieType` -> `Cause.isDieReason`: Apply this guard to an entry of cause.reasons; Cause itself is no longer a Die union variant. + +- `Cause.isEmpty` -> `self.reasons.length === 0`: V4 represents an empty cause with an empty reasons array and exports no isEmpty function. + +- `Cause.isEmptyType` -> `self.reasons.length === 0`: The check remains possible, but there is no Empty subtype to narrow to because v4 Cause is not a variant union. + +- `Cause.isExceededCapacityException` -> `Cause.isExceededCapacityError`: Rename the guard along with ExceededCapacityError. + +- `Cause.isFailType` -> `Cause.isFailReason`: Apply this guard to an entry of cause.reasons; Cause itself is no longer a Fail union variant. + +- `Cause.isFailure` -> `Cause.hasFails`: Use the v4 cause-level predicate for the presence of at least one Fail reason. + +- `Cause.isIllegalArgumentException` -> `Cause.isIllegalArgumentError`: Rename the guard along with IllegalArgumentError. + +- `Cause.isInterruptType` -> `Cause.isInterruptReason`: Apply this guard to an entry of cause.reasons; Cause itself is no longer an Interrupt union variant. + +- `Cause.isInterrupted` -> `Cause.hasInterrupts`: Use the v4 cause-level predicate for the presence of at least one Interrupt reason. + +- `Cause.isInterruptedException` -> `none`: No v4 InterruptError class or unknown-value guard is exported. When the Cause is available, test Cause.hasInterruptsOnly before squashing or rendering it. + +- `Cause.isInterruptedOnly` -> `Cause.hasInterruptsOnly`: Direct cause-level rename; it is false for Cause.empty and true only when at least one reason exists and every reason is Interrupt. + +- `Cause.isNoSuchElementException` -> `Cause.isNoSuchElementError`: Rename the guard along with NoSuchElementError. + +- `Cause.isParallelType` -> `none`: Parallel cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind. + +- `Cause.isRuntimeException` -> `none`: RuntimeException and its brand were removed. Use instanceof Error for generic errors or define a Data.Error/Data.TaggedError class with its own guard when nominal recognition is required. + +- `Cause.isSequentialType` -> `none`: Sequential cause nodes were removed; v4 stores all reasons in one flat array and does not retain composition kind. + +- `Cause.isTimeoutException` -> `Cause.isTimeoutError`: Rename the guard along with TimeoutError. + +- `Cause.isUnknownException` -> `Cause.isUnknownError`: Rename the guard along with UnknownError. + +- `Cause.keepDefects` -> `Cause.fromReasons(self.reasons.filter(Cause.isDieReason))`: Keep every Die reason, not merely the first defect. Return Option.none when the filtered array is empty and Option.some of the rebuilt cause otherwise; Cause.findDefect alone is not behaviorally equivalent. + +- `Cause.linearize` -> `self.reasons`: No direct replacement: v4 discarded sequential/parallel structure, so there are no parallel branches to linearize. Rewrite the consumer to process the flat Reason array. + +- `Cause.originalError` -> `Function.identity`: V3 used this to unwrap span-capture proxies. V4 stores tracing data on Reason.annotations and no longer proxies errors, so the input is already the original value. + +- `Cause.parallel` -> `Cause.combine`: Combine the two flat reason arrays; v4 intentionally no longer records whether composition was parallel or sequential. + +- `Cause.reduce` -> `self.reasons.reduce`: Reduce the flat Reason array directly. The callback now sees only Fail, Die, and Interrupt reasons, never Empty or composition nodes. + +- `Cause.reduceWithContext` -> `self.reasons.reduce`: Capture the context in the reducer closure and reduce the flat Reason array; sequentialCase and parallelCase have no v4 analogue. + +- `Cause.sequential` -> `Cause.combine`: Combine the two flat reason arrays; v4 intentionally no longer records whether composition was parallel or sequential. + +- `Cause.size` -> `self.reasons.length`: The v3 node count becomes the number of flat reasons in v4. + +- `Cause.squashWith` -> `Result.match(Cause.findError(self), { onSuccess: f, onFailure: Cause.squash })`: Apply f only to the first typed Fail error; if no Fail exists, squash the returned Cause\. This preserves v3's priority and avoids evaluating f for later Fail reasons. + +- `Cause.stripFailures` -> `Cause.fromReasons(self.reasons.filter((reason) => !Cause.isFailReason(reason)))`: Remove Fail reasons and retain Die plus Interrupt reasons, then rebuild the cause. The v3 prose saying interrupts were removed did not match its implementation. + +- `Cause.stripSomeDefects` -> `Cause.fromReasons + Option`: Filter out each Die reason for which pf(reason.defect) is Some, retain all other reasons, and rebuild. Return None only when a non-empty input loses every reason; preserve Some(Cause.empty) for empty input. + +### `effect/Channel` + +- `Channel.Channel` -> `Channel.Channel`: Retained, but reorder type parameters from \ to \. Convert Effect values explicitly with Channel.fromEffect or Channel.fromEffectDone. + +- `Channel.ChannelException` -> `none`: Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper. + +- `Channel.ChannelExceptionTypeId` -> `none`: Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper. + +- `Channel.ChannelTypeId` -> `Channel.TypeId`: Renamed to TypeId; the brand is now the string literal \~effect/Channel. Prefer Channel.isChannel for runtime checks. + +- `Channel.ChannelUnify` -> `Channel.ChannelUnify`: Retained; update inferred Channel arguments to the v4 generic order. + +- `Channel.ChannelUnifyIgnore` -> `Channel.ChannelUnifyIgnore`: Retained with a new shape: it no longer extends EffectUnifyIgnore and now contains Effect?: true. + +- `Channel.acquireReleaseOut` -> `Channel.acquireRelease`: Renamed to acquireRelease. The v4 release action cannot add environment requirements, so capture or provide any services it needs. + +- `Channel.as` -> `Channel.mapDone`: Replace Channel.as(self, value) with Channel.mapDone(self, () =\> value). + +- `Channel.asVoid` -> `Channel.mapDone`: Replace Channel.asVoid(self) with Channel.mapDone(self, () =\> void 0). + +- `Channel.bufferChunk` -> `none`: The inferred Channel.fromChunk match is not equivalent. Rebuild the buffered upstream-pull transform with Channel.fromTransform and Channel.toTransform. + +- `Channel.catchAll` -> `Channel.catch`: Renamed to catch for typed-error recovery. + +- `Channel.catchAllCause` -> `Channel.catchCause`: Renamed to catchCause for full-cause recovery. + +- `Channel.collect` -> `Channel.filterMap`: Use Channel.filterMap(self, Filter.fromPredicateOption(pf)) to adapt the v3 Option-returning partial function. + +- `Channel.concatAll` -> `Channel.flatten`: Use flatten for sequential emitted-channel flattening. V4 preserves the outer done value and discards child done values. + +- `Channel.concatAllWith` -> `none`: V4 removed child-done accumulation and the outer-done combiner. Use Channel.flatten or Channel.flatMap only when child done values may be discarded; otherwise implement a Pull transform. + +- `Channel.concatMap` -> `Channel.flatMap`: Renamed to flatMap. Sequential flattening is the default; child done values are discarded and the source done value is preserved. + +- `Channel.concatMapWith` -> `none`: V4 removed child-done accumulation and the outer-done combiner. Use Channel.flatten or Channel.flatMap only when child done values may be discarded; otherwise implement a Pull transform. + +- `Channel.concatMapWithCustom` -> `none`: Removed with the channel executor scheduling protocol. Use Channel.flatMap for ordinary sequencing or implement custom scheduling with Channel.fromTransform and Pull. + +- `Channel.concatOut` -> `Channel.flatten`: Use flatten for sequential emitted-channel flattening. V4 preserves the outer done value and discards child done values. + +- `Channel.context` -> `Channel.contextWith`: Use Channel.contextWith((context) =\> Channel.end(context)); the context was the v3 channel done value. + +- `Channel.contextWithChannel` -> `Channel.contextWith`: Renamed to contextWith. + +- `Channel.contextWithEffect` -> `Channel.contextWith`: Use Channel.contextWith((context) =\> Channel.fromEffectDone(f(context))) to preserve the effect result as the done value. + +- `Channel.doneCollect` -> `none`: No exact channel combinator remains. Drive Channel.toPull, collect output elements, and handle Cause.Done to retain both outputs and the done value. + +- `Channel.emitCollect` -> `none`: No exact channel combinator remains. Drive Channel.toPull, collect output elements, and handle Cause.Done to retain both outputs and the done value. + +- `Channel.ensuringWith` -> `Channel.onExit`: Renamed to onExit; the finalizer still receives the channel Exit. + +- `Channel.foldCauseChannel` -> `none`: V4 has no exact two-sided fold over failure and completion. Use catchCause or catch for failure-only handling, concatWith for success-only handling, or match the Pull in a custom transform. + +- `Channel.foldChannel` -> `none`: V4 has no exact two-sided fold over failure and completion. Use catchCause or catch for failure-only handling, concatWith for success-only handling, or match the Pull in a custom transform. + +- `Channel.fromEither` -> `Channel.fromEffectDone`: Either is now Result. Use Channel.fromEffectDone(Effect.fromResult(result)) to preserve success as the done value. + +- `Channel.fromInput` -> `none`: SingleProducerAsyncInput was removed. Model the producer with Queue and Pull; use Channel.fromPull with Queue.take when a typed done value matters. + +- `Channel.fromOption` -> `Channel.fromEffectDone`: Use Channel.fromEffectDone(Effect.fromOption(option, Option.none)) to preserve the v3 Option.none error, or omit onNone for the v4 NoSuchElementError default. + +- `Channel.fromPubSubScoped` -> `Channel.fromPubSubTake`: Change the protocol to PubSub\\> and use Channel.flattenArray(Channel.fromPubSubTake(pubsub)). The v4 constructor owns the scoped subscription and returns a Channel directly. + +- `Channel.interruptWhenDeferred` -> `Channel.interruptWhen`: Use Channel.interruptWhen(self, Deferred.await(deferred)); the Deferred-specific overload was removed. + +- `Channel.isChannelException` -> `none`: Removed implementation artifact. Channel.pipeToOrFail now handles upstream failures without exposing the v3 exception wrapper. + +- `Channel.mapErrorCause` -> `Channel.catchCause`: Use Channel.catchCause(self, (cause) =\> Channel.failCause(f(cause))). + +- `Channel.mapInputContext` -> `Channel.updateContext`: Renamed to updateContext for transforming the channel requirement Context. + +- `Channel.mapInputEffect` -> `none`: V4 removed upstream done/error effect mapping. Adapt Cause.Done or failure on the upstream Pull, then pass it through Channel.toTransform(self). + +- `Channel.mapInputErrorEffect` -> `none`: V4 removed upstream done/error effect mapping. Adapt Cause.Done or failure on the upstream Pull, then pass it through Channel.toTransform(self). + +- `Channel.mapInputIn` -> `Channel.mapInput`: Use Channel.mapInput(self, (value) =\> Effect.succeed(f(value))); v4 consolidated pure and effectful input mapping. + +- `Channel.mapInputInEffect` -> `Channel.mapInput`: Renamed to mapInput; the mapper remains effectful. + +- `Channel.mapOut` -> `Channel.map`: Renamed to map; the v4 mapper also receives the element index. + +- `Channel.mapOutEffect` -> `Channel.mapEffect`: Renamed to mapEffect for sequential effectful output mapping. + +- `Channel.mapOutEffectPar` -> `Channel.mapEffect`: Use Channel.mapEffect(self, f, { concurrency: n }); ordered output remains the default. + +- `Channel.mergeAllUnbounded` -> `Channel.mergeAll`: Use Channel.mergeAll(channels, { concurrency: "unbounded" }); child done values are discarded and the outer done value is preserved. + +- `Channel.mergeAllUnboundedWith` -> `none`: V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge. + +- `Channel.mergeAllWith` -> `none`: V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge. + +- `Channel.mergeMap` -> `Channel.flatMap`: Use Channel.flatMap with concurrency and bufferSize for backpressure, or Channel.switchMap with the same options for the v3 sliding strategy. + +- `Channel.mergeOut` -> `Channel.mergeAll`: Use Channel.mergeAll(self, { concurrency: n }) for bounded backpressured flattening when child done values are irrelevant. + +- `Channel.mergeOutWith` -> `none`: V4 mergeAll removed child-done combining. Use Channel.mergeAll if terminal accumulation can be dropped; otherwise implement a custom Pull merge. + +- `Channel.mergeWith` -> `Channel.merge`: Use Channel.merge with haltStrategy left, right, both, or either for standard policies. Custom MergeDecision effects require a Pull-level redesign. + +- `Channel.orDieWith` -> `Channel.catch`: Use Channel.catch(self, (error) =\> Channel.die(f(error))); v4 Channel.orDie has no mapping callback. + +- `Channel.orElse` -> `Channel.catch`: Use Channel.catch(self, () =\> that()) and keep the fallback lazy. + +- `Channel.provideLayer` -> `Channel.provide`: Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed. + +- `Channel.provideSomeLayer` -> `Channel.provide`: Both collapse into provide. V4 removes services supplied by the layer and retains remaining requirements; use options.local when a fresh layer instance is needed. + +- `Channel.read` -> `none`: The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling. + +- `Channel.readOrFail` -> `none`: The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling. + +- `Channel.readWith` -> `none`: The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling. + +- `Channel.readWithCause` -> `none`: The channel instruction AST was removed. Implement a one-step upstream read with Channel.fromTransform and Pull matching, including Cause.Done handling. + +- `Channel.repeated` -> `Channel.forever`: Use forever for infinite repetition. Channel.repeat takes a Schedule and may terminate, so it is not equivalent. + +- `Channel.run` -> `Channel.runDone`: Renamed to runDone for an inputless, outputless channel. Use runDrain if emitted elements should be discarded. + +- `Channel.runScoped` -> `Channel.toPull`: No direct scoped runner remains. Use toPull in the caller scope and recover Cause.Done; use runDone or runDrain when an internally managed scope is acceptable. + +- `Channel.scopedWith` -> `Channel.unwrap`: Use Channel.unwrap(Effect.map(Effect.scope, (scope) =\> Channel.fromEffect(f(scope)))) so the effect uses the active channel scope. + +- `Channel.toPullIn` -> `Channel.toPullScoped`: Renamed to toPullScoped. The returned Pull emits elements directly and represents completion with Cause.Done instead of Either. + +- `Channel.toSink` -> `Sink.fromChannel`: Constructor moved to Sink. Adapt the channel to non-empty array input, no emitted leftovers, and a Sink.End done value. + +- `Channel.toStream` -> `Stream.fromChannel`: Constructor moved to Stream. Adapt Chunk outputs to non-empty readonly arrays and map the channel done value to void. + +- `Channel.unwrapScoped` -> `Channel.unwrap`: Use unwrap; v4 supplies the active channel scope to the effect and removes Scope from the resulting requirement. + +- `Channel.unwrapScopedWith` -> `Channel.unwrap`: Use Channel.unwrap(Effect.flatMap(Effect.scope, f)) to pass the active channel scope to f. + +- `Channel.void` -> `Channel.empty`: Renamed to empty: emit nothing and end with void. + +- `Channel.write` -> `Channel.succeed`: Renamed to succeed, which emits one element in v4. Use Channel.end when migrating v3 succeed, which produced a done value. + +- `Channel.writeAll` -> `Channel.fromArray`: Replace the variadic writer with Channel.fromArray(outs). + +- `Channel.writeChunk` -> `Channel.fromChunk`: Renamed to fromChunk for emitting every Chunk element. + +- `Channel.zip` -> `Channel.concatWith`: For sequential zip, concatWith the left channel and mapDone the right result to a tuple. Concurrent tuple-done semantics require custom Pull coordination. + +- `Channel.zipLeft` -> `Channel.concatWith`: For sequential zipLeft, concatWith and mapDone the right result back to the left done value. Concurrent done preservation requires custom Pull coordination. + +- `Channel.zipRight` -> `Channel.concat`: Use concat for the sequential form; it preserves the right done value. Concurrent mode has no exact replacement. + +### `effect/Chunk` + +- `Chunk.Chunk` -> `Chunk.Chunk`: The model remains Chunk.Chunk\; continue using Chunk constructors rather than depending on its exposed representation fields. + +- `Chunk.TypeId` -> `none`: The v4 Chunk brand key is private; no public Chunk.TypeId type or value is exported. + +- `Chunk.getEquivalence` -> `Chunk.makeEquivalence`: Direct rename; pass the element Equivalence unchanged. + +- `Chunk.modifyOption` -> `Chunk.modify`: The Option suffix was dropped; an out-of-bounds index still returns Option.none. + +- `Chunk.partitionMap` -> `Chunk.partition`: Pass a Result-returning mapper instead of Either; the output remains [failures, successes]. + +- `Chunk.removeOption` -> `Chunk.remove`: The closest API now returns the unchanged Chunk out of bounds; use Chunk.get before Chunk.remove to preserve the old Option result. + +- `Chunk.replaceOption` -> `Chunk.replace`: The Option suffix was dropped; an out-of-bounds index still returns Option.none. + +- `Chunk.unsafeFromArray` -> `Chunk.fromArrayUnsafe`: Direct word-order rename; it still wraps without copying and is unsafe if the source array is mutated. + +- `Chunk.unsafeFromNonEmptyArray` -> `Chunk.fromNonEmptyArrayUnsafe`: Direct word-order rename; it still wraps without copying and preserves NonEmptyChunk. + +- `Chunk.unsafeGet` -> `Chunk.getUnsafe`: Direct word-order rename; it still throws for an out-of-bounds index. + +- `Chunk.unsafeHead` -> `Chunk.headUnsafe`: Direct word-order rename; it still throws on an empty Chunk. + +- `Chunk.unsafeLast` -> `Chunk.lastUnsafe`: Direct word-order rename; it still throws on an empty Chunk. + +### `effect/Clock` + +- `Clock.CancelToken` -> `none`: The public clock scheduler and cancellation-token protocol were removed. Use Effect.sleep for delays and Effect interruption or Fiber.interrupt for cancellation. + +- `Clock.Clock` -> `Clock.Clock`: The service interface remains, but unsafeCurrentTimeMillis and unsafeCurrentTimeNanos were renamed to currentTimeMillisUnsafe and currentTimeNanosUnsafe, the public type-id field was removed, and custom implementations must add monotonicTimeNanosUnsafe plus monotonicTimeNanos for elapsed-time measurement. + +- `Clock.ClockScheduler` -> `none`: The low-level clock scheduler is no longer public. Express scheduling with Effect.sleep and cancel the running fiber through normal Effect interruption. + +- `Clock.ClockTypeId` -> `none`: The Clock type-id is private in v4. Use the Clock.Clock Context.Reference to access, provide, or identify the clock service. + +- `Clock.Task` -> `none`: The low-level clock task alias was removed with ClockScheduler. Model delayed work as an Effect and run or fork it after Effect.sleep. + +- `Clock.make` -> `Layer.succeed(Clock.Clock, clock)`: The Clock constructor was removed. Implement the v4 Clock interface as a plain service value and provide it through Clock.Clock. + +### `effect/Config` + +- `Config.Config` -> `Config.Config`: The model remains a yieldable Effect and exposes parse(provider). Compose logical lookup paths with Config.schema(..., path) and Config.nested; parsing no longer accepts a public path prefix. + +- `Config.Config.IsPlainObject` -> `none`: This private conditional helper is no longer exposed; use Config.Wrap for the public recursive wrapping contract. + +- `Config.Config.Primitive` -> `Schema.Constraint`: Primitive descriptions and parsers were replaced by Schema codecs consumed through Config.schema. + +- `Config.Config.Variance` -> `none`: Config now carries its result type directly through Effect and has no public variance interface. + +- `Config.ConfigTypeId` -> `Config.isConfig`: The Config marker is private in v4; use the public guard for runtime narrowing. + +- `Config.LiteralValue` -> `SchemaAST.LiteralValue`: Use the literal value type shared by v4 Schema constructors. + +- `Config.all` -> `Config.all`: Combine an iterable or record of Config values. A wholly absent product can use Config.withDefault or Config.option, while a partially supplied product fails. + +- `Config.array` -> `Config.schema(Config.Array(valueSchema), path)`: Array parsing is schema-based in v4; rebuild the element Config as a Schema and pass the optional path to Config.schema. + +- `Config.boolean` -> `Config.boolean`: Unchanged. + +- `Config.branded` -> `Config.schema(schema.pipe(Schema.brand(brand)), path)`: Brand validation moved to Schema; define the branded schema and construct the Config with Config.schema. + +- `Config.chunk` -> `Config.schema(Schema.Chunk(valueSchema), path)`: Collection parsing is schema-based in v4; use Schema.Chunk when a Chunk result is still required. + +- `Config.date` -> `Config.date`: Unchanged. + +- `Config.duration` -> `Config.duration`: Unchanged. + +- `Config.fail` -> `Config.fail`: The v4 constructor takes a ConfigProvider.SourceError or Schema.SchemaError instead of a message; wrap the failure in the appropriate cause. + +- `Config.hashMap` -> `Config.schema(Schema.HashMap(Schema.String, valueSchema), path)`: HashMap parsing is schema-based in v4; replace the child Config with its value Schema. + +- `Config.hashSet` -> `Config.schema(Schema.HashSet(valueSchema), path)`: HashSet parsing is schema-based in v4; replace the child Config with its value Schema. + +- `Config.integer` -> `Config.int`: Renamed to the shorter v4 integer constructor. + +- `Config.literal` -> `Config.literals(literals, path)`: The v3 curried variadic constructor became Config.literals with an array and inline path; use Config.literal for one value. + +- `Config.logLevel` -> `Config.logLevel`: Unchanged. + +- `Config.mapAttempt` -> `Config.mapOrFail`: Catch exceptions explicitly and return an Effect failure containing Config.ConfigError; mapOrFail is Effect-based in v4. + +- `Config.nonEmptyString` -> `Config.nonEmptyString`: Unchanged. + +- `Config.number` -> `Config.number`: Unchanged; use Config.finite when NaN and infinities must be rejected. + +- `Config.orElseIf` -> `Config.orElse`: The fallback now receives Config.ConfigError; test it in the callback and re-fail with Config.fail(error.cause) when the predicate is false. + +- `Config.port` -> `Config.port`: Unchanged. + +- `Config.primitive` -> `Config.schema(customSchema, path)`: Custom primitive parsing moved to Schema codecs; express decoding and diagnostics in a Schema, then pass it to Config.schema. Its canonical StringTree encoding must expose a concrete shape; opaque encodings such as Schema.Any or Schema.Unknown are not supported. + +- `Config.redacted` -> `Config.redacted`: The string/path overload remains; replace the v3 Config argument overload with Config.map(config, Redacted.make). + +- `Config.repeat` -> `Config.schema(Config.Array(valueSchema), path)`: Repeated values are represented by an array Schema in v4; Config.Array also accepts flat separated input. + +- `Config.secret` -> `Config.redacted`: Secret was removed in favor of Redacted; this constructor already returns Redacted\. + +- `Config.string` -> `Config.string`: Unchanged. + +- `Config.succeed` -> `Config.succeed`: Unchanged. + +- `Config.suspend` -> `Config.schema(Schema.suspend(schemaThunk), path)`: General Config suspension was removed; model recursive parsing with a suspended Schema before constructing the Config. + +- `Config.sync` -> `Config.succeed(undefined).pipe(Config.map(() => thunk()))`: The dedicated lazy constant constructor was removed; mapping a constant Config preserves evaluation at parse time. + +- `Config.url` -> `Config.url`: Unchanged. + +- `Config.validate` -> `Config.schema(schema.check(check), path)`: Validation moved to Schema checks; attach the predicate and message to the Schema used by Config.schema. + +- `Config.withDescription` -> `Config.schema(schema.annotate({ description }), path)`: Config descriptions moved to Schema annotations in v4. + +- `Config.zip` -> `Config.all([self, that])`: Use the tuple overload of Config.all. + +- `Config.zipWith` -> `Config.all([self, that]).pipe(Config.map(([a, b]) => f(a, b)))`: Combine both configs with Config.all, then map the tuple. + +### `effect/ConfigError` + +- `ConfigError.And` -> `SchemaIssue.Composite`: The ConfigError boolean ADT was removed; combined schema failures are represented inside Config.ConfigError.cause as SchemaIssue.Composite. + +- `ConfigError.ConfigError` -> `Config.ConfigError`: Config errors are now a class in effect/Config wrapping either ConfigProvider.SourceError or Schema.SchemaError. + +- `ConfigError.ConfigError.Proto` -> `Config.ConfigError`: The public prototype interface was removed; use the Config.ConfigError class. + +- `ConfigError.ConfigError.Reducer` -> `none`: The ConfigError-specific reducer API was removed; inspect ConfigError.cause and recurse over SchemaError.issue when structured handling is required. + +- `ConfigError.ConfigErrorReducer` -> `none`: The ConfigError-specific reducer API was removed; inspect ConfigError.cause and recurse over SchemaError.issue when structured handling is required. + +- `ConfigError.ConfigErrorTypeId` -> `error instanceof Config.ConfigError`: The marker is gone because ConfigError is a class in v4. + +- `ConfigError.InvalidData` -> `new Config.ConfigError(new Schema.SchemaError(issue))`: Invalid configuration is now expressed as a SchemaIssue wrapped by SchemaError and Config.ConfigError. + +- `ConfigError.MissingData` -> `none`: There is no public missing-data error variant. A required absent config ultimately fails with a SchemaError, while Config.withDefault and Config.option handle semantic absence before it enters the public Effect error channel. + +- `ConfigError.Options` -> `none`: The shared constructor options type was removed; ConfigProvider.SourceError accepts message and optional cause, while Schema issues have issue-specific constructors. + +- `ConfigError.Or` -> `SchemaIssue.AnyOf`: The ConfigError boolean ADT was removed; alternative schema failures are represented inside Config.ConfigError.cause as SchemaIssue.AnyOf. + +- `ConfigError.SourceUnavailable` -> `new ConfigProvider.SourceError({ message, cause })`: Source failures moved to effect/ConfigProvider and are wrapped by Config.ConfigError when a Config is parsed. + +- `ConfigError.Unsupported` -> `none`: The variant was removed; use a SchemaError for unsupported input or ConfigProvider.SourceError for source capability failures. + +- `ConfigError.isAnd` -> `error.cause.issue._tag === "Composite"`: After narrowing cause with Schema.isSchemaError, inspect the SchemaIssue tag; the old And node no longer exists. + +- `ConfigError.isConfigError` -> `error instanceof Config.ConfigError`: ConfigError is a class in v4. + +- `ConfigError.isInvalidData` -> `Schema.isSchemaError(error.cause)`: Parsing and validation failures are SchemaError causes; inspect the contained SchemaIssue for finer classification. + +- `ConfigError.isMissingData` -> `none`: Do not infer semantic absence from a SchemaIssue. Use Config.withDefault or Config.option; they distinguish absent provider input from successful undefined, invalid input, and partial products. + +- `ConfigError.isMissingDataOnly` -> `Config.withDefault / Config.option`: The public classifier was removed. These combinators use provider lookup evidence rather than recursively classifying SchemaIssue values. + +- `ConfigError.isOr` -> `error.cause.issue._tag === "AnyOf"`: After narrowing cause with Schema.isSchemaError, inspect the SchemaIssue tag; the old Or node no longer exists. + +- `ConfigError.isSourceUnavailable` -> `error.cause instanceof ConfigProvider.SourceError`: Provider source failures now use the ConfigProvider.SourceError class. + +- `ConfigError.isUnsupported` -> `none`: The Unsupported variant was removed; report unsupported custom decoding through a SchemaError or source failures through ConfigProvider.SourceError. + +- `ConfigError.prefixed` -> `SchemaIssue.Pointer`: Represent path context by wrapping the underlying SchemaIssue in a Pointer before constructing SchemaError. + +- `ConfigError.reduceWithContext` -> `none`: The specialized fold was removed; branch on ConfigError.cause, then recurse over the public SchemaIssue union if a fold is needed. + +### `effect/ConfigProvider` + +- `ConfigProvider.ConfigProvider` -> `ConfigProvider.ConfigProvider`: The model remains but now exposes `load(path)`, returning `Effect`, and `mapInput(f)` for provider-owned path transformation. `undefined` means the path is missing; a `Node` means it exists. + +- `ConfigProvider.ConfigProvider.Flat` -> `ConfigProvider.ConfigProvider`: Flat providers were removed; implement the unified path-based provider with ConfigProvider.make. + +- `ConfigProvider.ConfigProvider.FromEnvConfig` -> `Parameters[0]`: Options are inline in v4 and contain env plus preserveEmptyStrings; custom path and sequence delimiters moved to provider path transforms and Config.Array/Config.Record schemas. + +- `ConfigProvider.ConfigProvider.FromMapConfig` -> `none`: fromMap and its delimiter options were removed; expand delimited keys into a nested value and use ConfigProvider.fromUnknown. + +- `ConfigProvider.ConfigProvider.KeyComponent` -> `ConfigProvider.Path[number]`: Tagged key components became plain string or number path segments. + +- `ConfigProvider.ConfigProvider.KeyIndex` -> `number`: Tagged KeyIndex values became numeric ConfigProvider.Path segments. + +- `ConfigProvider.ConfigProvider.KeyName` -> `string`: Tagged KeyName values became string ConfigProvider.Path segments. + +- `ConfigProvider.ConfigProvider.Proto` -> `ConfigProvider.ConfigProvider`: The public marker prototype was removed; use the provider interface itself. + +- `ConfigProvider.ConfigProviderTypeId` -> `ConfigProvider.ConfigProvider`: The runtime marker is private in v4; providers are created by public constructors and consumed structurally. + +- `ConfigProvider.FlatConfigProviderTypeId` -> `none`: The flat-provider abstraction and marker were removed. + +- `ConfigProvider.fromEnv` -> `ConfigProvider.fromEnv`: The constructor remains; pass env and preserveEmptyStrings options. Paths use underscore semantics, while sequence separators belong on Config schemas. + +- `ConfigProvider.fromFlat` -> `ConfigProvider.make`: Flat providers were unified with ConfigProvider; return a `Value`, `Record`, or `Array` node for a found path, or `undefined` when missing. + +- `ConfigProvider.fromJson` -> `ConfigProvider.fromUnknown`: Renamed to reflect support for any in-memory JavaScript value. + +- `ConfigProvider.fromMap` -> `ConfigProvider.fromUnknown`: Expand the map's delimited keys into a nested object first; v4 removed fromMap and its pathDelim/seqDelim options. + +- `ConfigProvider.kebabCase` -> `ConfigProvider.mapInput((path) => path.map((part) => typeof part === "string" ? String.kebabCase(part) : part))`: Named recasing helpers were removed except constantCase; transform string path segments explicitly. + +- `ConfigProvider.lowerCase` -> `ConfigProvider.mapInput((path) => path.map((part) => typeof part === "string" ? part.toLowerCase() : part))`: Transform string path segments explicitly with mapInput. + +- `ConfigProvider.make` -> `ConfigProvider.make`: The constructor now takes a path lookup returning `Effect`, rather than a full Config loader and flattened provider. Return `undefined` for a missing path and a `Node` for a found path. + +- `ConfigProvider.makeFlat` -> `ConfigProvider.make`: The flat-provider constructor was removed; return a `Value`, `Record`, or `Array` node for a found path, or `undefined` when missing. + +- `ConfigProvider.mapInputPath` -> `ConfigProvider.mapInput`: Renamed and generalized: the callback receives and returns the complete Path, including numeric array indexes. + +- `ConfigProvider.snakeCase` -> `ConfigProvider.mapInput((path) => path.map((part) => typeof part === "string" ? String.snakeCase(part) : part))`: Named recasing helpers were removed except constantCase; transform string path segments explicitly. + +- `ConfigProvider.unnested` -> `ConfigProvider.mapInput((path) => path[0] === name ? path.slice(1) : path)`: The named helper was removed; strip the matching leading segment explicitly. Add custom handling if the v3 mismatch error was significant. + +- `ConfigProvider.upperCase` -> `ConfigProvider.mapInput((path) => path.map((part) => typeof part === "string" ? part.toUpperCase() : part))`: Transform string path segments explicitly with mapInput. + +- `ConfigProvider.within` -> `ConfigProvider.orElse + ConfigProvider.mapInput`: The scoped transform helper was removed; build a provider that transforms paths below the prefix and falls back to the original provider elsewhere. + +### `effect/ConfigProviderPathPatch` + +- `ConfigProviderPathPatch.AndThen` -> `ConfigProvider.mapInput`: PathPatch is no longer public; compose path transformations as ordinary functions passed to mapInput. + +- `ConfigProviderPathPatch.Empty` -> `none`: The PathPatch ADT was removed; an unchanged provider represents the identity transformation. + +- `ConfigProviderPathPatch.MapName` -> `none`: The PathPatch ADT was removed; use a path transformation function with ConfigProvider.mapInput. + +- `ConfigProviderPathPatch.Nested` -> `none`: The PathPatch ADT was removed; use ConfigProvider.nested on the provider. + +- `ConfigProviderPathPatch.PathPatch` -> `(path: ConfigProvider.Path) => ConfigProvider.Path`: Path patches are ordinary full-path transformations in v4 and are installed with ConfigProvider.mapInput. + +- `ConfigProviderPathPatch.Unnested` -> `none`: The PathPatch ADT was removed; express prefix removal as a ConfigProvider.mapInput function. + +- `ConfigProviderPathPatch.empty` -> `ConfigProvider.ConfigProvider`: No identity patch value is needed; leave the provider untransformed. + +- `ConfigProviderPathPatch.mapName` -> `ConfigProvider.mapInput`: Map the string segments of the full ConfigProvider.Path explicitly. + +- `ConfigProviderPathPatch.nested` -> `ConfigProvider.nested`: Apply nesting directly to the provider instead of constructing a patch. + +- `ConfigProviderPathPatch.unnested` -> `ConfigProvider.mapInput`: Strip the expected leading path segment in a mapInput callback; v4 has no named unnested helper. + +### `effect/Console` + +- `Console.Console` -> `Console.Console`: Name retained, but v4 is a Context.Reference whose service methods are synchronous. Rewrite custom implementations from effectful methods plus .unsafe to direct console methods; module accessors such as Console.log still return Effect values. + +- `Console.TypeId` -> `none`: The public console brand was removed; v4 Console.Console is structural. + +- `Console.UnsafeConsole` -> `Console.Console`: The v4 service interface is the old unsafe/direct interface; .unsafe no longer exists. + +- `Console.setConsole` -> `Layer.succeed(Console.Console, console)`: Provide the v4 console reference as a layer. + +- `Console.withConsole` -> `Effect.provideService(effect, Console.Console, console)`: Console overrides now use the reference/service provider pattern. + +- `Console.withGroup` -> `Console.withGroup`: The API and data-first/data-last behavior remain. + +- `Console.withTime` -> `Console.withTime`: The API and data-first/data-last behavior remain. + +### `effect/Context` + +- `Context.Context` -> `Context.Context`: The type remains; unsafeMap is now mapUnsafe and v4 also exposes mutable. + +- `Context.GenericTag` -> `Context.Service(id)`: Use the function-style Context.Service constructor. + +- `Context.ReadonlyTag` -> `Context.Key`: Use the renamed service-key interface. + +- `Context.Reference` -> `Context.Reference`: Use Context.Reference\(id, { defaultValue }); the identifier type parameter was removed. + +- `Context.ReferenceClass` -> `Context.Reference(id, { defaultValue })`: Replace reference subclasses with a constant created by Context.Reference. + +- `Context.ReferenceTypeId` -> `none`: The marker is private in v4; use Context.isReference for runtime discrimination. + +- `Context.Tag` -> `Context.Service`: Use Context.Service\(id), or Context.Service\()(id) for class syntax. + +- `Context.Tag.Service` -> `Context.Service.Shape`: The namespace type helper was renamed with Tag. + +- `Context.TagClass` -> `Context.ServiceClass`: Use the renamed class-style service-key type. + +- `Context.TagClassShape` -> `Context.ServiceClass.Shape`: Use the renamed namespace type helper. + +- `Context.TagTypeId` -> `Context.ServiceTypeId`: The public type identifier was renamed with Tag. + +- `Context.TagUnify` -> `none`: The Context-specific unification hook was removed; Context.Key already extends Effect. + +- `Context.TagUnifyIgnore` -> `none`: The Context-specific Unify-ignore artifact was removed. + +- `Context.TypeId` -> `none`: The Context marker is private in v4; use Context.isContext for runtime checks. + +- `Context.ValidTagsById` -> `(key: Context.Key)`: The alias was removed; express the Context.Key constraint directly. + +- `Context.isTag` -> `Context.isKey`: The service-key guard was renamed. + +- `Context.unsafeGet` -> `Context.getUnsafe`: The unsafe getter was renamed. + +- `Context.unsafeMake` -> `Context.makeUnsafe`: The unsafe constructor was renamed and accepts a ReadonlyMap. + +### `effect/Cron` + +- `Cron.Cron` -> `Cron.Cron`: The model remains; update for the v4 representation and private type id. + +- `Cron.ParseError` -> `Cron.CronParseError`: The parse error was renamed and Cron.parse now returns Result.Result\. + +- `Cron.ParseErrorTypeId` -> `none`: The cron parse-error type id is private in v4. Use Cron.isCronParseError to narrow unknown failures. + +- `Cron.TypeId` -> `none`: The Cron type id is private in v4. Use Cron.isCron to identify cron values. + +- `Cron.isParseError` -> `Cron.isCronParseError`: The parse-error guard was renamed with the error type. + +- `Cron.sequenceReverse` -> `Cron.prev`: The reverse iterator was removed. Build an iterator that repeatedly calls Cron.prev, feeding each returned Date into the next call. + +- `Cron.unsafeParse` -> `Cron.parseUnsafe`: The throwing parser was renamed; it also accepts an optional time zone. + +### `effect/Data` + +- `Data.Case` -> `Data.TaggedEnum.ConstructorFrom`: The Case namespace was removed; its constructor helper moved under TaggedEnum. + +- `Data.Case.Constructor` -> `Data.TaggedEnum.ConstructorFrom`: Use the v4 tagged-enum constructor-function type. + +- `Data.Structural` -> `Data.Class`: Extend Data.Class instead of the removed Structural constructor alias. + +- `Data.TaggedEnum` -> `Data.TaggedEnum`: Still exported with the same record-to-discriminated-union role. + +- `Data.TaggedEnum.GenericMatchers` -> `Data.TaggedEnum.GenericMatchers`: Still exported with $is and $match helpers. + +- `Data.array` -> `none`: Use a normal copied array such as [...values]; v4 compares plain arrays structurally. + +- `Data.case` -> `none`: Use an ordinary typed object or identity constructor; plain objects are structurally equal in v4. + +- `Data.struct` -> `none`: Use an ordinary object or {...value}; plain objects are structurally equal in v4. + +- `Data.tuple` -> `none`: Use a normal tuple literal; plain tuples are structurally equal in v4. + +- `Data.unsafeArray` -> `none`: Use the array directly; v4 no longer needs prototype mutation for structural equality. + +- `Data.unsafeStruct` -> `none`: Use the object directly; v4 no longer needs prototype mutation for structural equality. + +### `effect/DateTime` + +- `DateTime.DateTime` -> `DateTime.DateTime`: The Utc | Zoned model remains; epochMillis fields are now epochMilliseconds and unit/part names use millisecond terminology. + +- `DateTime.DateTime.Input` -> `DateTime.DateTime.Input`: The input type remains and additionally accepts Instant and InstantWithZone objects. + +- `DateTime.DateTime.Parts` -> `DateTime.DateTime.Parts`: Rename millis, seconds, minutes, and hours fields to millisecond, second, minute, and hour. + +- `DateTime.DateTime.PartsForMath` -> `DateTime.DateTime.PartsForMath`: Rename the millis field to milliseconds; the other plural arithmetic fields remain. + +- `DateTime.DateTime.PartsWithWeekday` -> `DateTime.DateTime.PartsWithWeekday`: Rename millis, seconds, minutes, and hours fields to millisecond, second, minute, and hour. + +- `DateTime.DateTime.Proto` -> `DateTime.DateTime.Proto`: The protocol remains, but its marker uses the private v4 TypeId value. + +- `DateTime.DateTime.UnitPlural` -> `DateTime.DateTime.UnitPlural`: Use milliseconds instead of millis; the other plural unit strings remain. + +- `DateTime.DateTime.UnitSingular` -> `DateTime.DateTime.UnitSingular`: Use millisecond instead of milli; the other singular unit strings remain. + +- `DateTime.TimeZone` -> `DateTime.TimeZone`: The Offset | Named model remains; its public type-id marker type was removed. + +- `DateTime.TimeZone.Proto` -> `DateTime.TimeZone.Proto`: The protocol remains, but its marker uses the private v4 TimeZoneTypeId value. + +- `DateTime.TimeZoneTypeId` -> `none`: The time-zone type id is private in v4. Use DateTime.isTimeZone, isTimeZoneOffset, or isTimeZoneNamed. + +- `DateTime.TypeId` -> `none`: The DateTime type id is private in v4. Use DateTime.isDateTime, isUtc, or isZoned. + +- `DateTime.Utc` -> `DateTime.Utc`: The model remains; rename epochMillis to epochMilliseconds. + +- `DateTime.Zoned` -> `DateTime.Zoned`: The model remains; rename epochMillis and adjustedEpochMillis to epochMilliseconds and adjustedEpochMilliseconds. + +- `DateTime.distanceDuration` -> `Duration.millis(Math.abs(DateTime.distance(self, other)))`: DateTime.distance returns signed milliseconds in v4; take the absolute value and construct a Duration to preserve v3 behavior. + +- `DateTime.distanceDurationEither` -> `DateTime.distance + Result`: Compute the signed millisecond distance, wrap its absolute Duration as Result.succeed when positive and Result.fail when non-positive; v4 uses Result instead of Either. + +- `DateTime.greaterThan` -> `DateTime.isGreaterThan`: The comparison was renamed with the is prefix. + +- `DateTime.greaterThanOrEqualTo` -> `DateTime.isGreaterThanOrEqualTo`: The comparison was renamed with the is prefix. + +- `DateTime.lessThan` -> `DateTime.isLessThan`: The comparison was renamed with the is prefix. + +- `DateTime.lessThanOrEqualTo` -> `DateTime.isLessThanOrEqualTo`: The comparison was renamed with the is prefix. + +- `DateTime.unsafeFromDate` -> `DateTime.fromDateUnsafe`: The unsafe suffix moved to the end of the constructor name. + +- `DateTime.unsafeIsFuture` -> `DateTime.isFutureUnsafe`: The unsafe suffix moved to the end of the predicate name. + +- `DateTime.unsafeIsPast` -> `DateTime.isPastUnsafe`: The unsafe suffix moved to the end of the predicate name. + +- `DateTime.unsafeMake` -> `DateTime.makeUnsafe`: The unsafe suffix moved to the end of the constructor name. + +- `DateTime.unsafeMakeZoned` -> `DateTime.makeZonedUnsafe`: The unsafe suffix moved to the end of the constructor name. + +- `DateTime.unsafeNow` -> `DateTime.nowUnsafe`: The unsafe suffix moved to the end of the accessor name. + +- `DateTime.unsafeSetZoneNamed` -> `DateTime.setZoneNamedUnsafe`: The unsafe suffix moved to the end of the zone setter name. + +- `DateTime.zoneUnsafeMakeNamed` -> `DateTime.zoneMakeNamedUnsafe`: The unsafe suffix moved to the end of the named-zone constructor. + +### `effect/DefaultServices` + +- `DefaultServices.DefaultServices` -> `none`: The aggregate type and module were removed; Clock, Console, Random, ConfigProvider, and Tracer are independent defaulted references. + +- `DefaultServices.currentServices` -> `Effect.context() and Context.get(context, reference)`: The aggregate FiberRef was removed; access and override default Context.Reference services individually. + +- `DefaultServices.liveServices` -> `Context.empty() with individual Context.Reference defaults`: There is no live-services bundle; each default service reference supplies its own live default. + +### `effect/Deferred` + +- `Deferred.Deferred` -> `Deferred.Deferred`: The model remains but is now Pipeable rather than an Effect subtype; replace yielding the Deferred itself with Deferred.await. + +- `Deferred.Deferred.Variance` -> `Deferred.Deferred.Variance`: The marker remains under Deferred.Deferred, but its brand uses an internal type id; ordinary code should use Deferred.Deferred directly. + +- `Deferred.DeferredTypeId` -> `none`: The Deferred type id is internal in v4; do not inspect or construct the brand directly. + +- `Deferred.DeferredUnify` -> `none`: Deferred is no longer an Effect subtype, so its Effect unification helper was removed; call Deferred.await explicitly. + +- `Deferred.DeferredUnifyIgnore` -> `none`: Deferred is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `Deferred.await` -> `Deferred.await`: The function remains; call it explicitly because Deferred is no longer an Effect subtype in v4. + +- `Deferred.makeAs` -> `Deferred.makeUnsafe`: Use the synchronous v4 constructor; it no longer accepts or records a FiberId. + +- `Deferred.poll` -> `Deferred.poll`: The function remains and returns an Option containing the stored completion Effect. + +- `Deferred.unsafeDone` -> `Deferred.doneUnsafe`: The unsafe suffix moved to the end; the v4 function returns whether this call completed the Deferred. + +- `Deferred.unsafeMake` -> `Deferred.makeUnsafe`: The unsafe suffix moved to the end, and the v4 constructor takes no FiberId argument. + +### `effect/Differ` + +- `Differ.Differ` -> `Differ.Differ`: The interface remains, but is now an unbranded structural interface and patch takes arguments as patch(oldValue, patch). + +- `Differ.Differ.Chunk` -> `JsonPatch.JsonPatch`: The Chunk patch namespace was removed; Schema.toDifferJsonPatch uses the common RFC 6902 patch representation. + +- `Differ.Differ.Chunk.Patch` -> `JsonPatch.JsonPatch`: Use the patch type returned by Schema.toDifferJsonPatch instead of the removed Chunk-specific patch type. + +- `Differ.Differ.Chunk.TypeId` -> `none`: Chunk-specific patches and their public brand were removed; do not inspect a patch type id. + +- `Differ.Differ.Context` -> `Context.Context`: The Context patch namespace was removed; construct and merge Context values explicitly rather than diffing environments. + +- `Differ.Differ.Context.Patch` -> `none`: Context patches were removed; use Context.add, Context.merge, and Context.omit to build the desired Context directly. + +- `Differ.Differ.Context.TypeId` -> `none`: Context patches and their public brand were removed. + +- `Differ.Differ.HashMap` -> `JsonPatch.JsonPatch`: The HashMap patch namespace was removed; derive a JSON Patch differ from a Schema for the complete value. + +- `Differ.Differ.HashMap.Patch` -> `JsonPatch.JsonPatch`: Use the patch type returned by Schema.toDifferJsonPatch instead of the removed HashMap-specific patch type. + +- `Differ.Differ.HashMap.TypeId` -> `none`: HashMap-specific patches and their public brand were removed; do not inspect a patch type id. + +- `Differ.Differ.HashSet.Patch` -> `JsonPatch.JsonPatch`: Use the patch type returned by Schema.toDifferJsonPatch instead of the removed HashSet-specific patch type. + +- `Differ.Differ.HashSet.TypeId` -> `none`: HashSet-specific patches and their public brand were removed; do not inspect a patch type id. + +- `Differ.Differ.Or` -> `JsonPatch.JsonPatch`: The Either patch namespace was removed; derive one JSON Patch differ from the Schema for the union value. + +- `Differ.Differ.Or.Patch` -> `JsonPatch.JsonPatch`: Use the patch type returned by Schema.toDifferJsonPatch instead of the removed Either-specific patch type. + +- `Differ.Differ.Or.TypeId` -> `none`: Either-specific patches and their public brand were removed; do not inspect a patch type id. + +- `Differ.Differ.ReadonlyArray` -> `JsonPatch.JsonPatch`: The ReadonlyArray patch namespace was removed; Schema.toDifferJsonPatch uses the common RFC 6902 patch representation. + +- `Differ.Differ.ReadonlyArray.Patch` -> `JsonPatch.JsonPatch`: Use the patch type returned by Schema.toDifferJsonPatch instead of the removed ReadonlyArray-specific patch type. + +- `Differ.Differ.ReadonlyArray.TypeId` -> `none`: ReadonlyArray-specific patches and their public brand were removed; do not inspect a patch type id. + +- `Differ.TypeId` -> `none`: Differ is an unbranded structural interface in v4; do not inspect or implement a public type id. + +- `Differ.chunk` -> `Schema.toDifferJsonPatch`: Derive a JSON Patch differ from a Schema for the whole Chunk; v4 no longer exposes collection-specific patch constructors. + +- `Differ.combine` -> `differ.combine`: Call the combine method on the Differ value directly; the standalone helper was removed. + +- `Differ.diff` -> `differ.diff`: Call the diff method on the Differ value directly; the standalone helper was removed. + +- `Differ.empty` -> `differ.empty`: Read the empty property from the Differ value directly; the standalone accessor was removed. + +- `Differ.environment` -> `none`: The Context differ was removed; construct the target Context explicitly with Context.add, Context.merge, and Context.omit. + +- `Differ.hashMap` -> `Schema.toDifferJsonPatch`: Derive a JSON Patch differ from a Schema for the whole map; v4 no longer exposes collection-specific patch constructors. + +- `Differ.hashSet` -> `Schema.toDifferJsonPatch`: Derive a JSON Patch differ from a Schema for the whole set; v4 no longer exposes collection-specific patch constructors. + +- `Differ.make` -> `object literal satisfying Differ.Differ`: Differ is structural in v4; provide empty, diff, combine, and patch methods directly, with patch(oldValue, patch) argument order. + +- `Differ.orElseEither` -> `Schema.toDifferJsonPatch`: Derive one differ from the Schema for the Either value; the compositional Either-specific differ and patch type were removed. + +- `Differ.patch` -> `differ.patch`: Call the method directly and reverse the v3 method order: differ.patch(oldValue, patch). + +- `Differ.readonlyArray` -> `Schema.toDifferJsonPatch`: Derive a JSON Patch differ from a Schema for the whole array; v4 no longer exposes collection-specific patch constructors. + +- `Differ.transform` -> `object literal satisfying Differ.Differ`: There is no transform combinator; define a structural Differ that maps values before delegating to the original differ. + +- `Differ.update` -> `object literal satisfying Differ.Differ`: The update constructor was removed; define empty, diff, combine, and patch directly for function patches, or use Schema.toDifferJsonPatch. + +- `Differ.updateWith` -> `object literal satisfying Differ.Differ`: The updateWith constructor was removed; encode the desired merge rule in a structural Differ implementation. + +### `effect/Duration` + +- `Duration.Duration` -> `Duration.Duration`: The model remains and now also supports negative infinity; its type-id value is private. + +- `Duration.DurationInput` -> `Duration.Input`: The input type was renamed and expanded with negative values and Temporal.Duration-like objects. + +- `Duration.DurationValue` -> `Duration.DurationValue`: The tagged value remains and adds NegativeInfinity; its object fields are no longer readonly. + +- `Duration.TypeId` -> `none`: The Duration type id is private in v4. Use Duration.isDuration to narrow unknown values. + +- `Duration.decode` -> `Duration.fromInputUnsafe`: The throwing DurationInput decoder was renamed. + +- `Duration.decodeUnknown` -> `Duration.fromInput`: The safe decoder was renamed and accepts Duration.Input, returning Option\. + +- `Duration.formatIso` -> `none`: ISO 8601 duration formatting was removed from the v4 Duration module. The v4 source and migration guides expose no direct replacement; retain a local formatter when this wire format is required. + +- `Duration.fromIso` -> `none`: ISO 8601 duration parsing was removed from the v4 Duration module. The v4 source and migration guides expose no direct replacement; use a dedicated ISO parser and pass the resulting parts to Duration.fromInput. + +- `Duration.greaterThan` -> `Duration.isGreaterThan`: The comparison was renamed with the is prefix. + +- `Duration.greaterThanOrEqualTo` -> `Duration.isGreaterThanOrEqualTo`: The comparison was renamed with the is prefix. + +- `Duration.lessThan` -> `Duration.isLessThan`: The comparison was renamed with the is prefix. + +- `Duration.lessThanOrEqualTo` -> `Duration.isLessThanOrEqualTo`: The comparison was renamed with the is prefix. + +- `Duration.matchWith` -> `Duration.matchPair`: The two-duration matcher was renamed. + +- `Duration.unsafeDivide` -> `Duration.divideUnsafe`: The unsafe prefix moved to the end of the division function name. + +- `Duration.unsafeFormatIso` -> `none`: ISO 8601 duration formatting was removed from v4. The v4 Duration exports and migration guides contain no direct unsafe formatter; retain a local formatter if required. + +- `Duration.unsafeToNanos` -> `Duration.toNanosUnsafe`: The unsafe prefix moved to the end of the nanosecond conversion name. + +### `effect/Effect` + +- `Effect.Adapter` -> `none`: The generator adapter type was removed; yield Effect values directly inside `Effect.gen`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.All` -> `Effect.All`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.All.ExtractMode` -> `Effect.All.Return`: The `either` extraction helper was removed; use `mode: "result"` and the v4 return helper. Adapt arguments and imports to the v4 API. + +- `Effect.Blocked` -> `none`: The request-runtime blocked model is internal; use public `Request` and `RequestResolver` APIs. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.Effect` -> `Effect.Effect`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.Effect.AsEffect` -> `Effect.Effect`: Use the Effect type directly and extract channels with `Effect.Success`, `Effect.Error`, and `Effect.Services`. Adapt arguments and imports to the v4 API. + +- `Effect.Effect.Context` -> `Effect.Services`: Use the renamed type-level extractor for required services. Adapt arguments and imports to the v4 API. + +- `Effect.Effect.VarianceStruct` -> `Effect.Variance`: Use the v4 variance interface. Adapt arguments and imports to the v4 API. + +- `Effect.EffectGenerator` -> `Effect.EffectIterator`: Use the v4 iterator type used by generator delegation. Adapt arguments and imports to the v4 API. + +- `Effect.EffectTypeId` -> `Effect.TypeId`: Use the v4 type-level Effect identifier. Adapt arguments and imports to the v4 API. + +- `Effect.EffectUnify` -> `Effect.EffectUnify`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.EffectUnifyIgnore` -> `none`: The internal unification-ignore helper is no longer public; rely on v4 Effect inference. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.FunctionWithSpanOptions` -> `Tracer.SpanOptions`: Use the v4 tracing options type when wrapping functions with `Effect.withSpan`. Adapt arguments and imports to the v4 API. + +- `Effect.LatchUnify` -> `Latch.Latch`: Latch moved to the standalone `effect/Latch` module; rely on normal v4 inference. Adapt arguments and imports to the v4 API. + +- `Effect.LatchUnifyIgnore` -> `none`: The internal Latch unification helper was removed; use `Latch.Latch` directly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.Permit` -> `Semaphore.Semaphore`: Use the standalone Semaphore API and its `withPermit` / `withPermits` methods. Adapt arguments and imports to the v4 API. + +- `Effect.Repeat` -> `Effect.Repeat`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.Repeat.Options` -> `Effect.Repeat.Options`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.Retry` -> `Effect.Retry`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.Retry.Options` -> `Effect.Retry.Options`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.Service` -> `Context.Service`: Replace the `effect` constructor option with `make`. V4 does not generate a `Default` layer or wire `dependencies`; define a `Layer.effect` and provide its dependencies explicitly. + +- `Effect.Service.AllowedType` -> `Context.Service`: Service type machinery moved to `Context.Service`; do not reference its internal helper types. Adapt arguments and imports to the v4 API. + +- `Effect.Service.Class` -> `Context.Service`: Service classes are now defined with `Context.Service`. Adapt arguments and imports to the v4 API. + +- `Effect.Service.HasArguments` -> `Context.Service`: Service constructor typing is handled by `Context.Service`. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeAccessors` -> `Context.Service`: Use the generated `.use` helper instead of v3 accessor type machinery. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeArguments` -> `Context.Service`: Pass a `make` Effect in the v4 `Context.Service` options. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeContext` -> `Context.Service`: Service context typing is inferred by `Context.Service`. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeDeps` -> `Layer.provide`: Compose service dependencies explicitly with Layers. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeDepsE` -> `Layer.Error`: Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeDepsIn` -> `Layer.Services`: Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeDepsOut` -> `Layer.Success`: Use Layer channel extractors rather than Service internals. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeError` -> `Layer.Error`: Use the Layer error-channel extractor. Adapt arguments and imports to the v4 API. + +- `Effect.Service.MakeService` -> `Context.Service`: The service shape is inferred by `Context.Service`. Adapt arguments and imports to the v4 API. + +- `Effect.Service.ProhibitedType` -> `Context.Service`: Do not reference the removed internal validation type. Adapt arguments and imports to the v4 API. + +- `Effect.Tag` -> `Context.Service`: Define services with `Context.Service`; use the generated `.use` helper for accessors. Adapt arguments and imports to the v4 API. + +- `Effect.Tag.AllowedType` -> `Context.Service`: Tag validation internals were removed; use `Context.Service` directly. Adapt arguments and imports to the v4 API. + +- `Effect.Tag.ProhibitedType` -> `Context.Service`: Tag validation internals were removed; use `Context.Service` directly. Adapt arguments and imports to the v4 API. + +- `Effect.Tag.Proxy` -> `Context.Service.use`: Replace proxy accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API. + +- `Effect.acquireReleaseInterruptible` -> `Effect.acquireRelease`: Pass `{ interruptible: true }` in the options object. Adapt arguments and imports to the v4 API. + +- `Effect.allSuccesses` -> `Effect.all`: Run with `{ mode: "result" }`, then retain `Result.Success` values. Adapt arguments and imports to the v4 API. + +- `Effect.allWith` -> `Effect.all`: Wrap `Effect.all(values, options)` in a lambda when a data-last combinator is needed. Adapt arguments and imports to the v4 API. + +- `Effect.allowInterrupt` -> `Effect.yieldNow`: Yield to the scheduler to create an interruptible checkpoint. Adapt arguments and imports to the v4 API. + +- `Effect.annotateLogs` -> `Effect.annotateLogs`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.ap` -> `Effect.zipWith`: Zip the function effect and value effect, then apply the function in the combiner. Adapt arguments and imports to the v4 API. + +- `Effect.asSomeError` -> `Effect.mapError`: Map errors with `Option.some`. Adapt arguments and imports to the v4 API. + +- `Effect.async` -> `Effect.callback`: Use the renamed callback constructor. Adapt arguments and imports to the v4 API. + +- `Effect.asyncEffect` -> `Effect.callback`: The callback registration may return an Effect cleanup action in v4. Adapt arguments and imports to the v4 API. + +- `Effect.bindAll` -> `Effect.bind + Effect.all`: Bind the result of `Effect.all` explicitly in the do-notation pipeline. Adapt arguments and imports to the v4 API. + +- `Effect.blocked` -> `none`: The request-runtime blocked constructor is internal; express work with `Effect.request` and a `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.cacheRequestResult` -> `none`: Direct request-cache mutation was removed; configure request resolution through `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.cachedFunction` -> `none`: The function memoizer was removed; use `Cache` for keyed effectful caching. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.catch` -> `Effect.catch`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.catchAll` -> `Effect.catch`: Use the shortened v4 error-handler name. Adapt arguments and imports to the v4 API. + +- `Effect.catchAllCause` -> `Effect.catchCause`: Use the shortened v4 cause-handler name. Adapt arguments and imports to the v4 API. + +- `Effect.catchAllDefect` -> `Effect.catchDefect`: Use the shortened v4 defect-handler name. Adapt arguments and imports to the v4 API. + +- `Effect.catchSome` -> `Effect.catchFilter`: Replace the Option-returning partial function with a `Filter` and handler. Adapt arguments and imports to the v4 API. + +- `Effect.catchSomeCause` -> `Effect.catchCauseFilter`: Replace the Option-returning partial function with a cause `Filter` and handler. Adapt arguments and imports to the v4 API. + +- `Effect.catchSomeDefect` -> `none`: Use `Effect.catchDefect` and branch explicitly, re-dying for unmatched defects. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.cause` -> `Effect.exit`: Inspect `Exit.Failure.cause`; v4 no longer exposes an Effect-only cause extractor. Adapt arguments and imports to the v4 API. + +- `Effect.checkInterruptible` -> `none`: Interruptibility introspection was removed; structure the region explicitly with `Effect.interruptible` or `Effect.uninterruptible`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.clock` -> `Clock.Clock`: Services are Effects in v4; yield or compose `Clock.Clock` directly. Adapt imports to the v4 API. + +- `Effect.configProviderWith` -> `ConfigProvider.ConfigProvider.use`: Use the ConfigProvider reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API. + +- `Effect.console` -> `Console.Console`: Services are Effects in v4; yield or compose `Console.Console` directly. Adapt imports to the v4 API. + +- `Effect.consoleWith` -> `Console.Console.use`: Use the Console reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API. + +- `Effect.contextWithEffect` -> `Effect.contextWith`: `contextWith` accepts an effectful callback in v4. Adapt arguments and imports to the v4 API. + +- `Effect.currentPropagatedSpan` -> `Effect.currentParentSpan`: Use the current parent span representation. Adapt arguments and imports to the v4 API. + +- `Effect.custom` -> `none`: The low-level custom instruction constructor was removed; use public constructors such as `Effect.sync`, `Effect.suspend`, or `Effect.callback`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.daemonChildren` -> `Effect.awaitAllChildren`: Use structured child-fiber waiting, or fork explicitly with `Effect.forkDetach` when detachment is intended. Adapt arguments and imports to the v4 API. + +- `Effect.descriptor` -> `Effect.fiberId`: The full fiber descriptor was removed; retrieve the current numeric fiber id. Adapt arguments and imports to the v4 API. + +- `Effect.descriptorWith` -> `Effect.fiberId + Effect.flatMap`: Read the current fiber id and invoke the callback explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.dieMessage` -> `Effect.die`: Construct the desired defect explicitly and pass it to `Effect.die`. Adapt arguments and imports to the v4 API. + +- `Effect.dieSync` -> `Effect.suspend + Effect.die`: Evaluate the lazy defect inside `Effect.suspend`. Adapt arguments and imports to the v4 API. + +- `Effect.diffFiberRefs` -> `none`: The public FiberRefs diff API was removed; model fiber-local state with context references and scoped `Effect.provideService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.disconnect` -> `Effect.forkDetach`: Fork explicitly and decide how to await or interrupt the detached Fiber. Adapt arguments and imports to the v4 API. + +- `Effect.dropUntil` -> `none`: Use an explicit `Effect.gen` loop for an effectful stopping predicate. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.dropWhile` -> `none`: Use an explicit `Effect.gen` loop, or `Array.dropWhile` when the predicate is pure. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.either` -> `Effect.result`: V4 represents typed success/failure as `Result` instead of `Either`. Adapt arguments and imports to the v4 API. + +- `Effect.ensureErrorType` -> `Effect.satisfiesErrorType`: Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API. + +- `Effect.ensureRequirementsType` -> `Effect.satisfiesServicesType`: Use the renamed compile-time services constraint. Adapt arguments and imports to the v4 API. + +- `Effect.ensureSuccessType` -> `Effect.satisfiesSuccessType`: Use the renamed compile-time channel constraint. Adapt arguments and imports to the v4 API. + +- `Effect.ensuringChild` -> `Effect.ensuring + Fiber APIs`: Track the child Fiber explicitly and run the finalizer with `Effect.ensuring`. Adapt arguments and imports to the v4 API. + +- `Effect.ensuringChildren` -> `Effect.awaitAllChildren + Effect.ensuring`: Use structured child waiting and an explicit finalizer. Adapt arguments and imports to the v4 API. + +- `Effect.every` -> `Effect.forEach`: Evaluate predicates with `Effect.forEach`, then test the resulting booleans with `Array.every`. Adapt arguments and imports to the v4 API. + +- `Effect.exists` -> `Effect.findFirst`: Find the first value satisfying the effectful predicate and test the returned Option. Adapt arguments and imports to the v4 API. + +- `Effect.fiberIdWith` -> `Effect.fiberId + Effect.flatMap`: Read the numeric fiber id and invoke the callback explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.filterEffectOrElse` -> `Effect.flatMap`: Evaluate the effectful predicate and branch to `Effect.succeed` or the fallback. Adapt arguments and imports to the v4 API. + +- `Effect.filterEffectOrFail` -> `Effect.flatMap`: Evaluate the effectful predicate and branch to `Effect.succeed` or `Effect.fail`. Adapt arguments and imports to the v4 API. + +- `Effect.filterOrDie` -> `Effect.filterOrFail + Effect.orDie`: Filter with a typed failure, then convert it to a defect. Adapt arguments and imports to the v4 API. + +- `Effect.filterOrDieMessage` -> `Effect.filterOrFail + Effect.orDie`: Create the message-bearing error in `filterOrFail`, then convert it to a defect. Adapt arguments and imports to the v4 API. + +- `Effect.finalizersMask` -> `none`: Configurable finalizer execution strategies were removed; register ordered finalizers explicitly in a Scope. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.flipWith` -> `Effect.flip`: Flip, apply the transformation, then flip the resulting Effect back. Adapt arguments and imports to the v4 API. + +- `Effect.fn` -> `Effect.fn`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.fn.Gen` -> `Effect.fn.Return`: Use the v4 generator-return helper type. Adapt arguments and imports to the v4 API. + +- `Effect.fn.NonGen` -> `Effect.fn.Untraced`: Use the v4 function helper type for non-generator wrapping. Adapt arguments and imports to the v4 API. + +- `Effect.fork` -> `Effect.forkChild`: Use the renamed structured child-fiber combinator. Adapt arguments and imports to the v4 API. + +- `Effect.forkAll` -> `Effect.forEach + Effect.forkChild`: Fork each effect explicitly, or prefer a higher-level concurrent combinator. Adapt arguments and imports to the v4 API. + +- `Effect.forkDaemon` -> `Effect.forkDetach`: Use the renamed detached-fiber combinator. Adapt arguments and imports to the v4 API. + +- `Effect.forkWithErrorHandler` -> `Effect.forkChild + Fiber.await`: Fork explicitly and observe the Fiber result to handle errors. Adapt arguments and imports to the v4 API. + +- `Effect.fromFiber` -> `Fiber.join`: Join the Fiber to obtain an Effect of its result. Adapt arguments and imports to the v4 API. + +- `Effect.fromFiberEffect` -> `Effect.flatMap + Fiber.join`: FlatMap the effectful Fiber and join it. Adapt arguments and imports to the v4 API. + +- `Effect.fromNullable` -> `Effect.fromOption + Option.fromNullable`: Convert the nullable value to Option, then lift it into Effect. Adapt arguments and imports to the v4 API. + +- `Effect.functionWithSpan` -> `Effect.withSpan`: Wrap the function body with a span whose name/options are derived from its arguments. Adapt arguments and imports to the v4 API. + +- `Effect.gen` -> `Effect.gen`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.getFiberRefs` -> `none`: The FiberRefs collection is no longer public; access individual context references through Effect services. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.getRuntimeFlags` -> `none`: RuntimeFlags are no longer a public Effect service; use supported high-level runtime options. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.head` -> `Effect.flatMap + Array.head + Effect.fromOption`: Inspect the produced iterable explicitly and fail when it is empty. Adapt arguments and imports to the v4 API. + +- `Effect.if` -> `Effect.suspend`: Select the branch lazily with a JavaScript conditional inside `Effect.suspend`. Adapt arguments and imports to the v4 API. + +- `Effect.ignoreLogged` -> `Effect.ignore`: Pass `{ log: true }` to the consolidated ignore combinator. Adapt arguments and imports to the v4 API. + +- `Effect.inheritFiberRefs` -> `none`: Bulk FiberRef inheritance was removed; propagate required context references explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.interruptWith` -> `Effect.interrupt`: V4 interruption uses the current fiber identity; remove the explicit FiberId argument. Adapt arguments and imports to the v4 API. + +- `Effect.intoDeferred` -> `Deferred.into`: Use the Deferred module combinator. Adapt arguments and imports to the v4 API. + +- `Effect.iterate` -> `none`: Use an explicit stateful `Effect.gen` loop; v4 removed the Effect-specific loop helper. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.labelMetrics` -> `Metric.withAttributes`: Convert labels to metric attributes and scope them around the Effect. Adapt arguments and imports to the v4 API. + +- `Effect.labelMetricsScoped` -> `Metric.withAttributes`: Apply metric attributes to the scoped Effect rather than mutating scoped labels. Adapt arguments and imports to the v4 API. + +- `Effect.let` -> `Effect.let`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.liftPredicate` -> `Effect.filterOrFail`: Lift the input with `Effect.succeed` and apply `filterOrFail`. Adapt arguments and imports to the v4 API. + +- `Effect.linkSpanCurrent` -> `Effect.linkSpans`: Use the v4 span-link combinator. Adapt arguments and imports to the v4 API. + +- `Effect.locally` -> `Effect.provideService`: FiberRef values are context references in v4; provide the reference for the Effect lifetime. Adapt arguments and imports to the v4 API. + +- `Effect.locallyScoped` -> `Effect.provideService`: Provide the context reference around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.locallyScopedWith` -> `Effect.updateServiceScoped`: Context references replace FiberRefs in v4; update the reference for the current scope. Adapt arguments and imports to the v4 API. + +- `Effect.locallyWith` -> `Effect.updateService`: Context references replace FiberRefs in v4; update the reference around the target Effect. Adapt arguments and imports to the v4 API. + +- `Effect.logAnnotations` -> `References.CurrentLogAnnotations`: Context references are Effects in v4; yield or compose `References.CurrentLogAnnotations` directly. Adapt imports to the v4 API. + +- `Effect.loop` -> `none`: Use an explicit `Effect.gen` loop and collect results when needed. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.makeLatch` -> `Latch.make`: Latch constructors moved to `effect/Latch`. Adapt arguments and imports to the v4 API. + +- `Effect.makeSemaphore` -> `Semaphore.make`: Semaphore constructors moved to `effect/Semaphore`. Adapt arguments and imports to the v4 API. + +- `Effect.mapAccum` -> `Effect.reduce`: Carry `[state, output]` through an effectful reduction. Adapt arguments and imports to the v4 API. + +- `Effect.mapErrorCause` -> `Effect.catchCause + Effect.failCause`: Transform the Cause in a cause handler and fail with the mapped Cause. Adapt arguments and imports to the v4 API. + +- `Effect.mapInputContext` -> `Effect.contextWith + Effect.provide`: Build the required context from the incoming context and provide it explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.merge` -> `Effect.catch`: Recover each typed error with `Effect.succeed` so both channels become success values. Adapt arguments and imports to the v4 API. + +- `Effect.mergeAll` -> `Effect.reduce`: Reduce the input effects with an effectful accumulator. Adapt arguments and imports to the v4 API. + +- `Effect.metricLabels` -> `Metric.CurrentMetricAttributes`: Context references are Effects in v4; yield or compose `Metric.CurrentMetricAttributes` directly. Adapt imports to the v4 API. + +- `Effect.negate` -> `Effect.map`: Map the boolean result with logical negation. Adapt arguments and imports to the v4 API. + +- `Effect.none` -> `Effect.flatMap + Option.match`: Fail for `Some` and succeed with void for `None`. Adapt arguments and imports to the v4 API. + +- `Effect.once` -> `Effect.cached`: Create the cached Effect once, then execute the returned Effect repeatedly. Adapt arguments and imports to the v4 API. + +- `Effect.optionFromOptional` -> `Effect.catchTag`: Map success to `Option.some` and recover `NoSuchElementError` with `Option.none`. Adapt arguments and imports to the v4 API. + +- `Effect.orDieWith` -> `Effect.mapError + Effect.orDie`: Map the typed error to the desired defect, then convert failures to defects. Adapt arguments and imports to the v4 API. + +- `Effect.orElse` -> `Effect.catch`: Ignore the caught error and evaluate the fallback Effect. Adapt arguments and imports to the v4 API. + +- `Effect.orElseFail` -> `Effect.mapError`: Replace every typed error with the lazily produced failure value. Adapt arguments and imports to the v4 API. + +- `Effect.parallelErrors` -> `Effect.all`: Use `{ mode: "result", concurrency: "unbounded" }` and collect failures explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.parallelFinalizers` -> `none`: Parallel finalizer strategy mutation was removed; fork independent cleanup explicitly when ordering is irrelevant. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.patchFiberRefs` -> `none`: Bulk FiberRefs patching was removed; update individual context references with `Effect.updateService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.patchRuntimeFlags` -> `none`: RuntimeFlags patching was removed from the public API; use supported high-level runtime options. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.raceWith` -> `Effect.raceFirst + Fiber APIs`: Use `raceFirst` for first completion, or fork both effects and inspect their Exits for custom finishers. Adapt arguments and imports to the v4 API. + +- `Effect.random` -> `Random.Random`: Services are Effects in v4; yield or compose `Random.Random` directly. Adapt imports to the v4 API. + +- `Effect.randomWith` -> `Random.Random.use`: Use the Random reference's `.use` helper to invoke the effectful callback. Prefer module-level Random operations when possible. + +- `Effect.reduceEffect` -> `Effect.flatMap + Effect.reduce`: Evaluate the initial Effect, then reduce the remaining effects. Adapt arguments and imports to the v4 API. + +- `Effect.reduceRight` -> `Effect.reduce`: Reverse the input first, then perform the effectful reduction. Adapt arguments and imports to the v4 API. + +- `Effect.reduceWhile` -> `none`: Use an explicit `Effect.gen` loop that checks the accumulator before each step. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.repeatN` -> `Effect.repeat`: Pass `{ times: n }` to the consolidated repeat combinator. Adapt arguments and imports to the v4 API. + +- `Effect.runRequestBlock` -> `none`: The request-runtime block runner is internal; submit requests with `Effect.request`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.runtime` -> `Effect.context + Effect.runForkWith`: Capture services as a Context and use the corresponding `run*With` function. Adapt arguments and imports to the v4 API. + +- `Effect.scheduleForked` -> `Effect.schedule + Effect.forkScoped`: Schedule the Effect, then fork it in the current Scope. Adapt arguments and imports to the v4 API. + +- `Effect.scopeWith` -> `Effect.scopedWith`: Use the renamed scoped callback combinator. Adapt arguments and imports to the v4 API. + +- `Effect.sequentialFinalizers` -> `none`: Sequential reverse-order finalization is the normal Scope behavior; remove this wrapper. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.serviceConstants` -> `Context.Service.use`: Expose constants from the service explicitly or through the generated `use` helper. Adapt arguments and imports to the v4 API. + +- `Effect.serviceFunction` -> `Context.Service.use`: Use the service class `.use` helper to build an accessor function. Adapt arguments and imports to the v4 API. + +- `Effect.serviceFunctionEffect` -> `Context.Service.use`: Use the service class `.use` helper for effect-returning methods. Adapt arguments and imports to the v4 API. + +- `Effect.serviceFunctions` -> `Context.Service.use`: Define explicit service accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API. + +- `Effect.serviceMembers` -> `Context.Service.use`: Define explicit service accessors with the generated `.use` helper. Adapt arguments and imports to the v4 API. + +- `Effect.serviceOptional` -> `service`: Services are Effects in v4; yield or compose the service key directly. Use `Effect.serviceOption` only when absence is expected. + +- `Effect.setFiberRefs` -> `none`: Bulk FiberRefs replacement was removed; provide individual context references. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.step` -> `none`: The low-level Effect stepping API was removed from the public surface. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.summarized` -> `Effect.gen`: Run the summary Effect before and after the target Effect and combine the two measurements explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.supervised` -> `FiberSet`: Track explicitly forked Fibers in a scoped `FiberSet` instead of installing a runtime Supervisor. Adapt arguments and imports to the v4 API. + +- `Effect.tagMetrics` -> `Metric.withAttributes`: Convert key/value tags to metric attributes. Adapt arguments and imports to the v4 API. + +- `Effect.tagMetricsScoped` -> `Metric.withAttributes`: Apply attributes around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.takeUntil` -> `none`: Use an explicit `Effect.gen` loop for an effectful stopping predicate. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.takeWhile` -> `none`: Use an explicit `Effect.gen` loop, or `Array.takeWhile` when the predicate is pure. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.tapBoth` -> `Effect.tapError + Effect.tap`: Tap the failure path first, then tap successful values. Adapt arguments and imports to the v4 API. + +- `Effect.tapErrorCause` -> `Effect.tapCause`: Use the shortened v4 cause-tap name. Adapt arguments and imports to the v4 API. + +- `Effect.timedWith` -> `Effect.gen`: Read the supplied clock Effect before and after the target and compute the Duration explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.timeoutFail` -> `Effect.timeoutOrElse`: Use `Effect.fail(onTimeout())` as the timeout fallback. Adapt arguments and imports to the v4 API. + +- `Effect.timeoutFailCause` -> `Effect.timeoutOrElse`: Use `Effect.failCause(onTimeout())` as the timeout fallback. Adapt arguments and imports to the v4 API. + +- `Effect.timeoutTo` -> `Effect.timeoutOrElse + Effect.map`: Map successful values first and use the timeout fallback for `onTimeout`. Adapt arguments and imports to the v4 API. + +- `Effect.tracerWith` -> `Tracer.Tracer.use`: Use the Tracer reference's `.use` helper to invoke the effectful callback. Adapt arguments and imports to the v4 API. + +- `Effect.transplant` -> `none`: Fiber scope grafting was removed; use structured concurrency with `forkChild`, `forkScoped`, or `forkIn`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.transposeMapOption` -> `Option.match`: Return `Effect.succeedNone` for None and map the Effect result to Some. Adapt arguments and imports to the v4 API. + +- `Effect.try` -> `Effect.try`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.tryMap` -> `Effect.flatMap + Effect.try`: FlatMap the source value into the v4 synchronous try constructor. Adapt arguments and imports to the v4 API. + +- `Effect.tryMapPromise` -> `Effect.flatMap + Effect.tryPromise`: FlatMap the source value into the v4 Promise try constructor. Adapt arguments and imports to the v4 API. + +- `Effect.unless` -> `Effect.suspend`: Select `Effect.void` or the target Effect with a negated lazy condition. Adapt arguments and imports to the v4 API. + +- `Effect.unlessEffect` -> `Effect.when`: Negate the effectful boolean condition, then use the consolidated `when`. Adapt arguments and imports to the v4 API. + +- `Effect.unsafeMakeLatch` -> `Latch.makeUnsafe`: The unsafe constructor moved to `effect/Latch`. Adapt arguments and imports to the v4 API. + +- `Effect.unsafeMakeSemaphore` -> `Semaphore.makeUnsafe`: The unsafe constructor moved to `effect/Semaphore`. Adapt arguments and imports to the v4 API. + +- `Effect.unsandbox` -> `Effect.catch + Effect.failCause`: Treat the sandboxed Cause as an error and fail with that Cause. Adapt arguments and imports to the v4 API. + +- `Effect.updateFiberRefs` -> `none`: Bulk FiberRefs updates were removed; update individual context references with `Effect.updateService`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.using` -> `Effect.scoped + Effect.flatMap`: Acquire inside a fresh Scope, run the use Effect, and close the Scope afterward. Adapt arguments and imports to the v4 API. + +- `Effect.validateAll` -> `Effect.validate`: Use the consolidated collection validation combinator. Adapt arguments and imports to the v4 API. + +- `Effect.validateFirst` -> `Effect.firstSuccessOf`: Map inputs to validation effects and select the first success; handle accumulated diagnostics explicitly if required. Adapt arguments and imports to the v4 API. + +- `Effect.validateWith` -> `Effect.zipWith`: Zip and combine the Effects; use `mode: "result"` when both failures must be retained. Adapt arguments and imports to the v4 API. + +- `Effect.whenEffect` -> `Effect.when`: The v4 `when` combinator accepts an effectful boolean condition directly. Adapt arguments and imports to the v4 API. + +- `Effect.whenFiberRef` -> `reference.use + Effect.when`: Use the Context.Reference `.use` helper to inspect the value, test it, and branch explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.whenLogLevel` -> `none`: Log-level conditional execution was removed; configure Logger filtering and guard optional work explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.whenRef` -> `Ref.get + Effect.flatMap`: Read the Ref, test it, and branch explicitly. Adapt arguments and imports to the v4 API. + +- `Effect.withClock` -> `Effect.provideService`: Provide `Clock.Clock` for the target Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withClockScoped` -> `Effect.provideService`: Provide `Clock.Clock` around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withConcurrency` -> `none`: Ambient concurrency was removed; pass `concurrency` directly to `Effect.all`, `Effect.forEach`, and related combinators. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withConfigProvider` -> `Effect.provideService`: Provide `ConfigProvider.ConfigProvider` for the target Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withConfigProviderScoped` -> `Effect.provideService`: Provide the ConfigProvider around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withConsole` -> `Effect.provideService`: Provide `Console.Console` for the target Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withConsoleScoped` -> `Effect.provideService`: Provide the Console service around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withEarlyRelease` -> `Scope.make + Scope.close`: Create a Scope explicitly, provide it to acquisition, and retain a close action. Adapt arguments and imports to the v4 API. + +- `Effect.withFiberRuntime` -> `none`: Direct FiberRuntime access was removed; use public Effect, Fiber, and Context operations. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withLogSpan` -> `Effect.withLogSpan`: Still exported in v4; update call sites for the revised signature, options, and channel inference. + +- `Effect.withMaxOpsBeforeYield` -> `none`: The scheduler operation budget is no longer configurable through Effect. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withMetric` -> `Effect.tap + Metric.update`: Update the Metric explicitly from the Effect success value. Adapt arguments and imports to the v4 API. + +- `Effect.withRandom` -> `Effect.provideService`: Provide `Random.Random` for the target Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withRandomFixed` -> `Effect.provideService`: Provide a custom deterministic `Random.Random` implementation. Adapt arguments and imports to the v4 API. + +- `Effect.withRandomScoped` -> `Effect.provideService`: Provide the Random service around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withRequestBatching` -> `none`: Ambient request batching configuration was removed; configure batching in the `RequestResolver`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withRequestCache` -> `none`: Ambient request-cache replacement was removed; model keyed caching explicitly with `Cache`. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withRequestCaching` -> `none`: Ambient request caching was removed; configure resolution or use `Cache` explicitly. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withRuntimeFlagsPatch` -> `none`: RuntimeFlags patching was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withRuntimeFlagsPatchScoped` -> `none`: Scoped RuntimeFlags patching was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withScheduler` -> `none`: Ambient scheduler replacement was removed; use supported runtime run options or explicit scheduling combinators. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withSchedulingPriority` -> `none`: Ambient fiber scheduling priority was removed from the public API. No direct public replacement exists in v4; rewrite the call site around the stated v4 primitive. + +- `Effect.withTracerScoped` -> `Effect.provideService`: Provide the Tracer service around the scoped Effect. Adapt arguments and imports to the v4 API. + +- `Effect.withUnhandledErrorLogLevel` -> `Effect.ignore`: Handle or explicitly ignore child-fiber failures, selecting the desired log behavior at the boundary. Adapt arguments and imports to the v4 API. + +- `Effect.zipLeft` -> `Effect.zip + Effect.map`: Zip the Effects and select the first tuple element. Adapt arguments and imports to the v4 API. + +- `Effect.zipRight` -> `Effect.andThen`: Sequence the Effects and retain the second result. Adapt arguments and imports to the v4 API. + +### `effect/Effectable` + +- `Effectable.ChannelTypeId` -> `Channel.TypeId`: The public channel brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol. + +- `Effectable.Class` -> `Effectable.Class`: Still available; replace commit() with an override property or getter returning the Effect. + +- `Effectable.CommitPrimitive` -> `new() => Effect.Effect`: The named constructor interface was removed; inline the constructor type when needed. + +- `Effectable.CommitPrototype` -> `Effectable.Prototype`: Use Effectable.Prototype({ label, evaluate(fiber) { ... } }) and move the old commit body into evaluate. + +- `Effectable.EffectPrototype` -> `Effectable.Prototype`: The raw multi-branded prototype was removed; use Prototype with an explicit evaluate callback. + +- `Effectable.EffectTypeId` -> `Effect.TypeId`: The public Effect brand moved to Effect; v4 uses a string TypeId rather than the v3 Symbol. + +- `Effectable.SinkTypeId` -> `Sink.isSink`: Sink's TypeId is private in v4; use the public guard for runtime checks and public Sink constructors for values. + +- `Effectable.StreamTypeId` -> `Stream.TypeId`: The public stream brand moved to its owning module; v4 uses a string TypeId rather than the v3 Symbol. + +- `Effectable.StructuralClass` -> `Effectable.Class`: Use Class and migrate commit() to override; v4 equality is structural by default. + +- `Effectable.StructuralCommitPrototype` -> `Effectable.Prototype`: Use Prototype with evaluate; a separate structural prototype is unnecessary because v4 equality is structural by default. + +### `effect/Either` + +- `Either.Do` -> `Result.Do`: The empty successful do-notation value moved to Result. + +- `Either.Either` -> `Result.Result`: Either\ became Result\; Right and Left became Success and Failure. + +- `Either.Either.Left` -> `Result.Result.Failure`: Use the Result namespace extractor for the failure variant. + +- `Either.Either.Right` -> `Result.Result.Success`: Use the Result namespace extractor for the success variant. + +- `Either.EitherTypeLambda` -> `Result.ResultTypeLambda`: Moved and renamed with Result. + +- `Either.EitherUnify` -> `Result.ResultUnify`: Moved and renamed with Result. + +- `Either.EitherUnifyIgnore` -> `Result.ResultUnifyIgnore`: Moved and renamed with Result. + +- `Either.Left` -> `Result.Failure`: Left became Failure; .left became .failure. + +- `Either.Right` -> `Result.Success`: Right became Success; .right became .success. + +- `Either.TypeId` -> `none`: Result keeps its brand private and exports no public TypeId. + +- `Either.all` -> `Result.all`: Either moved to Result; collection behavior is retained with Failure and Success terminology. + +- `Either.ap` -> `Result.flatMap`: Use Result.flatMap(self, (f) =\> Result.map(that, f)); v4 has no Result.ap. + +- `Either.bind` -> `Result.bind`: The do-notation combinator moved to Result. + +- `Either.bindTo` -> `Result.bindTo`: The do-notation combinator moved to Result. + +- `Either.filterOrLeft` -> `Result.filterOrFail`: Left is now Failure, so the predicate combinator is filterOrFail. + +- `Either.flip` -> `Result.flip`: The channel-swapping combinator moved to Result. + +- `Either.fromNullable` -> `Result.fromNullishOr`: Renamed with v4 nullish-or terminology. + +- `Either.getEquivalence` -> `Result.makeEquivalence`: Pass success and failure equivalences positionally instead of a right and left object. + +- `Either.getLeft` -> `Result.getFailure`: Extract the Result failure as an Option. + +- `Either.getOrElse` -> `Result.getOrElse`: Moved unchanged to Result. + +- `Either.getOrThrow` -> `Result.getOrThrow`: V4 throws the raw Failure value; use getOrThrowWith when a custom Error is required. + +- `Either.getOrThrowWith` -> `Result.getOrThrowWith`: Moved to Result; the callback receives the Failure value. + +- `Either.getOrUndefined` -> `Result.getOrUndefined`: Moved unchanged to Result. + +- `Either.getRight` -> `Result.getSuccess`: Extract the Result success as an Option. + +- `Either.isEither` -> `Result.isResult`: Renamed with the data type. + +- `Either.isLeft` -> `Result.isFailure`: Left is now the Failure variant. + +- `Either.isRight` -> `Result.isSuccess`: Right is now the Success variant. + +- `Either.left` -> `Result.fail`: Construct a Failure with Result.fail. + +- `Either.let` -> `Result.let`: The do-notation combinator moved to Result. + +- `Either.map` -> `Result.map`: Map now transforms the Success channel. + +- `Either.mapLeft` -> `Result.mapError`: Left mapping became failure-channel error mapping. + +- `Either.match` -> `Result.match`: Rename handlers from onLeft and onRight to onFailure and onSuccess. + +- `Either.right` -> `Result.succeed`: Construct a Success with Result.succeed. + +- `Either.try` -> `Result.try`: The synchronous throwable constructor moved to Result. + +- `Either.void` -> `Result.void`: Use the prebuilt successful Result\. + +### `effect/Encoding` + +- `Encoding.DecodeException` -> `Encoding.EncodingError`: Use the unified error class with kind Decode. + +- `Encoding.DecodeExceptionTypeId` -> `Encoding.EncodingErrorTypeId`: Decode and encode failures now share one marker. + +- `Encoding.EncodeException` -> `Encoding.EncodingError`: Use the unified error class with kind Encode. + +- `Encoding.EncodeExceptionTypeId` -> `Encoding.EncodingErrorTypeId`: Decode and encode failures now share one marker. + +- `Encoding.decodeUriComponent` -> `Result.try`: Wrap decodeURIComponent in Result.try and map failure to EncodingError, or decode Schema.StringFromUriComponent. + +- `Encoding.encodeUriComponent` -> `Result.try`: Wrap encodeURIComponent in Result.try and map failure to EncodingError, or encode Schema.StringFromUriComponent. + +- `Encoding.isDecodeException` -> `Encoding.isEncodingError`: Use the unified guard and test kind === Decode when decode-only narrowing is required. + +- `Encoding.isEncodeException` -> `Encoding.isEncodingError`: Use the unified guard and test kind === Encode when encode-only narrowing is required. + +### `effect/Equal` + +- `Equal.equivalence` -> `Equal.asEquivalence`: Direct rename. The returned equivalence now follows v4 structural equality, including NaN equality and cached comparisons for immutable objects. + +### `effect/Equivalence` + +- `Equivalence.Equivalence` -> `Equivalence.Equivalence`: The callable type is retained but is now a type alias, so declaration merging is no longer supported. + +- `Equivalence.all` -> `Equivalence.Tuple([...collection])`: Materialize the comparator iterable for Tuple. Unlike v3 prefix comparison, v4 requires equal input lengths; use Equivalence.make for intentional prefix semantics. + +- `Equivalence.array` -> `Equivalence.Array`: Capitalized constructor name; positional equal-length array comparison is unchanged. + +- `Equivalence.bigint` -> `Equivalence.BigInt`: Capitalized instance name; strict bigint equality is unchanged. + +- `Equivalence.boolean` -> `Equivalence.Boolean`: Capitalized instance name; strict boolean equality is unchanged. + +- `Equivalence.combineMany` -> `Equivalence.combine(self, Equivalence.combineAll(collection))`: Compose combine with combineAll; the dedicated dual combineMany helper was removed. + +- `Equivalence.number` -> `Equivalence.Number`: Capitalized instance name. V4 considers NaN equivalent to NaN; use Equivalence.strictEqual\() for exact v3 strict-equality behavior. + +- `Equivalence.product` -> `Equivalence.Tuple([self, that])`: Replace the dual two-comparator helper with the single-array Tuple constructor. + +- `Equivalence.productMany` -> `Equivalence.Tuple([self, ...collection])`: Materialize the comparator iterable in one Tuple call; v4 rejects unequal input lengths instead of using v3 prefix semantics. + +- `Equivalence.strict` -> `Equivalence.strictEqual`: Renamed strict-equality constructor; call as Equivalence.strictEqual\(). + +- `Equivalence.string` -> `Equivalence.String`: Capitalized instance name; case-sensitive strict equality is unchanged. + +- `Equivalence.struct` -> `Equivalence.Struct`: Capitalized constructor name. V4 also compares configured symbol and non-enumerable keys via Reflect.ownKeys. + +- `Equivalence.symbol` -> `Equivalence.strictEqual()`: There is no Symbol instance export; strictEqual preserves the v3 symbol comparison. + +- `Equivalence.tuple` -> `Equivalence.Tuple([eqA, eqB, ...])`: Capitalized constructor now takes one comparator array instead of rest arguments and rejects unequal input lengths. + +### `effect/ExecutionPlan` + +- `ExecutionPlan.ExecutionPlan` -> `ExecutionPlan.ExecutionPlan`: The plan type remains; withRequirements was renamed to captureRequirements. + +- `ExecutionPlan.TypesBase` -> `ExecutionPlan.ConfigBase`: The base type for execution-plan step configuration was renamed. + +- `ExecutionPlan.make` -> `ExecutionPlan.make`: The variadic execution-plan constructor remains unchanged. + +### `effect/ExecutionStrategy` + +- `ExecutionStrategy.ExecutionStrategy` -> `Types.Concurrency | Scope.ExecutionStrategy`: The ADT was removed; use number | unbounded for operation concurrency, or sequential | parallel for Scope finalizers. + +- `ExecutionStrategy.Parallel` -> `"parallel" | "unbounded"`: The tagged case was removed; use the consumer-specific primitive value. + +- `ExecutionStrategy.ParallelN` -> `number`: The tagged case was removed; bounded operation concurrency is represented directly by a number. + +- `ExecutionStrategy.Sequential` -> `"sequential" | 1`: The tagged case was removed; use the consumer-specific primitive value. + +- `ExecutionStrategy.isParallel` -> `strategy === "parallel"`: Compare the Scope strategy directly; for concurrency options compare with unbounded. + +- `ExecutionStrategy.isParallelN` -> `typeof concurrency === "number"`: Bounded parallelism is represented directly by a numeric concurrency value. + +- `ExecutionStrategy.isSequential` -> `strategy === "sequential"`: Compare the Scope strategy directly; for operation concurrency use the value 1. + +- `ExecutionStrategy.match` -> `switch`: Use ordinary branching over the consumer-specific concurrency or Scope strategy primitive. + +- `ExecutionStrategy.parallel` -> `"parallel" | "unbounded"`: Use parallel for Scope finalizers or unbounded for operation concurrency. + +- `ExecutionStrategy.parallelN` -> `number`: Pass the parallelism directly as a numeric concurrency option; Scope has no bounded parallel strategy. + +- `ExecutionStrategy.sequential` -> `"sequential" | 1`: Use sequential for Scope finalizers or 1 for operation concurrency. + +### `effect/Exit` + +- `Exit.Exit` -> `Exit.Exit`: Still exported as Exit.Exit\ = Exit.Success\ | Exit.Failure\; v4 variants share Exit.Exit.Proto and remain Effect values. + +- `Exit.ExitUnify` -> `none`: Removed type-level implementation hook; delete direct references. V4 Success and Failure inherit Exit.Exit.Proto, but no exported Exit-specific Unify interface replaces this API. + +- `Exit.ExitUnifyIgnore` -> `none`: Removed type-level implementation hook; delete direct references. V4 Success and Failure inherit Exit.Exit.Proto, but no exported Exit-specific Unify interface replaces this API. + +- `Exit.Failure` -> `Exit.Failure`: Still exported with \_tag Failure and cause; it now extends Exit.Exit.Proto and no longer exposes the v3 \_op, effect\_instruction\_i0, or Exit-specific Unify fields. + +- `Exit.Success` -> `Exit.Success`: Still exported with \_tag Success and value; it now extends Exit.Exit.Proto, defaults E to never, and no longer exposes the v3 \_op, effect\_instruction\_i0, or Exit-specific Unify fields. + +- `Exit.all` -> `Exit.asVoidAll + Exit.isSuccess + Option.some / Option.none`: No direct value-collecting v4 equivalent. Materialize the iterable once; return Option.none for empty input, use Exit.asVoidAll to combine every failure, and otherwise collect each Success.value into Exit.succeed and wrap it in Option.some. The parallel option is gone because v4 Cause flattens sequential and parallel composition. + +- `Exit.as` -> `Exit.map`: Replace with Exit.map(self, () =\> value); this preserves a failed Exit and returns Exit data rather than a general Effect. + +- `Exit.causeOption` -> `Exit.getCause`: Direct rename; still returns Option.some(cause) for Failure and Option.none for Success. + +- `Exit.exists` -> `Exit.isSuccess`: No direct v4 combinator; use Exit.isSuccess(self) && predicate(self.value). If callers rely on the refinement overload, retain an explicitly typed wrapper returning self is Exit.Exit\. + +- `Exit.flatMapEffect` -> `Effect.matchCauseEffectEager`: Use Effect.matchCauseEffectEager(self, { onFailure: cause =\> Effect.succeed(Exit.failCause(cause)), onSuccess: f }). The explicit failure branch is required because v3 preserved an input Failure as a successful outer Effect; plain Effect.flatMap would instead fail the outer Effect. + +- `Exit.flatten` -> `Exit.match`: No direct v4 Exit flatten; use Exit.match(self, { onFailure: Exit.failCause, onSuccess: identity }) to return the inner Exit on success and preserve an outer failure as Exit data. + +- `Exit.forEachEffect` -> `Effect.flatMapEager + Effect.exit`: Use Effect.exit(Effect.flatMapEager(self, f)). This captures both the original Exit failure and failures from f into the returned Exit while keeping the outer Effect infallible; flatMapEager preserves v3's eager callback selection for an already-resolved Exit. + +- `Exit.fromEither` -> `Result.match + Exit.fail / Exit.succeed`: V3 Either is v4 Result. Convert with Result.match(result, { onFailure: Exit.fail, onSuccess: Exit.succeed }); there is no v4 Exit.fromResult constructor. + +- `Exit.fromOption` -> `Option.match + Exit.fail / Exit.succeed`: Use Option.match(option, { onNone: () =\> Exit.fail(undefined), onSome: Exit.succeed }) to preserve v3's Exit\ contract. Exit.findErrorOption is an accessor and is not a replacement. + +- `Exit.getOrElse` -> `Exit.match`: Use Exit.match(self, { onFailure: orElse, onSuccess: identity }); onFailure still receives the full Cause. + +- `Exit.isInterrupted` -> `Exit.hasInterrupts`: Direct semantic rename; true for a Failure whose Cause contains at least one Interrupt reason, false for Success. + +- `Exit.mapErrorCause` -> `Exit.match + Exit.failCause / Exit.succeed`: No direct v4 combinator. Use Exit.match(self, { onFailure: cause =\> Exit.failCause(f(cause)), onSuccess: Exit.succeed }); f now receives the flattened v4 Cause representation. Cause.map is only equivalent when f merely maps typed errors. + +- `Exit.matchEffect` -> `Effect.matchCauseEffectEager`: Direct cause-aware migration because Exit is an Effect in v4. Use the same onFailure/onSuccess handlers; the Eager variant preserves v3's immediate branch selection for resolved Exit values. + +- `Exit.zipLeft` -> `Exit.asVoidAll`: Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : self. This retains the left success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition. + +- `Exit.zipPar` -> `Exit.asVoidAll + Exit.succeed`: No direct v4 Exit pair combinator. Check Exit.asVoidAll([self, that]); return its Failure, or after narrowing both inputs to Success return Exit.succeed([self.value, that.value]). V4 Cause.combine has no parallel marker. + +- `Exit.zipParLeft` -> `Exit.asVoidAll`: Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : self. This retains the left success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition. + +- `Exit.zipParRight` -> `Exit.asVoidAll`: Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : that. This retains the right success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition. + +- `Exit.zipRight` -> `Exit.asVoidAll`: Use const checked = Exit.asVoidAll([self, that]); return Exit.isFailure(checked) ? checked : that. This retains the right success and combines dual failures; v4 Cause no longer distinguishes sequential from parallel composition. + +- `Exit.zipWith` -> `Exit.match`: No direct v4 equivalent. Nested-match both Exits: preserve a lone failure cause, call options.onFailure and Exit.failCause only when both fail, and call Exit.succeed(options.onSuccess(a, b)) when both succeed. + +### `effect/FastCheck` + +- `FastCheck.BigUintConstraints` -> `FastCheck.BigIntConstraints`: Import FastCheck from effect/testing. Unsigned bigint constraints were consolidated into BigIntConstraints with min: 0n. + +- `FastCheck.UnicodeJsonSharedConstraints` -> `FastCheck.JsonSharedConstraints`: Import FastCheck from effect/testing. Unicode JSON generation was consolidated into JsonSharedConstraints.stringUnit. + +#### `FastCheck.ascii` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. fast-check v4 replaced character arbitraries with string units. + +**Example** + +```ts +FastCheck.string({ unit: "binary-ascii", minLength: 1, maxLength: 1 }) +``` + +#### `FastCheck.asciiString` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. Use the binary-ascii string unit. + +**Example** + +```ts +FastCheck.string({ ...constraints, unit: "binary-ascii" }) +``` + +#### `FastCheck.base64` + +**Replacement:** `FastCheck.constantFrom` + +Import FastCheck from effect/testing. Generate one base64 alphabet character; base64String remains for complete encoded strings. + +**Example** + +```ts +FastCheck.constantFrom(..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/") +``` + +- `FastCheck.bigIntN` -> `FastCheck.bigInt`: Import FastCheck from effect/testing. Express the signed bit range with min and max constraints. + +#### `FastCheck.bigUint` + +**Replacement:** `FastCheck.bigInt` + +Import FastCheck from effect/testing. Use a minimum of 0n and the previous maximum. + +**Example** + +```ts +FastCheck.bigInt({ min: 0n, max }) +``` + +#### `FastCheck.bigUintN` + +**Replacement:** `FastCheck.bigInt` + +Import FastCheck from effect/testing. Express the unsigned bit range with min and max constraints. + +**Example** + +```ts +FastCheck.bigInt({ min: 0n, max: (1n << BigInt(n)) - 1n }) +``` + +#### `FastCheck.char` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. Use a one-unit printable ASCII string. + +**Example** + +```ts +FastCheck.string({ unit: "grapheme-ascii", minLength: 1, maxLength: 1 }) +``` + +#### `FastCheck.char16bits` + +**Replacement:** `FastCheck.nat` + +Import FastCheck from effect/testing. Map a 16-bit natural number through String.fromCharCode. + +**Example** + +```ts +FastCheck.nat({ max: 0xffff }).map(String.fromCharCode) +``` + +- `FastCheck.constant` -> `FastCheck.constant`: Import FastCheck from effect/testing. The API remains; v4 infers literal types by default. + +- `FastCheck.context` -> `FastCheck.context`: Import FastCheck from effect/testing. The API is otherwise unchanged. + +#### `FastCheck.fullUnicode` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. Use a one-unit binary Unicode string. + +**Example** + +```ts +FastCheck.string({ unit: "binary", minLength: 1, maxLength: 1 }) +``` + +#### `FastCheck.fullUnicodeString` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. Use the binary string unit. + +**Example** + +```ts +FastCheck.string({ ...constraints, unit: "binary" }) +``` + +- `FastCheck.hexa` -> `FastCheck.integer`: Import FastCheck from effect/testing. Map an integer from 0 through 15 to a hexadecimal character. + +- `FastCheck.hexaString` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a hexadecimal-character arbitrary as the string unit. + +- `FastCheck.stream` -> `FastCheck.stream`: Import FastCheck from effect/testing. The API remains; update custom generator and Random implementations for fast-check v4 typings. + +- `FastCheck.string16bits` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a char16bits-compatible arbitrary as the string unit. + +#### `FastCheck.stringOf` + +**Replacement:** `FastCheck.string` + +Import FastCheck from effect/testing. Pass the former character arbitrary as the unit constraint. + +**Example** + +```ts +FastCheck.string({ ...constraints, unit: arbitrary }) +``` + +- `FastCheck.unicode` -> `FastCheck.integer`: Import FastCheck from effect/testing. Map BMP code points while excluding surrogate code points; prefer the binary string unit for full Unicode. + +#### `FastCheck.unicodeJson` + +**Replacement:** `FastCheck.json` + +Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit. + +**Example** + +```ts +FastCheck.json({ stringUnit: "binary" }) +``` + +#### `FastCheck.unicodeJsonValue` + +**Replacement:** `FastCheck.jsonValue` + +Import FastCheck from effect/testing. Select binary or grapheme strings with stringUnit. + +**Example** + +```ts +FastCheck.jsonValue({ stringUnit: "binary" }) +``` + +- `FastCheck.unicodeString` -> `FastCheck.string`: Import FastCheck from effect/testing. Pass a BMP-code-point arbitrary as the unit constraint; prefer unit: binary for full Unicode. + +#### `FastCheck.uuidV` + +**Replacement:** `FastCheck.uuid` + +Import FastCheck from effect/testing. Specify the UUID version through constraints. + +**Example** + +```ts +FastCheck.uuid({ version: 4 }) +``` + +### `effect/Fiber` + +- `Fiber.Fiber` -> `Fiber.Fiber`: The v4 Fiber is the concrete runtime handle and is no longer itself an Effect; use Fiber.join or Fiber.await. + +- `Fiber.Fiber.Descriptor` -> `none`: Descriptors were removed; use Effect.withFiber for the current Fiber and read its id or runtime fields directly. + +- `Fiber.Fiber.Dump` -> `none`: Fiber dumps were removed; retain explicit Fiber handles and inspect their public runtime fields. + +- `Fiber.Fiber.Runtime` -> `Fiber.Fiber`: RuntimeFiber and the Fiber.Runtime alias were collapsed into the single v4 Fiber type. + +- `Fiber.Fiber.RuntimeVariance` -> `Fiber.Variance`: RuntimeFiber was collapsed into Fiber, leaving one variance encoding. + +- `Fiber.Fiber.Variance` -> `Fiber.Variance`: Retained as the variance encoding on the v4 Fiber interface. + +- `Fiber.FiberTypeId` -> `Fiber.isFiber`: The type-id symbol is private in v4; use the public Fiber.isFiber guard. + +- `Fiber.FiberUnify` -> `none`: Fiber no longer extends Effect, so its Effect unification helper was removed. + +- `Fiber.FiberUnifyIgnore` -> `none`: Fiber no longer extends Effect, so its Effect unification helper was removed. + +- `Fiber.Order` -> `Order.mapInput(Order.Number, (fiber) => fiber.id)`: The built-in Fiber order was removed; derive an order from the numeric id when ordering is actually required. + +- `Fiber.RuntimeFiber` -> `Fiber.Fiber`: RuntimeFiber and Fiber were collapsed into the single v4 Fiber interface. + +- `Fiber.RuntimeFiberTypeId` -> `Fiber.isFiber`: The separate RuntimeFiber marker was removed; use the public Fiber guard. + +- `Fiber.RuntimeFiberUnify` -> `none`: RuntimeFiber was collapsed into Fiber, which no longer participates in Effect unification. + +- `Fiber.RuntimeFiberUnifyIgnore` -> `none`: RuntimeFiber was collapsed into Fiber, which no longer participates in Effect unification. + +- `Fiber.all` -> `Fiber.joinAll`: Composite fibers were removed; join the iterable directly to obtain an Effect of all results. + +- `Fiber.await` -> `Fiber.await`: Unchanged; it returns an Effect containing the fiber Exit. + +- `Fiber.children` -> `none`: V4 fibers do not expose child-fiber enumeration; keep explicit handles in FiberSet or FiberMap when tracking is required. + +- `Fiber.done` -> `Effect.runFork`: Exit is an Effect in v4, so pass the Exit to Effect.runFork when a completed Fiber handle is required. + +- `Fiber.dumpAll` -> `none`: Fiber dump and global diagnostic APIs were removed; retain explicit fibers and inspect id and pollUnsafe where needed. + +- `Fiber.fail` -> `Effect.runFork(Effect.fail(error))`: Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required. + +- `Fiber.failCause` -> `Effect.runFork(Effect.failCause(cause))`: Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required. + +- `Fiber.fromEffect` -> `Effect.runFork`: V4 uses concrete runtime fibers; run the Effect directly, or keep using the Effect when no handle is needed. + +- `Fiber.getCurrentFiber` -> `Fiber.getCurrent`: Renamed and now returns Fiber | undefined synchronously instead of Option. + +- `Fiber.id` -> `fiber.id`: Fiber IDs are numbers exposed by the readonly id field. + +- `Fiber.inheritAll` -> `none`: FiberRef inheritance was removed with FiberRef; Context.Reference values are inherited through fiber context automatically. + +- `Fiber.interruptAsFork` -> `fiber.interruptUnsafe(fiberId)`: For fire-and-forget interruption use the immediate runtime hook; use Fiber.interruptAs when cleanup must be awaited. + +- `Fiber.interruptFork` -> `fiber.interruptUnsafe()`: Use the immediate runtime hook for fire-and-forget interruption; Fiber.interrupt waits for cleanup. + +- `Fiber.interrupted` -> `Effect.runFork(Exit.interrupt(fiberId))`: Synthetic Fiber constructors were removed; Exit is an Effect and can be run to obtain an interrupted Fiber. + +- `Fiber.isRuntimeFiber` -> `Fiber.isFiber`: All v4 Fiber values are concrete runtime fibers, so only the general guard remains. + +- `Fiber.map` -> `Effect.runFork(Effect.map(Fiber.join(fiber), f))`: Fiber transformation combinators were removed; transform its joined Effect and fork only if another handle is required. + +- `Fiber.mapEffect` -> `Effect.runFork(Effect.flatMap(Fiber.join(fiber), f))`: Fiber transformation combinators were removed; transform its joined Effect and fork only if another handle is required. + +- `Fiber.mapFiber` -> `Effect.flatMap(Fiber.join(fiber), (a) => Fiber.join(f(a)))`: Flatten through Fiber.join; fork the resulting Effect if another Fiber handle is required. + +- `Fiber.match` -> `none`: The virtual Fiber versus RuntimeFiber distinction no longer exists, so branch-specific matching is unnecessary. + +- `Fiber.never` -> `Effect.runFork(Effect.never)`: Synthetic Fiber constants were removed; run Effect.never when a never-completing Fiber is required. + +- `Fiber.orElse` -> `Effect.runFork(Effect.catchCause(Fiber.join(self), () => Fiber.join(that)))`: Compose joined Effects and fork the result only if another Fiber handle is required. + +- `Fiber.orElseEither` -> `Effect.catchCause`: Compose Fiber.join Effects explicitly and map each successful branch to your own tagged union; Either was also removed in v4. + +- `Fiber.poll` -> `fiber.pollUnsafe()`: Polling is now synchronous and returns Exit | undefined; wrap in Effect.sync and Option.fromUndefinedOr if the old shape is required. + +- `Fiber.pretty` -> `none`: Runtime fiber pretty-printing was removed; format the public id and polled Exit explicitly. + +- `Fiber.roots` -> `none`: The runtime no longer exposes a global root-fiber registry; track application fibers explicitly. + +- `Fiber.scoped` -> `Fiber.runIn`: Register the Fiber in an explicit Scope with Fiber.runIn; acquire the current Scope when migrating the old effectful form. + +- `Fiber.status` -> `fiber.pollUnsafe()`: FiberStatus was removed; undefined means not completed and an Exit means completed, with no public running/suspended distinction. + +- `Fiber.succeed` -> `Effect.runFork(Effect.succeed(value))`: Synthetic Fiber constructors were removed; run the corresponding Effect when a Fiber handle is required. + +- `Fiber.unsafeRoots` -> `none`: The runtime no longer exposes a global root-fiber registry; track application fibers explicitly. + +- `Fiber.void` -> `Effect.runFork(Effect.void)`: Synthetic Fiber constants were removed; run Effect.void when a completed Fiber\ is required. + +- `Fiber.zip` -> `Effect.runFork(Effect.zip(Fiber.join(self), Fiber.join(that)))`: Compose joined Effects and fork the result only if another Fiber handle is required. + +- `Fiber.zipLeft` -> `Effect.runFork(Effect.map(Effect.zip(Fiber.join(self), Fiber.join(that)), ([left]) => left))`: V4 has no Effect.zipLeft; zip joined Effects, project the left value, and fork only if another handle is required. + +- `Fiber.zipRight` -> `Effect.runFork(Effect.map(Effect.zip(Fiber.join(self), Fiber.join(that)), ([, right]) => right))`: V4 has no Effect.zipRight; zip joined Effects, project the right value, and fork only if another handle is required. + +- `Fiber.zipWith` -> `Effect.runFork(Effect.zipWith(Fiber.join(self), Fiber.join(that), f))`: Compose joined Effects and fork the result only if another Fiber handle is required. + +### `effect/FiberHandle` + +- `FiberHandle.FiberHandle` -> `FiberHandle.FiberHandle`: Retained; contained runtime fibers now use the unified Fiber type. + +- `FiberHandle.TypeId` -> `FiberHandle.isFiberHandle`: The type-id symbol is private in v4; use the public guard. + +- `FiberHandle.get` -> `FiberHandle.get`: Retained, but v4 returns Effect\\> instead of failing with NoSuchElementException when empty. + +- `FiberHandle.unsafeGet` -> `FiberHandle.getUnsafe`: Renamed to put the Unsafe suffix last. + +- `FiberHandle.unsafeSet` -> `FiberHandle.setUnsafe`: Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details. + +### `effect/FiberId` + +- `FiberId.Composite` -> `none`: Composite FiberId values were removed; v4 uses a single numeric fiber id. + +- `FiberId.FiberId` -> `number`: V4 represents a fiber identity as the numeric Fiber.id field. + +- `FiberId.FiberIdTypeId` -> `none`: Fiber IDs are primitive numbers in v4 and have no type-id symbol. + +- `FiberId.None` -> `undefined`: Optional interruptor IDs use undefined rather than a sentinel FiberId type. + +- `FiberId.Runtime` -> `number`: Runtime fiber IDs are primitive numbers in v4. + +- `FiberId.Single` -> `number | undefined`: Use a number, with undefined only where the old None case was meaningful. + +- `FiberId.combine` -> `none`: Composite FiberId values were removed; v4 uses a single numeric fiber id. + +- `FiberId.combineAll` -> `none`: Composite FiberId values were removed; v4 uses a single numeric fiber id. + +- `FiberId.composite` -> `none`: Composite FiberId values were removed; v4 uses a single numeric fiber id. + +- `FiberId.getOrElse` -> `fiberId ?? fallback`: Represent absence as undefined when migrating code that previously used FiberId.none. + +- `FiberId.ids` -> `new Set([fiberId])`: A v4 fiber has one numeric id; composite-id flattening is no longer required. + +- `FiberId.isComposite` -> `none`: Composite FiberId values do not exist in v4. + +- `FiberId.isFiberId` -> `Number.isNumber`: Fiber IDs are primitive numbers in v4. + +- `FiberId.isNone` -> `fiberId === undefined`: Use undefined for an absent optional interruptor id; there is no sentinel FiberId.none. + +- `FiberId.isRuntime` -> `Number.isNumber`: Every v4 fiber id is a runtime numeric id. + +- `FiberId.make` -> `id`: Use the numeric id directly; startTimeSeconds is no longer part of fiber identity. + +- `FiberId.none` -> `undefined`: Optional interruptor IDs use undefined rather than a sentinel FiberId value. + +- `FiberId.runtime` -> `id`: Use the numeric id directly; startTimeMillis is no longer part of fiber identity. + +- `FiberId.threadName` -> `String(fiberId)`: There is no built-in thread-name formatter; format the numeric id at the presentation boundary. + +- `FiberId.toSet` -> `new Set([fiberId])`: A v4 fiber has one numeric id, so composite-id flattening is unnecessary. + +- `FiberId.unsafeMake` -> `none`: There is no public fiber-id allocator; obtain the current id with Effect.fiberId or from Fiber.id. + +### `effect/FiberMap` + +- `FiberMap.FiberMap` -> `FiberMap.FiberMap`: Retained; contained runtime fibers now use the unified Fiber type. + +- `FiberMap.TypeId` -> `FiberMap.isFiberMap`: The type-id symbol is private in v4; use the public guard. + +- `FiberMap.unsafeGet` -> `FiberMap.getUnsafe`: Renamed to put the Unsafe suffix last. + +- `FiberMap.unsafeHas` -> `FiberMap.hasUnsafe`: Renamed to put the Unsafe suffix last. + +- `FiberMap.unsafeSet` -> `FiberMap.setUnsafe`: Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details. + +### `effect/FiberRef` + +- `FiberRef.FiberRef` -> `Context.Reference`: Fiber-local values and services share Context.Reference in v4; references have a defaultValue and no fork/join patching. + +- `FiberRef.FiberRefTypeId` -> `Context.isReference`: Use the public Context.Reference guard instead of a FiberRef type-id symbol. + +- `FiberRef.FiberRefUnify` -> `none`: Context.Reference is a service key and does not require the old FiberRef Effect-unification helper. + +- `FiberRef.FiberRefUnifyIgnore` -> `none`: Context.Reference is a service key and does not require the old FiberRef Effect-unification helper. + +- `FiberRef.Variance` -> `Context.Reference`: The FiberRef-specific variance interface was removed with FiberRef. + +- `FiberRef.currentConcurrency` -> `none`: Inherited concurrency was removed; pass concurrency explicitly to each v4 combinator that supports it. + +- `FiberRef.currentContext` -> `Effect.context`: Fiber services are stored directly in Context; use Effect.context to read them and Effect.provideContext to override them. + +- `FiberRef.currentLogAnnotations` -> `References.CurrentLogAnnotations`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentLogLevel` -> `References.CurrentLogLevel`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentLogSpan` -> `References.CurrentLogSpans`: Renamed and represented as a Context.Reference containing a readonly span array. + +- `FiberRef.currentLoggers` -> `References.CurrentLoggers`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentMaxOpsBeforeYield` -> `Scheduler.MaxOpsBeforeYield`: The scheduler setting is now a Context.Reference; yield it or provide it with Effect.provideService. + +- `FiberRef.currentMetricLabels` -> `Metric.CurrentMetricAttributes`: Metric labels became metric attributes stored in a Context.Reference. + +- `FiberRef.currentMinimumLogLevel` -> `References.MinimumLogLevel`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentRequestBatchingEnabled` -> `none`: The request batching FiberRef was removed; batching is defined by the v4 RequestResolver runAll implementation. + +- `FiberRef.currentRequestCache` -> `RequestResolver.withCache`: The ambient request cache was removed; wrap a RequestResolver with an explicit bounded cache. + +- `FiberRef.currentRequestCacheEnabled` -> `RequestResolver.withCache`: There is no ambient cache toggle; choose an explicitly cached or uncached RequestResolver. + +- `FiberRef.currentRuntimeFlags` -> `none`: RuntimeFlags and their FiberRef were removed; use specific v4 runtime options such as interruptibility and scheduler settings. + +- `FiberRef.currentScheduler` -> `Scheduler.Scheduler`: The scheduler is now a Context.Reference; yield it or provide it with Effect.provideService. + +- `FiberRef.currentSchedulingPriority` -> `none`: The ambient scheduling-priority FiberRef was removed; use explicit scheduler operations where priority is needed. + +- `FiberRef.currentSupervisor` -> `none`: The Supervisor and ambient supervisor FiberRef APIs were removed; track fibers explicitly with FiberSet or FiberMap. + +- `FiberRef.currentTracerEnabled` -> `References.TracerEnabled`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentTracerSpanAnnotations` -> `References.TracerSpanAnnotations`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentTracerSpanLinks` -> `References.TracerSpanLinks`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.currentTracerTimingEnabled` -> `References.TracerTimingEnabled`: Built-in FiberRefs are now Context.Reference values; yield the reference or provide it with Effect.provideService. + +- `FiberRef.delete` -> `Effect.provideService`: Context.Reference has no in-place delete; scope the default or desired value around the target Effect. + +- `FiberRef.get` -> `reference`: Context.Reference is yieldable as a service; yield it directly to read the current value. + +- `FiberRef.getAndUpdateSome` -> `Ref.getAndUpdateSome`: Use Ref for mutable state; for fiber-local configuration compute the value first and scope it with Effect.provideService. + +- `FiberRef.getWith` -> `Effect.flatMap(reference, f)`: Yield or flatMap the Context.Reference directly. + +- `FiberRef.interruptedCause` -> `none`: The pending interruption cause is no longer exposed as public fiber-local state; inspect completed failure Causes from Fiber.await. + +- `FiberRef.make` -> `Context.Reference`: Define a stable Context.Reference key with defaultValue; custom fork and join behavior is not supported. + +- `FiberRef.makeContext` -> `Context.Reference`: Define a Context.Reference whose defaultValue returns the Context; custom context diffing is no longer required. + +- `FiberRef.makeRuntimeFlags` -> `none`: RuntimeFlags and specialized FiberRef constructors were removed; migrate each flag to its explicit v4 runtime option. + +- `FiberRef.makeWith` -> `Context.Reference`: Use the lazy defaultValue option on a stable Context.Reference key. + +- `FiberRef.modify` -> `Ref.modify`: Use Ref for mutable state; Context.Reference updates are scoped with Effect.provideService rather than mutated in place. + +- `FiberRef.modifySome` -> `Ref.modifySome`: Use Ref for mutable state; Context.Reference updates are scoped with Effect.provideService rather than mutated in place. + +- `FiberRef.reset` -> `Effect.provideService`: Context.Reference has no in-place reset; scope its default value around the target Effect. + +- `FiberRef.set` -> `Effect.provideService`: Context.Reference values are overridden for an Effect scope instead of mutating the current fiber. + +- `FiberRef.unhandledErrorLogLevel` -> `References.UnhandledLogLevel`: Renamed and represented as a Context.Reference using Severity | undefined instead of Option\. + +- `FiberRef.unsafeMake` -> `Context.Reference`: Context.Reference construction is synchronous; provide a stable identifier and defaultValue. + +- `FiberRef.unsafeMakeContext` -> `Context.Reference`: Define a Context.Reference whose defaultValue returns the Context; there is no specialized unsafe constructor. + +- `FiberRef.unsafeMakeHashSet` -> `Context.Reference`: Define a normal Context.Reference with a readonly set default; specialized differ constructors were removed. + +- `FiberRef.unsafeMakePatch` -> `Context.Reference`: Define a normal Context.Reference; custom Differ, fork patches, and join behavior are not supported in v4. + +- `FiberRef.unsafeMakeSupervisor` -> `none`: Supervisor and FiberRef were removed; track managed fibers explicitly with FiberSet or FiberMap. + +- `FiberRef.update` -> `Ref.update`: Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService. + +- `FiberRef.updateSome` -> `Ref.updateSome`: Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService. + +- `FiberRef.updateSomeAndGet` -> `Ref.updateSomeAndGet`: Use Ref for mutable state; for fiber-local configuration compute a value and scope it with Effect.provideService. + +- `FiberRef.versionMismatchErrorLogLevel` -> `none`: The version-mismatch logging FiberRef was removed and no public v4 Context.Reference replaces it. + +### `effect/FiberRefs` + +- `FiberRefs.FiberRefs` -> `Context.Context`: Fiber-local services and reference overrides are stored directly in the Fiber context in v4. + +- `FiberRefs.FiberRefsSym` -> `none`: FiberRefs and its marker symbol were removed. + +- `FiberRefs.delete` -> `Context.omit`: FiberRefs became fiber Context; omit a Reference key when constructing the replacement Context. + +- `FiberRefs.empty` -> `Context.empty`: Use an empty Context as the starting collection of services and reference overrides. + +- `FiberRefs.fiberRefs` -> `none`: Context does not expose public enumeration of its Reference keys; retain the keys explicitly if enumeration is required. + +- `FiberRefs.forkAs` -> `none`: Context is inherited automatically when a v4 child fiber is forked; custom per-reference fork patches were removed. + +- `FiberRefs.get` -> `Context.getOption`: Read the service as an Option. Context.Reference defaults also produce Some; use Context.getOrUndefined when only stored overrides should count. + +- `FiberRefs.getOrDefault` -> `Context.get`: Reads an override or the Context.Reference default value. + +- `FiberRefs.joinAs` -> `none`: Child-to-parent FiberRef joining was removed; pass results explicitly or merge ordinary Context values where appropriate. + +- `FiberRefs.setAll` -> `Effect.provideContext`: Provide the replacement Context around the Effect that should observe its services and reference overrides. + +- `FiberRefs.unsafeMake` -> `Context.empty().pipe(Context.add(...))`: Build a Context from explicit Reference keys and values; FiberId histories and unsafe local maps no longer exist. + +- `FiberRefs.updateAs` -> `Context.add`: Add or replace a Reference value in Context; the FiberId parameter and history are removed. + +- `FiberRefs.updateManyAs` -> `Context.add`: Apply explicit Context.add calls for each Reference value; FiberId histories and forkAs are removed. + +### `effect/FiberRefsPatch` + +- `FiberRefsPatch.Add` -> `Context.add`: FiberRefsPatch was removed; apply Reference overrides directly to Context. + +- `FiberRefsPatch.AndThen` -> `Context.merge`: FiberRefsPatch was removed; compose Context updates directly, with later values overriding earlier ones. + +- `FiberRefsPatch.Empty` -> `Context.Context`: The empty patch model was removed; an empty Context represents no overrides. + +- `FiberRefsPatch.FiberRefsPatch` -> `none`: The patch data type was removed with FiberRefs; construct or merge Context values directly. + +- `FiberRefsPatch.combine` -> `Context.merge`: FiberRefsPatch was removed; merge the resulting Context values instead of combining patches. + +- `FiberRefsPatch.diff` -> `none`: There is no generic Context diff because FiberRef fork and join patch semantics were removed. + +- `FiberRefsPatch.empty` -> `Context.empty`: Use an empty Context when no services or Reference overrides are applied. + +- `FiberRefsPatch.patch` -> `Context.merge`: Merge explicit Context overrides into the base Context; FiberId-aware patch application no longer exists. + +### `effect/FiberSet` + +- `FiberSet.FiberSet` -> `FiberSet.FiberSet`: Retained; contained runtime fibers now use the unified Fiber type. + +- `FiberSet.TypeId` -> `FiberSet.isFiberSet`: The type-id symbol is private in v4; use the public guard. + +- `FiberSet.unsafeAdd` -> `FiberSet.addUnsafe`: Renamed to put the Unsafe suffix last; the interruptAs option was removed because IDs are now numeric runtime details. + +### `effect/FiberStatus` + +- `FiberStatus.Done` -> `Exit.Exit`: FiberStatus was removed; a defined fiber.pollUnsafe() result indicates completion and contains the Exit. + +- `FiberStatus.FiberStatus` -> `Exit.Exit | undefined`: Use fiber.pollUnsafe(); undefined means incomplete and Exit means completed, with no running/suspended distinction. + +- `FiberStatus.FiberStatusTypeId` -> `none`: FiberStatus and its type-id symbol were removed. + +- `FiberStatus.Running` -> `none`: The public runtime no longer models running status as a value. + +- `FiberStatus.Suspended` -> `none`: The public runtime no longer models suspended status as a value. + +- `FiberStatus.isDone` -> `fiber.pollUnsafe() !== undefined`: Completion is observable by synchronously polling the Fiber. + +- `FiberStatus.isFiberStatus` -> `none`: FiberStatus values no longer exist; inspect a Fiber with pollUnsafe instead. + +- `FiberStatus.isRunning` -> `fiber.pollUnsafe() === undefined`: V4 only exposes incomplete versus completed; it does not distinguish running from suspended. + +- `FiberStatus.isSuspended` -> `none`: The public runtime no longer exposes suspended status. + +- `FiberStatus.running` -> `none`: FiberStatus constructors were removed; keep the Fiber and poll it instead. + +- `FiberStatus.suspended` -> `none`: FiberStatus constructors and public suspended status were removed. + +### `effect/Function` + +- `Function.FunctionN` -> `Function.FunctionN`: No call-site migration; v4 keeps the same function shape as a type alias. + +- `Function.LazyArg` -> `Function.LazyArg`: No call-site migration; v4 keeps the same lazy function shape as a type alias. + +- `Function.isFunction` -> `Predicate.isFunction`: The function refinement moved to Predicate. + +- `Function.unsafeCoerce` -> `Function.cast`: Renamed type-only cast; runtime behavior remains identity with no validation. + +### `effect/GlobalValue` + +- `GlobalValue.globalValue` -> `module-scoped const`: The global registry helper was removed; use a module singleton, or explicitly own a globalThis and Symbol.for registry when cross-bundle identity is required. + +### `effect/Graph` + +- `Graph.Graph` -> `Graph.Graph`: The immutable type remains, but storage is opaque; replace field access with Graph nodes, edges, count, lookup, neighbor, and acyclicity APIs. + +- `Graph.MutableGraph` -> `Graph.MutableGraph`: The mutable type remains but no longer extends Graph.Proto; obtain it through Graph.mutate or Graph.beginMutation and use public mutation/query functions. + +- `Graph.Proto` -> `Graph.Proto`: The name remains as the opaque immutable graph protocol; it no longer exposes storage and is no longer the base of MutableGraph. + +- `Graph.SearchConfig` -> `Graph.SearchConfig`: The type remains; direction is now Graph.TraversalDirection and also accepts undirected, while radius limits traversal depth. + +### `effect/GroupBy` + +- `GroupBy.GroupBy` -> `Stream]>`: The GroupBy datatype is removed in v4; Stream.groupBy/groupByKey now return an ordinary Stream of readonly [key, substream] pairs, processed with regular Stream operators. + +- `GroupBy.GroupBy.Variance` -> `none`: Variance plumbing for the removed GroupBy datatype; v4 has no GroupBy type, so there is no variance interface to migrate to. + +- `GroupBy.GroupByTypeId` -> `none`: Brand symbol for the removed GroupBy datatype; v4 groupBy results are plain Streams, discriminated with Stream.isStream if needed. + +#### `GroupBy.evaluate` + +**Replacement:** `Stream.flatMap` + +Apply the per-group function over the [key, stream] pairs with Stream.flatMap (or Stream.mapEffect for an effectful result per group), using { concurrency: "unbounded" } to reproduce v3's parallel-groups/arbitrary-merge-order behavior; the v3 bufferSize option moved onto Stream.groupBy itself. + +**Example** + +```ts +// v3: stream.pipe(Stream.groupByKey(f), GroupBy.evaluate((key, s) => g(key, s))) +stream.pipe( + Stream.groupByKey(f), + Stream.flatMap(([key, s]) => g(key, s), { concurrency: "unbounded" }) +) + +``` + +- `GroupBy.filter` -> `Stream.filter`: Filter the groups by key with an ordinary Stream.filter on the pairs: Stream.filter(([key]) =\> predicate(key)). + +- `GroupBy.first` -> `Stream.take`: Keep only the first n groups with an ordinary Stream.take(n) on the [key, stream] pair stream. + +- `GroupBy.make` -> `none`: No wrapper to construct in v4: a grouped stream is just any Stream\]\>, so build the pair stream directly (Stream.groupBy/groupByKey produce it); the v3 shape Stream\<[K, Dequeue\\>]\> is gone along with the queue-of-Take encoding. + +### `effect/Hash` + +- `Hash.cached` -> `none`: Delete Hash.cached wrappers and return the computed value from Hash.symbol; Hash.hash now caches objects automatically in a private WeakMap without mutating them. + +### `effect/HashMap` + +- `HashMap.HashMap` -> `HashMap.HashMap`: The immutable two-parameter model remains; use public operations rather than depending on its representation. + +- `HashMap.TypeId` -> `HashMap.isHashMap`: The brand is private; use HashMap.isHashMap for runtime refinement and HashMap.HashMap\ in type positions. + +- `HashMap.countBy` -> `HashMap.reduce`: Count matches with HashMap.reduce(self, 0, (count, value, key) =\> count + (predicate(value, key) ? 1 : 0)). + +- `HashMap.keySet` -> `HashSet.fromIterable + HashMap.keys`: Construct the set with HashSet.fromIterable(HashMap.keys(self)); no direct keySet helper remains. + +- `HashMap.unsafeGet` -> `HashMap.getUnsafe`: Direct word-order rename; it still throws for a missing key. + +### `effect/HashSet` + +- `HashSet.HashSet` -> `HashSet.HashSet`: The immutable model remains, but the brand is private and transient mutation helpers were removed. + +- `HashSet.TypeId` -> `HashSet.isHashSet`: The brand is private; use HashSet.isHashSet for runtime refinement and HashSet.HashSet\ in type positions. + +- `HashSet.beginMutation` -> `none`: Transient mutation mode was removed; reassign immutable add/remove results or build a batch with HashSet.fromIterable. + +- `HashSet.endMutation` -> `none`: There is no mutation window to finalize; remove this call and use the latest immutable HashSet value. + +- `HashSet.flatMap` -> `HashSet.fromIterable + Iterable.flatMap`: Preserve set deduplication with HashSet.fromIterable(Iterable.flatMap(self, f)); no direct flatMap remains. + +- `HashSet.forEach` -> `Iterable.forEach`: HashSet remains Iterable, so Iterable.forEach(self, f) preserves eager side-effecting traversal. + +- `HashSet.mutate` -> `none`: Transient mutation was removed; reassign immutable HashSet.add/remove results or build a complete replacement with HashSet.fromIterable. + +- `HashSet.partition` -> `HashSet.filter`: Build [excluded, satisfying] with complementary HashSet.filter calls, or use one reduction when the predicate is expensive. + +- `HashSet.toValues` -> `Array.from`: HashSet remains iterable; Array.from(self) produces the former Array result. + +- `HashSet.toggle` -> `HashSet.has + HashSet.remove / HashSet.add`: Use HashSet.has(self, value) ? HashSet.remove(self, value) : HashSet.add(self, value). + +- `HashSet.values` -> `none`: The HashSet itself is iterable; iterate it directly or call self[Symbol.iterator]() when an iterator object is required. + +### `effect/Inspectable` + +- `Inspectable.redact` -> `Redactable.redact`: The redaction protocol moved to Redactable and now receives the current fiber Context. + +- `Inspectable.stringifyCircular` -> `Formatter.formatJson`: Use Formatter.formatJson(input, { space: whitespace }); it handles redaction and ancestor cycles. + +- `Inspectable.toJSON` -> `Inspectable.toJson`: Renamed to lower-camel toJson with the same recursive conversion role. + +- `Inspectable.withRedactableContext` -> `none`: Manual FiberRefs scoping was removed; Redactable.redact uses the current fiber Context automatically. + +### `effect/Iterable` + +- `Iterable.flatMapNullable` -> `Iterable.flatMapNullishOr`: Direct nullish-terminology rename; it remains lazy and drops null or undefined mapper results. + +- `Iterable.getLefts` -> `Iterable.getFailures`: Either became Result; this lazily extracts failure payloads. + +- `Iterable.getRights` -> `Iterable.getSuccesses`: Either became Result; this lazily extracts success payloads. + +- `Iterable.unsafeHead` -> `Iterable.headUnsafe`: Direct word-order rename; it still throws on an empty Iterable. + +### `effect/JSONSchema` + +- `JSONSchema.JsonSchema7` -> `JsonSchema.JsonSchema`: The draft-07-specific union was replaced by the dialect-neutral JSON Schema model. + +- `JSONSchema.JsonSchema7Any` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7AnyOf` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Array` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Boolean` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Enum` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Enums` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Integer` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Never` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Null` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Number` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Numeric` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Object` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Ref` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Root` -> `JsonSchema.Document<"draft-07">`: Use a typed JSON Schema document for a draft-07 root and definitions. + +- `JSONSchema.JsonSchema7String` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Unknown` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7Void` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7empty` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchema7object` -> `JsonSchema.JsonSchema`: Individual draft-07 node interfaces were consolidated into JsonSchema.JsonSchema. + +- `JSONSchema.JsonSchemaAnnotations` -> `Schema.Annotations.Documentation`: Schema metadata now uses string-keyed Schema annotations; JSON Schema-specific checks use toJsonSchema annotations. + +#### `JSONSchema.fromAST` + +**Replacement:** `Schema.toJsonSchemaDocument` + +Wrap a low-level AST with Schema.make, then generate a document; v4 generation targets draft 2020-12. + +**Example** + +```ts +Schema.toJsonSchemaDocument(Schema.make(ast)) +``` + +#### `JSONSchema.make` + +**Replacement:** `Schema.toJsonSchemaDocument` + +Generate draft 2020-12, then call JsonSchema.toDocumentDraft07 when draft-07 output is required. + +**Example** + +```ts +JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(schema)) +``` + +### `effect/KeyedPool` + +- `KeyedPool.KeyedPool` -> `RcMap.RcMap>`: Model keyed pools as an RcMap whose scoped lookup creates one Pool per key. + +- `KeyedPool.KeyedPool.Variance` -> `none`: KeyedPool and its variance marker were removed; use the RcMap and Pool public models without depending on branding internals. + +- `KeyedPool.KeyedPoolTypeId` -> `none`: KeyedPool was removed, so its runtime type id has no v4 equivalent. + +- `KeyedPool.get` -> `RcMap.get + Pool.get`: KeyedPool was removed; acquire the per-key Pool from an RcMap, then borrow an item with Pool.get in the current Scope. + +- `KeyedPool.invalidate` -> `RcMap.get + Pool.invalidate`: KeyedPool was removed; retain the key, get its Pool from RcMap, and call Pool.invalidate for the item. + +- `KeyedPool.make` -> `RcMap.make + Pool.make`: Create an RcMap with lookup key =\> Pool.make({ acquire: acquire(key), size }); RcMap.get followed by Pool.get replaces keyed borrowing. + +- `KeyedPool.makeWith` -> `RcMap.make + Pool.make`: Create an RcMap whose lookup uses Pool.make with size: size(key). + +- `KeyedPool.makeWithTTL` -> `RcMap.make + Pool.makeWithTTL`: Create an RcMap whose lookup uses Pool.makeWithTTL with min(key), max(key), and the shared timeToLive. + +- `KeyedPool.makeWithTTLBy` -> `RcMap.make + Pool.makeWithTTL`: Create an RcMap whose lookup uses Pool.makeWithTTL with min(key), max(key), and timeToLive(key). + +### `effect/Layer` + +- `Layer.CurrentMemoMap` -> `Layer.CurrentMemoMap`: The service remains but is now a Context.Service class with forkOrCreate. + +- `Layer.Layer` -> `Layer.Layer`: The type remains with Layer\ parameter order. + +- `Layer.Layer.Context` -> `Layer.Services`: The input-services extractor moved to the module level and was renamed. + +- `Layer.LayerTypeId` -> `none`: The marker is private in v4; use Layer.Any or Layer.Variance for type constraints. + +- `Layer.MemoMap` -> `Layer.MemoMap`: The interface remains and now supports parent-child ambient maps. + +- `Layer.MemoMapTypeId` -> `none`: The MemoMap marker is private in v4. + +- `Layer.annotateLogs` -> `Layer.fromBuild((memoMap, scope) => Effect.annotateLogs(Layer.buildWithMemoMap(self, memoMap, scope), ...annotations))`: Apply Effect.annotateLogs to the layer acquisition effect. + +- `Layer.annotateSpans` -> `Layer.fromBuild((memoMap, scope) => Effect.annotateSpans(Layer.buildWithMemoMap(self, memoMap, scope), ...annotations))`: Apply Effect.annotateSpans to the layer acquisition effect. + +- `Layer.catchAll` -> `Layer.catch`: The typed-error handler was renamed. + +- `Layer.catchAllCause` -> `Layer.catchCause`: The cause handler was renamed. + +- `Layer.context` -> `Layer.effectContext(Effect.context())`: Capture and return the current service context. + +- `Layer.die` -> `Layer.unwrap(Effect.die(defect))`: Lift the Effect defect constructor. + +- `Layer.dieSync` -> `Layer.unwrap(Effect.suspend(() => Effect.die(evaluate())))`: Suspend evaluation and lift Effect.die; Effect.dieSync was also removed. + +- `Layer.discard` -> `Layer.flatMap(self, () => Layer.empty)`: Build the layer while dropping its output context. + +- `Layer.ensureErrorType` -> `Layer.satisfiesErrorType`: The type constraint helper was renamed. + +- `Layer.ensureRequirementsType` -> `Layer.satisfiesServicesType`: The requirements type constraint was renamed to services. + +- `Layer.ensureSuccessType` -> `Layer.satisfiesSuccessType`: The type constraint helper was renamed. + +- `Layer.extendScope` -> `Layer.buildWithScope(self, outerScope) and Effect.provideContext(program, context)`: Explicitly build against the desired outer scope and provide the resulting context. + +- `Layer.fail` -> `Layer.unwrap(Effect.fail(error))`: Lift the Effect failure constructor. + +- `Layer.failCause` -> `Layer.unwrap(Effect.failCause(cause))`: Lift the Effect cause-failure constructor. + +- `Layer.failCauseSync` -> `Layer.unwrap(Effect.failCauseSync(evaluate))`: Lift the retained Effect constructor. + +- `Layer.failSync` -> `Layer.unwrap(Effect.failSync(evaluate))`: Lift the retained Effect constructor. + +- `Layer.fiberRefLocallyScopedWith` -> `Layer.effect(reference, Effect.map(reference, f))`: FiberRef was removed; compute and provide a transformed Context.Reference value. + +- `Layer.flatten` -> `Layer.flatMap(self, (context) => Context.get(context, key))`: Expand the removed convenience combinator with flatMap and Context.get. + +- `Layer.function` -> `Layer.effect(keyB, Effect.map(keyA, f))`: Read the input service through its Context.Key and provide the transformed service. + +- `Layer.isFresh` -> `none`: Layer.fresh remains, but its wrapper has no public freshness predicate. + +- `Layer.locally` -> `Layer.updateService(self, reference, () => value)`: Replace FiberRef-local configuration with Context.Reference provision. + +- `Layer.locallyEffect` -> `Layer.fromBuild((memoMap, scope) => f(Layer.buildWithMemoMap(self, memoMap, scope)))`: Transform the public layer acquisition effect directly. + +- `Layer.locallyScoped` -> `Layer.succeed(reference, value)`: Provide a v4 Context.Reference value as a configuration layer. + +- `Layer.locallyWith` -> `Layer.updateService(self, reference, f)`: Transform a Context.Reference during layer acquisition. + +- `Layer.map` -> `Layer.flatMap(self, (context) => Layer.succeedContext(f(context)))`: Expand the removed output-context mapping combinator. + +- `Layer.mapError` -> `Layer.fromBuild((memoMap, scope) => Effect.mapError(Layer.buildWithMemoMap(self, memoMap, scope), f))`: Transform the typed error of layer acquisition. + +- `Layer.match` -> `Layer.fromBuild with Effect.matchEffect over Layer.buildWithMemoMap`: Fold the source acquisition effect, then build the selected failure or success layer. + +- `Layer.matchCause` -> `Layer.fromBuild with Effect.matchCauseEffect over Layer.buildWithMemoMap`: Fold the source acquisition cause, then build the selected failure or success layer. + +- `Layer.memoize` -> `automatic shared memoization under Effect.provide`: Reuse the same Layer value; use { local: true } or Layer.fresh to opt out, or MemoMap APIs for manual control. + +- `Layer.orElse` -> `Layer.catch(self, () => fallback())`: Expand the removed lazy fallback alias with Layer.catch. + +- `Layer.passthrough` -> `Layer.merge(Layer.effectContext(Effect.context()), self)`: Capture required input services and merge them into the layer output. + +- `Layer.project` -> `Layer.flatMap(self, (context) => Layer.succeed(keyB, f(Context.get(context, keyA))))`: Project one derived service and drop the other outputs. + +- `Layer.retry` -> `Effect.retry(acquire, schedule) before Layer.effect or Layer.effectContext`: Retry the acquisition Effect; for an arbitrary layer, rebuild a fresh layer for each attempt through Layer.fromBuild. + +- `Layer.scope` -> `Layer.effect(Scope.Scope, Effect.acquireRelease(Scope.make(), Scope.close))`: Construct and close a child scope explicitly. + +- `Layer.scoped` -> `Layer.effect`: Scoped acquisition was merged into Layer.effect, which supplies and excludes the layer Scope. + +- `Layer.scopedContext` -> `Layer.effectContext`: Scoped context acquisition was merged into Layer.effectContext. + +- `Layer.scopedDiscard` -> `Layer.effectDiscard`: Scoped discard acquisition was merged into Layer.effectDiscard. + +- `Layer.service` -> `Layer.effect(key, key)`: A Context.Key is an Effect that reads and passes through its service. + +- `Layer.setClock` -> `Layer.succeed(Clock.Clock, clock)`: Clock.Clock is now a Context.Reference; provide it directly. + +- `Layer.setConfigProvider` -> `ConfigProvider.layer(configProvider)`: Use the dedicated ConfigProvider layer constructor. + +- `Layer.setRandom` -> `Layer.succeed(Random.Random, random)`: Random.Random is now a Context.Reference; provide it directly. + +- `Layer.setRequestBatching` -> `none`: Requests now use resolver-driven batching and expose no batching switch. + +- `Layer.setRequestCache` -> `none`: The public Request.Cache and its configuration API were removed. + +- `Layer.setRequestCaching` -> `none`: The public request-caching toggle was removed. + +- `Layer.setScheduler` -> `Layer.succeed(Scheduler.Scheduler, scheduler)`: Scheduler.Scheduler is now a Context.Reference; provide it directly. + +- `Layer.setTracer` -> `Layer.succeed(Tracer.Tracer, tracer)`: Tracer.Tracer is now a Context.Reference; provide it directly. + +- `Layer.setTracerEnabled` -> `Layer.succeed(References.TracerEnabled, enabled)`: Provide the v4 Reference instead of setting a FiberRef. + +- `Layer.setTracerTiming` -> `Layer.succeed(References.TracerTimingEnabled, enabled)`: Provide the renamed v4 Reference instead of setting a FiberRef. + +- `Layer.setUnhandledErrorLogLevel` -> `Layer.succeed(References.UnhandledLogLevel, severityOrUndefined)`: Provide LogLevel.Severity or undefined instead of Option\. + +- `Layer.setVersionMismatchErrorLogLevel` -> `none`: No version-mismatch log-level Reference or public replacement exists. + +- `Layer.tapErrorCause` -> `Layer.tapCause`: The cause observer was renamed. + +- `Layer.toRuntime` -> `Layer.build(self), then Effect.runForkWith, Effect.runPromiseWith, or Effect.runSyncWith`: Runtime\ was removed; build a Context, or use ManagedRuntime.make for a reusable managed runner. + +- `Layer.toRuntimeWithMemoMap` -> `Layer.buildWithMemoMap(self, memoMap, scope), then Effect.run*With(context)`: Explicit memo-map building now yields a Context rather than a Runtime. + +- `Layer.unwrapEffect` -> `Layer.unwrap`: The Effect-based unwrap constructor was renamed and generalized. + +- `Layer.unwrapScoped` -> `Layer.unwrap`: Scoped and unscoped unwrap were merged; Layer.unwrap supplies and excludes the layer Scope. + +- `Layer.updateService` -> `Layer.updateService`: The combinator remains and now accepts any Context.Key. + +- `Layer.zipWith` -> `Layer.fromBuild with concurrent Effect.zipWith over Layer.buildWithMemoMap`: Combine acquisition effects directly; use Layer.merge when the function only merged Context values. + +### `effect/LayerMap` + +- `LayerMap.LayerMap` -> `LayerMap.LayerMap`: The type remains; runtime(key) became contextEffect(key) and returns Context. + +- `LayerMap.Service` -> `LayerMap.Service`: Use layer instead of Default, layerNoDeps instead of DefaultWithoutDependencies, and contextEffect instead of runtime. + +- `LayerMap.Service.Context` -> `LayerMap.Service.Services`: The input-services extractor was renamed. + +- `LayerMap.TagClass` -> `LayerMap.TagClass`: The type remains and now extends Context.ServiceClass; use the renamed layer and contextEffect members. + +- `LayerMap.TypeId` -> `none`: The LayerMap marker is private in v4 and no public guard exists. + +### `effect/List` + +- `List.Cons` -> `Array.NonEmptyReadonlyArray`: Use the immutable non-empty array type; constructors may return the assignable mutable NonEmptyArray subtype. + +- `List.List` -> `ReadonlyArray`: Replace the persistent linked-list representation with ReadonlyArray\. + +- `List.List.AndNonEmpty` -> `Array.ReadonlyArray.AndNonEmpty`: Use the corresponding readonly-array utility type. + +- `List.List.OrNonEmpty` -> `Array.ReadonlyArray.OrNonEmpty`: Use the corresponding readonly-array utility type. + +- `List.List.With` -> `Array.ReadonlyArray.With`: Use the corresponding readonly-array utility type. + +- `List.Nil` -> `none`: Represent this case as readonly []; there is no tagged Nil interface in v4. + +- `List.TypeId` -> `none`: Arrays have no List runtime marker; remove TypeId inspection. + +- `List.append` -> `Array.append`: List was removed; use Array.append. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.appendAll` -> `Array.appendAll`: List was removed; use Array.appendAll. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.cons` -> `Array.prepend`: List was removed; change List.cons(head, tail) to Array.prepend(tail, head). + +- `List.empty` -> `Array.empty`: List was removed; use Array.empty. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.every` -> `Array.every`: List was removed; run the predicate against the replacement array with Array.every. + +- `List.filter` -> `Array.filter`: List was removed; use Array.filter. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.filterMap` -> `Array.filterMap`: List was removed; use Array.filterMap and change the callback from Option to Result. + +- `List.fromIterable` -> `Array.fromIterable`: List was removed; use Array.fromIterable. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.getEquivalence` -> `Array.makeEquivalence`: List was removed; compare the replacement arrays with Array.makeEquivalence. + +- `List.head` -> `Array.head`: List was removed; use Array.head. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.isCons` -> `Array.isReadonlyArrayNonEmpty`: List was removed; this checks that the replacement readonly array is non-empty. + +- `List.isList` -> `Array.isArray`: The List brand is gone; this now recognizes the replacement JavaScript array representation. + +- `List.isNil` -> `Array.isReadonlyArrayEmpty`: List was removed; this checks that the replacement readonly array is empty. + +- `List.last` -> `Array.last`: List was removed; use Array.last. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.make` -> `Array.make`: List was removed; use Array.make. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.map` -> `Array.map`: List was removed; use Array.map. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.nil` -> `Array.empty`: List was removed; represent Nil with an empty array. + +- `List.of` -> `Array.of`: List was removed; use Array.of. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.partition` -> `Array.partition`: Use a Result-returning callback: failure values form the first array and success values the second. + +- `List.partitionMap` -> `Array.partition`: Migrate the Either-returning mapper to Result; failures form the first array and successes the second. + +- `List.prependAll` -> `Array.prependAll`: List was removed; use Array.prependAll. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.prependAllReversed` -> `Array.prependAll + Array.reverse`: Use Array.prependAll(self, Array.reverse(prefix)) to preserve the old ordering. + +- `List.reduce` -> `Array.reduce`: List was removed; use Array.reduce. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.reduceRight` -> `Array.reduceRight`: List was removed; use Array.reduceRight. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.reverse` -> `Array.reverse`: List was removed; use Array.reverse. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.size` -> `Array.length`: List was removed; use the replacement array length helper or the .length property. + +- `List.some` -> `Array.some`: List was removed; run the predicate against the replacement array with Array.some. + +- `List.splitAt` -> `Array.splitAt`: List was removed; use Array.splitAt. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.tail` -> `Array.tail`: List was removed; use Array.tail. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.take` -> `Array.take`: List was removed; use Array.take. It preserves ordering but returns arrays rather than persistent linked lists. + +- `List.toArray` -> `Array.fromIterable`: After migrating the representation this is usually unnecessary; use Array.fromIterable when a fresh mutable array is required. + +- `List.toChunk` -> `Chunk.fromIterable`: Convert the replacement array or other iterable with Chunk.fromIterable. + +- `List.unsafeHead` -> `Array.headNonEmpty`: Use a NonEmptyReadonlyArray proof before accessing the head; the v4 helper does not accept an empty array. + +- `List.unsafeLast` -> `Array.lastNonEmpty`: Use a NonEmptyReadonlyArray proof before accessing the last element; the v4 helper does not accept an empty array. + +- `List.unsafeTail` -> `Array.tailNonEmpty`: Use a NonEmptyReadonlyArray proof before taking the tail; the v4 helper does not accept an empty array. + +### `effect/LogLevel` + +- `LogLevel.All` -> `"All"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Debug` -> `"Debug"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Error` -> `"Error"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Fatal` -> `"Fatal"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Info` -> `"Info"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Literal` -> `LogLevel.LogLevel`: This is the all-level replacement after renaming Warning to Warn. LogLevel.Severity is narrower because it excludes All and None. + +- `LogLevel.LogLevel` -> `LogLevel.LogLevel`: The name remains, but the representation is a string union and object fields such as \_tag, label, syslog, and ordinal are gone. Use toUpperCase() for labels and LogLevel.getOrdinal for ordering. + +- `LogLevel.None` -> `"None"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Trace` -> `"Trace"`: V4 levels are string literals rather than branded objects; use the literal as both value and singleton type. + +- `LogLevel.Warning` -> `"Warn"`: V4 renamed both the value and singleton type from Warning to the string literal Warn. + +- `LogLevel.allLevels` -> `LogLevel.values`: Use the ordered v4 array of all levels, including All and None. + +- `LogLevel.fromLiteral` -> `literal === "Warning" ? "Warn" : literal`: No constructor is needed because v4 levels are strings. Normalize the renamed Warning literal to Warn; all other v3 literals pass through. + +- `LogLevel.greaterThan` -> `LogLevel.isGreaterThan`: Direct rename; ordering remains severity ordering. + +- `LogLevel.greaterThanEqual` -> `LogLevel.isGreaterThanOrEqualTo`: Direct rename. + +- `LogLevel.lessThan` -> `LogLevel.isLessThan`: Direct rename. + +- `LogLevel.lessThanEqual` -> `LogLevel.isLessThanOrEqualTo`: Direct rename. + +- `LogLevel.locally` -> `Effect.provideService(effect, References.CurrentLogLevel, level)`: Current log level is now a reference. For threshold configuration, including All or None, provide References.MinimumLogLevel instead. + +### `effect/LogSpan` + +- `LogSpan.LogSpan` -> `readonly [label: string, timestamp: number]`: The module was removed. Active log spans are tuples in References.CurrentLogSpans; ordinary callers should prefer Effect.withLogSpan. + +- `LogSpan.make` -> `[label, startTime] as const`: Construct the tuple directly, or use Effect.withLogSpan so Effect obtains the timestamp and scopes the span. + +- `LogSpan.render` -> `custom tuple formatter`: No public standalone renderer remains. Built-in loggers format span tuples internally; custom formatters can render label and elapsed milliseconds themselves. + +### `effect/Logger` + +- `Logger.Logger` -> `Logger.Logger`: The name remains. Logger.Options now has fiber instead of fiberId; read the id from fiber.id and annotations or spans through fiber references. + +- `Logger.Logger.Variance` -> `none`: Public variance metadata was removed; use Logger.Logger\ directly. + +- `Logger.LoggerTypeId` -> `Logger.isLogger`: The brand is private in v4; use the public runtime guard. + +- `Logger.add` -> `Logger.layer([logger], { mergeWithExisting: true })`: Logger installation is whole-set based; mergeWithExisting reproduces add. + +- `Logger.addEffect` -> `Logger.layer([loggerEffect], { mergeWithExisting: true })`: Logger.layer accepts effects that construct loggers. + +- `Logger.addScoped` -> `Logger.layer([scopedLoggerEffect], { mergeWithExisting: true })`: Layer acquisition handles the scoped effect; the separate scoped constructor is gone. + +- `Logger.batched` -> `Logger.batched(logger, { window, flush })`: The trailing arguments moved into one options object. Provide any services needed by flush before constructing it. + +- `Logger.filterLogLevel` -> `Logger.make(options => predicate(options.logLevel) ? Option.some(logger.log(options)) : Option.none())`: No named combinator remains; rebuild the wrapper with Logger.make. Prefer References.MinimumLogLevel for ordinary threshold filtering. + +- `Logger.json` -> `Logger.layer([Logger.consoleJson, Logger.tracerLogger])`: Logger.layer replaces the active set. Include tracerLogger to preserve v3 built-in layer behavior, or omit it when trace log events are intentionally disabled. + +- `Logger.jsonLogger` -> `Logger.formatJson`: Formatter rename; v4 JSON output uses level rather than logLevel. + +- `Logger.logFmt` -> `Logger.layer([Logger.consoleLogFmt, Logger.tracerLogger])`: Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior. + +- `Logger.logfmtLogger` -> `Logger.formatLogFmt`: Formatter rename and capitalization change. + +- `Logger.map` -> `Logger.map`: Retained with the same output-mapping behavior. + +- `Logger.mapInput` -> `Logger.make(options => logger.log({ ...options, message: f(options.message) }))`: No named input contramap remains; rebuild it with Logger.make. + +- `Logger.mapInputOptions` -> `Logger.make(options => logger.log(f(options)))`: No named options contramap remains; rebuild it with Logger.make and adapt f to the v4 Logger.Options shape. + +- `Logger.minimumLogLevel` -> `Layer.succeed(References.MinimumLogLevel, level)`: Minimum log level is now a context reference. + +- `Logger.none` -> `Logger.make(() => undefined)`: Rebuild the no-op logger with Logger.make. + +- `Logger.pretty` -> `Logger.layer([Logger.consolePretty(), Logger.tracerLogger])`: Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior. + +- `Logger.prettyLogger` -> `Logger.consolePretty`: Direct constructor rename; call it with the same options. + +- `Logger.prettyLoggerDefault` -> `Logger.consolePretty()`: The prebuilt singleton became a constructor call. + +- `Logger.remove` -> `Logger.layer([...desiredLoggers])`: No named removal combinator remains. Declare the complete desired logger set; transform Logger.CurrentLoggers only when removing from an unknown inherited set is unavoidable. + +- `Logger.replace` -> `Logger.layer([...desiredLoggers])`: V4 replaces the whole active set. When replacing the old default logger, include Logger.tracerLogger explicitly if it must survive. + +- `Logger.replaceEffect` -> `Logger.layer([loggerEffect, ...otherLoggers])`: Logger.layer accepts effects. Explicitly list every logger that must remain active. + +- `Logger.replaceScoped` -> `Logger.layer([scopedLoggerEffect, ...otherLoggers])`: Logger.layer acquisition supplies the scope; explicitly list every logger that must remain active. + +- `Logger.simple` -> `Logger.make(({ message }) => log(message))`: V3 simple was a message-only custom logger constructor; rebuild it with Logger.make. + +- `Logger.stringLogger` -> `Logger.formatSimple`: The prebuilt string formatter was renamed. + +- `Logger.structured` -> `Logger.layer([Logger.consoleStructured, Logger.tracerLogger])`: Logger.layer replaces the active set; include tracerLogger to preserve v3 built-in layer behavior. + +- `Logger.structuredLogger` -> `Logger.formatStructured`: Formatter rename; its output field logLevel is now named level. + +- `Logger.succeed` -> `Logger.make(() => value)`: Rebuild the constant-output logger with Logger.make. + +- `Logger.sync` -> `Logger.make(() => evaluate())`: Rebuild the lazy-output logger; evaluate still runs once per log event. + +- `Logger.test` -> `Effect.log(input).pipe(Effect.provide(Logger.layer([capturingLogger])))`: No synthetic-options helper remains. Exercise the logger through the runtime and capture its output so it receives a real Fiber, cause, level, and date. + +- `Logger.withMinimumLogLevel` -> `Effect.provideService(effect, References.MinimumLogLevel, level)`: Replace the FiberRef-local helper with reference provisioning. + +- `Logger.withSpanAnnotations` -> `custom Logger.make wrapper using options.fiber.currentSpan`: No transparent generic equivalent remains. Read span identity from options.fiber.currentSpan and add it to custom output as needed. + +- `Logger.zip` -> `Logger.make(options => [left.log(options), right.log(options)])`: No named combinator remains; invoke both loggers and return their output tuple. + +- `Logger.zipLeft` -> `Logger.make(options => { const output = left.log(options); right.log(options); return output })`: Rebuild explicitly, preserving evaluation of both loggers and returning the left output. + +- `Logger.zipRight` -> `Logger.make(options => { left.log(options); return right.log(options) })`: Rebuild explicitly, preserving evaluation order and returning the right output. + +### `effect/Mailbox` + +- `Mailbox.Mailbox` -> `Queue.Queue`: Mailbox was folded into Queue; include Cause.Done in the error channel when normal end signaling is used. + +- `Mailbox.ReadonlyMailbox` -> `Queue.Dequeue`: Use explicit Queue taking operations; Queue.Dequeue is not itself an Effect yielding message chunks. + +- `Mailbox.ReadonlyTypeId` -> `Queue.isDequeue`: The public Mailbox type id was removed; use the Queue.isDequeue guard instead. + +- `Mailbox.TypeId` -> `Queue.isQueue`: The public Mailbox type id was removed; use the Queue.isQueue guard instead. + +- `Mailbox.fromStream` -> `Stream.toQueue`: Mailbox was renamed and folded into Queue; Stream.toQueue returns a scoped Queue.Dequeue whose error includes Cause.Done. + +- `Mailbox.into` -> `Queue.into`: Use Queue.into with a Queue.Enqueue whose error channel includes Cause.Done. + +- `Mailbox.isMailbox` -> `Queue.isQueue`: Mailbox became the completion-aware v4 Queue model. + +- `Mailbox.isReadonlyMailbox` -> `Queue.isDequeue`: ReadonlyMailbox became Queue.Dequeue. + +- `Mailbox.make` -> `Queue.make`: Pass the v4 options object with optional capacity and strategy; a numeric capacity argument must become { capacity }. + +- `Mailbox.toStream` -> `Stream.fromQueue`: Convert a Queue.Dequeue to a Stream; Cause.Done is excluded from the resulting stream error type. + +### `effect/ManagedRuntime` + +- `ManagedRuntime.ManagedRuntime` -> `ManagedRuntime.ManagedRuntime`: The handle remains but is no longer an Effect; runtimeEffect/runtime became contextEffect/context, and make accepts { memoMap }. + +- `ManagedRuntime.ManagedRuntime.Context` -> `ManagedRuntime.ManagedRuntime.Services`: The context extractor was renamed to Services. + +- `ManagedRuntime.ManagedRuntimeUnify` -> `none`: ManagedRuntime no longer extends Effect, so its unification artifact was removed; call run methods or contextEffect explicitly. + +- `ManagedRuntime.ManagedRuntimeUnifyIgnore` -> `none`: ManagedRuntime no longer extends Effect, so the Unify-ignore artifact was removed. + +- `ManagedRuntime.TypeId` -> `ManagedRuntime.isManagedRuntime`: The marker is private; use the public guard for runtime narrowing. + +### `effect/Match` + +- `Match.MatcherTypeId` -> `none`: The public matcher brand was internalized. Obtain matchers from Match.type or Match.value and use their public \_tag when discrimination is required. + +- `Match.SafeRefinementId` -> `none`: The public safe-refinement brand was internalized. Use Predicate.Refinement, Predicate.Predicate, or a built-in Match refinement instead of constructing the brand. + +- `Match.TypeMatcher` -> `Match.TypeMatcher`: The public type is retained, but its brand is private; create values with Match.type rather than implementing the interface. + +- `Match.Types` -> `Match.Types`: The public type-level namespace is retained with no call-site migration. + +- `Match.Types.ExtractAndNarrow` -> `Match.Types.ExtractAndNarrow`: The type-only matching helper is retained unchanged. + +- `Match.Types.MaybeReplace` -> `Match.Types.MaybeReplace`: The type-only matching helper is retained unchanged. + +- `Match.Types.NonFailKeys` -> `Match.Types.NonFailKeys`: The type-only matching helper is retained unchanged. + +- `Match.Types.PForNotMatch` -> `Match.Types.PForNotMatch`: The type-only matching helper is retained unchanged. + +- `Match.Types.ResolvePred` -> `Match.Types.ResolvePred`: The type-only matching helper is retained unchanged. + +- `Match.Types.SafeRefinementR` -> `Match.Types.SafeRefinementR`: The type-only matching helper is retained unchanged. + +- `Match.Types.ToInvertedRefinement` -> `Match.Types.ToInvertedRefinement`: The type-only matching helper is retained unchanged. + +- `Match.Types.ToSafeRefinement` -> `Match.Types.ToSafeRefinement`: The type-only matching helper is retained unchanged. + +- `Match.ValueMatcher` -> `Match.ValueMatcher`: The type is retained, but value now uses Result instead of Either and the brand is private; create values with Match.value. + +- `Match.either` -> `Match.result`: Renamed finalizer with a container change: matched Right and unmatched Left become Result.Success and Result.Failure. + +### `effect/MergeDecision` + +- `MergeDecision.Await` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.AwaitConst` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.Done` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.MergeDecision` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.MergeDecision.Variance` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.MergeDecisionTypeId` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.isMergeDecision` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +- `MergeDecision.match` -> `Channel.merge`: Replace Channel.mergeWith decision callbacks with Channel.merge and haltStrategy. V4 has no MergeDecision values; custom effectful exit folding must be restructured. + +### `effect/MergeStrategy` + +- `MergeStrategy.BackPressure` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.BufferSliding` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.MergeStrategy` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.MergeStrategy.Proto` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.MergeStrategyTypeId` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.isBackPressure` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.isBufferSliding` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.isMergeStrategy` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +- `MergeStrategy.match` -> `Channel.mergeAll`: Remove the MergeStrategy value and configure the v4 merge directly: backpressure is the default; sliding replacement uses Channel.mergeAll with switch: true or Channel.switchMap. + +### `effect/Metric` + +- `Metric.Metric` -> `Metric.Metric`: Drop the v3 key-type parameter. Metrics are no longer callable; use Effect.trackSuccesses for instrumentation and Metric.update or Metric.value for operations. + +- `Metric.Metric.Variance` -> `none`: The public variance interface was removed; Metric\ carries variance markers directly. + +- `Metric.MetricApply` -> `none`: Removed with Metric.make; v4 has no public low-level custom-metric constructor type. + +- `Metric.MetricTypeId` -> `Metric.isMetric`: The public unique-symbol type id was removed; use Metric.isMetric for runtime refinement. + +- `Metric.fiberActive` -> `Metric.enableRuntimeMetrics + Metric.snapshot`: The concrete metric is no longer exported. Enable runtime metrics, then read the Gauge snapshot whose id is child\_fibers\_active. + +- `Metric.fiberFailures` -> `Metric.enableRuntimeMetrics + Metric.snapshot`: The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child\_fiber\_failures. + +- `Metric.fiberLifetimes` -> `none`: The built-in lifetime histogram was removed. Define a Metric.timer and instrument selected effects with Effect.trackDuration when lifetime data is required. + +- `Metric.fiberStarted` -> `Metric.enableRuntimeMetrics + Metric.snapshot`: The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child\_fibers\_started. + +- `Metric.fiberSuccesses` -> `Metric.enableRuntimeMetrics + Metric.snapshot`: The concrete metric is no longer exported. Enable runtime metrics, then read the Counter snapshot whose id is child\_fiber\_successes. + +- `Metric.fromMetricKey` -> `Metric.counter / Metric.gauge / Metric.frequency / Metric.histogram / Metric.summary`: MetricKey and MetricKeyType were removed. Construct the required primitive metric directly. + +- `Metric.globalMetricRegistry` -> `Metric.MetricRegistry`: The process-global registry became a Context.Reference whose service is a Map. Access it in context or provide a fresh Map for isolation; use Metric.snapshot for normal reads. + +- `Metric.increment` -> `Metric.update / Metric.modify`: Use Metric.update(counter, 1 or 1n) for counters and Metric.modify(gauge, 1 or 1n) for gauges; gauge update sets an absolute value while modify adds a delta. + +- `Metric.incrementBy` -> `Metric.update / Metric.modify`: Use Metric.update(counter, amount) for counters and Metric.modify(gauge, amount) for gauges. + +- `Metric.make` -> `none`: The low-level arbitrary metric constructor was removed. Use a public primitive constructor and compose with mapInput, withConstantInput, and withAttributes. + +- `Metric.map` -> `Metric.value + Effect.map`: Metric-level state mapping was removed. Transform a read with Effect.map(Metric.value(metric), f). + +- `Metric.mapType` -> `none`: Drop this call. V4 Metric has no key-type type parameter and exposes a fixed runtime type discriminator. + +- `Metric.set` -> `Metric.update`: Use Metric.update(gauge, value); v4 update replaces a gauge's current value. + +- `Metric.succeed` -> `none`: Constant synthetic metrics were removed. Keep constants outside the metric and use Effect.succeed when an Effect value is required. + +- `Metric.summaryTimestamp` -> `Metric.summaryWithTimestamp`: Renamed and called as Metric.summaryWithTimestamp(name, options). Remove the v3 error option; inputs remain value/timestamp pairs. + +- `Metric.sync` -> `none`: Lazy synthetic metrics were removed. Keep the computation outside the metric and use Effect.sync when an Effect value is required. + +- `Metric.tagged` -> `Metric.withAttributes`: Replace tags with attributes, for example Metric.withAttributes(metric, { [key]: value }). + +- `Metric.taggedWithLabels` -> `Metric.withAttributes`: Replace MetricLabel objects with a string record or array of string tuples passed to Metric.withAttributes. + +- `Metric.taggedWithLabelsInput` -> `Metric.withAttributes + Metric.update`: No dynamic-attribute transform remains. Compute attributes at each update or tracking site, wrap with Metric.withAttributes, then update the metric. + +- `Metric.timerWithBoundaries` -> `Metric.timer`: Use Metric.timer(name, { boundaries, description }); boundaries moved into the options object. + +- `Metric.trackAll` -> `Effect.track`: Moved to Effect; use effect.pipe(Effect.track(metric, () =\> input)). + +- `Metric.trackDefect` -> `Effect.trackDefects`: Moved to Effect; use effect.pipe(Effect.trackDefects(metric)). + +- `Metric.trackDefectWith` -> `Effect.trackDefects`: Moved to Effect; pass the mapper as the optional second argument. + +- `Metric.trackDurationWith` -> `Effect.trackDuration`: Moved to Effect; pass the mapper as the optional second argument. V4 records duration on every Exit, whereas v3 updated only after success. + +- `Metric.trackError` -> `Effect.trackErrors`: Moved to Effect; use effect.pipe(Effect.trackErrors(metric)). + +- `Metric.trackErrorWith` -> `Effect.trackErrors`: Moved to Effect; pass the mapper as the optional second argument. + +- `Metric.trackSuccess` -> `Effect.trackSuccesses`: Moved to Effect; use effect.pipe(Effect.trackSuccesses(metric)). + +- `Metric.trackSuccessWith` -> `Effect.trackSuccesses`: Moved to Effect; pass the mapper as the optional second argument. + +- `Metric.unsafeSnapshot` -> `Metric.snapshotUnsafe`: Renamed and now requires an explicit Context.Context\. It returns structural snapshots rather than MetricPair values. + +- `Metric.withNow` -> `Metric.summary`: Metric.summary reads the current Clock automatically; use Metric.summaryWithTimestamp when timestamps are supplied explicitly. The generic timestamp-injecting combinator was removed. + +- `Metric.zip` -> `Effect.all + Metric.update / Metric.value`: Composite metrics were removed. Use Effect.all to update both metrics or combine their Metric.value reads. + +### `effect/MetricBoundaries` + +- `MetricBoundaries.MetricBoundaries` -> `ReadonlyArray`: The wrapper was removed; Metric.histogram accepts plain boundaries in its options. + +- `MetricBoundaries.MetricBoundariesTypeId` -> `none`: Boundaries are unbranded arrays, so the guard and public type-id symbol have no replacement. + +- `MetricBoundaries.exponential` -> `Metric.exponentialBoundaries`: Moved into effect/Metric and now returns ReadonlyArray\. V4 also filters non-positive boundaries. + +- `MetricBoundaries.fromIterable` -> `Metric.boundariesFromIterable`: Moved into effect/Metric and now returns an unbranded ReadonlyArray\; v4 removes non-positive values before appending Infinity. + +- `MetricBoundaries.isMetricBoundaries` -> `none`: Boundaries are unbranded arrays, so the guard and public type-id symbol have no replacement. + +- `MetricBoundaries.linear` -> `Metric.linearBoundaries`: Moved into effect/Metric, but the compared v4 implementation uses start + i + width rather than v3's start + i \* width. Preserve the v3 formula manually when width is not 1. + +### `effect/MetricHook` + +- `MetricHook.MetricHook` -> `Metric.Metric.Hooks`: The closest public structural interface is Metric.Metric.Hooks\; get, update, and modify also receive a Context. + +- `MetricHook.MetricHook.Root` -> `Metric.Metric.Hooks`: The named aliases were removed; specialize the public Hooks interface directly when low-level typing is unavoidable. + +- `MetricHook.MetricHook.Untyped` -> `Metric.Metric.Hooks`: The named aliases were removed; specialize the public Hooks interface directly when low-level typing is unavoidable. + +- `MetricHook.MetricHook.Variance` -> `none`: Hooks are structural and unbranded; the variance helper and public symbol were removed. + +- `MetricHook.MetricHookTypeId` -> `none`: Hooks are structural and unbranded; the variance helper and public symbol were removed. + +- `MetricHook.counter` -> `Metric.counter`: Hook construction was folded into the complete Metric.counter constructor; hooks are internal. + +- `MetricHook.frequency` -> `Metric.frequency`: Hook construction was folded into the complete Metric.frequency constructor; hooks are internal. + +- `MetricHook.gauge` -> `Metric.gauge`: Hook construction was folded into the complete Metric.gauge constructor; hooks are internal. + +- `MetricHook.histogram` -> `Metric.histogram`: Hook construction was folded into the complete Metric.histogram constructor; hooks are internal. + +- `MetricHook.make` -> `none`: There is no public hook constructor; metric classes create and attach hooks internally. + +- `MetricHook.onModify` -> `none`: The operation-specific hook decorators were removed. Metric.mapInput cannot distinguish update from modify. + +- `MetricHook.onUpdate` -> `none`: The operation-specific hook decorators were removed. Metric.mapInput cannot distinguish update from modify. + +- `MetricHook.summary` -> `Metric.summary`: Hook construction was folded into the complete Metric.summary constructor; hooks are internal. + +### `effect/MetricKey` + +- `MetricKey.MetricKey` -> `Metric.Metric`: Key identity, metadata, and operations are combined in Metric\. + +- `MetricKey.MetricKey.Untyped` -> `Metric.Metric`: The separate untyped key alias was removed; use an untyped complete Metric only where required. + +- `MetricKey.MetricKey.Variance` -> `Metric.Metric`: There is no separate key variance interface; variance is carried by Metric's Input and State phantom fields. + +- `MetricKey.MetricKeyTypeId` -> `none`: The key brand was removed; Metric's protocol key is internal. + +- `MetricKey.counter` -> `Metric.counter`: The key and key type were merged into the complete Metric.counter constructor. + +- `MetricKey.frequency` -> `Metric.frequency`: The key and key type were merged into the complete Metric.frequency constructor. + +- `MetricKey.gauge` -> `Metric.gauge`: The key and key type were merged into the complete Metric.gauge constructor. + +- `MetricKey.histogram` -> `Metric.histogram`: The key and key type were merged into the complete Metric.histogram constructor. + +- `MetricKey.isMetricKey` -> `Metric.isMetric`: Keys became complete metrics; use the complete-metric runtime guard. + +- `MetricKey.summary` -> `Metric.summary`: The key and key type were merged into the complete Metric.summary constructor. + +- `MetricKey.taggedWithLabels` -> `Metric.withAttributes`: Labels became attributes. Pass a string record or array of string tuples. + +### `effect/MetricKeyType` + +- `MetricKeyType.CounterKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.FrequencyKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.GaugeKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.HistogramKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.MetricKeyType` -> `Metric.Metric`: Input/state typing and kind configuration now live on the complete Metric\. + +- `MetricKeyType.MetricKeyType.InType` -> `Metric.Metric.Input`: Use Metric.Metric.Input\ to extract a metric's input type. + +- `MetricKeyType.MetricKeyType.OutType` -> `Metric.Metric.State`: Use Metric.Metric.State\ to extract a metric's state type. + +- `MetricKeyType.MetricKeyType.Untyped` -> `Metric.Metric`: The key-type descriptor no longer exists independently of a metric. + +- `MetricKeyType.MetricKeyType.Variance` -> `none`: The descriptor variance interface was removed; complete Metric carries Input and State variance. + +- `MetricKeyType.MetricKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.SummaryKeyTypeTypeId` -> `none`: All public key-type symbols were removed; use a complete metric's string type discriminant. + +- `MetricKeyType.counter` -> `Metric.counter`: The standalone descriptor was folded into the complete Metric.counter constructor. + +- `MetricKeyType.frequency` -> `Metric.frequency`: The standalone descriptor was folded into the complete Metric.frequency constructor. + +- `MetricKeyType.gauge` -> `Metric.gauge`: The standalone descriptor was folded into the complete Metric.gauge constructor. + +- `MetricKeyType.histogram` -> `Metric.histogram`: The standalone descriptor was folded into the complete Metric.histogram constructor. + +- `MetricKeyType.isCounterKey` -> `Metric.isMetric + metric.type`: Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant. + +- `MetricKeyType.isFrequencyKey` -> `Metric.isMetric + metric.type`: Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant. + +- `MetricKeyType.isGaugeKey` -> `Metric.isMetric + metric.type`: Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant. + +- `MetricKeyType.isHistogramKey` -> `Metric.isMetric + metric.type`: Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant. + +- `MetricKeyType.isMetricKeyType` -> `Metric.isMetric`: Standalone key-type values were removed; test complete metrics instead. + +- `MetricKeyType.isSummaryKey` -> `Metric.isMetric + metric.type`: Standalone refinements were removed. Complete metrics expose a Counter, Frequency, Gauge, Histogram, or Summary string discriminant. + +- `MetricKeyType.summary` -> `Metric.summary`: The standalone descriptor was folded into the complete Metric.summary constructor. + +### `effect/MetricLabel` + +- `MetricLabel.MetricLabel` -> `[string, string]`: A label is now an ordinary attribute tuple; collections are Metric.Metric.Attributes or Metric.Metric.AttributeSet. + +- `MetricLabel.MetricLabelTypeId` -> `none`: Attributes are plain tuples or records, so there is no branded guard or type-id symbol. + +- `MetricLabel.isMetricLabel` -> `none`: Attributes are plain tuples or records, so there is no branded guard or type-id symbol. + +- `MetricLabel.make` -> `[key, value]`: Construct an ordinary tuple, or place the pair in an attribute record passed to Metric.withAttributes or a metric constructor. + +### `effect/MetricPair` + +- `MetricPair.MetricPair` -> `Metric.Metric.Snapshot`: Registry key/state pairs became discriminated snapshots containing id, type, description, attributes, and state. + +- `MetricPair.MetricPair.Untyped` -> `Metric.Metric.Snapshot`: Registry key/state pairs became discriminated snapshots containing id, type, description, attributes, and state. + +- `MetricPair.MetricPair.Variance` -> `none`: Snapshots are structural, so the pair variance helper and brand symbol were removed. + +- `MetricPair.MetricPairTypeId` -> `none`: Snapshots are structural, so the pair variance helper and brand symbol were removed. + +- `MetricPair.make` -> `Metric.snapshot`: There is no pair constructor. Obtain snapshots with Metric.snapshot or Metric.snapshotUnsafe; manually constructed data can satisfy Metric.Metric.SnapshotProto. + +- `MetricPair.unsafeMake` -> `Metric.snapshot`: There is no pair constructor. Obtain snapshots with Metric.snapshot or Metric.snapshotUnsafe; manually constructed data can satisfy Metric.Metric.SnapshotProto. + +### `effect/MetricPolling` + +- `MetricPolling.MetricPolling` -> `local { metric, poll } record`: The module was removed. Keep a local record pairing a Metric with its polling Effect when this abstraction is still useful. + +- `MetricPolling.MetricPollingTypeId` -> `none`: The polling wrapper and its brand were removed. + +- `MetricPolling.collectAll` -> `Effect.forEach + Metric.update/value`: No combined metric replacement exists. Poll records, update each metric, and collect states explicitly. + +- `MetricPolling.launch` -> `Effect.repeat + Effect.forkScoped`: Repeat polling, updating, and reading with the schedule, then forkScoped. + +- `MetricPolling.make` -> `({ metric, poll })`: No public constructor remains; use the local record directly. + +- `MetricPolling.poll` -> `self.poll`: Access the polling Effect from the local record. + +- `MetricPolling.pollAndUpdate` -> `Effect.flatMap(self.poll, input => Metric.update(self.metric, input))`: Compose polling and metric update directly. + +- `MetricPolling.retry` -> `Effect.retry`: Retry the poll Effect and retain the same metric in the local record. + +### `effect/MetricRegistry` + +- `MetricRegistry.MetricRegistry` -> `Metric.MetricRegistry`: The method-bearing registry became a Context.Reference whose service is a Map. Metrics register metadata and hooks lazily. + +- `MetricRegistry.MetricRegistryTypeId` -> `none`: The registry service is an ordinary Map behind a Context.Reference and has no public brand. + +- `MetricRegistry.make` -> `new Map>()`: Provide a fresh Map to Metric.MetricRegistry for isolation. Read it through Metric.snapshot or snapshotUnsafe. + +### `effect/MetricState` + +- `MetricState.CounterStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.FrequencyStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.GaugeStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.HistogramStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.MetricState` -> `Metric.Metric.State`: The common branded state model was removed; extract a complete metric's state with Metric.Metric.State\ or use a concrete state interface. + +- `MetricState.MetricState.Untyped` -> `Metric.Metric.Snapshot['state']`: Use the state union from Metric.Metric.Snapshot, or explicitly union the five structural state interfaces. + +- `MetricState.MetricState.Variance` -> `none`: States are structural objects and no longer carry a variance brand. + +- `MetricState.MetricStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.SummaryStateTypeId` -> `none`: All state brand symbols were removed; v4 state interfaces are structural. + +- `MetricState.counter` -> `Metric.CounterState`: There is no state constructor. Obtain the structural state with Metric.value(Metric.counter(...)). + +- `MetricState.frequency` -> `Metric.FrequencyState`: There is no state constructor. Obtain the structural state with Metric.value(Metric.frequency(...)). + +- `MetricState.gauge` -> `Metric.GaugeState`: There is no state constructor. Obtain the structural state with Metric.value(Metric.gauge(...)). + +- `MetricState.histogram` -> `Metric.HistogramState`: There is no state constructor. Obtain the structural state with Metric.value(Metric.histogram(...)). + +- `MetricState.isCounterState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.isFrequencyState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.isGaugeState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.isHistogramState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.isMetricState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.isSummaryState` -> `none`: Standalone state guards were removed. Retain the enclosing snapshot and switch on snapshot.type for runtime discrimination. + +- `MetricState.summary` -> `Metric.SummaryState`: There is no state constructor. Obtain the structural state with Metric.value(Metric.summary(...)). + +### `effect/Micro` + +- `Micro.All.IsDiscard` -> `Effect.All.IsDiscard`: Type-level helper moved to the Effect.All namespace. + +- `Micro.All.MicroAny` -> `Effect.All.EffectAny`: Renamed: MicroAny becomes EffectAny in the Effect.All namespace. + +- `Micro.All.Return` -> `Effect.All.Return`: Type-level helper moved to the Effect.All namespace. + +- `Micro.CurrentConcurrency` -> `none`: Removed in v4 (no fiber-wide concurrency reference). Pass a { concurrency } option directly to the operations that fan out, e.g. Effect.all or Effect.forEach. + +- `Micro.CurrentScheduler` -> `References.Scheduler`: The scheduler reference lives in effect/References (also exported from effect/Scheduler as Scheduler.Scheduler). Override it with Effect.provideService/Effect.updateService. + +- `Micro.Do` -> `Effect.Do`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.Error` -> `Data.Error`: The yieldable error base class constructor is Data.Error from effect/Data in v4. + +- `Micro.MaxOpsBeforeYield` -> `References.MaxOpsBeforeYield`: The reference lives in effect/References (also exported from effect/Scheduler). Override it with Effect.updateService. + +- `Micro.Micro` -> `Effect.Effect`: The Micro\ type is Effect.Effect\ in v4; the v4 Effect runtime is itself lightweight. + +- `Micro.Micro.Context` -> `Effect.Services`: Type extractor renamed: Micro.Context\ becomes Effect.Services\ in v4. + +- `Micro.Micro.Error` -> `Effect.Error`: Type extractor: Micro.Error\ becomes Effect.Error\ in v4. + +- `Micro.Micro.Success` -> `Effect.Success`: Type extractor: Micro.Success\ becomes Effect.Success\ in v4. + +- `Micro.MicroCause` -> `Cause.Cause`: MicroCause\ becomes Cause.Cause\. Note v4 Cause holds a list of failure reasons (Fail | Die | Interrupt) rather than being a single tagged variant. + +- `Micro.MicroCause.Die` -> `Cause.Die`: The Die variant is a Reason in v4: Cause.Die from effect/Cause. + +- `Micro.MicroCause.Error` -> `Cause.Cause.Error`: Type extractor: use the Error helper in the Cause.Cause namespace to extract the error type. + +- `Micro.MicroCause.Fail` -> `Cause.Fail`: The Fail variant is a Reason in v4: Cause.Fail\ from effect/Cause. + +- `Micro.MicroCause.Interrupt` -> `Cause.Interrupt`: The Interrupt variant is a Reason in v4: Cause.Interrupt from effect/Cause. + +- `Micro.MicroCause.Proto` -> `Cause.Cause.ReasonProto`: Internal prototype type; the closest v4 equivalent is the ReasonProto interface in the Cause.Cause namespace. Rarely needed directly. + +- `Micro.MicroCauseTypeId` -> `Cause.TypeId`: Use Cause.TypeId from effect/Cause (value is "\~effect/Cause"). + +- `Micro.MicroExit` -> `Exit.Exit`: MicroExit\ becomes Exit.Exit\ from effect/Exit. In v4 Exit is a subtype of Effect. + +- `Micro.MicroExit.Failure` -> `Exit.Failure`: MicroExit.Failure becomes Exit.Failure\ from effect/Exit. + +- `Micro.MicroExit.Proto` -> `Exit.Exit.Proto`: Internal prototype type; v4 exposes the shared base as Proto in the Exit.Exit namespace. Rarely needed directly. + +- `Micro.MicroExit.Success` -> `Exit.Success`: MicroExit.Success becomes Exit.Success\ from effect/Exit. + +- `Micro.MicroExitTypeId` -> `none`: v4 Exit is a subtype of Effect and has no dedicated TypeId; use Exit.isExit to identify exits. + +- `Micro.MicroFiber` -> `Fiber.Fiber`: MicroFiber\ becomes Fiber.Fiber\ from effect/Fiber. + +- `Micro.MicroFiber.Variance` -> `none`: Type-level variance helper with no public v4 equivalent; the v4 Fiber.Fiber interface carries variance directly. + +- `Micro.MicroFiberTypeId` -> `none`: No public TypeId on v4 fibers; use Fiber.isFiber to identify fibers. + +- `Micro.MicroIterator` -> `Effect.EffectIterator`: Renamed: MicroIterator becomes Effect.EffectIterator (generator support for Effect.gen). + +- `Micro.MicroSchedule` -> `Schedule.Schedule`: v3 MicroSchedule was a plain function (attempt, elapsedMillis) =\> Option\; v4 uses the first-class Schedule.Schedule type from effect/Schedule. + +- `Micro.MicroScheduler` -> `Scheduler.Scheduler`: The scheduler interface lives in effect/Scheduler in v4. + +- `Micro.MicroSchedulerDefault` -> `Scheduler.MixedScheduler`: The default task scheduler implementation in v4 is Scheduler.MixedScheduler from effect/Scheduler. + +- `Micro.MicroScope` -> `Scope.Scope`: MicroScope becomes Scope.Scope from effect/Scope; the closeable variant is Scope.Closeable. + +- `Micro.MicroScopeTypeId` -> `none`: No public TypeId on v4 scopes; use the Scope.Scope service key to access the current scope. + +- `Micro.MicroTypeLambda` -> `Effect.EffectTypeLambda`: Renamed: MicroTypeLambda becomes Effect.EffectTypeLambda. + +- `Micro.MicroUnify` -> `Effect.EffectUnify`: Renamed: MicroUnify becomes Effect.EffectUnify. + +- `Micro.MicroUnifyIgnore` -> `none`: Removed; v4 Effect declares its unify-ignore slot inline and exposes no named UnifyIgnore interface. + +- `Micro.NoSuchElementException` -> `Cause.NoSuchElementError`: Renamed and moved: NoSuchElementException becomes Cause.NoSuchElementError from effect/Cause. + +- `Micro.TaggedError` -> `Data.TaggedError`: The yieldable tagged error class constructor is Data.TaggedError from effect/Data in v4. + +- `Micro.TimeoutException` -> `Cause.TimeoutError`: Renamed and moved: TimeoutException becomes Cause.TimeoutError from effect/Cause (raised by Effect.timeout). + +- `Micro.TypeId` -> `Effect.TypeId`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.YieldableError` -> `Cause.YieldableError`: Moved: YieldableError lives in effect/Cause in v4. + +- `Micro.acquireUseRelease` -> `Effect.acquireUseRelease`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.all` -> `Effect.all`: Micro was removed in v4; use Effect.all with the same iterable-or-record input and concurrency/discard options. + +- `Micro.as` -> `Effect.as`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.asSome` -> `Effect.asSome`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.asVoid` -> `Effect.asVoid`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.async` + +**Replacement:** `Effect.callback` + +Renamed: the async constructor is Effect.callback in v4. Same resume/AbortSignal semantics. + +**Example** + +```ts +Effect.callback((resume) => resume(Effect.succeed(1))) +``` + +- `Micro.bind` -> `Effect.bind`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.bindTo` -> `Effect.bindTo`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.catchAll` -> `Effect.catch`: Renamed: catchAll is Effect.catch in v4. + +- `Micro.catchAllCause` -> `Effect.catchCause`: Renamed: catchAllCause is Effect.catchCause in v4. + +- `Micro.catchAllDefect` -> `Effect.catchDefect`: Renamed: catchAllDefect is Effect.catchDefect in v4. + +- `Micro.catchIf` -> `Effect.catchIf`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.catchTag` -> `Effect.catchTag`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.causeDie` -> `Cause.die`: MicroCause was replaced by the unified effect/Cause module in v4. + +- `Micro.causeFail` -> `Cause.fail`: MicroCause was replaced by the unified effect/Cause module in v4. + +- `Micro.causeInterrupt` -> `Cause.interrupt`: MicroCause was replaced by the unified effect/Cause module in v4. Takes an optional fiber id. + +- `Micro.causeIsDie` -> `Cause.hasDies`: v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasDies checks for Die reasons; use Cause.isDieReason for a single Reason value. + +- `Micro.causeIsFail` -> `Cause.hasFails`: v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasFails checks for Fail reasons; use Cause.isFailReason for a single Reason value. + +- `Micro.causeIsInterrupt` -> `Cause.hasInterrupts`: v4 Cause aggregates multiple reasons, so tag refinements become reason queries: Cause.hasInterrupts checks for Interrupt reasons (see also Cause.hasInterruptsOnly). + +- `Micro.causeSquash` -> `Cause.squash`: Same behavior in the unified effect/Cause module. + +- `Micro.causeWithTrace` -> `Cause.annotate`: v4 causes carry structured annotations instead of a traces array; attach trace data with Cause.annotate (e.g. the Cause.StackTrace service). v4 also captures failure stack traces automatically. + +- `Micro.context` -> `Effect.context`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.delay` -> `Effect.delay`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.either` -> `Effect.result`: Either was replaced by Result in v4: Effect.result yields Result.Result\ instead of Either\. + +- `Micro.ensuring` -> `Effect.ensuring`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.exit` -> `Effect.exit`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.exitDie` -> `Exit.die`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.exitFail` -> `Exit.fail`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.exitFailCause` -> `Exit.failCause`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.exitInterrupt` -> `Exit.interrupt`: MicroExit was replaced by the unified effect/Exit module in v4. Takes an optional fiber id. + +- `Micro.exitIsDie` -> `Exit.hasDies`: v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasDies checks the failure cause for Die reasons. + +- `Micro.exitIsFail` -> `Exit.hasFails`: v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasFails checks the failure cause for Fail reasons. + +- `Micro.exitIsFailure` -> `Exit.isFailure`: Same refinement in the unified effect/Exit module. + +- `Micro.exitIsInterrupt` -> `Exit.hasInterrupts`: v4 exits carry a multi-reason Cause, so tag refinements become reason queries: Exit.hasInterrupts checks the failure cause for Interrupt reasons. + +- `Micro.exitIsSuccess` -> `Exit.isSuccess`: Same refinement in the unified effect/Exit module. + +- `Micro.exitSucceed` -> `Exit.succeed`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.exitVoid` -> `Exit.void`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.exitVoidAll` -> `Exit.asVoidAll`: Renamed: exitVoidAll becomes Exit.asVoidAll in the unified effect/Exit module. + +- `Micro.fail` -> `Effect.fail`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.failCause` -> `Effect.failCause`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.failCauseSync` -> `Effect.failCauseSync`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.failSync` -> `Effect.failSync`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.fiberAwait` -> `Fiber.await`: Fiber operations moved to the effect/Fiber module in v4. + +- `Micro.fiberInterrupt` -> `Fiber.interrupt`: Fiber operations moved to the effect/Fiber module in v4. + +- `Micro.fiberInterruptAll` -> `Fiber.interruptAll`: Fiber operations moved to the effect/Fiber module in v4. + +- `Micro.fiberJoin` -> `Fiber.join`: Fiber operations moved to the effect/Fiber module in v4. + +- `Micro.filter` -> `Effect.filter`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.filterMap` -> `Effect.filterMap`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.filterOrFail` -> `Effect.filterOrFail`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.filterOrFailCause` + +**Replacement:** `Effect.filterOrElse` + +No direct equivalent; use Effect.filterOrElse and fail with a cause in the fallback. + +**Example** + +```ts +Effect.filterOrElse(effect, predicate, { orElse: () => Effect.failCause(Cause.die("invalid")) }) +``` + +- `Micro.flatten` -> `Effect.flatten`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.flip` -> `Effect.flip`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.forkDaemon` -> `Effect.forkDetach`: Renamed: forkDaemon becomes Effect.forkDetach (fork detached from the parent's lifetime). + +- `Micro.forkIn` -> `Effect.forkIn`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.forkScoped` -> `Effect.forkScoped`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.fromEither` -> `Effect.fromResult`: Either was replaced by Result in v4: convert Result.Result values with Effect.fromResult. + +- `Micro.fromOption` -> `Effect.fromOption`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.ignore` -> `Effect.ignore`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.ignoreLogged` + +**Replacement:** `Effect.ignore` + +Removed; log explicitly before ignoring. + +**Example** + +```ts +effect.pipe(Effect.tapCause((cause) => Effect.logError(cause)), Effect.ignore) +``` + +- `Micro.interrupt` -> `Effect.interrupt`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.interruptible` -> `Effect.interruptible`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.isMicro` -> `Effect.isEffect`: Micro values are plain Effects in v4; use Effect.isEffect. + +- `Micro.isMicroCause` -> `Cause.isCause`: MicroCause was replaced by the unified effect/Cause module in v4. + +- `Micro.isMicroExit` -> `Exit.isExit`: MicroExit was replaced by the unified effect/Exit module in v4. + +- `Micro.let` -> `Effect.let`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.map` -> `Effect.map`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.mapErrorCause` + +**Replacement:** `Effect.catchCause` + +No direct equivalent; transform the cause by catching it and re-failing. + +**Example** + +```ts +Effect.catchCause(effect, (cause) => Effect.failCause(Cause.map(cause, transformError))) +``` + +- `Micro.match` -> `Effect.match`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.matchCause` -> `Effect.matchCause`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.matchCauseEffect` -> `Effect.matchCauseEffect`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.onExit` -> `Effect.onExit`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.onInterrupt` -> `Effect.onInterrupt`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.option` -> `Effect.option`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.orDie` -> `Effect.orDie`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.orElseSucceed` -> `Effect.orElseSucceed`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.promise` -> `Effect.promise`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.race` -> `Effect.race`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.raceAll` -> `Effect.raceAll`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.raceFirst` -> `Effect.raceFirst`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.repeatExit` + +**Replacement:** `Effect.repeat` + +Removed; use Effect.repeat with while/until/times/schedule options. To inspect failures while looping, run the body through Effect.exit and repeat on the Exit value. + +**Example** + +```ts +Effect.repeat(Effect.exit(effect), { while: (exit) => Exit.isFailure(exit), times: 3 }) +``` + +- `Micro.replicate` -> `Effect.replicate`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.replicateEffect` -> `Effect.replicateEffect`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.retry` -> `Effect.retry`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.runFork` -> `Effect.runFork`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.runPromise` -> `Effect.runPromise`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.runPromiseExit` -> `Effect.runPromiseExit`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.runSync` -> `Effect.runSync`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.runSyncExit` -> `Effect.runSyncExit`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.sandbox` -> `Effect.sandbox`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.scheduleAddDelay` -> `Schedule.addDelay`: Moved to effect/Schedule; the callback now receives schedule Metadata and returns a Duration (optionally effectful). + +- `Micro.scheduleExponential` -> `Schedule.exponential`: Moved to effect/Schedule; takes Duration input instead of raw millis. + +- `Micro.scheduleIntersect` -> `Schedule.max`: Intersection (recur while both recur, waiting for the slower) is Schedule.max([self, that]) in v4. + +- `Micro.scheduleRecurs` -> `Schedule.recurs`: Moved to effect/Schedule. + +- `Micro.scheduleSpaced` -> `Schedule.spaced`: Moved to effect/Schedule; takes Duration input instead of raw millis. + +- `Micro.scheduleUnion` -> `Schedule.min`: Union (recur while either recurs, waiting for the faster) is Schedule.min([self, that]) in v4. + +#### `Micro.scheduleWithMaxDelay` + +**Replacement:** `Schedule.modifyDelay` + +No direct equivalent; clamp the delay with Schedule.modifyDelay. + +**Example** + +```ts +Schedule.modifyDelay(schedule, ({ delay }) => Duration.min(delay, "10 seconds")) +``` + +#### `Micro.scheduleWithMaxElapsed` + +**Replacement:** `Schedule.upTo` + +Renamed: cap total elapsed time with Schedule.upTo({ duration }). + +**Example** + +```ts +Schedule.upTo(schedule, { duration: "30 seconds" }) +``` + +- `Micro.scopeMake` -> `Scope.make`: Moved to effect/Scope: Scope.make returns Effect\ and accepts an optional finalizer strategy. + +- `Micro.scopeUnsafeMake` -> `Scope.makeUnsafe`: Renamed and moved: scopeUnsafeMake becomes Scope.makeUnsafe from effect/Scope. + +- `Micro.scoped` -> `Effect.scoped`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.service` -> `service`: Micro was removed in v4, and services are Effects; yield or compose the service key directly in the rewritten Effect runtime. + +- `Micro.succeed` -> `Effect.succeed`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.sync` -> `Effect.sync`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.tap` -> `Effect.tap`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.tapDefect` -> `Effect.tapDefect`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.tapError` -> `Effect.tapError`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.tapErrorCause` -> `Effect.tapCause`: Renamed: tapErrorCause becomes Effect.tapCause. + +- `Micro.tapErrorCauseIf` -> `Effect.tapCauseIf`: Renamed: tapErrorCauseIf becomes Effect.tapCauseIf (see also Effect.tapCauseFilter for Filter-based matching). + +- `Micro.timeout` -> `Effect.timeout`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.timeoutOption` -> `Effect.timeoutOption`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.try` -> `Effect.try`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.tryPromise` -> `Effect.tryPromise`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.uninterruptibleMask` -> `Effect.uninterruptibleMask`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.updateContext` -> `Effect.updateContext`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.updateService` -> `Effect.updateService`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.void` -> `Effect.void`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.when` -> `Effect.when`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +- `Micro.whileLoop` -> `Effect.whileLoop`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +#### `Micro.withConcurrency` + +**Replacement:** `none` + +Removed in v4 along with "inherit" concurrency; pass a { concurrency } option directly to each concurrent operation. + +**Example** + +```ts +Effect.forEach(items, handle, { concurrency: 10 }) +``` + +- `Micro.withMicroFiber` -> `Effect.withFiber`: Renamed: withMicroFiber becomes Effect.withFiber, giving access to the current fiber. + +- `Micro.withTrace` -> `Effect.withSpan`: Removed; v4 captures failure stack traces automatically and cause annotations replace the traces array. For named tracing regions use Effect.withSpan. + +- `Micro.yieldFlush` -> `none`: Removed; access the current scheduler via the References.Scheduler service and call its flush() method directly if deterministic draining is needed. + +- `Micro.yieldNow` -> `Effect.yieldNow`: Micro was removed in v4; the rewritten Effect runtime is itself lightweight and replaces it. Same-name equivalent on effect/Effect. + +### `effect/MutableHashMap` + +- `MutableHashMap.MutableHashMap` -> `MutableHashMap.MutableHashMap`: The MutableHashMap model remains; use its public operations rather than depending on internal representation fields. + +- `MutableHashMap.TypeId` -> `none`: The public MutableHashMap.TypeId was removed; the v4 marker is private. + +### `effect/MutableHashSet` + +- `MutableHashSet.MutableHashSet` -> `MutableHashSet.MutableHashSet`: The MutableHashSet model remains; use its public operations rather than depending on internal representation fields. + +- `MutableHashSet.TypeId` -> `none`: The public MutableHashSet.TypeId was removed; the v4 marker is private. + +### `effect/MutableList` + +- `MutableList.MutableList` -> `MutableList.MutableList`: The model remains but was redesigned from an iterable doubly linked list into a bucketed FIFO structure. + +- `MutableList.TypeId` -> `none`: V4 MutableList has no public runtime marker. + +- `MutableList.empty` -> `MutableList.make`: Constructor rename; MutableList.Empty is the take sentinel, not a constructor. + +- `MutableList.forEach` -> `MutableList.toArray + Array.forEach`: No direct traversal helper remains; iterate a snapshot produced by MutableList.toArray. + +- `MutableList.fromIterable` -> `MutableList.make + MutableList.appendAll`: Create an empty list with MutableList.make, then append the iterable with MutableList.appendAll. + +- `MutableList.head` -> `MutableList.toArrayN`: Use MutableList.toArrayN(self, 1)[0]; the redesigned FIFO exposes buckets rather than the old Option-returning accessor. + +- `MutableList.isEmpty` -> `none`: Read self.length === 0; no named isEmpty helper remains. + +- `MutableList.length` -> `none`: Read the public self.length field; no named length helper remains. + +- `MutableList.pop` -> `none`: The bucketed FIFO has no remove-last operation; migrate code to front draining or use a different mutable collection. + +- `MutableList.reset` -> `MutableList.clear`: Direct behavioral replacement; the return type is now void. + +- `MutableList.shift` -> `MutableList.take`: Front removal remains synchronous, but emptiness is reported with MutableList.Empty instead of undefined. + +- `MutableList.tail` -> `MutableList.toArray`: Use MutableList.toArray(self).at(-1); the public self.tail field is an internal bucket, not the old last-element accessor. + +### `effect/MutableQueue` + +- `MutableQueue.EmptyMutableQueue` -> `none`: Queue.poll reports emptiness with Option.none, so no default sentinel is required. + +- `MutableQueue.MutableQueue` -> `Queue.Queue`: The replacement Queue is effectful, lifecycle-aware, and not Iterable. + +- `MutableQueue.MutableQueue.Empty` -> `none`: Use the Option returned by Queue.poll; the old empty sentinel was removed. + +- `MutableQueue.TypeId` -> `none`: The MutableQueue module and its public marker were removed. + +- `MutableQueue.bounded` -> `Queue.dropping`: The replacement constructor is effectful; dropping preserves the old immediate rejection when a bounded queue is full. + +- `MutableQueue.capacity` -> `none`: Read queue.capacity on the replacement Queue; unbounded queues expose Infinity. + +- `MutableQueue.isEmpty` -> `Queue.sizeUnsafe`: Use Queue.sizeUnsafe(queue) === 0, or map the effectful Queue.size result. + +- `MutableQueue.isFull` -> `Queue.isFullUnsafe`: Use Queue.isFullUnsafe for synchronous inspection or Queue.isFull for an Effect result. + +- `MutableQueue.length` -> `Queue.sizeUnsafe`: Use Queue.sizeUnsafe for synchronous inspection or Queue.size for an Effect result. + +- `MutableQueue.offer` -> `Queue.offerUnsafe`: Use with Queue.dropping to preserve the old synchronous boolean rejection at capacity; Queue.offer is the effectful form. + +- `MutableQueue.offerAll` -> `Queue.offerAllUnsafe`: The synchronous replacement returns the rejected remainder as an Array; Queue.offerAll is the effectful form. + +- `MutableQueue.poll` -> `Queue.poll`: Polling is now effectful and returns Option\ rather than accepting a default; Queue.takeUnsafe is the low-level synchronous alternative. + +- `MutableQueue.pollUpTo` -> `Queue.takeUnsafe`: No direct non-blocking take-up-to helper remains; repeatedly call Queue.takeUnsafe and collect successful exits without waiting. + +- `MutableQueue.unbounded` -> `Queue.unbounded`: The unbounded replacement constructor is effectful. + +### `effect/MutableRef` + +- `MutableRef.MutableRef` -> `MutableRef.MutableRef`: The MutableRef model remains; use its public operations rather than depending on internal representation fields. + +- `MutableRef.TypeId` -> `none`: The public MutableRef.TypeId was removed; the v4 marker is private. + +### `effect/Number` + +- `Number.greaterThan` -> `Number.isGreaterThan`: Renamed with the v4 is-prefix. + +- `Number.greaterThanOrEqualTo` -> `Number.isGreaterThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `Number.lessThan` -> `Number.isLessThan`: Renamed with the v4 is-prefix. + +- `Number.lessThanOrEqualTo` -> `Number.isLessThanOrEqualTo`: Renamed with the v4 is-prefix. + +- `Number.negate` -> `Number.multiply(-1)`: Use Number.multiply(n, -1), or Number.multiply(-1) as the equivalent unary function. + +- `Number.unsafeDivide` -> `Number.divideUnsafe`: Renamed; v4 throws for zero whereas v3 raw division returned Infinity or NaN. + +### `effect/Option` + +- `Option.None` -> `Option.None`: The variant remains, but Option is no longer an Effect or STM subtype. + +- `Option.Option` -> `Option.Option`: The union type remains, but Option is no longer an Effect or STM subtype. + +- `Option.OptionUnify` -> `Option.OptionUnify`: The unification hook remains under the same name. + +- `Option.OptionUnifyIgnore` -> `Option.OptionUnifyIgnore`: The marker remains, without the v3 Effect, Tag, and Either augmentation fields. + +- `Option.Some` -> `Option.Some`: The variant remains, but Option is no longer an Effect or STM subtype. + +- `Option.TypeId` -> `none`: The v4 Option brand is private and no public TypeId is exported. + +- `Option.ap` -> `Option.zipWith`: Use Option.zipWith(self, that, (f, a) =\> f(a)); v4 has no Option.ap. + +- `Option.flatMapNullable` -> `Option.flatMapNullishOr`: Renamed with v4 nullish-or terminology. + +- `Option.fromNullable` -> `Option.fromNullishOr`: Renamed with v4 nullish-or terminology. + +- `Option.getEquivalence` -> `Option.makeEquivalence`: Renamed from getEquivalence to makeEquivalence. + +- `Option.getLeft` -> `Option.getFailure`: Either input became Result input, and Left became Failure. + +- `Option.getOrder` -> `Option.makeOrder`: Renamed from getOrder to makeOrder. + +- `Option.getRight` -> `Option.getSuccess`: Either input became Result input, and Right became Success. + +- `Option.liftNullable` -> `Option.liftNullishOr`: Renamed with v4 nullish-or terminology. + +- `Option.orElseEither` -> `Option.orElseResult`: Either was replaced by Result; source tracking now uses Failure and Success. + +### `effect/Order` + +- `Order.Order` -> `Order.Order`: The callable type is retained; its return type remains the -1 | 0 | 1 Ordering union. + +- `Order.all` -> `Order.Tuple([...collection])`: Materialize the comparator iterable for Tuple. V4 evaluates the configured tuple instead of stopping at the shorter input; use Order.make for intentional v3 prefix semantics. + +- `Order.array` -> `Order.Array`: Capitalized constructor name; lexicographic array ordering and the length tie-break are unchanged. + +- `Order.between` -> `Order.isBetween`: Renamed with the v4 is-prefix; inclusive bounds and call forms are unchanged. + +- `Order.bigint` -> `Order.BigInt`: Capitalized instance name; bigint ordering is unchanged. + +- `Order.boolean` -> `Order.Boolean`: Capitalized instance name; false remains ordered before true. + +- `Order.combineAll` -> `Order.combineAll`: Retained with the same left-to-right tie-breaking and empty-iterable result. + +- `Order.combineMany` -> `Order.combine(self, Order.combineAll(collection))`: Compose combine with combineAll; the dedicated dual combineMany helper was removed. + +- `Order.empty` -> `Order.alwaysEqual`: Renamed constructor; call as Order.alwaysEqual\() to produce an order that always returns zero. + +- `Order.greaterThan` -> `Order.isGreaterThan`: Renamed with the v4 is-prefix; curried and uncurried comparisons are retained. + +- `Order.greaterThanOrEqualTo` -> `Order.isGreaterThanOrEqualTo`: Renamed with the v4 is-prefix; curried and uncurried comparisons are retained. + +- `Order.lessThan` -> `Order.isLessThan`: Renamed with the v4 is-prefix; curried and uncurried comparisons are retained. + +- `Order.lessThanOrEqualTo` -> `Order.isLessThanOrEqualTo`: Renamed with the v4 is-prefix; curried and uncurried comparisons are retained. + +- `Order.make` -> `Order.make`: Retained with the same comparator contract and reference-equality fast path. + +- `Order.number` -> `Order.Number`: Capitalized instance name. V4 orders NaN below non-NaN values and all NaNs equally; use a custom Order.make to preserve v3 edge behavior. + +- `Order.product` -> `Order.Tuple([self, that])`: Replace the dual two-order helper with the single-array Tuple constructor. + +- `Order.productMany` -> `Order.Tuple([self, ...collection])`: Materialize the order iterable in one Tuple call; v4 evaluates every configured comparator for short inputs. + +- `Order.reverse` -> `Order.flip`: Direct rename; the replacement reverses comparison by swapping the operands. + +- `Order.string` -> `Order.String`: Capitalized instance name; case-sensitive JavaScript lexicographic ordering is unchanged. + +- `Order.struct` -> `Order.Struct`: Capitalized constructor name; field-order tie-breaking is unchanged. + +- `Order.tuple` -> `Order.Tuple([orderA, orderB, ...])`: Capitalized constructor now takes one comparator array instead of rest arguments and evaluates every configured position. + +### `effect/Ordering` + +- `Ordering.combineAll` -> `Ordering.Reducer.combineAll`: The combination operation moved to the exported Reducer; first-nonzero and empty-input behavior are unchanged. + +- `Ordering.combineMany` -> `Ordering.Reducer.combineAll(Iterable.prepend(collection, self))`: Prepend the initial ordering before reducing to preserve v3 short-circuiting without consuming collection when self is nonzero. + +### `effect/ParseResult` + +#### `ParseResult.ArrayFormatter` + +**Replacement:** `SchemaIssue.makeFormatterStandardSchemaV1` + +Format error.issue with the Standard Schema formatter. + +**Example** + +```ts +SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues +``` + +- `ParseResult.ArrayFormatterIssue` -> `StandardSchemaV1.FailureResult["issues"][number]`: Use the Standard Schema issue shape returned by makeFormatterStandardSchemaV1. + +- `ParseResult.DeclarationDecodeUnknown` -> `SchemaGetter.Getter`: Custom declaration decoding now uses SchemaGetter values and Schema.declare annotations. + +- `ParseResult.DecodeUnknown` -> `Schema.decodeUnknownEffect`: Use the function type returned by Schema.decodeUnknownEffect. + +- `ParseResult.Forbidden` -> `SchemaIssue.Forbidden`: Forbidden failures use the v4 SchemaIssue class; its constructor takes issue annotations plus optional input and parse options, retaining input only when reportInput is true. + +- `ParseResult.Missing` -> `SchemaIssue.MissingKey`: Missing-key failures use the v4 SchemaIssue class. + +- `ParseResult.ParseErrorTypeId` -> `none`: The public symbol was removed; use Schema.isSchemaError for runtime narrowing. + +- `ParseResult.ParseIssue` -> `SchemaIssue.Issue`: The structured parse issue union moved to SchemaIssue. + +- `ParseResult.ParseResultFormatter` -> `SchemaIssue.Formatter`: Issue formatter types moved to SchemaIssue. + +- `ParseResult.Refinement` -> `SchemaIssue.Filter`: Refinement failures are represented as filter issues in v4. + +- `ParseResult.SingleOrNonEmpty` -> `ReadonlyArray`: This ParseResult helper type was removed; use an explicit value-or-non-empty-array type when still needed. + +#### `ParseResult.TreeFormatter` + +**Replacement:** `SchemaIssue.defaultFormatter` + +Use the default SchemaIssue string formatter. + +**Example** + +```ts +SchemaIssue.defaultFormatter(issue) +``` + +- `ParseResult.Type` -> `SchemaIssue.InvalidType`: Type mismatches use the v4 SchemaIssue class. + +- `ParseResult.Unexpected` -> `SchemaIssue.UnexpectedKey`: Unexpected object keys use the v4 SchemaIssue class. + +- `ParseResult.decodeEither` -> `Schema.decodeExit`: Either parsing was replaced by Exit parsing. + +- `ParseResult.decodePromise` -> `Schema.decodePromise`: Parsing helpers moved onto Schema and now fail with SchemaError. + +- `ParseResult.decodeSync` -> `Schema.decodeSync`: Parsing helpers moved onto Schema and now throw SchemaError. + +- `ParseResult.decodeUnknownEither` -> `Schema.decodeUnknownExit`: Either parsing was replaced by Exit parsing. + +- `ParseResult.decodeUnknownPromise` -> `Schema.decodeUnknownPromise`: Parsing helpers moved onto Schema and now reject with SchemaError. + +- `ParseResult.decodeUnknownSync` -> `Schema.decodeUnknownSync`: Parsing helpers moved onto Schema and now throw SchemaError. + +- `ParseResult.eitherOrUndefined` -> `none`: This ParseResult internal optimization was removed; use Effect, Exit, Option, or Result combinators directly. + +- `ParseResult.encodeEither` -> `Schema.encodeExit`: Either encoding was replaced by Exit encoding. + +- `ParseResult.encodeSync` -> `Schema.encodeSync`: Encoding helpers moved onto Schema and now throw SchemaError. + +- `ParseResult.encodeUnknownEither` -> `Schema.encodeUnknownExit`: Either encoding was replaced by Exit encoding. + +- `ParseResult.encodeUnknownSync` -> `Schema.encodeUnknownSync`: Encoding helpers moved onto Schema and now throw SchemaError. + +- `ParseResult.fail` -> `Effect.fail`: Schema transformations now use Effect and fail with SchemaIssue.Issue. + +- `ParseResult.flatMap` -> `Effect.flatMap`: Schema transformations now use Effect combinators. + +- `ParseResult.isComposite` -> `SchemaIssue.Composite`: Narrow with instanceof SchemaIssue.Composite or inspect the issue \_tag. + +- `ParseResult.isParseError` -> `Schema.isSchemaError`: ParseError was replaced by SchemaError. + +- `ParseResult.map` -> `Effect.map`: Schema transformations now use Effect combinators. + +- `ParseResult.orElse` -> `Effect.orElse`: Schema transformations now use Effect combinators. + +#### `ParseResult.parseError` + +**Replacement:** `Schema.SchemaError` + +Construct a SchemaError from a SchemaIssue.Issue. + +**Example** + +```ts +new Schema.SchemaError(issue) +``` + +- `ParseResult.succeed` -> `Effect.succeed`: Schema transformations now use Effect. + +- `ParseResult.try` -> `Effect.try`: Schema transformations now use Effect and map thrown errors to SchemaIssue values. + +#### `ParseResult.validate` + +**Replacement:** `Schema.decodeEffect + Schema.toType` + +Validation-only parsers were removed; decode the type-side schema instead. + +**Example** + +```ts +Schema.decodeEffect(Schema.toType(schema)) +``` + +#### `ParseResult.validateEither` + +**Replacement:** `Schema.decodeExit + Schema.toType` + +Validation-only parsers were removed; decode the type-side schema instead. + +**Example** + +```ts +Schema.decodeExit(Schema.toType(schema)) +``` + +#### `ParseResult.validateOption` + +**Replacement:** `Schema.decodeOption + Schema.toType` + +Validation-only parsers were removed; decode the type-side schema instead. + +**Example** + +```ts +Schema.decodeOption(Schema.toType(schema)) +``` + +#### `ParseResult.validatePromise` + +**Replacement:** `Schema.decodePromise + Schema.toType` + +Validation-only parsers were removed; decode the type-side schema instead. + +**Example** + +```ts +Schema.decodePromise(Schema.toType(schema)) +``` + +#### `ParseResult.validateSync` + +**Replacement:** `Schema.decodeSync + Schema.toType` + +Validation-only parsers were removed; decode the type-side schema instead. + +**Example** + +```ts +Schema.decodeSync(Schema.toType(schema)) +``` + +### `effect/PartitionedSemaphore` + +- `PartitionedSemaphore.PartitionedSemaphore` -> `PartitionedSemaphore.PartitionedSemaphore`: The model remains and now also exposes capacity, available, take, release, withPermit, and conditional permit operations. + +- `PartitionedSemaphore.TypeId` -> `PartitionedSemaphore.PartitionedTypeId`: The public type id was renamed to distinguish it from the regular Semaphore type id. + +### `effect/Pipeable` + +- `Pipeable.PipeableConstructor` -> `Pipeable.PipeableConstructor`: Still exported; its rest arguments are ReadonlyArray\ in v4, so make explicit constructor typings readonly-compatible. + +### `effect/Pool` + +- `Pool.Pool` -> `Pool.Pool`: The model remains but is now Pipeable rather than an Effect subtype; borrow resources explicitly with Pool.get. + +- `Pool.Pool.Variance` -> `none`: The public Pool variance marker was removed; use Pool.Pool directly. + +- `Pool.PoolTypeId` -> `none`: The Pool type id is internal in v4; use Pool.isPool for runtime refinement. + +- `Pool.PoolUnify` -> `none`: Pool is no longer an Effect subtype, so its Effect unification helper was removed; call Pool.get explicitly. + +- `Pool.PoolUnifyIgnore` -> `none`: Pool is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +### `effect/Predicate` + +- `Predicate.Predicate` -> `Predicate.Predicate`: The callable interface is retained. Predicate.Any now uses any rather than never, which can affect generic inference. + +- `Predicate.Refinement` -> `Predicate.Refinement`: The refinement interface and its In, Out, and Any namespace types are retained. + +- `Predicate.all` -> `Predicate.Tuple(Array.from(collection))`: Use positional Tuple, materializing an Iterable when needed. V4 checks every configured position instead of accepting missing input values. + +- `Predicate.every` -> `Predicate.every`: Retained with the same AND semantics, short-circuiting, and true result for an empty collection. + +- `Predicate.isBigInt` -> `Predicate.isBigInt`: Retained with the same bigint refinement. + +- `Predicate.isBoolean` -> `Predicate.isBoolean`: Retained with the same boolean refinement. + +- `Predicate.isDate` -> `Predicate.isDate`: Retained with the same instanceof Date check. + +- `Predicate.isError` -> `Predicate.isError`: Retained with the same instanceof Error check. + +- `Predicate.isFunction` -> `Predicate.isFunction`: Retained with the same function refinement. + +- `Predicate.isIterable` -> `Predicate.isIterable`: Retained; strings and values exposing Symbol.iterator are still accepted. + +- `Predicate.isMap` -> `Predicate.isMap`: Retained with the same instanceof Map check. + +- `Predicate.isNever` -> `Predicate.isNever`: Retained as the always-false refinement. + +- `Predicate.isNotNull` -> `Predicate.isNotNull`: Retained; undefined still passes while null is excluded. + +- `Predicate.isNotNullable` -> `Predicate.isNotNullish`: Renamed to use nullish terminology; it still excludes null and undefined. + +- `Predicate.isNotUndefined` -> `Predicate.isNotUndefined`: Retained; null still passes while undefined is excluded. + +- `Predicate.isNull` -> `Predicate.isNull`: Retained with the same strict null refinement. + +- `Predicate.isNullable` -> `Predicate.isNullish`: Renamed to use nullish terminology. The guard now narrows with A & (null | undefined), including unknown inputs correctly. + +- `Predicate.isNumber` -> `Predicate.isNumber`: Retained; NaN and infinite numbers still pass. + +- `Predicate.isObject` -> `Predicate.isObjectKeyword`: Use isObjectKeyword to preserve v3 behavior accepting arrays and functions. V4 isObject has the former record-like semantics instead. + +- `Predicate.isPromise` -> `Predicate.isPromise`: Retained as the structural check for callable then and catch properties. + +- `Predicate.isPromiseLike` -> `Predicate.isPromiseLike`: Retained as the structural check for a callable then property. + +- `Predicate.isReadonlyRecord` -> `Predicate.isReadonlyObject`: Renamed; runtime behavior is unchanged and the index-key type now explicitly includes numbers. + +- `Predicate.isRecord` -> `Predicate.isObject`: Renamed; it still accepts non-null, non-array objects and now narrows with PropertyKey indexes. + +- `Predicate.isRegExp` -> `Predicate.isRegExp`: Retained with the same instanceof RegExp check. + +- `Predicate.isSet` -> `Predicate.isSet`: Retained with the same instanceof Set check. + +- `Predicate.isString` -> `Predicate.isString`: Retained with the same primitive string refinement. + +- `Predicate.isSymbol` -> `Predicate.isSymbol`: Retained with the same symbol refinement. + +- `Predicate.isTruthy` -> `Predicate.isTruthy`: Retained as a plain boolean predicate using JavaScript truthiness. + +- `Predicate.isUint8Array` -> `Predicate.isUint8Array`: Retained with the same instanceof Uint8Array check. + +- `Predicate.isUndefined` -> `Predicate.isUndefined`: Retained with the same strict undefined refinement. + +- `Predicate.isUnknown` -> `Predicate.isUnknown`: Retained as the always-true refinement. + +- `Predicate.not` -> `Predicate.not`: Retained with the same boolean negation; refinements still become plain predicates. + +- `Predicate.product` -> `Predicate.Tuple([self, that])`: Replace the two-position product helper with the Tuple constructor. + +- `Predicate.productMany` -> `Predicate.Tuple([self, ...Array.from(collection)])`: Materialize the predicate iterable in one Tuple call; v4 checks missing tail positions as undefined. + +- `Predicate.some` -> `Predicate.some`: Retained with the same OR semantics, short-circuiting, and false result for an empty collection. + +- `Predicate.struct` -> `Predicate.Struct`: Capitalized constructor name; field checks and refinement-aware typing are retained. + +- `Predicate.tuple` -> `Predicate.Tuple([p1, p2, ...])`: Capitalized constructor now takes one predicate array instead of rest arguments and checks missing positions as undefined. + +### `effect/Pretty` + +- `Pretty.Pretty` -> `Formatter.Formatter`: The formatter function type is now exported by Formatter. + +- `Pretty.PrettyAnnotation` -> `Schema.Annotations.ToFormatter.Declaration`: Custom declaration formatter annotations now use the toFormatter key in Schema.Annotations. + +#### `Pretty.make` + +**Replacement:** `Schema.toFormatter` + +Formatter derivation moved onto Schema. + +**Example** + +```ts +Schema.toFormatter(schema) +``` + +- `Pretty.match` -> `Schema.toFormatter`: The compiler match table was removed; customize traversal with the toFormatter onBefore option. + +### `effect/PubSub` + +- `PubSub.PubSub` -> `PubSub.PubSub`: The model remains but no longer extends Queue.Enqueue; replace Queue operations with explicit PubSub.publish, PubSub.publishAll, and PubSub.subscribe calls. + +### `effect/Queue` + +- `Queue.BackingQueue` -> `none`: Custom backing queues were removed by the v4 Queue rewrite; use Queue.make and its built-in capacity and strategy options. + +- `Queue.BackingQueueTypeId` -> `none`: BackingQueue and its public type id were removed. + +- `Queue.BaseQueue` -> `Queue.Enqueue | Queue.Dequeue`: The shared BaseQueue interface was removed; accept the required enqueue or dequeue capability and call Queue operations explicitly. + +- `Queue.Dequeue` -> `Queue.Dequeue`: The model remains and gains an error parameter, but is no longer an Effect subtype; use Queue.take explicitly. + +- `Queue.DequeueTypeId` -> `Queue.isDequeue`: The dequeue type id is internal in v4; use Queue.isDequeue for runtime refinement. + +- `Queue.DequeueUnify` -> `none`: Queue.Dequeue is no longer an Effect subtype, so its Effect unification helper was removed. + +- `Queue.DequeueUnifyIgnore` -> `none`: Queue.Dequeue is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `Queue.Enqueue` -> `Queue.Enqueue`: The write-side model remains, gains an error parameter, and is operated through Queue.offer and related functions. + +- `Queue.EnqueueTypeId` -> `Queue.isEnqueue`: The enqueue type id is internal in v4; use Queue.isEnqueue for runtime refinement. + +- `Queue.Queue` -> `Queue.Queue`: The model remains, gains an error parameter and completion signaling, and is no longer an Effect subtype; use Queue.take explicitly. + +- `Queue.Queue.BackingQueueVariance` -> `none`: BackingQueue and its variance marker were removed by the v4 Queue rewrite. + +- `Queue.Queue.DequeueVariance` -> `Queue.Dequeue.Variance`: The read-side variance marker moved under the Queue.Dequeue namespace and now includes the error type. + +- `Queue.Queue.EnqueueVariance` -> `Queue.Enqueue.Variance`: The write-side variance marker moved under the Queue.Enqueue namespace and now includes the error type. + +- `Queue.Queue.StrategyVariance` -> `none`: Public Strategy values and their variance marker were removed; select a string strategy when constructing the Queue. + +- `Queue.QueueStrategyTypeId` -> `none`: Public Strategy values and their type id were removed. + +- `Queue.QueueUnify` -> `none`: Queue is no longer an Effect subtype, so its Effect unification helper was removed; call Queue.take explicitly. + +- `Queue.QueueUnifyIgnore` -> `none`: Queue is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `Queue.Strategy` -> `"suspend" | "dropping" | "sliding"`: The pluggable Strategy interface was removed; choose one of the built-in strategy strings in Queue.make. + +- `Queue.awaitShutdown` -> `Queue.await`: Queue completion now includes normal end and failure; Queue.await waits for Done and propagates non-Done terminal causes. + +- `Queue.backPressureStrategy` -> `Queue.make({ strategy: "suspend" })`: Strategies are now constructor options rather than public Strategy values; suspend is the default. + +- `Queue.capacity` -> `queue.capacity`: Capacity is now a property on Queue.Enqueue and Queue.Dequeue rather than a module function. + +- `Queue.droppingStrategy` -> `Queue.make({ strategy: "dropping" })`: Strategies are now constructor options rather than public Strategy values; Queue.dropping is the bounded convenience constructor. + +- `Queue.isEmpty` -> `Effect.map(Queue.size(self), (size) => size === 0)`: The dedicated helper was removed; derive emptiness from Queue.size. + +- `Queue.isShutdown` -> `queue.state._tag === "Done"`: The dedicated helper was removed; inspect the public queue lifecycle state. Done includes normal completion and failure, not only explicit shutdown. + +- `Queue.slidingStrategy` -> `Queue.make({ strategy: "sliding" })`: Strategies are now constructor options rather than public Strategy values; Queue.sliding is the bounded convenience constructor. + +- `Queue.takeUpTo` -> `Queue.poll`: No direct bounded batch helper remains; repeatedly call non-blocking Queue.poll up to the limit, or use Queue.clear when taking every buffered value is acceptable. + +- `Queue.unsafeOffer` -> `Queue.offerUnsafe`: The unsafe suffix moved to the end. + +### `effect/Random` + +- `Random.Random` -> `Random.Random`: The context key is now a Context.Reference whose low-level service only has nextIntUnsafe and nextDoubleUnsafe. Prefer module operations; custom providers implement those two primitives. + +- `Random.RandomTypeId` -> `none`: The service is structural and no longer carries a public RandomTypeId brand. + +- `Random.fixed` -> `Effect.provideService(Random.Random, customRandom)`: No exact built-in equivalent remains. For deterministic tests, provide a cycling service implementing nextIntUnsafe and nextDoubleUnsafe; map non-number sequences explicitly. + +- `Random.make` -> `Random.withSeed`: Replace service construction and withRandom with Random.withSeed(seed)(program). V4 accepts string or number, returns an Effect transformation, and uses a different PRNG, so sequences are not v3-compatible. + +- `Random.nextRange` -> `Random.nextBetween`: Direct rename; both produce a floating-point value in the half-open range [min, max). + +- `Random.randomWith` -> `Random.Random.use`: Use Random.Random.use for raw service access. Prefer replacing callbacks that selected an old method with the corresponding module-level Random operation. + +### `effect/RcMap` + +- `RcMap.RcMap` -> `RcMap.RcMap`: The model remains as a Pipeable reference-counted resource map; use RcMap.get explicitly inside a Scope. + +- `RcMap.RcMap.Variance` -> `none`: The public variance marker was removed; use RcMap.RcMap directly. + +- `RcMap.TypeId` -> `none`: The RcMap type id is internal in v4; do not inspect or construct the brand directly. + +### `effect/RcRef` + +- `RcRef.RcRef` -> `RcRef.RcRef`: The model remains but is now only Pipeable; replace yielding or reading the RcRef directly with RcRef.get in a Scope. + +- `RcRef.RcRefUnify` -> `none`: RcRef is no longer an Effect subtype, so its Effect unification helper was removed; call RcRef.get explicitly. + +- `RcRef.RcRefUnifyIgnore` -> `none`: RcRef is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `RcRef.TypeId` -> `none`: The RcRef type id is internal in v4; do not inspect or construct the brand directly. + +### `effect/Readable` + +- `Readable.Readable` -> `Effect.Effect`: The branded wrapper was removed; represent read access directly as Effect.Effect\. + +- `Readable.TypeId` -> `Effect.TypeId`: The Readable brand was removed; use Effect.TypeId only when branding checks remain necessary after collapsing to Effect. + +- `Readable.isReadable` -> `Effect.isEffect`: Readable was removed; after representing reads directly as Effect, use the Effect guard. + +- `Readable.make` -> `Effect.Effect`: Use the supplied Effect directly; the v3 constructor only wrapped it as a get property. + +- `Readable.map` -> `Effect.map`: Represent Readable as Effect and map it directly. + +- `Readable.mapEffect` -> `Effect.flatMap`: Represent Readable as Effect and flatMap it directly. + +- `Readable.unwrap` -> `Effect.flatten`: After replacing the inner Readable with Effect, flatten the nested Effect directly. + +### `effect/Record` + +- `Record.ReadonlyRecord` -> `Record.ReadonlyRecord`: The public type and parameter order are unchanged. + +- `Record.ReadonlyRecord.IsFiniteString` -> `Record.ReadonlyRecord.IsFiniteString`: The namespace utility type is unchanged. + +- `Record.getEquivalence` -> `Record.makeEquivalence`: Direct rename; pass the value equivalence unchanged. + +- `Record.getLefts` -> `Record.getFailures`: Extract Result.Failure values while preserving keys. + +- `Record.getRights` -> `Record.getSuccesses`: Extract Result.Success values while preserving keys. + +- `Record.modifyOption` -> `Record.modify`: The Option suffix was dropped; missing keys still return Option.none. + +- `Record.partitionMap` -> `Record.partition`: Pass a mapper returning Result; failures and successes form the two output records. + +- `Record.replaceOption` -> `Record.replace`: The Option suffix was dropped; missing keys still return Option.none. + +### `effect/RedBlackTree` + +- `RedBlackTree.Direction` -> `none`: The tree direction type was removed; use normal array order or Array.reverse. + +- `RedBlackTree.RedBlackTree` -> `ReadonlyArray`: The core tree was removed; use sorted immutable entries for small collections or an external persistent ordered multimap when complexity or duplicate-key semantics matter. + +- `RedBlackTree.RedBlackTree.Direction` -> `none`: The nested direction type was removed; use normal array order or Array.reverse. + +- `RedBlackTree.TypeId` -> `none`: The RedBlackTree module and brand symbol were removed. + +- `RedBlackTree.at` -> `Array.drop`: Represent the removed tree as sorted entries; for a non-negative index, Array.drop(entries, index) traverses forward from that absolute position. + +- `RedBlackTree.atReversed` -> `Array.take + Array.reverse`: For a valid absolute index, reverse Array.take(entries, index + 1) to traverse backward from it. + +- `RedBlackTree.empty` -> `Array.empty`: The module was removed; use an empty Array\ and retain the Order separately. + +- `RedBlackTree.first` -> `Array.head`: On a sorted entry array, Array.head returns the same optional minimum entry. + +- `RedBlackTree.forEachBetween` -> `Array.filter + Array.forEach`: Filter sorted entries to min \<= key \< max with the retained Order, then visit them with Array.forEach. + +- `RedBlackTree.forEachGreaterThanEqual` -> `Array.filter + Array.forEach`: Filter sorted entries to key \>= min with the retained Order, then visit them in ascending order. + +- `RedBlackTree.forEachLessThan` -> `Array.filter + Array.forEach`: Filter sorted entries to key \< max with the retained Order, then visit them in ascending order. + +- `RedBlackTree.fromIterable` -> `Array.sortWith`: Sort the entry iterable by key and retain the Order separately; this does not preserve logarithmic tree operations. + +- `RedBlackTree.getAt` -> `Array.get`: Array.get on sorted entries preserves the optional index lookup behavior. + +- `RedBlackTree.getOrder` -> `none`: No replacement collection stores an Order; retain and pass the Order explicitly. + +- `RedBlackTree.greaterThan` -> `Array.filter`: Filter sorted entries with the retained Order for key \> bound. + +- `RedBlackTree.greaterThanEqual` -> `Array.filter`: Filter sorted entries with the retained Order for key \>= bound. + +- `RedBlackTree.greaterThanEqualReversed` -> `Array.filter + Array.reverse`: Filter sorted entries with the retained Order for key \>= bound, then reverse for descending traversal. + +- `RedBlackTree.greaterThanReversed` -> `Array.filter + Array.reverse`: Filter sorted entries with the retained Order for key \> bound, then reverse for descending traversal. + +- `RedBlackTree.has` -> `Array.some`: Use Array.some on sorted entries with Equal.equals for key membership; this is linear rather than logarithmic. + +- `RedBlackTree.insert` -> `Array.prepend + Array.sortWith`: Prepend the entry and sort by key to preserve newest-first comparator ties; use an external ordered multimap if logarithmic updates matter. + +- `RedBlackTree.isRedBlackTree` -> `Array.isArray`: The brand was removed; Array.isArray only checks the replacement representation and cannot prove its sorted invariant. + +- `RedBlackTree.keys` -> `Array.map`: Map sorted entries to keys and iterate the resulting array. + +- `RedBlackTree.keysReversed` -> `Array.reverse + Array.map`: Reverse sorted entries, map them to keys, and iterate the resulting array. + +- `RedBlackTree.last` -> `Array.last`: On a sorted entry array, Array.last returns the same optional maximum entry. + +- `RedBlackTree.lessThan` -> `Array.filter`: Filter sorted entries with the retained Order for key \< bound. + +- `RedBlackTree.lessThanEqual` -> `Array.filter`: Filter sorted entries with the retained Order for key \<= bound. + +- `RedBlackTree.lessThanEqualReversed` -> `Array.filter + Array.reverse`: Filter sorted entries with the retained Order for key \<= bound, then reverse for descending traversal. + +- `RedBlackTree.lessThanReversed` -> `Array.filter + Array.reverse`: Filter sorted entries with the retained Order for key \< bound, then reverse for descending traversal. + +- `RedBlackTree.make` -> `Array.sortWith`: Sort the supplied entries by key and retain the Order separately; this is not a balanced tree. + +- `RedBlackTree.reduce` -> `Array.reduce`: Reduce sorted entries in ascending order, adapting the callback to receive [key, value]. + +- `RedBlackTree.removeFirst` -> `Array.findFirstIndex + Array.remove`: Find the first entry whose key is Equal.equals to the target, then remove that index; leave the array unchanged when absent. + +- `RedBlackTree.reversed` -> `Array.reverse`: Reverse the sorted entry array for descending traversal. + +- `RedBlackTree.size` -> `Array.length`: Use Array.length or the .length property on the replacement entry array. + +- `RedBlackTree.values` -> `Array.map`: Map sorted entries to values and iterate the resulting array to preserve key order. + +- `RedBlackTree.valuesReversed` -> `Array.reverse + Array.map`: Reverse sorted entries, map them to values, and iterate to preserve reverse key order. + +### `effect/Redacted` + +- `Redacted.Redacted` -> `Redacted.Redacted`: The sensitive-value wrapper remains and now optionally carries a label. + +- `Redacted.Redacted.Variance` -> `Redacted.Redacted.Variance`: The type-level variance member remains. + +- `Redacted.RedactedTypeId` -> `Redacted.isRedacted`: The marker is private in v4; use the public guard for runtime narrowing. + +- `Redacted.getEquivalence` -> `Redacted.makeEquivalence`: Renamed to the v4 make-prefix convention. + +- `Redacted.unsafeWipe` -> `Redacted.wipeUnsafe`: Renamed to use the v4 Unsafe suffix convention. + +### `effect/Ref` + +- `Ref.Ref` -> `Ref.Ref`: The model remains but is now Pipeable rather than an Effect or Readable subtype; read it explicitly with Ref.get. + +- `Ref.Ref.Variance` -> `Ref.Ref.Variance`: The marker remains under Ref.Ref, but its brand uses an internal type id; ordinary code should use Ref.Ref directly. + +- `Ref.RefTypeId` -> `none`: The Ref type id is internal in v4; do not inspect or construct the brand directly. + +- `Ref.RefUnify` -> `none`: Ref is no longer an Effect subtype, so its Effect unification helper was removed; call Ref.get explicitly. + +- `Ref.RefUnifyIgnore` -> `none`: Ref is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `Ref.getAndSet` -> `Ref.getAndSet`: The operation remains with data-first and data-last forms. + +- `Ref.getAndUpdate` -> `Ref.getAndUpdate`: The operation remains with data-first and data-last forms. + +- `Ref.getAndUpdateSome` -> `Ref.getAndUpdateSome`: The operation remains; Option.none leaves the value unchanged. + +- `Ref.modify` -> `Ref.modify`: The operation remains with data-first and data-last forms. + +- `Ref.set` -> `Ref.set`: The operation remains with data-first and data-last forms. + +- `Ref.setAndGet` -> `Ref.setAndGet`: The operation remains with data-first and data-last forms. + +- `Ref.unsafeMake` -> `Ref.makeUnsafe`: The unsafe suffix moved to the end. + +- `Ref.update` -> `Ref.update`: The operation remains with data-first and data-last forms. + +- `Ref.updateAndGet` -> `Ref.updateAndGet`: The operation remains with data-first and data-last forms. + +- `Ref.updateSome` -> `Ref.updateSome`: The operation remains; Option.none leaves the value unchanged. + +- `Ref.updateSomeAndGet` -> `Ref.updateSomeAndGet`: The operation remains; Option.none leaves the value unchanged and returns the current value. + +### `effect/Reloadable` + +- `Reloadable.Reloadable` -> `LayerRef.LayerRef`: LayerRef is the v4 refreshable layer-context abstraction. + +- `Reloadable.Reloadable.Variance` -> `none`: The exported variance artifact was removed and LayerRef has no public counterpart. + +- `Reloadable.ReloadableTypeId` -> `none`: Reloadable was removed and LayerRef's marker is private. + +- `Reloadable.auto` -> `LayerRef.Service(..., { layer, invalidationSchedule: schedule, preload: true }).layer`: Use LayerRef for scheduled refresh; add idleTimeToLive: Duration.infinity to preserve an always-resident instance. + +- `Reloadable.autoFromConfig` -> `Layer.unwrap with Effect.contextWith and LayerRef.make`: Compute the schedule from the current context, then construct a preloaded LayerRef; no config-specific constructor remains. + +- `Reloadable.get` -> `ServiceRef.get or Effect.map(ServiceRef.contextEffect, Context.get(Service))`: LayerRef.get provides the current context as a layer; contextEffect gives scoped direct access. + +- `Reloadable.manual` -> `LayerRef.Service(..., { layer, preload: true }).layer`: Refresh with the generated service's refresh effect; use infinite idleTimeToLive for v3's resident lifecycle. + +- `Reloadable.reload` -> `ServiceRef.refresh`: Refresh invalidates and immediately reacquires; invalidate alone rebuilds on the next borrow. + +- `Reloadable.reloadFork` -> `ServiceRef.refresh.pipe(Effect.ignore({ log: true }), Effect.forkDetach({ startImmediately: true }), Effect.asVoid)`: This recreates logged, ignored background refresh; forkDaemon became forkDetach. + +- `Reloadable.tag` -> `LayerRef.Service()(id, options)`: The generated LayerRef service class is itself the Context.Service key. + +### `effect/Request` + +- `Request.Cache` -> `RequestResolver.asCache`: The runtime request cache type was removed; expose resolver results through a first-class Cache, or use RequestResolver.withCache to retain a resolver. + +- `Request.Entry` -> `Request.Entry`: Entry remains but now carries request, context, uninterruptible, and completeUnsafe fields; Deferred, listener, owner, and state fields were removed. + +- `Request.EntryTypeId` -> `none`: Request entries are unbranded structural values in v4; do not inspect or construct an entry type id. + +- `Request.Listeners` -> `none`: Request listener accounting is no longer public; cancellation and shared request lifecycle are managed by the v4 runtime and resolver caching. + +- `Request.Request` -> `Request.Request`: The request model remains and adds a third R parameter for services required while resolving the request. + +- `Request.Request.OptionalResult` -> `Exit.Exit>, Request.Error>`: The named alias was removed; write the optional request exit type directly when it is still required. + +- `Request.RequestTypeId` -> `none`: The request type id is internal in v4; define requests by extending Request.Request or with Request.Class and do not depend on branding internals. + +- `Request.interruptWhenPossible` -> `none`: Request cancellation is managed by the v4 batching runtime; resolver code should complete the entries it receives and not wrap work with this internal listener helper. + +- `Request.isEntry` -> `none`: The entry guard was removed; entries are supplied structurally to RequestResolver callbacks. + +- `Request.makeCache` -> `RequestResolver.asCache`: Create a cache from a resolver with capacity and timeToLive options, or use RequestResolver.withCache for a cached resolver. + +### `effect/RequestBlock` + +- `RequestBlock.Empty` -> `none`: The public blocked-request graph was removed; application code should compose Effect.request computations instead of inspecting Empty nodes. + +- `RequestBlock.Par` -> `none`: The public blocked-request graph was removed; express parallel request execution with Effect concurrency combinators. + +- `RequestBlock.RequestBlock` -> `none`: RequestBlock is no longer public in v4; compose Effect.request values directly and let the runtime batch requests by resolver. + +- `RequestBlock.Seq` -> `none`: The public blocked-request graph was removed; express sequencing in the Effect program instead of constructing Seq nodes. + +- `RequestBlock.empty` -> `Effect.void`: RequestBlock was removed; represent an empty computation as Effect.void and let Effect.request perform batching. + +- `RequestBlock.mapRequestResolvers` -> `Effect.request`: Pass the selected resolver to each Effect.request call; the runtime request graph can no longer be traversed to rewrite resolvers. + +- `RequestBlock.parallel` -> `Effect.all`: Compose request effects with Effect.all and explicit concurrency; v4 batching is performed by resolver and batch key rather than RequestBlock nodes. + +- `RequestBlock.reduce` -> `none`: The public blocked-request graph and reducer were removed; structure analysis is now internal to the request runtime. + +- `RequestBlock.sequential` -> `Effect.andThen`: Sequence request effects with Effect.andThen, flatMap, or generator syntax; RequestBlock sequencing nodes were removed. + +- `RequestBlock.single` -> `Effect.request`: Construct the request effect directly with its Request value and RequestResolver; the runtime creates pending entries internally. + +### `effect/RequestResolver` + +- `RequestResolver.RequestResolver` -> `RequestResolver.RequestResolver`: The interface remains as RequestResolver\; remove its R parameter and move service requirements to Request\. + +- `RequestResolver.RequestResolver.Variance` -> `RequestResolver.RequestResolver.Variance`: The variance marker remains but tracks only the accepted Request type; resolver environment variance was removed. + +- `RequestResolver.RequestResolverTypeId` -> `none`: The resolver type id is internal in v4; use RequestResolver constructors and isRequestResolver rather than depending on its brand. + +- `RequestResolver.aroundRequests` -> `RequestResolver.around`: around now receives Request.Entry batches; map entries to entry.request in before and after when hooks need raw request values. + +- `RequestResolver.contextFromEffect` -> `Request.Request`: Resolvers no longer carry an environment parameter; declare R on each Request and use entry.context inside the resolver callback. + +- `RequestResolver.contextFromServices` -> `Request.Request`: Declare the selected services in the Request R parameter and read them from each entry.context; resolver-level context capture was removed. + +- `RequestResolver.eitherWith` -> `RequestResolver.fromEffectTagged`: Define one resolver for the combined tagged request union, or use RequestResolver.make to partition entries manually; resolver routing combinators were removed. + +- `RequestResolver.locally` -> `Effect.provideService`: FiberRef-based resolver localization was removed; migrate the FiberRef to Context.Reference and provide its value around the request effect or resolver work. + +- `RequestResolver.makeBatched` -> `RequestResolver.make`: make now receives a non-empty batch of Request.Entry values; read entry.request and complete every entry with completeUnsafe or Request completion helpers. + +- `RequestResolver.makeWithEntry` -> `RequestResolver.make`: Use make for entry-level handling; v4 supplies one non-empty batch and key instead of nested sequential and parallel entry arrays. + +- `RequestResolver.mapInputContext` -> `Request.Request`: Resolver environments were removed; put required services on the Request R parameter and transform or provide each entry.context explicitly when needed. + +- `RequestResolver.provideContext` -> `Effect.provideService`: Provide services to Effect.request so they are captured in entry.context; RequestResolver itself no longer has an environment parameter. + +### `effect/Resource` + +- `Resource.Resource` -> `Resource.Resource`: The type remains but no longer extends Effect; use Resource.get(resource). + +- `Resource.Resource.Variance` -> `none`: The exported variance artifact was removed. + +- `Resource.ResourceTypeId` -> `Resource.isResource`: The marker is private; use the public runtime guard. + +- `Resource.ResourceUnify` -> `none`: Resource no longer extends Effect; use Resource.get explicitly. + +- `Resource.ResourceUnifyIgnore` -> `none`: The Effect-unification implementation detail was removed. + +### `effect/Runtime` + +- `Runtime.AsyncFiberException` -> `Cause.AsyncFiberError`: The error thrown when synchronous execution encounters an async boundary was renamed. + +- `Runtime.Cancel` -> `ReturnType`: Use the cancellation function returned by runCallback; the named type was removed. + +- `Runtime.FiberFailure` -> `none`: The runner error wrapper was removed; use an Exit-returning runner to retain and inspect a structured Cause. + +- `Runtime.FiberFailureCauseId` -> `none`: The FiberFailure wrapper and its cause marker were removed; inspect Cause through Exit instead. + +- `Runtime.FiberFailureId` -> `none`: The FiberFailure wrapper and its brand were removed; inspect Cause through Exit instead. + +- `Runtime.RunCallbackOptions` -> `Effect.RunOptions & { readonly onExit: (exit: Exit.Exit) => void }`: The callback runner now combines Effect.RunOptions with an onExit callback; no named options type is exported. + +- `Runtime.RunForkOptions` -> `Effect.RunOptions`: Use common runner options; express scoped forking with Effect.forkIn or Effect.forkScoped. + +- `Runtime.Runtime` -> `Context.Context`: Runtime values were removed; carry a Context and invoke the corresponding Effect.run\*With function. + +- `Runtime.Runtime.Context` -> `none`: The Runtime context extractor was removed; carry the service union directly on Context.Context. + +- `Runtime.defaultRuntime` -> `Context.empty()`: Runtime values were removed; call Effect.run\* directly or use an empty Context with an Effect.run\*With function. + +- `Runtime.defaultRuntimeFlags` -> `none`: Runtime flags were removed; configure scheduler yielding, interruptibility, and runtime metrics independently. + +- `Runtime.deleteFiberRef` -> `Context.omit`: FiberRefs became Context.Reference values; omit the Reference override from the Context. + +- `Runtime.disableRuntimeFlag` -> `none`: Runtime flags were removed; disable the corresponding scheduler, interruptibility, or metric behavior directly. + +- `Runtime.enableRuntimeFlag` -> `none`: Runtime flags were removed; enable the corresponding scheduler, interruptibility, or metric behavior directly. + +- `Runtime.isAsyncFiberException` -> `Cause.isAsyncFiberError`: Use the renamed guard from Cause. + +- `Runtime.isFiberFailure` -> `none`: FiberFailure no longer exists; use an Exit-returning runner and inspect Exit or Cause. + +- `Runtime.make` -> `Context.make`: Runtime values were removed; construct the service Context passed to Effect.run\*With instead. + +- `Runtime.makeFiberFailure` -> `Cause.squash`: Use Cause.squash only when a Cause must become the value thrown or rejected by a runner. + +- `Runtime.runCallback` -> `Effect.runCallbackWith`: Run with the former Runtime's Context; use Effect.runCallback when no services are required. + +- `Runtime.runFork` -> `Effect.runForkWith`: Run with the former Runtime's Context; use Effect.runFork when no services are required. + +- `Runtime.runPromise` -> `Effect.runPromiseWith`: Run with the former Runtime's Context; use Effect.runPromise when no services are required. + +- `Runtime.runPromiseExit` -> `Effect.runPromiseExitWith`: Run with the former Runtime's Context; use Effect.runPromiseExit when no services are required. + +- `Runtime.runSync` -> `Effect.runSyncWith`: Run with the former Runtime's Context; use Effect.runSync when no services are required. + +- `Runtime.runSyncExit` -> `Effect.runSyncExitWith`: Run with the former Runtime's Context; use Effect.runSyncExit when no services are required. + +- `Runtime.setFiberRef` -> `Context.add`: FiberRefs became Context.Reference values; add the Reference override to the Context. + +- `Runtime.updateContext` -> `Context transformation + Effect.run*With`: Runtime values were removed; transform the carried Context directly, then pass the result to the corresponding Effect.run\*With function. + +- `Runtime.updateFiberRefs` -> `Context.add`: There is no aggregate FiberRefs update; add targeted Context.Reference overrides to the carried Context explicitly. + +- `Runtime.updateRuntimeFlags` -> `none`: Runtime flags and aggregate patches were removed; configure each semantic behavior independently. + +### `effect/RuntimeFlags` + +- `RuntimeFlags.CooperativeYielding` -> `References.PreventSchedulerYield`: Use the scheduler Reference with inverse boolean meaning. + +- `RuntimeFlags.Interruption` -> `Effect.interruptible | Effect.uninterruptible`: The bit flag was removed; control interruptibility with Effect regions. + +- `RuntimeFlags.None` -> `none`: The empty runtime-flags value and its type were removed. + +- `RuntimeFlags.OpSupervision` -> `none`: Operation supervision and its runtime flag were removed. + +- `RuntimeFlags.RuntimeFlag` -> `none`: Individual bit flags were removed; use the corresponding semantic API. + +- `RuntimeFlags.RuntimeFlags` -> `none`: The aggregate runtime-flags bitset was removed. + +- `RuntimeFlags.RuntimeMetrics` -> `Metric.FiberRuntimeMetrics`: Runtime metrics are now configured through a Context.Reference service rather than a bit flag. + +- `RuntimeFlags.WindDown` -> `none`: The wind-down flag is no longer public. + +- `RuntimeFlags.cooperativeYielding` -> `!References.PreventSchedulerYield`: Read the scheduler Reference and negate it; the aggregate flags value was removed. + +- `RuntimeFlags.diff` -> `none`: The runtime-flags bitset was removed; configure each semantic behavior directly. + +- `RuntimeFlags.differ` -> `none`: The runtime-flags bitset and patch differ were removed. + +- `RuntimeFlags.disable` -> `none`: The generic flag operation was removed; disable the corresponding behavior directly. + +- `RuntimeFlags.disableAll` -> `none`: The aggregate flags value was removed; configure scheduler yielding, interruptibility, and metrics independently. + +- `RuntimeFlags.disableCooperativeYielding` -> `Effect.provideService(References.PreventSchedulerYield, true)`: Prevent scheduler yielding through its Context.Reference. + +- `RuntimeFlags.disableInterruption` -> `Effect.uninterruptible`: Use an uninterruptible region instead of changing a runtime flag. + +- `RuntimeFlags.disableOpSupervision` -> `none`: Operation supervision and its runtime flag were removed. + +- `RuntimeFlags.disableRuntimeMetrics` -> `Metric.disableRuntimeMetrics`: Disable fiber runtime metrics directly; use disableRuntimeMetricsLayer when providing a Layer. + +- `RuntimeFlags.disableWindDown` -> `none`: The wind-down flag is runtime-internal in v4; use normal scoped finalizers and explicit interruptibility regions. + +- `RuntimeFlags.enable` -> `none`: The generic flag operation was removed; enable the corresponding behavior directly. + +- `RuntimeFlags.enableAll` -> `none`: The aggregate flags value was removed; configure scheduler yielding, interruptibility, and metrics independently. + +- `RuntimeFlags.enableCooperativeYielding` -> `Effect.provideService(References.PreventSchedulerYield, false)`: Allow scheduler yielding through its Context.Reference. + +- `RuntimeFlags.enableInterruption` -> `Effect.interruptible`: Use an interruptible region instead of changing a runtime flag. + +- `RuntimeFlags.enableOpSupervision` -> `none`: Operation supervision and its runtime flag were removed. + +- `RuntimeFlags.enableRuntimeMetrics` -> `Metric.enableRuntimeMetrics`: Enable fiber runtime metrics directly; use enableRuntimeMetricsLayer when providing a Layer. + +- `RuntimeFlags.enableWindDown` -> `none`: The wind-down flag is runtime-internal in v4; use normal scoped finalizers and explicit interruptibility regions. + +- `RuntimeFlags.interruptible` -> `none`: There is no public current-interruptibility getter; structure the program with Effect.interruptible or Effect.uninterruptible. + +- `RuntimeFlags.interruption` -> `none`: Interruptibility is controlled by Effect regions rather than queried from a flags value. + +- `RuntimeFlags.isDisabled` -> `none`: There is no aggregate flags value to query; inspect or control the corresponding semantic facility. + +- `RuntimeFlags.make` -> `none`: The runtime-flags bitset was removed; do not recreate it in v4. + +- `RuntimeFlags.none` -> `none`: The runtime-flags bitset was removed; configure each semantic behavior independently. + +- `RuntimeFlags.opSupervision` -> `none`: Operation supervision and its runtime flag were removed. + +- `RuntimeFlags.patch` -> `none`: Aggregate runtime-flags patches were removed; configure each semantic behavior directly. + +- `RuntimeFlags.render` -> `none`: The runtime-flags bitset and its renderer were removed. + +- `RuntimeFlags.runtimeMetrics` -> `Metric.FiberRuntimeMetrics`: Read the Context.Reference and test for undefined instead of querying a bit flag. + +- `RuntimeFlags.toSet` -> `none`: The runtime-flags bitset was removed; there is no set conversion. + +- `RuntimeFlags.windDown` -> `none`: The wind-down flag is no longer public. + +### `effect/STM` + +- `STM.Adapter` -> `none`: The STM.gen adapter was removed; Effect.gen accepts yielded Effects directly. + +- `STM.All.IsDiscard` -> `Effect.All.IsDiscard`: The helper moved to Effect.All because STM.all is now Effect.all. + +- `STM.All.Narrow` -> `none`: Effect.all uses a const generic directly, so the separate tuple-narrowing helper was removed. + +- `STM.All.Options` -> `none`: Effect.all inlines its options type; use its concurrency, discard, and mode options directly. + +- `STM.All.STMAny` -> `Effect.All.EffectAny`: STM inputs are ordinary Effects in v4, so use the Effect.All helper. + +- `STM.All.Signature` -> `typeof Effect.all`: The named STM all signature was removed; refer to Effect.all directly. + +- `STM.Do` -> `Effect.Do`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.STM` -> `Effect.Effect`: The distinct STM instruction type was removed. Tx APIs return Effect values; wrap the complete transaction in Effect.tx. + +- `STM.STM.Variance` -> `Effect.Variance`: The distinct STM variance marker was removed with STM; use the Effect marker. + +- `STM.STMTypeId` -> `Effect.TypeId`: The distinct STM type id was removed because transactions are represented by Effect values. + +- `STM.STMTypeLambda` -> `Effect.EffectTypeLambda`: Use the Effect type lambda; transactional requirements are represented by Effect.Transaction. + +- `STM.STMUnify` -> `Effect.EffectUnify`: STM unification moved to ordinary Effect unification. + +- `STM.STMUnifyIgnore` -> `none`: The STM-specific unification ignore marker was removed; rely on Effect inference. + +- `STM.acquireUseRelease` -> `Effect.acquireUseRelease + Effect.tx`: Wrap acquire, use, and release in separate Effect.tx calls to preserve the v3 separately committed phases; v4 release also receives the use Exit. + +- `STM.all` -> `Effect.all`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.as` -> `Effect.as`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.asSome` -> `Effect.asSome`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.asSomeError` -> `Effect.mapError(self, Option.some)`: The dedicated helper was removed; map the error into Option.some. + +- `STM.asVoid` -> `Effect.asVoid`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.attempt` -> `Effect.try`: The constructor was renamed; transaction programs are ordinary Effects in v4. + +- `STM.bind` -> `Effect.bind`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.bindTo` -> `Effect.bindTo`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.catchAll` -> `Effect.catch`: Use Effect.catch for typed failures. It does not catch Effect.txRetry or restore a transactional savepoint. + +- `STM.catchSome` -> `Effect.catch + Option.match`: Use Effect.catch and re-fail the original error when the partial handler returns None. + +- `STM.catchTag` -> `Effect.catchTag`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.check` -> `Effect.suspend + Effect.txRetry`: Evaluate the predicate lazily and return Effect.void when true or Effect.txRetry when false, inside Effect.tx. + +- `STM.collect` -> `Effect.flatMap + Option.match + Effect.txRetry`: Map Some to success and None to Effect.txRetry inside the surrounding Effect.tx transaction. + +- `STM.collectSTM` -> `Effect.flatMap + Option.match + Effect.txRetry`: Return the Effect held by Some and use Effect.txRetry for None, inside the surrounding Effect.tx transaction. + +- `STM.commit` -> `Effect.tx`: Effect.tx runs an Effect transaction and removes its Effect.Transaction requirement. + +- `STM.commitEither` -> `Effect.tx + Effect.result + Effect.fromResult`: Run Effect.tx(Effect.result(body)) before Effect.fromResult so journal changes commit even when the original transaction had a typed failure. + +- `STM.cond` -> `Effect.suspend`: Lazily branch to Effect.succeed or Effect.fail based on the predicate. + +- `STM.context` -> `Effect.context`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.contextWith` -> `Effect.contextWith`: The name remains, but the v4 callback returns an Effect directly. + +- `STM.contextWithSTM` -> `Effect.contextWith`: The Effect-returning context constructor no longer needs an STM suffix. + +- `STM.dieMessage` -> `Effect.die(new Error(message))`: The message-specific helper was removed; construct a message-bearing defect explicitly. + +- `STM.dieSync` -> `Effect.suspend(() => Effect.die(evaluate()))`: The lazy defect helper was removed; suspend construction and then die. + +- `STM.either` -> `Effect.result`: V4 uses Result instead of Either for materialized typed failures. + +- `STM.ensuring` -> `Effect.ensuring`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.eventually` -> `Effect.eventually`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.every` -> `Effect.findFirst + Option.isNone`: Search sequentially for the first false effectful predicate; no match means every element passed. + +- `STM.exists` -> `Effect.findFirst + Option.isSome`: Search sequentially for the first true effectful predicate. + +- `STM.fail` -> `Effect.fail`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.failSync` -> `Effect.failSync`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.fiberId` -> `Effect.fiberId`: The operation remains on Effect, but v4 yields the fiber id as a number. + +- `STM.filter` -> `Effect.filter`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.filterNot` -> `Effect.filter`: Negate the effectful predicate result and use Effect.filter. + +- `STM.filterOrDie` -> `Effect.filterOrFail + Effect.orDie`: Fail with the lazy defect when the predicate rejects, then convert that failure to a defect. + +- `STM.filterOrDieMessage` -> `Effect.filterOrFail + Effect.orDie`: Fail with a new Error carrying the message when the predicate rejects, then convert it to a defect. + +- `STM.filterOrFail` -> `Effect.filterOrFail`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.firstSuccessOf` -> `Effect.firstSuccessOf`: This only preserves typed-failure fallback. V4 has no equivalent for v3 retry-aware alternatives with journal savepoints. + +- `STM.flatMap` -> `Effect.flatMap`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.flatten` -> `Effect.flatten`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.flip` -> `Effect.flip`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.flipWith` -> `Effect.flip(self).pipe(f, Effect.flip)`: Compose the retained Effect.flip operation around the transforming function. + +- `STM.fromEither` -> `Effect.fromResult`: V4 replaced Either with Result; migrate the value and use Effect.fromResult. + +- `STM.head` -> `Effect.matchEffect`: Map source failures to Option.some, return the first iterable element, and fail with Option.none when empty. + +- `STM.if` -> `Effect.suspend or Effect.flatMap`: Select the true or false branch lazily; use flatMap when the condition is effectful. + +- `STM.ignore` -> `Effect.ignore`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.interrupt` -> `Effect.interrupt`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.interruptAs` -> `Effect.interrupt`: V4 exposes interruption of the current fiber only; remove the explicit FiberId argument. + +- `STM.isFailure` -> `Effect.isFailure`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.isSTM` -> `Effect.isEffect`: STM no longer has a distinct runtime representation; transaction programs are Effects. + +- `STM.isSuccess` -> `Effect.isSuccess`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.iterate` -> `Effect.gen loop`: No direct Effect iterate helper remains; carry state in an explicit sequential Effect.gen loop inside Effect.tx. + +- `STM.let` -> `Effect.let`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.loop` -> `Effect.gen loop`: No direct Effect loop helper remains; implement the state loop explicitly and collect values unless discard was requested. + +- `STM.map` -> `Effect.map`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.mapAttempt` -> `Effect.flatMap(self, (a) => Effect.try(() => f(a)))`: Use Effect.try in flatMap so thrown exceptions remain typed failures rather than defects. + +- `STM.mapInputContext` -> `Effect.updateContext`: The context-input mapping operation was renamed on Effect. + +- `STM.match` -> `Effect.match`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.matchSTM` -> `Effect.matchEffect`: The Effect-returning match combinator no longer has an STM suffix. + +- `STM.mergeAll` -> `Effect.reduce`: Reduce the input Effects sequentially and combine each produced value with the accumulator. + +- `STM.none` -> `Effect.matchEffect + Option.match`: Recreate the Option success/error shuffle explicitly; no dedicated helper remains. + +- `STM.option` -> `Effect.option`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.orDie` -> `Effect.orDie`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.orDieWith` -> `Effect.mapError + Effect.orDie`: Map the typed error to the desired defect and then use Effect.orDie. + +- `STM.orElse` -> `none`: V4 has no exact retry-aware transactional alternative with journal savepoint restoration. Effect.catch is only a failure-only approximation. + +- `STM.orElseEither` -> `none`: V4 has no exact retry-aware alternative. For typed failures only, compose Effect.catch and Result tagging manually. + +- `STM.orElseFail` -> `Effect.mapError`: Map typed failures to the replacement error; this does not preserve v3 retry fallback semantics. + +- `STM.orElseOptional` -> `Effect.catch + Option.match`: Run the fallback for None and re-fail Some errors explicitly. + +- `STM.orElseSucceed` -> `Effect.orElseSucceed`: The name remains for typed failures, but v4 does not preserve v3 retry fallback or journal savepoints. + +- `STM.orTry` -> `none`: V4 exposes no recoverable retry signal or public transactional savepoint; restructure branch selection before Effect.txRetry. + +- `STM.partition` -> `Effect.partition`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.provideServiceSTM` -> `Effect.provideServiceEffect`: The effectful service provider was renamed on Effect. + +- `STM.provideSomeContext` -> `Effect.provideContext or Effect.updateContext`: The dedicated partial-context helper was removed; provide or update the Effect context explicitly. + +- `STM.reduce` -> `Effect.reduce`: The combinator remains, but v4 takes the initial state lazily and also passes the element index. + +- `STM.reduceAll` -> `Effect.flatMap + Effect.reduce`: Evaluate the initial Effect, then reduce the remaining Effects sequentially. + +- `STM.reduceRight` -> `Effect.reduce over a reversed Array`: Materialize and reverse the iterable, then reduce while preserving the old state/element callback order. + +- `STM.refineOrDie` -> `Effect.catch + Option.match`: Re-fail Some refined errors and die with the original error for None. + +- `STM.refineOrDieWith` -> `Effect.catch + Option.match`: Re-fail Some refined errors and map None to the requested defect. + +- `STM.reject` -> `Effect.flatMap + Option.match`: Fail when the partial rejection returns Some; otherwise keep the original success. + +- `STM.rejectSTM` -> `Effect.flatMap + Option.match`: Run and fail with the Effect held by Some; otherwise keep the original success. + +- `STM.repeatUntil` -> `Effect.repeat(self, { until: predicate })`: The dedicated combinator moved to Effect.repeat options. + +- `STM.repeatWhile` -> `Effect.repeat(self, { while: predicate })`: The dedicated combinator moved to Effect.repeat options. + +- `STM.replicate` -> `Effect.replicate`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.replicateSTM` -> `Effect.replicateEffect`: Use the effectful replication combinator and keep execution sequential inside Effect.tx. + +- `STM.replicateSTMDiscard` -> `Effect.replicateEffect(self, n, { discard: true })`: Use effectful replication with discard enabled and keep execution sequential inside Effect.tx. + +- `STM.retry` -> `Effect.txRetry`: Do not use Effect.retry, which retries typed failures by schedule; Effect.txRetry waits for an accessed Tx value to change. + +- `STM.retryUntil` -> `Effect.flatMap + Effect.txRetry`: Succeed when the predicate passes; otherwise return Effect.txRetry inside Effect.tx. + +- `STM.retryWhile` -> `Effect.flatMap + Effect.txRetry`: Return Effect.txRetry while the predicate passes; otherwise succeed inside Effect.tx. + +- `STM.some` -> `Effect.matchEffect + Option.match`: Recreate the Option success/error shuffle explicitly; no dedicated helper remains. + +- `STM.succeed` -> `Effect.succeed`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.summarized` -> `Effect.gen`: Run the summary Effect before and after the body, then return the computed summary and body value. + +- `STM.sync` -> `Effect.sync`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.tap` -> `Effect.tap`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.tapBoth` -> `Effect.tapError + Effect.tap`: Compose the separate failure and success taps. + +- `STM.tapError` -> `Effect.tapError`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.try` -> `Effect.try`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.unless` -> `Effect.when(self, Effect.sync(() => !predicate()))`: V4 Effect.when takes an effectful condition; suspend and negate the old lazy boolean. + +- `STM.unlessSTM` -> `Effect.when(self, Effect.map(condition, (b) => !b))`: Negate the effectful condition and use Effect.when. + +- `STM.unsome` -> `Effect.matchEffect + Option.match`: Recreate the Option error/success shuffle explicitly; no dedicated helper remains. + +- `STM.validateAll` -> `Effect.validate`: The validation combinator was renamed and now returns a NonEmptyArray of errors. + +- `STM.validateFirst` -> `Effect.flip + Effect.forEach`: Flip each candidate result, traverse sequentially, then flip the aggregate to preserve all errors when every candidate fails. + +- `STM.void` -> `Effect.void`: The combinator keeps its name, but STM values are now ordinary Effects. Run the complete transaction with Effect.tx. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `STM.when` -> `Effect.when(self, Effect.sync(predicate))`: V4 Effect.when takes an effectful boolean, so suspend the old lazy predicate. + +- `STM.whenSTM` -> `Effect.when`: The effectful-condition form is now the only Effect.when form. + +- `STM.zipLeft` -> `Effect.zipWith(self, that, (left) => left)`: Use sequential Effect.zipWith and retain the left result. + +- `STM.zipRight` -> `Effect.andThen`: Use Effect.andThen for sequential composition that retains the right result. + +### `effect/Schedule` + +- `Schedule.CurrentIterationMetadata` -> `Schedule.CurrentMetadata`: The Context.Reference was renamed and now provides Schedule.Metadata with input, output, attempt, duration, and elapsed timing fields. + +- `Schedule.IterationMetadata` -> `Schedule.Metadata`: The metadata model now includes duration and uses attempt instead of recurrence; elapsed fields are millisecond numbers. + +- `Schedule.Schedule` -> `Schedule.Schedule`: The model remains but now has Schedule\; its public initial/step fields were replaced by Schedule.toStep and fromStep. + +- `Schedule.Schedule.DriverVariance` -> `none`: ScheduleDriver was removed in v4, so its variance marker has no replacement. Use the Schedule type parameters or the function returned by Schedule.toStepWithSleep. + +- `Schedule.Schedule.Variance` -> `Schedule.Schedule.Variance`: The variance marker remains and now tracks Output, Input, Error, and Env through the private Schedule TypeId. + +- `Schedule.ScheduleDriver` -> `Schedule.toStepWithSleep`: ScheduleDriver was removed. The acquired step function provides manual next calls with automatic sleeping; Schedule.toStep exposes raw delays. + +- `Schedule.ScheduleDriverTypeId` -> `none`: ScheduleDriver and its public type id were removed. Use the step function returned by Schedule.toStepWithSleep. + +- `Schedule.ScheduleTypeId` -> `none`: The Schedule type id is private in v4. Use Schedule.isSchedule to narrow unknown values. + +- `Schedule.addDelayEffect` -> `Schedule.addDelay`: The v4 function is effectful by default and its callback receives full Schedule.Metadata; read metadata.output when only the prior output is needed. + +- `Schedule.andThen` -> `Schedule.concat`: The sequencing combinator was renamed to Schedule.concat. + +- `Schedule.andThenEither` -> `Schedule.concatResult`: Sequential phase tagging now uses Result: self outputs are Result.fail and the following schedule outputs are Result.succeed. + +- `Schedule.as` -> `Schedule.map`: Map the metadata to the constant output; Schedule.map accepts either a plain value or an Effect. + +- `Schedule.asVoid` -> `Schedule.map`: Map every output to undefined. + +- `Schedule.bothInOut` -> `none`: There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done. + +- `Schedule.check` -> `Schedule.while`: Continue while a predicate over metadata.input and metadata.output returns true. + +- `Schedule.checkEffect` -> `Schedule.while`: Schedule.while accepts an effectful metadata predicate in v4. + +- `Schedule.collectAllInputs` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.collectAllOutputs` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.collectUntil` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.collectUntilEffect` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.collectWhile` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.collectWhileEffect` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.compose` -> `none`: There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done. + +- `Schedule.count` -> `Schedule.forever`: The forever schedule outputs the zero-based recurrence count. + +- `Schedule.dayOfMonth` -> `Schedule.cron`: Express the calendar constraint as a cron expression, for example `0 0 * *`, and map its Duration output if a numeric output is required. + +- `Schedule.dayOfWeek` -> `Schedule.cron`: Express the weekday constraint as a cron expression, for example `0 0 * * `, and map its Duration output if a numeric output is required. + +- `Schedule.delayed` -> `Schedule.modifyDelay`: Return Effect.succeed(f(metadata.duration)); delay transformations are effectful and receive full metadata in v4. + +- `Schedule.delayedEffect` -> `Schedule.modifyDelay`: The v4 delay modifier is effectful by default and receives full Schedule.Metadata. + +- `Schedule.delayedSchedule` -> `Schedule.modifyDelay`: Replace each delay with metadata.output, converting that Duration output through Effect.succeed. + +- `Schedule.delays` -> `Schedule.map`: Map each decision to metadata.duration to expose the selected recurrence delay. + +- `Schedule.driver` -> `Schedule.toStepWithSleep`: Acquire the sleeping step function and call it for each input; use Schedule.toStep when delay handling must remain manual. + +- `Schedule.either` -> `Schedule.min`: Use Schedule.min for fastest-delay composition. It outputs the selected Duration rather than a tuple of both outputs. + +- `Schedule.eitherWith` -> `Schedule.min`: Schedule.min implements the standard fastest-delay composition; custom interval merging requires a Schedule.fromStep implementation. + +- `Schedule.elapsed` -> `Schedule.map`: Map metadata.elapsed through Duration.millis. + +- `Schedule.ensuring` -> `Schedule.during`: Use the duration-bounded v4 schedule constructor. + +- `Schedule.fromDelay` -> `Schedule.duration`: The duration constructor recurs once after the supplied delay. + +- `Schedule.fromDelays` -> `Schedule.duration + Schedule.concat`: Build one Schedule.duration per delay and sequence them with Schedule.concat. + +- `Schedule.fromFunction` -> `Schedule.identity + Schedule.map`: Start with Schedule.identity\() and map metadata.input through the function. + +- `Schedule.hourOfDay` -> `Schedule.cron`: Express the hour constraint as a cron expression such as `0 * * *`. + +- `Schedule.intersect` -> `Schedule.max`: Use Schedule.max for slowest-delay composition. It outputs the selected Duration rather than a tuple of both outputs. + +- `Schedule.intersectWith` -> `Schedule.max`: Schedule.max implements the standard slowest-delay composition; custom interval merging requires a Schedule.fromStep implementation. + +- `Schedule.jitteredWith` -> `Schedule.modifyDelay`: For custom bounds, scale metadata.duration using Random.next inside the effectful delay callback; Schedule.jittered supplies the fixed v4 0.8-1.2 range. + +- `Schedule.linear` -> `Schedule.forever + Schedule.map + Schedule.modifyDelay`: Map the recurrence attempt to the linearly increasing Duration, then use that output as the recurrence delay. + +- `Schedule.makeWithState` -> `Schedule.fromStep`: Move mutable state into the acquired step closure; return [output, Duration] for recurrence and Cause.done(output) for termination. + +- `Schedule.mapBoth` -> `Schedule.fromStep + Schedule.toStep`: Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. + +- `Schedule.mapBothEffect` -> `Schedule.fromStep + Schedule.toStep`: Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. Apply the effectful output mapping to the returned tuple. + +- `Schedule.mapEffect` -> `Schedule.map`: Schedule.map accepts an Effect result and receives full Schedule.Metadata. + +- `Schedule.mapInput` -> `Schedule.fromStep + Schedule.toStep`: Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. + +- `Schedule.mapInputContext` -> `Schedule.fromStep + Effect.provide`: Provide the transformed service context to both Schedule.toStep acquisition and each returned step Effect. + +- `Schedule.mapInputEffect` -> `Schedule.fromStep + Schedule.toStep`: Input transformation is no longer a standalone combinator. Wrap Schedule.toStep(self) with Schedule.fromStep and transform the input before invoking the underlying step. Evaluate the input mapping Effect before the underlying step. + +- `Schedule.minuteOfHour` -> `Schedule.cron`: Express the minute constraint as a cron expression such as ` * * * *`. + +- `Schedule.modifyDelayEffect` -> `Schedule.modifyDelay`: The v4 delay modifier is effectful by default and receives full Schedule.Metadata. + +- `Schedule.onDecision` -> `Schedule.tap`: Use Schedule.tap for effects on recurrence metadata. To also observe final completion, wrap Schedule.toStep with Pull.matchEffect in Schedule.fromStep. + +- `Schedule.once` -> `Schedule.duration(Duration.zero)`: A zero-duration schedule recurs once and then completes; map its Duration output to void if needed. + +- `Schedule.provideContext` -> `Schedule.fromStep + Effect.provide`: Provide the Context to both Schedule.toStep acquisition and each Effect returned by the acquired step. + +- `Schedule.provideService` -> `Schedule.fromStep + Effect.provideService`: Provide the service to both Schedule.toStep acquisition and each Effect returned by the acquired step. + +- `Schedule.recurUntil` -> `Schedule.identity + Schedule.while`: Continue while the predicate over metadata.input is false. + +- `Schedule.recurUntilEffect` -> `Schedule.identity + Schedule.while`: Continue while the effectful predicate over metadata.input is false. + +- `Schedule.recurUntilOption` -> `Schedule.fromStep`: Use a custom step to evaluate the Option-producing function, emit Option.none while recurring, and terminate with the first Option.some result. + +- `Schedule.recurUpTo` -> `Schedule.during`: Use the duration-bounded schedule constructor. + +- `Schedule.recurWhile` -> `Schedule.identity + Schedule.while`: Continue while the predicate over metadata.input is true. + +- `Schedule.recurWhileEffect` -> `Schedule.identity + Schedule.while`: Continue while the effectful predicate over metadata.input is true. + +- `Schedule.reduce` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.reduceEffect` -> `none`: This stateful collection combinator was removed during the v4 Schedule simplification. Rebuild it with Schedule.fromStep and Schedule.toStep, keeping accumulation state inside the acquired step closure. + +- `Schedule.repeatForever` -> `Schedule.forever`: The infinite zero-delay counter schedule was renamed. + +- `Schedule.repetitions` -> `Schedule.map`: Map metadata.attempt to the required recurrence count, adjusting by one where the v3 zero-based value is expected. + +- `Schedule.resetAfter` -> `none`: Automatic schedule reset was removed. Wrap Schedule.toStep(self) with Schedule.fromStep and reacquire the inner step when the reset condition is met. + +- `Schedule.resetWhen` -> `none`: Automatic schedule reset was removed. Wrap Schedule.toStep(self) with Schedule.fromStep and reacquire the inner step when the reset condition is met. + +- `Schedule.run` -> `Schedule.toStep`: Acquire the step and traverse inputs manually, supplying each timestamp and collecting successful outputs until Cause.done. + +- `Schedule.secondOfMinute` -> `Schedule.cron`: Use the six-field cron form to express a seconds constraint, for example ` * * * * *`. + +- `Schedule.stop` -> `Schedule.fromStep`: Create a step that immediately returns Cause.done(undefined). + +- `Schedule.succeed` -> `Schedule.forever + Schedule.map`: Map every recurrence to the constant value. + +- `Schedule.sync` -> `Schedule.forever + Schedule.map`: Map every recurrence by lazily evaluating the thunk. + +- `Schedule.tapInput` -> `Schedule.tap`: Use the unified tap callback and read metadata.input. + +- `Schedule.tapOutput` -> `Schedule.tap`: Use the unified tap callback and read metadata.output. + +- `Schedule.unfold` -> `Schedule.fromStep`: Keep the evolving value inside the acquired step closure and emit each value with the desired Duration. + +- `Schedule.union` -> `Schedule.min`: Use Schedule.min for fastest-delay composition. It outputs the selected Duration rather than both schedule outputs. + +- `Schedule.unionWith` -> `Schedule.min`: Schedule.min covers the standard union behavior; a custom interval merge requires Schedule.fromStep. + +- `Schedule.untilInput` -> `Schedule.while`: Continue while the predicate over metadata.input is false. + +- `Schedule.untilInputEffect` -> `Schedule.while`: Continue while the effectful predicate over metadata.input is false. + +- `Schedule.untilOutput` -> `Schedule.while`: Continue while the predicate over metadata.output is false. + +- `Schedule.untilOutputEffect` -> `Schedule.while`: Continue while the effectful predicate over metadata.output is false. + +- `Schedule.whileInput` -> `Schedule.while`: Continue while the predicate over metadata.input is true. + +- `Schedule.whileInputEffect` -> `Schedule.while`: Continue while the effectful predicate over metadata.input is true. + +- `Schedule.whileOutput` -> `Schedule.while`: Continue while the predicate over metadata.output is true. + +- `Schedule.whileOutputEffect` -> `Schedule.while`: Continue while the effectful predicate over metadata.output is true. + +- `Schedule.zipLeft` -> `none`: There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done. + +- `Schedule.zipRight` -> `none`: There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done. + +- `Schedule.zipWith` -> `none`: There is no direct v4 combinator preserving this output shape. Rebuild it with Schedule.fromStep and Schedule.toStep; schedule steps now return [output, Duration] and terminate with Cause.done. + +### `effect/ScheduleDecision` + +- `ScheduleDecision.Done` -> `Cause.Done`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). + +- `ScheduleDecision.ScheduleDecision` -> `none`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). + +- `ScheduleDecision.continue` -> `Effect.succeed([output, duration])`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). + +- `ScheduleDecision.continueWith` -> `Effect.succeed([output, duration])`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). + +- `ScheduleDecision.isContinue` -> `none`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). Branch on the Pull result instead of inspecting a decision value. + +- `ScheduleDecision.isDone` -> `Cause.isDone`: ScheduleDecision was removed from the v4 public model. A Schedule.fromStep step recurs by returning [output, Duration] and terminates with Cause.done(output). + +### `effect/Scheduler` + +- `Scheduler.ControlledScheduler` -> `none`: No public step-controlled scheduler remains; implement Scheduler and SchedulerDispatcher for exact controlled stepping. + +- `Scheduler.MixedScheduler` -> `Scheduler.MixedScheduler`: The class remains with a redesigned constructor and makeDispatcher-based task API. + +- `Scheduler.PriorityBuckets` -> `none`: Priority buckets are now an internal Scheduler implementation detail. + +- `Scheduler.Scheduler` -> `Scheduler.Scheduler`: The interface remains but dispatch moved to SchedulerDispatcher returned by makeDispatcher. + +- `Scheduler.SchedulerRunner` -> `Scheduler.SchedulerDispatcher`: Task scheduling and flushing moved to the dispatcher returned by Scheduler.makeDispatcher. + +- `Scheduler.SyncScheduler` -> `new Scheduler.MixedScheduler("sync")`: Use a synchronous MixedScheduler and its dispatcher; call flush when directly driving queued tasks. + +- `Scheduler.Task` -> `() => void`: The named alias was removed; dispatcher APIs inline the task callback type. + +- `Scheduler.defaultScheduler` -> `Scheduler.Scheduler`: The default scheduler is now a Context.Reference; yield it to read or provide it to override the current scheduler. + +- `Scheduler.defaultShouldYield` -> `Scheduler.MixedScheduler#shouldYield`: The standalone function was removed; yielding is implemented by each Scheduler instance. + +- `Scheduler.make` -> `none`: Implement the redesigned Scheduler interface and return task dispatch through makeDispatcher. + +- `Scheduler.makeBatched` -> `new Scheduler.MixedScheduler("async", schedule)`: Pass a cancellable scheduling function; the dispatcher performs priority batching. + +- `Scheduler.makeMatrix` -> `none`: Matrix routing was removed; implement routing in a custom Scheduler and SchedulerDispatcher if still required. + +- `Scheduler.timer` -> `Effect.delay`: Use Effect delay or sleep for effect timing; implement a custom dispatcher for exact per-task scheduler timing. + +- `Scheduler.timerBatched` -> `new Scheduler.MixedScheduler("async", scheduleWithTimer)`: Use a setTimeout-based cancellable scheduling function; the dispatcher batches queued tasks. + +### `effect/Schema` + +- `Schema.Annotable` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotable.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotable.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotable.Self` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.AnnotableClass` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.AnnotableDeclare` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotations` -> `Schema.Annotations`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Annotations.Doc` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotations.Filter` -> `Schema.Annotations.Filter`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Annotations.GenericSchema` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Annotations.Schema` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Any` -> `Schema.Any`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Array$` -> `Schema.$Array`: Use the renamed v4 constructor result interface. + +- `Schema.ArrayEnsure` -> `Schema.ArrayEnsure`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.ArrayFormatterIssue` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.BetweenBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.BetweenBigIntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.BetweenDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.BetweenDurationSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.BetweenSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.BigDecimal` -> `Schema.BigDecimalFromString`: Use the string-to-BigDecimal codec; v4 `BigDecimal` is the self schema. + +- `Schema.BigDecimalFromNumber` -> `none`: No built-in number-to-BigDecimal codec remains; compose `decodeTo` with a `SchemaGetter` conversion. + +- `Schema.BigDecimalFromSelf` -> `Schema.BigDecimal`: The self schema dropped the `FromSelf` suffix. + +- `Schema.BigInt` -> `Schema.BigIntFromString`: Use the string-to-bigint codec; v4 `BigInt` is the self schema. + +- `Schema.BigIntFromNumber` -> `none`: No built-in number-to-bigint codec remains; compose `decodeTo` with a checked `SchemaGetter` conversion. + +- `Schema.BigIntFromSelf` -> `Schema.BigInt`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Boolean` -> `Schema.Boolean`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.BooleanFromString` -> `none`: No built-in string-to-boolean codec remains; use `decodeTo` with an explicit `SchemaGetter` transformation. + +- `Schema.BooleanFromUnknown` -> `Schema.Boolean`: Use the boolean schema and perform any coercion explicitly before decoding. + +- `Schema.BrandSchema` -> `Schema.brand`: Use the schema returned by the v4 `brand` combinator and infer its concrete type. + +- `Schema.BrandSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Capitalize` -> `Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isCapitalized()), SchemaTransformation.capitalize()))`: Rebuild the capitalization transformation with `decodeTo`. + +- `Schema.Capitalized` -> `Schema.String.check(Schema.isCapitalized())`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.CapitalizedSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Cause` -> `Schema.toCodecJson(Schema.Cause(error, defect))`: Use the derived JSON codec to preserve v3's encoded Cause representation; v4 `Cause` itself is the self schema. + +- `Schema.CauseEncoded` -> `Schema.CauseIso`: Use the v4 Cause JSON/iso representation type. + +- `Schema.CauseFromSelf` -> `Schema.Cause`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Char` -> `Schema.Char`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Chunk` -> `Schema.toCodecJson(Schema.Chunk(value))`: Use the derived JSON codec to preserve v3's array-to-Chunk behavior; v4 `Chunk` itself is the self schema. + +- `Schema.ChunkFromSelf` -> `Schema.Chunk`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Class` -> `Schema.Class`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Config` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.Data` -> `none`: Remove this wrapper. v4 structural equality works on ordinary decoded objects. + +- `Schema.DataFromSelf` -> `none`: Remove this wrapper. v4 structural equality works on ordinary decoded objects. + +- `Schema.Date` -> `Schema.DateFromString`: Use `DateFromString`; v4 `Date` is the self schema. + +- `Schema.DateFromNumber` -> `Schema.DateFromMillis`: Rename the milliseconds-to-Date codec. + +- `Schema.DateFromSelf` -> `Schema.Date`: The self schema dropped the `FromSelf` suffix. + +- `Schema.DateFromSelfSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.DateFromString` -> `Schema.DateFromString`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.DateTimeUtc` -> `Schema.DateTimeUtcFromString`: Use the string codec; v4 `DateTimeUtc` is the self schema. + +- `Schema.DateTimeUtcFromDate` -> `Schema.DateTimeUtcFromDate`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.DateTimeUtcFromNumber` -> `Schema.DateTimeUtcFromMillis`: Rename the milliseconds-to-DateTime codec. + +- `Schema.DateTimeUtcFromSelf` -> `Schema.DateTimeUtc`: The self schema dropped the `FromSelf` suffix. + +- `Schema.DateTimeZoned` -> `Schema.DateTimeZonedFromString`: Use the string codec; v4 `DateTimeZoned` is the self schema. + +- `Schema.DateTimeZonedFromSelf` -> `Schema.DateTimeZoned`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Defect` -> `Schema.Defect`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Duration` -> `Schema.DurationFromString`: Use the string codec; v4 `Duration` is the self schema. + +- `Schema.DurationEncoded` -> `Schema.Duration["Iso"]`: Use the v4 Duration iso representation type. + +- `Schema.DurationFromMillis` -> `Schema.DurationFromMillis`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.DurationFromNanos` -> `Schema.DurationFromNanos`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.DurationFromSelf` -> `Schema.Duration`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Either` -> `Schema.Result`: `Either` was renamed to `Result`; pass success and failure schemas positionally. + +- `Schema.EitherEncoded` -> `Schema.ResultIso`: Use the v4 Result iso representation type. + +- `Schema.EitherFromSelf` -> `Schema.Result`: `Either` was renamed to `Result` in v4. + +- `Schema.EitherFromUnion` -> `Schema.Result`: `Either` was renamed to `Result`; use its tagged Result representation. + +- `Schema.Element` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Element.Token` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.EndsWithSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Enums` -> `Schema.Enum`: Rename the enum constructor and pass the enum object. + +- `Schema.EnumsDefinition` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.Exit` -> `Schema.toCodecJson(Schema.Exit(value, error, defect))`: Use the derived JSON codec to preserve v3's encoded Exit representation; v4 `Exit` itself is the self schema. + +- `Schema.ExitEncoded` -> `Schema.ExitIso`: Use the v4 Exit iso representation type. + +- `Schema.ExitFromSelf` -> `Schema.Exit`: The self schema dropped the `FromSelf` suffix. + +- `Schema.FiberId` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.FiberIdEncoded` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.FiberIdFromSelf` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.FilterIssue` -> `Schema.FilterIssue`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.FilterOutput` -> `Schema.FilterOutput`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Finite` -> `Schema.Finite`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.FiniteSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.FromPropertySignature` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.GreaterThanBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanBigIntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanDurationSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanOrEqualToBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanOrEqualToBigIntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanOrEqualToDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanOrEqualToDurationSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanOrEqualToSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.GreaterThanSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.HashMap` -> `Schema.toCodecJson(Schema.HashMap(key, value))`: Pass key and value positionally and use the derived JSON codec to preserve v3's entry-array encoding. + +- `Schema.HashMapFromSelf` -> `Schema.HashMap`: The self schema dropped the `FromSelf` suffix; pass key and value positionally. + +- `Schema.HashSet` -> `Schema.toCodecJson(Schema.HashSet(value))`: Use the derived JSON codec to preserve v3's array-to-HashSet behavior. + +- `Schema.HashSetFromSelf` -> `Schema.HashSet`: The self schema dropped the `FromSelf` suffix. + +- `Schema.IncludesSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.IndexSignature` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.IndexSignature.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.IndexSignature.Encoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.IndexSignature.NonEmptyRecords` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.IndexSignature.Record` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.IndexSignature.Type` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.InstanceOfSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Int` -> `Schema.Int`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.IntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.ItemsCountSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.JsonNumber` -> `Schema.Finite`: Use the finite-number schema for JSON-compatible numbers. + +- `Schema.JsonNumberSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LeftEncoded` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.LengthSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanBigIntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanDurationSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanOrEqualToBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanOrEqualToBigIntSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanOrEqualToDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanOrEqualToDurationSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanOrEqualToSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.LessThanSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.List` -> `none`: The List schema was removed; migrate the model to `Schema.Array` or declare a custom List codec. + +- `Schema.ListFromSelf` -> `none`: The List self schema was removed; migrate to arrays or use `Schema.declare` for List values. + +- `Schema.Literal` -> `Schema.Literal / Schema.Literals`: Use `Literal(value)` for one non-null literal, `Null` for null, and `Literals([...])` for several literals. + +- `Schema.Lowercase` -> `Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isLowercased()), SchemaTransformation.toLowerCase()))`: Rebuild the lowercase transformation with `decodeTo`. + +- `Schema.Lowercased` -> `Schema.String.check(Schema.isLowercased())`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.LowercasedSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.MakeOptions` -> `Schema.MakeOptions`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Map` -> `Schema.toCodecJson(Schema.ReadonlyMap(key, value))`: Use `ReadonlyMap` with positional arguments and derive its JSON codec; mutable Map-specific schema types were removed. + +- `Schema.Map$` -> `Schema.$ReadonlyMap`: Use the renamed v4 constructor result interface. + +- `Schema.MapFromRecord` -> `none`: No direct record-to-Map codec remains; compose `Record` and `ReadonlyMap` with an explicit `decodeTo` transformation. + +- `Schema.MapFromSelf` -> `Schema.ReadonlyMap`: Use the readonly Map self schema with positional key and value arguments. + +- `Schema.MaxItemsSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.MaxLengthSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.MinItemsSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.MinLengthSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.MultipleOfSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Negative` -> `Schema.Number.check(Schema.isLessThan(0))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NegativeBigDecimalFromSelf` -> `Schema.BigDecimal.check(Schema.isLessThanBigDecimal(BigDecimal.fromNumber(0)))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NegativeBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.NegativeBigInt` -> `Schema.BigIntFromString.check(Schema.isLessThanBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NegativeBigIntFromSelf` -> `Schema.BigInt.check(Schema.isLessThanBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.Never` -> `Schema.Never`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NonEmptyArray` -> `Schema.NonEmptyArray`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NonEmptyArrayEnsure` -> `none`: No direct replacement remains; explicitly decode a single value or array to `Schema.NonEmptyArray`. + +- `Schema.NonEmptyChunk` -> `Schema.toCodecJson(Schema.Chunk(value).check(Schema.isMinLength(1)))`: Use a checked Chunk JSON codec. + +- `Schema.NonEmptyChunkFromSelf` -> `Schema.Chunk(value).check(Schema.isMinLength(1))`: Use the Chunk self schema with a minimum-length check. + +- `Schema.NonEmptyString` -> `Schema.NonEmptyString`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NonEmptyTrimmedString` -> `Schema.Trimmed.check(Schema.isNonEmpty())`: Compose the trimmed schema with the non-empty check. + +- `Schema.NonNaN` -> `Schema.Number.check(Schema.makeFilter((n) => !Number.isNaN(n)))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonNaNSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.NonNegative` -> `Schema.Number.check(Schema.isGreaterThanOrEqualTo(0))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonNegativeBigDecimalFromSelf` -> `Schema.BigDecimal.check(Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(0)))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonNegativeBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.NonNegativeBigInt` -> `Schema.BigIntFromString.check(Schema.isGreaterThanOrEqualToBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonNegativeBigIntFromSelf` -> `Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonNegativeInt` -> `Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonPositive` -> `Schema.Number.check(Schema.isLessThanOrEqualTo(0))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonPositiveBigDecimalFromSelf` -> `Schema.BigDecimal.check(Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromNumber(0)))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonPositiveBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.NonPositiveBigInt` -> `Schema.BigIntFromString.check(Schema.isLessThanOrEqualToBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.NonPositiveBigIntFromSelf` -> `Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.Not` -> `none`: The exclusion constructor was removed; express the accepted alternatives directly or add a `Schema.check`. + +- `Schema.Null` -> `Schema.Null`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NullOr` -> `Schema.NullOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NullishOr` -> `Schema.NullishOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Number` -> `Schema.Number`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.NumberFromString` -> `Schema.NumberFromString`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Object` -> `Schema.ObjectKeyword`: Rename the object keyword schema. + +- `Schema.Option` -> `Schema.toCodecJson(Schema.Option(value))`: Use the derived JSON codec to preserve v3's tagged Option encoding; v4 `Option` itself is the self schema. + +- `Schema.OptionEncoded` -> `Schema.OptionIso`: Use the v4 Option iso representation type. + +- `Schema.OptionFromNonEmptyTrimmedString` -> `Schema.Trimmed.check(Schema.isNonEmpty()).pipe(Schema.decodeTo(Schema.Option(Schema.String), ...))`: Rebuild the empty-string-to-None conversion explicitly with `decodeTo`. + +- `Schema.OptionFromNullOr` -> `Schema.OptionFromNullOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.OptionFromNullishOr` -> `Schema.OptionFromNullishOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.OptionFromSelf` -> `Schema.Option`: The self schema dropped the `FromSelf` suffix. + +- `Schema.OptionFromUndefinedOr` -> `Schema.OptionFromUndefinedOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.OptionalOptions` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.ParseJsonOptions` -> `none`: The old parse-json options type was removed; configure `fromJsonString` and its underlying getter directly. + +- `Schema.PatternSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Positive` -> `Schema.Number.check(Schema.isGreaterThan(0))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.PositiveBigDecimalFromSelf` -> `Schema.BigDecimal.check(Schema.isGreaterThanBigDecimal(BigDecimal.fromNumber(0)))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.PositiveBigDecimalSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.PositiveBigInt` -> `Schema.BigIntFromString.check(Schema.isGreaterThanBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.PositiveBigIntFromSelf` -> `Schema.BigInt.check(Schema.isGreaterThanBigInt(0n))`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.PropertyKey` -> `Schema.PropertyKey`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.PropertySignature` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignature.AST` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignature.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignature.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignature.Token` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignatureDeclaration` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignatureTransformation` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.PropertySignatureTypeId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.ReadonlyMap` -> `Schema.toCodecJson(Schema.ReadonlyMap(key, value))`: Pass key and value positionally and use the derived JSON codec to preserve v3's entry-array encoding. + +- `Schema.ReadonlyMap$` -> `Schema.$ReadonlyMap`: Use the renamed v4 constructor result interface. + +- `Schema.ReadonlyMapFromRecord` -> `none`: No direct record-to-ReadonlyMap codec remains; compose `Record` and `ReadonlyMap` with an explicit `decodeTo` transformation. + +- `Schema.ReadonlyMapFromSelf` -> `Schema.ReadonlyMap`: The self schema dropped the `FromSelf` suffix; pass key and value positionally. + +- `Schema.ReadonlySet` -> `Schema.toCodecJson(Schema.ReadonlySet(value))`: Use the derived JSON codec to preserve v3's array-to-ReadonlySet behavior. + +- `Schema.ReadonlySet$` -> `Schema.$ReadonlySet`: Use the renamed v4 constructor result interface. + +- `Schema.ReadonlySetFromSelf` -> `Schema.ReadonlySet`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Record` -> `Schema.Record(key, value)`: Pass key and value as separate arguments. + +- `Schema.Record$` -> `Schema.$Record`: Use the renamed v4 constructor result interface. + +- `Schema.Redacted` -> `Schema.RedactedFromValue`: Use `RedactedFromValue` to wrap decoded raw values; v4 `Redacted` is the self schema. + +- `Schema.RedactedFromSelf` -> `Schema.Redacted`: The self schema dropped the `FromSelf` suffix. + +- `Schema.RefineSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.RightEncoded` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.Schema` -> `Schema.Schema`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Schema.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.AnyNoContext` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.AsSchema` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.Encoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.ToAsserts` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Schema.Variance` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.SchemaClass` -> `Schema.Codec`: The concrete SchemaClass abstraction was removed; accept the appropriate v4 `Codec` or constraint type. + +- `Schema.Serializable` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Serializable.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Serializable.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Serializable.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Serializable.Encoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Serializable.Type` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.SerializableWithResult` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.SerializableWithResult.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.SerializableWithResult.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.SerializableWithResult.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Set` -> `Schema.toCodecJson(Schema.ReadonlySet(value))`: Use the readonly Set schema and derive its JSON codec; mutable Set-specific schema types were removed. + +- `Schema.Set$` -> `Schema.$ReadonlySet`: Use the renamed v4 constructor result interface. + +- `Schema.SetFromSelf` -> `Schema.ReadonlySet`: Use the readonly Set self schema. + +- `Schema.SimplifyMutable` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.SortedSet` -> `none`: The SortedSet schema was removed; migrate to `ReadonlySet` or declare a custom codec that applies the required ordering. + +- `Schema.SortedSetFromSelf` -> `none`: The SortedSet self schema was removed; use `Schema.declare` if SortedSet values must remain in the model. + +- `Schema.StartsWithSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.String` -> `Schema.String`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Struct` -> `Schema.Struct`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Struct.Constructor` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.Encoded` -> `Schema.Struct.Encoded`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Struct.EncodedOptionalKeys` -> `Schema.Struct.EncodedOptionalKeys`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Struct.Field` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.Key` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.OptionalEncodedPropertySignature` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.OptionalTypePropertySignature` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Struct.PropertySignatureWithDefault` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.Symbol` -> `none`: v4 `Symbol` is the self schema and has no built-in string-to-symbol codec; rebuild the conversion explicitly with `decodeTo`. + +- `Schema.SymbolFromSelf` -> `Schema.Symbol`: The self schema dropped the `FromSelf` suffix. + +- `Schema.TaggedClass` -> `Schema.TaggedClass`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.TaggedErrorClass` -> `Schema.TaggedError`: The exported helper interface was removed; use the class returned by Schema.TaggedError and infer its types. + +- `Schema.TaggedRequest` -> `effect/unstable/rpc/Rpc.make`: The Schema request/serialization protocol was removed; migrate RPC requests to the v4 Rpc APIs. + +- `Schema.TaggedRequest.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TaggedRequest.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TaggedRequestClass` -> `effect/unstable/rpc/Rpc.make`: The Schema request/serialization protocol was removed; migrate RPC requests to the v4 Rpc APIs. + +- `Schema.TaggedStruct` -> `Schema.TaggedStruct`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.TemplateLiteral` -> `Schema.TemplateLiteral(parts)`: Pass template literal parts as one array. + +- `Schema.TemplateLiteralParser` -> `Schema.TemplateLiteralParser(schema.parts)`: Create the template schema first and pass its `parts` property. + +- `Schema.TimeZone` -> `Schema.TimeZoneFromString`: Use the string codec; v4 `TimeZone` is the self schema. + +- `Schema.TimeZoneFromSelf` -> `Schema.TimeZone`: The self schema dropped the `FromSelf` suffix. + +- `Schema.TimeZoneNamed` -> `Schema.TimeZoneNamedFromString`: Use the string codec; v4 `TimeZoneNamed` is the self schema. + +- `Schema.TimeZoneNamedFromSelf` -> `Schema.TimeZoneNamed`: The self schema dropped the `FromSelf` suffix. + +- `Schema.TimeZoneOffset` -> `Schema.toCodecJson(Schema.TimeZoneOffset)`: Use the derived JSON codec to preserve v3's encoded offset representation; v4 `TimeZoneOffset` is the self schema. + +- `Schema.TimeZoneOffsetFromSelf` -> `Schema.TimeZoneOffset`: The self schema dropped the `FromSelf` suffix. + +- `Schema.ToPropertySignature` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.Trim` -> `Schema.Trim`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Trimmed` -> `Schema.Trimmed`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.TrimmedSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Tuple` -> `Schema.Tuple(elements)`: Pass tuple elements as one array. + +- `Schema.Tuple2` -> `Schema.Tuple`: Use the array-based tuple constructor. + +- `Schema.TupleType.ElementsEncoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TupleType.ElementsType` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TupleType.Encoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TupleType.Type` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TypeId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.TypeLiteral` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TypeLiteral.Constructor` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TypeLiteral.Encoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.TypeLiteral.Type` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.ULID` -> `Schema.String.check(Schema.isULID())`: Build the string schema with the ULID check. + +- `Schema.ULIDSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.URL` -> `Schema.URLFromString`: Use the string-to-URL codec; v4 `URL` is the self schema. + +- `Schema.URLFromSelf` -> `Schema.URL`: The self schema dropped the `FromSelf` suffix. + +- `Schema.UUID` -> `Schema.String.check(Schema.isUUID())`: Build the string schema with the UUID check. + +- `Schema.UUIDSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Uint8` -> `Schema.Number.check(Schema.isInt(), Schema.isBetween({ minimum: 0, maximum: 255 }))`: Rebuild the unsigned-byte schema from integer and range checks. + +- `Schema.Uint8Array` -> `Schema.toCodecJson(Schema.Uint8Array)`: Use the derived JSON codec to preserve v3's number-array encoding; v4 `Uint8Array` is the self schema. + +- `Schema.Uint8ArrayFromSelf` -> `Schema.Uint8Array`: The self schema dropped the `FromSelf` suffix. + +- `Schema.Uncapitalize` -> `Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isUncapitalized()), SchemaTransformation.uncapitalize()))`: Rebuild the uncapitalization transformation with `decodeTo`. + +- `Schema.Uncapitalized` -> `Schema.String.check(Schema.isUncapitalized())`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.UncapitalizedSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Undefined` -> `Schema.Undefined`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.UndefinedOr` -> `Schema.UndefinedOr`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Union` -> `Schema.Union(members)`: Pass union members as one array. + +- `Schema.UniqueSymbolFromSelf` -> `Schema.UniqueSymbol`: Use the v4 unique-symbol schema constructor. + +- `Schema.Unknown` -> `Schema.Unknown`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.Uppercase` -> `Schema.String.pipe(Schema.decodeTo(Schema.String.check(Schema.isUppercased()), SchemaTransformation.toUpperCase()))`: Rebuild the uppercase transformation with `decodeTo`. + +- `Schema.Uppercased` -> `Schema.String.check(Schema.isUppercased())`: Rebuild the removed convenience schema from the v4 base schema and check APIs. + +- `Schema.UppercasedSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.ValidDateFromSelf` -> `Schema.Date`: Use the v4 Date self schema, which rejects invalid Date values. + +- `Schema.ValidDateSchemaId` -> `none`: The v3 schema-id symbol was removed. Use the corresponding public v4 constructor/check instead of inspecting schema ids. + +- `Schema.Void` -> `Schema.Void`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.WithResult` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.All` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.Any` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.Context` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.Failure` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.FailureEncoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.Success` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.WithResult.SuccessEncoded` -> `none`: The v3 helper/protocol type was removed by the v4 Schema model rewrite. Use the public v4 constructor and infer its result types instead. + +- `Schema.annotations` -> `Schema.annotate`: Rename `annotations` to `annotate`. + +- `Schema.asSchema` -> `Schema.revealCodec`: Use `revealCodec` to expose a schema's codec type. + +- `Schema.asSerializable` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.asSerializableWithResult` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.asWithResult` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.attachPropertySignature` -> `Schema.tagDefaultOmit`: Map the struct fields and add `key: Schema.tagDefaultOmit(value)`; the old combinator was removed. + +- `Schema.between` -> `Schema.isBetween`: Rename the predicate to `isBetween` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.betweenBigDecimal` -> `Schema.isBetweenBigDecimal`: Rename the predicate to `isBetweenBigDecimal` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.betweenBigInt` -> `Schema.isBetweenBigInt`: Rename the predicate to `isBetweenBigInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.betweenDate` -> `Schema.isBetweenDate`: Rename the predicate to `isBetweenDate` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.betweenDuration` -> `Schema.isBetween`: Rename the predicate to `isBetween` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.brand` -> `Schema.brand`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.capitalized` -> `Schema.isCapitalized`: Rename the string predicate to `isCapitalized` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.clamp` -> `Schema.decodeTo + SchemaGetter.transform(Number.clamp(...))`: Rebuild clamping as an explicit reversible transformation. + +- `Schema.clampBigDecimal` -> `Schema.decodeTo + SchemaGetter.transform(BigDecimal.clamp(...))`: Rebuild BigDecimal clamping as an explicit reversible transformation. + +- `Schema.clampBigInt` -> `Schema.decodeTo + SchemaGetter.transform(BigInt.clamp(...))`: Rebuild bigint clamping as an explicit reversible transformation. + +- `Schema.clampDuration` -> `Schema.decodeTo + SchemaGetter.transform(Duration.clamp(...))`: Rebuild Duration clamping as an explicit reversible transformation. + +- `Schema.declare` -> `Schema.declare`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.decode` -> `Schema.decodeEffect`: Rename the effectful decoder. + +- `Schema.decodeEither` -> `Schema.decodeExit`: Rename the decoder returning an `Exit`. + +- `Schema.decodeUnknown` -> `Schema.decodeUnknownEffect`: Rename the effectful unknown-input decoder. + +- `Schema.decodeUnknownEither` -> `Schema.decodeUnknownExit`: Rename the unknown-input decoder returning an `Exit`. + +- `Schema.decodeUnknownPromise` -> `Schema.decodeUnknownPromise`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.deserialize` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.deserializeExit` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.deserializeFailure` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.deserializeSuccess` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.element` -> `none`: The tuple element wrapper was removed; express elements directly in `Tuple([...])` or use `TupleWithRest` for rest elements. + +- `Schema.encode` -> `Schema.encodeEffect`: Rename the effectful encoder. + +- `Schema.encodeEither` -> `Schema.encodeExit`: Rename the encoder returning an `Exit`. + +- `Schema.encodeUnknown` -> `Schema.encodeUnknownEffect`: Rename the effectful unknown-input encoder. + +- `Schema.encodeUnknownEither` -> `Schema.encodeUnknownExit`: Rename the unknown-input encoder returning an `Exit`. + +- `Schema.encodeUnknownPromise` -> `Schema.encodeUnknownPromise`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.encodedBoundSchema` -> `Schema.toEncoded`: Use the encoded side of the codec; service bounds are modeled by v4 codec service types. + +- `Schema.encodedSchema` -> `Schema.toEncoded`: Rename the encoded-side projection. + +- `Schema.endsWith` -> `Schema.isEndsWith`: Rename the string predicate to `isEndsWith` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.equivalence` -> `Schema.toEquivalence`: Rename the equivalence derivation utility. + +- `Schema.exitSchema` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.extend` -> `schema.mapFields(Struct.assign(fields))`: Replace struct extension with `mapFields(Struct.assign(...))` or `Schema.fieldsAssign`; map union members explicitly. + +- `Schema.failureSchema` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.filter` -> `Schema.check(Schema.makeFilter(predicate)) / Schema.refine(refinement)`: Use `check(makeFilter(...))` for predicates and `refine` for type refinements. + +- `Schema.filterEffect` -> `Schema.decode({ decode: SchemaGetter.checkEffect(...), encode: SchemaGetter.passthrough() })`: Rebuild effectful validation as a decode step with `SchemaGetter.checkEffect`. + +- `Schema.finite` -> `Schema.isFinite`: Rename the predicate to `isFinite` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.format` -> `SchemaRepresentation.toCodeDocument`: Build a representation with `SchemaRepresentation.toRepresentation`, `toMultiDocument`, then `toCodeDocument`. + +- `Schema.fromBrand` -> `Schema.fromBrand`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.fromKey` -> `Schema.encodeKeys`: Use `encodeKeys` to map decoded property names to encoded keys. + +- `Schema.getNumberIndexedAccess` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.greaterThan` -> `Schema.isGreaterThan`: Rename the predicate to `isGreaterThan` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanBigDecimal` -> `Schema.isGreaterThanBigDecimal`: Rename the predicate to `isGreaterThanBigDecimal` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanBigInt` -> `Schema.isGreaterThanBigInt`: Rename the predicate to `isGreaterThanBigInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanDate` -> `Schema.isGreaterThanDate`: Rename the predicate to `isGreaterThanDate` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanDuration` -> `Schema.isGreaterThan`: Rename the predicate to `isGreaterThan` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanOrEqualTo` -> `Schema.isGreaterThanOrEqualTo`: Rename the predicate to `isGreaterThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanOrEqualToBigDecimal` -> `Schema.isGreaterThanOrEqualToBigDecimal`: Rename the predicate to `isGreaterThanOrEqualToBigDecimal` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanOrEqualToBigInt` -> `Schema.isGreaterThanOrEqualToBigInt`: Rename the predicate to `isGreaterThanOrEqualToBigInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanOrEqualToDate` -> `Schema.isGreaterThanOrEqualToDate`: Rename the predicate to `isGreaterThanOrEqualToDate` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.greaterThanOrEqualToDuration` -> `Schema.isGreaterThanOrEqualTo`: Rename the predicate to `isGreaterThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.head` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.headNonEmpty` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.headOrElse` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.includes` -> `Schema.isIncludes`: Rename the string predicate to `isIncludes` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.instanceOf` -> `Schema.instanceOf`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.int` -> `Schema.isInt`: Rename the predicate to `isInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.isPropertySignature` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.isSchema` -> `Schema.isSchema`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.itemsCount` -> `Schema.isLengthBetween`: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.keyof` -> `none`: Removed with the schema model rewrite; derive keys from struct fields or use `Schema.Literals` explicitly. + +- `Schema.length` -> `Schema.isLengthBetween`: Use `isLengthBetween` with equal minimum and maximum values for an exact string length. + +- `Schema.lessThan` -> `Schema.isLessThan`: Rename the predicate to `isLessThan` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanBigDecimal` -> `Schema.isLessThanBigDecimal`: Rename the predicate to `isLessThanBigDecimal` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanBigInt` -> `Schema.isLessThanBigInt`: Rename the predicate to `isLessThanBigInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanDate` -> `Schema.isLessThanDate`: Rename the predicate to `isLessThanDate` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanDuration` -> `Schema.isLessThan`: Rename the predicate to `isLessThan` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanOrEqualTo` -> `Schema.isLessThanOrEqualTo`: Rename the predicate to `isLessThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanOrEqualToBigDecimal` -> `Schema.isLessThanOrEqualToBigDecimal`: Rename the predicate to `isLessThanOrEqualToBigDecimal` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanOrEqualToBigInt` -> `Schema.isLessThanOrEqualToBigInt`: Rename the predicate to `isLessThanOrEqualToBigInt` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanOrEqualToDate` -> `Schema.isLessThanOrEqualToDate`: Rename the predicate to `isLessThanOrEqualToDate` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lessThanOrEqualToDuration` -> `Schema.isLessThanOrEqualTo`: Rename the predicate to `isLessThanOrEqualTo` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.lowercased` -> `Schema.isLowercased`: Rename the string predicate to `isLowercased` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.make` -> `Schema.make`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.makePropertySignature` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.maxItems` -> `Schema.isMaxLength`: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.maxLength` -> `Schema.isMaxLength`: Rename the string predicate to `isMaxLength` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.minItems` -> `Schema.isMinLength`: Use the v4 collection-size check and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.minLength` -> `Schema.isMinLength`: Rename the string predicate to `isMinLength` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.multipleOf` -> `Schema.isMultipleOf`: Rename the predicate to `isMultipleOf` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.mutable` -> `Schema.mutable`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.negative` -> `Schema.isLessThan(0)`: Use `isLessThan(0)` as a v4 check. + +- `Schema.negativeBigDecimal` -> `Schema.isLessThanBigDecimal(BigDecimal.fromNumber(0))`: Use `isLessThanBigDecimal` as a v4 check. + +- `Schema.negativeBigInt` -> `Schema.isLessThanBigInt(0n)`: Use `isLessThanBigInt(0n)` as a v4 check. + +- `Schema.nonEmptyString` -> `Schema.isNonEmpty`: Rename the string predicate to `isNonEmpty` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.nonNaN` -> `Schema.makeFilter((n) => !Number.isNaN(n))`: Use an explicit filter because v4 has no dedicated non-NaN check. + +- `Schema.nonNegative` -> `Schema.isGreaterThanOrEqualTo(0)`: Use `isGreaterThanOrEqualTo(0)` as a v4 check. + +- `Schema.nonNegativeBigDecimal` -> `Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromNumber(0))`: Use `isGreaterThanOrEqualToBigDecimal` as a v4 check. + +- `Schema.nonNegativeBigInt` -> `Schema.isGreaterThanOrEqualToBigInt(0n)`: Use `isGreaterThanOrEqualToBigInt(0n)` as a v4 check. + +- `Schema.nonPositive` -> `Schema.isLessThanOrEqualTo(0)`: Use `isLessThanOrEqualTo(0)` as a v4 check. + +- `Schema.nonPositiveBigDecimal` -> `Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromNumber(0))`: Use `isLessThanOrEqualToBigDecimal` as a v4 check. + +- `Schema.nonPositiveBigInt` -> `Schema.isLessThanOrEqualToBigInt(0n)`: Use `isLessThanOrEqualToBigInt(0n)` as a v4 check. + +- `Schema.omit` -> `schema.mapFields(Struct.omit([keys]))`: Use `mapFields` with `Struct.omit`; pass keys as an array. + +- `Schema.optional` -> `Schema.optional`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.optionalElement` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.optionalToOptional` -> `Schema.decodeTo + SchemaGetter.transformOptional`: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. + +- `Schema.optionalToRequired` -> `Schema.decodeTo + SchemaGetter.transformOptional`: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. + +- `Schema.optionalWith` -> `Schema.optional / Schema.optionalKey / Schema.withDecodingDefaultType`: Choose `optional` or `optionalKey`; use the decoding-default helpers and an explicit nullable transformation as required by the old options. + +- `Schema.parseJson` -> `Schema.UnknownFromJsonString / Schema.fromJsonString(schema)`: Use `UnknownFromJsonString` without an inner schema or `fromJsonString(schema)` with one. + +- `Schema.parseNumber` -> `Schema.NumberFromString`: Use the built-in string-to-number codec. + +- `Schema.partial` -> `schema.mapFields(Struct.map(Schema.optional))`: Map struct fields with `Schema.optional`. + +- `Schema.partialWith` -> `schema.mapFields(Struct.map(Schema.optionalKey))`: For `{ exact: true }`, map struct fields with `Schema.optionalKey`; choose field helpers explicitly for other options. + +- `Schema.pattern` -> `Schema.isPattern`: Rename the string predicate to `isPattern` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.pick` -> `schema.mapFields(Struct.pick([keys]))`: Use `mapFields` with `Struct.pick`; pass keys as an array. + +- `Schema.pickLiteral` -> `Schema.Literals(values).pick(selected)`: Build a `Literals` schema from an array and call its `pick` method. + +- `Schema.pluck` -> `none`: No direct replacement remains; pick the field then use `decodeTo` with `SchemaGetter.transform` to map between the field and enclosing object. + +- `Schema.positive` -> `Schema.isGreaterThan(0)`: Use `isGreaterThan(0)` as a v4 check. + +- `Schema.positiveBigDecimal` -> `Schema.isGreaterThanBigDecimal(BigDecimal.fromNumber(0))`: Use `isGreaterThanBigDecimal` as a v4 check. + +- `Schema.positiveBigInt` -> `Schema.isGreaterThanBigInt(0n)`: Use `isGreaterThanBigInt(0n)` as a v4 check. + +- `Schema.propertySignature` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.refine` -> `Schema.refine`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.rename` -> `Schema.encodeKeys`: Use `encodeKeys` for encoded-key renaming. + +- `Schema.requiredToOptional` -> `Schema.decodeTo + SchemaGetter.transformOptional`: Rebuild optional-field transformations with `decodeTo` and `SchemaGetter.transformOptional`. + +- `Schema.serializableSchema` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.serialize` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.serializeExit` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.serializeFailure` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.serializeSuccess` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.split` -> `Schema.String.pipe(Schema.decodeTo(Schema.Array(Schema.String), SchemaTransformation.transform(...)))`: Rebuild splitting as an explicit reversible string/array transformation. + +- `Schema.standardSchemaV1` -> `Schema.toStandardSchemaV1`: Rename the Standard Schema adapter. + +- `Schema.startsWith` -> `Schema.isStartsWith`: Rename the string predicate to `isStartsWith` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.successSchema` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.suspend` -> `Schema.suspend`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.symbolSerializable` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.symbolWithResult` -> `none`: Compared the v3 declaration with v4 Schema and the schema migration guide; no direct public replacement remains. Rebuild the behavior from public v4 codecs/getters where still required. + +- `Schema.tag` -> `Schema.tag`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.transform` -> `schema.pipe(Schema.decodeTo(target, SchemaTransformation.transform({ decode, encode })))`: Replace the constructor with `decodeTo` and a `SchemaTransformation`. + +- `Schema.transformLiteral` -> `Schema.Literal(from).transform(to)`: Use the literal schema's `transform` method. + +- `Schema.transformLiterals` -> `Schema.Literals(fromValues).transform(toValues)`: Split the pairs into parallel arrays and use `Literals(...).transform(...)`. + +- `Schema.transformOrFail` -> `schema.pipe(Schema.decodeTo(target, { decode: SchemaGetter.transformOrFail(...), encode: ... }))`: Replace the constructor with `decodeTo` and fallible `SchemaGetter` transformations. + +- `Schema.trimmed` -> `Schema.isTrimmed`: Rename the string predicate to `isTrimmed` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.typeSchema` -> `Schema.toType`: Rename the type-side projection. + +- `Schema.uncapitalized` -> `Schema.isUncapitalized`: Rename the string predicate to `isUncapitalized` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.uppercased` -> `Schema.isUppercased`: Rename the string predicate to `isUppercased` and apply it with `Schema.check` or a schema's `check` method. + +- `Schema.validDate` -> `Schema.Date`: Use the v4 Date self schema, which rejects invalid Date values. + +- `Schema.validate` -> `Schema.decodeEffect(Schema.toType(schema))`: Validation was removed; decode through the schema's type side. + +- `Schema.validateEither` -> `Schema.decodeExit(Schema.toType(schema))`: Validation was removed; decode through the schema's type side. + +- `Schema.validatePromise` -> `Schema.decodePromise(Schema.toType(schema))`: Validation was removed; decode through the schema's type side. + +- `Schema.withConstructorDefault` -> `Schema.withConstructorDefault`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.withDecodingDefault` -> `Schema.withDecodingDefault`: The API remains public in v4, but its type/value declaration was consolidated; use the v4 declaration and update inferred types/signature as needed. + +- `Schema.withDefaults` -> `none`: Removed; choose `withConstructorDefault` and decoding-default helpers explicitly for each side. + +### `effect/SchemaAST` + +- `SchemaAST.AST` -> `SchemaAST.AST`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.Annotated` -> `SchemaAST.Base`: All v4 AST nodes extend Base, which owns annotations, checks, encoding, and context. + +- `SchemaAST.AnyKeyword` -> `SchemaAST.Any`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.ArbitraryAnnotationId` -> `Schema.Annotations.ToArbitrary`: Symbol annotation IDs were removed; use the toArbitrary annotation key and its Schema.Annotations types. + +- `SchemaAST.BatchingAnnotation` -> `none`: Per-schema batching annotations were removed; control asynchronous parsing with ParseOptions.concurrency. + +- `SchemaAST.BatchingAnnotationId` -> `none`: Symbol annotation IDs were removed and batching is no longer a schema annotation. + +- `SchemaAST.BigIntKeyword` -> `SchemaAST.BigInt`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.BooleanKeyword` -> `SchemaAST.Boolean`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.BrandAnnotation` -> `Schema.Annotations.Bottom["brands"]`: Brands are stored under the string-keyed brands annotation and normally added with Schema.brand. + +- `SchemaAST.BrandAnnotationId` -> `Schema.brand`: Symbol annotation IDs were removed; add brands through Schema.brand. + +- `SchemaAST.Compiler` -> `none`: The generic AST compiler abstraction was removed; traverse the discriminated SchemaAST.AST union directly or use a higher-level Schema derivation API. + +- `SchemaAST.ComposeTransformation` -> `SchemaAST.Encoding`: The marker transformation was replaced by explicit SchemaAST.Link encoding chains. + +- `SchemaAST.ConcurrencyAnnotation` -> `SchemaAST.ParseOptions["concurrency"]`: Concurrency is now a parse option rather than its own annotation type. + +- `SchemaAST.ConcurrencyAnnotationId` -> `Schema.Annotations.Bottom["parseOptions"]`: Symbol annotation IDs were removed; put concurrency inside the parseOptions annotation. + +- `SchemaAST.Declaration` -> `SchemaAST.Declaration`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.DecodingFallbackAnnotation` -> `Schema.catchDecoding`: Fallbacks are now encoding middleware added with Schema.catchDecoding. + +- `SchemaAST.DecodingFallbackAnnotationId` -> `Schema.catchDecoding`: The symbol annotation was removed; attach decoding recovery with Schema.catchDecoding. + +- `SchemaAST.DefaultAnnotation` -> `Schema.Annotations.Documentation["default"]`: Defaults are string-keyed schema annotations in v4. + +- `SchemaAST.DefaultAnnotationId` -> `Schema.Annotations.Documentation["default"]`: Symbol annotation IDs were removed; use the default key. + +- `SchemaAST.DescriptionAnnotation` -> `Schema.Annotations.Augment["description"]`: Descriptions are string-keyed schema annotations in v4. + +- `SchemaAST.DescriptionAnnotationId` -> `Schema.Annotations.Augment["description"]`: Symbol annotation IDs were removed; use the description key. + +- `SchemaAST.DocumentationAnnotation` -> `Schema.Annotations.Augment["documentation"]`: Documentation is a string-keyed schema annotation in v4. + +- `SchemaAST.DocumentationAnnotationId` -> `Schema.Annotations.Augment["documentation"]`: Symbol annotation IDs were removed; use the documentation key. + +- `SchemaAST.Enums` -> `SchemaAST.Enum`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.EquivalenceAnnotation` -> `Schema.Annotations.ToEquivalence.Declaration`: Equivalence derivation annotations now use the toEquivalence key in Schema.Annotations. + +- `SchemaAST.EquivalenceAnnotationId` -> `Schema.overrideToEquivalence`: The symbol annotation was removed; attach custom equivalence derivation with Schema.overrideToEquivalence. + +- `SchemaAST.ExamplesAnnotation` -> `Schema.Annotations.Documentation["examples"]`: Examples are string-keyed schema annotations in v4. + +- `SchemaAST.ExamplesAnnotationId` -> `Schema.Annotations.Documentation["examples"]`: Symbol annotation IDs were removed; use the examples key. + +- `SchemaAST.FinalTransformation` -> `SchemaTransformation.Transformation`: Transformations moved to SchemaTransformation and are stored in SchemaAST.Link values. + +- `SchemaAST.IdentifierAnnotation` -> `Schema.Annotations.Bottom["identifier"]`: Identifiers are string-keyed schema annotations in v4. + +- `SchemaAST.IdentifierAnnotationId` -> `Schema.Annotations.Bottom["identifier"]`: Symbol annotation IDs were removed; use the identifier key. + +- `SchemaAST.IndexSignature` -> `SchemaAST.IndexSignature`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.JSONIdentifierAnnotationId` -> `Schema.Annotations.Bottom["identifier"]`: The separate JSON identifier symbol was removed; use identifier. + +- `SchemaAST.JSONSchemaAnnotation` -> `JsonSchema.JsonSchema`: JSON Schema values use the v4 JsonSchema model; generation hooks use Schema representation annotations. + +- `SchemaAST.JSONSchemaAnnotationId` -> `Schema.Annotations.Filter["toJsonSchema"]`: The symbol annotation was replaced by the toJsonSchema key on check annotations. + +- `SchemaAST.Literal` -> `SchemaAST.Literal`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.LiteralValue` -> `SchemaAST.LiteralValue`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.Match` -> `none`: The Match compiler table was removed; traverse the discriminated SchemaAST.AST union directly. + +- `SchemaAST.Members` -> `ReadonlyArray`: Union members are ordinary readonly arrays in v4. + +- `SchemaAST.MessageAnnotation` -> `Schema.Annotations.Bottom["message"]`: Messages are string-keyed annotations and no longer receive the old ParseIssue callback shape. + +- `SchemaAST.MessageAnnotationId` -> `Schema.Annotations.Bottom["message"]`: Symbol annotation IDs were removed; use the message key. + +- `SchemaAST.MissingMessageAnnotation` -> `Schema.Annotations.Key["messageMissingKey"]`: Missing-key messages use the messageMissingKey key. + +- `SchemaAST.MissingMessageAnnotationId` -> `Schema.Annotations.Key["messageMissingKey"]`: Symbol annotation IDs were removed; use messageMissingKey. + +- `SchemaAST.NeverKeyword` -> `SchemaAST.Never`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.NumberKeyword` -> `SchemaAST.Number`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.ObjectKeyword` -> `SchemaAST.ObjectKeyword`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.OptionalType` -> `SchemaAST.Context`: Element and property optionality moved into per-node Context. + +- `SchemaAST.Parameter` -> `SchemaAST.AST`: The dedicated index-parameter union was removed; v4 validates supported key AST variants when building an IndexSignature. + +- `SchemaAST.ParseIssueTitleAnnotation` -> `none`: Issue-title callbacks were removed; use message or expected annotations and SchemaIssue formatters. + +- `SchemaAST.ParseIssueTitleAnnotationId` -> `none`: The symbol annotation was removed; use message or expected annotations. + +- `SchemaAST.ParseJsonSchemaId` -> `Schema.UnknownFromJsonString`: Use the built-in JSON string codec instead of checking the old schema ID. + +- `SchemaAST.ParseOptions` -> `SchemaAST.ParseOptions`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.ParseOptionsAnnotationId` -> `Schema.Annotations.Bottom["parseOptions"]`: Symbol annotation IDs were removed; use the parseOptions key. + +- `SchemaAST.PrettyAnnotationId` -> `Schema.overrideToFormatter`: The symbol annotation was removed; attach custom formatters with Schema.overrideToFormatter. + +- `SchemaAST.PropertySignature` -> `SchemaAST.PropertySignature`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.PropertySignatureTransformation` -> `Schema.encodeKeys`: Property-key transformations are now encoding links, normally built with Schema.encodeKeys. + +- `SchemaAST.Refinement` -> `SchemaAST.Check`: Refinements became Filter or FilterGroup checks attached to an AST node. + +- `SchemaAST.SchemaIdAnnotation` -> `Schema.Annotations.Bottom["identifier"]`: Schema IDs were consolidated into identifier annotations. + +- `SchemaAST.SchemaIdAnnotationId` -> `Schema.Annotations.Bottom["identifier"]`: Symbol annotation IDs were removed; use identifier. + +- `SchemaAST.StringKeyword` -> `SchemaAST.String`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.SurrogateAnnotation` -> `SchemaRepresentation.RepresentationAnnotation`: Surrogate AST metadata was replaced by schema representation annotations and declaration codec hooks. + +- `SchemaAST.SurrogateAnnotationId` -> `Schema.Annotations.Declaration["representation"]`: The symbol annotation was replaced by the representation key. + +- `SchemaAST.Suspend` -> `SchemaAST.Suspend`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.SymbolKeyword` -> `SchemaAST.Symbol`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.TemplateLiteral` -> `SchemaAST.TemplateLiteral`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.TemplateLiteralSpan` -> `SchemaAST.TemplateLiteral`: Template literal parts are represented directly as AST values in v4. + +- `SchemaAST.TitleAnnotation` -> `Schema.Annotations.Augment["title"]`: Titles are string-keyed schema annotations in v4. + +- `SchemaAST.TitleAnnotationId` -> `Schema.Annotations.Augment["title"]`: Symbol annotation IDs were removed; use title. + +- `SchemaAST.Transformation` -> `SchemaAST.Link`: Transformations are links in the Base.encoding chain in v4. + +- `SchemaAST.TransformationKind` -> `SchemaTransformation.Transformation`: Transformation implementations moved to SchemaTransformation and are stored on SchemaAST.Link. + +- `SchemaAST.TupleType` -> `SchemaAST.Arrays`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.Type` -> `SchemaAST.AST`: The tuple-element Type wrapper was removed; optionality and mutability moved to Context. + +- `SchemaAST.TypeConstructorAnnotation` -> `Schema.Annotations.Declaration["toCodec"]`: Type-constructor behavior moved to declaration codec annotations. + +- `SchemaAST.TypeConstructorAnnotationId` -> `Schema.Annotations.Declaration["toCodec"]`: Symbol annotation IDs were removed; use declaration codec annotation keys. + +- `SchemaAST.TypeLiteral` -> `SchemaAST.Objects`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.TypeLiteralTransformation` -> `SchemaAST.Encoding`: Object transformations are encoding links; use Schema.encodeKeys for key mappings. + +- `SchemaAST.UndefinedKeyword` -> `SchemaAST.Undefined`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.Union` -> `SchemaAST.Union`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.UniqueSymbol` -> `SchemaAST.UniqueSymbol`: The name remains, but its constructor and fields changed in the v4 Base/check/context/encoding model. + +- `SchemaAST.UnknownKeyword` -> `SchemaAST.Unknown`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.VoidKeyword` -> `SchemaAST.Void`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.annotations` -> `SchemaAST.annotate`: Use the v4 annotation helper and string-keyed Schema.Annotations. + +- `SchemaAST.anyKeyword` -> `SchemaAST.any`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.bigIntKeyword` -> `SchemaAST.bigInt`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.booleanKeyword` -> `SchemaAST.boolean`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.composeTransformation` -> `SchemaAST.Encoding`: V4 transformations are SchemaAST.Link values in an encoding chain; compose by adding links with SchemaAST.decodeTo. + +- `SchemaAST.defaultParseOption` -> `SchemaAST.defaultParseOptions`: The default parse options constant was pluralized. + +- `SchemaAST.encodedAST` -> `SchemaAST.toEncoded`: The encoded projection helper was renamed. + +- `SchemaAST.encodedBoundAST` -> `SchemaAST.toEncoded`: The separate encoded-bound projection was removed; use the encoded projection and v4 encoding links. + +- `SchemaAST.getAnnotation` -> `SchemaAST.resolveAt`: Resolve string-keyed annotations with resolveAt, or use resolveIdentifier, resolveTitle, and resolveDescription. + +- `SchemaAST.getBatchingAnnotation` -> `none`: Batching annotations were removed; read ParseOptions.concurrency when controlling asynchronous parsing. + +- `SchemaAST.getBrandAnnotation` -> `SchemaAST.resolveAt("brands")`: Resolve the string-keyed brands annotation. + +- `SchemaAST.getCompiler` -> `none`: The Match-based compiler was removed; traverse SchemaAST.AST directly or use the relevant Schema derivation API. + +- `SchemaAST.getConcurrencyAnnotation` -> `SchemaAST.resolveAt("parseOptions")`: Resolve parseOptions and read concurrency from it. + +- `SchemaAST.getDecodingFallbackAnnotation` -> `none`: Fallbacks are encoding middleware in v4, not readable annotations; attach them with Schema.catchDecoding. + +- `SchemaAST.getDefaultAnnotation` -> `SchemaAST.resolveAt("default")`: Resolve the string-keyed default annotation. + +- `SchemaAST.getDescriptionAnnotation` -> `SchemaAST.resolveDescription`: Use the dedicated resolved-description helper. + +- `SchemaAST.getDocumentationAnnotation` -> `SchemaAST.resolveAt("documentation")`: Resolve the string-keyed documentation annotation. + +- `SchemaAST.getExamplesAnnotation` -> `SchemaAST.resolveAt("examples")`: Resolve the string-keyed examples annotation. + +- `SchemaAST.getIdentifierAnnotation` -> `SchemaAST.resolveIdentifier`: Use the dedicated resolved-identifier helper. + +- `SchemaAST.getJSONIdentifier` -> `SchemaAST.resolveIdentifier`: JSON Schema references now use the normal resolved identifier. + +- `SchemaAST.getJSONIdentifierAnnotation` -> `SchemaAST.resolveIdentifier`: The separate JSON identifier annotation was removed; use identifier. + +- `SchemaAST.getJSONSchemaAnnotation` -> `SchemaAST.resolveAt("toJsonSchema")`: JSON Schema generation hooks use the string-keyed toJsonSchema annotation on checks. + +- `SchemaAST.getMessageAnnotation` -> `SchemaAST.resolveAt("message")`: Resolve the string-keyed message annotation. + +- `SchemaAST.getMissingMessageAnnotation` -> `SchemaAST.resolveAt("messageMissingKey")`: Missing-key messages use the messageMissingKey key. + +- `SchemaAST.getParseIssueTitleAnnotation` -> `none`: Issue-title callbacks were removed; use message or expected annotations and SchemaIssue formatters. + +- `SchemaAST.getParseOptionsAnnotation` -> `SchemaAST.resolveAt("parseOptions")`: Resolve the string-keyed parseOptions annotation. + +- `SchemaAST.getPropertySignatures` -> `SchemaAST.Objects.propertySignatures`: Narrow to Objects and read propertySignatures directly. + +- `SchemaAST.getSchemaIdAnnotation` -> `SchemaAST.resolveIdentifier`: Schema IDs were consolidated into the identifier annotation. + +- `SchemaAST.getSurrogateAnnotation` -> `SchemaAST.resolveAt("representation")`: Surrogate AST annotations were replaced by representation annotations and declaration codec hooks. + +- `SchemaAST.getTemplateLiteralCapturingRegExp` -> `none`: The low-level RegExp compiler was removed; use Schema.TemplateLiteral and schema parsing instead. + +- `SchemaAST.getTemplateLiteralRegExp` -> `none`: The low-level RegExp compiler was removed; use Schema.TemplateLiteral and schema parsing instead. + +- `SchemaAST.getTitleAnnotation` -> `SchemaAST.resolveTitle`: Use the dedicated resolved-title helper. + +- `SchemaAST.getTypeConstructorAnnotation` -> `SchemaAST.resolveAt("toCodec")`: Type-constructor behavior moved to declaration codec annotations. + +- `SchemaAST.isAnyKeyword` -> `SchemaAST.isAny`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isBigIntKeyword` -> `SchemaAST.isBigInt`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isBooleanKeyword` -> `SchemaAST.isBoolean`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isComposeTransformation` -> `none`: Compose transformation markers were replaced by explicit SchemaAST.Link encoding chains. + +- `SchemaAST.isEnums` -> `SchemaAST.isEnum`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isFinalTransformation` -> `SchemaTransformation.Transformation`: Use SchemaTransformation guards or the transformation object stored on a SchemaAST.Link. + +- `SchemaAST.isNeverKeyword` -> `SchemaAST.isNever`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isNumberKeyword` -> `SchemaAST.isNumber`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isParameter` -> `SchemaAST.isString`: The Parameter union was removed; inspect the v4 key AST variants directly. + +- `SchemaAST.isRefinement` -> `SchemaAST.Check`: Refinement AST nodes became checks attached to Base.checks. + +- `SchemaAST.isStringKeyword` -> `SchemaAST.isString`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isSymbolKeyword` -> `SchemaAST.isSymbol`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isTransformation` -> `SchemaAST.Encoding`: Transformation AST nodes became encoding links attached to Base.encoding. + +- `SchemaAST.isTupleType` -> `SchemaAST.isArrays`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isTypeLiteral` -> `SchemaAST.isObjects`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isTypeLiteralTransformation` -> `Schema.encodeKeys`: Property-key transformations are represented by encoding links and normally built with Schema.encodeKeys. + +- `SchemaAST.isUndefinedKeyword` -> `SchemaAST.isUndefined`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isUnknownKeyword` -> `SchemaAST.isUnknown`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.isVoidKeyword` -> `SchemaAST.isVoid`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.keyof` -> `none`: Low-level SchemaAST.keyof was removed; model the desired key literals explicitly. + +- `SchemaAST.mutable` -> `Schema.mutable`: Apply mutability at the Schema level; AST property mutability is represented by Context. + +- `SchemaAST.neverKeyword` -> `SchemaAST.never`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.null` -> `SchemaAST.null`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.numberKeyword` -> `SchemaAST.number`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.omit` -> `Schema.mapFields + Struct.omit`: Object projection moved to schema field transforms. + +- `SchemaAST.partial` -> `Schema.mapFields + Struct.map(Schema.optional)`: Partial object transforms moved to schema field transforms. + +- `SchemaAST.required` -> `Schema.mapFields + Struct.map(Schema.requiredKey)`: Required object transforms moved to schema field transforms. + +- `SchemaAST.stringKeyword` -> `SchemaAST.string`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.symbolKeyword` -> `SchemaAST.symbol`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.typeAST` -> `SchemaAST.toType`: The type-side projection helper was renamed. + +- `SchemaAST.undefinedKeyword` -> `SchemaAST.undefined`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.unknownKeyword` -> `SchemaAST.unknown`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +- `SchemaAST.voidKeyword` -> `SchemaAST.void`: The v4 SchemaAST redesign renamed this primitive, collection, or guard while preserving its role. + +### `effect/Scope` + +- `Scope.CloseableScope` -> `Scope.Closeable`: Use the renamed type and close it with Scope.close(scope, exit). + +- `Scope.CloseableScopeTypeId` -> `none`: The closeable-scope marker is private in v4. + +- `Scope.Scope` -> `Scope.Scope`: The type remains; use module functions instead of the removed instance methods. + +- `Scope.Scope.Closeable` -> `Scope.Closeable`: The nested alias is now the top-level Closeable interface. + +- `Scope.Scope.Finalizer` -> `(exit: Exit.Exit) => Effect.Effect`: No alias is exported; inline the Scope.addFinalizerExit callback type. + +- `Scope.ScopeTypeId` -> `none`: The Scope marker is private in v4 and has no public guard. + +- `Scope.extend` -> `Scope.provide`: The operation was renamed with the same data-first and curried forms. + +### `effect/ScopedCache` + +- `ScopedCache.Lookup` -> `(key: Key) => Effect.Effect`: The named alias was removed; use an inline lookup type or ScopedCache.ScopedCache\["lookup"]. + +- `ScopedCache.ScopedCache` -> `ScopedCache.ScopedCache`: The model remains as a Pipeable scoped cache; construct and use it inside a Scope with explicit ScopedCache operations. + +- `ScopedCache.ScopedCache.Variance` -> `none`: The public variance marker was removed; use ScopedCache.ScopedCache directly. + +- `ScopedCache.ScopedCacheTypeId` -> `none`: The ScopedCache type id is internal in v4; do not inspect or construct the brand directly. + +### `effect/ScopedRef` + +- `ScopedRef.ScopedRef` -> `ScopedRef.ScopedRef`: The type remains but no longer extends Effect; use ScopedRef.get or ScopedRef.getUnsafe. + +- `ScopedRef.ScopedRef.Variance` -> `none`: The exported variance artifact was removed. + +- `ScopedRef.ScopedRefTypeId` -> `none`: The marker is private in v4 and no public guard exists. + +- `ScopedRef.ScopedRefUnify` -> `none`: ScopedRef no longer extends Effect; use ScopedRef.get explicitly. + +- `ScopedRef.ScopedRefUnifyIgnore` -> `none`: The Effect-unification implementation detail was removed. + +### `effect/Secret` + +- `Secret.Secret` -> `Redacted.Redacted`: Secret was deprecated in v3 and removed in v4; use the generic Redacted wrapper. + +- `Secret.Secret.Proto` -> `none`: The Secret-specific prototype was removed with the module; use Redacted.Redacted\. + +- `Secret.SecretTypeId` -> `Redacted.isRedacted`: The Secret marker was removed; use the Redacted runtime guard. + +- `Secret.fromIterable` -> `Redacted.make(Array.from(iterable).join(""))`: Secret was removed; join the character iterable and wrap the resulting string in Redacted. + +- `Secret.isSecret` -> `Redacted.isRedacted`: Secret was removed in favor of Redacted. + +- `Secret.make` -> `Redacted.make(bytes.map((byte) => String.fromCharCode(byte)).join(""))`: Secret was removed; preserve the v3 byte-to-code-unit conversion explicitly, then wrap the string in Redacted. + +- `Secret.unsafeWipe` -> `Redacted.wipeUnsafe`: Redacted.wipeUnsafe removes the registry entry but, unlike v3 Secret, cannot zero a retained mutable byte array; zero external buffers separately when required. + +### `effect/SingleProducerAsyncInput` + +- `SingleProducerAsyncInput.AsyncInputConsumer` -> `Queue.Dequeue`: Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one. + +- `SingleProducerAsyncInput.AsyncInputProducer` -> `Queue.Enqueue`: Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one. + +- `SingleProducerAsyncInput.SingleProducerAsyncInput` -> `Queue.Queue`: Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one. + +- `SingleProducerAsyncInput.make` -> `Queue.make`: Use v4 Queue producer and consumer views and bridge to Channel with Channel.fromQueue, Channel.callback, or Channel.fromPull. Queue.make({ capacity: 0 }) is the closest rendezvous backpressure but is not one-for-one. + +### `effect/Sink` + +- `Sink.Sink` -> `Sink`: Interface kept as Sink\ with the same type parameters; the internal representation changed from a Channel wrapper to a `transform: (upstream: Pull>, scope) => Effect>` function, and completion is the tuple `Sink.End = readonly [value, leftover?]`. + +- `Sink.Sink.Variance` -> `Sink.Variance`: Still exists as the namespace interface Sink.Variance\ (with Sink.VarianceStruct); the variance key is now the internal string "\~effect/Sink" instead of the SinkTypeId symbol. + +- `Sink.SinkTypeId` -> `none`: The type id is the unexported internal string "\~effect/Sink" (no unique symbol, no export); use the new guard Sink.isSink(u) instead of checking the symbol. + +- `Sink.SinkUnify` -> `SinkUnify`: Kept with the same name and shape (extends Effect.EffectUnify, preserves all five Sink type parameters). + +- `Sink.SinkUnifyIgnore` -> `SinkUnifyIgnore`: Kept with the same name; now a standalone `{ Effect?: true }` interface instead of extending Effect.EffectUnifyIgnore. + +- `Sink.collectAll` -> `Sink.collect`: Renamed; returns Sink\, In\> collecting into a plain mutable Array instead of Chunk. + +- `Sink.collectAllFrom` -> `none`: Repeated-run result accumulation was removed; checked the v4 export list (no collectAllFrom/repeatedly). Re-implement with Sink.fromTransform, looping self.transform on the upstream pull (feeding leftovers back) until the upstream ends, accumulating results in an array. + +- `Sink.collectAllN` -> `Sink.take`: Sink.take(n) returns Sink\, In, In\> collecting up to n elements (Array instead of Chunk), emitting the unconsumed remainder as leftovers. + +- `Sink.collectAllToMap` -> `Sink.reduce`: Built-in HashMap collector removed; build a plain Map in the reducer: Sink.reduce(() =\> new Map\(), (m, in\_) =\> { const k = key(in\_); return m.set(k, m.has(k) ? merge(m.get(k)!, in\_) : in\_) }). + +- `Sink.collectAllToMapN` -> `none`: Removed; approximating with Sink.reduceWhile((...) , (m) =\> m.size \< n, ...) consumes (merges) the element that introduces the (n+1)-th key, whereas v3 left it as leftover — exact v3 leftover behavior needs a custom Sink.fromTransform that checks the key before consuming. + +- `Sink.collectAllToSet` -> `Sink.reduce`: Built-in HashSet collector removed; build a plain Set: Sink.reduce(() =\> new Set\(), (s, in\_) =\> s.add(in\_)). + +- `Sink.collectAllToSetN` -> `Sink.reduceWhile`: Removed; equivalent via Sink.reduceWhile(() =\> new Set\(), (s) =\> s.size \< n, (s, in\_) =\> s.add(in\_)) — stops with leftovers once n distinct values are collected (plain Set instead of HashSet). + +- `Sink.collectAllUntil` -> `Sink.takeUntil`: Renamed; Sink.takeUntil(predicate) collects into Array\ until the predicate matches, including the matching element, like v3. + +- `Sink.collectAllUntilEffect` -> `Sink.takeUntilEffect`: Renamed; Sink.takeUntilEffect(p) collects into Array\ until the effectful predicate returns true, including the matching element. + +- `Sink.collectAllWhile` -> `Sink.takeWhile`: Renamed; Sink.takeWhile(predicate) collects the matching prefix into Array\, keeps the refinement overload, and excludes the first failing element (returned via leftovers). + +- `Sink.collectAllWhileEffect` -> `Sink.takeWhileEffect`: Renamed; Sink.takeWhileEffect(p) collects into Array\ while the effectful predicate returns true. + +- `Sink.collectAllWhileWith` -> `none`: Repeatedly-run-and-fold was removed (no v4 counterpart in the export list); re-implement with Sink.fromTransform looping self.transform while the `while` predicate holds on each result, folding results with `body` and feeding leftovers back into the next run. + +- `Sink.collectLeftover` -> `Sink.mapEnd`: Use Sink.mapEnd to move the leftovers into the result: Sink.mapEnd(self, ([a, leftover]) =\> [[a, leftover ?? []] as const]); leftovers are NonEmptyReadonlyArray\ | undefined instead of Chunk\. + +- `Sink.context` -> `Sink.fromEffect(Effect.context())`: Sink.context was removed; Sink.fromEffect(Effect.context\()) yields the same Sink\, unknown, never, never, R\>. + +- `Sink.contextWith` -> `Sink.fromEffect(Effect.contextWith(f))`: Removed; compose Sink.fromEffect with Effect.contextWith to derive a value from the context. + +- `Sink.contextWithEffect` -> `Sink.fromEffect(Effect.flatMap(Effect.context(), f))`: Removed, and v4 Effect has no contextWithEffect; use Sink.fromEffect(Effect.flatMap(Effect.context\(), f)). + +- `Sink.contextWithSink` -> `Sink.unwrap(Effect.contextWith(f))`: Removed; Sink.unwrap(Effect.contextWith((ctx: Context.Context\) =\> f(ctx))) builds the sink from the context. + +- `Sink.dieMessage` -> `Sink.die`: Removed (v4 has no RuntimeException-based dieMessage anywhere); use Sink.die(new Error(message)). + +- `Sink.dieSync` -> `Sink.failCauseSync`: Removed; use Sink.failCauseSync(() =\> Cause.die(evaluate())) to defer defect evaluation, or Sink.die(defect) when eager is fine. + +- `Sink.dimap` -> `Sink.mapInput + Sink.map`: Removed; compose the two halves: self.pipe(Sink.mapInput(f), Sink.map(g)). + +- `Sink.dimapChunks` -> `Sink.mapInputArray + Sink.map`: Removed; compose self.pipe(Sink.mapInputArray(f), Sink.map(g)) — f now maps NonEmptyReadonlyArray instead of Chunk and must return a non-empty array. + +- `Sink.dimapChunksEffect` -> `Sink.mapInputArrayEffect + Sink.mapEffect`: Removed; compose self.pipe(Sink.mapInputArrayEffect(f), Sink.mapEffect(g)) — f maps NonEmptyReadonlyArray instead of Chunk and must return a non-empty array. + +- `Sink.dimapEffect` -> `Sink.mapInputEffect + Sink.mapEffect`: Removed; compose self.pipe(Sink.mapInputEffect(f), Sink.mapEffect(g)). + +- `Sink.drop` -> `none`: The drop\* sinks were removed (nothing in the v4 export list); drop on the stream side instead with Stream.drop(n) before running the sink, or write a Sink.fromTransform that discards the first n pulled elements. + +- `Sink.dropUntil` -> `none`: Removed with the other drop\* sinks; use Stream.dropUntil(predicate) on the stream before running the sink. + +- `Sink.dropUntilEffect` -> `none`: Removed; use Stream.dropUntilEffect(p) on the stream before running the sink. + +- `Sink.dropWhile` -> `none`: Removed; use Stream.dropWhile(predicate) on the stream before running the sink. + +- `Sink.dropWhileEffect` -> `none`: Removed; use Stream.dropWhileEffect(p) on the stream before running the sink. + +- `Sink.ensuringWith` -> `Sink.onExit`: Renamed; Sink.onExit(self, (exit: Exit\) =\> finalizer) runs after completion, failure, or interruption — the exit now carries the sink's result value A (v3 passed Exit\). Plain Sink.ensuring(effect) also still exists for the exit-independent case. + +- `Sink.filterInput` -> `none`: Removed, and not expressible via Sink.mapInputArray because its function must return a non-empty array (a fully-filtered batch is illegal); filter on the stream with Stream.filter(predicate) before running the sink, or write a Sink.fromTransform that skips empty filtered batches. + +- `Sink.filterInputEffect` -> `none`: Removed (same non-empty-array constraint as filterInput); use Stream.filterEffect(p) on the stream before running the sink. + +- `Sink.foldChunks` -> `Sink.reduceWhileArray`: Sink.reduceWhileArray(() =\> s, contFn, f) folds whole input batches; initial state is now a lazy thunk and f receives NonEmptyReadonlyArray\ instead of Chunk\. + +- `Sink.foldChunksEffect` -> `Sink.reduceWhileArrayEffect`: Sink.reduceWhileArrayEffect(() =\> s, contFn, f) is the effectful array-level fold with continuation predicate; lazy initial state, NonEmptyReadonlyArray instead of Chunk. Sink.foldArray has the same shape but does not check contFn on the initial state. + +- `Sink.foldEffect` -> `Sink.reduceWhileEffect`: Sink.reduceWhileEffect(() =\> s, contFn, f) folds element-by-element with an effectful step and continuation predicate (checked on the initial state, like v3); initial state is now a lazy thunk. v4 Sink.fold has the same signature but skips the initial-state check. + +- `Sink.foldLeft` -> `Sink.reduce`: Renamed; Sink.reduce(() =\> s, f) — initial state is now a lazy thunk, semantics otherwise identical. + +- `Sink.foldLeftChunks` -> `Sink.reduceArray`: Renamed; Sink.reduceArray(() =\> s, f) folds whole batches — lazy initial state, f receives NonEmptyReadonlyArray\ instead of Chunk\. + +- `Sink.foldLeftChunksEffect` -> `Sink.reduceWhileArrayEffect`: No plain reduceArrayEffect exists in v4; use Sink.reduceWhileArrayEffect(() =\> s, () =\> true, f) (constant-true predicate) — f receives NonEmptyReadonlyArray\ instead of Chunk\ and the result has L = never like v3. + +- `Sink.foldLeftEffect` -> `Sink.reduceEffect`: Renamed; Sink.reduceEffect(() =\> s, f) — lazy initial state, effectful step, no termination predicate. + +- `Sink.foldSink` -> `Sink.orElse + Sink.flatMap`: The two-channel match was removed; compose self.pipe(Sink.orElse((e) =\> options.onFailure(e)), Sink.flatMap((a) =\> options.onSuccess(a))) — orElse switches to the failure sink (resuming the same upstream), flatMap feeds leftovers to the success sink first. + +- `Sink.foldUntilEffect` -> `Sink.foldUntil`: v4 Sink.foldUntil(() =\> s, max, f) takes the effectful step function directly (f returns Effect\), so it covers v3 foldUntilEffect; initial state is now a lazy thunk. For the pure v3 foldUntil wrap the step in Effect.succeed. + +- `Sink.foldWeighted` -> `none`: The whole foldWeighted family was removed from v4 Sink (checked the export list); re-implement with Sink.fold carrying the accumulated cost in the state (cont while cost \< max), returning leftovers automatically when stopping mid-batch. + +- `Sink.foldWeightedDecompose` -> `none`: Removed with no decompose mechanism in v4; splitting oversized elements must happen upstream (transform the stream before the sink) or inside a custom Sink.fromTransform. + +- `Sink.foldWeightedDecomposeEffect` -> `none`: Removed; same as foldWeightedDecompose — no effectful weighted/decompose fold exists, re-implement via Sink.fromTransform or restructure upstream. + +- `Sink.foldWeightedEffect` -> `none`: Removed; re-implement with Sink.fold (its step is effectful in v4) tracking accumulated cost in the state. + +- `Sink.forEachChunk` -> `Sink.forEachArray`: Renamed; f receives NonEmptyReadonlyArray\ instead of Chunk\. + +- `Sink.forEachChunkWhile` -> `Sink.forEachWhileArray`: Renamed; f: (NonEmptyReadonlyArray\) =\> Effect\ continues while true, stops on false, as in v3. + +- `Sink.fromPush` -> `Sink.fromTransform`: The push-based protocol (Option\ push function failing with [Either, leftovers]) is gone; v4's low-level constructor is pull-based: Sink.fromTransform((upstream: Pull\\>, scope) =\> Effect\\>) — pull inputs from upstream and finish by succeeding with the [value, leftover?] tuple. + +- `Sink.leftover` -> `Sink.succeed`: Removed as a standalone constructor; Sink.succeed now takes optional leftovers: Sink.succeed(void 0, leftovers) where leftovers is a NonEmptyReadonlyArray\ instead of Chunk\. + +- `Sink.mapInputChunks` -> `Sink.mapInputArray`: Renamed; f maps NonEmptyReadonlyArray\ =\> NonEmptyReadonlyArray\ (must stay non-empty) instead of Chunk =\> Chunk. + +- `Sink.mapInputChunksEffect` -> `Sink.mapInputArrayEffect`: Renamed; f maps NonEmptyReadonlyArray\ =\> Effect\\> (must stay non-empty) instead of Chunk =\> Effect\. + +- `Sink.mkString` -> `Sink.reduceArray`: Removed as a built-in; equivalent one-liner: Sink.reduceArray(() =\> "", (s, arr) =\> s + arr.join("")). + +- `Sink.race` -> `none`: Sink racing (race/raceBoth/raceWith) was removed from v4; broadcast the stream (Stream.broadcast) into two consumers and race the resulting run effects with Effect.race, or write a custom Channel. + +- `Sink.raceBoth` -> `none`: Removed with the race family; broadcast the stream and use Effect.raceBoth (or Effect.race) on the two Stream.run effects to learn which side won. + +- `Sink.raceWith` -> `none`: Removed, along with the MergeDecision type it depended on; the closest is broadcasting the stream and combining the two run effects manually (Effect.raceWith on the run effects). + +- `Sink.refineOrDie` -> `Sink.catch`: Removed; rebuild with the typed-error handler: Sink.catch(self, (e) =\> Option.match(pf(e), { onSome: Effect.fail, onNone: () =\> Effect.die(e) })) — note Sink.catch replaces the result on recovery, so refined errors must be re-failed as shown. + +- `Sink.refineOrDieWith` -> `Sink.catch`: Removed; same pattern as refineOrDie but die with the mapped defect: Sink.catch(self, (e) =\> Option.match(pf(e), { onSome: Effect.fail, onNone: () =\> Effect.die(f(e)) })). + +- `Sink.splitWhere` -> `none`: Removed; it re-chunked input so the sink stopped before the first later element matching the predicate — closest v4 options are pre-splitting the stream (Stream.split / Stream.takeWhile) or a custom Sink.fromTransform that cuts pulled arrays at the predicate boundary and returns the rest as leftovers. + +- `Sink.unwrapScoped` -> `Sink.unwrap`: Folded into Sink.unwrap, whose signature now excludes Scope from R (Sink\<..., Exclude\ | R2\>), so scoped effects are accepted directly; resources stay open for the sink's lifetime. + +- `Sink.unwrapScopedWith` -> `Sink.unwrap`: Folded into Sink.unwrap — obtain the scope inside the effect via Effect.scope (Sink.unwrap(Effect.flatMap(Effect.scope, f))); for direct scope access use Sink.fromTransform, whose transform receives (upstream, scope). + +- `Sink.zip` -> `Sink.flatMap`: The zip family was removed; sequential zip is self.pipe(Sink.flatMap((a) =\> Sink.map(that, (a2) =\> [a, a2] as const))) — leftovers of the first sink feed the second. The { concurrent: true } racing mode has no v4 equivalent. + +- `Sink.zipLeft` -> `Sink.flatMap`: Removed; use self.pipe(Sink.flatMap((a) =\> Sink.as(that, a))) to run both sequentially and keep the first result (no concurrent option). + +- `Sink.zipRight` -> `Sink.flatMap`: Removed; use self.pipe(Sink.flatMap(() =\> that)) to run both sequentially and keep the second result (no concurrent option). + +- `Sink.zipWith` -> `Sink.flatMap`: Removed; use self.pipe(Sink.flatMap((a) =\> Sink.map(that, (a2) =\> f(a, a2)))) — sequential only, the { concurrent: true } option has no v4 equivalent. + +### `effect/SortedMap` + +- `SortedMap.SortedMap` -> `HashMap.HashMap`: Use HashMap as the immutable core model and retain Order externally; ordered iteration and range seeks require sorting on observation. + +- `SortedMap.TypeId` -> `none`: The SortedMap brand was removed and HashMap.TypeId is private; use HashMap.isHashMap when a guard is needed. + +- `SortedMap.empty` -> `HashMap.empty`: SortedMap was removed; use an immutable HashMap and retain the key Order separately. + +- `SortedMap.entries` -> `HashMap.entries + Array.sortWith`: Materialize HashMap.entries and sort by key with the retained Order when ordered traversal is required. + +- `SortedMap.fromIterable` -> `HashMap.fromIterable`: Use HashMap.fromIterable and retain the key Order separately; duplicate keys collapse. + +- `SortedMap.get` -> `HashMap.get`: Direct optional lookup on the replacement immutable map. + +- `SortedMap.getOrder` -> `none`: HashMap does not store an Order; retain and pass the key Order explicitly. + +- `SortedMap.has` -> `HashMap.has`: Use direct membership testing on the replacement HashMap; retain the key Order separately for sorted observations. + +- `SortedMap.headOption` -> `HashMap.entries + Array.sortWith + Array.head`: Sort entries by key with the retained Order, then take the optional first entry. + +- `SortedMap.isEmpty` -> `HashMap.isEmpty`: Direct emptiness check on the replacement immutable map. + +- `SortedMap.isNonEmpty` -> `HashMap.isEmpty`: Use !HashMap.isEmpty(self); no dedicated HashMap.isNonEmpty helper exists. + +- `SortedMap.isSortedMap` -> `HashMap.isHashMap`: Use the replacement model guard; it does not prove that observations were sorted. + +- `SortedMap.keys` -> `HashMap.entries + Array.sortWith + Array.map`: Sort entries by key, map to keys, and iterate the resulting array. + +- `SortedMap.lastOption` -> `HashMap.entries + Array.sortWith + Array.last`: Sort entries by key with the retained Order, then take the optional last entry. + +- `SortedMap.make` -> `HashMap.make`: Remove the outer order-curried constructor and pass entries directly to HashMap.make. + +- `SortedMap.map` -> `HashMap.map`: The value-and-key callback remains, but result iteration is unordered until explicitly sorted. + +- `SortedMap.partition` -> `HashMap.filter`: Build [excluded, satisfying] with complementary HashMap.filter calls; adapt the callback to the old key predicate. + +- `SortedMap.reduce` -> `HashMap.reduce`: Reduce the replacement HashMap, but explicitly sort entries first if the old key-order traversal affected the result. + +- `SortedMap.remove` -> `HashMap.remove`: Direct persistent removal; explicitly sort only when observing entries. + +- `SortedMap.set` -> `HashMap.set`: Direct persistent insert or update; explicitly sort only when observing entries. + +- `SortedMap.size` -> `HashMap.size`: Direct size query on the replacement immutable map. + +- `SortedMap.values` -> `HashMap.entries + Array.sortWith + Array.map`: Sort entries by key, map to values, and iterate the resulting array. + +### `effect/SortedSet` + +- `SortedSet.SortedSet` -> `HashSet.HashSet`: Use HashSet as the immutable core model and retain Order externally; ordered iteration requires sorting on observation. + +- `SortedSet.TypeId` -> `none`: The SortedSet brand was removed and HashSet.TypeId is private; use HashSet.isHashSet when a guard is needed. + +- `SortedSet.add` -> `HashSet.add`: Direct persistent add on the replacement set; sort only when traversing. + +- `SortedSet.difference` -> `HashSet.difference + HashSet.fromIterable`: Convert the old general iterable argument to HashSet before taking the difference. + +- `SortedSet.empty` -> `HashSet.empty`: SortedSet was removed; use an immutable HashSet and retain the element Order separately. + +- `SortedSet.every` -> `HashSet.every`: Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects. + +- `SortedSet.filter` -> `HashSet.filter`: Direct persistent filtering on the replacement set; traversal is unordered until explicitly sorted. + +- `SortedSet.fromIterable` -> `HashSet.fromIterable`: Use HashSet.fromIterable and retain the element Order separately. + +- `SortedSet.getEquivalence` -> `Equal.asEquivalence`: HashSet implements Effect equality by set content; use Equal.asEquivalence\\>(). + +- `SortedSet.has` -> `HashSet.has`: Use direct membership testing on the replacement HashSet. + +- `SortedSet.intersection` -> `HashSet.intersection + HashSet.fromIterable`: Convert the old general iterable argument to HashSet before taking the intersection. + +- `SortedSet.isSortedSet` -> `HashSet.isHashSet`: Use the replacement model guard; it does not prove that observations were sorted. + +- `SortedSet.make` -> `HashSet.make`: Remove the outer order-curried constructor and pass values directly to HashSet.make. + +- `SortedSet.map` -> `HashSet.map`: Remove the output Order argument; retain it externally and sort only when traversing. + +- `SortedSet.partition` -> `HashSet.filter`: Build [excluded, satisfying] with complementary HashSet.filter calls. + +- `SortedSet.remove` -> `HashSet.remove`: Direct persistent removal on the replacement set. + +- `SortedSet.size` -> `HashSet.size`: Direct size query on the replacement immutable set. + +- `SortedSet.some` -> `HashSet.some`: Run the predicate against the replacement HashSet; sort first only if traversal order has observable effects. + +- `SortedSet.union` -> `HashSet.union + HashSet.fromIterable`: Convert the old general iterable argument to HashSet before taking the union. + +- `SortedSet.values` -> `Array.sort`: Sort the replacement HashSet with the retained Order and iterate the resulting array. + +### `effect/Stream` + +- `Stream.Stream` -> `Stream`: The Stream\ interface is unchanged in shape and keeps the effect/Stream import path; the type-id key is now the string literal "\~effect/Stream" instead of a unique symbol. + +- `Stream.Stream.Context` -> `Stream.Services`: Type-level extractor of the R parameter renamed from Stream.Context\ to Stream.Services\; identical conditional-infer semantics. + +- `Stream.Stream.DynamicTuple` -> `Types.TupleOf`: Already deprecated in v3 in favor of Types.TupleOf; removed in v4. Use Types.TupleOf\ (v4 Stream.broadcastN uses it for its return type). + +- `Stream.Stream.DynamicTupleOf` -> `Types.TupleOf`: Recursive tuple-builder helper removed with Stream.DynamicTuple; Types.TupleOf\ is the v4 equivalent. + +- `Stream.StreamTypeId` -> `Stream.TypeId`: Renamed StreamTypeId -\> TypeId and changed from a unique symbol to the string literal "\~effect/Stream" (both the const and the type). + +- `Stream.StreamUnify` -> `Stream.StreamUnify`: Still exported under the same name in v4 (extends Effect.EffectUnify); no change needed besides any Chunk-related element types. + +- `Stream.StreamUnifyIgnore` -> `Stream.StreamUnifyIgnore`: Still exported under the same name in v4 (extends Effect.EffectUnifyIgnore with Effect ignored); unchanged. + +- `Stream.accumulateChunks` -> `none`: v3 accumulateChunks only rewrote the internal chunk layout (each chunk cumulatively contained all prior elements) without changing the emitted element sequence; v4 has no chunk-layout twin. Stream.accumulate emits the cumulative NonEmptyArray values, and Stream.rechunk controls chunk sizing. + +- `Stream.acquireRelease` -> `Stream.scoped(Stream.fromEffect(Effect.acquireRelease(acquire, release)))`: Dedicated constructor removed; compose Effect.acquireRelease (same (resource, exit) release signature) with Stream.fromEffect, then Stream.scoped to tie the finalizer to the stream's lifetime. + +- `Stream.aggregateWithinEither` -> `Stream.aggregateWithin`: Either-emitting variant removed; v4 aggregateWithin(sink, schedule) emits only the sink outputs B (schedule outputs are no longer surfaced as Either.right). + +- `Stream.as` -> `Stream.map(() => value)`: Stream.as was removed; replace each element with a constant via Stream.map. + +- `Stream.async` -> `Stream.callback`: Stream.callback((queue) =\> Effect | void, { bufferSize?, strategy? }) replaces the Emit-based async; push with Queue.offer/offerAll, end with Queue.end, fail with Queue.fail. + +- `Stream.asyncEffect` -> `Stream.callback`: The register function of Stream.callback may return an Effect (run before the stream starts pulling), covering asyncEffect; signal end/failure through the provided Queue. + +- `Stream.asyncPush` -> `Stream.callback`: Stream.callback's register effect can use Scope for acquire/release of the external subscription, replacing asyncPush; the Emit ops helpers become plain Queue operations. + +- `Stream.asyncScoped` -> `Stream.callback`: Stream.callback's register effect may use Scope (Scope is excluded from the resulting R), replacing asyncScoped; the Option\ end signal becomes Queue.end. + +#### `Stream.branchAfter` + +**Replacement:** `Stream.peel` + +Removed; the closest v4 primitive is Stream.peel(self, Sink.take(n)), a scoped Effect yielding [firstN, restStream] from which you build the continuation stream and re-wrap with Stream.unwrap. + +**Example** + +```ts +Stream.unwrap(Effect.map(Stream.peel(self, Sink.take(n)), ([head, rest]) => f(head)(rest))) +``` + +- `Stream.broadcastDynamic` -> `Stream.broadcast`: v4 Stream.broadcast({ capacity, strategy?, replay? }) is the dynamic-subscriber fan-out returning Effect\, never, Scope | R\> (v3 fixed-arity broadcast(n) became Stream.broadcastN); Stream.share adds refcounted/idleTimeToLive semantics. + +- `Stream.broadcastedQueues` -> `none`: Queue-of-Take fan-out surface removed. Use Stream.broadcastN({ n, capacity }) for a fixed tuple of mirror streams, or Stream.toPubSubTake to obtain a PubSub of Take values and subscribe consumers to it. + +- `Stream.broadcastedQueuesDynamic` -> `none`: Removed with broadcastedQueues. Use Stream.broadcast (dynamic mirror streams) or Stream.toPubSubTake + PubSub subscriptions when raw Take-level consumers are needed. + +- `Stream.bufferChunks` -> `Stream.bufferArray`: Chunk-\>Array rename; buffers whole arrays (chunks) up to capacity with the same strategy options. + +- `Stream.catchAll` -> `Stream.catch`: Renamed to Stream.catch (exported keyword-style); same (error) =\> Stream handler for all typed failures. + +- `Stream.catchAllCause` -> `Stream.catchCause`: Renamed; handler receives the full Cause\ and returns a recovery stream, identical semantics. + +- `Stream.catchSome` -> `Stream.catchFilter`: Option-returning partial handler replaced by the Filter API: Stream.catchFilter(filter, f, orElse?) recovers matched errors, unmatched failures pass through (Stream.catchIf for refinement/predicate matching). + +- `Stream.catchSomeCause` -> `Stream.catchCauseFilter`: Option-returning cause handler replaced by Stream.catchCauseFilter(filter, f, orElse?) using a Filter on the Cause (Stream.catchCauseIf for refinements). + +- `Stream.chunksWith` -> `Stream.flattenArray(f(Stream.chunks(self)))`: No dedicated combinator; expose chunk structure with Stream.chunks (Stream\\>), transform, then re-flatten with Stream.flattenArray. + +- `Stream.combineChunks` -> `Stream.combineArray`: Chunk-\>Array rename of the pull-level combining primitive; pulls now yield NonEmptyReadonlyArray values and halt via Cause.Done-failing Pull effects instead of Option-typed errors. + +- `Stream.concatAll` -> `Stream.flatten`: Chunk-of-streams constructor removed; sequential concatenation of many streams is Stream.flatten(Stream.fromIterable(streams)) (default concurrency 1 preserves order). + +- `Stream.context` -> `Stream.fromEffect(Effect.context())`: Dedicated accessor removed; lift Effect.context\() into a single-element stream. + +- `Stream.contextWith` -> `Stream.fromEffect(Effect.contextWith(f))`: Dedicated accessor removed; Effect.contextWith still exists in v4, lift it with Stream.fromEffect. + +- `Stream.contextWithEffect` -> `Stream.fromEffect(Effect.flatMap(Effect.context(), f))`: Removed; read the Context with Effect.context, feed it to the effectful function, and lift the result with Stream.fromEffect. + +- `Stream.contextWithStream` -> `Stream.unwrap(Effect.contextWith(f))`: Removed; build the dependent stream inside Effect.contextWith and flatten with Stream.unwrap. + +- `Stream.crossLeft` -> `Stream.crossWith(that, (a, _) => a)`: Removed; cartesian product keeping only left elements is expressed with Stream.crossWith and a left-projecting combiner. + +- `Stream.crossRight` -> `Stream.crossWith(that, (_, b) => b)`: Removed; cartesian product keeping only right elements is Stream.crossWith with a right-projecting combiner (equivalently Stream.flatMap(self, () =\> that)). + +- `Stream.dieMessage` -> `Stream.die(new Error(message))`: Removed along with RuntimeException; die with an explicit defect value via Stream.die. + +- `Stream.dieSync` -> `Stream.failCauseSync(() => Cause.die(evaluate()))`: Removed; lazily construct the defect cause with Cause.die inside Stream.failCauseSync. + +- `Stream.distributedWith` -> `none`: Predicate-routed fixed fan-out to Take queues removed (no v4 counterpart found among broadcast/broadcastN/share/toPubSub/partition). Closest patterns: Stream.broadcastN + Stream.filter per branch, Stream.partition for two-way splits, or manual routing by running the stream into per-consumer Queues. + +- `Stream.distributedWithDynamic` -> `none`: Dynamic predicate-routed fan-out removed with distributedWith. Use Stream.broadcast/Stream.share for dynamic mirrors plus per-subscriber Stream.filter, or hand-roll routing into Queues via Stream.runForEach. + +- `Stream.either` -> `Stream.result`: Either is replaced by Result in v4: Stream.result yields Stream\, never, R\> (element -\> Result.succeed, first error -\> Result.fail and the stream ends, as before). + +- `Stream.ensuringWith` -> `Stream.onExit`: Renamed; Stream.onExit runs the finalizer with the Exit\ of the stream, identical shape. + +- `Stream.execute` -> `Stream.fromEffectDrain`: Renamed; runs the effect for its side effects and emits nothing (Stream\). + +- `Stream.filterMapWhile` -> `Stream.takeWhileFilter`: Option-returning partial function replaced by the Filter API: Stream.takeWhileFilter(filter) maps and emits while the filter passes, ending the stream at the first miss. + +- `Stream.filterMapWhileEffect` -> `none`: No effectful takeWhileFilter variant in v4. Recreate by using Stream.takeWhileFilter with a Filter that selects the Effect\ to run, followed by Stream.mapEffect((eff) =\> eff) to execute it. + +- `Stream.finalizer` -> `Stream.ensuring`: One-element finalizer-registering stream removed; attach finalizers directly with Stream.ensuring/Stream.onExit, or register in the stream scope via Stream.scoped(Stream.fromEffect(Effect.addFinalizer(fin))) when the v3 concat-a-finalizer pattern must be preserved. + +- `Stream.find` -> `Stream.take(Stream.filter(self, predicate), 1)`: Removed; first-match semantics are Stream.filter followed by Stream.take(1). + +- `Stream.findEffect` -> `Stream.take(Stream.filterEffect(self, f), 1)`: Removed; Stream.filterEffect takes an effectful (a, index) =\> Effect\ predicate, then Stream.take(1) stops at the first match. + +- `Stream.flattenChunks` -> `Stream.flattenArray`: Chunk-\>Array rename; flattens a Stream of ReadonlyArray values into their elements. + +- `Stream.flattenExitOption` -> `Stream.flattenTake`: The Exit\\> end-of-stream encoding is gone; v4 uses Take\ = NonEmptyReadonlyArray\ | Exit and Stream.flattenTake unwraps it (emit arrays, end/fail on Exit). + +- `Stream.flattenIterables` -> `Stream.flattenIterable`: Renamed (singular); flattens a Stream of Iterables into their elements. + +- `Stream.fromChunk` -> `Stream.fromArray`: Chunk-\>Array rename; takes a ReadonlyArray and emits it as one chunk. + +- `Stream.fromChunkPubSub` -> `Stream.fromPubSub`: Chunked PubSub constructors are gone; v4 Stream.fromPubSub(pubsub) consumes PubSub\ directly (batched internally). For a PubSub carrying arrays use Stream.flattenArray(Stream.fromPubSub(pubsub)); the scoped/shutdown options were dropped (Stream.fromSubscription consumes an existing subscription). + +- `Stream.fromChunkQueue` -> `Stream.fromQueue`: Chunked Queue constructor gone; v4 Stream.fromQueue consumes Queue.Dequeue\ whose done/failure signals end the stream (no shutdown option). For array payloads wrap with Stream.flattenArray. + +- `Stream.fromChunks` -> `Stream.fromArrays`: Chunk-\>Array rename; variadic arrays, each emitted as one chunk. + +- `Stream.fromEffectOption` -> `none`: The Effect\\> encoding (fail None = empty stream) is removed; v4 signals early end with Cause.Done in Pull-level code. Rebuild with Stream.unwrap: map the success to Stream.succeed and match the Option error to Stream.empty (None) or Stream.fail (Some). + +- `Stream.fromReadableStreamByob` -> `Stream.fromReadableStream`: BYOB reader variant removed (no byob support in v4 source); Stream.fromReadableStream({ evaluate, onError, releaseLockOnEnd? }) consumes any ReadableStream with a default reader, without byte-buffer allocation control. + +- `Stream.fromTPubSub` -> `none`: STM TPubSub was replaced by the transactional TxPubSub module and v4 Stream has no Tx\* constructors; subscribe and repeatedly TxQueue.take from the subscription (e.g. inside Stream.fromPull/Stream.callback), or bridge through a regular PubSub and Stream.fromPubSub. + +- `Stream.fromTQueue` -> `none`: STM TQueue was replaced by TxQueue and v4 Stream has no Tx\* constructors; drain by repeatedly calling TxQueue.take inside a custom loop (Stream.fromPull/Stream.callback), or bridge into a regular Queue and use Stream.fromQueue. + +- `Stream.haltAfter` -> `Stream.haltWhen(Effect.sleep(duration))`: Duration-specialized halt removed; v3 documented it as haltWhen with a sleep — completes the stream after the duration without interrupting an in-flight pull. + +- `Stream.haltWhenDeferred` -> `Stream.haltWhen(Deferred.await(deferred))`: Deferred-specialized variant removed; Deferred.await is an Effect, so plain Stream.haltWhen covers it. + +- `Stream.identity` -> `Channel.identity`: The identity-pipeline Stream is gone; for pipeThrough-style plumbing use Stream.pipeThroughChannel(Channel.identity()), or simply the identity function where a Stream=\>Stream transform is expected. + +- `Stream.interruptAfter` -> `Stream.interruptWhen(Effect.sleep(duration))`: Duration-specialized interrupt removed; interruptWhen forks the sleep and also interrupts an in-progress pull, matching v3 semantics. + +- `Stream.interruptWhenDeferred` -> `Stream.interruptWhen(Deferred.await(deferred))`: Deferred-specialized variant removed; pass Deferred.await to Stream.interruptWhen (a Deferred failure surfaces as the stream's failure, as before). + +- `Stream.mapChunks` -> `Stream.mapArray`: Chunk-\>Array rename; transforms each emitted chunk as a NonEmptyReadonlyArray. + +- `Stream.mapChunksEffect` -> `Stream.mapArrayEffect`: Chunk-\>Array rename of the effectful per-chunk transform. + +- `Stream.mapConcat` -> `Stream.flattenIterable(Stream.map(self, f))`: Removed; map each element to an Iterable and flatten with Stream.flattenIterable. + +- `Stream.mapConcatChunk` -> `Stream.flattenArray(Stream.map(self, f))`: Chunk variant removed with Chunk itself; map to a ReadonlyArray and flatten with Stream.flattenArray. + +- `Stream.mapConcatChunkEffect` -> `Stream.flattenArray(Stream.mapEffect(self, f))`: Removed; effectfully map each element to a ReadonlyArray and flatten with Stream.flattenArray. + +- `Stream.mapConcatEffect` -> `Stream.flattenIterable(Stream.mapEffect(self, f))`: Removed; effectfully map each element to an Iterable and flatten with Stream.flattenIterable. + +- `Stream.mapErrorCause` -> `Stream.catchCause((cause) => Stream.failCause(f(cause)))`: Removed; transform the full Cause by catching it and re-failing with the mapped cause. + +- `Stream.mapInputContext` -> `Stream.updateContext`: Renamed; same contravariant (Context\) =\> Context\ mapping of the required services. + +- `Stream.mergeEither` -> `Stream.mergeResult`: Either replaced by Result: Stream.mergeResult(self, that) yields Result.Result\ with self -\> Result.succeed and that -\> Result.fail (v3 put self in Either.left and that in Either.right, so the success/left roles swap sides). + +- `Stream.mergeWith` -> `Stream.merge(Stream.map(self, onSelf), Stream.map(that, onOther), { haltStrategy })`: Removed; pre-map both streams to the common type and use Stream.merge, whose options accept the same haltStrategy union ("left" | "right" | "both" | "either"). + +#### `Stream.mergeWithTag` + +**Replacement:** `none` + +Struct-to-tagged-union merge removed. Recreate with Stream.mergeAll over the entries, tagging each stream first. + +**Example** + +```ts +Stream.mergeAll(Object.entries(streams).map(([_tag, s]) => Stream.map(s, (value) => ({ _tag, value }))), { concurrency }) +``` + +- `Stream.onDone` -> `Stream.onEnd`: Renamed; v4 onEnd takes an Effect value (not a () =\> Effect thunk) run when the stream ends successfully, and its error type may add to the stream's. + +- `Stream.orDieWith` -> `Stream.orDie`: orDieWith removed; transform the error first, then convert failures to defects: `self.pipe(Stream.mapError(f), Stream.orDie)`. + +- `Stream.orElse` -> `Stream.catch`: v3 catchAll was renamed to Stream.catch in v4; orElse ignored the error, so write `Stream.catch(self, () => that())`. + +- `Stream.orElseEither` -> `Stream.catch`: Removed; Either is replaced by Result in v4. Emulate: `Stream.map(self, Result.succeed).pipe(Stream.catch(() => Stream.map(that(), Result.fail)))` (same encoding v4 Stream.mergeResult uses). + +- `Stream.orElseFail` -> `Stream.mapError`: Removed; it only replaced the failure value: `Stream.mapError(self, () => error())` or `Stream.catch(self, () => Stream.fail(error()))`. + +- `Stream.orElseIfEmptyChunk` -> `Stream.orElseIfEmpty`: Folded into Stream.orElseIfEmpty, which now takes a lazy fallback Stream: `Stream.orElseIfEmpty(self, () => Stream.fromArray(array))`; Chunk is replaced by plain arrays. + +- `Stream.orElseIfEmptyStream` -> `Stream.orElseIfEmpty`: Direct rename: v4 Stream.orElseIfEmpty takes a LazyArg\ fallback, identical semantics. + +- `Stream.paginateChunk` -> `Stream.paginate`: v4 Stream.paginate is effectful and array-based: `paginate(s, (s) => Effect, Option]>)`; wrap the pure step in Effect.succeed and use an array instead of a Chunk. + +- `Stream.paginateChunkEffect` -> `Stream.paginate`: v4 Stream.paginate has exactly this shape; only Chunk becomes ReadonlyArray. + +- `Stream.paginateEffect` -> `Stream.paginate`: v4 Stream.paginate emits a batch per step; wrap the single value in an array: `(s) => Effect.map(step(s), ([a, next]) => [[a], next])`. + +- `Stream.partitionEither` -> `Stream.partitionEffect`: Either-based split replaced by Filter.FilterEffect: the function now returns Effect\\> (Result.succeed/Result.fail instead of Either.right/left). Returns Effect\<[passes, fails], never, R | Scope\> — note the tuple is [passes, fails], v3 was [left, right]; options are { capacity?, concurrency? }. + +- `Stream.provideLayer` -> `Stream.provide`: v4 Stream.provide accepts a Layer or a Context; behavior identical. + +- `Stream.provideServiceStream` -> `none`: Removed; v4 has provideService/provideServiceEffect but no stream-valued variant. Emulate with `Stream.flatMap(services, (s) => Stream.provideService(self, tag, s))` over the service stream, or use Stream.provideServiceEffect for effectful acquisition. + +- `Stream.provideSomeContext` -> `Stream.provideContext`: v4 Stream.provideContext is the single Context provider with `Exclude` semantics — same behavior as v3 provideSomeContext. + +- `Stream.provideSomeLayer` -> `Stream.provide`: v4 Stream.provide accepts a Layer (or Context) and excludes only the provided services from R — same partial-provision semantics. + +- `Stream.refineOrDie` -> `Stream.catch`: Removed; emulate with `Stream.catch(self, (e) => { const r = pf(e); return Option.isSome(r) ? Stream.fail(r.value) : Stream.die(e) })` — refail refined errors, die on the rest. + +- `Stream.refineOrDieWith` -> `Stream.catch`: Removed; same as refineOrDie but die with the mapped defect: `Stream.die(f(e))` for unrefined errors. + +- `Stream.repeatEffect` -> `Stream.fromEffectRepeat`: Renamed; repeats the effect forever emitting each result. + +- `Stream.repeatEffectChunk` -> `Stream.fromIterableEffectRepeat`: Renamed; the effect now produces an Iterable/array instead of a Chunk, repeated forever. + +- `Stream.repeatEffectChunkOption` -> `Stream.fromIterableEffectRepeat`: The Option\ error encoding is gone: end the stream by failing the effect with `Cause.done()` (a Cause.Done failure); Done is excluded from the resulting stream's error type (Pull.ExcludeDone\). + +- `Stream.repeatEffectOption` -> `Stream.fromEffectRepeat`: The Option\ error encoding is gone: fail the effect with `Cause.done()` instead of Option.none() to end the stream; other failures propagate as stream errors. + +- `Stream.repeatEffectWithSchedule` -> `Stream.fromEffectSchedule`: Renamed; runs the effect once, then repeats it per the schedule, emitting each result. + +- `Stream.repeatEither` -> `none`: Removed; v4 Stream.repeat(schedule) repeats the stream but never emits the schedule outputs, and no Either/unification variant exists. If schedule outputs must be observed, hand-roll with Channel or track them via a schedule that taps into a Ref. + +- `Stream.repeatElementsWith` -> `none`: Removed; v4 Stream.repeatElements(schedule) repeats each element per the schedule but never emits schedule outputs — the onElement/onSchedule unification is gone. Use repeatElements if only element repetition is needed. + +- `Stream.repeatValue` -> `Stream.fromEffectRepeat`: Removed; use `Stream.fromEffectRepeat(Effect.succeed(value))` or `Stream.forever(Stream.succeed(value))`. + +- `Stream.repeatWith` -> `none`: Removed; v4 Stream.repeat(schedule) covers the repetition but drops the schedule outputs and the onElement/onSchedule unification. Hand-roll if schedule outputs must appear in the stream. + +- `Stream.runFoldScoped` -> `Stream.runFold`: Scoped run variants are gone; v4 run functions manage the stream's scope internally and the initial value is now a LazyArg: `Stream.runFold(self, () => s, f)`. For enclosing-scope control, pull manually via `Stream.toPull` (Effect\). + +- `Stream.runFoldScopedEffect` -> `Stream.runFoldEffect`: Scoped run variants are gone; use `Stream.runFoldEffect(self, () => s, f)` — scope is managed internally, initial value is a LazyArg. Use Stream.toPull for manual scoped consumption. + +#### `Stream.runFoldWhile` + +**Replacement:** `none` + +v4 runFold has no early-exit predicate; emulate with Stream.runForEachWhile and a mutable accumulator. + +**Example** + +```ts +// v3: Stream.runFoldWhile(self, init, cont, f) +Effect.suspend(() => { + let acc = init + return Stream.runForEachWhile(self, (a) => { + acc = f(acc, a) + return Effect.succeed(cont(acc)) + }).pipe(Effect.map(() => acc)) +}) + +``` + +- `Stream.runFoldWhileEffect` -> `none`: v4 runFoldEffect has no early-exit predicate; emulate with Stream.runForEachWhile and a mutable accumulator, mapping the effectful step to Effect\ via cont(acc) (see runFoldWhile example). + +- `Stream.runFoldWhileScoped` -> `none`: Both the while-predicate and the scoped run variants are gone in v4; emulate the predicate with Stream.runForEachWhile plus a mutable accumulator (see runFoldWhile); scope is managed internally by v4 run functions. + +- `Stream.runFoldWhileScopedEffect` -> `none`: Both the while-predicate and the scoped run variants are gone in v4; emulate with Stream.runForEachWhile plus a mutable accumulator and effectful step; scope is managed internally by v4 run functions. + +- `Stream.runForEachChunk` -> `Stream.runForEachArray`: Renamed; the callback receives a NonEmptyReadonlyArray instead of a Chunk. + +- `Stream.runForEachChunkScoped` -> `Stream.runForEachArray`: Scoped run variants are gone; v4 runForEachArray manages the stream scope internally. Use Stream.toPull for manual scoped consumption. + +- `Stream.runForEachScoped` -> `Stream.runForEach`: Scoped run variants are gone; v4 runForEach manages the stream scope internally. Use Stream.toPull for manual scoped consumption. + +- `Stream.runForEachWhileScoped` -> `Stream.runForEachWhile`: Scoped run variants are gone; v4 runForEachWhile (callback returns Effect\) manages the stream scope internally. + +- `Stream.runIntoPubSubScoped` -> `Stream.runIntoPubSub`: Scoped variant removed; v4 runIntoPubSub(pubsub, { shutdownOnEnd? }) publishes plain values (the Take wrapper is gone) and does not require Scope — fork the returned effect (Effect.forkIn/Effect.forkScoped) to reproduce the background scoped behavior. + +- `Stream.runIntoQueueElementsScoped` -> `Stream.runIntoQueue`: The per-element Exit\\> encoding is gone; v4 runIntoQueue targets a Queue\ — elements are offered plainly and failure/end are signalled through the queue's error/done channel. Fork with Effect.forkIn for scoped background running. + +- `Stream.runIntoQueueScoped` -> `Stream.runIntoQueue`: Scoped variant removed; v4 runIntoQueue offers plain values to a Queue\ (Take wrapper gone) and requires no Scope — fork the returned effect into a scope (Effect.forkIn) if needed. + +- `Stream.runScoped` -> `Stream.run`: Scoped variant removed; v4 Stream.run(sink) manages the stream's scope internally. For consumption tied to an enclosing Scope, use Stream.toPull and drive the Pull manually. + +- `Stream.scanReduce` -> `Stream.mapAccum`: Removed; emulate first-element-as-seed with `Stream.mapAccum(self, () => undefined as A | undefined, (acc, a) => { const next = acc === undefined ? a : f(acc, a); return [next, [next]] })`. + +- `Stream.scanReduceEffect` -> `Stream.mapAccumEffect`: Removed; same first-element-as-seed emulation as scanReduce but with Stream.mapAccumEffect and an effectful step. + +- `Stream.scheduleWith` -> `none`: Removed; v4 Stream.schedule(schedule) only paces elements and never emits schedule outputs — the onElement/onSchedule unification is gone. Use Stream.schedule if only pacing is needed. + +- `Stream.scopedWith` -> `Stream.scoped`: Removed; v4 Stream.scoped scopes a Stream (provides a Scope kept open for the stream's lifetime). Emulate: `Stream.scoped(Stream.fromEffect(Effect.flatMap(Effect.scope, f)))` — Effect.scope accesses the ambient Scope. + +- `Stream.some` -> `none`: Removed along with Option\ error encodings. To drop None values use `Stream.filterMap(self, Filter.fromPredicateOption((o) => o))`; to fail on None use Stream.mapEffect with Option.match into Effect.fail/Effect.succeed. + +- `Stream.someOrElse` -> `Stream.map`: Removed; use `Stream.map(self, Option.getOrElse(() => fallback()))`. + +- `Stream.someOrFail` -> `Stream.mapEffect`: Removed; use `Stream.mapEffect(self, Option.match({ onNone: () => Effect.fail(error()), onSome: Effect.succeed }))`. + +- `Stream.splitOnChunk` -> `none`: Delimiter-subsequence splitting was removed; v4 keeps only Stream.split (predicate/refinement, emitting NonEmptyReadonlyArray segments) and Stream.splitLines. Hand-roll multi-element delimiter splitting with Stream.mapAccumArray. + +- `Stream.tapErrorCause` -> `Stream.tapCause`: Renamed; taps the full Cause on failure. + +- `Stream.timeoutFail` -> `Stream.timeoutOrElse`: Use `Stream.timeoutOrElse(self, { duration, orElse: () => Stream.fail(error()) })`; the timeout resets on every emitted value as before. + +- `Stream.timeoutFailCause` -> `Stream.timeoutOrElse`: Use `Stream.timeoutOrElse(self, { duration, orElse: () => Stream.failCause(cause()) })`. + +- `Stream.timeoutTo` -> `Stream.timeoutOrElse`: Renamed into an options form: `Stream.timeoutOrElse(self, { duration, orElse: () => that })` — the fallback stream is now lazy. + +- `Stream.toAsyncIterableRuntime` -> `Stream.toAsyncIterableWith`: Renamed; takes a `Context.Context` instead of a Runtime (v4 removed Runtime — a services Context is the execution environment). toAsyncIterable/toAsyncIterableEffect also still exist. + +- `Stream.toQueueOfElements` -> `Stream.toQueue`: The Exit\\>-per-element queue is gone; v4 toQueue(options: { capacity, strategy? }) returns Effect\, never, R | Scope\> — elements are plain values and failure/end arrive through the queue's error/done channel. + +- `Stream.toReadableStreamRuntime` -> `Stream.toReadableStreamWith`: Renamed; takes a `Context.Context` instead of a Runtime (v4 removed Runtime); options `{ strategy?: QueuingStrategy }` unchanged. + +- `Stream.transduce` -> `Stream.transduce`: Unchanged name and Sink-based shape; chunks are plain arrays in v4. + +- `Stream.unfoldChunk` -> `Stream.paginate`: Removed; v4 Stream.paginate(s, (s) =\> Effect\<[ReadonlyArray\, Option\]\>) is the array-emitting unfold — wrap the pure step in Effect.succeed; to end without emitting return `[[], Option.none()]`. + +- `Stream.unfoldChunkEffect` -> `Stream.paginate`: Removed; v4 Stream.paginate has the effectful array-step shape — map v3's Option\<[Chunk, S]\> result to `[array, Option]`, returning `[[], Option.none()]` to end without emitting. + +- `Stream.unfoldEffect` -> `Stream.unfold`: v4 Stream.unfold is effectful: `unfold(s, (s) => Effect)` — return the pair or `undefined` to end instead of Option. + +- `Stream.unwrapScoped` -> `Stream.unwrap`: v4 Stream.unwrap accepts scoped effects (`Exclude` built in); the scope stays open for the stream's lifetime — it replaces both unwrap and unwrapScoped. + +- `Stream.unwrapScopedWith` -> `Stream.unwrap`: Removed; access the ambient Scope explicitly: `Stream.unwrap(Effect.flatMap(Effect.scope, f))` — v4 unwrap keeps the scope open for the stream's lifetime. + +- `Stream.void` -> `Stream.succeed(void 0)`: The `Stream.void` constant (single void element) was removed; use `Stream.succeed(void 0)` or `Stream.make(void 0)`. + +- `Stream.whenCase` -> `none`: Removed; emulate with `Stream.suspend(() => Option.match(pf(evaluate()), { onNone: () => Stream.empty, onSome: (s) => s }))`. + +- `Stream.whenCaseEffect` -> `none`: Removed; emulate with `Stream.unwrap(Effect.map(self, (a) => Option.match(pf(a), { onNone: () => Stream.empty, onSome: (s) => s })))`. + +- `Stream.whenEffect` -> `Stream.when`: Folded into Stream.when, which now takes an `Effect` test directly (wrap a pure condition with Effect.sync). + +- `Stream.zipAll` -> `none`: The entire zipAll family was removed in v4 (only zip/zipLatest/zipLatestAll exist; zip ends at the shorter side, zipLatest\* combine latest values — different semantics). Pad-with-default zipping must be hand-rolled, e.g. with Stream.combineArray pulling both sides. + +- `Stream.zipAllLeft` -> `none`: Removed with the zipAll family; no default-padding zip exists in v4. Hand-roll with Stream.combineArray (or concat the remainder after a plain Stream.zipLeft) if needed. + +- `Stream.zipAllRight` -> `none`: Removed with the zipAll family; no default-padding zip exists in v4. Hand-roll with Stream.combineArray if needed. + +- `Stream.zipAllSortedByKey` -> `none`: Removed; the sorted-by-key merge-join family has no v4 equivalent (checked v4 Stream exports — only zip/zipLatest/zipLatestAll/zipWithArray). Hand-roll a keyed merge with Stream.combineArray. + +- `Stream.zipAllSortedByKeyLeft` -> `none`: Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray. + +- `Stream.zipAllSortedByKeyRight` -> `none`: Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray. + +- `Stream.zipAllSortedByKeyWith` -> `none`: Removed; see zipAllSortedByKey — no keyed merge-join in v4, hand-roll with Stream.combineArray. + +- `Stream.zipAllWith` -> `none`: Removed with the zipAll family; v4 has no zip that pads the shorter side with defaults. Hand-roll with Stream.combineArray. + +- `Stream.zipWithChunks` -> `Stream.zipWithArray`: Renamed; the combiner now receives two NonEmptyReadonlyArrays and returns `[output: NonEmptyReadonlyArray, leftoverLeft: ReadonlyArray, leftoverRight: ReadonlyArray]` — the Either-wrapped leftover (ZipChunksResult) is replaced by the two explicit leftover arrays. + +### `effect/StreamEmit` + +#### `StreamEmit.Emit` + +**Replacement:** `Queue.Queue` + +The StreamEmit module is gone; v4 Stream.callback hands the callback a Queue\ instead of an Emit function. Emit values with Queue.offer/Queue.offerAll, end with Queue.end, fail with Queue.fail/Queue.failCause. + +**Example** + +```ts +// v3: Stream.async((emit) => { emit.single(1); emit.end() }) +Stream.callback((queue) => + Effect.gen(function*() { + yield* Queue.offer(queue, 1) + yield* Queue.end(queue) + })) + +``` + +- `StreamEmit.EmitOps` -> `Queue.offer / Queue.offerAll / Queue.end / Queue.fail / Queue.failCause`: Method-by-method mapping onto the Queue passed to Stream.callback: single(a) -\> Queue.offer(queue, a); chunk(c) -\> Queue.offerAll(queue, c); end() -\> Queue.end(queue); fail(e) -\> Queue.fail(queue, e); halt(cause) -\> Queue.failCause(queue, cause); die(d)/dieMessage(m) -\> Queue.failCause(queue, Cause.die(d)); done(exit) -\> Queue.offer then Queue.end on success, Queue.failCause on failure; fromEffect(eff) -\> run eff and offer its value (Effect.flatMap(eff, (a) =\> Queue.offer(queue, a))). + +- `StreamEmit.EmitOpsPush` -> `Queue.offerUnsafe / Queue.offerAllUnsafe / Queue.endUnsafe / Queue.failCauseUnsafe`: The synchronous push interface of v3 Stream.asyncPush maps to the \*Unsafe Queue operations on the Queue given to Stream.callback: single/array -\> Queue.offerUnsafe/Queue.offerAllUnsafe, end -\> Queue.endUnsafe, fail/halt/die -\> Queue.failCauseUnsafe (wrap plain errors with Cause.fail, defects with Cause.die). + +### `effect/StreamHaltStrategy` + +- `StreamHaltStrategy.Both` -> `"both"`: The tagged constructor is replaced by the plain string literal "both" passed directly to haltStrategy options. + +- `StreamHaltStrategy.Either` -> `"either"`: The tagged constructor is replaced by the plain string literal "either" passed directly to haltStrategy options. + +- `StreamHaltStrategy.HaltStrategy` -> `Stream.HaltStrategy`: The StreamHaltStrategy module is gone; v4 HaltStrategy is the string-literal union "left" | "right" | "both" | "either" (defined in Channel, re-exported as Stream.HaltStrategy) instead of tagged objects. + +- `StreamHaltStrategy.HaltStrategyInput` -> `Stream.HaltStrategy`: The Input widening (tagged object OR string) is obsolete; v4 only ever uses the string literals, so haltStrategy options take Stream.HaltStrategy directly. + +- `StreamHaltStrategy.Left` -> `"left"`: The tagged constructor is replaced by the plain string literal "left" passed directly to haltStrategy options. + +- `StreamHaltStrategy.Right` -> `"right"`: The tagged constructor is replaced by the plain string literal "right" passed directly to haltStrategy options. + +- `StreamHaltStrategy.fromInput` -> `none`: Remove the call; there is no conversion step in v4 because strategies already are the string literals, so pass the value through unchanged. + +- `StreamHaltStrategy.isBoth` -> `strategy === "both"`: Refinements on the tagged union become plain string comparison against the literal. + +- `StreamHaltStrategy.isEither` -> `strategy === "either"`: Refinements on the tagged union become plain string comparison against the literal. + +- `StreamHaltStrategy.isLeft` -> `strategy === "left"`: Refinements on the tagged union become plain string comparison against the literal. + +- `StreamHaltStrategy.isRight` -> `strategy === "right"`: Refinements on the tagged union become plain string comparison against the literal. + +#### `StreamHaltStrategy.match` + +**Replacement:** `switch (strategy)` + +Fold over the strategy with an ordinary switch (or ternary chain) on the string literal; TypeScript exhaustiveness-checks the four cases. + +**Example** + +```ts +// v3: HaltStrategy.match(s, { onLeft, onRight, onBoth, onEither }) +switch (strategy) { + case "left": return onLeft() + case "right": return onRight() + case "both": return onBoth() + case "either": return onEither() +} + +``` + +### `effect/Struct` + +- `Struct.entries` -> `Object.entries`: Use the native helper, adding a cast when the old precise key and value type is required. + +- `Struct.getEquivalence` -> `Struct.makeEquivalence`: Direct rename; the fields object call shape is unchanged. + +- `Struct.getOrder` -> `Struct.makeOrder`: Direct rename; the fields object call shape is unchanged. + +### `effect/Subscribable` + +- `Subscribable.Subscribable` -> `custom { readonly get: Effect.Effect; readonly changes: Stream.Stream }`: No renamed generic model exists; prefer concrete SubscriptionRef APIs or own this unbranded structural type locally. + +- `Subscribable.TypeId` -> `none`: The Subscribable brand has no public replacement; use a concrete model guard or an application structural guard. + +- `Subscribable.isSubscribable` -> `none`: The common brand was removed; use a concrete guard such as SubscriptionRef.isSubscriptionRef or an application structural guard. + +- `Subscribable.make` -> `object literal { get, changes }`: No generic constructor remains; retain a local structural pair only when both the current read and change stream are needed. + +- `Subscribable.map` -> `Effect.map + Stream.map`: For a retained get and changes pair, map the Effect and Stream separately. + +- `Subscribable.mapEffect` -> `Effect.flatMap + Stream.mapEffect`: For a retained get and changes pair, flatMap the Effect and mapEffect the Stream separately. + +- `Subscribable.unwrap` -> `Effect.flatMap + Stream.unwrap`: Build get with Effect.flatMap and changes with Stream.unwrap; no single v4 helper remains. + +### `effect/SubscriptionRef` + +- `SubscriptionRef.SubscriptionRef` -> `SubscriptionRef.SubscriptionRef`: The model remains but no longer extends SynchronizedRef or Subscribable; use SubscriptionRef.get and SubscriptionRef.changes explicitly. + +- `SubscriptionRef.SubscriptionRef.Variance` -> `SubscriptionRef.SubscriptionRef.Variance`: The marker remains under SubscriptionRef.SubscriptionRef, but its brand uses an internal type id. + +- `SubscriptionRef.SubscriptionRefTypeId` -> `SubscriptionRef.isSubscriptionRef`: The type id is internal in v4; use the public runtime guard instead. + +- `SubscriptionRef.SubscriptionRefUnify` -> `none`: SubscriptionRef is no longer an Effect subtype, so its unification helper was removed; call SubscriptionRef.get explicitly. + +- `SubscriptionRef.SubscriptionRefUnifyIgnore` -> `none`: SubscriptionRef is no longer a SynchronizedRef or Effect subtype, so its unification ignore marker was removed. + +### `effect/Supervisor` + +- `Supervisor.AbstractSupervisor` -> `none`: The ambient Supervisor abstraction and runtime event hooks were removed. + +- `Supervisor.Supervisor` -> `none`: Ambient fiber supervision was removed; use structured concurrency or explicit FiberSet and FiberMap tracking. + +- `Supervisor.Supervisor.Variance` -> `none`: The Supervisor abstraction and its variance marker were removed. + +- `Supervisor.SupervisorTypeId` -> `none`: The Supervisor abstraction and its type identifier were removed. + +- `Supervisor.addSupervisor` -> `none`: Layer-installed ambient supervision was removed; use structured concurrency and explicit FiberSet or FiberMap tracking. + +- `Supervisor.fibersIn` -> `FiberSet`: Use a scoped FiberSet and explicitly run or add fibers; it does not ambiently observe every descendant. + +- `Supervisor.fromEffect` -> `none`: The Supervisor abstraction and its effect-valued observation hook were removed. + +- `Supervisor.none` -> `none`: The Supervisor abstraction was removed; normal structured concurrency needs no no-op supervisor. + +- `Supervisor.unsafeTrack` -> `FiberSet`: Use scoped FiberSet.make and explicitly run or add fibers; there is no unsafe unscoped ambient tracker. + +### `effect/Symbol` + +- `Symbol.Equivalence` -> `Equivalence.strictEqual()`: The dedicated symbol instance was removed; it used strict equality. + +### `effect/SynchronizedRef` + +- `SynchronizedRef.SynchronizedRef` -> `SynchronizedRef.SynchronizedRef`: The model remains, now extends the v4 Ref model, and is read or updated through explicit SynchronizedRef operations. + +- `SynchronizedRef.SynchronizedRef.Variance` -> `Ref.Ref.Variance`: SynchronizedRef now inherits Ref variance instead of declaring a separate public variance marker. + +- `SynchronizedRef.SynchronizedRefTypeId` -> `none`: The SynchronizedRef type id is internal in v4; do not inspect or construct the brand directly. + +- `SynchronizedRef.SynchronizedRefUnify` -> `none`: SynchronizedRef is no longer an Effect subtype, so its Effect unification helper was removed; call SynchronizedRef.get explicitly. + +- `SynchronizedRef.SynchronizedRefUnifyIgnore` -> `none`: SynchronizedRef is no longer an Effect subtype, so its Effect unification ignore marker was removed. + +- `SynchronizedRef.unsafeMake` -> `SynchronizedRef.makeUnsafe`: The unsafe suffix moved to the end. + +### `effect/TArray` + +- `TArray.TArray` -> `TxChunk.TxChunk`: TArray has no direct v4 counterpart; TxChunk is the closest rewrite target but uses whole-Chunk operations. + +- `TArray.TArray.Variance` -> `none`: TArray was removed and TxChunk exposes no public variance marker. + +- `TArray.TArrayTypeId` -> `TxChunk.isTxChunk`: TArray and its public type id were removed; use the TxChunk runtime guard after rewriting the data structure. + +- `TArray.collectFirst` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.collectFirstSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.contains` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.count` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.countSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.empty` -> `TxChunk.empty`: TArray was removed; TxChunk is the closest v4 transactional indexed collection. + +- `TArray.every` -> `Effect.map(TxChunk.get(self), Chunk.every(predicate))`: TArray was removed; read the TxChunk snapshot and test every element inside the surrounding Effect.tx transaction. + +- `TArray.everySTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.findFirstIndex` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findFirstIndexFrom` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findFirstIndexWhere` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findFirstIndexWhereFrom` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findFirstIndexWhereFromSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.findFirstIndexWhereSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.findFirstSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.findLast` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findLastIndex` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findLastIndexFrom` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.findLastSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.fromIterable` -> `TxChunk.fromIterable`: TArray was removed; construct the v4 TxChunk rewrite target from the iterable. + +- `TArray.get` -> `Effect.map(TxChunk.get(self), Chunk.get(index))`: TxChunk.get returns the whole Chunk, so apply Chunk.get to preserve indexed optional lookup. + +- `TArray.headOption` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.lastOption` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.make` -> `TxChunk.fromIterable(elements)`: TxChunk.make takes one Chunk rather than variadic elements; TxChunk.fromIterable preserves the old call shape after collecting arguments. + +- `TArray.maxOption` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.minOption` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.reduce` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.reduceOption` -> `TxChunk.get + Chunk/Array operation`: TArray was removed. Read the TxChunk snapshot and perform the equivalent pure collection query inside the surrounding Effect.tx transaction. + +- `TArray.reduceOptionSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.reduceSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.size` -> `TxChunk.size`: TxChunk is the closest v4 rewrite target. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TArray.some` -> `Effect.map(TxChunk.get(self), Chunk.some(predicate))`: TArray was removed; read the TxChunk snapshot and test for a matching element inside the surrounding Effect.tx transaction. + +- `TArray.someSTM` -> `Effect.tx + TxChunk.get + Effect traversal`: TArray was removed. Read the TxChunk snapshot and perform the effectful traversal explicitly within the same Effect.tx transaction. + +- `TArray.toArray` -> `Effect.map(TxChunk.get(self), Chunk.toArray)`: TxChunk.get returns a Chunk; convert that snapshot to an Array explicitly. + +- `TArray.transform` -> `TxChunk.update(self, Chunk.map(f))`: TArray was removed; transform the whole TxChunk snapshot with a Chunk mapping function. + +- `TArray.transformSTM` -> `Effect.tx + TxChunk.get/TxChunk.set`: Read the snapshot, traverse it effectfully, and write the rebuilt Chunk within one Effect.tx transaction. + +- `TArray.update` -> `TxChunk.modify`: TxChunk updates the whole Chunk; use modify to update the indexed element and preserve the old optional-index behavior. + +- `TArray.updateSTM` -> `Effect.tx + TxChunk.get/TxChunk.set`: Read, effectfully update the indexed element, and write the rebuilt Chunk within one Effect.tx transaction. + +### `effect/TDeferred` + +- `TDeferred.TDeferred` -> `TxDeferred.TxDeferred`: Rename the type and import from "effect/TxDeferred". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TDeferred.TDeferred.Variance` -> `none`: TxDeferred exposes no public variance marker. + +- `TDeferred.TDeferredTypeId` -> `TxDeferred.isTxDeferred`: The type id is internal in v4; use the public runtime guard. + +- `TDeferred.await` -> `TxDeferred.await`: Import TxDeferred from "effect/TxDeferred"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TDeferred.make` -> `TxDeferred.make`: Import TxDeferred from "effect/TxDeferred"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +### `effect/TMap` + +- `TMap.TMap` -> `TxHashMap.TxHashMap`: Rename the type and import from "effect/TxHashMap". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.TMap.Variance` -> `none`: TxHashMap exposes no public variance marker. + +- `TMap.TMapTypeId` -> `TxHashMap.isTxHashMap`: The type id is internal in v4; use the public runtime guard. + +- `TMap.empty` -> `TxHashMap.empty`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.findAllSTM` -> `TxHashMap.entries + Effect traversal`: No effectful mapped-find helper remains; traverse the entry snapshot explicitly inside Effect.tx. + +- `TMap.findSTM` -> `TxHashMap.entries + Effect.findFirst`: No effectful mapped-find helper remains; traverse entries explicitly inside Effect.tx. + +- `TMap.fromIterable` -> `TxHashMap.fromIterable`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.get` -> `TxHashMap.get`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.getOrElse` -> `Effect.map(TxHashMap.get(self, key), Option.getOrElse(fallback))`: Compose the retained optional get operation with Option.getOrElse. + +- `TMap.has` -> `TxHashMap.has`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.isEmpty` -> `TxHashMap.isEmpty`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.keys` -> `TxHashMap.keys`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.make` -> `TxHashMap.make`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.reduce` -> `TxHashMap.reduce`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.reduceSTM` -> `TxHashMap.entries + Effect.reduce`: Snapshot entries and reduce them effectfully inside the surrounding Effect.tx transaction. + +- `TMap.remove` -> `TxHashMap.remove`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.removeAll` -> `TxHashMap.removeMany`: The bulk removal operation was renamed. + +- `TMap.setIfAbsent` -> `Effect.tx + TxHashMap.get/TxHashMap.set`: No direct helper remains; check and conditionally set under one outer transaction. + +- `TMap.size` -> `TxHashMap.size`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TMap.takeFirst` -> `none`: No atomic take-and-match helper exists in TxHashMap; implement explicit selection and removal inside Effect.tx. + +- `TMap.takeFirstSTM` -> `none`: No effectful atomic take-and-match helper exists; implement explicit traversal and removal inside Effect.tx. + +- `TMap.takeSome` -> `none`: No atomic multi-take helper exists in TxHashMap; implement explicit selection and removals inside Effect.tx. + +- `TMap.takeSomeSTM` -> `none`: No effectful atomic multi-take helper exists; implement explicit traversal and removals inside Effect.tx. + +- `TMap.toArray` -> `TxHashMap.entries`: Use the entry snapshot; it replaces the old array conversion. + +- `TMap.toChunk` -> `Effect.map(TxHashMap.entries(self), Chunk.fromIterable)`: Convert the entry snapshot to Chunk explicitly. + +- `TMap.toHashMap` -> `TxHashMap.snapshot`: The immutable HashMap snapshot operation was renamed. + +- `TMap.toMap` -> `Effect.map(TxHashMap.entries(self), (entries) => new Map(entries))`: Build a JavaScript Map from the entry snapshot. + +- `TMap.transform` -> `TxHashMap.map`: V4 map returns a new map rather than mutating self; key-changing transforms require snapshot and rebuild logic. + +- `TMap.transformSTM` -> `TxHashMap.entries + Effect traversal + TxHashMap.fromIterable`: No in-place effectful transform remains; traverse a snapshot and rebuild inside Effect.tx. + +- `TMap.transformValues` -> `TxHashMap.map`: V4 map transforms values but returns a new map rather than mutating self. + +- `TMap.transformValuesSTM` -> `TxHashMap.entries + Effect traversal + TxHashMap.fromIterable`: No effectful map remains; traverse a snapshot and rebuild inside Effect.tx. + +- `TMap.updateWith` -> `TxHashMap.modifyAt`: modifyAt is the closest atomic keyed update, but returns void; preserve any old return value explicitly if needed. + +- `TMap.values` -> `TxHashMap.values`: Import TxHashMap from "effect/TxHashMap"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +### `effect/TPriorityQueue` + +- `TPriorityQueue.TPriorityQueue` -> `TxPriorityQueue.TxPriorityQueue`: Rename the type and import from "effect/TxPriorityQueue". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.TPriorityQueue.Variance` -> `none`: TxPriorityQueue exposes no public variance marker. + +- `TPriorityQueue.TPriorityQueueTypeId` -> `TxPriorityQueue.isTxPriorityQueue`: The type id is internal in v4; use the public runtime guard. + +- `TPriorityQueue.empty` -> `TxPriorityQueue.empty`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.fromIterable` -> `TxPriorityQueue.fromIterable`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.isEmpty` -> `TxPriorityQueue.isEmpty`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.isNonEmpty` -> `TxPriorityQueue.isNonEmpty`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.make` -> `TxPriorityQueue.make`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.peek` -> `TxPriorityQueue.peek`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.size` -> `TxPriorityQueue.size`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.take` -> `TxPriorityQueue.take`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation now returns an ordinary Effect, so compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.takeAll` -> `TxPriorityQueue.takeAll`: Import TxPriorityQueue from "effect/TxPriorityQueue"; it returns an ordinary Effect containing the priority-ordered Array. + +- `TPriorityQueue.toArray` -> `TxPriorityQueue.toArray`: Import TxPriorityQueue from "effect/TxPriorityQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPriorityQueue.toChunk` -> `Effect.map(TxPriorityQueue.toArray(self), Chunk.fromIterable)`: The direct Chunk conversion was removed; convert the retained Array snapshot explicitly. + +### `effect/TPubSub` + +- `TPubSub.TPubSub` -> `TxPubSub.TxPubSub`: Rename the type and import from "effect/TxPubSub". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.TPubSubTypeId` -> `TxPubSub.isTxPubSub`: The type id is internal in v4; use the public runtime guard. + +- `TPubSub.bounded` -> `TxPubSub.bounded`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.capacity` -> `TxPubSub.capacity`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.dropping` -> `TxPubSub.dropping`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.isEmpty` -> `TxPubSub.isEmpty`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.isFull` -> `TxPubSub.isFull`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.isShutdown` -> `TxPubSub.isShutdown`: Import TxPubSub from "effect/TxPubSub"; the operation now returns an ordinary Effect. + +- `TPubSub.shutdown` -> `TxPubSub.shutdown`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.size` -> `TxPubSub.size`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.sliding` -> `TxPubSub.sliding`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TPubSub.subscribeScoped` -> `TxPubSub.subscribe`: The scoped subscription constructor lost its Scoped suffix; it still requires Scope and returns a TxQueue. + +- `TPubSub.unbounded` -> `TxPubSub.unbounded`: Import TxPubSub from "effect/TxPubSub"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +### `effect/TQueue` + +- `TQueue.BaseTQueue` -> `TxQueue.TxQueueState`: The shared queue state model was renamed and now includes the richer open, closing, and done lifecycle. + +- `TQueue.TDequeue` -> `TxQueue.TxDequeue`: Rename the read-side type; it now carries an error channel. + +- `TQueue.TDequeueTypeId` -> `TxQueue.isTxDequeue`: The type id is internal in v4; use the public runtime guard. + +- `TQueue.TEnqueue` -> `TxQueue.TxEnqueue`: Rename the write-side type; it now carries an error channel. + +- `TQueue.TEnqueueTypeId` -> `TxQueue.isTxEnqueue`: The type id is internal in v4; use the public runtime guard. + +- `TQueue.TQueue` -> `TxQueue.TxQueue`: Rename the type; it now carries an error channel and completion lifecycle. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.TQueue.TDequeueVariance` -> `TxQueue.TxDequeue.Variance`: The read-side variance marker moved under TxDequeue and now includes the error type. + +- `TQueue.TQueue.TEnqueueVariance` -> `TxQueue.TxEnqueue.Variance`: The write-side variance marker moved under TxEnqueue and now includes the error type. + +- `TQueue.bounded` -> `TxQueue.bounded`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.capacity` -> `queue.capacity`: Capacity is now a property on TxQueue handles rather than a module function. + +- `TQueue.dropping` -> `TxQueue.dropping`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.isEmpty` -> `TxQueue.isEmpty`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.isFull` -> `TxQueue.isFull`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.isShutdown` -> `TxQueue.isShutdown`: Import TxQueue from "effect/TxQueue"; it checks the richer done lifecycle and returns an ordinary Effect. + +- `TQueue.isTDequeue` -> `TxQueue.isTxDequeue`: The runtime guard was renamed with the TxDequeue type. + +- `TQueue.isTEnqueue` -> `TxQueue.isTxEnqueue`: The runtime guard was renamed with the TxEnqueue type. + +- `TQueue.isTQueue` -> `TxQueue.isTxQueue`: The runtime guard was renamed with the TxQueue type. + +- `TQueue.offerAll` -> `TxQueue.offerAll`: The operation remains, but now returns rejected elements rather than a boolean. + +- `TQueue.peek` -> `TxQueue.peek`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.poll` -> `TxQueue.poll`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.seek` -> `none`: TxQueue has no seek helper; repeat TxQueue.take under Effect.tx until the predicate matches. + +- `TQueue.shutdown` -> `TxQueue.shutdown`: The operation remains, but now returns whether shutdown changed the queue state. + +- `TQueue.size` -> `TxQueue.size`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.sliding` -> `TxQueue.sliding`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.take` -> `TxQueue.take`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.takeAll` -> `TxQueue.takeAll`: The operation now blocks until at least one item is available, returns a NonEmptyArray, and propagates the queue error channel through an ordinary Effect. + +- `TQueue.takeBetween` -> `TxQueue.takeBetween`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.takeN` -> `TxQueue.takeN`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TQueue.unbounded` -> `TxQueue.unbounded`: Import TxQueue from "effect/TxQueue"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +### `effect/TRandom` + +- `TRandom.TRandom` -> `none`: The transactional random service was deliberately removed; use Random outside retried transactions where possible. + +- `TRandom.TRandomTypeId` -> `none`: TRandom and its public type id were removed; v4 has no TxRandom module. + +- `TRandom.Tag` -> `Random.Random`: Use the v4 Random Context.Reference; the transactional random service was removed. + +- `TRandom.next` -> `Random.next`: TxRandom was removed. Random.next is an ordinary Effect and may be re-executed if used inside a retried transaction. + +- `TRandom.nextBoolean` -> `Random.nextBoolean`: TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry. + +- `TRandom.nextInt` -> `Random.nextInt`: TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry. + +- `TRandom.nextIntBetween` -> `Random.nextIntBetween(low, high, { halfOpen: true })`: TxRandom was removed; request half-open bounds explicitly to preserve the v3 range behavior. + +- `TRandom.nextRange` -> `Random.nextBetween`: The operation was renamed and is no longer backed by rollback-safe transactional random state. + +- `TRandom.shuffle` -> `Random.shuffle`: TxRandom was removed. This ordinary Effect is not rollback-safe under transaction retry. + +### `effect/TReentrantLock` + +- `TReentrantLock.TReentrantLock` -> `TxReentrantLock.TxReentrantLock`: Rename the type and import from "effect/TxReentrantLock". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TReentrantLock.TReentrantLock.Proto` -> `none`: The public prototype interface was removed. + +- `TReentrantLock.TReentrantLockTypeId` -> `TxReentrantLock.isTxReentrantLock`: The type id is internal in v4; use the public runtime guard. + +- `TReentrantLock.fiberReadLocks` -> `none`: Per-fiber read-lock counts were removed; TxReentrantLock.readLocks reports only the total count. + +- `TReentrantLock.fiberWriteLocks` -> `none`: Per-fiber write-lock counts were removed; TxReentrantLock.writeLocks reports only the total count. + +- `TReentrantLock.lock` -> `TxReentrantLock.writeLock`: The generic lock helper was renamed to make write-lock acquisition explicit. + +- `TReentrantLock.make` -> `TxReentrantLock.make()`: The constructor keeps its name but is now a function call rather than a constant STM value. + +### `effect/TRef` + +- `TRef.TRef` -> `TxRef.TxRef`: Rename the type and import from "effect/TxRef". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TRef.TRef.Variance` -> `none`: TxRef exposes no public variance marker. + +- `TRef.TRefTypeId` -> `TxRef.isTxRef`: The type id is internal in v4; use the public runtime guard. + +- `TRef.get` -> `TxRef.get`: Import TxRef from "effect/TxRef"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TRef.getAndUpdateSome` -> `TxRef.modify`: Use one atomic modify and keep the old value when the partial update returns None. + +- `TRef.make` -> `TxRef.make`: Import TxRef from "effect/TxRef"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TRef.modifySome` -> `TxRef.modify`: Use one atomic modify and return the fallback result when the partial function returns None. + +- `TRef.setAndGet` -> `TxRef.modify`: Use one atomic modify that returns and stores the new value. + +- `TRef.updateSome` -> `TxRef.modify`: Use one atomic modify and retain the old value when the partial update returns None. + +- `TRef.updateSomeAndGet` -> `TxRef.modify`: Use one atomic modify that returns the resulting value, retaining the old value for None. + +### `effect/TSemaphore` + +- `TSemaphore.TSemaphore` -> `TxSemaphore.TxSemaphore`: Rename the type and import from "effect/TxSemaphore". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSemaphore.TSemaphore.Proto` -> `none`: The public prototype interface was removed. + +- `TSemaphore.TSemaphoreTypeId` -> `TxSemaphore.isTxSemaphore`: The type id is internal in v4; use the public runtime guard. + +- `TSemaphore.available` -> `TxSemaphore.available`: Import TxSemaphore from "effect/TxSemaphore"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSemaphore.make` -> `TxSemaphore.make`: Import TxSemaphore from "effect/TxSemaphore"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSemaphore.release` -> `TxSemaphore.release`: Import TxSemaphore from "effect/TxSemaphore"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSemaphore.unsafeMake` -> `none`: The unsafe constructor was removed; use TxSemaphore.make. + +- `TSemaphore.withPermit` -> `TxSemaphore.withPermit`: The helper remains, but data-first calls now pass the semaphore before the Effect. + +- `TSemaphore.withPermits` -> `TxSemaphore.withPermits`: The helper remains, but data-first calls now pass semaphore, permit count, then Effect. + +- `TSemaphore.withPermitsScoped` -> `TxSemaphore.acquireN + Effect.addFinalizer(TxSemaphore.releaseN)`: No scoped multi-permit helper remains; acquire and register release explicitly in a Scope. + +### `effect/TSet` + +- `TSet.TSet` -> `TxHashSet.TxHashSet`: Rename the type and import from "effect/TxHashSet". V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.TSet.Variance` -> `none`: TxHashSet exposes no public variance marker. + +- `TSet.TSetTypeId` -> `TxHashSet.isTxHashSet`: The type id is internal in v4; use the public runtime guard. + +- `TSet.difference` -> `TxHashSet.difference`: The name remains, but v4 returns a new set instead of mutating self. + +- `TSet.empty` -> `TxHashSet.empty`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.fromIterable` -> `TxHashSet.fromIterable`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.has` -> `TxHashSet.has`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.intersection` -> `TxHashSet.intersection`: The name remains, but v4 returns a new set instead of mutating self. + +- `TSet.isEmpty` -> `TxHashSet.isEmpty`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.make` -> `TxHashSet.make`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.reduce` -> `TxHashSet.reduce`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.reduceSTM` -> `TxHashSet.toHashSet + Effect.reduce`: Snapshot the set and reduce effectfully inside the surrounding Effect.tx transaction. + +- `TSet.remove` -> `TxHashSet.remove`: The name remains, but v4 returns whether the value existed. + +- `TSet.removeAll` -> `Effect.forEach(values, (value) => TxHashSet.remove(self, value))`: No bulk removal helper remains; remove each value inside one outer Effect.tx transaction. + +- `TSet.size` -> `TxHashSet.size`: Import TxHashSet from "effect/TxHashSet"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSet.takeFirst` -> `none`: No atomic take-and-match helper exists in TxHashSet; select and remove explicitly inside Effect.tx. + +- `TSet.takeFirstSTM` -> `none`: No effectful atomic take-and-match helper exists; traverse and remove explicitly inside Effect.tx. + +- `TSet.takeSome` -> `none`: No atomic multi-take helper exists in TxHashSet; select and remove explicitly inside Effect.tx. + +- `TSet.takeSomeSTM` -> `none`: No effectful atomic multi-take helper exists; traverse and remove explicitly inside Effect.tx. + +- `TSet.toArray` -> `Effect.map(TxHashSet.toHashSet(self), Array.from)`: Convert the immutable HashSet snapshot to an Array explicitly. + +- `TSet.toChunk` -> `Effect.map(TxHashSet.toHashSet(self), (set) => Chunk.fromIterable(set))`: Convert the immutable HashSet snapshot to Chunk explicitly. + +- `TSet.toReadonlySet` -> `Effect.map(TxHashSet.toHashSet(self), (set) => new Set(set))`: Convert the immutable HashSet snapshot to a JavaScript ReadonlySet explicitly. + +- `TSet.transform` -> `TxHashSet.map`: The closest helper returns a new set instead of mutating self. + +- `TSet.transformSTM` -> `TxHashSet.toHashSet + Effect traversal + TxHashSet.fromIterable`: No effectful transform remains; traverse a snapshot and rebuild inside Effect.tx. + +- `TSet.union` -> `TxHashSet.union`: The name remains, but v4 returns a new set instead of mutating self. + +### `effect/TSubscriptionRef` + +- `TSubscriptionRef.TSubscriptionRef` -> `TxSubscriptionRef.TxSubscriptionRef`: Rename the type; it no longer extends TxRef. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSubscriptionRef.TSubscriptionRef.Variance` -> `none`: TxSubscriptionRef exposes no public variance marker. + +- `TSubscriptionRef.TSubscriptionRefTypeId` -> `TxSubscriptionRef.isTxSubscriptionRef`: The type id is internal in v4; use the public runtime guard. + +- `TSubscriptionRef.changes` -> `none`: The old unscoped transactional subscription was removed; use scoped TxSubscriptionRef.changes. + +- `TSubscriptionRef.changesScoped` -> `TxSubscriptionRef.changes`: The scoped changes operation lost its Scoped suffix and returns a scoped TxQueue. + +- `TSubscriptionRef.get` -> `TxSubscriptionRef.get`: Import TxSubscriptionRef from "effect/TxSubscriptionRef"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSubscriptionRef.getAndUpdateSome` -> `TxSubscriptionRef.modify`: Use one atomic modify so successful updates are still published; retain the old value for None. + +- `TSubscriptionRef.make` -> `TxSubscriptionRef.make`: Import TxSubscriptionRef from "effect/TxSubscriptionRef"; the operation keeps its name. V4 Tx operations return ordinary Effects; compose multiple operations under one outer Effect.tx to keep them atomic. + +- `TSubscriptionRef.modifySome` -> `TxSubscriptionRef.modify`: Use one atomic modify so successful updates are still published; use the fallback result for None. + +- `TSubscriptionRef.setAndGet` -> `TxSubscriptionRef.modify`: Use one atomic modify that publishes and returns the newly stored value. + +- `TSubscriptionRef.updateSome` -> `TxSubscriptionRef.modify`: Use one atomic modify so updates are published, retaining the old value for None. + +- `TSubscriptionRef.updateSomeAndGet` -> `TxSubscriptionRef.modify`: Use one atomic modify that publishes and returns the resulting value, retaining the old value for None. + +### `effect/Take` + +- `Take.Take` -> `Take.Take`: v4 Take is the plain union NonEmptyReadonlyArray\ | Exit.Exit\ — no wrapper object or Pipeable: a value batch is a non-empty array, a failure is a failed Exit, and end-of-stream is a successful Exit carrying the Done value (void by default). The module keeps the effect/Take path but exports only the type and toPull. + +- `Take.Take.Variance` -> `none`: Variance plumbing removed; v4 Take is a plain union type with no branded interface, so there is nothing to migrate to. + +- `Take.TakeTypeId` -> `none`: No brand symbol in v4; discriminate the union with Exit.isExit(take) (Exit branch) vs the non-empty array branch (Array.isReadonlyArrayNonEmpty). + +- `Take.chunk` -> `NonEmptyReadonlyArray`: No constructor needed: a value-batch Take is just the non-empty array of values itself (convert a v3 Chunk with Array.fromIterable); empty batches are not representable and must be skipped. + +- `Take.dieMessage` -> `Exit.die(new Error(message))`: A defect Take is a died Exit; wrap the message in an Error yourself since there is no dedicated dieMessage helper. + +- `Take.done` -> `Take.toPull`: Take.toPull(take) converts a Take into a Pull (Effect succeeding with the batch); end-of-stream surfaces as Cause.Done in the error channel instead of v3's Option.none, and failures keep their cause. + +- `Take.fail` -> `Exit.fail`: A failing Take is simply the failed Exit: Exit.fail(error). + +- `Take.failCause` -> `Exit.failCause`: A failing Take with a full cause is simply Exit.failCause(cause). + +#### `Take.fromEffect` + +**Replacement:** `Effect.exit + Exit.isSuccess` + +Run the effect with Effect.exit and convert the result: a successful exit value a becomes the single-element batch [a], a failed exit is used directly as the Take. + +**Example** + +```ts +Effect.map(Effect.exit(effect), (exit) => Exit.isSuccess(exit) ? [exit.value] as const : exit) +``` + +- `Take.fromExit` -> `Exit.isSuccess(exit) ? [exit.value] : exit`: A success exit becomes the single-element batch [a]; a failure exit is already a valid v4 Take and is used as-is. + +#### `Take.fromPull` + +**Replacement:** `Effect.matchCause + Pull.doneExitFromCause` + +Convert one v4 Pull step into a Take: the success batch is the Take itself, and Pull.doneExitFromCause turns the failure cause into the Exit branch (Cause.Done becomes a successful end Exit, real failures become a failed Exit). + +**Example** + +```ts +Effect.matchCause(pull, { onSuccess: (arr) => arr, onFailure: Pull.doneExitFromCause }) +``` + +- `Take.isDone` -> `Exit.isExit(take) && Exit.isSuccess(take)`: End-of-stream is the successful-Exit branch of the union. + +- `Take.isFailure` -> `Exit.isExit(take) && Exit.isFailure(take)`: A failure Take is the failed-Exit branch of the union. + +- `Take.isSuccess` -> `!Exit.isExit(take)`: A value batch is the non-Exit branch; use Array.isReadonlyArrayNonEmpty(take) when a positive refinement to NonEmptyReadonlyArray\ is needed. + +- `Take.make` -> `none`: No wrapper constructor: build the union value directly — a non-empty array for values, Exit.fail/Exit.failCause for errors, Exit.succeed(done) (or Exit.void) for end-of-stream; the v3 Exit\, Option\\> encoding is gone. + +- `Take.map` -> `Exit.isExit(take) ? take : Array.map(take, f)`: Only the value batch is mapped; effect's Array.map preserves the NonEmptyReadonlyArray type, and Exit branches (failure/end) pass through unchanged. + +#### `Take.match` + +**Replacement:** `Exit.isExit + Exit.match` + +Branch on the union: the array branch is v3's onSuccess(chunk), and Exit.match splits the Exit branch into onFailure(cause) and end-of-stream (v3 onEnd, success value = Done). + +**Example** + +```ts +// v3: Take.match(take, { onEnd, onFailure, onSuccess }) +Exit.isExit(take) + ? Exit.match(take, { onSuccess: () => onEnd(), onFailure: (cause) => onFailure(cause) }) + : onSuccess(take) + +``` + +- `Take.matchEffect` -> `Pull.matchEffect(Take.toPull(take), { onSuccess, onFailure, onDone })`: Convert with Take.toPull and fold with Pull.matchEffect: onSuccess receives the batch (v3 onSuccess), onFailure the cause, onDone the completion value (v3 onEnd); alternatively branch manually with Exit.isExit as for match. + +- `Take.of` -> `[value]`: A single-value Take is just the one-element non-empty array literal. + +- `Take.tap` -> `Exit.isExit(take) ? Exit.asVoid(take) : Effect.asVoid(f(take))`: Peek at the value batch with f; Exit branches pass through as effects (a failed Exit re-propagates its cause, an end Exit becomes a void success), matching v3 tap semantics. + +### `effect/TestClock` + +- `TestClock.Data` -> `TestClock.TestClock.State`: The nearest state model is State, with timestamp and a private latch-based sleep queue. V4 exposes no full state getter or setter. + +- `TestClock.adjustWith` -> `Effect.zipWith(effect, TestClock.adjust(duration), (result) => result, { concurrent: true })`: V4 removed adjustWith. Run the tested effect and clock adjustment concurrently and retain the tested effect's result. + +- `TestClock.currentTimeMillis` -> `Clock.currentTimeMillis`: Read time from the active Clock reference; under it.effect or TestClock.layer() this is virtual time. + +- `TestClock.defaultTestClock` -> `TestClock.layer()`: The v4 layer creates an epoch-based test clock and captures the surrounding live Clock automatically; it no longer needs TestAnnotations or TestLive. + +- `TestClock.makeData` -> `TestClock.layer() + TestClock.setTime(instant)`: State injection was removed. Build the layer, then set initial time; seeded pending sleeps cannot migrate because the queue is private. + +- `TestClock.save` -> `none`: Full clock snapshots including pending sleeps are no longer public. For timestamp-only restoration, read Clock.currentTimeMillis and later call TestClock.setTime(savedMillis). + +- `TestClock.sleeps` -> `none`: The pending-sleep queue is private. Test observable behavior by forking sleepers, adjusting time, and joining or asserting the fibers. + +- `TestClock.testClock` -> `TestClock.testClockWith(Effect.succeed)`: V4 exposes callback-based access to the active test clock; use testClockWith directly when possible. + +### `effect/TestConfig` + +- `TestConfig.TestConfig` -> `none`: There is no v4 TestConfig service. Move runner settings to Vitest and FastCheck options, or define an application-specific Context.Reference if runtime access is needed. + +- `TestConfig.make` -> `{ repeats, retries, samples, shrinks }`: The v3 constructor only returned its parameter object. The TestConfig service was removed; keep a plain object only for application-owned configuration. + +### `effect/TestContext` + +- `TestContext.LiveContext` -> `@effect/vitest#live`: Default runtime references are live in v4. Use it.live for a whole live test; no LiveContext layer is required. + +- `TestContext.TestContext` -> `Layer.mergeAll(TestConsole.layer, TestClock.layer())`: This is the v4 test layer used by @effect/vitest. Prefer it.effect, which provides it automatically. + +### `effect/TestLive` + +- `TestLive.TestLive` -> `none`: There is no grouped live-default-services object. Use Context.Context plus Effect.provideContext, TestClock.withLive for live time, or it.live for the whole test. + +- `TestLive.TestLiveTypeId` -> `none`: The TestLive nominal wrapper was removed, so its type id has no replacement. + +- `TestLive.make` -> `Effect.provideContext`: The wrapper was removed. Apply a captured Context directly with Effect.provideContext; for live time inside it.effect, prefer TestClock.withLive. + +### `effect/TestServices` + +- `TestServices.TestServices` -> `TestClock.TestClock | TestConsole.TestConsole`: This is the v4 @effect/vitest test-environment union. it.effect provides both automatically; it.live provides neither override. + +- `TestServices.annotate` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.annotations` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.annotationsLayer` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.annotationsWith` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.currentServices` -> `Effect.context()`: The separate FiberRef\\> was removed. Test services now live in the ordinary Effect Context; override individual references with Effect.provideService. + +- `TestServices.get` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.liveLayer` -> `none`: The standalone TestLive service and layer were removed. TestClock.layer() captures its surrounding live Clock itself. + +- `TestServices.liveServices` -> `none`: There is no prebuilt aggregate test-service Context. @effect/vitest constructs TestClock and TestConsole layers per test; live references are defaults. + +- `TestServices.liveWith` -> `TestClock.withLive`: There is no TestLive callback object. Refactor to the effect ultimately run and apply TestClock.withLive, or use it.live for whole-test live execution. + +- `TestServices.provideLive` -> `TestClock.withLive`: For live time, run the effect with the Clock captured by TestClock.layer(). Use it.live when the entire test should omit all test-service overrides. + +- `TestServices.provideWithLive` -> `TestClock.testClockWith + TestClock.withLive + Effect.provideService`: To retain test time for the inner effect while its transformer uses live time, combine testClockWith, withLive, and provideService. Other v3 default services have no aggregate equivalent. + +- `TestServices.repeats` -> `Vitest TestOptions.repeats`: Configure repeats in the Vitest options passed to it.effect or it.live; it is no longer an Effect service value. + +- `TestServices.retries` -> `Vitest TestOptions.retry`: Configure retry in Vitest test options. To retry an Effect inside a test, use Effect.retry. + +- `TestServices.samples` -> `{ fastCheck: { numRuns } }`: Pass the run count through @effect/vitest property-test options, for example it.effect.prop(..., { fastCheck: { numRuns: samples } }). + +- `TestServices.shrinks` -> `none`: The legacy maximum-shrinks service setting was removed; @effect/vitest forwards FastCheck.Parameters, which has no equivalent service value. + +- `TestServices.size` -> `CurrentSize`: Define a custom Context.Reference\ and yield it to read the current size. + +- `TestServices.sized` -> `CurrentSize`: TestSized was removed. Use a custom Context.Reference\ directly instead of a wrapper object. + +- `TestServices.sizedLayer` -> `Layer.succeed(CurrentSize, size)`: Provide the custom size reference as a layer. + +- `TestServices.sizedWith` -> `CurrentSize.use`: Use the custom reference's callback, or preferably yield CurrentSize in Effect.gen. + +- `TestServices.supervisedFibers` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.testConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. + +- `TestServices.testConfigLayer` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. + +- `TestServices.testConfigWith` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. + +- `TestServices.withAnnotations` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.withAnnotationsScoped` -> `none`: The annotation service was removed. Use Vitest metadata/options for runner concerns, an ordinary Ref or Context.Reference for application-owned test state, and FiberSet for explicit fiber tracking. + +- `TestServices.withLiveScoped` -> `none`: There is no scoped TestLive service override. Apply TestClock.withLive to a specific effect, or choose it.live at test declaration time. + +- `TestServices.withSize` -> `Effect.provideService(effect, CurrentSize, size)`: Provide a custom size Context.Reference for the duration of the wrapped effect. + +- `TestServices.withSized` -> `Effect.provideService(effect, CurrentSize, size)`: Collapse the old TestSized wrapper to its numeric value and provide the custom reference. + +- `TestServices.withSizedScoped` -> `Effect.updateServiceScoped(CurrentSize, () => size)`: For a scope-bounded override use updateServiceScoped; otherwise prefer wrapping the workflow with Effect.provideService. + +- `TestServices.withTestConfig` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. + +- `TestServices.withTestConfigScoped` -> `none`: The runner no longer reads an Effect TestConfig service. Use Vitest TestOptions and property-test fastCheck options; model application state as a custom Context.Reference. + +### `effect/TestSized` + +- `TestSized.TestSized` -> `Context.Reference`: Collapse the wrapper service to the reference itself; yield the reference to read the current size. + +- `TestSized.TestSizedTypeId` -> `none`: The wrapper's nominal type id is unnecessary; Context.Reference supplies stable key identity. + +- `TestSized.fromFiberRef` -> `Context.Reference`: FiberRef and TestSized were removed. Define one stable Context.Reference\ instead of wrapping a FiberRef. + +- `TestSized.make` -> `Context.Reference`: Define a module-level reference with defaultValue; do not create a fresh key at each call site. + +### `effect/Tracer` + +- `Tracer.DisablePropagation` -> `Tracer.DisablePropagation`: Keep the reference value. The separate phantom interface is gone; the Context.Reference directly stores boolean. + +- `Tracer.ExternalSpan` -> `Tracer.ExternalSpan`: Keep the type, but rename the context field to annotations. Apply the same rename to Tracer.externalSpan options. + +- `Tracer.ParentSpan` -> `Tracer.ParentSpan`: Keep the API. It is now a Context.Service class for AnySpan rather than a separate phantom interface plus Context.Tag. + +- `Tracer.Span` -> `Tracer.Span`: Keep the type and rename span.context to span.annotations; the other public fields and methods remain. + +- `Tracer.SpanLink` -> `Tracer.SpanLink`: Keep the type but remove the \_tag property; v4 links contain only span and attributes. + +- `Tracer.SpanOptions` -> `Tracer.SpanOptions`: Keep the type and rename context to annotations. V4 splits trace options and additionally accepts sampled and level. + +- `Tracer.Tracer` -> `Tracer.Tracer`: The service is now a defaulted Context.Reference. Custom implementations are structural and receive one span options object; context is optional and now receives an Effect primitive plus Fiber. + +- `Tracer.TracerTypeId` -> `none`: Tracer implementations are structural and no longer carry a public type-id brand. + +- `Tracer.tracerWith` -> `Tracer.Tracer.use`: Replace tracerWith(f) with Tracer.Tracer.use(f); do not use TracerKey, which is only the raw string key. + +### `effect/Trie` + +- `Trie.TypeId` -> `none`: The Trie brand is private and there is no public Trie runtime guard; use Trie.Trie\ in type positions. + +- `Trie.unsafeGet` -> `Trie.getUnsafe`: Direct word-order rename; it still throws for a missing key. + +### `effect/Tuple` + +- `Tuple.TupleTypeLambda` -> `none`: Removed with tuple Bicovariant support; use the concrete tuple type or a local HKT TypeLambda. + +- `Tuple.at` -> `Tuple.get`: Renamed for indexed access; v4 constrains the index to a valid tuple position. + +- `Tuple.getEquivalence` -> `Tuple.makeEquivalence`: Pass equivalences as one array instead of variadic arguments. + +- `Tuple.getFirst` -> `Tuple.get(0)`: Use Tuple.get(self, 0), or Tuple.get(0) in a pipe. + +- `Tuple.getOrder` -> `Tuple.makeOrder`: Pass orders as one array instead of variadic arguments. + +- `Tuple.getSecond` -> `Tuple.get(1)`: Use Tuple.get(self, 1), or Tuple.get(1) in a pipe. + +- `Tuple.mapBoth` -> `Tuple.evolve`: Use Tuple.evolve(self, [options.onFirst, options.onSecond]). + +- `Tuple.mapFirst` -> `Tuple.evolve`: Use Tuple.evolve(self, [f]); unspecified positions are preserved. + +- `Tuple.mapSecond` -> `Tuple.evolve`: Use Tuple.evolve(self, [undefined, f]); undefined preserves the first position. + +- `Tuple.swap` -> `Tuple.renameIndices`: Swap a pair with Tuple.renameIndices(self, ["1", "0"]). + +### `effect/Types` + +- `Types.Concurrency` -> `Types.Concurrency`: Still exported, but v4 removes inherit; replace it with an explicit number or unbounded. + +- `Types.Contravariant` -> `Types.Contravariant`: Unchanged contravariant type helper. + +- `Types.Covariant` -> `Types.Covariant`: Unchanged covariant type helper. + +- `Types.Ctor` -> `new (...args: Array) => T`: The named alias was removed; inline the construct signature or define a local alias. + +- `Types.Invariant` -> `Types.Invariant`: Unchanged invariant type helper. + +- `Types.MatchRecord` -> `{} extends S ? onTrue : onFalse`: The alias was removed; inline its conditional because Types.VoidIfEmpty has different optional-record behavior. + +- `Types.MergeRecord` -> `Types.MergeLeft`: MergeRecord was an alias for the retained left-biased MergeLeft helper. + +- `Types.NoExcessProperties` -> `Types.NoExcessProperties`: Retained with equivalent excess-key checking. + +### `effect/Utils` + +- `Utils.Adapter` -> `none`: The generator-adapter type was removed; type generator bodies to yield v4 yieldable values directly. + +- `Utils.Gen` -> `Utils.Gen`: Still exported; drop the adapter type and resume parameter, then yield yieldable Kind values directly. + +- `Utils.GenKind` -> `none`: The adapter wrapper was removed; custom yieldable Kinds should implement Symbol.iterator and return Utils.SingleShotGen. + +- `Utils.GenKindImpl` -> `none`: The wrapper implementation was removed; implement direct yieldability with Symbol.iterator and Utils.SingleShotGen. + +- `Utils.GenKindTypeId` -> `none`: The GenKind runtime marker was removed with the wrapper infrastructure. + +- `Utils.OptionalNumber` -> `number | null | undefined`: The unused named alias was removed; inline its union. + +- `Utils.PCGRandom` -> `Random.withSeed + Random.next / Random.nextIntBetween`: Use the effectful Random service for seeded generation; v4 is not PCG-compatible. + +- `Utils.PCGRandomState` -> `none`: No public PCG state snapshot or restore API remains; Random.withSeed is reproducible but not state-compatible. + +- `Utils.SingleShotGen` -> `Utils.SingleShotGen`: Still exported; v4 removes its concrete return and throw methods, so do not call those optional iterator hooks. + +- `Utils.Variance` -> `Utils.Variance`: Still exported; remove the v3 GenKindTypeId marker from implementations. + +- `Utils.YieldWrap` -> `none`: The internal generator transport wrapper was removed; yieldable values are yielded directly. + +- `Utils.YieldWrapTypeId` -> `none`: The internal wrapper marker was removed with YieldWrap. + +- `Utils.adapter` -> `none`: Remove the adapter and resume parameter; v4 generators yield yieldable values directly. + +- `Utils.internalCall` -> `none`: This was internal and has no public replacement; application code should invoke its thunk directly. + +- `Utils.isGenKind` -> `none`: Removed with GenKind; v4 generator drivers consume directly yielded values. + +- `Utils.isGeneratorFunction` -> `none`: The unused constructor-identity predicate was removed; accept an explicit generator contract instead. + +- `Utils.makeGenKind` -> `none`: The wrapper constructor was removed; make custom Kinds yieldable with Symbol.iterator and Utils.SingleShotGen. + +- `Utils.structuralRegion` -> `none`: Remove the wrapper because v4 Equal.equals is structural by default; use a custom Equivalence for custom comparison. + +- `Utils.structuralRegionState` -> `none`: The mutable test hook was removed; v4 equality is structural by default. + +- `Utils.yieldWrapGet` -> `none`: The internal unwrapper was removed; generator drivers read the directly yielded value. + +### `effect/index` + +- `index.Context` -> `Context`: Keep importing Context from effect; v4 removes declaration merges that made tags and references STM subtypes. + +- `index.Effect` -> `Effect`: Keep importing Effect from effect; v4 removes declaration merges that made Effects structural Sink, Stream, and Channel subtypes. + +- `index.Either` -> `Result`: Either was renamed to Result; Right and Left became Success and Failure, with Result.succeed and Result.fail constructors. + +- `index.Option` -> `Option`: Keep importing Option from effect; Option is no longer an Effect or STM subtype, so use Effect.fromOption when needed. diff --git a/.context/effect/package.json b/.context/effect/package.json index 7214af7e9..60c4f2617 100644 --- a/.context/effect/package.json +++ b/.context/effect/package.json @@ -3,41 +3,46 @@ "type": "module", "packageManager": "pnpm@10.17.1", "scripts": { - "prepare": "effect-tsgo patch", + "prepare": "node scripts/setup-agents.mjs && effect-tsgo patch", "clean": "node scripts/clean.mjs", "codegen": "pnpm --recursive --parallel --filter \"./packages/**/*\" run codegen", "codemod": "node scripts/codemod.mjs", - "build": "tsc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build", + "build": "tsc -b tsconfig.packages.json && pnpm --recursive --parallel --filter \"./packages/**/*\" run build && node scripts/copy-ai-docs.mjs", "bundle-analyze": "bash scripts/bundle-analyze.sh", "bundle-compare": "bash scripts/bundle-compare.sh", "bundle-compare-selected": "bash scripts/bundle-compare-selected.sh", "circular": "node scripts/circular.mjs", "test": "vitest", + "test-cluster": "vitest run --project cluster-integration", + "doctest": "vitest --config vitest.docs.ts", "coverage": "vitest --coverage", "check": "tsc -b tsconfig.json", "typeperf": "pnpm --dir packages/effect exec node typeperf/run.mjs", "typeperf-compare": "pnpm --dir packages/effect exec node typeperf/compare.mjs", + "runtimeperf": "pnpm --dir packages/effect exec node runtimeperf/run.mts", + "runtimeperf-compare": "pnpm --dir packages/effect exec node runtimeperf/compare.mts", "check-recursive": "pnpm --recursive --filter \"./packages/**/*\" exec tsc -b tsconfig.json", "jsdocs": "effect-jsdocs", - "lint": "pnpm jsdocs && oxlint -f unix && dprint check", - "lint-fix": "pnpm jsdocs && oxlint --fix && dprint fmt", - "docgen": "pnpm --recursive --filter \"./packages/**/*\" exec docgen && node scripts/docs.mjs", + "lint": "oxlint -f unix && dprint check", + "lint-fix": "oxlint --fix && dprint fmt", "ai-docgen": "effect-ai-docgen ai-docs/src -o LLMS.md", "ai-docgen:watch": "pnpm ai-docgen --watch", + "api-diff": "pnpm --dir packages/tools/api-diff exec node src/bin.ts", "test-types": "tstyche --target '>=5.9'", "changeset-version": "changeset version", - "changeset-publish": "pnpm codemod && pnpm build && changeset publish" + "changeset-publish": "node scripts/set-strip-internal.mjs && pnpm codemod && pnpm build && changeset publish" }, "devDependencies": { - "@babel/cli": "^7.29.7", - "@babel/core": "^7.29.7", - "@babel/plugin-transform-export-namespace-from": "^7.29.7", - "@babel/plugin-transform-modules-commonjs": "^7.29.7", - "@changesets/changelog-github": "^0.7.0", - "@changesets/cli": "^2.31.1", + "@babel/cli": "^8.0.4", + "@babel/core": "^8.0.1", + "@babel/plugin-transform-export-namespace-from": "^8.0.1", + "@babel/plugin-transform-modules-commonjs": "^8.0.1", + "@changesets/changelog-github": "1.0.0-next.9", + "@changesets/cli": "3.0.0-next.11", "@effect/ai-docgen": "workspace:^", "@effect/bundle": "workspace:^", - "@effect/docgen": "https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5", + "@effect/docgen": "workspace:^", + "@effect/doctest": "workspace:^", "@effect/jsdocs": "workspace:^", "@effect/oxc": "workspace:^", "@effect/tsgo": "^0.21.0", @@ -48,11 +53,11 @@ "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@types/jscodeshift": "^17.3.0", - "@types/node": "^25.9.5", - "@vitest/browser": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/expect": "4.1.10", - "@vitest/web-worker": "4.1.10", + "@types/node": "^26.1.2", + "@vitest/browser": "^4.1.10", + "@vitest/coverage-v8": "^4.1.10", + "@vitest/expect": "^4.1.10", + "@vitest/web-worker": "^4.1.10", "ast-types": "^0.14.2", "babel-plugin-annotate-pure-calls": "^0.5.0", "dprint": "^0.55.2", @@ -61,19 +66,19 @@ "jscodeshift": "^17.4.0", "lalph": "^0.3.139", "madge": "^8.0.0", - "oxlint": "1.42.0", - "playwright": "^1.61.1", - "rollup": "^4.62.2", + "oxlint": "^1.76.0", + "pkg-pr-new": "0.0.78", + "playwright": "^1.62.0", + "rollup": "^4.62.3", "rollup-plugin-bundle-stats": "^4.22.2", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-visualizer": "^7.0.1", "terser": "^5.49.0", "tstyche": "^7.2.2", "typescript": "^7.0.2", - "vite": "^7.3.6", - "vite-tsconfig-paths": "^6.1.1", - "vitest": "4.1.10", - "vitest-websocket-mock": "^0.5.0", + "vite": "^8.1.5", + "vitest": "^4.1.10", + "vitest-websocket-mock": "^0.7.0", "zod": "^4.4.3" } } diff --git a/.context/effect/packages/ai/anthropic/CHANGELOG.md b/.context/effect/packages/ai/anthropic/CHANGELOG.md index cce51671d..044c137d0 100644 --- a/.context/effect/packages/ai/anthropic/CHANGELOG.md +++ b/.context/effect/packages/ai/anthropic/CHANGELOG.md @@ -1,5 +1,72 @@ # @effect/ai-anthropic +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7129](https://github.com/Effect-TS/effect/pull/7129) [`d0baed9`](https://github.com/Effect-TS/effect/commit/d0baed9d72c5191d5ab3945ffe464b9515d0ac53) Thanks @mkdynamic! - Correct the maximum output tokens for Claude Opus 4.6, 4.7, 4.8 and Sonnet 4.6. + + These models were grouped with the 4.5 family at 64000 output tokens, half of the 128000 the API actually allows, so requests defaulted to a cap far below the model's real limit. The 4.5 models keep 64000, which is correct for them. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#7070](https://github.com/Effect-TS/effect/pull/7070) [`a5404d4`](https://github.com/Effect-TS/effect/commit/a5404d4f4361350b6ecbb1a2f601365852141f60) Thanks @fubhy! - Decode byte-backed plain-text attachments as UTF-8 text in Anthropic requests. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6918](https://github.com/Effect-TS/effect/pull/6918) [`a913136`](https://github.com/Effect-TS/effect/commit/a9131368c1347ad4c409c9df80245d765e96a4fe) Thanks @fubhy! - Fix malformed JSON in streamed Anthropic code-execution tool parameters. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6650](https://github.com/Effect-TS/effect/pull/6650) [`acd385e`](https://github.com/Effect-TS/effect/commit/acd385ebb3f9edee37ab6715607119ee9762a615) Thanks @IMax153! - Redact the Anthropic API key from client error context. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/ai/anthropic/README.md b/.context/effect/packages/ai/anthropic/README.md new file mode 100644 index 000000000..ca546be53 --- /dev/null +++ b/.context/effect/packages/ai/anthropic/README.md @@ -0,0 +1,14 @@ +# @effect/ai-anthropic + +An [Anthropic](https://www.anthropic.com) provider for the Effect AI modules. Includes a typed Anthropic API client, language model layers, tools, and telemetry helpers. + +## Installation + +```sh +npm install effect@beta @effect/ai-anthropic@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/ai-anthropic) diff --git a/.context/effect/packages/ai/anthropic/docgen.json b/.context/effect/packages/ai/anthropic/docgen.json deleted file mode 100644 index 500517eb7..000000000 --- a/.context/effect/packages/ai/anthropic/docgen.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/anthropic/src/", - "exclude": ["src/internal/**/*.ts", "src/Generated.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "exactOptionalPropertyTypes": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["node"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/ai/anthropic/package.json b/.context/effect/packages/ai/anthropic/package.json index 0bbe81ab1..733e6c697 100644 --- a/.context/effect/packages/ai/anthropic/package.json +++ b/.context/effect/packages/ai/anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-anthropic", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "An Anthropic provider integration for Effect AI SDK", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/ai/anthropic/src/AnthropicClient.ts b/.context/effect/packages/ai/anthropic/src/AnthropicClient.ts index 646feadc9..93288554f 100644 --- a/.context/effect/packages/ai/anthropic/src/AnthropicClient.ts +++ b/.context/effect/packages/ai/anthropic/src/AnthropicClient.ts @@ -36,7 +36,7 @@ import * as Errors from "./internal/errors.ts" * Represents the Anthropic client service with methods for the Messages API, including regular and streaming message * creation. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { @@ -52,7 +52,7 @@ export interface Service { schema: S ) => (request: HttpClientRequest.HttpClientRequest) => Stream.Stream< S["Type"], - HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError, S["DecodingServices"] > @@ -194,6 +194,11 @@ const RedactedAnthropicHeaders = { AnthropicApiKey: "x-api-key" } +const withRedactedHeaders = Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedAnthropicHeaders)) +) + /** * Creates an Anthropic client service with the given options. * @@ -254,7 +259,7 @@ export const make = Effect.fnUntraced( (schema: S) => (request: HttpClientRequest.HttpClientRequest): Stream.Stream< S["Type"], - HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError, S["DecodingServices"] > => httpClientOk.execute(request).pipe( @@ -276,7 +281,8 @@ export const make = Effect.fnUntraced( BetaMessagesPost4XX: (error) => Effect.fail(Errors.mapClientError(error, "createMessage")), HttpClientError: (error) => Errors.mapHttpClientError(error, "createMessage"), SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createMessage")) - }) + }), + withRedactedHeaders ) const PingEvent = Schema.Struct({ @@ -306,6 +312,7 @@ export const make = Effect.fnUntraced( Stream.catchTags({ // TODO: handle SSE retries Retry: (error) => Stream.die(error), + SseError: (error) => Stream.fail(Errors.mapSseError(error, "createMessageStream")), HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createMessageStream")), SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createMessageStream")) }) @@ -329,7 +336,8 @@ export const make = Effect.fnUntraced( Effect.catchTag( "HttpClientError", (error) => Errors.mapHttpClientError(error, "createMessageStream") - ) + ), + withRedactedHeaders ) } @@ -340,10 +348,7 @@ export const make = Effect.fnUntraced( createMessageStream }) }, - Effect.updateService( - Headers.CurrentRedactedNames, - Array.appendAll(Object.values(RedactedAnthropicHeaders)) - ) + withRedactedHeaders ) // ============================================================================= diff --git a/.context/effect/packages/ai/anthropic/src/AnthropicConfig.ts b/.context/effect/packages/ai/anthropic/src/AnthropicConfig.ts index 26e3b21ad..df27b5581 100644 --- a/.context/effect/packages/ai/anthropic/src/AnthropicConfig.ts +++ b/.context/effect/packages/ai/anthropic/src/AnthropicConfig.ts @@ -52,7 +52,7 @@ export declare namespace AnthropicConfig { * * Use `transformClient` to wrap or replace the `HttpClient` used by generated Anthropic API requests. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { diff --git a/.context/effect/packages/ai/anthropic/src/AnthropicError.ts b/.context/effect/packages/ai/anthropic/src/AnthropicError.ts index d7a521907..590176e2f 100644 --- a/.context/effect/packages/ai/anthropic/src/AnthropicError.ts +++ b/.context/effect/packages/ai/anthropic/src/AnthropicError.ts @@ -77,7 +77,7 @@ declare module "effect/unstable/ai/AiError" { * * Includes request identifiers, Anthropic error types, and parsed request or token limit headers when the provider rejects a request due to rate limits. * - * @category configuration + * @category models * @since 4.0.0 */ export interface RateLimitErrorMetadata { @@ -91,7 +91,7 @@ declare module "effect/unstable/ai/AiError" { * * Captures the Anthropic error type and request identifier for failures where the account or workspace has exhausted its available quota. * - * @category configuration + * @category models * @since 4.0.0 */ export interface QuotaExhaustedErrorMetadata { @@ -105,7 +105,7 @@ declare module "effect/unstable/ai/AiError" { * * Preserves Anthropic error details for missing, invalid, or unauthorized API credentials while keeping the error in the shared AI error model. * - * @category configuration + * @category models * @since 4.0.0 */ export interface AuthenticationErrorMetadata { @@ -119,7 +119,7 @@ declare module "effect/unstable/ai/AiError" { * * Records Anthropic error details returned when a request or response is rejected by Anthropic safety or content policy enforcement. * - * @category configuration + * @category models * @since 4.0.0 */ export interface ContentPolicyErrorMetadata { @@ -133,7 +133,7 @@ declare module "effect/unstable/ai/AiError" { * * Provides the Anthropic error type and request identifier for malformed or unsupported requests rejected before model execution. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidRequestErrorMetadata { @@ -147,7 +147,7 @@ declare module "effect/unstable/ai/AiError" { * * Preserves Anthropic request correlation data for provider-side failures that should be reported or investigated with Anthropic support. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InternalProviderErrorMetadata { @@ -161,7 +161,7 @@ declare module "effect/unstable/ai/AiError" { * * Describes Anthropic-specific context for responses that could not be decoded or interpreted as valid AI output. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidOutputErrorMetadata { @@ -175,7 +175,7 @@ declare module "effect/unstable/ai/AiError" { * * Captures Anthropic error details for structured-output failures, including request correlation data useful when diagnosing schema-related responses. * - * @category configuration + * @category models * @since 4.0.0 */ export interface StructuredOutputErrorMetadata { @@ -189,7 +189,7 @@ declare module "effect/unstable/ai/AiError" { * * Provides Anthropic error details for schemas that cannot be represented by or submitted to the Anthropic API. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnsupportedSchemaErrorMetadata { @@ -203,7 +203,7 @@ declare module "effect/unstable/ai/AiError" { * * Retains the Anthropic error type and request identifier when a provider response cannot be classified as a more specific AI error. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnknownErrorMetadata { diff --git a/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts b/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts index ab37b1c0c..e65a7c9bd 100644 --- a/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts +++ b/.context/effect/packages/ai/anthropic/src/AnthropicLanguageModel.ts @@ -20,6 +20,7 @@ import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as SchemaAST from "effect/SchemaAST" +import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { Mutable, Simplify } from "effect/Types" @@ -39,6 +40,8 @@ import type { AnthropicTool } from "./AnthropicTool.ts" import type * as Generated from "./Generated.ts" import * as InternalUtilities from "./internal/utilities.ts" +const formatIssue = SchemaIssue.makeFormatterDefault() + /** * Known Anthropic Claude model identifiers exposed by the generated Anthropic schema. * @@ -65,7 +68,7 @@ export type Model = (typeof Generated.Model)["members"][1]["Encoded"] * requests. Scoped configuration overrides defaults supplied to `model`, * `make`, or `layer`. * - * @category configuration + * @category services * @since 4.0.0 */ export class Config extends Context.Service< @@ -111,7 +114,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when translating system messages into Anthropic * request content. * - * @category request + * @category models * @since 4.0.0 */ export interface SystemMessageOptions extends ProviderOptions { @@ -131,7 +134,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when translating user messages into Anthropic * request content. * - * @category request + * @category models * @since 4.0.0 */ export interface UserMessageOptions extends ProviderOptions { @@ -151,7 +154,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when replaying assistant messages in Anthropic * conversation history. * - * @category request + * @category models * @since 4.0.0 */ export interface AssistantMessageOptions extends ProviderOptions { @@ -171,7 +174,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when converting tool results into Anthropic user * content blocks. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolMessageOptions extends ProviderOptions { @@ -190,7 +193,7 @@ declare module "effect/unstable/ai/Prompt" { * * Use when you use these options to control how text blocks are sent to Anthropic. * - * @category request + * @category models * @since 4.0.0 */ export interface TextPartOptions extends ProviderOptions { @@ -210,7 +213,7 @@ declare module "effect/unstable/ai/Prompt" { * Preserves Claude thinking metadata when reasoning content is sent back to * Anthropic in later turns. * - * @category request + * @category models * @since 4.0.0 */ export interface ReasoningPartOptions extends ProviderOptions { @@ -245,7 +248,7 @@ declare module "effect/unstable/ai/Prompt" { * Controls document metadata, citations, and prompt caching for files sent to * Anthropic. * - * @category request + * @category models * @since 4.0.0 */ export interface FilePartOptions extends ProviderOptions { @@ -283,7 +286,7 @@ declare module "effect/unstable/ai/Prompt" { * Carries Anthropic tool caller metadata, MCP metadata, and cache control for * tool use blocks. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolCallPartOptions extends ProviderOptions { @@ -313,13 +316,23 @@ declare module "effect/unstable/ai/Prompt" { * * **Details** * - * Controls Anthropic prompt caching for tool result content. + * Carries Anthropic MCP metadata and controls prompt caching for tool result + * content. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolResultPartOptions extends ProviderOptions { readonly anthropic?: { + /** + * Contains details about the MCP tool that produced the result. + */ + readonly mcp_tool?: { + /** + * The name of the MCP server + */ + readonly server: string + } | null /** * A breakpoint which marks the end of reusable content eligible for caching. */ @@ -334,7 +347,7 @@ declare module "effect/unstable/ai/Prompt" { * * Controls prompt caching for human approval requests in conversations. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolApprovalRequestPartOptions extends ProviderOptions { @@ -353,7 +366,7 @@ declare module "effect/unstable/ai/Prompt" { * * Controls prompt caching for human approval responses in conversations. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolApprovalResponsePartOptions extends ProviderOptions { @@ -375,7 +388,7 @@ declare module "effect/unstable/ai/Response" { * Includes Claude thinking metadata needed to continue reasoning-aware * conversations. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningStartPartMetadata extends ProviderMetadata { @@ -405,7 +418,7 @@ declare module "effect/unstable/ai/Response" { * * Includes the signature for streamed Claude thinking content when available. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningDeltaPartMetadata extends ProviderMetadata { @@ -428,7 +441,7 @@ declare module "effect/unstable/ai/Response" { * * Preserves Claude thinking or redacted thinking information for later turns. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningPartMetadata extends ProviderMetadata { @@ -459,7 +472,7 @@ declare module "effect/unstable/ai/Response" { * Identifies Anthropic caller details and MCP tool metadata emitted by the * provider. * - * @category response + * @category models * @since 4.0.0 */ export interface ToolCallPartMetadata extends ProviderMetadata { @@ -488,7 +501,7 @@ declare module "effect/unstable/ai/Response" { * Identifies MCP tool metadata associated with provider-executed tool * results. * - * @category response + * @category models * @since 4.0.0 */ export interface ToolResultPartMetadata extends ProviderMetadata { @@ -512,7 +525,7 @@ declare module "effect/unstable/ai/Response" { * * Records the cited document span by character position or page number. * - * @category response + * @category models * @since 4.0.0 */ export interface DocumentSourcePartMetadata extends ProviderMetadata { @@ -556,7 +569,7 @@ declare module "effect/unstable/ai/Response" { * * Records cited URL text or web-search source freshness information. * - * @category response + * @category models * @since 4.0.0 */ export interface UrlSourcePartMetadata extends ProviderMetadata { @@ -586,7 +599,7 @@ declare module "effect/unstable/ai/Response" { * Includes container state, context management information, stop details, and * token usage reported by Anthropic. * - * @category response + * @category models * @since 4.0.0 */ export interface FinishPartMetadata extends ProviderMetadata { @@ -605,7 +618,7 @@ declare module "effect/unstable/ai/Response" { * * Includes the provider request identifier when Anthropic returns one. * - * @category response + * @category models * @since 4.0.0 */ export interface ErrorPartMetadata extends ProviderMetadata { @@ -889,7 +902,7 @@ const prepareMessages = Effect.fnUntraced( : { type: "text", media_type: "text/plain", - data: typeof part.data === "string" ? part.data : Encoding.encodeBase64(part.data) + data: typeof part.data === "string" ? part.data : new TextDecoder().decode(part.data) } as const content.push({ @@ -2575,7 +2588,7 @@ const makeStreamResponse = Effect.fnUntraced( (contentBlock.providerName === "bash_code_execution" || contentBlock.providerName === "text_editor_code_execution") ) { - delta = `{"type":${contentBlock.providerName},${delta.substring(1)}}` + delta = `{"type":${JSON.stringify(contentBlock.providerName)},${delta.substring(1)}` } parts.push({ @@ -2650,7 +2663,7 @@ const makeStreamResponse = Effect.fnUntraced( } const params = contentBlock.providerExecuted === true - ? finalParams + ? Tool.unsafeSecureJsonParse(finalParams) : yield* transformToolCallParams( options.tools, contentBlock.name, @@ -2971,13 +2984,20 @@ interface ModelCapabilities { */ const getModelCapabilities = (modelId: string): ModelCapabilities => { if ( - modelId.includes("claude-sonnet-4-5") || - modelId.includes("claude-opus-4-5") || - modelId.includes("claude-haiku-4-5") || modelId.includes("claude-opus-4-6") || modelId.includes("claude-sonnet-4-6") || modelId.includes("claude-opus-4-7") || modelId.includes("claude-opus-4-8") + ) { + return { + maxOutputTokens: 128000, + supportsStructuredOutput: true, + isKnownModel: true + } + } else if ( + modelId.includes("claude-sonnet-4-5") || + modelId.includes("claude-opus-4-5") || + modelId.includes("claude-haiku-4-5") ) { return { maxOutputTokens: 64000, @@ -3098,7 +3118,7 @@ const transformToolCallParams = Effect.fnUntraced(function* (error: Sse.SseError) => AiError.AiError, + (error: Sse.SseError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "AnthropicClient", + method, + reason: new AiError.InvalidOutputError({ description: error.message }) + })) + /** @internal */ export const mapClientError = dual< (method: string) => (error: Generated.AnthropicClientError) => AiError.AiError, diff --git a/.context/effect/packages/ai/anthropic/src/internal/utilities.ts b/.context/effect/packages/ai/anthropic/src/internal/utilities.ts index 320754c4d..322b035ac 100644 --- a/.context/effect/packages/ai/anthropic/src/internal/utilities.ts +++ b/.context/effect/packages/ai/anthropic/src/internal/utilities.ts @@ -15,7 +15,7 @@ export const resolveFinishReason = ( finishReason: string, isJsonResponse: boolean = false ): Response.FinishReason => { - const reason = finishReasonMap[finishReason] + const reason = Object.hasOwn(finishReasonMap, finishReason) ? finishReasonMap[finishReason] : undefined if (Predicate.isUndefined(reason)) { return "unknown" } diff --git a/.context/effect/packages/ai/anthropic/test/AnthropicClient.test.ts b/.context/effect/packages/ai/anthropic/test/AnthropicClient.test.ts new file mode 100644 index 000000000..aa9861c81 --- /dev/null +++ b/.context/effect/packages/ai/anthropic/test/AnthropicClient.test.ts @@ -0,0 +1,127 @@ +import { AnthropicClient } from "@effect/ai-anthropic" +import { assert, describe, it } from "@effect/vitest" +import { Context, Effect, Layer, Redacted, type Schema } from "effect" +import { + Headers, + HttpClient, + type HttpClientError, + type HttpClientRequest, + HttpClientResponse +} from "effect/unstable/http" + +describe("AnthropicClient", () => { + it.effect("redacts the API key in AI error context", () => + Effect.gen(function*() { + const client = yield* AnthropicClient.AnthropicClient + + const result = yield* client.createMessage({ + payload: { + model: "claude-sonnet-4-20250514", + max_tokens: 1, + messages: [{ role: "user", content: "hello" }] + } + }).pipe( + Effect.flip, + Effect.updateService(Headers.CurrentRedactedNames, () => []) + ) + + assert.strictEqual(result.reason._tag, "InvalidRequestError") + if (result.reason._tag !== "InvalidRequestError" || result.reason.http === undefined) { + return yield* Effect.die(new Error("Expected InvalidRequestError with HTTP context")) + } + const requests = yield* MockHttpClient.requests + assert.include(requests[0]?.url, "/v1/messages") + assert.strictEqual(String(result.reason.http.request.headers["x-api-key"]), "") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 400, + body: { + type: "error", + error: { + type: "invalid_request_error", + message: "Bad request" + }, + request_id: null + } + })))) +}) + +type MockResponse = + | { + readonly _tag: "Json" + readonly body: Schema.Json + readonly status?: number | undefined + readonly headers?: Record | undefined + } + | { + readonly _tag: "Sse" + readonly events: ReadonlyArray + readonly status?: number | undefined + readonly headers?: Record | undefined + } + +class MockAnthropicResponse extends Context.Service()("MockAnthropicResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = MockHttpClient.use((client) => client.requests) +} + +const makeHttpClientContext = Effect.gen(function*() { + const capturedRequests: Array = [] + const mock = yield* MockAnthropicResponse + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + capturedRequests.push(request) + return makeResponse(request, mock.response) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + const mockHttpClient: MockHttpClient["Service"] = { + requests: Effect.sync(() => capturedRequests) + } + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, mockHttpClient) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClientContext) + +const makeTestLayer = ( + response: MockResponse, + options: AnthropicClient.Options = { apiKey: Redacted.make("sk-test-key") } +) => + AnthropicClient.layer(options).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockAnthropicResponse, { response })) + ) + +const makeResponse = ( + request: HttpClientRequest.HttpClientRequest, + response: MockResponse +): HttpClientResponse.HttpClientResponse => { + const contentType = response._tag === "Json" + ? "application/json" + : "text/event-stream" + const body = response._tag === "Json" + ? JSON.stringify(response.body) + : response.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: response.status ?? 200, + headers: { + "content-type": contentType, + ...response.headers + } + }) + ) +} diff --git a/.context/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts b/.context/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts index f5d2848b0..32ebded70 100644 --- a/.context/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts +++ b/.context/effect/packages/ai/anthropic/test/AnthropicLanguageModel.test.ts @@ -1,7 +1,14 @@ import { AnthropicClient, AnthropicLanguageModel, AnthropicTool } from "@effect/ai-anthropic" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted, Schema, Stream } from "effect" -import { AnthropicStructuredOutput, LanguageModel, Tool, Toolkit } from "effect/unstable/ai" +import { + AnthropicStructuredOutput, + LanguageModel, + Prompt, + Response as AiResponse, + Tool, + Toolkit +} from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("AnthropicLanguageModel", () => { @@ -112,6 +119,122 @@ describe("AnthropicLanguageModel", () => { assert.deepStrictEqual(toolCall.params, toolParams) })) + const codeExecutionCases = [ + { + providerName: "bash_code_execution", + toolParams: { command: "pwd" }, + expectedParams: { type: "bash_code_execution", command: "pwd" } + }, + { + providerName: "text_editor_code_execution", + toolParams: { command: "view", path: "/tmp/example.txt" }, + expectedParams: { + type: "text_editor_code_execution", + command: "view", + path: "/tmp/example.txt" + } + } + ] as const + + for (const { expectedParams, providerName, toolParams } of codeExecutionCases) { + it.effect(`emits valid JSON for streamed ${providerName} parameters`, () => + Effect.gen(function*() { + const toolkit = Toolkit.make(AnthropicTool.CodeExecution_20250522()) + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + type: "message_start", + message: { + id: "msg_test_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [], + stop_reason: null, + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 0, + service_tier: null + } + } + }, + { + type: "content_block_start", + index: 0, + content_block: { + type: "server_tool_use", + id: "srvtoolu_test_1", + name: providerName, + input: {} + } + }, + { + type: "content_block_delta", + index: 0, + delta: { + type: "input_json_delta", + partial_json: JSON.stringify(toolParams) + } + }, + { + type: "content_block_stop", + index: 0 + }, + { + type: "message_delta", + delta: { + stop_reason: "tool_use", + stop_sequence: null + }, + usage: { + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + input_tokens: null, + output_tokens: 1 + } + }, + { + type: "message_stop" + } + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "run pwd", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + const parts = globalThis.Array.from(partsChunk) + + const delta = parts.find((part) => part.type === "tool-params-delta") + assert.isDefined(delta) + if (delta?.type === "tool-params-delta") { + assert.deepStrictEqual(JSON.parse(delta.delta), expectedParams) + } + + const toolCall = parts.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type === "tool-call") { + assert.deepStrictEqual(toolCall.params, expectedParams) + } + })) + } + // `Model` is an open enum in Anthropic's spec (`anyOf: [{ type: string }, ...consts]`), and it is // $ref'd by response schemas. Responses must therefore decode for model ids that are newer than the // generated literals, and the id must survive decoding unchanged. @@ -276,6 +399,156 @@ describe("AnthropicLanguageModel", () => { assert.strictEqual(dynamicTool.description, "A dynamic tool") assert.deepStrictEqual(dynamicTool.input_schema, inputSchema) })) + + it.effect("serializes provider-executed web_search parts from the assistant message", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined = undefined + const layer = AnthropicClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, { + id: "msg_test_2", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "You're welcome" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 10, + output_tokens: 5, + service_tier: null + } + })) + }) + )) + ) + + const searchResults = [{ + type: "web_search_result", + url: "https://example.com/gold", + title: "Gold price", + encrypted_content: "encrypted", + page_age: null + }] + + const history = Prompt.fromResponseParts([ + AiResponse.makePart("tool-call", { + id: "srvtoolu_1", + name: "AnthropicWebSearch", + params: { query: "gold price today" }, + providerExecuted: true + }), + AiResponse.makePart("tool-result", { + id: "srvtoolu_1", + name: "AnthropicWebSearch", + isFailure: false, + result: searchResults, + encodedResult: searchResults, + preliminary: false, + providerExecuted: true + }), + AiResponse.makePart("text", { text: "Gold is around $4,000." }) + ]) + + const prompt = Prompt.concat( + Prompt.concat(Prompt.make("what is the gold price?"), history), + Prompt.make("thanks") + ) + + yield* LanguageModel.generateText({ + prompt, + toolkit: Toolkit.make(AnthropicTool.WebSearch_20250305({})), + disableToolCallResolution: true + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const body = yield* getRequestBody(capturedRequest) + const assistantMessage = body.messages.find((message: any) => message.role === "assistant") + assert.isDefined(assistantMessage) + + const serverToolUse = assistantMessage.content.find((block: any) => block.type === "server_tool_use") + assert.isDefined(serverToolUse) + assert.strictEqual(serverToolUse.id, "srvtoolu_1") + assert.strictEqual(serverToolUse.name, "web_search") + + const searchResult = assistantMessage.content.find((block: any) => block.type === "web_search_tool_result") + assert.isDefined(searchResult) + assert.strictEqual(searchResult.tool_use_id, "srvtoolu_1") + + const clientToolResults = body.messages.flatMap((message: any) => + Array.isArray(message.content) + ? message.content.filter((block: any) => block.type === "tool_result") + : [] + ) + assert.strictEqual(clientToolResults.length, 0) + })) + + it.effect("encodes plaintext bytes as UTF-8 text", () => + Effect.gen(function*() { + let body: any + const client = AnthropicClient.layer({ apiKey: Redacted.make("test") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + body = JSON.parse(new TextDecoder().decode((request.body as any).body)) + return HttpClientResponse.fromWeb( + request, + new Response( + JSON.stringify({ + id: "msg_1", + type: "message", + role: "assistant", + model: "claude-sonnet-4-20250514", + content: [{ type: "text", text: "ok" }], + stop_reason: "end_turn", + stop_sequence: null, + usage: { + cache_creation: null, + cache_creation_input_tokens: null, + cache_read_input_tokens: null, + inference_geo: null, + input_tokens: 1, + output_tokens: 1, + service_tier: null + } + }), + { status: 200 } + ) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([{ + role: "user", + content: [Prompt.filePart({ mediaType: "text/plain", data: new TextEncoder().encode("hello") })] + }]) + }).pipe( + Effect.provide(AnthropicLanguageModel.model("claude-sonnet-4-20250514")), + Effect.provide(client) + ) + + assert.strictEqual(body.messages[0].content[0].source.data, "hello") + })) }) describe("generateObject", () => { diff --git a/.context/effect/packages/ai/anthropic/tsconfig.json b/.context/effect/packages/ai/anthropic/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/ai/anthropic/tsconfig.json +++ b/.context/effect/packages/ai/anthropic/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/ai/anthropic/vitest.config.ts b/.context/effect/packages/ai/anthropic/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/ai/anthropic/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/ai/openai-compat/CHANGELOG.md b/.context/effect/packages/ai/openai-compat/CHANGELOG.md index 717beeb34..4666c49d3 100644 --- a/.context/effect/packages/ai/openai-compat/CHANGELOG.md +++ b/.context/effect/packages/ai/openai-compat/CHANGELOG.md @@ -1,5 +1,75 @@ # @effect/ai-openai-compat +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7126](https://github.com/Effect-TS/effect/pull/7126) [`16b94c7`](https://github.com/Effect-TS/effect/commit/16b94c702419c318e0f3515c902c39cf3871ccce) Thanks @fubhy! - Fix OpenAI response telemetry attribute types to use the emitted response namespace. + +- [#7127](https://github.com/Effect-TS/effect/pull/7127) [`b588640`](https://github.com/Effect-TS/effect/commit/b588640b4f5ee8b000acf8275364852bf79fe426) Thanks @fubhy! - Fix the OpenAI-compatible telemetry response attribute namespace. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6667](https://github.com/Effect-TS/effect/pull/6667) [`5283841`](https://github.com/Effect-TS/effect/commit/52838418db2e04db6aaed2fa01b280f2aad4032a) Thanks @tim-smart! - Surface parsed chat completion stream events that do not match the expected schema as `UnknownChatCompletionEvent`. + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6882](https://github.com/Effect-TS/effect/pull/6882) [`25a029c`](https://github.com/Effect-TS/effect/commit/25a029ccf2f6478dc2ae1fca96ceed9c394deeb3) Thanks @tim-smart! - Decode streaming and non-streaming tool call parameters with the provider-facing OpenAI schema codec. + +- [#6719](https://github.com/Effect-TS/effect/pull/6719) [`20b9660`](https://github.com/Effect-TS/effect/commit/20b9660d42ae4afc00bb4251b57657e6363c8808) Thanks @IMax153! - Group consecutive tool calls into one assistant message when using Chat Completions APIs. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6650](https://github.com/Effect-TS/effect/pull/6650) [`acd385e`](https://github.com/Effect-TS/effect/commit/acd385ebb3f9edee37ab6715607119ee9762a615) Thanks @IMax153! - Redact OpenAI organization and project headers from client errors. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/ai/openai-compat/README.md b/.context/effect/packages/ai/openai-compat/README.md new file mode 100644 index 000000000..55a3e1e25 --- /dev/null +++ b/.context/effect/packages/ai/openai-compat/README.md @@ -0,0 +1,14 @@ +# @effect/ai-openai-compat + +Connects the Effect AI modules to any OpenAI-compatible API, with support for chat completions and embeddings. + +## Installation + +```sh +npm install effect@beta @effect/ai-openai-compat@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/ai-openai-compat) diff --git a/.context/effect/packages/ai/openai-compat/docgen.json b/.context/effect/packages/ai/openai-compat/docgen.json deleted file mode 100644 index 5b265ffa1..000000000 --- a/.context/effect/packages/ai/openai-compat/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openai-compat/src/", - "exclude": ["src/internal/**"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/ai/openai-compat/package.json b/.context/effect/packages/ai/openai-compat/package.json index 089dde13b..6fec2fb2f 100644 --- a/.context/effect/packages/ai/openai-compat/package.json +++ b/.context/effect/packages/ai/openai-compat/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai-compat", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "An OpenAI compat integration for Effect", @@ -31,6 +31,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -38,7 +39,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -48,6 +52,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -55,9 +60,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiClient.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiClient.ts index c1fe575a9..f8f7207ac 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiClient.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiClient.ts @@ -11,7 +11,7 @@ import * as Array from "effect/Array" import type * as Config from "effect/Config" import * as Context from "effect/Context" import * as Effect from "effect/Effect" -import { identity, pipe } from "effect/Function" +import { identity } from "effect/Function" import * as Layer from "effect/Layer" import * as Redacted from "effect/Redacted" import * as Schema from "effect/Schema" @@ -34,7 +34,7 @@ import { OpenAiConfig } from "./OpenAiConfig.ts" * completions, streaming chat completions, and embeddings. Transport and * schema decoding failures are mapped to `AiError`. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { @@ -99,10 +99,15 @@ export type Options = { } const RedactedOpenAiHeaders = { - OpenAiOrganization: "OpenAI-Organization", - OpenAiProject: "OpenAI-Project" + OpenAiOrganization: "openai-organization", + OpenAiProject: "openai-project" } +const withRedactedHeaders = Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) +) + /** * Constructs an OpenAI-compatible client service from explicit options. * @@ -175,24 +180,27 @@ export const make = Effect.fnUntraced( [body: CreateResponse200, response: HttpClientResponse.HttpClientResponse], AiError.AiError > => - Effect.flatMap(resolveHttpClient, (client) => - pipe( - HttpClientRequest.post("/chat/completions"), - HttpClientRequest.bodyJsonUnsafe(payload), - HttpClient.filterStatusOk(client).execute, - Effect.flatMap((response) => - Effect.map(decodeResponse(response), ( - body - ): [CreateResponse200, HttpClientResponse.HttpClientResponse] => [ - body, - response - ]) - ), - Effect.catchTags({ - HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), - SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) - }) - )) + resolveHttpClient.pipe( + Effect.flatMap((client) => + HttpClientRequest.post("/chat/completions").pipe( + HttpClientRequest.bodyJsonUnsafe(payload), + HttpClient.filterStatusOk(client).execute, + Effect.flatMap((response) => + Effect.map(decodeResponse(response), ( + body + ): [CreateResponse200, HttpClientResponse.HttpClientResponse] => [ + body, + response + ]) + ), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) + }) + ) + ), + withRedactedHeaders + ) const buildResponseStream = ( response: HttpClientResponse.HttpClientResponse @@ -210,6 +218,7 @@ export const make = Effect.fnUntraced( Stream.takeUntil((event) => event === "[DONE]"), Stream.catchTags({ Retry: (error) => Stream.die(error), + SseError: (error) => Stream.fail(Errors.mapSseError(error, "createResponseStream")), HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")) }) ) as any @@ -217,40 +226,46 @@ export const make = Effect.fnUntraced( } const createResponseStream: Service["createResponseStream"] = (payload) => - Effect.flatMap(resolveHttpClient, (client) => - pipe( - HttpClientRequest.post("/chat/completions"), - HttpClientRequest.bodyJsonUnsafe({ - ...payload, - stream: true, - stream_options: { - include_usage: true - } - }), - HttpClient.filterStatusOk(client).execute, - Effect.map(buildResponseStream), - Effect.catchTag( - "HttpClientError", - (error) => Errors.mapHttpClientError(error, "createResponseStream") + resolveHttpClient.pipe( + Effect.flatMap((client) => + HttpClientRequest.post("/chat/completions").pipe( + HttpClientRequest.bodyJsonUnsafe({ + ...payload, + stream: true, + stream_options: { + include_usage: true + } + }), + HttpClient.filterStatusOk(client).execute, + Effect.map(buildResponseStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createResponseStream") + ) ) - )) + ), + withRedactedHeaders + ) const decodeEmbedding = HttpClientResponse.schemaBodyJson(CreateEmbeddingResponseSchema) const createEmbedding = ( payload: CreateEmbeddingRequestJson ): Effect.Effect => - Effect.flatMap(resolveHttpClient, (client) => - pipe( - HttpClientRequest.post("/embeddings"), - HttpClientRequest.bodyJsonUnsafe(payload), - HttpClient.filterStatusOk(client).execute, - Effect.flatMap(decodeEmbedding), - Effect.catchTags({ - HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), - SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) - }) - )) + resolveHttpClient.pipe( + Effect.flatMap((client) => + HttpClientRequest.post("/embeddings").pipe( + HttpClientRequest.bodyJsonUnsafe(payload), + HttpClient.filterStatusOk(client).execute, + Effect.flatMap(decodeEmbedding), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) + }) + ) + ), + withRedactedHeaders + ) return OpenAiClient.of({ client: httpClient, @@ -259,10 +274,7 @@ export const make = Effect.fnUntraced( createEmbedding }) }, - Effect.updateService( - Headers.CurrentRedactedNames, - Array.appendAll(Object.values(RedactedOpenAiHeaders)) - ) + withRedactedHeaders ) /** @@ -340,7 +352,7 @@ type JsonObject = { readonly [x: string]: Schema.Json } /** * Optional response fields that can be requested with the `include` parameter. * - * @category response + * @category models * @since 4.0.0 */ export type IncludeEnum = @@ -379,7 +391,7 @@ type InputFileContent = { /** * Content blocks accepted in input messages. * - * @category request + * @category models * @since 4.0.0 */ export type InputContent = InputTextContent | InputImageContent | InputFileContent @@ -387,7 +399,7 @@ export type InputContent = InputTextContent | InputImageContent | InputFileConte /** * Text content block used for model-provided reasoning summaries. * - * @category response + * @category models * @since 4.0.0 */ export type SummaryTextContent = { @@ -449,7 +461,7 @@ type FilePathAnnotation = { /** * Citation and file-path annotations attached to output text content. * - * @category response + * @category models * @since 4.0.0 */ export type Annotation = @@ -488,7 +500,7 @@ type OutputMessage = { * Reasoning output item containing encrypted reasoning content, summaries, and * optional reasoning text. * - * @category response + * @category models * @since 4.0.0 */ export type ReasoningItem = { @@ -545,7 +557,7 @@ type ItemReference = { * Supports input messages, output messages, tool calls, tool outputs, reasoning * items, custom tool interactions, and item references. * - * @category request + * @category models * @since 4.0.0 */ export type InputItem = @@ -586,7 +598,7 @@ type CustomToolParam = { /** * Tool definitions that can be supplied to a Responses-style request. * - * @category request + * @category models * @since 4.0.0 */ export type Tool = @@ -637,7 +649,7 @@ export type TextResponseFormatConfiguration = * Request options for creating a Responses-style response with an * OpenAI-compatible provider. * - * @category request + * @category models * @since 4.0.0 */ export type CreateResponse = { @@ -677,7 +689,7 @@ export type CreateResponse = { /** * Token accounting reported on Responses-style response objects. * - * @category response + * @category models * @since 4.0.0 */ export type ResponseUsage = { @@ -698,7 +710,7 @@ type OutputItem = * Responses-style response object returned by compatible providers or embedded * in response stream lifecycle events. * - * @category response + * @category models * @since 4.0.0 */ export type Response = { @@ -855,7 +867,7 @@ export type ResponseStreamEvent = * string. The `index` field identifies the input item that produced this * embedding. * - * @category response + * @category models * @since 4.0.0 */ export type Embedding = { @@ -867,7 +879,7 @@ export type Embedding = { /** * Request payload for the embeddings endpoint. * - * @category request + * @category models * @since 4.0.0 */ export type CreateEmbeddingRequest = { @@ -881,7 +893,7 @@ export type CreateEmbeddingRequest = { /** * Successful response payload returned by the embeddings endpoint. * - * @category response + * @category models * @since 4.0.0 */ export type CreateEmbeddingResponse = { @@ -897,21 +909,21 @@ export type CreateEmbeddingResponse = { /** * JSON request body accepted by the embeddings endpoint. * - * @category request + * @category models * @since 4.0.0 */ export type CreateEmbeddingRequestJson = CreateEmbeddingRequest /** * Decoded successful embeddings response body. * - * @category response + * @category models * @since 4.0.0 */ export type CreateEmbedding200 = CreateEmbeddingResponse /** * Structured content parts accepted in chat completion messages. * - * @category request + * @category models * @since 4.0.0 */ export type ChatCompletionContentPart = @@ -929,7 +941,7 @@ export type ChatCompletionContentPart = /** * Tool call data attached to an assistant chat completion message. * - * @category request + * @category models * @since 4.0.0 */ export type ChatCompletionRequestToolCall = { @@ -943,7 +955,7 @@ export type ChatCompletionRequestToolCall = { /** * Message shapes accepted by the chat completions endpoint. * - * @category request + * @category models * @since 4.0.0 */ export type ChatCompletionRequestMessage = @@ -960,7 +972,7 @@ export type ChatCompletionRequestMessage = /** * Function tool definition accepted by the chat completions endpoint. * - * @category request + * @category models * @since 4.0.0 */ export type ChatCompletionTool = { @@ -1010,7 +1022,7 @@ export type ChatCompletionResponseFormat = /** * Request payload for the OpenAI-compatible chat completions endpoint. * - * @category request + * @category models * @since 4.0.0 */ export type ChatCompletionRequest = { @@ -1036,14 +1048,14 @@ export type ChatCompletionRequest = { /** * JSON request body used by this client when creating a chat completion response. * - * @category request + * @category models * @since 4.0.0 */ export type CreateResponseRequestJson = ChatCompletionRequest /** * Decoded successful chat completion response body returned by `createResponse`. * - * @category response + * @category models * @since 4.0.0 */ export type CreateResponse200 = ChatCompletionResponse @@ -1056,8 +1068,8 @@ export type CreateResponse200 = ChatCompletionResponse export type CreateResponse200Sse = ChatCompletionStreamEvent const EmbeddingSchema = Schema.Struct({ - embedding: Schema.Union([Schema.Array(Schema.Number), Schema.String]), - index: Schema.Number, + embedding: Schema.Union([Schema.Array(Schema.Finite), Schema.String]), + index: Schema.Int, object: Schema.optionalKey(Schema.String) }) @@ -1066,8 +1078,8 @@ const CreateEmbeddingResponseSchema = Schema.Struct({ model: Schema.String, object: Schema.optionalKey(Schema.Literal("list")), usage: Schema.optionalKey(Schema.Struct({ - prompt_tokens: Schema.Number, - total_tokens: Schema.Number + prompt_tokens: Schema.Int, + total_tokens: Schema.Int })) }) @@ -1086,14 +1098,14 @@ const ChatCompletionToolFunctionDelta = Schema.Struct({ const ChatCompletionToolCall = Schema.Struct({ id: Schema.optionalKey(Schema.String), - index: Schema.optionalKey(Schema.Number), + index: Schema.optionalKey(Schema.Int), type: Schema.optionalKey(Schema.String), function: Schema.optionalKey(ChatCompletionToolFunction) }) const ChatCompletionToolCallDelta = Schema.Struct({ id: Schema.optionalKey(Schema.String), - index: Schema.optionalKey(Schema.Number), + index: Schema.optionalKey(Schema.Int), type: Schema.optionalKey(Schema.String), function: Schema.optionalKey(ChatCompletionToolFunctionDelta) }) @@ -1115,16 +1127,16 @@ const ChatCompletionDelta = Schema.Struct({ }) const ChatCompletionChoice = Schema.Struct({ - index: Schema.Number, + index: Schema.Int, finish_reason: Schema.optionalKey(Schema.NullOr(Schema.String)), message: Schema.optionalKey(ChatCompletionMessage), delta: Schema.optionalKey(ChatCompletionDelta) }) const ChatCompletionUsage = Schema.Struct({ - prompt_tokens: Schema.Number, - completion_tokens: Schema.Number, - total_tokens: Schema.Number, + prompt_tokens: Schema.Int, + completion_tokens: Schema.Int, + total_tokens: Schema.Int, prompt_tokens_details: Schema.optionalKey(Schema.Any), completion_tokens_details: Schema.optionalKey(Schema.Any) }) @@ -1132,7 +1144,7 @@ const ChatCompletionUsage = Schema.Struct({ const ChatCompletionResponse = Schema.Struct({ id: Schema.String, model: Schema.String, - created: Schema.Number, + created: Schema.Int, choices: Schema.Array(ChatCompletionChoice), usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)), service_tier: Schema.optionalKey(Schema.String) @@ -1141,7 +1153,7 @@ const ChatCompletionResponse = Schema.Struct({ const ChatCompletionChunk = Schema.Struct({ id: Schema.String, model: Schema.String, - created: Schema.Number, + created: Schema.Int, choices: Schema.Array(ChatCompletionChoice), usage: Schema.optionalKey(Schema.NullOr(ChatCompletionUsage)), service_tier: Schema.optionalKey(Schema.String) @@ -1150,35 +1162,35 @@ const ChatCompletionChunk = Schema.Struct({ /** * Decoded tool-call object from a chat completion response or streaming chunk. * - * @category response + * @category models * @since 4.0.0 */ export type ChatCompletionToolCall = typeof ChatCompletionToolCall.Type /** * Decoded message object from a non-streaming chat completion choice. * - * @category response + * @category models * @since 4.0.0 */ export type ChatCompletionMessage = typeof ChatCompletionMessage.Type /** * Decoded choice object returned by chat completion responses and chunks. * - * @category response + * @category models * @since 4.0.0 */ export type ChatCompletionChoice = typeof ChatCompletionChoice.Type /** * Decoded token usage summary returned by chat completions. * - * @category response + * @category models * @since 4.0.0 */ export type ChatCompletionUsage = typeof ChatCompletionUsage.Type /** * Decoded successful response from the chat completions endpoint. * - * @category response + * @category models * @since 4.0.0 */ export type ChatCompletionResponse = typeof ChatCompletionResponse.Type @@ -1190,13 +1202,23 @@ export type ChatCompletionResponse = typeof ChatCompletionResponse.Type */ export type ChatCompletionChunk = typeof ChatCompletionChunk.Type /** - * Streaming chat completion event, including decoded chunks and the `[DONE]` - * sentinel. + * A parsed chat completion event that does not match the expected chunk schema. * * @category streaming * @since 4.0.0 */ -export type ChatCompletionStreamEvent = ChatCompletionChunk | "[DONE]" +export interface UnknownChatCompletionEvent { + readonly _tag: "UnknownChatCompletionEvent" + readonly data: unknown +} +/** + * Streaming chat completion event, including decoded chunks, unknown parsed + * events, and the `[DONE]` sentinel. + * + * @category streaming + * @since 4.0.0 + */ +export type ChatCompletionStreamEvent = ChatCompletionChunk | UnknownChatCompletionEvent | "[DONE]" const parseJson = (value: string): unknown => { try { @@ -1215,7 +1237,11 @@ const decodeChatCompletionSseData = ( return data } const parsed = parseJson(data) - return isChatCompletionChunk(parsed) - ? parsed - : undefined + if (parsed === undefined) { + return undefined + } + return isChatCompletionChunk(parsed) ? parsed : { + _tag: "UnknownChatCompletionEvent", + data: parsed + } } diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiConfig.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiConfig.ts index 452144aee..829196640 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiConfig.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiConfig.ts @@ -50,7 +50,7 @@ export declare namespace OpenAiConfig { * Configuration consumed by OpenAI-compatible clients when they build or * resolve the underlying HTTP client. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts index ca6de8da9..bfe251c4f 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiEmbeddingModel.ts @@ -46,7 +46,7 @@ type ModelConfig = Omit & { readonly [x: string]: unknow * * @see {@link withConfigOverride} for scoping embedding request overrides * - * @category context + * @category services * @since 4.0.0 */ export class Config extends Context.Service< diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts index 0803e5386..5418bd17f 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiLanguageModel.ts @@ -16,9 +16,11 @@ import { dual } from "effect/Function" import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Predicate from "effect/Predicate" +import * as Rec from "effect/Record" import * as Redactable from "effect/Redactable" -import type * as Schema from "effect/Schema" +import * as Schema from "effect/Schema" import * as AST from "effect/SchemaAST" +import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { DeepMutable, Simplify } from "effect/Types" @@ -35,6 +37,7 @@ import * as InternalUtilities from "./internal/utilities.ts" import { type Annotation, type ChatCompletionContentPart, + type ChatCompletionRequestToolCall, type CreateResponse, type CreateResponse200, type CreateResponse200Sse, @@ -47,10 +50,13 @@ import { type ReasoningItem, type SummaryTextContent, type TextResponseFormatConfiguration, - type Tool as OpenAiClientTool + type Tool as OpenAiClientTool, + type UnknownChatCompletionEvent } from "./OpenAiClient.ts" import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" +const formatIssue = SchemaIssue.makeFormatterDefault() + /** * Image detail level for vision requests. */ @@ -108,7 +114,7 @@ type ModelConfig = Omit & { readonly [x: string]: unknow * * @see {@link withConfigOverride} for scoping language model request overrides * - * @category context + * @category services * @since 4.0.0 */ export class Config extends Context.Service< @@ -124,7 +130,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-compatible options for file prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface FilePartOptions extends ProviderOptions { @@ -142,7 +148,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-compatible options for reasoning prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ReasoningPartOptions extends ProviderOptions { @@ -166,7 +172,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-compatible options for assistant tool-call prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolCallPartOptions extends ProviderOptions { @@ -188,7 +194,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-compatible options for tool-result prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolResultPartOptions extends ProviderOptions { @@ -210,7 +216,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-compatible options for text prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface TextPartOptions extends ProviderOptions { @@ -238,7 +244,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to a complete text response part. * - * @category response + * @category models * @since 4.0.0 */ export interface TextPartMetadata extends ProviderMetadata { @@ -270,7 +276,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata emitted when a streamed text part starts. * - * @category response + * @category models * @since 4.0.0 */ export interface TextStartPartMetadata extends ProviderMetadata { @@ -288,7 +294,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata emitted when a streamed text part ends. * - * @category response + * @category models * @since 4.0.0 */ export interface TextEndPartMetadata extends ProviderMetadata { @@ -310,7 +316,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to a complete reasoning response part. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningPartMetadata extends ProviderMetadata { @@ -332,7 +338,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata emitted when a streamed reasoning part starts. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningStartPartMetadata extends ProviderMetadata { @@ -354,7 +360,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata emitted for a streamed reasoning delta. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningDeltaPartMetadata extends ProviderMetadata { @@ -372,7 +378,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata emitted when a streamed reasoning part ends. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningEndPartMetadata extends ProviderMetadata { @@ -394,7 +400,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to tool-call response parts. * - * @category response + * @category models * @since 4.0.0 */ export interface ToolCallPartMetadata extends ProviderMetadata { @@ -412,7 +418,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to document source citations. * - * @category response + * @category models * @since 4.0.0 */ export interface DocumentSourcePartMetadata extends ProviderMetadata { @@ -468,7 +474,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to URL source citations. * - * @category response + * @category models * @since 4.0.0 */ export interface UrlSourcePartMetadata extends ProviderMetadata { @@ -494,7 +500,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI-compatible metadata attached to finish response parts. * - * @category response + * @category models * @since 4.0.0 */ export interface FinishPartMetadata extends ProviderMetadata { @@ -628,6 +634,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig const [rawResponse, response] = yield* client.createResponse(request) annotateResponse(options.span, rawResponse) return yield* makeResponse({ + options, rawResponse, response, toolNameMapper @@ -642,6 +649,7 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig annotateRequest(options.span, request) const [response, stream] = yield* client.createResponseStream(request) return yield* makeStreamResponse({ + options, stream, response, toolNameMapper @@ -828,7 +836,7 @@ const prepareMessages = Effect.fnUntraced( } case "assistant": { - const reasoningMessages: Record> = {} + const reasoningMessages: Record> = Object.create(null) for (const part of message.content) { switch (part.type) { @@ -1026,6 +1034,11 @@ const buildHttpResponseDetails = ( type ResponseStreamEvent = CreateResponse200Sse +const isUnknownChatCompletionEvent = ( + event: ResponseStreamEvent +): event is UnknownChatCompletionEvent => + typeof event !== "string" && "_tag" in event && event._tag === "UnknownChatCompletionEvent" + type ActiveToolCall = { readonly id: string name: string @@ -1034,10 +1047,12 @@ type ActiveToolCall = { const makeResponse = Effect.fnUntraced( function*>({ + options, rawResponse, response, toolNameMapper }: { + readonly options: LanguageModel.ProviderOptions readonly rawResponse: CreateResponse200 readonly response: HttpClientResponse.HttpClientResponse readonly toolNameMapper: Tool.NameMapper @@ -1076,9 +1091,9 @@ const makeResponse = Effect.fnUntraced( for (const [index, toolCall] of message.tool_calls.entries()) { const toolId = toolCall.id ?? `${rawResponse.id}_tool_${index}` const toolName = toolNameMapper.getCustomName(toolCall.function?.name ?? "unknown_tool") - const toolParams = toolCall.function?.arguments ?? "{}" - const params = yield* Effect.try({ - try: () => Tool.unsafeSecureJsonParse(toolParams), + const toolParamsJson = toolCall.function?.arguments ?? "{}" + const toolParams = yield* Effect.try({ + try: () => Tool.unsafeSecureJsonParse(toolParamsJson), catch: (cause) => AiError.make({ module: "OpenAiLanguageModel", @@ -1090,6 +1105,7 @@ const makeResponse = Effect.fnUntraced( }) }) }) + const params = yield* transformToolCallParams(options.tools, toolName, toolParams) hasToolCalls = true parts.push({ type: "tool-call", @@ -1122,10 +1138,12 @@ const makeResponse = Effect.fnUntraced( const makeStreamResponse = Effect.fnUntraced( function*>({ + options, stream, response, toolNameMapper }: { + readonly options: LanguageModel.ProviderOptions readonly stream: Stream.Stream readonly response: HttpClientResponse.HttpClientResponse readonly toolNameMapper: Tool.NameMapper @@ -1167,7 +1185,7 @@ const makeStreamResponse = Effect.fnUntraced( for (const toolCall of Object.values(activeToolCalls)) { const toolParams = toolCall.arguments.length > 0 ? toolCall.arguments : "{}" - const params = yield* Effect.try({ + const parsedParams = yield* Effect.try({ try: () => Tool.unsafeSecureJsonParse(toolParams), catch: (cause) => AiError.make({ @@ -1180,6 +1198,7 @@ const makeStreamResponse = Effect.fnUntraced( }) }) }) + const params = yield* transformToolCallParams(options.tools, toolCall.name, parsedParams) parts.push({ type: "tool-params-end", id: toolCall.id }) parts.push({ type: "tool-call", @@ -1204,6 +1223,12 @@ const makeStreamResponse = Effect.fnUntraced( return parts } + // Keep unknown events available to direct client consumers; this layer + // cannot translate provider-specific data into portable stream parts. + if (isUnknownChatCompletionEvent(event)) { + return parts + } + if (event.service_tier !== undefined) { serviceTier = event.service_tier } @@ -1391,6 +1416,12 @@ const unsupportedSchemaError = (error: unknown, method: string): AiError.AiError }) }) +const tryCodecTransform = (schema: S, method: string) => + Effect.try({ + try: () => toCodecOpenAI(schema), + catch: (error) => unsupportedSchemaError(error, method) + }) + const tryJsonSchema = (schema: S, method: string) => Effect.try({ try: () => Tool.getJsonSchemaFromSchema(schema, { transformer: toCodecOpenAI }), @@ -1403,6 +1434,42 @@ const tryToolJsonSchema = (tool: T, method: string) => catch: (error) => unsupportedSchemaError(error, method) }) +const transformToolCallParams = Effect.fnUntraced(function*>( + tools: Tools, + toolName: string, + toolParams: unknown +): Effect.fn.Return { + const tool = tools.find((tool) => tool.name === toolName) + + if (Predicate.isUndefined(tool)) { + return yield* AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolNotFoundError({ + toolName, + availableTools: tools.map((tool) => tool.name) + }) + }) + } + + const { codec } = yield* tryCodecTransform(tool.parametersSchema, "makeResponse") + const transform = Schema.decodeEffect(codec) + + return yield* ( + transform(toolParams) as Effect.Effect + ).pipe(Effect.mapError((error) => + AiError.make({ + module: "OpenAiLanguageModel", + method: "makeResponse", + reason: new AiError.ToolParameterValidationError({ + toolName, + toolParams, + description: formatIssue(error.issue) + }) + }) + )) +}) + const prepareTools = Effect.fnUntraced(function*>({ config, options, @@ -1541,7 +1608,7 @@ const extractCustomRequestProperties = (payload: CreateResponse): Record = {} for (const [key, value] of Object.entries(payload)) { if (!createResponseKnownProperties.has(key)) { - customProperties[key] = value + Rec.assignProperty(customProperties, key, value) } } return customProperties @@ -1653,7 +1720,24 @@ const toChatMessages = ( const messages: Array = [] for (const item of input) { - messages.push(...toChatMessagesFromItem(item)) + if (Predicate.hasProperty(item, "type") && item.type === "function_call") { + const previous = messages.at(-1) + const toolCall = toChatToolCall(item) + if (previous?.role === "assistant" && previous.tool_calls !== undefined) { + messages[messages.length - 1] = { + ...previous, + tool_calls: [...previous.tool_calls, toolCall] + } + } else { + messages.push({ + role: "assistant", + content: null, + tool_calls: [toolCall] + }) + } + } else { + messages.push(...toChatMessagesFromItem(item)) + } } return messages @@ -1681,14 +1765,7 @@ const toChatMessagesFromItem = ( return [{ role: "assistant", content: null, - tool_calls: [{ - id: item.call_id, - type: "function", - function: { - name: item.name, - arguments: item.arguments - } - }] + tool_calls: [toChatToolCall(item)] }] } @@ -1706,6 +1783,17 @@ const toChatMessagesFromItem = ( } } +const toChatToolCall = ( + item: Extract +): ChatCompletionRequestToolCall => ({ + id: item.call_id, + type: "function", + function: { + name: item.name, + arguments: item.arguments + } +}) + const toAssistantChatMessageContent = ( content: ReadonlyArray<{ readonly type: string diff --git a/.context/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts b/.context/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts index 349a139a0..ab4d10147 100644 --- a/.context/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts +++ b/.context/effect/packages/ai/openai-compat/src/OpenAiTelemetry.ts @@ -28,7 +28,7 @@ import * as Telemetry from "effect/unstable/ai/Telemetry" export type OpenAiTelemetryAttributes = Simplify< & Telemetry.GenAITelemetryAttributes & Telemetry.AttributesWithPrefix - & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix > /** diff --git a/.context/effect/packages/ai/openai-compat/src/internal/errors.ts b/.context/effect/packages/ai/openai-compat/src/internal/errors.ts index 2636d003a..67e42868b 100644 --- a/.context/effect/packages/ai/openai-compat/src/internal/errors.ts +++ b/.context/effect/packages/ai/openai-compat/src/internal/errors.ts @@ -10,6 +10,7 @@ import * as SchemaTransformation from "effect/SchemaTransformation" import * as String from "effect/String" import * as AiError from "effect/unstable/ai/AiError" import type * as Response from "effect/unstable/ai/Response" +import type * as Sse from "effect/unstable/encoding/Sse" import type * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" @@ -22,7 +23,7 @@ export const OpenAiErrorBody = Schema.Struct({ type: Schema.optional(Schema.NullOr(Schema.String)), status: Schema.optional(Schema.NullOr(Schema.String)), param: Schema.optional(Schema.NullOr(Schema.String)), - code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Number]))) + code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Finite]))) }) }) const OpenAiErrorBodyJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Union([ @@ -49,6 +50,17 @@ export const mapSchemaError = dual< reason: AiError.InvalidOutputError.fromSchemaError(error) })) +/** @internal */ +export const mapSseError = dual< + (method: string) => (error: Sse.SseError) => AiError.AiError, + (error: Sse.SseError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ description: error.message }) + })) + /** @internal */ export const mapHttpClientError = dual< (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, diff --git a/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts b/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts index a67365d98..75daf6b7d 100644 --- a/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts +++ b/.context/effect/packages/ai/openai-compat/src/internal/utilities.ts @@ -16,7 +16,7 @@ export const resolveFinishReason = ( if (finishReason == null) { return hasToolCalls ? "tool-calls" : "stop" } - const reason = finishReasonMap[finishReason] + const reason = Object.hasOwn(finishReasonMap, finishReason) ? finishReasonMap[finishReason] : undefined if (reason == null) { return hasToolCalls ? "tool-calls" : "unknown" } diff --git a/.context/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts b/.context/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts index 2d89ffb26..423ce3b53 100644 --- a/.context/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts +++ b/.context/effect/packages/ai/openai-compat/test/OpenAiClient.test.ts @@ -1,65 +1,42 @@ -import * as OpenAiClient from "@effect/ai-openai-compat/OpenAiClient" +import { OpenAiClient } from "@effect/ai-openai-compat" import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer, Redacted, Stream } from "effect" -import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { Context, Effect, Layer, Redacted, type Schema, Stream } from "effect" +import { + Headers, + HttpClient, + type HttpClientError, + type HttpClientRequest, + HttpClientResponse +} from "effect/unstable/http" describe("OpenAiClient", () => { describe("request behavior", () => { it.effect("sets auth and OpenAI headers on /chat/completions requests", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key"), - apiUrl: "https://compat.example.test/v1", - organizationId: Redacted.make("org_123"), - projectId: Redacted.make("proj_456") - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) - }) - )) - ) + const client = yield* OpenAiClient.OpenAiClient yield* client.createResponse({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }] }) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } + const requests = yield* MockHttpClient.requests + const request = requests[0] + const body = yield* getRequestBody(request) - assert.isTrue(capturedRequest.url.endsWith("/chat/completions")) - assert.isTrue(capturedRequest.url.startsWith("https://compat.example.test/v1")) - assert.strictEqual(capturedRequest.headers["authorization"], "Bearer sk-test-key") - assert.strictEqual(capturedRequest.headers["openai-organization"], "org_123") - assert.strictEqual(capturedRequest.headers["openai-project"], "proj_456") + assert.isTrue(request.url.endsWith("/chat/completions")) + assert.isTrue(request.url.startsWith("https://compat.example.test/v1")) + assert.strictEqual(request.headers["authorization"], "Bearer sk-test-key") + assert.strictEqual(request.headers["openai-organization"], "org_123") + assert.strictEqual(request.headers["openai-project"], "proj_456") - const body = yield* getRequestBody(capturedRequest) assert.strictEqual(body.messages[0]?.role, "user") assert.strictEqual(body.messages[0]?.content, "hello") - })) + }).pipe(Effect.provide(makeTestLayer()))) it.effect("passes custom chat-completions request properties through", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) - }) - )) - ) + const client = yield* OpenAiClient.OpenAiClient yield* client.createResponse({ model: "gpt-4o-mini", @@ -69,161 +46,188 @@ describe("OpenAiClient", () => { } }) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } - - const body = yield* getRequestBody(capturedRequest) + const requests = yield* MockHttpClient.requests + const request = requests[0] + const body = yield* getRequestBody(request) assert.deepStrictEqual(body.provider_feature, { enabled: true }) - })) + }).pipe(Effect.provide(makeTestLayer()))) it.effect("uses /embeddings path and decodes permissive embedding payloads", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key"), - apiUrl: "https://compat.example.test/v1" - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, 200, { - data: [{ - embedding: "YmFzZTY0LWRhdGE=", - index: 0, - object: "embedding", - vendor_payload: { future_field: true } - }], - model: "my-custom-embedding-model", - object: "list", - usage: { - prompt_tokens: 5, - total_tokens: 5 - }, - unknown_top_level: true - })) - }) - )) - ) + const client = yield* OpenAiClient.OpenAiClient const embedding = yield* client.createEmbedding({ model: "my-custom-embedding-model", input: "embed this" }) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } + const requests = yield* MockHttpClient.requests + const request = requests[0] - assert.isTrue(capturedRequest.url.endsWith("/embeddings")) + assert.isTrue(request.url.endsWith("/embeddings")) assert.strictEqual(embedding.model, "my-custom-embedding-model") assert.strictEqual(embedding.data[0]?.index, 0) assert.strictEqual(typeof embedding.data[0]?.embedding, "string") - })) + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + body: { + data: [{ + embedding: "YmFzZTY0LWRhdGE=", + index: 0, + object: "embedding", + vendor_payload: { future_field: true } + }], + model: "my-custom-embedding-model", + object: "list", + usage: { + prompt_tokens: 5, + total_tokens: 5 + }, + unknown_top_level: true + } + })))) it.effect("sets stream=true for createResponseStream and returns chat chunks", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const client = yield* OpenAiClient.OpenAiClient - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") + const events = yield* client.createResponseStream({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(sseResponse(request, [ - { - id: "chatcmpl_test_1", - object: "chat.completion.chunk", - model: "gpt-4o-mini", - created: 1, - future_provider_field: { accepted: true }, - choices: [{ - index: 0, - delta: { content: "Hello" }, - finish_reason: null - }] - }, - { - id: "chatcmpl_test_1", - object: "chat.completion.chunk", - model: "gpt-4o-mini", - created: 1, - usage: { - prompt_tokens: 4, - completion_tokens: 2, - total_tokens: 6, - prompt_tokens_details: { cached_tokens: 1 }, - completion_tokens_details: { reasoning_tokens: 1 } - }, - choices: [{ - index: 0, - delta: {}, - finish_reason: "stop" - }] - }, - "[DONE]" - ])) - }) - )) + Effect.flatMap(([_, stream]) => Stream.runCollect(stream)) ) - const eventsChunk = yield* client.createResponseStream({ + const requests = yield* MockHttpClient.requests + const request = requests[0] + const body = yield* getRequestBody(request) + assert.strictEqual(body.stream, true) + assert.strictEqual(body.stream_options.include_usage, true) + assert.isTrue(request.url.endsWith("/chat/completions")) + + assert.propertyVal(events[0], "id", "chatcmpl_test_1") + assert.propertyVal(events[1], "id", "chatcmpl_test_1") + assert.strictEqual(events[2], "[DONE]") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Sse", + events: [ + { + id: "chatcmpl_test_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + future_provider_field: { accepted: true }, + choices: [{ + index: 0, + delta: { content: "Hello" }, + finish_reason: null + }] + }, + { + id: "chatcmpl_test_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + usage: { + prompt_tokens: 4, + completion_tokens: 2, + total_tokens: 6, + prompt_tokens_details: { cached_tokens: 1 }, + completion_tokens_details: { reasoning_tokens: 1 } + }, + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop" + }] + }, + "[DONE]" + ] + })))) + + it.effect("surfaces schema-mismatched chat chunks and continues streaming", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + + const events = yield* client.createResponseStream({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }] }).pipe( Effect.flatMap(([_, stream]) => Stream.runCollect(stream)) ) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } - - const body = yield* getRequestBody(capturedRequest) - assert.strictEqual(body.stream, true) - assert.strictEqual(body.stream_options.include_usage, true) - assert.isTrue(capturedRequest.url.endsWith("/chat/completions")) - - const events = globalThis.Array.from(eventsChunk) - const firstEvent = events[0] - const secondEvent = events[1] - assert.isTrue(typeof firstEvent === "object") - assert.isTrue(typeof secondEvent === "object") - if ( - typeof firstEvent !== "object" || firstEvent === null || typeof secondEvent !== "object" || - secondEvent === null - ) { - return - } - assert.strictEqual(firstEvent.id, "chatcmpl_test_1") - assert.strictEqual(secondEvent.id, "chatcmpl_test_1") + assert.deepStrictEqual(events[0], { + _tag: "UnknownChatCompletionEvent", + data: { + type: "provider.chat.completion.delta", + provider_payload: { content: "provider-specific" } + } + }) + assert.propertyVal(events[1], "id", "chatcmpl_test_2") assert.strictEqual(events[2], "[DONE]") - })) + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Sse", + events: [ + { + type: "provider.chat.completion.delta", + provider_payload: { content: "provider-specific" } + }, + { + id: "chatcmpl_test_2", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { content: "Hello" }, + finish_reason: null + }] + }, + "[DONE]" + ] + })))) - it.effect("passes chat-completions tool_choice payload through unchanged", () => + it.effect("drops invalid JSON and continues streaming", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + const client = yield* OpenAiClient.OpenAiClient - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") + const events = yield* client.createResponseStream({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) - }) - )) + Effect.flatMap(([_, stream]) => Stream.runCollect(stream)) ) + assert.strictEqual(events.length, 2) + assert.propertyVal(events[0], "id", "chatcmpl_test_3") + assert.strictEqual(events[1], "[DONE]") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "RawSse", + body: [ + "data: {invalid-json\n\n", + `data: ${ + JSON.stringify({ + id: "chatcmpl_test_3", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { content: "Hello" }, + finish_reason: null + }] + }) + }\n\n`, + "data: [DONE]\n\n" + ].join("") + })))) + + it.effect("passes chat-completions tool_choice payload through unchanged", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }], @@ -249,30 +253,19 @@ describe("OpenAiClient", () => { }] }) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } + const requests = yield* MockHttpClient.requests + const request = requests[0] + const body = yield* getRequestBody(request) - const body = yield* getRequestBody(capturedRequest) - assert.deepStrictEqual(body.tool_choice, { type: "function", function: { name: "TestTool" } }) - })) + assert.deepStrictEqual(body.tool_choice, { + type: "function", + function: { name: "TestTool" } + }) + }).pipe(Effect.provide(makeTestLayer()))) it.effect("accepts assistant tool-call and tool result chat history", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => { - capturedRequest = request - return Effect.succeed(jsonResponse(request, 200, makeChatCompletion())) - }) - )) - ) + const client = yield* OpenAiClient.OpenAiClient yield* client.createResponse({ model: "gpt-4o-mini", @@ -303,12 +296,10 @@ describe("OpenAiClient", () => { ] }) - assert.isDefined(capturedRequest) - if (capturedRequest === undefined) { - return - } + const requests = yield* MockHttpClient.requests + const request = requests[0] + const body = yield* getRequestBody(request) - const body = yield* getRequestBody(capturedRequest) const assistantMessages = body.messages.filter((message: any) => message.role === "assistant") const patchMessage = assistantMessages.find((message: any) => message.tool_calls?.[0]?.function?.name === "apply_patch" @@ -329,28 +320,41 @@ describe("OpenAiClient", () => { const patchOutput = toolMessages.find((message: any) => message.tool_call_id === "patch_call_1") assert.isDefined(patchOutput) assert.strictEqual(patchOutput.content, "deleted") - })) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("redacts OpenAI-specific headers in AI error context", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + + const result = yield* client.createResponse({ + model: "gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }).pipe(Effect.flip) + + const headers = result.reason._tag === "InvalidRequestError" + ? result.reason.http?.request.headers ?? Headers.empty + : Headers.empty + + assert.strictEqual(String(headers["authorization"]), "") + assert.strictEqual(String(headers["openai-organization"]), "") + assert.strictEqual(String(headers["openai-project"]), "") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 400, + body: { + error: { + message: "Bad request", + type: "invalid_request_error", + code: null + } + } + })))) }) describe("error mapping", () => { it.effect("maps 400 responses to InvalidRequestError", () => Effect.gen(function*() { - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => - Effect.succeed(jsonResponse(request, 400, { - error: { - message: "Bad request", - type: "invalid_request_error", - code: null - } - })) - ) - )) - ) + const client = yield* OpenAiClient.OpenAiClient const error = yield* client.createResponse({ model: "gpt-4o-mini", @@ -360,26 +364,21 @@ describe("OpenAiClient", () => { assert.strictEqual(error._tag, "AiError") assert.strictEqual(error.method, "createResponse") assert.strictEqual(error.reason._tag, "InvalidRequestError") - })) + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 400, + body: { + error: { + message: "Bad request", + type: "invalid_request_error", + code: null + } + } + })))) it.effect("maps insufficient quota errors to QuotaExhaustedError", () => Effect.gen(function*() { - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-key") - }).pipe( - Effect.provide(Layer.succeed( - HttpClient.HttpClient, - makeHttpClient((request) => - Effect.succeed(jsonResponse(request, 429, { - error: { - message: "You exceeded your current quota", - type: "insufficient_quota", - code: "insufficient_quota" - } - })) - ) - )) - ) + const client = yield* OpenAiClient.OpenAiClient const error = yield* client.createResponse({ model: "gpt-4o-mini", @@ -389,24 +388,93 @@ describe("OpenAiClient", () => { assert.strictEqual(error._tag, "AiError") assert.strictEqual(error.method, "createResponse") assert.strictEqual(error.reason._tag, "QuotaExhaustedError") - })) + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 429, + body: { + error: { + message: "You exceeded your current quota", + type: "insufficient_quota", + code: "insufficient_quota" + } + } + })))) }) }) -const makeHttpClient = ( - handler: ( - request: HttpClientRequest.HttpClientRequest - ) => Effect.Effect -) => - HttpClient.makeWith( +type MockResponse = + | { + readonly _tag: "Json" + readonly body: Schema.Json + readonly status?: number | undefined + readonly headers?: Record | undefined + } + | { + readonly _tag: "Sse" + readonly events: ReadonlyArray + readonly status?: number | undefined + readonly headers?: Record | undefined + } + | { + readonly _tag: "RawSse" + readonly body: string + readonly status?: number | undefined + readonly headers?: Record | undefined + } + +class MockOpenAiResponse extends Context.Service()("MockOpenAiResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = MockHttpClient.use((client) => client.requests) +} + +const makeHttpClientContext = Effect.gen(function*() { + const capturedRequests: Array = [] + const mock = yield* MockOpenAiResponse + + const httpClient = HttpClient.makeWith( Effect.fnUntraced(function*(requestEffect) { const request = yield* requestEffect - return yield* handler(request) + capturedRequests.push(request) + return makeResponse(request, mock.response) }), Effect.succeed as HttpClient.HttpClient.Preprocess ) -const makeChatCompletion = () => ({ + const mockHttpClient: MockHttpClient["Service"] = { + requests: Effect.sync(() => capturedRequests) + } + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, mockHttpClient) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClientContext) + +const makeTestLayer = (response: MockResponse = { + _tag: "Json", + body: makeCreateResponse() +}) => + OpenAiClient.layer({ + apiKey: Redacted.make("sk-test-key"), + apiUrl: "https://compat.example.test/v1", + organizationId: Redacted.make("org_123"), + projectId: Redacted.make("proj_456") + }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { + response + })) + ) + +const makeCreateResponse = ( + overrides: Partial = {} +) => ({ id: "chatcmpl_test_1", object: "chat.completion", model: "gpt-4o-mini", @@ -423,37 +491,34 @@ const makeChatCompletion = () => ({ prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 - } + }, + ...overrides }) -const jsonResponse = ( +const makeResponse = ( request: HttpClientRequest.HttpClientRequest, - status: number, - body: unknown -): HttpClientResponse.HttpClientResponse => - HttpClientResponse.fromWeb( + response: MockResponse +): HttpClientResponse.HttpClientResponse => { + const contentType = response._tag === "Json" + ? "application/json" + : "text/event-stream" + const body = response._tag === "Json" + ? JSON.stringify(response.body) + : response._tag === "Sse" + ? toSseBody(response.events) + : response.body + + return HttpClientResponse.fromWeb( request, - new Response(JSON.stringify(body), { - status, + new Response(body, { + status: response.status ?? 200, headers: { - "content-type": "application/json" - } - }) - ) - -const sseResponse = ( - request: HttpClientRequest.HttpClientRequest, - events: ReadonlyArray -): HttpClientResponse.HttpClientResponse => - HttpClientResponse.fromWeb( - request, - new Response(toSseBody(events), { - status: 200, - headers: { - "content-type": "text/event-stream" + "content-type": contentType, + ...response.headers } }) ) +} const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => Effect.gen(function*() { @@ -465,10 +530,8 @@ const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => return yield* Effect.die(new Error("Expected Uint8Array body")) }) -const toSseBody = (events: ReadonlyArray): string => +const toSseBody = (events: ReadonlyArray): string => events.map((event) => { - if (typeof event === "string") { - return `data: ${event}\n\n` - } - return `data: ${JSON.stringify(event)}\n\n` + const data = event === "[DONE]" ? event : JSON.stringify(event) + return `data: ${data}\n\n` }).join("") diff --git a/.context/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts b/.context/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts index e6c26fbac..e968ba2c2 100644 --- a/.context/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts +++ b/.context/effect/packages/ai/openai-compat/test/OpenAiLanguageModel.test.ts @@ -329,6 +329,145 @@ describe("OpenAiLanguageModel", () => { assert.strictEqual(functionTool.function.strict, true) })) + it.effect("decodes tool call params with the OpenAI codec", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(jsonResponse( + request, + makeChatCompletion({ + choices: [{ + index: 0, + finish_reason: "tool_calls", + message: { + role: "assistant", + content: null, + tool_calls: [{ + id: "call_record_1", + type: "function", + function: { + name: "RecordTool", + arguments: JSON.stringify({ env: [{ 0: "PATH", 1: "/usr/bin" }] }) + } + }] + } + }] + }) + )) + ) + )) + ) + + const result = yield* LanguageModel.generateText({ + prompt: "read the environment", + toolkit: RecordToolkit, + disableToolCallResolution: true + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(RecordToolkitLayer), + Effect.provide(layer) + ) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + assert.deepStrictEqual(toolCall.params, { env: { PATH: "/usr/bin" } }) + })) + + it.effect("groups parallel tool calls into one assistant message", () => + Effect.gen(function*() { + let capturedRequest: HttpClientRequest.HttpClientRequest | undefined + + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => { + capturedRequest = request + return Effect.succeed(jsonResponse(request, makeChatCompletion())) + }) + )) + ) + + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "use both tools" }, + { + role: "assistant", + content: [ + Prompt.toolCallPart({ + id: "call_1", + name: "TestTool", + params: { input: "first" }, + providerExecuted: false + }), + Prompt.toolCallPart({ + id: "call_2", + name: "TestTool", + params: { input: "second" }, + providerExecuted: false + }) + ] + }, + { + role: "tool", + content: [ + Prompt.toolResultPart({ + id: "call_1", + name: "TestTool", + isFailure: false, + result: { output: "first" }, + providerExecuted: false + }), + Prompt.toolResultPart({ + id: "call_2", + name: "TestTool", + isFailure: false, + result: { output: "second" }, + providerExecuted: false + }) + ] + } + ]), + toolkit: TestToolkit + }).pipe( + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(TestToolkitLayer), + Effect.provide(layer) + ) + + assert.isDefined(capturedRequest) + if (capturedRequest === undefined) { + return + } + + const requestBody = yield* getRequestBody(capturedRequest) + assert.deepStrictEqual(requestBody.messages, [ + { role: "user", content: "use both tools" }, + { + role: "assistant", + content: null, + tool_calls: [ + { + id: "call_1", + type: "function", + function: { name: "TestTool", arguments: JSON.stringify({ input: "first" }) } + }, + { + id: "call_2", + type: "function", + function: { name: "TestTool", arguments: JSON.stringify({ input: "second" }) } + } + ] + }, + { role: "tool", tool_call_id: "call_1", content: JSON.stringify({ output: "first" }) }, + { role: "tool", tool_call_id: "call_2", content: JSON.stringify({ output: "second" }) } + ]) + })) + it.effect("converts dynamic tools to function type", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined @@ -762,6 +901,59 @@ describe("OpenAiLanguageModel", () => { } })) + it.effect("decodes streamed tool call params with the OpenAI codec", () => + Effect.gen(function*() { + const layer = OpenAiClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + makeHttpClient((request) => + Effect.succeed(sseResponse(request, [ + { + id: "chatcmpl_record_1", + object: "chat.completion.chunk", + model: "gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + id: "call_record_1", + type: "function", + function: { + name: "RecordTool", + arguments: JSON.stringify({ env: [{ 0: "PATH", 1: "/usr/bin" }] }) + } + }] + }, + finish_reason: "tool_calls" + }] + }, + "[DONE]" + ])) + ) + )) + ) + + const partsChunk = yield* LanguageModel.streamText({ + prompt: "read the environment", + toolkit: RecordToolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(RecordToolkitLayer), + Effect.provide(layer) + ) + + const toolCall = globalThis.Array.from(partsChunk).find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + if (toolCall?.type !== "tool-call") { + return + } + assert.deepStrictEqual(toolCall.params, { env: { PATH: "/usr/bin" } }) + })) + it.effect("maps local shell stream tool calls to local_shell call outputs", () => Effect.gen(function*() { const capturedRequests = yield* Ref.make>([]) @@ -834,7 +1026,8 @@ describe("OpenAiLanguageModel", () => { id: toolCall.id, name: toolCall.name, isFailure: false, - result: "done" + result: "done", + providerExecuted: false })] } ]), @@ -1064,11 +1257,12 @@ describe("OpenAiLanguageModel", () => { assert.deepStrictEqual(toolCall.params, expectedParams) })) - it.effect("streams known events and ignores unknown ones", () => + it.effect("continues after invalid JSON and schema-mismatched events", () => Effect.gen(function*() { let capturedRequest: HttpClientRequest.HttpClientRequest | undefined const events = [ + "{invalid-json", { id: "chatcmpl_stream_1", object: "chat.completion.chunk", @@ -1080,6 +1274,10 @@ describe("OpenAiLanguageModel", () => { finish_reason: null }] }, + { + type: "provider.chat.completion.delta", + provider_payload: { content: "provider-specific" } + }, { id: "chatcmpl_stream_1", object: "chat.completion.chunk", @@ -1411,6 +1609,19 @@ const TestToolkitLayer = TestToolkit.toLayer({ TestTool: ({ input }) => Effect.succeed({ output: input }) }) +const RecordTool = Tool.make("RecordTool", { + parameters: Schema.Struct({ + env: Schema.Record(Schema.String, Schema.String) + }), + success: Schema.String +}) + +const RecordToolkit = Toolkit.make(RecordTool) + +const RecordToolkitLayer = RecordToolkit.toLayer({ + RecordTool: () => Effect.succeed("done") +}) + const CompatApplyPatchTool = Tool.providerDefined({ id: "compat.apply_patch", customName: "CompatApplyPatch", diff --git a/.context/effect/packages/ai/openai-compat/tsconfig.json b/.context/effect/packages/ai/openai-compat/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/ai/openai-compat/tsconfig.json +++ b/.context/effect/packages/ai/openai-compat/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/ai/openai-compat/vitest.config.ts b/.context/effect/packages/ai/openai-compat/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/ai/openai-compat/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/ai/openai/CHANGELOG.md b/.context/effect/packages/ai/openai/CHANGELOG.md index 96b5a8472..c7a8bac68 100644 --- a/.context/effect/packages/ai/openai/CHANGELOG.md +++ b/.context/effect/packages/ai/openai/CHANGELOG.md @@ -1,5 +1,78 @@ # @effect/ai-openai +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7126](https://github.com/Effect-TS/effect/pull/7126) [`16b94c7`](https://github.com/Effect-TS/effect/commit/16b94c702419c318e0f3515c902c39cf3871ccce) Thanks @fubhy! - Fix OpenAI response telemetry attribute types to use the emitted response namespace. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#7043](https://github.com/Effect-TS/effect/pull/7043) [`dce8219`](https://github.com/Effect-TS/effect/commit/dce8219d4041e1b188a4710550caf20f0c452f09) Thanks @fubhy! - Preserve OpenAI provider errors from failed response stream events. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6921](https://github.com/Effect-TS/effect/pull/6921) [`4686265`](https://github.com/Effect-TS/effect/commit/468626540686305d7ce34ecdd76e67b2bef2a60e) Thanks @fubhy! - Emit specialized OpenAI tool results only once. + +- [#6722](https://github.com/Effect-TS/effect/pull/6722) [`9344742`](https://github.com/Effect-TS/effect/commit/9344742c6b0ae4ff627b4492a6ddf7fbac5c3785) Thanks @mrtdurdenthe2! - Fix OpenAI stable web search response decoding by preserving the provider action in tool call parameters. + +- [#6675](https://github.com/Effect-TS/effect/pull/6675) [`4cc95ae`](https://github.com/Effect-TS/effect/commit/4cc95ae4a88bc9f5a2e7595de771caeee354cf6e) Thanks @danieljvdm! - Accept image generation-specific lifecycle statuses and nullable results in OpenAI response items. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6920](https://github.com/Effect-TS/effect/pull/6920) [`da10211`](https://github.com/Effect-TS/effect/commit/da102116733d485e794c1d06f938a4e03daf418e) Thanks @fubhy! - Terminate OpenAI HTTP and WebSocket response streams when a `response.failed` event arrives. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6650](https://github.com/Effect-TS/effect/pull/6650) [`acd385e`](https://github.com/Effect-TS/effect/commit/acd385ebb3f9edee37ab6715607119ee9762a615) Thanks @IMax153! - Redact OpenAI organization and project headers from client errors. + +- [#6549](https://github.com/Effect-TS/effect/pull/6549) [`64c6ab1`](https://github.com/Effect-TS/effect/commit/64c6ab1951ac6fb0bdd5e0398795946dc2872be7) Thanks @xianjianlf2! - Encode OpenAI Responses API system messages as typed input text content. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/ai/openai/README.md b/.context/effect/packages/ai/openai/README.md new file mode 100644 index 000000000..1d5ae4902 --- /dev/null +++ b/.context/effect/packages/ai/openai/README.md @@ -0,0 +1,14 @@ +# @effect/ai-openai + +An [OpenAI](https://openai.com) provider for the Effect AI modules. Includes a typed OpenAI API client, language model and embedding model layers, tools, and telemetry helpers. + +## Installation + +```sh +npm install effect@beta @effect/ai-openai@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/ai-openai) diff --git a/.context/effect/packages/ai/openai/codegen.yaml b/.context/effect/packages/ai/openai/codegen.yaml index 19d06da16..05724087a 100644 --- a/.context/effect/packages/ai/openai/codegen.yaml +++ b/.context/effect/packages/ai/openai/codegen.yaml @@ -13,7 +13,6 @@ patches: - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/prompt_cache_key/nullable","value":true}]' - '[{"op":"add","path":"/components/schemas/Response/allOf/2/properties/usage/nullable","value":true}]' - '[{"op":"remove","path":"/components/schemas/ResponseFunctionCallArgumentsDoneEvent/required/2"}]' - - '[{"op":"remove","path":"/components/schemas/WebSearchActionSearch/required/1"}]' - '[{"op":"add","path":"/components/schemas/ModelResponseProperties/properties/prompt_cache_retention/anyOf/0/enum/1","value":"in_memory"}]' - '[{"op":"add","path":"/components/schemas/PromptCacheRetentionEnum/enum/1","value":"in-memory"}]' - '[{"op":"replace","path":"/components/schemas/OpenAIFile/properties/expires_at","value":{"anyOf":[{"type":"integer","format":"unixtime","description":"The Unix timestamp (in seconds) for when the file will expire."},{"type":"null"}]}}]' diff --git a/.context/effect/packages/ai/openai/docgen.json b/.context/effect/packages/ai/openai/docgen.json deleted file mode 100644 index 0cdaf8e85..000000000 --- a/.context/effect/packages/ai/openai/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openai/src/", - "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/ai/openai/package.json b/.context/effect/packages/ai/openai/package.json index c6afd3ef5..b44696fde 100644 --- a/.context/effect/packages/ai/openai/package.json +++ b/.context/effect/packages/ai/openai/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openai", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "An OpenAI provider integration for Effect AI SDK", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/ai/openai/src/Generated.ts b/.context/effect/packages/ai/openai/src/Generated.ts index d7189bf5d..d51b31244 100644 --- a/.context/effect/packages/ai/openai/src/Generated.ts +++ b/.context/effect/packages/ai/openai/src/Generated.ts @@ -30441,7 +30441,7 @@ export const make = ( request: HttpClientRequest.HttpClientRequest ): Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, DecodingServices > => HttpClient.filterStatusOk(httpClient).execute(request).pipe( @@ -32832,7 +32832,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateSpeechRequestJson.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateSpeech200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateSpeech200Sse.DecodingServices > /** @@ -32868,7 +32868,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateTranscriptionRequestFormData.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateTranscription200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateTranscription200Sse.DecodingServices > /** @@ -33058,7 +33058,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateChatCompletionRequestJson.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateChatCompletion200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateChatCompletion200Sse.DecodingServices > /** @@ -33659,7 +33659,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateImageEditRequestFormData.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateImageEdit200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateImageEdit200Sse.DecodingServices > /** @@ -33678,7 +33678,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateImageRequestJson.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateImage200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateImage200Sse.DecodingServices > /** @@ -34879,7 +34879,7 @@ export interface OpenAiClient { options: { readonly payload: typeof CreateResponseRequestJson.Encoded } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateResponse200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateResponse200Sse.DecodingServices > /** diff --git a/.context/effect/packages/ai/openai/src/OpenAiClient.ts b/.context/effect/packages/ai/openai/src/OpenAiClient.ts index a1774b6d3..43753b63b 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiClient.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiClient.ts @@ -47,7 +47,7 @@ import * as OpenAiSchema from "./OpenAiSchema.ts" * * Provides the configured HTTP client plus helpers for Responses API calls, streaming Responses events, and embeddings. Transport and schema decoding failures are mapped to `AiError`. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { @@ -158,6 +158,11 @@ const RedactedOpenAiHeaders = { OpenAiProject: "OpenAI-Project" } +const withRedactedHeaders = Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) +) + /** * Creates an OpenAI client service with the given options. * @@ -232,25 +237,27 @@ export const make = Effect.fnUntraced( [body: typeof OpenAiSchema.Response.Type, response: HttpClientResponse.HttpClientResponse], AiError.AiError > => - Effect.flatMap(resolveHttpClient, (client) => - client.execute( - HttpClientRequest.post("/responses", { + resolveHttpClient.pipe( + Effect.flatMap((client) => + client.execute(HttpClientRequest.post("/responses", { body: HttpBody.jsonUnsafe(payload) - }) - ).pipe( - Effect.flatMap((response) => - decodeResponse(response).pipe( - Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [ - body, - response - ]) - ) - ), - Effect.catchTags({ - HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), - SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) - }) - )) + })).pipe( + Effect.flatMap((response) => + decodeResponse(response).pipe( + Effect.map((body): [typeof OpenAiSchema.Response.Type, HttpClientResponse.HttpClientResponse] => [ + body, + response + ]) + ) + ), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createResponse"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createResponse")) + }) + ) + ), + withRedactedHeaders + ) const buildResponseStream = ( response: HttpClientResponse.HttpClientResponse @@ -263,12 +270,14 @@ export const make = Effect.fnUntraced( Stream.pipeThroughChannel(Sse.decodeDataSchema(OpenAiSchema.ResponseStreamEvent)), Stream.takeUntil((event) => event.data.type === "response.completed" || - event.data.type === "response.incomplete" + event.data.type === "response.incomplete" || + event.data.type === "response.failed" ), Stream.map((event) => event.data), Stream.catchTags({ // TODO: handle SSE retries Retry: (error) => Stream.die(error), + SseError: (error) => Stream.fail(Errors.mapSseError(error, "createResponseStream")), HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createResponseStream")), SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createResponseStream")) }) @@ -280,18 +289,20 @@ export const make = Effect.fnUntraced( Effect.contextWith((services) => { const socket = Context.getOrUndefined(services, OpenAiSocket) if (socket) return socket.createResponseStream(payload) - return Effect.flatMap(resolveHttpClient, (client) => - client.execute( - HttpClientRequest.post("/responses", { + return resolveHttpClient.pipe( + Effect.flatMap((client) => + client.execute(HttpClientRequest.post("/responses", { body: HttpBody.jsonUnsafe({ ...payload, stream: true }) - }) - ).pipe( - Effect.map(buildResponseStream), - Effect.catchTag( - "HttpClientError", - (error) => Errors.mapHttpClientError(error, "createResponseStream") + })).pipe( + Effect.map(buildResponseStream), + Effect.catchTag( + "HttpClientError", + (error) => Errors.mapHttpClientError(error, "createResponseStream") + ) ) - )) + ), + withRedactedHeaders + ) }) const decodeEmbedding = HttpClientResponse.schemaBodyJson(OpenAiSchema.CreateEmbeddingResponse) @@ -299,18 +310,20 @@ export const make = Effect.fnUntraced( const createEmbedding = ( payload: typeof OpenAiSchema.CreateEmbeddingRequest.Encoded ): Effect.Effect => - Effect.flatMap(resolveHttpClient, (client) => - client.execute( - HttpClientRequest.post("/embeddings", { + resolveHttpClient.pipe( + Effect.flatMap((client) => + client.execute(HttpClientRequest.post("/embeddings", { body: HttpBody.jsonUnsafe(payload) - }) - ).pipe( - Effect.flatMap(decodeEmbedding), - Effect.catchTags({ - HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), - SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) - }) - )) + })).pipe( + Effect.flatMap(decodeEmbedding), + Effect.catchTags({ + HttpClientError: (error) => Errors.mapHttpClientError(error, "createEmbedding"), + SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createEmbedding")) + }) + ) + ), + withRedactedHeaders + ) return OpenAiClient.of({ client: httpClient, @@ -319,10 +332,7 @@ export const make = Effect.fnUntraced( createEmbedding }) }, - Effect.updateService( - Headers.CurrentRedactedNames, - Array.appendAll(Object.values(RedactedOpenAiHeaders)) - ) + withRedactedHeaders ) // ============================================================================= @@ -424,7 +434,7 @@ export const layerConfig = (options?: { /** * Response stream event emitted by the OpenAI Responses API. * - * @category Events + * @category models * @since 4.0.0 */ export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type @@ -451,7 +461,7 @@ export type ResponseStreamEvent = typeof OpenAiSchema.ResponseStreamEvent.Type * @see {@link withWebSocketMode} for enabling WebSocket mode for one effect * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer * - * @category Websocket mode + * @category services * @since 4.0.0 */ export class OpenAiSocket extends Context.Service { - done = e.type === "response.completed" || e.type === "response.incomplete" + done = e.type === "response.completed" || e.type === "response.incomplete" || e.type === "response.failed" return done }) ) @@ -642,7 +656,7 @@ const makeSocket = Effect.gen(function*() { const ErrorEvent = Schema.Struct({ type: Schema.Literal("error"), - status: Schema.Number.pipe( + status: Schema.Int.pipe( Schema.withDecodingDefault(Effect.succeed(500)) ), error: Schema.Struct({ @@ -683,7 +697,7 @@ const decodeEvent = Schema.decodeUnknownSync(Schema.fromJsonString(AllEvents)) * @see {@link layerWebSocketMode} for providing WebSocket mode through a layer * @see {@link OpenAiSocket} for direct access to the WebSocket-backed streaming service * - * @category Websocket mode + * @category providing services * @since 4.0.0 */ export const withWebSocketMode = ( @@ -720,7 +734,7 @@ export const withWebSocketMode = ( * * @see {@link withWebSocketMode} for enabling WebSocket mode around a single effect * - * @category Websocket mode + * @category layers * @since 4.0.0 */ export const layerWebSocketMode: Layer.Layer< diff --git a/.context/effect/packages/ai/openai/src/OpenAiClientGenerated.ts b/.context/effect/packages/ai/openai/src/OpenAiClientGenerated.ts index 1b2c9b01f..6df83c43a 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiClientGenerated.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiClientGenerated.ts @@ -74,6 +74,11 @@ const RedactedOpenAiHeaders = { OpenAiProject: "OpenAI-Project" } +const withRedactedHeaders = Effect.updateService( + Headers.CurrentRedactedNames, + Array.appendAll(Object.values(RedactedOpenAiHeaders)) +) + // ============================================================================= // Constructor // ============================================================================= @@ -124,10 +129,7 @@ export const make = Effect.fnUntraced( }) }) }, - Effect.updateService( - Headers.CurrentRedactedNames, - Array.appendAll(Object.values(RedactedOpenAiHeaders)) - ) + withRedactedHeaders ) // ============================================================================= diff --git a/.context/effect/packages/ai/openai/src/OpenAiConfig.ts b/.context/effect/packages/ai/openai/src/OpenAiConfig.ts index ea0b05cde..37f18d7ac 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiConfig.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiConfig.ts @@ -49,7 +49,7 @@ export declare namespace OpenAiConfig { * Configuration values read by OpenAI provider operations when executing * requests. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { diff --git a/.context/effect/packages/ai/openai/src/OpenAiError.ts b/.context/effect/packages/ai/openai/src/OpenAiError.ts index cb0ccc2dd..5e2a96d60 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiError.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiError.ts @@ -68,7 +68,7 @@ declare module "effect/unstable/ai/AiError" { * from responses where the provider rejected the request because a limit was * reached. * - * @category configuration + * @category models * @since 4.0.0 */ export interface RateLimitErrorMetadata { @@ -86,7 +86,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for failures caused by exhausted account, * billing, or usage quota. * - * @category configuration + * @category models * @since 4.0.0 */ export interface QuotaExhaustedErrorMetadata { @@ -104,7 +104,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for failed API key, authorization, or * permission checks. * - * @category configuration + * @category models * @since 4.0.0 */ export interface AuthenticationErrorMetadata { @@ -122,7 +122,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when OpenAI rejects input or output because * it violates a content policy. * - * @category configuration + * @category models * @since 4.0.0 */ export interface ContentPolicyErrorMetadata { @@ -140,7 +140,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for malformed requests, unsupported * parameters, or other request validation failures reported by OpenAI. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidRequestErrorMetadata { @@ -158,7 +158,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for OpenAI-side failures such as transient * server errors. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InternalProviderErrorMetadata { @@ -176,7 +176,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when an OpenAI response cannot be parsed or * validated as the expected output. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidOutputErrorMetadata { @@ -194,7 +194,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when OpenAI returns content that does not * satisfy the requested structured output schema. * - * @category configuration + * @category models * @since 4.0.0 */ export interface StructuredOutputErrorMetadata { @@ -212,7 +212,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when an unsupported schema failure is * associated with an OpenAI response. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnsupportedSchemaErrorMetadata { @@ -230,7 +230,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for OpenAI failures that do not map cleanly * to a more specific AI error category. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnknownErrorMetadata { diff --git a/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts b/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts index aff410fbe..c78d3e2af 100644 --- a/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts +++ b/.context/effect/packages/ai/openai/src/OpenAiLanguageModel.ts @@ -19,6 +19,7 @@ import * as Predicate from "effect/Predicate" import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as AST from "effect/SchemaAST" +import * as SchemaIssue from "effect/SchemaIssue" import * as Stream from "effect/Stream" import type { Span } from "effect/Tracer" import type { DeepMutable, Mutable, Simplify } from "effect/Types" @@ -39,6 +40,8 @@ import type * as OpenAiSchema from "./OpenAiSchema.ts" import { addGenAIAnnotations } from "./OpenAiTelemetry.ts" import type * as OpenAiTool from "./OpenAiTool.ts" +const formatIssue = SchemaIssue.makeFormatterDefault() + const ResponseModelIds = Generated.ModelIdsResponses.members[1] const SharedModelIds = Generated.ModelIdsShared.members[1] @@ -127,7 +130,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-specific options for file prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface FilePartOptions extends ProviderOptions { @@ -145,7 +148,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-specific options for reasoning prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ReasoningPartOptions extends ProviderOptions { @@ -169,7 +172,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-specific options for assistant tool-call prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolCallPartOptions extends ProviderOptions { @@ -195,7 +198,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-specific options for tool-result prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolResultPartOptions extends ProviderOptions { @@ -221,7 +224,7 @@ declare module "effect/unstable/ai/Prompt" { /** * OpenAI-specific options for text prompt parts. * - * @category request + * @category models * @since 4.0.0 */ export interface TextPartOptions extends ProviderOptions { @@ -249,7 +252,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to a complete text response part. * - * @category response + * @category models * @since 4.0.0 */ export interface TextPartMetadata extends ProviderMetadata { @@ -281,7 +284,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata emitted when a streamed text part starts. * - * @category response + * @category models * @since 4.0.0 */ export interface TextStartPartMetadata extends ProviderMetadata { @@ -299,7 +302,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata emitted when a streamed text part ends. * - * @category response + * @category models * @since 4.0.0 */ export interface TextEndPartMetadata extends ProviderMetadata { @@ -321,7 +324,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to a complete reasoning response part. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningPartMetadata extends ProviderMetadata { @@ -343,7 +346,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata emitted when a streamed reasoning part starts. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningStartPartMetadata extends ProviderMetadata { @@ -365,7 +368,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata emitted for a streamed reasoning delta. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningDeltaPartMetadata extends ProviderMetadata { @@ -383,7 +386,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata emitted when a streamed reasoning part ends. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningEndPartMetadata extends ProviderMetadata { @@ -405,7 +408,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to tool-call response parts. * - * @category response + * @category models * @since 4.0.0 */ export interface ToolCallPartMetadata extends ProviderMetadata { @@ -423,7 +426,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to document source citations. * - * @category response + * @category models * @since 4.0.0 */ export interface DocumentSourcePartMetadata extends ProviderMetadata { @@ -479,7 +482,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to URL source citations. * - * @category response + * @category models * @since 4.0.0 */ export interface UrlSourcePartMetadata extends ProviderMetadata { @@ -505,7 +508,7 @@ declare module "effect/unstable/ai/Response" { /** * OpenAI metadata attached to finish response parts. * - * @category response + * @category models * @since 4.0.0 */ export interface FinishPartMetadata extends ProviderMetadata { @@ -782,7 +785,7 @@ const prepareMessages = Effect.fnUntraced( Tool.isProviderDefined(tool) && tool.name === "OpenAiCodeInterpreter" ) const shellTool = options.tools.find((tool): tool is ReturnType => - Tool.isProviderDefined(tool) && tool.name === "OpenAiFunctionShell" + Tool.isProviderDefined(tool) && tool.name === "OpenAiShell" ) const localShellTool = options.tools.find((tool): tool is ReturnType => Tool.isProviderDefined(tool) && tool.name === "OpenAiLocalShell" @@ -816,7 +819,7 @@ const prepareMessages = Effect.fnUntraced( case "system": { messages.push({ role: getSystemMessageMode(config.model as string), - content: message.content + content: [{ type: "input_text", text: message.content }] }) break } @@ -885,7 +888,8 @@ const prepareMessages = Effect.fnUntraced( } case "assistant": { - const reasoningMessages: Record> = {} + const reasoningMessages: Record> = Object + .create(null) for (const part of message.content) { switch (part.type) { @@ -1129,6 +1133,7 @@ const prepareMessages = Effect.fnUntraced( call_id: part.id, ...(part.result as any) }) + continue } if (Predicate.isNotUndefined(shellTool) && toolName === "shell") { @@ -1139,6 +1144,7 @@ const prepareMessages = Effect.fnUntraced( output: part.result as any, ...(Predicate.isNotNull(status) ? { status } : {}) }) + continue } if (Predicate.isNotUndefined(localShellTool) && toolName === "local_shell") { @@ -1149,6 +1155,7 @@ const prepareMessages = Effect.fnUntraced( output: part.result as any, ...(Predicate.isNotNull(status) ? { status } : {}) }) + continue } messages.push({ @@ -1614,7 +1621,9 @@ const makeResponse = Effect.fnUntraced( type: "tool-call", id: part.id, name: toolName, - params: {}, + params: webSearchTool?.name === "OpenAiWebSearchPreview" + ? {} + : { action: part.action }, providerExecuted: true }) parts.push({ @@ -1682,7 +1691,7 @@ const makeStreamResponse = Effect.fnUntraced( } // Track active reasoning items with state machine for proper concluding logic - const activeReasoning: Record = {} + const activeReasoning: Record = Object.create(null) const getOrCreateReasoningPart = ( itemId: string, @@ -1753,8 +1762,7 @@ const makeStreamResponse = Effect.fnUntraced( } case "response.completed": - case "response.incomplete": - case "response.failed": { + case "response.incomplete": { parts.push({ type: "finish", reason: InternalUtilities.resolveFinishReason( @@ -1768,6 +1776,20 @@ const makeStreamResponse = Effect.fnUntraced( break } + case "response.failed": { + if (event.response.error) { + parts.push({ type: "error", error: event.response.error }) + } + parts.push({ + type: "finish", + reason: "error", + usage: getUsage(event.response.usage), + response: buildHttpResponseDetails(response), + ...toServiceTier(event.response.service_tier) + }) + break + } + case "response.output_item.added": { switch (event.item.type) { case "apply_patch_call": { @@ -1942,6 +1964,9 @@ const makeStreamResponse = Effect.fnUntraced( id: event.item.id, name: toolName } + if (webSearchTool?.name === "OpenAiWebSearch") { + break + } parts.push({ type: "tool-params-start", id: event.item.id, @@ -2255,6 +2280,15 @@ const makeStreamResponse = Effect.fnUntraced( const toolName = toolNameMapper.getCustomName( webSearchTool?.name ?? "web_search" ) + if (webSearchTool?.name === "OpenAiWebSearch") { + parts.push({ + type: "tool-call", + id: event.item.id, + name: toolName, + params: { action: event.item.action }, + providerExecuted: true + }) + } parts.push({ type: "tool-result", id: event.item.id, @@ -3127,7 +3161,7 @@ const transformToolCallParams = Effect.fnUntraced(function* - & Telemetry.AttributesWithPrefix + & Telemetry.AttributesWithPrefix > /** diff --git a/.context/effect/packages/ai/openai/src/internal/errors.ts b/.context/effect/packages/ai/openai/src/internal/errors.ts index a1be0c963..19bd28e5c 100644 --- a/.context/effect/packages/ai/openai/src/internal/errors.ts +++ b/.context/effect/packages/ai/openai/src/internal/errors.ts @@ -8,6 +8,7 @@ import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as AiError from "effect/unstable/ai/AiError" import type * as Response from "effect/unstable/ai/Response" +import type * as Sse from "effect/unstable/encoding/Sse" import type * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" @@ -42,6 +43,17 @@ export const mapSchemaError = dual< reason: AiError.InvalidOutputError.fromSchemaError(error) })) +/** @internal */ +export const mapSseError = dual< + (method: string) => (error: Sse.SseError) => AiError.AiError, + (error: Sse.SseError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenAiClient", + method, + reason: new AiError.InvalidOutputError({ description: error.message }) + })) + /** @internal */ export const mapHttpClientError = dual< (method: string) => (error: HttpClientError.HttpClientError) => Effect.Effect, diff --git a/.context/effect/packages/ai/openai/src/internal/utilities.ts b/.context/effect/packages/ai/openai/src/internal/utilities.ts index 1aca7dae4..26cf670c3 100644 --- a/.context/effect/packages/ai/openai/src/internal/utilities.ts +++ b/.context/effect/packages/ai/openai/src/internal/utilities.ts @@ -19,7 +19,7 @@ export const resolveFinishReason = ( if (finishReason == null) { return hasToolCalls ? "tool-calls" : "stop" } - const reason = finishReasonMap[finishReason] + const reason = Object.hasOwn(finishReasonMap, finishReason) ? finishReasonMap[finishReason] : undefined if (reason == null) { return hasToolCalls ? "tool-calls" : "unknown" } diff --git a/.context/effect/packages/ai/openai/test/OpenAiClient.test.ts b/.context/effect/packages/ai/openai/test/OpenAiClient.test.ts index d9069fd5a..9c5471c99 100644 --- a/.context/effect/packages/ai/openai/test/OpenAiClient.test.ts +++ b/.context/effect/packages/ai/openai/test/OpenAiClient.test.ts @@ -1,251 +1,113 @@ +import type { OpenAiSchema } from "@effect/ai-openai" import type * as Generated from "@effect/ai-openai/Generated" import * as Errors from "@effect/ai-openai/internal/errors" import * as OpenAiClient from "@effect/ai-openai/OpenAiClient" import * as OpenAiClientGenerated from "@effect/ai-openai/OpenAiClientGenerated" import * as OpenAiConfig from "@effect/ai-openai/OpenAiConfig" import { assert, describe, it } from "@effect/vitest" -import { Config, ConfigProvider, Effect, Layer, Redacted, Schema, Stream } from "effect" -import type * as AiError from "effect/unstable/ai/AiError" +import { Config, ConfigProvider, Context, Effect, Layer, Redacted, Schema, Stream } from "effect" import * as HttpClient from "effect/unstable/http/HttpClient" import * as HttpClientError from "effect/unstable/http/HttpClientError" import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" - -// ============================================================================= -// Mock Helpers -// ============================================================================= - -const makeMockResponse = (options: { - readonly status: number - readonly body: unknown - readonly request?: HttpClientRequest.HttpClientRequest -}): HttpClientResponse.HttpClientResponse => { - // Always use a plain request for the response to avoid Redacted headers in error contexts - const request = HttpClientRequest.get(options.request?.url ?? "/") - const json = JSON.stringify(options.body) - return HttpClientResponse.fromWeb( - request, - new Response(json, { - status: options.status, - headers: { "content-type": "application/json" } - }) - ) -} - -const makeMockStreamResponse = (options: { - readonly events: ReadonlyArray - readonly request?: HttpClientRequest.HttpClientRequest -}): HttpClientResponse.HttpClientResponse => { - const request = HttpClientRequest.get(options.request?.url ?? "/") - const body = options.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") - return HttpClientResponse.fromWeb( - request, - new Response(body, { - status: 200, - headers: { "content-type": "text/event-stream" } - }) - ) -} - -const makeMockHttpClient = ( - handler: ( - request: HttpClientRequest.HttpClientRequest - ) => Effect.Effect -): HttpClient.HttpClient => - HttpClient.makeWith( - (effect) => - Effect.flatMap(effect, handler) as Effect.Effect< - HttpClientResponse.HttpClientResponse, - HttpClientError.HttpClientError, - never - >, - Effect.succeed - ) - -const makeResponseBody = ( - overrides: Partial = {} -): typeof Generated.Response.Encoded => ({ - id: "resp_test123", - object: "response", - created_at: 1, - model: "gpt-4o-mini", - status: "completed", - output: [], - metadata: null, - temperature: null, - top_p: null, - tools: [], - tool_choice: "auto", - error: null, - incomplete_details: null, - instructions: null, - parallel_tool_calls: false, - ...overrides -}) - -// ============================================================================= -// Tests -// ============================================================================= +import * as Socket from "effect/unstable/socket/Socket" +import { WS } from "vitest-websocket-mock" describe("OpenAiClient", () => { describe("make", () => { it.effect("sets Bearer token from apiKey", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("sk-test-12345") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - // Call method and ignore response parsing errors - we only care about the request - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - assert.isDefined(capturedRequest) - const authHeader = capturedRequest!.headers["authorization"] - assert.strictEqual(authHeader, "Bearer sk-test-12345") - })) + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer sk-test-12345") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("sk-test-12345") + })))) it.effect("prepends default URL", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - assert.isDefined(capturedRequest) - assert.isTrue(capturedRequest!.url.startsWith("https://api.openai.com/v1")) - })) + const requests = yield* MockHttpClient.requests + assert.isTrue(requests[0]?.url.startsWith("https://api.openai.com/v1")) + }).pipe(Effect.provide(makeTestLayer()))) it.effect("uses custom apiUrl when provided", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - apiUrl: "https://custom.api.com/v2" - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - assert.isDefined(capturedRequest) - assert.isTrue(capturedRequest!.url.startsWith("https://custom.api.com/v2")) - })) + const requests = yield* MockHttpClient.requests + assert.isTrue(requests[0]?.url.startsWith("https://custom.api.com/v2")) + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + apiUrl: "https://custom.api.com/v2" + })))) it.effect("sets OpenAI-Organization header when organizationId provided", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - organizationId: Redacted.make("org-12345") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["openai-organization"], "org-12345") - })) + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["openai-organization"], "org-12345") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + organizationId: Redacted.make("org-12345") + })))) it.effect("sets OpenAI-Project header when projectId provided", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - projectId: Redacted.make("proj-67890") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) - - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["openai-project"], "proj-67890") - })) + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - it.effect("applies transformClient option", () => - Effect.gen(function*() { - let transformApplied = false - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - ) + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["openai-project"], "proj-67890") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + projectId: Redacted.make("proj-67890") + })))) - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - transformClient: (client) => { - transformApplied = true - return client - } - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + it.effect("applies transformClient option", () => { + let transformApplied = false + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + yield* client.createResponse({ model: "gpt-4o", input: "test" }) assert.isTrue(transformApplied) - })) + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + transformApplied = true + return client + } + }))) + }) it.effect("exposes transformed HttpClient via client field", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - transformClient: (client) => - client.pipe(HttpClient.mapRequest(HttpClientRequest.setHeader("x-client-field", "enabled"))) - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - yield* client.client.execute(HttpClientRequest.get("/responses")).pipe(Effect.ignore) - - assert.isDefined(capturedRequest) - assert.isTrue(capturedRequest!.url.startsWith("https://api.openai.com/v1")) - assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer test-key") - assert.strictEqual(capturedRequest!.headers["x-client-field"], "enabled") - })) - - it.effect("applies OpenAiConfig transformClient after options transformClient", () => - Effect.gen(function*() { - let optionsTransformApplied = false - let configTransformApplied = false - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) - }) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key"), - transformClient: (client) => { - optionsTransformApplied = true - return client.pipe( - HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) - ) - } - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + const client = yield* OpenAiClient.OpenAiClient + yield* client.client.execute(HttpClientRequest.get("/responses")) + + const requests = yield* MockHttpClient.requests + const request = requests[0] + assert.isTrue(request?.url.startsWith("https://api.openai.com/v1")) + assert.strictEqual(request?.headers["authorization"], "Bearer test-key") + assert.strictEqual(request?.headers["x-client-field"], "enabled") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => + client.pipe(HttpClient.mapRequest(HttpClientRequest.setHeader("x-client-field", "enabled"))) + })))) + + it.effect("applies OpenAiConfig transformClient after options transformClient", () => { + let optionsTransformApplied = false + let configTransformApplied = false + return Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient yield* client.createResponse({ model: "gpt-4o", input: "test" @@ -258,87 +120,58 @@ describe("OpenAiClient", () => { }) ) + const requests = yield* MockHttpClient.requests assert.isTrue(optionsTransformApplied) assert.isTrue(configTransformApplied) - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["x-openai-transform"], "config") - })) + assert.strictEqual(requests[0]?.headers["x-openai-transform"], "config") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + optionsTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) + ) + } + }))) + }) }) describe("OpenAiClientGenerated", () => { it.effect("sets Bearer token from apiKey", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) - }) - - const client = yield* OpenAiClientGenerated.make({ - apiKey: Redacted.make("sk-generated-test") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - + const client = yield* OpenAiClientGenerated.OpenAiClientGenerated yield* client.createResponse({ - payload: { - model: "gpt-4o", - input: "test" - } + payload: { model: "gpt-4o", input: "test" } }) - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer sk-generated-test") - })) + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer sk-generated-test") + }).pipe(Effect.provide(makeGeneratedTestLayer({ + apiKey: Redacted.make("sk-generated-test") + })))) it.effect("prepends custom apiUrl", () => Effect.gen(function*() { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) - }) - - const client = yield* OpenAiClientGenerated.make({ - apiKey: Redacted.make("test-key"), - apiUrl: "https://generated.example.test/v2" - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - + const client = yield* OpenAiClientGenerated.OpenAiClientGenerated yield* client.createResponse({ - payload: { - model: "gpt-4o", - input: "test" - } + payload: { model: "gpt-4o", input: "test" } }) - assert.isDefined(capturedRequest) - assert.isTrue(capturedRequest!.url.startsWith("https://generated.example.test/v2")) - })) + const requests = yield* MockHttpClient.requests + assert.isTrue(requests[0]?.url.startsWith("https://generated.example.test/v2")) + }).pipe(Effect.provide(makeGeneratedTestLayer({ + apiKey: Redacted.make("test-key"), + apiUrl: "https://generated.example.test/v2" + })))) - it.effect("applies OpenAiConfig transformClient after options transformClient", () => - Effect.gen(function*() { - let optionsTransformApplied = false - let configTransformApplied = false - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - - const mockClient = makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: makeResponseBody(), request })) - }) - - const client = yield* OpenAiClientGenerated.make({ - apiKey: Redacted.make("test-key"), - transformClient: (client) => { - optionsTransformApplied = true - return client.pipe( - HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) - ) - } - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) + it.effect("applies OpenAiConfig transformClient after options transformClient", () => { + let optionsTransformApplied = false + let configTransformApplied = false + return Effect.gen(function*() { + const client = yield* OpenAiClientGenerated.OpenAiClientGenerated yield* client.createResponse({ - payload: { - model: "gpt-4o", - input: "test" - } + payload: { model: "gpt-4o", input: "test" } }).pipe( OpenAiConfig.withClientTransform((client) => { configTransformApplied = true @@ -348,40 +181,30 @@ describe("OpenAiClient", () => { }) ) + const requests = yield* MockHttpClient.requests assert.isTrue(optionsTransformApplied) assert.isTrue(configTransformApplied) - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["x-openai-transform"], "config") - })) + assert.strictEqual(requests[0]?.headers["x-openai-transform"], "config") + }).pipe(Effect.provide(makeGeneratedTestLayer({ + apiKey: Redacted.make("test-key"), + transformClient: (client) => { + optionsTransformApplied = true + return client.pipe( + HttpClient.mapRequest(HttpClientRequest.setHeader("x-openai-transform", "options")) + ) + } + }))) + }) }) describe("layer", () => { - it.effect("creates working service", () => { - const HttpClientLayer = Layer.succeed( - HttpClient.HttpClient, - makeMockHttpClient(() => Effect.succeed(makeMockResponse({ status: 200, body: {} }))) - ) - - const MainLayer = OpenAiClient.layer({ - apiKey: Redacted.make("test-key") - }).pipe(Layer.provide(HttpClientLayer)) - - return Effect.gen(function*() { + it.effect("creates working service", () => + Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient assert.isNotNull(client.client) - }).pipe(Effect.provide(MainLayer)) - }) + }).pipe(Effect.provide(makeTestLayer()))) it.effect("layerConfig loads from Config", () => { - let capturedRequest: HttpClientRequest.HttpClientRequest | undefined - const HttpClientLayer = Layer.succeed( - HttpClient.HttpClient, - makeMockHttpClient((request) => { - capturedRequest = request - return Effect.succeed(makeMockResponse({ status: 200, body: {}, request })) - }) - ) - const configProvider = ConfigProvider.fromEnv({ env: { MY_API_KEY: "sk-config-key", @@ -389,45 +212,51 @@ describe("OpenAiClient", () => { } }) - // Use explicit config values to test the layerConfig mechanism - // Provide explicit configs that won't fail for optional fields - const MainLayer = OpenAiClient.layerConfig({ - apiKey: Config.redacted("MY_API_KEY"), - apiUrl: Config.string("MY_API_URL") - }).pipe( - Layer.provide(HttpClientLayer), - Layer.provide(ConfigProvider.layer(configProvider)) - ) - return Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient - yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.ignore) + yield* client.createResponse({ model: "gpt-4o", input: "test" }) - assert.isDefined(capturedRequest) - assert.strictEqual(capturedRequest!.headers["authorization"], "Bearer sk-config-key") - assert.isTrue(capturedRequest!.url.startsWith("https://config.api.com/v1")) - }).pipe(Effect.provide(MainLayer)) + const requests = yield* MockHttpClient.requests + assert.strictEqual(requests[0]?.headers["authorization"], "Bearer sk-config-key") + assert.isTrue(requests[0]?.url.startsWith("https://config.api.com/v1")) + }).pipe(Effect.provide(makeConfigTestLayer(configProvider))) }) }) + describe("request behavior", () => { + it.effect("redacts OpenAI-specific headers in AI error context", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ + model: "gpt-4o", + input: "test" + }).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InvalidRequestError") + if (result.reason._tag !== "InvalidRequestError" || result.reason.http === undefined) { + return yield* Effect.die(new Error("Expected InvalidRequestError with HTTP context")) + } + const headers = result.reason.http.request.headers + assert.strictEqual(String(headers["authorization"]), "") + assert.strictEqual(String(headers["openai-organization"]), "") + assert.strictEqual(String(headers["openai-project"]), "") + }).pipe(Effect.provide(makeTestLayer({ + apiKey: Redacted.make("test-key"), + organizationId: Redacted.make("org-secret"), + projectId: Redacted.make("proj-secret") + }, { + _tag: "Json", + status: 400, + body: { error: { message: "Bad request" } } + })))) + }) + describe("error mapping", () => { it.effect("maps TransportError to NetworkError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient(() => - Effect.fail( - new HttpClientError.HttpClientError({ - reason: new HttpClientError.TransportError({ - request: HttpClientRequest.get("/"), - cause: new Error("Connection refused") - }) - }) - ) - ) - const client = yield* OpenAiClient.make({ apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - + }) const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( Effect.flip ) @@ -436,129 +265,88 @@ describe("OpenAiClient", () => { assert.strictEqual(result.module, "OpenAiClient") assert.strictEqual(result.method, "createResponse") assert.strictEqual(result.reason._tag, "NetworkError") - })) + }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, makeTransportErrorHttpClient())))) it.effect("maps 400 status to InvalidRequestError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 400, - body: { error: { message: "Bad request" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.module, "OpenAiClient") assert.strictEqual(result.method, "createResponse") assert.strictEqual(result.reason._tag, "InvalidRequestError") - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 400, + body: { error: { message: "Bad request" } } + })))) it.effect("maps 401 status to AuthenticationError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 401, - body: { error: { message: "Invalid API key" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "AuthenticationError") - assert.strictEqual((result.reason as AiError.AuthenticationError).kind, "InvalidKey") - })) + if (result.reason._tag === "AuthenticationError") { + assert.strictEqual(result.reason.kind, "InvalidKey") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 401, + body: { error: { message: "Invalid API key" } } + })))) it.effect("maps 403 status to AuthenticationError with InsufficientPermissions", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 403, - body: { error: { message: "Access denied" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "AuthenticationError") - assert.strictEqual((result.reason as AiError.AuthenticationError).kind, "InsufficientPermissions") - })) + if (result.reason._tag === "AuthenticationError") { + assert.strictEqual(result.reason.kind, "InsufficientPermissions") + } + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 403, + body: { error: { message: "Access denied" } } + })))) it.effect("maps 429 status to RateLimitError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 429, - body: { error: { message: "Rate limit exceeded", type: "rate_limit_error", code: null } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "RateLimitError") assert.isTrue(result.isRetryable) - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 429, + body: { error: { message: "Rate limit exceeded", type: "rate_limit_error", code: null } } + })))) it.effect("maps 429 with insufficient_quota code to QuotaExhaustedError", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 429, - body: { - error: { - message: "You exceeded your current quota", - type: "insufficient_quota", - code: "insufficient_quota" - } - }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "QuotaExhaustedError") assert.isFalse(result.isRetryable) - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 429, + body: { + error: { + message: "You exceeded your current quota", + type: "insufficient_quota", + code: "insufficient_quota" + } + } + })))) it("mapStatusCodeToReason detects insufficient_quota as QuotaExhaustedError", () => { const http = { @@ -600,66 +388,36 @@ describe("OpenAiClient", () => { it.effect("maps 5xx status to InternalProviderError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 500, - body: { error: { message: "Internal server error" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "InternalProviderError") assert.isTrue(result.isRetryable) - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 500, + body: { error: { message: "Internal server error" } } + })))) it.effect("maps schema error to InvalidOutputError reason", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 200, - body: { invalid: "response" }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - - const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe( - Effect.flip - ) + const client = yield* OpenAiClient.OpenAiClient + const result = yield* client.createResponse({ model: "gpt-4o", input: "test" }).pipe(Effect.flip) assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.method, "createResponse") assert.strictEqual(result.reason._tag, "InvalidOutputError") - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + body: { invalid: "response" } as any + })))) }) describe("createEmbedding", () => { it.effect("maps 400 error to AiError", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 400, - body: { error: { message: "Invalid model" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - + const client = yield* OpenAiClient.OpenAiClient const result = yield* client.createEmbedding({ model: "invalid-model", input: "test" @@ -668,22 +426,15 @@ describe("OpenAiClient", () => { assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.method, "createEmbedding") assert.strictEqual(result.reason._tag, "InvalidRequestError") - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 400, + body: { error: { message: "Invalid model" } } + })))) it.effect("maps 429 error to RateLimitError", () => Effect.gen(function*() { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 429, - body: { error: { message: "Rate limit exceeded" } }, - request - })) - ) - - const client = yield* OpenAiClient.make({ - apiKey: Redacted.make("test-key") - }).pipe(Effect.provide(Layer.succeed(HttpClient.HttpClient, mockClient))) - + const client = yield* OpenAiClient.OpenAiClient const result = yield* client.createEmbedding({ model: "text-embedding-ada-002", input: "test" @@ -691,47 +442,70 @@ describe("OpenAiClient", () => { assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "RateLimitError") - })) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 429, + body: { error: { message: "Rate limit exceeded" } } + })))) }) describe("createResponseStream", () => { - it.effect("accepts keepalive stream events", () => { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockStreamResponse({ - request, - events: [ - { - type: "response.created", - sequence_number: 1, - response: makeResponseBody({ - id: "resp_stream", - status: "in_progress" - }) - }, - { - type: "keepalive", - sequence_number: 2 - }, - { - type: "response.completed", - sequence_number: 3, - response: makeResponseBody({ - id: "resp_stream" - }) - } - ] - })) - ) - - const HttpClientLayer = Layer.succeed(HttpClient.HttpClient, mockClient) - - const MainLayer = OpenAiClient.layer({ - apiKey: Redacted.make("test-key") - }).pipe(Layer.provide(HttpClientLayer)) - - return Effect.gen(function*() { + it.live("terminates an SSE stream at response.failed", () => + Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient + const [, stream] = yield* client.createResponseStream({ model: "gpt-4o", input: "test" }) + const result = yield* Stream.runCollect(stream).pipe(Effect.timeoutOption("100 millis")) + + assert.strictEqual(result._tag, "Some") + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [{ + type: "response.failed", + sequence_number: 1, + response: makeResponseBody({ status: "failed" }) + }], + keepOpen: true + })))) + + it.live("terminates a WebSocket stream at response.failed", () => + Effect.gen(function*() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => new WS("wss://api.openai.com/v1/responses", { jsonProtocol: true })), + (server) => + Effect.sync(() => { + server.close() + WS.clean() + }) + ) + const event = { + type: "response.failed", + sequence_number: 1, + response: makeResponseBody({ status: "failed" }) + } + const result = yield* OpenAiClient.withWebSocketMode( + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient + const [, stream] = yield* client.createResponseStream({ model: "gpt-4o", input: "test" }) + const [events] = yield* Effect.all([ + Stream.runCollect(stream), + Effect.promise(() => server.nextMessage).pipe( + Effect.tap(() => Effect.sync(() => server.send(event))) + ) + ], { concurrency: "unbounded" }) + return events + }) + ).pipe( + Effect.provide(makeTestLayer()), + Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)), + Effect.timeoutOption("1 second") + ) + assert.strictEqual(result._tag, "Some") + })) + + it.effect("accepts keepalive stream events", () => + Effect.gen(function*() { + const client = yield* OpenAiClient.OpenAiClient const [_, stream] = yield* client.createResponseStream({ model: "gpt-4o", input: "test" @@ -761,27 +535,29 @@ describe("OpenAiClient", () => { assert.strictEqual(completed.response.id, "resp_stream") } } - }).pipe(Effect.provide(MainLayer)) - }) - - it.effect("maps HTTP error before stream starts", () => { - const mockClient = makeMockHttpClient((request) => - Effect.succeed(makeMockResponse({ - status: 500, - body: { error: { message: "Server error" } }, - request - })) - ) - - const HttpClientLayer = Layer.succeed(HttpClient.HttpClient, mockClient) - - const MainLayer = OpenAiClient.layer({ - apiKey: Redacted.make("test-key") - }).pipe(Layer.provide(HttpClientLayer)) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Sse", + events: [ + { + type: "response.created", + sequence_number: 1, + response: makeResponseBody({ id: "resp_stream", status: "in_progress" }) + }, + { + type: "keepalive", + sequence_number: 2 + }, + { + type: "response.completed", + sequence_number: 3, + response: makeResponseBody({ id: "resp_stream" }) + } + ] + })))) - return Effect.gen(function*() { + it.effect("maps HTTP error before stream starts", () => + Effect.gen(function*() { const client = yield* OpenAiClient.OpenAiClient - const result = yield* client.createResponseStream({ model: "gpt-4o", input: "test" @@ -792,7 +568,173 @@ describe("OpenAiClient", () => { assert.strictEqual(result._tag, "AiError") assert.strictEqual(result.reason._tag, "InternalProviderError") - }).pipe(Effect.provide(MainLayer)) - }) + }).pipe(Effect.provide(makeTestLayer(undefined, { + _tag: "Json", + status: 500, + body: { error: { message: "Server error" } } + })))) }) }) + +type MockResponse = + | { + readonly _tag: "Json" + readonly body: typeof Generated.Response.Encoded | { + readonly error: { + readonly message: string + readonly type?: string + readonly code?: string | null + } + } + readonly status?: number | undefined + readonly headers?: Record | undefined + } + | { + readonly _tag: "Sse" + readonly events: ReadonlyArray + readonly keepOpen?: boolean | undefined + readonly status?: number | undefined + readonly headers?: Record | undefined + } + +class MockOpenAiResponse extends Context.Service()("MockOpenAiResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = MockHttpClient.use((client) => client.requests) +} + +const makeHttpClientContext = Effect.gen(function*() { + const capturedRequests: Array = [] + const mock = yield* MockOpenAiResponse + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + capturedRequests.push(request) + return makeResponse(request, mock.response) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + const mockHttpClient: MockHttpClient["Service"] = { + requests: Effect.sync(() => capturedRequests) + } + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, mockHttpClient) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClientContext) + +const defaultResponse: MockResponse = { + _tag: "Json", + body: makeResponseBody() +} + +const makeTestLayer = ( + options: OpenAiClient.Options = { apiKey: Redacted.make("test-key") }, + response: MockResponse = defaultResponse +) => + OpenAiClient.layer(options).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { response })) + ) + +const makeGeneratedTestLayer = ( + options: OpenAiClientGenerated.Options, + response: MockResponse = defaultResponse +) => + OpenAiClientGenerated.layer(options).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { response })) + ) + +const makeConfigTestLayer = (configProvider: ConfigProvider.ConfigProvider) => + OpenAiClient.layerConfig({ + apiKey: Config.redacted("MY_API_KEY"), + apiUrl: Config.string("MY_API_URL") + }).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenAiResponse, { response: defaultResponse })), + Layer.provide(ConfigProvider.layer(configProvider)) + ) + +const makeTransportErrorHttpClient = (): HttpClient.HttpClient => + HttpClient.makeWith( + (requestEffect) => + Effect.flatMap(requestEffect, (request) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ + request, + cause: new Error("Connection refused") + }) + }) + )), + Effect.succeed + ) + +function makeResponseBody( + overrides: Partial = {} +): typeof Generated.Response.Encoded { + return { + id: "resp_test123", + object: "response", + created_at: 1, + model: "gpt-4o-mini", + status: "completed", + output: [], + metadata: null, + temperature: null, + top_p: null, + tools: [], + tool_choice: "auto", + error: null, + incomplete_details: null, + instructions: null, + parallel_tool_calls: false, + ...overrides + } +} + +const makeResponse = ( + request: HttpClientRequest.HttpClientRequest, + response: MockResponse +): HttpClientResponse.HttpClientResponse => { + const contentType = response._tag === "Json" + ? "application/json" + : "text/event-stream" + const body = response._tag === "Json" + ? JSON.stringify(response.body) + : response.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + + const httpResponse = HttpClientResponse.fromWeb( + request, + new Response(body, { + status: response.status ?? 200, + headers: { + "content-type": contentType, + ...response.headers + } + }) + ) + if (response._tag !== "Sse" || response.keepOpen !== true) return httpResponse + + const stream = Stream.concat( + Stream.succeed(new TextEncoder().encode(body)), + Stream.never + ) + // `fromWeb` stores the ReadableStream internally, so a proxy is needed to replace it with a non-terminating test stream. + return new Proxy(httpResponse, { + get(target, property) { + if (property === "stream") return stream + const value = Reflect.get(target, property, target) + return typeof value === "function" ? value.bind(target) : value + } + }) +} diff --git a/.context/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts b/.context/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts index c39b968bf..711733d66 100644 --- a/.context/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts +++ b/.context/effect/packages/ai/openai/test/OpenAiLanguageModel.test.ts @@ -2,7 +2,7 @@ import { type Generated, OpenAiClient, OpenAiLanguageModel, OpenAiSchema, OpenAi import { assert, describe, it } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Array, Context, Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" -import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" +import { LanguageModel, Prompt, Response as AiResponse, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" describe("OpenAiLanguageModel", () => { @@ -46,7 +46,10 @@ describe("OpenAiLanguageModel", () => { const systemMessage = body.input.find((m: any) => m.role === "system") assert.isDefined(systemMessage) - strictEqual(systemMessage.content, "You are a helpful assistant") + deepStrictEqual(systemMessage.content, [{ + type: "input_text", + text: "You are a helpful assistant" + }]) }).pipe(Effect.provide(makeTestLayer()))) it.effect("uses developer role for reasoning models", () => @@ -63,7 +66,10 @@ describe("OpenAiLanguageModel", () => { const devMessage = body.input.find((m: any) => m.role === "developer") assert.isDefined(devMessage) - strictEqual(devMessage.content, "You are a helpful assistant") + deepStrictEqual(devMessage.content, [{ + type: "input_text", + text: "You are a helpful assistant" + }]) }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } })))) it.effect("uses developer role for gpt-5 models", () => @@ -313,6 +319,45 @@ describe("OpenAiLanguageModel", () => { strictEqual(reasoningItem.id, "reasoning_123") }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } })))) + it.effect("replays encrypted reasoning from response parts", () => + Effect.gen(function*() { + const history = Prompt.fromResponseParts([ + AiResponse.makePart("reasoning-start", { + id: "reasoning_123:0", + metadata: { openai: { itemId: "reasoning_123" } } + }), + AiResponse.makePart("reasoning-delta", { + id: "reasoning_123:0", + delta: "Let me think..." + }), + AiResponse.makePart("reasoning-end", { + id: "reasoning_123:0", + metadata: { + openai: { + itemId: "reasoning_123", + encryptedContent: "encrypted-reasoning" + } + } + }) + ]) + + yield* LanguageModel.generateText({ + prompt: Prompt.concat(history, Prompt.make("Continue")) + }).pipe(Effect.provide(OpenAiLanguageModel.model("o1"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const reasoningItem = body.input.find((item: any) => item.type === "reasoning") + + assert.isDefined(reasoningItem) + deepStrictEqual(reasoningItem, { + type: "reasoning", + id: "reasoning_123", + summary: [{ type: "summary_text", text: "Let me think..." }], + encrypted_content: "encrypted-reasoning" + }) + }).pipe(Effect.provide(makeTestLayer({ body: { model: "o1" } })))) + it.effect("converts tool call parts to function_call", () => Effect.gen(function*() { yield* LanguageModel.generateText({ @@ -336,7 +381,8 @@ describe("OpenAiLanguageModel", () => { id: "call_abc", name: "TestTool", isFailure: false, - result: { output: "result" } + result: { output: "result" }, + providerExecuted: false }) ] } @@ -378,7 +424,8 @@ describe("OpenAiLanguageModel", () => { id: "call_abc", name: "TestTool", isFailure: false, - result: { output: "result" } + result: { output: "result" }, + providerExecuted: false }) ] } @@ -394,6 +441,144 @@ describe("OpenAiLanguageModel", () => { strictEqual(toolOutput.call_id, "call_abc") strictEqual(toolOutput.output, JSON.stringify({ output: "result" })) }).pipe(Effect.provide([makeTestLayer(), TestToolkitLayer]))) + + it.effect("emits only the specialized output for apply_patch results", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.ApplyPatch({})) + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Apply a patch" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_apply_patch", + name: "OpenAiApplyPatch", + params: { + call_id: "call_apply_patch", + operation: { type: "delete_file", path: "old.ts" } + }, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_apply_patch", + name: "OpenAiApplyPatch", + isFailure: false, + result: { status: "completed", output: "deleted" }, + providerExecuted: false + })] + } + ]), + toolkit, + disableToolCallResolution: true + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const outputs = body.input.filter((item: any) => + item.call_id === "call_apply_patch" && item.type.endsWith("_output") + ) + + deepStrictEqual(outputs.map((item: any) => item.type), ["apply_patch_call_output"]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("emits only the specialized output for shell results", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.Shell({})) + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Run a shell command" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_shell", + name: "OpenAiShell", + params: { + action: { + commands: ["echo hello"], + timeout_ms: null, + max_output_length: null + } + }, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_shell", + name: "OpenAiShell", + isFailure: false, + result: { + output: [{ + stdout: "hello\n", + stderr: "", + outcome: { type: "exit", exit_code: 0 } + }] + }, + providerExecuted: false + })] + } + ]), + toolkit, + disableToolCallResolution: true + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const outputs = body.input.filter((item: any) => + item.call_id === "call_shell" && item.type.endsWith("_output") + ) + + deepStrictEqual(outputs.map((item: any) => item.type), ["shell_call_output"]) + }).pipe(Effect.provide(makeTestLayer()))) + + it.effect("emits only the specialized output for local_shell results", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.LocalShell({})) + yield* LanguageModel.generateText({ + prompt: Prompt.make([ + { role: "user", content: "Run a local shell command" }, + { + role: "assistant", + content: [Prompt.toolCallPart({ + id: "call_local_shell", + name: "OpenAiLocalShell", + params: { + action: { + type: "exec", + command: ["echo", "hello"], + env: {} + } + }, + providerExecuted: false + })] + }, + { + role: "tool", + content: [Prompt.toolResultPart({ + id: "call_local_shell", + name: "OpenAiLocalShell", + isFailure: false, + result: { output: "hello\n" }, + providerExecuted: false + })] + } + ]), + toolkit, + disableToolCallResolution: true + }).pipe(Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini"))) + + const requests = yield* MockHttpClient.requests + const body = yield* getRequestBody(requests[0]) + const outputs = body.input.filter((item: any) => + item.call_id === "call_local_shell" && item.type.endsWith("_output") + ) + + deepStrictEqual(outputs.map((item: any) => item.type), ["local_shell_call_output"]) + }).pipe(Effect.provide(makeTestLayer()))) }) }) @@ -746,6 +931,38 @@ describe("OpenAiLanguageModel", () => { } })))) + it.each(["gpt-4.1", "gpt-5.6"] as const)( + "maps stable web search action to tool call parameters with %s", + (model) => + Effect.runPromise( + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.WebSearch({})) + const result = yield* LanguageModel.generateText({ + prompt: "Search the web", + toolkit + }).pipe(Effect.provide(OpenAiLanguageModel.model(model))) + + const toolCall = result.content.find((part) => part.type === "tool-call") + assert.isDefined(toolCall) + assert.deepStrictEqual(toolCall.params, { + action: { type: "search", query: "Effect TypeScript" } + }) + + const toolResult = result.content.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + assert.deepStrictEqual(toolResult.result, { + action: { type: "search", query: "Effect TypeScript" }, + status: "completed" + }) + }).pipe(Effect.provide(makeTestLayer({ + body: { + model, + output: [makeWebSearchCall()] + } + }))) + ) + ) + it.effect("uses canonical OpenAiMcp name for mcp_approval_request", () => Effect.gen(function*() { const result = yield* LanguageModel.generateText({ @@ -1093,6 +1310,59 @@ describe("OpenAiLanguageModel", () => { assert.isDefined(toolParamsEnd) })) + it.effect("waits for the stable streamed web search action before emitting the tool call", () => + Effect.gen(function*() { + const toolkit = Toolkit.make(OpenAiTool.WebSearch({})) + const streamEvents = [ + { + type: "response.created", + sequence_number: 1, + response: makeDefaultResponse({ status: "in_progress" }) + }, + { + type: "response.output_item.added", + sequence_number: 2, + output_index: 0, + item: { + type: "web_search_call", + id: "ws_123", + status: "in_progress" + } + }, + { + type: "response.output_item.done", + sequence_number: 3, + output_index: 0, + item: makeWebSearchCall() + } + ] as unknown as ReadonlyArray + + const parts = yield* LanguageModel.streamText({ + prompt: "Search the web", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(makeStreamTestLayer(streamEvents)) + ) + + const toolCalls = parts.filter((part) => part.type === "tool-call") + strictEqual(toolCalls.length, 1) + const toolCall = toolCalls[0] + assert.isDefined(toolCall) + assert.deepStrictEqual(toolCall.params, { + action: { type: "search", query: "Effect TypeScript" } + }) + + const toolResult = parts.find((part) => part.type === "tool-result") + assert.isDefined(toolResult) + assert.deepStrictEqual(toolResult.result, { + action: { type: "search", query: "Effect TypeScript" }, + status: "completed" + }) + })) + it.effect("handles reasoning summary events when reasoning state is missing", () => Effect.gen(function*() { const streamEvents = [ @@ -1506,6 +1776,16 @@ const makeFunctionCall = ( ...overrides }) +const makeWebSearchCall = ( + overrides: Partial = {} +): Generated.WebSearchToolCall => ({ + type: "web_search_call", + id: "ws_123", + status: "completed", + action: { type: "search", query: "Effect TypeScript" }, + ...overrides +}) + const makeMcpCall = ( name: string, args: Record, diff --git a/.context/effect/packages/ai/openai/test/OpenAiSchema.test.ts b/.context/effect/packages/ai/openai/test/OpenAiSchema.test.ts index 70a30f22c..26e56c4ac 100644 --- a/.context/effect/packages/ai/openai/test/OpenAiSchema.test.ts +++ b/.context/effect/packages/ai/openai/test/OpenAiSchema.test.ts @@ -1,7 +1,10 @@ +import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai" import * as OpenAiSchema from "@effect/ai-openai/OpenAiSchema" import { assert, describe, it } from "@effect/vitest" -import { Effect, Schema, Stream } from "effect" +import { Effect, Layer, Schema, Stream } from "effect" +import { LanguageModel } from "effect/unstable/ai" import * as Sse from "effect/unstable/encoding/Sse" +import { HttpClientRequest, HttpClientResponse } from "effect/unstable/http" const makeResponse = (overrides: Record = {}) => ({ id: "resp_123", @@ -77,6 +80,25 @@ describe("OpenAiSchema", () => { } }) + it("decodes image generation lifecycle statuses", () => { + for (const status of ["generating", "failed"]) { + const decoded = Schema.decodeUnknownSync(OpenAiSchema.Response)({ + ...makeResponse(), + output: [{ + id: "image_1", + type: "image_generation_call", + status, + result: null + }] + }) + + assert.strictEqual(decoded.output[0].type, "image_generation_call") + if (decoded.output[0].type === "image_generation_call") { + assert.strictEqual(decoded.output[0].status, status) + } + } + }) + it("decodes required stream events", () => { const response = makeResponse({ status: "in_progress" }) const applyPatchItem = { @@ -270,4 +292,53 @@ describe("OpenAiSchema", () => { assert.strictEqual(numeric.data[0].embedding[0], 0.1) assert.strictEqual(base64.data[0].embedding, "AQID") }) + + it.effect("exposes the response.failed error payload", () => { + const response = HttpClientResponse.fromWeb( + HttpClientRequest.get("https://api.openai.com/v1/responses"), + new Response() + ) + const failed = Schema.decodeUnknownSync(OpenAiSchema.ResponseStreamEvent)({ + type: "response.failed", + sequence_number: 1, + response: { + id: "resp_1", + object: "response", + created_at: 1, + model: "gpt-4o-mini", + status: "failed", + output: [], + error: { code: "server_error", message: "provider exploded" } + } + }) + const client = Layer.succeed( + OpenAiClient.OpenAiClient, + OpenAiClient.OpenAiClient.of({ + client: undefined as any, + createResponse: () => Effect.die("unexpected"), + createResponseStream: () => + Effect.succeed([ + response, + Stream.make(failed) + ]), + createEmbedding: () => Effect.die("unexpected") + }) + ) + + return LanguageModel.streamText({ prompt: "test" }).pipe( + Stream.runCollect, + Effect.tap((parts) => + Effect.sync(() => { + const error = Array.from(parts).find((part) => part.type === "error") + const finish = Array.from(parts).find((part) => part.type === "finish") + assert.isDefined(error) + assert.deepStrictEqual(error.error, { code: "server_error", message: "provider exploded" }) + assert.isDefined(finish) + assert.strictEqual(finish.reason, "error") + }) + ), + Effect.provide(OpenAiLanguageModel.model("gpt-4o-mini")), + Effect.provide(client) + ) + }) }) diff --git a/.context/effect/packages/ai/openai/tsconfig.json b/.context/effect/packages/ai/openai/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/ai/openai/tsconfig.json +++ b/.context/effect/packages/ai/openai/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/ai/openai/vitest.config.ts b/.context/effect/packages/ai/openai/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/ai/openai/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/ai/openrouter/CHANGELOG.md b/.context/effect/packages/ai/openrouter/CHANGELOG.md index 25137910e..434927d5f 100644 --- a/.context/effect/packages/ai/openrouter/CHANGELOG.md +++ b/.context/effect/packages/ai/openrouter/CHANGELOG.md @@ -1,5 +1,77 @@ # @effect/ai-openrouter +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7128](https://github.com/Effect-TS/effect/pull/7128) [`69756a2`](https://github.com/Effect-TS/effect/commit/69756a2290c11bb144b31240de29f39b8cb42c05) Thanks @fubhy! - Preserve start and end offsets for streamed OpenRouter citations. + +- [#7133](https://github.com/Effect-TS/effect/pull/7133) [`d6a4a9c`](https://github.com/Effect-TS/effect/commit/d6a4a9cef496c38663d3977a6a5796a33a9b19b7) Thanks @fubhy! - Fix the casing of OpenRouter reasoning-end metadata. + +- [#7132](https://github.com/Effect-TS/effect/pull/7132) [`3f01731`](https://github.com/Effect-TS/effect/commit/3f01731a09ebf5c53e9bede851ae8f8f28f1a5e8) Thanks @fubhy! - Emit incremental tool parameter fragments from OpenRouter streaming responses. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- [#6572](https://github.com/Effect-TS/effect/pull/6572) [`c0f9fc9`](https://github.com/Effect-TS/effect/commit/c0f9fc9fda8af8ac138b23293876e78bd052ab00) Thanks @tim-smart! - Regenerate the `Generated` module against OpenRouter's current published specification. This preserves nullable + generation statistics and streamed usage cost metadata while incorporating the broader upstream schema changes. + + Notable generated schema renames include `ChatGenerationParams` to `ChatRequest`, `ChatGenerationTokenUsage` to + `ChatUsage`, `AssistantMessage` to `ChatAssistantMessage`, `ChatStreamingResponseChunk` to `ChatStreamingResponse`, + and `ChatMessageContentItemCacheControl` to `ChatContentCacheControl`. Handwritten public aliases such as + `ChatStreamingResponseChunkData`, `ReasoningDetails`, and `FileAnnotation` retain their existing names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/ai/openrouter/README.md b/.context/effect/packages/ai/openrouter/README.md new file mode 100644 index 000000000..9fa56dcee --- /dev/null +++ b/.context/effect/packages/ai/openrouter/README.md @@ -0,0 +1,14 @@ +# @effect/ai-openrouter + +An [OpenRouter](https://openrouter.ai) provider for the Effect AI modules. Includes a typed OpenRouter API client and language model layers. + +## Installation + +```sh +npm install effect@beta @effect/ai-openrouter@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/ai-openrouter) diff --git a/.context/effect/packages/ai/openrouter/codegen.yml b/.context/effect/packages/ai/openrouter/codegen.yml index b556693a3..7fea7aae9 100644 --- a/.context/effect/packages/ai/openrouter/codegen.yml +++ b/.context/effect/packages/ai/openrouter/codegen.yml @@ -10,26 +10,25 @@ excludeAnnotations: - default disableAdditionalProperties: true patches: - # Replace OpenResponsesStreamEvent with a flat oneOf of $refs. - # The original uses allOf wrappers that inline OpenResponsesNonStreamingResponse - # into each variant, producing lines over 1M chars that crash dprint. - - '[{"op":"replace","path":"/components/schemas/OpenResponsesStreamEvent","value":{"oneOf":[{"$ref":"#/components/schemas/OpenResponsesCreatedEvent"},{"$ref":"#/components/schemas/OpenResponsesInProgressEvent"},{"$ref":"#/components/schemas/OpenResponsesCompletedEvent"},{"$ref":"#/components/schemas/OpenResponsesIncompleteEvent"},{"$ref":"#/components/schemas/OpenResponsesFailedEvent"},{"$ref":"#/components/schemas/OpenResponsesErrorEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputItemAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputItemDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesContentPartAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesContentPartDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesTextDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesTextDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesRefusalDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesRefusalDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesOutputTextAnnotationAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesFunctionCallArgumentsDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesFunctionCallArgumentsDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryPartAddedEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryPartDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryTextDeltaEvent"},{"$ref":"#/components/schemas/OpenResponsesReasoningSummaryTextDoneEvent"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallInProgress"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallGenerating"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallPartialImage"},{"$ref":"#/components/schemas/OpenResponsesImageGenCallCompleted"}]}}]' - # Fix AssistantMessage images to include type:"image_url" discriminator and nullable - - '[{"op":"replace","path":"/components/schemas/AssistantMessage/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' - # Add images to ChatStreamingMessageChunk (streaming delta) - - '[{"op":"add","path":"/components/schemas/ChatStreamingMessageChunk/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' - # Add annotations to AssistantMessage (non-streaming) - - '[{"op":"add","path":"/components/schemas/AssistantMessage/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' - # Add annotations to ChatStreamingMessageChunk (streaming delta) - - '[{"op":"add","path":"/components/schemas/ChatStreamingMessageChunk/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' + # Flatten Responses streaming event allOf wrappers to keep generated schemas referential + - '[{"op":"replace","path":"/components/schemas/OpenResponsesCreatedEvent","value":{"type":"object","description":"Event emitted when a response is created","properties":{"response":{"$ref":"#/components/schemas/OpenResponsesResult"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.created"]}},"required":["type","response","sequence_number"]}},{"op":"replace","path":"/components/schemas/OpenResponsesInProgressEvent","value":{"type":"object","description":"Event emitted when a response is in progress","properties":{"response":{"$ref":"#/components/schemas/OpenResponsesResult"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.in_progress"]}},"required":["type","response","sequence_number"]}},{"op":"replace","path":"/components/schemas/StreamEventsResponseCompleted","value":{"type":"object","description":"Event emitted when a response has completed successfully","properties":{"response":{"$ref":"#/components/schemas/OpenResponsesResult"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.completed"]}},"required":["type","response","sequence_number"]}},{"op":"replace","path":"/components/schemas/StreamEventsResponseIncomplete","value":{"type":"object","description":"Event emitted when a response is incomplete","properties":{"response":{"$ref":"#/components/schemas/OpenResponsesResult"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.incomplete"]}},"required":["type","response","sequence_number"]}},{"op":"replace","path":"/components/schemas/StreamEventsResponseFailed","value":{"type":"object","description":"Event emitted when a response has failed","properties":{"response":{"$ref":"#/components/schemas/OpenResponsesResult"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.failed"]}},"required":["type","response","sequence_number"]}},{"op":"replace","path":"/components/schemas/StreamEventsResponseOutputItemAdded","value":{"type":"object","description":"Event emitted when a new output item is added to the response","properties":{"item":{"$ref":"#/components/schemas/OutputItems"},"output_index":{"type":"integer"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.output_item.added"]}},"required":["type","output_index","item","sequence_number"]}},{"op":"replace","path":"/components/schemas/StreamEventsResponseOutputItemDone","value":{"type":"object","description":"Event emitted when an output item is complete","properties":{"item":{"$ref":"#/components/schemas/OutputItems"},"output_index":{"type":"integer"},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.output_item.done"]}},"required":["type","output_index","item","sequence_number"]}},{"op":"replace","path":"/components/schemas/ContentPartAddedEvent","value":{"type":"object","description":"Event emitted when a new content part is added to an output item","properties":{"content_index":{"type":"integer"},"item_id":{"type":"string"},"output_index":{"type":"integer"},"part":{"anyOf":[{"$ref":"#/components/schemas/ResponseOutputText"},{"$ref":"#/components/schemas/ReasoningTextContent"},{"$ref":"#/components/schemas/OpenAIResponsesRefusalContent"}]},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.content_part.added"]}},"required":["type","output_index","item_id","content_index","part","sequence_number"]}},{"op":"replace","path":"/components/schemas/ContentPartDoneEvent","value":{"type":"object","description":"Event emitted when a content part is complete","properties":{"content_index":{"type":"integer"},"item_id":{"type":"string"},"output_index":{"type":"integer"},"part":{"anyOf":[{"$ref":"#/components/schemas/ResponseOutputText"},{"$ref":"#/components/schemas/ReasoningTextContent"},{"$ref":"#/components/schemas/OpenAIResponsesRefusalContent"}]},"sequence_number":{"type":"integer"},"type":{"type":"string","enum":["response.content_part.done"]}},"required":["type","output_index","item_id","content_index","part","sequence_number"]}}]' + # Fix ChatAssistantMessage images to include type:"image_url" discriminator and nullable + - '[{"op":"replace","path":"/components/schemas/ChatAssistantMessage/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' + # Add images to ChatStreamDelta (streaming delta) + - '[{"op":"add","path":"/components/schemas/ChatStreamDelta/properties/images","value":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","const":"image_url"},"image_url":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}},"required":["type","image_url"]}},{"type":"null"}]}}]' + # Add annotations to ChatAssistantMessage (non-streaming) + - '[{"op":"add","path":"/components/schemas/ChatAssistantMessage/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' + # Add annotations to ChatStreamDelta (streaming delta) + - '[{"op":"add","path":"/components/schemas/ChatStreamDelta/properties/annotations","value":{"anyOf":[{"type":"array","items":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","const":"url_citation"},"url_citation":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"start_index":{"type":"number"},"end_index":{"type":"number"},"content":{"type":"string"}},"required":["url"]}},"required":["type","url_citation"]},{"type":"object","properties":{"type":{"type":"string","const":"file_annotation"},"file_annotation":{"type":"object","properties":{"file_id":{"type":"string"},"quote":{"type":"string"}},"required":["file_id"]}},"required":["type","file_annotation"]},{"type":"object","properties":{"type":{"type":"string","const":"file"},"file":{"type":"object","properties":{"hash":{"type":"string"},"name":{"type":"string"},"content":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"]}}},"required":["hash","name"]}},"required":["type","file"]}]}},{"type":"null"}]}}]' # Make tool call delta fields nullable (models like kimi-k2.5, minimax-m2.5 send null) - - '[{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/id","value":{"anyOf":[{"type":"string"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/type","value":{"anyOf":[{"type":"string","const":"function"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamingMessageToolCall/properties/function/properties/name","value":{"anyOf":[{"type":"string"},{"type":"null"}]}}]' + - '[{"op":"replace","path":"/components/schemas/ChatStreamToolCall/properties/id","value":{"anyOf":[{"type":"string"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamToolCall/properties/type","value":{"anyOf":[{"type":"string","const":"function"},{"type":"null"}]}},{"op":"replace","path":"/components/schemas/ChatStreamToolCall/properties/function/properties/name","value":{"anyOf":[{"type":"string"},{"type":"null"}]}}]' # Make finish_reason optional (only present on final streaming chunk) - - '[{"op":"remove","path":"/components/schemas/ChatStreamingChoice/required/1"}]' + - '[{"op":"remove","path":"/components/schemas/ChatStreamChoice/required/1"}]' replacements: - # Schema.Unknown doesn't work with Schema.toCodecJson (used by HttpClientResponse.schemaBodyJson) - # Replace with Schema.Json which properly handles arbitrary JSON values - - from: "Schema.Record(Schema.String, Schema.Unknown)" - to: "Schema.Record(Schema.String, Schema.Json)" - - from: "{ readonly [x: string]: unknown }" - to: "{ readonly [x: string]: Schema.Json }" + # Help TypeScript serialize oversized generated schema values + - from: "export const StreamEvents = Schema.Union([" + to: "export const StreamEvents: Schema.Schema = Schema.Union([" + - from: "export const ResponsesStreamingResponse = Schema.Struct({ \"data\": StreamEvents })" + to: "export const ResponsesStreamingResponse: Schema.Schema = Schema.Struct({ \"data\": StreamEvents })" + - from: "export const CreateResponses200Sse = ResponsesStreamingResponse" + to: "export const CreateResponses200Sse: Schema.Schema = ResponsesStreamingResponse" diff --git a/.context/effect/packages/ai/openrouter/docgen.json b/.context/effect/packages/ai/openrouter/docgen.json deleted file mode 100644 index 0dd5cf80a..000000000 --- a/.context/effect/packages/ai/openrouter/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/ai/openrouter/src/", - "exclude": ["src/Generated.ts", "src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/ai/openrouter/package.json b/.context/effect/packages/ai/openrouter/package.json index 669a3bb92..60c68bef3 100644 --- a/.context/effect/packages/ai/openrouter/package.json +++ b/.context/effect/packages/ai/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@effect/ai-openrouter", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "An OpenRouter provider integration for Effect AI SDK", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/ai/openrouter/src/Generated.ts b/.context/effect/packages/ai/openrouter/src/Generated.ts index c015b4429..4c3859a37 100644 --- a/.context/effect/packages/ai/openrouter/src/Generated.ts +++ b/.context/effect/packages/ai/openrouter/src/Generated.ts @@ -1,5 +1,5 @@ /** - * @since 4.0.0 + * @since 1.0.0 */ import * as Data from "effect/Data" @@ -13,2382 +13,3371 @@ import * as HttpClientError from "effect/unstable/http/HttpClientError" import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" // non-recursive definitions -export type OpenAIResponsesResponseStatus = - | "completed" - | "incomplete" - | "in_progress" - | "failed" - | "cancelled" - | "queued" -export const OpenAIResponsesResponseStatus = Schema.Literals([ - "completed", - "incomplete", - "in_progress", - "failed", - "cancelled", - "queued" -]) -export type FileCitation = { - readonly "type": "file_citation" +export type AABenchmarkEntry = { + readonly "agentic_index": number | null + readonly "coding_index": number | null + readonly "intelligence_index": number | null +} +export const AABenchmarkEntry = Schema.Struct({ + "agentic_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Artificial Analysis Agentic Index score", "format": "double" }), + "coding_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Artificial Analysis Coding Index score", "format": "double" }), + "intelligence_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Artificial Analysis Intelligence Index score", "format": "double" }) +}).annotate({ "description": "Artificial Analysis benchmark index scores.", "identifier": "AABenchmarkEntry" }) +export type ActivityItem = { + readonly "byok_usage_inference": number + readonly "completion_tokens": number + readonly "date": string + readonly "endpoint_id": string + readonly "model": string + readonly "model_permaslug": string + readonly "prompt_tokens": number + readonly "provider_name": string + readonly "reasoning_tokens": number + readonly "requests": number + readonly "usage": number +} +export const ActivityItem = Schema.Struct({ + "byok_usage_inference": Schema.Number.annotate({ + "description": "BYOK inference cost in USD (external credits spent)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "completion_tokens": Schema.Number.annotate({ "description": "Total completion tokens generated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "date": Schema.String.annotate({ "description": "Date of the activity (YYYY-MM-DD format)" }), + "endpoint_id": Schema.String.annotate({ "description": "Unique identifier for the endpoint" }), + "model": Schema.String.annotate({ "description": "Model slug (e.g., \"openai/gpt-4.1\")" }), + "model_permaslug": Schema.String.annotate({ "description": "Model permaslug (e.g., \"openai/gpt-4.1-2025-04-14\")" }), + "prompt_tokens": Schema.Number.annotate({ "description": "Total prompt tokens used" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "provider_name": Schema.String.annotate({ "description": "Name of the provider serving this endpoint" }), + "reasoning_tokens": Schema.Number.annotate({ "description": "Total reasoning tokens used" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "requests": Schema.Number.annotate({ "description": "Number of requests made" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "usage": Schema.Number.annotate({ "description": "Total cost in USD (OpenRouter credits spent)", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ "identifier": "ActivityItem" }) +export type AdvisorNestedTool = { readonly "parameters"?: {}; readonly "type": string } +export const AdvisorNestedTool = Schema.Struct({ + "parameters": Schema.optionalKey(Schema.Struct({})), + "type": Schema.String +}).annotate({ + "description": + "A tool made available to the advisor sub-agent. Only OpenRouter server tools (e.g. openrouter:web_search) are supported; function tools are rejected because the advisor has no way to execute them. The advisor tool may not list itself.", + "identifier": "AdvisorNestedTool" +}) +export type AdvisorReasoning = { + readonly "effort"?: "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" + readonly "max_tokens"?: number +} +export const AdvisorReasoning = Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Literals(["max", "xhigh", "high", "medium", "low", "minimal", "none"]).annotate({ + "description": "Reasoning effort level for the advisor call." + }) + ), + "max_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of reasoning tokens the advisor may use." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) +}).annotate({ + "description": + "Reasoning configuration forwarded to the advisor call. Use this to control reasoning effort and token budget for models that support extended thinking.", + "identifier": "AdvisorReasoning" +}) +export type AnthropicAdvisorToolResult = { + readonly "content": {} + readonly "tool_use_id": string + readonly "type": "advisor_tool_result" +} +export const AnthropicAdvisorToolResult = Schema.Struct({ + "content": Schema.Struct({}), + "tool_use_id": Schema.String, + "type": Schema.Literal("advisor_tool_result") +}).annotate({ "identifier": "AnthropicAdvisorToolResult" }) +export type AnthropicAllowedCallers = ReadonlyArray<"direct" | "code_execution_20250825" | "code_execution_20260120"> +export const AnthropicAllowedCallers = Schema.Array( + Schema.Literals(["direct", "code_execution_20250825", "code_execution_20260120"]) +).annotate({ "identifier": "AnthropicAllowedCallers" }) +export type AnthropicBase64PdfSource = { + readonly "data": string + readonly "media_type": "application/pdf" + readonly "type": "base64" +} +export const AnthropicBase64PdfSource = Schema.Struct({ + "data": Schema.String, + "media_type": Schema.Literal("application/pdf"), + "type": Schema.Literal("base64") +}).annotate({ "identifier": "AnthropicBase64PdfSource" }) +export type AnthropicBashCodeExecutionOutput = { readonly "file_id": string - readonly "filename": string - readonly "index": number + readonly "type": "bash_code_execution_output" } -export const FileCitation = Schema.Struct({ - "type": Schema.Literal("file_citation"), +export const AnthropicBashCodeExecutionOutput = Schema.Struct({ "file_id": Schema.String, - "filename": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) + "type": Schema.Literal("bash_code_execution_output") +}).annotate({ "identifier": "AnthropicBashCodeExecutionOutput" }) +export type AnthropicBashCodeExecutionToolResultError = { + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "output_file_too_large" + readonly "type": "bash_code_execution_tool_result_error" +} +export const AnthropicBashCodeExecutionToolResultError = Schema.Struct({ + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "output_file_too_large" + ]), + "type": Schema.Literal("bash_code_execution_tool_result_error") +}).annotate({ "identifier": "AnthropicBashCodeExecutionToolResultError" }) +export type AnthropicCacheControlTtl = "5m" | "1h" +export const AnthropicCacheControlTtl = Schema.Literals(["5m", "1h"]).annotate({ + "identifier": "AnthropicCacheControlTtl" }) -export type URLCitation = { - readonly "type": "url_citation" +export type Objects_ = { + readonly "ephemeral_1h_input_tokens": number + readonly "ephemeral_5m_input_tokens": number + readonly [x: string]: Schema.Json +} +export const Objects_ = Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type AnthropicCitationCharLocation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "file_id": string | null + readonly "start_char_index": number + readonly "type": "char_location" +} +export const AnthropicCitationCharLocation = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_char_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "file_id": Schema.Union([Schema.String, Schema.Null]), + "start_char_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("char_location") +}).annotate({ "identifier": "AnthropicCitationCharLocation" }) +export type AnthropicCitationCharLocationParam = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_char_index": number + readonly "start_char_index": number + readonly "type": "char_location" +} +export const AnthropicCitationCharLocationParam = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_char_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_char_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("char_location") +}).annotate({ "identifier": "AnthropicCitationCharLocationParam" }) +export type AnthropicCitationContentBlockLocation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "file_id": string | null + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const AnthropicCitationContentBlockLocation = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "file_id": Schema.Union([Schema.String, Schema.Null]), + "start_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("content_block_location") +}).annotate({ "identifier": "AnthropicCitationContentBlockLocation" }) +export type AnthropicCitationContentBlockLocationParam = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_block_index": number + readonly "start_block_index": number + readonly "type": "content_block_location" +} +export const AnthropicCitationContentBlockLocationParam = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("content_block_location") +}).annotate({ "identifier": "AnthropicCitationContentBlockLocationParam" }) +export type AnthropicCitationPageLocation = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "file_id": string | null + readonly "start_page_number": number + readonly "type": "page_location" +} +export const AnthropicCitationPageLocation = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_page_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "file_id": Schema.Union([Schema.String, Schema.Null]), + "start_page_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("page_location") +}).annotate({ "identifier": "AnthropicCitationPageLocation" }) +export type AnthropicCitationPageLocationParam = { + readonly "cited_text": string + readonly "document_index": number + readonly "document_title": string | null + readonly "end_page_number": number + readonly "start_page_number": number + readonly "type": "page_location" +} +export const AnthropicCitationPageLocationParam = Schema.Struct({ + "cited_text": Schema.String, + "document_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "document_title": Schema.Union([Schema.String, Schema.Null]), + "end_page_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_page_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("page_location") +}).annotate({ "identifier": "AnthropicCitationPageLocationParam" }) +export type Objects_1 = { readonly "enabled": boolean; readonly [x: string]: Schema.Json } +export const Objects_1 = Schema.StructWithRest(Schema.Struct({ "enabled": Schema.Boolean }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) +]) +export type AnthropicCitationSearchResultLocation = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const AnthropicCitationSearchResultLocation = Schema.Struct({ + "cited_text": Schema.String, + "end_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "search_result_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "source": Schema.String, + "start_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("search_result_location") +}).annotate({ "identifier": "AnthropicCitationSearchResultLocation" }) +export type AnthropicCitationSearchResultLocationParam = { + readonly "cited_text": string + readonly "end_block_index": number + readonly "search_result_index": number + readonly "source": string + readonly "start_block_index": number + readonly "title": string | null + readonly "type": "search_result_location" +} +export const AnthropicCitationSearchResultLocationParam = Schema.Struct({ + "cited_text": Schema.String, + "end_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "search_result_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "source": Schema.String, + "start_block_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("search_result_location") +}).annotate({ "identifier": "AnthropicCitationSearchResultLocationParam" }) +export type AnthropicCitationWebSearchResultLocation = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number } -export const URLCitation = Schema.Struct({ - "type": Schema.Literal("url_citation"), - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()) -}) -export type FilePath = { readonly "type": "file_path"; readonly "file_id": string; readonly "index": number } -export const FilePath = Schema.Struct({ - "type": Schema.Literal("file_path"), +export const AnthropicCitationWebSearchResultLocation = Schema.Struct({ + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("web_search_result_location"), + "url": Schema.String +}).annotate({ "identifier": "AnthropicCitationWebSearchResultLocation" }) +export type AnthropicCitationWebSearchResultLocationParam = { + readonly "cited_text": string + readonly "encrypted_index": string + readonly "title": string | null + readonly "type": "web_search_result_location" + readonly "url": string +} +export const AnthropicCitationWebSearchResultLocationParam = Schema.Struct({ + "cited_text": Schema.String, + "encrypted_index": Schema.String, + "title": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("web_search_result_location"), + "url": Schema.String +}).annotate({ "identifier": "AnthropicCitationWebSearchResultLocationParam" }) +export type AnthropicCodeExecution20250825Caller = { + readonly "tool_id": string + readonly "type": "code_execution_20250825" +} +export const AnthropicCodeExecution20250825Caller = Schema.Struct({ + "tool_id": Schema.String, + "type": Schema.Literal("code_execution_20250825") +}).annotate({ "identifier": "AnthropicCodeExecution20250825Caller" }) +export type AnthropicCodeExecution20260120Caller = { + readonly "tool_id": string + readonly "type": "code_execution_20260120" +} +export const AnthropicCodeExecution20260120Caller = Schema.Struct({ + "tool_id": Schema.String, + "type": Schema.Literal("code_execution_20260120") +}).annotate({ "identifier": "AnthropicCodeExecution20260120Caller" }) +export type AnthropicCodeExecutionOutput = { readonly "file_id": string; readonly "type": "code_execution_output" } +export const AnthropicCodeExecutionOutput = Schema.Struct({ "file_id": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) + "type": Schema.Literal("code_execution_output") +}).annotate({ "identifier": "AnthropicCodeExecutionOutput" }) +export type AnthropicCompactionBlock = { readonly "content": string | null; readonly "type": "compaction" } +export const AnthropicCompactionBlock = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("compaction") +}).annotate({ "identifier": "AnthropicCompactionBlock" }) +export type AnthropicContainer = { + readonly "expires_at": string + readonly "id": string + readonly [x: string]: Schema.Json +} | null +export const AnthropicContainer = Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "expires_at": Schema.String, "id": Schema.String }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]), + Schema.Null +]).annotate({ "identifier": "AnthropicContainer" }) +export type AnthropicContainerUpload = { readonly "file_id": string; readonly "type": "container_upload" } +export const AnthropicContainerUpload = Schema.Struct({ + "file_id": Schema.String, + "type": Schema.Literal("container_upload") +}).annotate({ "identifier": "AnthropicContainerUpload" }) +export type AnthropicDirectCaller = { readonly "type": "direct" } +export const AnthropicDirectCaller = Schema.Struct({ "type": Schema.Literal("direct") }).annotate({ + "identifier": "AnthropicDirectCaller" }) -export type OpenAIResponsesRefusalContent = { readonly "type": "refusal"; readonly "refusal": string } -export const OpenAIResponsesRefusalContent = Schema.Struct({ - "type": Schema.Literal("refusal"), - "refusal": Schema.String +export type AnthropicFileDocumentSource = { readonly "file_id": string; readonly "type": "file" } +export const AnthropicFileDocumentSource = Schema.Struct({ "file_id": Schema.String, "type": Schema.Literal("file") }) + .annotate({ "identifier": "AnthropicFileDocumentSource" }) +export type AnthropicImageMimeType = "image/jpeg" | "image/png" | "image/gif" | "image/webp" +export const AnthropicImageMimeType = Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]).annotate({ + "identifier": "AnthropicImageMimeType" }) -export type ReasoningTextContent = { readonly "type": "reasoning_text"; readonly "text": string } -export const ReasoningTextContent = Schema.Struct({ "type": Schema.Literal("reasoning_text"), "text": Schema.String }) -export type ReasoningSummaryText = { readonly "type": "summary_text"; readonly "text": string } -export const ReasoningSummaryText = Schema.Struct({ "type": Schema.Literal("summary_text"), "text": Schema.String }) -export type OutputItemFunctionCall = { - readonly "type": "function_call" - readonly "id"?: string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status"?: "completed" | "incomplete" | "in_progress" +export type Objects_2 = { readonly "type": "input_tokens"; readonly "value": number; readonly [x: string]: Schema.Json } +export const Objects_2 = Schema.StructWithRest( + Schema.Struct({ + "type": Schema.Literal("input_tokens"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type AnthropicInputTokensTrigger = { readonly "type": "input_tokens"; readonly "value": number } +export const AnthropicInputTokensTrigger = Schema.Struct({ + "type": Schema.Literal("input_tokens"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "AnthropicInputTokensTrigger" }) +export type Objects_3 = { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json } -export const OutputItemFunctionCall = Schema.Struct({ - "type": Schema.Literal("function_call"), - "id": Schema.optionalKey(Schema.String), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) -}) -export type ResponsesOutputItemFunctionCall = { - readonly "type": "function_call" - readonly "id"?: string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status"?: "completed" | "incomplete" | "in_progress" +export const Objects_3 = Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type AnthropicOutputTokensDetails = + | { readonly "thinking_tokens": number; readonly [x: string]: Schema.Json } + | null +export const AnthropicOutputTokensDetails = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ "thinking_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "identifier": "AnthropicOutputTokensDetails" }) +export type AnthropicPlainTextSource = { + readonly "data": string + readonly "media_type": "text/plain" + readonly "type": "text" } -export const ResponsesOutputItemFunctionCall = Schema.Struct({ - "type": Schema.Literal("function_call"), - "id": Schema.optionalKey(Schema.String), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) +export const AnthropicPlainTextSource = Schema.Struct({ + "data": Schema.String, + "media_type": Schema.Literal("text/plain"), + "type": Schema.Literal("text") +}).annotate({ "identifier": "AnthropicPlainTextSource" }) +export type AnthropicRedactedThinkingBlock = { readonly "data": string; readonly "type": "redacted_thinking" } +export const AnthropicRedactedThinkingBlock = Schema.Struct({ + "data": Schema.String, + "type": Schema.Literal("redacted_thinking") +}).annotate({ "identifier": "AnthropicRedactedThinkingBlock" }) +export type AnthropicRefusalStopDetails = { + readonly "category": "cyber" | "bio" | null + readonly "explanation": string | null + readonly "type": "refusal" + readonly [x: string]: Schema.Json +} | null +export const AnthropicRefusalStopDetails = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "category": Schema.Union([Schema.Literal("cyber"), Schema.Literal("bio"), Schema.Null]), + "explanation": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("refusal") + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Structured information about a refusal", "identifier": "AnthropicRefusalStopDetails" }) +export type AnthropicServerToolErrorCode = + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" +export const AnthropicServerToolErrorCode = Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded" +]).annotate({ "identifier": "AnthropicServerToolErrorCode" }) +export type AnthropicServerToolUsage = { + readonly "web_fetch_requests": number + readonly "web_search_requests": number + readonly [x: string]: Schema.Json +} | null +export const AnthropicServerToolUsage = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "web_fetch_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "web_search_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "identifier": "AnthropicServerToolUsage" }) +export type AnthropicServiceTier = "standard" | "priority" | "batch" | null +export const AnthropicServiceTier = Schema.Union([ + Schema.Literal("standard"), + Schema.Literal("priority"), + Schema.Literal("batch"), + Schema.Null +]).annotate({ "identifier": "AnthropicServiceTier" }) +export type AnthropicSpeed = "fast" | "standard" | null +export const AnthropicSpeed = Schema.Union([Schema.Literal("fast"), Schema.Literal("standard"), Schema.Null]).annotate({ + "identifier": "AnthropicSpeed" }) -export type WebSearchStatus = "completed" | "searching" | "in_progress" | "failed" -export const WebSearchStatus = Schema.Literals(["completed", "searching", "in_progress", "failed"]) -export type ImageGenerationStatus = "in_progress" | "completed" | "generating" | "failed" -export const ImageGenerationStatus = Schema.Literals(["in_progress", "completed", "generating", "failed"]) -export type ResponsesErrorField = { - readonly "code": - | "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" - readonly "message": string +export type AnthropicTextEditorCodeExecutionCreateResult = { + readonly "is_file_update": boolean + readonly "type": "text_editor_code_execution_create_result" } -export const ResponsesErrorField = Schema.Struct({ - "code": Schema.Literals([ - "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" - ]), - "message": Schema.String -}).annotate({ "description": "Error information returned from the API" }) -export type OpenAIResponsesIncompleteDetails = { readonly "reason"?: "max_output_tokens" | "content_filter" } -export const OpenAIResponsesIncompleteDetails = Schema.Struct({ - "reason": Schema.optionalKey(Schema.Literals(["max_output_tokens", "content_filter"])) -}) -export type OpenAIResponsesUsage = { - readonly "input_tokens": number - readonly "input_tokens_details": { readonly "cached_tokens": number } - readonly "output_tokens": number - readonly "output_tokens_details": { readonly "reasoning_tokens": number } - readonly "total_tokens": number +export const AnthropicTextEditorCodeExecutionCreateResult = Schema.Struct({ + "is_file_update": Schema.Boolean, + "type": Schema.Literal("text_editor_code_execution_create_result") +}).annotate({ "identifier": "AnthropicTextEditorCodeExecutionCreateResult" }) +export type AnthropicTextEditorCodeExecutionStrReplaceResult = { + readonly "lines": ReadonlyArray | null + readonly "new_lines": number | null + readonly "new_start": number | null + readonly "old_lines": number | null + readonly "old_start": number | null + readonly "type": "text_editor_code_execution_str_replace_result" } -export const OpenAIResponsesUsage = Schema.Struct({ - "input_tokens": Schema.Number.check(Schema.isFinite()), - "input_tokens_details": Schema.Struct({ "cached_tokens": Schema.Number.check(Schema.isFinite()) }), - "output_tokens": Schema.Number.check(Schema.isFinite()), - "output_tokens_details": Schema.Struct({ "reasoning_tokens": Schema.Number.check(Schema.isFinite()) }), - "total_tokens": Schema.Number.check(Schema.isFinite()) -}) -export type ResponseInputText = { readonly "type": "input_text"; readonly "text": string } -export const ResponseInputText = Schema.Struct({ "type": Schema.Literal("input_text"), "text": Schema.String }) - .annotate({ "description": "Text input content item" }) -export type ResponseInputImage = { - readonly "type": "input_image" - readonly "detail": "auto" | "high" | "low" - readonly "image_url"?: string -} -export const ResponseInputImage = Schema.Struct({ - "type": Schema.Literal("input_image"), - "detail": Schema.Literals(["auto", "high", "low"]), - "image_url": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Image input content item" }) -export type ResponseInputFile = { - readonly "type": "input_file" - readonly "file_id"?: string - readonly "file_data"?: string - readonly "filename"?: string - readonly "file_url"?: string +export const AnthropicTextEditorCodeExecutionStrReplaceResult = Schema.Struct({ + "lines": Schema.Union([Schema.Array(Schema.String), Schema.Null]), + "new_lines": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "new_start": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "old_lines": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "old_start": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "type": Schema.Literal("text_editor_code_execution_str_replace_result") +}).annotate({ "identifier": "AnthropicTextEditorCodeExecutionStrReplaceResult" }) +export type AnthropicTextEditorCodeExecutionToolResultError = { + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "too_many_requests" + | "execution_time_exceeded" + | "file_not_found" + readonly "error_message": string | null + readonly "type": "text_editor_code_execution_tool_result_error" } -export const ResponseInputFile = Schema.Struct({ - "type": Schema.Literal("input_file"), - "file_id": Schema.optionalKey(Schema.String), - "file_data": Schema.optionalKey(Schema.String), - "filename": Schema.optionalKey(Schema.String), - "file_url": Schema.optionalKey(Schema.String) -}).annotate({ "description": "File input content item" }) -export type ResponseInputAudio = { - readonly "type": "input_audio" - readonly "input_audio": { readonly "data": string; readonly "format": "mp3" | "wav" } +export const AnthropicTextEditorCodeExecutionToolResultError = Schema.Struct({ + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "too_many_requests", + "execution_time_exceeded", + "file_not_found" + ]), + "error_message": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("text_editor_code_execution_tool_result_error") +}).annotate({ "identifier": "AnthropicTextEditorCodeExecutionToolResultError" }) +export type AnthropicTextEditorCodeExecutionViewResult = { + readonly "content": string + readonly "file_type": "text" | "image" | "pdf" + readonly "num_lines": number | null + readonly "start_line": number | null + readonly "total_lines": number | null + readonly "type": "text_editor_code_execution_view_result" } -export const ResponseInputAudio = Schema.Struct({ - "type": Schema.Literal("input_audio"), - "input_audio": Schema.Struct({ "data": Schema.String, "format": Schema.Literals(["mp3", "wav"]) }) -}).annotate({ "description": "Audio input content item" }) -export type ToolCallStatus = "in_progress" | "completed" | "incomplete" -export const ToolCallStatus = Schema.Literals(["in_progress", "completed", "incomplete"]) -export type OpenResponsesRequestMetadata = {} -export const OpenResponsesRequestMetadata = Schema.Struct({}).annotate({ - "description": - "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 const AnthropicTextEditorCodeExecutionViewResult = Schema.Struct({ + "content": Schema.String, + "file_type": Schema.Literals(["text", "image", "pdf"]), + "num_lines": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "start_line": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]), + "total_lines": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "type": Schema.Literal("text_editor_code_execution_view_result") +}).annotate({ "identifier": "AnthropicTextEditorCodeExecutionViewResult" }) +export type AnthropicThinkingBlock = { + readonly "signature": string + readonly "thinking": string + readonly "type": "thinking" +} +export const AnthropicThinkingBlock = Schema.Struct({ + "signature": Schema.String, + "thinking": Schema.String, + "type": Schema.Literal("thinking") +}).annotate({ "identifier": "AnthropicThinkingBlock" }) +export type AnthropicThinkingDisplay = "summarized" | "omitted" | null +export const AnthropicThinkingDisplay = Schema.Union([ + Schema.Literal("summarized"), + Schema.Literal("omitted"), + Schema.Null +]).annotate({ "identifier": "AnthropicThinkingDisplay" }) +export type AnthropicThinkingTurns = { readonly "type": "thinking_turns"; readonly "value": number } +export const AnthropicThinkingTurns = Schema.Struct({ + "type": Schema.Literal("thinking_turns"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "AnthropicThinkingTurns" }) +export type AnthropicToolReference = { readonly "tool_name": string; readonly "type": "tool_reference" } +export const AnthropicToolReference = Schema.Struct({ + "tool_name": Schema.String, + "type": Schema.Literal("tool_reference") +}).annotate({ "identifier": "AnthropicToolReference" }) +export type AnthropicToolUsesKeep = { readonly "type": "tool_uses"; readonly "value": number } +export const AnthropicToolUsesKeep = Schema.Struct({ + "type": Schema.Literal("tool_uses"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "AnthropicToolUsesKeep" }) +export type AnthropicToolUsesTrigger = { readonly "type": "tool_uses"; readonly "value": number } +export const AnthropicToolUsesTrigger = Schema.Struct({ + "type": Schema.Literal("tool_uses"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "AnthropicToolUsesTrigger" }) +export type AnthropicUrlImageSource = { readonly "type": "url"; readonly "url": string } +export const AnthropicUrlImageSource = Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }).annotate({ + "identifier": "AnthropicUrlImageSource" }) -export type ResponsesSearchContextSize = "low" | "medium" | "high" -export const ResponsesSearchContextSize = Schema.Literals(["low", "medium", "high"]).annotate({ - "description": "Size of the search context for web search tools" +export type AnthropicUrlPdfSource = { readonly "type": "url"; readonly "url": string } +export const AnthropicUrlPdfSource = Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }).annotate({ + "identifier": "AnthropicUrlPdfSource" }) -export type WebSearchPreviewToolUserLocation = { - readonly "type": "approximate" - readonly "city"?: string - readonly "country"?: string - readonly "region"?: string - readonly "timezone"?: string -} -export const WebSearchPreviewToolUserLocation = Schema.Struct({ - "type": Schema.Literal("approximate"), - "city": Schema.optionalKey(Schema.String), - "country": Schema.optionalKey(Schema.String), - "region": Schema.optionalKey(Schema.String), - "timezone": Schema.optionalKey(Schema.String) -}) -export type ResponsesWebSearchUserLocation = { - readonly "type"?: "approximate" - readonly "city"?: string - readonly "country"?: string - readonly "region"?: string - readonly "timezone"?: string -} -export const ResponsesWebSearchUserLocation = Schema.Struct({ - "type": Schema.optionalKey(Schema.Literal("approximate")), - "city": Schema.optionalKey(Schema.String), - "country": Schema.optionalKey(Schema.String), - "region": Schema.optionalKey(Schema.String), - "timezone": Schema.optionalKey(Schema.String) -}).annotate({ "description": "User location information for web search" }) -export type OpenAIResponsesToolChoice = "auto" | "none" | "required" | { - readonly "type": "function" - readonly "name": string -} | { readonly "type": "web_search_preview_2025_03_11" | "web_search_preview" } -export const OpenAIResponsesToolChoice = Schema.Union([ - Schema.Literal("auto"), - Schema.Literal("none"), - Schema.Literal("required"), - Schema.Struct({ "type": Schema.Literal("function"), "name": Schema.String }), - Schema.Struct({ "type": Schema.Literals(["web_search_preview_2025_03_11", "web_search_preview"]) }) -]) -export type OpenAIResponsesPrompt = { readonly "id": string; readonly "variables"?: {} } -export const OpenAIResponsesPrompt = Schema.Struct({ - "id": Schema.String, - "variables": Schema.optionalKey(Schema.Struct({})) -}) -export type OpenAIResponsesReasoningEffort = "xhigh" | "high" | "medium" | "low" | "minimal" | "none" -export const OpenAIResponsesReasoningEffort = Schema.Literals(["xhigh", "high", "medium", "low", "minimal", "none"]) -export type ReasoningSummaryVerbosity = "auto" | "concise" | "detailed" -export const ReasoningSummaryVerbosity = Schema.Literals(["auto", "concise", "detailed"]) -export type OpenAIResponsesServiceTier = "auto" | "default" | "flex" | "priority" | "scale" -export const OpenAIResponsesServiceTier = Schema.Literals(["auto", "default", "flex", "priority", "scale"]) -export type OpenAIResponsesTruncation = "auto" | "disabled" -export const OpenAIResponsesTruncation = Schema.Literals(["auto", "disabled"]) -export type ResponsesFormatText = { readonly "type": "text" } -export const ResponsesFormatText = Schema.Struct({ "type": Schema.Literal("text") }).annotate({ - "description": "Plain text response format" -}) -export type ResponsesFormatJSONObject = { readonly "type": "json_object" } -export const ResponsesFormatJSONObject = Schema.Struct({ "type": Schema.Literal("json_object") }).annotate({ - "description": "JSON object response format" -}) -export type ResponsesFormatTextJSONSchemaConfig = { - readonly "type": "json_schema" - readonly "name": string - readonly "description"?: string - readonly "strict"?: boolean - readonly "schema": {} +export type AnthropicWebFetchToolResultError = { + readonly "error_code": + | "invalid_tool_input" + | "url_too_long" + | "url_not_allowed" + | "url_not_accessible" + | "unsupported_content_type" + | "too_many_requests" + | "max_uses_exceeded" + | "unavailable" + readonly "type": "web_fetch_tool_result_error" } -export const ResponsesFormatTextJSONSchemaConfig = Schema.Struct({ - "type": Schema.Literal("json_schema"), - "name": Schema.String, - "description": Schema.optionalKey(Schema.String), - "strict": Schema.optionalKey(Schema.Boolean), - "schema": Schema.Struct({}) -}).annotate({ "description": "JSON schema constrained response format" }) -export type OpenResponsesErrorEvent = { - readonly "type": "error" - readonly "code": string - readonly "message": string - readonly "param": string - readonly "sequence_number": number +export const AnthropicWebFetchToolResultError = Schema.Struct({ + "error_code": Schema.Literals([ + "invalid_tool_input", + "url_too_long", + "url_not_allowed", + "url_not_accessible", + "unsupported_content_type", + "too_many_requests", + "max_uses_exceeded", + "unavailable" + ]), + "type": Schema.Literal("web_fetch_tool_result_error") +}).annotate({ "identifier": "AnthropicWebFetchToolResultError" }) +export type AnthropicWebSearchResult = { + readonly "encrypted_content": string + readonly "page_age": string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string } -export const OpenResponsesErrorEvent = Schema.Struct({ - "type": Schema.Literal("error"), - "code": Schema.String, - "message": Schema.String, - "param": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when an error occurs during streaming" }) -export type OpenResponsesTopLogprobs = { readonly "token"?: string; readonly "logprob"?: number } -export const OpenResponsesTopLogprobs = Schema.Struct({ - "token": Schema.optionalKey(Schema.String), - "logprob": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) -}).annotate({ "description": "Alternative token with its log probability" }) -export type OpenResponsesRefusalDeltaEvent = { - readonly "type": "response.refusal.delta" - readonly "output_index": number - readonly "item_id": string - readonly "content_index": number - readonly "delta": string - readonly "sequence_number": number +export const AnthropicWebSearchResult = Schema.Struct({ + "encrypted_content": Schema.String, + "page_age": Schema.Union([Schema.String, Schema.Null]), + "title": Schema.String, + "type": Schema.Literal("web_search_result"), + "url": Schema.String +}).annotate({ "identifier": "AnthropicWebSearchResult" }) +export type AnthropicWebSearchResultBlockParam = { + readonly "encrypted_content": string + readonly "page_age"?: string | null + readonly "title": string + readonly "type": "web_search_result" + readonly "url": string } -export const OpenResponsesRefusalDeltaEvent = Schema.Struct({ - "type": Schema.Literal("response.refusal.delta"), - "output_index": Schema.Number.check(Schema.isFinite()), - "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "delta": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a refusal delta is streamed" }) -export type OpenResponsesRefusalDoneEvent = { - readonly "type": "response.refusal.done" - readonly "output_index": number +export const AnthropicWebSearchResultBlockParam = Schema.Struct({ + "encrypted_content": Schema.String, + "page_age": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "title": Schema.String, + "type": Schema.Literal("web_search_result"), + "url": Schema.String +}).annotate({ "identifier": "AnthropicWebSearchResultBlockParam" }) +export type AnthropicWebSearchToolResultError = { + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + | "request_too_large" + readonly "type": "web_search_tool_result_error" +} +export const AnthropicWebSearchToolResultError = Schema.Struct({ + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long", + "request_too_large" + ]), + "type": Schema.Literal("web_search_tool_result_error") +}).annotate({ "identifier": "AnthropicWebSearchToolResultError" }) +export type Objects_4 = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type": "approximate" + readonly [x: string]: Schema.Json +} +export const Objects_4 = Schema.StructWithRest( + Schema.Struct({ + "city": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "country": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "region": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "timezone": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("approximate") + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type ApiErrorType = + | "context_length_exceeded" + | "max_tokens_exceeded" + | "token_limit_exceeded" + | "string_too_long" + | "authentication" + | "permission_denied" + | "payment_required" + | "rate_limit_exceeded" + | "provider_overloaded" + | "provider_unavailable" + | "invalid_request" + | "invalid_prompt" + | "not_found" + | "precondition_failed" + | "payload_too_large" + | "unprocessable" + | "content_policy_violation" + | "refusal" + | "invalid_image" + | "image_too_large" + | "image_too_small" + | "unsupported_image_format" + | "image_not_found" + | "image_download_failed" + | "server" + | "timeout" + | "unmapped" +export const ApiErrorType = Schema.Literals([ + "context_length_exceeded", + "max_tokens_exceeded", + "token_limit_exceeded", + "string_too_long", + "authentication", + "permission_denied", + "payment_required", + "rate_limit_exceeded", + "provider_overloaded", + "provider_unavailable", + "invalid_request", + "invalid_prompt", + "not_found", + "precondition_failed", + "payload_too_large", + "unprocessable", + "content_policy_violation", + "refusal", + "invalid_image", + "image_too_large", + "image_too_small", + "unsupported_image_format", + "image_not_found", + "image_download_failed", + "server", + "timeout", + "unmapped" +]).annotate({ + "description": "Canonical OpenRouter error type, stable across all API formats", + "identifier": "ApiErrorType" +}) +export type ApplyPatchCallOperationDiffDeltaEvent = { + readonly "delta": string readonly "item_id": string - readonly "content_index": number - readonly "refusal": string + readonly "output_index": number readonly "sequence_number": number + readonly "type": "response.apply_patch_call_operation_diff.delta" } -export const OpenResponsesRefusalDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.refusal.done"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const ApplyPatchCallOperationDiffDeltaEvent = Schema.Struct({ + "delta": Schema.String, "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "refusal": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when refusal streaming is complete" }) -export type OpenResponsesFunctionCallArgumentsDeltaEvent = { - readonly "type": "response.function_call_arguments.delta" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.apply_patch_call_operation_diff.delta") +}).annotate({ + "description": "Incremental chunk of `operation.diff` for an `apply_patch_call`. Matches OpenAI's streaming shape.", + "identifier": "ApplyPatchCallOperationDiffDeltaEvent" +}) +export type ApplyPatchCallOperationDiffDoneEvent = { + readonly "diff": string readonly "item_id": string readonly "output_index": number - readonly "delta": string readonly "sequence_number": number + readonly "type": "response.apply_patch_call_operation_diff.done" } -export const OpenResponsesFunctionCallArgumentsDeltaEvent = Schema.Struct({ - "type": Schema.Literal("response.function_call_arguments.delta"), +export const ApplyPatchCallOperationDiffDoneEvent = Schema.Struct({ + "diff": Schema.String, "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "delta": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when function call arguments are being streamed" }) -export type OpenResponsesFunctionCallArgumentsDoneEvent = { - readonly "type": "response.function_call_arguments.done" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.apply_patch_call_operation_diff.done") +}).annotate({ + "description": "Emitted when `operation.diff` streaming completes for an `apply_patch_call`.", + "identifier": "ApplyPatchCallOperationDiffDoneEvent" +}) +export type ApplyPatchCallOutputItem = { + readonly "call_id": string + readonly "id"?: string | null + readonly "output"?: string | null + readonly "status": "completed" | "failed" + readonly "type": "apply_patch_call_output" +} +export const ApplyPatchCallOutputItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "output": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Literals(["completed", "failed"]), + "type": Schema.Literal("apply_patch_call_output") +}).annotate({ + "description": + "The client's echo of an `apply_patch_call` after applying the patch. `output` is an optional human-readable log; `status` is `completed` when the patch was applied successfully, `failed` otherwise.", + "identifier": "ApplyPatchCallOutputItem" +}) +export type ApplyPatchCallStatus = "in_progress" | "completed" +export const ApplyPatchCallStatus = Schema.Literals(["in_progress", "completed"]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item.", + "identifier": "ApplyPatchCallStatus" +}) +export type ApplyPatchCreateFileOperation = { + readonly "diff": string + readonly "path": string + readonly "type": "create_file" +} +export const ApplyPatchCreateFileOperation = Schema.Struct({ + "diff": Schema.String, + "path": Schema.String, + "type": Schema.Literal("create_file") +}).annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents.", + "identifier": "ApplyPatchCreateFileOperation" +}) +export type ApplyPatchDeleteFileOperation = { readonly "path": string; readonly "type": "delete_file" } +export const ApplyPatchDeleteFileOperation = Schema.Struct({ + "path": Schema.String, + "type": Schema.Literal("delete_file") +}).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required.", + "identifier": "ApplyPatchDeleteFileOperation" +}) +export type ApplyPatchEngineEnum = "auto" | "native" | "openrouter" +export const ApplyPatchEngineEnum = Schema.Literals(["auto", "native", "openrouter"]).annotate({ + "description": + "Which apply_patch engine to use. \"auto\" (default) uses native passthrough when the endpoint advertises native apply_patch support, otherwise falls back to OpenRouter's HITL validator. \"native\" forces native passthrough — when the endpoint does not support native, the request falls back to HITL. \"openrouter\" always runs the HITL validator. Native passthrough streams the diff incrementally via `apply_patch_call_operation_diff.delta` events; HITL buffers the diff for atomic delivery as a single delta.", + "identifier": "ApplyPatchEngineEnum" +}) +export type ApplyPatchServerTool = { readonly "type": "apply_patch" } +export const ApplyPatchServerTool = Schema.Struct({ "type": Schema.Literal("apply_patch") }).annotate({ + "description": "Apply patch tool configuration", + "identifier": "ApplyPatchServerTool" +}) +export type ApplyPatchUpdateFileOperation = { + readonly "diff": string + readonly "path": string + readonly "type": "update_file" +} +export const ApplyPatchUpdateFileOperation = Schema.Struct({ + "diff": Schema.String, + "path": Schema.String, + "type": Schema.Literal("update_file") +}).annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file.", + "identifier": "ApplyPatchUpdateFileOperation" +}) +export type AppRankingsItem = { + readonly "app_id": number + readonly "app_name": string + readonly "rank": number + readonly "total_requests": number + readonly "total_tokens": string +} +export const AppRankingsItem = Schema.Struct({ + "app_id": Schema.Number.annotate({ "description": "Stable numeric identifier of the app on OpenRouter." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "app_name": Schema.String.annotate({ "description": "Public display name of the app." }), + "rank": Schema.Number.annotate({ + "description": "1-based position of the app within this response, per the requested `sort`." + }).check(Schema.isInt().annotate({ "expected": "an integer" })), + "total_requests": Schema.Number.annotate({ + "description": "Number of requests attributed to the app inside the date window." + }).check(Schema.isInt().annotate({ "expected": "an integer" })), + "total_tokens": Schema.String.annotate({ + "description": + "Sum of `prompt_tokens + completion_tokens` attributed to the app inside the date window, returned as a decimal string so 64-bit values are not truncated." + }) +}).annotate({ "identifier": "AppRankingsItem" }) +export type AutoBetaRouterPlugin = { + readonly "allowed_models"?: ReadonlyArray + readonly "cost_quality_tradeoff"?: number + readonly "enabled"?: boolean + readonly "id": "auto-beta-router" +} +export const AutoBetaRouterPlugin = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "List of model patterns to filter which models the auto-beta-router can route between. Supports wildcards (e.g., \"anthropic/*\" matches all Anthropic models). When not specified, uses the default supported models list." + }) + ), + "cost_quality_tradeoff": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Balances routing between cost and quality on a 0-10 scale. The auto-beta-router ranks models for the classified task type by community spend share, then filters candidates by their average cost per generation for that task. Higher values favor cheaper models: 10 keeps only models around the cheapest 10th percentile, while 0 permits models up to the 90th percentile for cost. Defaults to 9." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(10).annotate({ "expected": "a value less than or equal to 10" })) + ), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the auto-beta-router plugin for this request. Defaults to true." + }) + ), + "id": Schema.Literal("auto-beta-router") +}).annotate({ "identifier": "AutoBetaRouterPlugin" }) +export type AutoRouterPlugin = { + readonly "allowed_models"?: ReadonlyArray + readonly "cost_quality_tradeoff"?: number + readonly "enabled"?: boolean + readonly "id": "auto-router" + readonly "pin_model"?: boolean +} +export const AutoRouterPlugin = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "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." + }) + ), + "cost_quality_tradeoff": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Controls cost vs. quality routing tradeoff (0–10). 0 = pure quality (best model regardless of cost), 10 = maximize for cost (cheapest model wins). Intermediate values blend quality and cost signals continuously. Defaults to 7." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(10).annotate({ "expected": "a value less than or equal to 10" })) + ), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the auto-router plugin for this request. Defaults to true." + }) + ), + "id": Schema.Literal("auto-router"), + "pin_model": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "When true, reuses the model from the most recent assistant message's `model` attribute for subsequent turns. Defaults to false." + }) + ) +}).annotate({ "identifier": "AutoRouterPlugin" }) +export type BadGatewayResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const BadGatewayResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for BadGatewayResponse", "identifier": "BadGatewayResponseErrorData" }) +export type BadRequestResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const BadRequestResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for BadRequestResponse", "identifier": "BadRequestResponseErrorData" }) +export type BaseCustomToolCallInputDeltaEvent = { + readonly "delta": string readonly "item_id": string readonly "output_index": number - readonly "name": string - readonly "arguments": string readonly "sequence_number": number + readonly "type": "response.custom_tool_call_input.delta" } -export const OpenResponsesFunctionCallArgumentsDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.function_call_arguments.done"), +export const BaseCustomToolCallInputDeltaEvent = Schema.Struct({ + "delta": Schema.String, "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "name": Schema.String, - "arguments": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when function call arguments streaming is complete" }) -export type OpenResponsesReasoningDeltaEvent = { - readonly "type": "response.reasoning_text.delta" - readonly "output_index": number + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.custom_tool_call_input.delta") +}).annotate({ + "description": + "Event emitted when a custom tool call's freeform input is being streamed. Mirrors `response.function_call_arguments.delta` but for `custom` tools whose input is opaque text rather than JSON arguments.", + "identifier": "BaseCustomToolCallInputDeltaEvent" +}) +export type BaseCustomToolCallInputDoneEvent = { + readonly "input": string readonly "item_id": string - readonly "content_index": number - readonly "delta": string + readonly "output_index": number readonly "sequence_number": number + readonly "type": "response.custom_tool_call_input.done" } -export const OpenResponsesReasoningDeltaEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_text.delta"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const BaseCustomToolCallInputDoneEvent = Schema.Struct({ + "input": Schema.String, "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "delta": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when reasoning text delta is streamed" }) -export type OpenResponsesReasoningDoneEvent = { - readonly "type": "response.reasoning_text.done" - readonly "output_index": number + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.custom_tool_call_input.done") +}).annotate({ + "description": + "Event emitted when a custom tool call's freeform input streaming is complete. Mirrors `response.function_call_arguments.done` but for `custom` tools.", + "identifier": "BaseCustomToolCallInputDoneEvent" +}) +export type BaseErrorEvent = { + readonly "code": string | null + readonly "message": string + readonly "param": string | null + readonly "sequence_number": number + readonly "type": "error" +} +export const BaseErrorEvent = Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "message": Schema.String, + "param": Schema.Union([Schema.String, Schema.Null]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("error") +}).annotate({ "description": "Event emitted when an error occurs during streaming", "identifier": "BaseErrorEvent" }) +export type BaseFunctionCallArgsDeltaEvent = { + readonly "delta": string readonly "item_id": string - readonly "content_index": number - readonly "text": string + readonly "output_index": number readonly "sequence_number": number + readonly "type": "response.function_call_arguments.delta" } -export const OpenResponsesReasoningDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_text.done"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const BaseFunctionCallArgsDeltaEvent = Schema.Struct({ + "delta": Schema.String, "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "text": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when reasoning text streaming is complete" }) -export type OpenResponsesReasoningSummaryTextDeltaEvent = { - readonly "type": "response.reasoning_summary_text.delta" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.function_call_arguments.delta") +}).annotate({ + "description": "Event emitted when function call arguments are being streamed", + "identifier": "BaseFunctionCallArgsDeltaEvent" +}) +export type BaseFunctionCallArgsDoneEvent = { + readonly "arguments": string readonly "item_id": string + readonly "name": string readonly "output_index": number - readonly "summary_index": number - readonly "delta": string readonly "sequence_number": number + readonly "type": "response.function_call_arguments.done" } -export const OpenResponsesReasoningSummaryTextDeltaEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_summary_text.delta"), +export const BaseFunctionCallArgsDoneEvent = Schema.Struct({ + "arguments": Schema.String, "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "summary_index": Schema.Number.check(Schema.isFinite()), + "name": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.function_call_arguments.done") +}).annotate({ + "description": "Event emitted when function call arguments streaming is complete", + "identifier": "BaseFunctionCallArgsDoneEvent" +}) +export type BaseReasoningDeltaEvent = { + readonly "content_index": number + readonly "delta": string + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.reasoning_text.delta" +} +export const BaseReasoningDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "delta": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when reasoning summary text delta is streamed" }) -export type OpenResponsesReasoningSummaryTextDoneEvent = { - readonly "type": "response.reasoning_summary_text.done" + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_text.delta") +}).annotate({ + "description": "Event emitted when reasoning text delta is streamed", + "identifier": "BaseReasoningDeltaEvent" +}) +export type BaseReasoningDoneEvent = { + readonly "content_index": number readonly "item_id": string readonly "output_index": number - readonly "summary_index": number - readonly "text": string readonly "sequence_number": number + readonly "text": string + readonly "type": "response.reasoning_text.done" } -export const OpenResponsesReasoningSummaryTextDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_summary_text.done"), +export const BaseReasoningDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "summary_index": Schema.Number.check(Schema.isFinite()), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "text": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when reasoning summary text streaming is complete" }) -export type OpenResponsesImageGenCallInProgress = { - readonly "type": "response.image_generation_call.in_progress" + "type": Schema.Literal("response.reasoning_text.done") +}).annotate({ + "description": "Event emitted when reasoning text streaming is complete", + "identifier": "BaseReasoningDoneEvent" +}) +export type BaseReasoningSummaryTextDeltaEvent = { + readonly "delta": string readonly "item_id": string readonly "output_index": number readonly "sequence_number": number + readonly "summary_index": number + readonly "type": "response.reasoning_summary_text.delta" } -export const OpenResponsesImageGenCallInProgress = Schema.Struct({ - "type": Schema.Literal("response.image_generation_call.in_progress"), +export const BaseReasoningSummaryTextDeltaEvent = Schema.Struct({ + "delta": Schema.String, "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Image generation call in progress" }) -export type OpenResponsesImageGenCallGenerating = { - readonly "type": "response.image_generation_call.generating" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_text.delta") +}).annotate({ + "description": "Event emitted when reasoning summary text delta is streamed", + "identifier": "BaseReasoningSummaryTextDeltaEvent" +}) +export type BaseReasoningSummaryTextDoneEvent = { readonly "item_id": string readonly "output_index": number readonly "sequence_number": number + readonly "summary_index": number + readonly "text": string + readonly "type": "response.reasoning_summary_text.done" } -export const OpenResponsesImageGenCallGenerating = Schema.Struct({ - "type": Schema.Literal("response.image_generation_call.generating"), +export const BaseReasoningSummaryTextDoneEvent = Schema.Struct({ "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Image generation call is generating" }) -export type OpenResponsesImageGenCallPartialImage = { - readonly "type": "response.image_generation_call.partial_image" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "text": Schema.String, + "type": Schema.Literal("response.reasoning_summary_text.done") +}).annotate({ + "description": "Event emitted when reasoning summary text streaming is complete", + "identifier": "BaseReasoningSummaryTextDoneEvent" +}) +export type BaseRefusalDeltaEvent = { + readonly "content_index": number + readonly "delta": string readonly "item_id": string readonly "output_index": number readonly "sequence_number": number - readonly "partial_image_b64": string - readonly "partial_image_index": number + readonly "type": "response.refusal.delta" } -export const OpenResponsesImageGenCallPartialImage = Schema.Struct({ - "type": Schema.Literal("response.image_generation_call.partial_image"), +export const BaseRefusalDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "delta": Schema.String, "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "sequence_number": Schema.Number.check(Schema.isFinite()), - "partial_image_b64": Schema.String, - "partial_image_index": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Image generation call with partial image" }) -export type OpenResponsesImageGenCallCompleted = { - readonly "type": "response.image_generation_call.completed" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.refusal.delta") +}).annotate({ "description": "Event emitted when a refusal delta is streamed", "identifier": "BaseRefusalDeltaEvent" }) +export type BaseRefusalDoneEvent = { + readonly "content_index": number readonly "item_id": string readonly "output_index": number + readonly "refusal": string readonly "sequence_number": number + readonly "type": "response.refusal.done" } -export const OpenResponsesImageGenCallCompleted = Schema.Struct({ - "type": Schema.Literal("response.image_generation_call.completed"), +export const BaseRefusalDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "item_id": Schema.String, - "output_index": Schema.Number.check(Schema.isFinite()), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Image generation call completed" }) -export type BadRequestResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "refusal": Schema.String, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.refusal.done") +}).annotate({ "description": "Event emitted when refusal streaming is complete", "identifier": "BaseRefusalDoneEvent" }) +export type BashServerToolEngine = "auto" | "native" | "openrouter" +export const BashServerToolEngine = Schema.Literals(["auto", "native", "openrouter"]).annotate({ + "description": + "Which bash engine to use. \"openrouter\" runs commands server-side in the OpenRouter sandbox. \"auto\" (default) and \"native\" use native passthrough, returning the tool call to your application to run client-side; OpenRouter does not execute the commands.", + "identifier": "BashServerToolEngine" +}) +export type BooleanCapability = { readonly "type": "boolean" } +export const BooleanCapability = Schema.Struct({ "type": Schema.Literal("boolean") }).annotate({ + "description": "A supported-or-not flag. Present means the parameter is accepted.", + "identifier": "BooleanCapability" +}) +export type BulkAddWorkspaceMembersRequest = { readonly "user_ids": ReadonlyArray } +export const BulkAddWorkspaceMembersRequest = Schema.Struct({ + "user_ids": Schema.Array(Schema.String).annotate({ + "description": + "List of user IDs to add to the workspace. Members are assigned the same role they hold in the organization." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ) +}).annotate({ "identifier": "BulkAddWorkspaceMembersRequest" }) +export type BulkAssignKeysRequest = { readonly "key_hashes": ReadonlyArray } +export const BulkAssignKeysRequest = Schema.Struct({ + "key_hashes": Schema.Array( + Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ).annotate({ "description": "Array of API key hashes to assign to the guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ "identifier": "BulkAssignKeysRequest" }) +export type BulkAssignKeysResponse = { readonly "assigned_count": number } +export const BulkAssignKeysResponse = Schema.Struct({ + "assigned_count": Schema.Number.annotate({ "description": "Number of keys successfully assigned" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "BulkAssignKeysResponse" }) +export type BulkAssignMembersRequest = { readonly "member_user_ids": ReadonlyArray } +export const BulkAssignMembersRequest = Schema.Struct({ + "member_user_ids": Schema.Array( + Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ).annotate({ "description": "Array of member user IDs to assign to the guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ "identifier": "BulkAssignMembersRequest" }) +export type BulkAssignMembersResponse = { readonly "assigned_count": number } +export const BulkAssignMembersResponse = Schema.Struct({ + "assigned_count": Schema.Number.annotate({ "description": "Number of members successfully assigned" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "BulkAssignMembersResponse" }) +export type BulkRemoveWorkspaceMembersRequest = { readonly "user_ids": ReadonlyArray } +export const BulkRemoveWorkspaceMembersRequest = Schema.Struct({ + "user_ids": Schema.Array(Schema.String).annotate({ "description": "List of user IDs to remove from the workspace" }) + .check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ) +}).annotate({ "identifier": "BulkRemoveWorkspaceMembersRequest" }) +export type BulkRemoveWorkspaceMembersResponse = { readonly "removed_count": number } +export const BulkRemoveWorkspaceMembersResponse = Schema.Struct({ + "removed_count": Schema.Number.annotate({ "description": "Number of members removed" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "BulkRemoveWorkspaceMembersResponse" }) +export type BulkUnassignKeysRequest = { readonly "key_hashes": ReadonlyArray } +export const BulkUnassignKeysRequest = Schema.Struct({ + "key_hashes": Schema.Array( + Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ).annotate({ "description": "Array of API key hashes to unassign from the guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ "identifier": "BulkUnassignKeysRequest" }) +export type BulkUnassignKeysResponse = { readonly "unassigned_count": number } +export const BulkUnassignKeysResponse = Schema.Struct({ + "unassigned_count": Schema.Number.annotate({ "description": "Number of keys successfully unassigned" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "BulkUnassignKeysResponse" }) +export type BulkUnassignMembersRequest = { readonly "member_user_ids": ReadonlyArray } +export const BulkUnassignMembersRequest = Schema.Struct({ + "member_user_ids": Schema.Array( + Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ).annotate({ "description": "Array of member user IDs to unassign from the guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ "identifier": "BulkUnassignMembersRequest" }) +export type BulkUnassignMembersResponse = { readonly "unassigned_count": number } +export const BulkUnassignMembersResponse = Schema.Struct({ + "unassigned_count": Schema.Number.annotate({ "description": "Number of members successfully unassigned" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "BulkUnassignMembersResponse" }) +export type BYOKProviderSlug = + | "ai21" + | "aion-labs" + | "akashml" + | "alibaba" + | "amazon-bedrock" + | "amazon-nova" + | "ambient" + | "anthropic" + | "arcee-ai" + | "atlas-cloud" + | "avian" + | "azure" + | "baidu" + | "baseten" + | "black-forest-labs" + | "byteplus" + | "cerebras" + | "chutes" + | "cirrascale" + | "clarifai" + | "cloudflare" + | "cohere" + | "coreweave" + | "crusoe" + | "darkbloom" + | "decart" + | "deepgram" + | "deepinfra" + | "deepseek" + | "dekallm" + | "digitalocean" + | "featherless" + | "fireworks" + | "fish-audio" + | "friendli" + | "gmicloud" + | "google-ai-studio" + | "google-vertex" + | "groq" + | "heygen" + | "inception" + | "inceptron" + | "inferact-vllm" + | "inference-net" + | "infermatic" + | "inflection" + | "io-net" + | "ionstream" + | "krea" + | "liquid" + | "mancer" + | "mara" + | "meta" + | "minimax" + | "mistral" + | "modelrun" + | "modular" + | "moonshotai" + | "morph" + | "ncompass" + | "nebius" + | "nex-agi" + | "nextbit" + | "novita" + | "nvidia" + | "open-inference" + | "openai" + | "parasail" + | "perceptron" + | "perplexity" + | "phala" + | "poolside" + | "quiver" + | "recraft" + | "reka" + | "relace" + | "runway" + | "sail-research" + | "sakana" + | "sambanova" + | "seed" + | "siliconflow" + | "sourceful" + | "stepfun" + | "streamlake" + | "switchpoint" + | "tencent" + | "tenstorrent" + | "together" + | "upstage" + | "venice" + | "wafer" + | "wandb" + | "xai" + | "xiaomi" + | "z-ai" +export const BYOKProviderSlug = Schema.Literals([ + "ai21", + "aion-labs", + "akashml", + "alibaba", + "amazon-bedrock", + "amazon-nova", + "ambient", + "anthropic", + "arcee-ai", + "atlas-cloud", + "avian", + "azure", + "baidu", + "baseten", + "black-forest-labs", + "byteplus", + "cerebras", + "chutes", + "cirrascale", + "clarifai", + "cloudflare", + "cohere", + "coreweave", + "crusoe", + "darkbloom", + "decart", + "deepgram", + "deepinfra", + "deepseek", + "dekallm", + "digitalocean", + "featherless", + "fireworks", + "fish-audio", + "friendli", + "gmicloud", + "google-ai-studio", + "google-vertex", + "groq", + "heygen", + "inception", + "inceptron", + "inferact-vllm", + "inference-net", + "infermatic", + "inflection", + "io-net", + "ionstream", + "krea", + "liquid", + "mancer", + "mara", + "meta", + "minimax", + "mistral", + "modelrun", + "modular", + "moonshotai", + "morph", + "ncompass", + "nebius", + "nex-agi", + "nextbit", + "novita", + "nvidia", + "open-inference", + "openai", + "parasail", + "perceptron", + "perplexity", + "phala", + "poolside", + "quiver", + "recraft", + "reka", + "relace", + "runway", + "sail-research", + "sakana", + "sambanova", + "seed", + "siliconflow", + "sourceful", + "stepfun", + "streamlake", + "switchpoint", + "tencent", + "tenstorrent", + "together", + "upstage", + "venice", + "wafer", + "wandb", + "xai", + "xiaomi", + "z-ai" +]).annotate({ + "description": + "The upstream provider this credential authenticates against, as a lowercase slug (e.g. `openai`, `anthropic`, `amazon-bedrock`).", + "identifier": "BYOKProviderSlug" +}) +export type ChatAssistantImages = ReadonlyArray<{ readonly "image_url": { readonly "url": string } }> +export const ChatAssistantImages = Schema.Array( + Schema.Struct({ + "image_url": Schema.Struct({ + "url": Schema.String.annotate({ "description": "URL or base64-encoded data of the generated image" }) + }) + }) +).annotate({ "description": "Generated images from image generation models", "identifier": "ChatAssistantImages" }) +export type ChatAudioOutput = { + readonly "data"?: string + readonly "expires_at"?: number + readonly "id"?: string + readonly "transcript"?: string } -export const BadRequestResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for BadRequestResponse" }) -export type UnauthorizedResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatAudioOutput = Schema.Struct({ + "data": Schema.optionalKey(Schema.String.annotate({ "description": "Base64 encoded audio data" })), + "expires_at": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio expiration timestamp" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "Audio output identifier" })), + "transcript": Schema.optionalKey(Schema.String.annotate({ "description": "Audio transcript" })) +}).annotate({ "description": "Audio output data or reference", "identifier": "ChatAudioOutput" }) +export type ChatContentAudio = { + readonly "input_audio": { readonly "data": string; readonly "format": string } + readonly "type": "input_audio" } -export const UnauthorizedResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for UnauthorizedResponse" }) -export type PaymentRequiredResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatContentAudio = Schema.Struct({ + "input_audio": Schema.Struct({ + "data": Schema.String.annotate({ "description": "Base64 encoded audio data" }), + "format": Schema.String.annotate({ + "description": + "Audio format (e.g., wav, mp3, flac, m4a, ogg, aiff, aac, pcm16, pcm24). Supported formats vary by provider." + }) + }), + "type": Schema.Literal("input_audio") +}).annotate({ + "description": "Audio input content part. Supported audio formats vary by provider.", + "identifier": "ChatContentAudio" +}) +export type ChatContentFile = { + readonly "file": { readonly "file_data"?: string; readonly "file_id"?: string; readonly "filename"?: string } + readonly "type": "file" } -export const PaymentRequiredResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for PaymentRequiredResponse" }) -export type NotFoundResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatContentFile = Schema.Struct({ + "file": Schema.Struct({ + "file_data": Schema.optionalKey( + Schema.String.annotate({ "description": "File content as base64 data URL or URL" }) + ), + "file_id": Schema.optionalKey(Schema.String.annotate({ "description": "File ID for previously uploaded files" })), + "filename": Schema.optionalKey(Schema.String.annotate({ "description": "Original filename" })) + }), + "type": Schema.Literal("file") +}).annotate({ "description": "File content part for document processing", "identifier": "ChatContentFile" }) +export type ChatContentImage = { + readonly "image_url": { readonly "detail"?: "auto" | "low" | "high" | "original"; readonly "url": string } + readonly "type": "image_url" } -export const NotFoundResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for NotFoundResponse" }) -export type RequestTimeoutResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatContentImage = Schema.Struct({ + "image_url": Schema.Struct({ + "detail": Schema.optionalKey( + Schema.Literals(["auto", "low", "high", "original"]).annotate({ + "description": + "Image detail level for vision models. `original` is an OpenRouter extension (not in the OpenAI Chat Completions spec) requesting true original-resolution media; it is downgraded to `high` for providers that lack an original-resolution tier." + }) + ), + "url": Schema.String.annotate({ "description": "URL of the image (data: URLs supported)" }) + }), + "type": Schema.Literal("image_url") +}).annotate({ "description": "Image content part for vision models", "identifier": "ChatContentImage" }) +export type ChatContentVideoInput = { readonly "url": string } +export const ChatContentVideoInput = Schema.Struct({ + "url": Schema.String.annotate({ "description": "URL of the video (data: URLs supported)" }) +}).annotate({ "description": "Video input object", "identifier": "ChatContentVideoInput" }) +export type ChatDebugOptions = { readonly "echo_upstream_body"?: boolean } +export const ChatDebugOptions = Schema.Struct({ + "echo_upstream_body": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "If true, includes the transformed upstream request body in a debug chunk at the start of the stream. Only works with streaming mode." + }) + ) +}).annotate({ + "description": "Debug options for inspecting request transformations (streaming only)", + "identifier": "ChatDebugOptions" +}) +export type ChatFinishReasonEnum = "tool_calls" | "stop" | "length" | "content_filter" | "error" | null +export const ChatFinishReasonEnum = Schema.Union([ + Schema.Literal("tool_calls"), + Schema.Literal("stop"), + Schema.Literal("length"), + Schema.Literal("content_filter"), + Schema.Literal("error"), + Schema.Null +]).annotate({ "identifier": "ChatFinishReasonEnum" }) +export type ChatFormatGrammarConfig = { readonly "grammar": string; readonly "type": "grammar" } +export const ChatFormatGrammarConfig = Schema.Struct({ + "grammar": Schema.String.annotate({ "description": "Custom grammar for text generation" }), + "type": Schema.Literal("grammar") +}).annotate({ "description": "Custom grammar response format", "identifier": "ChatFormatGrammarConfig" }) +export type ChatFormatJsonObjectConfig = { readonly "type": "json_object" } +export const ChatFormatJsonObjectConfig = Schema.Struct({ "type": Schema.Literal("json_object") }).annotate({ + "description": "JSON object response format", + "identifier": "ChatFormatJsonObjectConfig" +}) +export type ChatFormatPythonConfig = { readonly "type": "python" } +export const ChatFormatPythonConfig = Schema.Struct({ "type": Schema.Literal("python") }).annotate({ + "description": "Python code response format", + "identifier": "ChatFormatPythonConfig" +}) +export type ChatFormatTextConfig = { readonly "type": "text" } +export const ChatFormatTextConfig = Schema.Struct({ "type": Schema.Literal("text") }).annotate({ + "description": "Default text response format", + "identifier": "ChatFormatTextConfig" +}) +export type ChatJsonSchemaConfig = { + readonly "description"?: string + readonly "name": string + readonly "schema"?: {} + readonly "strict"?: boolean | null } -export const RequestTimeoutResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for RequestTimeoutResponse" }) -export type PayloadTooLargeResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatJsonSchemaConfig = Schema.Struct({ + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Schema description for the model" })), + "name": Schema.String.annotate({ "description": "Schema name (a-z, A-Z, 0-9, underscores, dashes, max 64 chars)" }) + .check(Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" })), + "schema": Schema.optionalKey(Schema.Struct({}).annotate({ "description": "JSON Schema object" })), + "strict": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ "description": "Enable strict schema adherence" }) + ) +}).annotate({ "description": "JSON Schema configuration object", "identifier": "ChatJsonSchemaConfig" }) +export type ChatNamedToolChoice = { readonly "function": { readonly "name": string }; readonly "type": "function" } +export const ChatNamedToolChoice = Schema.Struct({ + "function": Schema.Struct({ "name": Schema.String.annotate({ "description": "Function name to call" }) }), + "type": Schema.Literal("function") +}).annotate({ "description": "Named tool choice for specific function", "identifier": "ChatNamedToolChoice" }) +export type ChatReasoningSummaryVerbosityEnum = "auto" | "concise" | "detailed" | null +export const ChatReasoningSummaryVerbosityEnum = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("concise"), + Schema.Literal("detailed"), + Schema.Null +]).annotate({ "identifier": "ChatReasoningSummaryVerbosityEnum" }) +export type ChatServerToolChoice = { readonly "type": string } +export const ChatServerToolChoice = Schema.Struct({ + "type": Schema.String.annotate({ + "description": + "OpenRouter server-tool type to force (e.g. `openrouter:web_search`, `web_search`, `web_search_preview`)." + }) +}).annotate({ + "description": + "OpenRouter extension: force a specific server tool by naming it directly in `tool_choice.type` instead of wrapping it in `{ type: \"function\", function: { name } }`.", + "identifier": "ChatServerToolChoice" +}) +export type Objects_6 = { readonly "include_usage"?: boolean; readonly [x: string]: Schema.Json } +export const Objects_6 = Schema.StructWithRest( + Schema.Struct({ + "include_usage": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Deprecated: This field has no effect. Full usage details are always included." + }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type ChatStreamToolCall = { + readonly "function"?: { readonly "arguments"?: string; readonly "name"?: string | null } + readonly "id"?: string | null + readonly "index": number + readonly "type"?: "function" | null } -export const PayloadTooLargeResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for PayloadTooLargeResponse" }) -export type UnprocessableEntityResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatStreamToolCall = Schema.Struct({ + "function": Schema.optionalKey( + Schema.Struct({ + "arguments": Schema.optionalKey(Schema.String.annotate({ "description": "Function arguments as JSON string" })), + "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "Function call details" }) + ), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "index": Schema.Number.annotate({ "description": "Tool call index in the array" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "type": Schema.optionalKey(Schema.Union([Schema.Literal("function"), Schema.Null])) +}).annotate({ "description": "Tool call delta for streaming responses", "identifier": "ChatStreamToolCall" }) +export type ChatTokenLogprob = { + readonly "bytes": ReadonlyArray | null + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray | null; readonly "logprob": number; readonly "token": string } + > } -export const UnprocessableEntityResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for UnprocessableEntityResponse" }) -export type TooManyRequestsResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatTokenLogprob = Schema.Struct({ + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + Schema.Null + ]).annotate({ "description": "UTF-8 bytes of the token" }), + "logprob": Schema.Number.annotate({ "description": "Log probability of the token", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String.annotate({ "description": "The token" }), + "top_logprobs": Schema.Array(Schema.Struct({ + "bytes": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + Schema.Null + ]), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + })).annotate({ "description": "Top alternative tokens with probabilities" }) +}).annotate({ "description": "Token log probability information", "identifier": "ChatTokenLogprob" }) +export type ChatToolCall = { + readonly "function": { readonly "arguments": string; readonly "name": string } + readonly "id": string + readonly "type": "function" } -export const TooManyRequestsResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for TooManyRequestsResponse" }) -export type InternalServerResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const ChatToolCall = Schema.Struct({ + "function": Schema.Struct({ + "arguments": Schema.String.annotate({ "description": "Function arguments as JSON string" }), + "name": Schema.String.annotate({ "description": "Function name to call" }) + }), + "id": Schema.String.annotate({ "description": "Tool call identifier" }), + "type": Schema.Literal("function") +}).annotate({ "description": "Tool call made by the assistant", "identifier": "ChatToolCall" }) +export type Union_ = { + readonly "accepted_prediction_tokens"?: number | null + readonly "audio_tokens"?: number | null + readonly "reasoning_tokens"?: number | null + readonly "rejected_prediction_tokens"?: number | null + readonly [x: string]: Schema.Json +} | null +export const Union_ = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "accepted_prediction_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Accepted prediction tokens" }) + ), + "audio_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens used for audio output" }) + ), + "reasoning_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens used for reasoning" }) + ), + "rejected_prediction_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Rejected prediction tokens" }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Detailed completion token usage" }) +export type Union_1 = { + readonly "audio_tokens"?: number + readonly "cache_write_tokens"?: number + readonly "cached_tokens"?: number + readonly "video_tokens"?: number + readonly [x: string]: Schema.Json +} | null +export const Union_1 = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Audio input tokens" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "cache_write_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Tokens written to cache. Only returned for models with explicit caching and cache write pricing." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cached_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cached prompt tokens" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "video_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Video input tokens" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Detailed prompt token usage" }) +export type CodeInterpreterServerTool = { + readonly "container": string | { + readonly "file_ids"?: ReadonlyArray + readonly "memory_limit"?: "1g" | "4g" | "16g" | "64g" | null + readonly "type": "auto" + } + readonly "type": "code_interpreter" } -export const InternalServerResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for InternalServerResponse" }) -export type BadGatewayResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} +export const CodeInterpreterServerTool = Schema.Struct({ + "container": Schema.Union([ + Schema.String, + Schema.Struct({ + "file_ids": Schema.optionalKey(Schema.Array(Schema.String)), + "memory_limit": Schema.optionalKey( + Schema.Union([ + Schema.Literal("1g"), + Schema.Literal("4g"), + Schema.Literal("16g"), + Schema.Literal("64g"), + Schema.Null + ]) + ), + "type": Schema.Literal("auto") + }) + ]), + "type": Schema.Literal("code_interpreter") +}).annotate({ "description": "Code interpreter tool configuration", "identifier": "CodeInterpreterServerTool" }) +export type CodexLocalShellTool = { readonly "type": "local_shell" } +export const CodexLocalShellTool = Schema.Struct({ "type": Schema.Literal("local_shell") }).annotate({ + "description": "Local shell tool configuration", + "identifier": "CodexLocalShellTool" +}) +export type CompactionItem = { + readonly "encrypted_content": string + readonly "id"?: string | null + readonly "type": "compaction" } -export const BadGatewayResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for BadGatewayResponse" }) -export type ServiceUnavailableResponseErrorData = { +export const CompactionItem = Schema.Struct({ + "encrypted_content": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("compaction") +}).annotate({ "description": "A context compaction marker with encrypted summary", "identifier": "CompactionItem" }) +export type CompoundFilter = { readonly "filters": ReadonlyArray<{}>; readonly "type": "and" | "or" } +export const CompoundFilter = Schema.Struct({ + "filters": Schema.Array(Schema.Struct({})), + "type": Schema.Literals(["and", "or"]) +}).annotate({ + "description": "A compound filter that combines multiple comparison or compound filters", + "identifier": "CompoundFilter" +}) +export type ComputerUseServerTool = { + readonly "display_height": number + readonly "display_width": number + readonly "environment": "windows" | "mac" | "linux" | "ubuntu" | "browser" + readonly "type": "computer_use_preview" +} +export const ComputerUseServerTool = Schema.Struct({ + "display_height": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "display_width": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "environment": Schema.Literals(["windows", "mac", "linux", "ubuntu", "browser"]), + "type": Schema.Literal("computer_use_preview") +}).annotate({ "description": "Computer use preview tool configuration", "identifier": "ComputerUseServerTool" }) +export type ConflictResponseErrorData = { readonly "code": number readonly "message": string - readonly "metadata"?: {} + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null } -export const ServiceUnavailableResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), +export const ConflictResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for ServiceUnavailableResponse" }) + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for ConflictResponse", "identifier": "ConflictResponseErrorData" }) +export type ContainerAutoEnvironment = { readonly "type": "container_auto" } +export const ContainerAutoEnvironment = Schema.Struct({ "type": Schema.Literal("container_auto") }).annotate({ + "description": "An OpenRouter-managed, auto-provisioned ephemeral container.", + "identifier": "ContainerAutoEnvironment" +}) +export type ContainerReferenceEnvironment = { readonly "container_id": string; readonly "type": "container_reference" } +export const ContainerReferenceEnvironment = Schema.Struct({ + "container_id": Schema.String.annotate({ + "description": "Identifier of an existing container to reuse (max 20 characters)." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(20).annotate({ "expected": "a value with a length of at most 20" }) + ).check(Schema.isPattern(new RegExp("^[\\w-]+$")).annotate({ "expected": "a string matching the RegExp ^[\\w-]+$" })), + "type": Schema.Literal("container_reference") +}).annotate({ + "description": "Reference to a previously created container to reuse.", + "identifier": "ContainerReferenceEnvironment" +}) +export type ContentFilterAction = "redact" | "block" | "flag" +export const ContentFilterAction = Schema.Literals(["redact", "block", "flag"]).annotate({ + "description": "Action taken when the pattern matches", + "identifier": "ContentFilterAction" +}) +export type ContentFilterBuiltinAction = "redact" | "block" | "flag" +export const ContentFilterBuiltinAction = Schema.Literals(["redact", "block", "flag"]).annotate({ + "description": "Action taken when the builtin filter triggers", + "identifier": "ContentFilterBuiltinAction" +}) +export type ContentFilterBuiltinSlug = + | "email" + | "phone" + | "ssn" + | "credit-card" + | "ip-address" + | "person-name" + | "address" + | "regex-prompt-injection" +export const ContentFilterBuiltinSlug = Schema.Literals([ + "email", + "phone", + "ssn", + "credit-card", + "ip-address", + "person-name", + "address", + "regex-prompt-injection" +]).annotate({ "description": "The builtin filter identifier", "identifier": "ContentFilterBuiltinSlug" }) +export type ContentPartAudio = { readonly "audio_url": { readonly "url": string }; readonly "type": "audio_url" } +export const ContentPartAudio = Schema.Struct({ + "audio_url": Schema.Struct({ "url": Schema.String }), + "type": Schema.Literal("audio_url") +}).annotate({ "identifier": "ContentPartAudio" }) +export type ContentPartImage = { readonly "image_url": { readonly "url": string }; readonly "type": "image_url" } +export const ContentPartImage = Schema.Struct({ + "image_url": Schema.Struct({ "url": Schema.String }), + "type": Schema.Literal("image_url") +}).annotate({ "identifier": "ContentPartImage" }) +export type ContentPartVideo = { readonly "type": "video_url"; readonly "video_url": { readonly "url": string } } +export const ContentPartVideo = Schema.Struct({ + "type": Schema.Literal("video_url"), + "video_url": Schema.Struct({ "url": Schema.String }) +}).annotate({ "identifier": "ContentPartVideo" }) +export type ContextCompactionItem = { + readonly "encrypted_content"?: string | null + readonly "id"?: string | null + readonly "type": "context_compaction" +} +export const ContextCompactionItem = Schema.Struct({ + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("context_compaction") +}).annotate({ + "description": "A context compaction marker with an optional encrypted summary", + "identifier": "ContextCompactionItem" +}) +export type ContextCompressionEngine = "middle-out" +export const ContextCompressionEngine = Schema.Literal("middle-out").annotate({ + "description": "The compression engine to use. Defaults to \"middle-out\".", + "identifier": "ContextCompressionEngine" +}) +export type Objects_8 = { + readonly "upstream_inference_completions_cost": number + readonly "upstream_inference_cost"?: number | null + readonly "upstream_inference_prompt_cost": number + readonly [x: string]: Schema.Json +} +export const Objects_8 = Schema.StructWithRest( + Schema.Struct({ + "upstream_inference_completions_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "upstream_inference_cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "upstream_inference_prompt_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type CreateWorkspaceRequest = { + readonly "default_image_model"?: string | null + readonly "default_provider_sort"?: string | null + readonly "default_text_model"?: string | null + readonly "description"?: string | null + readonly "io_logging_api_key_ids"?: ReadonlyArray | null + readonly "io_logging_sampling_rate"?: number + readonly "is_data_discount_logging_enabled"?: boolean + readonly "is_observability_broadcast_enabled"?: boolean + readonly "is_observability_io_logging_enabled"?: boolean + readonly "name": string + readonly "slug": string +} +export const CreateWorkspaceRequest = Schema.Struct({ + "default_image_model": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Default image model for this workspace" }) + ), + "default_provider_sort": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Default provider sort preference (price, throughput, latency, exacto)" + }) + ), + "default_text_model": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Default text model for this workspace" }) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(500).annotate({ "expected": "a value with a length of at most 500" })), + Schema.Null + ]).annotate({ "description": "Description of the workspace" }) + ), + "io_logging_api_key_ids": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + Schema.Null + ]).annotate({ "description": "Optional array of API key IDs to filter I/O logging" }) + ), + "io_logging_sampling_rate": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sampling rate for I/O logging (0.0001-1)", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "is_data_discount_logging_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether data discount logging is enabled" }) + ), + "is_observability_broadcast_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether broadcast is enabled" }) + ), + "is_observability_io_logging_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether private logging is enabled" }) + ), + "name": Schema.String.annotate({ "description": "Name for the new workspace" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ).check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })), + "slug": Schema.String.annotate({ + "description": + "URL-friendly slug (lowercase alphanumeric segments separated by single hyphens, no leading/trailing hyphens)" + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(50).annotate({ "expected": "a value with a length of at most 50" }) + ).check( + Schema.isPattern(new RegExp("^[a-z0-9]+(?:-[a-z0-9]+)*$")).annotate({ + "expected": "a string matching the RegExp ^[a-z0-9]+(?:-[a-z0-9]+)*$" + }) + ) +}).annotate({ "identifier": "CreateWorkspaceRequest" }) +export type CustomTool = { + readonly "description"?: string + readonly "format"?: { readonly "type": "text" } | { + readonly "definition": string + readonly "syntax": "lark" | "regex" + readonly "type": "grammar" + } + readonly "name": string + readonly "type": "custom" +} +export const CustomTool = Schema.Struct({ + "description": Schema.optionalKey(Schema.String), + "format": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("text") }), + Schema.Struct({ + "definition": Schema.String, + "syntax": Schema.Literals(["lark", "regex"]), + "type": Schema.Literal("grammar") + }) + ]) + ), + "name": Schema.String, + "type": Schema.Literal("custom") +}).annotate({ "description": "Custom tool configuration", "identifier": "CustomTool" }) +export type CustomToolCallInputDeltaEvent = { + readonly "delta": string + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.custom_tool_call_input.delta" +} +export const CustomToolCallInputDeltaEvent = Schema.Struct({ + "delta": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.custom_tool_call_input.delta") +}).annotate({ + "description": + "Event emitted when a custom tool call's freeform input is being streamed. Mirrors `response.function_call_arguments.delta` but for `custom` tools whose input is opaque text rather than JSON arguments.", + "identifier": "CustomToolCallInputDeltaEvent" +}) +export type CustomToolCallInputDoneEvent = { + readonly "input": string + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.custom_tool_call_input.done" +} +export const CustomToolCallInputDoneEvent = Schema.Struct({ + "input": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.custom_tool_call_input.done") +}).annotate({ + "description": + "Event emitted when a custom tool call's freeform input streaming is complete. Mirrors `response.function_call_arguments.done` but for `custom` tools.", + "identifier": "CustomToolCallInputDoneEvent" +}) +export type CustomToolCallItem = { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": "custom_tool_call" +} +export const CustomToolCallItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Literal("custom_tool_call") +}).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments.", + "identifier": "CustomToolCallItem" +}) +export type DABenchmarkEntry = { + readonly "arena": string + readonly "category": string + readonly "elo": number + readonly "rank": number + readonly "win_rate": number +} +export const DABenchmarkEntry = Schema.Struct({ + "arena": Schema.String.annotate({ "description": "Arena type (e.g. models, builders, agents)" }), + "category": Schema.String.annotate({ + "description": "Category within the arena (e.g. website, gamedev, uicomponent)" + }), + "elo": Schema.Number.annotate({ "description": "ELO rating from head-to-head arena battles", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "rank": Schema.Number.annotate({ + "description": "Rank position within this arena+category among models available on OpenRouter (1 = highest ELO)" + }).check(Schema.isInt().annotate({ "expected": "an integer" })), + "win_rate": Schema.Number.annotate({ "description": "Win rate percentage in arena battles", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ + "description": "A single Design Arena benchmark entry for a specific arena+category", + "identifier": "DABenchmarkEntry" +}) +export type DatetimeServerToolConfig = { readonly "timezone"?: string } +export const DatetimeServerToolConfig = Schema.Struct({ + "timezone": Schema.optionalKey( + Schema.String.annotate({ "description": "IANA timezone name (e.g. \"America/New_York\"). Defaults to UTC." }) + ) +}).annotate({ + "description": "Configuration for the openrouter:datetime server tool", + "identifier": "DatetimeServerToolConfig" +}) +export type DebugEvent = { + readonly "debug": { + readonly "echo_upstream_body"?: {} + readonly "timings"?: { + readonly "epoch_ms": number + readonly "event": "adapter_request" | "upstream_headers_received" | "first_token_received" | "upstream_body_ended" + readonly "start_ms": number + } + } + readonly "sequence_number": number + readonly "type": "response.debug" +} +export const DebugEvent = Schema.Struct({ + "debug": Schema.Struct({ + "echo_upstream_body": Schema.optionalKey(Schema.Struct({})), + "timings": Schema.optionalKey(Schema.Struct({ + "epoch_ms": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "event": Schema.Literals([ + "adapter_request", + "upstream_headers_received", + "first_token_received", + "upstream_body_ended" + ]), + "start_ms": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + })) + }), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.debug") +}).annotate({ + "description": + "Debug event emitted when debug.echo_upstream_body is true. Contains the transformed upstream request body or timing milestones.", + "identifier": "DebugEvent" +}) +export type DefaultParameters = { + readonly "frequency_penalty"?: number | null + readonly "presence_penalty"?: number | null + readonly "repetition_penalty"?: number | null + readonly "temperature"?: number | null + readonly "top_k"?: number | null + readonly "top_p"?: number | null +} | null +export const DefaultParameters = Schema.Union([ + Schema.Struct({ + "frequency_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "presence_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "repetition_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "temperature": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "top_k": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "top_p": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ) + }), + Schema.Null +]).annotate({ "description": "Default parameters for this model", "identifier": "DefaultParameters" }) +export type DeleteBYOKKeyResponse = { readonly "deleted": true } +export const DeleteBYOKKeyResponse = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the BYOK credential was deleted." }) +}).annotate({ "identifier": "DeleteBYOKKeyResponse" }) +export type DeleteGuardrailResponse = { readonly "deleted": true } +export const DeleteGuardrailResponse = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the guardrail was deleted" }) +}).annotate({ "identifier": "DeleteGuardrailResponse" }) +export type DeleteObservabilityDestinationResponse = { readonly "deleted": true } +export const DeleteObservabilityDestinationResponse = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Always `true` on success." }) +}).annotate({ "identifier": "DeleteObservabilityDestinationResponse" }) +export type DeleteWorkspaceBudgetResponse = { readonly "deleted": true } +export const DeleteWorkspaceBudgetResponse = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ + "description": "Confirmation that the budget was deleted (or did not exist)" + }) +}).annotate({ "identifier": "DeleteWorkspaceBudgetResponse" }) +export type DeleteWorkspaceResponse = { readonly "deleted": true } +export const DeleteWorkspaceResponse = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the workspace was deleted" }) +}).annotate({ "identifier": "DeleteWorkspaceResponse" }) +export type DeprecatedRoute = "fallback" | "sort" | null +export const DeprecatedRoute = Schema.Union([Schema.Literal("fallback"), Schema.Literal("sort"), Schema.Null]).annotate( + { + "description": + "**DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: \"fallback\" (maps to \"model\"), \"sort\" (maps to \"none\").", + "identifier": "DeprecatedRoute" + } +) export type EdgeNetworkTimeoutResponseErrorData = { readonly "code": number readonly "message": string - readonly "metadata"?: {} + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null } export const EdgeNetworkTimeoutResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for EdgeNetworkTimeoutResponse" }) -export type ProviderOverloadedResponseErrorData = { + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for EdgeNetworkTimeoutResponse", + "identifier": "EdgeNetworkTimeoutResponseErrorData" +}) +export type EndpointInfo = { readonly "model": string; readonly "provider": string; readonly "selected": boolean } +export const EndpointInfo = Schema.Struct({ + "model": Schema.String, + "provider": Schema.String, + "selected": Schema.Boolean +}).annotate({ "identifier": "EndpointInfo" }) +export type EndpointStatus = 0 | -1 | -2 | -3 | -5 | -10 +export const EndpointStatus = Schema.Literals([0, -1, -2, -3, -5, -10]).annotate({ "identifier": "EndpointStatus" }) +export type EnumCapability = { readonly "type": "enum"; readonly "values": ReadonlyArray } +export const EnumCapability = Schema.Struct({ "type": Schema.Literal("enum"), "values": Schema.Array(Schema.String) }) + .annotate({ + "description": "A parameter that accepts one of a discrete set of string values.", + "identifier": "EnumCapability" + }) +export type ErrorEvent = { + readonly "code": string | null + readonly "message": string + readonly "param": string | null + readonly "sequence_number": number + readonly "type": "error" +} +export const ErrorEvent = Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "message": Schema.String, + "param": Schema.Union([Schema.String, Schema.Null]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("error") +}).annotate({ "description": "Event emitted when an error occurs during streaming", "identifier": "ErrorEvent" }) +export type FileCitation = { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": "file_citation" +} +export const FileCitation = Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_citation") +}).annotate({ "identifier": "FileCitation" }) +export type FileDeleteResponse = { readonly "id": string; readonly "type": "file_deleted" } +export const FileDeleteResponse = Schema.Struct({ "id": Schema.String, "type": Schema.Literal("file_deleted") }) + .annotate({ "description": "Confirmation that a file was deleted.", "identifier": "FileDeleteResponse" }) +export type FileMetadata = { + readonly "created_at": string + readonly "downloadable": boolean + readonly "filename": string + readonly "id": string + readonly "mime_type": string + readonly "size_bytes": number + readonly "type": "file" +} +export const FileMetadata = Schema.Struct({ + "created_at": Schema.String, + "downloadable": Schema.Boolean, + "filename": Schema.String, + "id": Schema.String, + "mime_type": Schema.String, + "size_bytes": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file") +}).annotate({ "description": "Metadata describing a stored file.", "identifier": "FileMetadata" }) +export type FilePath = { readonly "file_id": string; readonly "index": number; readonly "type": "file_path" } +export const FilePath = Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_path") +}).annotate({ "identifier": "FilePath" }) +export type FilesServerToolConfig = {} +export const FilesServerToolConfig = Schema.Struct({}).annotate({ + "description": "Configuration for the openrouter:files server tool", + "identifier": "FilesServerToolConfig" +}) +export type ForbiddenResponseErrorData = { readonly "code": number readonly "message": string - readonly "metadata"?: {} + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null } -export const ProviderOverloadedResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), +export const ForbiddenResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for ProviderOverloadedResponse" }) -export type ResponseInputVideo = { readonly "type": "input_video"; readonly "video_url": string } -export const ResponseInputVideo = Schema.Struct({ - "type": Schema.Literal("input_video"), - "video_url": Schema.String.annotate({ - "description": "A base64 data URL or remote URL that resolves to a video file" + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for ForbiddenResponse", "identifier": "ForbiddenResponseErrorData" }) +export type FormatJsonObjectConfig = { readonly "type": "json_object" } +export const FormatJsonObjectConfig = Schema.Struct({ "type": Schema.Literal("json_object") }).annotate({ + "description": "JSON object response format", + "identifier": "FormatJsonObjectConfig" +}) +export type FormatJsonSchemaConfig = { + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + readonly "type": "json_schema" +} +export const FormatJsonSchemaConfig = Schema.Struct({ + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("json_schema") +}).annotate({ "description": "JSON schema constrained response format", "identifier": "FormatJsonSchemaConfig" }) +export type FormatTextConfig = { readonly "type": "text" } +export const FormatTextConfig = Schema.Struct({ "type": Schema.Literal("text") }).annotate({ + "description": "Plain text response format", + "identifier": "FormatTextConfig" +}) +export type FrameImage = { + readonly "image_url": { readonly "url": string } + readonly "type": "image_url" + readonly "frame_type": "first_frame" | "last_frame" +} +export const FrameImage = Schema.Struct({ + "image_url": Schema.Struct({ "url": Schema.String }), + "type": Schema.Literal("image_url"), + "frame_type": Schema.Literals(["first_frame", "last_frame"]).annotate({ + "description": "Whether this image represents the first or last frame of the video" }) -}).annotate({ "description": "Video input content item" }) -export type ResponsesOutputModality = "text" | "image" -export const ResponsesOutputModality = Schema.Literals(["text", "image"]) -export type OpenAIResponsesIncludable = - | "file_search_call.results" - | "message.input_image.image_url" - | "computer_call_output.output.image_url" - | "reasoning.encrypted_content" - | "code_interpreter_call.outputs" -export const OpenAIResponsesIncludable = Schema.Literals([ - "file_search_call.results", - "message.input_image.image_url", - "computer_call_output.output.image_url", - "reasoning.encrypted_content", - "code_interpreter_call.outputs" -]) -export type DataCollection = "deny" | "allow" -export const DataCollection = Schema.Literals(["deny", "allow"]).annotate({ - "description": - "Data collection setting. If no available model provider meets the requirement, your request will return an error.\n- allow: (default) allow providers which store user data non-transiently and may train on it\n\n- deny: use only providers which do not collect user data." +}).annotate({ "identifier": "FrameImage" }) +export type FunctionCallArgsDeltaEvent = { + readonly "delta": string + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.function_call_arguments.delta" +} +export const FunctionCallArgsDeltaEvent = Schema.Struct({ + "delta": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.function_call_arguments.delta") +}).annotate({ + "description": "Event emitted when function call arguments are being streamed", + "identifier": "FunctionCallArgsDeltaEvent" }) -export type ProviderName = - | "AI21" - | "AionLabs" - | "Alibaba" - | "Ambient" - | "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" - | "Io Net" - | "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" - | "StepFun" - | "Stealth" - | "StreamLake" - | "Switchpoint" - | "Together" - | "Upstage" - | "Venice" - | "WandB" - | "Xiaomi" - | "xAI" - | "Z.AI" - | "FakeProvider" -export const ProviderName = Schema.Literals([ - "AI21", - "AionLabs", - "Alibaba", - "Ambient", - "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", - "Io Net", - "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", - "StepFun", - "Stealth", - "StreamLake", - "Switchpoint", - "Together", - "Upstage", - "Venice", - "WandB", - "Xiaomi", - "xAI", - "Z.AI", - "FakeProvider" -]) -export type Quantization = "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown" -export const Quantization = Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]) -export type ProviderSort = "price" | "throughput" | "latency" -export const ProviderSort = Schema.Literals(["price", "throughput", "latency"]) -export type BigNumberUnion = string -export const BigNumberUnion = Schema.String.annotate({ "description": "Price per million prompt tokens" }) -export type PercentileThroughputCutoffs = { - readonly "p50"?: number - readonly "p75"?: number - readonly "p90"?: number - readonly "p99"?: number +export type FunctionCallArgsDoneEvent = { + readonly "arguments": string + readonly "item_id": string + readonly "name": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.function_call_arguments.done" } -export const PercentileThroughputCutoffs = Schema.Struct({ - "p50": Schema.optionalKey( - Schema.Number.annotate({ "description": "Minimum p50 throughput (tokens/sec)" }).check(Schema.isFinite()) - ), - "p75": Schema.optionalKey( - Schema.Number.annotate({ "description": "Minimum p75 throughput (tokens/sec)" }).check(Schema.isFinite()) - ), - "p90": Schema.optionalKey( - Schema.Number.annotate({ "description": "Minimum p90 throughput (tokens/sec)" }).check(Schema.isFinite()) - ), - "p99": Schema.optionalKey( - Schema.Number.annotate({ "description": "Minimum p99 throughput (tokens/sec)" }).check(Schema.isFinite()) - ) +export const FunctionCallArgsDoneEvent = Schema.Struct({ + "arguments": Schema.String, + "item_id": Schema.String, + "name": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.function_call_arguments.done") }).annotate({ - "description": - "Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred." + "description": "Event emitted when function call arguments streaming is complete", + "identifier": "FunctionCallArgsDoneEvent" }) -export type PercentileLatencyCutoffs = { - readonly "p50"?: number - readonly "p75"?: number - readonly "p90"?: number - readonly "p99"?: number +export type FunctionTool = { + readonly "description"?: string | null + readonly "name": string + readonly "parameters": { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" } -export const PercentileLatencyCutoffs = Schema.Struct({ - "p50": Schema.optionalKey( - Schema.Number.annotate({ "description": "Maximum p50 latency (seconds)" }).check(Schema.isFinite()) - ), - "p75": Schema.optionalKey( - Schema.Number.annotate({ "description": "Maximum p75 latency (seconds)" }).check(Schema.isFinite()) - ), - "p90": Schema.optionalKey( - Schema.Number.annotate({ "description": "Maximum p90 latency (seconds)" }).check(Schema.isFinite()) - ), - "p99": Schema.optionalKey( - Schema.Number.annotate({ "description": "Maximum p99 latency (seconds)" }).check(Schema.isFinite()) - ) +export const FunctionTool = Schema.Struct({ + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "parameters": Schema.Union([ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), + Schema.Null + ]), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") +}).annotate({ "description": "Function tool definition", "identifier": "FunctionTool" }) +export type Arrays_1 = ReadonlyArray +export const Arrays_1 = Schema.Array(Schema.String) +export type Arrays_2 = ReadonlyArray +export const Arrays_2 = Schema.Array(Schema.String) +export type Arrays_3 = ReadonlyArray< + { + readonly "stances": ReadonlyArray<{ readonly "model": string; readonly "stance": string }> + readonly "topic": string + } +> +export const Arrays_3 = Schema.Array( + Schema.Struct({ + "stances": Schema.Array(Schema.Struct({ "model": Schema.String, "stance": Schema.String })), + "topic": Schema.String + }) +) +export type Arrays_4 = ReadonlyArray<{ readonly "models": ReadonlyArray; readonly "point": string }> +export const Arrays_4 = Schema.Array(Schema.Struct({ "models": Schema.Array(Schema.String), "point": Schema.String })) +export type Arrays_5 = ReadonlyArray<{ readonly "insight": string; readonly "model": string }> +export const Arrays_5 = Schema.Array(Schema.Struct({ "insight": Schema.String, "model": Schema.String })) +export type FusionCallAnalysisInProgressEvent = { + readonly "item_id": string + readonly "judge_model": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.analysis.in_progress" +} +export const FusionCallAnalysisInProgressEvent = Schema.Struct({ + "item_id": Schema.String, + "judge_model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.analysis.in_progress") }).annotate({ - "description": "Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred." + "description": "Emitted when the fusion judge starts producing the structured analysis.", + "identifier": "FusionCallAnalysisInProgressEvent" }) -export type WebSearchEngine = "native" | "exa" -export const WebSearchEngine = Schema.Literals(["native", "exa"]).annotate({ - "description": "The search engine to use for web search." +export type FusionCallCompletedEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.completed" +} +export const FusionCallCompletedEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.completed") +}).annotate({ + "description": "Emitted when the openrouter:fusion tool call finishes.", + "identifier": "FusionCallCompletedEvent" }) -export type PDFParserEngine = "mistral-ocr" | "pdf-text" | "native" -export const PDFParserEngine = Schema.Literals(["mistral-ocr", "pdf-text", "native"]).annotate({ - "description": "The engine to use for parsing PDF files." +export type FusionCallInProgressEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.in_progress" +} +export const FusionCallInProgressEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.in_progress") +}).annotate({ + "description": "Emitted when an openrouter:fusion tool call begins executing.", + "identifier": "FusionCallInProgressEvent" }) -export type AnthropicMessagesResponse = { - readonly "id": string - readonly "type": "message" - readonly "role": "assistant" - readonly "content": ReadonlyArray< - | { - readonly "type": "text" - readonly "text": string - readonly "citations": ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - readonly "file_id": string - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - readonly "file_id": string - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - readonly "file_id": string - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - } - | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } - | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } - | { readonly "type": "redacted_thinking"; readonly "data": string } - | { - readonly "type": "server_tool_use" - readonly "id": string - readonly "name": "web_search" - readonly "input"?: unknown - } - | { - readonly "type": "web_search_tool_result" - readonly "tool_use_id": string - readonly "content": - | ReadonlyArray< - { - readonly "type": "web_search_result" - readonly "encrypted_content": string - readonly "page_age": string - readonly "title": string - readonly "url": string - } - > - | { - readonly "type": "web_search_tool_result_error" - readonly "error_code": - | "invalid_tool_input" - | "unavailable" - | "max_uses_exceeded" - | "too_many_requests" - | "query_too_long" - } - } - > +export type FusionCallPanelAddedEvent = { + readonly "item_id": string readonly "model": string - readonly "stop_reason": "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" - readonly "stop_sequence": string - readonly "usage": { - readonly "input_tokens": number - readonly "output_tokens": number - readonly "cache_creation_input_tokens": number - readonly "cache_read_input_tokens": number - readonly "cache_creation": { - readonly "ephemeral_5m_input_tokens": number - readonly "ephemeral_1h_input_tokens": number - } - readonly "inference_geo": string - readonly "server_tool_use": { readonly "web_search_requests": number } - readonly "service_tier": "standard" | "priority" | "batch" - } + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.panel.added" } -export const AnthropicMessagesResponse = Schema.Struct({ - "id": Schema.String, - "type": Schema.Literal("message"), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) +export const FusionCallPanelAddedEvent = Schema.Struct({ + "item_id": Schema.String, + "model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.panel.added") +}).annotate({ + "description": "Emitted when a fusion analysis-panel model starts.", + "identifier": "FusionCallPanelAddedEvent" +}) +export type FusionCallPanelCompletedEvent = { + readonly "content": string + readonly "item_id": string + readonly "model": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.panel.completed" +} +export const FusionCallPanelCompletedEvent = Schema.Struct({ + "content": Schema.String, + "item_id": Schema.String, + "model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.panel.completed") +}).annotate({ + "description": "Emitted when a fusion panel model finishes with its full content.", + "identifier": "FusionCallPanelCompletedEvent" +}) +export type FusionCallPanelDeltaEvent = { + readonly "delta": string + readonly "item_id": string + readonly "model": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.panel.delta" +} +export const FusionCallPanelDeltaEvent = Schema.Struct({ + "delta": Schema.String, + "item_id": Schema.String, + "model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.panel.delta") +}).annotate({ + "description": "Incremental content token from a fusion panel model.", + "identifier": "FusionCallPanelDeltaEvent" +}) +export type FusionCallPanelFailedEvent = { + readonly "error": string + readonly "item_id": string + readonly "model": string + readonly "output_index": number + readonly "sequence_number": number + readonly "status_code"?: number + readonly "type": "response.fusion_call.panel.failed" +} +export const FusionCallPanelFailedEvent = Schema.Struct({ + "error": Schema.String, + "item_id": Schema.String, + "model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "status_code": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "type": Schema.Literal("response.fusion_call.panel.failed") +}).annotate({ "description": "Emitted when a fusion panel model fails.", "identifier": "FusionCallPanelFailedEvent" }) +export type FusionCallPanelReasoningDeltaEvent = { + readonly "delta": string + readonly "item_id": string + readonly "model": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.panel.reasoning.delta" +} +export const FusionCallPanelReasoningDeltaEvent = Schema.Struct({ + "delta": Schema.String, + "item_id": Schema.String, + "model": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.panel.reasoning.delta") +}).annotate({ + "description": "Incremental reasoning token from a fusion panel model.", + "identifier": "FusionCallPanelReasoningDeltaEvent" +}) +export type FusionPlugin = { + readonly "analysis_models"?: ReadonlyArray + readonly "enabled"?: boolean + readonly "id": "fusion" + readonly "max_tool_calls"?: number + readonly "model"?: string + readonly "preset"?: "general-high" | "general-budget" | "general-fast" + readonly "tools"?: ReadonlyArray<{ readonly "parameters"?: {}; readonly "type": string }> +} +export const FusionPlugin = Schema.Struct({ + "analysis_models": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Slugs of models to run in parallel as the \"expert panel\" the judge analyzes. Each model receives the same user prompt with web_search + web_fetch enabled. Capped at 8 models to bound cost amplification. When omitted, defaults to the Quality preset from the /labs/fusion UI (~anthropic/claude-opus-latest, ~openai/gpt-latest, ~google/gemini-pro-latest)." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(8).annotate({ "expected": "a value with a length of at most 8" }) + ) + ), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the fusion plugin for this request. Defaults to true." + }) + ), + "id": Schema.Literal("fusion"), + "max_tool_calls": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of tool-calling steps each panelist (analysis model) and the judge model may take during their agentic web-research loop. Models with web_search/web_fetch enabled iterate until they produce a text response or hit this ceiling. Defaults to 8. Capped at 16." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(16).annotate({ "expected": "a value less than or equal to 16" })) + ), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Slug of the model that performs both the judge step (with web_search + web_fetch) and the final synthesis. When omitted, defaults to the first model in the Quality preset." + }) + ), + "preset": Schema.optionalKey( + Schema.Literals(["general-high", "general-budget", "general-fast"]).annotate({ + "description": + "A curated OpenRouter fusion preset (slugs follow `-`, e.g. `general-high`). Expands server-side into the preset's analysis_models panel and judge model, so callers never name individual models. Explicitly provided `analysis_models` / `model` take precedence." + }) + ), + "tools": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": "Optional configuration forwarded as the tool's `parameters` object." }) - ], { mode: "oneOf" })) - }), + ), + "type": Schema.String.annotate({ + "description": "Server tool type identifier (e.g. \"openrouter:web_search\", \"openrouter:web_fetch\")." + }) + })).annotate({ + "description": + "Server tools available to panelist and judge inner calls. Each entry uses the same `{ type, parameters? }` shorthand as the outer Chat Completions request. When omitted, defaults to `[{ type: \"openrouter:web_search\" }, { type: \"openrouter:web_fetch\" }]`. Pass an empty array to disable tools entirely (panelists answer from parametric knowledge only)." + }).check(Schema.isMaxLength(8).annotate({ "expected": "a value with a length of at most 8" })) + ) +}).annotate({ "identifier": "FusionPlugin" }) +export type Arrays_6 = ReadonlyArray +export const Arrays_6 = Schema.Array(Schema.String).annotate({ + "description": + "Slugs of models to run in parallel as the analysis panel. Each model receives the user prompt with openrouter:web_search and openrouter:web_fetch enabled, then a judge model summarizes the collective output into structured analysis JSON. Capped at 8 models to bound cost amplification. Defaults to the Quality preset from /labs/fusion." +}).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(8).annotate({ "expected": "a value with a length of at most 8" }) +) +export type Objects_9 = { + readonly "effort"?: "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" + readonly "max_tokens"?: number +} +export const Objects_9 = Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Literals(["max", "xhigh", "high", "medium", "low", "minimal", "none"]).annotate({ + "description": "Reasoning effort level for panelist and judge inner calls." + }) + ), + "max_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of reasoning tokens each panelist and judge model may use. Helps bound cost when models allocate too much budget to chain-of-thought." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) +}).annotate({ + "description": + "Reasoning configuration forwarded to panelist and judge inner calls. Use this to control reasoning effort and token budget for models that support extended thinking." +}) +export type Arrays_7 = ReadonlyArray<{ readonly "parameters"?: {}; readonly "type": string }> +export const Arrays_7 = Schema.Array(Schema.Struct({ + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Optional configuration forwarded as the tool's `parameters` object." }) + ), + "type": Schema.String.annotate({ + "description": "Server tool type identifier (e.g. \"openrouter:web_search\", \"openrouter:web_fetch\")." + }) +})).annotate({ + "description": + "Server tools available to panelist and judge inner calls. Each entry uses the same `{ type, parameters? }` shorthand as the outer Chat Completions request. When omitted, defaults to `[{ type: \"openrouter:web_search\" }, { type: \"openrouter:web_fetch\" }]`. Pass an empty array to disable tools entirely (panelists answer from parametric knowledge only)." +}).check(Schema.isMaxLength(8).annotate({ "expected": "a value with a length of at most 8" })) +export type FusionSource = { readonly "title": string; readonly "url": string } +export const FusionSource = Schema.Struct({ + "title": Schema.String.annotate({ "description": "Title of the retrieved web page." }), + "url": Schema.String.annotate({ + "description": "URL of the web page a panel or the judge retrieved during the run." + }) +}).annotate({ "description": "A web page retrieved via web search during a fusion run.", "identifier": "FusionSource" }) +export type GenerationContentData = { + readonly "input": { readonly "prompt": string } | { readonly "messages": ReadonlyArray } + readonly "output": { readonly "completion": string | null; readonly "reasoning": string | null } +} +export const GenerationContentData = Schema.Struct({ + "input": Schema.Union([ + Schema.Struct({ "prompt": Schema.String }), + Schema.Struct({ "messages": Schema.Array(Schema.Json.annotate({ "expected": "JSON value" })) }) + ]).annotate({ "description": "The input to the generation — either a prompt string or an array of messages" }), + "output": Schema.Struct({ + "completion": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The completion output" }), + "reasoning": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Reasoning/thinking output, if any" + }) + }).annotate({ "description": "The output from the generation" }) +}).annotate({ "description": "Stored prompt and completion content", "identifier": "GenerationContentData" }) +export type GoneResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const GoneResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for GoneResponse", "identifier": "GoneResponseErrorData" }) +export type GuardrailInterval = "daily" | "weekly" | "monthly" | null +export const GuardrailInterval = Schema.Union([ + Schema.Literal("daily"), + Schema.Literal("weekly"), + Schema.Literal("monthly"), + Schema.Null +]).annotate({ + "description": "Interval at which the limit resets (daily, weekly, monthly)", + "identifier": "GuardrailInterval" +}) +export type ImageConfig = {} +export const ImageConfig = Schema.Struct({}).annotate({ + "description": + "Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/guides/overview/multimodal/image-generation for more details.", + "identifier": "ImageConfig" +}) +export type ImageGenCallCompletedEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.completed" +} +export const ImageGenCallCompletedEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.completed") +}).annotate({ "description": "Image generation call completed", "identifier": "ImageGenCallCompletedEvent" }) +export type ImageGenCallGeneratingEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.generating" +} +export const ImageGenCallGeneratingEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.generating") +}).annotate({ "description": "Image generation call is generating", "identifier": "ImageGenCallGeneratingEvent" }) +export type ImageGenCallInProgressEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.in_progress" +} +export const ImageGenCallInProgressEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.in_progress") +}).annotate({ "description": "Image generation call in progress", "identifier": "ImageGenCallInProgressEvent" }) +export type ImageGenCallPartialImageEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "partial_image_b64": string + readonly "partial_image_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.partial_image" +} +export const ImageGenCallPartialImageEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "partial_image_b64": Schema.String, + "partial_image_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.partial_image") +}).annotate({ + "description": "Image generation call with partial image", + "identifier": "ImageGenCallPartialImageEvent" +}) +export type ImageGenerationServerTool = { + readonly "background"?: "transparent" | "opaque" | "auto" + readonly "input_fidelity"?: "high" | "low" | null + readonly "input_image_mask"?: { readonly "file_id"?: string; readonly "image_url"?: string } + readonly "model"?: string + readonly "moderation"?: "auto" | "low" + readonly "output_compression"?: number + readonly "output_format"?: "png" | "webp" | "jpeg" + readonly "partial_images"?: number + readonly "quality"?: "low" | "medium" | "high" | "auto" + readonly "size"?: string + readonly "type": "image_generation" +} +export const ImageGenerationServerTool = Schema.Struct({ + "background": Schema.optionalKey(Schema.Literals(["transparent", "opaque", "auto"])), + "input_fidelity": Schema.optionalKey(Schema.Union([Schema.Literal("high"), Schema.Literal("low"), Schema.Null])), + "input_image_mask": Schema.optionalKey( + Schema.Struct({ "file_id": Schema.optionalKey(Schema.String), "image_url": Schema.optionalKey(Schema.String) }) + ), + "model": Schema.optionalKey(Schema.String), + "moderation": Schema.optionalKey(Schema.Literals(["auto", "low"])), + "output_compression": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_format": Schema.optionalKey(Schema.Literals(["png", "webp", "jpeg"])), + "partial_images": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "quality": Schema.optionalKey(Schema.Literals(["low", "medium", "high", "auto"])), + "size": Schema.optionalKey(Schema.String), + "type": Schema.Literal("image_generation") +}).annotate({ "description": "Image generation tool configuration", "identifier": "ImageGenerationServerTool" }) +export type ImageGenerationServerToolConfig = { readonly "model"?: string } +export const ImageGenerationServerToolConfig = Schema.Struct({ + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Which image generation model to use (e.g. \"openai/gpt-5-image\"). Defaults to \"openai/gpt-5-image\"." + }) + ) +}).annotate({ + "description": + "Configuration for the openrouter:image_generation server tool. Accepts all image_config params (aspect_ratio, quality, size, background, output_format, output_compression, moderation, etc.) plus a model field.", + "identifier": "ImageGenerationServerToolConfig" +}) +export type ImageGenerationStatus = "in_progress" | "completed" | "generating" | "failed" +export const ImageGenerationStatus = Schema.Literals(["in_progress", "completed", "generating", "failed"]).annotate({ + "identifier": "ImageGenerationStatus" +}) +export type Union_6 = { + readonly "audio_tokens"?: number | null + readonly "image_tokens"?: number | null + readonly "reasoning_tokens"?: number | null + readonly [x: string]: Schema.Json +} | null +export const Union_6 = Schema.Union([ + Schema.StructWithRest( Schema.Struct({ - "type": Schema.Literal("tool_use"), - "id": Schema.String, - "name": Schema.String, - "input": Schema.optionalKey(Schema.Unknown) + "audio_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens generated by the model for audio output." }) + ), + "image_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens generated by the model for image output." }) + ), + "reasoning_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens generated by the model for reasoning." }) + ) }), - Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), - Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]) +export type Union_8 = { + readonly "audio_tokens"?: number | null + readonly "cache_write_tokens"?: number | null + readonly "cached_tokens"?: number | null + readonly "file_tokens"?: number | null + readonly "video_tokens"?: number | null + readonly [x: string]: Schema.Json +} | null +export const Union_8 = Schema.Union([ + Schema.StructWithRest( Schema.Struct({ - "type": Schema.Literal("server_tool_use"), - "id": Schema.String, - "name": Schema.Literal("web_search"), - "input": Schema.optionalKey(Schema.Unknown) + "audio_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens used for input audio." }) + ), + "cache_write_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ + "description": + "Tokens written to cache. Only returned for models with explicit caching and cache write pricing." + }) + ), + "cached_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens cached by the endpoint." }) + ), + "file_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens used for input files/documents." }) + ), + "video_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Tokens used for input video." }) + ) }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Breakdown of tokens used in the prompt." }) +export type Union_9 = { + readonly "tool_calls_executed"?: number | null + readonly "tool_calls_requested"?: number | null + readonly "web_search_requests"?: number | null + readonly [x: string]: Schema.Json +} | null +export const Union_9 = Schema.Union([ + Schema.StructWithRest( Schema.Struct({ - "type": Schema.Literal("web_search_tool_result"), - "tool_use_id": Schema.String, - "content": Schema.Union([ - Schema.Array( - Schema.Struct({ - "type": Schema.Literal("web_search_result"), - "encrypted_content": Schema.String, - "page_age": Schema.String, - "title": Schema.String, - "url": Schema.String + "tool_calls_executed": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "Number of OpenRouter server tool calls that executed and produced a result." }) + ), + "tool_calls_requested": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ + "description": + "Total number of OpenRouter server-orchestrated tool calls the model requested, across all tool types. Provider-native tools (e.g. native web search) are not counted here." }) - ), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result_error"), - "error_code": Schema.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long" - ]) - }) - ]) - }) - ], { mode: "oneOf" })), - "model": Schema.String, - "stop_reason": Schema.Literals(["end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"]), - "stop_sequence": Schema.String, - "usage": Schema.Struct({ - "input_tokens": Schema.Number.check(Schema.isFinite()), - "output_tokens": Schema.Number.check(Schema.isFinite()), - "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), - "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), - "cache_creation": Schema.Struct({ - "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isFinite()), - "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isFinite()) + ), + "web_search_requests": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ + "description": + "Number of web searches performed by server-side tools. For server-orchestrated tool calls a web search is also counted in tool_calls_requested; provider-native web search may report web_search_requests only. Do not sum the two." + }) + ) }), - "inference_geo": Schema.String, - "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }), - "service_tier": Schema.Literals(["standard", "priority", "batch"]) - }) -}).annotate({ "description": "Non-streaming response from the Anthropic Messages API with OpenRouter extensions" }) -export type AnthropicMessagesStreamEvent = - | { - readonly "type": "message_start" - readonly "message": { - readonly "id": string - readonly "type": "message" - readonly "role": "assistant" - readonly "content": ReadonlyArray< - | { - readonly "type": "text" - readonly "text": string - readonly "citations": ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - readonly "file_id": string - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - readonly "file_id": string - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - readonly "file_id": string - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - } - | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } - | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } - | { readonly "type": "redacted_thinking"; readonly "data": string } - | { - readonly "type": "server_tool_use" - readonly "id": string - readonly "name": "web_search" - readonly "input"?: unknown - } - | { - readonly "type": "web_search_tool_result" - readonly "tool_use_id": string - readonly "content": - | ReadonlyArray< - { - readonly "type": "web_search_result" - readonly "encrypted_content": string - readonly "page_age": string - readonly "title": string - readonly "url": string - } - > - | { - readonly "type": "web_search_tool_result_error" - readonly "error_code": - | "invalid_tool_input" - | "unavailable" - | "max_uses_exceeded" - | "too_many_requests" - | "query_too_long" - } - } - > - readonly "model": string - readonly "stop_reason": unknown - readonly "stop_sequence": unknown - readonly "usage": { - readonly "input_tokens": number - readonly "output_tokens": number - readonly "cache_creation_input_tokens": number - readonly "cache_read_input_tokens": number - readonly "cache_creation": { - readonly "ephemeral_5m_input_tokens": number - readonly "ephemeral_1h_input_tokens": number - } - readonly "inference_geo": string - readonly "server_tool_use": { readonly "web_search_requests": number } - readonly "service_tier": "standard" | "priority" | "batch" - } - } - } - | { - readonly "type": "message_delta" - readonly "delta": { - readonly "stop_reason": "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal" - readonly "stop_sequence": string - } - readonly "usage": { - readonly "input_tokens": number - readonly "output_tokens": number - readonly "cache_creation_input_tokens": number - readonly "cache_read_input_tokens": number - readonly "server_tool_use": { readonly "web_search_requests": number } - } - } - | { readonly "type": "message_stop" } - | { - readonly "type": "content_block_start" - readonly "index": number - readonly "content_block": - | { - readonly "type": "text" - readonly "text": string - readonly "citations": ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - readonly "file_id": string - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - readonly "file_id": string - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - readonly "file_id": string - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - } - | { readonly "type": "tool_use"; readonly "id": string; readonly "name": string; readonly "input"?: unknown } - | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } - | { readonly "type": "redacted_thinking"; readonly "data": string } - | { - readonly "type": "server_tool_use" - readonly "id": string - readonly "name": "web_search" - readonly "input"?: unknown - } - | { - readonly "type": "web_search_tool_result" - readonly "tool_use_id": string - readonly "content": - | ReadonlyArray< - { - readonly "type": "web_search_result" - readonly "encrypted_content": string - readonly "page_age": string - readonly "title": string - readonly "url": string - } - > - | { - readonly "type": "web_search_tool_result_error" - readonly "error_code": - | "invalid_tool_input" - | "unavailable" - | "max_uses_exceeded" - | "too_many_requests" - | "query_too_long" - } - } - } - | { - readonly "type": "content_block_delta" - readonly "index": number - readonly "delta": - | { readonly "type": "text_delta"; readonly "text": string } - | { readonly "type": "input_json_delta"; readonly "partial_json": string } - | { readonly "type": "thinking_delta"; readonly "thinking": string } - | { readonly "type": "signature_delta"; readonly "signature": string } - | { - readonly "type": "citations_delta" - readonly "citation": { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - readonly "file_id": string - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - readonly "file_id": string - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - readonly "file_id": string - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - } + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Usage for server-side tool execution (e.g., web search)" }) +export type ImageGenPartialImageEvent = { + readonly "b64_json": string + readonly "partial_image_index": number + readonly "type": "image_generation.partial_image" +} +export const ImageGenPartialImageEvent = Schema.Struct({ + "b64_json": Schema.String.annotate({ "description": "Base64-encoded partial image data" }), + "partial_image_index": Schema.Number.annotate({ + "description": "0-based index indicating which partial image this is in the sequence" + }).check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("image_generation.partial_image").annotate({ "description": "The event type" }) +}).annotate({ + "description": "Emitted when a partial image becomes available during streaming generation", + "identifier": "ImageGenPartialImageEvent" +}) +export type ImageGenStreamErrorEvent = { + readonly "error": { + readonly "code"?: string | null + readonly "message": string + readonly "param"?: string | null + readonly "type"?: string | null } - | { readonly "type": "content_block_stop"; readonly "index": number } - | { readonly "type": "ping" } - | { readonly "type": "error"; readonly "error": { readonly "type": string; readonly "message": string } } -export const AnthropicMessagesStreamEvent = Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("message_start"), - "message": Schema.Struct({ - "id": Schema.String, - "type": Schema.Literal("message"), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" })) - }), - Schema.Struct({ - "type": Schema.Literal("tool_use"), - "id": Schema.String, - "name": Schema.String, - "input": Schema.optionalKey(Schema.Unknown) - }), - Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), - Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), - Schema.Struct({ - "type": Schema.Literal("server_tool_use"), - "id": Schema.String, - "name": Schema.Literal("web_search"), - "input": Schema.optionalKey(Schema.Unknown) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result"), - "tool_use_id": Schema.String, - "content": Schema.Union([ - Schema.Array( - Schema.Struct({ - "type": Schema.Literal("web_search_result"), - "encrypted_content": Schema.String, - "page_age": Schema.String, - "title": Schema.String, - "url": Schema.String - }) - ), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result_error"), - "error_code": Schema.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long" - ]) - }) - ]) - }) - ], { mode: "oneOf" })), - "model": Schema.String, - "stop_reason": Schema.Unknown, - "stop_sequence": Schema.Unknown, - "usage": Schema.Struct({ - "input_tokens": Schema.Number.check(Schema.isFinite()), - "output_tokens": Schema.Number.check(Schema.isFinite()), - "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), - "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), - "cache_creation": Schema.Struct({ - "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isFinite()), - "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isFinite()) - }), - "inference_geo": Schema.String, - "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }), - "service_tier": Schema.Literals(["standard", "priority", "batch"]) + readonly "type": "error" +} +export const ImageGenStreamErrorEvent = Schema.Struct({ + "error": Schema.Struct({ + "code": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Provider error code, when supplied" }) + ), + "message": Schema.String.annotate({ "description": "Provider error message" }), + "param": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Request parameter associated with the error, when supplied" }) - }) + ), + "type": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Provider error type, when supplied" }) + ) + }).annotate({ "description": "Provider error details" }), + "type": Schema.Literal("error").annotate({ "description": "The event type" }) +}).annotate({ + "description": "Emitted when streaming generation fails after the SSE response starts", + "identifier": "ImageGenStreamErrorEvent" +}) +export type ImageGenTextChunkEvent = { + readonly "phase": "content" | "reasoning" | "draft" + readonly "text": string + readonly "type": "image_generation.text_chunk" +} +export const ImageGenTextChunkEvent = Schema.Struct({ + "phase": Schema.Literals(["content", "reasoning", "draft"]).annotate({ + "description": + "The generation phase this chunk belongs to. `content` is the renderable output; `reasoning` and `draft` are intermediate provider phases." }), - Schema.Struct({ - "type": Schema.Literal("message_delta"), - "delta": Schema.Struct({ - "stop_reason": Schema.Literals(["end_turn", "max_tokens", "stop_sequence", "tool_use", "pause_turn", "refusal"]), - "stop_sequence": Schema.String - }), - "usage": Schema.Struct({ - "input_tokens": Schema.Number.check(Schema.isFinite()), - "output_tokens": Schema.Number.check(Schema.isFinite()), - "cache_creation_input_tokens": Schema.Number.check(Schema.isFinite()), - "cache_read_input_tokens": Schema.Number.check(Schema.isFinite()), - "server_tool_use": Schema.Struct({ "web_search_requests": Schema.Number.check(Schema.isFinite()) }) - }) + "text": Schema.String.annotate({ + "description": "A text fragment of the image being generated (e.g. partial SVG markup)" }), - Schema.Struct({ "type": Schema.Literal("message_stop") }), - Schema.Struct({ - "type": Schema.Literal("content_block_start"), - "index": Schema.Number.check(Schema.isFinite()), - "content_block": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" })) - }), + "type": Schema.Literal("image_generation.text_chunk").annotate({ "description": "The event type" }) +}).annotate({ + "description": + "Emitted when a text chunk becomes available during streaming generation of text-based formats (e.g. SVG)", + "identifier": "ImageGenTextChunkEvent" +}) +export type ImageInputModality = "text" | "image" | "file" | "audio" | "video" +export const ImageInputModality = Schema.Literals(["text", "image", "file", "audio", "video"]).annotate({ + "identifier": "ImageInputModality" +}) +export type ImageOutputModality = + | "text" + | "image" + | "embeddings" + | "audio" + | "video" + | "rerank" + | "speech" + | "transcription" +export const ImageOutputModality = Schema.Literals([ + "text", + "image", + "embeddings", + "audio", + "video", + "rerank", + "speech", + "transcription" +]).annotate({ "identifier": "ImageOutputModality" }) +export type ImagePricingEntry = { + readonly "billable": "output_image" | "input_image" | "input_font" | "input_reference" | "input_text" + readonly "cost_usd": number + readonly "unit": "image" | "megapixel" | "token" + readonly "variant"?: string +} +export const ImagePricingEntry = Schema.Struct({ + "billable": Schema.Literals(["output_image", "input_image", "input_font", "input_reference", "input_text"]), + "cost_usd": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "unit": Schema.Literals(["image", "megapixel", "token"]), + "variant": Schema.optionalKey(Schema.String) +}).annotate({ "description": "One billable pricing line for an image provider.", "identifier": "ImagePricingEntry" }) +export type IncompleteDetails = { + readonly "reason"?: "max_output_tokens" | "content_filter" + readonly [x: string]: Schema.Json +} | null +export const IncompleteDetails = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ "reason": Schema.optionalKey(Schema.Literals(["max_output_tokens", "content_filter"])) }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "identifier": "IncompleteDetails" }) +export type InputAudio = { + readonly "input_audio": { readonly "data": string; readonly "format": "mp3" | "wav" } + readonly "type": "input_audio" +} +export const InputAudio = Schema.Struct({ + "input_audio": Schema.Struct({ "data": Schema.String, "format": Schema.Literals(["mp3", "wav"]) }), + "type": Schema.Literal("input_audio") +}).annotate({ "description": "Audio input content item", "identifier": "InputAudio" }) +export type InputFile = { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": "input_file" +} +export const InputFile = Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Literal("input_file") +}).annotate({ "description": "File input content item", "identifier": "InputFile" }) +export type InputImage = { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" +} +export const InputImage = Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("input_image") +}).annotate({ "description": "Image input content item", "identifier": "InputImage" }) +export type InputModality = "text" | "image" | "file" | "audio" | "video" +export const InputModality = Schema.Literals(["text", "image", "file", "audio", "video"]).annotate({ + "identifier": "InputModality" +}) +export type InputVideo = { readonly "type": "input_video"; readonly "video_url": string } +export const InputVideo = Schema.Struct({ + "type": Schema.Literal("input_video"), + "video_url": Schema.String.annotate({ + "description": "A base64 data URL or remote URL that resolves to a video file" + }) +}).annotate({ "description": "Video input content item", "identifier": "InputVideo" }) +export type InstructType = + | "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" + | null +export const InstructType = Schema.Union([ + Schema.Literal("none"), + Schema.Literal("airoboros"), + Schema.Literal("alpaca"), + Schema.Literal("alpaca-modif"), + Schema.Literal("chatml"), + Schema.Literal("claude"), + Schema.Literal("code-llama"), + Schema.Literal("gemma"), + Schema.Literal("llama2"), + Schema.Literal("llama3"), + Schema.Literal("mistral"), + Schema.Literal("nemotron"), + Schema.Literal("neural"), + Schema.Literal("openchat"), + Schema.Literal("phi3"), + Schema.Literal("rwkv"), + Schema.Literal("vicuna"), + Schema.Literal("zephyr"), + Schema.Literal("deepseek-r1"), + Schema.Literal("deepseek-v3.1"), + Schema.Literal("qwq"), + Schema.Literal("qwen3"), + Schema.Null +]).annotate({ "description": "Instruction format type", "identifier": "InstructType" }) +export type InternalServerResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const InternalServerResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for InternalServerResponse", "identifier": "InternalServerResponseErrorData" }) +export type ItemReferenceItem = { readonly "id": string; readonly "type": "item_reference" } +export const ItemReferenceItem = Schema.Struct({ "id": Schema.String, "type": Schema.Literal("item_reference") }) + .annotate({ "description": "A reference to a previous response item by ID", "identifier": "ItemReferenceItem" }) +export type KeyAssignment = { + readonly "assigned_by": string | null + readonly "created_at": string + readonly "guardrail_id": string + readonly "id": string + readonly "key_hash": string + readonly "key_label": string + readonly "key_name": string +} +export const KeyAssignment = Schema.Struct({ + "assigned_by": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "User ID of who made the assignment" + }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "key_hash": Schema.String.annotate({ "description": "Hash of the assigned API key" }), + "key_label": Schema.String.annotate({ "description": "Label of the API key" }), + "key_name": Schema.String.annotate({ "description": "Name of the API key" }) +}).annotate({ "identifier": "KeyAssignment" }) +export type Legacy_ChatContentVideoInput = { readonly "url": string } +export const Legacy_ChatContentVideoInput = Schema.Struct({ + "url": Schema.String.annotate({ "description": "URL of the video (data: URLs supported)" }) +}).annotate({ "description": "Video input object", "identifier": "Legacy_ChatContentVideoInput" }) +export type McpApprovalRequestItem = { + readonly "arguments": string + readonly "id": string + readonly "name": string + readonly "server_label": string + readonly "type": "mcp_approval_request" +} +export const McpApprovalRequestItem = Schema.Struct({ + "arguments": Schema.String, + "id": Schema.String, + "name": Schema.String, + "server_label": Schema.String, + "type": Schema.Literal("mcp_approval_request") +}).annotate({ "description": "Request for approval to execute an MCP tool", "identifier": "McpApprovalRequestItem" }) +export type McpApprovalResponseItem = { + readonly "approval_request_id": string + readonly "approve": boolean + readonly "id"?: string | null + readonly "reason"?: string | null + readonly "type": "mcp_approval_response" +} +export const McpApprovalResponseItem = Schema.Struct({ + "approval_request_id": Schema.String, + "approve": Schema.Boolean, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "reason": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("mcp_approval_response") +}).annotate({ "description": "User response to an MCP tool approval request", "identifier": "McpApprovalResponseItem" }) +export type McpCallItem = { + readonly "arguments": string + readonly "error"?: string | null + readonly "id": string + readonly "name": string + readonly "output"?: string | null + readonly "server_label": string + readonly "type": "mcp_call" +} +export const McpCallItem = Schema.Struct({ + "arguments": Schema.String, + "error": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "name": Schema.String, + "output": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "server_label": Schema.String, + "type": Schema.Literal("mcp_call") +}).annotate({ "description": "An MCP tool call with its output or error", "identifier": "McpCallItem" }) +export type McpListToolsItem = { + readonly "error"?: string | null + readonly "id": string + readonly "server_label": string + readonly "tools": ReadonlyArray< + { + readonly "annotations"?: Schema.Json + readonly "description"?: string | null + readonly "input_schema": {} + readonly "name": string + } + > + readonly "type": "mcp_list_tools" +} +export const McpListToolsItem = Schema.Struct({ + "error": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "server_label": Schema.String, + "tools": Schema.Array( + Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "input_schema": Schema.Struct({}), + "name": Schema.String + }) + ), + "type": Schema.Literal("mcp_list_tools") +}).annotate({ "description": "List of available MCP tools from a server", "identifier": "McpListToolsItem" }) +export type McpServerTool = { + readonly "allowed_tools"?: ReadonlyArray | { + readonly "read_only"?: boolean + readonly "tool_names"?: ReadonlyArray + } | null + readonly "authorization"?: string + readonly "connector_id"?: + | "connector_dropbox" + | "connector_gmail" + | "connector_googlecalendar" + | "connector_googledrive" + | "connector_microsoftteams" + | "connector_outlookcalendar" + | "connector_outlookemail" + | "connector_sharepoint" + readonly "headers"?: { readonly [x: string]: string } | null + readonly "require_approval"?: + | { + readonly "always"?: { readonly "tool_names"?: ReadonlyArray } + readonly "never"?: { readonly "tool_names"?: ReadonlyArray } + } + | "always" + | "never" + | null + readonly "server_description"?: string + readonly "server_label": string + readonly "server_url"?: string + readonly "type": "mcp" +} +export const McpServerTool = Schema.Struct({ + "allowed_tools": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String), Schema.Struct({ - "type": Schema.Literal("tool_use"), - "id": Schema.String, - "name": Schema.String, - "input": Schema.optionalKey(Schema.Unknown) + "read_only": Schema.optionalKey(Schema.Boolean), + "tool_names": Schema.optionalKey(Schema.Array(Schema.String)) }), - Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), - Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), + Schema.Null + ]) + ), + "authorization": Schema.optionalKey(Schema.String), + "connector_id": Schema.optionalKey( + Schema.Literals([ + "connector_dropbox", + "connector_gmail", + "connector_googlecalendar", + "connector_googledrive", + "connector_microsoftteams", + "connector_outlookcalendar", + "connector_outlookemail", + "connector_sharepoint" + ]) + ), + "headers": Schema.optionalKey(Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null])), + "require_approval": Schema.optionalKey( + Schema.Union([ Schema.Struct({ - "type": Schema.Literal("server_tool_use"), - "id": Schema.String, - "name": Schema.Literal("web_search"), - "input": Schema.optionalKey(Schema.Unknown) + "always": Schema.optionalKey(Schema.Struct({ "tool_names": Schema.optionalKey(Schema.Array(Schema.String)) })), + "never": Schema.optionalKey(Schema.Struct({ "tool_names": Schema.optionalKey(Schema.Array(Schema.String)) })) }), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result"), - "tool_use_id": Schema.String, - "content": Schema.Union([ - Schema.Array( - Schema.Struct({ - "type": Schema.Literal("web_search_result"), - "encrypted_content": Schema.String, - "page_age": Schema.String, - "title": Schema.String, - "url": Schema.String - }) + Schema.Literal("always"), + Schema.Literal("never"), + Schema.Null + ]) + ), + "server_description": Schema.optionalKey(Schema.String), + "server_label": Schema.String, + "server_url": Schema.optionalKey(Schema.String), + "type": Schema.Literal("mcp") +}).annotate({ "description": "MCP (Model Context Protocol) tool configuration", "identifier": "McpServerTool" }) +export type MemberAssignment = { + readonly "assigned_by": string | null + readonly "created_at": string + readonly "guardrail_id": string + readonly "id": string + readonly "organization_id": string + readonly "user_id": string +} +export const MemberAssignment = Schema.Struct({ + "assigned_by": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "User ID of who made the assignment" + }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }), + "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), + "organization_id": Schema.String.annotate({ "description": "Organization ID" }), + "user_id": Schema.String.annotate({ "description": "Clerk user ID of the assigned member" }) +}).annotate({ "identifier": "MemberAssignment" }) +export type MessagesAdvisorToolResultBlock = { + readonly "content": {} + readonly "tool_use_id": string + readonly "type": "advisor_tool_result" +} +export const MessagesAdvisorToolResultBlock = Schema.Struct({ + "content": Schema.Struct({}), + "tool_use_id": Schema.String, + "type": Schema.Literal("advisor_tool_result") +}).annotate({ + "description": + "Advisor tool result from a prior assistant turn, replayed back to the model on the next turn. Mirrors the block Anthropic returns in assistant content when the `advisor_20260301` tool runs.", + "identifier": "MessagesAdvisorToolResultBlock" +}) +export type MessagesContentBlockStopEvent = { readonly "index": number; readonly "type": "content_block_stop" } +export const MessagesContentBlockStopEvent = Schema.Struct({ + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("content_block_stop") +}).annotate({ + "description": "Event sent when a content block is complete", + "identifier": "MessagesContentBlockStopEvent" +}) +export type MessagesFallbackParam = { readonly "model": string } +export const MessagesFallbackParam = Schema.Struct({ "model": Schema.String }).annotate({ + "description": + "Fallback model to try when the primary model fails or refuses. Only the `model` field is supported; per-attempt overrides are rejected.", + "identifier": "MessagesFallbackParam" +}) +export type Union_10 = + | { readonly "schema": {}; readonly "type": "json_schema"; readonly [x: string]: Schema.Json } + | null +export const Union_10 = Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "schema": Schema.Struct({}), "type": Schema.Literal("json_schema") }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]), + Schema.Null +]).annotate({ + "description": + "A schema to specify Claude's output format in responses. See [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs)." +}) +export type Union_11 = { + readonly "remaining"?: number | null + readonly "total": number + readonly "type": "tokens" + readonly [x: string]: Schema.Json +} | null +export const Union_11 = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "remaining": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) ), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result_error"), - "error_code": Schema.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long" - ]) - }) + Schema.Null ]) - }) - ], { mode: "oneOf" }) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_delta"), - "index": Schema.Number.check(Schema.isFinite()), - "delta": Schema.Union([ - Schema.Struct({ "type": Schema.Literal("text_delta"), "text": Schema.String }), - Schema.Struct({ "type": Schema.Literal("input_json_delta"), "partial_json": Schema.String }), - Schema.Struct({ "type": Schema.Literal("thinking_delta"), "thinking": Schema.String }), - Schema.Struct({ "type": Schema.Literal("signature_delta"), "signature": Schema.String }), - Schema.Struct({ - "type": Schema.Literal("citations_delta"), - "citation": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }) - }) - ], { mode: "oneOf" }) - }), - Schema.Struct({ "type": Schema.Literal("content_block_stop"), "index": Schema.Number.check(Schema.isFinite()) }), - Schema.Struct({ "type": Schema.Literal("ping") }), - Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) - }) -], { mode: "oneOf" }).annotate({ "description": "Union of all possible streaming events" }) -export type OpenRouterAnthropicMessageParam = { - readonly "role": "user" | "assistant" - readonly "content": - | string - | ReadonlyArray< - | { - readonly "type": "text" - readonly "text": string - readonly "citations"?: ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "image" - readonly "source": { - readonly "type": "base64" - readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" - readonly "data": string - } | { readonly "type": "url"; readonly "url": string } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "document" - readonly "source": - | { readonly "type": "base64"; readonly "media_type": "application/pdf"; readonly "data": string } - | { readonly "type": "text"; readonly "media_type": "text/plain"; readonly "data": string } - | { - readonly "type": "content" - readonly "content": - | string - | ReadonlyArray< - { - readonly "type": "text" - readonly "text": string - readonly "citations"?: ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } | { - readonly "type": "image" - readonly "source": { - readonly "type": "base64" - readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" - readonly "data": string - } | { readonly "type": "url"; readonly "url": string } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > - } - | { readonly "type": "url"; readonly "url": string } - readonly "citations"?: { readonly "enabled"?: boolean } - readonly "context"?: string - readonly "title"?: string - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "tool_use" - readonly "id": string - readonly "name": string - readonly "input"?: unknown - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "tool_result" - readonly "tool_use_id": string - readonly "content"?: - | string - | ReadonlyArray< - { - readonly "type": "text" - readonly "text": string - readonly "citations"?: ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } | { - readonly "type": "image" - readonly "source": { - readonly "type": "base64" - readonly "media_type": "image/jpeg" | "image/png" | "image/gif" | "image/webp" - readonly "data": string - } | { readonly "type": "url"; readonly "url": string } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > - readonly "is_error"?: boolean - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { readonly "type": "thinking"; readonly "thinking": string; readonly "signature": string } - | { readonly "type": "redacted_thinking"; readonly "data": string } - | { - readonly "type": "server_tool_use" - readonly "id": string - readonly "name": "web_search" - readonly "input"?: unknown - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "web_search_tool_result" - readonly "tool_use_id": string - readonly "content": - | ReadonlyArray< - { - readonly "type": "web_search_result" - readonly "encrypted_content": string - readonly "title": string - readonly "url": string - readonly "page_age"?: string - } - > - | { - readonly "type": "web_search_tool_result_error" - readonly "error_code": - | "invalid_tool_input" - | "unavailable" - | "max_uses_exceeded" - | "too_many_requests" - | "query_too_long" - } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - | { - readonly "type": "search_result" - readonly "source": string - readonly "title": string - readonly "content": ReadonlyArray< - { - readonly "type": "text" - readonly "text": string - readonly "citations"?: ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > - readonly "citations"?: { readonly "enabled"?: boolean } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > -} -export const OpenRouterAnthropicMessageParam = Schema.Struct({ - "role": Schema.Literals(["user", "assistant"]), - "content": Schema.Union([ - Schema.String, - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }))), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("image"), - "source": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("base64"), - "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), - "data": Schema.String - }), - Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) - ], { mode: "oneOf" }), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("document"), - "source": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("base64"), - "media_type": Schema.Literal("application/pdf"), - "data": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("text"), - "media_type": Schema.Literal("text/plain"), - "data": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("content"), - "content": Schema.Union([ - Schema.String, - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }))), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("image"), - "source": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("base64"), - "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), - "data": Schema.String - }), - Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) - ], { mode: "oneOf" }), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }) - ], { mode: "oneOf" })) - ]) - }), - Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) - ], { mode: "oneOf" }), - "citations": Schema.optionalKey(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) })), - "context": Schema.optionalKey(Schema.String), - "title": Schema.optionalKey(Schema.String), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("tool_use"), - "id": Schema.String, - "name": Schema.String, - "input": Schema.optionalKey(Schema.Unknown), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("tool_result"), - "tool_use_id": Schema.String, - "content": Schema.optionalKey(Schema.Union([ - Schema.String, - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }))), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("image"), - "source": Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("base64"), - "media_type": Schema.Literals(["image/jpeg", "image/png", "image/gif", "image/webp"]), - "data": Schema.String - }), - Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }) - ], { mode: "oneOf" }), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }) - ])) - ])), - "is_error": Schema.optionalKey(Schema.Boolean), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ "type": Schema.Literal("thinking"), "thinking": Schema.String, "signature": Schema.String }), - Schema.Struct({ "type": Schema.Literal("redacted_thinking"), "data": Schema.String }), - Schema.Struct({ - "type": Schema.Literal("server_tool_use"), - "id": Schema.String, - "name": Schema.Literal("web_search"), - "input": Schema.optionalKey(Schema.Unknown), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result"), - "tool_use_id": Schema.String, - "content": Schema.Union([ - Schema.Array( - Schema.Struct({ - "type": Schema.Literal("web_search_result"), - "encrypted_content": Schema.String, - "title": Schema.String, - "url": Schema.String, - "page_age": Schema.optionalKey(Schema.String) - }) - ), - Schema.Struct({ - "type": Schema.Literal("web_search_tool_result_error"), - "error_code": Schema.Literals([ - "invalid_tool_input", - "unavailable", - "max_uses_exceeded", - "too_many_requests", - "query_too_long" - ]) - }) - ]), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("search_result"), - "source": Schema.String, - "title": Schema.String, - "content": Schema.Array(Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }))), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - })), - "citations": Schema.optionalKey(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) })), - "cache_control": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) - }) - ) - }) - ], { mode: "oneOf" })) - ]) -}).annotate({ "description": "Anthropic message with OpenRouter extensions" }) -export type AnthropicOutputConfig = { readonly "effort"?: "low" | "medium" | "high" | "max" } -export const AnthropicOutputConfig = Schema.Struct({ - "effort": Schema.optionalKey( - Schema.Literals(["low", "medium", "high", "max"]).annotate({ - "description": - "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer. Valid values are `low`, `medium`, `high`, or `max`." - }) - ) -}).annotate({ + ), + "total": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(20000).annotate({ "expected": "a value greater than or equal to 20000" }) + ), + "type": Schema.Literal("tokens") + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": - "Configuration for controlling output behavior. Currently supports the effort parameter for Claude Opus 4.5." + "Task budget for an agentic turn. The model sees a countdown of remaining tokens and uses it to prioritize work and wind down gracefully. Advisory — does not enforce a hard cap." }) -export type ActivityItem = { - readonly "date": string - readonly "model": string - readonly "model_permaslug": string - readonly "endpoint_id": string - readonly "provider_name": string - readonly "usage": number - readonly "byok_usage_inference": number - readonly "requests": number - readonly "prompt_tokens": number - readonly "completion_tokens": number - readonly "reasoning_tokens": number -} -export const ActivityItem = Schema.Struct({ - "date": Schema.String.annotate({ "description": "Date of the activity (YYYY-MM-DD format)" }), - "model": Schema.String.annotate({ "description": "Model slug (e.g., \"openai/gpt-4.1\")" }), - "model_permaslug": Schema.String.annotate({ "description": "Model permaslug (e.g., \"openai/gpt-4.1-2025-04-14\")" }), - "endpoint_id": Schema.String.annotate({ "description": "Unique identifier for the endpoint" }), - "provider_name": Schema.String.annotate({ "description": "Name of the provider serving this endpoint" }), - "usage": Schema.Number.annotate({ "description": "Total cost in USD (OpenRouter credits spent)" }).check( - Schema.isFinite() - ), - "byok_usage_inference": Schema.Number.annotate({ - "description": "BYOK inference cost in USD (external credits spent)" - }).check(Schema.isFinite()), - "requests": Schema.Number.annotate({ "description": "Number of requests made" }).check(Schema.isFinite()), - "prompt_tokens": Schema.Number.annotate({ "description": "Total prompt tokens used" }).check(Schema.isFinite()), - "completion_tokens": Schema.Number.annotate({ "description": "Total completion tokens generated" }).check( - Schema.isFinite() - ), - "reasoning_tokens": Schema.Number.annotate({ "description": "Total reasoning tokens used" }).check(Schema.isFinite()) +export type MessagesPingEvent = { readonly "type": "ping" } +export const MessagesPingEvent = Schema.Struct({ "type": Schema.Literal("ping") }).annotate({ + "description": "Keep-alive ping event", + "identifier": "MessagesPingEvent" +}) +export type MetadataLevel = "disabled" | "enabled" +export const MetadataLevel = Schema.Literals(["disabled", "enabled"]).annotate({ + "description": "Opt-in level for surfacing routing metadata on the response under `openrouter_metadata`.", + "identifier": "MetadataLevel" }) -export type ForbiddenResponseErrorData = { - readonly "code": number - readonly "message": string - readonly "metadata"?: {} -} -export const ForbiddenResponseErrorData = Schema.Struct({ - "code": Schema.Number.check(Schema.isInt()), - "message": Schema.String, - "metadata": Schema.optionalKey(Schema.Struct({})) -}).annotate({ "description": "Error data for ForbiddenResponse" }) -export type CreateChargeRequest = { - readonly "amount": number - readonly "sender": string - readonly "chain_id": 1 | 137 | 8453 -} -export const CreateChargeRequest = Schema.Struct({ - "amount": Schema.Number.check(Schema.isFinite()), - "sender": Schema.String, - "chain_id": Schema.Literals([1, 137, 8453]) -}).annotate({ "description": "Create a Coinbase charge for crypto payment" }) -export type PublicPricing = { - readonly "prompt": string - readonly "completion": string - readonly "request"?: string - readonly "image"?: string - readonly "image_token"?: string - readonly "image_output"?: string - readonly "audio"?: string - readonly "audio_output"?: string - readonly "input_audio_cache"?: string - readonly "web_search"?: string - readonly "internal_reasoning"?: string - readonly "input_cache_read"?: string - readonly "input_cache_write"?: string - readonly "discount"?: number -} -export const PublicPricing = Schema.Struct({ - "prompt": Schema.String.annotate({ "description": "A number or string value representing a large number" }), - "completion": Schema.String.annotate({ "description": "A number or string value representing a large number" }), - "request": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image_token": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image_output": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "audio": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "audio_output": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_audio_cache": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "web_search": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "internal_reasoning": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_cache_read": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_cache_write": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "discount": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) -}).annotate({ "description": "Pricing information for the model" }) export type ModelGroup = | "Router" | "Media" @@ -2396,6 +3385,7 @@ export type ModelGroup = | "GPT" | "Claude" | "Gemini" + | "Gemma" | "Grok" | "Cohere" | "Nova" @@ -2416,6 +3406,7 @@ export const ModelGroup = Schema.Literals([ "GPT", "Claude", "Gemini", + "Gemma", "Grok", "Cohere", "Nova", @@ -2429,36 +3420,445 @@ export const ModelGroup = Schema.Literals([ "PaLM", "RWKV", "Qwen3" -]).annotate({ "description": "Tokenizer type used by the model" }) -export type InputModality = "text" | "image" | "file" | "audio" | "video" -export const InputModality = Schema.Literals(["text", "image", "file", "audio", "video"]) -export type OutputModality = "text" | "image" | "embeddings" | "audio" -export const OutputModality = Schema.Literals(["text", "image", "embeddings", "audio"]) -export type TopProviderInfo = { - readonly "context_length"?: number - readonly "max_completion_tokens"?: number - readonly "is_moderated": boolean +]).annotate({ "description": "Tokenizer type used by the model", "identifier": "ModelGroup" }) +export type ModelLinks = { readonly "details": string } +export const ModelLinks = Schema.Struct({ + "details": Schema.String.annotate({ "description": "URL for the model details/endpoints API" }) +}).annotate({ "description": "Related API endpoints and resources for this model.", "identifier": "ModelLinks" }) +export type ModelName = string +export const ModelName = Schema.String.annotate({ + "description": "Model to use for completion", + "identifier": "ModelName" +}) +export type ModelsCountResponse = { readonly "data": { readonly "count": number } } +export const ModelsCountResponse = Schema.Struct({ + "data": Schema.Struct({ + "count": Schema.Number.annotate({ "description": "Total number of available models" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + }).annotate({ "description": "Model count data" }) +}).annotate({ "description": "Model count data", "identifier": "ModelsCountResponse" }) +export type ModerationPlugin = { readonly "id": "moderation" } +export const ModerationPlugin = Schema.Struct({ "id": Schema.Literal("moderation") }).annotate({ + "identifier": "ModerationPlugin" +}) +export type MultimodalMedia = { readonly "data": string; readonly "format"?: string } +export const MultimodalMedia = Schema.Struct({ "data": Schema.String, "format": Schema.optionalKey(Schema.String) }) + .annotate({ "identifier": "MultimodalMedia" }) +export type NamespaceFunctionTool = { + readonly "allowed_callers"?: ReadonlyArray<"direct" | "programmatic"> | null + readonly "defer_loading"?: boolean + readonly "description"?: string | null + readonly "name": string + readonly "output_schema"?: { readonly [x: string]: Schema.Json } | null + readonly "parameters"?: { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" } -export const TopProviderInfo = Schema.Struct({ - "context_length": Schema.optionalKey( - Schema.Number.annotate({ "description": "Context length from the top provider" }).check(Schema.isFinite()) +export const NamespaceFunctionTool = Schema.Struct({ + "allowed_callers": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Literals(["direct", "programmatic"])), Schema.Null]) ), - "max_completion_tokens": Schema.optionalKey( - Schema.Number.annotate({ "description": "Maximum completion tokens from the top provider" }).check( - Schema.isFinite() - ) + "defer_loading": Schema.optionalKey(Schema.Boolean), + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "output_schema": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "is_moderated": Schema.Boolean.annotate({ "description": "Whether the top provider moderates content" }) -}).annotate({ "description": "Information about the top provider for this model" }) -export type PerRequestLimits = { readonly "prompt_tokens": number; readonly "completion_tokens": number } -export const PerRequestLimits = Schema.Struct({ - "prompt_tokens": Schema.Number.annotate({ "description": "Maximum prompt tokens per request" }).check( - Schema.isFinite() + "parameters": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "completion_tokens": Schema.Number.annotate({ "description": "Maximum completion tokens per request" }).check( - Schema.isFinite() + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") +}).annotate({ "description": "A function tool grouped inside a namespace tool", "identifier": "NamespaceFunctionTool" }) +export type NotFoundResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const NotFoundResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ) -}).annotate({ "description": "Per-request token limits" }) +}).annotate({ "description": "Error data for NotFoundResponse", "identifier": "NotFoundResponseErrorData" }) +export type ObservabilityFilterRuleGroup = { + readonly "logic"?: "and" | "or" + readonly "rules": ReadonlyArray< + { + readonly "field": + | "model" + | "provider" + | "session_id" + | "user_id" + | "api_key_name" + | "finish_reason" + | "input" + | "output" + | "total_cost" + | "total_tokens" + | "prompt_tokens" + | "completion_tokens" + readonly "operator": + | "equals" + | "not_equals" + | "contains" + | "not_contains" + | "regex" + | "starts_with" + | "ends_with" + | "gt" + | "lt" + | "gte" + | "lte" + | "exists" + | "not_exists" + readonly "value"?: string | number + } + > +} +export const ObservabilityFilterRuleGroup = Schema.Struct({ + "logic": Schema.optionalKey(Schema.Literals(["and", "or"])), + "rules": Schema.Array(Schema.Struct({ + "field": Schema.Literals([ + "model", + "provider", + "session_id", + "user_id", + "api_key_name", + "finish_reason", + "input", + "output", + "total_cost", + "total_tokens", + "prompt_tokens", + "completion_tokens" + ]), + "operator": Schema.Literals([ + "equals", + "not_equals", + "contains", + "not_contains", + "regex", + "starts_with", + "ends_with", + "gt", + "lt", + "gte", + "lte", + "exists", + "not_exists" + ]), + "value": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))]) + ) + })) +}).annotate({ "identifier": "ObservabilityFilterRuleGroup" }) +export type OpenAIResponseCustomToolCall = { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": "custom_tool_call" +} +export const OpenAIResponseCustomToolCall = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Literal("custom_tool_call") +}).annotate({ "identifier": "OpenAIResponseCustomToolCall" }) +export type OpenAIResponsesImageGenCallCompleted = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.completed" +} +export const OpenAIResponsesImageGenCallCompleted = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.completed") +}).annotate({ "identifier": "OpenAIResponsesImageGenCallCompleted" }) +export type OpenAIResponsesImageGenCallGenerating = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.generating" +} +export const OpenAIResponsesImageGenCallGenerating = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.generating") +}).annotate({ "identifier": "OpenAIResponsesImageGenCallGenerating" }) +export type OpenAIResponsesImageGenCallInProgress = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.in_progress" +} +export const OpenAIResponsesImageGenCallInProgress = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.in_progress") +}).annotate({ "identifier": "OpenAIResponsesImageGenCallInProgress" }) +export type OpenAIResponsesImageGenCallPartialImage = { + readonly "item_id": string + readonly "output_index": number + readonly "partial_image_b64": string + readonly "partial_image_index": number + readonly "sequence_number": number + readonly "type": "response.image_generation_call.partial_image" +} +export const OpenAIResponsesImageGenCallPartialImage = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "partial_image_b64": Schema.String, + "partial_image_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.image_generation_call.partial_image") +}).annotate({ "identifier": "OpenAIResponsesImageGenCallPartialImage" }) +export type OpenAIResponsesRefusalContent = { readonly "refusal": string; readonly "type": "refusal" } +export const OpenAIResponsesRefusalContent = Schema.Struct({ + "refusal": Schema.String, + "type": Schema.Literal("refusal") +}).annotate({ "identifier": "OpenAIResponsesRefusalContent" }) +export type OpenAIResponsesResponseStatus = + | "completed" + | "incomplete" + | "in_progress" + | "failed" + | "cancelled" + | "queued" +export const OpenAIResponsesResponseStatus = Schema.Literals([ + "completed", + "incomplete", + "in_progress", + "failed", + "cancelled", + "queued" +]).annotate({ "identifier": "OpenAIResponsesResponseStatus" }) +export type OpenAIResponsesSearchCompleted = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.web_search_call.completed" +} +export const OpenAIResponsesSearchCompleted = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.completed") +}).annotate({ "identifier": "OpenAIResponsesSearchCompleted" }) +export type Objects_11 = { readonly "name": string; readonly "type": "function" } +export const Objects_11 = Schema.Struct({ "name": Schema.String, "type": Schema.Literal("function") }) +export type Objects_12 = { readonly "type": "web_search_preview_2025_03_11" | "web_search_preview" } +export const Objects_12 = Schema.Struct({ + "type": Schema.Literals(["web_search_preview_2025_03_11", "web_search_preview"]) +}) +export type Objects_13 = { readonly "type": "apply_patch" } +export const Objects_13 = Schema.Struct({ "type": Schema.Literal("apply_patch") }) +export type Objects_14 = { readonly "type": "shell" } +export const Objects_14 = Schema.Struct({ "type": Schema.Literal("shell") }) +export type OpenAIResponsesTruncation = "auto" | "disabled" | null +export const OpenAIResponsesTruncation = Schema.Union([Schema.Literal("auto"), Schema.Literal("disabled"), Schema.Null]) + .annotate({ "identifier": "OpenAIResponsesTruncation" }) +export type Objects_15 = { readonly "cache_write_tokens"?: number | null; readonly "cached_tokens": number } +export const Objects_15 = Schema.Struct({ + "cache_write_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "cached_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}) +export type Objects_16 = { readonly "reasoning_tokens": number } +export const Objects_16 = Schema.Struct({ + "reasoning_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}) +export type OpenAIResponsesWebSearchCallInProgress = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.web_search_call.in_progress" +} +export const OpenAIResponsesWebSearchCallInProgress = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.in_progress") +}).annotate({ "identifier": "OpenAIResponsesWebSearchCallInProgress" }) +export type OpenAIResponsesWebSearchCallSearching = { + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.web_search_call.searching" +} +export const OpenAIResponsesWebSearchCallSearching = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.searching") +}).annotate({ "identifier": "OpenAIResponsesWebSearchCallSearching" }) +export type OpenResponsesTopLogprobs = { + readonly "bytes"?: ReadonlyArray + readonly "logprob"?: number + readonly "token"?: string +} +export const OpenResponsesTopLogprobs = Schema.Struct({ + "bytes": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })))), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "token": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Alternative token with its log probability", "identifier": "OpenResponsesTopLogprobs" }) +export type ORAnthropicStopReason = + | "end_turn" + | "max_tokens" + | "stop_sequence" + | "tool_use" + | "pause_turn" + | "refusal" + | "compaction" + | null +export const ORAnthropicStopReason = Schema.Union([ + Schema.Literal("end_turn"), + Schema.Literal("max_tokens"), + Schema.Literal("stop_sequence"), + Schema.Literal("tool_use"), + Schema.Literal("pause_turn"), + Schema.Literal("refusal"), + Schema.Literal("compaction"), + Schema.Null +]).annotate({ "identifier": "ORAnthropicStopReason" }) +export type OutputComputerCallItem = { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id"?: string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "incomplete" | "in_progress" + readonly "type": "computer_call" +} +export const OutputComputerCallItem = Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Literals(["completed", "incomplete", "in_progress"]), + "type": Schema.Literal("computer_call") +}).annotate({ "identifier": "OutputComputerCallItem" }) +export type OutputCustomToolCallItem = { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": "custom_tool_call" +} +export const OutputCustomToolCallItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Literal("custom_tool_call") +}).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments.", + "identifier": "OutputCustomToolCallItem" +}) +export type OutputFunctionCallItem = { + readonly "arguments": string + readonly "call_id": string + readonly "id"?: string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "function_call" +} +export const OutputFunctionCallItem = Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Literal("function_call") +}).annotate({ "identifier": "OutputFunctionCallItem" }) +export type OutputItemCustomToolCall = { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": "custom_tool_call" +} +export const OutputItemCustomToolCall = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Literal("custom_tool_call") +}).annotate({ "identifier": "OutputItemCustomToolCall" }) +export type OutputItemFunctionCall = { + readonly "arguments": string + readonly "call_id": string + readonly "id"?: string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "function_call" +} +export const OutputItemFunctionCall = Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Literal("function_call") +}).annotate({ "identifier": "OutputItemFunctionCall" }) +export type OutputModality = "text" | "image" | "embeddings" | "audio" | "video" | "rerank" | "speech" | "transcription" +export const OutputModality = Schema.Literals([ + "text", + "image", + "embeddings", + "audio", + "video", + "rerank", + "speech", + "transcription" +]).annotate({ "identifier": "OutputModality" }) +export type OutputModalityEnum = "text" | "image" +export const OutputModalityEnum = Schema.Literals(["text", "image"]).annotate({ "identifier": "OutputModalityEnum" }) export type Parameter = | "temperature" | "top_p" @@ -2469,9 +3869,11 @@ export type Parameter = | "presence_penalty" | "repetition_penalty" | "max_tokens" + | "max_completion_tokens" | "logit_bias" | "logprobs" | "top_logprobs" + | "prediction" | "seed" | "response_format" | "structured_outputs" @@ -2494,9 +3896,11 @@ export const Parameter = Schema.Literals([ "presence_penalty", "repetition_penalty", "max_tokens", + "max_completion_tokens", "logit_bias", "logprobs", "top_logprobs", + "prediction", "seed", "response_format", "structured_outputs", @@ -2509,53 +3913,350 @@ export const Parameter = Schema.Literals([ "reasoning_effort", "web_search_options", "verbosity" -]) -export type DefaultParameters = { - readonly "temperature"?: number - readonly "top_p"?: number - readonly "frequency_penalty"?: number +]).annotate({ "identifier": "Parameter" }) +export type ParetoRouterPlugin = { + readonly "enabled"?: boolean + readonly "id": "pareto-router" + readonly "max_price"?: number + readonly "min_coding_score"?: number + readonly "price_source"?: "prompt" | "weighted_avg" } -export const DefaultParameters = Schema.Struct({ - "temperature": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)) +export const ParetoRouterPlugin = Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the pareto-router plugin for this request. Defaults to true." + }) ), - "top_p": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(1)) + "id": Schema.Literal("pareto-router"), + "max_price": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum input price in USD per million tokens. When set, quality-tier selection (min_coding_score) is bypassed: the router computes the Pareto frontier over the top coding models and routes to the best-scoring frontier model priced at or below this cap, falling back through cheaper frontier models, then non-frontier models. Enforced against the price source given by price_source. Returns 404 when no candidate satisfies the cap.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ) ), - "frequency_penalty": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) + "min_coding_score": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Minimum coding quality score between 0 and 1. Maps to internal quality tiers: >= 0.66 → high (top coding models), >= 0.33 → medium (strong modern flagships), < 0.33 → low (capable coders above the median). Omit to default to the highest tier (equivalent to >= 0.66). Not used when max_price is set (price-based selection takes over).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(1).annotate({ "expected": "a value less than or equal to 1" })) + ), + "price_source": Schema.optionalKey( + Schema.Literals(["prompt", "weighted_avg"]).annotate({ + "description": + "Price source for the Pareto frontier cost axis and for enforcing max_price. \"prompt\" uses catalog list price (endpoint.pricing.prompt). \"weighted_avg\" uses traffic-weighted effective input price from ClickHouse, falling back to prompt price for models without traffic data. Defaults to \"prompt\"." + }) ) -}).annotate({ "description": "Default parameters for this model" }) -export type ModelsCountResponse = { readonly "data": { readonly "count": number } } -export const ModelsCountResponse = Schema.Struct({ - "data": Schema.Struct({ - "count": Schema.Number.annotate({ "description": "Total number of available models" }).check(Schema.isFinite()) - }).annotate({ "description": "Model count data" }) -}).annotate({ "description": "Model count data" }) -export type EndpointStatus = 0 | -1 | -2 | -3 | -5 | -10 -export const EndpointStatus = Schema.Literals([0, -1, -2, -3, -5, -10]) -export type PercentileStats = { - readonly "p50": number - readonly "p75": number - readonly "p90": number - readonly "p99": number +}).annotate({ "identifier": "ParetoRouterPlugin" }) +export type PayloadTooLargeResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const PayloadTooLargeResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for PayloadTooLargeResponse", + "identifier": "PayloadTooLargeResponseErrorData" +}) +export type PaymentRequiredResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null } -export const PercentileStats = Schema.Struct({ - "p50": Schema.Number.annotate({ "description": "Median (50th percentile)" }).check(Schema.isFinite()), - "p75": Schema.Number.annotate({ "description": "75th percentile" }).check(Schema.isFinite()), - "p90": Schema.Number.annotate({ "description": "90th percentile" }).check(Schema.isFinite()), - "p99": Schema.Number.annotate({ "description": "99th percentile" }).check(Schema.isFinite()) +export const PaymentRequiredResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) }).annotate({ + "description": "Error data for PaymentRequiredResponse", + "identifier": "PaymentRequiredResponseErrorData" +}) +export type PDFParserEngine = "mistral-ocr" | "native" | "cloudflare-ai" | "pdf-text" +export const PDFParserEngine = Schema.Union([ + Schema.Literals(["mistral-ocr", "native", "cloudflare-ai"]), + Schema.Literal("pdf-text") +]).annotate({ "description": - "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." + "The engine to use for parsing PDF files. \"pdf-text\" is deprecated and automatically redirected to \"cloudflare-ai\".", + "identifier": "PDFParserEngine" }) -export type __schema5 = ReadonlyArray< - | "AI21" - | "AionLabs" - | "Alibaba" - | "Ambient" - | "Amazon Bedrock" - | "Amazon Nova" +export type PercentileLatencyCutoffs = { + readonly "p50"?: number | null + readonly "p75"?: number | null + readonly "p90"?: number | null + readonly "p99"?: number | null +} +export const PercentileLatencyCutoffs = Schema.Struct({ + "p50": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Maximum p50 latency (seconds)", "format": "double" }) + ), + "p75": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Maximum p75 latency (seconds)", "format": "double" }) + ), + "p90": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Maximum p90 latency (seconds)", "format": "double" }) + ), + "p99": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Maximum p99 latency (seconds)", "format": "double" }) + ) +}).annotate({ + "description": "Percentile-based latency cutoffs. All specified cutoffs must be met for an endpoint to be preferred.", + "identifier": "PercentileLatencyCutoffs" +}) +export type PercentileStats = { + readonly "p50": number + readonly "p75": number + readonly "p90": number + readonly "p99": number + readonly [x: string]: Schema.Json +} | null +export const PercentileStats = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "p50": Schema.Number.annotate({ "description": "Median (50th percentile)", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "p75": Schema.Number.annotate({ "description": "75th percentile", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "p90": Schema.Number.annotate({ "description": "90th percentile", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "p99": Schema.Number.annotate({ "description": "99th percentile", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ + "description": + "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.", + "identifier": "PercentileStats" +}) +export type PercentileThroughputCutoffs = { + readonly "p50"?: number | null + readonly "p75"?: number | null + readonly "p90"?: number | null + readonly "p99"?: number | null +} +export const PercentileThroughputCutoffs = Schema.Struct({ + "p50": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Minimum p50 throughput (tokens/sec)", "format": "double" }) + ), + "p75": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Minimum p75 throughput (tokens/sec)", "format": "double" }) + ), + "p90": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Minimum p90 throughput (tokens/sec)", "format": "double" }) + ), + "p99": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Minimum p99 throughput (tokens/sec)", "format": "double" }) + ) +}).annotate({ + "description": + "Percentile-based throughput cutoffs. All specified cutoffs must be met for an endpoint to be preferred.", + "identifier": "PercentileThroughputCutoffs" +}) +export type PerRequestLimits = { + readonly "completion_tokens": number + readonly "prompt_tokens": number + readonly [x: string]: Schema.Json +} | null +export const PerRequestLimits = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ "description": "Maximum completion tokens per request" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "prompt_tokens": Schema.Number.annotate({ "description": "Maximum prompt tokens per request" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Per-request token limits", "identifier": "PerRequestLimits" }) +export type PipelineStageType = "guardrail" | "plugin" | "server_tools" | "response_healing" | "context_compression" +export const PipelineStageType = Schema.Literals([ + "guardrail", + "plugin", + "server_tools", + "response_healing", + "context_compression" +]).annotate({ + "description": + "Categorical kind of a pipeline stage. Multiple plugins can share a type (e.g. all guardrail-level plugins emit `guardrail`); the `name` field disambiguates which plugin emitted it.", + "identifier": "PipelineStageType" +}) +export type PredictionContentText = { readonly "text": string; readonly "type": "text" } +export const PredictionContentText = Schema.Struct({ "text": Schema.String, "type": Schema.Literal("text") }).annotate({ + "description": "Text content part for a predicted output.", + "identifier": "PredictionContentText" +}) +export type PresetDesignatedVersion = { + readonly "config": {} + readonly "created_at": string + readonly "creator_id": string + readonly "id": string + readonly "preset_id": string + readonly "system_prompt": string | null + readonly "updated_at": string + readonly "version": number + readonly [x: string]: Schema.Json +} | null +export const PresetDesignatedVersion = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "config": Schema.Struct({}), + "created_at": Schema.String, + "creator_id": Schema.String, + "id": Schema.String, + "preset_id": Schema.String, + "system_prompt": Schema.Union([Schema.String, Schema.Null]), + "updated_at": Schema.String, + "version": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ + "description": "A specific version of a preset, containing config and optional system prompt.", + "identifier": "PresetDesignatedVersion" +}) +export type PresetStatus = "active" | "disabled" | "archived" +export const PresetStatus = Schema.Literals(["active", "disabled", "archived"]).annotate({ + "description": "The status of a preset.", + "identifier": "PresetStatus" +}) +export type Objects_18 = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type": "approximate" + readonly [x: string]: Schema.Json +} +export const Objects_18 = Schema.StructWithRest( + Schema.Struct({ + "city": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "country": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "region": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "timezone": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("approximate") + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type PricingOverride = { + readonly "audio"?: string + readonly "completion"?: string + readonly "input_audio_cache"?: string + readonly "input_cache_read"?: string + readonly "input_cache_write"?: string + readonly "input_cache_write_1h"?: string + readonly "min_prompt_tokens"?: number + readonly "prompt"?: string + readonly "utc_end"?: number + readonly "utc_start"?: number +} +export const PricingOverride = Schema.Struct({ + "audio": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per audio input token" }) + ), + "completion": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per token for completion (output) generation" }) + ), + "input_audio_cache": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per cached audio input token" }) + ), + "input_cache_read": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per cached input token (read)" }) + ), + "input_cache_write": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per cache-write token" }) + ), + "input_cache_write_1h": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per 1-hour cache-write token" }) + ), + "min_prompt_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Condition: the entry applies when the total prompt tokens of a request are strictly greater than this threshold", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "Overridden price in USD per token for prompt (input) processing" }) + ), + "utc_end": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Condition: exclusive end of a daily UTC time window as an HHMM clock number (e.g. 400 = 04:00)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "utc_start": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Condition: inclusive start of a daily UTC time window as an HHMM clock number (e.g. 100 = 01:00, 1030 = 10:30). The entry applies while the current UTC time is inside the half-open window [utc_start, utc_end), which may wrap past midnight (utc_start > utc_end).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ) +}).annotate({ + "description": + "A conditional override of the base pricing. An entry applies only when all of its condition fields (e.g. min_prompt_tokens, or the utc_start/utc_end time window) match the request; among applicable entries, later entries win per price key; price keys absent from an entry inherit the base price.", + "identifier": "PricingOverride" +}) +export type Objects_19 = { readonly "mode": "explicit"; readonly [x: string]: Schema.Json } +export const Objects_19 = Schema.StructWithRest(Schema.Struct({ "mode": Schema.Literal("explicit") }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) +]) +export type Objects_20 = { + readonly "mode": "explicit" + readonly "ttl"?: string | null + readonly [x: string]: Schema.Json +} +export const Objects_20 = Schema.StructWithRest( + Schema.Struct({ + "mode": Schema.Literal("explicit"), + "ttl": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type PromptInjectionScanScope = "user_only" | "all_messages" +export const PromptInjectionScanScope = Schema.Literals(["user_only", "all_messages"]).annotate({ + "description": + "Which message roles to scan for prompt injection. Only applies to the regex-prompt-injection builtin. Defaults to all_messages.", + "identifier": "PromptInjectionScanScope" +}) +export type ProviderName = + | "AkashML" + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Baidu" + | "Amazon Bedrock" + | "Amazon Nova" | "Anthropic" | "Arcee AI" | "AtlasCloud" @@ -2570,26 +4271,37 @@ export type __schema5 = ReadonlyArray< | "Clarifai" | "Cloudflare" | "Cohere" + | "CoreWeave" + | "Crucible" | "Crusoe" + | "Darkbloom" + | "Decart" + | "Deepgram" | "DeepInfra" | "DeepSeek" + | "DekaLLM" + | "DigitalOcean" | "Featherless" | "Fireworks" + | "Fish Audio" | "Friendli" | "GMICloud" | "Google" | "Google AI Studio" | "Groq" - | "Hyperbolic" + | "HeyGen" | "Inception" | "Inceptron" | "InferenceNet" + | "Ionstream" | "Infermatic" | "Io Net" + | "Inferact vLLM" | "Inflection" | "Liquid" | "Mara" | "Mancer 2" + | "Meta" | "Minimax" | "ModelRun" | "Mistral" @@ -2598,15 +4310,22 @@ export type __schema5 = ReadonlyArray< | "Morph" | "NCompass" | "Nebius" + | "Nex AGI" | "NextBit" | "Novita" | "Nvidia" | "OpenAI" | "OpenInference" | "Parasail" + | "Poolside" + | "Perceptron" | "Perplexity" | "Phala" + | "Recraft" + | "Reka" | "Relace" + | "Sail Research" + | "Sakana AI" | "SambaNova" | "Seed" | "SiliconFlow" @@ -2615,23 +4334,575 @@ export type __schema5 = ReadonlyArray< | "Stealth" | "StreamLake" | "Switchpoint" + | "Tencent" + | "Tenstorrent" | "Together" | "Upstage" | "Venice" + | "Wafer" | "WandB" + | "Quiver" + | "Krea" + | "Runway" | "Xiaomi" | "xAI" | "Z.AI" | "FakeProvider" - | string -> -export const __schema5 = Schema.Array( - Schema.Union([ +export const ProviderName = Schema.Literals([ + "AkashML", + "AI21", + "AionLabs", + "Alibaba", + "Ambient", + "Baidu", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "CoreWeave", + "Crucible", + "Crusoe", + "Darkbloom", + "Decart", + "Deepgram", + "DeepInfra", + "DeepSeek", + "DekaLLM", + "DigitalOcean", + "Featherless", + "Fireworks", + "Fish Audio", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "HeyGen", + "Inception", + "Inceptron", + "InferenceNet", + "Ionstream", + "Infermatic", + "Io Net", + "Inferact vLLM", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Meta", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "Nex AGI", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Poolside", + "Perceptron", + "Perplexity", + "Phala", + "Recraft", + "Reka", + "Relace", + "Sail Research", + "Sakana AI", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "StepFun", + "Stealth", + "StreamLake", + "Switchpoint", + "Tencent", + "Tenstorrent", + "Together", + "Upstage", + "Venice", + "Wafer", + "WandB", + "Quiver", + "Krea", + "Runway", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" +]).annotate({ "identifier": "ProviderName" }) +export type Objects_21 = {} +export const Objects_21 = Schema.Struct({}) +export type Objects_22 = {} +export const Objects_22 = Schema.Struct({}) +export type Objects_23 = {} +export const Objects_23 = Schema.Struct({}) +export type Objects_24 = {} +export const Objects_24 = Schema.Struct({}) +export type Objects_25 = {} +export const Objects_25 = Schema.Struct({}) +export type Objects_26 = {} +export const Objects_26 = Schema.Struct({}) +export type Objects_27 = {} +export const Objects_27 = Schema.Struct({}) +export type Objects_28 = {} +export const Objects_28 = Schema.Struct({}) +export type Objects_29 = {} +export const Objects_29 = Schema.Struct({}) +export type Objects_30 = {} +export const Objects_30 = Schema.Struct({}) +export type Objects_31 = {} +export const Objects_31 = Schema.Struct({}) +export type Objects_32 = {} +export const Objects_32 = Schema.Struct({}) +export type Objects_33 = {} +export const Objects_33 = Schema.Struct({}) +export type Objects_34 = {} +export const Objects_34 = Schema.Struct({}) +export type Objects_35 = {} +export const Objects_35 = Schema.Struct({}) +export type Objects_36 = {} +export const Objects_36 = Schema.Struct({}) +export type Objects_37 = {} +export const Objects_37 = Schema.Struct({}) +export type Objects_38 = {} +export const Objects_38 = Schema.Struct({}) +export type Objects_39 = {} +export const Objects_39 = Schema.Struct({}) +export type Objects_40 = {} +export const Objects_40 = Schema.Struct({}) +export type Objects_41 = {} +export const Objects_41 = Schema.Struct({}) +export type Objects_42 = {} +export const Objects_42 = Schema.Struct({}) +export type Objects_43 = {} +export const Objects_43 = Schema.Struct({}) +export type Objects_44 = {} +export const Objects_44 = Schema.Struct({}) +export type Objects_45 = {} +export const Objects_45 = Schema.Struct({}) +export type Objects_46 = {} +export const Objects_46 = Schema.Struct({}) +export type Objects_47 = {} +export const Objects_47 = Schema.Struct({}) +export type Objects_48 = {} +export const Objects_48 = Schema.Struct({}) +export type Objects_49 = {} +export const Objects_49 = Schema.Struct({}) +export type Objects_50 = {} +export const Objects_50 = Schema.Struct({}) +export type Objects_51 = {} +export const Objects_51 = Schema.Struct({}) +export type Objects_52 = {} +export const Objects_52 = Schema.Struct({}) +export type Objects_53 = {} +export const Objects_53 = Schema.Struct({}) +export type Objects_54 = {} +export const Objects_54 = Schema.Struct({}) +export type Objects_55 = {} +export const Objects_55 = Schema.Struct({}) +export type Objects_56 = {} +export const Objects_56 = Schema.Struct({}) +export type Objects_57 = {} +export const Objects_57 = Schema.Struct({}) +export type Objects_58 = {} +export const Objects_58 = Schema.Struct({}) +export type Objects_59 = {} +export const Objects_59 = Schema.Struct({}) +export type Objects_60 = {} +export const Objects_60 = Schema.Struct({}) +export type Objects_61 = {} +export const Objects_61 = Schema.Struct({}) +export type Objects_62 = {} +export const Objects_62 = Schema.Struct({}) +export type Objects_63 = {} +export const Objects_63 = Schema.Struct({}) +export type Objects_64 = {} +export const Objects_64 = Schema.Struct({}) +export type Objects_65 = {} +export const Objects_65 = Schema.Struct({}) +export type Objects_66 = {} +export const Objects_66 = Schema.Struct({}) +export type Objects_67 = {} +export const Objects_67 = Schema.Struct({}) +export type Objects_68 = {} +export const Objects_68 = Schema.Struct({}) +export type Objects_69 = {} +export const Objects_69 = Schema.Struct({}) +export type Objects_70 = {} +export const Objects_70 = Schema.Struct({}) +export type Objects_71 = {} +export const Objects_71 = Schema.Struct({}) +export type Objects_72 = {} +export const Objects_72 = Schema.Struct({}) +export type Objects_73 = {} +export const Objects_73 = Schema.Struct({}) +export type Objects_74 = {} +export const Objects_74 = Schema.Struct({}) +export type Objects_75 = {} +export const Objects_75 = Schema.Struct({}) +export type Objects_76 = {} +export const Objects_76 = Schema.Struct({}) +export type Objects_77 = {} +export const Objects_77 = Schema.Struct({}) +export type Objects_78 = {} +export const Objects_78 = Schema.Struct({}) +export type Objects_79 = {} +export const Objects_79 = Schema.Struct({}) +export type Objects_80 = {} +export const Objects_80 = Schema.Struct({}) +export type Objects_81 = {} +export const Objects_81 = Schema.Struct({}) +export type Objects_82 = {} +export const Objects_82 = Schema.Struct({}) +export type Objects_83 = {} +export const Objects_83 = Schema.Struct({}) +export type Objects_84 = {} +export const Objects_84 = Schema.Struct({}) +export type Objects_85 = {} +export const Objects_85 = Schema.Struct({}) +export type Objects_86 = {} +export const Objects_86 = Schema.Struct({}) +export type Objects_87 = {} +export const Objects_87 = Schema.Struct({}) +export type Objects_88 = {} +export const Objects_88 = Schema.Struct({}) +export type Objects_89 = {} +export const Objects_89 = Schema.Struct({}) +export type Objects_90 = {} +export const Objects_90 = Schema.Struct({}) +export type Objects_91 = {} +export const Objects_91 = Schema.Struct({}) +export type Objects_92 = {} +export const Objects_92 = Schema.Struct({}) +export type Objects_93 = {} +export const Objects_93 = Schema.Struct({}) +export type Objects_94 = {} +export const Objects_94 = Schema.Struct({}) +export type Objects_95 = {} +export const Objects_95 = Schema.Struct({}) +export type Objects_96 = {} +export const Objects_96 = Schema.Struct({}) +export type Objects_97 = {} +export const Objects_97 = Schema.Struct({}) +export type Objects_98 = {} +export const Objects_98 = Schema.Struct({}) +export type Objects_99 = {} +export const Objects_99 = Schema.Struct({}) +export type Objects_100 = {} +export const Objects_100 = Schema.Struct({}) +export type Objects_101 = {} +export const Objects_101 = Schema.Struct({}) +export type Objects_102 = {} +export const Objects_102 = Schema.Struct({}) +export type Objects_103 = {} +export const Objects_103 = Schema.Struct({}) +export type Objects_104 = {} +export const Objects_104 = Schema.Struct({}) +export type Objects_105 = {} +export const Objects_105 = Schema.Struct({}) +export type Objects_106 = {} +export const Objects_106 = Schema.Struct({}) +export type Objects_107 = {} +export const Objects_107 = Schema.Struct({}) +export type Objects_108 = {} +export const Objects_108 = Schema.Struct({}) +export type Objects_109 = {} +export const Objects_109 = Schema.Struct({}) +export type Objects_110 = {} +export const Objects_110 = Schema.Struct({}) +export type Objects_111 = {} +export const Objects_111 = Schema.Struct({}) +export type Objects_112 = {} +export const Objects_112 = Schema.Struct({}) +export type Objects_113 = {} +export const Objects_113 = Schema.Struct({}) +export type Objects_114 = {} +export const Objects_114 = Schema.Struct({}) +export type Objects_115 = {} +export const Objects_115 = Schema.Struct({}) +export type Objects_116 = {} +export const Objects_116 = Schema.Struct({}) +export type Objects_117 = {} +export const Objects_117 = Schema.Struct({}) +export type Objects_118 = {} +export const Objects_118 = Schema.Struct({}) +export type Objects_119 = {} +export const Objects_119 = Schema.Struct({}) +export type Objects_120 = {} +export const Objects_120 = Schema.Struct({}) +export type Objects_121 = {} +export const Objects_121 = Schema.Struct({}) +export type Objects_122 = {} +export const Objects_122 = Schema.Struct({}) +export type Objects_123 = {} +export const Objects_123 = Schema.Struct({}) +export type Objects_124 = {} +export const Objects_124 = Schema.Struct({}) +export type Objects_125 = {} +export const Objects_125 = Schema.Struct({}) +export type Objects_126 = {} +export const Objects_126 = Schema.Struct({}) +export type Objects_127 = {} +export const Objects_127 = Schema.Struct({}) +export type Objects_128 = {} +export const Objects_128 = Schema.Struct({}) +export type Objects_129 = {} +export const Objects_129 = Schema.Struct({}) +export type Objects_130 = {} +export const Objects_130 = Schema.Struct({}) +export type Objects_131 = {} +export const Objects_131 = Schema.Struct({}) +export type Objects_132 = {} +export const Objects_132 = Schema.Struct({}) +export type Objects_133 = {} +export const Objects_133 = Schema.Struct({}) +export type Objects_134 = {} +export const Objects_134 = Schema.Struct({}) +export type Objects_135 = {} +export const Objects_135 = Schema.Struct({}) +export type Objects_136 = {} +export const Objects_136 = Schema.Struct({}) +export type Objects_137 = {} +export const Objects_137 = Schema.Struct({}) +export type Objects_138 = {} +export const Objects_138 = Schema.Struct({}) +export type Objects_139 = {} +export const Objects_139 = Schema.Struct({}) +export type Objects_140 = {} +export const Objects_140 = Schema.Struct({}) +export type Objects_141 = {} +export const Objects_141 = Schema.Struct({}) +export type Objects_142 = {} +export const Objects_142 = Schema.Struct({}) +export type Objects_143 = {} +export const Objects_143 = Schema.Struct({}) +export type Objects_144 = {} +export const Objects_144 = Schema.Struct({}) +export type Objects_145 = {} +export const Objects_145 = Schema.Struct({}) +export type Objects_146 = {} +export const Objects_146 = Schema.Struct({}) +export type Objects_147 = {} +export const Objects_147 = Schema.Struct({}) +export type ProviderOverloadedResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const ProviderOverloadedResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for ProviderOverloadedResponse", + "identifier": "ProviderOverloadedResponseErrorData" +}) +export type ProviderResponse = { + readonly "endpoint_id"?: string + readonly "id"?: string + readonly "is_byok"?: boolean + readonly "latency"?: number + readonly "model_permaslug"?: string + readonly "provider_name"?: + | "AnyScale" + | "Atoma" + | "Cent-ML" + | "CrofAI" + | "Enfer" + | "GoPomelo" + | "HuggingFace" + | "Hyperbolic" + | "Hyperbolic 2" + | "InoCloud" + | "Kluster" + | "Lambda" + | "Lepton" + | "Lynn 2" + | "Lynn" + | "Mancer" + | "Modal" + | "Nineteen" + | "OctoAI" + | "Recursal" + | "Reflection" + | "Replicate" + | "SambaNova 2" + | "SF Compute" + | "Targon" + | "Together 2" + | "Ubicloud" + | "01.AI" + | "AkashML" + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Baidu" + | "Amazon Bedrock" + | "Amazon Nova" + | "Anthropic" + | "Arcee AI" + | "AtlasCloud" + | "Avian" + | "Azure" + | "BaseTen" + | "BytePlus" + | "Black Forest Labs" + | "Cerebras" + | "Chutes" + | "Cirrascale" + | "Clarifai" + | "Cloudflare" + | "Cohere" + | "CoreWeave" + | "Crucible" + | "Crusoe" + | "Darkbloom" + | "Decart" + | "Deepgram" + | "DeepInfra" + | "DeepSeek" + | "DekaLLM" + | "DigitalOcean" + | "Featherless" + | "Fireworks" + | "Fish Audio" + | "Friendli" + | "GMICloud" + | "Google" + | "Google AI Studio" + | "Groq" + | "HeyGen" + | "Inception" + | "Inceptron" + | "InferenceNet" + | "Ionstream" + | "Infermatic" + | "Io Net" + | "Inferact vLLM" + | "Inflection" + | "Liquid" + | "Mara" + | "Mancer 2" + | "Meta" + | "Minimax" + | "ModelRun" + | "Mistral" + | "Modular" + | "Moonshot AI" + | "Morph" + | "NCompass" + | "Nebius" + | "Nex AGI" + | "NextBit" + | "Novita" + | "Nvidia" + | "OpenAI" + | "OpenInference" + | "Parasail" + | "Poolside" + | "Perceptron" + | "Perplexity" + | "Phala" + | "Recraft" + | "Reka" + | "Relace" + | "Sail Research" + | "Sakana AI" + | "SambaNova" + | "Seed" + | "SiliconFlow" + | "Sourceful" + | "StepFun" + | "Stealth" + | "StreamLake" + | "Switchpoint" + | "Tencent" + | "Tenstorrent" + | "Together" + | "Upstage" + | "Venice" + | "Wafer" + | "WandB" + | "Quiver" + | "Krea" + | "Runway" + | "Xiaomi" + | "xAI" + | "Z.AI" + | "FakeProvider" + readonly "routed_service_tier"?: "flex" | "priority" + readonly "status": number | null +} +export const ProviderResponse = Schema.Struct({ + "endpoint_id": Schema.optionalKey(Schema.String.annotate({ "description": "Internal endpoint identifier" })), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "Upstream provider response identifier" })), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether the request used a bring-your-own-key" }) + ), + "latency": Schema.optionalKey( + Schema.Number.annotate({ "description": "Response latency in milliseconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "model_permaslug": Schema.optionalKey(Schema.String.annotate({ "description": "Canonical model slug" })), + "provider_name": Schema.optionalKey( Schema.Literals([ + "AnyScale", + "Atoma", + "Cent-ML", + "CrofAI", + "Enfer", + "GoPomelo", + "HuggingFace", + "Hyperbolic", + "Hyperbolic 2", + "InoCloud", + "Kluster", + "Lambda", + "Lepton", + "Lynn 2", + "Lynn", + "Mancer", + "Modal", + "Nineteen", + "OctoAI", + "Recursal", + "Reflection", + "Replicate", + "SambaNova 2", + "SF Compute", + "Targon", + "Together 2", + "Ubicloud", + "01.AI", + "AkashML", "AI21", "AionLabs", "Alibaba", "Ambient", + "Baidu", "Amazon Bedrock", "Amazon Nova", "Anthropic", @@ -2648,26 +4919,37 @@ export const __schema5 = Schema.Array( "Clarifai", "Cloudflare", "Cohere", + "CoreWeave", + "Crucible", "Crusoe", + "Darkbloom", + "Decart", + "Deepgram", "DeepInfra", "DeepSeek", + "DekaLLM", + "DigitalOcean", "Featherless", "Fireworks", + "Fish Audio", "Friendli", "GMICloud", "Google", "Google AI Studio", "Groq", - "Hyperbolic", + "HeyGen", "Inception", "Inceptron", "InferenceNet", + "Ionstream", "Infermatic", "Io Net", + "Inferact vLLM", "Inflection", "Liquid", "Mara", "Mancer 2", + "Meta", "Minimax", "ModelRun", "Mistral", @@ -2676,15 +4958,22 @@ export const __schema5 = Schema.Array( "Morph", "NCompass", "Nebius", + "Nex AGI", "NextBit", "Novita", "Nvidia", "OpenAI", "OpenInference", "Parasail", + "Poolside", + "Perceptron", "Perplexity", "Phala", + "Recraft", + "Reka", "Relace", + "Sail Research", + "Sakana AI", "SambaNova", "Seed", "SiliconFlow", @@ -2693,6758 +4982,30620 @@ export const __schema5 = Schema.Array( "Stealth", "StreamLake", "Switchpoint", + "Tencent", + "Tenstorrent", "Together", "Upstage", "Venice", + "Wafer", "WandB", + "Quiver", + "Krea", + "Runway", "Xiaomi", "xAI", "Z.AI", "FakeProvider" - ]), - Schema.String - ]) -) -export type __schema11 = number -export const __schema11 = Schema.Number.check(Schema.isFinite()) -export type __schema13 = unknown -export const __schema13 = Schema.Unknown -export type __schema21 = string | null -export const __schema21 = Schema.Union([Schema.String, Schema.Null]) -export type __schema22 = - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - | null -export const __schema22 = Schema.Union([ - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]), - Schema.Null -]) -export type ModelName = string -export const ModelName = Schema.String -export type ChatMessageContentItemImage = { - readonly "type": "image_url" - readonly "image_url": { readonly "url": string; readonly "detail"?: "auto" | "low" | "high" } -} -export const ChatMessageContentItemImage = Schema.Struct({ - "type": Schema.Literal("image_url"), - "image_url": Schema.Struct({ - "url": Schema.String, - "detail": Schema.optionalKey(Schema.Literals(["auto", "low", "high"])) - }) -}) -export type ChatMessageContentItemAudio = { - readonly "type": "input_audio" - readonly "input_audio": { readonly "data": string; readonly "format": string } -} -export const ChatMessageContentItemAudio = Schema.Struct({ - "type": Schema.Literal("input_audio"), - "input_audio": Schema.Struct({ "data": Schema.String, "format": Schema.String }) + ]).annotate({ "description": "Name of the provider" }) + ), + "routed_service_tier": Schema.optionalKey( + Schema.Literals(["flex", "priority"]).annotate({ + "description": + "The service tier this request was routed to (e.g. flex, priority). The tier actually applied and billed is determined by the provider response and may differ." + }) + ), + "status": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "HTTP status code from the provider" }) +}).annotate({ + "description": "Details of a provider response for a generation attempt", + "identifier": "ProviderResponse" }) -export type ChatMessageContentItemVideo = { - readonly "type": "input_video" - readonly "video_url": { readonly "url": string } -} | { readonly "type": "video_url"; readonly "video_url": { readonly "url": string } } -export const ChatMessageContentItemVideo = Schema.Union([ - Schema.Struct({ "type": Schema.Literal("input_video"), "video_url": Schema.Struct({ "url": Schema.String }) }), - Schema.Struct({ "type": Schema.Literal("video_url"), "video_url": Schema.Struct({ "url": Schema.String }) }) -], { mode: "oneOf" }) -export type ChatMessageToolCall = { - readonly "id": string - readonly "type": "function" - readonly "function": { readonly "name": string; readonly "arguments": string } -} -export const ChatMessageToolCall = Schema.Struct({ - "id": Schema.String, - "type": Schema.Literal("function"), - "function": Schema.Struct({ "name": Schema.String, "arguments": Schema.String }) +export type ProviderSort = "price" | "throughput" | "latency" | "exacto" +export const ProviderSort = Schema.Literals(["price", "throughput", "latency", "exacto"]).annotate({ + "description": "The provider sorting strategy (price, throughput, latency)", + "identifier": "ProviderSort" }) -export type ChatMessageTokenLogprob = { - readonly "token": string - readonly "logprob": number - readonly "bytes": ReadonlyArray | null - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "logprob": number; readonly "bytes": ReadonlyArray | null } - > +export type ProviderSortConfig = { + readonly "by"?: "price" | "throughput" | "latency" | "exacto" | null + readonly "partition"?: "model" | "none" | null } -export const ChatMessageTokenLogprob = Schema.Struct({ - "token": Schema.String, - "logprob": Schema.Number.check(Schema.isFinite()), - "bytes": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.Null]), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "logprob": Schema.Number.check(Schema.isFinite()), - "bytes": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.Null]) +export const ProviderSortConfig = Schema.Struct({ + "by": Schema.optionalKey( + Schema.Union([ + Schema.Literal("price"), + Schema.Literal("throughput"), + Schema.Literal("latency"), + Schema.Literal("exacto"), + Schema.Null + ]).annotate({ "description": "The provider sorting strategy (price, throughput, latency)" }) + ), + "partition": Schema.optionalKey( + Schema.Union([Schema.Literal("model"), Schema.Literal("none"), Schema.Null]).annotate({ + "description": + "Partitioning strategy for sorting: \"model\" (default) groups endpoints by model before sorting (fallback models remain fallbacks), \"none\" sorts all endpoints together regardless of model." }) ) +}).annotate({ + "description": "The provider sorting strategy (price, throughput, latency)", + "identifier": "ProviderSortConfig" }) -export type ChatGenerationTokenUsage = { - readonly "completion_tokens": number - readonly "prompt_tokens": number - readonly "total_tokens": number - readonly "completion_tokens_details"?: { - readonly "reasoning_tokens"?: number | null - readonly "audio_tokens"?: number | null - readonly "accepted_prediction_tokens"?: number | null - readonly "rejected_prediction_tokens"?: number | null - } | null - readonly "prompt_tokens_details"?: { - readonly "cached_tokens"?: number - readonly "cache_write_tokens"?: number - readonly "audio_tokens"?: number - readonly "video_tokens"?: number - } | null -} -export const ChatGenerationTokenUsage = Schema.Struct({ - "completion_tokens": Schema.Number.check(Schema.isFinite()), - "prompt_tokens": Schema.Number.check(Schema.isFinite()), - "total_tokens": Schema.Number.check(Schema.isFinite()), - "completion_tokens_details": Schema.optionalKey(Schema.Union([ - Schema.Struct({ - "reasoning_tokens": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "audio_tokens": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "accepted_prediction_tokens": Schema.optionalKey( - Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]) - ), - "rejected_prediction_tokens": Schema.optionalKey( - Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null]) - ) - }), - Schema.Null - ])), - "prompt_tokens_details": Schema.optionalKey(Schema.Union([ - Schema.Struct({ - "cached_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "cache_write_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "audio_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "video_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) - }), - Schema.Null - ])) +export type Quantization = "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown" +export const Quantization = Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]) + .annotate({ "identifier": "Quantization" }) +export type RangeCapability = { readonly "max": number; readonly "min": number; readonly "type": "range" } +export const RangeCapability = Schema.Struct({ + "max": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "min": Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("range") +}).annotate({ + "description": "A parameter that accepts any value within an inclusive numeric range.", + "identifier": "RangeCapability" }) -export type ChatCompletionFinishReason = "tool_calls" | "stop" | "length" | "content_filter" | "error" -export const ChatCompletionFinishReason = Schema.Literals(["tool_calls", "stop", "length", "content_filter", "error"]) -export type JSONSchemaConfig = { - readonly "name": string - readonly "description"?: string - readonly "schema"?: {} - readonly "strict"?: boolean | null -} -export const JSONSchemaConfig = Schema.Struct({ - "name": Schema.String.check(Schema.isMaxLength(64)), - "description": Schema.optionalKey(Schema.String), - "schema": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), - "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) -}) -export type ResponseFormatTextGrammar = { readonly "type": "grammar"; readonly "grammar": string } -export const ResponseFormatTextGrammar = Schema.Struct({ "type": Schema.Literal("grammar"), "grammar": Schema.String }) -export type ChatMessageContentItemCacheControl = { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } -export const ChatMessageContentItemCacheControl = Schema.Struct({ - "type": Schema.Literal("ephemeral"), - "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) -}) -export type NamedToolChoice = { readonly "type": "function"; readonly "function": { readonly "name": string } } -export const NamedToolChoice = Schema.Struct({ - "type": Schema.Literal("function"), - "function": Schema.Struct({ "name": Schema.String }) -}) -export type ChatStreamOptions = { readonly "include_usage"?: boolean } -export const ChatStreamOptions = Schema.Struct({ "include_usage": Schema.optionalKey(Schema.Boolean) }) -export type ChatStreamingMessageToolCall = { - readonly "index": number - readonly "id"?: string | null - readonly "type"?: "function" | null - readonly "function"?: { readonly "name"?: string | null; readonly "arguments"?: string } +export type RankingsDailyItem = { + readonly "date": string + readonly "model_permaslug": string + readonly "total_tokens": string } -export const ChatStreamingMessageToolCall = Schema.Struct({ - "index": Schema.Number.check(Schema.isFinite()), - "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "type": Schema.optionalKey(Schema.Union([Schema.Literal("function"), Schema.Null])), - "function": Schema.optionalKey( - Schema.Struct({ - "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "arguments": Schema.optionalKey(Schema.String) - }) - ) -}) -export type ChatError = { - readonly "error": { - readonly "code": string | number | null - readonly "message": string - readonly "param"?: string | null - readonly "type"?: string | null - } +export const RankingsDailyItem = Schema.Struct({ + "date": Schema.String.annotate({ "description": "UTC calendar date the row is aggregated over (YYYY-MM-DD)." }), + "model_permaslug": Schema.String.annotate({ + "description": + "Model variant permaslug (e.g. `openai/gpt-4o-2024-05-13`, `openai/gpt-4o-2024-05-13:free`). Non-default variants include a `:variant` suffix and are ranked as their own entry. The reserved value `other` denotes the aggregated row covering every model outside the daily top 50 for that date — always sorted last within its date." + }), + "total_tokens": Schema.String.annotate({ + "description": + "Sum of `prompt_tokens + completion_tokens` for the day, returned as a decimal string so 64-bit values are not truncated." + }) +}).annotate({ "identifier": "RankingsDailyItem" }) +export type RankingsDailyMeta = { + readonly "as_of": string + readonly "end_date": string + readonly "start_date": string + readonly "version": "v1" } -export const ChatError = Schema.Struct({ - "error": Schema.Struct({ - "code": Schema.Union([Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())]), Schema.Null]), - "message": Schema.String, - "param": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "type": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +export const RankingsDailyMeta = Schema.Struct({ + "as_of": Schema.String.annotate({ + "description": + "ISO-8601 timestamp of when the response was generated. Reflects data-freshness because the underlying materialized view continuously ingests upstream events." + }), + "end_date": Schema.String.annotate({ "description": "Resolved end of the date window (UTC, inclusive)." }), + "start_date": Schema.String.annotate({ "description": "Resolved start of the date window (UTC, inclusive)." }), + "version": Schema.Literal("v1").annotate({ + "description": "Dataset version. Field names and grain are stable for the life of `v1`." }) +}).annotate({ "identifier": "RankingsDailyMeta" }) +export type ReasoningContext = "auto" | "all_turns" | "current_turn" | null +export const ReasoningContext = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("all_turns"), + Schema.Literal("current_turn"), + Schema.Null +]).annotate({ + "description": + "Controls which reasoning is available to the model. `auto` uses the model default (same as omitting); `all_turns` includes reasoning from earlier turns passed in input; `current_turn` limits to the current turn only. Only supported by OpenAI GPT-5.6 and newer.", + "identifier": "ReasoningContext" }) -export type OpenAIResponsesAnnotation = FileCitation | URLCitation | FilePath -export const OpenAIResponsesAnnotation = Schema.Union([FileCitation, URLCitation, FilePath]) -export type OutputItemReasoning = { - readonly "type": "reasoning" - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" +export type ReasoningDeltaEvent = { + readonly "content_index": number + readonly "delta": string + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.reasoning_text.delta" } -export const OutputItemReasoning = Schema.Struct({ - "type": Schema.Literal("reasoning"), - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) +export const ReasoningDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "delta": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_text.delta") +}).annotate({ + "description": "Event emitted when reasoning text delta is streamed", + "identifier": "ReasoningDeltaEvent" }) -export type ResponsesOutputItemReasoning = { - readonly "type": "reasoning" - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" -} -export const ResponsesOutputItemReasoning = Schema.Struct({ - "type": Schema.Literal("reasoning"), - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ) -}).annotate({ "description": "An output item containing reasoning" }) -export type OpenResponsesReasoningSummaryPartAddedEvent = { - readonly "type": "response.reasoning_summary_part.added" +export type ReasoningDoneEvent = { + readonly "content_index": number + readonly "item_id": string readonly "output_index": number + readonly "sequence_number": number + readonly "text": string + readonly "type": "response.reasoning_text.done" +} +export const ReasoningDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "text": Schema.String, + "type": Schema.Literal("response.reasoning_text.done") +}).annotate({ + "description": "Event emitted when reasoning text streaming is complete", + "identifier": "ReasoningDoneEvent" +}) +export type ReasoningEffort = "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" | null +export const ReasoningEffort = Schema.Union([ + Schema.Literal("max"), + Schema.Literal("xhigh"), + Schema.Literal("high"), + Schema.Literal("medium"), + Schema.Literal("low"), + Schema.Literal("minimal"), + Schema.Literal("none"), + Schema.Null +]).annotate({ "identifier": "ReasoningEffort" }) +export type ReasoningFormat = + | "unknown" + | "openai-responses-v1" + | "azure-openai-responses-v1" + | "xai-responses-v1" + | "meta-responses-v1" + | "anthropic-claude-v1" + | "google-gemini-v1" + | null +export const ReasoningFormat = Schema.Union([ + Schema.Literal("unknown"), + Schema.Literal("openai-responses-v1"), + Schema.Literal("azure-openai-responses-v1"), + Schema.Literal("xai-responses-v1"), + Schema.Literal("meta-responses-v1"), + Schema.Literal("anthropic-claude-v1"), + Schema.Literal("google-gemini-v1"), + Schema.Null +]).annotate({ "identifier": "ReasoningFormat" }) +export type ReasoningMode = "standard" | "pro" | null +export const ReasoningMode = Schema.Union([Schema.Literal("standard"), Schema.Literal("pro"), Schema.Null]).annotate({ + "description": + "Selects the reasoning mode. `standard` is the default; `pro` engages deeper reasoning on models that support it, billed at standard token rates. Only supported by OpenAI GPT-5.6 and newer.", + "identifier": "ReasoningMode" +}) +export type ReasoningSummaryText = { readonly "text": string; readonly "type": "summary_text" } +export const ReasoningSummaryText = Schema.Struct({ "text": Schema.String, "type": Schema.Literal("summary_text") }) + .annotate({ "identifier": "ReasoningSummaryText" }) +export type ReasoningSummaryTextDeltaEvent = { + readonly "delta": string readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number readonly "summary_index": number - readonly "part": ReasoningSummaryText + readonly "type": "response.reasoning_summary_text.delta" +} +export const ReasoningSummaryTextDeltaEvent = Schema.Struct({ + "delta": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_text.delta") +}).annotate({ + "description": "Event emitted when reasoning summary text delta is streamed", + "identifier": "ReasoningSummaryTextDeltaEvent" +}) +export type ReasoningSummaryTextDoneEvent = { + readonly "item_id": string + readonly "output_index": number readonly "sequence_number": number + readonly "summary_index": number + readonly "text": string + readonly "type": "response.reasoning_summary_text.done" } -export const OpenResponsesReasoningSummaryPartAddedEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_summary_part.added"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const ReasoningSummaryTextDoneEvent = Schema.Struct({ "item_id": Schema.String, - "summary_index": Schema.Number.check(Schema.isFinite()), - "part": ReasoningSummaryText, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a reasoning summary part is added" }) -export type OpenResponsesReasoningSummaryPartDoneEvent = { - readonly "type": "response.reasoning_summary_part.done" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "text": Schema.String, + "type": Schema.Literal("response.reasoning_summary_text.done") +}).annotate({ + "description": "Event emitted when reasoning summary text streaming is complete", + "identifier": "ReasoningSummaryTextDoneEvent" +}) +export type ReasoningSummaryVerbosity = "auto" | "concise" | "detailed" | null +export const ReasoningSummaryVerbosity = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("concise"), + Schema.Literal("detailed"), + Schema.Null +]).annotate({ "identifier": "ReasoningSummaryVerbosity" }) +export type ReasoningTextContent = { readonly "text": string; readonly "type": "reasoning_text" } +export const ReasoningTextContent = Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") }) + .annotate({ "identifier": "ReasoningTextContent" }) +export type RefusalDeltaEvent = { + readonly "content_index": number + readonly "delta": string + readonly "item_id": string readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.refusal.delta" +} +export const RefusalDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "delta": Schema.String, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.refusal.delta") +}).annotate({ "description": "Event emitted when a refusal delta is streamed", "identifier": "RefusalDeltaEvent" }) +export type RefusalDoneEvent = { + readonly "content_index": number readonly "item_id": string - readonly "summary_index": number - readonly "part": ReasoningSummaryText + readonly "output_index": number + readonly "refusal": string readonly "sequence_number": number + readonly "type": "response.refusal.done" } -export const OpenResponsesReasoningSummaryPartDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.reasoning_summary_part.done"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const RefusalDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "item_id": Schema.String, - "summary_index": Schema.Number.check(Schema.isFinite()), - "part": ReasoningSummaryText, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a reasoning summary part is complete" }) -export type OpenResponsesReasoning = { - readonly "type": "reasoning" - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" -} -export const OpenResponsesReasoning = Schema.Struct({ - "type": Schema.Literal("reasoning"), - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), - "signature": Schema.optionalKey(Schema.String), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]) + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "refusal": Schema.String, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.refusal.done") +}).annotate({ "description": "Event emitted when refusal streaming is complete", "identifier": "RefusalDoneEvent" }) +export type Objects_149 = { readonly [x: string]: string } +export const Objects_149 = Schema.Record( + Schema.String, + Schema.String.check(Schema.isMaxLength(512).annotate({ "expected": "a value with a length of at most 512" })) +) +export type RequestTimeoutResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const RequestTimeoutResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ) -}).annotate({ "description": "Reasoning output item with signature and format extensions" }) -export type OutputItemWebSearchCall = { - readonly "type": "web_search_call" - readonly "id": string - readonly "status": WebSearchStatus +}).annotate({ "description": "Error data for RequestTimeoutResponse", "identifier": "RequestTimeoutResponseErrorData" }) +export type ResponseHealingPlugin = { readonly "enabled"?: boolean; readonly "id": "response-healing" } +export const ResponseHealingPlugin = Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the response-healing plugin for this request. Defaults to true." + }) + ), + "id": Schema.Literal("response-healing") +}).annotate({ "identifier": "ResponseHealingPlugin" }) +export type ResponseIncludesEnum = + | "file_search_call.results" + | "message.input_image.image_url" + | "computer_call_output.output.image_url" + | "reasoning.encrypted_content" + | "code_interpreter_call.outputs" +export const ResponseIncludesEnum = Schema.Literals([ + "file_search_call.results", + "message.input_image.image_url", + "computer_call_output.output.image_url", + "reasoning.encrypted_content", + "code_interpreter_call.outputs" +]).annotate({ "identifier": "ResponseIncludesEnum" }) +export type ResponsesErrorField = { + readonly "code": + | "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" + | "bio_policy" + readonly "message": string + readonly [x: string]: Schema.Json +} | null +export const ResponsesErrorField = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "code": Schema.Literals([ + "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", + "bio_policy" + ]), + "message": Schema.String + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null +]).annotate({ "description": "Error information returned from the API", "identifier": "ResponsesErrorField" }) +export type RouterAttempt = { readonly "model": string; readonly "provider": string; readonly "status": number } +export const RouterAttempt = Schema.Struct({ + "model": Schema.String, + "provider": Schema.String, + "status": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "RouterAttempt" }) +export type RouterParams = { + readonly "quality_floor"?: number + readonly "throughput_floor"?: number + readonly "version_group"?: string } -export const OutputItemWebSearchCall = Schema.Struct({ - "type": Schema.Literal("web_search_call"), - "id": Schema.String, - "status": WebSearchStatus +export const RouterParams = Schema.Struct({ + "quality_floor": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "throughput_floor": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "version_group": Schema.optionalKey(Schema.String) +}).annotate({ "identifier": "RouterParams" }) +export type RoutingStrategy = + | "direct" + | "auto" + | "free" + | "latest" + | "alias" + | "fallback" + | "pareto" + | "bodybuilder" + | "fusion" +export const RoutingStrategy = Schema.Literals([ + "direct", + "auto", + "free", + "latest", + "alias", + "fallback", + "pareto", + "bodybuilder", + "fusion" +]).annotate({ "identifier": "RoutingStrategy" }) +export type SandboxSleepAfterSeconds = number +export const SandboxSleepAfterSeconds = Schema.Number.annotate({ + "description": + "How long (in seconds) the container stays warm after its last command before sleeping, freeing its capacity slot. Idle-based: each command renews the timer. Defaults to 900 (15 minutes); capped at 2592000 (30 days)." +}).check(Schema.isInt().annotate({ "expected": "an integer", "identifier": "SandboxSleepAfterSeconds" })) +export type SearchContextSizeEnum = "low" | "medium" | "high" +export const SearchContextSizeEnum = Schema.Literals(["low", "medium", "high"]).annotate({ + "description": "Size of the search context for web search tools", + "identifier": "SearchContextSizeEnum" }) -export type ResponsesWebSearchCallOutput = { - readonly "type": "web_search_call" - readonly "id": string - readonly "status": WebSearchStatus -} -export const ResponsesWebSearchCallOutput = Schema.Struct({ - "type": Schema.Literal("web_search_call"), - "id": Schema.String, - "status": WebSearchStatus +export type SearchModelsServerToolConfig = { readonly "max_results"?: number } +export const SearchModelsServerToolConfig = Schema.Struct({ + "max_results": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of models to return. Defaults to 5, max 20." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) +}).annotate({ + "description": "Configuration for the openrouter:experimental__search_models server tool", + "identifier": "SearchModelsServerToolConfig" }) -export type OutputItemFileSearchCall = { - readonly "type": "file_search_call" - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": WebSearchStatus -} -export const OutputItemFileSearchCall = Schema.Struct({ - "type": Schema.Literal("file_search_call"), - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": WebSearchStatus +export type SearchQualityLevel = "low" | "medium" | "high" +export const SearchQualityLevel = Schema.Literals(["low", "medium", "high"]).annotate({ + "description": + "How much context to retrieve per result. Applies to Exa, Parallel, and Perplexity engines; ignored with native provider search and Firecrawl. For Exa, pins a fixed per-result character cap (low=5,000, medium=15,000, high=30,000); when omitted, Exa picks an adaptive size per query and document (typically ~2,000–4,000 characters per result). For Parallel, controls the total characters across all results; when omitted, Parallel uses its own default size. For Perplexity, maps directly to the Search API's native search_context_size parameter. Overridden by `max_characters` when both are set.", + "identifier": "SearchQualityLevel" }) -export type ResponsesOutputItemFileSearchCall = { - readonly "type": "file_search_call" - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": WebSearchStatus +export type Objects_150 = { + readonly "tool_calls_executed"?: number | null + readonly "tool_calls_requested"?: number | null + readonly "web_search_requests"?: number | null + readonly [x: string]: Schema.Json } -export const ResponsesOutputItemFileSearchCall = Schema.Struct({ - "type": Schema.Literal("file_search_call"), - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": WebSearchStatus +export const Objects_150 = Schema.StructWithRest( + Schema.Struct({ + "tool_calls_executed": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Number of OpenRouter server tool calls that executed and produced a result." + }) + ), + "tool_calls_requested": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": + "Total number of OpenRouter server-orchestrated tool calls the model requested, across all tool types. Provider-native tools (e.g. native web search) are not counted here." + }) + ), + "web_search_requests": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": + "Number of web searches performed by server-side tools. For server-orchestrated tool calls a web search is also counted in tool_calls_requested; provider-native web search may report web_search_requests only. Do not sum the two." + }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type ServiceTier = "auto" | "default" | "flex" | "priority" | "scale" | null +export const ServiceTier = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("default"), + Schema.Literal("flex"), + Schema.Literal("priority"), + Schema.Literal("scale"), + Schema.Null +]).annotate({ "identifier": "ServiceTier" }) +export type ServiceUnavailableResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const ServiceUnavailableResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for ServiceUnavailableResponse", + "identifier": "ServiceUnavailableResponseErrorData" }) -export type OutputItemImageGenerationCall = { - readonly "type": "image_generation_call" - readonly "id": string - readonly "result"?: string - readonly "status": ImageGenerationStatus -} -export const OutputItemImageGenerationCall = Schema.Struct({ - "type": Schema.Literal("image_generation_call"), - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": ImageGenerationStatus +export type ShellCallStatus = "in_progress" | "completed" | "incomplete" +export const ShellCallStatus = Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "description": "Status of a shell call or its output.", + "identifier": "ShellCallStatus" }) -export type ResponsesImageGenerationCall = { - readonly "type": "image_generation_call" - readonly "id": string - readonly "result"?: string - readonly "status": ImageGenerationStatus -} -export const ResponsesImageGenerationCall = Schema.Struct({ - "type": Schema.Literal("image_generation_call"), - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": ImageGenerationStatus +export type ShellServerTool = { readonly "type": "shell" } +export const ShellServerTool = Schema.Struct({ "type": Schema.Literal("shell") }).annotate({ + "description": "Shell tool configuration", + "identifier": "ShellServerTool" }) -export type OpenResponsesFunctionToolCall = { - readonly "type": "function_call" - readonly "call_id": string - readonly "name": string - readonly "arguments": string - readonly "id": string - readonly "status"?: ToolCallStatus -} -export const OpenResponsesFunctionToolCall = Schema.Struct({ - "type": Schema.Literal("function_call"), - "call_id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "id": Schema.String, - "status": Schema.optionalKey(ToolCallStatus) -}).annotate({ "description": "A function call initiated by the model" }) -export type OpenResponsesFunctionCallOutput = { - readonly "type": "function_call_output" - readonly "id"?: string - readonly "call_id": string - readonly "output": string - readonly "status"?: ToolCallStatus -} -export const OpenResponsesFunctionCallOutput = Schema.Struct({ - "type": Schema.Literal("function_call_output"), - "id": Schema.optionalKey(Schema.String), - "call_id": Schema.String, - "output": Schema.String, - "status": Schema.optionalKey(ToolCallStatus) -}).annotate({ "description": "The output from a function call execution" }) -export type OpenResponsesWebSearchPreviewTool = { - readonly "type": "web_search_preview" - readonly "search_context_size"?: ResponsesSearchContextSize - readonly "user_location"?: WebSearchPreviewToolUserLocation -} -export const OpenResponsesWebSearchPreviewTool = Schema.Struct({ - "type": Schema.Literal("web_search_preview"), - "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), - "user_location": Schema.optionalKey(WebSearchPreviewToolUserLocation) -}).annotate({ "description": "Web search preview tool configuration" }) -export type OpenResponsesWebSearchPreview20250311Tool = { - readonly "type": "web_search_preview_2025_03_11" - readonly "search_context_size"?: ResponsesSearchContextSize - readonly "user_location"?: WebSearchPreviewToolUserLocation -} -export const OpenResponsesWebSearchPreview20250311Tool = Schema.Struct({ - "type": Schema.Literal("web_search_preview_2025_03_11"), - "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), - "user_location": Schema.optionalKey(WebSearchPreviewToolUserLocation) -}).annotate({ "description": "Web search preview tool configuration (2025-03-11 version)" }) -export type OpenResponsesWebSearchTool = { - readonly "type": "web_search" - readonly "filters"?: { readonly "allowed_domains"?: ReadonlyArray } - readonly "search_context_size"?: ResponsesSearchContextSize - readonly "user_location"?: ResponsesWebSearchUserLocation -} -export const OpenResponsesWebSearchTool = Schema.Struct({ - "type": Schema.Literal("web_search"), - "filters": Schema.optionalKey(Schema.Struct({ "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)) })), - "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), - "user_location": Schema.optionalKey(ResponsesWebSearchUserLocation) -}).annotate({ "description": "Web search tool configuration" }) -export type OpenResponsesWebSearch20250826Tool = { - readonly "type": "web_search_2025_08_26" - readonly "filters"?: { readonly "allowed_domains"?: ReadonlyArray } - readonly "search_context_size"?: ResponsesSearchContextSize - readonly "user_location"?: ResponsesWebSearchUserLocation -} -export const OpenResponsesWebSearch20250826Tool = Schema.Struct({ - "type": Schema.Literal("web_search_2025_08_26"), - "filters": Schema.optionalKey(Schema.Struct({ "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)) })), - "search_context_size": Schema.optionalKey(ResponsesSearchContextSize), - "user_location": Schema.optionalKey(ResponsesWebSearchUserLocation) -}).annotate({ "description": "Web search tool configuration (2025-08-26 version)" }) -export type OpenAIResponsesReasoningConfig = { - readonly "effort"?: OpenAIResponsesReasoningEffort - readonly "summary"?: ReasoningSummaryVerbosity -} -export const OpenAIResponsesReasoningConfig = Schema.Struct({ - "effort": Schema.optionalKey(OpenAIResponsesReasoningEffort), - "summary": Schema.optionalKey(ReasoningSummaryVerbosity) +export type ShellServerToolEngine = "auto" | "openrouter" +export const ShellServerToolEngine = Schema.Literals(["auto", "openrouter"]).annotate({ + "description": + "Which shell engine to use. \"openrouter\" runs commands server-side in the OpenRouter sandbox. \"auto\" (default) keeps the provider's native hosted shell when available (OpenAI); on other providers the call is routed to the OpenRouter sandbox.", + "identifier": "ShellServerToolEngine" }) -export type OpenResponsesReasoningConfig = { - readonly "effort"?: OpenAIResponsesReasoningEffort - readonly "summary"?: ReasoningSummaryVerbosity - readonly "max_tokens"?: number - readonly "enabled"?: boolean -} -export const OpenResponsesReasoningConfig = Schema.Struct({ - "effort": Schema.optionalKey(OpenAIResponsesReasoningEffort), - "summary": Schema.optionalKey(ReasoningSummaryVerbosity), - "max_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "enabled": Schema.optionalKey(Schema.Boolean) -}).annotate({ "description": "Configuration for reasoning mode in the response" }) -export type ResponseFormatTextConfig = - | ResponsesFormatText - | ResponsesFormatJSONObject - | ResponsesFormatTextJSONSchemaConfig -export const ResponseFormatTextConfig = Schema.Union([ - ResponsesFormatText, - ResponsesFormatJSONObject, - ResponsesFormatTextJSONSchemaConfig -]).annotate({ "description": "Text response format configuration" }) -export type OpenResponsesLogProbs = { +export type StopServerToolsWhenFinishReasonIs = { readonly "reason": string; readonly "type": "finish_reason_is" } +export const StopServerToolsWhenFinishReasonIs = Schema.Struct({ + "reason": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "type": Schema.Literal("finish_reason_is") +}).annotate({ + "description": "Stop when the upstream model emits this finish reason (e.g. `length`).", + "identifier": "StopServerToolsWhenFinishReasonIs" +}) +export type StopServerToolsWhenHasToolCall = { readonly "tool_name": string; readonly "type": "has_tool_call" } +export const StopServerToolsWhenHasToolCall = Schema.Struct({ + "tool_name": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "type": Schema.Literal("has_tool_call") +}).annotate({ + "description": "Stop after a tool with this name has been called.", + "identifier": "StopServerToolsWhenHasToolCall" +}) +export type StopServerToolsWhenMaxCost = { readonly "max_cost_in_dollars": number; readonly "type": "max_cost" } +export const StopServerToolsWhenMaxCost = Schema.Struct({ + "max_cost_in_dollars": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "type": Schema.Literal("max_cost") +}).annotate({ + "description": "Stop once cumulative cost across the loop exceeds this dollar threshold.", + "identifier": "StopServerToolsWhenMaxCost" +}) +export type StopServerToolsWhenMaxTokensUsed = { readonly "max_tokens": number; readonly "type": "max_tokens_used" } +export const StopServerToolsWhenMaxTokensUsed = Schema.Struct({ + "max_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("max_tokens_used") +}).annotate({ + "description": "Stop once cumulative token usage across the loop exceeds this threshold.", + "identifier": "StopServerToolsWhenMaxTokensUsed" +}) +export type StopServerToolsWhenStepCountIs = { readonly "step_count": number; readonly "type": "step_count_is" } +export const StopServerToolsWhenStepCountIs = Schema.Struct({ + "step_count": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("step_count_is") +}).annotate({ + "description": "Stop after the agent loop has executed this many steps.", + "identifier": "StopServerToolsWhenStepCountIs" +}) +export type StreamLogprob = { + readonly "bytes"?: ReadonlyArray readonly "logprob": number readonly "token": string - readonly "top_logprobs"?: ReadonlyArray + readonly "top_logprobs"?: ReadonlyArray< + { readonly "bytes"?: ReadonlyArray; readonly "logprob"?: number; readonly "token"?: string } + > } -export const OpenResponsesLogProbs = Schema.Struct({ - "logprob": Schema.Number.check(Schema.isFinite()), +export const StreamLogprob = Schema.Struct({ + "bytes": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), "token": Schema.String, - "top_logprobs": Schema.optionalKey(Schema.Array(OpenResponsesTopLogprobs)) -}).annotate({ "description": "Log probability information for a token" }) -export type BadRequestResponse = { readonly "error": BadRequestResponseErrorData; readonly "user_id"?: string } -export const BadRequestResponse = Schema.Struct({ - "error": BadRequestResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Bad Request - Invalid request parameters or malformed input" }) -export type UnauthorizedResponse = { readonly "error": UnauthorizedResponseErrorData; readonly "user_id"?: string } -export const UnauthorizedResponse = Schema.Struct({ - "error": UnauthorizedResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Unauthorized - Authentication required or invalid credentials" }) -export type PaymentRequiredResponse = { - readonly "error": PaymentRequiredResponseErrorData - readonly "user_id"?: string -} -export const PaymentRequiredResponse = Schema.Struct({ - "error": PaymentRequiredResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Payment Required - Insufficient credits or quota to complete request" }) -export type NotFoundResponse = { readonly "error": NotFoundResponseErrorData; readonly "user_id"?: string } -export const NotFoundResponse = Schema.Struct({ - "error": NotFoundResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Not Found - Resource does not exist" }) -export type RequestTimeoutResponse = { readonly "error": RequestTimeoutResponseErrorData; readonly "user_id"?: string } -export const RequestTimeoutResponse = Schema.Struct({ - "error": RequestTimeoutResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Request Timeout - Operation exceeded time limit" }) -export type PayloadTooLargeResponse = { - readonly "error": PayloadTooLargeResponseErrorData - readonly "user_id"?: string -} -export const PayloadTooLargeResponse = Schema.Struct({ - "error": PayloadTooLargeResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Payload Too Large - Request payload exceeds size limits" }) -export type UnprocessableEntityResponse = { - readonly "error": UnprocessableEntityResponseErrorData - readonly "user_id"?: string -} -export const UnprocessableEntityResponse = Schema.Struct({ - "error": UnprocessableEntityResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Unprocessable Entity - Semantic validation failure" }) -export type TooManyRequestsResponse = { - readonly "error": TooManyRequestsResponseErrorData - readonly "user_id"?: string -} -export const TooManyRequestsResponse = Schema.Struct({ - "error": TooManyRequestsResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Too Many Requests - Rate limit exceeded" }) -export type InternalServerResponse = { readonly "error": InternalServerResponseErrorData; readonly "user_id"?: string } -export const InternalServerResponse = Schema.Struct({ - "error": InternalServerResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Internal Server Error - Unexpected server error" }) -export type BadGatewayResponse = { readonly "error": BadGatewayResponseErrorData; readonly "user_id"?: string } -export const BadGatewayResponse = Schema.Struct({ - "error": BadGatewayResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Bad Gateway - Provider/upstream API failure" }) -export type ServiceUnavailableResponse = { - readonly "error": ServiceUnavailableResponseErrorData - readonly "user_id"?: string + "top_logprobs": Schema.optionalKey(Schema.Array( + Schema.Struct({ + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) + ), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "token": Schema.optionalKey(Schema.String) + }).annotate({ "description": "Alternative token with its log probability" }) + )) +}).annotate({ "description": "Log probability information for a token", "identifier": "StreamLogprob" }) +export type StreamLogprobTopLogprob = { + readonly "bytes"?: ReadonlyArray + readonly "logprob"?: number + readonly "token"?: string } -export const ServiceUnavailableResponse = Schema.Struct({ - "error": ServiceUnavailableResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Service Unavailable - Service temporarily unavailable" }) -export type EdgeNetworkTimeoutResponse = { - readonly "error": EdgeNetworkTimeoutResponseErrorData - readonly "user_id"?: string +export const StreamLogprobTopLogprob = Schema.Struct({ + "bytes": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })))), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "token": Schema.optionalKey(Schema.String) +}).annotate({ "description": "Alternative token with its log probability", "identifier": "StreamLogprobTopLogprob" }) +export type STTInputAudio = { readonly "data": string; readonly "format": string } +export const STTInputAudio = Schema.Struct({ + "data": Schema.String.annotate({ "description": "Base64-encoded audio data (raw bytes, not a data URI)" }), + "format": Schema.String.annotate({ + "description": "Audio format (e.g., wav, mp3, flac, m4a, ogg, webm, aac). Supported formats vary by provider." + }) +}).annotate({ "description": "Base64-encoded audio to transcribe", "identifier": "STTInputAudio" }) +export type STTSegment = { + readonly "avg_logprob"?: number + readonly "compression_ratio"?: number + readonly "end": number + readonly "id": number + readonly "no_speech_prob"?: number + readonly "seek"?: number + readonly "start": number + readonly "temperature"?: number + readonly "text": string + readonly "tokens"?: ReadonlyArray } -export const EdgeNetworkTimeoutResponse = Schema.Struct({ - "error": EdgeNetworkTimeoutResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Infrastructure Timeout - Provider request timed out at edge network" }) -export type ProviderOverloadedResponse = { - readonly "error": ProviderOverloadedResponseErrorData - readonly "user_id"?: string +export const STTSegment = Schema.Struct({ + "avg_logprob": Schema.optionalKey( + Schema.Number.annotate({ "description": "Average log probability of the segment", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "compression_ratio": Schema.optionalKey( + Schema.Number.annotate({ "description": "Compression ratio of the segment", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "end": Schema.Number.annotate({ "description": "Segment end time in seconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "id": Schema.Number.annotate({ "description": "Segment index within the transcript" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "no_speech_prob": Schema.optionalKey( + Schema.Number.annotate({ "description": "Probability the segment contains no speech", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "seek": Schema.optionalKey( + Schema.Number.annotate({ "description": "Seek offset of the segment" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "start": Schema.Number.annotate({ "description": "Segment start time in seconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "Temperature used for the segment", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "text": Schema.String.annotate({ "description": "Transcribed text of the segment" }), + "tokens": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))).annotate({ + "description": "Token IDs of the segment" + }) + ) +}).annotate({ + "description": "A timestamped transcript segment, returned when response_format is verbose_json", + "identifier": "STTSegment" +}) +export type STTTimestampGranularity = "word" | "segment" +export const STTTimestampGranularity = Schema.Literals(["word", "segment"]).annotate({ + "description": "A timestamp detail level for verbose_json transcription responses.", + "identifier": "STTTimestampGranularity" +}) +export type STTUsage = { + readonly "cost"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "seconds"?: number + readonly "total_tokens"?: number } -export const ProviderOverloadedResponse = Schema.Struct({ - "error": ProviderOverloadedResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Provider Overloaded - Provider is temporarily overloaded" }) -export type OpenResponsesEasyInputMessage = { - readonly "type"?: "message" - readonly "role": "user" | "system" | "assistant" | "developer" - readonly "content": - | ReadonlyArray< - | ResponseInputText - | { readonly "type": "input_image"; readonly "detail": "auto" | "high" | "low"; readonly "image_url"?: string } - | ResponseInputFile - | ResponseInputAudio - | ResponseInputVideo - > - | string +export const STTUsage = Schema.Struct({ + "cost": Schema.optionalKey( + Schema.Number.annotate({ "description": "Total cost of the request in USD", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of input tokens billed for this request" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of output tokens generated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "seconds": Schema.optionalKey( + Schema.Number.annotate({ "description": "Duration of the input audio in seconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "total_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Total number of tokens used (input + output)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) +}).annotate({ "description": "Aggregated usage statistics for the request", "identifier": "STTUsage" }) +export type STTWord = { + readonly "end": number + readonly "speaker"?: number + readonly "start": number + readonly "word": string } -export const OpenResponsesEasyInputMessage = Schema.Struct({ - "type": Schema.optionalKey(Schema.Literal("message")), - "role": Schema.Literals(["user", "system", "assistant", "developer"]), - "content": Schema.Union([ - Schema.Array( - Schema.Union([ - ResponseInputText, - Schema.Struct({ - "type": Schema.Literal("input_image"), - "detail": Schema.Literals(["auto", "high", "low"]), - "image_url": Schema.optionalKey(Schema.String) - }).annotate({ "description": "Image input content item" }), - ResponseInputFile, - ResponseInputAudio, - ResponseInputVideo - ], { mode: "oneOf" }) - ), - Schema.String - ]) +export const STTWord = Schema.Struct({ + "end": Schema.Number.annotate({ "description": "Word end time in seconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "speaker": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Zero-based speaker index for the word, present when the provider returns speaker diarization" + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "start": Schema.Number.annotate({ "description": "Word start time in seconds", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "word": Schema.String.annotate({ "description": "The transcribed word" }) +}).annotate({ + "description": "A timestamped word, returned when the provider includes word-level timestamps", + "identifier": "STTWord" }) -export type OpenResponsesInputMessageItem = { - readonly "id"?: string - readonly "type"?: "message" - readonly "role": "user" | "system" | "developer" - readonly "content": ReadonlyArray< - | ResponseInputText - | { readonly "type": "input_image"; readonly "detail": "auto" | "high" | "low"; readonly "image_url"?: string } - | ResponseInputFile - | ResponseInputAudio - | ResponseInputVideo - > +export type SubagentNestedTool = { readonly "parameters"?: {}; readonly "type": string } +export const SubagentNestedTool = Schema.Struct({ + "parameters": Schema.optionalKey(Schema.Struct({})), + "type": Schema.String +}).annotate({ + "description": + "A tool made available to the subagent. Only OpenRouter server tools (e.g. openrouter:web_search) are supported; function tools are rejected because the worker has no way to execute them. The subagent tool may not list itself.", + "identifier": "SubagentNestedTool" +}) +export type SubagentReasoning = { + readonly "effort"?: "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" + readonly "max_tokens"?: number } -export const OpenResponsesInputMessageItem = Schema.Struct({ - "id": Schema.optionalKey(Schema.String), - "type": Schema.optionalKey(Schema.Literal("message")), - "role": Schema.Literals(["user", "system", "developer"]), - "content": Schema.Array( - Schema.Union([ - ResponseInputText, - Schema.Struct({ - "type": Schema.Literal("input_image"), - "detail": Schema.Literals(["auto", "high", "low"]), - "image_url": Schema.optionalKey(Schema.String) - }).annotate({ "description": "Image input content item" }), - ResponseInputFile, - ResponseInputAudio, - ResponseInputVideo - ], { mode: "oneOf" }) +export const SubagentReasoning = Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Literals(["max", "xhigh", "high", "medium", "low", "minimal", "none"]).annotate({ + "description": "Reasoning effort level for the subagent call." + }) + ), + "max_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of reasoning tokens the subagent may use. Accepted and validated but not yet forwarded to the subagent call." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) ) +}).annotate({ + "description": + "Reasoning configuration forwarded to the subagent call. Use this to control reasoning effort and token budget for models that support extended thinking.", + "identifier": "SubagentReasoning" }) -export type ProviderSortConfig = { readonly "by"?: ProviderSort | null; readonly "partition"?: "model" | "none" | null } -export const ProviderSortConfig = Schema.Struct({ - "by": Schema.optionalKey(Schema.Union([ProviderSort, Schema.Null])), - "partition": Schema.optionalKey(Schema.Union([Schema.Literals(["model", "none"]), Schema.Null])) +export type SubmitGenerationFeedbackRequest = { + readonly "category": + | "latency" + | "incoherence" + | "incorrect_response" + | "formatting" + | "billing" + | "api_error" + | "other" + readonly "comment"?: string + readonly "generation_id": string +} +export const SubmitGenerationFeedbackRequest = Schema.Struct({ + "category": Schema.Literals([ + "latency", + "incoherence", + "incorrect_response", + "formatting", + "billing", + "api_error", + "other" + ]).annotate({ "description": "The category of feedback being reported" }), + "comment": Schema.optionalKey( + Schema.String.annotate({ "description": "An optional free-text comment describing the feedback" }).check( + Schema.isMaxLength(1000).annotate({ "expected": "a value with a length of at most 1000" }) + ) + ), + "generation_id": Schema.String.annotate({ "description": "The generation to submit feedback on" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ + "description": "Structured feedback about a specific generation", + "identifier": "SubmitGenerationFeedbackRequest" }) -export type PreferredMinThroughput = number | PercentileThroughputCutoffs | unknown -export const PreferredMinThroughput = Schema.Union([ - Schema.Number.check(Schema.isFinite()), - PercentileThroughputCutoffs, - Schema.Unknown -]).annotate({ - "description": - "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 type SubmitGenerationFeedbackResponse = { readonly "data": { readonly "success": true } } +export const SubmitGenerationFeedbackResponse = Schema.Struct({ + "data": Schema.Struct({ + "success": Schema.Literal(true).annotate({ "description": "Whether the feedback was recorded" }) + }) +}).annotate({ + "description": "Confirmation that the feedback was recorded", + "identifier": "SubmitGenerationFeedbackResponse" }) -export type PreferredMaxLatency = number | PercentileLatencyCutoffs | unknown -export const PreferredMaxLatency = Schema.Union([ - Schema.Number.check(Schema.isFinite()), - PercentileLatencyCutoffs, - Schema.Unknown -]).annotate({ +export type SupportedParameters = {} +export const SupportedParameters = Schema.Struct({}).annotate({ "description": - "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." + "Union of supported parameters across every endpoint of this model. Coarse discovery aid; the definitive per-endpoint set is behind the endpoints URL.", + "identifier": "SupportedParameters" }) -export type PDFParserOptions = { readonly "engine"?: PDFParserEngine } -export const PDFParserOptions = Schema.Struct({ "engine": Schema.optionalKey(PDFParserEngine) }).annotate({ - "description": "Options for PDF parsing." -}) -export type ForbiddenResponse = { readonly "error": ForbiddenResponseErrorData; readonly "user_id"?: string } -export const ForbiddenResponse = Schema.Struct({ - "error": ForbiddenResponseErrorData, - "user_id": Schema.optionalKey(Schema.String) -}).annotate({ "description": "Forbidden - Authentication successful but insufficient permissions" }) -export type ModelArchitecture = { - readonly "tokenizer"?: ModelGroup - readonly "instruct_type"?: - | "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" - readonly "modality": string - readonly "input_modalities": ReadonlyArray - readonly "output_modalities": ReadonlyArray +export type TaskClassificationMacroCategory = { + readonly "key": string + readonly "label": string + readonly "token_share": number + readonly "usage_share": number } -export const ModelArchitecture = Schema.Struct({ - "tokenizer": Schema.optionalKey(ModelGroup), - "instruct_type": Schema.optionalKey( - Schema.Literals([ - "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" - ]).annotate({ "description": "Instruction format type" }) - ), - "modality": Schema.String.annotate({ "description": "Primary modality of the model" }), - "input_modalities": Schema.Array(InputModality).annotate({ "description": "Supported input modalities" }), - "output_modalities": Schema.Array(OutputModality).annotate({ "description": "Supported output modalities" }) -}).annotate({ "description": "Model architecture information" }) -export type PublicEndpoint = { - readonly "name": string - readonly "model_id": string - readonly "model_name": string - readonly "context_length": number - readonly "pricing": { - readonly "prompt": string - readonly "completion": string - readonly "request"?: string - readonly "image"?: string - readonly "image_token"?: string - readonly "image_output"?: string - readonly "audio"?: string - readonly "audio_output"?: string - readonly "input_audio_cache"?: string - readonly "web_search"?: string - readonly "internal_reasoning"?: string - readonly "input_cache_read"?: string - readonly "input_cache_write"?: string - readonly "discount"?: number - } - readonly "provider_name": ProviderName - readonly "tag": string - readonly "quantization": "int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown" - readonly "max_completion_tokens": number - readonly "max_prompt_tokens": number - readonly "supported_parameters": ReadonlyArray - readonly "status"?: EndpointStatus - readonly "uptime_last_30m": number - readonly "supports_implicit_caching": boolean - readonly "latency_last_30m": PercentileStats - readonly "throughput_last_30m": { - readonly "p50": number - readonly "p75": number - readonly "p90": number - readonly "p99": number - } +export const TaskClassificationMacroCategory = Schema.Struct({ + "key": Schema.String.annotate({ "description": "Macro-category identifier." }), + "label": Schema.String.annotate({ "description": "Human-readable label for the macro-category." }), + "token_share": Schema.Number.annotate({ + "description": "Combined token share of all classifications in this macro-category (0–1).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_share": Schema.Number.annotate({ + "description": "Combined usage share of all classifications in this macro-category (0–1).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ "identifier": "TaskClassificationMacroCategory" }) +export type TaskClassificationModel = { + readonly "id": string + readonly "tag_token_share": number + readonly "tag_usage_share": number } -export const PublicEndpoint = Schema.Struct({ - "name": Schema.String, - "model_id": Schema.String.annotate({ "description": "The unique identifier for the model (permaslug)" }), - "model_name": Schema.String, - "context_length": Schema.Number.check(Schema.isFinite()), - "pricing": Schema.Struct({ - "prompt": Schema.String.annotate({ "description": "A number or string value representing a large number" }), - "completion": Schema.String.annotate({ "description": "A number or string value representing a large number" }), - "request": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image_token": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "image_output": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "audio": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "audio_output": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_audio_cache": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "web_search": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "internal_reasoning": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_cache_read": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "input_cache_write": Schema.optionalKey( - Schema.String.annotate({ "description": "A number or string value representing a large number" }) - ), - "discount": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) - }), - "provider_name": ProviderName, - "tag": Schema.String, - "quantization": Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"]), - "max_completion_tokens": Schema.Number.check(Schema.isFinite()), - "max_prompt_tokens": Schema.Number.check(Schema.isFinite()), - "supported_parameters": Schema.Array(Parameter), - "status": Schema.optionalKey(EndpointStatus), - "uptime_last_30m": Schema.Number.check(Schema.isFinite()), - "supports_implicit_caching": Schema.Boolean, - "latency_last_30m": PercentileStats, - "throughput_last_30m": Schema.Struct({ - "p50": Schema.Number.annotate({ "description": "Median (50th percentile)" }).check(Schema.isFinite()), - "p75": Schema.Number.annotate({ "description": "75th percentile" }).check(Schema.isFinite()), - "p90": Schema.Number.annotate({ "description": "90th percentile" }).check(Schema.isFinite()), - "p99": Schema.Number.annotate({ "description": "99th percentile" }).check(Schema.isFinite()) - }).annotate({ +export const TaskClassificationModel = Schema.Struct({ + "id": Schema.String.annotate({ "description": "Model identifier (permaslug)." }), + "tag_token_share": Schema.Number.annotate({ "description": - "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." - }) -}).annotate({ "description": "Information about a specific model endpoint" }) -export type __schema20 = { - readonly "type": "reasoning.summary" - readonly "summary": string - readonly "id"?: __schema21 - readonly "format"?: __schema22 - readonly "index"?: __schema11 -} | { - readonly "type": "reasoning.encrypted" - readonly "data": string - readonly "id"?: __schema21 - readonly "format"?: __schema22 - readonly "index"?: __schema11 -} | { - readonly "type": "reasoning.text" - readonly "text"?: string | null - readonly "signature"?: string | null - readonly "id"?: __schema21 - readonly "format"?: __schema22 - readonly "index"?: __schema11 -} -export const __schema20 = Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("reasoning.summary"), - "summary": Schema.String, - "id": Schema.optionalKey(__schema21), - "format": Schema.optionalKey(__schema22), - "index": Schema.optionalKey(__schema11) - }), - Schema.Struct({ - "type": Schema.Literal("reasoning.encrypted"), - "data": Schema.String, - "id": Schema.optionalKey(__schema21), - "format": Schema.optionalKey(__schema22), - "index": Schema.optionalKey(__schema11) - }), - Schema.Struct({ - "type": Schema.Literal("reasoning.text"), - "text": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "signature": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "id": Schema.optionalKey(__schema21), - "format": Schema.optionalKey(__schema22), - "index": Schema.optionalKey(__schema11) - }) -], { mode: "oneOf" }) -export type __schema14 = __schema11 | ModelName | __schema13 -export const __schema14 = Schema.Union([__schema11, ModelName, __schema13]) -export type ChatMessageTokenLogprobs = { - readonly "content": ReadonlyArray | null - readonly "refusal": ReadonlyArray | null -} -export const ChatMessageTokenLogprobs = Schema.Struct({ - "content": Schema.Union([Schema.Array(ChatMessageTokenLogprob), Schema.Null]), - "refusal": Schema.Union([Schema.Array(ChatMessageTokenLogprob), Schema.Null]) -}) -export type __schema26 = ChatCompletionFinishReason | null -export const __schema26 = Schema.Union([ChatCompletionFinishReason, Schema.Null]) -export type ResponseFormatJSONSchema = { readonly "type": "json_schema"; readonly "json_schema": JSONSchemaConfig } -export const ResponseFormatJSONSchema = Schema.Struct({ - "type": Schema.Literal("json_schema"), - "json_schema": JSONSchemaConfig -}) -export type ChatMessageContentItemText = { - readonly "type": "text" - readonly "text": string - readonly "cache_control"?: ChatMessageContentItemCacheControl -} -export const ChatMessageContentItemText = Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "cache_control": Schema.optionalKey(ChatMessageContentItemCacheControl) -}) -export type ToolDefinitionJson = { - readonly "type": "function" - readonly "function": { - readonly "name": string - readonly "description"?: string - readonly "parameters"?: {} - readonly "strict"?: boolean | null - } - readonly "cache_control"?: ChatMessageContentItemCacheControl -} -export const ToolDefinitionJson = Schema.Struct({ - "type": Schema.Literal("function"), - "function": Schema.Struct({ - "name": Schema.String.check(Schema.isMaxLength(64)), - "description": Schema.optionalKey(Schema.String), - "parameters": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), - "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) - }), - "cache_control": Schema.optionalKey(ChatMessageContentItemCacheControl) -}) -export type ToolChoiceOption = "none" | "auto" | "required" | NamedToolChoice -export const ToolChoiceOption = Schema.Union([ - Schema.Literal("none"), - Schema.Literal("auto"), - Schema.Literal("required"), - NamedToolChoice -]) -export type ResponseOutputText = { - readonly "type": "output_text" - readonly "text": string - readonly "annotations"?: ReadonlyArray - readonly "logprobs"?: ReadonlyArray< + "Fraction of this classification's sampled token volume attributed to this model (0–1). Sums to ≤1 across the returned models (only top-N are included and unattributed requests are excluded).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "tag_usage_share": Schema.Number.annotate({ + "description": + "Fraction of this classification's sampled requests attributed to this model (0–1). Sums to ≤1 across the returned models (only top-N are included and unattributed requests are excluded).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ "identifier": "TaskClassificationModel" }) +export type TextDeltaEvent = { + readonly "content_index": number + readonly "delta": string + readonly "item_id": string + readonly "logprobs": ReadonlyArray< { - readonly "token": string - readonly "bytes": ReadonlyArray + readonly "bytes"?: ReadonlyArray readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } + readonly "token": string + readonly "top_logprobs"?: ReadonlyArray< + { readonly "bytes"?: ReadonlyArray; readonly "logprob"?: number; readonly "token"?: string } > } > -} -export const ResponseOutputText = Schema.Struct({ - "type": Schema.Literal("output_text"), - "text": Schema.String, - "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))) -}) -export type OpenResponsesOutputTextAnnotationAddedEvent = { - readonly "type": "response.output_text.annotation.added" readonly "output_index": number - readonly "item_id": string - readonly "content_index": number readonly "sequence_number": number - readonly "annotation_index": number - readonly "annotation": OpenAIResponsesAnnotation -} -export const OpenResponsesOutputTextAnnotationAddedEvent = Schema.Struct({ - "type": Schema.Literal("response.output_text.annotation.added"), - "output_index": Schema.Number.check(Schema.isFinite()), - "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "sequence_number": Schema.Number.check(Schema.isFinite()), - "annotation_index": Schema.Number.check(Schema.isFinite()), - "annotation": OpenAIResponsesAnnotation -}).annotate({ "description": "Event emitted when a text annotation is added to output" }) -export type ResponseTextConfig = { - readonly "format"?: ResponseFormatTextConfig - readonly "verbosity"?: "high" | "low" | "medium" -} -export const ResponseTextConfig = Schema.Struct({ - "format": Schema.optionalKey(ResponseFormatTextConfig), - "verbosity": Schema.optionalKey(Schema.Literals(["high", "low", "medium"])) -}).annotate({ "description": "Text output configuration including format and verbosity" }) -export type OpenResponsesResponseText = { - readonly "format"?: ResponseFormatTextConfig - readonly "verbosity"?: "high" | "low" | "medium" -} -export const OpenResponsesResponseText = Schema.Struct({ - "format": Schema.optionalKey(ResponseFormatTextConfig), - "verbosity": Schema.optionalKey(Schema.Literals(["high", "low", "medium"])) -}).annotate({ "description": "Text output configuration including format and verbosity" }) -export type OpenResponsesTextDeltaEvent = { readonly "type": "response.output_text.delta" - readonly "logprobs": ReadonlyArray - readonly "output_index": number - readonly "item_id": string - readonly "content_index": number - readonly "delta": string - readonly "sequence_number": number } -export const OpenResponsesTextDeltaEvent = Schema.Struct({ - "type": Schema.Literal("response.output_text.delta"), - "logprobs": Schema.Array(OpenResponsesLogProbs), - "output_index": Schema.Number.check(Schema.isFinite()), - "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), +export const TextDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "delta": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a text delta is streamed" }) -export type OpenResponsesTextDoneEvent = { - readonly "type": "response.output_text.done" - readonly "output_index": number - readonly "item_id": string + "item_id": Schema.String, + "logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) + ), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.optionalKey(Schema.Array( + Schema.Struct({ + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) + ), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "token": Schema.optionalKey(Schema.String) + }).annotate({ "description": "Alternative token with its log probability" }) + )) + }).annotate({ "description": "Log probability information for a token" }) + ), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_text.delta") +}).annotate({ "description": "Event emitted when a text delta is streamed", "identifier": "TextDeltaEvent" }) +export type TextDoneEvent = { readonly "content_index": number - readonly "text": string + readonly "item_id": string + readonly "logprobs": ReadonlyArray< + { + readonly "bytes"?: ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs"?: ReadonlyArray< + { readonly "bytes"?: ReadonlyArray; readonly "logprob"?: number; readonly "token"?: string } + > + } + > + readonly "output_index": number readonly "sequence_number": number - readonly "logprobs": ReadonlyArray + readonly "text": string + readonly "type": "response.output_text.done" } -export const OpenResponsesTextDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.output_text.done"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const TextDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), + "logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) + ), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.optionalKey(Schema.Array( + Schema.Struct({ + "bytes": Schema.optionalKey( + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) + ), + "logprob": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "token": Schema.optionalKey(Schema.String) + }).annotate({ "description": "Alternative token with its log probability" }) + )) + }).annotate({ "description": "Log probability information for a token" }) + ), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), "text": Schema.String, - "sequence_number": Schema.Number.check(Schema.isFinite()), - "logprobs": Schema.Array(OpenResponsesLogProbs) -}).annotate({ "description": "Event emitted when text streaming is complete" }) -export type ProviderSortUnion = ProviderSort | ProviderSortConfig -export const ProviderSortUnion = Schema.Union([ProviderSort, ProviderSortConfig]) -export type ProviderPreferences = { - readonly "allow_fallbacks"?: boolean - readonly "require_parameters"?: boolean - readonly "data_collection"?: DataCollection - readonly "zdr"?: boolean - readonly "enforce_distillable_text"?: boolean - readonly "order"?: ReadonlyArray - readonly "only"?: ReadonlyArray - readonly "ignore"?: ReadonlyArray - readonly "quantizations"?: ReadonlyArray - readonly "sort"?: "price" | "price" | "throughput" | "throughput" | "latency" | "latency" - readonly "max_price"?: { - readonly "prompt"?: BigNumberUnion - readonly "completion"?: string - readonly "image"?: string - readonly "audio"?: string - readonly "request"?: string - } - readonly "preferred_min_throughput"?: PreferredMinThroughput - readonly "preferred_max_latency"?: PreferredMaxLatency + "type": Schema.Literal("response.output_text.done") +}).annotate({ "description": "Event emitted when text streaming is complete", "identifier": "TextDoneEvent" }) +export type ToolCallStatus = "in_progress" | "completed" | "incomplete" +export const ToolCallStatus = Schema.Literals(["in_progress", "completed", "incomplete"]).annotate({ + "identifier": "ToolCallStatus" +}) +export type ToolChoiceAllowed = { + readonly "mode": "auto" | "required" + readonly "tools": ReadonlyArray<{}> + readonly "type": "allowed_tools" } -export const ProviderPreferences = Schema.Struct({ - "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ - "description": - "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" - })), - "require_parameters": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "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." +export const ToolChoiceAllowed = Schema.Struct({ + "mode": Schema.Literals(["auto", "required"]), + "tools": Schema.Array(Schema.Struct({})), + "type": Schema.Literal("allowed_tools") +}).annotate({ + "description": "Constrains the model to a pre-defined set of allowed tools", + "identifier": "ToolChoiceAllowed" +}) +export type TooManyRequestsResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const TooManyRequestsResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for TooManyRequestsResponse", + "identifier": "TooManyRequestsResponseErrorData" +}) +export type TopProviderInfo = { + readonly "context_length"?: number | null + readonly "is_moderated": boolean + readonly "max_completion_tokens"?: number | null +} +export const TopProviderInfo = Schema.Struct({ + "context_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Context length from the top provider" }) ), - "data_collection": Schema.optionalKey(DataCollection), - "zdr": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." + "is_moderated": Schema.Boolean.annotate({ "description": "Whether the top provider moderates content" }), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Maximum completion tokens from the top provider" }) + ) +}).annotate({ "description": "Information about the top provider for this model", "identifier": "TopProviderInfo" }) +export type TraceConfig = { + readonly "generation_name"?: string + readonly "parent_span_id"?: string + readonly "span_name"?: string + readonly "trace_id"?: string + readonly "trace_name"?: string +} +export const TraceConfig = Schema.Struct({ + "generation_name": Schema.optionalKey(Schema.String), + "parent_span_id": Schema.optionalKey(Schema.String), + "span_name": Schema.optionalKey(Schema.String), + "trace_id": Schema.optionalKey(Schema.String), + "trace_name": Schema.optionalKey(Schema.String) +}).annotate({ + "description": + "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations.", + "identifier": "TraceConfig" +}) +export type Truncation = "auto" | "disabled" | null +export const Truncation = Schema.Union([Schema.Literal("auto"), Schema.Literal("disabled"), Schema.Null]).annotate({ + "identifier": "Truncation" +}) +export type UnauthorizedResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const UnauthorizedResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ "description": "Error data for UnauthorizedResponse", "identifier": "UnauthorizedResponseErrorData" }) +export type UnifiedBenchmarkPricing = { + readonly "completion": string + readonly "prompt": string + readonly [x: string]: Schema.Json +} | null +export const UnifiedBenchmarkPricing = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "completion": Schema.String.annotate({ "description": "Cost per output token (USD, decimal string)." }), + "prompt": Schema.String.annotate({ "description": "Cost per input token (USD, decimal string)." }) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] ), - "enforce_distillable_text": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." + Schema.Null +]).annotate({ + "description": "OpenRouter pricing per token for this model. Null if pricing is unavailable.", + "identifier": "UnifiedBenchmarkPricing" +}) +export type UnifiedBenchmarksMeta = { + readonly "as_of": string + readonly "citation": string | null + readonly "model_count": number + readonly "source": "artificial-analysis" | "design-arena" | null + readonly "source_url": string | null + readonly "task_type": string | null + readonly "version": "v1" +} +export const UnifiedBenchmarksMeta = Schema.Struct({ + "as_of": Schema.String.annotate({ "description": "ISO-8601 timestamp of when this data was last updated." }), + "citation": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Required attribution when republishing this data, or null when results span multiple sources (attribute each item individually by its `source` discriminator)." + }), + "model_count": Schema.Number.annotate({ "description": "Number of unique models in the response." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "source": Schema.Union([Schema.Literal("artificial-analysis"), Schema.Literal("design-arena"), Schema.Null]).annotate( + { "description": "The source filter applied, or null when all sources are returned." } + ), + "source_url": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "URL of the upstream data source, or null when results span multiple sources." + }), + "task_type": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The task_type filter applied, or null if showing all." + }), + "version": Schema.Literal("v1").annotate({ "description": "Dataset version." }) +}).annotate({ "identifier": "UnifiedBenchmarksMeta" }) +export type UnprocessableEntityResponseErrorData = { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly [x: string]: Schema.Json } | null +} +export const UnprocessableEntityResponseErrorData = Schema.Struct({ + "code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "message": Schema.String, + "metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ) +}).annotate({ + "description": "Error data for UnprocessableEntityResponse", + "identifier": "UnprocessableEntityResponseErrorData" +}) +export type UpdateBYOKKeyRequest = { + readonly "allowed_models"?: ReadonlyArray | null + readonly "allowed_user_ids"?: ReadonlyArray | null + readonly "disabled"?: boolean + readonly "is_fallback"?: boolean + readonly "key"?: string + readonly "name"?: string | null +} +export const UpdateBYOKKeyRequest = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of model slugs this credential may be used for. `null` means no restriction." }) ), - "order": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "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." + "allowed_user_ids": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of user IDs that may use this credential. `null` means no restriction." }) ), - "only": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "disabled": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Whether this credential is disabled." })), + "is_fallback": Schema.optionalKey( + Schema.Boolean.annotate({ "description": - "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + "Whether this credential is treated as a fallback — used only after non-fallback keys for the same provider have been tried." }) ), - "ignore": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ + "key": Schema.optionalKey( + Schema.String.annotate({ "description": - "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + "A new raw provider API key to rotate the credential in-place. The previous key material is overwritten and the masked label is regenerated. Encrypted at rest and never returned in API responses." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(255).annotate({ "expected": "a value with a length of at most 255" })), + Schema.Null + ]).annotate({ "description": "Optional human-readable name for the credential." }) + ) +}).annotate({ "identifier": "UpdateBYOKKeyRequest" }) +export type UpdateWorkspaceRequest = { + readonly "default_image_model"?: string | null + readonly "default_provider_sort"?: string | null + readonly "default_text_model"?: string | null + readonly "description"?: string | null + readonly "io_logging_api_key_ids"?: ReadonlyArray | null + readonly "io_logging_sampling_rate"?: number + readonly "is_data_discount_logging_enabled"?: boolean + readonly "is_observability_broadcast_enabled"?: boolean + readonly "is_observability_io_logging_enabled"?: boolean + readonly "name"?: string + readonly "slug"?: string +} +export const UpdateWorkspaceRequest = Schema.Struct({ + "default_image_model": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Default image model for this workspace" }) + ), + "default_provider_sort": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Default provider sort preference (price, throughput, latency, exacto)" }) ), - "quantizations": Schema.optionalKey( - Schema.Array(Quantization).annotate({ "description": "A list of quantization levels to filter the provider by." }) + "default_text_model": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Default text model for this workspace" }) ), - "sort": Schema.optionalKey( + "description": Schema.optionalKey( Schema.Union([ - Schema.Union([Schema.Literal("price"), Schema.Literal("price")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }), - Schema.Union([Schema.Literal("throughput"), Schema.Literal("throughput")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }), - Schema.Union([Schema.Literal("latency"), Schema.Literal("latency")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }) - ]) + Schema.String.check(Schema.isMaxLength(500).annotate({ "expected": "a value with a length of at most 500" })), + Schema.Null + ]).annotate({ "description": "New description for the workspace" }) ), - "max_price": Schema.optionalKey( - Schema.Struct({ - "prompt": Schema.optionalKey(BigNumberUnion), - "completion": Schema.optionalKey( - Schema.String.annotate({ "description": "Price per million completion tokens" }) - ), - "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), - "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), - "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) - }).annotate({ - "description": - "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." - }) - ), - "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), - "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) -}).annotate({ "description": "Provider routing preferences for the request." }) -export type AnthropicMessagesRequest = { - readonly "model": string - readonly "max_tokens": number - readonly "messages": ReadonlyArray - readonly "system"?: - | string - | ReadonlyArray< - { - readonly "type": "text" - readonly "text": string - readonly "citations"?: ReadonlyArray< - { - readonly "type": "char_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_char_index": number - readonly "end_char_index": number - } | { - readonly "type": "page_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_page_number": number - readonly "end_page_number": number - } | { - readonly "type": "content_block_location" - readonly "cited_text": string - readonly "document_index": number - readonly "document_title": string - readonly "start_block_index": number - readonly "end_block_index": number - } | { - readonly "type": "web_search_result_location" - readonly "cited_text": string - readonly "encrypted_index": string - readonly "title": string - readonly "url": string - } | { - readonly "type": "search_result_location" - readonly "cited_text": string - readonly "search_result_index": number - readonly "source": string - readonly "title": string - readonly "start_block_index": number - readonly "end_block_index": number - } - > - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > - readonly "metadata"?: { readonly "user_id"?: string } - readonly "stop_sequences"?: ReadonlyArray - readonly "stream"?: boolean - readonly "temperature"?: number - readonly "top_p"?: number - readonly "top_k"?: number - readonly "tools"?: ReadonlyArray< - { - readonly "name": string - readonly "description"?: string - readonly "input_schema": { - readonly "type": "object" - readonly "properties"?: unknown - readonly "required"?: ReadonlyArray - } - readonly "type"?: "custom" - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } | { - readonly "type": "bash_20250124" - readonly "name": "bash" - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } | { - readonly "type": "text_editor_20250124" - readonly "name": "str_replace_editor" - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } | { - readonly "type": "web_search_20250305" - readonly "name": "web_search" - readonly "allowed_domains"?: ReadonlyArray - readonly "blocked_domains"?: ReadonlyArray - readonly "max_uses"?: number - readonly "user_location"?: { - readonly "type": "approximate" - readonly "city"?: string - readonly "country"?: string - readonly "region"?: string - readonly "timezone"?: string - } - readonly "cache_control"?: { readonly "type": "ephemeral"; readonly "ttl"?: "5m" | "1h" } - } - > - readonly "tool_choice"?: - | { readonly "type": "auto"; readonly "disable_parallel_tool_use"?: boolean } - | { readonly "type": "any"; readonly "disable_parallel_tool_use"?: boolean } - | { readonly "type": "none" } - | { readonly "type": "tool"; readonly "name": string; readonly "disable_parallel_tool_use"?: boolean } - readonly "thinking"?: { readonly "type": "enabled"; readonly "budget_tokens": number } | { - readonly "type": "disabled" - } | { readonly "type": "adaptive" } - readonly "service_tier"?: "auto" | "standard_only" - readonly "provider"?: { - readonly "allow_fallbacks"?: boolean - readonly "require_parameters"?: boolean - readonly "data_collection"?: DataCollection - readonly "zdr"?: boolean - readonly "enforce_distillable_text"?: boolean - readonly "order"?: ReadonlyArray - readonly "only"?: ReadonlyArray - readonly "ignore"?: ReadonlyArray - readonly "quantizations"?: ReadonlyArray - readonly "sort"?: "price" | "price" | "throughput" | "throughput" | "latency" | "latency" - readonly "max_price"?: { - readonly "prompt"?: BigNumberUnion - readonly "completion"?: string - readonly "image"?: string - readonly "audio"?: string - readonly "request"?: string - } - readonly "preferred_min_throughput"?: PreferredMinThroughput - readonly "preferred_max_latency"?: PreferredMaxLatency - } - readonly "plugins"?: ReadonlyArray< - | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } - | { readonly "id": "moderation" } - | { - readonly "id": "web" - readonly "enabled"?: boolean - readonly "max_results"?: number - readonly "search_prompt"?: string - readonly "engine"?: WebSearchEngine - } - | { readonly "id": "file-parser"; readonly "enabled"?: boolean; readonly "pdf"?: PDFParserOptions } - | { readonly "id": "response-healing"; readonly "enabled"?: boolean } - > - readonly "route"?: "fallback" | "sort" - readonly "user"?: string - readonly "session_id"?: string - readonly "trace"?: { - readonly "trace_id"?: string - readonly "trace_name"?: string - readonly "span_name"?: string - readonly "generation_name"?: string - readonly "parent_span_id"?: string - } - readonly "models"?: ReadonlyArray - readonly "output_config"?: AnthropicOutputConfig -} -export const AnthropicMessagesRequest = Schema.Struct({ - "model": Schema.String, - "max_tokens": Schema.Number.check(Schema.isFinite()), - "messages": Schema.Array(OpenRouterAnthropicMessageParam), - "system": Schema.optionalKey(Schema.Union([ - Schema.String, - Schema.Array(Schema.Struct({ - "type": Schema.Literal("text"), - "text": Schema.String, - "citations": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("char_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_char_index": Schema.Number.check(Schema.isFinite()), - "end_char_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("page_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_page_number": Schema.Number.check(Schema.isFinite()), - "end_page_number": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("content_block_location"), - "cited_text": Schema.String, - "document_index": Schema.Number.check(Schema.isFinite()), - "document_title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_result_location"), - "cited_text": Schema.String, - "encrypted_index": Schema.String, - "title": Schema.String, - "url": Schema.String - }), - Schema.Struct({ - "type": Schema.Literal("search_result_location"), - "cited_text": Schema.String, - "search_result_index": Schema.Number.check(Schema.isFinite()), - "source": Schema.String, - "title": Schema.String, - "start_block_index": Schema.Number.check(Schema.isFinite()), - "end_block_index": Schema.Number.check(Schema.isFinite()) - }) - ], { mode: "oneOf" }))), - "cache_control": Schema.optionalKey( - Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) - ) - })) - ])), - "metadata": Schema.optionalKey(Schema.Struct({ "user_id": Schema.optionalKey(Schema.String) })), - "stop_sequences": Schema.optionalKey(Schema.Array(Schema.String)), - "stream": Schema.optionalKey(Schema.Boolean), - "temperature": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "top_p": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "top_k": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "tools": Schema.optionalKey(Schema.Array(Schema.Union([ - Schema.Struct({ - "name": Schema.String, - "description": Schema.optionalKey(Schema.String), - "input_schema": Schema.Struct({ - "type": Schema.Literal("object"), - "properties": Schema.optionalKey(Schema.Unknown), - "required": Schema.optionalKey(Schema.Array(Schema.String)) - }), - "type": Schema.optionalKey(Schema.Literal("custom")), - "cache_control": Schema.optionalKey( - Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("bash_20250124"), - "name": Schema.Literal("bash"), - "cache_control": Schema.optionalKey( - Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("text_editor_20250124"), - "name": Schema.Literal("str_replace_editor"), - "cache_control": Schema.optionalKey( - Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_20250305"), - "name": Schema.Literal("web_search"), - "allowed_domains": Schema.optionalKey(Schema.Array(Schema.String)), - "blocked_domains": Schema.optionalKey(Schema.Array(Schema.String)), - "max_uses": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "user_location": Schema.optionalKey( - Schema.Struct({ - "type": Schema.Literal("approximate"), - "city": Schema.optionalKey(Schema.String), - "country": Schema.optionalKey(Schema.String), - "region": Schema.optionalKey(Schema.String), - "timezone": Schema.optionalKey(Schema.String) - }) - ), - "cache_control": Schema.optionalKey( - Schema.Struct({ "type": Schema.Literal("ephemeral"), "ttl": Schema.optionalKey(Schema.Literals(["5m", "1h"])) }) - ) - }) - ], { mode: "oneOf" }))), - "tool_choice": Schema.optionalKey( + "io_logging_api_key_ids": Schema.optionalKey( Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("auto"), - "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) - }), - Schema.Struct({ "type": Schema.Literal("any"), "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) }), - Schema.Struct({ "type": Schema.Literal("none") }), - Schema.Struct({ - "type": Schema.Literal("tool"), - "name": Schema.String, - "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean) - }) - ], { mode: "oneOf" }) + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + Schema.Null + ]).annotate({ "description": "Optional array of API key IDs to filter I/O logging" }) ), - "thinking": Schema.optionalKey( - Schema.Union([ - Schema.Struct({ "type": Schema.Literal("enabled"), "budget_tokens": Schema.Number.check(Schema.isFinite()) }), - Schema.Struct({ "type": Schema.Literal("disabled") }), - Schema.Struct({ "type": Schema.Literal("adaptive") }) - ], { mode: "oneOf" }) + "io_logging_sampling_rate": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sampling rate for I/O logging (0.0001-1)", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) ), - "service_tier": Schema.optionalKey(Schema.Literals(["auto", "standard_only"])), - "provider": Schema.optionalKey( - Schema.Struct({ - "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ - "description": - "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" - })), - "require_parameters": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "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." - }) - ), - "data_collection": Schema.optionalKey(DataCollection), - "zdr": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." - }) - ), - "enforce_distillable_text": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." - }) - ), - "order": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "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." - }) - ), - "only": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." - }) - ), - "ignore": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." - }) - ), - "quantizations": Schema.optionalKey( - Schema.Array(Quantization).annotate({ - "description": "A list of quantization levels to filter the provider by." - }) - ), - "sort": Schema.optionalKey( - Schema.Union([ - Schema.Union([Schema.Literal("price"), Schema.Literal("price")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }), - Schema.Union([Schema.Literal("throughput"), Schema.Literal("throughput")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }), - Schema.Union([Schema.Literal("latency"), Schema.Literal("latency")]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }) - ]) - ), - "max_price": Schema.optionalKey( - Schema.Struct({ - "prompt": Schema.optionalKey(BigNumberUnion), - "completion": Schema.optionalKey( - Schema.String.annotate({ "description": "Price per million completion tokens" }) - ), - "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), - "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), - "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) - }).annotate({ - "description": - "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." - }) - ), - "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), - "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) - }).annotate({ - "description": "When multiple model providers are available, optionally indicate your routing preference." - }) + "is_data_discount_logging_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether data discount logging is enabled" }) ), - "plugins": Schema.optionalKey( - Schema.Array(Schema.Union([ - Schema.Struct({ - "id": Schema.Literal("auto-router"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the auto-router plugin for this request. Defaults to true." - }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ - "description": - "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." - }) - ) - }), - Schema.Struct({ "id": Schema.Literal("moderation") }), - Schema.Struct({ - "id": Schema.Literal("web"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the web-search plugin for this request. Defaults to true." - }) - ), - "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "search_prompt": Schema.optionalKey(Schema.String), - "engine": Schema.optionalKey(WebSearchEngine) - }), - Schema.Struct({ - "id": Schema.Literal("file-parser"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the file-parser plugin for this request. Defaults to true." - }) - ), - "pdf": Schema.optionalKey(PDFParserOptions) - }), - Schema.Struct({ - "id": Schema.Literal("response-healing"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the response-healing plugin for this request. Defaults to true." - }) - ) - }) - ], { mode: "oneOf" })).annotate({ - "description": "Plugins you want to enable for this request, including their settings." - }) + "is_observability_broadcast_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether broadcast is enabled" }) ), - "route": Schema.optionalKey( - Schema.Literals(["fallback", "sort"]).annotate({ - "description": - "**DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: \"fallback\" (maps to \"model\"), \"sort\" (maps to \"none\")." - }) + "is_observability_io_logging_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether private logging is enabled" }) ), - "user": Schema.optionalKey( - Schema.String.annotate({ - "description": - "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." - }).check(Schema.isMaxLength(128)) + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "New name for the workspace" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ).check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })) ), - "session_id": Schema.optionalKey( + "slug": Schema.optionalKey( Schema.String.annotate({ "description": - "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." - }).check(Schema.isMaxLength(128)) - ), - "trace": Schema.optionalKey( - Schema.Struct({ - "trace_id": Schema.optionalKey(Schema.String), - "trace_name": Schema.optionalKey(Schema.String), - "span_name": Schema.optionalKey(Schema.String), - "generation_name": Schema.optionalKey(Schema.String), - "parent_span_id": Schema.optionalKey(Schema.String) - }).annotate({ - "description": - "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." - }) + "New URL-friendly slug (lowercase alphanumeric segments separated by single hyphens, no leading/trailing hyphens)" + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(50).annotate({ "expected": "a value with a length of at most 50" }) + ).check( + Schema.isPattern(new RegExp("^[a-z0-9]+(?:-[a-z0-9]+)*$")).annotate({ + "expected": "a string matching the RegExp ^[a-z0-9]+(?:-[a-z0-9]+)*$" + }) + ) + ) +}).annotate({ "identifier": "UpdateWorkspaceRequest" }) +export type UpsertWorkspaceBudgetRequest = { readonly "limit_usd": number } +export const UpsertWorkspaceBudgetRequest = Schema.Struct({ + "limit_usd": Schema.Number.annotate({ + "description": "Spending limit in USD. Must be greater than 0.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ "identifier": "UpsertWorkspaceBudgetRequest" }) +export type URLCitation = { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": "url_citation" + readonly "url": string +} +export const URLCitation = Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Literal("url_citation"), + "url": Schema.String +}).annotate({ "identifier": "URLCitation" }) +export type VideoGenerationUsage = { readonly "cost"?: number | null; readonly "is_byok"?: boolean } +export const VideoGenerationUsage = Schema.Struct({ + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "The cost of the video generation in USD.", "format": "double" }) ), - "models": Schema.optionalKey(Schema.Array(Schema.String)), - "output_config": Schema.optionalKey(AnthropicOutputConfig) -}).annotate({ "description": "Request schema for Anthropic Messages API endpoint" }) -export type Model = { - readonly "id": string + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether the request was made using a Bring Your Own Key configuration." }) + ) +}).annotate({ + "description": "Usage and cost information for the video generation. Available once the job has completed.", + "identifier": "VideoGenerationUsage" +}) +export type VideoModel = { + readonly "allowed_passthrough_parameters": ReadonlyArray readonly "canonical_slug": string - readonly "hugging_face_id"?: string - readonly "name": string readonly "created": number readonly "description"?: string - readonly "pricing": PublicPricing - readonly "context_length": number - readonly "architecture": ModelArchitecture - readonly "top_provider": TopProviderInfo - readonly "per_request_limits": PerRequestLimits - readonly "supported_parameters": ReadonlyArray - readonly "default_parameters": DefaultParameters - readonly "expiration_date"?: string + readonly "generate_audio": boolean | null + readonly "hugging_face_id"?: string | null + readonly "id": string + readonly "name": string + readonly "pricing_skus"?: { readonly [x: string]: string } | null + readonly "seed": boolean | null + readonly "supported_aspect_ratios": + | ReadonlyArray<"16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "9:21"> + | null + readonly "supported_durations": ReadonlyArray | null + readonly "supported_frame_images": ReadonlyArray<"first_frame" | "last_frame"> | null + readonly "supported_resolutions": ReadonlyArray<"480p" | "720p" | "1080p" | "1K" | "2K" | "4K"> | null + readonly "supported_sizes": + | ReadonlyArray< + | "480x480" + | "480x640" + | "480x720" + | "480x854" + | "480x1120" + | "640x480" + | "720x480" + | "720x720" + | "720x960" + | "720x1080" + | "720x1280" + | "720x1680" + | "854x480" + | "960x720" + | "1080x720" + | "1080x1080" + | "1080x1440" + | "1080x1620" + | "1080x1920" + | "1080x2520" + | "1120x480" + | "1280x720" + | "1440x1080" + | "1620x1080" + | "1680x720" + | "1920x1080" + | "2160x2160" + | "2160x2880" + | "2160x3240" + | "2160x3840" + | "2160x5040" + | "2520x1080" + | "2880x2160" + | "3240x2160" + | "3840x2160" + | "5040x2160" + > + | null } -export const Model = Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), +export const VideoModel = Schema.Struct({ + "allowed_passthrough_parameters": Schema.Array(Schema.String).annotate({ + "description": "List of parameters that are allowed to be passed through to the provider" + }), "canonical_slug": Schema.String.annotate({ "description": "Canonical slug for the model" }), - "hugging_face_id": Schema.optionalKey( - Schema.String.annotate({ "description": "Hugging Face model identifier, if applicable" }) - ), - "name": Schema.String.annotate({ "description": "Display name of the model" }), "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( - Schema.isFinite() + Schema.isInt().annotate({ "expected": "an integer" }) ), "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the model" })), - "pricing": PublicPricing, - "context_length": Schema.Number.annotate({ "description": "Maximum context length in tokens" }).check( - Schema.isFinite() - ), - "architecture": ModelArchitecture, - "top_provider": TopProviderInfo, - "per_request_limits": PerRequestLimits, - "supported_parameters": Schema.Array(Parameter).annotate({ - "description": "List of supported parameters for this model" + "generate_audio": Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Whether the model supports generating audio alongside video" }), - "default_parameters": DefaultParameters, - "expiration_date": Schema.optionalKey( - Schema.String.annotate({ - "description": - "The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration." + "hugging_face_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Hugging Face model identifier, if applicable" }) - ) -}).annotate({ "description": "Information about an AI model available on OpenRouter" }) -export type ListEndpointsResponse = { - readonly "id": string - readonly "name": string - readonly "created": number - readonly "description": string - readonly "architecture": { - readonly "tokenizer": - | "Router" - | "Media" - | "Other" - | "GPT" - | "Claude" - | "Gemini" - | "Grok" - | "Cohere" - | "Nova" - | "Qwen" - | "Yi" - | "DeepSeek" - | "Mistral" - | "Llama2" - | "Llama3" - | "Llama4" - | "PaLM" - | "RWKV" - | "Qwen3" - readonly "instruct_type": - | "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" - readonly "modality": string - readonly "input_modalities": ReadonlyArray<"text" | "image" | "file" | "audio" | "video"> - readonly "output_modalities": ReadonlyArray<"text" | "image" | "embeddings" | "audio"> - } - readonly "endpoints": ReadonlyArray -} -export const ListEndpointsResponse = Schema.Struct({ + ), "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), "name": Schema.String.annotate({ "description": "Display name of the model" }), - "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( - Schema.isFinite() + "pricing_skus": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.String), Schema.Null]).annotate({ + "description": "Pricing SKUs with provider prefix stripped, values as strings" + }) ), - "description": Schema.String.annotate({ "description": "Description of the model" }), - "architecture": Schema.Struct({ - "tokenizer": Schema.Union([ - Schema.Literal("Router").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Media").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Other").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("GPT").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Claude").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Gemini").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Grok").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Cohere").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Nova").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Qwen").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Yi").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("DeepSeek").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Mistral").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Llama2").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Llama3").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Llama4").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("PaLM").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("RWKV").annotate({ "description": "Tokenizer type used by the model" }), - Schema.Literal("Qwen3").annotate({ "description": "Tokenizer type used by the model" }) - ]).annotate({ "description": "Tokenizer type used by the model" }), - "instruct_type": Schema.Union([ - Schema.Literal("none").annotate({ "description": "Instruction format type" }), - Schema.Literal("airoboros").annotate({ "description": "Instruction format type" }), - Schema.Literal("alpaca").annotate({ "description": "Instruction format type" }), - Schema.Literal("alpaca-modif").annotate({ "description": "Instruction format type" }), - Schema.Literal("chatml").annotate({ "description": "Instruction format type" }), - Schema.Literal("claude").annotate({ "description": "Instruction format type" }), - Schema.Literal("code-llama").annotate({ "description": "Instruction format type" }), - Schema.Literal("gemma").annotate({ "description": "Instruction format type" }), - Schema.Literal("llama2").annotate({ "description": "Instruction format type" }), - Schema.Literal("llama3").annotate({ "description": "Instruction format type" }), - Schema.Literal("mistral").annotate({ "description": "Instruction format type" }), - Schema.Literal("nemotron").annotate({ "description": "Instruction format type" }), - Schema.Literal("neural").annotate({ "description": "Instruction format type" }), - Schema.Literal("openchat").annotate({ "description": "Instruction format type" }), - Schema.Literal("phi3").annotate({ "description": "Instruction format type" }), - Schema.Literal("rwkv").annotate({ "description": "Instruction format type" }), - Schema.Literal("vicuna").annotate({ "description": "Instruction format type" }), - Schema.Literal("zephyr").annotate({ "description": "Instruction format type" }), - Schema.Literal("deepseek-r1").annotate({ "description": "Instruction format type" }), - Schema.Literal("deepseek-v3.1").annotate({ "description": "Instruction format type" }), - Schema.Literal("qwq").annotate({ "description": "Instruction format type" }), - Schema.Literal("qwen3").annotate({ "description": "Instruction format type" }) - ]).annotate({ "description": "Instruction format type" }), - "modality": Schema.String.annotate({ "description": "Primary modality of the model" }), - "input_modalities": Schema.Array( - Schema.Union([ - Schema.Literal("text"), - Schema.Literal("image"), - Schema.Literal("file"), - Schema.Literal("audio"), - Schema.Literal("video") - ]) - ).annotate({ "description": "Supported input modalities" }), - "output_modalities": Schema.Array( - Schema.Union([ - Schema.Literal("text"), - Schema.Literal("image"), - Schema.Literal("embeddings"), - Schema.Literal("audio") + "seed": Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Whether the model supports deterministic generation via seed parameter" + }), + "supported_aspect_ratios": Schema.Union([ + Schema.Array(Schema.Literals(["16:9", "9:16", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9", "9:21"])), + Schema.Null + ]).annotate({ "description": "Supported output aspect ratios" }), + "supported_durations": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + Schema.Null + ]).annotate({ "description": "Supported video durations in seconds" }), + "supported_frame_images": Schema.Union([Schema.Array(Schema.Literals(["first_frame", "last_frame"])), Schema.Null]) + .annotate({ "description": "Supported frame image types (e.g. first_frame, last_frame)" }), + "supported_resolutions": Schema.Union([ + Schema.Array(Schema.Literals(["480p", "720p", "1080p", "1K", "2K", "4K"])), + Schema.Null + ]).annotate({ "description": "Supported output resolutions" }), + "supported_sizes": Schema.Union([ + Schema.Array( + Schema.Literals([ + "480x480", + "480x640", + "480x720", + "480x854", + "480x1120", + "640x480", + "720x480", + "720x720", + "720x960", + "720x1080", + "720x1280", + "720x1680", + "854x480", + "960x720", + "1080x720", + "1080x1080", + "1080x1440", + "1080x1620", + "1080x1920", + "1080x2520", + "1120x480", + "1280x720", + "1440x1080", + "1620x1080", + "1680x720", + "1920x1080", + "2160x2160", + "2160x2880", + "2160x3240", + "2160x3840", + "2160x5040", + "2520x1080", + "2880x2160", + "3240x2160", + "3840x2160", + "5040x2160" ]) - ).annotate({ "description": "Supported output modalities" }) - }).annotate({ "description": "Model architecture information" }), - "endpoints": Schema.Array(PublicEndpoint).annotate({ "description": "List of available endpoints for this model" }) -}).annotate({ "description": "List of available endpoints for a model" }) -export type ChatStreamingMessageChunk = { - readonly "role"?: "assistant" - readonly "content"?: string | null - readonly "reasoning"?: string | null - readonly "refusal"?: string | null - readonly "tool_calls"?: ReadonlyArray - readonly "reasoning_details"?: ReadonlyArray<__schema20> - readonly "images"?: - | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> - | null - readonly "annotations"?: - | ReadonlyArray< - { - readonly "type": "url_citation" - readonly "url_citation": { - readonly "url": string - readonly "title"?: string - readonly "start_index"?: number - readonly "end_index"?: number - readonly "content"?: string - } - } | { - readonly "type": "file_annotation" - readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } - } | { - readonly "type": "file" - readonly "file": { - readonly "hash": string - readonly "name": string - readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> - } - } - > - | null -} -export const ChatStreamingMessageChunk = Schema.Struct({ - "role": Schema.optionalKey(Schema.Literal("assistant")), - "content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "reasoning": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "refusal": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "tool_calls": Schema.optionalKey(Schema.Array(ChatStreamingMessageToolCall)), - "reasoning_details": Schema.optionalKey(Schema.Array(__schema20)), - "images": Schema.optionalKey( - Schema.Union([ - Schema.Array( - Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) - ), - Schema.Null - ]) - ), - "annotations": Schema.optionalKey(Schema.Union([ - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("url_citation"), - "url_citation": Schema.Struct({ - "url": Schema.String, - "title": Schema.optionalKey(Schema.String), - "start_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "end_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "content": Schema.optionalKey(Schema.String) - }) - }), - Schema.Struct({ - "type": Schema.Literal("file_annotation"), - "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) - }), - Schema.Struct({ - "type": Schema.Literal("file"), - "file": Schema.Struct({ - "hash": Schema.String, - "name": Schema.String, - "content": Schema.optionalKey( - Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) - ) - }) - }) - ], { mode: "oneOf" })), + ), Schema.Null - ])) -}) -export type ChatMessageContentItem = - | ChatMessageContentItemText - | ChatMessageContentItemImage - | ChatMessageContentItemAudio - | ChatMessageContentItemVideo -export const ChatMessageContentItem = Schema.Union([ - ChatMessageContentItemText, - ChatMessageContentItemImage, - ChatMessageContentItemAudio, - ChatMessageContentItemVideo -], { mode: "oneOf" }) -export type SystemMessage = { - readonly "role": "system" - readonly "content": string | ReadonlyArray - readonly "name"?: string -} -export const SystemMessage = Schema.Struct({ - "role": Schema.Literal("system"), - "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItemText)]), - "name": Schema.optionalKey(Schema.String) -}) -export type DeveloperMessage = { - readonly "role": "developer" - readonly "content": string | ReadonlyArray - readonly "name"?: string -} -export const DeveloperMessage = Schema.Struct({ - "role": Schema.Literal("developer"), - "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItemText)]), - "name": Schema.optionalKey(Schema.String) -}) -export type OutputMessage = { - readonly "id": string - readonly "role": "assistant" - readonly "type": "message" - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content": ReadonlyArray -} -export const OutputMessage = Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Literal("message"), - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) -}) -export type ResponsesOutputMessage = { - readonly "id": string - readonly "role": "assistant" - readonly "type": "message" - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content": ReadonlyArray + ]).annotate({ "description": "Supported output sizes (width x height)" }) +}).annotate({ "identifier": "VideoModel" }) +export type WebFetchEngineEnum = "auto" | "native" | "openrouter" | "exa" | "parallel" | "firecrawl" +export const WebFetchEngineEnum = Schema.Literals(["auto", "native", "openrouter", "exa", "parallel", "firecrawl"]) + .annotate({ + "description": + "Which fetch engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in fetch. \"exa\" uses Exa Contents API. \"openrouter\" uses direct HTTP fetch. \"firecrawl\" uses Firecrawl scrape (requires BYOK). \"parallel\" uses the Parallel extract API.", + "identifier": "WebFetchEngineEnum" + }) +export type WebFetchPlugin = { + readonly "allowed_domains"?: ReadonlyArray + readonly "blocked_domains"?: ReadonlyArray + readonly "id": "web-fetch" + readonly "max_content_tokens"?: number + readonly "max_uses"?: number } -export const ResponsesOutputMessage = Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Literal("message"), - "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) -}).annotate({ "description": "An output message item" }) -export type OpenResponsesContentPartAddedEvent = { - readonly "type": "response.content_part.added" - readonly "output_index": number +export const WebFetchPlugin = Schema.Struct({ + "allowed_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Only fetch from these domains." }) + ), + "blocked_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ "description": "Never fetch from these domains." }) + ), + "id": Schema.Literal("web-fetch"), + "max_content_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum content length in approximate tokens. Content exceeding this limit is truncated." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_uses": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum number of web fetches per request. Once exceeded, the tool returns an error." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) +}).annotate({ "identifier": "WebFetchPlugin" }) +export type Arrays_13 = ReadonlyArray +export const Arrays_13 = Schema.Array(Schema.String).annotate({ "description": "Only fetch from these domains." }) +export type Arrays_14 = ReadonlyArray +export const Arrays_14 = Schema.Array(Schema.String).annotate({ "description": "Never fetch from these domains." }) +export type WebSearchCallCompletedEvent = { readonly "item_id": string - readonly "content_index": number - readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "output_index": number readonly "sequence_number": number + readonly "type": "response.web_search_call.completed" } -export const OpenResponsesContentPartAddedEvent = Schema.Struct({ - "type": Schema.Literal("response.content_part.added"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const WebSearchCallCompletedEvent = Schema.Struct({ "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a new content part is added to an output item" }) -export type OpenResponsesContentPartDoneEvent = { - readonly "type": "response.content_part.done" + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.completed") +}).annotate({ "description": "Web search call completed", "identifier": "WebSearchCallCompletedEvent" }) +export type WebSearchCallInProgressEvent = { + readonly "item_id": string readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.web_search_call.in_progress" +} +export const WebSearchCallInProgressEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.in_progress") +}).annotate({ "description": "Web search call in progress", "identifier": "WebSearchCallInProgressEvent" }) +export type WebSearchCallSearchingEvent = { readonly "item_id": string - readonly "content_index": number - readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "output_index": number readonly "sequence_number": number + readonly "type": "response.web_search_call.searching" } -export const OpenResponsesContentPartDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.content_part.done"), - "output_index": Schema.Number.check(Schema.isFinite()), +export const WebSearchCallSearchingEvent = Schema.Struct({ "item_id": Schema.String, - "content_index": Schema.Number.check(Schema.isFinite()), - "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a content part is complete" }) -export type ModelsListResponseData = ReadonlyArray -export const ModelsListResponseData = Schema.Array(Model).annotate({ "description": "List of available models" }) -export type ChatStreamingChoice = { - readonly "delta": ChatStreamingMessageChunk - readonly "finish_reason"?: __schema26 - readonly "index": number - readonly "logprobs"?: ChatMessageTokenLogprobs | null + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.web_search_call.searching") +}).annotate({ "description": "Web search call is searching", "identifier": "WebSearchCallSearchingEvent" }) +export type Arrays_15 = ReadonlyArray +export const Arrays_15 = Schema.Array(Schema.String).annotate({ + "description": + "Limit search results to these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, and most native providers (Anthropic, OpenAI, xAI). Cannot be used with excluded_domains." +}) +export type Arrays_16 = ReadonlyArray +export const Arrays_16 = Schema.Array(Schema.String).annotate({ + "description": + "Exclude search results from these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, Anthropic, and xAI. Not supported with OpenAI (silently ignored). Cannot be used with allowed_domains." +}) +export type Objects_152 = { + readonly "allowed_domains"?: ReadonlyArray | null + readonly "excluded_domains"?: ReadonlyArray | null + readonly [x: string]: Schema.Json } -export const ChatStreamingChoice = Schema.Struct({ - "delta": ChatStreamingMessageChunk, - "finish_reason": Schema.optionalKey(__schema26), - "index": Schema.Number.check(Schema.isFinite()), - "logprobs": Schema.optionalKey(Schema.Union([ChatMessageTokenLogprobs, Schema.Null])) +export const Objects_152 = Schema.StructWithRest( + Schema.Struct({ + "allowed_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "excluded_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type WebSearchEngine = "native" | "exa" | "firecrawl" | "parallel" | "perplexity" +export const WebSearchEngine = Schema.Literals(["native", "exa", "firecrawl", "parallel", "perplexity"]).annotate({ + "description": "The search engine to use for web search.", + "identifier": "WebSearchEngine" }) -export type UserMessage = { - readonly "role": "user" - readonly "content": string | ReadonlyArray - readonly "name"?: string +export type WebSearchEngineEnum = "native" | "exa" | "parallel" | "firecrawl" | "perplexity" | "auto" +export const WebSearchEngineEnum = Schema.Literals(["native", "exa", "parallel", "firecrawl", "perplexity", "auto"]) + .annotate({ + "description": + "Which search engine to use. \"auto\" (default) uses native if the provider supports it, otherwise Exa. \"native\" forces the provider's built-in search. \"exa\" forces the Exa search API. \"firecrawl\" uses Firecrawl (requires BYOK). \"parallel\" uses the Parallel search API. \"perplexity\" uses the Perplexity Search API (raw ranked results).", + "identifier": "WebSearchEngineEnum" + }) +export type Arrays_17 = ReadonlyArray +export const Arrays_17 = Schema.Array(Schema.String).annotate({ + "description": + "Limit search results to these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, and most native providers (Anthropic, OpenAI, xAI). Cannot be used with excluded_domains." +}) +export type Arrays_18 = ReadonlyArray +export const Arrays_18 = Schema.Array(Schema.String).annotate({ + "description": + "Exclude search results from these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, Anthropic, and xAI. Not supported with OpenAI (silently ignored). Cannot be used with allowed_domains." +}) +export type WebSearchSource = { readonly "type": "url"; readonly "url": string } +export const WebSearchSource = Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String }).annotate({ + "identifier": "WebSearchSource" +}) +export type WebSearchStatus = "completed" | "searching" | "in_progress" | "failed" +export const WebSearchStatus = Schema.Literals(["completed", "searching", "in_progress", "failed"]).annotate({ + "identifier": "WebSearchStatus" +}) +export type Objects_153 = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type"?: "approximate" + readonly [x: string]: Schema.Json +} +export const Objects_153 = Schema.StructWithRest( + Schema.Struct({ + "city": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "country": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "region": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "timezone": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.optionalKey(Schema.Literal("approximate")) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type WebSearchUserLocationServerTool = { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type"?: "approximate" } -export const UserMessage = Schema.Struct({ - "role": Schema.Literal("user"), - "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), - "name": Schema.optionalKey(Schema.String) +export const WebSearchUserLocationServerTool = Schema.Struct({ + "city": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "country": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "region": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "timezone": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.optionalKey(Schema.Literal("approximate")) +}).annotate({ + "description": "Approximate user location for location-biased results.", + "identifier": "WebSearchUserLocationServerTool" }) -export type AssistantMessage = { - readonly "role": "assistant" - readonly "content"?: string | ReadonlyArray | null - readonly "name"?: string - readonly "tool_calls"?: ReadonlyArray - readonly "refusal"?: string | null - readonly "reasoning"?: string | null - readonly "reasoning_details"?: ReadonlyArray<__schema20> - readonly "images"?: - | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> - | null - readonly "annotations"?: - | ReadonlyArray< - { - readonly "type": "url_citation" - readonly "url_citation": { - readonly "url": string - readonly "title"?: string - readonly "start_index"?: number - readonly "end_index"?: number - readonly "content"?: string - } - } | { - readonly "type": "file_annotation" - readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } - } | { - readonly "type": "file" - readonly "file": { - readonly "hash": string - readonly "name": string - readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> - } - } - > - | null +export type Workspace = { + readonly "created_at": string + readonly "created_by": string | null + readonly "default_guardrail_id": string + readonly "default_image_model": string | null + readonly "default_provider_sort": string | null + readonly "default_text_model": string | null + readonly "description": string | null + readonly "id": string + readonly "io_logging_api_key_ids": ReadonlyArray | null + readonly "io_logging_sampling_rate": number + readonly "is_data_discount_logging_enabled": boolean + readonly "is_observability_broadcast_enabled": boolean + readonly "is_observability_io_logging_enabled": boolean + readonly "name": string + readonly "slug": string + readonly "updated_at": string | null } -export const AssistantMessage = Schema.Struct({ - "role": Schema.Literal("assistant"), - "content": Schema.optionalKey( - Schema.Union([Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), Schema.Null]) - ), - "name": Schema.optionalKey(Schema.String), - "tool_calls": Schema.optionalKey(Schema.Array(ChatMessageToolCall)), - "refusal": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "reasoning": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "reasoning_details": Schema.optionalKey(Schema.Array(__schema20)), - "images": Schema.optionalKey( - Schema.Union([ - Schema.Array( - Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) - ), - Schema.Null - ]) - ), - "annotations": Schema.optionalKey(Schema.Union([ - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("url_citation"), - "url_citation": Schema.Struct({ - "url": Schema.String, - "title": Schema.optionalKey(Schema.String), - "start_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "end_index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "content": Schema.optionalKey(Schema.String) - }) - }), - Schema.Struct({ - "type": Schema.Literal("file_annotation"), - "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) - }), - Schema.Struct({ - "type": Schema.Literal("file"), - "file": Schema.Struct({ - "hash": Schema.String, - "name": Schema.String, - "content": Schema.optionalKey( - Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) - ) - }) - }) - ], { mode: "oneOf" })), +export const Workspace = Schema.Struct({ + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the workspace was created" }), + "created_by": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "User ID of the workspace creator" + }), + "default_guardrail_id": Schema.String.annotate({ + "description": "Deterministic ID of the workspace's implicitly-created default guardrail", + "format": "uuid" + }), + "default_image_model": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Default image model for this workspace" + }), + "default_provider_sort": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Default provider sort preference (price, throughput, latency, exacto)" + }), + "default_text_model": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Default text model for this workspace" + }), + "description": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Description of the workspace" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the workspace", "format": "uuid" }), + "io_logging_api_key_ids": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), Schema.Null - ])) + ]).annotate({ + "description": "Optional array of API key IDs to filter I/O logging. Null means all keys are logged." + }), + "io_logging_sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for I/O logging (0.0001-1). 1 means 100% of requests are logged.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "is_data_discount_logging_enabled": Schema.Boolean.annotate({ + "description": "Whether data discount logging is enabled for this workspace" + }), + "is_observability_broadcast_enabled": Schema.Boolean.annotate({ + "description": "Whether broadcast is enabled for this workspace" + }), + "is_observability_io_logging_enabled": Schema.Boolean.annotate({ + "description": "Whether private logging is enabled for this workspace" + }), + "name": Schema.String.annotate({ "description": "Name of the workspace" }), + "slug": Schema.String.annotate({ "description": "URL-friendly slug for the workspace" }), + "updated_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the workspace was last updated" + }) +}).annotate({ "identifier": "Workspace" }) +export type WorkspaceBudget = { + readonly "created_at": string + readonly "id": string + readonly "limit_usd": number + readonly "reset_interval": "daily" | "weekly" | "monthly" | null + readonly "updated_at": string + readonly "workspace_id": string +} +export const WorkspaceBudget = Schema.Struct({ + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the budget was created" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the budget", "format": "uuid" }), + "limit_usd": Schema.Number.annotate({ "description": "Spending limit in USD for this interval", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "reset_interval": Schema.Union([ + Schema.Literal("daily"), + Schema.Literal("weekly"), + Schema.Literal("monthly"), + Schema.Null + ]).annotate({ "description": "Interval at which spend resets. Null means a lifetime (one-time) budget." }), + "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the budget was last updated" }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace the budget belongs to", + "format": "uuid" + }) +}).annotate({ "identifier": "WorkspaceBudget" }) +export type WorkspaceBudgetInterval = "daily" | "weekly" | "monthly" | "lifetime" +export const WorkspaceBudgetInterval = Schema.Literals(["daily", "weekly", "monthly", "lifetime"]).annotate({ + "description": "Budget reset interval. Use \"lifetime\" for a one-time budget that never resets.", + "identifier": "WorkspaceBudgetInterval" }) -export type ToolResponseMessage = { - readonly "role": "tool" - readonly "content": string | ReadonlyArray - readonly "tool_call_id": string +export type WorkspaceMember = { + readonly "created_at": string + readonly "id": string + readonly "role": "admin" | "member" + readonly "user_id": string + readonly "workspace_id": string } -export const ToolResponseMessage = Schema.Struct({ - "role": Schema.Literal("tool"), - "content": Schema.Union([Schema.String, Schema.Array(ChatMessageContentItem)]), - "tool_call_id": Schema.String +export const WorkspaceMember = Schema.Struct({ + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the membership was created" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the workspace membership", "format": "uuid" }), + "role": Schema.Literals(["admin", "member"]).annotate({ "description": "Role of the member in the workspace" }), + "user_id": Schema.String.annotate({ "description": "Clerk user ID of the member" }), + "workspace_id": Schema.String.annotate({ "description": "ID of the workspace", "format": "uuid" }) +}).annotate({ "identifier": "WorkspaceMember" }) +export type ActivityResponse = { readonly "data": ReadonlyArray } +export const ActivityResponse = Schema.Struct({ + "data": Schema.Array(ActivityItem).annotate({ "description": "List of activity items" }) +}).annotate({ "identifier": "ActivityResponse" }) +export type Arrays_ = ReadonlyArray +export const Arrays_ = Schema.Array(AdvisorNestedTool).annotate({ + "description": + "Tools the advisor sub-agent may use while forming its advice. The advisor runs as an agentic sub-agent over these tools, then returns its text. Only OpenRouter server tools are supported — function tools are rejected — and the list must not include the advisor tool itself." }) -export type OpenAIResponsesInput = - | string - | ReadonlyArray< - | { - readonly "type"?: "message" - readonly "role": "user" | "system" | "assistant" | "developer" - readonly "content": - | ReadonlyArray - | string - } - | { - readonly "id": string - readonly "type"?: "message" - readonly "role": "user" | "system" | "developer" - readonly "content": ReadonlyArray - } - | { - readonly "type": "function_call_output" - readonly "id"?: string - readonly "call_id": string - readonly "output": string - readonly "status"?: ToolCallStatus - } +export type AnthropicBashCodeExecutionResult = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "bash_code_execution_result" +} +export const AnthropicBashCodeExecutionResult = Schema.Struct({ + "content": Schema.Array(AnthropicBashCodeExecutionOutput), + "return_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "stderr": Schema.String, + "stdout": Schema.String, + "type": Schema.Literal("bash_code_execution_result") +}).annotate({ "identifier": "AnthropicBashCodeExecutionResult" }) +export type AnthropicCacheControlDirective = { readonly "ttl"?: AnthropicCacheControlTtl; readonly "type": "ephemeral" } +export const AnthropicCacheControlDirective = Schema.Struct({ + "ttl": Schema.optionalKey(AnthropicCacheControlTtl), + "type": Schema.Literal("ephemeral") +}).annotate({ + "description": + "Enable automatic prompt caching. When set at the top level, the system automatically applies cache breakpoints to the last cacheable block in the request. When set on an individual content block, it marks an explicit cache breakpoint; block-level markers also work on OpenAI models that support explicit prompt caching — OpenRouter converts them to the provider's native format.", + "identifier": "AnthropicCacheControlDirective" +}) +export type ChatContentCacheControl = { readonly "ttl"?: AnthropicCacheControlTtl; readonly "type": "ephemeral" } +export const ChatContentCacheControl = Schema.Struct({ + "ttl": Schema.optionalKey(AnthropicCacheControlTtl), + "type": Schema.Literal("ephemeral") +}).annotate({ + "description": + "Anthropic-style cache breakpoint for the content part. Interchangeable with the OpenAI-style `prompt_cache_breakpoint` marker: OpenRouter converts between the two based on the provider serving the request.", + "identifier": "ChatContentCacheControl" +}) +export type AnthropicCacheCreation = Objects_ | null +export const AnthropicCacheCreation = Schema.Union([Objects_, Schema.Null]).annotate({ + "identifier": "AnthropicCacheCreation" +}) +export type AnthropicCitationsConfig = Objects_1 | null +export const AnthropicCitationsConfig = Schema.Union([Objects_1, Schema.Null]).annotate({ + "identifier": "AnthropicCitationsConfig" +}) +export type AnthropicTextCitation = + | AnthropicCitationCharLocation + | AnthropicCitationPageLocation + | AnthropicCitationContentBlockLocation + | AnthropicCitationWebSearchResultLocation + | AnthropicCitationSearchResultLocation +export const AnthropicTextCitation = Schema.Union([ + AnthropicCitationCharLocation, + AnthropicCitationPageLocation, + AnthropicCitationContentBlockLocation, + AnthropicCitationWebSearchResultLocation, + AnthropicCitationSearchResultLocation +], { mode: "oneOf" }).annotate({ "identifier": "AnthropicTextCitation" }) +export type MessagesContentBlockDeltaEvent = { + readonly "delta": + | { readonly "text": string; readonly "type": "text_delta" } + | { readonly "partial_json": string; readonly "type": "input_json_delta" } + | { readonly "thinking": string; readonly "type": "thinking_delta" } + | { readonly "signature": string; readonly "type": "signature_delta" } | { - readonly "type": "function_call" - readonly "call_id": string - readonly "name": string - readonly "arguments": string - readonly "id"?: string - readonly "status"?: ToolCallStatus + readonly "citation": + | AnthropicCitationCharLocation + | AnthropicCitationPageLocation + | AnthropicCitationContentBlockLocation + | AnthropicCitationWebSearchResultLocation + | AnthropicCitationSearchResultLocation + readonly "type": "citations_delta" } - | OutputItemImageGenerationCall - | OutputMessage - > - | unknown -export const OpenAIResponsesInput = Schema.Union([ - Schema.String, - Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.optionalKey(Schema.Literal("message")), - "role": Schema.Literals(["user", "system", "assistant", "developer"]), - "content": Schema.Union([ - Schema.Array( - Schema.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio], { - mode: "oneOf" - }) - ), - Schema.String - ]) - }), - Schema.Struct({ - "id": Schema.String, - "type": Schema.optionalKey(Schema.Literal("message")), - "role": Schema.Literals(["user", "system", "developer"]), - "content": Schema.Array( - Schema.Union([ResponseInputText, ResponseInputImage, ResponseInputFile, ResponseInputAudio], { mode: "oneOf" }) - ) - }), - Schema.Struct({ - "type": Schema.Literal("function_call_output"), - "id": Schema.optionalKey(Schema.String), - "call_id": Schema.String, - "output": Schema.String, - "status": Schema.optionalKey(ToolCallStatus) - }), + | { readonly "content": string | null; readonly "type": "compaction_delta" } + readonly "index": number + readonly "type": "content_block_delta" +} +export const MessagesContentBlockDeltaEvent = Schema.Struct({ + "delta": Schema.Union([ + Schema.Struct({ "text": Schema.String, "type": Schema.Literal("text_delta") }), + Schema.Struct({ "partial_json": Schema.String, "type": Schema.Literal("input_json_delta") }), + Schema.Struct({ "thinking": Schema.String, "type": Schema.Literal("thinking_delta") }), + Schema.Struct({ "signature": Schema.String, "type": Schema.Literal("signature_delta") }), Schema.Struct({ - "type": Schema.Literal("function_call"), - "call_id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "id": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey(ToolCallStatus) + "citation": Schema.Union([ + AnthropicCitationCharLocation, + AnthropicCitationPageLocation, + AnthropicCitationContentBlockLocation, + AnthropicCitationWebSearchResultLocation, + AnthropicCitationSearchResultLocation + ], { mode: "oneOf" }), + "type": Schema.Literal("citations_delta") }), - OutputItemImageGenerationCall, - OutputMessage - ])), - Schema.Unknown -]) -export type OpenResponsesOutputItemAddedEvent = { - readonly "type": "response.output_item.added" - readonly "output_index": number - readonly "item": - | OutputMessage - | OutputItemReasoning - | OutputItemFunctionCall - | OutputItemWebSearchCall - | OutputItemFileSearchCall - | OutputItemImageGenerationCall - readonly "sequence_number": number -} -export const OpenResponsesOutputItemAddedEvent = Schema.Struct({ - "type": Schema.Literal("response.output_item.added"), - "output_index": Schema.Number.check(Schema.isFinite()), - "item": Schema.Union([ - OutputMessage, - OutputItemReasoning, - OutputItemFunctionCall, - OutputItemWebSearchCall, - OutputItemFileSearchCall, - OutputItemImageGenerationCall + Schema.Struct({ "content": Schema.Union([Schema.String, Schema.Null]), "type": Schema.Literal("compaction_delta") }) ], { mode: "oneOf" }), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a new output item is added to the response" }) -export type OpenResponsesOutputItemDoneEvent = { - readonly "type": "response.output_item.done" - readonly "output_index": number - readonly "item": - | OutputMessage - | OutputItemReasoning - | OutputItemFunctionCall - | OutputItemWebSearchCall - | OutputItemFileSearchCall - | OutputItemImageGenerationCall - readonly "sequence_number": number + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("content_block_delta") +}).annotate({ + "description": "Event sent when content is added to a content block", + "identifier": "MessagesContentBlockDeltaEvent" +}) +export type AnthropicCodeExecutionResult = { + readonly "content": ReadonlyArray + readonly "return_code": number + readonly "stderr": string + readonly "stdout": string + readonly "type": "code_execution_result" } -export const OpenResponsesOutputItemDoneEvent = Schema.Struct({ - "type": Schema.Literal("response.output_item.done"), - "output_index": Schema.Number.check(Schema.isFinite()), - "item": Schema.Union([ - OutputMessage, - OutputItemReasoning, - OutputItemFunctionCall, - OutputItemWebSearchCall, - OutputItemFileSearchCall, - OutputItemImageGenerationCall - ], { mode: "oneOf" }), - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when an output item is complete" }) -export type OpenResponsesInput = - | string - | ReadonlyArray< - | OpenResponsesReasoning - | OpenResponsesEasyInputMessage - | OpenResponsesInputMessageItem - | OpenResponsesFunctionToolCall - | OpenResponsesFunctionCallOutput - | ResponsesOutputMessage - | ResponsesOutputItemReasoning - | ResponsesOutputItemFunctionCall - | ResponsesWebSearchCallOutput - | ResponsesOutputItemFileSearchCall - | ResponsesImageGenerationCall - > -export const OpenResponsesInput = Schema.Union([ - Schema.String, - Schema.Array( - Schema.Union([ - OpenResponsesReasoning, - OpenResponsesEasyInputMessage, - OpenResponsesInputMessageItem, - OpenResponsesFunctionToolCall, - OpenResponsesFunctionCallOutput, - ResponsesOutputMessage, - ResponsesOutputItemReasoning, - ResponsesOutputItemFunctionCall, - ResponsesWebSearchCallOutput, - ResponsesOutputItemFileSearchCall, - ResponsesImageGenerationCall - ]) - ) -]).annotate({ "description": "Input for a response request - can be a string or array of items" }) -export type ModelsListResponse = { readonly "data": ModelsListResponseData } -export const ModelsListResponse = Schema.Struct({ "data": ModelsListResponseData }).annotate({ - "description": "List of available models" +export const AnthropicCodeExecutionResult = Schema.Struct({ + "content": Schema.Array(AnthropicCodeExecutionOutput), + "return_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "stderr": Schema.String, + "stdout": Schema.String, + "type": Schema.Literal("code_execution_result") +}).annotate({ "identifier": "AnthropicCodeExecutionResult" }) +export type AnthropicEncryptedCodeExecutionResult = { + readonly "content": ReadonlyArray + readonly "encrypted_stdout": string + readonly "return_code": number + readonly "stderr": string + readonly "type": "encrypted_code_execution_result" +} +export const AnthropicEncryptedCodeExecutionResult = Schema.Struct({ + "content": Schema.Array(AnthropicCodeExecutionOutput), + "encrypted_stdout": Schema.String, + "return_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "stderr": Schema.String, + "type": Schema.Literal("encrypted_code_execution_result") +}).annotate({ "identifier": "AnthropicEncryptedCodeExecutionResult" }) +export type AnthropicCaller = + | AnthropicDirectCaller + | AnthropicCodeExecution20250825Caller + | AnthropicCodeExecution20260120Caller +export const AnthropicCaller = Schema.Union([ + AnthropicDirectCaller, + AnthropicCodeExecution20250825Caller, + AnthropicCodeExecution20260120Caller +], { mode: "oneOf" }).annotate({ "identifier": "AnthropicCaller" }) +export type ORAnthropicNullableCaller = + | AnthropicDirectCaller + | AnthropicCodeExecution20250825Caller + | AnthropicCodeExecution20260120Caller + | null +export const ORAnthropicNullableCaller = Schema.Union([ + AnthropicDirectCaller, + AnthropicCodeExecution20250825Caller, + AnthropicCodeExecution20260120Caller, + Schema.Null +], { mode: "oneOf" }).annotate({ "identifier": "ORAnthropicNullableCaller" }) +export type AnthropicBase64ImageSource = { + readonly "data": string + readonly "media_type": AnthropicImageMimeType + readonly "type": "base64" +} +export const AnthropicBase64ImageSource = Schema.Struct({ + "data": Schema.String, + "media_type": AnthropicImageMimeType, + "type": Schema.Literal("base64") +}).annotate({ "identifier": "AnthropicBase64ImageSource" }) +export type AnthropicInputTokensClearAtLeast = Objects_2 | null +export const AnthropicInputTokensClearAtLeast = Schema.Union([Objects_2, Schema.Null]).annotate({ + "identifier": "AnthropicInputTokensClearAtLeast" }) -export type ChatStreamingResponseChunk = { - readonly "data": { - readonly "id": string - readonly "choices": ReadonlyArray - readonly "created": number - readonly "model": string - readonly "object": "chat.completion.chunk" - readonly "system_fingerprint"?: string | null - readonly "error"?: { readonly "message": string; readonly "code": number } - readonly "usage"?: ChatGenerationTokenUsage - } +export type AnthropicIterationCacheCreation = Objects_3 | null +export const AnthropicIterationCacheCreation = Schema.Union([Objects_3, Schema.Null]).annotate({ + "identifier": "AnthropicIterationCacheCreation" +}) +export type AnthropicCodeExecutionToolResultError = { + readonly "error_code": AnthropicServerToolErrorCode + readonly "type": "code_execution_tool_result_error" } -export const ChatStreamingResponseChunk = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String, - "choices": Schema.Array(ChatStreamingChoice), - "created": Schema.Number.check(Schema.isFinite()), - "model": Schema.String, - "object": Schema.Literal("chat.completion.chunk"), - "system_fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "error": Schema.optionalKey( - Schema.Struct({ "message": Schema.String, "code": Schema.Number.check(Schema.isFinite()) }) - ), - "usage": Schema.optionalKey(ChatGenerationTokenUsage) +export const AnthropicCodeExecutionToolResultError = Schema.Struct({ + "error_code": AnthropicServerToolErrorCode, + "type": Schema.Literal("code_execution_tool_result_error") +}).annotate({ "identifier": "AnthropicCodeExecutionToolResultError" }) +export type AnthropicToolSearchResultError = { + readonly "error_code": AnthropicServerToolErrorCode + readonly "error_message": string | null + readonly "type": "tool_search_tool_result_error" +} +export const AnthropicToolSearchResultError = Schema.Struct({ + "error_code": AnthropicServerToolErrorCode, + "error_message": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("tool_search_tool_result_error") +}).annotate({ "identifier": "AnthropicToolSearchResultError" }) +export type AnthropicTextEditorCodeExecutionContent = + | AnthropicTextEditorCodeExecutionToolResultError + | AnthropicTextEditorCodeExecutionViewResult + | AnthropicTextEditorCodeExecutionCreateResult + | AnthropicTextEditorCodeExecutionStrReplaceResult +export const AnthropicTextEditorCodeExecutionContent = Schema.Union([ + AnthropicTextEditorCodeExecutionToolResultError, + AnthropicTextEditorCodeExecutionViewResult, + AnthropicTextEditorCodeExecutionCreateResult, + AnthropicTextEditorCodeExecutionStrReplaceResult +], { mode: "oneOf" }).annotate({ "identifier": "AnthropicTextEditorCodeExecutionContent" }) +export type AnthropicToolSearchResult = { + readonly "tool_references": ReadonlyArray + readonly "type": "tool_search_tool_search_result" +} +export const AnthropicToolSearchResult = Schema.Struct({ + "tool_references": Schema.Array(AnthropicToolReference), + "type": Schema.Literal("tool_search_tool_search_result") +}).annotate({ "identifier": "AnthropicToolSearchResult" }) +export type AnthropicWebSearchToolUserLocation = Objects_4 | null +export const AnthropicWebSearchToolUserLocation = Schema.Union([Objects_4, Schema.Null]).annotate({ + "identifier": "AnthropicWebSearchToolUserLocation" +}) +export type MessagesErrorDetail = { + readonly "error_type"?: ApiErrorType + readonly "message": string + readonly "type": string +} +export const MessagesErrorDetail = Schema.Struct({ + "error_type": Schema.optionalKey(ApiErrorType), + "message": Schema.String, + "type": Schema.String +}).annotate({ "identifier": "MessagesErrorDetail" }) +export type ApplyPatchServerToolConfig = { readonly "engine"?: ApplyPatchEngineEnum } +export const ApplyPatchServerToolConfig = Schema.Struct({ "engine": Schema.optionalKey(ApplyPatchEngineEnum) }) + .annotate({ + "description": "Configuration for the openrouter:apply_patch server tool", + "identifier": "ApplyPatchServerToolConfig" }) +export type ApplyPatchCallOperation = + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation +export const ApplyPatchCallOperation = Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation +], { mode: "oneOf" }).annotate({ + "description": + "The patch operation requested by an `apply_patch_call`. `create_file` and `update_file` carry a V4A diff; `delete_file` omits it.", + "identifier": "ApplyPatchCallOperation" }) -export type ChatResponseChoice = { - readonly "finish_reason": __schema26 - readonly "index": number - readonly "message": AssistantMessage - readonly "logprobs"?: ChatMessageTokenLogprobs | null -} -export const ChatResponseChoice = Schema.Struct({ - "finish_reason": __schema26, - "index": Schema.Number.check(Schema.isFinite()), - "message": AssistantMessage, - "logprobs": Schema.optionalKey(Schema.Union([ChatMessageTokenLogprobs, Schema.Null])) -}) -export type Message = SystemMessage | UserMessage | DeveloperMessage | AssistantMessage | ToolResponseMessage -export const Message = Schema.Union([ - SystemMessage, - UserMessage, - DeveloperMessage, - AssistantMessage, - ToolResponseMessage -], { mode: "oneOf" }) -export type OpenAIResponsesNonStreamingResponse = { +export type OutputItemApplyPatchCall = { + readonly "call_id": string + readonly "created_by"?: string readonly "id": string - readonly "object": "response" - readonly "created_at": number - readonly "model": string - readonly "status": OpenAIResponsesResponseStatus - readonly "completed_at": number - readonly "output": ReadonlyArray< - | OutputMessage - | OutputItemReasoning - | OutputItemFunctionCall - | OutputItemWebSearchCall - | OutputItemFileSearchCall - | OutputItemImageGenerationCall - > - readonly "user"?: string - readonly "output_text"?: string - readonly "prompt_cache_key"?: string - readonly "safety_identifier"?: string - readonly "error": ResponsesErrorField - readonly "incomplete_details": OpenAIResponsesIncompleteDetails - readonly "usage"?: OpenAIResponsesUsage - readonly "max_tool_calls"?: number - readonly "top_logprobs"?: number - readonly "max_output_tokens"?: number - readonly "temperature": number - readonly "top_p": number - readonly "presence_penalty": number - readonly "frequency_penalty": number - readonly "instructions": OpenAIResponsesInput - readonly "metadata": OpenResponsesRequestMetadata - readonly "tools": ReadonlyArray< - | { - readonly "type": "function" - readonly "name": string - readonly "description"?: string - readonly "strict"?: boolean - readonly "parameters": {} - } - | OpenResponsesWebSearchPreviewTool - | OpenResponsesWebSearchPreview20250311Tool - | OpenResponsesWebSearchTool - | OpenResponsesWebSearch20250826Tool - > - readonly "tool_choice": OpenAIResponsesToolChoice - readonly "parallel_tool_calls": boolean - readonly "prompt"?: OpenAIResponsesPrompt - readonly "background"?: boolean - readonly "previous_response_id"?: string - readonly "reasoning"?: OpenAIResponsesReasoningConfig - readonly "service_tier"?: OpenAIResponsesServiceTier - readonly "store"?: boolean - readonly "truncation"?: OpenAIResponsesTruncation - readonly "text"?: ResponseTextConfig + readonly "operation": ApplyPatchCreateFileOperation | ApplyPatchUpdateFileOperation | ApplyPatchDeleteFileOperation + readonly "status": "in_progress" | "completed" + readonly "type": "apply_patch_call" } -export const OpenAIResponsesNonStreamingResponse = Schema.Struct({ +export const OutputItemApplyPatchCall = Schema.Struct({ + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), "id": Schema.String, - "object": Schema.Literal("response"), - "created_at": Schema.Number.check(Schema.isFinite()), - "model": Schema.String, - "status": OpenAIResponsesResponseStatus, - "completed_at": Schema.Number.check(Schema.isFinite()), - "output": Schema.Array( - Schema.Union([ - OutputMessage, - OutputItemReasoning, - OutputItemFunctionCall, - OutputItemWebSearchCall, - OutputItemFileSearchCall, - OutputItemImageGenerationCall - ], { mode: "oneOf" }) + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }), + "status": Schema.Literals(["in_progress", "completed"]), + "type": Schema.Literal("apply_patch_call") +}).annotate({ "identifier": "OutputItemApplyPatchCall" }) +export type BadGatewayResponse = { + readonly "error": BadGatewayResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const BadGatewayResponse = Schema.Struct({ + "error": BadGatewayResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "user": Schema.optionalKey(Schema.String), - "output_text": Schema.optionalKey(Schema.String), - "prompt_cache_key": Schema.optionalKey(Schema.String), - "safety_identifier": Schema.optionalKey(Schema.String), - "error": ResponsesErrorField, - "incomplete_details": OpenAIResponsesIncompleteDetails, - "usage": Schema.optionalKey(OpenAIResponsesUsage), - "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "temperature": Schema.Number.check(Schema.isFinite()), - "top_p": Schema.Number.check(Schema.isFinite()), - "presence_penalty": Schema.Number.check(Schema.isFinite()), - "frequency_penalty": Schema.Number.check(Schema.isFinite()), - "instructions": OpenAIResponsesInput, - "metadata": OpenResponsesRequestMetadata, - "tools": Schema.Array( - Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("function"), - "name": Schema.String, - "description": Schema.optionalKey(Schema.String), - "strict": Schema.optionalKey(Schema.Boolean), - "parameters": Schema.Struct({}) - }).annotate({ "description": "Function tool definition" }), - OpenResponsesWebSearchPreviewTool, - OpenResponsesWebSearchPreview20250311Tool, - OpenResponsesWebSearchTool, - OpenResponsesWebSearch20250826Tool - ], { mode: "oneOf" }) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ "description": "Bad Gateway - Provider/upstream API failure", "identifier": "BadGatewayResponse" }) +export type BadRequestResponse = { + readonly "error": BadRequestResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const BadRequestResponse = Schema.Struct({ + "error": BadRequestResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "tool_choice": OpenAIResponsesToolChoice, - "parallel_tool_calls": Schema.Boolean, - "prompt": Schema.optionalKey(OpenAIResponsesPrompt), - "background": Schema.optionalKey(Schema.Boolean), - "previous_response_id": Schema.optionalKey(Schema.String), - "reasoning": Schema.optionalKey(OpenAIResponsesReasoningConfig), - "service_tier": Schema.optionalKey(OpenAIResponsesServiceTier), - "store": Schema.optionalKey(Schema.Boolean), - "truncation": Schema.optionalKey(OpenAIResponsesTruncation), - "text": Schema.optionalKey(ResponseTextConfig) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Bad Request - Invalid request parameters or malformed input", + "identifier": "BadRequestResponse" }) -export type OpenResponsesNonStreamingResponse = { +export type BYOKKey = { + readonly "allowed_api_key_hashes": ReadonlyArray | null + readonly "allowed_models": ReadonlyArray | null + readonly "allowed_user_ids": ReadonlyArray | null + readonly "created_at": string + readonly "disabled": boolean readonly "id": string - readonly "object": "response" - readonly "created_at": number - readonly "model": string - readonly "status": OpenAIResponsesResponseStatus - readonly "completed_at": number - readonly "output": ReadonlyArray< - { - readonly "id": string - readonly "role": "assistant" - readonly "type": "message" - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content": ReadonlyArray< - { - readonly "type": "output_text" - readonly "text": string - readonly "annotations"?: ReadonlyArray< - { - readonly "type": "file_citation" - readonly "file_id": string - readonly "filename": string - readonly "index": number - } | { - readonly "type": never - readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number - readonly "file_id": string - readonly "filename": string - readonly "index": number - } | { - readonly "type": never - readonly "file_id": string - readonly "index": number - readonly "filename": string - } | { - readonly "type": never - readonly "file_id": string - readonly "filename": string - readonly "index": number - readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number - } | { - readonly "type": "url_citation" - readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number - } | { - readonly "type": never - readonly "file_id": string - readonly "index": number - readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number - } | { - readonly "type": never - readonly "file_id": string - readonly "filename": string - readonly "index": number - } | { - readonly "type": never - readonly "url": string - readonly "title": string - readonly "start_index": number - readonly "end_index": number - readonly "file_id": string - readonly "index": number - } | { readonly "type": "file_path"; readonly "file_id": string; readonly "index": number } - > - readonly "logprobs"?: ReadonlyArray< - { - readonly "token": string - readonly "bytes": ReadonlyArray - readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } - > - } - > - } | { - readonly "type": never - readonly "refusal": string - readonly "text": string - readonly "annotations"?: ReadonlyArray - readonly "logprobs"?: ReadonlyArray< - { - readonly "token": string - readonly "bytes": ReadonlyArray - readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } - > - } - > - } | { - readonly "type": never - readonly "text": string - readonly "annotations"?: ReadonlyArray - readonly "logprobs"?: ReadonlyArray< - { - readonly "token": string - readonly "bytes": ReadonlyArray - readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } - > - } - > - readonly "refusal": string - } | { readonly "type": "refusal"; readonly "refusal": string } - > - } | { - readonly "type": never - readonly "id": string - readonly "content": ReadonlyArray< - { - readonly "type": never - readonly "text": string - readonly "annotations"?: ReadonlyArray - readonly "logprobs"?: ReadonlyArray< - { - readonly "token": string - readonly "bytes": ReadonlyArray - readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } - > - } - > - } | { readonly "type": never; readonly "refusal": string; readonly "text": string } - > - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - readonly "role": "assistant" - } | { - readonly "type": never - readonly "id": string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "role": "assistant" - readonly "content": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "status": "completed" | "in_progress" - readonly "role": "assistant" - readonly "content": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "in_progress" - readonly "role": "assistant" - readonly "content": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" - readonly "role": "assistant" - readonly "content": ReadonlyArray - } | { - readonly "id": string - readonly "role": "assistant" - readonly "type": never - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content": ReadonlyArray< - { - readonly "type": never - readonly "text": string - readonly "annotations"?: ReadonlyArray - readonly "logprobs"?: ReadonlyArray< - { - readonly "token": string - readonly "bytes": ReadonlyArray - readonly "logprob": number - readonly "top_logprobs": ReadonlyArray< - { readonly "token": string; readonly "bytes": ReadonlyArray; readonly "logprob": number } - > - } - > - } | { readonly "type": never; readonly "refusal": string; readonly "text": string } - > - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - } | { - readonly "type": "reasoning" - readonly "id": string - readonly "content"?: ReadonlyArray<{ readonly "type": "reasoning_text"; readonly "text": string }> - readonly "summary": ReadonlyArray<{ readonly "type": "summary_text"; readonly "text": string }> - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - } | { - readonly "type": never - readonly "id": string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - } | { - readonly "type": never - readonly "id": string - readonly "status": "completed" | "in_progress" - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - } | { - readonly "type": never - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "in_progress" - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - } | { - readonly "type": never - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - } | { - readonly "id": string - readonly "role": "assistant" - readonly "type": never - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "content": ReadonlyArray - readonly "name": string - readonly "arguments": string - readonly "call_id": string - } | { - readonly "type": never - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status"?: "completed" | "incomplete" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - readonly "name": string - readonly "arguments": string - readonly "call_id": string - } | { - readonly "type": "function_call" - readonly "id"?: string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status"?: "completed" | "incomplete" | "in_progress" - } | { - readonly "type": never - readonly "id": string - readonly "status": "completed" | "in_progress" - readonly "name": string - readonly "arguments": string - readonly "call_id": string - } | { - readonly "type": never - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "in_progress" - readonly "name": string - readonly "arguments": string - readonly "call_id": string - } | { - readonly "type": never - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" - readonly "name": string - readonly "arguments": string - readonly "call_id": string - } | { - readonly "id": string - readonly "role": "assistant" - readonly "type": never - readonly "status": "completed" | "in_progress" - readonly "content": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status": "completed" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - } | { - readonly "type": never - readonly "id": string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status": "completed" | "in_progress" - } | { - readonly "type": "web_search_call" - readonly "id": string - readonly "status": "completed" | "searching" | "in_progress" | "failed" - } | { - readonly "type": never - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "searching" | "in_progress" | "failed" - } | { - readonly "type": never - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" | "failed" - } | { - readonly "id": string - readonly "role": "assistant" - readonly "type": never - readonly "status": "completed" | "in_progress" - readonly "content": ReadonlyArray - readonly "queries": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status": "completed" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - readonly "queries": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status": "completed" | "in_progress" - readonly "queries": ReadonlyArray - } | { - readonly "type": never - readonly "id": string - readonly "status": "completed" | "searching" | "in_progress" | "failed" - readonly "queries": ReadonlyArray - } | { - readonly "type": "file_search_call" - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "searching" | "in_progress" | "failed" - } | { - readonly "type": never - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" | "failed" - readonly "queries": ReadonlyArray - } | { - readonly "id": string - readonly "role": "assistant" - readonly "type": never - readonly "status": "completed" | "in_progress" - readonly "content": ReadonlyArray - readonly "result"?: string - } | { - readonly "type": never - readonly "id": string - readonly "content"?: ReadonlyArray - readonly "summary": ReadonlyArray - readonly "encrypted_content"?: string - readonly "status": "completed" | "in_progress" - readonly "signature"?: string - readonly "format"?: - | "unknown" - | "openai-responses-v1" - | "azure-openai-responses-v1" - | "xai-responses-v1" - | "anthropic-claude-v1" - | "google-gemini-v1" - readonly "result"?: string - } | { - readonly "type": never - readonly "id": string - readonly "name": string - readonly "arguments": string - readonly "call_id": string - readonly "status": "completed" | "in_progress" - readonly "result"?: string - } | { - readonly "type": never - readonly "id": string - readonly "status": "completed" | "in_progress" | "failed" - readonly "result"?: string - } | { - readonly "type": never - readonly "id": string - readonly "queries": ReadonlyArray - readonly "status": "completed" | "in_progress" | "failed" - readonly "result"?: string - } | { - readonly "type": "image_generation_call" - readonly "id": string - readonly "result"?: string - readonly "status": "in_progress" | "completed" | "generating" | "failed" - } - > - readonly "user"?: string - readonly "output_text"?: string - readonly "prompt_cache_key"?: string - readonly "safety_identifier"?: string - readonly "error": ResponsesErrorField - readonly "incomplete_details": OpenAIResponsesIncompleteDetails - readonly "usage"?: { - readonly "input_tokens": number - readonly "input_tokens_details": { readonly "cached_tokens": number } - readonly "output_tokens": number - readonly "output_tokens_details": { readonly "reasoning_tokens": number } - readonly "total_tokens": number - readonly "cost"?: number - readonly "is_byok"?: boolean - readonly "cost_details"?: { - readonly "upstream_inference_cost"?: number - readonly "upstream_inference_input_cost": number - readonly "upstream_inference_output_cost": number - } - } - readonly "max_tool_calls"?: number - readonly "top_logprobs"?: number - readonly "max_output_tokens"?: number - readonly "temperature": number - readonly "top_p": number - readonly "presence_penalty": number - readonly "frequency_penalty": number - readonly "instructions": OpenAIResponsesInput - readonly "metadata": OpenResponsesRequestMetadata - readonly "tools": ReadonlyArray< + readonly "is_fallback": boolean + readonly "label": string + readonly "name"?: string | null + readonly "provider": BYOKProviderSlug + readonly "sort_order": number + readonly "workspace_id": string +} +export const BYOKKey = Schema.Struct({ + "allowed_api_key_hashes": Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) that may use this credential. `null` means no restriction." + }), + "allowed_models": Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of model slugs this credential may be used for. `null` means no restriction." + }), + "allowed_user_ids": Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of user IDs that may use this credential. `null` means no restriction." + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the credential was created." }), + "disabled": Schema.Boolean.annotate({ "description": "Whether this credential is currently disabled." }), + "id": Schema.String.annotate({ + "description": "Stable public identifier for this BYOK credential.", + "format": "uuid" + }), + "is_fallback": Schema.Boolean.annotate({ + "description": + "Whether this credential is treated as a fallback — used only after non-fallback keys for the same provider have been tried." + }), + "label": Schema.String.annotate({ + "description": "Short masked snippet of the key (e.g. the first/last few characters) used to identify it in the UI." + }), + "name": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Optional human-readable name for the credential." + }) + ), + "provider": BYOKProviderSlug, + "sort_order": Schema.Number.annotate({ + "description": "Position within the provider — credentials are tried in ascending sort order." + }).check(Schema.isInt().annotate({ "expected": "an integer" })), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this credential belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "BYOKKey" }) +export type CreateBYOKKeyRequest = { + readonly "allowed_models"?: ReadonlyArray | null + readonly "allowed_user_ids"?: ReadonlyArray | null + readonly "disabled"?: boolean + readonly "is_fallback"?: boolean + readonly "key": string + readonly "name"?: string | null + readonly "provider": BYOKProviderSlug + readonly "workspace_id"?: string +} +export const CreateBYOKKeyRequest = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of model slugs this credential may be used for. `null` means no restriction." + }) + ), + "allowed_user_ids": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" }) + ), + Schema.Null + ]).annotate({ + "description": "Optional allowlist of user IDs that may use this credential. `null` means no restriction." + }) + ), + "disabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this credential should be created in a disabled state." }) + ), + "is_fallback": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether this credential is treated as a fallback — used only after non-fallback keys for the same provider have been tried." + }) + ), + "key": Schema.String.annotate({ + "description": + "The raw provider API key or credential. This value is encrypted at rest and never returned in API responses." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "name": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(255).annotate({ "expected": "a value with a length of at most 255" })), + Schema.Null + ]).annotate({ "description": "Optional human-readable name for the credential." }) + ), + "provider": BYOKProviderSlug, + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional workspace ID. Defaults to the authenticated entity's default workspace.", + "format": "uuid" + }) + ) +}).annotate({ "identifier": "CreateBYOKKeyRequest" }) +export type ChatContentVideo = { readonly "type": "video_url"; readonly "video_url": ChatContentVideoInput } +export const ChatContentVideo = Schema.Struct({ + "type": Schema.Literal("video_url"), + "video_url": ChatContentVideoInput +}).annotate({ "description": "Video input content part", "identifier": "ChatContentVideo" }) +export type ChatFormatJsonSchemaConfig = { + readonly "json_schema": ChatJsonSchemaConfig + readonly "type": "json_schema" +} +export const ChatFormatJsonSchemaConfig = Schema.Struct({ + "json_schema": ChatJsonSchemaConfig, + "type": Schema.Literal("json_schema") +}).annotate({ + "description": "JSON Schema response format for structured outputs", + "identifier": "ChatFormatJsonSchemaConfig" +}) +export type ChatToolChoice = "none" | "auto" | "required" | ChatNamedToolChoice | ChatServerToolChoice +export const ChatToolChoice = Schema.Union([ + Schema.Literal("none"), + Schema.Literal("auto"), + Schema.Literal("required"), + ChatNamedToolChoice, + ChatServerToolChoice +]).annotate({ "description": "Tool choice configuration", "identifier": "ChatToolChoice" }) +export type ChatStreamOptions = Objects_6 | null +export const ChatStreamOptions = Schema.Union([Objects_6, Schema.Null]).annotate({ + "description": "Streaming configuration options", + "identifier": "ChatStreamOptions" +}) +export type Objects_7 = { + readonly "content": ReadonlyArray | null + readonly "refusal"?: ReadonlyArray | null + readonly [x: string]: Schema.Json +} +export const Objects_7 = Schema.StructWithRest( + Schema.Struct({ + "content": Schema.Union([Schema.Array(ChatTokenLogprob), Schema.Null]).annotate({ + "description": "Log probabilities for content tokens" + }), + "refusal": Schema.optionalKey( + Schema.Union([Schema.Array(ChatTokenLogprob), Schema.Null]).annotate({ + "description": "Log probabilities for refusal tokens" + }) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type FileSearchServerTool = { + readonly "filters"?: | { - readonly "type": "function" - readonly "name": string - readonly "description"?: string - readonly "strict"?: boolean - readonly "parameters": {} + readonly "key": string + readonly "type": "eq" | "ne" | "gt" | "gte" | "lt" | "lte" + readonly "value": string | number | boolean | ReadonlyArray } - | OpenResponsesWebSearchPreviewTool - | OpenResponsesWebSearchPreview20250311Tool - | OpenResponsesWebSearchTool - | OpenResponsesWebSearch20250826Tool - > - readonly "tool_choice": OpenAIResponsesToolChoice - readonly "parallel_tool_calls": boolean - readonly "prompt"?: OpenAIResponsesPrompt - readonly "background"?: boolean - readonly "previous_response_id"?: string - readonly "reasoning"?: OpenAIResponsesReasoningConfig - readonly "service_tier"?: OpenAIResponsesServiceTier - readonly "store"?: boolean - readonly "truncation"?: OpenAIResponsesTruncation - readonly "text"?: ResponseTextConfig + | CompoundFilter + | null + readonly "max_num_results"?: number + readonly "ranking_options"?: { + readonly "ranker"?: "auto" | "default-2024-11-15" + readonly "score_threshold"?: number + } + readonly "type": "file_search" + readonly "vector_store_ids": ReadonlyArray } -export const OpenResponsesNonStreamingResponse = Schema.Struct({ - "id": Schema.String, - "object": Schema.Literal("response"), - "created_at": Schema.Number.check(Schema.isFinite()), - "model": Schema.String, - "status": OpenAIResponsesResponseStatus, - "completed_at": Schema.Number.check(Schema.isFinite()), - "output": Schema.Array(Schema.Union([ - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Literal("message"), - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) +export const FileSearchServerTool = Schema.Struct({ + "filters": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "key": Schema.String, + "type": Schema.Literals(["eq", "ne", "gt", "gte", "lt", "lte"]), + "value": Schema.Union([ + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) ), - "content": Schema.Array(Schema.Union([ + Schema.Boolean, + Schema.Array( Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("output_text"), - "text": Schema.String, - "annotations": Schema.optionalKey( - Schema.Array( - Schema.Union([ - Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("file_citation"), - "file_id": Schema.String, - "filename": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Never, - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String, - "filename": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Never, - "file_id": Schema.String, - "index": Schema.Number.check(Schema.isFinite()), - "filename": Schema.String - }) - ]), - Schema.Union([ - Schema.Struct({ - "type": Schema.Never, - "file_id": Schema.String, - "filename": Schema.String, - "index": Schema.Number.check(Schema.isFinite()), - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("url_citation"), - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Never, - "file_id": Schema.String, - "index": Schema.Number.check(Schema.isFinite()), - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()) - }) - ]), - Schema.Union([ - Schema.Struct({ - "type": Schema.Never, - "file_id": Schema.String, - "filename": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Never, - "url": Schema.String, - "title": Schema.String, - "start_index": Schema.Number.check(Schema.isFinite()), - "end_index": Schema.Number.check(Schema.isFinite()), - "file_id": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) - }), - Schema.Struct({ - "type": Schema.Literal("file_path"), - "file_id": Schema.String, - "index": Schema.Number.check(Schema.isFinite()) - }) - ]) - ]) - ) - ), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))) - }), - Schema.Struct({ - "type": Schema.Never, - "refusal": Schema.String, - "text": Schema.String, - "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))) - }) - ]), - Schema.Union([ - Schema.Struct({ - "type": Schema.Never, - "text": Schema.String, - "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))), - "refusal": Schema.String - }), - Schema.Struct({ "type": Schema.Literal("refusal"), "refusal": Schema.String }) + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) ]) - ])) - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "content": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Never, - "text": Schema.String, - "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))) - }), - Schema.Struct({ "type": Schema.Never, "refusal": Schema.String, "text": Schema.String }) - ])), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ), - "role": Schema.Literal("assistant") - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), - "role": Schema.Literal("assistant"), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) - }) - ]).annotate({ "description": "An output item from the response" }), - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Never, - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "content": Schema.Array(Schema.Union([ - Schema.Struct({ - "type": Schema.Never, - "text": Schema.String, - "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), - "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()), - "top_logprobs": Schema.Array( - Schema.Struct({ - "token": Schema.String, - "bytes": Schema.Array(Schema.Number.check(Schema.isFinite())), - "logprob": Schema.Number.check(Schema.isFinite()) - }) - ) - }))) - }), - Schema.Struct({ "type": Schema.Never, "refusal": Schema.String, "text": Schema.String }) - ])), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String) - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Literal("reasoning"), - "id": Schema.String, - "content": Schema.optionalKey( - Schema.Array(Schema.Struct({ "type": Schema.Literal("reasoning_text"), "text": Schema.String })) - ), - "summary": Schema.Array(Schema.Struct({ "type": Schema.Literal("summary_text"), "text": Schema.String })), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) ) - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String) - }) - ]).annotate({ "description": "An output item from the response" }), - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Never, - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) - ), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Literal("function_call"), - "id": Schema.optionalKey(Schema.String), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.optionalKey( - Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ]) + }), + CompoundFilter, + Schema.Null + ])), + "max_num_results": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "ranking_options": Schema.optionalKey( + Schema.Struct({ + "ranker": Schema.optionalKey(Schema.Literals(["auto", "default-2024-11-15"])), + "score_threshold": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) ) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String - }) - ]).annotate({ "description": "An output item from the response" }), - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Never, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])) - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ) - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]) - }), - Schema.Struct({ - "type": Schema.Literal("web_search_call"), - "id": Schema.String, - "status": Schema.Union([ - Schema.Literal("completed"), - Schema.Literal("searching"), - Schema.Literal("in_progress"), - Schema.Literal("failed") - ]) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([ - Schema.Literal("completed"), - Schema.Literal("searching"), - Schema.Literal("in_progress"), - Schema.Literal("failed") - ]) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]) - }) - ]).annotate({ "description": "An output item from the response" }), - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Never, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), - "queries": Schema.Array(Schema.String) - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ), - "queries": Schema.Array(Schema.String) - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "queries": Schema.Array(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "status": Schema.Union([ - Schema.Literal("completed"), - Schema.Literal("searching"), - Schema.Literal("in_progress"), - Schema.Literal("failed") - ]), - "queries": Schema.Array(Schema.String) - }), - Schema.Struct({ - "type": Schema.Literal("file_search_call"), - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([ - Schema.Literal("completed"), - Schema.Literal("searching"), - Schema.Literal("in_progress"), - Schema.Literal("failed") - ]) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]), - "queries": Schema.Array(Schema.String) - }) - ]).annotate({ "description": "An output item from the response" }), - Schema.Union([ - Schema.Struct({ - "id": Schema.String, - "role": Schema.Literal("assistant"), - "type": Schema.Never, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), - "result": Schema.optionalKey(Schema.String) - }).annotate({ "description": "An output message item" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), - "summary": Schema.Array(ReasoningSummaryText), - "encrypted_content": Schema.optionalKey(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "signature": Schema.optionalKey( - Schema.String.annotate({ "description": "A signature for the reasoning content, used for verification" }) - ), - "format": Schema.optionalKey( - Schema.Literals([ - "unknown", - "openai-responses-v1", - "azure-openai-responses-v1", - "xai-responses-v1", - "anthropic-claude-v1", - "google-gemini-v1" - ]).annotate({ "description": "The format of the reasoning content" }) - ), - "result": Schema.optionalKey(Schema.String) - }).annotate({ "description": "An output item containing reasoning" }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "name": Schema.String, - "arguments": Schema.String, - "call_id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), - "result": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), - "result": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Never, - "id": Schema.String, - "queries": Schema.Array(Schema.String), - "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), - "result": Schema.optionalKey(Schema.String) - }), - Schema.Struct({ - "type": Schema.Literal("image_generation_call"), - "id": Schema.String, - "result": Schema.optionalKey(Schema.String), - "status": Schema.Union([ - Schema.Literal("in_progress"), - Schema.Literal("completed"), - Schema.Literal("generating"), - Schema.Literal("failed") - ]) - }) - ]).annotate({ "description": "An output item from the response" }) - ])), - "user": Schema.optionalKey(Schema.String), - "output_text": Schema.optionalKey(Schema.String), - "prompt_cache_key": Schema.optionalKey(Schema.String), - "safety_identifier": Schema.optionalKey(Schema.String), - "error": ResponsesErrorField, - "incomplete_details": OpenAIResponsesIncompleteDetails, - "usage": Schema.optionalKey( - Schema.Struct({ - "input_tokens": Schema.Number.check(Schema.isFinite()), - "input_tokens_details": Schema.Struct({ "cached_tokens": Schema.Number.check(Schema.isFinite()) }), - "output_tokens": Schema.Number.check(Schema.isFinite()), - "output_tokens_details": Schema.Struct({ "reasoning_tokens": Schema.Number.check(Schema.isFinite()) }), - "total_tokens": Schema.Number.check(Schema.isFinite()), - "cost": Schema.optionalKey( - Schema.Number.annotate({ "description": "Cost of the completion" }).check(Schema.isFinite()) - ), - "is_byok": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Whether a request was made using a Bring Your Own Key configuration" - }) - ), - "cost_details": Schema.optionalKey( - Schema.Struct({ - "upstream_inference_cost": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "upstream_inference_input_cost": Schema.Number.check(Schema.isFinite()), - "upstream_inference_output_cost": Schema.Number.check(Schema.isFinite()) - }) ) - }).annotate({ "description": "Token usage information for the response" }) - ), - "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "temperature": Schema.Number.check(Schema.isFinite()), - "top_p": Schema.Number.check(Schema.isFinite()), - "presence_penalty": Schema.Number.check(Schema.isFinite()), - "frequency_penalty": Schema.Number.check(Schema.isFinite()), - "instructions": OpenAIResponsesInput, - "metadata": OpenResponsesRequestMetadata, - "tools": Schema.Array( - Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("function"), - "name": Schema.String, - "description": Schema.optionalKey(Schema.String), - "strict": Schema.optionalKey(Schema.Boolean), - "parameters": Schema.Struct({}) - }).annotate({ "description": "Function tool definition" }), - OpenResponsesWebSearchPreviewTool, - OpenResponsesWebSearchPreview20250311Tool, - OpenResponsesWebSearchTool, - OpenResponsesWebSearch20250826Tool - ], { mode: "oneOf" }) + }) ), - "tool_choice": OpenAIResponsesToolChoice, - "parallel_tool_calls": Schema.Boolean, - "prompt": Schema.optionalKey(OpenAIResponsesPrompt), - "background": Schema.optionalKey(Schema.Boolean), - "previous_response_id": Schema.optionalKey(Schema.String), - "reasoning": Schema.optionalKey(OpenAIResponsesReasoningConfig), - "service_tier": Schema.optionalKey(OpenAIResponsesServiceTier), - "store": Schema.optionalKey(Schema.Boolean), - "truncation": Schema.optionalKey(OpenAIResponsesTruncation), - "text": Schema.optionalKey(ResponseTextConfig) -}).annotate({ "description": "Complete non-streaming response from the Responses API" }) -export type OpenResponsesRequest = { - readonly "input"?: OpenResponsesInput - readonly "instructions"?: string - readonly "metadata"?: OpenResponsesRequestMetadata - readonly "tools"?: ReadonlyArray< - | { - readonly "type": "function" - readonly "name": string - readonly "description"?: string - readonly "strict"?: boolean - readonly "parameters": {} - } - | OpenResponsesWebSearchPreviewTool - | OpenResponsesWebSearchPreview20250311Tool - | OpenResponsesWebSearchTool - | OpenResponsesWebSearch20250826Tool - > - readonly "tool_choice"?: OpenAIResponsesToolChoice - readonly "parallel_tool_calls"?: boolean - readonly "model"?: string - readonly "models"?: ReadonlyArray - readonly "text"?: OpenResponsesResponseText - readonly "reasoning"?: OpenResponsesReasoningConfig - readonly "max_output_tokens"?: number - readonly "temperature"?: number - readonly "top_p"?: number - readonly "top_logprobs"?: number - readonly "max_tool_calls"?: number - readonly "presence_penalty"?: number - readonly "frequency_penalty"?: number - readonly "top_k"?: number - readonly "image_config"?: {} - readonly "modalities"?: ReadonlyArray - readonly "prompt_cache_key"?: string - readonly "previous_response_id"?: string - readonly "prompt"?: OpenAIResponsesPrompt - readonly "include"?: ReadonlyArray - readonly "background"?: boolean - readonly "safety_identifier"?: string - readonly "store"?: false - readonly "service_tier"?: "auto" - readonly "truncation"?: "auto" | "disabled" - readonly "stream"?: boolean - readonly "provider"?: { - readonly "allow_fallbacks"?: boolean - readonly "require_parameters"?: boolean - readonly "data_collection"?: DataCollection - readonly "zdr"?: boolean - readonly "enforce_distillable_text"?: boolean - readonly "order"?: ReadonlyArray - readonly "only"?: ReadonlyArray - readonly "ignore"?: ReadonlyArray - readonly "quantizations"?: ReadonlyArray - readonly "sort"?: ProviderSort | ProviderSortConfig | unknown - readonly "max_price"?: { - readonly "prompt"?: BigNumberUnion - readonly "completion"?: string - readonly "image"?: string - readonly "audio"?: string - readonly "request"?: string - } - readonly "preferred_min_throughput"?: PreferredMinThroughput - readonly "preferred_max_latency"?: PreferredMaxLatency - } - readonly "plugins"?: ReadonlyArray< - | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } - | { readonly "id": "moderation" } - | { - readonly "id": "web" - readonly "enabled"?: boolean - readonly "max_results"?: number - readonly "search_prompt"?: string - readonly "engine"?: WebSearchEngine - } - | { readonly "id": "file-parser"; readonly "enabled"?: boolean; readonly "pdf"?: PDFParserOptions } - | { readonly "id": "response-healing"; readonly "enabled"?: boolean } - > - readonly "route"?: "fallback" | "sort" - readonly "user"?: string - readonly "session_id"?: string - readonly "trace"?: { - readonly "trace_id"?: string - readonly "trace_name"?: string - readonly "span_name"?: string - readonly "generation_name"?: string - readonly "parent_span_id"?: string - } + "type": Schema.Literal("file_search"), + "vector_store_ids": Schema.Array(Schema.String) +}).annotate({ "description": "File search tool configuration", "identifier": "FileSearchServerTool" }) +export type ConflictResponse = { + readonly "error": ConflictResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null } -export const OpenResponsesRequest = Schema.Struct({ - "input": Schema.optionalKey(OpenResponsesInput), - "instructions": Schema.optionalKey(Schema.String), - "metadata": Schema.optionalKey(OpenResponsesRequestMetadata), - "tools": Schema.optionalKey( - Schema.Array( - Schema.Union([ - Schema.Struct({ - "type": Schema.Literal("function"), - "name": Schema.String, - "description": Schema.optionalKey(Schema.String), - "strict": Schema.optionalKey(Schema.Boolean), - "parameters": Schema.Struct({}) - }).annotate({ "description": "Function tool definition" }), - OpenResponsesWebSearchPreviewTool, - OpenResponsesWebSearchPreview20250311Tool, - OpenResponsesWebSearchTool, - OpenResponsesWebSearch20250826Tool - ], { mode: "oneOf" }) - ) +export const ConflictResponse = Schema.Struct({ + "error": ConflictResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "tool_choice": Schema.optionalKey(OpenAIResponsesToolChoice), - "parallel_tool_calls": Schema.optionalKey(Schema.Boolean), - "model": Schema.optionalKey(Schema.String), - "models": Schema.optionalKey(Schema.Array(Schema.String)), - "text": Schema.optionalKey(OpenResponsesResponseText), - "reasoning": Schema.optionalKey(OpenResponsesReasoningConfig), - "max_output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "temperature": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(2)) - ), - "top_p": Schema.optionalKey(Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0))), - "top_logprobs": Schema.optionalKey( - Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)).check(Schema.isLessThanOrEqualTo(20)) - ), - "max_tool_calls": Schema.optionalKey(Schema.Number.check(Schema.isInt())), - "presence_penalty": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) - ), - "frequency_penalty": Schema.optionalKey( - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check(Schema.isLessThanOrEqualTo(2)) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Conflict - Resource conflict or concurrent modification", + "identifier": "ConflictResponse" +}) +export type BashServerToolEnvironment = ContainerAutoEnvironment | ContainerReferenceEnvironment +export const BashServerToolEnvironment = Schema.Union([ContainerAutoEnvironment, ContainerReferenceEnvironment], { + mode: "oneOf" +}).annotate({ + "description": "Execution environment for the bash server tool.", + "identifier": "BashServerToolEnvironment" +}) +export type ShellServerToolEnvironment = ContainerAutoEnvironment | ContainerReferenceEnvironment +export const ShellServerToolEnvironment = Schema.Union([ContainerAutoEnvironment, ContainerReferenceEnvironment], { + mode: "oneOf" +}).annotate({ + "description": + "Server-side execution environment for the shell tool. Only container-backed environments are supported; \"local\" shells are not.", + "identifier": "ShellServerToolEnvironment" +}) +export type ContentFilterEntry = { + readonly "action": ContentFilterAction + readonly "label"?: string | null + readonly "pattern": string +} +export const ContentFilterEntry = Schema.Struct({ + "action": ContentFilterAction, + "label": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })), + Schema.Null + ]).annotate({ "description": "Optional label used in redaction placeholders or error messages" }) ), - "top_k": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "image_config": Schema.optionalKey( - Schema.Struct({}).annotate({ - "description": - "Provider-specific image configuration options. Keys and values vary by model/provider. See https://openrouter.ai/docs/features/multimodal/image-generation for more details." + "pattern": Schema.String.annotate({ "description": "A regex pattern to match against request content" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}).annotate({ + "description": "A custom regex content filter that scans request messages for matching patterns.", + "identifier": "ContentFilterEntry" +}) +export type InputReference = ContentPartImage | ContentPartAudio | ContentPartVideo +export const InputReference = Schema.Union([ContentPartImage, ContentPartAudio, ContentPartVideo], { mode: "oneOf" }) + .annotate({ + "description": + "A reference asset used to guide video generation. Image references are supported by all providers; audio and video references are only honored by providers that support them (currently BytePlus Seedance 2.0).", + "identifier": "InputReference" + }) +export type ContextCompressionPlugin = { + readonly "enabled"?: boolean + readonly "engine"?: ContextCompressionEngine + readonly "id": "context-compression" +} +export const ContextCompressionPlugin = Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the context-compression plugin for this request. Defaults to true." }) ), - "modalities": Schema.optionalKey( - Schema.Array(ResponsesOutputModality).annotate({ - "description": "Output modalities for the response. Supported values are \"text\" and \"image\"." - }) + "engine": Schema.optionalKey(ContextCompressionEngine), + "id": Schema.Literal("context-compression") +}).annotate({ "identifier": "ContextCompressionPlugin" }) +export type CostDetails = Objects_8 | null +export const CostDetails = Schema.Union([Objects_8, Schema.Null]).annotate({ + "description": "Breakdown of upstream inference costs", + "identifier": "CostDetails" +}) +export type Arrays_9 = ReadonlyArray +export const Arrays_9 = Schema.Array(DABenchmarkEntry).annotate({ + "description": "Design Arena ELO rankings across arena+category pairs." +}) +export type DatetimeServerTool = { + readonly "parameters"?: DatetimeServerToolConfig + readonly "type": "openrouter:datetime" +} +export const DatetimeServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(DatetimeServerToolConfig), + "type": Schema.Literal("openrouter:datetime") +}).annotate({ + "description": "OpenRouter built-in server tool: returns the current date and time", + "identifier": "DatetimeServerTool" +}) +export type EdgeNetworkTimeoutResponse = { + readonly "error": EdgeNetworkTimeoutResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const EdgeNetworkTimeoutResponse = Schema.Struct({ + "error": EdgeNetworkTimeoutResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "prompt_cache_key": Schema.optionalKey(Schema.String), - "previous_response_id": Schema.optionalKey(Schema.String), - "prompt": Schema.optionalKey(OpenAIResponsesPrompt), - "include": Schema.optionalKey(Schema.Array(OpenAIResponsesIncludable)), - "background": Schema.optionalKey(Schema.Boolean), - "safety_identifier": Schema.optionalKey(Schema.String), - "store": Schema.optionalKey(Schema.Literal(false)), - "service_tier": Schema.optionalKey(Schema.Literal("auto")), - "truncation": Schema.optionalKey(Schema.Literals(["auto", "disabled"])), - "stream": Schema.optionalKey(Schema.Boolean), - "provider": Schema.optionalKey( - Schema.Struct({ - "allow_fallbacks": Schema.optionalKey(Schema.Boolean.annotate({ - "description": - "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" - })), - "require_parameters": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "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." - }) - ), - "data_collection": Schema.optionalKey(DataCollection), - "zdr": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." - }) - ), - "enforce_distillable_text": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": - "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." - }) - ), - "order": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "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." - }) - ), - "only": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." - }) - ), - "ignore": Schema.optionalKey( - Schema.Array(Schema.Union([ProviderName, Schema.String])).annotate({ - "description": - "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." - }) - ), - "quantizations": Schema.optionalKey( - Schema.Array(Quantization).annotate({ - "description": "A list of quantization levels to filter the provider by." - }) - ), - "sort": Schema.optionalKey( - Schema.Union([ProviderSort, ProviderSortConfig, Schema.Unknown]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }) - ), - "max_price": Schema.optionalKey( - Schema.Struct({ - "prompt": Schema.optionalKey(BigNumberUnion), - "completion": Schema.optionalKey( - Schema.String.annotate({ "description": "Price per million completion tokens" }) - ), - "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price per image" })), - "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price per audio unit" })), - "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price per request" })) - }).annotate({ - "description": - "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." - }) - ), - "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), - "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency) - }).annotate({ - "description": "When multiple model providers are available, optionally indicate your routing preference." - }) - ), - "plugins": Schema.optionalKey( - Schema.Array(Schema.Union([ - Schema.Struct({ - "id": Schema.Literal("auto-router"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the auto-router plugin for this request. Defaults to true." - }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ - "description": - "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." - }) - ) - }), - Schema.Struct({ "id": Schema.Literal("moderation") }), - Schema.Struct({ - "id": Schema.Literal("web"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the web-search plugin for this request. Defaults to true." - }) - ), - "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "search_prompt": Schema.optionalKey(Schema.String), - "engine": Schema.optionalKey(WebSearchEngine) - }), - Schema.Struct({ - "id": Schema.Literal("file-parser"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the file-parser plugin for this request. Defaults to true." - }) - ), - "pdf": Schema.optionalKey(PDFParserOptions) - }), - Schema.Struct({ - "id": Schema.Literal("response-healing"), - "enabled": Schema.optionalKey( - Schema.Boolean.annotate({ - "description": "Set to false to disable the response-healing plugin for this request. Defaults to true." - }) - ) - }) - ], { mode: "oneOf" })).annotate({ - "description": "Plugins you want to enable for this request, including their settings." - }) - ), - "route": Schema.optionalKey( - Schema.Literals(["fallback", "sort"]).annotate({ - "description": - "**DEPRECATED** Use providers.sort.partition instead. Backwards-compatible alias for providers.sort.partition. Accepts legacy values: \"fallback\" (maps to \"model\"), \"sort\" (maps to \"none\")." - }) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Infrastructure Timeout - Provider request timed out at edge network", + "identifier": "EdgeNetworkTimeoutResponse" +}) +export type EndpointsMetadata = { readonly "available": ReadonlyArray; readonly "total": number } +export const EndpointsMetadata = Schema.Struct({ + "available": Schema.Array(EndpointInfo), + "total": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "EndpointsMetadata" }) +export type FileListResponse = { + readonly "cursor": string | null + readonly "data": ReadonlyArray + readonly "first_id": string | null + readonly "has_more": boolean + readonly "last_id": string | null +} +export const FileListResponse = Schema.Struct({ + "cursor": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Opaque cursor for the next page; null when there are no more results." + }), + "data": Schema.Array(FileMetadata), + "first_id": Schema.Union([Schema.String, Schema.Null]), + "has_more": Schema.Boolean, + "last_id": Schema.Union([Schema.String, Schema.Null]) +}).annotate({ + "description": "A page of files belonging to the requesting workspace.", + "identifier": "FileListResponse" +}) +export type FilesServerTool = { readonly "parameters"?: FilesServerToolConfig; readonly "type": "openrouter:files" } +export const FilesServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(FilesServerToolConfig), + "type": Schema.Literal("openrouter:files") +}).annotate({ + "description": + "OpenRouter built-in server tool: read, write, edit, and list workspace files via the Files API. Requires the `x-openrouter-file-ids: openrouter` request header.", + "identifier": "FilesServerTool" +}) +export type ForbiddenResponse = { + readonly "error": ForbiddenResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const ForbiddenResponse = Schema.Struct({ + "error": ForbiddenResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "user": Schema.optionalKey( - Schema.String.annotate({ - "description": - "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." - }).check(Schema.isMaxLength(128)) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Forbidden - Authentication successful but insufficient permissions", + "identifier": "ForbiddenResponse" +}) +export type Formats = FormatTextConfig | FormatJsonObjectConfig | FormatJsonSchemaConfig +export const Formats = Schema.Union([FormatTextConfig, FormatJsonObjectConfig, FormatJsonSchemaConfig]).annotate({ + "description": "Text response format configuration", + "identifier": "Formats" +}) +export type FusionAnalysisResult = { + readonly "blind_spots": Arrays_1 + readonly "consensus": Arrays_2 + readonly "contradictions": Arrays_3 + readonly "partial_coverage": Arrays_4 + readonly "unique_insights": Arrays_5 +} +export const FusionAnalysisResult = Schema.Struct({ + "blind_spots": Arrays_1, + "consensus": Arrays_2, + "contradictions": Arrays_3, + "partial_coverage": Arrays_4, + "unique_insights": Arrays_5 +}).annotate({ + "description": "Structured analysis produced by the fusion judge model.", + "identifier": "FusionAnalysisResult" +}) +export type GenerationContentResponse = { readonly "data": GenerationContentData } +export const GenerationContentResponse = Schema.Struct({ "data": GenerationContentData }).annotate({ + "description": "Stored prompt and completion content for a generation", + "identifier": "GenerationContentResponse" +}) +export type GoneResponse = { + readonly "error": GoneResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const GoneResponse = Schema.Struct({ + "error": GoneResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "session_id": Schema.optionalKey( - Schema.String.annotate({ - "description": - "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." - }).check(Schema.isMaxLength(128)) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Gone - Endpoint has been permanently removed or deprecated", + "identifier": "GoneResponse" +}) +export type ImageGenerationServerTool_OpenRouter = { + readonly "parameters"?: ImageGenerationServerToolConfig + readonly "type": "openrouter:image_generation" +} +export const ImageGenerationServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(ImageGenerationServerToolConfig), + "type": Schema.Literal("openrouter:image_generation") +}).annotate({ + "description": "OpenRouter built-in server tool: generates images from text prompts using an image generation model", + "identifier": "ImageGenerationServerTool_OpenRouter" +}) +export type OutputImageGenerationCallItem = { + readonly "id": string + readonly "result"?: string | null + readonly "status": ImageGenerationStatus + readonly "type": "image_generation_call" + readonly "prompt"?: string +} +export const OutputImageGenerationCallItem = Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": ImageGenerationStatus, + "type": Schema.Literal("image_generation_call"), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt (possibly rewritten) that the image was generated from." }) + ) +}).annotate({ "identifier": "OutputImageGenerationCallItem" }) +export type OutputItemImageGenerationCall = { + readonly "id": string + readonly "result"?: string | null + readonly "status": ImageGenerationStatus + readonly "type": "image_generation_call" +} +export const OutputItemImageGenerationCall = Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": ImageGenerationStatus, + "type": Schema.Literal("image_generation_call") +}).annotate({ "identifier": "OutputItemImageGenerationCall" }) +export type ImageModelArchitecture = { + readonly "input_modalities": ReadonlyArray + readonly "output_modalities": ReadonlyArray +} +export const ImageModelArchitecture = Schema.Struct({ + "input_modalities": Schema.Array(ImageInputModality).annotate({ "description": "Supported input modalities" }), + "output_modalities": Schema.Array(ImageOutputModality).annotate({ "description": "Supported output modalities" }) +}).annotate({ "identifier": "ImageModelArchitecture" }) +export type InternalServerResponse = { + readonly "error": InternalServerResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const InternalServerResponse = Schema.Struct({ + "error": InternalServerResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "trace": Schema.optionalKey( - Schema.Struct({ - "trace_id": Schema.optionalKey(Schema.String), - "trace_name": Schema.optionalKey(Schema.String), - "span_name": Schema.optionalKey(Schema.String), - "generation_name": Schema.optionalKey(Schema.String), - "parent_span_id": Schema.optionalKey(Schema.String) - }).annotate({ - "description": - "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." - }) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Internal Server Error - Unexpected server error", + "identifier": "InternalServerResponse" +}) +export type ListKeyAssignmentsResponse = { + readonly "data": ReadonlyArray + readonly "total_count": number +} +export const ListKeyAssignmentsResponse = Schema.Struct({ + "data": Schema.Array(KeyAssignment).annotate({ "description": "List of key assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of key assignments for this guardrail" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) ) -}).annotate({ "description": "Request schema for Responses endpoint" }) -export type ChatGenerationParams = { - readonly "provider"?: { - readonly "allow_fallbacks"?: boolean | null - readonly "require_parameters"?: boolean | null - readonly "data_collection"?: "deny" | "allow" | null - readonly "zdr"?: boolean | null - readonly "enforce_distillable_text"?: boolean | null - readonly "order"?: __schema5 | null - readonly "only"?: __schema5 | null - readonly "ignore"?: __schema5 | null - readonly "quantizations"?: - | ReadonlyArray<"int4" | "int8" | "fp4" | "fp6" | "fp8" | "fp16" | "bf16" | "fp32" | "unknown"> - | null - readonly "sort"?: ProviderSortUnion | null - readonly "max_price"?: { - readonly "prompt"?: __schema11 | ModelName | __schema13 - readonly "completion"?: __schema11 | ModelName | __schema13 - readonly "image"?: __schema14 - readonly "audio"?: __schema14 - readonly "request"?: __schema14 - } - readonly "preferred_min_throughput"?: number | { - readonly "p50"?: number | null - readonly "p75"?: number | null - readonly "p90"?: number | null - readonly "p99"?: number | null - } | null - readonly "preferred_max_latency"?: number | { - readonly "p50"?: number | null - readonly "p75"?: number | null - readonly "p90"?: number | null - readonly "p99"?: number | null - } | null - } | null - readonly "plugins"?: ReadonlyArray< - | { readonly "id": "auto-router"; readonly "enabled"?: boolean; readonly "allowed_models"?: ReadonlyArray } - | { readonly "id": "moderation" } - | { - readonly "id": "web" - readonly "enabled"?: boolean - readonly "max_results"?: number - readonly "search_prompt"?: string - readonly "engine"?: "native" | "exa" - } - | { - readonly "id": "file-parser" - readonly "enabled"?: boolean - readonly "pdf"?: { readonly "engine"?: "mistral-ocr" | "pdf-text" | "native" } - } - | { readonly "id": "response-healing"; readonly "enabled"?: boolean } - > - readonly "route"?: "fallback" | "sort" | null - readonly "user"?: string - readonly "session_id"?: string - readonly "trace"?: { - readonly "trace_id"?: string - readonly "trace_name"?: string - readonly "span_name"?: string - readonly "generation_name"?: string - readonly "parent_span_id"?: string - } - readonly "messages": ReadonlyArray - readonly "model"?: ModelName - readonly "models"?: ReadonlyArray - readonly "frequency_penalty"?: number | null - readonly "logit_bias"?: {} | null - readonly "logprobs"?: boolean | null - readonly "top_logprobs"?: number | null - readonly "max_completion_tokens"?: number | null - readonly "max_tokens"?: number | null - readonly "metadata"?: {} - readonly "presence_penalty"?: number | null - readonly "reasoning"?: { - readonly "effort"?: "xhigh" | "high" | "medium" | "low" | "minimal" | "none" | null - readonly "summary"?: ReasoningSummaryVerbosity | null - } - readonly "response_format"?: - | { readonly "type": "text" } - | { readonly "type": "json_object" } - | ResponseFormatJSONSchema - | ResponseFormatTextGrammar - | { readonly "type": "python" } - readonly "seed"?: number | null - readonly "stop"?: string | ReadonlyArray | null - readonly "stream"?: boolean - readonly "stream_options"?: ChatStreamOptions | null - readonly "temperature"?: number | null - readonly "parallel_tool_calls"?: boolean | null - readonly "tool_choice"?: ToolChoiceOption - readonly "tools"?: ReadonlyArray - readonly "top_p"?: number | null - readonly "debug"?: { readonly "echo_upstream_body"?: boolean } - readonly "image_config"?: {} - readonly "modalities"?: ReadonlyArray<"text" | "image"> +}).annotate({ "identifier": "ListKeyAssignmentsResponse" }) +export type Legacy_ChatContentVideo = { + readonly "type": "input_video" + readonly "video_url": Legacy_ChatContentVideoInput } -export const ChatGenerationParams = Schema.Struct({ - "provider": Schema.optionalKey( +export const Legacy_ChatContentVideo = Schema.Struct({ + "type": Schema.Literal("input_video"), + "video_url": Legacy_ChatContentVideoInput +}).annotate({ + "description": "Video input content part (legacy format - deprecated)", + "identifier": "Legacy_ChatContentVideo" +}) +export type ListMemberAssignmentsResponse = { + readonly "data": ReadonlyArray + readonly "total_count": number +} +export const ListMemberAssignmentsResponse = Schema.Struct({ + "data": Schema.Array(MemberAssignment).annotate({ "description": "List of member assignments" }), + "total_count": Schema.Number.annotate({ "description": "Total number of member assignments" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "ListMemberAssignmentsResponse" }) +export type MessagesOutputConfig = { + readonly "effort"?: "low" | "medium" | "high" | "xhigh" | "max" | null + readonly "format"?: Union_10 + readonly "task_budget"?: Union_11 +} +export const MessagesOutputConfig = Schema.Struct({ + "effort": Schema.optionalKey( Schema.Union([ - Schema.Struct({ - "allow_fallbacks": Schema.optionalKey( - Schema.Union([Schema.Boolean, Schema.Null]).annotate({ - "description": - "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" - }) - ), - "require_parameters": Schema.optionalKey( - Schema.Union([Schema.Boolean, Schema.Null]).annotate({ - "description": - "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." - }) - ), - "data_collection": Schema.optionalKey( - Schema.Union([Schema.Literals(["deny", "allow"]), Schema.Null]).annotate({ - "description": - "Data collection setting. If no available model provider meets the requirement, your request will return an error.\n- allow: (default) allow providers which store user data non-transiently and may train on it\n\n- deny: use only providers which do not collect user data." - }) - ), - "zdr": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - "enforce_distillable_text": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - "order": Schema.optionalKey( - Schema.Union([__schema5, Schema.Null]).annotate({ - "description": - "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." - }) - ), - "only": Schema.optionalKey( - Schema.Union([__schema5, Schema.Null]).annotate({ - "description": - "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." - }) - ), - "ignore": Schema.optionalKey( - Schema.Union([__schema5, Schema.Null]).annotate({ - "description": - "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." - }) - ), - "quantizations": Schema.optionalKey( - Schema.Union([ - Schema.Array(Schema.Literals(["int4", "int8", "fp4", "fp6", "fp8", "fp16", "bf16", "fp32", "unknown"])), - Schema.Null - ]).annotate({ "description": "A list of quantization levels to filter the provider by." }) - ), - "sort": Schema.optionalKey( - Schema.Union([ProviderSortUnion, Schema.Null]).annotate({ - "description": - "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." - }) - ), - "max_price": Schema.optionalKey( - Schema.Struct({ - "prompt": Schema.optionalKey(Schema.Union([__schema11, ModelName, __schema13])), - "completion": Schema.optionalKey(Schema.Union([__schema11, ModelName, __schema13])), - "image": Schema.optionalKey(__schema14), - "audio": Schema.optionalKey(__schema14), - "request": Schema.optionalKey(__schema14) - }).annotate({ - "description": - "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." - }) - ), - "preferred_min_throughput": Schema.optionalKey( - Schema.Union([ - Schema.Union([ - Schema.Number.check(Schema.isFinite()), - Schema.Struct({ - "p50": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p75": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p90": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p99": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])) - }) - ]), - Schema.Null - ]).annotate({ - "description": - "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_max_latency": Schema.optionalKey( - Schema.Union([ - Schema.Union([ - Schema.Number.check(Schema.isFinite()), - Schema.Struct({ - "p50": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p75": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p90": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])), - "p99": Schema.optionalKey(Schema.Union([Schema.Number.check(Schema.isFinite()), Schema.Null])) - }) - ]), - Schema.Null - ]).annotate({ - "description": - "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." - }) - ) - }), + Schema.Literal("low"), + Schema.Literal("medium"), + Schema.Literal("high"), + Schema.Literal("xhigh"), + Schema.Literal("max"), Schema.Null ]).annotate({ - "description": "When multiple model providers are available, optionally indicate your routing preference." - }) - ), - "plugins": Schema.optionalKey( - Schema.Array( - Schema.Union([ - Schema.Struct({ - "id": Schema.Literal("auto-router"), - "enabled": Schema.optionalKey(Schema.Boolean), - "allowed_models": Schema.optionalKey(Schema.Array(Schema.String)) - }), - Schema.Struct({ "id": Schema.Literal("moderation") }), - Schema.Struct({ - "id": Schema.Literal("web"), - "enabled": Schema.optionalKey(Schema.Boolean), - "max_results": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "search_prompt": Schema.optionalKey(Schema.String), - "engine": Schema.optionalKey(Schema.Literals(["native", "exa"])) - }), - Schema.Struct({ - "id": Schema.Literal("file-parser"), - "enabled": Schema.optionalKey(Schema.Boolean), - "pdf": Schema.optionalKey( - Schema.Struct({ "engine": Schema.optionalKey(Schema.Literals(["mistral-ocr", "pdf-text", "native"])) }) - ) - }), - Schema.Struct({ "id": Schema.Literal("response-healing"), "enabled": Schema.optionalKey(Schema.Boolean) }) - ], { mode: "oneOf" }) - ).annotate({ "description": "Plugins you want to enable for this request, including their settings." }) - ), - "route": Schema.optionalKey(Schema.Union([Schema.Literals(["fallback", "sort"]), Schema.Null])), - "user": Schema.optionalKey(Schema.String), - "session_id": Schema.optionalKey( - Schema.String.annotate({ - "description": - "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." - }).check(Schema.isMaxLength(128)) - ), - "trace": Schema.optionalKey( - Schema.Struct({ - "trace_id": Schema.optionalKey(Schema.String), - "trace_name": Schema.optionalKey(Schema.String), - "span_name": Schema.optionalKey(Schema.String), - "generation_name": Schema.optionalKey(Schema.String), - "parent_span_id": Schema.optionalKey(Schema.String) - }).annotate({ "description": - "Metadata for observability and tracing. Known keys (trace_id, trace_name, span_name, generation_name, parent_span_id) have special handling. Additional keys are passed through as custom metadata to configured broadcast destinations." + "How much effort the model should put into its response. Higher effort levels may result in more thorough analysis but take longer. Valid values are `low`, `medium`, `high`, `xhigh`, or `max`." }) ), - "messages": Schema.Array(Message).check(Schema.isMinLength(1)), - "model": Schema.optionalKey(ModelName), - "models": Schema.optionalKey(Schema.Array(ModelName)), - "frequency_penalty": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check( - Schema.isLessThanOrEqualTo(2) - ), - Schema.Null - ]) + "format": Schema.optionalKey(Union_10), + "task_budget": Schema.optionalKey(Union_11) +}).annotate({ + "description": + "Configuration for controlling output behavior. Supports the effort parameter and structured output format.", + "identifier": "MessagesOutputConfig" +}) +export type ChatModelNames = ReadonlyArray +export const ChatModelNames = Schema.Array( + Schema.suspend((): Schema.Codec => ModelName).annotate({ + "description": "Available OpenRouter chat completion models" + }) +).annotate({ "description": "Models to use for completion", "identifier": "ChatModelNames" }) +export type ContentPartInputAudio = { readonly "input_audio": MultimodalMedia; readonly "type": "input_audio" } +export const ContentPartInputAudio = Schema.Struct({ + "input_audio": MultimodalMedia, + "type": Schema.Literal("input_audio") +}).annotate({ "identifier": "ContentPartInputAudio" }) +export type ContentPartInputFile = { readonly "input_file": MultimodalMedia; readonly "type": "input_file" } +export const ContentPartInputFile = Schema.Struct({ + "input_file": MultimodalMedia, + "type": Schema.Literal("input_file") +}).annotate({ "identifier": "ContentPartInputFile" }) +export type ContentPartInputVideo = { readonly "input_video": MultimodalMedia; readonly "type": "input_video" } +export const ContentPartInputVideo = Schema.Struct({ + "input_video": MultimodalMedia, + "type": Schema.Literal("input_video") +}).annotate({ "identifier": "ContentPartInputVideo" }) +export type NamespaceTool = { + readonly "description": string + readonly "name": string + readonly "tools": ReadonlyArray + readonly "type": "namespace" +} +export const NamespaceTool = Schema.Struct({ + "description": Schema.String, + "name": Schema.String, + "tools": Schema.Array(Schema.Union([NamespaceFunctionTool, CustomTool])), + "type": Schema.Literal("namespace") +}).annotate({ "description": "Groups function/custom tools under a shared namespace", "identifier": "NamespaceTool" }) +export type NotFoundResponse = { + readonly "error": NotFoundResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const NotFoundResponse = Schema.Struct({ + "error": NotFoundResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "logit_bias": Schema.optionalKey( - Schema.Union([Schema.Struct({}).check(Schema.isPropertyNames(Schema.String)), Schema.Null]) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ "description": "Not Found - Resource does not exist", "identifier": "NotFoundResponse" }) +export type ObservabilityFilterRulesConfig = { + readonly "enabled"?: boolean + readonly "groups": ReadonlyArray + readonly [x: string]: Schema.Json +} | null +export const ObservabilityFilterRulesConfig = Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "enabled": Schema.optionalKey(Schema.Boolean), + "groups": Schema.Array(ObservabilityFilterRuleGroup) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] ), - "logprobs": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - "top_logprobs": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( - Schema.isLessThanOrEqualTo(20) - ), - Schema.Null - ]) + Schema.Null +]).annotate({ + "description": "Optional structured filter rules controlling which events are forwarded.", + "identifier": "ObservabilityFilterRulesConfig" +}) +export type Objects_10 = { + readonly "enabled"?: boolean + readonly "groups": ReadonlyArray + readonly [x: string]: Schema.Json +} +export const Objects_10 = Schema.StructWithRest( + Schema.Struct({ + "enabled": Schema.optionalKey(Schema.Boolean), + "groups": Schema.Array(ObservabilityFilterRuleGroup) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type OpenAIResponsesUsage = { + readonly "input_tokens": number + readonly "input_tokens_details": Objects_15 + readonly "output_tokens": number + readonly "output_tokens_details": Objects_16 + readonly "total_tokens": number +} +export const OpenAIResponsesUsage = Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "input_tokens_details": Objects_15, + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": Objects_16, + "total_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "OpenAIResponsesUsage" }) +export type OpenResponsesLogProbs = { + readonly "bytes"?: ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs"?: ReadonlyArray +} +export const OpenResponsesLogProbs = Schema.Struct({ + "bytes": Schema.optionalKey(Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) ), - "max_completion_tokens": Schema.optionalKey( - Schema.Union([Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]) + "token": Schema.String, + "top_logprobs": Schema.optionalKey(Schema.Array(OpenResponsesTopLogprobs)) +}).annotate({ "description": "Log probability information for a token", "identifier": "OpenResponsesLogProbs" }) +export type ModelArchitecture = { + readonly "input_modalities": ReadonlyArray + readonly "instruct_type"?: InstructType + readonly "modality": string | null + readonly "output_modalities": ReadonlyArray + readonly "tokenizer"?: ModelGroup +} +export const ModelArchitecture = Schema.Struct({ + "input_modalities": Schema.Array(InputModality).annotate({ "description": "Supported input modalities" }), + "instruct_type": Schema.optionalKey(InstructType), + "modality": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Primary modality of the model" }), + "output_modalities": Schema.Array(OutputModality).annotate({ "description": "Supported output modalities" }), + "tokenizer": Schema.optionalKey(ModelGroup) +}).annotate({ "description": "Model architecture information", "identifier": "ModelArchitecture" }) +export type PayloadTooLargeResponse = { + readonly "error": PayloadTooLargeResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const PayloadTooLargeResponse = Schema.Struct({ + "error": PayloadTooLargeResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "max_tokens": Schema.optionalKey( - Schema.Union([Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(1)), Schema.Null]) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Payload Too Large - Request payload exceeds size limits", + "identifier": "PayloadTooLargeResponse" +}) +export type PaymentRequiredResponse = { + readonly "error": PaymentRequiredResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const PaymentRequiredResponse = Schema.Struct({ + "error": PaymentRequiredResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "metadata": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), - "presence_penalty": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(-2)).check( - Schema.isLessThanOrEqualTo(2) - ), - Schema.Null - ]) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Payment Required - Insufficient credits or quota to complete request", + "identifier": "PaymentRequiredResponse" +}) +export type PDFParserOptions = { readonly "engine"?: PDFParserEngine } +export const PDFParserOptions = Schema.Struct({ "engine": Schema.optionalKey(PDFParserEngine) }).annotate({ + "description": "Options for PDF parsing.", + "identifier": "PDFParserOptions" +}) +export type PreferredMaxLatency = number | PercentileLatencyCutoffs | null +export const PreferredMaxLatency = Schema.Union([ + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + PercentileLatencyCutoffs, + Schema.Null +]).annotate({ + "description": + "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.", + "identifier": "PreferredMaxLatency" +}) +export type PreferredMinThroughput = number | PercentileThroughputCutoffs | null +export const PreferredMinThroughput = Schema.Union([ + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + PercentileThroughputCutoffs, + Schema.Null +]).annotate({ + "description": + "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.", + "identifier": "PreferredMinThroughput" +}) +export type PipelineStage = { + readonly "cost_usd"?: number | null + readonly "data"?: {} + readonly "guardrail_id"?: string + readonly "guardrail_scope"?: string + readonly "name": string + readonly "summary"?: string + readonly "type": PipelineStageType +} +export const PipelineStage = Schema.Struct({ + "cost_usd": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) ), - "reasoning": Schema.optionalKey( - Schema.Struct({ - "effort": Schema.optionalKey( - Schema.Union([Schema.Literals(["xhigh", "high", "medium", "low", "minimal", "none"]), Schema.Null]) - ), - "summary": Schema.optionalKey(Schema.Union([ReasoningSummaryVerbosity, Schema.Null])) - }) + "data": Schema.optionalKey(Schema.Struct({})), + "guardrail_id": Schema.optionalKey(Schema.String), + "guardrail_scope": Schema.optionalKey(Schema.String), + "name": Schema.String, + "summary": Schema.optionalKey(Schema.String), + "type": PipelineStageType +}).annotate({ "identifier": "PipelineStage" }) +export type Objects_17 = { + readonly "content": string | ReadonlyArray + readonly "type": "content" + readonly [x: string]: Schema.Json +} +export const Objects_17 = Schema.StructWithRest( + Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(PredictionContentText)]), + "type": Schema.Literal("content") + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type GetPresetVersionResponse = { readonly "data": PresetDesignatedVersion } +export const GetPresetVersionResponse = Schema.Struct({ "data": PresetDesignatedVersion }).annotate({ + "description": "A single version of a preset.", + "identifier": "GetPresetVersionResponse" +}) +export type ListPresetVersionsResponse = { + readonly "data": ReadonlyArray + readonly "total_count": number +} +export const ListPresetVersionsResponse = Schema.Struct({ + "data": Schema.Array(PresetDesignatedVersion), + "total_count": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "description": "A paginated list of preset versions.", "identifier": "ListPresetVersionsResponse" }) +export type Preset = { + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "description": string | null + readonly "designated_version_id": string | null + readonly "id": string + readonly "name": string + readonly "slug": string + readonly "status": PresetStatus + readonly "status_updated_at": string | null + readonly "updated_at": string + readonly "workspace_id": string | null +} +export const Preset = Schema.Struct({ + "created_at": Schema.String, + "creator_user_id": Schema.Union([Schema.String, Schema.Null]), + "description": Schema.Union([Schema.String, Schema.Null]), + "designated_version_id": Schema.Union([Schema.String, Schema.Null]), + "id": Schema.String, + "name": Schema.String, + "slug": Schema.String, + "status": PresetStatus, + "status_updated_at": Schema.Union([Schema.String, Schema.Null]), + "updated_at": Schema.String, + "workspace_id": Schema.Union([Schema.String, Schema.Null]) +}).annotate({ "description": "A preset without version details.", "identifier": "Preset" }) +export type PresetWithDesignatedVersion = { + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "description": string | null + readonly "designated_version_id": string | null + readonly "id": string + readonly "name": string + readonly "slug": string + readonly "status": PresetStatus + readonly "status_updated_at": string | null + readonly "updated_at": string + readonly "workspace_id": string | null + readonly "designated_version": PresetDesignatedVersion +} +export const PresetWithDesignatedVersion = Schema.Struct({ + "created_at": Schema.String, + "creator_user_id": Schema.Union([Schema.String, Schema.Null]), + "description": Schema.Union([Schema.String, Schema.Null]), + "designated_version_id": Schema.Union([Schema.String, Schema.Null]), + "id": Schema.String, + "name": Schema.String, + "slug": Schema.String, + "status": PresetStatus, + "status_updated_at": Schema.Union([Schema.String, Schema.Null]), + "updated_at": Schema.String, + "workspace_id": Schema.Union([Schema.String, Schema.Null]), + "designated_version": PresetDesignatedVersion +}).annotate({ + "description": "A preset with its currently designated version.", + "identifier": "PresetWithDesignatedVersion" +}) +export type Preview_WebSearchUserLocation = Objects_18 | null +export const Preview_WebSearchUserLocation = Schema.Union([Objects_18, Schema.Null]).annotate({ + "identifier": "Preview_WebSearchUserLocation" +}) +export type PublicPricing = { + readonly "audio"?: string + readonly "audio_output"?: string + readonly "completion": string + readonly "discount"?: number + readonly "image"?: string + readonly "image_output"?: string + readonly "image_token"?: string + readonly "input_audio_cache"?: string + readonly "input_cache_read"?: string + readonly "input_cache_write"?: string + readonly "input_cache_write_1h"?: string + readonly "internal_reasoning"?: string + readonly "overrides"?: ReadonlyArray + readonly "prompt": string + readonly "request"?: string + readonly "web_search"?: string +} +export const PublicPricing = Schema.Struct({ + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per audio input token" })), + "audio_output": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per audio output token" })), + "completion": Schema.String.annotate({ "description": "Price in USD per token for completion (output) generation" }), + "discount": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Fractional discount applied to this endpoint's pricing; the price is multiplied by (1 - discount) (0 = no discount, 1 = free)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) ), - "response_format": Schema.optionalKey( - Schema.Union([ - Schema.Struct({ "type": Schema.Literal("text") }), - Schema.Struct({ "type": Schema.Literal("json_object") }), - ResponseFormatJSONSchema, - ResponseFormatTextGrammar, - Schema.Struct({ "type": Schema.Literal("python") }) - ], { mode: "oneOf" }) + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per input image" })), + "image_output": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per output image" })), + "image_token": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per image token" })), + "input_audio_cache": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per cached audio input token" }) ), - "seed": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(-9007199254740991)).check( - Schema.isLessThanOrEqualTo(9007199254740991) - ), - Schema.Null - ]) + "input_cache_read": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per cached input token (read)" }) ), - "stop": Schema.optionalKey( - Schema.Union([Schema.Union([Schema.String, Schema.Array(ModelName).check(Schema.isMaxLength(4))]), Schema.Null]) + "input_cache_write": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Price per cache-write token, in USD per token. For providers with multiple cache TTLs (e.g. Anthropic), this is the default (5-minute) cache-write rate." + }) ), - "stream": Schema.optionalKey(Schema.Boolean), - "stream_options": Schema.optionalKey(Schema.Union([ChatStreamOptions, Schema.Null])), - "temperature": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( - Schema.isLessThanOrEqualTo(2) - ), - Schema.Null - ]) + "input_cache_write_1h": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Price per 1-hour cache-write token, in USD per token. Only present for providers that price an extended (1-hour) cache TTL separately, such as Anthropic." + }) ), - "parallel_tool_calls": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), - "tool_choice": Schema.optionalKey(ToolChoiceOption), - "tools": Schema.optionalKey(Schema.Array(ToolDefinitionJson)), - "top_p": Schema.optionalKey( - Schema.Union([ - Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(0)).check( - Schema.isLessThanOrEqualTo(1) - ), - Schema.Null - ]) + "internal_reasoning": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per internal reasoning token" }) + ), + "overrides": Schema.optionalKey( + Schema.Array(PricingOverride).annotate({ + "description": + "Conditional overrides of the base pricing (e.g. long-context or time-based pricing). An entry applies when all of its condition fields (e.g. min_prompt_tokens, or the utc_start/utc_end time window) match the request; among applicable entries, later entries win per key; price keys absent from an entry inherit the base price. The top-level pricing keys always reflect the price that applies under default conditions." + }) ), - "debug": Schema.optionalKey(Schema.Struct({ "echo_upstream_body": Schema.optionalKey(Schema.Boolean) })), - "image_config": Schema.optionalKey(Schema.Struct({}).check(Schema.isPropertyNames(Schema.String))), - "modalities": Schema.optionalKey(Schema.Array(Schema.Literals(["text", "image"]))) + "prompt": Schema.String.annotate({ "description": "Price in USD per token for prompt (input) processing" }), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per request" })), + "web_search": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per web search" })) +}).annotate({ "description": "Pricing information for the model", "identifier": "PublicPricing" }) +export type PromptCacheBreakpoint = Objects_19 | null +export const PromptCacheBreakpoint = Schema.Union([Objects_19, Schema.Null]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically.", + "identifier": "PromptCacheBreakpoint" }) -export type OpenResponsesCreatedEvent = { - readonly "type": "response.created" - readonly "response": OpenAIResponsesNonStreamingResponse - readonly "sequence_number": number -} -export const OpenResponsesCreatedEvent = Schema.Struct({ - "type": Schema.Literal("response.created"), - "response": OpenAIResponsesNonStreamingResponse, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a response is created" }) -export type OpenResponsesInProgressEvent = { - readonly "type": "response.in_progress" - readonly "response": OpenAIResponsesNonStreamingResponse - readonly "sequence_number": number -} -export const OpenResponsesInProgressEvent = Schema.Struct({ - "type": Schema.Literal("response.in_progress"), - "response": OpenAIResponsesNonStreamingResponse, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a response is in progress" }) -export type OpenResponsesCompletedEvent = { - readonly "type": "response.completed" - readonly "response": OpenAIResponsesNonStreamingResponse - readonly "sequence_number": number -} -export const OpenResponsesCompletedEvent = Schema.Struct({ - "type": Schema.Literal("response.completed"), - "response": OpenAIResponsesNonStreamingResponse, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a response has completed successfully" }) -export type OpenResponsesIncompleteEvent = { - readonly "type": "response.incomplete" - readonly "response": OpenAIResponsesNonStreamingResponse - readonly "sequence_number": number -} -export const OpenResponsesIncompleteEvent = Schema.Struct({ - "type": Schema.Literal("response.incomplete"), - "response": OpenAIResponsesNonStreamingResponse, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a response is incomplete" }) -export type OpenResponsesFailedEvent = { - readonly "type": "response.failed" - readonly "response": OpenAIResponsesNonStreamingResponse - readonly "sequence_number": number -} -export const OpenResponsesFailedEvent = Schema.Struct({ - "type": Schema.Literal("response.failed"), - "response": OpenAIResponsesNonStreamingResponse, - "sequence_number": Schema.Number.check(Schema.isFinite()) -}).annotate({ "description": "Event emitted when a response has failed" }) -export type OpenResponsesStreamEvent = - | OpenResponsesCreatedEvent - | OpenResponsesInProgressEvent - | OpenResponsesCompletedEvent - | OpenResponsesIncompleteEvent - | OpenResponsesFailedEvent - | OpenResponsesErrorEvent - | OpenResponsesOutputItemAddedEvent - | OpenResponsesOutputItemDoneEvent - | OpenResponsesContentPartAddedEvent - | OpenResponsesContentPartDoneEvent - | OpenResponsesTextDeltaEvent - | OpenResponsesTextDoneEvent - | OpenResponsesRefusalDeltaEvent - | OpenResponsesRefusalDoneEvent - | OpenResponsesOutputTextAnnotationAddedEvent - | OpenResponsesFunctionCallArgumentsDeltaEvent - | OpenResponsesFunctionCallArgumentsDoneEvent - | OpenResponsesReasoningDeltaEvent - | OpenResponsesReasoningDoneEvent - | OpenResponsesReasoningSummaryPartAddedEvent - | OpenResponsesReasoningSummaryPartDoneEvent - | OpenResponsesReasoningSummaryTextDeltaEvent - | OpenResponsesReasoningSummaryTextDoneEvent - | OpenResponsesImageGenCallInProgress - | OpenResponsesImageGenCallGenerating - | OpenResponsesImageGenCallPartialImage - | OpenResponsesImageGenCallCompleted -export const OpenResponsesStreamEvent = Schema.Union([ - OpenResponsesCreatedEvent, - OpenResponsesInProgressEvent, - OpenResponsesCompletedEvent, - OpenResponsesIncompleteEvent, - OpenResponsesFailedEvent, - OpenResponsesErrorEvent, - OpenResponsesOutputItemAddedEvent, - OpenResponsesOutputItemDoneEvent, - OpenResponsesContentPartAddedEvent, - OpenResponsesContentPartDoneEvent, - OpenResponsesTextDeltaEvent, - OpenResponsesTextDoneEvent, - OpenResponsesRefusalDeltaEvent, - OpenResponsesRefusalDoneEvent, - OpenResponsesOutputTextAnnotationAddedEvent, - OpenResponsesFunctionCallArgumentsDeltaEvent, - OpenResponsesFunctionCallArgumentsDoneEvent, - OpenResponsesReasoningDeltaEvent, - OpenResponsesReasoningDoneEvent, - OpenResponsesReasoningSummaryPartAddedEvent, - OpenResponsesReasoningSummaryPartDoneEvent, - OpenResponsesReasoningSummaryTextDeltaEvent, - OpenResponsesReasoningSummaryTextDoneEvent, - OpenResponsesImageGenCallInProgress, - OpenResponsesImageGenCallGenerating, - OpenResponsesImageGenCallPartialImage, - OpenResponsesImageGenCallCompleted -], { mode: "oneOf" }) -// schemas -export type CreateResponsesRequestJson = OpenResponsesRequest -export const CreateResponsesRequestJson = OpenResponsesRequest -export type CreateResponses200 = OpenResponsesNonStreamingResponse -export const CreateResponses200 = OpenResponsesNonStreamingResponse -export type CreateResponses200Sse = { readonly "data": OpenResponsesStreamEvent } -export const CreateResponses200Sse = Schema.Struct({ "data": OpenResponsesStreamEvent }) -export type CreateResponses400 = BadRequestResponse -export const CreateResponses400 = BadRequestResponse -export type CreateResponses401 = UnauthorizedResponse -export const CreateResponses401 = UnauthorizedResponse -export type CreateResponses402 = PaymentRequiredResponse -export const CreateResponses402 = PaymentRequiredResponse -export type CreateResponses404 = NotFoundResponse -export const CreateResponses404 = NotFoundResponse -export type CreateResponses408 = RequestTimeoutResponse -export const CreateResponses408 = RequestTimeoutResponse -export type CreateResponses413 = PayloadTooLargeResponse -export const CreateResponses413 = PayloadTooLargeResponse -export type CreateResponses422 = UnprocessableEntityResponse -export const CreateResponses422 = UnprocessableEntityResponse -export type CreateResponses429 = TooManyRequestsResponse -export const CreateResponses429 = TooManyRequestsResponse -export type CreateResponses500 = InternalServerResponse -export const CreateResponses500 = InternalServerResponse -export type CreateResponses502 = BadGatewayResponse -export const CreateResponses502 = BadGatewayResponse -export type CreateResponses503 = ServiceUnavailableResponse -export const CreateResponses503 = ServiceUnavailableResponse -export type CreateResponses524 = EdgeNetworkTimeoutResponse -export const CreateResponses524 = EdgeNetworkTimeoutResponse -export type CreateResponses529 = ProviderOverloadedResponse -export const CreateResponses529 = ProviderOverloadedResponse -export type CreateMessagesRequestJson = AnthropicMessagesRequest -export const CreateMessagesRequestJson = AnthropicMessagesRequest -export type CreateMessages200 = AnthropicMessagesResponse -export const CreateMessages200 = AnthropicMessagesResponse -export type CreateMessages200Sse = { readonly "event": string; readonly "data": AnthropicMessagesStreamEvent } -export const CreateMessages200Sse = Schema.Struct({ "event": Schema.String, "data": AnthropicMessagesStreamEvent }) -export type CreateMessages400 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } -} -export const CreateMessages400 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export type PromptCacheOptions = Objects_20 | null +export const PromptCacheOptions = Schema.Union([Objects_20, Schema.Null]).annotate({ + "description": + "Request-level prompt-cache controls. `mode: \"explicit\"` disables OpenAI-managed breakpoints so only blocks marked with `prompt_cache_breakpoint` are cached. Only supported by OpenAI GPT-5.6 and newer.", + "identifier": "PromptCacheOptions" }) -export type CreateMessages401 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } +export type ContentFilterBuiltinEntry = { + readonly "action": ContentFilterBuiltinAction + readonly "label"?: string + readonly "scan_scope"?: PromptInjectionScanScope + readonly "slug": ContentFilterBuiltinSlug } -export const CreateMessages401 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export const ContentFilterBuiltinEntry = Schema.Struct({ + "action": ContentFilterBuiltinAction, + "label": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Read-only, system-assigned redaction placeholder derived from the slug (e.g. \"[EMAIL]\", \"[PHONE]\"). Not settable by the caller." + }).check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })) + ), + "scan_scope": Schema.optionalKey(PromptInjectionScanScope), + "slug": ContentFilterBuiltinSlug +}).annotate({ + "description": + "A builtin content filter entry. Builtin filters include PII detectors and the regex-based prompt injection detector.", + "identifier": "ContentFilterBuiltinEntry" }) -export type CreateMessages403 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } +export type ContentFilterBuiltinEntryInput = { + readonly "action": ContentFilterBuiltinAction + readonly "label"?: string + readonly "scan_scope"?: PromptInjectionScanScope + readonly "slug": ContentFilterBuiltinSlug } -export const CreateMessages403 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export const ContentFilterBuiltinEntryInput = Schema.Struct({ + "action": ContentFilterBuiltinAction, + "label": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Deprecated: labels are system-assigned and cannot be set by the caller. Accepted for backward compatibility but silently ignored." + }).check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })) + ), + "scan_scope": Schema.optionalKey(PromptInjectionScanScope), + "slug": ContentFilterBuiltinSlug +}).annotate({ + "description": + "A builtin content filter entry for create/update requests. Labels are system-assigned and cannot be set by the caller.", + "identifier": "ContentFilterBuiltinEntryInput" }) -export type CreateMessages404 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } -} -export const CreateMessages404 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export type Union_2 = ReadonlyArray | null +export const Union_2 = Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." }) -export type CreateMessages429 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } -} -export const CreateMessages429 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export type Union_3 = ReadonlyArray | null +export const Union_3 = Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." }) -export type CreateMessages500 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } -} -export const CreateMessages500 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export type Union_4 = ReadonlyArray | null +export const Union_4 = Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "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." }) -export type CreateMessages503 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } +export type ProviderOptions = { + readonly "01ai"?: Objects_21 + readonly "ai21"?: Objects_22 + readonly "aion-labs"?: Objects_23 + readonly "akashml"?: Objects_24 + readonly "alibaba"?: Objects_25 + readonly "amazon-bedrock"?: Objects_26 + readonly "amazon-nova"?: Objects_27 + readonly "ambient"?: Objects_28 + readonly "anthropic"?: Objects_29 + readonly "anyscale"?: Objects_30 + readonly "arcee-ai"?: Objects_31 + readonly "atlas-cloud"?: Objects_32 + readonly "atoma"?: Objects_33 + readonly "avian"?: Objects_34 + readonly "azure"?: Objects_35 + readonly "baidu"?: Objects_36 + readonly "baseten"?: Objects_37 + readonly "black-forest-labs"?: Objects_38 + readonly "byteplus"?: Objects_39 + readonly "centml"?: Objects_40 + readonly "cerebras"?: Objects_41 + readonly "chutes"?: Objects_42 + readonly "cirrascale"?: Objects_43 + readonly "clarifai"?: Objects_44 + readonly "cloudflare"?: Objects_45 + readonly "cohere"?: Objects_46 + readonly "coreweave"?: Objects_47 + readonly "crofai"?: Objects_48 + readonly "crucible"?: Objects_49 + readonly "crusoe"?: Objects_50 + readonly "darkbloom"?: Objects_51 + readonly "decart"?: Objects_52 + readonly "deepgram"?: Objects_53 + readonly "deepinfra"?: Objects_54 + readonly "deepseek"?: Objects_55 + readonly "dekallm"?: Objects_56 + readonly "digitalocean"?: Objects_57 + readonly "enfer"?: Objects_58 + readonly "fake-provider"?: Objects_59 + readonly "featherless"?: Objects_60 + readonly "fireworks"?: Objects_61 + readonly "fish-audio"?: Objects_62 + readonly "friendli"?: Objects_63 + readonly "gmicloud"?: Objects_64 + readonly "google-ai-studio"?: Objects_65 + readonly "google-vertex"?: Objects_66 + readonly "gopomelo"?: Objects_67 + readonly "groq"?: Objects_68 + readonly "heygen"?: Objects_69 + readonly "huggingface"?: Objects_70 + readonly "hyperbolic"?: Objects_71 + readonly "hyperbolic-quantized"?: Objects_72 + readonly "inception"?: Objects_73 + readonly "inceptron"?: Objects_74 + readonly "inferact-vllm"?: Objects_75 + readonly "inference-net"?: Objects_76 + readonly "infermatic"?: Objects_77 + readonly "inflection"?: Objects_78 + readonly "inocloud"?: Objects_79 + readonly "io-net"?: Objects_80 + readonly "ionstream"?: Objects_81 + readonly "klusterai"?: Objects_82 + readonly "krea"?: Objects_83 + readonly "lambda"?: Objects_84 + readonly "lepton"?: Objects_85 + readonly "liquid"?: Objects_86 + readonly "lynn"?: Objects_87 + readonly "lynn-private"?: Objects_88 + readonly "mancer"?: Objects_89 + readonly "mancer-old"?: Objects_90 + readonly "mara"?: Objects_91 + readonly "meta"?: Objects_92 + readonly "minimax"?: Objects_93 + readonly "mistral"?: Objects_94 + readonly "modal"?: Objects_95 + readonly "modelrun"?: Objects_96 + readonly "modular"?: Objects_97 + readonly "moonshotai"?: Objects_98 + readonly "morph"?: Objects_99 + readonly "ncompass"?: Objects_100 + readonly "nebius"?: Objects_101 + readonly "nex-agi"?: Objects_102 + readonly "nextbit"?: Objects_103 + readonly "nineteen"?: Objects_104 + readonly "novita"?: Objects_105 + readonly "nvidia"?: Objects_106 + readonly "octoai"?: Objects_107 + readonly "open-inference"?: Objects_108 + readonly "openai"?: Objects_109 + readonly "parasail"?: Objects_110 + readonly "perceptron"?: Objects_111 + readonly "perplexity"?: Objects_112 + readonly "phala"?: Objects_113 + readonly "poolside"?: Objects_114 + readonly "quiver"?: Objects_115 + readonly "recraft"?: Objects_116 + readonly "recursal"?: Objects_117 + readonly "reflection"?: Objects_118 + readonly "reka"?: Objects_119 + readonly "relace"?: Objects_120 + readonly "replicate"?: Objects_121 + readonly "runway"?: Objects_122 + readonly "sail-research"?: Objects_123 + readonly "sakana"?: Objects_124 + readonly "sambanova"?: Objects_125 + readonly "sambanova-cloaked"?: Objects_126 + readonly "seed"?: Objects_127 + readonly "sf-compute"?: Objects_128 + readonly "siliconflow"?: Objects_129 + readonly "sourceful"?: Objects_130 + readonly "stealth"?: Objects_131 + readonly "stepfun"?: Objects_132 + readonly "streamlake"?: Objects_133 + readonly "switchpoint"?: Objects_134 + readonly "targon"?: Objects_135 + readonly "tencent"?: Objects_136 + readonly "tenstorrent"?: Objects_137 + readonly "together"?: Objects_138 + readonly "together-lite"?: Objects_139 + readonly "ubicloud"?: Objects_140 + readonly "upstage"?: Objects_141 + readonly "venice"?: Objects_142 + readonly "wafer"?: Objects_143 + readonly "wandb"?: Objects_144 + readonly "xai"?: Objects_145 + readonly "xiaomi"?: Objects_146 + readonly "z-ai"?: Objects_147 } -export const CreateMessages503 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) +export const ProviderOptions = Schema.Struct({ + "01ai": Schema.optionalKey(Objects_21), + "ai21": Schema.optionalKey(Objects_22), + "aion-labs": Schema.optionalKey(Objects_23), + "akashml": Schema.optionalKey(Objects_24), + "alibaba": Schema.optionalKey(Objects_25), + "amazon-bedrock": Schema.optionalKey(Objects_26), + "amazon-nova": Schema.optionalKey(Objects_27), + "ambient": Schema.optionalKey(Objects_28), + "anthropic": Schema.optionalKey(Objects_29), + "anyscale": Schema.optionalKey(Objects_30), + "arcee-ai": Schema.optionalKey(Objects_31), + "atlas-cloud": Schema.optionalKey(Objects_32), + "atoma": Schema.optionalKey(Objects_33), + "avian": Schema.optionalKey(Objects_34), + "azure": Schema.optionalKey(Objects_35), + "baidu": Schema.optionalKey(Objects_36), + "baseten": Schema.optionalKey(Objects_37), + "black-forest-labs": Schema.optionalKey(Objects_38), + "byteplus": Schema.optionalKey(Objects_39), + "centml": Schema.optionalKey(Objects_40), + "cerebras": Schema.optionalKey(Objects_41), + "chutes": Schema.optionalKey(Objects_42), + "cirrascale": Schema.optionalKey(Objects_43), + "clarifai": Schema.optionalKey(Objects_44), + "cloudflare": Schema.optionalKey(Objects_45), + "cohere": Schema.optionalKey(Objects_46), + "coreweave": Schema.optionalKey(Objects_47), + "crofai": Schema.optionalKey(Objects_48), + "crucible": Schema.optionalKey(Objects_49), + "crusoe": Schema.optionalKey(Objects_50), + "darkbloom": Schema.optionalKey(Objects_51), + "decart": Schema.optionalKey(Objects_52), + "deepgram": Schema.optionalKey(Objects_53), + "deepinfra": Schema.optionalKey(Objects_54), + "deepseek": Schema.optionalKey(Objects_55), + "dekallm": Schema.optionalKey(Objects_56), + "digitalocean": Schema.optionalKey(Objects_57), + "enfer": Schema.optionalKey(Objects_58), + "fake-provider": Schema.optionalKey(Objects_59), + "featherless": Schema.optionalKey(Objects_60), + "fireworks": Schema.optionalKey(Objects_61), + "fish-audio": Schema.optionalKey(Objects_62), + "friendli": Schema.optionalKey(Objects_63), + "gmicloud": Schema.optionalKey(Objects_64), + "google-ai-studio": Schema.optionalKey(Objects_65), + "google-vertex": Schema.optionalKey(Objects_66), + "gopomelo": Schema.optionalKey(Objects_67), + "groq": Schema.optionalKey(Objects_68), + "heygen": Schema.optionalKey(Objects_69), + "huggingface": Schema.optionalKey(Objects_70), + "hyperbolic": Schema.optionalKey(Objects_71), + "hyperbolic-quantized": Schema.optionalKey(Objects_72), + "inception": Schema.optionalKey(Objects_73), + "inceptron": Schema.optionalKey(Objects_74), + "inferact-vllm": Schema.optionalKey(Objects_75), + "inference-net": Schema.optionalKey(Objects_76), + "infermatic": Schema.optionalKey(Objects_77), + "inflection": Schema.optionalKey(Objects_78), + "inocloud": Schema.optionalKey(Objects_79), + "io-net": Schema.optionalKey(Objects_80), + "ionstream": Schema.optionalKey(Objects_81), + "klusterai": Schema.optionalKey(Objects_82), + "krea": Schema.optionalKey(Objects_83), + "lambda": Schema.optionalKey(Objects_84), + "lepton": Schema.optionalKey(Objects_85), + "liquid": Schema.optionalKey(Objects_86), + "lynn": Schema.optionalKey(Objects_87), + "lynn-private": Schema.optionalKey(Objects_88), + "mancer": Schema.optionalKey(Objects_89), + "mancer-old": Schema.optionalKey(Objects_90), + "mara": Schema.optionalKey(Objects_91), + "meta": Schema.optionalKey(Objects_92), + "minimax": Schema.optionalKey(Objects_93), + "mistral": Schema.optionalKey(Objects_94), + "modal": Schema.optionalKey(Objects_95), + "modelrun": Schema.optionalKey(Objects_96), + "modular": Schema.optionalKey(Objects_97), + "moonshotai": Schema.optionalKey(Objects_98), + "morph": Schema.optionalKey(Objects_99), + "ncompass": Schema.optionalKey(Objects_100), + "nebius": Schema.optionalKey(Objects_101), + "nex-agi": Schema.optionalKey(Objects_102), + "nextbit": Schema.optionalKey(Objects_103), + "nineteen": Schema.optionalKey(Objects_104), + "novita": Schema.optionalKey(Objects_105), + "nvidia": Schema.optionalKey(Objects_106), + "octoai": Schema.optionalKey(Objects_107), + "open-inference": Schema.optionalKey(Objects_108), + "openai": Schema.optionalKey(Objects_109), + "parasail": Schema.optionalKey(Objects_110), + "perceptron": Schema.optionalKey(Objects_111), + "perplexity": Schema.optionalKey(Objects_112), + "phala": Schema.optionalKey(Objects_113), + "poolside": Schema.optionalKey(Objects_114), + "quiver": Schema.optionalKey(Objects_115), + "recraft": Schema.optionalKey(Objects_116), + "recursal": Schema.optionalKey(Objects_117), + "reflection": Schema.optionalKey(Objects_118), + "reka": Schema.optionalKey(Objects_119), + "relace": Schema.optionalKey(Objects_120), + "replicate": Schema.optionalKey(Objects_121), + "runway": Schema.optionalKey(Objects_122), + "sail-research": Schema.optionalKey(Objects_123), + "sakana": Schema.optionalKey(Objects_124), + "sambanova": Schema.optionalKey(Objects_125), + "sambanova-cloaked": Schema.optionalKey(Objects_126), + "seed": Schema.optionalKey(Objects_127), + "sf-compute": Schema.optionalKey(Objects_128), + "siliconflow": Schema.optionalKey(Objects_129), + "sourceful": Schema.optionalKey(Objects_130), + "stealth": Schema.optionalKey(Objects_131), + "stepfun": Schema.optionalKey(Objects_132), + "streamlake": Schema.optionalKey(Objects_133), + "switchpoint": Schema.optionalKey(Objects_134), + "targon": Schema.optionalKey(Objects_135), + "tencent": Schema.optionalKey(Objects_136), + "tenstorrent": Schema.optionalKey(Objects_137), + "together": Schema.optionalKey(Objects_138), + "together-lite": Schema.optionalKey(Objects_139), + "ubicloud": Schema.optionalKey(Objects_140), + "upstage": Schema.optionalKey(Objects_141), + "venice": Schema.optionalKey(Objects_142), + "wafer": Schema.optionalKey(Objects_143), + "wandb": Schema.optionalKey(Objects_144), + "xai": Schema.optionalKey(Objects_145), + "xiaomi": Schema.optionalKey(Objects_146), + "z-ai": Schema.optionalKey(Objects_147) +}).annotate({ + "description": + "Provider-specific options keyed by provider slug. Only options for the matched provider are forwarded; the rest are ignored. Unrecognized keys are silently dropped.", + "identifier": "ProviderOptions" }) -export type CreateMessages529 = { - readonly "type": "error" - readonly "error": { readonly "type": string; readonly "message": string } +export type ProviderOverloadedResponse = { + readonly "error": ProviderOverloadedResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null } -export const CreateMessages529 = Schema.Struct({ - "type": Schema.Literal("error"), - "error": Schema.Struct({ "type": Schema.String, "message": Schema.String }) -}) -export type GetUserActivityParams = { readonly "date"?: string } -export const GetUserActivityParams = Schema.Struct({ - "date": Schema.optionalKey( - Schema.String.annotate({ "description": "Filter by a single UTC date in the last 30 days (YYYY-MM-DD format)." }) - ) -}) -export type GetUserActivity200 = { readonly "data": ReadonlyArray } -export const GetUserActivity200 = Schema.Struct({ - "data": Schema.Array(ActivityItem).annotate({ "description": "List of activity items" }) +export const ProviderOverloadedResponse = Schema.Struct({ + "error": ProviderOverloadedResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ), + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Provider Overloaded - Provider is temporarily overloaded", + "identifier": "ProviderOverloadedResponse" }) -export type GetUserActivity400 = BadRequestResponse -export const GetUserActivity400 = BadRequestResponse -export type GetUserActivity401 = UnauthorizedResponse -export const GetUserActivity401 = UnauthorizedResponse -export type GetUserActivity403 = ForbiddenResponse -export const GetUserActivity403 = ForbiddenResponse -export type GetUserActivity500 = InternalServerResponse -export const GetUserActivity500 = InternalServerResponse -export type GetCredits200 = { readonly "data": { readonly "total_credits": number; readonly "total_usage": number } } -export const GetCredits200 = Schema.Struct({ - "data": Schema.Struct({ - "total_credits": Schema.Number.annotate({ "description": "Total credits purchased" }).check(Schema.isFinite()), - "total_usage": Schema.Number.annotate({ "description": "Total credits used" }).check(Schema.isFinite()) - }) -}).annotate({ "description": "Total credits purchased and used" }) -export type GetCredits401 = UnauthorizedResponse -export const GetCredits401 = UnauthorizedResponse -export type GetCredits403 = ForbiddenResponse -export const GetCredits403 = ForbiddenResponse -export type GetCredits500 = InternalServerResponse -export const GetCredits500 = InternalServerResponse -export type CreateCoinbaseChargeRequestJson = CreateChargeRequest -export const CreateCoinbaseChargeRequestJson = CreateChargeRequest -export type CreateCoinbaseCharge200 = { +export type GenerationResponse = { readonly "data": { - readonly "id": string + readonly "api_type": "completions" | "embeddings" | "rerank" | "tts" | "stt" | "video" | "image" | null + readonly "app_id": number | null + readonly "cache_discount": number | null + readonly "cancelled": boolean | null readonly "created_at": string - readonly "expires_at": string - readonly "web3_data": { - readonly "transfer_intent": { - readonly "call_data": { - readonly "deadline": string - readonly "fee_amount": string - readonly "id": string - readonly "operator": string - readonly "prefix": string - readonly "recipient": string - readonly "recipient_amount": string - readonly "recipient_currency": string - readonly "refund_destination": string - readonly "signature": string - } - readonly "metadata": { - readonly "chain_id": number - readonly "contract_address": string - readonly "sender": string - } - } - } - } -} -export const CreateCoinbaseCharge200 = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String, - "created_at": Schema.String, - "expires_at": Schema.String, - "web3_data": Schema.Struct({ - "transfer_intent": Schema.Struct({ - "call_data": Schema.Struct({ - "deadline": Schema.String, - "fee_amount": Schema.String, - "id": Schema.String, - "operator": Schema.String, - "prefix": Schema.String, - "recipient": Schema.String, - "recipient_amount": Schema.String, - "recipient_currency": Schema.String, - "refund_destination": Schema.String, - "signature": Schema.String - }), - "metadata": Schema.Struct({ - "chain_id": Schema.Number.check(Schema.isFinite()), - "contract_address": Schema.String, - "sender": Schema.String - }) - }) - }) - }) -}) -export type CreateCoinbaseCharge400 = BadRequestResponse -export const CreateCoinbaseCharge400 = BadRequestResponse -export type CreateCoinbaseCharge401 = UnauthorizedResponse -export const CreateCoinbaseCharge401 = UnauthorizedResponse -export type CreateCoinbaseCharge429 = TooManyRequestsResponse -export const CreateCoinbaseCharge429 = TooManyRequestsResponse -export type CreateCoinbaseCharge500 = InternalServerResponse -export const CreateCoinbaseCharge500 = InternalServerResponse -export type CreateEmbeddingsRequestJson = { - readonly "input": - | string - | ReadonlyArray - | ReadonlyArray - | ReadonlyArray> - | ReadonlyArray< - { - readonly "content": ReadonlyArray< - { readonly "type": "text"; readonly "text": string } | { - readonly "type": "image_url" - readonly "image_url": { readonly "url": string } - } - > - } - > - readonly "model": string - readonly "encoding_format"?: "float" | "base64" - readonly "dimensions"?: number - readonly "user"?: string - readonly "provider"?: ProviderPreferences - readonly "input_type"?: string -} -export const CreateEmbeddingsRequestJson = Schema.Struct({ - "input": Schema.Union([ - Schema.String, - Schema.Array(Schema.String), - Schema.Array(Schema.Number.check(Schema.isFinite())), - Schema.Array(Schema.Array(Schema.Number.check(Schema.isFinite()))), - Schema.Array( - Schema.Struct({ - "content": Schema.Array( - Schema.Union([ - Schema.Struct({ "type": Schema.Literal("text"), "text": Schema.String }), - Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) - ], { mode: "oneOf" }) - ) - }) - ) - ]), - "model": Schema.String, - "encoding_format": Schema.optionalKey(Schema.Literals(["float", "base64"])), - "dimensions": Schema.optionalKey(Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), - "user": Schema.optionalKey(Schema.String), - "provider": Schema.optionalKey(ProviderPreferences), - "input_type": Schema.optionalKey(Schema.String) -}) -export type CreateEmbeddings200 = { - readonly "id"?: string - readonly "object": "list" - readonly "data": ReadonlyArray< - { readonly "object": "embedding"; readonly "embedding": ReadonlyArray | string; readonly "index"?: number } - > - readonly "model": string - readonly "usage"?: { readonly "prompt_tokens": number; readonly "total_tokens": number; readonly "cost"?: number } -} -export const CreateEmbeddings200 = Schema.Struct({ - "id": Schema.optionalKey(Schema.String), - "object": Schema.Literal("list"), - "data": Schema.Array( - Schema.Struct({ - "object": Schema.Literal("embedding"), - "embedding": Schema.Union([Schema.Array(Schema.Number.check(Schema.isFinite())), Schema.String]), - "index": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) - }) - ), - "model": Schema.String, - "usage": Schema.optionalKey( - Schema.Struct({ - "prompt_tokens": Schema.Number.check(Schema.isFinite()), - "total_tokens": Schema.Number.check(Schema.isFinite()), - "cost": Schema.optionalKey(Schema.Number.check(Schema.isFinite())) - }) - ) -}) -export type CreateEmbeddings200Sse = string -export const CreateEmbeddings200Sse = Schema.String.annotate({ - "description": "Not used for embeddings - embeddings do not support streaming" -}) -export type CreateEmbeddings400 = BadRequestResponse -export const CreateEmbeddings400 = BadRequestResponse -export type CreateEmbeddings401 = UnauthorizedResponse -export const CreateEmbeddings401 = UnauthorizedResponse -export type CreateEmbeddings402 = PaymentRequiredResponse -export const CreateEmbeddings402 = PaymentRequiredResponse -export type CreateEmbeddings404 = NotFoundResponse -export const CreateEmbeddings404 = NotFoundResponse -export type CreateEmbeddings429 = TooManyRequestsResponse -export const CreateEmbeddings429 = TooManyRequestsResponse -export type CreateEmbeddings500 = InternalServerResponse -export const CreateEmbeddings500 = InternalServerResponse -export type CreateEmbeddings502 = BadGatewayResponse -export const CreateEmbeddings502 = BadGatewayResponse -export type CreateEmbeddings503 = ServiceUnavailableResponse -export const CreateEmbeddings503 = ServiceUnavailableResponse -export type CreateEmbeddings524 = EdgeNetworkTimeoutResponse -export const CreateEmbeddings524 = EdgeNetworkTimeoutResponse -export type CreateEmbeddings529 = ProviderOverloadedResponse -export const CreateEmbeddings529 = ProviderOverloadedResponse -export type ListEmbeddingsModels200 = ModelsListResponse -export const ListEmbeddingsModels200 = ModelsListResponse -export type ListEmbeddingsModels400 = BadRequestResponse -export const ListEmbeddingsModels400 = BadRequestResponse -export type ListEmbeddingsModels500 = InternalServerResponse -export const ListEmbeddingsModels500 = InternalServerResponse -export type GetGenerationParams = { readonly "id": string } -export const GetGenerationParams = Schema.Struct({ "id": Schema.String.check(Schema.isMinLength(1)) }) -export type GetGeneration200 = { - readonly "data": { + readonly "data_region": "global" | "europe" + readonly "external_user": string | null + readonly "finish_reason": string | null + readonly "generation_time": number | null + readonly "http_referer": string | null readonly "id": string - readonly "upstream_id": string - readonly "total_cost": number - readonly "cache_discount": number - readonly "upstream_inference_cost": number - readonly "created_at": string + readonly "is_byok": boolean + readonly "latency": number | null readonly "model": string - readonly "app_id": number - readonly "streamed": boolean - readonly "cancelled": boolean - readonly "provider_name": string - readonly "latency": number - readonly "moderation_latency": number - readonly "generation_time": number - readonly "finish_reason": string - readonly "tokens_prompt": number - readonly "tokens_completion": number - readonly "native_tokens_prompt": number - readonly "native_tokens_completion": number - readonly "native_tokens_completion_images": number - readonly "native_tokens_reasoning": number - readonly "native_tokens_cached": number - readonly "num_media_prompt": number - readonly "num_input_audio_prompt": number - readonly "num_media_completion": number - readonly "num_search_results": number + readonly "moderation_latency": number | null + readonly "native_finish_reason": string | null + readonly "native_tokens_cached": number | null + readonly "native_tokens_completion": number | null + readonly "native_tokens_completion_images": number | null + readonly "native_tokens_prompt": number | null + readonly "native_tokens_reasoning": number | null + readonly "num_fetches": number | null + readonly "num_input_audio_prompt": number | null + readonly "num_media_completion": number | null + readonly "num_media_prompt": number | null + readonly "num_search_results": number | null readonly "origin": string + readonly "preset_id": string | null + readonly "provider_name": string | null + readonly "provider_responses": ReadonlyArray | null + readonly "request_id"?: string | null + readonly "response_cache_source_id"?: string | null + readonly "router": string | null + readonly "service_tier": string | null + readonly "session_id"?: string | null + readonly "streamed": boolean | null + readonly "tokens_completion": number | null + readonly "tokens_prompt": number | null + readonly "total_cost": number + readonly "upstream_id": string | null + readonly "upstream_inference_cost": number | null readonly "usage": number - readonly "is_byok": boolean - readonly "native_finish_reason": string - readonly "external_user": string - readonly "api_type": "completions" | "embeddings" - readonly "router": string - readonly "provider_responses": ReadonlyArray< - { - readonly "id"?: string - readonly "endpoint_id"?: string - readonly "model_permaslug"?: string - readonly "provider_name"?: - | "AnyScale" - | "Atoma" - | "Cent-ML" - | "CrofAI" - | "Enfer" - | "GoPomelo" - | "HuggingFace" - | "Hyperbolic 2" - | "InoCloud" - | "Kluster" - | "Lambda" - | "Lepton" - | "Lynn 2" - | "Lynn" - | "Mancer" - | "Meta" - | "Modal" - | "Nineteen" - | "OctoAI" - | "Recursal" - | "Reflection" - | "Replicate" - | "SambaNova 2" - | "SF Compute" - | "Targon" - | "Together 2" - | "Ubicloud" - | "01.AI" - | "AI21" - | "AionLabs" - | "Alibaba" - | "Ambient" - | "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" - | "Io Net" - | "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" - | "StepFun" - | "Stealth" - | "StreamLake" - | "Switchpoint" - | "Together" - | "Upstage" - | "Venice" - | "WandB" - | "Xiaomi" - | "xAI" - | "Z.AI" - | "FakeProvider" - readonly "status": number - readonly "latency"?: number - readonly "is_byok"?: boolean - } - > + readonly "user_agent": string | null + readonly "web_search_engine": string | null } } -export const GetGeneration200 = Schema.Struct({ +export const GenerationResponse = Schema.Struct({ "data": Schema.Struct({ + "api_type": Schema.Union([ + Schema.Literal("completions"), + Schema.Literal("embeddings"), + Schema.Literal("rerank"), + Schema.Literal("tts"), + Schema.Literal("stt"), + Schema.Literal("video"), + Schema.Literal("image"), + Schema.Null + ]).annotate({ "description": "Type of API used for the generation" }), + "app_id": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + .annotate({ "description": "ID of the app that made the request" }), + "cache_discount": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Discount applied due to caching", "format": "double" }), + "cancelled": Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Whether the generation was cancelled" + }), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the generation was created" }), + "data_region": Schema.Literals(["global", "europe"]).annotate({ + "description": + "The data region this generation was routed through. 'europe' for EU-routed requests, 'global' otherwise." + }), + "external_user": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "External user identifier" }), + "finish_reason": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Reason the generation finished" + }), + "generation_time": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Time taken for generation in milliseconds", "format": "double" }), + "http_referer": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Referer header from the request" + }), "id": Schema.String.annotate({ "description": "Unique identifier for the generation" }), - "upstream_id": Schema.String.annotate({ "description": "Upstream provider's identifier for this generation" }), - "total_cost": Schema.Number.annotate({ "description": "Total cost of the generation in USD" }).check( - Schema.isFinite() + "is_byok": Schema.Boolean.annotate({ "description": "Whether this used bring-your-own-key" }), + "latency": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Total latency in milliseconds", "format": "double" }), + "model": Schema.String.annotate({ "description": "Model used for the generation" }), + "moderation_latency": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Moderation latency in milliseconds", "format": "double" }), + "native_finish_reason": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Native finish reason as reported by provider" + }), + "native_tokens_cached": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Native cached tokens as reported by provider" }), + "native_tokens_completion": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Native completion tokens as reported by provider" }), + "native_tokens_completion_images": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Native completion image tokens as reported by provider" }), + "native_tokens_prompt": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Native prompt tokens as reported by provider" }), + "native_tokens_reasoning": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Native reasoning tokens as reported by provider" }), + "num_fetches": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of web fetches performed" }), + "num_input_audio_prompt": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of audio inputs in the prompt" }), + "num_media_completion": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of media items in the completion" }), + "num_media_prompt": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of media items in the prompt" }), + "num_search_results": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of search results included" }), + "origin": Schema.String.annotate({ "description": "Origin URL of the request" }), + "preset_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ID of the preset used for this generation, null if no preset was used" + }), + "provider_name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Name of the provider that served the request" + }), + "provider_responses": Schema.Union([Schema.Array(ProviderResponse), Schema.Null]).annotate({ + "description": "List of provider responses for this generation, including fallback attempts" + }), + "request_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Unique identifier grouping all generations from a single API request" + }) ), - "cache_discount": Schema.Number.annotate({ "description": "Discount applied due to caching" }).check( - Schema.isFinite() + "response_cache_source_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "If this generation was served from response cache, contains the original generation ID. Null otherwise." + }) ), - "upstream_inference_cost": Schema.Number.annotate({ "description": "Cost charged by the upstream provider" }).check( - Schema.isFinite() + "router": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Router used for the request (e.g., openrouter/auto)" + }), + "service_tier": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Service tier the upstream provider reported running this request on, or null if it did not report one." + }), + "session_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Session identifier grouping multiple generations in the same session" + }) ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the generation was created" }), - "model": Schema.String.annotate({ "description": "Model used for the generation" }), - "app_id": Schema.Number.annotate({ "description": "ID of the app that made the request" }).check(Schema.isFinite()), - "streamed": Schema.Boolean.annotate({ "description": "Whether the response was streamed" }), - "cancelled": Schema.Boolean.annotate({ "description": "Whether the generation was cancelled" }), - "provider_name": Schema.String.annotate({ "description": "Name of the provider that served the request" }), - "latency": Schema.Number.annotate({ "description": "Total latency in milliseconds" }).check(Schema.isFinite()), - "moderation_latency": Schema.Number.annotate({ "description": "Moderation latency in milliseconds" }).check( - Schema.isFinite() + "streamed": Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": "Whether the response was streamed" + }), + "tokens_completion": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of tokens in the completion" }), + "tokens_prompt": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Number of tokens in the prompt" }), + "total_cost": Schema.Number.annotate({ "description": "Total cost of the generation in USD", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "upstream_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Upstream provider's identifier for this generation" + }), + "upstream_inference_cost": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Cost charged by the upstream provider", "format": "double" }), + "usage": Schema.Number.annotate({ "description": "Usage amount in USD", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) ), - "generation_time": Schema.Number.annotate({ "description": "Time taken for generation in milliseconds" }).check( - Schema.isFinite() + "user_agent": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "User-Agent header from the request" + }), + "web_search_engine": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The resolved web search engine used for this generation (e.g. exa, firecrawl, parallel)" + }) + }).annotate({ "description": "Generation data" }) +}).annotate({ "description": "Generation response", "identifier": "GenerationResponse" }) +export type Union_5 = ProviderSort | ProviderSortConfig | null +export const Union_5 = Schema.Union([ProviderSort, ProviderSortConfig, Schema.Null]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." +}) +export type PublicEndpoint = { + readonly "context_length": number + readonly "latency_last_30m": PercentileStats + readonly "max_completion_tokens": number | null + readonly "max_prompt_tokens": number | null + readonly "model_id": string + readonly "model_name": string + readonly "name": string + readonly "pricing": { + readonly "audio"?: string + readonly "audio_output"?: string + readonly "completion": string + readonly "discount"?: number + readonly "image"?: string + readonly "image_output"?: string + readonly "image_token"?: string + readonly "input_audio_cache"?: string + readonly "input_cache_read"?: string + readonly "input_cache_write"?: string + readonly "input_cache_write_1h"?: string + readonly "internal_reasoning"?: string + readonly "overrides"?: ReadonlyArray + readonly "prompt": string + readonly "request"?: string + readonly "web_search"?: string + } + readonly "provider_name": ProviderName + readonly "quantization": Quantization | null + readonly "status"?: EndpointStatus + readonly "supported_parameters": ReadonlyArray + readonly "supports_implicit_caching": boolean + readonly "tag": string + readonly "throughput_last_30m": PercentileStats + readonly "uptime_last_1d": number | null + readonly "uptime_last_30m": number | null + readonly "uptime_last_5m": number | null +} +export const PublicEndpoint = Schema.Struct({ + "context_length": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "latency_last_30m": PercentileStats, + "max_completion_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "max_prompt_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "model_id": Schema.String.annotate({ "description": "The unique identifier for the model (permaslug)" }), + "model_name": Schema.String, + "name": Schema.String, + "pricing": Schema.Struct({ + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per audio input token" })), + "audio_output": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per audio output token" }) ), - "finish_reason": Schema.String.annotate({ "description": "Reason the generation finished" }), - "tokens_prompt": Schema.Number.annotate({ "description": "Number of tokens in the prompt" }).check( - Schema.isFinite() + "completion": Schema.String.annotate({ + "description": "Price in USD per token for completion (output) generation" + }), + "discount": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Fractional discount applied to this endpoint's pricing; the price is multiplied by (1 - discount) (0 = no discount, 1 = free)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) ), - "tokens_completion": Schema.Number.annotate({ "description": "Number of tokens in the completion" }).check( - Schema.isFinite() + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per input image" })), + "image_output": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per output image" })), + "image_token": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per image token" })), + "input_audio_cache": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per cached audio input token" }) ), - "native_tokens_prompt": Schema.Number.annotate({ "description": "Native prompt tokens as reported by provider" }) - .check(Schema.isFinite()), - "native_tokens_completion": Schema.Number.annotate({ - "description": "Native completion tokens as reported by provider" - }).check(Schema.isFinite()), - "native_tokens_completion_images": Schema.Number.annotate({ - "description": "Native completion image tokens as reported by provider" - }).check(Schema.isFinite()), - "native_tokens_reasoning": Schema.Number.annotate({ - "description": "Native reasoning tokens as reported by provider" - }).check(Schema.isFinite()), - "native_tokens_cached": Schema.Number.annotate({ "description": "Native cached tokens as reported by provider" }) - .check(Schema.isFinite()), - "num_media_prompt": Schema.Number.annotate({ "description": "Number of media items in the prompt" }).check( - Schema.isFinite() + "input_cache_read": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per cached input token (read)" }) ), - "num_input_audio_prompt": Schema.Number.annotate({ "description": "Number of audio inputs in the prompt" }).check( - Schema.isFinite() + "input_cache_write": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Price per cache-write token, in USD per token. For providers with multiple cache TTLs (e.g. Anthropic), this is the default (5-minute) cache-write rate." + }) ), - "num_media_completion": Schema.Number.annotate({ "description": "Number of media items in the completion" }).check( - Schema.isFinite() + "input_cache_write_1h": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Price per 1-hour cache-write token, in USD per token. Only present for providers that price an extended (1-hour) cache TTL separately, such as Anthropic." + }) ), - "num_search_results": Schema.Number.annotate({ "description": "Number of search results included" }).check( - Schema.isFinite() + "internal_reasoning": Schema.optionalKey( + Schema.String.annotate({ "description": "Price in USD per internal reasoning token" }) ), - "origin": Schema.String.annotate({ "description": "Origin URL of the request" }), - "usage": Schema.Number.annotate({ "description": "Usage amount in USD" }).check(Schema.isFinite()), - "is_byok": Schema.Boolean.annotate({ "description": "Whether this used bring-your-own-key" }), - "native_finish_reason": Schema.String.annotate({ "description": "Native finish reason as reported by provider" }), - "external_user": Schema.String.annotate({ "description": "External user identifier" }), - "api_type": Schema.Literals(["completions", "embeddings"]).annotate({ - "description": "Type of API used for the generation" - }), - "router": Schema.String.annotate({ "description": "Router used for the request (e.g., openrouter/auto)" }), - "provider_responses": Schema.Array(Schema.Struct({ - "id": Schema.optionalKey(Schema.String), - "endpoint_id": Schema.optionalKey(Schema.String), - "model_permaslug": Schema.optionalKey(Schema.String), - "provider_name": Schema.optionalKey( - Schema.Literals([ - "AnyScale", - "Atoma", - "Cent-ML", - "CrofAI", - "Enfer", - "GoPomelo", - "HuggingFace", - "Hyperbolic 2", - "InoCloud", - "Kluster", - "Lambda", - "Lepton", - "Lynn 2", - "Lynn", - "Mancer", - "Meta", - "Modal", - "Nineteen", - "OctoAI", - "Recursal", - "Reflection", - "Replicate", - "SambaNova 2", - "SF Compute", - "Targon", - "Together 2", - "Ubicloud", - "01.AI", - "AI21", - "AionLabs", - "Alibaba", - "Ambient", - "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", - "Io Net", - "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", - "StepFun", - "Stealth", - "StreamLake", - "Switchpoint", - "Together", - "Upstage", - "Venice", - "WandB", - "Xiaomi", - "xAI", - "Z.AI", - "FakeProvider" - ]) - ), - "status": Schema.Number.check(Schema.isFinite()), - "latency": Schema.optionalKey(Schema.Number.check(Schema.isFinite())), - "is_byok": Schema.optionalKey(Schema.Boolean) - })).annotate({ "description": "List of provider responses for this generation, including fallback attempts" }) - }).annotate({ "description": "Generation data" }) -}).annotate({ "description": "Generation response" }) -export type GetGeneration401 = UnauthorizedResponse -export const GetGeneration401 = UnauthorizedResponse -export type GetGeneration402 = PaymentRequiredResponse -export const GetGeneration402 = PaymentRequiredResponse -export type GetGeneration404 = NotFoundResponse -export const GetGeneration404 = NotFoundResponse -export type GetGeneration429 = TooManyRequestsResponse -export const GetGeneration429 = TooManyRequestsResponse -export type GetGeneration500 = InternalServerResponse -export const GetGeneration500 = InternalServerResponse -export type GetGeneration502 = BadGatewayResponse -export const GetGeneration502 = BadGatewayResponse -export type GetGeneration524 = EdgeNetworkTimeoutResponse -export const GetGeneration524 = EdgeNetworkTimeoutResponse -export type GetGeneration529 = ProviderOverloadedResponse -export const GetGeneration529 = ProviderOverloadedResponse -export type ListModelsCount200 = ModelsCountResponse -export const ListModelsCount200 = ModelsCountResponse -export type ListModelsCount500 = InternalServerResponse -export const ListModelsCount500 = InternalServerResponse -export type GetModelsParams = { - readonly "category"?: - | "programming" - | "roleplay" - | "marketing" - | "marketing/seo" - | "technology" - | "science" - | "translation" - | "legal" - | "finance" - | "health" - | "trivia" - | "academia" - readonly "supported_parameters"?: string -} -export const GetModelsParams = Schema.Struct({ - "category": Schema.optionalKey( - Schema.Literals([ - "programming", - "roleplay", - "marketing", - "marketing/seo", - "technology", - "science", - "translation", - "legal", - "finance", - "health", - "trivia", - "academia" - ]).annotate({ "description": "Filter models by use case category" }) - ), - "supported_parameters": Schema.optionalKey(Schema.String) + "overrides": Schema.optionalKey( + Schema.Array(PricingOverride).annotate({ + "description": + "Conditional overrides of the base pricing (e.g. long-context or time-based pricing). An entry applies when all of its condition fields (e.g. min_prompt_tokens, or the utc_start/utc_end time window) match the request; among applicable entries, later entries win per key; price keys absent from an entry inherit the base price. The top-level pricing keys always reflect the price that applies under default conditions." + }) + ), + "prompt": Schema.String.annotate({ "description": "Price in USD per token for prompt (input) processing" }), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per request" })), + "web_search": Schema.optionalKey(Schema.String.annotate({ "description": "Price in USD per web search" })) + }), + "provider_name": ProviderName, + "quantization": Schema.Union([Quantization, Schema.Null]), + "status": Schema.optionalKey(EndpointStatus), + "supported_parameters": Schema.Array(Parameter), + "supports_implicit_caching": Schema.Boolean, + "tag": Schema.String, + "throughput_last_30m": Schema.suspend((): Schema.Codec => PercentileStats).annotate({ + "description": + "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." + }), + "uptime_last_1d": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ + "description": + "Uptime percentage over the last 1 day, calculated as successful requests / (successful + error requests) * 100. Rate-limited requests are excluded. Returns null if insufficient data.", + "format": "double" + }), + "uptime_last_30m": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "uptime_last_5m": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ + "description": + "Uptime percentage over the last 5 minutes, calculated as successful requests / (successful + error requests) * 100. Rate-limited requests are excluded. Returns null if insufficient data.", + "format": "double" + }) +}).annotate({ "description": "Information about a specific model endpoint", "identifier": "PublicEndpoint" }) +export type CapabilityDescriptor = EnumCapability | RangeCapability | BooleanCapability +export const CapabilityDescriptor = Schema.Union([EnumCapability, RangeCapability, BooleanCapability], { + mode: "oneOf" +}).annotate({ + "description": "A typed descriptor for one supported request parameter.", + "identifier": "CapabilityDescriptor" }) -export type GetModels200 = ModelsListResponse -export const GetModels200 = ModelsListResponse -export type GetModels400 = BadRequestResponse -export const GetModels400 = BadRequestResponse -export type GetModels500 = InternalServerResponse -export const GetModels500 = InternalServerResponse -export type ListModelsUser200 = ModelsListResponse -export const ListModelsUser200 = ModelsListResponse -export type ListModelsUser401 = UnauthorizedResponse -export const ListModelsUser401 = UnauthorizedResponse -export type ListModelsUser404 = NotFoundResponse -export const ListModelsUser404 = NotFoundResponse -export type ListModelsUser500 = InternalServerResponse -export const ListModelsUser500 = InternalServerResponse -export type ListEndpoints200 = { readonly "data": ListEndpointsResponse } -export const ListEndpoints200 = Schema.Struct({ "data": ListEndpointsResponse }) -export type ListEndpoints404 = NotFoundResponse -export const ListEndpoints404 = NotFoundResponse -export type ListEndpoints500 = InternalServerResponse -export const ListEndpoints500 = InternalServerResponse -export type ListEndpointsZdr200 = { readonly "data": ReadonlyArray } -export const ListEndpointsZdr200 = Schema.Struct({ "data": Schema.Array(PublicEndpoint) }) -export type ListEndpointsZdr500 = InternalServerResponse -export const ListEndpointsZdr500 = InternalServerResponse -export type ListProviders200 = { - readonly "data": ReadonlyArray< - { - readonly "name": string - readonly "slug": string - readonly "privacy_policy_url": string - readonly "terms_of_service_url"?: string - readonly "status_page_url"?: string - } - > +export type AppRankingsResponse = { + readonly "data": ReadonlyArray + readonly "meta": RankingsDailyMeta } -export const ListProviders200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "name": Schema.String.annotate({ "description": "Display name of the provider" }), - "slug": Schema.String.annotate({ "description": "URL-friendly identifier for the provider" }), - "privacy_policy_url": Schema.String.annotate({ "description": "URL to the provider's privacy policy" }), - "terms_of_service_url": Schema.optionalKey( - Schema.String.annotate({ "description": "URL to the provider's terms of service" }) - ), - "status_page_url": Schema.optionalKey( - Schema.String.annotate({ "description": "URL to the provider's status page" }) - ) - })) +export const AppRankingsResponse = Schema.Struct({ + "data": Schema.Array(AppRankingsItem).annotate({ + "description": + "Apps ranked per the requested `sort`, re-numbered 1..N after category filtering. `popular` sorts by `total_tokens` descending; `trending` sorts by absolute excess token growth descending and may return fewer than `limit` rows when few apps are growing." + }), + "meta": RankingsDailyMeta +}).annotate({ "identifier": "AppRankingsResponse" }) +export type RankingsDailyResponse = { + readonly "data": ReadonlyArray + readonly "meta": RankingsDailyMeta +} +export const RankingsDailyResponse = Schema.Struct({ + "data": Schema.Array(RankingsDailyItem).annotate({ + "description": + "Up to 51 rows per day — the top 50 public models by `total_tokens` for each UTC calendar date in the window, plus one aggregated `other` row summing every model outside that top 50 (omitted when the long tail is empty). Rows are sorted by `date` ascending, then by `total_tokens` descending, with `other` pinned last within its date. Ties between real models break alphabetically on `model_permaslug` so the order is stable across requests." + }), + "meta": RankingsDailyMeta +}).annotate({ "identifier": "RankingsDailyResponse" }) +export type Union_12 = ReadonlyArray | null +export const Union_12 = Schema.Union([Schema.Array(ReasoningEffort), Schema.Null]).annotate({ + "description": + "Allowed reasoning effort values for this model, in descending effort order (highest first). Null means no allowlist — all gateway effort values are accepted." }) -export type ListProviders500 = InternalServerResponse -export const ListProviders500 = InternalServerResponse -export type ListParams = { readonly "include_disabled"?: string; readonly "offset"?: string } -export const ListParams = Schema.Struct({ - "include_disabled": Schema.optionalKey( - Schema.String.annotate({ "description": "Whether to include disabled API keys in the response" }) - ), - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of API keys to skip for pagination" })) +export type ReasoningDetailEncrypted = { + readonly "data": string + readonly "format"?: ReasoningFormat + readonly "id"?: string | null + readonly "index"?: number + readonly "type": "reasoning.encrypted" +} +export const ReasoningDetailEncrypted = Schema.Struct({ + "data": Schema.String, + "format": Schema.optionalKey(ReasoningFormat), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "index": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "type": Schema.Literal("reasoning.encrypted") +}).annotate({ "description": "Reasoning detail encrypted schema", "identifier": "ReasoningDetailEncrypted" }) +export type ReasoningDetailServerToolCall = { + readonly "arguments": string + readonly "format"?: ReasoningFormat + readonly "id"?: string | null + readonly "index"?: number + readonly "result": string + readonly "tool_call_id"?: string | null + readonly "tool_name": string + readonly "type": "reasoning.server_tool_call" +} +export const ReasoningDetailServerToolCall = Schema.Struct({ + "arguments": Schema.String, + "format": Schema.optionalKey(ReasoningFormat), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "index": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "result": Schema.String, + "tool_call_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "tool_name": Schema.String, + "type": Schema.Literal("reasoning.server_tool_call") +}).annotate({ + "description": + "Record of an OpenRouter server-tool invocation (e.g. openrouter:fusion), carried in reasoning_details so a prior tool call can be rehydrated into a later turn of the same conversation.", + "identifier": "ReasoningDetailServerToolCall" }) -export type List200 = { - readonly "data": ReadonlyArray< - { - readonly "hash": string - readonly "name": string - readonly "label": string - readonly "disabled": boolean - readonly "limit": number - readonly "limit_remaining": number - readonly "limit_reset": string - readonly "include_byok_in_limit": boolean - readonly "usage": number - readonly "usage_daily": number - readonly "usage_weekly": number - readonly "usage_monthly": number - readonly "byok_usage": number - readonly "byok_usage_daily": number - readonly "byok_usage_weekly": number - readonly "byok_usage_monthly": number - readonly "created_at": string - readonly "updated_at": string - readonly "expires_at"?: string - } - > +export type ReasoningDetailSummary = { + readonly "format"?: ReasoningFormat + readonly "id"?: string | null + readonly "index"?: number + readonly "summary": string + readonly "type": "reasoning.summary" } -export const List200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), - "name": Schema.String.annotate({ "description": "Name of the API key" }), - "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), - "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), - "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( - Schema.isFinite() - ), - "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( - Schema.isFinite() - ), - "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), - "include_byok_in_limit": Schema.Boolean.annotate({ - "description": "Whether to include external BYOK usage in the credit limit" - }), - "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) - .check(Schema.isFinite()), - "usage_weekly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "usage_monthly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC month" - }).check(Schema.isFinite()), - "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "byok_usage_daily": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC day" - }).check(Schema.isFinite()), - "byok_usage_weekly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "byok_usage_monthly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for current UTC month" - }).check(Schema.isFinite()), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), - "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", - "format": "date-time" - }) - ) - })).annotate({ "description": "List of API keys" }) +export const ReasoningDetailSummary = Schema.Struct({ + "format": Schema.optionalKey(ReasoningFormat), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "index": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "summary": Schema.String, + "type": Schema.Literal("reasoning.summary") +}).annotate({ "description": "Reasoning detail summary schema", "identifier": "ReasoningDetailSummary" }) +export type ReasoningDetailText = { + readonly "format"?: ReasoningFormat + readonly "id"?: string | null + readonly "index"?: number + readonly "signature"?: string | null + readonly "text"?: string | null + readonly "type": "reasoning.text" +} +export const ReasoningDetailText = Schema.Struct({ + "format": Schema.optionalKey(ReasoningFormat), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "index": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "signature": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "text": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("reasoning.text") +}).annotate({ "description": "Reasoning detail text schema", "identifier": "ReasoningDetailText" }) +export type BaseReasoningSummaryPartAddedEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number + readonly "summary_index": number + readonly "type": "response.reasoning_summary_part.added" +} +export const BaseReasoningSummaryPartAddedEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_part.added") +}).annotate({ + "description": "Event emitted when a reasoning summary part is added", + "identifier": "BaseReasoningSummaryPartAddedEvent" }) -export type List401 = UnauthorizedResponse -export const List401 = UnauthorizedResponse -export type List429 = TooManyRequestsResponse -export const List429 = TooManyRequestsResponse -export type List500 = InternalServerResponse -export const List500 = InternalServerResponse -export type CreateKeysRequestJson = { - readonly "name": string - readonly "limit"?: number - readonly "limit_reset"?: "daily" | "weekly" | "monthly" - readonly "include_byok_in_limit"?: boolean - readonly "expires_at"?: string +export type BaseReasoningSummaryPartDoneEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number + readonly "summary_index": number + readonly "type": "response.reasoning_summary_part.done" } -export const CreateKeysRequestJson = Schema.Struct({ - "name": Schema.String.annotate({ "description": "Name for the new API key" }).check(Schema.isMinLength(1)), - "limit": Schema.optionalKey( - Schema.Number.annotate({ "description": "Optional spending limit for the API key in USD" }).check(Schema.isFinite()) - ), - "limit_reset": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": - "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." - }) - ), - "include_byok_in_limit": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) +export const BaseReasoningSummaryPartDoneEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_part.done") +}).annotate({ + "description": "Event emitted when a reasoning summary part is complete", + "identifier": "BaseReasoningSummaryPartDoneEvent" +}) +export type OutputReasoningItem = { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": "reasoning" + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null +} +export const OutputReasoningItem = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") }))]) ), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": - "Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected", - "format": "date-time" + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Literal("reasoning"), + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" }) ) +}).annotate({ "description": "An output item containing reasoning", "identifier": "OutputReasoningItem" }) +export type ReasoningItem = { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": "reasoning" + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null +} +export const ReasoningItem = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") }))]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Literal("reasoning"), + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Reasoning output item with signature and format extensions", + "identifier": "ReasoningItem" }) -export type CreateKeys201 = { - readonly "data": { - readonly "hash": string - readonly "name": string - readonly "label": string - readonly "disabled": boolean - readonly "limit": number - readonly "limit_remaining": number - readonly "limit_reset": string - readonly "include_byok_in_limit": boolean - readonly "usage": number - readonly "usage_daily": number - readonly "usage_weekly": number - readonly "usage_monthly": number - readonly "byok_usage": number - readonly "byok_usage_daily": number - readonly "byok_usage_weekly": number - readonly "byok_usage_monthly": number - readonly "created_at": string - readonly "updated_at": string - readonly "expires_at"?: string - } - readonly "key": string +export type ReasoningSummaryPartAddedEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number + readonly "summary_index": number + readonly "type": "response.reasoning_summary_part.added" } -export const CreateKeys201 = Schema.Struct({ - "data": Schema.Struct({ - "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), - "name": Schema.String.annotate({ "description": "Name of the API key" }), - "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), - "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), - "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( - Schema.isFinite() - ), - "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( - Schema.isFinite() - ), - "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), - "include_byok_in_limit": Schema.Boolean.annotate({ - "description": "Whether to include external BYOK usage in the credit limit" - }), - "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) - .check(Schema.isFinite()), - "usage_weekly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "usage_monthly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC month" - }).check(Schema.isFinite()), - "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "byok_usage_daily": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC day" - }).check(Schema.isFinite()), - "byok_usage_weekly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "byok_usage_monthly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for current UTC month" - }).check(Schema.isFinite()), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), - "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", - "format": "date-time" - }) - ) - }).annotate({ "description": "The created API key information" }), - "key": Schema.String.annotate({ "description": "The actual API key string (only shown once)" }) +export const ReasoningSummaryPartAddedEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_part.added") +}).annotate({ + "description": "Event emitted when a reasoning summary part is added", + "identifier": "ReasoningSummaryPartAddedEvent" }) -export type CreateKeys400 = BadRequestResponse -export const CreateKeys400 = BadRequestResponse -export type CreateKeys401 = UnauthorizedResponse -export const CreateKeys401 = UnauthorizedResponse -export type CreateKeys429 = TooManyRequestsResponse -export const CreateKeys429 = TooManyRequestsResponse -export type CreateKeys500 = InternalServerResponse -export const CreateKeys500 = InternalServerResponse -export type GetKey200 = { - readonly "data": { - readonly "hash": string - readonly "name": string - readonly "label": string - readonly "disabled": boolean - readonly "limit": number - readonly "limit_remaining": number - readonly "limit_reset": string - readonly "include_byok_in_limit": boolean - readonly "usage": number - readonly "usage_daily": number - readonly "usage_weekly": number - readonly "usage_monthly": number - readonly "byok_usage": number - readonly "byok_usage_daily": number - readonly "byok_usage_weekly": number - readonly "byok_usage_monthly": number - readonly "created_at": string - readonly "updated_at": string - readonly "expires_at"?: string - } +export type ReasoningSummaryPartDoneEvent = { + readonly "item_id": string + readonly "output_index": number + readonly "part": ReasoningSummaryText + readonly "sequence_number": number + readonly "summary_index": number + readonly "type": "response.reasoning_summary_part.done" } -export const GetKey200 = Schema.Struct({ - "data": Schema.Struct({ - "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), - "name": Schema.String.annotate({ "description": "Name of the API key" }), - "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), - "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), - "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( - Schema.isFinite() - ), - "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( - Schema.isFinite() - ), - "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), - "include_byok_in_limit": Schema.Boolean.annotate({ - "description": "Whether to include external BYOK usage in the credit limit" - }), - "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) - .check(Schema.isFinite()), - "usage_weekly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "usage_monthly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC month" - }).check(Schema.isFinite()), - "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "byok_usage_daily": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC day" - }).check(Schema.isFinite()), - "byok_usage_weekly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "byok_usage_monthly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for current UTC month" - }).check(Schema.isFinite()), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), - "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", - "format": "date-time" - }) - ) - }).annotate({ "description": "The API key information" }) +export const ReasoningSummaryPartDoneEvent = Schema.Struct({ + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": ReasoningSummaryText, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "summary_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.reasoning_summary_part.done") +}).annotate({ + "description": "Event emitted when a reasoning summary part is complete", + "identifier": "ReasoningSummaryPartDoneEvent" }) -export type GetKey401 = UnauthorizedResponse -export const GetKey401 = UnauthorizedResponse -export type GetKey404 = NotFoundResponse -export const GetKey404 = NotFoundResponse -export type GetKey429 = TooManyRequestsResponse -export const GetKey429 = TooManyRequestsResponse -export type GetKey500 = InternalServerResponse -export const GetKey500 = InternalServerResponse -export type DeleteKeys200 = { readonly "deleted": true } -export const DeleteKeys200 = Schema.Struct({ - "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the API key was deleted" }) +export type Objects_5 = { + readonly "context"?: ReasoningContext + readonly "effort"?: ReasoningEffort + readonly "mode"?: ReasoningMode + readonly "summary"?: ReasoningSummaryVerbosity + readonly [x: string]: Schema.Json +} +export const Objects_5 = Schema.StructWithRest( + Schema.Struct({ + "context": Schema.optionalKey(ReasoningContext), + "effort": Schema.optionalKey(ReasoningEffort), + "mode": Schema.optionalKey(ReasoningMode), + "summary": Schema.optionalKey(ReasoningSummaryVerbosity) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type Union_13 = { + readonly "context"?: ReasoningContext + readonly "effort"?: ReasoningEffort + readonly "mode"?: ReasoningMode + readonly "summary"?: ReasoningSummaryVerbosity + readonly "enabled"?: boolean | null + readonly "max_tokens"?: number | null +} +export const Union_13 = Schema.Union([Schema.Struct({ + "context": Schema.optionalKey(ReasoningContext), + "effort": Schema.optionalKey(ReasoningEffort), + "mode": Schema.optionalKey(ReasoningMode), + "summary": Schema.optionalKey(ReasoningSummaryVerbosity), + "enabled": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "max_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ) +})]) +export type OutputItemReasoning = { + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": "reasoning" +} +export const OutputItemReasoning = Schema.Struct({ + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Literal("reasoning") +}).annotate({ "identifier": "OutputItemReasoning" }) +export type RequestMetadata = Objects_149 | null +export const RequestMetadata = Schema.Union([Objects_149, Schema.Null]).annotate({ + "description": + "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.", + "identifier": "RequestMetadata" }) -export type DeleteKeys401 = UnauthorizedResponse -export const DeleteKeys401 = UnauthorizedResponse -export type DeleteKeys404 = NotFoundResponse -export const DeleteKeys404 = NotFoundResponse -export type DeleteKeys429 = TooManyRequestsResponse -export const DeleteKeys429 = TooManyRequestsResponse -export type DeleteKeys500 = InternalServerResponse -export const DeleteKeys500 = InternalServerResponse -export type UpdateKeysRequestJson = { - readonly "name"?: string - readonly "disabled"?: boolean - readonly "limit"?: number - readonly "limit_reset"?: "daily" | "weekly" | "monthly" - readonly "include_byok_in_limit"?: boolean +export type RequestTimeoutResponse = { + readonly "error": RequestTimeoutResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null } -export const UpdateKeysRequestJson = Schema.Struct({ - "name": Schema.optionalKey(Schema.String.annotate({ "description": "New name for the API key" })), - "disabled": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Whether to disable the API key" })), - "limit": Schema.optionalKey( - Schema.Number.annotate({ "description": "New spending limit for the API key in USD" }).check(Schema.isFinite()) - ), - "limit_reset": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": - "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 const RequestTimeoutResponse = Schema.Struct({ + "error": RequestTimeoutResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "include_byok_in_limit": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) - ) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Request Timeout - Operation exceeded time limit", + "identifier": "RequestTimeoutResponse" }) -export type UpdateKeys200 = { - readonly "data": { - readonly "hash": string - readonly "name": string - readonly "label": string - readonly "disabled": boolean - readonly "limit": number - readonly "limit_remaining": number - readonly "limit_reset": string - readonly "include_byok_in_limit": boolean - readonly "usage": number - readonly "usage_daily": number - readonly "usage_weekly": number - readonly "usage_monthly": number - readonly "byok_usage": number - readonly "byok_usage_daily": number - readonly "byok_usage_weekly": number - readonly "byok_usage_monthly": number - readonly "created_at": string - readonly "updated_at": string - readonly "expires_at"?: string - } +export type Arrays_10 = ReadonlyArray +export const Arrays_10 = Schema.Array(RouterAttempt) +export type ChatSearchModelsServerTool = { + readonly "parameters"?: SearchModelsServerToolConfig + readonly "type": "openrouter:experimental__search_models" } -export const UpdateKeys200 = Schema.Struct({ - "data": Schema.Struct({ - "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), - "name": Schema.String.annotate({ "description": "Name of the API key" }), - "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), - "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), - "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( - Schema.isFinite() - ), - "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( - Schema.isFinite() - ), - "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), - "include_byok_in_limit": Schema.Boolean.annotate({ - "description": "Whether to include external BYOK usage in the credit limit" - }), - "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) - .check(Schema.isFinite()), - "usage_weekly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "usage_monthly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC month" - }).check(Schema.isFinite()), - "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "byok_usage_daily": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC day" - }).check(Schema.isFinite()), - "byok_usage_weekly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "byok_usage_monthly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for current UTC month" - }).check(Schema.isFinite()), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), - "updated_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was last updated" }), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", - "format": "date-time" - }) - ) - }).annotate({ "description": "The updated API key information" }) +export const ChatSearchModelsServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(SearchModelsServerToolConfig), + "type": Schema.Literal("openrouter:experimental__search_models") +}).annotate({ + "description": "OpenRouter built-in server tool: searches and filters AI models available on OpenRouter", + "identifier": "ChatSearchModelsServerTool" }) -export type UpdateKeys400 = BadRequestResponse -export const UpdateKeys400 = BadRequestResponse -export type UpdateKeys401 = UnauthorizedResponse -export const UpdateKeys401 = UnauthorizedResponse -export type UpdateKeys404 = NotFoundResponse -export const UpdateKeys404 = NotFoundResponse -export type UpdateKeys429 = TooManyRequestsResponse -export const UpdateKeys429 = TooManyRequestsResponse -export type UpdateKeys500 = InternalServerResponse -export const UpdateKeys500 = InternalServerResponse -export type ListGuardrailsParams = { readonly "offset"?: string; readonly "limit"?: string } -export const ListGuardrailsParams = Schema.Struct({ - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), - "limit": Schema.optionalKey( - Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) - ) +export type MessagesSearchModelsServerTool = { + readonly "parameters"?: SearchModelsServerToolConfig + readonly "type": "openrouter:experimental__search_models" +} +export const MessagesSearchModelsServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(SearchModelsServerToolConfig), + "type": Schema.Literal("openrouter:experimental__search_models") +}).annotate({ + "description": "OpenRouter built-in server tool: searches and filters AI models available on OpenRouter", + "identifier": "MessagesSearchModelsServerTool" }) -export type ListGuardrails200 = { - readonly "data": ReadonlyArray< - { - readonly "id": string - readonly "name": string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean - readonly "created_at": string - readonly "updated_at"?: string - } - > - readonly "total_count": number +export type SearchModelsServerTool_OpenRouter = { + readonly "parameters"?: SearchModelsServerToolConfig + readonly "type": "openrouter:experimental__search_models" } -export const ListGuardrails200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), - "name": Schema.String.annotate({ "description": "Name of the guardrail" }), - "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" - }) - ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) - ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) - ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), - "updated_at": Schema.optionalKey( - Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) - ) - })).annotate({ "description": "List of guardrails" }), - "total_count": Schema.Number.annotate({ "description": "Total number of guardrails" }).check(Schema.isFinite()) +export const SearchModelsServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(SearchModelsServerToolConfig), + "type": Schema.Literal("openrouter:experimental__search_models") +}).annotate({ + "description": "OpenRouter built-in server tool: searches and filters AI models available on OpenRouter", + "identifier": "SearchModelsServerTool_OpenRouter" }) -export type ListGuardrails401 = UnauthorizedResponse -export const ListGuardrails401 = UnauthorizedResponse -export type ListGuardrails500 = InternalServerResponse -export const ListGuardrails500 = InternalServerResponse -export type CreateGuardrailRequestJson = { - readonly "name": string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean +export type ServerToolUseDetails = Objects_150 | null +export const ServerToolUseDetails = Schema.Union([Objects_150, Schema.Null]).annotate({ + "description": "Usage for server-side tool execution (e.g., web search)", + "identifier": "ServerToolUseDetails" +}) +export type ServiceUnavailableResponse = { + readonly "error": ServiceUnavailableResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null } -export const CreateGuardrailRequestJson = Schema.Struct({ - "name": Schema.String.annotate({ "description": "Name for the new guardrail" }).check(Schema.isMinLength(1)).check( - Schema.isMaxLength(200) - ), - "description": Schema.optionalKey( - Schema.String.annotate({ "description": "Description of the guardrail" }).check(Schema.isMaxLength(1000)) - ), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" - }) - ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }).check(Schema.isMinLength(1)) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ - "description": "Array of model identifiers (slug or canonical_slug accepted)" - }).check(Schema.isMinLength(1)) +export const ServiceUnavailableResponse = Schema.Struct({ + "error": ServiceUnavailableResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) - ) + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Service Unavailable - Service temporarily unavailable", + "identifier": "ServiceUnavailableResponse" }) -export type CreateGuardrail201 = { - readonly "data": { - readonly "id": string - readonly "name": string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean - readonly "created_at": string - readonly "updated_at"?: string +export type OutputShellCallItem = { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null } + readonly "call_id": string + readonly "id": string + readonly "status": ShellCallStatus + readonly "type": "shell_call" } -export const CreateGuardrail201 = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), - "name": Schema.String.annotate({ "description": "Name of the guardrail" }), - "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" - }) - ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) - ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) - ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), - "updated_at": Schema.optionalKey( - Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) - ) - }).annotate({ "description": "The created guardrail" }) +export const OutputShellCallItem = Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": ShellCallStatus, + "type": Schema.Literal("shell_call") +}).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool.", + "identifier": "OutputShellCallItem" }) -export type CreateGuardrail400 = BadRequestResponse -export const CreateGuardrail400 = BadRequestResponse -export type CreateGuardrail401 = UnauthorizedResponse -export const CreateGuardrail401 = UnauthorizedResponse -export type CreateGuardrail500 = InternalServerResponse -export const CreateGuardrail500 = InternalServerResponse -export type GetGuardrail200 = { - readonly "data": { - readonly "id": string - readonly "name": string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean - readonly "created_at": string - readonly "updated_at"?: string - } +export type OutputShellCallOutputItem = { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": ShellCallStatus + readonly "type": "shell_call_output" } -export const GetGuardrail200 = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), - "name": Schema.String.annotate({ "description": "Name of the guardrail" }), - "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" - }) - ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) - ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) - ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), - "updated_at": Schema.optionalKey( - Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) - ) - }).annotate({ "description": "The guardrail" }) +export const OutputShellCallOutputItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": ShellCallStatus, + "type": Schema.Literal("shell_call_output") +}).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome.", + "identifier": "OutputShellCallOutputItem" }) -export type GetGuardrail401 = UnauthorizedResponse -export const GetGuardrail401 = UnauthorizedResponse -export type GetGuardrail404 = NotFoundResponse -export const GetGuardrail404 = NotFoundResponse -export type GetGuardrail500 = InternalServerResponse -export const GetGuardrail500 = InternalServerResponse -export type DeleteGuardrail200 = { readonly "deleted": true } -export const DeleteGuardrail200 = Schema.Struct({ - "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the guardrail was deleted" }) +export type StopServerToolsWhenCondition = + | StopServerToolsWhenStepCountIs + | StopServerToolsWhenHasToolCall + | StopServerToolsWhenMaxTokensUsed + | StopServerToolsWhenMaxCost + | StopServerToolsWhenFinishReasonIs +export const StopServerToolsWhenCondition = Schema.Union([ + StopServerToolsWhenStepCountIs, + StopServerToolsWhenHasToolCall, + StopServerToolsWhenMaxTokensUsed, + StopServerToolsWhenMaxCost, + StopServerToolsWhenFinishReasonIs +], { mode: "oneOf" }).annotate({ + "description": "A single condition that, when met, halts the server-tool agent loop.", + "identifier": "StopServerToolsWhenCondition" }) -export type DeleteGuardrail401 = UnauthorizedResponse -export const DeleteGuardrail401 = UnauthorizedResponse -export type DeleteGuardrail404 = NotFoundResponse -export const DeleteGuardrail404 = NotFoundResponse -export type DeleteGuardrail500 = InternalServerResponse -export const DeleteGuardrail500 = InternalServerResponse -export type UpdateGuardrailRequestJson = { - readonly "name"?: string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean +export type STTResponse = { + readonly "duration"?: number + readonly "language"?: string + readonly "segments"?: ReadonlyArray + readonly "task"?: string + readonly "text": string + readonly "usage"?: STTUsage + readonly "words"?: ReadonlyArray } -export const UpdateGuardrailRequestJson = Schema.Struct({ - "name": Schema.optionalKey( - Schema.String.annotate({ "description": "New name for the guardrail" }).check(Schema.isMinLength(1)).check( - Schema.isMaxLength(200) - ) - ), - "description": Schema.optionalKey( - Schema.String.annotate({ "description": "New description for the guardrail" }).check(Schema.isMaxLength(1000)) +export const STTResponse = Schema.Struct({ + "duration": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Duration of the input audio in seconds, present when response_format is verbose_json", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) ), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "New spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "Detected or forced language, present when response_format is verbose_json" }) ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "New list of allowed provider IDs" }).check( - Schema.isMinLength(1) - ) + "segments": Schema.optionalKey( + Schema.Array(STTSegment).annotate({ + "description": "Timestamped transcript segments, present when response_format is verbose_json" + }) ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ - "description": "Array of model identifiers (slug or canonical_slug accepted)" - }).check(Schema.isMinLength(1)) + "task": Schema.optionalKey( + Schema.String.annotate({ "description": "The task performed, present when response_format is verbose_json" }) ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + "text": Schema.String.annotate({ "description": "The transcribed text" }), + "usage": Schema.optionalKey(STTUsage), + "words": Schema.optionalKey( + Schema.Array(STTWord).annotate({ + "description": "Timestamped words, present when the provider returns word-level timestamps" + }) ) +}).annotate({ + "description": "STT response containing transcribed text and optional usage statistics", + "identifier": "STTResponse" }) -export type UpdateGuardrail200 = { - readonly "data": { - readonly "id": string - readonly "name": string - readonly "description"?: string - readonly "limit_usd"?: number - readonly "reset_interval"?: "daily" | "weekly" | "monthly" - readonly "allowed_providers"?: ReadonlyArray - readonly "allowed_models"?: ReadonlyArray - readonly "enforce_zdr"?: boolean - readonly "created_at": string - readonly "updated_at"?: string - } +export type Arrays_12 = ReadonlyArray +export const Arrays_12 = Schema.Array(SubagentNestedTool).annotate({ + "description": + "Tools the subagent may use while executing a delegated task. The subagent runs as an agentic sub-agent over these tools, then returns its outcome. Only OpenRouter server tools are supported — function tools are rejected — and the list must not include the subagent tool itself." +}) +export type ImageEndpoint = { + readonly "allowed_passthrough_parameters": ReadonlyArray + readonly "pricing": ReadonlyArray + readonly "provider_name": string + readonly "provider_slug": string + readonly "provider_tag": string | null + readonly "supported_parameters": SupportedParameters + readonly "supports_streaming": boolean } -export const UpdateGuardrail200 = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), - "name": Schema.String.annotate({ "description": "Name of the guardrail" }), - "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the guardrail" })), - "limit_usd": Schema.optionalKey( - Schema.Number.annotate({ "description": "Spending limit in USD" }).check(Schema.isFinite()).check( - Schema.isGreaterThanOrEqualTo(0) - ) - ), - "reset_interval": Schema.optionalKey( - Schema.Literals(["daily", "weekly", "monthly"]).annotate({ - "description": "Interval at which the limit resets (daily, weekly, monthly)" - }) - ), - "allowed_providers": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "List of allowed provider IDs" }) - ), - "allowed_models": Schema.optionalKey( - Schema.Array(Schema.String).annotate({ "description": "Array of model canonical_slugs (immutable identifiers)" }) +export const ImageEndpoint = Schema.Struct({ + "allowed_passthrough_parameters": Schema.Array(Schema.String).annotate({ + "description": "Provider-specific options accepted under provider.options[provider_slug]." + }), + "pricing": Schema.Array(ImagePricingEntry).annotate({ "description": "Billable pricing lines for this endpoint." }), + "provider_name": Schema.String.annotate({ "description": "Provider display name" }), + "provider_slug": Schema.String.annotate({ "description": "Provider slug" }), + "provider_tag": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Provider tag for request-side selection" + }), + "supported_parameters": Schema.suspend((): Schema.Codec => SupportedParameters).annotate({ + "description": "The definitive set of parameters this endpoint accepts for this model." + }), + "supports_streaming": Schema.Boolean.annotate({ + "description": "Whether this endpoint supports native SSE streaming (`stream: true` in the request)." + }) +}).annotate({ "description": "An endpoint that serves a given image model.", "identifier": "ImageEndpoint" }) +export type TaskClassificationItem = { + readonly "category_token_share": number + readonly "category_usage_share": number + readonly "display_name": string + readonly "macro_category": string + readonly "models": ReadonlyArray + readonly "tag": string + readonly "token_share": number + readonly "usage_share": number +} +export const TaskClassificationItem = Schema.Struct({ + "category_token_share": Schema.Number.annotate({ + "description": + "Fraction of this classification's token volume within its macro-category (0–1). Sums to 1 across all classifications sharing the same `macro_category`.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "category_usage_share": Schema.Number.annotate({ + "description": + "Fraction of this classification's usage within its macro-category (0–1). Sums to 1 across all classifications sharing the same `macro_category`.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "display_name": Schema.String.annotate({ "description": "Human-readable label for the classification." }), + "macro_category": Schema.String.annotate({ + "description": "Coarse grouping derived from the tag prefix: `code`, `data`, `agent`, or `general`." + }), + "models": Schema.Array(TaskClassificationModel).annotate({ + "description": + "Top models for this classification by request volume, sorted descending. Each entry reports the model's share of this classification's requests and tokens." + }), + "tag": Schema.String.annotate({ + "description": "Classification tag identifier (e.g. `code:general_impl`, `agent:web_search`)." + }), + "token_share": Schema.Number.annotate({ + "description": + "Fraction of classified sampled token volume (prompt + completion) attributed to this classification (0–1). The unclassified `other` bucket is excluded from the denominator.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_share": Schema.Number.annotate({ + "description": + "Fraction of classified sampled requests attributed to this classification (0–1). The unclassified `other` bucket is excluded from the denominator.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) +}).annotate({ "identifier": "TaskClassificationItem" }) +export type CodeInterpreterCallItem = { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": ToolCallStatus + readonly "type": "code_interpreter_call" +} +export const CodeInterpreterCallItem = Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) ), - "enforce_zdr": Schema.optionalKey( - Schema.Boolean.annotate({ "description": "Whether to enforce zero data retention" }) + Schema.Null + ]), + "status": ToolCallStatus, + "type": Schema.Literal("code_interpreter_call") +}).annotate({ + "description": "A code interpreter execution call with outputs", + "identifier": "CodeInterpreterCallItem" +}) +export type FunctionCallItem = { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: ToolCallStatus + readonly "type": "function_call" +} +export const FunctionCallItem = Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey(ToolCallStatus), + "type": Schema.Literal("function_call") +}).annotate({ "description": "A function call initiated by the model", "identifier": "FunctionCallItem" }) +export type LocalShellCallItem = { + readonly "action": { + readonly "command": ReadonlyArray + readonly "env": {} + readonly "timeout_ms"?: number | null + readonly "type": "exec" + readonly "user"?: string | null + readonly "working_directory"?: string | null + } + readonly "call_id": string + readonly "id": string + readonly "status": ToolCallStatus + readonly "type": "local_shell_call" +} +export const LocalShellCallItem = Schema.Struct({ + "action": Schema.Struct({ + "command": Schema.Array(Schema.String), + "env": Schema.Struct({}), + "timeout_ms": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), - "updated_at": Schema.optionalKey( - Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was last updated" }) - ) - }).annotate({ "description": "The updated guardrail" }) + "type": Schema.Literal("exec"), + "user": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "working_directory": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + "call_id": Schema.String, + "id": Schema.String, + "status": ToolCallStatus, + "type": Schema.Literal("local_shell_call") +}).annotate({ "description": "A local shell command execution call", "identifier": "LocalShellCallItem" }) +export type LocalShellCallOutputItem = { + readonly "id": string + readonly "output": string + readonly "status"?: ToolCallStatus | null + readonly "type": "local_shell_call_output" +} +export const LocalShellCallOutputItem = Schema.Struct({ + "id": Schema.String, + "output": Schema.String, + "status": Schema.optionalKey(Schema.Union([ToolCallStatus, Schema.Null])), + "type": Schema.Literal("local_shell_call_output") +}).annotate({ "description": "Output from a local shell command execution", "identifier": "LocalShellCallOutputItem" }) +export type OpenAIResponseFunctionToolCall = { + readonly "arguments": string + readonly "call_id": string + readonly "id"?: string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: ToolCallStatus + readonly "type": "function_call" +} +export const OpenAIResponseFunctionToolCall = Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey(ToolCallStatus), + "type": Schema.Literal("function_call") +}).annotate({ "identifier": "OpenAIResponseFunctionToolCall" }) +export type OutputAdvisorServerToolItem = { + readonly "advice"?: string + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:advisor" +} +export const OutputAdvisorServerToolItem = Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:advisor") +}).annotate({ + "description": "An openrouter:advisor server tool output item", + "identifier": "OutputAdvisorServerToolItem" }) -export type UpdateGuardrail400 = BadRequestResponse -export const UpdateGuardrail400 = BadRequestResponse -export type UpdateGuardrail401 = UnauthorizedResponse -export const UpdateGuardrail401 = UnauthorizedResponse -export type UpdateGuardrail404 = NotFoundResponse -export const UpdateGuardrail404 = NotFoundResponse -export type UpdateGuardrail500 = InternalServerResponse -export const UpdateGuardrail500 = InternalServerResponse -export type ListKeyAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } -export const ListKeyAssignmentsParams = Schema.Struct({ - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), - "limit": Schema.optionalKey( - Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) - ) +export type OutputBashServerToolItem = { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": "openrouter:bash" +} +export const OutputBashServerToolItem = Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Literal("openrouter:bash") +}).annotate({ "description": "An openrouter:bash server tool output item", "identifier": "OutputBashServerToolItem" }) +export type OutputBrowserUseServerToolItem = { + readonly "action"?: string + readonly "id"?: string + readonly "screenshotB64"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:browser_use" +} +export const OutputBrowserUseServerToolItem = Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "screenshotB64": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:browser_use") +}).annotate({ + "description": "An openrouter:browser_use server tool output item", + "identifier": "OutputBrowserUseServerToolItem" }) -export type ListKeyAssignments200 = { - readonly "data": ReadonlyArray< - { - readonly "id": string - readonly "key_hash": string - readonly "guardrail_id": string - readonly "key_name": string - readonly "key_label": string - readonly "assigned_by": string - readonly "created_at": string - } - > - readonly "total_count": number +export type OutputCodeInterpreterCallItem = { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": ToolCallStatus + readonly "type": "code_interpreter_call" } -export const ListKeyAssignments200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), - "key_hash": Schema.String.annotate({ "description": "Hash of the assigned API key" }), - "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), - "key_name": Schema.String.annotate({ "description": "Name of the API key" }), - "key_label": Schema.String.annotate({ "description": "Label of the API key" }), - "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) - })).annotate({ "description": "List of key assignments" }), - "total_count": Schema.Number.annotate({ "description": "Total number of key assignments for this guardrail" }).check( - Schema.isFinite() - ) +export const OutputCodeInterpreterCallItem = Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": ToolCallStatus, + "type": Schema.Literal("code_interpreter_call") +}).annotate({ + "description": "A code interpreter execution call with outputs", + "identifier": "OutputCodeInterpreterCallItem" }) -export type ListKeyAssignments401 = UnauthorizedResponse -export const ListKeyAssignments401 = UnauthorizedResponse -export type ListKeyAssignments500 = InternalServerResponse -export const ListKeyAssignments500 = InternalServerResponse -export type ListMemberAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } -export const ListMemberAssignmentsParams = Schema.Struct({ - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), - "limit": Schema.optionalKey( - Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) - ) +export type OutputCodeInterpreterServerToolItem = { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "language"?: string + readonly "status": ToolCallStatus + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": "openrouter:code_interpreter" +} +export const OutputCodeInterpreterServerToolItem = Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "language": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Literal("openrouter:code_interpreter") +}).annotate({ + "description": "An openrouter:code_interpreter server tool output item", + "identifier": "OutputCodeInterpreterServerToolItem" }) -export type ListMemberAssignments200 = { - readonly "data": ReadonlyArray< - { - readonly "id": string - readonly "user_id": string - readonly "organization_id": string - readonly "guardrail_id": string - readonly "assigned_by": string - readonly "created_at": string - } - > - readonly "total_count": number +export type OutputDatetimeItem = { + readonly "datetime": string + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "timezone": string + readonly "type": "openrouter:datetime" } -export const ListMemberAssignments200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), - "user_id": Schema.String.annotate({ "description": "Clerk user ID of the assigned member" }), - "organization_id": Schema.String.annotate({ "description": "Organization ID" }), - "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), - "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) - })).annotate({ "description": "List of member assignments" }), - "total_count": Schema.Number.annotate({ "description": "Total number of member assignments" }).check( - Schema.isFinite() - ) +export const OutputDatetimeItem = Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Literal("openrouter:datetime") +}).annotate({ "description": "An openrouter:datetime server tool output item", "identifier": "OutputDatetimeItem" }) +export type OutputFileSearchServerToolItem = { + readonly "id"?: string + readonly "queries"?: ReadonlyArray + readonly "status": ToolCallStatus + readonly "type": "openrouter:file_search" +} +export const OutputFileSearchServerToolItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:file_search") +}).annotate({ + "description": "An openrouter:file_search server tool output item", + "identifier": "OutputFileSearchServerToolItem" }) -export type ListMemberAssignments401 = UnauthorizedResponse -export const ListMemberAssignments401 = UnauthorizedResponse -export type ListMemberAssignments500 = InternalServerResponse -export const ListMemberAssignments500 = InternalServerResponse -export type ListGuardrailKeyAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } -export const ListGuardrailKeyAssignmentsParams = Schema.Struct({ - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), - "limit": Schema.optionalKey( - Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) - ) -}) -export type ListGuardrailKeyAssignments200 = { - readonly "data": ReadonlyArray< - { - readonly "id": string - readonly "key_hash": string - readonly "guardrail_id": string - readonly "key_name": string - readonly "key_label": string - readonly "assigned_by": string - readonly "created_at": string - } - > - readonly "total_count": number +export type OutputFilesServerToolItem = { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id"?: string + readonly "operation"?: string + readonly "result"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:files" } -export const ListGuardrailKeyAssignments200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), - "key_hash": Schema.String.annotate({ "description": "Hash of the assigned API key" }), - "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), - "key_name": Schema.String.annotate({ "description": "Name of the API key" }), - "key_label": Schema.String.annotate({ "description": "Label of the API key" }), - "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) - })).annotate({ "description": "List of key assignments" }), - "total_count": Schema.Number.annotate({ "description": "Total number of key assignments for this guardrail" }).check( - Schema.isFinite() - ) -}) -export type ListGuardrailKeyAssignments401 = UnauthorizedResponse -export const ListGuardrailKeyAssignments401 = UnauthorizedResponse -export type ListGuardrailKeyAssignments404 = NotFoundResponse -export const ListGuardrailKeyAssignments404 = NotFoundResponse -export type ListGuardrailKeyAssignments500 = InternalServerResponse -export const ListGuardrailKeyAssignments500 = InternalServerResponse -export type BulkAssignKeysToGuardrailRequestJson = { readonly "key_hashes": ReadonlyArray } -export const BulkAssignKeysToGuardrailRequestJson = Schema.Struct({ - "key_hashes": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ - "description": "Array of API key hashes to assign to the guardrail" - }).check(Schema.isMinLength(1)) -}) -export type BulkAssignKeysToGuardrail200 = { readonly "assigned_count": number } -export const BulkAssignKeysToGuardrail200 = Schema.Struct({ - "assigned_count": Schema.Number.annotate({ "description": "Number of keys successfully assigned" }).check( - Schema.isFinite() - ) -}) -export type BulkAssignKeysToGuardrail400 = BadRequestResponse -export const BulkAssignKeysToGuardrail400 = BadRequestResponse -export type BulkAssignKeysToGuardrail401 = UnauthorizedResponse -export const BulkAssignKeysToGuardrail401 = UnauthorizedResponse -export type BulkAssignKeysToGuardrail404 = NotFoundResponse -export const BulkAssignKeysToGuardrail404 = NotFoundResponse -export type BulkAssignKeysToGuardrail500 = InternalServerResponse -export const BulkAssignKeysToGuardrail500 = InternalServerResponse -export type ListGuardrailMemberAssignmentsParams = { readonly "offset"?: string; readonly "limit"?: string } -export const ListGuardrailMemberAssignmentsParams = Schema.Struct({ - "offset": Schema.optionalKey(Schema.String.annotate({ "description": "Number of records to skip for pagination" })), - "limit": Schema.optionalKey( - Schema.String.annotate({ "description": "Maximum number of records to return (max 100)" }) - ) -}) -export type ListGuardrailMemberAssignments200 = { - readonly "data": ReadonlyArray< - { - readonly "id": string - readonly "user_id": string - readonly "organization_id": string - readonly "guardrail_id": string - readonly "assigned_by": string - readonly "created_at": string - } - > - readonly "total_count": number +export const OutputFilesServerToolItem = Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:files") +}).annotate({ "description": "An openrouter:files server tool output item", "identifier": "OutputFilesServerToolItem" }) +export type OutputImageGenerationServerToolItem = { + readonly "id"?: string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:image_generation" } -export const ListGuardrailMemberAssignments200 = Schema.Struct({ - "data": Schema.Array(Schema.Struct({ - "id": Schema.String.annotate({ "description": "Unique identifier for the assignment", "format": "uuid" }), - "user_id": Schema.String.annotate({ "description": "Clerk user ID of the assigned member" }), - "organization_id": Schema.String.annotate({ "description": "Organization ID" }), - "guardrail_id": Schema.String.annotate({ "description": "ID of the guardrail", "format": "uuid" }), - "assigned_by": Schema.String.annotate({ "description": "User ID of who made the assignment" }), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the assignment was created" }) - })).annotate({ "description": "List of member assignments" }), - "total_count": Schema.Number.annotate({ "description": "Total number of member assignments" }).check( - Schema.isFinite() - ) -}) -export type ListGuardrailMemberAssignments401 = UnauthorizedResponse -export const ListGuardrailMemberAssignments401 = UnauthorizedResponse -export type ListGuardrailMemberAssignments404 = NotFoundResponse -export const ListGuardrailMemberAssignments404 = NotFoundResponse -export type ListGuardrailMemberAssignments500 = InternalServerResponse -export const ListGuardrailMemberAssignments500 = InternalServerResponse -export type BulkAssignMembersToGuardrailRequestJson = { readonly "member_user_ids": ReadonlyArray } -export const BulkAssignMembersToGuardrailRequestJson = Schema.Struct({ - "member_user_ids": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ - "description": "Array of member user IDs to assign to the guardrail" - }).check(Schema.isMinLength(1)) -}) -export type BulkAssignMembersToGuardrail200 = { readonly "assigned_count": number } -export const BulkAssignMembersToGuardrail200 = Schema.Struct({ - "assigned_count": Schema.Number.annotate({ "description": "Number of members successfully assigned" }).check( - Schema.isFinite() - ) -}) -export type BulkAssignMembersToGuardrail400 = BadRequestResponse -export const BulkAssignMembersToGuardrail400 = BadRequestResponse -export type BulkAssignMembersToGuardrail401 = UnauthorizedResponse -export const BulkAssignMembersToGuardrail401 = UnauthorizedResponse -export type BulkAssignMembersToGuardrail404 = NotFoundResponse -export const BulkAssignMembersToGuardrail404 = NotFoundResponse -export type BulkAssignMembersToGuardrail500 = InternalServerResponse -export const BulkAssignMembersToGuardrail500 = InternalServerResponse -export type BulkUnassignKeysFromGuardrailRequestJson = { readonly "key_hashes": ReadonlyArray } -export const BulkUnassignKeysFromGuardrailRequestJson = Schema.Struct({ - "key_hashes": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ - "description": "Array of API key hashes to unassign from the guardrail" - }).check(Schema.isMinLength(1)) -}) -export type BulkUnassignKeysFromGuardrail200 = { readonly "unassigned_count": number } -export const BulkUnassignKeysFromGuardrail200 = Schema.Struct({ - "unassigned_count": Schema.Number.annotate({ "description": "Number of keys successfully unassigned" }).check( - Schema.isFinite() - ) -}) -export type BulkUnassignKeysFromGuardrail400 = BadRequestResponse -export const BulkUnassignKeysFromGuardrail400 = BadRequestResponse -export type BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse -export const BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse -export type BulkUnassignKeysFromGuardrail404 = NotFoundResponse -export const BulkUnassignKeysFromGuardrail404 = NotFoundResponse -export type BulkUnassignKeysFromGuardrail500 = InternalServerResponse -export const BulkUnassignKeysFromGuardrail500 = InternalServerResponse -export type BulkUnassignMembersFromGuardrailRequestJson = { readonly "member_user_ids": ReadonlyArray } -export const BulkUnassignMembersFromGuardrailRequestJson = Schema.Struct({ - "member_user_ids": Schema.Array(Schema.String.check(Schema.isMinLength(1))).annotate({ - "description": "Array of member user IDs to unassign from the guardrail" - }).check(Schema.isMinLength(1)) -}) -export type BulkUnassignMembersFromGuardrail200 = { readonly "unassigned_count": number } -export const BulkUnassignMembersFromGuardrail200 = Schema.Struct({ - "unassigned_count": Schema.Number.annotate({ "description": "Number of members successfully unassigned" }).check( - Schema.isFinite() - ) +export const OutputImageGenerationServerToolItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt (possibly rewritten) that the image was generated from." }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:image_generation") +}).annotate({ + "description": "An openrouter:image_generation server tool output item", + "identifier": "OutputImageGenerationServerToolItem" }) -export type BulkUnassignMembersFromGuardrail400 = BadRequestResponse -export const BulkUnassignMembersFromGuardrail400 = BadRequestResponse -export type BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse -export const BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse -export type BulkUnassignMembersFromGuardrail404 = NotFoundResponse -export const BulkUnassignMembersFromGuardrail404 = NotFoundResponse -export type BulkUnassignMembersFromGuardrail500 = InternalServerResponse -export const BulkUnassignMembersFromGuardrail500 = InternalServerResponse -export type GetCurrentKey200 = { - readonly "data": { - readonly "label": string - readonly "limit": number - readonly "usage": number - readonly "usage_daily": number - readonly "usage_weekly": number - readonly "usage_monthly": number - readonly "byok_usage": number - readonly "byok_usage_daily": number - readonly "byok_usage_weekly": number - readonly "byok_usage_monthly": number - readonly "is_free_tier": boolean - readonly "is_management_key": boolean - readonly "is_provisioning_key": boolean - readonly "limit_remaining": number - readonly "limit_reset": string - readonly "include_byok_in_limit": boolean - readonly "expires_at"?: string - readonly "rate_limit": { readonly "requests": number; readonly "interval": string; readonly "note": string } - } +export type OutputMcpServerToolItem = { + readonly "id"?: string + readonly "serverLabel"?: string + readonly "status": ToolCallStatus + readonly "toolName"?: string + readonly "type": "openrouter:mcp" } -export const GetCurrentKey200 = Schema.Struct({ - "data": Schema.Struct({ - "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), - "limit": Schema.Number.annotate({ "description": "Spending limit for the API key in USD" }).check( - Schema.isFinite() - ), - "usage": Schema.Number.annotate({ "description": "Total OpenRouter credit usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "usage_daily": Schema.Number.annotate({ "description": "OpenRouter credit usage (in USD) for the current UTC day" }) - .check(Schema.isFinite()), - "usage_weekly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "usage_monthly": Schema.Number.annotate({ - "description": "OpenRouter credit usage (in USD) for the current UTC month" - }).check(Schema.isFinite()), - "byok_usage": Schema.Number.annotate({ "description": "Total external BYOK usage (in USD) for the API key" }).check( - Schema.isFinite() - ), - "byok_usage_daily": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC day" - }).check(Schema.isFinite()), - "byok_usage_weekly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)" - }).check(Schema.isFinite()), - "byok_usage_monthly": Schema.Number.annotate({ - "description": "External BYOK usage (in USD) for current UTC month" - }).check(Schema.isFinite()), - "is_free_tier": Schema.Boolean.annotate({ "description": "Whether this is a free tier API key" }), - "is_management_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), - "is_provisioning_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), - "limit_remaining": Schema.Number.annotate({ "description": "Remaining spending limit in USD" }).check( - Schema.isFinite() - ), - "limit_reset": Schema.String.annotate({ "description": "Type of limit reset for the API key" }), - "include_byok_in_limit": Schema.Boolean.annotate({ - "description": "Whether to include external BYOK usage in the credit limit" - }), - "expires_at": Schema.optionalKey( - Schema.String.annotate({ - "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", - "format": "date-time" - }) - ), - "rate_limit": Schema.Struct({ - "requests": Schema.Number.annotate({ "description": "Number of requests allowed per interval" }).check( - Schema.isFinite() - ), - "interval": Schema.String.annotate({ "description": "Rate limit interval" }), - "note": Schema.String.annotate({ "description": "Note about the rate limit" }) - }).annotate({ "description": "Legacy rate limit information about a key. Will always return -1." }) - }).annotate({ "description": "Current API key information" }) +export const OutputMcpServerToolItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "serverLabel": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Literal("openrouter:mcp") +}).annotate({ "description": "An openrouter:mcp server tool output item", "identifier": "OutputMcpServerToolItem" }) +export type OutputMemoryServerToolItem = { + readonly "action"?: "read" | "write" | "delete" + readonly "id"?: string + readonly "key"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:memory" + readonly "value"?: Schema.Json +} +export const OutputMemoryServerToolItem = Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.optionalKey(Schema.String), + "key": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:memory"), + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })) +}).annotate({ + "description": "An openrouter:memory server tool output item", + "identifier": "OutputMemoryServerToolItem" }) -export type GetCurrentKey401 = UnauthorizedResponse -export const GetCurrentKey401 = UnauthorizedResponse -export type GetCurrentKey500 = InternalServerResponse -export const GetCurrentKey500 = InternalServerResponse -export type ExchangeAuthCodeForAPIKeyRequestJson = { - readonly "code": string - readonly "code_verifier"?: string - readonly "code_challenge_method"?: "S256" | "plain" +export type OutputSearchModelsServerToolItem = { + readonly "arguments"?: string + readonly "id"?: string + readonly "query"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:experimental__search_models" } -export const ExchangeAuthCodeForAPIKeyRequestJson = Schema.Struct({ - "code": Schema.String.annotate({ "description": "The authorization code received from the OAuth redirect" }), - "code_verifier": Schema.optionalKey( +export const OutputSearchModelsServerToolItem = Schema.Struct({ + "arguments": Schema.optionalKey( Schema.String.annotate({ - "description": "The code verifier if code_challenge was used in the authorization request" + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" }) ), - "code_challenge_method": Schema.optionalKey( - Schema.Literals(["S256", "plain"]).annotate({ "description": "The method used to generate the code challenge" }) - ) -}) -export type ExchangeAuthCodeForAPIKey200 = { readonly "key": string; readonly "user_id": string } -export const ExchangeAuthCodeForAPIKey200 = Schema.Struct({ - "key": Schema.String.annotate({ "description": "The API key to use for OpenRouter requests" }), - "user_id": Schema.String.annotate({ "description": "User ID associated with the API key" }) + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:experimental__search_models") +}).annotate({ + "description": "An openrouter:experimental__search_models server tool output item", + "identifier": "OutputSearchModelsServerToolItem" }) -export type ExchangeAuthCodeForAPIKey400 = BadRequestResponse -export const ExchangeAuthCodeForAPIKey400 = BadRequestResponse -export type ExchangeAuthCodeForAPIKey403 = ForbiddenResponse -export const ExchangeAuthCodeForAPIKey403 = ForbiddenResponse -export type ExchangeAuthCodeForAPIKey500 = InternalServerResponse -export const ExchangeAuthCodeForAPIKey500 = InternalServerResponse -export type CreateAuthKeysCodeRequestJson = { - readonly "callback_url": string - readonly "code_challenge"?: string - readonly "code_challenge_method"?: "S256" | "plain" - readonly "limit"?: number - readonly "expires_at"?: string +export type OutputSubagentServerToolItem = { + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": ToolCallStatus + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": "openrouter:subagent" } -export const CreateAuthKeysCodeRequestJson = Schema.Struct({ - "callback_url": Schema.String.annotate({ - "description": - "The callback URL to redirect to after authorization. Note, only https URLs on ports 443 and 3000 are allowed.", - "format": "uri" - }), - "code_challenge": Schema.optionalKey( - Schema.String.annotate({ "description": "PKCE code challenge for enhanced security" }) +export const OutputSubagentServerToolItem = Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) ), - "code_challenge_method": Schema.optionalKey( - Schema.Literals(["S256", "plain"]).annotate({ "description": "The method used to generate the code challenge" }) + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) ), - "limit": Schema.optionalKey( - Schema.Number.annotate({ "description": "Credit limit for the API key to be created" }).check(Schema.isFinite()) + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) ), - "expires_at": Schema.optionalKey( + "outcome": Schema.optionalKey( Schema.String.annotate({ - "description": "Optional expiration time for the API key to be created", - "format": "date-time" + "description": "The worker model's result (the outcome text returned to the delegating model)." }) - ) + ), + "status": ToolCallStatus, + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Literal("openrouter:subagent") +}).annotate({ + "description": "An openrouter:subagent server tool output item", + "identifier": "OutputSubagentServerToolItem" }) -export type CreateAuthKeysCode200 = { - readonly "data": { readonly "id": string; readonly "app_id": number; readonly "created_at": string } +export type OutputTextEditorServerToolItem = { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:text_editor" } -export const CreateAuthKeysCode200 = Schema.Struct({ - "data": Schema.Struct({ - "id": Schema.String.annotate({ "description": "The authorization code ID to use in the exchange request" }), - "app_id": Schema.Number.annotate({ "description": "The application ID associated with this auth code" }).check( - Schema.isFinite() - ), - "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the auth code was created" }) - }).annotate({ "description": "Auth code data" }) +export const OutputTextEditorServerToolItem = Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:text_editor") +}).annotate({ + "description": "An openrouter:text_editor server tool output item", + "identifier": "OutputTextEditorServerToolItem" }) -export type CreateAuthKeysCode400 = BadRequestResponse -export const CreateAuthKeysCode400 = BadRequestResponse -export type CreateAuthKeysCode401 = UnauthorizedResponse -export const CreateAuthKeysCode401 = UnauthorizedResponse -export type CreateAuthKeysCode500 = InternalServerResponse -export const CreateAuthKeysCode500 = InternalServerResponse -export type SendChatCompletionRequestRequestJson = ChatGenerationParams -export const SendChatCompletionRequestRequestJson = ChatGenerationParams -export type SendChatCompletionRequest200 = { - readonly "id": string - readonly "choices": ReadonlyArray - readonly "created": number - readonly "model": string - readonly "object": "chat.completion" - readonly "system_fingerprint"?: string | null - readonly "usage"?: ChatGenerationTokenUsage +export type OutputToolSearchServerToolItem = { + readonly "id"?: string + readonly "query"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:tool_search" } -export const SendChatCompletionRequest200 = Schema.Struct({ - "id": Schema.String, - "choices": Schema.Array(ChatResponseChoice), - "created": Schema.Number.check(Schema.isFinite()), - "model": Schema.String, - "object": Schema.Literal("chat.completion"), - "system_fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), - "usage": Schema.optionalKey(ChatGenerationTokenUsage) -}).annotate({ "description": "Chat completion response" }) -export type SendChatCompletionRequest200Sse = ChatStreamingResponseChunk -export const SendChatCompletionRequest200Sse = ChatStreamingResponseChunk -export type SendChatCompletionRequest400 = ChatError -export const SendChatCompletionRequest400 = ChatError -export type SendChatCompletionRequest401 = ChatError -export const SendChatCompletionRequest401 = ChatError -export type SendChatCompletionRequest429 = ChatError -export const SendChatCompletionRequest429 = ChatError -export type SendChatCompletionRequest500 = ChatError -export const SendChatCompletionRequest500 = ChatError - -export interface OperationConfig { - /** - * Whether or not the response should be included in the value returned from - * an operation. - * - * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, - * where `A` is the success type of the operation. - * - * If set to `false`, only the success type of the operation will be returned. - */ - readonly includeResponse?: boolean | undefined +export const OutputToolSearchServerToolItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:tool_search") +}).annotate({ + "description": "An openrouter:tool_search server tool output item", + "identifier": "OutputToolSearchServerToolItem" +}) +export type OutputWebFetchServerToolItem = { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "title"?: string + readonly "type": "openrouter:web_fetch" + readonly "url"?: string } - -/** - * A utility type which optionally includes the response in the return result - * of an operation based upon the value of the `includeResponse` configuration - * option. - */ -export type WithOptionalResponse = Config extends { - readonly includeResponse: true -} ? [A, HttpClientResponse.HttpClientResponse] : - A - -export const make = ( - httpClient: HttpClient.HttpClient, - options: { - readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined - } = {} -): OpenRouterClient => { - const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => - Effect.flatMap( - Effect.orElseSucceed(response.json, () => "Unexpected status code"), - (description) => - Effect.fail( - new HttpClientError.HttpClientError({ - reason: new HttpClientError.StatusCodeError({ - request: response.request, - response, - description: typeof description === "string" ? description : JSON.stringify(description) - }) - }) - ) +export const OutputWebFetchServerToolItem = Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey(Schema.String.annotate({ "description": "The error message if the fetch failed." })), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) ) - const withResponse = (config: Config | undefined) => - ( - f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect - ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { - const withOptionalResponse = ( - config?.includeResponse - ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) - : (response: HttpClientResponse.HttpClientResponse) => f(response) - ) as any - return options?.transformClient - ? (request) => - Effect.flatMap( - Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), - withOptionalResponse - ) - : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + ), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "title": Schema.optionalKey(Schema.String), + "type": Schema.Literal("openrouter:web_fetch"), + "url": Schema.optionalKey(Schema.String) +}).annotate({ + "description": "An openrouter:web_fetch server tool output item", + "identifier": "OutputWebFetchServerToolItem" +}) +export type OutputWebSearchServerToolItem = { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" } - const sseRequest = < - Type, - DecodingServices - >( - schema: Schema.ConstraintDecoder - ) => - ( - request: HttpClientRequest.HttpClientRequest - ): Stream.Stream< - { readonly event: string; readonly id: string | undefined; readonly data: Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, - DecodingServices - > => - HttpClient.filterStatusOk(httpClient).execute(request).pipe( - Effect.map((response) => response.stream), - Stream.unwrap, - Stream.decodeText(), - Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) - ) - const decodeSuccess = - (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => - HttpClientResponse.schemaBodyJson(schema)(response) - const decodeError = - (tag: Tag, schema: Schema) => - (response: HttpClientResponse.HttpClientResponse) => - Effect.flatMap( - HttpClientResponse.schemaBodyJson(schema)(response), - (cause) => Effect.fail(OpenRouterClientError(tag, cause, response)) - ) - return { - httpClient, - "createResponses": (options) => - HttpClientRequest.post(`/responses`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateResponses200), - "400": decodeError("CreateResponses400", CreateResponses400), - "401": decodeError("CreateResponses401", CreateResponses401), - "402": decodeError("CreateResponses402", CreateResponses402), - "404": decodeError("CreateResponses404", CreateResponses404), - "408": decodeError("CreateResponses408", CreateResponses408), - "413": decodeError("CreateResponses413", CreateResponses413), - "422": decodeError("CreateResponses422", CreateResponses422), - "429": decodeError("CreateResponses429", CreateResponses429), - "500": decodeError("CreateResponses500", CreateResponses500), - "502": decodeError("CreateResponses502", CreateResponses502), - "503": decodeError("CreateResponses503", CreateResponses503), - "524": decodeError("CreateResponses524", CreateResponses524), - "529": decodeError("CreateResponses529", CreateResponses529), - orElse: unexpectedStatus - })) - ), - "createResponsesSse": (options) => - HttpClientRequest.post(`/responses`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - sseRequest(CreateResponses200Sse) - ), - "createMessages": (options) => - HttpClientRequest.post(`/messages`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateMessages200), - "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 - })) - ), - "createMessagesSse": (options) => - HttpClientRequest.post(`/messages`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - sseRequest(CreateMessages200Sse) - ), - "getUserActivity": (options) => - HttpClientRequest.get(`/activity`).pipe( - HttpClientRequest.setUrlParams({ "date": options?.params?.["date"] as any }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetUserActivity200), - "400": decodeError("GetUserActivity400", GetUserActivity400), - "401": decodeError("GetUserActivity401", GetUserActivity401), - "403": decodeError("GetUserActivity403", GetUserActivity403), - "500": decodeError("GetUserActivity500", GetUserActivity500), - orElse: unexpectedStatus - })) - ), - "getCredits": (options) => - HttpClientRequest.get(`/credits`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetCredits200), - "401": decodeError("GetCredits401", GetCredits401), - "403": decodeError("GetCredits403", GetCredits403), - "500": decodeError("GetCredits500", GetCredits500), - orElse: unexpectedStatus - })) - ), - "createCoinbaseCharge": (options) => - HttpClientRequest.post(`/credits/coinbase`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateCoinbaseCharge200), - "400": decodeError("CreateCoinbaseCharge400", CreateCoinbaseCharge400), - "401": decodeError("CreateCoinbaseCharge401", CreateCoinbaseCharge401), - "429": decodeError("CreateCoinbaseCharge429", CreateCoinbaseCharge429), - "500": decodeError("CreateCoinbaseCharge500", CreateCoinbaseCharge500), - orElse: unexpectedStatus - })) - ), - "createEmbeddings": (options) => - HttpClientRequest.post(`/embeddings`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateEmbeddings200), - "400": decodeError("CreateEmbeddings400", CreateEmbeddings400), - "401": decodeError("CreateEmbeddings401", CreateEmbeddings401), - "402": decodeError("CreateEmbeddings402", CreateEmbeddings402), - "404": decodeError("CreateEmbeddings404", CreateEmbeddings404), - "429": decodeError("CreateEmbeddings429", CreateEmbeddings429), - "500": decodeError("CreateEmbeddings500", CreateEmbeddings500), - "502": decodeError("CreateEmbeddings502", CreateEmbeddings502), - "503": decodeError("CreateEmbeddings503", CreateEmbeddings503), - "524": decodeError("CreateEmbeddings524", CreateEmbeddings524), - "529": decodeError("CreateEmbeddings529", CreateEmbeddings529), - orElse: unexpectedStatus - })) - ), - "createEmbeddingsSse": (options) => - HttpClientRequest.post(`/embeddings`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - sseRequest(CreateEmbeddings200Sse) - ), - "listEmbeddingsModels": (options) => - HttpClientRequest.get(`/embeddings/models`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListEmbeddingsModels200), - "400": decodeError("ListEmbeddingsModels400", ListEmbeddingsModels400), - "500": decodeError("ListEmbeddingsModels500", ListEmbeddingsModels500), - orElse: unexpectedStatus - })) - ), - "getGeneration": (options) => - HttpClientRequest.get(`/generation`).pipe( - HttpClientRequest.setUrlParams({ "id": options.params["id"] as any }), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetGeneration200), - "401": decodeError("GetGeneration401", GetGeneration401), - "402": decodeError("GetGeneration402", GetGeneration402), - "404": decodeError("GetGeneration404", GetGeneration404), - "429": decodeError("GetGeneration429", GetGeneration429), - "500": decodeError("GetGeneration500", GetGeneration500), - "502": decodeError("GetGeneration502", GetGeneration502), - "524": decodeError("GetGeneration524", GetGeneration524), - "529": decodeError("GetGeneration529", GetGeneration529), - orElse: unexpectedStatus - })) - ), - "listModelsCount": (options) => - HttpClientRequest.get(`/models/count`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListModelsCount200), - "500": decodeError("ListModelsCount500", ListModelsCount500), - orElse: unexpectedStatus - })) - ), - "getModels": (options) => - HttpClientRequest.get(`/models`).pipe( - HttpClientRequest.setUrlParams({ - "category": options?.params?.["category"] as any, - "supported_parameters": options?.params?.["supported_parameters"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetModels200), - "400": decodeError("GetModels400", GetModels400), - "500": decodeError("GetModels500", GetModels500), - orElse: unexpectedStatus - })) + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "type": "openrouter:web_search" +} +export const OutputWebSearchServerToolItem = Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) ), - "listModelsUser": (options) => - HttpClientRequest.get(`/models/user`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListModelsUser200), - "401": decodeError("ListModelsUser401", ListModelsUser401), - "404": decodeError("ListModelsUser404", ListModelsUser404), - "500": decodeError("ListModelsUser500", ListModelsUser500), - orElse: unexpectedStatus - })) + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:web_search") +}).annotate({ + "description": "An openrouter:web_search server tool output item", + "identifier": "OutputWebSearchServerToolItem" +}) +export type ShellCallItem = { + readonly "action": { + readonly "commands": ReadonlyArray + readonly "max_output_length"?: number | null + readonly "timeout_ms"?: number | null + } + readonly "call_id": string + readonly "environment"?: Schema.Json + readonly "id"?: string | null + readonly "status"?: ToolCallStatus | null + readonly "type": "shell_call" +} +export const ShellCallItem = Schema.Struct({ + "action": Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "timeout_ms": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ) + }), + "call_id": Schema.String, + "environment": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.optionalKey(Schema.Union([ToolCallStatus, Schema.Null])), + "type": Schema.Literal("shell_call") +}).annotate({ "description": "A shell command execution call (newer variant)", "identifier": "ShellCallItem" }) +export type ShellCallOutputItem = { + readonly "call_id": string + readonly "id"?: string | null + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { readonly "content"?: string | null; readonly "exit_code"?: number | null; readonly "type": string } + > + readonly "status"?: ToolCallStatus | null + readonly "type": "shell_call_output" +} +export const ShellCallOutputItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array( + Schema.Struct({ + "content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "exit_code": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) ), - "listEndpoints": (author, slug, options) => - HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListEndpoints200), - "404": decodeError("ListEndpoints404", ListEndpoints404), - "500": decodeError("ListEndpoints500", ListEndpoints500), - orElse: unexpectedStatus - })) - ), - "listEndpointsZdr": (options) => - HttpClientRequest.get(`/endpoints/zdr`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListEndpointsZdr200), - "500": decodeError("ListEndpointsZdr500", ListEndpointsZdr500), - orElse: unexpectedStatus - })) - ), - "listProviders": (options) => - HttpClientRequest.get(`/providers`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListProviders200), - "500": decodeError("ListProviders500", ListProviders500), - orElse: unexpectedStatus - })) - ), - "list": (options) => - HttpClientRequest.get(`/keys`).pipe( - HttpClientRequest.setUrlParams({ - "include_disabled": options?.params?.["include_disabled"] as any, - "offset": options?.params?.["offset"] as any + "type": Schema.String + }) + ), + "status": Schema.optionalKey(Schema.Union([ToolCallStatus, Schema.Null])), + "type": Schema.Literal("shell_call_output") +}).annotate({ + "description": "Output from a shell command execution (newer variant)", + "identifier": "ShellCallOutputItem" +}) +export type OpenAIResponsesToolChoice = + | "auto" + | "none" + | "required" + | Objects_11 + | Objects_12 + | ToolChoiceAllowed + | Objects_13 + | Objects_14 +export const OpenAIResponsesToolChoice = Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("none"), + Schema.Literal("required"), + Objects_11, + Objects_12, + ToolChoiceAllowed, + Objects_13, + Objects_14 +]).annotate({ "identifier": "OpenAIResponsesToolChoice" }) +export type TooManyRequestsResponse = { + readonly "error": TooManyRequestsResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const TooManyRequestsResponse = Schema.Struct({ + "error": TooManyRequestsResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ), + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ "description": "Too Many Requests - Rate limit exceeded", "identifier": "TooManyRequestsResponse" }) +export type UnauthorizedResponse = { + readonly "error": UnauthorizedResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const UnauthorizedResponse = Schema.Struct({ + "error": UnauthorizedResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ), + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Unauthorized - Authentication required or invalid credentials", + "identifier": "UnauthorizedResponse" +}) +export type UnifiedBenchmarksAAItem = { + readonly "agentic_index": number | null + readonly "coding_index": number | null + readonly "display_name": string + readonly "intelligence_index": number | null + readonly "model_permaslug": string + readonly "pricing": UnifiedBenchmarkPricing + readonly "source": "artificial-analysis" +} +export const UnifiedBenchmarksAAItem = Schema.Struct({ + "agentic_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ + "description": "Artificial Analysis Agentic Index composite score. Higher is better.", + "format": "double" + }), + "coding_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ + "description": "Artificial Analysis Coding Index composite score. Higher is better.", + "format": "double" + }), + "display_name": Schema.String.annotate({ "description": "Model name as listed on Artificial Analysis." }), + "intelligence_index": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ + "description": "Artificial Analysis Intelligence Index composite score. Higher is better.", + "format": "double" + }), + "model_permaslug": Schema.String.annotate({ "description": "Stable OpenRouter model identifier." }), + "pricing": UnifiedBenchmarkPricing, + "source": Schema.Literal("artificial-analysis").annotate({ "description": "Benchmark source discriminator." }) +}).annotate({ "identifier": "UnifiedBenchmarksAAItem" }) +export type UnifiedBenchmarksDAItem = { + readonly "arena": string + readonly "avg_generation_time_ms": number | null + readonly "category": string + readonly "display_name": string + readonly "elo": number + readonly "model_permaslug": string + readonly "pricing": UnifiedBenchmarkPricing + readonly "source": "design-arena" + readonly "tournament_stats": { + readonly "first_place": number | null + readonly "fourth_place": number | null + readonly "second_place": number | null + readonly "third_place": number | null + readonly "total": number | null + } + readonly "win_rate": number +} +export const UnifiedBenchmarksDAItem = Schema.Struct({ + "arena": Schema.String.annotate({ "description": "Arena this ranking belongs to." }), + "avg_generation_time_ms": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Average generation time in milliseconds.", "format": "double" }), + "category": Schema.String.annotate({ "description": "Category within the arena." }), + "display_name": Schema.String.annotate({ "description": "Human-readable model name from Design Arena." }), + "elo": Schema.Number.annotate({ "description": "ELO rating from head-to-head arena battles.", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "model_permaslug": Schema.String.annotate({ + "description": "Stable OpenRouter model identifier when mapped; otherwise the upstream Design Arena model id." + }), + "pricing": UnifiedBenchmarkPricing, + "source": Schema.Literal("design-arena").annotate({ "description": "Benchmark source discriminator." }), + "tournament_stats": Schema.Struct({ + "first_place": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "fourth_place": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "second_place": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "third_place": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "total": Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + }).annotate({ "description": "Placement distribution from tournament matches." }), + "win_rate": Schema.Number.annotate({ "description": "Win rate as a percentage (0–100).", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) +}).annotate({ "identifier": "UnifiedBenchmarksDAItem" }) +export type UnprocessableEntityResponse = { + readonly "error": UnprocessableEntityResponseErrorData + readonly "openrouter_metadata"?: { readonly [x: string]: Schema.Json } | null + readonly "user_id"?: string | null +} +export const UnprocessableEntityResponse = Schema.Struct({ + "error": UnprocessableEntityResponseErrorData, + "openrouter_metadata": Schema.optionalKey( + Schema.Union([Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), Schema.Null]) + ), + "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ + "description": "Unprocessable Entity - Semantic validation failure", + "identifier": "UnprocessableEntityResponse" +}) +export type OpenAIResponsesAnnotation = FileCitation | URLCitation | FilePath +export const OpenAIResponsesAnnotation = Schema.Union([FileCitation, URLCitation, FilePath]).annotate({ + "identifier": "OpenAIResponsesAnnotation" +}) +export type VideoGenerationResponse = { + readonly "error"?: string + readonly "generation_id"?: string + readonly "id": string + readonly "polling_url": string + readonly "status": "pending" | "in_progress" | "completed" | "failed" | "cancelled" | "expired" + readonly "unsigned_urls"?: ReadonlyArray + readonly "usage"?: VideoGenerationUsage +} +export const VideoGenerationResponse = Schema.Struct({ + "error": Schema.optionalKey(Schema.String), + "generation_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "The generation ID associated with this video generation job. Available once the job has been processed." + }) + ), + "id": Schema.String, + "polling_url": Schema.String, + "status": Schema.Literals(["pending", "in_progress", "completed", "failed", "cancelled", "expired"]), + "unsigned_urls": Schema.optionalKey(Schema.Array(Schema.String)), + "usage": Schema.optionalKey(VideoGenerationUsage) +}).annotate({ "identifier": "VideoGenerationResponse" }) +export type VideoModelsListResponse = { readonly "data": ReadonlyArray } +export const VideoModelsListResponse = Schema.Struct({ "data": Schema.Array(VideoModel) }).annotate({ + "identifier": "VideoModelsListResponse" +}) +export type WebFetchServerToolConfig = { + readonly "allowed_domains"?: Arrays_13 + readonly "blocked_domains"?: Arrays_14 + readonly "engine"?: WebFetchEngineEnum + readonly "max_content_tokens"?: number + readonly "max_uses"?: number +} +export const WebFetchServerToolConfig = Schema.Struct({ + "allowed_domains": Schema.optionalKey(Arrays_13), + "blocked_domains": Schema.optionalKey(Arrays_14), + "engine": Schema.optionalKey(WebFetchEngineEnum), + "max_content_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum content length in approximate tokens. Content exceeding this limit is truncated." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_uses": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum number of web fetches per request. Once exceeded, the tool returns an error." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) +}).annotate({ + "description": "Configuration for the openrouter:web_fetch server tool", + "identifier": "WebFetchServerToolConfig" +}) +export type WebSearchDomainFilter = Objects_152 | null +export const WebSearchDomainFilter = Schema.Union([Objects_152, Schema.Null]).annotate({ + "identifier": "WebSearchDomainFilter" +}) +export type WebSearchPlugin = { + readonly "enabled"?: boolean + readonly "engine"?: WebSearchEngine + readonly "exclude_domains"?: ReadonlyArray + readonly "id": "web" + readonly "include_domains"?: ReadonlyArray + readonly "max_results"?: number + readonly "max_uses"?: number + readonly "search_prompt"?: string + readonly "user_location"?: { + readonly "city"?: string | null + readonly "country"?: string | null + readonly "region"?: string | null + readonly "timezone"?: string | null + readonly "type": "approximate" + readonly [x: string]: Schema.Json + } +} +export const WebSearchPlugin = Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the web-search plugin for this request. Defaults to true." + }) + ), + "engine": Schema.optionalKey(WebSearchEngine), + "exclude_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of domains to exclude from web search results. Supports wildcards (e.g. \"*.substack.com\") and path filtering (e.g. \"openai.com/blog\")." + }) + ), + "id": Schema.Literal("web"), + "include_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "A list of domains to restrict web search results to. Supports wildcards (e.g. \"*.substack.com\") and path filtering (e.g. \"openai.com/blog\")." + }) + ), + "max_results": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "max_uses": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of times the model can invoke web search in a single turn. Passed through to native providers that support it (e.g. Anthropic)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_prompt": Schema.optionalKey(Schema.String), + "user_location": Schema.optionalKey( + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "city": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "country": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "region": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "timezone": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("approximate") }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(List200), - "401": decodeError("List401", List401), - "429": decodeError("List429", List429), - "500": decodeError("List500", List500), - orElse: unexpectedStatus - })) - ), - "createKeys": (options) => - HttpClientRequest.post(`/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateKeys201), - "400": decodeError("CreateKeys400", CreateKeys400), - "401": decodeError("CreateKeys401", CreateKeys401), - "429": decodeError("CreateKeys429", CreateKeys429), - "500": decodeError("CreateKeys500", CreateKeys500), - orElse: unexpectedStatus - })) + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ).annotate({ + "description": + "Approximate user location for location-biased search results. Passed through to native providers that support it (e.g. Anthropic)." + }) + ]).annotate({ "description": "User location information for web search" }) + ) +}).annotate({ "identifier": "WebSearchPlugin" }) +export type OutputFileSearchCallItem = { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus + readonly "type": "file_search_call" +} +export const OutputFileSearchCallItem = Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus, + "type": Schema.Literal("file_search_call") +}).annotate({ "identifier": "OutputFileSearchCallItem" }) +export type OutputItemFileSearchCall = { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus + readonly "type": "file_search_call" +} +export const OutputItemFileSearchCall = Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus, + "type": Schema.Literal("file_search_call") +}).annotate({ "identifier": "OutputItemFileSearchCall" }) +export type OutputItemWebSearchCall = { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": WebSearchStatus + readonly "type": "web_search_call" +} +export const OutputItemWebSearchCall = Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": WebSearchStatus, + "type": Schema.Literal("web_search_call") +}).annotate({ "identifier": "OutputItemWebSearchCall" }) +export type OutputWebSearchCallItem = { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": WebSearchStatus + readonly "type": "web_search_call" +} +export const OutputWebSearchCallItem = Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": WebSearchStatus, + "type": Schema.Literal("web_search_call") +}).annotate({ "identifier": "OutputWebSearchCallItem" }) +export type WebSearchUserLocation = Objects_153 | null +export const WebSearchUserLocation = Schema.Union([Objects_153, Schema.Null]).annotate({ + "description": "User location information for web search", + "identifier": "WebSearchUserLocation" +}) +export type WebSearchConfig = { + readonly "allowed_domains"?: Arrays_15 + readonly "engine"?: WebSearchEngineEnum + readonly "excluded_domains"?: Arrays_16 + readonly "max_characters"?: number + readonly "max_results"?: number + readonly "max_total_results"?: number + readonly "search_context_size"?: SearchQualityLevel + readonly "user_location"?: WebSearchUserLocationServerTool +} +export const WebSearchConfig = Schema.Struct({ + "allowed_domains": Schema.optionalKey(Arrays_15), + "engine": Schema.optionalKey(WebSearchEngineEnum), + "excluded_domains": Schema.optionalKey(Arrays_16), + "max_characters": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Exact maximum number of characters of content per search result. Applies to the Exa, Parallel, and Perplexity engines; ignored with native provider search and Firecrawl. For Exa, caps highlight content per result. For Parallel, caps excerpt content per result (default 1,500 when omitted). For Perplexity, maps to the native `max_tokens_per_page` parameter (converted from characters to tokens) and trims the response to the exact character cap. When both `max_characters` and `search_context_size` are set, `max_characters` takes precedence. When omitted, falls back to `search_context_size` mapping (Exa) or engine defaults (Parallel, Perplexity)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_total_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum total number of search results across all search calls in a single request. Once this limit is reached, the tool will stop returning new results. Useful for controlling cost and context size in agentic loops. Defaults to 50 when not specified." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchQualityLevel), + "user_location": Schema.optionalKey(WebSearchUserLocationServerTool) +}).annotate({ "identifier": "WebSearchConfig" }) +export type WebSearchServerToolConfig = { + readonly "allowed_domains"?: Arrays_17 + readonly "engine"?: WebSearchEngineEnum + readonly "excluded_domains"?: Arrays_18 + readonly "max_characters"?: number + readonly "max_results"?: number + readonly "max_total_results"?: number + readonly "search_context_size"?: SearchQualityLevel + readonly "user_location"?: WebSearchUserLocationServerTool +} +export const WebSearchServerToolConfig = Schema.Struct({ + "allowed_domains": Schema.optionalKey(Arrays_17), + "engine": Schema.optionalKey(WebSearchEngineEnum), + "excluded_domains": Schema.optionalKey(Arrays_18), + "max_characters": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Exact maximum number of characters of content per search result. Applies to the Exa, Parallel, and Perplexity engines; ignored with native provider search and Firecrawl. For Exa, caps highlight content per result. For Parallel, caps excerpt content per result (default 1,500 when omitted). For Perplexity, maps to the native `max_tokens_per_page` parameter (converted from characters to tokens) and trims the response to the exact character cap. When both `max_characters` and `search_context_size` are set, `max_characters` takes precedence. When omitted, falls back to `search_context_size` mapping (Exa) or engine defaults (Parallel, Perplexity)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_total_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum total number of search results across all search calls in a single request. Once this limit is reached, the tool will stop returning new results. Useful for controlling cost and context size in agentic loops. Defaults to 50 when not specified." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchQualityLevel), + "user_location": Schema.optionalKey(WebSearchUserLocationServerTool) +}).annotate({ + "description": "Configuration for the openrouter:web_search server tool", + "identifier": "WebSearchServerToolConfig" +}) +export type CreateWorkspaceResponse = { readonly "data": Workspace } +export const CreateWorkspaceResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Workspace).annotate({ "description": "The created workspace" }) +}).annotate({ "identifier": "CreateWorkspaceResponse" }) +export type GetWorkspaceResponse = { readonly "data": Workspace } +export const GetWorkspaceResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Workspace).annotate({ "description": "The workspace" }) +}).annotate({ "identifier": "GetWorkspaceResponse" }) +export type ListWorkspacesResponse = { readonly "data": ReadonlyArray; readonly "total_count": number } +export const ListWorkspacesResponse = Schema.Struct({ + "data": Schema.Array(Workspace).annotate({ "description": "List of workspaces" }), + "total_count": Schema.Number.annotate({ "description": "Total number of workspaces" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "ListWorkspacesResponse" }) +export type UpdateWorkspaceResponse = { readonly "data": Workspace } +export const UpdateWorkspaceResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Workspace).annotate({ "description": "The updated workspace" }) +}).annotate({ "identifier": "UpdateWorkspaceResponse" }) +export type ListWorkspaceBudgetsResponse = { readonly "data": ReadonlyArray } +export const ListWorkspaceBudgetsResponse = Schema.Struct({ + "data": Schema.Array(WorkspaceBudget).annotate({ "description": "List of budgets configured for the workspace" }) +}).annotate({ "identifier": "ListWorkspaceBudgetsResponse" }) +export type UpsertWorkspaceBudgetResponse = { readonly "data": WorkspaceBudget } +export const UpsertWorkspaceBudgetResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => WorkspaceBudget).annotate({ + "description": "The created or updated budget" + }) +}).annotate({ "identifier": "UpsertWorkspaceBudgetResponse" }) +export type BulkAddWorkspaceMembersResponse = { + readonly "added_count": number + readonly "data": ReadonlyArray +} +export const BulkAddWorkspaceMembersResponse = Schema.Struct({ + "added_count": Schema.Number.annotate({ "description": "Number of workspace memberships created or updated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "data": Schema.Array(WorkspaceMember).annotate({ "description": "List of added workspace memberships" }) +}).annotate({ "identifier": "BulkAddWorkspaceMembersResponse" }) +export type ListWorkspaceMembersResponse = { + readonly "data": ReadonlyArray + readonly "total_count": number +} +export const ListWorkspaceMembersResponse = Schema.Struct({ + "data": Schema.Array(WorkspaceMember).annotate({ "description": "List of workspace members" }), + "total_count": Schema.Number.annotate({ "description": "Total number of members in the workspace" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "ListWorkspaceMembersResponse" }) +export type AdvisorServerToolConfig = { + readonly "forward_transcript"?: boolean + readonly "instructions"?: string + readonly "max_completion_tokens"?: number + readonly "max_tool_calls"?: number + readonly "model"?: string + readonly "name"?: string + readonly "reasoning"?: AdvisorReasoning + readonly "stream"?: boolean + readonly "temperature"?: number + readonly "tools"?: Arrays_ +} +export const AdvisorServerToolConfig = Schema.Struct({ + "forward_transcript": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "When true, the full parent conversation is forwarded to the advisor so it sees the same context the executor does (and the tool-call `prompt`, if given, is appended as a final user turn). When false or omitted, the advisor receives only the `prompt` the executor passes in the tool call." + })), + "instructions": Schema.optionalKey( + Schema.String.annotate({ + "description": + "System instructions for the advisor sub-agent. When omitted, the advisor responds with no system prompt of its own." + }) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of output tokens (including reasoning) the advisor may produce. When omitted, the provider's default applies." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of tool-calling steps the advisor sub-agent may take during its agentic loop. Capped at 25. Only relevant when the advisor is given tools." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(25).annotate({ "expected": "a value less than or equal to 25" })) + ), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Slug of the advisor model to consult (any OpenRouter model). When omitted, the executor can choose it via the tool call's `model` argument; if neither is set, the model from the outer API request is used. The advisor tool itself cannot be the advisor model." + }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Optional name for this advisor. The model sees one tool per named advisor (and one default for an unnamed entry). Names must be unique across advisor entries. Letters, digits, spaces, underscores, and dashes; trimmed; 1–64 chars." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" }) + ).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9 _-]+$")).annotate({ + "expected": "a string matching the RegExp ^[a-zA-Z0-9 _-]+$" + }) + ) + ), + "reasoning": Schema.optionalKey(AdvisorReasoning), + "stream": Schema.optionalKey(Schema.Boolean.annotate({ + "description": + "When true, the advisor's advice streams incrementally as it is produced. In the Responses API this emits `response.output_text.delta` events targeting the advisor output item; the final `advice` field is still set on the completed item. Has no effect on the Chat Completions API (where the advice arrives only as the final tool result). When false or omitted, the advice arrives only as the final result." + })), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Sampling temperature forwarded to the advisor call. When omitted, the provider's default applies.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "tools": Schema.optionalKey(Arrays_) +}).annotate({ + "description": "Configuration for one openrouter:advisor server tool entry.", + "identifier": "AdvisorServerToolConfig" +}) +export type AnthropicBashCodeExecutionContent = + | AnthropicBashCodeExecutionToolResultError + | AnthropicBashCodeExecutionResult +export const AnthropicBashCodeExecutionContent = Schema.Union([ + AnthropicBashCodeExecutionToolResultError, + AnthropicBashCodeExecutionResult +], { mode: "oneOf" }).annotate({ "identifier": "AnthropicBashCodeExecutionContent" }) +export type AnthropicTextBlockParam = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "citations"?: + | ReadonlyArray< + | AnthropicCitationCharLocationParam + | AnthropicCitationPageLocationParam + | AnthropicCitationContentBlockLocationParam + | AnthropicCitationWebSearchResultLocationParam + | AnthropicCitationSearchResultLocationParam + > + | null + readonly "text": string + readonly "type": "text" +} +export const AnthropicTextBlockParam = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + AnthropicCitationCharLocationParam, + AnthropicCitationPageLocationParam, + AnthropicCitationContentBlockLocationParam, + AnthropicCitationWebSearchResultLocationParam, + AnthropicCitationSearchResultLocationParam + ], { mode: "oneOf" }) ), - "getKey": (hash, options) => - HttpClientRequest.get(`/keys/${hash}`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetKey200), - "401": decodeError("GetKey401", GetKey401), - "404": decodeError("GetKey404", GetKey404), - "429": decodeError("GetKey429", GetKey429), - "500": decodeError("GetKey500", GetKey500), - orElse: unexpectedStatus - })) - ), - "deleteKeys": (hash, options) => - HttpClientRequest.delete(`/keys/${hash}`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(DeleteKeys200), - "401": decodeError("DeleteKeys401", DeleteKeys401), - "404": decodeError("DeleteKeys404", DeleteKeys404), - "429": decodeError("DeleteKeys429", DeleteKeys429), - "500": decodeError("DeleteKeys500", DeleteKeys500), - orElse: unexpectedStatus - })) - ), - "updateKeys": (hash, options) => - HttpClientRequest.patch(`/keys/${hash}`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(UpdateKeys200), - "400": decodeError("UpdateKeys400", UpdateKeys400), - "401": decodeError("UpdateKeys401", UpdateKeys401), - "404": decodeError("UpdateKeys404", UpdateKeys404), - "429": decodeError("UpdateKeys429", UpdateKeys429), - "500": decodeError("UpdateKeys500", UpdateKeys500), - orElse: unexpectedStatus - })) - ), - "listGuardrails": (options) => - HttpClientRequest.get(`/guardrails`).pipe( - HttpClientRequest.setUrlParams({ - "offset": options?.params?.["offset"] as any, - "limit": options?.params?.["limit"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrails200), - "401": decodeError("ListGuardrails401", ListGuardrails401), - "500": decodeError("ListGuardrails500", ListGuardrails500), - orElse: unexpectedStatus - })) - ), - "createGuardrail": (options) => - HttpClientRequest.post(`/guardrails`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateGuardrail201), - "400": decodeError("CreateGuardrail400", CreateGuardrail400), - "401": decodeError("CreateGuardrail401", CreateGuardrail401), - "500": decodeError("CreateGuardrail500", CreateGuardrail500), - orElse: unexpectedStatus - })) - ), - "getGuardrail": (id, options) => - HttpClientRequest.get(`/guardrails/${id}`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetGuardrail200), - "401": decodeError("GetGuardrail401", GetGuardrail401), - "404": decodeError("GetGuardrail404", GetGuardrail404), - "500": decodeError("GetGuardrail500", GetGuardrail500), - orElse: unexpectedStatus - })) - ), - "deleteGuardrail": (id, options) => - HttpClientRequest.delete(`/guardrails/${id}`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(DeleteGuardrail200), - "401": decodeError("DeleteGuardrail401", DeleteGuardrail401), - "404": decodeError("DeleteGuardrail404", DeleteGuardrail404), - "500": decodeError("DeleteGuardrail500", DeleteGuardrail500), - orElse: unexpectedStatus - })) - ), - "updateGuardrail": (id, options) => - HttpClientRequest.patch(`/guardrails/${id}`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(UpdateGuardrail200), - "400": decodeError("UpdateGuardrail400", UpdateGuardrail400), - "401": decodeError("UpdateGuardrail401", UpdateGuardrail401), - "404": decodeError("UpdateGuardrail404", UpdateGuardrail404), - "500": decodeError("UpdateGuardrail500", UpdateGuardrail500), - orElse: unexpectedStatus - })) - ), - "listKeyAssignments": (options) => - HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( - HttpClientRequest.setUrlParams({ - "offset": options?.params?.["offset"] as any, - "limit": options?.params?.["limit"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListKeyAssignments200), - "401": decodeError("ListKeyAssignments401", ListKeyAssignments401), - "500": decodeError("ListKeyAssignments500", ListKeyAssignments500), - orElse: unexpectedStatus - })) - ), - "listMemberAssignments": (options) => - HttpClientRequest.get(`/guardrails/assignments/members`).pipe( - HttpClientRequest.setUrlParams({ - "offset": options?.params?.["offset"] as any, - "limit": options?.params?.["limit"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListMemberAssignments200), - "401": decodeError("ListMemberAssignments401", ListMemberAssignments401), - "500": decodeError("ListMemberAssignments500", ListMemberAssignments500), - orElse: unexpectedStatus - })) - ), - "listGuardrailKeyAssignments": (id, options) => - HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( - HttpClientRequest.setUrlParams({ - "offset": options?.params?.["offset"] as any, - "limit": options?.params?.["limit"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrailKeyAssignments200), - "401": decodeError("ListGuardrailKeyAssignments401", ListGuardrailKeyAssignments401), - "404": decodeError("ListGuardrailKeyAssignments404", ListGuardrailKeyAssignments404), - "500": decodeError("ListGuardrailKeyAssignments500", ListGuardrailKeyAssignments500), - orElse: unexpectedStatus - })) - ), - "bulkAssignKeysToGuardrail": (id, options) => - HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), - "400": decodeError("BulkAssignKeysToGuardrail400", BulkAssignKeysToGuardrail400), - "401": decodeError("BulkAssignKeysToGuardrail401", BulkAssignKeysToGuardrail401), - "404": decodeError("BulkAssignKeysToGuardrail404", BulkAssignKeysToGuardrail404), - "500": decodeError("BulkAssignKeysToGuardrail500", BulkAssignKeysToGuardrail500), - orElse: unexpectedStatus - })) - ), - "listGuardrailMemberAssignments": (id, options) => - HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( - HttpClientRequest.setUrlParams({ - "offset": options?.params?.["offset"] as any, - "limit": options?.params?.["limit"] as any - }), - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ListGuardrailMemberAssignments200), - "401": decodeError("ListGuardrailMemberAssignments401", ListGuardrailMemberAssignments401), - "404": decodeError("ListGuardrailMemberAssignments404", ListGuardrailMemberAssignments404), - "500": decodeError("ListGuardrailMemberAssignments500", ListGuardrailMemberAssignments500), - orElse: unexpectedStatus - })) - ), - "bulkAssignMembersToGuardrail": (id, options) => - HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), - "400": decodeError("BulkAssignMembersToGuardrail400", BulkAssignMembersToGuardrail400), - "401": decodeError("BulkAssignMembersToGuardrail401", BulkAssignMembersToGuardrail401), - "404": decodeError("BulkAssignMembersToGuardrail404", BulkAssignMembersToGuardrail404), - "500": decodeError("BulkAssignMembersToGuardrail500", BulkAssignMembersToGuardrail500), - orElse: unexpectedStatus - })) - ), - "bulkUnassignKeysFromGuardrail": (id, options) => - HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), - "400": decodeError("BulkUnassignKeysFromGuardrail400", BulkUnassignKeysFromGuardrail400), - "401": decodeError("BulkUnassignKeysFromGuardrail401", BulkUnassignKeysFromGuardrail401), - "404": decodeError("BulkUnassignKeysFromGuardrail404", BulkUnassignKeysFromGuardrail404), - "500": decodeError("BulkUnassignKeysFromGuardrail500", BulkUnassignKeysFromGuardrail500), - orElse: unexpectedStatus - })) - ), - "bulkUnassignMembersFromGuardrail": (id, options) => - HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), - "400": decodeError("BulkUnassignMembersFromGuardrail400", BulkUnassignMembersFromGuardrail400), - "401": decodeError("BulkUnassignMembersFromGuardrail401", BulkUnassignMembersFromGuardrail401), - "404": decodeError("BulkUnassignMembersFromGuardrail404", BulkUnassignMembersFromGuardrail404), - "500": decodeError("BulkUnassignMembersFromGuardrail500", BulkUnassignMembersFromGuardrail500), - orElse: unexpectedStatus - })) - ), - "getCurrentKey": (options) => - HttpClientRequest.get(`/key`).pipe( - withResponse(options?.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(GetCurrentKey200), - "401": decodeError("GetCurrentKey401", GetCurrentKey401), - "500": decodeError("GetCurrentKey500", GetCurrentKey500), - orElse: unexpectedStatus - })) - ), - "exchangeAuthCodeForAPIKey": (options) => - HttpClientRequest.post(`/auth/keys`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), - "400": decodeError("ExchangeAuthCodeForAPIKey400", ExchangeAuthCodeForAPIKey400), - "403": decodeError("ExchangeAuthCodeForAPIKey403", ExchangeAuthCodeForAPIKey403), - "500": decodeError("ExchangeAuthCodeForAPIKey500", ExchangeAuthCodeForAPIKey500), - orElse: unexpectedStatus - })) - ), - "createAuthKeysCode": (options) => - HttpClientRequest.post(`/auth/keys/code`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(CreateAuthKeysCode200), - "400": decodeError("CreateAuthKeysCode400", CreateAuthKeysCode400), - "401": decodeError("CreateAuthKeysCode401", CreateAuthKeysCode401), - "500": decodeError("CreateAuthKeysCode500", CreateAuthKeysCode500), - orElse: unexpectedStatus - })) - ), - "sendChatCompletionRequest": (options) => - HttpClientRequest.post(`/chat/completions`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - withResponse(options.config)(HttpClientResponse.matchStatus({ - "2xx": decodeSuccess(SendChatCompletionRequest200), - "400": decodeError("SendChatCompletionRequest400", SendChatCompletionRequest400), - "401": decodeError("SendChatCompletionRequest401", SendChatCompletionRequest401), - "429": decodeError("SendChatCompletionRequest429", SendChatCompletionRequest429), - "500": decodeError("SendChatCompletionRequest500", SendChatCompletionRequest500), - orElse: unexpectedStatus - })) - ), - "sendChatCompletionRequestSse": (options) => - HttpClientRequest.post(`/chat/completions`).pipe( - HttpClientRequest.bodyJsonUnsafe(options.payload), - sseRequest(SendChatCompletionRequest200Sse) - ) - } + Schema.Null + ]) + ), + "text": Schema.String, + "type": Schema.Literal("text") +}).annotate({ "identifier": "AnthropicTextBlockParam" }) +export type AnthropicToolSearchToolBm25 = { + readonly "allowed_callers"?: AnthropicAllowedCallers + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_bm25" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_bm25_20251119" | "tool_search_tool_bm25" } - -export interface OpenRouterClient { - readonly httpClient: HttpClient.HttpClient +export const AnthropicToolSearchToolBm25 = Schema.Struct({ + "allowed_callers": Schema.optionalKey(AnthropicAllowedCallers), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "defer_loading": Schema.optionalKey(Schema.Boolean), + "name": Schema.Literal("tool_search_tool_bm25"), + "strict": Schema.optionalKey(Schema.Boolean), + "type": Schema.Literals(["tool_search_tool_bm25_20251119", "tool_search_tool_bm25"]) +}).annotate({ "identifier": "AnthropicToolSearchToolBm25" }) +export type AnthropicToolSearchToolRegex = { + readonly "allowed_callers"?: AnthropicAllowedCallers + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "defer_loading"?: boolean + readonly "name": "tool_search_tool_regex" + readonly "strict"?: boolean + readonly "type": "tool_search_tool_regex_20251119" | "tool_search_tool_regex" +} +export const AnthropicToolSearchToolRegex = Schema.Struct({ + "allowed_callers": Schema.optionalKey(AnthropicAllowedCallers), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "defer_loading": Schema.optionalKey(Schema.Boolean), + "name": Schema.Literal("tool_search_tool_regex"), + "strict": Schema.optionalKey(Schema.Boolean), + "type": Schema.Literals(["tool_search_tool_regex_20251119", "tool_search_tool_regex"]) +}).annotate({ "identifier": "AnthropicToolSearchToolRegex" }) +export type FusionServerToolConfig = { + readonly "analysis_models"?: Arrays_6 + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "max_completion_tokens"?: number + readonly "max_tool_calls"?: number + readonly "model"?: string + readonly "reasoning"?: Objects_9 + readonly "temperature"?: number + readonly "tools"?: Arrays_7 +} +export const FusionServerToolConfig = Schema.Struct({ + "analysis_models": Schema.optionalKey(Arrays_6), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of output tokens (including reasoning tokens) each panelist and the judge model may produce per inner call. Controls the total output budget so reasoning-heavy models like GPT-5.5 do not exhaust their token allowance before producing visible text. When omitted, panelists default to 32000 and the judge to 20000." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of tool-calling steps each panelist (analysis model) and the judge model may take during their agentic web-research loop. Models with web_search/web_fetch enabled iterate until they produce a text response or hit this ceiling. Defaults to 8. Capped at 16." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(16).annotate({ "expected": "a value less than or equal to 16" })) + ), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Slug of the judge model that produces the structured analysis JSON. Defaults to the model used in the outer API request." + }) + ), + "reasoning": Schema.optionalKey(Objects_9), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Temperature forwarded to panelist inner calls. The judge always runs at temperature 0 regardless of this value. When omitted, the provider's default applies.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "tools": Schema.optionalKey(Arrays_7) +}).annotate({ + "description": "Configuration for the openrouter:fusion server tool.", + "identifier": "FusionServerToolConfig" +}) +export type AnthropicUsage = { + readonly "cache_creation": AnthropicCacheCreation + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "output_tokens": number + readonly "output_tokens_details": AnthropicOutputTokensDetails + readonly "server_tool_use": AnthropicServerToolUsage + readonly "service_tier": AnthropicServiceTier +} +export const AnthropicUsage = Schema.Struct({ + "cache_creation": AnthropicCacheCreation, + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "inference_geo": Schema.Union([Schema.String, Schema.Null]), + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": AnthropicOutputTokensDetails, + "server_tool_use": AnthropicServerToolUsage, + "service_tier": AnthropicServiceTier +}).annotate({ "identifier": "AnthropicUsage" }) +export type AnthropicDocumentBlock = { + readonly "citations"?: AnthropicCitationsConfig + readonly "source": AnthropicBase64PdfSource | AnthropicPlainTextSource + readonly "title": string | null + readonly "type": "document" +} +export const AnthropicDocumentBlock = Schema.Struct({ + "citations": Schema.optionalKey(AnthropicCitationsConfig), + "source": Schema.Union([AnthropicBase64PdfSource, AnthropicPlainTextSource]), + "title": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("document") +}).annotate({ "identifier": "AnthropicDocumentBlock" }) +export type AnthropicTextBlock = { + readonly "citations": ReadonlyArray | null + readonly "text": string + readonly "type": "text" +} +export const AnthropicTextBlock = Schema.Struct({ + "citations": Schema.Union([Schema.Array(AnthropicTextCitation), Schema.Null]), + "text": Schema.String, + "type": Schema.Literal("text") +}).annotate({ "identifier": "AnthropicTextBlock" }) +export type AnthropicToolUseBlock = { + readonly "caller": AnthropicCaller + readonly "id": string + readonly "input"?: Schema.Json + readonly "name": string + readonly "type": "tool_use" +} +export const AnthropicToolUseBlock = Schema.Struct({ + "caller": AnthropicCaller, + "id": Schema.String, + "input": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "name": Schema.String, + "type": Schema.Literal("tool_use") +}).annotate({ "identifier": "AnthropicToolUseBlock" }) +export type AnthropicWebSearchToolResult = { + readonly "caller": AnthropicCaller + readonly "content": ReadonlyArray | AnthropicWebSearchToolResultError + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" +} +export const AnthropicWebSearchToolResult = Schema.Struct({ + "caller": AnthropicCaller, + "content": Schema.Union([Schema.Array(AnthropicWebSearchResult), AnthropicWebSearchToolResultError]), + "tool_use_id": Schema.String, + "type": Schema.Literal("web_search_tool_result") +}).annotate({ "identifier": "AnthropicWebSearchToolResult" }) +export type ORAnthropicServerToolUseBlock = { + readonly "caller"?: ORAnthropicNullableCaller + readonly "id": string + readonly "input"?: Schema.Json + readonly "name": string + readonly "type": "server_tool_use" +} +export const ORAnthropicServerToolUseBlock = Schema.Struct({ + "caller": Schema.optionalKey(ORAnthropicNullableCaller), + "id": Schema.String, + "input": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "name": Schema.String, + "type": Schema.Literal("server_tool_use") +}).annotate({ "identifier": "ORAnthropicServerToolUseBlock" }) +export type AnthropicImageBlockParam = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "source": AnthropicBase64ImageSource | AnthropicUrlImageSource + readonly "type": "image" +} +export const AnthropicImageBlockParam = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "source": Schema.Union([AnthropicBase64ImageSource, AnthropicUrlImageSource], { mode: "oneOf" }), + "type": Schema.Literal("image") +}).annotate({ "identifier": "AnthropicImageBlockParam" }) +export type AnthropicAdvisorMessageUsageIteration = { + readonly "cache_creation"?: AnthropicIterationCacheCreation + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": "advisor_message" +} +export const AnthropicAdvisorMessageUsageIteration = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicIterationCacheCreation), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "model": Schema.String, + "type": Schema.Literal("advisor_message") +}).annotate({ "identifier": "AnthropicAdvisorMessageUsageIteration" }) +export type AnthropicBaseUsageIteration = { + readonly "cache_creation"?: AnthropicIterationCacheCreation + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number +} +export const AnthropicBaseUsageIteration = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicIterationCacheCreation), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))) +}).annotate({ "identifier": "AnthropicBaseUsageIteration" }) +export type AnthropicCompactionUsageIteration = { + readonly "cache_creation"?: AnthropicIterationCacheCreation + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "compaction" +} +export const AnthropicCompactionUsageIteration = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicIterationCacheCreation), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "type": Schema.Literal("compaction") +}).annotate({ "identifier": "AnthropicCompactionUsageIteration" }) +export type AnthropicMessageUsageIteration = { + readonly "cache_creation"?: AnthropicIterationCacheCreation + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model"?: string + readonly "type": "message" +} +export const AnthropicMessageUsageIteration = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicIterationCacheCreation), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "model": Schema.optionalKey(Schema.String), + "type": Schema.Literal("message") +}).annotate({ "identifier": "AnthropicMessageUsageIteration" }) +export type AnthropicUnknownUsageIteration = { + readonly "cache_creation"?: AnthropicIterationCacheCreation + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": string +} +export const AnthropicUnknownUsageIteration = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicIterationCacheCreation), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "output_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "type": Schema.String +}).annotate({ "identifier": "AnthropicUnknownUsageIteration" }) +export type AnthropicCodeExecutionContent = + | AnthropicCodeExecutionToolResultError + | AnthropicCodeExecutionResult + | AnthropicEncryptedCodeExecutionResult +export const AnthropicCodeExecutionContent = Schema.Union([ + AnthropicCodeExecutionToolResultError, + AnthropicCodeExecutionResult, + AnthropicEncryptedCodeExecutionResult +], { mode: "oneOf" }).annotate({ "identifier": "AnthropicCodeExecutionContent" }) +export type AnthropicTextEditorCodeExecutionToolResult = { + readonly "content": AnthropicTextEditorCodeExecutionContent + readonly "tool_use_id": string + readonly "type": "text_editor_code_execution_tool_result" +} +export const AnthropicTextEditorCodeExecutionToolResult = Schema.Struct({ + "content": AnthropicTextEditorCodeExecutionContent, + "tool_use_id": Schema.String, + "type": Schema.Literal("text_editor_code_execution_tool_result") +}).annotate({ "identifier": "AnthropicTextEditorCodeExecutionToolResult" }) +export type AnthropicToolSearchContent = AnthropicToolSearchResultError | AnthropicToolSearchResult +export const AnthropicToolSearchContent = Schema.Union([AnthropicToolSearchResultError, AnthropicToolSearchResult], { + mode: "oneOf" +}).annotate({ "identifier": "AnthropicToolSearchContent" }) +export type MessagesErrorResponse = { readonly "error": MessagesErrorDetail; readonly "type": "error" } +export const MessagesErrorResponse = Schema.Struct({ "error": MessagesErrorDetail, "type": Schema.Literal("error") }) + .annotate({ "identifier": "MessagesErrorResponse" }) +export type ApplyPatchServerTool_OpenRouter = { + readonly "parameters"?: ApplyPatchServerToolConfig + readonly "type": "openrouter:apply_patch" +} +export const ApplyPatchServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(ApplyPatchServerToolConfig), + "type": Schema.Literal("openrouter:apply_patch") +}).annotate({ + "description": + "OpenRouter built-in server tool: validates V4A diff patches for file operations (create, update, delete). Restricted to the Responses API.", + "identifier": "ApplyPatchServerTool_OpenRouter" +}) +export type ApplyPatchCallItem = { + readonly "call_id": string + readonly "id"?: string | null + readonly "operation": ApplyPatchCallOperation + readonly "status": ApplyPatchCallStatus + readonly "type": "apply_patch_call" +} +export const ApplyPatchCallItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "operation": ApplyPatchCallOperation, + "status": ApplyPatchCallStatus, + "type": Schema.Literal("apply_patch_call") +}).annotate({ + "description": + "A tool call emitted by the model requesting a V4A patch operation. The client applies the patch and echoes an `apply_patch_call_output` on the next turn.", + "identifier": "ApplyPatchCallItem" +}) +export type OutputApplyPatchCallItem = { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": ApplyPatchCallStatus + readonly "type": "apply_patch_call" +} +export const OutputApplyPatchCallItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": ApplyPatchCallStatus, + "type": Schema.Literal("apply_patch_call") +}).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand.", + "identifier": "OutputApplyPatchCallItem" +}) +export type OutputApplyPatchServerToolItem = { + readonly "call_id"?: string + readonly "id"?: string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": ToolCallStatus + readonly "type": "openrouter:apply_patch" +} +export const OutputApplyPatchServerToolItem = Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:apply_patch") +}).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn.", + "identifier": "OutputApplyPatchServerToolItem" +}) +export type CreateBYOKKeyResponse = { readonly "data": BYOKKey } +export const CreateBYOKKeyResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => BYOKKey).annotate({ + "description": "The created BYOK credential." + }) +}).annotate({ "identifier": "CreateBYOKKeyResponse" }) +export type GetBYOKKeyResponse = { readonly "data": BYOKKey } +export const GetBYOKKeyResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => BYOKKey).annotate({ "description": "The BYOK credential." }) +}).annotate({ "identifier": "GetBYOKKeyResponse" }) +export type ListBYOKKeysResponse = { readonly "data": ReadonlyArray; readonly "total_count": number } +export const ListBYOKKeysResponse = Schema.Struct({ + "data": Schema.Array(BYOKKey).annotate({ "description": "List of BYOK credentials." }), + "total_count": Schema.Number.annotate({ "description": "Total number of BYOK credentials matching the filters." }) + .check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "identifier": "ListBYOKKeysResponse" }) +export type UpdateBYOKKeyResponse = { readonly "data": BYOKKey } +export const UpdateBYOKKeyResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => BYOKKey).annotate({ + "description": "The updated BYOK credential." + }) +}).annotate({ "identifier": "UpdateBYOKKeyResponse" }) +export type ChatTokenLogprobs = Objects_7 | null +export const ChatTokenLogprobs = Schema.Union([Objects_7, Schema.Null]).annotate({ + "description": "Log probabilities for the completion", + "identifier": "ChatTokenLogprobs" +}) +export type BashServerToolConfig = { + readonly "engine"?: BashServerToolEngine + readonly "environment"?: BashServerToolEnvironment + readonly "sleep_after_seconds"?: SandboxSleepAfterSeconds +} +export const BashServerToolConfig = Schema.Struct({ + "engine": Schema.optionalKey(BashServerToolEngine), + "environment": Schema.optionalKey(BashServerToolEnvironment), + "sleep_after_seconds": Schema.optionalKey(SandboxSleepAfterSeconds) +}).annotate({ + "description": "Configuration for the openrouter:bash server tool", + "identifier": "BashServerToolConfig" +}) +export type ShellServerToolConfig = { + readonly "engine"?: ShellServerToolEngine + readonly "environment"?: ShellServerToolEnvironment + readonly "sleep_after_seconds"?: SandboxSleepAfterSeconds +} +export const ShellServerToolConfig = Schema.Struct({ + "engine": Schema.optionalKey(ShellServerToolEngine), + "environment": Schema.optionalKey(ShellServerToolEnvironment), + "sleep_after_seconds": Schema.optionalKey(SandboxSleepAfterSeconds) +}).annotate({ + "description": "Configuration for the openrouter:shell server tool", + "identifier": "ShellServerToolConfig" +}) +export type ModelBenchmarks = { readonly "artificial_analysis"?: AABenchmarkEntry; readonly "design_arena": Arrays_9 } +export const ModelBenchmarks = Schema.Struct({ + "artificial_analysis": Schema.optionalKey(AABenchmarkEntry), + "design_arena": Arrays_9 +}).annotate({ + "description": "Third-party benchmark rankings for this model. Omitted when no benchmark data is available.", + "identifier": "ModelBenchmarks" +}) +export type TextConfig = { readonly "format"?: Formats; readonly "verbosity"?: "high" | "low" | "medium" | null } +export const TextConfig = Schema.Struct({ + "format": Schema.optionalKey(Formats), + "verbosity": Schema.optionalKey( + Schema.Union([Schema.Literal("high"), Schema.Literal("low"), Schema.Literal("medium"), Schema.Null]) + ) +}).annotate({ "description": "Text output configuration including format and verbosity", "identifier": "TextConfig" }) +export type TextExtendedConfig = { + readonly "format"?: Formats + readonly "verbosity"?: "high" | "low" | "medium" | null +} +export const TextExtendedConfig = Schema.Struct({ + "format": Schema.optionalKey(Formats), + "verbosity": Schema.optionalKey( + Schema.Union([Schema.Literal("high"), Schema.Literal("low"), Schema.Literal("medium"), Schema.Union([Schema.Null])]) + ) +}).annotate({ + "description": "Text output configuration including format and verbosity", + "identifier": "TextExtendedConfig" +}) +export type FusionCallAnalysisCompletedEvent = { + readonly "analysis": FusionAnalysisResult + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.fusion_call.analysis.completed" +} +export const FusionCallAnalysisCompletedEvent = Schema.Struct({ + "analysis": FusionAnalysisResult, + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.fusion_call.analysis.completed") +}).annotate({ + "description": "Emitted when the fusion judge completes with the structured analysis.", + "identifier": "FusionCallAnalysisCompletedEvent" +}) +export type OutputFusionServerToolItem = { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id"?: string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": ToolCallStatus + readonly "type": "openrouter:fusion" +} +export const OutputFusionServerToolItem = Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the fusion run did not produce an analysis result." }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.optionalKey(Schema.String), + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })).annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": ToolCallStatus, + "type": Schema.Literal("openrouter:fusion") +}).annotate({ + "description": "An openrouter:fusion server tool output item", + "identifier": "OutputFusionServerToolItem" +}) +export type ImageModelListItem = { + readonly "architecture": ImageModelArchitecture + readonly "created": number + readonly "description": string + readonly "endpoints": string + readonly "id": string + readonly "name": string + readonly "supported_parameters": SupportedParameters + readonly "supports_streaming": boolean +} +export const ImageModelListItem = Schema.Struct({ + "architecture": ImageModelArchitecture, + "created": Schema.Number.annotate({ "description": "Unix timestamp (seconds) of when the model was created" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "description": Schema.String, + "endpoints": Schema.String.annotate({ + "description": "Relative URL to the full per-endpoint records for this model" + }), + "id": Schema.String.annotate({ "description": "Model slug" }), + "name": Schema.String.annotate({ "description": "Display name" }), + "supported_parameters": SupportedParameters, + "supports_streaming": Schema.Boolean.annotate({ + "description": + "Whether any endpoint of this model supports native SSE streaming on the dedicated Image API (i.e. `stream: true` in the request). OR across endpoints." + }) +}).annotate({ "description": "A single image model in the discovery listing.", "identifier": "ImageModelListItem" }) +export type ObservabilityArizeDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "baseUrl"?: string + readonly "headers"?: {} + readonly "modelId": string + readonly "spaceKey": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "arize" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityArizeDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "baseUrl": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "modelId": Schema.String.annotate({ "description": "The name of the tracing project in Arize AX" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "spaceKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("arize"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityArizeDestination" }) +export type ObservabilityBraintrustDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "baseUrl"?: string + readonly "headers"?: {} + readonly "projectId": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "braintrust" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityBraintrustDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "baseUrl": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "projectId": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("braintrust"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityBraintrustDestination" }) +export type ObservabilityClickhouseDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "database": string + readonly "headers"?: {} + readonly "host": string + readonly "password": string + readonly "table"?: string + readonly "username": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "clickhouse" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityClickhouseDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "database": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "host": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "password": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "table": Schema.optionalKey(Schema.String), + "username": Schema.String.annotate({ + "description": "If you have not set a specific username in ClickHouse, simply type in 'default' below." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("clickhouse"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityClickhouseDestination" }) +export type ObservabilityDatadogDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "headers"?: {} + readonly "mlApp": string + readonly "url"?: string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "datadog" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityDatadogDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.annotate({ + "description": "Datadog API key must have LLM Observability permissions. Create at: " + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "mlApp": Schema.String.annotate({ "description": "Name to identify your application in Datadog LLM Observability" }) + .check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "url": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Datadog API URL for your region (e.g., https://api.datadoghq.com, https://api.us3.datadoghq.com, https://api.datadoghq.eu)" + }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("datadog"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityDatadogDestination" }) +export type ObservabilityGrafanaDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "baseUrl"?: string + readonly "headers"?: {} + readonly "instanceId": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "grafana" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityGrafanaDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "baseUrl": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "instanceId": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("grafana"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityGrafanaDestination" }) +export type ObservabilityLangfuseDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "baseUrl"?: string + readonly "headers"?: {} + readonly "publicKey": string + readonly "secretKey": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "langfuse" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityLangfuseDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "baseUrl": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "publicKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "secretKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("langfuse"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityLangfuseDestination" }) +export type ObservabilityLangsmithDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "endpoint"?: string + readonly "headers"?: {} + readonly "project"?: string + readonly "workspaceId"?: string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "langsmith" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityLangsmithDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "endpoint": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "project": Schema.optionalKey( + Schema.String.annotate({ + "description": "The name for this project, such as pr-openrouter-demo. Defaults to \"main\" if not set." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + ), + "workspaceId": Schema.optionalKey( + Schema.String.annotate({ + "description": "Required for org-scoped API keys. Find this in your LangSmith workspace settings." + }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("langsmith"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityLangsmithDestination" }) +export type ObservabilityNewrelicDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "headers"?: {}; readonly "licenseKey": string; readonly "region"?: "us" | "eu" } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "newrelic" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityNewrelicDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "licenseKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "region": Schema.optionalKey(Schema.Literals(["us", "eu"])) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("newrelic"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityNewrelicDestination" }) +export type ObservabilityOpikDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "headers"?: {} + readonly "projectName": string + readonly "workspace": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "opik" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityOpikDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "projectName": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "workspace": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("opik"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityOpikDestination" }) +export type ObservabilityOtelCollectorDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "endpoint": string; readonly "headers"?: {} } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "otel-collector" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityOtelCollectorDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "endpoint": Schema.String, + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "Custom HTTP headers as a JSON object. For Axiom, use {\"Authorization\": \"Bearer xaat-xxx\", \"X-Axiom-Dataset\": \"your-dataset\"}" + }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("otel-collector"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityOtelCollectorDestination" }) +export type ObservabilityPosthogDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "apiKey": string; readonly "endpoint"?: string; readonly "headers"?: {} } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "posthog" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityPosthogDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "endpoint": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("posthog"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityPosthogDestination" }) +export type ObservabilityRampDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "apiKey": string; readonly "baseUrl"?: string; readonly "headers"?: {} } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "ramp" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityRampDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.annotate({ "description": "Generate this in your Ramp integration settings." }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "baseUrl": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to Ramp." }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("ramp"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityRampDestination" }) +export type ObservabilityS3Destination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "accessKeyId": string + readonly "bucketName": string + readonly "endpoint"?: string + readonly "headers"?: {} + readonly "pathTemplate"?: string + readonly "prefix"?: string + readonly "region"?: string + readonly "secretAccessKey": string + readonly "sessionToken"?: string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "s3" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityS3Destination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "accessKeyId": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "bucketName": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "endpoint": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Only for S3-compatible services like Cloudflare R2 (https://account-id.r2.cloudflarestorage.com) or MinIO. Leave blank for standard AWS S3.", + "format": "uri" + }) + ), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "pathTemplate": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Template for S3 object path. The filename ({traceId}-{timestamp}.json) is automatically appended. Available variables: {prefix}, {date}, {year}, {month}, {day}, {apiKeyName}" + }) + ), + "prefix": Schema.optionalKey(Schema.String), + "region": Schema.optionalKey(Schema.String), + "secretAccessKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "sessionToken": Schema.optionalKey(Schema.String) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("s3"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityS3Destination" }) +export type ObservabilitySentryDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "dsn": string; readonly "headers"?: {}; readonly "otlpEndpoint": string } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "sentry" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilitySentryDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "dsn": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })) + .check( + Schema.isPattern(new RegExp("^https:\\/\\/([^:@]+)(?::[^@]*)?@([^/]+)(?:\\/[^/]+)*\\/(\\d+)\\/?$")).annotate({ + "expected": "a string matching the RegExp ^https:\\/\\/([^:@]+)(?::[^@]*)?@([^/]+)(?:\\/[^/]+)*\\/(\\d+)\\/?$" + }) + ), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "otlpEndpoint": Schema.String + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("sentry"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilitySentryDestination" }) +export type ObservabilitySnowflakeDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "account": string + readonly "database"?: string + readonly "headers"?: {} + readonly "schema"?: string + readonly "table"?: string + readonly "token": string + readonly "warehouse"?: string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "snowflake" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilitySnowflakeDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "account": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "database": Schema.optionalKey(Schema.String), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "schema": Schema.optionalKey(Schema.String), + "table": Schema.optionalKey(Schema.String), + "token": Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "warehouse": Schema.optionalKey(Schema.String) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("snowflake"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilitySnowflakeDestination" }) +export type ObservabilityWeaveDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { + readonly "apiKey": string + readonly "baseUrl"?: string + readonly "entity": string + readonly "headers"?: {} + readonly "project": string + } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "weave" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityWeaveDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "apiKey": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "baseUrl": Schema.optionalKey(Schema.String), + "entity": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "headers": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Custom HTTP headers to include in requests to this destination." }) + ), + "project": Schema.String.check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("weave"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityWeaveDestination" }) +export type ObservabilityWebhookDestination = { + readonly "api_key_hashes": ReadonlyArray | null + readonly "config": { readonly "headers"?: {}; readonly "method"?: "POST" | "PUT"; readonly "url": string } + readonly "created_at": string + readonly "enabled": boolean + readonly "filter_rules": ObservabilityFilterRulesConfig + readonly "id": string + readonly "name": string | null + readonly "privacy_mode": boolean + readonly "sampling_rate": number + readonly "type": "webhook" + readonly "updated_at": string + readonly "workspace_id": string +} +export const ObservabilityWebhookDestination = Schema.Struct({ + "api_key_hashes": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes (`api_keys.hash`) whose traffic is forwarded to this destination. `null` means all keys." + }), + "config": Schema.Struct({ + "headers": Schema.optionalKey(Schema.Struct({})), + "method": Schema.optionalKey(Schema.Literals(["POST", "PUT"])), + "url": Schema.String + }), + "created_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was created." }), + "enabled": Schema.Boolean.annotate({ "description": "Whether this destination is currently enabled." }), + "filter_rules": ObservabilityFilterRulesConfig, + "id": Schema.String.annotate({ "description": "Stable public identifier for this destination.", "format": "uuid" }), + "name": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Human-readable name for the destination." + }), + "privacy_mode": Schema.Boolean.annotate({ + "description": "When true, request/response bodies are not forwarded to this destination — only metadata." + }), + "sampling_rate": Schema.Number.annotate({ + "description": "Sampling rate for events sent to this destination, between 0.0001 and 1 (1 = 100%).", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "type": Schema.Literal("webhook"), + "updated_at": Schema.String.annotate({ "description": "ISO timestamp of when the destination was last updated." }), + "workspace_id": Schema.String.annotate({ + "description": "ID of the workspace this destination belongs to.", + "format": "uuid" + }) +}).annotate({ "identifier": "ObservabilityWebhookDestination" }) +export type ObservabilityFilterRulesConfigNullable = Objects_10 | null +export const ObservabilityFilterRulesConfigNullable = Schema.Union([Objects_10, Schema.Null]).annotate({ + "description": "Optional structured filter rules controlling which events are forwarded.", + "identifier": "ObservabilityFilterRulesConfigNullable" +}) +export type BaseTextDeltaEvent = { + readonly "content_index": number + readonly "delta": string + readonly "item_id": string + readonly "logprobs": ReadonlyArray + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_text.delta" +} +export const BaseTextDeltaEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "delta": Schema.String, + "item_id": Schema.String, + "logprobs": Schema.Array(OpenResponsesLogProbs), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_text.delta") +}).annotate({ "description": "Event emitted when a text delta is streamed", "identifier": "BaseTextDeltaEvent" }) +export type BaseTextDoneEvent = { + readonly "content_index": number + readonly "item_id": string + readonly "logprobs": ReadonlyArray + readonly "output_index": number + readonly "sequence_number": number + readonly "text": string + readonly "type": "response.output_text.done" +} +export const BaseTextDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "logprobs": Schema.Array(OpenResponsesLogProbs), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "text": Schema.String, + "type": Schema.Literal("response.output_text.done") +}).annotate({ "description": "Event emitted when text streaming is complete", "identifier": "BaseTextDoneEvent" }) +export type FileParserPlugin = { + readonly "enabled"?: boolean + readonly "id": "file-parser" + readonly "pdf"?: PDFParserOptions +} +export const FileParserPlugin = Schema.Struct({ + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Set to false to disable the file-parser plugin for this request. Defaults to true." + }) + ), + "id": Schema.Literal("file-parser"), + "pdf": Schema.optionalKey(PDFParserOptions) +}).annotate({ "identifier": "FileParserPlugin" }) +export type Objects_148 = { + readonly "allow_fallbacks"?: boolean | null + readonly "data_collection"?: "deny" | "allow" | null + readonly "enforce_distillable_text"?: boolean | null + readonly "ignore"?: ReadonlyArray | null + readonly "max_price"?: { + readonly "audio"?: string + readonly "completion"?: string + readonly "image"?: string + readonly "prompt"?: string + readonly "request"?: string + } + readonly "only"?: ReadonlyArray | null + readonly "order"?: ReadonlyArray | null + readonly "preferred_max_latency"?: PreferredMaxLatency + readonly "preferred_min_throughput"?: PreferredMinThroughput + readonly "quantizations"?: ReadonlyArray | null + readonly "require_parameters"?: boolean | null + readonly "sort"?: ProviderSort | ProviderSortConfig | null + readonly "zdr"?: boolean | null +} +export const Objects_148 = Schema.Struct({ + "allow_fallbacks": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + }) + ), + "data_collection": Schema.optionalKey( + Schema.Union([Schema.Literal("deny"), Schema.Literal("allow"), Schema.Null]).annotate({ + "description": + "Data collection setting. If no available model provider meets the requirement, your request will return an error.\n- allow: (default) allow providers which store user data non-transiently and may train on it\n\n- deny: use only providers which do not collect user data." + }) + ), + "enforce_distillable_text": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to restrict routing to only models that allow text distillation. When true, only models where the author has allowed distillation will be used." + }) + ), + "ignore": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "List of provider slugs to ignore. If provided, this list is merged with your account-wide ignored provider settings for this request." + }) + ), + "max_price": Schema.optionalKey( + Schema.Struct({ + "audio": Schema.optionalKey(Schema.String.annotate({ "description": "Maximum price in USD per audio unit" })), + "completion": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum price in USD per million completion tokens" }) + ), + "image": Schema.optionalKey(Schema.String.annotate({ "description": "Maximum price in USD per image" })), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "Maximum price in USD per million prompt tokens" }) + ), + "request": Schema.optionalKey(Schema.String.annotate({ "description": "Maximum price in USD per request" })) + }).annotate({ + "description": + "The object specifying the maximum price you want to pay for this request. USD price per million tokens, for prompt and completion." + }) + ), + "only": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "List of provider slugs to allow. If provided, this list is merged with your account-wide allowed provider settings for this request." + }) + ), + "order": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Union([ProviderName, Schema.String])), Schema.Null]).annotate({ + "description": + "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." + }) + ), + "preferred_max_latency": Schema.optionalKey(PreferredMaxLatency), + "preferred_min_throughput": Schema.optionalKey(PreferredMinThroughput), + "quantizations": Schema.optionalKey( + Schema.Union([Schema.Array(Quantization), Schema.Null]).annotate({ + "description": "A list of quantization levels to filter the provider by." + }) + ), + "require_parameters": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "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." + }) + ), + "sort": Schema.optionalKey( + Schema.Union([ProviderSort, ProviderSortConfig, Schema.Null]).annotate({ + "description": + "The sorting strategy to use for this request, if \"order\" is not specified. When set, no load balancing is performed." + }) + ), + "zdr": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to restrict routing to only ZDR (Zero Data Retention) endpoints. When true, only endpoints that do not retain prompts will be used." + }) + ) +}) +export type Arrays_11 = ReadonlyArray +export const Arrays_11 = Schema.Array(PipelineStage) +export type Prediction = Objects_17 | null +export const Prediction = Schema.Union([Objects_17, Schema.Null]).annotate({ + "description": + "Static predicted output content. Supported models can use this to reduce latency when much of the response is known in advance.", + "identifier": "Prediction" +}) +export type ListPresetsResponse = { readonly "data": ReadonlyArray; readonly "total_count": number } +export const ListPresetsResponse = Schema.Struct({ + "data": Schema.Array(Preset), + "total_count": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) +}).annotate({ "description": "A paginated list of presets.", "identifier": "ListPresetsResponse" }) +export type CreatePresetFromInferenceResponse = { readonly "data": PresetWithDesignatedVersion } +export const CreatePresetFromInferenceResponse = Schema.Struct({ "data": PresetWithDesignatedVersion }).annotate({ + "description": "Response containing the created preset with its designated version.", + "identifier": "CreatePresetFromInferenceResponse" +}) +export type GetPresetResponse = { readonly "data": PresetWithDesignatedVersion } +export const GetPresetResponse = Schema.Struct({ "data": PresetWithDesignatedVersion }).annotate({ + "description": "A preset with its currently designated version.", + "identifier": "GetPresetResponse" +}) +export type ChatContentText = { + readonly "cache_control"?: ChatContentCacheControl + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": "text" +} +export const ChatContentText = Schema.Struct({ + "cache_control": Schema.optionalKey(ChatContentCacheControl), + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Literal("text") +}).annotate({ "description": "Text content part", "identifier": "ChatContentText" }) +export type CustomToolCallOutputItem = { + readonly "call_id": string + readonly "id"?: string + readonly "output": + | string + | ReadonlyArray< + { + readonly "prompt_cache_breakpoint"?: { readonly "mode": "explicit"; readonly [x: string]: Schema.Json } | null + readonly "text": string + readonly "type": "input_text" + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": never + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": never + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + } | { + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": never + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": never + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + } | { + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": never + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": never + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": "input_file" + } + > + readonly "type": "custom_tool_call_output" +} +export const CustomToolCallOutputItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "output": Schema.Union([ + Schema.Union([Schema.String]), + Schema.Union([Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "mode": Schema.Literal("explicit") }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]) + ]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }) + ]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }) + ), + "text": Schema.String, + "type": Schema.Literal("input_text") + }).annotate({ "description": "Text input content item" }), + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Never, + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String + }).annotate({ "description": "Text input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String + }).annotate({ "description": "Text input content item" }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Never, + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "Image input content item" }), + Schema.Struct({ + "detail": Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("high"), + Schema.Literal("low"), + Schema.Literal("original") + ]), + "image_url": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])), + "type": Schema.Literal("input_image") + }).annotate({ "description": "Image input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "Image input content item" }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Never, + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String) + }).annotate({ "description": "File input content item" }), + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Never, + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String) + }).annotate({ "description": "File input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Literal("input_file") + }).annotate({ "description": "File input content item" }) + ], { mode: "oneOf" }) + ], { mode: "oneOf" }))]) + ]), + "type": Schema.Literal("custom_tool_call_output") +}).annotate({ + "description": + "The output from a custom (freeform-grammar) tool call execution. Mirrors `function_call_output` but is matched to a `custom_tool_call` rather than a `function_call`.", + "identifier": "CustomToolCallOutputItem" +}) +export type FunctionCallOutputItem = { + readonly "call_id": string + readonly "id"?: string | null + readonly "output": + | string + | ReadonlyArray< + { + readonly "prompt_cache_breakpoint"?: { readonly "mode": "explicit"; readonly [x: string]: Schema.Json } | null + readonly "text": string + readonly "type": "input_text" + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": never + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": never + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + } | { + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": never + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": never + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + } | { + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": never + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + } | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": never + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + } | { + readonly "file_data"?: string + readonly "file_id"?: string | null + readonly "file_url"?: string + readonly "filename"?: string + readonly "type": "input_file" + } + > + readonly "status"?: ToolCallStatus | null + readonly "type": "function_call_output" +} +export const FunctionCallOutputItem = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "output": Schema.Union([ + Schema.Union([Schema.String]), + Schema.Union([Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "mode": Schema.Literal("explicit") }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]) + ]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }) + ]).annotate({ + "description": + "Marks an explicit prompt-cache boundary on this content block (OpenAI-style). Everything through the block carrying this marker is part of the candidate cached prefix. Supported natively by OpenAI GPT-5.6 and newer; on providers that use Anthropic-style `cache_control`, OpenRouter converts the marker to that format automatically." + }) + ), + "text": Schema.String, + "type": Schema.Literal("input_text") + }).annotate({ "description": "Text input content item" }), + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Never, + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String + }).annotate({ "description": "Text input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String + }).annotate({ "description": "Text input content item" }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Never, + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "Image input content item" }), + Schema.Struct({ + "detail": Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("high"), + Schema.Literal("low"), + Schema.Literal("original") + ]), + "image_url": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])), + "type": Schema.Literal("input_image") + }).annotate({ "description": "Image input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "Image input content item" }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Never, + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String) + }).annotate({ "description": "File input content item" }), + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Never, + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String) + }).annotate({ "description": "File input content item" }), + Schema.Struct({ + "file_data": Schema.optionalKey(Schema.String), + "file_id": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])), + "file_url": Schema.optionalKey(Schema.String), + "filename": Schema.optionalKey(Schema.String), + "type": Schema.Literal("input_file") + }).annotate({ "description": "File input content item" }) + ], { mode: "oneOf" }) + ], { mode: "oneOf" }))]) + ]), + "status": Schema.optionalKey(Schema.Union([ToolCallStatus, Schema.Null])), + "type": Schema.Literal("function_call_output") +}).annotate({ "description": "The output from a function call execution", "identifier": "FunctionCallOutputItem" }) +export type InputText = { + readonly "prompt_cache_breakpoint"?: PromptCacheBreakpoint + readonly "text": string + readonly "type": "input_text" +} +export const InputText = Schema.Struct({ + "prompt_cache_breakpoint": Schema.optionalKey(PromptCacheBreakpoint), + "text": Schema.String, + "type": Schema.Literal("input_text") +}).annotate({ "description": "Text input content item", "identifier": "InputText" }) +export type Guardrail = { + readonly "allowed_models"?: ReadonlyArray | null + readonly "allowed_providers"?: ReadonlyArray | null + readonly "content_filter_builtins"?: ReadonlyArray | null + readonly "content_filters"?: ReadonlyArray | null + readonly "created_at": string + readonly "description"?: string | null + readonly "enforce_zdr"?: boolean | null + readonly "enforce_zdr_anthropic"?: boolean | null + readonly "enforce_zdr_google"?: boolean | null + readonly "enforce_zdr_openai"?: boolean | null + readonly "enforce_zdr_other"?: boolean | null + readonly "enforce_zdr_xai"?: boolean | null + readonly "id": string + readonly "ignored_models"?: ReadonlyArray | null + readonly "ignored_providers"?: ReadonlyArray | null + readonly "limit_usd"?: number | null + readonly "name": string + readonly "reset_interval"?: GuardrailInterval + readonly "updated_at"?: string | null + readonly "workspace_id": string +} +export const Guardrail = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": "Array of model canonical_slugs (immutable identifiers)" + }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ "description": "List of allowed provider IDs" }) + ), + "content_filter_builtins": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterBuiltinEntry), Schema.Null]).annotate({ + "description": + "Builtin content filters applied to requests. Includes PII detectors and the regex-based prompt injection detector." + }) + ), + "content_filters": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterEntry), Schema.Null]).annotate({ + "description": "Custom regex content filters applied to request messages" + }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the guardrail was created" }), + "description": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Description of the guardrail" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Deprecated. Use enforce_zdr_anthropic, enforce_zdr_openai, enforce_zdr_google, enforce_zdr_xai, and enforce_zdr_other instead. When provided, its value is copied into any of those per-provider fields that are not explicitly specified on the request." + }) + ), + "enforce_zdr_anthropic": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Anthropic models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_google": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Google models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_openai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for OpenAI models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_other": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for models that are not from Anthropic, OpenAI, Google, or xAI. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_xai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for xAI models. Falls back to enforce_zdr when not provided." + }) + ), + "id": Schema.String.annotate({ "description": "Unique identifier for the guardrail", "format": "uuid" }), + "ignored_models": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": "Array of model canonical_slugs to exclude from routing" + }) + ), + "ignored_providers": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": "List of provider IDs to exclude from routing" + }) + ), + "limit_usd": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Spending limit in USD", "format": "double" }) + ), + "name": Schema.String.annotate({ "description": "Name of the guardrail" }), + "reset_interval": Schema.optionalKey(GuardrailInterval), + "updated_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the guardrail was last updated" + }) + ), + "workspace_id": Schema.String.annotate({ "description": "The workspace ID this guardrail belongs to." }) +}).annotate({ "identifier": "Guardrail" }) +export type CreateGuardrailRequest = { + readonly "allowed_models"?: ReadonlyArray | null + readonly "allowed_providers"?: ReadonlyArray | null + readonly "content_filter_builtins"?: ReadonlyArray | null + readonly "content_filters"?: ReadonlyArray | null + readonly "description"?: string | null + readonly "enforce_zdr"?: boolean | null + readonly "enforce_zdr_anthropic"?: boolean | null + readonly "enforce_zdr_google"?: boolean | null + readonly "enforce_zdr_openai"?: boolean | null + readonly "enforce_zdr_other"?: boolean | null + readonly "enforce_zdr_xai"?: boolean | null + readonly "ignored_models"?: ReadonlyArray | null + readonly "ignored_providers"?: ReadonlyArray | null + readonly "limit_usd"?: number | null + readonly "name": string + readonly "reset_interval"?: GuardrailInterval + readonly "workspace_id"?: string +} +export const CreateGuardrailRequest = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "Array of model identifiers (slug or canonical_slug accepted)" }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "List of allowed provider IDs" }) + ), + "content_filter_builtins": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterBuiltinEntryInput), Schema.Null]).annotate({ + "description": + "Builtin content filters to apply. Every builtin slug supports \"block\", \"redact\", and the detect-only \"flag\" action." + }) + ), + "content_filters": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterEntry), Schema.Null]).annotate({ + "description": "Custom regex content filters to apply to request messages" + }) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(1000).annotate({ "expected": "a value with a length of at most 1000" })), + Schema.Null + ]).annotate({ "description": "Description of the guardrail" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Deprecated. Use enforce_zdr_anthropic, enforce_zdr_openai, enforce_zdr_google, enforce_zdr_xai, and enforce_zdr_other instead. When provided, its value is copied into any of those per-provider fields that are not explicitly specified on the request." + }) + ), + "enforce_zdr_anthropic": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Anthropic models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_google": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Google models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_openai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for OpenAI models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_other": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for models that are not from Anthropic, OpenAI, Google, or xAI. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_xai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for xAI models. Falls back to enforce_zdr when not provided." + }) + ), + "ignored_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ + "description": "Array of model identifiers to exclude from routing (slug or canonical_slug accepted)" + }) + ), + "ignored_providers": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "List of provider IDs to exclude from routing" }) + ), + "limit_usd": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Spending limit in USD", "format": "double" }) + ), + "name": Schema.String.annotate({ "description": "Name for the new guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ).check(Schema.isMaxLength(200).annotate({ "expected": "a value with a length of at most 200" })), + "reset_interval": Schema.optionalKey(GuardrailInterval), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The workspace to create the guardrail in. Defaults to the default workspace if not provided.", + "format": "uuid" + }) + ) +}).annotate({ "identifier": "CreateGuardrailRequest" }) +export type UpdateGuardrailRequest = { + readonly "allowed_models"?: ReadonlyArray | null + readonly "allowed_providers"?: ReadonlyArray | null + readonly "content_filter_builtins"?: ReadonlyArray | null + readonly "content_filters"?: ReadonlyArray | null + readonly "description"?: string | null + readonly "enforce_zdr"?: boolean | null + readonly "enforce_zdr_anthropic"?: boolean | null + readonly "enforce_zdr_google"?: boolean | null + readonly "enforce_zdr_openai"?: boolean | null + readonly "enforce_zdr_other"?: boolean | null + readonly "enforce_zdr_xai"?: boolean | null + readonly "ignored_models"?: ReadonlyArray | null + readonly "ignored_providers"?: ReadonlyArray | null + readonly "limit_usd"?: number | null + readonly "name"?: string + readonly "reset_interval"?: GuardrailInterval +} +export const UpdateGuardrailRequest = Schema.Struct({ + "allowed_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "Array of model identifiers (slug or canonical_slug accepted)" }) + ), + "allowed_providers": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "New list of allowed provider IDs" }) + ), + "content_filter_builtins": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterBuiltinEntryInput), Schema.Null]).annotate({ + "description": + "Builtin content filters to apply. Set to null to remove. Every builtin slug supports \"block\", \"redact\", and the detect-only \"flag\" action." + }) + ), + "content_filters": Schema.optionalKey( + Schema.Union([Schema.Array(ContentFilterEntry), Schema.Null]).annotate({ + "description": "Custom regex content filters to apply. Set to null to remove." + }) + ), + "description": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMaxLength(1000).annotate({ "expected": "a value with a length of at most 1000" })), + Schema.Null + ]).annotate({ "description": "New description for the guardrail" }) + ), + "enforce_zdr": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Deprecated. Use enforce_zdr_anthropic, enforce_zdr_openai, enforce_zdr_google, enforce_zdr_xai, and enforce_zdr_other instead. When provided, its value is copied into any of those per-provider fields that are not explicitly specified on the request." + }) + ), + "enforce_zdr_anthropic": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Anthropic models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_google": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for Google models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_openai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for OpenAI models. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_other": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for models that are not from Anthropic, OpenAI, Google, or xAI. Falls back to enforce_zdr when not provided." + }) + ), + "enforce_zdr_xai": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enforce zero data retention for xAI models. Falls back to enforce_zdr when not provided." + }) + ), + "ignored_models": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ + "description": "Array of model identifiers to exclude from routing (slug or canonical_slug accepted)" + }) + ), + "ignored_providers": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ "description": "List of provider IDs to exclude from routing" }) + ), + "limit_usd": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "New spending limit in USD", "format": "double" }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ "description": "New name for the guardrail" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ).check(Schema.isMaxLength(200).annotate({ "expected": "a value with a length of at most 200" })) + ), + "reset_interval": Schema.optionalKey(GuardrailInterval) +}).annotate({ "identifier": "UpdateGuardrailRequest" }) +export type SpeechRequest = { + readonly "input": string + readonly "model": string + readonly "provider"?: { readonly "options"?: ProviderOptions } + readonly "response_format"?: "mp3" | "pcm" + readonly "speed"?: number + readonly "voice": string +} +export const SpeechRequest = Schema.Struct({ + "input": Schema.String.annotate({ "description": "Text to synthesize" }), + "model": Schema.String.annotate({ "description": "TTS model identifier" }), + "provider": Schema.optionalKey( + Schema.Struct({ "options": Schema.optionalKey(ProviderOptions) }).annotate({ + "description": "Provider-specific passthrough configuration" + }) + ), + "response_format": Schema.optionalKey( + Schema.Literals(["mp3", "pcm"]).annotate({ "description": "Audio output format" }) + ), + "speed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Playback speed multiplier. Only used by models that support it (e.g. OpenAI TTS). Ignored by other providers.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "voice": Schema.String.annotate({ "description": "Voice identifier (provider-specific)." }) +}).annotate({ "description": "Text-to-speech request input", "identifier": "SpeechRequest" }) +export type STTRequest = { + readonly "input_audio": STTInputAudio + readonly "language"?: string + readonly "model": string + readonly "provider"?: { readonly "options"?: ProviderOptions } + readonly "response_format"?: "json" | "verbose_json" + readonly "temperature"?: number + readonly "timestamp_granularities"?: ReadonlyArray +} +export const STTRequest = Schema.Struct({ + "input_audio": STTInputAudio, + "language": Schema.optionalKey( + Schema.String.annotate({ + "description": "ISO-639-1 language code (e.g., \"en\", \"ja\"). Auto-detected if omitted." + }) + ), + "model": Schema.String.annotate({ "description": "STT model identifier" }), + "provider": Schema.optionalKey( + Schema.Struct({ "options": Schema.optionalKey(ProviderOptions) }).annotate({ + "description": "Provider-specific passthrough configuration" + }) + ), + "response_format": Schema.optionalKey( + Schema.Literals(["json", "verbose_json"]).annotate({ + "description": + "Output format. \"json\" (default) returns { text, usage }. \"verbose_json\" additionally returns task, language, duration, and segment-level timestamps; only supported by OpenAI-compatible providers." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sampling temperature for transcription", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "timestamp_granularities": Schema.optionalKey( + Schema.Array(STTTimestampGranularity).annotate({ + "description": + "Timestamp detail levels to include when response_format is \"verbose_json\". \"segment\" returns segment-level timestamps; \"word\" additionally returns word-level timestamps in the words array. Ignored unless response_format is \"verbose_json\"." + }) + ) +}).annotate({ + "description": "Speech-to-text request input. Accepts a JSON body with input_audio containing base64-encoded audio.", + "identifier": "STTRequest" +}) +export type VideoGenerationRequest = { + readonly "aspect_ratio"?: "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "9:21" + readonly "callback_url"?: string + readonly "duration"?: number + readonly "frame_images"?: ReadonlyArray + readonly "generate_audio"?: boolean + readonly "input_references"?: ReadonlyArray + readonly "model": string + readonly "prompt"?: string + readonly "provider"?: { readonly "options"?: ProviderOptions } + readonly "resolution"?: "480p" | "720p" | "1080p" | "1K" | "2K" | "4K" + readonly "seed"?: number + readonly "size"?: string +} +export const VideoGenerationRequest = Schema.Struct({ + "aspect_ratio": Schema.optionalKey( + Schema.Literals(["16:9", "9:16", "1:1", "4:3", "3:4", "3:2", "2:3", "21:9", "9:21"]).annotate({ + "description": "Aspect ratio of the generated video" + }) + ), + "callback_url": Schema.optionalKey( + Schema.String.annotate({ + "description": + "URL to receive a webhook notification when the video generation job completes. Overrides the workspace-level default callback URL if set. Must be HTTPS.", + "format": "uri" + }) + ), + "duration": Schema.optionalKey( + Schema.Number.annotate({ "description": "Duration of the generated video in seconds" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })) + ), + "frame_images": Schema.optionalKey( + Schema.Array(FrameImage).annotate({ + "description": + "Images to use as the first and/or last frame of the generated video. Each image must specify a frame_type of first_frame or last_frame." + }) + ), + "generate_audio": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Whether to generate audio alongside the video. Defaults to the endpoint's generate_audio capability flag, false if not set." + }) + ), + "input_references": Schema.optionalKey( + Schema.Array(InputReference).annotate({ + "description": + "Reference assets to guide video generation. Accepts image, audio, and video references. Audio and video references are only honored by providers that support them (currently BytePlus Seedance 2.0); other providers use image references and ignore the rest." + }) + ), + "model": Schema.String, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Text prompt describing the video to generate. Optional for models that support generating a video from image input alone; required by all other models." + }) + ), + "provider": Schema.optionalKey( + Schema.Struct({ "options": Schema.optionalKey(ProviderOptions) }).annotate({ + "description": "Provider-specific passthrough configuration" + }) + ), + "resolution": Schema.optionalKey( + Schema.Literals(["480p", "720p", "1080p", "1K", "2K", "4K"]).annotate({ + "description": "Resolution of the generated video" + }) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "If specified, the generation will sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed for all providers." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "size": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Exact pixel dimensions of the generated video in \"WIDTHxHEIGHT\" format (e.g. \"1280x720\"). Interchangeable with resolution + aspect_ratio." + }) + ) +}).annotate({ "identifier": "VideoGenerationRequest" }) +export type ImageGenerationProviderPreferences = { + readonly "allow_fallbacks"?: boolean | null + readonly "ignore"?: Union_2 + readonly "only"?: Union_3 + readonly "options"?: ProviderOptions + readonly "order"?: Union_4 + readonly "sort"?: Union_5 +} +export const ImageGenerationProviderPreferences = Schema.Struct({ + "allow_fallbacks": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to allow backup providers to serve requests\n- true: (default) when the primary provider (or your custom providers in \"order\") is unavailable, use the next best provider.\n- false: use only the primary/custom provider, and return the upstream error if it's unavailable.\n" + }) + ), + "ignore": Schema.optionalKey(Union_2), + "only": Schema.optionalKey(Union_3), + "options": Schema.optionalKey(ProviderOptions), + "order": Schema.optionalKey(Union_4), + "sort": Schema.optionalKey(Union_5) +}).annotate({ + "description": "Provider routing preferences and provider-specific passthrough configuration.", + "identifier": "ImageGenerationProviderPreferences" +}) +export type ListEndpointsResponse = { + readonly "architecture": { + readonly "input_modalities": ReadonlyArray<"text" | "image" | "file" | "audio" | "video"> + readonly "instruct_type": + | "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" + | null + readonly "modality": string | null + readonly "output_modalities": ReadonlyArray< + "text" | "image" | "embeddings" | "audio" | "video" | "rerank" | "speech" | "transcription" + > + readonly "tokenizer": + | "Router" + | "Media" + | "Other" + | "GPT" + | "Claude" + | "Gemini" + | "Gemma" + | "Grok" + | "Cohere" + | "Nova" + | "Qwen" + | "Yi" + | "DeepSeek" + | "Mistral" + | "Llama2" + | "Llama3" + | "Llama4" + | "PaLM" + | "RWKV" + | "Qwen3" + } + readonly "created": number + readonly "description": string + readonly "endpoints": ReadonlyArray + readonly "id": string + readonly "name": string +} +export const ListEndpointsResponse = Schema.Struct({ + "architecture": Schema.Struct({ + "input_modalities": Schema.Array( + Schema.Union([ + Schema.Literal("text"), + Schema.Literal("image"), + Schema.Literal("file"), + Schema.Literal("audio"), + Schema.Literal("video") + ]) + ).annotate({ "description": "Supported input modalities" }), + "instruct_type": Schema.Union([ + Schema.Literal("none").annotate({ "description": "Instruction format type" }), + Schema.Literal("airoboros").annotate({ "description": "Instruction format type" }), + Schema.Literal("alpaca").annotate({ "description": "Instruction format type" }), + Schema.Literal("alpaca-modif").annotate({ "description": "Instruction format type" }), + Schema.Literal("chatml").annotate({ "description": "Instruction format type" }), + Schema.Literal("claude").annotate({ "description": "Instruction format type" }), + Schema.Literal("code-llama").annotate({ "description": "Instruction format type" }), + Schema.Literal("gemma").annotate({ "description": "Instruction format type" }), + Schema.Literal("llama2").annotate({ "description": "Instruction format type" }), + Schema.Literal("llama3").annotate({ "description": "Instruction format type" }), + Schema.Literal("mistral").annotate({ "description": "Instruction format type" }), + Schema.Literal("nemotron").annotate({ "description": "Instruction format type" }), + Schema.Literal("neural").annotate({ "description": "Instruction format type" }), + Schema.Literal("openchat").annotate({ "description": "Instruction format type" }), + Schema.Literal("phi3").annotate({ "description": "Instruction format type" }), + Schema.Literal("rwkv").annotate({ "description": "Instruction format type" }), + Schema.Literal("vicuna").annotate({ "description": "Instruction format type" }), + Schema.Literal("zephyr").annotate({ "description": "Instruction format type" }), + Schema.Literal("deepseek-r1").annotate({ "description": "Instruction format type" }), + Schema.Literal("deepseek-v3.1").annotate({ "description": "Instruction format type" }), + Schema.Literal("qwq").annotate({ "description": "Instruction format type" }), + Schema.Literal("qwen3").annotate({ "description": "Instruction format type" }), + Schema.Union([Schema.Null]).annotate({ "description": "Instruction format type" }) + ]).annotate({ "description": "Instruction format type" }), + "modality": Schema.Union([ + Schema.Union([Schema.String]).annotate({ "description": "Primary modality of the model" }), + Schema.Union([Schema.Null]).annotate({ "description": "Primary modality of the model" }) + ]).annotate({ "description": "Primary modality of the model" }), + "output_modalities": Schema.Array( + Schema.Union([ + Schema.Literal("text"), + Schema.Literal("image"), + Schema.Literal("embeddings"), + Schema.Literal("audio"), + Schema.Literal("video"), + Schema.Literal("rerank"), + Schema.Literal("speech"), + Schema.Literal("transcription") + ]) + ).annotate({ "description": "Supported output modalities" }), + "tokenizer": Schema.Union([ + Schema.Union([Schema.Literal("Router").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Media").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Other").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("GPT").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Claude").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Gemini").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Gemma").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Grok").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Cohere").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Nova").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Qwen").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Yi").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("DeepSeek").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Mistral").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Llama2").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Llama3").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Llama4").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("PaLM").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("RWKV").annotate({ "description": "Tokenizer type used by the model" })]), + Schema.Union([Schema.Literal("Qwen3").annotate({ "description": "Tokenizer type used by the model" })]) + ]).annotate({ "description": "Tokenizer type used by the model" }) + }).annotate({ "description": "Model architecture information" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "description": Schema.String.annotate({ "description": "Description of the model" }), + "endpoints": Schema.Array(PublicEndpoint).annotate({ "description": "List of available endpoints for this model" }), + "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), + "name": Schema.String.annotate({ "description": "Display name of the model" }) +}).annotate({ "description": "List of available endpoints for a model", "identifier": "ListEndpointsResponse" }) +export type ModelReasoning = { + readonly "default_effort"?: ReasoningEffort + readonly "default_enabled"?: boolean + readonly "mandatory": boolean + readonly "supported_efforts"?: Union_12 + readonly "supports_max_tokens"?: boolean +} +export const ModelReasoning = Schema.Struct({ + "default_effort": Schema.optionalKey( + Schema.suspend((): Schema.Codec => ReasoningEffort).annotate({ + "description": + "Default reasoning effort when the client enables reasoning without specifying effort. Maps to `reasoning.effort` in chat requests. When `\"none\"`, prefer omitting effort unless the user explicitly disables reasoning." + }) + ), + "default_enabled": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Default reasoning enabled state when the client does not set `reasoning.enabled`." + }) + ), + "mandatory": Schema.Boolean.annotate({ + "description": "When true, reasoning cannot be disabled and effort \"none\" is rejected." + }), + "supported_efforts": Schema.optionalKey(Union_12), + "supports_max_tokens": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "Present and `true` when the model accepts `reasoning.max_tokens` in requests (Anthropic-style) instead of or in addition to `reasoning.effort`. Omitted otherwise." + }) + ) +}).annotate({ + "description": "Reasoning effort configuration. Omitted for non-reasoning models and dynamic router models.", + "identifier": "ModelReasoning" +}) +export type ReasoningDetailUnion = + | ReasoningDetailSummary + | ReasoningDetailEncrypted + | ReasoningDetailText + | ReasoningDetailServerToolCall +export const ReasoningDetailUnion = Schema.Union([ + ReasoningDetailSummary, + ReasoningDetailEncrypted, + ReasoningDetailText, + ReasoningDetailServerToolCall +], { mode: "oneOf" }).annotate({ "description": "Reasoning detail union schema", "identifier": "ReasoningDetailUnion" }) +export type BaseReasoningConfig = Objects_5 | null +export const BaseReasoningConfig = Schema.Union([Objects_5, Schema.Null]).annotate({ + "identifier": "BaseReasoningConfig" +}) +export type ReasoningConfig = Union_13 | null +export const ReasoningConfig = Schema.Union([Union_13, Schema.Null]).annotate({ + "description": "Configuration for reasoning mode in the response", + "identifier": "ReasoningConfig" +}) +export type ChatUsage = { + readonly "completion_tokens": number + readonly "completion_tokens_details"?: Union_ + readonly "cost"?: number | null + readonly "cost_details"?: CostDetails + readonly "is_byok"?: boolean + readonly "prompt_tokens": number + readonly "prompt_tokens_details"?: Union_1 + readonly "server_tool_use_details"?: ServerToolUseDetails + readonly "total_tokens": number +} +export const ChatUsage = Schema.Struct({ + "completion_tokens": Schema.Number.annotate({ "description": "Number of tokens in the completion" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "completion_tokens_details": Schema.optionalKey(Union_), + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Cost of the completion", "format": "double" }) + ), + "cost_details": Schema.optionalKey(CostDetails), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether a request was made using a Bring Your Own Key configuration" }) + ), + "prompt_tokens": Schema.Number.annotate({ "description": "Number of tokens in the prompt" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "prompt_tokens_details": Schema.optionalKey(Union_1), + "server_tool_use_details": Schema.optionalKey(ServerToolUseDetails), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "description": "Token usage statistics", "identifier": "ChatUsage" }) +export type Usage = { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cache_write_tokens"?: number | null; readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number + readonly "cost"?: number | null + readonly "cost_details"?: { + readonly "upstream_inference_cost"?: number | null + readonly "upstream_inference_input_cost": number + readonly "upstream_inference_output_cost": number + } + readonly "is_byok"?: boolean + readonly "server_tool_use_details"?: ServerToolUseDetails +} | null +export const Usage = Schema.Union([ + Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "input_tokens_details": Schema.Struct({ + "cache_write_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "cached_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": Schema.Struct({ + "reasoning_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + "total_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Cost of the completion", "format": "double" }) + ), + "cost_details": Schema.optionalKey(Schema.Struct({ + "upstream_inference_cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "upstream_inference_input_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "upstream_inference_output_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + })), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether a request was made using a Bring Your Own Key configuration" }) + ), + "server_tool_use_details": Schema.optionalKey(ServerToolUseDetails) + }), + Schema.Null +]).annotate({ "description": "Token usage information for the response", "identifier": "Usage" }) +export type StopServerToolsWhen = ReadonlyArray +export const StopServerToolsWhen = Schema.Array(StopServerToolsWhenCondition).annotate({ + "description": + "Stop conditions for the server-tool agent loop. Any condition firing halts the loop (OR logic). When set, this overrides `max_tool_calls`. When a condition fires while the model is still emitting tool calls, the pending tool calls are executed and one final turn is made with tool calls disabled so the response ends with a natural-language answer instead of an unfinished tool call." +}).check( + Schema.isMinLength(1).annotate({ + "expected": "a value with a length of at least 1", + "identifier": "StopServerToolsWhen" + }) +) +export type SubagentServerToolConfig = { + readonly "instructions"?: string + readonly "max_completion_tokens"?: number + readonly "max_tool_calls"?: number + readonly "model"?: string + readonly "name"?: string + readonly "reasoning"?: SubagentReasoning + readonly "temperature"?: number + readonly "tools"?: Arrays_12 +} +export const SubagentServerToolConfig = Schema.Struct({ + "instructions": Schema.optionalKey( + Schema.String.annotate({ + "description": + "System instructions for the subagent. When omitted, the subagent responds with no system prompt of its own." + }) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of output tokens (including reasoning) the subagent may produce. When omitted, the provider's default applies." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of tool-calling steps the subagent may take during its agentic loop. Capped at 25. Only relevant when the subagent is given tools. Accepted and validated but not yet enforced on the subagent call." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(25).annotate({ "expected": "a value less than or equal to 25" })) + ), + "model": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Slug of the model that executes delegated tasks (any OpenRouter model). Typically a smaller, cheaper, faster model than the one delegating. When omitted, the model from the outer API request is used. The subagent tool itself cannot be the subagent model." + }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Optional name for this subagent. The model sees one tool per named subagent (and one default for an unnamed entry). Names must be unique across subagent entries. Letters, digits, spaces, underscores, and dashes; trimmed; 1–64 chars." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" }) + ).check( + Schema.isPattern(new RegExp("^[a-zA-Z0-9 _-]+$")).annotate({ + "expected": "a string matching the RegExp ^[a-zA-Z0-9 _-]+$" + }) + ) + ), + "reasoning": Schema.optionalKey(SubagentReasoning), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Sampling temperature forwarded to the subagent call. When omitted, the provider's default applies.", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "tools": Schema.optionalKey(Arrays_12) +}).annotate({ + "description": "Configuration for one openrouter:subagent server tool entry.", + "identifier": "SubagentServerToolConfig" +}) +export type ImageModelEndpointsResponse = { readonly "endpoints": ReadonlyArray; readonly "id": string } +export const ImageModelEndpointsResponse = Schema.Struct({ + "endpoints": Schema.Array(ImageEndpoint), + "id": Schema.String.annotate({ "description": "Model slug" }) +}).annotate({ + "description": "The full per-endpoint records for an image model.", + "identifier": "ImageModelEndpointsResponse" +}) +export type TaskClassificationResponse = { + readonly "data": { + readonly "as_of": string + readonly "classifications": ReadonlyArray + readonly "macro_categories": ReadonlyArray + readonly "window_days": number + } +} +export const TaskClassificationResponse = Schema.Struct({ + "data": Schema.Struct({ + "as_of": Schema.String.annotate({ + "description": + "UTC date (YYYY-MM-DD) of the window upper bound (yesterday). Data is exclusive of the current incomplete UTC day. This is the expected latest date in the snapshot; it does not confirm data presence for that date." + }), + "classifications": Schema.Array(TaskClassificationItem).annotate({ + "description": "Per-task classification market-share data, sorted by usage_share descending." + }), + "macro_categories": Schema.Array(TaskClassificationMacroCategory).annotate({ + "description": "Aggregate market-share data per macro-category (code, data, agent, general)." + }), + "window_days": Schema.Number.annotate({ "description": "Number of trailing days covered by this snapshot." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + }) +}).annotate({ "identifier": "TaskClassificationResponse" }) +export type UnifiedBenchmarksResponse = { + readonly "data": ReadonlyArray + readonly "meta": UnifiedBenchmarksMeta +} +export const UnifiedBenchmarksResponse = Schema.Struct({ + "data": Schema.Array(Schema.Union([UnifiedBenchmarksAAItem, UnifiedBenchmarksDAItem], { mode: "oneOf" })), + "meta": UnifiedBenchmarksMeta +}).annotate({ "identifier": "UnifiedBenchmarksResponse" }) +export type AnnotationAddedEvent = { + readonly "annotation": OpenAIResponsesAnnotation + readonly "annotation_index": number + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_text.annotation.added" +} +export const AnnotationAddedEvent = Schema.Struct({ + "annotation": OpenAIResponsesAnnotation, + "annotation_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_text.annotation.added") +}).annotate({ + "description": "Event emitted when a text annotation is added to output", + "identifier": "AnnotationAddedEvent" +}) +export type BaseAnnotationAddedEvent = { + readonly "annotation": OpenAIResponsesAnnotation + readonly "annotation_index": number + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_text.annotation.added" +} +export const BaseAnnotationAddedEvent = Schema.Struct({ + "annotation": OpenAIResponsesAnnotation, + "annotation_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_text.annotation.added") +}).annotate({ + "description": "Event emitted when a text annotation is added to output", + "identifier": "BaseAnnotationAddedEvent" +}) +export type ResponseOutputText = { + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": "output_text" +} +export const ResponseOutputText = Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Literal("output_text") +}).annotate({ "identifier": "ResponseOutputText" }) +export type WebFetchServerTool = { + readonly "parameters"?: WebFetchServerToolConfig + readonly "type": "openrouter:web_fetch" +} +export const WebFetchServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(WebFetchServerToolConfig), + "type": Schema.Literal("openrouter:web_fetch") +}).annotate({ + "description": "OpenRouter built-in server tool: fetches full content from a URL (web page or PDF)", + "identifier": "WebFetchServerTool" +}) +export type Preview_20250311_WebSearchServerTool = { + readonly "engine"?: WebSearchEngineEnum + readonly "filters"?: WebSearchDomainFilter + readonly "max_results"?: number + readonly "search_context_size"?: SearchContextSizeEnum + readonly "type": "web_search_preview_2025_03_11" + readonly "user_location"?: Preview_WebSearchUserLocation +} +export const Preview_20250311_WebSearchServerTool = Schema.Struct({ + "engine": Schema.optionalKey(WebSearchEngineEnum), + "filters": Schema.optionalKey(WebSearchDomainFilter), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchContextSizeEnum), + "type": Schema.Literal("web_search_preview_2025_03_11"), + "user_location": Schema.optionalKey(Preview_WebSearchUserLocation) +}).annotate({ + "description": "Web search preview tool configuration (2025-03-11 version)", + "identifier": "Preview_20250311_WebSearchServerTool" +}) +export type Preview_WebSearchServerTool = { + readonly "engine"?: WebSearchEngineEnum + readonly "filters"?: WebSearchDomainFilter + readonly "max_results"?: number + readonly "search_context_size"?: SearchContextSizeEnum + readonly "type": "web_search_preview" + readonly "user_location"?: Preview_WebSearchUserLocation +} +export const Preview_WebSearchServerTool = Schema.Struct({ + "engine": Schema.optionalKey(WebSearchEngineEnum), + "filters": Schema.optionalKey(WebSearchDomainFilter), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchContextSizeEnum), + "type": Schema.Literal("web_search_preview"), + "user_location": Schema.optionalKey(Preview_WebSearchUserLocation) +}).annotate({ "description": "Web search preview tool configuration", "identifier": "Preview_WebSearchServerTool" }) +export type Legacy_WebSearchServerTool = { + readonly "engine"?: WebSearchEngineEnum + readonly "filters"?: WebSearchDomainFilter + readonly "max_results"?: number + readonly "search_context_size"?: SearchContextSizeEnum + readonly "type": "web_search" + readonly "user_location"?: WebSearchUserLocation +} +export const Legacy_WebSearchServerTool = Schema.Struct({ + "engine": Schema.optionalKey(WebSearchEngineEnum), + "filters": Schema.optionalKey(WebSearchDomainFilter), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchContextSizeEnum), + "type": Schema.Literal("web_search"), + "user_location": Schema.optionalKey(WebSearchUserLocation) +}).annotate({ "description": "Web search tool configuration", "identifier": "Legacy_WebSearchServerTool" }) +export type WebSearchServerTool = { + readonly "engine"?: WebSearchEngineEnum + readonly "filters"?: WebSearchDomainFilter + readonly "max_results"?: number + readonly "search_context_size"?: SearchContextSizeEnum + readonly "type": "web_search_2025_08_26" + readonly "user_location"?: WebSearchUserLocation +} +export const WebSearchServerTool = Schema.Struct({ + "engine": Schema.optionalKey(WebSearchEngineEnum), + "filters": Schema.optionalKey(WebSearchDomainFilter), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "search_context_size": Schema.optionalKey(SearchContextSizeEnum), + "type": Schema.Literal("web_search_2025_08_26"), + "user_location": Schema.optionalKey(WebSearchUserLocation) +}).annotate({ + "description": "Web search tool configuration (2025-08-26 version)", + "identifier": "WebSearchServerTool" +}) +export type ChatWebSearchShorthand = { + readonly "allowed_domains"?: ReadonlyArray + readonly "engine"?: WebSearchEngineEnum + readonly "excluded_domains"?: ReadonlyArray + readonly "max_characters"?: number + readonly "max_results"?: number + readonly "max_total_results"?: number + readonly "parameters"?: WebSearchConfig + readonly "search_context_size"?: SearchQualityLevel + readonly "type": "web_search" | "web_search_preview" | "web_search_preview_2025_03_11" | "web_search_2025_08_26" + readonly "user_location"?: WebSearchUserLocationServerTool +} +export const ChatWebSearchShorthand = Schema.Struct({ + "allowed_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Limit search results to these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, and most native providers (Anthropic, OpenAI, xAI). Cannot be used with excluded_domains." + }) + ), + "engine": Schema.optionalKey(WebSearchEngineEnum), + "excluded_domains": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Exclude search results from these domains. Supported by Exa, Firecrawl, Parallel, Perplexity, Anthropic, and xAI. Not supported with OpenAI (silently ignored). Cannot be used with allowed_domains." + }) + ), + "max_characters": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Exact maximum number of characters of content per search result. Applies to the Exa, Parallel, and Perplexity engines; ignored with native provider search and Firecrawl. For Exa, caps highlight content per result. For Parallel, caps excerpt content per result (default 1,500 when omitted). For Perplexity, maps to the native `max_tokens_per_page` parameter (converted from characters to tokens) and trims the response to the exact character cap. When both `max_characters` and `search_context_size` are set, `max_characters` takes precedence. When omitted, falls back to `search_context_size` mapping (Exa) or engine defaults (Parallel, Perplexity)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of search results to return per search call. Defaults to 5. Applies to Exa, Firecrawl, Parallel, and Perplexity engines; ignored with native provider search. Perplexity supports a maximum of 20; values above 20 are clamped." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "max_total_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum total number of search results across all search calls in a single request. Once this limit is reached, the tool will stop returning new results. Useful for controlling cost and context size in agentic loops. Defaults to 50 when not specified." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "parameters": Schema.optionalKey(WebSearchConfig), + "search_context_size": Schema.optionalKey(SearchQualityLevel), + "type": Schema.Literals([ + "web_search", + "web_search_preview", + "web_search_preview_2025_03_11", + "web_search_2025_08_26" + ]), + "user_location": Schema.optionalKey(WebSearchUserLocationServerTool) +}).annotate({ + "description": "Web search tool using OpenAI Responses API syntax. Automatically converted to openrouter:web_search.", + "identifier": "ChatWebSearchShorthand" +}) +export type OpenRouterWebSearchServerTool = { + readonly "parameters"?: WebSearchConfig + readonly "type": "openrouter:web_search" +} +export const OpenRouterWebSearchServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(WebSearchConfig), + "type": Schema.Literal("openrouter:web_search") +}).annotate({ + "description": "OpenRouter built-in server tool: searches the web for current information", + "identifier": "OpenRouterWebSearchServerTool" +}) +export type WebSearchServerTool_OpenRouter = { + readonly "parameters"?: WebSearchServerToolConfig + readonly "type": "openrouter:web_search" +} +export const WebSearchServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(WebSearchServerToolConfig), + "type": Schema.Literal("openrouter:web_search") +}).annotate({ + "description": "OpenRouter built-in server tool: searches the web for current information", + "identifier": "WebSearchServerTool_OpenRouter" +}) +export type AdvisorServerTool_OpenRouter = { + readonly "parameters"?: AdvisorServerToolConfig + readonly "type": "openrouter:advisor" +} +export const AdvisorServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(AdvisorServerToolConfig), + "type": Schema.Literal("openrouter:advisor") +}).annotate({ + "description": + "OpenRouter built-in server tool: consults a higher-intelligence advisor model (any OpenRouter model) for guidance mid-generation and returns its response. The advisor may run as a sub-agent with its own tools. Include multiple entries to offer several named advisors; at most one entry may omit `name` to act as the default advisor.", + "identifier": "AdvisorServerTool_OpenRouter" +}) +export type AnthropicBashCodeExecutionToolResult = { + readonly "content": AnthropicBashCodeExecutionContent + readonly "tool_use_id": string + readonly "type": "bash_code_execution_tool_result" +} +export const AnthropicBashCodeExecutionToolResult = Schema.Struct({ + "content": AnthropicBashCodeExecutionContent, + "tool_use_id": Schema.String, + "type": Schema.Literal("bash_code_execution_tool_result") +}).annotate({ "identifier": "AnthropicBashCodeExecutionToolResult" }) +export type AnthropicSearchResultBlockParam = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "citations"?: { readonly "enabled"?: boolean } + readonly "content": ReadonlyArray + readonly "source": string + readonly "title": string + readonly "type": "search_result" +} +export const AnthropicSearchResultBlockParam = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "citations": Schema.optionalKey(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) })), + "content": Schema.Array(AnthropicTextBlockParam), + "source": Schema.String, + "title": Schema.String, + "type": Schema.Literal("search_result") +}).annotate({ "identifier": "AnthropicSearchResultBlockParam" }) +export type FusionServerTool_OpenRouter = { + readonly "parameters"?: FusionServerToolConfig + readonly "type": "openrouter:fusion" +} +export const FusionServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(FusionServerToolConfig), + "type": Schema.Literal("openrouter:fusion") +}).annotate({ + "description": + "OpenRouter built-in server tool: fans out the user prompt to a panel of analysis models, then asks a judge model to summarize their collective output as structured JSON the outer model can synthesize from.", + "identifier": "FusionServerTool_OpenRouter" +}) +export type AnthropicWebFetchBlock = { + readonly "content": AnthropicDocumentBlock + readonly "retrieved_at": string | null + readonly "type": "web_fetch_result" + readonly "url": string +} +export const AnthropicWebFetchBlock = Schema.Struct({ + "content": AnthropicDocumentBlock, + "retrieved_at": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("web_fetch_result"), + "url": Schema.String +}).annotate({ "identifier": "AnthropicWebFetchBlock" }) +export type AnthropicDocumentBlockParam = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "citations"?: { readonly "enabled"?: boolean; readonly [x: string]: Schema.Json } | null + readonly "context"?: string | null + readonly "source": + | AnthropicBase64PdfSource + | AnthropicPlainTextSource + | { + readonly "content": string | ReadonlyArray + readonly "type": "content" + } + | AnthropicUrlPdfSource + | AnthropicFileDocumentSource + readonly "title"?: string | null + readonly "type": "document" +} +export const AnthropicDocumentBlockParam = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "citations": Schema.optionalKey( + Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "enabled": Schema.optionalKey(Schema.Boolean) }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]), + Schema.Null + ]) + ), + "context": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "source": Schema.Union([ + AnthropicBase64PdfSource, + AnthropicPlainTextSource, + Schema.Struct({ + "content": Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([AnthropicTextBlockParam, AnthropicImageBlockParam], { mode: "oneOf" })) + ]), + "type": Schema.Literal("content") + }), + AnthropicUrlPdfSource, + AnthropicFileDocumentSource + ], { mode: "oneOf" }), + "title": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("document") +}).annotate({ "identifier": "AnthropicDocumentBlockParam" }) +export type AnthropicUsageIteration = + | AnthropicCompactionUsageIteration + | AnthropicMessageUsageIteration + | AnthropicAdvisorMessageUsageIteration + | AnthropicUnknownUsageIteration +export const AnthropicUsageIteration = Schema.Union([ + AnthropicCompactionUsageIteration, + AnthropicMessageUsageIteration, + AnthropicAdvisorMessageUsageIteration, + AnthropicUnknownUsageIteration +]).annotate({ "identifier": "AnthropicUsageIteration" }) +export type AnthropicCodeExecutionToolResult = { + readonly "content": AnthropicCodeExecutionContent + readonly "tool_use_id": string + readonly "type": "code_execution_tool_result" +} +export const AnthropicCodeExecutionToolResult = Schema.Struct({ + "content": AnthropicCodeExecutionContent, + "tool_use_id": Schema.String, + "type": Schema.Literal("code_execution_tool_result") +}).annotate({ "identifier": "AnthropicCodeExecutionToolResult" }) +export type AnthropicToolSearchToolResult = { + readonly "content": AnthropicToolSearchContent + readonly "tool_use_id": string + readonly "type": "tool_search_tool_result" +} +export const AnthropicToolSearchToolResult = Schema.Struct({ + "content": AnthropicToolSearchContent, + "tool_use_id": Schema.String, + "type": Schema.Literal("tool_search_tool_result") +}).annotate({ "identifier": "AnthropicToolSearchToolResult" }) +export type BashServerTool = { readonly "parameters"?: BashServerToolConfig; readonly "type": "openrouter:bash" } +export const BashServerTool = Schema.Struct({ + "parameters": Schema.optionalKey(BashServerToolConfig), + "type": Schema.Literal("openrouter:bash") +}).annotate({ + "description": "OpenRouter built-in server tool: runs shell commands server-side in a sandboxed container", + "identifier": "BashServerTool" +}) +export type ShellServerTool_OpenRouter = { + readonly "parameters"?: ShellServerToolConfig + readonly "type": "openrouter:shell" +} +export const ShellServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(ShellServerToolConfig), + "type": Schema.Literal("openrouter:shell") +}).annotate({ + "description": + "OpenRouter built-in server tool: runs shell commands server-side in a sandboxed container (a sandbox-backed clone of OpenAI's hosted shell tool)", + "identifier": "ShellServerTool_OpenRouter" +}) +export type ImageModelsListResponse = { readonly "data": ReadonlyArray } +export const ImageModelsListResponse = Schema.Struct({ "data": Schema.Array(ImageModelListItem) }).annotate({ + "description": "List of image generation models.", + "identifier": "ImageModelsListResponse" +}) +export type ObservabilityDestination = + | ObservabilityArizeDestination + | ObservabilityBraintrustDestination + | ObservabilityClickhouseDestination + | ObservabilityDatadogDestination + | ObservabilityGrafanaDestination + | ObservabilityLangfuseDestination + | ObservabilityLangsmithDestination + | ObservabilityNewrelicDestination + | ObservabilityOpikDestination + | ObservabilityOtelCollectorDestination + | ObservabilityPosthogDestination + | ObservabilityRampDestination + | ObservabilityS3Destination + | ObservabilitySentryDestination + | ObservabilitySnowflakeDestination + | ObservabilityWeaveDestination + | ObservabilityWebhookDestination +export const ObservabilityDestination = Schema.Union([ + ObservabilityArizeDestination, + ObservabilityBraintrustDestination, + ObservabilityClickhouseDestination, + ObservabilityDatadogDestination, + ObservabilityGrafanaDestination, + ObservabilityLangfuseDestination, + ObservabilityLangsmithDestination, + ObservabilityNewrelicDestination, + ObservabilityOpikDestination, + ObservabilityOtelCollectorDestination, + ObservabilityPosthogDestination, + ObservabilityRampDestination, + ObservabilityS3Destination, + ObservabilitySentryDestination, + ObservabilitySnowflakeDestination, + ObservabilityWeaveDestination, + ObservabilityWebhookDestination +], { mode: "oneOf" }).annotate({ "identifier": "ObservabilityDestination" }) +export type CreateObservabilityDestinationRequest = { + readonly "api_key_hashes"?: ReadonlyArray | null + readonly "config": {} + readonly "enabled"?: boolean + readonly "filter_rules"?: ObservabilityFilterRulesConfigNullable + readonly "name": string + readonly "privacy_mode"?: boolean + readonly "sampling_rate"?: number + readonly "type": + | "arize" + | "braintrust" + | "clickhouse" + | "datadog" + | "grafana" + | "langfuse" + | "langsmith" + | "newrelic" + | "opik" + | "otel-collector" + | "posthog" + | "ramp" + | "s3" + | "sentry" + | "snowflake" + | "weave" + | "webhook" + readonly "workspace_id"?: string +} +export const CreateObservabilityDestinationRequest = Schema.Struct({ + "api_key_hashes": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes whose traffic is forwarded. `null` or omitted means all keys. Must contain at least one hash if provided." + }) + ), + "config": Schema.Struct({}).annotate({ + "description": "Provider-specific configuration. The shape depends on `type` and is validated server-side." + }), + "enabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether this destination should be enabled immediately." }) + ), + "filter_rules": Schema.optionalKey(ObservabilityFilterRulesConfigNullable), + "name": Schema.String.annotate({ "description": "Human-readable name for the destination." }), + "privacy_mode": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "When true, request/response bodies are not forwarded — only metadata." }) + ), + "sampling_rate": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sampling rate between 0.0001 and 1 (1 = 100%).", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "type": Schema.Literals([ + "arize", + "braintrust", + "clickhouse", + "datadog", + "grafana", + "langfuse", + "langsmith", + "newrelic", + "opik", + "otel-collector", + "posthog", + "ramp", + "s3", + "sentry", + "snowflake", + "weave", + "webhook" + ]).annotate({ "description": "The destination type. Only stable destination types are accepted." }), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional workspace ID. Defaults to the authenticated entity's default workspace.", + "format": "uuid" + }) + ) +}).annotate({ "identifier": "CreateObservabilityDestinationRequest" }) +export type UpdateObservabilityDestinationRequest = { + readonly "api_key_hashes"?: ReadonlyArray | null + readonly "config"?: {} + readonly "enabled"?: boolean + readonly "filter_rules"?: ObservabilityFilterRulesConfigNullable + readonly "name"?: string + readonly "privacy_mode"?: boolean + readonly "sampling_rate"?: number +} +export const UpdateObservabilityDestinationRequest = Schema.Struct({ + "api_key_hashes": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.String).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + Schema.Null + ]).annotate({ + "description": + "Optional allowlist of OpenRouter API key hashes. `null` clears the filter (all keys). Omitting leaves the current value. Must contain at least one hash if provided." + }) + ), + "config": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": + "Provider-specific configuration fields to update. Masked values are ignored; unset fields keep their current value." + }) + ), + "enabled": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Whether the destination is enabled." })), + "filter_rules": Schema.optionalKey( + Schema.suspend((): Schema.Codec => ObservabilityFilterRulesConfigNullable) + .annotate({ + "description": "Optional structured filter rules. `null` clears the rules. Omitting keeps the current value." + }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Human-readable name for the destination." })), + "privacy_mode": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "When true, request/response bodies are not forwarded — only metadata." }) + ), + "sampling_rate": Schema.optionalKey( + Schema.Number.annotate({ "description": "Sampling rate between 0.0001 and 1 (1 = 100%).", "format": "double" }) + .check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ) +}).annotate({ "identifier": "UpdateObservabilityDestinationRequest" }) +export type ProviderPreferences = Objects_148 | null +export const ProviderPreferences = Schema.Union([Objects_148, Schema.Null]).annotate({ + "description": "When multiple model providers are available, optionally indicate your routing preference.", + "identifier": "ProviderPreferences" +}) +export type OpenRouterMetadata = { + readonly "attempt": number + readonly "attempts"?: Arrays_10 + readonly "endpoints": EndpointsMetadata + readonly "is_byok": boolean + readonly "params"?: RouterParams + readonly "pipeline"?: Arrays_11 + readonly "region": string | null + readonly "requested": string + readonly "strategy": RoutingStrategy + readonly "summary": string +} +export const OpenRouterMetadata = Schema.Struct({ + "attempt": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "attempts": Schema.optionalKey(Arrays_10), + "endpoints": EndpointsMetadata, + "is_byok": Schema.Boolean, + "params": Schema.optionalKey(RouterParams), + "pipeline": Schema.optionalKey(Arrays_11), + "region": Schema.Union([Schema.String, Schema.Null]), + "requested": Schema.String, + "strategy": RoutingStrategy, + "summary": Schema.String +}).annotate({ "identifier": "OpenRouterMetadata" }) +export type ChatContentItems = + | ChatContentText + | ChatContentImage + | ChatContentAudio + | Legacy_ChatContentVideo + | ChatContentVideo + | ChatContentFile +export const ChatContentItems = Schema.Union([ + ChatContentText, + ChatContentImage, + ChatContentAudio, + Legacy_ChatContentVideo, + ChatContentVideo, + ChatContentFile +], { mode: "oneOf" }).annotate({ + "description": "Content part for chat completion messages", + "identifier": "ChatContentItems" +}) +export type ChatDeveloperMessage = { + readonly "content": string | ReadonlyArray + readonly "name"?: string + readonly "role": "developer" +} +export const ChatDeveloperMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(ChatContentText)]).annotate({ + "description": "Developer message content" + }), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Optional name for the developer message" })), + "role": Schema.Literal("developer") +}).annotate({ "description": "Developer message", "identifier": "ChatDeveloperMessage" }) +export type ChatSystemMessage = { + readonly "content": string | ReadonlyArray + readonly "name"?: string + readonly "role": "system" +} +export const ChatSystemMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(ChatContentText)]).annotate({ + "description": "System message content" + }), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Optional name for the system message" })), + "role": Schema.Literal("system") +}).annotate({ "description": "System message for setting behavior", "identifier": "ChatSystemMessage" }) +export type AgentMessageItem = { + readonly "agent"?: { readonly "agent_name": string; readonly [x: string]: Schema.Json } | null + readonly "author": string + readonly "content": ReadonlyArray< + InputText | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" + } | { readonly "encrypted_content": string; readonly "type": "encrypted_content" } + > + readonly "id"?: string | null + readonly "recipient": string + readonly "type": "agent_message" +} +export const AgentMessageItem = Schema.Struct({ + "agent": Schema.optionalKey( + Schema.Union([ + Schema.StructWithRest(Schema.Struct({ "agent_name": Schema.String }), [ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })) + ]), + Schema.Null + ]) + ), + "author": Schema.String, + "content": Schema.Array( + Schema.Union([ + InputText, + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("input_image") + }).annotate({ "description": "Image input content item" }), + Schema.Struct({ "encrypted_content": Schema.String, "type": Schema.Literal("encrypted_content") }) + ], { mode: "oneOf" }) + ), + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "recipient": Schema.String, + "type": Schema.Literal("agent_message") +}).annotate({ + "description": "A message routed between agents in a multi-agent session", + "identifier": "AgentMessageItem" +}) +export type EasyInputMessage = { + readonly "content"?: + | ReadonlyArray< + | InputText + | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" + } + | InputFile + | InputAudio + | InputVideo + > + | string + | null + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "user" | "system" | "assistant" | "developer" + readonly "type"?: "message" +} +export const EasyInputMessage = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + InputText, + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("input_image") + }).annotate({ "description": "Image input content item" }), + InputFile, + InputAudio, + InputVideo + ], { mode: "oneOf" }) + ), + Schema.String, + Schema.Null + ]) + ), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literals(["user", "system", "assistant", "developer"]), + "type": Schema.optionalKey(Schema.Literal("message")) +}).annotate({ "identifier": "EasyInputMessage" }) +export type InputMessageItem = { + readonly "content"?: + | ReadonlyArray< + | InputText + | { + readonly "detail": "auto" | "high" | "low" | "original" + readonly "image_url"?: string | null + readonly "type": "input_image" + } + | InputFile + | InputAudio + | InputVideo + > + | null + readonly "id"?: string + readonly "role": "user" | "system" | "developer" + readonly "type"?: "message" +} +export const InputMessageItem = Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Union([ + InputText, + Schema.Struct({ + "detail": Schema.Literals(["auto", "high", "low", "original"]), + "image_url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "type": Schema.Literal("input_image") + }).annotate({ "description": "Image input content item" }), + InputFile, + InputAudio, + InputVideo + ], { mode: "oneOf" }) + ), + Schema.Null + ]) + ), + "id": Schema.optionalKey(Schema.String), + "role": Schema.Literals(["user", "system", "developer"]), + "type": Schema.optionalKey(Schema.Literal("message")) +}).annotate({ "identifier": "InputMessageItem" }) +export type OpenAIResponseCustomToolCallOutput = { + readonly "call_id": string + readonly "id"?: string + readonly "output": string | ReadonlyArray + readonly "type": "custom_tool_call_output" +} +export const OpenAIResponseCustomToolCallOutput = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "output": Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([InputText, InputImage, InputFile], { mode: "oneOf" })) + ]), + "type": Schema.Literal("custom_tool_call_output") +}).annotate({ "identifier": "OpenAIResponseCustomToolCallOutput" }) +export type OpenAIResponseFunctionToolCallOutput = { + readonly "call_id": string + readonly "id"?: string | null + readonly "output": string | ReadonlyArray + readonly "status"?: ToolCallStatus | null + readonly "type": "function_call_output" +} +export const OpenAIResponseFunctionToolCallOutput = Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "output": Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([InputText, InputImage, InputFile], { mode: "oneOf" })) + ]), + "status": Schema.optionalKey(Schema.Union([ToolCallStatus, Schema.Null])), + "type": Schema.Literal("function_call_output") +}).annotate({ "identifier": "OpenAIResponseFunctionToolCallOutput" }) +export type OpenAIResponseInputMessageItem = { + readonly "content": ReadonlyArray + readonly "id": string + readonly "role": "user" | "system" | "developer" + readonly "type"?: "message" +} +export const OpenAIResponseInputMessageItem = Schema.Struct({ + "content": Schema.Array(Schema.Union([InputText, InputImage, InputFile, InputAudio], { mode: "oneOf" })), + "id": Schema.String, + "role": Schema.Literals(["user", "system", "developer"]), + "type": Schema.optionalKey(Schema.Literal("message")) +}).annotate({ "identifier": "OpenAIResponseInputMessageItem" }) +export type Objects_151 = { + readonly "id": string + readonly "variables"?: { readonly [x: string]: string | InputText | InputImage | InputFile } | null + readonly [x: string]: Schema.Json +} +export const Objects_151 = Schema.StructWithRest( + Schema.Struct({ + "id": Schema.String, + "variables": Schema.optionalKey( + Schema.Union([ + Schema.Record(Schema.String, Schema.Union([Schema.String, InputText, InputImage, InputFile])), + Schema.Null + ]) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] +) +export type CreateGuardrailResponse = { readonly "data": Guardrail } +export const CreateGuardrailResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Guardrail).annotate({ "description": "The created guardrail" }) +}).annotate({ "identifier": "CreateGuardrailResponse" }) +export type GetGuardrailResponse = { readonly "data": Guardrail } +export const GetGuardrailResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Guardrail).annotate({ "description": "The guardrail" }) +}).annotate({ "identifier": "GetGuardrailResponse" }) +export type ListGuardrailsResponse = { readonly "data": ReadonlyArray; readonly "total_count": number } +export const ListGuardrailsResponse = Schema.Struct({ + "data": Schema.Array(Guardrail).annotate({ "description": "List of guardrails" }), + "total_count": Schema.Number.annotate({ "description": "Total number of guardrails" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "ListGuardrailsResponse" }) +export type UpdateGuardrailResponse = { readonly "data": Guardrail } +export const UpdateGuardrailResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => Guardrail).annotate({ "description": "The updated guardrail" }) +}).annotate({ "identifier": "UpdateGuardrailResponse" }) +export type ImageGenerationRequest = { + readonly "aspect_ratio"?: + | "1:1" + | "1:2" + | "1:4" + | "1:8" + | "2:1" + | "2:3" + | "3:2" + | "3:4" + | "4:1" + | "4:3" + | "4:5" + | "5:4" + | "8:1" + | "9:16" + | "16:9" + | "9:19.5" + | "19.5:9" + | "9:20" + | "20:9" + | "9:21" + | "21:9" + | "auto" + readonly "background"?: "auto" | "transparent" | "opaque" + readonly "input_references"?: ReadonlyArray + readonly "model": string + readonly "n"?: number + readonly "output_compression"?: number + readonly "output_format"?: "png" | "jpeg" | "webp" | "svg" + readonly "prompt": string + readonly "provider"?: ImageGenerationProviderPreferences + readonly "quality"?: "auto" | "low" | "medium" | "high" + readonly "resolution"?: "512" | "1K" | "2K" | "4K" + readonly "seed"?: number + readonly "size"?: string + readonly "stream"?: boolean +} +export const ImageGenerationRequest = Schema.Struct({ + "aspect_ratio": Schema.optionalKey( + Schema.Literals([ + "1:1", + "1:2", + "1:4", + "1:8", + "2:1", + "2:3", + "3:2", + "3:4", + "4:1", + "4:3", + "4:5", + "5:4", + "8:1", + "9:16", + "16:9", + "9:19.5", + "19.5:9", + "9:20", + "20:9", + "9:21", + "21:9", + "auto" + ]).annotate({ + "description": "Normalized aspect ratio of the generated image. Providers clamp to their supported subset." + }) + ), + "background": Schema.optionalKey( + Schema.Literals(["auto", "transparent", "opaque"]).annotate({ + "description": "Background treatment. `transparent` requires an output_format that supports alpha (png or webp)." + }) + ), + "input_references": Schema.optionalKey( + Schema.Array(ContentPartImage).annotate({ + "description": "Reference images to guide image-to-image generation, as base64 data URLs or HTTP(S) URLs." + }).check(Schema.isMaxLength(16).annotate({ "expected": "a value with a length of at most 16" })) + ), + "model": Schema.String.annotate({ "description": "The image generation model to use" }), + "n": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Number of images to generate (1-10). Providers that only support single-image generation reject n > 1." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_compression": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Compression level (0-100) for webp/jpeg output. Ignored for png and by providers without a compression knob." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_format": Schema.optionalKey( + Schema.Literals(["png", "jpeg", "webp", "svg"]).annotate({ + "description": + "Encoding of the returned image bytes. Most models produce raster formats (png, jpeg, webp). SVG is supported by vectorization models (e.g. Quiver) — the SVG markup is UTF-8 base64-encoded in `b64_json`." + }) + ), + "prompt": Schema.String.annotate({ "description": "Text description of the desired image" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "provider": Schema.optionalKey(ImageGenerationProviderPreferences), + "quality": Schema.optionalKey( + Schema.Literals(["auto", "low", "medium", "high"]).annotate({ + "description": "Rendering quality. Providers without a quality knob ignore this." + }) + ), + "resolution": Schema.optionalKey( + Schema.Literals(["512", "1K", "2K", "4K"]).annotate({ + "description": + "Normalized resolution tier of the generated image. Concrete pixel dimensions are derived per-provider." + }) + ), + "seed": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "If specified, the generation will sample deterministically, such that repeated requests with the same seed and parameters should return the same result. Determinism is not guaranteed for all providers." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "size": Schema.optionalKey(Schema.String.annotate({ + "description": + "Optional. A convenience shorthand for output dimensions — pass a tier (\"2K\", \"4K\") or explicit pixels (\"2048x2048\") and we normalize it to the right dimensions for the chosen provider. A tier size is equivalent to setting `resolution` and combines with `aspect_ratio`. An explicit pixel size is authoritative: a mismatched `resolution` or `aspect_ratio` alongside it is rejected with a 400." + })), + "stream": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "If true, partial images are streamed as SSE events as they become available. Only supported by providers with native streaming (currently OpenAI). Non-streaming providers ignore this flag and return a buffered response." + }) + ) +}).annotate({ "description": "Image generation request input", "identifier": "ImageGenerationRequest" }) +export type Model = { + readonly "architecture": ModelArchitecture + readonly "benchmarks"?: ModelBenchmarks + readonly "canonical_slug": string + readonly "context_length": number | null + readonly "created": number + readonly "default_parameters": DefaultParameters + readonly "description"?: string + readonly "expiration_date"?: string | null + readonly "hugging_face_id"?: string | null + readonly "id": string + readonly "knowledge_cutoff"?: string | null + readonly "links": ModelLinks + readonly "name": string + readonly "per_request_limits": PerRequestLimits + readonly "pricing": PublicPricing + readonly "reasoning"?: ModelReasoning + readonly "supported_parameters": ReadonlyArray + readonly "supported_voices": ReadonlyArray | null + readonly "top_provider": TopProviderInfo +} +export const Model = Schema.Struct({ + "architecture": ModelArchitecture, + "benchmarks": Schema.optionalKey(ModelBenchmarks), + "canonical_slug": Schema.String.annotate({ "description": "Canonical slug for the model" }), + "context_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]).annotate({ "description": "Maximum context length in tokens" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of when the model was created" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "default_parameters": DefaultParameters, + "description": Schema.optionalKey(Schema.String.annotate({ "description": "Description of the model" })), + "expiration_date": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The date after which the model may be removed. ISO 8601 date string (YYYY-MM-DD) or null if no expiration." + }) + ), + "hugging_face_id": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Hugging Face model identifier, if applicable" + }) + ), + "id": Schema.String.annotate({ "description": "Unique identifier for the model" }), + "knowledge_cutoff": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The date up to which the model was trained on data. ISO 8601 date string (YYYY-MM-DD) or null if unknown." + }) + ), + "links": ModelLinks, + "name": Schema.String.annotate({ "description": "Display name of the model" }), + "per_request_limits": PerRequestLimits, + "pricing": PublicPricing, + "reasoning": Schema.optionalKey(ModelReasoning), + "supported_parameters": Schema.Array(Parameter).annotate({ + "description": "List of supported parameters for this model" + }), + "supported_voices": Schema.Union([Schema.Array(Schema.String), Schema.Null]).annotate({ + "description": "List of supported voice identifiers for TTS models. Null for non-TTS models." + }), + "top_provider": TopProviderInfo +}).annotate({ "description": "Information about an AI model available on OpenRouter", "identifier": "Model" }) +export type ChatReasoningDetails = ReadonlyArray +export const ChatReasoningDetails = Schema.Array(ReasoningDetailUnion).annotate({ + "description": "Reasoning details for extended thinking models", + "identifier": "ChatReasoningDetails" +}) +export type ChatStreamReasoningDetails = ReadonlyArray +export const ChatStreamReasoningDetails = Schema.Array(ReasoningDetailUnion).annotate({ + "description": "Reasoning details for extended thinking models", + "identifier": "ChatStreamReasoningDetails" +}) +export type SubagentServerTool_OpenRouter = { + readonly "parameters"?: SubagentServerToolConfig + readonly "type": "openrouter:subagent" +} +export const SubagentServerTool_OpenRouter = Schema.Struct({ + "parameters": Schema.optionalKey(SubagentServerToolConfig), + "type": Schema.Literal("openrouter:subagent") +}).annotate({ + "description": + "OpenRouter built-in server tool: delegates self-contained tasks to a smaller, cheaper, faster worker model (any OpenRouter model) mid-generation and returns its outcome. The worker may run as a sub-agent with its own tools.", + "identifier": "SubagentServerTool_OpenRouter" +}) +export type BaseContentPartAddedEvent = { + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "sequence_number": number + readonly "type": "response.content_part.added" +} +export const BaseContentPartAddedEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.content_part.added") +}).annotate({ + "description": "Event emitted when a new content part is added to an output item", + "identifier": "BaseContentPartAddedEvent" +}) +export type BaseContentPartDoneEvent = { + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "part": ResponseOutputText | OpenAIResponsesRefusalContent + readonly "sequence_number": number + readonly "type": "response.content_part.done" +} +export const BaseContentPartDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.content_part.done") +}).annotate({ + "description": "Event emitted when a content part is complete", + "identifier": "BaseContentPartDoneEvent" +}) +export type ContentPartAddedEvent = { + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "part": ResponseOutputText | ReasoningTextContent | OpenAIResponsesRefusalContent + readonly "sequence_number": number + readonly "type": "response.content_part.added" +} +export const ContentPartAddedEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": Schema.Union([ResponseOutputText, ReasoningTextContent, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.content_part.added") +}).annotate({ + "description": "Event emitted when a new content part is added to an output item", + "identifier": "ContentPartAddedEvent" +}) +export type ContentPartDoneEvent = { + readonly "content_index": number + readonly "item_id": string + readonly "output_index": number + readonly "part": ResponseOutputText | ReasoningTextContent | OpenAIResponsesRefusalContent + readonly "sequence_number": number + readonly "type": "response.content_part.done" +} +export const ContentPartDoneEvent = Schema.Struct({ + "content_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "item_id": Schema.String, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "part": Schema.Union([ResponseOutputText, ReasoningTextContent, OpenAIResponsesRefusalContent]), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.content_part.done") +}).annotate({ "description": "Event emitted when a content part is complete", "identifier": "ContentPartDoneEvent" }) +export type OutputMessage = { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "message" +} +export const OutputMessage = Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Literal("message") +}).annotate({ "identifier": "OutputMessage" }) +export type OutputMessageItem = { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "message" +} +export const OutputMessageItem = Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Literal("message") +}).annotate({ "description": "An output message item", "identifier": "OutputMessageItem" }) +export type AnthropicWebFetchContent = AnthropicWebFetchToolResultError | AnthropicWebFetchBlock +export const AnthropicWebFetchContent = Schema.Union([AnthropicWebFetchToolResultError, AnthropicWebFetchBlock], { + mode: "oneOf" +}).annotate({ "identifier": "AnthropicWebFetchContent" }) +export type MessagesMessageParam = { + readonly "content": + | string + | ReadonlyArray< + | AnthropicTextBlockParam + | AnthropicImageBlockParam + | AnthropicDocumentBlockParam + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "id": string + readonly "input"?: Schema.Json + readonly "name": string + readonly "type": "tool_use" + } + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "content"?: + | string + | ReadonlyArray< + | AnthropicTextBlockParam + | AnthropicImageBlockParam + | { readonly "tool_name": string; readonly "type": "tool_reference" } + | AnthropicSearchResultBlockParam + | AnthropicDocumentBlockParam + > + readonly "is_error"?: boolean + readonly "tool_use_id": string + readonly "type": "tool_result" + } + | { readonly "signature": string; readonly "thinking": string; readonly "type": "thinking" } + | { readonly "data": string; readonly "type": "redacted_thinking" } + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "id": string + readonly "input"?: Schema.Json + readonly "name": string + readonly "type": "server_tool_use" + } + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "content": ReadonlyArray | { + readonly "error_code": + | "invalid_tool_input" + | "unavailable" + | "max_uses_exceeded" + | "too_many_requests" + | "query_too_long" + readonly "type": "web_search_tool_result_error" + } + readonly "tool_use_id": string + readonly "type": "web_search_tool_result" + } + | AnthropicSearchResultBlockParam + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "content": string | null + readonly "type": "compaction" + } + | MessagesAdvisorToolResultBlock + > + readonly "role": "user" | "assistant" | "system" +} +export const MessagesMessageParam = Schema.Struct({ + "content": Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([ + AnthropicTextBlockParam, + AnthropicImageBlockParam, + AnthropicDocumentBlockParam, + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "id": Schema.String, + "input": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "name": Schema.String, + "type": Schema.Literal("tool_use") + }), + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "content": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Array( + Schema.Union([ + AnthropicTextBlockParam, + AnthropicImageBlockParam, + Schema.Struct({ "tool_name": Schema.String, "type": Schema.Literal("tool_reference") }), + AnthropicSearchResultBlockParam, + AnthropicDocumentBlockParam + ]) + ) + ]) + ), + "is_error": Schema.optionalKey(Schema.Boolean), + "tool_use_id": Schema.String, + "type": Schema.Literal("tool_result") + }), + Schema.Struct({ "signature": Schema.String, "thinking": Schema.String, "type": Schema.Literal("thinking") }), + Schema.Struct({ "data": Schema.String, "type": Schema.Literal("redacted_thinking") }), + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "id": Schema.String, + "input": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "name": Schema.String, + "type": Schema.Literal("server_tool_use") + }), + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "content": Schema.Union([ + Schema.Array(AnthropicWebSearchResultBlockParam), + Schema.Struct({ + "error_code": Schema.Literals([ + "invalid_tool_input", + "unavailable", + "max_uses_exceeded", + "too_many_requests", + "query_too_long" + ]), + "type": Schema.Literal("web_search_tool_result_error") + }) + ]), + "tool_use_id": Schema.String, + "type": Schema.Literal("web_search_tool_result") + }), + AnthropicSearchResultBlockParam, + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "content": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("compaction") + }), + MessagesAdvisorToolResultBlock + ], { mode: "oneOf" }) + ) + ]), + "role": Schema.Literals(["user", "assistant", "system"]) +}).annotate({ "description": "Anthropic message with OpenRouter extensions", "identifier": "MessagesMessageParam" }) +export type Union_7 = ReadonlyArray | null +export const Union_7 = Schema.Union([Schema.Array(AnthropicUsageIteration), Schema.Null]) +export type MessagesDeltaEvent = { + readonly "delta": { + readonly "container": AnthropicContainer + readonly "stop_details": AnthropicRefusalStopDetails + readonly "stop_reason": ORAnthropicStopReason + readonly "stop_sequence": string | null + } + readonly "type": "message_delta" + readonly "usage": { + readonly "cache_creation"?: AnthropicCacheCreation + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "input_tokens": number | null + readonly "iterations"?: ReadonlyArray + readonly "output_tokens": number + readonly "output_tokens_details": AnthropicOutputTokensDetails + readonly "server_tool_use": { + readonly "web_fetch_requests": number + readonly "web_search_requests": number + readonly [x: string]: Schema.Json + } | null + } +} +export const MessagesDeltaEvent = Schema.Struct({ + "delta": Schema.Struct({ + "container": AnthropicContainer, + "stop_details": AnthropicRefusalStopDetails, + "stop_reason": ORAnthropicStopReason, + "stop_sequence": Schema.Union([Schema.String, Schema.Null]) + }), + "type": Schema.Literal("message_delta"), + "usage": Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicCacheCreation), + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "iterations": Schema.optionalKey(Schema.Array(AnthropicUsageIteration)), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": AnthropicOutputTokensDetails, + "server_tool_use": Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "web_fetch_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "web_search_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null + ]) + }) +}).annotate({ + "description": "Event sent when the message metadata changes (e.g., stop_reason)", + "identifier": "MessagesDeltaEvent" +}) +export type CreateObservabilityDestinationResponse = { readonly "data": ObservabilityDestination } +export const CreateObservabilityDestinationResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => ObservabilityDestination).annotate({ + "description": "The newly created observability destination." + }) +}).annotate({ "identifier": "CreateObservabilityDestinationResponse" }) +export type GetObservabilityDestinationResponse = { readonly "data": ObservabilityDestination } +export const GetObservabilityDestinationResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => ObservabilityDestination).annotate({ + "description": "The observability destination." + }) +}).annotate({ "identifier": "GetObservabilityDestinationResponse" }) +export type ListObservabilityDestinationsResponse = { + readonly "data": ReadonlyArray + readonly "total_count": number +} +export const ListObservabilityDestinationsResponse = Schema.Struct({ + "data": Schema.Array(ObservabilityDestination).annotate({ "description": "List of observability destinations." }), + "total_count": Schema.Number.annotate({ "description": "Total number of destinations matching the filters." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "identifier": "ListObservabilityDestinationsResponse" }) +export type UpdateObservabilityDestinationResponse = { readonly "data": ObservabilityDestination } +export const UpdateObservabilityDestinationResponse = Schema.Struct({ + "data": Schema.suspend((): Schema.Codec => ObservabilityDestination).annotate({ + "description": "The updated observability destination." + }) +}).annotate({ "identifier": "UpdateObservabilityDestinationResponse" }) +export type MessagesErrorEvent = { + readonly "error": { readonly "error_type"?: ApiErrorType; readonly "message": string; readonly "type": string } + readonly "openrouter_metadata"?: OpenRouterMetadata + readonly "type": "error" +} +export const MessagesErrorEvent = Schema.Struct({ + "error": Schema.Struct({ + "error_type": Schema.optionalKey(ApiErrorType), + "message": Schema.String, + "type": Schema.String + }), + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata), + "type": Schema.Literal("error") +}).annotate({ "description": "Error event in the stream", "identifier": "MessagesErrorEvent" }) +export type MessagesStopEvent = { readonly "openrouter_metadata"?: OpenRouterMetadata; readonly "type": "message_stop" } +export const MessagesStopEvent = Schema.Struct({ + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata), + "type": Schema.Literal("message_stop") +}).annotate({ "description": "Event sent when the message is complete", "identifier": "MessagesStopEvent" }) +export type ChatToolMessage = { + readonly "content": string | ReadonlyArray + readonly "role": "tool" + readonly "tool_call_id": string +} +export const ChatToolMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(ChatContentItems)]).annotate({ + "description": "Tool response content" + }), + "role": Schema.Literal("tool"), + "tool_call_id": Schema.String.annotate({ + "description": "ID of the assistant message tool call this message responds to" + }) +}).annotate({ "description": "Tool response message", "identifier": "ChatToolMessage" }) +export type ChatUserMessage = { + readonly "content": string | ReadonlyArray + readonly "name"?: string + readonly "role": "user" +} +export const ChatUserMessage = Schema.Struct({ + "content": Schema.Union([Schema.String, Schema.Array(ChatContentItems)]).annotate({ + "description": "User message content" + }), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Optional name for the user" })), + "role": Schema.Literal("user") +}).annotate({ "description": "User message", "identifier": "ChatUserMessage" }) +export type StoredPromptTemplate = Objects_151 | null +export const StoredPromptTemplate = Schema.Union([Objects_151, Schema.Null]).annotate({ + "identifier": "StoredPromptTemplate" +}) +export type ModelResponse = { readonly "data": Model } +export const ModelResponse = Schema.Struct({ "data": Model }).annotate({ + "description": "Single model response", + "identifier": "ModelResponse" +}) +export type ModelsListResponseData = ReadonlyArray +export const ModelsListResponseData = Schema.Array(Model).annotate({ + "description": "List of available models", + "identifier": "ModelsListResponseData" +}) +export type ChatAssistantMessage = { + readonly "audio"?: ChatAudioOutput + readonly "content"?: string | ReadonlyArray | null + readonly "images"?: + | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> + | null + readonly "model"?: string + readonly "name"?: string + readonly "reasoning"?: string | null + readonly "reasoning_details"?: ChatReasoningDetails + readonly "refusal"?: string | null + readonly "role": "assistant" + readonly "tool_calls"?: ReadonlyArray + readonly "annotations"?: + | ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "url": string + readonly "title"?: string + readonly "start_index"?: number + readonly "end_index"?: number + readonly "content"?: string + } + } | { + readonly "type": "file_annotation" + readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } + } | { + readonly "type": "file" + readonly "file": { + readonly "hash": string + readonly "name": string + readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> + } + } + > + | null +} +export const ChatAssistantMessage = Schema.Struct({ + "audio": Schema.optionalKey(ChatAudioOutput), + "content": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Array(ChatContentItems), Schema.Null]).annotate({ + "description": "Assistant message content" + }) + ), + "images": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) + ), + Schema.Null + ]) + ), + "model": Schema.optionalKey(Schema.String.annotate({ "description": "Model that generated this assistant message" })), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "Optional name for the assistant" })), + "reasoning": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Reasoning output" }) + ), + "reasoning_details": Schema.optionalKey(ChatReasoningDetails), + "refusal": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Refusal message if content was refused" }) + ), + "role": Schema.Literal("assistant"), + "tool_calls": Schema.optionalKey( + Schema.Array(ChatToolCall).annotate({ "description": "Tool calls made by the assistant" }) + ), + "annotations": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url_citation": Schema.Struct({ + "url": Schema.String, + "title": Schema.optionalKey(Schema.String), + "start_index": Schema.optionalKey( + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "end_index": Schema.optionalKey( + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "content": Schema.optionalKey(Schema.String) + }) + }), + Schema.Struct({ + "type": Schema.Literal("file_annotation"), + "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) + }), + Schema.Struct({ + "type": Schema.Literal("file"), + "file": Schema.Struct({ + "hash": Schema.String, + "name": Schema.String, + "content": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) + ) + }) + }) + ], { mode: "oneOf" })), + Schema.Null + ])) +}).annotate({ "description": "Assistant message for requests and responses", "identifier": "ChatAssistantMessage" }) +export type ChatStreamDelta = { + readonly "audio"?: ChatAudioOutput + readonly "content"?: string | null + readonly "reasoning"?: string | null + readonly "reasoning_details"?: ChatStreamReasoningDetails + readonly "refusal"?: string | null + readonly "role"?: "assistant" + readonly "tool_calls"?: ReadonlyArray + readonly "images"?: + | ReadonlyArray<{ readonly "type": "image_url"; readonly "image_url": { readonly "url": string } }> + | null + readonly "annotations"?: + | ReadonlyArray< + { + readonly "type": "url_citation" + readonly "url_citation": { + readonly "url": string + readonly "title"?: string + readonly "start_index"?: number + readonly "end_index"?: number + readonly "content"?: string + } + } | { + readonly "type": "file_annotation" + readonly "file_annotation": { readonly "file_id": string; readonly "quote"?: string } + } | { + readonly "type": "file" + readonly "file": { + readonly "hash": string + readonly "name": string + readonly "content"?: ReadonlyArray<{ readonly "type": string; readonly "text"?: string }> + } + } + > + | null +} +export const ChatStreamDelta = Schema.Struct({ + "audio": Schema.optionalKey( + Schema.suspend((): Schema.Codec => ChatAudioOutput).annotate({ + "description": "Audio output data" + }) + ), + "content": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Message content delta" }) + ), + "reasoning": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Reasoning content delta" }) + ), + "reasoning_details": Schema.optionalKey(ChatStreamReasoningDetails), + "refusal": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Refusal message delta" }) + ), + "role": Schema.optionalKey(Schema.Literal("assistant").annotate({ "description": "The role of the message author" })), + "tool_calls": Schema.optionalKey(Schema.Array(ChatStreamToolCall).annotate({ "description": "Tool calls delta" })), + "images": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Struct({ "type": Schema.Literal("image_url"), "image_url": Schema.Struct({ "url": Schema.String }) }) + ), + Schema.Null + ]) + ), + "annotations": Schema.optionalKey(Schema.Union([ + Schema.Array(Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("url_citation"), + "url_citation": Schema.Struct({ + "url": Schema.String, + "title": Schema.optionalKey(Schema.String), + "start_index": Schema.optionalKey( + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "end_index": Schema.optionalKey( + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "content": Schema.optionalKey(Schema.String) + }) + }), + Schema.Struct({ + "type": Schema.Literal("file_annotation"), + "file_annotation": Schema.Struct({ "file_id": Schema.String, "quote": Schema.optionalKey(Schema.String) }) + }), + Schema.Struct({ + "type": Schema.Literal("file"), + "file": Schema.Struct({ + "hash": Schema.String, + "name": Schema.String, + "content": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.String, "text": Schema.optionalKey(Schema.String) })) + ) + }) + }) + ], { mode: "oneOf" })), + Schema.Null + ])) +}).annotate({ "description": "Delta changes in streaming response", "identifier": "ChatStreamDelta" }) +export type AdditionalToolsItem = { + readonly "id"?: string | null + readonly "role": "unknown" | "user" | "assistant" | "system" | "critic" | "discriminator" | "developer" | "tool" + readonly "tools": ReadonlyArray< + | { + readonly "description"?: string | null + readonly "name": string + readonly "parameters": { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" + } + | Preview_WebSearchServerTool + | Preview_20250311_WebSearchServerTool + | Legacy_WebSearchServerTool + | WebSearchServerTool + | FileSearchServerTool + | ComputerUseServerTool + | CodeInterpreterServerTool + | McpServerTool + | ImageGenerationServerTool + | CodexLocalShellTool + | ShellServerTool + | ApplyPatchServerTool + | CustomTool + | NamespaceTool + | AdvisorServerTool_OpenRouter + | SubagentServerTool_OpenRouter + | DatetimeServerTool + | FilesServerTool + | FusionServerTool_OpenRouter + | ImageGenerationServerTool_OpenRouter + | SearchModelsServerTool_OpenRouter + | WebFetchServerTool + | WebSearchServerTool_OpenRouter + | ApplyPatchServerTool_OpenRouter + | BashServerTool + | ShellServerTool_OpenRouter + | { readonly "type": string } + > + readonly "type": "additional_tools" +} +export const AdditionalToolsItem = Schema.Struct({ + "id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "role": Schema.Literals(["unknown", "user", "assistant", "system", "critic", "discriminator", "developer", "tool"]), + "tools": Schema.Array(Schema.Union([ + Schema.Struct({ + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "parameters": Schema.Union([ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), + Schema.Null + ]), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") + }).annotate({ "description": "Function tool definition" }), + Preview_WebSearchServerTool, + Preview_20250311_WebSearchServerTool, + Legacy_WebSearchServerTool, + WebSearchServerTool, + FileSearchServerTool, + ComputerUseServerTool, + CodeInterpreterServerTool, + McpServerTool, + ImageGenerationServerTool, + CodexLocalShellTool, + ShellServerTool, + ApplyPatchServerTool, + CustomTool, + NamespaceTool, + AdvisorServerTool_OpenRouter, + SubagentServerTool_OpenRouter, + DatetimeServerTool, + FilesServerTool, + FusionServerTool_OpenRouter, + ImageGenerationServerTool_OpenRouter, + SearchModelsServerTool_OpenRouter, + WebFetchServerTool, + WebSearchServerTool_OpenRouter, + ApplyPatchServerTool_OpenRouter, + BashServerTool, + ShellServerTool_OpenRouter, + Schema.Struct({ "type": Schema.String }) + ])), + "type": Schema.Literal("additional_tools") +}).annotate({ + "description": "Additional tools made available to the model at this point in the input", + "identifier": "AdditionalToolsItem" +}) +export type ChatFunctionTool = + | { + readonly "cache_control"?: ChatContentCacheControl + readonly "function": { + readonly "description"?: string + readonly "name": string + readonly "parameters"?: {} + readonly "strict"?: boolean | null + } + readonly "type": "function" + } + | AdvisorServerTool_OpenRouter + | BashServerTool + | DatetimeServerTool + | FilesServerTool + | FusionServerTool_OpenRouter + | ImageGenerationServerTool_OpenRouter + | ChatSearchModelsServerTool + | SubagentServerTool_OpenRouter + | WebFetchServerTool + | OpenRouterWebSearchServerTool + | ChatWebSearchShorthand +export const ChatFunctionTool = Schema.Union([ + Schema.Struct({ + "cache_control": Schema.optionalKey(ChatContentCacheControl), + "function": Schema.Struct({ + "description": Schema.optionalKey( + Schema.String.annotate({ "description": "Function description for the model" }) + ), + "name": Schema.String.annotate({ + "description": "Function name (a-z, A-Z, 0-9, underscores, dashes, max 64 chars)" + }).check(Schema.isMaxLength(64).annotate({ "expected": "a value with a length of at most 64" })), + "parameters": Schema.optionalKey( + Schema.Struct({}).annotate({ "description": "Function parameters as JSON Schema object" }) + ), + "strict": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ "description": "Enable strict schema adherence" }) + ) + }).annotate({ "description": "Function definition for tool calling" }), + "type": Schema.Literal("function") + }), + AdvisorServerTool_OpenRouter, + BashServerTool, + DatetimeServerTool, + FilesServerTool, + FusionServerTool_OpenRouter, + ImageGenerationServerTool_OpenRouter, + ChatSearchModelsServerTool, + SubagentServerTool_OpenRouter, + WebFetchServerTool, + OpenRouterWebSearchServerTool, + ChatWebSearchShorthand +]).annotate({ + "description": "Tool definition for function calling (regular function or OpenRouter built-in server tool)", + "identifier": "ChatFunctionTool" +}) +export type BaseInputs = + | string + | ReadonlyArray< + | { + readonly "content": ReadonlyArray | string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "user" | "system" | "assistant" | "developer" + readonly "type"?: "message" + } + | OpenAIResponseInputMessageItem + | OpenAIResponseFunctionToolCallOutput + | OpenAIResponseFunctionToolCall + | OutputItemImageGenerationCall + | OutputMessage + | OpenAIResponseCustomToolCall + | OpenAIResponseCustomToolCallOutput + | ApplyPatchCallItem + | ApplyPatchCallOutputItem + > + | null +export const BaseInputs = Schema.Union([ + Schema.String, + Schema.Array(Schema.Union([ + Schema.Struct({ + "content": Schema.Union([ + Schema.Array(Schema.Union([InputText, InputImage, InputFile, InputAudio], { mode: "oneOf" })), + Schema.String + ]), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]) + ), + "role": Schema.Literals(["user", "system", "assistant", "developer"]), + "type": Schema.optionalKey(Schema.Literal("message")) + }), + OpenAIResponseInputMessageItem, + OpenAIResponseFunctionToolCallOutput, + OpenAIResponseFunctionToolCall, + OutputItemImageGenerationCall, + OutputMessage, + OpenAIResponseCustomToolCall, + OpenAIResponseCustomToolCallOutput, + ApplyPatchCallItem, + ApplyPatchCallOutputItem + ])), + Schema.Null +]).annotate({ "identifier": "BaseInputs" }) +export type OutputItemAddedEvent = { + readonly "item": + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemCustomToolCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + | OutputItemApplyPatchCall + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_item.added" +} +export const OutputItemAddedEvent = Schema.Struct({ + "item": Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemCustomToolCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall, + OutputItemApplyPatchCall + ], { mode: "oneOf" }), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_item.added") +}).annotate({ + "description": "Event emitted when a new output item is added to the response", + "identifier": "OutputItemAddedEvent" +}) +export type OutputItemDoneEvent = { + readonly "item": + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemCustomToolCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + | OutputItemApplyPatchCall + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_item.done" +} +export const OutputItemDoneEvent = Schema.Struct({ + "item": Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemCustomToolCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall, + OutputItemApplyPatchCall + ], { mode: "oneOf" }), + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_item.done") +}).annotate({ "description": "Event emitted when an output item is complete", "identifier": "OutputItemDoneEvent" }) +export type OutputItems = + | OutputMessageItem + | OutputReasoningItem + | OutputFunctionCallItem + | OutputWebSearchCallItem + | OutputFileSearchCallItem + | OutputImageGenerationCallItem + | OutputCodeInterpreterCallItem + | OutputComputerCallItem + | OutputDatetimeItem + | OutputWebSearchServerToolItem + | OutputCodeInterpreterServerToolItem + | OutputFileSearchServerToolItem + | OutputImageGenerationServerToolItem + | OutputBrowserUseServerToolItem + | OutputBashServerToolItem + | OutputTextEditorServerToolItem + | OutputApplyPatchServerToolItem + | OutputApplyPatchCallItem + | OutputShellCallItem + | OutputShellCallOutputItem + | OutputWebFetchServerToolItem + | OutputToolSearchServerToolItem + | OutputMemoryServerToolItem + | OutputMcpServerToolItem + | OutputSearchModelsServerToolItem + | OutputFusionServerToolItem + | OutputAdvisorServerToolItem + | OutputSubagentServerToolItem + | OutputFilesServerToolItem + | OutputCustomToolCallItem +export const OutputItems = Schema.Union([ + OutputMessageItem, + OutputReasoningItem, + OutputFunctionCallItem, + OutputWebSearchCallItem, + OutputFileSearchCallItem, + OutputImageGenerationCallItem, + OutputCodeInterpreterCallItem, + OutputComputerCallItem, + OutputDatetimeItem, + OutputWebSearchServerToolItem, + OutputCodeInterpreterServerToolItem, + OutputFileSearchServerToolItem, + OutputImageGenerationServerToolItem, + OutputBrowserUseServerToolItem, + OutputBashServerToolItem, + OutputTextEditorServerToolItem, + OutputApplyPatchServerToolItem, + OutputApplyPatchCallItem, + OutputShellCallItem, + OutputShellCallOutputItem, + OutputWebFetchServerToolItem, + OutputToolSearchServerToolItem, + OutputMemoryServerToolItem, + OutputMcpServerToolItem, + OutputSearchModelsServerToolItem, + OutputFusionServerToolItem, + OutputAdvisorServerToolItem, + OutputSubagentServerToolItem, + OutputFilesServerToolItem, + OutputCustomToolCallItem +], { mode: "oneOf" }).annotate({ "description": "An output item from the response", "identifier": "OutputItems" }) +export type AnthropicWebFetchToolResult = { + readonly "caller": AnthropicCaller + readonly "content": AnthropicWebFetchContent + readonly "tool_use_id": string + readonly "type": "web_fetch_tool_result" +} +export const AnthropicWebFetchToolResult = Schema.Struct({ + "caller": AnthropicCaller, + "content": AnthropicWebFetchContent, + "tool_use_id": Schema.String, + "type": Schema.Literal("web_fetch_tool_result") +}).annotate({ "identifier": "AnthropicWebFetchToolResult" }) +export type MessagesRequest = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "context_management"?: { + readonly "edits"?: ReadonlyArray< + { + readonly "clear_at_least"?: AnthropicInputTokensClearAtLeast + readonly "clear_tool_inputs"?: boolean | ReadonlyArray | null + readonly "exclude_tools"?: ReadonlyArray | null + readonly "keep"?: AnthropicToolUsesKeep + readonly "trigger"?: AnthropicInputTokensTrigger | AnthropicToolUsesTrigger + readonly "type": "clear_tool_uses_20250919" + } | { + readonly "keep"?: AnthropicThinkingTurns | { readonly "type": "all" } | "all" + readonly "type": "clear_thinking_20251015" + } | { + readonly "instructions"?: string | null + readonly "pause_after_compaction"?: boolean + readonly "trigger"?: { readonly "type": "input_tokens"; readonly "value": number } | null + readonly "type": "compact_20260112" + } + > + readonly [x: string]: Schema.Json + } | null + readonly "fallbacks"?: ReadonlyArray | null + readonly "max_tokens"?: number + readonly "messages": ReadonlyArray | null + readonly "metadata"?: { readonly "user_id"?: string | null } + readonly "model": string + readonly "models"?: ReadonlyArray + readonly "output_config"?: MessagesOutputConfig + readonly "plugins"?: ReadonlyArray< + | AutoRouterPlugin + | AutoBetaRouterPlugin + | ModerationPlugin + | WebSearchPlugin + | WebFetchPlugin + | FileParserPlugin + | ResponseHealingPlugin + | ContextCompressionPlugin + | ParetoRouterPlugin + | FusionPlugin + > + readonly "provider"?: ProviderPreferences + readonly "route"?: DeprecatedRoute + readonly "service_tier"?: string + readonly "session_id"?: string + readonly "speed"?: AnthropicSpeed + readonly "stop_sequences"?: ReadonlyArray + readonly "stop_server_tools_when"?: StopServerToolsWhen + readonly "stream"?: boolean + readonly "system"?: string | ReadonlyArray + readonly "temperature"?: number + readonly "thinking"?: + | { readonly "budget_tokens": number; readonly "display"?: AnthropicThinkingDisplay; readonly "type": "enabled" } + | { readonly "type": "disabled" } + | { readonly "display"?: AnthropicThinkingDisplay; readonly "type": "adaptive" } + readonly "tool_choice"?: + | { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "auto" } + | { readonly "disable_parallel_tool_use"?: boolean; readonly "type": "any" } + | { readonly "type": "none" } + | { readonly "disable_parallel_tool_use"?: boolean; readonly "name": string; readonly "type": "tool" } + readonly "tools"?: ReadonlyArray< + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "description"?: string + readonly "input_schema": { + readonly "properties"?: Schema.Json + readonly "required"?: ReadonlyArray | null + readonly "type"?: string + } + readonly "name": string + readonly "type"?: "custom" + } + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "name": "bash" + readonly "type": "bash_20250124" + } + | { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "name": "str_replace_editor" + readonly "type": "text_editor_20250124" + } + | { + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "type": "web_search_20250305" + readonly "user_location"?: AnthropicWebSearchToolUserLocation + } + | { + readonly "allowed_callers"?: AnthropicAllowedCallers + readonly "allowed_domains"?: ReadonlyArray | null + readonly "blocked_domains"?: ReadonlyArray | null + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "max_uses"?: number | null + readonly "name": "web_search" + readonly "type": "web_search_20260209" + readonly "user_location"?: AnthropicWebSearchToolUserLocation + } + | { + readonly "allowed_callers"?: AnthropicAllowedCallers + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "caching"?: AnthropicCacheControlDirective | null + readonly "defer_loading"?: boolean + readonly "max_uses"?: number + readonly "model": string + readonly "name": "advisor" + readonly "type": "advisor_20260301" + } + | BashServerTool + | DatetimeServerTool + | ImageGenerationServerTool_OpenRouter + | MessagesSearchModelsServerTool + | WebFetchServerTool + | OpenRouterWebSearchServerTool + | { readonly "type": string } + | AnthropicToolSearchToolBm25 + | AnthropicToolSearchToolRegex + > + readonly "top_k"?: number + readonly "top_p"?: number + readonly "trace"?: TraceConfig + readonly "user"?: string +} +export const MessagesRequest = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "context_management": Schema.optionalKey(Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "edits": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "clear_at_least": Schema.optionalKey(AnthropicInputTokensClearAtLeast), + "clear_tool_inputs": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Array(Schema.String), Schema.Null]) + ), + "exclude_tools": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "keep": Schema.optionalKey(AnthropicToolUsesKeep), + "trigger": Schema.optionalKey( + Schema.Union([AnthropicInputTokensTrigger, AnthropicToolUsesTrigger], { mode: "oneOf" }) + ), + "type": Schema.Literal("clear_tool_uses_20250919") + }), + Schema.Struct({ + "keep": Schema.optionalKey( + Schema.Union([ + AnthropicThinkingTurns, + Schema.Struct({ "type": Schema.Literal("all") }), + Schema.Literal("all") + ]) + ), + "type": Schema.Literal("clear_thinking_20251015") + }), + Schema.Struct({ + "instructions": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "pause_after_compaction": Schema.optionalKey(Schema.Boolean), + "trigger": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "type": Schema.Literal("input_tokens"), + "value": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + Schema.Null + ]) + ), + "type": Schema.Literal("compact_20260112") + }) + ], { mode: "oneOf" }))) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null + ])), + "fallbacks": Schema.optionalKey( + Schema.Union([Schema.Array(MessagesFallbackParam), Schema.Null]).annotate({ + "description": + "Fallback models to try if the primary model fails or refuses, in order. Handled by OpenRouter multi-model routing rather than Anthropic server-side fallbacks; cannot be combined with `models`. Each entry accepts only `model`. Maximum of 3 entries." + }) + ), + "max_tokens": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "messages": Schema.Union([Schema.Array(MessagesMessageParam), Schema.Null]), + "metadata": Schema.optionalKey( + Schema.Struct({ "user_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) }) + ), + "model": Schema.String, + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "output_config": Schema.optionalKey(MessagesOutputConfig), + "plugins": Schema.optionalKey( + Schema.Array( + Schema.Union([ + AutoRouterPlugin, + AutoBetaRouterPlugin, + ModerationPlugin, + WebSearchPlugin, + WebFetchPlugin, + FileParserPlugin, + ResponseHealingPlugin, + ContextCompressionPlugin, + ParetoRouterPlugin, + FusionPlugin + ], { mode: "oneOf" }) + ).annotate({ "description": "Plugins you want to enable for this request, including their settings." }) + ), + "provider": Schema.optionalKey(ProviderPreferences), + "route": Schema.optionalKey(DeprecatedRoute), + "service_tier": Schema.optionalKey(Schema.String), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow). When provided, OpenRouter uses it as the sticky routing key, routing all requests in the session to the same provider to maximize prompt cache hits. Also used for observability grouping. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 256 characters." + }).check(Schema.isMaxLength(256).annotate({ "expected": "a value with a length of at most 256" })) + ), + "speed": Schema.optionalKey( + Schema.suspend((): Schema.Codec => AnthropicSpeed).annotate({ + "description": + "Controls output generation speed. When set to `fast`, uses a higher-speed inference configuration at premium pricing. Defaults to `standard` when omitted." + }) + ), + "stop_sequences": Schema.optionalKey(Schema.Array(Schema.String)), + "stop_server_tools_when": Schema.optionalKey(StopServerToolsWhen), + "stream": Schema.optionalKey(Schema.Boolean), + "system": Schema.optionalKey(Schema.Union([Schema.String, Schema.Array(AnthropicTextBlockParam)])), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "thinking": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "budget_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "display": Schema.optionalKey(AnthropicThinkingDisplay), + "type": Schema.Literal("enabled") + }), + Schema.Struct({ "type": Schema.Literal("disabled") }), + Schema.Struct({ "display": Schema.optionalKey(AnthropicThinkingDisplay), "type": Schema.Literal("adaptive") }) + ], { mode: "oneOf" }) + ), + "tool_choice": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean), + "type": Schema.Literal("auto") + }), + Schema.Struct({ "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean), "type": Schema.Literal("any") }), + Schema.Struct({ "type": Schema.Literal("none") }), + Schema.Struct({ + "disable_parallel_tool_use": Schema.optionalKey(Schema.Boolean), + "name": Schema.String, + "type": Schema.Literal("tool") + }) + ], { mode: "oneOf" }) + ), + "tools": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "description": Schema.optionalKey(Schema.String), + "input_schema": Schema.Struct({ + "properties": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "required": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "type": Schema.optionalKey(Schema.String) + }), + "name": Schema.String, + "type": Schema.optionalKey(Schema.Literal("custom")) + }), + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "name": Schema.Literal("bash"), + "type": Schema.Literal("bash_20250124") + }), + Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "name": Schema.Literal("str_replace_editor"), + "type": Schema.Literal("text_editor_20250124") + }), + Schema.Struct({ + "allowed_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "blocked_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "name": Schema.Literal("web_search"), + "type": Schema.Literal("web_search_20250305"), + "user_location": Schema.optionalKey(AnthropicWebSearchToolUserLocation) + }), + Schema.Struct({ + "allowed_callers": Schema.optionalKey(AnthropicAllowedCallers), + "allowed_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "blocked_domains": Schema.optionalKey(Schema.Union([Schema.Array(Schema.String), Schema.Null])), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "max_uses": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "name": Schema.Literal("web_search"), + "type": Schema.Literal("web_search_20260209"), + "user_location": Schema.optionalKey(AnthropicWebSearchToolUserLocation) + }), + Schema.Struct({ + "allowed_callers": Schema.optionalKey(AnthropicAllowedCallers), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "caching": Schema.optionalKey(Schema.Union([AnthropicCacheControlDirective, Schema.Null])), + "defer_loading": Schema.optionalKey(Schema.Boolean), + "max_uses": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "model": Schema.String, + "name": Schema.Literal("advisor"), + "type": Schema.Literal("advisor_20260301") + }), + BashServerTool, + DatetimeServerTool, + ImageGenerationServerTool_OpenRouter, + MessagesSearchModelsServerTool, + WebFetchServerTool, + OpenRouterWebSearchServerTool, + Schema.Struct({ "type": Schema.String }), + AnthropicToolSearchToolBm25, + AnthropicToolSearchToolRegex + ]))), + "top_k": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "top_p": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + ), + "trace": Schema.optionalKey(TraceConfig), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "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 256 characters." + }).check(Schema.isMaxLength(256).annotate({ "expected": "a value with a length of at most 256" })) + ) +}).annotate({ "description": "Request schema for Anthropic Messages API endpoint", "identifier": "MessagesRequest" }) +export type ImageGenerationUsage = { + readonly "cache_creation"?: AnthropicCacheCreation + readonly "completion_tokens": number + readonly "completion_tokens_details"?: Union_6 + readonly "cost"?: number | null + readonly "cost_details"?: CostDetails + readonly "is_byok"?: boolean + readonly "iterations"?: Union_7 + readonly "prompt_tokens": number + readonly "prompt_tokens_details"?: Union_8 + readonly "server_tool_use"?: Union_9 + readonly "service_tier"?: string | null + readonly "speed"?: AnthropicSpeed + readonly "total_tokens": number +} +export const ImageGenerationUsage = Schema.Struct({ + "cache_creation": Schema.optionalKey(AnthropicCacheCreation), + "completion_tokens": Schema.Number.annotate({ "description": "The tokens generated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "completion_tokens_details": Schema.optionalKey(Union_6), + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Cost of the completion", "format": "double" }) + ), + "cost_details": Schema.optionalKey(CostDetails), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether a request was made using a Bring Your Own Key configuration" }) + ), + "iterations": Schema.optionalKey(Union_7), + "prompt_tokens": Schema.Number.annotate({ "description": "Including images, input audio, and tools if any" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "prompt_tokens_details": Schema.optionalKey(Union_8), + "server_tool_use": Schema.optionalKey(Union_9), + "service_tier": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The service tier used by the upstream provider for this request" + }) + ), + "speed": Schema.optionalKey(AnthropicSpeed), + "total_tokens": Schema.Number.annotate({ "description": "Sum of the above two fields" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ + "description": "Token and cost usage for the image generation request, when available", + "identifier": "ImageGenerationUsage" +}) +export type ModelsListResponse = { + readonly "data": ModelsListResponseData + readonly "links": { readonly "next": string | null } + readonly "total_count": number +} +export const ModelsListResponse = Schema.Struct({ + "data": ModelsListResponseData, + "links": Schema.Struct({ + "next": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "URL for the next page of results, or null if this is the last page" + }) + }).annotate({ "description": "Pagination links" }), + "total_count": Schema.Number.annotate({ "description": "Total number of models matching the query" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}).annotate({ "description": "List of available models", "identifier": "ModelsListResponse" }) +export type ChatChoice = { + readonly "finish_reason": ChatFinishReasonEnum + readonly "index": number + readonly "logprobs"?: ChatTokenLogprobs + readonly "message": ChatAssistantMessage +} +export const ChatChoice = Schema.Struct({ + "finish_reason": ChatFinishReasonEnum, + "index": Schema.Number.annotate({ "description": "Choice index" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "logprobs": Schema.optionalKey(ChatTokenLogprobs), + "message": ChatAssistantMessage +}).annotate({ "description": "Chat completion choice", "identifier": "ChatChoice" }) +export type ChatMessages = + | ChatSystemMessage + | ChatUserMessage + | ChatDeveloperMessage + | ChatAssistantMessage + | ChatToolMessage +export const ChatMessages = Schema.Union([ + ChatSystemMessage, + ChatUserMessage, + ChatDeveloperMessage, + ChatAssistantMessage, + ChatToolMessage +], { mode: "oneOf" }).annotate({ + "description": "Chat completion message with role-based discrimination", + "identifier": "ChatMessages" +}) +export type ChatStreamChoice = { + readonly "delta": ChatStreamDelta + readonly "finish_reason"?: ChatFinishReasonEnum + readonly "index": number + readonly "logprobs"?: ChatTokenLogprobs +} +export const ChatStreamChoice = Schema.Struct({ + "delta": ChatStreamDelta, + "finish_reason": Schema.optionalKey(ChatFinishReasonEnum), + "index": Schema.Number.annotate({ "description": "Choice index" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "logprobs": Schema.optionalKey(ChatTokenLogprobs) +}).annotate({ "description": "Streaming completion choice chunk", "identifier": "ChatStreamChoice" }) +export type Arrays_8 = ReadonlyArray< + | ReasoningItem + | EasyInputMessage + | InputMessageItem + | FunctionCallItem + | FunctionCallOutputItem + | ApplyPatchCallItem + | ApplyPatchCallOutputItem + | { + readonly "content": ReadonlyArray< + { + readonly "annotations"?: ReadonlyArray< + { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": "file_citation" + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": never + readonly "url": string + readonly "file_id": string + readonly "filename": string + readonly "index": number + } | { + readonly "file_id": string + readonly "index": number + readonly "type": never + readonly "filename": string + } | { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": never + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "url": string + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": "url_citation" + readonly "url": string + } | { + readonly "file_id": string + readonly "index": number + readonly "type": never + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "url": string + } | { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": never + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": never + readonly "url": string + readonly "file_id": string + readonly "index": number + } | { readonly "file_id": string; readonly "index": number; readonly "type": "file_path" } + > + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": "output_text" + } | { + readonly "refusal": string + readonly "type": never + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + } | { + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": never + readonly "refusal": string + } | { readonly "refusal": string; readonly "type": "refusal" } + > + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "message" + } + | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray<{ readonly "text": string; readonly "type": "summary_text" }> + readonly "type": "reasoning" + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + } + | OutputFunctionCallItem + | OutputCustomToolCallItem + | OutputWebSearchCallItem + | OutputFileSearchCallItem + | OutputImageGenerationCallItem + | OutputCodeInterpreterCallItem + | OutputComputerCallItem + | OutputDatetimeItem + | OutputWebSearchServerToolItem + | OutputCodeInterpreterServerToolItem + | OutputFileSearchServerToolItem + | OutputImageGenerationServerToolItem + | OutputBrowserUseServerToolItem + | OutputBashServerToolItem + | OutputTextEditorServerToolItem + | OutputApplyPatchServerToolItem + | OutputWebFetchServerToolItem + | OutputToolSearchServerToolItem + | OutputMemoryServerToolItem + | OutputMcpServerToolItem + | OutputSearchModelsServerToolItem + | OutputFusionServerToolItem + | OutputAdvisorServerToolItem + | OutputSubagentServerToolItem + | OutputFilesServerToolItem + | LocalShellCallItem + | LocalShellCallOutputItem + | ShellCallItem + | ShellCallOutputItem + | McpListToolsItem + | McpApprovalRequestItem + | McpApprovalResponseItem + | McpCallItem + | CustomToolCallItem + | CustomToolCallOutputItem + | CompactionItem + | ContextCompactionItem + | ItemReferenceItem + | AdditionalToolsItem + | AgentMessageItem +> +export const Arrays_8 = Schema.Array( + Schema.Union([ + ReasoningItem, + EasyInputMessage, + InputMessageItem, + FunctionCallItem, + FunctionCallOutputItem, + ApplyPatchCallItem, + ApplyPatchCallOutputItem, + Schema.Struct({ + "content": Schema.Union([Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_citation") + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "filename": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Literal("url_citation"), + "url": Schema.String + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "url": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_path") + }) + ]) + ]) + ) + ), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Literal("output_text") + }), + Schema.Struct({ + "refusal": Schema.String, + "type": Schema.Never, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Never, + "refusal": Schema.String + }), + Schema.Struct({ "refusal": Schema.String, "type": Schema.Literal("refusal") }) + ]) + ]))]), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Literal("message") + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") }))]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("summary_text") })) + ]), + "type": Schema.Literal("reasoning"), + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + OutputFunctionCallItem, + OutputCustomToolCallItem, + OutputWebSearchCallItem, + OutputFileSearchCallItem, + OutputImageGenerationCallItem, + OutputCodeInterpreterCallItem, + OutputComputerCallItem, + OutputDatetimeItem, + OutputWebSearchServerToolItem, + OutputCodeInterpreterServerToolItem, + OutputFileSearchServerToolItem, + OutputImageGenerationServerToolItem, + OutputBrowserUseServerToolItem, + OutputBashServerToolItem, + OutputTextEditorServerToolItem, + OutputApplyPatchServerToolItem, + OutputWebFetchServerToolItem, + OutputToolSearchServerToolItem, + OutputMemoryServerToolItem, + OutputMcpServerToolItem, + OutputSearchModelsServerToolItem, + OutputFusionServerToolItem, + OutputAdvisorServerToolItem, + OutputSubagentServerToolItem, + OutputFilesServerToolItem, + LocalShellCallItem, + LocalShellCallOutputItem, + ShellCallItem, + ShellCallOutputItem, + McpListToolsItem, + McpApprovalRequestItem, + McpApprovalResponseItem, + McpCallItem, + CustomToolCallItem, + CustomToolCallOutputItem, + CompactionItem, + ContextCompactionItem, + ItemReferenceItem, + AdditionalToolsItem, + AgentMessageItem + ]) +) +export type BaseResponsesResult = { + readonly "background"?: boolean | null + readonly "completed_at": number | null + readonly "created_at": number + readonly "error": ResponsesErrorField + readonly "frequency_penalty": number | null + readonly "id": string + readonly "incomplete_details": IncompleteDetails + readonly "instructions": BaseInputs + readonly "max_output_tokens"?: number | null + readonly "max_tool_calls"?: number | null + readonly "metadata": RequestMetadata + readonly "model": string + readonly "object": "response" + readonly "output": ReadonlyArray< + | OutputMessage + | OutputItemReasoning + | OutputItemFunctionCall + | OutputItemCustomToolCall + | OutputItemWebSearchCall + | OutputItemFileSearchCall + | OutputItemImageGenerationCall + | OutputItemApplyPatchCall + > + readonly "output_text"?: string + readonly "parallel_tool_calls": boolean + readonly "presence_penalty": number | null + readonly "previous_response_id"?: string | null + readonly "prompt"?: StoredPromptTemplate + readonly "prompt_cache_key"?: string | null + readonly "prompt_cache_options"?: PromptCacheOptions + readonly "reasoning"?: BaseReasoningConfig + readonly "safety_identifier"?: string | null + readonly "service_tier"?: ServiceTier + readonly "status": OpenAIResponsesResponseStatus + readonly "store"?: boolean + readonly "temperature": number | null + readonly "text"?: TextConfig + readonly "tool_choice": OpenAIResponsesToolChoice + readonly "tools": ReadonlyArray< + | { + readonly "description"?: string | null + readonly "name": string + readonly "parameters": { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" + } + | Preview_WebSearchServerTool + | Preview_20250311_WebSearchServerTool + | Legacy_WebSearchServerTool + | WebSearchServerTool + | FileSearchServerTool + | ComputerUseServerTool + | CodeInterpreterServerTool + | McpServerTool + | ImageGenerationServerTool + | CodexLocalShellTool + | ShellServerTool + | ApplyPatchServerTool + | CustomTool + | NamespaceTool + > + readonly "top_logprobs"?: number + readonly "top_p": number | null + readonly "truncation"?: Truncation + readonly "usage"?: OpenAIResponsesUsage + readonly "user"?: string | null +} +export const BaseResponsesResult = Schema.Struct({ + "background": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "completed_at": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "created_at": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "error": ResponsesErrorField, + "frequency_penalty": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "id": Schema.String, + "incomplete_details": IncompleteDetails, + "instructions": BaseInputs, + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "metadata": RequestMetadata, + "model": Schema.String, + "object": Schema.Literal("response"), + "output": Schema.Array( + Schema.Union([ + OutputMessage, + OutputItemReasoning, + OutputItemFunctionCall, + OutputItemCustomToolCall, + OutputItemWebSearchCall, + OutputItemFileSearchCall, + OutputItemImageGenerationCall, + OutputItemApplyPatchCall + ], { mode: "oneOf" }) + ), + "output_text": Schema.optionalKey(Schema.String), + "parallel_tool_calls": Schema.Boolean, + "presence_penalty": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "previous_response_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt": Schema.optionalKey(StoredPromptTemplate), + "prompt_cache_key": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt_cache_options": Schema.optionalKey(PromptCacheOptions), + "reasoning": Schema.optionalKey(BaseReasoningConfig), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "service_tier": Schema.optionalKey(ServiceTier), + "status": OpenAIResponsesResponseStatus, + "store": Schema.optionalKey(Schema.Boolean), + "temperature": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "text": Schema.optionalKey(TextConfig), + "tool_choice": OpenAIResponsesToolChoice, + "tools": Schema.Array(Schema.Union([ + Schema.Struct({ + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "parameters": Schema.Union([ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), + Schema.Null + ]), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") + }).annotate({ "description": "Function tool definition" }), + Preview_WebSearchServerTool, + Preview_20250311_WebSearchServerTool, + Legacy_WebSearchServerTool, + WebSearchServerTool, + FileSearchServerTool, + ComputerUseServerTool, + CodeInterpreterServerTool, + McpServerTool, + ImageGenerationServerTool, + CodexLocalShellTool, + ShellServerTool, + ApplyPatchServerTool, + CustomTool, + NamespaceTool + ], { mode: "oneOf" })), + "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "top_p": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "truncation": Schema.optionalKey(Truncation), + "usage": Schema.optionalKey(OpenAIResponsesUsage), + "user": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) +}).annotate({ "identifier": "BaseResponsesResult" }) +export type OpenResponsesResult = { + readonly "background"?: boolean | null + readonly "completed_at": number | null + readonly "created_at": number + readonly "error": ResponsesErrorField + readonly "frequency_penalty": number | null + readonly "id": string + readonly "incomplete_details": IncompleteDetails + readonly "instructions": BaseInputs + readonly "max_output_tokens"?: number | null + readonly "max_tool_calls"?: number | null + readonly "metadata": RequestMetadata + readonly "model": string + readonly "object": "response" + readonly "output": ReadonlyArray< + { + readonly "content": ReadonlyArray< + { + readonly "annotations"?: ReadonlyArray< + { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": "file_citation" + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": never + readonly "url": string + readonly "file_id": string + readonly "filename": string + readonly "index": number + } | { + readonly "file_id": string + readonly "index": number + readonly "type": never + readonly "filename": string + } | { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": never + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "url": string + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": "url_citation" + readonly "url": string + } | { + readonly "file_id": string + readonly "index": number + readonly "type": never + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "url": string + } | { + readonly "file_id": string + readonly "filename": string + readonly "index": number + readonly "type": never + } | { + readonly "content"?: string + readonly "end_index": number + readonly "start_index": number + readonly "title": string + readonly "type": never + readonly "url": string + readonly "file_id": string + readonly "index": number + } | { readonly "file_id": string; readonly "index": number; readonly "type": "file_path" } + > + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": "output_text" + } | { + readonly "refusal": string + readonly "type": never + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + } | { + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": never + readonly "refusal": string + } | { readonly "refusal": string; readonly "type": "refusal" } + > + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "message" + } | { + readonly "content": ReadonlyArray< + { + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": never + } | { readonly "refusal": string; readonly "type": never; readonly "text": string } + > + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "prompt"?: string + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "timezone": string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: string + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "call_id"?: string + readonly "id": string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "content": never + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "value"?: Schema.Json + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "toolName"?: string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "content": ReadonlyArray + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + } | { + readonly "content": ReadonlyArray< + { + readonly "annotations"?: ReadonlyArray + readonly "logprobs"?: ReadonlyArray< + { + readonly "bytes": ReadonlyArray + readonly "logprob": number + readonly "token": string + readonly "top_logprobs": ReadonlyArray< + { readonly "bytes": ReadonlyArray; readonly "logprob": number; readonly "token": string } + > + } + > + readonly "text": string + readonly "type": never + } | { readonly "refusal": string; readonly "type": never; readonly "text": string } + > + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray<{ readonly "text": string; readonly "type": "summary_text" }> + readonly "type": "reasoning" + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "prompt"?: string + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "timezone": string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: string + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "call_id"?: string + readonly "id": string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "content"?: never + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "value"?: Schema.Json + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "toolName"?: string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "summary": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "content"?: ReadonlyArray + readonly "encrypted_content"?: string | null + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id"?: string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": "function_call" + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "prompt"?: string + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id"?: string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "arguments": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "datetime": string + readonly "id"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "timezone": string + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "language"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: string + readonly "id"?: string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments": string + readonly "call_id": string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "name": string + readonly "namespace"?: string + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id"?: string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "arguments": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id"?: string + readonly "key"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "value"?: Schema.Json + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "toolName"?: string + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments": string + readonly "id"?: string + readonly "query"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id"?: string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name": string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "namespace"?: string + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id"?: string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" | "incomplete" + readonly "type": never + readonly "arguments": string + readonly "call_id": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "arguments": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id"?: string + readonly "name": string + readonly "namespace"?: string + readonly "status"?: "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "input": string + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": WebSearchStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": ImageGenerationStatus + readonly "type": never + readonly "prompt"?: string + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id"?: string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "incomplete" | "in_progress" + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "datetime": string + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "timezone": string + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "language"?: string + readonly "status": ToolCallStatus + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "queries"?: ReadonlyArray + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: string + readonly "id"?: string + readonly "screenshotB64"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments"?: string + readonly "call_id": string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id"?: string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": ToolCallStatus + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": ApplyPatchCallStatus + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": ShellCallStatus + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": ShellCallStatus + readonly "type": never + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id"?: string + readonly "status": ToolCallStatus + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "query"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id"?: string + readonly "key"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "value"?: Schema.Json + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "id"?: string + readonly "serverLabel"?: string + readonly "status": ToolCallStatus + readonly "toolName"?: string + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "arguments"?: string + readonly "id"?: string + readonly "query"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id"?: string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "error"?: string + readonly "id"?: string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name": string + readonly "outcome"?: string + readonly "status": ToolCallStatus + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "namespace"?: string + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id"?: string + readonly "operation"?: string + readonly "result"?: string + readonly "status": ToolCallStatus + readonly "type": never + readonly "call_id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + } | { + readonly "call_id": string + readonly "id"?: string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": "custom_tool_call" + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + | { + readonly "type": never + readonly "url"?: string | null + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + } + | { + readonly "pattern": string + readonly "type": never + readonly "url": string + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + } + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": never + readonly "url"?: string | null + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": never; readonly "url": string } + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": never + readonly "pattern": string + readonly "url": string + } + | { readonly "type": never; readonly "url": string; readonly "pattern": string } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "searching" | "in_progress" | "failed" + readonly "type": "web_search_call" + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "searching" | "in_progress" | "failed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" | "failed" + readonly "type": never + readonly "prompt"?: string + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "in_progress" + readonly "type": never + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "timezone": string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } | { + readonly "type": never + readonly "url"?: string | null + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + } | { + readonly "pattern": string + readonly "type": never + readonly "url": string + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + } + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "id": string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: never + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "call_id"?: string + readonly "id": string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } | { + readonly "type": "open_page" + readonly "url"?: string | null + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } | { + readonly "pattern": string + readonly "type": "find_in_page" + readonly "url": string + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "action"?: never + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "value"?: Schema.Json + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" + readonly "toolName"?: string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "status": WebSearchStatus + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "queries": ReadonlyArray + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "searching" | "in_progress" | "failed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "searching" | "in_progress" | "failed" + readonly "type": "file_search_call" + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" | "failed" + readonly "type": never + readonly "prompt"?: string + readonly "queries": ReadonlyArray + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "timezone": string + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: string + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "call_id"?: string + readonly "id": string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "queries": ReadonlyArray + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "value"?: Schema.Json + readonly "queries": ReadonlyArray + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" + readonly "toolName"?: string + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "queries": ReadonlyArray + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "queries": ReadonlyArray + readonly "status": WebSearchStatus + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "result"?: string | null + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "in_progress" | "failed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" | "failed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" | "generating" | "failed" + readonly "type": "image_generation_call" + readonly "prompt"?: string + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "timezone": string + readonly "type": never + readonly "result"?: string | null + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "result"?: string | null + } | { + readonly "id": string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "action"?: string + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "arguments"?: string + readonly "call_id"?: string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "result"?: string | null + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "call_id"?: string + readonly "id": string + readonly "operation"?: ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": ApplyPatchCallOperation + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "result"?: string | null + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "value"?: Schema.Json + readonly "result"?: string | null + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" + readonly "toolName"?: string + readonly "type": never + readonly "result"?: string | null + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "result"?: string | null + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "result"?: string | null + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation"?: string + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "result"?: string | null + readonly "status": ImageGenerationStatus + } | { + readonly "content": ReadonlyArray + readonly "id": string + readonly "phase"?: "commentary" | "final_answer" | null + readonly "role": "assistant" + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "content"?: ReadonlyArray<{ readonly "text": string; readonly "type": "reasoning_text" }> + readonly "encrypted_content"?: string | null + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "summary": ReadonlyArray + readonly "type": never + readonly "format"?: ReasoningFormat + readonly "signature"?: string | null + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "arguments": string + readonly "call_id": string + readonly "id": string + readonly "name": string + readonly "namespace"?: string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "action"?: + | { + readonly "queries"?: ReadonlyArray + readonly "query": string + readonly "sources"?: ReadonlyArray + readonly "type": "search" + } + | { readonly "type": "open_page"; readonly "url"?: string | null } + | { readonly "pattern": string; readonly "type": "find_in_page"; readonly "url": string } + readonly "id": string + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "queries": ReadonlyArray + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "result"?: string | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "prompt"?: string + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "code": string | null + readonly "container_id": string + readonly "id": string + readonly "outputs": + | ReadonlyArray< + { readonly "type": "image"; readonly "url": string } | { readonly "logs": string; readonly "type": "logs" } + > + | null + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "action"?: Schema.Json + readonly "call_id": string + readonly "id": string + readonly "pending_safety_checks": ReadonlyArray< + { readonly "code": string; readonly "id": string; readonly "message": string } + > + readonly "status": "completed" | "in_progress" + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "datetime": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "timezone": string + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "action"?: { + readonly "query": string + readonly "sources"?: ReadonlyArray<{ readonly "type": "url"; readonly "url": string }> + readonly "type": "search" + } + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "code"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "language"?: string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "queries"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "imageB64"?: string + readonly "imageUrl"?: string + readonly "prompt"?: string + readonly "result"?: string | null + readonly "revisedPrompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "action"?: string + readonly "id": string + readonly "screenshotB64"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "arguments"?: string + readonly "call_id": string + readonly "command"?: string + readonly "exitCode"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "stderr"?: string + readonly "stdout"?: string + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "command"?: "view" | "create" | "str_replace" | "insert" + readonly "filePath"?: string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": + | { readonly "diff": string; readonly "path": string; readonly "type": "create_file" } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "path": string; readonly "type": never; readonly "diff": string } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "diff": string; readonly "path": string; readonly "type": "update_file" } + | { readonly "path": string; readonly "type": never; readonly "diff": string } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "path": string; readonly "type": "delete_file" } + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "created_by"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "operation": + | { readonly "diff": string; readonly "path": string; readonly "type": "create_file" } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "path": string; readonly "type": never; readonly "diff": string } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "diff": string; readonly "path": string; readonly "type": "update_file" } + | { readonly "path": string; readonly "type": never; readonly "diff": string } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "diff": string; readonly "path": string; readonly "type": never } + | { readonly "path": string; readonly "type": "delete_file" } + readonly "status": "in_progress" | "completed" + readonly "type": "apply_patch_call" + readonly "created_by"?: string + } | { + readonly "action"?: { + readonly "commands": ReadonlyArray + readonly "max_output_length": number | null + readonly "timeout_ms": number | null + } + readonly "call_id": string + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "call_id": string + readonly "id": string + readonly "max_output_length"?: number | null + readonly "output": ReadonlyArray< + { + readonly "outcome": { readonly "exit_code": number; readonly "type": "exit" } | { readonly "type": "timeout" } + readonly "stderr": string + readonly "stdout": string + } + > + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "content"?: string + readonly "error"?: string + readonly "httpStatus"?: number + readonly "id": string + readonly "status": "in_progress" | "completed" + readonly "title"?: string + readonly "type": never + readonly "url"?: string + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "action"?: "read" | "write" | "delete" + readonly "id": string + readonly "key"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "value"?: Schema.Json + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "id": string + readonly "serverLabel"?: string + readonly "status": "in_progress" | "completed" + readonly "toolName"?: string + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "arguments"?: string + readonly "id": string + readonly "query"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "analysis"?: FusionAnalysisResult + readonly "error"?: string + readonly "failed_models"?: ReadonlyArray< + { readonly "error": string; readonly "model": string; readonly "status_code"?: number } + > + readonly "failure_reason"?: string + readonly "id": string + readonly "responses"?: ReadonlyArray<{ readonly "content"?: string; readonly "model": string }> + readonly "sources"?: ReadonlyArray + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "advice"?: string + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "prompt"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "error"?: string + readonly "id": string + readonly "instance_name"?: string + readonly "model"?: string + readonly "name"?: string + readonly "outcome"?: string + readonly "status": "in_progress" | "completed" + readonly "task_description"?: string + readonly "task_name"?: string + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + } | { + readonly "error"?: string + readonly "file_id"?: string + readonly "filename"?: string + readonly "id": string + readonly "operation": never + readonly "result"?: string + readonly "status": "in_progress" | "completed" + readonly "type": never + readonly "call_id": string + readonly "created_by"?: string + } | { + readonly "call_id": string + readonly "id": string + readonly "input": string + readonly "name": string + readonly "namespace"?: string + readonly "type": never + readonly "created_by"?: string + readonly "operation": + | ApplyPatchCreateFileOperation + | ApplyPatchUpdateFileOperation + | ApplyPatchDeleteFileOperation + readonly "status": "in_progress" | "completed" + } + > + readonly "output_text"?: string + readonly "parallel_tool_calls": boolean + readonly "presence_penalty": number | null + readonly "previous_response_id"?: string | null + readonly "prompt"?: StoredPromptTemplate + readonly "prompt_cache_key"?: string | null + readonly "prompt_cache_options"?: PromptCacheOptions + readonly "reasoning"?: BaseReasoningConfig + readonly "safety_identifier"?: string | null + readonly "service_tier"?: "auto" | "default" | "flex" | "priority" | "scale" | null + readonly "status": OpenAIResponsesResponseStatus + readonly "store"?: boolean + readonly "temperature": number | null + readonly "text"?: { + readonly "format"?: + | { readonly "type": "text" } + | { readonly "type": never } + | { + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + readonly "type": never + } + | { readonly "type": never } + | { readonly "type": "json_object" } + | { + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + readonly "type": never + } + | { + readonly "type": never + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + } + | { + readonly "type": never + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + } + | { + readonly "description"?: string + readonly "name": string + readonly "schema": {} + readonly "strict"?: boolean | null + readonly "type": "json_schema" + } + readonly "verbosity"?: "high" | "low" | "medium" | null + } + readonly "tool_choice": OpenAIResponsesToolChoice + readonly "tools": ReadonlyArray< + | { + readonly "description"?: string | null + readonly "name": string + readonly "parameters": { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" + } + | Preview_WebSearchServerTool + | Preview_20250311_WebSearchServerTool + | Legacy_WebSearchServerTool + | WebSearchServerTool + | FileSearchServerTool + | ComputerUseServerTool + | CodeInterpreterServerTool + | McpServerTool + | ImageGenerationServerTool + | CodexLocalShellTool + | ShellServerTool + | ApplyPatchServerTool + | CustomTool + | NamespaceTool + > + readonly "top_logprobs"?: number + readonly "top_p": number | null + readonly "truncation"?: Truncation + readonly "usage"?: { + readonly "input_tokens": number + readonly "input_tokens_details": { readonly "cache_write_tokens"?: number | null; readonly "cached_tokens": number } + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "reasoning_tokens": number } + readonly "total_tokens": number + readonly "cost"?: number | null + readonly "cost_details"?: { + readonly "upstream_inference_cost"?: number | null + readonly "upstream_inference_input_cost": number + readonly "upstream_inference_output_cost": number + } + readonly "is_byok"?: boolean + readonly "server_tool_use_details"?: ServerToolUseDetails + } + readonly "user"?: string | null + readonly "error_type"?: ApiErrorType + readonly "openrouter_metadata"?: OpenRouterMetadata +} +export const OpenResponsesResult = Schema.Struct({ + "background": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "completed_at": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "created_at": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "error": ResponsesErrorField, + "frequency_penalty": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "id": Schema.String, + "incomplete_details": IncompleteDetails, + "instructions": BaseInputs, + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "metadata": RequestMetadata, + "model": Schema.String, + "object": Schema.Literal("response"), + "output": Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey( + Schema.Array( + Schema.Union([ + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_citation") + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "filename": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "url": Schema.String + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Literal("url_citation"), + "url": Schema.String + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "url": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "file_id": Schema.String, + "filename": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Never + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "end_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "start_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "title": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + Schema.Struct({ + "file_id": Schema.String, + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("file_path") + }) + ]) + ]) + ) + ), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Literal("output_text") + }), + Schema.Struct({ + "refusal": Schema.String, + "type": Schema.Never, + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Never, + "refusal": Schema.String + }), + Schema.Struct({ "refusal": Schema.String, "type": Schema.Literal("refusal") }) + ]) + ])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([ + Schema.Literal("commentary").annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }), + Schema.Literal("final_answer").annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }), + Schema.Union([Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Literal("message") + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.Union([Schema.Array(Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Never + }), + Schema.Struct({ "refusal": Schema.String, "type": Schema.Never, "text": Schema.String }) + ]))]), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("incomplete"), + Schema.Literal("in_progress") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.String, + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.Never, + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant") + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ + Schema.Struct({ + "annotations": Schema.optionalKey(Schema.Array(OpenAIResponsesAnnotation)), + "logprobs": Schema.optionalKey(Schema.Array(Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String, + "top_logprobs": Schema.Array( + Schema.Struct({ + "bytes": Schema.Array(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "logprob": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "token": Schema.String + }) + ) + }))), + "text": Schema.String, + "type": Schema.Never + }), + Schema.Struct({ "refusal": Schema.String, "type": Schema.Never, "text": Schema.String }) + ])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Never, + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey( + Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])]) + ), + "id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "summary": Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("summary_text") })), + "type": Schema.Literal("reasoning"), + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("incomplete"), + Schema.Literal("in_progress") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.String, + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.Never), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "content": Schema.optionalKey(Schema.Array(ReasoningTextContent)), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Array(ReasoningSummaryText) + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey( + Schema.Union([Schema.Literal("completed"), Schema.Literal("incomplete"), Schema.Literal("in_progress")]) + ), + "type": Schema.Literal("function_call") + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("incomplete"), + Schema.Literal("in_progress") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.String.annotate({ + "description": "The raw tool-call arguments string as emitted by the model." + }), + "call_id": Schema.String.annotate({ + "description": "The model-generated tool call id from the originating turn." + }), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "arguments": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "arguments": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]).annotate({ "description": "Status of a shell call or its output." }), + "type": Schema.Never, + "arguments": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.optionalKey(Schema.String), + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }), + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.optionalKey(Schema.String), + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("incomplete") + ]), + "type": Schema.Never, + "arguments": Schema.String, + "call_id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "arguments": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])) + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.optionalKey(Schema.Literals(["completed", "incomplete", "in_progress"])), + "type": Schema.Never, + "input": Schema.String + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": WebSearchStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": ImageGenerationStatus, + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Literals(["completed", "incomplete", "in_progress"]), + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "language": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "screenshotB64": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.String.annotate({ + "description": "The model-generated tool call id from the originating turn." + }), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": ToolCallStatus, + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": ApplyPatchCallStatus, + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": ShellCallStatus, + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": ShellCallStatus, + "type": Schema.Never, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.optionalKey(Schema.String), + "key": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.optionalKey(Schema.String), + "serverLabel": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.optionalKey(Schema.String), + "query": Schema.optionalKey(Schema.String), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.optionalKey(Schema.String), + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.optionalKey(Schema.String), + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": ToolCallStatus, + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.optionalKey(Schema.String), + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": ToolCallStatus, + "type": Schema.Never, + "call_id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.optionalKey(Schema.String), + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Literal("custom_tool_call") + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Never, + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)) + }), + Schema.Struct({ + "pattern": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)) + }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Never, "url": Schema.Union([Schema.String]) }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Never, + "pattern": Schema.String, + "url": Schema.String + }), + Schema.Struct({ "type": Schema.Never, "url": Schema.Union([Schema.String]), "pattern": Schema.String }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]), + "type": Schema.Literal("web_search_call") + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }), + Schema.Struct({ + "type": Schema.Never, + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ) + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }), + Schema.Struct({ + "pattern": Schema.String, + "type": Schema.Never, + "url": Schema.String, + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ) + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Never), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.String, + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search"), + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + }), + Schema.Struct({ + "pattern": Schema.String, + "type": Schema.Literal("find_in_page"), + "url": Schema.String, + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + }) + ], { mode: "oneOf" })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Never), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "status": WebSearchStatus + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([ + Schema.Literal("completed"), + Schema.Literal("searching"), + Schema.Literal("in_progress"), + Schema.Literal("failed") + ]), + "type": Schema.Literal("file_search_call") + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed"), Schema.Literal("failed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.String, + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "queries": Schema.Array(Schema.String) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "queries": Schema.Array(Schema.String), + "status": WebSearchStatus + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress"), Schema.Literal("failed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])])), + "status": Schema.Union([ + Schema.Literal("in_progress"), + Schema.Literal("completed"), + Schema.Literal("generating"), + Schema.Literal("failed") + ]), + "type": Schema.Literal("image_generation_call"), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The model-generated tool call id from the originating turn." }) + ), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.optionalKey(Schema.String), + "id": Schema.String, + "operation": Schema.optionalKey(ApplyPatchCallOperation), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": ApplyPatchCallOperation, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.optionalKey( + Schema.String.annotate({ "description": "The file operation performed (list, read, write, or edit)." }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String.annotate({ "description": "JSON-serialized result of the file operation." })]) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": ImageGenerationStatus + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }), + Schema.Union([ + Schema.Struct({ + "content": Schema.Array(Schema.Union([ResponseOutputText, OpenAIResponsesRefusalContent])), + "id": Schema.String, + "phase": Schema.optionalKey( + Schema.Union([Schema.Literal("commentary"), Schema.Literal("final_answer"), Schema.Null]).annotate({ + "description": + "The phase of an assistant message. Use `commentary` for an intermediate assistant message and `final_answer` for the final assistant message. For follow-up requests with models like `gpt-5.3-codex` and later, preserve and resend phase on all assistant messages. Omitting it can degrade performance. Not used for user messages." + }) + ), + "role": Schema.Literal("assistant"), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An output message item" }), + Schema.Struct({ + "content": Schema.optionalKey( + Schema.Union([ + Schema.Array(Schema.Struct({ "text": Schema.String, "type": Schema.Literal("reasoning_text") })) + ]) + ), + "encrypted_content": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "summary": Schema.Array(ReasoningSummaryText), + "type": Schema.Never, + "format": Schema.optionalKey(ReasoningFormat), + "signature": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "A signature for the reasoning content, used for verification" + }) + ), + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An output item containing reasoning" }), + Schema.Struct({ + "arguments": Schema.String, + "call_id": Schema.String, + "id": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Union([ + Schema.Struct({ + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "query": Schema.String, + "sources": Schema.optionalKey(Schema.Array(WebSearchSource)), + "type": Schema.Literal("search") + }), + Schema.Struct({ + "type": Schema.Literal("open_page"), + "url": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])) + }), + Schema.Struct({ "pattern": Schema.String, "type": Schema.Literal("find_in_page"), "url": Schema.String }) + ], { mode: "oneOf" }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.Array(Schema.String), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "id": Schema.String, + "result": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "code": Schema.Union([Schema.String, Schema.Null]), + "container_id": Schema.String, + "id": Schema.String, + "outputs": Schema.Union([ + Schema.Array( + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("image"), "url": Schema.String }), + Schema.Struct({ "logs": Schema.String, "type": Schema.Literal("logs") }) + ]) + ), + Schema.Null + ]), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "A code interpreter execution call with outputs" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "id": Schema.String, + "pending_safety_checks": Schema.Array( + Schema.Struct({ "code": Schema.String, "id": Schema.String, "message": Schema.String }) + ), + "status": Schema.Union([Schema.Literal("completed"), Schema.Literal("in_progress")]), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }), + Schema.Struct({ + "datetime": Schema.String.annotate({ "description": "ISO 8601 datetime string" }), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "timezone": Schema.String.annotate({ "description": "IANA timezone name" }), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:datetime server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey( + Schema.Struct({ + "query": Schema.String, + "sources": Schema.optionalKey( + Schema.Array(Schema.Struct({ "type": Schema.Literal("url"), "url": Schema.String })) + ), + "type": Schema.Literal("search") + }).annotate({ + "description": + "The search action performed, matching OpenAI web_search_call.action shape. Includes the query the model issued and optional source URLs returned by the search provider." + }) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:web_search server tool output item" }), + Schema.Struct({ + "code": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "language": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:code_interpreter server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "queries": Schema.optionalKey(Schema.Array(Schema.String)), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:file_search server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "imageB64": Schema.optionalKey(Schema.String), + "imageUrl": Schema.optionalKey(Schema.String), + "prompt": Schema.optionalKey( + Schema.String.annotate({ + "description": "The prompt (possibly rewritten) that the image was generated from." + }) + ), + "result": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The generated image as a base64-encoded string or URL, matching OpenAI image_generation_call format" + }) + ), + "revisedPrompt": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:image_generation server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.String), + "id": Schema.String, + "screenshotB64": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:browser_use server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ "description": "The raw tool-call arguments string as emitted by the model." }) + ), + "call_id": Schema.String.annotate({ + "description": "The model-generated tool call id from the originating turn." + }), + "command": Schema.optionalKey(Schema.String), + "exitCode": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "stderr": Schema.optionalKey(Schema.String), + "stdout": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:bash server tool output item" }), + Schema.Struct({ + "command": Schema.optionalKey(Schema.Literals(["view", "create", "str_replace", "insert"])), + "filePath": Schema.optionalKey(Schema.String), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:text_editor server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": Schema.Union([ + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Literal("create_file") }) + .annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Never, "diff": Schema.String }).annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Literal("update_file") }) + .annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Never, "diff": Schema.String }).annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Literal("delete_file") }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }) + ], { mode: "oneOf" }) + ], { mode: "oneOf" }).annotate({ + "description": + "The patch operation requested by an `apply_patch_call`. `create_file` and `update_file` carry a V4A diff; `delete_file` omits it." + }), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "An openrouter:apply_patch server tool output item. The turn halts when validation succeeds so the client can apply the patch and echo an `apply_patch_call_output` on the next turn." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "operation": Schema.Union([ + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Literal("create_file") }) + .annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Never, "diff": Schema.String }).annotate({ + "description": + "The `create_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing the new file contents." + }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Literal("update_file") }) + .annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Never, "diff": Schema.String }).annotate({ + "description": + "The `update_file` variant of an `apply_patch_call.operation`. Carries a V4A diff describing edits to an existing file." + }) + ], { mode: "oneOf" }), + Schema.Union([ + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }), + Schema.Struct({ "diff": Schema.String, "path": Schema.String, "type": Schema.Never }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }), + Schema.Struct({ "path": Schema.String, "type": Schema.Literal("delete_file") }).annotate({ + "description": + "The `delete_file` variant of an `apply_patch_call.operation`. Identifies the file to remove; no diff is required." + }) + ], { mode: "oneOf" }) + ], { mode: "oneOf" }).annotate({ + "description": + "The patch operation requested by an `apply_patch_call`. `create_file` and `update_file` carry a V4A diff; `delete_file` omits it." + }), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Lifecycle state of an `apply_patch_call` output item." + }), + "type": Schema.Literal("apply_patch_call"), + "created_by": Schema.optionalKey(Schema.String) + }).annotate({ + "description": + "A native `apply_patch_call` output item matching OpenAI's Responses API shape. Emitted when the client requested the `apply_patch` shorthand." + }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Struct({ + "commands": Schema.Array(Schema.String), + "max_output_length": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "timeout_ms": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]) + })), + "call_id": Schema.String, + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ + "description": + "A native `shell_call` output item matching OpenAI's Responses API shape. Emitted for the sandbox-backed `shell` tool." + }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "max_output_length": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "output": Schema.Array(Schema.Struct({ + "outcome": Schema.Union([ + Schema.Struct({ + "exit_code": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("exit") + }), + Schema.Struct({ "type": Schema.Literal("timeout") }) + ], { mode: "oneOf" }), + "stderr": Schema.String, + "stdout": Schema.String + })), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]).annotate({ + "description": "Status of a shell call or its output." + }), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ + "description": + "A native `shell_call_output` item matching OpenAI's Responses API shape. Carries per-command stdout, stderr, and the exit/timeout outcome." + }), + Schema.Struct({ + "content": Schema.optionalKey(Schema.String), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "The error message if the fetch failed." }) + ), + "httpStatus": Schema.optionalKey( + Schema.Number.annotate({ "description": "The HTTP status code returned by the upstream URL fetch." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "id": Schema.String, + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "title": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "url": Schema.optionalKey(Schema.String), + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:web_fetch server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:tool_search server tool output item" }), + Schema.Struct({ + "action": Schema.optionalKey(Schema.Literals(["read", "write", "delete"])), + "id": Schema.String, + "key": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "value": Schema.optionalKey(Schema.Json.annotate({ "expected": "JSON value" })), + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:memory server tool output item" }), + Schema.Struct({ + "id": Schema.String, + "serverLabel": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "toolName": Schema.optionalKey(Schema.String), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:mcp server tool output item" }), + Schema.Struct({ + "arguments": Schema.optionalKey( + Schema.String.annotate({ + "description": "The JSON arguments submitted to the search tool (e.g. {\"query\":\"Claude\"})" + }) + ), + "id": Schema.String, + "query": Schema.optionalKey(Schema.String), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:experimental__search_models server tool output item" }), + Schema.Struct({ + "analysis": Schema.optionalKey(FusionAnalysisResult), + "error": Schema.optionalKey( + Schema.String.annotate({ + "description": "Error message when the fusion run did not produce an analysis result." + }) + ), + "failed_models": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "error": Schema.String.annotate({ "description": "Error message describing why the model failed." }), + "model": Schema.String.annotate({ "description": "Slug of the analysis model that failed." }), + "status_code": Schema.optionalKey( + Schema.Number.annotate({ + "description": "HTTP status code from the upstream response, when available (e.g. 402, 429)." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + })).annotate({ + "description": + "Models that were requested as part of the analysis panel but did not produce a response. Present when at least one requested analysis model failed. The fusion result is still usable but was produced from a degraded panel." + }) + ), + "failure_reason": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Typed failure reason when the fusion run failed. Possible values include: all_panels_failed, insufficient_credits, rate_limited, judge_not_valid_json, judge_schema_mismatch, judge_upstream_error, judge_empty_completion." + }) + ), + "id": Schema.String, + "responses": Schema.optionalKey( + Schema.Array(Schema.Struct({ "content": Schema.optionalKey(Schema.String), "model": Schema.String })) + .annotate({ + "description": + "Analysis models that produced a response in this fusion run, with each model's full panel content." + }) + ), + "sources": Schema.optionalKey( + Schema.Array(FusionSource).annotate({ + "description": + "Web pages the analysis panels and judge retrieved via web search during this fusion run, deduplicated by URL across the whole run. Present when at least one model cited a source." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:fusion server tool output item" }), + Schema.Struct({ + "advice": Schema.optionalKey( + Schema.String.annotate({ + "description": "The advisor model's response (the advice text returned to the executor)." + }) + ), + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the advisor call did not produce advice." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific advisor instance that produced this item (e.g. `openrouter_advisor__1`). Present only when more than one advisor tool is configured; omitted for the default single advisor. Echo this field back unchanged so the advisor's cross-request memory stays namespaced to the correct instance. This identity is positional: it is derived from the index of the advisor entry in the request `tools` array, so clients must keep the order of advisor tool entries stable across requests in a conversation. Reordering or inserting advisor entries shifts these names and causes each advisor's cross-request memory to be attributed to the wrong instance." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the advisor model that was consulted." }) + ), + "prompt": Schema.optionalKey( + Schema.String.annotate({ "description": "The prompt the executor sent to the advisor." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:advisor server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the subagent task did not produce an outcome." }) + ), + "id": Schema.String, + "instance_name": Schema.optionalKey(Schema.String.annotate({ + "description": + "Provider-safe function name of the specific subagent instance that produced this item (e.g. `openrouter_subagent__1`). Present only on items from non-default instances — the second and later subagent entries in the request `tools` array. The first (default) instance omits it, even when multiple subagents are configured. When a replayed item echoes this field back, the transcript rehydrates the call under that instance's tool. This identity is positional: it is derived from the index of the subagent entry in the request `tools` array, so keep the order of subagent entries stable across requests in a conversation." + })), + "model": Schema.optionalKey( + Schema.String.annotate({ "description": "Slug of the worker model that executed the task." }) + ), + "name": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Configured name of the subagent that executed the task (the `name` on its tool entry). Present only for named subagents; omitted for an unnamed (default) subagent." + }) + ), + "outcome": Schema.optionalKey( + Schema.String.annotate({ + "description": "The worker model's result (the outcome text returned to the delegating model)." + }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "task_description": Schema.optionalKey( + Schema.String.annotate({ "description": "The task description the delegating model sent to the worker." }) + ), + "task_name": Schema.optionalKey( + Schema.String.annotate({ "description": "The short task identifier the delegating model supplied." }) + ), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }) + }).annotate({ "description": "An openrouter:subagent server tool output item" }), + Schema.Struct({ + "error": Schema.optionalKey( + Schema.String.annotate({ "description": "Error message when the file operation failed." }) + ), + "file_id": Schema.optionalKey( + Schema.String.annotate({ "description": "The target file id supplied in the tool-call arguments." }) + ), + "filename": Schema.optionalKey( + Schema.String.annotate({ "description": "The target filename supplied in the tool-call arguments." }) + ), + "id": Schema.String, + "operation": Schema.Never, + "result": Schema.optionalKey( + Schema.String.annotate({ "description": "JSON-serialized result of the file operation." }) + ), + "status": Schema.Union([Schema.Literal("in_progress"), Schema.Literal("completed")]), + "type": Schema.Never, + "call_id": Schema.String, + "created_by": Schema.optionalKey(Schema.String) + }).annotate({ "description": "An openrouter:files server tool output item" }), + Schema.Struct({ + "call_id": Schema.String, + "id": Schema.String, + "input": Schema.String, + "name": Schema.String, + "namespace": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Namespace qualifier for tools registered as part of a namespace tool group (e.g. an MCP server)" + }) + ), + "type": Schema.Never, + "created_by": Schema.optionalKey(Schema.String), + "operation": Schema.Union([ + ApplyPatchCreateFileOperation, + ApplyPatchUpdateFileOperation, + ApplyPatchDeleteFileOperation + ], { mode: "oneOf" }), + "status": Schema.Literals(["in_progress", "completed"]) + }).annotate({ + "description": + "A call to a custom (freeform-grammar) tool created by the model — distinct from `function_call`. Used for tools like Codex CLI's `apply_patch` whose payload is opaque text rather than JSON arguments." + }) + ], { mode: "oneOf" }).annotate({ "description": "An output item from the response" }) + ], { mode: "oneOf" })), + "output_text": Schema.optionalKey(Schema.String), + "parallel_tool_calls": Schema.Boolean, + "presence_penalty": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "previous_response_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt": Schema.optionalKey(StoredPromptTemplate), + "prompt_cache_key": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt_cache_options": Schema.optionalKey(PromptCacheOptions), + "reasoning": Schema.optionalKey(BaseReasoningConfig), + "safety_identifier": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "service_tier": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("default"), + Schema.Literal("flex"), + Schema.Literal("priority"), + Schema.Literal("scale"), + Schema.Union([Schema.Null]) + ]) + ), + "status": OpenAIResponsesResponseStatus, + "store": Schema.optionalKey(Schema.Boolean), + "temperature": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "text": Schema.optionalKey( + Schema.Struct({ + "format": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.Struct({ "type": Schema.Literal("text") }).annotate({ "description": "Plain text response format" }), + Schema.Struct({ "type": Schema.Never }).annotate({ "description": "Plain text response format" }), + Schema.Struct({ + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Never + }).annotate({ "description": "Plain text response format" }) + ]).annotate({ "description": "Text response format configuration" }), + Schema.Union([ + Schema.Struct({ "type": Schema.Never }).annotate({ "description": "JSON object response format" }), + Schema.Struct({ "type": Schema.Literal("json_object") }).annotate({ + "description": "JSON object response format" + }), + Schema.Struct({ + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Never + }).annotate({ "description": "JSON object response format" }) + ]).annotate({ "description": "Text response format configuration" }), + Schema.Union([ + Schema.Struct({ + "type": Schema.Never, + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) + }).annotate({ "description": "JSON schema constrained response format" }), + Schema.Struct({ + "type": Schema.Never, + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])) + }).annotate({ "description": "JSON schema constrained response format" }), + Schema.Struct({ + "description": Schema.optionalKey(Schema.String), + "name": Schema.String, + "schema": Schema.Struct({}), + "strict": Schema.optionalKey(Schema.Union([Schema.Union([Schema.Boolean]), Schema.Union([Schema.Null])])), + "type": Schema.Literal("json_schema") + }).annotate({ "description": "JSON schema constrained response format" }) + ]).annotate({ "description": "Text response format configuration" }) + ]).annotate({ "description": "Text response format configuration" }) + ), + "verbosity": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Literal("high")]), + Schema.Union([Schema.Literal("low")]), + Schema.Union([Schema.Literal("medium")]), + Schema.Union([Schema.Union([Schema.Null])]) + ]) + ) + }).annotate({ "description": "Text output configuration including format and verbosity" }) + ), + "tool_choice": OpenAIResponsesToolChoice, + "tools": Schema.Array(Schema.Union([ + Schema.Struct({ + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "parameters": Schema.Union([ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), + Schema.Null + ]), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") + }).annotate({ "description": "Function tool definition" }), + Preview_WebSearchServerTool, + Preview_20250311_WebSearchServerTool, + Legacy_WebSearchServerTool, + WebSearchServerTool, + FileSearchServerTool, + ComputerUseServerTool, + CodeInterpreterServerTool, + McpServerTool, + ImageGenerationServerTool, + CodexLocalShellTool, + ShellServerTool, + ApplyPatchServerTool, + CustomTool, + NamespaceTool + ], { mode: "oneOf" })), + "top_logprobs": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "top_p": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }), + "truncation": Schema.optionalKey(Truncation), + "usage": Schema.optionalKey( + Schema.Union([Schema.Struct({ + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "input_tokens_details": Schema.Struct({ + "cache_write_tokens": Schema.optionalKey( + Schema.Union([ + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))]), + Schema.Union([Schema.Null]) + ]) + ), + "cached_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": Schema.Struct({ + "reasoning_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + "total_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Cost of the completion", "format": "double" }) + ), + "cost_details": Schema.optionalKey(Schema.Struct({ + "upstream_inference_cost": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "format": "double" }) + ), + "upstream_inference_input_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "upstream_inference_output_cost": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + })), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Whether a request was made using a Bring Your Own Key configuration" + }) + ), + "server_tool_use_details": Schema.optionalKey(ServerToolUseDetails) + })]).annotate({ "description": "Token usage information for the response" }) + ), + "user": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "error_type": Schema.optionalKey(ApiErrorType), + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata) +}).annotate({ + "description": "Complete non-streaming response from the Responses API", + "identifier": "OpenResponsesResult" +}) +export type StreamEventsResponseOutputItemAdded = { + readonly "item": OutputItems + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_item.added" +} +export const StreamEventsResponseOutputItemAdded = Schema.Struct({ + "item": OutputItems, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_item.added") +}).annotate({ + "description": "Event emitted when a new output item is added to the response", + "identifier": "StreamEventsResponseOutputItemAdded" +}) +export type StreamEventsResponseOutputItemDone = { + readonly "item": OutputItems + readonly "output_index": number + readonly "sequence_number": number + readonly "type": "response.output_item.done" +} +export const StreamEventsResponseOutputItemDone = Schema.Struct({ + "item": OutputItems, + "output_index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.output_item.done") +}).annotate({ + "description": "Event emitted when an output item is complete", + "identifier": "StreamEventsResponseOutputItemDone" +}) +export type MessagesContentBlockStartEvent = { + readonly "content_block": + | AnthropicTextBlock + | AnthropicToolUseBlock + | AnthropicThinkingBlock + | AnthropicRedactedThinkingBlock + | ORAnthropicServerToolUseBlock + | AnthropicWebSearchToolResult + | AnthropicWebFetchToolResult + | AnthropicCodeExecutionToolResult + | AnthropicBashCodeExecutionToolResult + | AnthropicTextEditorCodeExecutionToolResult + | AnthropicToolSearchToolResult + | AnthropicContainerUpload + | AnthropicCompactionBlock + | AnthropicAdvisorToolResult + | { readonly "content": string | null; readonly "type": "compaction" } + readonly "index": number + readonly "type": "content_block_start" +} +export const MessagesContentBlockStartEvent = Schema.Struct({ + "content_block": Schema.Union([ + AnthropicTextBlock, + AnthropicToolUseBlock, + AnthropicThinkingBlock, + AnthropicRedactedThinkingBlock, + ORAnthropicServerToolUseBlock, + AnthropicWebSearchToolResult, + AnthropicWebFetchToolResult, + AnthropicCodeExecutionToolResult, + AnthropicBashCodeExecutionToolResult, + AnthropicTextEditorCodeExecutionToolResult, + AnthropicToolSearchToolResult, + AnthropicContainerUpload, + AnthropicCompactionBlock, + AnthropicAdvisorToolResult, + Schema.Struct({ "content": Schema.Union([Schema.String, Schema.Null]), "type": Schema.Literal("compaction") }) + ]), + "index": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("content_block_start") +}).annotate({ + "description": "Event sent when a new content block starts", + "identifier": "MessagesContentBlockStartEvent" +}) +export type ORAnthropicContentBlock = + | AnthropicTextBlock + | AnthropicToolUseBlock + | AnthropicThinkingBlock + | AnthropicRedactedThinkingBlock + | ORAnthropicServerToolUseBlock + | AnthropicWebSearchToolResult + | AnthropicWebFetchToolResult + | AnthropicCodeExecutionToolResult + | AnthropicBashCodeExecutionToolResult + | AnthropicTextEditorCodeExecutionToolResult + | AnthropicToolSearchToolResult + | AnthropicContainerUpload + | AnthropicCompactionBlock + | AnthropicAdvisorToolResult +export const ORAnthropicContentBlock = Schema.Union([ + AnthropicTextBlock, + AnthropicToolUseBlock, + AnthropicThinkingBlock, + AnthropicRedactedThinkingBlock, + ORAnthropicServerToolUseBlock, + AnthropicWebSearchToolResult, + AnthropicWebFetchToolResult, + AnthropicCodeExecutionToolResult, + AnthropicBashCodeExecutionToolResult, + AnthropicTextEditorCodeExecutionToolResult, + AnthropicToolSearchToolResult, + AnthropicContainerUpload, + AnthropicCompactionBlock, + AnthropicAdvisorToolResult +], { mode: "oneOf" }).annotate({ "identifier": "ORAnthropicContentBlock" }) +export type ImageGenCompletedEvent = { + readonly "b64_json": string + readonly "created": number + readonly "media_type"?: string + readonly "type": "image_generation.completed" + readonly "usage"?: ImageGenerationUsage +} +export const ImageGenCompletedEvent = Schema.Struct({ + "b64_json": Schema.String.annotate({ "description": "Base64-encoded final image data" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp (seconds) when the image was generated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "media_type": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Media type (MIME type) of the image, e.g. `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`. May be omitted if the format could not be determined." + }) + ), + "type": Schema.Literal("image_generation.completed").annotate({ "description": "The event type" }), + "usage": Schema.optionalKey(ImageGenerationUsage) +}).annotate({ + "description": "Emitted when generation completes and the final image is available", + "identifier": "ImageGenCompletedEvent" +}) +export type ImageGenerationResponse = { + readonly "created": number + readonly "data": ReadonlyArray<{ readonly "b64_json": string; readonly "media_type"?: string }> + readonly "usage"?: ImageGenerationUsage +} +export const ImageGenerationResponse = Schema.Struct({ + "created": Schema.Number.annotate({ "description": "Unix timestamp (seconds) when the image was generated" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "data": Schema.Array(Schema.Struct({ + "b64_json": Schema.String.annotate({ "description": "Base64-encoded image bytes" }), + "media_type": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Media type (MIME type) of the image, e.g. `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`. May be omitted if the format could not be determined." + }) + ) + })).annotate({ "description": "Generated images" }), + "usage": Schema.optionalKey(ImageGenerationUsage) +}).annotate({ "description": "Image generation response", "identifier": "ImageGenerationResponse" }) +export type ChatResult = { + readonly "choices": ReadonlyArray + readonly "created": number + readonly "id": string + readonly "model": string + readonly "object": "chat.completion" + readonly "openrouter_metadata"?: OpenRouterMetadata + readonly "service_tier"?: string | null + readonly "system_fingerprint": string | null + readonly "usage"?: ChatUsage +} +export const ChatResult = Schema.Struct({ + "choices": Schema.Array(ChatChoice).annotate({ "description": "List of completion choices" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of creation" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "id": Schema.String.annotate({ "description": "Unique completion identifier" }), + "model": Schema.String.annotate({ "description": "Model used for completion" }), + "object": Schema.Literal("chat.completion"), + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata), + "service_tier": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The service tier used by the upstream provider for this request" + }) + ), + "system_fingerprint": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "System fingerprint" }), + "usage": Schema.optionalKey(ChatUsage) +}).annotate({ "description": "Chat completion response", "identifier": "ChatResult" }) +export type ChatRequest = { + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "debug"?: ChatDebugOptions + readonly "frequency_penalty"?: number | null + readonly "image_config"?: ImageConfig + readonly "logit_bias"?: { readonly [x: string]: number } | null + readonly "logprobs"?: boolean | null + readonly "max_completion_tokens"?: number | null + readonly "max_tokens"?: number | null + readonly "messages": ReadonlyArray + readonly "metadata"?: {} + readonly "min_p"?: number | null + readonly "modalities"?: ReadonlyArray<"text" | "image" | "audio"> + readonly "model"?: ModelName + readonly "models"?: ChatModelNames + readonly "parallel_tool_calls"?: boolean | null + readonly "plugins"?: ReadonlyArray< + | AutoRouterPlugin + | AutoBetaRouterPlugin + | ModerationPlugin + | WebSearchPlugin + | WebFetchPlugin + | FileParserPlugin + | ResponseHealingPlugin + | ContextCompressionPlugin + | ParetoRouterPlugin + | FusionPlugin + > + readonly "prediction"?: Prediction + readonly "presence_penalty"?: number | null + readonly "prompt_cache_key"?: string | null + readonly "prompt_cache_options"?: PromptCacheOptions + readonly "provider"?: ProviderPreferences + readonly "reasoning"?: { + readonly "effort"?: "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" | null + readonly "summary"?: ChatReasoningSummaryVerbosityEnum + } + readonly "reasoning_effort"?: "max" | "xhigh" | "high" | "medium" | "low" | "minimal" | "none" | null + readonly "repetition_penalty"?: number | null + readonly "response_format"?: + | ChatFormatTextConfig + | ChatFormatJsonObjectConfig + | ChatFormatJsonSchemaConfig + | ChatFormatGrammarConfig + | ChatFormatPythonConfig + readonly "route"?: DeprecatedRoute + readonly "seed"?: number | null + readonly "service_tier"?: "auto" | "default" | "flex" | "priority" | "scale" | null + readonly "session_id"?: string + readonly "stop"?: string | ReadonlyArray | null + readonly "stop_server_tools_when"?: StopServerToolsWhen + readonly "stream"?: boolean + readonly "stream_options"?: ChatStreamOptions + readonly "temperature"?: number | null + readonly "tool_choice"?: ChatToolChoice + readonly "tools"?: ReadonlyArray + readonly "top_a"?: number | null + readonly "top_k"?: number | null + readonly "top_logprobs"?: number | null + readonly "top_p"?: number | null + readonly "trace"?: TraceConfig + readonly "user"?: string +} +export const ChatRequest = Schema.Struct({ + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "debug": Schema.optionalKey(ChatDebugOptions), + "frequency_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Frequency penalty (-2.0 to 2.0)", "format": "double" }) + ), + "image_config": Schema.optionalKey(ImageConfig), + "logit_bias": Schema.optionalKey( + Schema.Union([ + Schema.Record( + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + Schema.Null + ]).annotate({ "description": "Token logit bias adjustments" }) + ), + "logprobs": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ "description": "Return log probabilities" }) + ), + "max_completion_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Maximum tokens in completion" + }) + ), + "max_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": + "Maximum tokens (deprecated, use max_completion_tokens). Note: some providers enforce a minimum of 16." + }) + ), + "messages": Schema.Array(ChatMessages).annotate({ "description": "List of messages for the conversation" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "metadata": Schema.optionalKey( + Schema.Struct({}).annotate({ + "description": "Key-value pairs for additional object information (max 16 pairs, 64 char keys, 512 char values)" + }) + ), + "min_p": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ + "description": + "Minimum probability threshold relative to the most likely token. Tokens with probability below min_p * (probability of top token) are filtered out. Not all providers support this parameter.", + "format": "double" + }) + ), + "modalities": Schema.optionalKey( + Schema.Array(Schema.Literals(["text", "image", "audio"])).annotate({ + "description": "Output modalities for the response. Supported values are \"text\", \"image\", and \"audio\"." + }) + ), + "model": Schema.optionalKey(ModelName), + "models": Schema.optionalKey(ChatModelNames), + "parallel_tool_calls": Schema.optionalKey( + Schema.Union([Schema.Boolean, Schema.Null]).annotate({ + "description": + "Whether to enable parallel function calling during tool use. When true, the model may generate multiple tool calls in a single response." + }) + ), + "plugins": Schema.optionalKey( + Schema.Array( + Schema.Union([ + AutoRouterPlugin, + AutoBetaRouterPlugin, + ModerationPlugin, + WebSearchPlugin, + WebFetchPlugin, + FileParserPlugin, + ResponseHealingPlugin, + ContextCompressionPlugin, + ParetoRouterPlugin, + FusionPlugin + ], { mode: "oneOf" }) + ).annotate({ "description": "Plugins you want to enable for this request, including their settings." }) + ), + "prediction": Schema.optionalKey(Prediction), + "presence_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Presence penalty (-2.0 to 2.0)", "format": "double" }) + ), + "prompt_cache_key": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt_cache_options": Schema.optionalKey(PromptCacheOptions), + "provider": Schema.optionalKey(ProviderPreferences), + "reasoning": Schema.optionalKey( + Schema.Struct({ + "effort": Schema.optionalKey( + Schema.Union([ + Schema.Literal("max"), + Schema.Literal("xhigh"), + Schema.Literal("high"), + Schema.Literal("medium"), + Schema.Literal("low"), + Schema.Literal("minimal"), + Schema.Literal("none"), + Schema.Null + ]).annotate({ "description": "Constrains effort on reasoning for reasoning models" }) + ), + "summary": Schema.optionalKey(ChatReasoningSummaryVerbosityEnum) + }).annotate({ "description": "Configuration options for reasoning models" }) + ), + "reasoning_effort": Schema.optionalKey( + Schema.Union([ + Schema.Literal("max"), + Schema.Literal("xhigh"), + Schema.Literal("high"), + Schema.Literal("medium"), + Schema.Literal("low"), + Schema.Literal("minimal"), + Schema.Literal("none"), + Schema.Null + ]).annotate({ + "description": + "Shorthand for setting reasoning effort. Equivalent to setting reasoning.effort. Cannot be used simultaneously with reasoning.effort if they differ." + }) + ), + "repetition_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ + "description": + "Penalizes tokens based on how much they have already appeared in the text. A value of 1.0 means no penalty. Values above 1.0 penalize repeated tokens more strongly. Not all providers support this parameter.", + "format": "double" + }) + ), + "response_format": Schema.optionalKey( + Schema.Union([ + ChatFormatTextConfig, + ChatFormatJsonObjectConfig, + ChatFormatJsonSchemaConfig, + ChatFormatGrammarConfig, + ChatFormatPythonConfig + ], { mode: "oneOf" }).annotate({ "description": "Response format configuration" }) + ), + "route": Schema.optionalKey(DeprecatedRoute), + "seed": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Random seed for deterministic outputs" + }) + ), + "service_tier": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("default"), + Schema.Literal("flex"), + Schema.Literal("priority"), + Schema.Literal("scale"), + Schema.Null + ]).annotate({ "description": "The service tier to use for processing this request." }) + ), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow). When provided, OpenRouter uses it as the sticky routing key, routing all requests in the session to the same provider to maximize prompt cache hits. Also used for observability grouping. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 256 characters." + }).check(Schema.isMaxLength(256).annotate({ "expected": "a value with a length of at most 256" })) + ), + "stop": Schema.optionalKey( + Schema.Union([ + Schema.String, + Schema.Array(Schema.String).check( + Schema.isMaxLength(4).annotate({ "expected": "a value with a length of at most 4" }) + ), + Schema.Null + ]).annotate({ "description": "Stop sequences (up to 4)" }) + ), + "stop_server_tools_when": Schema.optionalKey(StopServerToolsWhen), + "stream": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Enable streaming response" })), + "stream_options": Schema.optionalKey(ChatStreamOptions), + "temperature": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Sampling temperature (0-2)", "format": "double" }) + ), + "tool_choice": Schema.optionalKey(ChatToolChoice), + "tools": Schema.optionalKey( + Schema.Array(ChatFunctionTool).annotate({ "description": "Available tools for function calling" }) + ), + "top_a": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ + "description": + "Consider only tokens with \"sufficiently high\" probabilities based on the probability of the most likely token. Not all providers support this parameter.", + "format": "double" + }) + ), + "top_k": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": + "Limits the model to choose from the top K most likely tokens at each step. A value of 1 means the model will always pick the most likely next token. Not all providers support this parameter." + }) + ), + "top_logprobs": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": "Number of top log probabilities to return (0-20)" + }) + ), + "top_p": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Nucleus sampling parameter (0-1)", "format": "double" }) + ), + "trace": Schema.optionalKey(TraceConfig), + "user": Schema.optionalKey(Schema.String.annotate({ + "description": + "Per-end-user identifier for abuse isolation. Use a stable ID, hash, or pseudonym. When a provider requires a user identity, OpenRouter folds it into the hashed identity sent upstream and never forwards it raw. If omitted, requests use an account-level identity, so provider policy blocks can affect the whole account." + })) +}).annotate({ "description": "Chat completion request parameters", "identifier": "ChatRequest" }) +export type ChatStreamChunk = { + readonly "choices": ReadonlyArray + readonly "created": number + readonly "error"?: { + readonly "code": number + readonly "message": string + readonly "metadata"?: { readonly "error_type": ApiErrorType; readonly "provider_code"?: string } + } + readonly "id": string + readonly "model": string + readonly "object": "chat.completion.chunk" + readonly "openrouter_metadata"?: OpenRouterMetadata + readonly "service_tier"?: string | null + readonly "system_fingerprint"?: string + readonly "usage"?: ChatUsage +} +export const ChatStreamChunk = Schema.Struct({ + "choices": Schema.Array(ChatStreamChoice).annotate({ "description": "List of streaming chunk choices" }), + "created": Schema.Number.annotate({ "description": "Unix timestamp of creation" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "error": Schema.optionalKey( + Schema.Struct({ + "code": Schema.Number.annotate({ "description": "Error code", "format": "int32" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "message": Schema.String.annotate({ "description": "Error message" }), + "metadata": Schema.optionalKey( + Schema.Struct({ + "error_type": ApiErrorType, + "provider_code": Schema.optionalKey( + Schema.String.annotate({ "description": "Upstream provider-specific error code, when available" }) + ) + }).annotate({ "description": "Structured error metadata" }) + ) + }).annotate({ "description": "Error information" }) + ), + "id": Schema.String.annotate({ "description": "Unique chunk identifier" }), + "model": Schema.String.annotate({ "description": "Model used for completion" }), + "object": Schema.Literal("chat.completion.chunk"), + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata), + "service_tier": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "The service tier used by the upstream provider for this request" + }) + ), + "system_fingerprint": Schema.optionalKey(Schema.String.annotate({ "description": "System fingerprint" })), + "usage": Schema.optionalKey(ChatUsage) +}).annotate({ "description": "Streaming chat completion chunk", "identifier": "ChatStreamChunk" }) +export type Inputs = string | Arrays_8 +export const Inputs = Schema.Union([Schema.String, Arrays_8]).annotate({ + "description": "Input for a response request - can be a string or array of items", + "identifier": "Inputs" +}) +export type CompletedEvent = { + readonly "response": BaseResponsesResult + readonly "sequence_number": number + readonly "type": "response.completed" +} +export const CompletedEvent = Schema.Struct({ + "response": BaseResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.completed") +}).annotate({ + "description": "Event emitted when a response has completed successfully", + "identifier": "CompletedEvent" +}) +export type CreatedEvent = { + readonly "response": BaseResponsesResult + readonly "sequence_number": number + readonly "type": "response.created" +} +export const CreatedEvent = Schema.Struct({ + "response": BaseResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.created") +}).annotate({ "description": "Event emitted when a response is created", "identifier": "CreatedEvent" }) +export type FailedEvent = { + readonly "response": BaseResponsesResult + readonly "sequence_number": number + readonly "type": "response.failed" +} +export const FailedEvent = Schema.Struct({ + "response": BaseResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.failed") +}).annotate({ "description": "Event emitted when a response has failed", "identifier": "FailedEvent" }) +export type IncompleteEvent = { + readonly "response": BaseResponsesResult + readonly "sequence_number": number + readonly "type": "response.incomplete" +} +export const IncompleteEvent = Schema.Struct({ + "response": BaseResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.incomplete") +}).annotate({ "description": "Event emitted when a response is incomplete", "identifier": "IncompleteEvent" }) +export type InProgressEvent = { + readonly "response": BaseResponsesResult + readonly "sequence_number": number + readonly "type": "response.in_progress" +} +export const InProgressEvent = Schema.Struct({ + "response": BaseResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.in_progress") +}).annotate({ "description": "Event emitted when a response is in progress", "identifier": "InProgressEvent" }) +export type OpenResponsesCreatedEvent = { + readonly "response": OpenResponsesResult + readonly "sequence_number": number + readonly "type": "response.created" +} +export const OpenResponsesCreatedEvent = Schema.Struct({ + "response": OpenResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.created") +}).annotate({ "description": "Event emitted when a response is created", "identifier": "OpenResponsesCreatedEvent" }) +export type OpenResponsesInProgressEvent = { + readonly "response": OpenResponsesResult + readonly "sequence_number": number + readonly "type": "response.in_progress" +} +export const OpenResponsesInProgressEvent = Schema.Struct({ + "response": OpenResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.in_progress") +}).annotate({ + "description": "Event emitted when a response is in progress", + "identifier": "OpenResponsesInProgressEvent" +}) +export type StreamEventsResponseCompleted = { + readonly "response": OpenResponsesResult + readonly "sequence_number": number + readonly "type": "response.completed" +} +export const StreamEventsResponseCompleted = Schema.Struct({ + "response": OpenResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.completed") +}).annotate({ + "description": "Event emitted when a response has completed successfully", + "identifier": "StreamEventsResponseCompleted" +}) +export type StreamEventsResponseFailed = { + readonly "response": OpenResponsesResult + readonly "sequence_number": number + readonly "type": "response.failed" +} +export const StreamEventsResponseFailed = Schema.Struct({ + "response": OpenResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.failed") +}).annotate({ "description": "Event emitted when a response has failed", "identifier": "StreamEventsResponseFailed" }) +export type StreamEventsResponseIncomplete = { + readonly "response": OpenResponsesResult + readonly "sequence_number": number + readonly "type": "response.incomplete" +} +export const StreamEventsResponseIncomplete = Schema.Struct({ + "response": OpenResponsesResult, + "sequence_number": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "type": Schema.Literal("response.incomplete") +}).annotate({ + "description": "Event emitted when a response is incomplete", + "identifier": "StreamEventsResponseIncomplete" +}) +export type BaseMessagesResult = { + readonly "container": AnthropicContainer + readonly "content": ReadonlyArray + readonly "id": string + readonly "model": string + readonly "role": "assistant" + readonly "stop_details": AnthropicRefusalStopDetails + readonly "stop_reason": ORAnthropicStopReason + readonly "stop_sequence": string | null + readonly "type": "message" + readonly "usage": { + readonly "cache_creation": AnthropicCacheCreation + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "output_tokens": number + readonly "output_tokens_details": AnthropicOutputTokensDetails + readonly "server_tool_use": AnthropicServerToolUsage + readonly "service_tier": AnthropicServiceTier + readonly "iterations"?: ReadonlyArray + readonly "speed"?: AnthropicSpeed + } +} +export const BaseMessagesResult = Schema.Struct({ + "container": AnthropicContainer, + "content": Schema.Array(ORAnthropicContentBlock), + "id": Schema.String, + "model": Schema.String, + "role": Schema.Literal("assistant"), + "stop_details": AnthropicRefusalStopDetails, + "stop_reason": ORAnthropicStopReason, + "stop_sequence": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("message"), + "usage": Schema.Struct({ + "cache_creation": AnthropicCacheCreation, + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "inference_geo": Schema.Union([Schema.String, Schema.Null]), + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": AnthropicOutputTokensDetails, + "server_tool_use": AnthropicServerToolUsage, + "service_tier": AnthropicServiceTier, + "iterations": Schema.optionalKey(Schema.Array(AnthropicUsageIteration)), + "speed": Schema.optionalKey(AnthropicSpeed) + }) +}).annotate({ + "description": "Base Anthropic Messages API response before OpenRouter extensions", + "identifier": "BaseMessagesResult" +}) +export type MessagesResult = { + readonly "container": AnthropicContainer + readonly "content": ReadonlyArray + readonly "id": string + readonly "model": string + readonly "role": "assistant" + readonly "stop_details": AnthropicRefusalStopDetails + readonly "stop_reason": ORAnthropicStopReason + readonly "stop_sequence": string | null + readonly "type": "message" + readonly "usage": { + readonly "cache_creation": { + readonly "ephemeral_1h_input_tokens": number + readonly "ephemeral_5m_input_tokens": number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "output_tokens": number + readonly "output_tokens_details": { readonly "thinking_tokens": number; readonly [x: string]: Schema.Json } | null + readonly "server_tool_use": { + readonly "web_fetch_requests": number + readonly "web_search_requests": number + readonly [x: string]: Schema.Json + } | null + readonly "service_tier": "standard" | "priority" | "batch" | null + readonly "iterations"?: ReadonlyArray< + { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "compaction" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model"?: string + readonly "type": never + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": never + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "compaction" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": never + readonly "model"?: string + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model"?: string + readonly "type": "message" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": never + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "message" + readonly "model"?: string + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": never + readonly "model": string + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": never + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": "advisor_message" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "advisor_message" + readonly "model": string + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": "compaction" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model"?: string + readonly "type": "message" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "model": string + readonly "type": "advisor_message" + } | { + readonly "cache_creation"?: { + readonly "ephemeral_1h_input_tokens"?: number + readonly "ephemeral_5m_input_tokens"?: number + readonly [x: string]: Schema.Json + } | null + readonly "cache_creation_input_tokens"?: number + readonly "cache_read_input_tokens"?: number + readonly "input_tokens"?: number + readonly "output_tokens"?: number + readonly "type": string + } + > + readonly "speed"?: "fast" | "standard" | null + readonly "cost"?: number | null + readonly "cost_details"?: CostDetails + readonly "is_byok"?: boolean + } + readonly "context_management"?: { + readonly "applied_edits": ReadonlyArray<{ readonly "type": string }> + readonly [x: string]: Schema.Json + } | null + readonly "openrouter_metadata"?: OpenRouterMetadata + readonly "provider"?: ProviderName +} +export const MessagesResult = Schema.Struct({ + "container": AnthropicContainer, + "content": Schema.Array(ORAnthropicContentBlock), + "id": Schema.String, + "model": Schema.String, + "role": Schema.Literal("assistant"), + "stop_details": AnthropicRefusalStopDetails, + "stop_reason": ORAnthropicStopReason, + "stop_sequence": Schema.Union([Schema.String, Schema.Null]), + "type": Schema.Literal("message"), + "usage": Schema.Struct({ + "cache_creation": Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "ephemeral_5m_input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]), + "cache_creation_input_tokens": Schema.Union([ + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))]), + Schema.Union([Schema.Null]) + ]), + "cache_read_input_tokens": Schema.Union([ + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))]), + Schema.Union([Schema.Null]) + ]), + "inference_geo": Schema.Union([Schema.Union([Schema.String]), Schema.Union([Schema.Null])]), + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "thinking_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]), + "server_tool_use": Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "web_fetch_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "web_search_requests": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]), + "service_tier": Schema.Union([ + Schema.Union([Schema.Literal("standard")]), + Schema.Union([Schema.Literal("priority")]), + Schema.Union([Schema.Literal("batch")]), + Schema.Union([Schema.Union([Schema.Null])]) + ]), + "iterations": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Union([ + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Literal("compaction") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.optionalKey(Schema.String), + "type": Schema.Never + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.String, + "type": Schema.Never + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Literal("compaction") + }) + ]), + Schema.Union([ + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Never, + "model": Schema.optionalKey(Schema.String) + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.optionalKey(Schema.String), + "type": Schema.Literal("message") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.String, + "type": Schema.Never + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Literal("message"), + "model": Schema.optionalKey(Schema.String) + }) + ]), + Schema.Union([ + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Never, + "model": Schema.String + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.String, + "type": Schema.Never + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.String, + "type": Schema.Literal("advisor_message") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Literal("advisor_message"), + "model": Schema.String + }) + ]), + Schema.Union([ + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.Literal("compaction") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.optionalKey(Schema.String), + "type": Schema.Literal("message") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "model": Schema.String, + "type": Schema.Literal("advisor_message") + }), + Schema.Struct({ + "cache_creation": Schema.optionalKey( + Schema.Union([ + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ + "ephemeral_1h_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "ephemeral_5m_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ) + }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ) + ]), + Schema.Union([Schema.Null]) + ]) + ), + "cache_creation_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "cache_read_input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "input_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "output_tokens": Schema.optionalKey( + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "type": Schema.String + }) + ]) + ]))), + "speed": Schema.optionalKey( + Schema.Union([Schema.Literal("fast"), Schema.Literal("standard"), Schema.Union([Schema.Null])]) + ), + "cost": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "cost_details": Schema.optionalKey(CostDetails), + "is_byok": Schema.optionalKey(Schema.Boolean) + }), + "context_management": Schema.optionalKey( + Schema.Union([ + Schema.StructWithRest( + Schema.Struct({ "applied_edits": Schema.Array(Schema.Struct({ "type": Schema.String })) }), + [Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" }))] + ), + Schema.Null + ]) + ), + "openrouter_metadata": Schema.optionalKey(OpenRouterMetadata), + "provider": Schema.optionalKey(ProviderName) +}).annotate({ + "description": "Non-streaming response from the Anthropic Messages API with OpenRouter extensions", + "identifier": "MessagesResult" +}) +export type MessagesStartEvent = { + readonly "message": { + readonly "container": AnthropicContainer + readonly "content": ReadonlyArray + readonly "id": string + readonly "model": string + readonly "provider"?: + | "AnyScale" + | "Atoma" + | "Cent-ML" + | "CrofAI" + | "Enfer" + | "GoPomelo" + | "HuggingFace" + | "Hyperbolic" + | "Hyperbolic 2" + | "InoCloud" + | "Kluster" + | "Lambda" + | "Lepton" + | "Lynn 2" + | "Lynn" + | "Mancer" + | "Modal" + | "Nineteen" + | "OctoAI" + | "Recursal" + | "Reflection" + | "Replicate" + | "SambaNova 2" + | "SF Compute" + | "Targon" + | "Together 2" + | "Ubicloud" + | "01.AI" + | "AkashML" + | "AI21" + | "AionLabs" + | "Alibaba" + | "Ambient" + | "Baidu" + | "Amazon Bedrock" + | "Amazon Nova" + | "Anthropic" + | "Arcee AI" + | "AtlasCloud" + | "Avian" + | "Azure" + | "BaseTen" + | "BytePlus" + | "Black Forest Labs" + | "Cerebras" + | "Chutes" + | "Cirrascale" + | "Clarifai" + | "Cloudflare" + | "Cohere" + | "CoreWeave" + | "Crucible" + | "Crusoe" + | "Darkbloom" + | "Decart" + | "Deepgram" + | "DeepInfra" + | "DeepSeek" + | "DekaLLM" + | "DigitalOcean" + | "Featherless" + | "Fireworks" + | "Fish Audio" + | "Friendli" + | "GMICloud" + | "Google" + | "Google AI Studio" + | "Groq" + | "HeyGen" + | "Inception" + | "Inceptron" + | "InferenceNet" + | "Ionstream" + | "Infermatic" + | "Io Net" + | "Inferact vLLM" + | "Inflection" + | "Liquid" + | "Mara" + | "Mancer 2" + | "Meta" + | "Minimax" + | "ModelRun" + | "Mistral" + | "Modular" + | "Moonshot AI" + | "Morph" + | "NCompass" + | "Nebius" + | "Nex AGI" + | "NextBit" + | "Novita" + | "Nvidia" + | "OpenAI" + | "OpenInference" + | "Parasail" + | "Poolside" + | "Perceptron" + | "Perplexity" + | "Phala" + | "Recraft" + | "Reka" + | "Relace" + | "Sail Research" + | "Sakana AI" + | "SambaNova" + | "Seed" + | "SiliconFlow" + | "Sourceful" + | "StepFun" + | "Stealth" + | "StreamLake" + | "Switchpoint" + | "Tencent" + | "Tenstorrent" + | "Together" + | "Upstage" + | "Venice" + | "Wafer" + | "WandB" + | "Quiver" + | "Krea" + | "Runway" + | "Xiaomi" + | "xAI" + | "Z.AI" + | "FakeProvider" + readonly "role": "assistant" + readonly "stop_details": AnthropicRefusalStopDetails + readonly "stop_reason": Schema.Json + readonly "stop_sequence": Schema.Json + readonly "type": "message" + readonly "usage": { + readonly "cache_creation": AnthropicCacheCreation + readonly "cache_creation_input_tokens": number | null + readonly "cache_read_input_tokens": number | null + readonly "inference_geo": string | null + readonly "input_tokens": number + readonly "output_tokens": number + readonly "output_tokens_details": AnthropicOutputTokensDetails + readonly "server_tool_use": AnthropicServerToolUsage + readonly "service_tier": AnthropicServiceTier + readonly "iterations"?: ReadonlyArray + readonly "speed"?: AnthropicSpeed + } + } + readonly "type": "message_start" +} +export const MessagesStartEvent = Schema.Struct({ + "message": Schema.Struct({ + "container": AnthropicContainer, + "content": Schema.Array(ORAnthropicContentBlock), + "id": Schema.String, + "model": Schema.String, + "provider": Schema.optionalKey( + Schema.Literals([ + "AnyScale", + "Atoma", + "Cent-ML", + "CrofAI", + "Enfer", + "GoPomelo", + "HuggingFace", + "Hyperbolic", + "Hyperbolic 2", + "InoCloud", + "Kluster", + "Lambda", + "Lepton", + "Lynn 2", + "Lynn", + "Mancer", + "Modal", + "Nineteen", + "OctoAI", + "Recursal", + "Reflection", + "Replicate", + "SambaNova 2", + "SF Compute", + "Targon", + "Together 2", + "Ubicloud", + "01.AI", + "AkashML", + "AI21", + "AionLabs", + "Alibaba", + "Ambient", + "Baidu", + "Amazon Bedrock", + "Amazon Nova", + "Anthropic", + "Arcee AI", + "AtlasCloud", + "Avian", + "Azure", + "BaseTen", + "BytePlus", + "Black Forest Labs", + "Cerebras", + "Chutes", + "Cirrascale", + "Clarifai", + "Cloudflare", + "Cohere", + "CoreWeave", + "Crucible", + "Crusoe", + "Darkbloom", + "Decart", + "Deepgram", + "DeepInfra", + "DeepSeek", + "DekaLLM", + "DigitalOcean", + "Featherless", + "Fireworks", + "Fish Audio", + "Friendli", + "GMICloud", + "Google", + "Google AI Studio", + "Groq", + "HeyGen", + "Inception", + "Inceptron", + "InferenceNet", + "Ionstream", + "Infermatic", + "Io Net", + "Inferact vLLM", + "Inflection", + "Liquid", + "Mara", + "Mancer 2", + "Meta", + "Minimax", + "ModelRun", + "Mistral", + "Modular", + "Moonshot AI", + "Morph", + "NCompass", + "Nebius", + "Nex AGI", + "NextBit", + "Novita", + "Nvidia", + "OpenAI", + "OpenInference", + "Parasail", + "Poolside", + "Perceptron", + "Perplexity", + "Phala", + "Recraft", + "Reka", + "Relace", + "Sail Research", + "Sakana AI", + "SambaNova", + "Seed", + "SiliconFlow", + "Sourceful", + "StepFun", + "Stealth", + "StreamLake", + "Switchpoint", + "Tencent", + "Tenstorrent", + "Together", + "Upstage", + "Venice", + "Wafer", + "WandB", + "Quiver", + "Krea", + "Runway", + "Xiaomi", + "xAI", + "Z.AI", + "FakeProvider" + ]) + ), + "role": Schema.Literal("assistant"), + "stop_details": AnthropicRefusalStopDetails, + "stop_reason": Schema.Json.annotate({ "expected": "JSON value" }), + "stop_sequence": Schema.Json.annotate({ "expected": "JSON value" }), + "type": Schema.Literal("message"), + "usage": Schema.Struct({ + "cache_creation": AnthropicCacheCreation, + "cache_creation_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "cache_read_input_tokens": Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + Schema.Null + ]), + "inference_geo": Schema.Union([Schema.String, Schema.Null]), + "input_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "output_tokens_details": AnthropicOutputTokensDetails, + "server_tool_use": AnthropicServerToolUsage, + "service_tier": AnthropicServiceTier, + "iterations": Schema.optionalKey(Schema.Array(AnthropicUsageIteration)), + "speed": Schema.optionalKey(AnthropicSpeed) + }) + }), + "type": Schema.Literal("message_start") +}).annotate({ "description": "Event sent at the start of a streaming message", "identifier": "MessagesStartEvent" }) +export type ImageStreamingResponse = { + readonly "data": + | ImageGenPartialImageEvent + | ImageGenTextChunkEvent + | ImageGenCompletedEvent + | ImageGenStreamErrorEvent +} +export const ImageStreamingResponse = Schema.Struct({ + "data": Schema.Union([ + ImageGenPartialImageEvent, + ImageGenTextChunkEvent, + ImageGenCompletedEvent, + ImageGenStreamErrorEvent + ]) +}).annotate({ "identifier": "ImageStreamingResponse" }) +export type ChatStreamingResponse = { readonly "data": ChatStreamChunk } +export const ChatStreamingResponse = Schema.Struct({ "data": ChatStreamChunk }).annotate({ + "identifier": "ChatStreamingResponse" +}) +export type ResponsesRequest = { + readonly "background"?: boolean | null + readonly "cache_control"?: AnthropicCacheControlDirective + readonly "debug"?: ChatDebugOptions + readonly "frequency_penalty"?: number | null + readonly "image_config"?: ImageConfig + readonly "include"?: ReadonlyArray | null + readonly "input"?: Inputs + readonly "instructions"?: string | null + readonly "max_output_tokens"?: number | null + readonly "max_tool_calls"?: number | null + readonly "metadata"?: RequestMetadata + readonly "modalities"?: ReadonlyArray + readonly "model"?: string + readonly "models"?: ReadonlyArray + readonly "parallel_tool_calls"?: boolean | null + readonly "plugins"?: ReadonlyArray< + | AutoRouterPlugin + | AutoBetaRouterPlugin + | ModerationPlugin + | WebSearchPlugin + | WebFetchPlugin + | FileParserPlugin + | ResponseHealingPlugin + | ContextCompressionPlugin + | ParetoRouterPlugin + | FusionPlugin + > + readonly "presence_penalty"?: number | null + readonly "previous_response_id"?: Schema.Json + readonly "prompt"?: StoredPromptTemplate + readonly "prompt_cache_key"?: string | null + readonly "prompt_cache_options"?: PromptCacheOptions + readonly "provider"?: ProviderPreferences + readonly "reasoning"?: ReasoningConfig + readonly "route"?: DeprecatedRoute + readonly "safety_identifier"?: string | null + readonly "service_tier"?: "auto" | "default" | "flex" | "priority" | "scale" | null + readonly "session_id"?: string + readonly "stop_server_tools_when"?: StopServerToolsWhen + readonly "store"?: false + readonly "stream"?: boolean + readonly "temperature"?: number | null + readonly "text"?: TextExtendedConfig + readonly "tool_choice"?: OpenAIResponsesToolChoice + readonly "tools"?: ReadonlyArray< + | { + readonly "description"?: string | null + readonly "name": string + readonly "parameters": { readonly [x: string]: Schema.Json } | null + readonly "strict"?: boolean | null + readonly "type": "function" + } + | Preview_WebSearchServerTool + | Preview_20250311_WebSearchServerTool + | Legacy_WebSearchServerTool + | WebSearchServerTool + | FileSearchServerTool + | ComputerUseServerTool + | CodeInterpreterServerTool + | McpServerTool + | ImageGenerationServerTool + | CodexLocalShellTool + | ShellServerTool + | ApplyPatchServerTool + | CustomTool + | NamespaceTool + | AdvisorServerTool_OpenRouter + | SubagentServerTool_OpenRouter + | DatetimeServerTool + | FilesServerTool + | FusionServerTool_OpenRouter + | ImageGenerationServerTool_OpenRouter + | SearchModelsServerTool_OpenRouter + | WebFetchServerTool + | WebSearchServerTool_OpenRouter + | ApplyPatchServerTool_OpenRouter + | BashServerTool + | ShellServerTool_OpenRouter + > + readonly "top_k"?: number + readonly "top_logprobs"?: number | null + readonly "top_p"?: number | null + readonly "trace"?: TraceConfig + readonly "truncation"?: OpenAIResponsesTruncation + readonly "user"?: string +} +export const ResponsesRequest = Schema.Struct({ + "background": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "cache_control": Schema.optionalKey(AnthropicCacheControlDirective), + "debug": Schema.optionalKey(ChatDebugOptions), + "frequency_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "image_config": Schema.optionalKey(ImageConfig), + "include": Schema.optionalKey(Schema.Union([Schema.Array(ResponseIncludesEnum), Schema.Null])), + "input": Schema.optionalKey(Inputs), + "instructions": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "max_output_tokens": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "max_tool_calls": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]).annotate({ + "description": + "Maximum number of server-tool (e.g. `openrouter:web_search`) agent steps the model may take during a request. Defaults to 30, which is also the maximum. Ignored when `stop_server_tools_when` is set." + }) + ), + "metadata": Schema.optionalKey(RequestMetadata), + "modalities": Schema.optionalKey( + Schema.Array(OutputModalityEnum).annotate({ + "description": "Output modalities for the response. Supported values are \"text\" and \"image\"." + }) + ), + "model": Schema.optionalKey(Schema.String), + "models": Schema.optionalKey(Schema.Array(Schema.String)), + "parallel_tool_calls": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "plugins": Schema.optionalKey( + Schema.Array( + Schema.Union([ + AutoRouterPlugin, + AutoBetaRouterPlugin, + ModerationPlugin, + WebSearchPlugin, + WebFetchPlugin, + FileParserPlugin, + ResponseHealingPlugin, + ContextCompressionPlugin, + ParetoRouterPlugin, + FusionPlugin + ], { mode: "oneOf" }) + ).annotate({ "description": "Plugins you want to enable for this request, including their settings." }) + ), + "presence_penalty": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "previous_response_id": Schema.optionalKey( + Schema.Json.annotate({ + "expected": "JSON value", + "description": + "Not supported. The Responses API is stateless: no responses are stored, so a previous response cannot be referenced. Requests with a non-null value are rejected with a 400 error. Send the full conversation history in `input` instead." + }) + ), + "prompt": Schema.optionalKey(StoredPromptTemplate), + "prompt_cache_key": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "prompt_cache_options": Schema.optionalKey(PromptCacheOptions), + "provider": Schema.optionalKey(ProviderPreferences), + "reasoning": Schema.optionalKey(ReasoningConfig), + "route": Schema.optionalKey(DeprecatedRoute), + "safety_identifier": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Recommended per-end-user identifier for abuse isolation. Use a stable ID, hash, or pseudonym. When a provider requires a user identity, OpenRouter folds it into the hashed identity sent upstream and never forwards it raw. If omitted, requests use an account-level identity, so provider policy blocks can affect the whole account." + }) + ), + "service_tier": Schema.optionalKey( + Schema.Union([ + Schema.Literal("auto"), + Schema.Literal("default"), + Schema.Literal("flex"), + Schema.Literal("priority"), + Schema.Literal("scale"), + Schema.Null + ]) + ), + "session_id": Schema.optionalKey( + Schema.String.annotate({ + "description": + "A unique identifier for grouping related requests (e.g., a conversation or agent workflow). When provided, OpenRouter uses it as the sticky routing key, routing all requests in the session to the same provider to maximize prompt cache hits. Also used for observability grouping. If provided in both the request body and the x-session-id header, the body value takes precedence. Maximum of 256 characters." + }).check(Schema.isMaxLength(256).annotate({ "expected": "a value with a length of at most 256" })) + ), + "stop_server_tools_when": Schema.optionalKey(StopServerToolsWhen), + "store": Schema.optionalKey(Schema.Literal(false)), + "stream": Schema.optionalKey(Schema.Boolean), + "temperature": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "text": Schema.optionalKey(TextExtendedConfig), + "tool_choice": Schema.optionalKey(OpenAIResponsesToolChoice), + "tools": Schema.optionalKey(Schema.Array(Schema.Union([ + Schema.Struct({ + "description": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null])), + "name": Schema.String, + "parameters": Schema.Union([ + Schema.Record(Schema.String, Schema.Json.annotate({ "expected": "JSON value" })), + Schema.Null + ]), + "strict": Schema.optionalKey(Schema.Union([Schema.Boolean, Schema.Null])), + "type": Schema.Literal("function") + }).annotate({ "description": "Function tool definition" }), + Preview_WebSearchServerTool, + Preview_20250311_WebSearchServerTool, + Legacy_WebSearchServerTool, + WebSearchServerTool, + FileSearchServerTool, + ComputerUseServerTool, + CodeInterpreterServerTool, + McpServerTool, + ImageGenerationServerTool, + CodexLocalShellTool, + ShellServerTool, + ApplyPatchServerTool, + CustomTool, + NamespaceTool, + AdvisorServerTool_OpenRouter, + SubagentServerTool_OpenRouter, + DatetimeServerTool, + FilesServerTool, + FusionServerTool_OpenRouter, + ImageGenerationServerTool_OpenRouter, + SearchModelsServerTool_OpenRouter, + WebFetchServerTool, + WebSearchServerTool_OpenRouter, + ApplyPatchServerTool_OpenRouter, + BashServerTool, + ShellServerTool_OpenRouter + ]))), + "top_k": Schema.optionalKey(Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" }))), + "top_logprobs": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), Schema.Null]) + ), + "top_p": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "format": "double" }) + ), + "trace": Schema.optionalKey(TraceConfig), + "truncation": Schema.optionalKey(OpenAIResponsesTruncation), + "user": Schema.optionalKey( + Schema.String.annotate({ + "description": + "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 256 characters." + }).check(Schema.isMaxLength(256).annotate({ "expected": "a value with a length of at most 256" })) + ) +}).annotate({ "description": "Request schema for Responses endpoint", "identifier": "ResponsesRequest" }) +export type StreamEvents = + | OpenResponsesCreatedEvent + | OpenResponsesInProgressEvent + | StreamEventsResponseCompleted + | StreamEventsResponseIncomplete + | StreamEventsResponseFailed + | ErrorEvent + | StreamEventsResponseOutputItemAdded + | StreamEventsResponseOutputItemDone + | ContentPartAddedEvent + | ContentPartDoneEvent + | TextDeltaEvent + | TextDoneEvent + | RefusalDeltaEvent + | RefusalDoneEvent + | AnnotationAddedEvent + | FunctionCallArgsDeltaEvent + | FunctionCallArgsDoneEvent + | ReasoningDeltaEvent + | ReasoningDoneEvent + | ReasoningSummaryPartAddedEvent + | ReasoningSummaryPartDoneEvent + | ReasoningSummaryTextDeltaEvent + | ReasoningSummaryTextDoneEvent + | ImageGenCallInProgressEvent + | ImageGenCallGeneratingEvent + | ImageGenCallPartialImageEvent + | ImageGenCallCompletedEvent + | WebSearchCallInProgressEvent + | WebSearchCallSearchingEvent + | WebSearchCallCompletedEvent + | CustomToolCallInputDeltaEvent + | CustomToolCallInputDoneEvent + | ApplyPatchCallOperationDiffDeltaEvent + | ApplyPatchCallOperationDiffDoneEvent + | FusionCallInProgressEvent + | FusionCallPanelAddedEvent + | FusionCallPanelDeltaEvent + | FusionCallPanelReasoningDeltaEvent + | FusionCallPanelCompletedEvent + | FusionCallPanelFailedEvent + | FusionCallAnalysisInProgressEvent + | FusionCallAnalysisCompletedEvent + | FusionCallCompletedEvent + | DebugEvent +export const StreamEvents: Schema.Schema = Schema.Union([ + OpenResponsesCreatedEvent, + OpenResponsesInProgressEvent, + StreamEventsResponseCompleted, + StreamEventsResponseIncomplete, + StreamEventsResponseFailed, + ErrorEvent, + StreamEventsResponseOutputItemAdded, + StreamEventsResponseOutputItemDone, + ContentPartAddedEvent, + ContentPartDoneEvent, + TextDeltaEvent, + TextDoneEvent, + RefusalDeltaEvent, + RefusalDoneEvent, + AnnotationAddedEvent, + FunctionCallArgsDeltaEvent, + FunctionCallArgsDoneEvent, + ReasoningDeltaEvent, + ReasoningDoneEvent, + ReasoningSummaryPartAddedEvent, + ReasoningSummaryPartDoneEvent, + ReasoningSummaryTextDeltaEvent, + ReasoningSummaryTextDoneEvent, + ImageGenCallInProgressEvent, + ImageGenCallGeneratingEvent, + ImageGenCallPartialImageEvent, + ImageGenCallCompletedEvent, + WebSearchCallInProgressEvent, + WebSearchCallSearchingEvent, + WebSearchCallCompletedEvent, + CustomToolCallInputDeltaEvent, + CustomToolCallInputDoneEvent, + ApplyPatchCallOperationDiffDeltaEvent, + ApplyPatchCallOperationDiffDoneEvent, + FusionCallInProgressEvent, + FusionCallPanelAddedEvent, + FusionCallPanelDeltaEvent, + FusionCallPanelReasoningDeltaEvent, + FusionCallPanelCompletedEvent, + FusionCallPanelFailedEvent, + FusionCallAnalysisInProgressEvent, + FusionCallAnalysisCompletedEvent, + FusionCallCompletedEvent, + DebugEvent +], { mode: "oneOf" }).annotate({ + "description": "Union of all possible event types emitted during response streaming", + "identifier": "StreamEvents" +}) +export type MessagesStreamEvents = + | MessagesStartEvent + | MessagesDeltaEvent + | MessagesStopEvent + | MessagesContentBlockStartEvent + | MessagesContentBlockDeltaEvent + | MessagesContentBlockStopEvent + | MessagesPingEvent + | MessagesErrorEvent +export const MessagesStreamEvents = Schema.Union([ + MessagesStartEvent, + MessagesDeltaEvent, + MessagesStopEvent, + MessagesContentBlockStartEvent, + MessagesContentBlockDeltaEvent, + MessagesContentBlockStopEvent, + MessagesPingEvent, + MessagesErrorEvent +], { mode: "oneOf" }).annotate({ + "description": "Union of all possible streaming events", + "identifier": "MessagesStreamEvents" +}) +export type ResponsesStreamingResponse = { readonly "data": StreamEvents } +export const ResponsesStreamingResponse: Schema.Schema = Schema.Struct({ + "data": StreamEvents +}).annotate({ "identifier": "ResponsesStreamingResponse" }) +export type MessagesStreamingResponse = { readonly "data": MessagesStreamEvents; readonly "event": string } +export const MessagesStreamingResponse = Schema.Struct({ "data": MessagesStreamEvents, "event": Schema.String }) + .annotate({ "identifier": "MessagesStreamingResponse" }) +// schemas +export type GetUserActivityParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "date"?: string + readonly "api_key_hash"?: string + readonly "user_id"?: string +} +export const GetUserActivityParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "date": Schema.optionalKey( + Schema.String.annotate({ "description": "Filter by a single UTC date in the last 30 days (YYYY-MM-DD format)." }) + ), + "api_key_hash": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter by API key hash (SHA-256 hex string, as returned by the keys API)." + }) + ), + "user_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter by org member user ID. Only applicable for organization accounts." + }) + ) +}) +export type GetUserActivity200 = ActivityResponse +export const GetUserActivity200 = ActivityResponse +export type GetUserActivity400 = BadRequestResponse +export const GetUserActivity400 = BadRequestResponse +export type GetUserActivity401 = UnauthorizedResponse +export const GetUserActivity401 = UnauthorizedResponse +export type GetUserActivity403 = ForbiddenResponse +export const GetUserActivity403 = ForbiddenResponse +export type GetUserActivity404 = NotFoundResponse +export const GetUserActivity404 = NotFoundResponse +export type GetUserActivity500 = InternalServerResponse +export const GetUserActivity500 = InternalServerResponse +export type GetAnalyticsMetaParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetAnalyticsMetaParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetAnalyticsMeta200 = { + readonly "data": { + readonly "dimensions": ReadonlyArray<{ readonly "display_label": string; readonly "name": string }> + readonly "granularities": ReadonlyArray< + { readonly "display_label": string; readonly "name": "minute" | "hour" | "day" | "week" | "month" } + > + readonly "metrics": ReadonlyArray< + { + readonly "display_format": "number" | "currency" | "percent" | "latency" | "throughput" + readonly "display_label": string + readonly "is_rate": boolean + readonly "name": string + } + > + readonly "operators": ReadonlyArray< + { + readonly "name": "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" + readonly "value_type": "scalar" | "array" + } + > + } +} +export const GetAnalyticsMeta200 = Schema.Struct({ + "data": Schema.Struct({ + "dimensions": Schema.Array( + Schema.Struct({ + "display_label": Schema.String.annotate({ "description": "Human-readable label" }), + "name": Schema.String.annotate({ "description": "Dimension identifier used in query requests" }) + }) + ), + "granularities": Schema.Array( + Schema.Struct({ + "display_label": Schema.String.annotate({ "description": "Human-readable label" }), + "name": Schema.Literals(["minute", "hour", "day", "week", "month"]).annotate({ + "description": "Granularity identifier" + }) + }) + ), + "metrics": Schema.Array(Schema.Struct({ + "display_format": Schema.Literals(["number", "currency", "percent", "latency", "throughput"]).annotate({ + "description": + "How this metric value should be formatted for display (e.g. percent → multiply by 100 and append %, currency → prefix with $)" + }), + "display_label": Schema.String.annotate({ "description": "Human-readable label" }), + "is_rate": Schema.Boolean.annotate({ + "description": "Whether this metric is a rate/ratio (averaged, not summed)" + }), + "name": Schema.String.annotate({ "description": "Metric identifier used in query requests" }) + })), + "operators": Schema.Array(Schema.Struct({ + "name": Schema.Literals(["eq", "neq", "in", "not_in", "gt", "gte", "lt", "lte"]).annotate({ + "description": "Operator identifier used in filter definitions" + }), + "value_type": Schema.Literals(["scalar", "array"]).annotate({ + "description": "Whether the operator expects a single value or an array" + }) + })) + }) +}) +export type GetAnalyticsMeta401 = UnauthorizedResponse +export const GetAnalyticsMeta401 = UnauthorizedResponse +export type GetAnalyticsMeta403 = ForbiddenResponse +export const GetAnalyticsMeta403 = ForbiddenResponse +export type GetAnalyticsMeta500 = InternalServerResponse +export const GetAnalyticsMeta500 = InternalServerResponse +export type QueryAnalyticsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const QueryAnalyticsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type QueryAnalyticsRequestJson = { + readonly "classifier_dimensions"?: { + readonly "classifier_id": string + readonly "dimension_names"?: ReadonlyArray + readonly "include_nulls"?: boolean + } + readonly "classifier_filters"?: { + readonly "classifier_id": string + readonly "filters": ReadonlyArray< + { + readonly "field": string + readonly "operator": string + readonly "value": string | number | ReadonlyArray + } + > + } + readonly "dimensions"?: ReadonlyArray + readonly "filters"?: ReadonlyArray< + { + readonly "field": string + readonly "operator": string + readonly "value": string | number | ReadonlyArray + } + > + readonly "granularity"?: string + readonly "group_limit"?: number + readonly "limit"?: number + readonly "metrics": ReadonlyArray + readonly "order_by"?: { readonly "direction": "asc" | "desc"; readonly "field": string } + readonly "time_range"?: { readonly "end": string; readonly "start": string } +} +export const QueryAnalyticsRequestJson = Schema.Struct({ + "classifier_dimensions": Schema.optionalKey( + Schema.Struct({ + "classifier_id": Schema.String.annotate({ + "description": "UUID of the classifier whose tags to group by.", + "format": "uuid" + }), + "dimension_names": Schema.optionalKey( + Schema.Array( + Schema.String.annotate({ + "description": + "Classifier dimension name (snake_case identifier). When exactly one name is provided, the response uses it as the column key; with multiple names or none, the response uses `clf_dimension_name`/`clf_dimension_value` columns." + }) + ).check(Schema.isMaxLength(10).annotate({ "expected": "a value with a length of at most 10" })) + ), + "include_nulls": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": + "When true, also include generations that have no tag from this classifier. Defaults to false, which returns only classified generations." + }) + ) + }).annotate({ + "description": + "Group results by custom classifier tags, breaking down metrics by the specified dimension values. Requires an active classifier on the workspace." + }) + ), + "classifier_filters": Schema.optionalKey( + Schema.Struct({ + "classifier_id": Schema.String.annotate({ + "description": + "UUID of the classifier whose tags to filter by. Must match classifier_dimensions.classifier_id when both are specified.", + "format": "uuid" + }), + "filters": Schema.Array(Schema.Struct({ + "field": Schema.String.annotate({ + "description": + "Classifier dimension name to filter on (snake_case identifier, e.g. \"department\", \"work_type\")." + }), + "operator": Schema.String.annotate({ + "description": + "Filter operator. Only equality/set operators are supported (eq, neq, in, not_in) — ordered comparisons are not available because classification values are strings." + }), + "value": Schema.Union([ + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + Schema.Array( + Schema.Union([ + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ]) + ) + ]).annotate({ + "description": "Filter value. Use a scalar (string or number) for eq/neq, or an array for in/not_in." + }) + })).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })).check( + Schema.isMaxLength(10).annotate({ "expected": "a value with a length of at most 10" }) + ) + }).annotate({ + "description": + "Filter results to generations with specific classifier tag values. Can be combined with classifier_dimensions (must use the same classifier_id) or used independently with standard dimensions." + }) + ), + "dimensions": Schema.optionalKey( + Schema.Array( + Schema.String.annotate({ + "description": "Dimension to group by (up to 2). Use the /meta endpoint for available dimensions." + }) + ).check(Schema.isMaxLength(2).annotate({ "expected": "a value with a length of at most 2" })) + ), + "filters": Schema.optionalKey( + Schema.Array(Schema.Struct({ + "field": Schema.String.annotate({ + "description": "Dimension to filter on. Use the /meta endpoint for available dimensions." + }), + "operator": Schema.String.annotate({ "description": "Filter operator" }), + "value": Schema.Union([ + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + Schema.Array( + Schema.Union([ + Schema.String, + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ]) + ) + ]).annotate({ + "description": + "Filter value (scalar or array depending on operator). Several dimensions are enriched in responses (returned as human-readable labels), but filters must use the underlying ID: `api_key_id` — numeric ID (from generation metadata) or key hash (64-char hex from GET /api/v1/keys, resolved server-side); `user` — Clerk user ID (e.g. \"user_abc123\"), not the display name; `workspace` — workspace UUID, not the workspace name; `app` — numeric app ID, not the app title; `model` — permaslug (e.g. \"openai/gpt-4o\"), not the display name. Other dimensions (provider, origin, country, etc.) are not enriched and accept the value as returned." + }) + })).check(Schema.isMaxLength(20).annotate({ "expected": "a value with a length of at most 20" })) + ), + "granularity": Schema.optionalKey(Schema.String.annotate({ "description": "Time granularity" })), + "group_limit": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum rows per distinct combination of dimensions. When omitted on time-series queries (granularity + dimensions), auto-computed to avoid truncating time windows. Explicit values override the default and may truncate time buckets if set lower than the number of buckets in the range. Ignored when no dimensions are specified." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum total rows returned. Defaults to 1000. On time-series queries with dimensions and no explicit group_limit, the server may raise this to accommodate the expected number of unique time-bucket/dimension combinations." + }).check(Schema.isInt().annotate({ "expected": "an integer" })) + ), + "metrics": Schema.Array(Schema.String.annotate({ "description": "Metric name" })).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "order_by": Schema.optionalKey( + Schema.Struct({ + "direction": Schema.Literals(["asc", "desc"]), + "field": Schema.String.annotate({ + "description": + "Field to order by: a metric included in `metrics` (or \"request_count\", which may be ordered by without being requested), a requested dimension, or \"date\"." + }) + }) + ), + "time_range": Schema.optionalKey( + Schema.Struct({ + "end": Schema.String.annotate({ "format": "date-time" }), + "start": Schema.String.annotate({ "format": "date-time" }) + }) + ) +}) +export type QueryAnalytics200 = { + readonly "data": { + readonly "cachedAt"?: number + readonly "data": ReadonlyArray<{}> + readonly "metadata": { + readonly "query_time_ms": number + readonly "row_count": number + readonly "truncated": boolean + } + readonly "warnings"?: ReadonlyArray + } +} +export const QueryAnalytics200 = Schema.Struct({ + "data": Schema.Struct({ + "cachedAt": Schema.optionalKey( + Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "data": Schema.Array( + Schema.Struct({}).annotate({ "description": "A row of analytics data with metric/dimension values" }) + ), + "metadata": Schema.Struct({ + "query_time_ms": Schema.Number.annotate({ "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "row_count": Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })), + "truncated": Schema.Boolean + }), + "warnings": Schema.optionalKey( + Schema.Array(Schema.String).annotate({ + "description": + "Warnings about filter resolution issues (e.g. unresolvable api_key_id hashes). The query still runs normally; these inform the caller that some filter values could not be resolved." + }) + ) + }) +}) +export type QueryAnalytics400 = BadRequestResponse +export const QueryAnalytics400 = BadRequestResponse +export type QueryAnalytics401 = UnauthorizedResponse +export const QueryAnalytics401 = UnauthorizedResponse +export type QueryAnalytics403 = ForbiddenResponse +export const QueryAnalytics403 = ForbiddenResponse +export type QueryAnalytics408 = RequestTimeoutResponse +export const QueryAnalytics408 = RequestTimeoutResponse +export type QueryAnalytics500 = InternalServerResponse +export const QueryAnalytics500 = InternalServerResponse +export type CreateAudioSpeechParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateAudioSpeechParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateAudioSpeechRequestJson = SpeechRequest +export const CreateAudioSpeechRequestJson = SpeechRequest +export type CreateAudioSpeech400 = BadRequestResponse +export const CreateAudioSpeech400 = BadRequestResponse +export type CreateAudioSpeech401 = UnauthorizedResponse +export const CreateAudioSpeech401 = UnauthorizedResponse +export type CreateAudioSpeech402 = PaymentRequiredResponse +export const CreateAudioSpeech402 = PaymentRequiredResponse +export type CreateAudioSpeech404 = NotFoundResponse +export const CreateAudioSpeech404 = NotFoundResponse +export type CreateAudioSpeech429 = TooManyRequestsResponse +export const CreateAudioSpeech429 = TooManyRequestsResponse +export type CreateAudioSpeech500 = InternalServerResponse +export const CreateAudioSpeech500 = InternalServerResponse +export type CreateAudioSpeech502 = BadGatewayResponse +export const CreateAudioSpeech502 = BadGatewayResponse +export type CreateAudioSpeech503 = ServiceUnavailableResponse +export const CreateAudioSpeech503 = ServiceUnavailableResponse +export type CreateAudioSpeech524 = EdgeNetworkTimeoutResponse +export const CreateAudioSpeech524 = EdgeNetworkTimeoutResponse +export type CreateAudioSpeech529 = ProviderOverloadedResponse +export const CreateAudioSpeech529 = ProviderOverloadedResponse +export type CreateAudioTranscriptionsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateAudioTranscriptionsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateAudioTranscriptionsRequestJson = STTRequest +export const CreateAudioTranscriptionsRequestJson = STTRequest +export type CreateAudioTranscriptionsRequestFormData = { + readonly "file": string + readonly "language"?: string + readonly "model": string + readonly "response_format"?: "json" | "verbose_json" + readonly "temperature"?: number + readonly "timestamp_granularities[]"?: ReadonlyArray<"word" | "segment"> +} +export const CreateAudioTranscriptionsRequestFormData = Schema.Struct({ + "file": Schema.String.annotate({ + "description": + "The audio file to transcribe. The format is derived from the filename extension or the file part content type. Max 25 MB; send larger files as base64 JSON via input_audio.", + "format": "binary" + }), + "language": Schema.optionalKey( + Schema.String.annotate({ "description": "The language of the input audio (ISO-639-1)." }) + ), + "model": Schema.String.annotate({ "description": "The model to use for transcription." }), + "response_format": Schema.optionalKey( + Schema.Literals(["json", "verbose_json"]).annotate({ + "description": + "The response format. \"json\" (default) returns { text, usage }; \"verbose_json\" additionally returns task, language, duration, and segment-level timestamps (OpenAI-compatible providers only)." + }) + ), + "temperature": Schema.optionalKey( + Schema.Number.annotate({ "description": "The sampling temperature." }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "timestamp_granularities[]": Schema.optionalKey( + Schema.Array(Schema.Literals(["word", "segment"])).annotate({ + "description": + "Timestamp detail levels to include when response_format is \"verbose_json\". \"word\" additionally returns word-level timestamps in the words array." + }) + ) +}) +export type CreateAudioTranscriptions200 = STTResponse +export const CreateAudioTranscriptions200 = STTResponse +export type CreateAudioTranscriptions400 = BadRequestResponse +export const CreateAudioTranscriptions400 = BadRequestResponse +export type CreateAudioTranscriptions401 = UnauthorizedResponse +export const CreateAudioTranscriptions401 = UnauthorizedResponse +export type CreateAudioTranscriptions402 = PaymentRequiredResponse +export const CreateAudioTranscriptions402 = PaymentRequiredResponse +export type CreateAudioTranscriptions404 = NotFoundResponse +export const CreateAudioTranscriptions404 = NotFoundResponse +export type CreateAudioTranscriptions429 = TooManyRequestsResponse +export const CreateAudioTranscriptions429 = TooManyRequestsResponse +export type CreateAudioTranscriptions500 = InternalServerResponse +export const CreateAudioTranscriptions500 = InternalServerResponse +export type CreateAudioTranscriptions502 = BadGatewayResponse +export const CreateAudioTranscriptions502 = BadGatewayResponse +export type CreateAudioTranscriptions503 = ServiceUnavailableResponse +export const CreateAudioTranscriptions503 = ServiceUnavailableResponse +export type CreateAudioTranscriptions524 = EdgeNetworkTimeoutResponse +export const CreateAudioTranscriptions524 = EdgeNetworkTimeoutResponse +export type CreateAudioTranscriptions529 = ProviderOverloadedResponse +export const CreateAudioTranscriptions529 = ProviderOverloadedResponse +export type ExchangeAuthCodeForAPIKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ExchangeAuthCodeForAPIKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ExchangeAuthCodeForAPIKeyRequestJson = { + readonly "code": string + readonly "code_challenge_method"?: "S256" | "plain" | null + readonly "code_verifier"?: string +} +export const ExchangeAuthCodeForAPIKeyRequestJson = Schema.Struct({ + "code": Schema.String.annotate({ "description": "The authorization code received from the OAuth redirect" }), + "code_challenge_method": Schema.optionalKey( + Schema.Union([Schema.Literal("S256"), Schema.Literal("plain"), Schema.Null]).annotate({ + "description": "The method used to generate the code challenge" + }) + ), + "code_verifier": Schema.optionalKey( + Schema.String.annotate({ + "description": "The code verifier if code_challenge was used in the authorization request" + }) + ) +}) +export type ExchangeAuthCodeForAPIKey200 = { readonly "key": string; readonly "user_id": string | null } +export const ExchangeAuthCodeForAPIKey200 = Schema.Struct({ + "key": Schema.String.annotate({ "description": "The API key to use for OpenRouter requests" }), + "user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "User ID associated with the API key" + }) +}) +export type ExchangeAuthCodeForAPIKey400 = BadRequestResponse +export const ExchangeAuthCodeForAPIKey400 = BadRequestResponse +export type ExchangeAuthCodeForAPIKey403 = ForbiddenResponse +export const ExchangeAuthCodeForAPIKey403 = ForbiddenResponse +export type ExchangeAuthCodeForAPIKey500 = InternalServerResponse +export const ExchangeAuthCodeForAPIKey500 = InternalServerResponse +export type CreateAuthKeysCodeParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateAuthKeysCodeParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateAuthKeysCodeRequestJson = { + readonly "callback_url": string + readonly "code_challenge"?: string + readonly "code_challenge_method"?: "S256" | "plain" + readonly "expires_at"?: string | null + readonly "key_label"?: string + readonly "limit"?: number + readonly "spawn_agent"?: string + readonly "spawn_cloud"?: string + readonly "usage_limit_type"?: "daily" | "weekly" | "monthly" + readonly "workspace_id"?: string +} +export const CreateAuthKeysCodeRequestJson = Schema.Struct({ + "callback_url": Schema.String.annotate({ + "description": + "The callback URL to redirect to after authorization. Supports https URLs and localhost/127.0.0.1 URLs on any port for local CLI tools.", + "format": "uri" + }), + "code_challenge": Schema.optionalKey( + Schema.String.annotate({ "description": "PKCE code challenge for enhanced security" }) + ), + "code_challenge_method": Schema.optionalKey( + Schema.Literals(["S256", "plain"]).annotate({ "description": "The method used to generate the code challenge" }) + ), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Optional expiration time for the API key to be created", + "format": "date-time" + }) + ), + "key_label": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional custom label for the API key. Defaults to the app name if not provided." + }).check(Schema.isMaxLength(100).annotate({ "expected": "a value with a length of at most 100" })) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Credit limit for the API key to be created", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "spawn_agent": Schema.optionalKey(Schema.String.annotate({ "description": "Agent identifier for spawn telemetry" })), + "spawn_cloud": Schema.optionalKey(Schema.String.annotate({ "description": "Cloud identifier for spawn telemetry" })), + "usage_limit_type": Schema.optionalKey( + Schema.Literals(["daily", "weekly", "monthly"]).annotate({ + "description": "Optional credit limit reset interval. When set, the credit limit resets on this interval." + }) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ "description": "Optional workspace ID to associate the API key with", "format": "uuid" }) + ) +}) +export type CreateAuthKeysCode200 = { + readonly "data": { readonly "app_id": number; readonly "created_at": string; readonly "id": string } +} +export const CreateAuthKeysCode200 = Schema.Struct({ + "data": Schema.Struct({ + "app_id": Schema.Number.annotate({ "description": "The application ID associated with this auth code" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the auth code was created" }), + "id": Schema.String.annotate({ "description": "The authorization code ID to use in the exchange request" }) + }).annotate({ "description": "Auth code data" }) +}) +export type CreateAuthKeysCode400 = BadRequestResponse +export const CreateAuthKeysCode400 = BadRequestResponse +export type CreateAuthKeysCode401 = UnauthorizedResponse +export const CreateAuthKeysCode401 = UnauthorizedResponse +export type CreateAuthKeysCode403 = ForbiddenResponse +export const CreateAuthKeysCode403 = ForbiddenResponse +export type CreateAuthKeysCode409 = ConflictResponse +export const CreateAuthKeysCode409 = ConflictResponse +export type CreateAuthKeysCode500 = InternalServerResponse +export const CreateAuthKeysCode500 = InternalServerResponse +export type GetBenchmarksParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "source"?: "artificial-analysis" | "design-arena" + readonly "task_type"?: "coding" | "intelligence" | "agentic" + readonly "arena"?: "models" | "builders" | "agents" + readonly "category"?: string + readonly "max_results"?: number +} +export const GetBenchmarksParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "source": Schema.optionalKey( + Schema.Literals(["artificial-analysis", "design-arena"]).annotate({ + "description": + "Benchmark source to query. Determines the shape of the returned items. When omitted, returns results from all sources." + }) + ), + "task_type": Schema.optionalKey( + Schema.Literals(["coding", "intelligence", "agentic"]).annotate({ + "description": + "Filter results by task type. For Artificial Analysis, maps to the corresponding index. For Design Arena, maps to the matching category." + }) + ), + "arena": Schema.optionalKey( + Schema.Literals(["models", "builders", "agents"]).annotate({ + "description": "Design Arena only: arena to query. Defaults to `models` when source is `design-arena`." + }) + ), + "category": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Design Arena only: category within the arena (e.g. `codecategories`, `uicomponent`, `gamedev`, `3d`, `dataviz`, `image`, `video`, `svg`). When omitted, returns all categories." + }) + ), + "max_results": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Maximum number of items to return. When omitted, all matching results are returned." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ) + ) +}) +export type GetBenchmarks200 = UnifiedBenchmarksResponse +export const GetBenchmarks200 = UnifiedBenchmarksResponse +export type GetBenchmarks400 = BadRequestResponse +export const GetBenchmarks400 = BadRequestResponse +export type GetBenchmarks401 = UnauthorizedResponse +export const GetBenchmarks401 = UnauthorizedResponse +export type GetBenchmarks429 = TooManyRequestsResponse +export const GetBenchmarks429 = TooManyRequestsResponse +export type GetBenchmarks500 = InternalServerResponse +export const GetBenchmarks500 = InternalServerResponse +export type ListBYOKKeysParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number + readonly "workspace_id"?: string + readonly "provider"?: + | "ai21" + | "aion-labs" + | "akashml" + | "alibaba" + | "amazon-bedrock" + | "amazon-nova" + | "ambient" + | "anthropic" + | "arcee-ai" + | "atlas-cloud" + | "avian" + | "azure" + | "baidu" + | "baseten" + | "black-forest-labs" + | "byteplus" + | "cerebras" + | "chutes" + | "cirrascale" + | "clarifai" + | "cloudflare" + | "cohere" + | "coreweave" + | "crusoe" + | "darkbloom" + | "decart" + | "deepgram" + | "deepinfra" + | "deepseek" + | "dekallm" + | "digitalocean" + | "featherless" + | "fireworks" + | "fish-audio" + | "friendli" + | "gmicloud" + | "google-ai-studio" + | "google-vertex" + | "groq" + | "heygen" + | "inception" + | "inceptron" + | "inferact-vllm" + | "inference-net" + | "infermatic" + | "inflection" + | "io-net" + | "ionstream" + | "krea" + | "liquid" + | "mancer" + | "mara" + | "meta" + | "minimax" + | "mistral" + | "modelrun" + | "modular" + | "moonshotai" + | "morph" + | "ncompass" + | "nebius" + | "nex-agi" + | "nextbit" + | "novita" + | "nvidia" + | "open-inference" + | "openai" + | "parasail" + | "perceptron" + | "perplexity" + | "phala" + | "poolside" + | "quiver" + | "recraft" + | "reka" + | "relace" + | "runway" + | "sail-research" + | "sakana" + | "sambanova" + | "seed" + | "siliconflow" + | "sourceful" + | "stepfun" + | "streamlake" + | "switchpoint" + | "tencent" + | "tenstorrent" + | "together" + | "upstage" + | "venice" + | "wafer" + | "wandb" + | "xai" + | "xiaomi" + | "z-ai" +} +export const ListBYOKKeysParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional workspace ID to filter by. Defaults to the authenticated entity's default workspace.", + "format": "uuid" + }) + ), + "provider": Schema.optionalKey( + Schema.Literals([ + "ai21", + "aion-labs", + "akashml", + "alibaba", + "amazon-bedrock", + "amazon-nova", + "ambient", + "anthropic", + "arcee-ai", + "atlas-cloud", + "avian", + "azure", + "baidu", + "baseten", + "black-forest-labs", + "byteplus", + "cerebras", + "chutes", + "cirrascale", + "clarifai", + "cloudflare", + "cohere", + "coreweave", + "crusoe", + "darkbloom", + "decart", + "deepgram", + "deepinfra", + "deepseek", + "dekallm", + "digitalocean", + "featherless", + "fireworks", + "fish-audio", + "friendli", + "gmicloud", + "google-ai-studio", + "google-vertex", + "groq", + "heygen", + "inception", + "inceptron", + "inferact-vllm", + "inference-net", + "infermatic", + "inflection", + "io-net", + "ionstream", + "krea", + "liquid", + "mancer", + "mara", + "meta", + "minimax", + "mistral", + "modelrun", + "modular", + "moonshotai", + "morph", + "ncompass", + "nebius", + "nex-agi", + "nextbit", + "novita", + "nvidia", + "open-inference", + "openai", + "parasail", + "perceptron", + "perplexity", + "phala", + "poolside", + "quiver", + "recraft", + "reka", + "relace", + "runway", + "sail-research", + "sakana", + "sambanova", + "seed", + "siliconflow", + "sourceful", + "stepfun", + "streamlake", + "switchpoint", + "tencent", + "tenstorrent", + "together", + "upstage", + "venice", + "wafer", + "wandb", + "xai", + "xiaomi", + "z-ai" + ]).annotate({ + "description": "Optional provider slug to filter by (e.g. `openai`, `anthropic`, `amazon-bedrock`)." + }) + ) +}) +export type ListBYOKKeys200 = ListBYOKKeysResponse +export const ListBYOKKeys200 = ListBYOKKeysResponse +export type ListBYOKKeys400 = BadRequestResponse +export const ListBYOKKeys400 = BadRequestResponse +export type ListBYOKKeys401 = UnauthorizedResponse +export const ListBYOKKeys401 = UnauthorizedResponse +export type ListBYOKKeys500 = InternalServerResponse +export const ListBYOKKeys500 = InternalServerResponse +export type CreateBYOKKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateBYOKKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateBYOKKeyRequestJson = CreateBYOKKeyRequest +export const CreateBYOKKeyRequestJson = CreateBYOKKeyRequest +export type CreateBYOKKey201 = CreateBYOKKeyResponse +export const CreateBYOKKey201 = CreateBYOKKeyResponse +export type CreateBYOKKey400 = BadRequestResponse +export const CreateBYOKKey400 = BadRequestResponse +export type CreateBYOKKey401 = UnauthorizedResponse +export const CreateBYOKKey401 = UnauthorizedResponse +export type CreateBYOKKey403 = ForbiddenResponse +export const CreateBYOKKey403 = ForbiddenResponse +export type CreateBYOKKey500 = InternalServerResponse +export const CreateBYOKKey500 = InternalServerResponse +export type GetBYOKKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetBYOKKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetBYOKKey200 = GetBYOKKeyResponse +export const GetBYOKKey200 = GetBYOKKeyResponse +export type GetBYOKKey401 = UnauthorizedResponse +export const GetBYOKKey401 = UnauthorizedResponse +export type GetBYOKKey404 = NotFoundResponse +export const GetBYOKKey404 = NotFoundResponse +export type GetBYOKKey500 = InternalServerResponse +export const GetBYOKKey500 = InternalServerResponse +export type DeleteBYOKKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteBYOKKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteBYOKKey200 = DeleteBYOKKeyResponse +export const DeleteBYOKKey200 = DeleteBYOKKeyResponse +export type DeleteBYOKKey401 = UnauthorizedResponse +export const DeleteBYOKKey401 = UnauthorizedResponse +export type DeleteBYOKKey404 = NotFoundResponse +export const DeleteBYOKKey404 = NotFoundResponse +export type DeleteBYOKKey500 = InternalServerResponse +export const DeleteBYOKKey500 = InternalServerResponse +export type UpdateBYOKKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpdateBYOKKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpdateBYOKKeyRequestJson = UpdateBYOKKeyRequest +export const UpdateBYOKKeyRequestJson = UpdateBYOKKeyRequest +export type UpdateBYOKKey200 = UpdateBYOKKeyResponse +export const UpdateBYOKKey200 = UpdateBYOKKeyResponse +export type UpdateBYOKKey400 = BadRequestResponse +export const UpdateBYOKKey400 = BadRequestResponse +export type UpdateBYOKKey401 = UnauthorizedResponse +export const UpdateBYOKKey401 = UnauthorizedResponse +export type UpdateBYOKKey404 = NotFoundResponse +export const UpdateBYOKKey404 = NotFoundResponse +export type UpdateBYOKKey500 = InternalServerResponse +export const UpdateBYOKKey500 = InternalServerResponse +export type SendChatCompletionRequestParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "X-OpenRouter-Metadata"?: MetadataLevel +} +export const SendChatCompletionRequestParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "X-OpenRouter-Metadata": Schema.optionalKey(MetadataLevel) +}) +export type SendChatCompletionRequestRequestJson = ChatRequest +export const SendChatCompletionRequestRequestJson = ChatRequest +export type SendChatCompletionRequest200 = ChatResult +export const SendChatCompletionRequest200 = ChatResult +export type SendChatCompletionRequest200Sse = ChatStreamingResponse +export const SendChatCompletionRequest200Sse = ChatStreamingResponse +export type SendChatCompletionRequest400 = BadRequestResponse +export const SendChatCompletionRequest400 = BadRequestResponse +export type SendChatCompletionRequest401 = UnauthorizedResponse +export const SendChatCompletionRequest401 = UnauthorizedResponse +export type SendChatCompletionRequest402 = PaymentRequiredResponse +export const SendChatCompletionRequest402 = PaymentRequiredResponse +export type SendChatCompletionRequest403 = ForbiddenResponse +export const SendChatCompletionRequest403 = ForbiddenResponse +export type SendChatCompletionRequest404 = NotFoundResponse +export const SendChatCompletionRequest404 = NotFoundResponse +export type SendChatCompletionRequest408 = RequestTimeoutResponse +export const SendChatCompletionRequest408 = RequestTimeoutResponse +export type SendChatCompletionRequest413 = PayloadTooLargeResponse +export const SendChatCompletionRequest413 = PayloadTooLargeResponse +export type SendChatCompletionRequest422 = UnprocessableEntityResponse +export const SendChatCompletionRequest422 = UnprocessableEntityResponse +export type SendChatCompletionRequest429 = TooManyRequestsResponse +export const SendChatCompletionRequest429 = TooManyRequestsResponse +export type SendChatCompletionRequest500 = InternalServerResponse +export const SendChatCompletionRequest500 = InternalServerResponse +export type SendChatCompletionRequest502 = BadGatewayResponse +export const SendChatCompletionRequest502 = BadGatewayResponse +export type SendChatCompletionRequest503 = ServiceUnavailableResponse +export const SendChatCompletionRequest503 = ServiceUnavailableResponse +export type SendChatCompletionRequest524 = EdgeNetworkTimeoutResponse +export const SendChatCompletionRequest524 = EdgeNetworkTimeoutResponse +export type SendChatCompletionRequest529 = ProviderOverloadedResponse +export const SendChatCompletionRequest529 = ProviderOverloadedResponse +export type GetTaskClassificationsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "window"?: "7d" +} +export const GetTaskClassificationsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "window": Schema.optionalKey( + Schema.Literal("7d").annotate({ + "description": + "Trailing time window for the classification data. Currently only `7d` (trailing 7 days) is supported." + }) + ) +}) +export type GetTaskClassifications200 = TaskClassificationResponse +export const GetTaskClassifications200 = TaskClassificationResponse +export type GetTaskClassifications400 = BadRequestResponse +export const GetTaskClassifications400 = BadRequestResponse +export type GetTaskClassifications401 = UnauthorizedResponse +export const GetTaskClassifications401 = UnauthorizedResponse +export type GetTaskClassifications429 = TooManyRequestsResponse +export const GetTaskClassifications429 = TooManyRequestsResponse +export type GetTaskClassifications500 = InternalServerResponse +export const GetTaskClassifications500 = InternalServerResponse +export type GetCreditsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetCreditsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetCredits200 = { readonly "data": { readonly "total_credits": number; readonly "total_usage": number } } +export const GetCredits200 = Schema.Struct({ + "data": Schema.Struct({ + "total_credits": Schema.Number.annotate({ "description": "Total credits purchased", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ), + "total_usage": Schema.Number.annotate({ "description": "Total credits used", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + }) +}).annotate({ "description": "Total credits purchased and used" }) +export type GetCredits401 = UnauthorizedResponse +export const GetCredits401 = UnauthorizedResponse +export type GetCredits403 = ForbiddenResponse +export const GetCredits403 = ForbiddenResponse +export type GetCredits500 = InternalServerResponse +export const GetCredits500 = InternalServerResponse +export type CreateCoinbaseChargeParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateCoinbaseChargeParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateCoinbaseCharge410 = GoneResponse +export const CreateCoinbaseCharge410 = GoneResponse +export type GetAppRankingsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "category"?: "coding" | "creative" | "productivity" | "entertainment" + readonly "subcategory"?: + | "cli-agent" + | "ide-extension" + | "cloud-agent" + | "programming-app" + | "native-app-builder" + | "creative-writing" + | "video-gen" + | "image-gen" + | "audio-gen" + | "roleplay" + | "game" + | "writing-assistant" + | "general-chat" + | "personal-agent" + | "legal" + readonly "sort"?: "popular" | "trending" + readonly "start_date"?: string + readonly "end_date"?: string + readonly "limit"?: number + readonly "offset"?: number | null +} +export const GetAppRankingsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "category": Schema.optionalKey( + Schema.Literals(["coding", "creative", "productivity", "entertainment"]).annotate({ + "description": + "Marketplace category group to filter by (e.g. `coding`). Only apps tagged with a subcategory inside this group are returned. Mutually combinable with `subcategory` — when both are supplied the `subcategory` must belong to the `category` group." + }) + ), + "subcategory": Schema.optionalKey( + Schema.Literals([ + "cli-agent", + "ide-extension", + "cloud-agent", + "programming-app", + "native-app-builder", + "creative-writing", + "video-gen", + "image-gen", + "audio-gen", + "roleplay", + "game", + "writing-assistant", + "general-chat", + "personal-agent", + "legal" + ]).annotate({ + "description": + "Marketplace subcategory to filter by (e.g. `cli-agent`). Takes precedence over `category` for the actual filter; when `category` is also supplied the pair must be consistent." + }) + ), + "sort": Schema.optionalKey( + Schema.Literals(["popular", "trending"]).annotate({ + "description": + "`popular` ranks apps by total token volume inside the date window. `trending` ranks apps by absolute excess token growth: window volume minus the average volume of the three equal-length periods immediately preceding the window. Apps with no excess growth are omitted from `trending` results." + }) + ), + "start_date": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Start of the date window in YYYY-MM-DD (UTC), inclusive. Defaults to 30 days before `end_date`. The dataset begins at 2025-01-01; earlier values are clamped forward to that floor and the resolved value is echoed in `meta.start_date`." + }).check( + Schema.isPattern(new RegExp("^\\d{4}-\\d{2}-\\d{2}$")).annotate({ + "expected": "a string matching the RegExp ^\\d{4}-\\d{2}-\\d{2}$" + }) + ) + ), + "end_date": Schema.optionalKey( + Schema.String.annotate({ + "description": + "End of the date window in YYYY-MM-DD (UTC), inclusive. Defaults to the most recent completed UTC day. Must be on or after 2025-01-01; earlier values are rejected with a 400." + }).check( + Schema.isPattern(new RegExp("^\\d{4}-\\d{2}-\\d{2}$")).annotate({ + "expected": "a string matching the RegExp ^\\d{4}-\\d{2}-\\d{2}$" + }) + ) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of apps to return (1-100). Defaults to 50." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" })), + Schema.Null + ]).annotate({ + "description": + "Number of ranked apps to skip before the first returned row (0-100). Defaults to 0. `rank` stays absolute, so the first row of `offset=50` is `rank: 51`." + }) + ) +}) +export type GetAppRankings200 = AppRankingsResponse +export const GetAppRankings200 = AppRankingsResponse +export type GetAppRankings400 = BadRequestResponse +export const GetAppRankings400 = BadRequestResponse +export type GetAppRankings401 = UnauthorizedResponse +export const GetAppRankings401 = UnauthorizedResponse +export type GetAppRankings429 = TooManyRequestsResponse +export const GetAppRankings429 = TooManyRequestsResponse +export type GetAppRankings500 = InternalServerResponse +export const GetAppRankings500 = InternalServerResponse +export type GetRankingsDailyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "start_date"?: string + readonly "end_date"?: string + readonly "period"?: "day" | "week" | "month" + readonly "modality"?: "text" | "image" | "image_output" | "audio" | "tool_calling" + readonly "context_bucket"?: "1K" | "10K" | "100K" | "1M" | "10M" + readonly "category"?: + | "programming" + | "roleplay" + | "marketing" + | "marketing/seo" + | "technology" + | "science" + | "translation" + | "legal" + | "finance" + | "health" + | "trivia" + | "academia" + readonly "language_type"?: "natural" | "programming" +} +export const GetRankingsDailyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "start_date": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Start of the date window in YYYY-MM-DD (UTC), inclusive. Defaults to 30 days before `end_date`. The dataset begins at 2025-01-01; earlier values are clamped forward to that floor and the resolved value is echoed in `meta.start_date`." + }).check( + Schema.isPattern(new RegExp("^\\d{4}-\\d{2}-\\d{2}$")).annotate({ + "expected": "a string matching the RegExp ^\\d{4}-\\d{2}-\\d{2}$" + }) + ) + ), + "end_date": Schema.optionalKey( + Schema.String.annotate({ + "description": + "End of the date window in YYYY-MM-DD (UTC), inclusive. Defaults to the most recent completed UTC day. Must be on or after 2025-01-01; earlier values are rejected with a 400." + }).check( + Schema.isPattern(new RegExp("^\\d{4}-\\d{2}-\\d{2}$")).annotate({ + "expected": "a string matching the RegExp ^\\d{4}-\\d{2}-\\d{2}$" + }) + ) + ), + "period": Schema.optionalKey( + Schema.Literals(["day", "week", "month"]).annotate({ + "description": + "Time grain of each row. `day` (default) returns the per-UTC-day series; `week` buckets by ISO week start; `month` buckets by month start. With `category` or `language_type` only `week` (default) and `month` are available — `day` is rejected with a 400 because those datasets are aggregated weekly. For those sampled datasets `period=month` buckets each week by its week-start month, so totals are approximate at month boundaries." + }) + ), + "modality": Schema.optionalKey( + Schema.Literals(["text", "image", "image_output", "audio", "tool_calling"]).annotate({ + "description": + "Restrict to models for a modality surface: `text` / `image_output` match output modality, `image` / `audio` match input modality, and `tool_calling` keeps only rows that recorded at least one tool call. Exact dataset — cannot be combined with `category` or `language_type`." + }) + ), + "context_bucket": Schema.optionalKey( + Schema.Literals(["1K", "10K", "100K", "1M", "10M"]).annotate({ + "description": + "Restrict to requests whose context length falls in this bucket (`1K`, `10K`, `100K`, `1M`, or `10M`). Exact dataset — cannot be combined with `category` or `language_type`." + }) + ), + "category": Schema.optionalKey( + Schema.Literals([ + "programming", + "roleplay", + "marketing", + "marketing/seo", + "technology", + "science", + "translation", + "legal", + "finance", + "health", + "trivia", + "academia" + ]).annotate({ + "description": + "Restrict to a use-case category (e.g. `programming`, `roleplay`). Sourced from a sampled, upsampled dataset, so `total_tokens` is an estimate and is aggregated weekly (the trailing weekly bucket may include traffic past `end_date`). Cannot be combined with `modality`, `context_bucket`, or `language_type`." + }) + ), + "language_type": Schema.optionalKey( + Schema.Literals(["natural", "programming"]).annotate({ + "description": + "Restrict to natural-language or programming-language tagged activity. Sourced from a sampled, upsampled dataset, so `total_tokens` is an estimate and is aggregated weekly (the trailing weekly bucket may include traffic past `end_date`). Cannot be combined with `modality`, `context_bucket`, or `category`." + }) + ) +}) +export type GetRankingsDaily200 = RankingsDailyResponse +export const GetRankingsDaily200 = RankingsDailyResponse +export type GetRankingsDaily400 = BadRequestResponse +export const GetRankingsDaily400 = BadRequestResponse +export type GetRankingsDaily401 = UnauthorizedResponse +export const GetRankingsDaily401 = UnauthorizedResponse +export type GetRankingsDaily429 = TooManyRequestsResponse +export const GetRankingsDaily429 = TooManyRequestsResponse +export type GetRankingsDaily500 = InternalServerResponse +export const GetRankingsDaily500 = InternalServerResponse +export type CreateEmbeddingsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateEmbeddingsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateEmbeddingsRequestJson = { + readonly "dimensions"?: number + readonly "encoding_format"?: "float" | "base64" + readonly "input": + | string + | ReadonlyArray + | ReadonlyArray + | ReadonlyArray> + | ReadonlyArray< + { + readonly "content": ReadonlyArray< + | { readonly "text": string; readonly "type": "text" } + | { readonly "image_url": { readonly "url": string }; readonly "type": "image_url" } + | ContentPartInputAudio + | ContentPartInputVideo + | ContentPartInputFile + > + } + > + readonly "input_type"?: string + readonly "model": string + readonly "provider"?: ProviderPreferences + readonly "user"?: string +} +export const CreateEmbeddingsRequestJson = Schema.Struct({ + "dimensions": Schema.optionalKey( + Schema.Number.annotate({ "description": "The number of dimensions for the output embeddings" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })) + ), + "encoding_format": Schema.optionalKey( + Schema.Literals(["float", "base64"]).annotate({ "description": "The format of the output embeddings" }) + ), + "input": Schema.Union([ + Schema.String, + Schema.Array(Schema.String), + Schema.Array(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))), + Schema.Array(Schema.Array(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })))), + Schema.Array(Schema.Struct({ + "content": Schema.Array( + Schema.Union([ + Schema.Struct({ "text": Schema.String, "type": Schema.Literal("text") }), + Schema.Struct({ "image_url": Schema.Struct({ "url": Schema.String }), "type": Schema.Literal("image_url") }), + ContentPartInputAudio, + ContentPartInputVideo, + ContentPartInputFile + ], { mode: "oneOf" }) + ) + })) + ]).annotate({ "description": "Text, token, or multimodal input(s) to embed" }), + "input_type": Schema.optionalKey( + Schema.String.annotate({ "description": "The type of input (e.g. search_query, search_document)" }) + ), + "model": Schema.String.annotate({ "description": "The model to use for embeddings" }), + "provider": Schema.optionalKey( + Schema.suspend((): Schema.Codec => ProviderPreferences).annotate({ + "description": "Provider routing preferences for the request." + }) + ), + "user": Schema.optionalKey(Schema.String.annotate({ "description": "A unique identifier for the end-user" })) +}).annotate({ "description": "Embeddings request input" }) +export type CreateEmbeddings200 = { + readonly "data": ReadonlyArray< + { readonly "embedding": ReadonlyArray | string; readonly "index"?: number; readonly "object": "embedding" } + > + readonly "id"?: string + readonly "model": string + readonly "object": "list" + readonly "usage"?: { + readonly "cost"?: number + readonly "cost_details"?: CostDetails + readonly "is_byok"?: boolean + readonly "prompt_tokens": number + readonly "prompt_tokens_details"?: { + readonly "audio_tokens"?: number + readonly "file_tokens"?: number + readonly "image_tokens"?: number + readonly "text_tokens"?: number + readonly "video_tokens"?: number + } + readonly "total_tokens": number + } +} +export const CreateEmbeddings200 = Schema.Struct({ + "data": Schema.Array( + Schema.Struct({ + "embedding": Schema.Union([ + Schema.Array(Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" }))), + Schema.String + ]).annotate({ "description": "Embedding vector as an array of floats or a base64 string" }), + "index": Schema.optionalKey( + Schema.Number.annotate({ "description": "Index of the embedding in the input list" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "object": Schema.Literal("embedding") + }).annotate({ "description": "A single embedding object" }) + ).annotate({ "description": "List of embedding objects" }), + "id": Schema.optionalKey(Schema.String.annotate({ "description": "Unique identifier for the embeddings response" })), + "model": Schema.String.annotate({ "description": "The model used for embeddings" }), + "object": Schema.Literal("list"), + "usage": Schema.optionalKey( + Schema.Struct({ + "cost": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cost of the request in credits", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "cost_details": Schema.optionalKey(CostDetails), + "is_byok": Schema.optionalKey( + Schema.Boolean.annotate({ + "description": "Whether a request was made using a Bring Your Own Key configuration" + }) + ), + "prompt_tokens": Schema.Number.annotate({ "description": "Number of tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "prompt_tokens_details": Schema.optionalKey( + Schema.Struct({ + "audio_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of audio tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "file_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of file/document tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "image_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of image tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "text_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of text tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "video_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of video tokens in the input" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) + }).annotate({ + "description": + "Per-modality token breakdown. Only present when the input contains 2+ modalities (e.g. text + image) and the upstream provider returns modality-level usage data. Only non-zero modality counts are included." + }) + ), + "total_tokens": Schema.Number.annotate({ "description": "Total number of tokens used" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + }).annotate({ "description": "Token usage statistics" }) + ) +}).annotate({ "description": "Embeddings response containing embedding vectors" }) +export type CreateEmbeddings200Sse = string +export const CreateEmbeddings200Sse = Schema.String.annotate({ + "description": "Not used for embeddings - embeddings do not support streaming" +}) +export type CreateEmbeddings400 = BadRequestResponse +export const CreateEmbeddings400 = BadRequestResponse +export type CreateEmbeddings401 = UnauthorizedResponse +export const CreateEmbeddings401 = UnauthorizedResponse +export type CreateEmbeddings402 = PaymentRequiredResponse +export const CreateEmbeddings402 = PaymentRequiredResponse +export type CreateEmbeddings404 = NotFoundResponse +export const CreateEmbeddings404 = NotFoundResponse +export type CreateEmbeddings429 = TooManyRequestsResponse +export const CreateEmbeddings429 = TooManyRequestsResponse +export type CreateEmbeddings500 = InternalServerResponse +export const CreateEmbeddings500 = InternalServerResponse +export type CreateEmbeddings502 = BadGatewayResponse +export const CreateEmbeddings502 = BadGatewayResponse +export type CreateEmbeddings503 = ServiceUnavailableResponse +export const CreateEmbeddings503 = ServiceUnavailableResponse +export type CreateEmbeddings524 = EdgeNetworkTimeoutResponse +export const CreateEmbeddings524 = EdgeNetworkTimeoutResponse +export type CreateEmbeddings529 = ProviderOverloadedResponse +export const CreateEmbeddings529 = ProviderOverloadedResponse +export type ListEmbeddingsModelsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListEmbeddingsModelsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ + "description": + "Number of records to skip for pagination. When both offset and limit are omitted, the full list is returned" + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of records to return (max 1000). When both offset and limit are omitted, the full list is returned" + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(1000).annotate({ "expected": "a value less than or equal to 1000" })) + ) +}) +export type ListEmbeddingsModels200 = ModelsListResponse +export const ListEmbeddingsModels200 = ModelsListResponse +export type ListEmbeddingsModels400 = BadRequestResponse +export const ListEmbeddingsModels400 = BadRequestResponse +export type ListEmbeddingsModels500 = InternalServerResponse +export const ListEmbeddingsModels500 = InternalServerResponse +export type ListEndpointsZdrParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListEndpointsZdrParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListEndpointsZdr200 = { readonly "data": ReadonlyArray } +export const ListEndpointsZdr200 = Schema.Struct({ "data": Schema.Array(PublicEndpoint) }) +export type ListEndpointsZdr403 = ForbiddenResponse +export const ListEndpointsZdr403 = ForbiddenResponse +export type ListEndpointsZdr500 = InternalServerResponse +export const ListEndpointsZdr500 = InternalServerResponse +export type ListFilesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "limit"?: number + readonly "cursor"?: string + readonly "workspace_id"?: string +} +export const ListFilesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of files to return (1–1000)." }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(1000).annotate({ "expected": "a value less than or equal to 1000" }) + ) + ), + "cursor": Schema.optionalKey( + Schema.String.annotate({ "description": "Opaque pagination cursor from a previous response." }) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Workspace to scope the request to. Defaults to the caller’s default workspace.", + "format": "uuid" + }) + ) +}) +export type ListFiles200 = FileListResponse +export const ListFiles200 = FileListResponse +export type ListFiles400 = BadRequestResponse +export const ListFiles400 = BadRequestResponse +export type ListFiles401 = UnauthorizedResponse +export const ListFiles401 = UnauthorizedResponse +export type ListFiles429 = TooManyRequestsResponse +export const ListFiles429 = TooManyRequestsResponse +export type ListFiles500 = InternalServerResponse +export const ListFiles500 = InternalServerResponse +export type UploadFileParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "workspace_id"?: string +} +export const UploadFileParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Workspace to scope the request to. Defaults to the caller’s default workspace.", + "format": "uuid" + }) + ) +}) +export type UploadFileRequestFormData = { readonly "file": string } +export const UploadFileRequestFormData = Schema.Struct({ "file": Schema.String.annotate({ "format": "binary" }) }) +export type UploadFile200 = FileMetadata +export const UploadFile200 = FileMetadata +export type UploadFile400 = BadRequestResponse +export const UploadFile400 = BadRequestResponse +export type UploadFile401 = UnauthorizedResponse +export const UploadFile401 = UnauthorizedResponse +export type UploadFile403 = ForbiddenResponse +export const UploadFile403 = ForbiddenResponse +export type UploadFile413 = PayloadTooLargeResponse +export const UploadFile413 = PayloadTooLargeResponse +export type UploadFile429 = TooManyRequestsResponse +export const UploadFile429 = TooManyRequestsResponse +export type UploadFile500 = InternalServerResponse +export const UploadFile500 = InternalServerResponse +export type GetFileMetadataParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "workspace_id"?: string +} +export const GetFileMetadataParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Workspace to scope the request to. Defaults to the caller’s default workspace.", + "format": "uuid" + }) + ) +}) +export type GetFileMetadata200 = FileMetadata +export const GetFileMetadata200 = FileMetadata +export type GetFileMetadata401 = UnauthorizedResponse +export const GetFileMetadata401 = UnauthorizedResponse +export type GetFileMetadata404 = NotFoundResponse +export const GetFileMetadata404 = NotFoundResponse +export type GetFileMetadata429 = TooManyRequestsResponse +export const GetFileMetadata429 = TooManyRequestsResponse +export type GetFileMetadata500 = InternalServerResponse +export const GetFileMetadata500 = InternalServerResponse +export type DeleteFileParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "workspace_id"?: string +} +export const DeleteFileParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Workspace to scope the request to. Defaults to the caller’s default workspace.", + "format": "uuid" + }) + ) +}) +export type DeleteFile200 = FileDeleteResponse +export const DeleteFile200 = FileDeleteResponse +export type DeleteFile401 = UnauthorizedResponse +export const DeleteFile401 = UnauthorizedResponse +export type DeleteFile404 = NotFoundResponse +export const DeleteFile404 = NotFoundResponse +export type DeleteFile429 = TooManyRequestsResponse +export const DeleteFile429 = TooManyRequestsResponse +export type DeleteFile500 = InternalServerResponse +export const DeleteFile500 = InternalServerResponse +export type DownloadFileContentParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "workspace_id"?: string +} +export const DownloadFileContentParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Workspace to scope the request to. Defaults to the caller’s default workspace.", + "format": "uuid" + }) + ) +}) +export type DownloadFileContent400 = BadRequestResponse +export const DownloadFileContent400 = BadRequestResponse +export type DownloadFileContent401 = UnauthorizedResponse +export const DownloadFileContent401 = UnauthorizedResponse +export type DownloadFileContent404 = NotFoundResponse +export const DownloadFileContent404 = NotFoundResponse +export type DownloadFileContent429 = TooManyRequestsResponse +export const DownloadFileContent429 = TooManyRequestsResponse +export type DownloadFileContent500 = InternalServerResponse +export const DownloadFileContent500 = InternalServerResponse +export type GetGenerationParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "id": string +} +export const GetGenerationParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "id": Schema.String.annotate({ "description": "The generation ID" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}) +export type GetGeneration200 = GenerationResponse +export const GetGeneration200 = GenerationResponse +export type GetGeneration401 = UnauthorizedResponse +export const GetGeneration401 = UnauthorizedResponse +export type GetGeneration402 = PaymentRequiredResponse +export const GetGeneration402 = PaymentRequiredResponse +export type GetGeneration404 = NotFoundResponse +export const GetGeneration404 = NotFoundResponse +export type GetGeneration429 = TooManyRequestsResponse +export const GetGeneration429 = TooManyRequestsResponse +export type GetGeneration500 = InternalServerResponse +export const GetGeneration500 = InternalServerResponse +export type GetGeneration502 = BadGatewayResponse +export const GetGeneration502 = BadGatewayResponse +export type GetGeneration524 = EdgeNetworkTimeoutResponse +export const GetGeneration524 = EdgeNetworkTimeoutResponse +export type GetGeneration529 = ProviderOverloadedResponse +export const GetGeneration529 = ProviderOverloadedResponse +export type ListGenerationContentParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "id": string +} +export const ListGenerationContentParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "id": Schema.String.annotate({ "description": "The generation ID" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ) +}) +export type ListGenerationContent200 = GenerationContentResponse +export const ListGenerationContent200 = GenerationContentResponse +export type ListGenerationContent401 = UnauthorizedResponse +export const ListGenerationContent401 = UnauthorizedResponse +export type ListGenerationContent403 = ForbiddenResponse +export const ListGenerationContent403 = ForbiddenResponse +export type ListGenerationContent404 = NotFoundResponse +export const ListGenerationContent404 = NotFoundResponse +export type ListGenerationContent429 = TooManyRequestsResponse +export const ListGenerationContent429 = TooManyRequestsResponse +export type ListGenerationContent500 = InternalServerResponse +export const ListGenerationContent500 = InternalServerResponse +export type ListGenerationContent502 = BadGatewayResponse +export const ListGenerationContent502 = BadGatewayResponse +export type ListGenerationContent524 = EdgeNetworkTimeoutResponse +export const ListGenerationContent524 = EdgeNetworkTimeoutResponse +export type ListGenerationContent529 = ProviderOverloadedResponse +export const ListGenerationContent529 = ProviderOverloadedResponse +export type SubmitGenerationFeedbackParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const SubmitGenerationFeedbackParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type SubmitGenerationFeedbackRequestJson = SubmitGenerationFeedbackRequest +export const SubmitGenerationFeedbackRequestJson = SubmitGenerationFeedbackRequest +export type SubmitGenerationFeedback200 = SubmitGenerationFeedbackResponse +export const SubmitGenerationFeedback200 = SubmitGenerationFeedbackResponse +export type SubmitGenerationFeedback400 = BadRequestResponse +export const SubmitGenerationFeedback400 = BadRequestResponse +export type SubmitGenerationFeedback401 = UnauthorizedResponse +export const SubmitGenerationFeedback401 = UnauthorizedResponse +export type SubmitGenerationFeedback404 = NotFoundResponse +export const SubmitGenerationFeedback404 = NotFoundResponse +export type SubmitGenerationFeedback429 = TooManyRequestsResponse +export const SubmitGenerationFeedback429 = TooManyRequestsResponse +export type SubmitGenerationFeedback500 = InternalServerResponse +export const SubmitGenerationFeedback500 = InternalServerResponse +export type ListGuardrailsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number + readonly "workspace_id"?: string +} +export const ListGuardrailsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter guardrails by workspace ID. By default, guardrails in the default workspace are returned.", + "format": "uuid" + }) + ) +}) +export type ListGuardrails200 = ListGuardrailsResponse +export const ListGuardrails200 = ListGuardrailsResponse +export type ListGuardrails400 = BadRequestResponse +export const ListGuardrails400 = BadRequestResponse +export type ListGuardrails401 = UnauthorizedResponse +export const ListGuardrails401 = UnauthorizedResponse +export type ListGuardrails500 = InternalServerResponse +export const ListGuardrails500 = InternalServerResponse +export type CreateGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateGuardrailRequestJson = CreateGuardrailRequest +export const CreateGuardrailRequestJson = CreateGuardrailRequest +export type CreateGuardrail201 = CreateGuardrailResponse +export const CreateGuardrail201 = CreateGuardrailResponse +export type CreateGuardrail400 = BadRequestResponse +export const CreateGuardrail400 = BadRequestResponse +export type CreateGuardrail401 = UnauthorizedResponse +export const CreateGuardrail401 = UnauthorizedResponse +export type CreateGuardrail403 = ForbiddenResponse +export const CreateGuardrail403 = ForbiddenResponse +export type CreateGuardrail500 = InternalServerResponse +export const CreateGuardrail500 = InternalServerResponse +export type GetGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetGuardrail200 = GetGuardrailResponse +export const GetGuardrail200 = GetGuardrailResponse +export type GetGuardrail401 = UnauthorizedResponse +export const GetGuardrail401 = UnauthorizedResponse +export type GetGuardrail404 = NotFoundResponse +export const GetGuardrail404 = NotFoundResponse +export type GetGuardrail500 = InternalServerResponse +export const GetGuardrail500 = InternalServerResponse +export type DeleteGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteGuardrail200 = DeleteGuardrailResponse +export const DeleteGuardrail200 = DeleteGuardrailResponse +export type DeleteGuardrail401 = UnauthorizedResponse +export const DeleteGuardrail401 = UnauthorizedResponse +export type DeleteGuardrail404 = NotFoundResponse +export const DeleteGuardrail404 = NotFoundResponse +export type DeleteGuardrail500 = InternalServerResponse +export const DeleteGuardrail500 = InternalServerResponse +export type UpdateGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpdateGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpdateGuardrailRequestJson = UpdateGuardrailRequest +export const UpdateGuardrailRequestJson = UpdateGuardrailRequest +export type UpdateGuardrail200 = UpdateGuardrailResponse +export const UpdateGuardrail200 = UpdateGuardrailResponse +export type UpdateGuardrail400 = BadRequestResponse +export const UpdateGuardrail400 = BadRequestResponse +export type UpdateGuardrail401 = UnauthorizedResponse +export const UpdateGuardrail401 = UnauthorizedResponse +export type UpdateGuardrail404 = NotFoundResponse +export const UpdateGuardrail404 = NotFoundResponse +export type UpdateGuardrail500 = InternalServerResponse +export const UpdateGuardrail500 = InternalServerResponse +export type ListGuardrailKeyAssignmentsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListGuardrailKeyAssignmentsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListGuardrailKeyAssignments200 = ListKeyAssignmentsResponse +export const ListGuardrailKeyAssignments200 = ListKeyAssignmentsResponse +export type ListGuardrailKeyAssignments401 = UnauthorizedResponse +export const ListGuardrailKeyAssignments401 = UnauthorizedResponse +export type ListGuardrailKeyAssignments404 = NotFoundResponse +export const ListGuardrailKeyAssignments404 = NotFoundResponse +export type ListGuardrailKeyAssignments500 = InternalServerResponse +export const ListGuardrailKeyAssignments500 = InternalServerResponse +export type BulkAssignKeysToGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkAssignKeysToGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkAssignKeysToGuardrailRequestJson = BulkAssignKeysRequest +export const BulkAssignKeysToGuardrailRequestJson = BulkAssignKeysRequest +export type BulkAssignKeysToGuardrail200 = BulkAssignKeysResponse +export const BulkAssignKeysToGuardrail200 = BulkAssignKeysResponse +export type BulkAssignKeysToGuardrail400 = BadRequestResponse +export const BulkAssignKeysToGuardrail400 = BadRequestResponse +export type BulkAssignKeysToGuardrail401 = UnauthorizedResponse +export const BulkAssignKeysToGuardrail401 = UnauthorizedResponse +export type BulkAssignKeysToGuardrail404 = NotFoundResponse +export const BulkAssignKeysToGuardrail404 = NotFoundResponse +export type BulkAssignKeysToGuardrail500 = InternalServerResponse +export const BulkAssignKeysToGuardrail500 = InternalServerResponse +export type BulkUnassignKeysFromGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkUnassignKeysFromGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkUnassignKeysFromGuardrailRequestJson = BulkUnassignKeysRequest +export const BulkUnassignKeysFromGuardrailRequestJson = BulkUnassignKeysRequest +export type BulkUnassignKeysFromGuardrail200 = BulkUnassignKeysResponse +export const BulkUnassignKeysFromGuardrail200 = BulkUnassignKeysResponse +export type BulkUnassignKeysFromGuardrail400 = BadRequestResponse +export const BulkUnassignKeysFromGuardrail400 = BadRequestResponse +export type BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse +export const BulkUnassignKeysFromGuardrail401 = UnauthorizedResponse +export type BulkUnassignKeysFromGuardrail404 = NotFoundResponse +export const BulkUnassignKeysFromGuardrail404 = NotFoundResponse +export type BulkUnassignKeysFromGuardrail500 = InternalServerResponse +export const BulkUnassignKeysFromGuardrail500 = InternalServerResponse +export type ListGuardrailMemberAssignmentsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListGuardrailMemberAssignmentsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListGuardrailMemberAssignments200 = ListMemberAssignmentsResponse +export const ListGuardrailMemberAssignments200 = ListMemberAssignmentsResponse +export type ListGuardrailMemberAssignments401 = UnauthorizedResponse +export const ListGuardrailMemberAssignments401 = UnauthorizedResponse +export type ListGuardrailMemberAssignments404 = NotFoundResponse +export const ListGuardrailMemberAssignments404 = NotFoundResponse +export type ListGuardrailMemberAssignments500 = InternalServerResponse +export const ListGuardrailMemberAssignments500 = InternalServerResponse +export type BulkAssignMembersToGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkAssignMembersToGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkAssignMembersToGuardrailRequestJson = BulkAssignMembersRequest +export const BulkAssignMembersToGuardrailRequestJson = BulkAssignMembersRequest +export type BulkAssignMembersToGuardrail200 = BulkAssignMembersResponse +export const BulkAssignMembersToGuardrail200 = BulkAssignMembersResponse +export type BulkAssignMembersToGuardrail400 = BadRequestResponse +export const BulkAssignMembersToGuardrail400 = BadRequestResponse +export type BulkAssignMembersToGuardrail401 = UnauthorizedResponse +export const BulkAssignMembersToGuardrail401 = UnauthorizedResponse +export type BulkAssignMembersToGuardrail404 = NotFoundResponse +export const BulkAssignMembersToGuardrail404 = NotFoundResponse +export type BulkAssignMembersToGuardrail500 = InternalServerResponse +export const BulkAssignMembersToGuardrail500 = InternalServerResponse +export type BulkUnassignMembersFromGuardrailParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkUnassignMembersFromGuardrailParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkUnassignMembersFromGuardrailRequestJson = BulkUnassignMembersRequest +export const BulkUnassignMembersFromGuardrailRequestJson = BulkUnassignMembersRequest +export type BulkUnassignMembersFromGuardrail200 = BulkUnassignMembersResponse +export const BulkUnassignMembersFromGuardrail200 = BulkUnassignMembersResponse +export type BulkUnassignMembersFromGuardrail400 = BadRequestResponse +export const BulkUnassignMembersFromGuardrail400 = BadRequestResponse +export type BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse +export const BulkUnassignMembersFromGuardrail401 = UnauthorizedResponse +export type BulkUnassignMembersFromGuardrail404 = NotFoundResponse +export const BulkUnassignMembersFromGuardrail404 = NotFoundResponse +export type BulkUnassignMembersFromGuardrail500 = InternalServerResponse +export const BulkUnassignMembersFromGuardrail500 = InternalServerResponse +export type ListKeyAssignmentsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListKeyAssignmentsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListKeyAssignments200 = ListKeyAssignmentsResponse +export const ListKeyAssignments200 = ListKeyAssignmentsResponse +export type ListKeyAssignments401 = UnauthorizedResponse +export const ListKeyAssignments401 = UnauthorizedResponse +export type ListKeyAssignments500 = InternalServerResponse +export const ListKeyAssignments500 = InternalServerResponse +export type ListMemberAssignmentsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListMemberAssignmentsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListMemberAssignments200 = ListMemberAssignmentsResponse +export const ListMemberAssignments200 = ListMemberAssignmentsResponse +export type ListMemberAssignments401 = UnauthorizedResponse +export const ListMemberAssignments401 = UnauthorizedResponse +export type ListMemberAssignments500 = InternalServerResponse +export const ListMemberAssignments500 = InternalServerResponse +export type CreateImagesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateImagesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateImagesRequestJson = ImageGenerationRequest +export const CreateImagesRequestJson = ImageGenerationRequest +export type CreateImages200 = ImageGenerationResponse +export const CreateImages200 = ImageGenerationResponse +export type CreateImages200Sse = ImageStreamingResponse +export const CreateImages200Sse = ImageStreamingResponse +export type CreateImages400 = BadRequestResponse +export const CreateImages400 = BadRequestResponse +export type CreateImages401 = UnauthorizedResponse +export const CreateImages401 = UnauthorizedResponse +export type CreateImages402 = PaymentRequiredResponse +export const CreateImages402 = PaymentRequiredResponse +export type CreateImages403 = ForbiddenResponse +export const CreateImages403 = ForbiddenResponse +export type CreateImages404 = NotFoundResponse +export const CreateImages404 = NotFoundResponse +export type CreateImages413 = PayloadTooLargeResponse +export const CreateImages413 = PayloadTooLargeResponse +export type CreateImages429 = TooManyRequestsResponse +export const CreateImages429 = TooManyRequestsResponse +export type CreateImages500 = InternalServerResponse +export const CreateImages500 = InternalServerResponse +export type CreateImages502 = BadGatewayResponse +export const CreateImages502 = BadGatewayResponse +export type CreateImages524 = EdgeNetworkTimeoutResponse +export const CreateImages524 = EdgeNetworkTimeoutResponse +export type CreateImages529 = ProviderOverloadedResponse +export const CreateImages529 = ProviderOverloadedResponse +export type ListImageModelsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListImageModelsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListImageModels200 = ImageModelsListResponse +export const ListImageModels200 = ImageModelsListResponse +export type ListImageModels500 = InternalServerResponse +export const ListImageModels500 = InternalServerResponse +export type ListImageModelEndpointsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListImageModelEndpointsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListImageModelEndpoints200 = ImageModelEndpointsResponse +export const ListImageModelEndpoints200 = ImageModelEndpointsResponse +export type ListImageModelEndpoints404 = NotFoundResponse +export const ListImageModelEndpoints404 = NotFoundResponse +export type ListImageModelEndpoints500 = InternalServerResponse +export const ListImageModelEndpoints500 = InternalServerResponse +export type GetCurrentKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetCurrentKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetCurrentKey200 = { + readonly "data": { + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_monthly": number + readonly "byok_usage_weekly": number + readonly "creator_user_id": string | null + readonly "expires_at"?: string | null + readonly "include_byok_in_limit": boolean + readonly "is_free_tier": boolean + readonly "is_management_key": boolean + readonly "is_provisioning_key": boolean + readonly "label": string + readonly "limit": number | null + readonly "limit_remaining": number | null + readonly "limit_reset": string | null + readonly "rate_limit": { readonly "interval": string; readonly "note": string; readonly "requests": number } + readonly "usage": number + readonly "usage_daily": number + readonly "usage_monthly": number + readonly "usage_weekly": number + } +} +export const GetCurrentKey200 = Schema.Struct({ + "data": Schema.Struct({ + "byok_usage": Schema.Number.annotate({ + "description": "Total external BYOK usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "creator_user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The user ID of the key creator. For organization-owned keys, this is the member who created the key. For individual users, this is the user's own ID." + }), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "is_free_tier": Schema.Boolean.annotate({ "description": "Whether this is a free tier API key" }), + "is_management_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), + "is_provisioning_key": Schema.Boolean.annotate({ "description": "Whether this is a management key" }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Spending limit for the API key in USD", "format": "double" }), + "limit_remaining": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Remaining spending limit in USD", "format": "double" }), + "limit_reset": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Type of limit reset for the API key" + }), + "rate_limit": Schema.Struct({ + "interval": Schema.String.annotate({ "description": "Rate limit interval" }), + "note": Schema.String.annotate({ "description": "Note about the rate limit" }), + "requests": Schema.Number.annotate({ "description": "Number of requests allowed per interval" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + }).annotate({ "description": "Legacy rate limit information about a key. Will always return -1." }), + "usage": Schema.Number.annotate({ + "description": "Total OpenRouter credit usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_daily": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + }).annotate({ "description": "Current API key information" }) +}) +export type GetCurrentKey401 = UnauthorizedResponse +export const GetCurrentKey401 = UnauthorizedResponse +export type GetCurrentKey500 = InternalServerResponse +export const GetCurrentKey500 = InternalServerResponse +export type ListParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "include_disabled"?: boolean + readonly "offset"?: number | null + readonly "workspace_id"?: string +} +export const ListParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "include_disabled": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to include disabled API keys in the response" }) + ), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of API keys to skip for pagination" }) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter API keys by workspace ID. By default, keys in the default workspace are returned.", + "format": "uuid" + }) + ) +}) +export type List200 = { + readonly "data": ReadonlyArray< + { + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_monthly": number + readonly "byok_usage_weekly": number + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "disabled": boolean + readonly "expires_at"?: string | null + readonly "hash": string + readonly "include_byok_in_limit": boolean + readonly "label": string + readonly "limit": number | null + readonly "limit_remaining": number | null + readonly "limit_reset": string | null + readonly "name": string + readonly "updated_at": string | null + readonly "usage": number + readonly "usage_daily": number + readonly "usage_monthly": number + readonly "usage_weekly": number + readonly "workspace_id": string + } + > +} +export const List200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "byok_usage": Schema.Number.annotate({ + "description": "Total external BYOK usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "creator_user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The user ID of the key creator. For organization-owned keys, this is the member who created the key. For individual users, this is the user's own ID." + }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Spending limit for the API key in USD", "format": "double" }), + "limit_remaining": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Remaining spending limit in USD", "format": "double" }), + "limit_reset": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Type of limit reset for the API key" + }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "updated_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the API key was last updated" + }), + "usage": Schema.Number.annotate({ + "description": "Total OpenRouter credit usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_daily": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "workspace_id": Schema.String.annotate({ "description": "The workspace ID this API key belongs to." }) + })).annotate({ "description": "List of API keys" }) +}) +export type List400 = BadRequestResponse +export const List400 = BadRequestResponse +export type List401 = UnauthorizedResponse +export const List401 = UnauthorizedResponse +export type List429 = TooManyRequestsResponse +export const List429 = TooManyRequestsResponse +export type List500 = InternalServerResponse +export const List500 = InternalServerResponse +export type CreateKeysParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateKeysParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateKeysRequestJson = { + readonly "creator_user_id"?: string | null + readonly "expires_at"?: string | null + readonly "include_byok_in_limit"?: boolean + readonly "limit"?: number | null + readonly "limit_reset"?: "daily" | "weekly" | "monthly" | null + readonly "name": string + readonly "workspace_id"?: string +} +export const CreateKeysRequestJson = Schema.Struct({ + "creator_user_id": Schema.optionalKey( + Schema.Union([ + Schema.String.check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + Schema.Null + ]).annotate({ + "description": + "Optional user ID of the key creator. Only meaningful for organization-owned keys where a specific member is creating the key." + }) + ), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "Optional ISO 8601 UTC timestamp when the API key should expire. Must be UTC, other timezones will be rejected", + "format": "date-time" + }) + ), + "include_byok_in_limit": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) + ), + "limit": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "Optional spending limit for the API key in USD", "format": "double" }) + ), + "limit_reset": Schema.optionalKey( + Schema.Union([Schema.Literal("daily"), Schema.Literal("weekly"), Schema.Literal("monthly"), Schema.Null]).annotate({ + "description": + "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." + }) + ), + "name": Schema.String.annotate({ "description": "Name for the new API key" }).check( + Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" }) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "The workspace to create the API key in. Defaults to the default workspace if not provided.", + "format": "uuid" + }) + ) +}) +export type CreateKeys201 = { + readonly "data": { + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_monthly": number + readonly "byok_usage_weekly": number + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "disabled": boolean + readonly "expires_at"?: string | null + readonly "hash": string + readonly "include_byok_in_limit": boolean + readonly "label": string + readonly "limit": number | null + readonly "limit_remaining": number | null + readonly "limit_reset": string | null + readonly "name": string + readonly "updated_at": string | null + readonly "usage": number + readonly "usage_daily": number + readonly "usage_monthly": number + readonly "usage_weekly": number + readonly "workspace_id": string + } + readonly "key": string +} +export const CreateKeys201 = Schema.Struct({ + "data": Schema.Struct({ + "byok_usage": Schema.Number.annotate({ + "description": "Total external BYOK usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "creator_user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The user ID of the key creator. For organization-owned keys, this is the member who created the key. For individual users, this is the user's own ID." + }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Spending limit for the API key in USD", "format": "double" }), + "limit_remaining": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Remaining spending limit in USD", "format": "double" }), + "limit_reset": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Type of limit reset for the API key" + }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "updated_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the API key was last updated" + }), + "usage": Schema.Number.annotate({ + "description": "Total OpenRouter credit usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_daily": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "workspace_id": Schema.String.annotate({ "description": "The workspace ID this API key belongs to." }) + }).annotate({ "description": "The created API key information" }), + "key": Schema.String.annotate({ "description": "The actual API key string (only shown once)" }) +}) +export type CreateKeys400 = BadRequestResponse +export const CreateKeys400 = BadRequestResponse +export type CreateKeys401 = UnauthorizedResponse +export const CreateKeys401 = UnauthorizedResponse +export type CreateKeys403 = ForbiddenResponse +export const CreateKeys403 = ForbiddenResponse +export type CreateKeys429 = TooManyRequestsResponse +export const CreateKeys429 = TooManyRequestsResponse +export type CreateKeys500 = InternalServerResponse +export const CreateKeys500 = InternalServerResponse +export type GetKeyParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetKeyParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetKey200 = { + readonly "data": { + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_monthly": number + readonly "byok_usage_weekly": number + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "disabled": boolean + readonly "expires_at"?: string | null + readonly "hash": string + readonly "include_byok_in_limit": boolean + readonly "label": string + readonly "limit": number | null + readonly "limit_remaining": number | null + readonly "limit_reset": string | null + readonly "name": string + readonly "updated_at": string | null + readonly "usage": number + readonly "usage_daily": number + readonly "usage_monthly": number + readonly "usage_weekly": number + readonly "workspace_id": string + } +} +export const GetKey200 = Schema.Struct({ + "data": Schema.Struct({ + "byok_usage": Schema.Number.annotate({ + "description": "Total external BYOK usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "creator_user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The user ID of the key creator. For organization-owned keys, this is the member who created the key. For individual users, this is the user's own ID." + }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Spending limit for the API key in USD", "format": "double" }), + "limit_remaining": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Remaining spending limit in USD", "format": "double" }), + "limit_reset": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Type of limit reset for the API key" + }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "updated_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the API key was last updated" + }), + "usage": Schema.Number.annotate({ + "description": "Total OpenRouter credit usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_daily": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "workspace_id": Schema.String.annotate({ "description": "The workspace ID this API key belongs to." }) + }).annotate({ "description": "The API key information" }) +}) +export type GetKey401 = UnauthorizedResponse +export const GetKey401 = UnauthorizedResponse +export type GetKey404 = NotFoundResponse +export const GetKey404 = NotFoundResponse +export type GetKey429 = TooManyRequestsResponse +export const GetKey429 = TooManyRequestsResponse +export type GetKey500 = InternalServerResponse +export const GetKey500 = InternalServerResponse +export type DeleteKeysParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteKeysParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteKeys200 = { readonly "deleted": true } +export const DeleteKeys200 = Schema.Struct({ + "deleted": Schema.Literal(true).annotate({ "description": "Confirmation that the API key was deleted" }) +}) +export type DeleteKeys401 = UnauthorizedResponse +export const DeleteKeys401 = UnauthorizedResponse +export type DeleteKeys404 = NotFoundResponse +export const DeleteKeys404 = NotFoundResponse +export type DeleteKeys429 = TooManyRequestsResponse +export const DeleteKeys429 = TooManyRequestsResponse +export type DeleteKeys500 = InternalServerResponse +export const DeleteKeys500 = InternalServerResponse +export type UpdateKeysParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpdateKeysParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpdateKeysRequestJson = { + readonly "disabled"?: boolean + readonly "include_byok_in_limit"?: boolean + readonly "limit"?: number | null + readonly "limit_reset"?: "daily" | "weekly" | "monthly" | null + readonly "name"?: string +} +export const UpdateKeysRequestJson = Schema.Struct({ + "disabled": Schema.optionalKey(Schema.Boolean.annotate({ "description": "Whether to disable the API key" })), + "include_byok_in_limit": Schema.optionalKey( + Schema.Boolean.annotate({ "description": "Whether to include BYOK usage in the limit" }) + ), + "limit": Schema.optionalKey( + Schema.Union([Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), Schema.Null]) + .annotate({ "description": "New spending limit for the API key in USD", "format": "double" }) + ), + "limit_reset": Schema.optionalKey( + Schema.Union([Schema.Literal("daily"), Schema.Literal("weekly"), Schema.Literal("monthly"), Schema.Null]).annotate({ + "description": + "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." + }) + ), + "name": Schema.optionalKey(Schema.String.annotate({ "description": "New name for the API key" })) +}) +export type UpdateKeys200 = { + readonly "data": { + readonly "byok_usage": number + readonly "byok_usage_daily": number + readonly "byok_usage_monthly": number + readonly "byok_usage_weekly": number + readonly "created_at": string + readonly "creator_user_id": string | null + readonly "disabled": boolean + readonly "expires_at"?: string | null + readonly "hash": string + readonly "include_byok_in_limit": boolean + readonly "label": string + readonly "limit": number | null + readonly "limit_remaining": number | null + readonly "limit_reset": string | null + readonly "name": string + readonly "updated_at": string | null + readonly "usage": number + readonly "usage_daily": number + readonly "usage_monthly": number + readonly "usage_weekly": number + readonly "workspace_id": string + } +} +export const UpdateKeys200 = Schema.Struct({ + "data": Schema.Struct({ + "byok_usage": Schema.Number.annotate({ + "description": "Total external BYOK usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_daily": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_monthly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "byok_usage_weekly": Schema.Number.annotate({ + "description": "External BYOK usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "created_at": Schema.String.annotate({ "description": "ISO 8601 timestamp of when the API key was created" }), + "creator_user_id": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": + "The user ID of the key creator. For organization-owned keys, this is the member who created the key. For individual users, this is the user's own ID." + }), + "disabled": Schema.Boolean.annotate({ "description": "Whether the API key is disabled" }), + "expires_at": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 UTC timestamp when the API key expires, or null if no expiration", + "format": "date-time" + }) + ), + "hash": Schema.String.annotate({ "description": "Unique hash identifier for the API key" }), + "include_byok_in_limit": Schema.Boolean.annotate({ + "description": "Whether to include external BYOK usage in the credit limit" + }), + "label": Schema.String.annotate({ "description": "Human-readable label for the API key" }), + "limit": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Spending limit for the API key in USD", "format": "double" }), + "limit_remaining": Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })), + Schema.Null + ]).annotate({ "description": "Remaining spending limit in USD", "format": "double" }), + "limit_reset": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "Type of limit reset for the API key" + }), + "name": Schema.String.annotate({ "description": "Name of the API key" }), + "updated_at": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "ISO 8601 timestamp of when the API key was last updated" + }), + "usage": Schema.Number.annotate({ + "description": "Total OpenRouter credit usage (in USD) for the API key", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_daily": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC day", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_monthly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC month", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "usage_weekly": Schema.Number.annotate({ + "description": "OpenRouter credit usage (in USD) for the current UTC week (Monday-Sunday)", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })), + "workspace_id": Schema.String.annotate({ "description": "The workspace ID this API key belongs to." }) + }).annotate({ "description": "The updated API key information" }) +}) +export type UpdateKeys400 = BadRequestResponse +export const UpdateKeys400 = BadRequestResponse +export type UpdateKeys401 = UnauthorizedResponse +export const UpdateKeys401 = UnauthorizedResponse +export type UpdateKeys404 = NotFoundResponse +export const UpdateKeys404 = NotFoundResponse +export type UpdateKeys429 = TooManyRequestsResponse +export const UpdateKeys429 = TooManyRequestsResponse +export type UpdateKeys500 = InternalServerResponse +export const UpdateKeys500 = InternalServerResponse +export type CreateMessagesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "X-OpenRouter-Metadata"?: MetadataLevel +} +export const CreateMessagesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "X-OpenRouter-Metadata": Schema.optionalKey(MetadataLevel) +}) +export type CreateMessagesRequestJson = MessagesRequest +export const CreateMessagesRequestJson = MessagesRequest +export type CreateMessages200 = MessagesResult +export const CreateMessages200 = MessagesResult +export type CreateMessages200Sse = MessagesStreamingResponse +export const CreateMessages200Sse = MessagesStreamingResponse +export type CreateMessages400 = MessagesErrorResponse +export const CreateMessages400 = MessagesErrorResponse +export type CreateMessages401 = MessagesErrorResponse +export const CreateMessages401 = MessagesErrorResponse +export type CreateMessages403 = ForbiddenResponse +export const CreateMessages403 = ForbiddenResponse +export type CreateMessages404 = MessagesErrorResponse +export const CreateMessages404 = MessagesErrorResponse +export type CreateMessages429 = MessagesErrorResponse +export const CreateMessages429 = MessagesErrorResponse +export type CreateMessages500 = MessagesErrorResponse +export const CreateMessages500 = MessagesErrorResponse +export type CreateMessages503 = MessagesErrorResponse +export const CreateMessages503 = MessagesErrorResponse +export type CreateMessages529 = MessagesErrorResponse +export const CreateMessages529 = MessagesErrorResponse +export type GetModelParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetModelParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetModel200 = ModelResponse +export const GetModel200 = ModelResponse +export type GetModel403 = ForbiddenResponse +export const GetModel403 = ForbiddenResponse +export type GetModel404 = NotFoundResponse +export const GetModel404 = NotFoundResponse +export type GetModel500 = InternalServerResponse +export const GetModel500 = InternalServerResponse +export type GetModelsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number + readonly "category"?: + | "programming" + | "roleplay" + | "marketing" + | "marketing/seo" + | "technology" + | "science" + | "translation" + | "legal" + | "finance" + | "health" + | "trivia" + | "academia" + readonly "supported_parameters"?: string + readonly "output_modalities"?: string + readonly "sort"?: + | "most-popular" + | "newest" + | "top-weekly" + | "pricing-low-to-high" + | "pricing-high-to-low" + | "context-high-to-low" + | "throughput-high-to-low" + | "latency-low-to-high" + | "intelligence-high-to-low" + | "coding-high-to-low" + | "agentic-high-to-low" + | "design-arena-elo-high-to-low" + readonly "q"?: string + readonly "input_modalities"?: string + readonly "context"?: number + readonly "min_price"?: number | null + readonly "max_price"?: number | null + readonly "arch"?: string + readonly "model_authors"?: string + readonly "providers"?: string + readonly "distillable"?: "true" | "false" + readonly "zdr"?: "true" + readonly "region"?: "eu" + readonly "min_output_price"?: number | null + readonly "max_output_price"?: number | null + readonly "min_age_days"?: number | null + readonly "max_age_days"?: number | null + readonly "min_intelligence_index"?: number | null + readonly "max_intelligence_index"?: number | null + readonly "min_coding_index"?: number | null + readonly "max_coding_index"?: number | null + readonly "min_agentic_index"?: number | null + readonly "max_agentic_index"?: number | null + readonly "min_tool_success_rate"?: number | null + readonly "max_tool_success_rate"?: number | null +} +export const GetModelsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ + "description": + "Number of records to skip for pagination. When both offset and limit are omitted, the full list is returned" + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of records to return (max 1000). When both offset and limit are omitted, the full list is returned" + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(1000).annotate({ "expected": "a value less than or equal to 1000" })) + ), + "category": Schema.optionalKey( + Schema.Literals([ + "programming", + "roleplay", + "marketing", + "marketing/seo", + "technology", + "science", + "translation", + "legal", + "finance", + "health", + "trivia", + "academia" + ]).annotate({ "description": "Filter models by use case category" }) + ), + "supported_parameters": Schema.optionalKey( + Schema.String.annotate({ "description": "Filter models by supported parameter (comma-separated)" }) + ), + "output_modalities": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Filter models by output modality. Accepts a comma-separated list of modalities (text, image, audio, embeddings) or \"all\" to include all models. Defaults to \"text\"." + }) + ), + "sort": Schema.optionalKey( + Schema.Literals([ + "most-popular", + "newest", + "top-weekly", + "pricing-low-to-high", + "pricing-high-to-low", + "context-high-to-low", + "throughput-high-to-low", + "latency-low-to-high", + "intelligence-high-to-low", + "coding-high-to-low", + "agentic-high-to-low", + "design-arena-elo-high-to-low" + ]).annotate({ + "description": + "Sort the returned models server-side. Prefer this over fetching the full list and sorting client-side. Options: pricing-low-to-high, pricing-high-to-low (average prompt/completion price), context-high-to-low (context length), throughput-high-to-low, latency-low-to-high (recent median performance), most-popular, top-weekly (tokens processed in the last week), newest (creation date), intelligence-high-to-low, coding-high-to-low, agentic-high-to-low (Artificial Analysis indices), design-arena-elo-high-to-low (best Design Arena ELO across arenas). Models without a score for the chosen benchmark are placed last. When omitted, the existing default ordering is preserved." + }) + ), + "q": Schema.optionalKey(Schema.String.annotate({ "description": "Free-text search by model name or slug." })), + "input_modalities": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter models by input modality. Comma-separated list of: text, image, audio, file." + }) + ), + "context": Schema.optionalKey( + Schema.Number.annotate({ + "description": "Minimum context length (tokens). Models with smaller context are excluded." + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ) + ), + "min_price": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum prompt price in $/M tokens." }) + ), + "max_price": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum prompt price in $/M tokens." }) + ), + "arch": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter models by architecture/model family (e.g. GPT, Claude, Gemini, Llama)." + }) + ), + "model_authors": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter models by the organization that created the model. Comma-separated list of author slugs." + }) + ), + "providers": Schema.optionalKey( + Schema.String.annotate({ + "description": "Filter models by hosting provider. Comma-separated list of provider names." + }) + ), + "distillable": Schema.optionalKey( + Schema.Literals(["true", "false"]).annotate({ + "description": + "Filter by distillation capability. \"true\" returns only distillable models, \"false\" excludes them." + }) + ), + "zdr": Schema.optionalKey( + Schema.Literal("true").annotate({ + "description": "When set to \"true\", return only models with zero data retention endpoints." + }) + ), + "region": Schema.optionalKey( + Schema.Literal("eu").annotate({ + "description": "Filter to models with endpoints in the given data region. Currently only \"eu\" is supported." + }) + ), + "min_output_price": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum completion (output) price in $/M tokens." }) + ), + "max_output_price": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum completion (output) price in $/M tokens." }) + ), + "min_age_days": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum model age in days since its creation date." }) + ), + "max_age_days": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum model age in days since its creation date." }) + ), + "min_intelligence_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum Artificial Analysis intelligence index." }) + ), + "max_intelligence_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum Artificial Analysis intelligence index." }) + ), + "min_coding_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum Artificial Analysis coding index." }) + ), + "max_coding_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum Artificial Analysis coding index." }) + ), + "min_agentic_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Minimum Artificial Analysis agentic index." }) + ), + "max_agentic_index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Maximum Artificial Analysis agentic index." }) + ), + "min_tool_success_rate": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(1).annotate({ "expected": "a value less than or equal to 1" })), + Schema.Null + ]).annotate({ + "description": + "Minimum tool-calling success rate, as a fraction in [0, 1] (e.g. 0.9 = 90% of requests finishing with a tool_calls finish reason)." + }) + ), + "max_tool_success_rate": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isFinite().annotate({ "expected": "a finite number" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ).check(Schema.isLessThanOrEqualTo(1).annotate({ "expected": "a value less than or equal to 1" })), + Schema.Null + ]).annotate({ "description": "Maximum tool-calling success rate, as a fraction in [0, 1]." }) + ) +}) +export type GetModels200 = ModelsListResponse +export const GetModels200 = ModelsListResponse +export type GetModels400 = BadRequestResponse +export const GetModels400 = BadRequestResponse +export type GetModels403 = ForbiddenResponse +export const GetModels403 = ForbiddenResponse +export type GetModels500 = InternalServerResponse +export const GetModels500 = InternalServerResponse +export type ListEndpointsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListEndpointsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListEndpoints200 = { readonly "data": ListEndpointsResponse } +export const ListEndpoints200 = Schema.Struct({ "data": ListEndpointsResponse }) +export type ListEndpoints403 = ForbiddenResponse +export const ListEndpoints403 = ForbiddenResponse +export type ListEndpoints404 = NotFoundResponse +export const ListEndpoints404 = NotFoundResponse +export type ListEndpoints500 = InternalServerResponse +export const ListEndpoints500 = InternalServerResponse +export type ListModelsCountParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "output_modalities"?: string +} +export const ListModelsCountParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "output_modalities": Schema.optionalKey( + Schema.String.annotate({ + "description": + "Filter models by output modality. Accepts a comma-separated list of modalities (text, image, audio, embeddings) or \"all\" to include all models. Defaults to \"text\"." + }) + ) +}) +export type ListModelsCount200 = ModelsCountResponse +export const ListModelsCount200 = ModelsCountResponse +export type ListModelsCount400 = BadRequestResponse +export const ListModelsCount400 = BadRequestResponse +export type ListModelsCount403 = ForbiddenResponse +export const ListModelsCount403 = ForbiddenResponse +export type ListModelsCount500 = InternalServerResponse +export const ListModelsCount500 = InternalServerResponse +export type ListModelsUserParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListModelsUserParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ + "description": + "Number of records to skip for pagination. When both offset and limit are omitted, the full list is returned" + }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ + "description": + "Maximum number of records to return (max 1000). When both offset and limit are omitted, the full list is returned" + }).check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" }) + ).check(Schema.isLessThanOrEqualTo(1000).annotate({ "expected": "a value less than or equal to 1000" })) + ) +}) +export type ListModelsUser200 = ModelsListResponse +export const ListModelsUser200 = ModelsListResponse +export type ListModelsUser401 = UnauthorizedResponse +export const ListModelsUser401 = UnauthorizedResponse +export type ListModelsUser403 = ForbiddenResponse +export const ListModelsUser403 = ForbiddenResponse +export type ListModelsUser404 = NotFoundResponse +export const ListModelsUser404 = NotFoundResponse +export type ListModelsUser500 = InternalServerResponse +export const ListModelsUser500 = InternalServerResponse +export type ListObservabilityDestinationsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number + readonly "workspace_id"?: string +} +export const ListObservabilityDestinationsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ), + "workspace_id": Schema.optionalKey( + Schema.String.annotate({ + "description": "Optional workspace ID to filter by. Defaults to the authenticated entity's default workspace.", + "format": "uuid" + }) + ) +}) +export type ListObservabilityDestinations200 = ListObservabilityDestinationsResponse +export const ListObservabilityDestinations200 = ListObservabilityDestinationsResponse +export type ListObservabilityDestinations400 = BadRequestResponse +export const ListObservabilityDestinations400 = BadRequestResponse +export type ListObservabilityDestinations401 = UnauthorizedResponse +export const ListObservabilityDestinations401 = UnauthorizedResponse +export type ListObservabilityDestinations500 = InternalServerResponse +export const ListObservabilityDestinations500 = InternalServerResponse +export type CreateObservabilityDestinationParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateObservabilityDestinationParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateObservabilityDestinationRequestJson = CreateObservabilityDestinationRequest +export const CreateObservabilityDestinationRequestJson = CreateObservabilityDestinationRequest +export type CreateObservabilityDestination201 = CreateObservabilityDestinationResponse +export const CreateObservabilityDestination201 = CreateObservabilityDestinationResponse +export type CreateObservabilityDestination400 = BadRequestResponse +export const CreateObservabilityDestination400 = BadRequestResponse +export type CreateObservabilityDestination401 = UnauthorizedResponse +export const CreateObservabilityDestination401 = UnauthorizedResponse +export type CreateObservabilityDestination403 = ForbiddenResponse +export const CreateObservabilityDestination403 = ForbiddenResponse +export type CreateObservabilityDestination409 = ConflictResponse +export const CreateObservabilityDestination409 = ConflictResponse +export type CreateObservabilityDestination500 = InternalServerResponse +export const CreateObservabilityDestination500 = InternalServerResponse +export type GetObservabilityDestinationParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetObservabilityDestinationParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetObservabilityDestination200 = GetObservabilityDestinationResponse +export const GetObservabilityDestination200 = GetObservabilityDestinationResponse +export type GetObservabilityDestination401 = UnauthorizedResponse +export const GetObservabilityDestination401 = UnauthorizedResponse +export type GetObservabilityDestination404 = NotFoundResponse +export const GetObservabilityDestination404 = NotFoundResponse +export type GetObservabilityDestination500 = InternalServerResponse +export const GetObservabilityDestination500 = InternalServerResponse +export type DeleteObservabilityDestinationParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteObservabilityDestinationParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteObservabilityDestination200 = DeleteObservabilityDestinationResponse +export const DeleteObservabilityDestination200 = DeleteObservabilityDestinationResponse +export type DeleteObservabilityDestination401 = UnauthorizedResponse +export const DeleteObservabilityDestination401 = UnauthorizedResponse +export type DeleteObservabilityDestination404 = NotFoundResponse +export const DeleteObservabilityDestination404 = NotFoundResponse +export type DeleteObservabilityDestination500 = InternalServerResponse +export const DeleteObservabilityDestination500 = InternalServerResponse +export type UpdateObservabilityDestinationParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpdateObservabilityDestinationParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpdateObservabilityDestinationRequestJson = UpdateObservabilityDestinationRequest +export const UpdateObservabilityDestinationRequestJson = UpdateObservabilityDestinationRequest +export type UpdateObservabilityDestination200 = UpdateObservabilityDestinationResponse +export const UpdateObservabilityDestination200 = UpdateObservabilityDestinationResponse +export type UpdateObservabilityDestination400 = BadRequestResponse +export const UpdateObservabilityDestination400 = BadRequestResponse +export type UpdateObservabilityDestination401 = UnauthorizedResponse +export const UpdateObservabilityDestination401 = UnauthorizedResponse +export type UpdateObservabilityDestination404 = NotFoundResponse +export const UpdateObservabilityDestination404 = NotFoundResponse +export type UpdateObservabilityDestination409 = ConflictResponse +export const UpdateObservabilityDestination409 = ConflictResponse +export type UpdateObservabilityDestination500 = InternalServerResponse +export const UpdateObservabilityDestination500 = InternalServerResponse +export type ListOrganizationMembersParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListOrganizationMembersParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListOrganizationMembers200 = { + readonly "data": ReadonlyArray< + { + readonly "email": string + readonly "first_name": string | null + readonly "id": string + readonly "last_name": string | null + readonly "role": "org:admin" | "org:member" + } + > + readonly "total_count": number +} +export const ListOrganizationMembers200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "email": Schema.String.annotate({ "description": "Email address of the member" }), + "first_name": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "First name of the member" }), + "id": Schema.String.annotate({ "description": "User ID of the organization member" }), + "last_name": Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Last name of the member" }), + "role": Schema.Literals(["org:admin", "org:member"]).annotate({ + "description": "Role of the member in the organization" + }) + })).annotate({ "description": "List of organization members" }), + "total_count": Schema.Number.annotate({ "description": "Total number of members in the organization" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) +}) +export type ListOrganizationMembers401 = UnauthorizedResponse +export const ListOrganizationMembers401 = UnauthorizedResponse +export type ListOrganizationMembers404 = NotFoundResponse +export const ListOrganizationMembers404 = NotFoundResponse +export type ListOrganizationMembers500 = InternalServerResponse +export const ListOrganizationMembers500 = InternalServerResponse +export type ListPresetsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListPresetsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListPresets200 = ListPresetsResponse +export const ListPresets200 = ListPresetsResponse +export type ListPresets400 = BadRequestResponse +export const ListPresets400 = BadRequestResponse +export type ListPresets401 = UnauthorizedResponse +export const ListPresets401 = UnauthorizedResponse +export type ListPresets500 = InternalServerResponse +export const ListPresets500 = InternalServerResponse +export type GetPresetParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetPresetParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetPreset200 = GetPresetResponse +export const GetPreset200 = GetPresetResponse +export type GetPreset400 = BadRequestResponse +export const GetPreset400 = BadRequestResponse +export type GetPreset401 = UnauthorizedResponse +export const GetPreset401 = UnauthorizedResponse +export type GetPreset404 = NotFoundResponse +export const GetPreset404 = NotFoundResponse +export type GetPreset500 = InternalServerResponse +export const GetPreset500 = InternalServerResponse +export type CreatePresetsChatCompletionsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreatePresetsChatCompletionsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreatePresetsChatCompletionsRequestJson = ChatRequest +export const CreatePresetsChatCompletionsRequestJson = ChatRequest +export type CreatePresetsChatCompletions200 = CreatePresetFromInferenceResponse +export const CreatePresetsChatCompletions200 = CreatePresetFromInferenceResponse +export type CreatePresetsChatCompletions400 = BadRequestResponse +export const CreatePresetsChatCompletions400 = BadRequestResponse +export type CreatePresetsChatCompletions401 = UnauthorizedResponse +export const CreatePresetsChatCompletions401 = UnauthorizedResponse +export type CreatePresetsChatCompletions403 = ForbiddenResponse +export const CreatePresetsChatCompletions403 = ForbiddenResponse +export type CreatePresetsChatCompletions404 = NotFoundResponse +export const CreatePresetsChatCompletions404 = NotFoundResponse +export type CreatePresetsChatCompletions409 = ConflictResponse +export const CreatePresetsChatCompletions409 = ConflictResponse +export type CreatePresetsChatCompletions500 = InternalServerResponse +export const CreatePresetsChatCompletions500 = InternalServerResponse +export type CreatePresetsMessagesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreatePresetsMessagesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreatePresetsMessagesRequestJson = MessagesRequest +export const CreatePresetsMessagesRequestJson = MessagesRequest +export type CreatePresetsMessages200 = CreatePresetFromInferenceResponse +export const CreatePresetsMessages200 = CreatePresetFromInferenceResponse +export type CreatePresetsMessages400 = BadRequestResponse +export const CreatePresetsMessages400 = BadRequestResponse +export type CreatePresetsMessages401 = UnauthorizedResponse +export const CreatePresetsMessages401 = UnauthorizedResponse +export type CreatePresetsMessages403 = ForbiddenResponse +export const CreatePresetsMessages403 = ForbiddenResponse +export type CreatePresetsMessages404 = NotFoundResponse +export const CreatePresetsMessages404 = NotFoundResponse +export type CreatePresetsMessages409 = ConflictResponse +export const CreatePresetsMessages409 = ConflictResponse +export type CreatePresetsMessages500 = InternalServerResponse +export const CreatePresetsMessages500 = InternalServerResponse +export type CreatePresetsResponsesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreatePresetsResponsesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreatePresetsResponsesRequestJson = ResponsesRequest +export const CreatePresetsResponsesRequestJson = ResponsesRequest +export type CreatePresetsResponses200 = CreatePresetFromInferenceResponse +export const CreatePresetsResponses200 = CreatePresetFromInferenceResponse +export type CreatePresetsResponses400 = BadRequestResponse +export const CreatePresetsResponses400 = BadRequestResponse +export type CreatePresetsResponses401 = UnauthorizedResponse +export const CreatePresetsResponses401 = UnauthorizedResponse +export type CreatePresetsResponses403 = ForbiddenResponse +export const CreatePresetsResponses403 = ForbiddenResponse +export type CreatePresetsResponses404 = NotFoundResponse +export const CreatePresetsResponses404 = NotFoundResponse +export type CreatePresetsResponses409 = ConflictResponse +export const CreatePresetsResponses409 = ConflictResponse +export type CreatePresetsResponses500 = InternalServerResponse +export const CreatePresetsResponses500 = InternalServerResponse +export type ListPresetVersionsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListPresetVersionsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListPresetVersions200 = ListPresetVersionsResponse +export const ListPresetVersions200 = ListPresetVersionsResponse +export type ListPresetVersions400 = BadRequestResponse +export const ListPresetVersions400 = BadRequestResponse +export type ListPresetVersions401 = UnauthorizedResponse +export const ListPresetVersions401 = UnauthorizedResponse +export type ListPresetVersions404 = NotFoundResponse +export const ListPresetVersions404 = NotFoundResponse +export type ListPresetVersions500 = InternalServerResponse +export const ListPresetVersions500 = InternalServerResponse +export type GetPresetVersionParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetPresetVersionParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetPresetVersion200 = GetPresetVersionResponse +export const GetPresetVersion200 = GetPresetVersionResponse +export type GetPresetVersion400 = BadRequestResponse +export const GetPresetVersion400 = BadRequestResponse +export type GetPresetVersion401 = UnauthorizedResponse +export const GetPresetVersion401 = UnauthorizedResponse +export type GetPresetVersion404 = NotFoundResponse +export const GetPresetVersion404 = NotFoundResponse +export type GetPresetVersion500 = InternalServerResponse +export const GetPresetVersion500 = InternalServerResponse +export type ListProvidersParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListProvidersParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListProviders200 = { + readonly "data": ReadonlyArray< + { + readonly "datacenters"?: + | ReadonlyArray< + | "AD" + | "AE" + | "AF" + | "AG" + | "AI" + | "AL" + | "AM" + | "AO" + | "AQ" + | "AR" + | "AS" + | "AT" + | "AU" + | "AW" + | "AX" + | "AZ" + | "BA" + | "BB" + | "BD" + | "BE" + | "BF" + | "BG" + | "BH" + | "BI" + | "BJ" + | "BL" + | "BM" + | "BN" + | "BO" + | "BQ" + | "BR" + | "BS" + | "BT" + | "BV" + | "BW" + | "BY" + | "BZ" + | "CA" + | "CC" + | "CD" + | "CF" + | "CG" + | "CH" + | "CI" + | "CK" + | "CL" + | "CM" + | "CN" + | "CO" + | "CR" + | "CU" + | "CV" + | "CW" + | "CX" + | "CY" + | "CZ" + | "DE" + | "DJ" + | "DK" + | "DM" + | "DO" + | "DZ" + | "EC" + | "EE" + | "EG" + | "EH" + | "ER" + | "ES" + | "ET" + | "FI" + | "FJ" + | "FK" + | "FM" + | "FO" + | "FR" + | "GA" + | "GB" + | "GD" + | "GE" + | "GF" + | "GG" + | "GH" + | "GI" + | "GL" + | "GM" + | "GN" + | "GP" + | "GQ" + | "GR" + | "GS" + | "GT" + | "GU" + | "GW" + | "GY" + | "HK" + | "HM" + | "HN" + | "HR" + | "HT" + | "HU" + | "ID" + | "IE" + | "IL" + | "IM" + | "IN" + | "IO" + | "IQ" + | "IR" + | "IS" + | "IT" + | "JE" + | "JM" + | "JO" + | "JP" + | "KE" + | "KG" + | "KH" + | "KI" + | "KM" + | "KN" + | "KP" + | "KR" + | "KW" + | "KY" + | "KZ" + | "LA" + | "LB" + | "LC" + | "LI" + | "LK" + | "LR" + | "LS" + | "LT" + | "LU" + | "LV" + | "LY" + | "MA" + | "MC" + | "MD" + | "ME" + | "MF" + | "MG" + | "MH" + | "MK" + | "ML" + | "MM" + | "MN" + | "MO" + | "MP" + | "MQ" + | "MR" + | "MS" + | "MT" + | "MU" + | "MV" + | "MW" + | "MX" + | "MY" + | "MZ" + | "NA" + | "NC" + | "NE" + | "NF" + | "NG" + | "NI" + | "NL" + | "NO" + | "NP" + | "NR" + | "NU" + | "NZ" + | "OM" + | "PA" + | "PE" + | "PF" + | "PG" + | "PH" + | "PK" + | "PL" + | "PM" + | "PN" + | "PR" + | "PS" + | "PT" + | "PW" + | "PY" + | "QA" + | "RE" + | "RO" + | "RS" + | "RU" + | "RW" + | "SA" + | "SB" + | "SC" + | "SD" + | "SE" + | "SG" + | "SH" + | "SI" + | "SJ" + | "SK" + | "SL" + | "SM" + | "SN" + | "SO" + | "SR" + | "SS" + | "ST" + | "SV" + | "SX" + | "SY" + | "SZ" + | "TC" + | "TD" + | "TF" + | "TG" + | "TH" + | "TJ" + | "TK" + | "TL" + | "TM" + | "TN" + | "TO" + | "TR" + | "TT" + | "TV" + | "TW" + | "TZ" + | "UA" + | "UG" + | "UM" + | "US" + | "UY" + | "UZ" + | "VA" + | "VC" + | "VE" + | "VG" + | "VI" + | "VN" + | "VU" + | "WF" + | "WS" + | "YE" + | "YT" + | "ZA" + | "ZM" + | "ZW" + > + | null + readonly "headquarters"?: + | "AD" + | "AE" + | "AF" + | "AG" + | "AI" + | "AL" + | "AM" + | "AO" + | "AQ" + | "AR" + | "AS" + | "AT" + | "AU" + | "AW" + | "AX" + | "AZ" + | "BA" + | "BB" + | "BD" + | "BE" + | "BF" + | "BG" + | "BH" + | "BI" + | "BJ" + | "BL" + | "BM" + | "BN" + | "BO" + | "BQ" + | "BR" + | "BS" + | "BT" + | "BV" + | "BW" + | "BY" + | "BZ" + | "CA" + | "CC" + | "CD" + | "CF" + | "CG" + | "CH" + | "CI" + | "CK" + | "CL" + | "CM" + | "CN" + | "CO" + | "CR" + | "CU" + | "CV" + | "CW" + | "CX" + | "CY" + | "CZ" + | "DE" + | "DJ" + | "DK" + | "DM" + | "DO" + | "DZ" + | "EC" + | "EE" + | "EG" + | "EH" + | "ER" + | "ES" + | "ET" + | "FI" + | "FJ" + | "FK" + | "FM" + | "FO" + | "FR" + | "GA" + | "GB" + | "GD" + | "GE" + | "GF" + | "GG" + | "GH" + | "GI" + | "GL" + | "GM" + | "GN" + | "GP" + | "GQ" + | "GR" + | "GS" + | "GT" + | "GU" + | "GW" + | "GY" + | "HK" + | "HM" + | "HN" + | "HR" + | "HT" + | "HU" + | "ID" + | "IE" + | "IL" + | "IM" + | "IN" + | "IO" + | "IQ" + | "IR" + | "IS" + | "IT" + | "JE" + | "JM" + | "JO" + | "JP" + | "KE" + | "KG" + | "KH" + | "KI" + | "KM" + | "KN" + | "KP" + | "KR" + | "KW" + | "KY" + | "KZ" + | "LA" + | "LB" + | "LC" + | "LI" + | "LK" + | "LR" + | "LS" + | "LT" + | "LU" + | "LV" + | "LY" + | "MA" + | "MC" + | "MD" + | "ME" + | "MF" + | "MG" + | "MH" + | "MK" + | "ML" + | "MM" + | "MN" + | "MO" + | "MP" + | "MQ" + | "MR" + | "MS" + | "MT" + | "MU" + | "MV" + | "MW" + | "MX" + | "MY" + | "MZ" + | "NA" + | "NC" + | "NE" + | "NF" + | "NG" + | "NI" + | "NL" + | "NO" + | "NP" + | "NR" + | "NU" + | "NZ" + | "OM" + | "PA" + | "PE" + | "PF" + | "PG" + | "PH" + | "PK" + | "PL" + | "PM" + | "PN" + | "PR" + | "PS" + | "PT" + | "PW" + | "PY" + | "QA" + | "RE" + | "RO" + | "RS" + | "RU" + | "RW" + | "SA" + | "SB" + | "SC" + | "SD" + | "SE" + | "SG" + | "SH" + | "SI" + | "SJ" + | "SK" + | "SL" + | "SM" + | "SN" + | "SO" + | "SR" + | "SS" + | "ST" + | "SV" + | "SX" + | "SY" + | "SZ" + | "TC" + | "TD" + | "TF" + | "TG" + | "TH" + | "TJ" + | "TK" + | "TL" + | "TM" + | "TN" + | "TO" + | "TR" + | "TT" + | "TV" + | "TW" + | "TZ" + | "UA" + | "UG" + | "UM" + | "US" + | "UY" + | "UZ" + | "VA" + | "VC" + | "VE" + | "VG" + | "VI" + | "VN" + | "VU" + | "WF" + | "WS" + | "YE" + | "YT" + | "ZA" + | "ZM" + | "ZW" + | null + readonly "name": string + readonly "privacy_policy_url": string | null + readonly "slug": string + readonly "status_page_url"?: string | null + readonly "terms_of_service_url"?: string | null + } + > +} +export const ListProviders200 = Schema.Struct({ + "data": Schema.Array(Schema.Struct({ + "datacenters": Schema.optionalKey( + Schema.Union([ + Schema.Array( + Schema.Literals([ + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BL", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MF", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW" + ]) + ), + Schema.Null + ]).annotate({ "description": "ISO 3166-1 Alpha-2 country codes of the provider datacenter locations" }) + ), + "headquarters": Schema.optionalKey( + Schema.Union([ + Schema.Literal("AD"), + Schema.Literal("AE"), + Schema.Literal("AF"), + Schema.Literal("AG"), + Schema.Literal("AI"), + Schema.Literal("AL"), + Schema.Literal("AM"), + Schema.Literal("AO"), + Schema.Literal("AQ"), + Schema.Literal("AR"), + Schema.Literal("AS"), + Schema.Literal("AT"), + Schema.Literal("AU"), + Schema.Literal("AW"), + Schema.Literal("AX"), + Schema.Literal("AZ"), + Schema.Literal("BA"), + Schema.Literal("BB"), + Schema.Literal("BD"), + Schema.Literal("BE"), + Schema.Literal("BF"), + Schema.Literal("BG"), + Schema.Literal("BH"), + Schema.Literal("BI"), + Schema.Literal("BJ"), + Schema.Literal("BL"), + Schema.Literal("BM"), + Schema.Literal("BN"), + Schema.Literal("BO"), + Schema.Literal("BQ"), + Schema.Literal("BR"), + Schema.Literal("BS"), + Schema.Literal("BT"), + Schema.Literal("BV"), + Schema.Literal("BW"), + Schema.Literal("BY"), + Schema.Literal("BZ"), + Schema.Literal("CA"), + Schema.Literal("CC"), + Schema.Literal("CD"), + Schema.Literal("CF"), + Schema.Literal("CG"), + Schema.Literal("CH"), + Schema.Literal("CI"), + Schema.Literal("CK"), + Schema.Literal("CL"), + Schema.Literal("CM"), + Schema.Literal("CN"), + Schema.Literal("CO"), + Schema.Literal("CR"), + Schema.Literal("CU"), + Schema.Literal("CV"), + Schema.Literal("CW"), + Schema.Literal("CX"), + Schema.Literal("CY"), + Schema.Literal("CZ"), + Schema.Literal("DE"), + Schema.Literal("DJ"), + Schema.Literal("DK"), + Schema.Literal("DM"), + Schema.Literal("DO"), + Schema.Literal("DZ"), + Schema.Literal("EC"), + Schema.Literal("EE"), + Schema.Literal("EG"), + Schema.Literal("EH"), + Schema.Literal("ER"), + Schema.Literal("ES"), + Schema.Literal("ET"), + Schema.Literal("FI"), + Schema.Literal("FJ"), + Schema.Literal("FK"), + Schema.Literal("FM"), + Schema.Literal("FO"), + Schema.Literal("FR"), + Schema.Literal("GA"), + Schema.Literal("GB"), + Schema.Literal("GD"), + Schema.Literal("GE"), + Schema.Literal("GF"), + Schema.Literal("GG"), + Schema.Literal("GH"), + Schema.Literal("GI"), + Schema.Literal("GL"), + Schema.Literal("GM"), + Schema.Literal("GN"), + Schema.Literal("GP"), + Schema.Literal("GQ"), + Schema.Literal("GR"), + Schema.Literal("GS"), + Schema.Literal("GT"), + Schema.Literal("GU"), + Schema.Literal("GW"), + Schema.Literal("GY"), + Schema.Literal("HK"), + Schema.Literal("HM"), + Schema.Literal("HN"), + Schema.Literal("HR"), + Schema.Literal("HT"), + Schema.Literal("HU"), + Schema.Literal("ID"), + Schema.Literal("IE"), + Schema.Literal("IL"), + Schema.Literal("IM"), + Schema.Literal("IN"), + Schema.Literal("IO"), + Schema.Literal("IQ"), + Schema.Literal("IR"), + Schema.Literal("IS"), + Schema.Literal("IT"), + Schema.Literal("JE"), + Schema.Literal("JM"), + Schema.Literal("JO"), + Schema.Literal("JP"), + Schema.Literal("KE"), + Schema.Literal("KG"), + Schema.Literal("KH"), + Schema.Literal("KI"), + Schema.Literal("KM"), + Schema.Literal("KN"), + Schema.Literal("KP"), + Schema.Literal("KR"), + Schema.Literal("KW"), + Schema.Literal("KY"), + Schema.Literal("KZ"), + Schema.Literal("LA"), + Schema.Literal("LB"), + Schema.Literal("LC"), + Schema.Literal("LI"), + Schema.Literal("LK"), + Schema.Literal("LR"), + Schema.Literal("LS"), + Schema.Literal("LT"), + Schema.Literal("LU"), + Schema.Literal("LV"), + Schema.Literal("LY"), + Schema.Literal("MA"), + Schema.Literal("MC"), + Schema.Literal("MD"), + Schema.Literal("ME"), + Schema.Literal("MF"), + Schema.Literal("MG"), + Schema.Literal("MH"), + Schema.Literal("MK"), + Schema.Literal("ML"), + Schema.Literal("MM"), + Schema.Literal("MN"), + Schema.Literal("MO"), + Schema.Literal("MP"), + Schema.Literal("MQ"), + Schema.Literal("MR"), + Schema.Literal("MS"), + Schema.Literal("MT"), + Schema.Literal("MU"), + Schema.Literal("MV"), + Schema.Literal("MW"), + Schema.Literal("MX"), + Schema.Literal("MY"), + Schema.Literal("MZ"), + Schema.Literal("NA"), + Schema.Literal("NC"), + Schema.Literal("NE"), + Schema.Literal("NF"), + Schema.Literal("NG"), + Schema.Literal("NI"), + Schema.Literal("NL"), + Schema.Literal("NO"), + Schema.Literal("NP"), + Schema.Literal("NR"), + Schema.Literal("NU"), + Schema.Literal("NZ"), + Schema.Literal("OM"), + Schema.Literal("PA"), + Schema.Literal("PE"), + Schema.Literal("PF"), + Schema.Literal("PG"), + Schema.Literal("PH"), + Schema.Literal("PK"), + Schema.Literal("PL"), + Schema.Literal("PM"), + Schema.Literal("PN"), + Schema.Literal("PR"), + Schema.Literal("PS"), + Schema.Literal("PT"), + Schema.Literal("PW"), + Schema.Literal("PY"), + Schema.Literal("QA"), + Schema.Literal("RE"), + Schema.Literal("RO"), + Schema.Literal("RS"), + Schema.Literal("RU"), + Schema.Literal("RW"), + Schema.Literal("SA"), + Schema.Literal("SB"), + Schema.Literal("SC"), + Schema.Literal("SD"), + Schema.Literal("SE"), + Schema.Literal("SG"), + Schema.Literal("SH"), + Schema.Literal("SI"), + Schema.Literal("SJ"), + Schema.Literal("SK"), + Schema.Literal("SL"), + Schema.Literal("SM"), + Schema.Literal("SN"), + Schema.Literal("SO"), + Schema.Literal("SR"), + Schema.Literal("SS"), + Schema.Literal("ST"), + Schema.Literal("SV"), + Schema.Literal("SX"), + Schema.Literal("SY"), + Schema.Literal("SZ"), + Schema.Literal("TC"), + Schema.Literal("TD"), + Schema.Literal("TF"), + Schema.Literal("TG"), + Schema.Literal("TH"), + Schema.Literal("TJ"), + Schema.Literal("TK"), + Schema.Literal("TL"), + Schema.Literal("TM"), + Schema.Literal("TN"), + Schema.Literal("TO"), + Schema.Literal("TR"), + Schema.Literal("TT"), + Schema.Literal("TV"), + Schema.Literal("TW"), + Schema.Literal("TZ"), + Schema.Literal("UA"), + Schema.Literal("UG"), + Schema.Literal("UM"), + Schema.Literal("US"), + Schema.Literal("UY"), + Schema.Literal("UZ"), + Schema.Literal("VA"), + Schema.Literal("VC"), + Schema.Literal("VE"), + Schema.Literal("VG"), + Schema.Literal("VI"), + Schema.Literal("VN"), + Schema.Literal("VU"), + Schema.Literal("WF"), + Schema.Literal("WS"), + Schema.Literal("YE"), + Schema.Literal("YT"), + Schema.Literal("ZA"), + Schema.Literal("ZM"), + Schema.Literal("ZW"), + Schema.Null + ]).annotate({ "description": "ISO 3166-1 Alpha-2 country code of the provider headquarters" }) + ), + "name": Schema.String.annotate({ "description": "Display name of the provider" }), + "privacy_policy_url": Schema.Union([Schema.String, Schema.Null]).annotate({ + "description": "URL to the provider's privacy policy" + }), + "slug": Schema.String.annotate({ "description": "URL-friendly identifier for the provider" }), + "status_page_url": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "URL to the provider's status page" }) + ), + "terms_of_service_url": Schema.optionalKey( + Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "URL to the provider's terms of service" }) + ) + })) +}) +export type ListProviders500 = InternalServerResponse +export const ListProviders500 = InternalServerResponse +export type CreateRerankParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateRerankParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateRerankRequestJson = { + readonly "documents": ReadonlyArray + readonly "model": string + readonly "provider"?: ProviderPreferences + readonly "query": string + readonly "top_n"?: number +} +export const CreateRerankRequestJson = Schema.Struct({ + "documents": Schema.Array( + Schema.Union([ + Schema.String, + Schema.Struct({ + "image": Schema.optionalKey( + Schema.String.annotate({ + "description": + "An image associated with the document, as a remote URL (http/https) or a base64-encoded data URI (data:image/...)." + }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The document text" })) + }).annotate({ + "description": + "A structured document with optional text and/or image content. At least one of `text` or `image` must be provided." + }) + ]).annotate({ + "description": + "A document to rerank. Either a plain string, or a structured object with optional `text` and/or `image`." + }) + ).annotate({ + "description": + "The list of documents to rerank. Documents may be plain strings, or structured objects with `text` and/or `image` for multimodal models." + }).check(Schema.isMinLength(1).annotate({ "expected": "a value with a length of at least 1" })), + "model": Schema.String.annotate({ "description": "The rerank model to use" }), + "provider": Schema.optionalKey( + Schema.suspend((): Schema.Codec => ProviderPreferences).annotate({ + "description": "Provider routing preferences for the request." + }) + ), + "query": Schema.String.annotate({ "description": "The search query to rerank documents against" }), + "top_n": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of most relevant documents to return" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })) + ) +}).annotate({ "description": "Rerank request input" }) +export type CreateRerank200 = { + readonly "id"?: string + readonly "model": string + readonly "provider"?: string + readonly "results": ReadonlyArray< + { + readonly "document": { readonly "image"?: string; readonly "text"?: string } + readonly "index": number + readonly "relevance_score": number + } + > + readonly "usage"?: { readonly "cost"?: number; readonly "search_units"?: number; readonly "total_tokens"?: number } +} +export const CreateRerank200 = Schema.Struct({ + "id": Schema.optionalKey( + Schema.String.annotate({ "description": "Unique identifier for the rerank response (ORID format)" }) + ), + "model": Schema.String.annotate({ "description": "The model used for reranking" }), + "provider": Schema.optionalKey( + Schema.String.annotate({ "description": "The provider that served the rerank request" }) + ), + "results": Schema.Array( + Schema.Struct({ + "document": Schema.Struct({ + "image": Schema.optionalKey( + Schema.String.annotate({ "description": "The image (URL or data URI) from the original document" }) + ), + "text": Schema.optionalKey(Schema.String.annotate({ "description": "The document text" })) + }).annotate({ "description": "The document object echoing the original input (text and/or image)" }), + "index": Schema.Number.annotate({ "description": "Index of the document in the original input list" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ), + "relevance_score": Schema.Number.annotate({ + "description": "Relevance score of the document to the query", + "format": "double" + }).check(Schema.isFinite().annotate({ "expected": "a finite number" })) + }).annotate({ "description": "A single rerank result" }) + ).annotate({ "description": "List of rerank results sorted by relevance" }), + "usage": Schema.optionalKey( + Schema.Struct({ + "cost": Schema.optionalKey( + Schema.Number.annotate({ "description": "Cost of the request in credits", "format": "double" }).check( + Schema.isFinite().annotate({ "expected": "a finite number" }) + ) + ), + "search_units": Schema.optionalKey( + Schema.Number.annotate({ "description": "Number of search units consumed (Cohere billing)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ), + "total_tokens": Schema.optionalKey( + Schema.Number.annotate({ "description": "Total number of tokens used" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ) + ) + }).annotate({ "description": "Usage statistics" }) + ) +}).annotate({ "description": "Rerank response containing ranked results" }) +export type CreateRerank200Sse = string +export const CreateRerank200Sse = Schema.String.annotate({ + "description": "Not used for rerank - rerank does not support streaming" +}) +export type CreateRerank400 = BadRequestResponse +export const CreateRerank400 = BadRequestResponse +export type CreateRerank401 = UnauthorizedResponse +export const CreateRerank401 = UnauthorizedResponse +export type CreateRerank402 = PaymentRequiredResponse +export const CreateRerank402 = PaymentRequiredResponse +export type CreateRerank404 = NotFoundResponse +export const CreateRerank404 = NotFoundResponse +export type CreateRerank429 = TooManyRequestsResponse +export const CreateRerank429 = TooManyRequestsResponse +export type CreateRerank500 = InternalServerResponse +export const CreateRerank500 = InternalServerResponse +export type CreateRerank502 = BadGatewayResponse +export const CreateRerank502 = BadGatewayResponse +export type CreateRerank503 = ServiceUnavailableResponse +export const CreateRerank503 = ServiceUnavailableResponse +export type CreateRerank524 = EdgeNetworkTimeoutResponse +export const CreateRerank524 = EdgeNetworkTimeoutResponse +export type CreateRerank529 = ProviderOverloadedResponse +export const CreateRerank529 = ProviderOverloadedResponse +export type CreateResponsesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "X-OpenRouter-Metadata"?: MetadataLevel +} +export const CreateResponsesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "X-OpenRouter-Metadata": Schema.optionalKey(MetadataLevel) +}) +export type CreateResponsesRequestJson = ResponsesRequest +export const CreateResponsesRequestJson = ResponsesRequest +export type CreateResponses200 = OpenResponsesResult +export const CreateResponses200 = OpenResponsesResult +export type CreateResponses200Sse = ResponsesStreamingResponse +export const CreateResponses200Sse: Schema.Schema = ResponsesStreamingResponse +export type CreateResponses400 = BadRequestResponse +export const CreateResponses400 = BadRequestResponse +export type CreateResponses401 = UnauthorizedResponse +export const CreateResponses401 = UnauthorizedResponse +export type CreateResponses402 = PaymentRequiredResponse +export const CreateResponses402 = PaymentRequiredResponse +export type CreateResponses403 = ForbiddenResponse +export const CreateResponses403 = ForbiddenResponse +export type CreateResponses404 = NotFoundResponse +export const CreateResponses404 = NotFoundResponse +export type CreateResponses408 = RequestTimeoutResponse +export const CreateResponses408 = RequestTimeoutResponse +export type CreateResponses413 = PayloadTooLargeResponse +export const CreateResponses413 = PayloadTooLargeResponse +export type CreateResponses422 = UnprocessableEntityResponse +export const CreateResponses422 = UnprocessableEntityResponse +export type CreateResponses429 = TooManyRequestsResponse +export const CreateResponses429 = TooManyRequestsResponse +export type CreateResponses500 = InternalServerResponse +export const CreateResponses500 = InternalServerResponse +export type CreateResponses502 = BadGatewayResponse +export const CreateResponses502 = BadGatewayResponse +export type CreateResponses503 = ServiceUnavailableResponse +export const CreateResponses503 = ServiceUnavailableResponse +export type CreateResponses524 = EdgeNetworkTimeoutResponse +export const CreateResponses524 = EdgeNetworkTimeoutResponse +export type CreateResponses529 = ProviderOverloadedResponse +export const CreateResponses529 = ProviderOverloadedResponse +export type CreateVideosParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateVideosParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateVideosRequestJson = VideoGenerationRequest +export const CreateVideosRequestJson = VideoGenerationRequest +export type CreateVideos202 = VideoGenerationResponse +export const CreateVideos202 = VideoGenerationResponse +export type CreateVideos400 = BadRequestResponse +export const CreateVideos400 = BadRequestResponse +export type CreateVideos401 = UnauthorizedResponse +export const CreateVideos401 = UnauthorizedResponse +export type CreateVideos402 = PaymentRequiredResponse +export const CreateVideos402 = PaymentRequiredResponse +export type CreateVideos404 = NotFoundResponse +export const CreateVideos404 = NotFoundResponse +export type CreateVideos429 = TooManyRequestsResponse +export const CreateVideos429 = TooManyRequestsResponse +export type CreateVideos500 = InternalServerResponse +export const CreateVideos500 = InternalServerResponse +export type GetVideosParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetVideosParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetVideos200 = VideoGenerationResponse +export const GetVideos200 = VideoGenerationResponse +export type GetVideos401 = UnauthorizedResponse +export const GetVideos401 = UnauthorizedResponse +export type GetVideos404 = NotFoundResponse +export const GetVideos404 = NotFoundResponse +export type GetVideos500 = InternalServerResponse +export const GetVideos500 = InternalServerResponse +export type ListVideosContentParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "index"?: number | null +} +export const ListVideosContentParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "index": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]) + ) +}) +export type ListVideosContent400 = BadRequestResponse +export const ListVideosContent400 = BadRequestResponse +export type ListVideosContent401 = UnauthorizedResponse +export const ListVideosContent401 = UnauthorizedResponse +export type ListVideosContent404 = NotFoundResponse +export const ListVideosContent404 = NotFoundResponse +export type ListVideosContent500 = InternalServerResponse +export const ListVideosContent500 = InternalServerResponse +export type ListVideosContent502 = BadGatewayResponse +export const ListVideosContent502 = BadGatewayResponse +export type ListVideosModelsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListVideosModelsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListVideosModels200 = VideoModelsListResponse +export const ListVideosModels200 = VideoModelsListResponse +export type ListVideosModels400 = BadRequestResponse +export const ListVideosModels400 = BadRequestResponse +export type ListVideosModels500 = InternalServerResponse +export const ListVideosModels500 = InternalServerResponse +export type ListWorkspacesParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListWorkspacesParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListWorkspaces200 = ListWorkspacesResponse +export const ListWorkspaces200 = ListWorkspacesResponse +export type ListWorkspaces401 = UnauthorizedResponse +export const ListWorkspaces401 = UnauthorizedResponse +export type ListWorkspaces500 = InternalServerResponse +export const ListWorkspaces500 = InternalServerResponse +export type CreateWorkspaceParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const CreateWorkspaceParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type CreateWorkspaceRequestJson = CreateWorkspaceRequest +export const CreateWorkspaceRequestJson = CreateWorkspaceRequest +export type CreateWorkspace201 = CreateWorkspaceResponse +export const CreateWorkspace201 = CreateWorkspaceResponse +export type CreateWorkspace400 = BadRequestResponse +export const CreateWorkspace400 = BadRequestResponse +export type CreateWorkspace401 = UnauthorizedResponse +export const CreateWorkspace401 = UnauthorizedResponse +export type CreateWorkspace403 = ForbiddenResponse +export const CreateWorkspace403 = ForbiddenResponse +export type CreateWorkspace500 = InternalServerResponse +export const CreateWorkspace500 = InternalServerResponse +export type GetWorkspaceParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const GetWorkspaceParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type GetWorkspace200 = GetWorkspaceResponse +export const GetWorkspace200 = GetWorkspaceResponse +export type GetWorkspace401 = UnauthorizedResponse +export const GetWorkspace401 = UnauthorizedResponse +export type GetWorkspace404 = NotFoundResponse +export const GetWorkspace404 = NotFoundResponse +export type GetWorkspace500 = InternalServerResponse +export const GetWorkspace500 = InternalServerResponse +export type DeleteWorkspaceParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteWorkspaceParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteWorkspace200 = DeleteWorkspaceResponse +export const DeleteWorkspace200 = DeleteWorkspaceResponse +export type DeleteWorkspace400 = BadRequestResponse +export const DeleteWorkspace400 = BadRequestResponse +export type DeleteWorkspace401 = UnauthorizedResponse +export const DeleteWorkspace401 = UnauthorizedResponse +export type DeleteWorkspace403 = ForbiddenResponse +export const DeleteWorkspace403 = ForbiddenResponse +export type DeleteWorkspace404 = NotFoundResponse +export const DeleteWorkspace404 = NotFoundResponse +export type DeleteWorkspace500 = InternalServerResponse +export const DeleteWorkspace500 = InternalServerResponse +export type UpdateWorkspaceParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpdateWorkspaceParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpdateWorkspaceRequestJson = UpdateWorkspaceRequest +export const UpdateWorkspaceRequestJson = UpdateWorkspaceRequest +export type UpdateWorkspace200 = UpdateWorkspaceResponse +export const UpdateWorkspace200 = UpdateWorkspaceResponse +export type UpdateWorkspace400 = BadRequestResponse +export const UpdateWorkspace400 = BadRequestResponse +export type UpdateWorkspace401 = UnauthorizedResponse +export const UpdateWorkspace401 = UnauthorizedResponse +export type UpdateWorkspace403 = ForbiddenResponse +export const UpdateWorkspace403 = ForbiddenResponse +export type UpdateWorkspace404 = NotFoundResponse +export const UpdateWorkspace404 = NotFoundResponse +export type UpdateWorkspace500 = InternalServerResponse +export const UpdateWorkspace500 = InternalServerResponse +export type ListWorkspaceBudgetsParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const ListWorkspaceBudgetsParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type ListWorkspaceBudgets200 = ListWorkspaceBudgetsResponse +export const ListWorkspaceBudgets200 = ListWorkspaceBudgetsResponse +export type ListWorkspaceBudgets401 = UnauthorizedResponse +export const ListWorkspaceBudgets401 = UnauthorizedResponse +export type ListWorkspaceBudgets404 = NotFoundResponse +export const ListWorkspaceBudgets404 = NotFoundResponse +export type ListWorkspaceBudgets500 = InternalServerResponse +export const ListWorkspaceBudgets500 = InternalServerResponse +export type UpsertWorkspaceBudgetParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const UpsertWorkspaceBudgetParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type UpsertWorkspaceBudgetRequestJson = UpsertWorkspaceBudgetRequest +export const UpsertWorkspaceBudgetRequestJson = UpsertWorkspaceBudgetRequest +export type UpsertWorkspaceBudget200 = UpsertWorkspaceBudgetResponse +export const UpsertWorkspaceBudget200 = UpsertWorkspaceBudgetResponse +export type UpsertWorkspaceBudget400 = BadRequestResponse +export const UpsertWorkspaceBudget400 = BadRequestResponse +export type UpsertWorkspaceBudget401 = UnauthorizedResponse +export const UpsertWorkspaceBudget401 = UnauthorizedResponse +export type UpsertWorkspaceBudget404 = NotFoundResponse +export const UpsertWorkspaceBudget404 = NotFoundResponse +export type UpsertWorkspaceBudget500 = InternalServerResponse +export const UpsertWorkspaceBudget500 = InternalServerResponse +export type DeleteWorkspaceBudgetParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const DeleteWorkspaceBudgetParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type DeleteWorkspaceBudget200 = DeleteWorkspaceBudgetResponse +export const DeleteWorkspaceBudget200 = DeleteWorkspaceBudgetResponse +export type DeleteWorkspaceBudget401 = UnauthorizedResponse +export const DeleteWorkspaceBudget401 = UnauthorizedResponse +export type DeleteWorkspaceBudget404 = NotFoundResponse +export const DeleteWorkspaceBudget404 = NotFoundResponse +export type DeleteWorkspaceBudget500 = InternalServerResponse +export const DeleteWorkspaceBudget500 = InternalServerResponse +export type ListWorkspaceMembersParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string + readonly "offset"?: number | null + readonly "limit"?: number +} +export const ListWorkspaceMembersParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String), + "offset": Schema.optionalKey( + Schema.Union([ + Schema.Number.check(Schema.isInt().annotate({ "expected": "an integer" })).check( + Schema.isGreaterThanOrEqualTo(0).annotate({ "expected": "a value greater than or equal to 0" }) + ), + Schema.Null + ]).annotate({ "description": "Number of records to skip for pagination" }) + ), + "limit": Schema.optionalKey( + Schema.Number.annotate({ "description": "Maximum number of records to return (max 100)" }).check( + Schema.isInt().annotate({ "expected": "an integer" }) + ).check(Schema.isGreaterThanOrEqualTo(1).annotate({ "expected": "a value greater than or equal to 1" })).check( + Schema.isLessThanOrEqualTo(100).annotate({ "expected": "a value less than or equal to 100" }) + ) + ) +}) +export type ListWorkspaceMembers200 = ListWorkspaceMembersResponse +export const ListWorkspaceMembers200 = ListWorkspaceMembersResponse +export type ListWorkspaceMembers401 = UnauthorizedResponse +export const ListWorkspaceMembers401 = UnauthorizedResponse +export type ListWorkspaceMembers403 = ForbiddenResponse +export const ListWorkspaceMembers403 = ForbiddenResponse +export type ListWorkspaceMembers404 = NotFoundResponse +export const ListWorkspaceMembers404 = NotFoundResponse +export type ListWorkspaceMembers500 = InternalServerResponse +export const ListWorkspaceMembers500 = InternalServerResponse +export type BulkAddWorkspaceMembersParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkAddWorkspaceMembersParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkAddWorkspaceMembersRequestJson = BulkAddWorkspaceMembersRequest +export const BulkAddWorkspaceMembersRequestJson = BulkAddWorkspaceMembersRequest +export type BulkAddWorkspaceMembers200 = BulkAddWorkspaceMembersResponse +export const BulkAddWorkspaceMembers200 = BulkAddWorkspaceMembersResponse +export type BulkAddWorkspaceMembers400 = BadRequestResponse +export const BulkAddWorkspaceMembers400 = BadRequestResponse +export type BulkAddWorkspaceMembers401 = UnauthorizedResponse +export const BulkAddWorkspaceMembers401 = UnauthorizedResponse +export type BulkAddWorkspaceMembers403 = ForbiddenResponse +export const BulkAddWorkspaceMembers403 = ForbiddenResponse +export type BulkAddWorkspaceMembers404 = NotFoundResponse +export const BulkAddWorkspaceMembers404 = NotFoundResponse +export type BulkAddWorkspaceMembers500 = InternalServerResponse +export const BulkAddWorkspaceMembers500 = InternalServerResponse +export type BulkRemoveWorkspaceMembersParams = { + readonly "HTTP-Referer"?: string + readonly "X-OpenRouter-Title"?: string + readonly "X-OpenRouter-Categories"?: string +} +export const BulkRemoveWorkspaceMembersParams = Schema.Struct({ + "HTTP-Referer": Schema.optionalKey(Schema.String), + "X-OpenRouter-Title": Schema.optionalKey(Schema.String), + "X-OpenRouter-Categories": Schema.optionalKey(Schema.String) +}) +export type BulkRemoveWorkspaceMembersRequestJson = BulkRemoveWorkspaceMembersRequest +export const BulkRemoveWorkspaceMembersRequestJson = BulkRemoveWorkspaceMembersRequest +export type BulkRemoveWorkspaceMembers200 = BulkRemoveWorkspaceMembersResponse +export const BulkRemoveWorkspaceMembers200 = BulkRemoveWorkspaceMembersResponse +export type BulkRemoveWorkspaceMembers400 = BadRequestResponse +export const BulkRemoveWorkspaceMembers400 = BadRequestResponse +export type BulkRemoveWorkspaceMembers401 = UnauthorizedResponse +export const BulkRemoveWorkspaceMembers401 = UnauthorizedResponse +export type BulkRemoveWorkspaceMembers403 = ForbiddenResponse +export const BulkRemoveWorkspaceMembers403 = ForbiddenResponse +export type BulkRemoveWorkspaceMembers404 = NotFoundResponse +export const BulkRemoveWorkspaceMembers404 = NotFoundResponse +export type BulkRemoveWorkspaceMembers500 = InternalServerResponse +export const BulkRemoveWorkspaceMembers500 = InternalServerResponse + +export interface OperationConfig { + /** + * Whether or not the response should be included in the value returned from + * an operation. + * + * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, + * where `A` is the success type of the operation. + * + * If set to `false`, only the success type of the operation will be returned. + */ + readonly includeResponse?: boolean | undefined +} + +/** + * A utility type which optionally includes the response in the return result + * of an operation based upon the value of the `includeResponse` configuration + * option. + */ +export type WithOptionalResponse = Config extends { + readonly includeResponse: true +} ? [A, HttpClientResponse.HttpClientResponse] : + A + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): OpenRouterClient => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description) + }) + }) + ) + ) + const withResponse = (config: Config | undefined) => + ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect + ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { + const withOptionalResponse = ( + config?.includeResponse + ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) + : (response: HttpClientResponse.HttpClientResponse) => f(response) + ) as any + return options?.transformClient + ? (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + withOptionalResponse + ) + : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + } + const sseRequest = < + Type, + DecodingServices + >( + schema: Schema.ConstraintDecoder + ) => + ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + DecodingServices + > => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) + ) + const binaryRequest = ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap + ) + const decodeSuccess = + (schema: Schema) => (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(OpenRouterClientError(tag, cause, response)) + ) + return { + httpClient, + "getUserActivity": (options) => + HttpClientRequest.get(`/activity`).pipe( + HttpClientRequest.setUrlParams({ + "date": options?.params?.["date"] as any, + "api_key_hash": options?.params?.["api_key_hash"] as any, + "user_id": options?.params?.["user_id"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetUserActivity200), + "400": decodeError("GetUserActivity400", GetUserActivity400), + "401": decodeError("GetUserActivity401", GetUserActivity401), + "403": decodeError("GetUserActivity403", GetUserActivity403), + "404": decodeError("GetUserActivity404", GetUserActivity404), + "500": decodeError("GetUserActivity500", GetUserActivity500), + orElse: unexpectedStatus + })) + ), + "getAnalyticsMeta": (options) => + HttpClientRequest.get(`/analytics/meta`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetAnalyticsMeta200), + "401": decodeError("GetAnalyticsMeta401", GetAnalyticsMeta401), + "403": decodeError("GetAnalyticsMeta403", GetAnalyticsMeta403), + "500": decodeError("GetAnalyticsMeta500", GetAnalyticsMeta500), + orElse: unexpectedStatus + })) + ), + "queryAnalytics": (options) => + HttpClientRequest.post(`/analytics/query`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(QueryAnalytics200), + "400": decodeError("QueryAnalytics400", QueryAnalytics400), + "401": decodeError("QueryAnalytics401", QueryAnalytics401), + "403": decodeError("QueryAnalytics403", QueryAnalytics403), + "408": decodeError("QueryAnalytics408", QueryAnalytics408), + "500": decodeError("QueryAnalytics500", QueryAnalytics500), + orElse: unexpectedStatus + })) + ), + "createAudioSpeech": (options) => + HttpClientRequest.post(`/audio/speech`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "400": decodeError("CreateAudioSpeech400", CreateAudioSpeech400), + "401": decodeError("CreateAudioSpeech401", CreateAudioSpeech401), + "402": decodeError("CreateAudioSpeech402", CreateAudioSpeech402), + "404": decodeError("CreateAudioSpeech404", CreateAudioSpeech404), + "429": decodeError("CreateAudioSpeech429", CreateAudioSpeech429), + "500": decodeError("CreateAudioSpeech500", CreateAudioSpeech500), + "502": decodeError("CreateAudioSpeech502", CreateAudioSpeech502), + "503": decodeError("CreateAudioSpeech503", CreateAudioSpeech503), + "524": decodeError("CreateAudioSpeech524", CreateAudioSpeech524), + "529": decodeError("CreateAudioSpeech529", CreateAudioSpeech529), + orElse: unexpectedStatus + })) + ), + "createAudioTranscriptions": (options) => + HttpClientRequest.post(`/audio/transcriptions`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateAudioTranscriptions200), + "400": decodeError("CreateAudioTranscriptions400", CreateAudioTranscriptions400), + "401": decodeError("CreateAudioTranscriptions401", CreateAudioTranscriptions401), + "402": decodeError("CreateAudioTranscriptions402", CreateAudioTranscriptions402), + "404": decodeError("CreateAudioTranscriptions404", CreateAudioTranscriptions404), + "429": decodeError("CreateAudioTranscriptions429", CreateAudioTranscriptions429), + "500": decodeError("CreateAudioTranscriptions500", CreateAudioTranscriptions500), + "502": decodeError("CreateAudioTranscriptions502", CreateAudioTranscriptions502), + "503": decodeError("CreateAudioTranscriptions503", CreateAudioTranscriptions503), + "524": decodeError("CreateAudioTranscriptions524", CreateAudioTranscriptions524), + "529": decodeError("CreateAudioTranscriptions529", CreateAudioTranscriptions529), + orElse: unexpectedStatus + })) + ), + "exchangeAuthCodeForAPIKey": (options) => + HttpClientRequest.post(`/auth/keys`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ExchangeAuthCodeForAPIKey200), + "400": decodeError("ExchangeAuthCodeForAPIKey400", ExchangeAuthCodeForAPIKey400), + "403": decodeError("ExchangeAuthCodeForAPIKey403", ExchangeAuthCodeForAPIKey403), + "500": decodeError("ExchangeAuthCodeForAPIKey500", ExchangeAuthCodeForAPIKey500), + orElse: unexpectedStatus + })) + ), + "createAuthKeysCode": (options) => + HttpClientRequest.post(`/auth/keys/code`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateAuthKeysCode200), + "400": decodeError("CreateAuthKeysCode400", CreateAuthKeysCode400), + "401": decodeError("CreateAuthKeysCode401", CreateAuthKeysCode401), + "403": decodeError("CreateAuthKeysCode403", CreateAuthKeysCode403), + "409": decodeError("CreateAuthKeysCode409", CreateAuthKeysCode409), + "500": decodeError("CreateAuthKeysCode500", CreateAuthKeysCode500), + orElse: unexpectedStatus + })) + ), + "getBenchmarks": (options) => + HttpClientRequest.get(`/benchmarks`).pipe( + HttpClientRequest.setUrlParams({ + "source": options?.params?.["source"] as any, + "task_type": options?.params?.["task_type"] as any, + "arena": options?.params?.["arena"] as any, + "category": options?.params?.["category"] as any, + "max_results": options?.params?.["max_results"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetBenchmarks200), + "400": decodeError("GetBenchmarks400", GetBenchmarks400), + "401": decodeError("GetBenchmarks401", GetBenchmarks401), + "429": decodeError("GetBenchmarks429", GetBenchmarks429), + "500": decodeError("GetBenchmarks500", GetBenchmarks500), + orElse: unexpectedStatus + })) + ), + "listBYOKKeys": (options) => + HttpClientRequest.get(`/byok`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any, + "workspace_id": options?.params?.["workspace_id"] as any, + "provider": options?.params?.["provider"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListBYOKKeys200), + "400": decodeError("ListBYOKKeys400", ListBYOKKeys400), + "401": decodeError("ListBYOKKeys401", ListBYOKKeys401), + "500": decodeError("ListBYOKKeys500", ListBYOKKeys500), + orElse: unexpectedStatus + })) + ), + "createBYOKKey": (options) => + HttpClientRequest.post(`/byok`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateBYOKKey201), + "400": decodeError("CreateBYOKKey400", CreateBYOKKey400), + "401": decodeError("CreateBYOKKey401", CreateBYOKKey401), + "403": decodeError("CreateBYOKKey403", CreateBYOKKey403), + "500": decodeError("CreateBYOKKey500", CreateBYOKKey500), + orElse: unexpectedStatus + })) + ), + "getBYOKKey": (id, options) => + HttpClientRequest.get(`/byok/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetBYOKKey200), + "401": decodeError("GetBYOKKey401", GetBYOKKey401), + "404": decodeError("GetBYOKKey404", GetBYOKKey404), + "500": decodeError("GetBYOKKey500", GetBYOKKey500), + orElse: unexpectedStatus + })) + ), + "deleteBYOKKey": (id, options) => + HttpClientRequest.delete(`/byok/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteBYOKKey200), + "401": decodeError("DeleteBYOKKey401", DeleteBYOKKey401), + "404": decodeError("DeleteBYOKKey404", DeleteBYOKKey404), + "500": decodeError("DeleteBYOKKey500", DeleteBYOKKey500), + orElse: unexpectedStatus + })) + ), + "updateBYOKKey": (id, options) => + HttpClientRequest.patch(`/byok/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateBYOKKey200), + "400": decodeError("UpdateBYOKKey400", UpdateBYOKKey400), + "401": decodeError("UpdateBYOKKey401", UpdateBYOKKey401), + "404": decodeError("UpdateBYOKKey404", UpdateBYOKKey404), + "500": decodeError("UpdateBYOKKey500", UpdateBYOKKey500), + orElse: unexpectedStatus + })) + ), + "sendChatCompletionRequest": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SendChatCompletionRequest200), + "400": decodeError("SendChatCompletionRequest400", SendChatCompletionRequest400), + "401": decodeError("SendChatCompletionRequest401", SendChatCompletionRequest401), + "402": decodeError("SendChatCompletionRequest402", SendChatCompletionRequest402), + "403": decodeError("SendChatCompletionRequest403", SendChatCompletionRequest403), + "404": decodeError("SendChatCompletionRequest404", SendChatCompletionRequest404), + "408": decodeError("SendChatCompletionRequest408", SendChatCompletionRequest408), + "413": decodeError("SendChatCompletionRequest413", SendChatCompletionRequest413), + "422": decodeError("SendChatCompletionRequest422", SendChatCompletionRequest422), + "429": decodeError("SendChatCompletionRequest429", SendChatCompletionRequest429), + "500": decodeError("SendChatCompletionRequest500", SendChatCompletionRequest500), + "502": decodeError("SendChatCompletionRequest502", SendChatCompletionRequest502), + "503": decodeError("SendChatCompletionRequest503", SendChatCompletionRequest503), + "524": decodeError("SendChatCompletionRequest524", SendChatCompletionRequest524), + "529": decodeError("SendChatCompletionRequest529", SendChatCompletionRequest529), + orElse: unexpectedStatus + })) + ), + "sendChatCompletionRequestSse": (options) => + HttpClientRequest.post(`/chat/completions`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(SendChatCompletionRequest200Sse) + ), + "getTaskClassifications": (options) => + HttpClientRequest.get(`/classifications/task`).pipe( + HttpClientRequest.setUrlParams({ "window": options?.params?.["window"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetTaskClassifications200), + "400": decodeError("GetTaskClassifications400", GetTaskClassifications400), + "401": decodeError("GetTaskClassifications401", GetTaskClassifications401), + "429": decodeError("GetTaskClassifications429", GetTaskClassifications429), + "500": decodeError("GetTaskClassifications500", GetTaskClassifications500), + orElse: unexpectedStatus + })) + ), + "getCredits": (options) => + HttpClientRequest.get(`/credits`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCredits200), + "401": decodeError("GetCredits401", GetCredits401), + "403": decodeError("GetCredits403", GetCredits403), + "500": decodeError("GetCredits500", GetCredits500), + orElse: unexpectedStatus + })) + ), + "createCoinbaseCharge": (options) => + HttpClientRequest.post(`/credits/coinbase`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "410": decodeError("CreateCoinbaseCharge410", CreateCoinbaseCharge410), + "200": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "getAppRankings": (options) => + HttpClientRequest.get(`/datasets/app-rankings`).pipe( + HttpClientRequest.setUrlParams({ + "category": options?.params?.["category"] as any, + "subcategory": options?.params?.["subcategory"] as any, + "sort": options?.params?.["sort"] as any, + "start_date": options?.params?.["start_date"] as any, + "end_date": options?.params?.["end_date"] as any, + "limit": options?.params?.["limit"] as any, + "offset": options?.params?.["offset"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetAppRankings200), + "400": decodeError("GetAppRankings400", GetAppRankings400), + "401": decodeError("GetAppRankings401", GetAppRankings401), + "429": decodeError("GetAppRankings429", GetAppRankings429), + "500": decodeError("GetAppRankings500", GetAppRankings500), + orElse: unexpectedStatus + })) + ), + "getRankingsDaily": (options) => + HttpClientRequest.get(`/datasets/rankings-daily`).pipe( + HttpClientRequest.setUrlParams({ + "start_date": options?.params?.["start_date"] as any, + "end_date": options?.params?.["end_date"] as any, + "period": options?.params?.["period"] as any, + "modality": options?.params?.["modality"] as any, + "context_bucket": options?.params?.["context_bucket"] as any, + "category": options?.params?.["category"] as any, + "language_type": options?.params?.["language_type"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetRankingsDaily200), + "400": decodeError("GetRankingsDaily400", GetRankingsDaily400), + "401": decodeError("GetRankingsDaily401", GetRankingsDaily401), + "429": decodeError("GetRankingsDaily429", GetRankingsDaily429), + "500": decodeError("GetRankingsDaily500", GetRankingsDaily500), + orElse: unexpectedStatus + })) + ), + "createEmbeddings": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateEmbeddings200), + "400": decodeError("CreateEmbeddings400", CreateEmbeddings400), + "401": decodeError("CreateEmbeddings401", CreateEmbeddings401), + "402": decodeError("CreateEmbeddings402", CreateEmbeddings402), + "404": decodeError("CreateEmbeddings404", CreateEmbeddings404), + "429": decodeError("CreateEmbeddings429", CreateEmbeddings429), + "500": decodeError("CreateEmbeddings500", CreateEmbeddings500), + "502": decodeError("CreateEmbeddings502", CreateEmbeddings502), + "503": decodeError("CreateEmbeddings503", CreateEmbeddings503), + "524": decodeError("CreateEmbeddings524", CreateEmbeddings524), + "529": decodeError("CreateEmbeddings529", CreateEmbeddings529), + orElse: unexpectedStatus + })) + ), + "createEmbeddingsSse": (options) => + HttpClientRequest.post(`/embeddings`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateEmbeddings200Sse) + ), + "listEmbeddingsModels": (options) => + HttpClientRequest.get(`/embeddings/models`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEmbeddingsModels200), + "400": decodeError("ListEmbeddingsModels400", ListEmbeddingsModels400), + "500": decodeError("ListEmbeddingsModels500", ListEmbeddingsModels500), + orElse: unexpectedStatus + })) + ), + "listEndpointsZdr": (options) => + HttpClientRequest.get(`/endpoints/zdr`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpointsZdr200), + "403": decodeError("ListEndpointsZdr403", ListEndpointsZdr403), + "500": decodeError("ListEndpointsZdr500", ListEndpointsZdr500), + orElse: unexpectedStatus + })) + ), + "listFiles": (options) => + HttpClientRequest.get(`/files`).pipe( + HttpClientRequest.setUrlParams({ + "limit": options?.params?.["limit"] as any, + "cursor": options?.params?.["cursor"] as any, + "workspace_id": options?.params?.["workspace_id"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListFiles200), + "400": decodeError("ListFiles400", ListFiles400), + "401": decodeError("ListFiles401", ListFiles401), + "429": decodeError("ListFiles429", ListFiles429), + "500": decodeError("ListFiles500", ListFiles500), + orElse: unexpectedStatus + })) + ), + "uploadFile": (options) => + HttpClientRequest.post(`/files`).pipe( + HttpClientRequest.setUrlParams({ "workspace_id": options.params?.["workspace_id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyFormData(options.payload as any), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UploadFile200), + "400": decodeError("UploadFile400", UploadFile400), + "401": decodeError("UploadFile401", UploadFile401), + "403": decodeError("UploadFile403", UploadFile403), + "413": decodeError("UploadFile413", UploadFile413), + "429": decodeError("UploadFile429", UploadFile429), + "500": decodeError("UploadFile500", UploadFile500), + orElse: unexpectedStatus + })) + ), + "getFileMetadata": (fileId, options) => + HttpClientRequest.get(`/files/${fileId}`).pipe( + HttpClientRequest.setUrlParams({ "workspace_id": options?.params?.["workspace_id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetFileMetadata200), + "401": decodeError("GetFileMetadata401", GetFileMetadata401), + "404": decodeError("GetFileMetadata404", GetFileMetadata404), + "429": decodeError("GetFileMetadata429", GetFileMetadata429), + "500": decodeError("GetFileMetadata500", GetFileMetadata500), + orElse: unexpectedStatus + })) + ), + "deleteFile": (fileId, options) => + HttpClientRequest.delete(`/files/${fileId}`).pipe( + HttpClientRequest.setUrlParams({ "workspace_id": options?.params?.["workspace_id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteFile200), + "401": decodeError("DeleteFile401", DeleteFile401), + "404": decodeError("DeleteFile404", DeleteFile404), + "429": decodeError("DeleteFile429", DeleteFile429), + "500": decodeError("DeleteFile500", DeleteFile500), + orElse: unexpectedStatus + })) + ), + "downloadFileContent": (fileId, options) => + HttpClientRequest.get(`/files/${fileId}/content`).pipe( + HttpClientRequest.setUrlParams({ "workspace_id": options?.params?.["workspace_id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "400": decodeError("DownloadFileContent400", DownloadFileContent400), + "401": decodeError("DownloadFileContent401", DownloadFileContent401), + "404": decodeError("DownloadFileContent404", DownloadFileContent404), + "429": decodeError("DownloadFileContent429", DownloadFileContent429), + "500": decodeError("DownloadFileContent500", DownloadFileContent500), + orElse: unexpectedStatus + })) + ), + "downloadFileContentStream": (fileId, options) => + HttpClientRequest.get(`/files/${fileId}/content`).pipe( + HttpClientRequest.setUrlParams({ "workspace_id": options?.params?.["workspace_id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + binaryRequest + ), + "getGeneration": (options) => + HttpClientRequest.get(`/generation`).pipe( + HttpClientRequest.setUrlParams({ "id": options.params["id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGeneration200), + "401": decodeError("GetGeneration401", GetGeneration401), + "402": decodeError("GetGeneration402", GetGeneration402), + "404": decodeError("GetGeneration404", GetGeneration404), + "429": decodeError("GetGeneration429", GetGeneration429), + "500": decodeError("GetGeneration500", GetGeneration500), + "502": decodeError("GetGeneration502", GetGeneration502), + "524": decodeError("GetGeneration524", GetGeneration524), + "529": decodeError("GetGeneration529", GetGeneration529), + orElse: unexpectedStatus + })) + ), + "listGenerationContent": (options) => + HttpClientRequest.get(`/generation/content`).pipe( + HttpClientRequest.setUrlParams({ "id": options.params["id"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGenerationContent200), + "401": decodeError("ListGenerationContent401", ListGenerationContent401), + "403": decodeError("ListGenerationContent403", ListGenerationContent403), + "404": decodeError("ListGenerationContent404", ListGenerationContent404), + "429": decodeError("ListGenerationContent429", ListGenerationContent429), + "500": decodeError("ListGenerationContent500", ListGenerationContent500), + "502": decodeError("ListGenerationContent502", ListGenerationContent502), + "524": decodeError("ListGenerationContent524", ListGenerationContent524), + "529": decodeError("ListGenerationContent529", ListGenerationContent529), + orElse: unexpectedStatus + })) + ), + "submitGenerationFeedback": (options) => + HttpClientRequest.post(`/generation/feedback`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SubmitGenerationFeedback200), + "400": decodeError("SubmitGenerationFeedback400", SubmitGenerationFeedback400), + "401": decodeError("SubmitGenerationFeedback401", SubmitGenerationFeedback401), + "404": decodeError("SubmitGenerationFeedback404", SubmitGenerationFeedback404), + "429": decodeError("SubmitGenerationFeedback429", SubmitGenerationFeedback429), + "500": decodeError("SubmitGenerationFeedback500", SubmitGenerationFeedback500), + orElse: unexpectedStatus + })) + ), + "listGuardrails": (options) => + HttpClientRequest.get(`/guardrails`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any, + "workspace_id": options?.params?.["workspace_id"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrails200), + "400": decodeError("ListGuardrails400", ListGuardrails400), + "401": decodeError("ListGuardrails401", ListGuardrails401), + "500": decodeError("ListGuardrails500", ListGuardrails500), + orElse: unexpectedStatus + })) + ), + "createGuardrail": (options) => + HttpClientRequest.post(`/guardrails`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateGuardrail201), + "400": decodeError("CreateGuardrail400", CreateGuardrail400), + "401": decodeError("CreateGuardrail401", CreateGuardrail401), + "403": decodeError("CreateGuardrail403", CreateGuardrail403), + "500": decodeError("CreateGuardrail500", CreateGuardrail500), + orElse: unexpectedStatus + })) + ), + "getGuardrail": (id, options) => + HttpClientRequest.get(`/guardrails/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetGuardrail200), + "401": decodeError("GetGuardrail401", GetGuardrail401), + "404": decodeError("GetGuardrail404", GetGuardrail404), + "500": decodeError("GetGuardrail500", GetGuardrail500), + orElse: unexpectedStatus + })) + ), + "deleteGuardrail": (id, options) => + HttpClientRequest.delete(`/guardrails/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteGuardrail200), + "401": decodeError("DeleteGuardrail401", DeleteGuardrail401), + "404": decodeError("DeleteGuardrail404", DeleteGuardrail404), + "500": decodeError("DeleteGuardrail500", DeleteGuardrail500), + orElse: unexpectedStatus + })) + ), + "updateGuardrail": (id, options) => + HttpClientRequest.patch(`/guardrails/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateGuardrail200), + "400": decodeError("UpdateGuardrail400", UpdateGuardrail400), + "401": decodeError("UpdateGuardrail401", UpdateGuardrail401), + "404": decodeError("UpdateGuardrail404", UpdateGuardrail404), + "500": decodeError("UpdateGuardrail500", UpdateGuardrail500), + orElse: unexpectedStatus + })) + ), + "listGuardrailKeyAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailKeyAssignments200), + "401": decodeError("ListGuardrailKeyAssignments401", ListGuardrailKeyAssignments401), + "404": decodeError("ListGuardrailKeyAssignments404", ListGuardrailKeyAssignments404), + "500": decodeError("ListGuardrailKeyAssignments500", ListGuardrailKeyAssignments500), + orElse: unexpectedStatus + })) + ), + "bulkAssignKeysToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignKeysToGuardrail200), + "400": decodeError("BulkAssignKeysToGuardrail400", BulkAssignKeysToGuardrail400), + "401": decodeError("BulkAssignKeysToGuardrail401", BulkAssignKeysToGuardrail401), + "404": decodeError("BulkAssignKeysToGuardrail404", BulkAssignKeysToGuardrail404), + "500": decodeError("BulkAssignKeysToGuardrail500", BulkAssignKeysToGuardrail500), + orElse: unexpectedStatus + })) + ), + "bulkUnassignKeysFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/keys/remove`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignKeysFromGuardrail200), + "400": decodeError("BulkUnassignKeysFromGuardrail400", BulkUnassignKeysFromGuardrail400), + "401": decodeError("BulkUnassignKeysFromGuardrail401", BulkUnassignKeysFromGuardrail401), + "404": decodeError("BulkUnassignKeysFromGuardrail404", BulkUnassignKeysFromGuardrail404), + "500": decodeError("BulkUnassignKeysFromGuardrail500", BulkUnassignKeysFromGuardrail500), + orElse: unexpectedStatus + })) + ), + "listGuardrailMemberAssignments": (id, options) => + HttpClientRequest.get(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGuardrailMemberAssignments200), + "401": decodeError("ListGuardrailMemberAssignments401", ListGuardrailMemberAssignments401), + "404": decodeError("ListGuardrailMemberAssignments404", ListGuardrailMemberAssignments404), + "500": decodeError("ListGuardrailMemberAssignments500", ListGuardrailMemberAssignments500), + orElse: unexpectedStatus + })) + ), + "bulkAssignMembersToGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAssignMembersToGuardrail200), + "400": decodeError("BulkAssignMembersToGuardrail400", BulkAssignMembersToGuardrail400), + "401": decodeError("BulkAssignMembersToGuardrail401", BulkAssignMembersToGuardrail401), + "404": decodeError("BulkAssignMembersToGuardrail404", BulkAssignMembersToGuardrail404), + "500": decodeError("BulkAssignMembersToGuardrail500", BulkAssignMembersToGuardrail500), + orElse: unexpectedStatus + })) + ), + "bulkUnassignMembersFromGuardrail": (id, options) => + HttpClientRequest.post(`/guardrails/${id}/assignments/members/remove`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkUnassignMembersFromGuardrail200), + "400": decodeError("BulkUnassignMembersFromGuardrail400", BulkUnassignMembersFromGuardrail400), + "401": decodeError("BulkUnassignMembersFromGuardrail401", BulkUnassignMembersFromGuardrail401), + "404": decodeError("BulkUnassignMembersFromGuardrail404", BulkUnassignMembersFromGuardrail404), + "500": decodeError("BulkUnassignMembersFromGuardrail500", BulkUnassignMembersFromGuardrail500), + orElse: unexpectedStatus + })) + ), + "listKeyAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/keys`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListKeyAssignments200), + "401": decodeError("ListKeyAssignments401", ListKeyAssignments401), + "500": decodeError("ListKeyAssignments500", ListKeyAssignments500), + orElse: unexpectedStatus + })) + ), + "listMemberAssignments": (options) => + HttpClientRequest.get(`/guardrails/assignments/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListMemberAssignments200), + "401": decodeError("ListMemberAssignments401", ListMemberAssignments401), + "500": decodeError("ListMemberAssignments500", ListMemberAssignments500), + orElse: unexpectedStatus + })) + ), + "createImages": (options) => + HttpClientRequest.post(`/images`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateImages200), + "400": decodeError("CreateImages400", CreateImages400), + "401": decodeError("CreateImages401", CreateImages401), + "402": decodeError("CreateImages402", CreateImages402), + "403": decodeError("CreateImages403", CreateImages403), + "404": decodeError("CreateImages404", CreateImages404), + "413": decodeError("CreateImages413", CreateImages413), + "429": decodeError("CreateImages429", CreateImages429), + "500": decodeError("CreateImages500", CreateImages500), + "502": decodeError("CreateImages502", CreateImages502), + "524": decodeError("CreateImages524", CreateImages524), + "529": decodeError("CreateImages529", CreateImages529), + orElse: unexpectedStatus + })) + ), + "createImagesSse": (options) => + HttpClientRequest.post(`/images`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateImages200Sse) + ), + "listImageModels": (options) => + HttpClientRequest.get(`/images/models`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListImageModels200), + "500": decodeError("ListImageModels500", ListImageModels500), + orElse: unexpectedStatus + })) + ), + "listImageModelEndpoints": (author, slug, options) => + HttpClientRequest.get(`/images/models/${author}/${slug}/endpoints`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListImageModelEndpoints200), + "404": decodeError("ListImageModelEndpoints404", ListImageModelEndpoints404), + "500": decodeError("ListImageModelEndpoints500", ListImageModelEndpoints500), + orElse: unexpectedStatus + })) + ), + "getCurrentKey": (options) => + HttpClientRequest.get(`/key`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetCurrentKey200), + "401": decodeError("GetCurrentKey401", GetCurrentKey401), + "500": decodeError("GetCurrentKey500", GetCurrentKey500), + orElse: unexpectedStatus + })) + ), + "list": (options) => + HttpClientRequest.get(`/keys`).pipe( + HttpClientRequest.setUrlParams({ + "include_disabled": options?.params?.["include_disabled"] as any, + "offset": options?.params?.["offset"] as any, + "workspace_id": options?.params?.["workspace_id"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(List200), + "400": decodeError("List400", List400), + "401": decodeError("List401", List401), + "429": decodeError("List429", List429), + "500": decodeError("List500", List500), + orElse: unexpectedStatus + })) + ), + "createKeys": (options) => + HttpClientRequest.post(`/keys`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateKeys201), + "400": decodeError("CreateKeys400", CreateKeys400), + "401": decodeError("CreateKeys401", CreateKeys401), + "403": decodeError("CreateKeys403", CreateKeys403), + "429": decodeError("CreateKeys429", CreateKeys429), + "500": decodeError("CreateKeys500", CreateKeys500), + orElse: unexpectedStatus + })) + ), + "getKey": (hash, options) => + HttpClientRequest.get(`/keys/${hash}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetKey200), + "401": decodeError("GetKey401", GetKey401), + "404": decodeError("GetKey404", GetKey404), + "429": decodeError("GetKey429", GetKey429), + "500": decodeError("GetKey500", GetKey500), + orElse: unexpectedStatus + })) + ), + "deleteKeys": (hash, options) => + HttpClientRequest.delete(`/keys/${hash}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteKeys200), + "401": decodeError("DeleteKeys401", DeleteKeys401), + "404": decodeError("DeleteKeys404", DeleteKeys404), + "429": decodeError("DeleteKeys429", DeleteKeys429), + "500": decodeError("DeleteKeys500", DeleteKeys500), + orElse: unexpectedStatus + })) + ), + "updateKeys": (hash, options) => + HttpClientRequest.patch(`/keys/${hash}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateKeys200), + "400": decodeError("UpdateKeys400", UpdateKeys400), + "401": decodeError("UpdateKeys401", UpdateKeys401), + "404": decodeError("UpdateKeys404", UpdateKeys404), + "429": decodeError("UpdateKeys429", UpdateKeys429), + "500": decodeError("UpdateKeys500", UpdateKeys500), + orElse: unexpectedStatus + })) + ), + "createMessages": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateMessages200), + "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 + })) + ), + "createMessagesSse": (options) => + HttpClientRequest.post(`/messages`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateMessages200Sse) + ), + "getModel": (author, slug, options) => + HttpClientRequest.get(`/model/${author}/${slug}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetModel200), + "403": decodeError("GetModel403", GetModel403), + "404": decodeError("GetModel404", GetModel404), + "500": decodeError("GetModel500", GetModel500), + orElse: unexpectedStatus + })) + ), + "getModels": (options) => + HttpClientRequest.get(`/models`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any, + "category": options?.params?.["category"] as any, + "supported_parameters": options?.params?.["supported_parameters"] as any, + "output_modalities": options?.params?.["output_modalities"] as any, + "sort": options?.params?.["sort"] as any, + "q": options?.params?.["q"] as any, + "input_modalities": options?.params?.["input_modalities"] as any, + "context": options?.params?.["context"] as any, + "min_price": options?.params?.["min_price"] as any, + "max_price": options?.params?.["max_price"] as any, + "arch": options?.params?.["arch"] as any, + "model_authors": options?.params?.["model_authors"] as any, + "providers": options?.params?.["providers"] as any, + "distillable": options?.params?.["distillable"] as any, + "zdr": options?.params?.["zdr"] as any, + "region": options?.params?.["region"] as any, + "min_output_price": options?.params?.["min_output_price"] as any, + "max_output_price": options?.params?.["max_output_price"] as any, + "min_age_days": options?.params?.["min_age_days"] as any, + "max_age_days": options?.params?.["max_age_days"] as any, + "min_intelligence_index": options?.params?.["min_intelligence_index"] as any, + "max_intelligence_index": options?.params?.["max_intelligence_index"] as any, + "min_coding_index": options?.params?.["min_coding_index"] as any, + "max_coding_index": options?.params?.["max_coding_index"] as any, + "min_agentic_index": options?.params?.["min_agentic_index"] as any, + "max_agentic_index": options?.params?.["max_agentic_index"] as any, + "min_tool_success_rate": options?.params?.["min_tool_success_rate"] as any, + "max_tool_success_rate": options?.params?.["max_tool_success_rate"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetModels200), + "400": decodeError("GetModels400", GetModels400), + "403": decodeError("GetModels403", GetModels403), + "500": decodeError("GetModels500", GetModels500), + orElse: unexpectedStatus + })) + ), + "listEndpoints": (author, slug, options) => + HttpClientRequest.get(`/models/${author}/${slug}/endpoints`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListEndpoints200), + "403": decodeError("ListEndpoints403", ListEndpoints403), + "404": decodeError("ListEndpoints404", ListEndpoints404), + "500": decodeError("ListEndpoints500", ListEndpoints500), + orElse: unexpectedStatus + })) + ), + "listModelsCount": (options) => + HttpClientRequest.get(`/models/count`).pipe( + HttpClientRequest.setUrlParams({ "output_modalities": options?.params?.["output_modalities"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModelsCount200), + "400": decodeError("ListModelsCount400", ListModelsCount400), + "403": decodeError("ListModelsCount403", ListModelsCount403), + "500": decodeError("ListModelsCount500", ListModelsCount500), + orElse: unexpectedStatus + })) + ), + "listModelsUser": (options) => + HttpClientRequest.get(`/models/user`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListModelsUser200), + "401": decodeError("ListModelsUser401", ListModelsUser401), + "403": decodeError("ListModelsUser403", ListModelsUser403), + "404": decodeError("ListModelsUser404", ListModelsUser404), + "500": decodeError("ListModelsUser500", ListModelsUser500), + orElse: unexpectedStatus + })) + ), + "listObservabilityDestinations": (options) => + HttpClientRequest.get(`/observability/destinations`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any, + "workspace_id": options?.params?.["workspace_id"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListObservabilityDestinations200), + "400": decodeError("ListObservabilityDestinations400", ListObservabilityDestinations400), + "401": decodeError("ListObservabilityDestinations401", ListObservabilityDestinations401), + "500": decodeError("ListObservabilityDestinations500", ListObservabilityDestinations500), + orElse: unexpectedStatus + })) + ), + "createObservabilityDestination": (options) => + HttpClientRequest.post(`/observability/destinations`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateObservabilityDestination201), + "400": decodeError("CreateObservabilityDestination400", CreateObservabilityDestination400), + "401": decodeError("CreateObservabilityDestination401", CreateObservabilityDestination401), + "403": decodeError("CreateObservabilityDestination403", CreateObservabilityDestination403), + "409": decodeError("CreateObservabilityDestination409", CreateObservabilityDestination409), + "500": decodeError("CreateObservabilityDestination500", CreateObservabilityDestination500), + orElse: unexpectedStatus + })) + ), + "getObservabilityDestination": (id, options) => + HttpClientRequest.get(`/observability/destinations/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetObservabilityDestination200), + "401": decodeError("GetObservabilityDestination401", GetObservabilityDestination401), + "404": decodeError("GetObservabilityDestination404", GetObservabilityDestination404), + "500": decodeError("GetObservabilityDestination500", GetObservabilityDestination500), + orElse: unexpectedStatus + })) + ), + "deleteObservabilityDestination": (id, options) => + HttpClientRequest.delete(`/observability/destinations/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteObservabilityDestination200), + "401": decodeError("DeleteObservabilityDestination401", DeleteObservabilityDestination401), + "404": decodeError("DeleteObservabilityDestination404", DeleteObservabilityDestination404), + "500": decodeError("DeleteObservabilityDestination500", DeleteObservabilityDestination500), + orElse: unexpectedStatus + })) + ), + "updateObservabilityDestination": (id, options) => + HttpClientRequest.patch(`/observability/destinations/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateObservabilityDestination200), + "400": decodeError("UpdateObservabilityDestination400", UpdateObservabilityDestination400), + "401": decodeError("UpdateObservabilityDestination401", UpdateObservabilityDestination401), + "404": decodeError("UpdateObservabilityDestination404", UpdateObservabilityDestination404), + "409": decodeError("UpdateObservabilityDestination409", UpdateObservabilityDestination409), + "500": decodeError("UpdateObservabilityDestination500", UpdateObservabilityDestination500), + orElse: unexpectedStatus + })) + ), + "listOrganizationMembers": (options) => + HttpClientRequest.get(`/organization/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListOrganizationMembers200), + "401": decodeError("ListOrganizationMembers401", ListOrganizationMembers401), + "404": decodeError("ListOrganizationMembers404", ListOrganizationMembers404), + "500": decodeError("ListOrganizationMembers500", ListOrganizationMembers500), + orElse: unexpectedStatus + })) + ), + "listPresets": (options) => + HttpClientRequest.get(`/presets`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPresets200), + "400": decodeError("ListPresets400", ListPresets400), + "401": decodeError("ListPresets401", ListPresets401), + "500": decodeError("ListPresets500", ListPresets500), + orElse: unexpectedStatus + })) + ), + "getPreset": (slug, options) => + HttpClientRequest.get(`/presets/${slug}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetPreset200), + "400": decodeError("GetPreset400", GetPreset400), + "401": decodeError("GetPreset401", GetPreset401), + "404": decodeError("GetPreset404", GetPreset404), + "500": decodeError("GetPreset500", GetPreset500), + orElse: unexpectedStatus + })) + ), + "createPresetsChatCompletions": (slug, options) => + HttpClientRequest.post(`/presets/${slug}/chat/completions`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreatePresetsChatCompletions200), + "400": decodeError("CreatePresetsChatCompletions400", CreatePresetsChatCompletions400), + "401": decodeError("CreatePresetsChatCompletions401", CreatePresetsChatCompletions401), + "403": decodeError("CreatePresetsChatCompletions403", CreatePresetsChatCompletions403), + "404": decodeError("CreatePresetsChatCompletions404", CreatePresetsChatCompletions404), + "409": decodeError("CreatePresetsChatCompletions409", CreatePresetsChatCompletions409), + "500": decodeError("CreatePresetsChatCompletions500", CreatePresetsChatCompletions500), + orElse: unexpectedStatus + })) + ), + "createPresetsMessages": (slug, options) => + HttpClientRequest.post(`/presets/${slug}/messages`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreatePresetsMessages200), + "400": decodeError("CreatePresetsMessages400", CreatePresetsMessages400), + "401": decodeError("CreatePresetsMessages401", CreatePresetsMessages401), + "403": decodeError("CreatePresetsMessages403", CreatePresetsMessages403), + "404": decodeError("CreatePresetsMessages404", CreatePresetsMessages404), + "409": decodeError("CreatePresetsMessages409", CreatePresetsMessages409), + "500": decodeError("CreatePresetsMessages500", CreatePresetsMessages500), + orElse: unexpectedStatus + })) + ), + "createPresetsResponses": (slug, options) => + HttpClientRequest.post(`/presets/${slug}/responses`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreatePresetsResponses200), + "400": decodeError("CreatePresetsResponses400", CreatePresetsResponses400), + "401": decodeError("CreatePresetsResponses401", CreatePresetsResponses401), + "403": decodeError("CreatePresetsResponses403", CreatePresetsResponses403), + "404": decodeError("CreatePresetsResponses404", CreatePresetsResponses404), + "409": decodeError("CreatePresetsResponses409", CreatePresetsResponses409), + "500": decodeError("CreatePresetsResponses500", CreatePresetsResponses500), + orElse: unexpectedStatus + })) + ), + "listPresetVersions": (slug, options) => + HttpClientRequest.get(`/presets/${slug}/versions`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPresetVersions200), + "400": decodeError("ListPresetVersions400", ListPresetVersions400), + "401": decodeError("ListPresetVersions401", ListPresetVersions401), + "404": decodeError("ListPresetVersions404", ListPresetVersions404), + "500": decodeError("ListPresetVersions500", ListPresetVersions500), + orElse: unexpectedStatus + })) + ), + "getPresetVersion": (slug, version, options) => + HttpClientRequest.get(`/presets/${slug}/versions/${version}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetPresetVersion200), + "400": decodeError("GetPresetVersion400", GetPresetVersion400), + "401": decodeError("GetPresetVersion401", GetPresetVersion401), + "404": decodeError("GetPresetVersion404", GetPresetVersion404), + "500": decodeError("GetPresetVersion500", GetPresetVersion500), + orElse: unexpectedStatus + })) + ), + "listProviders": (options) => + HttpClientRequest.get(`/providers`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListProviders200), + "500": decodeError("ListProviders500", ListProviders500), + orElse: unexpectedStatus + })) + ), + "createRerank": (options) => + HttpClientRequest.post(`/rerank`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateRerank200), + "400": decodeError("CreateRerank400", CreateRerank400), + "401": decodeError("CreateRerank401", CreateRerank401), + "402": decodeError("CreateRerank402", CreateRerank402), + "404": decodeError("CreateRerank404", CreateRerank404), + "429": decodeError("CreateRerank429", CreateRerank429), + "500": decodeError("CreateRerank500", CreateRerank500), + "502": decodeError("CreateRerank502", CreateRerank502), + "503": decodeError("CreateRerank503", CreateRerank503), + "524": decodeError("CreateRerank524", CreateRerank524), + "529": decodeError("CreateRerank529", CreateRerank529), + orElse: unexpectedStatus + })) + ), + "createRerankSse": (options) => + HttpClientRequest.post(`/rerank`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateRerank200Sse) + ), + "createResponses": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateResponses200), + "400": decodeError("CreateResponses400", CreateResponses400), + "401": decodeError("CreateResponses401", CreateResponses401), + "402": decodeError("CreateResponses402", CreateResponses402), + "403": decodeError("CreateResponses403", CreateResponses403), + "404": decodeError("CreateResponses404", CreateResponses404), + "408": decodeError("CreateResponses408", CreateResponses408), + "413": decodeError("CreateResponses413", CreateResponses413), + "422": decodeError("CreateResponses422", CreateResponses422), + "429": decodeError("CreateResponses429", CreateResponses429), + "500": decodeError("CreateResponses500", CreateResponses500), + "502": decodeError("CreateResponses502", CreateResponses502), + "503": decodeError("CreateResponses503", CreateResponses503), + "524": decodeError("CreateResponses524", CreateResponses524), + "529": decodeError("CreateResponses529", CreateResponses529), + orElse: unexpectedStatus + })) + ), + "createResponsesSse": (options) => + HttpClientRequest.post(`/responses`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined, + "X-OpenRouter-Metadata": options.params?.["X-OpenRouter-Metadata"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + sseRequest(CreateResponses200Sse) + ), + "createVideos": (options) => + HttpClientRequest.post(`/videos`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateVideos202), + "400": decodeError("CreateVideos400", CreateVideos400), + "401": decodeError("CreateVideos401", CreateVideos401), + "402": decodeError("CreateVideos402", CreateVideos402), + "404": decodeError("CreateVideos404", CreateVideos404), + "429": decodeError("CreateVideos429", CreateVideos429), + "500": decodeError("CreateVideos500", CreateVideos500), + orElse: unexpectedStatus + })) + ), + "getVideos": (jobId, options) => + HttpClientRequest.get(`/videos/${jobId}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetVideos200), + "401": decodeError("GetVideos401", GetVideos401), + "404": decodeError("GetVideos404", GetVideos404), + "500": decodeError("GetVideos500", GetVideos500), + orElse: unexpectedStatus + })) + ), + "listVideosContent": (jobId, options) => + HttpClientRequest.get(`/videos/${jobId}/content`).pipe( + HttpClientRequest.setUrlParams({ "index": options?.params?.["index"] as any }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "400": decodeError("ListVideosContent400", ListVideosContent400), + "401": decodeError("ListVideosContent401", ListVideosContent401), + "404": decodeError("ListVideosContent404", ListVideosContent404), + "500": decodeError("ListVideosContent500", ListVideosContent500), + "502": decodeError("ListVideosContent502", ListVideosContent502), + orElse: unexpectedStatus + })) + ), + "listVideosModels": (options) => + HttpClientRequest.get(`/videos/models`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListVideosModels200), + "400": decodeError("ListVideosModels400", ListVideosModels400), + "500": decodeError("ListVideosModels500", ListVideosModels500), + orElse: unexpectedStatus + })) + ), + "listWorkspaces": (options) => + HttpClientRequest.get(`/workspaces`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListWorkspaces200), + "401": decodeError("ListWorkspaces401", ListWorkspaces401), + "500": decodeError("ListWorkspaces500", ListWorkspaces500), + orElse: unexpectedStatus + })) + ), + "createWorkspace": (options) => + HttpClientRequest.post(`/workspaces`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateWorkspace201), + "400": decodeError("CreateWorkspace400", CreateWorkspace400), + "401": decodeError("CreateWorkspace401", CreateWorkspace401), + "403": decodeError("CreateWorkspace403", CreateWorkspace403), + "500": decodeError("CreateWorkspace500", CreateWorkspace500), + orElse: unexpectedStatus + })) + ), + "getWorkspace": (id, options) => + HttpClientRequest.get(`/workspaces/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetWorkspace200), + "401": decodeError("GetWorkspace401", GetWorkspace401), + "404": decodeError("GetWorkspace404", GetWorkspace404), + "500": decodeError("GetWorkspace500", GetWorkspace500), + orElse: unexpectedStatus + })) + ), + "deleteWorkspace": (id, options) => + HttpClientRequest.delete(`/workspaces/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteWorkspace200), + "400": decodeError("DeleteWorkspace400", DeleteWorkspace400), + "401": decodeError("DeleteWorkspace401", DeleteWorkspace401), + "403": decodeError("DeleteWorkspace403", DeleteWorkspace403), + "404": decodeError("DeleteWorkspace404", DeleteWorkspace404), + "500": decodeError("DeleteWorkspace500", DeleteWorkspace500), + orElse: unexpectedStatus + })) + ), + "updateWorkspace": (id, options) => + HttpClientRequest.patch(`/workspaces/${id}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateWorkspace200), + "400": decodeError("UpdateWorkspace400", UpdateWorkspace400), + "401": decodeError("UpdateWorkspace401", UpdateWorkspace401), + "403": decodeError("UpdateWorkspace403", UpdateWorkspace403), + "404": decodeError("UpdateWorkspace404", UpdateWorkspace404), + "500": decodeError("UpdateWorkspace500", UpdateWorkspace500), + orElse: unexpectedStatus + })) + ), + "listWorkspaceBudgets": (id, options) => + HttpClientRequest.get(`/workspaces/${id}/budgets`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListWorkspaceBudgets200), + "401": decodeError("ListWorkspaceBudgets401", ListWorkspaceBudgets401), + "404": decodeError("ListWorkspaceBudgets404", ListWorkspaceBudgets404), + "500": decodeError("ListWorkspaceBudgets500", ListWorkspaceBudgets500), + orElse: unexpectedStatus + })) + ), + "upsertWorkspaceBudget": (id, interval, options) => + HttpClientRequest.put(`/workspaces/${id}/budgets/${interval}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpsertWorkspaceBudget200), + "400": decodeError("UpsertWorkspaceBudget400", UpsertWorkspaceBudget400), + "401": decodeError("UpsertWorkspaceBudget401", UpsertWorkspaceBudget401), + "404": decodeError("UpsertWorkspaceBudget404", UpsertWorkspaceBudget404), + "500": decodeError("UpsertWorkspaceBudget500", UpsertWorkspaceBudget500), + orElse: unexpectedStatus + })) + ), + "deleteWorkspaceBudget": (id, interval, options) => + HttpClientRequest.delete(`/workspaces/${id}/budgets/${interval}`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteWorkspaceBudget200), + "401": decodeError("DeleteWorkspaceBudget401", DeleteWorkspaceBudget401), + "404": decodeError("DeleteWorkspaceBudget404", DeleteWorkspaceBudget404), + "500": decodeError("DeleteWorkspaceBudget500", DeleteWorkspaceBudget500), + orElse: unexpectedStatus + })) + ), + "listWorkspaceMembers": (id, options) => + HttpClientRequest.get(`/workspaces/${id}/members`).pipe( + HttpClientRequest.setUrlParams({ + "offset": options?.params?.["offset"] as any, + "limit": options?.params?.["limit"] as any + }), + HttpClientRequest.setHeaders({ + "HTTP-Referer": options?.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options?.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options?.params?.["X-OpenRouter-Categories"] ?? undefined + }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListWorkspaceMembers200), + "401": decodeError("ListWorkspaceMembers401", ListWorkspaceMembers401), + "403": decodeError("ListWorkspaceMembers403", ListWorkspaceMembers403), + "404": decodeError("ListWorkspaceMembers404", ListWorkspaceMembers404), + "500": decodeError("ListWorkspaceMembers500", ListWorkspaceMembers500), + orElse: unexpectedStatus + })) + ), + "bulkAddWorkspaceMembers": (id, options) => + HttpClientRequest.post(`/workspaces/${id}/members/add`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkAddWorkspaceMembers200), + "400": decodeError("BulkAddWorkspaceMembers400", BulkAddWorkspaceMembers400), + "401": decodeError("BulkAddWorkspaceMembers401", BulkAddWorkspaceMembers401), + "403": decodeError("BulkAddWorkspaceMembers403", BulkAddWorkspaceMembers403), + "404": decodeError("BulkAddWorkspaceMembers404", BulkAddWorkspaceMembers404), + "500": decodeError("BulkAddWorkspaceMembers500", BulkAddWorkspaceMembers500), + orElse: unexpectedStatus + })) + ), + "bulkRemoveWorkspaceMembers": (id, options) => + HttpClientRequest.post(`/workspaces/${id}/members/remove`).pipe( + HttpClientRequest.setHeaders({ + "HTTP-Referer": options.params?.["HTTP-Referer"] ?? undefined, + "X-OpenRouter-Title": options.params?.["X-OpenRouter-Title"] ?? undefined, + "X-OpenRouter-Categories": options.params?.["X-OpenRouter-Categories"] ?? undefined + }), + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(BulkRemoveWorkspaceMembers200), + "400": decodeError("BulkRemoveWorkspaceMembers400", BulkRemoveWorkspaceMembers400), + "401": decodeError("BulkRemoveWorkspaceMembers401", BulkRemoveWorkspaceMembers401), + "403": decodeError("BulkRemoveWorkspaceMembers403", BulkRemoveWorkspaceMembers403), + "404": decodeError("BulkRemoveWorkspaceMembers404", BulkRemoveWorkspaceMembers404), + "500": decodeError("BulkRemoveWorkspaceMembers500", BulkRemoveWorkspaceMembers500), + orElse: unexpectedStatus + })) + ) + } +} + +export interface OpenRouterClient { + readonly httpClient: HttpClient.HttpClient + /** + * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getUserActivity": ( + options: { + readonly params?: typeof GetUserActivityParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetUserActivity400", typeof GetUserActivity400.Type> + | OpenRouterClientError<"GetUserActivity401", typeof GetUserActivity401.Type> + | OpenRouterClientError<"GetUserActivity403", typeof GetUserActivity403.Type> + | OpenRouterClientError<"GetUserActivity404", typeof GetUserActivity404.Type> + | OpenRouterClientError<"GetUserActivity500", typeof GetUserActivity500.Type> + > + /** + * Returns the available metrics, dimensions, filter operators, and granularities for the analytics query endpoint. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getAnalyticsMeta": ( + options: { + readonly params?: typeof GetAnalyticsMetaParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetAnalyticsMeta401", typeof GetAnalyticsMeta401.Type> + | OpenRouterClientError<"GetAnalyticsMeta403", typeof GetAnalyticsMeta403.Type> + | OpenRouterClientError<"GetAnalyticsMeta500", typeof GetAnalyticsMeta500.Type> + > + /** + * Execute an analytics query with specified metrics, dimensions, filters, and time range. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "queryAnalytics": ( + options: { + readonly params?: typeof QueryAnalyticsParams.Encoded | undefined + readonly payload: typeof QueryAnalyticsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"QueryAnalytics400", typeof QueryAnalytics400.Type> + | OpenRouterClientError<"QueryAnalytics401", typeof QueryAnalytics401.Type> + | OpenRouterClientError<"QueryAnalytics403", typeof QueryAnalytics403.Type> + | OpenRouterClientError<"QueryAnalytics408", typeof QueryAnalytics408.Type> + | OpenRouterClientError<"QueryAnalytics500", typeof QueryAnalytics500.Type> + > + /** + * Synthesizes audio from the input text. Returns a raw audio bytestream in the requested format (e.g. mp3, pcm, wav). + */ + readonly "createAudioSpeech": ( + options: { + readonly params?: typeof CreateAudioSpeechParams.Encoded | undefined + readonly payload: typeof CreateAudioSpeechRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateAudioSpeech400", typeof CreateAudioSpeech400.Type> + | OpenRouterClientError<"CreateAudioSpeech401", typeof CreateAudioSpeech401.Type> + | OpenRouterClientError<"CreateAudioSpeech402", typeof CreateAudioSpeech402.Type> + | OpenRouterClientError<"CreateAudioSpeech404", typeof CreateAudioSpeech404.Type> + | OpenRouterClientError<"CreateAudioSpeech429", typeof CreateAudioSpeech429.Type> + | OpenRouterClientError<"CreateAudioSpeech500", typeof CreateAudioSpeech500.Type> + | OpenRouterClientError<"CreateAudioSpeech502", typeof CreateAudioSpeech502.Type> + | OpenRouterClientError<"CreateAudioSpeech503", typeof CreateAudioSpeech503.Type> + | OpenRouterClientError<"CreateAudioSpeech524", typeof CreateAudioSpeech524.Type> + | OpenRouterClientError<"CreateAudioSpeech529", typeof CreateAudioSpeech529.Type> + > + /** + * Transcribes audio into text. Accepts base64-encoded audio input as JSON or an OpenAI-style multipart/form-data file upload, and returns the transcribed text. + */ + readonly "createAudioTranscriptions": ( + options: { + readonly params?: typeof CreateAudioTranscriptionsParams.Encoded | undefined + readonly payload: typeof CreateAudioTranscriptionsRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateAudioTranscriptions400", typeof CreateAudioTranscriptions400.Type> + | OpenRouterClientError<"CreateAudioTranscriptions401", typeof CreateAudioTranscriptions401.Type> + | OpenRouterClientError<"CreateAudioTranscriptions402", typeof CreateAudioTranscriptions402.Type> + | OpenRouterClientError<"CreateAudioTranscriptions404", typeof CreateAudioTranscriptions404.Type> + | OpenRouterClientError<"CreateAudioTranscriptions429", typeof CreateAudioTranscriptions429.Type> + | OpenRouterClientError<"CreateAudioTranscriptions500", typeof CreateAudioTranscriptions500.Type> + | OpenRouterClientError<"CreateAudioTranscriptions502", typeof CreateAudioTranscriptions502.Type> + | OpenRouterClientError<"CreateAudioTranscriptions503", typeof CreateAudioTranscriptions503.Type> + | OpenRouterClientError<"CreateAudioTranscriptions524", typeof CreateAudioTranscriptions524.Type> + | OpenRouterClientError<"CreateAudioTranscriptions529", typeof CreateAudioTranscriptions529.Type> + > + /** + * Exchange an authorization code from the PKCE flow for a user-controlled API key + */ + readonly "exchangeAuthCodeForAPIKey": ( + options: { + readonly params?: typeof ExchangeAuthCodeForAPIKeyParams.Encoded | undefined + readonly payload: typeof ExchangeAuthCodeForAPIKeyRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey400", typeof ExchangeAuthCodeForAPIKey400.Type> + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey403", typeof ExchangeAuthCodeForAPIKey403.Type> + | OpenRouterClientError<"ExchangeAuthCodeForAPIKey500", typeof ExchangeAuthCodeForAPIKey500.Type> + > + /** + * Create an authorization code for the PKCE flow to generate a user-controlled API key + */ + readonly "createAuthKeysCode": ( + options: { + readonly params?: typeof CreateAuthKeysCodeParams.Encoded | undefined + readonly payload: typeof CreateAuthKeysCodeRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateAuthKeysCode400", typeof CreateAuthKeysCode400.Type> + | OpenRouterClientError<"CreateAuthKeysCode401", typeof CreateAuthKeysCode401.Type> + | OpenRouterClientError<"CreateAuthKeysCode403", typeof CreateAuthKeysCode403.Type> + | OpenRouterClientError<"CreateAuthKeysCode409", typeof CreateAuthKeysCode409.Type> + | OpenRouterClientError<"CreateAuthKeysCode500", typeof CreateAuthKeysCode500.Type> + > + /** + * Unified benchmark endpoint that aggregates scores from multiple benchmark sources (Artificial Analysis, Design Arena). Filter by source to reproduce the exact shapes from the legacy per-source endpoints, or use task_type to find models suited for specific workloads. Authenticate with any valid OpenRouter API key. Rate-limited to 30 requests/minute per key and 500 requests/day per account. + */ + readonly "getBenchmarks": ( + options: + | { readonly params?: typeof GetBenchmarksParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetBenchmarks400", typeof GetBenchmarks400.Type> + | OpenRouterClientError<"GetBenchmarks401", typeof GetBenchmarks401.Type> + | OpenRouterClientError<"GetBenchmarks429", typeof GetBenchmarks429.Type> + | OpenRouterClientError<"GetBenchmarks500", typeof GetBenchmarks500.Type> + > + /** + * List the bring-your-own-key (BYOK) provider credentials for the authenticated entity's default workspace. Use the `workspace_id` query parameter to scope the result to a different workspace, or the `provider` query parameter to filter by upstream provider. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listBYOKKeys": ( + options: + | { readonly params?: typeof ListBYOKKeysParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListBYOKKeys400", typeof ListBYOKKeys400.Type> + | OpenRouterClientError<"ListBYOKKeys401", typeof ListBYOKKeys401.Type> + | OpenRouterClientError<"ListBYOKKeys500", typeof ListBYOKKeys500.Type> + > + /** + * Create a new bring-your-own-key (BYOK) provider credential. The raw key is encrypted at rest and never returned in API responses. Defaults to the authenticated entity's default workspace; use the `workspace_id` body field to scope to a different workspace. Treat the raw key as write-only; it is never returned after creation. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "createBYOKKey": ( + options: { + readonly params?: typeof CreateBYOKKeyParams.Encoded | undefined + readonly payload: typeof CreateBYOKKeyRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateBYOKKey400", typeof CreateBYOKKey400.Type> + | OpenRouterClientError<"CreateBYOKKey401", typeof CreateBYOKKey401.Type> + | OpenRouterClientError<"CreateBYOKKey403", typeof CreateBYOKKey403.Type> + | OpenRouterClientError<"CreateBYOKKey500", typeof CreateBYOKKey500.Type> + > + /** + * Get a single bring-your-own-key (BYOK) provider credential by its `id`. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getBYOKKey": ( + id: string, + options: + | { readonly params?: typeof GetBYOKKeyParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetBYOKKey401", typeof GetBYOKKey401.Type> + | OpenRouterClientError<"GetBYOKKey404", typeof GetBYOKKey404.Type> + | OpenRouterClientError<"GetBYOKKey500", typeof GetBYOKKey500.Type> + > + /** + * Delete (soft-delete) a bring-your-own-key (BYOK) provider credential by its `id`. The encrypted key material is wiped and the record is marked as deleted. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteBYOKKey": ( + id: string, + options: + | { readonly params?: typeof DeleteBYOKKeyParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteBYOKKey401", typeof DeleteBYOKKey401.Type> + | OpenRouterClientError<"DeleteBYOKKey404", typeof DeleteBYOKKey404.Type> + | OpenRouterClientError<"DeleteBYOKKey500", typeof DeleteBYOKKey500.Type> + > + /** + * Update an existing bring-your-own-key (BYOK) provider credential by its `id`. Include the `key` field to rotate the raw provider API key in-place (the previous key material is overwritten). [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "updateBYOKKey": ( + id: string, + options: { + readonly params?: typeof UpdateBYOKKeyParams.Encoded | undefined + readonly payload: typeof UpdateBYOKKeyRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UpdateBYOKKey400", typeof UpdateBYOKKey400.Type> + | OpenRouterClientError<"UpdateBYOKKey401", typeof UpdateBYOKKey401.Type> + | OpenRouterClientError<"UpdateBYOKKey404", typeof UpdateBYOKKey404.Type> + | OpenRouterClientError<"UpdateBYOKKey500", typeof UpdateBYOKKey500.Type> + > + /** + * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + */ + readonly "sendChatCompletionRequest": ( + options: { + readonly params?: typeof SendChatCompletionRequestParams.Encoded | undefined + readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"SendChatCompletionRequest400", typeof SendChatCompletionRequest400.Type> + | OpenRouterClientError<"SendChatCompletionRequest401", typeof SendChatCompletionRequest401.Type> + | OpenRouterClientError<"SendChatCompletionRequest402", typeof SendChatCompletionRequest402.Type> + | OpenRouterClientError<"SendChatCompletionRequest403", typeof SendChatCompletionRequest403.Type> + | OpenRouterClientError<"SendChatCompletionRequest404", typeof SendChatCompletionRequest404.Type> + | OpenRouterClientError<"SendChatCompletionRequest408", typeof SendChatCompletionRequest408.Type> + | OpenRouterClientError<"SendChatCompletionRequest413", typeof SendChatCompletionRequest413.Type> + | OpenRouterClientError<"SendChatCompletionRequest422", typeof SendChatCompletionRequest422.Type> + | OpenRouterClientError<"SendChatCompletionRequest429", typeof SendChatCompletionRequest429.Type> + | OpenRouterClientError<"SendChatCompletionRequest500", typeof SendChatCompletionRequest500.Type> + | OpenRouterClientError<"SendChatCompletionRequest502", typeof SendChatCompletionRequest502.Type> + | OpenRouterClientError<"SendChatCompletionRequest503", typeof SendChatCompletionRequest503.Type> + | OpenRouterClientError<"SendChatCompletionRequest524", typeof SendChatCompletionRequest524.Type> + | OpenRouterClientError<"SendChatCompletionRequest529", typeof SendChatCompletionRequest529.Type> + > + /** + * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + */ + readonly "sendChatCompletionRequestSse": ( + options: { + readonly params?: typeof SendChatCompletionRequestParams.Encoded | undefined + readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded + } + ) => Stream.Stream< + { + readonly event: string + readonly id: string | undefined + readonly data: typeof SendChatCompletionRequest200Sse.Type + }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + typeof SendChatCompletionRequest200Sse.DecodingServices + > + /** + * Returns the market-share breakdown of OpenRouter traffic by task classification + * (e.g. code generation, web search, summarization) over a trailing time window. + * + * Each classification reports its share of classified sampled requests (`usage_share`) + * and classified sampled token volume (`token_share`) as fractions between 0 and 1. + * The unclassified `other` bucket is excluded. Absolute volumes are not exposed + * because the underlying data is sampled. + * + * Each classification also includes a `models` array listing the top models by + * request volume within that classification, with their within-tag usage and token shares. + * + * Classifications are grouped into macro-categories (Code, Data, Agent, General) + * with aggregate shares provided for each. + * + * Authenticate with any valid OpenRouter API key (same key used for inference). + * Rate-limited to 30 requests/minute per key and 500 requests/day per account. + * + * When republishing or quoting this data, cite as: + * "Source: OpenRouter (openrouter.ai/rankings), as of {as_of}." + */ + readonly "getTaskClassifications": ( + options: { + readonly params?: typeof GetTaskClassificationsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetTaskClassifications400", typeof GetTaskClassifications400.Type> + | OpenRouterClientError<"GetTaskClassifications401", typeof GetTaskClassifications401.Type> + | OpenRouterClientError<"GetTaskClassifications429", typeof GetTaskClassifications429.Type> + | OpenRouterClientError<"GetTaskClassifications500", typeof GetTaskClassifications500.Type> + > + /** + * Get total credits purchased and used for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getCredits": ( + options: + | { readonly params?: typeof GetCreditsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetCredits401", typeof GetCredits401.Type> + | OpenRouterClientError<"GetCredits403", typeof GetCredits403.Type> + | OpenRouterClientError<"GetCredits500", typeof GetCredits500.Type> + > + /** + * Deprecated. The Coinbase APIs used by this endpoint have been deprecated, so Coinbase Commerce charges have been removed. Use the web credits purchase flow instead. + */ + readonly "createCoinbaseCharge": ( + options: { + readonly params?: typeof CreateCoinbaseChargeParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateCoinbaseCharge410", typeof CreateCoinbaseCharge410.Type> + > + /** + * Returns the top public apps on OpenRouter ranked by token usage inside the requested + * date window, matching the public apps marketplace on openrouter.ai/apps. Token totals + * are `prompt_tokens + completion_tokens`; hidden and private apps are excluded and + * traffic from related app aliases is merged into the canonical visible app. + * + * `sort=popular` (default) ranks by total token volume inside the window. + * `sort=trending` ranks by absolute excess token growth: window volume minus the average + * volume of the three equal-length periods immediately preceding the window. Apps with + * no excess growth are omitted, so `trending` may return fewer than `limit` rows. + * + * Filter with `category` (marketplace category group, e.g. `coding`) or `subcategory` + * (e.g. `cli-agent`). Ranks are re-numbered 1..N after filtering. Page with `offset` — + * `rank` stays absolute, so the first row of `offset=50` is `rank: 51`. + * + * Authenticate with any valid OpenRouter API key (same key used for inference). + * Rate-limited to 30 requests/minute per key and 500 requests/day per account. + * + * When republishing or quoting this dataset, OpenRouter must be cited as: + * "Source: OpenRouter (openrouter.ai/apps), as of {as_of}." + * + * Token counts come from each upstream provider's own tokenizer, so a token attributed + * to one app is not directly comparable to a token attributed to another app whose + * traffic flows through a different provider. + */ + readonly "getAppRankings": ( + options: + | { readonly params?: typeof GetAppRankingsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetAppRankings400", typeof GetAppRankings400.Type> + | OpenRouterClientError<"GetAppRankings401", typeof GetAppRankings401.Type> + | OpenRouterClientError<"GetAppRankings429", typeof GetAppRankings429.Type> + | OpenRouterClientError<"GetAppRankings500", typeof GetAppRankings500.Type> + > + /** + * Returns the top 50 public models per day by total token usage on OpenRouter, plus a + * single aggregated `other` row per day that sums every model outside that top 50. + * Token totals are `prompt_tokens + completion_tokens`, matching the public rankings + * chart on openrouter.ai/rankings. + * + * Each row is a distinct `(date, model_permaslug)` pair. The `other` row uses the + * reserved permaslug `other` and is always returned last within its date, so callers + * can compute `top-50 traffic / total daily traffic` without a second request. + * + * Optional filters slice the dataset. `period` (`day`/`week`/`month`) sets the time + * grain. `modality` and `context_bucket` narrow the exact dataset by output/input + * modality (or tool-calling activity) and request context length. `category` and + * `language_type` instead read a sampled, upsampled dataset whose `total_tokens` are + * weekly-grain estimates — they cannot be combined with each other or with the exact + * filters, and reject `period=day` with a 400. + * + * Authenticate with any valid OpenRouter API key (same key used for inference). + * Rate-limited to 30 requests/minute per key and 500 requests/day per account. + * + * When republishing or quoting this dataset, OpenRouter must be cited as: + * "Source: OpenRouter (openrouter.ai/rankings), as of {as_of}." + * + * Token counts come from each upstream provider's own tokenizer (Anthropic counts + * are as reported by Anthropic, OpenAI counts are as reported by OpenAI, etc.), so + * a token in one row is not directly comparable to a token in another row from a + * different provider. + */ + readonly "getRankingsDaily": ( + options: { + readonly params?: typeof GetRankingsDailyParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetRankingsDaily400", typeof GetRankingsDaily400.Type> + | OpenRouterClientError<"GetRankingsDaily401", typeof GetRankingsDaily401.Type> + | OpenRouterClientError<"GetRankingsDaily429", typeof GetRankingsDaily429.Type> + | OpenRouterClientError<"GetRankingsDaily500", typeof GetRankingsDaily500.Type> + > + /** + * Submits an embedding request to the embeddings router + */ + readonly "createEmbeddings": ( + options: { + readonly params?: typeof CreateEmbeddingsParams.Encoded | undefined + readonly payload: typeof CreateEmbeddingsRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateEmbeddings400", typeof CreateEmbeddings400.Type> + | OpenRouterClientError<"CreateEmbeddings401", typeof CreateEmbeddings401.Type> + | OpenRouterClientError<"CreateEmbeddings402", typeof CreateEmbeddings402.Type> + | OpenRouterClientError<"CreateEmbeddings404", typeof CreateEmbeddings404.Type> + | OpenRouterClientError<"CreateEmbeddings429", typeof CreateEmbeddings429.Type> + | OpenRouterClientError<"CreateEmbeddings500", typeof CreateEmbeddings500.Type> + | OpenRouterClientError<"CreateEmbeddings502", typeof CreateEmbeddings502.Type> + | OpenRouterClientError<"CreateEmbeddings503", typeof CreateEmbeddings503.Type> + | OpenRouterClientError<"CreateEmbeddings524", typeof CreateEmbeddings524.Type> + | OpenRouterClientError<"CreateEmbeddings529", typeof CreateEmbeddings529.Type> + > + /** + * Submits an embedding request to the embeddings router + */ + readonly "createEmbeddingsSse": ( + options: { + readonly params?: typeof CreateEmbeddingsParams.Encoded | undefined + readonly payload: typeof CreateEmbeddingsRequestJson.Encoded + } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateEmbeddings200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + typeof CreateEmbeddings200Sse.DecodingServices + > + /** + * Returns a list of all available embeddings models and their properties + */ + readonly "listEmbeddingsModels": ( + options: { + readonly params?: typeof ListEmbeddingsModelsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListEmbeddingsModels400", typeof ListEmbeddingsModels400.Type> + | OpenRouterClientError<"ListEmbeddingsModels500", typeof ListEmbeddingsModels500.Type> + > + /** + * Preview the impact of ZDR on the available endpoints + */ + readonly "listEndpointsZdr": ( + options: { + readonly params?: typeof ListEndpointsZdrParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListEndpointsZdr403", typeof ListEndpointsZdr403.Type> + | OpenRouterClientError<"ListEndpointsZdr500", typeof ListEndpointsZdr500.Type> + > + /** + * Lists files belonging to the workspace of the authenticating API key. + */ + readonly "listFiles": ( + options: + | { readonly params?: typeof ListFilesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListFiles400", typeof ListFiles400.Type> + | OpenRouterClientError<"ListFiles401", typeof ListFiles401.Type> + | OpenRouterClientError<"ListFiles429", typeof ListFiles429.Type> + | OpenRouterClientError<"ListFiles500", typeof ListFiles500.Type> + > + /** + * Uploads a file to be referenced in future API calls. The file is stored under the workspace of the authenticating API key. Maximum file size: 100 MB. + */ + readonly "uploadFile": ( + options: { + readonly params?: typeof UploadFileParams.Encoded | undefined + readonly payload: typeof UploadFileRequestFormData.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UploadFile400", typeof UploadFile400.Type> + | OpenRouterClientError<"UploadFile401", typeof UploadFile401.Type> + | OpenRouterClientError<"UploadFile403", typeof UploadFile403.Type> + | OpenRouterClientError<"UploadFile413", typeof UploadFile413.Type> + | OpenRouterClientError<"UploadFile429", typeof UploadFile429.Type> + | OpenRouterClientError<"UploadFile500", typeof UploadFile500.Type> + > + /** + * Retrieves metadata for a single file owned by the requesting workspace. + */ + readonly "getFileMetadata": ( + fileId: string, + options: { + readonly params?: typeof GetFileMetadataParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetFileMetadata401", typeof GetFileMetadata401.Type> + | OpenRouterClientError<"GetFileMetadata404", typeof GetFileMetadata404.Type> + | OpenRouterClientError<"GetFileMetadata429", typeof GetFileMetadata429.Type> + | OpenRouterClientError<"GetFileMetadata500", typeof GetFileMetadata500.Type> + > + /** + * Deletes a file owned by the requesting workspace. Deletion is irreversible. + */ + readonly "deleteFile": ( + fileId: string, + options: + | { readonly params?: typeof DeleteFileParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteFile401", typeof DeleteFile401.Type> + | OpenRouterClientError<"DeleteFile404", typeof DeleteFile404.Type> + | OpenRouterClientError<"DeleteFile429", typeof DeleteFile429.Type> + | OpenRouterClientError<"DeleteFile500", typeof DeleteFile500.Type> + > + /** + * Downloads the raw bytes of a file. Only files created server-side are downloadable; uploaded files return 400. + */ + readonly "downloadFileContent": ( + fileId: string, + options: { + readonly params?: typeof DownloadFileContentParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DownloadFileContent400", typeof DownloadFileContent400.Type> + | OpenRouterClientError<"DownloadFileContent401", typeof DownloadFileContent401.Type> + | OpenRouterClientError<"DownloadFileContent404", typeof DownloadFileContent404.Type> + | OpenRouterClientError<"DownloadFileContent429", typeof DownloadFileContent429.Type> + | OpenRouterClientError<"DownloadFileContent500", typeof DownloadFileContent500.Type> + > + /** + * Downloads the raw bytes of a file. Only files created server-side are downloadable; uploaded files return 400. + */ + readonly "downloadFileContentStream": ( + fileId: string, + options: { readonly params?: typeof DownloadFileContentParams.Encoded | undefined } | undefined + ) => Stream.Stream + /** + * Get request & usage metadata for a generation + */ + readonly "getGeneration": ( + options: { readonly params: typeof GetGenerationParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetGeneration401", typeof GetGeneration401.Type> + | OpenRouterClientError<"GetGeneration402", typeof GetGeneration402.Type> + | OpenRouterClientError<"GetGeneration404", typeof GetGeneration404.Type> + | OpenRouterClientError<"GetGeneration429", typeof GetGeneration429.Type> + | OpenRouterClientError<"GetGeneration500", typeof GetGeneration500.Type> + | OpenRouterClientError<"GetGeneration502", typeof GetGeneration502.Type> + | OpenRouterClientError<"GetGeneration524", typeof GetGeneration524.Type> + | OpenRouterClientError<"GetGeneration529", typeof GetGeneration529.Type> + > + /** + * Get stored prompt and completion content for a generation + */ + readonly "listGenerationContent": ( + options: { readonly params: typeof ListGenerationContentParams.Encoded; readonly config?: Config | undefined } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGenerationContent401", typeof ListGenerationContent401.Type> + | OpenRouterClientError<"ListGenerationContent403", typeof ListGenerationContent403.Type> + | OpenRouterClientError<"ListGenerationContent404", typeof ListGenerationContent404.Type> + | OpenRouterClientError<"ListGenerationContent429", typeof ListGenerationContent429.Type> + | OpenRouterClientError<"ListGenerationContent500", typeof ListGenerationContent500.Type> + | OpenRouterClientError<"ListGenerationContent502", typeof ListGenerationContent502.Type> + | OpenRouterClientError<"ListGenerationContent524", typeof ListGenerationContent524.Type> + | OpenRouterClientError<"ListGenerationContent529", typeof ListGenerationContent529.Type> + > + /** + * Submit structured feedback on a generation the authenticated user made. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "submitGenerationFeedback": ( + options: { + readonly params?: typeof SubmitGenerationFeedbackParams.Encoded | undefined + readonly payload: typeof SubmitGenerationFeedbackRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"SubmitGenerationFeedback400", typeof SubmitGenerationFeedback400.Type> + | OpenRouterClientError<"SubmitGenerationFeedback401", typeof SubmitGenerationFeedback401.Type> + | OpenRouterClientError<"SubmitGenerationFeedback404", typeof SubmitGenerationFeedback404.Type> + | OpenRouterClientError<"SubmitGenerationFeedback429", typeof SubmitGenerationFeedback429.Type> + | OpenRouterClientError<"SubmitGenerationFeedback500", typeof SubmitGenerationFeedback500.Type> + > + /** + * List all guardrails for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrails": ( + options: + | { readonly params?: typeof ListGuardrailsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrails400", typeof ListGuardrails400.Type> + | OpenRouterClientError<"ListGuardrails401", typeof ListGuardrails401.Type> + | OpenRouterClientError<"ListGuardrails500", typeof ListGuardrails500.Type> + > + /** + * Create a new guardrail for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "createGuardrail": ( + options: { + readonly params?: typeof CreateGuardrailParams.Encoded | undefined + readonly payload: typeof CreateGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateGuardrail400", typeof CreateGuardrail400.Type> + | OpenRouterClientError<"CreateGuardrail401", typeof CreateGuardrail401.Type> + | OpenRouterClientError<"CreateGuardrail403", typeof CreateGuardrail403.Type> + | OpenRouterClientError<"CreateGuardrail500", typeof CreateGuardrail500.Type> + > + /** + * Get a single guardrail by ID. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getGuardrail": ( + id: string, + options: + | { readonly params?: typeof GetGuardrailParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetGuardrail401", typeof GetGuardrail401.Type> + | OpenRouterClientError<"GetGuardrail404", typeof GetGuardrail404.Type> + | OpenRouterClientError<"GetGuardrail500", typeof GetGuardrail500.Type> + > + /** + * Delete an existing guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteGuardrail": ( + id: string, + options: { + readonly params?: typeof DeleteGuardrailParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteGuardrail401", typeof DeleteGuardrail401.Type> + | OpenRouterClientError<"DeleteGuardrail404", typeof DeleteGuardrail404.Type> + | OpenRouterClientError<"DeleteGuardrail500", typeof DeleteGuardrail500.Type> + > + /** + * Update an existing guardrail. Collection fields use replace semantics: send the full desired set on every update. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "updateGuardrail": ( + id: string, + options: { + readonly params?: typeof UpdateGuardrailParams.Encoded | undefined + readonly payload: typeof UpdateGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UpdateGuardrail400", typeof UpdateGuardrail400.Type> + | OpenRouterClientError<"UpdateGuardrail401", typeof UpdateGuardrail401.Type> + | OpenRouterClientError<"UpdateGuardrail404", typeof UpdateGuardrail404.Type> + | OpenRouterClientError<"UpdateGuardrail500", typeof UpdateGuardrail500.Type> + > + /** + * List all API key assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrailKeyAssignments": ( + id: string, + options: { + readonly params?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrailKeyAssignments401", typeof ListGuardrailKeyAssignments401.Type> + | OpenRouterClientError<"ListGuardrailKeyAssignments404", typeof ListGuardrailKeyAssignments404.Type> + | OpenRouterClientError<"ListGuardrailKeyAssignments500", typeof ListGuardrailKeyAssignments500.Type> + > + /** + * Assign multiple API keys to a specific guardrail. A key may hold at most one guardrail; assigning replaces any existing assignment. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkAssignKeysToGuardrail": ( + id: string, + options: { + readonly params?: typeof BulkAssignKeysToGuardrailParams.Encoded | undefined + readonly payload: typeof BulkAssignKeysToGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkAssignKeysToGuardrail400", typeof BulkAssignKeysToGuardrail400.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail401", typeof BulkAssignKeysToGuardrail401.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail404", typeof BulkAssignKeysToGuardrail404.Type> + | OpenRouterClientError<"BulkAssignKeysToGuardrail500", typeof BulkAssignKeysToGuardrail500.Type> + > + /** + * Unassign multiple API keys from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkUnassignKeysFromGuardrail": ( + id: string, + options: { + readonly params?: typeof BulkUnassignKeysFromGuardrailParams.Encoded | undefined + readonly payload: typeof BulkUnassignKeysFromGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail400", typeof BulkUnassignKeysFromGuardrail400.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail401", typeof BulkUnassignKeysFromGuardrail401.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail404", typeof BulkUnassignKeysFromGuardrail404.Type> + | OpenRouterClientError<"BulkUnassignKeysFromGuardrail500", typeof BulkUnassignKeysFromGuardrail500.Type> + > + /** + * List all organization member assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listGuardrailMemberAssignments": ( + id: string, + options: { + readonly params?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListGuardrailMemberAssignments401", typeof ListGuardrailMemberAssignments401.Type> + | OpenRouterClientError<"ListGuardrailMemberAssignments404", typeof ListGuardrailMemberAssignments404.Type> + | OpenRouterClientError<"ListGuardrailMemberAssignments500", typeof ListGuardrailMemberAssignments500.Type> + > + /** + * Assign multiple organization members to a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkAssignMembersToGuardrail": ( + id: string, + options: { + readonly params?: typeof BulkAssignMembersToGuardrailParams.Encoded | undefined + readonly payload: typeof BulkAssignMembersToGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkAssignMembersToGuardrail400", typeof BulkAssignMembersToGuardrail400.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail401", typeof BulkAssignMembersToGuardrail401.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail404", typeof BulkAssignMembersToGuardrail404.Type> + | OpenRouterClientError<"BulkAssignMembersToGuardrail500", typeof BulkAssignMembersToGuardrail500.Type> + > + /** + * Unassign multiple organization members from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "bulkUnassignMembersFromGuardrail": ( + id: string, + options: { + readonly params?: typeof BulkUnassignMembersFromGuardrailParams.Encoded | undefined + readonly payload: typeof BulkUnassignMembersFromGuardrailRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail400", typeof BulkUnassignMembersFromGuardrail400.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail401", typeof BulkUnassignMembersFromGuardrail401.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail404", typeof BulkUnassignMembersFromGuardrail404.Type> + | OpenRouterClientError<"BulkUnassignMembersFromGuardrail500", typeof BulkUnassignMembersFromGuardrail500.Type> + > + /** + * List all API key guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listKeyAssignments": ( + options: { + readonly params?: typeof ListKeyAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListKeyAssignments401", typeof ListKeyAssignments401.Type> + | OpenRouterClientError<"ListKeyAssignments500", typeof ListKeyAssignments500.Type> + > + /** + * List all organization member guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "listMemberAssignments": ( + options: { + readonly params?: typeof ListMemberAssignmentsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListMemberAssignments401", typeof ListMemberAssignments401.Type> + | OpenRouterClientError<"ListMemberAssignments500", typeof ListMemberAssignments500.Type> + > + /** + * Generates an image from a text prompt via the image generation router + */ + readonly "createImages": ( + options: { + readonly params?: typeof CreateImagesParams.Encoded | undefined + readonly payload: typeof CreateImagesRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateImages400", typeof CreateImages400.Type> + | OpenRouterClientError<"CreateImages401", typeof CreateImages401.Type> + | OpenRouterClientError<"CreateImages402", typeof CreateImages402.Type> + | OpenRouterClientError<"CreateImages403", typeof CreateImages403.Type> + | OpenRouterClientError<"CreateImages404", typeof CreateImages404.Type> + | OpenRouterClientError<"CreateImages413", typeof CreateImages413.Type> + | OpenRouterClientError<"CreateImages429", typeof CreateImages429.Type> + | OpenRouterClientError<"CreateImages500", typeof CreateImages500.Type> + | OpenRouterClientError<"CreateImages502", typeof CreateImages502.Type> + | OpenRouterClientError<"CreateImages524", typeof CreateImages524.Type> + | OpenRouterClientError<"CreateImages529", typeof CreateImages529.Type> + > + /** + * Generates an image from a text prompt via the image generation router + */ + readonly "createImagesSse": ( + options: { + readonly params?: typeof CreateImagesParams.Encoded | undefined + readonly payload: typeof CreateImagesRequestJson.Encoded + } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateImages200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + typeof CreateImages200Sse.DecodingServices + > + /** + * Lists every image generation model with its top-level supported-parameter superset and a URL to its full per-endpoint records. + */ + readonly "listImageModels": ( + options: { + readonly params?: typeof ListImageModelsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListImageModels500", typeof ListImageModels500.Type> + > + /** + * Returns the full per-endpoint records for an image model: each endpoint's definitive supported parameters, pricing, and passthrough allowlist. + */ + readonly "listImageModelEndpoints": ( + author: string, + slug: string, + options: { + readonly params?: typeof ListImageModelEndpointsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListImageModelEndpoints404", typeof ListImageModelEndpoints404.Type> + | OpenRouterClientError<"ListImageModelEndpoints500", typeof ListImageModelEndpoints500.Type> + > + /** + * Get information on the API key associated with the current authentication session + */ + readonly "getCurrentKey": ( + options: + | { readonly params?: typeof GetCurrentKeyParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetCurrentKey401", typeof GetCurrentKey401.Type> + | OpenRouterClientError<"GetCurrentKey500", typeof GetCurrentKey500.Type> + > /** - * Creates a streaming or non-streaming response using OpenResponses API format + * List all API keys for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "createResponses": ( - options: { readonly payload: typeof CreateResponsesRequestJson.Encoded; readonly config?: Config | undefined } + readonly "list": ( + options: + | { readonly params?: typeof ListParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateResponses400", typeof CreateResponses400.Type> - | OpenRouterClientError<"CreateResponses401", typeof CreateResponses401.Type> - | OpenRouterClientError<"CreateResponses402", typeof CreateResponses402.Type> - | OpenRouterClientError<"CreateResponses404", typeof CreateResponses404.Type> - | OpenRouterClientError<"CreateResponses408", typeof CreateResponses408.Type> - | OpenRouterClientError<"CreateResponses413", typeof CreateResponses413.Type> - | OpenRouterClientError<"CreateResponses422", typeof CreateResponses422.Type> - | OpenRouterClientError<"CreateResponses429", typeof CreateResponses429.Type> - | OpenRouterClientError<"CreateResponses500", typeof CreateResponses500.Type> - | OpenRouterClientError<"CreateResponses502", typeof CreateResponses502.Type> - | OpenRouterClientError<"CreateResponses503", typeof CreateResponses503.Type> - | OpenRouterClientError<"CreateResponses524", typeof CreateResponses524.Type> - | OpenRouterClientError<"CreateResponses529", typeof CreateResponses529.Type> + | OpenRouterClientError<"List400", typeof List400.Type> + | OpenRouterClientError<"List401", typeof List401.Type> + | OpenRouterClientError<"List429", typeof List429.Type> + | OpenRouterClientError<"List500", typeof List500.Type> > /** - * Creates a streaming or non-streaming response using OpenResponses API format + * Create a new API key for the authenticated user. The plaintext `key` is returned only in this response. Treat it as a write-only, sensitive value; it cannot be retrieved later. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "createResponsesSse": ( - options: { readonly payload: typeof CreateResponsesRequestJson.Encoded } - ) => Stream.Stream< - { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateResponses200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, - typeof CreateResponses200Sse.DecodingServices + readonly "createKeys": ( + options: { + readonly params?: typeof CreateKeysParams.Encoded | undefined + readonly payload: typeof CreateKeysRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"CreateKeys400", typeof CreateKeys400.Type> + | OpenRouterClientError<"CreateKeys401", typeof CreateKeys401.Type> + | OpenRouterClientError<"CreateKeys403", typeof CreateKeys403.Type> + | OpenRouterClientError<"CreateKeys429", typeof CreateKeys429.Type> + | OpenRouterClientError<"CreateKeys500", typeof CreateKeys500.Type> + > + /** + * Get a single API key by hash. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "getKey": ( + hash: string, + options: + | { readonly params?: typeof GetKeyParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetKey401", typeof GetKey401.Type> + | OpenRouterClientError<"GetKey404", typeof GetKey404.Type> + | OpenRouterClientError<"GetKey429", typeof GetKey429.Type> + | OpenRouterClientError<"GetKey500", typeof GetKey500.Type> + > + /** + * Delete an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteKeys": ( + hash: string, + options: + | { readonly params?: typeof DeleteKeysParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"DeleteKeys401", typeof DeleteKeys401.Type> + | OpenRouterClientError<"DeleteKeys404", typeof DeleteKeys404.Type> + | OpenRouterClientError<"DeleteKeys429", typeof DeleteKeys429.Type> + | OpenRouterClientError<"DeleteKeys500", typeof DeleteKeys500.Type> + > + /** + * Update an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "updateKeys": ( + hash: string, + options: { + readonly params?: typeof UpdateKeysParams.Encoded | undefined + readonly payload: typeof UpdateKeysRequestJson.Encoded + readonly config?: Config | undefined + } + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"UpdateKeys400", typeof UpdateKeys400.Type> + | OpenRouterClientError<"UpdateKeys401", typeof UpdateKeys401.Type> + | OpenRouterClientError<"UpdateKeys404", typeof UpdateKeys404.Type> + | OpenRouterClientError<"UpdateKeys429", typeof UpdateKeys429.Type> + | OpenRouterClientError<"UpdateKeys500", typeof UpdateKeys500.Type> > /** * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. */ readonly "createMessages": ( - options: { readonly payload: typeof CreateMessagesRequestJson.Encoded; readonly config?: Config | undefined } + options: { + readonly params?: typeof CreateMessagesParams.Encoded | undefined + readonly payload: typeof CreateMessagesRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< WithOptionalResponse, | HttpClientError.HttpClientError @@ -9462,542 +35613,693 @@ export interface OpenRouterClient { * Creates a message using the Anthropic Messages API format. Supports text, images, PDFs, tools, and extended thinking. */ readonly "createMessagesSse": ( - options: { readonly payload: typeof CreateMessagesRequestJson.Encoded } + options: { + readonly params?: typeof CreateMessagesParams.Encoded | undefined + readonly payload: typeof CreateMessagesRequestJson.Encoded + } ) => Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateMessages200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof CreateMessages200Sse.DecodingServices > /** - * Returns user activity data grouped by endpoint for the last 30 (completed) UTC days. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Returns full details for a single model identified by its author and slug (e.g. openai/gpt-4). Supports variant suffixes (e.g. openai/gpt-4:free) and resolves known slug aliases. */ - readonly "getUserActivity": ( - options: { - readonly params?: typeof GetUserActivityParams.Encoded | undefined - readonly config?: Config | undefined - } | undefined + readonly "getModel": ( + author: string, + slug: string, + options: + | { readonly params?: typeof GetModelParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetUserActivity400", typeof GetUserActivity400.Type> - | OpenRouterClientError<"GetUserActivity401", typeof GetUserActivity401.Type> - | OpenRouterClientError<"GetUserActivity403", typeof GetUserActivity403.Type> - | OpenRouterClientError<"GetUserActivity500", typeof GetUserActivity500.Type> + | OpenRouterClientError<"GetModel403", typeof GetModel403.Type> + | OpenRouterClientError<"GetModel404", typeof GetModel404.Type> + | OpenRouterClientError<"GetModel500", typeof GetModel500.Type> > /** - * Get total credits purchased and used for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * List all models and their properties */ - readonly "getCredits": ( - options: { readonly config?: Config | undefined } | undefined + readonly "getModels": ( + options: + | { readonly params?: typeof GetModelsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetCredits401", typeof GetCredits401.Type> - | OpenRouterClientError<"GetCredits403", typeof GetCredits403.Type> - | OpenRouterClientError<"GetCredits500", typeof GetCredits500.Type> + | OpenRouterClientError<"GetModels400", typeof GetModels400.Type> + | OpenRouterClientError<"GetModels403", typeof GetModels403.Type> + | OpenRouterClientError<"GetModels500", typeof GetModels500.Type> > /** - * Create a Coinbase charge for crypto payment + * List all endpoints for a model */ - readonly "createCoinbaseCharge": ( - options: { readonly payload: typeof CreateCoinbaseChargeRequestJson.Encoded; readonly config?: Config | undefined } + readonly "listEndpoints": ( + author: string, + slug: string, + options: + | { readonly params?: typeof ListEndpointsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateCoinbaseCharge400", typeof CreateCoinbaseCharge400.Type> - | OpenRouterClientError<"CreateCoinbaseCharge401", typeof CreateCoinbaseCharge401.Type> - | OpenRouterClientError<"CreateCoinbaseCharge429", typeof CreateCoinbaseCharge429.Type> - | OpenRouterClientError<"CreateCoinbaseCharge500", typeof CreateCoinbaseCharge500.Type> + | OpenRouterClientError<"ListEndpoints403", typeof ListEndpoints403.Type> + | OpenRouterClientError<"ListEndpoints404", typeof ListEndpoints404.Type> + | OpenRouterClientError<"ListEndpoints500", typeof ListEndpoints500.Type> > /** - * Submits an embedding request to the embeddings router + * Get total count of available models */ - readonly "createEmbeddings": ( - options: { readonly payload: typeof CreateEmbeddingsRequestJson.Encoded; readonly config?: Config | undefined } + readonly "listModelsCount": ( + options: { + readonly params?: typeof ListModelsCountParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateEmbeddings400", typeof CreateEmbeddings400.Type> - | OpenRouterClientError<"CreateEmbeddings401", typeof CreateEmbeddings401.Type> - | OpenRouterClientError<"CreateEmbeddings402", typeof CreateEmbeddings402.Type> - | OpenRouterClientError<"CreateEmbeddings404", typeof CreateEmbeddings404.Type> - | OpenRouterClientError<"CreateEmbeddings429", typeof CreateEmbeddings429.Type> - | OpenRouterClientError<"CreateEmbeddings500", typeof CreateEmbeddings500.Type> - | OpenRouterClientError<"CreateEmbeddings502", typeof CreateEmbeddings502.Type> - | OpenRouterClientError<"CreateEmbeddings503", typeof CreateEmbeddings503.Type> - | OpenRouterClientError<"CreateEmbeddings524", typeof CreateEmbeddings524.Type> - | OpenRouterClientError<"CreateEmbeddings529", typeof CreateEmbeddings529.Type> + | OpenRouterClientError<"ListModelsCount400", typeof ListModelsCount400.Type> + | OpenRouterClientError<"ListModelsCount403", typeof ListModelsCount403.Type> + | OpenRouterClientError<"ListModelsCount500", typeof ListModelsCount500.Type> > /** - * Submits an embedding request to the embeddings router + * List models filtered by user provider preferences, [privacy settings](https://openrouter.ai/docs/guides/privacy/provider-logging), and [guardrails](https://openrouter.ai/docs/guides/features/guardrails). If requesting through `eu.openrouter.ai/api/v1/...` the results will be filtered to models that satisfy [EU in-region routing](https://openrouter.ai/docs/guides/privacy/provider-logging#enterprise-eu-in-region-routing). */ - readonly "createEmbeddingsSse": ( - options: { readonly payload: typeof CreateEmbeddingsRequestJson.Encoded } - ) => Stream.Stream< - { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateEmbeddings200Sse.Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, - typeof CreateEmbeddings200Sse.DecodingServices + readonly "listModelsUser": ( + options: + | { readonly params?: typeof ListModelsUserParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"ListModelsUser401", typeof ListModelsUser401.Type> + | OpenRouterClientError<"ListModelsUser403", typeof ListModelsUser403.Type> + | OpenRouterClientError<"ListModelsUser404", typeof ListModelsUser404.Type> + | OpenRouterClientError<"ListModelsUser500", typeof ListModelsUser500.Type> > /** - * Returns a list of all available embeddings models and their properties + * List the observability destinations configured for the authenticated entity's default workspace. Use the `workspace_id` query parameter to scope the result to a different workspace. Only destinations with stable release status are surfaced — destinations of other types are excluded. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listEmbeddingsModels": ( - options: { readonly config?: Config | undefined } | undefined + readonly "listObservabilityDestinations": ( + options: { + readonly params?: typeof ListObservabilityDestinationsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListEmbeddingsModels400", typeof ListEmbeddingsModels400.Type> - | OpenRouterClientError<"ListEmbeddingsModels500", typeof ListEmbeddingsModels500.Type> + | OpenRouterClientError<"ListObservabilityDestinations400", typeof ListObservabilityDestinations400.Type> + | OpenRouterClientError<"ListObservabilityDestinations401", typeof ListObservabilityDestinations401.Type> + | OpenRouterClientError<"ListObservabilityDestinations500", typeof ListObservabilityDestinations500.Type> > /** - * Get request & usage metadata for a generation + * Create a new observability destination. A maximum of 5 destinations per type is allowed. Defaults to the authenticated entity's default workspace; use the `workspace_id` body field to scope to a different workspace. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "getGeneration": ( - options: { readonly params: typeof GetGenerationParams.Encoded; readonly config?: Config | undefined } + readonly "createObservabilityDestination": ( + options: { + readonly params?: typeof CreateObservabilityDestinationParams.Encoded | undefined + readonly payload: typeof CreateObservabilityDestinationRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetGeneration401", typeof GetGeneration401.Type> - | OpenRouterClientError<"GetGeneration402", typeof GetGeneration402.Type> - | OpenRouterClientError<"GetGeneration404", typeof GetGeneration404.Type> - | OpenRouterClientError<"GetGeneration429", typeof GetGeneration429.Type> - | OpenRouterClientError<"GetGeneration500", typeof GetGeneration500.Type> - | OpenRouterClientError<"GetGeneration502", typeof GetGeneration502.Type> - | OpenRouterClientError<"GetGeneration524", typeof GetGeneration524.Type> - | OpenRouterClientError<"GetGeneration529", typeof GetGeneration529.Type> + | OpenRouterClientError<"CreateObservabilityDestination400", typeof CreateObservabilityDestination400.Type> + | OpenRouterClientError<"CreateObservabilityDestination401", typeof CreateObservabilityDestination401.Type> + | OpenRouterClientError<"CreateObservabilityDestination403", typeof CreateObservabilityDestination403.Type> + | OpenRouterClientError<"CreateObservabilityDestination409", typeof CreateObservabilityDestination409.Type> + | OpenRouterClientError<"CreateObservabilityDestination500", typeof CreateObservabilityDestination500.Type> > /** - * Get total count of available models + * Fetch a single observability destination by its UUID. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listModelsCount": ( - options: { readonly config?: Config | undefined } | undefined + readonly "getObservabilityDestination": ( + id: string, + options: { + readonly params?: typeof GetObservabilityDestinationParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListModelsCount500", typeof ListModelsCount500.Type> + | OpenRouterClientError<"GetObservabilityDestination401", typeof GetObservabilityDestination401.Type> + | OpenRouterClientError<"GetObservabilityDestination404", typeof GetObservabilityDestination404.Type> + | OpenRouterClientError<"GetObservabilityDestination500", typeof GetObservabilityDestination500.Type> > /** - * List all models and their properties + * Delete an existing observability destination. This performs a soft delete. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "getModels": ( - options: - | { readonly params?: typeof GetModelsParams.Encoded | undefined; readonly config?: Config | undefined } - | undefined + readonly "deleteObservabilityDestination": ( + id: string, + options: { + readonly params?: typeof DeleteObservabilityDestinationParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetModels400", typeof GetModels400.Type> - | OpenRouterClientError<"GetModels500", typeof GetModels500.Type> + | OpenRouterClientError<"DeleteObservabilityDestination401", typeof DeleteObservabilityDestination401.Type> + | OpenRouterClientError<"DeleteObservabilityDestination404", typeof DeleteObservabilityDestination404.Type> + | OpenRouterClientError<"DeleteObservabilityDestination500", typeof DeleteObservabilityDestination500.Type> > /** - * List models filtered by user provider preferences, [privacy settings](https://openrouter.ai/docs/guides/privacy/logging), and [guardrails](https://openrouter.ai/docs/guides/features/guardrails). If requesting through `eu.openrouter.ai/api/v1/...` the results will be filtered to models that satisfy [EU in-region routing](https://openrouter.ai/docs/guides/privacy/logging#enterprise-eu-in-region-routing). + * Update an existing observability destination. Only the fields provided in the request body are updated. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listModelsUser": ( - options: { readonly config?: Config | undefined } | undefined + readonly "updateObservabilityDestination": ( + id: string, + options: { + readonly params?: typeof UpdateObservabilityDestinationParams.Encoded | undefined + readonly payload: typeof UpdateObservabilityDestinationRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListModelsUser401", typeof ListModelsUser401.Type> - | OpenRouterClientError<"ListModelsUser404", typeof ListModelsUser404.Type> - | OpenRouterClientError<"ListModelsUser500", typeof ListModelsUser500.Type> + | OpenRouterClientError<"UpdateObservabilityDestination400", typeof UpdateObservabilityDestination400.Type> + | OpenRouterClientError<"UpdateObservabilityDestination401", typeof UpdateObservabilityDestination401.Type> + | OpenRouterClientError<"UpdateObservabilityDestination404", typeof UpdateObservabilityDestination404.Type> + | OpenRouterClientError<"UpdateObservabilityDestination409", typeof UpdateObservabilityDestination409.Type> + | OpenRouterClientError<"UpdateObservabilityDestination500", typeof UpdateObservabilityDestination500.Type> > /** - * List all endpoints for a model + * List all members of the organization associated with the authenticated management key. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listEndpoints": ( - author: string, - slug: string, - options: { readonly config?: Config | undefined } | undefined + readonly "listOrganizationMembers": ( + options: { + readonly params?: typeof ListOrganizationMembersParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListEndpoints404", typeof ListEndpoints404.Type> - | OpenRouterClientError<"ListEndpoints500", typeof ListEndpoints500.Type> + | OpenRouterClientError<"ListOrganizationMembers401", typeof ListOrganizationMembers401.Type> + | OpenRouterClientError<"ListOrganizationMembers404", typeof ListOrganizationMembers404.Type> + | OpenRouterClientError<"ListOrganizationMembers500", typeof ListOrganizationMembers500.Type> > /** - * Preview the impact of ZDR on the available endpoints + * Lists all presets for the authenticated user, ordered by most recently updated first. */ - readonly "listEndpointsZdr": ( - options: { readonly config?: Config | undefined } | undefined + readonly "listPresets": ( + options: + | { readonly params?: typeof ListPresetsParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListEndpointsZdr500", typeof ListEndpointsZdr500.Type> + | OpenRouterClientError<"ListPresets400", typeof ListPresets400.Type> + | OpenRouterClientError<"ListPresets401", typeof ListPresets401.Type> + | OpenRouterClientError<"ListPresets500", typeof ListPresets500.Type> > /** - * List all providers + * Retrieves a preset by its slug with its currently designated version inline. */ - readonly "listProviders": ( - options: { readonly config?: Config | undefined } | undefined + readonly "getPreset": ( + slug: string, + options: + | { readonly params?: typeof GetPresetParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListProviders500", typeof ListProviders500.Type> + | OpenRouterClientError<"GetPreset400", typeof GetPreset400.Type> + | OpenRouterClientError<"GetPreset401", typeof GetPreset401.Type> + | OpenRouterClientError<"GetPreset404", typeof GetPreset404.Type> + | OpenRouterClientError<"GetPreset500", typeof GetPreset500.Type> > /** - * List all API keys for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Creates a preset (or a new version of an existing one) from an inference request body. Only fields that overlap with the preset config are persisted; other fields (e.g. `messages`, `stream`, `prompt`) are silently ignored. */ - readonly "list": ( - options: - | { readonly params?: typeof ListParams.Encoded | undefined; readonly config?: Config | undefined } - | undefined + readonly "createPresetsChatCompletions": ( + slug: string, + options: { + readonly params?: typeof CreatePresetsChatCompletionsParams.Encoded | undefined + readonly payload: typeof CreatePresetsChatCompletionsRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"List401", typeof List401.Type> - | OpenRouterClientError<"List429", typeof List429.Type> - | OpenRouterClientError<"List500", typeof List500.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions400", typeof CreatePresetsChatCompletions400.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions401", typeof CreatePresetsChatCompletions401.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions403", typeof CreatePresetsChatCompletions403.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions404", typeof CreatePresetsChatCompletions404.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions409", typeof CreatePresetsChatCompletions409.Type> + | OpenRouterClientError<"CreatePresetsChatCompletions500", typeof CreatePresetsChatCompletions500.Type> > /** - * Create a new API key for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Creates a preset (or a new version of an existing one) from an inference request body. Only fields that overlap with the preset config are persisted; other fields (e.g. `messages`, `stream`, `prompt`) are silently ignored. */ - readonly "createKeys": ( - options: { readonly payload: typeof CreateKeysRequestJson.Encoded; readonly config?: Config | undefined } + readonly "createPresetsMessages": ( + slug: string, + options: { + readonly params?: typeof CreatePresetsMessagesParams.Encoded | undefined + readonly payload: typeof CreatePresetsMessagesRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateKeys400", typeof CreateKeys400.Type> - | OpenRouterClientError<"CreateKeys401", typeof CreateKeys401.Type> - | OpenRouterClientError<"CreateKeys429", typeof CreateKeys429.Type> - | OpenRouterClientError<"CreateKeys500", typeof CreateKeys500.Type> + | OpenRouterClientError<"CreatePresetsMessages400", typeof CreatePresetsMessages400.Type> + | OpenRouterClientError<"CreatePresetsMessages401", typeof CreatePresetsMessages401.Type> + | OpenRouterClientError<"CreatePresetsMessages403", typeof CreatePresetsMessages403.Type> + | OpenRouterClientError<"CreatePresetsMessages404", typeof CreatePresetsMessages404.Type> + | OpenRouterClientError<"CreatePresetsMessages409", typeof CreatePresetsMessages409.Type> + | OpenRouterClientError<"CreatePresetsMessages500", typeof CreatePresetsMessages500.Type> > /** - * Get a single API key by hash. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Creates a preset (or a new version of an existing one) from an inference request body. Only fields that overlap with the preset config are persisted; other fields (e.g. `messages`, `stream`, `prompt`) are silently ignored. */ - readonly "getKey": ( - hash: string, - options: { readonly config?: Config | undefined } | undefined + readonly "createPresetsResponses": ( + slug: string, + options: { + readonly params?: typeof CreatePresetsResponsesParams.Encoded | undefined + readonly payload: typeof CreatePresetsResponsesRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetKey401", typeof GetKey401.Type> - | OpenRouterClientError<"GetKey404", typeof GetKey404.Type> - | OpenRouterClientError<"GetKey429", typeof GetKey429.Type> - | OpenRouterClientError<"GetKey500", typeof GetKey500.Type> + | OpenRouterClientError<"CreatePresetsResponses400", typeof CreatePresetsResponses400.Type> + | OpenRouterClientError<"CreatePresetsResponses401", typeof CreatePresetsResponses401.Type> + | OpenRouterClientError<"CreatePresetsResponses403", typeof CreatePresetsResponses403.Type> + | OpenRouterClientError<"CreatePresetsResponses404", typeof CreatePresetsResponses404.Type> + | OpenRouterClientError<"CreatePresetsResponses409", typeof CreatePresetsResponses409.Type> + | OpenRouterClientError<"CreatePresetsResponses500", typeof CreatePresetsResponses500.Type> > /** - * Delete an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Lists all versions of a preset, ordered by version number ascending (oldest first). */ - readonly "deleteKeys": ( - hash: string, - options: { readonly config?: Config | undefined } | undefined + readonly "listPresetVersions": ( + slug: string, + options: { + readonly params?: typeof ListPresetVersionsParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"DeleteKeys401", typeof DeleteKeys401.Type> - | OpenRouterClientError<"DeleteKeys404", typeof DeleteKeys404.Type> - | OpenRouterClientError<"DeleteKeys429", typeof DeleteKeys429.Type> - | OpenRouterClientError<"DeleteKeys500", typeof DeleteKeys500.Type> + | OpenRouterClientError<"ListPresetVersions400", typeof ListPresetVersions400.Type> + | OpenRouterClientError<"ListPresetVersions401", typeof ListPresetVersions401.Type> + | OpenRouterClientError<"ListPresetVersions404", typeof ListPresetVersions404.Type> + | OpenRouterClientError<"ListPresetVersions500", typeof ListPresetVersions500.Type> > /** - * Update an existing API key. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Retrieves a specific version of a preset by its slug and version number. */ - readonly "updateKeys": ( - hash: string, - options: { readonly payload: typeof UpdateKeysRequestJson.Encoded; readonly config?: Config | undefined } + readonly "getPresetVersion": ( + slug: string, + version: string, + options: { + readonly params?: typeof GetPresetVersionParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"UpdateKeys400", typeof UpdateKeys400.Type> - | OpenRouterClientError<"UpdateKeys401", typeof UpdateKeys401.Type> - | OpenRouterClientError<"UpdateKeys404", typeof UpdateKeys404.Type> - | OpenRouterClientError<"UpdateKeys429", typeof UpdateKeys429.Type> - | OpenRouterClientError<"UpdateKeys500", typeof UpdateKeys500.Type> + | OpenRouterClientError<"GetPresetVersion400", typeof GetPresetVersion400.Type> + | OpenRouterClientError<"GetPresetVersion401", typeof GetPresetVersion401.Type> + | OpenRouterClientError<"GetPresetVersion404", typeof GetPresetVersion404.Type> + | OpenRouterClientError<"GetPresetVersion500", typeof GetPresetVersion500.Type> > /** - * List all guardrails for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * List all providers */ - readonly "listGuardrails": ( + readonly "listProviders": ( options: - | { readonly params?: typeof ListGuardrailsParams.Encoded | undefined; readonly config?: Config | undefined } + | { readonly params?: typeof ListProvidersParams.Encoded | undefined; readonly config?: Config | undefined } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListGuardrails401", typeof ListGuardrails401.Type> - | OpenRouterClientError<"ListGuardrails500", typeof ListGuardrails500.Type> + | OpenRouterClientError<"ListProviders500", typeof ListProviders500.Type> > /** - * Create a new guardrail for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Submits a rerank request to the rerank router */ - readonly "createGuardrail": ( - options: { readonly payload: typeof CreateGuardrailRequestJson.Encoded; readonly config?: Config | undefined } + readonly "createRerank": ( + options: { + readonly params?: typeof CreateRerankParams.Encoded | undefined + readonly payload: typeof CreateRerankRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateGuardrail400", typeof CreateGuardrail400.Type> - | OpenRouterClientError<"CreateGuardrail401", typeof CreateGuardrail401.Type> - | OpenRouterClientError<"CreateGuardrail500", typeof CreateGuardrail500.Type> + | OpenRouterClientError<"CreateRerank400", typeof CreateRerank400.Type> + | OpenRouterClientError<"CreateRerank401", typeof CreateRerank401.Type> + | OpenRouterClientError<"CreateRerank402", typeof CreateRerank402.Type> + | OpenRouterClientError<"CreateRerank404", typeof CreateRerank404.Type> + | OpenRouterClientError<"CreateRerank429", typeof CreateRerank429.Type> + | OpenRouterClientError<"CreateRerank500", typeof CreateRerank500.Type> + | OpenRouterClientError<"CreateRerank502", typeof CreateRerank502.Type> + | OpenRouterClientError<"CreateRerank503", typeof CreateRerank503.Type> + | OpenRouterClientError<"CreateRerank524", typeof CreateRerank524.Type> + | OpenRouterClientError<"CreateRerank529", typeof CreateRerank529.Type> > /** - * Get a single guardrail by ID. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Submits a rerank request to the rerank router */ - readonly "getGuardrail": ( - id: string, - options: { readonly config?: Config | undefined } | undefined + readonly "createRerankSse": ( + options: { + readonly params?: typeof CreateRerankParams.Encoded | undefined + readonly payload: typeof CreateRerankRequestJson.Encoded + } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateRerank200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + typeof CreateRerank200Sse.DecodingServices + > + /** + * Creates a streaming or non-streaming response using OpenResponses API format + */ + readonly "createResponses": ( + options: { + readonly params?: typeof CreateResponsesParams.Encoded | undefined + readonly payload: typeof CreateResponsesRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetGuardrail401", typeof GetGuardrail401.Type> - | OpenRouterClientError<"GetGuardrail404", typeof GetGuardrail404.Type> - | OpenRouterClientError<"GetGuardrail500", typeof GetGuardrail500.Type> + | OpenRouterClientError<"CreateResponses400", typeof CreateResponses400.Type> + | OpenRouterClientError<"CreateResponses401", typeof CreateResponses401.Type> + | OpenRouterClientError<"CreateResponses402", typeof CreateResponses402.Type> + | OpenRouterClientError<"CreateResponses403", typeof CreateResponses403.Type> + | OpenRouterClientError<"CreateResponses404", typeof CreateResponses404.Type> + | OpenRouterClientError<"CreateResponses408", typeof CreateResponses408.Type> + | OpenRouterClientError<"CreateResponses413", typeof CreateResponses413.Type> + | OpenRouterClientError<"CreateResponses422", typeof CreateResponses422.Type> + | OpenRouterClientError<"CreateResponses429", typeof CreateResponses429.Type> + | OpenRouterClientError<"CreateResponses500", typeof CreateResponses500.Type> + | OpenRouterClientError<"CreateResponses502", typeof CreateResponses502.Type> + | OpenRouterClientError<"CreateResponses503", typeof CreateResponses503.Type> + | OpenRouterClientError<"CreateResponses524", typeof CreateResponses524.Type> + | OpenRouterClientError<"CreateResponses529", typeof CreateResponses529.Type> > /** - * Delete an existing guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Creates a streaming or non-streaming response using OpenResponses API format */ - readonly "deleteGuardrail": ( - id: string, - options: { readonly config?: Config | undefined } | undefined + readonly "createResponsesSse": ( + options: { + readonly params?: typeof CreateResponsesParams.Encoded | undefined + readonly payload: typeof CreateResponsesRequestJson.Encoded + } + ) => Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: typeof CreateResponses200Sse.Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + typeof CreateResponses200Sse.DecodingServices + > + /** + * Submits a video generation request and returns a polling URL to check status + */ + readonly "createVideos": ( + options: { + readonly params?: typeof CreateVideosParams.Encoded | undefined + readonly payload: typeof CreateVideosRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"DeleteGuardrail401", typeof DeleteGuardrail401.Type> - | OpenRouterClientError<"DeleteGuardrail404", typeof DeleteGuardrail404.Type> - | OpenRouterClientError<"DeleteGuardrail500", typeof DeleteGuardrail500.Type> + | OpenRouterClientError<"CreateVideos400", typeof CreateVideos400.Type> + | OpenRouterClientError<"CreateVideos401", typeof CreateVideos401.Type> + | OpenRouterClientError<"CreateVideos402", typeof CreateVideos402.Type> + | OpenRouterClientError<"CreateVideos404", typeof CreateVideos404.Type> + | OpenRouterClientError<"CreateVideos429", typeof CreateVideos429.Type> + | OpenRouterClientError<"CreateVideos500", typeof CreateVideos500.Type> > /** - * Update an existing guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Returns job status and content URLs when completed */ - readonly "updateGuardrail": ( - id: string, - options: { readonly payload: typeof UpdateGuardrailRequestJson.Encoded; readonly config?: Config | undefined } + readonly "getVideos": ( + jobId: string, + options: + | { readonly params?: typeof GetVideosParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"UpdateGuardrail400", typeof UpdateGuardrail400.Type> - | OpenRouterClientError<"UpdateGuardrail401", typeof UpdateGuardrail401.Type> - | OpenRouterClientError<"UpdateGuardrail404", typeof UpdateGuardrail404.Type> - | OpenRouterClientError<"UpdateGuardrail500", typeof UpdateGuardrail500.Type> + | OpenRouterClientError<"GetVideos401", typeof GetVideos401.Type> + | OpenRouterClientError<"GetVideos404", typeof GetVideos404.Type> + | OpenRouterClientError<"GetVideos500", typeof GetVideos500.Type> > /** - * List all API key guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Streams the generated video content from the upstream provider */ - readonly "listKeyAssignments": ( + readonly "listVideosContent": ( + jobId: string, options: { - readonly params?: typeof ListKeyAssignmentsParams.Encoded | undefined + readonly params?: typeof ListVideosContentParams.Encoded | undefined readonly config?: Config | undefined } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListKeyAssignments401", typeof ListKeyAssignments401.Type> - | OpenRouterClientError<"ListKeyAssignments500", typeof ListKeyAssignments500.Type> + | OpenRouterClientError<"ListVideosContent400", typeof ListVideosContent400.Type> + | OpenRouterClientError<"ListVideosContent401", typeof ListVideosContent401.Type> + | OpenRouterClientError<"ListVideosContent404", typeof ListVideosContent404.Type> + | OpenRouterClientError<"ListVideosContent500", typeof ListVideosContent500.Type> + | OpenRouterClientError<"ListVideosContent502", typeof ListVideosContent502.Type> > /** - * List all organization member guardrail assignments for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Returns a list of all available video generation models and their properties */ - readonly "listMemberAssignments": ( + readonly "listVideosModels": ( options: { - readonly params?: typeof ListMemberAssignmentsParams.Encoded | undefined + readonly params?: typeof ListVideosModelsParams.Encoded | undefined readonly config?: Config | undefined } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListMemberAssignments401", typeof ListMemberAssignments401.Type> - | OpenRouterClientError<"ListMemberAssignments500", typeof ListMemberAssignments500.Type> + | OpenRouterClientError<"ListVideosModels400", typeof ListVideosModels400.Type> + | OpenRouterClientError<"ListVideosModels500", typeof ListVideosModels500.Type> > /** - * List all API key assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * List all workspaces for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listGuardrailKeyAssignments": ( - id: string, - options: { - readonly params?: typeof ListGuardrailKeyAssignmentsParams.Encoded | undefined - readonly config?: Config | undefined - } | undefined + readonly "listWorkspaces": ( + options: + | { readonly params?: typeof ListWorkspacesParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListGuardrailKeyAssignments401", typeof ListGuardrailKeyAssignments401.Type> - | OpenRouterClientError<"ListGuardrailKeyAssignments404", typeof ListGuardrailKeyAssignments404.Type> - | OpenRouterClientError<"ListGuardrailKeyAssignments500", typeof ListGuardrailKeyAssignments500.Type> + | OpenRouterClientError<"ListWorkspaces401", typeof ListWorkspaces401.Type> + | OpenRouterClientError<"ListWorkspaces500", typeof ListWorkspaces500.Type> > /** - * Assign multiple API keys to a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Create a new workspace for the authenticated user. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "bulkAssignKeysToGuardrail": ( - id: string, + readonly "createWorkspace": ( options: { - readonly payload: typeof BulkAssignKeysToGuardrailRequestJson.Encoded + readonly params?: typeof CreateWorkspaceParams.Encoded | undefined + readonly payload: typeof CreateWorkspaceRequestJson.Encoded readonly config?: Config | undefined } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"BulkAssignKeysToGuardrail400", typeof BulkAssignKeysToGuardrail400.Type> - | OpenRouterClientError<"BulkAssignKeysToGuardrail401", typeof BulkAssignKeysToGuardrail401.Type> - | OpenRouterClientError<"BulkAssignKeysToGuardrail404", typeof BulkAssignKeysToGuardrail404.Type> - | OpenRouterClientError<"BulkAssignKeysToGuardrail500", typeof BulkAssignKeysToGuardrail500.Type> + | OpenRouterClientError<"CreateWorkspace400", typeof CreateWorkspace400.Type> + | OpenRouterClientError<"CreateWorkspace401", typeof CreateWorkspace401.Type> + | OpenRouterClientError<"CreateWorkspace403", typeof CreateWorkspace403.Type> + | OpenRouterClientError<"CreateWorkspace500", typeof CreateWorkspace500.Type> > /** - * List all organization member assignments for a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Get a single workspace by ID or slug. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "listGuardrailMemberAssignments": ( + readonly "getWorkspace": ( + id: string, + options: + | { readonly params?: typeof GetWorkspaceParams.Encoded | undefined; readonly config?: Config | undefined } + | undefined + ) => Effect.Effect< + WithOptionalResponse, + | HttpClientError.HttpClientError + | SchemaError + | OpenRouterClientError<"GetWorkspace401", typeof GetWorkspace401.Type> + | OpenRouterClientError<"GetWorkspace404", typeof GetWorkspace404.Type> + | OpenRouterClientError<"GetWorkspace500", typeof GetWorkspace500.Type> + > + /** + * Delete an existing workspace. The default workspace cannot be deleted. Workspaces with active API keys cannot be deleted; remove the keys first. [Management key](/docs/guides/overview/auth/management-api-keys) required. + */ + readonly "deleteWorkspace": ( id: string, options: { - readonly params?: typeof ListGuardrailMemberAssignmentsParams.Encoded | undefined + readonly params?: typeof DeleteWorkspaceParams.Encoded | undefined readonly config?: Config | undefined } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ListGuardrailMemberAssignments401", typeof ListGuardrailMemberAssignments401.Type> - | OpenRouterClientError<"ListGuardrailMemberAssignments404", typeof ListGuardrailMemberAssignments404.Type> - | OpenRouterClientError<"ListGuardrailMemberAssignments500", typeof ListGuardrailMemberAssignments500.Type> + | OpenRouterClientError<"DeleteWorkspace400", typeof DeleteWorkspace400.Type> + | OpenRouterClientError<"DeleteWorkspace401", typeof DeleteWorkspace401.Type> + | OpenRouterClientError<"DeleteWorkspace403", typeof DeleteWorkspace403.Type> + | OpenRouterClientError<"DeleteWorkspace404", typeof DeleteWorkspace404.Type> + | OpenRouterClientError<"DeleteWorkspace500", typeof DeleteWorkspace500.Type> > /** - * Assign multiple organization members to a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Update an existing workspace by ID or slug. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "bulkAssignMembersToGuardrail": ( + readonly "updateWorkspace": ( id: string, options: { - readonly payload: typeof BulkAssignMembersToGuardrailRequestJson.Encoded + readonly params?: typeof UpdateWorkspaceParams.Encoded | undefined + readonly payload: typeof UpdateWorkspaceRequestJson.Encoded readonly config?: Config | undefined } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"BulkAssignMembersToGuardrail400", typeof BulkAssignMembersToGuardrail400.Type> - | OpenRouterClientError<"BulkAssignMembersToGuardrail401", typeof BulkAssignMembersToGuardrail401.Type> - | OpenRouterClientError<"BulkAssignMembersToGuardrail404", typeof BulkAssignMembersToGuardrail404.Type> - | OpenRouterClientError<"BulkAssignMembersToGuardrail500", typeof BulkAssignMembersToGuardrail500.Type> + | OpenRouterClientError<"UpdateWorkspace400", typeof UpdateWorkspace400.Type> + | OpenRouterClientError<"UpdateWorkspace401", typeof UpdateWorkspace401.Type> + | OpenRouterClientError<"UpdateWorkspace403", typeof UpdateWorkspace403.Type> + | OpenRouterClientError<"UpdateWorkspace404", typeof UpdateWorkspace404.Type> + | OpenRouterClientError<"UpdateWorkspace500", typeof UpdateWorkspace500.Type> > /** - * Unassign multiple API keys from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * List all budgets configured for a workspace. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "bulkUnassignKeysFromGuardrail": ( + readonly "listWorkspaceBudgets": ( id: string, options: { - readonly payload: typeof BulkUnassignKeysFromGuardrailRequestJson.Encoded + readonly params?: typeof ListWorkspaceBudgetsParams.Encoded | undefined readonly config?: Config | undefined - } + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"BulkUnassignKeysFromGuardrail400", typeof BulkUnassignKeysFromGuardrail400.Type> - | OpenRouterClientError<"BulkUnassignKeysFromGuardrail401", typeof BulkUnassignKeysFromGuardrail401.Type> - | OpenRouterClientError<"BulkUnassignKeysFromGuardrail404", typeof BulkUnassignKeysFromGuardrail404.Type> - | OpenRouterClientError<"BulkUnassignKeysFromGuardrail500", typeof BulkUnassignKeysFromGuardrail500.Type> + | OpenRouterClientError<"ListWorkspaceBudgets401", typeof ListWorkspaceBudgets401.Type> + | OpenRouterClientError<"ListWorkspaceBudgets404", typeof ListWorkspaceBudgets404.Type> + | OpenRouterClientError<"ListWorkspaceBudgets500", typeof ListWorkspaceBudgets500.Type> > /** - * Unassign multiple organization members from a specific guardrail. [Management key](/docs/guides/overview/auth/management-api-keys) required. + * Create or update the budget for a given interval. Budget limits must strictly decrease as the interval narrows (lifetime > monthly > weekly > daily). [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "bulkUnassignMembersFromGuardrail": ( + readonly "upsertWorkspaceBudget": ( id: string, + interval: string, options: { - readonly payload: typeof BulkUnassignMembersFromGuardrailRequestJson.Encoded + readonly params?: typeof UpsertWorkspaceBudgetParams.Encoded | undefined + readonly payload: typeof UpsertWorkspaceBudgetRequestJson.Encoded readonly config?: Config | undefined } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"BulkUnassignMembersFromGuardrail400", typeof BulkUnassignMembersFromGuardrail400.Type> - | OpenRouterClientError<"BulkUnassignMembersFromGuardrail401", typeof BulkUnassignMembersFromGuardrail401.Type> - | OpenRouterClientError<"BulkUnassignMembersFromGuardrail404", typeof BulkUnassignMembersFromGuardrail404.Type> - | OpenRouterClientError<"BulkUnassignMembersFromGuardrail500", typeof BulkUnassignMembersFromGuardrail500.Type> + | OpenRouterClientError<"UpsertWorkspaceBudget400", typeof UpsertWorkspaceBudget400.Type> + | OpenRouterClientError<"UpsertWorkspaceBudget401", typeof UpsertWorkspaceBudget401.Type> + | OpenRouterClientError<"UpsertWorkspaceBudget404", typeof UpsertWorkspaceBudget404.Type> + | OpenRouterClientError<"UpsertWorkspaceBudget500", typeof UpsertWorkspaceBudget500.Type> > /** - * Get information on the API key associated with the current authentication session + * Remove the budget for a given interval. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "getCurrentKey": ( - options: { readonly config?: Config | undefined } | undefined + readonly "deleteWorkspaceBudget": ( + id: string, + interval: string, + options: { + readonly params?: typeof DeleteWorkspaceBudgetParams.Encoded | undefined + readonly config?: Config | undefined + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"GetCurrentKey401", typeof GetCurrentKey401.Type> - | OpenRouterClientError<"GetCurrentKey500", typeof GetCurrentKey500.Type> + | OpenRouterClientError<"DeleteWorkspaceBudget401", typeof DeleteWorkspaceBudget401.Type> + | OpenRouterClientError<"DeleteWorkspaceBudget404", typeof DeleteWorkspaceBudget404.Type> + | OpenRouterClientError<"DeleteWorkspaceBudget500", typeof DeleteWorkspaceBudget500.Type> > /** - * Exchange an authorization code from the PKCE flow for a user-controlled API key + * List all members of a workspace. Returns paginated results. For the default workspace, returns all organization members (implicit membership). [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "exchangeAuthCodeForAPIKey": ( + readonly "listWorkspaceMembers": ( + id: string, options: { - readonly payload: typeof ExchangeAuthCodeForAPIKeyRequestJson.Encoded + readonly params?: typeof ListWorkspaceMembersParams.Encoded | undefined readonly config?: Config | undefined - } + } | undefined ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"ExchangeAuthCodeForAPIKey400", typeof ExchangeAuthCodeForAPIKey400.Type> - | OpenRouterClientError<"ExchangeAuthCodeForAPIKey403", typeof ExchangeAuthCodeForAPIKey403.Type> - | OpenRouterClientError<"ExchangeAuthCodeForAPIKey500", typeof ExchangeAuthCodeForAPIKey500.Type> + | OpenRouterClientError<"ListWorkspaceMembers401", typeof ListWorkspaceMembers401.Type> + | OpenRouterClientError<"ListWorkspaceMembers403", typeof ListWorkspaceMembers403.Type> + | OpenRouterClientError<"ListWorkspaceMembers404", typeof ListWorkspaceMembers404.Type> + | OpenRouterClientError<"ListWorkspaceMembers500", typeof ListWorkspaceMembers500.Type> > /** - * Create an authorization code for the PKCE flow to generate a user-controlled API key + * Add multiple organization members to a workspace. Members are assigned the same role they hold in the organization. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "createAuthKeysCode": ( - options: { readonly payload: typeof CreateAuthKeysCodeRequestJson.Encoded; readonly config?: Config | undefined } + readonly "bulkAddWorkspaceMembers": ( + id: string, + options: { + readonly params?: typeof BulkAddWorkspaceMembersParams.Encoded | undefined + readonly payload: typeof BulkAddWorkspaceMembersRequestJson.Encoded + readonly config?: Config | undefined + } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"CreateAuthKeysCode400", typeof CreateAuthKeysCode400.Type> - | OpenRouterClientError<"CreateAuthKeysCode401", typeof CreateAuthKeysCode401.Type> - | OpenRouterClientError<"CreateAuthKeysCode500", typeof CreateAuthKeysCode500.Type> + | OpenRouterClientError<"BulkAddWorkspaceMembers400", typeof BulkAddWorkspaceMembers400.Type> + | OpenRouterClientError<"BulkAddWorkspaceMembers401", typeof BulkAddWorkspaceMembers401.Type> + | OpenRouterClientError<"BulkAddWorkspaceMembers403", typeof BulkAddWorkspaceMembers403.Type> + | OpenRouterClientError<"BulkAddWorkspaceMembers404", typeof BulkAddWorkspaceMembers404.Type> + | OpenRouterClientError<"BulkAddWorkspaceMembers500", typeof BulkAddWorkspaceMembers500.Type> > /** - * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. + * Remove multiple members from a workspace. Members with active API keys in the workspace cannot be removed. SCIM-managed members cannot be removed; changes must be made in your identity provider. [Management key](/docs/guides/overview/auth/management-api-keys) required. */ - readonly "sendChatCompletionRequest": ( + readonly "bulkRemoveWorkspaceMembers": ( + id: string, options: { - readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded + readonly params?: typeof BulkRemoveWorkspaceMembersParams.Encoded | undefined + readonly payload: typeof BulkRemoveWorkspaceMembersRequestJson.Encoded readonly config?: Config | undefined } ) => Effect.Effect< - WithOptionalResponse, + WithOptionalResponse, | HttpClientError.HttpClientError | SchemaError - | OpenRouterClientError<"SendChatCompletionRequest400", typeof SendChatCompletionRequest400.Type> - | OpenRouterClientError<"SendChatCompletionRequest401", typeof SendChatCompletionRequest401.Type> - | OpenRouterClientError<"SendChatCompletionRequest429", typeof SendChatCompletionRequest429.Type> - | OpenRouterClientError<"SendChatCompletionRequest500", typeof SendChatCompletionRequest500.Type> - > - /** - * Sends a request for a model response for the given chat conversation. Supports both streaming and non-streaming modes. - */ - readonly "sendChatCompletionRequestSse": ( - options: { readonly payload: typeof SendChatCompletionRequestRequestJson.Encoded } - ) => Stream.Stream< - { - readonly event: string - readonly id: string | undefined - readonly data: typeof SendChatCompletionRequest200Sse.Type - }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, - typeof SendChatCompletionRequest200Sse.DecodingServices + | OpenRouterClientError<"BulkRemoveWorkspaceMembers400", typeof BulkRemoveWorkspaceMembers400.Type> + | OpenRouterClientError<"BulkRemoveWorkspaceMembers401", typeof BulkRemoveWorkspaceMembers401.Type> + | OpenRouterClientError<"BulkRemoveWorkspaceMembers403", typeof BulkRemoveWorkspaceMembers403.Type> + | OpenRouterClientError<"BulkRemoveWorkspaceMembers404", typeof BulkRemoveWorkspaceMembers404.Type> + | OpenRouterClientError<"BulkRemoveWorkspaceMembers500", typeof BulkRemoveWorkspaceMembers500.Type> > } diff --git a/.context/effect/packages/ai/openrouter/src/OpenRouterClient.ts b/.context/effect/packages/ai/openrouter/src/OpenRouterClient.ts index ec461a1eb..8a2e66429 100644 --- a/.context/effect/packages/ai/openrouter/src/OpenRouterClient.ts +++ b/.context/effect/packages/ai/openrouter/src/OpenRouterClient.ts @@ -37,21 +37,21 @@ import { OpenRouterConfig } from "./OpenRouterConfig.ts" * Provides methods for interacting with OpenRouter's Chat Completions API, * including both synchronous and streaming message creation. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { readonly client: Generated.OpenRouterClient readonly createChatCompletion: ( - options: typeof Generated.ChatGenerationParams.Encoded + options: typeof Generated.ChatRequest.Encoded ) => Effect.Effect< [body: typeof Generated.SendChatCompletionRequest200.Type, response: HttpClientResponse.HttpClientResponse], AiError.AiError > readonly createChatCompletionStream: ( - options: Omit + options: Omit ) => Effect.Effect< [ response: HttpClientResponse.HttpClientResponse, @@ -72,7 +72,7 @@ export interface Service { * @category models * @since 4.0.0 */ -export type ChatStreamingResponseChunkData = typeof Generated.ChatStreamingResponseChunk.fields.data.Type +export type ChatStreamingResponseChunkData = typeof Generated.ChatStreamingResponse.fields.data.Type // ============================================================================= // Service Identifier @@ -197,8 +197,18 @@ export const make = Effect.fnUntraced( Effect.catchTags({ SendChatCompletionRequest400: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), SendChatCompletionRequest401: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest402: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest403: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest404: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest408: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest413: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest422: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), SendChatCompletionRequest429: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), SendChatCompletionRequest500: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest502: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest503: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest524: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), + SendChatCompletionRequest529: (error) => Effect.fail(Errors.mapClientError(error, "createChatCompletion")), HttpClientError: (error) => Errors.mapHttpClientError(error, "createChatCompletion"), SchemaError: (error) => Effect.fail(Errors.mapSchemaError(error, "createChatCompletion")) }) @@ -218,6 +228,7 @@ export const make = Effect.fnUntraced( Stream.catchTags({ // TODO: handle SSE retries Retry: (error) => Stream.die(error), + SseError: (error) => Stream.fail(Errors.mapSseError(error, "createChatCompletionStream")), HttpClientError: (error) => Stream.fromEffect(Errors.mapHttpClientError(error, "createChatCompletionStream")), SchemaError: (error) => Stream.fail(Errors.mapSchemaError(error, "createChatCompletionStream")) }) @@ -346,7 +357,7 @@ export const layerConfig = (options?: { // Internal Utilities // ============================================================================= -const ChatStreamingResponseChunkDataFromString = Schema.fromJsonString(Generated.ChatStreamingResponseChunk.fields.data) +const ChatStreamingResponseChunkDataFromString = Schema.fromJsonString(Generated.ChatStreamingResponse.fields.data) const decodeChatStreamingResponseChunkData = Schema.decodeUnknownEffect(ChatStreamingResponseChunkDataFromString) const decodeChatCompletionSseData = ( diff --git a/.context/effect/packages/ai/openrouter/src/OpenRouterConfig.ts b/.context/effect/packages/ai/openrouter/src/OpenRouterConfig.ts index 7ca997872..11df7e7e1 100644 --- a/.context/effect/packages/ai/openrouter/src/OpenRouterConfig.ts +++ b/.context/effect/packages/ai/openrouter/src/OpenRouterConfig.ts @@ -50,7 +50,7 @@ export declare namespace OpenRouterConfig { * Configuration values read by OpenRouter provider operations when resolving * the generated HTTP client. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { diff --git a/.context/effect/packages/ai/openrouter/src/OpenRouterError.ts b/.context/effect/packages/ai/openrouter/src/OpenRouterError.ts index 00db3860d..63d0555e0 100644 --- a/.context/effect/packages/ai/openrouter/src/OpenRouterError.ts +++ b/.context/effect/packages/ai/openrouter/src/OpenRouterError.ts @@ -51,7 +51,7 @@ declare module "effect/unstable/ai/AiError" { * information from responses where the provider rejected the request because * a limit was reached. * - * @category configuration + * @category models * @since 4.0.0 */ export interface RateLimitErrorMetadata { @@ -69,7 +69,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for failures caused by exhausted account, * billing, or usage quota. * - * @category configuration + * @category models * @since 4.0.0 */ export interface QuotaExhaustedErrorMetadata { @@ -87,7 +87,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for failed API key, authorization, or * permission checks. * - * @category configuration + * @category models * @since 4.0.0 */ export interface AuthenticationErrorMetadata { @@ -105,7 +105,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when OpenRouter rejects input or output * because it violates a content policy. * - * @category configuration + * @category models * @since 4.0.0 */ export interface ContentPolicyErrorMetadata { @@ -123,7 +123,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for malformed requests, unsupported * parameters, or other request validation failures reported by OpenRouter. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidRequestErrorMetadata { @@ -141,7 +141,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for OpenRouter-side failures such as * transient server errors or overload responses. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InternalProviderErrorMetadata { @@ -159,7 +159,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when an OpenRouter response cannot be * parsed or validated as the expected output. * - * @category configuration + * @category models * @since 4.0.0 */ export interface InvalidOutputErrorMetadata { @@ -177,7 +177,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when OpenRouter returns content that does * not satisfy the requested structured output schema. * - * @category configuration + * @category models * @since 4.0.0 */ export interface StructuredOutputErrorMetadata { @@ -195,7 +195,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details when an unsupported schema failure is * associated with an OpenRouter response. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnsupportedSchemaErrorMetadata { @@ -213,7 +213,7 @@ declare module "effect/unstable/ai/AiError" { * Preserves provider error details for OpenRouter failures that do not map * cleanly to a more specific AI error category. * - * @category configuration + * @category models * @since 4.0.0 */ export interface UnknownErrorMetadata { diff --git a/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts b/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts index aeae7ea36..ffe75b739 100644 --- a/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts +++ b/.context/effect/packages/ai/openrouter/src/OpenRouterLanguageModel.ts @@ -62,7 +62,7 @@ export class Config extends Context.Service< Simplify< & Partial< Omit< - typeof Generated.ChatGenerationParams.Encoded, + typeof Generated.ChatRequest.Encoded, "messages" | "response_format" | "tools" | "tool_choice" | "stream" | "stream_options" > > @@ -89,7 +89,7 @@ export class Config extends Context.Service< * @category models * @since 4.0.0 */ -export type ReasoningDetails = Exclude +export type ReasoningDetails = Exclude /** * File annotations emitted on OpenRouter assistant messages and exposed in @@ -99,7 +99,7 @@ export type ReasoningDetails = Exclude[number], + NonNullable[number], { type: "file" } > @@ -112,7 +112,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when translating system instructions into * OpenRouter chat messages. * - * @category request + * @category models * @since 4.0.0 */ export interface SystemMessageOptions extends ProviderOptions { @@ -123,7 +123,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } @@ -135,7 +135,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when translating user content into OpenRouter chat * messages. * - * @category request + * @category models * @since 4.0.0 */ export interface UserMessageOptions extends ProviderOptions { @@ -146,7 +146,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } @@ -158,7 +158,7 @@ declare module "effect/unstable/ai/Prompt" { * Preserves reasoning metadata when assistant messages are replayed in later * OpenRouter requests. * - * @category request + * @category models * @since 4.0.0 */ export interface AssistantMessageOptions extends ProviderOptions { @@ -169,7 +169,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null /** * Reasoning details associated with the assistant message. */ @@ -185,7 +185,7 @@ declare module "effect/unstable/ai/Prompt" { * These options are used when converting tool results into OpenRouter chat * messages. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolMessageOptions extends ProviderOptions { @@ -196,7 +196,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } @@ -207,7 +207,7 @@ declare module "effect/unstable/ai/Prompt" { * * Use when you use these options to control how text content is sent to OpenRouter. * - * @category request + * @category models * @since 4.0.0 */ export interface TextPartOptions extends ProviderOptions { @@ -218,7 +218,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } @@ -230,7 +230,7 @@ declare module "effect/unstable/ai/Prompt" { * Preserves provider reasoning blocks so reasoning-aware conversations can * continue across OpenRouter requests. * - * @category request + * @category models * @since 4.0.0 */ export interface ReasoningPartOptions extends ProviderOptions { @@ -241,7 +241,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null /** * Reasoning details associated with the reasoning part. */ @@ -256,7 +256,7 @@ declare module "effect/unstable/ai/Prompt" { * * Controls file naming and prompt caching for files sent to OpenRouter. * - * @category request + * @category models * @since 4.0.0 */ export interface FilePartOptions extends ProviderOptions { @@ -272,7 +272,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } @@ -284,7 +284,7 @@ declare module "effect/unstable/ai/Prompt" { * Preserves reasoning details associated with tool calls when a conversation * is sent back to OpenRouter. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolCallPartOptions extends ProviderOptions { @@ -306,7 +306,7 @@ declare module "effect/unstable/ai/Prompt" { * * Controls prompt caching for tool results sent to OpenRouter. * - * @category request + * @category models * @since 4.0.0 */ export interface ToolResultPartOptions extends ProviderOptions { @@ -317,7 +317,7 @@ declare module "effect/unstable/ai/Prompt" { /** * A breakpoint which marks the end of reusable content eligible for caching. */ - readonly cacheControl?: typeof Generated.ChatMessageContentItemCacheControl.Encoded | null + readonly cacheControl?: typeof Generated.ChatContentCacheControl.Encoded | null } | null } } @@ -330,7 +330,7 @@ declare module "effect/unstable/ai/Response" { * * Preserves provider reasoning details that can be sent back in later turns. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningPartMetadata extends ProviderMetadata { @@ -352,7 +352,7 @@ declare module "effect/unstable/ai/Response" { * * Carries the first reasoning detail chunk when OpenRouter exposes one. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningStartPartMetadata extends ProviderMetadata { @@ -374,7 +374,7 @@ declare module "effect/unstable/ai/Response" { * * Carries provider reasoning detail chunks as they arrive from OpenRouter. * - * @category response + * @category models * @since 4.0.0 */ export interface ReasoningDeltaPartMetadata extends ProviderMetadata { @@ -397,7 +397,7 @@ declare module "effect/unstable/ai/Response" { * Associates tool calls with provider reasoning details when the model emits * reasoning and tool calls together. * - * @category response + * @category models * @since 4.0.0 */ export interface ToolCallPartMetadata extends ProviderMetadata { @@ -420,7 +420,7 @@ declare module "effect/unstable/ai/Response" { * Includes citation text and offsets returned by providers that support URL * annotations. * - * @category response + * @category models * @since 4.0.0 */ export interface UrlSourcePartMetadata extends ProviderMetadata { @@ -451,7 +451,7 @@ declare module "effect/unstable/ai/Response" { * Exposes provider response details that are not represented by the common * Effect AI finish part fields. * - * @category response + * @category models * @since 4.0.0 */ export interface FinishPartMetadata extends ProviderMetadata { @@ -466,7 +466,7 @@ declare module "effect/unstable/ai/Response" { /** * Raw token usage reported by OpenRouter. */ - readonly usage?: typeof Generated.ChatGenerationTokenUsage.Encoded | null + readonly usage?: typeof Generated.ChatUsage.Encoded | null /** * File annotations returned by the provider. */ @@ -554,11 +554,11 @@ export const make = Effect.fnUntraced(function*({ model, config: providerConfig function*({ config, options }: { readonly config: typeof Config.Service readonly options: LanguageModel.ProviderOptions - }): Effect.fn.Return { + }): Effect.fn.Return { const messages = yield* prepareMessages({ options }) const { tools, toolChoice } = yield* prepareTools({ options, transformer: codecTransformer }) const responseFormat = yield* getResponseFormat({ config, options, transformer: codecTransformer }) - const request: typeof Generated.ChatGenerationParams.Encoded = { + const request: typeof Generated.ChatRequest.Encoded = { ...config, messages, ...(Predicate.isNotUndefined(responseFormat) ? { response_format: responseFormat } : undefined), @@ -667,8 +667,8 @@ export const withConfigOverride: { const prepareMessages = Effect.fnUntraced( function*({ options }: { readonly options: LanguageModel.ProviderOptions - }): Effect.fn.Return, AiError.AiError> { - const messages: Array = [] + }): Effect.fn.Return, AiError.AiError> { + const messages: Array = [] const reasoningDetailsTracker = new ReasoningDetailsDuplicateTracker() @@ -690,7 +690,7 @@ const prepareMessages = Effect.fnUntraced( } case "user": { - const content: Array = [] + const content: Array = [] // Get the message-level cache control const messageCacheControl = getCacheControl(message) @@ -824,7 +824,7 @@ const prepareMessages = Effect.fnUntraced( case "assistant": { let text = "" let reasoning = "" - const toolCalls: Array = [] + const toolCalls: Array = [] for (const part of message.content) { switch (part.type) { @@ -1320,7 +1320,7 @@ const makeStreamResponse = Effect.fnUntraced( // The signature typically arrives in the last reasoning delta, // but reasoning-start only carries the first delta's metadata. metadata: accumulatedReasoningDetails.length > 0 - ? { openRouter: { reasoningDetails: accumulatedReasoningDetails } } + ? { openrouter: { reasoningDetails: accumulatedReasoningDetails } } : undefined }) reasoningStarted = false @@ -1361,7 +1361,7 @@ const makeStreamResponse = Effect.fnUntraced( ? { startIndex: annotation.url_citation.start_index } : undefined), ...(Predicate.isNotUndefined(annotation.url_citation.end_index) - ? { startIndex: annotation.url_citation.end_index } + ? { endIndex: annotation.url_citation.end_index } : undefined) } } @@ -1377,6 +1377,7 @@ const makeStreamResponse = Effect.fnUntraced( for (const toolCall of toolCalls) { const index = toolCall.index ?? toolCalls.length - 1 let activeToolCall = activeToolCalls[index] + const argumentsDelta = toolCall.function?.arguments ?? "" // Tool call start - OpenRouter returns all information except the // tool call parameters in the first chunk @@ -1415,7 +1416,7 @@ const makeStreamResponse = Effect.fnUntraced( id: toolCall.id, type: "function", name: toolCall.function.name, - params: toolCall.function.arguments ?? "" + params: argumentsDelta } activeToolCalls[index] = activeToolCall @@ -1425,23 +1426,16 @@ const makeStreamResponse = Effect.fnUntraced( 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 ?? "" + activeToolCall.params += argumentsDelta + } + + // Emit a tool call delta part if parameters were also sent + if (argumentsDelta.length > 0) { parts.push({ type: "tool-params-delta", id: activeToolCall.id, - delta: activeToolCall.params + delta: argumentsDelta }) } @@ -1588,8 +1582,8 @@ const prepareTools = Effect.fnUntraced( readonly options: LanguageModel.ProviderOptions readonly transformer: LanguageModel.CodecTransformer }): Effect.fn.Return<{ - readonly tools: ReadonlyArray | undefined - readonly toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined + readonly tools: ReadonlyArray | undefined + readonly toolChoice: typeof Generated.ChatToolChoice.Encoded | undefined }, AiError.AiError> { if (options.tools.length === 0) { return { tools: undefined, toolChoice: undefined } @@ -1607,8 +1601,8 @@ const prepareTools = Effect.fnUntraced( }) } - let tools: Array = [] - let toolChoice: typeof Generated.ToolChoiceOption.Encoded | undefined = undefined + let tools: Array> = [] + let toolChoice: typeof Generated.ChatToolChoice.Encoded | undefined = undefined for (const tool of options.tools) { const description = Tool.getDescription(tool) @@ -1650,7 +1644,7 @@ const prepareTools = Effect.fnUntraced( const annotateRequest = ( span: Span, - request: typeof Generated.ChatGenerationParams.Encoded + request: typeof Generated.ChatRequest.Encoded ): void => { addGenAIAnnotations(span, { system: "openrouter", @@ -1717,7 +1711,7 @@ const getCacheControl = ( | Prompt.ReasoningPart | Prompt.FilePart | Prompt.ToolResultPart -): typeof Generated.ChatMessageContentItemCacheControl.Encoded | null => part.options.openrouter?.cacheControl ?? null +): typeof Generated.ChatContentCacheControl.Encoded | null => part.options.openrouter?.cacheControl ?? null const findFirstReasoningDetails = (content: ReadonlyArray): ReasoningDetails | null => { for (const part of content) { @@ -1786,7 +1780,7 @@ const getResponseFormat = Effect.fnUntraced(function*({ config, options, transfo readonly config: typeof Config.Service readonly options: LanguageModel.ProviderOptions readonly transformer: LanguageModel.CodecTransformer -}): Effect.fn.Return { +}): Effect.fn.Return { if (options.responseFormat.type === "json") { const description = SchemaAST.resolveDescription(options.responseFormat.schema.ast) const jsonSchema = yield* tryJsonSchema(options.responseFormat.schema, "getResponseFormat", transformer) @@ -1839,7 +1833,7 @@ const getBase64FromDataUrl = (dataUrl: string): string => { return match ? match[1]! : dataUrl } -const getUsage = (usage: Generated.ChatGenerationTokenUsage | undefined): Response.Usage => { +const getUsage = (usage: Generated.ChatUsage | undefined): Response.Usage => { if (Predicate.isUndefined(usage)) { return { inputTokens: { uncached: undefined, total: 0, cacheRead: undefined, cacheWrite: undefined }, diff --git a/.context/effect/packages/ai/openrouter/src/internal/errors.ts b/.context/effect/packages/ai/openrouter/src/internal/errors.ts index d01128e12..65c10cdf4 100644 --- a/.context/effect/packages/ai/openrouter/src/internal/errors.ts +++ b/.context/effect/packages/ai/openrouter/src/internal/errors.ts @@ -8,6 +8,7 @@ import * as Redactable from "effect/Redactable" import * as Schema from "effect/Schema" import * as AiError from "effect/unstable/ai/AiError" import type * as Response from "effect/unstable/ai/Response" +import type * as Sse from "effect/unstable/encoding/Sse" import type * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import type * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" @@ -23,7 +24,7 @@ export const OpenRouterErrorBody = Schema.Struct({ error: Schema.Struct({ message: Schema.String, type: Schema.optional(Schema.NullOr(Schema.String)), - code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Number.check(Schema.isFinite())]))) + code: Schema.optional(Schema.NullOr(Schema.Union([Schema.String, Schema.Finite]))) }) }) @@ -52,6 +53,17 @@ export const mapSchemaError = dual< reason: AiError.InvalidOutputError.fromSchemaError(error) })) +/** @internal */ +export const mapSseError = dual< + (method: string) => (error: Sse.SseError) => AiError.AiError, + (error: Sse.SseError, method: string) => AiError.AiError +>(2, (error, method) => + AiError.make({ + module: "OpenRouterClient", + method, + reason: new AiError.InvalidOutputError({ description: error.message }) + })) + /** @internal */ export const mapClientError = dual< (method: string) => (error: Generated.OpenRouterClientError) => AiError.AiError, diff --git a/.context/effect/packages/ai/openrouter/src/internal/utilities.ts b/.context/effect/packages/ai/openrouter/src/internal/utilities.ts index 5df64c53b..1d5112e94 100644 --- a/.context/effect/packages/ai/openrouter/src/internal/utilities.ts +++ b/.context/effect/packages/ai/openrouter/src/internal/utilities.ts @@ -14,10 +14,12 @@ const finishReasonMap: Record = { /** @internal */ export const resolveFinishReason = ( finishReason: string | null | undefined -): Response.FinishReason => - Predicate.isNotNullish(finishReason) - ? finishReasonMap[finishReason] - : "other" +): Response.FinishReason => { + if (Predicate.isNullish(finishReason)) { + return "other" + } + return Object.hasOwn(finishReasonMap, finishReason) ? finishReasonMap[finishReason] : "unknown" +} /** * Tracks ReasoningDetailUnion entries and deduplicates them based diff --git a/.context/effect/packages/ai/openrouter/test/Generated.test.ts b/.context/effect/packages/ai/openrouter/test/Generated.test.ts new file mode 100644 index 000000000..8790fed73 --- /dev/null +++ b/.context/effect/packages/ai/openrouter/test/Generated.test.ts @@ -0,0 +1,75 @@ +import { Generated } from "@effect/ai-openrouter" +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { Schema } from "effect" + +describe("Generated", () => { + it("decodes nullable generation statistics", () => { + const response: Generated.GetGeneration200 = { + data: { + id: "gen-test", + upstream_id: null, + total_cost: 0.003294, + cache_discount: null, + upstream_inference_cost: null, + created_at: "2026-07-24T12:00:00Z", + data_region: "global", + model: "openrouter/auto", + app_id: null, + streamed: null, + cancelled: null, + provider_name: null, + http_referer: null, + latency: null, + moderation_latency: null, + generation_time: null, + finish_reason: null, + tokens_prompt: null, + tokens_completion: null, + native_tokens_prompt: null, + native_tokens_completion: null, + native_tokens_completion_images: null, + native_tokens_reasoning: null, + native_tokens_cached: null, + num_fetches: null, + num_media_prompt: null, + num_input_audio_prompt: null, + num_media_completion: null, + num_search_results: null, + origin: "https://openrouter.ai/", + preset_id: null, + usage: 0.003294, + is_byok: false, + native_finish_reason: null, + external_user: null, + api_type: null, + request_id: null, + response_cache_source_id: null, + router: null, + service_tier: null, + session_id: null, + provider_responses: null, + user_agent: null, + web_search_engine: null + } + } + + deepStrictEqual(Schema.decodeUnknownSync(Generated.GetGeneration200)(response), response) + }) + + it("preserves streamed usage cost fields", () => { + const usage = { + completion_tokens: 11, + prompt_tokens: 7, + total_tokens: 18, + cost: 0.000365, + is_byok: false, + prompt_tokens_details: { + cached_tokens: 0, + cache_write_tokens: 0 + } + } + + deepStrictEqual(Schema.decodeUnknownSync(Generated.ChatUsage)(usage), usage) + }) +}) diff --git a/.context/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts b/.context/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts new file mode 100644 index 000000000..b9fef344c --- /dev/null +++ b/.context/effect/packages/ai/openrouter/test/OpenRouterClient.test.ts @@ -0,0 +1,113 @@ +import { OpenRouterClient } from "@effect/ai-openrouter" +import { assert, describe, it } from "@effect/vitest" +import { Context, Effect, Layer, Redacted, type Schema } from "effect" +import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" + +describe("OpenRouterClient", () => { + it.effect("redacts the API key in AI error context", () => + Effect.gen(function*() { + const client = yield* OpenRouterClient.OpenRouterClient + + const result = yield* client.createChatCompletion({ + model: "openai/gpt-4o-mini", + messages: [{ role: "user", content: "hello" }] + }).pipe(Effect.flip) + + assert.strictEqual(result.reason._tag, "InvalidRequestError") + if (result.reason._tag !== "InvalidRequestError" || result.reason.http === undefined) { + return yield* Effect.die(new Error("Expected InvalidRequestError with HTTP context")) + } + const requests = yield* MockHttpClient.requests + assert.include(requests[0]?.url, "/chat/completions") + assert.strictEqual(String(result.reason.http.request.headers["authorization"]), "") + }).pipe(Effect.provide(makeTestLayer({ + _tag: "Json", + status: 400, + body: { + error: { + code: 400, + message: "Bad request" + } + } + })))) +}) + +type MockResponse = + | { + readonly _tag: "Json" + readonly body: Schema.Json + readonly status?: number | undefined + readonly headers?: Record | undefined + } + | { + readonly _tag: "Sse" + readonly events: ReadonlyArray + readonly status?: number | undefined + readonly headers?: Record | undefined + } + +class MockOpenRouterResponse extends Context.Service()("MockOpenRouterResponse") {} + +class MockHttpClient extends Context.Service> +}>()("MockHttpClient") { + static requests = MockHttpClient.use((client) => client.requests) +} + +const makeHttpClientContext = Effect.gen(function*() { + const capturedRequests: Array = [] + const mock = yield* MockOpenRouterResponse + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + capturedRequests.push(request) + return makeResponse(request, mock.response) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + const mockHttpClient: MockHttpClient["Service"] = { + requests: Effect.sync(() => capturedRequests) + } + + return Context.make(HttpClient.HttpClient, httpClient).pipe( + Context.add(MockHttpClient, mockHttpClient) + ) +}) + +const HttpClientLayer = Layer.effectContext(makeHttpClientContext) + +const makeTestLayer = ( + response: MockResponse, + options: OpenRouterClient.Options = { apiKey: Redacted.make("sk-test-key") } +) => + OpenRouterClient.layer(options).pipe( + Layer.provideMerge(HttpClientLayer), + Layer.provide(Layer.succeed(MockOpenRouterResponse, { response })) + ) + +const makeResponse = ( + request: HttpClientRequest.HttpClientRequest, + response: MockResponse +): HttpClientResponse.HttpClientResponse => { + const contentType = response._tag === "Json" + ? "application/json" + : "text/event-stream" + const body = response._tag === "Json" + ? JSON.stringify(response.body) + : response.events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: response.status ?? 200, + headers: { + "content-type": contentType, + ...response.headers + } + }) + ) +} diff --git a/.context/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts b/.context/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts index 72bd51942..ab6738fdd 100644 --- a/.context/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts +++ b/.context/effect/packages/ai/openrouter/test/OpenRouterLanguageModel.test.ts @@ -1,7 +1,7 @@ import { Generated, OpenRouterClient, OpenRouterLanguageModel } from "@effect/ai-openrouter" import { assert, describe, it } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Array, Context, Effect, Layer, Redacted, Ref, Schema } from "effect" +import { Array, Context, Effect, Layer, Redacted, Ref, Schema, Stream } from "effect" import { LanguageModel, Prompt, Tool, Toolkit } from "effect/unstable/ai" import { HttpClient, type HttpClientError, type HttpClientRequest, HttpClientResponse } from "effect/unstable/http" @@ -209,6 +209,142 @@ describe("OpenRouterLanguageModel", () => { }).pipe(Effect.provide(makeTestLayer()))) }) }) + + describe("streamText", () => { + it.effect("preserves streamed citation start and end indexes", () => + Effect.gen(function*() { + const parts = yield* LanguageModel.streamText({ prompt: "cite a source" }).pipe( + Stream.runCollect, + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")), + Effect.provide(makeStreamTestLayer([{ + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + annotations: [{ + type: "url_citation", + url_citation: { + url: "https://example.com/source", + title: "source", + start_index: 2, + end_index: 9 + } + }] + } + }] + }])) + ) + + const source = globalThis.Array.from(parts).find((part) => part.type === "source") + assert.isDefined(source) + if (source?.type === "source") { + assert.deepStrictEqual(source.metadata, { + openrouter: { startIndex: 2, endIndex: 9 } + }) + } + })) + + it.effect("uses lowercase openrouter reasoning-end metadata", () => + Effect.gen(function*() { + const reasoningDetails = [{ + type: "reasoning.text", + text: "thinking", + signature: "signature-final", + format: "unknown" + }] as const + const parts = yield* LanguageModel.streamText({ prompt: "reason then answer" }).pipe( + Stream.runCollect, + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")), + Effect.provide(makeStreamTestLayer([ + { + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ index: 0, delta: { reasoning_details: reasoningDetails } }] + }, + { + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ index: 0, finish_reason: "stop", delta: { content: "answer" } }] + } + ])) + ) + + const reasoningEnd = parts.find((part) => part.type === "reasoning-end") + deepStrictEqual(reasoningEnd?.metadata, { openrouter: { reasoningDetails } }) + })) + + it.effect("emits incremental tool parameter fragments", () => + Effect.gen(function*() { + const ProbeTool = Tool.make("ProbeTool", { + parameters: Schema.Struct({ a: Schema.Number }), + success: Schema.String + }) + const toolkit = Toolkit.make(ProbeTool) + const parts = yield* LanguageModel.streamText({ + prompt: "call the tool", + toolkit, + disableToolCallResolution: true + }).pipe( + Stream.runCollect, + Effect.provide(OpenRouterLanguageModel.model("openai/gpt-4o-mini")), + Effect.provide(toolkit.toLayer({ ProbeTool: () => Effect.succeed("ok") })), + Effect.provide(makeStreamTestLayer([ + { + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { + tool_calls: [{ + index: 0, + id: "call-1", + type: "function", + function: { name: "ProbeTool", arguments: "{\"a\":" } + }] + } + }] + }, + { + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + delta: { tool_calls: [{ index: 0 }] } + }] + }, + { + id: "response-1", + object: "chat.completion.chunk", + model: "openai/gpt-4o-mini", + created: 1, + choices: [{ + index: 0, + finish_reason: "tool_calls", + delta: { tool_calls: [{ index: 0, function: { arguments: "1}" } }] } + }] + } + ])) + ) + + deepStrictEqual( + globalThis.Array.from(parts) + .filter((part) => part.type === "tool-params-delta") + .map((part) => part.delta), + ["{\"a\":", "1}"] + ) + })) + }) }) // ============================================================================= @@ -273,6 +409,7 @@ const makeDefaultResponse = ( created: 1234567890, model: "google/gemini-2.5-flash", object: "chat.completion", + system_fingerprint: null, ...overrides }) @@ -299,3 +436,23 @@ const getRequestBody = (request: HttpClientRequest.HttpClientRequest) => } return yield* Effect.die(new Error("Expected Uint8Array body")) }) + +const makeStreamTestLayer = (events: ReadonlyArray) => { + const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") + "data: [DONE]\n\n" + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + return HttpClientResponse.fromWeb( + request, + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" } + }) + ) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + return OpenRouterClient.layer({ apiKey: Redacted.make("sk-test-key") }).pipe( + Layer.provide(Layer.succeed(HttpClient.HttpClient, httpClient)) + ) +} diff --git a/.context/effect/packages/ai/openrouter/tsconfig.json b/.context/effect/packages/ai/openrouter/tsconfig.json index a9b59a318..1cf21756b 100644 --- a/.context/effect/packages/ai/openrouter/tsconfig.json +++ b/.context/effect/packages/ai/openrouter/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/ai/openrouter/vitest.config.ts b/.context/effect/packages/ai/openrouter/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/ai/openrouter/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/atom/react/CHANGELOG.md b/.context/effect/packages/atom/react/CHANGELOG.md index 72bea2bac..625aed5f4 100644 --- a/.context/effect/packages/atom/react/CHANGELOG.md +++ b/.context/effect/packages/atom/react/CHANGELOG.md @@ -1,5 +1,59 @@ # @effect/atom-react +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6672](https://github.com/Effect-TS/effect/pull/6672) [`83d571c`](https://github.com/Effect-TS/effect/commit/83d571c9500d200e9f08aaf64632c502ae6f5afe) Thanks @andrskr! - Scope `useAtomSuspense` promises to their atom registry so concurrent registries resolve independently. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/atom/react/README.md b/.context/effect/packages/atom/react/README.md index 1cd2f2005..7c6c3e65b 100644 --- a/.context/effect/packages/atom/react/README.md +++ b/.context/effect/packages/atom/react/README.md @@ -1,7 +1,14 @@ -# `@effect/atom-react` +# @effect/atom-react -React bindings for the Effect Atom modules. +[React](https://react.dev) bindings for Atom, the reactive state management modules for Effect. Includes hooks for reading and updating atoms, and helpers for server-side rendering hydration. + +## Installation + +```sh +npm install effect@beta @effect/atom-react@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-react). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/atom-react) diff --git a/.context/effect/packages/atom/react/docgen.json b/.context/effect/packages/atom/react/docgen.json deleted file mode 100644 index 2eb419bc7..000000000 --- a/.context/effect/packages/atom/react/docgen.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/react/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["node"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/atom/react/package.json b/.context/effect/packages/atom/react/package.json index afe40fe29..038431387 100644 --- a/.context/effect/packages/atom/react/package.json +++ b/.context/effect/packages/atom/react/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-react", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "React bindings for the Effect Atom modules", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,32 +50,31 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "peerDependencies": { "effect": "workspace:^", - "react": "^19.2.4", - "scheduler": "*" + "react": ">=19.2.7 <20.0.0", + "scheduler": ">=0.27.0 <0.28.0" }, "devDependencies": { "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.2", "@types/scheduler": "^0.26.0", "effect": "workspace:^", - "jsdom": "^29.1.1", - "react": "19.2.7", - "react-dom": "19.2.7", + "jsdom": "^30.0.0", + "react": "^19.2.7", + "react-dom": "^19.2.7", "react-error-boundary": "^6.1.2", "scheduler": "^0.27.0" } diff --git a/.context/effect/packages/atom/react/src/Hooks.ts b/.context/effect/packages/atom/react/src/Hooks.ts index 381d86d6c..4632ea333 100644 --- a/.context/effect/packages/atom/react/src/Hooks.ts +++ b/.context/effect/packages/atom/react/src/Hooks.ts @@ -293,8 +293,14 @@ export const useAtom = , Promise>(), - default: new Map, Promise>() + suspendOnWaiting: new WeakMap< + AtomRegistry.AtomRegistry, + WeakMap, Promise> + >(), + default: new WeakMap< + AtomRegistry.AtomRegistry, + WeakMap, Promise> + >() } function atomToPromise( @@ -302,7 +308,12 @@ function atomToPromise( atom: Atom.Atom>, suspendOnWaiting: boolean ) { - const map = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default + const registries = suspendOnWaiting ? atomPromiseMap.suspendOnWaiting : atomPromiseMap.default + let map = registries.get(registry) + if (map === undefined) { + map = new WeakMap() + registries.set(registry, map) + } let promise = map.get(atom) if (promise !== undefined) { return promise diff --git a/.context/effect/packages/atom/react/src/ScopedAtom.ts b/.context/effect/packages/atom/react/src/ScopedAtom.ts index 8a6048229..9e7e3e7a0 100644 --- a/.context/effect/packages/atom/react/src/ScopedAtom.ts +++ b/.context/effect/packages/atom/react/src/ScopedAtom.ts @@ -42,10 +42,11 @@ export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom" * * **Example** (Providing and reading a scoped atom) * - * ```ts + * ```ts import.meta.vitest * import { make, useAtomValue } from "@effect/atom-react" * import { Atom } from "effect/unstable/reactivity" * import * as React from "react" + * import { renderToStaticMarkup } from "react-dom/server" * * const Counter = make(() => Atom.make(0)) * @@ -58,6 +59,8 @@ export const TypeId: TypeId = "~@effect/atom-react/ScopedAtom" * export function App() { * return React.createElement(Counter.Provider, null, React.createElement(View)) * } + * + * renderToStaticMarkup(React.createElement(App)) // => "

" * ``` * * @category models @@ -92,10 +95,11 @@ export interface ScopedAtom, Input = never> { * * **Example** (Creating a scoped atom with input) * - * ```ts + * ```ts import.meta.vitest * import { make, useAtomValue } from "@effect/atom-react" * import { Atom } from "effect/unstable/reactivity" * import * as React from "react" + * import { renderToStaticMarkup } from "react-dom/server" * * const User = make((name: string) => Atom.make(name)) * @@ -112,6 +116,8 @@ export interface ScopedAtom, Input = never> { * React.createElement(UserName) * ) * } + * + * renderToStaticMarkup(React.createElement(App)) // => "Ada" * ``` * * @category constructors diff --git a/.context/effect/packages/atom/react/test/index.test.tsx b/.context/effect/packages/atom/react/test/index.test.tsx index df4d8ca9d..06c2668b9 100644 --- a/.context/effect/packages/atom/react/test/index.test.tsx +++ b/.context/effect/packages/atom/react/test/index.test.tsx @@ -135,6 +135,49 @@ describe("atom-react", () => { expect(screen.getByTestId("loading")).toBeInTheDocument() }) + + test("suspense subscriptions are isolated per registry", async () => { + const atom = Atom.make(AsyncResult.initial()) + const firstRegistry = AtomRegistry.make() + const secondRegistry = AtomRegistry.make() + + function TestComponent({ id }: { readonly id: string }) { + const value = useAtomSuspense(atom).value + return
{value}
+ } + + render( + + Loading...}> + + + + ) + render( + + Loading...}> + + + + ) + + act(() => { + secondRegistry.set(atom, AsyncResult.success("second")) + }) + + await waitFor(() => { + expect(screen.getByTestId("second-value")).toHaveTextContent("second") + }) + expect(screen.getByTestId("first-loading")).toBeInTheDocument() + + act(() => { + firstRegistry.set(atom, AsyncResult.success("first")) + }) + + await waitFor(() => { + expect(screen.getByTestId("first-value")).toHaveTextContent("first") + }) + }) }) describe("ScopedAtom", () => { diff --git a/.context/effect/packages/atom/react/tsconfig.json b/.context/effect/packages/atom/react/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/atom/react/tsconfig.json +++ b/.context/effect/packages/atom/react/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/atom/react/vitest.config.ts b/.context/effect/packages/atom/react/vitest.config.ts deleted file mode 100644 index d9ba9429e..000000000 --- a/.context/effect/packages/atom/react/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { mergeConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -export default mergeConfig(shared, { - test: { - environment: "jsdom", - setupFiles: ["./vitest.setup.ts"] - } -}) diff --git a/.context/effect/packages/atom/solid/CHANGELOG.md b/.context/effect/packages/atom/solid/CHANGELOG.md index 73c4f4e5d..7f0f76e9d 100644 --- a/.context/effect/packages/atom/solid/CHANGELOG.md +++ b/.context/effect/packages/atom/solid/CHANGELOG.md @@ -1,5 +1,57 @@ # @effect/atom-solid +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/atom/solid/README.md b/.context/effect/packages/atom/solid/README.md index a4239bf9c..3e4da49f1 100644 --- a/.context/effect/packages/atom/solid/README.md +++ b/.context/effect/packages/atom/solid/README.md @@ -1,7 +1,14 @@ -# `@effect/atom-solid` +# @effect/atom-solid -SolidJS bindings for the Effect Atom modules. +[SolidJS](https://www.solidjs.com) bindings for Atom, the reactive state management modules for Effect. + +## Installation + +```sh +npm install effect@beta @effect/atom-solid@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-solid). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/atom-solid) diff --git a/.context/effect/packages/atom/solid/docgen.json b/.context/effect/packages/atom/solid/docgen.json deleted file mode 100644 index a0d90fe4d..000000000 --- a/.context/effect/packages/atom/solid/docgen.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "projectHomepage": "https://effect-ts.github.io/effect/docs/atom-solid", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/solid/src/", - "enforceVersion": true, - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/atom/solid/package.json b/.context/effect/packages/atom/solid/package.json index 37a8a470b..8a2481fcd 100644 --- a/.context/effect/packages/atom/solid/package.json +++ b/.context/effect/packages/atom/solid/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-solid", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "SolidJS bindings for the Effect Atom modules", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,26 +50,25 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "peerDependencies": { "effect": "workspace:^", - "solid-js": ">=1 <2" + "solid-js": ">=1.9.14 <2.0.0" }, "devDependencies": { "@solidjs/testing-library": "^0.8.0", "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", + "@testing-library/jest-dom": "^7.0.0", "effect": "workspace:^", - "jsdom": "^29.1.1", + "jsdom": "^30.0.0", "solid-js": "^1.9.14" } } diff --git a/.context/effect/packages/atom/solid/tsconfig.json b/.context/effect/packages/atom/solid/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/atom/solid/tsconfig.json +++ b/.context/effect/packages/atom/solid/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/atom/solid/vitest.config.ts b/.context/effect/packages/atom/solid/vitest.config.ts deleted file mode 100644 index 48bb912d7..000000000 --- a/.context/effect/packages/atom/solid/vitest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mergeConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -export default mergeConfig(shared, { - resolve: { - conditions: ["browser"] - }, - esbuild: { - target: "es2022" - }, - test: { - environment: "jsdom", - setupFiles: ["./vitest.setup.ts"] - } -}) diff --git a/.context/effect/packages/atom/vue/CHANGELOG.md b/.context/effect/packages/atom/vue/CHANGELOG.md index f3c113bdd..b52e9d1ee 100644 --- a/.context/effect/packages/atom/vue/CHANGELOG.md +++ b/.context/effect/packages/atom/vue/CHANGELOG.md @@ -1,5 +1,57 @@ # @effect/atom-vue +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/atom/vue/README.md b/.context/effect/packages/atom/vue/README.md index 5ef604acc..c94be3cd1 100644 --- a/.context/effect/packages/atom/vue/README.md +++ b/.context/effect/packages/atom/vue/README.md @@ -1,7 +1,14 @@ -# `@effect/atom-vue` +# @effect/atom-vue -Vue bindings for the Effect Atom modules. +[Vue](https://vuejs.org) bindings for Atom, the reactive state management modules for Effect. + +## Installation + +```sh +npm install effect@beta @effect/atom-vue@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/atom-vue). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/atom-vue) diff --git a/.context/effect/packages/atom/vue/docgen.json b/.context/effect/packages/atom/vue/docgen.json deleted file mode 100644 index 26f71de45..000000000 --- a/.context/effect/packages/atom/vue/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/atom/vue/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/atom/vue/package.json b/.context/effect/packages/atom/vue/package.json index e83ea9633..e275314ed 100644 --- a/.context/effect/packages/atom/vue/package.json +++ b/.context/effect/packages/atom/vue/package.json @@ -1,6 +1,6 @@ { "name": "@effect/atom-vue", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "Vue bindings for the Effect Atom modules", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,15 +50,14 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^", @@ -62,6 +65,6 @@ }, "peerDependencies": { "effect": "workspace:^", - "vue": "^3.5.0" + "vue": ">=3.5.39 <4.0.0" } } diff --git a/.context/effect/packages/atom/vue/src/index.ts b/.context/effect/packages/atom/vue/src/index.ts index 799c9ebd4..a2f434007 100644 --- a/.context/effect/packages/atom/vue/src/index.ts +++ b/.context/effect/packages/atom/vue/src/index.ts @@ -12,25 +12,25 @@ import { computed, type ComputedRef, inject, type InjectionKey, type Ref, shallo /** * @since 4.0.0 - * @category modules + * @category re-exports */ export * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry" /** * @since 4.0.0 - * @category modules + * @category re-exports */ export * as AsyncResult from "effect/unstable/reactivity/AsyncResult" /** * @since 4.0.0 - * @category modules + * @category re-exports */ export * as Atom from "effect/unstable/reactivity/Atom" /** * @since 4.0.0 - * @category modules + * @category re-exports */ export * as AtomRef from "effect/unstable/reactivity/AtomRef" @@ -42,25 +42,25 @@ export * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi" /** * @since 4.0.0 - * @category modules + * @category re-exports */ export * as AtomRpc from "effect/unstable/reactivity/AtomRpc" /** * @since 4.0.0 - * @category registry + * @category symbols */ export const registryKey = Symbol.for("@effect/atom-vue/registryKey") as InjectionKey /** * @since 4.0.0 - * @category registry + * @category constants */ export const defaultRegistry: AtomRegistry.AtomRegistry = AtomRegistry.make() /** * @since 4.0.0 - * @category registry + * @category accessors */ export const injectRegistry = (): AtomRegistry.AtomRegistry => { return inject(registryKey, defaultRegistry) diff --git a/.context/effect/packages/atom/vue/tsconfig.json b/.context/effect/packages/atom/vue/tsconfig.json index d3687aaaa..dc6af14d2 100644 --- a/.context/effect/packages/atom/vue/tsconfig.json +++ b/.context/effect/packages/atom/vue/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/atom/vue/vitest.config.ts b/.context/effect/packages/atom/vue/vitest.config.ts deleted file mode 100644 index d5870de3d..000000000 --- a/.context/effect/packages/atom/vue/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { mergeConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -export default mergeConfig(shared, { - test: { - environment: "happy-dom" - } -}) diff --git a/.context/effect/packages/effect/CHANGELOG.md b/.context/effect/packages/effect/CHANGELOG.md index 62c049302..db7b9ea22 100644 --- a/.context/effect/packages/effect/CHANGELOG.md +++ b/.context/effect/packages/effect/CHANGELOG.md @@ -1,5 +1,906 @@ # effect +## 4.0.0-rc.108 + +### Patch Changes + +- [#6546](https://github.com/Effect-TS/effect/pull/6546) [`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8) Thanks @xianjianlf2! - Handle BigInt values safely and consistently across JSON diagnostics and logger formats. + +- [#7174](https://github.com/Effect-TS/effect/pull/7174) [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa) Thanks @tim-smart! - Fix `Queue.await` failing with `Cause.Done` when registered before the queue ends. + +- [#7180](https://github.com/Effect-TS/effect/pull/7180) [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75) Thanks @gcanti! - Prioritize redacted representations in formatters and normalize text logger levels to uppercase. + +- [#7193](https://github.com/Effect-TS/effect/pull/7193) [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed) Thanks @kitlangton! - Fix `Deferred.await` dying with a `TypeError` when a waiter is interrupted after the `Deferred` has been completed. + +- [#7179](https://github.com/Effect-TS/effect/pull/7179) [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1) Thanks @tim-smart! - Fix `DurableDeferred.raceAll` so a completed deferred can wake an active workflow without changing success-biased race semantics + +- [#7189](https://github.com/Effect-TS/effect/pull/7189) [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd) Thanks @gcanti! - Fix `HttpApi` query decoding for array parameters with a single value. + +- [#6550](https://github.com/Effect-TS/effect/pull/6550) [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a) Thanks @xianjianlf2! - Return fresh OpenAPI specs from cached `OpenApi.fromApi` calls. + +- [#7188](https://github.com/Effect-TS/effect/pull/7188) [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66) Thanks @gcanti! - Mark the internal `~sentinels` Schema annotation as `@internal` so release declaration stripping removes it together with `SchemaAST.Sentinel`. This keeps the published declarations self-consistent for consumers that type-check dependencies with `skipLibCheck: false`. + +- [#7158](https://github.com/Effect-TS/effect/pull/7158) [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576) Thanks @k3dom! - Improve Union candidate selection: a nested union member is dispatched by the sentinels common to all its members, and candidates whose sentinel the input contradicts are excluded. + +- [#7178](https://github.com/Effect-TS/effect/pull/7178) [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b) Thanks @tim-smart! - Use Promise microtasks for synchronous Scheduler dispatch. + +- [#7181](https://github.com/Effect-TS/effect/pull/7181) [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc) Thanks @gcanti! - Move `SchemaError` into the `Schema` module and remove the standalone `SchemaError` module. + +- [#7195](https://github.com/Effect-TS/effect/pull/7195) [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120) Thanks @tim-smart! - Ensure discarded non-persisted cluster messages complete without waiting for the entity reply. + +- [#7191](https://github.com/Effect-TS/effect/pull/7191) [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887) Thanks @Digifox03! - Fix `HttpRouter.Middleware.layer` to provide request error services for errors declared in `handles`, and expose global + middleware errors from `HttpRouter.toHttpEffect`. + +## 4.0.0-beta.107 + +### Patch Changes + +- [#7156](https://github.com/Effect-TS/effect/pull/7156) [`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8) Thanks @tim-smart! - Terminate active multipart file streams when a parser limit is exceeded or the body ends unexpectedly, so file parts fail instead of hanging. + +- [#7153](https://github.com/Effect-TS/effect/pull/7153) [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb) Thanks @rajanpanth! - Fix `Duration`'s `Hash.symbol` implementation to hash a canonical nanoseconds form instead of the raw internal `Millis`/`Nanos` representation. Two durations that `Duration.equals`/`Equal.equals` consider equal (e.g. `Duration.seconds(5)` and `Duration.nanos(5_000_000_000n)`) previously hashed differently, violating the Hash/Equal contract and silently breaking `HashSet`/`HashMap` lookups keyed by `Duration`. + +- [#7166](https://github.com/Effect-TS/effect/pull/7166) [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018) Thanks @CDVolvik! - Import migrations through a file URL in `Migrator.fromFileSystem`, so absolute Windows paths are accepted by the ESM loader. + + Previously the directory and file name were passed to `import` as a plain path. On Windows that produced a specifier such as `D:\migrations\1_init.ts`, which the ESM loader rejects with `Only URLs with a scheme in: file, data, and node are supported`. + + `fromFileSystem` now resolves the specifier through the `Path` service, so its type widens from `Loader` to `Loader`. Callers that already provide an aggregate platform layer such as `NodeServices.layer` are unaffected; callers that provide `FileSystem` on its own now also need a `Path` layer, and on Windows it must be a platform-aware one rather than the POSIX `Path.layer`. + +- [#7157](https://github.com/Effect-TS/effect/pull/7157) [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede) Thanks @tim-smart! - Add `Channel.mkUint8Array` and reuse it from `Stream` and multipart file collection. This also fixes quadratic buffering in `File.contentEffect`, improving collection of a 16 MiB chunked upload by approximately 90x. + +- [#7149](https://github.com/Effect-TS/effect/pull/7149) [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9) Thanks @gcanti! - Require explicit handling for regular expression pattern constraints translated from JSON Schema documents, with modes to apply trusted patterns or ignore their constraints. + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7110](https://github.com/Effect-TS/effect/pull/7110) [`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7) Thanks @fubhy! - Ensure concurrent first `RcRef` borrowers share the same resource generation. + +- [#7114](https://github.com/Effect-TS/effect/pull/7114) [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651) Thanks @fubhy! - Report buffered worker send failures as `WorkerError` values. + +- [#7117](https://github.com/Effect-TS/effect/pull/7117) [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f) Thanks @fubhy! - Make `TxQueue.shutdown` safe to call after a queue has already been interrupted. + +- [#7119](https://github.com/Effect-TS/effect/pull/7119) [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d) Thanks @fubhy! - Prevent SQL resolvers from invoking non-empty batch callbacks when every request fails encoding. + +- [#7105](https://github.com/Effect-TS/effect/pull/7105) [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266) Thanks @tim-smart! - Add `ConfigProvider.fromEnvRecord` for building a provider from an explicit environment record. + +- [#7111](https://github.com/Effect-TS/effect/pull/7111) [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae) Thanks @fubhy! - Preserve input fiber error types in `Fiber.joinAll`. + +- [#7134](https://github.com/Effect-TS/effect/pull/7134) [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d) Thanks @marbemac! - Fix cluster shutdown hangs by failing abandoned non-discard requests and stream chunk acknowledgements with `EntityNotAssignedToRunner`, including persisted requests sent after runner unregistration. This adds `EntityNotAssignedToRunner` to the typed error channel of entity clients and request-only `EntityProxy` RPC/HTTP endpoints; discard endpoints remain unchanged. + +- [#7107](https://github.com/Effect-TS/effect/pull/7107) [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03) Thanks @fubhy! - Preserve FormData bodies when converting client requests through HttpServerRequest. + +- [#7120](https://github.com/Effect-TS/effect/pull/7120) [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1) Thanks @fubhy! - Fix `SqlResolver.findById` failing to complete duplicate requests when id encoding fails, which surfaced as a `RequestResolver did not complete request` defect instead of the underlying `SchemaError`. + +- [#7131](https://github.com/Effect-TS/effect/pull/7131) [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7) Thanks @fubhy! - Ignore MCP cancellation notifications for unknown request identifiers. + +- [#7104](https://github.com/Effect-TS/effect/pull/7104) [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1) Thanks @gcanti! - Add `Function.memoizeIdempotent` and use it to avoid reprocessing canonical Schema ASTs, including optional and mutable property modifiers. Cache Config schema cursor AST compilation. + +- [#7144](https://github.com/Effect-TS/effect/pull/7144) [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997) Thanks @fubhy! - Stop multipart parsing after part count, part size, or field size limits are exceeded. + +- [#7121](https://github.com/Effect-TS/effect/pull/7121) [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675) Thanks @fubhy! - Prevent execution-plan event observer defects from changing attempt outcomes or leaving attempt events unpaired. + +- [#7147](https://github.com/Effect-TS/effect/pull/7147) [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752) Thanks @tim-smart! - Release worker pool entries when an RPC worker's receive loop fails. + +- [#7148](https://github.com/Effect-TS/effect/pull/7148) [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf) Thanks @gcanti! - Consolidate schema arbitrary derivation into `Schema.toArbitrary`, which now returns a `Schema.Arbitrary` factory that accepts the fast-check module. Remove `Schema.toArbitraryLazy` and arbitrary derivation reports. + +- [#7109](https://github.com/Effect-TS/effect/pull/7109) [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d) Thanks @fubhy! - Fix `RcRef` leaking resources acquired before a failed acquisition. + +- [#7146](https://github.com/Effect-TS/effect/pull/7146) [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f) Thanks @gcanti! - Improve Schema representation identity, anonymous-reference eligibility, and JSON Schema alias finalization. + +- [#6862](https://github.com/Effect-TS/effect/pull/6862) [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999) Thanks @fubhy! - Ensure `ScopedRef.set` releases a replacement when the previous value's finalizer defects. + +- [#7060](https://github.com/Effect-TS/effect/pull/7060) [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6) Thanks @fubhy! - Preserve `maxItems` semantics when importing JSON Schema `prefixItems`. + +- [#7116](https://github.com/Effect-TS/effect/pull/7116) [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b) Thanks @fubhy! - Keep span end times at zero when tracer timing is disabled. + +- [#7124](https://github.com/Effect-TS/effect/pull/7124) [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726) Thanks @fubhy! - Use a distinct AES-GCM initialization vector for each encrypted event log entry. `EventLogEncryption.encrypt` now returns each IV with its ciphertext, and encrypted event log clients and servers must be upgraded together because the `WriteEntries` wire shape changed. + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7087](https://github.com/Effect-TS/effect/pull/7087) [`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4) Thanks @tim-smart! - Recognize tagged Config and RPC errors across duplicated `effect` package copies. + +- [#6827](https://github.com/Effect-TS/effect/pull/6827) [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1) Thanks @jaipaljadeja! - Add bounded 429 retries and custom response header names to `HttpClient.withRateLimiter`. + +- [#7084](https://github.com/Effect-TS/effect/pull/7084) [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8) Thanks @tim-smart! - Stop capturing definition-location stack frames in `Context.Service`. + +- [#7090](https://github.com/Effect-TS/effect/pull/7090) [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2) Thanks @tim-smart! - Expose `stdinIsTerminal` and `stdoutIsTerminal` effects through the `Stdio` service. + +- [#7093](https://github.com/Effect-TS/effect/pull/7093) [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6) Thanks @gcanti! - Add the opt-in `reportInput` parse option for retaining rejected inputs in enumerable fields on value-bearing schema issues and including them in default formatted messages. Value-bearing issue constructors accept the rejected input and parse options directly, and `Schema.Annotations.Issue` now supports `expected` for default messages. + + Schema issues no longer format implicitly through `Issue#toString`. Use `SchemaIssue.makeFormatterDefault()` when a human-readable message is needed. The throwing and Promise-based adapters in `SchemaParser` now use the generic message `"Schema validation failed"` and expose the structured `SchemaIssue.Issue` as the error `cause`; consumers that previously read the formatted error message should inspect and explicitly format that cause instead. + + `Schema.makeEffect` now returns `SchemaIssue.Issue` failures instead of wrapping them in `SchemaError`, and `Schema.withConstructorDefault` accepts an `Effect` that fails with `SchemaIssue.Issue`. Fallible `Optic` operations return structured `SchemaIssue.Issue` failures, while schema failures from `Schema.toIso` and `Schema.toDifferJsonPatch` use the generic error message and preserve the issue in `cause` instead of formatting it internally. + +- [#7097](https://github.com/Effect-TS/effect/pull/7097) [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a) Thanks @tim-smart! - Add `Cron.format` for converting a `Cron` instance to a cron expression, with an option to include the seconds field. + +## 4.0.0-beta.104 + +### Minor Changes + +- [#7076](https://github.com/Effect-TS/effect/pull/7076) [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d) Thanks @tim-smart! - Return the new file offset as a `Size` from `File.seek`. + +### Patch Changes + +- [#6934](https://github.com/Effect-TS/effect/pull/6934) [`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20) Thanks @tim-smart! - httpapi: add typed response headers across handlers, generated clients (including `HttpApiTest`), streaming responses, and OpenAPI with `HttpApiSchema.WithHeaders`. Add `HttpApiSchema.encodeToWithHeaders` for folding response headers into domain types such as error classes. Explicit `content-type` and `content-length` values applied with `HttpServerResponse.setHeader` or `setHeaders` now override body-derived values. + +- [#7044](https://github.com/Effect-TS/effect/pull/7044) [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4) Thanks @fubhy! - Commit SQL event journal entries only after their write callback succeeds. + +- [#6957](https://github.com/Effect-TS/effect/pull/6957) [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322) Thanks @fubhy! - Select Bash completions for the active positional argument. + +- [#6941](https://github.com/Effect-TS/effect/pull/6941) [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56) Thanks @fubhy! - Generate even and odd safe integers in Crypto random APIs. + +- [#6965](https://github.com/Effect-TS/effect/pull/6965) [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5) Thanks @fubhy! - Correct the runtime tag spelling for `CliError.UnknownSubcommand`. + +- [#6963](https://github.com/Effect-TS/effect/pull/6963) [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c) Thanks @fubhy! - Exclude disabled choices from multi-select prompt selection and submission. + +- [#7001](https://github.com/Effect-TS/effect/pull/7001) [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006) Thanks @fubhy! - Keep ordered SQL resolver results aligned when batched request encoding fails. + +- [#6937](https://github.com/Effect-TS/effect/pull/6937) [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924) Thanks @fubhy! - Fix the encoded output type of `TestSchema.Encoding.encodeUnknownEffect`. + +- [#7014](https://github.com/Effect-TS/effect/pull/7014) [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7) Thanks @tim-smart! - Add lightweight INI, YAML, and TOML parsers under `effect/unstable/encoding` and remove their runtime dependencies. + +- [#7053](https://github.com/Effect-TS/effect/pull/7053) [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb) Thanks @fubhy! - Fix arbitrary generation for tuples with multiple optional elements. + +- [#7047](https://github.com/Effect-TS/effect/pull/7047) [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8) Thanks @fubhy! - Fix `Tuple.pick` return types to preserve the requested index order and duplicate indices. + +- [#7066](https://github.com/Effect-TS/effect/pull/7066) [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8) Thanks @fubhy! - Close `ResourceMap` acquisition scopes when a lookup fails. + +- [#7036](https://github.com/Effect-TS/effect/pull/7036) [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7) Thanks @fubhy! - Fix scoped reentrant lock finalizers releasing under the wrong fiber owner. + +- [#6983](https://github.com/Effect-TS/effect/pull/6983) [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a) Thanks @fubhy! - Apply byte range and chunk size options to default Web file responses. + +- [#7071](https://github.com/Effect-TS/effect/pull/7071) [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370) Thanks @fubhy! - Fix MCP sampling metadata optionality and validate it as an object. + +- [#6946](https://github.com/Effect-TS/effect/pull/6946) [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210) Thanks @fubhy! - Defer memoized Layer state installation until Effect execution. + +- [#6943](https://github.com/Effect-TS/effect/pull/6943) [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e) Thanks @fubhy! - Reject zero execution attempts in `ExecutionPlan` steps. + +- [#7026](https://github.com/Effect-TS/effect/pull/7026) [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899) Thanks @tim-smart! - Add execution-plan lifecycle events via an optional `onEvent` handler on `Effect.withExecutionPlan` and `Stream.withExecutionPlan`. + + The handler receives an `ExecutionPlan.Event`, a tagged union of `AttemptStart`, `AttemptSuccess`, and `AttemptFailure`, allowing attempt outcomes to be observed from outside the effect for logging and metrics: + + ```ts + import { Effect } from "effect" + + Effect.withExecutionPlan(program, plan, { + onEvent: (event) => Effect.log("execution plan event", event) + }) + ``` + + Every `AttemptStart` is followed by exactly one terminal event. `AttemptFailure` carries the full failure `Cause`, so defects and interruption are reported as well as expected errors, and terminal events run like finalizers so they are emitted even when the attempt is interrupted. Event numbering matches `ExecutionPlan.CurrentMetadata`: `attempt` is cumulative across steps, while `stepAttempt` is 1-based within the current step. + +- [#7077](https://github.com/Effect-TS/effect/pull/7077) [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0) Thanks @tim-smart! - Rename `Schedule.andThen` and `Schedule.andThenResult` to `Schedule.concat` and `Schedule.concatResult`. + +- [#6975](https://github.com/Effect-TS/effect/pull/6975) [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93) Thanks @fubhy! - Encode SSE events with empty data as dispatchable events. + +- [#7037](https://github.com/Effect-TS/effect/pull/7037) [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86) Thanks @fubhy! - Preserve OTLP metric delta checkpoints when an export fails. + +- [#7057](https://github.com/Effect-TS/effect/pull/7057) [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7) Thanks @fubhy! - Fix the error type exposed by the curried `Sink.catch` overload. + +- [#6947](https://github.com/Effect-TS/effect/pull/6947) [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667) Thanks @fubhy! - Check symbol-keyed properties in Match object patterns. + +- [#6956](https://github.com/Effect-TS/effect/pull/6956) [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5) Thanks @fubhy! - Emit valid CSI sequences from the unstable CLI `cursorTo` helper. + +- [#7008](https://github.com/Effect-TS/effect/pull/7008) [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba) Thanks @tim-smart! - Prevent Bash completions from treating flag values as subcommands. + +- [#7032](https://github.com/Effect-TS/effect/pull/7032) [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812) Thanks @marbemac! - Fix a `@effect/cluster` shutdown deadlock on single-runner topologies (e.g. single-node deployments and `TestRunner`), where `Sharding.sendOutgoing` retried `EntityNotAssignedToRunner` forever during teardown. + +- [#6940](https://github.com/Effect-TS/effect/pull/6940) [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20) Thanks @fubhy! - Omit services removed by `Context.addOrOmit` from the returned context type. + +- [#7065](https://github.com/Effect-TS/effect/pull/7065) [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a) Thanks @tim-smart! - Fix DevTools span requests to preserve their state when queued for sending. + +- [#7016](https://github.com/Effect-TS/effect/pull/7016) [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747) Thanks @brandon-julio-t! - Normalize cluster durable clock wake-up timestamps to whole milliseconds. + +- [#6945](https://github.com/Effect-TS/effect/pull/6945) [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62) Thanks @fubhy! - Preserve integral precision when parsing decimal nano and micro duration inputs + +- [#7050](https://github.com/Effect-TS/effect/pull/7050) [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7) Thanks @fubhy! - Include schedule errors in the error channel of `Effect.schedule` and `Effect.scheduleFrom`. + +- [#7062](https://github.com/Effect-TS/effect/pull/7062) [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2) Thanks @fubhy! - Fix the inspectable JSON identity of `FiberSet`. + +- [#6959](https://github.com/Effect-TS/effect/pull/6959) [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349) Thanks @fubhy! - Match Fish completions against the full nested command path. + +- [#6951](https://github.com/Effect-TS/effect/pull/6951) [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8) Thanks @fubhy! - Use the supplied hash for `HashMap.modifyHash` insertions, updates, and removals. + +- [#6989](https://github.com/Effect-TS/effect/pull/6989) [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e) Thanks @fubhy! - Support standard `BodyInit` values when reading converted client request bodies through `HttpServerRequest`. + +- [#6986](https://github.com/Effect-TS/effect/pull/6986) [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f) Thanks @fubhy! - Synchronize HTTP server response content headers when replacing the body. + +- [#6944](https://github.com/Effect-TS/effect/pull/6944) [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced) Thanks @fubhy! - Make `Iterable.flatten` stack safe across empty iterables. + +- [#6968](https://github.com/Effect-TS/effect/pull/6968) [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf) Thanks @fubhy! - Allow MCP tool calls to omit optional arguments. + +- [#7033](https://github.com/Effect-TS/effect/pull/7033) [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411) Thanks @fubhy! - Fix memory journal conflict detection skipping the first newer entry. + +- [#7034](https://github.com/Effect-TS/effect/pull/7034) [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac) Thanks @fubhy! - Return the first unused remote sequence from the in-memory event journal. + +- [#7042](https://github.com/Effect-TS/effect/pull/7042) [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3) Thanks @fubhy! - Relay entries imported into an in-memory event journal to other remotes. + +- [#7074](https://github.com/Effect-TS/effect/pull/7074) [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1) Thanks @fubhy! - Preserve and update runner health in the in-memory cluster runner storage. + +- [#7038](https://github.com/Effect-TS/effect/pull/7038) [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34) Thanks @fubhy! - Clear in-memory message primary-key indexes when clearing an entity address. + +- [#7005](https://github.com/Effect-TS/effect/pull/7005) [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18) Thanks @fubhy! - Generate valid MSSQL upserts for multi-table persistence. + +- [#6998](https://github.com/Effect-TS/effect/pull/6998) [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635) Thanks @fubhy! - Decode split UTF-8 sequences correctly in NDJSON streams. + +- [#6972](https://github.com/Effect-TS/effect/pull/6972) [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916) Thanks @tim-smart! - Persist a serializable defect when a cluster reply cannot be encoded, preventing persisted entity callers from hanging. + +- [#6962](https://github.com/Effect-TS/effect/pull/6962) [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336) Thanks @fubhy! - Support empty records and non-array iterables in `Prompt.all`. + +- [#7023](https://github.com/Effect-TS/effect/pull/7023) [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf) Thanks @tim-smart! - Rename `RateLimiter.makeSleep` to `RateLimiter.sleep` and support self-first partially applied and uncurried usage. + +- [#7041](https://github.com/Effect-TS/effect/pull/7041) [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006) Thanks @fubhy! - End runner streams after emitting their terminal replies. + +- [#7020](https://github.com/Effect-TS/effect/pull/7020) [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738) Thanks @gcanti! - Fix `Schema.make` to preserve existing nested `Schema.Class` instances, including in array fields, while recursively constructing plain class inputs provided at runtime inside unions. Constructor defaults remain scoped to structural field and element occurrences, with `SchemaAST.Context.constructorDefault` representing the single default link for each occurrence. + + Optimize `Function.memoize` to use a single `WeakMap` lookup for cached values. Its callback no longer accepts `undefined` as a return type because `undefined` represents a cache miss. + + The performance of the two array paths can be reproduced by saving the following program as + `scratchpad/schema-make-6890-benchmark.ts` and running `node scratchpad/schema-make-6890-benchmark.ts` from the repository + root: + + ```ts + import { Schema } from "effect" + import { performance } from "node:perf_hooks" + + class Row extends Schema.Class("Row")({ value: Schema.String }) {} + class DirectTable extends Schema.Class("DirectTable")({ rows: Schema.Array(Row) }) {} + class UnionTable extends Schema.Class("UnionTable")({ rows: Schema.Array(Schema.Union([Row])) }) {} + + const rows = Array.from({ length: 30_000 }, (_, value) => Row.make({ value: String(value) })) + + function benchmark(label: string, make: () => { readonly rows: ReadonlyArray }) { + const samples: Array = [] + for (let i = 0; i < 6; i++) { + const start = performance.now() + const result = make() + samples.push(performance.now() - start) + if (result.rows[0] !== rows[0] || result.rows.at(-1) !== rows.at(-1)) { + throw new Error(`${label} did not preserve Row identity`) + } + } + console.log(`${label}: ${samples.slice(1).map((n) => n.toFixed(3)).join(", ")} ms`) + } + + benchmark("Array(Class)", () => DirectTable.make({ rows })) + benchmark("Array(Union([Class]))", () => UnionTable.make({ rows })) + ``` + + Representative local results on Node 24.12.0 (six runs, with the first discarded): + + ```text + Array(Class): 0.639, 0.498, 0.447, 0.448, 0.451 ms + Array(Union([Class])): 3.141, 2.195, 2.126, 2.108, 2.057 ms + ``` + +- [#7055](https://github.com/Effect-TS/effect/pull/7055) [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250) Thanks @fubhy! - Fix `Stream.slidingSize` to produce the same windows regardless of upstream chunk boundaries. + +- [#6978](https://github.com/Effect-TS/effect/pull/6978) [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830) Thanks @fubhy! - Retain the last SSE event ID across dispatched events. + +- [#6976](https://github.com/Effect-TS/effect/pull/6976) [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3) Thanks @fubhy! - Recognize and ignore a leading UTF-8 byte order mark in server-sent event streams. + +- [#7048](https://github.com/Effect-TS/effect/pull/7048) [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a) Thanks @fubhy! - Ignore malformed retry directives when parsing server-sent event streams. + +- [#7028](https://github.com/Effect-TS/effect/pull/7028) [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38) Thanks @fubhy! - Fix `Trie.insert` to replace existing values without mutating the original trie or increasing its size. + +- [#6973](https://github.com/Effect-TS/effect/pull/6973) [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9) Thanks @fubhy! - Separate the default `VariantSchema` cache from named variant entries. + +- [#7072](https://github.com/Effect-TS/effect/pull/7072) [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748) Thanks @fubhy! - Keep MCP tool calls that return void successful. + +- [#7000](https://github.com/Effect-TS/effect/pull/7000) [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb) Thanks @fubhy! - Close suspended workflow scopes after resumed completion. + +- [#7027](https://github.com/Effect-TS/effect/pull/7027) [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5) Thanks @pawelblaszczyk5! - Prevent Effect.updateService and Effect.updateServiceScoped supertype widening + +- [#6948](https://github.com/Effect-TS/effect/pull/6948) [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc) Thanks @fubhy! - Honor numeric zero time-to-live values in `Cache.make` and `ScopedCache.make`. + +- [#6974](https://github.com/Effect-TS/effect/pull/6974) [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5) Thanks @fubhy! - Handle accepted undefined fields during variant extraction. + +- [#7013](https://github.com/Effect-TS/effect/pull/7013) [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a) Thanks @tim-smart! - Fix several edge cases in the vendored FindMyWay router. + +- [#6949](https://github.com/Effect-TS/effect/pull/6949) [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d) Thanks @fubhy! - Keep TestClock nanosecond access total after infinite adjustments. + +- [#6997](https://github.com/Effect-TS/effect/pull/6997) [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd) Thanks @fubhy! - Isolate compiled SQL fragment caches by compiler instance. + +- [#6960](https://github.com/Effect-TS/effect/pull/6960) [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01) Thanks @fubhy! - Mark omittable CLI flags and arguments as optional in structured help. + +- [#7025](https://github.com/Effect-TS/effect/pull/7025) [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f) Thanks @pawelblaszczyk5! - Prevent Effect.provideServiceEffect supertype widening + +- [#7056](https://github.com/Effect-TS/effect/pull/7056) [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21) Thanks @fubhy! - Fix Map and Set equality allowing a right-side entry to match multiple left-side entries. + +- [#7063](https://github.com/Effect-TS/effect/pull/7063) [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5) Thanks @fubhy! - Preserve literal element types in `Tuple.make`. + +- [#6955](https://github.com/Effect-TS/effect/pull/6955) [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f) Thanks @fubhy! - Include plain variant structs in the default variant union. + +- [#6954](https://github.com/Effect-TS/effect/pull/6954) [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646) Thanks @fubhy! - Preserve CRLF state across SSE input chunk boundaries. + +- [#6950](https://github.com/Effect-TS/effect/pull/6950) [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad) Thanks @fubhy! - Preserve nanosecond precision for large `TestClock` wall-clock timestamps. + +- [#6958](https://github.com/Effect-TS/effect/pull/6958) [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a) Thanks @fubhy! - Preserve hidden command metadata when adding subcommands or shared flags. + +- [#6939](https://github.com/Effect-TS/effect/pull/6939) [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9) Thanks @fubhy! - Preserve sibling provider input evidence when `Config.all` evaluates a failing child. + +- [#6836](https://github.com/Effect-TS/effect/pull/6836) [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17) Thanks @mkdynamic! - Route provider-executed tool results into the assistant message in `Prompt.fromResponseParts` + +- [#7003](https://github.com/Effect-TS/effect/pull/7003) [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36) Thanks @fubhy! - Persist permanent entries in KVS `setMany` operations. + +- [#7039](https://github.com/Effect-TS/effect/pull/7039) [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1) Thanks @fubhy! - Fix failed `ResourceRef` rebuilds permanently blocking waiters. + +- [#6971](https://github.com/Effect-TS/effect/pull/6971) [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8) Thanks @tim-smart! - Serialize concurrent nested SQL transactions to prevent savepoint collisions. Cross-dependent sibling nested + transactions now deadlock instead of interleaving and risking silent data corruption. + +- [#6952](https://github.com/Effect-TS/effect/pull/6952) [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3) Thanks @fubhy! - Register alternate flags used by `Param.orElse` and `Param.orElseResult`. + +- [#6732](https://github.com/Effect-TS/effect/pull/6732) [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172) Thanks @tim-smart! - Rename the Schema error constructors to align with their `Data` counterparts. + + - `Schema.ErrorClass` is now `Schema.Error`. + - `Schema.TaggedErrorClass` is now `Schema.TaggedError`. + - The JavaScript `Error` instance schema is now `Schema.ErrorInstance`. + - `Schema.ErrorReviver` is now `Schema.ErrorInstanceReviver`. + +- [#7068](https://github.com/Effect-TS/effect/pull/7068) [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d) Thanks @tim-smart! - Report retried RPC socket open failures through the `onTransientError` protocol hook and fail in-flight requests when the retry policy is exhausted. + +- [#7006](https://github.com/Effect-TS/effect/pull/7006) [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06) Thanks @fubhy! - Scope custom persisted queue ID deduplication to each named queue. + +- [#6938](https://github.com/Effect-TS/effect/pull/6938) [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351) Thanks @fubhy! - Honor populated variables before dotenv expansion defaults in `ConfigProvider`. + +- [#7040](https://github.com/Effect-TS/effect/pull/7040) [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8) Thanks @fubhy! - Fix `SynchronizedRef.getAndUpdateSome` to update its backing ref. + +- [#7018](https://github.com/Effect-TS/effect/pull/7018) [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da) Thanks @tim-smart! - Hold persisted cluster messages while entity layers are still registering, while retaining a bounded failure when + registration never begins. + +- [#6987](https://github.com/Effect-TS/effect/pull/6987) [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2) Thanks @fubhy! - Map WebSocket send exceptions and transform stream write rejections to typed `SocketError` failures. + +- [#6977](https://github.com/Effect-TS/effect/pull/6977) [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e) Thanks @fubhy! - Default empty Server-Sent Event types to `message`. + +- [#7010](https://github.com/Effect-TS/effect/pull/7010) [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d) Thanks @tim-smart! - Rename `Command.withHidden` to `Command.unlisted`, along with the `hidden` command property which is now `unlisted`. + +- [#7054](https://github.com/Effect-TS/effect/pull/7054) [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5) Thanks @fubhy! - Fix the return type of `Channel.runCount` to expose its numeric result. + +- [#7012](https://github.com/Effect-TS/effect/pull/7012) [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217) Thanks @tim-smart! - Vendor the multipart parser as `effect/unstable/http/MultipartParser`, add the Node.js adapter at `@effect/platform-node/NodeMultipartParser`, and remove the external `multipasta` dependency. + +- [#6953](https://github.com/Effect-TS/effect/pull/6953) [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c) Thanks @fubhy! - Reject truncated MessagePack frames at the end of a stream. + +- [#6961](https://github.com/Effect-TS/effect/pull/6961) [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948) Thanks @fubhy! - Preserve file and directory semantics in CLI completion descriptors. + +- [#6990](https://github.com/Effect-TS/effect/pull/6990) [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a) Thanks @fubhy! - Generate unique persisted paths for multipart files with duplicate filenames. + +- [#7019](https://github.com/Effect-TS/effect/pull/7019) [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191) Thanks @tim-smart! - Round Redis persistence TTLs up to whole milliseconds before passing them to integer-only expiration commands. + +- [#7012](https://github.com/Effect-TS/effect/pull/7012) [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217) Thanks @tim-smart! - Prevent malformed encoded multipart filenames from throwing during parsing. + +- [#7029](https://github.com/Effect-TS/effect/pull/7029) [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731) Thanks @fubhy! - Fix unencrypted event log conflict scanning to inspect the newer history suffix. + +- [#6988](https://github.com/Effect-TS/effect/pull/6988) [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d) Thanks @fubhy! - Preserve lexical ordering in streaming template interpolation. + +- [#6964](https://github.com/Effect-TS/effect/pull/6964) [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a) Thanks @fubhy! - Correct year, ordinal, and meridiem date-mask formatting. + +- [#6966](https://github.com/Effect-TS/effect/pull/6966) [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee) Thanks @fubhy! - Preserve fractional leading zeros while editing float prompts. + +- [#6982](https://github.com/Effect-TS/effect/pull/6982) [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99) Thanks @fubhy! - Reject NDJSON values without a JSON representation. + +- [#6942](https://github.com/Effect-TS/effect/pull/6942) [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693) Thanks @fubhy! - Validate object-based DateTime instants before construction. + +- [#6985](https://github.com/Effect-TS/effect/pull/6985) [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114) Thanks @fubhy! - Preserve original HTTP response bytes when reading response text first. + +## 4.0.0-beta.103 + +### Minor Changes + +- [#6793](https://github.com/Effect-TS/effect/pull/6793) [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923) Thanks @tim-smart! - Add `Semaphore.takeIfAvailable` for non-blocking manual permit acquisition. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - Expose object-shaped Toolkit success schemas as MCP tool output schemas. + +- [#6807](https://github.com/Effect-TS/effect/pull/6807) [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273) Thanks @alecbuffi! - Separate wall-clock timestamps from monotonic elapsed time. + + `Clock.Clock` now requires `monotonicTimeNanosUnsafe()` and `monotonicTimeNanos` for measuring elapsed time. Custom `Clock` implementations must provide both members. The live clock's `currentTimeNanos` now re-anchors its high-resolution Unix wall-clock timestamp when it drifts from `Date.now()`, while `Effect.timed`, duration metric tracking, and `Sink.withDuration` use monotonic time so wall-clock corrections do not distort elapsed durations. + +### Patch Changes + +- [#6697](https://github.com/Effect-TS/effect/pull/6697) [`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944) Thanks @schickling-assistant! - Add a configurable filter for HTTP client request and response header span attributes. + +- [#6883](https://github.com/Effect-TS/effect/pull/6883) [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6) Thanks @gcanti! - Add support for converting JSON Schema documents to Draft-04, preserve literal `$ref` values, `$ref` sibling constraints, `not`, `readOnly`, and `writeOnly` in Draft-07 conversions, correct the Draft-07 meta-schema URI, and prevent OpenAPI component-key collisions during conversion. + +- [#6564](https://github.com/Effect-TS/effect/pull/6564) [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539) Thanks @AVtheking! - Run shared-table SQL persistence expiration cleanup in indexed, bounded background batches. + +- [#6911](https://github.com/Effect-TS/effect/pull/6911) [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe) Thanks @fubhy! - Update existing `HashRing` nodes when adding a value with the same primary key. + +- [#6909](https://github.com/Effect-TS/effect/pull/6909) [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657) Thanks @AlfGoto! - Add `DateTime.toEpochSeconds` and `DateTime.fromEpochSeconds` for converting date-time values to and from Unix epoch seconds. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP tool handler defects now return a stable internal error without exposing defect details. + +- [#6874](https://github.com/Effect-TS/effect/pull/6874) [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006) Thanks @fubhy! - Fix `Equal.equals` and `Hash.hash` to handle invalid dates and `DataView` values without throwing. + +- [#6869](https://github.com/Effect-TS/effect/pull/6869) [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147) Thanks @fubhy! - Fix SQL-backed Persistence `getMany` to preserve duplicate key positions. + +- [#6868](https://github.com/Effect-TS/effect/pull/6868) [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7) Thanks @fubhy! - Ensure clearing an empty Redis-backed persistence store succeeds. + +- [#6903](https://github.com/Effect-TS/effect/pull/6903) [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588) Thanks @fubhy! - Preserve equals signs in inline CLI option values after the first separator. + +- [#6876](https://github.com/Effect-TS/effect/pull/6876) [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15) Thanks @fubhy! - Fix `Sink.reduceWhileArray` applying its reducer more than once per input array. + +- [#6802](https://github.com/Effect-TS/effect/pull/6802) [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92) Thanks @tim-smart! - Cap incomplete RPC frames buffered by the NDJSON and MessagePack streaming decoders, and close socket transports when the limit is exceeded. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - RPC servers now suppress responses after a client cancels an in-flight request. + +- [#6723](https://github.com/Effect-TS/effect/pull/6723) [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e) Thanks @tim-smart! - add platform literal to HttpPlatform + +- [#6788](https://github.com/Effect-TS/effect/pull/6788) [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5) Thanks @gcanti! - Refine the `ConfigProvider` interface so lookup absence uses `undefined` and + path transformation is provider behavior. + + `ConfigProvider.load` and the lookup function accepted by + `ConfigProvider.make` now return `Node | undefined`. Use `undefined` when a path + does not exist and return the `Node` directly when it does. + + `ConfigProvider` now exposes `mapInput` as a capability. The exported + `ConfigProvider.mapInput` combinator delegates to it, preserving transformation + order and composition through `orElse` without requiring provider + representation state. + +- [#6863](https://github.com/Effect-TS/effect/pull/6863) [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9) Thanks @fubhy! - Decode percent-encoded OTLP environment header values. + +- [#6781](https://github.com/Effect-TS/effect/pull/6781) [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9) Thanks @gcanti! - Deduplicate equivalent fallback definitions when compiling JSON Schema, and reconstruct only definitions reachable from multi-document roots. + + Remove `SchemaMultiDocument` and `fromSchemaMultiDocument`; multi-document import and revival now return the ordered root schemas directly. + + Stop the OpenAPI generator from emitting component schemas that are not reachable from a generated root. + +- [#6717](https://github.com/Effect-TS/effect/pull/6717) [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab) Thanks @IMax153! - Document that `CommandOptions.extendEnv` defaults to `false` and that providing `env` without enabling it replaces the inherited child environment. + +- [#6657](https://github.com/Effect-TS/effect/pull/6657) [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7) Thanks @tim-smart! - Use cancellable microtasks when dispatching yielded work from synchronous Effect runs. + +- [#6661](https://github.com/Effect-TS/effect/pull/6661) [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f) Thanks @tim-smart! - Fix hydrated atoms with `Atom.withReactivity` to refresh after reactive mutations. + +- [#6665](https://github.com/Effect-TS/effect/pull/6665) [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd) Thanks @tim-smart! - Fix `HttpRouter.toWebHandler` context inference for services provided by the application layer. + +- [#6681](https://github.com/Effect-TS/effect/pull/6681) [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319) Thanks @tim-smart! - Add Web Stream interoperability for `Channel` and `Sink`, plus byte limiting and `ArrayBuffer` collection for `Stream`. + +- [#6730](https://github.com/Effect-TS/effect/pull/6730) [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30) Thanks @tim-smart! - Support replaying initial WebSocket messages and normalize `ArrayBuffer` frames to `Uint8Array`. + +- [#6763](https://github.com/Effect-TS/effect/pull/6763) [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2) Thanks @tim-smart! - Validate cookie names, domains, and paths before constructing or serializing cookies. + +- [#6771](https://github.com/Effect-TS/effect/pull/6771) [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec) Thanks @tim-smart! - Strip credential headers on cross-origin HTTP redirects and align redirected request methods with fetch. + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6772](https://github.com/Effect-TS/effect/pull/6772) [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c) Thanks @tim-smart! - Reject empty, `.` and `..` keys in file-backed key-value stores. + +- [#6773](https://github.com/Effect-TS/effect/pull/6773) [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37) Thanks @tim-smart! - Escape terminal control characters in unstable CLI error output. + +- [#6898](https://github.com/Effect-TS/effect/pull/6898) [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b) Thanks @tim-smart! - Add HTTP response compression support. Node.js, Bun, and Deno use asynchronous + `node:zlib` one-shot compression for byte-array bodies, preserving an exact + `Content-Length`; stream and raw bodies remain streaming transforms. + +- [#6859](https://github.com/Effect-TS/effect/pull/6859) [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f) Thanks @fubhy! - Fix `String.snakeToCamel` and `String.snakeToPascal` to return an empty string for empty input. + +- [#6746](https://github.com/Effect-TS/effect/pull/6746) [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff) Thanks @fubhy! - Prefer explicit OTLP resource configuration over environment configuration. + +- [#6677](https://github.com/Effect-TS/effect/pull/6677) [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7) Thanks @tim-smart! - Expose runtime schemas for AI prompt parts and message-specific part unions. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now advertise logging and honor each client's selected log level when sending log notifications. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - Preserve MCP sampling request preferences and response content. + +- [#6878](https://github.com/Effect-TS/effect/pull/6878) [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e) Thanks @fubhy! - Fix Array index operations handling `NaN` and fractional indexes. + +- [#6751](https://github.com/Effect-TS/effect/pull/6751) [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b) Thanks @tim-smart! - Fix Atom dependency tracking and re-entrant invalidation during batch rebuilds. + +- [#6870](https://github.com/Effect-TS/effect/pull/6870) [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4) Thanks @fubhy! - Ensure `BigInt.gcd` and `BigInt.lcm` return non-negative values and handle zero operands in `BigInt.lcm`. + +- [#6844](https://github.com/Effect-TS/effect/pull/6844) [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772) Thanks @fubhy! - Prevent an interrupted cache lookup from removing a newer value written with `Cache.set`. + +- [#6879](https://github.com/Effect-TS/effect/pull/6879) [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e) Thanks @fubhy! - Preserve failure annotations when mapping errors with `Cause.map`. + +- [#6820](https://github.com/Effect-TS/effect/pull/6820) [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd) Thanks @fubhy! - Fix `ChannelSchema.decodeUnknown` to accept unknown input chunks while keeping `ChannelSchema.decode` typed to the schema's encoded input. + +- [#6899](https://github.com/Effect-TS/effect/pull/6899) [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43) Thanks @fubhy! - Ensure `Chunk.take` and `Chunk.drop` produce valid chunks for fractional counts. + +- [#6579](https://github.com/Effect-TS/effect/pull/6579) [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7) Thanks @marbemac! - Scope cluster reply serialization failures and peer-delivered defects to their own request instead of the whole runner connection + +- [#6800](https://github.com/Effect-TS/effect/pull/6800) [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada) Thanks @tim-smart! - Fix persisted cluster stream recovery when SQL drivers return a null reply kind. + +- [#6814](https://github.com/Effect-TS/effect/pull/6814) [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204) Thanks @gcanti! - Preserve provider input evidence when `Config.orElse` recovers a configuration failure. + +- [#6873](https://github.com/Effect-TS/effect/pull/6873) [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3) Thanks @fubhy! - Propagate the `FiberSet.runtime` interruption option when registering managed fibers. + +- [#6872](https://github.com/Effect-TS/effect/pull/6872) [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024) Thanks @fubhy! - Fix `Formatter.format` handling of shared references and ensure `Formatter.formatJson` always returns valid JSON. + +- [#6867](https://github.com/Effect-TS/effect/pull/6867) [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90) Thanks @fubhy! - Remove stale `content-length` headers when replacing an HTTP client request body with one of unknown length. + +- [#6924](https://github.com/Effect-TS/effect/pull/6924) [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab) Thanks @fubhy! - Ignore `uniqueItems` when set to `false` while importing JSON Schema documents. + +- [#6871](https://github.com/Effect-TS/effect/pull/6871) [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70) Thanks @fubhy! - Fix `LayerMap` preload options so configured entries are acquired during construction. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP completion handlers now receive resolved argument context, and completion responses are limited to one hundred values. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now return protocol errors for invalid tool, prompt, completion, resource, and logging requests. + +- [#6901](https://github.com/Effect-TS/effect/pull/6901) [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984) Thanks @fubhy! - Prevent distinct metric attribute sets from sharing registry state. + +- [#6822](https://github.com/Effect-TS/effect/pull/6822) [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4) Thanks @fubhy! - Fix `Metric.isMetric` to recognize metrics using their current runtime brand. + +- [#6821](https://github.com/Effect-TS/effect/pull/6821) [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f) Thanks @fubhy! - Fix `Metric.linearBoundaries` to space boundaries by the configured width. + +- [#6847](https://github.com/Effect-TS/effect/pull/6847) [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91) Thanks @fubhy! - Fix `MutableList.prepend` on empty lists and handle non-positive `toArrayN` bounds. + +- [#6865](https://github.com/Effect-TS/effect/pull/6865) [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570) Thanks @fubhy! - Fix `OtlpResource` to decode percent-encoded environment attributes and preserve bigint precision. + +- [#6805](https://github.com/Effect-TS/effect/pull/6805) [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819) Thanks @tim-smart! - Prevent replay-enabled PubSubs from retaining values beyond each subscription's replay window. + +- [#6711](https://github.com/Effect-TS/effect/pull/6711) [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786) Thanks @andrskr! - Preserve serialization and retention metadata on reactive `AtomRpc` and `AtomHttpApi` queries. + +- [#6855](https://github.com/Effect-TS/effect/pull/6855) [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4) Thanks @fubhy! - Fix `Schedule.during` to recur until the configured duration has elapsed. + +- [#6712](https://github.com/Effect-TS/effect/pull/6712) [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322) Thanks @gcanti! - Make `Schema.isPattern` deterministic for regular expressions with global or sticky flags. + +- [#6782](https://github.com/Effect-TS/effect/pull/6782) [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe) Thanks @gcanti! - SchemaRepresentation: generate references from encoded AST identity, suffix colliding identifiers instead of throwing, and preserve sharing across property-key context. This avoids false-positive duplicate identifier errors while keeping referentially distinct schemas addressable; generated fallback definitions now use the clearer `Encoded` suffix. + +- [#6704](https://github.com/Effect-TS/effect/pull/6704) [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf) Thanks @gcanti! - Fix Union candidate selection for recovering middleware and suspended members. + +- [#6848](https://github.com/Effect-TS/effect/pull/6848) [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014) Thanks @fubhy! - Keep the current `ScopedRef` resource alive when acquiring its replacement fails. + +- [#6910](https://github.com/Effect-TS/effect/pull/6910) [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676) Thanks @z4p5a9! - Fix `Semaphore.withPermits` leaking permits when interrupted between acquiring them and installing their release. + +- [#6877](https://github.com/Effect-TS/effect/pull/6877) [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68) Thanks @tim-smart! - Fix `Stream.aggregateWithin` and `Stream.groupedWithin` retaining fiber continuations on every schedule tick while upstream is idle. + +- [#6889](https://github.com/Effect-TS/effect/pull/6889) [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8) Thanks @tim-smart! - Fix `Stream.withExecutionPlan` retry limits resetting after partial stream emissions. + +- [#6823](https://github.com/Effect-TS/effect/pull/6823) [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2) Thanks @fubhy! - Fix data-first dispatch for `Stream.mapAccumArrayEffect`. + +- [#6900](https://github.com/Effect-TS/effect/pull/6900) [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289) Thanks @fubhy! - Ensure `Stream.range` emits the full range when the chunk size is zero. + +- [#6849](https://github.com/Effect-TS/effect/pull/6849) [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3) Thanks @fubhy! - Fix `SubscriptionRef.getAndUpdateSome` to return the current value when no update is selected. + +- [#6808](https://github.com/Effect-TS/effect/pull/6808) [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596) Thanks @fubhy! - Fix `SubscriptionRef.getAndUpdateEffect` to execute the effectful update. + +- [#6862](https://github.com/Effect-TS/effect/pull/6862) [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999) Thanks @fubhy! - Fix `Trie.longestPrefixOf` returning a valued sibling that does not match the input key. + +- [#6856](https://github.com/Effect-TS/effect/pull/6856) [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250) Thanks @fubhy! - Fix `Trie` to preserve entries whose value is `undefined`. + +- [#6850](https://github.com/Effect-TS/effect/pull/6850) [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8) Thanks @fubhy! - Fix `TxPubSub.publishAll` dropping values from one-shot iterables when a transaction retries. + +- [#6851](https://github.com/Effect-TS/effect/pull/6851) [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3) Thanks @fubhy! - Ensure `TxQueue.poll` and `TxQueue.clear` complete a closing queue after draining its buffered items. + +- [#6853](https://github.com/Effect-TS/effect/pull/6853) [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6) Thanks @fubhy! - Fix `TxQueue.offerAll` to preserve one-shot iterables across transaction retries and repeated runs. + +- [#6783](https://github.com/Effect-TS/effect/pull/6783) [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103) Thanks @tim-smart! - Propagate trace context through persisted cluster workflow requests. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now return standard JSON-RPC errors for malformed requests, unknown methods, and invalid parameters. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now enforce revision-specific JSON-RPC batch and protocol-version header requirements. + +- [#6707](https://github.com/Effect-TS/effect/pull/6707) [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49) Thanks @gcanti! - Mark `Schema.UnknownFromJsonString` as internal and remove its type-level interface. Use `Schema.fromJsonString(Schema.Unknown)` instead. Add `reviver`, callback or array `replacer`, and `space` options to `Schema.fromJsonString`, and make `SchemaTransformation.fromJsonString` a configurable factory. + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. + +- [#6780](https://github.com/Effect-TS/effect/pull/6780) [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac) Thanks @tim-smart! - Include typed tool output schemas in MCP `tools/list` responses. + +- [#6733](https://github.com/Effect-TS/effect/pull/6733) [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e) Thanks @gcanti! - Avoid validating `Schema.Class` fields twice when decoding. + +- [#6659](https://github.com/Effect-TS/effect/pull/6659) [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff) Thanks @tim-smart! - Preserve prototype accessors when code is compiled with loose object spread transforms. + +- [#6912](https://github.com/Effect-TS/effect/pull/6912) [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21) Thanks @gcanti! - Remove `actual` fields from every `SchemaIssue` variant, together with + `SchemaIssue.getActual`, `SchemaIssue.redact`, and `Schema.redact`. Built-in + formatters now use static messages that do not interpolate rejected input, + while paths, AST metadata, union successes, and user-provided messages and + annotations are preserved unchanged. + + Runtime performance was measured across the 16 Effect fixtures in the + `schema-benchmarks` suite. These are the scenarios used for the cross-library + comparison with Valibot and Zod. The paired HEAD-versus-`main` run classified 3 + fixtures as improvements, 0 as regressions, and 13 as inconclusive. Negative + changes are faster. Absolute library values are medians from the same + cross-library run; `—` means that the corresponding adapter does not expose + that scenario. + + | Scenario | Effect (ns/op) | Valibot (ns/op) | Zod (ns/op) | HEAD vs main | Classification | + | ------------------------ | -------------: | --------------: | ----------: | -----------: | -------------- | + | `initialization-schema` | 108191.30 | **30549.81** | 212715.66 | -0.92% | inconclusive | + | `initialization-decoder` | **109796.34** | — | — | +1.98% | inconclusive | + | `validation-valid` | 5221.80 | **5070.81** | — | +2.06% | inconclusive | + | `validation-invalid` | 1279.77 | **234.92** | — | +0.59% | inconclusive | + | `parsing-all-valid` | **5144.58** | 5192.19 | 7176.19 | -3.79% | inconclusive | + | `parsing-all-invalid` | **7594.49** | 15236.82 | 37780.35 | -5.94% | improvement | + | `parsing-first-valid` | 5188.33 | **5135.75** | — | -1.49% | inconclusive | + | `parsing-first-invalid` | 1330.82 | **243.64** | — | +1.01% | inconclusive | + | `standard-all-valid` | 5722.01 | 5200.05 | **3801.26** | -1.78% | inconclusive | + | `standard-all-invalid` | **12024.65** | 15528.50 | 30982.17 | -7.78% | improvement | + | `standard-first-valid` | **5655.33** | — | — | +3.84% | inconclusive | + | `standard-first-invalid` | **2001.69** | — | — | -4.56% | inconclusive | + | `codec-typed-encode` | 342.59 | — | **39.29** | -7.62% | inconclusive | + | `codec-typed-decode` | 418.78 | — | **50.14** | -10.89% | improvement | + | `codec-unknown-encode` | **328.38** | — | — | -5.55% | inconclusive | + | `codec-unknown-decode` | **347.35** | — | — | -5.25% | inconclusive | + +- [#6692](https://github.com/Effect-TS/effect/pull/6692) [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2) Thanks @schickling-assistant! - Fix unstable CLI subcommands dropping operands after the `--` end-of-options terminator. + +- [#6625](https://github.com/Effect-TS/effect/pull/6625) [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376) Thanks @lloydrichards! - Add adapter-valued MCP server protocol declarations, route requests through the selected protocol before schema decoding, and add built-in support for MCP `2025-06-18`. + +- [#6864](https://github.com/Effect-TS/effect/pull/6864) [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe) Thanks @fubhy! - Honor HTTP-date `Retry-After` values when retrying OTLP exports. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP Streamable HTTP servers now validate content negotiation, session lifecycle, negotiated protocol versions, and browser Origins before dispatching requests. + +- [#6824](https://github.com/Effect-TS/effect/pull/6824) [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d) Thanks @tim-smart! - Skip HTTP server span attribute collection when the span is not sampled. + +- [#6814](https://github.com/Effect-TS/effect/pull/6814) [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204) Thanks @gcanti! - Refine `Config` loading and absence semantics. `Config.schema` now derives a provider loading policy from the encoded `StringTree` schema, materializes mixed-shape union members independently, and leaves separated scalar parsing to `Config.Array` and `Config.Record`. Schemas whose canonical `StringTree` encoding remains opaque, such as `Schema.Any`, `Schema.Unknown`, or `Schema.Json`, are rejected when the config is constructed; use a concrete shape or `Schema.fromJsonString(Schema.Json)` for scalar JSON. Missing or unavailable representations are decoded as `undefined` before `Config.withDefault` and `Config.option` decide absence. Partially supplied `Config.all` groups are rejected, successful values such as `undefined` and explicitly present empty structures are preserved, and the internal path prefix is removed from the public `Config.parse` signature. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now refresh roots after capable clients report that their root list changed. + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Remove `Context.mutate` and `Context.getReferenceUnsafe`. Context updates now use overlays, and `Context.get` resolves reference defaults. + +- [#6649](https://github.com/Effect-TS/effect/pull/6649) [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4) Thanks @gcanti! - Remove the `keyValueCombiner` option from `Schema.Record` and the corresponding + `SchemaAST.KeyValueCombiner` and `SchemaAST.IndexSignature.merge` APIs. + For transformed key collisions, sequential parsing keeps the later selected + value, while concurrent parsing keeps the value applied last in completion + order. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP servers now support session-scoped resource subscriptions on transports that can deliver server notifications and filter resource updates by each client's subscribed URIs. + +- [#6649](https://github.com/Effect-TS/effect/pull/6649) [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4) Thanks @gcanti! - Preserve untouched `Result` branches by identity in `Result.map` and + `Result.mapError`. + +- [#6649](https://github.com/Effect-TS/effect/pull/6649) [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4) Thanks @gcanti! - Improve Schema parsing, schema construction and adapter runtime performance + while preserving current parsing behavior. + + ## Runtime performance + + The `effect@beta`, Valibot and Zod timing cases from + [`open-circle/schema-benchmarks`](https://github.com/open-circle/schema-benchmarks) + were reproduced as a dedicated `runtimeperf` suite. The table includes every + case exposed by each upstream adapter; `—` means that the adapter does not + provide that benchmark. + + Effect `main` (`45e781088`) and the branch based on `d775bf4b2` were compared + with five paired processes per case, 150 ms measurement time and 50 ms warmup. + The two initially inconclusive Effect cases were repeated with 15 paired + processes, 500 ms measurement time and 150 ms warmup. Valibot and Zod values + use five processes, 300 ms measurement time and 100 ms warmup. Environment: + Node `v24.12.0`, macOS arm64, Apple M3. + + Zod parsing uses `safeParse` with `{ jitless: true }`; its Standard Schema and + codec cases use the corresponding native adapter APIs. All values are median + microseconds per operation (`µs/op`), lower is better. Cross-library values are + diagnostic because they are independent rather than paired measurements. + + | Scenario | Effect `main` | Effect branch | Valibot | Zod 4 | Delta | 95% CI | Classification | + | ------------------------------------ | ------------: | ------------: | ---------: | ---------: | ------: | ------------------ | -------------- | + | Initialize schema | 137.28 | 118.23 | **40.24** | 318.56 | -12.69% | -21.02% to -5.35% | improvement | + | Initialize schema and decoder | 144.81 | **130.50** | — | — | -10.88% | -14.22% to -3.29% | improvement | + | Validate valid product | 8.478 | **5.415** | 5.63 | — | -35.18% | -41.65% to -32.83% | improvement | + | Validate invalid product | 1.516 | 1.348 | **0.2431** | — | -11.59% | -13.81% to -6.31% | improvement | + | Parse valid product, all errors | 8.360 | 5.366 | **5.22** | 7.16 | -36.28% | -54.41% to -31.67% | improvement | + | Parse invalid product, all errors | 11.302 | **9.100** | 15.70 | 41.58 | -19.42% | -21.32% to -13.12% | improvement | + | Parse valid product, first error | 8.201 | **5.294** | 5.37 | — | -35.44% | -37.75% to -34.59% | improvement | + | Parse invalid product, first error | 1.510 | 1.352 | **0.2572** | — | -10.51% | -12.52% to -9.53% | improvement | + | Standard Schema valid, all errors | 9.284 | 5.935 | 5.35 | **3.83** | -35.96% | -53.29% to -33.49% | improvement | + | Standard Schema invalid, all errors | 16.718 | **15.203** | 16.51 | 32.85 | -11.31% | -13.97% to -7.65% | improvement | + | Standard Schema valid, first error | 8.889 | **5.843** | — | — | -34.17% | -35.13% to -33.94% | improvement | + | Standard Schema invalid, first error | 2.435 | **2.244** | — | — | -8.44% | -12.76% to -4.82% | improvement | + | Typed codec encode | 0.4692 | 0.3420 | — | **0.0405** | -27.60% | -32.35% to -22.50% | improvement | + | Typed codec decode | 0.5191 | 0.3762 | — | **0.0463** | -27.19% | -34.75% to -22.71% | improvement | + | Unknown codec encode | 0.4910 | **0.3472** | — | — | -28.58% | -30.42% to -27.59% | improvement | + | Unknown codec decode | 0.5061 | **0.3637** | — | — | -29.26% | -29.82% to -21.70% | improvement | + + Overall Effect classification: 16 improvements and no regressions. + +- [#6896](https://github.com/Effect-TS/effect/pull/6896) [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5) Thanks @tim-smart! - Bind event-log read and write requests to the identities authenticated on their RPC connection. + +- [#6735](https://github.com/Effect-TS/effect/pull/6735) [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444) Thanks @gcanti! - Fix three issues in the public `Optic` API: + + - Composed `Iso` and `Prism` setters no longer try to read a source value before writing. + - Calling `notUndefined` on an `Optional` now returns an `Optional`, because writing can still fail. + - The internal `node` property is no longer exposed by public optic types. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6875](https://github.com/Effect-TS/effect/pull/6875) [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452) Thanks @fubhy! - Fix protobuf serialization of negative signed integers to use ten-byte two's-complement varints. + +- [#6696](https://github.com/Effect-TS/effect/pull/6696) [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9) Thanks @tim-smart! - remove file descriptor type + +- [#6860](https://github.com/Effect-TS/effect/pull/6860) [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a) Thanks @fubhy! - Honor custom split and strip regular expressions passed to `String.noCase`. + +- [#6866](https://github.com/Effect-TS/effect/pull/6866) [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c) Thanks @fubhy! - Fix partial file-backed HTTP bodies to report the selected byte range as their content length. + +- [#6693](https://github.com/Effect-TS/effect/pull/6693) [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046) Thanks @lloydrichards! - MCP HTTP servers now reject requests sent before initialization with the required lifecycle response. + +- [#6759](https://github.com/Effect-TS/effect/pull/6759) [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea) Thanks @tim-smart! - Harden JSON-RPC wire message classification against inherited properties. + +- [#6705](https://github.com/Effect-TS/effect/pull/6705) [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146) Thanks @tylergibbs1! - Restore the `recursive` option for `FileSystem.watch`, with non-recursive watching as the default. + +- [#6798](https://github.com/Effect-TS/effect/pull/6798) [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0) Thanks @tim-smart! - Namespace PostgreSQL advisory shard locks by the `SqlRunnerStorage` table prefix. + + This changes the advisory-lock protocol. PostgreSQL clusters using advisory locks require a full cluster stop before upgrading; a rolling deploy is unsafe because old and new runners use different lock keys and can both acquire the same shard. + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6563](https://github.com/Effect-TS/effect/pull/6563) [`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef) Thanks @tim-smart! - unstable/reactivity Atom: add `withEquality` combinator for customizing how the registry detects value changes + +- [#6574](https://github.com/Effect-TS/effect/pull/6574) [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628) Thanks @tim-smart! - unstable/http HttpClientRequest: add `updateHeaders` and `removeHeader` combinators for transforming or removing request headers, closes [#6271](https://github.com/Effect-TS/effect/issues/6271) + +- [#6641](https://github.com/Effect-TS/effect/pull/6641) [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128) Thanks @tim-smart! - Add manual flushing to the OTLP exporters through a shared `Flusher` service exposed by each signal layer. The signal layer output types now include `Flusher`, and `OtlpExporter.make` requires it so custom exporters register unconditionally. + +- [#6616](https://github.com/Effect-TS/effect/pull/6616) [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065) Thanks @tim-smart! - Add `Tool.setNeedsApproval` for replacing the approval policy of an existing tool. + +- [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999) Thanks @IMax153! - Add `Effect.updateServiceScoped` for updating a context service until the current scope closes, with customizable reset behavior. + +- [#6593](https://github.com/Effect-TS/effect/pull/6593) [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6) Thanks @tim-smart! - Fix `Channel.mergeAll` to propagate outer failures promptly and interrupt active inner channels. + +- [#6610](https://github.com/Effect-TS/effect/pull/6610) [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f) Thanks @ebramanti! - Update `McpServer.layerHttp` to return `405` for unsupported HTTP methods, reject unsupported `MCP-Protocol-Version` headers with `400`, and return an empty `202` for accepted notifications and responses. + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6599](https://github.com/Effect-TS/effect/pull/6599) [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470) Thanks @tim-smart! - Interrupt in-flight stream pulls when closing an async iterator. + +- [#6638](https://github.com/Effect-TS/effect/pull/6638) [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c) Thanks @tim-smart! - Fix `PartitionedSemaphore.take` leaking partially acquired permits when interrupted. + +- [#6615](https://github.com/Effect-TS/effect/pull/6615) [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5) Thanks @tim-smart! - Expose the tool call ID to AI tool handlers and `Toolkit.WithHandler.handle` wrappers. + +- [#6561](https://github.com/Effect-TS/effect/pull/6561) [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5) Thanks @hsubra89! - Reject unexpected positional arguments left after command parsing, including values exceeding `Argument.variadic` maximum bounds. + +- [#6613](https://github.com/Effect-TS/effect/pull/6613) [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123) Thanks @tim-smart! - Ignore duplicate chunk indexes when joining event log messages. + +- [#6552](https://github.com/Effect-TS/effect/pull/6552) [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5) Thanks @xianjianlf2! - Fix a race where FiberHandle.clear could remove a newer fiber installed while the previous fiber was still interrupting. + +- [#6598](https://github.com/Effect-TS/effect/pull/6598) [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d) Thanks @tim-smart! - Fix `LanguageModel.streamText` to apply the configured concurrency limit to tool call resolution, including approval checks. + +- [#6637](https://github.com/Effect-TS/effect/pull/6637) [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f) Thanks @tim-smart! - Fix Latch open/release resuming waiters that registered after a subsequent close. + + `Latch.open` and `Latch.release` schedule the waiter flush on the fiber's + dispatcher. Previously the flush drained whatever waiters existed at flush + time, so a waiter that registered after the latch was closed again could be + resumed by the stale flush. The waiters are now snapshotted at schedule time, + so only waiters covered by an `open`/`release` call are resumed. + +- [#6614](https://github.com/Effect-TS/effect/pull/6614) [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a) Thanks @tim-smart! - Fix histogram and summary maximum values for negative-only observations. + +- [#6634](https://github.com/Effect-TS/effect/pull/6634) [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252) Thanks @fubhy! - Fix OTLP exporter shutdown to await in-flight and final buffered exports up to the configured shutdown timeout. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- [#6592](https://github.com/Effect-TS/effect/pull/6592) [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a) Thanks @tim-smart! - Fix `Stream.haltWhen` to observe halt effects at pull boundaries for synchronous streams. + +- [#6618](https://github.com/Effect-TS/effect/pull/6618) [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e) Thanks @tim-smart! - unstable/cluster: hash over-length SQL message deduplication keys to prevent `message_id` overflow, closes [#6317](https://github.com/Effect-TS/effect/issues/6317). + + The composed request deduplication key (`entityType/entityId/tag/primaryKey`) can legally exceed the 255-character `message_id` column — the address columns alone allow 458 characters before the RPC primary key is appended. `SqlMessageStorage` now stores a SHA-256 digest (64 hex characters) of the composed key in the unique `message_id` column when the key exceeds 255 characters, so keys of any length work on PostgreSQL, MySQL, MSSQL, and SQLite. Keys that fit are stored as plaintext, byte-compatible with rows written by previous versions, so existing deployments keep deduplicating with no migration or schema change. + + `SqlMessageStorage.layer`/`layerWith` (and consequently `SingleRunner.layer`) now require `Crypto.Crypto`. The Node and Bun cluster convenience layers provide the platform Crypto implementation internally, so their requirements are unchanged. + +- [#6577](https://github.com/Effect-TS/effect/pull/6577) [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c) Thanks @tim-smart! - ManagedRuntime: add `Symbol.asyncDispose`, enabling `await using` syntax + + ```ts + import { Effect, Layer, ManagedRuntime } from "effect"; + + await using runtime = ManagedRuntime.make(Layer.empty); + + await runtime.runPromise(Effect.log("Hello, world!")); + // runtime is disposed automatically at the end of the scope + ``` + +- [#6644](https://github.com/Effect-TS/effect/pull/6644) [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580) Thanks @gcanti! - Improve the performance of `Array.dedupe`, `Array.union`, `Array.intersection`, `Array.difference`, and Schema unique item validation by using hash-based equality lookup. + +- [#6609](https://github.com/Effect-TS/effect/pull/6609) [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260) Thanks @tim-smart! - Allow embedding usage input tokens to be omitted during decoding, including after JSON serialization. + +- [#6606](https://github.com/Effect-TS/effect/pull/6606) [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b) Thanks @tim-smart! - Allow optional AI response fields to be omitted during decoding, including after JSON serialization. + +- [#6607](https://github.com/Effect-TS/effect/pull/6607) [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333) Thanks @ebramanti! - Prevent MCP tool failures from exposing Cause rendering, stack traces, and internal paths while preserving actionable validation messages. + +- [#6576](https://github.com/Effect-TS/effect/pull/6576) [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1) Thanks @tim-smart! - Record: make `fromIterableBy` dual, allowing data-last usage in `pipe` + + ```ts + import { pipe, Record } from "effect"; + + const users = [ + { id: "2", name: "name2" }, + { id: "1", name: "name1" }, + ]; + + pipe( + users, + Record.fromIterableBy((user) => user.id), + ); + ``` + +- [#6622](https://github.com/Effect-TS/effect/pull/6622) [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85) Thanks @gcanti! - Remove the experimental `SchemaUtils` module and its `getNativeClassSchema` helper. The helper duplicated a composition already available through the primary Schema APIs and did not justify a separate public module. + +- [#6653](https://github.com/Effect-TS/effect/pull/6653) [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7) Thanks @tim-smart! - Remove `Effect.withConcurrency`, the `References.CurrentConcurrency` reference backing it, and the `"inherit"` option from `Types.Concurrency`. Use an explicit `number` or `"unbounded"` concurrency value instead. + +- [#6620](https://github.com/Effect-TS/effect/pull/6620) [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0) Thanks @gcanti! - Make `Schema.Date` reject invalid dates and remove the redundant `Schema.DateValid`, `Schema.isDateValid`, and `Schema.isDateValidReviver` APIs. + + `Schema.DateFromString` and `Schema.DateFromMillis` now fail decoding when their input would produce an invalid date. + + Remove `Schema.Annotations.ToArbitrary.GenerationConstraint.valid`; `Schema.Date` arbitraries now generate only valid dates by default. + +- [#6575](https://github.com/Effect-TS/effect/pull/6575) [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375) Thanks @gcanti! - Schema: make schemas directly extendable as classes with static method support + and remove `Schema.asClass`. + + `Bottom` and `BottomLazy` now include the class-compatible `new` signature, + while `BottomWithoutNew` and `BottomLazyWithoutNew` expose the schema protocol + without it for schema types that define a specialized construct signature. + + **Example** + + ```ts + import { Schema } from "effect"; + + class MyString extends Schema.String { + static readonly decodeUnknownSync = Schema.decodeUnknownSync(this); + } + + MyString.decodeUnknownSync("a"); // "a" + ``` + +- [#6424](https://github.com/Effect-TS/effect/pull/6424) [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20) Thanks @gcanti! - Refactor the `SchemaRepresentation` module to improve clarity and maintainability. + + The representation pipeline is now open and compiler-extensible. The same encoded-side representation is used for JSON persistence, runtime reconstruction, JSON Schema Draft 2020-12 compilation, TypeScript code generation, AI structured output, and HTTP / OpenAPI schemas. + + ### New representation model + - Add `RepresentationAnnotation` and `CheckRepresentationAnnotation`, which identify declarations and checks with a stable `id`, JSON `payload`, and optional schema dependencies. + - Preserve checks on every non-reference representation node instead of storing constraints in the previous closed `meta` unions. + - Add compiler hooks for checks and declarations through `SchemaRepresentation.ToJsonSchema` and `SchemaRepresentation.Generation`. + - Add `SchemaMultiDocument`, `fromSchemaMultiDocument`, and `fromRepresentations` so several live schemas and named definitions can be converted and reconstructed together. Explicit definitions are preserved even when no root references them. + - Preserve shared structural nodes, annotated recursion, union member order, identifiers, reference siblings, and structural checks when projecting encoded schemas. + + ### Persistence and revivers + - Add `toJson`, `fromJson`, `toJsonMultiDocument`, and `fromJsonMultiDocument` as the persistence boundary for representation documents. + - Live representations store literal, enum, and property-name scalars as native values. JSON persistence encodes them as `{ type, value }` tagged unions so their runtime types remain distinct across persistence formats, canonically encodes structural bigint and global symbol values, keeps JSON-valued annotations, and removes runtime-only callbacks and other non-JSON annotation values. + - Replace the generic reviver callback with typed `DeclarationReviver`, `FilterReviver`, and `FilterGroupReviver` contracts. Add `makeDeclarationReviver`, `makeFilterReviver`, and `makeFilterGroupReviver`, which infer their payload type from `payloadSchema`. + - Resolve acyclic references to concrete runtime schemas and reserve `Schema.suspend` wrappers for recursive back-edges. Acyclic alias chains may be normalized while preserving the outer reference identifier. + - Export individual revivers for built-in declarations and checks from `Schema`. Consumers opt in to exactly the revivers accepted when reconstructing persisted documents: + - declaration revivers: `OptionReviver`, `ResultReviver`, `RedactedReviver`, `CauseReasonReviver`, `CauseReviver`, `ErrorReviver`, `ExitReviver`, `ReadonlyMapReviver`, `HashMapReviver`, `ReadonlySetReviver`, `HashSetReviver`, `ChunkReviver`, `RegExpReviver`, `URLReviver`, `DateReviver`, `DurationReviver`, `BigDecimalReviver`, `FileReviver`, `FormDataReviver`, `URLSearchParamsReviver`, `Uint8ArrayReviver`, `DateTimeUtcReviver`, `TimeZoneOffsetReviver`, `TimeZoneNamedReviver`, `TimeZoneReviver`, `DateTimeZonedReviver`, `JsonReviver`, and `MutableJsonReviver` + - check revivers: `isTrimmedReviver`, `isPatternReviver`, `isStringFiniteReviver`, `isStringBigIntReviver`, `isStringSymbolReviver`, `isUUIDReviver`, `isGUIDReviver`, `isULIDReviver`, `isBase64Reviver`, `isBase64UrlReviver`, `isStartsWithReviver`, `isEndsWithReviver`, `isIncludesReviver`, `isUppercasedReviver`, `isLowercasedReviver`, `isCapitalizedReviver`, `isUncapitalizedReviver`, `isFiniteReviver`, `isGreaterThanReviver`, `isGreaterThanOrEqualToReviver`, `isLessThanReviver`, `isLessThanOrEqualToReviver`, `isBetweenReviver`, `isMultipleOfReviver`, `isIntReviver`, `isDateValidReviver`, `isGreaterThanDateReviver`, `isGreaterThanOrEqualToDateReviver`, `isLessThanDateReviver`, `isLessThanOrEqualToDateReviver`, `isBetweenDateReviver`, `isGreaterThanBigIntReviver`, `isGreaterThanOrEqualToBigIntReviver`, `isLessThanBigIntReviver`, `isLessThanOrEqualToBigIntReviver`, `isBetweenBigIntReviver`, `isMinLengthReviver`, `isMaxLengthReviver`, `isLengthBetweenReviver`, `isMinSizeReviver`, `isMaxSizeReviver`, `isSizeBetweenReviver`, `isMinPropertiesReviver`, `isMaxPropertiesReviver`, `isPropertiesLengthBetweenReviver`, `isPropertyNamesReviver`, and `isUniqueReviver` + - Validate reviver payloads with their `payloadSchema`, and report missing or duplicate reviver identifiers. + + ### JSON Schema and code generation + - Compile JSON Schema from the canonical JSON codec and the encoded-side representation. Custom checks can contribute constraints through `Annotations.Filter.toJsonSchema` without modifying a central metadata registry. + - Import JSON Schema directly as live schemas. The importer now supports shared definitions, aliases, recursion, reference siblings, and definitions that are not reachable from a root. + - Add the named `FromJsonSchemaOptions` type for the importer `onEnter` callback. + - Generate code from live `toCode` annotations on declarations and checks. Compiler callbacks receive generated type parameters or schema dependencies and can emit multiple import declarations. + - Add import artifacts to `CodeDocument` and preserve all explicit definitions during multi-document code generation. + - Reject distinct schemas that declare the same identifier instead of silently merging them or generating suffixed references. + + ### Canonical codecs and integrations + - Preserve schema identifiers, property context, key encodings, and applicable checks while deriving canonical JSON codecs. + - Treat `Schema.Json` and `Schema.MutableJson` as already canonical. JSON validation now rejects sparse arrays, and non-finite numbers decode only from the canonical strings `"Infinity"`, `"-Infinity"`, and `"NaN"` rather than raw non-finite numeric inputs. + - Declarations without `toCodecJson` or `toCodec` now use JSON validation as their fallback instead of silently encoding to `null`. `toCodecJson` callbacks may return `undefined` when a declaration is already canonical. + - Add `Annotations.Declaration.toCodecStringTree`; StringTree derivation now requires a declaration to provide a structural StringTree, JSON, or general codec instead of silently encoding an opaque declaration to `undefined`. + - Update AI structured-output, HTTP schema, HttpApi OpenAPI, and OpenAPI generator integrations to consume the same canonical encoded representation and compiler hooks. Provider-specific structured-output transforms may remove unsupported JSON Schema keywords, while the Effect codec remains the validation authority. + + ### Breaking changes + - Rename the low-level representation constructors: + - `SchemaRepresentation.fromAST` -> `SchemaRepresentation.toRepresentation` + - `SchemaRepresentation.fromASTs` -> `SchemaRepresentation.toRepresentations` + - Replace `SchemaRepresentation.toSchema` with `fromRepresentation`, and add `fromRepresentations` for multi-root documents. Both reconstruction functions require `{ revivers: [...] }`; no default reviver is installed implicitly. + - Remove `SchemaRepresentation.toSchemaDefaultReviver`. Pass the required built-in revivers exported by `Schema`, or custom revivers created with the new constructors. + - Replace `DocumentFromJson` and `MultiDocumentFromJson` with the `toJson` / `fromJson` and `toJsonMultiDocument` / `fromJsonMultiDocument` functions. + - The persisted `Document` and `MultiDocument` format is incompatible with the previous format. Nodes now contain `checks`; encoded literal values, enum values, and property signature names use tagged `{ type, value }` objects while decoded documents expose their native scalar values; declarations no longer contain `encodedSchema`; persisted opaque declarations and leaf filters require a `{ id, payload }` representation identity; and checks no longer contain closed `meta` payloads. Regenerate stored documents from their source schemas with the new API, or migrate their shape before passing them to `fromJson`. + - Replace the generic `Reviver` function type with `DeclarationReviver

`, `FilterReviver

`, `FilterGroupReviver

`, `CheckReviver

`, `Reviver

`, and `AnyReviver`. + - Remove the closed metadata types `StringMeta`, `NumberMeta`, `BigIntMeta`, `ArraysMeta`, `ObjectsMeta`, `DateMeta`, `SizeMeta`, `DeclarationMeta`, and `Meta` from `SchemaRepresentation`. + - Remove the exported representation validation schemas and `PrimitiveTree`: `$PrimitiveTree`, `$Annotations`, `$Null`, `$Undefined`, `$Void`, `$Never`, `$Unknown`, `$Any`, `$StringMeta`, `$String`, `$NumberMeta`, `$Number`, `$Boolean`, `$BigInt`, `$Symbol`, `$LiteralValue`, `$Literal`, `$UniqueSymbol`, `$ObjectKeyword`, `$Enum`, `$TemplateLiteral`, `$Element`, `$Arrays`, `$PropertySignature`, `$IndexSignature`, `$ObjectsMeta`, `$Objects`, `$Union`, `$Reference`, `$DateMeta`, `$SizeMeta`, `$DeclarationMeta`, `$Declaration`, `$Suspend`, `$Representation`, `$Document`, and `$MultiDocument`. + - Replace schema annotations as follows: + - remove `Annotations.Bottom.meta` and `Annotations.Filter.meta` + - remove `Annotations.Declaration.typeConstructor`; use `representation` + - remove `Annotations.Declaration.generation`; use the `toCode` callback + - add `Annotations.Filter.representation`, `toJsonSchema`, and `toCode` + - add `Annotations.Augment.contentSchema` as a JSON-valued annotation + - allow `Annotations.Declaration.toCodecJson` and `toCodecStringTree` to return `undefined` + - Remove the top-level `contentMediaType` and `contentSchema` fields from `SchemaRepresentation.String`. Content metadata is now carried in ordinary annotations, and `contentSchema` is a JSON Schema value rather than a nested Effect representation. + - Remove `Schema.Annotations.BuiltInMetaDefinitions`, `BuiltInMeta`, `MetaDefinitions`, and `Meta`. Custom checks should carry a representation identity and compiler callbacks instead of augmenting the metadata registry. + - `fromJsonSchemaDocument` now returns `Schema.Top` instead of a representation `Document`. `fromJsonSchemaMultiDocument` now returns `SchemaMultiDocument` instead of `MultiDocument`; call `fromSchemaMultiDocument` when a representation multi-document is required. + - `toCodeDocument` now accepts only a live `MultiDocument`; remove its `reviver` option. Reconstruct persisted documents first so revivers can restore runtime compiler callbacks. + - Rename the `generation` field of `Artifact` values for symbols and enums to `code`. Declaration generation no longer has an `Encoded` output, and `importDeclaration` is replaced by `importDeclarations` on callback output. + - Remove the exported `sanitizeJavaScriptIdentifier`, `topologicalSort`, and `TopologicalSort` helpers. + - Negative zero no longer receives special representation handling. Do not rely on preserving its sign across JSON persistence or generated code, where it may be normalized to `0`. + - With `{ errors: "all" }`, structural checks run only after their base array, object, or declaration parses successfully; they are no longer added to an already failing child parse. + +- [#6646](https://github.com/Effect-TS/effect/pull/6646) [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38) Thanks @gcanti! - Precompile union formatters and equivalences, select transformed union members using their decoded type, and allow deriving an equivalence for `Never`. + +- [#6516](https://github.com/Effect-TS/effect/pull/6516) [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797) Thanks @tim-smart! - Prevent SQL runner lock refreshes from hanging when reserved connections become unresponsive. + ## 4.0.0-beta.101 ### Patch Changes @@ -1620,7 +2521,7 @@ - [#1725](https://github.com/Effect-TS/effect-smol/pull/1725) [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba) Thanks @tim-smart! - Improve unstable HttpApi runtime failures for missing server middleware and missing group implementations. - HttpApiBuilder.applyMiddleware now resolves middleware services via Context.getUnsafe, so missing middleware fails with a clear "Service not found: " error instead of an opaque is not a function TypeError. - HttpApiBuilder.layer now reports missing groups with actionable context (group identifier, service key, suggested HttpApiBuilder.group(...) call, and available group keys). - - Added regression tests in packages/platform-node/test/HttpApi.test.ts covering: + - Added regression tests in packages/platform/node/test/HttpApi.test.ts covering: - addHttpApi + API-level middleware applied across merged groups - missing middleware service diagnostics - missing addHttpApi group layer diagnostics diff --git a/.context/effect/packages/effect/CONFIG.md b/.context/effect/packages/effect/CONFIG.md index 158768692..8b932b790 100644 --- a/.context/effect/packages/effect/CONFIG.md +++ b/.context/effect/packages/effect/CONFIG.md @@ -97,11 +97,22 @@ Each constructor reads a single value and decodes it into the appropriate type. The optional `name` parameter sets the local path segment for lookup. If the config is wrapped with `Config.nested`, the nested prefix is prepended to this local path. Omit `name` when the config should decode the provider root. +### Parsing and Path Ownership + +A `Config` exposes `parse(provider)`; lookup prefixes are not part of this public method. Build paths declaratively with the constructor's `name` / `path` argument and `Config.nested`. + +This keeps the two path responsibilities separate: + +- `Config.schema(..., path)` and `Config.nested(name)` describe the logical path of a setting. +- `ConfigProvider.mapInput`, `ConfigProvider.nested`, and case-conversion combinators map logical paths to a source. + +The same rule applies when a `Config` is yielded as an `Effect`: the config uses the current `ConfigProvider`, while its internally composed logical path stays an implementation detail. + ## Config Combinators -### `Config.withDefault` — Fallback for Missing Keys +### `Config.withDefault` — Fallback for Absent Input -Only triggers when data is missing. Validation errors (wrong type, out of range) still propagate. +Triggers when the config cannot resolve and none of its relevant provider input is present. Validation errors and partially supplied groups still propagate. ```ts import { Config, ConfigProvider, Effect } from "effect" @@ -114,7 +125,7 @@ Effect.runSync(port.parse(provider)) // 3000 ### `Config.option` — Optional Values -Returns `Option.some(value)` on success and `Option.none()` when data is missing. +Returns `Option.some(value)` on success and `Option.none()` when the config is absent. A successful `undefined` value is still a success, so a schema that accepts missing input produces `Option.some(undefined)`, not `Option.none()`. ```ts import { Config, ConfigProvider, Effect } from "effect" @@ -204,7 +215,7 @@ Effect.runSync(config.parse(provider)) // "localhost" ### `Config.all` — Combine Multiple Configs -Accepts a record or a tuple: +Accepts a record or a tuple. A wholly absent group can be handled by `Config.withDefault` or `Config.option`. If any child reads provider input, every other required child must also resolve; partial groups fail instead of silently replacing user input with a whole-group default. ```ts import { Config } from "effect" @@ -220,6 +231,66 @@ const appConfig = Config.all({ const pair = Config.all([Config.string("a"), Config.int("b")]) ``` +For example, providing only `host` is an error here: + +```ts +import { Config } from "effect" + +const database = Config.all({ + host: Config.string("host"), + port: Config.int("port") +}).pipe( + Config.withDefault({ host: "localhost", port: 5432 }) +) +``` + +The default applies when both keys are absent, but not when only one key is present. Defaults on individual children do not count as provider input: + +```ts +const listener = Config.all({ + host: Config.string("host"), + port: Config.int("port").pipe(Config.withDefault(8080)) +}).pipe(Config.option) +``` + +`listener` is `None` when both keys are absent, `Some` when `host` is present, and fails when only `port` is present. + +### How Absence Is Decided + +Configuration evaluation distinguishes three situations before producing the public `Effect`: + +1. **Resolved** — decoding succeeded. The value may legitimately be `undefined`, `{}`, or `[]`. +2. **Absent** — the config could not resolve and no relevant provider representation was found. +3. **Failed** — the provider failed, input was invalid, or a combined config was only partially supplied. + +`Config.withDefault` and `Config.option` handle only the second case. `Config.orElse` handles both absence and failures. + +At the lookup path of a `Config.schema`, an unavailable representation is passed to the schema decoder as `undefined`. This includes a missing node and a present node whose shape cannot represent the schema: for example, an array node cannot represent a struct. Missing properties inside an object remain omitted so the schema's property semantics still apply. The decoder runs before absence is decided. Consequently: + +- `Config.schema(Schema.UndefinedOr(Schema.String), "key")` succeeds with `undefined` when `key` is absent. +- An explicitly present empty object can decode to `{}` when the schema permits it. +- Wrapping either successful result in `Config.option` produces `Some`, because decoding succeeded. +- If the schema rejects `undefined` and no relevant representation was found, `Config.withDefault` uses its fallback and `Config.option` returns `None`. +- Present invalid data and partially supplied `Config.all` groups are failures. +- `SourceError` is always a failure and is never replaced by `withDefault` or `option`. + +`Config.schema(Schema.Struct(...))` and `Config.all(...)` share the same decoder-first rule but describe different lookup models. A struct schema owns one structured input, so an explicitly present empty object is relevant input and its required fields are validated. `Config.all` evaluates independent child configs; an empty parent object does not make the group present when every child is absent. Field optionality in `Config.all` is expressed on each child with `Config.option` or `Config.withDefault`. + +### How Schema Input Is Loaded + +`Config.schema` converts its codec to the canonical `Schema.StringTree` codec and uses the encoded AST to decide which provider representation to load: + +- A scalar schema reads the node's scalar value. A record or array node may have a co-located scalar value in addition to its children. +- A struct loads its declared properties and omits children that the provider does not contain. A record schema also loads advertised keys that match its index signature. +- An array or tuple loads its indexed children. Missing positions are represented as `undefined` so the element schema decides whether they are valid. +- A union whose members require different shapes materializes each member independently. Schema then applies the union's declared order or `oneOf` rule and any checks attached to the original union. + +This keeps the provider responsible only for reporting what exists. Schema remains responsible for deciding whether the loaded representation is valid. + +Plain `Schema.Array` and `Schema.Record` accept structural provider input only. Use `Config.Array` for separated scalar input such as `"a,b,c"`, and `Config.Record` for input such as `"a=1,b=2"`. + +The canonical `StringTree` encoding must expose a concrete scalar, object, array, or union shape. `Config.schema` rejects opaque encodings such as `Schema.Any`, `Schema.Unknown`, `Schema.ObjectKeyword`, `Schema.Json`, and `Schema.MutableJson` synchronously when the config is constructed, including when they are nested in another schema. Suspended recursive schemas and declarations such as `Schema.URL` remain supported when their eventual canonical encoding has a concrete shape. To read arbitrary JSON from one scalar provider value, use `Schema.fromJsonString(Schema.Json)`. + ### Custom Config Logic There is no public low-level `Config.make` constructor. For custom validation or transformation, start from one of the public constructors or `Config.schema`, then use `Config.map`, `Config.mapOrFail`, `Config.all`, `Config.orElse`, or `Config.withDefault`. @@ -236,12 +307,23 @@ For reusable codecs you can pass directly to `Config.schema`: | `Schema.DurationFromString` | `Duration` | Decodes human-readable duration strings | | `Config.Port` | `number` | Integer in 1–65535 | | `Config.LogLevel` | `string` | One of the standard log level literals | +| `Config.Array(value)` | `Array` | Also parses flat `"v1,v2"` strings | | `Config.Record(key, value)` | `Record` | Also parses flat `"k1=v1,k2=v2"` strings | ## ConfigProvider Sources The concrete built-in source providers `fromEnv`, `fromDotEnvContents`, `fromDotEnv`, `fromUnknown`, and `fromDir` treat literal empty strings as missing values by default when they are loaded as values. Container discovery still reflects the source structure, so a key or file can appear in a `Record` or `Array` node and then load as missing. Pass `{ preserveEmptyStrings: true }` to preserve empty strings as explicit values. +At the raw provider interface, `load(path)` succeeds with `Node | undefined`: a +`Node` means the path exists, while `undefined` means it does not. A +`SourceError` represents a failure to read the source and remains in the Effect +error channel. + +Lookup-level `undefined` is distinct from the `value` field of a found `Record` +or `Array` node. Such a container can exist while +`node.value === undefined`, which means that it has children but no co-located +scalar value. + ### `ConfigProvider.fromEnv` — Environment Variables (Default) This is the default provider. Path segments are joined with `_` for lookup. @@ -383,7 +465,10 @@ const provider = ConfigProvider.make((path) => { }) ``` -Return `undefined` for "not found". Only fail with `SourceError` for actual I/O errors. +Return `undefined` for "not found" and a `Node` for a path that exists. Only +fail with `SourceError` when the source itself cannot be read. Providers created +with `make` automatically support the path-transformation behavior used by +`mapInput`, `constantCase`, and `nested`. ## ConfigProvider Combinators @@ -512,6 +597,14 @@ const upper = ConfigProvider.mapInput( ) ``` +Path transformation is a capability of the `ConfigProvider` interface. The +exported `ConfigProvider.mapInput` combinator delegates to that capability, +rather than passing an extra transformation argument to `load`. This keeps +ordinary lookup fixed as `load(path)` and allows composite providers to +preserve their own behavior without exposing representation state. Custom +source providers should normally be constructed with `ConfigProvider.make`, +which implements this capability automatically. + `mapInput` runs after earlier provider transformations, so it sees the full path produced so far: ```ts @@ -604,6 +697,8 @@ const program = Effect.gen(function*() { const result = Effect.runSync(host.parse(provider)) ``` + The method accepts only the provider. Use `Config.nested` or the path argument of `Config.schema` to scope lookups. + ## Error Handling Config operations fail with `ConfigError`, which wraps either: @@ -631,7 +726,7 @@ const program = Config.int("PORT").parse( ) ``` -**Important**: `Config.withDefault` and `Config.option` only recover from missing-data errors. Validation errors still propagate. +**Important**: `Config.withDefault` and `Config.option` recover only from semantic absence. They do not classify `SchemaIssue` values as “missing.” Validation errors, source failures, and partially supplied groups still propagate. ## Practical Example: Web Server Config diff --git a/.context/effect/packages/effect/HTTPAPI.md b/.context/effect/packages/effect/HTTPAPI.md index 041389d3e..fa6bc465d 100644 --- a/.context/effect/packages/effect/HTTPAPI.md +++ b/.context/effect/packages/effect/HTTPAPI.md @@ -1550,7 +1550,170 @@ The following encodings are supported: ## Setting Response Headers -To add custom headers to the outgoing response, call `HttpEffect.appendPreResponseHandler` inside your handler. The callback receives the request and response objects and must return the updated response. +Response headers can be declared in the endpoint's schemas, so they are type-checked on the server, rendered in the OpenAPI documentation, and decoded by the derived client. Two mechanisms are available: + +- `HttpApiSchema.WithHeaders(schema, headers)` wraps a response schema together with a headers schema. Handlers return the body and headers as a pair. Recommended for success responses, including streams. +- `HttpApiSchema.encodeToWithHeaders` folds headers into an opaque domain type such as an error class, so handlers keep working with plain domain values. + +Only one response schema carrying headers may be declared for each status, though plain responses with different content types may share that status. + +For headers that are not part of the API contract, `HttpEffect.appendPreResponseHandler` remains available as an untyped escape hatch. + +### Declaring Response Headers with WithHeaders + +Wrap the success schema with `HttpApiSchema.WithHeaders(schema, headers)`. The headers argument accepts a fields shorthand (as below) or any schema, mirroring the request-side `headers` option. The handler then returns a value built with `HttpApiSchema.withHeaders({ body, headers })`. + +**Example** (Declaring a Response Header on a Success Schema) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi").add( + HttpApiGroup.make("Users").add( + HttpApiEndpoint.get("getUsers", "/users", { + // Wrap the success schema with a response headers schema + success: HttpApiSchema.WithHeaders(Schema.Array(User), { + "x-total-count": Schema.Int + }) + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers.handle("getUsers", () => + // Return the body together with the declared headers + Effect.succeed(HttpApiSchema.withHeaders({ + body: [{ id: 1, name: "John" }], + headers: { "x-total-count": 1 } + }))) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl -v "http://localhost:3000/users" 2>&1 | grep -i "x-total-count" +// < x-total-count: 1 +``` + +The derived client detects the wrapper and returns the same shape, with the headers decoded through the headers schema: + +```ts +const users = yield * client.Users.getUsers() +users.body // => [{ id: 1, name: "John" }] +users.headers // => { "x-total-count": 1 } +``` + +Things to know: + +- Header values are converted to strings at the HTTP boundary, the same way as request headers, params, and query. `Schema.Int` goes out as `"1"` and decodes back to `1` on the client. `undefined` values are omitted from the response. +- Status and encoding annotations resolve from the wrapper first, then fall through to the inner schema, so `HttpApiSchema.WithHeaders(User.pipe(HttpApiSchema.status(201)), ...)` responds with `201`. +- Declared headers are applied after the body is encoded and override headers set by the encoding on collision, including `content-type`. +- Stream success schemas (`HttpApiSchema.StreamSse`, `HttpApiSchema.StreamUint8Array`) can be wrapped too. Headers are encoded before the response starts streaming, and the client resolves to a value whose `body` is the stream. +- `WithHeaders` is also allowed on error schemas, in which case the handler fails with the wrapped value. For errors, `encodeToWithHeaders` (below) is usually more convenient because handlers can fail with the domain error directly. + +### Folding Headers into Domain Types with encodeToWithHeaders + +`HttpApiSchema.encodeToWithHeaders` encodes a schema as a `{ body, headers }` pair while its Type stays unchanged. This lets an error class carry data that travels in a response header: handlers fail with plain error instances, and the client receives the same class with the header folded back in. + +The body schema is authoritative for everything wire-level: status, content type, and response encoding resolve from the body schema's annotations. A status annotation on the source schema stops mattering once wrapped, so spell the status on the body — `HttpApiSchema.Empty(404)` declares an empty body with status 404. + +**Example** (Returning an Error With a Response Header) + +```ts +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer, Schema } from "effect" +import { HttpRouter } from "effect/unstable/http" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi" +import { createServer } from "node:http" + +class UserNotFound extends Schema.TaggedError()("UserNotFound", { + userId: Schema.Int +}) {} + +const UserNotFoundWithHeaders = UserNotFound.pipe( + HttpApiSchema.encodeToWithHeaders({ + // The body schema is authoritative for status and content type + body: HttpApiSchema.Empty(404), + headers: { + "x-user-id": Schema.Int + } + }, { + // Pure mappings between the domain type and the { body, headers } pair + decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }), + encode: (error) => ({ + headers: { "x-user-id": error.userId }, + body: undefined + }) + }) +) + +const User = Schema.Struct({ + id: Schema.Int, + name: Schema.String +}) + +const Api = HttpApi.make("MyApi").add( + HttpApiGroup.make("Users").add( + HttpApiEndpoint.get("getUser", "/user/:id", { + params: { + id: Schema.Int + }, + success: User, + error: UserNotFoundWithHeaders + }) + ) +) + +const GroupLive = HttpApiBuilder.group( + Api, + "Users", + (handlers) => + handlers.handle("getUser", (ctx) => { + const id = ctx.params.id + if (id === 1) { + // Fail with the plain error instance + return Effect.fail(new UserNotFound({ userId: id })) + } + return Effect.succeed({ id, name: `User ${id}` }) + }) +) + +const ApiLive = HttpApiBuilder.layer(Api).pipe( + Layer.provide(GroupLive), + HttpRouter.serve, + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(ApiLive).pipe(NodeRuntime.runMain) + +// curl -v "http://localhost:3000/user/1" 2>&1 | grep -i "x-user-id" +// < x-user-id: 1 +``` + +The `decode`/`encode` mappings are pure total functions: validation lives in the body and headers schemas, the mappings only reshape valid data. The error channel is unchanged — a client calling this endpoint fails with a `UserNotFound` instance whose `userId` was decoded from the header. + +`encodeToWithHeaders` also works on custom success types, but avoid burying stream schemas in it; wrap streams with `WithHeaders` instead so the generated client keeps the stream's error channel. + +### Untyped Response Headers + +For headers that should not appear in the API contract, call `HttpEffect.appendPreResponseHandler` inside your handler. The callback receives the request and response objects and must return the updated response. These headers bypass the schemas, the OpenAPI documentation, and the derived client. **Example** (Adding a Custom Response Header) @@ -2024,7 +2187,7 @@ import { import { createServer } from "node:http" // Define a custom error for validation failures -class ValidationError extends Schema.TaggedErrorClass()( +class ValidationError extends Schema.TaggedError()( "ValidationError", { message: Schema.String @@ -2263,7 +2426,7 @@ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSecur class User extends Schema.Class("User")({ id: Schema.Finite }) {} // Define a schema for the "Unauthorized" error -class Unauthorized extends Schema.TaggedErrorClass()( +class Unauthorized extends Schema.TaggedError()( "Unauthorized", {}, // Specify the HTTP status code for unauthorized errors @@ -2322,7 +2485,7 @@ import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" class User extends Schema.Class("User")({ id: Schema.Finite }) {} -class Unauthorized extends Schema.TaggedErrorClass()( +class Unauthorized extends Schema.TaggedError()( "Unauthorized", {}, // Specify the HTTP status code for unauthorized errors @@ -2378,7 +2541,7 @@ import { HttpApiMiddleware, HttpApiSecurity, OpenApi } from "effect/unstable/htt class User extends Schema.Class("User")({ id: Schema.Finite }) {} -class Unauthorized extends Schema.TaggedErrorClass()( +class Unauthorized extends Schema.TaggedError()( "Unauthorized", {}, // Specify the HTTP status code for unauthorized errors diff --git a/.context/effect/packages/effect/MCP.md b/.context/effect/packages/effect/MCP.md index ff30493f8..3668e6a9e 100644 --- a/.context/effect/packages/effect/MCP.md +++ b/.context/effect/packages/effect/MCP.md @@ -12,7 +12,7 @@ Here is an example of a MCP server implementation: import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node" import { Effect, Layer, Logger } from "effect" import { Schema } from "effect/schema" -import { McpServer, Tool, Toolkit } from "effect/unstable/ai" +import { McpProtocol, McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define a simple tool const DemoTool = Tool.make("DemoTool", { @@ -58,6 +58,7 @@ const ServerLayer = Layer.mergeAll( McpServer.layerStdio({ name: "Demo MCP Server", version: "1.0.0", + protocols: [McpProtocol.v2025_06_18], stdin: NodeStream.stdin, stdout: NodeSink.stdout }) @@ -78,8 +79,10 @@ The server exposes three main parts: The part layers are merged into one layer that has a MCP server implementation as dependency. `McpServer.layerStdio` is used to create a standard I/O–based MCP server identified by its name and -version. Because of the layer architecture the server implementation can be easily exchanged with an -HTTP based implementation with `McpServer.layerHttp`. Finally, a logging layer is added with +version. Its ordered, non-empty `protocols` declaration names implemented protocol adapters rather +than arbitrary version strings. This release supports `McpProtocol.v2025_06_18`. Because of the +layer architecture the server implementation can be easily exchanged with an HTTP-based implementation +with `McpServer.layerHttp`. Finally, a logging layer is added with `Logger.layer([Logger.consolePretty({ stderr: true })])`, ensuring logs are written to `stderr`. This is essential when using stdio, as any output to `stdout` would interfere with the protocol communication. @@ -238,7 +241,7 @@ Here's a complete, copy/pastable MCP server example that combines all the concep ```typescript import { NodeRuntime, NodeStdio } from "@effect/platform-node" import { Effect, Layer, Logger, Schema } from "effect" -import { McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai" +import { McpProtocol, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai" // Define tools const GreetTool = Tool.make("GreetTool", { @@ -357,7 +360,8 @@ const ServerLayer = Layer.mergeAll( Layer.provide( McpServer.layerStdio({ name: "Demo MCP Server", - version: "1.0.0" + version: "1.0.0", + protocols: [McpProtocol.v2025_06_18] }) ), Layer.provide(NodeStdio.layer), diff --git a/.context/effect/packages/effect/README.md b/.context/effect/packages/effect/README.md index f98948465..13035cb3c 100644 --- a/.context/effect/packages/effect/README.md +++ b/.context/effect/packages/effect/README.md @@ -1,43 +1,36 @@ -# `effect` Core Package +# effect -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. +Effect is a library for building robust, maintainable, type-safe, and production grade applications in TypeScript. -## Requirements +The `effect` package is the core of the framework. It provides primitives for managing side effects, errors, concurrency, resources, and structured data, alongside a rich standard library. -- **TypeScript 5.9 or Newer:** - Ensure you are using a compatible TypeScript version. +## Requirements -- **Strict Type-Checking:** - The `strict` flag must be enabled in your `tsconfig.json`. For example: +- **TypeScript 5.9 or newer** +- **Strict type-checking:** the `strict` flag must be enabled in your `tsconfig.json`: ```json { "compilerOptions": { "strict": true - // ...other options } } ``` ## Installation -Install the core package using your preferred package manager. For example, with npm: - -```bash -npm install effect +```sh +npm install effect@beta ``` ## Documentation -- **Website:** - For detailed information and usage examples, visit the [Effect website](https://www.effect.website/). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/effect) -- **API Reference:** - For a complete API reference of the core package `effect`, see the [Effect API documentation](https://effect-ts.github.io/effect/). +## Overview -## 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: +The `effect` package is a collection of modules. Some of the core ones: | Module | Description | | -------- | -------------------------------------------------------------------------------------------------------------------------- | @@ -49,3 +42,5 @@ The `effect` package provides a collection of modules designed for functional pr | 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. | + +In v4, functionality that previously lived in separate packages ships inside `effect` under the `effect/unstable/*` namespaces, including `http`, `httpapi`, `rpc`, `cluster`, `workflow`, `cli`, `ai`, `sql`, and `reactivity`. diff --git a/.context/effect/packages/effect/SCHEMA.md b/.context/effect/packages/effect/SCHEMA.md index 7e7baa167..00095dc0d 100644 --- a/.context/effect/packages/effect/SCHEMA.md +++ b/.context/effect/packages/effect/SCHEMA.md @@ -37,6 +37,39 @@ Use Schema to: 13. **Integrations** — working examples for TanStack Form and Elysia. 14. **Migration from v3** — API mapping from Schema v3 to v4. +## Runtime Performance + +Effect Schema is benchmarked against the public +[`schema-benchmarks`](https://github.com/open-circle/schema-benchmarks) suite. +It exercises a realistic product schema across validation, parsing, error +reporting, schema creation, and codecs. + +The table below compares Effect Schema with the Valibot and Zod cases available +in the same suite. + +Values are microseconds per operation and lower is better. Results vary between +machines, so they are most useful for understanding relative costs. A dash +means that the library does not provide that benchmark. + +| Scenario | Effect Schema | Valibot | Zod 4 | +| ------------------------------------- | ------------: | ---------: | ---------: | +| Create a schema | 118.23 | **40.24** | 318.56 | +| Create a schema and parser | **130.50** | — | — | +| Validate valid data | **5.415** | 5.63 | — | +| Validate invalid data | 1.348 | **0.2431** | — | +| Parse valid data and collect errors | 5.366 | **5.22** | 7.16 | +| Parse invalid data and collect errors | **9.100** | 15.70 | 41.58 | +| Parse valid data and stop early | **5.294** | 5.37 | — | +| Parse invalid data and stop early | 1.352 | **0.2572** | — | +| Standard Schema, valid data | 5.935 | 5.35 | **3.83** | +| Standard Schema, invalid data | **15.203** | 16.51 | 32.85 | +| Standard Schema, valid, stop early | **5.843** | — | — | +| Standard Schema, invalid, stop early | **2.244** | — | — | +| Encode with a typed codec | 0.3420 | — | **0.0405** | +| Decode with a typed codec | 0.3762 | — | **0.0463** | +| Encode unknown input | **0.3472** | — | — | +| Decode unknown input | **0.3637** | — | — | + # Defining Elementary Schemas Schema provides built-in schemas for all common TypeScript types. These schemas represent a single value — like a string or a number — and they are the building blocks you combine into more complex shapes. @@ -244,9 +277,8 @@ Schema.BigInt.check(isNonPositive) ## Dates -The `Schema.Date` schema matches `Date` objects (even invalid dates). - -If you want to validate only valid dates, use `Schema.DateValid` instead. +The `Schema.Date` schema matches valid `Date` objects and rejects invalid dates +such as `new Date(NaN)`. ## Template literals @@ -282,7 +314,7 @@ Success("a@b.com") console.log(String(Schema.decodeUnknownExit(email)("@b.com"))) /* -Failure(Cause([Fail(SchemaError(Expected a string matching template literal parts, got "@b.com"))])) +Failure(Cause([Fail(SchemaError(Expected a string matching template literal parts))])) */ ``` @@ -308,11 +340,11 @@ console.log(String(Schema.decodeUnknownExit(schema)("aa:1"))) // Success(["aa",":",1]) console.log(String(Schema.decodeUnknownExit(schema)("a:1"))) -// Failure(Cause([Fail(SchemaError(Expected a value with a length of at least 2, got "a" +// Failure(Cause([Fail(SchemaError(Expected a value with a length of at least 2 // at [0]))])) console.log(String(Schema.decodeUnknownExit(schema)("aa:1.2"))) -// Failure(Cause([Fail(SchemaError(Expected an integer, got 1.2 +// Failure(Cause([Fail(SchemaError(Expected an integer // at [2]))])) ``` @@ -1133,7 +1165,7 @@ console.log( }) ) ) -// Failure(Cause([Fail(SchemaError: Expected a === b, got {"a":"a","b":"b","c":"c"})])) +// Failure(Cause([Fail(SchemaError: Expected a === b)])) ``` #### Mapping individual fields @@ -1610,7 +1642,7 @@ import { Schema } from "effect" const schema = Schema.UniqueArray(Schema.String) console.log(String(Schema.decodeUnknownExit(schema)(["a", "b", "a"]))) -// Failure(Cause([Fail(SchemaError: Expected an array with unique items, got ["a","b","a"])])) +// Failure(Cause([Fail(SchemaError: Expected an array with unique items)])) ``` ## Records @@ -1638,9 +1670,12 @@ console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, c_d: 2 })) // { aB: 1, cD: 2 } ``` -By default, if a transformation results in duplicate keys, the last value wins. +When parsing sequentially, transformed keys are applied in selection order, so +the later selected property wins if a transformation produces a duplicate key. +With concurrency greater than `1`, completion order determines which value is +retained. -**Example** (Merging transformed keys by keeping the last one) +**Example** (Keeping the later selected value when parsing sequentially) ```ts import { Schema, SchemaTransformation } from "effect" @@ -1653,35 +1688,6 @@ console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, aB: 2 })) // { aB: 2 } ``` -You can customize how key conflicts are resolved by providing a `combine` function. - -**Example** (Combining values for conflicting keys) - -```ts -import { Schema, SchemaTransformation } from "effect" - -const SnakeToCamel = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())) - -const schema = Schema.Record(SnakeToCamel, Schema.Number, { - keyValueCombiner: { - decode: { - // When decoding, combine values of conflicting keys by summing them - combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] // you can pass a Semigroup to combine keys - }, - encode: { - // Same logic applied when encoding - combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] - } - } -}) - -console.log(Schema.decodeUnknownSync(schema)({ a_b: 1, aB: 2 })) -// { aB: 3 } - -console.log(Schema.encodeUnknownSync(schema)({ a_b: 1, aB: 2 })) -// { a_b: 3 } -``` - ### Number Keys Records with number keys are supported. @@ -1700,7 +1706,7 @@ console.log(String(Schema.decodeUnknownExit(schema)({ 1.1: "ignored" }))) // Success({}) console.log(String(Schema.decodeUnknownExit(schema)({ 1: null }))) -// Failure(Cause([Fail(SchemaError(Expected string, got null +// Failure(Cause([Fail(SchemaError(Expected string // at ["1"]))])) ``` @@ -1810,7 +1816,7 @@ import { Schema } from "effect" const schema = Schema.Union([Schema.NonEmptyString, Schema.Number]) console.log(String(Schema.decodeUnknownExit(schema)(""))) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "")])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1)])) ``` If none of the union members match the input, the union fails with a message at the top level. @@ -1823,7 +1829,7 @@ import { Schema } from "effect" const schema = Schema.Union([Schema.NonEmptyString, Schema.Number]) console.log(String(Schema.decodeUnknownExit(schema)(null))) -// Failure(Cause([Fail(SchemaError: Expected string | number, got null)])) +// Failure(Cause([Fail(SchemaError: Expected string | number)])) ``` This behavior is especially helpful when working with literal values. Instead of producing a separate error for each literal (as in version 3), the schema reports a single, clear message. @@ -1836,7 +1842,7 @@ import { Schema } from "effect" const schema = Schema.Literals(["a", "b"]) console.log(String(Schema.decodeUnknownExit(schema)(null))) -// Failure(Cause([Fail(SchemaError: Expected "a" | "b", got null)])) +// Failure(Cause([Fail(SchemaError: Expected "a" | "b")])) ``` ### Exclusive Unions @@ -1853,7 +1859,7 @@ const schema = Schema.Union([Schema.Struct({ a: Schema.String }), Schema.Struct( }) console.log(String(Schema.decodeUnknownExit(schema)({ a: "a", b: 1 }))) -// Failure(Cause([Fail(SchemaError: Expected exactly one member to match the input {"a":"a","b":1})])) +// Failure(Cause([Fail(SchemaError: Expected exactly one member to match)])) ``` ### Deriving Unions @@ -2205,7 +2211,7 @@ console.log(String(Schema.decodeUnknownExit(URLSchema)(new URL("https://example. // Success(https://example.com/) console.log(String(Schema.decodeUnknownExit(URLSchema)(null))) -// Failure(Cause([Fail(SchemaError(Expected , got null))])) +// Failure(Cause([Fail(SchemaError(Expected ))])) ``` > **Tip**: For simple `instanceof` checks, prefer `Schema.instanceOf(URL)`, it wraps `Schema.declare` with an `instanceof` guard automatically. @@ -2225,7 +2231,7 @@ const URLSchema = Schema.declare( ) console.log(String(Schema.decodeUnknownExit(URLSchema)(null))) -// Failure(Cause([Fail(SchemaError(Expected URL, got null))])) +// Failure(Cause([Fail(SchemaError(Expected URL))])) // ^^^ // Now the error message shows "URL" instead of "" ``` @@ -2261,7 +2267,7 @@ You build a `Link` using `Schema.link()`, which takes two arguments: **Example** (Making `URL` JSON-serializable) ```ts -import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" +import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect" const URLSchema = Schema.declare( (u): u is URL => u instanceof URL, @@ -2275,10 +2281,10 @@ const URLSchema = Schema.declare( // How to convert between URL and string SchemaTransformation.transformOrFail({ // JSON string -> URL (may fail if the string is not a valid URL) - decode: (s) => + decode: (s, options) => Effect.try({ try: () => new URL(s), - catch: (e) => new SchemaIssue.InvalidValue(Option.some(s), { message: globalThis.String(e) }) + catch: () => new SchemaIssue.InvalidValue({ message: "Invalid URL string" }, s, options) }), // URL -> JSON string (always succeeds) encode: (url) => Effect.succeed(url.href) @@ -2333,7 +2339,7 @@ The parsing function you return from `run` is responsible for: **Example** (A generic `Box` container) ```ts -import { Effect, Option, Schema, SchemaIssue, SchemaParser } from "effect" +import { Effect, Schema, SchemaIssue, SchemaParser } from "effect" // 1. Define the type interface Box { @@ -2354,7 +2360,7 @@ const Box = (item: A) => (u, ast, options) => { // First, check the outer shape if (!isBox(u)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(u))) + return Effect.fail(new SchemaIssue.InvalidType(ast, u, options)) } // Then, decode the inner value using the item codec return Effect.mapBothEager( @@ -2375,7 +2381,7 @@ console.log(String(Schema.decodeUnknownExit(schema)({ value: "1" }))) // Success({ value: 1 }) console.log(String(Schema.decodeUnknownExit(schema)({ value: "a" }))) -// Failure(Cause([Fail(SchemaError(Expected a finite number, got NaN +// Failure(Cause([Fail(SchemaError(Expected a finite number // at ["value"]))])) ``` @@ -2398,7 +2404,7 @@ import { Schema } from "effect" const schema = Schema.String.check(Schema.makeFilter((s) => s.length >= 3)) console.log(String(Schema.decodeUnknownExit(schema)(""))) -// Failure(Cause([Fail(SchemaError: Expected , got "")])) +// Failure(Cause([Fail(SchemaError: Expected )])) ``` You can attach annotations and provide a custom error message when defining a filter. @@ -2443,10 +2449,10 @@ import { Schema } from "effect" const Username = Schema.NonEmptyString.annotate({ identifier: "Username" }) console.log(String(Schema.decodeUnknownExit(Username)(null))) -// Failure(Cause([Fail(SchemaError: Expected Username, got null)])) +// Failure(Cause([Fail(SchemaError: Expected Username)])) console.log(String(Schema.decodeUnknownExit(Username)(""))) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "")])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1)])) ``` ### Filter return shapes @@ -2557,7 +2563,7 @@ const schema = Schema.String.check( ) console.log(String(Schema.decodeUnknownExit(schema)(" a"))) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a")])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)])) ``` **Example** (Using `isMinLength` with an object that has `length`) @@ -2569,7 +2575,7 @@ import { Schema } from "effect" const schema = Schema.Struct({ length: Schema.Number }).check(Schema.isMinLength(3)) console.log(String(Schema.decodeUnknownExit(schema)({ length: 2 }))) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got {"length":2}])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)])) ``` **Example** (Validating array length) @@ -2581,7 +2587,7 @@ import { Schema } from "effect" const schema = Schema.Array(Schema.String).check(Schema.isMinLength(3)) console.log(String(Schema.decodeUnknownExit(schema)(["a", "b"]))) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got ["a","b"]])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)])) ``` ## Multiple Issues Reporting @@ -2603,8 +2609,8 @@ console.log( ) ) /* -Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a" -Expected a string with no leading or trailing whitespace, got " a")])) +Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3 +Expected a string with no leading or trailing whitespace)])) */ ``` @@ -2629,7 +2635,7 @@ console.log( }) ) ) -// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3, got " a")])) +// Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 3)])) ``` ## Filter Groups @@ -2686,16 +2692,16 @@ const branded = Schema.String.pipe(Schema.brand("UserId")) Some filters check the structure of a value rather than its contents — for example, the number of items in an array or the number of keys in an object. These are called **structural filters**. -Structural filters are evaluated separately from item-level filters, which allows multiple issues to be reported when `{ errors: "all" }` is used. Examples include: +Examples include: - `isMinLength` or `isMaxLength` on arrays - `isMinSize` or `isMaxSize` on objects with a `size` property - `isMinProperties` or `isMaxProperties` on objects - any constraint that applies to the "shape" of a value rather than to its nested values -These filters are evaluated separately from item-level filters and allow multiple issues to be reported when `{ errors: "all" }` is used. +Structural filters run only after the base array, object, or declaration and its nested values parse successfully. If a nested value fails, its issue is reported but structural filters on the containing value are not evaluated, even with `{ errors: "all" }`. -**Example** (Validating an array with item and structural constraints) +**Example** (A nested failure prevents the structural filter from running) ```ts import { Schema } from "effect" @@ -2708,10 +2714,8 @@ const schema = Schema.Struct({ console.log(String(Schema.decodeUnknownExit(schema)({ tags: ["a", ""] }, { errors: "all" }))) /* -Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1, got "" - at ["tags"][1] -Expected a value with a length of at least 3, got ["a",""] - at ["tags"])])) +Failure(Cause([Fail(SchemaError: Expected a value with a length of at least 1 + at ["tags"][1])])) */ ``` @@ -2724,7 +2728,7 @@ Define an effectful filter with `Getter.checkEffect` as part of a transformation **Example** (Asynchronous validation of a numeric value) ```ts -import { Effect, Option, Result, Schema, SchemaGetter, SchemaIssue } from "effect" +import { Effect, Result, Schema, SchemaGetter, SchemaIssue } from "effect" // Simulated API call that fails when userId is 0 const myapi = (userId: number) => @@ -2737,13 +2741,15 @@ const myapi = (userId: number) => const schema = Schema.Finite.pipe( Schema.decode({ - decode: SchemaGetter.checkEffect((n) => + decode: SchemaGetter.checkEffect((n, options) => Effect.gen(function*() { // Call the async API and wrap the result in a Result const user = yield* Effect.result(myapi(n)) // If the result is an error, return a SchemaIssue - return Result.isFailure(user) ? new SchemaIssue.InvalidValue(Option.some(n), { title: "not found" }) : undefined // No issue, value is valid + return Result.isFailure(user) + ? new SchemaIssue.InvalidValue({ message: "not found" }, n, options) + : undefined // No issue, value is valid }) ), encode: SchemaGetter.passthrough() @@ -3206,16 +3212,16 @@ This is useful when you need to validate input or enforce rules that may not alw **Example** (Converting a string URL into a `URL` object) ```ts -import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" +import { Effect, Schema, SchemaIssue, SchemaTransformation } from "effect" const URLFromString = Schema.String.pipe( Schema.decodeTo( Schema.instanceOf(URL), SchemaTransformation.transformOrFail({ - decode: (s) => + decode: (s, options) => Effect.try({ try: () => new URL(s), - catch: () => new Issue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` }) + catch: () => new SchemaIssue.InvalidValue({ message: "Invalid URL string" }, s, options) }), encode: (url) => Effect.succeed(url.href) }) @@ -3675,7 +3681,7 @@ class Person extends Schema.Opaque()( ) {} console.log(String(Schema.decodeUnknownExit(Person)(null))) -// Failure(Cause([Fail(SchemaError: Expected Person, got null)])) +// Failure(Cause([Fail(SchemaError: Expected Person)])) ``` When you call methods like `annotate` on an opaque struct, you get back the original struct, not a new class. @@ -3840,7 +3846,7 @@ g(A.make({ a: "a" })) // error: Argument of type 'A' is not assignable to parame ## Schema as a Class -`Schema.asClass` turns any schema into a class that can be extended with `extends`. The resulting class inherits the full schema API (e.g. `annotate`) and supports static methods that reference `this`. +Any schema can be extended directly with `extends`. The resulting class inherits the full schema API (e.g. `annotate`) and supports static methods that reference `this`. Unlike `Schema.Opaque`, it does **not** make the decoded type nominally distinct, and unlike `Schema.Class`, it does **not** create prototype-backed instances with methods or constructors. It is a lightweight way to attach custom static helpers to a schema. @@ -3849,7 +3855,7 @@ Unlike `Schema.Opaque`, it does **not** make the decoded type nominally distinct ```ts import { Schema } from "effect" -class MyString extends Schema.asClass(Schema.String) { +class MyString extends Schema.String { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) } @@ -3862,9 +3868,7 @@ console.log(MyString.decodeUnknownSync("a")) ```ts import { Schema } from "effect" -class MyStruct extends Schema.asClass( - Schema.Struct({ name: Schema.String }) -) { +class MyStruct extends Schema.Struct({ name: Schema.String }) { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) } @@ -3874,12 +3878,12 @@ console.log(MyStruct.decodeUnknownSync({ name: "a" })) ### Subclassing -You can extend an `asClass` class to layer on more static helpers: +You can extend a schema class to layer on more static helpers: ```ts import { Schema } from "effect" -class MyString extends Schema.asClass(Schema.FiniteFromString) { +class MyString extends Schema.FiniteFromString { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) } @@ -3923,7 +3927,7 @@ try { } } /* -Expected a finite number, got NaN +Expected a finite number at [1] */ ``` @@ -4037,7 +4041,7 @@ class PersonWithEmail extends Person { **Example** (Extending Data.Error) ```ts -import { Data, Effect, identity, Schema, SchemaTransformation, SchemaUtils } from "effect" +import { Data, Effect, identity, Schema, SchemaTransformation } from "effect" const Props = Schema.Struct({ message: Schema.String @@ -4083,9 +4087,6 @@ const schema = Schema.instanceOf(Err, { json: () => Schema.link()(Props, transformation) } }).pipe(Schema.encodeTo(Props, transformation)) - -// built-in helper? -const builtIn = SchemaUtils.getNativeClassSchema(Err, { encoding: Props }) ``` ### Class API @@ -4132,14 +4133,14 @@ try { } catch (error: any) { console.log(error.message) } -// Expected a === b, got {"a":"a","b":"b"} +// Expected a === b try { Schema.decodeUnknownSync(A)({ a: "a", b: "b" }) } catch (error: any) { console.log(error.message) } -// Expected a === b, got {"a":"a","b":"b"} +// Expected a === b ``` #### Branded Classes @@ -4419,19 +4420,19 @@ console.log(Schema.decodeUnknownSync(Animal)({ _tag: "Cat", lives: 9 })) All features from `Class` are available: `extend`, `annotate`, `check`, branded classes, and recursive definitions. -### ErrorClass +### Error ```ts import { Schema } from "effect" -class E extends Schema.ErrorClass("E")({ +class E extends Schema.Error("E")({ id: Schema.Number }) {} ``` -### TaggedErrorClass +### TaggedError -`TaggedErrorClass` combines `ErrorClass` with an automatic `_tag` field, giving you a tagged error that can be caught with `Effect.catchTag`. +`TaggedError` combines `Error` with an automatic `_tag` field, giving you a tagged error that can be caught with `Effect.catchTag`. Like `TaggedClass`, the tag value doubles as the identifier by default, and you can pass an explicit identifier as the first argument to override it. @@ -4440,7 +4441,7 @@ Like `TaggedClass`, the tag value doubles as the identifier by default, and you ```ts import { Effect, Schema } from "effect" -class HttpError extends Schema.TaggedErrorClass()("HttpError", { +class HttpError extends Schema.TaggedError()("HttpError", { status: Schema.Number, message: Schema.String }) {} @@ -4459,11 +4460,11 @@ const recovered = program.pipe( ```ts import { Effect, Schema } from "effect" -class NotFound extends Schema.TaggedErrorClass()("NotFound", { +class NotFound extends Schema.TaggedError()("NotFound", { path: Schema.String }) {} -class Unauthorized extends Schema.TaggedErrorClass()("Unauthorized", { +class Unauthorized extends Schema.TaggedError()("Unauthorized", { reason: Schema.String }) {} @@ -4484,7 +4485,7 @@ const recovered = program.pipe( ) ``` -All features from `ErrorClass` are available: `extend`, `annotate`, and `check`. +All features from `Error` are available: `extend`, `annotate`, and `check`. # Serialization @@ -5557,9 +5558,9 @@ console.log(JSON.stringify(document, null, 2)) ### Generating an Arbitrary from a Schema -Property-based tests need generators. `Schema.toArbitrary` derives a -`fast-check` `Arbitrary` that generates decoded `Type` values accepted by the -schema. +Property-based tests need generators. `Schema.toArbitrary` derives a factory +that accepts the `fast-check` module and returns an `Arbitrary` that generates +decoded `Type` values accepted by the schema. Most schemas do not need any extra work: @@ -5572,23 +5573,11 @@ const Person = Schema.Struct({ age: Schema.Int.check(Schema.isBetween({ minimum: 18, maximum: 80 })) }) -const PersonArbitrary = Schema.toArbitrary(Person) +const PersonArbitrary = Schema.toArbitrary(Person)(FastCheck) console.log(FastCheck.sample(PersonArbitrary, 3)) ``` -Use `Schema.toArbitraryLazy` only when you want the caller to provide -`fast-check`: - -```ts -import { Schema } from "effect" -import { FastCheck } from "effect/testing" - -const makeStringArbitrary = Schema.toArbitraryLazy(Schema.String) - -const StringArbitrary = makeStringArbitrary(FastCheck) -``` - `Schema.Never` and declaration schemas without a `toArbitrary` annotation cannot be derived automatically. @@ -5641,35 +5630,6 @@ This works because the final predicate check rejects strings that are not palindromes. It may need many attempts, because the base string generator has no reason to produce mirrored strings. -#### Reports - -Use `{ report: true }` when you want to know which filters did not guide -generation: - -```ts -import { Schema } from "effect" - -const isPalindrome = (s: string) => s === Array.from(s).reverse().join("") - -const Palindrome = Schema.String.check( - Schema.makeFilter(isPalindrome, { - expected: "a palindrome" - }) -) - -const result = Schema.toArbitrary(Palindrome, { report: true }) - -result.value -result.report.warnings -``` - -An `OpaqueFilter` warning means: "this filter is still checked, but it did not -help build the generator." - -Reports contain warnings only. Unsupported schemas, impossible constraints, -invalid candidates, and recursive schemas without a finite terminal path still -fail immediately. - #### Custom Filters With Constraints If part of a custom filter can be described as a normal generation constraint, @@ -5838,7 +5798,7 @@ Generic declarations receive one derivation per type parameter: For an opaque wrapper type, you usually map both sources in the same way: ```ts -import { Effect, Option, Schema, SchemaIssue, SchemaParser } from "effect" +import { Effect, Schema, SchemaIssue, SchemaParser } from "effect" class Box { private constructor(private readonly value: A) {} @@ -5859,7 +5819,7 @@ const BoxSchema = (value: A) => [value], ([valueCodec]) => (input, ast, options) => { if (!isBox(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } return Effect.map( SchemaParser.decodeUnknownEffect(valueCodec)(Box.unbox(input), options), @@ -5946,7 +5906,7 @@ const Person = Schema.Struct({ company: Company }) -console.log(FastCheck.sample(Schema.toArbitrary(Person), 3)) +console.log(FastCheck.sample(Schema.toArbitrary(Person)(FastCheck), 3)) ``` These overrides are useful because the values have domain shape: names look like @@ -6071,6 +6031,8 @@ console.log(_s.replace("b", new B({ a: new A({ s: "a" }) }))) // B { a: A { s: 'b' } } ``` +Reading through the generated `Iso` encodes the schema value, while replacing through it decodes the new focus. Either direction can throw an `Error` with the generic message `"Schema validation failed"` and a `SchemaIssue.Issue` in its `cause`. Use `SchemaIssue.makeFormatterDefault()` to format that cause when human-readable details are needed. + ### Using the Differ Module for Type-Safe JSON Patches The `Differ` module lets you compute and apply JSON Patch (RFC 6902) changes for any value described by a `Schema`. You give it a schema once, then use the returned differ to produce a patch from an old value to a new value, and to apply that patch. @@ -6150,49 +6112,55 @@ The idea is simple: if you have a `Schema` for a type `T`, you can serialize any This approach keeps patches independent from TypeScript types and uses the schema as the guardrail when turning JSON back into `T`. +Schema conversion failures from `diff` or `patch` throw an `Error` with the generic message `"Schema validation failed"` and a `SchemaIssue.Issue` in its `cause`. Format that cause explicitly with `SchemaIssue.makeFormatterDefault()`. Errors raised while applying an invalid JSON Patch operation are separate `JsonPatch` errors rather than schema validation failures. + # Schema Representation -The `SchemaRepresentation` module converts a `Schema` into a portable data structure and back again. +The `SchemaRepresentation` module exposes the structural form used to inspect, persist, compile, and rebuild schemas. + +A representation is always a projection of one side of a schema. By default, `Schema.toRepresentation` and +`SchemaRepresentation.toRepresentation` project the encoded side. Apply `Schema.toType` or `SchemaAST.toType` first when +you need the decoded type side instead. Use it when you need to: -- store schemas on disk (for example in a cache) -- send schemas over the network -- rebuild runtime schemas later -- convert to JSON Schema (Draft 2020-12) -- generate TypeScript code that recreates schemas +- inspect the structural form of a schema +- store schemas on disk or send them over the network +- rebuild runtime schemas with an explicit set of revivers +- compile live representations to JSON Schema Draft 2020-12 +- generate TypeScript code from live representations At a high level: -- `fromAST` / `fromASTs` turn a schema AST into a `Document` / `MultiDocument` -- `DocumentFromJson` (schema) round-trip that document through JSON -- `toSchema` rebuilds a runtime `Schema` from the stored representation -- `toJsonSchemaDocument` produces a Draft 2020-12 JSON Schema document -- `toCodeDocument` prepares data for code generation (via `toMultiDocument`) +- `Schema.toRepresentation(schema)` converts a schema to a `Document` +- `SchemaRepresentation.toRepresentation(ast)` and `toRepresentations(asts)` convert schema ASTs to a `Document` or + `MultiDocument` +- `toJson` / `fromJson` cross the persistence boundary +- `fromRepresentation` / `fromRepresentations` rebuild runtime schemas using explicit revivers +- `toJsonSchemaDocument` compiles a live `Document` to JSON Schema Draft 2020-12 +- `toCodeDocument` compiles a live `MultiDocument` to runtime and TypeScript source fragments ```mermaid flowchart TD - S[Schema] -->|fromAST|D{"SchemaRepresentation.Document"} - S -->|fromASTs|MD{"SchemaRepresentation.MultiDocument"} - JS["JSON Schema (draft-07, draft-2020-12, openapi-3.0, openapi-3.1)"] -->JSD - JD --> JS - JD["JsonSchema.Document"] -->|fromJsonSchemaDocument|D - D <--> |"DocumentFromJson (schema)"|JSON - D --> |toJsonSchemaDocument|JD - D --> |toSchema|S - MD --> |toCodeDocument|CodeDocument["CodeDocument"] - D --> |toMultiDocument|MD - MD --> |toJsonSchemaMultiDocument|JMD[JsonSchema.MultiDocument] - MD <--> |"MultiDocumentFromJson (schema)"|JSON + S[Schema] -->|Schema.toRepresentation|LD["live Document"] + AST[SchemaAST] -->|SchemaRepresentation.toRepresentation|LD + LD -->|toJson|JSON["JSON value"] + JSON -->|fromJson|PD["persisted Document"] + PD -->|"fromRepresentation + revivers"|S + LD -->|toJsonSchemaDocument|JD["JsonSchema.Document (draft-2020-12)"] + JD -->|fromJsonSchemaDocument|S + LD -->|toMultiDocument|LMD["live MultiDocument"] + LMD -->|toCodeDocument|CodeDocument + LMD -->|toJsonSchemaMultiDocument|JMD[JsonSchema.MultiDocument] + LMD -->|toJsonMultiDocument|JSON ``` ## The data model ### `Representation` -A `Representation` is a tagged object tree (`_tag` fields like `"String"`, `"Objects"`, `"Union"`, ...). It describes the _structure_ of a schema in a JSON-friendly way. - -Only a subset of schema features can be represented. See "Limitations" below. +A `Representation` is a tagged object tree (`_tag` fields like `"String"`, `"Objects"`, `"Union"`, ...). It describes one +structural side of a schema. Named or recursive nodes use `Reference` values instead of duplicating their definitions. ### `Document` @@ -6209,131 +6177,287 @@ A `MultiDocument` stores multiple root representations that share the same `refe This is useful if you want to serialize a set of schemas together, or if you want to generate code for multiple schemas while emitting shared definitions only once. -## Limitations +## Projection and persistence boundaries -`SchemaRepresentation` is meant for schemas that can be described without user code. +### Representations use the encoded side -That has a few consequences. +`toRepresentation` follows a schema's encoding chain and represents its last encoded side. It does not serialize the +transformation functions. -### Transformations are not supported +```ts +import { Schema } from "effect" -The representation format describes the schema's _shape_ and a set of known checks. It does not store transformation logic. +const encoded = Schema.toRepresentation(Schema.NumberFromString) +console.log(encoded.representation._tag) +// "String" -Schemas that rely on transformations cannot be round-tripped, including: +const decoded = Schema.toRepresentation(Schema.toType(Schema.NumberFromString)) +console.log(decoded.representation._tag) +// "Number" +``` -- `Schema.transform(...)` -- `Schema.encodeTo(...)` -- custom codecs or any schema that changes how values are encoded/decoded +Consequently, rebuilding `encoded` produces a schema for the string representation; it does not recreate the original +string-to-number transformation. -If you serialize a transformed schema, the transformation logic will be lost. When you rebuild it with `toSchema`, you will only get the structural schema. +### Live and persisted documents -> **Aside** (Why transformations are excluded) -> -> A transformation is user code (functions). JSON cannot store functions, and serializing functions as strings would not be safe or portable. +A live `Document` can contain functions in its ordinary annotations. These callbacks allow compilers to handle custom +behavior: -### Only built-in checks can be represented +- a check can provide `toJsonSchema` +- a declaration or check can provide `toCode` -Checks are stored as `Filter` / `FilterGroup` nodes with a small `meta` object. +Functions cannot cross the JSON persistence boundary. `toJson` removes them and keeps only JSON-valued ordinary +annotations. Nested JSON arrays and objects are preserved; a complete annotation value is omitted when it contains a +function, `undefined`, `bigint`, a symbol, a cycle, or another non-JSON value. -Only checks that match the built-in meta definitions are supported, such as: +Structural values such as bigint literals and registered unique symbols have dedicated canonical encodings. That does not +make bigint or symbol values valid generic annotations. -- string checks: `isMinLength`, `isPattern`, `isUUID`, ... -- number checks: `isInt`, `isBetween`, `isMultipleOf`, ... -- bigint checks: `isGreaterThanBigInt`, ... -- array checks: `isLength`, `isUnique`, ... -- object checks: `isMinProperties`, ... -- date checks: `isBetweenDate`, ... +### Persistence identities -Custom predicates (for example `Schema.filter((x) => ...)`) are not supported, because the representation has nowhere to store the function. +Opaque declarations and checks need a stable identity before they can be persisted: -### Annotations are filtered +```ts +interface RepresentationAnnotation { + readonly id: string + readonly payload: Schema.Json +} -Annotations are stored as a record, but: +interface CheckRepresentationAnnotation extends RepresentationAnnotation { + readonly schemas?: ReadonlyArray +} +``` -- only values that look like JSON primitives (plus `bigint` and `symbol` in the in-memory form) are kept -- some annotation keys are dropped using an internal blacklist +`id` selects a reviver, `payload` contains its JSON configuration, and a check can use `schemas` for schema dependencies. +This replaces the previous closed set of check metadata. Custom declarations and checks are therefore persistable when +they provide a representation identity and the consumer provides a matching reviver. -In practice, documentation annotations like `title` and `description` are preserved, while complex values (functions, instances, nested objects) are ignored. +An unannotated custom declaration or leaf filter can still exist in a live representation, but `toJson` rejects it because +there is no portable way to reconstruct its user code. -### Declarations need a reviver +## Creating representations -Some runtime schemas are represented as `Declaration` nodes. Rebuilding them requires a "reviver" function. +Use `Schema.toRepresentation` when starting from a schema: -`toSchema` ships with a default reviver (`toSchemaDefaultReviver`) that recognizes a fixed set of constructors, including: +```ts +import { Schema } from "effect" -- `effect/Option`, `effect/Result`, `effect/Exit`, ... -- `ReadonlyMap`, `ReadonlySet` -- `RegExp`, `URL`, `Date` -- `FormData`, `URLSearchParams`, `Uint8Array` -- `DateTime.Utc`, `effect/Duration` +const document = Schema.toRepresentation( + Schema.Struct({ name: Schema.NonEmptyString }) +) +``` + +Use the lower-level functions when working directly with ASTs or several roots: + +```ts +import { Schema, SchemaRepresentation } from "effect" + +const document = SchemaRepresentation.toRepresentation(Schema.String.ast) + +const multiDocument = SchemaRepresentation.toRepresentations([ + Schema.String.ast, + Schema.Number.ast +]) +``` -If your document contains other declarations, pass a custom `reviver` to `toSchema`. +Repeated structural nodes, identifiers, and recursive schemas are placed in `references`. Repeated `Suspend` and +`Declaration` nodes are reference candidates as well. For unions, enums, template literals, and string literals, the +converter uses an inexpensive size estimate and creates an anonymous reference only when it expects the reference to be +smaller than repeating the body. `toMultiDocument(document)` wraps a single document when a compiler requires multiple +roots. -## JSON round-tripping +An explicit `identifier` requests a reference name within a conversion. Reusing the same schema shares its reference. +Context-only copies created through `SchemaAST.replaceContext` retain the original AST as their reference owner, including +across several successive context changes. Context still belongs to each occurrence and does not, by itself, make a node a +reference candidate. Independently constructed ASTs are not canonicalized merely because their other fields contain the +same references. When distinct schemas request the same name, the first schema keeps it and later schemas receive numeric +suffixes in encounter order, such as `Value_1` and `Value_2`. Internal `~identifier` annotations are fallback allocation +hints; their generated names use the `Encoded` suffix and follow the same collision rules. + +## JSON persistence ### `toJson` / `fromJson` -- `toJson(document)` returns JSON-compatible data (safe to `JSON.stringify`) -- `fromJson(unknown)` validates and parses JSON data back into a `Document` +`toJson(document)` projects and validates a live document, then returns a `Schema.Json` value suitable for storage or +transport. `fromJson(input)` validates persisted JSON and returns a `Document`; it does not restore runtime callbacks. -Internally, these functions use a canonical JSON codec for `Document$`. This is why values like `bigint` in annotations are encoded as strings in the JSON form and restored on decode. +The multi-root equivalents are `toJsonMultiDocument` and `fromJsonMultiDocument`. -## Rebuilding runtime schemas +```ts +import { Schema, SchemaRepresentation } from "effect" -### `toSchema` +const live = Schema.toRepresentation( + Schema.String.check(Schema.isMinLength(3)) +) -`toSchema(document)` walks the representation tree and recreates a runtime schema. +const json = SchemaRepresentation.toJson(live) +const persisted = SchemaRepresentation.fromJson(json) +``` + +Persisted `Declaration` and `Filter` nodes must contain a representation identity. `fromJson` validates the document but +does not require the corresponding revivers until reconstruction. -What it does: +## Rebuilding runtime schemas -- rebuilds the structural schema nodes (`Struct`, `Tuple`, `Union`, ...) -- resolves references from `document.references` -- supports recursive references using `Schema.suspend` -- re-attaches stored annotations via `.annotate(...)` and `.annotateKey(...)` -- re-applies supported checks via `.check(...)` +### `fromRepresentation` -If you need custom handling for declarations: +`fromRepresentation` rebuilds structural nodes, resolves references, restores recursion, reattaches annotations, and +reapplies checks. Revivers are resolved by `id`; none are installed implicitly, so the `revivers` array is required even +when it is empty. ```ts -SchemaRepresentation.toSchema(document, { - reviver: (declaration, recur) => { - // Return a runtime schema to override how a Declaration is rebuilt. - // Return undefined to fall back to the default behavior. - return undefined - } +import { Schema, SchemaRepresentation } from "effect" + +const json = SchemaRepresentation.toJson( + Schema.toRepresentation( + Schema.String.check(Schema.isMinLength(3)) + ) +) + +const document = SchemaRepresentation.fromJson(json) +const rebuilt = SchemaRepresentation.fromRepresentation(document, { + revivers: [Schema.isMinLengthReviver] }) + +console.log(Schema.is(rebuilt)("abc")) +// true +console.log(Schema.is(rebuilt)("a")) +// false +``` + +Effect exports individual revivers next to the built-in declarations and checks they reconstruct, such as +`Schema.OptionReviver`, `Schema.DateReviver`, and `Schema.isMinLengthReviver`. Supply every reviver required by the +document; a missing or duplicate `id`, or a payload that does not satisfy its reviver's `payloadSchema`, is an error. + +`fromRepresentations` rebuilds the ordered roots of a `MultiDocument` in a shared reference environment. Only references +reachable from those roots are revived. + +### Custom revivers + +There are separate reviver contracts for opaque declarations, leaf filters, and opaque filter groups: + +- `DeclarationReviver

` +- `FilterReviver

` +- `FilterGroupReviver

` + +Use `makeDeclarationReviver`, `makeFilterReviver`, and `makeFilterGroupReviver` to infer `P` from `payloadSchema`. + +```ts +import { Schema, SchemaRepresentation } from "effect" + +const id = "acme/schema/minLength" + +function minLength( + minimum: number, + annotations?: Schema.Annotations.Filter +) { + return Schema.makeFilter((value) => value.length >= minimum, { + ...annotations, + representation: { id, payload: { minimum } } + }) +} + +const minLengthReviver = SchemaRepresentation.makeFilterReviver( + id, + Schema.Struct({ minimum: Schema.Number }), + ({ annotations, payload }) => minLength(payload.minimum, annotations) +) ``` -## JSON Schema output +The same reviver can then be included in the `revivers` array passed to `fromRepresentation` or +`fromRepresentations`. + +## JSON Schema + +### Exporting JSON Schema + +For a runtime schema, prefer `Schema.toJsonSchemaDocument(schema)`. It first derives the schema's canonical JSON codec, +then compiles its encoded representation to JSON Schema Draft 2020-12. During this high-level conversion, declarations +are not extracted into anonymous references: their JSON Schema body is unconstrained, and leaving it inline preserves +empty-schema simplifications. Explicit and recursive references are unaffected. + +At the lower level, `SchemaRepresentation.toJsonSchemaDocument(document)` compiles a live `Document`, and +`toJsonSchemaMultiDocument` compiles a live `MultiDocument`. Check-level `toJsonSchema` callbacks contribute JSON Schema +constraints. Opaque declarations that have not been structurally lowered compile to an unconstrained JSON Schema. + +`toJsonSchema` callbacks must treat their input schemas as immutable and return a valid JSON Schema object graph. After a +callback returns, it must not mutate that object or anything reachable from it; returning a new graph is the supported way +to produce different output during a later compilation. The compiler may cache structural comparisons while +deduplicating completed definitions, so mutating a previously returned graph can make equality results stale. + +Definitions are compared only with definitions in the same internal fallback-identifier group. Equal definitions in +different groups and definitions with explicit identifiers remain distinct. After compilation, local `#/$defs/...` +references are rewritten to the surviving definition, including references returned directly by callbacks. External +references and other local JSON Pointers remain unchanged. + +Because compiler callbacks are not persisted, compile the live document before calling `toJson`, or rebuild and lower the +schema with revivers first. + +### Importing JSON Schema -### `toJsonSchemaDocument` / `toJsonSchemaMultiDocument` +`SchemaRepresentation.fromJsonSchemaDocument` imports a JSON Schema Draft 2020-12 document as a runtime `Schema.Top`. +It does not return a representation document. -These functions convert a `Document` or `MultiDocument` into a Draft 2020-12 JSON Schema document. +`fromJsonSchemaMultiDocument` returns the ordered root schemas. It translates only definitions reachable from those +roots. To pass the result to a representation compiler, call `toRepresentations` with the returned schemas' ASTs. -This is useful for tooling that expects JSON Schema, or for producing OpenAPI-compatible schema pieces (depending on your pipeline). +Import is best-effort: JSON Schema constructs are translated to Effect schemas where possible, but the result is not a +lossless reconstruction of an original Effect schema. The optional `onEnter` callback can normalize each JSON Schema node +before it is translated. + +Regular expression constraints reached during best-effort translation are rejected by default because imported patterns +use the runtime's native regular expression engine and may block validation for an unbounded amount of time. Set +`patterns: "apply"` only for trusted documents. Set `patterns: "ignore"` to skip reached pattern constraints explicitly; +the resulting schema accepts values that the source document may reject. The policy includes `pattern`, the keys of +`patternProperties`, and patterns nested in `propertyNames`. Ignoring `patternProperties` also skips its value constraints +and `additionalProperties`, because matching keys cannot be determined without evaluating the patterns. ## Code generation ### `toCodeDocument` -`toCodeDocument` converts a `MultiDocument` into a structure that is convenient for generating TypeScript source. - -It: +`toCodeDocument` compiles a live `MultiDocument` into runtime and TypeScript source fragments. It: -- sorts references so non-recursive definitions can be emitted in dependency order -- keeps recursive definitions separate (they must be emitted using `Schema.suspend`) +- returns one `Code` value for each root +- sorts non-recursive references in dependency order +- keeps recursive references separate so callers can emit `Schema.suspend` - sanitizes reference names into valid JavaScript identifiers -- collects extra artifacts that must be emitted (enums, symbols, imports) +- collects symbol, enum, and import artifacts -You can customize: - -- `sanitizeReference` to control how `$ref` strings become identifiers -- `reviver` to generate custom code for `Declaration` nodes +Opaque declarations and checks provide code through their `toCode` callbacks. `toCodeDocument` does not accept a +reviver option. To generate code from persisted JSON, first reconstruct the schemas with `fromRepresentation` or +`fromRepresentations`, then create a new live representation so the revivers can restore the callbacks. # Error Handling and Formatting When validation fails, Schema produces structured error objects that describe what went wrong. Formatters turn those error objects into human-readable messages you can display to users or write to logs. +### Reporting Rejected Inputs + +By default, schema issues neither retain rejected input values nor include them in formatted messages. Pass `{ reportInput: true }` to a parser when the additional diagnostic context is worth the disclosure and retention risk: + +```ts +import { Result, Schema, SchemaIssue, SchemaParser } from "effect" + +const result = SchemaParser.decodeUnknownResult(Schema.String)(1, { reportInput: true }) +const formatIssue = SchemaIssue.makeFormatterDefault() + +if (Result.isFailure(result)) { + SchemaIssue.hasInput(result.failure) // true + if (SchemaIssue.hasInput(result.failure)) { + result.failure.input // 1 + } + formatIssue(result.failure) // "Expected string, got 1" +} +``` + +Value-bearing issues created by the parser then expose an enumerable own `input` field. The input is retained by reference, not copied. Use `SchemaIssue.hasInput(issue)` instead of checking `issue.input !== undefined`, because a present input whose value is `undefined` is distinct from an issue that does not retain input. + +Enabling this option can retain or disclose secrets, personally identifiable information, and large object graphs. Object enumeration, spread, serialization, `SchemaIssue.makeFormatterDefault()`, `SchemaError.message`, and Standard Schema messages may expose the retained value. A Standard Schema failure still contains only its standard `message` and `path` fields; the input can appear inside `message`, but no non-standard `input` field is added. + +User-created `SchemaIssue.Issue` values returned directly by declarations, checks, transformations, or middleware are not modified. To make a custom value-bearing issue honor `reportInput`, pass the callback's input and effective parse options to its constructor, for example `new SchemaIssue.InvalidValue(annotations, input, options)`. + ### Formatters #### StandardSchemaV1 formatter @@ -6411,7 +6535,7 @@ Output: { issues: [ { path: [ 'a' ], message: 'Missing key' }, - { path: [ 'b' ], message: 'Expected a value with a length of at least 1, got ""' } + { path: [ 'b' ], message: 'Expected a value with a length of at least 1' } ] } */ @@ -6607,7 +6731,7 @@ if (r._tag === "Failure") { { issues: [ { - message: 'Expected a value with a length of at least 1, got ""', + message: 'Expected a value with a length of at least 1', path: [ 'a' ] }, { message: 'Missing key', path: [ 'c', 0 ] }, diff --git a/.context/effect/packages/effect/benchmark/http/multipart.ts b/.context/effect/packages/effect/benchmark/http/multipart.ts new file mode 100644 index 000000000..22ef22378 --- /dev/null +++ b/.context/effect/packages/effect/benchmark/http/multipart.ts @@ -0,0 +1,122 @@ +import { Effect, Stream } from "effect" +import * as Multipart from "effect/unstable/http/Multipart" +import * as MultipartParser from "effect/unstable/http/MultipartParser" +import { Bench } from "tinybench" + +const bench = new Bench() + +const boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW" +const headers = { + "content-type": `multipart/form-data; boundary=${boundary}` +} +const encoder = new TextEncoder() + +const filePayload = (fileSize: number, chunkSize: number): Array => { + const head = encoder.encode( + `--${boundary}\r\ncontent-disposition: form-data; name="file"; filename="file.bin"\r\ncontent-type: application/octet-stream\r\n\r\n` + ) + const body = new Uint8Array(fileSize) + for (let i = 0; i < fileSize; i++) { + body[i] = i % 251 + } + const tail = encoder.encode(`\r\n--${boundary}--\r\n`) + const payload = new Uint8Array(head.length + body.length + tail.length) + payload.set(head, 0) + payload.set(body, head.length) + payload.set(tail, head.length + body.length) + const chunks: Array = [] + for (let i = 0; i < payload.length; i += chunkSize) { + chunks.push(payload.subarray(i, i + chunkSize)) + } + return chunks +} + +const fieldsPayload = (fieldCount: number): Array => { + let out = "" + for (let i = 0; i < fieldCount; i++) { + out += `--${boundary}\r\ncontent-disposition: form-data; name="field${i}"\r\n\r\nvalue of field number ${i}\r\n` + } + out += `--${boundary}--\r\n` + return [encoder.encode(out)] +} + +const file16MiB64KiB = filePayload(16 * 1024 * 1024, 64 * 1024) +const file16MiB4KiB = filePayload(16 * 1024 * 1024, 4 * 1024) +const file1MiB64KiB = filePayload(1024 * 1024, 64 * 1024) +const fields100 = fieldsPayload(100) + +const runParser = (chunks: Array) => { + const parser = MultipartParser.make({ + headers, + onField() {}, + onFile() { + return () => {} + }, + onError(error) { + throw new Error(`unexpected error: ${error._tag}`) + }, + onDone() {} + }) + for (let i = 0; i < chunks.length; i++) { + parser.write(chunks[i]) + } + parser.end() +} + +const runChannelDrain = (chunks: Array) => + Effect.runPromise( + Stream.fromArray(chunks).pipe( + Stream.pipeThroughChannel(Multipart.makeChannel(headers)), + Stream.mapEffect((part) => part._tag === "File" ? Stream.runDrain(part.content) : Effect.void), + Stream.runDrain + ) + ) + +// Stream.fromArray emits the chunks in one batch, so these are single-pull controls. +const runChannelCollect = (chunks: Array) => + Effect.runPromise( + Stream.fromArray(chunks).pipe( + Stream.pipeThroughChannel(Multipart.makeChannel(headers)), + Stream.mapEffect((part) => part._tag === "File" ? part.contentEffect : Effect.void), + Stream.runDrain + ) + ) + +// rechunk(1) forces one upstream pull per chunk, simulating network reads and +// guarding against quadratic accumulation across pulls. +const runChannelCollectStreaming = (chunks: Array) => + Effect.runPromise( + Stream.fromArray(chunks).pipe( + Stream.rechunk(1), + Stream.pipeThroughChannel(Multipart.makeChannel(headers)), + Stream.mapEffect((part) => part._tag === "File" ? part.contentEffect : Effect.void), + Stream.runDrain + ) + ) + +bench + .add("parser: 16MiB file / 64KiB chunks", function() { + runParser(file16MiB64KiB) + }) + .add("parser: 16MiB file / 4KiB chunks", function() { + runParser(file16MiB4KiB) + }) + .add("parser: 100 small fields", function() { + runParser(fields100) + }) + .add("channel drain: 16MiB file / 64KiB chunks", async function() { + await runChannelDrain(file16MiB64KiB) + }) + .add("channel collect: 16MiB file / 64KiB chunks", async function() { + await runChannelCollect(file16MiB64KiB) + }) + .add("channel collect: 1MiB file / 64KiB chunks", async function() { + await runChannelCollect(file1MiB64KiB) + }) + .add("channel collect streaming: 16MiB file / 64KiB chunks", async function() { + await runChannelCollectStreaming(file16MiB64KiB) + }) + +await bench.run() + +console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/Enums.ts b/.context/effect/packages/effect/benchmark/schema/Enums.ts deleted file mode 100644 index fad7abed0..000000000 --- a/.context/effect/packages/effect/benchmark/schema/Enums.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Schema } from "effect" -import { Bench } from "tinybench" - -/* -┌─────────┬───────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼───────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ -│ 0 │ 'good' │ '79.26 ± 0.73%' │ '83.00 ± 1.00' │ '14443802 ± 0.02%' │ '12048193 ± 143431' │ 12616336 │ -│ 1 │ 'bad' │ '131.52 ± 0.92%' │ '125.00 ± 0.00' │ '7976946 ± 0.01%' │ '8000000 ± 0' │ 7603261 │ -└─────────┴───────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ -*/ - -const bench = new Bench() - -enum Enum { - A = "a", - B = "b" -} - -const schema = Schema.Enum(Enum) - -const good = "b" -const bad = "c" - -const decodeUnknownExit = Schema.decodeUnknownExit(schema) - -// console.log(decodeUnknownExit(valid)) -// console.log(decodeUnknownExit(invalid)) - -bench - .add("good", function() { - decodeUnknownExit(good) - }) - .add("bad", function() { - decodeUnknownExit(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/Optic.ts b/.context/effect/packages/effect/benchmark/schema/Optic.ts index 0581d5fa3..5d4741772 100644 --- a/.context/effect/packages/effect/benchmark/schema/Optic.ts +++ b/.context/effect/packages/effect/benchmark/schema/Optic.ts @@ -1,19 +1,24 @@ import { Optic, Schema } from "effect" import { Bench } from "tinybench" -/* -┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ -│ 0 │ 'iso get' │ '907.53 ± 1.06%' │ '834.00 ± 1.00' │ '1159005 ± 0.02%' │ '1199041 ± 1439' │ 1101891 │ -│ 1 │ 'optic get' │ '32.79 ± 0.20%' │ '42.00 ± 1.00' │ '25353263 ± 0.00%' │ '23809524 ± 580720' │ 30500447 │ -│ 2 │ 'direct get' │ '23.12 ± 0.48%' │ '41.00 ± 1.00' │ '32734753 ± 0.01%' │ '24390244 ± 580720' │ 43255789 │ -│ 3 │ 'iso replace' │ '2693.0 ± 2.87%' │ '2459.0 ± 41.00' │ '396398 ± 0.03%' │ '406669 ± 6669' │ 371349 │ -│ 4 │ 'direct replace' │ '848.59 ± 0.45%' │ '792.00 ± 1.00' │ '1244301 ± 0.02%' │ '1262626 ± 1596' │ 1178430 │ -└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ -*/ +// Batching bounds sample storage and keeps sub-microsecond timings above timer resolution. +const batchSize = 1_000 +const bench = new Bench({ + iterations: 1_000, + time: 0, + warmupIterations: 100, + warmupTime: 0, + timestampProvider: "hrtimeNow" +}) +let sink: unknown -const bench = new Bench() +const batch = (run: () => A) => () => { + let value = run() + for (let index = 1; index < batchSize; index++) { + value = run() + } + sink = value +} // Define a class with nested properties class User extends Schema.Class("User")({ @@ -47,33 +52,53 @@ const iso = Schema.toIso(User).key("profile").key("address").key("street") const optic = Optic.id().key("profile").key("address").key("street") bench - .add("iso get", function() { - iso.get(user) - }) - .add("optic get", function() { - optic.get(user) - }) - .add("direct get", function() { - // oxlint-disable-next-line no-unused-expressions - user.profile.address.street - }) - .add("iso replace", function() { - iso.replace("Updated", user) - }) - .add("direct replace", function() { - // oxlint-disable-next-line no-new - new User({ - ...user, - profile: { - ...user.profile, - address: { - ...user.profile.address, - street: "Updated" + .add("iso get", batch(() => iso.get(user))) + .add("optic get", batch(() => optic.get(user))) + .add("direct get", batch(() => user.profile.address.street)) + .add("iso replace", batch(() => iso.replace("Updated", user))) + .add( + "direct replace", + batch(() => + new User({ + ...user, + profile: { + ...user.profile, + address: { + ...user.profile.address, + street: "Updated" + } } - } - }) - }) + }) + ) + ) await bench.run() -console.table(bench.table()) +if (sink === undefined) { + throw new Error("Benchmark did not run") +} + +console.table(bench.table((task) => { + const result = task.result + if (result?.state === "errored") { + return { + "Task name": task.name, + Error: result.error.message + } + } + if (result?.state !== "completed") { + return { + "Task name": task.name, + State: result?.state ?? "missing result" + } + } + const latencyToNs = (value: number) => value * 1_000_000 / batchSize + return { + "Task name": task.name, + "Latency avg (ns/op)": latencyToNs(result.latency.mean).toFixed(2), + "Latency med (ns/op)": latencyToNs(result.latency.p50).toFixed(2), + "Latency RME": `${result.latency.rme.toFixed(2)}%`, + "Throughput avg (ops/s)": Math.round(result.throughput.mean * batchSize), + Samples: result.latency.samplesCount + } +})) diff --git a/.context/effect/packages/effect/benchmark/schema/array.ts b/.context/effect/packages/effect/benchmark/schema/array.ts deleted file mode 100644 index b4596fabe..000000000 --- a/.context/effect/packages/effect/benchmark/schema/array.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type } from "arktype" -import { Schema } from "effect" -import { Bench } from "tinybench" -import * as v from "valibot" -import { z } from "zod/v4-mini" - -/* -┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ -│ 0 │ 'Schema (good)' │ '218.06 ± 1.54%' │ '208.00 ± 0.00' │ '4904782 ± 0.01%' │ '4807692 ± 0' │ 4585950 │ -│ 1 │ 'Schema (bad)' │ '362.29 ± 3.26%' │ '292.00 ± 1.00' │ '3199501 ± 0.01%' │ '3424658 ± 11769' │ 2760191 │ -│ 2 │ 'Valibot (good)' │ '67.50 ± 3.42%' │ '42.00 ± 1.00' │ '18944492 ± 0.02%' │ '23809524 ± 580720' │ 14872899 │ -│ 3 │ 'Valibot (bad)' │ '129.11 ± 0.74%' │ '125.00 ± 0.00' │ '8244804 ± 0.01%' │ '8000000 ± 0' │ 7745459 │ -│ 4 │ 'Arktype (good)' │ '25.76 ± 6.47%' │ '41.00 ± 1.00' │ '30181117 ± 0.01%' │ '24390244 ± 580720' │ 38824544 │ -│ 5 │ 'Arktype (bad)' │ '1837.1 ± 2.51%' │ '1750.0 ± 41.00' │ '567189 ± 0.02%' │ '571429 ± 13393' │ 544325 │ -│ 6 │ 'Zod (good)' │ '43.74 ± 4.91%' │ '42.00 ± 0.00' │ '23500345 ± 0.00%' │ '23809524 ± 0' │ 22863784 │ -│ 7 │ 'Zod (bad)' │ '5205.5 ± 0.76%' │ '4958.0 ± 83.00' │ '199317 ± 0.04%' │ '201694 ± 3360' │ 192106 │ -└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ -*/ - -const bench = new Bench() - -const schema = Schema.Array(Schema.String) - -const valibot = v.array(v.string()) - -const arktype = type("string[]") - -const zod = z.array(z.string()) - -const good = ["a", "b"] -const bad = ["a", 1] - -const decodeUnknownExit = Schema.decodeUnknownExit(schema) - -// console.log(decodeUnknownExit(good)) -// console.log(decodeUnknownExit(bad)) -// console.log(v.safeParse(valibot, good)) -// console.log(v.safeParse(valibot, bad)) -// console.log(arktype(good)) -// console.log(arktype(bad)) -// console.log(zod.safeParse(good)) -// console.log(zod.safeParse(bad)) - -bench - .add("Schema (good)", function() { - decodeUnknownExit(good) - }) - .add("Schema (bad)", function() { - decodeUnknownExit(bad) - }) - .add("Valibot (good)", function() { - v.safeParse(valibot, good) - }) - .add("Valibot (bad)", function() { - v.safeParse(valibot, bad) - }) - .add("Arktype (good)", function() { - arktype(good) - }) - .add("Arktype (bad)", function() { - arktype(bad) - }) - .add("Zod (good)", function() { - zod.safeParse(good) - }) - .add("Zod (bad)", function() { - zod.safeParse(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/filter.ts b/.context/effect/packages/effect/benchmark/schema/filter.ts deleted file mode 100644 index 6ff05e83a..000000000 --- a/.context/effect/packages/effect/benchmark/schema/filter.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type } from "arktype" -import { Schema } from "effect" -import { Bench } from "tinybench" -import * as v from "valibot" -import { z } from "zod/v4-mini" - -/* -┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ -│ 0 │ 'Schema (good)' │ '129.33 ± 0.60%' │ '125.00 ± 0.00' │ '8158156 ± 0.01%' │ '8000000 ± 0' │ 7732112 │ -│ 1 │ 'Schema (bad)' │ '239.58 ± 1.44%' │ '209.00 ± 1.00' │ '4413764 ± 0.01%' │ '4784689 ± 23003' │ 4174352 │ -│ 2 │ 'Valibot (good)' │ '49.81 ± 1.01%' │ '42.00 ± 0.00' │ '22751446 ± 0.01%' │ '23809524 ± 1' │ 20075175 │ -│ 3 │ 'Valibot (bad)' │ '70.52 ± 1.11%' │ '83.00 ± 1.00' │ '16762925 ± 0.02%' │ '12048193 ± 143431' │ 14179523 │ -│ 4 │ 'Arktype (good)' │ '23.37 ± 0.14%' │ '41.00 ± 1.00' │ '32327042 ± 0.01%' │ '24390244 ± 580720' │ 42787691 │ -│ 5 │ 'Arktype (bad)' │ '1361.2 ± 2.65%' │ '1333.0 ± 41.00' │ '757323 ± 0.01%' │ '750188 ± 23806' │ 734659 │ -│ 6 │ 'Zod (good)' │ '44.10 ± 0.72%' │ '42.00 ± 0.00' │ '23479006 ± 0.00%' │ '23809524 ± 0' │ 22674716 │ -│ 7 │ 'Zod (bad)' │ '5005.9 ± 1.50%' │ '4834.0 ± 83.00' │ '204249 ± 0.03%' │ '206868 ± 3492' │ 199767 │ -└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ -*/ - -const bench = new Bench() - -const schema = Schema.String.check(Schema.isNonEmpty()) - -const valibot = v.pipe(v.string(), v.nonEmpty()) - -const arktype = type("string > 0") - -const zod = z.string().check(z.minLength(1)) - -const good = "a" -const bad = "" - -const decodeUnknownExit = Schema.decodeUnknownExit(schema) - -// console.log(decodeUnknownExit(good)) -// console.log(decodeUnknownExit(bad)) -// console.log(v.safeParse(valibot, good)) -// console.log(v.safeParse(valibot, bad)) -// console.log(arktype(good)) -// console.log(arktype(bad)) -// console.log(zod.safeParse(good)) -// console.log(zod.safeParse(bad)) - -bench - .add("Schema (good)", function() { - decodeUnknownExit(good) - }) - .add("Schema (bad)", function() { - decodeUnknownExit(bad) - }) - .add("Valibot (good)", function() { - v.safeParse(valibot, good) - }) - .add("Valibot (bad)", function() { - v.safeParse(valibot, bad) - }) - .add("Arktype (good)", function() { - arktype(good) - }) - .add("Arktype (bad)", function() { - arktype(bad) - }) - .add("Zod (good)", function() { - zod.safeParse(good) - }) - .add("Zod (bad)", function() { - zod.safeParse(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/object.ts b/.context/effect/packages/effect/benchmark/schema/object.ts deleted file mode 100644 index 7e164daef..000000000 --- a/.context/effect/packages/effect/benchmark/schema/object.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { type } from "arktype" -import { Schema, SchemaParser } from "effect" -import { Bench } from "tinybench" -import * as v from "valibot" -import { z } from "zod/v4-mini" - -/* -┌─────────┬──────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬──────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼──────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼──────────┤ -│ 0 │ 'Schema (good)' │ '124.63 ± 0.14%' │ '125.00 ± 0.00' │ '8434862 ± 0.01%' │ '8000000 ± 0' │ 8023546 │ -│ 1 │ 'Schema (bad)' │ '203.36 ± 6.27%' │ '208.00 ± 41.00' │ '5319764 ± 0.01%' │ '4807692 ± 807692' │ 4917416 │ -│ 2 │ 'Valibot (good)' │ '48.91 ± 0.14%' │ '42.00 ± 0.00' │ '22152363 ± 0.01%' │ '23809524 ± 1' │ 20444356 │ -│ 3 │ 'Valibot (bad)' │ '101.98 ± 0.88%' │ '84.00 ± 1.00' │ '10439854 ± 0.01%' │ '11904762 ± 143431' │ 9806230 │ -│ 4 │ 'Arktype (good)' │ '14.57 ± 1.46%' │ '0.00 ± 0.00' │ '53567410 ± 0.01%' │ '68616078 ± 0' │ 68616080 │ -│ 5 │ 'Arktype (bad)' │ '2001.6 ± 7.05%' │ '1750.0 ± 41.00' │ '554199 ± 0.03%' │ '571429 ± 13393' │ 499602 │ -│ 6 │ 'Zod (good)' │ '33.75 ± 6.13%' │ '41.00 ± 1.00' │ '25375235 ± 0.00%' │ '24390240 ± 580716' │ 29633666 │ -│ 7 │ 'Zod (bad)' │ '5392.2 ± 3.11%' │ '5167.0 ± 42.00' │ '190866 ± 0.03%' │ '193536 ± 1586' │ 185454 │ -└─────────┴──────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴──────────┘ -*/ - -const bench = new Bench() - -const schema = Schema.Struct({ - a: Schema.String -}) - -const valibot = v.object({ - a: v.string() -}) - -const arktype = type({ - a: "string" -}) - -const zod = z.object({ - a: z.string() -}) - -const good = { a: "a" } -const bad = { a: 1 } - -const decodeUnknownExit = SchemaParser.decodeUnknownExit(schema) - -// console.log(decodeUnknownExit(good)) -// console.log(decodeUnknownExit(bad)) -// console.log(v.safeParse(valibot, good)) -// console.log(v.safeParse(valibot, bad)) -// console.log(arktype(good)) -// console.log(arktype(bad)) -// console.log(zod.safeParse(good)) -// console.log(zod.safeParse(bad)) - -bench - .add("Schema (good)", function() { - decodeUnknownExit(good) - }) - .add("Schema (bad)", function() { - decodeUnknownExit(bad) - }) - .add("Valibot (good)", function() { - v.safeParse(valibot, good) - }) - .add("Valibot (bad)", function() { - v.safeParse(valibot, bad) - }) - .add("Arktype (good)", function() { - arktype(good) - }) - .add("Arktype (bad)", function() { - arktype(bad) - }) - .add("Zod (good)", function() { - zod.safeParse(good) - }) - .add("Zod (bad)", function() { - zod.safeParse(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/tagged-union.ts b/.context/effect/packages/effect/benchmark/schema/tagged-union.ts deleted file mode 100644 index 2250ea4c0..000000000 --- a/.context/effect/packages/effect/benchmark/schema/tagged-union.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Array as RA, Schema } from "effect" -import { Bench } from "tinybench" - -/* -┌─────────┬────────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬─────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼────────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼─────────┤ -│ 0 │ 'Schema (good)' │ '488.43 ± 0.48%' │ '459.00 ± 1.00' │ '2119495 ± 0.01%' │ '2178649 ± 4757' │ 2047396 │ -│ 1 │ 'Schema (bad)' │ '629.17 ± 0.29%' │ '584.00 ± 1.00' │ '1645689 ± 0.01%' │ '1712329 ± 2937' │ 1589395 │ -│ 2 │ 'candidate (good)' │ '327.20 ± 0.27%' │ '292.00 ± 1.00' │ '3205291 ± 0.01%' │ '3424658 ± 11769' │ 3056264 │ -│ 3 │ 'candidate (bad)' │ '449.52 ± 2.41%' │ '417.00 ± 0.00' │ '2372897 ± 0.01%' │ '2398082 ± 0' │ 2224610 │ -└─────────┴────────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴─────────┘ -*/ - -const bench = new Bench({ time: 1000 }) - -const n = 100 -const f = (i: number) => - Schema.Struct({ - kind: Schema.Literal(i), - a: Schema.String, - b: Schema.Number, - c: Schema.Boolean - }) -const members = RA.makeBy(n, f) - -const schema = Schema.Union(members) - -const candidate = f(n - 1) - -const good = { - kind: n - 1, - a: "a", - b: 1, - c: true -} - -const bad = { - kind: n - 1, - a: "a", - b: 1, - c: "c" -} - -const decodeUnknownExit = Schema.decodeUnknownExit(schema) -const decodeUnknownExitCandidate = Schema.decodeUnknownExit(candidate) - -// console.log(decodeUnknownExit(good)) -// console.log(decodeUnknownExit(bad)) -// console.log(decodeUnknownExitCandidate(good)) -// console.log(decodeUnknownExitCandidate(bad)) - -bench - .add("Schema (good)", function() { - decodeUnknownExit(good) - }) - .add("Schema (bad)", function() { - decodeUnknownExit(bad) - }) - .add("candidate (good)", function() { - decodeUnknownExitCandidate(good) - }) - .add("candidate (bad)", function() { - decodeUnknownExitCandidate(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/benchmark/schema/transformation.ts b/.context/effect/packages/effect/benchmark/schema/transformation.ts deleted file mode 100644 index 40cae620f..000000000 --- a/.context/effect/packages/effect/benchmark/schema/transformation.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Schema, SchemaTransformation } from "effect" -import { Bench } from "tinybench" -import { z } from "zod" - -/* -┌─────────┬─────────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐ -│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ -├─────────┼─────────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤ -│ 0 │ 'Schema (good)' │ '1097.8 ± 1.12%' │ '1042.0 ± 1.00' │ '949296 ± 0.01%' │ '959693 ± 922' │ 910953 │ -│ 1 │ 'Zod (good)' │ '267.92 ± 4.46%' │ '208.00 ± 0.00' │ '4505289 ± 0.02%' │ '4807692 ± 0' │ 3732515 │ -│ 2 │ 'Schema (bad)' │ '683.49 ± 1.54%' │ '625.00 ± 0.00' │ '1593775 ± 0.01%' │ '1600000 ± 0' │ 1463090 │ -│ 3 │ 'Zod (bad)' │ '8172.9 ± 4.43%' │ '6417.0 ± 125.00' │ '152563 ± 0.07%' │ '155836 ± 3096' │ 122357 │ -└─────────┴─────────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘ -*/ - -const bench = new Bench() - -const schema = Schema.Struct({ - a: Schema.String, - id: Schema.String, - c: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), - d: Schema.String -}).pipe(Schema.decodeTo( - Schema.Struct({ - a: Schema.String, - b: Schema.Struct({ id: Schema.String }), - c: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)), - d: Schema.String - }), - SchemaTransformation.transform({ - decode: ({ id, ...v }) => ({ ...v, b: { id } }), - encode: ({ b: { id }, ...v }) => ({ ...v, id }) - }) -)) - -const zod = z.codec( - z.object({ - a: z.string(), - id: z.string(), - c: z.number().check(z.nonnegative()), - d: z.string() - }), - z.object({ - a: z.string(), - b: z.object({ id: z.string() }), - c: z.number().check(z.nonnegative()), - d: z.string() - }), - { - decode: ({ id, ...v }) => ({ ...v, b: { id } }), - encode: ({ b: { id }, ...v }) => ({ ...v, id }) - } -) - -const good = { - a: "a", - id: "id", - c: 1, - d: "d" -} -const bad = { - a: "a", - id: "id", - c: -1, - d: "d" -} - -const decodeUnknownExit = Schema.decodeUnknownExit(schema) - -// console.log(decodeUnknownExit(good)) -// console.log(String(decodeUnknownExit(bad))) -// console.log(zod.safeDecode(good)) -// console.log(zod.safeDecode(bad)) - -bench - .add("Schema (good)", function() { - decodeUnknownExit(good) - }) - .add("Zod (good)", function() { - zod.safeDecode(good) - }) - .add("Schema (bad)", function() { - decodeUnknownExit(bad) - }) - .add("Zod (bad)", function() { - zod.safeDecode(bad) - }) - -await bench.run() - -console.table(bench.table()) diff --git a/.context/effect/packages/effect/docgen.json b/.context/effect/packages/effect/docgen.json deleted file mode 100644 index b47475e4b..000000000 --- a/.context/effect/packages/effect/docgen.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "exclude": ["src/internal/**/*.ts", "src/unstable/**/internal/**/*.ts", "src/schema/StandardSchema.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": ["ESNext", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"], - "@effect/platform-node": ["../../../platform-node/src/index.js"], - "@effect/platform-node/*": ["../../../platform-node/src/*.js"] - }, - "plugins": [ - { - "name": "@effect/language-service", - "includeSuggestionsInTsc": false, - "diagnosticSeverity": { - "unusedDirective": "off", - "floatingEffect": "off", - "multipleEffectProvide": "off", - "globalErrorInEffectFailure": "off", - "unknownInEffectCatch": "off", - "globalErrorInEffectCatch": "off", - "missingReturnYieldStar": "off" - } - } - ] - } -} diff --git a/.context/effect/packages/effect/package.json b/.context/effect/packages/effect/package.json index 60c4c3360..df5b2912f 100644 --- a/.context/effect/packages/effect/package.json +++ b/.context/effect/packages/effect/package.json @@ -1,7 +1,7 @@ { "name": "effect", "type": "module", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "license": "MIT", "description": "The missing standard library for TypeScript, for writing production-grade software.", "homepage": "https://effect.website", @@ -49,9 +49,10 @@ "./unstable/workflow": "./src/unstable/workflow/index.ts", "./unstable/workers": "./src/unstable/workers/index.ts", "./*": "./src/*.ts", - "./internal/*": null, "./unstable/cli/internal/*": null, "./unstable/cluster/internal/*": null, + "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -59,7 +60,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -90,6 +94,7 @@ "./internal/*": null, "./unstable/cli/internal/*": null, "./unstable/cluster/internal/*": null, + "./index": null, "./*/index": null } }, @@ -97,15 +102,12 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest --sequence.concurrent=false", - "coverage": "vitest --run --coverage --sequence.concurrent=false" + "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@types/ini": "^4.1.1", - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "ajv": "^8.20.0", - "arktype": "^2.2.3", + "ajv-draft-04": "^1.0.0", "ast-types": "^0.14.2", "immer": "^11.1.11", "tinybench": "^6.0.2", @@ -114,13 +116,8 @@ "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", - "find-my-way-ts": "^0.1.6", - "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", - "multipasta": "^0.2.8", - "toml": "^4.1.2", - "uuid": "^14.0.1", - "yaml": "^2.9.0" + "uuid": "^14.0.1" } } diff --git a/.context/effect/packages/effect/runtimeperf/README.md b/.context/effect/packages/effect/runtimeperf/README.md new file mode 100644 index 000000000..8ff0458bb --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/README.md @@ -0,0 +1,135 @@ +# Effect Runtime Performance + +This harness measures focused synchronous runtime paths in fresh Node +processes. It supports: + +- focused Effect Schema diagnostics; +- the upstream Effect, Valibot and Zod benchmark matrix; +- paired comparisons between Git revisions or the current working tree. + +Cross-library results are diagnostic. Effect base/head comparisons are the +authoritative measurement for source changes. + +## Commands + +Run the complete registry: + +```sh +pnpm runtimeperf +``` + +Run the cases extracted from the `effect@beta`, Valibot and Zod adapters in +[`open-circle/schema-benchmarks`](https://github.com/open-circle/schema-benchmarks): + +```sh +pnpm runtimeperf schema-benchmarks +pnpm runtimeperf-compare schema-benchmarks --base main --head HEAD +``` + +This suite covers every upstream timing case supported by those adapters: +schema and decoder initialization, validation, parsing and Standard Schema +with valid/invalid inputs and first/all error modes, plus BigInt codec +operations. The upstream bundle and stack reports are not throughput +benchmarks, and the adapters do not define the optional string-format cases. + +Select a suite, fixture, shared scenario, tier, family or implementation: + +```sh +pnpm runtimeperf schema +pnpm runtimeperf object-32-valid +pnpm runtimeperf schema/object-32-valid-effect +pnpm runtimeperf --family arrays +pnpm runtimeperf --implementation zod4 +``` + +Override measurement settings: + +```sh +pnpm runtimeperf object-32-valid --rounds 9 --time 500 --warmup-time 150 +``` + +Compare Effect `HEAD` with the working tree: + +```sh +pnpm runtimeperf-compare schema/object-32-valid-effect +``` + +Compare explicit refs: + +```sh +pnpm runtimeperf-compare schema --base main --head HEAD +``` + +Only `--fail-on-regression` turns a statistically classified regression into a +non-zero comparison exit code. Worker, fixture, configuration and Git errors +always fail. + +Reports are written under `tmp/runtimeperf/results/`. Temporary Git worktrees +are created under `tmp/runtimeperf/worktrees/` and removed in `finally`. + +## Registry + +`config.json` groups fixture cases by source file. Every case records: + +- tier and family; +- covered Effect AST tags; +- operation and path; +- input size; +- implementation; +- a shared scenario name for cross-library comparisons. + +The `schema` suite is Effect-only and retains targeted diagnostics for scaling, +template literals, unions, records, transformations, optional properties, +adapters, recursion and cold paths. The `schema-benchmarks` suite contains the +complete timing matrices exposed by the upstream Effect, Valibot and Zod +adapters. + +Zod parsing cases import `zod/v4` and call `safeParse` with `{ jitless: true }`; +its Standard Schema and codec cases use their native APIs. Valibot uses the +corresponding `is`, `safeParse` and Standard Schema APIs. The focused Effect +adapter family measures the overhead of public APIs that wrap parser issues. + +## Measurement model + +Each worker validates the fixture before and after measuring. Calibration finds +a batch large enough for the configured target duration. Each implementation +uses its own calibrated batch and executes in a separate process, with rotating +order within the scenario. + +Tinybench measures one synchronous batched task. The primary process result is: + +```text +nsPerOp = totalTimeMs * 1_000_000 / (task.runs * batchSize) +``` + +Tinybench latency statistics are retained as diagnostics and normalized by the +batch size. Independent Node processes, not Tinybench samples, are the +statistical observations. + +Base/head comparisons alternate execution order by round and analyze paired +log ratios with a deterministic bootstrap. The report keeps all raw worker +results so aggregates can be recalculated. + +## Fixture contract + +Each configured export is a factory: + +```ts +type RuntimePerfCase = { + readonly run: () => unknown + readonly validate: (result: unknown) => void +} + +type RuntimePerfCaseFactory = () => RuntimePerfCase +``` + +Construct schemas, steady-state adapters and deterministic inputs in the +factory. Cold fixtures deliberately construct them inside `run`. A fixture +must measure one named operation, perform no I/O and return no Promise. + +## Validation + +```sh +node --test packages/effect/runtimeperf/test/*.test.mts +pnpm exec dprint check package.json packages/effect/runtimeperf +``` diff --git a/.context/effect/packages/effect/runtimeperf/compare.mts b/.context/effect/packages/effect/runtimeperf/compare.mts new file mode 100644 index 000000000..7c2cf346e --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/compare.mts @@ -0,0 +1,265 @@ +import { spawnSync } from "node:child_process" +import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs" +import os from "node:os" +import { join } from "node:path" +import process from "node:process" +import { materializeFixture } from "./materialize.mts" +import { analyzePairs } from "./stats.mts" +import { + aggregateMeasurements, + calibrateFixture, + configPath, + coverageSummary, + effectDir, + formatNs, + hashFile, + libraryVersions, + loadRegistry, + makeRunId, + measureFixture, + parseArgs, + printTable, + relativeToRepo, + repoRoot, + reportPath, + resolveDefaults, + selectFixtures, + sha256, + workerPath, + writeJson +} from "./utils.mts" + +const usage = `Usage: pnpm runtimeperf-compare [suite[/fixture]|scenario] [options] + +Options: + --base Defaults to HEAD + --head Defaults to worktree + --rounds + --time + --warmup-time + --tier <0-3> + --family + --fail-on-regression +` + +const run = (command, args, options = {}) => + spawnSync(command, args, { + encoding: "utf8", + maxBuffer: 50 * 1024 * 1024, + ...options + }) + +const runGit = (args) => { + const result = run("git", args, { cwd: repoRoot }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${result.stdout}${result.stderr}`.trim()) + } + return result.stdout.trim() +} + +const resolveRef = (ref) => runGit(["rev-parse", "--verify", `${ref}^{commit}`]) + +const linkDirectory = (source, target) => { + if (!existsSync(target)) { + symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir") + } +} + +const removeWorktree = (path) => { + const result = run("git", ["worktree", "remove", "--force", path], { cwd: repoRoot }) + if (result.status !== 0) { + process.stderr.write(`${result.stdout}${result.stderr}`) + } +} + +const createWorktree = (runRoot, name, sha) => { + const path = join(runRoot, name) + const result = run("git", ["worktree", "add", "--detach", path, sha], { cwd: repoRoot }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${result.stdout}${result.stderr}`.trim()) + } + try { + linkDirectory(join(repoRoot, "node_modules"), join(path, "node_modules")) + linkDirectory(join(effectDir, "node_modules"), join(path, "packages", "effect", "node_modules")) + } catch (error) { + removeWorktree(path) + throw error + } + return path +} + +const worktreeState = () => { + const diff = runGit(["diff", "--binary", "HEAD", "--"]) + const untrackedOutput = runGit([ + "ls-files", + "--others", + "--exclude-standard", + "--", + "package.json", + "packages/effect" + ]) + const untracked = untrackedOutput === "" + ? [] + : untrackedOutput.split("\n").map((path) => ({ + path, + hash: hashFile(join(repoRoot, path)) + })) + return { + dirty: diff !== "" || untracked.length > 0, + diffHash: sha256(diff), + untracked + } +} + +const main = () => { + const options = parseArgs(process.argv.slice(2), { compare: true }) + if (options.help) { + process.stdout.write(usage) + return + } + const { config, fixtures } = loadRegistry() + const selected = selectFixtures(fixtures, options, { effectOnly: true }) + const defaults = resolveDefaults(config, options) + const baseSha = resolveRef(options.base) + const headSha = options.head === "worktree" ? resolveRef("HEAD") : resolveRef(options.head) + const state = worktreeState() + const runId = makeRunId() + const tmpRoot = join(repoRoot, "tmp", "runtimeperf", "worktrees") + mkdirSync(tmpRoot, { recursive: true }) + const runRoot = mkdtempSync(join(tmpRoot, "run-")) + const worktrees = [] + const results = [] + const executionOrder = [] + + try { + const baseRoot = createWorktree(runRoot, "base", baseSha) + worktrees.push(baseRoot) + const headRoot = options.head === "worktree" + ? repoRoot + : createWorktree(runRoot, "head", headSha) + if (headRoot !== repoRoot) worktrees.push(headRoot) + + for (const fixture of selected) { + const baseFixturePath = materializeFixture(baseRoot, fixture) + const headFixturePath = headRoot === repoRoot + ? fixture.fixturePath + : materializeFixture(headRoot, fixture) + const baseCalibration = calibrateFixture(fixture, defaults, baseFixturePath) + const headCalibration = calibrateFixture(fixture, defaults, headFixturePath) + const batchSize = Math.max(baseCalibration.batchSize, headCalibration.batchSize) + const baseMeasurements = [] + const headMeasurements = [] + + for (let round = 0; round < defaults.rounds; round++) { + const order = round % 2 === 0 ? ["base", "head"] : ["head", "base"] + for (const side of order) { + const measurement = side === "base" + ? measureFixture(fixture, defaults, batchSize, baseFixturePath) + : measureFixture(fixture, defaults, batchSize, headFixturePath) + ;(side === "base" ? baseMeasurements : headMeasurements).push(measurement) + executionOrder.push({ target: fixture.target, round: round + 1, side }) + } + } + + results.push({ + fixture, + batchSize, + calibration: { + base: baseCalibration, + head: headCalibration + }, + base: { + measurements: baseMeasurements, + aggregate: aggregateMeasurements(baseMeasurements) + }, + head: { + measurements: headMeasurements, + aggregate: aggregateMeasurements(headMeasurements) + }, + comparison: analyzePairs( + baseMeasurements.map((item) => item.nsPerOp), + headMeasurements.map((item) => item.nsPerOp), + { + iterations: defaults.bootstrapIterations, + seed: defaults.bootstrapSeed, + minImprovementPercent: defaults.minImprovementPercent, + maxRegressionPercent: defaults.maxRegressionPercent + } + ) + }) + } + } finally { + for (const worktree of worktrees.reverse()) { + removeWorktree(worktree) + } + rmSync(runRoot, { recursive: true, force: true }) + } + + const report = { + schemaVersion: 1, + kind: "comparison", + runId, + target: options.target ?? null, + filters: { + tier: options.tier ?? null, + family: options.family ?? null + }, + config: defaults, + base: { + ref: options.base, + sha: baseSha + }, + head: { + ref: options.head, + sha: headSha, + worktree: options.head === "worktree" ? state : undefined + }, + environment: { + node: process.version, + v8: process.versions.v8, + platform: process.platform, + arch: process.arch, + cpu: os.cpus()[0]?.model ?? "unknown" + }, + libraries: libraryVersions(), + artifactMode: "repository", + coverage: coverageSummary(selected), + hashes: { + config: hashFile(configPath), + worker: hashFile(workerPath), + fixtures: Object.fromEntries( + [...new Set(selected.map((fixture) => fixture.fixturePath))] + .map((path) => [relativeToRepo(path), hashFile(path)]) + ) + }, + executionOrder, + results + } + const path = reportPath(runId, options.target, "compare") + writeJson(path, report) + printTable( + ["fixture", "base", "head", "delta", "ci low", "ci high", "status"], + results.map((result) => [ + result.fixture.target, + formatNs(result.base.aggregate.median), + formatNs(result.head.aggregate.median), + `${result.comparison.deltaPercent.toFixed(2)}%`, + `${result.comparison.lowPercent.toFixed(2)}%`, + `${result.comparison.highPercent.toFixed(2)}%`, + result.comparison.status + ]) + ) + process.stdout.write(`\nReport: ${relativeToRepo(path)}\n`) + if (options.failOnRegression && results.some((result) => result.comparison.status === "regression")) { + process.exitCode = 1 + } +} + +try { + main() +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : error}\n`) + process.exitCode = 1 +} diff --git a/.context/effect/packages/effect/runtimeperf/config.json b/.context/effect/packages/effect/runtimeperf/config.json new file mode 100644 index 000000000..92d07ddf0 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/config.json @@ -0,0 +1,1000 @@ +{ + "defaults": { + "rounds": 5, + "timeMs": 300, + "warmupTimeMs": 100, + "targetBatchTimeNs": 100000, + "maxBatchSize": 1048576, + "bootstrapIterations": 10000, + "bootstrapSeed": 1592594996, + "minImprovementPercent": 2, + "maxRegressionPercent": 5 + }, + "suites": [ + { + "name": "schema", + "fixtures": [ + { + "file": "suites/schema/fixtures/comparison.ts", + "defaults": { + "tier": 1, + "operation": "decode" + }, + "cases": [ + { + "name": "object-32-valid-effect", + "export": "effectObject32Valid", + "scenario": "object-32-valid", + "implementation": "effect", + "family": "objects", + "astTags": [ + "Objects" + ], + "path": "valid", + "size": 32 + }, + { + "name": "object-32-invalid-last-effect", + "export": "effectObject32InvalidLast", + "scenario": "object-32-invalid-last", + "implementation": "effect", + "family": "objects", + "astTags": [ + "Objects" + ], + "path": "invalid", + "size": 32 + }, + { + "name": "array-32-valid-effect", + "export": "effectArray32Valid", + "scenario": "array-32-valid", + "implementation": "effect", + "family": "arrays", + "astTags": [ + "Arrays" + ], + "path": "valid", + "size": 32 + }, + { + "name": "array-32-invalid-last-effect", + "export": "effectArray32InvalidLast", + "scenario": "array-32-invalid-last", + "implementation": "effect", + "family": "arrays", + "astTags": [ + "Arrays" + ], + "path": "invalid", + "size": 32 + }, + { + "name": "record-32-valid-effect", + "export": "effectRecord32Valid", + "scenario": "record-32-valid", + "implementation": "effect", + "family": "records", + "astTags": [ + "Objects" + ], + "path": "valid", + "size": 32 + }, + { + "name": "record-32-invalid-last-effect", + "export": "effectRecord32InvalidLast", + "scenario": "record-32-invalid-last", + "implementation": "effect", + "family": "records", + "astTags": [ + "Objects" + ], + "path": "invalid", + "size": 32 + }, + { + "name": "literal-100-valid-last-effect", + "export": "effectLiteral100ValidLast", + "scenario": "literal-100-valid-last", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Literal" + ], + "path": "valid", + "size": 100 + }, + { + "name": "literal-100-invalid-effect", + "export": "effectLiteral100Invalid", + "scenario": "literal-100-invalid", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Literal" + ], + "path": "invalid", + "size": 100 + }, + { + "name": "tagged-100-valid-last-effect", + "export": "effectTagged100ValidLast", + "scenario": "tagged-100-valid-last", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "valid", + "size": 100 + }, + { + "name": "tagged-100-invalid-selected-effect", + "export": "effectTagged100InvalidSelected", + "scenario": "tagged-100-invalid-selected", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "invalid", + "size": 100 + }, + { + "name": "tagged-100-invalid-tag-effect", + "export": "effectTagged100InvalidTag", + "scenario": "tagged-100-invalid-tag", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "invalid", + "size": 100 + }, + { + "name": "multi-sentinel-100-valid-first-effect", + "export": "effectMultiSentinel100ValidFirst", + "scenario": "multi-sentinel-100-valid-first", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "valid", + "size": 100 + }, + { + "name": "multi-sentinel-100-invalid-variant-effect", + "export": "effectMultiSentinel100InvalidVariant", + "scenario": "multi-sentinel-100-invalid-variant", + "implementation": "effect", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "invalid", + "size": 100 + } + ] + }, + { + "file": "suites/schema/fixtures/behavior.ts", + "defaults": { + "tier": 2, + "implementation": "effect", + "operation": "decode" + }, + "cases": [ + { + "name": "checks-invalid-first", + "export": "checksInvalidFirst", + "scenario": "checks-invalid-first", + "family": "checks", + "astTags": [ + "String" + ], + "path": "invalid", + "size": 2 + }, + { + "name": "checks-invalid-last", + "export": "checksInvalidLast", + "scenario": "checks-invalid-last", + "family": "checks", + "astTags": [ + "String" + ], + "path": "invalid", + "size": 2 + }, + { + "name": "encoding-check-valid", + "export": "encodingCheckValid", + "scenario": "encoding-check-valid", + "family": "checks", + "astTags": [ + "String" + ], + "path": "valid", + "size": 1 + }, + { + "name": "template-literal-linear-valid", + "export": "templateLiteralLinearValid", + "scenario": "template-literal-linear-valid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Arrays" + ], + "path": "valid", + "size": 5 + }, + { + "name": "template-literal-linear-invalid", + "export": "templateLiteralLinearInvalid", + "scenario": "template-literal-linear-invalid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Arrays" + ], + "path": "invalid", + "size": 5 + }, + { + "name": "template-literal-backtracking-valid", + "export": "templateLiteralBacktrackingValid", + "scenario": "template-literal-backtracking-valid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Arrays", + "String" + ], + "path": "valid", + "size": 4 + }, + { + "name": "template-literal-backtracking-invalid", + "export": "templateLiteralBacktrackingInvalid", + "scenario": "template-literal-backtracking-invalid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Arrays", + "String" + ], + "path": "invalid", + "size": 4 + }, + { + "name": "template-literal-transformed-valid", + "export": "templateLiteralTransformedValid", + "scenario": "template-literal-transformed-valid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Arrays", + "String", + "Number" + ], + "path": "valid", + "size": 3 + }, + { + "name": "template-literal-record-32-valid", + "export": "templateLiteralRecord32Valid", + "scenario": "template-literal-record-32-valid", + "family": "template-literal", + "astTags": [ + "TemplateLiteral", + "Objects", + "String", + "Number" + ], + "path": "valid", + "size": 32 + }, + { + "name": "transformation-decode-valid", + "export": "transformationDecodeValid", + "scenario": "transformation-decode-valid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "path": "valid", + "size": 1 + }, + { + "name": "transformation-decode-invalid", + "export": "transformationDecodeInvalid", + "scenario": "transformation-decode-invalid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "path": "invalid", + "size": 1 + }, + { + "name": "transformation-encode-valid", + "export": "transformationEncodeValid", + "scenario": "transformation-encode-valid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "operation": "encode", + "path": "valid", + "size": 1 + }, + { + "name": "encoding-chain-8-decode-valid", + "export": "encodingChain8DecodeValid", + "scenario": "encoding-chain-8-decode-valid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "path": "valid", + "size": 8 + }, + { + "name": "encoding-chain-8-decode-invalid", + "export": "encodingChain8DecodeInvalid", + "scenario": "encoding-chain-8-decode-invalid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "path": "invalid", + "size": 8 + }, + { + "name": "encoding-chain-8-encode-valid", + "export": "encodingChain8EncodeValid", + "scenario": "encoding-chain-8-encode-valid", + "family": "transformation", + "astTags": [ + "String", + "Number" + ], + "operation": "encode", + "path": "valid", + "size": 8 + }, + { + "name": "record-32-transformed-keys-valid", + "export": "transformedKeyRecordValid", + "scenario": "record-32-transformed-keys-valid", + "family": "records", + "astTags": [ + "Objects", + "String" + ], + "path": "valid", + "size": 32 + }, + { + "name": "optional-valid", + "export": "optionalValid", + "scenario": "optional-valid", + "family": "optional", + "astTags": [ + "Objects", + "Union", + "Undefined" + ], + "path": "valid", + "size": 3 + }, + { + "name": "optional-present-valid", + "export": "optionalPresentValid", + "scenario": "optional-present-valid", + "family": "optional", + "astTags": [ + "Objects", + "Union", + "Undefined" + ], + "path": "valid", + "size": 3 + }, + { + "name": "optional-present-invalid", + "export": "optionalPresentInvalid", + "scenario": "optional-present-invalid", + "family": "optional", + "astTags": [ + "Objects", + "Union", + "Undefined" + ], + "path": "invalid", + "size": 3 + }, + { + "name": "object-32-suspended-middle-valid", + "export": "object32SuspendedMiddleValid", + "scenario": "object-32-suspended-middle-valid", + "family": "objects", + "astTags": [ + "Objects", + "String", + "Transformation" + ], + "path": "valid", + "size": 32 + }, + { + "name": "literal-2-valid-last", + "export": "literal2ValidLast", + "scenario": "literal-2-valid-last", + "family": "union", + "astTags": [ + "Union", + "Literal" + ], + "path": "valid", + "size": 2 + }, + { + "name": "literal-100-valid-first", + "export": "literal100ValidFirst", + "scenario": "literal-100-valid-first", + "family": "union", + "astTags": [ + "Union", + "Literal" + ], + "path": "valid", + "size": 100 + }, + { + "name": "homogeneous-union-100-invalid", + "export": "homogeneousUnion100Invalid", + "scenario": "homogeneous-union-100-invalid", + "family": "union", + "astTags": [ + "Union", + "String" + ], + "path": "invalid", + "size": 100 + }, + { + "name": "tagged-2-valid-last", + "export": "tagged2ValidLast", + "scenario": "tagged-2-valid-last", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "valid", + "size": 2 + }, + { + "name": "tagged-with-fallback-valid", + "export": "taggedWithFallbackValid", + "scenario": "tagged-with-fallback-valid", + "family": "union", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "path": "valid", + "size": 2 + }, + { + "name": "property-order-original", + "export": "propertyOrderOriginal", + "scenario": "property-order-original", + "family": "parse-options", + "astTags": [ + "Objects" + ], + "path": "valid", + "size": 3 + }, + { + "name": "recursive-tree-depth-16-valid", + "export": "recursiveTreeDepth16Valid", + "scenario": "recursive-tree-depth-16-valid", + "family": "recursion", + "astTags": [ + "Objects", + "Arrays", + "Suspend" + ], + "path": "valid", + "size": 16 + } + ] + }, + { + "file": "suites/schema/fixtures/adapters.ts", + "defaults": { + "tier": 2, + "implementation": "effect", + "family": "adapters", + "astTags": [ + "Objects" + ], + "path": "valid", + "size": 2 + }, + "cases": [ + { + "name": "adapter-parser-decode-exit-invalid", + "export": "parserExitInvalid", + "scenario": "adapter-parser-decode-exit-invalid", + "operation": "decode", + "path": "invalid", + "adapter": "SchemaParser Exit" + }, + { + "name": "adapter-decode-exit-valid", + "export": "exitValid", + "scenario": "adapter-decode-exit-valid", + "operation": "decode", + "adapter": "Exit" + }, + { + "name": "adapter-decode-exit-invalid", + "export": "exitInvalid", + "scenario": "adapter-decode-exit-invalid", + "operation": "decode", + "path": "invalid", + "adapter": "Exit" + }, + { + "name": "adapter-decode-option-valid", + "export": "optionValid", + "scenario": "adapter-decode-option-valid", + "operation": "decode", + "adapter": "Option" + }, + { + "name": "adapter-decode-option-invalid", + "export": "optionInvalid", + "scenario": "adapter-decode-option-invalid", + "operation": "decode", + "path": "invalid", + "adapter": "Option" + }, + { + "name": "adapter-decode-result-valid", + "export": "resultValid", + "scenario": "adapter-decode-result-valid", + "operation": "decode", + "adapter": "Result" + }, + { + "name": "adapter-decode-result-invalid", + "export": "resultInvalid", + "scenario": "adapter-decode-result-invalid", + "operation": "decode", + "path": "invalid", + "adapter": "Result" + }, + { + "name": "adapter-decode-sync-invalid", + "export": "syncInvalid", + "scenario": "adapter-decode-sync-invalid", + "operation": "decode", + "path": "invalid", + "adapter": "Sync" + } + ] + }, + { + "file": "suites/schema/fixtures/cold.ts", + "defaults": { + "tier": 2, + "family": "cold", + "astTags": [ + "Objects" + ], + "path": "valid", + "size": 32 + }, + "cases": [ + { + "name": "schema-creation-template-literal-effect", + "export": "effectSchemaCreationTemplateLiteral", + "scenario": "schema-creation-template-literal", + "implementation": "effect", + "operation": "schema", + "astTags": [ + "TemplateLiteral" + ], + "size": 5 + }, + { + "name": "first-decode-checked-object-32-effect", + "export": "effectFirstDecodeCheckedObject32", + "scenario": "first-decode-checked-object-32", + "implementation": "effect", + "operation": "first-decode" + }, + { + "name": "first-decode-template-literal-effect", + "export": "effectFirstDecodeTemplateLiteral", + "scenario": "first-decode-template-literal", + "implementation": "effect", + "operation": "first-decode", + "astTags": [ + "TemplateLiteral" + ], + "size": 5 + }, + { + "name": "first-decode-record-32-effect", + "export": "effectFirstDecodeRecord32", + "scenario": "first-decode-record-32", + "implementation": "effect", + "operation": "first-decode" + }, + { + "name": "first-decode-literal-100-effect", + "export": "effectFirstDecodeLiteral100", + "scenario": "first-decode-literal-100", + "implementation": "effect", + "operation": "first-decode", + "astTags": [ + "Union", + "Literal" + ], + "size": 100 + }, + { + "name": "first-decode-tagged-100-effect", + "export": "effectFirstDecodeTagged100", + "scenario": "first-decode-tagged-100", + "implementation": "effect", + "operation": "first-decode", + "astTags": [ + "Union", + "Objects", + "Literal" + ], + "size": 100 + }, + { + "name": "first-decode-encoding-chain-8-effect", + "export": "effectFirstDecodeEncodingChain8", + "scenario": "first-decode-encoding-chain-8", + "implementation": "effect", + "operation": "first-decode", + "astTags": [ + "String", + "Number" + ], + "size": 8 + } + ] + } + ] + }, + { + "name": "schema-benchmarks", + "fixtures": [ + { + "file": "suites/schema-benchmarks/fixtures/valibot.ts", + "defaults": { + "tier": 3, + "implementation": "valibot", + "family": "schema-benchmarks", + "astTags": [], + "size": "product" + }, + "cases": [ + { + "name": "initialization-schema-valibot", + "export": "initializationSchema", + "scenario": "schema-benchmarks-initialization-schema", + "operation": "schema", + "path": "cold" + }, + { + "name": "validation-valid-valibot", + "export": "validationValid", + "scenario": "schema-benchmarks-validation-valid", + "operation": "is", + "path": "valid" + }, + { + "name": "validation-invalid-valibot", + "export": "validationInvalid", + "scenario": "schema-benchmarks-validation-invalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "parsing-all-valid-valibot", + "export": "parsingAllValid", + "scenario": "schema-benchmarks-parsing-all-valid", + "operation": "safe-parse-all", + "path": "valid" + }, + { + "name": "parsing-all-invalid-valibot", + "export": "parsingAllInvalid", + "scenario": "schema-benchmarks-parsing-all-invalid", + "operation": "safe-parse-all", + "path": "invalid" + }, + { + "name": "parsing-first-valid-valibot", + "export": "parsingFirstValid", + "scenario": "schema-benchmarks-parsing-first-valid", + "operation": "safe-parse-first", + "path": "valid" + }, + { + "name": "parsing-first-invalid-valibot", + "export": "parsingFirstInvalid", + "scenario": "schema-benchmarks-parsing-first-invalid", + "operation": "safe-parse-first", + "path": "invalid" + }, + { + "name": "standard-all-valid-valibot", + "export": "standardAllValid", + "scenario": "schema-benchmarks-standard-all-valid", + "operation": "standard-schema-all", + "path": "valid" + }, + { + "name": "standard-all-invalid-valibot", + "export": "standardAllInvalid", + "scenario": "schema-benchmarks-standard-all-invalid", + "operation": "standard-schema-all", + "path": "invalid" + } + ] + }, + { + "file": "suites/schema-benchmarks/fixtures/zod.ts", + "defaults": { + "tier": 3, + "implementation": "zod4", + "family": "schema-benchmarks", + "astTags": [], + "size": "product" + }, + "cases": [ + { + "name": "initialization-schema-zod4", + "export": "initializationSchema", + "scenario": "schema-benchmarks-initialization-schema", + "operation": "schema", + "path": "cold" + }, + { + "name": "parsing-all-valid-zod4", + "export": "parsingAllValid", + "scenario": "schema-benchmarks-parsing-all-valid", + "operation": "safe-parse-all", + "path": "valid" + }, + { + "name": "parsing-all-invalid-zod4", + "export": "parsingAllInvalid", + "scenario": "schema-benchmarks-parsing-all-invalid", + "operation": "safe-parse-all", + "path": "invalid" + }, + { + "name": "standard-all-valid-zod4", + "export": "standardAllValid", + "scenario": "schema-benchmarks-standard-all-valid", + "operation": "standard-schema-all", + "path": "valid" + }, + { + "name": "standard-all-invalid-zod4", + "export": "standardAllInvalid", + "scenario": "schema-benchmarks-standard-all-invalid", + "operation": "standard-schema-all", + "path": "invalid" + }, + { + "name": "codec-typed-encode-zod4", + "export": "codecTypedEncode", + "scenario": "schema-benchmarks-codec-typed-encode", + "operation": "encode", + "path": "valid", + "size": 1 + }, + { + "name": "codec-typed-decode-zod4", + "export": "codecTypedDecode", + "scenario": "schema-benchmarks-codec-typed-decode", + "operation": "decode", + "path": "valid", + "size": 1 + } + ] + }, + { + "file": "suites/schema-benchmarks/fixtures/effect-beta.ts", + "defaults": { + "tier": 3, + "implementation": "effect", + "family": "schema-benchmarks", + "astTags": [ + "Objects", + "Arrays", + "Literal", + "String", + "Number", + "Null", + "Declaration" + ], + "size": "product" + }, + "cases": [ + { + "name": "initialization-schema", + "export": "initializationSchema", + "scenario": "schema-benchmarks-initialization-schema", + "operation": "schema", + "path": "cold" + }, + { + "name": "initialization-decoder", + "export": "initializationDecoder", + "scenario": "schema-benchmarks-initialization-decoder", + "operation": "schema-and-decoder", + "path": "cold" + }, + { + "name": "validation-valid", + "export": "validationValid", + "scenario": "schema-benchmarks-validation-valid", + "operation": "is", + "path": "valid" + }, + { + "name": "validation-invalid", + "export": "validationInvalid", + "scenario": "schema-benchmarks-validation-invalid", + "operation": "is", + "path": "invalid" + }, + { + "name": "parsing-all-valid", + "export": "parsingAllValid", + "scenario": "schema-benchmarks-parsing-all-valid", + "operation": "decode-option-all", + "path": "valid" + }, + { + "name": "parsing-all-invalid", + "export": "parsingAllInvalid", + "scenario": "schema-benchmarks-parsing-all-invalid", + "operation": "decode-option-all", + "path": "invalid" + }, + { + "name": "parsing-first-valid", + "export": "parsingFirstValid", + "scenario": "schema-benchmarks-parsing-first-valid", + "operation": "decode-option-first", + "path": "valid" + }, + { + "name": "parsing-first-invalid", + "export": "parsingFirstInvalid", + "scenario": "schema-benchmarks-parsing-first-invalid", + "operation": "decode-option-first", + "path": "invalid" + }, + { + "name": "standard-all-valid", + "export": "standardAllValid", + "scenario": "schema-benchmarks-standard-all-valid", + "operation": "standard-schema-all", + "path": "valid" + }, + { + "name": "standard-all-invalid", + "export": "standardAllInvalid", + "scenario": "schema-benchmarks-standard-all-invalid", + "operation": "standard-schema-all", + "path": "invalid" + }, + { + "name": "standard-first-valid", + "export": "standardFirstValid", + "scenario": "schema-benchmarks-standard-first-valid", + "operation": "standard-schema-first", + "path": "valid" + }, + { + "name": "standard-first-invalid", + "export": "standardFirstInvalid", + "scenario": "schema-benchmarks-standard-first-invalid", + "operation": "standard-schema-first", + "path": "invalid" + }, + { + "name": "codec-typed-encode", + "export": "codecTypedEncode", + "scenario": "schema-benchmarks-codec-typed-encode", + "operation": "encode-sync", + "path": "valid", + "astTags": [ + "String", + "BigInt" + ], + "size": 1 + }, + { + "name": "codec-typed-decode", + "export": "codecTypedDecode", + "scenario": "schema-benchmarks-codec-typed-decode", + "operation": "decode-sync", + "path": "valid", + "astTags": [ + "String", + "BigInt" + ], + "size": 1 + }, + { + "name": "codec-unknown-encode", + "export": "codecUnknownEncode", + "scenario": "schema-benchmarks-codec-unknown-encode", + "operation": "encode-unknown-sync", + "path": "valid", + "astTags": [ + "String", + "BigInt" + ], + "size": 1 + }, + { + "name": "codec-unknown-decode", + "export": "codecUnknownDecode", + "scenario": "schema-benchmarks-codec-unknown-decode", + "operation": "decode-unknown-sync", + "path": "valid", + "astTags": [ + "String", + "BigInt" + ], + "size": 1 + } + ] + } + ] + } + ] +} diff --git a/.context/effect/packages/effect/runtimeperf/materialize.mts b/.context/effect/packages/effect/runtimeperf/materialize.mts new file mode 100644 index 000000000..5635ebe8d --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/materialize.mts @@ -0,0 +1,14 @@ +import { cpSync, existsSync, mkdirSync } from "node:fs" +import { basename, dirname, join, relative } from "node:path" +import { runtimeperfDir, sanitize } from "./utils.mts" + +export const materializeFixture = (targetRoot, fixture) => { + const sourceDir = dirname(fixture.fixturePath) + const name = sanitize(relative(runtimeperfDir, sourceDir)) + const targetDir = join(targetRoot, "packages", "effect", ".runtimeperf-compare", name) + mkdirSync(dirname(targetDir), { recursive: true }) + if (!existsSync(targetDir)) { + cpSync(sourceDir, targetDir, { recursive: true }) + } + return join(targetDir, basename(fixture.fixturePath)) +} diff --git a/.context/effect/packages/effect/runtimeperf/run.mts b/.context/effect/packages/effect/runtimeperf/run.mts new file mode 100644 index 000000000..e1f38f163 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/run.mts @@ -0,0 +1,163 @@ +import os from "node:os" +import process from "node:process" +import { analyzePairs } from "./stats.mts" +import { + aggregateMeasurements, + calibrateFixture, + configPath, + coverageSummary, + currentGitState, + formatNs, + hashFile, + libraryVersions, + loadRegistry, + makeRunId, + measureFixture, + parseArgs, + printTable, + relativeToRepo, + reportPath, + resolveDefaults, + selectFixtures, + workerPath, + writeJson +} from "./utils.mts" + +const usage = `Usage: pnpm runtimeperf [suite[/fixture]|scenario] [options] + +Options: + --rounds + --time + --warmup-time + --tier <0-3> + --family + --implementation +` + +const rotate = (items, offset) => items.map((_, index) => items[(index + offset) % items.length]) + +const main = () => { + const options = parseArgs(process.argv.slice(2)) + if (options.help) { + process.stdout.write(usage) + return + } + const { config, fixtures } = loadRegistry() + const selected = selectFixtures(fixtures, options) + const defaults = resolveDefaults(config, options) + const runId = makeRunId() + const groups = Map.groupBy(selected, (fixture) => fixture.scenario) + const results = [] + const executionOrder = [] + + for (const [scenario, group] of groups) { + const calibrations = new Map(group.map((fixture) => [fixture, calibrateFixture(fixture, defaults)])) + const byTarget = new Map(group.map((fixture) => [fixture.target, []])) + + for (let round = 0; round < defaults.rounds; round++) { + for (const fixture of rotate(group, round % group.length)) { + const calibration = calibrations.get(fixture) + const measurement = measureFixture(fixture, defaults, calibration.batchSize) + byTarget.get(fixture.target).push(measurement) + executionOrder.push({ scenario, round: round + 1, target: fixture.target }) + } + } + + for (const fixture of group) { + const calibration = calibrations.get(fixture) + const measurements = byTarget.get(fixture.target) + results.push({ + fixture, + batchSize: calibration.batchSize, + calibration, + measurements, + aggregate: aggregateMeasurements(measurements) + }) + } + } + + const crossLibrary = [] + for (const [scenario, group] of Map.groupBy(results, (result) => result.fixture.scenario)) { + const effect = group.find((result) => result.fixture.implementation === "effect") + if (!effect) continue + for (const candidate of group) { + if (candidate === effect) continue + crossLibrary.push({ + scenario, + implementation: candidate.fixture.implementation, + comparison: analyzePairs( + effect.measurements.map((item) => item.nsPerOp), + candidate.measurements.map((item) => item.nsPerOp), + { + iterations: defaults.bootstrapIterations, + seed: defaults.bootstrapSeed + } + ) + }) + } + } + + const report = { + schemaVersion: 1, + kind: "single", + runId, + target: options.target ?? null, + filters: { + tier: options.tier ?? null, + family: options.family ?? null, + implementation: options.implementation ?? null + }, + config: defaults, + environment: { + node: process.version, + v8: process.versions.v8, + platform: process.platform, + arch: process.arch, + cpu: os.cpus()[0]?.model ?? "unknown" + }, + libraries: libraryVersions(), + crossLibraryDecodeApis: { + effect: "SchemaParser.decodeUnknownExit (SchemaIssue)", + valibot: "safeParser", + zod4: "safeParse ({ jitless: true })" + }, + artifactMode: "repository", + git: currentGitState(), + coverage: coverageSummary(selected), + hashes: { + config: hashFile(configPath), + worker: hashFile(workerPath), + fixtures: Object.fromEntries( + [...new Set(selected.map((fixture) => fixture.fixturePath))] + .map((path) => [relativeToRepo(path), hashFile(path)]) + ) + }, + executionOrder, + results, + crossLibrary + } + const path = reportPath(runId, options.target, "single") + writeJson(path, report) + const comparisons = new Map(crossLibrary.map((item) => [`${item.scenario}/${item.implementation}`, item])) + printTable( + ["scenario", "implementation", "ns/op", "mad", "vs Effect"], + results.map((result) => { + const comparison = comparisons.get(`${result.fixture.scenario}/${result.fixture.implementation}`) + return [ + result.fixture.scenario, + result.fixture.implementation, + formatNs(result.aggregate.median), + formatNs(result.aggregate.mad), + comparison ? `${comparison.comparison.ratio.toFixed(3)}x` : "-" + ] + }) + ) + process.stdout.write(`\nReport: ${relativeToRepo(path)}\n`) +} + +try { + main() +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : error}\n`) + process.exitCode = 1 +} diff --git a/.context/effect/packages/effect/runtimeperf/stats.mts b/.context/effect/packages/effect/runtimeperf/stats.mts new file mode 100644 index 000000000..4f4be2068 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/stats.mts @@ -0,0 +1,131 @@ +const assertFiniteNumbers = (values, label) => { + if (!Array.isArray(values) || values.length === 0) { + throw new Error(`${label} must be a non-empty array`) + } + for (const value of values) { + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`${label} must contain finite positive numbers`) + } + } +} + +export const median = (values) => { + if (!Array.isArray(values) || values.length === 0) { + throw new Error("values must be a non-empty array") + } + const sorted = values.slice().sort((a, b) => a - b) + const middle = Math.floor(sorted.length / 2) + return sorted.length % 2 === 0 + ? (sorted[middle - 1] + sorted[middle]) / 2 + : sorted[middle] +} + +export const percentile = (values, probability) => { + if (!Array.isArray(values) || values.length === 0) { + throw new Error("values must be a non-empty array") + } + if (!Number.isFinite(probability) || probability < 0 || probability > 1) { + throw new Error("probability must be between 0 and 1") + } + const sorted = values.slice().sort((a, b) => a - b) + const index = (sorted.length - 1) * probability + const lower = Math.floor(index) + const upper = Math.ceil(index) + if (lower === upper) return sorted[lower] + const weight = index - lower + return sorted[lower] * (1 - weight) + sorted[upper] * weight +} + +const makeRandom = (seed) => { + let state = seed >>> 0 + return () => { + state += 0x6d2b79f5 + let value = state + value = Math.imul(value ^ value >>> 15, value | 1) + value ^= value + Math.imul(value ^ value >>> 7, value | 61) + return ((value ^ value >>> 14) >>> 0) / 4294967296 + } +} + +export const aggregate = (values) => { + assertFiniteNumbers(values, "values") + const center = median(values) + return { + median: center, + min: Math.min(...values), + max: Math.max(...values), + mad: median(values.map((value) => Math.abs(value - center))) + } +} + +export const bootstrapMedianLogRatio = ( + ratios, + { confidence = 0.95, iterations = 10_000, seed = 0x5eed1234 } = {} +) => { + assertFiniteNumbers(ratios, "ratios") + if (!Number.isInteger(iterations) || iterations <= 0) { + throw new Error("iterations must be a positive integer") + } + if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) { + throw new Error("confidence must be between 0 and 1") + } + if (!Number.isInteger(seed)) { + throw new Error("seed must be an integer") + } + + const logRatios = ratios.map(Math.log) + const random = makeRandom(seed) + const samples = new Array(iterations) + const resample = new Array(logRatios.length) + for (let iteration = 0; iteration < iterations; iteration++) { + for (let index = 0; index < logRatios.length; index++) { + resample[index] = logRatios[Math.floor(random() * logRatios.length)] + } + samples[iteration] = median(resample) + } + const tail = (1 - confidence) / 2 + return { + ratio: Math.exp(median(logRatios)), + lowRatio: Math.exp(percentile(samples, tail)), + highRatio: Math.exp(percentile(samples, 1 - tail)), + confidence, + iterations, + seed + } +} + +export const analyzePairs = ( + base, + head, + { + confidence = 0.95, + iterations = 10_000, + seed = 0x5eed1234, + minImprovementPercent = 2, + maxRegressionPercent = 5 + } = {} +) => { + assertFiniteNumbers(base, "base") + assertFiniteNumbers(head, "head") + if (base.length !== head.length) { + throw new Error("base and head must contain the same number of observations") + } + const ratios = base.map((value, index) => head[index] / value) + const interval = bootstrapMedianLogRatio(ratios, { confidence, iterations, seed }) + const deltaPercent = (interval.ratio - 1) * 100 + const lowPercent = (interval.lowRatio - 1) * 100 + const highPercent = (interval.highRatio - 1) * 100 + const status = interval.highRatio < 1 - minImprovementPercent / 100 + ? "improvement" + : interval.lowRatio > 1 + maxRegressionPercent / 100 + ? "regression" + : "inconclusive" + return { + ratios, + deltaPercent, + lowPercent, + highPercent, + status, + ...interval + } +} diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts new file mode 100644 index 000000000..af9d40583 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/data.ts @@ -0,0 +1,159 @@ +// Schema Benchmarks requires real Date instances in the shared input. +const date = new Date(0) + +export const validData = { + id: 252, + created: date, + title: "Apple", + brand: "Sunny Backyard", + description: "Red apple from Lake Constance", + price: 89, + discount: null, + quantity: 5, + tags: ["fruit", "red", "round", "sweet", "juicy", "healthy"], + images: [ + { + id: 248, + created: date, + title: "Close up of an apple on a tree", + type: "jpg", + size: 92357232, + url: "https://www.example.com/images/248" + }, + { + id: 295, + created: date, + title: "Our apples in the final packaging", + type: "jpg", + size: 83247232, + url: "https://www.example.com/images/295" + }, + { + id: 723, + created: date, + title: "Our fruit fields at Lake Constance", + type: "jpg", + size: 72356345, + url: "https://www.example.com/images/723" + } + ], + ratings: [ + { + id: 315, + stars: 4.5, + title: "Tastes super delicious", + text: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor.", + images: [ + { + id: 835, + created: date, + title: "The result of our apple pie", + type: "jpg", + size: 8247493, + url: "https://www.example.com/images/835" + } + ] + }, + { + id: 642, + stars: 5, + title: "Very tasty! I will buy them again!", + text: + "In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt.", + images: [ + { + id: 352, + created: date, + title: "The fruit salad in a bowl", + type: "jpg", + size: 3582543, + url: "https://www.example.com/images/352" + }, + { + id: 465, + created: date, + title: "The fruit salad on a plate", + type: "jpg", + size: 9824742, + url: "https://www.example.com/images/465" + } + ] + } + ] +} + +export const invalidData = { + id: 252, + created: date, + title: "", + brand: "Sunny Backyard", + description: "Red apple from Lake Constance", + price: 0, + discount: null, + quantity: 1000, + tags: ["fruit", null, "round", undefined, "juicy", "healthy"], + images: [ + { + created: null, + title: "Close up of an apple on a tree", + type: "mp4", + size: 92357232, + url: "https://www.example.com/images/248" + }, + { + id: 295, + created: date, + title: "Our apples in the final packaging", + type: "jpg", + size: 83247232 + }, + { + id: 723, + created: date, + title: "Our fruit fields at Lake Constance", + type: "jpg", + size: 72356345, + url: "https://www.example.com/images/723" + } + ], + ratings: [ + { + id: 315, + stars: 4.5, + title: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor.", + text: "Tastes super delicious", + images: [ + { + id: 835, + created: date, + title: "The result of our apple pie", + type: "jpg", + size: 8247493, + url: "https://www.example.com/images/835" + } + ] + }, + { + id: 642, + stars: 5, + title: "Very tasty! I will buy them again!", + text: + "In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt.", + images: [ + { + id: "352", + created: undefined, + title: "The fruit salad in a bowl", + type: "jpg", + size: 3582543, + url: "INVALID_URL" + }, + { + id: 465, + created: date, + url: "https://www.example.com/images/465" + } + ] + } + ] +} diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts new file mode 100644 index 000000000..b21c0dd2d --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/effect-beta.ts @@ -0,0 +1,133 @@ +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import assert from "node:assert/strict" +import { invalidData, validData } from "./data.ts" + +// Extracted from open-circle/schema-benchmarks at +// 11fab2a741cef95a1374910276023c51218c0683. + +const makeSchema = () => { + const Image = Schema.Struct({ + id: Schema.Number, + created: Schema.instanceOf(Date), + title: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100)), + type: Schema.Literals(["jpg", "png"]), + size: Schema.Number, + url: Schema.String.check(Schema.makeFilter((value) => URL.canParse(value))) + }) + const Rating = Schema.Struct({ + id: Schema.Number, + stars: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(5)), + title: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100)), + text: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(1000)), + images: Schema.mutable(Schema.Array(Image)) + }) + return Schema.Struct({ + id: Schema.Number, + created: Schema.instanceOf(Date), + title: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(100)), + brand: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(30)), + description: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)), + price: Schema.Number.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(10000)), + discount: Schema.NullOr( + Schema.Number.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(100)) + ), + quantity: Schema.Number.check(Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(10)), + tags: Schema.mutable( + Schema.Array(Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(30))) + ), + images: Schema.mutable(Schema.Array(Image)), + ratings: Schema.mutable(Schema.Array(Rating)) + }) +} + +export const initializationSchema = () => ({ + run: makeSchema, + validate: (schema) => assert.equal(schema.ast._tag, "Objects") +}) + +export const initializationDecoder = () => ({ + run: () => Schema.decodeUnknownOption(makeSchema()), + validate: (decode) => assert.equal(typeof decode, "function") +}) + +const validationCase = (input, expected) => () => { + const run = Schema.is(makeSchema()) + return { + run: () => run(input), + validate: (result) => assert.equal(result, expected) + } +} + +export const validationValid = validationCase(validData, true) +export const validationInvalid = validationCase(invalidData, false) + +const parsingCase = (input, errors, success) => () => { + const run = Schema.decodeUnknownOption(makeSchema()) + return { + run: () => run(input, { errors }), + validate: (result) => assert.equal(Option.isSome(result), success) + } +} + +export const parsingAllValid = parsingCase(validData, "all", true) +export const parsingAllInvalid = parsingCase(invalidData, "all", false) +export const parsingFirstValid = parsingCase(validData, "first", true) +export const parsingFirstInvalid = parsingCase(invalidData, "first", false) + +const standardCase = (input, errors, success) => () => { + const schema = Schema.toStandardSchemaV1(makeSchema(), { parseOptions: { errors } }) + return { + run: () => schema["~standard"].validate(input), + validate: (result) => { + assert.equal(typeof result?.then, "undefined") + if (success) { + assert.equal(result.issues, undefined) + assert.deepEqual(result.value, validData) + } else { + assert.ok(result.issues) + assert.ok(result.issues.length > 0) + } + } + } +} + +export const standardAllValid = standardCase(validData, "all", true) +export const standardAllInvalid = standardCase(invalidData, "all", false) +export const standardFirstValid = standardCase(validData, "first", true) +export const standardFirstInvalid = standardCase(invalidData, "first", false) + +const bigint = BigInt("1234567890123456789") +const bigintString = bigint.toString() + +export const codecTypedEncode = () => { + const run = Schema.encodeSync(Schema.BigIntFromString) + return { + run: () => run(bigint), + validate: (result) => assert.equal(result, bigintString) + } +} + +export const codecTypedDecode = () => { + const run = Schema.decodeSync(Schema.BigIntFromString) + return { + run: () => run(bigintString), + validate: (result) => assert.equal(result, bigint) + } +} + +export const codecUnknownEncode = () => { + const run = Schema.encodeUnknownSync(Schema.BigIntFromString) + return { + run: () => run(bigint), + validate: (result) => assert.equal(result, bigintString) + } +} + +export const codecUnknownDecode = () => { + const run = Schema.decodeUnknownSync(Schema.BigIntFromString) + return { + run: () => run(bigintString), + validate: (result) => assert.equal(result, bigint) + } +} diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts new file mode 100644 index 000000000..fdff6a1b5 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/valibot.ts @@ -0,0 +1,80 @@ +import assert from "node:assert/strict" +import * as v from "valibot" +import { invalidData, validData } from "./data.ts" + +// Extracted from open-circle/schema-benchmarks at +// 11fab2a741cef95a1374910276023c51218c0683. + +const makeSchema = () => { + const image = v.object({ + id: v.number(), + created: v.date(), + title: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), + type: v.picklist(["jpg", "png"]), + size: v.number(), + url: v.pipe(v.string(), v.url()) + }) + const rating = v.object({ + id: v.number(), + stars: v.pipe(v.number(), v.minValue(1), v.maxValue(5)), + title: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), + text: v.pipe(v.string(), v.minLength(1), v.maxLength(1000)), + images: v.array(image) + }) + return v.object({ + id: v.number(), + created: v.date(), + title: v.pipe(v.string(), v.minLength(1), v.maxLength(100)), + brand: v.pipe(v.string(), v.minLength(1), v.maxLength(30)), + description: v.pipe(v.string(), v.minLength(1), v.maxLength(500)), + price: v.pipe(v.number(), v.minValue(1), v.maxValue(10000)), + discount: v.nullable(v.pipe(v.number(), v.minValue(1), v.maxValue(100))), + quantity: v.pipe(v.number(), v.minValue(1), v.maxValue(10)), + tags: v.array(v.pipe(v.string(), v.minLength(1), v.maxLength(30))), + images: v.array(image), + ratings: v.array(rating) + }) +} + +export const initializationSchema = () => ({ + run: makeSchema, + validate: (schema) => assert.equal(schema.type, "object") +}) + +const validationCase = (input, expected) => () => { + const schema = makeSchema() + return { + run: () => v.is(schema, input), + validate: (result) => assert.equal(result, expected) + } +} + +export const validationValid = validationCase(validData, true) +export const validationInvalid = validationCase(invalidData, false) + +const parsingCase = (input, options, success) => () => { + const schema = makeSchema() + return { + run: () => v.safeParse(schema, input, options), + validate: (result) => assert.equal(result.success, success) + } +} + +export const parsingAllValid = parsingCase(validData, undefined, true) +export const parsingAllInvalid = parsingCase(invalidData, undefined, false) +export const parsingFirstValid = parsingCase(validData, { abortEarly: true }, true) +export const parsingFirstInvalid = parsingCase(invalidData, { abortEarly: true }, false) + +const standardCase = (input, success) => () => { + const schema = makeSchema() + return { + run: () => schema["~standard"].validate(input), + validate: (result) => { + assert.equal(typeof result?.then, "undefined") + assert.equal(result.issues === undefined, success) + } + } +} + +export const standardAllValid = standardCase(validData, true) +export const standardAllInvalid = standardCase(invalidData, false) diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts new file mode 100644 index 000000000..81e56f482 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema-benchmarks/fixtures/zod.ts @@ -0,0 +1,85 @@ +import assert from "node:assert/strict" +import { z } from "zod/v4" +import { invalidData, validData } from "./data.ts" + +// Extracted from open-circle/schema-benchmarks at +// 11fab2a741cef95a1374910276023c51218c0683. + +const makeSchema = () => { + const image = z.object({ + id: z.number(), + created: z.date(), + title: z.string().min(1).max(100), + type: z.enum(["jpg", "png"]), + size: z.number(), + url: z.url() + }) + const rating = z.object({ + id: z.number(), + stars: z.number().min(0).max(5), + title: z.string().min(1).max(100), + text: z.string().min(1).max(1000), + images: z.array(image) + }) + return z.object({ + id: z.number(), + created: z.date(), + title: z.string().min(1).max(100), + brand: z.string().min(1).max(30), + description: z.string().min(1).max(500), + price: z.number().min(1).max(10000), + discount: z.number().min(1).max(100).nullable(), + quantity: z.number().min(0).max(10), + tags: z.array(z.string().min(1).max(30)), + images: z.array(image), + ratings: z.array(rating) + }) +} + +export const initializationSchema = () => ({ + run: makeSchema, + validate: (schema) => assert.equal(schema.type, "object") +}) + +const parsingCase = (input, success) => () => { + const schema = makeSchema() + const options = { jitless: true } + return { + run: () => schema.safeParse(input, options), + validate: (result) => assert.equal(result.success, success) + } +} + +export const parsingAllValid = parsingCase(validData, true) +export const parsingAllInvalid = parsingCase(invalidData, false) + +const standardCase = (input, success) => () => { + const schema = makeSchema() + return { + run: () => schema["~standard"].validate(input), + validate: (result) => { + assert.equal(typeof result?.then, "undefined") + assert.equal(result.issues === undefined, success) + } + } +} + +export const standardAllValid = standardCase(validData, true) +export const standardAllInvalid = standardCase(invalidData, false) + +const codec = z.codec(z.string(), z.bigint(), { + decode: (value) => BigInt(value), + encode: (value) => value.toString() +}) +const bigint = BigInt("1234567890123456789") +const bigintString = bigint.toString() + +export const codecTypedEncode = () => ({ + run: () => codec.encode(bigint), + validate: (result) => assert.equal(result, bigintString) +}) + +export const codecTypedDecode = () => ({ + run: () => codec.decode(bigintString), + validate: (result) => assert.equal(result, bigint) +}) diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts new file mode 100644 index 000000000..2a84cab07 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/adapters.ts @@ -0,0 +1,83 @@ +import * as Option from "effect/Option" +import * as Result from "effect/Result" +import * as Schema from "effect/Schema" +import * as SchemaParser from "effect/SchemaParser" +import assert from "node:assert/strict" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.Number +}) +const input = { a: "a", b: 1 } +const invalidInput = { a: "a", b: "invalid" } + +export const parserExitInvalid = () => { + const run = SchemaParser.decodeUnknownExit(schema) + return { + run: () => run(invalidInput), + validate: (result) => assert.equal(result._tag, "Failure") + } +} + +export const exitValid = () => { + const run = Schema.decodeUnknownExit(schema) + return { + run: () => run(input), + validate: (result) => assert.equal(result._tag, "Success") + } +} + +export const exitInvalid = () => { + const run = Schema.decodeUnknownExit(schema) + return { + run: () => run(invalidInput), + validate: (result) => assert.equal(result._tag, "Failure") + } +} + +export const optionValid = () => { + const run = Schema.decodeUnknownOption(schema) + return { + run: () => run(input), + validate: (result) => assert.equal(Option.isSome(result), true) + } +} + +export const optionInvalid = () => { + const run = Schema.decodeUnknownOption(schema) + return { + run: () => run(invalidInput), + validate: (result) => assert.equal(Option.isNone(result), true) + } +} + +export const resultValid = () => { + const run = Schema.decodeUnknownResult(schema) + return { + run: () => run(input), + validate: (result) => assert.equal(Result.isSuccess(result), true) + } +} + +export const resultInvalid = () => { + const run = Schema.decodeUnknownResult(schema) + return { + run: () => run(invalidInput), + validate: (result) => assert.equal(Result.isFailure(result), true) + } +} + +export const syncInvalid = () => { + const run = Schema.decodeUnknownSync(schema) + return { + run: () => { + try { + run(invalidInput) + } catch (error) { + return error + } + return undefined + }, + validate: (result) => assert.equal(result instanceof Error, true) + } +} diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts new file mode 100644 index 000000000..8ed02deaf --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/behavior.ts @@ -0,0 +1,215 @@ +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as SchemaGetter from "effect/SchemaGetter" +import * as SchemaParser from "effect/SchemaParser" +import * as SchemaTransformation from "effect/SchemaTransformation" +import assert from "node:assert/strict" + +const decodeCase = (schema, input, success, options) => () => { + const run = Schema.decodeUnknownExit(schema, options) + return { + run: () => run(input), + validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + } +} + +const decodeParserCase = (schema, input, success, options) => () => { + const run = SchemaParser.decodeUnknownExit(schema, options) + return { + run: () => run(input), + validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + } +} + +const encodeParserCase = (schema, input, success, options) => () => { + const run = SchemaParser.encodeUnknownExit(schema, options) + return { + run: () => run(input), + validate: (result) => assert.equal(result._tag, success ? "Success" : "Failure") + } +} + +const checkedString = Schema.String + .check(Schema.isMinLength(2)) + .check(Schema.isPattern(/^[a-z]+$/)) + +export const checksInvalidFirst = decodeParserCase(checkedString, "", false) +export const checksInvalidLast = decodeParserCase(checkedString, "runtime-perf", false) + +const encodingCheckedString = Schema.String.pipe( + Schema.flip, + Schema.check(Schema.isMinLength(2)), + Schema.flip +) + +export const encodingCheckValid = decodeParserCase(encodingCheckedString, "runtimeperf", true) + +const templateLiteralLinear = Schema.TemplateLiteralParser([ + "prefix-", + Schema.String, + "-middle-", + Schema.Number, + "-suffix" +]) + +export const templateLiteralLinearValid = decodeParserCase( + templateLiteralLinear, + "prefix-value-middle-123-suffix", + true +) +export const templateLiteralLinearInvalid = decodeParserCase( + templateLiteralLinear, + "prefix-value-middle-invalid", + false +) + +const templateLiteralBacktracking = Schema.TemplateLiteralParser([ + Schema.String, + ":", + Schema.NonEmptyString, + "x" +]) + +export const templateLiteralBacktrackingValid = decodeParserCase( + templateLiteralBacktracking, + "a:b:x", + true +) +export const templateLiteralBacktrackingInvalid = decodeParserCase( + templateLiteralBacktracking, + "a:x", + false +) + +export const templateLiteralTransformedValid = decodeParserCase( + Schema.TemplateLiteralParser([Schema.FiniteFromString, "a", Schema.NonEmptyString]), + "100ab23a", + true +) + +const templateLiteralRecordInput = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field-${index}`, `value${index}`]) +) + +export const templateLiteralRecord32Valid = decodeParserCase( + Schema.Record(Schema.TemplateLiteral(["field-", Schema.Number]), Schema.String), + templateLiteralRecordInput, + true +) + +export const transformationDecodeValid = decodeParserCase(Schema.FiniteFromString, "123", true) +export const transformationDecodeInvalid = decodeParserCase(Schema.FiniteFromString, "invalid", false) +export const transformationEncodeValid = encodeParserCase(Schema.FiniteFromString, 123, true) + +const makeEncodingChain = (size) => { + let schema = Schema.FiniteFromString + for (let i = 1; i < size; i++) { + schema = Schema.String.pipe( + Schema.decodeTo(schema, SchemaTransformation.passthrough()) + ) + } + return schema +} + +const encodingChain8 = makeEncodingChain(8) + +export const encodingChain8DecodeValid = decodeParserCase(encodingChain8, "123", true) +export const encodingChain8DecodeInvalid = decodeParserCase(encodingChain8, "invalid", false) +export const encodingChain8EncodeValid = encodeParserCase(encodingChain8, 123, true) + +const transformedKeyRecord = Schema.Record( + Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())), + Schema.String +) +const transformedKeyRecordInput = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field_${index}_value`, `value${index}`]) +) + +export const transformedKeyRecordValid = decodeParserCase( + transformedKeyRecord, + transformedKeyRecordInput, + true +) + +const optionalStruct = Schema.Struct({ + required: Schema.String, + optionalKey: Schema.optionalKey(Schema.String), + optionalValue: Schema.optional(Schema.String) +}) + +export const optionalValid = decodeCase(optionalStruct, { required: "value" }, true) +export const optionalPresentValid = decodeCase( + optionalStruct, + { required: "value", optionalKey: "key", optionalValue: "value" }, + true +) +export const optionalPresentInvalid = decodeCase(optionalStruct, { required: "value", optionalKey: 1 }, false) + +const suspendedString = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter((input) => Effect.suspend(() => Effect.succeed(input))), + encode: SchemaGetter.passthrough() +})) +const suspendedObjectFields = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field${index}`, index === 16 ? suspendedString : Schema.String]) +) +const suspendedObjectInput = Object.fromEntries( + Array.from({ length: 32 }, (_, index) => [`field${index}`, `value${index}`]) +) + +export const object32SuspendedMiddleValid = decodeParserCase( + Schema.Struct(suspendedObjectFields), + suspendedObjectInput, + true +) + +const literal2 = Schema.Literals(["value0", "value1"]) +const literal100 = Schema.Literals(Array.from({ length: 100 }, (_, index) => `value${index}`)) +const homogeneousUnion100 = Schema.Union( + Array.from( + { length: 100 }, + (_, index) => Schema.String.check(Schema.makeFilter((value) => value === `value${index}`)) + ) +) +const tagged2 = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }), + Schema.Struct({ kind: Schema.Literal("b"), value: Schema.String }) +]) +const taggedWithFallback = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }), + Schema.Struct({ value: Schema.String }) +]) + +export const literal2ValidLast = decodeParserCase(literal2, "value1", true) +export const literal100ValidFirst = decodeParserCase(literal100, "value0", true) +export const homogeneousUnion100Invalid = decodeParserCase(homogeneousUnion100, "missing", false) +export const tagged2ValidLast = decodeParserCase(tagged2, { kind: "b", value: "value" }, true) +export const taggedWithFallbackValid = decodeParserCase( + taggedWithFallback, + { kind: "a", value: "value" }, + true +) + +const propertyOrderSchema = Schema.Struct({ + a: Schema.String, + b: Schema.String +}) +const propertyOrderInput = { extra: "extra", b: "b", a: "a" } + +export const propertyOrderOriginal = decodeCase( + propertyOrderSchema, + propertyOrderInput, + true, + { onExcessProperty: "preserve", propertyOrder: "original" } +) + +const recursiveTree = Schema.Struct({ + value: Schema.String, + children: Schema.Array(Schema.suspend(() => recursiveTree)) +}) + +const makeTree = (depth) => + depth === 0 + ? { value: "leaf", children: [] } + : { value: `node${depth}`, children: [makeTree(depth - 1)] } + +export const recursiveTreeDepth16Valid = decodeCase(recursiveTree, makeTree(16), true) diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts new file mode 100644 index 000000000..2f62d5ac0 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/cold.ts @@ -0,0 +1,87 @@ +import * as Schema from "effect/Schema" +import * as SchemaParser from "effect/SchemaParser" +import * as SchemaTransformation from "effect/SchemaTransformation" +import assert from "node:assert/strict" + +const size = 32 +const input = Object.fromEntries(Array.from({ length: size }, (_, index) => [`field${index}`, `value${index}`])) + +const makeEffectCheckedSchema = () => + Schema.Struct( + Object.fromEntries(Array.from({ length: size }, (_, index) => [`field${index}`, Schema.NonEmptyString])) + ) + +const makeEffectTemplateLiteralSchema = () => + Schema.TemplateLiteral(["prefix-", Schema.String, "-middle-", Schema.Number, "-suffix"]) + +const makeEffectRecordSchema = () => Schema.Record(Schema.String, Schema.String) + +const literalValues100 = Array.from({ length: 100 }, (_, index) => `value${index}`) + +const makeEffectLiteral100Schema = () => Schema.Literals(literalValues100) + +const makeEffectTaggedMember = (index) => + Schema.Struct({ + kind: Schema.Literal(`kind${index}`), + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean + }) + +const makeEffectTagged100Schema = () => + Schema.Union(Array.from({ length: 100 }, (_, index) => makeEffectTaggedMember(index))) + +const taggedInput = { + kind: "kind99", + a: "a", + b: 1, + c: true +} + +const makeEffectEncodingChain = (size) => { + let schema = Schema.FiniteFromString + for (let i = 1; i < size; i++) { + schema = Schema.String.pipe( + Schema.decodeTo(schema, SchemaTransformation.passthrough()) + ) + } + return schema +} + +export const effectSchemaCreationTemplateLiteral = () => ({ + run: makeEffectTemplateLiteralSchema, + validate: (schema) => assert.equal(schema.ast._tag, "TemplateLiteral") +}) + +export const effectFirstDecodeCheckedObject32 = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectCheckedSchema())(input), + validate: (result) => assert.equal(result._tag, "Success") +}) + +export const effectFirstDecodeTemplateLiteral = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectTemplateLiteralSchema())("prefix-value-middle-123-suffix"), + validate: (result) => assert.equal(result._tag, "Success") +}) + +export const effectFirstDecodeRecord32 = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectRecordSchema())(input), + validate: (result) => assert.equal(result._tag, "Success") +}) + +export const effectFirstDecodeLiteral100 = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectLiteral100Schema())("value99"), + validate: (result) => assert.equal(result._tag, "Success") +}) + +export const effectFirstDecodeTagged100 = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectTagged100Schema())(taggedInput), + validate: (result) => assert.equal(result._tag, "Success") +}) + +export const effectFirstDecodeEncodingChain8 = () => ({ + run: () => SchemaParser.decodeUnknownExit(makeEffectEncodingChain(8))("123"), + validate: (result) => { + assert.equal(result._tag, "Success") + assert.equal(result.value, 123) + } +}) diff --git a/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts new file mode 100644 index 000000000..252635985 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/suites/schema/fixtures/comparison.ts @@ -0,0 +1,91 @@ +import * as Schema from "effect/Schema" +import * as SchemaParser from "effect/SchemaParser" +import assert from "node:assert/strict" + +const effectCase = (schema, input, success) => () => { + const run = SchemaParser.decodeUnknownExit(schema) + return { + run: () => run(input), + validate: (result) => { + assert.equal(result._tag, success ? "Success" : "Failure") + if (success) assert.deepEqual(result.value, input) + } + } +} + +const makeObjectInput = (size) => + Object.fromEntries(Array.from({ length: size }, (_, index) => [`field${index}`, `value${index}`])) + +const makeInvalidLastObjectInput = (size) => ({ + ...makeObjectInput(size), + [`field${size - 1}`]: 1 +}) + +const object32 = makeObjectInput(32) +const object32InvalidLast = makeInvalidLastObjectInput(32) +const effectObject32 = Schema.Struct( + Object.fromEntries(Array.from({ length: 32 }, (_, index) => [`field${index}`, Schema.String])) +) + +export const effectObject32Valid = effectCase(effectObject32, object32, true) +export const effectObject32InvalidLast = effectCase(effectObject32, object32InvalidLast, false) + +const array32 = Array.from({ length: 32 }, (_, index) => `value${index}`) +const array32InvalidLast = array32.slice() +array32InvalidLast[array32InvalidLast.length - 1] = 1 +const effectArray32 = Schema.Array(Schema.String) + +export const effectArray32Valid = effectCase(effectArray32, array32, true) +export const effectArray32InvalidLast = effectCase(effectArray32, array32InvalidLast, false) + +const record32 = makeObjectInput(32) +const record32InvalidLast = makeInvalidLastObjectInput(32) +const effectRecord32 = Schema.Record(Schema.String, Schema.String) + +export const effectRecord32Valid = effectCase(effectRecord32, record32, true) +export const effectRecord32InvalidLast = effectCase(effectRecord32, record32InvalidLast, false) + +const literalValues = Array.from({ length: 100 }, (_, index) => `value${index}`) +const effectLiteral100 = Schema.Literals(literalValues) + +export const effectLiteral100ValidLast = effectCase(effectLiteral100, "value99", true) +export const effectLiteral100Invalid = effectCase(effectLiteral100, "missing", false) + +const makeEffectTaggedMember = (index) => + Schema.Struct({ + kind: Schema.Literal(`kind${index}`), + a: Schema.String, + b: Schema.Number, + c: Schema.Boolean + }) + +const taggedInput = { kind: "kind99", a: "a", b: 1, c: true } +const taggedInvalidSelected = { kind: "kind99", a: "a", b: 1, c: "invalid" } +const taggedInvalidTag = { kind: "missing", a: "a", b: 1, c: true } +const effectTagged100 = Schema.Union( + Array.from({ length: 100 }, (_, index) => makeEffectTaggedMember(index)) +) + +export const effectTagged100ValidLast = effectCase(effectTagged100, taggedInput, true) +export const effectTagged100InvalidSelected = effectCase(effectTagged100, taggedInvalidSelected, false) +export const effectTagged100InvalidTag = effectCase(effectTagged100, taggedInvalidTag, false) + +const makeEffectMultiSentinelMember = (index) => + Schema.Struct({ + kind: Schema.Literal("shared"), + variant: Schema.Literal(`variant${index}`), + value: Schema.String + }) + +const effectMultiSentinel100 = Schema.Union( + Array.from({ length: 100 }, (_, index) => makeEffectMultiSentinelMember(index)) +) +const multiSentinelValidFirst = { kind: "shared", variant: "variant0", value: "value" } +const multiSentinelInvalidVariant = { kind: "shared", variant: "missing", value: "value" } + +export const effectMultiSentinel100ValidFirst = effectCase(effectMultiSentinel100, multiSentinelValidFirst, true) +export const effectMultiSentinel100InvalidVariant = effectCase( + effectMultiSentinel100, + multiSentinelInvalidVariant, + false +) diff --git a/.context/effect/packages/effect/runtimeperf/test/materialize.test.mts b/.context/effect/packages/effect/runtimeperf/test/materialize.test.mts new file mode 100644 index 000000000..e4331c510 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/test/materialize.test.mts @@ -0,0 +1,24 @@ +import assert from "node:assert/strict" +import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { dirname, join } from "node:path" +import { describe, it } from "node:test" +import { materializeFixture } from "../materialize.mts" + +describe("runtimeperf fixture materialization", () => { + it("preserves relative fixture dependencies", () => { + const root = mkdtempSync(join(tmpdir(), "effect-runtimeperf-materialize-")) + try { + const fixturePath = join(root, "fixtures", "fixture.mts") + mkdirSync(dirname(fixturePath), { recursive: true }) + writeFileSync(join(dirname(fixturePath), "data.mts"), "export const value = 1\n") + writeFileSync(fixturePath, 'import { value } from "./data.mts"\nexport { value }\n') + + const materialized = materializeFixture(join(root, "target"), { fixturePath }) + + assert.equal(existsSync(join(dirname(materialized), "data.mts")), true) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/.context/effect/packages/effect/runtimeperf/test/registry.test.mts b/.context/effect/packages/effect/runtimeperf/test/registry.test.mts new file mode 100644 index 000000000..e43831bc8 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/test/registry.test.mts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict" +import { readFile } from "node:fs/promises" +import { describe, it } from "node:test" +import { pathToFileURL } from "node:url" +import { loadRegistry } from "../utils.mts" + +describe("runtimeperf registry", () => { + it("uses unique fixture targets and valid implementations", () => { + const { fixtures } = loadRegistry() + assert.equal(new Set(fixtures.map((fixture) => fixture.target)).size, fixtures.length) + for (const fixture of fixtures) { + assert.ok(["effect", "valibot", "zod4"].includes(fixture.implementation)) + } + }) + + it("keeps the focused Schema diagnostics Effect-only", () => { + const { fixtures } = loadRegistry() + const diagnostics = fixtures.filter((fixture) => fixture.suite === "schema") + assert.ok(diagnostics.length > 0) + assert.equal(diagnostics.every((fixture) => fixture.implementation === "effect"), true) + }) + + it("includes the complete effect@beta Schema Benchmarks matrix", () => { + const { fixtures } = loadRegistry() + assert.deepEqual( + fixtures + .filter((fixture) => fixture.suite === "schema-benchmarks" && fixture.implementation === "effect") + .map((fixture) => fixture.name) + .sort(), + [ + "codec-typed-decode", + "codec-typed-encode", + "codec-unknown-decode", + "codec-unknown-encode", + "initialization-decoder", + "initialization-schema", + "parsing-all-invalid", + "parsing-all-valid", + "parsing-first-invalid", + "parsing-first-valid", + "standard-all-invalid", + "standard-all-valid", + "standard-first-invalid", + "standard-first-valid", + "validation-invalid", + "validation-valid" + ] + ) + }) + + it("includes the complete Valibot and Zod Schema Benchmarks matrices", () => { + const { fixtures } = loadRegistry() + const names = (implementation) => + fixtures + .filter((fixture) => fixture.suite === "schema-benchmarks" && fixture.implementation === implementation) + .map((fixture) => fixture.name) + .sort() + assert.deepEqual(names("valibot"), [ + "initialization-schema-valibot", + "parsing-all-invalid-valibot", + "parsing-all-valid-valibot", + "parsing-first-invalid-valibot", + "parsing-first-valid-valibot", + "standard-all-invalid-valibot", + "standard-all-valid-valibot", + "validation-invalid-valibot", + "validation-valid-valibot" + ]) + assert.deepEqual(names("zod4"), [ + "codec-typed-decode-zod4", + "codec-typed-encode-zod4", + "initialization-schema-zod4", + "parsing-all-invalid-zod4", + "parsing-all-valid-zod4", + "standard-all-invalid-zod4", + "standard-all-valid-zod4" + ]) + }) + + it("uses Zod 4 standard and jitless safeParse for the zod4 fixtures", async () => { + const { fixtures } = loadRegistry() + const zodFiles = new Set( + fixtures + .filter((fixture) => fixture.implementation === "zod4") + .map((fixture) => fixture.fixturePath) + ) + assert.ok(zodFiles.size > 0) + for (const path of zodFiles) { + const source = await readFile(path, "utf8") + assert.match(source, /from "zod\/v4"/) + assert.doesNotMatch(source, /from "zod\/v4-mini"/) + assert.match(source, /jitless:\s*true/) + } + }) + + it("loads, runs and validates every fixture export", async () => { + const { fixtures } = loadRegistry() + const modules = new Map() + for (const fixture of fixtures) { + let module = modules.get(fixture.fixturePath) + if (module === undefined) { + module = await import(pathToFileURL(fixture.fixturePath)) + modules.set(fixture.fixturePath, module) + } + assert.equal(typeof module[fixture.export], "function", fixture.target) + const runtimeCase = module[fixture.export]() + const result = runtimeCase.run() + assert.equal(typeof result?.then, "undefined", fixture.target) + runtimeCase.validate(result) + } + }) +}) diff --git a/.context/effect/packages/effect/runtimeperf/test/stats.test.mts b/.context/effect/packages/effect/runtimeperf/test/stats.test.mts new file mode 100644 index 000000000..4114ab6f1 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/test/stats.test.mts @@ -0,0 +1,46 @@ +import assert from "node:assert/strict" +import { describe, it } from "node:test" +import { aggregate, analyzePairs, bootstrapMedianLogRatio, median, percentile } from "../stats.mts" + +describe("runtimeperf stats", () => { + it("calculates medians and percentiles", () => { + assert.equal(median([3, 1, 2]), 2) + assert.equal(median([4, 1, 3, 2]), 2.5) + assert.equal(percentile([1, 2, 3, 4, 5], 0.5), 3) + }) + + it("aggregates raw observations", () => { + assert.deepEqual(aggregate([10, 12, 14]), { + median: 12, + min: 10, + max: 14, + mad: 2 + }) + }) + + it("classifies improvements, regressions and parity", () => { + const options = { iterations: 1_000, seed: 1 } + assert.equal(analyzePairs([100, 101, 99], [80, 81, 79], options).status, "improvement") + assert.equal(analyzePairs([100, 101, 99], [120, 121, 119], options).status, "regression") + assert.equal(analyzePairs([100, 101, 99], [100, 101, 99], options).status, "inconclusive") + }) + + it("keeps exact percentage thresholds inconclusive", () => { + const options = { iterations: 100, seed: 1 } + assert.equal(analyzePairs([100, 100, 100], [98, 98, 98], options).status, "inconclusive") + assert.equal(analyzePairs([100, 100, 100], [105, 105, 105], options).status, "inconclusive") + }) + + it("is deterministic for a fixed seed", () => { + const first = bootstrapMedianLogRatio([0.8, 0.9, 1], { iterations: 1_000, seed: 42 }) + const second = bootstrapMedianLogRatio([0.8, 0.9, 1], { iterations: 1_000, seed: 42 }) + assert.deepEqual(first, second) + }) + + it("rejects invalid input", () => { + assert.throws(() => median([]), /non-empty/) + assert.throws(() => percentile([1], 2), /between 0 and 1/) + assert.throws(() => bootstrapMedianLogRatio([0]), /finite positive/) + assert.throws(() => analyzePairs([1], [1, 2]), /same number/) + }) +}) diff --git a/.context/effect/packages/effect/runtimeperf/test/worker.test.mts b/.context/effect/packages/effect/runtimeperf/test/worker.test.mts new file mode 100644 index 000000000..72cc79f52 --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/test/worker.test.mts @@ -0,0 +1,60 @@ +import assert from "node:assert/strict" +import { spawnSync } from "node:child_process" +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { describe, it } from "node:test" +import { effectDir, workerPath } from "../utils.mts" + +describe("runtimeperf worker", () => { + it("measures one calibrated batch per minimum iteration and validates the final sink", () => { + const root = mkdtempSync(join(tmpdir(), "effect-runtimeperf-worker-")) + try { + const fixturePath = join(root, "fixture.mjs") + const validationsPath = join(root, "validations.txt") + writeFileSync( + fixturePath, + `import { appendFileSync } from "node:fs" +let count = 0 +export const runtimeCase = () => ({ + run: () => { + count++ + const end = process.hrtime.bigint() + 2_000_000n + while (process.hrtime.bigint() < end) {} + return count + }, + validate: (value) => appendFileSync(${JSON.stringify(validationsPath)}, \`\${value}\\n\`) +}) +` + ) + const result = spawnSync(process.execPath, [ + workerPath, + "--mode", + "measure", + "--fixture", + fixturePath, + "--export", + "runtimeCase", + "--batch-size", + "3", + "--time-ms", + "1", + "--warmup-time-ms", + "1" + ], { + cwd: effectDir, + encoding: "utf8" + }) + assert.equal(result.status, 0, result.stderr) + const output = JSON.parse(result.stdout) + assert.equal(output.runs, 1) + assert.equal(output.batchSize, 3) + assert.deepEqual( + readFileSync(validationsPath, "utf8").trim().split("\n").map(Number), + [1, 7] + ) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/.context/effect/packages/effect/runtimeperf/utils.mts b/.context/effect/packages/effect/runtimeperf/utils.mts new file mode 100644 index 000000000..a146ac9db --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/utils.mts @@ -0,0 +1,252 @@ +import { spawnSync } from "node:child_process" +import { createHash, randomBytes } from "node:crypto" +import { mkdirSync, readFileSync, writeFileSync } from "node:fs" +import { dirname, join, relative, resolve } from "node:path" +import { fileURLToPath } from "node:url" +import { aggregate } from "./stats.mts" + +export const runtimeperfDir = dirname(fileURLToPath(import.meta.url)) +export const effectDir = resolve(runtimeperfDir, "..") +export const repoRoot = resolve(effectDir, "../..") +export const workerPath = join(runtimeperfDir, "worker.mts") +export const configPath = join(runtimeperfDir, "config.json") +export const resultsRoot = join(repoRoot, "tmp", "runtimeperf", "results") + +export const readJson = (path) => JSON.parse(readFileSync(path, "utf8")) + +export const writeJson = (path, value) => { + mkdirSync(dirname(path), { recursive: true }) + writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`) +} + +export const sha256 = (value) => createHash("sha256").update(value).digest("hex") + +export const hashFile = (path) => sha256(readFileSync(path)) + +export const libraryVersions = () => ({ + effect: readJson(join(effectDir, "package.json")).version, + tinybench: readJson(join(effectDir, "node_modules", "tinybench", "package.json")).version, + valibot: readJson(join(effectDir, "node_modules", "valibot", "package.json")).version, + zod: readJson(join(repoRoot, "node_modules", "zod", "package.json")).version, + zodExport: "zod/v4" +}) + +export const currentGitState = () => { + const git = (args) => { + const result = spawnSync("git", args, { cwd: repoRoot, encoding: "utf8" }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${result.stdout}${result.stderr}`.trim()) + } + return result.stdout.trim() + } + const status = git(["status", "--short", "--untracked-files=all"]) + const diff = git(["diff", "--binary", "HEAD", "--"]) + return { + sha: git(["rev-parse", "HEAD"]), + dirty: status !== "", + status: status === "" ? [] : status.split("\n"), + diffHash: sha256(diff) + } +} + +export const makeRunId = () => + `${new Date().toISOString().replace(/[:.]/g, "-")}-${process.pid}-${randomBytes(3).toString("hex")}` + +export const sanitize = (name) => name.replace(/[^a-zA-Z0-9._-]/g, "-") + +export const parseArgs = (args, { compare = false } = {}) => { + const options = { + target: undefined, + rounds: undefined, + timeMs: undefined, + warmupTimeMs: undefined, + tier: undefined, + family: undefined, + implementation: undefined, + base: "HEAD", + head: "worktree", + failOnRegression: false, + help: false + } + const valueOptions = new Map([ + ["--rounds", "rounds"], + ["--time", "timeMs"], + ["--warmup-time", "warmupTimeMs"], + ["--tier", "tier"], + ["--family", "family"], + ["--implementation", "implementation"], + ["--base", "base"], + ["--head", "head"] + ]) + for (let index = 0; index < args.length; index++) { + const arg = args[index] + if (arg === "--help" || arg === "-h") { + options.help = true + } else if (arg === "--fail-on-regression" && compare) { + options.failOnRegression = true + } else if (valueOptions.has(arg)) { + const value = args[++index] + if (value === undefined || value.startsWith("-")) { + throw new Error(`Missing value for ${arg}`) + } + options[valueOptions.get(arg)] = value + } else if (arg.startsWith("-")) { + throw new Error(`Unknown option: ${arg}`) + } else if (options.target === undefined) { + options.target = arg + } else { + throw new Error(`Expected at most one target, got ${options.target} and ${arg}`) + } + } + for (const key of ["rounds", "timeMs", "warmupTimeMs", "tier"]) { + if (options[key] !== undefined) { + const value = Number(options[key]) + if (!Number.isInteger(value) || value < 0 || (key !== "tier" && value === 0)) { + throw new Error(`--${key} must be ${key === "tier" ? "a non-negative" : "a positive"} integer`) + } + options[key] = value + } + } + return options +} + +export const loadRegistry = () => { + const config = readJson(configPath) + const fixtures = config.suites.flatMap((suite) => + suite.fixtures.flatMap((fixtureGroup) => + fixtureGroup.cases.map((runtimeCase) => ({ + ...fixtureGroup.defaults, + ...runtimeCase, + suite: suite.name, + target: `${suite.name}/${runtimeCase.name}`, + fixturePath: resolve(runtimeperfDir, fixtureGroup.file) + })) + ) + ) + return { config, fixtures } +} + +export const selectFixtures = (fixtures, options, { effectOnly = false } = {}) => { + let selected = fixtures + if (options.target !== undefined) { + selected = selected.filter((fixture) => + fixture.suite === options.target || + fixture.target === options.target || + fixture.scenario === options.target + ) + } + if (options.tier !== undefined) { + selected = selected.filter((fixture) => fixture.tier === options.tier) + } + if (options.family !== undefined) { + selected = selected.filter((fixture) => fixture.family === options.family) + } + if (options.implementation !== undefined) { + selected = selected.filter((fixture) => fixture.implementation === options.implementation) + } + if (effectOnly) { + selected = selected.filter((fixture) => fixture.implementation === "effect") + } + if (selected.length === 0) { + throw new Error("No runtimeperf fixtures matched the selection") + } + return selected +} + +export const resolveDefaults = (config, options) => ({ + rounds: options.rounds ?? config.defaults.rounds, + timeMs: options.timeMs ?? config.defaults.timeMs, + warmupTimeMs: options.warmupTimeMs ?? config.defaults.warmupTimeMs, + targetBatchTimeNs: config.defaults.targetBatchTimeNs, + maxBatchSize: config.defaults.maxBatchSize, + bootstrapIterations: config.defaults.bootstrapIterations, + bootstrapSeed: config.defaults.bootstrapSeed, + minImprovementPercent: config.defaults.minImprovementPercent, + maxRegressionPercent: config.defaults.maxRegressionPercent +}) + +export const runWorker = (workerArgs) => { + const result = spawnSync(process.execPath, [workerPath, ...workerArgs], { + cwd: effectDir, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024 + }) + if (result.error) throw result.error + if (result.status !== 0) { + throw new Error(`${result.stderr || result.stdout}`.trim()) + } + try { + return JSON.parse(result.stdout) + } catch { + throw new Error(`Worker returned invalid JSON: ${result.stdout}`) + } +} + +export const calibrateFixture = (fixture, defaults, fixturePath = fixture.fixturePath) => + runWorker([ + "--mode", + "calibrate", + "--fixture", + fixturePath, + "--export", + fixture.export, + "--target-batch-time-ns", + String(defaults.targetBatchTimeNs), + "--max-batch-size", + String(defaults.maxBatchSize) + ]) + +export const measureFixture = (fixture, defaults, batchSize, fixturePath = fixture.fixturePath) => + runWorker([ + "--mode", + "measure", + "--fixture", + fixturePath, + "--export", + fixture.export, + "--batch-size", + String(batchSize), + "--time-ms", + String(defaults.timeMs), + "--warmup-time-ms", + String(defaults.warmupTimeMs) + ]) + +export const aggregateMeasurements = (measurements) => aggregate(measurements.map((item) => item.nsPerOp)) + +export const coverageSummary = (fixtures) => ({ + tiers: [...new Set(fixtures.map((fixture) => fixture.tier))].sort(), + families: [...new Set(fixtures.map((fixture) => fixture.family))].sort(), + implementations: [...new Set(fixtures.map((fixture) => fixture.implementation))].sort(), + effectAstTags: [ + ...new Set( + fixtures + .filter((fixture) => fixture.implementation === "effect") + .flatMap((fixture) => fixture.astTags) + ) + ].sort() +}) + +export const formatNs = (value) => value < 1_000 + ? value.toFixed(1) + : value < 1_000_000 + ? `${(value / 1_000).toFixed(2)}µs` + : `${(value / 1_000_000).toFixed(2)}ms` + +export const printTable = (headers, rows) => { + const textRows = rows.map((row) => row.map(String)) + const table = [headers, ...textRows] + const widths = headers.map((_, index) => Math.max(...table.map((row) => row[index].length))) + table.forEach((row, index) => { + process.stdout.write(`${row.map((cell, cellIndex) => cell.padEnd(widths[cellIndex])).join(" ")}\n`) + if (index === 0) { + process.stdout.write(`${widths.map((width) => "-".repeat(width)).join(" ")}\n`) + } + }) +} + +export const reportPath = (runId, target, kind) => + join(resultsRoot, `${runId}-${kind}-${sanitize(target ?? "all")}.json`) + +export const relativeToRepo = (path) => relative(repoRoot, path) diff --git a/.context/effect/packages/effect/runtimeperf/worker.mts b/.context/effect/packages/effect/runtimeperf/worker.mts new file mode 100644 index 000000000..19bc6d1ae --- /dev/null +++ b/.context/effect/packages/effect/runtimeperf/worker.mts @@ -0,0 +1,154 @@ +import { pathToFileURL } from "node:url" +import { Bench } from "tinybench" +import { median } from "./stats.mts" + +let sink + +const args = process.argv.slice(2) +const readOption = (name) => { + const index = args.indexOf(name) + if (index === -1) return undefined + const value = args[index + 1] + if (value === undefined || value.startsWith("--")) { + throw new Error(`Missing value for ${name}`) + } + return value +} + +const readPositiveNumber = (name, fallback) => { + const raw = readOption(name) + if (raw === undefined) return fallback + const value = Number(raw) + if (!Number.isFinite(value) || value <= 0) { + throw new Error(`${name} must be a positive number`) + } + return value +} + +const isPromiseLike = (value) => + value !== null && + (typeof value === "object" || typeof value === "function") && + typeof value.then === "function" + +const loadCase = async (fixturePath, exportName) => { + const fixture = await import(`${pathToFileURL(fixturePath).href}?runtimeperf=${process.pid}`) + const factory = fixture[exportName] + if (typeof factory !== "function") { + throw new Error(`Fixture export ${exportName} is not a function`) + } + const runtimeCase = factory() + if ( + runtimeCase === null || + typeof runtimeCase !== "object" || + typeof runtimeCase.run !== "function" || + typeof runtimeCase.validate !== "function" + ) { + throw new Error(`Fixture export ${exportName} did not return a RuntimePerfCase`) + } + const validationResult = runtimeCase.run() + if (isPromiseLike(validationResult)) { + throw new Error(`Fixture export ${exportName} returned a Promise`) + } + runtimeCase.validate(validationResult) + sink = validationResult + return runtimeCase +} + +const runBatch = (run, batchSize) => { + let value + for (let index = 0; index < batchSize; index++) { + value = run() + } + if (isPromiseLike(value)) { + throw new Error("Synchronous runtimeperf task returned a Promise") + } + sink = value +} + +const calibrate = (runtimeCase, targetBatchTimeNs, maxBatchSize) => { + const warnings = [] + let batchSize = 1 + while (true) { + const samples = new Array(5) + for (let attempt = 0; attempt < samples.length; attempt++) { + const start = process.hrtime.bigint() + runBatch(runtimeCase.run, batchSize) + samples[attempt] = Number(process.hrtime.bigint() - start) + } + if (median(samples) >= targetBatchTimeNs || batchSize >= maxBatchSize) { + if (batchSize >= maxBatchSize && median(samples) < targetBatchTimeNs) { + warnings.push(`Maximum batch size ${maxBatchSize} did not reach ${targetBatchTimeNs} ns`) + } + return { batchSize, warnings } + } + batchSize = Math.min(batchSize * 2, maxBatchSize) + } +} + +const measure = (runtimeCase, batchSize, timeMs, warmupTimeMs) => { + const bench = new Bench({ + iterations: 1, + time: timeMs, + warmup: true, + warmupIterations: 1, + warmupTime: warmupTimeMs, + timestampProvider: "hrtimeNow" + }) + bench.add("runtimeperf", () => runBatch(runtimeCase.run, batchSize), { async: false }) + bench.runSync() + const task = bench.tasks[0] + const result = task.result + if (result?.state !== "completed") { + throw new Error(`Tinybench task did not complete: ${result?.state ?? "missing result"}`) + } + const latencyToNs = (value) => value * 1_000_000 / batchSize + return { + nsPerOp: result.totalTime * 1_000_000 / (task.runs * batchSize), + runs: task.runs, + batchSize, + totalTimeMs: result.totalTime, + latency: { + p50Ns: latencyToNs(result.latency.p50), + p99Ns: latencyToNs(result.latency.p99), + minNs: latencyToNs(result.latency.min), + maxNs: latencyToNs(result.latency.max), + rme: result.latency.rme, + samplesCount: result.latency.samplesCount + }, + runtime: result.runtime, + runtimeVersion: result.runtimeVersion, + timestampProviderName: result.timestampProviderName + } +} + +const main = async () => { + const mode = readOption("--mode") + const fixturePath = readOption("--fixture") + const exportName = readOption("--export") + if (mode !== "calibrate" && mode !== "measure") { + throw new Error("--mode must be calibrate or measure") + } + if (fixturePath === undefined || exportName === undefined) { + throw new Error("--fixture and --export are required") + } + const runtimeCase = await loadCase(fixturePath, exportName) + const output = mode === "calibrate" + ? calibrate( + runtimeCase, + readPositiveNumber("--target-batch-time-ns", 100_000), + readPositiveNumber("--max-batch-size", 1_048_576) + ) + : measure( + runtimeCase, + readPositiveNumber("--batch-size", 1), + readPositiveNumber("--time-ms", 500), + readPositiveNumber("--warmup-time-ms", 150) + ) + runtimeCase.validate(sink) + process.stdout.write(`${JSON.stringify({ ok: true, mode, ...output })}\n`) +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`) + process.exitCode = 1 +}) diff --git a/.context/effect/packages/effect/src/Array.ts b/.context/effect/packages/effect/src/Array.ts index 7e3958d3a..c35cf612a 100644 --- a/.context/effect/packages/effect/src/Array.ts +++ b/.context/effect/packages/effect/src/Array.ts @@ -13,9 +13,11 @@ import * as Equal from "./Equal.ts" import * as Equivalence from "./Equivalence.ts" import type { LazyArg } from "./Function.ts" import { dual, identity } from "./Function.ts" +import * as Hash from "./Hash.ts" import type { TypeLambda } from "./HKT.ts" import * as internalArray from "./internal/array.ts" import * as internalDoNotation from "./internal/doNotation.ts" +import * as InternalRecord from "./internal/record.ts" import * as moduleIterable from "./Iterable.ts" import * as Option from "./Option.ts" import * as Order from "./Order.ts" @@ -36,11 +38,10 @@ import type { NoInfer, TupleOf } from "./Types.ts" * * **Example** (Accessing the Array constructor) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const arr = new Array.Array(3) - * console.log(arr) // [undefined, undefined, undefined] + * Array.Array === globalThis.Array // => true * ``` * * @category constructors @@ -51,7 +52,7 @@ export const Array = globalThis.Array /** * Type lambda for `ReadonlyArray`, used for higher-kinded type operations. * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface ReadonlyArrayTypeLambda extends TypeLambda { @@ -68,11 +69,13 @@ export interface ReadonlyArrayTypeLambda extends TypeLambda { * * **Example** (Typing a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * const nonEmpty: Array.NonEmptyReadonlyArray = [1, 2, 3] * const head: number = nonEmpty[0] // guaranteed to exist + * + * head // => 1 * ``` * * @see {@link NonEmptyArray} — mutable counterpart @@ -99,11 +102,13 @@ export type NonEmptyReadonlyArray = readonly [A, ...Array] * * **Example** (Typing a mutable non-empty array) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * const nonEmpty: Array.NonEmptyArray = [1, 2, 3] * nonEmpty.push(4) + * + * nonEmpty // => [1, 2, 3, 4] * ``` * * @see {@link NonEmptyReadonlyArray} — readonly counterpart @@ -128,11 +133,10 @@ export type NonEmptyArray = [A, ...Array] * * **Example** (Creating an array from values) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.make(1, 2, 3) - * console.log(result) // [1, 2, 3] + * Array.make(1, 2, 3) // => [1, 2, 3] * ``` * * @see {@link of} — create a single-element array @@ -158,11 +162,10 @@ export const make = >( * * **Example** (Allocating a fixed-size array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.allocate(3) - * console.log(result.length) // 3 + * Array.allocate(3).length // => 3 * ``` * * @see {@link makeBy} — create an array by computing each element @@ -187,11 +190,10 @@ export const allocate = (n: number): Array => new Arra * * **Example** (Generating values from indices) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.makeBy(5, (n) => n * 2) - * console.log(result) // [0, 2, 4, 6, 8] + * Array.makeBy(5, (n) => n * 2) // => [0, 2, 4, 6, 8] * ``` * * @see {@link range} — create a range of integers @@ -226,11 +228,10 @@ export const makeBy: { * * **Example** (Creating a range) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.range(1, 3) - * console.log(result) // [1, 2, 3] + * Array.range(1, 3) // => [1, 2, 3] * ``` * * @see {@link makeBy} — generate values from a function @@ -256,11 +257,10 @@ export const range = (start: number, end: number): NonEmptyArray => * * **Example** (Repeating a value) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.replicate("a", 3) - * console.log(result) // ["a", "a", "a"] + * Array.replicate("a", 3) // => ["a", "a", "a"] * ``` * * @see {@link makeBy} — vary values based on index @@ -288,11 +288,10 @@ export const replicate: { * * **Example** (Converting a Set to an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.fromIterable(new Set([1, 2, 3])) - * console.log(result) // [1, 2, 3] + * Array.fromIterable(new Set([1, 2, 3])) // => [1, 2, 3] * ``` * * @see {@link ensure} — wrap a single value or return an existing array @@ -320,11 +319,11 @@ export const fromIterable = (collection: Iterable): Array => * * **Example** (Normalizing input) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.ensure("a")) // ["a"] - * console.log(Array.ensure(["a", "b", "c"])) // ["a", "b", "c"] + * Array.ensure("a") // => ["a"] + * Array.ensure(["a", "b", "c"]) // => ["a", "b", "c"] * ``` * * @see {@link of} — always wrap in a single-element array @@ -350,11 +349,10 @@ export const ensure = (self: ReadonlyArray | A): Array => Array.isArray * * **Example** (Converting a record to entries) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.fromRecord({ a: 1, b: 2, c: 3 }) - * console.log(result) // [["a", 1], ["b", 2], ["c", 3]] + * Array.fromRecord({ a: 1, b: 2, c: 3 }) // => [["a", 1], ["b", 2], ["c", 3]] * ``` * * @see {@link Record.toEntries} the equivalent function from the Record module @@ -374,11 +372,11 @@ export const fromRecord: (self: Readonly>) => * * **Example** (Converting an Option to an array) * - * ```ts + * ```ts import.meta.vitest * import { Array, Option } from "effect" * - * console.log(Array.fromOption(Option.some(1))) // [1] - * console.log(Array.fromOption(Option.none())) // [] + * Array.fromOption(Option.some(1)) // => [1] + * Array.fromOption(Option.none()) // => [] * ``` * * @see {@link getSomes} — extract `Some` values from an array of Options @@ -402,15 +400,16 @@ export const fromOption: (self: Option.Option) => Array = Option.toArra * * **Example** (Branching on emptiness) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * * const describe = Array.match({ * onEmpty: () => "empty", * onNonEmpty: ([head, ...tail]) => `head: ${head}, tail: ${tail.length}` * }) - * console.log(describe([])) // "empty" - * console.log(describe([1, 2, 3])) // "head: 1, tail: 2" + * + * describe([]) // => "empty" + * describe([1, 2, 3]) // => "head: 1, tail: 2" * ``` * * @see {@link matchLeft} — destructures into head + tail @@ -456,15 +455,16 @@ export const match: { * * **Example** (Destructuring head and tail) * - * ```ts + * ```ts import.meta.vitest * 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" + * + * matchLeft([]) // => "empty" + * matchLeft([1, 2, 3]) // => "head: 1, tail: 2" * ``` * * @see {@link match} — receives the full non-empty array @@ -510,15 +510,16 @@ export const matchLeft: { * * **Example** (Destructuring init and last) * - * ```ts + * ```ts import.meta.vitest * 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" + * + * matchRight([]) // => "empty" + * matchRight([1, 2, 3]) // => "init: 2, last: 3" * ``` * * @see {@link match} — receives the full non-empty array @@ -562,11 +563,10 @@ export const matchRight: { * * **Example** (Prepending an element) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.prepend([2, 3, 4], 1) - * console.log(result) // [1, 2, 3, 4] + * Array.prepend([2, 3, 4], 1) // => [1, 2, 3, 4] * ``` * * @see {@link append} — add to the end @@ -593,11 +593,10 @@ export const prepend: { * * **Example** (Prepending multiple elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.prependAll([2, 3], [0, 1]) - * console.log(result) // [0, 1, 2, 3] + * Array.prependAll([2, 3], [0, 1]) // => [0, 1, 2, 3] * ``` * * @see {@link prepend} — add a single element to the front @@ -628,11 +627,10 @@ export const prependAll: { * * **Example** (Appending an element) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.append([1, 2, 3], 4) - * console.log(result) // [1, 2, 3, 4] + * Array.append([1, 2, 3], 4) // => [1, 2, 3, 4] * ``` * * @see {@link prepend} — add to the front @@ -660,11 +658,10 @@ export const append: { * * **Example** (Concatenating arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.appendAll([1, 2], [3, 4]) - * console.log(result) // [1, 2, 3, 4] + * Array.appendAll([1, 2], [3, 4]) // => [1, 2, 3, 4] * ``` * * @see {@link append} — add a single element to the end @@ -700,11 +697,10 @@ export const appendAll: { * * **Example** (Running totals) * - * ```ts + * ```ts import.meta.vitest * 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] + * Array.scan([1, 2, 3, 4], 0, (acc, value) => acc + value) // => [0, 1, 3, 6, 10] * ``` * * @see {@link scanRight} — right-to-left scan @@ -741,11 +737,10 @@ export const scan: { * * **Example** (Scanning running totals in reverse) * - * ```ts + * ```ts import.meta.vitest * 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] + * Array.scanRight([1, 2, 3, 4], 0, (acc, value) => acc + value) // => [10, 9, 7, 4, 0] * ``` * * @see {@link scan} — left-to-right scan @@ -781,11 +776,11 @@ export const scanRight: { * * **Example** (Type-guarding an unknown value) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.isArray(null)) // false - * console.log(Array.isArray([1, 2, 3])) // true + * Array.isArray(null) // => false + * Array.isArray([1, 2, 3]) // => true * ``` * * @see {@link isArrayEmpty} — check for an empty array @@ -804,11 +799,11 @@ export const isArray: { * * **Example** (Checking for an empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.isArrayEmpty([])) // true - * console.log(Array.isArrayEmpty([1, 2, 3])) // false + * Array.isArrayEmpty([]) // => true + * Array.isArrayEmpty([1, 2, 3]) // => false * ``` * * @see {@link isReadonlyArrayEmpty} — readonly variant @@ -824,11 +819,11 @@ export const isArrayEmpty = (self: Array): self is [] => self.length === 0 * * **Example** (Checking for an empty readonly array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.isReadonlyArrayEmpty([])) // true - * console.log(Array.isReadonlyArrayEmpty([1, 2, 3])) // false + * Array.isReadonlyArrayEmpty([]) // => true + * Array.isReadonlyArrayEmpty([1, 2, 3]) // => false * ``` * * @see {@link isArrayEmpty} — mutable variant @@ -850,11 +845,11 @@ export const isReadonlyArrayEmpty: (self: ReadonlyArray) => self is readon * * **Example** (Checking for a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.isArrayNonEmpty([])) // false - * console.log(Array.isArrayNonEmpty([1, 2, 3])) // true + * Array.isArrayNonEmpty([]) // => false + * Array.isArrayNonEmpty([1, 2, 3]) // => true * ``` * * @see {@link isReadonlyArrayNonEmpty} — readonly variant @@ -876,11 +871,11 @@ export const isArrayNonEmpty: (self: Array) => self is NonEmptyArray = * * **Example** (Checking for a non-empty readonly array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.isReadonlyArrayNonEmpty([])) // false - * console.log(Array.isReadonlyArrayNonEmpty([1, 2, 3])) // true + * Array.isReadonlyArrayNonEmpty([]) // => false + * Array.isReadonlyArrayNonEmpty([1, 2, 3]) // => true * ``` * * @see {@link isArrayNonEmpty} — mutable variant @@ -901,10 +896,10 @@ export const isReadonlyArrayNonEmpty: (self: ReadonlyArray) => self is Non * * **Example** (Getting the length) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.length([1, 2, 3])) // 3 + * Array.length([1, 2, 3]) // => 3 * ``` * * @category getters @@ -914,7 +909,7 @@ export const length = (self: ReadonlyArray): number => self.length /** @internal */ export function isOutOfBounds(i: number, as: ReadonlyArray): boolean { - return i < 0 || i >= as.length + return !Number.isFinite(i) || i < 0 || i >= as.length } const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.min(Math.max(0, i), as.length)) @@ -934,11 +929,11 @@ const clamp = (i: number, as: ReadonlyArray): number => Math.floor(Math.mi * * **Example** (Accessing indexes safely) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.get([1, 2, 3], 1)) // Some(2) - * console.log(Array.get([1, 2, 3], 10)) // None + * Array.get([1, 2, 3], 1) // => Option.some(2) + * Array.get([1, 2, 3], 10) // => Option.none() * ``` * * @see {@link getUnsafe} for indexed access that throws when the index is out of bounds @@ -971,10 +966,10 @@ export const get: { * * **Example** (Accessing indexes unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.getUnsafe([1, 2, 3], 1)) // 2 + * Array.getUnsafe([1, 2, 3], 1) // => 2 * // Array.getUnsafe([1, 2, 3], 10) // throws Error * ``` * @@ -1008,11 +1003,10 @@ export const getUnsafe: { * * **Example** (Destructuring head and tail) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.unprepend([1, 2, 3, 4]) - * console.log(result) // [1, [2, 3, 4]] + * Array.unprepend([1, 2, 3, 4]) // => [1, [2, 3, 4]] * ``` * * @see {@link unappend} for splitting a non-empty array into init and last @@ -1041,11 +1035,10 @@ export const unprepend = ( * * **Example** (Destructuring init and last) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.unappend([1, 2, 3, 4]) - * console.log(result) // [[1, 2, 3], 4] + * Array.unappend([1, 2, 3, 4]) // => [[1, 2, 3], 4] * ``` * * @see {@link unprepend} for splitting a non-empty array into head and tail @@ -1069,11 +1062,11 @@ export const unappend = ( * * **Example** (Getting the first element) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.head([1, 2, 3])) // Some(1) - * console.log(Array.head([])) // None + * Array.head([1, 2, 3]) // => Option.some(1) + * Array.head([]) // => Option.none() * ``` * * @see {@link headNonEmpty} — direct access when array is known non-empty @@ -1095,10 +1088,10 @@ export const head: (self: ReadonlyArray) => Option.Option = get(0) * * **Example** (Getting the head of a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.headNonEmpty([1, 2, 3, 4])) // 1 + * Array.headNonEmpty([1, 2, 3, 4]) // => 1 * ``` * * @see {@link head} — safe version for possibly-empty arrays @@ -1118,11 +1111,11 @@ export const headNonEmpty: (self: NonEmptyReadonlyArray) => A = getUnsafe( * * **Example** (Getting the last element) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.last([1, 2, 3])) // Some(3) - * console.log(Array.last([])) // None + * Array.last([1, 2, 3]) // => Option.some(3) + * Array.last([]) // => Option.none() * ``` * * @see {@link lastNonEmpty} — direct access when array is known non-empty @@ -1145,10 +1138,10 @@ export const last = (self: ReadonlyArray): Option.Option => * * **Example** (Getting the last of a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.lastNonEmpty([1, 2, 3, 4])) // 4 + * Array.lastNonEmpty([1, 2, 3, 4]) // => 4 * ``` * * @see {@link last} — safe version for possibly-empty arrays @@ -1171,11 +1164,11 @@ export const lastNonEmpty = (self: NonEmptyReadonlyArray): A => self[self. * * **Example** (Getting the tail) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.tail([1, 2, 3, 4])) // Option.some([2, 3, 4]) - * console.log(Array.tail([])) // Option.none() + * Array.tail([1, 2, 3, 4]) // => Option.some([2, 3, 4]) + * Array.tail([]) // => Option.none() * ``` * * @see {@link tailNonEmpty} — when the array is known non-empty @@ -1198,10 +1191,10 @@ export function tail(self: Iterable): Option.Option> { * * **Example** (Getting the tail of a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.tailNonEmpty([1, 2, 3, 4])) // [2, 3, 4] + * Array.tailNonEmpty([1, 2, 3, 4]) // => [2, 3, 4] * ``` * * @see {@link tail} — safe version for possibly-empty arrays @@ -1226,11 +1219,11 @@ export const tailNonEmpty = (self: NonEmptyReadonlyArray): Array => sel * * **Example** (Getting init) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.init([1, 2, 3, 4])) // Option.some([1, 2, 3]) - * console.log(Array.init([])) // Option.none() + * Array.init([1, 2, 3, 4]) // => Option.some([1, 2, 3]) + * Array.init([]) // => Option.none() * ``` * * @see {@link initNonEmpty} — when the array is known non-empty @@ -1253,10 +1246,10 @@ export function init(self: Iterable): Option.Option> { * * **Example** (Getting init of a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.initNonEmpty([1, 2, 3, 4])) // [1, 2, 3] + * Array.initNonEmpty([1, 2, 3, 4]) // => [1, 2, 3] * ``` * * @see {@link init} — safe version for possibly-empty arrays @@ -1280,10 +1273,10 @@ export const initNonEmpty = (self: NonEmptyReadonlyArray): Array => sel * * **Example** (Taking from the start) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.take([1, 2, 3, 4, 5], 3)) // [1, 2, 3] + * Array.take([1, 2, 3, 4, 5], 3) // => [1, 2, 3] * ``` * * @see {@link takeRight} for keeping elements from the end @@ -1314,10 +1307,10 @@ export const take: { * * **Example** (Taking from the end) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.takeRight([1, 2, 3, 4, 5], 3)) // [3, 4, 5] + * Array.takeRight([1, 2, 3, 4, 5], 3) // => [3, 4, 5] * ``` * * @see {@link take} — keep from the start @@ -1351,10 +1344,10 @@ export const takeRight: { * * **Example** (Taking while condition holds) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.takeWhile([1, 3, 2, 4, 1, 2], (x) => x < 4)) // [1, 3, 2] + * Array.takeWhile([1, 3, 2, 4, 1, 2], (x) => x < 4) // => [1, 3, 2] * ``` * * @see {@link take} for keeping a fixed number of leading elements @@ -1446,10 +1439,10 @@ const spanIndex = (self: Iterable, predicate: (a: A, i: number) => boolean * * **Example** (Splitting at predicate boundary) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.span([1, 3, 2, 4, 5], (x) => x % 2 === 1)) // [[1, 3], [2, 4, 5]] + * Array.span([1, 3, 2, 4, 5], (x) => x % 2 === 1) // => [[1, 3], [2, 4, 5]] * ``` * * @see {@link takeWhile} for keeping only the matching prefix @@ -1492,10 +1485,10 @@ export const span: { * * **Example** (Dropping from the start) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.drop([1, 2, 3, 4, 5], 2)) // [3, 4, 5] + * Array.drop([1, 2, 3, 4, 5], 2) // => [3, 4, 5] * ``` * * @see {@link dropRight} for removing a fixed number of elements from the end @@ -1526,10 +1519,10 @@ export const drop: { * * **Example** (Dropping from the end) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dropRight([1, 2, 3, 4, 5], 2)) // [1, 2, 3] + * Array.dropRight([1, 2, 3, 4, 5], 2) // => [1, 2, 3] * ``` * * @see {@link drop} — remove from the start @@ -1559,10 +1552,10 @@ export const dropRight: { * * **Example** (Dropping while condition holds) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dropWhile([1, 2, 3, 4, 5], (x) => x < 4)) // [4, 5] + * Array.dropWhile([1, 2, 3, 4, 5], (x) => x < 4) // => [4, 5] * ``` * * @see {@link takeWhile} — keep the matching prefix instead @@ -1634,16 +1627,16 @@ export const dropWhileFilter: { * * **Example** (Finding an index) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.findFirstIndex([5, 3, 8, 9], (x) => x > 5)) // Option.some(2) + * Array.findFirstIndex([5, 3, 8, 9], (x) => x > 5) // => Option.some(2) * ``` * * @see {@link findLastIndex} — search from the end * @see {@link findFirst} — get the element itself * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirstIndex: { @@ -1670,16 +1663,16 @@ export const findFirstIndex: { * * **Example** (Finding the last matching index) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.findLastIndex([1, 3, 8, 9], (x) => x < 5)) // Option.some(1) + * Array.findLastIndex([1, 3, 8, 9], (x) => x < 5) // => Option.some(1) * ``` * * @see {@link findFirstIndex} — search from the start * @see {@link findLast} — get the element itself * - * @category elements + * @category searching * @since 2.0.0 */ export const findLastIndex: { @@ -1712,17 +1705,17 @@ export const findLastIndex: { * * **Example** (Finding the first match) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.findFirst([1, 2, 3, 4, 5], (x) => x > 3)) // Option.some(4) + * Array.findFirst([1, 2, 3, 4, 5], (x) => x > 3) // => Option.some(4) * ``` * * @see {@link findLast} — search from the end * @see {@link findFirstIndex} — get the index instead * @see {@link findFirstWithIndex} — get both element and index * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirst: { @@ -1750,16 +1743,16 @@ export const findFirst: { * * **Example** (Finding element with its index) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.findFirstWithIndex([1, 2, 3, 4, 5], (x) => x > 3)) // Option.some([4, 3]) + * Array.findFirstWithIndex([1, 2, 3, 4, 5], (x) => x > 3) // => Option.some([4, 3]) * ``` * * @see {@link findFirst} — get only the element * @see {@link findFirstIndex} — get only the index * - * @category elements + * @category searching * @since 3.17.0 */ export const findFirstWithIndex: { @@ -1808,16 +1801,16 @@ export const findFirstWithIndex: { * * **Example** (Finding the last match) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.findLast([1, 2, 3, 4, 5], (n) => n % 2 === 0)) // Option.some(4) + * Array.findLast([1, 2, 3, 4, 5], (n) => n % 2 === 0) // => Option.some(4) * ``` * * @see {@link findFirst} — search from the start * @see {@link findLastIndex} — get the index instead * - * @category elements + * @category searching * @since 2.0.0 */ export const findLast: { @@ -1865,16 +1858,16 @@ export const findLast: { * * **Example** (Inserting at an index) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.insertAt(["a", "b", "c", "e"], 3, "d")) // Option.some(["a", "b", "c", "d", "e"]) + * Array.insertAt(["a", "b", "c", "e"], 3, "d") // => Option.some(["a", "b", "c", "d", "e"]) * ``` * * @see {@link replace} — replace an existing element * @see {@link modify} — transform an element at an index * - * @category elements + * @category transforming * @since 2.0.0 */ export const insertAt: { @@ -1882,10 +1875,11 @@ export const insertAt: { (self: Iterable, i: number, b: B): Option.Option> } = dual(3, (self: Iterable, i: number, b: B): Option.Option> => { const out: Array = Array.from(self) // copy because `splice` mutates the array - if (i < 0 || i > out.length) { + const index = Math.floor(i) + if (index !== out.length && isOutOfBounds(index, out)) { return Option.none() } - out.splice(i, 0, b) + out.splice(index, 0, b) return Option.some(out as any) }) @@ -1903,16 +1897,16 @@ export const insertAt: { * * **Example** (Replacing an element) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" * - * console.log(Array.replace([1, 2, 3], 1, 4)) // Option.some([1, 4, 3]) + * Array.replace([1, 2, 3], 1, 4) // => Option.some([1, 4, 3]) * ``` * * @see {@link modify} — transform an element with a function * @see {@link insertAt} — insert without removing * - * @category elements + * @category transforming * @since 2.0.0 */ export const replace: { @@ -1944,18 +1938,21 @@ export const replace: { * * **Example** (Modifying an element) * - * ```ts - * import { Array } from "effect" + * ```ts import.meta.vitest + * import { Array, Option } from "effect" + * + * const values = [1, 2, 3, 4] + * const double = (n: number) => n * 2 * - * console.log(Array.modify([1, 2, 3, 4], 2, (n) => n * 2)) // Option.some([1, 2, 6, 4]) - * console.log(Array.modify([1, 2, 3, 4], 5, (n) => n * 2)) // Option.none() + * Array.modify(values, 2, double) // => Option.some([1, 2, 6, 4]) + * Array.modify(values, 5, double) // => Option.none() * ``` * * @see {@link replace} — set a fixed value at an index * @see {@link modifyHeadNonEmpty} — modify the first element * @see {@link modifyLastNonEmpty} — modify the last element * - * @category elements + * @category transforming * @since 2.0.0 */ export const modify: { @@ -1970,12 +1967,13 @@ export const modify: { ): Option.Option | B>> } = dual(3, (self: Iterable, i: number, f: (a: A) => B): Option.Option> => { const arr = Array.from(self) - if (isOutOfBounds(i, arr)) { + const index = Math.floor(i) + if (isOutOfBounds(index, arr)) { return Option.none() } const out: Array = arr - const b = f(arr[i]) - out[i] = b + const b = f(arr[index]) + out[index] = b return Option.some(out) }) @@ -1990,17 +1988,17 @@ export const modify: { * * **Example** (Removing an element) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.remove([1, 2, 3, 4], 2)) // [1, 2, 4] - * console.log(Array.remove([1, 2, 3, 4], 5)) // [1, 2, 3, 4] + * Array.remove([1, 2, 3, 4], 2) // => [1, 2, 4] + * Array.remove([1, 2, 3, 4], 5) // => [1, 2, 3, 4] * ``` * * @see {@link insertAt} — insert an element * @see {@link filter} — remove elements by predicate * - * @category elements + * @category transforming * @since 2.0.0 */ export const remove: { @@ -2008,10 +2006,11 @@ export const remove: { (self: Iterable, i: number): Array } = dual(2, (self: Iterable, i: number): Array => { const out = Array.from(self) - if (isOutOfBounds(i, out)) { + const index = Math.floor(i) + if (isOutOfBounds(index, out)) { return out } - out.splice(i, 1) + out.splice(index, 1) return out }) @@ -2029,13 +2028,13 @@ export const remove: { * * **Example** (Reversing an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.reverse([1, 2, 3, 4])) // [4, 3, 2, 1] + * Array.reverse([1, 2, 3, 4]) // => [4, 3, 2, 1] * ``` * - * @category elements + * @category transforming * @since 2.0.0 */ export const reverse = >( @@ -2057,10 +2056,10 @@ export const reverse = >( * * **Example** (Sorting numbers) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order } from "effect" * - * console.log(Array.sort([3, 1, 4, 1, 5], Order.Number)) // [1, 1, 3, 4, 5] + * Array.sort([3, 1, 4, 1, 5], Order.Number) // => [1, 1, 3, 4, 5] * ``` * * @see {@link sortWith} — sort by a mapping function @@ -2096,17 +2095,16 @@ export const sort: { * * **Example** (Sorting strings by length) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order } from "effect" * - * console.log(Array.sortWith(["aaa", "b", "cc"], (s) => s.length, Order.Number)) - * // ["b", "cc", "aaa"] + * Array.sortWith(["aaa", "b", "cc"], (s) => s.length, Order.Number) // => ["b", "cc", "aaa"] * ``` * * @see {@link sort} for sorting with an `Order` that compares the elements directly * @see {@link sortBy} for sorting with multiple `Order`s applied in sequence * - * @category elements + * @category sorting * @since 2.0.0 */ export const sortWith: { @@ -2138,7 +2136,7 @@ export const sortWith: { * * **Example** (Sorting by multiple keys) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order, pipe } from "effect" * * const users = [ @@ -2147,15 +2145,15 @@ export const sortWith: { * { name: "Charlie", age: 30 } * ] * - * const result = pipe( + * const sortedUsers = 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 }] + * + * sortedUsers.map((user) => user.name).join(",") // => "Bob,Alice,Charlie" * ``` * * @see {@link sort} — sort by a single `Order` @@ -2193,10 +2191,10 @@ export const sortBy = >( * * **Example** (Zipping two arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.zip([1, 2, 3], ["a", "b"])) // [[1, "a"], [2, "b"]] + * Array.zip([1, 2, 3], ["a", "b"]) // => [[1, "a"], [2, "b"]] * ``` * * @see {@link zipWith} — zip with a combiner function @@ -2226,10 +2224,10 @@ export const zip: { * * **Example** (Zipping with addition) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.zipWith([1, 2, 3], [4, 5, 6], (a, b) => a + b)) // [5, 7, 9] + * Array.zipWith([1, 2, 3], [4, 5, 6], (a, b) => a + b) // => [5, 7, 9] * ``` * * @see {@link zip} — zip into tuples @@ -2261,10 +2259,10 @@ export const zipWith: { * * **Example** (Unzipping pairs) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.unzip([[1, "a"], [2, "b"], [3, "c"]])) // [[1, 2, 3], ["a", "b", "c"]] + * Array.unzip([[1, "a"], [2, "b"], [3, "c"]]) // => [[1, 2, 3], ["a", "b", "c"]] * ``` * * @see {@link zip} — combine two arrays into pairs @@ -2304,15 +2302,15 @@ export const unzip: >( * * **Example** (Interspersing a separator) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.intersperse([1, 2, 3], 0)) // [1, 0, 2, 0, 3] + * Array.intersperse([1, 2, 3], 0) // => [1, 0, 2, 0, 3] * ``` * * @see {@link join} — intersperse and join into a string * - * @category elements + * @category transforming * @since 2.0.0 */ export const intersperse: { @@ -2347,16 +2345,16 @@ export const intersperse: { * * **Example** (Modifying the head) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.modifyHeadNonEmpty([1, 2, 3], (n) => n * 10)) // [10, 2, 3] + * Array.modifyHeadNonEmpty([1, 2, 3], (n) => n * 10) // => [10, 2, 3] * ``` * * @see {@link setHeadNonEmpty} — replace with a fixed value * @see {@link modifyLastNonEmpty} — modify the last element * - * @category elements + * @category transforming * @since 4.0.0 */ export const modifyHeadNonEmpty: { @@ -2380,16 +2378,16 @@ export const modifyHeadNonEmpty: { * * **Example** (Setting the head) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.setHeadNonEmpty([1, 2, 3], 10)) // [10, 2, 3] + * Array.setHeadNonEmpty([1, 2, 3], 10) // => [10, 2, 3] * ``` * * @see {@link modifyHeadNonEmpty} — transform the head with a function * @see {@link setLastNonEmpty} — replace the last element * - * @category elements + * @category transforming * @since 4.0.0 */ export const setHeadNonEmpty: { @@ -2411,16 +2409,16 @@ export const setHeadNonEmpty: { * * **Example** (Modifying the last element) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.modifyLastNonEmpty([1, 2, 3], (n) => n * 2)) // [1, 2, 6] + * Array.modifyLastNonEmpty([1, 2, 3], (n) => n * 2) // => [1, 2, 6] * ``` * * @see {@link setLastNonEmpty} — replace with a fixed value * @see {@link modifyHeadNonEmpty} — modify the first element * - * @category elements + * @category transforming * @since 4.0.0 */ export const modifyLastNonEmpty: { @@ -2442,16 +2440,16 @@ export const modifyLastNonEmpty: { * * **Example** (Setting the last element) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.setLastNonEmpty([1, 2, 3], 4)) // [1, 2, 4] + * Array.setLastNonEmpty([1, 2, 3], 4) // => [1, 2, 4] * ``` * * @see {@link modifyLastNonEmpty} — transform the last element with a function * @see {@link setHeadNonEmpty} — replace the first element * - * @category elements + * @category transforming * @since 4.0.0 */ export const setLastNonEmpty: { @@ -2479,16 +2477,16 @@ export const setLastNonEmpty: { * * **Example** (Rotating elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.rotate(["a", "b", "c", "d"], 2)) // ["c", "d", "a", "b"] + * Array.rotate(["a", "b", "c", "d"], 2) // => ["c", "d", "a", "b"] * ``` * * @see {@link take} for taking a fixed number of elements from the start * @see {@link drop} for dropping a fixed number of elements from the start * - * @category elements + * @category transforming * @since 2.0.0 */ export const rotate: { @@ -2523,16 +2521,17 @@ export const rotate: { * * **Example** (Checking with custom equality) * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * * const containsNumber = Array.containsWith((a: number, b: number) => a === b) - * console.log(pipe([1, 2, 3, 4], containsNumber(3))) // true + * + * pipe([1, 2, 3, 4], containsNumber(3)) // => true * ``` * * @see {@link contains} for the `Equal.equivalence()` variant * - * @category elements + * @category predicates * @since 2.0.0 */ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { @@ -2559,15 +2558,15 @@ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { * * **Example** (Checking membership) * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * - * console.log(pipe(["a", "b", "c", "d"], Array.contains("c"))) // true + * pipe(["a", "b", "c", "d"], Array.contains("c")) // => true * ``` * * @see {@link containsWith} — use custom equality * - * @category elements + * @category predicates * @since 2.0.0 */ export const contains: { @@ -2591,20 +2590,16 @@ export const contains: { * * **Example** (Chopping an array) * - * ```ts + * ```ts import.meta.vitest * 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] + * Array.chop([1, 2, 3, 4, 5], (as): [number, Array] => [as[0] * 2, as.slice(1)]) // => [2, 4, 6, 8, 10] * ``` * * @see {@link chunksOf} — split into fixed-size chunks * @see {@link splitAt} — split at an index * - * @category elements + * @category splitting * @since 2.0.0 */ export const chop: { @@ -2652,10 +2647,10 @@ export const chop: { * * **Example** (Splitting at an index) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.splitAt([1, 2, 3, 4, 5], 3)) // [[1, 2, 3], [4, 5]] + * Array.splitAt([1, 2, 3, 4, 5], 3) // => [[1, 2, 3], [4, 5]] * ``` * * @see {@link splitAtNonEmpty} — for non-empty arrays @@ -2690,11 +2685,10 @@ export const splitAt: { * * **Example** (Splitting a non-empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.splitAtNonEmpty(["a", "b", "c", "d", "e"], 3)) - * // [["a", "b", "c"], ["d", "e"]] + * Array.splitAtNonEmpty(["a", "b", "c", "d", "e"], 3) // => [["a", "b", "c"], ["d", "e"]] * ``` * * @see {@link splitAt} — for possibly-empty arrays @@ -2725,10 +2719,10 @@ export const splitAtNonEmpty: { * * **Example** (Splitting into groups) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.split([1, 2, 3, 4, 5, 6, 7, 8], 3)) // [[1, 2, 3], [4, 5, 6], [7, 8]] + * Array.split([1, 2, 3, 4, 5, 6, 7, 8], 3) // => [[1, 2, 3], [4, 5, 6], [7, 8]] * ``` * * @see {@link chunksOf} — split into fixed-size chunks @@ -2755,10 +2749,10 @@ export const split: { * * **Example** (Splitting at a condition) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.splitWhere([1, 2, 3, 4, 5], (n) => n > 3)) // [[1, 2, 3], [4, 5]] + * Array.splitWhere([1, 2, 3, 4, 5], (n) => n > 3) // => [[1, 2, 3], [4, 5]] * ``` * * @see {@link span} — splits at the first element that fails the predicate @@ -2793,18 +2787,19 @@ export const splitWhere: { * * **Example** (Copying an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * * const original = [1, 2, 3] * const copied = Array.copy(original) - * console.log(copied) // [1, 2, 3] - * console.log(original === copied) // false + * + * copied // => [1, 2, 3] + * original === copied // => false * ``` * * @see {@link fromIterable} — returns the same reference for arrays * - * @category elements + * @category transforming * @since 2.0.0 */ export const copy: { @@ -2826,16 +2821,16 @@ export const copy: { * * **Example** (Padding an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.pad([1, 2, 3], 6, 0)) // [1, 2, 3, 0, 0, 0] + * Array.pad([1, 2, 3], 6, 0) // => [1, 2, 3, 0, 0, 0] * ``` * * @see {@link take} — truncate without padding * @see {@link replicate} — create an array of a single repeated value * - * @category elements + * @category transforming * @since 3.8.4 */ export const pad: { @@ -2872,10 +2867,10 @@ export const pad: { * * **Example** (Chunking an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.chunksOf([1, 2, 3, 4, 5], 2)) // [[1, 2], [3, 4], [5]] + * Array.chunksOf([1, 2, 3, 4, 5], 2) // => [[1, 2], [3, 4], [5]] * ``` * * @see {@link split} — split into a given number of groups @@ -2914,11 +2909,13 @@ export const chunksOf: { * * **Example** (Creating sliding windows) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.window([1, 2, 3, 4, 5], 3)) // [[1, 2, 3], [2, 3, 4], [3, 4, 5]] - * console.log(Array.window([1, 2, 3, 4, 5], 6)) // [] + * const values = [1, 2, 3, 4, 5] + * + * Array.window(values, 3) // => [[1, 2, 3], [2, 3, 4], [3, 4, 5]] + * Array.window(values, 6) // => [] * ``` * * @see {@link chunksOf} — non-overlapping chunks @@ -2955,11 +2952,13 @@ export const window: { * * **Example** (Grouping consecutive equal elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.groupWith(["a", "a", "b", "b", "b", "c", "a"], (x, y) => x === y)) - * // [["a", "a"], ["b", "b", "b"], ["c"], ["a"]] + * Array.groupWith( + * ["a", "a", "b", "b", "b", "c", "a"], + * (x, y) => x === y + * ) // => [["a", "a"], ["b", "b", "b"], ["c"], ["a"]] * ``` * * @see {@link group} for grouping adjacent elements with `Equal.equivalence()` @@ -3004,10 +3003,10 @@ export const groupWith: { * * **Example** (Grouping adjacent equal elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.group([1, 1, 2, 2, 2, 3, 1])) // [[1, 1], [2, 2, 2], [3], [1]] + * Array.group([1, 1, 2, 2, 2, 3, 1]) // => [[1, 1], [2, 2, 2], [3], [1]] * ``` * * @see {@link groupWith} — use custom equality @@ -3035,7 +3034,7 @@ export const group: (self: NonEmptyReadonlyArray) => NonEmptyArray(self: NonEmptyReadonlyArray) => NonEmptyArray person.group) - * console.log(result) - * // { A: [{ name: "Alice", group: "A" }, { name: "Charlie", group: "A" }], B: [{ name: "Bob", group: "B" }] } + * Object.keys(Array.groupBy(people, (person) => person.group)).join(",") // => "A,B" * ``` * * @see {@link group} — group adjacent equal elements @@ -3073,12 +3070,52 @@ export const groupBy: { if (Object.hasOwn(out, k)) { out[k].push(a) } else { - out[k] = [a] + InternalRecord.assignProperty(out, k, [a]) } } return out }) +type HashBuckets = Map> + +const hashBucketsAdd = (buckets: HashBuckets, value: unknown): boolean => { + const hash = Hash.hash(value) + const bucket = buckets.get(hash) + if (bucket === undefined) { + buckets.set(hash, [value]) + return true + } + // Hash collisions still require an Effect equality check. + for (const previous of bucket) { + if (Equal.equals(previous, value)) { + return false + } + } + bucket.push(value) + return true +} + +const makeHashBuckets = (values: Iterable): HashBuckets => { + const buckets: HashBuckets = new Map() + for (const value of values) { + hashBucketsAdd(buckets, value) + } + return buckets +} + +const hashBucketsHas = (buckets: HashBuckets, value: unknown): boolean => { + const bucket = buckets.get(Hash.hash(value)) + if (bucket === undefined) { + return false + } + for (const candidate of bucket) { + if (Equal.equals(candidate, value)) { + return true + } + } + return false +} + /** * Computes the union of two arrays using a custom equivalence, removing * duplicates. @@ -3090,17 +3127,17 @@ export const groupBy: { * * **Example** (Computing unions with custom equality) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.unionWith([1, 2], [2, 3], (a, b) => a === b)) // [1, 2, 3] + * Array.unionWith([1, 2], [2, 3], (a, b) => a === b) // => [1, 2, 3] * ``` * * @see {@link union} for the `Equal.equivalence()` variant * @see {@link intersectionWith} for keeping elements present in both arrays * @see {@link differenceWith} for keeping elements present only in the first array * - * @category elements + * @category set operations * @since 2.0.0 */ export const unionWith: { @@ -3138,17 +3175,17 @@ export const unionWith: { * * **Example** (Computing array unions) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.union([1, 2], [2, 3])) // [1, 2, 3] + * Array.union([1, 2], [2, 3]) // => [1, 2, 3] * ``` * * @see {@link unionWith} — use custom equality * @see {@link intersection} — elements in both arrays * @see {@link difference} — elements only in the first array * - * @category elements + * @category set operations * @since 2.0.0 */ export const union: { @@ -3162,7 +3199,14 @@ export const union: { (self: Iterable, that: Iterable): Array } = dual( 2, - (self: Iterable, that: Iterable): Array => unionWith(self, that, Equal.asEquivalence()) + (self: Iterable, that: Iterable): Array => { + const a = fromIterable(self) + const b = fromIterable(that) + if (isReadonlyArrayNonEmpty(a)) { + return isReadonlyArrayNonEmpty(b) ? dedupe(appendAll(a, b)) : a + } + return b + } ) /** @@ -3176,20 +3220,21 @@ export const union: { * * **Example** (Computing intersections with custom equality) * - * ```ts + * ```ts import.meta.vitest * 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 - * console.log(Array.intersectionWith(isEquivalent)(array2)(array1)) // [{ id: 1 }, { id: 3 }] + * + * Array.intersectionWith(isEquivalent)(array2)(array1) // => [{ id: 1 }, { id: 3 }] * ``` * * @see {@link intersection} for the `Equal.equivalence()` variant * @see {@link unionWith} for keeping values from either array with custom equality * @see {@link differenceWith} for keeping values only from the first array with custom equality * - * @category elements + * @category set operations * @since 2.0.0 */ export const intersectionWith = (isEquivalent: (self: A, that: A) => boolean): { @@ -3217,23 +3262,31 @@ export const intersectionWith = (isEquivalent: (self: A, that: A) => boolean) * * **Example** (Computing array intersections) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.intersection([1, 2, 3], [3, 4, 1])) // [1, 3] + * Array.intersection([1, 2, 3], [3, 4, 1]) // => [1, 3] * ``` * * @see {@link intersectionWith} — use custom equality * @see {@link union} — elements in either array * @see {@link difference} — elements only in the first array * - * @category elements + * @category set operations * @since 2.0.0 */ export const intersection: { (that: Iterable): (self: Iterable) => Array (self: Iterable, that: Iterable): Array -} = intersectionWith(Equal.asEquivalence()) +} = dual(2, (self: Iterable, that: Iterable): Array => { + const thatArray = fromIterable(that) + const selfArray = fromIterable(self) + if (selfArray.length === 0 || thatArray.length === 0) { + return [] + } + const buckets = makeHashBuckets(thatArray) + return selfArray.filter((value): value is A & B => hashBucketsHas(buckets, value)) +}) /** * Computes elements in the first array that are not in the second, using a @@ -3246,18 +3299,17 @@ export const intersection: { * * **Example** (Computing differences with custom equality) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const diff = Array.differenceWith((a, b) => a === b)([1, 2, 3], [2, 3, 4]) - * console.log(diff) // [1] + * Array.differenceWith((a, b) => a === b)([1, 2, 3], [2, 3, 4]) // => [1] * ``` * * @see {@link difference} for the `Equal.equivalence()` variant * @see {@link unionWith} for keeping values from either array with custom equality * @see {@link intersectionWith} for keeping values present in both arrays with custom equality * - * @category elements + * @category set operations * @since 2.0.0 */ export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): { @@ -3285,23 +3337,34 @@ export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): * * **Example** (Computing array differences) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.difference([1, 2, 3], [2, 3, 4])) // [1] + * Array.difference([1, 2, 3], [2, 3, 4]) // => [1] * ``` * * @see {@link differenceWith} — use custom equality * @see {@link union} — elements in either array * @see {@link intersection} — elements in both arrays * - * @category elements + * @category set operations * @since 2.0.0 */ export const difference: { (that: Iterable): (self: Iterable) => Array (self: Iterable, that: Iterable): Array -} = differenceWith(Equal.asEquivalence()) +} = dual(2, (self: Iterable, that: Iterable): Array => { + const thatArray = fromIterable(that) + const selfArray = fromIterable(self) + if (selfArray.length === 0) { + return [] + } + if (thatArray.length === 0) { + return selfArray.filter(() => true) + } + const buckets = makeHashBuckets(thatArray) + return selfArray.filter((value) => !hashBucketsHas(buckets, value)) +}) /** * Creates an empty array. @@ -3312,11 +3375,10 @@ export const difference: { * * **Example** (Creating an empty array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.empty() - * console.log(result) // [] + * Array.empty() // => [] * ``` * * @see {@link of} — create a single-element array @@ -3332,10 +3394,10 @@ export const empty: () => Array = () => [] * * **Example** (Creating a single-element array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.of(1)) // [1] + * Array.of(1) // => [1] * ``` * * @see {@link make} — create from multiple values @@ -3358,14 +3420,14 @@ export declare namespace ReadonlyArray { * * **Example** (Inferring an element type) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * type StringArrayType = Array.ReadonlyArray.Infer> * // StringArrayType is string * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type Infer> = S extends ReadonlyArray ? A @@ -3377,14 +3439,14 @@ export declare namespace ReadonlyArray { * * **Example** (Preserving non-emptiness) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * type Result = Array.ReadonlyArray.With * // Result is NonEmptyArray * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type With, A> = S extends NonEmptyReadonlyArray ? NonEmptyArray @@ -3395,7 +3457,7 @@ export declare namespace ReadonlyArray { * * **Example** (Preserving non-emptiness from either input) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * type Result = Array.ReadonlyArray.OrNonEmpty< @@ -3406,7 +3468,7 @@ export declare namespace ReadonlyArray { * // Result is NonEmptyArray * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type OrNonEmpty< @@ -3422,7 +3484,7 @@ export declare namespace ReadonlyArray { * * **Example** (Preserving non-emptiness from both inputs) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * type Result = Array.ReadonlyArray.AndNonEmpty< @@ -3433,7 +3495,7 @@ export declare namespace ReadonlyArray { * // Result is NonEmptyArray * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type AndNonEmpty< @@ -3449,7 +3511,7 @@ export declare namespace ReadonlyArray { * * **Example** (Flattening nested array types) * - * ```ts + * ```ts import.meta.vitest * import type { Array } from "effect" * * type Nested = ReadonlyArray> @@ -3457,7 +3519,7 @@ export declare namespace ReadonlyArray { * // Flattened is Array * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type Flatten>> = T extends @@ -3479,10 +3541,10 @@ export declare namespace ReadonlyArray { * * **Example** (Doubling values) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.map([1, 2, 3], (x) => x * 2)) // [2, 4, 6] + * Array.map([1, 2, 3], (x) => x * 2) // => [2, 4, 6] * ``` * * @see {@link flatMap} — map and flatten @@ -3512,10 +3574,10 @@ export const map: { * * **Example** (Flat mapping an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.flatMap([1, 2, 3], (x) => [x, x * 2])) // [1, 2, 2, 4, 3, 6] + * Array.flatMap([1, 2, 3], (x) => [x, x * 2]) // => [1, 2, 2, 4, 3, 6] * ``` * * @see {@link map} — transform without flattening @@ -3557,10 +3619,10 @@ export const flatMap: { * * **Example** (Flattening nested arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.flatten([[1, 2], [], [3, 4], [], [5, 6]])) // [1, 2, 3, 4, 5, 6] + * Array.flatten([[1, 2], [], [3, 4], [], [5, 6]]) // => [1, 2, 3, 4, 5, 6] * ``` * * @see {@link flatMap} — map then flatten in one step @@ -3581,10 +3643,10 @@ export const flatten: >>(self: * * **Example** (Extracting Some values) * - * ```ts + * ```ts import.meta.vitest * import { Array, Option } from "effect" * - * console.log(Array.getSomes([Option.some(1), Option.none(), Option.some(2)])) // [1, 2] + * Array.getSomes([Option.some(1), Option.none(), Option.some(2)]) // => [1, 2] * ``` * * @see {@link fromOption} — convert a single Option @@ -3617,11 +3679,10 @@ export const getSomes: >, X = any>( * * **Example** (Extracting failures) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * - * console.log(Array.getFailures([Result.succeed(1), Result.fail("err"), Result.succeed(2)])) - * // ["err"] + * Array.getFailures([Result.succeed(1), Result.fail("err"), Result.succeed(2)]) // => ["err"] * ``` * * @see {@link getSuccesses} — extract success values @@ -3654,11 +3715,10 @@ export const getFailures = >>( * * **Example** (Extracting successes) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * - * console.log(Array.getSuccesses([Result.succeed(1), Result.fail("err"), Result.succeed(2)])) - * // [1, 2] + * Array.getSuccesses([Result.succeed(1), Result.fail("err"), Result.succeed(2)]) // => [1, 2] * ``` * * @see {@link getFailures} — extract failure values @@ -3694,11 +3754,10 @@ export const getSuccesses = >>( * * **Example** (Filtering and transforming) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * - * console.log(Array.filterMap([1, 2, 3, 4], (n) => n % 2 === 0 ? Result.succeed(n * 10) : Result.failVoid)) - * // [20, 40] + * Array.filterMap([1, 2, 3, 4], (n) => n % 2 === 0 ? Result.succeed(n * 10) : Result.failVoid) // => [20, 40] * ``` * * @see {@link filter} — keep original elements matching a predicate @@ -3737,10 +3796,10 @@ export const filterMap: { * * **Example** (Filtering even numbers) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.filter([1, 2, 3, 4], (x) => x % 2 === 0)) // [2, 4] + * Array.filter([1, 2, 3, 4], (x) => x % 2 === 0) // => [2, 4] * ``` * * @see {@link partition} — split into matching and non-matching @@ -3782,13 +3841,12 @@ export const filter: { * * **Example** (Partitioning with a filter) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * - * console.log(Array.partition([1, -2, 3], (n, i) => + * Array.partition([1, -2, 3], (n, i) => * n > 0 ? Result.succeed(n + i) : Result.fail(`negative:${n}`) - * )) - * // [["negative:-2"], [1, 5]] + * ) // => [["negative:-2"], [1, 5]] * ``` * * @see {@link filter} — keep only matching elements @@ -3841,14 +3899,10 @@ export const partition: { * * **Example** (Separating Results) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * - * const [failures, successes] = Array.separate([ - * Result.succeed(1), Result.fail("error"), Result.succeed(2) - * ]) - * console.log(failures) // ["error"] - * console.log(successes) // [1, 2] + * Array.separate([Result.succeed(1), Result.fail("error"), Result.succeed(2)]) // => [["error"], [1, 2]] * ``` * * @see {@link getFailures} — extract only failures @@ -3878,10 +3932,10 @@ export const separate: >>( * * **Example** (Summing an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.reduce([1, 2, 3], 0, (acc, n) => acc + n)) // 6 + * Array.reduce([1, 2, 3], 0, (acc, n) => acc + n) // => 6 * ``` * * @see {@link reduceRight} — fold from right to left @@ -3912,10 +3966,10 @@ export const reduce: { * * **Example** (Folding from right to left) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.reduceRight([1, 2, 3], 0, (acc, n) => acc + n)) // 6 + * Array.reduceRight([1, 2, 3], 0, (acc, n) => acc + n) // => 6 * ``` * * @see {@link reduce} — fold from left to right @@ -3939,13 +3993,13 @@ export const reduceRight: { * * **Example** (Wrapping values conditionally) * - * ```ts + * ```ts import.meta.vitest * 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] + * const fromEven = Array.liftPredicate((n: number) => n % 2 === 0) + * + * fromEven(1) // => [] + * fromEven(2) // => [2] * ``` * * @see {@link liftOption} — lift an Option-returning function @@ -3969,15 +4023,16 @@ export const liftPredicate: { // Note: I intentionally avoid using the NoInfer p * * **Example** (Lifting an Option function) * - * ```ts + * ```ts import.meta.vitest * import { Array, Option } from "effect" * * const parseNumber = Array.liftOption((s: string) => { * const n = Number(s) * return isNaN(n) ? Option.none() : Option.some(n) * }) - * console.log(parseNumber("123")) // [123] - * console.log(parseNumber("abc")) // [] + * + * parseNumber("123") // => [123] + * parseNumber("abc") // => [] * ``` * * @see {@link liftPredicate} — lift a boolean predicate @@ -4001,12 +4056,12 @@ export const liftOption = , B>( * * **Example** (Converting nullable values to an array) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.fromNullishOr(1)) // [1] - * console.log(Array.fromNullishOr(null)) // [] - * console.log(Array.fromNullishOr(undefined)) // [] + * Array.fromNullishOr(1) // => [1] + * Array.fromNullishOr(null) // => [] + * Array.fromNullishOr(undefined) // => [] * ``` * * @see {@link liftNullishOr} — lift a nullable-returning function @@ -4023,15 +4078,16 @@ export const fromNullishOr = (a: A): Array> => a == null ? emp * * **Example** (Lifting a nullable function) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * * const parseNumber = Array.liftNullishOr((s: string) => { * const n = Number(s) * return isNaN(n) ? null : n * }) - * console.log(parseNumber("123")) // [123] - * console.log(parseNumber("abc")) // [] + * + * parseNumber("123") // => [123] + * parseNumber("abc") // => [] * ``` * * @see {@link fromNullishOr} — convert a single nullable value @@ -4056,11 +4112,10 @@ export const liftNullishOr = , B>( * * **Example** (Flat mapping with nullable values) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.flatMapNullishOr([1, 2, 3], (n) => (n % 2 === 0 ? null : n))) - * // [1, 3] + * Array.flatMapNullishOr([1, 2, 3], (n) => (n % 2 === 0 ? null : n)) // => [1, 3] * ``` * * @see {@link flatMap} for mapping each element to an array and flattening @@ -4088,7 +4143,7 @@ export const flatMapNullishOr: { * * **Example** (Lifting a Result function) * - * ```ts + * ```ts import.meta.vitest * import { Array, Result } from "effect" * * const parseNumber = (s: string): Result.Result => @@ -4097,8 +4152,9 @@ export const flatMapNullishOr: { * : Result.succeed(Number(s)) * * const liftedParseNumber = Array.liftResult(parseNumber) - * console.log(liftedParseNumber("42")) // [42] - * console.log(liftedParseNumber("not a number")) // [] + * + * liftedParseNumber("42") // => [42] + * liftedParseNumber("not a number") // => [] * ``` * * @see {@link liftOption} — lift an Option-returning function @@ -4126,16 +4182,16 @@ export const liftResult = , E, B>( * * **Example** (Testing all elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.every([2, 4, 6], (x) => x % 2 === 0)) // true - * console.log(Array.every([2, 3, 6], (x) => x % 2 === 0)) // false + * Array.every([2, 4, 6], (x) => x % 2 === 0) // => true + * Array.every([2, 3, 6], (x) => x % 2 === 0) // => false * ``` * * @see {@link some} — test if any element matches * - * @category elements + * @category guards * @since 2.0.0 */ export const every: { @@ -4157,17 +4213,17 @@ export const every: { * * **Example** (Testing for any match) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.some([1, 3, 4], (x) => x % 2 === 0)) // true - * console.log(Array.some([1, 3, 5], (x) => x % 2 === 0)) // false + * Array.some([1, 3, 4], (x) => x % 2 === 0) // => true + * Array.some([1, 3, 5], (x) => x % 2 === 0) // => false * ``` * * @see {@link every} — test if all elements match * @see {@link contains} — test for a specific value * - * @category elements + * @category guards * @since 2.0.0 */ export const some: { @@ -4196,10 +4252,10 @@ export const some: { * * **Example** (Computing suffix lengths) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.extend([1, 2, 3], (as) => as.length)) // [3, 2, 1] + * Array.extend([1, 2, 3], (as) => as.length) // => [3, 2, 1] * ``` * * @see {@link scan} for keeping intermediate accumulator values during a fold @@ -4221,16 +4277,16 @@ export const extend: { * * **Example** (Finding the minimum) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order } from "effect" * - * console.log(Array.min([3, 1, 2], Order.Number)) // 1 + * Array.min([3, 1, 2], Order.Number) // => 1 * ``` * * @see {@link max} — find the maximum * @see {@link sort} — sort the entire array * - * @category elements + * @category getters * @since 2.0.0 */ export const min: { @@ -4244,16 +4300,16 @@ export const min: { * * **Example** (Finding the maximum) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order } from "effect" * - * console.log(Array.max([3, 1, 2], Order.Number)) // 3 + * Array.max([3, 1, 2], Order.Number) // => 3 * ``` * * @see {@link min} — find the minimum * @see {@link sort} — sort the entire array * - * @category elements + * @category getters * @since 2.0.0 */ export const max: { @@ -4268,11 +4324,10 @@ export const max: { * * **Example** (Generating a sequence) * - * ```ts + * ```ts import.meta.vitest * import { Array, Option } from "effect" * - * console.log(Array.unfold(1, (n) => n <= 5 ? Option.some([n, n + 1]) : Option.none())) - * // [1, 2, 3, 4, 5] + * Array.unfold(1, (n) => n <= 5 ? Option.some([n, n + 1]) : Option.none()) // => [1, 2, 3, 4, 5] * ``` * * @see {@link makeBy} — generate from index @@ -4303,11 +4358,12 @@ export const unfold = (b: B, f: (b: B) => Option.Option): * * **Example** (Comparing arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array, Order } from "effect" * * const arrayOrder = Array.makeOrder(Order.Number) - * console.log(arrayOrder([1, 2], [1, 3])) // -1 + * + * arrayOrder([1, 2], [1, 3]) // => -1 * ``` * * @see {@link makeEquivalence} — create an equivalence for arrays @@ -4324,11 +4380,12 @@ export const makeOrder: (O: Order.Order) => Order.Order> * * **Example** (Comparing arrays for equality) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * * const eq = Array.makeEquivalence((a, b) => a === b) - * console.log(eq([1, 2, 3], [1, 2, 3])) // true + * + * eq([1, 2, 3], [1, 2, 3]) // => true * ``` * * @see {@link makeOrder} — create an ordering for arrays @@ -4350,15 +4407,18 @@ export const makeEquivalence: ( * * **Example** (Iterating with side-effects) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * Array.forEach([1, 2, 3], (n) => console.log(n)) // 1, 2, 3 + * const visited: Array = [] + * Array.forEach([1, 2, 3], (n) => visited.push(n)) + * + * visited // => [1, 2, 3] * ``` * * @see {@link map} for transforming each element into a new array * - * @category elements + * @category traversing * @since 2.0.0 */ export const forEach: { @@ -4377,16 +4437,16 @@ export const forEach: { * * **Example** (Deduplicating with custom equality) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dedupeWith([1, 2, 2, 3, 3, 3], (a, b) => a === b)) // [1, 2, 3] + * Array.dedupeWith([1, 2, 2, 3, 3, 3], (a, b) => a === b) // => [1, 2, 3] * ``` * * @see {@link dedupe} — uses default equality * @see {@link dedupeAdjacentWith} — only dedupes consecutive elements * - * @category elements + * @category deduplication * @since 2.0.0 */ export const dedupeWith: { @@ -4424,22 +4484,34 @@ export const dedupeWith: { * * **Example** (Removing duplicates) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dedupe([1, 2, 1, 3, 2, 4])) // [1, 2, 3, 4] + * Array.dedupe([1, 2, 1, 3, 2, 4]) // => [1, 2, 3, 4] * ``` * * @see {@link dedupeWith} — use custom equality * @see {@link dedupeAdjacent} — only dedupes consecutive elements * - * @category elements + * @category deduplication * @since 2.0.0 */ export const dedupe = >( self: S -): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => - dedupeWith(self, Equal.asEquivalence()) as any +): S extends NonEmptyReadonlyArray ? NonEmptyArray : S extends Iterable ? Array : never => { + const input = fromIterable(self) + if (input.length < 2) { + return [...input] as any + } + const buckets: HashBuckets = new Map() + const out: Array = [] + for (const value of input) { + if (hashBucketsAdd(buckets, value)) { + out.push(value) + } + } + return out as any +} /** * Removes consecutive duplicate elements using a custom equivalence. @@ -4456,17 +4528,16 @@ export const dedupe = >( * * **Example** (Deduplicating adjacent elements) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dedupeAdjacentWith([1, 1, 2, 2, 3, 3], (a, b) => a === b)) - * // [1, 2, 3] + * Array.dedupeAdjacentWith([1, 1, 2, 2, 3, 3], (a, b) => a === b) // => [1, 2, 3] * ``` * * @see {@link dedupeAdjacent} — uses default equality * @see {@link dedupeWith} — dedupes all duplicates, not just adjacent * - * @category elements + * @category deduplication * @since 2.0.0 */ export const dedupeAdjacentWith: { @@ -4494,16 +4565,16 @@ export const dedupeAdjacentWith: { * * **Example** (Removing adjacent duplicates) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.dedupeAdjacent([1, 1, 2, 2, 3, 3])) // [1, 2, 3] + * Array.dedupeAdjacent([1, 1, 2, 2, 3, 3]) // => [1, 2, 3] * ``` * * @see {@link dedupeAdjacentWith} — use custom equality * @see {@link dedupe} — remove all duplicates * - * @category elements + * @category deduplication * @since 2.0.0 */ export const dedupeAdjacent: (self: Iterable) => Array = dedupeAdjacentWith(Equal.asEquivalence()) @@ -4513,10 +4584,10 @@ export const dedupeAdjacent: (self: Iterable) => Array = dedupeAdjacent * * **Example** (Joining strings) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * console.log(Array.join(["a", "b", "c"], "-")) // "a-b-c" + * Array.join(["a", "b", "c"], "-") // => "a-b-c" * ``` * * @see {@link intersperse} — insert separator elements without joining @@ -4546,11 +4617,10 @@ export const join: { * * **Example** (Running sum alongside mapped values) * - * ```ts + * ```ts import.meta.vitest * 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]] + * Array.mapAccum([1, 2, 3], 0, (acc, n) => [acc + n, acc + n]) // => [6, [1, 3, 6]] * ``` * * @see {@link scan} — when you only need the accumulated results (not the final state) @@ -4601,16 +4671,15 @@ export const mapAccum: { * * **Example** (Combining numbers and letters) * - * ```ts + * ```ts import.meta.vitest * 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"] + * Array.cartesianWith([1, 2], ["a", "b"], (a, b) => `${a}-${b}`) // => ["1-a", "1-b", "2-a", "2-b"] * ``` * * @see {@link cartesian} for returning tuples instead of applying a combiner * - * @category elements + * @category combining * @since 2.0.0 */ export const cartesianWith: { @@ -4636,16 +4705,15 @@ export const cartesianWith: { * * **Example** (Generating all pairs from two arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.cartesian([1, 2], ["a", "b"]) - * console.log(result) // [[1, "a"], [1, "b"], [2, "a"], [2, "b"]] + * Array.cartesian([1, 2], ["a", "b"]) // => [[1, "a"], [1, "b"], [2, "a"], [2, "b"]] * ``` * * @see {@link cartesianWith} — apply a combiner to each pair * - * @category elements + * @category combining * @since 2.0.0 */ export const cartesian: { @@ -4676,24 +4744,23 @@ export const cartesian: { * * **Example** (Building array comprehensions with do notation) * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * - * const result = pipe( + * pipe( * Array.Do, * Array.bind("x", () => [1, 3, 5]), * Array.bind("y", () => [2, 4, 6]), * Array.filter(({ x, y }) => x < y), * Array.map(({ x, y }) => [x, y] as const) - * ) - * console.log(result) // [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] + * ) // => [[1, 2], [1, 4], [1, 6], [3, 4], [3, 6], [5, 6]] * ``` * * @see {@link bind} — introduce an array variable into the scope * @see {@link bindTo} — start a pipeline by naming the first array * @see {@link let_ let} — introduce a plain computed value * - * @category do notation + * @category constructors * @since 3.2.0 */ export const Do: ReadonlyArray<{}> = of({}) @@ -4714,23 +4781,21 @@ export const Do: ReadonlyArray<{}> = of({}) * * **Example** (Binding two arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * - * const result = pipe( + * pipe( * Array.Do, * Array.bind("x", () => [1, 2]), * Array.bind("y", () => ["a", "b"]) - * ) - * console.log(result) - * // [{ x: 1, y: "a" }, { x: 1, y: "b" }, { x: 2, y: "a" }, { x: 2, y: "b" }] + * ) // => [{ x: 1, y: "a" }, { x: 1, y: "b" }, { x: 2, y: "a" }, { x: 2, y: "b" }] * ``` * * @see {@link Do} — start a do-notation pipeline * @see {@link bindTo} — name the first array in a pipeline * @see {@link let_ let} — add a plain computed value * - * @category do notation + * @category sequencing * @since 3.2.0 */ export const bind: { @@ -4762,20 +4827,16 @@ export const bind: { * * **Example** (Naming an existing array) * - * ```ts - * import { Array, pipe } from "effect" + * ```ts import.meta.vitest + * import { Array } from "effect" * - * const result = pipe( - * [1, 2, 3], - * Array.bindTo("x") - * ) - * console.log(result) // [{ x: 1 }, { x: 2 }, { x: 3 }] + * Array.bindTo([1, 2, 3], "x") // => [{ x: 1 }, { x: 2 }, { x: 3 }] * ``` * * @see {@link Do} — start with an empty scope * @see {@link bind} — add another array variable to the scope * - * @category do notation + * @category mapping * @since 3.2.0 */ export const bindTo: { @@ -4812,22 +4873,20 @@ export { * * **Example** (Adding a computed value) * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * - * const result = pipe( + * pipe( * Array.Do, * Array.bind("x", () => [1, 2, 3]), * Array.let("doubled", ({ x }) => x * 2) - * ) - * console.log(result) - * // [{ x: 1, doubled: 2 }, { x: 2, doubled: 4 }, { x: 3, doubled: 6 }] + * ) // => [{ x: 1, doubled: 2 }, { x: 2, doubled: 4 }, { x: 3, doubled: 6 }] * ``` * * @see {@link Do} — start a do-notation pipeline * @see {@link bind} — introduce an array variable (produces cartesian product) * - * @category do notation + * @category mapping * @since 3.2.0 */ let_ as let @@ -4874,11 +4933,10 @@ export function makeReducerConcat(): Reducer.Reducer> { * * **Example** (Counting even numbers) * - * ```ts + * ```ts import.meta.vitest * import { Array } from "effect" * - * const result = Array.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) - * console.log(result) // 2 + * Array.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) // => 2 * ``` * * @see {@link filter} — when you need the matching elements, not just the count diff --git a/.context/effect/packages/effect/src/BigDecimal.ts b/.context/effect/packages/effect/src/BigDecimal.ts index 8ca0678d9..865db4449 100644 --- a/.context/effect/packages/effect/src/BigDecimal.ts +++ b/.context/effect/packages/effect/src/BigDecimal.ts @@ -34,13 +34,13 @@ const TypeId = "~effect/BigDecimal" * * **Example** (Inspecting BigDecimal storage) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const d = BigDecimal.fromStringUnsafe("123.45") * - * console.log(d.value) // 12345n - * console.log(d.scale) // 2 + * d.value // => 12345n + * d.scale // => 2 * ``` * * @category models @@ -90,13 +90,14 @@ const BigDecimalProto: Omit = { * * **Example** (Checking BigDecimal values) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const decimal = BigDecimal.fromNumber(123.45) - * console.log(BigDecimal.isBigDecimal(decimal)) // true - * console.log(BigDecimal.isBigDecimal(123.45)) // false - * console.log(BigDecimal.isBigDecimal("123.45")) // false + * BigDecimal.isBigDecimal(decimal) // => false + * BigDecimal.isBigDecimal(BigDecimal.fromStringUnsafe("123.45")) // => true + * BigDecimal.isBigDecimal(123.45) // => false + * BigDecimal.isBigDecimal("123.45") // => false * ``` * * @category guards @@ -114,16 +115,16 @@ export const isBigDecimal = (u: unknown): u is BigDecimal => hasProperty(u, Type * * **Example** (Creating decimals from bigint and scale) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * // Create 123.45 (12345 with scale 2) * const decimal = BigDecimal.make(12345n, 2) - * console.log(BigDecimal.format(decimal)) // "123.45" + * decimal // => BigDecimal.fromStringUnsafe("123.45") * * // Create 42 (42 with scale 0) * const integer = BigDecimal.make(42n, 0) - * console.log(BigDecimal.format(integer)) // "42" + * integer // => BigDecimal.fromBigInt(42n) * ``` * * @see {@link fromBigInt} for constructing an integer decimal from a `bigint` @@ -173,18 +174,14 @@ const one = makeNormalizedUnsafe(bigint1, 0) * * **Example** (Normalizing trailing zeros) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.normalize(BigDecimal.fromStringUnsafe("123.00000")), - * BigDecimal.normalize(BigDecimal.make(123n, 0)) - * ) - * assert.deepStrictEqual( - * BigDecimal.normalize(BigDecimal.fromStringUnsafe("12300000")), - * BigDecimal.normalize(BigDecimal.make(123n, -5)) - * ) + * + * const decimal = BigDecimal.normalize(BigDecimal.fromStringUnsafe("123.00000")) + * const decimalStorage = [decimal.value, decimal.scale] // => [123n, 0] + * + * const largeDecimal = BigDecimal.normalize(BigDecimal.fromStringUnsafe("12300000")) + * const largeDecimalStorage = [largeDecimal.value, largeDecimal.scale] // => [123n, -5] * ``` * * @see {@link format} for rendering normalized decimals as strings @@ -236,18 +233,18 @@ export const normalize = (self: BigDecimal): BigDecimal => { * * **Example** (Scaling decimal precision) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const decimal = BigDecimal.fromNumberUnsafe(123.45) * * // Increase scale (add more precision) * const scaled = BigDecimal.scale(decimal, 4) - * console.log(BigDecimal.format(scaled)) // "123.4500" + * const scaledStorage = [scaled.value, scaled.scale] // => [1234500n, 4] * - * // Decrease scale (reduce precision, rounds down) + * // Decrease scale (reduce precision, truncating toward zero) * const reduced = BigDecimal.scale(decimal, 1) - * console.log(BigDecimal.format(reduced)) // "123.4" + * reduced // => BigDecimal.fromStringUnsafe("123.4") * ``` * * @see {@link round} for changing scale with configurable rounding @@ -280,14 +277,13 @@ export const scale: { * * **Example** (Adding decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.sum(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * BigDecimal.fromStringUnsafe("5") - * ) + * const result = BigDecimal.sum( + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3") + * ) // => BigDecimal.fromBigInt(5n) * ``` * * @see {@link sumAll} for summing an iterable of `BigDecimal` values @@ -328,14 +324,14 @@ export const sum: { * * **Example** (Adding multiple decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.sumAll([BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4")]), - * BigDecimal.fromStringUnsafe("9") - * ) + * const result = BigDecimal.sumAll([ + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3"), + * BigDecimal.fromStringUnsafe("4") + * ]) // => BigDecimal.fromBigInt(9n) * ``` * * @see {@link sum} for adding two `BigDecimal` values @@ -360,14 +356,13 @@ export const sumAll = (collection: Iterable): BigDecimal => { * * **Example** (Multiplying decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.multiply(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * BigDecimal.fromStringUnsafe("6") - * ) + * const result = BigDecimal.multiply( + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3") + * ) // => BigDecimal.fromBigInt(6n) * ``` * * @see {@link multiplyAll} for multiplying an iterable of `BigDecimal` values @@ -395,14 +390,14 @@ export const multiply: { * * **Example** (Multiplying multiple decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.multiplyAll([BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("4")]), - * BigDecimal.fromStringUnsafe("24") - * ) + * const result = BigDecimal.multiplyAll([ + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3"), + * BigDecimal.fromStringUnsafe("4") + * ]) // => BigDecimal.fromBigInt(24n) * ``` * * @see {@link multiply} for multiplying two `BigDecimal` values @@ -430,14 +425,13 @@ export const multiplyAll = (collection: Iterable): BigDecimal => { * * **Example** (Subtracting decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.subtract(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * BigDecimal.fromStringUnsafe("-1") - * ) + * const result = BigDecimal.subtract( + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3") + * ) // => BigDecimal.fromBigInt(-1n) * ``` * * @category math @@ -550,33 +544,14 @@ export const roundTerminal = (n: bigint): bigint => { * * **Example** (Dividing decimals safely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal, Option } from "effect" * - * console.log( - * Option.getOrThrow( - * BigDecimal.divide( - * BigDecimal.fromStringUnsafe("6"), - * BigDecimal.fromStringUnsafe("3") - * ) - * ) - * ) // BigDecimal(2) - * console.log( - * Option.getOrThrow( - * BigDecimal.divide( - * BigDecimal.fromStringUnsafe("6"), - * BigDecimal.fromStringUnsafe("4") - * ) - * ) - * ) // BigDecimal(1.5) - * console.log( - * Option.isNone( - * BigDecimal.divide( - * BigDecimal.fromStringUnsafe("6"), - * BigDecimal.fromStringUnsafe("0") - * ) - * ) - * ) // true + * const six = BigDecimal.fromBigInt(6n) + * + * BigDecimal.divide(six, BigDecimal.fromBigInt(3n)) // => Option.some(BigDecimal.fromBigInt(2n)) + * BigDecimal.divide(six, BigDecimal.fromBigInt(4n)) // => Option.some(BigDecimal.fromStringUnsafe("1.5")) + * BigDecimal.divide(six, BigDecimal.fromBigInt(0n)) // => Option.none() * ``` * * @see {@link divideUnsafe} for division that throws when the divisor is zero @@ -624,11 +599,11 @@ export const divide: { * * **Example** (Dividing decimals unsafely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * - * console.log(BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("3"))) // BigDecimal(2) - * console.log(BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("4"))) // BigDecimal(1.5) + * BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("3")) // => BigDecimal.fromBigInt(2n) + * BigDecimal.divideUnsafe(BigDecimal.fromStringUnsafe("6"), BigDecimal.fromStringUnsafe("4")) // => BigDecimal.fromStringUnsafe("1.5") * ``` * * @see {@link divide} for division that returns `Option.none` when the divisor is zero @@ -665,16 +640,16 @@ export const divideUnsafe: { * * **Example** (Comparing decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const a = BigDecimal.fromNumberUnsafe(1.5) * const b = BigDecimal.fromNumberUnsafe(2.3) * const c = BigDecimal.fromNumberUnsafe(1.5) * - * console.log(BigDecimal.Order(a, b)) // -1 (a < b) - * console.log(BigDecimal.Order(b, a)) // 1 (b > a) - * console.log(BigDecimal.Order(a, c)) // 0 (a === c) + * BigDecimal.Order(a, b) // => -1 + * BigDecimal.Order(b, a) // => 1 + * BigDecimal.Order(a, c) // => 0 * ``` * * @category instances @@ -706,22 +681,16 @@ export const Order: order.Order = order.make((self, that) => { * * **Example** (Checking less-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * true - * ) - * assert.deepStrictEqual( - * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), - * false - * ) - * assert.deepStrictEqual( - * BigDecimal.isLessThan(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), - * false - * ) + * + * const two = BigDecimal.fromStringUnsafe("2") + * const three = BigDecimal.fromStringUnsafe("3") + * const four = BigDecimal.fromStringUnsafe("4") + * + * BigDecimal.isLessThan(two, three) // => true + * BigDecimal.isLessThan(three, three) // => false + * BigDecimal.isLessThan(four, three) // => false * ``` * * @category predicates @@ -741,22 +710,16 @@ export const isLessThan: { * * **Example** (Checking less-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * true - * ) - * assert.deepStrictEqual( - * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), - * true - * ) - * assert.deepStrictEqual( - * BigDecimal.isLessThanOrEqualTo(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), - * false - * ) + * + * const two = BigDecimal.fromStringUnsafe("2") + * const three = BigDecimal.fromStringUnsafe("3") + * const four = BigDecimal.fromStringUnsafe("4") + * + * BigDecimal.isLessThanOrEqualTo(two, three) // => true + * BigDecimal.isLessThanOrEqualTo(three, three) // => true + * BigDecimal.isLessThanOrEqualTo(four, three) // => false * ``` * * @category predicates @@ -776,22 +739,16 @@ export const isLessThanOrEqualTo: { * * **Example** (Checking greater-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * false - * ) - * assert.deepStrictEqual( - * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), - * false - * ) - * assert.deepStrictEqual( - * BigDecimal.isGreaterThan(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), - * true - * ) + * + * const two = BigDecimal.fromStringUnsafe("2") + * const three = BigDecimal.fromStringUnsafe("3") + * const four = BigDecimal.fromStringUnsafe("4") + * + * BigDecimal.isGreaterThan(two, three) // => false + * BigDecimal.isGreaterThan(three, three) // => false + * BigDecimal.isGreaterThan(four, three) // => true * ``` * * @category predicates @@ -811,22 +768,16 @@ export const isGreaterThan: { * * **Example** (Checking greater-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * false - * ) - * assert.deepStrictEqual( - * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("3")), - * true - * ) - * assert.deepStrictEqual( - * BigDecimal.isGreaterThanOrEqualTo(BigDecimal.fromStringUnsafe("4"), BigDecimal.fromStringUnsafe("3")), - * true - * ) + * + * const two = BigDecimal.fromStringUnsafe("2") + * const three = BigDecimal.fromStringUnsafe("3") + * const four = BigDecimal.fromStringUnsafe("4") + * + * BigDecimal.isGreaterThanOrEqualTo(two, three) // => false + * BigDecimal.isGreaterThanOrEqualTo(three, three) // => true + * BigDecimal.isGreaterThanOrEqualTo(four, three) // => true * ``` * * @category predicates @@ -846,18 +797,17 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking decimal ranges) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * * const between = BigDecimal.between({ * minimum: BigDecimal.fromStringUnsafe("1"), * maximum: BigDecimal.fromStringUnsafe("5") * }) * - * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("3")), true) - * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("0")), false) - * assert.deepStrictEqual(between(BigDecimal.fromStringUnsafe("6")), false) + * between(BigDecimal.fromStringUnsafe("3")) // => true + * between(BigDecimal.fromStringUnsafe("0")) // => false + * between(BigDecimal.fromStringUnsafe("6")) // => false * ``` * * @see {@link clamp} for forcing a `BigDecimal` into an inclusive range @@ -891,27 +841,17 @@ export const between: { * * **Example** (Clamping decimals to a range) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * * const clamp = BigDecimal.clamp({ * minimum: BigDecimal.fromStringUnsafe("1"), * maximum: BigDecimal.fromStringUnsafe("5") * }) * - * assert.deepStrictEqual( - * clamp(BigDecimal.fromStringUnsafe("3")), - * BigDecimal.fromStringUnsafe("3") - * ) - * assert.deepStrictEqual( - * clamp(BigDecimal.fromStringUnsafe("0")), - * BigDecimal.fromStringUnsafe("1") - * ) - * assert.deepStrictEqual( - * clamp(BigDecimal.fromStringUnsafe("6")), - * BigDecimal.fromStringUnsafe("5") - * ) + * clamp(BigDecimal.fromStringUnsafe("3")) // => BigDecimal.fromBigInt(3n) + * clamp(BigDecimal.fromStringUnsafe("0")) // => BigDecimal.fromBigInt(1n) + * clamp(BigDecimal.fromStringUnsafe("6")) // => BigDecimal.fromBigInt(5n) * ``` * * @see {@link between} for checking whether a `BigDecimal` is already inside a range @@ -939,14 +879,13 @@ export const clamp: { * * **Example** (Selecting the smaller decimal) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.min(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), - * BigDecimal.fromStringUnsafe("2") - * ) + * const result = BigDecimal.min( + * BigDecimal.fromStringUnsafe("2"), + * BigDecimal.fromStringUnsafe("3") + * ) // => BigDecimal.fromBigInt(2n) * ``` * * @see {@link max} for selecting the larger value @@ -968,14 +907,13 @@ export const min: { * * **Example** (Selecting the larger decimal) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.max(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("3")), + * const result = BigDecimal.max( + * BigDecimal.fromStringUnsafe("2"), * BigDecimal.fromStringUnsafe("3") - * ) + * ) // => BigDecimal.fromBigInt(3n) * ``` * * @see {@link min} for selecting the smaller value @@ -997,13 +935,12 @@ export const max: { * * **Example** (Reading decimal signs) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("-5")), -1) - * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("0")), 0) - * assert.deepStrictEqual(BigDecimal.sign(BigDecimal.fromStringUnsafe("5")), 1) + * BigDecimal.sign(BigDecimal.fromStringUnsafe("-5")) // => -1 + * BigDecimal.sign(BigDecimal.fromStringUnsafe("0")) // => 0 + * BigDecimal.sign(BigDecimal.fromStringUnsafe("5")) // => 1 * ``` * * @category math @@ -1020,13 +957,12 @@ export const sign = (n: BigDecimal): Ordering => n.value === bigint0 ? 0 : n.val * * **Example** (Calculating absolute values) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("-5")), BigDecimal.fromStringUnsafe("5")) - * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("0")), BigDecimal.fromStringUnsafe("0")) - * assert.deepStrictEqual(BigDecimal.abs(BigDecimal.fromStringUnsafe("5")), BigDecimal.fromStringUnsafe("5")) + * BigDecimal.abs(BigDecimal.fromStringUnsafe("-5")) // => BigDecimal.fromBigInt(5n) + * BigDecimal.abs(BigDecimal.fromStringUnsafe("0")) // => BigDecimal.fromBigInt(0n) + * BigDecimal.abs(BigDecimal.fromStringUnsafe("5")) // => BigDecimal.fromBigInt(5n) * ``` * * @category math @@ -1043,12 +979,11 @@ export const abs = (n: BigDecimal): BigDecimal => n.value < bigint0 ? make(-n.va * * **Example** (Negating decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.negate(BigDecimal.fromStringUnsafe("3")), BigDecimal.fromStringUnsafe("-3")) - * assert.deepStrictEqual(BigDecimal.negate(BigDecimal.fromStringUnsafe("-6")), BigDecimal.fromStringUnsafe("6")) + * BigDecimal.negate(BigDecimal.fromStringUnsafe("3")) // => BigDecimal.fromBigInt(-3n) + * BigDecimal.negate(BigDecimal.fromStringUnsafe("-6")) // => BigDecimal.fromBigInt(6n) * ``` * * @category math @@ -1071,31 +1006,15 @@ export const negate = (n: BigDecimal): BigDecimal => make(-n.value, n.scale) * * **Example** (Computing remainders safely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal, Option } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.remainder( - * BigDecimal.fromStringUnsafe("2"), - * BigDecimal.fromStringUnsafe("2") - * ), - * Option.some(BigDecimal.fromStringUnsafe("0")) - * ) - * assert.deepStrictEqual( - * BigDecimal.remainder( - * BigDecimal.fromStringUnsafe("3"), - * BigDecimal.fromStringUnsafe("2") - * ), - * Option.some(BigDecimal.fromStringUnsafe("1")) - * ) - * assert.deepStrictEqual( - * BigDecimal.remainder( - * BigDecimal.fromStringUnsafe("-4"), - * BigDecimal.fromStringUnsafe("2") - * ), - * Option.some(BigDecimal.fromStringUnsafe("0")) - * ) + * + * const two = BigDecimal.fromStringUnsafe("2") + * const three = BigDecimal.fromStringUnsafe("3") + * const zero = BigDecimal.fromStringUnsafe("0") + * + * BigDecimal.remainder(three, two) // => Option.some(BigDecimal.fromBigInt(1n)) + * BigDecimal.remainder(two, zero) // => Option.none() * ``` * * @see {@link remainderUnsafe} for remainder calculation that throws when the divisor is zero @@ -1131,22 +1050,13 @@ export const remainder: { * * **Example** (Computing remainders unsafely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("2"), BigDecimal.fromStringUnsafe("2")), - * BigDecimal.fromStringUnsafe("0") - * ) - * assert.deepStrictEqual( - * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("3"), BigDecimal.fromStringUnsafe("2")), - * BigDecimal.fromStringUnsafe("1") - * ) - * assert.deepStrictEqual( - * BigDecimal.remainderUnsafe(BigDecimal.fromStringUnsafe("-4"), BigDecimal.fromStringUnsafe("2")), - * BigDecimal.fromStringUnsafe("0") - * ) + * + * BigDecimal.remainderUnsafe( + * BigDecimal.fromStringUnsafe("3"), + * BigDecimal.fromStringUnsafe("2") + * ) // => BigDecimal.fromBigInt(1n) * ``` * * @see {@link remainder} for returning `Option.none` when the divisor is zero @@ -1176,15 +1086,15 @@ export const remainderUnsafe: { * * **Example** (Checking decimal equivalence) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const a = BigDecimal.fromStringUnsafe("1.50") * const b = BigDecimal.fromStringUnsafe("1.5") * const c = BigDecimal.fromStringUnsafe("2.0") * - * console.log(BigDecimal.Equivalence(a, b)) // true (1.50 === 1.5) - * console.log(BigDecimal.Equivalence(a, c)) // false (1.50 !== 2.0) + * BigDecimal.Equivalence(a, b) // => true + * BigDecimal.Equivalence(a, c) // => false * ``` * * @category instances @@ -1211,15 +1121,15 @@ export const Equivalence: Equ.Equivalence = Equ.make((self, that) => * * **Example** (Checking decimal equality) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const a = BigDecimal.fromStringUnsafe("1.5") * const b = BigDecimal.fromStringUnsafe("1.50") * const c = BigDecimal.fromStringUnsafe("2.0") * - * console.log(BigDecimal.equals(a, b)) // true - * console.log(BigDecimal.equals(a, c)) // false + * BigDecimal.equals(a, b) // => true + * BigDecimal.equals(a, c) // => false * ``` * * @see {@link Equivalence} for passing decimal equality to APIs that require an `Equivalence` @@ -1241,14 +1151,14 @@ export const equals: { * * **Example** (Creating decimals from bigint) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * * const decimal = BigDecimal.fromBigInt(123n) - * console.log(BigDecimal.format(decimal)) // "123" + * decimal // => BigDecimal.fromStringUnsafe("123") * * const largeBigInt = BigDecimal.fromBigInt(9007199254740991n) - * console.log(BigDecimal.format(largeBigInt)) // "9007199254740991" + * largeBigInt // => BigDecimal.fromStringUnsafe("9007199254740991") * ``` * * @see {@link make} for constructing a decimal with an explicit scale @@ -1274,12 +1184,11 @@ export const fromBigInt = (n: bigint): BigDecimal => make(n, 0) * * **Example** (Creating decimals from finite numbers) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.fromNumberUnsafe(123), BigDecimal.make(123n, 0)) - * assert.deepStrictEqual(BigDecimal.fromNumberUnsafe(123.456), BigDecimal.make(123456n, 3)) + * BigDecimal.fromNumberUnsafe(123) // => BigDecimal.fromBigInt(123n) + * BigDecimal.fromNumberUnsafe(123.456) // => BigDecimal.fromStringUnsafe("123.456") * ``` * * @see {@link fromNumber} for returning `Option.none` when the number is not finite @@ -1310,16 +1219,11 @@ export const fromNumberUnsafe = (n: number): BigDecimal => { * * **Example** (Creating decimals from numbers safely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal, Option } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual(BigDecimal.fromNumber(123), Option.some(BigDecimal.make(123n, 0))) - * assert.deepStrictEqual( - * BigDecimal.fromNumber(123.456), - * Option.some(BigDecimal.make(123456n, 3)) - * ) - * assert.deepStrictEqual(BigDecimal.fromNumber(Infinity), Option.none()) + * + * BigDecimal.fromNumber(123.456) // => Option.some(BigDecimal.fromStringUnsafe("123.456")) + * BigDecimal.fromNumber(Infinity) // => Option.none() * ``` * * @see {@link fromNumberUnsafe} for throwing when the number is not finite @@ -1357,16 +1261,11 @@ export const fromNumber = (n: number): Option.Option => { * * **Example** (Parsing decimal strings safely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal, Option } from "effect" - * import * as assert from "node:assert" - * - * 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()) + * + * BigDecimal.fromString("123.456") // => Option.some(BigDecimal.make(123456n, 3)) + * BigDecimal.fromString("123.abc") // => Option.none() * ``` * * @see {@link fromStringUnsafe} for parsing that throws on invalid input @@ -1435,13 +1334,11 @@ export const fromString = (s: string): Option.Option => { * * **Example** (Parsing decimal strings unsafely) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.fromStringUnsafe("123"), BigDecimal.make(123n, 0)) - * assert.deepStrictEqual(BigDecimal.fromStringUnsafe("123.456"), BigDecimal.make(123456n, 3)) - * assert.throws(() => BigDecimal.fromStringUnsafe("123.abc")) + * BigDecimal.fromStringUnsafe("123") // => BigDecimal.fromBigInt(123n) + * BigDecimal.fromStringUnsafe("123.456") // => BigDecimal.make(123456n, 3) * ``` * * @see {@link fromString} for returning `Option.none` on invalid input @@ -1468,13 +1365,12 @@ export const fromStringUnsafe = (s: string): BigDecimal => { * * **Example** (Formatting decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("-5")), "-5") - * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("123.456")), "123.456") - * assert.deepStrictEqual(BigDecimal.format(BigDecimal.fromStringUnsafe("-0.00000123")), "-0.00000123") + * BigDecimal.format(BigDecimal.fromStringUnsafe("-5")) // => "-5" + * BigDecimal.format(BigDecimal.fromStringUnsafe("123.456")) // => "123.456" + * BigDecimal.format(BigDecimal.fromStringUnsafe("-0.00000123")) // => "-0.00000123" * ``` * * @see {@link toExponential} for always rendering scientific notation @@ -1522,11 +1418,10 @@ export const format = (n: BigDecimal): string => { * * **Example** (Formatting decimals exponentially) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.toExponential(BigDecimal.make(123456n, -5)), "1.23456e+10") + * BigDecimal.toExponential(BigDecimal.make(123456n, -5)) // => "1.23456e+10" * ``` * * @see {@link format} for plain decimal formatting when possible @@ -1569,11 +1464,10 @@ export const toExponential = (n: BigDecimal): string => { * * **Example** (Converting decimals to numbers) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.toNumberUnsafe(BigDecimal.fromStringUnsafe("123.456")), 123.456) + * BigDecimal.toNumberUnsafe(BigDecimal.fromStringUnsafe("123.456")) // => 123.456 * ``` * * @see {@link format} for preserving decimal precision as text @@ -1592,13 +1486,12 @@ export const toNumberUnsafe = (n: BigDecimal): number => Number(format(n)) * * **Example** (Checking integer decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("0")), true) - * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1")), true) - * assert.deepStrictEqual(BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1.1")), false) + * BigDecimal.isInteger(BigDecimal.fromStringUnsafe("0")) // => true + * BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1")) // => true + * BigDecimal.isInteger(BigDecimal.fromStringUnsafe("1.1")) // => false * ``` * * @category predicates @@ -1615,12 +1508,11 @@ export const isInteger = (n: BigDecimal): boolean => normalize(n).scale <= 0 * * **Example** (Checking zero decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.isZero(BigDecimal.fromStringUnsafe("0")), true) - * assert.deepStrictEqual(BigDecimal.isZero(BigDecimal.fromStringUnsafe("1")), false) + * BigDecimal.isZero(BigDecimal.fromStringUnsafe("0")) // => true + * BigDecimal.isZero(BigDecimal.fromStringUnsafe("1")) // => false * ``` * * @category predicates @@ -1637,13 +1529,12 @@ export const isZero = (n: BigDecimal): boolean => n.value === bigint0 * * **Example** (Checking negative decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("-1")), true) - * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("0")), false) - * assert.deepStrictEqual(BigDecimal.isNegative(BigDecimal.fromStringUnsafe("1")), false) + * BigDecimal.isNegative(BigDecimal.fromStringUnsafe("-1")) // => true + * BigDecimal.isNegative(BigDecimal.fromStringUnsafe("0")) // => false + * BigDecimal.isNegative(BigDecimal.fromStringUnsafe("1")) // => false * ``` * * @category predicates @@ -1660,13 +1551,12 @@ export const isNegative = (n: BigDecimal): boolean => n.value < bigint0 * * **Example** (Checking positive decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("-1")), false) - * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("0")), false) - * assert.deepStrictEqual(BigDecimal.isPositive(BigDecimal.fromStringUnsafe("1")), true) + * BigDecimal.isPositive(BigDecimal.fromStringUnsafe("-1")) // => false + * BigDecimal.isPositive(BigDecimal.fromStringUnsafe("0")) // => false + * BigDecimal.isPositive(BigDecimal.fromStringUnsafe("1")) // => true * ``` * * @category predicates @@ -1726,18 +1616,14 @@ export type RoundingMode = * * **Example** (Rounding decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.round(BigDecimal.fromStringUnsafe("145"), { mode: "from-zero", scale: -1 }), - * BigDecimal.fromStringUnsafe("150") - * ) - * assert.deepStrictEqual( - * BigDecimal.round(BigDecimal.fromStringUnsafe("-14.5")), - * BigDecimal.fromStringUnsafe("-15") - * ) + * + * const positive = BigDecimal.round(BigDecimal.fromStringUnsafe("145"), { mode: "from-zero", scale: -1 }) + * positive // => BigDecimal.fromBigInt(150n) + * + * const negative = BigDecimal.round(BigDecimal.fromStringUnsafe("-14.5")) + * negative // => BigDecimal.fromBigInt(-15n) * ``` * * @see {@link ceil} for fixed rounding toward positive infinity @@ -1808,11 +1694,11 @@ export const round: { * * **Example** (Truncating decimals) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" * - * console.log(BigDecimal.truncate(BigDecimal.fromStringUnsafe("145"), -1)) // BigDecimal(140) - * console.log(BigDecimal.truncate(BigDecimal.fromStringUnsafe("-14.5"))) // BigDecimal(-14) + * BigDecimal.truncate(BigDecimal.fromStringUnsafe("145"), -1) // => BigDecimal.fromBigInt(140n) + * BigDecimal.truncate(BigDecimal.fromStringUnsafe("-14.5")) // => BigDecimal.fromBigInt(-14n) * ``` * * @see {@link round} for configurable rounding modes @@ -1853,15 +1739,11 @@ export const truncate: { * * **Example** (Rounding decimals up) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * BigDecimal.ceil(BigDecimal.fromStringUnsafe("145"), -1), - * BigDecimal.fromStringUnsafe("150") - * ) - * assert.deepStrictEqual(BigDecimal.ceil(BigDecimal.fromStringUnsafe("-14.5")), BigDecimal.fromStringUnsafe("-14")) + * BigDecimal.ceil(BigDecimal.fromStringUnsafe("145"), -1) // => BigDecimal.fromBigInt(150n) + * BigDecimal.ceil(BigDecimal.fromStringUnsafe("-14.5")) // => BigDecimal.fromBigInt(-14n) * ``` * * @category math @@ -1908,18 +1790,11 @@ export const digitAt: { * * **Example** (Rounding decimals down) * - * ```ts + * ```ts import.meta.vitest * import { BigDecimal } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * BigDecimal.floor(BigDecimal.fromStringUnsafe("145"), -1), - * BigDecimal.fromStringUnsafe("140") - * ) - * assert.deepStrictEqual( - * BigDecimal.floor(BigDecimal.fromStringUnsafe("-14.5")), - * BigDecimal.fromStringUnsafe("-15") - * ) + * + * BigDecimal.floor(BigDecimal.fromStringUnsafe("145"), -1) // => BigDecimal.fromBigInt(140n) + * BigDecimal.floor(BigDecimal.fromStringUnsafe("-14.5")) // => BigDecimal.fromBigInt(-15n) * ``` * * @see {@link ceil} for rounding toward positive infinity diff --git a/.context/effect/packages/effect/src/BigInt.ts b/.context/effect/packages/effect/src/BigInt.ts index 34a973647..18ac6ba68 100644 --- a/.context/effect/packages/effect/src/BigInt.ts +++ b/.context/effect/packages/effect/src/BigInt.ts @@ -36,14 +36,11 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Constructing bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" * - * const bigInt = BigInt.BigInt(123) - * console.log(bigInt) // 123n - * - * const fromString = BigInt.BigInt("456") - * console.log(fromString) // 456n + * BigInt.BigInt(123) // => 123n + * BigInt.BigInt("456") // => 456n * ``` * * @category constructors @@ -64,12 +61,11 @@ const bigint2 = BigInt(2) * * **Example** (Checking for bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.isBigInt(1n), true) - * assert.deepStrictEqual(BigInt.isBigInt(1), false) + * BigInt.isBigInt(1n) // => true + * BigInt.isBigInt(1) // => false * ``` * * @category guards @@ -87,11 +83,10 @@ export const isBigInt: (u: unknown) => u is bigint = predicate.isBigInt * * **Example** (Adding bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.sum(2n, 3n), 5n) + * BigInt.sum(2n, 3n) // => 5n * ``` * * @see {@link sumAll} for summing an iterable of `bigint` values @@ -113,11 +108,10 @@ export const sum: { * * **Example** (Multiplying bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.multiply(2n, 3n), 6n) + * BigInt.multiply(2n, 3n) // => 6n * ``` * * @see {@link multiplyAll} for multiplying an iterable of `bigint` values @@ -139,11 +133,10 @@ export const multiply: { * * **Example** (Subtracting bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.subtract(2n, 3n), -1n) + * BigInt.subtract(2n, 3n) // => -1n * ``` * * @category math @@ -169,12 +162,11 @@ export const subtract: { * * **Example** (Dividing bigints safely) * - * ```ts + * ```ts import.meta.vitest * import { BigInt, Option } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.divide(6n, 3n), Option.some(2n)) - * assert.deepStrictEqual(BigInt.divide(6n, 0n), Option.none()) + * BigInt.divide(6n, 3n) // => Option.some(2n) + * BigInt.divide(6n, 0n) // => Option.none() * ``` * * @see {@link divideUnsafe} for division that throws when the divisor is `0n` @@ -210,12 +202,11 @@ export const divide: { * * **Example** (Dividing bigints unsafely) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.divideUnsafe(6n, 3n), 2n) - * assert.deepStrictEqual(BigInt.divideUnsafe(6n, 4n), 1n) + * BigInt.divideUnsafe(6n, 3n) // => 2n + * BigInt.divideUnsafe(6n, 4n) // => 1n * ``` * * @see {@link divide} for division that returns `Option.none` when the divisor is `0n` @@ -237,11 +228,10 @@ export const divideUnsafe: { * * **Example** (Incrementing a bigint) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.increment(2n), 3n) + * BigInt.increment(2n) // => 3n * ``` * * @category math @@ -258,11 +248,10 @@ export const increment = (n: bigint): bigint => n + bigint1 * * **Example** (Decrementing a bigint) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.decrement(3n), 2n) + * BigInt.decrement(3n) // => 2n * ``` * * @category math @@ -280,16 +269,16 @@ export const decrement = (n: bigint): bigint => n - bigint1 * * **Example** (Comparing bigints with Order) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" * * const a = 123n * const b = 456n * const c = 123n * - * console.log(BigInt.Order(a, b)) // -1 (a < b) - * console.log(BigInt.Order(b, a)) // 1 (b > a) - * console.log(BigInt.Order(a, c)) // 0 (a === c) + * BigInt.Order(a, b) // => -1 + * BigInt.Order(b, a) // => 1 + * BigInt.Order(a, c) // => 0 * ``` * * @category instances @@ -307,11 +296,11 @@ export const Order: order.Order = order.BigInt * * **Example** (Comparing bigints for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" * - * console.log(BigInt.Equivalence(1n, 1n)) // true - * console.log(BigInt.Equivalence(1n, 2n)) // false + * BigInt.Equivalence(1n, 1n) // => true + * BigInt.Equivalence(1n, 2n) // => false * ``` * * @category instances @@ -328,13 +317,12 @@ export const Equivalence: Equ.Equivalence = Equ.BigInt * * **Example** (Checking less-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.isLessThan(2n, 3n), true) - * assert.deepStrictEqual(BigInt.isLessThan(3n, 3n), false) - * assert.deepStrictEqual(BigInt.isLessThan(4n, 3n), false) + * BigInt.isLessThan(2n, 3n) // => true + * BigInt.isLessThan(3n, 3n) // => false + * BigInt.isLessThan(4n, 3n) // => false * ``` * * @category predicates @@ -354,13 +342,12 @@ export const isLessThan: { * * **Example** (Checking less-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(2n, 3n), true) - * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(3n, 3n), true) - * assert.deepStrictEqual(BigInt.isLessThanOrEqualTo(4n, 3n), false) + * BigInt.isLessThanOrEqualTo(2n, 3n) // => true + * BigInt.isLessThanOrEqualTo(3n, 3n) // => true + * BigInt.isLessThanOrEqualTo(4n, 3n) // => false * ``` * * @category predicates @@ -380,13 +367,12 @@ export const isLessThanOrEqualTo: { * * **Example** (Checking greater-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.isGreaterThan(2n, 3n), false) - * assert.deepStrictEqual(BigInt.isGreaterThan(3n, 3n), false) - * assert.deepStrictEqual(BigInt.isGreaterThan(4n, 3n), true) + * BigInt.isGreaterThan(2n, 3n) // => false + * BigInt.isGreaterThan(3n, 3n) // => false + * BigInt.isGreaterThan(4n, 3n) // => true * ``` * * @category predicates @@ -406,13 +392,12 @@ export const isGreaterThan: { * * **Example** (Checking greater-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(2n, 3n), false) - * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(3n, 3n), true) - * assert.deepStrictEqual(BigInt.isGreaterThanOrEqualTo(4n, 3n), true) + * BigInt.isGreaterThanOrEqualTo(2n, 3n) // => false + * BigInt.isGreaterThanOrEqualTo(3n, 3n) // => true + * BigInt.isGreaterThanOrEqualTo(4n, 3n) // => true * ``` * * @category predicates @@ -432,15 +417,14 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking whether a bigint is within bounds) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * * const between = BigInt.between({ minimum: 0n, maximum: 5n }) * - * assert.deepStrictEqual(between(3n), true) - * assert.deepStrictEqual(between(-1n), false) - * assert.deepStrictEqual(between(6n), false) + * between(3n) // => true + * between(-1n) // => false + * between(6n) // => false * ``` * * @see {@link clamp} for forcing a `bigint` into an inclusive range @@ -474,15 +458,14 @@ export const between: { * * **Example** (Clamping a bigint to bounds) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * * const clamp = BigInt.clamp({ minimum: 1n, maximum: 5n }) * - * assert.equal(clamp(3n), 3n) - * assert.equal(clamp(0n), 1n) - * assert.equal(clamp(6n), 5n) + * clamp(3n) // => 3n + * clamp(0n) // => 1n + * clamp(6n) // => 5n * ``` * * @see {@link between} for checking whether a `bigint` is already inside a range @@ -510,11 +493,10 @@ export const clamp: { * * **Example** (Finding the minimum bigint) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.min(2n, 3n), 2n) + * BigInt.min(2n, 3n) // => 2n * ``` * * @see {@link max} for selecting the larger value @@ -536,11 +518,10 @@ export const min: { * * **Example** (Finding the maximum bigint) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.max(2n, 3n), 3n) + * BigInt.max(2n, 3n) // => 3n * ``` * * @see {@link min} for selecting the smaller value @@ -562,13 +543,12 @@ export const max: { * * **Example** (Determining bigint signs) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.sign(-5n), -1) - * assert.deepStrictEqual(BigInt.sign(0n), 0) - * assert.deepStrictEqual(BigInt.sign(5n), 1) + * BigInt.sign(-5n) // => -1 + * BigInt.sign(0n) // => 0 + * BigInt.sign(5n) // => 1 * ``` * * @category math @@ -585,13 +565,12 @@ export const sign = (n: bigint): Ordering => order.BigInt(n, bigint0) * * **Example** (Calculating absolute values) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.abs(-5n), 5n) - * assert.deepStrictEqual(BigInt.abs(0n), 0n) - * assert.deepStrictEqual(BigInt.abs(5n), 5n) + * BigInt.abs(-5n) // => 5n + * BigInt.abs(0n) // => 0n + * BigInt.abs(5n) // => 5n * ``` * * @category math @@ -608,13 +587,12 @@ export const abs = (n: bigint): bigint => (n < bigint0 ? -n : n) * * **Example** (Calculating greatest common divisors) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.gcd(2n, 3n), 1n) - * assert.deepStrictEqual(BigInt.gcd(2n, 4n), 2n) - * assert.deepStrictEqual(BigInt.gcd(16n, 24n), 8n) + * BigInt.gcd(2n, 3n) // => 1n + * BigInt.gcd(2n, 4n) // => 2n + * BigInt.gcd(16n, 24n) // => 8n * ``` * * @see {@link lcm} for computing the least common multiple @@ -631,7 +609,7 @@ export const gcd: { that = self % that self = t } - return self + return abs(self) }) /** @@ -643,13 +621,12 @@ export const gcd: { * * **Example** (Calculating least common multiples) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.lcm(2n, 3n), 6n) - * assert.deepStrictEqual(BigInt.lcm(2n, 4n), 4n) - * assert.deepStrictEqual(BigInt.lcm(16n, 24n), 48n) + * BigInt.lcm(2n, 3n) // => 6n + * BigInt.lcm(2n, 4n) // => 4n + * BigInt.lcm(16n, 24n) // => 48n * ``` * * @see {@link gcd} for computing the greatest common divisor @@ -660,7 +637,11 @@ export const gcd: { 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)) +} = dual( + 2, + (self: bigint, that: bigint): bigint => + self === bigint0 || that === bigint0 ? bigint0 : abs((self * that) / gcd(self, that)) +) /** * Returns the integer square root of a non-negative `bigint`. @@ -682,13 +663,12 @@ export const lcm: { * * **Example** (Calculating square roots unsafely) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.sqrtUnsafe(4n), 2n) - * assert.deepStrictEqual(BigInt.sqrtUnsafe(9n), 3n) - * assert.deepStrictEqual(BigInt.sqrtUnsafe(16n), 4n) + * BigInt.sqrtUnsafe(4n) // => 2n + * BigInt.sqrtUnsafe(9n) // => 3n + * BigInt.sqrtUnsafe(16n) // => 4n * ``` * * @see {@link sqrt} for returning `Option.none` when the input is negative @@ -726,13 +706,13 @@ export const sqrtUnsafe = (n: bigint): bigint => { * * **Example** (Calculating square roots safely) * - * ```ts - * import { BigInt } from "effect" + * ```ts import.meta.vitest + * import { BigInt, Option } from "effect" * - * BigInt.sqrt(4n) // Option.some(2n) - * BigInt.sqrt(9n) // Option.some(3n) - * BigInt.sqrt(16n) // Option.some(4n) - * BigInt.sqrt(-1n) // Option.none() + * BigInt.sqrt(4n) // => Option.some(2n) + * BigInt.sqrt(9n) // => Option.some(3n) + * BigInt.sqrt(16n) // => Option.some(4n) + * BigInt.sqrt(-1n) // => Option.none() * ``` * * @see {@link sqrtUnsafe} for square root computation that throws on negative input @@ -753,11 +733,10 @@ export const sqrt = (n: bigint): Option.Option => * * **Example** (Summing iterable bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.sumAll([2n, 3n, 4n]), 9n) + * BigInt.sumAll([2n, 3n, 4n]) // => 9n * ``` * * @see {@link sum} for adding two `bigint` values @@ -783,11 +762,10 @@ export const sumAll = (collection: Iterable): bigint => { * * **Example** (Multiplying iterable bigints) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(BigInt.multiplyAll([2n, 3n, 4n]), 24n) + * BigInt.multiplyAll([2n, 3n, 4n]) // => 24n * ``` * * @see {@link multiply} for multiplying two `bigint` values @@ -822,12 +800,12 @@ export const multiplyAll = (collection: Iterable): bigint => { * * **Example** (Converting bigints to numbers) * - * ```ts - * import { BigInt as BI } from "effect" + * ```ts import.meta.vitest + * import { BigInt as BI, Option } from "effect" * - * BI.toNumber(42n) // Option.some(42) - * BI.toNumber(BigInt(Number.MAX_SAFE_INTEGER) + 1n) // Option.none() - * BI.toNumber(BigInt(Number.MIN_SAFE_INTEGER) - 1n) // Option.none() + * BI.toNumber(42n) // => Option.some(42) + * BI.toNumber(9007199254740992n) // => Option.none() + * BI.toNumber(-9007199254740992n) // => Option.none() * ``` * * @see {@link fromNumber} for converting a safe integer number to `bigint` @@ -856,12 +834,12 @@ export const toNumber = (b: bigint): Option.Option => { * * **Example** (Parsing strings as bigints) * - * ```ts - * import { BigInt } from "effect" + * ```ts import.meta.vitest + * import { BigInt, Option } from "effect" * - * BigInt.fromString("42") // Option.some(42n) - * BigInt.fromString(" ") // Option.none() - * BigInt.fromString("a") // Option.none() + * BigInt.fromString("42") // => Option.some(42n) + * BigInt.fromString(" ") // => Option.none() + * BigInt.fromString("a") // => Option.none() * ``` * * @see {@link BigInt} for native constructor coercion that throws on invalid input @@ -894,13 +872,12 @@ export const fromString = (s: string): Option.Option => { * * **Example** (Converting numbers to bigints) * - * ```ts - * import { BigInt } from "effect" - * - * BigInt.fromNumber(42) // Option.some(42n) + * ```ts import.meta.vitest + * import { BigInt, Option } from "effect" * - * BigInt.fromNumber(Number.MAX_SAFE_INTEGER + 1) // Option.none() - * BigInt.fromNumber(Number.MIN_SAFE_INTEGER - 1) // Option.none() + * BigInt.fromNumber(42) // => Option.some(42n) + * BigInt.fromNumber(Number.MAX_SAFE_INTEGER + 1) // => Option.none() + * BigInt.fromNumber(Number.MIN_SAFE_INTEGER - 1) // => Option.none() * ``` * * @see {@link toNumber} for converting `bigint` values back to safe integer numbers @@ -935,12 +912,11 @@ export function fromNumber(n: number): Option.Option { * * **Example** (Calculating remainders) * - * ```ts + * ```ts import.meta.vitest * import { BigInt } from "effect" * - * BigInt.remainder(10n, 3n) // 1n - * - * BigInt.remainder(15n, 4n) // 3n + * BigInt.remainder(10n, 3n) // => 1n + * BigInt.remainder(15n, 4n) // => 3n * ``` * * @see {@link divide} for quotient calculation with division-by-zero represented as `Option.none` diff --git a/.context/effect/packages/effect/src/Boolean.ts b/.context/effect/packages/effect/src/Boolean.ts index d69e52a27..405e7b35a 100644 --- a/.context/effect/packages/effect/src/Boolean.ts +++ b/.context/effect/packages/effect/src/Boolean.ts @@ -31,17 +31,12 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Coercing values to booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" * - * const bool = Boolean.Boolean(1) - * console.log(bool) // true - * - * const fromString = Boolean.Boolean("false") - * console.log(fromString) // true (non-empty string) - * - * const fromZero = Boolean.Boolean(0) - * console.log(fromZero) // false + * Boolean.Boolean(1) // => true + * Boolean.Boolean("false") // => true + * Boolean.Boolean(0) // => false * ``` * * @category constructors @@ -58,12 +53,11 @@ export const Boolean = globalThis.Boolean * * **Example** (Checking for booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.isBoolean(true), true) - * assert.deepStrictEqual(Boolean.isBoolean("true"), false) + * Boolean.isBoolean(true) // => true + * Boolean.isBoolean("true") // => false * ``` * * @category guards @@ -80,17 +74,13 @@ export const isBoolean: (input: unknown) => input is boolean = predicate.isBoole * * **Example** (Pattern matching on booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * Boolean.match(true, { - * onFalse: () => "It's false!", - * onTrue: () => "It's true!" - * }), - * "It's true!" - * ) + * + * Boolean.match(true, { + * onFalse: () => "It's false!", + * onTrue: () => "It's true!" + * }) // => "It's true!" * ``` * * @category pattern matching @@ -121,12 +111,12 @@ export const match: { * * **Example** (Comparing booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" * - * console.log(Boolean.Order(false, true)) // -1 (false < true) - * console.log(Boolean.Order(true, false)) // 1 (true > false) - * console.log(Boolean.Order(true, true)) // 0 (true === true) + * Boolean.Order(false, true) // => -1 + * Boolean.Order(true, false) // => 1 + * Boolean.Order(true, true) // => 0 * ``` * * @category instances @@ -144,11 +134,11 @@ export const Order: order.Order = order.Boolean * * **Example** (Comparing booleans for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" * - * console.log(Boolean.Equivalence(true, true)) // true - * console.log(Boolean.Equivalence(true, false)) // false + * Boolean.Equivalence(true, true) // => true + * Boolean.Equivalence(true, false) // => false * ``` * * @category instances @@ -165,12 +155,11 @@ export const Equivalence: Equ.Equivalence = Equ.Boolean * * **Example** (Negating booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.not(true), false) - * assert.deepStrictEqual(Boolean.not(false), true) + * Boolean.not(true) // => false + * Boolean.not(false) // => true * ``` * * @category combinators @@ -191,14 +180,13 @@ export const not = (self: boolean): boolean => !self * * **Example** (Combining booleans with AND) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.and(true, true), true) - * assert.deepStrictEqual(Boolean.and(true, false), false) - * assert.deepStrictEqual(Boolean.and(false, true), false) - * assert.deepStrictEqual(Boolean.and(false, false), false) + * Boolean.and(true, true) // => true + * Boolean.and(true, false) // => false + * Boolean.and(false, true) // => false + * Boolean.and(false, false) // => false * ``` * * @category combinators @@ -218,14 +206,13 @@ export const and: { * * **Example** (Combining booleans with NAND) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.nand(true, true), false) - * assert.deepStrictEqual(Boolean.nand(true, false), true) - * assert.deepStrictEqual(Boolean.nand(false, true), true) - * assert.deepStrictEqual(Boolean.nand(false, false), true) + * Boolean.nand(true, true) // => false + * Boolean.nand(true, false) // => true + * Boolean.nand(false, true) // => true + * Boolean.nand(false, false) // => true * ``` * * @category combinators @@ -245,14 +232,13 @@ export const nand: { * * **Example** (Combining booleans with OR) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.or(true, true), true) - * assert.deepStrictEqual(Boolean.or(true, false), true) - * assert.deepStrictEqual(Boolean.or(false, true), true) - * assert.deepStrictEqual(Boolean.or(false, false), false) + * Boolean.or(true, true) // => true + * Boolean.or(true, false) // => true + * Boolean.or(false, true) // => true + * Boolean.or(false, false) // => false * ``` * * @category combinators @@ -272,14 +258,13 @@ export const or: { * * **Example** (Combining booleans with NOR) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.nor(true, true), false) - * assert.deepStrictEqual(Boolean.nor(true, false), false) - * assert.deepStrictEqual(Boolean.nor(false, true), false) - * assert.deepStrictEqual(Boolean.nor(false, false), true) + * Boolean.nor(true, true) // => false + * Boolean.nor(true, false) // => false + * Boolean.nor(false, true) // => false + * Boolean.nor(false, false) // => true * ``` * * @category combinators @@ -299,14 +284,13 @@ export const nor: { * * **Example** (Combining booleans with XOR) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.xor(true, true), false) - * assert.deepStrictEqual(Boolean.xor(true, false), true) - * assert.deepStrictEqual(Boolean.xor(false, true), true) - * assert.deepStrictEqual(Boolean.xor(false, false), false) + * Boolean.xor(true, true) // => false + * Boolean.xor(true, false) // => true + * Boolean.xor(false, true) // => true + * Boolean.xor(false, false) // => false * ``` * * @category combinators @@ -326,14 +310,13 @@ export const xor: { * * **Example** (Checking boolean equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.eqv(true, true), true) - * assert.deepStrictEqual(Boolean.eqv(true, false), false) - * assert.deepStrictEqual(Boolean.eqv(false, true), false) - * assert.deepStrictEqual(Boolean.eqv(false, false), true) + * Boolean.eqv(true, true) // => true + * Boolean.eqv(true, false) // => false + * Boolean.eqv(false, true) // => false + * Boolean.eqv(false, false) // => true * ``` * * @category combinators @@ -353,14 +336,13 @@ export const eqv: { * * **Example** (Checking boolean implication) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.implies(true, true), true) - * assert.deepStrictEqual(Boolean.implies(true, false), false) - * assert.deepStrictEqual(Boolean.implies(false, true), true) - * assert.deepStrictEqual(Boolean.implies(false, false), true) + * Boolean.implies(true, true) // => true + * Boolean.implies(true, false) // => false + * Boolean.implies(false, true) // => true + * Boolean.implies(false, false) // => true * ``` * * @category combinators @@ -380,12 +362,11 @@ export const implies: { * * **Example** (Checking every boolean) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.every([true, true, true]), true) - * assert.deepStrictEqual(Boolean.every([true, false, true]), false) + * Boolean.every([true, true, true]) // => true + * Boolean.every([true, false, true]) // => false * ``` * * @see {@link some} for checking whether at least one value is `true` @@ -412,12 +393,11 @@ export const every = (collection: Iterable): boolean => { * * **Example** (Checking some booleans) * - * ```ts + * ```ts import.meta.vitest * import { Boolean } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Boolean.some([true, false, true]), true) - * assert.deepStrictEqual(Boolean.some([false, false, false]), false) + * Boolean.some([true, false, true]) // => true + * Boolean.some([false, false, false]) // => false * ``` * * @see {@link every} for checking whether all values are `true` diff --git a/.context/effect/packages/effect/src/Brand.ts b/.context/effect/packages/effect/src/Brand.ts index 39789b23a..1cbba59fd 100644 --- a/.context/effect/packages/effect/src/Brand.ts +++ b/.context/effect/packages/effect/src/Brand.ts @@ -12,7 +12,7 @@ import * as Option from "./Option.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" -import type * as SchemaIssue from "./SchemaIssue.ts" +import * as SchemaIssue from "./SchemaIssue.ts" import type * as Types from "./Types.ts" const TypeId = "~effect/Brand" @@ -92,8 +92,8 @@ export interface Constructor> { * * **Details** * - * The error wraps a `SchemaIssue.Issue`, exposes `message` through - * `issue.toString()`, and formats as `BrandError()`. + * The error wraps a `SchemaIssue.Issue`, renders `message` with the default + * schema issue formatter, and formats as `BrandError()`. * * **Gotchas** * @@ -131,7 +131,7 @@ export class BrandError { * @since 4.0.0 */ get message() { - return this.issue.toString() + return SchemaIssue.defaultFormatter(this.issue) } /** * Formats the brand error together with its validation message. diff --git a/.context/effect/packages/effect/src/Cache.ts b/.context/effect/packages/effect/src/Cache.ts index 12c4fd69d..d1e3091b2 100644 --- a/.context/effect/packages/effect/src/Cache.ts +++ b/.context/effect/packages/effect/src/Cache.ts @@ -32,7 +32,7 @@ const TypeId = "~effect/Cache" * * **Example** (Creating a basic cache) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Basic cache with string keys and number values @@ -49,12 +49,15 @@ const TypeId = "~effect/Cache" * * return [value1, value2, value3] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [5, 5, 5] * ``` * * **Example** (Handling lookup failures) * - * ```ts - * import { Cache, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cache, Effect, Exit } from "effect" * * // Cache with error handling * const program = Effect.gen(function*() { @@ -67,16 +70,19 @@ const TypeId = "~effect/Cache" * }) * * // Handle successful and failed lookups - * const success = yield* Cache.get(cache, "test") // 4 - * const failure = yield* Effect.exit(Cache.get(cache, "error")) // Exit.fail + * const success = yield* Cache.get(cache, "test") + * const failure = yield* Effect.exit(Cache.get(cache, "error")) * - * return { success, failure } + * return [success, failure] as const * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [4, Exit.fail("Lookup failed")] * ``` * * **Example** (Using complex keys with TTL) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Data, Duration, Effect } from "effect" * * // Cache with complex key types and TTL @@ -92,8 +98,11 @@ const TypeId = "~effect/Cache" * const userId = new UserId({ id: 123 }) * const userName = yield* Cache.get(userCache, userId) * - * return userName // "User-123" + * return userName * }) + * + * const actual = await Effect.runPromise(program) + * actual // => "User-123" * ``` * * @category models @@ -148,11 +157,11 @@ export interface Entry { * * **Example** (Configuring dynamic time to live) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect, Exit } from "effect" * * // Cache with TTL based on computed value - * const userCache = Effect.gen(function*() { + * const program = Effect.gen(function*() { * const cache = yield* Cache.makeWith( * (id: number) => Effect.succeed({ id, active: id % 2 === 0 }), * { @@ -167,8 +176,11 @@ export interface Entry { * } * ) * - * return cache + * return cache.capacity * }) + * + * const actual = await Effect.runPromise(program) + * actual // => 1000 * ``` * * @see {@link make} for a simpler cache constructor with a fixed time-to-live for all entries @@ -215,7 +227,7 @@ export const makeWith = < * * **Example** (Creating a basic cache) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Basic cache with string keys @@ -227,13 +239,16 @@ export const makeWith = < * * const result1 = yield* Cache.get(cache, "hello") * const result2 = yield* Cache.get(cache, "world") - * console.log({ result1, result2 }) // { result1: 5, result2: 5 } + * return { result1, result2 } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { result1: 5, result2: 5 } * ``` * * **Example** (Creating a cache with TTL) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -259,11 +274,12 @@ export const makeWith = < * }) * * const user1 = yield* Cache.get(cache, 123) - * console.log(user1) // { name: "Ada", email: "ada@example.com" } - * * const user2 = yield* Cache.get(cache, 123) - * console.log(user2) // { name: "Ada", email: "ada@example.com" } + * return [user1, user2, user1 === user2] as const * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [{ name: "Ada", email: "ada@example.com" }, { name: "Ada", email: "ada@example.com" }, true] * ``` * * @category constructors @@ -289,7 +305,7 @@ export const make = < > => makeWith(options.lookup, { ...options, - timeToLive: options.timeToLive ? () => options.timeToLive! : defaultTimeToLive + timeToLive: options.timeToLive !== undefined ? () => options.timeToLive! : defaultTimeToLive }) const Proto = { @@ -318,7 +334,7 @@ const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Du * * **Example** (Getting cached values) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -329,20 +345,21 @@ const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Du * * // Cache miss - triggers lookup function * const result1 = yield* Cache.get(cache, "hello") - * console.log(result1) // 5 * * // Cache hit - returns cached value without lookup * const result2 = yield* Cache.get(cache, "hello") - * console.log(result2) // 5 (from cache) * * return { result1, result2 } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { result1: 5, result2: 5 } * ``` * * **Example** (Handling lookup failures) * - * ```ts - * import { Cache, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cache, Effect, Exit } from "effect" * * // Error handling when lookup fails * const program = Effect.gen(function*() { @@ -356,17 +373,19 @@ const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Du * * // Successful lookup * const success = yield* Cache.get(cache, "hello") - * console.log(success) // 5 * * // Failed lookup - returns error * const failure = yield* Effect.exit(Cache.get(cache, "error")) - * console.log(failure) // Exit.fail("Lookup failed") + * return [success, failure] as const * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [5, Exit.fail("Lookup failed")] * ``` * * **Example** (Sharing concurrent lookups) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Concurrent access - multiple gets of same key only invoke lookup once @@ -388,9 +407,11 @@ const defaultTimeToLive = (_: Exit.Exit, _key: unknown): Duration.Du * Cache.get(cache, "hello") * ], { concurrency: "unbounded" }) * - * console.log(results) // [5, 5, 5] - * console.log(lookupCount) // 1 (lookup called only once) + * return { results, lookupCount } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { results: [5, 5, 5], lookupCount: 1 } * ``` * * @category combinators @@ -413,7 +434,10 @@ export const get: { const entry = new EntryImpl(fiber, self.lookup(key)) entry.fiber.addObserver((exit) => { if (effect.exitHasInterrupts(exit)) { - MutableHashMap.remove(self.map, key) + const current = MutableHashMap.get(self.map, key) + if (Option.isSome(current) && current.value === entry) { + MutableHashMap.remove(self.map, key) + } return } const ttl = self.timeToLive(exit, key) @@ -487,8 +511,8 @@ const checkCapacity = (self: Cache) => { * * **Example** (Reading cached values without lookup) * - * ```ts - * import { Cache, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cache, Effect, Option } from "effect" * * const program = Effect.gen(function*() { * const cache = yield* Cache.make({ @@ -498,23 +522,23 @@ const checkCapacity = (self: Cache) => { * * // No value in cache yet - returns None without lookup * const empty = yield* Cache.getOption(cache, "hello") - * console.log(empty) // Option.none() * * // Populate cache using get * yield* Cache.get(cache, "hello") * * // Now getOption returns the cached value * const cached = yield* Cache.getOption(cache, "hello") - * console.log(cached) // Option.some(5) - * - * return { empty, cached } + * return [empty, cached] as const * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [Option.none(), Option.some(5)] * ``` * * **Example** (Skipping expired entries) * - * ```ts - * import { Cache, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cache, Effect, Option } from "effect" * import { TestClock } from "effect/testing" * * // Expired entries return None @@ -530,21 +554,23 @@ const checkCapacity = (self: Cache) => { * * // Value exists before expiration * const beforeExpiry = yield* Cache.getOption(cache, "hello") - * console.log(beforeExpiry) // Option.some(5) * * // Simulate time passing * yield* TestClock.adjust("2 hours") * * // Value expired - returns None * const afterExpiry = yield* Cache.getOption(cache, "hello") - * console.log(afterExpiry) // Option.none() + * return [beforeExpiry, afterExpiry] as const * }) + * + * const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer())) + * actual // => [Option.some(5), Option.none()] * ``` * * **Example** (Waiting for pending lookups) * - * ```ts - * import { Cache, Deferred, Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Cache, Deferred, Effect, Fiber, Option } from "effect" * * // Waits for ongoing computation to complete * const program = Effect.gen(function*() { @@ -564,11 +590,12 @@ const checkCapacity = (self: Cache) => { * yield* Deferred.succeed(deferred, void 0) * * const result = yield* Fiber.join(optionFiber) - * console.log(result) // Option.some(42) - * * const value = yield* Fiber.join(getFiber) - * console.log(value) // 42 + * return [result, value] as const * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [Option.some(42), 42] * ``` * * @category combinators @@ -642,7 +669,7 @@ export const getSuccess: { * * **Example** (Setting values directly) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -653,14 +680,16 @@ export const getSuccess: { * * // Set a value directly without invoking lookup * yield* Cache.set(cache, "hello", 42) - * const result = yield* Cache.get(cache, "hello") - * console.log(result) // 42 (not 5 from lookup) + * return yield* Cache.get(cache, "hello") * }) + * + * const actual = await Effect.runPromise(program) + * actual // => 42 * ``` * * **Example** (Overwriting cached values) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Overwriting existing cached values @@ -677,13 +706,16 @@ export const getSuccess: { * yield* Cache.set(cache, "test", 999) * const updated = yield* Cache.get(cache, "test") // 999 * - * console.log({ original, updated }) + * return { original, updated } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { original: 4, updated: 999 } * ``` * * **Example** (Applying TTL to set values) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -697,17 +729,21 @@ export const getSuccess: { * * // Set value with TTL applied * yield* Cache.set(cache, "temporary", 123) - * console.log(yield* Cache.has(cache, "temporary")) // true + * const beforeExpiry = yield* Cache.has(cache, "temporary") * * // Advance time past TTL * yield* TestClock.adjust("2 hours") - * console.log(yield* Cache.has(cache, "temporary")) // false + * const afterExpiry = yield* Cache.has(cache, "temporary") + * return [beforeExpiry, afterExpiry] * }) + * + * const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer())) + * actual // => [true, false] * ``` * * **Example** (Enforcing capacity when setting values) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Capacity enforcement with set operations @@ -720,14 +756,18 @@ export const getSuccess: { * // Fill cache to capacity * yield* Cache.set(cache, "a", 1) * yield* Cache.set(cache, "b", 2) - * console.log(yield* Cache.size(cache)) // 2 + * const sizeBeforeEviction = yield* Cache.size(cache) * * // Adding another entry evicts oldest * yield* Cache.set(cache, "c", 3) - * console.log(yield* Cache.size(cache)) // 2 - * console.log(yield* Cache.has(cache, "a")) // false (evicted) - * console.log(yield* Cache.has(cache, "c")) // true + * const sizeAfterEviction = yield* Cache.size(cache) + * const hasOldest = yield* Cache.has(cache, "a") + * const hasNewest = yield* Cache.has(cache, "c") + * return [sizeBeforeEviction, sizeAfterEviction, hasOldest, hasNewest] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [2, 2, false, true] * ``` * * @category combinators @@ -766,7 +806,7 @@ export const set: { * * **Example** (Checking for cached keys) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -776,17 +816,21 @@ export const set: { * }) * * // Check non-existent key - * console.log(yield* Cache.has(cache, "missing")) // false + * const missing = yield* Cache.has(cache, "missing") * * // Add entry and check existence * yield* Cache.get(cache, "hello") - * console.log(yield* Cache.has(cache, "hello")) // true + * const present = yield* Cache.has(cache, "hello") + * return [missing, present] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [false, true] * ``` * * **Example** (Checking TTL expiration) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -800,21 +844,25 @@ export const set: { * * // Add entry with TTL * yield* Cache.get(cache, "expires") - * console.log(yield* Cache.has(cache, "expires")) // true + * const initial = yield* Cache.has(cache, "expires") * * // Still valid before expiration * yield* TestClock.adjust("30 minutes") - * console.log(yield* Cache.has(cache, "expires")) // true + * const beforeExpiry = yield* Cache.has(cache, "expires") * * // Expired after TTL * yield* TestClock.adjust("31 minutes") - * console.log(yield* Cache.has(cache, "expires")) // false + * const afterExpiry = yield* Cache.has(cache, "expires") + * return [initial, beforeExpiry, afterExpiry] * }) + * + * const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer())) + * actual // => [true, true, false] * ``` * * **Example** (Checking multiple keys) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Checking multiple keys efficiently @@ -830,16 +878,16 @@ export const set: { * * // Check multiple keys * const keys = ["apple", "banana", "cherry", "date"] + * const results: Array = [] * for (const key of keys) { * const exists = yield* Cache.has(cache, key) - * console.log(`${key}: ${exists}`) + * results.push(`${key}: ${exists}`) * } - * // Output: - * // apple: true - * // banana: true - * // cherry: false - * // date: false + * return results * }) + * + * const actual = await Effect.runPromise(program) + * actual // => ["apple: true", "banana: true", "cherry: false", "date: false"] * ``` * * @category combinators @@ -862,7 +910,7 @@ export const has: { * * **Example** (Invalidating cached entries) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -873,11 +921,11 @@ export const has: { * * // Add a value to the cache * yield* Cache.get(cache, "hello") - * console.log(yield* Cache.has(cache, "hello")) // true + * const beforeInvalidation = yield* Cache.has(cache, "hello") * * // Invalidate the entry * yield* Cache.invalidate(cache, "hello") - * console.log(yield* Cache.has(cache, "hello")) // false + * const afterInvalidation = yield* Cache.has(cache, "hello") * * // Invalidating non-existent keys doesn't error * yield* Cache.invalidate(cache, "nonexistent") @@ -896,7 +944,11 @@ export const has: { * yield* Cache.get(cache2, "test") // lookupCount = 1 * yield* Cache.invalidate(cache2, "test") * yield* Cache.get(cache2, "test") // lookupCount = 2 (lookup called again) + * return { beforeInvalidation, afterInvalidation, lookupCount } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { beforeInvalidation: true, afterInvalidation: false, lookupCount: 2 } * ``` * * @category combinators @@ -916,7 +968,7 @@ export const invalidate: { * * **Example** (Invalidating entries conditionally) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -935,8 +987,7 @@ export const invalidate: { * "hello", * (value) => value === 5 * ) - * console.log(invalidated1) // true - * console.log(yield* Cache.has(cache, "hello")) // false + * const hasHello = yield* Cache.has(cache, "hello") * * // Don't invalidate when predicate doesn't match * const invalidated2 = yield* Cache.invalidateWhen( @@ -944,8 +995,7 @@ export const invalidate: { * "hi", * (value) => value === 5 * ) - * console.log(invalidated2) // false - * console.log(yield* Cache.has(cache, "hi")) // true (still present) + * const hasHi = yield* Cache.has(cache, "hi") * * // Returns false for non-existent keys * const invalidated3 = yield* Cache.invalidateWhen( @@ -953,7 +1003,6 @@ export const invalidate: { * "nonexistent", * () => true * ) - * console.log(invalidated3) // false * * // Returns false for failed cached values * const cacheWithErrors = yield* Cache.make({ @@ -968,8 +1017,11 @@ export const invalidate: { * "fail", * () => true * ) - * console.log(invalidated4) // false (can't invalidate failed values) + * return [invalidated1, hasHello, invalidated2, hasHi, invalidated3, invalidated4] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [true, false, false, true, false, false] * ``` * * @category combinators @@ -1009,7 +1061,7 @@ export const invalidateWhen: { * * **Example** (Refreshing cached values) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Force refresh of existing cached values @@ -1022,25 +1074,25 @@ export const invalidateWhen: { * * // Initial cache population * const value1 = yield* Cache.get(cache, "user") - * console.log(value1) // "user-1" * * // Get from cache (no lookup) * const value2 = yield* Cache.get(cache, "user") - * console.log(value2) // "user-1" (same value) * * // Force refresh - always calls lookup * const refreshed = yield* Cache.refresh(cache, "user") - * console.log(refreshed) // "user-2" (new value) * * // Subsequent gets return refreshed value * const value3 = yield* Cache.get(cache, "user") - * console.log(value3) // "user-2" + * return [value1, value2, refreshed, value3, counter] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => ["user-1", "user-1", "user-2", "user-2", 2] * ``` * * **Example** (Resetting TTL on refresh) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -1056,20 +1108,24 @@ export const invalidateWhen: { * yield* TestClock.adjust("45 minutes") * * // Entry would normally expire in 15 minutes - * console.log(yield* Cache.has(cache, "test")) // true + * const beforeRefresh = yield* Cache.has(cache, "test") * * // Refresh resets the TTL to full 1 hour * yield* Cache.refresh(cache, "test") * yield* TestClock.adjust("30 minutes") * * // Still valid because TTL was reset - * console.log(yield* Cache.has(cache, "test")) // true + * const afterRefresh = yield* Cache.has(cache, "test") + * return [beforeRefresh, afterRefresh] * }) + * + * const actual = await Effect.runPromise(Effect.provide(program, TestClock.layer())) + * actual // => [true, true] * ``` * * **Example** (Refreshing missing keys) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Refresh non-existent keys @@ -1081,11 +1137,14 @@ export const invalidateWhen: { * * // Refresh non-existent key creates new entry * const result = yield* Cache.refresh(cache, "newKey") - * console.log(result) // "value-for-newKey" * * // Verify it's now cached - * console.log(yield* Cache.has(cache, "newKey")) // true + * const cached = yield* Cache.has(cache, "newKey") + * return [result, cached] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => ["value-for-newKey", true] * ``` * * @category combinators @@ -1130,7 +1189,7 @@ export const refresh: { * * **Example** (Invalidating all entries) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * // Clear all cached entries at once @@ -1145,18 +1204,29 @@ export const refresh: { * yield* Cache.get(cache, "banana") * yield* Cache.get(cache, "cherry") * - * console.log(yield* Cache.size(cache)) // 3 - * console.log(yield* Cache.has(cache, "apple")) // true + * const sizeBeforeInvalidation = yield* Cache.size(cache) + * const hasAppleBeforeInvalidation = yield* Cache.has(cache, "apple") * * // Clear all entries * yield* Cache.invalidateAll(cache) * * // Verify cache is empty - * console.log(yield* Cache.size(cache)) // 0 - * console.log(yield* Cache.has(cache, "apple")) // false - * console.log(yield* Cache.has(cache, "banana")) // false - * console.log(yield* Cache.has(cache, "cherry")) // false + * const sizeAfterInvalidation = yield* Cache.size(cache) + * const hasAppleAfterInvalidation = yield* Cache.has(cache, "apple") + * const hasBananaAfterInvalidation = yield* Cache.has(cache, "banana") + * const hasCherryAfterInvalidation = yield* Cache.has(cache, "cherry") + * return [ + * sizeBeforeInvalidation, + * hasAppleBeforeInvalidation, + * sizeAfterInvalidation, + * hasAppleAfterInvalidation, + * hasBananaAfterInvalidation, + * hasCherryAfterInvalidation + * ] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [3, true, 0, false, false, false] * ``` * * @category combinators @@ -1178,7 +1248,7 @@ export const invalidateAll = (self: Cache): Effect.E * * **Example** (Reading cache size) * - * ```ts + * ```ts import.meta.vitest * import { Cache, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -1189,19 +1259,20 @@ export const invalidateAll = (self: Cache): Effect.E * * // Empty cache has size 0 * const emptySize = yield* Cache.size(cache) - * console.log(emptySize) // 0 * * // Add entries and check size * yield* Cache.get(cache, "hello") * yield* Cache.get(cache, "world") * const sizeAfterAdding = yield* Cache.size(cache) - * console.log(sizeAfterAdding) // 2 * * // Size decreases after invalidation * yield* Cache.invalidate(cache, "hello") * const sizeAfterInvalidation = yield* Cache.size(cache) - * console.log(sizeAfterInvalidation) // 1 + * return [emptySize, sizeAfterAdding, sizeAfterInvalidation] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [0, 2, 1] * ``` * * @category combinators @@ -1215,7 +1286,7 @@ export const size = (self: Cache): Effect.Effect(self: Cache): Effect.Effect ["cache", "hello", "world"] * ``` * * @category combinators @@ -1258,7 +1331,7 @@ export const keys = (self: Cache): Effect.Effect(self: Cache): Effect.Effect [1, 2, 3] * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/Cause.ts b/.context/effect/packages/effect/src/Cause.ts index 2d0fda116..2511f27bc 100644 --- a/.context/effect/packages/effect/src/Cause.ts +++ b/.context/effect/packages/effect/src/Cause.ts @@ -63,12 +63,10 @@ export const ReasonTypeId: "~effect/Cause/Reason" = core.CauseReasonTypeId * * **Example** (Creating and inspecting a cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause = Cause.fail("Something went wrong") - * console.log(cause.reasons.length) // 1 - * console.log(Cause.isFailReason(cause.reasons[0])) // true + * Cause.fail("Something went wrong") // => Cause.fail("Something went wrong") * ``` * * @category models @@ -84,11 +82,11 @@ export interface Cause extends Pipeable, Inspectable, Equal { * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isCause(Cause.fail("error"))) // true - * console.log(Cause.isCause("not a cause")) // false + * Cause.isCause(Cause.fail("error")) // => true + * Cause.isCause("not a cause") // => false * ``` * * @category guards @@ -101,12 +99,12 @@ export const isCause: (self: unknown) => self is Cause = core.isCause * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const reason = Cause.fail("error").reasons[0] - * console.log(Cause.isReason(reason)) // true - * console.log(Cause.isReason("not a reason")) // false + * Cause.isReason(reason) // => true + * Cause.isReason("not a reason") // => false * ``` * * @category guards @@ -131,12 +129,12 @@ export const isReason: (self: unknown) => self is Reason = core.isCause * * **Example** (Narrowing a reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const reason = Cause.fail("error").reasons[0] * if (Cause.isFailReason(reason)) { - * console.log(reason.error) // "error" + * reason.error // => "error" * } * ``` * @@ -155,12 +153,12 @@ export type Reason = Fail | Die | Interrupt * * **Example** (Filtering fail reasons) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.fail("error") * const fails = cause.reasons.filter(Cause.isFailReason) - * console.log(fails[0].error) // "error" + * fails[0].error // => "error" * ``` * * @see {@link isDieReason} — narrow to `Die` @@ -181,12 +179,12 @@ export const isFailReason: (self: Reason) => self is Fail = core.isFail * * **Example** (Filtering die reasons) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.die("defect") * const dies = cause.reasons.filter(Cause.isDieReason) - * console.log(dies[0].defect) // "defect" + * dies[0].defect // => "defect" * ``` * * @see {@link isFailReason} — narrow to `Fail` @@ -207,12 +205,12 @@ export const isDieReason: (self: Reason) => self is Die = core.isDieReason * * **Example** (Filtering interrupt reasons) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.interrupt(123) * const interrupts = cause.reasons.filter(Cause.isInterruptReason) - * console.log(interrupts[0].fiberId) // 123 + * interrupts[0].fiberId // => 123 * ``` * * @see {@link isFailReason} — narrow to `Fail` @@ -234,14 +232,14 @@ export declare namespace Cause { * * **Example** (Extracting the error type) * - * ```ts + * ```ts import.meta.vitest * import type { Cause } from "effect" * * // string * type E = Cause.Cause.Error> * ``` * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = T extends Cause ? E : never @@ -280,14 +278,14 @@ export declare namespace Reason { * * **Example** (Extracting the error type) * - * ```ts + * ```ts import.meta.vitest * import type { Cause } from "effect" * * // string * type E = Cause.Reason.Error> * ``` * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = T extends Reason ? E : never @@ -309,13 +307,13 @@ export declare namespace Reason { * * **Example** (Accessing the defect) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.die("Unexpected") * const reason = cause.reasons[0] * if (Cause.isDieReason(reason)) { - * console.log(reason.defect) // "Unexpected" + * reason.defect // => "Unexpected" * } * ``` * @@ -344,13 +342,13 @@ export interface Die extends Cause.ReasonProto<"Die"> { * * **Example** (Accessing the error) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.fail("Something went wrong") * const reason = cause.reasons[0] * if (Cause.isFailReason(reason)) { - * console.log(reason.error) // "Something went wrong" + * reason.error // => "Something went wrong" * } * ``` * @@ -374,13 +372,13 @@ export interface Fail extends Cause.ReasonProto<"Fail"> { * * **Example** (Accessing the fiber ID) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.interrupt(123) * const reason = cause.reasons[0] * if (Cause.isInterruptReason(reason)) { - * console.log(reason.fiberId) // 123 + * reason.fiberId // => 123 * } * ``` * @@ -412,15 +410,14 @@ export interface Interrupt extends Cause.ReasonProto<"Interrupt"> { * * **Example** (Building a cause from reasons) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const reasons = [ * Cause.makeFailReason("err1"), * Cause.makeFailReason("err2") * ] - * const cause = Cause.fromReasons(reasons) - * console.log(cause.reasons.length) // 2 + * Cause.fromReasons(reasons) // => Cause.combine(Cause.fail("err1"), Cause.fail("err2")) * ``` * * @see {@link combine} — merge two existing causes @@ -447,13 +444,10 @@ export const fromReasons: ( * * **Example** (Combining with the empty cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause = Cause.combine(Cause.empty, Cause.fail("boom")) - * - * console.log(cause.reasons.length) // 1 - * console.log(Cause.hasFails(cause)) // true + * Cause.combine(Cause.empty, Cause.fail("boom")) // => Cause.fail("boom") * ``` * * @see {@link combine} for merging causes where `empty` acts as the identity @@ -473,12 +467,10 @@ export const empty: Cause = core.causeEmpty * * **Example** (Creating a fail cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause = Cause.fail("Something went wrong") - * console.log(cause.reasons.length) // 1 - * console.log(Cause.isFailReason(cause.reasons[0])) // true + * Cause.fail("Something went wrong") // => Cause.fromReasons([Cause.makeFailReason("Something went wrong")]) * ``` * * @see {@link die} — for untyped defects @@ -499,12 +491,10 @@ export const fail: (error: E) => Cause = core.causeFail * * **Example** (Creating a die cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause = Cause.die("Unexpected") - * console.log(cause.reasons.length) // 1 - * console.log(Cause.isDieReason(cause.reasons[0])) // true + * Cause.die("Unexpected") // => Cause.fromReasons([Cause.makeDieReason("Unexpected")]) * ``` * * @see {@link fail} — for typed errors @@ -521,12 +511,10 @@ export const die: (defect: unknown) => Cause = core.causeDie * * **Example** (Creating an interrupt cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause = Cause.interrupt(123) - * console.log(cause.reasons.length) // 1 - * console.log(Cause.isInterruptReason(cause.reasons[0])) // true + * Cause.interrupt(123) // => Cause.fromReasons([Cause.makeInterruptReason(123)]) * ``` * * @see {@link fail} — for typed errors @@ -547,12 +535,10 @@ export const interrupt: (fiberId?: number | undefined) => Cause = effect. * * **Example** (Creating a Fail reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const reason = Cause.makeFailReason("error") - * console.log(reason._tag) // "Fail" - * console.log(reason.error) // "error" + * Cause.makeFailReason("error") // => Cause.fail("error").reasons[0] * ``` * * @see {@link makeDieReason} — create a `Die` reason @@ -573,12 +559,10 @@ export const makeFailReason = (error: E): Fail => new core.Fail(error) * * **Example** (Creating a Die reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const reason = Cause.makeDieReason("bug") - * console.log(reason._tag) // "Die" - * console.log(reason.defect) // "bug" + * Cause.makeDieReason("bug") // => Cause.die("bug").reasons[0] * ``` * * @see {@link makeFailReason} — create a `Fail` reason @@ -600,12 +584,10 @@ export const makeDieReason = (defect: unknown): Die => new core.Die(defect) * * **Example** (Creating an Interrupt reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const reason = Cause.makeInterruptReason(42) - * console.log(reason._tag) // "Interrupt" - * console.log(reason.fiberId) // 42 + * Cause.makeInterruptReason(42) // => Cause.interrupt(42).reasons[0] * ``` * * @see {@link makeFailReason} — create a `Fail` reason @@ -626,12 +608,12 @@ export const makeInterruptReason: (fiberId?: number | undefined) => Interrupt = * * **Example** (Checking interrupt-only causes) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.hasInterruptsOnly(Cause.interrupt(123))) // true - * console.log(Cause.hasInterruptsOnly(Cause.fail("error"))) // false - * console.log(Cause.hasInterruptsOnly(Cause.empty)) // false + * Cause.hasInterruptsOnly(Cause.interrupt(123)) // => true + * Cause.hasInterruptsOnly(Cause.fail("error")) // => false + * Cause.hasInterruptsOnly(Cause.empty) // => false * ``` * * @see {@link hasInterrupts} — `true` if the cause contains *any* interrupts @@ -659,14 +641,14 @@ export const hasInterruptsOnly: (self: Cause) => boolean = effect.hasInter * * **Example** (Mapping errors to uppercase) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * * const cause = Cause.fail("error") * const mapped = Cause.map(cause, (e) => e.toUpperCase()) * const reason = mapped.reasons[0] * if (Cause.isFailReason(reason)) { - * console.log(reason.error) // "ERROR" + * reason.error // => "ERROR" * } * ``` * @@ -694,13 +676,11 @@ export const map: { * * **Example** (Combining two causes) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const cause1 = Cause.fail("error1") - * const cause2 = Cause.fail("error2") - * const combined = Cause.combine(cause1, cause2) - * console.log(combined.reasons.length) // 2 + * const combined = Cause.combine(Cause.fail("error1"), Cause.fail("error2")) + * combined // => Cause.fromReasons([Cause.makeFailReason("error1"), Cause.makeFailReason("error2")]) * ``` * * @see {@link fromReasons} — build a cause from an array of reasons @@ -740,11 +720,11 @@ export const combine: { * * **Example** (Squashing a cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.squash(Cause.fail("error"))) // "error" - * console.log(Cause.squash(Cause.die("defect"))) // "defect" + * Cause.squash(Cause.fail("error")) // => "error" + * Cause.squash(Cause.die("defect")) // => "defect" * ``` * * @see {@link prettyErrors} — non-lossy conversion to `Array` @@ -765,11 +745,11 @@ export const squash: (self: Cause) => unknown = effect.causeSquash * * **Example** (Checking for typed errors) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.hasFails(Cause.fail("error"))) // true - * console.log(Cause.hasFails(Cause.die("defect"))) // false + * Cause.hasFails(Cause.fail("error")) // => true + * Cause.hasFails(Cause.die("defect")) // => false * ``` * * @see {@link hasDies} — check for defects @@ -793,13 +773,10 @@ export const hasFails: (self: Cause) => boolean = effect.hasFails * * **Example** (Extracting the first Fail reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Result } from "effect" * - * const result = Cause.findFail(Cause.fail("error")) - * if (!Result.isFailure(result)) { - * console.log(result.success.error) // "error" - * } + * Cause.findFail(Cause.fail("error")) // => Result.succeed(Cause.makeFailReason("error")) * ``` * * @see {@link findError} — extract the unwrapped `E` value @@ -823,13 +800,10 @@ export const findFail: (self: Cause) => Result.Result, Cause Result.succeed("error") * ``` * * @see {@link findFail} — extract the full `Fail` reason @@ -851,14 +825,11 @@ export const findError: (self: Cause) => Result.Result> = * * **Example** (Extracting an error as Option) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Option } from "effect" * - * const some = Cause.findErrorOption(Cause.fail("error")) - * console.log(Option.isSome(some)) // true - * - * const none = Cause.findErrorOption(Cause.die("defect")) - * console.log(Option.isNone(none)) // true + * Cause.findErrorOption(Cause.fail("error")) // => Option.some("error") + * Cause.findErrorOption(Cause.die("defect")) // => Option.none() * ``` * * @see {@link findError} — `Result`-based variant @@ -878,11 +849,11 @@ export const findErrorOption: (input: Cause) => Option = effect.findErr * * **Example** (Checking for defects) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.hasDies(Cause.die("defect"))) // true - * console.log(Cause.hasDies(Cause.fail("error"))) // false + * Cause.hasDies(Cause.die("defect")) // => true + * Cause.hasDies(Cause.fail("error")) // => false * ``` * * @see {@link hasFails} — check for typed errors @@ -905,13 +876,10 @@ export const hasDies: (self: Cause) => boolean = effect.hasDies * * **Example** (Extracting the first Die reason) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Result } from "effect" * - * const result = Cause.findDie(Cause.die("defect")) - * if (!Result.isFailure(result)) { - * console.log(result.success.defect) // "defect" - * } + * Cause.findDie(Cause.die("defect")) // => Result.succeed(Cause.makeDieReason("defect")) * ``` * * @see {@link findDefect} — extract the unwrapped defect value @@ -934,13 +902,10 @@ export const findDie: (self: Cause) => Result.Result> = effe * * **Example** (Extracting the first defect) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Result } from "effect" * - * const result = Cause.findDefect(Cause.die("defect")) - * if (!Result.isFailure(result)) { - * console.log(result.success) // "defect" - * } + * Cause.findDefect(Cause.die("defect")) // => Result.succeed("defect") * ``` * * @see {@link findDie} — extract the full `Die` reason @@ -956,11 +921,11 @@ export const findDefect: (self: Cause) => Result.Result> * * **Example** (Checking for interruptions) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.hasInterrupts(Cause.interrupt(123))) // true - * console.log(Cause.hasInterrupts(Cause.fail("error"))) // false + * Cause.hasInterrupts(Cause.interrupt(123)) // => true + * Cause.hasInterrupts(Cause.fail("error")) // => false * ``` * * @see {@link hasInterruptsOnly} — `true` only when *all* reasons are interrupts @@ -984,13 +949,10 @@ export const hasInterrupts: (self: Cause) => boolean = effect.hasInterrupt * * **Example** (Extracting the first interrupt) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Result } from "effect" * - * const result = Cause.findInterrupt(Cause.interrupt(42)) - * if (!Result.isFailure(result)) { - * console.log(result.success.fiberId) // 42 - * } + * Cause.findInterrupt(Cause.interrupt(42)) // => Result.succeed(Cause.makeInterruptReason(42)) * ``` * * @see {@link interruptors} — collect all interrupting fiber IDs as a `Set` @@ -1012,7 +974,7 @@ export const findInterrupt: (self: Cause) => Result.Result(self: Cause) => Result.Result new Set([1, 2]) * ``` * * @see {@link filterInterruptors} — `Result`-based variant * - * @category accessors + * @category getters * @since 2.0.0 */ export const interruptors: (self: Cause) => ReadonlySet = effect.causeInterruptors @@ -1048,13 +1010,10 @@ export const interruptors: (self: Cause) => ReadonlySet = effect.c * * **Example** (Extracting interruptors with Result) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Result } from "effect" * - * const result = Cause.filterInterruptors(Cause.interrupt(1)) - * if (!Result.isFailure(result)) { - * console.log(result.success) // Set(1) { 1 } - * } + * Cause.filterInterruptors(Cause.interrupt(1)) // => Result.succeed(new Set([1])) * ``` * * @see {@link interruptors} — always-succeeding variant @@ -1094,18 +1053,16 @@ export const filterInterruptors: (self: Cause) => Result.Result "boom" * ``` * * @see {@link pretty} — renders the cause as a single string * @see {@link squash} — lossy collapse to a single thrown value * - * @category rendering + * @category formatting * @since 3.2.0 */ export const prettyErrors: (self: Cause, options?: { @@ -1144,16 +1101,15 @@ export const prettyErrors: (self: Cause, options?: { * * **Example** (Rendering a cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const rendered = Cause.pretty(Cause.fail("something went wrong")) - * console.log(rendered.includes("something went wrong")) // true + * Cause.pretty(Cause.fail("something went wrong")).includes("something went wrong") // => true * ``` * * @see {@link prettyErrors} — get the individual `Error` instances * - * @category rendering + * @category formatting * @since 2.0.0 */ export const pretty: (cause: Cause) => string = effect.causePretty @@ -1171,14 +1127,16 @@ export const pretty: (cause: Cause) => string = effect.causePretty * * **Example** (Yielding an error in Effect.gen) * - * ```ts - * import { Cause, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" * * const error = new Cause.NoSuchElementError("not found") * * const program = Effect.gen(function*() { * return yield* error // fails the effect with NoSuchElementError * }) + * + * await Effect.runPromiseExit(program) // => Exit.fail(error) * ``` * * @category errors @@ -1194,11 +1152,11 @@ export interface YieldableError extends Error, Pipeable, Inspectable { * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isNoSuchElementError(new Cause.NoSuchElementError())) // true - * console.log(Cause.isNoSuchElementError("nope")) // false + * Cause.isNoSuchElementError(new Cause.NoSuchElementError()) // => true + * Cause.isNoSuchElementError("nope") // => false * ``` * * @category guards @@ -1233,16 +1191,6 @@ export const NoSuchElementErrorTypeId: "~effect/Cause/NoSuchElementError" = core * expected case. This error is mainly for APIs that intentionally turn absence * into a thrown value or failed effect. * - * **Example** (Creating and checking a NoSuchElementError) - * - * ```ts - * import { Cause } from "effect" - * - * const error = new Cause.NoSuchElementError("Element not found") - * console.log(error._tag) // "NoSuchElementError" - * console.log(error.message) // "Element not found" - * ``` - * * @category errors * @since 4.0.0 */ @@ -1261,11 +1209,10 @@ export interface NoSuchElementError extends YieldableError { * * **Example** (Creating a NoSuchElementError) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const error = new Cause.NoSuchElementError("Element not found") - * console.log(error.message) // "Element not found" + * new Cause.NoSuchElementError("Element not found").message // => "Element not found" * ``` * * @see {@link isNoSuchElementError} for checking unknown values @@ -1280,11 +1227,11 @@ export const NoSuchElementError: new(message?: string) => NoSuchElementError = c * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isDone(Cause.Done())) // true - * console.log(Cause.isDone("not done")) // false + * Cause.isDone(Cause.Done()) // => true + * Cause.isDone("not done") // => false * ``` * * @category guards @@ -1317,7 +1264,7 @@ export const DoneTypeId: "~effect/Cause/Done" = core.DoneTypeId * * **Example** (Signaling queue completion) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1325,9 +1272,12 @@ export const DoneTypeId: "~effect/Cause/Done" = core.DoneTypeId * yield* Queue.offer(queue, 1) * yield* Queue.end(queue) * + * yield* Queue.take(queue) * const result = yield* Effect.flip(Queue.take(queue)) - * console.log(Cause.isDone(result)) // true + * return Cause.isDone(result) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category errors @@ -1387,14 +1337,12 @@ export const Done: (value?: A) => Done = core.Done * * **Example** (Failing with Done) * - * ```ts - * import { Cause, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" * * const program = Cause.done("finished") * - * Effect.runPromiseExit(program).then((exit) => { - * console.log(exit._tag) // "Failure" - * }) + * await Effect.runPromiseExit(program) // => Exit.fail(Cause.Done("finished")) * ``` * * @see {@link Done} — create the signal value without an Effect @@ -1417,11 +1365,11 @@ export const TimeoutErrorTypeId: "~effect/Cause/TimeoutError" = effect.TimeoutEr * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isTimeoutError(new Cause.TimeoutError())) // true - * console.log(Cause.isTimeoutError("nope")) // false + * Cause.isTimeoutError(new Cause.TimeoutError()) // => true + * Cause.isTimeoutError("nope") // => false * ``` * * @category guards @@ -1437,16 +1385,6 @@ export const isTimeoutError: (u: unknown) => u is TimeoutError = effect.isTimeou * Produced by `Effect.timeout` and related APIs. Implements * `YieldableError`. * - * **Example** (Creating and checking a TimeoutError) - * - * ```ts - * import { Cause } from "effect" - * - * const error = new Cause.TimeoutError("Operation timed out") - * console.log(error._tag) // "TimeoutError" - * console.log(error.message) // "Operation timed out" - * ``` - * * @category errors * @since 4.0.0 */ @@ -1460,11 +1398,10 @@ export interface TimeoutError extends YieldableError { * * **Example** (Creating a TimeoutError) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const error = new Cause.TimeoutError("Operation timed out") - * console.log(error.message) // "Operation timed out" + * new Cause.TimeoutError("Operation timed out").message // => "Operation timed out" * ``` * * @category constructors @@ -1485,11 +1422,11 @@ export const IllegalArgumentErrorTypeId: "~effect/Cause/IllegalArgumentError" = * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isIllegalArgumentError(new Cause.IllegalArgumentError())) // true - * console.log(Cause.isIllegalArgumentError("nope")) // false + * Cause.isIllegalArgumentError(new Cause.IllegalArgumentError()) // => true + * Cause.isIllegalArgumentError("nope") // => false * ``` * * @category guards @@ -1505,16 +1442,6 @@ export const isIllegalArgumentError: (u: unknown) => u is IllegalArgumentError = * * Implements `YieldableError`. * - * **Example** (Creating and checking an IllegalArgumentError) - * - * ```ts - * import { Cause } from "effect" - * - * const error = new Cause.IllegalArgumentError("Expected positive number") - * console.log(error._tag) // "IllegalArgumentError" - * console.log(error.message) // "Expected positive number" - * ``` - * * @category errors * @since 4.0.0 */ @@ -1528,11 +1455,10 @@ export interface IllegalArgumentError extends YieldableError { * * **Example** (Creating an IllegalArgumentError) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const error = new Cause.IllegalArgumentError("Invalid argument") - * console.log(error.message) // "Invalid argument" + * new Cause.IllegalArgumentError("Invalid argument").message // => "Invalid argument" * ``` * * @category constructors @@ -1545,11 +1471,11 @@ export const IllegalArgumentError: new(message?: string) => IllegalArgumentError * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isExceededCapacityError(new Cause.ExceededCapacityError())) // true - * console.log(Cause.isExceededCapacityError("nope")) // false + * Cause.isExceededCapacityError(new Cause.ExceededCapacityError()) // => true + * Cause.isExceededCapacityError("nope") // => false * ``` * * @category guards @@ -1578,16 +1504,6 @@ export const ExceededCapacityErrorTypeId: "~effect/Cause/ExceededCapacityError" * * Implements `YieldableError`. * - * **Example** (Creating and checking an ExceededCapacityError) - * - * ```ts - * import { Cause } from "effect" - * - * const error = new Cause.ExceededCapacityError("Queue full") - * console.log(error._tag) // "ExceededCapacityError" - * console.log(error.message) // "Queue full" - * ``` - * * @category errors * @since 4.0.0 */ @@ -1605,11 +1521,10 @@ export interface ExceededCapacityError extends YieldableError { * * **Example** (Creating an ExceededCapacityError) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const error = new Cause.ExceededCapacityError("Queue full") - * console.log(error.message) // "Queue full" + * new Cause.ExceededCapacityError("Queue full").message // => "Queue full" * ``` * * @see {@link isExceededCapacityError} for checking unknown values @@ -1633,15 +1548,14 @@ export const AsyncFiberErrorTypeId: "~effect/Cause/AsyncFiberError" = effect.Asy * * **Example** (Checking the runtime type) * - * ```ts - * import { Cause } from "effect" - * import type { Fiber } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect } from "effect" * - * declare const fiber: Fiber.Fiber + * const fiber = Effect.runFork(Effect.void) * * const error = new Cause.AsyncFiberError(fiber) - * console.log(Cause.isAsyncFiberError(error)) // true - * console.log(Cause.isAsyncFiberError("nope")) // false + * Cause.isAsyncFiberError(error) // => true + * Cause.isAsyncFiberError("nope") // => false * ``` * * @category guards @@ -1664,15 +1578,14 @@ export const isAsyncFiberError: (u: unknown) => u is AsyncFiberError = effect.is * * **Example** (Accessing the fiber) * - * ```ts - * import { Cause } from "effect" - * import type { Fiber } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect } from "effect" * - * declare const fiber: Fiber.Fiber + * const fiber = Effect.runFork(Effect.void) * - * const error = new Cause.AsyncFiberError(fiber) - * console.log(error._tag) // "AsyncFiberError" - * console.log(error.fiber === fiber) // true + * const value = new Cause.AsyncFiberError(fiber) + * const isSameFiber = value.fiber === fiber + * isSameFiber // => true * ``` * * @category errors @@ -1695,14 +1608,12 @@ export interface AsyncFiberError extends YieldableError { * * **Example** (Creating an AsyncFiberError) * - * ```ts - * import { Cause } from "effect" - * import type { Fiber } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect } from "effect" * - * declare const fiber: Fiber.Fiber + * const fiber = Effect.runFork(Effect.void) * - * const error = new Cause.AsyncFiberError(fiber) - * console.log(error.message) // "An asynchronous Effect was executed with Effect.runSync" + * new Cause.AsyncFiberError(fiber).message // => "An asynchronous Effect was executed with Effect.runSync" * ``` * * @see {@link isAsyncFiberError} for checking unknown values @@ -1725,11 +1636,11 @@ export const UnknownErrorTypeId: "~effect/Cause/UnknownError" = effect.UnknownEr * * **Example** (Checking the runtime type) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * console.log(Cause.isUnknownError(new Cause.UnknownError("x"))) // true - * console.log(Cause.isUnknownError("nope")) // false + * Cause.isUnknownError(new Cause.UnknownError("x")) // => true + * Cause.isUnknownError("nope") // => false * ``` * * @category guards @@ -1746,16 +1657,6 @@ export const isUnknownError: (u: unknown) => u is UnknownError = effect.isUnknow * typed error. The original value is stored in the `cause` property inherited * from `Error`. Implements `YieldableError`. * - * **Example** (Creating and checking an UnknownError) - * - * ```ts - * import { Cause } from "effect" - * - * const error = new Cause.UnknownError("original", "Something unknown") - * console.log(error._tag) // "UnknownError" - * console.log(error.message) // "Something unknown" - * ``` - * * @category errors * @since 4.0.0 */ @@ -1771,11 +1672,10 @@ export interface UnknownError extends YieldableError { * * **Example** (Creating an UnknownError) * - * ```ts + * ```ts import.meta.vitest * import { Cause } from "effect" * - * const error = new Cause.UnknownError({ raw: true }, "Unexpected value") - * console.log(error.message) // "Unexpected value" + * new Cause.UnknownError({ raw: true }, "Unexpected value").message // => "Unexpected value" * ``` * * @category constructors @@ -1802,15 +1702,13 @@ export const UnknownError: new(cause: unknown, message?: string) => UnknownError * * **Example** (Annotating a cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Context } from "effect" * * class RequestId extends Context.Service()("RequestId") {} * - * const cause = Cause.fail("error") - * const annotated = Cause.annotate(cause, Context.make(RequestId, "req-1")) - * - * console.log(Context.getOrUndefined(Cause.annotations(annotated), RequestId)) // "req-1" + * const annotated = Cause.annotate(Cause.fail("error"), Context.make(RequestId, "req-1")) + * Context.getOrUndefined(Cause.annotations(annotated), RequestId) // => "req-1" * ``` * * @see {@link annotations} for reading merged annotations from a cause @@ -1841,7 +1739,7 @@ export const annotate: { * * **Example** (Reading reason annotations) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Context } from "effect" * * class RequestId extends Context.Service()("RequestId") {} @@ -1849,7 +1747,7 @@ export const annotate: { * const reason = Cause.makeFailReason("error") * const annotated = reason.annotate(Context.make(RequestId, "req-1")) * - * console.log(Context.getOrUndefined(Cause.reasonAnnotations(annotated), RequestId)) // "req-1" + * Context.getOrUndefined(Cause.reasonAnnotations(annotated), RequestId) // => "req-1" * ``` * * @see {@link annotations} — merged annotations from all reasons in a cause @@ -1873,7 +1771,7 @@ export const reasonAnnotations: (self: Reason) => Context.Context = * * **Example** (Reading merged annotations) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Context } from "effect" * * class RequestId extends Context.Service()("RequestId") {} @@ -1883,7 +1781,7 @@ export const reasonAnnotations: (self: Reason) => Context.Context = * Context.make(RequestId, "req-1") * ) * - * console.log(Context.getOrUndefined(Cause.annotations(cause), RequestId)) // "req-1" + * Context.getOrUndefined(Cause.annotations(cause), RequestId) // => "req-1" * ``` * * @see {@link reasonAnnotations} — annotations from a single reason @@ -1911,7 +1809,7 @@ export const annotations: (self: Cause) => Context.Context = effect * @see {@link annotations} for reading merged annotations from a cause * @see {@link InterruptorStackTrace} for the interrupt-specific stack-frame annotation * - * @category annotations + * @category services * @since 4.0.0 */ export class StackTrace extends Context.Service()("effect/Cause/StackTrace") {} @@ -1933,7 +1831,7 @@ export class StackTrace extends Context.Service()("effec * @see {@link reasonAnnotations} for reading annotations from a single reason * @see {@link annotate} for attaching annotations to a cause * - * @category annotations + * @category services * @since 4.0.0 */ export class InterruptorStackTrace diff --git a/.context/effect/packages/effect/src/Channel.ts b/.context/effect/packages/effect/src/Channel.ts index f094d9bad..a9d09d3c1 100644 --- a/.context/effect/packages/effect/src/Channel.ts +++ b/.context/effect/packages/effect/src/Channel.ts @@ -26,6 +26,7 @@ import * as Iterable from "./Iterable.ts" import * as Latch from "./Latch.ts" import * as Layer from "./Layer.ts" import type { Severity } from "./LogLevel.ts" +import * as MutableRef from "./MutableRef.ts" import * as Option from "./Option.ts" import type { Pipeable } from "./Pipeable.ts" import { pipeArguments } from "./Pipeable.ts" @@ -67,12 +68,12 @@ export const TypeId: TypeId = "~effect/Channel" * * **Example** (Checking for channels) * - * ```ts + * ```ts import.meta.vitest * import { Channel } from "effect" * * const channel = Channel.succeed(42) - * console.log(Channel.isChannel(channel)) // true - * console.log(Channel.isChannel("not a channel")) // false + * Channel.isChannel(channel) // => true + * Channel.isChannel("not a channel") // => false * ``` * * @category guards @@ -109,8 +110,8 @@ export const isChannel = ( * * **Example** (Typing channels) * - * ```ts - * import type { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // A channel that outputs numbers and requires no environment * type NumberChannel = Channel.Channel @@ -128,6 +129,9 @@ export const isChannel = ( * boolean, // InDone - input completion * { db: string } // Env - required environment * > + * + * const channel: NumberChannel = Channel.succeed(1) + * Effect.runSync(Channel.runCollect(channel)) // => [1] * ``` * * @category models @@ -264,12 +268,13 @@ const ChannelProto = { * * **Example** (Creating channels from transforms) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect } from "effect" * * const channel = Channel.fromTransform((upstream, scope) => * Effect.succeed(upstream) * ) + * await Effect.runPromise(Channel.runCollect(channel)) // => [] * ``` * * @category constructors @@ -300,7 +305,7 @@ export const fromTransform = value * 2) * ) * ) - * // Outputs: 2, 4, 6 + * await Effect.runPromise(Channel.runCollect(transformedChannel)) // => [2, 4, 6] * ``` * * @category constructors @@ -354,12 +359,18 @@ export const transformPull = < * * **Example** (Creating channels from pulls) * - * ```ts - * import { Channel, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Channel, Effect } from "effect" * - * const channel = Channel.fromPull( - * Effect.succeed(Effect.succeed(42)) - * ) + * const channel = Channel.fromPull(Effect.sync(() => { + * let emitted = false + * return Effect.suspend(() => { + * if (emitted) return Cause.done() + * emitted = true + * return Effect.succeed(42) + * }) + * })) + * await Effect.runPromise(Channel.runCollect(channel)) // => [42] * ``` * * @category constructors @@ -410,12 +421,13 @@ export const fromTransformBracket = "function" + * Effect.runSync(Channel.runCollect(channel)) // => [42] * ``` * * @category destructors @@ -433,10 +445,10 @@ export const toTransform = 4096 * ``` * * @category constants @@ -465,7 +477,7 @@ const asyncQueue = ( * * **Example** (Creating channels from callbacks) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect, Queue } from "effect" * * const channel = Channel.callback((queue) => @@ -473,8 +485,10 @@ const asyncQueue = ( * yield* Queue.offer(queue, 1) * yield* Queue.offer(queue, 2) * yield* Queue.offer(queue, 3) + * yield* Queue.end(queue) * }) * ) + * await Effect.runPromise(Channel.runCollect(channel)) // => [1, 2, 3] * ``` * * @category constructors @@ -494,14 +508,15 @@ export const callback = ( * * **Example** (Creating array channels from callbacks) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect, Queue } from "effect" * * const channel = Channel.callbackArray(Effect.fn(function*(queue) { * yield* Queue.offer(queue, 1) * yield* Queue.offer(queue, 2) + * yield* Queue.end(queue) * })) - * // Emits arrays of numbers instead of individual numbers + * await Effect.runPromise(Channel.runCollect(channel)) // => [[1, 2]] * ``` * * @category constructors @@ -521,11 +536,11 @@ export const callbackArray = ( * * **Example** (Suspending channel creation) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * const channel = Channel.suspend(() => Channel.succeed(42)) - * // The inner channel is not created until the suspended channel is run + * Effect.runSync(Channel.runCollect(channel)) // => [42] * ``` * * @category constructors @@ -548,14 +563,16 @@ export const suspend = ( * * **Example** (Managing resources with acquire-use-release) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect } from "effect" * + * const released: Array = [] * const channel = Channel.acquireUseRelease( * Effect.succeed("resource"), * (resource) => Channel.succeed(resource.toUpperCase()), - * (resource, exit) => Effect.log(`Released: ${resource}`) + * (resource, exit) => Effect.sync(() => released.push(resource)) * ) + * const observed = [await Effect.runPromise(Channel.runCollect(channel)), released] // => [["RESOURCE"], ["resource"]] * ``` * * @category constructors @@ -591,13 +608,15 @@ export const acquireUseRelease = = [] * const channel = Channel.acquireRelease( * Effect.succeed("resource"), - * (resource, exit) => Effect.log(`Released: ${resource}`) + * (resource, exit) => Effect.sync(() => released.push(resource)) * ) + * const observed = [await Effect.runPromise(Channel.runCollect(channel)), released] // => [["resource"], ["resource"]] * ``` * * @category constructors @@ -625,12 +644,12 @@ export const acquireRelease: { * * **Example** (Creating channels from iterators) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * const numbers = [1, 2, 3, 4, 5] * const channel = Channel.fromIterator(() => numbers[Symbol.iterator]()) - * // Emits: 1, 2, 3, 4, 5 + * Effect.runSync(Channel.runCollect(channel)) // => [1, 2, 3, 4, 5] * ``` * * @category constructors @@ -652,11 +671,11 @@ export const fromIterator = (iterator: LazyArg>): Channel [1, 2, 3, 4, 5] * ``` * * @category constructors @@ -673,12 +692,12 @@ export const fromArray = (array: ReadonlyArray): Channel => * * **Example** (Creating channels from chunks) * - * ```ts - * import { Channel, Chunk } from "effect" + * ```ts import.meta.vitest + * import { Channel, Chunk, Effect } from "effect" * * const chunk = Chunk.make(1, 2, 3) * const channel = Channel.fromChunk(chunk) - * // Emits: 1, 2, 3 + * Effect.runSync(Channel.runCollect(channel)) // => [1, 2, 3] * ``` * * @category constructors @@ -691,8 +710,8 @@ export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chu * * **Example** (Batching iterator output) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create a channel from a simple iterator * const numberIterator = (): Iterator => { @@ -708,13 +727,13 @@ export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chu * } * * const channel = Channel.fromIteratorArray(() => numberIterator(), 2) - * // This will emit arrays: [0, 1], [2], then complete with "finished" + * Effect.runSync(Channel.runCollect(channel)) // => [[0, 1], [2]] * ``` * * **Example** (Batching generator output) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create channel from a generator function * function* fibonacci(): Generator { @@ -726,7 +745,7 @@ export const fromChunk = (chunk: Chunk.Chunk): Channel => fromArray(Chu * } * * const fibChannel = Channel.fromIteratorArray(() => fibonacci(), 3) - * // Emits: [0, 1, 1], [2, 3], then completes + * Effect.runSync(Channel.runCollect(fibChannel)) // => [[0, 1, 1], [2, 3]] * ``` * * @category constructors @@ -764,12 +783,12 @@ export const fromIteratorArray = ( * * **Example** (Creating channels from iterables) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * const set = new Set([1, 2, 3]) * const channel = Channel.fromIterable(set) - * // Emits: 1, 2, 3 + * Effect.runSync(Channel.runCollect(channel)) // => [1, 2, 3] * ``` * * @category constructors @@ -783,12 +802,12 @@ export const fromIterable = (iterable: Iterable): Channel [[1, 2, 3, 4], [5]] * ``` * * @category constructors @@ -804,11 +823,11 @@ export const fromIterableArray = ( * * **Example** (Creating channels that succeed) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * const channel = Channel.succeed(42) - * // Emits: 42 + * Effect.runSync(Channel.runCollect(channel)) // => [42] * ``` * * @category constructors @@ -821,11 +840,11 @@ export const succeed = (value: A): Channel => fromEffect(Effect.succeed(va * * **Example** (Ending with a value) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * const channel = Channel.end("done") - * // Ends immediately with "done", emits nothing + * Effect.runSync(Channel.runCollect(channel)) // => [] * ``` * * @category constructors @@ -847,8 +866,8 @@ export const endSync = (evaluate: LazyArg): Channel => * * **Example** (Computing values lazily) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * let requests = 0 * @@ -856,7 +875,7 @@ export const endSync = (evaluate: LazyArg): Channel => * requests += 1 * return `request-${requests}` * }) - * // Emits "request-1" when the channel runs for the first time + * Effect.runSync(Channel.runCollect(channel)) // => ["request-1"] * ``` * * @category constructors @@ -869,8 +888,8 @@ export const sync = (evaluate: LazyArg): Channel => fromEffect(Effect.s * * **Example** (Creating empty channels) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create an empty channel * const emptyChannel = Channel.empty @@ -882,6 +901,8 @@ export const sync = (evaluate: LazyArg): Channel => fromEffect(Effect.s * // Empty channel can be used as a no-op in conditional logic * const conditionalChannel = (shouldEmit: boolean) => * shouldEmit ? Channel.succeed("data") : Channel.empty + * + * Effect.runSync(Channel.runCollect(conditionalChannel(true))) // => ["data"] * ``` * * @category constructors @@ -894,7 +915,7 @@ export const empty: Channel = fromPull(Effect.succeed(Cause.done())) * * **Example** (Creating non-terminating channels) * - * ```ts + * ```ts import.meta.vitest * import { Channel } from "effect" * * // Create a channel that never completes @@ -909,6 +930,8 @@ export const empty: Channel = fromPull(Effect.succeed(Cause.done())) * // Never channel is useful for testing or as a placeholder * const conditionalChannel = (shouldComplete: boolean) => * shouldComplete ? Channel.succeed("done") : Channel.never + * + * Channel.isChannel(conditionalChannel(false)) // => true * ``` * * @category constructors @@ -921,26 +944,11 @@ export const never: Channel = fromPull(Effect.succeed(Effec * * **Example** (Failing with an error) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Exit } from "effect" * - * // Create a channel that fails with a string error * const failedChannel = Channel.fail("Something went wrong") - * - * // Create a channel that fails with a custom error - * class CustomError extends Error { - * constructor(message: string) { - * super(message) - * this.name = "CustomError" - * } - * } - * const customErrorChannel = Channel.fail(new CustomError("Custom error")) - * - * // Use in error handling by piping to another channel - * const channelWithFallback = Channel.concatWith( - * failedChannel, - * () => Channel.succeed("fallback value") - * ) + * Effect.runSync(Effect.exit(Channel.runCollect(failedChannel))) // => Exit.fail("Something went wrong") * ``` * * @category constructors @@ -959,27 +967,18 @@ export const fail = (error: E): Channel => fromPull(Effect.s * * **Example** (Failing with a lazy error) * - * ```ts - * import { Channel } from "effect" - * - * // Create a channel that fails with a lazily computed error - * const failedChannel = Channel.failSync(() => { - * console.log("Computing error...") - * return new Error("Computed at runtime") - * }) + * ```ts import.meta.vitest + * import { Channel, Effect, Exit } from "effect" * - * // The error computation is deferred until the channel runs * let attempts = 0 * const conditionalError = Channel.failSync(() => { * attempts += 1 * return `Error after attempt ${attempts}` * }) - * - * // Use with expensive error construction - * const expensiveError = Channel.failSync(() => { - * const requestId = "request-123" - * return new Error(`Failed while processing ${requestId}`) - * }) + * const observed = [ + * Effect.runSync(Effect.exit(Channel.runCollect(conditionalError))), + * attempts + * ] // => [Exit.fail("Error after attempt 1"), 1] * ``` * * @category constructors @@ -997,20 +996,12 @@ export const failSync = (evaluate: LazyArg): Channel => f * * **Example** (Failing with causes) * - * ```ts - * import { Cause, Channel } from "effect" + * ```ts import.meta.vitest + * import { Cause, Channel, Effect, Exit } from "effect" * - * // Create a channel that fails with a simple cause * const simpleCause = Cause.fail("Simple error") * const failedChannel = Channel.failCause(simpleCause) - * - * // Create a channel with a die cause - * const dieCause = Cause.die(new Error("System error")) - * const dieFailure = Channel.failCause(dieCause) - * - * // Create a channel with a simple fail cause - * const failCause = Cause.fail("Simple error") - * const simpleFail = Channel.failCause(failCause) + * Effect.runSync(Effect.exit(Channel.runCollect(failedChannel))) // => Exit.failCause(simpleCause) * ``` * * @category constructors @@ -1024,8 +1015,8 @@ export const failCause = (cause: Cause.Cause): Channel => * * **Example** (Failing with lazy causes) * - * ```ts - * import { Cause, Channel } from "effect" + * ```ts import.meta.vitest + * import { Cause, Channel, Effect, Exit } from "effect" * * // Create a channel that fails with a lazily computed cause * let attempts = 0 @@ -1034,11 +1025,10 @@ export const failCause = (cause: Cause.Cause): Channel => * return Cause.fail(`Runtime error after attempt ${attempts}`) * }) * - * // Create a channel with die cause computation - * const dieCauseChannel = Channel.failCauseSync(() => { - * const operation = "load-profile" - * return Cause.die(`Unexpected defect during ${operation}`) - * }) + * const observed = [ + * Effect.runSync(Effect.exit(Channel.runCollect(failedChannel))), + * attempts + * ] // => [Exit.fail("Runtime error after attempt 1"), 1] * ``` * * @category constructors @@ -1053,20 +1043,12 @@ export const failCauseSync = ( * * **Example** (Dying with defects) * - * ```ts - * import { Channel } from "effect" - * - * // Create a channel that dies with a string defect - * const diedChannel = Channel.die("Unrecoverable error") - * - * // Create a channel that dies with an Error object - * const errorDefect = Channel.die(new Error("System failure")) + * ```ts import.meta.vitest + * import { Cause, Channel, Effect, Exit } from "effect" * - * // Die with any value as a defect - * const objectDefect = Channel.die({ - * code: "SYSTEM_FAILURE", - * details: "Critical system component failed" - * }) + * const defect = "Unrecoverable error" + * const diedChannel = Channel.die(defect) + * Effect.runSync(Effect.exit(Channel.runCollect(diedChannel))) // => Exit.failCause(Cause.die(defect)) * ``` * * @category constructors @@ -1079,33 +1061,13 @@ export const die = (defect: unknown): Channel => failCause( * * **Example** (Creating channels from effects) * - * ```ts - * import { Channel, Data, Effect } from "effect" - * - * class DatabaseError extends Data.TaggedError("DatabaseError")<{ - * readonly message: string - * }> {} + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * - * // Create a channel from a successful effect * const successChannel = Channel.fromEffect( * Effect.succeed("Hello from effect!") * ) - * - * // Create a channel from an effect that might fail - * const fetchUserChannel = Channel.fromEffect( - * Effect.tryPromise({ - * try: () => fetch("/api/user").then((res) => res.json()), - * catch: (error) => new DatabaseError({ message: String(error) }) - * }) - * ) - * - * // Channel from effect with async computation - * const asyncChannel = Channel.fromEffect( - * Effect.gen(function*() { - * yield* Effect.sleep("100 millis") - * return "Async result" - * }) - * ) + * Effect.runSync(Channel.runCollect(successChannel)) // => ["Hello from effect!"] * ``` * * @category constructors @@ -1173,35 +1135,17 @@ export const fromEffectTake = ( * * **Example** (Creating channels from queues) * - * ```ts - * import { Channel, Data, Effect, Queue } from "effect" - * - * class QueueError extends Data.TaggedError("QueueError")<{ - * readonly reason: string - * }> {} + * ```ts import.meta.vitest + * import { Cause, Channel, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * // Create a bounded queue - * const queue = yield* Queue.bounded(10) - * - * // Add some items to the queue - * yield* Queue.offer(queue, "item1") - * yield* Queue.offer(queue, "item2") - * yield* Queue.offer(queue, "item3") - * - * // Create a channel from the queue + * const queue = yield* Queue.bounded(3) + * yield* Queue.offerAll(queue, ["item1", "item2", "item3"]) + * yield* Queue.end(queue) * const channel = Channel.fromQueue(queue) - * - * // The channel will read items from the queue one by one - * return channel - * }) - * - * // Sliding queue example - * const slidingProgram = Effect.gen(function*() { - * const slidingQueue = yield* Queue.sliding(5) - * yield* Queue.offerAll(slidingQueue, [1, 2, 3, 4, 5, 6]) - * return Channel.fromQueue(slidingQueue) + * return yield* Channel.runCollect(channel) * }) + * await Effect.runPromise(program) // => ["item1", "item2", "item3"] * ``` * * @category constructors @@ -1216,39 +1160,17 @@ export const fromQueue = ( * * **Example** (Creating batched channels from queues) * - * ```ts - * import { Channel, Data, Effect, Queue } from "effect" - * - * class ProcessingError extends Data.TaggedError("ProcessingError")<{ - * readonly stage: string - * }> {} + * ```ts import.meta.vitest + * import { Cause, Channel, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { - * // Create a queue for batch processing - * const queue = yield* Queue.bounded(100) - * - * // Fill queue with data - * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - * - * // Create a channel that reads arrays from the queue + * const queue = yield* Queue.bounded(4) + * yield* Queue.offerAll(queue, [1, 2, 3, 4]) + * yield* Queue.end(queue) * const arrayChannel = Channel.fromQueueArray(queue) - * - * // This will emit non-empty arrays of elements instead of individual items - * // Useful for batch processing scenarios - * return arrayChannel - * }) - * - * // High-throughput processing example - * const batchProcessor = Effect.gen(function*() { - * const dataQueue = yield* Queue.dropping(1000) - * const batchChannel = Channel.fromQueueArray(dataQueue) - * - * // Process data in batches for better performance - * return Channel.map( - * batchChannel, - * (batch) => batch.map((item) => item.toUpperCase()) - * ) + * return yield* Channel.runCollect(arrayChannel) * }) + * await Effect.runPromise(program) // => [[1, 2, 3, 4]] * ``` * * @category constructors @@ -1273,8 +1195,8 @@ export const identity = (): Channel(): Channel Option.some("Hello") * * // Real-time notifications example * const notificationChannel = Effect.gen(function*() { @@ -1327,8 +1251,8 @@ export const fromSubscription = ( * * **Example** (Batching subscription values) * - * ```ts - * import { Channel, Data, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Option, PubSub } from "effect" * * class StreamError extends Data.TaggedError("StreamError")<{ * readonly message: string @@ -1350,12 +1274,14 @@ export const fromSubscription = ( * // The channel will output arrays like [1, 2, 3] and [4] * return channel * }) + * const result = Effect.scoped(Effect.flatMap(program, Channel.runHead)) + * await Effect.runPromise(result) // => Option.some([1, 2, 3, 4]) * ``` * * **Example** (Processing subscription values in batches) * - * ```ts - * import { Channel, Data, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Option, PubSub } from "effect" * * class BatchProcessingError extends Data.TaggedError("BatchProcessingError")<{ * readonly reason: string @@ -1369,19 +1295,21 @@ export const fromSubscription = ( * const batchChannel = Channel.fromSubscriptionArray(subscription) * * // Transform to process each batch - * const processedChannel = Channel.map(batchChannel, (batch) => { - * console.log(`Processing batch of ${batch.length} items:`, batch) - * return batch.map((item) => item.toUpperCase()) - * }) + * const processedChannel = Channel.map(batchChannel, (batch) => + * batch.map((item) => item.toUpperCase()) + * ) * + * yield* PubSub.publishAll(pubsub, ["one", "two"]) * return processedChannel * }) + * const batch = Effect.scoped(Effect.flatMap(batchProcessor, Channel.runHead)) + * await Effect.runPromise(batch) // => Option.some(["ONE", "TWO"]) * ``` * * **Example** (Aggregating subscription metrics) * - * ```ts - * import { Channel, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Option, PubSub } from "effect" * * const metricsAggregator = Effect.gen(function*() { * const metricsPubSub = yield* PubSub.bounded< @@ -1411,8 +1339,12 @@ export const fromSubscription = ( * } * }) * + * yield* PubSub.publish(metricsPubSub, { timestamp: 1, value: 10 }) * return aggregatedChannel * }) + * const metric = Effect.scoped(Effect.flatMap(metricsAggregator, Channel.runHead)) + * const result = await Effect.runPromise(metric) + * Option.map(result, ({ count, sum, average, min, max }) => ({ count, sum, average, min, max })) // => Option.some({ count: 1, sum: 10, average: 10, min: 10, max: 10 }) * ``` * * @category constructors @@ -1434,15 +1366,15 @@ export const fromSubscriptionArray = ( * * **Example** (Creating channels from PubSubs) * - * ```ts - * import { Channel, Data, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Option, PubSub } from "effect" * * class StreamError extends Data.TaggedError("StreamError")<{ * readonly message: string * }> {} * * const program = Effect.gen(function*() { - * const pubsub = yield* PubSub.bounded(16) + * const pubsub = yield* PubSub.unbounded({ replay: 3 }) * * // Create a channel that reads individual values * const channel = Channel.fromPubSub(pubsub) @@ -1455,15 +1387,17 @@ export const fromSubscriptionArray = ( * // The channel will output: 1, 2, 3 (individual values) * return channel * }) + * const result = Effect.scoped(Effect.flatMap(program, Channel.runHead)) + * await Effect.runPromise(result) // => Option.some(1) * ``` * * **Example** (Streaming PubSub notifications) * - * ```ts - * import { Channel, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Option, PubSub } from "effect" * * const notificationService = Effect.gen(function*() { - * const notificationPubSub = yield* PubSub.bounded(50) + * const notificationPubSub = yield* PubSub.unbounded({ replay: 1 }) * * // Create a channel for real-time notifications * const notificationChannel = Channel.fromPubSub(notificationPubSub) @@ -1476,14 +1410,17 @@ export const fromSubscriptionArray = ( * id: `notification:${message}` * })) * + * yield* PubSub.publish(notificationPubSub, "ready") * return timestampedChannel * }) + * const notification = Effect.scoped(Effect.flatMap(notificationService, Channel.runHead)) + * await Effect.runPromise(notification) // => Option.some({ message: "ready", receivedAt: "2024-01-01T00:00:00.000Z", id: "notification:ready" }) * ``` * * **Example** (Processing PubSub events) * - * ```ts - * import { Channel, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Option, PubSub } from "effect" * * interface DomainEvent { * readonly type: string @@ -1492,7 +1429,7 @@ export const fromSubscriptionArray = ( * } * * const eventProcessor = Effect.gen(function*() { - * const eventPubSub = yield* PubSub.bounded(100) + * const eventPubSub = yield* PubSub.unbounded({ replay: 1 }) * * // Create a channel for processing domain events * const eventChannel = Channel.fromPubSub(eventPubSub) @@ -1509,8 +1446,11 @@ export const fromSubscriptionArray = ( * return event * }) * + * yield* PubSub.publish(eventPubSub, { type: "user.created", payload: {}, timestamp: 1 }) * return processedChannel * }) + * const event = Effect.scoped(Effect.flatMap(eventProcessor, Channel.runHead)) + * const result = await Effect.runPromise(event) // => Option.some({ type: "user.created", payload: {}, timestamp: 1, processed: true, processedAt: 2 }) * ``` * * @category constructors @@ -1531,15 +1471,15 @@ export const fromPubSub = ( * * **Example** (Batching PubSub values) * - * ```ts - * import { Channel, Data, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Option, PubSub } from "effect" * * class BatchError extends Data.TaggedError("BatchError")<{ * readonly message: string * }> {} * * const program = Effect.gen(function*() { - * const pubsub = yield* PubSub.bounded(16) + * const pubsub = yield* PubSub.unbounded({ replay: 4 }) * * // Create a channel that reads arrays of values * const channel = Channel.fromPubSubArray(pubsub) @@ -1553,12 +1493,14 @@ export const fromPubSub = ( * // The channel will output arrays like [1, 2, 3] and [4] * return channel * }) + * const result = Effect.scoped(Effect.flatMap(program, Channel.runHead)) + * await Effect.runPromise(result) // => Option.some([1, 2, 3, 4]) * ``` * * **Example** (Processing PubSub orders in batches) * - * ```ts - * import { Channel, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Option, PubSub } from "effect" * * interface Order { * readonly id: string @@ -1569,7 +1511,7 @@ export const fromPubSub = ( * } * * const orderBatchProcessor = Effect.gen(function*() { - * const orderPubSub = yield* PubSub.bounded(100) + * const orderPubSub = yield* PubSub.unbounded({ replay: 1 }) * * // Create a channel that processes orders in batches * const orderChannel = Channel.fromPubSubArray(orderPubSub) @@ -1590,14 +1532,20 @@ export const fromPubSub = ( * } * }) * + * yield* PubSub.publish(orderPubSub, { + * id: "1", customerId: "a", items: ["book"], total: 10, submittedAt: 1 + * }) * return processedChannel * }) + * const order = Effect.scoped(Effect.flatMap(orderBatchProcessor, Channel.runHead)) + * const result = await Effect.runPromise(order) + * Option.map(result, (batch) => [batch.batchSize, batch.totalRevenue, batch.uniqueCustomers]) // => Option.some([1, 10, 1]) * ``` * * **Example** (Processing PubSub logs in batches) * - * ```ts - * import { Channel, Effect, PubSub } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Option, PubSub } from "effect" * * interface LogEntry { * readonly timestamp: number @@ -1607,7 +1555,7 @@ export const fromPubSub = ( * } * * const logAggregator = Effect.gen(function*() { - * const logPubSub = yield* PubSub.bounded(500) + * const logPubSub = yield* PubSub.unbounded({ replay: 1 }) * * // Create a channel that collects logs in batches * const logChannel = Channel.fromPubSubArray(logPubSub) @@ -1634,8 +1582,17 @@ export const fromPubSub = ( * } * }) * + * yield* PubSub.publish(logPubSub, { + * timestamp: 1, + * level: "info", + * message: "ready", + * source: "app" + * } satisfies LogEntry) * return analysisChannel * }) + * const log = Effect.scoped(Effect.flatMap(logAggregator, Channel.runHead)) + * const result = await Effect.runPromise(log) + * Option.map(result, (batch) => [batch.batchId, batch.totalEntries, batch.infoCount]) // => Option.some(["1-1", 1, 1]) * ``` * * @category constructors @@ -1671,6 +1628,229 @@ export const fromSchedule = ( ): Channel => fromPull(Effect.map(Schedule.toStepWithSleep(schedule), (step) => step(void 0))) +/** + * Creates a channel from a lazily supplied Web `ReadableStream`. + * + * **Example** (Reading from a Web stream) + * + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" + * + * const channel = Channel.fromReadableStream({ + * evaluate: () => new ReadableStream({ + * start(controller) { + * controller.enqueue(1) + * controller.close() + * } + * }), + * onError: (cause) => new Error(String(cause)) + * }) + * + * await Effect.runPromise(Channel.runCollect(channel)) // => [[1]] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromReadableStream = (options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined +}): Channel, E> => + fromTransform((_, scope) => + readableStreamToPullUnsafe({ + scope, + readable: options.evaluate(), + onError: options.onError, + releaseLockOnEnd: options.releaseLockOnEnd + }) + ) + +/** @internal */ +export const pullIntoWritableStream = (options: { + readonly pull: Pull.Pull, IE, unknown> + readonly writable: WritableStream + readonly onError: (error: unknown) => E + readonly closeOnDone?: boolean | undefined +}): Pull.Pull => + Effect.acquireUseRelease( + Effect.sync(() => options.writable.getWriter()), + (writer) => { + const loop = options.pull.pipe( + Effect.flatMap((chunk) => + Effect.forEach( + chunk, + (value) => + Effect.tryPromise({ + try: () => writer.ready.then(() => writer.write(value)), + catch: options.onError + }), + { discard: true } + ) + ), + Effect.forever({ disableYield: true }) + ) + const withClose = options.closeOnDone !== false + ? Pull.catchDone(loop, (done) => + Effect.andThen( + Effect.tryPromise({ + try: () => writer.close(), + catch: options.onError + }), + Cause.done(done) + )) + : loop + return Effect.onError( + withClose, + (cause) => + Pull.isDoneCause(cause) + ? Effect.void + : Effect.promise(() => writer.abort(cause).catch(constVoid)) + ) + }, + (writer) => Effect.sync(() => writer.releaseLock()) + ) + +/** + * Creates a channel that writes upstream values to a lazily supplied Web + * `WritableStream`. + * + * **Example** (Writing channel input) + * + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" + * + * const written: Array = [] + * const sink = Channel.fromWritableStream({ + * evaluate: () => new WritableStream({ + * write(value) { + * written.push(value) + * } + * }), + * onError: (cause) => new Error(String(cause)) + * }) + * + * const program = Channel.fromArray([[1, 2] as [number, number]]).pipe( + * Channel.pipeTo(sink), + * Channel.runDrain + * ) + * + * await Effect.runPromise(program) + * written // => [1, 2] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromWritableStream = (options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly closeOnDone?: boolean | undefined +}): Channel, IE> => + fromTransform((pull: Pull.Pull, IE, unknown>) => { + const writable = options.evaluate() + return Effect.succeed(pullIntoWritableStream({ ...options, writable, pull })) + }) + +/** + * Creates a channel backed by a Web `TransformStream`, writing upstream values + * while emitting transformed values from its readable side. + * + * **Example** (Transforming channel input) + * + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" + * + * const transform = Channel.fromTransformStream({ + * evaluate: () => new TransformStream({ + * transform(value, controller) { + * controller.enqueue(value * 2) + * } + * }), + * onError: (cause) => new Error(String(cause)) + * }) + * + * const program = Channel.fromArray([[1, 2] as [number, number]]).pipe( + * Channel.pipeTo(transform), + * Channel.runCollect + * ) + * + * await Effect.runPromise(program) // => [[2], [4]] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromTransformStream = (options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly closeOnDone?: boolean | undefined + readonly releaseLockOnEnd?: boolean | undefined +}): Channel, IE | E, void, Arr.NonEmptyReadonlyArray, IE> => + fromTransform((upstream, scope) => { + const transform = options.evaluate() + const exit = MutableRef.make | undefined>(undefined) + return pullIntoWritableStream({ + pull: upstream, + writable: transform.writable, + onError: options.onError, + closeOnDone: options.closeOnDone + }).pipe( + Effect.catchCause((cause) => { + if (!Pull.isDoneCause(cause)) { + exit.current = Exit.failCause(cause as Cause.Cause) + } + return Effect.void + }), + Effect.forkIn(scope), + Effect.flatMap(() => + readableStreamToPullUnsafe({ + scope, + exit, + readable: transform.readable, + onError: options.onError, + releaseLockOnEnd: options.releaseLockOnEnd + }) + ) + ) + }) + +const readableStreamToPullUnsafe = (options: { + readonly scope: Scope.Scope + readonly exit?: MutableRef.MutableRef | undefined> | undefined + readonly readable: ReadableStream + readonly onError: (error: unknown) => E + readonly releaseLockOnEnd?: boolean | undefined +}): Effect.Effect, E | E2>, never> => { + const reader = options.readable.getReader() + const exit = options.exit ?? MutableRef.make(undefined) + const pull = Effect.suspend(() => { + if (exit.current) return exit.current + return Effect.matchCauseEffect( + Effect.tryPromise({ + try: () => reader.read(), + catch: options.onError + }), + { + onFailure: (cause) => exit.current ?? Effect.failCause(cause), + onSuccess: ({ done, value }) => { + if (exit.current) return exit.current + return done ? Cause.done() : Effect.succeed(Arr.of(value)) + } + } + ) + }) + return Effect.as( + Scope.addFinalizer( + options.scope, + options.releaseLockOnEnd + ? Effect.sync(() => reader.releaseLock()) + : Effect.promise(() => reader.cancel().catch(constVoid)) + ), + pull + ) +} + /** * Creates a channel that pulls values from an `AsyncIterable`. * @@ -1725,8 +1905,8 @@ export const fromAsyncIterableArray = ( * * **Example** (Mapping channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class TransformError extends Data.TaggedError("TransformError")<{ * readonly reason: string @@ -1735,12 +1915,12 @@ export const fromAsyncIterableArray = ( * // Basic mapping of channel values * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) * const doubledChannel = Channel.map(numbersChannel, (n) => n * 2) - * // Outputs: 2, 4, 6, 8, 10 + * Effect.runSync(Channel.runCollect(doubledChannel)) // => [2, 4, 6, 8, 10] * * // Transform string data * const wordsChannel = Channel.fromIterable(["hello", "world", "effect"]) * const upperCaseChannel = Channel.map(wordsChannel, (word) => word.toUpperCase()) - * // Outputs: "HELLO", "WORLD", "EFFECT" + * Effect.runSync(Channel.runCollect(upperCaseChannel)) // => ["HELLO", "WORLD", "EFFECT"] * * // Complex object transformation * type User = { id: number; name: string } @@ -1754,6 +1934,7 @@ export const fromAsyncIterableArray = ( * displayName: `User: ${user.name}`, * isActive: true * })) + * Effect.runSync(Channel.runCollect(displayChannel)) // => [{ displayName: "User: Alice", isActive: true }, { displayName: "User: Bob", isActive: true }] * ``` * * @category sequencing @@ -1862,40 +2043,15 @@ const concurrencyIsSequential = ( * * **Example** (Mapping channel output with effects) * - * ```ts - * import { Channel, Data, Effect } from "effect" - * - * class NetworkError extends Data.TaggedError("NetworkError")<{ - * readonly url: string - * }> {} - * - * // Transform values using effectful operations - * const urlsChannel = Channel.fromIterable([ - * "/api/users/1", - * "/api/users/2", - * "/api/users/3" - * ]) - * - * const fetchDataChannel = Channel.mapEffect( - * urlsChannel, - * (url) => - * Effect.tryPromise({ - * try: () => fetch(url).then((res) => res.json()), - * catch: () => new NetworkError({ url }) - * }) - * ) + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * - * // Concurrent processing with options * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) * const processedChannel = Channel.mapEffect( * numbersChannel, - * (n) => - * Effect.gen(function*() { - * yield* Effect.sleep("100 millis") // Simulate async work - * return n * n - * }), - * { concurrency: 3, unordered: true } + * (n) => Effect.succeed(n * n) * ) + * await Effect.runPromise(Channel.runCollect(processedChannel)) // => [1, 4, 9, 16, 25] * ``` * * @category sequencing @@ -2124,8 +2280,8 @@ export const mapInputError: { * * **Example** (Tapping channel output) * - * ```ts - * import { Channel, Console, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class LogError extends Data.TaggedError("LogError")<{ * readonly message: string @@ -2135,13 +2291,13 @@ export const mapInputError: { * const numberChannel = Channel.fromIterable([1, 2, 3]) * * // Tap into each output element to perform side effects + * const processed: Array = [] * const tappedChannel = Channel.tap( * numberChannel, - * (n) => Console.log(`Processing number: ${n}`) + * (n) => Effect.sync(() => processed.push(n)) * ) * - * // The channel still outputs the same elements but logs each one - * // Outputs: 1, 2, 3 (while logging each) + * const observed = [await Effect.runPromise(Channel.runCollect(tappedChannel)), processed] // => [[1, 2, 3], [1, 2, 3]] * ``` * * @category sequencing @@ -2188,8 +2344,8 @@ export const tap: { * * **Example** (Flat mapping channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class ProcessError extends Data.TaggedError("ProcessError")<{ * readonly cause: string @@ -2205,8 +2361,7 @@ export const tap: { * Channel.fromIterable(Array.from({ length: n }, (_, i) => `item-${n}-${i}`)) * ) * - * // Flattens nested channels into a single stream - * // Outputs: "item-1-0", "item-2-0", "item-2-1", "item-3-0", "item-3-1", "item-3-2" + * Effect.runSync(Channel.runCollect(flatMappedChannel)) // => ["item-1-0", "item-2-0", "item-2-1", "item-3-0", "item-3-1", "item-3-2"] * ``` * * @category sequencing @@ -2394,8 +2549,8 @@ const flatMapConcurrent = < * * **Example** (Concatenating with completion values) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class ConcatError extends Data.TaggedError("ConcatError")<{ * readonly reason: string @@ -2406,8 +2561,7 @@ const flatMapConcurrent = < * Channel.concatWith((sum: void) => Channel.succeed(`Completed processing`)) * ) * - * // Concatenates additional channel based on completion value - * // Outputs: 1, 2, 3, then "Completed processing" + * Effect.runSync(Channel.runCollect(numberChannel)) // => [1, 2, 3, "Completed processing"] * ``` * * @category sequencing @@ -2507,8 +2661,8 @@ export const concatWith: { * * **Example** (Concatenating channels) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class ConcatError extends Data.TaggedError("ConcatError")<{ * readonly reason: string @@ -2521,7 +2675,7 @@ export const concatWith: { * // Concatenate them * const concatenatedChannel = Channel.concat(firstChannel, secondChannel) * - * // Outputs: 1, 2, 3, "a", "b", "c" + * Effect.runSync(Channel.runCollect(concatenatedChannel)) // => [1, 2, 3, "a", "b", "c"] * ``` * * @category sequencing @@ -2831,8 +2985,8 @@ export const orElseIfEmpty: { * * **Example** (Flattening nested channels) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class FlattenError extends Data.TaggedError("FlattenError")<{ * readonly cause: string @@ -2848,7 +3002,7 @@ export const orElseIfEmpty: { * // Flatten the nested channels * const flattenedChannel = Channel.flatten(nestedChannels) * - * // Outputs: 1, 2, 3, 4, 5, 6 + * Effect.runSync(Channel.runCollect(flattenedChannel)) // => [1, 2, 3, 4, 5, 6] * ``` * * @category constructors @@ -2886,8 +3040,8 @@ export const flatten = < * * **Example** (Flattening arrays of channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class FlattenError extends Data.TaggedError("FlattenError")<{ * readonly message: string @@ -2903,7 +3057,7 @@ export const flatten = < * // Flatten the arrays into individual elements * const flattenedChannel = Channel.flattenArray(arrayChannel) * - * // Outputs: 1, 2, 3, 4, 5, 6, 7, 8, 9 + * Effect.runSync(Channel.runCollect(flattenedChannel)) // => [1, 2, 3, 4, 5, 6, 7, 8, 9] * ``` * * @category transforming @@ -2981,8 +3135,8 @@ export const flattenTake = < * * **Example** (Draining channel output) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create a channel that outputs values * const sourceChannel = Channel.fromIterable([1, 2, 3, 4, 5]) @@ -2990,8 +3144,7 @@ export const flattenTake = < * // Drain all output, keeping only the completion * const drainedChannel = Channel.drain(sourceChannel) * - * // The channel completes but emits no values - * // Useful for consuming side effects without collecting output + * Effect.runSync(Channel.runCollect(drainedChannel)) // => [] * ``` * * @category constructors @@ -3139,15 +3292,15 @@ export const schedule: { * * **Example** (Filtering channel output) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create a channel with mixed numbers * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5, 6, 7, 8]) * * // Filter to keep only even numbers * const evenChannel = Channel.filter(numbersChannel, (n) => n % 2 === 0) - * // Outputs: 2, 4, 6, 8 + * Effect.runSync(Channel.runCollect(evenChannel)) // => [2, 4, 6, 8] * * // Filter with type refinement * const mixedChannel = Channel.fromIterable([1, "hello", 2, "world", 3]) @@ -3155,7 +3308,7 @@ export const schedule: { * mixedChannel, * (value): value is number => typeof value === "number" * ) - * // Outputs: 1, 2, 3 (all typed as numbers) + * Effect.runSync(Channel.runCollect(numbersOnlyChannel)) // => [1, 2, 3] * ``` * * @category filtering @@ -3349,8 +3502,8 @@ export const filterMapEffect: { * * **Example** (Filtering array output) * - * ```ts - * import { Array, Channel } from "effect" + * ```ts import.meta.vitest + * import { Array, Channel, Effect } from "effect" * * const nonEmptyArrayPredicate = Array.isReadonlyArrayNonEmpty * @@ -3363,7 +3516,7 @@ export const filterMapEffect: { * * // Filter arrays to keep only even numbers * const evenArraysChannel = Channel.filterArray(arrayChannel, (n) => n % 2 === 0) - * // Outputs: [2, 4], [6, 8, 10], [12, 14] + * Effect.runSync(Channel.runCollect(evenArraysChannel)) // => [[2, 4], [6, 8, 10], [12, 14]] * // Note: Only non-empty filtered arrays are emitted * * // Arrays that would become empty after filtering are discarded entirely @@ -3373,7 +3526,7 @@ export const filterMapEffect: { * Array.make(7, 9) * ]).pipe(Channel.filter(nonEmptyArrayPredicate)) * const filteredOddChannel = Channel.filterArray(oddChannel, (n) => n % 2 === 0) - * // Outputs: [2, 4] (the arrays [1,3,5] and [7,9] are discarded) + * Effect.runSync(Channel.runCollect(filteredOddChannel)) // => [[2, 4]] * ``` * * @category filtering @@ -3554,7 +3707,7 @@ export const filterMapArrayEffect: { * * **Example** (Mapping with accumulated state) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect } from "effect" * * // Create a channel with numbers @@ -3570,8 +3723,6 @@ export const filterMapArrayEffect: { * return [newSum, [current, newSum]] as const * } * ) - * // Outputs: 1, 1, 2, 3, 3, 6, 4, 10 - * * // Using with Effect for async processing * const asyncMapAccum = Channel.mapAccum( * numbersChannel, @@ -3582,6 +3733,8 @@ export const filterMapArrayEffect: { * return [newAcc, [`${value}-processed`, newAcc]] as const * }) * ) + * Effect.runSync(Channel.runCollect(runningSum)) // => [1, 1, 2, 3, 3, 6, 4, 10] + * Effect.runSync(Channel.runCollect(asyncMapAccum)) // => ["1-processed", "1", "2-processed", "12", "3-processed", "123", "4-processed", "1234"] * ``` * * @category sequencing @@ -3696,15 +3849,15 @@ export const mapAccum: { * * **Example** (Scanning channel output) * - * ```ts - * import { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create a channel with numbers * const numbersChannel = Channel.fromIterable([1, 2, 3, 4, 5]) * * // Scan to create running sum * const runningSumChannel = Channel.scan(numbersChannel, 0, (sum, n) => sum + n) - * // Outputs: 0, 1, 3, 6, 10, 15 + * Effect.runSync(Channel.runCollect(runningSumChannel)) // => [0, 1, 3, 6, 10, 15] * // Note: emits the initial value and each intermediate result * * // Scan with string concatenation @@ -3714,7 +3867,7 @@ export const mapAccum: { * "", * (sentence, word) => sentence === "" ? word : `${sentence} ${word}` * ) - * // Outputs: "", "hello", "hello world", "hello world from", "hello world from effect" + * Effect.runSync(Channel.runCollect(sentenceChannel)) // => ["", "hello", "hello world", "hello world from", "hello world from effect"] * ``` * * @category sequencing @@ -3760,7 +3913,7 @@ export const scan: { * * **Example** (Scanning channel output with effects) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Data, Effect } from "effect" * * class ScanError extends Data.TaggedError("ScanError")<{ @@ -3776,12 +3929,10 @@ export const scan: { * "", * (acc, value) => * Effect.gen(function*() { - * // Simulate async work - * yield* Effect.sleep("10 millis") * return acc + value.toString() * }) * ) - * // Outputs: "", "1", "12", "123", "1234" + * await Effect.runPromise(Channel.runCollect(asyncScanChannel)) // => ["", "1", "12", "123", "1234"] * * // Scan with error handling * const errorHandlingScan = Channel.scanEffect( @@ -3794,6 +3945,7 @@ export const scan: { * return Effect.succeed(sum + n) * } * ) + * await Effect.runPromise(Channel.runCollect(errorHandlingScan)) // => [0, 1, 3, 6, 10] * ``` * * @category sequencing @@ -3852,8 +4004,8 @@ export const scanEffect: { * * **Example** (Recovering from failure causes) * - * ```ts - * import { Cause, Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Cause, Channel, Data, Effect } from "effect" * * class ProcessError extends Data.TaggedError("ProcessError")<{ * readonly reason: string @@ -3876,7 +4028,7 @@ export const scanEffect: { * return Channel.succeed("Recovered from interruption") * }) * - * // The channel recovers gracefully from errors + * Effect.runSync(Channel.runCollect(recoveredChannel)) // => ["Recovered from failure"] * ``` * * @category error handling @@ -5072,8 +5224,8 @@ export const catchTag: { * * **Example** (Recovering from nested reasons) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ * retryAfter: number @@ -5087,15 +5239,15 @@ export const catchTag: { * reason: RateLimitError | QuotaExceededError * }> {} * - * const channel = Channel.fail( - * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) - * ) + * const reason = new RateLimitError({ retryAfter: 60 }) + * const channel = Channel.fail(new AiError({ reason })) * * const recovered = channel.pipe( * Channel.catchReason("AiError", "RateLimitError", (reason) => * Channel.succeed(`retry: ${reason.retryAfter}`) * ) * ) + * Effect.runSync(Channel.runCollect(recovered)) // => ["retry: 60"] * ``` * * @category error handling @@ -5457,8 +5609,8 @@ export const catchReasons: { * * **Example** (Promoting nested reasons) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Exit } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ * retryAfter: number @@ -5472,11 +5624,11 @@ export const catchReasons: { * reason: RateLimitError | QuotaExceededError * }> {} * - * const channel = Channel.fail( - * new AiError({ reason: new RateLimitError({ retryAfter: 60 }) }) - * ) + * const reason = new RateLimitError({ retryAfter: 60 }) + * const channel = Channel.fail(new AiError({ reason })) * * const unwrapped = channel.pipe(Channel.unwrapReason("AiError")) + * Effect.runSync(Effect.exit(Channel.runCollect(unwrapped))) // => Exit.fail(reason) * ``` * * @category error handling @@ -5577,20 +5729,21 @@ export const mapError: { * * **Example** (Converting failures to defects) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Cause, Channel, Data, Effect, Exit } from "effect" * * class ValidationError extends Data.TaggedError("ValidationError")<{ * readonly field: string * }> {} * * // Create a channel that might fail - * const failingChannel = Channel.fail(new ValidationError({ field: "email" })) + * const error = new ValidationError({ field: "email" }) + * const failingChannel = Channel.fail(error) * * // Convert failures to defects * const fatalChannel = Channel.orDie(failingChannel) * - * // Any failure will now become a defect (uncaught exception) + * Effect.runSync(Effect.exit(Channel.runCollect(fatalChannel))) // => Exit.failCause(Cause.die(error)) * ``` * * @category error handling @@ -5791,8 +5944,8 @@ export const retry: { * * **Example** (Switching mapped channels) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class SwitchError extends Data.TaggedError("SwitchError")<{ * readonly reason: string @@ -5807,7 +5960,7 @@ export const retry: { * (n) => Channel.fromIterable([`value-${n}`]) * ) * - * // Outputs: "value-1", "value-2", "value-3" + * await Effect.runPromise(Channel.runCollect(switchedChannel)) // => ["value-3"] * ``` * * @category sequencing @@ -5915,8 +6068,8 @@ export const switchMap: { * * **Example** (Merging nested channels) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class MergeAllError extends Data.TaggedError("MergeAllError")<{ * readonly reason: string @@ -5931,11 +6084,11 @@ export const switchMap: { * * // Merge all channels with bounded concurrency * const mergedChannel = Channel.mergeAll({ - * concurrency: 2, + * concurrency: 1, * bufferSize: 16 * })(nestedChannels) * - * // Outputs: 1, 2, 3, 4, 5, 6 (order may vary due to concurrency) + * await Effect.runPromise(Channel.runCollect(mergedChannel)) // => [1, 2, 3, 4, 5, 6] * ``` * * @category combining @@ -6033,8 +6186,21 @@ export const mergeAll: { yield* Effect.gen(function*() { while (true) { - if (semaphore) yield* semaphore.take(1) - const channel = yield* pull + let pullFiber: Fiber.Fiber, any> | undefined + if (semaphore) { + if (fibers.size < concurrencyN) { + yield* semaphore.take(1) + } else { + pullFiber = yield* Effect.forkChild(pull) + yield* Effect.raceFirst( + semaphore.take(1), + Effect.andThen(Fiber.join(pullFiber), Effect.never) + ) + } + } + const channel = pullFiber === undefined + ? yield* pull + : yield* Fiber.join(pullFiber) const childScope = Scope.forkUnsafe(forkedScope) const childPull = yield* toTransform(channel)(upstream, childScope) @@ -6069,7 +6235,13 @@ export const mergeAll: { fibers.add(fiber) } }).pipe( - Effect.catchCause((cause) => doneLatch.whenOpen(Queue.failCause(queue, cause))), + Effect.catchCause((cause) => { + const halt = Pull.filterDone(cause) + if (Result.isSuccess(halt)) { + return doneLatch.whenOpen(Queue.failCause(queue, cause)) + } + return Queue.failCause(queue, cause) + }), Effect.forkIn(forkedScope) ) @@ -6083,14 +6255,11 @@ export const mergeAll: { * * **Example** (Choosing merge halt strategies) * - * ```ts - * import type { Channel } from "effect" + * ```ts import.meta.vitest + * import { Channel } from "effect" * * // Different halt strategies for channel merging - * const leftFirst: Channel.HaltStrategy = "left" // Stop when left channel halts - * const rightFirst: Channel.HaltStrategy = "right" // Stop when right channel halts - * const both: Channel.HaltStrategy = "both" // Stop when both channels halt - * const either: Channel.HaltStrategy = "either" // Stop when either channel halts + * const strategies: Array = ["left", "right", "both", "either"] // => ["left", "right", "both", "either"] * ``` * * @category models @@ -6104,24 +6273,18 @@ export type HaltStrategy = "left" | "right" | "both" | "either" * * **Example** (Merging channels) * - * ```ts - * import { Channel, Data } from "effect" - * - * class MergeError extends Data.TaggedError("MergeError")<{ - * readonly source: string - * }> {} + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" * * // Create two channels * const leftChannel = Channel.fromIterable([1, 2, 3]) * const rightChannel = Channel.fromIterable(["a", "b", "c"]) * - * // Merge them with "either" halt strategy - * const mergedChannel = Channel.merge(leftChannel, rightChannel, { - * haltStrategy: "either" - * }) + * // The default "both" strategy waits for both channels to complete + * const mergedChannel = Channel.merge(leftChannel, rightChannel) * - * // Outputs elements from both channels concurrently - * // Order may vary: 1, "a", 2, "b", 3, "c" + * const values = await Effect.runPromise(Channel.runCollect(mergedChannel)) + * values.map(String).sort() // => ["1", "2", "3", "a", "b", "c"] * ``` * * @category combining @@ -6314,19 +6477,16 @@ export const mergeEffect: { * * **Example** (Splitting string chunks into lines) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function*() { - * const result = yield* Stream.runCollect( - * Stream.splitLines(Stream.make("hel", "lo\r\nwor", "ld\n")) - * ) - * console.log(result) - * // [ 'hello', 'world' ] - * })) + * const result = await Effect.runPromise(Stream.runCollect( + * Stream.splitLines(Stream.make("hel", "lo\r\nwor", "ld\n")) + * )) + * result // => ["hello", "world"] * ``` * - * @category String manipulation + * @category splitting * @since 2.0.0 */ export const splitLines = (): Channel< @@ -6440,7 +6600,7 @@ export const splitLines = (): Channel< * span `Uint8Array` boundaries. The optional `encoding` and `options` are * passed to `TextDecoder`. * - * @category String manipulation + * @category decoding * @since 4.0.0 */ export const decodeText = (encoding?: string, options?: TextDecoderOptions): Channel< @@ -6466,7 +6626,7 @@ export const decodeText = (encoding?: string, options?: TextDecoderOp * * Each string inside an emitted array is encoded independently. * - * @category String manipulation + * @category encoding * @since 4.0.0 */ export const encodeText = (): Channel< @@ -6492,8 +6652,8 @@ export const encodeText = (): Channel< * * **Example** (Piping one channel into another) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class PipeError extends Data.TaggedError("PipeError")<{ * readonly stage: string @@ -6506,7 +6666,7 @@ export const encodeText = (): Channel< * // Pipe the source into the transform * const pipedChannel = Channel.pipeTo(sourceChannel, transformChannel) * - * // Outputs: 2, 4, 6 + * Effect.runSync(Channel.runCollect(pipedChannel)) // => [2, 4, 6] * ``` * * @category sequencing @@ -6540,21 +6700,22 @@ export const pipeTo: { * * **Example** (Piping while preserving failures) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Exit } from "effect" * * class SourceError extends Data.TaggedError("SourceError")<{ * readonly code: number * }> {} * * // Create a failing source channel - * const failingSource = Channel.fail(new SourceError({ code: 404 })) - * const safeTransform = Channel.succeed("transformed") + * const error = new SourceError({ code: 404 }) + * const failingSource = Channel.fail(error) + * const safeTransform = Channel.identity() * * // Pipe while preserving source failures * const safePipedChannel = Channel.pipeToOrFail(failingSource, safeTransform) * - * // Source errors are preserved and not sent to transform channel + * Effect.runSync(Effect.exit(Channel.runCollect(safePipedChannel))) // => Exit.fail(error) * ``` * * @category sequencing @@ -6602,7 +6763,7 @@ export const pipeToOrFail: { * * **Example** (Unwrapping channel effects) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Data, Effect } from "effect" * * class UnwrapError extends Data.TaggedError("UnwrapError")<{ @@ -6617,7 +6778,7 @@ export const pipeToOrFail: { * // Unwrap the effect to get the channel * const unwrappedChannel = Channel.unwrap(channelEffect) * - * // The resulting channel outputs: 1, 2, 3 + * Effect.runSync(Channel.runCollect(unwrappedChannel)) // => [1, 2, 3] * ``` * * @category constructors @@ -6666,7 +6827,7 @@ export const scoped = ( * * **Example** (Embedding custom input handling) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Effect } from "effect" * * // Create a base channel @@ -6677,13 +6838,11 @@ export const scoped = ( * baseChannel, * (upstream) => * upstream.pipe( - * Effect.tap((message) => - * Effect.sync(() => console.log(message)) - * ), * Effect.forever, * Effect.ignore * ) * ) + * await Effect.runPromise(Channel.runCollect(embeddedChannel)) // => [1, 2, 3] * ``` * * @category sequencing @@ -6741,7 +6900,7 @@ export const embedInput: { * * @see {@link bufferArray} for buffering elements from array outputs * - * @category Buffering + * @category buffering * @since 2.0.0 */ export const buffer: { @@ -6807,7 +6966,7 @@ export const buffer: { * * @see {@link buffer} for buffering output elements without flattening arrays * - * @category Buffering + * @category buffering * @since 4.0.0 */ export const bufferArray: { @@ -6915,17 +7074,16 @@ export const haltWhen: { ): Channel => fromTransformBracket(Effect.fnUntraced(function*(upstream, scope, forkedScope) { const pull = yield* toTransform(self)(upstream, scope) - let haltCause: Cause.Cause> | undefined = undefined - yield* effect.pipe( - Effect.catchCause((cause) => { - haltCause = cause - return Effect.void - }), - Effect.forkIn(forkedScope) - ) - return Effect.suspend((): Pull.Pull => - haltCause ? Effect.failCause(haltCause) : pull - ) + const fiber = yield* Effect.forkIn(effect, forkedScope, { startImmediately: true }) + return Effect.suspend((): Pull.Pull => { + const exit = fiber.pollUnsafe() + return exit === undefined + ? pull + : Exit.match(exit, { + onFailure: Effect.failCause, + onSuccess: Cause.done + }) + }) }))) /** @@ -6961,8 +7119,8 @@ export const onError: { * * **Example** (Running exit finalizers) * - * ```ts - * import { Channel, Console, Data, Exit } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Exit } from "effect" * * class ExitError extends Data.TaggedError("ExitError")<{ * readonly stage: string @@ -6972,13 +7130,12 @@ export const onError: { * const dataChannel = Channel.fromIterable([1, 2, 3]) * * // Attach exit handler + * const exits: Array> = [] * const channelWithExit = Channel.onExit(dataChannel, (exit) => { - * if (Exit.isSuccess(exit)) { - * return Console.log(`Channel completed successfully with: ${exit.value}`) - * } else { - * return Console.log(`Channel failed with: ${exit.cause}`) - * } + * exits.push(exit) + * return Effect.void * }) + * const observed = [await Effect.runPromise(Channel.runCollect(channelWithExit)), exits] // => [[1, 2, 3], [Exit.void]] * ``` * * @category resource management @@ -7109,8 +7266,8 @@ export const onEnd: { * * **Example** (Ensuring cleanup runs) * - * ```ts - * import { Channel, Console, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class EnsureError extends Data.TaggedError("EnsureError")<{ * readonly operation: string @@ -7120,10 +7277,12 @@ export const onEnd: { * const dataChannel = Channel.fromIterable([1, 2, 3]) * * // Ensure cleanup always runs + * const events: Array = [] * const channelWithCleanup = Channel.ensuring( * dataChannel, - * Console.log("Cleanup executed regardless of success or failure") + * Effect.sync(() => events.push("cleanup")) * ) + * const observed = [await Effect.runPromise(Channel.runCollect(channelWithCleanup)), events] // => [[1, 2, 3], ["cleanup"]] * ``` * * @category resource management @@ -7170,7 +7329,7 @@ const runWith = < /** * Creates a channel from the specified services. * - * @category services + * @category accessors * @since 2.0.0 */ export const contextWith = ( @@ -7184,7 +7343,7 @@ export const contextWith = ( /** * The starting channel for Do notation, emitting an empty object. * - * @category do notation + * @category constructors * @since 4.0.0 */ export const Do: Channel<{}> = succeed({}) @@ -7503,7 +7662,7 @@ export { /** * Adds a computed field to each object emitted by a channel. * - * @category do notation + * @category mapping * @since 4.0.0 */ let_ as let @@ -7519,7 +7678,7 @@ export { * channel's output becomes the value of the new field. `options.concurrency` * and `options.bufferSize` control how derived channels are flattened. * - * @category do notation + * @category sequencing * @since 4.0.0 */ export const bind: { @@ -7625,7 +7784,7 @@ export const bind: { * @see {@link bind} for adding a field produced by another channel * @see {@link let_ let} for adding a computed field * - * @category do notation + * @category mapping * @since 4.0.0 */ export const bindTo: { @@ -7670,8 +7829,8 @@ export const bindTo: { * * **Example** (Counting channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class CountError extends Data.TaggedError("CountError")<{ * readonly reason: string @@ -7683,23 +7842,23 @@ export const bindTo: { * // Count the elements * const countEffect = Channel.runCount(numbersChannel) * - * // Effect.runSync(countEffect) // Returns: 5 + * Effect.runSync(countEffect) // => 5 * ``` * - * @category execution + * @category running * @since 4.0.0 */ export const runCount = ( self: Channel -): Effect.Effect => runFold(self, () => 0, (acc) => acc + 1) +): Effect.Effect => runFold(self, () => 0, (acc) => acc + 1) /** * Runs a channel and discards all output elements, returning only the final result. * * **Example** (Draining channel output at runtime) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class DrainError extends Data.TaggedError("DrainError")<{ * readonly stage: string @@ -7707,18 +7866,15 @@ export const runCount = ( * * // Create a channel that outputs elements and completes with a result * const resultChannel = Channel.fromIterable([1, 2, 3]) - * const completedChannel = Channel.concatWith( - * resultChannel, - * () => Channel.succeed("completed") - * ) + * const completedChannel = Channel.concat(resultChannel, Channel.end("completed")) * * // Drain all elements and get only the final result * const drainEffect = Channel.runDrain(completedChannel) * - * // Effect.runSync(drainEffect) // Returns: "completed" + * Effect.runSync(drainEffect) // => "completed" * ``` * - * @category execution + * @category running * @since 2.0.0 */ export const runDrain = ( @@ -7730,8 +7886,8 @@ export const runDrain = ( * * **Example** (Running effects for each output) * - * ```ts - * import { Channel, Console, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class ForEachError extends Data.TaggedError("ForEachError")<{ * readonly element: unknown @@ -7740,16 +7896,18 @@ export const runDrain = ( * // Create a channel with numbers * const numbersChannel = Channel.fromIterable([1, 2, 3]) * - * // Run forEach to log each element + * // Run forEach to process each element + * const processed: Array = [] * const forEachEffect = Channel.runForEach( * numbersChannel, - * (n) => Console.log(`Processing: ${n}`) + * (n) => Effect.sync(() => processed.push(n)) * ) * - * // Logs: "Processing: 1", "Processing: 2", "Processing: 3" + * await Effect.runPromise(forEachEffect) + * processed // => [1, 2, 3] * ``` * - * @category execution + * @category running * @since 4.0.0 */ export const runForEach: { @@ -7780,7 +7938,7 @@ export const runForEach: { * Returning `true` continues consuming the channel. Returning `false` stops * consumption early. The returned effect completes with `void`. * - * @category execution + * @category running * @since 4.0.0 */ export const runForEachWhile: { @@ -7807,13 +7965,71 @@ export const runForEachWhile: { )) ) +/** + * Concatenates a channel's `Uint8Array` chunks into a single `Uint8Array`. + * + * **Example** (Joining channel byte chunks) + * + * ```ts import.meta.vitest + * import { Channel, Effect } from "effect" + * + * const channel = Channel.fromArray([ + * [new Uint8Array([1, 2])], + * [new Uint8Array([3, 4])] + * ] as const) + * + * const bytes = Effect.runSync(Channel.mkUint8Array(channel)) + * Array.from(bytes) // => [1, 2, 3, 4] + * ``` + * + * **Gotchas** + * + * This materializes the full content in memory. The source channel must not + * reuse or mutate emitted buffers, which are retained until collection completes. + * + * @category running + * @since 4.0.0 + */ +export const mkUint8Array = ( + self: Channel, OutErr, OutDone, unknown, unknown, unknown, Env> +): Effect.Effect, OutErr, Env> => + Effect.map( + runFold( + self, + (): { + bytes: number + readonly arrays: Array + } => ({ + bytes: 0, + arrays: [] + }), + (acc, chunk) => { + for (let i = 0; i < chunk.length; i++) { + acc.bytes += chunk[i].length + acc.arrays.push(chunk[i]) + } + return acc + } + ), + ({ arrays, bytes }) => { + const result = new Uint8Array(bytes) + let offset = 0 + for (let i = 0; i < arrays.length; i++) { + const array = arrays[i] + result.set(array, offset) + offset += array.length + } + return result + } + ) + /** * Runs a channel and collects all output elements into an array. * * **Example** (Collecting channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class CollectError extends Data.TaggedError("CollectError")<{ * readonly reason: string @@ -7825,10 +8041,10 @@ export const runForEachWhile: { * // Collect all elements into an array * const collectEffect = Channel.runCollect(numbersChannel) * - * // Effect.runSync(collectEffect) // Returns: [1, 2, 3, 4, 5] + * Effect.runSync(collectEffect) // => [1, 2, 3, 4, 5] * ``` * - * @category execution + * @category running * @since 2.0.0 */ export const runCollect = ( @@ -7842,7 +8058,7 @@ export const runCollect = ( /** * Runs a channel and outputs the done value. * - * @category execution + * @category running * @since 4.0.0 */ export const runDone = ( @@ -7858,7 +8074,7 @@ export const runDone = ( * Returns `Option.some` with the first output element, or `Option.none` if the * channel completes without emitting output. * - * @category execution + * @category running * @since 4.0.0 */ export const runHead = ( @@ -7885,7 +8101,7 @@ export const runHead = ( * Returns `Option.some` with the last emitted element, or `Option.none` if the * channel completes without emitting output. * - * @category execution + * @category running * @since 4.0.0 */ export const runLast = ( @@ -7913,8 +8129,8 @@ export const runLast = ( * * **Example** (Folding channel output) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect } from "effect" * * class FoldError extends Data.TaggedError("FoldError")<{ * readonly operation: string @@ -7926,10 +8142,10 @@ export const runLast = ( * // Fold to calculate sum * const sumEffect = Channel.runFold(numbersChannel, () => 0, (acc, n) => acc + n) * - * // Effect.runSync(sumEffect) // Returns: 15 + * Effect.runSync(sumEffect) // => 15 * ``` * - * @category execution + * @category running * @since 4.0.0 */ export const runFold: { @@ -7979,7 +8195,7 @@ export const runFold: { * the effectful accumulator function. The returned effect succeeds with the * final accumulator value. * - * @category execution + * @category running * @since 4.0.0 */ export const runFoldEffect: { @@ -8029,7 +8245,7 @@ export const runFoldEffect: { * * **Example** (Converting channels to pulls) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Data, Effect } from "effect" * * class PullError extends Data.TaggedError("PullError")<{ @@ -8039,12 +8255,11 @@ export const runFoldEffect: { * // Create a channel * const numbersChannel = Channel.fromIterable([1, 2, 3]) * - * // Convert to Pull within a scope - * const pullEffect = Effect.scoped( - * Channel.toPull(numbersChannel) - * ) - * - * // Use the Pull to manually consume elements + * const program = Effect.scoped(Effect.gen(function*() { + * const pull = yield* Channel.toPull(numbersChannel) + * return [yield* pull, yield* pull, yield* pull] + * })) + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category destructors @@ -8078,7 +8293,7 @@ export const toPull: ( * * **Example** (Converting channels to scoped pulls) * - * ```ts + * ```ts import.meta.vitest * import { Channel, Data, Effect, Scope } from "effect" * * class ScopedPullError extends Data.TaggedError("ScopedPullError")<{ @@ -8090,10 +8305,11 @@ export const toPull: ( * * // Convert to Pull with explicit scope * const scopedPullEffect = Effect.gen(function*() { - * const scope = yield* Scope.make() + * const scope = yield* Effect.scope * const pull = yield* Channel.toPullScoped(numbersChannel, scope) - * return pull + * return [yield* pull, yield* pull, yield* pull] * }) + * await Effect.runPromise(Effect.scoped(scopedPullEffect)) // => [1, 2, 3] * ``` * * @category destructors @@ -8201,8 +8417,8 @@ export const runIntoQueueArray: { * * **Example** (Converting channels to queues) * - * ```ts - * import { Channel, Data } from "effect" + * ```ts import.meta.vitest + * import { Channel, Data, Effect, Queue } from "effect" * * class QueueError extends Data.TaggedError("QueueError")<{ * readonly operation: string @@ -8212,10 +8428,11 @@ export const runIntoQueueArray: { * const dataChannel = Channel.fromIterable([1, 2, 3, 4, 5]) * * // Convert to queue for concurrent processing - * const queueEffect = Channel.toQueue(dataChannel, { capacity: 32 }) - * - * // The queue can be used for concurrent consumption - * // Multiple consumers can read from the queue + * const program = Effect.scoped(Effect.gen(function*() { + * const queue = yield* Channel.toQueue(dataChannel, { capacity: 32 }) + * return yield* Queue.takeBetween(queue, 5, 5) + * })) + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5] * ``` * * @category destructors diff --git a/.context/effect/packages/effect/src/ChannelSchema.ts b/.context/effect/packages/effect/src/ChannelSchema.ts index fb2b4864c..6f91859ec 100644 --- a/.context/effect/packages/effect/src/ChannelSchema.ts +++ b/.context/effect/packages/effect/src/ChannelSchema.ts @@ -136,7 +136,7 @@ export const decodeUnknown: ( Arr.NonEmptyReadonlyArray, IE | Schema.SchemaError, Done, - Arr.NonEmptyReadonlyArray, + Arr.NonEmptyReadonlyArray, IE, Done, S["DecodingServices"] diff --git a/.context/effect/packages/effect/src/Chunk.ts b/.context/effect/packages/effect/src/Chunk.ts index 67b2f997a..7a75730a6 100644 --- a/.context/effect/packages/effect/src/Chunk.ts +++ b/.context/effect/packages/effect/src/Chunk.ts @@ -37,12 +37,12 @@ const TypeId = "~effect/collections/Chunk" * * **Example** (Inspecting chunk values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk: Chunk.Chunk = Chunk.make(1, 2, 3) - * console.log(chunk.length) // 3 - * console.log(Chunk.toArray(chunk)) // [1, 2, 3] + * chunk.length // => 3 + * Chunk.toArray(chunk) // => [1, 2, 3] * ``` * * @category models @@ -64,12 +64,12 @@ export interface Chunk extends Iterable, Equal.Equal, Pipeable, Inspec * * **Example** (Working with non-empty chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nonEmptyChunk: Chunk.NonEmptyChunk = Chunk.make(1, 2, 3) - * console.log(Chunk.headNonEmpty(nonEmptyChunk)) // 1 - * console.log(Chunk.lastNonEmpty(nonEmptyChunk)) // 3 + * Chunk.headNonEmpty(nonEmptyChunk) // => 1 + * Chunk.lastNonEmpty(nonEmptyChunk) // => 3 * ``` * * @category models @@ -82,7 +82,7 @@ export interface NonEmptyChunk extends Chunk, NonEmptyIterable {} * * **Example** (Applying the Chunk type lambda) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk, HKT } from "effect" * * // Create a Chunk type using the type lambda @@ -90,7 +90,7 @@ export interface NonEmptyChunk extends Chunk, NonEmptyIterable {} * // Equivalent to: Chunk * ``` * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface ChunkTypeLambda extends TypeLambda { @@ -152,7 +152,7 @@ const emptyArray: ReadonlyArray = [] * * **Example** (Comparing chunks for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Equivalence } from "effect" * * const chunk1 = Chunk.make(1, 2, 3) @@ -160,8 +160,8 @@ const emptyArray: ReadonlyArray = [] * const chunk3 = Chunk.make(1, 2, 4) * * const eq = Chunk.makeEquivalence(Equivalence.strictEqual()) - * console.log(eq(chunk1, chunk2)) // true - * console.log(eq(chunk1, chunk3)) // false + * eq(chunk1, chunk2) // => true + * eq(chunk1, chunk3) // => false * ``` * * @category instances @@ -262,18 +262,18 @@ const makeChunk = (backing: Backing): Chunk => { * * **Example** (Checking for chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) * const array = [1, 2, 3] * - * console.log(Chunk.isChunk(chunk)) // true - * console.log(Chunk.isChunk(array)) // false - * console.log(Chunk.isChunk("string")) // false + * Chunk.isChunk(chunk) // => true + * Chunk.isChunk(array) // => false + * Chunk.isChunk("string") // => false * ``` * - * @category constructors + * @category guards * @since 2.0.0 */ export const isChunk: { @@ -288,11 +288,10 @@ const _empty = makeChunk({ _tag: "IEmpty" }) * * **Example** (Creating an empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const emptyChunk = Chunk.empty() - * console.log(Chunk.size(emptyChunk)) // 0 + * Chunk.size(Chunk.empty()) // => 0 * ``` * * @category constructors @@ -305,11 +304,10 @@ export const empty: () => Chunk = () => _empty * * **Example** (Creating a non-empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.make(1, 2, 3, 4)) // => [1, 2, 3, 4] * ``` * * @category constructors @@ -323,11 +321,10 @@ export const make = ]>(...as: As): NonEm * * **Example** (Creating a single-element chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.of("hello") - * console.log(Chunk.toArray(chunk)) // ["hello"] + * Chunk.toArray(Chunk.of("hello")) // => ["hello"] * ``` * * @category constructors @@ -340,11 +337,10 @@ export const of = (a: A): NonEmptyChunk => makeChunk({ _tag: "ISingleton", * * **Example** (Creating chunks from iterables) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.fromIterable([1, 2, 3]) - * console.log(Chunk.toArray(chunk)) // [1, 2, 3] + * Chunk.toArray(Chunk.fromIterable([1, 2, 3])) // => [1, 2, 3] * ``` * * @category constructors @@ -390,17 +386,16 @@ const toArray_ = (self: Chunk): Array => toReadonlyArray(self).slice() * * **Example** (Converting chunks to mutable arrays) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) * const array = Chunk.toArray(chunk) - * console.log(array) // [1, 2, 3] - * console.log(Array.isArray(array)) // true + * array // => [1, 2, 3] + * Array.isArray(array) // => true * * // With empty chunk - * const emptyChunk = Chunk.empty() - * console.log(Chunk.toArray(emptyChunk)) // [] + * Chunk.toArray(Chunk.empty()) // => [] * ``` * * @category converting @@ -440,19 +435,18 @@ const toReadonlyArray_ = (self: Chunk): ReadonlyArray => { * * **Example** (Converting chunks to readonly arrays) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) * const readonlyArray = Chunk.toReadonlyArray(chunk) - * console.log(readonlyArray) // [1, 2, 3] + * readonlyArray // => [1, 2, 3] * * // The result is read-only, modifications would cause TypeScript errors * // readonlyArray[0] = 10 // TypeScript error * * // With empty chunk - * const emptyChunk = Chunk.empty() - * console.log(Chunk.toReadonlyArray(emptyChunk)) // [] + * Chunk.toReadonlyArray(Chunk.empty()) // => [] * ``` * * @category converting @@ -493,16 +487,14 @@ const reverseChunk = (self: Chunk): Chunk => { * * **Example** (Reversing chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) - * const result = Chunk.reverse(chunk) - * - * console.log(Chunk.toArray(result)) // [3, 2, 1] + * Chunk.toArray(Chunk.reverse(chunk)) // => [3, 2, 1] * ``` * - * @category elements + * @category transforming * @since 2.0.0 */ export const reverse: >(self: S) => Chunk.With> = reverseChunk as any @@ -513,21 +505,20 @@ export const reverse: >(self: S) => Chunk.With Option.some("b") + * Chunk.get(chunk, 10) // => Option.none() + * Chunk.get(chunk, -1) // => Option.none() * * // Using pipe syntax - * const result = chunk.pipe(Chunk.get(2)) - * console.log(result) // Option.some("c") + * chunk.pipe(Chunk.get(2)) // => Option.some("c") * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const get: { @@ -553,16 +544,16 @@ export const get: { * * **Example** (Creating chunks without copying arrays) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const array = [1, 2, 3, 4, 5] * const chunk = Chunk.fromArrayUnsafe(array) - * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * Chunk.toArray(chunk) // => [1, 2, 3, 4, 5] * * // Warning: Since this doesn't copy the array, mutations affect the chunk * array[0] = 999 - * console.log(Chunk.toArray(chunk)) // [999, 2, 3, 4, 5] + * Chunk.toArray(chunk) // => [999, 2, 3, 4, 5] * ``` * * @category unsafe @@ -585,15 +576,15 @@ export const fromArrayUnsafe = (self: ReadonlyArray): Chunk => * * **Example** (Creating non-empty chunks without copying arrays) * - * ```ts + * ```ts import.meta.vitest * import { Array, Chunk } from "effect" * * const nonEmptyArray = Array.make(1, 2, 3, 4, 5) * const chunk = Chunk.fromNonEmptyArrayUnsafe(nonEmptyArray) - * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * Chunk.toArray(chunk) // => [1, 2, 3, 4, 5] * * // The result is guaranteed to be non-empty - * console.log(Chunk.isNonEmpty(chunk)) // true + * Chunk.isNonEmpty(chunk) // => true * ``` * * @category unsafe @@ -616,16 +607,16 @@ export const fromNonEmptyArrayUnsafe = (self: NonEmptyReadonlyArray): NonE * * **Example** (Accessing elements unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make("a", "b", "c", "d") * - * console.log(Chunk.getUnsafe(chunk, 1)) // "b" - * console.log(Chunk.getUnsafe(chunk, 3)) // "d" + * Chunk.getUnsafe(chunk, 1) // => "b" + * Chunk.getUnsafe(chunk, 3) // => "d" * * // Use Chunk.get when the index may be out of bounds - * console.log(Option.isNone(Chunk.get(chunk, 10))) // true + * Option.isNone(Chunk.get(chunk, 10)) // => true * ``` * * @category unsafe @@ -673,17 +664,15 @@ export const getUnsafe: { * * **Example** (Appending an element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) - * const newChunk = Chunk.append(chunk, 4) - * console.log(Chunk.toArray(newChunk)) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.append(chunk, 4)) // => [1, 2, 3, 4] * * // Appending to empty chunk * const emptyChunk = Chunk.empty() - * const singleElement = Chunk.append(emptyChunk, 42) - * console.log(Chunk.toArray(singleElement)) // [42] + * Chunk.toArray(Chunk.append(emptyChunk, 42)) // => [42] * ``` * * @see {@link prepend} for adding one element before the existing elements @@ -702,17 +691,15 @@ export const append: { * * **Example** (Prepending an element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(2, 3, 4) - * const newChunk = Chunk.prepend(chunk, 1) - * console.log(Chunk.toArray(newChunk)) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.prepend(chunk, 1)) // => [1, 2, 3, 4] * * // Prepending to empty chunk * const emptyChunk = Chunk.empty() - * const singleElement = Chunk.prepend(emptyChunk, "first") - * console.log(Chunk.toArray(singleElement)) // ["first"] + * Chunk.toArray(Chunk.prepend(emptyChunk, "first")) // => ["first"] * ``` * * @category combining @@ -728,21 +715,21 @@ export const prepend: { * * **Example** (Taking elements from the start) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.take(chunk, 3) - * console.log(Chunk.toArray(result)) // [1, 2, 3] + * Chunk.toArray(Chunk.take(chunk, 3)) // => [1, 2, 3] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const take: { (n: number): (self: Chunk) => Chunk (self: Chunk, n: number): Chunk -} = dual(2, (self: Chunk, n: number): Chunk => { +} = dual(2, (self: Chunk, _n: number): Chunk => { + const n = Math.floor(_n) if (n <= 0) { return _empty } else if (n >= self.length) { @@ -785,21 +772,21 @@ export const take: { * * **Example** (Dropping elements from the start) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.drop(chunk, 2) - * console.log(Chunk.toArray(result)) // [3, 4, 5] + * Chunk.toArray(Chunk.drop(chunk, 2)) // => [3, 4, 5] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const drop: { (n: number): (self: Chunk) => Chunk (self: Chunk, n: number): Chunk -} = dual(2, (self: Chunk, n: number): Chunk => { +} = dual(2, (self: Chunk, _n: number): Chunk => { + const n = Math.floor(_n) if (n <= 0) { return self } else if (n >= self.length) { @@ -841,15 +828,14 @@ export const drop: { * * **Example** (Dropping elements from the end) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.dropRight(chunk, 2) - * console.log(Chunk.toArray(result)) // [1, 2, 3] + * Chunk.toArray(Chunk.dropRight(chunk, 2)) // => [1, 2, 3] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const dropRight: { @@ -862,15 +848,14 @@ export const dropRight: { * * **Example** (Dropping elements while a predicate matches) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.dropWhile(chunk, (n) => n < 3) - * console.log(Chunk.toArray(result)) // [3, 4, 5] + * Chunk.toArray(Chunk.dropWhile(chunk, (n) => n < 3)) // => [3, 4, 5] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const dropWhile: { @@ -892,16 +877,13 @@ export const dropWhile: { * * **Example** (Prepending all elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const result = Chunk.make(1, 2).pipe( + * Chunk.make(1, 2).pipe( * Chunk.prependAll(Chunk.make("a", "b")), * Chunk.toArray - * ) - * - * console.log(result) - * // [ "a", "b", 1, 2 ] + * ) // => ["a", "b", 1, 2] * ``` * * @category combining @@ -927,16 +909,13 @@ export const prependAll: { * * **Example** (Appending all elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const result = Chunk.make(1, 2).pipe( + * Chunk.make(1, 2).pipe( * Chunk.appendAll(Chunk.make("a", "b")), * Chunk.toArray - * ) - * - * console.log(result) - * // [ 1, 2, "a", "b" ] + * ) // => [1, 2, "a", "b"] * ``` * * @see {@link prependAll} for concatenating chunks in the opposite order @@ -998,7 +977,7 @@ export const appendAll: { * * **Example** (Filtering and mapping values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Result } from "effect" * * const chunk = Chunk.make("1", "2", "hello", "3", "world") @@ -1006,14 +985,14 @@ export const appendAll: { * const num = parseInt(str) * return isNaN(num) ? Result.failVoid : Result.succeed(num) * }) - * console.log(Chunk.toArray(numbers)) // [1, 2, 3] + * Chunk.toArray(numbers) // => [1, 2, 3] * * // With index parameter * const evenIndexNumbers = Chunk.filterMap(chunk, (str, i) => { * const num = parseInt(str) * return isNaN(num) || i % 2 !== 0 ? Result.failVoid : Result.succeed(num) * }) - * console.log(Chunk.toArray(evenIndexNumbers)) // [1] + * Chunk.toArray(evenIndexNumbers) // => [1] * ``` * * @category filtering @@ -1042,17 +1021,17 @@ export const filterMap: { * * **Example** (Filtering values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) * const evenNumbers = Chunk.filter(chunk, (n) => n % 2 === 0) - * console.log(Chunk.toArray(evenNumbers)) // [2, 4, 6] + * Chunk.toArray(evenNumbers) // => [2, 4, 6] * * // With refinement * const mixed = Chunk.make("hello", 42, "world", 100) * const numbers = Chunk.filter(mixed, (x): x is number => typeof x === "number") - * console.log(Chunk.toArray(numbers)) // [42, 100] + * Chunk.toArray(numbers) // => [42, 100] * ``` * * @category filtering @@ -1073,23 +1052,19 @@ export const filter: { * * **Example** (Filtering and mapping while values match) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Result } from "effect" * * const chunk = Chunk.make("1", "2", "hello", "3", "4") - * const result = Chunk.filterMapWhile(chunk, (s) => { - * const num = parseInt(s) - * return isNaN(num) ? Result.failVoid : Result.succeed(num) - * }) - * console.log(Chunk.toArray(result)) // [1, 2] - * // Stops at "hello" and doesn't process "3", "4" + * Chunk.toArray(Chunk.filterMapWhile(chunk, (s) => { + * const n = Number(s) + * return Number.isNaN(n) ? Result.failVoid : Result.succeed(n) + * })) // => [1, 2] * - * // Compare with regular filterMap - * const allNumbers = Chunk.filterMap(chunk, (s) => { - * const num = parseInt(s) - * return isNaN(num) ? Result.failVoid : Result.succeed(num) - * }) - * console.log(Chunk.toArray(allNumbers)) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.filterMap(chunk, (s) => { + * const n = Number(s) + * return Number.isNaN(n) ? Result.failVoid : Result.succeed(n) + * })) // => [1, 2, 3, 4] * ``` * * @category filtering @@ -1116,12 +1091,11 @@ export const filterMapWhile: { * * **Example** (Compacting optional values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(Option.some(1), Option.none(), Option.some(3)) - * const result = Chunk.compact(chunk) - * console.log(Chunk.toArray(result)) // [1, 3] + * Chunk.toArray(Chunk.compact(chunk)) // => [1, 3] * ``` * * @category filtering @@ -1142,12 +1116,12 @@ export const compact = (self: Chunk>): Chunk => { * * **Example** (Flat mapping chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3) * const duplicated = Chunk.flatMap(chunk, (n) => Chunk.make(n, n)) - * console.log(Chunk.toArray(duplicated)) // [1, 1, 2, 2, 3, 3] + * Chunk.toArray(duplicated) // => [1, 1, 2, 2, 3, 3] * * // Flattening nested arrays * const words = Chunk.make("hello", "world") @@ -1155,11 +1129,11 @@ export const compact = (self: Chunk>): Chunk => { * words, * (word) => Chunk.fromIterable(word.split("")) * ) - * console.log(Chunk.toArray(letters)) // ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"] + * Chunk.toArray(letters).join("") // => "helloworld" * * // With index parameter * const indexed = Chunk.flatMap(chunk, (n, i) => Chunk.make(n + i)) - * console.log(Chunk.toArray(indexed)) // [1, 3, 5] + * Chunk.toArray(indexed) // => [1, 3, 5] * ``` * * @category sequencing @@ -1195,26 +1169,19 @@ export const flatMap: { * * **Example** (Iterating over chunk values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) * - * // Log each element - * Chunk.forEach(chunk, (n) => console.log(`Value: ${n}`)) - * // Output: - * // Value: 1 - * // Value: 2 - * // Value: 3 - * // Value: 4 + * const values: Array = [] + * Chunk.forEach(chunk, (n) => values.push(`Value: ${n}`)) + * values // => ["Value: 1", "Value: 2", "Value: 3", "Value: 4"] * * // With index parameter - * Chunk.forEach(chunk, (n, i) => console.log(`Index ${i}: ${n}`)) - * // Output: - * // Index 0: 1 - * // Index 1: 2 - * // Index 2: 3 - * // Index 3: 4 + * const indexed: Array = [] + * Chunk.forEach(chunk, (n, i) => indexed.push(`Index ${i}: ${n}`)) + * indexed // => ["Index 0: 1", "Index 1: 2", "Index 2: 3", "Index 3: 4"] * ``` * * @category combinators @@ -1230,7 +1197,7 @@ export const forEach: { * * **Example** (Flattening nested chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nested = Chunk.make( @@ -1238,8 +1205,7 @@ export const forEach: { * Chunk.make(3, 4, 5), * Chunk.make(6) * ) - * const flattened = Chunk.flatten(nested) - * console.log(Chunk.toArray(flattened)) // [1, 2, 3, 4, 5, 6] + * Chunk.toArray(Chunk.flatten(nested)) // => [1, 2, 3, 4, 5, 6] * * // With empty chunks * const withEmpty = Chunk.make( @@ -1247,7 +1213,7 @@ export const forEach: { * Chunk.empty(), * Chunk.make(3, 4) * ) - * console.log(Chunk.toArray(Chunk.flatten(withEmpty))) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.flatten(withEmpty)) // => [1, 2, 3, 4] * ``` * * @category sequencing @@ -1274,25 +1240,23 @@ export const flatten: >>(self: S) => Chunk.Flatten * * **Example** (Splitting into fixed-size chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8, 9) * const chunked = Chunk.chunksOf(chunk, 3) * - * console.log(Chunk.toArray(chunked).map(Chunk.toArray)) - * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * Chunk.toArray(chunked).map(Chunk.toArray) // => [[1, 2, 3], [4, 5, 6], [7, 8, 9]] * * // When length is not evenly divisible * const chunk2 = Chunk.make(1, 2, 3, 4, 5) * const chunked2 = Chunk.chunksOf(chunk2, 2) - * console.log(Chunk.toArray(chunked2).map(Chunk.toArray)) - * // [[1, 2], [3, 4], [5]] + * Chunk.toArray(chunked2).map(Chunk.toArray) // => [[1, 2], [3, 4], [5]] * ``` * * @see {@link split} for splitting into a target number of chunks instead of a fixed chunk size * - * @category elements + * @category splitting * @since 2.0.0 */ export const chunksOf: { @@ -1323,26 +1287,25 @@ export const chunksOf: { * * **Example** (Intersecting chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk1 = Chunk.make(1, 2, 3, 4) * const chunk2 = Chunk.make(3, 4, 5, 6) - * const result = Chunk.intersection(chunk1, chunk2) - * console.log(Chunk.toArray(result)) // [3, 4] + * Chunk.toArray(Chunk.intersection(chunk1, chunk2)) // => [3, 4] * * // With strings * const words1 = Chunk.make("hello", "world", "foo") * const words2 = Chunk.make("world", "bar", "foo") - * console.log(Chunk.toArray(Chunk.intersection(words1, words2))) // ["world", "foo"] + * Chunk.toArray(Chunk.intersection(words1, words2)) // => ["world", "foo"] * * // No intersection * const chunk3 = Chunk.make(1, 2) * const chunk4 = Chunk.make(3, 4) - * console.log(Chunk.toArray(Chunk.intersection(chunk3, chunk4))) // [] + * Chunk.toArray(Chunk.intersection(chunk3, chunk4)) // => [] * ``` * - * @category elements + * @category set operations * @since 2.0.0 */ export const intersection: { @@ -1359,14 +1322,14 @@ export const intersection: { * * **Example** (Checking for empty chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * console.log(Chunk.isEmpty(Chunk.empty())) // true - * console.log(Chunk.isEmpty(Chunk.make(1, 2, 3))) // false + * Chunk.isEmpty(Chunk.empty()) // => true + * Chunk.isEmpty(Chunk.make(1, 2, 3)) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: Chunk): boolean => self.length === 0 @@ -1376,14 +1339,14 @@ export const isEmpty = (self: Chunk): boolean => self.length === 0 * * **Example** (Checking for non-empty chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * console.log(Chunk.isNonEmpty(Chunk.empty())) // false - * console.log(Chunk.isNonEmpty(Chunk.make(1, 2, 3))) // true + * Chunk.isNonEmpty(Chunk.empty()) // => false + * Chunk.isNonEmpty(Chunk.make(1, 2, 3)) // => true * ``` * - * @category elements + * @category guards * @since 2.0.0 */ export const isNonEmpty = (self: Chunk): self is NonEmptyChunk => self.length > 0 @@ -1393,14 +1356,14 @@ export const isNonEmpty = (self: Chunk): self is NonEmptyChunk => self. * * **Example** (Getting the first element) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * - * console.log(Chunk.head(Chunk.empty())) // { _tag: "None" } - * console.log(Chunk.head(Chunk.make(1, 2, 3))) // { _tag: "Some", value: 1 } + * Chunk.head(Chunk.empty()) // => Option.none() + * Chunk.head(Chunk.make(1, 2, 3)) // => Option.some(1) * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const head: (self: Chunk) => Option = get(0) @@ -1419,17 +1382,17 @@ export const head: (self: Chunk) => Option = get(0) * * **Example** (Getting the first element unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.headUnsafe(chunk)) // 1 + * Chunk.headUnsafe(chunk) // => 1 * * const singleElement = Chunk.make("hello") - * console.log(Chunk.headUnsafe(singleElement)) // "hello" + * Chunk.headUnsafe(singleElement) // => "hello" * * // Use Chunk.head when the chunk may be empty - * console.log(Option.isNone(Chunk.head(Chunk.empty()))) // true + * Option.isNone(Chunk.head(Chunk.empty())) // => true * ``` * * @category unsafe @@ -1442,20 +1405,20 @@ export const headUnsafe = (self: Chunk): A => getUnsafe(self, 0) * * **Example** (Getting the first element of a non-empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.headNonEmpty(nonEmptyChunk)) // 1 + * Chunk.headNonEmpty(nonEmptyChunk) // => 1 * * const singleElement = Chunk.make("hello") - * console.log(Chunk.headNonEmpty(singleElement)) // "hello" + * Chunk.headNonEmpty(singleElement) // => "hello" * * // Type safety: this function only accepts NonEmptyChunk * // Chunk.headNonEmpty(Chunk.empty()) // TypeScript error * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const headNonEmpty: (self: NonEmptyChunk) => A = headUnsafe @@ -1465,14 +1428,14 @@ export const headNonEmpty: (self: NonEmptyChunk) => A = headUnsafe * * **Example** (Getting the last element) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * - * console.log(Chunk.last(Chunk.empty())) // { _tag: "None" } - * console.log(Chunk.last(Chunk.make(1, 2, 3))) // { _tag: "Some", value: 3 } + * Chunk.last(Chunk.empty()) // => Option.none() + * Chunk.last(Chunk.make(1, 2, 3)) // => Option.some(3) * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const last = (self: Chunk): Option => get(self, self.length - 1) @@ -1491,17 +1454,17 @@ export const last = (self: Chunk): Option => get(self, self.length - 1) * * **Example** (Getting the last element unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.lastUnsafe(chunk)) // 4 + * Chunk.lastUnsafe(chunk) // => 4 * * const singleElement = Chunk.make("hello") - * console.log(Chunk.lastUnsafe(singleElement)) // "hello" + * Chunk.lastUnsafe(singleElement) // => "hello" * * // Use Chunk.last when the chunk may be empty - * console.log(Option.isNone(Chunk.last(Chunk.empty()))) // true + * Option.isNone(Chunk.last(Chunk.empty())) // => true * ``` * * @category unsafe @@ -1514,20 +1477,20 @@ export const lastUnsafe = (self: Chunk): A => getUnsafe(self, self.length * * **Example** (Getting the last element of a non-empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.lastNonEmpty(nonEmptyChunk)) // 4 + * Chunk.lastNonEmpty(nonEmptyChunk) // => 4 * * const singleElement = Chunk.make("hello") - * console.log(Chunk.lastNonEmpty(singleElement)) // "hello" + * Chunk.lastNonEmpty(singleElement) // => "hello" * * // Type safety: this function only accepts NonEmptyChunk * // Chunk.lastNonEmpty(Chunk.empty()) // TypeScript error * ``` * - * @category elements + * @category getters * @since 3.4.0 */ export const lastNonEmpty: (self: NonEmptyChunk) => A = lastUnsafe @@ -1537,7 +1500,7 @@ export const lastNonEmpty: (self: NonEmptyChunk) => A = lastUnsafe * * **Example** (Working with Chunk utility types) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * // Extract the element type from a Chunk @@ -1557,7 +1520,7 @@ export declare namespace Chunk { * * **Example** (Inferring element types) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * declare const numberChunk: Chunk.Chunk @@ -1567,7 +1530,7 @@ export declare namespace Chunk { * type StringType = Chunk.Chunk.Infer // string * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type Infer> = S extends Chunk ? A : never @@ -1577,7 +1540,7 @@ export declare namespace Chunk { * * **Example** (Preserving non-emptiness) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * declare const regularChunk: Chunk.Chunk @@ -1587,7 +1550,7 @@ export declare namespace Chunk { * type WithString2 = Chunk.Chunk.With // Chunk.NonEmptyChunk * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type With, A> = S extends NonEmptyChunk ? NonEmptyChunk : Chunk @@ -1597,7 +1560,7 @@ export declare namespace Chunk { * * **Example** (Preserving non-emptiness from either input) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * declare const emptyChunk: Chunk.Chunk @@ -1620,7 +1583,7 @@ export declare namespace Chunk { * > // Chunk.NonEmptyChunk * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type OrNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? @@ -1633,7 +1596,7 @@ export declare namespace Chunk { * * **Example** (Requiring non-emptiness from both inputs) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * declare const emptyChunk: Chunk.Chunk @@ -1656,7 +1619,7 @@ export declare namespace Chunk { * > // Chunk.NonEmptyChunk * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type AndNonEmpty, T extends Chunk, A> = S extends NonEmptyChunk ? @@ -1669,7 +1632,7 @@ export declare namespace Chunk { * * **Example** (Flattening nested chunk types) * - * ```ts + * ```ts import.meta.vitest * import type { Chunk } from "effect" * * declare const nestedChunk: Chunk.Chunk> @@ -1679,7 +1642,7 @@ export declare namespace Chunk { * type Flattened2 = Chunk.Chunk.Flatten // Chunk.NonEmptyChunk * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type Flatten>> = T extends NonEmptyChunk> ? NonEmptyChunk @@ -1693,12 +1656,10 @@ export declare namespace Chunk { * * **Example** (Mapping values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const result = Chunk.map(Chunk.make(1, 2), (n) => n + 1) - * - * console.log(Chunk.toArray(result)) // [2, 3] + * Chunk.toArray(Chunk.map(Chunk.make(1, 2), (n) => n + 1)) // => [2, 3] * ``` * * @category mapping @@ -1718,7 +1679,7 @@ export const map: { * * **Example** (Mapping with accumulated state) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) @@ -1727,8 +1688,8 @@ export const map: { * state + current // output running sum * ]) * - * console.log(finalState) // 15 (final accumulated sum) - * console.log(Chunk.toArray(mapped)) // [1, 3, 6, 10, 15] (running sums) + * finalState // => 15 + * Chunk.toArray(mapped) // => [1, 3, 6, 10, 15] * * // Building a string with indices * const words = Chunk.make("hello", "world", "effect") @@ -1736,8 +1697,8 @@ export const map: { * index + 1, * `${index}: ${word}` * ]) - * console.log(count) // 3 - * console.log(Chunk.toArray(indexed)) // ["0: hello", "1: world", "2: effect"] + * count // => 3 + * Chunk.toArray(indexed) // => ["0: hello", "1: world", "2: effect"] * ``` * * @category folding @@ -1760,15 +1721,15 @@ export const mapAccum: { * * **Example** (Partitioning with a Result) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Result } from "effect" * * const [excluded, satisfying] = Chunk.partition(Chunk.make(1, -2, 3), (n, i) => * n > 0 ? Result.succeed(n + i) : Result.fail(`negative:${n}`) * ) * - * console.log(Chunk.toArray(excluded)) // ["negative:-2"] - * console.log(Chunk.toArray(satisfying)) // [1, 5] + * Chunk.toArray(excluded) // => ["negative:-2"] + * Chunk.toArray(satisfying) // => [1, 5] * ``` * * @category filtering @@ -1804,7 +1765,7 @@ export const partition: { * * **Example** (Separating failures and successes) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Result } from "effect" * * const chunk = Chunk.make( @@ -1816,14 +1777,14 @@ export const partition: { * ) * * const [errors, values] = Chunk.separate(chunk) - * console.log(Chunk.toArray(errors)) // ["error1", "error2"] - * console.log(Chunk.toArray(values)) // [1, 2, 3] + * Chunk.toArray(errors) // => ["error1", "error2"] + * Chunk.toArray(values) // => [1, 2, 3] * * // All successes * const allSuccesses = Chunk.make(Result.succeed(1), Result.succeed(2)) * const [noErrors, allValues] = Chunk.separate(allSuccesses) - * console.log(Chunk.toArray(noErrors)) // [] - * console.log(Chunk.toArray(allValues)) // [1, 2] + * Chunk.toArray(noErrors) // => [] + * Chunk.toArray(allValues) // => [1, 2] * ``` * * @category filtering @@ -1840,14 +1801,13 @@ export const separate = (self: Chunk>): [Chunk, Chunk] * * **Example** (Getting chunk size) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.make(1, 2, 3) - * console.log(Chunk.size(chunk)) // 3 + * Chunk.size(Chunk.make(1, 2, 3)) // => 3 * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const size = (self: Chunk): number => self.length @@ -1857,21 +1817,18 @@ export const size = (self: Chunk): number => self.length * * **Example** (Sorting chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Order } from "effect" * * const numbers = Chunk.make(3, 1, 4, 1, 5, 9, 2, 6) - * const sorted = Chunk.sort(numbers, Order.Number) - * console.log(Chunk.toArray(sorted)) // [1, 1, 2, 3, 4, 5, 6, 9] + * Chunk.toArray(Chunk.sort(numbers, Order.Number)) // => [1, 1, 2, 3, 4, 5, 6, 9] * * // Reverse order - * const reverseSorted = Chunk.sort(numbers, Order.flip(Order.Number)) - * console.log(Chunk.toArray(reverseSorted)) // [9, 6, 5, 4, 3, 2, 1, 1] + * Chunk.toArray(Chunk.sort(numbers, Order.flip(Order.Number))) // => [9, 6, 5, 4, 3, 2, 1, 1] * * // String sorting * const words = Chunk.make("banana", "apple", "cherry") - * const sortedWords = Chunk.sort(words, Order.String) - * console.log(Chunk.toArray(sortedWords)) // ["apple", "banana", "cherry"] + * Chunk.toArray(Chunk.sort(words, Order.String)) // => ["apple", "banana", "cherry"] * ``` * * @category sorting @@ -1890,7 +1847,7 @@ export const sort: { * * **Example** (Sorting chunks by a derived value) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Order } from "effect" * * const people = Chunk.make( @@ -1901,18 +1858,15 @@ export const sort: { * * // Sort by age * const byAge = Chunk.sortWith(people, (person) => person.age, Order.Number) - * console.log(Chunk.toArray(byAge)) - * // [{ name: "Bob", age: 25 }, { name: "Alice", age: 30 }, { name: "Charlie", age: 35 }] + * Chunk.toArray(byAge).map((person) => person.name) // => ["Bob", "Alice", "Charlie"] * * // Sort by name * const byName = Chunk.sortWith(people, (person) => person.name, Order.String) - * console.log(Chunk.toArray(byName)) - * // [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }, { name: "Charlie", age: 35 }] + * Chunk.toArray(byName).map((person) => person.name) // => ["Alice", "Bob", "Charlie"] * * // Sort by string length * const words = Chunk.make("a", "abc", "ab") - * const byLength = Chunk.sortWith(words, (word) => word.length, Order.Number) - * console.log(Chunk.toArray(byLength)) // ["a", "ab", "abc"] + * Chunk.toArray(Chunk.sortWith(words, (word) => word.length, Order.Number)) // => ["a", "ab", "abc"] * ``` * * @category sorting @@ -1931,23 +1885,23 @@ export const sortWith: { * * **Example** (Splitting at an index) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) * const [before, after] = Chunk.splitAt(chunk, 3) - * console.log(Chunk.toArray(before)) // [1, 2, 3] - * console.log(Chunk.toArray(after)) // [4, 5, 6] + * Chunk.toArray(before) // => [1, 2, 3] + * Chunk.toArray(after) // => [4, 5, 6] * * // Split at index 0 * const [empty, all] = Chunk.splitAt(chunk, 0) - * console.log(Chunk.toArray(empty)) // [] - * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] + * Chunk.toArray(empty) // => [] + * Chunk.toArray(all) // => [1, 2, 3, 4, 5, 6] * * // Split beyond length * const [allElements, empty2] = Chunk.splitAt(chunk, 10) - * console.log(Chunk.toArray(allElements)) // [1, 2, 3, 4, 5, 6] - * console.log(Chunk.toArray(empty2)) // [] + * Chunk.toArray(allElements) // => [1, 2, 3, 4, 5, 6] + * Chunk.toArray(empty2) // => [] * ``` * * @category splitting @@ -1970,18 +1924,18 @@ export const splitAt: { * * **Example** (Splitting non-empty chunks at an index) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nonEmptyChunk = Chunk.make(1, 2, 3, 4, 5, 6) * const [before, after] = Chunk.splitNonEmptyAt(nonEmptyChunk, 3) - * console.log(Chunk.toArray(before)) // [1, 2, 3] - * console.log(Chunk.toArray(after)) // [4, 5, 6] + * Chunk.toArray(before) // => [1, 2, 3] + * Chunk.toArray(after) // => [4, 5, 6] * * // Split at 1 (minimum) * const [first, rest] = Chunk.splitNonEmptyAt(nonEmptyChunk, 1) - * console.log(Chunk.toArray(first)) // [1] - * console.log(Chunk.toArray(rest)) // [2, 3, 4, 5, 6] + * Chunk.toArray(first) // => [1] + * Chunk.toArray(rest) // => [2, 3, 4, 5, 6] * * // The first part is guaranteed to be NonEmptyChunk * // while the second part may be empty @@ -2010,24 +1964,21 @@ export const splitNonEmptyAt: { * * **Example** (Splitting chunks into groups) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8, 9) * const chunks = Chunk.split(chunk, 3) - * console.log(Chunk.toArray(chunks).map(Chunk.toArray)) - * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * Chunk.toArray(chunks).map(Chunk.toArray) // => [[1, 2, 3], [4, 5, 6], [7, 8, 9]] * * // Uneven split * const chunk2 = Chunk.make(1, 2, 3, 4, 5, 6, 7, 8) * const chunks2 = Chunk.split(chunk2, 3) - * console.log(Chunk.toArray(chunks2).map(Chunk.toArray)) - * // [[1, 2, 3], [4, 5, 6], [7, 8]] + * Chunk.toArray(chunks2).map(Chunk.toArray) // => [[1, 2, 3], [4, 5, 6], [7, 8]] * * // Split into 1 chunk * const chunks3 = Chunk.split(chunk, 1) - * console.log(Chunk.toArray(chunks3).map(Chunk.toArray)) - * // [[1, 2, 3, 4, 5, 6, 7, 8, 9]] + * Chunk.toArray(chunks3).map(Chunk.toArray) // => [[1, 2, 3, 4, 5, 6, 7, 8, 9]] * ``` * * @category splitting @@ -2044,23 +1995,23 @@ export const split: { * * **Example** (Splitting at a matching element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) * const [before, fromMatch] = Chunk.splitWhere(chunk, (n) => n > 3) - * console.log(Chunk.toArray(before)) // [1, 2, 3] - * console.log(Chunk.toArray(fromMatch)) // [4, 5, 6] + * Chunk.toArray(before) // => [1, 2, 3] + * Chunk.toArray(fromMatch) // => [4, 5, 6] * * // No match found * const [all, empty] = Chunk.splitWhere(chunk, (n) => n > 10) - * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] - * console.log(Chunk.toArray(empty)) // [] + * Chunk.toArray(all) // => [1, 2, 3, 4, 5, 6] + * Chunk.toArray(empty) // => [] * * // Match on first element * const [emptyBefore, allFromFirst] = Chunk.splitWhere(chunk, (n) => n === 1) - * console.log(Chunk.toArray(emptyBefore)) // [] - * console.log(Chunk.toArray(allFromFirst)) // [1, 2, 3, 4, 5, 6] + * Chunk.toArray(emptyBefore) // => [] + * Chunk.toArray(allFromFirst) // => [1, 2, 3, 4, 5, 6] * ``` * * @category splitting @@ -2086,20 +2037,19 @@ export const splitWhere: { * * **Example** (Getting the tail safely) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) - * console.log(Chunk.tail(chunk)) // Option.some(Chunk.make(2, 3, 4)) + * Chunk.tail(chunk) // => Option.some(Chunk.make(2, 3, 4)) * * const singleElement = Chunk.make(1) - * console.log(Chunk.tail(singleElement)) // Option.some(Chunk.empty()) + * Chunk.tail(singleElement) // => Option.some(Chunk.empty()) * - * const empty = Chunk.empty() - * console.log(Chunk.tail(empty)) // Option.none() + * Chunk.tail(Chunk.empty()) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const tail = (self: Chunk): O.Option> => self.length > 0 ? O.some(drop(self, 1)) : O.none() @@ -2109,22 +2059,20 @@ export const tail = (self: Chunk): O.Option> => self.length > 0 ? * * **Example** (Getting the tail of a non-empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const nonEmptyChunk = Chunk.make(1, 2, 3, 4) - * const result = Chunk.tailNonEmpty(nonEmptyChunk) - * console.log(Chunk.toArray(result)) // [2, 3, 4] + * Chunk.toArray(Chunk.tailNonEmpty(nonEmptyChunk)) // => [2, 3, 4] * * const singleElement = Chunk.make(1) - * const resultSingle = Chunk.tailNonEmpty(singleElement) - * console.log(Chunk.toArray(resultSingle)) // [] + * Chunk.toArray(Chunk.tailNonEmpty(singleElement)) // => [] * * // Type safety: this function only accepts NonEmptyChunk * // Chunk.tailNonEmpty(Chunk.empty()) // TypeScript error * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, 1) @@ -2134,23 +2082,20 @@ export const tailNonEmpty = (self: NonEmptyChunk): Chunk => drop(self, * * **Example** (Taking elements from the end) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5, 6) - * const lastThree = Chunk.takeRight(chunk, 3) - * console.log(Chunk.toArray(lastThree)) // [4, 5, 6] + * Chunk.toArray(Chunk.takeRight(chunk, 3)) // => [4, 5, 6] * * // Take more than available - * const all = Chunk.takeRight(chunk, 10) - * console.log(Chunk.toArray(all)) // [1, 2, 3, 4, 5, 6] + * Chunk.toArray(Chunk.takeRight(chunk, 10)) // => [1, 2, 3, 4, 5, 6] * * // Take zero - * const none = Chunk.takeRight(chunk, 0) - * console.log(Chunk.toArray(none)) // [] + * Chunk.toArray(Chunk.takeRight(chunk, 0)) // => [] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const takeRight: { @@ -2163,24 +2108,21 @@ export const takeRight: { * * **Example** (Taking elements while a predicate matches) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 3, 2, 1) - * const result = Chunk.takeWhile(chunk, (n) => n < 4) - * console.log(Chunk.toArray(result)) // [1, 2, 3] + * Chunk.toArray(Chunk.takeWhile(chunk, (n) => n < 4)) // => [1, 2, 3] * * // Empty if first element doesn't match - * const none = Chunk.takeWhile(chunk, (n) => n > 5) - * console.log(Chunk.toArray(none)) // [] + * Chunk.toArray(Chunk.takeWhile(chunk, (n) => n > 5)) // => [] * * // Takes all if all match * const small = Chunk.make(1, 2, 3) - * const all = Chunk.takeWhile(small, (n) => n < 10) - * console.log(Chunk.toArray(all)) // [1, 2, 3] + * Chunk.toArray(Chunk.takeWhile(small, (n) => n < 10)) // => [1, 2, 3] * ``` * - * @category elements + * @category filtering * @since 2.0.0 */ export const takeWhile: { @@ -2205,22 +2147,20 @@ export const takeWhile: { * * **Example** (Unioning chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk1 = Chunk.make(1, 2, 3) * const chunk2 = Chunk.make(3, 4, 5) - * const result = Chunk.union(chunk1, chunk2) - * console.log(Chunk.toArray(result)) // [1, 2, 3, 4, 5] + * Chunk.toArray(Chunk.union(chunk1, chunk2)) // => [1, 2, 3, 4, 5] * * // Handles duplicates within the same chunk * const withDupes1 = Chunk.make(1, 1, 2) * const withDupes2 = Chunk.make(2, 3, 3) - * const unified = Chunk.union(withDupes1, withDupes2) - * console.log(Chunk.toArray(unified)) // [1, 2, 3] + * Chunk.toArray(Chunk.union(withDupes1, withDupes2)) // => [1, 2, 3] * ``` * - * @category elements + * @category set operations * @since 2.0.0 */ export const union: { @@ -2237,25 +2177,22 @@ export const union: { * * **Example** (Removing duplicate values) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 2, 3, 1, 4, 3) - * const result = Chunk.dedupe(chunk) - * console.log(Chunk.toArray(result)) // [1, 2, 3, 4] + * Chunk.toArray(Chunk.dedupe(chunk)) // => [1, 2, 3, 4] * * // Empty chunk * const empty = Chunk.empty() - * const emptyDeduped = Chunk.dedupe(empty) - * console.log(Chunk.toArray(emptyDeduped)) // [] + * Chunk.toArray(Chunk.dedupe(empty)) // => [] * * // No duplicates * const unique = Chunk.make(1, 2, 3) - * const uniqueDeduped = Chunk.dedupe(unique) - * console.log(Chunk.toArray(uniqueDeduped)) // [1, 2, 3] + * Chunk.toArray(Chunk.dedupe(unique)) // => [1, 2, 3] * ``` * - * @category elements + * @category deduplication * @since 2.0.0 */ export const dedupe = (self: Chunk): Chunk => fromArrayUnsafe(RA.dedupe(toReadonlyArray(self))) @@ -2265,17 +2202,15 @@ export const dedupe = (self: Chunk): Chunk => fromArrayUnsafe(RA.dedupe * * **Example** (Removing adjacent duplicates) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 1, 2, 2, 2, 3, 1, 1) - * const result = Chunk.dedupeAdjacent(chunk) - * console.log(Chunk.toArray(result)) // [1, 2, 3, 1] + * Chunk.toArray(Chunk.dedupeAdjacent(chunk)) // => [1, 2, 3, 1] * * // Only removes adjacent duplicates, not all duplicates * const mixed = Chunk.make("a", "a", "b", "a", "a") - * const mixedResult = Chunk.dedupeAdjacent(mixed) - * console.log(Chunk.toArray(mixedResult)) // ["a", "b", "a"] + * Chunk.toArray(Chunk.dedupeAdjacent(mixed)) // => ["a", "b", "a"] * ``` * * @category filtering @@ -2292,7 +2227,7 @@ export const dedupeAdjacent = (self: Chunk): Chunk => fromArrayUnsafe(R * * **Example** (Unzipping pairs) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const pairs = Chunk.make( @@ -2301,17 +2236,17 @@ export const dedupeAdjacent = (self: Chunk): Chunk => fromArrayUnsafe(R * [3, "c"] as const * ) * const [numbers, letters] = Chunk.unzip(pairs) - * console.log(Chunk.toArray(numbers)) // [1, 2, 3] - * console.log(Chunk.toArray(letters)) // ["a", "b", "c"] + * Chunk.toArray(numbers) // => [1, 2, 3] + * Chunk.toArray(letters) // => ["a", "b", "c"] * * // Empty chunk * const empty = Chunk.empty<[number, string]>() * const [emptyNums, emptyStrs] = Chunk.unzip(empty) - * console.log(Chunk.toArray(emptyNums)) // [] - * console.log(Chunk.toArray(emptyStrs)) // [] + * Chunk.toArray(emptyNums) // => [] + * Chunk.toArray(emptyStrs) // => [] * ``` * - * @category elements + * @category splitting * @since 2.0.0 */ export const unzip = (self: Chunk): [Chunk, Chunk] => { @@ -2324,19 +2259,17 @@ export const unzip = (self: Chunk): [Chunk, Chunk] * * **Example** (Zipping chunks with a function) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const numbers = Chunk.make(1, 2, 3) * const letters = Chunk.make("a", "b", "c") - * const result = Chunk.zipWith(numbers, letters, (n, l) => `${n}-${l}`) - * console.log(Chunk.toArray(result)) // ["1-a", "2-b", "3-c"] + * Chunk.toArray(Chunk.zipWith(numbers, letters, (n, l) => `${n}-${l}`)) // => ["1-a", "2-b", "3-c"] * * // Different lengths - takes minimum * const short = Chunk.make(1, 2) * const long = Chunk.make("a", "b", "c", "d") - * const mixed = Chunk.zipWith(short, long, (n, l) => [n, l]) - * console.log(Chunk.toArray(mixed)) // [[1, "a"], [2, "b"]] + * Chunk.toArray(Chunk.zipWith(short, long, (n, l) => [n, l])) // => [[1, "a"], [2, "b"]] * ``` * * @category zipping @@ -2356,19 +2289,17 @@ export const zipWith: { * * **Example** (Zipping chunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const numbers = Chunk.make(1, 2, 3) * const letters = Chunk.make("a", "b", "c") - * const result = Chunk.zip(numbers, letters) - * console.log(Chunk.toArray(result)) // [[1, "a"], [2, "b"], [3, "c"]] + * Chunk.toArray(Chunk.zip(numbers, letters)) // => [[1, "a"], [2, "b"], [3, "c"]] * * // Different lengths - takes minimum length * const short = Chunk.make(1, 2) * const long = Chunk.make("a", "b", "c", "d") - * const zipped = Chunk.zip(short, long) - * console.log(Chunk.toArray(zipped)) // [[1, "a"], [2, "b"]] + * Chunk.toArray(Chunk.zip(short, long)) // => [[1, "a"], [2, "b"]] * ``` * * @category zipping @@ -2387,23 +2318,20 @@ export const zip: { * * **Example** (Removing an element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make("a", "b", "c", "d") - * const result = Chunk.remove(chunk, 1) - * console.log(Chunk.toArray(result)) // ["a", "c", "d"] + * Chunk.toArray(Chunk.remove(chunk, 1)) // => ["a", "c", "d"] * * // Remove first element - * const removeFirst = Chunk.remove(chunk, 0) - * console.log(Chunk.toArray(removeFirst)) // ["b", "c", "d"] + * Chunk.toArray(Chunk.remove(chunk, 0)) // => ["b", "c", "d"] * * // Index out of bounds returns same chunk - * const outOfBounds = Chunk.remove(chunk, 10) - * console.log(Chunk.toArray(outOfBounds)) // ["a", "b", "c", "d"] + * Chunk.toArray(Chunk.remove(chunk, 10)) // => ["a", "b", "c", "d"] * ``` * - * @category elements + * @category transforming * @since 2.0.0 */ export const remove: { @@ -2420,23 +2348,20 @@ export const remove: { * * **Example** (Modifying an element) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) - * const result = Chunk.modify(chunk, 1, (n) => n * 10) - * console.log(result) // Option.some(Chunk.make(1, 20, 3, 4)) + * Chunk.modify(chunk, 1, (n) => n * 10) // => Option.some(Chunk.make(1, 20, 3, 4)) * * // Index out of bounds returns None - * const outOfBounds = chunk.pipe(Chunk.modify(10, (n) => n * 10)) - * console.log(outOfBounds) // Option.none() + * chunk.pipe(Chunk.modify(10, (n) => n * 10)) // => Option.none() * * // Negative index returns None - * const negative = chunk.pipe(Chunk.modify(-1, (n) => n * 10)) - * console.log(negative) // Option.none() + * chunk.pipe(Chunk.modify(-1, (n) => n * 10)) // => Option.none() * ``` * - * @category elements + * @category transforming * @since 2.0.0 */ export const modify: { @@ -2454,23 +2379,20 @@ export const modify: { * * **Example** (Replacing an element) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * * const chunk = Chunk.make("a", "b", "c", "d") - * const result = Chunk.replace(chunk, 1, "X") - * console.log(result) // Option.some(Chunk.make("a", "X", "c", "d")) + * Chunk.replace(chunk, 1, "X") // => Option.some(Chunk.make("a", "X", "c", "d")) * * // Index out of bounds returns None - * const outOfBounds = chunk.pipe(Chunk.replace(10, "Y")) - * console.log(outOfBounds) // Option.none() + * chunk.pipe(Chunk.replace(10, "Y")) // => Option.none() * * // Negative index returns None - * const negative = chunk.pipe(Chunk.replace(-1, "Z")) - * console.log(negative) // Option.none() + * chunk.pipe(Chunk.replace(-1, "Z")) // => Option.none() * ``` * - * @category elements + * @category transforming * @since 2.0.0 */ export const replace: { @@ -2487,11 +2409,10 @@ export const replace: { * * **Example** (Generating chunks from indices) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.makeBy(5, (i) => i * 2) - * console.log(Chunk.toArray(chunk)) // [0, 2, 4, 6, 8] + * Chunk.toArray(Chunk.makeBy(5, (i) => i * 2)) // => [0, 2, 4, 6, 8] * ``` * * @category constructors @@ -2513,11 +2434,10 @@ export const makeBy: { * * **Example** (Creating a range) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * - * const chunk = Chunk.range(1, 5) - * console.log(Chunk.toArray(chunk)) // [1, 2, 3, 4, 5] + * Chunk.toArray(Chunk.range(1, 5)) // => [1, 2, 3, 4, 5] * ``` * * @category constructors @@ -2535,24 +2455,23 @@ export const range = (start: number, end: number): NonEmptyChunk => * * **Example** (Checking membership) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * console.log(Chunk.contains(chunk, 3)) // true - * console.log(Chunk.contains(chunk, 6)) // false + * Chunk.contains(chunk, 3) // => true + * Chunk.contains(chunk, 6) // => false * * // Works with strings * const words = Chunk.make("apple", "banana", "cherry") - * console.log(Chunk.contains(words, "banana")) // true - * console.log(Chunk.contains(words, "grape")) // false + * Chunk.contains(words, "banana") // => true + * Chunk.contains(words, "grape") // => false * * // Empty chunk - * const empty = Chunk.empty() - * console.log(Chunk.contains(empty, 1)) // false + * Chunk.contains(Chunk.empty(), 1) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const contains: { @@ -2565,7 +2484,7 @@ export const contains: { * * **Example** (Checking membership with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make({ id: 1, name: "Alice" }, { id: 2, name: "Bob" }) @@ -2574,19 +2493,19 @@ export const contains: { * const containsById = Chunk.containsWith<{ id: number; name: string }>((a, b) => * a.id === b.id * ) - * console.log(containsById(chunk, { id: 1, name: "Different" })) // true - * console.log(containsById(chunk, { id: 3, name: "Charlie" })) // false + * containsById(chunk, { id: 1, name: "Different" }) // => true + * containsById(chunk, { id: 3, name: "Charlie" }) // => false * * // Case-insensitive string comparison * const words = Chunk.make("Apple", "Banana", "Cherry") * const containsCaseInsensitive = Chunk.containsWith((a, b) => * a.toLowerCase() === b.toLowerCase() * ) - * console.log(containsCaseInsensitive(words, "apple")) // true - * console.log(containsCaseInsensitive(words, "grape")) // false + * containsCaseInsensitive(words, "apple") // => true + * containsCaseInsensitive(words, "grape") // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const containsWith: ( @@ -2602,17 +2521,14 @@ export const containsWith: ( * * **Example** (Finding the first matching element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.findFirst(chunk, (n) => n > 3) - * console.log(Option.isSome(result)) // true - * console.log(Option.getOrElse(result, () => 0)) // 4 + * Chunk.findFirst(chunk, (n) => n > 3) // => Option.some(4) * * // No match found - * const notFound = Chunk.findFirst(chunk, (n) => n > 10) - * console.log(Option.isNone(notFound)) // true + * Chunk.findFirst(chunk, (n) => n > 10) // => Option.none() * * // With type refinement * const mixed = Chunk.make(1, "hello", 2, "world", 3) @@ -2620,10 +2536,10 @@ export const containsWith: ( * mixed, * (x): x is string => typeof x === "string" * ) - * console.log(Option.getOrElse(firstString, () => "")) // "hello" + * firstString // => Option.some("hello") * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirst: { @@ -2638,23 +2554,20 @@ export const findFirst: { * * **Example** (Finding the first matching index) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.findFirstIndex(chunk, (n) => n > 3) - * console.log(result) // Option.some(3) + * Chunk.findFirstIndex(chunk, (n) => n > 3) // => Option.some(3) * * // No match found - * const notFound = Chunk.findFirstIndex(chunk, (n) => n > 10) - * console.log(notFound) // Option.none() + * Chunk.findFirstIndex(chunk, (n) => n > 10) // => Option.none() * * // Find first even number - * const firstEven = Chunk.findFirstIndex(chunk, (n) => n % 2 === 0) - * console.log(firstEven) // Option.some(1) + * Chunk.findFirstIndex(chunk, (n) => n % 2 === 0) // => Option.some(1) * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirstIndex: { @@ -2670,24 +2583,20 @@ export const findFirstIndex: { * * **Example** (Finding the last matching element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.findLast(chunk, (n) => n < 4) - * console.log(Option.isSome(result)) // true - * console.log(Option.getOrElse(result, () => 0)) // 3 + * Chunk.findLast(chunk, (n) => n < 4) // => Option.some(3) * * // No match found - * const notFound = Chunk.findLast(chunk, (n) => n > 10) - * console.log(Option.isNone(notFound)) // true + * Chunk.findLast(chunk, (n) => n > 10) // => Option.none() * * // Find last even number - * const lastEven = Chunk.findLast(chunk, (n) => n % 2 === 0) - * console.log(Option.getOrElse(lastEven, () => 0)) // 4 + * Chunk.findLast(chunk, (n) => n % 2 === 0) // => Option.some(4) * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findLast: { @@ -2702,23 +2611,20 @@ export const findLast: { * * **Example** (Finding the last matching index) * - * ```ts - * import { Chunk } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Option } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const result = Chunk.findLastIndex(chunk, (n) => n < 4) - * console.log(result) // Option.some(2) + * Chunk.findLastIndex(chunk, (n) => n < 4) // => Option.some(2) * * // No match found - * const notFound = Chunk.findLastIndex(chunk, (n) => n > 10) - * console.log(notFound) // Option.none() + * Chunk.findLastIndex(chunk, (n) => n > 10) // => Option.none() * * // Find last even number index - * const lastEven = Chunk.findLastIndex(chunk, (n) => n % 2 === 0) - * console.log(lastEven) // Option.some(3) + * Chunk.findLastIndex(chunk, (n) => n % 2 === 0) // => Option.some(3) * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findLastIndex: { @@ -2734,26 +2640,24 @@ export const findLastIndex: { * * **Example** (Checking every element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const allPositive = Chunk.make(1, 2, 3, 4, 5) - * console.log(Chunk.every(allPositive, (n) => n > 0)) // true - * console.log(Chunk.every(allPositive, (n) => n > 3)) // false + * Chunk.every(allPositive, (n) => n > 0) // => true + * Chunk.every(allPositive, (n) => n > 3) // => false * * // Empty chunk returns true - * const empty = Chunk.empty() - * console.log(Chunk.every(empty, (n) => n > 0)) // true + * Chunk.every(Chunk.empty(), (n) => n > 0) // => true * * // Type refinement * const mixed = Chunk.make(1, 2, 3) * if (Chunk.every(mixed, (x): x is number => typeof x === "number")) { * // mixed is now typed as Chunk - * console.log("All elements are numbers") * } * ``` * - * @category elements + * @category guards * @since 2.0.0 */ export const every: { @@ -2772,23 +2676,22 @@ export const every: { * * **Example** (Checking for some matching element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * console.log(Chunk.some(chunk, (n) => n > 4)) // true - * console.log(Chunk.some(chunk, (n) => n > 10)) // false + * Chunk.some(chunk, (n) => n > 4) // => true + * Chunk.some(chunk, (n) => n > 10) // => false * * // Empty chunk returns false - * const empty = Chunk.empty() - * console.log(Chunk.some(empty, (n) => n > 0)) // false + * Chunk.some(Chunk.empty(), (n) => n > 0) // => false * * // Check for specific value * const words = Chunk.make("apple", "banana", "cherry") - * console.log(Chunk.some(words, (word) => word.includes("ban"))) // true + * Chunk.some(words, (word) => word.includes("ban")) // => true * ``` * - * @category elements + * @category guards * @since 2.0.0 */ export const some: { @@ -2804,24 +2707,20 @@ export const some: { * * **Example** (Joining chunks into a string) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make("apple", "banana", "cherry") - * const result = Chunk.join(chunk, ", ") - * console.log(result) // "apple, banana, cherry" + * Chunk.join(chunk, ", ") // => "apple, banana, cherry" * * // With different separator - * const withPipe = Chunk.join(chunk, " | ") - * console.log(withPipe) // "apple | banana | cherry" + * Chunk.join(chunk, " | ") // => "apple | banana | cherry" * * // Empty chunk - * const empty = Chunk.empty() - * console.log(Chunk.join(empty, ", ")) // "" + * Chunk.join(Chunk.empty(), ", ") // => "" * * // Single element - * const single = Chunk.make("hello") - * console.log(Chunk.join(single, ", ")) // "hello" + * Chunk.join(Chunk.make("hello"), ", ") // => "hello" * ``` * * @category folding @@ -2837,21 +2736,18 @@ export const join: { * * **Example** (Reducing from the left) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4, 5) - * const sum = Chunk.reduce(chunk, 0, (acc, n) => acc + n) - * console.log(sum) // 15 + * Chunk.reduce(chunk, 0, (acc, n) => acc + n) // => 15 * * // String concatenation with index * const words = Chunk.make("a", "b", "c") - * const result = Chunk.reduce(words, "", (acc, word, i) => acc + `${i}:${word} `) - * console.log(result) // "0:a 1:b 2:c " + * Chunk.reduce(words, "", (acc, word, i) => acc + `${i}:${word} `).trimEnd() // => "0:a 1:b 2:c" * * // Find maximum - * const max = Chunk.reduce(chunk, -Infinity, (acc, n) => Math.max(acc, n)) - * console.log(max) // 5 + * Chunk.reduce(chunk, -Infinity, (acc, n) => Math.max(acc, n)) // => 5 * ``` * * @category folding @@ -2867,25 +2763,22 @@ export const reduce: { * * **Example** (Reducing from the right) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk = Chunk.make(1, 2, 3, 4) - * const result = Chunk.reduceRight(chunk, 0, (acc, n) => acc + n) - * console.log(result) // 10 + * Chunk.reduceRight(chunk, 0, (acc, n) => acc + n) // => 10 * * // String building (right to left) * const words = Chunk.make("a", "b", "c") - * const reversed = Chunk.reduceRight( + * Chunk.reduceRight( * words, * "", * (acc, word, i) => acc + `${i}:${word} ` - * ) - * console.log(reversed) // "2:c 1:b 0:a " + * ).trim() // => "2:c 1:b 0:a" * * // Subtract from right to left - * const subtraction = Chunk.reduceRight(chunk, 0, (acc, n) => n - acc) - * console.log(subtraction) // -2 (4 - (3 - (2 - (1 - 0)))) + * Chunk.reduceRight(chunk, 0, (acc, n) => n - acc) // => -2 * ``` * * @category folding @@ -2902,7 +2795,7 @@ export const reduceRight: { * * **Example** (Computing difference with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk1 = Chunk.make({ id: 1, name: "Alice" }, { id: 2, name: "Bob" }) @@ -2912,8 +2805,7 @@ export const reduceRight: { * const byId = Chunk.differenceWith<{ id: number; name: string }>((a, b) => * a.id === b.id * ) - * const result = byId(chunk1, chunk2) - * console.log(Chunk.toArray(result)) // [{ id: 2, name: "Bob" }] + * Chunk.toArray(byId(chunk1, chunk2)) // => [{ id: 2, name: "Bob" }] * * // String comparison case-insensitive * const words1 = Chunk.make("Apple", "Banana", "Cherry") @@ -2921,8 +2813,7 @@ export const reduceRight: { * const caseInsensitive = Chunk.differenceWith((a, b) => * a.toLowerCase() === b.toLowerCase() * ) - * const wordDiff = caseInsensitive(words1, words2) - * console.log(Chunk.toArray(wordDiff)) // ["Banana", "Cherry"] + * Chunk.toArray(caseInsensitive(words1, words2)) // => ["Banana", "Cherry"] * ``` * * @category filtering @@ -2944,24 +2835,20 @@ export const differenceWith = (isEquivalent: (self: A, that: A) => boolean): * * **Example** (Computing chunk difference) * - * ```ts + * ```ts import.meta.vitest * import { Chunk } from "effect" * * const chunk1 = Chunk.make(1, 2, 3, 4, 5) * const chunk2 = Chunk.make(3, 4, 6, 7) - * const result = Chunk.difference(chunk1, chunk2) - * console.log(Chunk.toArray(result)) // [1, 2, 5] + * Chunk.toArray(Chunk.difference(chunk1, chunk2)) // => [1, 2, 5] * * // String difference * const words1 = Chunk.make("apple", "banana", "cherry") * const words2 = Chunk.make("banana", "grape") - * const wordDiff = Chunk.difference(words1, words2) - * console.log(Chunk.toArray(wordDiff)) // ["apple", "cherry"] + * Chunk.toArray(Chunk.difference(words1, words2)) // => ["apple", "cherry"] * * // Empty second chunk returns original - * const empty = Chunk.empty() - * const unchanged = Chunk.difference(chunk1, empty) - * console.log(Chunk.toArray(unchanged)) // [1, 2, 3, 4, 5] + * Chunk.toArray(Chunk.difference(chunk1, Chunk.empty())) // => [1, 2, 3, 4, 5] * ``` * * @category filtering diff --git a/.context/effect/packages/effect/src/Clock.ts b/.context/effect/packages/effect/src/Clock.ts index 03a8b210e..5434197be 100644 --- a/.context/effect/packages/effect/src/Clock.ts +++ b/.context/effect/packages/effect/src/Clock.ts @@ -1,8 +1,9 @@ /** * Service and helpers for reading time and sleeping inside Effect programs. - * The active `Clock` provides current time in milliseconds or nanoseconds and a - * `sleep` operation for delaying work. Because time is accessed through a - * service, tests can replace the clock with a controlled implementation. + * The active `Clock` provides Unix time, monotonic time for measuring elapsed + * durations, and a `sleep` operation for delaying work. Because time is + * accessed through a service, tests can replace the clock with a controlled + * implementation. * * @since 2.0.0 */ @@ -22,56 +23,121 @@ import * as effect from "./internal/effect.ts" * * **Example** (Reading current time) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * + * const testClock: Clock.Clock = { + * currentTimeMillisUnsafe: () => 1_000, + * currentTimeMillis: Effect.succeed(1_000), + * monotonicTimeNanosUnsafe: () => 1_000_000_000n, + * monotonicTimeNanos: Effect.succeed(1_000_000_000n), + * currentTimeNanosUnsafe: () => 1_000_000_000n, + * currentTimeNanos: Effect.succeed(1_000_000_000n), + * sleep: () => Effect.void + * } + * * const clockOperations = Effect.gen(function*() { * const currentTime = yield* Clock.currentTimeMillis * const currentTimeNanos = yield* Clock.currentTimeNanos - * - * console.log(`Current time (ms): ${currentTime}`) - * console.log(`Current time (ns): ${currentTimeNanos}`) + * return [currentTime, currentTimeNanos] as const * }) + * + * await Effect.runPromise(Effect.provideService(clockOperations, Clock.Clock, testClock)) // => [1_000, 1_000_000_000n] * ``` * - * @category models + * @category services * @since 2.0.0 */ export interface Clock { /** - * Returns the current time in milliseconds unsafely. + * Returns the current Unix time in milliseconds unsafely. * * **When to use** * - * Use to read millisecond time synchronously when you already have a `Clock` - * service and can accept non-effectful access. + * Use to read a wall-clock timestamp synchronously when you already have a + * `Clock` service and can accept non-effectful access. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. */ currentTimeMillisUnsafe(): number /** - * Returns the current time in milliseconds. + * Returns the current Unix time in milliseconds. * * **When to use** * - * Use to read millisecond time through this `Clock` service in `Effect`. + * Use to read a wall-clock timestamp through this `Clock` service in + * `Effect`. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. */ readonly currentTimeMillis: Effect /** - * Returns the current time in nanoseconds unsafely. + * Returns the current Unix time in nanoseconds unsafely. * * **When to use** * - * Use to read nanosecond time synchronously when you already have a `Clock` - * service and can accept non-effectful access. + * Use to read a wall-clock timestamp synchronously when you already have a + * `Clock` service and can accept non-effectful access. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. */ currentTimeNanosUnsafe(): bigint /** - * Returns the current time in nanoseconds. + * Returns the current Unix time in nanoseconds. * * **When to use** * - * Use to read nanosecond time through this `Clock` service in `Effect`. + * Use to read a wall-clock timestamp through this `Clock` service in + * `Effect`. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. */ readonly currentTimeNanos: Effect + /** + * Returns the current monotonic time in nanoseconds unsafely. + * + * **When to use** + * + * Use to measure elapsed time synchronously when you already have a `Clock` + * service and can accept non-effectful access. + * + * **Gotchas** + * + * The value has an arbitrary origin and is unsuitable for serialization. Use + * it only to subtract readings produced by the same clock. Whether it + * advances while the host is suspended depends on the runtime. + * + * @since 4.0.0 + */ + monotonicTimeNanosUnsafe(): bigint + /** + * Returns the current monotonic time in nanoseconds. + * + * **When to use** + * + * Use to measure elapsed time through this `Clock` service in `Effect`. + * + * **Gotchas** + * + * The value has an arbitrary origin and is unsuitable for serialization. Use + * it only to subtract readings produced by the same clock. Whether it + * advances while the host is suspended depends on the runtime. + * + * @since 4.0.0 + */ + readonly monotonicTimeNanos: Effect /** * Asynchronously sleeps for the specified duration. * @@ -92,20 +158,32 @@ export interface Clock { * * **Example** (Accessing the Clock service) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * + * const testClock: Clock.Clock = { + * currentTimeMillisUnsafe: () => 1_000, + * currentTimeMillis: Effect.succeed(1_000), + * monotonicTimeNanosUnsafe: () => 1_000_000_000n, + * monotonicTimeNanos: Effect.succeed(1_000_000_000n), + * currentTimeNanosUnsafe: () => 1_000_000_000n, + * currentTimeNanos: Effect.succeed(1_000_000_000n), + * sleep: () => Effect.void + * } + * * const program = Effect.gen(function*() { * const clock = yield* Clock.Clock * return clock.currentTimeMillisUnsafe() * }) + * + * await Effect.runPromise(Effect.provideService(program, Clock.Clock, testClock)) // => 1_000 * ``` * * @see {@link clockWith} for using the current Clock service inside an effect * @see {@link currentTimeMillis} for reading the current time in milliseconds * @see {@link currentTimeNanos} for reading the current time in nanoseconds * - * @category references + * @category services * @since 2.0.0 */ export const Clock: Context.Reference = effect.ClockRef @@ -120,75 +198,129 @@ export const Clock: Context.Reference = effect.ClockRef * * **Example** (Accessing the current Clock service) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * - * const program = Clock.clockWith((clock) => - * Effect.sync(() => { - * const currentTime = clock.currentTimeMillisUnsafe() - * console.log(`Current time: ${currentTime}`) - * return currentTime - * }) - * ) + * const testClock: Clock.Clock = { + * currentTimeMillisUnsafe: () => 1_000, + * currentTimeMillis: Effect.succeed(1_000), + * monotonicTimeNanosUnsafe: () => 1_000_000_000n, + * monotonicTimeNanos: Effect.succeed(1_000_000_000n), + * currentTimeNanosUnsafe: () => 1_000_000_000n, + * currentTimeNanos: Effect.succeed(1_000_000_000n), + * sleep: () => Effect.void + * } + * + * const program = Clock.clockWith((clock) => Effect.sync(() => clock.currentTimeMillisUnsafe())) + * + * await Effect.runPromise(Effect.provideService(program, Clock.Clock, testClock)) // => 1_000 * ``` * * @see {@link Clock} for the service reference * @see {@link currentTimeMillis} for convenience accessor that returns milliseconds * @see {@link currentTimeNanos} for convenience accessor that returns nanoseconds - * @category constructors + * @category accessors * @since 2.0.0 */ export const clockWith: (f: (clock: Clock) => Effect) => Effect = effect.clockWith /** - * Returns an Effect that succeeds with the current time in milliseconds. + * Returns an Effect that succeeds with the current Unix time in milliseconds. * * **When to use** * - * Use to read wall-clock time from the active Clock service with millisecond - * precision. + * Use to create wall-clock timestamps from the active `Clock` service with + * millisecond precision. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. * * **Example** (Reading milliseconds) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const currentTime = yield* Clock.currentTimeMillis - * console.log(`Current time: ${currentTime}ms`) - * return currentTime - * }) + * const testClock: Clock.Clock = { + * currentTimeMillisUnsafe: () => 1_000, + * currentTimeMillis: Effect.succeed(1_000), + * monotonicTimeNanosUnsafe: () => 1_000_000_000n, + * monotonicTimeNanos: Effect.succeed(1_000_000_000n), + * currentTimeNanosUnsafe: () => 1_000_000_000n, + * currentTimeNanos: Effect.succeed(1_000_000_000n), + * sleep: () => Effect.void + * } + * + * await Effect.runPromise(Effect.provideService(Clock.currentTimeMillis, Clock.Clock, testClock)) // => 1_000 * ``` * * @see {@link currentTimeNanos} for nanosecond precision + * @see {@link monotonicTimeNanos} for measuring elapsed time * @see {@link clockWith} for accessing the full Clock service * - * @category constructors + * @category accessors * @since 2.0.0 */ export const currentTimeMillis: Effect = effect.currentTimeMillis /** - * Returns an Effect that succeeds with the current time in nanoseconds. + * Returns an Effect that succeeds with the current Unix time in nanoseconds. * * **When to use** * - * Use to read wall-clock time from the active `Clock` service with nanosecond - * precision. + * Use to create wall-clock timestamps from the active `Clock` service with + * nanosecond precision. + * + * **Gotchas** + * + * The value can move backward or forward when the system wall clock is + * corrected, so it is not suitable for measuring elapsed time. + * The live clock allows up to one second of drift from `Date.now()` before + * re-anchoring. * * **Example** (Reading nanoseconds) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const currentTime = yield* Clock.currentTimeNanos - * console.log(`Current time: ${currentTime}ns`) - * return currentTime - * }) + * const testClock: Clock.Clock = { + * currentTimeMillisUnsafe: () => 1_000, + * currentTimeMillis: Effect.succeed(1_000), + * monotonicTimeNanosUnsafe: () => 1_000_000_000n, + * monotonicTimeNanos: Effect.succeed(1_000_000_000n), + * currentTimeNanosUnsafe: () => 1_000_000_000n, + * currentTimeNanos: Effect.succeed(1_000_000_000n), + * sleep: () => Effect.void + * } + * + * await Effect.runPromise(Effect.provideService(Clock.currentTimeNanos, Clock.Clock, testClock)) // => 1_000_000_000n * ``` * - * @category constructors + * @see {@link monotonicTimeNanos} for measuring elapsed time + * + * @category accessors * @since 2.0.0 */ export const currentTimeNanos: Effect = effect.currentTimeNanos + +/** + * Returns an Effect that succeeds with the current monotonic time in + * nanoseconds. + * + * **When to use** + * + * Use to measure elapsed time by subtracting two readings. + * + * **Gotchas** + * + * The value has an arbitrary origin and is unsuitable for serialization. Use + * it only to subtract readings produced by the same clock. Whether it advances + * while the host is suspended depends on the runtime. + * + * @see {@link currentTimeNanos} for Unix wall-clock timestamps + * + * @category accessors + * @since 4.0.0 + */ +export const monotonicTimeNanos: Effect = effect.monotonicTimeNanos diff --git a/.context/effect/packages/effect/src/Combiner.ts b/.context/effect/packages/effect/src/Combiner.ts index 1e9ac7c78..75ab2d492 100644 --- a/.context/effect/packages/effect/src/Combiner.ts +++ b/.context/effect/packages/effect/src/Combiner.ts @@ -26,13 +26,12 @@ import type * as Order from "./Order.ts" * * **Example** (Combining numbers with addition) * - * ```ts + * ```ts import.meta.vitest * import { Combiner } from "effect" * * const Sum = Combiner.make((self, that) => self + that) * - * console.log(Sum.combine(3, 4)) - * // Output: 7 + * Sum.combine(3, 4) // => 7 * ``` * * @see {@link make} – create a `Combiner` from a function @@ -65,13 +64,12 @@ export interface Combiner { * * **Example** (Multiplying numbers) * - * ```ts + * ```ts import.meta.vitest * import { Combiner } from "effect" * * const Product = Combiner.make((self, that) => self * that) * - * console.log(Product.combine(3, 5)) - * // Output: 15 + * Product.combine(3, 5) // => 15 * ``` * * @see {@link Combiner} – the interface this creates @@ -97,13 +95,12 @@ export function make(combine: (self: A, that: A) => A): Combiner { * * **Example** (Reversing string concatenation) * - * ```ts + * ```ts import.meta.vitest * import { Combiner, String } from "effect" * * const Prepend = Combiner.flip(String.ReducerConcat) * - * console.log(Prepend.combine("a", "b")) - * // Output: "ba" + * Prepend.combine("a", "b") // => "ba" * ``` * * @see {@link make} @@ -130,16 +127,13 @@ export function flip(combiner: Combiner): Combiner { * * **Example** (Selecting the minimum of two numbers) * - * ```ts + * ```ts import.meta.vitest * import { Combiner, Number } from "effect" * * const Min = Combiner.min(Number.Order) * - * console.log(Min.combine(3, 1)) - * // Output: 1 - * - * console.log(Min.combine(1, 3)) - * // Output: 1 + * Min.combine(3, 1) // => 1 + * Min.combine(1, 3) // => 1 * ``` * * @see {@link max} @@ -166,16 +160,13 @@ export function min(order: Order.Order): Combiner { * * **Example** (Selecting the maximum of two numbers) * - * ```ts + * ```ts import.meta.vitest * import { Combiner, Number } from "effect" * * const Max = Combiner.max(Number.Order) * - * console.log(Max.combine(3, 1)) - * // Output: 3 - * - * console.log(Max.combine(1, 3)) - * // Output: 3 + * Max.combine(3, 1) // => 3 + * Max.combine(1, 3) // => 3 * ``` * * @see {@link min} @@ -199,13 +190,12 @@ export function max(order: Order.Order): Combiner { * * **Example** (Keeping the first value) * - * ```ts + * ```ts import.meta.vitest * import { Combiner } from "effect" * * const First = Combiner.first() * - * console.log(First.combine(1, 2)) - * // Output: 1 + * First.combine(1, 2) // => 1 * ``` * * @see {@link last} @@ -229,13 +219,12 @@ export function first(): Combiner { * * **Example** (Keeping the last value) * - * ```ts + * ```ts import.meta.vitest * import { Combiner } from "effect" * * const Last = Combiner.last() * - * console.log(Last.combine(1, 2)) - * // Output: 2 + * Last.combine(1, 2) // => 2 * ``` * * @see {@link first} @@ -261,13 +250,12 @@ export function last(): Combiner { * * **Example** (Always returning zero) * - * ```ts + * ```ts import.meta.vitest * import { Combiner } from "effect" * * const Zero = Combiner.constant(0) * - * console.log(Zero.combine(42, 99)) - * // Output: 0 + * Zero.combine(42, 99) // => 0 * ``` * * @see {@link first} @@ -297,13 +285,12 @@ export function constant(a: A): Combiner { * * **Example** (Joining strings with a separator) * - * ```ts + * ```ts import.meta.vitest * import { Combiner, String } from "effect" * * const commaSep = Combiner.intercalate(",")(String.ReducerConcat) * - * console.log(commaSep.combine("a", "b")) - * // Output: "a,b" + * commaSep.combine("a", "b") // => "a,b" * ``` * * @see {@link make} diff --git a/.context/effect/packages/effect/src/Config.ts b/.context/effect/packages/effect/src/Config.ts index 026b4e050..9de373eb1 100644 --- a/.context/effect/packages/effect/src/Config.ts +++ b/.context/effect/packages/effect/src/Config.ts @@ -11,11 +11,13 @@ import type { Path, SourceError } from "./ConfigProvider.ts" import * as ConfigProvider from "./ConfigProvider.ts" import * as Effect from "./Effect.ts" import * as Effectable from "./Effectable.ts" -import { dual } from "./Function.ts" +import { dual, memoize } from "./Function.ts" +import * as InternalRecord from "./internal/record.ts" import * as LogLevel_ from "./LogLevel.ts" import * as Option from "./Option.ts" import * as Predicate from "./Predicate.ts" import * as Rec from "./Record.ts" +import * as Result from "./Result.ts" import * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaGetter from "./SchemaGetter.ts" @@ -35,11 +37,11 @@ const TypeId = "~effect/Config" * * **Example** (Checking Config values) * - * ```ts + * ```ts import.meta.vitest * import { Config } from "effect" * - * console.log(Config.isConfig(Config.string("HOST"))) // true - * console.log(Config.isConfig("not a config")) // false + * Config.isConfig(Config.string("HOST")) // => true + * Config.isConfig("not a config") // => false * ``` * * @category guards @@ -62,7 +64,7 @@ export const isConfig = (u: unknown): u is Config => Predicate.hasPrope * (wrong type, out of range, missing key, etc.). * * @see {@link orElse} – recover from a ConfigError - * @see {@link withDefault} – provide a fallback for missing-data errors + * @see {@link withDefault} – provide a fallback when relevant input is absent * * @category errors * @since 4.0.0 @@ -93,9 +95,7 @@ export class ConfigError { * **Details** * * Key members: - * - `parse(provider, pathPrefix?)` – runs the config against a specific provider. - * The optional path prefix is the logical scope accumulated from outer - * `Config.nested` calls. + * - `parse(provider)` – runs the config against a specific provider. * - Yieldable – can be yielded inside `Effect.gen`, which automatically * resolves the current `ConfigProvider` from the context. * - Pipeable – supports `.pipe(Config.map(...))` etc. @@ -107,10 +107,39 @@ export class ConfigError { */ export interface Config extends Effect.Effect { readonly [TypeId]: typeof TypeId - readonly parse: ( - provider: ConfigProvider.ConfigProvider, - pathPrefix?: Path - ) => Effect.Effect + readonly parse: (provider: ConfigProvider.ConfigProvider) => Effect.Effect +} + +// Config composition needs to distinguish an absent recipe from a hard failure +// before the public Effect error channel is finalized. `hasInput` records +// provider evidence separately from the value, because successful values such +// as `undefined` and values supplied by defaults are not evidence of input. +// Hard failures carry the same evidence so recovery cannot erase it. +interface Resolved { + readonly _tag: "Resolved" + readonly value: T + readonly hasInput: boolean +} + +interface Absent { + readonly _tag: "Absent" + readonly error: ConfigError +} + +type Resolution = Resolved | Absent + +interface EvaluationFailure { + readonly error: ConfigError + readonly hasInput: boolean +} + +type Evaluator = ( + provider: ConfigProvider.ConfigProvider, + pathPrefix: Path +) => Effect.Effect, EvaluationFailure> + +interface ConfigImpl extends Config { + readonly evaluator: Evaluator } const Proto = { @@ -129,13 +158,71 @@ const Proto = { } function make( - parse: (provider: ConfigProvider.ConfigProvider, pathPrefix: Path) => Effect.Effect + evaluator: Evaluator ): Config { const self = Object.create(Proto) - self.parse = (provider: ConfigProvider.ConfigProvider, pathPrefix: Path = []) => parse(provider, pathPrefix) + self.evaluator = evaluator + self.parse = (provider: ConfigProvider.ConfigProvider) => + evaluator(provider, []).pipe( + Effect.mapErrorEager((failure) => failure.error), + Effect.flatMapEager((resolution) => + resolution._tag === "Resolved" ? Effect.succeed(resolution.value) : Effect.fail(resolution.error) + ) + ) return self } +const evaluateAt = ( + self: Config, + provider: ConfigProvider.ConfigProvider, + pathPrefix: Path +): Effect.Effect, EvaluationFailure> => (self as ConfigImpl).evaluator(provider, pathPrefix) + +const resolved = (value: T, hasInput: boolean): Resolution => ({ + _tag: "Resolved", + value, + hasInput +}) + +const absent = (error: ConfigError): Absent => ({ + _tag: "Absent", + error +}) + +const evaluationFailure = (error: ConfigError, hasInput: boolean): EvaluationFailure => ({ + error, + hasInput +}) + +const isSourceError = (u: unknown): u is ConfigProvider.SourceError => Predicate.isTagged(u, "SourceError") + +const catchSourceError = ( + self: Effect.Effect, + hasInput: boolean +): Effect.Effect => + self.pipe( + Effect.catchDefect((defect) => + isSourceError(defect) + ? Effect.fail(evaluationFailure(new ConfigError(defect), hasInput)) + : Effect.die(defect) + ) + ) + +const preserveInputEvidence = ( + self: Effect.Effect, EvaluationFailure>, + hasInput: boolean +): Effect.Effect, EvaluationFailure> => { + if (!hasInput) return self + return self.pipe( + Effect.mapErrorEager((failure) => evaluationFailure(failure.error, true)), + Effect.flatMapEager((resolution) => + resolution._tag === "Resolved" + ? Effect.succeed(resolved(resolution.value, true)) + : Effect.fail(evaluationFailure(resolution.error, true)) + ) + ) +} + /** * Transforms the parsed value of a config with a pure function. * @@ -146,7 +233,7 @@ function make( * * **Example** (Uppercasing a string config) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const upper = Config.string("name").pipe( @@ -154,7 +241,7 @@ function make( * ) * * const provider = ConfigProvider.fromUnknown({ name: "alice" }) - * // Effect.runSync(upper.parse(provider)) // "ALICE" + * Effect.runSync(upper.parse(provider)) // => "ALICE" * ``` * * @see {@link mapOrFail} – when the transformation can fail @@ -166,7 +253,12 @@ export const map: { (f: (a: A) => B): (self: Config) => Config (self: Config, f: (a: A) => B): Config } = dual(2, (self: Config, f: (a: A) => B): Config => { - return make((provider, pathPrefix) => Effect.map(self.parse(provider, pathPrefix), f)) + return make((provider, pathPrefix) => + Effect.map(evaluateAt(self, provider, pathPrefix), (resolution) => + resolution._tag === "Resolved" + ? resolved(f(resolution.value), resolution.hasInput) + : resolution) + ) }) /** @@ -179,12 +271,14 @@ export const map: { * * **Example** (Wrapping a value in an effectful transformation) * - * ```ts - * import { Config, Effect } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * const trimmed = Config.string("name").pipe( * Config.mapOrFail((s) => Effect.succeed(s.trim())) * ) + * const provider = ConfigProvider.fromUnknown({ name: " Alice " }) + * Effect.runSync(trimmed.parse(provider)) // => "Alice" * ``` * * @see {@link map} – when the transformation cannot fail @@ -196,7 +290,15 @@ export const mapOrFail: { (f: (a: A) => Effect.Effect): (self: Config) => Config (self: Config, f: (a: A) => Effect.Effect): Config } = dual(2, (self: Config, f: (a: A) => Effect.Effect): Config => { - return make((provider, pathPrefix) => Effect.flatMap(self.parse(provider, pathPrefix), f)) + return make((provider, pathPrefix) => + Effect.flatMap(evaluateAt(self, provider, pathPrefix), (resolution) => + resolution._tag === "Resolved" + ? f(resolution.value).pipe( + Effect.mapEager((value) => resolved(value, resolution.hasInput)), + Effect.mapErrorEager((error) => evaluationFailure(error, resolution.hasInput)) + ) + : Effect.succeed(resolution)) + ) }) /** @@ -209,21 +311,30 @@ export const mapOrFail: { * * **Details** * - * Unlike {@link withDefault}, this catches **all** `ConfigError`s (not just - * missing data). The fallback function receives the error and returns a new + * Unlike {@link withDefault}, this handles both semantic absence and **all** + * `ConfigError`s. The fallback function receives the error and returns a new * `Config`. * + * **Gotchas** + * + * Recovery preserves whether the primary config read provider input. When the + * recovered config is composed with {@link all}, invalid input in the primary + * branch still makes the enclosing group partially supplied, so an outer + * {@link withDefault} or {@link option} does not replace the whole group. + * * **Example** (Falling back to a literal) * - * ```ts - * import { Config } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * const hostConfig = Config.string("HOST").pipe( * Config.orElse(() => Config.succeed("localhost")) * ) + * const provider = ConfigProvider.fromUnknown({}) + * Effect.runSync(hostConfig.parse(provider)) // => "localhost" * ``` * - * @see {@link withDefault} – fallback only on missing data + * @see {@link withDefault} – fallback only on semantic absence * * @category combinators * @since 2.0.0 @@ -232,8 +343,18 @@ export const orElse: { (that: (error: ConfigError) => Config): (self: Config) => Config (self: Config, that: (error: ConfigError) => Config): Config } = dual(2, (self: Config, that: (error: ConfigError) => Config): Config => { - return make((provider, pathPrefix) => - Effect.catch(self.parse(provider, pathPrefix), (error) => that(error).parse(provider, pathPrefix)) + return make((provider, pathPrefix) => + Effect.matchEffect(evaluateAt(self, provider, pathPrefix), { + onFailure: (failure) => + preserveInputEvidence( + evaluateAt(that(failure.error), provider, pathPrefix), + failure.hasInput + ), + onSuccess: (resolution): Effect.Effect, EvaluationFailure> => + resolution._tag === "Absent" + ? evaluateAt(that(resolution.error), provider, pathPrefix) + : Effect.succeed(resolution) + }) ) }) @@ -249,9 +370,19 @@ export const orElse: { * Accepts a tuple (preserves positions), an iterable, or a record of configs. * Returns a config whose parsed value mirrors the input shape. * + * A combined config is absent when at least one child cannot resolve and none + * of the other children read provider input. This lets {@link withDefault} and + * {@link option} handle a wholly absent group. Once any child reads input, a + * missing sibling makes the group incomplete and parsing fails. Values supplied + * by child defaults do not count as provider input. + * + * Unlike a `Schema.Struct` passed to {@link schema}, `all` only considers input + * read by its children. An explicitly present but empty parent container does + * not by itself make the group present. + * * **Example** (Combining configs as a struct) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const dbConfig = Config.all({ @@ -260,8 +391,7 @@ export const orElse: { * }) * * const provider = ConfigProvider.fromUnknown({ host: "localhost", port: 5432 }) - * // Effect.runSync(dbConfig.parse(provider)) - * // { host: "localhost", port: 5432 } + * Effect.runSync(dbConfig.parse(provider)) // => { host: "localhost", port: 5432 } * ``` * * @category combinators @@ -286,46 +416,85 @@ export function all> | Record - Effect.all(configs.map((config) => config.parse(provider, pathPrefix))) + Effect.flatMapEager( + Effect.all(configs.map((config) => Effect.result(evaluateAt(config, provider, pathPrefix)))), + resolveArray + ) ) as any } else { return make((provider, pathPrefix) => - Effect.all(Rec.map(configs, (config) => config.parse(provider, pathPrefix))) + Effect.flatMapEager( + Effect.all(Rec.map(configs, (config) => Effect.result(evaluateAt(config, provider, pathPrefix)))), + resolveRecord + ) ) as any } } -function isMissingDataOnly(issue: SchemaIssue.Issue): boolean { - switch (issue._tag) { - case "MissingKey": - return true - case "InvalidType": - case "InvalidValue": - return Option.isNone(issue.actual) || (Option.isSome(issue.actual) && issue.actual.value === undefined) - case "OneOf": - return issue.actual === undefined - case "Encoding": - return Option.isNone(issue.actual) || (Option.isSome(issue.actual) && issue.actual.value === undefined) - ? true - : isMissingDataOnly(issue.issue) - case "Pointer": - return isMissingDataOnly(issue.issue) - case "Filter": - case "UnexpectedKey": - case "Forbidden": - return false - case "Composite": - return issue.issues.every(isMissingDataOnly) - case "AnyOf": - if (issue.issues.length === 0) { - return issue.actual === undefined - } - return issue.issues.every(isMissingDataOnly) +const resolveArray = ( + results: ReadonlyArray, EvaluationFailure>> +): Effect.Effect>, EvaluationFailure> => { + const values: Array = [] + let firstFailure: EvaluationFailure | undefined + let firstAbsent: Absent | undefined + let hasInput = false + for (const result of results) { + if (Result.isFailure(result)) { + firstFailure ??= result.failure + hasInput = hasInput || result.failure.hasInput + continue + } + const resolution = result.success + if (resolution._tag === "Absent") { + firstAbsent ??= resolution + } else { + values.push(resolution.value) + hasInput = hasInput || resolution.hasInput + } } + if (firstFailure !== undefined) { + return Effect.fail(evaluationFailure(firstFailure.error, hasInput)) + } + if (firstAbsent !== undefined) { + return hasInput ? Effect.fail(evaluationFailure(firstAbsent.error, true)) : Effect.succeed(firstAbsent) + } + return Effect.succeed(resolved(values, hasInput)) +} + +const resolveRecord = ( + results: Record, EvaluationFailure>> +): Effect.Effect>, EvaluationFailure> => { + const values: Record = {} + let firstFailure: EvaluationFailure | undefined + let firstAbsent: Absent | undefined + let hasInput = false + for (const key in results) { + const result = results[key] + if (Result.isFailure(result)) { + firstFailure ??= result.failure + hasInput = hasInput || result.failure.hasInput + continue + } + const resolution = result.success + if (resolution._tag === "Absent") { + firstAbsent ??= resolution + } else { + InternalRecord.assignProperty(values, key, resolution.value) + hasInput = hasInput || resolution.hasInput + } + } + if (firstFailure !== undefined) { + return Effect.fail(evaluationFailure(firstFailure.error, hasInput)) + } + if (firstAbsent !== undefined) { + return hasInput ? Effect.fail(evaluationFailure(firstAbsent.error, true)) : Effect.succeed(firstAbsent) + } + return Effect.succeed(resolved(values, hasInput)) } /** - * Provides a fallback value when the config fails due to missing data. + * Provides a fallback value when the config cannot resolve because none of its + * relevant input is present. * * **When to use** * @@ -333,23 +502,25 @@ function isMissingDataOnly(issue: SchemaIssue.Issue): boolean { * * **Gotchas** * - * Only applies when the error is a `SchemaError` caused exclusively by - * missing data (missing keys, undefined values). Validation errors (wrong - * type, out of range) still propagate. + * Validation errors and partially supplied groups still propagate. A schema + * that successfully decodes absent input also keeps its decoded value instead + * of using the default. Schema configs first represent a missing or + * incompatible provider shape as `undefined`; the default is used only when + * the schema rejects that value and no relevant input was found. * * **Example** (Defaulting a missing port) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const port = Config.number("port").pipe(Config.withDefault(3000)) * * const provider = ConfigProvider.fromUnknown({}) - * // Effect.runSync(port.parse(provider)) // 3000 + * Effect.runSync(port.parse(provider)) // => 3000 * ``` * * @see {@link option} – returns `Option` instead of a default value - * @see {@link orElse} – catches all errors, not just missing data + * @see {@link orElse} – catches all errors, not just absent input * * @category combinators * @since 2.0.0 @@ -358,20 +529,17 @@ export const withDefault: { (defaultValue: A2): (self: Config) => Config (self: Config, defaultValue: A2): Config } = dual(2, (self: Config, defaultValue: A2): Config => { - return orElse(self, (err) => { - if (Schema.isSchemaError(err.cause)) { - const issue = err.cause.issue - if (isMissingDataOnly(issue)) { - return succeed(defaultValue) - } - } - return fail(err.cause) - }) + return make((provider, pathPrefix) => + Effect.mapEager( + evaluateAt(self, provider, pathPrefix), + (resolution) => resolution._tag === "Absent" ? resolved(defaultValue, false) : resolution + ) + ) }) /** - * Makes a config optional: returns `Some(value)` on success and `None` when - * data is missing. + * Makes a config optional: returns `Some(value)` on success and `None` when the + * config cannot resolve because none of its relevant input is present. * * **When to use** * @@ -379,18 +547,21 @@ export const withDefault: { * * **Gotchas** * - * Like {@link withDefault}, only missing-data errors produce `None`. - * Validation errors still propagate. + * Validation errors and partially supplied groups still propagate. Successful + * values are always wrapped in `Some`, including `undefined` when the schema + * explicitly accepts it. Schema configs first represent a missing or + * incompatible provider shape as `undefined`; `None` is returned only when the + * schema rejects that value and no relevant input was found. * * **Example** (Reading optional config) * - * ```ts - * import { Config, ConfigProvider, Effect } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect, Option } from "effect" * * const maybePort = Config.option(Config.number("port")) * * const provider = ConfigProvider.fromUnknown({}) - * // Effect.runSync(maybePort.parse(provider)) // { _tag: "None" } + * Effect.runSync(maybePort.parse(provider)) // => Option.none() * ``` * * @see {@link withDefault} – provide a concrete fallback value instead @@ -432,7 +603,7 @@ export type Success = [T] extends [Config] ? A : never * * @see {@link unwrap} – construct a `Config` from a `Wrap` * - * @category Wrap + * @category utility types * @since 2.0.0 */ export type Wrap = [NonNullable] extends [infer T] ? [IsPlainObject] extends [true] ? @@ -460,8 +631,8 @@ type IsPlainObject = [A] extends [Record] * * **Example** (Unwrapping a record of configs) * - * ```ts - * import { Config } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * interface Options { * key: string @@ -469,128 +640,158 @@ type IsPlainObject = [A] extends [Record] * * const makeConfig = (config: Config.Wrap): Config.Config => * Config.unwrap(config) + * + * const config = makeConfig({ key: Config.string("key") }) + * const provider = ConfigProvider.fromUnknown({ key: "value" }) + * Effect.runSync(config.parse(provider)) // => { key: "value" } * ``` * * @see {@link Wrap} – the utility type accepted by this function * - * @category Wrap + * @category converting * @since 2.0.0 */ export const unwrap = (wrapped: Wrap): Config => { if (isConfig(wrapped)) return wrapped - return make((provider, pathPrefix) => { - const entries = Object.entries(wrapped) - const configs = entries.map(([key, config]) => - unwrap(config as any).parse(provider, pathPrefix).pipe(Effect.map((value) => [key, value] as const)) - ) - return Effect.all(configs).pipe(Effect.map(Object.fromEntries)) - }) + return all(Rec.map(wrapped as Record>, (config) => unwrap(config))) as Config } // ----------------------------------------------------------------------------- // schema // ----------------------------------------------------------------------------- -const dump: ( +interface ConfigCursor { + readonly provider: ConfigProvider.ConfigProvider + readonly path: Path + readonly node: ConfigProvider.Node | undefined + readonly toString: () => string +} + +const cursorToString = (): string => "" + +const loadCursor: ( provider: ConfigProvider.ConfigProvider, path: Path -) => Effect.Effect = Effect.fnUntraced(function*( - provider, - path -) { - const stat = yield* provider.load(path) - if (stat === undefined) return undefined - switch (stat._tag) { - case "Value": - return stat.value - case "Record": { - if (stat.value !== undefined) return stat.value - const out: Record = {} - for (const key of stat.keys) { - const child = yield* dump(provider, [...path, key]) - if (child !== undefined) out[key] = child - } - return out - } - case "Array": { - if (stat.value !== undefined) return stat.value - const out: Array = [] - for (let i = 0; i < stat.length; i++) { - out.push(yield* dump(provider, [...path, i])) - } - return out - } +) => Effect.Effect = (provider, path) => + provider.load(path).pipe( + Effect.orDie, + Effect.mapEager((node) => ({ provider, path, node, toString: cursorToString })) + ) + +const loadChildCursor = (cursor: ConfigCursor, segment: string | number): Effect.Effect => + loadCursor(cursor.provider, [...cursor.path, segment]) + +const getScalar = (node: ConfigProvider.Node | undefined): string | undefined => node?.value + +const decodeFromCursor = ( + ast: SchemaAST.AST, + decode: (cursor: ConfigCursor) => Effect.Effect +): SchemaAST.AST => + SchemaAST.decodeTo( + SchemaAST.unknown, + ast, + new SchemaTransformation.Transformation( + SchemaGetter.transformOrFail((input: unknown) => decode(input as ConfigCursor)), + SchemaGetter.passthrough() + ) + ) + +const isScalarInput = (ast: SchemaAST.AST): boolean => { + switch (ast._tag) { + case "Union": + return ast.types.every(isScalarInput) + case "Objects": + case "Arrays": + case "Suspend": + return false + default: + return true } -}) +} -const recur: ( +const hasProviderInput = ( ast: SchemaAST.AST, - provider: ConfigProvider.ConfigProvider, - path: Path -) => Effect.Effect = Effect.fnUntraced( - function*(ast, provider, path) { + node: ConfigProvider.Node | undefined +): boolean => { + switch (ast._tag) { + case "Objects": + return node?._tag === "Record" + case "Arrays": + return node?._tag === "Array" + case "Union": + return ast.types.some((ast) => hasProviderInput(ast, node)) + case "Suspend": + return hasProviderInput(ast.thunk(), node) + default: + return getScalar(node) !== undefined + } +} + +const toConfigCursorAST = memoize((root: SchemaAST.AST): SchemaAST.AST => { + const seen = new WeakSet() + const recur = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { + seen.add(ast) switch (ast._tag) { case "Objects": { - const stat = yield* provider.load(path) - if (stat === undefined && path.length > 0) return undefined - const out: Record = {} - for (const ps of ast.propertySignatures) { - const name = ps.name - if (typeof name === "string") { - const value = yield* recur(ps.type, provider, [...path, name]) - if (value !== undefined) out[name] = value + const matchesIndex = ast.indexSignatures.map((is) => SchemaParser._is(is.parameter)) + const materialize = Effect.fnUntraced(function*(cursor: ConfigCursor) { + if (cursor.node?._tag !== "Record") { + return undefined } - } - if (ast.indexSignatures.length > 0) { - if (stat && stat._tag === "Record") { - for (const is of ast.indexSignatures) { - const matches = SchemaParser._is(is.parameter) - for (const key of stat.keys) { - if (!Object.hasOwn(out, key) && matches(key)) { - const value = yield* recur(is.type, provider, [...path, key]) - if (value !== undefined) out[key] = value - } - } + const node = cursor.node + const keys = new Set() + for (const property of ast.propertySignatures) { + if (typeof property.name === "string") keys.add(property.name) + } + if (matchesIndex.length > 0) { + for (const key of node.keys) { + if (matchesIndex.some((matches) => matches(key))) keys.add(key) } } - } - return out + const out: Record = {} + for (const key of keys) { + const child = yield* loadChildCursor(cursor, key) + if (child.node !== undefined) InternalRecord.assignProperty(out, key, child) + } + return out + }) + return decodeFromCursor(ast.recur(recur, (ast) => ast), materialize) } case "Arrays": { - const stat = yield* provider.load(path) - if (stat === undefined) return undefined - if (stat && stat._tag === "Value") return stat.value === "" ? [] : stat.value.split(",") - if (stat && stat._tag === "Array" && stat.value !== undefined) { - return stat.value === "" ? [] : stat.value.split(",") - } - const out: Array = [] - const length = stat && stat._tag === "Array" ? stat.length : ast.elements.length - for (let i = 0; i < length; i++) { - const element = ast.elements[i] ?? ast.rest[0] - if (element !== undefined) { - out.push(yield* recur(element, provider, [...path, i])) + const materialize = Effect.fnUntraced(function*(cursor: ConfigCursor) { + if (cursor.node?._tag !== "Array") { + return undefined } - } - return out + const out: Array = [] + for (let i = 0; i < cursor.node.length; i++) { + out.push(yield* loadChildCursor(cursor, i)) + } + return out + }) + return decodeFromCursor(ast.recur(recur), materialize) } case "Union": - // Let downstream decoding decide; dump can return a string, object, or array. - return yield* dump(provider, path) - case "Suspend": - return yield* recur(ast.thunk(), provider, path) - default: { - // Base primitives / string-like encoded nodes. - const stat = yield* provider.load(path) - if (stat === undefined) return undefined - if (stat._tag === "Value") return stat.value - if (stat._tag === "Record" && stat.value !== undefined) return stat.value - if (stat._tag === "Array" && stat.value !== undefined) return stat.value - // Container without a co-located value cannot satisfy a scalar request. - return undefined + for (const member of ast.types) { + recur(member) + } + return isScalarInput(ast) + ? decodeFromCursor(ast, (cursor) => Effect.succeed(getScalar(cursor.node))) + : ast.recur(recur) + case "Suspend": { + const target = ast.thunk() + // Force new branches so opaque encodings fail when the Config is constructed. + if (!seen.has(target)) recur(target) + return ast.recur(recur) } + case "Declaration": + case "Any": + throw new globalThis.Error("Config.schema does not support opaque StringTree encodings", { cause: ast }) + default: + return decodeFromCursor(ast, (cursor) => Effect.succeed(getScalar(cursor.node))) } - } -) + }) + return recur(root) +}) /** * Creates a `Config` from a `Schema.Codec`. @@ -609,12 +810,47 @@ const recur: ( * Convenience constructors such as `string`, `number`, and `boolean` delegate * to this API. * - * The codec is used to decode the raw `StringTree` produced by the provider - * into `T`. Schema validation errors are wrapped in `ConfigError`. + * The codec is converted to its canonical `StringTree` form. Its encoded shape + * determines how provider data is loaded: scalar schemas read a co-located + * scalar value, object schemas read declared properties and matching record + * keys, and array schemas read indexed children. A mixed-shape union loads each + * member according to that member's shape before applying the union's mode and + * checks. + * + * At the config's lookup path, a missing node or a node that cannot provide the + * representation required by the schema is decoded as `undefined`. Missing + * object properties remain omitted so the schema's property semantics still + * apply. Decoding success always wins, even when no provider input was found. + * For example, + * `Schema.UndefinedOr(Schema.String)` decodes to `undefined` and is not replaced + * by {@link withDefault}. If decoding fails and no relevant representation was + * found, the config is absent. Invalid data in a relevant representation is a + * validation failure. Provider `SourceError`s are always failures. + * + * **Gotchas** + * + * Plain `Schema.Array` and `Schema.Record` schemas use structural provider + * input. Use {@link Array} or {@link Record} when a flat separated string must + * also be accepted. + * + * `Schema.Struct` and {@link all} describe different lookup models. An + * explicitly present empty object is relevant input for a struct and required + * fields are validated. The same empty parent container does not make an + * `all` group present when all of its child configs are absent. + * + * The canonical `StringTree` encoding must expose a concrete scalar, object, + * array, or union shape. Opaque encodings such as `Schema.Any`, + * `Schema.Unknown`, `Schema.ObjectKeyword`, `Schema.Json`, and + * `Schema.MutableJson` are rejected synchronously when this config is + * constructed, including when they are nested in another schema. Suspended + * recursive schemas remain supported when their eventual shape is concrete. + * Declarations such as `Schema.URL` also remain supported when their canonical + * encoding has a concrete shape. To read arbitrary JSON from one scalar value, + * use `Schema.fromJsonString(Schema.Json)`. * * **Example** (Reading a structured config) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect, Schema } from "effect" * * const DbConfig = Config.schema( @@ -629,8 +865,7 @@ const recur: ( * db: { host: "localhost", port: 5432 } * }) * - * // Effect.runSync(DbConfig.parse(provider)) - * // { host: "localhost", port: 5432 } + * Effect.runSync(DbConfig.parse(provider)) // => { host: "localhost", port: 5432 } * ``` * * @see {@link string} / {@link number} / {@link boolean} – shortcuts for @@ -641,20 +876,31 @@ const recur: ( */ export function schema(codec: Schema.ConstraintCodec, path?: string | ConfigProvider.Path): Config { const codecStringTree = Schema.toCodecStringTree(codec) - const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(codecStringTree) - const codecStringTreeEncoded = SchemaAST.toEncoded(codecStringTree.ast) + const encodedAst = SchemaAST.toEncoded(codecStringTree.ast) + const decodeCursor = SchemaParser.decodeUnknownEffect( + Schema.make>(toConfigCursorAST(codecStringTree.ast)) + ) const localPath = typeof path === "string" ? [path] : path ?? [] return make((provider, pathPrefix) => { const fullPath = [...pathPrefix, ...localPath] - return recur(codecStringTreeEncoded, provider, fullPath).pipe( - Effect.flatMapEager((tree) => - decodeUnknownEffect(tree).pipe( - Effect.mapErrorEager((issue) => - new Schema.SchemaError(fullPath.length > 0 ? new SchemaIssue.Pointer(fullPath, issue) : issue) - ) + return catchSourceError(loadCursor(provider, fullPath), false).pipe( + Effect.flatMapEager((cursor) => { + const hasInput = hasProviderInput(encodedAst, cursor.node) + return catchSourceError( + decodeCursor(cursor).pipe( + Effect.mapEager((value) => resolved(value, hasInput)), + Effect.catchEager((issue) => { + const error = new ConfigError( + new Schema.SchemaError(fullPath.length > 0 ? new SchemaIssue.Pointer(fullPath, issue) : issue) + ) + return hasInput + ? Effect.fail(evaluationFailure(error, true)) + : Effect.succeed(absent(error)) + }) + ), + hasInput ) - ), - Effect.mapErrorEager((cause) => new ConfigError(cause)) + }) ) }) } @@ -745,7 +991,7 @@ export const LogLevel = Schema.Literals(LogLevel_.values) * * **Example** (Parsing a comma-separated record) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect, Schema } from "effect" * * const schema = Config.Record(Schema.String, Schema.String) @@ -758,14 +1004,14 @@ export const LogLevel = Schema.Literals(LogLevel_.values) * } * }) * - * console.dir(Effect.runSync(config.parse(provider))) - * // { - * // 'service.name': 'my-service', - * // 'service.version': '1.0.0', - * // 'custom.attribute': 'value' - * // } + * const result = Effect.runSync(config.parse(provider)) + * result["service.name"] // => "my-service" + * result["service.version"] // => "1.0.0" + * result["custom.attribute"] // => "value" * ``` * + * @see {@link Array} for separated or structural array input + * * @category schemas * @since 4.0.0 */ @@ -774,35 +1020,31 @@ export const Record = readonly keyValueSeparator?: string | undefined }) => { const record = Schema.Record(key, value) + const split = SchemaTransformation.splitKeyValue(options) const recordString = Schema.String.pipe( - Schema.decodeTo( - Schema.Record(Schema.String, Schema.String), - SchemaTransformation.splitKeyValue(options) - ), - Schema.decodeTo(record) + Schema.decodeTo(Schema.toCodecStringTree(record), { + decode: split.decode, + encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + split.encode + ) + }) ) return Schema.Union([record, recordString]) } -/** - * @category schemas - * @since 4.0.0 - */ const ArrayConfig = (value: V, options?: { readonly separator?: string | undefined }) => { const array = Schema.Array(value) const separator = options?.separator ?? "," const arrayString = Schema.String.pipe( - Schema.decodeTo( - Schema.Array(Schema.String), - { - decode: SchemaGetter.split(options), - encode: SchemaGetter.transform((input: ReadonlyArray) => input.join(separator)) - } - ), - Schema.decodeTo(array) + Schema.decodeTo(Schema.toCodecStringTree(array), { + decode: SchemaGetter.split(options), + encode: SchemaGetter.passthrough, Schema.StringTree>({ strict: false }).compose( + SchemaGetter.transform((input) => input.join(separator)) + ) + }) ) return Schema.Union([arrayString, array]) @@ -822,6 +1064,8 @@ export { * Accepts either a JSON-like array from the provider or a flat string like * `"a,b,c"`. The `separator` defaults to `","` and can be customized. * + * @see {@link Record} for separated or structural record input + * * @category schemas * @since 4.0.0 */ @@ -844,7 +1088,7 @@ export { * @since 2.0.0 */ export function fail(err: SourceError | Schema.SchemaError) { - return make(() => Effect.fail(new ConfigError(err))) + return make(() => Effect.fail(evaluationFailure(new ConfigError(err), false))) } /** @@ -858,19 +1102,21 @@ export function fail(err: SourceError | Schema.SchemaError) { * * **Example** (Returning a constant fallback) * - * ```ts - * import { Config } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * const host = Config.string("HOST").pipe( * Config.orElse(() => Config.succeed("localhost")) * ) + * const provider = ConfigProvider.fromUnknown({}) + * Effect.runSync(host.parse(provider)) // => "localhost" * ``` * * @category constructors * @since 2.0.0 */ export function succeed(value: T) { - return make(() => Effect.succeed(value)) + return make(() => Effect.succeed(resolved(value, false))) } /** @@ -886,13 +1132,13 @@ export function succeed(value: T) { * * **Example** (Reading a string config) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const host = Config.string("HOST") * * const provider = ConfigProvider.fromUnknown({ HOST: "localhost" }) - * // Effect.runSync(host.parse(provider)) // "localhost" + * Effect.runSync(host.parse(provider)) // => "localhost" * ``` * * @see {@link nonEmptyString} – rejects empty strings @@ -1003,10 +1249,12 @@ export function int(name?: string) { * * **Example** (Restricting to a literal) * - * ```ts - * import { Config } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * const env = Config.literal("production", "ENV") + * const provider = ConfigProvider.fromUnknown({ ENV: "production" }) + * Effect.runSync(env.parse(provider)) // => "production" * ``` * * @see {@link literals} – accepts multiple literal values @@ -1030,10 +1278,12 @@ export function literal(literal: L, name?: str * * **Example** (Restricting to a set of literals) * - * ```ts - * import { Config } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect } from "effect" * * const env = Config.literals(["development", "production"], "ENV") + * const provider = ConfigProvider.fromUnknown({ ENV: "development" }) + * Effect.runSync(env.parse(provider)) // => "development" * ``` * * @see {@link literal} for accepting one specific literal value @@ -1062,13 +1312,10 @@ export function literals>( * * **Example** (Reading a boolean flag) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const flag = yield* Config.boolean("FEATURE_FLAG") - * console.log(flag) - * }) + * const program = Config.boolean("FEATURE_FLAG") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1078,8 +1325,7 @@ export function literals>( * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: true + * ) // => true * ``` * * @see {@link Boolean} for the underlying boolean codec @@ -1108,13 +1354,10 @@ export function boolean(name?: string) { * * **Example** (Reading a duration) * - * ```ts - * import { Config, ConfigProvider, Effect } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Duration, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const duration = yield* Config.duration("DURATION") - * console.log(duration) - * }) + * const program = Config.duration("DURATION").pipe(Effect.map(Duration.toMillis)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1124,8 +1367,7 @@ export function boolean(name?: string) { * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: Duration { _tag: "millis", value: 10000 } + * ) // => 10000 * ``` * * @see {@link schema} for decoding configuration values with a custom codec @@ -1150,13 +1392,10 @@ export function duration(name?: string) { * * **Example** (Reading a port) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const port = yield* Config.port("PORT") - * console.log(port) - * }) + * const program = Config.port("PORT") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1166,8 +1405,7 @@ export function duration(name?: string) { * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: 8080 + * ) // => 8080 * ``` * * @see {@link int} for integer config values outside the port range @@ -1196,13 +1434,10 @@ export function port(name?: string) { * * **Example** (Reading a log level) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const logLevel = yield* Config.logLevel("LOG_LEVEL") - * console.log(logLevel) - * }) + * const program = Config.logLevel("LOG_LEVEL") * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1212,8 +1447,7 @@ export function port(name?: string) { * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: "Info" + * ) // => "Info" * ``` * * @see {@link LogLevel} for the underlying log-level codec @@ -1240,13 +1474,10 @@ export function logLevel(name?: string) { * * **Example** (Reading a secret) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const apiKey = yield* Config.redacted("API_KEY") - * console.log(apiKey) - * }) + * const program = Config.redacted("API_KEY").pipe(Effect.map(String)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1256,8 +1487,7 @@ export function logLevel(name?: string) { * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: + * ) // => "" * ``` * * @see {@link string} for non-secret string settings @@ -1286,13 +1516,10 @@ export function redacted(name?: string) { * * **Example** (Reading a URL) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * - * const program = Effect.gen(function*() { - * const url = yield* Config.url("URL") - * console.log(url) - * }) + * const program = Config.url("URL").pipe(Effect.map((url) => url.href)) * * const provider = ConfigProvider.fromEnv({ * env: { @@ -1302,22 +1529,7 @@ export function redacted(name?: string) { * * Effect.runSync( * program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider)) - * ) - * // Output: - * // URL { - * // href: 'https://example.com/', - * // origin: 'https://example.com', - * // protocol: 'https:', - * // username: '', - * // password: '', - * // host: 'example.com', - * // hostname: 'example.com', - * // port: '', - * // pathname: '/', - * // search: '', - * // searchParams: URLSearchParams {}, - * // hash: '' - * // } + * ) // => "https://example.com/" * ``` * * @see {@link schema} for decoding configuration values with a custom codec @@ -1338,7 +1550,7 @@ export function url(name?: string) { * * **Details** * - * Shortcut for `Config.schema(Schema.DateValid, name)`. + * Shortcut for `Config.schema(Schema.Date, name)`. * * **Gotchas** * @@ -1346,21 +1558,20 @@ export function url(name?: string) { * * **Example** (Reading a date) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const createdAt = Config.date("CREATED_AT") * * const provider = ConfigProvider.fromUnknown({ CREATED_AT: "2024-01-15" }) - * // Effect.runSync(createdAt.parse(provider)) - * // Date("2024-01-15T00:00:00.000Z") + * Effect.runSync(createdAt.parse(provider)).toISOString() // => "2024-01-15T00:00:00.000Z" * ``` * * @category constructors * @since 2.0.0 */ export function date(name?: string) { - return schema(Schema.DateValid, name) + return schema(Schema.Date, name) } /** @@ -1381,7 +1592,7 @@ export function date(name?: string) { * * **Example** (Nesting a struct config under `"database"`) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const dbConfig = Config.all({ @@ -1392,13 +1603,12 @@ export function date(name?: string) { * const provider = ConfigProvider.fromUnknown({ * database: { host: "localhost", port: "5432" } * }) - * // Effect.runSync(dbConfig.parse(provider)) - * // { host: "localhost", port: 5432 } + * Effect.runSync(dbConfig.parse(provider)) // => { host: "localhost", port: 5432 } * ``` * * **Example** (Reading env vars with a nested prefix) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const host = Config.string("host").pipe(Config.nested("database")) @@ -1406,7 +1616,7 @@ export function date(name?: string) { * const provider = ConfigProvider.fromEnv({ * env: { database_host: "localhost" } * }) - * // Effect.runSync(host.parse(provider)) // "localhost" + * Effect.runSync(host.parse(provider)) // => "localhost" * ``` * * @see {@link all} – combine multiple configs into a struct @@ -1421,5 +1631,5 @@ export const nested: { } = dual( 2, (self: Config, name: string): Config => - make((provider, pathPrefix) => self.parse(provider, [...pathPrefix, name])) + make((provider, pathPrefix) => evaluateAt(self, provider, [...pathPrefix, name])) ) diff --git a/.context/effect/packages/effect/src/ConfigProvider.ts b/.context/effect/packages/effect/src/ConfigProvider.ts index 3f1282543..a847dc743 100644 --- a/.context/effect/packages/effect/src/ConfigProvider.ts +++ b/.context/effect/packages/effect/src/ConfigProvider.ts @@ -41,6 +41,10 @@ import * as Str from "./String.ts" * `value`. `Array` is an indexed container with a known `length` and may also * carry an optional co-located `value`. * + * Provider lookups return `undefined` when no node exists at the requested + * path. Within a node that was found, `value: undefined` has a narrower + * structural meaning: the container exists but has no co-located scalar value. + * * @see {@link makeValue} – construct a `Value` node * @see {@link makeRecord} – construct a `Record` node * @see {@link makeArray} – construct an `Array` node @@ -81,11 +85,10 @@ export type Node = * * **Example** (Creating a value node) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider } from "effect" * - * const node = ConfigProvider.makeValue("3000") - * // { _tag: "Value", value: "3000" } + * ConfigProvider.makeValue("3000") // => { _tag: "Value", value: "3000" } * ``` * * @see {@link makeRecord} – for object-like containers @@ -115,11 +118,15 @@ export function makeValue(value: string): Node { * * **Example** (Creating a record node) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider } from "effect" * * const node = ConfigProvider.makeRecord(new Set(["host", "port"])) - * // { _tag: "Record", keys: Set(["host", "port"]), value: undefined } + * node._tag // => "Record" + * if (node._tag === "Record") { + * node.keys // => new Set(["host", "port"]) + * node.value // => undefined + * } * ``` * * @see {@link makeValue} – for terminal leaves @@ -148,11 +155,10 @@ export function makeRecord(keys: ReadonlySet, value?: string): Node { * * **Example** (Creating an array node) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider } from "effect" * - * const node = ConfigProvider.makeArray(3) - * // { _tag: "Array", length: 3, value: undefined } + * ConfigProvider.makeArray(3) // => { _tag: "Array", length: 3, value: undefined } * ``` * * @see {@link makeValue} – for terminal leaves @@ -180,7 +186,7 @@ export function makeArray(length: number, value?: string): Node { * * **Example** (Failing with a SourceError) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.make((_path) => @@ -188,12 +194,14 @@ export function makeArray(length: number, value?: string): Node { * new ConfigProvider.SourceError({ message: "connection refused" }) * ) * ) + * + * Effect.runSync(Effect.flip(provider.load(["host"]))).message // => "connection refused" * ``` * * @see {@link ConfigProvider} – the interface whose `load` may fail with this * error * - * @category models + * @category errors * @since 4.0.0 */ export class SourceError extends Data.TaggedError("SourceError")<{ @@ -213,10 +221,11 @@ export class SourceError extends Data.TaggedError("SourceError")<{ * * **Example** (A typical config path) * - * ```ts + * ```ts import.meta.vitest * import type { ConfigProvider } from "effect" * * const path: ConfigProvider.Path = ["database", "replicas", 0, "host"] + * path.join(".") // => "database.replicas.0.host" * ``` * * @category models @@ -236,29 +245,63 @@ export type Path = ReadonlyArray * * `load(path)` is the semantic lookup operation used by the `Config` module. * It applies provider transformations and composition before consulting the - * underlying source. `undefined` means "not found" and `SourceError` means the - * source itself failed. + * underlying source. `undefined` means "not found", a `Node` means the path + * exists, and `SourceError` means the source itself failed. + * + * `mapInput(f)` is the provider's path-transformation capability. Keeping this + * capability on the provider allows source and composite providers to preserve + * their own lookup behavior without exposing an internal representation. + * Transformations compose in application order: `f` receives the path produced + * by earlier transformations. + * + * `load` deliberately accepts only a `Path`. Path transformation is modeled by + * returning another provider through `mapInput`, rather than by adding a + * transformation callback to every lookup. Custom implementations therefore + * expose lookup and transformation behavior, but no source or composition + * state. * * @see {@link make} – construct a provider from a lookup function * @see {@link orElse} – compose providers with fallback * - * @category models - * @since 2.0.0 + * @category services + * @since 4.0.0 */ export interface ConfigProvider extends Pipeable { /** - * Returns the node found at `path`, or `undefined` if it does not exist. - * Fails with `SourceError` when the underlying source cannot be read. + * Returns a `Node` when `path` exists or `undefined` when it does not. Fails + * with `SourceError` when the underlying source cannot be read. * * **When to use** * * Use to resolve a path through this provider's path transformations before * reading the backing source. + * + * **Details** + * + * Lookup absence controls provider composition, such as whether + * {@link orElse} consults its fallback. An optional `value` inside a found + * `Record` or `Array` node remains `undefined` because it describes the shape + * of that node rather than the outcome of the lookup. */ readonly load: (path: Path) => Effect.Effect - /** @internal */ - readonly state: ProviderState + /** + * Returns a provider that applies `f` to lookup paths after any existing path + * transformations. + * + * **When to use** + * + * Use to implement provider-specific path transformation behavior. Most + * callers should use the pipeable {@link mapInput} combinator. + * + * **Details** + * + * This capability is part of the provider interface so composite providers + * can distribute transformations to their operands while preserving each + * operand's behavior. Providers created with {@link make} implement it + * automatically. + */ + readonly mapInput: (f: (path: Path) => Path) => ConfigProvider } /** @@ -274,7 +317,7 @@ export interface ConfigProvider extends Pipeable { * * **Example** (Providing a custom provider) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromUnknown({ port: 8080 }) @@ -285,13 +328,15 @@ export interface ConfigProvider extends Pipeable { * }).pipe( * Effect.provideService(ConfigProvider.ConfigProvider, provider) * ) + * + * Effect.runSync(program) === provider // => true * ``` * * @see {@link layer} – install a provider as a Layer * @see {@link layerAdd} – add a fallback provider as a Layer * * @category services - * @since 2.0.0 + * @since 4.0.0 */ export const ConfigProvider: Context.Reference = Context.Reference( "effect/ConfigProvider", @@ -307,29 +352,15 @@ const Proto = { } } -type SourceState = { - readonly _tag: "Source" - readonly get: (path: Path) => Effect.Effect - readonly transform: (path: Path) => Path -} - -type OrElseState = { - readonly _tag: "OrElse" - readonly first: ConfigProvider - readonly second: ConfigProvider -} - -type ProviderState = SourceState | OrElseState - const identityPath = (path: Path): Path => path function makeProvider( - state: ProviderState, - load: (path: Path) => Effect.Effect + load: (path: Path) => Effect.Effect, + mapInput: (f: (path: Path) => Path) => ConfigProvider ): ConfigProvider { const self = Object.create(Proto) - self.state = state self.load = load + self.mapInput = mapInput return self } @@ -337,25 +368,21 @@ function makeSource( get: (path: Path) => Effect.Effect, transform: (path: Path) => Path ): ConfigProvider { - const state: SourceState = { - _tag: "Source", - get, - transform - } - return makeProvider(state, (path) => state.get(state.transform(path))) + return makeProvider( + (path) => get(transform(path)), + (f) => makeSource(get, flow(transform, f)) + ) } function makeOrElse(first: ConfigProvider, second: ConfigProvider): ConfigProvider { - const state: OrElseState = { - _tag: "OrElse", - first, - second - } - return makeProvider(state, (path) => - Effect.flatMap( - state.first.load(path), - (node) => node ? Effect.succeed(node) : state.second.load(path) - )) + return makeProvider( + (path) => + Effect.flatMap( + first.load(path), + (node) => node !== undefined ? Effect.succeed(node) : second.load(path) + ), + (f) => makeOrElse(first.mapInput(f), second.mapInput(f)) + ) } /** @@ -369,12 +396,17 @@ function makeOrElse(first: ConfigProvider, second: ConfigProvider): ConfigProvid * **Details** * * The `get` callback receives a `Path` and must return - * `Effect`. Return `undefined` when the path - * does not exist; fail with `SourceError` only for actual I/O errors. + * `Effect`. Return `undefined` when the path does + * not exist, a `Node` when it does, and fail with `SourceError` only when the + * source cannot be read. + * + * Providers created by `make` also implement the path-transformation + * capability used by {@link mapInput}, {@link constantCase}, and + * {@link nested}. * * **Example** (Creating a simple in-memory provider) * - * ```ts + * ```ts import.meta.vitest * import { ConfigProvider, Effect } from "effect" * * const data: Record = { @@ -389,13 +421,15 @@ function makeOrElse(first: ConfigProvider, second: ConfigProvider): ConfigProvid * value !== undefined ? ConfigProvider.makeValue(value) : undefined * ) * }) + * + * Effect.runSync(provider.load(["host"])) // => ConfigProvider.makeValue("localhost") * ``` * * @see {@link fromEnv} – pre-built provider for environment variables * @see {@link fromUnknown} – pre-built provider for JSON objects * * @category constructors - * @since 2.0.0 + * @since 4.0.0 */ export function make(get: (path: Path) => Effect.Effect): ConfigProvider { return makeSource(get, identityPath) @@ -423,8 +457,8 @@ export function make(get: (path: Path) => Effect.Effect Effect.Effect ["prod.example.com", "3000"] * ``` * * @see {@link layerAdd} – install a fallback provider via a Layer @@ -463,10 +501,14 @@ export const orElse: { * `f` runs. For providers composed with {@link orElse}, the transformation is * applied to each operand. * + * The combinator delegates transformation to the provider itself. Use + * {@link make} for custom sources so this capability is implemented + * automatically. + * * **Example** (Uppercasing path segments) * - * ```ts - * import { ConfigProvider } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromEnv({ * env: { APP_HOST: "localhost" } @@ -477,6 +519,9 @@ export const orElse: { * typeof seg === "string" ? seg.toUpperCase() : seg * ) * ) + * + * const node = Effect.runSync(upper.load(["app_host"])) + * node?.value // => "localhost" * ``` * * @see {@link constantCase} – a preset that converts to `CONSTANT_CASE` @@ -490,15 +535,7 @@ export const mapInput: { (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider } = dual( 2, - (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider => { - const state = self.state - switch (state._tag) { - case "Source": - return makeSource(state.get, flow(state.transform, f)) - case "OrElse": - return makeOrElse(mapInput(state.first, f), mapInput(state.second, f)) - } - } + (self: ConfigProvider, f: (path: Path) => Path): ConfigProvider => self.mapInput(f) ) /** @@ -517,14 +554,16 @@ export const mapInput: { * * **Example** (Resolving camelCase keys to env vars) * - * ```ts - * import { ConfigProvider } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromEnv({ * env: { DATABASE_HOST: "localhost" } * }).pipe(ConfigProvider.constantCase) * * // path ["databaseHost"] now resolves to env var DATABASE_HOST + * const node = Effect.runSync(provider.load(["databaseHost"])) + * node?.value // => "localhost" * ``` * * @see {@link mapInput} – for arbitrary path transformations @@ -560,8 +599,8 @@ export const constantCase: (self: ConfigProvider) => ConfigProvider = mapInput(( * * **Example** (Nesting under a prefix) * - * ```ts - * import { ConfigProvider } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromEnv({ * env: { APP_HOST: "localhost", APP_PORT: "3000" } @@ -569,6 +608,8 @@ export const constantCase: (self: ConfigProvider) => ConfigProvider = mapInput(( * * // Lookups for ["HOST"] now resolve to ["APP", "HOST"] * const scoped = ConfigProvider.nested(provider, "APP") + * const node = Effect.runSync(scoped.load(["HOST"])) + * node?.value // => "localhost" * ``` * * @see {@link mapInput} – for arbitrary path transformations @@ -583,13 +624,7 @@ export const nested: { 2, (self: ConfigProvider, prefix: string | Path): ConfigProvider => { const path = typeof prefix === "string" ? [prefix] : prefix - const state = self.state - switch (state._tag) { - case "Source": - return makeSource(state.get, flow(state.transform, (input) => [...path, ...input])) - case "OrElse": - return makeOrElse(nested(state.first, path), nested(state.second, path)) - } + return mapInput(self, (input) => [...path, ...input]) } ) @@ -608,7 +643,7 @@ export const nested: { * * **Example** (Reading config from a JSON object) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect, Layer } from "effect" * * const TestLayer = ConfigProvider.layer( @@ -620,7 +655,7 @@ export const nested: { * return port * }) * - * // Effect.runSync(Effect.provide(program, TestLayer)) // 8080 + * Effect.runSync(Effect.provide(program, TestLayer)) // => 8080 * ``` * * @see {@link layerAdd} – add a provider without replacing the existing one @@ -646,13 +681,13 @@ export const layer = ( * **Details** * * By default, the new provider acts as a fallback and is consulted only when - * the current provider returns `undefined`. Set `asPrimary: true` to make the - * new provider the primary source, with the existing one as fallback. + * the current provider returns `undefined`. Set `asPrimary: true` to make + * the new provider the primary source, with the existing one as fallback. * * **Example** (Adding default values) * - * ```ts - * import { ConfigProvider } from "effect" + * ```ts import.meta.vitest + * import { Config, ConfigProvider, Effect, Layer } from "effect" * * const defaults = ConfigProvider.fromUnknown({ * HOST: "localhost", @@ -661,6 +696,11 @@ export const layer = ( * * // The current env provider is tried first; `defaults` is the fallback * const DefaultsLayer = ConfigProvider.layerAdd(defaults) + * const BaseLayer = ConfigProvider.layer(ConfigProvider.fromUnknown({})) + * const program = Config.string("HOST") + * + * const layer = Layer.provide(DefaultsLayer, BaseLayer) + * Effect.runSync(Effect.provide(program, layer)) // => "localhost" * ``` * * @see {@link layer} – replace the provider entirely @@ -695,8 +735,8 @@ export const layerAdd = ( * **Details** * * Path traversal follows standard JS rules: string segments index into object - * keys, numeric segments index into arrays. Returns `undefined` for any path - * that cannot be resolved. Never fails with `SourceError`. + * keys, numeric segments index into arrays. Returns `undefined` for any + * path that cannot be resolved. Never fails with `SourceError`. * * Primitive values (`number`, `boolean`, `bigint`) are stringified via * `String(...)`. @@ -713,7 +753,7 @@ export const layerAdd = ( * * **Example** (Providing config from a plain object) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromUnknown({ @@ -727,13 +767,13 @@ export const layerAdd = ( * provider.pipe(ConfigProvider.nested("database")) * ) * - * // Effect.runSync(host) // "localhost" + * Effect.runSync(host) // => "localhost" * ``` * * @see {@link fromEnv} – for environment variables * @see {@link make} – for custom backing stores * - * @category ConfigProviders + * @category constructors * @since 4.0.0 */ export function fromUnknown(root: unknown, options?: { @@ -792,14 +832,50 @@ function emptyStringAsMissing(value: string | undefined, preserveEmptyStrings: b return value === "" && !preserveEmptyStrings ? undefined : value } +/** + * Creates a `ConfigProvider` backed by an explicit environment record. + * + * **When to use** + * + * Use when a restricted runtime cannot evaluate the automatic environment + * detection performed by {@link fromEnv}, or whenever the environment record + * must be supplied explicitly. + * + * **Details** + * + * `undefined` values are ignored. Path lookup and child discovery otherwise + * use the same environment-variable semantics as {@link fromEnv}. + * + * Environment variable names are captured at construction time to establish + * record keys and array lengths. The supplied record remains live for value + * lookups, so updates to known paths are observed by later loads. Keys added + * after construction can be loaded directly, but do not appear in captured + * parent record keys or array lengths. + * + * Literal empty strings are treated as missing values by default. Pass + * `{ preserveEmptyStrings: true }` to keep empty strings as explicit values. + * + * @see {@link fromEnv} – automatically reads the runtime environment + * + * @category constructors + * @since 4.0.0 + */ +export function fromEnvRecord( + env: Record, + options?: { readonly preserveEmptyStrings?: boolean | undefined } +): ConfigProvider { + const preserveEmptyStrings = options?.preserveEmptyStrings === true + const trie = buildEnvTrie(env) + return make((path) => Effect.succeed(nodeAtEnv(trie, env, path, preserveEmptyStrings))) +} + /** * Creates a `ConfigProvider` backed by environment variables. * * **When to use** * * Use to read configuration from `process.env`, which is the default when no - * provider is explicitly set, or pass a custom env record for testing or - * non-Node runtimes. + * provider is explicitly set, or pass a custom env record for testing. * * **Details** * @@ -822,7 +898,7 @@ function emptyStringAsMissing(value: string | undefined, preserveEmptyStrings: b * * **Example** (Reading from a custom env record) * - * ```ts + * ```ts import.meta.vitest * import { Config, ConfigProvider, Effect } from "effect" * * const provider = ConfigProvider.fromEnv({ @@ -836,13 +912,14 @@ function emptyStringAsMissing(value: string | undefined, preserveEmptyStrings: b * provider.pipe(ConfigProvider.nested("DATABASE")) * ) * - * // Effect.runSync(host) // "localhost" + * Effect.runSync(host) // => "localhost" * ``` * * @see {@link fromUnknown} – for JSON objects + * @see {@link fromEnvRecord} – for explicit records in restricted runtimes * @see {@link constantCase} – bridge camelCase keys to SCREAMING_SNAKE_CASE * - * @category ConfigProviders + * @category constructors * @since 2.0.0 */ export function fromEnv(options?: { @@ -850,13 +927,12 @@ export function fromEnv(options?: { readonly preserveEmptyStrings?: boolean | undefined }): ConfigProvider { const env: Record = options?.env ?? { - ...globalThis?.process?.env, + ...(globalThis as { + readonly process?: { readonly env?: Record } + }).process?.env, ...(import.meta as any)?.env } - const preserveEmptyStrings = options?.preserveEmptyStrings === true - const trie = buildEnvTrie(env) - - return make((path) => Effect.succeed(nodeAtEnv(trie, env, path, preserveEmptyStrings))) + return fromEnvRecord(env, { preserveEmptyStrings: options?.preserveEmptyStrings }) } type EnvTrieNode = { @@ -874,8 +950,8 @@ function buildEnvTrie(env: Record): EnvTrieNode { let node = trie for (const seg of segments) { - node.children ??= {} - node = node.children[seg] ??= {} + const children = node.children ??= Object.create(null) + node = children[seg] ??= {} } } @@ -891,7 +967,7 @@ function nodeAtEnv( preserveEmptyStrings: boolean ): Node | undefined { const key = path.map(String).join("_") - const leafValue = emptyStringAsMissing(env[key], preserveEmptyStrings) + const leafValue = emptyStringAsMissing(Object.hasOwn(env, key) ? env[key] : undefined, preserveEmptyStrings) const trieNode = trieNodeAt(trie, path) const children = trieNode?.children ? Object.keys(trieNode.children) : [] @@ -942,12 +1018,12 @@ function trieNodeAt(root: EnvTrieNode, path: Path): EnvTrieNode | undefined { * * Parsing is based on the `dotenv` / `dotenv-expand` algorithm. * - * Internally delegates to {@link fromEnv} with the parsed key-value pairs. + * Internally delegates to {@link fromEnvRecord} with the parsed key-value pairs. * * **Example** (Parsing .env contents) * - * ```ts - * import { ConfigProvider } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect } from "effect" * * const contents = ` * HOST=localhost @@ -956,12 +1032,15 @@ function trieNodeAt(root: EnvTrieNode, path: Path): EnvTrieNode | undefined { * ` * * const provider = ConfigProvider.fromDotEnvContents(contents) + * const port = Effect.runSync(provider.load(["PORT"])) + * port?.value // => "3000" * ``` * * @see {@link fromDotEnv} – loads a `.env` file from disk + * @see {@link fromEnvRecord} – for explicit environment records * @see {@link fromEnv} – for raw environment variable access * - * @category ConfigProviders + * @category constructors * @since 4.0.0 */ export function fromDotEnvContents(lines: string, options?: { @@ -972,14 +1051,14 @@ export function fromDotEnvContents(lines: string, options?: { if (options?.expandVariables) { env = dotEnvExpand(env) } - return fromEnv({ env, preserveEmptyStrings: options?.preserveEmptyStrings }) + return fromEnvRecord(env, { preserveEmptyStrings: options?.preserveEmptyStrings }) } const DOT_ENV_LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg function parseDotEnvContents(lines: string): Record { - const obj: Record = {} + const obj: Record = Object.create(null) // Convert line breaks to same format lines = lines.replace(/\r\n?/gm, "\n") @@ -1014,9 +1093,9 @@ function parseDotEnvContents(lines: string): Record { } function dotEnvExpand(parsed: Record): Record { - const newParsed: Record = {} + const newParsed: Record = Object.create(null) - for (const configKey in parsed) { + for (const configKey of Object.keys(parsed)) { // resolve escape sequences newParsed[configKey] = interpolate(parsed[configKey], parsed).replace(/\\\$/g, "$") } @@ -1052,9 +1131,12 @@ function interpolate(envValue: string, parsed: Record): string { if (match !== null) { const [_, group, variableName, defaultValue] = match + const value = Object.hasOwn(parsed, variableName) && parsed[variableName] !== "" + ? parsed[variableName] + : defaultValue ?? "" return interpolate( - envValue.replace(group, defaultValue || parsed[variableName] || ""), + envValue.replace(group, value), parsed ) } @@ -1092,19 +1174,28 @@ function searchLast(str: string, rgx: RegExp): number { * * **Example** (Loading a .env file) * - * ```ts - * import { ConfigProvider, Effect } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect, FileSystem } from "effect" + * + * const fileSystem = FileSystem.makeNoop({ + * readFileString: () => Effect.succeed("HOST=localhost") + * }) * * const program = Effect.gen(function*() { * const provider = yield* ConfigProvider.fromDotEnv() - * return provider + * return yield* provider.load(["HOST"]) * }) + * + * const node = await Effect.runPromise( + * Effect.provideService(program, FileSystem.FileSystem, fileSystem) + * ) + * node?.value // => "localhost" * ``` * * @see {@link fromDotEnvContents} – parse a `.env` string directly * @see {@link fromEnv} – read from the runtime environment * - * @category ConfigProviders + * @category constructors * @since 4.0.0 */ export const fromDotEnv: (options?: { @@ -1146,21 +1237,36 @@ export const fromDotEnv: (options?: { * * **Example** (Reading config from a directory) * - * ```ts - * import { ConfigProvider, Effect } from "effect" + * ```ts import.meta.vitest + * import { ConfigProvider, Effect, FileSystem, Path } from "effect" + * + * const fileSystem = FileSystem.makeNoop({ + * readFileString: (path) => + * path === "/etc/myapp/host" + * ? Effect.succeed("localhost") + * : Effect.die("unexpected path") + * }) * * const program = Effect.gen(function*() { * const provider = yield* ConfigProvider.fromDir({ * rootPath: "/etc/myapp" * }) - * return provider + * return yield* provider.load(["host"]) * }) + * + * const node = await Effect.runPromise( + * program.pipe( + * Effect.provide(Path.layer), + * Effect.provideService(FileSystem.FileSystem, fileSystem) + * ) + * ) + * node?.value // => "localhost" * ``` * * @see {@link fromEnv} – for environment variables * @see {@link fromDotEnv} – for `.env` files * - * @category ConfigProviders + * @category constructors * @since 4.0.0 */ export const fromDir: (options?: { diff --git a/.context/effect/packages/effect/src/Console.ts b/.context/effect/packages/effect/src/Console.ts index 72f8bcf7f..83264a74b 100644 --- a/.context/effect/packages/effect/src/Console.ts +++ b/.context/effect/packages/effect/src/Console.ts @@ -19,7 +19,7 @@ import type { Scope } from "./Scope.ts" /** * Represents a console interface for logging, debugging, timing, and grouping output. * - * @category models + * @category services * @since 2.0.0 */ export interface Console { @@ -58,19 +58,26 @@ export interface Console { * * **Example** (Accessing the current console) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * log: (...args: ReadonlyArray) => messages.push(...args) + * }) * const program = Console.consoleWith((console) => * Effect.sync(() => { * console.log("Hello from current console!") * }) * ) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => ["Hello from current console!"] * ``` * * @see {@link consoleWith} for using the current console service inside an effect * - * @category references + * @category services * @since 2.0.0 */ export const Console: Context.Reference = effect.ConsoleRef @@ -80,15 +87,23 @@ export const Console: Context.Reference = effect.ConsoleRef * * **Example** (Accessing the current console service) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * log: (...args: ReadonlyArray) => messages.push(...args), + * error: (...args: ReadonlyArray) => messages.push(...args) + * }) * const program = Console.consoleWith((console) => * Effect.sync(() => { * console.log("Hello, world!") * console.error("This is an error message") * }) * ) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => ["Hello, world!", "This is an error message"] * ``` * * @category constructors @@ -102,13 +117,22 @@ export const consoleWith = (f: (console: Console) => Effect.Effect = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * assert: (condition: boolean, ...args: ReadonlyArray) => { + * if (!condition) errors.push(...args) + * } + * }) * const program = Effect.gen(function*() { * yield* Console.assert(2 + 2 === 4, "Math is working correctly") * yield* Console.assert(2 + 2 === 5, "This will be logged as an error") * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * errors // => ["This will be logged as an error"] * ``` * * @category accessors @@ -136,14 +160,22 @@ export const assert = (condition: boolean, ...args: ReadonlyArray): Effect. * * **Example** (Clearing console output) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const operations: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * log: (message: string) => operations.push(`log:${message}`), + * clear: () => operations.push("clear") + * }) * const program = Effect.gen(function*() { * yield* Console.log("This will be cleared") * yield* Console.clear * yield* Console.log("This appears after clearing") * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * operations // => ["log:This will be cleared", "clear", "log:This appears after clearing"] * ``` * * @category accessors @@ -160,14 +192,26 @@ export const clear: Effect.Effect = consoleWith((console) => * * **Example** (Counting repeated calls) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const counters = new Map() + * const messages: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * count: (label = "default") => { + * const count = (counters.get(label) ?? 0) + 1 + * counters.set(label, count) + * messages.push(`${label}: ${count}`) + * } + * }) * const program = Effect.gen(function*() { * yield* Console.count("my-counter") - * yield* Console.count("my-counter") // Will show: my-counter: 2 - * yield* Console.count() // Default counter + * yield* Console.count("my-counter") + * yield* Console.count() * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => ["my-counter: 1", "my-counter: 2", "default: 1"] * ``` * * @category accessors @@ -185,15 +229,28 @@ export const count = (label?: string): Effect.Effect => * * **Example** (Resetting a counter) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const counters = new Map() + * const messages: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * count: (label = "default") => { + * const count = (counters.get(label) ?? 0) + 1 + * counters.set(label, count) + * messages.push(`${label}: ${count}`) + * }, + * countReset: (label = "default") => counters.set(label, 0) + * }) * const program = Effect.gen(function*() { * yield* Console.count("my-counter") - * yield* Console.count("my-counter") // Will show: my-counter: 2 + * yield* Console.count("my-counter") * yield* Console.countReset("my-counter") - * yield* Console.count("my-counter") // Will show: my-counter: 1 + * yield* Console.count("my-counter") * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => ["my-counter: 1", "my-counter: 2", "my-counter: 1"] * ``` * * @category accessors @@ -217,13 +274,20 @@ export const countReset = (label?: string): Effect.Effect => * * **Example** (Writing debug messages) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * debug: (...args: ReadonlyArray) => messages.push(args) + * }) * const program = Effect.gen(function*() { * yield* Console.debug("Debug info:", { userId: 123, action: "login" }) * yield* Console.debug("Processing step", 1, "of", 5) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => [["Debug info:", { userId: 123, action: "login" }], ["Processing step", 1, "of", 5]] * ``` * * @category accessors @@ -241,14 +305,25 @@ export const debug = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Inspecting an object) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const inspected: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * dir: (item: unknown, options?: unknown) => inspected.push([item, options]) + * }) * const program = Effect.gen(function*() { * const obj = { name: "John", age: 30, nested: { city: "New York" } } * yield* Console.dir(obj) - * yield* Console.dir(obj, { depth: 2, colors: true }) + * yield* Console.dir(obj, { depth: 2 }) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * [{ name: "John", age: 30, nested: { city: "New York" } }, undefined], + * [{ name: "John", age: 30, nested: { city: "New York" } }, { depth: 2 }] + * ] + * inspected // => expected * ``` * * @category accessors @@ -266,15 +341,19 @@ export const dir = (item: any, options?: any): Effect.Effect => * * **Example** (Inspecting XML-like data) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * dirxml: (...args: ReadonlyArray) => messages.push(...args) + * }) * const program = Effect.gen(function*() { * yield* Console.dirxml("Ada") * }) * - * Effect.runSync(program) - * // Ada + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * messages // => ["Ada"] * ``` * * @category accessors @@ -293,9 +372,13 @@ export const dirxml = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Writing error messages) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * error: (...args: ReadonlyArray) => messages.push(args) + * }) * const program = Effect.gen(function*() { * yield* Console.error("Something went wrong!") * yield* Console.error("Error details:", { @@ -303,6 +386,13 @@ export const dirxml = (...args: ReadonlyArray): Effect.Effect => * message: "Internal Server Error" * }) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * ["Something went wrong!"], + * ["Error details:", { code: 500, message: "Internal Server Error" }] + * ] + * messages // => expected * ``` * * @category accessors @@ -320,9 +410,15 @@ export const error = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Grouping scoped output) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const operations: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * group: (label?: string) => operations.push(`group:${label}`), + * groupEnd: () => operations.push("groupEnd"), + * log: (message: string) => operations.push(`log:${message}`) + * }) * const program = Effect.gen(function*() { * yield* Effect.scoped( * Effect.gen(function*() { @@ -333,6 +429,16 @@ export const error = (...args: ReadonlyArray): Effect.Effect => * }) * ) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * "group:User Processing", + * "log:Loading user data...", + * "log:Validating user...", + * "log:User processed successfully", + * "groupEnd" + * ] + * operations // => expected * ``` * * @category accessors @@ -363,9 +469,13 @@ export const group = ( * * **Example** (Writing informational messages) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * info: (...args: ReadonlyArray) => messages.push(args) + * }) * const program = Effect.gen(function*() { * yield* Console.info("Application started successfully") * yield* Console.info("Server configuration:", { @@ -373,6 +483,13 @@ export const group = ( * env: "development" * }) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * ["Application started successfully"], + * ["Server configuration:", { port: 3000, env: "development" }] + * ] + * messages // => expected * ``` * * @category accessors @@ -390,14 +507,26 @@ export const info = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Writing log messages) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * log: (...args: ReadonlyArray) => messages.push(args) + * }) * const program = Effect.gen(function*() { * yield* Console.log("Hello, world!") * yield* Console.log("User data:", { name: "John", age: 30 }) * yield* Console.log("Processing", 42, "items") * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * ["Hello, world!"], + * ["User data:", { name: "John", age: 30 }], + * ["Processing", 42, "items"] + * ] + * messages // => expected * ``` * * @category accessors @@ -415,9 +544,16 @@ export const log = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Displaying tabular data) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const calls: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * table: (data: ReadonlyArray, properties?: ReadonlyArray) => { + * calls.push({ rows: data.length, properties }) + * } + * }) + * * const program = Effect.gen(function*() { * const users = [ * { name: "John", age: 30, city: "New York" }, @@ -427,6 +563,9 @@ export const log = (...args: ReadonlyArray): Effect.Effect => * yield* Console.table(users) * yield* Console.table(users, ["name", "age"]) // Only show specific columns * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * calls // => [{ rows: 3, properties: undefined }, { rows: 3, properties: ["name", "age"] }] * ``` * * @category accessors @@ -444,19 +583,28 @@ export const table = (tabularData: any, properties?: ReadonlyArray): Eff * * **Example** (Timing scoped work) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const operations: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * time: (label?: string) => operations.push(`start:${label}`), + * timeEnd: (label?: string) => operations.push(`end:${label}`), + * log: (message: string) => operations.push(`log:${message}`) + * }) + * * const program = Effect.gen(function*() { * yield* Effect.scoped( * Effect.gen(function*() { * yield* Console.time("operation-timer") - * yield* Effect.sleep("1 second") * yield* Console.log("Operation completed") * // Timer ends automatically when scope closes * }) * ) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * operations // => ["start:operation-timer", "log:Operation completed", "end:operation-timer"] * ``` * * @category accessors @@ -480,20 +628,28 @@ export const time = (label?: string | undefined): Effect.Effect = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * time: (label?: string) => operations.push(["start", label]), + * timeLog: (label?: string, ...args: ReadonlyArray) => operations.push(["log", label, ...args]), + * timeEnd: (label?: string) => operations.push(["end", label]) + * }) + * * const program = Effect.gen(function*() { * yield* Effect.scoped( * Effect.gen(function*() { * yield* Console.time("long-operation") - * yield* Effect.sleep("500 millis") * yield* Console.timeLog("long-operation", "Halfway done") - * yield* Effect.sleep("500 millis") * // Timer ends when scope closes * }) * ) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * operations // => [["start", "long-operation"], ["log", "long-operation", "Halfway done"], ["end", "long-operation"]] * ``` * * @category accessors @@ -512,13 +668,21 @@ export const timeLog = (label?: string, ...args: ReadonlyArray): Effect.Eff * * **Example** (Writing stack traces) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const traces: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * trace: (...args: ReadonlyArray) => traces.push(args) + * }) + * * const program = Effect.gen(function*() { * yield* Console.trace("Debug trace point") * yield* Console.trace("Function call:", { functionName: "processData" }) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * traces // => [["Debug trace point"], ["Function call:", { functionName: "processData" }]] * ``` * * @category accessors @@ -537,15 +701,26 @@ export const trace = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Writing warning messages) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * warn: (...args: ReadonlyArray) => messages.push(args) + * }) * const program = Effect.gen(function*() { * yield* Console.warn("This feature is deprecated") * yield* Console.warn("Performance warning:", { * slowQuery: "SELECT * FROM large_table" * }) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * ["This feature is deprecated"], + * ["Performance warning:", { slowQuery: "SELECT * FROM large_table" }] + * ] + * messages // => expected * ``` * * @category accessors @@ -563,9 +738,15 @@ export const warn = (...args: ReadonlyArray): Effect.Effect => * * **Example** (Wrapping an effect in a group) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const operations: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * group: (label?: string) => operations.push(`group:${label}`), + * groupEnd: () => operations.push("groupEnd"), + * log: (message: string) => operations.push(`log:${message}`) + * }) * const program = Effect.gen(function*() { * yield* Console.withGroup( * Effect.gen(function*() { @@ -576,6 +757,16 @@ export const warn = (...args: ReadonlyArray): Effect.Effect => * { label: "Processing Steps", collapsed: false } * ) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * "group:Processing Steps", + * "log:Step 1: Initialize", + * "log:Step 2: Process", + * "log:Step 3: Complete", + * "groupEnd" + * ] + * operations // => expected * ``` * * @category accessors @@ -618,18 +809,27 @@ export const withGroup = dual< * * **Example** (Timing an effect) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * + * const operations: Array = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * time: (label?: string) => operations.push(`start:${label}`), + * timeEnd: (label?: string) => operations.push(`end:${label}`), + * log: (message: string) => operations.push(`log:${message}`) + * }) + * * const program = Effect.gen(function*() { * yield* Console.withTime( * Effect.gen(function*() { - * yield* Effect.sleep("1 second") * yield* Console.log("Operation completed") * }), * "my-operation" * ) * }) + * + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * operations // => ["start:my-operation", "log:Operation completed", "end:my-operation"] * ``` * * @category accessors diff --git a/.context/effect/packages/effect/src/Context.ts b/.context/effect/packages/effect/src/Context.ts index cfc8cd983..631e2d1d7 100644 --- a/.context/effect/packages/effect/src/Context.ts +++ b/.context/effect/packages/effect/src/Context.ts @@ -17,7 +17,6 @@ import { dual, type LazyArg } from "./Function.ts" import * as Hash from "./Hash.ts" import type { Inspectable } from "./Inspectable.ts" import { exitSucceed, PipeInspectableProto, withFiber } from "./internal/core.ts" -import { getStackTraceLimit, setStackTraceLimit } from "./internal/stackTraceLimit.ts" import * as Option from "./Option.ts" import type { Pipeable } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" @@ -67,7 +66,6 @@ export interface Key extends Effect extends Effect extends Effect `Result: ${sql}` }) + * Context.get(context, Database).query("SELECT 1") // => "Result: SELECT 1" * ``` * - * @category models + * @category services * @since 4.0.0 */ export interface Service extends Key { @@ -118,7 +117,7 @@ export interface Service extends Key @@ -139,7 +138,7 @@ export declare namespace ServiceClass { * Runtime and type-level metadata carried by a class-style service key, * including its service type identifier, string key, and service shape. * - * @category models + * @category services * @since 4.0.0 */ export interface Shape { @@ -172,7 +171,7 @@ export declare namespace ServiceClass { * * **Example** (Creating service keys) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * // Create a simple service @@ -190,15 +189,23 @@ export declare namespace ServiceClass { * query: (sql) => `Result: ${sql}` * }) * const config = Context.make(Config, { port: 8080 }) + * Context.get(db, Database).query("SELECT 1") // => "Result: SELECT 1" + * Context.get(config, Config).port // => 8080 * ``` * * @see {@link Reference} for service keys with default values * - * @category constructors + * @category services * @since 4.0.0 */ export const Service: { - (key: string): Service + ( + key: string, + options?: { + /** @internal */ + readonly fiberCached?: boolean | undefined + } | undefined + ): Service (): < const Identifier extends string, E, @@ -207,7 +214,9 @@ export const Service: { >( id: Identifier, options?: { - readonly make: ((...args: Args) => Effect) | Effect | undefined + readonly make?: ((...args: Args) => Effect) | Effect | undefined + /** @internal */ + readonly fiberCached?: boolean | undefined } | undefined ) => & ServiceClass @@ -220,6 +229,8 @@ export const Service: { id: Identifier, options: { readonly make: Make + /** @internal */ + readonly fiberCached?: boolean | undefined } ) => & ServiceClass< @@ -231,36 +242,28 @@ export const Service: { > & { readonly make: Make } } = function() { - const prevLimit = getStackTraceLimit() - setStackTraceLimit(2) - const err = new Error() - setStackTraceLimit(prevLimit) function KeyClass() {} const self = KeyClass as any as Types.Mutable> Object.setPrototypeOf(self, ServiceProto) - // @effect-diagnostics-next-line floatingEffect:off - Object.defineProperty(self, "stack", { - get() { - return err.stack - } - }) - if (arguments.length > 0) { - self.key = arguments[0] - if (arguments[1]?.defaultValue) { - self[ReferenceTypeId] = ReferenceTypeId - self.defaultValue = arguments[1].defaultValue - } - return self - } - return function(key: string, options?: { + const init = (key: string, options?: { + readonly defaultValue?: any readonly make?: any - }) { + readonly fiberCached?: boolean + }) => { self.key = key + if (options?.defaultValue) { + self[ReferenceTypeId] = ReferenceTypeId + self.defaultValue = options.defaultValue + } if (options?.make) { ;(self as any).make = options.make } + if (options?.fiberCached) { + cacheKeys.add(key) + } return self } + return arguments.length > 0 ? init(arguments[0], arguments[1]) : init } as any const ServiceProto: any = { @@ -274,8 +277,7 @@ const ServiceProto: any = { toJSON(this: Service) { return { _id: "Service", - key: this.key, - stack: this.stack + key: this.key } }, of(this: void, self: Service): Service { @@ -295,6 +297,8 @@ const ServiceProto: any = { } } +const cacheKeys = new Set() + const ReferenceTypeId = "~effect/Context/Reference" as const /** @@ -308,21 +312,24 @@ const ReferenceTypeId = "~effect/Context/Reference" as const * * **Example** (Defining a reference with a default value) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * // Define a reference with a default value + * const messages: Array = [] * const LoggerRef: Context.Reference<{ log: (msg: string) => void }> = * Context.Reference("Logger", { - * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * defaultValue: () => ({ log: (msg) => { messages.push(msg) } }) * }) * * // The reference can be used without explicit provision * const context = Context.empty() * const logger = Context.get(context, LoggerRef) // Uses default value + * logger.log("default logger") + * messages // => ["default logger"] * ``` * - * @category models + * @category services * @since 3.11.0 */ export interface Reference extends Service { @@ -337,7 +344,7 @@ export interface Reference extends Service { * * **Example** (Extracting service types) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * const Database = Context.Service<{ @@ -349,6 +356,8 @@ export interface Reference extends Service { * * // Extract identifier type from a key * type DatabaseId = Context.Service.Identifier + * + * Database.key // => "Database" * ``` * * @since 2.0.0 @@ -360,7 +369,7 @@ export declare namespace Service { * * **Example** (Typing any service key) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * // Any represents any possible service type @@ -368,9 +377,10 @@ export declare namespace Service { * Context.Service<{ log: (msg: string) => void }>("Logger"), * Context.Service<{ query: (sql: string) => string }>("Database") * ] + * services.map((service) => service.key) // => ["Logger", "Database"] * ``` * - * @category models + * @category utility types * @since 4.0.0 */ export type Any = Key | Key @@ -381,7 +391,7 @@ export declare namespace Service { * * **Example** (Extracting a service shape) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * const Database = Context.Service<{ query: (sql: string) => string }>( @@ -391,9 +401,10 @@ export declare namespace Service { * // Extract the service shape from the service * type DatabaseService = Context.Service.Shape * // DatabaseService is { query: (sql: string) => string } + * Database.key // => "Database" * ``` * - * @category models + * @category utility types * @since 4.0.0 */ export type Shape = T extends Key ? S : never @@ -404,7 +415,7 @@ export declare namespace Service { * * **Example** (Extracting a service identifier) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * const Database = Context.Service<{ query: (sql: string) => string }>( @@ -414,9 +425,10 @@ export declare namespace Service { * // Extract the identifier type from a key * type DatabaseId = Context.Service.Identifier * // DatabaseId is the identifier type + * Database.key // => "Database" * ``` * - * @category models + * @category utility types * @since 2.0.0 */ export type Identifier = T extends Key ? I : never @@ -435,7 +447,7 @@ const TypeId = "~effect/Context" as const * * **Example** (Creating a context with multiple services) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * // Create a context with multiple services @@ -444,10 +456,9 @@ const TypeId = "~effect/Context" as const * "Database" * ) * - * const context = Context.make(Logger, { - * log: (msg: string) => console.log(msg) - * }) + * const context = Context.make(Logger, { log: (_msg: string) => {} }) * .pipe(Context.add(Database, { query: (sql) => `Result: ${sql}` })) + * Context.get(context, Database).query("SELECT 1") // => "Result: SELECT 1" * ``` * * @category models @@ -458,7 +469,81 @@ export interface Context extends Equal.Equal, Pipeable, Inspectable readonly _Services: Types.Contravariant } readonly mapUnsafe: ReadonlyMap - mutable: boolean +} + +interface ContextImpl extends Context { + cacheRoot: ContextImpl | undefined + base: ReadonlyMap + baseHits: number + overlay: Overlay | undefined + depth: number + _flat: ReadonlyMap | undefined +} + +interface Overlay { + readonly key: string + readonly value: unknown + readonly parent: Overlay | undefined +} + +const MaxDepth = 8 +const FlattenAfterBaseHits = 8 + +const makeImpl = ( + cacheRoot: ContextImpl | undefined, + base: ReadonlyMap, + overlay: Overlay | undefined, + depth: number +): ContextImpl => { + const self: ContextImpl = Object.create(Proto) + self.cacheRoot = cacheRoot ?? self + self.base = base + self.overlay = overlay + self.depth = depth + self._flat = undefined + self.baseHits = 0 + return self +} + +const applyOverlays = (map: Map, overlay: Overlay | undefined): void => { + if (!overlay) return + applyOverlays(map, overlay.parent) + map.set(overlay.key, overlay.value) +} + +const flatten = (self: ContextImpl): ReadonlyMap => { + if (self._flat) return self._flat + if (!self.overlay) return self._flat = self.base + const map = new Map(self.base) + applyOverlays(map, self.overlay) + return self._flat = map +} + +const withFlat = (self: Context, f: (map: Map) => void): Context => { + const map = new Map(self.mapUnsafe) + f(map) + return makeUnsafe(map) +} + +// A private symbol so user code cannot forge a value that reads as absent +const notFound = Symbol() + +const lookup = (self: Context, key: string): unknown => { + const impl = self as ContextImpl + for (let overlay = impl.overlay; overlay; overlay = overlay.parent) { + if (overlay.key === key) return overlay.value + } + const value = impl.base.get(key) + // Misses must not advance the counter: reference-default lookups miss the + // base on every fiber cache refresh, which would flatten every short-lived + // request context and reintroduce the O(services) per-request cost + if (value === undefined && !impl.base.has(key)) return notFound + if (impl.overlay && ++impl.baseHits >= FlattenAfterBaseHits) { + impl.base = flatten(impl) + impl.overlay = undefined + impl.depth = 0 + } + return value } /** @@ -471,38 +556,41 @@ export interface Context extends Equal.Equal, Pipeable, Inspectable * * **Gotchas** * - * This is unsafe because later mutation of the provided map can affect the - * created `Context`. Prefer `empty`, `make`, `add`, or `merge` for normal - * Context construction. + * The provided map is retained without copying and must not be mutated after + * construction. Prefer `empty`, `make`, `add`, or `merge` for normal Context + * construction. * * **Example** (Creating a context from a map) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * // Create a context from a Map (unsafe) * const map = new Map([ - * ["Logger", { log: (msg: string) => console.log(msg) }] + * ["Logger", { log: (_msg: string) => {} }] * ]) * * const context = Context.makeUnsafe(map) + * context.mapUnsafe.size // => 1 * ``` * * @category constructors * @since 4.0.0 */ -export const makeUnsafe = (mapUnsafe: ReadonlyMap): Context => { - const self = Object.create(Proto) - self.mapUnsafe = mapUnsafe - self.mutable = false - return self -} +export const makeUnsafe = (mapUnsafe: ReadonlyMap): Context => + makeImpl(undefined, mapUnsafe, undefined, 0) -const Proto: Omit, "mapUnsafe" | "mutable"> = { +const Proto: Omit< + ContextImpl, + "cacheRoot" | "base" | "overlay" | "depth" | "_flat" | "baseHits" +> = { ...PipeInspectableProto, [TypeId]: { _Services: (_: never) => _ }, + get mapUnsafe() { + return flatten(this as any as ContextImpl) + }, toJSON(this: Context) { return { _id: "Context", @@ -510,17 +598,12 @@ const Proto: Omit, "mapUnsafe" | "mutable"> = { } }, [Equal.symbol](this: Context, that: unknown): boolean { - if ( - !isContext(that) - || this.mapUnsafe.size !== that.mapUnsafe.size - ) return false - for (const k of this.mapUnsafe.keys()) { - if ( - !that.mapUnsafe.has(k) || - !Equal.equals(this.mapUnsafe.get(k), that.mapUnsafe.get(k)) - ) { - return false - } + if (!isContext(that)) return false + const self = this.mapUnsafe + const other = that.mapUnsafe + if (self.size !== other.size) return false + for (const [key, value] of self) { + if (!other.has(key) || !Equal.equals(value, other.get(key))) return false } return true }, @@ -529,6 +612,12 @@ const Proto: Omit, "mapUnsafe" | "mutable"> = { } } +/** @internal */ +export const hasSameCache = ( + self: Context, + that: Context +): boolean => (self as ContextImpl).cacheRoot === (that as ContextImpl).cacheRoot + /** * Checks whether the provided argument is a `Context`. * @@ -549,11 +638,9 @@ const Proto: Omit, "mapUnsafe" | "mutable"> = { * * **Example** (Checking for contexts) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" - * - * assert.strictEqual(Context.isContext(Context.empty()), true) + * Context.isContext(Context.empty()) // => true * ``` * * @see {@link isKey} for checking service keys @@ -569,11 +656,9 @@ export const isContext = (u: unknown): u is Context => hasProperty(u, Typ * * **Example** (Checking for keys) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" - * - * assert.strictEqual(Context.isKey(Context.Service("Service")), true) + * Context.isKey(Context.Service("Service")) // => true * ``` * * @category guards @@ -586,33 +671,30 @@ export const isKey = (u: unknown): u is Key => hasProperty(u, ServiceT * * **Example** (Checking for references) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" * * const LoggerRef = Context.Reference("Logger", { - * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * defaultValue: () => ({ log: (_msg: string) => {} }) * }) * - * assert.strictEqual(Context.isReference(LoggerRef), true) - * assert.strictEqual(Context.isReference(Context.Service("Key")), false) + * Context.isReference(LoggerRef) // => true + * Context.isReference(Context.Service("Key")) // => false * ``` * * @category guards * @since 3.11.0 */ -export const isReference = (u: unknown): u is Reference => hasProperty(u, ReferenceTypeId) +export const isReference = (u: Key): u is Reference => !!(u as Reference)[ReferenceTypeId] /** * Returns an empty `Context`. * * **Example** (Creating an empty context) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" - * - * assert.strictEqual(Context.isContext(Context.empty()), true) + * Context.empty().mapUnsafe.size // => 0 * ``` * * @category constructors @@ -626,15 +708,14 @@ const emptyContext = makeUnsafe(new Map()) * * **Example** (Creating a context with one service) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * * const context = Context.make(Port, { PORT: 8080 }) * - * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) + * Context.get(context, Port).PORT // => 8080 * ``` * * @category constructors @@ -659,9 +740,8 @@ export const make = ( * * **Example** (Adding a service to a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, pipe } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -673,13 +753,13 @@ export const make = ( * Context.add(Timeout, { TIMEOUT: 5000 }) * ) * - * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) - * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * const values = [Context.get(context, Port).PORT, Context.get(context, Timeout).TIMEOUT] + * values // => [8080, 5000] * ``` * * @see {@link addOrOmit} for adding or removing a service from an `Option` * - * @category adders + * @category combining * @since 2.0.0 */ export const add: { @@ -696,10 +776,24 @@ export const add: { self: Context, key: Key, service: Types.NoInfer -): Context => - withMapUnsafe(self, (map) => { +): Context => { + const impl = self as ContextImpl + const cacheRoot = cacheKeys.has(key.key) ? undefined : impl.cacheRoot + if (impl.depth >= MaxDepth) { + // Rebase the overlay chain into a flat map, keeping the cacheRoot so a + // rebase on an ordinary key does not invalidate fiber caches + const map = new Map(impl.mapUnsafe) map.set(key.key, service) - })) + return makeImpl(cacheRoot, map, undefined, 0) + } + + return makeImpl( + cacheRoot, + impl.base, + { key: key.key, value: service, parent: impl.overlay }, + impl.depth + 1 + ) +}) /** * Adds or removes a service depending on an `Option`. @@ -715,7 +809,7 @@ export const add: { * * **Example** (Adding optional services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option } from "effect" * * const Port = Context.Service<{ PORT: number }>("Port") @@ -727,35 +821,33 @@ export const add: { * const withoutPort = withPort.pipe( * Context.addOrOmit(Port, Option.none()) * ) + * Context.getOption(withPort, Port) // => Option.some({ PORT: 8080 }) + * Context.getOption(withoutPort, Port) // => Option.none() * ``` * * @see {@link add} for always storing a service value * - * @category adders + * @category combining * @since 4.0.0 */ export const addOrOmit: { ( key: Key, service: Option.Option> - ): (self: Context) => Context + ): (self: Context) => Context> ( self: Context, key: Key, service: Option.Option> - ): Context + ): Context> } = dual(3, ( self: Context, key: Key, service: Option.Option> -): Context => - withMapUnsafe(self, (map) => { - if (service._tag === "None") { - map.delete(key.key) - } else { - map.set(key.key, service.value) - } - })) +): Context> => + service._tag === "None" + ? omit(key)(self) + : add(self, key, service.value) as any) /** * Gets the service for a key, or evaluates the fallback when a non-reference @@ -778,7 +870,7 @@ export const addOrOmit: { * * **Example** (Falling back for missing services) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * * const Logger = Context.Service<{ log: (msg: string) => void }>("Logger") @@ -786,9 +878,7 @@ export const addOrOmit: { * "Database" * ) * - * const context = Context.make(Logger, { - * log: (msg: string) => console.log(msg) - * }) + * const context = Context.make(Logger, { log: (_msg: string) => {} }) * * const logger = Context.getOrElse(context, Logger, () => ({ log: () => {} })) * const database = Context.getOrElse( @@ -797,8 +887,8 @@ export const addOrOmit: { * () => ({ query: () => "fallback" }) * ) * - * console.log(logger === Context.get(context, Logger)) // true - * console.log(database.query("SELECT 1")) // "fallback" + * logger === Context.get(context, Logger) // => true + * database.query("SELECT 1") // => "fallback" * ``` * * @see {@link getOption} for returning `Option.none` when a non-reference key is missing @@ -810,9 +900,8 @@ export const getOrElse: { (key: Key, orElse: LazyArg): (self: Context) => S | B (self: Context, key: Key, orElse: LazyArg): S | B } = dual(3, (self: Context, key: Key, orElse: LazyArg): S | B => { - if (self.mapUnsafe.has(key.key)) { - return self.mapUnsafe.get(key.key)! as any - } + const value = lookup(self, key.key) + if (value !== notFound) return value as any return isReference(key) ? getDefaultValue(key) : orElse() }) @@ -840,9 +929,15 @@ export const getOrUndefined: { (self: Context, key: Key): S | undefined } = dual( 2, - (self: Context, key: Key): S | undefined => self.mapUnsafe.get(key.key) + (self: Context, key: Key): S | undefined => getOrUndefinedUnsafe(self, key.key) ) +/** @internal */ +export const getOrUndefinedUnsafe = (self: Context, key: string): A | undefined => { + const value = lookup(self, key) + return value === notFound ? undefined : value as A +} + /** * Gets the service for a key, throwing if an absent non-reference key cannot be * resolved. @@ -860,17 +955,16 @@ export const getOrUndefined: { * * **Example** (Getting services unsafely) * - * ```ts - * import { Context } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Context, Option } from "effect" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") * * const context = Context.make(Port, { PORT: 8080 }) * - * assert.deepStrictEqual(Context.getUnsafe(context, Port), { PORT: 8080 }) - * assert.throws(() => Context.getUnsafe(context, Timeout)) + * Context.getUnsafe(context, Port).PORT // => 8080 + * Context.getOption(context, Timeout) // => Option.none() * ``` * * @see {@link get} for type-checked service access @@ -885,11 +979,12 @@ export const getUnsafe: { } = dual( 2, (self: Context, service: Key): S => { - if (!self.mapUnsafe.has(service.key)) { - if (ReferenceTypeId in service) return getDefaultValue(service as any) + const value = lookup(self, service.key) + if (value === notFound) { + if (isReference(service)) return getDefaultValue(service as any) throw serviceNotFoundError(service) } - return self.mapUnsafe.get(service.key)! as any + return value as any } ) @@ -903,9 +998,8 @@ export const getUnsafe: { * * **Example** (Getting a service from a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, pipe } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -915,7 +1009,7 @@ export const getUnsafe: { * Context.add(Timeout, { TIMEOUT: 5000 }) * ) * - * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * Context.get(context, Timeout).TIMEOUT // => 5000 * ``` * * @see {@link getOption} for optional service access @@ -929,54 +1023,6 @@ export const get: { (self: Context, service: Key): S } = getUnsafe -/** - * Gets the value for a `Context.Reference`, returning its cached default when - * the context does not contain an override. - * - * **When to use** - * - * Use when you need a `Context.Reference` value resolved from either a stored - * override or the reference's default value. - * - * **Details** - * - * Stored overrides take precedence. If no override is present, the reference's - * default value is computed lazily and cached on the reference itself. - * - * **Gotchas** - * - * Mutable default values can be shared across contexts unless an override is - * provided, because the default is cached on the `Context.Reference`. - * - * **Example** (Getting reference defaults unsafely) - * - * ```ts - * import { Context } from "effect" - * - * const LoggerRef = Context.Reference("Logger", { - * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) - * }) - * - * const context = Context.empty() - * const logger = Context.getReferenceUnsafe(context, LoggerRef) - * - * console.log(typeof logger.log) // "function" - * ``` - * - * @see {@link getUnsafe} for unsafe access with any service key - * @see {@link get} for type-checked reference-aware access - * @see {@link getOption} for optional access to non-reference keys - * - * @category unsafe - * @since 4.0.0 - */ -export const getReferenceUnsafe = (self: Context, service: Reference): S => { - if (!self.mapUnsafe.has(service.key)) { - return getDefaultValue(service as any) - } - return self.mapUnsafe.get(service.key)! as any -} - const defaultValueCacheKey = "~effect/Context/defaultValue" as const const getDefaultValue = (ref: Reference) => { @@ -990,15 +1036,6 @@ const serviceNotFoundError = (service: Key) => { const error = new Error( `Service not found${service.key ? `: ${String(service.key)}` : ""}` ) - if (service.stack) { - const lines = service.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) @@ -1023,20 +1060,16 @@ const serviceNotFoundError = (service: Key) => { * * **Example** (Getting optional services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") * * const context = Context.make(Port, { PORT: 8080 }) * - * assert.deepStrictEqual( - * Context.getOption(context, Port), - * Option.some({ PORT: 8080 }) - * ) - * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * Context.getOption(context, Port) // => Option.some({ PORT: 8080 }) + * Context.getOption(context, Timeout) // => Option.none() * ``` * * @see {@link getOrElse} for returning a fallback value directly @@ -1048,9 +1081,8 @@ export const getOption: { (service: Key): (self: Context) => Option.Option (self: Context, service: Key): Option.Option } = dual(2, (self: Context, service: Key): Option.Option => { - if (self.mapUnsafe.has(service.key)) { - return Option.some(self.mapUnsafe.get(service.key)! as any) - } + const value = lookup(self, service.key) + if (value !== notFound) return Option.some(value as any) return isReference(service) ? Option.some(getDefaultValue(service as any)) : Option.none() }) @@ -1068,9 +1100,8 @@ export const getOption: { * * **Example** (Merging two contexts) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -1080,8 +1111,8 @@ export const getOption: { * * const context = Context.merge(firstContext, secondContext) * - * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) - * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) + * const values = [Context.get(context, Port).PORT, Context.get(context, Timeout).TIMEOUT] + * values // => [8080, 5000] * ``` * * @see {@link mergeAll} for merging more than two contexts at once @@ -1095,9 +1126,7 @@ export const merge: { } = dual(2, (self: Context, that: Context): Context => { if (self.mapUnsafe.size === 0) return that as any if (that.mapUnsafe.size === 0) return self as any - return withMapUnsafe(self, (map) => { - that.mapUnsafe.forEach((value, key) => map.set(key, value)) - }) + return withFlat(self, (map) => that.mapUnsafe.forEach((value, key) => map.set(key, value))) }) /** @@ -1114,9 +1143,8 @@ export const merge: { * * **Example** (Merging multiple contexts) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -1132,9 +1160,7 @@ export const merge: { * thirdContext * ) * - * assert.deepStrictEqual(Context.get(context, Port), { PORT: 8080 }) - * assert.deepStrictEqual(Context.get(context, Timeout), { TIMEOUT: 5000 }) - * assert.deepStrictEqual(Context.get(context, Host), { HOST: "localhost" }) + * context.mapUnsafe.size // => 3 * ``` * * @see {@link merge} for merging two contexts @@ -1163,9 +1189,8 @@ export const mergeAll = >( * * **Example** (Picking services from a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option, pipe } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -1177,11 +1202,8 @@ export const mergeAll = >( * * const context = pipe(someContext, Context.pick(Port)) * - * assert.deepStrictEqual( - * Context.getOption(context, Port), - * Option.some({ PORT: 8080 }) - * ) - * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * Context.getOption(context, Port) // => Option.some({ PORT: 8080 }) + * Context.getOption(context, Timeout) // => Option.none() * ``` * * @see {@link omit} for removing selected services @@ -1192,14 +1214,13 @@ export const mergeAll = >( export const pick = >>( ...services: S ) => -(self: Context): Context> => - withMapUnsafe(self, (map) => { - const keySet = new Set(services.map((key) => key.key)) +(self: Context): Context> => { + const keep = new Set(services.map((key) => key.key)) + return withFlat(self, (map) => map.forEach((_, key) => { - if (keySet.has(key)) return - map.delete(key) - }) - }) + if (!keep.has(key)) map.delete(key) + })) +} /** * Returns a new `Context` with the specified service keys removed. @@ -1210,9 +1231,8 @@ export const pick = >>( * * **Example** (Omitting services from a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option, pipe } from "effect" - * import * as assert from "node:assert" * * const Port = Context.Service<{ PORT: number }>("Port") * const Timeout = Context.Service<{ TIMEOUT: number }>("Timeout") @@ -1224,11 +1244,8 @@ export const pick = >>( * * const context = pipe(someContext, Context.omit(Timeout)) * - * assert.deepStrictEqual( - * Context.getOption(context, Port), - * Option.some({ PORT: 8080 }) - * ) - * assert.deepStrictEqual(Context.getOption(context, Timeout), Option.none()) + * Context.getOption(context, Port) // => Option.some({ PORT: 8080 }) + * Context.getOption(context, Timeout) // => Option.none() * ``` * * @see {@link pick} for keeping selected services @@ -1240,56 +1257,12 @@ export const omit = >>( ...keys: S ) => (self: Context): Context>> => - withMapUnsafe(self, (map) => { + withFlat(self, (map) => { for (let i = 0; i < keys.length; i++) { map.delete(keys[i].key) } }) -/** - * Performs a series of mutations on a `Context`. Prevents unnecessary copying - * of the underlying map when multiple mutations are needed. - * - * **When to use** - * - * Use to apply several `Context` transformations in one callback while copying - * the underlying service map only once. - * - * @see {@link add} for adding or replacing a service - * @see {@link addOrOmit} for adding or removing a service from an `Option` - * @see {@link merge} for combining two contexts - * @see {@link pick} for keeping selected services - * @see {@link omit} for removing selected services - * - * @category mutations - * @since 4.0.0 - */ -export const mutate: { - ( - f: (context: Context) => Context - ): (self: Context) => Context - (self: Context, f: (context: Context) => Context): Context -} = dual( - 2, - (self: Context, f: (context: Context) => Context): Context => { - const next = makeUnsafe(new Map(self.mapUnsafe)) - next.mutable = true - const result = f(next) - result.mutable = false - return result - } -) - -const withMapUnsafe = (self: Context, f: (map: Map) => void): Context => { - if (self.mutable) { - f(self.mapUnsafe as any) - return self as any - } - const map = new Map(self.mapUnsafe) - f(map) - return makeUnsafe(map) -} - /** * Creates a context key with a default value. * @@ -1308,12 +1281,13 @@ const withMapUnsafe = (self: Context, f: (map: Map = [] * const LoggerRef = Context.Reference("Logger", { - * defaultValue: () => ({ log: (msg: string) => console.log(msg) }) + * defaultValue: () => ({ log: (msg: string) => messages.push(`Default: ${msg}`) }) * }) * * // The reference provides the default value when accessed from an empty context @@ -1322,17 +1296,24 @@ const withMapUnsafe = (self: Context, f: (map: Map `Custom: ${msg}` + * log: (msg: string) => messages.push(`Custom: ${msg}`) * }) * const customLogger = Context.get(customContext, LoggerRef) + * logger.log("default") + * customLogger.log("message") + * messages // => ["Default: default", "Custom: message"] * ``` * * @see {@link Service} for required services without default values * - * @category references + * @category services * @since 3.11.0 */ export const Reference: ( key: string, - options: { readonly defaultValue: () => Service } + options: { + readonly defaultValue: () => Service + /** @internal */ + readonly fiberCached?: boolean | undefined + } ) => Reference = Service as any diff --git a/.context/effect/packages/effect/src/Cron.ts b/.context/effect/packages/effect/src/Cron.ts index b2312dcc5..259c6a6f2 100644 --- a/.context/effect/packages/effect/src/Cron.ts +++ b/.context/effect/packages/effect/src/Cron.ts @@ -12,7 +12,7 @@ import * as Data from "./Data.ts" import type * as DateTime from "./DateTime.ts" import * as Equal from "./Equal.ts" import * as Equ from "./Equivalence.ts" -import { format } from "./Formatter.ts" +import { format as formatValue } from "./Formatter.ts" import { constVoid, dual, pipe } from "./Function.ts" import * as Hash from "./Hash.ts" import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" @@ -43,8 +43,8 @@ const TypeId = "~effect/time/Cron" * * **Example** (Creating a cron schedule) * - * ```ts - * import { Cron } from "effect" + * ```ts import.meta.vitest + * import { Cron, DateTime } from "effect" * * // Create a cron that runs at 9 AM on weekdays * const weekdayMorning = Cron.make({ @@ -52,12 +52,12 @@ const TypeId = "~effect/time/Cron" * hours: [9], * days: [], * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - * weekdays: [1, 2, 3, 4, 5] // Monday to Friday + * weekdays: [1, 2, 3, 4, 5], // Monday to Friday + * tz: DateTime.zoneMakeNamedUnsafe("UTC") * }) * * // Check if a date matches the schedule - * const matches = Cron.match(weekdayMorning, new Date("2023-06-05T09:00:00")) - * console.log(matches) // true if it's 9 AM on a weekday + * Cron.match(weekdayMorning, "2023-06-05T09:00:00Z") // => true * ``` * * @see {@link make} for creating a schedule from explicit field constraints @@ -152,7 +152,7 @@ const CronProto = { return toPojo(this) }, toString(this: Cron) { - return `Cron(${format(toPojo(this))})` + return `Cron(${formatValue(toPojo(this))})` }, toJSON(this: Cron) { const out = toPojo(this) @@ -182,7 +182,7 @@ const CronProto = { * * **Example** (Checking cron values) * - * ```ts + * ```ts import.meta.vitest * import { Cron } from "effect" * * const cron = Cron.make({ @@ -193,9 +193,9 @@ const CronProto = { * weekdays: [1, 2, 3, 4, 5] * }) * - * console.log(Cron.isCron(cron)) // true - * console.log(Cron.isCron({})) // false - * console.log(Cron.isCron("not a cron")) // false + * Cron.isCron(cron) // => true + * Cron.isCron({}) // => false + * Cron.isCron("not a cron") // => false * ``` * * @see {@link make} for constructing a `Cron` value directly @@ -225,8 +225,10 @@ export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId) * * **Example** (Creating schedules from constraints) * - * ```ts - * import { Cron } from "effect" + * ```ts import.meta.vitest + * import { Cron, DateTime } from "effect" + * + * const utc = DateTime.zoneMakeNamedUnsafe("UTC") * * // Every day at midnight * const midnight = Cron.make({ @@ -266,7 +268,8 @@ export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId) * 31 * ], * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - * weekdays: [0, 1, 2, 3, 4, 5, 6] + * weekdays: [0, 1, 2, 3, 4, 5, 6], + * tz: utc * }) * * // Every 15 minutes during business hours on weekdays @@ -275,8 +278,12 @@ export const isCron = (u: unknown): u is Cron => hasProperty(u, TypeId) * hours: [9, 10, 11, 12, 13, 14, 15, 16, 17], * days: [], * months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - * weekdays: [1, 2, 3, 4, 5] // Monday to Friday + * weekdays: [1, 2, 3, 4, 5], // Monday to Friday + * tz: utc * }) + * + * Cron.match(midnight, "2024-01-01T00:00:00Z") // => true + * Cron.match(businessHours, "2024-01-01T09:15:00Z") // => true * ``` * * @see {@link parse} for building a schedule from a cron expression string @@ -444,15 +451,15 @@ const CronParseErrorTypeId = "~effect/time/Cron/CronParseError" * * **Example** (Handling cron parse failures) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" * - * const result = Cron.parse("invalid expression") - * if (Result.isFailure(result)) { - * const error: Cron.CronParseError = result.failure - * console.log(error.message) // "Invalid number of segments in cron expression" - * console.log(error.input) // "invalid expression" - * } + * const expected = Result.fail(new Cron.CronParseError({ + * message: "Invalid number of segments in cron expression", + * input: "invalid expression" + * })) + * + * Cron.parse("invalid expression") // => expected * ``` * * @see {@link parse} for the parser that returns this error in `Result.fail` @@ -483,17 +490,12 @@ export class CronParseError extends Data.TaggedError("CronParseError")<{ * * **Example** (Checking cron parse errors) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" * - * const result = Cron.parse("invalid cron expression") - * if (Result.isFailure(result)) { - * const error = result.failure - * console.log(Cron.isCronParseError(error)) // true - * } - * - * console.log(Cron.isCronParseError(new Error("regular error"))) // false - * console.log(Cron.isCronParseError("not an error")) // false + * Result.mapError(Cron.parse("invalid cron expression"), Cron.isCronParseError) // => Result.fail(true) + * Cron.isCronParseError(new Error("regular error")) // => false + * Cron.isCronParseError("not an error") // => false * ``` * * @see {@link CronParseError} for the parse error type @@ -524,22 +526,14 @@ export const isCronParseError = (u: unknown): u is CronParseError => hasProperty * * **Example** (Parsing cron expressions) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" - * import * as assert from "node:assert" * * // At 04:00 on every day-of-month from 8 through 14. - * assert.deepStrictEqual( - * Cron.parse("0 0 4 8-14 * *"), - * Result.succeed(Cron.make({ - * seconds: [0], - * minutes: [0], - * hours: [4], - * days: [8, 9, 10, 11, 12, 13, 14], - * months: [], - * weekdays: [] - * })) - * ) + * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *")) + * + * Array.from(cron.hours) // => [4] + * Array.from(cron.days) // => [8, 9, 10, 11, 12, 13, 14] * ``` * * @see {@link parseUnsafe} for throwing on invalid cron expressions @@ -598,17 +592,19 @@ export const parse = (cron: string, tz?: DateTime.TimeZone | string): Result.Res * * **Example** (Parsing cron expressions unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Cron } from "effect" * * // At 04:00 on every day-of-month from 8 through 14 - * const cron = Cron.parseUnsafe("0 0 4 8-14 * *") + * const cron = Cron.parseUnsafe("0 0 4 8-14 * *", "UTC") * * // With timezone * const cronWithTz = Cron.parseUnsafe("0 0 9 * * *", "America/New_York") * * // This would throw an error * // const invalid = Cron.parseUnsafe("invalid expression") + * Cron.match(cron, "2024-01-10T04:00:00Z") // => true + * Cron.match(cronWithTz, "2024-01-01T14:00:00Z") // => true * ``` * * @category constructors @@ -616,6 +612,69 @@ export const parse = (cron: string, tz?: DateTime.TimeZone | string): Result.Res */ export const parseUnsafe = (cron: string, tz?: DateTime.TimeZone | string): Cron => Result.getOrThrow(parse(cron, tz)) +/** + * Formats a `Cron` instance as a cron expression. + * + * **Details** + * + * The default seconds field (`0`) is omitted unless `includeSeconds` is `true`. + * Other seconds configurations are always included. + * + * **Gotchas** + * + * Formatting drops the timezone information and the `and` restriction between + * days and weekdays. Parsing the result is therefore not guaranteed to produce + * an equivalent schedule. + * + * **Example** (Formatting a cron expression) + * + * ```ts import.meta.vitest + * import { Cron } from "effect" + * + * const cron = Cron.parseUnsafe("23 0-20/2 * * 0", "UTC") + * + * Cron.format(cron) // => "23 0-20/2 * * 0" + * Cron.format(cron, { includeSeconds: true }) // => "0 23 0-20/2 * * 0" + * ``` + * + * @category getters + * @since 4.0.0 + */ +export const format = (cron: Cron, options?: { + readonly includeSeconds?: boolean | undefined +}): string => { + const segments = [cron.seconds, cron.minutes, cron.hours, cron.days, cron.months, cron.weekdays] + .map(formatSegment) + return ( + options?.includeSeconds !== true && cron.seconds.size === 1 && cron.seconds.has(0) ? segments.slice(1) : segments + ).join(" ") +} + +const formatSegment = (values: ReadonlySet): string => { + if (values.size === 0) { + return "*" + } + const array = Array.from(values) + const segments: Array = [] + let index = 0 + while (index < array.length) { + const start = array[index]! + const step = array[index + 1]! - start + if (index + 2 < array.length && array[index + 2]! - array[index + 1]! === step) { + let end = index + 2 + while (end + 1 < array.length && array[end + 1]! - array[end]! === step) { + end++ + } + segments.push(`${start}-${array[end]}${step === 1 ? "" : `/${step}`}`) + index = end + 1 + } else { + segments.push(`${start}`) + index++ + } + } + return segments.join(",") +} + /** * Returns `true` when a date/time matches a `Cron` schedule. * @@ -635,20 +694,15 @@ export const parseUnsafe = (cron: string, tz?: DateTime.TimeZone | string): Cron * * **Example** (Matching dates against a schedule) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" * * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *", "UTC")) * * // Check if specific dates match - * const matches1 = Cron.match(cron, new Date("2021-01-08T04:00:00Z")) - * console.log(matches1) // true - 4 AM on the 8th - * - * const matches2 = Cron.match(cron, new Date("2021-01-08T05:00:00Z")) - * console.log(matches2) // false - wrong hour - * - * const matches3 = Cron.match(cron, new Date("2021-01-07T04:00:00Z")) - * console.log(matches3) // false - wrong day + * Cron.match(cron, "2021-01-08T04:00:00Z") // => true + * Cron.match(cron, "2021-01-08T05:00:00Z") // => false + * Cron.match(cron, "2021-01-07T04:00:00Z") // => false * ``` * * @see {@link next} for finding the next matching date/time @@ -717,19 +771,13 @@ const daysInMonth = (date: Date): number => * * **Example** (Finding the next occurrence) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" * * const cron = Result.getOrThrow(Cron.parse("0 0 4 8-14 * *", "UTC")) * * // Get next run after a specific date - * const after = new Date("2021-01-01T00:00:00Z") - * const nextRun = Cron.next(cron, after) - * console.log(nextRun.toISOString()) // 2021-01-08T04:00:00.000Z - * - * // Get next run from current time - * const nextFromNow = Cron.next(cron) - * console.log(nextFromNow) // Next occurrence from now + * Cron.next(cron, "2021-01-01T00:00:00Z").toISOString() // => "2021-01-08T04:00:00.000Z" * ``` * * @see {@link prev} for finding the previous scheduled occurrence @@ -964,17 +1012,23 @@ const stepCron = (cron: Cron, now: DateTime.DateTime.Input | undefined, directio * * **Example** (Iterating scheduled occurrences) * - * ```ts + * ```ts import.meta.vitest * import { Cron, Result } from "effect" * * const cron = Result.getOrThrow(Cron.parse("0 0 9 * * 1-5", "UTC")) // 9 AM weekdays * * // Get first 5 occurrences - * const iterator = Cron.sequence(cron, new Date("2023-01-01T00:00:00Z")) + * const iterator = Cron.sequence(cron, "2023-01-01T00:00:00Z") * const next5 = Array.from({ length: 5 }, () => iterator.next().value.toISOString()) - * - * console.log(next5) - * // ["2023-01-02T09:00:00.000Z", "2023-01-03T09:00:00.000Z", ...] + * const expected = [ + * "2023-01-02T09:00:00.000Z", + * "2023-01-03T09:00:00.000Z", + * "2023-01-04T09:00:00.000Z", + * "2023-01-05T09:00:00.000Z", + * "2023-01-06T09:00:00.000Z" + * ] + * + * next5 // => expected * ``` * * @see {@link next} for computing one next occurrence @@ -1004,7 +1058,7 @@ export const sequence = function*(cron: Cron, now?: DateTime.DateTime.Input): It * * **Example** (Comparing schedules with equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Cron } from "effect" * * const cron1 = Cron.make({ @@ -1023,7 +1077,7 @@ export const sequence = function*(cron: Cron, now?: DateTime.DateTime.Input): It * weekdays: [1, 2, 3, 4, 5] * }) * - * console.log(Cron.Equivalence(cron1, cron2)) // true + * Cron.Equivalence(cron1, cron2) // => true * ``` * * @see {@link equals} for directly comparing two `Cron` values @@ -1062,7 +1116,7 @@ const restrictionsEquals = (self: ReadonlySet, that: ReadonlySet * * **Example** (Checking schedule equality) * - * ```ts + * ```ts import.meta.vitest * import { Cron } from "effect" * * const cron1 = Cron.make({ @@ -1081,8 +1135,8 @@ const restrictionsEquals = (self: ReadonlySet, that: ReadonlySet * weekdays: [1, 2, 3, 4, 5] * }) * - * console.log(Cron.equals(cron1, cron2)) // true - * console.log(Cron.equals(cron1)(cron2)) // true (curried form) + * Cron.equals(cron1, cron2) // => true + * Cron.equals(cron1)(cron2) // => true * ``` * * @see {@link Equivalence} for the reusable equivalence instance diff --git a/.context/effect/packages/effect/src/Crypto.ts b/.context/effect/packages/effect/src/Crypto.ts index d3cdc018b..b817a3e8c 100644 --- a/.context/effect/packages/effect/src/Crypto.ts +++ b/.context/effect/packages/effect/src/Crypto.ts @@ -25,7 +25,7 @@ const TypeId = "~effect/platform/Crypto" * * **Example** (Using a digest algorithm) * - * ```ts + * ```ts import.meta.vitest * import { Crypto } from "effect" * * const algorithm: Crypto.DigestAlgorithm = "SHA-256" @@ -47,7 +47,7 @@ export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" * * **Example** (Using cryptographic operations) * - * ```ts + * ```ts import.meta.vitest * import { Crypto, Effect, Layer } from "effect" * * const TestCrypto = Layer.succeed( @@ -62,15 +62,14 @@ export type DigestAlgorithm = "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512" * const crypto = yield* Crypto.Crypto * const bytes = yield* crypto.randomBytes(16) * const uuidv4 = yield* crypto.randomUUIDv4 - * const uuidv7 = yield* crypto.randomUUIDv7 * const hash = yield* crypto.digest("SHA-256", bytes) - * return { uuidv4, uuidv7, hash } + * return [bytes.length, uuidv4.length, hash.length] * }) * - * Effect.runPromise(Effect.provide(program, TestCrypto)) + * await Effect.runPromise(Effect.provide(program, TestCrypto)) // => [16, 36, 16] * ``` * - * @category models + * @category services * @since 4.0.0 */ export interface Crypto { @@ -196,16 +195,15 @@ export const Crypto: Context.Service = Context.Service("effect/C * * **Example** (Creating a Crypto service) * - * ```ts - * import { Crypto, Effect, Layer } from "effect" + * ```ts import.meta.vitest + * import { Crypto, Effect } from "effect" * - * const TestCrypto = Layer.succeed( - * Crypto.Crypto, - * Crypto.make({ - * randomBytes: (size) => new Uint8Array(size), - * digest: (_algorithm, data) => Effect.succeed(data) - * }) - * ) + * const testCrypto = Crypto.make({ + * randomBytes: (size) => new Uint8Array(size), + * digest: (_algorithm, data) => Effect.succeed(data) + * }) + * + * await Effect.runPromise(testCrypto.randomBytes(4)) // => new Uint8Array([0, 0, 0, 0]) * ``` * * @category constructors @@ -224,15 +222,24 @@ export const make = ( const randomBytes: Crypto["randomBytes"] = (size) => Effect.map(validateSize("randomBytes", size), randomBytesUnsafe) - const nextDoubleUnsafe = (): number => { - const bytes = randomBytesUnsafe(7) - const value = ((bytes[0] & 0x1f) * 2 ** 48) + (bytes[1] * 2 ** 40) + (bytes[2] * 2 ** 32) + - (bytes[3] * 2 ** 24) + (bytes[4] * 2 ** 16) + (bytes[5] * 2 ** 8) + bytes[6] - return value / 2 ** 53 - } + const readUint53 = (bytes: Uint8Array): number => + ((bytes[0] & 0x1f) * 2 ** 48) + (bytes[1] * 2 ** 40) + (bytes[2] * 2 ** 32) + + (bytes[3] * 2 ** 24) + (bytes[4] * 2 ** 16) + (bytes[5] * 2 ** 8) + bytes[6] + + const nextDoubleUnsafe = (): number => readUint53(randomBytesUnsafe(7)) / 2 ** 53 - const nextIntUnsafe = (): number => - Math.floor(nextDoubleUnsafe() * (Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER + 1)) + Number.MIN_SAFE_INTEGER + const nextIntUnsafe = (): number => { + while (true) { + const bytes = randomBytesUnsafe(7) + const value = readUint53(bytes) + if ((bytes[0] & 0x20) === 0) { + return value + Number.MIN_SAFE_INTEGER + } + if (value < Number.MAX_SAFE_INTEGER) { + return value + 1 + } + } + } return Crypto.of({ [TypeId]: TypeId, diff --git a/.context/effect/packages/effect/src/Data.ts b/.context/effect/packages/effect/src/Data.ts index 206297749..2228bb0e9 100644 --- a/.context/effect/packages/effect/src/Data.ts +++ b/.context/effect/packages/effect/src/Data.ts @@ -10,6 +10,7 @@ */ import type * as Cause from "./Cause.ts" import * as core from "./internal/core.ts" +import * as InternalRecord from "./internal/record.ts" import * as Pipeable from "./Pipeable.ts" import * as Predicate from "./Predicate.ts" import type * as Types from "./Types.ts" @@ -30,16 +31,12 @@ import type { Unify } from "./Unify.ts" * * **Example** (Defining a value class) * - * ```ts + * ```ts import.meta.vitest * import { Data, Equal } from "effect" * * class Person extends Data.Class<{ readonly name: string }> {} * - * const mike1 = new Person({ name: "Mike" }) - * const mike2 = new Person({ name: "Mike" }) - * - * console.log(Equal.equals(mike1, mike2)) - * // true + * Equal.equals(new Person({ name: "Mike" }), new Person({ name: "Mike" })) // => true * ``` * * @see {@link TaggedClass} — adds a `_tag` field @@ -51,10 +48,10 @@ import type { Unify } from "./Unify.ts" export const Class: new = {}>( args: Types.VoidIfEmpty<{ readonly [P in keyof A]: A[P] }> ) => Readonly & Pipeable.Pipeable = class extends Pipeable.Class { - constructor(props: any) { + constructor(props: object | undefined) { super() if (props) { - Object.assign(this, props) + InternalRecord.assignProperties(this, props) } } } as any @@ -74,16 +71,14 @@ export const Class: new = {}>( * * **Example** (Defining a tagged class) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * class Person extends Data.TaggedClass("Person")<{ * readonly name: string * }> {} * - * const mike = new Person({ name: "Mike" }) - * console.log(mike._tag) - * // "Person" + * new Person({ name: "Mike" })._tag // => "Person" * ``` * * @see {@link Class} — without a `_tag` @@ -120,7 +115,7 @@ export const TaggedClass = ( * * **Example** (Defining a tagged enum) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * type HttpError = Data.TaggedEnum<{ @@ -134,9 +129,7 @@ export const TaggedClass = ( * * const { BadRequest, NotFound } = Data.taggedEnum() * - * const err = BadRequest({ status: 400, message: "missing id" }) - * console.log(err._tag) - * // "BadRequest" + * BadRequest({ status: 400, message: "missing id" })._tag // => "BadRequest" * ``` * * @see {@link taggedEnum} — constructors and matchers for a `TaggedEnum` @@ -197,7 +190,7 @@ export declare namespace TaggedEnum { * * **Example** (Defining a generic tagged enum) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * type MyResult = Data.TaggedEnum<{ @@ -213,6 +206,7 @@ export declare namespace TaggedEnum { * * const ok = Success({ value: 42 }) * // ok: { readonly _tag: "Success"; readonly value: number } + * ok // => { value: 42, _tag: "Success" } * ``` * * @see {@link Kind} — apply concrete types to a `WithGenerics` definition @@ -241,7 +235,7 @@ export declare namespace TaggedEnum { * * **Example** (Applying generics) * - * ```ts + * ```ts import.meta.vitest * import type { Data } from "effect" * * type Option = Data.TaggedEnum<{ @@ -252,9 +246,9 @@ export declare namespace TaggedEnum { * readonly taggedEnum: Option * } * - * // Resolve to the concrete union for `string` - * type StringOption = Data.TaggedEnum.Kind + * // Resolves to the concrete union for `string`: * // { _tag: "None" } | { _tag: "Some"; value: string } + * type StringOption = Data.TaggedEnum.Kind * ``` * * @see {@link WithGenerics} — define the generic shape @@ -290,7 +284,7 @@ export declare namespace TaggedEnum { * * **Example** (Extracting variant args) * - * ```ts + * ```ts import.meta.vitest * import type { Data } from "effect" * * type Result = @@ -327,7 +321,7 @@ export declare namespace TaggedEnum { * * **Example** (Extracting a variant type) * - * ```ts + * ```ts import.meta.vitest * import type { Data } from "effect" * * type Result = @@ -367,7 +361,7 @@ export declare namespace TaggedEnum { * * **Example** (Using the constructor object) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * type Shape = @@ -378,21 +372,19 @@ export declare namespace TaggedEnum { * * const shape = Circle({ radius: 10 }) * - * // Type guard * if ($is("Circle")(shape)) { - * console.log(shape.radius) + * shape.radius // => 10 * } * - * // Pattern matching - * const label = $match(shape, { + * $match(shape, { * Circle: (s) => `circle r=${s.radius}`, * Rect: (s) => `rect ${s.w}x${s.h}` - * }) + * }) // => "circle r=10" * ``` * * @see {@link taggedEnum} — creates constructors and matchers * - * @category types + * @category utility types * @since 3.1.0 */ export type Constructor = Types.Simplify< @@ -539,7 +531,7 @@ export declare namespace TaggedEnum { * * **Example** (Creating and matching tagged enum values) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * type HttpError = Data.TaggedEnum<{ @@ -551,20 +543,17 @@ export declare namespace TaggedEnum { * * const err = NotFound({ url: "/missing" }) * - * // Type guard - * console.log($is("NotFound")(err)) // true + * $is("NotFound")(err) // => true * - * // Pattern matching - * const msg = $match(err, { + * $match(err, { * BadRequest: (e) => e.message, * NotFound: (e) => `${e.url} not found` - * }) - * console.log(msg) // "/missing not found" + * }) // => "/missing not found" * ``` * * **Example** (Defining a generic tagged enum) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * * type MyResult = Data.TaggedEnum<{ @@ -578,6 +567,7 @@ export declare namespace TaggedEnum { * * const ok = Success({ value: 42 }) * // ok: { readonly _tag: "Success"; readonly value: number } + * ok // => { value: 42, _tag: "Success" } * ``` * * @see {@link TaggedEnum} — the type-level companion @@ -698,8 +688,8 @@ function taggedMatch< * * **Example** (Defining a yieldable error) * - * ```ts - * import { Data, Effect } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Exit } from "effect" * * class NetworkError extends Data.Error<{ * readonly code: number @@ -710,8 +700,7 @@ function taggedMatch< * return yield* new NetworkError({ code: 500, message: "timeout" }) * }) * - * // The effect fails with a NetworkError - * Effect.runSync(Effect.exit(program)) + * Effect.runSync(Effect.exit(program)) // => Exit.fail(new NetworkError({ code: 500, message: "timeout" })) * ``` * * @see {@link TaggedError} — adds a `_tag` for `Effect.catchTag` @@ -740,7 +729,7 @@ export const Error: new = {}>( * * **Example** (Recovering by tag) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class NotFound extends Data.TaggedError("NotFound")<{ @@ -759,6 +748,8 @@ export const Error: new = {}>( * Effect.catchTag("NotFound", (e) => * Effect.succeed(`missing: ${e.resource}`)) * ) + * + * await Effect.runPromise(recovered) // => "missing: /users/42" * ``` * * @see {@link Error} — without a `_tag` diff --git a/.context/effect/packages/effect/src/DateTime.ts b/.context/effect/packages/effect/src/DateTime.ts index 36df2d897..c121861a2 100644 --- a/.context/effect/packages/effect/src/DateTime.ts +++ b/.context/effect/packages/effect/src/DateTime.ts @@ -353,21 +353,21 @@ export declare namespace TimeZone { * * **Example** (Resolving ambiguous local times) * - * ```ts - * import { DateTime } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Option } 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 ambiguousTime = { year: 2025, month: 11, day: 2, hour: 1, minute: 30 } * const timeZone = DateTime.zoneMakeNamedUnsafe("America/New_York") * - * DateTime.makeZoned(ambiguousTime, { + * const earlier = DateTime.makeZoned(ambiguousTime, { * timeZone, * adjustForTimeZone: true, * disambiguation: "earlier" * }) * // Earlier occurrence (DST time): 2025-11-02T05:30:00.000Z * - * DateTime.makeZoned(ambiguousTime, { + * const later = DateTime.makeZoned(ambiguousTime, { * timeZone, * adjustForTimeZone: true, * disambiguation: "later" @@ -375,21 +375,26 @@ export declare namespace TimeZone { * // 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 } + * const gapTime = { year: 2025, month: 3, day: 9, hour: 2, minute: 30 } * - * DateTime.makeZoned(gapTime, { + * const beforeGap = DateTime.makeZoned(gapTime, { * timeZone, * adjustForTimeZone: true, * disambiguation: "earlier" * }) * // Time before gap: 2025-03-09T06:30:00.000Z (01:30 EST) * - * DateTime.makeZoned(gapTime, { + * const afterGap = DateTime.makeZoned(gapTime, { * timeZone, * adjustForTimeZone: true, * disambiguation: "later" * }) * // Time after gap: 2025-03-09T07:30:00.000Z (03:30 EDT) + * + * earlier.pipe(Option.getOrThrow, DateTime.formatIso) // => "2025-11-02T05:30:00.000Z" + * later.pipe(Option.getOrThrow, DateTime.formatIso) // => "2025-11-02T06:30:00.000Z" + * beforeGap.pipe(Option.getOrThrow, DateTime.formatIso) // => "2025-03-09T06:30:00.000Z" + * afterGap.pipe(Option.getOrThrow, DateTime.formatIso) // => "2025-03-09T07:30:00.000Z" * ``` * * @category models @@ -510,7 +515,7 @@ export const isZoned: (self: DateTime) => self is Zoned = Internal.isZoned * * **Example** (Comparing DateTime values for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") @@ -518,7 +523,7 @@ export const isZoned: (self: DateTime) => self is Zoned = Internal.isZoned * timeZone: "Europe/London" * }) * - * console.log(DateTime.Equivalence(utc, zoned)) // true + * DateTime.Equivalence(utc, zoned) // => true * ``` * * @category instances @@ -536,7 +541,7 @@ export const Equivalence: Equ.Equivalence = Internal.Equivalence * * **Example** (Sorting DateTime values chronologically) * - * ```ts + * ```ts import.meta.vitest * import { Array, DateTime } from "effect" * * const dates = [ @@ -545,8 +550,7 @@ export const Equivalence: Equ.Equivalence = Internal.Equivalence * DateTime.makeUnsafe("2024-02-01") * ] * - * const sorted = Array.sort(dates, DateTime.Order) - * // Results in chronological order: 2024-01-01, 2024-02-01, 2024-03-01 + * Array.sort(dates, DateTime.Order).map(DateTime.formatIsoDateUtc) // => ["2024-01-01", "2024-02-01", "2024-03-01"] * ``` * * @category instances @@ -565,15 +569,14 @@ export const Order: order.Order = Internal.Order * * **Example** (Clamping DateTime values) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const min = DateTime.makeUnsafe("2024-01-01") * const max = DateTime.makeUnsafe("2024-12-31") * const date = DateTime.makeUnsafe("2025-06-15") * - * const clamped = DateTime.clamp(date, { minimum: min, maximum: max }) - * // clamped equals max (2024-12-31) + * DateTime.clamp(date, { minimum: min, maximum: max }) // => DateTime.makeUnsafe("2024-12-31") * ``` * * @category ordering @@ -602,13 +605,10 @@ export const clamp: { * * **Example** (Creating DateTime values from Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * const date = new Date("2024-01-01T12:00:00Z") - * const dateTime = DateTime.fromDateUnsafe(date) - * - * console.log(DateTime.formatIso(dateTime)) // "2024-01-01T12:00:00.000Z" + * DateTime.fromDateUnsafe(new Date("2024-01-01T12:00:00Z")) // => DateTime.makeUnsafe("2024-01-01T12:00:00Z") * ``` * * @category constructors @@ -634,20 +634,17 @@ export const fromDateUnsafe: (date: Date) => Utc = Internal.fromDateUnsafe * * **Example** (Creating DateTime values unsafely) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // from Date - * const fromDate = DateTime.makeUnsafe(new Date("2024-01-01T12:00:00Z")) - * console.log(DateTime.formatIso(fromDate)) // "2024-01-01T12:00:00.000Z" + * DateTime.makeUnsafe(new Date("2024-01-01T12:00:00Z")) // => DateTime.makeUnsafe("2024-01-01T12:00:00Z") * * // from parts - * const fromParts = DateTime.makeUnsafe({ year: 2024 }) - * console.log(DateTime.formatIso(fromParts)) // "2024-01-01T00:00:00.000Z" + * DateTime.makeUnsafe({ year: 2024 }) // => DateTime.makeUnsafe("2024-01-01T00:00:00Z") * * // from string - * const fromString = DateTime.makeUnsafe("2024-01-01") - * console.log(DateTime.formatIso(fromString)) // "2024-01-01T00:00:00.000Z" + * DateTime.makeUnsafe("2024-01-01") // => DateTime.makeUnsafe("2024-01-01T00:00:00Z") * ``` * * @category constructors @@ -655,6 +652,22 @@ export const fromDateUnsafe: (date: Date) => Utc = Internal.fromDateUnsafe */ export const makeUnsafe: (input: A) => DateTime.PreserveZone = Internal.makeUnsafe +/** + * Creates a `DateTime.Utc` from the number of seconds since the Unix epoch. + * + * **Example** (Creating from epoch seconds) + * + * ```ts import.meta.vitest + * import { DateTime } from "effect" + * + * DateTime.fromEpochSeconds(1704067200).toJSON() // => "2024-01-01T00:00:00.000Z" + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromEpochSeconds: (seconds: number) => Utc = Internal.fromEpochSeconds + /** * Create a `DateTime.Zoned` using `DateTime.makeUnsafe` and a time zone. * @@ -678,14 +691,14 @@ export const makeUnsafe: (input: A) => DateTime.Preser * * **Example** (Creating zoned DateTime values unsafely) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const zoned = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { * timeZone: "Europe/London" * }) * - * console.log(DateTime.formatIsoZoned(zoned)) // "2024-06-15T15:30:00.000+01:00[Europe/London]" + * DateTime.formatIsoZoned(zoned) // => "2024-06-15T15:30:00.000+01:00[Europe/London]" * ``` * * @category constructors @@ -720,17 +733,14 @@ export const makeZonedUnsafe: (input: DateTime.Input, options?: { * * **Example** (Creating optional zoned DateTime values) * - * ```ts - * import { DateTime } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Option } from "effect" * * const result = DateTime.makeZoned("2024-06-15T14:30:00Z", { * timeZone: "Europe/London" * }) * - * console.log(result._tag) // "Some" - * if (result._tag === "Some") { - * console.log(DateTime.formatIsoZoned(result.value)) // "2024-06-15T15:30:00.000+01:00[Europe/London]" - * } + * result.pipe(Option.map(DateTime.formatIsoZoned)) // => Option.some("2024-06-15T15:30:00.000+01:00[Europe/London]") * ``` * * @category constructors @@ -762,23 +772,19 @@ export const makeZoned: ( * * **Example** (Creating optional DateTime values) * - * ```ts - * import { DateTime } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Option } from "effect" * * // from Date - * const fromDate = DateTime.make(new Date("2024-01-01T12:00:00Z")) - * console.log(fromDate._tag) // "Some" + * DateTime.make(new Date("2024-01-01T12:00:00Z")) // => Option.some(DateTime.makeUnsafe("2024-01-01T12:00:00Z")) * * // from parts - * const fromParts = DateTime.make({ year: 2024 }) - * console.log(fromParts._tag) // "Some" + * DateTime.make({ year: 2024 }) // => Option.some(DateTime.makeUnsafe("2024-01-01T00:00:00Z")) * * // from string - * const fromString = DateTime.make("2024-01-01") - * console.log(fromString._tag) // "Some" + * DateTime.make("2024-01-01") // => Option.some(DateTime.makeUnsafe("2024-01-01T00:00:00Z")) * - * const invalid = DateTime.make("not a date") - * console.log(invalid._tag) // "None" + * DateTime.make("not a date") // => Option.none() * ``` * * @category constructors @@ -798,19 +804,15 @@ export const make: (input: A) => Option.Option Option.some("2024-01-01T11:00:00.000+01:00[Europe/Berlin]") * - * const invalid = DateTime.makeZonedFromString("invalid") - * console.log(invalid._tag === "None") // true + * DateTime.makeZonedFromString("2024-01-01T12:00:00Z") // => Option.none() + * DateTime.makeZonedFromString("invalid") // => Option.none() * ``` * * @category constructors @@ -819,17 +821,15 @@ export const make: (input: A) => Option.Option Option.Option = Internal.makeZonedFromString /** - * Gets the current time using the `Clock` service and convert it to a `DateTime`. + * Gets the current time using the `Clock` service and converts it to a `DateTime`. * * **Example** (Getting the current DateTime) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" + * import { TestClock } from "effect/testing" * - * Effect.gen(function*() { - * const now = yield* DateTime.nowAsDate - * console.log(now instanceof Date) // true - * }) + * await Effect.runPromise(Effect.map(DateTime.now, DateTime.isDateTime)) // => true * ``` * * @category constructors @@ -843,12 +843,11 @@ export const now: Effect.Effect = Internal.now * * **Example** (Getting the current Date) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" + * import { TestClock } from "effect/testing" * - * Effect.gen(function*() { - * const now = yield* DateTime.now - * }) + * await Effect.runPromise(Effect.map(DateTime.nowAsDate, (now) => now instanceof Date)) // => true * ``` * * @category constructors @@ -871,11 +870,10 @@ export const nowAsDate: Effect.Effect = Internal.nowAsDate * * **Example** (Getting the current DateTime unsafely) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * const now = DateTime.nowUnsafe() - * console.log(DateTime.formatIso(now)) + * Number.isFinite(DateTime.toEpochMillis(DateTime.nowUnsafe())) // => true * ``` * * @category constructors @@ -901,7 +899,7 @@ export const nowUnsafe: LazyArg = Internal.nowUnsafe * * **Example** (Converting DateTime values to UTC) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const now = DateTime.makeZonedUnsafe({ year: 2024 }, { @@ -910,9 +908,10 @@ export const nowUnsafe: LazyArg = Internal.nowUnsafe * * // set as UTC * const utc: DateTime.Utc = DateTime.toUtc(now) + * utc // => DateTime.makeUnsafe("2024-01-01T00:00:00Z") * ``` * - * @category time zones + * @category converting * @since 3.13.0 */ export const toUtc: (self: DateTime) => Utc = Internal.toUtc @@ -922,19 +921,16 @@ export const toUtc: (self: DateTime) => Utc = Internal.toUtc * * **Example** (Setting time zones) * - * ```ts - * import { DateTime, Effect } from "effect" + * ```ts import.meta.vitest + * import { DateTime } from "effect" * - * Effect.gen(function*() { - * const now = yield* DateTime.now - * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") + * const zoned: DateTime.Zoned = DateTime.setZone(DateTime.makeUnsafe("2024-01-01"), zone) * - * // set the time zone - * const zoned: DateTime.Zoned = DateTime.setZone(now, zone) - * }) + * DateTime.isZoned(zoned) // => true * ``` * - * @category time zones + * @category transforming * @since 3.6.0 */ export const setZone: { @@ -957,18 +953,16 @@ export const setZone: { * * **Example** (Setting fixed-offset time zones) * - * ```ts - * import { DateTime, Effect } from "effect" + * ```ts import.meta.vitest + * import { DateTime } from "effect" * - * Effect.gen(function*() { - * const now = yield* DateTime.now + * const dateTime = DateTime.makeUnsafe("2024-01-01") + * const zoned: DateTime.Zoned = DateTime.setZoneOffset(dateTime, 3 * 60 * 60 * 1000) * - * // set the offset time zone in milliseconds - * const zoned: DateTime.Zoned = DateTime.setZoneOffset(now, 3 * 60 * 60 * 1000) - * }) + * DateTime.zoneToString(zoned.zone) // => "+03:00" * ``` * - * @category time zones + * @category transforming * @since 3.6.0 */ export const setZoneOffset: { @@ -996,20 +990,17 @@ export const setZoneOffset: { * * **Example** (Creating named time zones unsafely) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * const londonZone = DateTime.zoneMakeNamedUnsafe("Europe/London") - * console.log(DateTime.zoneToString(londonZone)) // "Europe/London" - * - * const tokyoZone = DateTime.zoneMakeNamedUnsafe("Asia/Tokyo") - * console.log(DateTime.zoneToString(tokyoZone)) // "Asia/Tokyo" + * DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Europe/London")) // => "Europe/London" + * DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Asia/Tokyo")) // => "Asia/Tokyo" * * // This would throw an IllegalArgumentError: * // DateTime.zoneMakeNamedUnsafe("Invalid/Zone") * ``` * - * @category time zones + * @category constructors * @since 4.0.0 */ export const zoneMakeNamedUnsafe: (zoneId: string) => TimeZone.Named = Internal.zoneMakeNamedUnsafe @@ -1024,7 +1015,7 @@ export const zoneMakeNamedUnsafe: (zoneId: string) => TimeZone.Named = Internal. * * **Example** (Creating fixed-offset time zones) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // Create a time zone with +3 hours offset @@ -1033,9 +1024,10 @@ export const zoneMakeNamedUnsafe: (zoneId: string) => TimeZone.Named = Internal. * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { * timeZone: zone * }) + * DateTime.formatIsoZoned(dt) // => "2024-01-01T15:00:00.000+03:00" * ``` * - * @category time zones + * @category constructors * @since 3.6.0 */ export const zoneMakeOffset: (offset: number) => TimeZone.Offset = Internal.zoneMakeOffset @@ -1049,17 +1041,14 @@ export const zoneMakeOffset: (offset: number) => TimeZone.Offset = Internal.zone * * **Example** (Creating optional named time zones) * - * ```ts - * import { DateTime } from "effect" - * - * const validZone = DateTime.zoneMakeNamed("Europe/London") - * console.log(validZone._tag === "Some") // true + * ```ts import.meta.vitest + * import { DateTime, Option } from "effect" * - * const invalidZone = DateTime.zoneMakeNamed("Invalid/Zone") - * console.log(invalidZone._tag === "None") // true + * DateTime.zoneMakeNamed("Europe/London").pipe(Option.map(DateTime.zoneToString)) // => Option.some("Europe/London") + * DateTime.zoneMakeNamed("Invalid/Zone") // => Option.none() * ``` * - * @category time zones + * @category constructors * @since 3.6.0 */ export const zoneMakeNamed: (zoneId: string) => Option.Option = Internal.zoneMakeNamed @@ -1074,7 +1063,7 @@ export const zoneMakeNamed: (zoneId: string) => Option.Option = * * **Example** (Creating named time zones effectfully) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * * const program = Effect.gen(function*() { @@ -1082,9 +1071,11 @@ export const zoneMakeNamed: (zoneId: string) => Option.Option = * const now = yield* DateTime.now * return DateTime.setZone(now, zone) * }) + * + * DateTime.zoneToString((await Effect.runPromise(program)).zone) // => "Europe/London" * ``` * - * @category time zones + * @category constructors * @since 3.6.0 */ export const zoneMakeNamedEffect: (zoneId: string) => Effect.Effect = @@ -1100,14 +1091,13 @@ export const zoneMakeNamedEffect: (zoneId: string) => Effect.Effect true * ``` * - * @category time zones + * @category constructors * @since 3.6.0 */ export const zoneMakeLocal: () => TimeZone.Named = Internal.zoneMakeLocal @@ -1121,19 +1111,15 @@ export const zoneMakeLocal: () => TimeZone.Named = Internal.zoneMakeLocal * * **Example** (Parsing time zones) * - * ```ts - * import { DateTime } from "effect" - * - * const namedZone = DateTime.zoneFromString("Europe/London") - * const offsetZone = DateTime.zoneFromString("+03:00") - * const invalid = DateTime.zoneFromString("invalid") + * ```ts import.meta.vitest + * import { DateTime, Option } from "effect" * - * console.log(namedZone._tag === "Some") // true - * console.log(offsetZone._tag === "Some") // true - * console.log(invalid._tag === "None") // true + * DateTime.zoneFromString("Europe/London").pipe(Option.map(DateTime.zoneToString)) // => Option.some("Europe/London") + * DateTime.zoneFromString("+03:00").pipe(Option.map(DateTime.zoneToString)) // => Option.some("+03:00") + * DateTime.zoneFromString("invalid") // => Option.none() * ``` * - * @category time zones + * @category decoding * @since 3.6.0 */ export const zoneFromString: (zone: string) => Option.Option = Internal.zoneFromString @@ -1143,17 +1129,14 @@ export const zoneFromString: (zone: string) => Option.Option = Interna * * **Example** (Formatting time zones) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * // Outputs "+03:00" - * DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) - * - * // Outputs "Europe/London" - * DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Europe/London")) + * DateTime.zoneToString(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) // => "+03:00" + * DateTime.zoneToString(DateTime.zoneMakeNamedUnsafe("Europe/London")) // => "Europe/London" * ``` * - * @category time zones + * @category encoding * @since 3.6.0 */ export const zoneToString: (self: TimeZone) => string = Internal.zoneToString @@ -1164,17 +1147,16 @@ export const zoneToString: (self: TimeZone) => string = Internal.zoneToString * * **Example** (Setting named time zones safely) * - * ```ts - * import { DateTime, Effect } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Option } from "effect" * - * Effect.gen(function*() { - * const now = yield* DateTime.now - * // set the time zone, returns an Option - * DateTime.setZoneNamed(now, "Europe/London") - * }) + * const dateTime = DateTime.makeUnsafe("2024-01-01") + * const result = DateTime.setZoneNamed(dateTime, "Europe/London").pipe(Option.map(DateTime.formatIsoZoned)) + * + * result // => Option.some("2024-01-01T00:00:00.000+00:00[Europe/London]") * ``` * - * @category time zones + * @category transforming * @since 3.6.0 */ export const setZoneNamed: { @@ -1194,17 +1176,16 @@ export const setZoneNamed: { * * **Example** (Setting named time zones unsafely) * - * ```ts - * import { DateTime, Effect } from "effect" + * ```ts import.meta.vitest + * import { DateTime } from "effect" * - * Effect.gen(function*() { - * const now = yield* DateTime.now - * // set the time zone - * DateTime.setZoneNamedUnsafe(now, "Europe/London") - * }) + * const dateTime = DateTime.makeUnsafe("2024-01-01") + * const zoned = DateTime.setZoneNamedUnsafe(dateTime, "Europe/London") + * + * DateTime.zoneToString(zoned.zone) // => "Europe/London" * ``` * - * @category time zones + * @category transforming * @since 4.0.0 */ export const setZoneNamedUnsafe: { @@ -1234,16 +1215,13 @@ export const setZoneNamedUnsafe: { * * **Example** (Measuring distance between DateTime values) * - * ```ts - * import { DateTime, Effect } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Duration } from "effect" * - * Effect.gen(function*() { - * const now = yield* DateTime.now - * const other = DateTime.add(now, { minutes: 1 }) + * const start = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + * const end = DateTime.add(start, { minutes: 1 }) * - * // returns Duration.minutes(1) - * DateTime.distance(now, other) - * }) + * DateTime.distance(start, end) // => Duration.minutes(1) * ``` * * @category comparisons @@ -1259,14 +1237,13 @@ export const distance: { * * **Example** (Selecting the earlier DateTime) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-01-01") * const date2 = DateTime.makeUnsafe("2024-02-01") * - * const earlier = DateTime.min(date1, date2) - * // earlier equals date1 (2024-01-01) + * DateTime.min(date1, date2) // => DateTime.makeUnsafe("2024-01-01") * ``` * * @category comparisons @@ -1282,14 +1259,13 @@ export const min: { * * **Example** (Selecting the later DateTime) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-01-01") * const date2 = DateTime.makeUnsafe("2024-02-01") * - * const later = DateTime.max(date1, date2) - * // later equals date2 (2024-02-01) + * DateTime.max(date1, date2) // => DateTime.makeUnsafe("2024-02-01") * ``` * * @category comparisons @@ -1305,14 +1281,14 @@ export const max: { * * **Example** (Checking whether a DateTime is later) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-02-01") * const date2 = DateTime.makeUnsafe("2024-01-01") * - * console.log(DateTime.isGreaterThan(date1, date2)) // true - * console.log(DateTime.isGreaterThan(date2, date1)) // false + * DateTime.isGreaterThan(date1, date2) // => true + * DateTime.isGreaterThan(date2, date1) // => false * ``` * * @category comparisons @@ -1328,16 +1304,16 @@ export const isGreaterThan: { * * **Example** (Checking whether a DateTime is later or equal) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-01-01") * const date2 = DateTime.makeUnsafe("2024-01-01") * const date3 = DateTime.makeUnsafe("2024-02-01") * - * console.log(DateTime.isGreaterThanOrEqualTo(date1, date2)) // true - * console.log(DateTime.isGreaterThanOrEqualTo(date3, date1)) // true - * console.log(DateTime.isGreaterThanOrEqualTo(date1, date3)) // false + * DateTime.isGreaterThanOrEqualTo(date1, date2) // => true + * DateTime.isGreaterThanOrEqualTo(date3, date1) // => true + * DateTime.isGreaterThanOrEqualTo(date1, date3) // => false * ``` * * @category comparisons @@ -1353,14 +1329,14 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking whether a DateTime is earlier) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-01-01") * const date2 = DateTime.makeUnsafe("2024-02-01") * - * console.log(DateTime.isLessThan(date1, date2)) // true - * console.log(DateTime.isLessThan(date2, date1)) // false + * DateTime.isLessThan(date1, date2) // => true + * DateTime.isLessThan(date2, date1) // => false * ``` * * @category comparisons @@ -1376,16 +1352,16 @@ export const isLessThan: { * * **Example** (Checking whether a DateTime is earlier or equal) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const date1 = DateTime.makeUnsafe("2024-01-01") * const date2 = DateTime.makeUnsafe("2024-01-01") * const date3 = DateTime.makeUnsafe("2024-02-01") * - * console.log(DateTime.isLessThanOrEqualTo(date1, date2)) // true - * console.log(DateTime.isLessThanOrEqualTo(date1, date3)) // true - * console.log(DateTime.isLessThanOrEqualTo(date3, date1)) // false + * DateTime.isLessThanOrEqualTo(date1, date2) // => true + * DateTime.isLessThanOrEqualTo(date1, date3) // => true + * DateTime.isLessThanOrEqualTo(date3, date1) // => false * ``` * * @category comparisons @@ -1401,14 +1377,14 @@ export const isLessThanOrEqualTo: { * * **Example** (Checking whether a DateTime is within bounds) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const min = DateTime.makeUnsafe("2024-01-01") * const max = DateTime.makeUnsafe("2024-12-31") * const date = DateTime.makeUnsafe("2024-06-15") * - * console.log(DateTime.between(date, { minimum: min, maximum: max })) // true + * DateTime.between(date, { minimum: min, maximum: max }) // => true * ``` * * @category comparisons @@ -1428,14 +1404,12 @@ export const between: { * * **Example** (Checking future DateTime values effectfully) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" + * import { TestClock } from "effect/testing" * - * const program = Effect.gen(function*() { - * const futureDate = DateTime.add(yield* DateTime.now, { hours: 1 }) - * const isFuture = yield* DateTime.isFuture(futureDate) - * console.log(isFuture) // true - * }) + * const futureDate = DateTime.makeUnsafe(1) + * await Effect.runPromise(Effect.provide(DateTime.isFuture(futureDate), TestClock.layer())) // => true * ``` * * @category comparisons @@ -1460,11 +1434,8 @@ export const isFuture: (self: DateTime) => Effect.Effect = Internal.isF * ```ts * import { DateTime } from "effect" * - * const now = DateTime.nowUnsafe() - * const futureDate = DateTime.add(now, { hours: 1 }) - * - * console.log(DateTime.isFutureUnsafe(futureDate)) // true - * console.log(DateTime.isFutureUnsafe(now)) // false + * const oneHourFromNow = DateTime.add(DateTime.nowUnsafe(), { hours: 1 }) + * DateTime.isFutureUnsafe(oneHourFromNow) * ``` * * @category comparisons @@ -1481,14 +1452,12 @@ export const isFutureUnsafe: (self: DateTime) => boolean = Internal.isFutureUnsa * * **Example** (Checking past DateTime values effectfully) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" + * import { TestClock } from "effect/testing" * - * const program = Effect.gen(function*() { - * const pastDate = DateTime.subtract(yield* DateTime.now, { hours: 1 }) - * const isPast = yield* DateTime.isPast(pastDate) - * console.log(isPast) // true - * }) + * const pastDate = DateTime.makeUnsafe(-1) + * await Effect.runPromise(Effect.provide(DateTime.isPast(pastDate), TestClock.layer())) // => true * ``` * * @category comparisons @@ -1513,11 +1482,8 @@ export const isPast: (self: DateTime) => Effect.Effect = Internal.isPas * ```ts * import { DateTime } from "effect" * - * const now = DateTime.nowUnsafe() - * const pastDate = DateTime.subtract(now, { hours: 1 }) - * - * console.log(DateTime.isPastUnsafe(pastDate)) // true - * console.log(DateTime.isPastUnsafe(now)) // false + * const oneHourAgo = DateTime.subtract(DateTime.nowUnsafe(), { hours: 1 }) + * DateTime.isPastUnsafe(oneHourAgo) * ``` * * @category comparisons @@ -1538,15 +1504,14 @@ export const isPastUnsafe: (self: DateTime) => boolean = Internal.isPastUnsafe * * **Example** (Converting DateTime values to UTC Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { * timeZone: "Europe/London" * }) * - * const utcDate = DateTime.toDateUtc(dt) - * console.log(utcDate.toISOString()) // "2024-01-01T12:00:00.000Z" + * DateTime.toDateUtc(dt).toISOString() // => "2024-01-01T12:00:00.000Z" * ``` * * @category converting @@ -1564,7 +1529,7 @@ export const toDateUtc: (self: DateTime) => Date = Internal.toDateUtc * * **Example** (Converting DateTime values to Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") @@ -1572,8 +1537,8 @@ export const toDateUtc: (self: DateTime) => Date = Internal.toDateUtc * timeZone: "Europe/London" * }) * - * console.log(DateTime.toDate(utc).toISOString()) - * console.log(DateTime.toDate(zoned).toISOString()) + * DateTime.toDate(utc).toISOString() // => "2024-01-01T12:00:00.000Z" + * DateTime.toDate(zoned).toISOString() // => "2024-01-01T12:00:00.000Z" * ``` * * @category converting @@ -1591,15 +1556,14 @@ export const toDate: (self: DateTime) => Date = Internal.toDate * * **Example** (Reading zoned offsets) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { * timeZone: "Europe/London" * }) * - * const offset = DateTime.zonedOffset(zoned) - * console.log(offset) // 0 (London is UTC+0 in winter) + * DateTime.zonedOffset(zoned) // => 0 * ``` * * @category converting @@ -1616,15 +1580,14 @@ export const zonedOffset: (self: Zoned) => number = Internal.zonedOffset * * **Example** (Formatting zoned offsets) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) // +3 hours * }) * - * const offsetString = DateTime.zonedOffsetIso(zoned) - * console.log(offsetString) // "+03:00" + * DateTime.zonedOffsetIso(zoned) // => "+03:00" * ``` * * @category converting @@ -1641,13 +1604,11 @@ export const zonedOffsetIso: (self: Zoned) => string = Internal.zonedOffsetIso * * **Example** (Reading epoch milliseconds) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") - * const epochMillis = DateTime.toEpochMillis(dt) - * - * console.log(epochMillis) // 1704067200000 + * DateTime.toEpochMillis(dt) // => 1704067200000 * ``` * * @category converting @@ -1655,13 +1616,35 @@ export const zonedOffsetIso: (self: Zoned) => string = Internal.zonedOffsetIso */ export const toEpochMillis: (self: DateTime) => number = Internal.toEpochMillis +/** + * Converts a `DateTime` to the number of seconds since the Unix epoch. + * + * **Details** + * + * This returns the UTC timestamp regardless of any time zone information. + * The result is floored to the nearest second. + * + * **Example** (Reading epoch seconds) + * + * ```ts import.meta.vitest + * import { DateTime } from "effect" + * + * const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + * DateTime.toEpochSeconds(dt) // => 1704067200 + * ``` + * + * @category converting + * @since 4.0.0 + */ +export const toEpochSeconds: (self: DateTime) => number = Internal.toEpochSeconds + /** * Removes the time aspect of a `DateTime`, first adjusting for the time * zone. It will return a `DateTime.Utc` only containing the date. * * **Example** (Removing time components) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // returns "2024-01-01T00:00:00Z" @@ -1671,7 +1654,7 @@ export const toEpochMillis: (self: DateTime) => number = Internal.toEpochMillis * }).pipe( * DateTime.removeTime, * DateTime.formatIso - * ) + * ) // => "2024-01-01T00:00:00.000Z" * ``` * * @category converting @@ -1692,26 +1675,16 @@ export const removeTime: (self: DateTime) => Utc = Internal.removeTime * * **Example** (Reading DateTime parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T12:30:45.123Z") * const parts = DateTime.toParts(dt) * - * console.log(parts) - * // { - * // year: 2024, - * // month: 1, - * // day: 1, - * // hours: 12, - * // minutes: 30, - * // seconds: 45, - * // millis: 123, - * // weekDay: 1 // Monday - * // } + * const selectedParts = [parts.year, parts.month, parts.day, parts.hour] // => [2024, 1, 1, 12] * ``` * - * @category parts + * @category getters * @since 3.6.0 */ export const toParts: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toParts @@ -1725,7 +1698,7 @@ export const toParts: (self: DateTime) => DateTime.PartsWithWeekday = Internal.t * * **Example** (Reading UTC DateTime parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:30:45.123Z", { @@ -1733,11 +1706,10 @@ export const toParts: (self: DateTime) => DateTime.PartsWithWeekday = Internal.t * }) * const parts = DateTime.toPartsUtc(zoned) * - * console.log(parts) - * // Always returns UTC parts regardless of time zone + * const selectedParts = [parts.year, parts.month, parts.day, parts.hour] // => [2024, 1, 1, 12] * ``` * - * @category parts + * @category getters * @since 3.6.0 */ export const toPartsUtc: (self: DateTime) => DateTime.PartsWithWeekday = Internal.toPartsUtc @@ -1751,15 +1723,14 @@ export const toPartsUtc: (self: DateTime) => DateTime.PartsWithWeekday = Interna * * **Example** (Reading UTC DateTime parts by key) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dateTime = DateTime.makeUnsafe({ year: 2024 }) - * const year = DateTime.getPartUtc(dateTime, "year") - * console.log(year) // 2024 + * DateTime.getPartUtc(dateTime, "year") // => 2024 * ``` * - * @category parts + * @category getters * @since 3.6.0 */ export const getPartUtc: { @@ -1776,17 +1747,16 @@ export const getPartUtc: { * * **Example** (Reading DateTime parts by key) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dateTime = DateTime.makeZonedUnsafe({ year: 2024 }, { * timeZone: "Europe/London" * }) - * const year = DateTime.getPart(dateTime, "year") - * console.log(year) // 2024 + * DateTime.getPart(dateTime, "year") // => 2024 * ``` * - * @category parts + * @category getters * @since 3.6.0 */ export const getPart: { @@ -1803,20 +1773,20 @@ export const getPart: { * * **Example** (Updating DateTime parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") + * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { timeZone: "UTC" }) * const updated = DateTime.setParts(dt, { * year: 2025, * month: 6, * day: 15 * }) * - * console.log(DateTime.formatIso(updated)) // "2025-06-15T12:00:00.000Z" + * updated // => DateTime.makeZonedUnsafe("2025-06-15T12:00:00Z", { timeZone: "UTC" }) * ``` * - * @category parts + * @category transforming * @since 3.6.0 */ export const setParts: { @@ -1833,7 +1803,7 @@ export const setParts: { * * **Example** (Updating UTC DateTime parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") @@ -1842,10 +1812,10 @@ export const setParts: { * hour: 18 * }) * - * console.log(DateTime.formatIso(updated)) // "2025-01-01T18:00:00.000Z" + * updated // => DateTime.makeUnsafe("2025-01-01T18:00:00Z") * ``` * - * @category parts + * @category transforming * @since 3.6.0 */ export const setPartsUtc: { @@ -1869,21 +1839,19 @@ export const setPartsUtc: { * * **Example** (Accessing the current time zone service) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * * const program = Effect.gen(function*() { - * // Access the current time zone service - * const zone = yield* DateTime.CurrentTimeZone - * console.log(DateTime.zoneToString(zone)) + * return DateTime.zoneToString(yield* DateTime.CurrentTimeZone) * }) * * // Provide a time zone * const layer = DateTime.layerCurrentZoneNamed("Europe/London") - * Effect.provide(program, layer) + * await Effect.runPromise(Effect.provide(program, layer)) // => "Europe/London" * ``` * - * @category current time zone + * @category services * @since 3.11.0 */ export class CurrentTimeZone extends Context.Service()( @@ -1896,18 +1864,16 @@ export class CurrentTimeZone extends Context.Service( * * **Example** (Setting the current time zone) * - * ```ts + * ```ts import.meta.vitest * 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")) + * await Effect.runPromise(Effect.gen(function*() { + * const zoned = yield* DateTime.setZoneCurrent(DateTime.makeUnsafe("2024-01-01")) + * return DateTime.zoneToString(zoned.zone) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London"))) // => "Europe/London" * ``` * - * @category current time zone + * @category accessors * @since 3.6.0 */ export const setZoneCurrent = (self: DateTime): Effect.Effect => @@ -1918,17 +1884,18 @@ export const setZoneCurrent = (self: DateTime): Effect.Effect "Europe/London" * ``` * - * @category current time zone + * @category providing services * @since 3.6.0 */ export const withCurrentZone: { @@ -1942,16 +1909,15 @@ export const withCurrentZone: { * * **Example** (Providing the local time zone) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * - * Effect.gen(function*() { - * // will use the system's local time zone - * const now = yield* DateTime.nowInCurrentZone - * }).pipe(DateTime.withCurrentZoneLocal) + * await Effect.runPromise(Effect.gen(function*() { + * return DateTime.isZoned(yield* DateTime.nowInCurrentZone) + * }).pipe(DateTime.withCurrentZoneLocal)) // => true * ``` * - * @category current time zone + * @category providing services * @since 3.6.0 */ export const withCurrentZoneLocal = ( @@ -1964,16 +1930,17 @@ export const withCurrentZoneLocal = ( * * **Example** (Providing a fixed-offset time zone) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * - * Effect.gen(function*() { - * const zone = yield* DateTime.CurrentTimeZone - * console.log(DateTime.zoneToString(zone)) // "+03:00" + * const program = Effect.gen(function*() { + * return DateTime.zoneToString(yield* DateTime.CurrentTimeZone) * }).pipe(DateTime.withCurrentZoneOffset(3 * 60 * 60 * 1000)) + * + * await Effect.runPromise(program) // => "+03:00" * ``` * - * @category current time zone + * @category providing services * @since 3.6.0 */ export const withCurrentZoneOffset: { @@ -1997,16 +1964,16 @@ export const withCurrentZoneOffset: { * * **Example** (Providing a named time zone) * - * ```ts + * ```ts import.meta.vitest * 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")) + * await Effect.runPromise(Effect.gen(function*() { + * const zoned = yield* DateTime.setZoneCurrent(DateTime.makeUnsafe("2024-01-01")) + * return DateTime.zoneToString(zoned.zone) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London"))) // => "Europe/London" * ``` * - * @category current time zone + * @category providing services * @since 3.6.0 */ export const withCurrentZoneNamed: { @@ -2031,16 +1998,15 @@ export const withCurrentZoneNamed: { * * **Example** (Getting the current time in the current zone) * - * ```ts + * ```ts import.meta.vitest * 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")) + * await Effect.runPromise(Effect.gen(function*() { + * return DateTime.zoneToString((yield* DateTime.nowInCurrentZone).zone) + * }).pipe(DateTime.withCurrentZoneNamed("Europe/London"))) // => "Europe/London" * ``` * - * @category current time zone + * @category accessors * @since 3.6.0 */ export const nowInCurrentZone: Effect.Effect = Effect.flatMap(now, setZoneCurrent) @@ -2066,17 +2032,15 @@ export const nowInCurrentZone: Effect.Effect = Ef * * **Example** (Mutating DateTime values with Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T12:00:00Z") * - * const modified = DateTime.mutate(dt, (date) => { + * DateTime.mutate(dt, (date) => { * date.setHours(15) // Set to 3 PM * date.setMinutes(30) // Set to 30 minutes * }) - * - * console.log(DateTime.formatIso(modified)) // "2024-01-01T15:30:00.000Z" * ``` * * @category mapping @@ -2108,7 +2072,7 @@ export const mutate: { * * **Example** (Mutating DateTime values with UTC Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { @@ -2119,7 +2083,7 @@ export const mutate: { * date.setUTCHours(18) // Set UTC time to 6 PM * }) * - * console.log(DateTime.formatIso(modified)) // "2024-01-01T18:00:00.000Z" + * modified // => DateTime.makeZonedUnsafe("2024-01-01T18:00:00Z", { timeZone: "Europe/London" }) * ``` * * @category mapping @@ -2136,13 +2100,14 @@ export const mutateUtc: { * * **Example** (Mapping epoch milliseconds) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // add 10 milliseconds - * DateTime.makeUnsafe(0).pipe( + * const result = DateTime.makeUnsafe(0).pipe( * DateTime.mapEpochMillis((millis) => millis + 10) * ) + * result // => DateTime.makeUnsafe(10) * ``` * * @category mapping @@ -2165,13 +2130,13 @@ export const mapEpochMillis: { * * **Example** (Applying time zone adjusted Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // get the time zone adjusted date in milliseconds * DateTime.makeZonedUnsafe(0, { timeZone: "Europe/London" }).pipe( * DateTime.withDate((date) => date.getTime()) - * ) + * ) // => 3600000 * ``` * * @category mapping @@ -2193,13 +2158,13 @@ export const withDate: { * * **Example** (Applying UTC Dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // get the date in milliseconds * DateTime.makeUnsafe(0).pipe( * DateTime.withDateUtc((date) => date.getTime()) - * ) + * ) // => 0 * ``` * * @category mapping @@ -2215,7 +2180,7 @@ export const withDateUtc: { * * **Example** (Pattern matching DateTime variants) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt1 = DateTime.makeUnsafe("2024-01-01T12:00:00Z") // Utc @@ -2233,8 +2198,8 @@ export const withDateUtc: { * onZoned: (zoned) => `Zoned: ${DateTime.formatIsoZoned(zoned)}` * }) * - * console.log(result1) // "UTC: 2024-01-01T12:00:00.000Z" - * console.log(result2) // "Zoned: 2024-06-15T15:30:00.000+01:00[Europe/London]" + * result1 // => "UTC: 2024-01-01T12:00:00.000Z" + * result2 // => "Zoned: 2024-06-15T15:30:00.000+01:00[Europe/London]" * ``` * * @category mapping @@ -2276,13 +2241,13 @@ export const match: { * * **Example** (Adding durations) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // add 5 minutes * DateTime.makeUnsafe(0).pipe( * DateTime.addDuration("5 minutes") - * ) + * ) // => DateTime.makeUnsafe(300000) * ``` * * @see {@link add} for calendar-aware date/time part arithmetic @@ -2301,13 +2266,13 @@ export const addDuration: { * * **Example** (Subtracting durations) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // subtract 5 minutes * DateTime.makeUnsafe(0).pipe( * DateTime.subtractDuration("5 minutes") - * ) + * ) // => DateTime.makeUnsafe(-300000) * ``` * * @category math @@ -2328,13 +2293,13 @@ export const subtractDuration: { * * **Example** (Adding date and time parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // add 5 minutes * DateTime.makeUnsafe(0).pipe( * DateTime.add({ minutes: 5 }) - * ) + * ) // => DateTime.makeUnsafe(300000) * ``` * * @category math @@ -2350,13 +2315,13 @@ export const add: { * * **Example** (Subtracting date and time parts) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // subtract 5 minutes * DateTime.makeUnsafe(0).pipe( * DateTime.subtract({ minutes: 5 }) - * ) + * ) // => DateTime.makeUnsafe(-300000) * ``` * * @category math @@ -2377,14 +2342,13 @@ export const subtract: { * * **Example** (Rounding down DateTime values) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // returns "2024-01-01T00:00:00Z" * DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe( * DateTime.startOf("day"), - * DateTime.formatIso - * ) + * ) // => DateTime.makeUnsafe("2024-01-01T00:00:00Z") * ``` * * @category math @@ -2412,14 +2376,13 @@ export const startOf: { * * **Example** (Rounding up DateTime values) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // returns "2024-01-01T23:59:59.999Z" * DateTime.makeUnsafe("2024-01-01T12:00:00Z").pipe( * DateTime.endOf("day"), - * DateTime.formatIso - * ) + * ) // => DateTime.makeUnsafe("2024-01-01T23:59:59.999Z") * ``` * * @category math @@ -2447,14 +2410,13 @@ export const endOf: { * * **Example** (Rounding DateTime values to nearest units) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * // returns "2024-01-02T00:00:00Z" * DateTime.makeUnsafe("2024-01-01T12:01:00Z").pipe( * DateTime.nearest("day"), - * DateTime.formatIso - * ) + * ) // => DateTime.makeUnsafe("2024-01-02T00:00:00Z") * ``` * * @category math @@ -2490,20 +2452,18 @@ export const nearest: { * * **Example** (Formatting DateTime values with Intl options) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { * timeZone: "Europe/London" * }) * - * const formatted = DateTime.format(dt, { + * DateTime.format(dt, { * dateStyle: "full", * timeStyle: "short", * locale: "en-US" - * }) - * - * console.log(formatted) // "Saturday, June 15, 2024 at 3:30 PM" + * }) // => "Saturday, June 15, 2024 at 3:30 PM" * ``` * * @category formatting @@ -2533,21 +2493,19 @@ export const format: { * * **Example** (Formatting DateTime values locally) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-06-15T14:30:00Z") * * // Uses system local time zone and locale - * const local = DateTime.formatLocal(dt, { + * DateTime.formatLocal(dt, { * year: "numeric", * month: "long", * day: "numeric", * hour: "2-digit", * minute: "2-digit" * }) - * - * console.log(local) // Output depends on system locale/timezone * ``` * * @category formatting @@ -2580,7 +2538,7 @@ export const formatLocal: { * * **Example** (Formatting DateTime values in UTC) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeZonedUnsafe("2024-06-15T14:30:00Z", { @@ -2588,7 +2546,7 @@ export const formatLocal: { * }) * * // Force UTC formatting regardless of time zone - * const utcFormatted = DateTime.formatUtc(dt, { + * DateTime.formatUtc(dt, { * year: "numeric", * month: "2-digit", * day: "2-digit", @@ -2596,8 +2554,6 @@ export const formatLocal: { * minute: "2-digit", * timeZoneName: "short" * }) - * - * console.log(utcFormatted) // "06/15/2024, 02:30 PM UTC" * ``` * * @category formatting @@ -2636,7 +2592,7 @@ export const formatUtc: { * * **Example** (Formatting DateTime values with custom formatters) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-06-15T14:30:00Z") @@ -2651,8 +2607,7 @@ export const formatUtc: { * timeZone: "Europe/Berlin" * }) * - * const formatted = DateTime.formatIntl(dt, formatter) - * console.log(formatted.length > 0) // true + * DateTime.formatIntl(dt, formatter).length > 0 // => true * ``` * * @see {@link formatUtc} for formatting with options forced to UTC @@ -2675,16 +2630,15 @@ export const formatIntl: { * * **Example** (Formatting DateTime values as ISO strings) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * - * const dt = DateTime.makeUnsafe("2024-01-01T12:30:45.123Z") - * console.log(DateTime.formatIso(dt)) // "2024-01-01T12:30:45.123Z" + * DateTime.formatIso(DateTime.makeUnsafe("2024-01-01T12:30:45.123Z")) // => "2024-01-01T12:30:45.123Z" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:30:45.123Z", { * timeZone: "Europe/London" * }) - * console.log(DateTime.formatIso(zoned)) // "2024-01-01T12:30:45.123Z" + * DateTime.formatIso(zoned) // => "2024-01-01T12:30:45.123Z" * ``` * * @category formatting @@ -2701,16 +2655,16 @@ export const formatIso: (self: DateTime) => string = Internal.formatIso * * **Example** (Formatting DateTime values as ISO dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T23:30:00Z") - * console.log(DateTime.formatIsoDate(dt)) // "2024-01-01" + * DateTime.formatIsoDate(dt) // => "2024-01-01" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T23:30:00Z", { * timeZone: "Pacific/Auckland" // UTC+12/13 * }) - * console.log(DateTime.formatIsoDate(zoned)) // "2024-01-02" (next day in Auckland) + * DateTime.formatIsoDate(zoned) // => "2024-01-02" * ``` * * @category formatting @@ -2727,16 +2681,16 @@ export const formatIsoDate: (self: DateTime) => string = Internal.formatIsoDate * * **Example** (Formatting DateTime values as UTC ISO dates) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const dt = DateTime.makeUnsafe("2024-01-01T23:30:00Z") - * console.log(DateTime.formatIsoDateUtc(dt)) // "2024-01-01" + * DateTime.formatIsoDateUtc(dt) // => "2024-01-01" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T23:30:00Z", { * timeZone: "Pacific/Auckland" * }) - * console.log(DateTime.formatIsoDateUtc(zoned)) // "2024-01-01" (always UTC) + * DateTime.formatIsoDateUtc(zoned) // => "2024-01-01" * ``` * * @category formatting @@ -2754,16 +2708,16 @@ export const formatIsoDateUtc: (self: DateTime) => string = Internal.formatIsoDa * * **Example** (Formatting DateTime values with offsets) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const utc = DateTime.makeUnsafe("2024-01-01T12:00:00Z") - * console.log(DateTime.formatIsoOffset(utc)) // "2024-01-01T12:00:00.000Z" + * DateTime.formatIsoOffset(utc) // => "2024-01-01T12:00:00.000Z" * * const zoned = DateTime.makeZonedUnsafe("2024-01-01T12:00:00Z", { * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) * }) - * console.log(DateTime.formatIsoOffset(zoned)) // "2024-01-01T15:00:00.000+03:00" + * DateTime.formatIsoOffset(zoned) // => "2024-01-01T15:00:00.000+03:00" * ``` * * @category formatting @@ -2780,22 +2734,20 @@ export const formatIsoOffset: (self: DateTime) => string = Internal.formatIsoOff * * **Example** (Formatting zoned DateTime values) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * * const zoned = DateTime.makeZonedUnsafe("2024-06-15T14:30:45.123Z", { * timeZone: "Europe/London" * }) * - * const formatted = DateTime.formatIsoZoned(zoned) - * console.log(formatted) // "2024-06-15T15:30:45.123+01:00[Europe/London]" + * DateTime.formatIsoZoned(zoned) // => "2024-06-15T15:30:45.123+01:00[Europe/London]" * * const offsetZone = DateTime.makeZonedUnsafe("2024-06-15T14:30:45.123Z", { * timeZone: DateTime.zoneMakeOffset(3 * 60 * 60 * 1000) * }) * - * const offsetFormatted = DateTime.formatIsoZoned(offsetZone) - * console.log(offsetFormatted) // "2024-06-15T17:30:45.123+03:00" + * DateTime.formatIsoZoned(offsetZone) // => "2024-06-15T17:30:45.123+03:00" * ``` * * @category formatting @@ -2812,7 +2764,7 @@ export const formatIsoZoned: (self: Zoned) => string = Internal.formatIsoZoned * * **Example** (Providing current time zone layers) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * * const zone = DateTime.zoneMakeNamedUnsafe("Europe/London") @@ -2820,14 +2772,14 @@ export const formatIsoZoned: (self: Zoned) => string = Internal.formatIsoZoned * * const program = Effect.gen(function*() { * const now = yield* DateTime.nowInCurrentZone - * return DateTime.formatIsoZoned(now) + * return DateTime.zoneToString(now.zone) * }) * * // Use the layer to provide the time zone - * Effect.provide(program, layer) + * await Effect.runPromise(Effect.provide(program, layer)) // => "Europe/London" * ``` * - * @category current time zone + * @category layers * @since 3.6.0 */ export const layerCurrentZone: (resource: NoInfer) => Layer.Layer = Layer.succeed( @@ -2843,7 +2795,7 @@ export const layerCurrentZone: (resource: NoInfer) => Layer.Layer) => Layer.Layer "+03:00" * ``` * - * @category current time zone + * @category layers * @since 3.6.0 */ export const layerCurrentZoneOffset = (offset: number): Layer.Layer => @@ -2873,20 +2825,20 @@ export const layerCurrentZoneOffset = (offset: number): Layer.Layer "Europe/London" * ``` * - * @category current time zone + * @category layers * @since 3.6.0 */ export const layerCurrentZoneNamed: (zoneId: string) => Layer.Layer< @@ -2904,19 +2856,19 @@ export const layerCurrentZoneNamed: (zoneId: string) => Layer.Layer< * * **Example** (Providing local time zone layers) * - * ```ts + * ```ts import.meta.vitest * import { DateTime, Effect } from "effect" * * const program = Effect.gen(function*() { * const now = yield* DateTime.nowInCurrentZone - * return DateTime.formatIsoZoned(now) + * return DateTime.isZoned(now) * }) * * // Use the system's local time zone - * Effect.provide(program, DateTime.layerCurrentZoneLocal) + * await Effect.runPromise(Effect.provide(program, DateTime.layerCurrentZoneLocal)) // => true * ``` * - * @category current time zone + * @category layers * @since 3.6.0 */ export const layerCurrentZoneLocal: Layer.Layer = Layer.sync(CurrentTimeZone)(zoneMakeLocal) diff --git a/.context/effect/packages/effect/src/Deferred.ts b/.context/effect/packages/effect/src/Deferred.ts index a2f626078..d7685cc9e 100644 --- a/.context/effect/packages/effect/src/Deferred.ts +++ b/.context/effect/packages/effect/src/Deferred.ts @@ -33,36 +33,23 @@ const TypeId = "~effect/Deferred" * * **Example** (Creating a Deferred for inter-fiber communication) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect, Fiber } from "effect" * - * // Create and use a Deferred for inter-fiber communication * const program = Effect.gen(function*() { - * // Create a Deferred that will hold a string value * const deferred: Deferred.Deferred = yield* Deferred.make() - * - * // Fork a fiber that will set the deferred value * const producer = yield* Effect.forkChild( * Effect.gen(function*() { - * yield* Effect.sleep("100 millis") * yield* Deferred.succeed(deferred, "Hello, World!") * }) * ) * - * // Fork a fiber that will await the deferred value - * const consumer = yield* Effect.forkChild( - * Effect.gen(function*() { - * const value = yield* Deferred.await(deferred) - * console.log("Received:", value) - * return value - * }) - * ) - * - * // Wait for both fibers to complete + * const consumer = yield* Effect.forkChild(Deferred.await(deferred)) * yield* Fiber.join(producer) - * const result = yield* Fiber.join(consumer) - * return result + * return yield* Fiber.join(consumer) * }) + * + * await Effect.runPromise(program) // => "Hello, World!" * ``` * * @category models @@ -140,11 +127,11 @@ const DeferredProto = { * * **Example** (Creating a Deferred unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Deferred } from "effect" * * const deferred = Deferred.makeUnsafe() - * console.log(deferred) + * Deferred.isDoneUnsafe(deferred) // => false * ``` * * @category unsafe @@ -166,15 +153,16 @@ export const makeUnsafe = (): Deferred => { * * **Example** (Creating a Deferred) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * yield* Deferred.succeed(deferred, 42) - * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return yield* Deferred.await(deferred) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category constructors @@ -188,8 +176,12 @@ const _await = (self: Deferred): Effect => self.resumes ??= [] self.resumes.push(resume) return internalEffect.sync(() => { - const index = self.resumes!.indexOf(resume) - self.resumes!.splice(index, 1) + // Completion resumes all waiters and clears `resumes`, so a cleanup + // running after completion has nothing to unregister. + const resumes = self.resumes + if (resumes === undefined) return + const index = resumes.indexOf(resume) + if (index >= 0) resumes.splice(index, 1) }) }) @@ -209,16 +201,17 @@ export { * * **Example** (Awaiting a Deferred value) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * yield* Deferred.succeed(deferred, 42) * - * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return yield* Deferred.await(deferred) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @see {@link complete} for completing from an effect and memoizing its result @@ -246,17 +239,17 @@ export { * * **Example** (Completing a Deferred from an effect) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const completed = yield* Deferred.complete(deferred, Effect.succeed(42)) - * console.log(completed) // true - * * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return [completed, value] * }) + * + * await Effect.runPromise(program) // => [true, 42] * ``` * * @see {@link completeWith} for storing an effect directly without memoizing its result @@ -293,17 +286,17 @@ export const complete: { * * **Example** (Completing a Deferred with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const completed = yield* Deferred.completeWith(deferred, Effect.succeed(42)) - * console.log(completed) // true - * * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return [completed, value] * }) + * + * await Effect.runPromise(program) // => [true, 42] * ``` * * @see {@link complete} for running an effect once and sharing its result @@ -336,16 +329,16 @@ export const completeWith: { * * **Example** (Completing a Deferred with an Exit) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * yield* Deferred.done(deferred, Exit.succeed(42)) - * - * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return yield* Effect.exit(Deferred.await(deferred)) * }) + * + * await Effect.runPromise(program) // => Exit.succeed(42) * ``` * * @see {@link complete} for completing from an effect and memoizing its result @@ -376,14 +369,17 @@ export const done: { * * **Example** (Failing a Deferred with an error) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const success = yield* Deferred.fail(deferred, "Operation failed") - * console.log(success) // true + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.fail("Operation failed")] * ``` * * @category completion @@ -411,14 +407,17 @@ export const fail: { * * **Example** (Failing a Deferred with a lazy error) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const success = yield* Deferred.failSync(deferred, () => "Lazy error") - * console.log(success) // true + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.fail("Lazy error")] * ``` * * @category completion @@ -448,17 +447,17 @@ export const failSync: { * * **Example** (Failing a Deferred with a Cause) * - * ```ts - * import { Cause, Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() - * const success = yield* Deferred.failCause( - * deferred, - * Cause.fail("Operation failed") - * ) - * console.log(success) // true + * const success = yield* Deferred.failCause(deferred, Cause.fail("Operation failed")) + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.failCause(Cause.fail("Operation failed"))] * ``` * * @category completion @@ -489,17 +488,17 @@ export const failCause: { * * **Example** (Failing a Deferred with a lazy Cause) * - * ```ts - * import { Cause, Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() - * const success = yield* Deferred.failCauseSync( - * deferred, - * () => Cause.fail("Lazy error") - * ) - * console.log(success) // true + * const success = yield* Deferred.failCauseSync(deferred, () => Cause.fail("Lazy error")) + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.failCause(Cause.fail("Lazy error"))] * ``` * * @category completion @@ -529,17 +528,18 @@ export const failCauseSync: { * * **Example** (Killing a Deferred with a defect) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * + * const defect = new Error("Something went wrong") * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() - * const success = yield* Deferred.die( - * deferred, - * new Error("Something went wrong") - * ) - * console.log(success) // true + * const success = yield* Deferred.die(deferred, defect) + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.die(defect)] * ``` * * @category completion @@ -566,17 +566,18 @@ export const die: { * * **Example** (Killing a Deferred with a lazy defect) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * + * const defect = new Error("Lazy error") * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() - * const success = yield* Deferred.dieSync( - * deferred, - * () => new Error("Lazy error") - * ) - * console.log(success) // true + * const success = yield* Deferred.dieSync(deferred, () => defect) + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.die(defect)] * ``` * * @category completion @@ -607,14 +608,19 @@ export const dieSync: { * * **Example** (Interrupting a Deferred) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const success = yield* Deferred.interrupt(deferred) - * console.log(success) // true + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] as const * }) + * + * const [success, exit] = await Effect.runPromise(program) + * success // => true + * Exit.hasInterrupts(exit) // => true * ``` * * @category completion @@ -639,14 +645,17 @@ export const interrupt = (self: Deferred): Effect => * * **Example** (Interrupting a Deferred with a fiber id) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Exit } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const success = yield* Deferred.interruptWith(deferred, 42) - * console.log(success) // true + * const exit = yield* Effect.exit(Deferred.await(deferred)) + * return [success, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.interrupt(42)] * ``` * * @category completion @@ -671,21 +680,21 @@ export const interruptWith: { * * **Example** (Checking Deferred completion) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const beforeCompletion = yield* Deferred.isDone(deferred) - * console.log(beforeCompletion) // false - * * yield* Deferred.succeed(deferred, 42) * const afterCompletion = yield* Deferred.isDone(deferred) - * console.log(afterCompletion) // true + * return [beforeCompletion, afterCompletion] * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isDone = (self: Deferred): Effect => internalEffect.sync(() => isDoneUnsafe(self)) @@ -701,7 +710,7 @@ export const isDone = (self: Deferred): Effect => internalE * @see {@link isDone} for checking completion inside `Effect` * @see {@link poll} for reading the completed effect when available * - * @category getters + * @category predicates * @since 4.0.0 */ export const isDoneUnsafe = (self: Deferred): boolean => self.effect !== undefined @@ -718,18 +727,19 @@ export const isDoneUnsafe = (self: Deferred): boolean => self.effect * * **Example** (Polling Deferred completion) * - * ```ts - * import { Deferred, Effect } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Option } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * const beforeCompletion = yield* Deferred.poll(deferred) - * console.log(beforeCompletion._tag === "None") // true - * * yield* Deferred.succeed(deferred, 42) * const afterCompletion = yield* Deferred.poll(deferred) - * console.log(afterCompletion._tag === "Some") // true + * const afterValue = yield* Effect.transposeOption(afterCompletion) + * return [beforeCompletion, afterValue] * }) + * + * await Effect.runPromise(program) // => [Option.none(), Option.some(42)] * ``` * * @category getters @@ -754,16 +764,17 @@ export function poll(self: Deferred): Effect() * yield* Deferred.succeed(deferred, 42) * - * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return yield* Deferred.await(deferred) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category completion @@ -791,16 +802,16 @@ export const succeed: { * * **Example** (Completing a Deferred with a lazy value) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* Deferred.make() * yield* Deferred.sync(deferred, () => 42) - * - * const value = yield* Deferred.await(deferred) - * console.log(value) // 42 + * return yield* Deferred.await(deferred) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category completion @@ -832,12 +843,11 @@ export const sync: { * * **Example** (Completing a Deferred unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect } from "effect" * * const deferred = Deferred.makeUnsafe() - * const success = Deferred.doneUnsafe(deferred, Effect.succeed(42)) - * console.log(success) // true + * Deferred.doneUnsafe(deferred, Effect.succeed(42)) // => true * ``` * * @category unsafe @@ -873,33 +883,22 @@ export const doneUnsafe = (self: Deferred, effect: Effect): bo * * **Example** (Completing a Deferred from an effect result) * - * ```ts + * ```ts import.meta.vitest * 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* Deferred.into(successEffect, deferred) - * - * // Access the value of the deferred * const value = yield* Deferred.await(deferred) - * console.log(value) - * - * return isCompleted + * return [isCompleted, value] * }) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // 42 - * // true + * await Effect.runPromise(program) // => [true, 42] * ``` * - * @category Synchronization Utilities + * @category completion * @since 4.0.0 */ export const into: { diff --git a/.context/effect/packages/effect/src/Duration.ts b/.context/effect/packages/effect/src/Duration.ts index d2fe8eb41..4f4e7fa1e 100644 --- a/.context/effect/packages/effect/src/Duration.ts +++ b/.context/effect/packages/effect/src/Duration.ts @@ -27,6 +27,8 @@ const TypeId = "~effect/time/Duration" const bigint0 = BigInt(0) const bigint1 = BigInt(1) +const bigint2 = BigInt(2) +const bigint10 = BigInt(10) const bigint24 = BigInt(24) const bigint60 = BigInt(60) const bigint1e3 = BigInt(1_000) @@ -38,8 +40,20 @@ const roundTiesAwayFromZero = (input: number): bigint => const roundMillisToNanos = (millis: number): bigint => roundTiesAwayFromZero(millis * 1_000_000) -const parseNanos = (input: string, scale: bigint): bigint => - input.includes(".") ? roundTiesAwayFromZero(Number(input) * Number(scale)) : BigInt(input) * scale +const parseNanos = (input: string, scale: bigint): bigint => { + const decimalIndex = input.indexOf(".") + if (decimalIndex === -1) return BigInt(input) * scale + + const isNegative = input[0] === "-" + const fractional = input.slice(decimalIndex + 1) + const fractionalScale = bigint10 ** BigInt(fractional.length) + const scaled = ( + BigInt(input.slice(isNegative ? 1 : 0, decimalIndex)) * fractionalScale + BigInt(fractional) + ) * scale + const rounded = scaled / fractionalScale + + (scaled % fractionalScale * bigint2 >= fractionalScale ? bigint1 : bigint0) + return isNegative ? -rounded : rounded +} const nanosToHrTime = (nanos: bigint): [seconds: number, nanos: number] => { const sign = nanos < bigint0 ? -bigint1 : bigint1 @@ -175,12 +189,12 @@ export type Input = * * **Example** (Combining duration object fields) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * Duration.fromInputUnsafe({ seconds: 30 }) - * Duration.fromInputUnsafe({ days: 1 }) - * Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 }) + * Duration.fromInputUnsafe({ seconds: 30 }) // => Duration.seconds(30) + * Duration.fromInputUnsafe({ days: 1 }) // => Duration.days(1) + * Duration.fromInputUnsafe({ seconds: 1, nanoseconds: 500 }) // => Duration.nanos(1_000_000_500n) * ``` * * @category models @@ -213,13 +227,13 @@ const DURATION_REGEXP = /^(-?\d+(?:\.\d+)?)\s+(nanos?|micros?|millis?|seconds?|m * * **Example** (Decoding duration inputs) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration1 = Duration.fromInputUnsafe(1000) // 1000 milliseconds - * const duration2 = Duration.fromInputUnsafe("5 seconds") - * const duration3 = Duration.fromInputUnsafe("Infinity") - * const duration4 = Duration.fromInputUnsafe([2, 500_000_000]) // 2 seconds and 500ms + * Duration.fromInputUnsafe(1000) // => Duration.millis(1000) + * Duration.fromInputUnsafe("5 seconds") // => Duration.seconds(5) + * Duration.fromInputUnsafe("Infinity") // => Duration.infinity + * Duration.fromInputUnsafe([2, 500_000_000]) // => Duration.nanos(2_500_000_000n) * ``` * * @category constructors @@ -316,12 +330,11 @@ const invalid = (input: unknown): never => { * * **Example** (Safely decoding duration inputs) * - * ```ts + * ```ts import.meta.vitest * import { Duration, Option } from "effect" * - * Duration.fromInput(1000).pipe(Option.map(Duration.toSeconds)) // Some(1) - * - * Duration.fromInput("invalid" as any) // None + * Duration.fromInput(1000) // => Option.some(Duration.seconds(1)) + * Duration.fromInput("invalid" as any) // => Option.none() * ``` * * @category constructors @@ -338,7 +351,18 @@ const negativeInfinityDurationValue: DurationValue = { _tag: "NegativeInfinity" const DurationProto: Omit = { [TypeId]: TypeId, [Hash.symbol](this: Duration) { - return Hash.structure(this.value) + // Hash equal finite durations using the same canonical nanoseconds + // representation used by `equals`. + switch (this.value._tag) { + case "Millis": { + const nanos = this.value.millis * 1_000_000 + return Number.isFinite(nanos) ? Hash.hash(roundTiesAwayFromZero(nanos)) : Hash.number(this.value.millis) + } + case "Nanos": + return Hash.hash(this.value.nanos) + default: + return Hash.structure(this.value) + } }, [Equal.symbol](this: Duration, that: unknown): boolean { return isDuration(that) && equals(this, that) @@ -400,11 +424,11 @@ const make = (input: number | bigint): Duration => { * * **Example** (Checking for durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.isDuration(Duration.seconds(1))) // true - * console.log(Duration.isDuration(1000)) // false + * Duration.isDuration(Duration.seconds(1)) // => true + * Duration.isDuration(1000) // => false * ``` * * @category guards @@ -417,14 +441,14 @@ export const isDuration = (u: unknown): u is Duration => hasProperty(u, TypeId) * * **Example** (Checking finite durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.isFinite(Duration.seconds(5))) // true - * console.log(Duration.isFinite(Duration.infinity)) // false + * Duration.isFinite(Duration.seconds(5)) // => true + * Duration.isFinite(Duration.infinity) // => false * ``` * - * @category guards + * @category predicates * @since 2.0.0 */ export const isFinite = (self: Duration): boolean => @@ -435,14 +459,14 @@ export const isFinite = (self: Duration): boolean => * * **Example** (Checking for zero durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.isZero(Duration.zero)) // true - * console.log(Duration.isZero(Duration.seconds(1))) // false + * Duration.isZero(Duration.zero) // => true + * Duration.isZero(Duration.seconds(1)) // => false * ``` * - * @category guards + * @category predicates * @since 3.5.0 */ export const isZero = (self: Duration): boolean => { @@ -462,15 +486,15 @@ export const isZero = (self: Duration): boolean => { * * **Example** (Checking for negative durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.isNegative(Duration.seconds(-5))) // true - * console.log(Duration.isNegative(Duration.zero)) // false - * console.log(Duration.isNegative(Duration.negativeInfinity)) // true + * Duration.isNegative(Duration.seconds(-5)) // => true + * Duration.isNegative(Duration.zero) // => false + * Duration.isNegative(Duration.negativeInfinity) // => true * ``` * - * @category guards + * @category predicates * @since 4.0.0 */ export const isNegative = (self: Duration): boolean => { @@ -491,15 +515,15 @@ export const isNegative = (self: Duration): boolean => { * * **Example** (Checking for positive durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.isPositive(Duration.seconds(5))) // true - * console.log(Duration.isPositive(Duration.zero)) // false - * console.log(Duration.isPositive(Duration.infinity)) // true + * Duration.isPositive(Duration.seconds(5)) // => true + * Duration.isPositive(Duration.zero) // => false + * Duration.isPositive(Duration.infinity) // => true * ``` * - * @category guards + * @category predicates * @since 4.0.0 */ export const isPositive = (self: Duration): boolean => { @@ -520,11 +544,11 @@ export const isPositive = (self: Duration): boolean => { * * **Example** (Taking absolute duration values) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * Duration.toMillis(Duration.abs(Duration.seconds(-5))) // 5000 - * Duration.abs(Duration.negativeInfinity) === Duration.infinity // true + * Duration.abs(Duration.seconds(-5)) // => Duration.seconds(5) + * Duration.abs(Duration.negativeInfinity) // => Duration.infinity * ``` * * @category math @@ -547,11 +571,11 @@ export const abs = (self: Duration): Duration => { * * **Example** (Negating durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * Duration.toMillis(Duration.negate(Duration.seconds(5))) // -5000 - * Duration.negate(Duration.infinity) === Duration.negativeInfinity // true + * Duration.negate(Duration.seconds(5)) // => Duration.seconds(-5) + * Duration.negate(Duration.infinity) // => Duration.negativeInfinity * ``` * * @category math @@ -575,10 +599,10 @@ export const negate = (self: Duration): Duration => { * * **Example** (Referencing the zero duration) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toMillis(Duration.zero)) // 0 + * Duration.toMillis(Duration.zero) // => 0 * ``` * * @category constructors @@ -591,10 +615,10 @@ export const zero: Duration = make(0) * * **Example** (Referencing infinite duration) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toMillis(Duration.infinity)) // Infinity + * Duration.toMillis(Duration.infinity) // => Infinity * ``` * * @category constructors @@ -607,10 +631,10 @@ export const infinity: Duration = make(Infinity) * * **Example** (Referencing negative infinite duration) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toMillis(Duration.negativeInfinity)) // -Infinity + * Duration.toMillis(Duration.negativeInfinity) // => -Infinity * ``` * * @category constructors @@ -623,11 +647,10 @@ export const negativeInfinity: Duration = make(-Infinity) * * **Example** (Creating durations from nanoseconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.nanos(BigInt(500_000_000)) - * console.log(Duration.toMillis(duration)) // 500 + * Duration.nanos(500_000_000n) // => Duration.nanos(500_000_000n) * ``` * * @category constructors @@ -640,11 +663,10 @@ export const nanos = (nanos: bigint): Duration => make(nanos) * * **Example** (Creating durations from microseconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.micros(BigInt(500_000)) - * console.log(Duration.toMillis(duration)) // 500 + * Duration.micros(500_000n) // => Duration.nanos(500_000_000n) * ``` * * @category constructors @@ -657,11 +679,10 @@ export const micros = (micros: bigint): Duration => make(micros * bigint1e3) * * **Example** (Creating durations from milliseconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.millis(1000) - * console.log(Duration.toMillis(duration)) // 1000 + * Duration.toMillis(Duration.millis(1000)) // => 1000 * ``` * * @category constructors @@ -674,11 +695,10 @@ export const millis = (millis: number): Duration => make(millis) * * **Example** (Creating durations from seconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.seconds(30) - * console.log(Duration.toMillis(duration)) // 30000 + * Duration.toMillis(Duration.seconds(30)) // => 30_000 * ``` * * @category constructors @@ -691,11 +711,10 @@ export const seconds = (seconds: number): Duration => make(seconds * 1000) * * **Example** (Creating durations from minutes) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.minutes(5) - * console.log(Duration.toMillis(duration)) // 300000 + * Duration.toMillis(Duration.minutes(5)) // => 300_000 * ``` * * @category constructors @@ -708,11 +727,10 @@ export const minutes = (minutes: number): Duration => make(minutes * 60_000) * * **Example** (Creating durations from hours) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.hours(2) - * console.log(Duration.toMillis(duration)) // 7200000 + * Duration.toMillis(Duration.hours(2)) // => 7_200_000 * ``` * * @category constructors @@ -725,11 +743,10 @@ export const hours = (hours: number): Duration => make(hours * 3_600_000) * * **Example** (Creating durations from days) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.days(1) - * console.log(Duration.toMillis(duration)) // 86400000 + * Duration.toMillis(Duration.days(1)) // => 86_400_000 * ``` * * @category constructors @@ -742,11 +759,10 @@ export const days = (days: number): Duration => make(days * 86_400_000) * * **Example** (Creating durations from weeks) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.weeks(1) - * console.log(Duration.toMillis(duration)) // 604800000 + * Duration.toMillis(Duration.weeks(1)) // => 604_800_000 * ``` * * @category constructors @@ -759,11 +775,11 @@ export const weeks = (weeks: number): Duration => make(weeks * 604_800_000) * * **Example** (Converting durations to milliseconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toMillis(Duration.seconds(5))) // 5000 - * console.log(Duration.toMillis(Duration.minutes(2))) // 120000 + * Duration.toMillis(Duration.seconds(5)) // => 5000 + * Duration.toMillis(Duration.minutes(2)) // => 120_000 * ``` * * @category getters @@ -782,11 +798,11 @@ export const toMillis = (self: Input): number => * * **Example** (Converting durations to seconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toSeconds(Duration.millis(5000))) // 5 - * console.log(Duration.toSeconds(Duration.minutes(2))) // 120 + * Duration.toSeconds(Duration.millis(5000)) // => 5 + * Duration.toSeconds(Duration.minutes(2)) // => 120 * ``` * * @category getters @@ -805,11 +821,11 @@ export const toSeconds = (self: Input): number => * * **Example** (Converting durations to minutes) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toMinutes(Duration.seconds(120))) // 2 - * console.log(Duration.toMinutes(Duration.hours(1))) // 60 + * Duration.toMinutes(Duration.seconds(120)) // => 2 + * Duration.toMinutes(Duration.hours(1)) // => 60 * ``` * * @category getters @@ -828,11 +844,11 @@ export const toMinutes = (self: Input): number => * * **Example** (Converting durations to hours) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toHours(Duration.minutes(120))) // 2 - * console.log(Duration.toHours(Duration.days(1))) // 24 + * Duration.toHours(Duration.minutes(120)) // => 2 + * Duration.toHours(Duration.days(1)) // => 24 * ``` * * @category getters @@ -851,11 +867,11 @@ export const toHours = (self: Input): number => * * **Example** (Converting durations to days) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toDays(Duration.hours(48))) // 2 - * console.log(Duration.toDays(Duration.weeks(1))) // 7 + * Duration.toDays(Duration.hours(48)) // => 2 + * Duration.toDays(Duration.weeks(1)) // => 7 * ``` * * @category getters @@ -874,11 +890,11 @@ export const toDays = (self: Input): number => * * **Example** (Converting durations to weeks) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * console.log(Duration.toWeeks(Duration.days(14))) // 2 - * console.log(Duration.toWeeks(Duration.days(7))) // 1 + * Duration.toWeeks(Duration.days(14)) // => 2 + * Duration.toWeeks(Duration.days(7)) // => 1 * ``` * * @category getters @@ -911,12 +927,10 @@ export const toWeeks = (self: Input): number => * * **Example** (Reading nanoseconds unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.seconds(2) - * const nanos = Duration.toNanosUnsafe(duration) - * console.log(nanos) // 2000000000n + * Duration.toNanosUnsafe(Duration.seconds(2)) // => 2_000_000_000n * * // Duration.toNanosUnsafe(Duration.infinity) * // throws Error: "Cannot convert infinite duration to nanos" @@ -947,13 +961,11 @@ export const toNanosUnsafe = (input: Input): bigint => { * * **Example** (Safely reading nanoseconds) * - * ```ts + * ```ts import.meta.vitest * import { Duration, Option } from "effect" * - * Duration.toNanos(Duration.seconds(1)) // Some(1000000000n) - * - * Duration.toNanos(Duration.infinity) // None - * Option.getOrUndefined(Duration.toNanos(Duration.infinity)) // undefined + * Duration.toNanos(Duration.seconds(1)) // => Option.some(1_000_000_000n) + * Duration.toNanos(Duration.infinity) // => Option.none() * ``` * * @category getters @@ -966,12 +978,10 @@ export const toNanos: (self: Input) => Option.Option = Option.liftThrowa * * **Example** (Converting durations to high-resolution time) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const duration = Duration.millis(1500) - * const hrtime = Duration.toHrTime(duration) - * console.log(hrtime) // [1, 500000000] + * Duration.toHrTime(Duration.millis(1500)) // => [1, 500_000_000] * ``` * * @category getters @@ -1002,15 +1012,14 @@ export const toHrTime = (input: Input): [seconds: number, nanos: number] => { * * **Example** (Pattern matching on duration representations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const result = Duration.match(Duration.seconds(5), { + * Duration.match(Duration.seconds(5), { * onMillis: (millis) => `${millis} milliseconds`, * onNanos: (nanos) => `${nanos} nanoseconds`, * onInfinity: () => "infinite" - * }) - * console.log(result) // "5000 milliseconds" + * }) // => "5000 milliseconds" * ``` * * @category pattern matching @@ -1060,15 +1069,14 @@ export const match: { * * **Example** (Pattern matching on duration pairs) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const sum = Duration.matchPair(Duration.seconds(3), Duration.seconds(2), { + * Duration.matchPair(Duration.seconds(3), Duration.seconds(2), { * onMillis: (a, b) => a + b, * onNanos: (a, b) => Number(a + b), * onInfinity: () => Infinity - * }) - * console.log(sum) // 5000 + * }) // => 5000 * ``` * * @category pattern matching @@ -1123,7 +1131,7 @@ export const matchPair: { * * **Example** (Sorting durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * * const durations = [ @@ -1131,8 +1139,7 @@ export const matchPair: { * Duration.seconds(1), * Duration.seconds(2) * ] - * const sorted = durations.sort((a, b) => Duration.Order(a, b)) - * console.log(sorted.map(Duration.toSeconds)) // [1, 2, 3] + * durations.sort((a, b) => Duration.Order(a, b)).map(Duration.toSeconds) // => [1, 2, 3] * ``` * * @category instances @@ -1173,14 +1180,13 @@ export const Order: order.Order = order.make((self, that) => * * **Example** (Checking duration ranges) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isInRange = Duration.between(Duration.seconds(3), { + * Duration.between(Duration.seconds(3), { * minimum: Duration.seconds(2), * maximum: Duration.seconds(5) - * }) - * console.log(isInRange) // true + * }) // => true * ``` * * @see {@link clamp} for constraining a duration to a range @@ -1200,11 +1206,10 @@ export const between: { * * **Example** (Comparing durations for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isEqual = Duration.Equivalence(Duration.seconds(5), Duration.millis(5000)) - * console.log(isEqual) // true + * Duration.Equivalence(Duration.seconds(5), Duration.millis(5000)) // => true * ``` * * @category instances @@ -1222,11 +1227,10 @@ export const Equivalence: Equ.Equivalence = (self, that) => * * **Example** (Selecting the shorter duration) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const shorter = Duration.min(Duration.seconds(5), Duration.seconds(3)) - * console.log(Duration.toSeconds(shorter)) // 3 + * Duration.min(Duration.seconds(5), Duration.seconds(3)) // => Duration.seconds(3) * ``` * * @category ordering @@ -1242,11 +1246,10 @@ export const min: { * * **Example** (Selecting the longer duration) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const longer = Duration.max(Duration.seconds(5), Duration.seconds(3)) - * console.log(Duration.toSeconds(longer)) // 5 + * Duration.max(Duration.seconds(5), Duration.seconds(3)) // => Duration.seconds(5) * ``` * * @category ordering @@ -1262,14 +1265,13 @@ export const max: { * * **Example** (Clamping durations to a range) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const clamped = Duration.clamp(Duration.seconds(10), { + * Duration.clamp(Duration.seconds(10), { * minimum: Duration.seconds(2), * maximum: Duration.seconds(5) - * }) - * console.log(Duration.toSeconds(clamped)) // 5 + * }) // => Duration.seconds(5) * ``` * * @category ordering @@ -1291,13 +1293,11 @@ export const clamp: { * * **Example** (Safely dividing durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration, Option } from "effect" * - * const d = Duration.divide(Duration.seconds(10), 2) - * console.log(Option.map(d, Duration.toSeconds)) // Some(5) - * - * Duration.divide(Duration.seconds(10), 0) // None + * Duration.divide(Duration.seconds(10), 2) // => Option.some(Duration.seconds(5)) + * Duration.divide(Duration.seconds(10), 0) // => Option.none() * ``` * * @category math @@ -1345,14 +1345,11 @@ export const divide: { * * **Example** (Dividing durations unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const half = Duration.divideUnsafe(Duration.seconds(10), 2) - * console.log(Duration.toSeconds(half)) // 5 - * - * const infinite = Duration.divideUnsafe(Duration.seconds(10), 0) - * console.log(Duration.toMillis(infinite)) // Infinity + * Duration.divideUnsafe(Duration.seconds(10), 2) // => Duration.seconds(5) + * Duration.divideUnsafe(Duration.seconds(10), 0) // => Duration.infinity * ``` * * @category math @@ -1399,11 +1396,10 @@ export const divideUnsafe: { * * **Example** (Multiplying durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const doubled = Duration.times(Duration.seconds(5), 2) - * console.log(Duration.toSeconds(doubled)) // 10 + * Duration.times(Duration.seconds(5), 2) // => Duration.seconds(10) * ``` * * @category math @@ -1437,11 +1433,10 @@ export const times: { * * **Example** (Subtracting durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const result = Duration.subtract(Duration.seconds(10), Duration.seconds(3)) - * console.log(Duration.toSeconds(result)) // 7 + * Duration.subtract(Duration.seconds(10), Duration.seconds(3)) // => Duration.seconds(7) * ``` * * @category math @@ -1481,11 +1476,10 @@ export const subtract: { * * **Example** (Adding durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const total = Duration.sum(Duration.seconds(5), Duration.seconds(3)) - * console.log(Duration.toSeconds(total)) // 8 + * Duration.sum(Duration.seconds(5), Duration.seconds(3)) // => Duration.seconds(8) * ``` * * @category math @@ -1518,11 +1512,10 @@ export const sum: { * * **Example** (Comparing durations with less than) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isLess = Duration.isLessThan(Duration.seconds(3), Duration.seconds(5)) - * console.log(isLess) // true + * Duration.isLessThan(Duration.seconds(3), Duration.seconds(5)) // => true * ``` * * @category predicates @@ -1538,14 +1531,13 @@ export const isLessThan: { * * **Example** (Comparing durations with less than or equal) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isLessOrEqual = Duration.isLessThanOrEqualTo( + * Duration.isLessThanOrEqualTo( * Duration.seconds(5), * Duration.seconds(5) - * ) - * console.log(isLessOrEqual) // true + * ) // => true * ``` * * @category predicates @@ -1561,11 +1553,10 @@ export const isLessThanOrEqualTo: { * * **Example** (Comparing durations with greater than) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isGreater = Duration.isGreaterThan(Duration.seconds(5), Duration.seconds(3)) - * console.log(isGreater) // true + * Duration.isGreaterThan(Duration.seconds(5), Duration.seconds(3)) // => true * ``` * * @category predicates @@ -1581,14 +1572,13 @@ export const isGreaterThan: { * * **Example** (Comparing durations with greater than or equal) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isGreaterOrEqual = Duration.isGreaterThanOrEqualTo( + * Duration.isGreaterThanOrEqualTo( * Duration.seconds(5), * Duration.seconds(5) - * ) - * console.log(isGreaterOrEqual) // true + * ) // => true * ``` * * @category predicates @@ -1604,11 +1594,10 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking duration equality) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * const isEqual = Duration.equals(Duration.seconds(5), Duration.millis(5000)) - * console.log(isEqual) // true + * Duration.equals(Duration.seconds(5), Duration.millis(5000)) // => true * ``` * * @category predicates @@ -1630,7 +1619,7 @@ export const equals: { * * **Example** (Decomposing durations into parts) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * * // Create a complex duration by adding multiple parts @@ -1641,28 +1630,10 @@ export const equals: { * ), * Duration.millis(123) * ) - * const components = Duration.parts(duration) - * console.log(components) - * // { - * // days: 1, - * // hours: 2, - * // minutes: 30, - * // seconds: 45, - * // millis: 123, - * // nanos: 0 - * // } + * Duration.parts(duration) // => ({ days: 1, hours: 2, minutes: 30, seconds: 45, millis: 123, nanos: 0 }) * * const complex = Duration.sum(Duration.hours(25), Duration.minutes(90)) - * const complexParts = Duration.parts(complex) - * console.log(complexParts) - * // { - * // days: 1, - * // hours: 2, - * // minutes: 30, - * // seconds: 0, - * // millis: 0, - * // nanos: 0 - * // } + * Duration.parts(complex) // => ({ days: 1, hours: 2, minutes: 30, seconds: 0, millis: 0, nanos: 0 }) * ``` * * @category converting @@ -1722,11 +1693,11 @@ export const parts = (self: Duration): { * * **Example** (Formatting durations) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * - * Duration.format(Duration.millis(1000)) // "1s" - * Duration.format(Duration.millis(1001)) // "1s 1ms" + * Duration.format(Duration.millis(1000)) // => "1s" + * Duration.format(Duration.millis(1001)) // => "1s 1ms" * ``` * * @category converting diff --git a/.context/effect/packages/effect/src/Effect.ts b/.context/effect/packages/effect/src/Effect.ts index f283da12e..6cb027671 100644 --- a/.context/effect/packages/effect/src/Effect.ts +++ b/.context/effect/packages/effect/src/Effect.ts @@ -15,7 +15,7 @@ import type * as Cause from "./Cause.ts" import type { Clock } from "./Clock.ts" import * as Context from "./Context.ts" import * as Duration from "./Duration.ts" -import type { ExecutionPlan } from "./ExecutionPlan.ts" +import type * as ExecutionPlan from "./ExecutionPlan.ts" import * as Exit from "./Exit.ts" import type { Fiber } from "./Fiber.ts" import type * as Filter from "./Filter.ts" @@ -26,6 +26,7 @@ import * as core from "./internal/core.ts" import * as internal from "./internal/effect.ts" import * as internalExecutionPlan from "./internal/executionPlan.ts" import * as internalLayer from "./internal/layer.ts" +import * as InternalRecord from "./internal/record.ts" import * as internalRequest from "./internal/request.ts" import * as internalSchedule from "./internal/schedule.ts" import type * as Layer from "./Layer.ts" @@ -137,7 +138,7 @@ export interface EffectUnify { /** * Type lambda used to represent `Effect` in higher-kinded APIs. * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface EffectTypeLambda extends TypeLambda { @@ -167,7 +168,7 @@ export interface Variance { * @see {@link Error} for extracting the failure type from the same `Effect` * @see {@link Services} for extracting the required services from the same `Effect` * - * @category models + * @category utility types * @since 2.0.0 */ export type Success = T extends Effect ? _A @@ -188,7 +189,7 @@ export type Success = T extends Effect ? _A * @see {@link Success} for extracting the success value type instead * @see {@link Services} for extracting the required services type instead * - * @category models + * @category utility types * @since 2.0.0 */ export type Error = T extends Effect ? _E @@ -205,7 +206,7 @@ export type Error = T extends Effect ? _E * @see {@link Success} for extracting the success value type instead * @see {@link Error} for extracting the failure type instead * - * @category models + * @category utility types * @since 4.0.0 */ export type Services = T extends Effect ? _R @@ -216,11 +217,11 @@ export type Services = T extends Effect ? _R * * **Example** (Checking whether a value is an Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * console.log(Effect.isEffect(Effect.succeed(1))) // true - * console.log(Effect.isEffect("hello")) // false + * Effect.isEffect(Effect.succeed(1)) // => true + * Effect.isEffect("hello") // => false * ``` * * @category guards @@ -403,112 +404,91 @@ export declare namespace All { * * **Example** (Collecting tuple results in order) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const tupleOfEffects = [ - * Effect.succeed(42).pipe(Effect.tap(Console.log)), - * Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * Effect.succeed(42), + * Effect.succeed("Hello") * ] as const * * // ┌─── Effect<[number, string], never, never> * // ▼ * const resultsAsTuple = Effect.all(tupleOfEffects) * - * Effect.runPromise(resultsAsTuple).then(console.log) - * // Output: - * // 42 - * // Hello - * // [ 42, 'Hello' ] + * await Effect.runPromise(resultsAsTuple) // => [42, "Hello"] * ``` * * **Example** (Collecting iterable results in order) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const iterableOfEffects: Iterable> = [1, 2, 3].map( - * (n) => Effect.succeed(n).pipe(Effect.tap(Console.log)) + * Effect.succeed * ) * * // ┌─── Effect * // ▼ * const resultsAsArray = Effect.all(iterableOfEffects) * - * Effect.runPromise(resultsAsArray).then(console.log) - * // Output: - * // 1 - * // 2 - * // 3 - * // [ 1, 2, 3 ] + * await Effect.runPromise(resultsAsArray) // => [1, 2, 3] * ``` * * **Example** (Collecting struct results by key) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const structOfEffects = { - * a: Effect.succeed(42).pipe(Effect.tap(Console.log)), - * b: Effect.succeed("Hello").pipe(Effect.tap(Console.log)) + * a: Effect.succeed(42), + * b: Effect.succeed("Hello") * } * * // ┌─── 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' } + * await Effect.runPromise(resultsAsStruct) // => { a: 42, b: "Hello" } * ``` * * **Example** (Collecting record results by key) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const recordOfEffects: Record> = { - * key1: Effect.succeed(1).pipe(Effect.tap(Console.log)), - * key2: Effect.succeed(2).pipe(Effect.tap(Console.log)) + * key1: Effect.succeed(1), + * key2: Effect.succeed(2) * } * * // ┌─── Effect<{ [x: string]: number; }, never, never> * // ▼ * const resultsAsRecord = Effect.all(recordOfEffects) * - * Effect.runPromise(resultsAsRecord).then(console.log) - * // Output: - * // 1 - * // 2 - * // { key1: 1, key2: 2 } + * await Effect.runPromise(resultsAsRecord) // => { key1: 1, key2: 2 } * ``` * * **Example** (Stopping on the first failure) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] + * const record = (value: unknown) => Effect.sync(() => { output.push(value) }) * * const program = Effect.all([ - * Effect.succeed("Task1").pipe(Effect.tap(Console.log)), - * Effect.fail("Task2: Oh no!").pipe(Effect.tap(Console.log)), + * Effect.succeed("Task1").pipe(Effect.tap(record)), + * Effect.fail("Task2: Oh no!").pipe(Effect.tap(record)), * // Won't execute due to earlier failure - * Effect.succeed("Task3").pipe(Effect.tap(Console.log)) + * Effect.succeed("Task3").pipe(Effect.tap(record)) * ]) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: - * // Task1 - * // { - * // _id: 'Exit', - * // _tag: 'Failure', - * // cause: { _id: 'Cause', _tag: 'Fail', failure: 'Task2: Oh no!' } - * // } + * const outcome = await Effect.runPromiseExit(program) + * const observation = [output, outcome] // => [["Task1"], Exit.fail("Task2: Oh no!")] * ``` * * @see {@link forEach} for iterating over elements and applying an effect. - * @category collecting + * @category combining * @since 2.0.0 */ export const all: < @@ -539,18 +519,17 @@ export const all: < * * **Example** (Separating successes and failures) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.partition([0, 1, 2, 3], (n) => * n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n) * ) * - * Effect.runPromise(program).then(console.log) - * // [ ["0 is even", "2 is even"], [1, 3] ] + * await Effect.runPromise(program) // => [['0 is even', '2 is even'], [1, 3]] * ``` * - * @category collecting + * @category filtering * @since 2.0.0 */ export const partition: { @@ -582,27 +561,24 @@ export const partition: { * * **Example** (Summing values sequentially) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.reduce( * [1, 2, 3], * () => 0, * (total, value, index) => - * Console.log(`Adding ${value} at index ${index}`).pipe( + * Effect.sync(() => { output.push(`Adding ${value} at index ${index}`) }).pipe( * Effect.as(total + value) * ) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Adding 1 at index 0 - * // Adding 2 at index 1 - * // Adding 3 at index 2 - * // 6 + * void output.push(await Effect.runPromise(program)) + * output // => ["Adding 1 at index 0", "Adding 2 at index 1", "Adding 3 at index 2", 6] * ``` * - * @category collecting + * @category folding * @since 2.0.0 */ export const reduce: { @@ -631,28 +607,17 @@ export const reduce: { * * **Example** (Validating every element) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" * * const program = Effect.validate([0, 1, 2, 3], (n) => * n % 2 === 0 ? Effect.fail(`${n} is even`) : Effect.succeed(n) * ) * - * Effect.runPromiseExit(program).then(console.log) - * // { - * // _id: 'Exit', - * // _tag: 'Failure', - * // cause: { - * // _id: 'Cause', - * // reasons: [ - * // { _id: 'Reason', _tag: 'Fail', error: '0 is even' }, - * // { _id: 'Reason', _tag: 'Fail', error: '2 is even' } - * // ] - * // } - * // } + * await Effect.runPromiseExit(program) // => Exit.fail(["0 is even", "2 is even"]) * ``` * - * @category error accumulation + * @category validation * @since 2.0.0 */ export const validate: { @@ -698,16 +663,15 @@ export const validate: { * * **Example** (Finding the first successful match) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * * const program = Effect.findFirst([1, 2, 3, 4], (n) => Effect.succeed(n > 2)) * - * Effect.runPromise(program).then(console.log) - * // { _id: 'Option', _tag: 'Some', value: 3 } + * await Effect.runPromise(program) // => Option.some(3) * ``` * - * @category collecting + * @category searching * @since 2.0.0 */ export const findFirst: { @@ -735,7 +699,7 @@ export const findFirst: { * * @see {@link findFirst} for the simpler effectful predicate-based variant * - * @category collecting + * @category searching * @since 4.0.0 */ export const findFirstFilter: { @@ -776,50 +740,40 @@ export const findFirstFilter: { * * **Example** (Mapping over an iterable with effects) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const result = Effect.forEach( * [1, 2, 3, 4, 5], * (n, index) => - * Console.log(`Currently at index ${index}`).pipe(Effect.as(n * 2)) + * Effect.sync(() => { output.push(`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 ] + * void output.push(await Effect.runPromise(result)) + * 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** (Running effects without collecting results) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * // 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)), + * Effect.sync(() => { output.push(`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 + * void output.push(await Effect.runPromise(result)) + * 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. - * @category collecting + * @category sequencing * @since 2.0.0 */ export const forEach: { @@ -839,27 +793,23 @@ export const forEach: { * * **Example** (Repeating an effectful loop) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * let counter = 0 * * const program = Effect.whileLoop({ * while: () => counter < 5, * body: () => Effect.sync(() => ++counter), - * step: (n) => console.log(`Current count: ${n}`) + * step: (n) => void output.push(`Current count: ${n}`) * }) * - * Effect.runPromise(program) - * // Output: - * // Current count: 1 - * // Current count: 2 - * // Current count: 3 - * // Current count: 4 - * // Current count: 5 + * await Effect.runPromise(program) + * output // => ["Current count: 1", "Current count: 2", "Current count: 3", "Current count: 4", "Current count: 5"] * ``` * - * @category collecting + * @category repetition * @since 2.0.0 */ export const whileLoop: (options: { @@ -896,22 +846,16 @@ export const whileLoop: (options: { * * **Example** (Wrapping a non-rejecting Promise) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const delay = (message: string) => - * Effect.promise( - * () => - * new Promise((resolve) => { - * setTimeout(() => { - * resolve(message) - * }, 2000) - * }) - * ) + * const succeedAsync = (message: string) => + * Effect.promise(() => Promise.resolve(message)) * * // ┌─── Effect * // ▼ - * const program = delay("Async operation completed successfully!") + * const program = succeedAsync("Async operation completed successfully!") + * await Effect.runPromise(program) // => "Async operation completed successfully!" * ``` * * @see {@link tryPromise} for a version that can handle failures. @@ -955,37 +899,37 @@ export const promise: ( * * **Example** (Wrapping a fetch request that may fail) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const getTodo = (id: number) => - * // Will catch any errors and propagate them as UnknownError - * Effect.tryPromise(() => - * fetch(`https://jsonplaceholder.typicode.com/todos/${id}`) - * ) + * Effect.tryPromise(() => Promise.resolve({ id, completed: false })) * - * // ┌─── Effect + * // ┌─── Effect<{ id: number; completed: boolean }, UnknownError, never> * // ▼ * const program = getTodo(1) + * await Effect.runPromise(program) // => { id: 1, completed: false } * ``` * * **Example** (Mapping Promise rejections to a tagged error) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class TodoFetchError extends Data.TaggedError("TodoFetchError")<{ readonly cause: unknown }> {} * * const getTodo = (id: number) => * Effect.tryPromise({ - * try: () => fetch(`https://jsonplaceholder.typicode.com/todos/${id}`), + * try: () => Promise.reject(`Todo ${id} is unavailable`), * // remap the error * catch: (cause) => new TodoFetchError({ cause }) * }) * - * // ┌─── Effect + * // ┌─── Effect * // ▼ - * const program = getTodo(1) + * const program = Effect.flip(getTodo(1)) + * const error = await Effect.runPromise(program) + * error._tag // => "TodoFetchError" * ``` * * @see {@link promise} if the effectful computation is asynchronous and does not throw errors. @@ -1008,7 +952,7 @@ export const tryPromise: ( * * **Example** (Creating a successful effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // Creating an effect that represents a successful scenario @@ -1016,6 +960,7 @@ export const tryPromise: ( * // ┌─── Effect * // ▼ * const success = Effect.succeed(42) + * Effect.runSync(success) // => 42 * ``` * * @see {@link fail} to create an effect that represents a failure. @@ -1029,13 +974,12 @@ export const succeed: (value: A) => Effect = internal.succeed * * **Example** (Succeeding with Option.none) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * * const program = Effect.succeedNone * - * Effect.runPromise(program).then(console.log) - * // Output: { _id: 'Option', _tag: 'None' } + * Effect.runSync(program) // => Option.none() * ``` * * @category constructors @@ -1048,13 +992,12 @@ export const succeedNone: Effect> = internal.succeedNone * * **Example** (Succeeding with Option.some) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * * const program = Effect.succeedSome(42) * - * Effect.runPromise(program).then(console.log) - * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * Effect.runSync(program) // => Option.some(42) * ``` * * @category constructors @@ -1080,7 +1023,7 @@ export const succeedSome: (value: A) => Effect> = internal.succeedS * * **Example** (Lazily evaluating side effects) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * let i = 0 @@ -1089,16 +1032,16 @@ export const succeedSome: (value: A) => Effect> = internal.succeedS * * const good = Effect.suspend(() => Effect.succeed(i++)) * - * console.log(Effect.runSync(bad)) // Output: 0 - * console.log(Effect.runSync(bad)) // Output: 0 + * Effect.runSync(bad) // => 0 + * Effect.runSync(bad) // => 0 * - * console.log(Effect.runSync(good)) // Output: 1 - * console.log(Effect.runSync(good)) // Output: 2 + * Effect.runSync(good) // => 1 + * Effect.runSync(good) // => 2 * ``` * * **Example** (Suspending recursive Fibonacci evaluation) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const blowsUp = (n: number): Effect.Effect => @@ -1118,13 +1061,12 @@ export const succeedSome: (value: A) => Effect> = internal.succeedS * (a, b) => a + b * ) * - * console.log(Effect.runSync(allGood(32))) - * // Output: 3524578 + * Effect.runSync(allGood(16)) // => 1597 * ``` * * **Example** (Helping TypeScript infer recursive effect types) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // Without suspend, TypeScript may struggle with type inference. @@ -1145,6 +1087,8 @@ export const succeedSome: (value: A) => Effect> = internal.succeedS * ? Effect.fail(new Error("Cannot divide by zero")) * : Effect.succeed(a / b) * ) + * + * Effect.runSync(withSuspend(6, 2)) // => 3 * ``` * * @category constructors @@ -1173,17 +1117,20 @@ export const suspend: ( * * **Example** (Capturing synchronous logging in an Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * const log = (message: string) => * Effect.sync(() => { - * console.log(message) // side effect + * void output.push(message) // side effect * }) * * // ┌─── Effect * // ▼ * const program = log("Hello, World!") + * Effect.runSync(program) + * output // => ["Hello, World!"] * ``` * * @see {@link try_ | try} for a version that can handle failures. @@ -1231,19 +1178,20 @@ export { * * **Example** (Integrating callback APIs) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * - * const delay = (ms: number) => + * const fromCallback = (message: string) => * Effect.callback((resume) => { - * const timeoutId = setTimeout(() => { + * queueMicrotask(() => { + * void output.push(message) * resume(Effect.void) - * }, ms) - * // Cleanup function for interruption - * return Effect.sync(() => clearTimeout(timeoutId)) + * }) * }) * - * const program = delay(1000) + * await Effect.runPromise(fromCallback("callback completed")) + * output // => ["callback completed"] * ``` * * @category constructors @@ -1263,17 +1211,11 @@ export const callback: ( * * **Example** (Creating a never-ending effect) * - * ```ts - * import { Effect } from "effect" - * - * // This effect will never complete - * const program = Effect.never - * - * // This will run forever (or until interrupted) - * // Effect.runPromise(program) // Never resolves + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * - * // Use with timeout for practical applications - * const timedProgram = Effect.timeout(program, "1 second") + * const program = Effect.timeoutOption(Effect.never, 0) + * await Effect.runPromise(program) // => Option.none() * ``` * * @category constructors @@ -1287,7 +1229,7 @@ export const never: Effect = internal.never * * **Example** (Starting do notation) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } from "effect" * * const program = pipe( @@ -1296,9 +1238,11 @@ export const never: Effect = internal.never * Effect.bind("y", ({ x }) => Effect.succeed(x + 1)), * Effect.let("sum", ({ x, y }) => x + y) * ) + * + * Effect.runSync(program) // => { x: 2, y: 3, sum: 5 } * ``` * - * @category do notation + * @category constructors * @since 2.0.0 */ export const Do: Effect<{}> = internal.Do @@ -1315,7 +1259,7 @@ export const Do: Effect<{}> = internal.Do * @see {@link Do} for starting from an empty accumulated record * @see {@link bind} for adding fields produced by effects * - * @category do notation + * @category mapping * @since 2.0.0 */ export const bindTo: { @@ -1357,7 +1301,7 @@ export { * @see {@link Do} for starting from an empty accumulated record * @see {@link gen} for sequencing without accumulating a record * - * @category do notation + * @category mapping * @since 2.0.0 */ let_ as let @@ -1387,7 +1331,7 @@ export { * @see {@link bindTo} for naming the success value of an existing effect * @see {@link gen} for generator-based sequencing without accumulating a record * - * @category do notation + * @category sequencing * @since 2.0.0 */ export const bind: { @@ -1420,7 +1364,7 @@ export const bind: { * * **Example** (Sequencing effects with generators) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} @@ -1449,6 +1393,8 @@ export const bind: { * const finalAmount = addServiceCharge(discountedAmount) * return `Final amount to charge: ${finalAmount}` * }) + * + * await Effect.runPromise(program) // => "Final amount to charge: 96" * ``` * * @category constructors @@ -1511,7 +1457,7 @@ export declare namespace gen { * * **Example** (Creating a failed effect) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class OperationFailedError extends Data.TaggedError("OperationFailedError")<{}> {} @@ -1521,6 +1467,7 @@ export declare namespace gen { * const failure = Effect.fail( * new OperationFailedError() * ) + * Effect.runSync(Effect.flip(failure))._tag // => "OperationFailedError" * ``` * * @see {@link succeed} to create an effect that represents a successful value. @@ -1542,15 +1489,14 @@ export const fail: (error: E) => Effect = internal.fail * * **Example** (Lazily creating failures) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * - * class ProgramError extends Data.TaggedError("ProgramError")<{ readonly failedAt: Date }> {} + * class ProgramError extends Data.TaggedError("ProgramError")<{ readonly operation: string }> {} * - * const program = Effect.failSync(() => new ProgramError({ failedAt: new Date() })) + * const program = Effect.failSync(() => new ProgramError({ operation: "sync" })) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * Effect.runSync(Effect.flip(program)).operation // => "sync" * ``` * * @category constructors @@ -1574,15 +1520,14 @@ export const failSync: (evaluate: LazyArg) => Effect = internal. * * **Example** (Failing with a full Cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect } from "effect" * * const program = Effect.failCause( * Cause.fail("Network error") * ) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * Effect.runSync(Effect.flip(program)) // => "Network error" * ``` * * @category constructors @@ -1603,15 +1548,14 @@ export const failCause: (cause: Cause.Cause) => Effect = interna * * **Example** (Lazily creating a Cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect } from "effect" * * const program = Effect.failCauseSync(() => * Cause.fail("Error computed at runtime") * ) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: { _id: 'Exit', _tag: 'Failure', cause: ... } + * Effect.runSync(Effect.flip(program)) // => "Error computed at runtime" * ``` * * @category constructors @@ -1640,22 +1584,20 @@ export const failCauseSync: ( * * **Example** (Failing on division by zero) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" * + * const defect = new Error("Cannot divide by zero") * const divide = (a: number, b: number) => * b === 0 - * ? Effect.die(new Error("Cannot divide by zero")) + * ? Effect.die(defect) * : 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... + * Effect.runSyncExit(program) // => Exit.die(defect) * ``` * * @category constructors @@ -1698,23 +1640,23 @@ export { * * **Example** (Parsing JSON) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const parseJSON = (input: string) => * Effect.try(() => JSON.parse(input)) * * // Success case - * Effect.runPromise(parseJSON("{\"name\": \"Alice\"}")).then(console.log) - * // Output: { name: "Alice" } + * await Effect.runPromise(parseJSON("{\"name\": \"Alice\"}")) // => { name: 'Alice' } * * // Failure case maps the thrown value to UnknownError - * Effect.runPromiseExit(parseJSON("invalid json")).then(console.log) + * const exit = await Effect.runPromiseExit(parseJSON("invalid json")) + * exit._tag // => "Failure" * ``` * * **Example** (Mapping exceptions to a tagged error) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class JsonParsingError extends Data.TaggedError("JsonParsingError")<{ readonly cause: unknown }> {} @@ -1725,8 +1667,8 @@ export { * catch: (cause) => new JsonParsingError({ cause }) * }) * - * Effect.runPromiseExit(parseJSON("invalid json")).then(console.log) - * // Output: Exit.failure with custom Error message + * const error = await Effect.runPromise(Effect.flip(parseJSON("invalid json"))) + * error._tag // => "JsonParsingError" * ``` * * @see {@link sync} if the effectful computation is synchronous and does not @@ -1742,16 +1684,18 @@ export { * * **Example** (Yielding to other fibers) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * console.log("Before yield") + * void output.push("Before yield") * yield* Effect.yieldNow - * console.log("After yield") + * void output.push("After yield") * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) + * output // => ["Before yield", "After yield"] * ``` * * @category constructors @@ -1764,16 +1708,18 @@ export const yieldNow: Effect = internal.yieldNow * * **Example** (Yielding with priority) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * console.log("High priority task") + * void output.push("High priority task") * yield* Effect.yieldNowWith(10) // Higher priority - * console.log("Continued after yield") + * void output.push("Continued after yield") * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) + * output // => ["High priority task", "Continued after yield"] * ``` * * @category constructors @@ -1786,15 +1732,12 @@ export const yieldNowWith: (priority?: number) => Effect = internal.yieldN * * **Example** (Reading the current fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const program = Effect.withFiber((fiber) => - * Effect.succeed(`Fiber ID: ${fiber.id}`) - * ) + * const program = Effect.withFiber((fiber) => Effect.succeed(typeof fiber.id)) * - * Effect.runPromise(program).then(console.log) - * // Output: Fiber ID: 1 + * Effect.runSync(program) // => "number" * ``` * * @category constructors @@ -1813,8 +1756,9 @@ export const withFiber: ( * * **Example** (Converting a Result into an Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Result } from "effect" + * const output: Array = [] * * const success = Result.succeed(42) * const failure = Result.fail("Something went wrong") @@ -1822,9 +1766,9 @@ export const withFiber: ( * const effect1 = Effect.fromResult(success) * const effect2 = Effect.fromResult(failure) * - * Effect.runPromise(effect1).then(console.log) // 42 - * Effect.runPromiseExit(effect2).then(console.log) - * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' } } + * void output.push(Effect.runSync(effect1)) + * void output.push(Effect.runSync(Effect.flip(effect2))) + * output // => [42, "Something went wrong"] * ``` * * @category converting @@ -1849,8 +1793,9 @@ export const fromResult: (result: Result.Result) => Effect = i * * **Example** (Converting an Option into an Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option } from "effect" + * const output: Array = [] * * const some = Option.some(42) * const none = Option.none() @@ -1859,9 +1804,10 @@ export const fromResult: (result: Result.Result) => Effect = i * const effect2 = Effect.fromOption(none) * const effect3 = Effect.fromOption(none, () => new Error("missing")) * - * Effect.runPromise(effect1).then(console.log) // 42 - * Effect.runPromiseExit(effect2).then(console.log) - * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: { _id: 'NoSuchElementError' } } } + * void output.push(Effect.runSync(effect1)) + * void output.push(Effect.runSync(Effect.flip(effect2))._tag) + * void output.push(Effect.runSync(Effect.flip(effect3)).message) + * output // => [42, "NoSuchElementError", "missing"] * ``` * * @category converting @@ -1890,7 +1836,7 @@ export const fromOption: | LazyArg, E = Ca * * **Example** (Transposing an Option of an Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option } from "effect" * * const some = Option.some(Effect.succeed(42)) @@ -1899,8 +1845,7 @@ export const fromOption: | LazyArg, E = Ca * // ▼ * const program = Effect.transposeOption(some) * - * Effect.runPromise(program).then(console.log) - * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * Effect.runSync(program) // => Option.some(42) * ``` * * @category converting @@ -1916,20 +1861,20 @@ export const transposeOption: ( * * **Example** (Failing on nullish values) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.fn(function*(input: string | null) { * const value = yield* Effect.fromNullishOr(input) - * yield* Console.log(value) + * yield* Effect.sync(() => { output.push(value) }) * }, - * Effect.catch(() => Console.log("missing")) + * Effect.catch(() => Effect.sync(() => { output.push("missing") })) * ) * - * Effect.runPromise(program(null)) - * // Output: missing - * Effect.runPromise(program("hello")) - * // Output: hello + * await Effect.runPromise(program(null)) + * await Effect.runPromise(program("hello")) + * output // => ["missing", "hello"] * ``` * * @category converting @@ -1963,8 +1908,9 @@ export const fromNullishOr: (value: A) => Effect, Cause.NoSuch * * **Example** (Choosing flatMap syntax variants) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } from "effect" + * const output: Array = [] * * const myEffect = Effect.succeed(1) * const transformation = (n: number) => Effect.succeed(n + 1) @@ -1972,11 +1918,18 @@ export const fromNullishOr: (value: A) => Effect, Cause.NoSuch * const flatMappedWithPipe = pipe(myEffect, Effect.flatMap(transformation)) * const flatMappedWithDataFirst = Effect.flatMap(myEffect, transformation) * const flatMappedWithMethod = myEffect.pipe(Effect.flatMap(transformation)) + * + * void output.push(Effect.runSync(Effect.all([ + * flatMappedWithPipe, + * flatMappedWithDataFirst, + * flatMappedWithMethod + * ]))) + * output // => [[2, 2, 2]] * ``` * * **Example** (Sequencing dependent effects) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, pipe } from "effect" * * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} @@ -1999,8 +1952,7 @@ export const fromNullishOr: (value: A) => Effect, Cause.NoSuch * Effect.flatMap((amount) => applyDiscount(amount, 5)) * ) * - * Effect.runPromise(finalAmount).then(console.log) - * // Output: 95 + * await Effect.runPromise(finalAmount) // => 95 * ``` * * @see {@link tap} for a version that ignores the result of the effect. @@ -2022,16 +1974,19 @@ export const flatMap: { * * **Example** (Flattening nested effects) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const nested = Effect.succeed(Effect.succeed("hello")) * * const program = Effect.gen(function*() { * const value = yield* Effect.flatten(nested) - * yield* Console.log(value) - * // Output: hello + * yield* Effect.sync(() => { output.push(value) }) * }) + * + * Effect.runSync(program) + * output // => ["hello"] * ``` * * @category sequencing @@ -2061,8 +2016,9 @@ export const flatten: (self: Effect, E2, R2>) = * * **Example** (Choosing andThen syntax variants) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } from "effect" + * const output: Array = [] * * const myEffect = Effect.succeed(1) * const anotherEffect = Effect.succeed("done") @@ -2070,11 +2026,18 @@ export const flatten: (self: Effect, E2, R2>) = * const transformedWithPipe = pipe(myEffect, Effect.andThen(anotherEffect)) * const transformedWithDataFirst = Effect.andThen(myEffect, anotherEffect) * const transformedWithMethod = myEffect.pipe(Effect.andThen(anotherEffect)) + * + * void output.push(Effect.runSync(Effect.all([ + * transformedWithPipe, + * transformedWithDataFirst, + * transformedWithMethod + * ]))) + * output // => [['done', 'done', 'done']] * ``` * * **Example** (Sequencing a discount calculation after fetching a total) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, pipe } from "effect" * * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} @@ -2098,8 +2061,7 @@ export const flatten: (self: Effect, E2, R2>) = * Effect.flatMap((amount) => applyDiscount(amount, 5)) * ) * - * Effect.runPromise(result1).then(console.log) - * // Output: 190 + * await Effect.runPromise(result1) // => 190 * * // Using Effect.andThen * const result2 = pipe( @@ -2108,8 +2070,7 @@ export const flatten: (self: Effect, E2, R2>) = * Effect.andThen((amount) => applyDiscount(amount, 5)) * ) * - * Effect.runPromise(result2).then(console.log) - * // Output: 190 + * await Effect.runPromise(result2) // => 190 * ``` * * @category sequencing @@ -2150,8 +2111,9 @@ export const andThen: { * * **Example** (Logging a step in a pipeline) * - * ```ts - * import { Console, Data, Effect, pipe } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, pipe } from "effect" + * const output: Array = [] * * class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {} * @@ -2170,15 +2132,13 @@ export const andThen: { * const finalAmount = pipe( * fetchTransactionAmount, * // Log the fetched transaction amount - * Effect.tap((amount) => Console.log(`Apply a discount to: ${amount}`)), + * Effect.tap((amount) => Effect.sync(() => { output.push(`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 + * void output.push(await Effect.runPromise(finalAmount)) + * output // => ["Apply a discount to: 100", 95] * ``` * * @category sequencing @@ -2232,8 +2192,8 @@ export const tap: { * * **Example** (Capturing success or failure as Result) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Result } from "effect" * * const success = Effect.succeed(42) * const failure = Effect.fail("Something went wrong") @@ -2241,17 +2201,15 @@ export const tap: { * const program1 = Effect.result(success) * const program2 = Effect.result(failure) * - * Effect.runPromise(program1).then(console.log) - * // { _id: 'Result', _tag: 'Success', value: 42 } + * Effect.runSync(program1) // => Result.succeed(42) * - * Effect.runPromise(program2).then(console.log) - * // { _id: 'Result', _tag: 'Failure', error: 'Something went wrong' } + * Effect.runSync(program2) // => Result.fail("Something went wrong") * ``` * * @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`. * - * @category outcome encapsulation + * @category error handling * @since 4.0.0 */ export const result: (self: Effect) => Effect, never, R> = internal.result @@ -2280,26 +2238,21 @@ export const result: (self: Effect) => Effect [Option.some(1), Option.none()] * ``` * * @see {@link result} for a version that uses `Result` instead. * @see {@link exit} for a version that encapsulates both recoverable errors and defects in an `Exit`. * - * @category outcome encapsulation + * @category error handling * @since 2.0.0 */ export const option: (self: Effect) => Effect, never, R> = internal.option @@ -2324,8 +2277,8 @@ export const option: (self: Effect) => Effect, never * * **Example** (Capturing completion as Exit) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" * * const success = Effect.succeed(42) * const failure = Effect.fail("Something went wrong") @@ -2333,17 +2286,15 @@ export const option: (self: Effect) => Effect, never * const program1 = Effect.exit(success) * const program2 = Effect.exit(failure) * - * Effect.runPromise(program1).then(console.log) - * // { _id: 'Exit', _tag: 'Success', value: 42 } + * Effect.runSync(program1) // => Exit.succeed(42) * - * Effect.runPromise(program2).then(console.log) - * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Something went wrong' } } + * Effect.runSync(program2) // => Exit.fail("Something went wrong") * ``` * * @see {@link option} for a version that uses `Option` instead. * @see {@link result} for a version that uses `Result` instead. * - * @category outcome encapsulation + * @category error handling * @since 2.0.0 */ export const exit: ( @@ -2370,8 +2321,9 @@ export const exit: ( * * **Example** (Choosing map syntax variants) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } from "effect" + * const output: Array = [] * * const myEffect = Effect.succeed(1) * const transformation = (n: number) => n + 1 @@ -2379,11 +2331,18 @@ export const exit: ( * const mappedWithPipe = pipe(myEffect, Effect.map(transformation)) * const mappedWithDataFirst = Effect.map(myEffect, transformation) * const mappedWithMethod = myEffect.pipe(Effect.map(transformation)) + * + * void output.push(Effect.runSync(Effect.all([ + * mappedWithPipe, + * mappedWithDataFirst, + * mappedWithMethod + * ]))) + * output // => [[2, 2, 2]] * ``` * * **Example** (Adding a service charge) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } from "effect" * * const addServiceCharge = (amount: number) => amount + 1 @@ -2395,8 +2354,7 @@ export const exit: ( * Effect.map(addServiceCharge) * ) * - * Effect.runPromise(finalAmount).then(console.log) - * // Output: 101 + * await Effect.runPromise(finalAmount) // => 101 * ``` * * @see {@link mapError} for a version that operates on the error channel. @@ -2425,14 +2383,13 @@ export const map: { * * **Example** (Replacing a success value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, pipe } 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" + * Effect.runSync(program) // => "new value" * ``` * * @see {@link map} for deriving the replacement value from the success value @@ -2451,13 +2408,12 @@ export const as: { * * **Example** (Wrapping success in Option.some) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * * const program = Effect.asSome(Effect.succeed(42)) * - * Effect.runPromise(program).then(console.log) - * // { _id: 'Option', _tag: 'Some', value: 42 } + * Effect.runSync(program) // => Option.some(42) * ``` * * @category mapping @@ -2470,13 +2426,12 @@ export const asSome: (self: Effect) => Effect, E, R> * * **Example** (Discarding success values) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.asVoid(Effect.succeed(42)) * - * Effect.runPromise(program).then(console.log) - * // undefined (void) + * Effect.runSync(program) // => undefined * ``` * * @category mapping @@ -2497,7 +2452,7 @@ export const asVoid: (self: Effect) => Effect = in * * **Example** (Swapping success and failure channels) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // ┌─── Effect @@ -2507,6 +2462,7 @@ export const asVoid: (self: Effect) => Effect = in * // ┌─── Effect * // ▼ * const flipped = Effect.flip(program) + * Effect.runSync(flipped) // => "Oh uh!" * ``` * * @category mapping @@ -2537,17 +2493,11 @@ export const flip: (self: Effect) => Effect = interna * * **Example** (Combining two effects sequentially) * - * ```ts + * ```ts import.meta.vitest * 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 task1 = Effect.succeed(1) + * const task2 = Effect.succeed("hello") * * // Combine the two effects together * // @@ -2555,35 +2505,21 @@ export const flip: (self: Effect) => Effect = interna * // ▼ * 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' ] + * Effect.runSync(program) // => [1, 'hello'] * ``` * * **Example** (Combining two effects concurrently) * - * ```ts + * ```ts import.meta.vitest * 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 task1 = Effect.succeed(1) + * const task2 = Effect.succeed("hello") * * // 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' ] + * await Effect.runPromise(program) // => [1, 'hello'] * ``` * * @see {@link zipWith} for a version that combines the results with a custom function. @@ -2622,17 +2558,11 @@ export const zip: { * * **Example** (Combining two success values with a function) * - * ```ts + * ```ts import.meta.vitest * 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 task1 = Effect.succeed(1) + * const task2 = Effect.succeed("hello") * * const task3 = Effect.zipWith( * task1, @@ -2641,11 +2571,7 @@ export const zip: { * (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 + * Effect.runSync(task3) // => 6 * ``` * * @category zipping @@ -2724,7 +2650,7 @@ export { * * **Example** (Handling a tagged error) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * class NetworkError { @@ -2737,13 +2663,16 @@ export { * constructor(readonly message: string) {} * } * - * declare const task: Effect.Effect + * const task: Effect.Effect = + * Effect.fail(new NetworkError("offline")) * * const program = Effect.catchTag( * task, * "NetworkError", * (error) => Effect.succeed(`Recovered from network error: ${error.message}`) * ) + * + * Effect.runSync(program) // => "Recovered from network error: offline" * ``` * * @see {@link catchTags} for handling multiple tagged errors in one call @@ -2822,7 +2751,7 @@ export const catchTag: { * * **Example** (Handling multiple tagged errors) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * // Define tagged error types @@ -2835,7 +2764,8 @@ export const catchTag: { * }> {} * * // An effect that might fail with multiple error types - * declare const program: Effect.Effect + * const program: Effect.Effect = + * Effect.fail(new NetworkError({ statusCode: 503 })) * * // Handle multiple error types at once * const handled = Effect.catchTags(program, { @@ -2843,6 +2773,8 @@ export const catchTag: { * Effect.succeed(`Validation failed: ${error.message}`), * NetworkError: (error) => Effect.succeed(`Network error: ${error.statusCode}`) * }) + * + * Effect.runSync(handled) // => "Network error: 503" * ``` * * @category error handling @@ -2927,7 +2859,7 @@ export const catchTags: { * * **Example** (Handling an error reason) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ @@ -2942,7 +2874,9 @@ export const catchTags: { * reason: RateLimitError | QuotaExceededError * }> {} * - * declare const program: Effect.Effect + * const program: Effect.Effect = Effect.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 30 }) }) + * ) * * // Handle rate limits specifically * const handled = program.pipe( @@ -2950,6 +2884,8 @@ export const catchTags: { * Effect.succeed(`Retry after ${reason.retryAfter}s`) * ) * ) + * + * Effect.runSync(handled) // => "Retry after 30s" * ``` * * @see {@link catchReasons} for handling several nested reason tags @@ -3020,7 +2956,7 @@ export const catchReason: { * * **Example** (Handling multiple error reasons) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ @@ -3035,7 +2971,9 @@ export const catchReason: { * reason: RateLimitError | QuotaExceededError * }> {} * - * declare const program: Effect.Effect + * const program: Effect.Effect = Effect.fail( + * new AiError({ reason: new QuotaExceededError({ limit: 100 }) }) + * ) * * const handled = program.pipe( * Effect.catchReasons("AiError", { @@ -3045,6 +2983,8 @@ export const catchReason: { * Effect.succeed(`Quota exceeded: ${reason.limit}`) * }) * ) + * + * Effect.runSync(handled) // => "Quota exceeded: 100" * ``` * * @category error handling @@ -3166,7 +3106,7 @@ export type TagsWithReason = { * * **Example** (Extracting the reason from a tagged error) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ @@ -3181,11 +3121,14 @@ export type TagsWithReason = { * reason: RateLimitError | QuotaExceededError * }> {} * - * declare const program: Effect.Effect + * const program: Effect.Effect = Effect.fail( + * new AiError({ reason: new RateLimitError({ retryAfter: 30 }) }) + * ) * * // Before: Effect * // After: Effect * const unwrapped = program.pipe(Effect.unwrapReason("AiError")) + * Effect.runSync(Effect.flip(unwrapped))._tag // => "RateLimitError" * ``` * * @category error handling @@ -3229,8 +3172,9 @@ export const unwrapReason: { * * **Example** (Recovering from full failure causes) * - * ```ts - * import { Cause, Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect } from "effect" + * const output: Array = [] * * // An effect that might fail in different ways * const program = Effect.die("Something went wrong") @@ -3238,12 +3182,15 @@ export const unwrapReason: { * // Recover from any cause (including defects) * const recovered = Effect.catchCause(program, (cause) => { * if (Cause.hasDies(cause)) { - * return Console.log("Caught defect").pipe( + * return Effect.sync(() => { output.push("Caught defect") }).pipe( * Effect.as("Recovered from defect") * ) * } * return Effect.succeed("Unknown error") * }) + * + * void output.push(Effect.runSync(recovered)) + * output // => ["Caught defect", "Recovered from defect"] * ``` * * @category error handling @@ -3279,8 +3226,9 @@ export const catchCause: { * * **Example** (Recovering from defects) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * // An effect that might throw an unexpected error (defect) * const program = Effect.sync(() => { @@ -3289,10 +3237,13 @@ export const catchCause: { * * // Recover from defects only * const recovered = Effect.catchDefect(program, (defect) => { - * return Console.log(`Caught defect: ${defect}`).pipe( + * return Effect.sync(() => { output.push(`Caught defect: ${(defect as Error).message}`) }).pipe( * Effect.as("Recovered from defect") * ) * }) + * + * void output.push(Effect.runSync(recovered)) + * output // => ["Caught defect: Unexpected error", "Recovered from defect"] * ``` * * @category error handling @@ -3323,7 +3274,7 @@ export const catchDefect: { * * **Example** (Recovering when a predicate matches) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, Filter } from "effect" * * class NotFound extends Data.TaggedError("NotFound")<{ id: string }> {} @@ -3345,6 +3296,8 @@ export const catchDefect: { * (error) => Effect.succeed(`missing:${error.id}`) * ) * ) + * + * Effect.runSync(Effect.all([recovered, recovered2])) // => ['missing:user-1', 'missing:user-1'] * ``` * * @category error handling @@ -3434,14 +3387,16 @@ export const catchFilter: { * * **Example** (Recovering from missing Option values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option } from "effect" + * const output: Array = [] * * const some = Effect.fromNullishOr(1).pipe(Effect.catchNoSuchElement) * const none = Effect.fromNullishOr(null).pipe(Effect.catchNoSuchElement) * - * Effect.runPromise(some).then(console.log) // { _id: 'Option', _tag: 'Some', value: 1 } - * Effect.runPromise(none).then(console.log) // { _id: 'Option', _tag: 'None' } + * void output.push(Effect.runSync(some)) + * void output.push(Effect.runSync(none)) + * output // => [Option.some(1), Option.none()] * ``` * * @see {@link fromOption} for converting `Option.none` into `NoSuchElementError` @@ -3470,8 +3425,9 @@ export const catchNoSuchElement: ( * * **Example** (Recovering from selected causes) * - * ```ts - * import { Cause, Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect } from "effect" + * const output: Array = [] * * const httpRequest = Effect.fail("Network Error") * @@ -3481,14 +3437,13 @@ export const catchNoSuchElement: ( * Cause.hasFails, * (cause) => * Effect.gen(function*() { - * yield* Console.log(`Caught network error: ${Cause.squash(cause)}`) + * yield* Effect.sync(() => { output.push(`Caught network error: ${Cause.squash(cause)}`) }) * return "Fallback response" * }) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: "Caught network error: Network Error" - * // Then: "Fallback response" + * void output.push(Effect.runSync(program)) + * output // => ["Caught network error: Network Error", "Fallback response"] * ``` * * @see {@link catchCause} for recovering from every cause @@ -3559,7 +3514,7 @@ export const catchCauseFilter: { * * **Example** (Transforming the error channel) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} @@ -3574,6 +3529,7 @@ export const catchCauseFilter: { * simulatedTask, * (message) => new TaskError({ message }) * ) + * Effect.runSync(Effect.flip(mapped)).message // => "Oh no!" * ``` * * @see {@link map} for a version that operates on the success channel. @@ -3604,7 +3560,7 @@ export const mapError: { * * **Example** (Transforming success and failure channels) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} @@ -3619,6 +3575,7 @@ export const mapError: { * onFailure: (message) => new TaskError({ message }), * onSuccess: (n) => n > 0 * }) + * Effect.runSync(Effect.flip(modified)).message // => "Oh no!" * ``` * * @see {@link map} for a version that operates on the success channel. @@ -3648,8 +3605,8 @@ export const mapBoth: { * * **Example** (Converting typed failures into defects) * - * ```ts - * import { Data, Effect } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Exit } from "effect" * * class DivideByZeroError extends Data.TaggedError("DivideByZeroError")<{}> {} * @@ -3662,13 +3619,10 @@ export const mapBoth: { * // ▼ * const program = Effect.orDie(divide(1, 0)) * - * Effect.runPromise(program).catch(console.error) - * // Output: - * // (FiberFailure) DivideByZeroError - * // ...stack trace... + * Effect.runSyncExit(program) // => Exit.die(new DivideByZeroError()) * ``` * - * @category converting failures to defects + * @category error handling * @since 2.0.0 */ export const orDie: (self: Effect) => Effect = internal.orDie @@ -3685,8 +3639,9 @@ export const orDie: (self: Effect) => Effect = in * * **Example** (Running effects on failure) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * // Simulate a task that fails with an error * const task: Effect.Effect = Effect.fail("NetworkError") @@ -3694,12 +3649,11 @@ export const orDie: (self: Effect) => Effect = in * // Use tapError to log the error message when the task fails * const tapping = Effect.tapError( * task, - * (error) => Console.log(`expected error: ${error}`) + * (error) => Effect.sync(() => { output.push(`expected error: ${error}`) }) * ) * - * Effect.runFork(tapping) - * // Output: - * // expected error: NetworkError + * void output.push(Effect.runSyncExit(tapping)) + * output // => ["expected error: NetworkError", Exit.fail("NetworkError")] * ``` * * @category sequencing @@ -3726,8 +3680,9 @@ export const tapError: { * * **Example** (Running effects for tagged failures) * - * ```ts - * import { Console, Data, Effect } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Exit } from "effect" + * const output: Array = [] * * class NetworkError extends Data.TaggedError("NetworkError")<{ * statusCode: number @@ -3741,12 +3696,11 @@ export const tapError: { * Effect.fail(new NetworkError({ statusCode: 504 })) * * const program = Effect.tapErrorTag(task, "NetworkError", (error) => - * Console.log(`expected error: ${error.statusCode}`) + * Effect.sync(() => { output.push(`expected error: ${error.statusCode}`) }) * ) * - * Effect.runPromiseExit(program) - * // Output: - * // expected error: 504 + * void output.push(Effect.runSyncExit(program)) + * output // => ["expected error: 504", Exit.fail(new NetworkError({ statusCode: 504 }))] * ``` * * @category sequencing @@ -3789,19 +3743,19 @@ export const tapErrorTag: { * * **Example** (Observing full failure causes) * - * ```ts - * import { Cause, Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" + * const output: Array = [] * * const task = Effect.fail("Something went wrong") * * const program = Effect.tapCause( * task, - * (cause) => Console.log(`Logging cause: ${Cause.squash(cause)}`) + * (cause) => Effect.sync(() => { output.push(`Logging cause: ${Cause.squash(cause)}`) }) * ) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: "Logging cause: Error: Something went wrong" - * // Then: { _id: 'Exit', _tag: 'Failure', cause: ... } + * void output.push(Effect.runSyncExit(program)) + * output // => ["Logging cause: Something went wrong", Exit.fail("Something went wrong")] * ``` * * @category sequencing @@ -3828,8 +3782,9 @@ export const tapCause: { * * **Example** (Observing selected failure causes) * - * ```ts - * import { Cause, Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" + * const output: Array = [] * * const task = Effect.fail("Network timeout") * @@ -3837,12 +3792,11 @@ export const tapCause: { * const program = Effect.tapCauseIf( * task, * Cause.hasFails, - * (cause) => Console.log(`Logging failure cause: ${Cause.squash(cause)}`) + * (cause) => Effect.sync(() => { output.push(`Logging failure cause: ${Cause.squash(cause)}`) }) * ) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: "Logging failure cause: Network timeout" - * // Then: { _id: 'Exit', _tag: 'Failure', cause: ... } + * void output.push(Effect.runSyncExit(program)) + * output // => ["Logging failure cause: Network timeout", Exit.fail("Network timeout")] * ``` * * @category sequencing @@ -3906,20 +3860,9 @@ export const tapCauseFilter: { * * **Example** (Observing defects) * - * ```ts - * import { Console, Effect } 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 + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * // Simulate a severe failure in the system * const task2: Effect.Effect = Effect.die( @@ -3929,13 +3872,11 @@ export const tapCauseFilter: { * // Log the defect using tapDefect * const tapping2 = Effect.tapDefect( * task2, - * (cause) => Console.log(`defect: ${cause}`) + * (defect) => Effect.sync(() => { output.push(`defect: ${defect}`) }) * ) * - * Effect.runFork(tapping2) - * // Output: - * // defect: RuntimeException: Something went wrong - * // ... stack trace ... + * void output.push(Effect.runSyncExit(tapping2)) + * output // => ["defect: Something went wrong", Exit.die("Something went wrong")] * ``` * * @category sequencing @@ -3955,14 +3896,15 @@ export const tapDefect: { * * **Example** (Retrying until success) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * let attempts = 0 * * const flaky = Effect.gen(function*() { * attempts++ - * yield* Console.log(`Attempt ${attempts}`) + * yield* Effect.sync(() => { output.push(`Attempt ${attempts}`) }) * if (attempts < 3) { * return yield* Effect.fail("Not ready") * } @@ -3971,12 +3913,8 @@ export const tapDefect: { * * const program = Effect.eventually(flaky) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Attempt 1 - * // Attempt 2 - * // Attempt 3 - * // Ready + * void output.push(await Effect.runPromise(program)) + * output // => ["Attempt 1", "Attempt 2", "Attempt 3", "Ready"] * ``` * * @category repetition @@ -4062,7 +4000,7 @@ export declare namespace Retry { * * **Example** (Retrying with a schedule) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, Schedule } from "effect" * * class AttemptError extends Data.TaggedError("AttemptError")<{ readonly attempt: number }> {} @@ -4077,11 +4015,10 @@ export declare namespace Retry { * } * }) * - * const policy = Schedule.addDelay(Schedule.recurs(5), () => Effect.succeed("100 millis")) + * const policy = Schedule.recurs(5) * const program = Effect.retry(task, policy) * - * Effect.runPromise(program).then(console.log) - * // Output: "Success!" (after 2 retries) + * await Effect.runPromise(program) // => "Success!" * ``` * * @see {@link retryOrElse} for a version that allows you to run a fallback. @@ -4130,15 +4067,16 @@ export const retry: { * * **Example** (Falling back after retries are exhausted) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Schedule } from "effect" + * const output: Array = [] * * class NetworkTimeoutError extends Data.TaggedError("NetworkTimeoutError")<{}> {} * * let attempt = 0 * const networkRequest = Effect.gen(function*() { * attempt++ - * yield* Console.log(`Network attempt ${attempt}`) + * yield* Effect.sync(() => { output.push(`Network attempt ${attempt}`) }) * if (attempt < 3) { * return yield* Effect.fail(new NetworkTimeoutError()) * } @@ -4151,17 +4089,13 @@ export const retry: { * Schedule.recurs(2), * (error, retryCount) => * Effect.gen(function*() { - * yield* Console.log(`All ${retryCount} retries failed, using cache`) + * yield* Effect.sync(() => { output.push(`All ${retryCount} retries failed, using cache`) }) * return "Cached data" * }) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Network attempt 1 - * // Network attempt 2 - * // Network attempt 3 - * // Network data + * void output.push(await Effect.runPromise(program)) + * output // => ["Network attempt 1", "Network attempt 2", "Network attempt 3", "Network data"] * ``` * * @see {@link retry} for a version that does not run a fallback effect. @@ -4191,7 +4125,7 @@ export const retryOrElse: { * * **Example** (Exposing failures as causes) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect } from "effect" * * const task = Effect.fail("Something went wrong") @@ -4202,8 +4136,7 @@ export const retryOrElse: { * return `Caught cause: ${Cause.squash(result)}` * }) * - * Effect.runPromise(program).then(console.log) - * // Output: "Caught cause: Something went wrong" + * Effect.runSync(program) // => "Caught cause: Something went wrong" * ``` * * @category error handling @@ -4228,7 +4161,7 @@ export const sandbox: ( * * **Example** (Discarding success and failure values) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // ┌─── Effect @@ -4238,17 +4171,18 @@ export const sandbox: ( * // ┌─── Effect * // ▼ * const program = task.pipe(Effect.ignore) + * Effect.runSync(program) // => undefined * ``` * * **Example** (Logging failures while ignoring results) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const task = Effect.fail("Uh oh!") * - * const program = task.pipe(Effect.ignore({ log: true })) - * const programWarn = task.pipe(Effect.ignore({ log: "Warn", message: "Ignoring task failure" })) + * const program = task.pipe(Effect.ignore) + * Effect.runSync(program) // => undefined * ``` * * @category error handling @@ -4286,13 +4220,13 @@ export const ignore: < * * **Example** (Ignoring failures and logging causes) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const task = Effect.fail("boom") * * const program = task.pipe(Effect.ignoreCause) - * const programLog = task.pipe(Effect.ignoreCause({ log: true, message: "Ignoring failure cause" })) + * Effect.runSync(program) // => undefined * ``` * * @category error handling @@ -4325,9 +4259,17 @@ export const ignoreCause: < * and retry timing is derived per step (the first attempt uses the remaining * attempts schedule; later retries apply the step schedule at least once). * + * Attempts can be observed from outside the effect by passing + * `options.onEvent`, which receives an `ExecutionPlan.Event` before each + * attempt and after it settles. The handler is awaited inline before and after + * every attempt, so events are strictly ordered; keep it cheap. It cannot + * fail, which keeps observation from changing the plan's outcome, and its + * requirements are added to the resulting effect. Terminal events run like + * finalizers, so they are emitted even when the attempt is interrupted. + * * **Example** (Retrying with an execution plan) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, ExecutionPlan, Layer } from "effect" * * const Endpoint = Context.Service<{ url: string }>("Endpoint") @@ -4346,21 +4288,58 @@ export const ignoreCause: < * ) * * const program = Effect.withExecutionPlan(fetchUrl, plan) + * Effect.runSync(program) // => "good" + * ``` + * + * **Example** (Observing execution-plan attempts) + * + * ```ts import.meta.vitest + * import { Context, Effect, ExecutionPlan, Layer } from "effect" + * + * const Endpoint = Context.Service<{ url: string }>("Endpoint") + * + * const fetchUrl = Effect.gen(function*() { + * const endpoint = yield* Effect.service(Endpoint) + * if (endpoint.url === "bad") { + * return yield* Effect.fail("Unavailable") + * } + * return endpoint.url + * }) + * + * const plan = ExecutionPlan.make( + * { provide: Layer.succeed(Endpoint, { url: "bad" }) }, + * { provide: Layer.succeed(Endpoint, { url: "good" }) } + * ) + * + * const events: Array = [] + * const program = Effect.withExecutionPlan(fetchUrl, plan, { + * onEvent: (event) => Effect.sync(() => events.push(`${event._tag}:${event.stepIndex}`)) + * }) + * + * await Effect.runPromise(program) // => "good" + * + * events // => ["AttemptStart:0", "AttemptFailure:0", "AttemptStart:1", "AttemptSuccess:1"] * ``` * - * @category fallback + * @category error handling * @since 3.16.0 */ export const withExecutionPlan: { - ( - plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> + ( + plan: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }>, + options?: { + readonly onEvent?: ((event: ExecutionPlan.Event) => Effect) | undefined + } ): ( effect: Effect - ) => Effect | PlanR> - ( + ) => Effect | PlanR | RX> + ( effect: Effect, - plan: ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }> - ): Effect | PlanR> + plan: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PlanE; requirements: PlanR }>, + options?: { + readonly onEvent?: ((event: ExecutionPlan.Event) => Effect) | undefined + } + ): Effect | PlanR | RX> } = internalExecutionPlan.withExecutionPlan /** @@ -4401,8 +4380,8 @@ export const withErrorReporting: < * * **Example** (Replacing failures with a value) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" * * const validate = (age: number): Effect.Effect => { * if (age < 0) { @@ -4416,12 +4395,10 @@ export const withErrorReporting: < * * const program = Effect.orElseSucceed(validate(-1), () => 18) * - * console.log(Effect.runSyncExit(program)) - * // Output: - * // { _id: 'Exit', _tag: 'Success', value: 18 } + * Effect.runSyncExit(program) // => Exit.succeed(18) * ``` * - * @category fallback + * @category error handling * @since 2.0.0 */ export const orElseSucceed: { @@ -4456,7 +4433,7 @@ export const orElseSucceed: { * * **Example** (Trying alternatives until one succeeds) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const primary = Effect.fail("primary unavailable") @@ -4471,11 +4448,10 @@ export const orElseSucceed: { * tertiary * ]) * - * console.log(Effect.runSync(program)) - * // Output: "secondary result" + * Effect.runSync(program) // => "secondary result" * ``` * - * @category fallback + * @category error handling * @since 2.0.0 */ export const firstSuccessOf: >( @@ -4499,8 +4475,8 @@ export const firstSuccessOf: >( * * The `timeout` function allows you to specify a time limit for an * effect's execution. If the effect does not complete within the given time, a - * `TimeoutException` is raised. This can be useful for controlling how long - * your program waits for a task to finish, ensuring that it doesn't hang + * `TimeoutError` is raised. This can be useful for controlling how long your + * program waits for a task to finish, ensuring that it doesn't hang * indefinitely if the task takes too long. * * **Gotchas** @@ -4509,32 +4485,12 @@ export const firstSuccessOf: >( * * **Example** (Failing when work takes too long) * - * ```ts + * ```ts import.meta.vitest * 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' } - * // } - * // } + * const timedEffect = Effect.never.pipe(Effect.timeout(0)) + * const error = await Effect.runPromise(Effect.flip(timedEffect)) + * error._tag // => "TimeoutError" * ``` * * @see {@link timeoutOption} for returning `Option.none` on timeout. @@ -4570,33 +4526,14 @@ export const timeout: { * * **Example** (Returning None on timeout) * - * ```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")) - * ]) + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * - * Effect.runPromise(timedOutEffect).then(console.log) - * // Output: - * // Start processing... - * // Processing complete. - * // Start processing... - * // [ - * // { _id: 'Option', _tag: 'Some', value: 'Result' }, - * // { _id: 'Option', _tag: 'None' } - * // ] + * const timedOutEffect = Effect.never.pipe(Effect.timeoutOption(0)) + * await Effect.runPromise(timedOutEffect) // => Option.none() * ``` * - * @see {@link timeout} for a version that raises a `TimeoutException`. + * @see {@link timeout} for a version that raises a `TimeoutError`. * @see {@link timeoutOrElse} for a version that allows specifying both success and timeout handlers. * * @category delays & timeouts @@ -4631,33 +4568,22 @@ export const timeoutOption: { * * **Example** (Falling back on timeout) * - * ```ts - * import { Console, Effect } from "effect" - * - * const slowQuery = Effect.gen(function*() { - * yield* Console.log("Starting database query...") - * yield* Effect.sleep("5 seconds") - * return "Database result" - * }) + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * - * // Use cached data as fallback when timeout is reached - * const program = Effect.timeoutOrElse(slowQuery, { - * duration: "2 seconds", - * orElse: () => - * Effect.gen(function*() { - * yield* Console.log("Query timed out, using cached data") - * return "Cached result" - * }) + * const program = Effect.timeoutOrElse(Effect.never, { + * duration: 0, + * orElse: () => Effect.sync(() => { output.push("Query timed out, using cached data") }).pipe( + * Effect.as("Cached result") + * ) * }) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Starting database query... - * // Query timed out, using cached data - * // Cached result + * void output.push(await Effect.runPromise(program)) + * output // => ["Query timed out, using cached data", "Cached result"] * ``` * - * @see {@link timeout} for failing with a `TimeoutException`. + * @see {@link timeout} for failing with a `TimeoutError`. * @see {@link timeoutOption} for returning `Option.none` on timeout. * * @category delays & timeouts @@ -4683,16 +4609,14 @@ export const timeoutOrElse: { * * **Example** (Delaying an effect) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * - * const program = Effect.delay( - * Console.log("Delayed message"), - * "1 second" - * ) + * const program = Effect.delay(Effect.sync(() => { output.push("Delayed message") }), 0) * - * Effect.runFork(program) - * // Waits 1 second, then prints: "Delayed message" + * await Effect.runPromise(program) + * output // => ["Delayed message"] * ``` * * @category delays & timeouts @@ -4714,18 +4638,18 @@ export const delay: { * * **Example** (Pausing without blocking) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * yield* Console.log("Start") - * yield* Effect.sleep("2 seconds") - * yield* Console.log("End") + * yield* Effect.sync(() => { output.push("Start") }) + * yield* Effect.sleep(0) + * yield* Effect.sync(() => { output.push("End") }) * }) * - * Effect.runFork(program) - * // Output: "Start" (immediately) - * // Output: "End" (after 2 seconds) + * await Effect.runPromise(program) + * output // => ["Start", "End"] * ``` * * @category delays & timeouts @@ -4743,13 +4667,15 @@ export const sleep: (duration: Duration.Input) => Effect = internal.sleep * * **Example** (Measuring execution time) * - * ```ts - * import { Console, Duration, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const program = Effect.gen(function*() { - * const [duration, value] = yield* Effect.timed(Effect.succeed("ok")) - * yield* Console.log(`took ${Duration.toMillis(duration)}ms: ${value}`) + * const [, value] = yield* Effect.timed(Effect.succeed("ok")) + * return value * }) + * + * Effect.runSync(program) // => "ok" * ``` * * @category delays & timeouts @@ -4779,18 +4705,14 @@ export const timed: (self: Effect) => Effect<[duration: Durati * * **Example** (Racing many effects) * - * ```ts - * import { Duration, Effect } from "effect" - * - * // Multiple effects with different delays - * const effect1 = Effect.delay(Effect.succeed("Fast"), Duration.millis(100)) - * const effect2 = Effect.delay(Effect.succeed("Slow"), Duration.millis(500)) - * const effect3 = Effect.delay(Effect.succeed("Very Slow"), Duration.millis(1000)) - * - * // Race all effects - the first to succeed wins - * const raced = Effect.raceAll([effect1, effect2, effect3]) + * ```ts import.meta.vitest + * import { Effect } from "effect" * - * // Result: "Fast" (after ~100ms) + * const raced = Effect.raceAll([ + * Effect.succeed("Fast"), + * Effect.never + * ]) + * await Effect.runPromise(raced) // => "Fast" * ``` * * @see {@link race} for a version that handles only two effects. @@ -4820,18 +4742,14 @@ export const raceAll: >( * * **Example** (Taking the first settled result) * - * ```ts - * import { Duration, Effect } from "effect" - * - * // Multiple effects with different delays and potential failures - * const effect1 = Effect.delay(Effect.succeed("First"), Duration.millis(200)) - * const effect2 = Effect.delay(Effect.fail("Second failed"), Duration.millis(100)) - * const effect3 = Effect.delay(Effect.succeed("Third"), Duration.millis(300)) - * - * // Race all effects - the first to succeed wins - * const raced = Effect.raceAllFirst([effect1, effect2, effect3]) + * ```ts import.meta.vitest + * import { Effect } from "effect" * - * // Result: "First" (after ~200ms, even though effect2 completes first but fails) + * const raced = Effect.raceAllFirst([ + * Effect.fail("First failed"), + * Effect.never + * ]) + * await Effect.runPromise(Effect.flip(raced)) // => "First failed" * ``` * * @category racing @@ -4858,19 +4776,20 @@ export const raceAllFirst: >( * * **Example** (Racing two effects) * - * ```ts - * import { Console, Duration, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * - * const fastFail = Effect.delay(Effect.fail("fast-fail"), Duration.millis(10)) - * const slowSuccess = Effect.delay(Effect.succeed("slow-success"), Duration.millis(50)) + * const fastFail = Effect.fail("fast-fail") + * const slowSuccess = Effect.succeed("slow-success") * * const program = Effect.gen(function*() { * const result = yield* Effect.race(fastFail, slowSuccess) - * yield* Console.log(`winner: ${result}`) + * yield* Effect.sync(() => { output.push(`winner: ${result}`) }) * }) * - * Effect.runPromise(program) - * // Output: winner: slow-success + * await Effect.runPromise(program) + * output // => ["winner: slow-success"] * ``` * * @category racing @@ -4911,22 +4830,23 @@ export const race: { * * **Example** (Observing the winning fiber) * - * ```ts - * import { Console, Duration, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * - * const fastFail = Effect.delay(Effect.fail("fast-fail"), Duration.millis(10)) - * const slowSuccess = Effect.delay(Effect.succeed("slow-success"), Duration.millis(50)) + * const fastFail = Effect.fail("fast-fail") + * const slowSuccess = Effect.never * * const program = Effect.gen(function*() { * const message = yield* Effect.match(Effect.raceFirst(fastFail, slowSuccess), { * onFailure: (error) => `failed: ${error}`, * onSuccess: (value) => `succeeded: ${value}` * }) - * yield* Console.log(message) + * yield* Effect.sync(() => { output.push(message) }) * }) * - * Effect.runPromise(program) - * // Output: failed: fast-fail + * await Effect.runPromise(program) + * output // => ["failed: fast-fail"] * ``` * * @category racing @@ -4962,8 +4882,9 @@ export const raceFirst: { * * **Example** (Filtering success values) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * // Sync predicate * const evens = Effect.filter([1, 2, 3, 4], (n) => n % 2 === 0) @@ -4971,7 +4892,9 @@ export const raceFirst: { * // Effectful predicate * const checked = Effect.filter([1, 2, 3], (n) => Effect.succeed(n > 1)) * - * // Use Effect.filterMapEffect for effectful Filter.Filter callbacks + * void output.push(Effect.runSync(evens)) + * void output.push(Effect.runSync(checked)) + * output // => [[2, 4], [2, 3]] * ``` * * @category filtering @@ -5085,7 +5008,7 @@ export const filterMapEffect: { * * **Example** (Filtering with a fallback effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // An effect that produces a number @@ -5098,7 +5021,7 @@ export const filterMapEffect: { * (n) => Effect.succeed(`Number ${n} is odd`) * ) * - * // Result: "Number 5 is odd" (since 5 is not even) + * Effect.runSync(filtered) // => "Number 5 is odd" * ``` * * @category filtering @@ -5168,7 +5091,7 @@ export const filterMapOrElse: { * * **Example** (Filtering with a custom failure) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // An effect that produces a number @@ -5181,7 +5104,7 @@ export const filterMapOrElse: { * (n) => `Expected even number, got ${n}` * ) * - * // Result: Effect.fail("Expected even number, got 5") + * Effect.runSync(Effect.flip(filtered)) // => "Expected even number, got 5" * ``` * * @category filtering @@ -5283,22 +5206,22 @@ export const filterMapOrFail: { * * **Example** (Conditionally running an effect) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" + * const output: Array = [] * * const shouldLog = true * * const program = Effect.when( - * Console.log("Condition is true!"), + * Effect.sync(() => { output.push("Condition is true!") }), * Effect.succeed(shouldLog) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: "Condition is true!" - * // { _id: 'Option', _tag: 'Some', value: undefined } + * void output.push(Effect.runSync(program)) + * output // => ["Condition is true!", Option.some(undefined)] * ``` * - * @category conditional operators + * @category filtering * @since 2.0.0 */ export const when: { @@ -5333,7 +5256,7 @@ export const when: { * * **Example** (Matching success and failure values) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class ExampleError extends Data.TaggedError("ExampleError")<{ readonly message: string }> {} @@ -5346,8 +5269,7 @@ export const when: { * }) * * // Run and log the result of the successful effect - * Effect.runPromise(program1).then(console.log) - * // Output: "success: 42" + * Effect.runSync(program1) // => "success: 42" * * const failure: Effect.Effect = Effect.fail( * new ExampleError({ message: "Uh oh!" }) @@ -5359,8 +5281,7 @@ export const when: { * }) * * // Run and log the result of the failed effect - * Effect.runPromise(program2).then(console.log) - * // Output: "failure: Uh oh!" + * Effect.runSync(program2) // => "failure: Uh oh!" * ``` * * @see {@link matchEffect} if you need to perform side effects in the handlers. @@ -5399,16 +5320,20 @@ export const match: { * * **Example** (Pattern matching eagerly when possible) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * const result = yield* Effect.matchEager(Effect.succeed(42), { * onFailure: (error) => `Failed: ${error}`, * onSuccess: (value) => `Success: ${value}` * }) - * console.log(result) // "Success: 42" + * void output.push(result) * }) + * + * Effect.runSync(program) + * output // => ["Success: 42"] * ``` * * @see {@link match} for the non-eager version. @@ -5445,7 +5370,7 @@ export const matchEager: { * * **Example** (Matching on success or failure causes) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect } from "effect" * * const task = Effect.fail("Something went wrong") @@ -5455,8 +5380,7 @@ export const matchEager: { * onSuccess: (value) => `Success: ${value}` * }) * - * Effect.runPromise(program).then(console.log) - * // Output: "Failed: Error: Something went wrong" + * Effect.runSync(program) // => "Failed: Something went wrong" * ``` * * @see {@link matchCauseEffect} if you need to perform side effects in the @@ -5495,13 +5419,14 @@ export const matchCause: { * * **Example** (Eagerly matching already completed effects) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const handleResult = Effect.matchCauseEager(Effect.succeed(42), { * onSuccess: (value) => `Success: ${value}`, * onFailure: (cause) => `Failed: ${cause}` * }) + * Effect.runSync(handleResult) // => "Success: 42" * ``` * * @category pattern matching @@ -5576,8 +5501,9 @@ export const matchCauseEffectEager: { * * **Example** (Effectfully matching on causes) * - * ```ts - * import { Cause, Console, Data, Effect, Result } from "effect" + * ```ts import.meta.vitest + * import { Cause, Data, Effect, Result } from "effect" + * const output: Array = [] * * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} * @@ -5589,25 +5515,23 @@ export const matchCauseEffectEager: { * if (Cause.hasFails(cause)) { * const error = Cause.findError(cause) * if (Result.isSuccess(error)) { - * yield* Console.log(`Handling error: ${error.success.message}`) + * yield* Effect.sync(() => { output.push(`Handling error: ${error.success.message}`) }) * } * return "recovered from error" * } else { - * yield* Console.log("Handling interruption or defect") + * yield* Effect.sync(() => { output.push("Handling interruption or defect") }) * return "recovered from interruption/defect" * } * }), * onSuccess: (value) => * Effect.gen(function*() { - * yield* Console.log(`Success: ${value}`) + * yield* Effect.sync(() => { output.push(`Success: ${value}`) }) * return `processed ${value}` * }) * }) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Handling error: Task failed - * // recovered from error + * void output.push(Effect.runSync(program)) + * output // => ["Handling error: Task failed", "recovered from error"] * ``` * * @see {@link matchCause} if you don't need side effects and only want to handle the result or failure. @@ -5646,7 +5570,7 @@ export const matchCauseEffect: { * * **Example** (Matching success and failure with effectful handlers) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class ExampleError extends Data.TaggedError("ExampleError")<{ readonly message: string }> {} @@ -5658,31 +5582,21 @@ export const matchCauseEffect: { * * const program1 = Effect.matchEffect(success, { * onFailure: (error) => - * Effect.succeed(`failure: ${error.message}`).pipe( - * Effect.tap(Effect.log) - * ), + * Effect.succeed(`failure: ${error.message}`), * onSuccess: (value) => - * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * Effect.succeed(`success: ${value}`) * }) * - * console.log(Effect.runSync(program1)) - * // Output: - * // timestamp=... level=INFO fiber=#0 message="success: 42" - * // success: 42 + * Effect.runSync(program1) // => "success: 42" * * const program2 = Effect.matchEffect(failure, { * onFailure: (error) => - * Effect.succeed(`failure: ${error.message}`).pipe( - * Effect.tap(Effect.log) - * ), + * Effect.succeed(`failure: ${error.message}`), * onSuccess: (value) => - * Effect.succeed(`success: ${value}`).pipe(Effect.tap(Effect.log)) + * Effect.succeed(`success: ${value}`) * }) * - * console.log(Effect.runSync(program2)) - * // Output: - * // timestamp=... level=INFO fiber=#1 message="failure: Uh oh!" - * // failure: Uh oh! + * Effect.runSync(program2) // => "failure: Uh oh!" * ``` * * @see {@link match} if you don't need side effects and only want to handle the @@ -5717,19 +5631,20 @@ export const matchEffect: { * * **Example** (Checking whether an effect fails) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * const failed = yield* Effect.isFailure(Effect.fail("Uh oh!")) - * yield* Console.log(failed) + * yield* Effect.sync(() => { output.push(failed) }) * }) * - * Effect.runPromise(program) - * // Output: true + * Effect.runSync(program) + * output // => [true] * ``` * - * @category condition checking + * @category predicates * @since 2.0.0 */ export const isFailure: (self: Effect) => Effect = internal.isFailure @@ -5744,23 +5659,22 @@ export const isFailure: (self: Effect) => Effect = [] * * const program = Effect.gen(function*() { * const ok = yield* Effect.isSuccess(Effect.succeed("done")) * const failed = yield* Effect.isSuccess(Effect.fail("Uh oh")) - * yield* Console.log(`ok: ${ok}`) - * yield* Console.log(`failed: ${failed}`) + * yield* Effect.sync(() => { output.push(`ok: ${ok}`) }) + * yield* Effect.sync(() => { output.push(`failed: ${failed}`) }) * }) * - * Effect.runPromise(program) - * // Output: - * // ok: true - * // failed: false + * Effect.runSync(program) + * output // => ["ok: true", "failed: false"] * ``` * - * @category condition checking + * @category predicates * @since 2.0.0 */ export const isSuccess: (self: Effect) => Effect = internal.isSuccess @@ -5784,8 +5698,9 @@ export const isSuccess: (self: Effect) => Effect = [] * * const Logger = Context.Service<{ * log: (msg: string) => void @@ -5801,20 +5716,22 @@ export const isSuccess: (self: Effect) => Effect { output.push(`Logger available: ${Option.isSome(loggerOption)}`) }) + * yield* Effect.sync(() => { output.push(`Database available: ${Option.isSome(databaseOption)}`) }) * }) * - * const context = Context.make(Logger, { log: console.log }) + * const context = Context.make(Logger, { log: () => {} }) * .pipe(Context.add(Database, { query: () => "result" })) * * const provided = Effect.provideContext(program, context) + * Effect.runSync(provided) + * output // => ["Logger available: true", "Database available: true"] * ``` * * @see {@link contextWith} for deriving an effect from the complete context * @see {@link service} for reading one service from the context * - * @category environment + * @category accessors * @since 2.0.0 */ export const context: () => Effect, never, R> = internal.context @@ -5834,8 +5751,9 @@ export const context: () => Effect, never, R> = in * * **Example** (Deriving values from the context) * - * ```ts - * import { Console, Context, Effect, Option } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Option } from "effect" + * const output: Array = [] * * const Logger = Context.Service<{ * log: (msg: string) => void @@ -5844,19 +5762,19 @@ export const context: () => Effect, never, R> = in * get: (key: string) => string | null * }>("Cache") * - * const program = Effect.contextWith((services) => { + * const program = Effect.contextWith((services: Context.Context>) => { * const cacheOption = Context.getOption(services, Cache) * const hasCache = Option.isSome(cacheOption) * * if (hasCache) { * return Effect.gen(function*() { * const cache = yield* Effect.service(Cache) - * yield* Console.log("Using cached data") + * yield* Effect.sync(() => { output.push("Using cached data") }) * return cache.get("user:123") || "default" * }) * } else { * return Effect.gen(function*() { - * yield* Console.log("No cache available, using fallback") + * yield* Effect.sync(() => { output.push("No cache available, using fallback") }) * return "fallback data" * }) * } @@ -5865,12 +5783,14 @@ export const context: () => Effect, never, R> = in * const withCache = Effect.provideService(program, Cache, { * get: () => "cached_value" * }) + * void output.push(Effect.runSync(withCache)) + * output // => ["Using cached data", "cached_value"] * ``` * * @see {@link context} for reading the complete context as a value * @see {@link service} for reading one service from the context * - * @category environment + * @category accessors * @since 2.0.0 */ export const contextWith: ( @@ -5884,7 +5804,7 @@ export const contextWith: ( * * **Example** (Providing dependencies with a layer) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * interface Database { @@ -5904,11 +5824,10 @@ export const contextWith: ( * * const provided = Effect.provide(program, DatabaseLive) * - * Effect.runPromise(provided).then(console.log) - * // Output: "Result for: SELECT * FROM users" + * await Effect.runPromise(provided) // => "Result for: SELECT * FROM users" * ``` * - * @category environment + * @category providing services * @since 2.0.0 */ export const provide: { @@ -5970,8 +5889,9 @@ export const provide: { * * **Example** (Providing a complete context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" + * const output: Array = [] * * // Define service keys * const Logger = Context.Service<{ @@ -5982,7 +5902,7 @@ export const provide: { * }>("Database") * * // Create a context with multiple services - * const context = Context.make(Logger, { log: console.log }) + * const context = Context.make(Logger, { log: (message) => { output.push(message) } }) * .pipe(Context.add(Database, { query: () => "result" })) * * // An effect that requires both services @@ -5994,9 +5914,11 @@ export const provide: { * }) * * const provided = Effect.provideContext(program, context) + * void output.push(Effect.runSync(provided)) + * output // => ["Querying database", "result"] * ``` * - * @category environment + * @category providing services * @since 4.0.0 */ export const provideContext: { @@ -6025,7 +5947,7 @@ export const provideContext: { * * **Example** (Running with a complete context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" * * class Config extends Context.Service "Hello, World!" * ``` * * @see {@link provideContext} for partially satisfying an effect's context requirements. * @see {@link updateContext} for deriving the required context from the current one. * - * @category environment + * @category providing services * @since 4.0.0 */ export const setContext: { @@ -6061,7 +5982,7 @@ export const setContext: { * * **Example** (Accessing a required service) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" * * interface Database { @@ -6074,9 +5995,14 @@ export const setContext: { * const db = yield* Effect.service(Database) * return yield* db.query("SELECT * FROM users") * }) + * + * const runnable = Effect.provideService(program, Database, { + * query: (sql) => Effect.succeed(`Result for: ${sql}`) + * }) + * Effect.runSync(runnable) // => "Result for: SELECT * FROM users" * ``` * - * @category context + * @category accessors * @since 4.0.0 */ export const service: (service: Context.Key) => Effect = internal.service @@ -6098,8 +6024,9 @@ export const service: (service: Context.Key) => Effect * * **Example** (Accessing an optional service) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Option } from "effect" + * const output: Array = [] * * // Define a service key * const Logger = Context.Service<{ @@ -6113,12 +6040,15 @@ export const service: (service: Context.Key) => Effect * if (Option.isSome(maybeLogger)) { * maybeLogger.value.log("Service is available") * } else { - * console.log("Service not available") + * void output.push("Service not available") * } * }) + * + * Effect.runSync(program) + * output // => ["Service not available"] * ``` * - * @category context + * @category accessors * @since 2.0.0 */ export const serviceOption: (key: Context.Key) => Effect> = internal.serviceOption @@ -6133,7 +6063,7 @@ export const serviceOption: (key: Context.Key) => Effect> * * **Example** (Updating the context before running) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" * * // Define services @@ -6150,18 +6080,19 @@ export const serviceOption: (key: Context.Key) => Effect> * * // Transform services by providing Config while keeping Logger requirement * const configured = program.pipe( - * Effect.updateContext((context: Context.Context) => + * Effect.updateContext((context: Context.Context>) => * Context.add(context, Config, { name: "World" }) * ) * ) * * // The effect now requires only Logger service * const result = Effect.provideService(configured, Logger, { - * log: (msg) => console.log(msg) + * log: () => {} * }) + * Effect.runSync(result) // => "Hello World!" * ``` * - * @category context + * @category providing services * @since 4.0.0 */ export const updateContext: { @@ -6185,15 +6116,16 @@ export const updateContext: { * * **Example** (Replacing a service for one effect) * - * ```ts - * import { Console, Context, Effect } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect } from "effect" + * const output: Array = [] * * // Define a counter service * const Counter = Context.Service<{ count: number }>("Counter") * * const program = Effect.gen(function*() { * const updatedCounter = yield* Effect.service(Counter) - * yield* Console.log(`Updated count: ${updatedCounter.count}`) + * yield* Effect.sync(() => { output.push(`Updated count: ${updatedCounter.count}`) }) * return updatedCounter.count * }).pipe( * Effect.updateService(Counter, (counter) => ({ count: counter.count + 1 })) @@ -6201,26 +6133,92 @@ export const updateContext: { * * // Provide initial service and run * const result = Effect.provideService(program, Counter, { count: 0 }) - * Effect.runPromise(result).then(console.log) - * // Output: Updated count: 1 - * // 1 + * void output.push(Effect.runSync(result)) + * output // => ["Updated count: 1", 1] * ``` * - * @category context + * @category providing services * @since 2.0.0 */ export const updateService: { ( service: Context.Key, - f: (value: A) => A + f: (value: A) => NoInfer ): (self: Effect) => Effect ( self: Effect, service: Context.Key, - f: (value: A) => A + f: (value: A) => NoInfer ): Effect } = internal.updateService +/** + * Updates a service for the lifetime of the current scope and restores its + * previous value when the scope closes. + * + * **When to use** + * + * Use when you need a setup effect to change a service for subsequent effects + * in the same scope. + * + * **Details** + * + * The updater receives the currently visible service value. A + * `Context.Service` remains in the requirements, while a `Context.Reference` + * uses its default when no override is present and adds no service requirement. + * The returned effect always requires `Scope`. The optional `reset` function + * receives the original, updated, and current values when the scope closes, + * allowing changes to be merged during restoration. It defaults to returning + * the original value. + * + * **Example** (Updating a reference within a scope) + * + * ```ts import.meta.vitest + * import { Context, Effect } from "effect" + * const output: Array = [] + * + * const CurrentNumber = Context.Reference("CurrentNumber", { + * defaultValue: () => 1 + * }) + * + * const program = Effect.gen(function*() { + * const before = yield* CurrentNumber + * const during = yield* Effect.scoped( + * Effect.gen(function*() { + * yield* Effect.updateServiceScoped( + * CurrentNumber, + * (value) => value + 1, + * { + * // Optional: when omitted, the original value is restored + * reset: (original, updated, current) => + * Math.max(original, updated, current) + 1 + * } + * ) + * return yield* CurrentNumber + * }) + * ) + * const after = yield* CurrentNumber + * + * void output.push([before, during, after]) + * }) + * + * await Effect.runPromise(program) + * output // => [[1, 2, 3]] + * ``` + * + * @see {@link updateService} for updating a service only within a wrapped effect + * + * @category providing services + * @since 4.0.0 + */ +export const updateServiceScoped: ( + service: Context.Key, + f: (value: A) => NoInfer, + options?: { + readonly reset?: ((original: A, updated: A, current: A) => A) | undefined + } | undefined +) => Effect = internal.updateServiceScoped + /** * Provides one concrete service implementation to an effect. * @@ -6235,8 +6233,9 @@ export const updateService: { * * **Example** (Providing a service value) * - * ```ts - * import { Console, Context, Effect } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect } from "effect" + * const output: Array = [] * * // Define a service for configuration * const Config = Context.Service<{ @@ -6246,8 +6245,8 @@ export const updateService: { * * const fetchData = Effect.gen(function*() { * const config = yield* Effect.service(Config) - * yield* Console.log(`Fetching from: ${config.apiUrl}`) - * yield* Console.log(`Timeout: ${config.timeout}ms`) + * yield* Effect.sync(() => { output.push(`Fetching from: ${config.apiUrl}`) }) + * yield* Effect.sync(() => { output.push(`Timeout: ${config.timeout}ms`) }) * return "data" * }) * @@ -6257,17 +6256,14 @@ export const updateService: { * timeout: 5000 * }) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Fetching from: https://api.example.com - * // Timeout: 5000ms - * // data + * void output.push(Effect.runSync(program)) + * output // => ["Fetching from: https://api.example.com", "Timeout: 5000ms", "data"] * ``` * * @see {@link provide} for providing multiple layers to an effect. * @see {@link provideServiceEffect} for acquiring the service implementation effectfully. * @see {@link provideContext} for providing a complete context. - * @category context + * @category providing services * @since 2.0.0 */ export const provideService: { @@ -6305,8 +6301,9 @@ export const provideService: { * * **Example** (Providing a service with an effect) * - * ```ts - * import { Console, Context, Effect } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect } from "effect" + * const output: Array = [] * * // Define a database connection service * interface DatabaseConnection { @@ -6316,9 +6313,8 @@ export const provideService: { * * // Effect that creates a database connection * const createConnection = Effect.gen(function*() { - * yield* Console.log("Establishing database connection...") - * yield* Effect.sleep("100 millis") // Simulate connection time - * yield* Console.log("Database connected!") + * yield* Effect.sync(() => { output.push("Establishing database connection...") }) + * yield* Effect.sync(() => { output.push("Database connected!") }) * return { * query: (sql: string) => Effect.succeed(`Result for: ${sql}`) * } @@ -6336,74 +6332,25 @@ export const provideService: { * createConnection * ) * - * Effect.runPromise(withDatabase).then(console.log) - * // Output: - * // Establishing database connection... - * // Database connected! - * // Result for: SELECT * FROM users + * void output.push(await Effect.runPromise(withDatabase)) + * output // => ["Establishing database connection...", "Database connected!", "Result for: SELECT * FROM users"] * ``` * - * @category context + * @category providing services * @since 2.0.0 */ export const provideServiceEffect: { ( service: Context.Key, - acquire: Effect + acquire: Effect, E2, R2> ): (self: Effect) => Effect | R2> ( self: Effect, service: Context.Key, - acquire: Effect + acquire: Effect, E2, R2> ): Effect | R2> } = internal.provideServiceEffect -// ----------------------------------------------------------------------------- -// References -// ----------------------------------------------------------------------------- - -/** - * Sets the concurrency level for parallel operations within an effect. - * - * **Example** (Setting local concurrency) - * - * ```ts - * import { Console, Effect } from "effect" - * - * const task = (id: number) => - * Effect.gen(function*() { - * yield* Console.log(`Task ${id} starting`) - * yield* Effect.sleep("100 millis") - * yield* Console.log(`Task ${id} completed`) - * return id - * }) - * - * // Run tasks with limited concurrency (max 2 at a time) - * const program = Effect.gen(function*() { - * const tasks = [1, 2, 3, 4, 5].map(task) - * return yield* Effect.all(tasks, { concurrency: 2 }) - * }).pipe( - * Effect.withConcurrency(2) - * ) - * - * Effect.runPromise(program).then(console.log) - * // Tasks will run with max 2 concurrent operations - * // [1, 2, 3, 4, 5] - * ``` - * - * @category references - * @since 2.0.0 - */ -export const withConcurrency: { - ( - concurrency: number | "unbounded" - ): (self: Effect) => Effect - ( - self: Effect, - concurrency: number | "unbounded" - ): Effect -} = internal.withConcurrency - // ----------------------------------------------------------------------------- // Resource management & finalization // ----------------------------------------------------------------------------- @@ -6413,28 +6360,25 @@ export const withConcurrency: { * * **Example** (Accessing the current scope) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * const currentScope = yield* Effect.scope - * yield* Console.log("Got scope for resource management") + * yield* Effect.sync(() => { output.push("Got scope for resource management") }) * * // Use the scope to manually manage resources if needed * const resource = yield* Effect.acquireRelease( - * Console.log("Acquiring resource").pipe(Effect.as("resource")), - * () => Console.log("Releasing resource") + * Effect.sync(() => { output.push("Acquiring resource") }).pipe(Effect.as("resource")), + * () => Effect.sync(() => { output.push("Releasing resource") }) * ) * * return resource * }) * - * Effect.runPromise(Effect.scoped(program)).then(console.log) - * // Output: - * // Got scope for resource management - * // Acquiring resource - * // resource - * // Releasing resource + * void output.push(Effect.runSync(Effect.scoped(program))) + * output // => ["Got scope for resource management", "Acquiring resource", "Releasing resource", "resource"] * ``` * * @category resource management @@ -6456,26 +6400,25 @@ export const scope: Effect = internal.scope * * **Example** (Running a scoped acquisition) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const resource = Effect.acquireRelease( - * Console.log("Acquiring resource").pipe(Effect.as("resource")), - * () => Console.log("Releasing resource") + * Effect.sync(() => { output.push("Acquiring resource") }).pipe(Effect.as("resource")), + * () => Effect.sync(() => { output.push("Releasing resource") }) * ) * * const program = Effect.scoped( * Effect.gen(function*() { * const res = yield* resource - * yield* Console.log(`Using ${res}`) + * yield* Effect.sync(() => { output.push(`Using ${res}`) }) * return res * }) * ) * - * Effect.runFork(program) - * // Output: "Acquiring resource" - * // Output: "Using resource" - * // Output: "Releasing resource" + * Effect.runSync(program) + * output // => ["Acquiring resource", "Using resource", "Releasing resource"] * ``` * * @category resource management @@ -6495,21 +6438,22 @@ export const scoped: ( * * **Example** (Working with an explicit scope) * - * ```ts - * import { Console, Effect, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Scope } from "effect" + * const output: Array = [] * * const program = Effect.scopedWith((scope) => * Effect.gen(function*() { - * yield* Console.log("Inside scoped context") + * yield* Effect.sync(() => { output.push("Inside scoped context") }) * * // Manually add a finalizer to the scope - * yield* Scope.addFinalizer(scope, Console.log("Manual finalizer")) + * yield* Scope.addFinalizer(scope, Effect.sync(() => { output.push("Manual finalizer") })) * * // Create a scoped resource * const resource = yield* Effect.scoped( * Effect.acquireRelease( - * Console.log("Acquiring resource").pipe(Effect.as("resource")), - * () => Console.log("Releasing resource") + * Effect.sync(() => { output.push("Acquiring resource") }).pipe(Effect.as("resource")), + * () => Effect.sync(() => { output.push("Releasing resource") }) * ) * ) * @@ -6517,13 +6461,8 @@ export const scoped: ( * }) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Inside scoped context - * // Acquiring resource - * // resource - * // Releasing resource - * // Manual finalizer + * void output.push(Effect.runSync(program)) + * output // => ["Inside scoped context", "Acquiring resource", "Releasing resource", "Manual finalizer", "resource"] * ``` * * @category resource management @@ -6552,8 +6491,9 @@ export const scopedWith: ( * * **Example** (Acquiring and releasing a resource) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * // Simulate a resource that needs cleanup * interface FileHandle { @@ -6563,17 +6503,17 @@ export const scopedWith: ( * * // Acquire a file handle * const acquire = Effect.gen(function*() { - * yield* Console.log("Opening file") + * yield* Effect.sync(() => { output.push("Opening file") }) * return { path: "/tmp/file.txt", content: "file content" } * }) * * // Release the file handle * const release = (handle: FileHandle, exit: Exit.Exit) => - * Console.log( + * Effect.sync(() => { output.push( * `Closing file ${handle.path} with exit: ${ * Exit.isSuccess(exit) ? "success" : "failure" * }` - * ) + * ) }) * * // Create a scoped resource * const resource = Effect.acquireRelease(acquire, release) @@ -6582,10 +6522,13 @@ export const scopedWith: ( * const program = Effect.scoped( * Effect.gen(function*() { * const handle = yield* resource - * yield* Console.log(`Using file: ${handle.path}`) + * yield* Effect.sync(() => { output.push(`Using file: ${handle.path}`) }) * return handle.content * }) * ) + * + * void output.push(Effect.runSync(program)) + * output // => ["Opening file", "Using file: /tmp/file.txt", "Closing file /tmp/file.txt with exit: success", "file content"] * ``` * * @see {@link acquireDisposable} for resources that implement JavaScript disposal protocols @@ -6622,22 +6565,25 @@ export const acquireRelease: ( * * **Example** (Acquiring a disposable resource) * - * ```ts - * import sqlite from "node:sqlite"; + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * - * const program = Effect.scoped( - * Effect.gen(function* () { - * // acquire database connection - * // database will be closed when the scope is closed - * const db = yield* Effect.acquireDisposable( - * Effect.sync(() => new sqlite.DatabaseSync(":memory:")) - * ) + * class Resource implements Disposable { + * [Symbol.dispose]() { + * void output.push("disposed") + * } + * } * - * const row = db.prepare("SELECT 1 AS value").get() - * yield* Effect.log(row) // { value: 1 } + * const program = Effect.scoped( + * Effect.gen(function*() { + * yield* Effect.acquireDisposable(Effect.succeed(new Resource())) + * void output.push("acquired") * }) * ) + * + * Effect.runSync(program) + * output // => ["acquired", "disposed"] * ``` * * @see {@link acquireRelease} for resources that need an explicit finalizer @@ -6677,8 +6623,9 @@ export const acquireDisposable: ( * * **Example** (Acquiring resources with cleanup) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * interface Database { * readonly connection: string @@ -6688,7 +6635,7 @@ export const acquireDisposable: ( * const program = Effect.acquireUseRelease( * // Acquire - connect to database * Effect.gen(function*() { - * yield* Console.log("Connecting to database...") + * yield* Effect.sync(() => { output.push("Connecting to database...") }) * return { * connection: "db://localhost:5432", * query: (sql: string) => Effect.succeed(`Result for: ${sql}`) @@ -6697,28 +6644,24 @@ export const acquireDisposable: ( * // Use - perform database operations * (db) => * Effect.gen(function*() { - * yield* Console.log(`Connected to ${db.connection}`) + * yield* Effect.sync(() => { output.push(`Connected to ${db.connection}`) }) * const result = yield* db.query("SELECT * FROM users") - * yield* Console.log(`Query result: ${result}`) + * yield* Effect.sync(() => { output.push(`Query result: ${result}`) }) * return result * }), * // Release - close database connection * (db, exit) => * Effect.gen(function*() { * if (Exit.isSuccess(exit)) { - * yield* Console.log(`Closing connection to ${db.connection} (success)`) + * yield* Effect.sync(() => { output.push(`Closing connection to ${db.connection} (success)`) }) * } else { - * yield* Console.log(`Closing connection to ${db.connection} (failure)`) + * yield* Effect.sync(() => { output.push(`Closing connection to ${db.connection} (failure)`) }) * } * }) * ) * - * Effect.runPromise(program) - * // Output: - * // Connecting to database... - * // Connected to db://localhost:5432 - * // Query result: Result for: SELECT * FROM users - * // Closing connection to db://localhost:5432 (success) + * await Effect.runPromise(program) + * output // => ["Connecting to database...", "Connected to db://localhost:5432", "Query result: Result for: SELECT * FROM users", "Closing connection to db://localhost:5432 (success)"] * ``` * * @see {@link acquireRelease} for scoped resources whose use happens later @@ -6746,32 +6689,30 @@ export const acquireUseRelease: ( * * **Example** (Registering scope finalizers) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * const program = Effect.scoped( * Effect.gen(function*() { * // Add a finalizer that runs when the scope closes * yield* Effect.addFinalizer((exit) => - * Console.log( + * Effect.sync(() => { output.push( * Exit.isSuccess(exit) * ? "Cleanup: Operation completed successfully" * : "Cleanup: Operation failed, cleaning up resources" - * ) + * ) }) * ) * - * yield* Console.log("Performing main operation...") + * yield* Effect.sync(() => { output.push("Performing main operation...") }) * * // This could succeed or fail * return "operation result" * }) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Performing main operation... - * // Cleanup: Operation completed successfully - * // operation result + * void output.push(Effect.runSync(program)) + * output // => ["Performing main operation...", "Cleanup: Operation completed successfully", "operation result"] * ``` * * @see {@link acquireRelease} for resource acquisition with a release finalizer @@ -6799,28 +6740,24 @@ export const addFinalizer: ( * * **Example** (Always running cleanup) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const task = Effect.gen(function*() { - * yield* Console.log("Task started") - * yield* Effect.sleep("1 second") - * yield* Console.log("Task completed") + * yield* Effect.sync(() => { output.push("Task started") }) + * yield* Effect.sync(() => { output.push("Task completed") }) * return 42 * }) * * // Ensure cleanup always runs, regardless of success or failure * const program = Effect.ensuring( * task, - * Console.log("Cleanup: This always runs!") + * Effect.sync(() => { output.push("Cleanup: This always runs!") }) * ) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Task started - * // Task completed - * // Cleanup: This always runs! - * // 42 + * void output.push(Effect.runSync(program)) + * output // => ["Task started", "Task completed", "Cleanup: This always runs!", 42] * ``` * * @category resource management @@ -6842,22 +6779,22 @@ export const ensuring: { * * **Example** (Running cleanup on failure) * - * ```ts - * import { Cause, Console, Data, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Data, Effect, Exit } from "effect" + * const output: Array = [] * * class TaskError extends Data.TaggedError("TaskError")<{ readonly message: string }> {} * - * const task = Effect.fail(new TaskError({ message: "Something went wrong" })) + * const error = new TaskError({ message: "Something went wrong" }) + * const task = Effect.fail(error) * * const program = Effect.onError( * task, - * (cause) => Console.log(`Cleanup on error: ${Cause.squash(cause)}`) + * (cause) => Effect.sync(() => { output.push(`Cleanup on error: ${Cause.squash(cause)}`) }) * ) * - * Effect.runPromise(program).catch(console.error) - * // Output: - * // Cleanup on error: TaskError: Something went wrong - * // TaskError: Something went wrong + * void output.push(Effect.runSyncExit(program)) + * output // => ["Cleanup on error: TaskError: Something went wrong", Exit.fail(error)] * ``` * * @category resource management @@ -6879,8 +6816,9 @@ export const onError: { * * **Example** (Running cleanup for selected failures) * - * ```ts - * import { Cause, Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" + * const output: Array = [] * * const task = Effect.fail("boom") * @@ -6889,9 +6827,12 @@ export const onError: { * Cause.hasFails, * (cause) => * Effect.gen(function*() { - * yield* Console.log(`Cause: ${Cause.pretty(cause)}`) + * yield* Effect.sync(() => { output.push(`Cause: ${Cause.squash(cause)}`) }) * }) * ) + * + * void output.push(Effect.runSyncExit(program)) + * output // => ["Cause: boom", Exit.fail("boom")] * ``` * * @category resource management @@ -6969,22 +6910,21 @@ export const onExitPrimitive: ( * * **Example** (Observing every exit) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * const task = Effect.succeed(42) * * const program = Effect.onExit(task, (exit) => - * Console.log( + * Effect.sync(() => { output.push( * Exit.isSuccess(exit) * ? `Task succeeded with: ${exit.value}` * : `Task failed: ${Exit.isFailure(exit) ? exit.cause : "interrupted"}` - * )) + * ) })) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Task succeeded with: 42 - * // 42 + * void output.push(Effect.runSync(program)) + * output // => ["Task succeeded with: 42", 42] * ``` * * @category resource management @@ -7006,17 +6946,21 @@ export const onExit: { * * **Example** (Observing selected exits) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * const program = Effect.onExitIf( * Effect.succeed(42), * Exit.isSuccess, * (exit) => * Exit.isSuccess(exit) - * ? Console.log(`Succeeded with: ${exit.value}`) + * ? Effect.sync(() => { output.push(`Succeeded with: ${exit.value}`) }) * : Effect.void * ) + * + * void output.push(Effect.runSync(program)) + * output // => ["Succeeded with: 42", 42] * ``` * * @category resource management @@ -7089,40 +7033,29 @@ export const onExitFilter: { * * **Example** (Memoizing an effect until invalidated) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] + * const record = (value: unknown) => Effect.sync(() => { output.push(value) }) * * let i = 1 - * const expensiveTask = Effect.promise(() => { - * console.log("expensive task...") - * return new Promise((resolve) => { - * setTimeout(() => { - * resolve(`result ${i++}`) - * }, 100) - * }) + * const expensiveTask = Effect.sync(() => { + * void output.push("expensive task...") + * return `result ${i++}` * }) * * 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:") + * void output.push("non-cached version:") + * yield* expensiveTask.pipe(Effect.andThen(record)) + * yield* expensiveTask.pipe(Effect.andThen(record)) + * void output.push("cached version:") * const cached = yield* Effect.cached(expensiveTask) - * yield* cached.pipe(Effect.andThen(Console.log)) - * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(record)) + * yield* cached.pipe(Effect.andThen(record)) * }) * - * Effect.runFork(program) - * // Output: - * // non-cached version: - * // expensive task... - * // result 1 - * // expensive task... - * // result 2 - * // cached version: - * // expensive task... - * // result 3 - * // result 3 + * await Effect.runPromise(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 @@ -7157,34 +7090,26 @@ export const cached: (self: Effect) => Effect> * * **Example** (Memoizing an effect with TTL) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] + * const record = (value: unknown) => Effect.sync(() => { output.push(value) }) * * let i = 1 - * const expensiveTask = Effect.promise(() => { - * console.log("expensive task...") - * return new Promise((resolve) => { - * setTimeout(() => { - * resolve(`result ${i++}`) - * }, 100) - * }) + * const expensiveTask = Effect.sync(() => { + * void output.push("expensive task...") + * return `result ${i++}` * }) * * 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)) + * const cached = yield* Effect.cachedWithTTL(expensiveTask, "1 hour") + * yield* cached.pipe(Effect.andThen(record)) + * yield* cached.pipe(Effect.andThen(record)) + * yield* cached.pipe(Effect.andThen(record)) * }) * - * Effect.runFork(program) - * // Output: - * // expensive task... - * // result 1 - * // result 1 - * // expensive task... - * // result 2 + * Effect.runSync(program) + * output // => ["expensive task...", "result 1", "result 1", "result 1"] * ``` * * @see {@link cached} for a similar function that caches the result @@ -7224,17 +7149,15 @@ export const cachedWithTTL: { * * **Example** (Memoizing with TTL and invalidation) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] + * const record = (value: unknown) => Effect.sync(() => { output.push(value) }) * * let i = 1 - * const expensiveTask = Effect.promise(() => { - * console.log("expensive task...") - * return new Promise((resolve) => { - * setTimeout(() => { - * resolve(`result ${i++}`) - * }, 100) - * }) + * const expensiveTask = Effect.sync(() => { + * void output.push("expensive task...") + * return `result ${i++}` * }) * * const program = Effect.gen(function*() { @@ -7242,19 +7165,14 @@ export const cachedWithTTL: { * expensiveTask, * "1 hour" * ) - * yield* cached.pipe(Effect.andThen(Console.log)) - * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(record)) + * yield* cached.pipe(Effect.andThen(record)) * yield* invalidate - * yield* cached.pipe(Effect.andThen(Console.log)) + * yield* cached.pipe(Effect.andThen(record)) * }) * - * Effect.runFork(program) - * // Output: - * // expensive task... - * // result 1 - * // result 1 - * // expensive task... - * // result 2 + * Effect.runSync(program) + * output // => ["expensive task...", "result 1", "result 1", "expensive task...", "result 2"] * ``` * * @see {@link cached} for a similar function that caches the result @@ -7278,7 +7196,7 @@ export const cachedInvalidateWithTTL: { * * **Example** (Creating an interrupted effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { @@ -7286,8 +7204,7 @@ export const cachedInvalidateWithTTL: { * yield* Effect.succeed("This won't execute and is unreachable") * }) * - * Effect.runPromise(program).catch(console.error) - * // Throws: InterruptedException + * Effect.runSyncExit(program)._tag // => "Failure" * ``` * * @category interruption @@ -7300,16 +7217,13 @@ export const interrupt: Effect = internal.interrupt * * **Example** (Allowing interruption) * - * ```ts - * import { Effect } from "effect" - * - * const longRunning = Effect.forever(Effect.succeed("working...")) - * - * const program = Effect.interruptible(longRunning) + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * - * // This effect can now be interrupted - * const fiber = Effect.runFork(program) - * // Later: fiber.interrupt() + * const program = Effect.interruptible(Effect.never).pipe( + * Effect.timeoutOption(0) + * ) + * await Effect.runPromise(program) // => Option.none() * ``` * * @category interruption @@ -7324,20 +7238,20 @@ export const interruptible: ( * * **Example** (Running cleanup on interruption) * - * ```ts - * import { Console, Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" + * const output: Array = [] * * const task = Effect.forever(Effect.succeed("working...")) * * const program = Effect.onInterrupt( * task, - * () => Console.log("Task was interrupted, cleaning up...") + * () => Effect.sync(() => { output.push("Task was interrupted, cleaning up...") }) * ) * * const fiber = Effect.runFork(program) - * // Later interrupt the task - * Effect.runFork(Fiber.interrupt(fiber)) - * // Output: Task was interrupted, cleaning up... + * await Effect.runPromise(Fiber.interrupt(fiber)) + * output // => ["Task was interrupted, cleaning up..."] * ``` * * @category interruption @@ -7358,20 +7272,19 @@ export const onInterrupt: { * * **Example** (Preventing interruption) * - * ```ts - * import { Console, Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const criticalTask = Effect.gen(function*() { - * yield* Console.log("Starting critical section...") - * yield* Effect.sleep("2 seconds") - * yield* Console.log("Critical section completed") + * yield* Effect.sync(() => { output.push("Starting critical section...") }) + * yield* Effect.sync(() => { output.push("Critical section completed") }) * }) * * const program = Effect.uninterruptible(criticalTask) * - * const fiber = Effect.runFork(program) - * // Even if interrupted, the critical task will complete - * Effect.runPromise(Fiber.interrupt(fiber)) + * Effect.runSync(program) + * output // => ["Starting critical section...", "Critical section completed"] * ``` * * @category interruption @@ -7387,25 +7300,26 @@ export const uninterruptible: ( * * **Example** (Restoring interruption in protected regions) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.uninterruptibleMask((restore) => * Effect.gen(function*() { - * yield* Console.log("Uninterruptible phase...") - * yield* Effect.sleep("1 second") - * + * yield* Effect.sync(() => { output.push("Uninterruptible phase...") }) * // Restore interruptibility for this part * yield* restore( * Effect.gen(function*() { - * yield* Console.log("Interruptible phase...") - * yield* Effect.sleep("2 seconds") + * yield* Effect.sync(() => { output.push("Interruptible phase...") }) * }) * ) * - * yield* Console.log("Back to uninterruptible") + * yield* Effect.sync(() => { output.push("Back to uninterruptible") }) * }) * ) + * + * Effect.runSync(program) + * output // => ["Uninterruptible phase...", "Interruptible phase...", "Back to uninterruptible"] * ``` * * @category interruption @@ -7423,25 +7337,26 @@ export const uninterruptibleMask: ( * * **Example** (Controlling interruptibility locally) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.interruptibleMask((restore) => * Effect.gen(function*() { - * yield* Console.log("Interruptible phase...") - * yield* Effect.sleep("1 second") - * + * yield* Effect.sync(() => { output.push("Interruptible phase...") }) * // Make this part uninterruptible * yield* restore( * Effect.gen(function*() { - * yield* Console.log("Uninterruptible phase...") - * yield* Effect.sleep("2 seconds") + * yield* Effect.sync(() => { output.push("Uninterruptible phase...") }) * }) * ) * - * yield* Console.log("Back to interruptible") + * yield* Effect.sync(() => { output.push("Back to interruptible") }) * }) * ) + * + * Effect.runSync(program) + * output // => ["Interruptible phase...", "Uninterruptible phase...", "Back to interruptible"] * ``` * * @category interruption @@ -7539,26 +7454,11 @@ export declare namespace Repeat { * * **Example** (Repeating forever) * - * ```ts - * import { Console, Effect, Fiber } from "effect" - * - * const task = Effect.gen(function*() { - * yield* Console.log("Task running...") - * yield* Effect.sleep("1 second") - * }) - * - * // This will run forever, printing every second - * const program = task.pipe(Effect.forever) - * - * // This will run forever, without yielding every iteration - * const programNoYield = task.pipe(Effect.forever({ disableYield: true })) + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" * - * // Run for 5 seconds then interrupt - * const timedProgram = Effect.gen(function*() { - * const fiber = yield* Effect.forkChild(program) - * yield* Effect.sleep("5 seconds") - * yield* Fiber.interrupt(fiber) - * }) + * const program = Effect.forever(Effect.never).pipe(Effect.timeoutOption(0)) + * await Effect.runPromise(program) // => Option.none() * ``` * * @category repetition @@ -7611,41 +7511,45 @@ export const forever: < * * **Example** (Repeating successful effects with a schedule) * - * ```ts + * ```ts import.meta.vitest * // Success Example - * import { Console, Effect, Schedule } from "effect" + * import { Effect, Schedule } from "effect" + * const output: Array = [] * - * const action = Console.log("success") - * const policy = Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("100 millis")) + * const action = Effect.sync(() => { output.push("success") }) + * const policy = Schedule.recurs(2) * const program = Effect.repeat(action, policy) * - * // Effect.runPromise(program).then((n) => console.log(`repetitions: ${n}`)) + * void output.push(Effect.runSync(program)) + * output // => ["success", "success", "success", 2] * ``` * * **Example** (Stopping repetition on failure) * - * ```ts + * ```ts import.meta.vitest * // Failure Example * import { Effect, Schedule } from "effect" + * const output: Array = [] * * let count = 0 * * // Define a callback effect that simulates an action with possible failures * const action = Effect.callback((resume) => { * if (count > 1) { - * console.log("failure") + * void output.push("failure") * resume(Effect.fail("Uh oh!")) * } else { * count++ - * console.log("success") + * void output.push("success") * resume(Effect.succeed("yay!")) * } * }) * - * const policy = Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("100 millis")) + * const policy = Schedule.recurs(2) * const program = Effect.repeat(action, policy) * - * // Effect.runPromiseExit(program).then(console.log) + * void output.push((await Effect.runPromiseExit(program))._tag) + * output // => ["success", "success", "failure", "Failure"] * ``` * * @see {@link retry} for failure-based repetition @@ -7695,17 +7599,18 @@ export const repeat: { * * **Example** (Recovering after repetition stops) * - * ```ts - * import { Console, Effect, Option, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Schedule } from "effect" + * const output: Array = [] * * let attempt = 0 * const task = Effect.gen(function*() { * attempt++ * if (attempt <= 2) { - * yield* Console.log(`Attempt ${attempt} failed`) + * yield* Effect.sync(() => { output.push(`Attempt ${attempt} failed`) }) * return yield* Effect.fail(`Error ${attempt}`) * } - * yield* Console.log(`Attempt ${attempt} succeeded`) + * yield* Effect.sync(() => { output.push(`Attempt ${attempt} succeeded`) }) * return "success" * }) * @@ -7713,12 +7618,15 @@ export const repeat: { * task, * Schedule.recurs(3), * (error, attempts) => - * Console.log( + * Effect.sync(() => { output.push( * `Final failure: ${error}, after ${ * Option.getOrElse(attempts, () => 0) * } attempts` - * ).pipe(Effect.map(() => 0)) + * ) }).pipe(Effect.map(() => 0)) * ) + * + * void output.push(Effect.runSync(program)) + * output // => ["Attempt 1 failed", "Final failure: Error 1, after 0 attempts", 0] * ``` * * @category repetition @@ -7751,7 +7659,7 @@ export const repeatOrElse: { * @see {@link all} for running the returned effects and collecting results * @see {@link replicateEffect} for repeating an effect and collecting results in one step with concurrency and discard options * - * @category collecting + * @category repetition * @since 2.0.0 */ export const replicate: { @@ -7773,16 +7681,20 @@ export const replicate: { * * **Example** (Replicating an effect) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * const results = yield* Effect.replicateEffect(3)(Effect.succeed(1)) - * yield* Console.log(results) + * yield* Effect.sync(() => { output.push(results) }) * }) + * + * Effect.runSync(program) + * output // => [[1, 1, 1]] * ``` * - * @category collecting + * @category repetition * @since 2.0.0 */ export const replicateEffect: { @@ -7825,26 +7737,19 @@ export const replicateEffect: { * * **Example** (Scheduling repeated execution) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * const output: Array = [] * * const task = Effect.gen(function*() { - * yield* Console.log("Task executing...") - * return Math.random() + * yield* Effect.sync(() => { output.push("Task executing...") }) + * return 1 * }) * - * // Repeat 3 times with 1 second delay between executions - * const program = Effect.schedule( - * task, - * Schedule.addDelay(Schedule.recurs(2), () => Effect.succeed("1 second")) - * ) + * const program = Effect.schedule(task, Schedule.recurs(2)) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // Task executing... (immediate) - * // Task executing... (after 1 second) - * // Task executing... (after 1 second) - * // Returns the count from Schedule.recurs + * void output.push(Effect.runSync(program)) + * output // => ["Task executing...", "Task executing...", 2] * ``` * * @see {@link scheduleFrom} for a variant that allows the schedule's decision @@ -7856,15 +7761,15 @@ export const replicateEffect: { export const schedule: { ( schedule: Schedule - ): (self: Effect) => Effect + ): (self: Effect) => Effect ( self: Effect, schedule: Schedule - ): Effect + ): Effect } = dual(2, ( self: Effect, schedule: Schedule -): Effect => scheduleFrom(self, undefined, schedule)) +): Effect => scheduleFrom(self, undefined, schedule)) /** * Runs an effect repeatedly according to a schedule that is initialized with a @@ -7880,12 +7785,13 @@ export const schedule: { * * **Example** (Scheduling from an initial value) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * const output: Array = [] * * const task = (input: number) => * Effect.gen(function*() { - * yield* Console.log(`Processing: ${input}`) + * yield* Effect.sync(() => { output.push(`Processing: ${input}`) }) * return input + 1 * }) * @@ -7896,8 +7802,8 @@ export const schedule: { * Schedule.recurs(2) * ) * - * Effect.runPromise(program).then(console.log) - * // Returns the schedule count + * void output.push(Effect.runSync(program)) + * output // => ["Processing: 0", "Processing: 0", 2] * ``` * * @category repetition @@ -7907,12 +7813,12 @@ export const scheduleFrom: { ( initial: Input, schedule: Schedule - ): (self: Effect) => Effect + ): (self: Effect) => Effect ( self: Effect, initial: Input, schedule: Schedule - ): Effect + ): Effect } = internalSchedule.scheduleFrom // ----------------------------------------------------------------------------- @@ -7924,14 +7830,15 @@ export const scheduleFrom: { * * **Example** (Accessing the current tracer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * const currentTracer = yield* Effect.tracer - * yield* Effect.log(`Using tracer: ${currentTracer}`) - * return "operation completed" + * return typeof currentTracer.span * }) + * + * Effect.runSync(program) // => "function" * ``` * * @category tracing @@ -7944,16 +7851,15 @@ export const tracer: Effect = internal.tracer * * **Example** (Providing a tracer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { - * yield* Effect.log("Using tracer") - * return "completed" + * const tracer = yield* Effect.tracer + * return yield* Effect.withTracer(Effect.succeed("completed"), tracer) * }) * - * // withTracer provides a tracer to the effect context - * // const traced = Effect.withTracer(program, customTracer) + * Effect.runSync(program) // => "completed" * ``` * * @category tracing @@ -7974,14 +7880,15 @@ export const withTracer: { * * **Example** (Enabling or disabling tracing) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * Effect.succeed(42).pipe( + * const program = Effect.succeed(42).pipe( * Effect.withSpan("my-span"), * // the span will not be registered with the tracer * Effect.withTracerEnabled(false) * ) + * Effect.runSync(program) // => 42 * ``` * * @category tracing @@ -7997,14 +7904,15 @@ export const withTracerEnabled: { * * **Example** (Enabling or disabling tracing timing) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * Effect.succeed(42).pipe( + * const program = Effect.succeed(42).pipe( * Effect.withSpan("my-span"), * // the span will not have timing information * Effect.withTracerTiming(false) * ) + * Effect.runSync(program) // => 42 * ``` * * @category tracing @@ -8020,13 +7928,10 @@ export const withTracerTiming: { * * **Example** (Annotating all spans) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const program = Effect.gen(function*() { - * yield* Effect.log("Doing some work...") - * return "result" - * }) + * const program = Effect.succeed("result") * * // Add single annotation * const annotated1 = Effect.annotateSpans(program, "user", "john") @@ -8037,6 +7942,8 @@ export const withTracerTiming: { * version: "1.0.0", * environment: "production" * }) + * + * Effect.runSync(Effect.all([annotated1, annotated2])) // => ['result', 'result'] * ``` * * @category tracing @@ -8066,20 +7973,19 @@ export const annotateSpans: { * * **Example** (Annotating the current span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * yield* Effect.annotateCurrentSpan("userId", "123") * yield* Effect.annotateCurrentSpan({ - * operation: "user-lookup", - * timestamp: Date.now() + * operation: "user-lookup" * }) - * yield* Effect.log("User lookup completed") * return "success" * }) * * const traced = Effect.withSpan(program, "user-operation") + * Effect.runSync(traced) // => "success" * ``` * * @category tracing @@ -8100,16 +8006,16 @@ export const annotateCurrentSpan: { * * **Example** (Reading the current span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * const span = yield* Effect.currentSpan - * yield* Effect.log(`Current span: ${span}`) - * return "done" + * return span.name * }) * * const traced = Effect.withSpan(program, "my-span") + * Effect.runSync(traced) // => "my-span" * ``` * * @category tracing @@ -8128,21 +8034,18 @@ export const currentSpan: Effect = internal.curr * * **Example** (Reading the parent span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const childOperation = Effect.gen(function*() { * const parentSpan = yield* Effect.currentParentSpan - * yield* Effect.log(`Parent span: ${parentSpan}`) - * return "child completed" + * return parentSpan._tag * }) * - * const program = Effect.gen(function*() { - * yield* Effect.withSpan(childOperation, "child-span") - * return "parent completed" - * }) + * const program = Effect.withSpan(childOperation, "child-span") * * const traced = Effect.withSpan(program, "parent-span") + * Effect.runSync(traced) // => "Span" * ``` * * @category tracing @@ -8160,23 +8063,15 @@ export const currentParentSpan: Effect = inte * * **Example** (Providing span annotations) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { - * // Add some annotations to the current span - * yield* Effect.annotateCurrentSpan("userId", "123") - * yield* Effect.annotateCurrentSpan("operation", "data-processing") - * - * // Retrieve all annotations * const annotations = yield* Effect.spanAnnotations - * - * console.log("Current span annotations:", annotations) * return annotations - * }) + * }).pipe(Effect.annotateSpans({ userId: "123", operation: "data-processing" })) * - * Effect.runPromise(program).then(console.log) - * // Output: Current span annotations: { userId: "123", operation: "data-processing" } + * Effect.runSync(program) // => { userId: '123', operation: 'data-processing' } * ``` * * @category tracing @@ -8194,15 +8089,16 @@ export const spanAnnotations: Effect>> = intern * * **Example** (Providing span links) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * // Get the current span links * const links = yield* Effect.spanLinks - * console.log(`Current span has ${links.length} links`) * return links * }) + * + * Effect.runSync(program).length // => 0 * ``` * * @category tracing @@ -8221,44 +8117,37 @@ export const spanLinks: Effect> = internal.spanLinks * * **Example** (Linking one span to another span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const parentEffect = Effect.withSpan("parent-operation")( - * Effect.succeed("parent result") - * ) - * - * const childEffect = Effect.withSpan("child-operation")( - * Effect.succeed("child result") - * ) - * - * // Link the child span to the parent span - * const program = Effect.gen(function*() { + * const program = Effect.withSpan(Effect.gen(function*() { * const parentSpan = yield* Effect.currentSpan - * const result = yield* childEffect.pipe( + * return yield* Effect.spanLinks.pipe( * Effect.linkSpans(parentSpan, { relationship: "follows" }) * ) - * return result - * }) + * }), "parent-operation") + * + * Effect.runSync(program).length // => 1 * ``` * * **Example** (Linking multiple spans at once) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * // Link multiple spans * const program = Effect.gen(function*() { - * const span1 = yield* Effect.currentSpan - * const span2 = yield* Effect.currentSpan + * const span1 = yield* Effect.makeSpan("span-1") + * const span2 = yield* Effect.makeSpan("span-2") * - * return yield* Effect.succeed("result").pipe( + * return yield* Effect.spanLinks.pipe( * Effect.linkSpans([span1, span2], { * type: "dependency", * source: "multiple-operations" * }) * ) * }) + * + * Effect.runSync(program).length // => 2 * ``` * * @category tracing @@ -8287,14 +8176,15 @@ export const linkSpans: { * * **Example** (Creating a span manually) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * const span = yield* Effect.makeSpan("my-operation") - * yield* Effect.log("Operation in progress") - * return "completed" + * return span.name * }) + * + * Effect.runSync(program) // => "my-operation" * ``` * * @category tracing @@ -8313,17 +8203,18 @@ export const makeSpan: (name: string, options?: SpanOptionsNoTrace) => Effect "scoped-operation" * ``` * * @category tracing @@ -8345,17 +8236,14 @@ export const makeSpanScoped: ( * * **Example** (Running an effect with a standalone span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.useSpan( * "user-operation", - * (span) => - * Effect.gen(function*() { - * yield* Effect.log("Processing user data") - * return "success" - * }) + * (span) => Effect.succeed(`${span.name}: success`) * ) + * Effect.runSync(program) // => "user-operation: success" * ``` * * @category tracing @@ -8371,17 +8259,15 @@ export const useSpan: { * * **Example** (Wrapping an effect in a child span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const task = Effect.gen(function*() { - * yield* Effect.log("Executing task") - * return "result" - * }) + * const task = Effect.succeed("result") * * const traced = Effect.withSpan(task, "my-task", { * attributes: { version: "1.0" } * }) + * Effect.runSync(traced) // => "result" * ``` * * @category tracing @@ -8412,16 +8298,17 @@ export const withSpan: { * * **Example** (Creating a scoped child span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.scoped( * Effect.gen(function*() { - * const task = Effect.log("Working...") + * const task = Effect.succeed("working") * yield* Effect.withSpanScoped(task, "scoped-task") * return "completed" * }) * ) + * Effect.runSync(program) // => "completed" * ``` * * @category tracing @@ -8446,15 +8333,16 @@ export const withSpanScoped: { * * **Example** (Setting a parent span) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const program = Effect.gen(function*() { * const span = yield* Effect.makeSpan("parent-span") - * const childTask = Effect.log("Child operation") + * const childTask = Effect.succeed("child operation") * yield* Effect.withParentSpan(childTask, span) * return "completed" * }) + * Effect.runSync(program) // => "completed" * ``` * * @category tracing @@ -8478,8 +8366,9 @@ export const withParentSpan: { * * **Example** (Executing a request through a resolver) * - * ```ts - * import { Console, Effect, Exit, Request, RequestResolver } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Request, RequestResolver } from "effect" + * const output: Array = [] * * interface GetUser extends Request.Request { * readonly _tag: "GetUser" @@ -8497,13 +8386,16 @@ export const withParentSpan: { * * const program = Effect.gen(function*() { * const name = yield* Effect.request(GetUser({ id: 1 }), resolver) - * yield* Console.log(name) + * yield* Effect.sync(() => { output.push(name) }) * }) + * + * await Effect.runPromise(program) + * output // => ["user-1"] * ``` * * @see {@link requestUnsafe} for the low-level entry point when you already have a `Context` and need to enqueue outside an `Effect` * - * @category requests & batching + * @category running * @since 2.0.0 */ export const request: { @@ -8530,7 +8422,7 @@ export const request: { * * @see {@link request} for the `Effect`-returning API used for normal request execution * - * @category requests & batching + * @category unsafe * @since 4.0.0 */ export const requestUnsafe: ( @@ -8571,28 +8463,21 @@ export const requestUnsafe: ( * * **Example** (Forking a child fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * - * const longRunningTask = Effect.gen(function*() { - * yield* Effect.sleep("2 seconds") - * yield* Effect.log("Task completed") - * return "result" - * }) + * const task = Effect.succeed("result") * * const program = Effect.gen(function*() { - * const fiber = yield* longRunningTask.pipe(Effect.forkChild) - * - * // or fork a fiber that starts immediately: - * yield* longRunningTask.pipe(Effect.forkChild({ startImmediately: true })) - * - * yield* Effect.log("Task forked, continuing...") + * const fiber = yield* task.pipe(Effect.forkChild) * const result = yield* Fiber.join(fiber) * return result * }) + * + * await Effect.runPromise(program) // => "result" * ``` * - * @category supervision & fibers + * @category forking * @since 4.0.0 */ export const forkChild: < @@ -8618,26 +8503,24 @@ export const forkChild: < * * **Example** (Forking into a supplied scope) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const task = Effect.gen(function*() { - * yield* Effect.sleep("10 seconds") - * return "completed" - * }) + * const task = Effect.never * * const program = Effect.scoped( * Effect.gen(function*() { * const scope = yield* Effect.scope * const fiber = yield* Effect.forkIn(task, scope) - * yield* Effect.sleep("1 second") * // Fiber will be interrupted when scope closes * return "done" * }) * ) + * + * await Effect.runPromise(program) // => "done" * ``` * - * @category supervision & fibers + * @category forking * @since 2.0.0 */ export const forkIn: { @@ -8663,32 +8546,24 @@ export const forkIn: { * * **Example** (Forking into the current scope) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const backgroundTask = Effect.gen(function*() { - * yield* Effect.sleep("5 seconds") - * yield* Effect.log("Background task completed") - * return "result" - * }) + * const backgroundTask = Effect.never * * const program = Effect.scoped( * Effect.gen(function*() { - * const fiber = yield* backgroundTask.pipe(Effect.forkScoped) - * - * // or fork a fiber that starts immediately: - * yield* backgroundTask.pipe(Effect.forkScoped({ startImmediately: true })) - * - * yield* Effect.log("Task forked in scope") - * yield* Effect.sleep("1 second") + * yield* backgroundTask.pipe(Effect.forkScoped) * * // Fiber will be interrupted when scope closes * return "scope completed" * }) * ) + * + * await Effect.runPromise(program) // => "scope completed" * ``` * - * @category supervision & fibers + * @category forking * @since 2.0.0 */ export const forkScoped: < @@ -8715,30 +8590,20 @@ export const forkScoped: < * * **Example** (Forking a detached fiber) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * - * const daemonTask = Effect.gen(function*() { - * while (true) { - * yield* Effect.sleep("1 second") - * yield* Effect.log("Daemon running...") - * } - * }) + * const daemonTask = Effect.succeed("daemon result") * * const program = Effect.gen(function*() { * const fiber = yield* daemonTask.pipe(Effect.forkDetach) - * - * // or fork a fiber that starts immediately: - * yield* daemonTask.pipe(Effect.forkDetach({ startImmediately: true })) - * - * yield* Effect.log("Daemon started") - * yield* Effect.sleep("3 seconds") - * // Daemon continues running after this effect completes - * return "main completed" + * return yield* Fiber.join(fiber) * }) + * + * await Effect.runPromise(program) // => "daemon result" * ``` * - * @category supervision & fibers + * @category forking * @since 4.0.0 */ export const forkDetach: < @@ -8777,7 +8642,7 @@ export const forkDetach: < * @see {@link forkIn} for forking into an explicit scope * @see {@link forkScoped} for forking fibers tied to the current scope * - * @category supervision & fibers + * @category sequencing * @since 2.0.0 */ export const awaitAllChildren: (self: Effect) => Effect = internal.awaitAllChildren @@ -8787,16 +8652,20 @@ export const awaitAllChildren: (self: Effect) => Effect = [] * * const program = Effect.gen(function*() { * const fiber = yield* Effect.fiber - * yield* Console.log(`Fiber id: ${fiber.id}`) + * yield* Effect.sync(() => { output.push(typeof fiber.id) }) * }) + * + * Effect.runSync(program) + * output // => ["number"] * ``` * - * @category supervision & fibers + * @category accessors * @since 4.0.0 */ export const fiber: Effect> = internal.fiber @@ -8806,21 +8675,14 @@ export const fiber: Effect> = internal.fiber * * **Example** (Accessing the current fiber id) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * const program = Effect.log("event").pipe( - * // Read the current span with the fiber id for tagging. - * Effect.andThen(Effect.all([Effect.currentSpan, Effect.fiberId])), - * Effect.withSpan("A"), - * Effect.map(([span, fiberId]) => ({ - * spanName: span.name, - * fiberId - * })) - * ) + * const program = Effect.fiberId.pipe(Effect.map((id) => typeof id)) + * Effect.runSync(program) // => "number" * ``` * - * @category supervision & fibers + * @category accessors * @since 2.0.0 */ export const fiberId: Effect = internal.fiberId @@ -8869,23 +8731,20 @@ export interface RunOptions { * * **Example** (Running an effect in the background) * - * ```ts - * import { Console, Effect, Fiber, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" + * const output: Array = [] * * // ┌─── Effect * // ▼ - * const program = Effect.repeat( - * Console.log("running..."), - * Schedule.spaced("200 millis") - * ) + * const program = Effect.sync(() => { output.push("running...") }).pipe(Effect.as("done")) * * // ┌─── RuntimeFiber * // ▼ * const fiber = Effect.runFork(program) * - * setTimeout(() => { - * Effect.runFork(Fiber.interrupt(fiber)) - * }, 500) + * void output.push(await Effect.runPromise(Fiber.join(fiber))) + * output // => ["running...", "done"] * ``` * * @category running @@ -8904,8 +8763,9 @@ export const runFork: (effect: Effect, options?: RunOptions | * * **Example** (Running with services in the background) * - * ```ts - * import { Context, Effect } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Fiber } from "effect" + * const output: Array = [] * * interface Logger { * log: (message: string) => void @@ -8914,7 +8774,7 @@ export const runFork: (effect: Effect, options?: RunOptions | * const Logger = Context.Service("Logger") * * const services = Context.make(Logger, { - * log: (message) => console.log(message) + * log: (message) => void output.push(message) * }) * * const program = Effect.gen(function*() { @@ -8924,6 +8784,8 @@ export const runFork: (effect: Effect, options?: RunOptions | * }) * * const fiber = Effect.runForkWith(services)(program) + * void output.push(await Effect.runPromise(Fiber.join(fiber))) + * output // => ["Hello from service!", "done"] * ``` * * @category running @@ -8947,8 +8809,9 @@ export const runForkWith: ( * * **Example** (Running with services and a callback) * - * ```ts - * import { Console, Context, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect } from "effect" + * const output: Array = [] * * interface Logger { * log: (message: string) => Effect.Effect @@ -8957,7 +8820,7 @@ export const runForkWith: ( * const Logger = Context.Service("Logger") * * const services = Context.make(Logger, { - * log: (message) => Console.log(message) + * log: (message) => Effect.sync(() => { output.push(message) }) * }) * * const program = Effect.gen(function*() { @@ -8966,16 +8829,15 @@ export const runForkWith: ( * return "done" * }) * - * const interrupt = Effect.runCallbackWith(services)(program, { - * onExit: (exit) => { - * if (Exit.isFailure(exit)) { - * // handle failure or interruption + * await new Promise((resolve) => { + * Effect.runCallbackWith(services)(program, { + * onExit: (exit) => { + * void output.push(exit._tag) + * resolve() * } - * } + * }) * }) - * - * // Use the interruptor if you need to cancel the fiber later. - * interrupt() + * output // => ["Started", "Success"] * ``` * * @category running @@ -8999,30 +8861,30 @@ export const runCallbackWith: ( * * **Example** (Running with a callback) * - * ```ts - * import { Console, Effect, Exit } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * yield* Console.log("working") + * yield* Effect.sync(() => { output.push("working") }) * return "done" * }) * - * const interrupt = Effect.runCallback(program, { - * onExit: (exit) => { - * Effect.runSync( - * Exit.match(exit, { - * onFailure: () => Console.log("failed"), - * onSuccess: (value) => Console.log(`success: ${value}`) - * }) - * ) - * } + * await new Promise((resolve) => { + * Effect.runCallback(program, { + * onExit: (exit) => { + * Effect.runSync( + * Exit.match(exit, { + * onFailure: () => Effect.sync(() => { output.push("failed") }), + * onSuccess: (value) => Effect.sync(() => { output.push(`success: ${value}`) }) + * }) + * ) + * resolve() + * } + * }) * }) * - * // Output: - * // working - * // success: done - * - * // interrupt() to cancel the fiber if needed + * output // => ["working", "success: done"] * ``` * * @category running @@ -9047,22 +8909,23 @@ export const runCallback: ( * * **Example** (Running a successful effect as a Promise) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * - * Effect.runPromise(Effect.succeed(1)).then(console.log) - * // Output: 1 + * await Effect.runPromise(Effect.succeed(1)) // => 1 * ``` * * **Example** (Running effects as promises) * - * ```ts + * ```ts import.meta.vitest * //Example: Handling a Failing Effect as a Rejected Promise * import { Effect } from "effect" + * const output: Array = [] * - * Effect.runPromise(Effect.fail("my error")).catch(console.error) - * // Output: - * // (FiberFailure) Error: my error + * await Effect.runPromise(Effect.fail("my error")).catch(() => { + * void output.push("rejected") + * }) + * output // => ["rejected"] * ``` * * @see {@link runPromiseExit} for a version that returns an `Exit` type instead of rejecting. @@ -9084,7 +8947,7 @@ export const runPromise: ( * * **Example** (Running with services as a promise) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" * * interface Config { @@ -9102,7 +8965,7 @@ export const runPromise: ( * return `Connecting to ${config.apiUrl}` * }) * - * Effect.runPromiseWith(context)(program).then(console.log) + * await Effect.runPromiseWith(context)(program) // => "Connecting to https://api.example.com" * ``` * * @category running @@ -9129,30 +8992,14 @@ export const runPromiseWith: ( * * **Example** (Observing promise results as Exit) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } 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 - * // } + * await Effect.runPromiseExit(Effect.succeed(1)) // => Exit.succeed(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" - * // } - * // } + * await Effect.runPromiseExit(Effect.fail("my error")) // => Exit.fail("my error") * ``` * * @see {@link runPromise} for a version that rejects on failure. @@ -9175,8 +9022,9 @@ export const runPromiseExit: ( * * **Example** (Running with services as an Exit promise) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Exit } from "effect" + * const output: Array = [] * * interface Database { * query: (sql: string) => string @@ -9193,11 +9041,11 @@ export const runPromiseExit: ( * return db.query("SELECT * FROM users") * }) * - * Effect.runPromiseExitWith(services)(program).then((exit) => { - * if (Exit.isSuccess(exit)) { - * console.log("Success:", exit.value) - * } - * }) + * const exit = await Effect.runPromiseExitWith(services)(program) + * if (Exit.isSuccess(exit)) { + * void output.push(`Success: ${exit.value}`) + * } + * output // => ["Success: Result for: SELECT * FROM users"] * ``` * * @category running @@ -9224,43 +9072,39 @@ export const runPromiseExitWith: ( * * **Example** (Running a synchronous effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * const program = Effect.sync(() => { - * console.log("Hello, World!") + * void output.push("Hello, World!") * return 1 * }) * * const result = Effect.runSync(program) - * // Output: Hello, World! - * - * console.log(result) - * // Output: 1 + * void output.push(result) + * output // => ["Hello, World!", 1] * ``` * * **Example** (Throwing for failed or async effects) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * try { * // Attempt to run an effect that fails * Effect.runSync(Effect.fail("my error")) * } catch (e) { - * console.error(e) + * void output.push("failed effect") * } - * // 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) + * void output.push("async effect") * } - * // Output: - * // (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work + * output // => ["failed effect", "async effect"] * ``` * * @see {@link runSyncExit} for a version that returns an `Exit` type instead of @@ -9280,7 +9124,7 @@ export const runSync: (effect: Effect) => A = internal.runSync * * **Example** (Running synchronously with services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect } from "effect" * * interface MathService { @@ -9299,7 +9143,7 @@ export const runSync: (effect: Effect) => A = internal.runSync * }) * * const result = Effect.runSyncWith(context)(program) - * console.log(result) // 5 + * result // => 5 * ``` * * @category running @@ -9330,50 +9174,25 @@ export const runSyncWith: ( * * **Example** (Observing synchronous results as Exit) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit } from "effect" + * + * Effect.runSyncExit(Effect.succeed(1)) // => Exit.succeed(1) * - * 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" - * // } - * // } + * Effect.runSyncExit(Effect.fail("my error")) // => Exit.fail("my error") * ``` * * **Example** (Capturing async work as a Die cause) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit } from "effect" + * + * const exit = Effect.runSyncExit(Effect.promise(() => Promise.resolve(1))) + * const isAsyncDie = Exit.hasDies(exit) && exit.cause.reasons.some( + * (reason) => Cause.isDieReason(reason) && Cause.isAsyncFiberError(reason.defect) + * ) * - * 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' - * // } - * // } - * // } + * isAsyncDie // => true * ``` * * @see {@link runSync} for a version that throws on failure. @@ -9393,8 +9212,9 @@ export const runSyncExit: (effect: Effect) => Exit.Exit = inte * * **Example** (Running synchronously with services as Exit) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Exit } from "effect" + * const output: Array = [] * * // Define a logger service * const Logger = Context.Service<{ @@ -9409,19 +9229,17 @@ export const runSyncExit: (effect: Effect) => Exit.Exit = inte * * // Prepare context * const context = Context.make(Logger, { - * log: (msg) => console.log(`[LOG] ${msg}`) + * log: (msg) => void output.push(`[LOG] ${msg}`) * }) * * const exit = Effect.runSyncExitWith(context)(program) * * if (Exit.isSuccess(exit)) { - * console.log(`Success: ${exit.value}`) + * void output.push(`Success: ${exit.value}`) * } else { - * console.log(`Failure: ${exit.cause}`) + * void output.push(`Failure: ${exit.cause}`) * } - * // Output: - * // [LOG] Computing result... - * // Success: 42 + * output // => ["[LOG] Computing result...", "Success: 42"] * ``` * * @category running @@ -9455,7 +9273,7 @@ export declare namespace fn { * * **Example** (Annotating an Effect function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced(function*( @@ -9467,11 +9285,12 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => 5 * ``` * * **Example** (Annotating a parametric Effect function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced(function*( @@ -9483,6 +9302,7 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => "hello" * ``` * * @category utility types @@ -13545,7 +13365,7 @@ export declare namespace fn { * * **Example** (Defining untraced effect functions) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced(function*( @@ -13557,11 +13377,12 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => 5 * ``` * * **Example** (Transforming the returned Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced( @@ -13575,11 +13396,12 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => "hello: 5" * ``` * * **Example** (Annotating an untraced non-parametric function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced(function*( @@ -13591,11 +13413,12 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => 5 * ``` * * **Example** (Annotating an untraced parametric function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fnUntraced(function*( @@ -13607,9 +13430,10 @@ export declare namespace fn { * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => "hello" * ``` * - * @category functions + * @category constructors * @since 3.12.0 */ export const fnUntraced: fn.Untraced = internal.fnUntraced @@ -13642,7 +13466,7 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * * **Example** (Defining traced effect functions) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fn("calculateLength")(function*(value: string) { @@ -13652,11 +13476,12 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => 5 * ``` * * **Example** (Transforming the returned Effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fn("formatLength")( @@ -13670,11 +13495,12 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => "hello: 5" * ``` * * **Example** (Binding this) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * class Counter { @@ -13694,11 +13520,12 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * // ┌─── Effect.Effect * // ▼ * const program = counter.increment(1) + * Effect.runSync(program) // => 1 * ``` * * **Example** (Annotating a traced non-parametric function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fn("calculateLength")(function*( @@ -13710,11 +13537,12 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => 5 * ``` * * **Example** (Annotating a traced parametric function) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const f = Effect.fn("succeed")(function*( @@ -13726,9 +13554,10 @@ export const fnUntraced: fn.Untraced = internal.fnUntraced * // ┌─── Effect.Effect * // ▼ * const program = f("hello") + * Effect.runSync(program) // => "hello" * ``` * - * @category functions + * @category constructors * @since 3.11.0 */ export const fn: fn.Traced & { @@ -13745,22 +13574,19 @@ export const fn: fn.Traced & { * * **Example** (Accessing the Clock service) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * * const program = Effect.clockWith((clock) => * clock.currentTimeMillis.pipe( - * Effect.map((currentTime) => `Current time is: ${currentTime}`), - * Effect.tap(Console.log) + * Effect.map(() => "Clock is available") * ) * ) * - * Effect.runFork(program) - * // Example Output: - * // Current time is: 1735484929744 + * Effect.runSync(program) // => "Clock is available" * ``` * - * @category clock + * @category accessors * @since 2.0.0 */ export const clockWith: ( @@ -13781,14 +13607,24 @@ export const clockWith: ( * * **Example** (Logging at a dynamic level) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger, References } from "effect" + * const output: Array = [] * * const logWarn = Effect.logWithLevel("Warn") * * const program = Effect.gen(function*() { - * yield* logWarn("Cache miss", { key: "user:1" }) + * yield* logWarn("Cache miss") * }) + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * const runnable = program.pipe( + * Effect.provideService(References.MinimumLogLevel, "Debug"), + * Effect.provide(Logger.layer([logger])) + * ) + * Effect.runSync(runnable) + * output // => ["Warn: Cache miss"] * ``` * * @category logging @@ -13802,23 +13638,22 @@ export const logWithLevel: (level?: Severity) => (...message: ReadonlyArray * * **Example** (Logging at the default level) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * yield* Effect.log("Starting computation") * const result = 2 + 2 * yield* Effect.log("Result:", result) - * yield* Effect.log("Multiple", "values", "can", "be", "logged") * return result * }) * - * Effect.runPromise(program).then(console.log) - * // Output: - * // timestamp=2023-... level=INFO message="Starting computation" - * // timestamp=2023-... level=INFO message="Result: 4" - * // timestamp=2023-... level=INFO message="Multiple values can be logged" - * // 4 + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * const runnable = Effect.provide(program, Logger.layer([logger])) + * void output.push(Effect.runSync(runnable)) + * output // => ["Info: Result: 4", 4] * ``` * * @category logging @@ -13831,24 +13666,20 @@ export const log: (...message: ReadonlyArray) => Effect = internal.lo * * **Example** (Logging fatal messages) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { - * try { - * // Simulate a critical system failure - * throw new Error("System memory exhausted") - * } catch (error) { - * const errorMessage = error instanceof Error ? error.message : String(error) - * yield* Effect.logFatal("Critical system failure:", errorMessage) - * yield* Effect.logFatal("System shutting down") - * } + * yield* Effect.logFatal("Critical system failure") * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=FATAL message="Critical system failure: System memory exhausted" - * // timestamp=2023-... level=FATAL message="System shutting down" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * const runnable = Effect.provide(program, Logger.layer([logger])) + * Effect.runSync(runnable) + * output // => ["Fatal: Critical system failure"] * ``` * * @category logging @@ -13861,25 +13692,19 @@ export const logFatal: (...message: ReadonlyArray) => Effect = intern * * **Example** (Logging warnings) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.logWarning("API rate limit approaching") - * yield* Effect.logWarning("Retries remaining:", 2, "Operation:", "fetchData") - * - * // Useful for non-critical issues - * const deprecated = true - * if (deprecated) { - * yield* Effect.logWarning("Using deprecated API endpoint") - * } * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=WARN message="API rate limit approaching" - * // timestamp=2023-... level=WARN message="Retries remaining: 2 Operation: fetchData" - * // timestamp=2023-... level=WARN message="Using deprecated API endpoint" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * Effect.runSync(Effect.provide(program, Logger.layer([logger]))) + * output // => ["Warn: API rate limit approaching"] * ``` * * @category logging @@ -13892,28 +13717,19 @@ export const logWarning: (...message: ReadonlyArray) => Effect = inte * * **Example** (Logging errors) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.logError("Database connection failed") - * yield* Effect.logError( - * "Error code:", - * 500, - * "Message:", - * "Internal server error" - * ) - * - * // Can be used with error objects - * const error = new Error("Something went wrong") - * yield* Effect.logError("Caught error:", error.message) * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=ERROR message="Database connection failed" - * // timestamp=2023-... level=ERROR message="Error code: 500 Message: Internal server error" - * // timestamp=2023-... level=ERROR message="Caught error: Something went wrong" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * Effect.runSync(Effect.provide(program, Logger.layer([logger]))) + * output // => ["Error: Database connection failed"] * ``` * * @category logging @@ -13926,23 +13742,19 @@ export const logError: (...message: ReadonlyArray) => Effect = intern * * **Example** (Logging information) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.logInfo("Application starting up") - * yield* Effect.logInfo("Config loaded:", "production", "Port:", 3000) - * - * // Useful for general information - * const version = "1.2.3" - * yield* Effect.logInfo("Application version:", version) * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=INFO message="Application starting up" - * // timestamp=2023-... level=INFO message="Config loaded: production Port: 3000" - * // timestamp=2023-... level=INFO message="Application version: 1.2.3" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * Effect.runSync(Effect.provide(program, Logger.layer([logger]))) + * output // => ["Info: Application starting up"] * ``` * * @category logging @@ -13955,24 +13767,23 @@ export const logInfo: (...message: ReadonlyArray) => Effect = interna * * **Example** (Logging debug messages) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger, References } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.logDebug("Debug mode enabled") - * - * const userInput = { name: "Alice", age: 30 } - * yield* Effect.logDebug("Processing user input:", userInput) - * - * // Useful for detailed diagnostic information - * yield* Effect.logDebug("Variable state:", "x=10", "y=20", "z=30") * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=DEBUG message="Debug mode enabled" - * // timestamp=2023-... level=DEBUG message="Processing user input: [object Object]" - * // timestamp=2023-... level=DEBUG message="Variable state: x=10 y=20 z=30" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * const runnable = program.pipe( + * Effect.provideService(References.MinimumLogLevel, "Debug"), + * Effect.provide(Logger.layer([logger])) + * ) + * Effect.runSync(runnable) + * output // => ["Debug: Debug mode enabled"] * ``` * * @category logging @@ -13985,27 +13796,23 @@ export const logDebug: (...message: ReadonlyArray) => Effect = intern * * **Example** (Logging trace messages) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger, References } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.logTrace("Entering function processData") - * - * // Trace detailed execution flow - * for (let i = 0; i < 3; i++) { - * yield* Effect.logTrace("Loop iteration:", i, "Processing item") - * } - * - * yield* Effect.logTrace("Exiting function processData") * }) * - * Effect.runPromise(program) - * // Output: - * // timestamp=2023-... level=TRACE message="Entering function processData" - * // timestamp=2023-... level=TRACE message="Loop iteration: 0 Processing item" - * // timestamp=2023-... level=TRACE message="Loop iteration: 1 Processing item" - * // timestamp=2023-... level=TRACE message="Loop iteration: 2 Processing item" - * // timestamp=2023-... level=TRACE message="Exiting function processData" + * const logger = Logger.make(({ logLevel, message }) => { + * void output.push(`${logLevel}: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) + * }) + * const runnable = program.pipe( + * Effect.provideService(References.MinimumLogLevel, "Trace"), + * Effect.provide(Logger.layer([logger])) + * ) + * Effect.runSync(runnable) + * output // => ["Trace: Entering function processData"] * ``` * * @category logging @@ -14018,12 +13825,13 @@ export const logTrace: (...message: ReadonlyArray) => Effect = intern * * **Example** (Adding a logger to an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * const output: Array = [] * * // Create a custom logger that logs to the console - * const customLogger = Logger.make(({ message }) => - * Effect.sync(() => console.log(`[CUSTOM]: ${message}`)) + * const customLogger = Logger.make(({ message }) => + * void output.push(`[CUSTOM]: ${Array.isArray(message) ? message.map(String).join(" ") : String(message)}`) * ) * * const program = Effect.gen(function*() { @@ -14034,8 +13842,8 @@ export const logTrace: (...message: ReadonlyArray) => Effect = intern * // Add the custom logger to the effect * const programWithLogger = Effect.withLogger(program, customLogger) * - * Effect.runPromise(programWithLogger) - * // Output includes both default and custom log outputs + * Effect.runSync(Effect.provide(programWithLogger, Logger.layer([]))) + * output // => ["[CUSTOM]: This will go to both default and custom logger"] * ``` * * @category logging @@ -14061,13 +13869,12 @@ export const withLogger = dual< * * **Example** (Adding log annotations) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * yield* Effect.log("Starting operation") - * yield* Effect.log("Processing data") - * yield* Effect.log("Operation completed") * }) * * // Add annotations to all log messages @@ -14079,8 +13886,14 @@ export const withLogger = dual< * // Also supports single key-value annotations * const singleAnnotated = Effect.annotateLogs(program, "requestId", "req-456") * - * Effect.runPromise(annotatedProgram) - * // All log messages will include the userId and operation annotations + * const logger = Logger.make(({ message }) => + * void output.push(Array.isArray(message) ? message.join(" ") : String(message)) + * ) + * const run = (effect: Effect.Effect) => + * Effect.runSync(Effect.provide(effect, Logger.layer([logger]))) + * run(annotatedProgram) + * run(singleAnnotated) + * output // => ["Starting operation", "Starting operation"] * ``` * * @category logging @@ -14114,11 +13927,11 @@ export const annotateLogs = dual< ...args: [Record] | [key: string, value: unknown] ): Effect => internal.updateService(effect, CurrentLogAnnotations, (annotations) => { - const newAnnotations = { ...annotations } + const newAnnotations = args.length === 1 ? { ...annotations, ...args[0] } : { ...annotations } if (args.length === 1) { - Object.assign(newAnnotations, args[0]) + return newAnnotations } else { - newAnnotations[args[0]] = args[1] + InternalRecord.assignProperty(newAnnotations, args[0], args[1]) } return newAnnotations }) @@ -14139,8 +13952,9 @@ export const annotateLogs = dual< * * **Example** (Adding scoped log annotations) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const program = Effect.scoped( * Effect.gen(function*() { @@ -14150,7 +13964,11 @@ export const annotateLogs = dual< * }) * ) * - * Effect.runPromise(program) + * const logger = Logger.make(({ message }) => + * void output.push(Array.isArray(message) ? message.join(" ") : String(message)) + * ) + * Effect.runSync(Effect.provide(program, Logger.layer([logger]))) + * output // => ["before", "inside scope"] * ``` * * @see {@link annotateLogs} for annotating one effect @@ -14168,8 +13986,9 @@ export const annotateLogsScoped: { * * **Example** (Adding a log span) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" + * const output: Array = [] * * const databaseOperation = Effect.gen(function*() { * yield* Effect.log("Connecting to database") @@ -14187,8 +14006,11 @@ export const annotateLogsScoped: { * * const program = Effect.withLogSpan(httpRequest, "http-handler") * - * Effect.runPromise(program) - * // All log messages will include span information showing the nested operation context + * const logger = Logger.make(({ message }) => + * void output.push(Array.isArray(message) ? message.join(" ") : String(message)) + * ) + * void output.push(Effect.runSync(Effect.provide(program, Logger.layer([logger])))) + * output // => ["Making HTTP request", "Connecting to database", "Executing query", "Processing results", "Sending response", "data"] * ``` * * @category logging @@ -14221,7 +14043,7 @@ export const withLogSpan = dual< * * **Example** (Incrementing a metric for each execution) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const counter = Metric.counter("effect_executions", { @@ -14232,16 +14054,13 @@ export const withLogSpan = dual< * Effect.track(counter) * ) * - * // This will increment the counter by 1 when executed - * Effect.runPromise(program).then(() => - * Effect.runPromise(Metric.value(counter)).then(console.log) - * // Output: { count: 1, incremental: false } - * ) + * Effect.runSync(program) + * Effect.runSync(Metric.value(counter)).count // => 1 * ``` * * **Example** (Mapping exits before updating a metric) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Metric } from "effect" * * // Track different exit types with custom mapping @@ -14258,9 +14077,11 @@ export const withLogSpan = dual< * const effect = Effect.succeed("result").pipe( * Effect.track(exitTracker, mapExitToString) * ) + * Effect.runSync(effect) + * Effect.runSync(Metric.value(exitTracker)).occurrences.get("success") // => 1 * ``` * - * @category tracking + * @category metrics * @since 4.0.0 */ export const track: { @@ -14304,7 +14125,7 @@ export const track: { * * **Example** (Counting successful results) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const successCounter = Metric.counter("successes").pipe( @@ -14315,15 +14136,13 @@ export const track: { * Effect.trackSuccesses(successCounter) * ) * - * Effect.runPromise(program).then(() => - * Effect.runPromise(Metric.value(successCounter)).then(console.log) - * // Output: { count: 1, incremental: false } - * ) + * Effect.runSync(program) + * Effect.runSync(Metric.value(successCounter)).count // => 1 * ``` * * **Example** (Mapping successes before tracking) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * // Track successful request sizes @@ -14333,13 +14152,11 @@ export const track: { * Effect.trackSuccesses(requestSizeGauge, (value: string) => value.length) * ) * - * Effect.runPromise(program).then(() => - * Effect.runPromise(Metric.value(requestSizeGauge)).then(console.log) - * // Output: { value: 12 } - * ) + * Effect.runSync(program) + * Effect.runSync(Metric.value(requestSizeGauge)).value // => 12 * ``` * - * @category tracking + * @category metrics * @since 4.0.0 */ export const trackSuccesses: { @@ -14383,7 +14200,7 @@ export const trackSuccesses: { * * **Example** (Counting expected failures) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const errorCounter = Metric.counter("errors").pipe( @@ -14394,15 +14211,13 @@ export const trackSuccesses: { * Effect.trackErrors(errorCounter) * ) * - * Effect.runPromiseExit(program).then(() => - * Effect.runPromise(Metric.value(errorCounter)).then(console.log) - * // Output: { count: 1, incremental: false } - * ) + * Effect.runSyncExit(program) + * Effect.runSync(Metric.value(errorCounter)).count // => 1 * ``` * * **Example** (Mapping errors before tracking) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, Metric } from "effect" * * class ConnectionFailedError extends Data.TaggedError("ConnectionFailedError")<{}> {} @@ -14414,13 +14229,11 @@ export const trackSuccesses: { * Effect.trackErrors(errorTypeFrequency, (error: ConnectionFailedError) => error._tag) * ) * - * Effect.runPromiseExit(program).then(() => - * Effect.runPromise(Metric.value(errorTypeFrequency)).then(console.log) - * // Output: { occurrences: Map(1) { "ConnectionFailedError" => 1 } } - * ) + * Effect.runSyncExit(program) + * Effect.runSync(Metric.value(errorTypeFrequency)).occurrences.get("ConnectionFailedError") // => 1 * ``` * - * @category tracking + * @category metrics * @since 4.0.0 */ export const trackErrors: { @@ -14464,7 +14277,7 @@ export const trackErrors: { * * **Example** (Counting defects) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const defectCounter = Metric.counter("defects").pipe( @@ -14475,15 +14288,13 @@ export const trackErrors: { * Effect.trackDefects(defectCounter) * ) * - * Effect.runPromiseExit(program).then(() => - * Effect.runPromise(Metric.value(defectCounter)).then(console.log) - * // Output: { count: 1, incremental: false } - * ) + * Effect.runSyncExit(program) + * Effect.runSync(Metric.value(defectCounter)).count // => 1 * ``` * * **Example** (Mapping defects before tracking) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * // Track defect types using frequency metric @@ -14496,13 +14307,11 @@ export const trackErrors: { * }) * ) * - * Effect.runPromiseExit(program).then(() => - * Effect.runPromise(Metric.value(defectTypeFrequency)).then(console.log) - * // Output: { occurrences: Map(1) { "Error" => 1 } } - * ) + * Effect.runSyncExit(program) + * Effect.runSync(Metric.value(defectTypeFrequency)).occurrences.get("Error") // => 1 * ``` * - * @category tracking + * @category metrics * @since 4.0.0 */ export const trackDefects: { @@ -14543,40 +14352,36 @@ export const trackDefects: { * * **Example** (Recording execution duration) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const executionTimer = Metric.timer("execution_time") * - * const program = Effect.sleep("100 millis").pipe( + * const program = Effect.succeed("done").pipe( * Effect.trackDuration(executionTimer) * ) * - * Effect.runPromise(program).then(() => - * Effect.runPromise(Metric.value(executionTimer)).then(console.log) - * // Output: { count: 1, min: 100000000, max: 100000000, sum: 100000000 } - * ) + * Effect.runSync(program) + * Effect.runSync(Metric.value(executionTimer)).count // => 1 * ``` * * **Example** (Mapping duration before tracking) * - * ```ts - * import { Duration, Effect, Metric } from "effect" + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * // Track execution time in milliseconds using custom mapping * const durationGauge = Metric.gauge("execution_millis") * - * const program = Effect.sleep("200 millis").pipe( - * Effect.trackDuration(durationGauge, (duration) => Duration.toMillis(duration)) + * const program = Effect.succeed("done").pipe( + * Effect.trackDuration(durationGauge, () => 1) * ) * - * Effect.runPromise(program).then(() => - * Effect.runPromise(Metric.value(durationGauge)).then(console.log) - * // Output: { value: 200 } - * ) + * Effect.runSync(program) + * Effect.runSync(Metric.value(durationGauge)).value // => 1 * ``` * - * @category tracking + * @category metrics * @since 4.0.0 */ export const trackDuration: { @@ -14604,9 +14409,9 @@ export const trackDuration: { f: ((duration: Duration.Duration) => Input) | undefined ): Effect => clockWith((clock) => { - const startTime = clock.currentTimeNanosUnsafe() + const startTime = clock.monotonicTimeNanosUnsafe() return onExit(self, () => { - const endTime = clock.currentTimeNanosUnsafe() + const endTime = clock.monotonicTimeNanosUnsafe() const duration = Duration.subtract( Duration.fromInputUnsafe(endTime), Duration.fromInputUnsafe(startTime) @@ -14631,7 +14436,7 @@ export const trackDuration: { * * **Example** (Building transactions) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // Transaction class for software transactional memory operations @@ -14640,9 +14445,15 @@ export const trackDuration: { * // Use transaction for coordinated state changes * return "Transaction complete" * }) + * + * const runnable = Effect.provideService(txEffect, Effect.Transaction, { + * retry: false, + * journal: new Map() + * }) + * Effect.runSync(runnable) // => "Transaction complete" * ``` * - * @category transactions + * @category services * @since 4.0.0 */ export class Transaction extends Context.Service< @@ -14678,8 +14489,9 @@ export class Transaction extends Context.Service< * * **Example** (Running a transaction) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" + * const output: Array = [] * * const program = Effect.gen(function*() { * const ref1 = yield* TxRef.make(0) @@ -14690,12 +14502,15 @@ export class Transaction extends Context.Service< * yield* TxRef.set(ref1, 10) * yield* Effect.tx(TxRef.set(ref2, 20)) * const sum = (yield* TxRef.get(ref1)) + (yield* TxRef.get(ref2)) - * console.log(`Transaction sum: ${sum}`) + * void output.push(`Transaction sum: ${sum}`) * })) * - * console.log(`Final ref1: ${yield* TxRef.get(ref1)}`) // 10 - * console.log(`Final ref2: ${yield* TxRef.get(ref2)}`) // 20 + * void output.push(`Final ref1: ${yield* TxRef.get(ref1)}`) + * void output.push(`Final ref2: ${yield* TxRef.get(ref2)}`) * }) + * + * Effect.runSync(program) + * output // => ["Transaction sum: 30", "Final ref1: 10", "Final ref2: 20"] * ``` * * @category transactions @@ -14799,31 +14614,28 @@ function clearTransaction(state: Transaction["Service"]) { * * **Example** (Retrying transactions) * - * ```ts - * import { Effect, TxRef } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { - * // create a transactional reference * const ref = yield* TxRef.make(0) + * const update = yield* Deferred.make() * - * // forks a fiber that increases the value of `ref` every 100 millis - * yield* Effect.forkChild(Effect.forever( - * // update to transactional value - * Effect.tx(TxRef.update(ref, (n) => n + 1)).pipe(Effect.delay("100 millis")) - * )) + * yield* Effect.forkChild( + * Deferred.await(update).pipe(Effect.andThen(Effect.tx(TxRef.set(ref, 1)))) + * ) * - * // the following will retry 10 times until the `ref` value is 10 - * yield* Effect.tx(Effect.gen(function*() { + * return yield* Effect.tx(Effect.gen(function*() { * const value = yield* TxRef.get(ref) - * if (value < 10) { - * yield* Effect.log(`retry due to value: ${value}`) + * if (value === 0) { + * yield* Deferred.succeed(update, undefined) * return yield* Effect.txRetry * } - * yield* Effect.log(`transaction done with value: ${value}`) + * return value * })) * }) * - * Effect.runPromise(program).catch(console.error) + * await Effect.runPromise(program) // => 1 * ``` * * @category transactions @@ -14853,7 +14665,7 @@ export declare namespace Effectify { /** * Converts a callback-based function type into an `Effect`-returning function type. * - * @category effectify + * @category utility types * @since 4.0.0 */ export type Effectify = T extends { @@ -15001,7 +14813,7 @@ export declare namespace Effectify { /** * Extracts the callback error type from a callback-based function type. * - * @category effectify + * @category utility types * @since 4.0.0 */ export type EffectifyError = T extends { @@ -15096,38 +14908,42 @@ export declare namespace Effectify { * * **Example** (Converting callbacks to effects) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" - * import * as fs from "fs" * - * // Convert Node.js readFile to an Effect - * const readFile = Effect.effectify(fs.readFile) + * const uppercase = ( + * input: string, + * callback: (error: Error | null, value?: string) => void + * ) => queueMicrotask(() => callback(null, input.toUpperCase())) * - * // Use the effectified function - * const program = readFile("package.json", "utf8") + * const effectfulUppercase = Effect.effectify(uppercase) + * const program = effectfulUppercase("hello") * - * Effect.runPromise(program).then(console.log) - * // Output: contents of package.json + * await Effect.runPromise(program) // => "HELLO" * ``` * * **Example** (Mapping callback errors to typed failures) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" - * import * as fs from "fs" * - * const readFile = Effect.effectify( - * fs.readFile, - * (error, args) => new Error(`Failed to read file ${args[0]}: ${error.message}`) + * const fail = ( + * input: string, + * callback: (error: Error | null, value?: string) => void + * ) => queueMicrotask(() => callback(new Error("unavailable"))) + * + * const effectfulFail = Effect.effectify( + * fail, + * (error, args) => new Error(`Failed to process ${args[0]}: ${error.message}`) * ) * - * const program = readFile("nonexistent.txt", "utf8") + * const program = Effect.flip(effectfulFail("hello")) * - * Effect.runPromiseExit(program).then(console.log) - * // Output: Exit.failure with custom error message + * const error = await Effect.runPromise(program) + * error.message // => "Failed to process hello: unavailable" * ``` * - * @category effectify + * @category converting * @since 4.0.0 */ export const effectify: { @@ -15172,7 +14988,7 @@ export const effectify: { * * **Example** (Constraining the success type) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // Define a constraint that the success type must be a number @@ -15180,6 +14996,7 @@ export const effectify: { * * // This works - Effect<42, never, never> extends Effect * const validEffect = satisfiesNumber(Effect.succeed(42)) + * Effect.runSync(validEffect) // => 42 * * // This would cause a TypeScript compilation error: * // const invalidEffect = satisfiesNumber(Effect.succeed("string")) @@ -15202,7 +15019,7 @@ export const satisfiesSuccessType = () => (effect: Effect * * **Example** (Constraining the error type) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect } from "effect" * * class ValidationError extends Data.TaggedError("ValidationError")<{}> {} @@ -15212,6 +15029,7 @@ export const satisfiesSuccessType = () => (effect: Effect * * // This works - Effect extends the constrained type * const validEffect = satisfiesError(Effect.fail(new ValidationError())) + * Effect.runSync(Effect.flip(validEffect))._tag // => "ValidationError" * * // This would cause a TypeScript compilation error: * // const invalidEffect = satisfiesError(Effect.fail("string error")) @@ -15234,7 +15052,7 @@ export const satisfiesErrorType = () => (effect: Effect() => (effect: Effec * * **Example** (Mapping already completed effects) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // For resolved effects, the mapping is applied immediately @@ -15277,11 +15095,13 @@ export const satisfiesServicesType = () => (effect: Effec * const mapped = Effect.mapEager(resolved, (n) => n * 2) // Applied eagerly * * // For pending effects, behaves like regular map - * const pending = Effect.delay(Effect.succeed(5), "100 millis") + * const pending = Effect.delay(Effect.succeed(5), 0) * const mappedPending = Effect.mapEager(pending, (n) => n * 2) // Uses regular map + * + * await Effect.runPromise(Effect.all([mapped, mappedPending])) // => [10, 10] * ``` * - * @category eager + * @category mapping * @since 4.0.0 */ export const mapEager: { @@ -15306,22 +15126,29 @@ export const mapEager: { * * **Example** (Mapping errors eagerly when possible) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * // For resolved failure effects, the error mapping is applied immediately * const failed = Effect.fail("original error") * const mapped = Effect.mapErrorEager(failed, (err: string) => `mapped: ${err}`) // Applied eagerly * * // For pending effects, behaves like regular mapError - * const pending = Effect.delay(Effect.fail("error"), "100 millis") + * const pending = Effect.delay(Effect.fail("error"), 0) * const mappedPending = Effect.mapErrorEager( * pending, * (err: string) => `mapped: ${err}` * ) // Uses regular mapError + * + * void output.push(await Effect.runPromise(Effect.all([ + * Effect.flip(mapped), + * Effect.flip(mappedPending) + * ]))) + * output // => [['mapped: original error', 'mapped: error']] * ``` * - * @category eager + * @category error handling * @since 4.0.0 */ export const mapErrorEager: { @@ -15345,8 +15172,9 @@ export const mapErrorEager: { * * **Example** (Mapping both channels eagerly when possible) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * // For resolved effects, the appropriate mapping is applied immediately * const success = Effect.succeed(5) @@ -15360,9 +15188,13 @@ export const mapErrorEager: { * onFailure: (err: string) => `Failed: ${err}`, * onSuccess: (n: number) => n * 2 * }) // onFailure applied eagerly + * + * void output.push(Effect.runSync(mapped)) + * void output.push(Effect.runSync(Effect.flip(mappedError))) + * output // => [10, "Failed: error"] * ``` * - * @category eager + * @category mapping * @since 4.0.0 */ export const mapBothEager: { @@ -15391,7 +15223,7 @@ export const mapBothEager: { * * **Example** (Flat mapping eagerly when possible) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // For resolved effects, the flatMap is applied immediately @@ -15399,14 +15231,16 @@ export const mapBothEager: { * const flatMapped = Effect.flatMapEager(resolved, (n) => Effect.succeed(n * 2)) // Applied eagerly * * // For pending effects, behaves like regular flatMap - * const pending = Effect.delay(Effect.succeed(5), "100 millis") + * const pending = Effect.delay(Effect.succeed(5), 0) * const flatMappedPending = Effect.flatMapEager( * pending, * (n) => Effect.succeed(n * 2) * ) // Uses regular flatMap + * + * await Effect.runPromise(Effect.all([flatMapped, flatMappedPending])) // => [10, 10] * ``` * - * @category eager + * @category sequencing * @since 4.0.0 */ export const flatMapEager: { @@ -15430,8 +15264,9 @@ export const flatMapEager: { * * **Example** (Catching failures eagerly when possible) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" + * const output: Array = [] * * // For resolved failure effects, the catch function is applied immediately * const failed = Effect.fail("original error") @@ -15448,14 +15283,21 @@ export const flatMapEager: { * ) // Returns success as-is * * // For pending effects, behaves like regular catch - * const pending = Effect.delay(Effect.fail("error"), "100 millis") + * const pending = Effect.delay(Effect.fail("error"), 0) * const recoveredPending = Effect.catchEager( * pending, * (err: string) => Effect.succeed(`recovered from: ${err}`) * ) // Uses regular catch + * + * void output.push(await Effect.runPromise(Effect.all([ + * recovered, + * unchanged, + * recoveredPending + * ]))) + * output // => [['recovered from: original error', 42, 'recovered from: error']] * ``` * - * @category eager + * @category error handling * @since 4.0.0 */ export const catchEager: { @@ -15478,7 +15320,7 @@ export const catchEager: { * * **Example** (Defining eager untraced effect functions) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * const computation = Effect.fnUntracedEager(function*() { @@ -15488,9 +15330,10 @@ export const catchEager: { * }) * * const effect = computation() // Executed immediately if all effects are sync + * Effect.runSync(effect) // => "computed eagerly" * ``` * - * @category eager + * @category constructors * @since 4.0.0 */ export const fnUntracedEager: fn.Untraced = internal.fnUntracedEager diff --git a/.context/effect/packages/effect/src/Encoding.ts b/.context/effect/packages/effect/src/Encoding.ts index 11885fb90..918f05e55 100644 --- a/.context/effect/packages/effect/src/Encoding.ts +++ b/.context/effect/packages/effect/src/Encoding.ts @@ -64,7 +64,7 @@ export type EncodingErrorTypeId = typeof EncodingErrorTypeId * message. * * @see {@link isEncodingError} for checking whether a value is an EncodingError - * @category constructors + * @category errors * @since 4.0.0 */ export class EncodingError extends Data.TaggedError("EncodingError")<{ @@ -126,15 +126,15 @@ export const isEncodingError = (u: unknown): u is EncodingError => hasProperty(u * * **Example** (Encoding Base64 strings and bytes) * - * ```ts + * ```ts import.meta.vitest * import { Encoding } from "effect" * * // Encode a string - * console.log(Encoding.encodeBase64("hello")) // "aGVsbG8=" + * Encoding.encodeBase64("hello") // => "aGVsbG8=" * * // Encode binary data * const bytes = new Uint8Array([72, 101, 108, 108, 111]) - * console.log(Encoding.encodeBase64(bytes)) // "SGVsbG8=" + * Encoding.encodeBase64(bytes) // => "SGVsbG8=" * ``` * * @see {@link decodeBase64} for decoding standard Base64 to bytes @@ -162,13 +162,10 @@ export const encodeBase64: (input: Uint8Array | string) => string = (input) => * * **Example** (Decoding Base64 bytes) * - * ```ts + * ```ts import.meta.vitest * import { Encoding, Result } from "effect" * - * const result = Encoding.decodeBase64("SGVsbG8=") - * if (Result.isSuccess(result)) { - * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111] - * } + * Encoding.decodeBase64("SGVsbG8=") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111])) * ``` * * @category decoding @@ -242,13 +239,10 @@ export const decodeBase64 = (str: string): Result.Result Result.map(decodeBase64(str), * * **Example** (Encoding URL-safe Base64) * - * ```ts + * ```ts import.meta.vitest * import { Encoding } from "effect" * * // URL-safe base64 encoding (uses - and _ instead of + and /) - * console.log(Encoding.encodeBase64Url("hello?")) // "aGVsbG8_" + * Encoding.encodeBase64Url("hello?") // => "aGVsbG8_" * * const bytes = new Uint8Array([72, 101, 108, 108, 111, 63]) - * console.log(Encoding.encodeBase64Url(bytes)) // "SGVsbG8_" + * Encoding.encodeBase64Url(bytes) // => "SGVsbG8_" * ``` * * @see {@link decodeBase64Url} for decoding URL-safe Base64 to bytes @@ -313,13 +307,10 @@ export const encodeBase64Url: (input: Uint8Array | string) => string = (input) = * * **Example** (Decoding URL-safe Base64 bytes) * - * ```ts + * ```ts import.meta.vitest * import { Encoding, Result } from "effect" * - * const result = Encoding.decodeBase64Url("SGVsbG8_") - * if (Result.isSuccess(result)) { - * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111, 63] - * } + * Encoding.decodeBase64Url("SGVsbG8_") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111, 63])) * ``` * * @category decoding @@ -373,13 +364,10 @@ export const decodeBase64Url = (str: string): Result.Result Result.succeed("hello?") * ``` * * @category decoding @@ -400,15 +388,15 @@ export const decodeBase64UrlString = (str: string) => Result.map(decodeBase64Url * * **Example** (Encoding hex strings and bytes) * - * ```ts + * ```ts import.meta.vitest * import { Encoding } from "effect" * * // Encode a string to hex - * console.log(Encoding.encodeHex("hello")) // "68656c6c6f" + * Encoding.encodeHex("hello") // => "68656c6c6f" * * // Encode binary data to hex * const bytes = new Uint8Array([72, 101, 108, 108, 111]) - * console.log(Encoding.encodeHex(bytes)) // "48656c6c6f" + * Encoding.encodeHex(bytes) // => "48656c6c6f" * ``` * * @category encoding @@ -432,13 +420,10 @@ export const encodeHex: (input: Uint8Array | string) => string = (input) => * * **Example** (Decoding hex bytes) * - * ```ts + * ```ts import.meta.vitest * import { Encoding, Result } from "effect" * - * const result = Encoding.decodeHex("48656c6c6f") - * if (Result.isSuccess(result)) { - * console.log(Array.from(result.success)) // [72, 101, 108, 108, 111] - * } + * Encoding.decodeHex("48656c6c6f") // => Result.succeed(new Uint8Array([72, 101, 108, 108, 111])) * ``` * * @category decoding @@ -494,13 +479,10 @@ export const decodeHex = (str: string): Result.Result * * **Example** (Decoding hex strings) * - * ```ts + * ```ts import.meta.vitest * import { Encoding, Result } from "effect" * - * const result = Encoding.decodeHexString("68656c6c6f") - * if (Result.isSuccess(result)) { - * console.log(result.success) // "hello" - * } + * Encoding.decodeHexString("68656c6c6f") // => Result.succeed("hello") * ``` * * @category decoding diff --git a/.context/effect/packages/effect/src/Equal.ts b/.context/effect/packages/effect/src/Equal.ts index d02b47412..0b45a6ad3 100644 --- a/.context/effect/packages/effect/src/Equal.ts +++ b/.context/effect/packages/effect/src/Equal.ts @@ -28,7 +28,7 @@ import { hasProperty } from "./Predicate.ts" * * **Example** (Implementing Equal on a class) * - * ```ts + * ```ts import.meta.vitest * import { Equal, Hash } from "effect" * * class UserId implements Equal.Equal { @@ -42,6 +42,9 @@ import { hasProperty } from "./Predicate.ts" * return Hash.string(this.id) * } * } + * + * Equal.equals(new UserId("1"), new UserId("1")) // => true + * Equal.equals(new UserId("1"), new UserId("2")) // => false * ``` * * @see {@link Equal} — the interface that uses this symbol @@ -76,7 +79,7 @@ export const symbol = "~effect/interfaces/Equal" * * **Example** (Comparing coordinates by value) * - * ```ts + * ```ts import.meta.vitest * import { Equal, Hash } from "effect" * * class Coordinate implements Equal.Equal { @@ -93,8 +96,8 @@ export const symbol = "~effect/interfaces/Equal" * } * } * - * console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(1, 2))) // true - * console.log(Equal.equals(new Coordinate(1, 2), new Coordinate(3, 4))) // false + * Equal.equals(new Coordinate(1, 2), new Coordinate(1, 2)) // => true + * Equal.equals(new Coordinate(1, 2), new Coordinate(3, 4)) // => false * ``` * * @see {@link symbol} — the property key used by the equality method @@ -139,30 +142,25 @@ export interface Equal extends Hash.Hash { * * **Example** (Comparing values) * - * ```ts + * ```ts import.meta.vitest * import { Equal } from "effect" * - * // Primitives - * console.log(Equal.equals(1, 1)) // true - * console.log(Equal.equals(NaN, NaN)) // true - * console.log(Equal.equals("a", "b")) // false + * Equal.equals(1, 1) // => true + * Equal.equals(NaN, NaN) // => true + * Equal.equals("a", "b") // => false * - * // Objects and arrays - * console.log(Equal.equals({ a: 1, b: 2 }, { a: 1, b: 2 })) // true - * console.log(Equal.equals([1, [2, 3]], [1, [2, 3]])) // true + * Equal.equals({ a: 1, b: 2 }, { a: 1, b: 2 }) // => true + * Equal.equals([1, [2, 3]], [1, [2, 3]]) // => true * - * // Dates - * console.log(Equal.equals(new Date("2024-01-01"), new Date("2024-01-01"))) // true + * Equal.equals(new Date("2024-01-01"), new Date("2024-01-01")) // => true * - * // Maps (order-independent) * const m1 = new Map([["a", 1], ["b", 2]]) * const m2 = new Map([["b", 2], ["a", 1]]) - * console.log(Equal.equals(m1, m2)) // true + * Equal.equals(m1, m2) // => true * - * // Curried form * const is5 = Equal.equals(5) - * console.log(is5(5)) // true - * console.log(is5(3)) // false + * is5(5) // => true + * is5(3) // => false * ``` * * @see {@link Equal} — the interface for custom equality @@ -235,7 +233,9 @@ function compareObjects(self: object, that: object): boolean { return false } else if (self instanceof Date) { if (!(that instanceof Date)) return false - return self.toISOString() === that.toISOString() + const selfTime = self.getTime() + const thatTime = that.getTime() + return selfTime === thatTime || (Number.isNaN(selfTime) && Number.isNaN(thatTime)) } else if (self instanceof RegExp) { if (!(that instanceof RegExp)) return false return self.toString() === that.toString() @@ -256,9 +256,21 @@ function compareObjects(self: object, that: object): boolean { } return compareArrays(self, that) } else if (ArrayBuffer.isView(self)) { - if (!ArrayBuffer.isView(that) || self.byteLength !== that.byteLength) { + const selfIsDataView = self instanceof DataView + if ( + !ArrayBuffer.isView(that) || + self.byteLength !== that.byteLength || + selfIsDataView !== (that instanceof DataView) + ) { return false } + if (selfIsDataView) { + const thatDataView = that as DataView + return compareTypedArrays( + new Uint8Array(self.buffer, self.byteOffset, self.byteLength), + new Uint8Array(thatDataView.buffer, thatDataView.byteOffset, thatDataView.byteLength) + ) + } return compareTypedArrays(self as Uint8Array, that as Uint8Array) } else if (self instanceof Map) { if (!(that instanceof Map) || self.size !== that.size) { @@ -348,10 +360,14 @@ function compareRecords( /** @internal */ export function makeCompareMap(keyEquivalence: Equivalence, valueEquivalence: Equivalence) { return function compareMaps(self: Iterable<[K, V]>, that: Iterable<[K, V]>): boolean { + const thatEntries = Array.from(that) for (const [selfKey, selfValue] of self) { let found = false - for (const [thatKey, thatValue] of that) { + for (let i = 0; i < thatEntries.length; i++) { + const [thatKey, thatValue] = thatEntries[i] if (keyEquivalence(selfKey, thatKey) && valueEquivalence(selfValue, thatValue)) { + thatEntries[i] = thatEntries[thatEntries.length - 1] + thatEntries.pop() found = true break } @@ -370,10 +386,14 @@ const compareMaps = makeCompareMap(compareBoth, compareBoth) /** @internal */ export function makeCompareSet(equivalence: Equivalence) { return function compareSets(self: Iterable, that: Iterable): boolean { + const thatValues = Array.from(that) for (const selfValue of self) { let found = false - for (const thatValue of that) { + for (let i = 0; i < thatValues.length; i++) { + const thatValue = thatValues[i] if (equivalence(selfValue, thatValue)) { + thatValues[i] = thatValues[thatValues.length - 1] + thatValues.pop() found = true break } @@ -406,7 +426,7 @@ const compareSets = makeCompareSet(compareBoth) * * **Example** (Checking Equal values) * - * ```ts + * ```ts import.meta.vitest * import { Equal, Hash } from "effect" * * class Token implements Equal.Equal { @@ -419,9 +439,9 @@ const compareSets = makeCompareSet(compareBoth) * } * } * - * console.log(Equal.isEqual(new Token("abc"))) // true - * console.log(Equal.isEqual({ x: 1 })) // false - * console.log(Equal.isEqual(42)) // false + * Equal.isEqual(new Token("abc")) // => true + * Equal.isEqual({ x: 1 }) // => false + * Equal.isEqual(42) // => false * ``` * * @see {@link Equal} — the interface being checked @@ -447,12 +467,10 @@ export const isEqual = (u: unknown): u is Equal => hasProperty(u, symbol) * * **Example** (Deduplicating with Equal semantics) * - * ```ts + * ```ts import.meta.vitest * import { Array, Equal } from "effect" * - * const eq = Equal.asEquivalence() - * const result = Array.dedupeWith([1, 2, 2, 3, 1], eq) - * console.log(result) // [1, 2, 3] + * Array.dedupeWith([1, 2, 2, 3, 1], Equal.asEquivalence()) // => [1, 2, 3] * ``` * * @see {@link equals} — the underlying comparison function @@ -481,18 +499,18 @@ export const asEquivalence: () => Equivalence = () => equals * * **Example** (Opting out of structural equality) * - * ```ts + * ```ts import.meta.vitest * import { Equal } from "effect" * * const a = { x: 1 } * const b = { x: 1 } * - * console.log(Equal.equals(a, b)) // true (structural) + * Equal.equals(a, b) // => true * * const aRef = Equal.byReference(a) - * console.log(Equal.equals(aRef, b)) // false (reference) - * console.log(Equal.equals(aRef, aRef)) // true (same reference) - * console.log(aRef.x) // 1 (proxy reads through) + * Equal.equals(aRef, b) // => false + * Equal.equals(aRef, aRef) // => true + * aRef.x // => 1 * ``` * * @see {@link byReferenceUnsafe} — same effect without a proxy (mutates the @@ -526,17 +544,17 @@ export const byReference = (obj: T): T => byReferenceUnsafe(ne * * **Example** (Marking an object for reference equality) * - * ```ts + * ```ts import.meta.vitest * import { Equal } from "effect" * * const obj1 = { a: 1, b: 2 } * const obj2 = { a: 1, b: 2 } * - * Equal.byReferenceUnsafe(obj1) + * const marked = Equal.byReferenceUnsafe(obj1) * - * console.log(Equal.equals(obj1, obj2)) // false (reference) - * console.log(Equal.equals(obj1, obj1)) // true (same reference) - * console.log(obj1 === Equal.byReferenceUnsafe(obj1)) // true (same object) + * Equal.equals(obj1, obj2) // => false + * Equal.equals(obj1, obj1) // => true + * marked === obj1 // => true * ``` * * @see {@link byReference} — safer alternative that creates a proxy diff --git a/.context/effect/packages/effect/src/Equivalence.ts b/.context/effect/packages/effect/src/Equivalence.ts index 9a5a20fc5..e1fe8ce5d 100644 --- a/.context/effect/packages/effect/src/Equivalence.ts +++ b/.context/effect/packages/effect/src/Equivalence.ts @@ -27,18 +27,18 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Defining simple number equivalence) * - * ```ts + * ```ts import.meta.vitest * import type { Equivalence } from "effect" * * const numberEq: Equivalence.Equivalence = (a, b) => a === b * - * console.log(numberEq(1, 1)) // true - * console.log(numberEq(1, 2)) // false + * numberEq(1, 1) // => true + * numberEq(1, 2) // => false * ``` * * **Example** (Defining custom object equivalence) * - * ```ts + * ```ts import.meta.vitest * import type { Equivalence } from "effect" * * interface Point { @@ -49,12 +49,12 @@ import * as Reducer from "./Reducer.ts" * const pointEq: Equivalence.Equivalence = (a, b) => * a.x === b.x && a.y === b.y * - * console.log(pointEq({ x: 1, y: 2 }, { x: 1, y: 2 })) // true + * pointEq({ x: 1, y: 2 }, { x: 1, y: 2 }) // => true * ``` * * @see {@link make} * @see {@link strictEqual} - * @category type class + * @category models * @since 2.0.0 */ export type Equivalence = (self: A, that: A) => boolean @@ -73,7 +73,7 @@ export type Equivalence = (self: A, that: A) => boolean * * **Example** (Type-level usage) * - * ```ts + * ```ts import.meta.vitest * import type { Equivalence, HKT } from "effect" * * // Used internally for type-level computations @@ -89,7 +89,7 @@ export type Equivalence = (self: A, that: A) => boolean * * @see {@link Equivalence} * @see {@link TypeLambda} - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface EquivalenceTypeLambda extends TypeLambda { @@ -113,30 +113,30 @@ export interface EquivalenceTypeLambda extends TypeLambda { * * **Example** (Case-insensitive string equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const caseInsensitive = Equivalence.make((a, b) => * a.toLowerCase() === b.toLowerCase() * ) * - * console.log(caseInsensitive("Hello", "HELLO")) // true - * console.log(caseInsensitive("foo", "bar")) // false + * caseInsensitive("Hello", "HELLO") // => true + * caseInsensitive("foo", "bar") // => false * * // Same reference optimization * const str = "test" - * console.log(caseInsensitive(str, str)) // true (fast path) + * caseInsensitive(str, str) // => true * ``` * * **Example** (Comparing numbers with tolerance) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const tolerance = Equivalence.make((a, b) => Math.abs(a - b) < 0.0001) * - * console.log(tolerance(1.0, 1.001)) // false - * console.log(tolerance(1.0, 1.00001)) // true + * tolerance(1.0, 1.001) // => false + * tolerance(1.0, 1.00001) // => true * ``` * * @see {@link strictEqual} @@ -169,26 +169,26 @@ const isStrictEquivalent = (x: unknown, y: unknown) => x === y * * **Example** (Comparing primitive types) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const strictEq = Equivalence.strictEqual() * - * console.log(strictEq(1, 1)) // true - * console.log(strictEq(1, 2)) // false - * console.log(strictEq(NaN, NaN)) // false (NaN !== NaN) + * strictEq(1, 1) // => true + * strictEq(1, 2) // => false + * strictEq(NaN, NaN) // => false * ``` * * **Example** (Comparing objects by reference) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const obj = { value: 42 } * const strictObjEq = Equivalence.strictEqual() * - * console.log(strictObjEq(obj, obj)) // true - * console.log(strictObjEq(obj, { value: 42 })) // false (different references) + * strictObjEq(obj, obj) // => true + * strictObjEq(obj, { value: 42 }) // => false * ``` * * @see {@link make} @@ -207,11 +207,11 @@ export const strictEqual: () => Equivalence = () => isStrictEquivalent * * **Example** (Comparing strings) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * - * console.log(Equivalence.String("hello", "hello")) // true - * console.log(Equivalence.String("hello", "world")) // false + * Equivalence.String("hello", "hello") // => true + * Equivalence.String("hello", "world") // => false * ``` * * @category instances @@ -228,12 +228,12 @@ export const String: Equivalence = isStrictEquivalent * * **Example** (Comparing numbers) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * - * console.log(Equivalence.Number(1, 1)) // true - * console.log(Equivalence.Number(1, 2)) // false - * console.log(Equivalence.Number(NaN, NaN)) // true + * Equivalence.Number(1, 1) // => true + * Equivalence.Number(1, 2) // => false + * Equivalence.Number(NaN, NaN) // => true * ``` * * @category instances @@ -252,11 +252,11 @@ export const Number: Equivalence = make((self, that) => * * **Example** (Comparing booleans) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * - * console.log(Equivalence.Boolean(true, true)) // true - * console.log(Equivalence.Boolean(true, false)) // false + * Equivalence.Boolean(true, true) // => true + * Equivalence.Boolean(true, false) // => false * ``` * * @category instances @@ -273,11 +273,11 @@ export const Boolean: Equivalence = isStrictEquivalent * * **Example** (Comparing bigints) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * - * console.log(Equivalence.BigInt(1n, 1n)) // true - * console.log(Equivalence.BigInt(1n, 2n)) // false + * Equivalence.BigInt(1n, 1n) // => true + * Equivalence.BigInt(1n, 2n) // => false * ``` * * @category instances @@ -301,7 +301,7 @@ export const BigInt: Equivalence = isStrictEquivalent * * **Example** (Combining name and age equivalences) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * interface Person { @@ -325,8 +325,8 @@ export const BigInt: Equivalence = isStrictEquivalent * const person2 = { name: "Alice", age: 30 } * const person3 = { name: "Alice", age: 31 } * - * console.log(personEquivalence(person1, person2)) // true - * console.log(personEquivalence(person1, person3)) // false (different age) + * personEquivalence(person1, person2) // => true + * personEquivalence(person1, person3) // => false * ``` * * @see {@link combineAll} @@ -356,7 +356,7 @@ export const combine: { * * **Example** (Combining multiple field equivalences) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * interface Point3D { @@ -384,18 +384,18 @@ export const combine: { * const point2 = { x: 1, y: 2, z: 3 } * const point3 = { x: 1, y: 2, z: 4 } * - * console.log(point3DEq(point1, point2)) // true - * console.log(point3DEq(point1, point3)) // false (different z) + * point3DEq(point1, point2) // => true + * point3DEq(point1, point3) // => false * ``` * * **Example** (Handling empty collections) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * // Empty collection always returns true * const alwaysEq = Equivalence.combineAll([]) - * console.log(alwaysEq("anything", "else")) // true + * alwaysEq("anything", "else") // => true * ``` * * @see {@link combine} @@ -431,7 +431,7 @@ export const combineAll = (collection: Iterable>): Equivalence * * **Example** (Deriving equivalence from an object property) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * interface User { @@ -450,13 +450,13 @@ export const combineAll = (collection: Iterable>): Equivalence * const user2 = { id: 1, name: "Alice Smith", email: "alice.smith@example.com" } * const user3 = { id: 2, name: "Bob", email: "bob@example.com" } * - * console.log(userByIdEq(user1, user2)) // true (same ID) - * console.log(userByIdEq(user1, user3)) // false (different ID) + * userByIdEq(user1, user2) // => true + * userByIdEq(user1, user3) // => false * ``` * * **Example** (Case-insensitive string equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const caseInsensitiveEq = Equivalence.mapInput( @@ -464,8 +464,8 @@ export const combineAll = (collection: Iterable>): Equivalence * (s: string) => s.toLowerCase() * ) * - * console.log(caseInsensitiveEq("Hello", "HELLO")) // true - * console.log(caseInsensitiveEq("Hello", "World")) // false + * caseInsensitiveEq("Hello", "HELLO") // => true + * caseInsensitiveEq("Hello", "World") // => false * ``` * * @see {@link combine} @@ -499,7 +499,7 @@ export const mapInput: { * * **Example** (Comparing homogeneous tuples) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const stringTupleEq = Equivalence.Tuple([ @@ -512,13 +512,13 @@ export const mapInput: { * const tuple2 = ["hello", "world", "test"] as const * const tuple3 = ["hello", "world", "different"] as const * - * console.log(stringTupleEq(tuple1, tuple2)) // true - * console.log(stringTupleEq(tuple1, tuple3)) // false (different third element) + * stringTupleEq(tuple1, tuple2) // => true + * stringTupleEq(tuple1, tuple3) // => false * ``` * * **Example** (Comparing tuples with custom equivalences) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const caseInsensitive = Equivalence.mapInput( @@ -532,9 +532,7 @@ export const mapInput: { * caseInsensitive * ]) * - * console.log( - * customTupleEq(["Hello", "World", "Test"], ["HELLO", "WORLD", "TEST"]) - * ) // true + * customTupleEq(["Hello", "World", "Test"], ["HELLO", "WORLD", "TEST"]) // => true * ``` * * @category combinators @@ -588,19 +586,19 @@ export { * * **Example** (Comparing number arrays) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const numberArrayEq = Equivalence.Array(Equivalence.strictEqual()) * - * console.log(numberArrayEq([1, 2, 3], [1, 2, 3])) // true - * console.log(numberArrayEq([1, 2, 3], [1, 2, 4])) // false - * console.log(numberArrayEq([1, 2], [1, 2, 3])) // false (different length) + * numberArrayEq([1, 2, 3], [1, 2, 3]) // => true + * numberArrayEq([1, 2, 3], [1, 2, 4]) // => false + * numberArrayEq([1, 2], [1, 2, 3]) // => false * ``` * * **Example** (Case-insensitive string array) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const caseInsensitive = Equivalence.mapInput( @@ -609,9 +607,9 @@ export { * ) * const stringArrayEq = Equivalence.Array(caseInsensitive) * - * console.log(stringArrayEq(["Hello", "World"], ["HELLO", "WORLD"])) // true - * console.log(stringArrayEq(["Hello"], ["Hi"])) // false - * console.log(stringArrayEq([], [])) // true (empty arrays) + * stringArrayEq(["Hello", "World"], ["HELLO", "WORLD"]) // => true + * stringArrayEq(["Hello"], ["Hi"]) // => false + * stringArrayEq([], []) // => true * ``` * * @see {@link Tuple} @@ -640,7 +638,7 @@ export { * * **Example** (Comparing structs with different equivalences per field) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * interface Person { @@ -664,13 +662,13 @@ export { * const person2 = { name: "ALICE", age: 30, email: "ALICE@EXAMPLE.COM" } * const person3 = { name: "Alice", age: 31, email: "alice@example.com" } * - * console.log(personEq(person1, person2)) // true (case-insensitive match) - * console.log(personEq(person1, person3)) // false (different age) + * personEq(person1, person2) // => true + * personEq(person1, person3) // => false * ``` * * **Example** (Comparing specific fields) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const nameAgeEq = Equivalence.Struct({ @@ -681,7 +679,7 @@ export { * // Only compares name and age, ignores other properties * const obj1 = { name: "Alice", age: 30, extra: "ignored" } * const obj2 = { name: "Alice", age: 30, extra: "different" } - * console.log(nameAgeEq(obj1, obj2)) // true + * nameAgeEq(obj1, obj2) // => true * ``` * * @see {@link Record} @@ -721,7 +719,7 @@ export function Struct>>( * * **Example** (Defining records with string values) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const stringRecordEq = Equivalence.Record(Equivalence.strictEqual()) @@ -731,14 +729,14 @@ export function Struct>>( * const record3 = { a: "hello", b: "different" } * const record4 = { a: "hello" } // missing key 'b' * - * console.log(stringRecordEq(record1, record2)) // true - * console.log(stringRecordEq(record1, record3)) // false - * console.log(stringRecordEq(record1, record4)) // false (different keys) + * stringRecordEq(record1, record2) // => true + * stringRecordEq(record1, record3) // => false + * stringRecordEq(record1, record4) // => false * ``` * * **Example** (Defining records with number values) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const numberRecordEq = Equivalence.Record(Equivalence.strictEqual()) @@ -747,8 +745,8 @@ export function Struct>>( * const scores2 = { alice: 100, bob: 85 } * const scores3 = { alice: 100, bob: 90 } * - * console.log(numberRecordEq(scores1, scores2)) // true - * console.log(numberRecordEq(scores1, scores3)) // false + * numberRecordEq(scores1, scores2) // => true + * numberRecordEq(scores1, scores3) // => false * ``` * * @category combinators @@ -787,7 +785,7 @@ export function Record(value: Equivalence): Equivalence() @@ -798,8 +796,8 @@ export function Record(value: Equivalence): Equivalence true + * combined(1, 1.5) // => false * ``` * * @see {@link combine} Combine two equivalences @@ -831,7 +829,7 @@ export function makeReducer() { * * **Example** (Comparing Date values) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const d1 = new Date("2020-01-01T00:00:00.000Z") @@ -840,22 +838,22 @@ export function makeReducer() { * const invalidDate1 = new Date("foo") * const invalidDate2 = new Date("bar") * - * console.log(Equivalence.Date(d1, d2)) // true - * console.log(Equivalence.Date(d1, d3)) // false - * console.log(Equivalence.Date(invalidDate1, invalidDate2)) // true - * console.log(Equivalence.Date(invalidDate1, d1)) // false + * Equivalence.Date(d1, d2) // => true + * Equivalence.Date(d1, d3) // => false + * Equivalence.Date(invalidDate1, invalidDate2) // => true + * Equivalence.Date(invalidDate1, d1) // => false * ``` * * **Example** (Comparing reference and value equality) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence } from "effect" * * const d1 = new Date(0) * const d2 = new Date(0) * - * console.log(d1 === d2) // false (different references) - * console.log(Equivalence.Date(d1, d2)) // true (same time value) + * d1 === d2 // => false + * Equivalence.Date(d1, d2) // => true * ``` * * @see {@link Number} for the numeric equivalence applied to each `Date#getTime()` result diff --git a/.context/effect/packages/effect/src/ErrorReporter.ts b/.context/effect/packages/effect/src/ErrorReporter.ts index 1fbc6ccc8..586429547 100644 --- a/.context/effect/packages/effect/src/ErrorReporter.ts +++ b/.context/effect/packages/effect/src/ErrorReporter.ts @@ -70,7 +70,7 @@ export const TypeId: TypeId = "~effect/ErrorReporter" * @see {@link report} for manually reporting a `Cause` * @see {@link Effect.withErrorReporting} for reporting failures from an effect * - * @category models + * @category services * @since 4.0.0 */ export interface ErrorReporter { @@ -97,17 +97,24 @@ export interface ErrorReporter { * and resolves the `ignore`, `severity`, and `attributes` annotations on * each error before invoking your callback. * - * **Example** (Forwarding errors to the console) + * **Example** (Forwarding errors to a callback) * - * ```ts - * import { ErrorReporter } from "effect" + * ```ts import.meta.vitest + * import { Effect, ErrorReporter } from "effect" * - * // Forward every failure to the console - * const consoleReporter = ErrorReporter.make( - * ({ error, severity, attributes }) => { - * console.error(`[${severity}]`, error.message, attributes) - * } + * const reports: Array<{ message: string; severity: string; attributes: object }> = [] + * const reporter = ErrorReporter.make(({ error, severity, attributes }) => { + * reports.push({ message: error.message, severity, attributes }) + * }) + * + * const program = Effect.fail(new Error("boom")).pipe( + * Effect.withErrorReporting, + * Effect.provide(ErrorReporter.layer([reporter])), + * Effect.exit * ) + * + * await Effect.runPromise(program) + * reports // => [{ message: "boom", severity: "Info", attributes: {} }] * ``` * * @see {@link layer} for registering reporters in the environment @@ -163,7 +170,7 @@ export const make = ( * Use when you need to read or replace the current set of error reporters * directly. * - * @category references + * @category services * @since 4.0.0 */ export const CurrentErrorReporters: Context.Reference> = references.CurrentErrorReporters @@ -185,33 +192,37 @@ export const CurrentErrorReporters: Context.Reference * * **Example** (Providing error reporters) * - * ```ts + * ```ts import.meta.vitest * import { Effect, ErrorReporter } from "effect" * - * const consoleReporter = ErrorReporter.make(({ error, severity }) => { - * console.error(`[${severity}]`, error.message) + * const reports: Array = [] + * const firstReporter = ErrorReporter.make(({ error, severity }) => { + * reports.push(`[${severity}] ${error.message}`) * }) - * - * const metricsReporter = ErrorReporter.make(({ severity }) => { - * // increment an error counter by severity + * const secondReporter = ErrorReporter.make(({ error, severity }) => { + * reports.push(`${severity}: ${error.message}`) * }) * * // Replace all existing reporters * const ReporterLive = ErrorReporter.layer([ - * consoleReporter, - * metricsReporter + * firstReporter, + * secondReporter * ]) * * // Add to existing reporters instead of replacing * const ReporterMerged = ErrorReporter.layer( - * [metricsReporter], + * [secondReporter], * { mergeWithExisting: true } * ) * * const program = Effect.fail("boom").pipe( * Effect.withErrorReporting, - * Effect.provide(ReporterLive) + * Effect.provide(ReporterLive), + * Effect.exit * ) + * + * await Effect.runPromise(program) + * reports // => ["[Info] boom", "Info: boom"] * ``` * * @see {@link make} for creating an `ErrorReporter` from a callback @@ -255,18 +266,25 @@ export const layer = < * * **Example** (Reporting a cause manually) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, ErrorReporter } from "effect" * - * // Log the cause for monitoring, then continue with a fallback + * const messages: Array = [] * const program = Effect.gen(function*() { * const cause = Cause.fail("something went wrong") * yield* ErrorReporter.report(cause) * return "fallback value" * }) + * + * const reporter = ErrorReporter.make(({ error }) => messages.push(error.message)) + * const output = await Effect.runPromise( + * Effect.provide(program, ErrorReporter.layer([reporter])) + * ) + * messages // => ["something went wrong"] + * output // => "fallback value" * ``` * - * @category Reporting + * @category logging * @since 4.0.0 */ export const report = (cause: Cause.Cause): Effect.Effect => @@ -347,12 +365,14 @@ export type ignore = "~effect/ErrorReporter/ignore" * * **Example** (Marking errors as ignored) * - * ```ts + * ```ts import.meta.vitest * import { Data, ErrorReporter } from "effect" * * class NotFoundError extends Data.TaggedError("NotFoundError")<{}> { * readonly [ErrorReporter.ignore] = true * } + * + * ErrorReporter.isIgnored(new NotFoundError()) // => true * ``` * * @see {@link isIgnored} for checking whether a value carries this annotation @@ -375,7 +395,7 @@ export const ignore: ignore = "~effect/ErrorReporter/ignore" * * @see {@link ignore} for the annotation key this predicate reads * - * @category annotations + * @category predicates * @since 4.0.0 */ export const isIgnored = (u: unknown): boolean => @@ -414,12 +434,14 @@ export type severity = "~effect/ErrorReporter/severity" * * **Example** (Setting error severity annotations) * - * ```ts + * ```ts import.meta.vitest * import { Data, ErrorReporter } from "effect" * * class DeprecationWarning extends Data.TaggedError("DeprecationWarning")<{}> { * readonly [ErrorReporter.severity] = "Warn" as const * } + * + * ErrorReporter.getSeverity(new DeprecationWarning()) // => "Warn" * ``` * * @see {@link getSeverity} for reading the severity stored under this key @@ -488,7 +510,7 @@ export type attributes = "~effect/ErrorReporter/attributes" * * **Example** (Setting error attributes) * - * ```ts + * ```ts import.meta.vitest * import { Data, ErrorReporter } from "effect" * * class PaymentError extends Data.TaggedError("PaymentError")<{ @@ -498,6 +520,8 @@ export type attributes = "~effect/ErrorReporter/attributes" * orderId: this.orderId * } * } + * + * ErrorReporter.getAttributes(new PaymentError({ orderId: "order-123" })) // => { orderId: "order-123" } * ``` * * @see {@link ignore} for suppressing reports for expected object errors diff --git a/.context/effect/packages/effect/src/ExecutionPlan.ts b/.context/effect/packages/effect/src/ExecutionPlan.ts index fd00503b5..236004359 100644 --- a/.context/effect/packages/effect/src/ExecutionPlan.ts +++ b/.context/effect/packages/effect/src/ExecutionPlan.ts @@ -10,7 +10,9 @@ * @since 3.16.0 */ import type { NonEmptyReadonlyArray } from "./Array.ts" +import type * as Cause from "./Cause.ts" import * as Context from "./Context.ts" +import type * as Duration from "./Duration.ts" import type * as Effect from "./Effect.ts" import { constant } from "./Function.ts" import * as effect from "./internal/effect.ts" @@ -66,41 +68,20 @@ export const isExecutionPlan = (u: unknown): u is ExecutionPlan => Predicat * * **Example** (Defining fallback execution steps) * - * ```ts - * import { Effect, ExecutionPlan, Schedule } from "effect" - * import type { Layer } from "effect" - * import type { LanguageModel } from "effect/unstable/ai" - * - * declare const layerBad: Layer.Layer - * declare const layerGood: Layer.Layer + * ```ts import.meta.vitest + * import { Context, ExecutionPlan } from "effect" * * 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) + * provide: Context.empty(), + * attempts: 2 * }, - * // Finally try with the good layer. - * // - * // If `attempts` is omitted, the plan will only attempt once, unless a schedule is provided. * { - * provide: layerGood + * provide: Context.empty() * } * ) * - * declare const effect: Effect.Effect< - * void, - * never, - * LanguageModel.LanguageModel - * > - * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ThePlan.steps.map((step) => step.attempts ?? 1) // => [2, 1] * ``` * * @category models @@ -166,41 +147,20 @@ export type ConfigBase = { * * **Example** (Creating an execution plan) * - * ```ts - * import { Effect, ExecutionPlan, Schedule } from "effect" - * import type { Layer } from "effect" - * import type { LanguageModel } from "effect/unstable/ai" - * - * declare const layerBad: Layer.Layer - * declare const layerGood: Layer.Layer + * ```ts import.meta.vitest + * import { Context, ExecutionPlan } from "effect" * * 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) + * provide: Context.empty(), + * attempts: 2 * }, - * // 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 + * provide: Context.empty() * } * ) * - * declare const effect: Effect.Effect< - * void, - * never, - * LanguageModel.LanguageModel - * > - * const withPlan: Effect.Effect = Effect.withExecutionPlan(effect, ThePlan) + * ThePlan.steps.length // => 2 * ``` * * @category constructors @@ -221,7 +181,7 @@ export const make = >( | (Steps[number]["schedule"] extends Schedule.Schedule ? R : never) }> => makeProto(steps.map((options, i) => { - if (options.attempts && options.attempts < 1) { + if (options.attempts !== undefined && options.attempts < 1) { throw new Error(`ExecutionPlan.make: step[${i}].attempts must be greater than 0`) } return { @@ -405,7 +365,7 @@ export interface Metadata { * Use to read the active plan step and attempt while code is running under an * execution plan. * - * @category metadata + * @category services * @since 4.0.0 */ export const CurrentMetadata = Context.Reference("effect/ExecutionPlan/CurrentMetadata", { @@ -414,3 +374,78 @@ export const CurrentMetadata = Context.Reference("effect/ExecutionPlan stepIndex: 0 }) }) + +/** + * Lifecycle event emitted before an execution-plan attempt runs. + * + * **Details** + * + * `attempt` is the cumulative 1-based attempt number across all steps and + * matches `CurrentMetadata.attempt` for the same attempt. `stepAttempt` is the + * 1-based attempt number within the current step, and `stepIndex` is the + * 0-based index of the step being attempted. + * + * @category models + * @since 4.0.0 + */ +export interface AttemptStart { + readonly _tag: "AttemptStart" + readonly attempt: number + readonly stepAttempt: number + readonly stepIndex: number +} + +/** + * Lifecycle event emitted when an execution-plan attempt succeeds. + * + * **Details** + * + * A successful attempt completes the plan, so this is always the final event. + * `duration` is the elapsed time of the attempt. + * + * @category models + * @since 4.0.0 + */ +export interface AttemptSuccess { + readonly _tag: "AttemptSuccess" + readonly attempt: number + readonly stepAttempt: number + readonly stepIndex: number + readonly duration: Duration.Duration +} + +/** + * Lifecycle event emitted when an execution-plan attempt fails. + * + * **Details** + * + * `cause` holds the full failure cause, so defects and interruption are + * reported as well as expected errors. Whether the plan retries or fails over + * afterwards is decided by the step's `attempts`, `while`, and `schedule`; a + * following `AttemptStart` indicates another attempt was made. + * + * @category models + * @since 4.0.0 + */ +export interface AttemptFailure { + readonly _tag: "AttemptFailure" + readonly attempt: number + readonly stepAttempt: number + readonly stepIndex: number + readonly duration: Duration.Duration + readonly cause: Cause.Cause +} + +/** + * Union of the lifecycle events emitted while an execution plan runs. + * + * **Details** + * + * Every `AttemptStart` is followed by exactly one terminal event, either + * `AttemptSuccess` or `AttemptFailure`. An interrupted attempt emits + * `AttemptFailure` with the interruption cause. + * + * @category models + * @since 4.0.0 + */ +export type Event = AttemptStart | AttemptSuccess | AttemptFailure diff --git a/.context/effect/packages/effect/src/Exit.ts b/.context/effect/packages/effect/src/Exit.ts index d3e45bdad..889d81d48 100644 --- a/.context/effect/packages/effect/src/Exit.ts +++ b/.context/effect/packages/effect/src/Exit.ts @@ -37,16 +37,16 @@ const TypeId = core.ExitTypeId * * **Example** (Pattern matching on an Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * * const success: Exit.Exit = Exit.succeed(42) * const failure: Exit.Exit = Exit.fail("error") * - * const result = Exit.match(success, { + * Exit.match(success, { * onSuccess: (value) => `Got value: ${value}`, * onFailure: (cause) => `Got error: ${cause}` - * }) + * }) // => "Got value: 42" * ``` * * @see {@link Success} for the success case @@ -99,14 +99,13 @@ export declare namespace Exit { * * **Example** (Accessing the success value) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * * const success = Exit.succeed(42) * * if (Exit.isSuccess(success)) { - * console.log(success._tag) // "Success" - * console.log(success.value) // 42 + * success.value // => 42 * } * ``` * @@ -136,14 +135,13 @@ export interface Success extends Exit.Proto { * * **Example** (Accessing the failure cause) * - * ```ts - * import { Exit } from "effect" + * ```ts import.meta.vitest + * import { Cause, Exit } from "effect" * * const failure = Exit.fail("something went wrong") * * if (Exit.isFailure(failure)) { - * console.log(failure._tag) // "Failure" - * console.log(failure.cause) // Cause representing the error + * failure.cause // => Cause.fail("something went wrong") * } * ``` * @@ -173,12 +171,12 @@ export interface Failure extends Exit.Proto { * * **Example** (Checking if a value is an Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * console.log(Exit.isExit(Exit.succeed(42))) // true - * console.log(Exit.isExit(Exit.fail("err"))) // true - * console.log(Exit.isExit("not an exit")) // false + * Exit.isExit(Exit.succeed(42)) // => true + * Exit.isExit(Exit.fail("err")) // => true + * Exit.isExit("not an exit") // => false * ``` * * @see {@link isSuccess} to check for a successful Exit @@ -203,11 +201,10 @@ export const isExit: (u: unknown) => u is Exit = core.isExit * * **Example** (Creating a successful Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.succeed(42) - * console.log(Exit.isSuccess(exit)) // true + * Exit.succeed(42) // => Exit.succeed(42) * ``` * * @see {@link fail} to create a failed Exit @@ -234,12 +231,10 @@ export const succeed: (a: A) => Exit = core.exitSucceed * * **Example** (Creating a failed Exit from a Cause) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Exit } from "effect" * - * const cause = Cause.fail("Something went wrong") - * const exit = Exit.failCause(cause) - * console.log(Exit.isFailure(exit)) // true + * Exit.failCause(Cause.fail("Something went wrong")) // => Exit.fail("Something went wrong") * ``` * * @see {@link fail} to create a Failure from a plain error value @@ -265,11 +260,10 @@ export const failCause: (cause: Cause.Cause) => Exit = core.exit * * **Example** (Creating a failed Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.fail("Something went wrong") - * console.log(Exit.isFailure(exit)) // true + * Exit.fail("Something went wrong") // => Exit.fail("Something went wrong") * ``` * * @see {@link succeed} to create a successful Exit @@ -298,11 +292,10 @@ export const fail: (e: E) => Exit = core.exitFail * * **Example** (Creating a defect Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.die(new Error("Unexpected error")) - * console.log(Exit.isFailure(exit)) // true + * Exit.die("Unexpected error") // => Exit.die("Unexpected error") * ``` * * @see {@link fail} to create a Failure from a typed error @@ -327,12 +320,10 @@ export const die: (defect: unknown) => Exit = core.exitDie * * **Example** (Creating an interruption Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.interrupt(123) - * console.log(Exit.isFailure(exit)) // true - * console.log(Exit.hasInterrupts(exit)) // true + * Exit.interrupt(123) // => Exit.interrupt(123) * ``` * * @see {@link hasInterrupts} to check whether an Exit contains interruptions @@ -358,11 +349,10 @@ export { * * **Example** (Referencing the void Exit) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.void - * console.log(Exit.isSuccess(exit)) // true + * Exit.void // => Exit.succeed(undefined) * ``` * * @see {@link succeed} to create a success with a specific value @@ -384,13 +374,13 @@ export { * * **Example** (Narrowing to success) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * * const exit = Exit.succeed(42) * * if (Exit.isSuccess(exit)) { - * console.log(exit.value) // 42 + * exit.value // => 42 * } * ``` * @@ -412,13 +402,13 @@ export const isSuccess: (self: Exit) => self is Success = effe * * **Example** (Narrowing to failure) * - * ```ts - * import { Exit } from "effect" + * ```ts import.meta.vitest + * import { Cause, Exit } from "effect" * * const exit = Exit.fail("error") * * if (Exit.isFailure(exit)) { - * console.log(exit.cause) + * exit.cause // => Cause.fail("error") * } * ``` * @@ -444,12 +434,12 @@ export const isFailure: (self: Exit) => self is Failure = effe * * **Example** (Checking for typed errors) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * console.log(Exit.hasFails(Exit.fail("err"))) // true - * console.log(Exit.hasFails(Exit.die(new Error("bug")))) // false - * console.log(Exit.hasFails(Exit.succeed(42))) // false + * Exit.hasFails(Exit.fail("err")) // => true + * Exit.hasFails(Exit.die("bug")) // => false + * Exit.hasFails(Exit.succeed(42)) // => false * ``` * * @see {@link hasDies} to check for defects @@ -474,12 +464,12 @@ export const hasFails: (self: Exit) => self is Failure = effec * * **Example** (Checking for defects) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * console.log(Exit.hasDies(Exit.die(new Error("bug")))) // true - * console.log(Exit.hasDies(Exit.fail("err"))) // false - * console.log(Exit.hasDies(Exit.succeed(42))) // false + * Exit.hasDies(Exit.die("bug")) // => true + * Exit.hasDies(Exit.fail("err")) // => false + * Exit.hasDies(Exit.succeed(42)) // => false * ``` * * @see {@link hasFails} to check for typed errors @@ -504,12 +494,12 @@ export const hasDies: (self: Exit) => self is Failure = effect * * **Example** (Checking for interruptions) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * console.log(Exit.hasInterrupts(Exit.interrupt(1))) // true - * console.log(Exit.hasInterrupts(Exit.fail("err"))) // false - * console.log(Exit.hasInterrupts(Exit.succeed(42))) // false + * Exit.hasInterrupts(Exit.interrupt(1)) // => true + * Exit.hasInterrupts(Exit.fail("err")) // => false + * Exit.hasInterrupts(Exit.succeed(42)) // => false * ``` * * @see {@link hasFails} to check for typed errors @@ -540,13 +530,10 @@ export const hasInterrupts: (self: Exit) => self is Failure = * * **Example** (Filtering for success) * - * ```ts + * ```ts import.meta.vitest * import { Exit, Result } from "effect" * - * const exit = Exit.succeed(42) - * const result = Exit.filterSuccess(exit) - * - * console.log(Result.isSuccess(result)) // true + * Exit.filterSuccess(Exit.succeed(42)) // => Result.succeed(Exit.succeed(42)) * ``` * * @see {@link filterFailure} for the inverse @@ -580,13 +567,10 @@ export const filterSuccess: ( * * **Example** (Filtering for the value) * - * ```ts + * ```ts import.meta.vitest * import { Exit, Result } from "effect" * - * const exit = Exit.succeed(42) - * const result = Exit.filterValue(exit) - * - * console.log(Result.isSuccess(result) && result.success) // 42 + * Exit.filterValue(Exit.succeed(42)) // => Result.succeed(42) * ``` * * @see {@link filterSuccess} to get the full Success object @@ -617,13 +601,10 @@ export const filterValue: (self: Exit) => Result.Result Result.succeed(Exit.fail("err")) * ``` * * @see {@link filterSuccess} for the inverse @@ -655,13 +636,10 @@ export const filterFailure: (self: Exit) => Result.Result Result.succeed(Cause.fail("err")) * ``` * * @see {@link filterFailure} to get the full Failure object @@ -692,16 +670,11 @@ export const filterCause: (self: Exit) => Result.Result Result.succeed("not found") + * Exit.findError(Exit.die("bug")) // => Result.fail(Exit.die("bug")) * ``` * * @see {@link findErrorOption} to get the error as an Option instead @@ -732,16 +705,11 @@ export const findError: (input: Exit) => Result.Result * * **Example** (Finding the first defect) * - * ```ts + * ```ts import.meta.vitest * import { Exit, Result } from "effect" * - * const exit = Exit.die("boom") - * const result = Exit.findDefect(exit) - * console.log(Result.isSuccess(result) && result.success) // "boom" - * - * const typed = Exit.fail("err") - * const noDefect = Exit.findDefect(typed) - * console.log(Result.isFailure(noDefect)) // true + * Exit.findDefect(Exit.die("boom")) // => Result.succeed("boom") + * Exit.findDefect(Exit.fail("err")) // => Result.fail(Exit.fail("err")) * ``` * * @see {@link findError} to find typed errors instead @@ -767,16 +735,13 @@ export const findDefect: (input: Exit) => Result.Result `Got: ${value}`, * onFailure: () => "Failed" - * }) - * console.log(result) // "Got: 42" + * }) // => "Got: 42" * ``` * * @see {@link isSuccess} and {@link isFailure} for simple boolean checks @@ -813,12 +778,10 @@ export const match: { * * **Example** (Mapping over a success) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.succeed(21) - * const doubled = Exit.map(exit, (x) => x * 2) - * console.log(Exit.isSuccess(doubled) && doubled.value) // 42 + * Exit.map(Exit.succeed(21), (x) => x * 2) // => Exit.succeed(42) * ``` * * @see {@link mapError} to transform the error @@ -852,14 +815,10 @@ export const map: { * * **Example** (Mapping over an error) * - * ```ts - * import { Data, Exit } from "effect" - * - * class ExitError extends Data.TaggedError("ExitError")<{ readonly input: string }> {} + * ```ts import.meta.vitest + * import { Exit } from "effect" * - * const exit = Exit.fail("bad input") - * const mapped = Exit.mapError(exit, (e) => new ExitError({ input: e })) - * console.log(Exit.isFailure(mapped)) // true + * Exit.mapError(Exit.fail("bad input"), (error) => error.toUpperCase()) // => Exit.fail("BAD INPUT") * ``` * * @see {@link map} to transform the success value @@ -893,17 +852,13 @@ export const mapError: { * * **Example** (Mapping both channels) * - * ```ts - * import { Data, Exit } from "effect" - * - * class ExitError extends Data.TaggedError("ExitError")<{ readonly input: string }> {} + * ```ts import.meta.vitest + * import { Exit } from "effect" * - * const exit = Exit.succeed(42) - * const mapped = Exit.mapBoth(exit, { + * Exit.mapBoth(Exit.succeed(42), { * onSuccess: (x) => String(x), - * onFailure: (e: string) => new ExitError({ input: e }) - * }) - * console.log(Exit.isSuccess(mapped) && mapped.value) // "42" + * onFailure: (error: string) => error.toUpperCase() + * }) // => Exit.succeed("42") * ``` * * @see {@link map} to transform only the success value @@ -938,12 +893,10 @@ export const mapBoth: { * * **Example** (Discarding the success value) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exit = Exit.succeed(42) - * const voided = Exit.asVoid(exit) - * console.log(Exit.isSuccess(voided)) // true + * Exit.asVoid(Exit.succeed(42)) // => Exit.succeed(undefined) * ``` * * @see {@link void_ void} for a pre-allocated void success @@ -971,14 +924,11 @@ export const asVoid: (self: Exit) => Exit = effect.exitAsVo * * **Example** (Combining exits) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * - * const exits = [Exit.succeed(1), Exit.succeed(2), Exit.succeed(3)] - * console.log(Exit.isSuccess(Exit.asVoidAll(exits))) // true - * - * const mixed = [Exit.succeed(1), Exit.fail("err"), Exit.succeed(3)] - * console.log(Exit.isFailure(Exit.asVoidAll(mixed))) // true + * Exit.asVoidAll([Exit.succeed(1), Exit.succeed(2), Exit.succeed(3)]) // => Exit.succeed(undefined) + * Exit.asVoidAll([Exit.succeed(1), Exit.fail("err"), Exit.succeed(3)]) // => Exit.fail("err") * ``` * * @see {@link asVoid} to discard the value of a single Exit @@ -1004,17 +954,17 @@ export const asVoidAll: >>( * * **Example** (Getting the success value) * - * ```ts - * import { Exit } from "effect" + * ```ts import.meta.vitest + * import { Exit, Option } from "effect" * - * console.log(Exit.getSuccess(Exit.succeed(42))) // { _tag: "Some", value: 42 } - * console.log(Exit.getSuccess(Exit.fail("err"))) // { _tag: "None" } + * Exit.getSuccess(Exit.succeed(42)) // => Option.some(42) + * Exit.getSuccess(Exit.fail("err")) // => Option.none() * ``` * * @see {@link getCause} to extract the Cause of a failure * @see {@link filterValue} for filter-pipeline usage * - * @category accessors + * @category getters * @since 4.0.0 */ export const getSuccess: (self: Exit) => Option = effect.exitGetSuccess @@ -1033,17 +983,17 @@ export const getSuccess: (self: Exit) => Option = effect.exitGetS * * **Example** (Getting the failure cause) * - * ```ts - * import { Exit } from "effect" + * ```ts import.meta.vitest + * import { Cause, Exit, Option } from "effect" * - * console.log(Exit.getCause(Exit.fail("err"))) // { _tag: "Some", value: ... } - * console.log(Exit.getCause(Exit.succeed(42))) // { _tag: "None" } + * Exit.getCause(Exit.fail("err")) // => Option.some(Cause.fail("err")) + * Exit.getCause(Exit.succeed(42)) // => Option.none() * ``` * * @see {@link getSuccess} to extract the success value * @see {@link filterCause} for filter-pipeline usage * - * @category accessors + * @category getters * @since 4.0.0 */ export const getCause: (self: Exit) => Option> = effect.exitGetCause @@ -1068,18 +1018,18 @@ export const getCause: (self: Exit) => Option> = effe * * **Example** (Getting the first error) * - * ```ts - * import { Exit } from "effect" + * ```ts import.meta.vitest + * import { Exit, Option } from "effect" * - * console.log(Exit.findErrorOption(Exit.fail("err"))) // { _tag: "Some", value: "err" } - * console.log(Exit.findErrorOption(Exit.die(new Error("bug")))) // { _tag: "None" } - * console.log(Exit.findErrorOption(Exit.succeed(42))) // { _tag: "None" } + * Exit.findErrorOption(Exit.fail("err")) // => Option.some("err") + * Exit.findErrorOption(Exit.die("bug")) // => Option.none() + * Exit.findErrorOption(Exit.succeed(42)) // => Option.none() * ``` * * @see {@link findError} for filter-pipeline usage * @see {@link getCause} to get the full Cause as an Option * - * @category accessors + * @category getters * @since 4.0.0 */ export const findErrorOption: (self: Exit) => Option = effect.exitFindErrorOption diff --git a/.context/effect/packages/effect/src/Fiber.ts b/.context/effect/packages/effect/src/Fiber.ts index ed60b91e9..4e511ee27 100644 --- a/.context/effect/packages/effect/src/Fiber.ts +++ b/.context/effect/packages/effect/src/Fiber.ts @@ -12,7 +12,6 @@ import type * as Context from "./Context.ts" import type { Effect } from "./Effect.ts" import type { Exit } from "./Exit.ts" import * as effect from "./internal/effect.ts" -import { version } from "./internal/version.ts" import type { LogLevel } from "./LogLevel.ts" import type { Pipeable } from "./Pipeable.ts" import { hasProperty } from "./Predicate.ts" @@ -22,7 +21,7 @@ import type { Scope } from "./Scope.ts" import type { AnySpan } from "./Tracer.ts" import type { Covariant } from "./Types.ts" -const TypeId = `~effect/Fiber/${version}` +const TypeId = "~effect/Fiber" /** * A runtime fiber is a lightweight thread that executes Effects. Fibers are @@ -49,8 +48,8 @@ const TypeId = `~effect/Fiber/${version}` * * **Example** (Awaiting a forked fiber) * - * ```ts - * import { Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Fiber } from "effect" * * const program = Effect.gen(function*() { * // Fork an effect to run in a new fiber @@ -58,10 +57,11 @@ const TypeId = `~effect/Fiber/${version}` * * // Wait for the fiber to complete and get its result * const result = yield* Fiber.await(fiber) - * console.log(result) // Exit.succeed(42) - * * return result * }) + * + * const actual = await Effect.runPromise(program) + * actual // => Exit.succeed(42) * ``` * * @category models @@ -106,7 +106,7 @@ export interface Fiber extends Pipeable { * * **Example** (Working with fiber types) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { @@ -116,13 +116,12 @@ export interface Fiber extends Pipeable { * // Use namespace types for variance * const typedFiber: Fiber.Fiber = fiber * - * // Access fiber properties - * console.log(`Fiber ID: ${fiber.id}`) - * * // Join the fiber - * const result = yield* Fiber.join(fiber) - * return result // 42 + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(program) + * actual // => 42 * ``` * * @since 2.0.0 @@ -139,12 +138,14 @@ export declare namespace Fiber { * * **Example** (Upcasting fibers safely) * - * ```ts - * import type { Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * * // Variance allows safe subtyping - * declare const fiber: Fiber.Fiber + * const fiber: Fiber.Fiber = Effect.runFork(Effect.succeed(1)) * const upcast: Fiber.Fiber = fiber + * const actual = await Effect.runPromise(Fiber.join(upcast)) + * actual // => 1 * ``` * * @category models @@ -178,14 +179,16 @@ export { * * **Example** (Awaiting a fiber exit) * - * ```ts - * import { Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Fiber } from "effect" * * const program = Effect.gen(function*() { * const fiber = yield* Effect.forkChild(Effect.succeed(42)) - * const exit = yield* Fiber.await(fiber) - * console.log(exit) // Exit.succeed(42) + * return yield* Fiber.await(fiber) * }) + * + * const actual = await Effect.runPromise(program) + * actual // => Exit.succeed(42) * ``` * * @category combinators @@ -213,15 +216,17 @@ export { * * **Example** (Awaiting multiple fiber exits) * - * ```ts - * import { Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Fiber } from "effect" * * const program = Effect.gen(function*() { * const fiber1 = yield* Effect.forkChild(Effect.succeed(1)) * const fiber2 = yield* Effect.forkChild(Effect.succeed(2)) - * const exits = yield* Fiber.awaitAll([fiber1, fiber2]) - * console.log(exits) // [Exit.succeed(1), Exit.succeed(2)] + * return yield* Fiber.awaitAll([fiber1, fiber2]) * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [Exit.succeed(1), Exit.succeed(2)] * ``` * * @category combinators @@ -254,14 +259,16 @@ export const awaitAll: >( * * **Example** (Joining a fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { * const fiber = yield* Effect.forkChild(Effect.succeed(42)) - * const result = yield* Fiber.join(fiber) - * console.log(result) // 42 + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(program) + * actual // => 42 * ``` * * @see {@link await_ await} for inspecting the fiber outcome as an Exit @@ -302,7 +309,7 @@ export const joinAll: >>( A, A extends Iterable> ? _A : never >, - A extends Fiber ? _E : never + A extends Iterable> ? _E : never > = effect.fiberJoinAll /** @@ -325,7 +332,7 @@ export const joinAll: >>( * * **Example** (Interrupting a fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { @@ -333,8 +340,9 @@ export const joinAll: >>( * Effect.delay("1 second")(Effect.succeed(42)) * ) * yield* Fiber.interrupt(fiber) - * console.log("Fiber interrupted") * }) + * + * await Effect.runPromise(program) * ``` * * @see {@link interruptAs} for specifying the interrupting fiber ID @@ -365,7 +373,7 @@ export const interrupt: (self: Fiber) => Effect = effect.fiber * * **Example** (Interrupting a fiber as another fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { @@ -375,8 +383,9 @@ export const interrupt: (self: Fiber) => Effect = effect.fiber * * // Interrupt the fiber, specifying fiber ID 123 as the interruptor * yield* Fiber.interruptAs(targetFiber, 123) - * console.log("Fiber interrupted by fiber #123") * }) + * + * await Effect.runPromise(program) * ``` * * @see {@link interrupt} for using the current fiber as the interruptor @@ -417,41 +426,17 @@ export const interruptAs: { * * **Example** (Interrupting multiple fibers) * - * ```ts - * import { Console, Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { - * // Create multiple long-running fibers - * const fiber1 = yield* Effect.forkChild( - * Effect.gen(function*() { - * yield* Effect.sleep("5 seconds") - * yield* Console.log("Task 1 completed") - * return "result1" - * }) - * ) - * - * const fiber2 = yield* Effect.forkChild( - * Effect.gen(function*() { - * yield* Effect.sleep("3 seconds") - * yield* Console.log("Task 2 completed") - * return "result2" - * }) - * ) - * - * const fiber3 = yield* Effect.forkChild( - * Effect.gen(function*() { - * yield* Effect.sleep("4 seconds") - * yield* Console.log("Task 3 completed") - * return "result3" - * }) - * ) - * - * // Wait a bit, then interrupt all fibers - * yield* Effect.sleep("1 second") - * yield* Console.log("Interrupting all fibers...") + * const fiber1 = yield* Effect.forkChild(Effect.never) + * const fiber2 = yield* Effect.forkChild(Effect.never) + * const fiber3 = yield* Effect.forkChild(Effect.never) * yield* Fiber.interruptAll([fiber1, fiber2, fiber3]) - * yield* Console.log("All fibers have been interrupted") * }) + * + * await Effect.runPromise(program) * ``` * * @see {@link interruptAllAs} for specifying the interrupting fiber ID @@ -485,36 +470,20 @@ export const interruptAll: >>( * * **Example** (Interrupting multiple fibers as another fiber) * - * ```ts - * import { Console, Effect, Fiber } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { * // Create a controlling fiber * const controllerFiber = yield* Effect.forkChild(Effect.succeed("controller")) * - * // Create multiple worker fibers - * const worker1 = yield* Effect.forkChild( - * Effect.gen(function*() { - * yield* Effect.sleep("5 seconds") - * yield* Console.log("Worker 1 completed") - * return "worker1" - * }) - * ) - * - * const worker2 = yield* Effect.forkChild( - * Effect.gen(function*() { - * yield* Effect.sleep("3 seconds") - * yield* Console.log("Worker 2 completed") - * return "worker2" - * }) - * ) + * const worker1 = yield* Effect.forkChild(Effect.never) + * const worker2 = yield* Effect.forkChild(Effect.never) * - * // Interrupt all workers using the controller fiber's ID - * yield* Effect.sleep("1 second") - * yield* Console.log("Interrupting workers from controller...") * yield* Fiber.interruptAllAs([worker1, worker2], controllerFiber.id) - * yield* Console.log("All workers interrupted by controller") * }) + * + * await Effect.runPromise(program) * ``` * * @see {@link interruptAll} for using the current fiber as the interruptor @@ -543,7 +512,7 @@ export const interruptAllAs: { * * **Example** (Checking for fibers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { @@ -551,18 +520,11 @@ export const interruptAllAs: { * const fiber = yield* Effect.forkChild(Effect.succeed(42)) * * // Test if values are fibers - * console.log(Fiber.isFiber(fiber)) // true - * console.log(Fiber.isFiber("hello")) // false - * console.log(Fiber.isFiber(42)) // false - * console.log(Fiber.isFiber(null)) // false - * - * // Use as a type guard - * const maybeValue: unknown = fiber - * if (Fiber.isFiber(maybeValue)) { - * // TypeScript knows maybeValue is a Fiber here - * console.log(`Fiber ID: ${maybeValue.id}`) - * } + * return [Fiber.isFiber(fiber), Fiber.isFiber("hello"), Fiber.isFiber(42), Fiber.isFiber(null)] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [true, false, false, false] * ``` * * @category guards @@ -588,18 +550,19 @@ export const isFiber = ( * * **Example** (Getting the current fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber } from "effect" * * const program = Effect.gen(function*() { * const current = Fiber.getCurrent() - * if (current) { - * console.log(`Current fiber ID: ${current.id}`) - * } + * return current !== undefined * }) + * + * const actual = await Effect.runPromise(program) + * actual // => true * ``` * - * @category accessors + * @category getters * @since 4.0.0 */ export const getCurrent: () => Fiber | undefined = effect.getCurrentFiber diff --git a/.context/effect/packages/effect/src/FiberHandle.ts b/.context/effect/packages/effect/src/FiberHandle.ts index fe1b3f834..7a004bfa1 100644 --- a/.context/effect/packages/effect/src/FiberHandle.ts +++ b/.context/effect/packages/effect/src/FiberHandle.ts @@ -35,18 +35,20 @@ const TypeId = "~effect/FiberHandle" * * **Example** (Managing a single fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * // Create a FiberHandle that can hold fibers producing strings * const handle = yield* FiberHandle.make() * * // The handle can store and manage a single fiber * const fiber = yield* FiberHandle.run(handle, Effect.succeed("hello")) - * const result = yield* Fiber.await(fiber) - * console.log(result) // "hello" + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "hello" * ``` * * @category models @@ -69,18 +71,20 @@ export interface FiberHandle extends Pipeable, * * **Example** (Checking fiber handles) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * - * console.log(FiberHandle.isFiberHandle(handle)) // true - * console.log(FiberHandle.isFiberHandle("not a handle")) // false + * return [FiberHandle.isFiberHandle(handle), FiberHandle.isFiberHandle("not a handle")] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [true, false] * ``` * - * @category refinements + * @category guards * @since 2.0.0 */ export const isFiberHandle = (u: unknown): u is FiberHandle => Predicate.hasProperty(u, TypeId) @@ -115,10 +119,10 @@ const makeUnsafe = (): FiberHandle => { * * **Example** (Creating a scoped fiber handle) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * * // run some effects @@ -126,10 +130,14 @@ const makeUnsafe = (): FiberHandle => { * // this will interrupt the previous fiber * yield* FiberHandle.run(handle, Effect.never) * - * yield* Effect.sleep(1000) + * yield* Effect.yieldNow + * return handle.state._tag === "Open" && handle.state.fiber !== undefined * }).pipe( * Effect.scoped // The fiber will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(program) + * actual // => true * ``` * * @category constructors @@ -162,20 +170,24 @@ export const make = (): Effect.Effect() * * // Run effects and get fibers back - * const fiberA = run(Effect.succeed("first")) + * const fiberA = run(Effect.never) * const fiberB = run(Effect.succeed("second")) * * // The second fiber will interrupt the first * const resultA = yield* Fiber.await(fiberA) * const resultB = yield* Fiber.await(fiberB) + * return [resultA, resultB] * }).pipe(Effect.scoped) + * + * const actual = await Effect.runPromise(program) + * actual // => [Exit.failCause(Cause.interrupt(-1)), Exit.succeed("second")] * ``` * * @category constructors @@ -218,17 +230,19 @@ export const makeRuntime = (): Effect.Effect< * * **Example** (Running effects as promises) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const run = yield* FiberHandle.makeRuntimePromise() * * // Run effects and get promises back * const promise = run(Effect.succeed("hello")) - * const result = yield* Effect.promise(() => promise) - * console.log(result) // "hello" + * return yield* Effect.promise(() => promise) * }).pipe(Effect.scoped) + * + * const actual = await Effect.runPromise(program) + * actual // => "hello" * ``` * * @category constructors @@ -269,10 +283,10 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * * **Example** (Setting a fiber unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * const fiber = Effect.runFork(Effect.succeed("hello")) * @@ -280,9 +294,11 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * FiberHandle.setUnsafe(handle, fiber) * * // The fiber is now managed by the handle - * const result = yield* Fiber.await(fiber) - * console.log(result) // "hello" + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "hello" * ``` * * @category combinators @@ -355,10 +371,10 @@ export const setUnsafe: { * * **Example** (Setting a fiber safely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * const fiber = Effect.runFork(Effect.succeed("hello")) * @@ -366,9 +382,11 @@ export const setUnsafe: { * yield* FiberHandle.set(handle, fiber) * * // The fiber is now managed by the handle - * const result = yield* Fiber.await(fiber) - * console.log(result) // "hello" + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "hello" * ``` * * @category combinators @@ -415,21 +433,23 @@ export const set: { * * **Example** (Reading the current fiber unsafely) * - * ```ts - * import { Effect, FiberHandle } from "effect" + * ```ts import.meta.vitest + * import { Effect, FiberHandle, Option } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * * // No fiber initially * const emptyFiber = FiberHandle.getUnsafe(handle) - * console.log(emptyFiber._tag === "None") // true * * // Add a fiber - * yield* FiberHandle.run(handle, Effect.succeed("hello")) + * yield* FiberHandle.run(handle, Effect.never) * const fiber = FiberHandle.getUnsafe(handle) - * console.log(fiber._tag === "Some") // true + * return [emptyFiber, Option.map(fiber, () => true)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [Option.none(), Option.some(true)] * ``` * * @category combinators @@ -444,22 +464,22 @@ export function getUnsafe(self: FiberHandle): Option.Option true) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => Option.some(true) * ``` * * @category combinators @@ -475,10 +495,10 @@ export function get(self: FiberHandle): Effect.Effect(self: FiberHandle): Effect.Effect Option.none() * ``` * * @category combinators @@ -501,10 +523,11 @@ export const clear = (self: FiberHandle): Effect.Effect => if (self.state._tag === "Closed" || self.state.fiber === undefined) { return Effect.void } + const fiber = self.state.fiber return Effect.andThen( - restore(Fiber.interruptAs(self.state.fiber, internalFiberId)), + restore(Fiber.interruptAs(fiber, internalFiberId)), Effect.sync(() => { - if (self.state._tag === "Open") { + if (self.state._tag === "Open" && self.state.fiber === fiber) { self.state.fiber = undefined } }) @@ -532,22 +555,24 @@ const constInterruptedFiber = (function() { * * **Example** (Running an effect in a fiber handle) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * * // Run an effect and get the fiber * const fiber = yield* FiberHandle.run(handle, Effect.succeed("hello")) - * const result = yield* Fiber.await(fiber) - * console.log(result) // "hello" + * const result = yield* Fiber.join(fiber) * * // Running another effect will interrupt the previous one * const fiber2 = yield* FiberHandle.run(handle, Effect.succeed("world")) - * const result2 = yield* Fiber.await(fiber2) - * console.log(result2) // "world" + * const result2 = yield* Fiber.join(fiber2) + * return [result, result2] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["hello", "world"] * ``` * * @category combinators @@ -611,28 +636,32 @@ const runImpl = ( * * **Example** (Capturing a runtime for fiber handles) * - * ```ts - * import { Context, Effect, FiberHandle } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Fiber, FiberHandle } from "effect" * - * interface Users { - * readonly _: unique symbol - * } - * const Users = Context.Service> - * }>("Users") + * class Users extends Context.Service> + * }>()("Users") {} * - * Effect.gen(function*() { + * const program = 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)) + * const fiberA = run(Effect.andThen(Users, (_) => _.getAll)) * * // this will interrupt the previous fiber - * run(Effect.andThen(Users, (_) => _.getAll)) + * const fiberB = run(Effect.andThen(Users, (_) => _.getAll)) + * yield* Fiber.await(fiberA) + * return (yield* Fiber.join(fiberB)).length * }).pipe( * Effect.scoped // The fiber will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(Effect.provideService(program, Users, { + * getAll: Effect.succeed([]) + * })) + * actual // => 0 * ``` * * @category combinators @@ -694,18 +723,20 @@ export const runtime: ( * * **Example** (Capturing a runtime for promises) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * const runPromise = yield* FiberHandle.runtimePromise(handle)() * * // Run an effect and get a promise * const promise = runPromise(Effect.succeed("hello")) - * const result = yield* Effect.promise(() => promise) - * console.log(result) // "hello" + * return yield* Effect.promise(() => promise) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "hello" * ``` * * @category combinators @@ -764,16 +795,19 @@ export const runtimePromise = (self: FiberHandle): () => * * **Example** (Propagating fiber failures) * - * ```ts - * import { Effect, FiberHandle } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, FiberHandle } from "effect" * - * Effect.gen(function*() { + * const program = 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) * }) + * + * const actual = await Effect.runPromise(Effect.exit(Effect.scoped(program))) + * actual // => Exit.fail("error") * ``` * * @category combinators @@ -787,20 +821,22 @@ export const join = (self: FiberHandle): Effect.Effect => * * **Example** (Waiting for a fiber to complete) * - * ```ts - * import { Effect, FiberHandle } from "effect" + * ```ts import.meta.vitest + * import { Effect, FiberHandle, Option } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const handle = yield* FiberHandle.make() * - * // Start a long-running effect - * yield* FiberHandle.run(handle, Effect.sleep(1000)) + * yield* FiberHandle.run(handle, Effect.yieldNow) * * // Wait for the fiber to complete * yield* FiberHandle.awaitEmpty(handle) * - * console.log("Fiber completed") + * return yield* FiberHandle.get(handle) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => Option.none() * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/FiberMap.ts b/.context/effect/packages/effect/src/FiberMap.ts index 968413277..828632ac5 100644 --- a/.context/effect/packages/effect/src/FiberMap.ts +++ b/.context/effect/packages/effect/src/FiberMap.ts @@ -35,7 +35,7 @@ const TypeId = "~effect/FiberMap" * * **Example** (Managing fibers in a map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * // Create a FiberMap with string keys @@ -47,9 +47,11 @@ const TypeId = "~effect/FiberMap" * yield* FiberMap.run(map, "task2", Effect.never) * * // Get the size of the map - * const size = yield* FiberMap.size(map) - * console.log(size) // 2 + * return yield* FiberMap.size(map) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 2 * ``` * * @category models @@ -77,19 +79,20 @@ export interface FiberMap * * **Example** (Checking if a value is a FiberMap) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { * const map = yield* FiberMap.make() * - * console.log(FiberMap.isFiberMap(map)) // true - * console.log(FiberMap.isFiberMap({})) // false - * console.log(FiberMap.isFiberMap(null)) // false + * return [FiberMap.isFiberMap(map), FiberMap.isFiberMap({}), FiberMap.isFiberMap(null)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [true, false, false] * ``` * - * @category refinements + * @category guards * @since 2.0.0 */ export const isFiberMap = (u: unknown): u is FiberMap => Predicate.hasProperty(u, TypeId) @@ -133,20 +136,24 @@ const makeUnsafe = ( * * **Example** (Creating a scoped FiberMap) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * - * Effect.gen(function*() { + * const program = 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) + * yield* Effect.yieldNow + * return yield* FiberMap.size(map) * }).pipe( * Effect.scoped // The fibers will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(program) + * actual // => 2 * ``` * * @category constructors @@ -183,7 +190,7 @@ export const make = (): Effect.Effect(): Effect.Effect ["Hello", "World"] * ``` * * @category constructors @@ -240,7 +247,7 @@ export const makeRuntime = (): Effect.Effect< * * **Example** (Creating a promise runtime) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -251,11 +258,11 @@ export const makeRuntime = (): Effect.Effect< * const promise2 = run("task2", Effect.succeed("World")) * * // Convert to Effect and await - * const result1 = yield* Effect.promise(() => promise1) - * const result2 = yield* Effect.promise(() => promise2) - * - * console.log(result1, result2) // "Hello", "World" + * return [yield* Effect.promise(() => promise1), yield* Effect.promise(() => promise2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["Hello", "World"] * ``` * * @category constructors @@ -302,7 +309,7 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * * **Example** (Adding a fiber unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect, Fiber, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -316,9 +323,11 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * yield* Deferred.succeed(deferred, "Hello") * * // Join the fiber to get its successful value - * const result = yield* Fiber.join(fiber) - * console.log(result) // "Hello" + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "Hello" * ``` * * @category combinators @@ -402,7 +411,7 @@ export const setUnsafe: { * * **Example** (Adding a fiber) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect, Fiber, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -416,9 +425,11 @@ export const setUnsafe: { * yield* Deferred.succeed(deferred, "Hello") * * // Join the fiber to get its successful value - * const result = yield* Fiber.join(fiber) - * console.log(result) // "Hello" + * return yield* Fiber.join(fiber) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => "Hello" * ``` * * @category combinators @@ -462,8 +473,8 @@ export const set: { * * **Example** (Retrieving a fiber unsafely) * - * ```ts - * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Fiber, FiberMap, Option } from "effect" * * const program = Effect.gen(function*() { * const map = yield* FiberMap.make() @@ -475,13 +486,13 @@ export const set: { * * // Retrieve the fiber * const retrieved = FiberMap.getUnsafe(map, "greeting") - * if (retrieved._tag === "Some") { - * yield* Deferred.succeed(deferred, "Hello") - * - * const result = yield* Fiber.join(retrieved.value) - * console.log(result) // "Hello" - * } + * yield* Deferred.succeed(deferred, "Hello") + * const result = yield* Fiber.join(fiber) + * return Option.map(retrieved, () => result) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => Option.some("Hello") * ``` * * @category combinators @@ -506,8 +517,8 @@ export const getUnsafe: { * * **Example** (Retrieving a fiber) * - * ```ts - * import { Deferred, Effect, Fiber, FiberMap } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Fiber, FiberMap, Option } from "effect" * * const program = Effect.gen(function*() { * const map = yield* FiberMap.make() @@ -519,13 +530,13 @@ export const getUnsafe: { * * // Retrieve the fiber with error handling * const retrieved = yield* FiberMap.get(map, "greeting") - * if (retrieved._tag === "Some") { - * yield* Deferred.succeed(deferred, "Hello") - * - * const result = yield* Fiber.join(retrieved.value) - * console.log(result) // "Hello" - * } + * yield* Deferred.succeed(deferred, "Hello") + * const result = yield* Fiber.join(fiber) + * return Option.map(retrieved, () => result) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => Option.some("Hello") * ``` * * @category combinators @@ -545,7 +556,7 @@ export const get: { * * **Example** (Checking if a key exists unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -555,9 +566,11 @@ export const get: { * yield* FiberMap.run(map, "task1", Effect.never) * * // Check if keys exist - * console.log(FiberMap.hasUnsafe(map, "task1")) // true - * console.log(FiberMap.hasUnsafe(map, "task2")) // false + * return [FiberMap.hasUnsafe(map, "task1"), FiberMap.hasUnsafe(map, "task2")] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [true, false] * ``` * * @category combinators @@ -578,7 +591,7 @@ export const hasUnsafe: { * * **Example** (Checking if a key exists) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -588,12 +601,11 @@ export const hasUnsafe: { * yield* FiberMap.run(map, "task1", Effect.never) * * // Check if keys exist using Effect - * const exists1 = yield* FiberMap.has(map, "task1") - * const exists2 = yield* FiberMap.has(map, "task2") - * - * console.log(exists1) // true - * console.log(exists2) // false + * return [yield* FiberMap.has(map, "task1"), yield* FiberMap.has(map, "task2")] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [true, false] * ``` * * @category combinators @@ -612,7 +624,7 @@ export const has: { * * **Example** (Removing a fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -622,13 +634,16 @@ export const has: { * yield* FiberMap.run(map, "task1", Effect.never) * yield* FiberMap.run(map, "task2", Effect.never) * - * console.log(yield* FiberMap.size(map)) // 2 + * const sizeBefore = yield* FiberMap.size(map) * * // Remove a specific fiber (this will interrupt it) * yield* FiberMap.remove(map, "task1") * - * console.log(yield* FiberMap.size(map)) // 1 + * return [sizeBefore, yield* FiberMap.size(map)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [2, 1] * ``` * * @category combinators @@ -662,7 +677,7 @@ export const remove: { * * **Example** (Clearing all fibers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -673,13 +688,16 @@ export const remove: { * yield* FiberMap.run(map, "task2", Effect.never) * yield* FiberMap.run(map, "task3", Effect.never) * - * console.log(yield* FiberMap.size(map)) // 3 + * const sizeBefore = yield* FiberMap.size(map) * * // Clear all fibers (this will interrupt all of them) * yield* FiberMap.clear(map) * - * console.log(yield* FiberMap.size(map)) // 0 + * return [sizeBefore, yield* FiberMap.size(map)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [3, 0] * ``` * * @category combinators @@ -713,7 +731,7 @@ const constInterruptedFiber = (function() { * * **Example** (Forking effects into a map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -726,10 +744,11 @@ const constInterruptedFiber = (function() { * // Join the fibers to get their successful values * const result1 = yield* Fiber.join(fiber1) * const result2 = yield* Fiber.join(fiber2) - * - * console.log(result1, result2) // "Hello", "World" - * console.log(yield* FiberMap.size(map)) // 0 (fibers are removed after completion) + * return [result1, result2, yield* FiberMap.size(map)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["Hello", "World", 0] * ``` * * @category combinators @@ -798,26 +817,29 @@ const runImpl = ( * * **Example** (Capturing a runtime) * - * ```ts - * import { Context, Effect, FiberMap } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Fiber, FiberMap } from "effect" * - * interface Users { - * readonly _: unique symbol - * } - * const Users = Context.Service> - * }>("Users") + * class Users extends Context.Service> + * }>()("Users") {} * - * Effect.gen(function*() { + * const program = 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)) + * const fiberA = run("effect-a", Effect.andThen(Users, (_) => _.getAll)) + * const fiberB = run("effect-b", Effect.andThen(Users, (_) => _.getAll)) + * return [(yield* Fiber.join(fiberA)).length, (yield* Fiber.join(fiberB)).length] * }).pipe( * Effect.scoped // The fibers will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(Effect.provideService(program, Users, { + * getAll: Effect.succeed([]) + * })) + * actual // => [0, 0] * ``` * * @category combinators @@ -878,7 +900,7 @@ export const runtime: ( * * **Example** (Running effects as promises) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { @@ -890,11 +912,11 @@ export const runtime: ( * const promise2 = runPromise("task2", Effect.succeed("World")) * * // Convert promises back to Effects and await - * const result1 = yield* Effect.promise(() => promise1) - * const result2 = yield* Effect.promise(() => promise2) - * - * console.log(result1, result2) // "Hello", "World" + * return [yield* Effect.promise(() => promise1), yield* Effect.promise(() => promise2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["Hello", "World"] * ``` * * @category combinators @@ -941,20 +963,23 @@ export const runtimePromise = (self: FiberMap): () * * **Example** (Checking the map size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { * const map = yield* FiberMap.make() * - * console.log(yield* FiberMap.size(map)) // 0 + * const sizeBefore = yield* FiberMap.size(map) * * // Add some fibers * yield* FiberMap.run(map, "task1", Effect.never) * yield* FiberMap.run(map, "task2", Effect.never) * - * console.log(yield* FiberMap.size(map)) // 2 + * return [sizeBefore, yield* FiberMap.size(map)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [0, 2] * ``` * * @category combinators @@ -975,16 +1000,19 @@ export const size = (self: FiberMap): Effect.Effect => * * **Example** (Joining failing fibers) * - * ```ts - * import { Effect, FiberMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, FiberMap } from "effect" * - * Effect.gen(function*() { + * const program = 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) * }) + * + * const actual = await Effect.runPromise(Effect.exit(Effect.scoped(program))) + * actual // => Exit.fail("error") * ``` * * @category combinators @@ -999,24 +1027,23 @@ export const join = (self: FiberMap): Effect.Effect = * * **Example** (Waiting for an empty map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberMap } from "effect" * * const program = Effect.gen(function*() { * const map = yield* FiberMap.make() * - * // Add some fibers that will complete after a delay - * yield* FiberMap.run(map, "task1", Effect.sleep(1000)) - * yield* FiberMap.run(map, "task2", Effect.sleep(2000)) - * - * console.log("Waiting for all fibers to complete...") + * yield* FiberMap.run(map, "task1", Effect.yieldNow) + * yield* FiberMap.run(map, "task2", Effect.yieldNow) * * // Wait for the map to be empty * yield* FiberMap.awaitEmpty(map) * - * console.log("All fibers completed!") - * console.log(yield* FiberMap.size(map)) // 0 + * return yield* FiberMap.size(map) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 0 * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/FiberSet.ts b/.context/effect/packages/effect/src/FiberSet.ts index 7b7b36689..f99040716 100644 --- a/.context/effect/packages/effect/src/FiberSet.ts +++ b/.context/effect/packages/effect/src/FiberSet.ts @@ -31,7 +31,7 @@ const TypeId = "~effect/FiberSet" * * **Example** (Managing fibers in a set) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -43,7 +43,11 @@ const TypeId = "~effect/FiberSet" * * // Wait for all fibers to complete * yield* FiberSet.awaitEmpty(set) + * return yield* FiberSet.size(set) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 0 * ``` * * @category models @@ -67,18 +71,20 @@ export interface FiberSet * * **Example** (Checking if a value is a FiberSet) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const set = yield* FiberSet.make() * - * console.log(FiberSet.isFiberSet(set)) // true - * console.log(FiberSet.isFiberSet({})) // false + * return [FiberSet.isFiberSet(set), FiberSet.isFiberSet({})] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [true, false] * ``` * - * @category refinements + * @category guards * @since 2.0.0 */ export const isFiberSet = (u: unknown): u is FiberSet => Predicate.hasProperty(u, TypeId) @@ -94,7 +100,7 @@ const Proto = { ...PipeInspectableProto, toJSON(this: FiberSet) { return { - _id: "FiberMap", + _id: "FiberSet", state: this.state } } @@ -122,20 +128,24 @@ const makeUnsafe = ( * * **Example** (Creating a scoped FiberSet) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * - * Effect.gen(function*() { + * const program = 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) + * yield* Effect.yieldNow + * return yield* FiberSet.size(set) * }).pipe( * Effect.scoped // The fibers will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(program) + * actual // => 2 * ``` * * @category constructors @@ -166,7 +176,7 @@ export const make = (): Effect.Effect, * * **Example** (Creating a scoped runtime) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -176,11 +186,11 @@ export const make = (): Effect.Effect, * const fiber1 = runFork(Effect.succeed("hello")) * const fiber2 = runFork(Effect.succeed("world")) * - * const result1 = yield* Fiber.await(fiber1) - * const result2 = yield* Fiber.await(fiber2) - * - * console.log(result1, result2) // "hello" "world" + * return [yield* Fiber.join(fiber1), yield* Fiber.join(fiber2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["hello", "world"] * ``` * * @category constructors @@ -216,7 +226,7 @@ export const makeRuntime = (): Effect.Effec * * **Example** (Creating a promise runtime) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -226,11 +236,11 @@ export const makeRuntime = (): Effect.Effec * const promise1 = runPromise(Effect.succeed("hello")) * const promise2 = runPromise(Effect.succeed("world")) * - * const result1 = yield* Effect.promise(() => promise1) - * const result2 = yield* Effect.promise(() => promise2) - * - * console.log(result1, result2) // "hello" "world" + * return [yield* Effect.promise(() => promise1), yield* Effect.promise(() => promise2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["hello", "world"] * ``` * * @category constructors @@ -272,19 +282,22 @@ const isInternalInterruption = Filter.toPredicate(Filter.compose( * * **Example** (Adding a fiber unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { * const set = yield* FiberSet.make() - * const fiber = yield* Effect.forkChild(Effect.succeed("hello")) + * const fiber = yield* Effect.forkChild(Effect.never) * * // Unsafe add - doesn't return an Effect * FiberSet.addUnsafe(set, fiber) * * // The fiber is now managed by the set - * console.log(yield* FiberSet.size(set)) // 1 + * return yield* FiberSet.size(set) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 1 * ``` * * @category combinators @@ -341,19 +354,22 @@ export const addUnsafe: { * * **Example** (Adding a fiber) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { * const set = yield* FiberSet.make() - * const fiber = yield* Effect.forkChild(Effect.succeed("hello")) + * const fiber = yield* Effect.forkChild(Effect.never) * * // Add the fiber to the set * yield* FiberSet.add(set, fiber) * * // The fiber is now managed by the set - * console.log(yield* FiberSet.size(set)) // 1 + * return yield* FiberSet.size(set) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 1 * ``` * * @category combinators @@ -389,7 +405,7 @@ export const add: { * * **Example** (Clearing all fibers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -399,13 +415,16 @@ export const add: { * yield* FiberSet.run(set, Effect.never) * yield* FiberSet.run(set, Effect.never) * - * console.log(yield* FiberSet.size(set)) // 2 + * const sizeBefore = yield* FiberSet.size(set) * * // Clear all fibers * yield* FiberSet.clear(set) * - * console.log(yield* FiberSet.size(set)) // 0 + * return [sizeBefore, yield* FiberSet.size(set)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [2, 0] * ``` * * @category combinators @@ -435,7 +454,7 @@ const constInterruptedFiber = (function() { * * **Example** (Forking effects into a set) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -446,11 +465,11 @@ const constInterruptedFiber = (function() { * const fiber2 = yield* FiberSet.run(set, Effect.succeed("world")) * * // Get results - * const result1 = yield* Fiber.await(fiber1) - * const result2 = yield* Fiber.await(fiber2) - * - * console.log(result1, result2) // "hello" "world" + * return [yield* Fiber.join(fiber1), yield* Fiber.join(fiber2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["hello", "world"] * ``` * * @category combinators @@ -504,25 +523,28 @@ const runImpl = ( * * **Example** (Capturing a runtime) * - * ```ts - * import { Context, Effect, FiberSet } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Fiber, FiberSet } from "effect" * - * interface Users { - * readonly _: unique symbol - * } - * const Users = Context.Service> - * }>("Users") + * class Users extends Context.Service> + * }>()("Users") {} * - * Effect.gen(function*() { + * const program = 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)) + * const fiber = run(Effect.andThen(Users, (_) => _.getAll)) + * return (yield* Fiber.join(fiber)).length * }).pipe( * Effect.scoped // The fibers will be interrupted when the scope is closed * ) + * + * const actual = await Effect.runPromise(Effect.provideService(program, Users, { + * getAll: Effect.succeed([]) + * })) + * actual // => 0 * ``` * * @category combinators @@ -554,7 +576,7 @@ export const runtime: ( return constInterruptedFiber() } const fiber = runFork(effect, options) - addUnsafe(self, fiber) + addUnsafe(self, fiber, options) return fiber } } @@ -575,7 +597,7 @@ export const runtime: ( * * **Example** (Running effects as promises) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { @@ -586,11 +608,11 @@ export const runtime: ( * const promise1 = runPromise(Effect.succeed("hello")) * const promise2 = runPromise(Effect.succeed("world")) * - * const result1 = yield* Effect.promise(() => promise1) - * const result2 = yield* Effect.promise(() => promise2) - * - * console.log(result1, result2) // "hello" "world" + * return [yield* Effect.promise(() => promise1), yield* Effect.promise(() => promise2)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => ["hello", "world"] * ``` * * @see {@link runtime} for a runner that returns the forked `Fiber` @@ -634,20 +656,23 @@ export const runtimePromise = (self: FiberSet): () => Eff * * **Example** (Checking the set size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { * const set = yield* FiberSet.make() * - * console.log(yield* FiberSet.size(set)) // 0 + * const sizeBefore = yield* FiberSet.size(set) * * // Add some fibers * yield* FiberSet.run(set, Effect.never) * yield* FiberSet.run(set, Effect.never) * - * console.log(yield* FiberSet.size(set)) // 2 + * return [sizeBefore, yield* FiberSet.size(set)] * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => [0, 2] * ``` * * @category combinators @@ -662,16 +687,19 @@ export const size = (self: FiberSet): Effect.Effect => * * **Example** (Joining failing fibers) * - * ```ts - * import { Effect, FiberSet } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, FiberSet } from "effect" * - * Effect.gen(function*() { + * const program = 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) * }) + * + * const actual = await Effect.runPromise(Effect.exit(Effect.scoped(program))) + * actual // => Exit.fail("error") * ``` * * @category combinators @@ -685,21 +713,23 @@ export const join = (self: FiberSet): Effect.Effect => * * **Example** (Waiting for an empty set) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FiberSet } from "effect" * * const program = Effect.gen(function*() { * const set = yield* FiberSet.make() * - * // Add some fibers that will complete - * yield* FiberSet.run(set, Effect.sleep(100)) - * yield* FiberSet.run(set, Effect.sleep(200)) + * yield* FiberSet.run(set, Effect.yieldNow) + * yield* FiberSet.run(set, Effect.yieldNow) * * // Wait for all fibers to complete * yield* FiberSet.awaitEmpty(set) * - * console.log(yield* FiberSet.size(set)) // 0 + * return yield* FiberSet.size(set) * }) + * + * const actual = await Effect.runPromise(Effect.scoped(program)) + * actual // => 0 * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/FileSystem.ts b/.context/effect/packages/effect/src/FileSystem.ts index beadaa7a2..5cca2154d 100644 --- a/.context/effect/packages/effect/src/FileSystem.ts +++ b/.context/effect/packages/effect/src/FileSystem.ts @@ -11,7 +11,7 @@ * @since 4.0.0 */ import * as Arr from "./Array.ts" -import * as Brand from "./Brand.ts" +import type * as Brand from "./Brand.ts" import * as Cause from "./Cause.ts" import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" @@ -38,8 +38,15 @@ const TypeId = "~effect/platform/FileSystem" * * **Example** (Accessing file system operations) * - * ```ts - * import { Console, Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem } from "effect" + * + * const fileSystem = FileSystem.makeNoop({ + * exists: () => Effect.succeed(true), + * makeDirectory: () => Effect.void, + * stat: () => Effect.succeed({ size: FileSystem.Size(22) } as FileSystem.File.Info), + * readFileString: () => Effect.succeed("{\"env\": \"development\"}") + * }) * * const program = Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem @@ -55,15 +62,17 @@ const TypeId = "~effect/platform/FileSystem" * * // File information * const stats = yield* fs.stat("./config.json") - * yield* Console.log(`File size: ${stats.size} bytes`) - * - * // Streaming operations + * // Read the file contents * const content = yield* fs.readFileString("./config.json") - * yield* Console.log("Config:", content) + * return { size: stats.size, content } * }) + * + * const result = Effect.runSync(Effect.provideService(program, FileSystem.FileSystem, fileSystem)) + * result.size // => 22n + * result.content // => "{\"env\": \"development\"}" * ``` * - * @category models + * @category services * @since 4.0.0 */ export interface FileSystem { @@ -341,9 +350,15 @@ export interface FileSystem { mtime: Date | number ) => Effect.Effect /** - * Watch a directory or file for changes + * Watch a directory or file for changes. + * + * **Details** + * + * 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. */ - readonly watch: (path: string) => Stream.Stream + readonly watch: (path: string, options?: WatchOptions) => Stream.Stream /** * Write data to a file at `path`. */ @@ -380,18 +395,11 @@ export interface FileSystem { * * **Example** (Creating branded file sizes) * - * ```ts - * import { Effect, FileSystem } from "effect" - * - * // Create sizes using the Size constructor - * const smallFile = FileSystem.Size(1024) // 1 KB - * const largeFile = FileSystem.Size(BigInt("9007199254740992")) // Very large + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * // Use with file operations - * const truncateToSize = Effect.fnUntraced(function*(path: string, size: FileSystem.Size) { - * const fs = yield* FileSystem.FileSystem - * return yield* fs.truncate(path, size) - * }) + * FileSystem.Size(1024) // => 1024n + * FileSystem.Size(BigInt("9007199254740992")) // => 9007199254740992n * ``` * * @category sizes @@ -410,17 +418,15 @@ export type Size = Brand.Branded * * **Example** (Using size inputs) * - * ```ts - * import { Effect, FileSystem } from "effect" - * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * // All of these are valid SizeInput values - * yield* fs.truncate("file1.txt", 1024) // number - * yield* fs.truncate("file2.txt", BigInt(2048)) // bigint - * yield* fs.truncate("file3.txt", FileSystem.Size(4096)) // Size - * }) + * const inputs: ReadonlyArray = [ + * 1024, + * 2048n, + * FileSystem.Size(4096) + * ] + * inputs.map(FileSystem.Size) // => [1024n, 2048n, 4096n] * ``` * * @category sizes @@ -439,27 +445,19 @@ export type SizeInput = bigint | number | Size * * **Example** (Converting size inputs) * - * ```ts - * import { Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * * // From number * const size1 = FileSystem.Size(1024) - * console.log(typeof size1) // "bigint" + * typeof size1 // => "bigint" * * // From bigint * const size2 = FileSystem.Size(BigInt(2048)) * * // From existing Size (identity) * const size3 = FileSystem.Size(size1) - * - * // Use in file operations - * const readChunk = (path: string, chunkSize: number) => - * Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * return fs.stream(path, { - * chunkSize: FileSystem.Size(chunkSize) - * }) - * }) + * const sizes = [size2, size3] // => [2048n, 1024n] * ``` * * @category sizes @@ -477,22 +475,11 @@ export const Size = (bytes: SizeInput): Size => typeof bytes === "bigint" ? byte * * **Example** (Creating kibibyte sizes) * - * ```ts - * import { Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Create a 64 KiB buffer size for streaming - * const bufferSize = FileSystem.KiB(64) - * - * const stream = fs.stream("large-file.txt", { - * chunkSize: bufferSize - * }) - * - * // Truncate file to 100 KiB - * yield* fs.truncate("data.txt", FileSystem.KiB(100)) - * }) + * FileSystem.KiB(64) // => 65536n + * FileSystem.KiB(100) // => 102400n * ``` * * @category sizes @@ -510,26 +497,11 @@ export const KiB = (n: number): Size => Size(n * 1024) * * **Example** (Creating mebibyte sizes) * - * ```ts - * import { Effect, FileSystem } from "effect" - * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Set a 10 MiB chunk size for large file operations - * const largeChunkSize = FileSystem.MiB(10) - * - * const stream = fs.stream("video.mp4", { - * chunkSize: largeChunkSize - * }) + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * // Check if file is larger than 100 MiB - * const stats = yield* fs.stat("archive.zip") - * const maxSize = FileSystem.MiB(100) - * if (stats.size > maxSize) { - * yield* Effect.log("File is very large!") - * } - * }) + * FileSystem.MiB(10) // => 10485760n + * FileSystem.MiB(100) // => 104857600n * ``` * * @category sizes @@ -547,24 +519,10 @@ export const MiB = (n: number): Size => Size(n * 1024 * 1024) * * **Example** (Creating gibibyte sizes) * - * ```ts - * import { Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Use GiB values as size thresholds - * const maxArchiveSize = FileSystem.GiB(1) - * console.log(maxArchiveSize.toString()) // "1073741824" - * - * const tempFile = yield* fs.makeTempFile({ prefix: "archive-" }) - * yield* fs.writeFileString(tempFile, "backup data") - * - * const info = yield* fs.stat(tempFile) - * console.log(info.size < maxArchiveSize) // true - * - * yield* fs.remove(tempFile) - * }) + * FileSystem.GiB(1) // => 1073741824n * ``` * * @category sizes @@ -582,25 +540,10 @@ export const GiB = (n: number): Size => Size(n * 1024 * 1024 * 1024) * * **Example** (Creating tebibyte sizes) * - * ```ts - * import { Console, Effect, FileSystem } from "effect" - * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Check if we're dealing with very large files - * const stats = yield* fs.stat("database-backup.sql") - * const oneTiB = FileSystem.TiB(1) - * - * if (stats.size > oneTiB) { - * yield* Console.log("This is a very large database backup!") + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * // Use larger chunk sizes for such files - * const stream = fs.stream("database-backup.sql", { - * chunkSize: FileSystem.MiB(100) // 100 MiB chunks - * }) - * } - * }) + * FileSystem.TiB(1) // => 1099511627776n * ``` * * @category sizes @@ -622,24 +565,10 @@ const bigintPiB = bigint1024 * bigint1024 * bigint1024 * bigint1024 * bigint1024 * * **Example** (Creating pebibyte sizes) * - * ```ts - * import { Console, Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { FileSystem } from "effect" * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // For extremely large data processing scenarios - * const massiveDataset = FileSystem.PiB(2) // 2 PiB - * - * // This would typically be used in enterprise/cloud scenarios - * yield* Console.log(`Processing ${massiveDataset} bytes of data`) - * - * // Such large files would require specialized streaming - * const stream = fs.stream("massive-dataset.bin", { - * chunkSize: FileSystem.GiB(1), // 1 GiB chunks - * offset: FileSystem.TiB(100) // Start from 100 TiB offset - * }) - * }) + * FileSystem.PiB(2) // => 2251799813685248n * ``` * * @category sizes @@ -668,24 +597,11 @@ export const PiB = (n: number): Size => Size(BigInt(n) * bigintPiB) * * **Example** (Opening files with flags) * - * ```ts - * import { Effect, FileSystem } from "effect" - * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Open for reading only - * const readFile = yield* fs.open("data.txt", { flag: "r" }) - * - * // Open for writing, truncating existing content - * const writeFile = yield* fs.open("output.txt", { flag: "w" }) - * - * // Open for appending - * const appendFile = yield* fs.open("log.txt", { flag: "a" }) + * ```ts import.meta.vitest + * import type { FileSystem } from "effect" * - * // Open for read/write, but fail if file doesn't exist - * const editFile = yield* fs.open("config.json", { flag: "r+" }) - * }) + * const flags: ReadonlyArray = ["r", "w", "a", "r+"] + * flags // => ["r", "w", "a", "r+"] * ``` * * @category models @@ -717,32 +633,28 @@ export type OpenFlag = * * **Example** (Accessing and providing FileSystem) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FileSystem } from "effect" * + * const customFs = FileSystem.makeNoop({ + * exists: () => Effect.succeed(true), + * readFileString: () => Effect.succeed("contents") + * }) + * * // Access the FileSystem service * const program = Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem * * const exists = yield* fs.exists("./data.txt") - * if (exists) { - * const content = yield* fs.readFileString("./data.txt") - * yield* Effect.log("File content:", content) - * } + * return exists ? yield* fs.readFileString("./data.txt") : undefined * }) * - * // Provide a custom FileSystem implementation - * declare const platformImpl: Omit< - * FileSystem.FileSystem, - * "exists" | "readFileString" | "stream" | "sink" | "writeFileString" - * > - * const customFs = FileSystem.make(platformImpl) - * * const withCustomFs = Effect.provideService( * program, * FileSystem.FileSystem, * customFs * ) + * Effect.runSync(withCustomFs) // => "contents" * ``` * * @category services @@ -870,7 +782,7 @@ const notFound = (method: string, path: string) => * * **Example** (Creating a no-op FileSystem) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FileSystem, PlatformError } from "effect" * * // Create a test filesystem that only allows reading specific files @@ -895,7 +807,7 @@ const notFound = (method: string, path: string) => * // Use in tests * const program = Effect.gen(function*() { * const content = yield* testFs.readFileString("test-config.json") - * // Will succeed with mocked content + * return content * }) * * // Test with the no-op filesystem @@ -904,6 +816,7 @@ const notFound = (method: string, path: string) => * FileSystem.FileSystem, * testFs * ) + * Effect.runSync(testProgram) // => "{\"test\": true}" * ``` * * @category constructors @@ -1015,7 +928,7 @@ export const makeNoop = (fileSystem: Partial): FileSystem => * * **Example** (Providing a no-op FileSystem layer) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FileSystem } from "effect" * * // Create a test layer with specific behaviors @@ -1032,6 +945,7 @@ export const makeNoop = (fileSystem: Partial): FileSystem => * * // Provide the test layer * const testProgram = Effect.provide(program, testLayer) + * Effect.runSync(testProgram) // => "mocked content" * ``` * * @category layers @@ -1074,7 +988,7 @@ export const FileTypeId = "~effect/platform/FileSystem/File" * @see {@link File} for the file-handle interface narrowed by this guard * @see {@link FileTypeId} for the runtime marker checked by this guard * - * @category file + * @category guards * @since 4.0.0 */ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) @@ -1090,44 +1004,43 @@ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) * * **Example** (Working with file handles) * - * ```ts - * import { Console, Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Option } from "effect" + * + * const file: FileSystem.File = { + * [FileSystem.FileTypeId]: FileSystem.FileTypeId, + * stat: Effect.succeed({ size: FileSystem.Size(5) } as FileSystem.File.Info), + * seek: () => Effect.succeed(FileSystem.Size(0)), + * sync: Effect.void, + * read: (buffer) => Effect.sync(() => { + * buffer.set([1, 2, 3, 4, 5]) + * return FileSystem.Size(5) + * }), + * readAlloc: () => Effect.succeed(Option.none()), + * truncate: () => Effect.void, + * write: (buffer) => Effect.succeed(FileSystem.Size(buffer.length)), + * writeAll: () => Effect.void + * } * * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // Open a file and work with the handle - * yield* Effect.scoped( - * Effect.gen(function*() { - * const file = yield* fs.open("./data.txt", { flag: "r+" }) - * - * // Get file information - * const stats = yield* file.stat - * yield* Console.log(`File size: ${stats.size} bytes`) - * - * // Read from specific position - * yield* file.seek(10, "start") - * const buffer = new Uint8Array(5) - * const bytesRead = yield* file.read(buffer) - * yield* Console.log(`Read ${bytesRead} bytes:`, buffer) - * - * // Write data - * const data = new TextEncoder().encode("Hello") - * yield* file.write(data) - * yield* file.sync // Flush to disk - * }) - * ) + * const stats = yield* file.stat + * const buffer = new Uint8Array(5) + * const bytesRead = yield* file.read(buffer) + * yield* file.writeAll(new TextEncoder().encode("Hello")) + * yield* file.sync + * return { size: stats.size, bytesRead, buffer: Array.from(buffer) } * }) + * + * Effect.runSync(program) // => { size: 5n, bytesRead: 5n, buffer: [1, 2, 3, 4, 5] } * ``` * - * @category file + * @category models * @since 4.0.0 */ export interface File { readonly [FileTypeId]: typeof FileTypeId - readonly fd: File.Descriptor readonly stat: Effect.Effect - readonly seek: (offset: SizeInput, from: SeekMode) => 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> @@ -1143,19 +1056,6 @@ export interface File { * @since 4.0.0 */ export declare namespace File { - /** - * Branded type for file descriptors. - * - * **Details** - * - * File descriptors are numeric handles used by the operating system - * to identify open files. The branded type ensures type safety. - * - * @category file - * @since 4.0.0 - */ - export type Descriptor = Brand.Branded - /** * Enumeration of possible file system entry types. * @@ -1164,7 +1064,7 @@ export declare namespace File { * Represents the different types of entries that can exist in a file system, * from regular files to special device files and symbolic links. * - * @category file + * @category models * @since 4.0.0 */ export type Type = @@ -1188,38 +1088,39 @@ export declare namespace File { * * **Example** (Inspecting file information) * - * ```ts - * import { Effect, FileSystem, Option } from "effect" - * - * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * const path = yield* fs.makeTempFile({ prefix: "info-" }) - * yield* fs.writeFileString(path, "hello") - * - * const info: FileSystem.File.Info = yield* fs.stat(path) - * - * console.log(`File type: ${info.type}`) // "File type: File" - * console.log(`File size: ${info.size} bytes`) // "File size: 5 bytes" - * console.log(`Mode: ${info.mode.toString(8)}`) // Octal permissions + * ```ts import.meta.vitest + * import { FileSystem, Option } from "effect" * - * // Handle optional timestamps without inventing a fallback date - * const modified = Option.match(info.mtime, { - * onNone: () => "unavailable", - * onSome: (mtime) => mtime.toISOString() - * }) - * console.log(`Modified: ${modified}`) + * const info: FileSystem.File.Info = { + * type: "File", + * mtime: Option.none(), + * atime: Option.none(), + * birthtime: Option.none(), + * dev: 1, + * ino: Option.none(), + * mode: 0o644, + * nlink: Option.none(), + * uid: Option.none(), + * gid: Option.none(), + * rdev: Option.none(), + * size: FileSystem.Size(5), + * blksize: Option.none(), + * blocks: Option.none() + * } * - * // Check if it's a regular file - * if (info.type === "File") { - * console.log("Processing regular file...") // "Processing regular file..." - * } + * info.type // => "File" + * info.size // => 5n + * info.mode.toString(8) // => "644" * - * yield* fs.remove(path) + * const modified = Option.match(info.mtime, { + * onNone: () => "unavailable", + * onSome: (mtime) => mtime.toISOString() * }) + * modified // => "unavailable" + * info.type === "File" // => true * ``` * - * @category file + * @category models * @since 4.0.0 */ export interface Info { @@ -1240,32 +1141,6 @@ export declare namespace File { } } -/** - * Creates a `File.Descriptor` from a number. - * - * **When to use** - * - * Use to brand an operating-system file descriptor number when implementing a - * `FileSystem` that returns custom `File` handles. - * - * **Details** - * - * `File.Descriptor` is a branded integer handle used by operating systems to - * identify open files. - * - * **Gotchas** - * - * This constructor is nominal and does not check that the number is an integer - * or that it refers to an open file descriptor. - * - * @see {@link File.Descriptor} for the branded descriptor type produced by this constructor - * @see {@link File} for file handles that expose a descriptor through `fd` - * - * @category constructors - * @since 4.0.0 - */ -export const FileDescriptor = Brand.nominal() - /** * Specifies the reference point for seeking within an open file. * @@ -1287,6 +1162,19 @@ export const FileDescriptor = Brand.nominal() */ export type SeekMode = "start" | "current" +/** + * Options for watching files or directories. + * + * @category models + * @since 4.0.0 + */ +export interface WatchOptions { + /** + * When `true`, changes in subdirectories are also reported. + */ + readonly recursive?: boolean | undefined +} + /** * Represents file system events emitted when watching files or directories. * @@ -1373,7 +1261,7 @@ export declare namespace WatchEvent { * * **Example** (Providing a custom watch backend) * - * ```ts + * ```ts import.meta.vitest * import { Effect, FileSystem, Option, Stream } from "effect" * * // Custom watch backend implementation @@ -1384,12 +1272,11 @@ export declare namespace WatchEvent { * } * } * - * // Provide custom watch backend * const program = Effect.gen(function*() { - * const fs = yield* FileSystem.FileSystem - * - * // File watching will use the custom backend - * const watcher = fs.watch("./directory") + * const backend = yield* FileSystem.WatchBackend + * return Option.isSome( + * backend.register("./directory", { type: "Directory" } as FileSystem.File.Info) + * ) * }) * * const withCustomBackend = Effect.provideService( @@ -1397,11 +1284,16 @@ export declare namespace WatchEvent { * FileSystem.WatchBackend, * customWatchBackend * ) + * Effect.runSync(withCustomBackend) // => true * ``` * - * @category file watcher + * @category services * @since 4.0.0 */ export class WatchBackend extends Context.Service Option.Option> + readonly register: ( + path: string, + stat: File.Info, + options?: WatchOptions + ) => Option.Option> }>()("effect/platform/FileSystem/WatchBackend") {} diff --git a/.context/effect/packages/effect/src/Filter.ts b/.context/effect/packages/effect/src/Filter.ts index c492eb2e7..631d3e03d 100644 --- a/.context/effect/packages/effect/src/Filter.ts +++ b/.context/effect/packages/effect/src/Filter.ts @@ -28,14 +28,14 @@ import type { EqualsWith, ExcludeTag, ExtractReason, ExtractTag, ReasonTags, Tag * * **Example** (Defining a positive number filter) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * * // A filter that only passes positive numbers * const positiveFilter: Filter.Filter = (n) => n > 0 ? Result.succeed(n) : Result.fail(n) * - * console.log(positiveFilter(5)) // Result.succeed(5) - * console.log(positiveFilter(-3)) // Result.fail(-3) + * positiveFilter(5) // => Result.succeed(5) + * positiveFilter(-3) // => Result.fail(-3) * ``` * * @category models @@ -56,7 +56,7 @@ export interface Filter { * * **Example** (Defining an effectful user filter) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Filter, Result } from "effect" * * // An effectful filter that validates user data @@ -74,6 +74,9 @@ export interface Filter { * const user: User = { id, isActive: id.length > 0 } * return user.isActive ? Result.succeed(user) : Result.fail(user) * }) + * + * await Effect.runPromise(validateUser("alice")) // => Result.succeed({ id: "alice", isActive: true }) + * await Effect.runPromise(validateUser("")) // => Result.fail({ id: "", isActive: false }) * ``` * * @category models @@ -103,7 +106,7 @@ export interface FilterEffect< * * **Example** (Creating custom filters) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * * // Create a filter for positive numbers @@ -113,6 +116,8 @@ export interface FilterEffect< * const uppercaseFilter = Filter.make((s: string) => * s.length > 0 ? Result.succeed(s.toUpperCase()) : Result.fail(s) * ) + * positiveFilter(1) // => Result.succeed(1) + * uppercaseFilter("ok") // => Result.succeed("OK") * ``` * * @category constructors @@ -133,7 +138,7 @@ export const make = ( * * **Example** (Creating effectful filters) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Filter, Result } from "effect" * * // Create an effectful filter that validates async @@ -143,6 +148,8 @@ export const make = ( * return isValid ? Result.succeed(id) : Result.fail(id) * }) * ) + * + * await Effect.runPromise(asyncValidate("id")) // => Result.succeed("id") * ``` * * @category constructors @@ -201,7 +208,7 @@ export { * * **Example** (Creating filters from predicates) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * * // Create filter from predicate @@ -212,6 +219,9 @@ export { * const isString = Filter.fromPredicate((x: unknown): x is string => * typeof x === "string" * ) + * positiveNumbers(1) // => Result.succeed(1) + * nonEmptyStrings("") // => Result.fail("") + * isString("ok") // => Result.succeed("ok") * ``` * * @category constructors @@ -259,11 +269,11 @@ export const toPredicate = ( * * **Example** (Filtering strings) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * - * console.log(Filter.string("hello")) // Result.succeed("hello") - * console.log(Filter.string(42)) // fail + * Filter.string("hello") // => Result.succeed("hello") + * Filter.string(42) // => Result.fail(42) * ``` * * @category constructors @@ -350,11 +360,11 @@ export const instanceOf = * * **Example** (Filtering numbers) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * - * console.log(Filter.number(42)) // Result.succeed(42) - * console.log(Filter.number("42")) // fail + * Filter.number(42) // => Result.succeed(42) + * Filter.number("42") // => Result.fail("42") * ``` * * @category constructors @@ -617,13 +627,14 @@ export const zipWith: { * * **Example** (Zipping filters) * - * ```ts - * import { Filter } from "effect" + * ```ts import.meta.vitest + * import { Filter, Result } from "effect" * * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) * const evenNumbers = Filter.fromPredicate((n: number) => n % 2 === 0) * * const positiveAndEven = Filter.zip(positiveNumbers, evenNumbers) + * positiveAndEven(2) // => Result.succeed([2, 2]) * ``` * * @category combinators @@ -650,13 +661,14 @@ export const zip: { * * **Example** (Keeping the left filter result) * - * ```ts - * import { Filter } from "effect" + * ```ts import.meta.vitest + * import { Filter, Result } from "effect" * * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) * const evenNumbers = Filter.fromPredicate((n: number) => n % 2 === 0) * * const positiveEven = Filter.andLeft(positiveNumbers, evenNumbers) + * positiveEven(2) // => Result.succeed(2) * ``` * * @category combinators @@ -682,7 +694,7 @@ export const andLeft: { * * **Example** (Keeping the right filter result) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * * const positiveNumbers = Filter.fromPredicate((n: number) => n > 0) @@ -691,6 +703,7 @@ export const andLeft: { * ) * * const positiveDoubled = Filter.andRight(positiveNumbers, doubleNumbers) + * positiveDoubled(2) // => Result.succeed(4) * ``` * * @category combinators @@ -716,7 +729,7 @@ export const andRight: { * * **Example** (Composing filters) * - * ```ts + * ```ts import.meta.vitest * import { Filter, Result } from "effect" * * const stringFilter = Filter.string @@ -725,6 +738,7 @@ export const andRight: { * ) * * const stringToUpper = Filter.compose(stringFilter, nonEmptyUpper) + * stringToUpper("hello") // => Result.succeed("HELLO") * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/Formatter.ts b/.context/effect/packages/effect/src/Formatter.ts index 426131f10..db202ef93 100644 --- a/.context/effect/packages/effect/src/Formatter.ts +++ b/.context/effect/packages/effect/src/Formatter.ts @@ -25,13 +25,12 @@ import { getRedacted, redact, symbolRedactable } from "./Redactable.ts" * * **Example** (Defining a custom formatter) * - * ```ts + * ```ts import.meta.vitest * import type { Formatter } from "effect" * * const upper: Formatter.Formatter = (s) => s.toUpperCase() * - * console.log(upper("hello")) - * // HELLO + * upper("hello") // => "HELLO" * ``` * * @see {@link format} @@ -75,37 +74,29 @@ export interface Formatter { * * **Example** (Formatting compact output) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * - * console.log(Formatter.format({ a: 1, b: [2, 3] })) - * // {"a":1,"b":[2,3]} + * Formatter.format({ a: 1, b: [2, 3] }) // => "{\"a\":1,\"b\":[2,3]}" * ``` * * **Example** (Pretty-printed output) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * - * console.log(Formatter.format({ a: 1, b: [2, 3] }, { space: 2 })) - * // { - * // "a": 1, - * // "b": [ - * // 2, - * // 3 - * // ] - * // } + * const output = Formatter.format({ a: 1, b: [2, 3] }, { space: 2 }) + * output // => "{\n \"a\": 1,\n \"b\": [\n 2,\n 3\n ]\n}" * ``` * * **Example** (Handling circular references) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * * const obj: any = { name: "loop" } * obj.self = obj - * console.log(Formatter.format(obj)) - * // {"name":"loop","self":[Circular]} + * Formatter.format(obj) // => "{\"name\":\"loop\",\"self\":[Circular]}" * ``` * * @see {@link formatJson} @@ -118,7 +109,7 @@ export function format(input: unknown, options?: { readonly ignoreToString?: boolean | undefined }): string { const space = options?.space ?? 0 - const seen = new WeakSet() + const ancestors = new WeakSet() const gap = !space ? "" : (typeof space === "number" ? " ".repeat(space) : space) const ind = (d: number) => gap.repeat(d) @@ -136,30 +127,6 @@ export function format(input: unknown, options?: { } function recur(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) => recur(x, d)).join(",")}]` - const inner = v.map((x) => recur(x, d + 1)).join(",\n" + ind(d + 1)) - return `[\n${ind(d + 1)}${inner}\n${ind(d)}]` - } - - if (v instanceof Date) return formatDate(v) - - if ( - !options?.ignoreToString && - Predicate.hasProperty(v, "toString") && - typeof v["toString"] === "function" && - v["toString"] !== Object.prototype.toString && - v["toString"] !== Array.prototype.toString - ) { - const s = safeToString(v) - if (v instanceof Error && v.cause) { - return `${s} (cause: ${recur(v.cause, d)})` - } - return s - } - if (typeof v === "string") return JSON.stringify(v) if ( @@ -172,24 +139,43 @@ export function format(input: unknown, options?: { if (typeof v === "bigint") return String(v) + "n" if (typeof v === "object" || typeof v === "function") { - if (seen.has(v)) return CIRCULAR - seen.add(v) + if (ancestors.has(v)) return CIRCULAR + ancestors.add(v) - if (symbolRedactable in v) return format(getRedacted(v as any)) - - if (Symbol.iterator in v) { - return `${v.constructor.name}(${recur(Array.from(v as any), d)})` - } - - const keys = ownKeys(v) - if (!gap || keys.length <= 1) { - const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${recur((v as any)[k], d)}`).join(",")}}` - return wrap(v, body) + let output: string + if (symbolRedactable in v) { + output = recur(getRedacted(v as any), d) + } else if (Array.isArray(v)) { + output = !gap || v.length <= 1 + ? `[${v.map((x) => recur(x, d)).join(",")}]` + : `[\n${ind(d + 1)}${v.map((x) => recur(x, d + 1)).join(",\n" + ind(d + 1))}\n${ind(d)}]` + } else if (v instanceof Date) { + output = formatDate(v) + } else if ( + !options?.ignoreToString && + Predicate.hasProperty(v, "toString") && + typeof v["toString"] === "function" && + v["toString"] !== Object.prototype.toString && + v["toString"] !== Array.prototype.toString + ) { + const s = safeToString(v) + output = v instanceof Error && v.cause ? `${s} (cause: ${recur(v.cause, d)})` : s + } else if (Symbol.iterator in v) { + output = `${v.constructor.name}(${recur(Array.from(v as any), d)})` + } else { + const keys = ownKeys(v) + if (!gap || keys.length <= 1) { + const body = `{${keys.map((k) => `${formatPropertyKey(k)}:${recur((v as any)[k], d)}`).join(",")}}` + output = wrap(v, body) + } else { + const body = `{\n${ + keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${recur((v as any)[k], d + 1)}`).join(",\n") + }\n${ind(d)}}` + output = wrap(v, body) + } } - const body = `{\n${ - keys.map((k) => `${ind(d + 1)}${formatPropertyKey(k)}: ${recur((v as any)[k], d + 1)}`).join(",\n") - }\n${ind(d)}}` - return wrap(v, body) + ancestors.delete(v) + return output } return String(v) @@ -253,40 +239,41 @@ function safeToString(input: any): string { * Uses `JSON.stringify` internally with a replacer that tracks the current * object ancestry. Circular references are replaced with `undefined`, which * omits them from object output. `Redactable` values are automatically redacted - * before serialization. Values not supported by JSON, such as `BigInt`, - * `Symbol`, `undefined`, and functions, follow standard `JSON.stringify` + * before serialization. `BigInt` values are stringified with an `n` suffix. + * Values not supported by JSON otherwise follow standard `JSON.stringify` * behavior. The `space` parameter controls indentation and defaults to `0`. * + * **Gotchas** + * + * When the root input is `undefined`, a symbol, or a function, `formatJson` + * returns `"null"` instead of the `undefined` returned by `JSON.stringify`. + * Nested values retain standard `JSON.stringify` behavior. + * * **Example** (Formatting compact JSON) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * - * console.log(Formatter.formatJson({ name: "Alice", age: 30 })) - * // {"name":"Alice","age":30} + * Formatter.formatJson({ name: "Alice", age: 30 }) // => "{\"name\":\"Alice\",\"age\":30}" * ``` * * **Example** (Handling circular references) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * * const obj: any = { name: "test" } * obj.self = obj - * console.log(Formatter.formatJson(obj)) - * // {"name":"test"} + * Formatter.formatJson(obj) // => "{\"name\":\"test\"}" * ``` * * **Example** (Pretty-printed JSON) * - * ```ts + * ```ts import.meta.vitest * import { Formatter } from "effect" * - * console.log(Formatter.formatJson({ name: "Alice", age: 30 }, { space: 2 })) - * // { - * // "name": "Alice", - * // "age": 30 - * // } + * const output = Formatter.formatJson({ name: "Alice", age: 30 }, { space: 2 }) + * output // => "{\n \"name\": \"Alice\",\n \"age\": 30\n}" * ``` * * @see {@link format} @@ -300,8 +287,14 @@ export function formatJson(input: unknown, options?: { const ancestors: Array = [] return JSON.stringify( input, - function(this: unknown, _key: string, value: unknown) { - const redacted = redact(value) + function(this: object, key: string, value: unknown) { + const original = Object.getOwnPropertyDescriptor(this, key)?.value + const redacted = Predicate.hasProperty(original, symbolRedactable) + ? redact(original) + : redact(value) + if (typeof redacted === "bigint") { + return format(redacted) + } if (typeof redacted !== "object" || redacted === null) { return redacted } @@ -315,5 +308,5 @@ export function formatJson(input: unknown, options?: { return redacted }, options?.space - ) + ) ?? "null" } diff --git a/.context/effect/packages/effect/src/Function.ts b/.context/effect/packages/effect/src/Function.ts index 02506c83f..f12e50a74 100644 --- a/.context/effect/packages/effect/src/Function.ts +++ b/.context/effect/packages/effect/src/Function.ts @@ -21,7 +21,7 @@ import { pipeArguments } from "./Pipeable.ts" * * **Example** (Creating a function type with a type lambda) * - * ```ts + * ```ts import.meta.vitest * import type { Function, HKT } from "effect" * * // Create a function type using the type lambda @@ -29,7 +29,7 @@ import { pipeArguments } from "./Pipeable.ts" * // Equivalent to: (a: string) => number * ``` * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface FunctionTypeLambda extends TypeLambda { @@ -53,7 +53,7 @@ export interface FunctionTypeLambda extends TypeLambda { * * **Example** (Selecting data-first or data-last style by arity) * - * ```ts + * ```ts import.meta.vitest * import { Function, pipe } from "effect" * * const sum = Function.dual< @@ -61,13 +61,13 @@ export interface FunctionTypeLambda extends TypeLambda { * (self: number, that: number) => number * >(2, (self, that) => self + that) * - * console.log(sum(2, 3)) // 5 - * console.log(pipe(2, sum(3))) // 5 + * sum(2, 3) // => 5 + * pipe(2, sum(3)) // => 5 * ``` * * **Example** (Defining overloads with call signatures) * - * ```ts + * ```ts import.meta.vitest * import { Function, pipe } from "effect" * * const sum: { @@ -75,13 +75,13 @@ export interface FunctionTypeLambda extends TypeLambda { * (self: number, that: number): number * } = Function.dual(2, (self: number, that: number): number => self + that) * - * console.log(sum(2, 3)) // 5 - * console.log(pipe(2, sum(3))) // 5 + * sum(2, 3) // => 5 + * pipe(2, sum(3)) // => 5 * ``` * * **Example** (Selecting data-first or data-last style with a predicate) * - * ```ts + * ```ts import.meta.vitest * import { Function, pipe } from "effect" * * const sum = Function.dual< @@ -92,8 +92,8 @@ export interface FunctionTypeLambda extends TypeLambda { * (self, that) => self + that * ) * - * console.log(sum(2, 3)) // 5 - * console.log(pipe(2, sum(3))) // 5 + * sum(2, 3) // => 5 + * pipe(2, sum(3)) // => 5 * ``` * * @category combinators @@ -169,11 +169,10 @@ export const dual: { * * **Example** (Applying an argument to a function) * - * ```ts + * ```ts import.meta.vitest * import { Function, pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe(String.length, Function.apply("hello")), 5) + * pipe(String.length, Function.apply("hello")) // => 5 * ``` * * @see {@link pipe} for building left-to-right pipelines @@ -192,10 +191,11 @@ export const apply = (a: A) => (self: (a: A) => B): B => self(a) * * **Example** (Creating a lazy argument) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" * * const constNull: Function.LazyArg = Function.constant(null) + * constNull() // => null * ``` * * @category models @@ -213,12 +213,11 @@ export type LazyArg = () => A * * **Example** (Typing a variadic function) * - * ```ts + * ```ts import.meta.vitest * import type { Function } from "effect" - * import * as assert from "node:assert" * * const sum: Function.FunctionN<[number, number], number> = (a, b) => a + b - * assert.deepStrictEqual(sum(2, 3), 5) + * sum(2, 3) // => 5 * ``` * * @category models @@ -235,11 +234,10 @@ export type FunctionN, B> = (...args: A) => B * * **Example** (Returning the same value) * - * ```ts + * ```ts import.meta.vitest * import { identity } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(identity(5), 5) + * identity(5) // => 5 * ``` * * @category combinators @@ -258,17 +256,14 @@ export const identity = (a: A): A => a * * **Example** (Checking an expression against a type) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * const test1 = Function.satisfies()(5 as const) + * const test1 = Function.satisfies()(5 as const) // => 5 * // ^? const test: 5 * // @ts-expect-error * const test2 = Function.satisfies()(5) * // ^? Argument of type 'number' is not assignable to parameter of type 'string' - * - * assert.deepStrictEqual(Function.satisfies()(5), 5) * ``` * * @see {@link cast} for changing only the static TypeScript type @@ -308,14 +303,13 @@ export const cast: (a: A) => B = identity as any * * **Example** (Creating a constant thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * * const constNull = Function.constant(null) * - * assert.deepStrictEqual(constNull(), null) - * assert.deepStrictEqual(constNull(), null) + * constNull() // => null + * constNull() // => null * ``` * * @category constructors @@ -332,11 +326,10 @@ export const constant = (value: A): LazyArg => () => value * * **Example** (Returning true from a thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.constTrue(), true) + * Function.constTrue() // => true * ``` * * @category constants @@ -353,11 +346,10 @@ export const constTrue: LazyArg = constant(true) * * **Example** (Returning false from a thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.constFalse(), false) + * Function.constFalse() // => false * ``` * * @category constants @@ -374,11 +366,10 @@ export const constFalse: LazyArg = constant(false) * * **Example** (Returning null from a thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.constNull(), null) + * Function.constNull() // => null * ``` * * @category constants @@ -395,11 +386,10 @@ export const constNull: LazyArg = constant(null) * * **Example** (Returning undefined from a thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.constUndefined(), undefined) + * Function.constUndefined() // => undefined * ``` * * @category constants @@ -417,11 +407,10 @@ export const constUndefined: LazyArg = constant(undefined) * * **Example** (Returning void from a thunk) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.constVoid(), undefined) + * Function.constVoid() // => undefined * ``` * * @category constants @@ -439,13 +428,12 @@ export const constVoid: LazyArg = constUndefined * * **Example** (Flipping curried arguments) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * * const f = (a: number) => (b: string) => a - b.length * - * assert.deepStrictEqual(Function.flip(f)("aaa")(2), -1) + * Function.flip(f)("aaa")(2) // => -1 * ``` * * @category combinators @@ -467,14 +455,13 @@ export const flip = , B extends Array, C>( * * **Example** (Composing two functions) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * * const increment = (n: number) => n + 1 * const square = (n: number) => n * n * - * assert.strictEqual(Function.compose(increment, square)(2), 9) + * Function.compose(increment, square)(2) // => 9 * ``` * * @see {@link flow} for composing a left-to-right sequence of functions @@ -504,7 +491,7 @@ export const compose: { * * **Example** (Handling impossible values) * - * ```ts + * ```ts import.meta.vitest * import { absurd } from "effect" * * const handleNever = (value: never) => { @@ -528,13 +515,12 @@ export const absurd = (_: never): A => { * * **Example** (Converting arguments to a tuple) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * * const sumTupled = Function.tupled((x: number, y: number): number => x + y) * - * assert.deepStrictEqual(sumTupled([1, 2]), 3) + * sumTupled([1, 2]) // => 3 * ``` * * @see {@link untupled} for adapting a tuple-argument function back to multiple arguments @@ -553,13 +539,12 @@ export const tupled = , B>(f: (...a: A) => B): * * **Example** (Converting a tuple to arguments) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * * const getFirst = Function.untupled((tuple: [A, B]): A => tuple[0]) * - * assert.deepStrictEqual(getFirst(1, 2), 1) + * getFirst(1, 2) // => 1 * ``` * * @see {@link tupled} for adapting a multi-argument function to one tuple argument @@ -593,82 +578,33 @@ export const untupled = , B>(f: (a: A) => B): ( * In this example, `1` is passed to the first function, and each result becomes * the input for the next function. * - * ```ts + * ```ts import.meta.vitest * import { pipe } from "effect" * - * const result = pipe( + * pipe( * 1, * (n) => n + 1, * (n) => n * 2, * (n) => `result: ${n}` - * ) - * - * console.log(result) // "result: 4" - * ``` - * - * **Example** (Chaining methods before conversion) - * - * ```ts - * const numbers = [1, 2, 3, 4] - * const double = (n: number) => n * 2 - * const greaterThanFour = (n: number) => n > 4 - * - * const result = numbers.map(double).filter(greaterThanFour) - * - * console.log(result) // [6, 8] + * ) // => "result: 4" * ``` * * **Example** (Rewriting method chains with pipe) * * The same transformation can be written with data-last functions. * - * ```ts + * ```ts import.meta.vitest * import { Array, pipe } from "effect" * * const numbers = [1, 2, 3, 4] * const double = (n: number) => n * 2 * const greaterThanFour = (n: number) => n > 4 * - * const result = pipe( + * pipe( * numbers, * Array.map(double), * Array.filter(greaterThanFour) - * ) - * - * console.log(result) // [6, 8] - * ``` - * - * **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 - * ``` - * - * **Example** (Building a simple transformation pipeline) - * - * ```ts - * import { pipe } from "effect" - * - * // Simple transformation pipeline - * const result = pipe( - * 5, - * (x) => x * 2, // 10 - * (x) => x + 1, // 11 - * (x) => x.toString() // "11" - * ) - * - * console.log(result) // "11" + * ) // => [6, 8] * ``` * * @category combinators @@ -1141,16 +1077,15 @@ export function pipe(a: unknown, ...args: Array): unknown { * * **Example** (Composing functions left to right) * - * ```ts + * ```ts import.meta.vitest * import { flow } from "effect" - * import * as assert from "node:assert" * * const len = (s: string): number => s.length * const double = (n: number): number => n * 2 * * const f = flow(len, double) * - * assert.strictEqual(f("aaa"), 6) + * f("aaa") // => 6 * ``` * * @see {@link pipe} for applying a value through a left-to-right sequence immediately @@ -1338,7 +1273,7 @@ export function flow( * * **Example** (Creating a development placeholder) * - * ```ts + * ```ts import.meta.vitest * import { hole } from "effect" * * // Intentionally not called: `hole` throws if the placeholder is evaluated. @@ -1347,7 +1282,6 @@ export function flow( * name: hole() * }) * - * console.log(typeof buildUser) // "function" * ``` * * @category utility types @@ -1366,11 +1300,10 @@ export const hole: () => T = cast(absurd) * * **Example** (Discarding the first argument) * - * ```ts + * ```ts import.meta.vitest * import { Function } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Function.SK(0, "hello"), "hello") + * Function.SK(0, "hello") // => "hello" * ``` * * @category combinators @@ -1390,11 +1323,12 @@ export const SK = (_: A, b: B): B => b * **Details** * * Each memoized wrapper owns a private `WeakMap` keyed by object identity. - * Cached `undefined` results are still returned because the cache is checked - * with `WeakMap.has`. * * **Gotchas** * + * `undefined` is reserved to represent a cache miss and is therefore not + * supported as a return value. + * * Structurally equal objects do not share cache entries. If the same object is * mutated after its first call, later calls still return the cached result for * that reference. @@ -1402,14 +1336,50 @@ export const SK = (_: A, b: B): B => b * @category caching * @since 4.0.0 */ -export function memoize(f: (a: A) => O): (ast: A) => O { +export function memoize(f: (a: A) => O): (ast: A) => O { const cache = new WeakMap() return (a) => { - if (cache.has(a)) { - return cache.get(a)! - } + const cached = cache.get(a) + if (cached !== undefined) return cached + const result = f(a) + cache.set(a, result) + return result + } +} + +/** + * Creates a memoized idempotent object transformation that caches both inputs + * and their outputs by object identity. + * + * **When to use** + * + * Use when an object transformation is idempotent and its output can be safely + * reused as a fixed point. + * + * **Details** + * + * After computing an input, the returned function caches both the input and + * the output. Calling it with either reference returns the output without + * invoking the supplied function again. + * + * **Gotchas** + * + * The returned function treats each computed output as a fixed point. If + * applying the supplied function to an output would produce an observably + * different value, this memoization changes that behavior. + * + * @see {@link memoize} for memoizing functions without an idempotence requirement + * @category caching + * @since 4.0.0 + */ +export function memoizeIdempotent(f: (a: A) => A): (a: A) => A { + const cache = new WeakMap() + return (a) => { + const cached = cache.get(a) + if (cached !== undefined) return cached const result = f(a) cache.set(a, result) + cache.set(result, result) return result } } diff --git a/.context/effect/packages/effect/src/Graph.ts b/.context/effect/packages/effect/src/Graph.ts index dd4472746..f564b512b 100644 --- a/.context/effect/packages/effect/src/Graph.ts +++ b/.context/effect/packages/effect/src/Graph.ts @@ -482,7 +482,7 @@ export const isGraph = "directed" * ``` * * @see {@link directed} for constructing a directed graph directly @@ -525,7 +525,7 @@ export const make = * * **Example** (Creating a directed graph) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Directed graph with initial nodes and edges @@ -536,6 +536,7 @@ export const make = * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) + * Array.of(Graph.nodeCount(graph), Graph.edgeCount(graph)) // => [3, 2] * ``` * * @category constructors @@ -550,7 +551,7 @@ export const directed: ( * * **Example** (Creating an undirected graph) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Undirected graph with initial nodes and edges @@ -561,6 +562,7 @@ export const directed: ( * Graph.addEdge(mutable, a, b, "A-B") * Graph.addEdge(mutable, b, c, "B-C") * }) + * Array.of(Graph.nodeCount(graph), Graph.edgeCount(graph)) // => [3, 2] * ``` * * @category constructors @@ -579,12 +581,13 @@ export const undirected: ( * * **Example** (Beginning a mutation scope) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // Now mutable can be safely modified without affecting original graph + * Array.of(Graph.nodeCount(mutable), Graph.nodeCount(graph)) // => [0, 0] * ``` * * @category mutations @@ -621,13 +624,13 @@ export const beginMutation = ( * * **Example** (Ending a mutation scope) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed() * const mutable = Graph.beginMutation(graph) * // ... perform mutations on mutable ... - * const newGraph = Graph.endMutation(mutable) + * Graph.nodeCount(Graph.endMutation(mutable)) // => 0 * ``` * * @category mutations @@ -673,7 +676,7 @@ const mutateScoped = ( * * **Example** (Applying scoped mutations) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed() @@ -683,8 +686,8 @@ const mutateScoped = ( * Graph.addEdge(mutable, nodeA, nodeB, 1) * }) * - * console.log(Graph.nodeCount(newGraph)) // 2 - * console.log(Graph.edgeCount(newGraph)) // 1 + * Graph.nodeCount(newGraph) // => 2 + * Graph.edgeCount(newGraph) // => 1 * ``` * * @category mutations @@ -875,7 +878,7 @@ const assertSameKind = (self: Graph, that: Graph): * * **Example** (Combining graphs) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const left = Graph.directed<{ id: string }, string>((mutable) => { @@ -894,8 +897,8 @@ const assertSameKind = (self: Graph, that: Graph): * nodeIdentity: (node) => node.id * }) * - * console.log(Graph.nodeCount(result)) // 3 - * console.log(Graph.edgeCount(result)) // 2 + * Graph.nodeCount(result) // => 3 + * Graph.edgeCount(result) // => 2 * ``` * * @category set operations @@ -967,7 +970,7 @@ export const compose: { * * **Example** (Finding shared structure) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const left = Graph.directed((mutable) => { @@ -984,8 +987,8 @@ export const compose: { * * const result = Graph.intersection(left, right) * - * console.log(Graph.nodeCount(result)) // 2 - * console.log(Graph.edgeCount(result)) // 1 + * Graph.nodeCount(result) // => 2 + * Graph.edgeCount(result) // => 1 * ``` * * @category set operations @@ -1063,7 +1066,7 @@ export const intersection: { * * **Example** (Removing shared edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const left = Graph.directed((mutable) => { @@ -1082,8 +1085,8 @@ export const intersection: { * * const result = Graph.difference(left, right) * - * console.log(Graph.nodeCount(result)) // 3 - * console.log(Graph.edgeCount(result)) // 1 + * Graph.nodeCount(result) // => 3 + * Graph.edgeCount(result) // => 1 * ``` * * @category set operations @@ -1149,7 +1152,7 @@ export const difference: { * * **Example** (Finding differing edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const left = Graph.directed((mutable) => { @@ -1170,8 +1173,8 @@ export const difference: { * * const result = Graph.symmetricDifference(left, right) * - * console.log(Graph.nodeCount(result)) // 4 - * console.log(Graph.edgeCount(result)) // 2 + * Graph.nodeCount(result) // => 4 + * Graph.edgeCount(result) // => 2 * ``` * * @category set operations @@ -1239,7 +1242,7 @@ export const symmetricDifference: { * * **Example** (Finding missing relationships) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -1250,7 +1253,7 @@ export const symmetricDifference: { * * const result = Graph.complement(graph, (source, target) => `${source}-${target}`) * - * console.log(Graph.edgeCount(result)) // 1 + * Graph.edgeCount(result) // => 1 * ``` * * @category set operations @@ -1328,7 +1331,7 @@ export interface NeighborhoodConfig { * * **Example** (Getting a local neighborhood) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -1341,7 +1344,7 @@ export interface NeighborhoodConfig { * * const result = Graph.neighborhood(graph, 1, { radius: 1 }) * - * console.log(Graph.nodeCount(result)) // 2 + * Graph.nodeCount(result) // => 2 * ``` * * @category set operations @@ -1456,14 +1459,12 @@ export const sum: { * * **Example** (Adding nodes) * - * ```ts + * ```ts import.meta.vitest * 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 + * Graph.mutate(Graph.directed(), (mutable) => { + * Graph.addNode(mutable, "Node A") // => 0 + * Graph.addNode(mutable, "Node B") // => 1 * }) * ``` * @@ -1501,19 +1502,14 @@ export const addNode = ( * * **Example** (Getting node data) * - * ```ts + * ```ts import.meta.vitest * 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" - * } + * Graph.getNode(graph, 0) // => Option.some("Node A") * ``` * * @category getters @@ -1540,20 +1536,15 @@ export const getNode: { * * **Example** (Checking node existence) * - * ```ts + * ```ts import.meta.vitest * 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 + * Graph.hasNode(graph, 0) // => true + * Graph.hasNode(graph, 999) // => false * ``` * * @category getters @@ -1572,11 +1563,11 @@ export const hasNode: { * * **Example** (Counting nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const emptyGraph = Graph.directed() - * console.log(Graph.nodeCount(emptyGraph)) // 0 + * Graph.nodeCount(emptyGraph) // => 0 * * const graphWithNodes = Graph.mutate(emptyGraph, (mutable) => { * Graph.addNode(mutable, "Node A") @@ -1584,7 +1575,7 @@ export const hasNode: { * Graph.addNode(mutable, "Node C") * }) * - * console.log(Graph.nodeCount(graphWithNodes)) // 3 + * Graph.nodeCount(graphWithNodes) // => 3 * ``` * * @category getters @@ -1599,8 +1590,8 @@ export const nodeCount = ( * * **Example** (Finding the first matching node) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * Graph.addNode(mutable, "Node A") @@ -1608,11 +1599,8 @@ export const nodeCount = ( * 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() + * Graph.findNode(graph, (data) => data.startsWith("Node B")) // => Option.some(1) + * Graph.findNode(graph, (data) => data === "Node D") // => Option.none() * ``` * * @category getters @@ -1644,7 +1632,7 @@ export const findNode: { * * **Example** (Finding matching nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { @@ -1653,11 +1641,8 @@ export const findNode: { * 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) // [] + * Graph.findNodes(graph, (data) => data.startsWith("Start")) // => [0, 2] + * Graph.findNodes(graph, (data) => data === "Not Found") // => [] * ``` * * @category getters @@ -1690,8 +1675,8 @@ export const findNodes: { * * **Example** (Finding the first matching edge) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") @@ -1701,11 +1686,8 @@ export const findNodes: { * 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() + * Graph.findEdge(graph, (data) => data > 15) // => Option.some(1) + * Graph.findEdge(graph, (data) => data > 100) // => Option.none() * ``` * * @category getters @@ -1737,7 +1719,7 @@ export const findEdge: { * * **Example** (Finding matching edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { @@ -1749,11 +1731,8 @@ export const findEdge: { * 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) // [] + * Graph.findEdges(graph, (data) => data >= 20) // => [1, 2] + * Graph.findEdges(graph, (data) => data > 100) // => [] * ``` * * @category getters @@ -1786,8 +1765,8 @@ export const findEdges: { * * **Example** (Updating node data) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "Node A") @@ -1795,8 +1774,7 @@ export const findEdges: { * Graph.updateNode(mutable, 0, (data) => data.toUpperCase()) * }) * - * const nodeData = Graph.getNode(graph, 0) - * console.log(nodeData) // Option.some("NODE A") + * Graph.getNode(graph, 0) // => Option.some("NODE A") * ``` * * @category transforming @@ -1824,8 +1802,8 @@ export const updateNode = ( * * **Example** (Updating edge data) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") @@ -1834,8 +1812,7 @@ export const updateNode = ( * Graph.updateEdge(mutable, edgeIndex, (data) => data * 2) * }) * - * const edgeData = Graph.getEdge(result, 0) - * console.log(edgeData) // Option.some(new Graph.Edge({ source: 0, target: 1, data: 20 })) + * Option.map(Graph.getEdge(result, 0), (edge) => edge.data) // => Option.some(20) * ``` * * @category mutations @@ -1868,8 +1845,8 @@ export const updateEdge = ( * * **Example** (Mapping node data) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * Graph.addNode(mutable, "node a") @@ -1878,8 +1855,7 @@ export const updateEdge = ( * Graph.mapNodes(mutable, (data) => data.toUpperCase()) * }) * - * const nodeData = Graph.getNode(graph, 0) - * console.log(nodeData) // Option.some("NODE A") + * Graph.getNode(graph, 0) // => Option.some("NODE A") * ``` * * @category transforming @@ -1904,8 +1880,8 @@ export const mapNodes = ( * * **Example** (Mapping edge data) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") @@ -1916,8 +1892,7 @@ export const mapNodes = ( * Graph.mapEdges(mutable, (data) => data * 2) * }) * - * const edgeData = Graph.getEdge(graph, 0) - * console.log(edgeData) // Option.some(new Graph.Edge({ source: 0, target: 1, data: 20 })) + * Option.map(Graph.getEdge(graph, 0), (edge) => edge.data) // => Option.some(20) * ``` * * @category transforming @@ -1973,8 +1948,8 @@ const rebuildAdjacency = ( * * **Example** (Reversing edge directions) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") @@ -1985,8 +1960,7 @@ const rebuildAdjacency = ( * Graph.reverse(mutable) // Now B -> A, C -> B * }) * - * const edge0 = Graph.getEdge(graph, 0) - * console.log(edge0) // Option.some(new Graph.Edge({ source: 1, target: 0, data: 1 })) + * Option.map(Graph.getEdge(graph, 0), (edge) => edge.source) // => Option.some(1) * ``` * * @category transforming @@ -2026,7 +2000,7 @@ export const reverse = ( * * **Example** (Filtering and mapping nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { @@ -2044,7 +2018,7 @@ export const reverse = ( * ) * }) * - * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * Graph.nodeCount(graph) // => 2 * ``` * * @category transforming @@ -2083,7 +2057,7 @@ export const filterMapNodes = ( * * **Example** (Filtering and mapping edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { @@ -2101,7 +2075,7 @@ export const filterMapNodes = ( * ) * }) * - * console.log(Graph.edgeCount(graph)) // 2 (edges with weight 5 removed) + * Graph.edgeCount(graph) // => 2 * ``` * * @category transforming @@ -2146,7 +2120,7 @@ export const filterMapEdges = ( * * **Example** (Filtering nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -2159,7 +2133,7 @@ export const filterMapEdges = ( * Graph.filterNodes(mutable, (data) => data === "active") * }) * - * console.log(Graph.nodeCount(graph)) // 2 (only "active" nodes remain) + * Graph.nodeCount(graph) // => 2 * ``` * * @category transforming @@ -2193,7 +2167,7 @@ export const filterNodes = ( * * **Example** (Filtering edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -2209,7 +2183,7 @@ export const filterNodes = ( * Graph.filterEdges(mutable, (data) => data >= 10) * }) * - * console.log(Graph.edgeCount(graph)) // 2 (edge with weight 5 removed) + * Graph.edgeCount(graph) // => 2 * ``` * * @category transforming @@ -2286,14 +2260,13 @@ const invalidateCycleFlagOnAddition = ( * * **Example** (Adding edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * - * const result = Graph.mutate(Graph.directed(), (mutable) => { + * 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 + * Graph.addEdge(mutable, nodeA, nodeB, 42) // => 0 * }) * ``` * @@ -2367,7 +2340,7 @@ export const addEdge = ( * * **Example** (Removing a node) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { @@ -2378,6 +2351,7 @@ export const addEdge = ( * // Remove nodeA and all edges connected to it * Graph.removeNode(mutable, nodeA) * }) + * Array.of(Graph.nodeCount(result), Graph.edgeCount(result)) // => [1, 0] * ``` * * @category mutations @@ -2434,7 +2408,7 @@ export const removeNode = ( * * **Example** (Removing an edge) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const result = Graph.mutate(Graph.directed(), (mutable) => { @@ -2445,6 +2419,7 @@ export const removeNode = ( * // Remove the edge * Graph.removeEdge(mutable, edge) * }) + * Array.of(Graph.nodeCount(result), Graph.edgeCount(result)) // => [2, 0] * ``` * * @category mutations @@ -2530,8 +2505,8 @@ const removeEdgeInternal = ( * * **Example** (Getting edge data) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") @@ -2539,14 +2514,7 @@ const removeEdgeInternal = ( * Graph.addEdge(mutable, nodeA, nodeB, 42) * }) * - * const edgeIndex = 0 - * const edgeData = Graph.getEdge(graph, edgeIndex) - * - * if (edgeData._tag === "Some") { - * console.log(edgeData.value.data) // 42 - * console.log(edgeData.value.source) // 0 - * console.log(edgeData.value.target) // 1 - * } + * Graph.getEdge(graph, 0) // => Option.some(new Graph.Edge({ source: 0, target: 1, data: 42 })) * ``` * * @category getters @@ -2570,7 +2538,7 @@ export const getEdge: { * * **Example** (Checking edge existence) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { @@ -2580,15 +2548,8 @@ export const getEdge: { * 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 + * Graph.hasEdge(graph, 0, 1) // => true + * Graph.hasEdge(graph, 0, 2) // => false * ``` * * @category getters @@ -2634,11 +2595,11 @@ export const hasEdge: { * * **Example** (Counting edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const emptyGraph = Graph.directed() - * console.log(Graph.edgeCount(emptyGraph)) // 0 + * Graph.edgeCount(emptyGraph) // => 0 * * const graphWithEdges = Graph.mutate(emptyGraph, (mutable) => { * const nodeA = Graph.addNode(mutable, "Node A") @@ -2649,7 +2610,7 @@ export const hasEdge: { * Graph.addEdge(mutable, nodeC, nodeA, 3) * }) * - * console.log(Graph.edgeCount(graphWithEdges)) // 3 + * Graph.edgeCount(graphWithEdges) // => 3 * ``` * * @category getters @@ -2695,7 +2656,7 @@ const getDirectedNeighbors = ( * * **Example** (Getting outgoing neighbors) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { @@ -2706,15 +2667,8 @@ const getDirectedNeighbors = ( * Graph.addEdge(mutable, nodeA, nodeC, 2) * }) * - * const nodeA = 0 - * const nodeB = 1 - * const nodeC = 2 - * - * const neighborsA = Graph.neighbors(graph, nodeA) - * console.log(neighborsA) // [1, 2] - * - * const neighborsB = Graph.neighbors(graph, nodeB) - * console.log(neighborsB) // [] + * Graph.neighbors(graph, 0) // => [1, 2] + * Graph.neighbors(graph, 1) // => [] * ``` * * @category getters @@ -2755,7 +2709,7 @@ export const neighbors: { * @see {@link predecessors} for incoming neighbors in a directed graph * @see {@link neighbors} for generic neighbor lookup across graph kinds * - * @category queries + * @category getters * @since 4.0.0 */ export const successors: { @@ -2791,7 +2745,7 @@ export const successors: { * @see {@link successors} for outgoing neighbors in a directed graph * @see {@link neighbors} for generic neighbor lookup across graph kinds * - * @category queries + * @category getters * @since 4.0.0 */ export const predecessors: { @@ -2826,7 +2780,7 @@ export const predecessors: { * * **Example** (Traversing directed neighbors) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -2843,12 +2797,13 @@ export const predecessors: { * * // Get incoming neighbors (nodes that point to nodeB) * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming") + * Array.of(outgoing, incoming) // => [[1], [0]] * ``` * * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors. * @see {@link successors} for outgoing neighbors in a directed graph * @see {@link predecessors} for incoming neighbors in a directed graph - * @category queries + * @category getters * @since 3.18.0 */ export const neighborsDirected: { @@ -2886,7 +2841,7 @@ export const neighborsDirected: { * * **Example** (Configuring GraphViz labels) * - * ```ts + * ```ts import.meta.vitest * import type { Graph } from "effect" * * // Basic options with custom labels @@ -2901,6 +2856,7 @@ export const neighborsDirected: { * edgeLabel: (data) => data, * graphName: "MyDependencyGraph" * } + * Array.of(basicOptions.nodeLabel?.("A"), namedOptions.graphName) // => ["Node: A", "MyDependencyGraph"] * ``` * * @category options @@ -2934,7 +2890,7 @@ const escapeGraphVizString = (value: string): string => * * **Example** (Exporting GraphViz DOT) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.mutate(Graph.directed(), (mutable) => { @@ -2946,16 +2902,7 @@ const escapeGraphVizString = (value: string): string => * 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"]; - * // } + * Graph.toGraphViz(graph).split("\n") // => ['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"];', "}"] * ``` * * @category converting @@ -3025,7 +2972,7 @@ export const toGraphViz: { * * **Example** (Selecting Mermaid node shapes) * - * ```ts + * ```ts import.meta.vitest * import type { Graph } from "effect" * * // Shape selector function for different node types @@ -3040,6 +2987,7 @@ export const toGraphViz: { * const options: Graph.MermaidOptions = { * nodeShape: shapeSelector * } + * options.nodeShape?.("decision") // => "diamond" * ``` * * @category models @@ -3068,7 +3016,7 @@ export type MermaidNodeShape = * * **Example** (Configuring Mermaid directions) * - * ```ts + * ```ts import.meta.vitest * import type { Graph } from "effect" * * // Horizontal workflow diagram @@ -3085,6 +3033,7 @@ export type MermaidNodeShape = * const bottomUpOptions: Graph.MermaidOptions = { * direction: "BT" * } + * Array.of(horizontalOptions.direction, verticalOptions.direction, bottomUpOptions.direction) // => ["LR", "TB", "BT"] * ``` * * @category models @@ -3111,7 +3060,7 @@ export type MermaidDirection = * * **Example** (Selecting Mermaid diagram types) * - * ```ts + * ```ts import.meta.vitest * import type { Graph } from "effect" * * // Force flowchart format (even for undirected graphs) @@ -3126,6 +3075,7 @@ export type MermaidDirection = * * // Auto-detection (recommended, default behavior) * const autoOptions: Graph.MermaidOptions = {} + * Array.of(flowchartOptions.diagramType, graphOptions.diagramType, autoOptions.diagramType) // => ["flowchart", "graph", undefined] * ``` * * @category models @@ -3151,7 +3101,7 @@ export type MermaidDiagramType = * * **Example** (Configuring Mermaid output) * - * ```ts + * ```ts import.meta.vitest * import type { Graph } from "effect" * * // Basic options with custom labels @@ -3168,6 +3118,7 @@ export type MermaidDiagramType = * direction: "LR", * nodeShape: (data) => data.includes("start") ? "circle" : "rectangle" * } + * Array.of(basicOptions.nodeLabel?.("A"), advancedOptions.nodeShape?.("start")) // => ["Node: A", "circle"] * ``` * * @category options @@ -3271,7 +3222,7 @@ const formatMermaidNode = ( * * **Example** (Exporting a directed Mermaid diagram) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Basic directed graph export @@ -3283,19 +3234,12 @@ const formatMermaidNode = ( * 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 + * Graph.toMermaid(graph).split("\n") // => ["flowchart TD", ' 0["App"]', ' 1["Database"]', ' 2["Cache"]', ' 0 -->|"1"| 1', ' 0 -->|"2"| 2'] * ``` * * **Example** (Exporting an undirected Mermaid diagram) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Undirected graph with custom labels and direction @@ -3312,18 +3256,12 @@ const formatMermaidNode = ( * edgeLabel: (relationship) => relationship, * direction: "LR" * }) - * console.log(mermaid) - * // graph LR - * // 0["Alice"] - * // 1["Bob"] - * // 2["Charlie"] - * // 0 ---|"friends"| 1 - * // 1 ---|"colleagues"| 2 + * mermaid.split("\n") // => ["graph LR", ' 0["Alice"]', ' 1["Bob"]', ' 2["Charlie"]', ' 0 ---|"friends"| 1', ' 1 ---|"colleagues"| 2'] * ``` * * **Example** (Customizing Mermaid node shapes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Advanced styling with node shapes for flowchart @@ -3362,20 +3300,12 @@ const formatMermaidNode = ( * } * } * }) - * console.log(mermaid) - * // flowchart TD - * // 0(["Begin"]) - * // 1["Process Data"] - * // 2{"Valid?"} - * // 3(["Complete"]) - * // 0 --> 1 - * // 1 --> 2 - * // 2 --> 3 + * mermaid.split("\n") // => ["flowchart TD", ' 0(["Begin"])', ' 1["Process Data"]', ' 2{"Valid?"}', ' 3(["Complete"])', " 0 --> 1", " 1 --> 2", ' 2 -->|"yes"| 3'] * ``` * * **Example** (Visualizing dependency graphs) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Real-world example: Software dependency graph @@ -3424,15 +3354,7 @@ const formatMermaidNode = ( * direction: "TB" * }) * - * console.log(dependencyDiagram) - * // flowchart TB - * // 0["MyApp\nv1.0.0"] - * // 1{{"React\nv18.0.0"}} - * // 2["Lodash\nv4.17.0"] - * // 3{"Webpack\nv5.0.0"} - * // 0 -->|"depends on"| 1 - * // 0 -->|"depends on"| 2 - * // 0 -->|"builds with"| 3 + * dependencyDiagram.split("\n") // => ["flowchart TB", ' 0["MyApp#92;nv1.0.0"]', ' 1{{"React#92;nv18.0.0"}}', ' 2["Lodash#92;nv4.17.0"]', ' 3{"Webpack#92;nv5.0.0"}', ' 0 -->|"depends on"| 1', ' 0 -->|"depends on"| 2', ' 0 -->|"builds with"| 3'] * ``` * * @category converting @@ -3522,7 +3444,7 @@ export type Direction = "outgoing" | "incoming" * * **Example** (Traversing by direction) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -3533,17 +3455,9 @@ export type Direction = "outgoing" | "incoming" * Graph.addEdge(mutable, a, c, "A-C") * }) * - * const outgoing = Array.from( - * Graph.indices(Graph.bfs(graph, { start: [0], direction: "outgoing" })) - * ) // [0, 1, 2] - * - * const incoming = Array.from( - * Graph.indices(Graph.bfs(graph, { start: [1], direction: "incoming" })) - * ) // [1, 0] - * - * const undirected = Array.from( - * Graph.indices(Graph.bfs(graph, { start: [1], direction: "undirected" })) - * ) // [1, 0, 2] + * Array.from(Graph.indices(Graph.bfs(graph, { start: [0], direction: "outgoing" }))) // => [0, 1, 2] + * Array.from(Graph.indices(Graph.bfs(graph, { start: [1], direction: "incoming" }))) // => [1, 0] + * Array.from(Graph.indices(Graph.bfs(graph, { start: [1], direction: "undirected" }))) // => [1, 0, 2] * ``` * * @category models @@ -3567,7 +3481,7 @@ export type TraversalDirection = Direction | "undirected" * * **Example** (Checking cycles) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Acyclic directed graph (DAG) @@ -3578,7 +3492,7 @@ export type TraversalDirection = Direction | "undirected" * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, c, "B->C") * }) - * console.log(Graph.isAcyclic(dag)) // true + * Graph.isAcyclic(dag) // => true * * // Cyclic directed graph * const cyclic = Graph.directed((mutable) => { @@ -3587,7 +3501,7 @@ export type TraversalDirection = Direction | "undirected" * Graph.addEdge(mutable, a, b, "A->B") * Graph.addEdge(mutable, b, a, "B->A") // Creates cycle * }) - * console.log(Graph.isAcyclic(cyclic)) // false + * Graph.isAcyclic(cyclic) // => false * ``` * * @category algorithms @@ -3727,7 +3641,7 @@ export const isAcyclic = ( * * **Example** (Checking bipartite graphs) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * // Bipartite graph (alternating coloring possible) @@ -3740,7 +3654,7 @@ export const isAcyclic = ( * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, d, "edge") * }) - * console.log(Graph.isBipartite(bipartite)) // true + * Graph.isBipartite(bipartite) // => true * * // Non-bipartite graph (odd cycle) * const triangle = Graph.undirected((mutable) => { @@ -3751,7 +3665,7 @@ export const isAcyclic = ( * Graph.addEdge(mutable, b, c, "edge") * Graph.addEdge(mutable, c, a, "edge") // Triangle (3-cycle) * }) - * console.log(Graph.isBipartite(triangle)) // false + * Graph.isBipartite(triangle) // => false * ``` * * @category algorithms @@ -3864,7 +3778,7 @@ const getTraversableNeighbor = ( * * **Example** (Finding connected components) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.undirected((mutable) => { @@ -3876,8 +3790,7 @@ const getTraversableNeighbor = ( * Graph.addEdge(mutable, c, d, "edge") // Component 2: C-D * }) * - * const components = Graph.connectedComponents(graph) - * console.log(components) // [[0, 1], [2, 3]] + * Graph.connectedComponents(graph) // => [[0, 1], [2, 3]] * ``` * * @category algorithms @@ -3928,7 +3841,7 @@ export const connectedComponents = ( * * **Example** (Finding strongly connected components) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -3940,8 +3853,7 @@ export const connectedComponents = ( * Graph.addEdge(mutable, c, a, "C->A") // Creates SCC: A-B-C * }) * - * const sccs = Graph.stronglyConnectedComponents(graph) - * console.log(sccs) // [[0, 1, 2]] + * Graph.stronglyConnectedComponents(graph) // => [[0, 2, 1]] * ``` * * @category algorithms @@ -4174,8 +4086,8 @@ export interface DijkstraConfig { * * **Example** (Finding shortest paths with Dijkstra) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") @@ -4192,10 +4104,7 @@ export interface DijkstraConfig { * cost: (edgeData) => edgeData * }) * - * if (result._tag === "Some") { - * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C - * console.log(result.value.distance) // 7 - total distance - * } + * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([7, [0, 1, 2]]) * ``` * * @category algorithms @@ -4370,7 +4279,7 @@ export interface AllPairsResult { * * **Example** (Finding all-pairs shortest paths) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -4383,8 +4292,8 @@ export interface AllPairsResult { * }) * * 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] + * const shortest = { distance: result.distances.get(0)?.get(2), path: result.paths.get(0)?.get(2) } + * shortest // => { distance: 5, path: [0, 1, 2] } * ``` * * @category algorithms @@ -4565,8 +4474,8 @@ export interface AstarConfig { * * **Example** (Finding shortest paths with A-star) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed<{ x: number; y: number }, number>((mutable) => { * const a = Graph.addNode(mutable, { x: 0, y: 0 }) @@ -4589,10 +4498,7 @@ export interface AstarConfig { * heuristic * }) * - * if (result._tag === "Some") { - * console.log(result.value.path) // [0, 1, 2] - shortest path - * console.log(result.value.distance) // 2 - total distance - * } + * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([2, [0, 1, 2]]) * ``` * * @category algorithms @@ -4795,8 +4701,8 @@ export interface BellmanFordConfig { * * **Example** (Finding shortest paths with Bellman-Ford) * - * ```ts - * import { Graph } from "effect" + * ```ts import.meta.vitest + * import { Graph, Option } from "effect" * * const graph = Graph.directed((mutable) => { * const a = Graph.addNode(mutable, "A") @@ -4813,10 +4719,7 @@ export interface BellmanFordConfig { * cost: (edgeData) => edgeData * }) * - * if (result._tag === "Some") { - * console.log(result.value.path) // [0, 1, 2] - shortest path A->B->C - * console.log(result.value.distance) // 2 - total distance - * } + * Option.map(result, ({ distance, path }) => [distance, path] as const) // => Option.some([2, [0, 1, 2]]) * ``` * * @category algorithms @@ -4967,7 +4870,7 @@ export const bellmanFord: { * * **Example** (Working with node walkers) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -4986,8 +4889,8 @@ export const bellmanFord: { * } * * // 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"]] + * Array.from(Graph.values(dfsNodes)) // => ["A", "B"] + * Array.from(Graph.entries(allNodes)) // => [[0, "A"], [1, "B"]] * ``` * * @category models @@ -5008,7 +4911,7 @@ export class Walker implements Iterable<[T, N]> { * * **Example** (Visiting walker elements) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5020,14 +4923,10 @@ export class Walker implements Iterable<[T, N]> { * 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"] + * Array.from(dfs.visit((index, data) => data)) // => ["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" }] + * Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) // => [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @since 4.0.0 @@ -5044,7 +4943,7 @@ export class Walker implements Iterable<[T, N]> { * * **Example** (Visiting walker elements) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5056,14 +4955,10 @@ export class Walker implements Iterable<[T, N]> { * 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"] + * Array.from(dfs.visit((index, data) => data)) // => ["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" }] + * Array.from(dfs.visit((index, data) => ({ id: index, name: data }))) // => [{ id: 0, name: "A" }, { id: 1, name: "B" }] * ``` * * @category iterators @@ -5117,7 +5012,7 @@ export type EdgeWalker = Walker> * * **Example** (Iterating walker indices) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5127,8 +5022,7 @@ export type EdgeWalker = Walker> * }) * * const dfs = Graph.dfs(graph, { start: [0] }) - * const indices = Array.from(Graph.indices(dfs)) - * console.log(indices) // [0, 1] + * Array.from(Graph.indices(dfs)) // => [0, 1] * ``` * * @category iterators @@ -5141,7 +5035,7 @@ export const indices = (walker: Walker): Iterable => walker.visit * * **Example** (Iterating walker values) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5151,8 +5045,7 @@ export const indices = (walker: Walker): Iterable => walker.visit * }) * * const dfs = Graph.dfs(graph, { start: [0] }) - * const values = Array.from(Graph.values(dfs)) - * console.log(values) // ["A", "B"] + * Array.from(Graph.values(dfs)) // => ["A", "B"] * ``` * * @category iterators @@ -5165,7 +5058,7 @@ export const values = (walker: Walker): Iterable => walker.visit( * * **Example** (Iterating walker entries) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5175,8 +5068,7 @@ export const values = (walker: Walker): Iterable => walker.visit( * }) * * const dfs = Graph.dfs(graph, { start: [0] }) - * const entries = Array.from(Graph.entries(dfs)) - * console.log(entries) // [[0, "A"], [1, "B"]] + * Array.from(Graph.entries(dfs)) // => [[0, "A"], [1, "B"]] * ``` * * @category iterators @@ -5231,7 +5123,7 @@ export interface SearchConfig { * * **Example** (Traversing depth-first) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5243,14 +5135,10 @@ export interface SearchConfig { * }) * * // 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 - * } + * Array.from(Graph.indices(Graph.dfs(graph, { start: [0] }))) // => [0, 1, 2] * * // Empty iterator (no starting nodes) - * const dfs2 = Graph.dfs(graph) - * // Can be used programmatically + * Graph.dfs(graph) * ``` * * @category iterators @@ -5368,7 +5256,7 @@ export const dfs: { * * **Example** (Traversing breadth-first) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5380,14 +5268,10 @@ export const dfs: { * }) * * // 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 - * } + * Array.from(Graph.indices(Graph.bfs(graph, { start: [0] }))) // => [0, 1, 2] * * // Empty iterator (no starting nodes) - * const bfs2 = Graph.bfs(graph) - * // Can be used programmatically + * Graph.bfs(graph) * ``` * * @category iterators @@ -5489,7 +5373,7 @@ export interface TopoConfig { * * **Example** (Sorting topologically) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5501,13 +5385,10 @@ export interface TopoConfig { * }) * * // Standard topological sort - * const topo1 = Graph.topo(graph) - * for (const nodeIndex of Graph.indices(topo1)) { - * console.log(nodeIndex) // 0, 1, 2 (topological order) - * } + * Array.from(Graph.indices(Graph.topo(graph))) // => [0, 1, 2] * * // With initial nodes - * const topo2 = Graph.topo(graph, { initials: [0] }) + * Graph.topo(graph, { initials: [0] }) * * // Check before sorting a cyclic graph * const cyclicGraph = Graph.directed((mutable) => { @@ -5517,9 +5398,7 @@ export interface TopoConfig { * Graph.addEdge(mutable, b, a, 2) // Creates cycle * }) * - * if (!Graph.isAcyclic(cyclicGraph)) { - * console.log("cyclic graph") // cyclic graph - * } + * Graph.isAcyclic(cyclicGraph) // => false * ``` * * @category iterators @@ -5653,7 +5532,7 @@ export const topo: { * * **Example** (Traversing in postorder) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5665,10 +5544,7 @@ export const topo: { * }) * * // Postorder: children before parents - * const postOrder = Graph.dfsPostOrder(graph, { start: [0] }) - * for (const node of postOrder) { - * console.log(node) // 1, 2, 0 - * } + * Array.from(Graph.indices(Graph.dfsPostOrder(graph, { start: [0] }))) // => [1, 2, 0] * ``` * * @category iterators @@ -5767,7 +5643,7 @@ export const dfsPostOrder: { * * **Example** (Iterating all nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5777,8 +5653,7 @@ export const dfsPostOrder: { * Graph.addEdge(mutable, a, b, 1) * }) * - * const indices = Array.from(Graph.indices(Graph.nodes(graph))) - * console.log(indices) // [0, 1, 2] + * Array.from(Graph.indices(Graph.nodes(graph))) // => [0, 1, 2] * ``` * * @category iterators @@ -5815,7 +5690,7 @@ export const nodes = ( * * **Example** (Iterating all edges) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5826,8 +5701,7 @@ export const nodes = ( * Graph.addEdge(mutable, b, c, 2) * }) * - * const indices = Array.from(Graph.indices(Graph.edges(graph))) - * console.log(indices) // [0, 1] + * Array.from(Graph.indices(Graph.edges(graph))) // => [0, 1] * ``` * * @category iterators @@ -5889,7 +5763,7 @@ export interface ExternalsConfig { * * **Example** (Iterating external nodes) * - * ```ts + * ```ts import.meta.vitest * import { Graph } from "effect" * * const graph = Graph.directed((mutable) => { @@ -5903,16 +5777,10 @@ export interface ExternalsConfig { * }) * * // Nodes with no outgoing edges (sinks + isolated) - * const sinks = Array.from( - * Graph.indices(Graph.externals(graph, { direction: "outgoing" })) - * ) - * console.log(sinks) // [2, 3] + * Array.from(Graph.indices(Graph.externals(graph, { direction: "outgoing" }))) // => [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] + * Array.from(Graph.indices(Graph.externals(graph, { direction: "incoming" }))) // => [0, 3] * ``` * * @category iterators diff --git a/.context/effect/packages/effect/src/HKT.ts b/.context/effect/packages/effect/src/HKT.ts index 4cbbfead4..ae7a5608d 100644 --- a/.context/effect/packages/effect/src/HKT.ts +++ b/.context/effect/packages/effect/src/HKT.ts @@ -26,7 +26,7 @@ import type * as Types from "./Types.ts" * * **Example** (Linking a type class to a type lambda) * - * ```ts + * ```ts import.meta.vitest * import type { HKT } from "effect" * * interface IdentityTypeLambda extends HKT.TypeLambda { @@ -45,7 +45,6 @@ import type * as Types from "./Types.ts" * type LinkedTypeLambda = typeof identity[typeof HKT.URI] * * const value: HKT.Kind, never, never, never, string> = identity.of("ok") - * console.log(value) // "ok" * ``` * * @category symbols @@ -68,7 +67,7 @@ export declare const URI: unique symbol * * **Example** (Defining higher-kinded type classes) * - * ```ts + * ```ts import.meta.vitest * import type { HKT } from "effect" * * // Define a Functor type class @@ -86,6 +85,8 @@ export declare const URI: unique symbol * f: (a: A) => HKT.Kind * ): HKT.Kind * } + * + * const witness: keyof Monad = "flatMap" * ``` * * @category models @@ -112,7 +113,7 @@ export interface TypeClass { * * **Example** (Defining type lambdas) * - * ```ts + * ```ts import.meta.vitest * import type { Effect, HKT } from "effect" * * // TypeLambda for Array @@ -129,6 +130,8 @@ export interface TypeClass { * interface FunctionTypeLambda extends HKT.TypeLambda { * readonly type: (a: this["In"]) => this["Target"] * } + * + * const witness: HKT.Kind = ["ok"] * ``` * * @category models @@ -158,8 +161,9 @@ export interface TypeLambda { * * **Example** (Applying type lambdas) * - * ```ts - * import type { Effect, HKT, Option } from "effect" + * ```ts import.meta.vitest + * import { Option } from "effect" + * import type { Effect, HKT } from "effect" * * // Define TypeLambdas * interface OptionTypeLambda extends HKT.TypeLambda { @@ -191,6 +195,8 @@ export interface TypeLambda { * never, * string * > + * + * const witness: OptionString = Option.some("ok") * ``` * * @category utility types diff --git a/.context/effect/packages/effect/src/Hash.ts b/.context/effect/packages/effect/src/Hash.ts index 2d925c337..0a12cca93 100644 --- a/.context/effect/packages/effect/src/Hash.ts +++ b/.context/effect/packages/effect/src/Hash.ts @@ -44,7 +44,7 @@ export const symbol = "~effect/interfaces/Hash" * * **Example** (Implementing Hash) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * * class MyClass implements Hash.Hash { @@ -55,8 +55,7 @@ export const symbol = "~effect/interfaces/Hash" * } * } * - * const instance = new MyClass(42) - * console.log(instance[Hash.symbol]()) // hash value of 42 + * new MyClass(42)[Hash.symbol]() // => 42 * ``` * * @category models @@ -90,18 +89,12 @@ export interface Hash { * * **Example** (Hashing different values) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * - * // Hash primitive values - * console.log(Hash.hash(42)) // numeric hash - * console.log(Hash.hash("hello")) // string hash - * console.log(Hash.hash(true)) // boolean hash - * - * // Hash objects and arrays - * console.log(Hash.hash({ name: "John", age: 30 })) - * console.log(Hash.hash([1, 2, 3])) - * console.log(Hash.hash({ id: "user-1", roles: ["admin", "editor"] })) + * Hash.hash(42) === Hash.hash(42) // => true + * Hash.hash("hello") === Hash.hash("hello") // => true + * Hash.hash([1, 2, 3]) === Hash.hash([1, 2, 3]) // => true * ``` * * @category hashing @@ -126,6 +119,9 @@ export const hash: (self: A) => number = (self: A) => { if (self === null) { return string("null") } else if (self instanceof Date) { + if (Number.isNaN(self.getTime())) { + return string("Invalid Date") + } return string(self.toISOString()) } else if (self instanceof RegExp) { return string(self.toString()) @@ -141,6 +137,8 @@ export const hash: (self: A) => number = (self: A) => { return self[symbol]() } else if (typeof self === "function") { return random(self) + } else if (self instanceof DataView) { + return array(new Uint8Array(self.buffer, self.byteOffset, self.byteLength)) } else if (Array.isArray(self) || ArrayBuffer.isView(self)) { return array(self as any) } else if (self instanceof Map) { @@ -176,17 +174,15 @@ export const hash: (self: A) => number = (self: A) => { * * **Example** (Hashing objects by reference) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * * const obj1 = { a: 1 } * const obj2 = { a: 1 } * - * // Same object always returns the same hash - * console.log(Hash.random(obj1) === Hash.random(obj1)) // true + * Hash.random(obj1) === Hash.random(obj1) // => true * - * // Different objects get different hashes - * console.log(Hash.random(obj1) === Hash.random(obj2)) // false + * typeof Hash.random(obj2) // => "number" * ``` * * @category hashing @@ -214,18 +210,14 @@ export const random: (self: A) => number = (self) => { * * **Example** (Combining hash values) * - * ```ts + * ```ts import.meta.vitest * import { Hash, pipe } from "effect" * - * // Can also be used with pipe - * * const hash1 = Hash.hash("hello") * const hash2 = Hash.hash("world") * - * // Combine two hash values * const combined = Hash.combine(hash2)(hash1) - * console.log(combined) - * const result = pipe(hash1, Hash.combine(hash2)) + * combined === pipe(hash1, Hash.combine(hash2)) // => true * ``` * * @see {@link hash} for computing hash values from arbitrary inputs @@ -253,15 +245,10 @@ export const combine: { * * **Example** (Optimizing a hash value) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * - * const rawHash = 1234567890 - * const optimizedHash = Hash.optimize(rawHash) - * console.log(optimizedHash) // optimized hash value - * - * // Often used internally by other hash functions - * const stringHash = Hash.optimize(Hash.string("hello")) + * Hash.optimize(1234567890) // => 160826066 * ``` * * @category hashing @@ -283,7 +270,7 @@ export const optimize = (n: number): number => (n & 0xbfffffff) | ((n >>> 1) & 0 * * **Example** (Checking for Hash support) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * * class MyHashable implements Hash.Hash { @@ -292,10 +279,9 @@ export const optimize = (n: number): number => (n & 0xbfffffff) | ((n >>> 1) & 0 * } * } * - * const obj = new MyHashable() - * console.log(Hash.isHash(obj)) // true - * console.log(Hash.isHash({})) // false - * console.log(Hash.isHash("string")) // false + * Hash.isHash(new MyHashable()) // => true + * Hash.isHash({}) // => false + * Hash.isHash("string") // => false * ``` * * @category guards @@ -318,16 +304,14 @@ export const isHash = (u: unknown): u is Hash => hasProperty(u, symbol) * * **Example** (Hashing numbers) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * - * console.log(Hash.number(42)) // hash of 42 - * console.log(Hash.number(3.14)) // hash of 3.14 - * console.log(Hash.number(NaN)) // hash of "NaN" - * console.log(Hash.number(Infinity)) // 0 (special case) - * - * // Same numbers produce the same hash - * console.log(Hash.number(100) === Hash.number(100)) // true + * Number.isInteger(Hash.number(42)) // => true + * Number.isInteger(Hash.number(3.14)) // => true + * Hash.number(NaN) === Hash.number(NaN) // => true + * Hash.number(Infinity) === Hash.number(Infinity) // => true + * Hash.number(100) === Hash.number(100) // => true * ``` * * @category hashing @@ -369,15 +353,13 @@ export const number = (n: number) => { * * **Example** (Hashing strings) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * - * console.log(Hash.string("hello")) // hash of "hello" - * console.log(Hash.string("world")) // hash of "world" - * console.log(Hash.string("")) // hash of empty string - * - * // Same strings produce the same hash - * console.log(Hash.string("test") === Hash.string("test")) // true + * Hash.string("hello") // => 181380007 + * Hash.string("world") // => 164394279 + * Hash.string("") // => 5381 + * Hash.string("test") === Hash.string("test") // => true * ``` * * @category hashing @@ -406,22 +388,20 @@ export const string = (str: string) => { * * **Example** (Hashing selected object keys) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * * const person = { name: "John", age: 30, city: "New York" } * - * // Hash only specific keys * const hash1 = Hash.structureKeys(person, ["name", "age"]) * const hash2 = Hash.structureKeys(person, ["name", "city"]) * - * console.log(hash1) // hash based on name and age - * console.log(hash2) // hash based on name and city + * hash1 // => -590673747 + * hash2 // => 284850673 * - * // Same keys produce the same hash * const person2 = { name: "John", age: 30, city: "Boston" } * const hash3 = Hash.structureKeys(person2, ["name", "age"]) - * console.log(hash1 === hash3) // true + * hash1 === hash3 // => true * ``` * * @category hashing @@ -449,19 +429,17 @@ export const structureKeys = (o: object, keys: Iterable) => { * * **Example** (Hashing object structures) * - * ```ts + * ```ts import.meta.vitest * import { Hash } from "effect" * * const obj1 = { name: "John", age: 30 } * const obj2 = { name: "Jane", age: 25 } * const obj3 = { name: "John", age: 30 } * - * console.log(Hash.structure(obj1)) // hash of obj1 - * console.log(Hash.structure(obj2)) // different hash - * console.log(Hash.structure(obj3)) // same as obj1 - * - * // Objects with same properties produce same hash - * console.log(Hash.structure(obj1) === Hash.structure(obj3)) // true + * Hash.structure(obj1) // => -590673747 + * Hash.structure(obj2) // => -590160631 + * Hash.structure(obj3) // => -590673747 + * Hash.structure(obj1) === Hash.structure(obj3) // => true * ``` * * @category hashing @@ -496,19 +474,18 @@ const iterableWith = (seed: number, f: (el: any) => number) => (iter: Iterable 6151 + * Hash.array(arr2) // => 6151 + * Hash.array(arr3) // => 6151 + * Hash.array(arr1) === Hash.array(arr2) // => true + * Hash.array(arr1) === Hash.array(arr3) // => true * ``` * * @see {@link hash} for the general-purpose hash dispatcher diff --git a/.context/effect/packages/effect/src/HashMap.ts b/.context/effect/packages/effect/src/HashMap.ts index 223d78101..7a9e80837 100644 --- a/.context/effect/packages/effect/src/HashMap.ts +++ b/.context/effect/packages/effect/src/HashMap.ts @@ -27,22 +27,21 @@ const TypeId = internal.HashMapTypeId * * **Example** (Using basic HashMap operations) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * // Create a HashMap * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * * // Access values - * const valueA = HashMap.get(map, "a") // Option.some(1) - * const valueD = HashMap.get(map, "d") // Option.none() + * HashMap.get(map, "a") // => Option.some(1) + * HashMap.get(map, "d") // => Option.none() * * // Check if key exists - * console.log(HashMap.has(map, "b")) // true + * HashMap.has(map, "b") // => true * * // Add/update values (returns new HashMap) - * const updated = HashMap.set(map, "d", 4) - * console.log(HashMap.size(updated)) // 4 + * HashMap.set(map, "d", 4) // => HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) * ``` * * @category models @@ -58,7 +57,7 @@ export interface HashMap extends Iterable<[Key, Value]>, Equ * * **Example** (Extracting HashMap types) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * // Create a concrete HashMap for type extraction @@ -82,6 +81,8 @@ export interface HashMap extends Iterable<[Key, Value]>, Equ * // Example of extracted types in action * const newProduct: Product = { quantity: 10, price: 199 } * const updatedInventory = updateInventory("tablet", newProduct) + * processEntry(["tablet", newProduct]) // => "tablet: 10 @ $199" + * updatedInventory // => HashMap.make(["laptop", { quantity: 5, price: 999 }], ["mouse", { quantity: 20, price: 29 }], ["tablet", newProduct]) * ``` * * @since 2.0.0 @@ -94,7 +95,7 @@ export declare namespace HashMap { * * **Example** (Updating values from Options) * - * ```ts + * ```ts import.meta.vitest * import { HashMap, Option } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) @@ -104,7 +105,7 @@ export declare namespace HashMap { * Option.isSome(option) ? Option.some(option.value + 1) : Option.some(1) * * const updated = HashMap.modifyAt(map, "a", updateFn) - * console.log(HashMap.get(updated, "a")) // Option.some(2) + * HashMap.get(updated, "a") // => Option.some(2) * ``` * * @category models @@ -117,8 +118,8 @@ export declare namespace HashMap { * * **Example** (Extracting key types) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * // Create a HashMap to extract key type from * const userMap = HashMap.make( @@ -131,7 +132,7 @@ export declare namespace HashMap { * * // Use the extracted type in functions * const getUserById = (id: UserKey) => HashMap.get(userMap, id) - * console.log(getUserById("alice")) // Option.some({ name: "Alice", age: 30 }) + * getUserById("alice") // => Option.some({ name: "Alice", age: 30 }) * ``` * * @category utility types @@ -144,8 +145,8 @@ export declare namespace HashMap { * * **Example** (Extracting value types) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * // Create a HashMap with user data * const userMap = HashMap.make( @@ -161,8 +162,9 @@ export declare namespace HashMap { * return user.active ? `${user.name} (active)` : `${user.name} (inactive)` * } * - * const alice = HashMap.get(userMap, "alice") - * // alice has type Option thanks to type extraction + * // The lookup has type Option thanks to type extraction + * HashMap.get(userMap, "alice") // => Option.some({ name: "Alice", age: 30, active: true }) + * processUser({ name: "Alice", age: 30, active: true }) // => "Alice (active)" * ``` * * @category utility types @@ -175,7 +177,7 @@ export declare namespace HashMap { * * **Example** (Extracting entry types) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * // Create a product catalog HashMap @@ -194,7 +196,7 @@ export declare namespace HashMap { * * // Convert to entries, process, and sort for deterministic output * const descriptions = HashMap.toEntries(catalog).map(processEntry).sort() - * console.log(descriptions) // ["book: $29 (education)", "laptop: $999 (electronics)"] + * descriptions // => ["book: $29 (education)", "laptop: $999 (electronics)"] * ``` * * @category utility types @@ -208,18 +210,18 @@ export declare namespace HashMap { * * **Example** (Checking HashMap values) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) * const notMap = { a: 1 } * - * console.log(HashMap.isHashMap(map)) // true - * console.log(HashMap.isHashMap(notMap)) // false - * console.log(HashMap.isHashMap(null)) // false + * HashMap.isHashMap(map) // => true + * HashMap.isHashMap(notMap) // => false + * HashMap.isHashMap(null) // => false * ``` * - * @category refinements + * @category guards * @since 2.0.0 */ export const isHashMap: { @@ -232,12 +234,10 @@ export const isHashMap: { * * **Example** (Creating an empty HashMap) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * - * const map = HashMap.empty() - * console.log(HashMap.isEmpty(map)) // true - * console.log(HashMap.size(map)) // 0 + * HashMap.empty() // => HashMap.empty() * ``` * * @category constructors @@ -250,12 +250,10 @@ export const empty: () => HashMap = internal.empty * * **Example** (Creating a HashMap from entries) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * - * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) - * console.log(HashMap.size(map)) // 3 - * console.log(HashMap.get(map, "b")) // Option.some(2) + * HashMap.make(["a", 1], ["b", 2], ["c", 3]) // => HashMap.make(["a", 1], ["b", 2], ["c", 3]) * ``` * * @category constructors @@ -273,13 +271,11 @@ export const make: >( * * **Example** (Creating a HashMap from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const entries = [["a", 1], ["b", 2], ["c", 3]] as const - * const map = HashMap.fromIterable(entries) - * console.log(HashMap.size(map)) // 3 - * console.log(HashMap.get(map, "a")) // Option.some(1) + * HashMap.fromIterable(entries) // => HashMap.make(["a", 1], ["b", 2], ["c", 3]) * ``` * * @category constructors @@ -292,17 +288,17 @@ export const fromIterable: (entries: Iterable) => HashMap * * **Example** (Checking for empty HashMaps) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const emptyMap = HashMap.empty() * const nonEmptyMap = HashMap.make(["a", 1]) * - * console.log(HashMap.isEmpty(emptyMap)) // true - * console.log(HashMap.isEmpty(nonEmptyMap)) // false + * HashMap.isEmpty(emptyMap) // => true + * HashMap.isEmpty(nonEmptyMap) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const isEmpty: (self: HashMap) => boolean = internal.isEmpty @@ -313,20 +309,19 @@ export const isEmpty: (self: HashMap) => boolean = internal.isEmpty * * **Example** (Looking up values) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) * - * console.log(HashMap.get(map, "a")) // Option.some(1) - * console.log(HashMap.get(map, "c")) // Option.none() + * HashMap.get(map, "a") // => Option.some(1) + * HashMap.get(map, "c") // => Option.none() * * // Using pipe syntax - * const value = HashMap.get("b")(map) - * console.log(value) // Option.some(2) + * HashMap.get("b")(map) // => Option.some(2) * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const get: { @@ -339,8 +334,8 @@ export const get: { * * **Example** (Looking up values with a hash) * - * ```ts - * import { Hash, HashMap } from "effect" + * ```ts import.meta.vitest + * import { Hash, HashMap, Option } from "effect" * * // Useful when implementing custom equality for complex keys * const userMap = HashMap.make( @@ -353,15 +348,13 @@ export const get: { * const precomputedHash = Hash.string(userId) * * // Lookup with custom hash (e.g., cached hash value) - * const user = HashMap.getHash(userMap, userId, precomputedHash) - * console.log(user) // Option.some({ name: "Alice", role: "admin" }) + * HashMap.getHash(userMap, userId, precomputedHash) // => Option.some({ name: "Alice", role: "admin" }) * * // This avoids recomputing the hash when you already have it - * const notFound = HashMap.getHash(userMap, "user999", Hash.string("user999")) - * console.log(notFound) // Option.none() + * HashMap.getHash(userMap, "user999", Hash.string("user999")) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const getHash: { @@ -385,7 +378,7 @@ export const getHash: { * * **Example** (Unsafely looking up values) * - * ```ts + * ```ts import.meta.vitest * import { HashMap, Option } from "effect" * * const config = HashMap.make( @@ -395,14 +388,10 @@ export const getHash: { * ) * * // Safe: use when you're certain the key exists - * const apiUrl = HashMap.getUnsafe(config, "api_url") // "https://api.example.com" - * console.log(`Connecting to: ${apiUrl}`) + * HashMap.getUnsafe(config, "api_url") // => "https://api.example.com" * * // Preferred: use get() for uncertain keys - * const dbUrl = HashMap.get(config, "db_url") // Option.none() - * if (Option.isSome(dbUrl)) { - * console.log(`Database: ${dbUrl.value}`) - * } + * HashMap.get(config, "db_url") // => Option.none() * * // This would throw: HashMap.getUnsafe(config, "db_url") * // Error: "HashMap.getUnsafe: key not found" @@ -421,20 +410,19 @@ export const getUnsafe: { * * **Example** (Checking for keys) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) * - * console.log(HashMap.has(map, "a")) // true - * console.log(HashMap.has(map, "c")) // false + * HashMap.has(map, "a") // => true + * HashMap.has(map, "c") // => false * * // Using pipe syntax - * const hasB = HashMap.has("b")(map) - * console.log(hasB) // true + * HashMap.has("b")(map) // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -448,7 +436,7 @@ export const has: { * * **Example** (Checking keys with a hash) * - * ```ts + * ```ts import.meta.vitest * import { Hash, HashMap } from "effect" * * // Create a map with case-sensitive keys @@ -459,17 +447,17 @@ export const has: { * * // Check with exact hash * const exactHash = Hash.string("Admin") - * console.log(HashMap.hasHash(userMap, "Admin", exactHash)) // true + * HashMap.hasHash(userMap, "Admin", exactHash) // => true * * // A matching hash does not override key equality - * console.log(HashMap.hasHash(userMap, "admin", exactHash)) // false + * HashMap.hasHash(userMap, "admin", exactHash) // => false * * // A different hash also cannot find the existing key * const lowercaseHash = Hash.string("admin") - * console.log(HashMap.hasHash(userMap, "Admin", lowercaseHash)) // false + * HashMap.hasHash(userMap, "Admin", lowercaseHash) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const hasHash: { @@ -482,15 +470,15 @@ export const hasHash: { * * **Example** (Checking entries by predicate) * - * ```ts + * ```ts import.meta.vitest * 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 + * HashMap.hasBy(hm, (value, key) => value === "a" && key === 1) // => true + * HashMap.hasBy(hm, (value) => value === "b") // => false * ``` * - * @category elements + * @category predicates * @since 3.16.0 */ export const hasBy: { @@ -504,17 +492,14 @@ export const hasBy: { * * **Example** (Setting a value) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map1 = HashMap.make(["a", 1]) - * const map2 = HashMap.set(map1, "b", 2) - * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.get(map2, "b")) // Option.some(2) + * HashMap.set(map1, "b", 2) // => HashMap.make(["a", 1], ["b", 2]) * * // Original map is unchanged - * console.log(HashMap.size(map1)) // 1 + * map1 // => HashMap.make(["a", 1]) * ``` * * @category transforming @@ -530,12 +515,11 @@ export const set: { * * **Example** (Iterating keys) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) - * const keys = Array.from(HashMap.keys(map)) - * console.log(keys.sort()) // ["a", "b", "c"] + * Array.from(HashMap.keys(map)).sort() // => ["a", "b", "c"] * ``` * * @category getters @@ -548,12 +532,11 @@ export const keys: (self: HashMap) => IterableIterator = internal * * **Example** (Iterating values) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) - * const values = Array.from(HashMap.values(map)) - * console.log(values.sort()) // [1, 2, 3] + * Array.from(HashMap.values(map)).sort() // => [1, 2, 3] * ``` * * @category getters @@ -566,7 +549,7 @@ export const values: (self: HashMap) => IterableIterator = intern * * **Example** (Converting values to an array) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const employees = HashMap.make( @@ -577,15 +560,13 @@ export const values: (self: HashMap) => IterableIterator = intern * * // Extract all employee records * const allEmployees = HashMap.toValues(employees) - * console.log(allEmployees.length) // 3 + * allEmployees.length // => 3 * * // Calculate total salary - * const totalSalary = allEmployees.reduce((sum, emp) => sum + emp.salary, 0) - * console.log(totalSalary) // 260000 + * allEmployees.reduce((sum, emp) => sum + emp.salary, 0) // => 260000 * * // Filter by department - * const engineers = allEmployees.filter((emp) => emp.department === "engineering") - * console.log(engineers.length) // 2 + * allEmployees.filter((emp) => emp.department === "engineering").length // => 2 * ``` * * @category getters @@ -598,7 +579,7 @@ export const toValues = (self: HashMap): Array => Array.from(valu * * **Example** (Iterating entries) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * // Create a configuration map @@ -613,12 +594,10 @@ export const toValues = (self: HashMap): Array => Array.from(valu * .sort(([left], [right]) => left.localeCompare(right)) * .map(([key, value]) => `Setting ${key} = ${value}`) * - * console.log(settings) - * // ["Setting cache.enabled = true", "Setting database.host = localhost", "Setting database.port = 5432"] + * settings // => ["Setting cache.enabled = true", "Setting database.host = localhost", "Setting database.port = 5432"] * * // Convert to array when you need all entries at once - * const allEntries = Array.from(HashMap.entries(config)) - * console.log(allEntries.length) // 3 + * Array.from(HashMap.entries(config)).length // => 3 * ``` * * @category getters @@ -631,7 +610,7 @@ export const entries: (self: HashMap) => IterableIterator<[K, V]> = * * **Example** (Converting entries to an array) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const gameScores = HashMap.make( @@ -648,11 +627,10 @@ export const entries: (self: HashMap) => IterableIterator<[K, V]> = * .sort(([, a], [, b]) => b - a) * .map(([player, score], rank) => `${rank + 1}. ${player}: ${score}`) * - * console.log(leaderboard) - * // ["1. alice: 1250", "2. charlie: 1100", "3. bob: 980"] + * leaderboard // => ["1. alice: 1250", "2. charlie: 1100", "3. bob: 980"] * * // Convert back to HashMap if needed - * const sortedMap = HashMap.fromIterable(scoreEntries) + * HashMap.fromIterable(scoreEntries) // => HashMap.make(["alice", 1250], ["charlie", 1100], ["bob", 980]) * ``` * * @category getters @@ -665,14 +643,14 @@ export const toEntries = (self: HashMap): Array<[K, V]> => Array.fro * * **Example** (Getting the size) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const emptyMap = HashMap.empty() * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * - * console.log(HashMap.size(emptyMap)) // 0 - * console.log(HashMap.size(map)) // 3 + * HashMap.size(emptyMap) // => 0 + * HashMap.size(map) // => 3 * ``` * * @category getters @@ -690,7 +668,7 @@ export const size: (self: HashMap) => number = internal.size * * **Example** (Beginning batch mutation) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1]) @@ -704,8 +682,7 @@ export const size: (self: HashMap) => number = internal.size * HashMap.remove(mutable, "a") * * // End mutation to get final immutable result - * const result = HashMap.endMutation(mutable) - * console.log(HashMap.size(result)) // 2 + * HashMap.endMutation(mutable) // => HashMap.make(["b", 2], ["c", 3]) * ``` * * @category mutations @@ -718,7 +695,7 @@ export const beginMutation: (self: HashMap) => HashMap = inter * * **Example** (Ending batch mutation) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * // Start with an existing map @@ -733,11 +710,7 @@ export const beginMutation: (self: HashMap) => HashMap = inter * HashMap.set(mutable, "w", 40) * * // End mutation to get final immutable result - * const final = HashMap.endMutation(mutable) - * - * console.log(HashMap.size(final)) // 3 - * console.log(HashMap.has(final, "x")) // false - * console.log(HashMap.get(final, "z")) // Option.some(30) + * HashMap.endMutation(mutable) // => HashMap.make(["y", 20], ["z", 30], ["w", 40]) * ``` * * @category mutations @@ -756,7 +729,7 @@ export const endMutation: (self: HashMap) => HashMap = interna * * **Example** (Applying batched mutations) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map1 = HashMap.make(["a", 1]) @@ -764,7 +737,7 @@ export const endMutation: (self: HashMap) => HashMap = interna * HashMap.set(mutable, "b", 2) * HashMap.set(mutable, "c", 3) * }) - * // Returns a new HashMap with mutations applied + * map2 // => HashMap.make(["a", 1], ["b", 2], ["c", 3]) * ``` * * @category mutations @@ -786,7 +759,7 @@ export const mutate: { * * **Example** (Updating values with Options) * - * ```ts + * ```ts import.meta.vitest * import { HashMap, Option } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) @@ -796,7 +769,7 @@ export const mutate: { * Option.isSome(option) ? Option.some(option.value + 1) : Option.some(1) * * const updated = HashMap.modifyAt(map, "a", updateFn) - * console.log(HashMap.get(updated, "a")) // Option.some(2) + * HashMap.get(updated, "a") // => Option.some(2) * ``` * * @category transforming @@ -819,7 +792,7 @@ export const modifyAt: { * * **Example** (Updating values with a hash) * - * ```ts + * ```ts import.meta.vitest * import { Hash, HashMap, Option } from "effect" * * // Useful when working with precomputed hashes for performance @@ -840,7 +813,7 @@ export const modifyAt: { * cachedHash, * incrementCounter * ) - * console.log(HashMap.get(updated, "downloads")) // Option.some(101) + * HashMap.get(updated, "downloads") // => Option.some(101) * * // Add new metric with precomputed hash * const newMetric = "clicks" @@ -851,7 +824,7 @@ export const modifyAt: { * clicksHash, * incrementCounter * ) - * console.log(HashMap.get(withClicks, "clicks")) // Option.some(1) + * HashMap.get(withClicks, "clicks") // => Option.some(1) * ``` * * @category transforming @@ -867,14 +840,14 @@ export const modifyHash: { * * **Example** (Modifying existing values) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2]) * const map2 = HashMap.modify(map1, "a", (value) => value * 3) * - * console.log(HashMap.get(map2, "a")) // Option.some(3) - * console.log(HashMap.get(map2, "b")) // Option.some(2) + * HashMap.get(map2, "a") // => Option.some(3) + * HashMap.get(map2, "b") // => Option.some(2) * ``` * * @category transforming @@ -895,15 +868,15 @@ export const modify: { * * **Example** (Combining HashMaps) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2]) * const map2 = HashMap.make(["b", 20], ["c", 3]) * const union = HashMap.union(map1, map2) * - * console.log(HashMap.size(union)) // 3 - * console.log(HashMap.get(union, "b")) // Option.some(20) - map2 wins + * union // => HashMap.make(["a", 1], ["b", 20], ["c", 3]) + * HashMap.get(union, "b") // => Option.some(20) * ``` * * @category combining @@ -920,15 +893,13 @@ export const union: { * * **Example** (Removing a key) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * const map2 = HashMap.remove(map1, "b") * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.has(map2, "b")) // false - * console.log(HashMap.has(map2, "a")) // true + * map2 // => HashMap.make(["a", 1], ["c", 3]) * ``` * * @category transforming @@ -944,15 +915,13 @@ export const remove: { * * **Example** (Removing multiple keys) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) * const map2 = HashMap.removeMany(map1, ["b", "d"]) * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.has(map2, "a")) // true - * console.log(HashMap.has(map2, "c")) // true + * map2 // => HashMap.make(["a", 1], ["c", 3]) * ``` * * @category transforming @@ -968,16 +937,15 @@ export const removeMany: { * * **Example** (Setting multiple entries) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2]) * const newEntries = [["c", 3], ["d", 4], ["a", 10]] as const // "a" will be overwritten * const map2 = HashMap.setMany(map1, newEntries) * - * console.log(HashMap.size(map2)) // 4 - * console.log(HashMap.get(map2, "a")) // Option.some(10) - * console.log(HashMap.get(map2, "c")) // Option.some(3) + * map2 // => HashMap.make(["a", 10], ["b", 2], ["c", 3], ["d", 4]) + * HashMap.get(map2, "a") // => Option.some(10) * ``` * * @category transforming @@ -993,14 +961,14 @@ export const setMany: { * * **Example** (Mapping values) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * const map2 = HashMap.map(map1, (value, key) => `${key}:${value * 2}`) * - * console.log(HashMap.get(map2, "a")) // Option.some("a:2") - * console.log(HashMap.get(map2, "b")) // Option.some("b:4") + * HashMap.get(map2, "a") // => Option.some("a:2") + * HashMap.get(map2, "b") // => Option.some("b:4") * ``` * * @category mapping @@ -1020,8 +988,8 @@ export const map: { * * **Example** (Flat mapping values) * - * ```ts - * import { HashMap } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2]) * const map2 = HashMap.flatMap( @@ -1029,9 +997,8 @@ export const map: { * (value, key) => HashMap.make([key + "1", value], [key + "2", value * 2]) * ) * - * console.log(HashMap.size(map2)) // 4 - * console.log(HashMap.get(map2, "a1")) // Option.some(1) - * console.log(HashMap.get(map2, "b2")) // Option.some(4) + * map2 // => HashMap.make(["a1", 1], ["a2", 2], ["b1", 2], ["b2", 4]) + * HashMap.get(map2, "b2") // => Option.some(4) * ``` * * @category sequencing @@ -1047,7 +1014,7 @@ export const flatMap: { * * **Example** (Iterating with side effects) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2]) @@ -1057,7 +1024,7 @@ export const flatMap: { * collected.push([key, value]) * }) * - * console.log(collected.sort()) // [["a", 1], ["b", 2]] + * collected.sort() // => [["a", 1], ["b", 2]] * ``` * * @category traversing @@ -1073,13 +1040,11 @@ export const forEach: { * * **Example** (Reducing values) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) - * const sum = HashMap.reduce(map, 0, (acc, value) => acc + value) - * - * console.log(sum) // 6 + * HashMap.reduce(map, 0, (acc, value) => acc + value) // => 6 * ``` * * @category folding @@ -1095,16 +1060,13 @@ export const reduce: { * * **Example** (Filtering entries) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) * const map2 = HashMap.filter(map1, (value) => value % 2 === 0) * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.has(map2, "b")) // true - * console.log(HashMap.has(map2, "d")) // true - * console.log(HashMap.has(map2, "a")) // false + * map2 // => HashMap.make(["b", 2], ["d", 4]) * ``` * * @category filtering @@ -1120,7 +1082,7 @@ export const filter: { * * **Example** (Compacting Option values) * - * ```ts + * ```ts import.meta.vitest * import { HashMap, Option } from "effect" * * const map1 = HashMap.make( @@ -1130,9 +1092,8 @@ export const filter: { * ) * const map2 = HashMap.compact(map1) * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.get(map2, "a")) // Option.some(1) - * console.log(HashMap.has(map2, "b")) // false + * map2 // => HashMap.make(["a", 1], ["c", 3]) + * HashMap.get(map2, "a") // => Option.some(1) * ``` * * @category filtering @@ -1146,8 +1107,8 @@ export const compact: (self: HashMap>) => HashMap = int * * **Example** (Filtering and mapping Results) * - * ```ts - * import { HashMap, Result } from "effect" + * ```ts import.meta.vitest + * import { HashMap, Option, Result } from "effect" * * const map1 = HashMap.make(["a", 1], ["b", 2], ["c", 3], ["d", 4]) * const map2 = HashMap.filterMap( @@ -1155,9 +1116,8 @@ export const compact: (self: HashMap>) => HashMap = int * (value) => value % 2 === 0 ? Result.succeed(value * 2) : Result.failVoid * ) * - * console.log(HashMap.size(map2)) // 2 - * console.log(HashMap.get(map2, "b")) // Option.some(4) - * console.log(HashMap.get(map2, "d")) // Option.some(8) + * map2 // => HashMap.make(["b", 4], ["d", 8]) + * HashMap.get(map2, "b") // => Option.some(4) * ``` * * @category filtering @@ -1174,16 +1134,14 @@ export const filterMap: { * * **Example** (Finding the first matching entry) * - * ```ts + * ```ts import.meta.vitest * import { HashMap, Option } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) - * const result = HashMap.findFirst(map, (value, key) => key === "b" && value > 1) - * console.log(result) // Option.some(["b", 2]) - * console.log(Option.getOrElse(result, () => ["", 0])) // ["b", 2] + * HashMap.findFirst(map, (value, key) => key === "b" && value > 1) // => Option.some(["b", 2]) * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirst: { @@ -1196,16 +1154,16 @@ export const findFirst: { * * **Example** (Checking for any matching entry) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * - * console.log(HashMap.some(map, (value) => value > 2)) // true - * console.log(HashMap.some(map, (value) => value > 5)) // false + * HashMap.some(map, (value) => value > 2) // => true + * HashMap.some(map, (value) => value > 5) // => false * ``` * - * @category elements + * @category predicates * @since 3.13.0 */ export const some: { @@ -1218,16 +1176,16 @@ export const some: { * * **Example** (Checking all entries) * - * ```ts + * ```ts import.meta.vitest * import { HashMap } from "effect" * * const map = HashMap.make(["a", 1], ["b", 2], ["c", 3]) * - * console.log(HashMap.every(map, (value) => value > 0)) // true - * console.log(HashMap.every(map, (value) => value > 1)) // false + * HashMap.every(map, (value) => value > 0) // => true + * HashMap.every(map, (value) => value > 1) // => false * ``` * - * @category elements + * @category predicates * @since 3.14.0 */ export const every: { diff --git a/.context/effect/packages/effect/src/HashRing.ts b/.context/effect/packages/effect/src/HashRing.ts index 0b9f9c833..f85ac52fa 100644 --- a/.context/effect/packages/effect/src/HashRing.ts +++ b/.context/effect/packages/effect/src/HashRing.ts @@ -145,6 +145,7 @@ export const addMany: { const key = PrimaryKey.value(node) const entry = self.nodes.get(key) if (entry) { + entry[0] = node if (entry[1] === weight) continue toRemove ??= new Set() toRemove.add(key) diff --git a/.context/effect/packages/effect/src/HashSet.ts b/.context/effect/packages/effect/src/HashSet.ts index fc6e6dd7b..2f088d062 100644 --- a/.context/effect/packages/effect/src/HashSet.ts +++ b/.context/effect/packages/effect/src/HashSet.ts @@ -26,23 +26,23 @@ const TypeId = internal.HashSetTypeId * * **Example** (Creating and updating a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * // Create a HashSet * const set = HashSet.make("apple", "banana", "cherry") * * // Check membership - * console.log(HashSet.has(set, "apple")) // true - * console.log(HashSet.has(set, "grape")) // false + * HashSet.has(set, "apple") // => true + * HashSet.has(set, "grape") // => false * * // Add values (returns new HashSet) * const updated = HashSet.add(set, "grape") - * console.log(HashSet.size(updated)) // 4 + * updated // => HashSet.make("apple", "banana", "cherry", "grape") * * // Remove values (returns new HashSet) * const smaller = HashSet.remove(set, "banana") - * console.log(HashSet.size(smaller)) // 2 + * smaller // => HashSet.make("apple", "cherry") * ``` * * @category models @@ -58,7 +58,7 @@ export interface HashSet extends Iterable, Equal, Pipeable, In * * **Example** (Extracting value types from a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * // Create a concrete HashSet for type extraction @@ -71,6 +71,7 @@ export interface HashSet extends Iterable, Equal, Pipeable, In * const processFruit = (fruit: Fruit) => { * return `Processing ${fruit}` * } + * processFruit("apple") // => "Processing apple" * ``` * * @since 2.0.0 @@ -85,7 +86,7 @@ export declare namespace HashSet { * * **Example** (Extracting a HashSet value type) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const numbers = HashSet.make(1, 2, 3, 4, 5) @@ -94,6 +95,7 @@ export declare namespace HashSet { * type NumberType = HashSet.HashSet.Value // number * * const processNumber = (n: NumberType) => n * 2 + * processNumber(3) // => 6 * ``` * * @category utility types @@ -107,17 +109,17 @@ export declare namespace HashSet { * * **Example** (Creating an empty HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const set = HashSet.empty() * - * console.log(HashSet.size(set)) // 0 - * console.log(HashSet.isEmpty(set)) // true + * HashSet.size(set) // => 0 + * HashSet.isEmpty(set) // => true * * // Add some values * const withValues = HashSet.add(HashSet.add(set, "hello"), "world") - * console.log(HashSet.size(withValues)) // 2 + * withValues // => HashSet.make("hello", "world") * ``` * * @category constructors @@ -130,17 +132,14 @@ export const empty: () => HashSet = internal.empty * * **Example** (Creating a HashSet from values) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const fruits = HashSet.make("apple", "banana", "cherry") - * console.log(HashSet.size(fruits)) // 3 + * HashSet.make("apple", "banana", "cherry") // => HashSet.make("apple", "banana", "cherry") * - * const numbers = HashSet.make(1, 2, 3, 2, 1) // Duplicates ignored - * console.log(HashSet.size(numbers)) // 3 + * HashSet.make(1, 2, 3, 2, 1) // => HashSet.make(1, 2, 3) * - * const mixed = HashSet.make("hello", 42, true) - * console.log(HashSet.size(mixed)) // 3 + * HashSet.make("hello", 42, true) // => HashSet.make("hello", 42, true) * ``` * * @category constructors @@ -155,17 +154,14 @@ export const make: >( * * **Example** (Creating a HashSet from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const fromArray = HashSet.fromIterable(["a", "b", "c", "b", "a"]) - * console.log(HashSet.size(fromArray)) // 3 + * HashSet.fromIterable(["a", "b", "c", "b", "a"]) // => HashSet.make("a", "b", "c") * - * const fromSet = HashSet.fromIterable(new Set([1, 2, 3])) - * console.log(HashSet.size(fromSet)) // 3 + * HashSet.fromIterable(new Set([1, 2, 3])) // => HashSet.make(1, 2, 3) * - * const fromString = HashSet.fromIterable("hello") - * console.log(Array.from(fromString)) // ["h", "e", "l", "o"] + * HashSet.fromIterable("hello") // => HashSet.make("h", "e", "l", "o") * ``` * * @category constructors @@ -178,15 +174,15 @@ export const fromIterable: (values: Iterable) => HashSet = internal.fro * * **Example** (Checking for a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const set = HashSet.make(1, 2, 3) * const array = [1, 2, 3] * - * console.log(HashSet.isHashSet(set)) // true - * console.log(HashSet.isHashSet(array)) // false - * console.log(HashSet.isHashSet(null)) // false + * HashSet.isHashSet(set) // => true + * HashSet.isHashSet(array) // => false + * HashSet.isHashSet(null) // => false * ``` * * @category guards @@ -202,19 +198,18 @@ export const isHashSet: { * * **Example** (Adding values to a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const set = HashSet.make("a", "b") * const withC = HashSet.add(set, "c") * - * console.log(HashSet.size(set)) // 2 (original unchanged) - * console.log(HashSet.size(withC)) // 3 - * console.log(HashSet.has(withC, "c")) // true + * set // => HashSet.make("a", "b") + * withC // => HashSet.make("a", "b", "c") + * HashSet.has(withC, "c") // => true * * // Adding existing value has no effect - * const same = HashSet.add(set, "a") - * console.log(HashSet.size(same)) // 2 + * HashSet.add(set, "a") // => HashSet.make("a", "b") * ``` * * @category mutations @@ -233,15 +228,15 @@ export const add: { * * **Example** (Checking HashSet membership) * - * ```ts + * ```ts import.meta.vitest * import { Equal, Hash, HashSet } from "effect" * * // Works with any type that implements Equal * * const set = HashSet.make("apple", "banana", "cherry") * - * console.log(HashSet.has(set, "apple")) // true - * console.log(HashSet.has(set, "grape")) // false + * HashSet.has(set, "apple") // => true + * HashSet.has(set, "grape") // => false * * class Person implements Equal.Equal { * constructor(readonly name: string) {} @@ -256,10 +251,10 @@ export const add: { * } * * const people = HashSet.make(new Person("Alice"), new Person("Bob")) - * console.log(HashSet.has(people, new Person("Alice"))) // true + * HashSet.has(people, new Person("Alice")) // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -275,19 +270,18 @@ export const has: { * * **Example** (Removing values from a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const set = HashSet.make("a", "b", "c") * const withoutB = HashSet.remove(set, "b") * - * console.log(HashSet.size(set)) // 3 (original unchanged) - * console.log(HashSet.size(withoutB)) // 2 - * console.log(HashSet.has(withoutB, "b")) // false + * set // => HashSet.make("a", "b", "c") + * withoutB // => HashSet.make("a", "c") + * HashSet.has(withoutB, "b") // => false * * // Removing non-existent value has no effect - * const same = HashSet.remove(set, "d") - * console.log(HashSet.size(same)) // 3 + * HashSet.remove(set, "d") // => HashSet.make("a", "b", "c") * ``` * * @category mutations @@ -306,17 +300,14 @@ export const remove: { * * **Example** (Getting the HashSet size) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const empty = HashSet.empty() - * console.log(HashSet.size(empty)) // 0 + * HashSet.size(HashSet.empty()) // => 0 * - * const small = HashSet.make("a", "b") - * console.log(HashSet.size(small)) // 2 + * HashSet.size(HashSet.make("a", "b")) // => 2 * - * const withDuplicates = HashSet.fromIterable(["x", "y", "z", "x", "y"]) - * console.log(HashSet.size(withDuplicates)) // 3 + * HashSet.size(HashSet.fromIterable(["x", "y", "z", "x", "y"])) // => 3 * ``` * * @category getters @@ -329,17 +320,15 @@ export const size: (self: HashSet) => number = internal.size * * **Example** (Checking whether a HashSet is empty) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const empty = HashSet.empty() - * console.log(HashSet.isEmpty(empty)) // true + * HashSet.isEmpty(HashSet.empty()) // => true * - * const nonEmpty = HashSet.make("a") - * console.log(HashSet.isEmpty(nonEmpty)) // false + * HashSet.isEmpty(HashSet.make("a")) // => false * ``` * - * @category getters + * @category predicates * @since 4.0.0 */ export const isEmpty: (self: HashSet) => boolean = internal.isEmpty @@ -349,15 +338,10 @@ export const isEmpty: (self: HashSet) => boolean = internal.isEmpty * * **Example** (Combining HashSets) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const set1 = HashSet.make("a", "b") - * const set2 = HashSet.make("b", "c") - * const combined = HashSet.union(set1, set2) - * - * console.log(Array.from(combined).sort()) // ["a", "b", "c"] - * console.log(HashSet.size(combined)) // 3 + * HashSet.union(HashSet.make("a", "b"), HashSet.make("b", "c")) // => HashSet.make("a", "b", "c") * ``` * * @category combinators @@ -376,15 +360,10 @@ export const union: { * * **Example** (Finding common HashSet values) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const set1 = HashSet.make("a", "b", "c") - * const set2 = HashSet.make("b", "c", "d") - * const common = HashSet.intersection(set1, set2) - * - * console.log(Array.from(common).sort()) // ["b", "c"] - * console.log(HashSet.size(common)) // 2 + * HashSet.intersection(HashSet.make("a", "b", "c"), HashSet.make("b", "c", "d")) // => HashSet.make("b", "c") * ``` * * @category combinators @@ -403,15 +382,10 @@ export const intersection: { * * **Example** (Finding HashSet differences) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const set1 = HashSet.make("a", "b", "c") - * const set2 = HashSet.make("b", "d") - * const diff = HashSet.difference(set1, set2) - * - * console.log(Array.from(diff).sort()) // ["a", "c"] - * console.log(HashSet.size(diff)) // 2 + * HashSet.difference(HashSet.make("a", "b", "c"), HashSet.make("b", "d")) // => HashSet.make("a", "c") * ``` * * @category combinators @@ -430,20 +404,20 @@ export const difference: { * * **Example** (Checking subset relationships) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const small = HashSet.make("a", "b") * const large = HashSet.make("a", "b", "c", "d") * const other = HashSet.make("x", "y") * - * console.log(HashSet.isSubset(small, large)) // true - * console.log(HashSet.isSubset(large, small)) // false - * console.log(HashSet.isSubset(small, other)) // false - * console.log(HashSet.isSubset(small, small)) // true + * HashSet.isSubset(small, large) // => true + * HashSet.isSubset(large, small) // => false + * HashSet.isSubset(small, other) // => false + * HashSet.isSubset(small, small) // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const isSubset: { @@ -459,19 +433,18 @@ export const isSubset: { * * **Example** (Mapping HashSet values) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const numbers = HashSet.make(1, 2, 3) * const doubled = HashSet.map(numbers, (n) => n * 2) * - * console.log(Array.from(doubled).sort()) // [2, 4, 6] - * console.log(HashSet.size(doubled)) // 3 + * doubled // => HashSet.make(2, 4, 6) * * // Mapping can reduce size if function produces duplicates * const strings = HashSet.make("apple", "banana", "cherry") * const lengths = HashSet.map(strings, (s) => s.length) - * console.log(Array.from(lengths).sort()) // [5, 6] (apple=5, banana=6, cherry=6) + * lengths // => HashSet.make(5, 6) * ``` * * @category mapping @@ -490,14 +463,10 @@ export const map: { * * **Example** (Filtering HashSet values) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * - * const numbers = HashSet.make(1, 2, 3, 4, 5, 6) - * const evens = HashSet.filter(numbers, (n) => n % 2 === 0) - * - * console.log(Array.from(evens).sort()) // [2, 4, 6] - * console.log(HashSet.size(evens)) // 3 + * HashSet.filter(HashSet.make(1, 2, 3, 4, 5, 6), (n) => n % 2 === 0) // => HashSet.make(2, 4, 6) * ``` * * @category filtering @@ -524,19 +493,18 @@ export const filter: { * * **Example** (Testing whether some values match) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const numbers = HashSet.make(1, 2, 3, 4, 5) * - * console.log(HashSet.some(numbers, (n) => n > 3)) // true - * console.log(HashSet.some(numbers, (n) => n > 10)) // false + * HashSet.some(numbers, (n) => n > 3) // => true + * HashSet.some(numbers, (n) => n > 10) // => false * - * const empty = HashSet.empty() - * console.log(HashSet.some(empty, (n) => n > 0)) // false + * HashSet.some(HashSet.empty(), (n) => n > 0) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const some: { @@ -552,19 +520,18 @@ export const some: { * * **Example** (Testing whether every value matches) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const numbers = HashSet.make(2, 4, 6, 8) * - * console.log(HashSet.every(numbers, (n) => n % 2 === 0)) // true - * console.log(HashSet.every(numbers, (n) => n > 5)) // false + * HashSet.every(numbers, (n) => n % 2 === 0) // => true + * HashSet.every(numbers, (n) => n > 5) // => false * - * const empty = HashSet.empty() - * console.log(HashSet.every(empty, (n) => n > 0)) // true (vacuously true) + * HashSet.every(HashSet.empty(), (n) => n > 0) // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const every: { @@ -580,17 +547,11 @@ export const every: { * * **Example** (Reducing HashSet values) * - * ```ts + * ```ts import.meta.vitest * import { HashSet } from "effect" * * const numbers = HashSet.make(1, 2, 3, 4, 5) - * const sum = HashSet.reduce(numbers, 0, (acc, n) => acc + n) - * - * console.log(sum) // 15 - * - * const strings = HashSet.make("a", "b", "c") - * const concatenated = HashSet.reduce(strings, "", (acc, s) => acc + s) - * console.log(concatenated) // Order may vary: "abc", "bac", etc. + * HashSet.reduce(numbers, 0, (acc, n) => acc + n) // => 15 * ``` * * @category folding diff --git a/.context/effect/packages/effect/src/Inspectable.ts b/.context/effect/packages/effect/src/Inspectable.ts index 05fed12bf..47843fffa 100644 --- a/.context/effect/packages/effect/src/Inspectable.ts +++ b/.context/effect/packages/effect/src/Inspectable.ts @@ -28,7 +28,7 @@ import { redact } from "./Redactable.ts" * * **Example** (Defining custom Node inspection) * - * ```ts + * ```ts import.meta.vitest * import { Inspectable } from "effect" * * class CustomObject { @@ -40,7 +40,7 @@ import { redact } from "./Redactable.ts" * } * * const obj = new CustomObject("hello") - * console.log(obj) // Displays: CustomObject(hello) + * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(hello)" * ``` * * @category symbols @@ -59,7 +59,7 @@ export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom") * * **Example** (Typing custom Node inspection) * - * ```ts + * ```ts import.meta.vitest * import { Inspectable } from "effect" * * class CustomObject { @@ -71,7 +71,7 @@ export const NodeInspectSymbol = Symbol.for("nodejs.util.inspect.custom") * } * * const obj = new CustomObject("test") - * console.log(obj) // CustomObject(test) + * obj[Inspectable.NodeInspectSymbol]() // => "CustomObject(test)" * ``` * * @category symbols @@ -94,7 +94,7 @@ export type NodeInspectSymbol = typeof NodeInspectSymbol * * **Example** (Implementing inspectable objects) * - * ```ts + * ```ts import.meta.vitest * import { Formatter, Inspectable } from "effect" * * class Result implements Inspectable.Inspectable { @@ -117,7 +117,7 @@ export type NodeInspectSymbol = typeof NodeInspectSymbol * } * * const success = new Result("Success", 42) - * console.log(success.toString()) // Pretty formatted JSON + * success.toString() // => "{\"_tag\":\"Success\",\"value\":42}" * ``` * * @category models @@ -130,19 +130,20 @@ export interface Inspectable { } /** - * Converts a value to a JSON-serializable representation safely. + * Converts a value to its structured inspection representation. * * **When to use** * - * Use when you need a safe, JSON-serializable representation of a value + * Use when you need the structured representation of an inspectable value * without risking unhandled errors. * * **Details** * - * This function attempts to extract JSON data from objects that implement the - * `toJSON` method, recursively processes arrays, and handles errors gracefully. - * For objects that don't have a `toJSON` method, it applies redaction to - * protect sensitive information. + * This function applies redaction before extracting data from objects that + * implement `toJSON`, recursively processes arrays, and handles errors + * gracefully. Plain objects are returned unchanged, so the result is not + * guaranteed to be accepted by `JSON.stringify`; it may still contain values + * such as `BigInt`, functions, or circular references. * * @see {@link toStringUnknown} for converting unknown values to strings * @@ -151,6 +152,7 @@ export interface Inspectable { */ export const toJson = (input: unknown): unknown => { try { + input = redact(input) if ( Predicate.hasProperty(input, "toJSON") && Predicate.isFunction(input["toJSON"]) && @@ -160,10 +162,10 @@ export const toJson = (input: unknown): unknown => { } else if (Array.isArray(input)) { return input.map(toJson) } + return input } catch { return "[toJSON threw]" } - return redact(input) } /** @@ -187,7 +189,7 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi return u } try { - return typeof u === "object" ? formatJson(u, { space: whitespace }) : String(u) + return typeof u === "object" ? formatJson(u, { space: whitespace }) : format(u, { space: whitespace }) } catch { return String(u) } @@ -208,7 +210,7 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi * * **Example** (Using the base inspectable prototype) * - * ```ts + * ```ts import.meta.vitest * import { Inspectable } from "effect" * * // Use as prototype @@ -216,7 +218,7 @@ export const toStringUnknown = (u: unknown, whitespace: number | string | undefi * myObject.name = "example" * myObject.value = 42 * - * console.log(myObject.toString()) // Pretty printed representation + * myObject.toString() // => "\"[toJSON threw]\"" * * // Or extend in a constructor * function MyClass(this: any, name: string) { @@ -256,7 +258,7 @@ export const BaseProto: Inspectable = { * * **Example** (Extending the inspectable base class) * - * ```ts + * ```ts import.meta.vitest * import { Inspectable } from "effect" * * class User extends Inspectable.Class { @@ -279,11 +281,11 @@ export const BaseProto: Inspectable = { * } * * const user = new User(1, "Alice", "alice@example.com") - * console.log(user.toString()) // Pretty printed JSON with _tag, id, name, email - * console.log(user) // In Node.js, shows the same formatted output + * user.toString() // => "{\"_tag\":\"User\",\"id\":1,\"name\":\"Alice\",\"email\":\"alice@example.com\"}" + * user[Inspectable.NodeInspectSymbol]() // => { _tag: "User", id: 1, name: "Alice", email: "alice@example.com" } * ``` * - * @category classes + * @category models * @since 2.0.0 */ export abstract class Class { diff --git a/.context/effect/packages/effect/src/Iterable.ts b/.context/effect/packages/effect/src/Iterable.ts index d30a08234..39e588f8b 100644 --- a/.context/effect/packages/effect/src/Iterable.ts +++ b/.context/effect/packages/effect/src/Iterable.ts @@ -12,6 +12,7 @@ import type { NonEmptyArray } from "./Array.ts" import * as Equal from "./Equal.ts" import { dual } from "./Function.ts" +import * as InternalRecord from "./internal/record.ts" import type { Option } from "./Option.ts" import * as O from "./Option.ts" import { isBoolean } from "./Predicate.ts" @@ -32,21 +33,21 @@ import type { NoInfer } from "./Types.ts" * * **Example** (Generating values by index) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Generate first 5 even numbers * const evens = Iterable.makeBy((n) => n * 2, { length: 5 }) - * console.log(Array.from(evens)) // [0, 2, 4, 6, 8] + * Array.from(evens) // => [0, 2, 4, 6, 8] * * // Generate squares * const squares = Iterable.makeBy((n) => n * n, { length: 4 }) - * console.log(Array.from(squares)) // [0, 1, 4, 9] + * Array.from(squares) // => [0, 1, 4, 9] * * // Infinite sequence (be careful when consuming!) * const naturals = Iterable.makeBy((n) => n) * const first10 = Iterable.take(naturals, 10) - * console.log(Array.from(first10)) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + * Array.from(first10) // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] * ``` * * @category constructors @@ -82,11 +83,10 @@ export const makeBy = (f: (i: number) => A, options?: { * * **Example** (Creating a range) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Array.from(Iterable.range(1, 3)), [1, 2, 3]) + * Array.from(Iterable.range(1, 3)) // => [1, 2, 3] * ``` * * @category constructors @@ -110,11 +110,10 @@ export const range = (start: number, end?: number): Iterable => { * * **Example** (Repeating a value) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Array.from(Iterable.replicate("a", 3)), ["a", "a", "a"]) + * Array.from(Iterable.replicate("a", 3)) // => ["a", "a", "a"] * ``` * * @category constructors @@ -173,15 +172,11 @@ export const forever = (self: Iterable): Iterable => repeat(self, Infin * * **Example** (Converting a record to entries) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3 } - * assert.deepStrictEqual(Array.from(Iterable.fromRecord(x)), [["a", 1], ["b", 2], [ - * "c", - * 3 - * ]]) + * Array.from(Iterable.fromRecord(x)) // => [["a", 1], ["b", 2], ["c", 3]] * ``` * * @category converting @@ -202,17 +197,17 @@ export const fromRecord = (self: Readonly>): I * * **Example** (Prepending an element) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [2, 3, 4] * const withOne = Iterable.prepend(numbers, 1) - * console.log(Array.from(withOne)) // [1, 2, 3, 4] + * Array.from(withOne) // => [1, 2, 3, 4] * * // Works with any iterable * const letters = "abc" * const withZ = Iterable.prepend(letters, "z") - * console.log(Array.from(withZ)) // ["z", "a", "b", "c"] + * Array.from(withZ) // => ["z", "a", "b", "c"] * ``` * * @category combining @@ -228,14 +223,10 @@ export const prepend: { * * **Example** (Prepending another iterable) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Array.from(Iterable.prependAll([1, 2], ["a", "b"])), - * ["a", "b", 1, 2] - * ) + * Array.from(Iterable.prependAll([1, 2], ["a", "b"])) // => ["a", "b", 1, 2] * ``` * * @category combining @@ -269,19 +260,11 @@ export const prependAll: { * * **Example** (Appending an element) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3] - * const withFour = Iterable.append(numbers, 4) - * console.log(Array.from(withFour)) // [1, 2, 3, 4] - * - * // Chain multiple appends - * const result = Iterable.append( - * Iterable.append([1, 2], 3), - * 4 - * ) - * console.log(Array.from(result)) // [1, 2, 3, 4] + * Array.from(Iterable.append(numbers, 4)) // => [1, 2, 3, 4] * ``` * * @see {@link prepend} for adding one element before the existing elements @@ -314,25 +297,21 @@ export const append: { * * **Example** (Concatenating iterables) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * - * const first = [1, 2, 3] - * const second = [4, 5, 6] - * const combined = Iterable.appendAll(first, second) - * console.log(Array.from(combined)) // [1, 2, 3, 4, 5, 6] + * Array.from(Iterable.appendAll([1, 2, 3], [4, 5, 6])) // => [1, 2, 3, 4, 5, 6] * * // Works with different iterable types * const numbers = [1, 2] * const letters = "abc" * const mixed = Iterable.appendAll(numbers, letters) - * console.log(Array.from(mixed)) // [1, 2, "a", "b", "c"] + * Array.from(mixed) // => [1, 2, "a", "b", "c"] * * // Lazy evaluation - only consumes what's needed * const infinite = Iterable.range(1) * const finite = [0, -1, -2] - * const result = Iterable.take(Iterable.appendAll(finite, infinite), 5) - * console.log(Array.from(result)) // [0, -1, -2, 1, 2] + * Array.from(Iterable.take(Iterable.appendAll(finite, infinite), 5)) // => [0, -1, -2, 1, 2] * ``` * * @see {@link append} for appending one value instead of another iterable @@ -374,23 +353,23 @@ export const appendAll: { * * **Example** (Tracking running results) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Running sum of numbers * const numbers = [1, 2, 3, 4, 5] * const runningSum = Iterable.scan(numbers, 0, (acc, n) => acc + n) - * console.log(Array.from(runningSum)) // [0, 1, 3, 6, 10, 15] + * Array.from(runningSum) // => [0, 1, 3, 6, 10, 15] * * // Build strings progressively * const letters = ["a", "b", "c"] * const progressive = Iterable.scan(letters, "", (acc, letter) => acc + letter) - * console.log(Array.from(progressive)) // ["", "a", "ab", "abc"] + * Array.from(progressive) // => ["", "a", "ab", "abc"] * * // Track maximum values seen so far * const values = [3, 1, 4, 1, 5, 9, 2] * const runningMax = Iterable.scan(values, -Infinity, Math.max) - * console.log(Array.from(runningMax)) // [-Infinity, 3, 3, 4, 4, 5, 9, 9] + * Array.from(runningMax) // => [-Infinity, 3, 3, 4, 4, 5, 9, 9] * ``` * * @category folding @@ -424,12 +403,11 @@ export const scan: { * * **Example** (Checking for emptiness) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Iterable.isEmpty([]), true) - * assert.deepStrictEqual(Iterable.isEmpty([1, 2, 3]), false) + * Iterable.isEmpty([]) // => true + * Iterable.isEmpty([1, 2, 3]) // => false * ``` * * @category guards @@ -445,22 +423,22 @@ export const isEmpty = (self: Iterable): self is Iterable => { * * **Example** (Counting iterable elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3, 4, 5] - * console.log(Iterable.size(numbers)) // 5 + * Iterable.size(numbers) // => 5 * * const empty = Iterable.empty() - * console.log(Iterable.size(empty)) // 0 + * Iterable.size(empty) // => 0 * * // Works with any iterable * const letters = "hello" - * console.log(Iterable.size(letters)) // 5 + * Iterable.size(letters) // => 5 * * // Note: This consumes the entire iterable * const range = Iterable.range(1, 100) - * console.log(Iterable.size(range)) // 100 + * Iterable.size(range) // => 100 * ``` * * @category getters @@ -480,24 +458,24 @@ export const size = (self: Iterable): number => { * * **Example** (Getting the first element) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Option } from "effect" * * const numbers = [1, 2, 3] - * console.log(Iterable.head(numbers)) // Option.some(1) + * Iterable.head(numbers) // => Option.some(1) * * const empty = Iterable.empty() - * console.log(Iterable.head(empty)) // Option.none() + * Iterable.head(empty) // => Option.none() * * // Safe way to get first element * const firstEven = Iterable.head( * Iterable.filter([1, 3, 4, 5], (x) => x % 2 === 0) * ) - * console.log(firstEven) // Option.some(4) + * firstEven // => Option.some(4) * * // Use with Option methods * const doubled = Option.map(Iterable.head([5, 10, 15]), (x) => x * 2) - * console.log(doubled) // Option.some(10) + * doubled // => Option.some(10) * ``` * * @category getters @@ -523,21 +501,21 @@ export const head = (self: Iterable): Option => { * * **Example** (Getting the first element unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3] - * console.log(Iterable.headUnsafe(numbers)) // 1 + * Iterable.headUnsafe(numbers) // => 1 * * const letters = "hello" - * console.log(Iterable.headUnsafe(letters)) // "h" + * Iterable.headUnsafe(letters) // => "h" * * // Iterable.headUnsafe(Iterable.empty()) * // throws Error: "headUnsafe: empty iterable" * * // Use only when you're certain the iterable is non-empty * const nonEmpty = Iterable.range(1, 10) - * console.log(Iterable.headUnsafe(nonEmpty)) // 1 + * Iterable.headUnsafe(nonEmpty) // => 1 * ``` * * @category getters @@ -559,25 +537,25 @@ export const headUnsafe = (self: Iterable): A => { * * **Example** (Taking from the start) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3, 4, 5] * const firstThree = Iterable.take(numbers, 3) - * console.log(Array.from(firstThree)) // [1, 2, 3] + * Array.from(firstThree) // => [1, 2, 3] * * // Taking more than available returns all elements * const firstTen = Iterable.take(numbers, 10) - * console.log(Array.from(firstTen)) // [1, 2, 3, 4, 5] + * Array.from(firstTen) // => [1, 2, 3, 4, 5] * * // Taking 0 or negative returns empty * const none = Iterable.take(numbers, 0) - * console.log(Array.from(none)) // [] + * Array.from(none) // => [] * * // Useful with infinite iterables * const naturals = Iterable.range(1) * const firstFive = Iterable.take(naturals, 5) - * console.log(Array.from(firstFive)) // [1, 2, 3, 4, 5] + * Array.from(firstFive) // => [1, 2, 3, 4, 5] * ``` * * @category getters @@ -608,22 +586,22 @@ export const take: { * * **Example** (Taking while a predicate holds) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [2, 4, 6, 8, 3, 10, 12] * const evenPrefix = Iterable.takeWhile(numbers, (x) => x % 2 === 0) - * console.log(Array.from(evenPrefix)) // [2, 4, 6, 8] + * Array.from(evenPrefix) // => [2, 4, 6, 8] * * // With index * const letters = ["a", "b", "c", "d", "e"] * const firstThreeByIndex = Iterable.takeWhile(letters, (_, i) => i < 3) - * console.log(Array.from(firstThreeByIndex)) // ["a", "b", "c"] + * Array.from(firstThreeByIndex) // => ["a", "b", "c"] * * // Stops at first non-matching element * const mixed = [1, 3, 5, 4, 7, 9] * const oddPrefix = Iterable.takeWhile(mixed, (x) => x % 2 === 1) - * console.log(Array.from(oddPrefix)) // [1, 3, 5] + * Array.from(oddPrefix) // => [1, 3, 5] * * // Type refinement * const values: Array = ["a", "b", "c", 1, "d"] @@ -631,7 +609,7 @@ export const take: { * values, * (x): x is string => typeof x === "string" * ) - * console.log(Array.from(stringPrefix)) // ["a", "b", "c"] (typed as string[]) + * Array.from(stringPrefix) // => ["a", "b", "c"] * ``` * * @category getters @@ -667,24 +645,24 @@ export const takeWhile: { * * **Example** (Dropping from the start) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3, 4, 5] * const withoutFirstTwo = Iterable.drop(numbers, 2) - * console.log(Array.from(withoutFirstTwo)) // [3, 4, 5] + * Array.from(withoutFirstTwo) // => [3, 4, 5] * * // Dropping more than available returns empty * const withoutFirstTen = Iterable.drop(numbers, 10) - * console.log(Array.from(withoutFirstTen)) // [] + * Array.from(withoutFirstTen) // => [] * * // Dropping 0 or negative returns all elements * const all = Iterable.drop(numbers, 0) - * console.log(Array.from(all)) // [1, 2, 3, 4, 5] + * Array.from(all) // => [1, 2, 3, 4, 5] * * // Combine with take for slicing * const slice = Iterable.take(Iterable.drop(numbers, 1), 3) - * console.log(Array.from(slice)) // [2, 3, 4] + * Array.from(slice) // => [2, 3, 4] * ``` * * @category getters @@ -718,20 +696,20 @@ export const drop: { * * **Example** (Finding the first match) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Option } from "effect" * * const numbers = [1, 3, 4, 6, 8] * const firstEven = Iterable.findFirst(numbers, (x) => x % 2 === 0) - * console.log(firstEven) // Option.some(4) + * firstEven // => Option.some(4) * * const firstGreaterThan10 = Iterable.findFirst(numbers, (x) => x > 10) - * console.log(firstGreaterThan10) // Option.none() + * firstGreaterThan10 // => Option.none() * * // With index * const letters = ["a", "b", "c", "d"] * const atEvenIndex = Iterable.findFirst(letters, (_, i) => i % 2 === 0) - * console.log(atEvenIndex) // Option.some("a") + * atEvenIndex // => Option.some("a") * * // Type refinement * const mixed: Array = [1, "hello", 2, "world"] @@ -739,17 +717,17 @@ export const drop: { * mixed, * (x): x is string => typeof x === "string" * ) - * console.log(firstString) // Option.some("hello") + * firstString // => Option.some("hello") * * // Transform during search * const findSquareRoot = Iterable.findFirst([1, 4, 9, 16], (x) => { * const sqrt = Math.sqrt(x) * return Number.isInteger(sqrt) ? Option.some(sqrt) : Option.none() * }) - * console.log(findSquareRoot) // Option.some(1) + * findSquareRoot // => Option.some(1) * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findFirst: { @@ -785,20 +763,20 @@ export const findFirst: { * * **Example** (Finding the last match) * - * ```ts - * import { Iterable } from "effect" + * ```ts import.meta.vitest + * import { Iterable, Option } from "effect" * * const numbers = [1, 3, 4, 6, 8, 2] * const lastEven = Iterable.findLast(numbers, (x) => x % 2 === 0) - * console.log(lastEven) // Option.some(2) + * lastEven // => Option.some(2) * * const lastGreaterThan10 = Iterable.findLast(numbers, (x) => x > 10) - * console.log(lastGreaterThan10) // Option.none() + * lastGreaterThan10 // => Option.none() * * // With index * const letters = ["a", "b", "c", "d", "e"] * const lastAtEvenIndex = Iterable.findLast(letters, (_, i) => i % 2 === 0) - * console.log(lastAtEvenIndex) // Option.some("e") (index 4) + * lastAtEvenIndex // => Option.some("e") * * // Type refinement * const mixed: Array = [1, "hello", 2, "world", 3] @@ -806,10 +784,10 @@ export const findFirst: { * mixed, * (x): x is string => typeof x === "string" * ) - * console.log(lastString) // Option.some("world") + * lastString // => Option.some("world") * ``` * - * @category elements + * @category searching * @since 2.0.0 */ export const findLast: { @@ -846,31 +824,31 @@ export const findLast: { * * **Example** (Zipping iterables) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3] * const letters = ["a", "b", "c"] * const zipped = Iterable.zip(numbers, letters) - * console.log(Array.from(zipped)) // [[1, "a"], [2, "b"], [3, "c"]] + * Array.from(zipped) // => [[1, "a"], [2, "b"], [3, "c"]] * * // Different lengths - shorter one determines result length * const short = [1, 2] * const long = ["a", "b", "c", "d"] * const partial = Iterable.zip(short, long) - * console.log(Array.from(partial)) // [[1, "a"], [2, "b"]] + * Array.from(partial) // => [[1, "a"], [2, "b"]] * * // Works with any iterables * const range = Iterable.range(1, 3) * const word = "abc" * const mixed = Iterable.zip(range, word) - * console.log(Array.from(mixed)) // [[1, "a"], [2, "b"], [3, "c"]] + * Array.from(mixed) // => [[1, "a"], [2, "b"], [3, "c"]] * * // Create indexed pairs * const values = ["apple", "banana", "cherry"] * const indices = Iterable.range(0, 2) * const indexed = Iterable.zip(indices, values) - * console.log(Array.from(indexed)) // [[0, "apple"], [1, "banana"], [2, "cherry"]] + * Array.from(indexed) // => [[0, "apple"], [1, "banana"], [2, "cherry"]] * ``` * * @category zipping @@ -890,14 +868,14 @@ export const zip: { * * **Example** (Zipping with a combining function) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Add corresponding elements * const a = [1, 2, 3, 4] * const b = [10, 20, 30, 40] * const sums = Iterable.zipWith(a, b, (x, y) => x + y) - * console.log(Array.from(sums)) // [11, 22, 33, 44] + * Array.from(sums) // => [11, 22, 33, 44] * * // Combine strings * const firstNames = ["John", "Jane", "Bob"] @@ -907,7 +885,7 @@ export const zip: { * lastNames, * (first, last) => `${first} ${last}` * ) - * console.log(Array.from(fullNames)) // ["John Doe", "Jane Smith", "Bob Johnson"] + * Array.from(fullNames) // => ["John Doe", "Jane Smith", "Bob Johnson"] * * // Different lengths - stops at shorter * const short = [1, 2] @@ -917,7 +895,7 @@ export const zip: { * long, * (num, letter) => `${num}${letter}` * ) - * console.log(Array.from(combined)) // ["1a", "2b"] + * Array.from(combined) // => ["1a", "2b"] * * // Complex transformations * const prices = [10.99, 25.50, 5.00] @@ -925,7 +903,7 @@ export const zip: { * const totals = Iterable.zipWith(prices, quantities, (price, qty) => { * return Math.round(price * qty * 100) / 100 // round to 2 decimal places * }) - * console.log(Array.from(totals)) // [21.98, 25.5, 15] + * Array.from(totals) // => [21.98, 25.5, 15] * ``` * * @category zipping @@ -964,33 +942,33 @@ export const zipWith: { * * **Example** (Interspersing separators) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Join numbers with separator * const numbers = [1, 2, 3, 4] * const withCommas = Iterable.intersperse(numbers, ",") - * console.log(Array.from(withCommas)) // [1, ",", 2, ",", 3, ",", 4] + * Array.from(withCommas) // => [1, ",", 2, ",", 3, ",", 4] * * // Join words with spaces * const words = ["hello", "world", "from", "effect"] * const sentence = Iterable.intersperse(words, " ") - * console.log(Array.from(sentence).join("")) // "hello world from effect" + * Array.from(sentence).join("") // => "hello world from effect" * * // Empty iterable remains empty * const empty = Iterable.empty() * const stillEmpty = Iterable.intersperse(empty, "-") - * console.log(Array.from(stillEmpty)) // [] + * Array.from(stillEmpty) // => [] * * // Single element has no separators added * const single = [42] * const noSeparator = Iterable.intersperse(single, "|") - * console.log(Array.from(noSeparator)) // [42] + * Array.from(noSeparator) // => [42] * * // Build CSS-like strings * const styles = ["color: red", "font-size: 14px", "margin: 10px"] * const css = Iterable.intersperse(styles, "; ") - * console.log(Array.from(css).join("")) // "color: red; font-size: 14px; margin: 10px" + * Array.from(css).join("") // => "color: red; font-size: 14px; margin: 10px" * ``` * * @category combining @@ -1026,7 +1004,7 @@ export const intersperse: { * * **Example** (Checking membership with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Custom equivalence for objects @@ -1035,7 +1013,7 @@ export const intersperse: { * * const users = [{ id: 1 }, { id: 2 }] * const hasUser1 = containsById(users, { id: 1 }) - * console.log(hasUser1) // true (same id) + * hasUser1 // => true * * // Case-insensitive string comparison * const caseInsensitive = (a: string, b: string) => @@ -1044,7 +1022,7 @@ export const intersperse: { * * const words = ["Hello", "World"] * const hasHello = containsCaseInsensitive(words, "hello") - * console.log(hasHello) // true + * hasHello // => true * * // Approximate number comparison * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.1 @@ -1052,10 +1030,10 @@ export const intersperse: { * * const values = [1.0, 2.0, 3.0] * const hasAlmostTwo = containsApprox(values, 2.05) - * console.log(hasAlmostTwo) // true + * hasAlmostTwo // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { @@ -1082,29 +1060,29 @@ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { * * **Example** (Checking membership) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3, 4, 5] - * console.log(Iterable.contains(numbers, 3)) // true - * console.log(Iterable.contains(numbers, 6)) // false + * Iterable.contains(numbers, 3) // => true + * Iterable.contains(numbers, 6) // => false * * const letters = "hello" - * console.log(Iterable.contains(letters, "l")) // true - * console.log(Iterable.contains(letters, "x")) // false + * Iterable.contains(letters, "l") // => true + * Iterable.contains(letters, "x") // => false * * // Works with any iterable * const range = Iterable.range(1, 100) - * console.log(Iterable.contains(range, 50)) // true - * console.log(Iterable.contains(range, 150)) // false + * Iterable.contains(range, 50) // => true + * Iterable.contains(range, 150) // => false * * // Curried version * const containsThree = Iterable.contains(3) - * console.log(containsThree([1, 2, 3])) // true - * console.log(containsThree([4, 5, 6])) // false + * containsThree([1, 2, 3]) // => true + * containsThree([4, 5, 6]) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const contains: { @@ -1118,25 +1096,22 @@ export const contains: { * * **Example** (Chunking an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9] * const chunks = Iterable.chunksOf(numbers, 3) - * console.log(Array.from(chunks).map((chunk) => Array.from(chunk))) - * // [[1, 2, 3], [4, 5, 6], [7, 8, 9]] + * Array.from(chunks) // => [[1, 2, 3], [4, 5, 6], [7, 8, 9]] * * // Last chunk can be shorter * const uneven = [1, 2, 3, 4, 5, 6, 7] * const chunks2 = Iterable.chunksOf(uneven, 3) - * console.log(Array.from(chunks2).map((chunk) => Array.from(chunk))) - * // [[1, 2, 3], [4, 5, 6], [7]] + * Array.from(chunks2) // => [[1, 2, 3], [4, 5, 6], [7]] * * // Chunk size larger than iterable * const small = [1, 2] * const chunks3 = Iterable.chunksOf(small, 5) - * console.log(Array.from(chunks3).map((chunk) => Array.from(chunk))) - * // [[1, 2]] + * Array.from(chunks3) // => [[1, 2]] * * // Process data in batches * const data = Iterable.range(1, 100) @@ -1145,7 +1120,7 @@ export const contains: { * batches, * (batch) => Iterable.reduce(batch, 0, (sum, n) => sum + n) * ) - * console.log(Array.from(Iterable.take(batchSums, 3))) // [55, 155, 255] + * Array.from(Iterable.take(batchSums, 3)) // => [55, 155, 255] * ``` * * @category splitting @@ -1187,35 +1162,31 @@ export const chunksOf: { * * **Example** (Grouping consecutive elements with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Group consecutive equal numbers * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] * const grouped = Iterable.groupWith(numbers, (a, b) => a === b) - * console.log(Array.from(grouped)) - * // [[1, 1], [2, 2, 2], [3], [1, 1]] + * Array.from(grouped) // => [[1, 1], [2, 2, 2], [3], [1, 1]] * * // Case-insensitive grouping of strings * const words = ["Apple", "APPLE", "banana", "Banana", "cherry"] * const caseInsensitive = (a: string, b: string) => * a.toLowerCase() === b.toLowerCase() * const groupedWords = Iterable.groupWith(words, caseInsensitive) - * console.log(Array.from(groupedWords)) - * // [["Apple", "APPLE"], ["banana", "Banana"], ["cherry"]] + * Array.from(groupedWords) // => [["Apple", "APPLE"], ["banana", "Banana"], ["cherry"]] * * // Group by approximate equality * const floats = [1.1, 1.12, 1.9, 2.01, 2.05, 3.5] * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.2 * const groupedFloats = Iterable.groupWith(floats, approxEqual) - * console.log(Array.from(groupedFloats)) - * // [[1.1, 1.12], [1.9, 2.01, 2.05], [3.5]] + * Array.from(groupedFloats) // => [[1.1, 1.12], [1.9, 2.01, 2.05], [3.5]] * * // Only groups consecutive elements * const scattered = [1, 2, 1, 2, 1] * const scatteredGroups = Iterable.groupWith(scattered, (a, b) => a === b) - * console.log(Array.from(scatteredGroups)) - * // [[1], [2], [1], [2], [1]] (no grouping since none are consecutive) + * Array.from(scatteredGroups) // => [[1], [2], [1], [2], [1]] * ``` * * @category grouping @@ -1266,18 +1237,16 @@ export const groupWith: { * * **Example** (Grouping consecutive elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] * const grouped = Iterable.group(numbers) - * console.log(Array.from(grouped)) - * // [[1, 1], [2, 2, 2], [3], [1, 1]] + * Array.from(grouped) // => [[1, 1], [2, 2, 2], [3], [1, 1]] * * const letters = "aabbccaa" * const groupedLetters = Iterable.group(letters) - * console.log(Array.from(groupedLetters)) - * // [["a", "a"], ["b", "b"], ["c", "c"], ["a", "a"]] + * Array.from(groupedLetters) // => [["a", "a"], ["b", "b"], ["c", "c"], ["a", "a"]] * * // Works with objects using deep equality * const objects = [ @@ -1287,7 +1256,7 @@ export const groupWith: { * { type: "A", value: 1 } * ] * const groupedObjects = Iterable.group(objects) - * console.log(Array.from(groupedObjects).length) // 3 groups + * Array.from(groupedObjects).length // => 3 * // Note: Only consecutive equal objects are grouped together * ``` * @@ -1309,20 +1278,18 @@ export const group: (self: Iterable) => Iterable> = group * * **Example** (Grouping by a key) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Group by string length * const words = ["a", "bb", "ccc", "dd", "eee", "f"] * const byLength = Iterable.groupBy(words, (word) => word.length.toString()) - * console.log(byLength) - * // { "1": ["a", "f"], "2": ["bb", "dd"], "3": ["ccc", "eee"] } + * byLength // => { "1": ["a", "f"], "2": ["bb", "dd"], "3": ["ccc", "eee"] } * * // Group by first letter * const names = ["Alice", "Bob", "Charlie", "David", "Anna", "Betty"] * const byFirstLetter = Iterable.groupBy(names, (name) => name[0]) - * console.log(byFirstLetter) - * // { "A": ["Alice", "Anna"], "B": ["Bob", "Betty"], "C": ["Charlie"], "D": ["David"] } + * byFirstLetter // => { A: ["Alice", "Anna"], B: ["Bob", "Betty"], C: ["Charlie"], D: ["David"] } * * // Group by category * const items = [ @@ -1332,17 +1299,12 @@ export const group: (self: Iterable) => Iterable> = group * { name: "broccoli", category: "vegetable" } * ] * const byCategory = Iterable.groupBy(items, (item) => item.category) - * console.log(byCategory) - * // { - * // "fruit": [{ name: "apple", category: "fruit" }, { name: "banana", category: "fruit" }], - * // "vegetable": [{ name: "carrot", category: "vegetable" }, { name: "broccoli", category: "vegetable" }] - * // } + * Object.keys(byCategory) // => ["fruit", "vegetable"] * * // Group numbers by even/odd * const numbers = [1, 2, 3, 4, 5, 6] * const evenOdd = Iterable.groupBy(numbers, (n) => n % 2 === 0 ? "even" : "odd") - * console.log(evenOdd) - * // { "odd": [1, 3, 5], "even": [2, 4, 6] } + * evenOdd // => { odd: [1, 3, 5], even: [2, 4, 6] } * ``` * * @category grouping @@ -1366,7 +1328,7 @@ export const groupBy: { if (Object.hasOwn(out, k)) { out[k].push(a) } else { - out[k] = [a] + InternalRecord.assignProperty(out, k, [a]) } } return out @@ -1393,18 +1355,10 @@ const constEmptyIterator: Iterator = { * * **Example** (Creating an empty iterable) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * - * const empty = Iterable.empty() - * console.log(Array.from(empty)) // [] - * console.log(Iterable.isEmpty(empty)) // true - * - * // Useful as base case for reductions - * const hasData = true - * const result = hasData - * ? Iterable.range(1, 5) - * : Iterable.empty() + * Array.from(Iterable.empty()) // => [] * ``` * * @category constructors @@ -1422,11 +1376,11 @@ export const empty = (): Iterable => constEmpty * * **Example** (Wrapping a single value) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const single = Iterable.of(42) - * console.log(Array.from(single)) // [42] + * Array.from(single) // => [42] * * // Useful for creating homogeneous sequences * const sequences = [ @@ -1441,7 +1395,7 @@ export const empty = (): Iterable => constEmpty * numbers, * (n) => n % 2 === 0 ? Iterable.of(n) : Iterable.empty() * ) - * console.log(Array.from(evensOnly)) // [2, 4] + * Array.from(evensOnly) // => [2, 4] * ``` * * @category constructors @@ -1461,24 +1415,22 @@ export const of = (a: A): Iterable => [a] * * **Example** (Mapping elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Transform numbers to their squares * const numbers = [1, 2, 3, 4, 5] * const squares = Iterable.map(numbers, (x) => x * x) - * console.log(Array.from(squares)) // [1, 4, 9, 16, 25] + * Array.from(squares) // => [1, 4, 9, 16, 25] * * // Use index in transformation * const indexed = Iterable.map(["a", "b", "c"], (char, i) => `${i}: ${char}`) - * console.log(Array.from(indexed)) // ["0: a", "1: b", "2: c"] + * Array.from(indexed) // => ["0: a", "1: b", "2: c"] * - * // Chain transformations - * const result = Iterable.map( + * Array.from(Iterable.map( * Iterable.map([1, 2, 3], (x) => x * 2), * (x) => x + 1 - * ) - * console.log(Array.from(result)) // [3, 5, 7] + * )) // => [3, 5, 7] * ``` * * @category mapping @@ -1510,18 +1462,18 @@ export const map: { * * **Example** (Flat mapping iterables) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Expand each number to a range * const numbers = [1, 2, 3] * const expanded = Iterable.flatMap(numbers, (n) => Iterable.range(1, n)) - * console.log(Array.from(expanded)) // [1, 1, 2, 1, 2, 3] + * Array.from(expanded) // => [1, 1, 2, 1, 2, 3] * * // Split strings into characters * const words = ["hi", "bye"] * const chars = Iterable.flatMap(words, (word) => word) - * console.log(Array.from(chars)) // ["h", "i", "b", "y", "e"] + * Array.from(chars) // => ["h", "i", "b", "y", "e"] * * // Conditional expansion with empty iterables * const values = [1, 2, 3, 4, 5] @@ -1529,7 +1481,7 @@ export const map: { * values, * (n) => n % 2 === 0 ? [n, n * 2, n * 3] : [] * ) - * console.log(Array.from(evenMultiples)) // [2, 4, 6, 4, 8, 12] + * Array.from(evenMultiples) // => [2, 4, 6, 4, 8, 12] * * // Use index in transformation * const letters = ["a", "b", "c"] @@ -1537,7 +1489,7 @@ export const map: { * letters, * (letter, i) => Iterable.replicate(letter, i + 1) * ) - * console.log(Array.from(indexed)) // ["a", "b", "b", "c", "c", "c"] + * Array.from(indexed) // => ["a", "b", "b", "c", "c", "c"] * ``` * * @category sequencing @@ -1558,29 +1510,29 @@ export const flatMap: { * * **Example** (Flattening nested iterables) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Flatten nested arrays * const nested = [[1, 2], [3, 4], [5, 6]] * const flat = Iterable.flatten(nested) - * console.log(Array.from(flat)) // [1, 2, 3, 4, 5, 6] + * Array.from(flat) // => [1, 2, 3, 4, 5, 6] * * // Flatten different iterable types * const mixed: Array> = ["ab", "cd"] * const flatMixed = Iterable.flatten(mixed) - * console.log(Array.from(flatMixed)) // ["a", "b", "c", "d"] + * Array.from(flatMixed) // => ["a", "b", "c", "d"] * * // Flatten deeply nested (only one level) * const deepNested = [[[1, 2]], [[3, 4]]] * const oneLevelFlat = Iterable.flatten(deepNested) - * console.log(Array.from(oneLevelFlat).map((arr) => Array.from(arr))) + * Array.from(oneLevelFlat) // => [[1, 2], [3, 4]] * // [[1, 2], [3, 4]] (still contains arrays) * * // Empty iterables are handled correctly * const withEmpty = [[1, 2], [], [3, 4], []] * const flatWithEmpty = Iterable.flatten(withEmpty) - * console.log(Array.from(flatWithEmpty)) // [1, 2, 3, 4] + * Array.from(flatWithEmpty) // => [1, 2, 3, 4] * ``` * * @category sequencing @@ -1591,19 +1543,20 @@ export const flatten = (self: Iterable>): Iterable => ({ const outerIterator = self[Symbol.iterator]() let innerIterator: Iterator | undefined function next() { - if (innerIterator === undefined) { - const next = outerIterator.next() - if (next.done) { - return next + while (true) { + if (innerIterator === undefined) { + const next = outerIterator.next() + if (next.done) { + return next + } + innerIterator = next.value[Symbol.iterator]() + } + const result = innerIterator.next() + if (!result.done) { + return result } - innerIterator = next.value[Symbol.iterator]() - } - const result = innerIterator.next() - if (result.done) { innerIterator = undefined - return next() } - return result } return { next } } @@ -1620,7 +1573,7 @@ export const flatten = (self: Iterable>): Iterable => ({ * * **Example** (Filtering and transforming Result values) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Result } from "effect" * * // Parse strings to numbers, keeping only valid ones @@ -1629,7 +1582,7 @@ export const flatten = (self: Iterable>): Iterable => ({ * const num = parseInt(s) * return isNaN(num) ? Result.failVoid : Result.succeed(num) * }) - * console.log(Array.from(numbers)) // [1, 2, 4] + * Array.from(numbers) // => [1, 2, 4] * * // Extract specific properties from objects * const users = [ @@ -1643,7 +1596,7 @@ export const flatten = (self: Iterable>): Iterable => ({ * (user) => * user.age >= 18 && user.email ? Result.succeed(user.email) : Result.failVoid * ) - * console.log(Array.from(adultEmails)) // ["alice@example.com", "charlie@example.com"] + * Array.from(adultEmails) // => ["alice@example.com", "charlie@example.com"] * * // Use index in transformation * const items = ["a", "b", "c", "d", "e"] @@ -1651,7 +1604,7 @@ export const flatten = (self: Iterable>): Iterable => ({ * items, * (item, i) => i % 2 === 0 ? Result.succeed(`${i}: ${item}`) : Result.failVoid * ) - * console.log(Array.from(evenIndexItems)) // ["0: a", "2: c", "4: e"] + * Array.from(evenIndexItems) // => ["0: a", "2: c", "4: e"] * ``` * * @category filtering @@ -1688,7 +1641,7 @@ export const filterMap: { * * **Example** (Filtering and transforming until failure) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Result } from "effect" * * // Parse numbers until we hit an invalid one @@ -1697,7 +1650,7 @@ export const filterMap: { * const num = parseInt(s) * return isNaN(num) ? Result.failVoid : Result.succeed(num) * }) - * console.log(Array.from(numbers)) // [1, 2, 3] (stops at "invalid") + * Array.from(numbers) // => [1, 2, 3] * * // Take elements while they meet a condition and transform them * const values = [2, 4, 6, 7, 8, 10] @@ -1705,7 +1658,7 @@ export const filterMap: { * values, * (n) => n % 2 === 0 ? Result.succeed(n * 2) : Result.failVoid * ) - * console.log(Array.from(doubledEvens)) // [4, 8, 12] (stops at 7) + * Array.from(doubledEvens) // => [4, 8, 12] * * // Process with index until condition fails * const letters = ["a", "b", "c", "d", "e"] @@ -1713,7 +1666,7 @@ export const filterMap: { * letters, * (letter, i) => letter !== "c" ? Result.succeed(`${i}: ${letter}`) : Result.failVoid * ) - * console.log(Array.from(indexedUntilC)) // ["0: a", "1: b"] (stops at "c") + * Array.from(indexedUntilC) // => ["0: a", "1: b"] * ``` * * @category filtering @@ -1747,16 +1700,10 @@ export const filterMapWhile: { * * **Example** (Extracting Some values) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Option } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Array.from( - * Iterable.getSomes([Option.some(1), Option.none(), Option.some(2)]) - * ), - * [1, 2] - * ) + * Array.from(Iterable.getSomes([Option.some(1), Option.none(), Option.some(2)])) // => [1, 2] * ``` * * @category filtering @@ -1788,20 +1735,14 @@ export const getSomes = (self: Iterable>): Iterable => { * * **Example** (Extracting failures) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Result } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * Array.from( - * Iterable.getFailures([ - * Result.succeed(1), - * Result.fail("err"), - * Result.succeed(2) - * ]) - * ), - * ["err"] - * ) + * + * Array.from(Iterable.getFailures([ + * Result.succeed(1), + * Result.fail("err"), + * Result.succeed(2) + * ])) // => ["err"] * ``` * * @category filtering @@ -1833,20 +1774,14 @@ export const getFailures = (self: Iterable>): Iterable = * * **Example** (Extracting successes) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Result } from "effect" - * import * as assert from "node:assert" - * - * assert.deepStrictEqual( - * Array.from( - * Iterable.getSuccesses([ - * Result.succeed(1), - * Result.fail("err"), - * Result.succeed(2) - * ]) - * ), - * [1, 2] - * ) + * + * Array.from(Iterable.getSuccesses([ + * Result.succeed(1), + * Result.fail("err"), + * Result.succeed(2) + * ])) // => [1, 2] * ``` * * @category filtering @@ -1883,18 +1818,18 @@ export const getSuccesses = (self: Iterable>): Iterable * * **Example** (Filtering elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Filter even numbers * const numbers = [1, 2, 3, 4, 5, 6] * const evens = Iterable.filter(numbers, (x) => x % 2 === 0) - * console.log(Array.from(evens)) // [2, 4, 6] + * Array.from(evens) // => [2, 4, 6] * * // Filter with index * const items = ["a", "b", "c", "d"] * const oddPositions = Iterable.filter(items, (_, i) => i % 2 === 1) - * console.log(Array.from(oddPositions)) // ["b", "d"] + * Array.from(oddPositions) // => ["b", "d"] * * // Type refinement * const mixed: Array = ["hello", 42, "world", 100] @@ -1902,14 +1837,14 @@ export const getSuccesses = (self: Iterable>): Iterable * mixed, * (x): x is string => typeof x === "string" * ) - * console.log(Array.from(onlyStrings)) // ["hello", "world"] (typed as string[]) + * Array.from(onlyStrings) // => ["hello", "world"] * * // Combine with map * const processed = Iterable.map( * Iterable.filter([1, 2, 3, 4, 5], (x) => x > 2), * (x) => x * 10 * ) - * console.log(Array.from(processed)) // [30, 40, 50] + * Array.from(processed) // => [30, 40, 50] * ``` * * @category filtering @@ -1952,7 +1887,7 @@ export const filter: { * * **Example** (Flat mapping nullable results) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Extract valid elements from nullable function results @@ -1961,7 +1896,7 @@ export const filter: { * const num = parseInt(s) * return isNaN(num) ? null : num * 2 * }) - * console.log(Array.from(parsed)) // [2, 4, 8] + * Array.from(parsed) // => [2, 4, 8] * * // Safe property access * const objects = [ @@ -1971,7 +1906,7 @@ export const filter: { * {} * ] * const values = Iterable.flatMapNullishOr(objects, (obj) => obj.nested?.value) - * console.log(Array.from(values)) // [10, 20] + * Array.from(values) // => [10, 20] * * // Working with Map.get (returns undefined for missing keys) * const map = new Map([ @@ -1981,7 +1916,7 @@ export const filter: { * ]) * const keys = ["a", "x", "b", "y", "c"] * const foundValues = Iterable.flatMapNullishOr(keys, (key) => map.get(key)) - * console.log(Array.from(foundValues)) // [1, 2, 3] + * Array.from(foundValues) // => [1, 2, 3] * ``` * * @category sequencing @@ -2004,21 +1939,21 @@ export const flatMapNullishOr: { * * **Example** (Checking whether some element matches) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * const numbers = [1, 3, 5, 7, 8] * const hasEven = Iterable.some(numbers, (x) => x % 2 === 0) - * console.log(hasEven) // true (because of 8) + * hasEven // => true * * const allOdd = [1, 3, 5, 7] * const hasEvenInAllOdd = Iterable.some(allOdd, (x) => x % 2 === 0) - * console.log(hasEvenInAllOdd) // false + * hasEvenInAllOdd // => false * * // With index * const letters = ["a", "b", "c"] * const hasElementAtIndex2 = Iterable.some(letters, (_, i) => i === 2) - * console.log(hasElementAtIndex2) // true + * hasElementAtIndex2 // => true * * // Early termination - stops at first match * const infiniteOdds = Iterable.filter(Iterable.range(1), (x) => x % 2 === 1) @@ -2026,7 +1961,7 @@ export const flatMapNullishOr: { * Iterable.take(infiniteOdds, 1000), * (x) => x % 2 === 0 * ) - * console.log(hasEvenInInfiniteOdds) // false (quickly, doesn't check all 1000) + * hasEvenInInfiniteOdds // => false * * // Type guard usage * const mixed: Array = [1, 2, "hello"] @@ -2034,10 +1969,10 @@ export const flatMapNullishOr: { * mixed, * (x): x is string => typeof x === "string" * ) - * console.log(hasString) // true + * hasString // => true * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const some: { @@ -2068,21 +2003,21 @@ export const some: { * * **Example** (Unfolding state into values) * - * ```ts + * ```ts import.meta.vitest * import { Iterable, Option } from "effect" * * // Generate Fibonacci sequence * const fibonacci = Iterable.unfold([0, 1], ([a, b]) => Option.some([a, [b, a + b]])) * const first10Fib = Iterable.take(fibonacci, 10) - * console.log(Array.from(first10Fib)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] + * Array.from(first10Fib) // => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] * * // Generate powers of 2 up to a limit * const powersOf2 = Iterable.unfold(1, (n) => n <= 1000 ? Option.some([n, n * 2]) : Option.none()) - * console.log(Array.from(powersOf2)) // [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + * Array.from(powersOf2) // => [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] * * // Generate countdown * const countdown = Iterable.unfold(5, (n) => n > 0 ? Option.some([n, n - 1]) : Option.none()) - * console.log(Array.from(countdown)) // [5, 4, 3, 2, 1] + * Array.from(countdown) // => [5, 4, 3, 2, 1] * * // Generate collatz sequence * const collatz = Iterable.unfold(7, (n) => { @@ -2090,7 +2025,7 @@ export const some: { * const next = n % 2 === 0 ? n / 2 : n * 3 + 1 * return Option.some([n, next]) * }) - * console.log(Array.from(collatz)) // [7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2] + * Array.from(collatz) // => [7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2] * ``` * * @category constructors @@ -2118,37 +2053,40 @@ export const unfold = (b: B, f: (b: B) => Option): Iterab * * **Example** (Iterating with side effects) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * - * // Print each element + * // Collect each visited element * const numbers = [1, 2, 3, 4, 5] - * Iterable.forEach(numbers, (n) => console.log(n)) - * // Prints: 1, 2, 3, 4, 5 + * const visited: Array = [] + * Iterable.forEach(numbers, (n) => visited.push(n)) + * visited // => [1, 2, 3, 4, 5] * * // Use index in the callback * const letters = ["a", "b", "c"] + * const indexed: Array = [] * Iterable.forEach(letters, (letter, i) => { - * console.log(`${i}: ${letter}`) + * indexed.push(`${i}: ${letter}`) * }) - * // Prints: "0: a", "1: b", "2: c" + * indexed // => ["0: a", "1: b", "2: c"] * * // Side effects with any iterable * const results: Array = [] * Iterable.forEach(Iterable.range(1, 5), (n) => { * results.push(n * n) * }) - * console.log(results) // [1, 4, 9, 16, 25] + * results // => [1, 4, 9, 16, 25] * * // Process in chunks * const data = Iterable.chunksOf([1, 2, 3, 4, 5, 6], 2) + * const processed: Array> = [] * Iterable.forEach(data, (chunk) => { - * console.log(`Processing chunk: ${Array.from(chunk)}`) + * processed.push(Array.from(chunk)) * }) - * // Prints: "Processing chunk: 1,2", "Processing chunk: 3,4", "Processing chunk: 5,6" + * processed // => [[1, 2], [3, 4], [5, 6]] * ``` * - * @category elements + * @category traversing * @since 2.0.0 */ export const forEach: { @@ -2171,18 +2109,17 @@ export const forEach: { * * **Example** (Reducing an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Sum all numbers * const numbers = [1, 2, 3, 4, 5] * const sum = Iterable.reduce(numbers, 0, (acc, n) => acc + n) - * console.log(sum) // 15 + * sum // => 15 * * // Find maximum value * const values = [3, 1, 4, 1, 5, 9, 2] - * const max = Iterable.reduce(values, -Infinity, Math.max) - * console.log(max) // 9 + * Iterable.reduce(values, -Infinity, (max, value) => Math.max(max, value)) // => 9 * * // Build an object from key-value pairs * const pairs = [["a", 1], ["b", 2], ["c", 3]] as const @@ -2194,7 +2131,7 @@ export const forEach: { * return acc * } * ) - * console.log(obj) // { a: 1, b: 2, c: 3 } + * obj // => { a: 1, b: 2, c: 3 } * * // Use index in the reducer * const letters = ["a", "b", "c"] @@ -2206,7 +2143,7 @@ export const forEach: { * return acc * } * ) - * console.log(indexed) // ["0: a", "1: b", "2: c"] + * indexed // => ["0: a", "1: b", "2: c"] * ``` * * @category folding @@ -2232,20 +2169,20 @@ export const reduce: { * * **Example** (Deduplicating adjacent elements with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Remove adjacent duplicates with custom equality * const numbers = [1, 1, 2, 2, 3, 1, 1] * const dedupedNumbers = Iterable.dedupeAdjacentWith(numbers, (a, b) => a === b) - * console.log(Array.from(dedupedNumbers)) // [1, 2, 3, 1] + * Array.from(dedupedNumbers) // => [1, 2, 3, 1] * * // Case-insensitive deduplication * const words = ["Hello", "HELLO", "world", "World", "test"] * const caseInsensitive = (a: string, b: string) => * a.toLowerCase() === b.toLowerCase() * const dedupedWords = Iterable.dedupeAdjacentWith(words, caseInsensitive) - * console.log(Array.from(dedupedWords)) // ["Hello", "world", "test"] + * Array.from(dedupedWords) // => ["Hello", "world", "test"] * * // Deduplication by object property * const users = [ @@ -2257,13 +2194,13 @@ export const reduce: { * ] * const byId = (a: typeof users[0], b: typeof users[0]) => a.id === b.id * const dedupedUsers = Iterable.dedupeAdjacentWith(users, byId) - * console.log(Array.from(dedupedUsers).map((u) => u.id)) // [1, 2, 3] + * Array.from(dedupedUsers, (user) => user.id) // => [1, 2, 3] * * // Approximate numeric equality * const floats = [1.0, 1.01, 1.02, 2.0, 2.01, 3.0] * const approxEqual = (a: number, b: number) => Math.abs(a - b) < 0.1 * const dedupedFloats = Iterable.dedupeAdjacentWith(floats, approxEqual) - * console.log(Array.from(dedupedFloats)) // [1.0, 2.0, 3.0] + * Array.from(dedupedFloats) // => [1, 2, 3] * ``` * * @category filtering @@ -2303,18 +2240,18 @@ export const dedupeAdjacentWith: { * * **Example** (Deduplicating adjacent elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Remove adjacent duplicate numbers * const numbers = [1, 1, 2, 2, 2, 3, 1, 1] * const deduped = Iterable.dedupeAdjacent(numbers) - * console.log(Array.from(deduped)) // [1, 2, 3, 1] + * Array.from(deduped) // => [1, 2, 3, 1] * * // Remove adjacent duplicate characters * const letters = "aabbccaa" * const dedupedLetters = Iterable.dedupeAdjacent(letters) - * console.log(Array.from(dedupedLetters)) // ["a", "b", "c", "a"] + * Array.from(dedupedLetters) // => ["a", "b", "c", "a"] * * // Works with objects using deep equality * const objects = [ @@ -2325,12 +2262,12 @@ export const dedupeAdjacentWith: { * { type: "A" } * ] * const dedupedObjects = Iterable.dedupeAdjacent(objects) - * console.log(Array.from(dedupedObjects).map((o) => o.type)) // ["A", "B", "A"] + * Array.from(dedupedObjects, (object) => object.type) // => ["A", "B", "A"] * * // Clean up streaming data * const sensorData = [100, 100, 100, 101, 101, 102, 102, 102, 100] * const cleanedData = Iterable.dedupeAdjacent(sensorData) - * console.log(Array.from(cleanedData)) // [100, 101, 102, 100] + * Array.from(cleanedData) // => [100, 101, 102, 100] * ``` * * @category filtering @@ -2343,14 +2280,14 @@ export const dedupeAdjacent: (self: Iterable) => Iterable = dedupeAdjac * * **Example** (Combining cartesian products) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // Create coordinate pairs * const xs = [1, 2] * const ys = ["a", "b", "c"] * const coordinates = Iterable.cartesianWith(xs, ys, (x, y) => `(${x},${y})`) - * console.log(Array.from(coordinates)) // ["(1,a)", "(1,b)", "(1,c)", "(2,a)", "(2,b)", "(2,c)"] + * Array.from(coordinates) // => ["(1,a)", "(1,b)", "(1,c)", "(2,a)", "(2,b)", "(2,c)"] * * // Generate all combinations of options * const sizes = ["S", "M", "L"] @@ -2360,18 +2297,13 @@ export const dedupeAdjacent: (self: Iterable) => Iterable = dedupeAdjac * colors, * (size, color) => ({ size, color }) * ) - * console.log(Array.from(products)) - * // [ - * // { size: "S", color: "red" }, { size: "S", color: "blue" }, - * // { size: "M", color: "red" }, { size: "M", color: "blue" }, - * // { size: "L", color: "red" }, { size: "L", color: "blue" } - * // ] + * Array.from(products, ({ color, size }) => `${size}:${color}`) // => ["S:red", "S:blue", "M:red", "M:blue", "L:red", "L:blue"] * * // Mathematical operations on all pairs * const a = [1, 2, 3] * const b = [10, 20] * const mathProducts = Iterable.cartesianWith(a, b, (x, y) => x * y) - * console.log(Array.from(mathProducts)) // [10, 20, 20, 40, 30, 60] + * Array.from(mathProducts) // => [10, 20, 20, 40, 30, 60] * * // Create test data combinations * const userTypes = ["admin", "user"] @@ -2381,11 +2313,10 @@ export const dedupeAdjacent: (self: Iterable) => Iterable = dedupeAdjac * features, * (user, feature) => `${user}_can_${feature}` * ) - * console.log(Array.from(testCases)) - * // ["admin_can_read", "admin_can_write", "admin_can_delete", "user_can_read", "user_can_write", "user_can_delete"] + * Array.from(testCases) // => ["admin_can_read", "admin_can_write", "admin_can_delete", "user_can_read", "user_can_write", "user_can_delete"] * ``` * - * @category elements + * @category combining * @since 2.0.0 */ export const cartesianWith: { @@ -2432,40 +2363,34 @@ export const cartesianWith: { * * **Example** (Generating cartesian pairs) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * * // All pairs of numbers and letters * const numbers = [1, 2, 3] * const letters = ["a", "b"] * const pairs = Iterable.cartesian(numbers, letters) - * console.log(Array.from(pairs)) - * // [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]] + * Array.from(pairs) // => [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]] * * // Generate coordinate grid * const x = [0, 1, 2] * const y = [0, 1] * const grid = Iterable.cartesian(x, y) - * console.log(Array.from(grid)) - * // [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]] + * Array.from(grid) // => [[0, 0], [0, 1], [1, 0], [1, 1], [2, 0], [2, 1]] * * // All combinations for testing * const browsers = ["chrome", "firefox"] * const devices = ["desktop", "mobile", "tablet"] * const testMatrix = Iterable.cartesian(browsers, devices) - * console.log(Array.from(testMatrix)) - * // [ - * // ["chrome", "desktop"], ["chrome", "mobile"], ["chrome", "tablet"], - * // ["firefox", "desktop"], ["firefox", "mobile"], ["firefox", "tablet"] - * // ] + * Array.from(testMatrix, ([browser, device]) => `${browser}:${device}`) // => ["chrome:desktop", "chrome:mobile", "chrome:tablet", "firefox:desktop", "firefox:mobile", "firefox:tablet"] * * // Empty iterable results in empty cartesian product * const empty = Iterable.empty() * const withEmpty = Iterable.cartesian([1, 2], empty) - * console.log(Array.from(withEmpty)) // [] + * Array.from(withEmpty) // => [] * ``` * - * @category elements + * @category combining * @since 2.0.0 */ export const cartesian: { @@ -2481,11 +2406,10 @@ export const cartesian: { * * **Example** (Counting matching elements) * - * ```ts + * ```ts import.meta.vitest * import { Iterable } from "effect" * - * const result = Iterable.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) - * console.log(result) // 2 + * Iterable.countBy([1, 2, 3, 4, 5], (n) => n % 2 === 0) // => 2 * ``` * * @category folding diff --git a/.context/effect/packages/effect/src/JsonPatch.ts b/.context/effect/packages/effect/src/JsonPatch.ts index 5e05cd7d3..605d24e2b 100644 --- a/.context/effect/packages/effect/src/JsonPatch.ts +++ b/.context/effect/packages/effect/src/JsonPatch.ts @@ -7,9 +7,8 @@ * * @since 4.0.0 */ -import { format } from "./Formatter.ts" +import * as InternalRecord from "./internal/record.ts" import { escapeToken, unescapeToken } from "./JsonPointer.ts" -import * as Predicate from "./Predicate.ts" import type * as Schema from "./Schema.ts" /** @@ -31,7 +30,7 @@ import type * as Schema from "./Schema.ts" * * **Example** (Defining all operation types) * - * ```ts + * ```ts import.meta.vitest * import { JsonPatch } from "effect" * * const addOp: JsonPatch.JsonPatchOperation = { @@ -50,6 +49,8 @@ import type * as Schema from "./Schema.ts" * path: "/users/0/name", * value: "Bob" * } + * + * Array.of(addOp.op, removeOp.op, replaceOp.op) // => ["add", "remove", "replace"] * ``` * * @see {@link JsonPatch} for the array of operations forming a complete patch @@ -115,7 +116,7 @@ export type JsonPatchOperation = * * **Example** (Defining a multi-operation patch) * - * ```ts + * ```ts import.meta.vitest * import { JsonPatch } from "effect" * * const patch: JsonPatch.JsonPatch = [ @@ -124,8 +125,7 @@ export type JsonPatchOperation = * { op: "remove", path: "/oldField" } * ] * - * const result = JsonPatch.apply(patch, { count: 3, oldField: "value" }) - * // { count: 5, items: ["apple"] } + * JsonPatch.apply(patch, { items: [], count: 3, oldField: "value" }) // => { items: ["apple"], count: 5 } * ``` * * @see {@link JsonPatchOperation} for individual operation types @@ -160,18 +160,16 @@ export type JsonPatch = ReadonlyArray * * **Example** (Computing object diff) * - * ```ts + * ```ts import.meta.vitest * import { JsonPatch } from "effect" * * const oldValue = { users: [{ id: 1, name: "Alice" }], count: 1 } * const newValue = { users: [{ id: 1, name: "Bob" }, { id: 2, name: "Charlie" }], count: 2 } * * const patch = JsonPatch.get(oldValue, newValue) - * // [ - * // { op: "replace", path: "/users/0/name", value: "Bob" }, - * // { op: "add", path: "/users/1", value: { id: 2, name: "Charlie" } }, - * // { op: "replace", path: "/count", value: 2 } - * // ] + * patch[0] // => { op: "replace", path: "/count", value: 2 } + * patch[1] // => { op: "replace", path: "/users/0/name", value: "Bob" } + * patch[2] // => { op: "add", path: "/users/1", value: { id: 2, name: "Charlie" } } * ``` * * @see {@link apply} to apply the generated patch to a document @@ -180,9 +178,18 @@ export type JsonPatch = ReadonlyArray * @since 4.0.0 */ export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { - if (Object.is(oldValue, newValue)) return [] const patches: Array = [] + getLoop(oldValue, newValue, "", patches) + return patches +} +function getLoop( + oldValue: Schema.Json, + newValue: Schema.Json, + path: string, + patches: Array +): void { + if (Object.is(oldValue, newValue)) return if (Array.isArray(oldValue) && Array.isArray(newValue)) { const len1 = oldValue.length const len2 = newValue.length @@ -190,25 +197,20 @@ export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { // Compare shared prefix by index const shared = Math.min(len1, len2) for (let i = 0; i < shared; i++) { - const path = `/${i}` - const patch = get(oldValue[i], newValue[i]) - for (const op of patch) { - prefixPathInPlace(op, path) - patches.push(op) - } + getLoop(oldValue[i], newValue[i], `${path}/${i}`, patches) } // Remove from end to start so later indices do not shift. for (let i = len1 - 1; i >= len2; i--) { - patches.push({ op: "remove", path: `/${i}` }) + patches.push({ op: "remove", path: `${path}/${i}` }) } // Add from beginning to end. for (let i = len1; i < len2; i++) { - patches.push({ op: "add", path: `/${i}`, value: newValue[i] }) + patches.push({ op: "add", path: `${path}/${i}`, value: newValue[i] }) } - return patches + return } if (isJsonObject(oldValue) && isJsonObject(newValue)) { @@ -217,29 +219,23 @@ export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { const allKeys = Array.from(new Set([...keys1, ...keys2])).sort() for (const key of allKeys) { - const esc = escapeToken(key) - const path = `/${esc}` + const keyPath = `${path}/${escapeToken(key)}` const hasKey1 = Object.hasOwn(oldValue, key) const hasKey2 = Object.hasOwn(newValue, key) if (hasKey1 && hasKey2) { - const patch = get(oldValue[key], newValue[key]) - for (const op of patch) { - prefixPathInPlace(op, path) - patches.push(op) - } + getLoop(oldValue[key], newValue[key], keyPath, patches) } else if (!hasKey1 && hasKey2) { - patches.push({ op: "add", path, value: newValue[key] }) - } else if (hasKey1 && !hasKey2) { - patches.push({ op: "remove", path }) + patches.push({ op: "add", path: keyPath, value: newValue[key] }) + } else { + patches.push({ op: "remove", path: keyPath }) } } - return patches + return } - patches.push({ op: "replace", path: "", value: newValue }) - return patches + patches.push({ op: "replace", path, value: newValue }) } /** @@ -266,7 +262,7 @@ export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { * * **Example** (Applying a patch) * - * ```ts + * ```ts import.meta.vitest * import { JsonPatch } from "effect" * * const document = { items: [1, 2, 3], total: 6 } @@ -275,8 +271,7 @@ export function get(oldValue: Schema.Json, newValue: Schema.Json): JsonPatch { * { op: "replace", path: "/total", value: 10 } * ] * - * const result = JsonPatch.apply(patch, document) - * // { items: [1, 2, 3, 4], total: 10 } + * JsonPatch.apply(patch, document) // => { items: [1, 2, 3, 4], total: 10 } * ``` * * @see {@link get} to generate patches from value differences @@ -288,32 +283,14 @@ export function apply(patch: JsonPatch, oldValue: Schema.Json): Schema.Json { let doc = oldValue for (const op of patch) { - switch (op.op) { - case "replace": { - doc = op.path === "" ? op.value : setAt(doc, op.path, op.value, "replace") - break - } - case "add": { - doc = addAt(doc, op.path, op.value) - break - } - case "remove": { - doc = setAt(doc, op.path, undefined, "remove") - break - } - } + doc = applyOperation(doc, op) } return doc } -// Mutates op.path in place for perf; safe because child ops are freshly created and not shared. -function prefixPathInPlace(op: JsonPatchOperation, parent: string): void { - ;(op as any).path = op.path === "" ? parent : parent + op.path -} - function isJsonObject(value: unknown): value is Schema.JsonObject { - return Predicate.isObject(value) + return typeof value === "object" && value !== null && !Array.isArray(value) } /** @@ -325,7 +302,7 @@ function isJsonObject(value: unknown): value is Schema.JsonObject { function tokenize(pointer: string): Array { if (pointer === "") return [] if (pointer.charCodeAt(0) !== 47 /* "/" */) { - throw new Error(`Invalid JSON Pointer, it must start with "/": ${format(pointer)}`) + throw new Error(`Invalid JSON Pointer, it must start with "/": ${JSON.stringify(pointer)}`) } return pointer.split("/").slice(1).map(unescapeToken) } @@ -338,72 +315,44 @@ function toIndex(token: string): number { return Number(token) } -function addAt(doc: Schema.Json, pointer: string, val: Schema.Json): Schema.Json { - if (pointer === "") return val - - const resolved = resolveParent(doc, pointer) - if (resolved === null) { - throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) - } - - const { lastToken, parent, stack } = resolved - - if (Array.isArray(parent)) { - const idx = lastToken === "-" ? parent.length : toIndex(lastToken) - if (idx < 0 || idx > parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) - const updated = parent.slice() - updated.splice(idx, 0, val) - return rebuildFromStack(stack, updated) - } - - if (isJsonObject(parent)) { - const updated = { ...parent } - updated[lastToken] = val - return rebuildFromStack(stack, updated) - } - - throw new Error(`Cannot add at "${pointer}" (parent not found or not a container).`) -} - -function setAt( - doc: Schema.Json, - pointer: string, - val: Schema.Json | undefined, - mode: "replace" | "remove" -): Schema.Json { - if (pointer === "") { - if (mode === "remove" || val === undefined) throw new Error("Unsupported operation at the root") - return val +function applyOperation(doc: Schema.Json, op: JsonPatchOperation): Schema.Json { + if (op.path === "") { + if (op.op === "remove") throw new Error("Unsupported operation at the root") + return op.value } - const resolved = resolveParent(doc, pointer) + const resolved = resolveParent(doc, op.path) if (resolved === null) { - throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) + throw new Error(`Cannot ${op.op} at "${op.path}" (parent not found or not a container).`) } const { lastToken, parent, stack } = resolved if (Array.isArray(parent)) { - if (lastToken === "-") throw new Error(`"-" is not valid for ${mode} at "${pointer}".`) - const idx = toIndex(lastToken) - if (idx < 0 || idx >= parent.length) throw new Error(`Array index out of bounds at "${pointer}".`) + if (lastToken === "-" && op.op !== "add") { + throw new Error(`"-" is not valid for ${op.op} at "${op.path}".`) + } + const index = lastToken === "-" ? parent.length : toIndex(lastToken) + const maxIndex = op.op === "add" ? parent.length : parent.length - 1 + if (index > maxIndex) throw new Error(`Array index out of bounds at "${op.path}".`) const updated = parent.slice() - if (mode === "remove") updated.splice(idx, 1) - else updated[idx] = val + if (op.op === "add") updated.splice(index, 0, op.value) + else if (op.op === "remove") updated.splice(index, 1) + else updated[index] = op.value return rebuildFromStack(stack, updated) } if (isJsonObject(parent)) { - if (!Object.hasOwn(parent, lastToken)) { - throw new Error(`Property "${lastToken}" does not exist at "${pointer}".`) + if (op.op !== "add" && !Object.hasOwn(parent, lastToken)) { + throw new Error(`Property "${lastToken}" does not exist at "${op.path}".`) } const updated = { ...parent } - if (mode === "remove") delete updated[lastToken] - else updated[lastToken] = val! + if (op.op === "remove") delete updated[lastToken] + else InternalRecord.assignProperty(updated, lastToken, op.value) return rebuildFromStack(stack, updated) } - throw new Error(`Cannot ${mode} at "${pointer}" (parent not found or not a container).`) + throw new Error(`Cannot ${op.op} at "${op.path}" (parent not found or not a container).`) } type StackEntry = { readonly container: unknown; readonly token: number | string } @@ -424,20 +373,18 @@ function resolveParent( for (let i = 0; i < tokens.length - 1; i++) { const token = tokens[i] - if (cur == null) return null - if (Array.isArray(cur)) { const idx = toIndex(token) - if (idx < 0 || idx >= cur.length) return null + if (idx >= cur.length) return null stack.push({ container: cur, token: idx }) cur = cur[idx] continue } - if (cur && typeof cur === "object") { + if (isJsonObject(cur)) { if (!Object.hasOwn(cur, token)) return null stack.push({ container: cur, token }) - cur = (cur as any)[token] + cur = cur[token] continue } @@ -460,7 +407,7 @@ function rebuildFromStack(stack: ReadonlyArray, newParent: Schema.Js acc = copy } else { const copy = { ...(container as Schema.JsonObject) } - copy[token as string] = acc + InternalRecord.assignProperty(copy, token as string, acc) acc = copy } } diff --git a/.context/effect/packages/effect/src/JsonPointer.ts b/.context/effect/packages/effect/src/JsonPointer.ts index a3d073381..385d578b6 100644 --- a/.context/effect/packages/effect/src/JsonPointer.ts +++ b/.context/effect/packages/effect/src/JsonPointer.ts @@ -27,12 +27,12 @@ * * **Example** (Escaping special characters) * - * ```ts + * ```ts import.meta.vitest * import { JsonPointer } from "effect" * - * JsonPointer.escapeToken("a/b") // "a~1b" - * JsonPointer.escapeToken("c~d") // "c~0d" - * JsonPointer.escapeToken("path/to~key") // "path~1to~0key" + * JsonPointer.escapeToken("a/b") // => "a~1b" + * JsonPointer.escapeToken("c~d") // => "c~0d" + * JsonPointer.escapeToken("path/to~key") // => "path~1to~0key" * ``` * * @see {@link unescapeToken} The inverse operation for decoding escaped tokens @@ -63,12 +63,12 @@ export function escapeToken(token: string): string { * * **Example** (Unescaping special characters) * - * ```ts + * ```ts import.meta.vitest * import { JsonPointer } from "effect" * - * JsonPointer.unescapeToken("a~1b") // "a/b" - * JsonPointer.unescapeToken("c~0d") // "c~d" - * JsonPointer.unescapeToken("path~1to~0key") // "path/to~key" + * JsonPointer.unescapeToken("a~1b") // => "a/b" + * JsonPointer.unescapeToken("c~0d") // => "c~d" + * JsonPointer.unescapeToken("path~1to~0key") // => "path/to~key" * ``` * * @see {@link escapeToken} The inverse operation for encoding tokens diff --git a/.context/effect/packages/effect/src/JsonSchema.ts b/.context/effect/packages/effect/src/JsonSchema.ts index 7def9d323..5fb6c0c30 100644 --- a/.context/effect/packages/effect/src/JsonSchema.ts +++ b/.context/effect/packages/effect/src/JsonSchema.ts @@ -2,13 +2,14 @@ * Helpers for normalizing and converting JSON Schema and OpenAPI schema * documents. Supported inputs include JSON Schema Draft-07, Draft 2020-12, * OpenAPI 3.0, and OpenAPI 3.1; conversions normalize through - * `Document<"draft-2020-12">` before emitting another dialect. The module also - * defines document types, meta-schema constants, OpenAPI component-key helpers, - * and `$ref` resolution utilities. + * `Document<"draft-2020-12">` before emitting another dialect, including + * JSON Schema Draft-04. The module also defines document types, meta-schema + * constants, OpenAPI component-key helpers, and `$ref` resolution utilities. * * @since 4.0.0 */ import * as Arr from "./Array.ts" +import * as InternalRecord from "./internal/record.ts" import { unescapeToken } from "./JsonPointer.ts" import * as Predicate from "./Predicate.ts" import * as Rec from "./Record.ts" @@ -42,9 +43,10 @@ export interface JsonSchema { * * **Details** * - * Supported values are `"draft-07"` for JSON Schema Draft-07, - * `"draft-2020-12"` for JSON Schema Draft 2020-12 and the canonical internal - * form, `"openapi-3.1"` for OpenAPI 3.1, and `"openapi-3.0"` for OpenAPI 3.0. + * Supported values are `"draft-04"` for JSON Schema Draft-04, `"draft-07"` + * for JSON Schema Draft-07, `"draft-2020-12"` for JSON Schema Draft 2020-12 + * and the canonical internal form, `"openapi-3.1"` for OpenAPI 3.1, and + * `"openapi-3.0"` for OpenAPI 3.0. * * @see {@link Document} for a single root schema tagged with a dialect * @see {@link MultiDocument} for multiple root schemas tagged with a dialect @@ -52,7 +54,7 @@ export interface JsonSchema { * @category models * @since 4.0.0 */ -export type Dialect = "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0" +export type Dialect = "draft-04" | "draft-07" | "draft-2020-12" | "openapi-3.1" | "openapi-3.0" /** * The JSON Schema primitive type names. @@ -103,12 +105,12 @@ export interface Definitions extends Record {} * The `schema` field holds the root schema *without* the definitions * collection. Root definitions are stored separately in `definitions` and * referenced via `#/$defs/` for Draft-2020-12, `#/definitions/` - * for Draft-07, and `#/components/schemas/` for OpenAPI 3.1 and - * OpenAPI 3.0. + * for Draft-04 and Draft-07, and `#/components/schemas/` for OpenAPI 3.1 + * and OpenAPI 3.0. * * **Example** (Inspecting a parsed document) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const raw: JsonSchema.JsonSchema = { @@ -118,9 +120,9 @@ export interface Definitions extends Record {} * * const doc = JsonSchema.fromSchemaDraft2020_12(raw) * - * console.log(doc.dialect) // "draft-2020-12" - * console.log(doc.schema) // { type: "string" } - * console.log(doc.definitions) // { Trimmed: { type: "string", minLength: 1 } } + * doc.dialect // => "draft-2020-12" + * doc.schema // => { type: "string" } + * doc.definitions // => { Trimmed: { type: "string", minLength: 1 } } * ``` * * @see {@link MultiDocument} @@ -158,6 +160,20 @@ export interface MultiDocument { readonly definitions: Definitions } +/** + * Represents the `$schema` meta-schema URI for JSON Schema Draft-04. + * + * **When to use** + * + * Use when constructing a Draft-04 JSON Schema document and you need a stable + * value for the root `$schema` field. + * + * @see {@link META_SCHEMA_URI_DRAFT_07} for the Draft-07 `$schema` URI + * @category constants + * @since 4.0.0 + */ +export const META_SCHEMA_URI_DRAFT_04 = "http://json-schema.org/draft-04/schema#" + /** * Represents the `$schema` meta-schema URI for JSON Schema Draft-07. * @@ -169,14 +185,15 @@ export interface MultiDocument { * **Details** * * The exported value is the literal string - * `http://json-schema.org/draft-07/schema`. + * `http://json-schema.org/draft-07/schema#`. * + * @see {@link META_SCHEMA_URI_DRAFT_04} for the Draft-04 `$schema` URI * @see {@link META_SCHEMA_URI_DRAFT_2020_12} for the Draft 2020-12 `$schema` URI * * @category constants * @since 4.0.0 */ -export const META_SCHEMA_URI_DRAFT_07 = "http://json-schema.org/draft-07/schema" +export const META_SCHEMA_URI_DRAFT_07 = "http://json-schema.org/draft-07/schema#" /** * Represents the `$schema` meta-schema URI for JSON Schema Draft 2020-12. @@ -202,6 +219,44 @@ const RE_DEFINITIONS = /^#\/definitions(?=\/|$)/ const RE_DEFS = /^#\/\$defs(?=\/|$)/ const RE_COMPONENTS_SCHEMAS = /^#\/components\/schemas(?=\/|$)/ +const DRAFT_04_COPY_KEYWORDS = new Set([ + "$ref", + "type", + "required", + "enum", + "title", + "description", + "default", + "format", + "pattern", + "minLength", + "maxLength", + "minItems", + "maxItems", + "minProperties", + "maxProperties", + "multipleOf", + "uniqueItems" +]) + +const DRAFT_07_COPY_KEYWORDS = new Set([ + ...DRAFT_04_COPY_KEYWORDS, + "const", + "examples", + "readOnly", + "writeOnly", + "minimum", + "maximum", + "exclusiveMinimum", + "exclusiveMaximum" +]) + +const DRAFT_04_SINGLE_SUBSCHEMA_KEYWORDS = new Set(["not"]) +const DRAFT_07_SINGLE_SUBSCHEMA_KEYWORDS = new Set(["not", "additionalProperties", "propertyNames"]) + +const MAP_SUBSCHEMA_KEYWORDS = new Set(["properties", "patternProperties"]) +const ARRAY_SUBSCHEMA_KEYWORDS = new Set(["allOf", "anyOf", "oneOf"]) + /** * Parses a raw Draft-07 JSON Schema into a `Document<"draft-2020-12">`. * @@ -223,7 +278,7 @@ const RE_COMPONENTS_SCHEMAS = /^#\/components\/schemas(?=\/|$)/ * * **Example** (Parsing a Draft-07 schema) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const raw: JsonSchema.JsonSchema = { @@ -237,8 +292,8 @@ const RE_COMPONENTS_SCHEMAS = /^#\/components\/schemas(?=\/|$)/ * } * * const doc = JsonSchema.fromSchemaDraft07(raw) - * console.log(doc.dialect) // "draft-2020-12" - * console.log(doc.schema.properties) // { tags: { type: "array", items: { type: "string" } } } + * doc.dialect // => "draft-2020-12" + * doc.schema.properties // => { tags: { type: "array", items: { type: "string" } } } * ``` * * @see {@link fromSchemaDraft2020_12} @@ -258,7 +313,7 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { } function walk(node: unknown, isRoot: boolean): unknown { - if (Array.isArray(node)) return node.map((v) => walk(v, false)) + if (Array.isArray(node)) return node.map(walkNested) if (!Predicate.isObject(node)) return node const out: Record = {} @@ -269,13 +324,19 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { for (const k of Object.keys(node)) { const v = node[k] - switch (k) { - case "$ref": - out.$ref = typeof v === "string" ? v.replace(RE_DEFINITIONS, "#/$defs") : v - break + if (k === "$ref") { + out.$ref = typeof v === "string" ? v.replace(RE_DEFINITIONS, "#/$defs") : v + continue + } + if (DRAFT_07_COPY_KEYWORDS.has(k)) { + out[k] = v + continue + } + if (rewriteSubschemaKeyword(out, k, v, walkNested, DRAFT_07_SINGLE_SUBSCHEMA_KEYWORDS)) continue + switch (k) { case "definitions": { - const mapped = walk_object(v, walk) + const mapped = mapObject(v, walkNested) if (isRoot) { definitions = mapped as Definitions | undefined } else { @@ -291,51 +352,6 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { additionalItems = v break - case "properties": - case "patternProperties": { - const mapped = walk_object(v, walk) - out[k] = mapped ?? v - break - } - - case "additionalProperties": - case "propertyNames": - out[k] = walk(v, false) - break - - case "allOf": - case "anyOf": - case "oneOf": - out[k] = Array.isArray(v) ? v.map((x) => walk(x, false)) : v - break - - case "type": - case "required": - case "enum": - case "const": - case "title": - case "description": - case "default": - case "examples": - case "format": - case "readOnly": - case "writeOnly": - case "pattern": - case "minimum": - case "maximum": - case "exclusiveMinimum": - case "exclusiveMaximum": - case "minLength": - case "maxLength": - case "minItems": - case "maxItems": - case "minProperties": - case "maxProperties": - case "multipleOf": - case "uniqueItems": - out[k] = v - break - default: break } @@ -344,15 +360,19 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { // Draft-07 tuples -> 2020-12 tuples if (prefixItems !== undefined) { if (Array.isArray(prefixItems)) { - out.prefixItems = prefixItems.map((x) => walk(x, false)) - if (additionalItems !== undefined) out.items = walk(additionalItems, false) + out.prefixItems = prefixItems.map(walkNested) + if (additionalItems !== undefined) out.items = walkNested(additionalItems) } else { - out.items = walk(prefixItems, false) + out.items = walkNested(prefixItems) } } return out } + + function walkNested(node: unknown): unknown { + return walk(node, false) + } } /** @@ -369,7 +389,7 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { * * **Example** (Parsing a Draft-2020-12 schema) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const raw: JsonSchema.JsonSchema = { @@ -379,8 +399,8 @@ export function fromSchemaDraft07(js: JsonSchema): Document<"draft-2020-12"> { * } * * const doc = JsonSchema.fromSchemaDraft2020_12(raw) - * console.log(doc.schema) // { type: "number", minimum: 0 } - * console.log(doc.definitions) // { PositiveInt: { type: "integer", minimum: 1 } } + * doc.schema // => { type: "number", minimum: 0 } + * doc.definitions // => { PositiveInt: { type: "integer", minimum: 1 } } * ``` * * @see {@link fromSchemaDraft07} @@ -412,7 +432,7 @@ export function fromSchemaDraft2020_12(js: JsonSchema): Document<"draft-2020-12" * * **Example** (Parsing an OpenAPI 3.1 schema) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const raw: JsonSchema.JsonSchema = { @@ -423,8 +443,7 @@ export function fromSchemaDraft2020_12(js: JsonSchema): Document<"draft-2020-12" * } * * const doc = JsonSchema.fromSchemaOpenApi3_1(raw) - * // $ref is rewritten to Draft-2020-12 form - * console.log(doc.schema.properties) // { user: { $ref: "#/$defs/User" } } + * doc.schema.properties // => { user: { $ref: "#/$defs/User" } } * ``` * * @see {@link fromSchemaOpenApi3_0} @@ -433,7 +452,7 @@ export function fromSchemaDraft2020_12(js: JsonSchema): Document<"draft-2020-12" * @since 4.0.0 */ export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> { - const schema = rewrite_refs(js, (ref) => ref.replace(RE_COMPONENTS_SCHEMAS, "#/$defs")) as JsonSchema + const schema = rewriteRefs(js, (ref) => ref.replace(RE_COMPONENTS_SCHEMAS, "#/$defs")) return fromSchemaDraft2020_12(schema) } @@ -454,7 +473,7 @@ export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> * * **Example** (Parsing an OpenAPI 3.0 nullable schema) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const raw: JsonSchema.JsonSchema = { @@ -463,8 +482,7 @@ export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> * } * * const doc = JsonSchema.fromSchemaOpenApi3_0(raw) - * // nullable is expanded into a type array - * console.log(doc.schema.type) // ["string", "null"] + * doc.schema.type // => ["string", "null"] * ``` * * @see {@link fromSchemaOpenApi3_1} @@ -473,7 +491,7 @@ export function fromSchemaOpenApi3_1(js: JsonSchema): Document<"draft-2020-12"> * @since 4.0.0 */ export function fromSchemaOpenApi3_0(schema: JsonSchema): Document<"draft-2020-12"> { - const normalized = normalize_OpenApi3_0_to_Draft07(schema) + const normalized = normalizeOpenApi3_0ToDraft07(schema) return fromSchemaDraft07(normalized as JsonSchema) } @@ -498,7 +516,7 @@ export function fromSchemaOpenApi3_0(schema: JsonSchema): Document<"draft-2020-1 * * **Example** (Converting to Draft-07) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const doc = JsonSchema.fromSchemaDraft2020_12({ @@ -508,12 +526,13 @@ export function fromSchemaOpenApi3_0(schema: JsonSchema): Document<"draft-2020-1 * }) * * const draft07 = JsonSchema.toDocumentDraft07(doc) - * console.log(draft07.dialect) // "draft-07" - * console.log(draft07.schema.items) // [{ type: "string" }, { type: "number" }] - * console.log(draft07.schema.additionalItems) // { type: "boolean" } + * draft07.dialect // => "draft-07" + * draft07.schema.items // => [{ type: "string" }, { type: "number" }] + * draft07.schema.additionalItems // => { type: "boolean" } * ``` * * @see {@link fromSchemaDraft07} + * @see {@link toDocumentDraft04} for converting to Draft-04 * @see {@link toMultiDocumentOpenApi3_1} * @category encoding * @since 4.0.0 @@ -526,75 +545,165 @@ export function toDocumentDraft07(document: Document<"draft-2020-12">): Document } } -function toSchemaDraft07(schema: JsonSchema): JsonSchema { - return rewrite(schema) - - function rewrite(node: unknown): JsonSchema { - return walk(rewrite_refs(node, (ref) => ref.replace(RE_DEFS, "#/definitions")), true) as JsonSchema +/** + * Converts a `Document<"draft-2020-12">` to a `Document<"draft-04">`. + * + * **When to use** + * + * Use when you need to output a canonical JSON Schema document in Draft-04 + * format. + * + * **Details** + * + * This rewrites `#/$defs/...` refs to `#/definitions/...`, converts tuple + * syntax, lowers `const` to `enum`, converts numeric exclusive bounds to the + * Draft-04 boolean form, and converts both the root schema and all definitions. + * + * **Gotchas** + * + * Unsupported Draft-2020-12 and Draft-07 keywords are dropped. For example, + * `propertyNames` has no general Draft-04 equivalent and is omitted. + * + * **Example** (Converting exclusive bounds) + * + * ```ts import.meta.vitest + * import { JsonSchema } from "effect" + * + * const doc = JsonSchema.fromSchemaDraft2020_12({ + * type: "number", + * exclusiveMinimum: 0 + * }) + * + * JsonSchema.toDocumentDraft04(doc).schema // => { type: "number", minimum: 0, exclusiveMinimum: true } + * ``` + * + * @see {@link toDocumentDraft07} for converting to Draft-07 + * @category encoding + * @since 4.0.0 + */ +export function toDocumentDraft04(document: Document<"draft-2020-12">): Document<"draft-04"> { + const draft07 = toDocumentDraft07(document) + return { + dialect: "draft-04", + schema: toSchemaDraft04(draft07.schema), + definitions: Rec.map(draft07.definitions, toSchemaDraft04) } +} + +function toSchemaDraft04(schema: JsonSchema): JsonSchema { + return walk(schema) as JsonSchema - function walk(node: unknown, _isRoot: boolean): unknown { - if (Array.isArray(node)) return node.map((v) => walk(v, false)) + function walk(node: unknown): unknown { + if (node === true) return {} + if (node === false) return { not: {} } + if (Array.isArray(node)) return node.map(walk) if (!Predicate.isObject(node)) return node const src = node as Record const out: Record = {} - let prefixItems: unknown = undefined - let items: unknown = undefined + let hasConst = false + let constValue: unknown = undefined for (const k of Object.keys(src)) { const v = src[k] + if (DRAFT_04_COPY_KEYWORDS.has(k)) { + out[k] = v + continue + } + if (rewriteSubschemaKeyword(out, k, v, walk, DRAFT_04_SINGLE_SUBSCHEMA_KEYWORDS)) continue + switch (k) { - // We already rewrote $ref via rewrite_refs, so just copy it through. - case "$ref": - case "type": - case "required": - case "enum": case "const": - case "title": - case "description": - case "default": - case "examples": - case "format": - case "pattern": + hasConst = true + constValue = v + break + case "minimum": case "maximum": case "exclusiveMinimum": case "exclusiveMaximum": - case "minLength": - case "maxLength": - case "minItems": - case "maxItems": - case "minProperties": - case "maxProperties": - case "multipleOf": - case "uniqueItems": - out[k] = v break - // Schema maps - case "properties": - case "patternProperties": { - const mapped = walk_object(v, walk) - out[k] = mapped ?? v + case "additionalProperties": + case "additionalItems": + out[k] = typeof v === "boolean" ? v : walk(v) break - } - // Single subschemas - case "additionalProperties": - case "propertyNames": - out[k] = walk(v, false) + case "items": + out.items = Array.isArray(v) ? v.map(walk) : walk(v) break - // Schema arrays - case "allOf": - case "anyOf": - case "oneOf": - out[k] = Array.isArray(v) ? v.map((x) => walk(x, false)) : v + default: break + } + } + + convertExclusiveBound(src, out, "minimum", "exclusiveMinimum", (bound, exclusive) => bound > exclusive) + convertExclusiveBound(src, out, "maximum", "exclusiveMaximum", (bound, exclusive) => bound < exclusive) + + if (hasConst) { + const constSchema = { enum: [constValue] } + if (Object.hasOwn(src, "enum")) { + out.allOf = Array.isArray(out.allOf) ? [...out.allOf, constSchema] : [constSchema] + } else { + out.enum = constSchema.enum + } + } + + return out + } +} + +function convertExclusiveBound( + src: Record, + out: Record, + boundKey: "minimum" | "maximum", + exclusiveKey: "exclusiveMinimum" | "exclusiveMaximum", + isBoundStricter: (bound: number, exclusive: number) => boolean +): void { + const bound = src[boundKey] + const exclusive = src[exclusiveKey] + + if (typeof exclusive === "number") { + if (typeof bound === "number" && isBoundStricter(bound, exclusive)) { + out[boundKey] = bound + } else { + out[boundKey] = exclusive + out[exclusiveKey] = true + } + } else if (bound !== undefined) { + out[boundKey] = bound + } +} + +function toSchemaDraft07(schema: JsonSchema): JsonSchema { + return transformSchema(schema, (src) => { + rewriteSchemaRef(src, (ref) => ref.replace(RE_DEFS, "#/definitions")) + const out: Record = {} + + let prefixItems: unknown = undefined + let items: unknown = undefined + + for (const k of Object.keys(src)) { + const v = src[k] + if (k === "required" && Array.isArray(v) && v.length === 0) continue + if (DRAFT_07_COPY_KEYWORDS.has(k)) { + out[k] = v + continue + } + if ( + MAP_SUBSCHEMA_KEYWORDS.has(k) || + ARRAY_SUBSCHEMA_KEYWORDS.has(k) || + DRAFT_07_SINGLE_SUBSCHEMA_KEYWORDS.has(k) + ) { + out[k] = v + continue + } + + switch (k) { // Tuple handling (2020-12 form) case "prefixItems": prefixItems = v @@ -612,19 +721,25 @@ function toSchemaDraft07(schema: JsonSchema): JsonSchema { // 2020-12 tuples -> Draft-07 tuples if (prefixItems !== undefined) { if (Array.isArray(prefixItems)) { - out.items = prefixItems.map((x) => walk(x, false)) - if (items !== undefined) out.additionalItems = walk(items, false) + out.items = prefixItems + if (items !== undefined) out.additionalItems = items } else { // Non-standard, but keep a reasonable behavior - out.items = walk(prefixItems, false) + out.items = prefixItems } } else if (items !== undefined) { // Regular items schema stays as items - out.items = walk(items, false) + out.items = items + } + + const $ref = out.$ref + if (typeof $ref === "string" && Object.keys(out).length > 1) { + delete out.$ref + out.allOf = [{ $ref }, ...(Array.isArray(out.allOf) ? out.allOf : [])] } return out - } + }) as JsonSchema } /** @@ -638,15 +753,21 @@ function toSchemaDraft07(schema: JsonSchema): JsonSchema { * * **Details** * - * This rewrites `#/$defs/...` refs to `#/components/schemas/...`, sanitizes - * definition keys to match the OpenAPI component key pattern - * (`^[a-zA-Z0-9.\-_]+$`) by replacing invalid characters with `_`, updates all - * `$ref` pointers to use the sanitized keys, and converts all schemas and - * definitions in the multi-document. + * This rewrites local `#/$defs/...` refs to `#/components/schemas/...` and + * sanitizes definition keys to match the OpenAPI component key pattern + * (`^[a-zA-Z0-9.\-_]+$`) by replacing invalid characters with `_`. Valid keys + * are preserved. When sanitized keys collide, the converter appends the first + * available `_1`, `_2`, and subsequent suffix, with allocation independent of + * definition insertion order. All local refs are updated to use the allocated + * keys, including refs to paths within a definition. + * + * **Gotchas** + * + * External refs and local refs outside `#/$defs` are left unchanged. * * **Example** (Converting to OpenAPI 3.1) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const multi: JsonSchema.MultiDocument<"draft-2020-12"> = { @@ -658,8 +779,8 @@ function toSchemaDraft07(schema: JsonSchema): JsonSchema { * } * * const openapi = JsonSchema.toMultiDocumentOpenApi3_1(multi) - * console.log(openapi.dialect) // "openapi-3.1" - * console.log(openapi.schemas[0]) // { $ref: "#/components/schemas/User" } + * openapi.dialect // => "openapi-3.1" + * openapi.schemas[0] // => { $ref: "#/components/schemas/User" } * ``` * * @see {@link toDocumentDraft07} @@ -668,26 +789,39 @@ function toSchemaDraft07(schema: JsonSchema): JsonSchema { * @since 4.0.0 */ export function toMultiDocumentOpenApi3_1(multiDocument: MultiDocument<"draft-2020-12">): MultiDocument<"openapi-3.1"> { + const definitionKeys = Object.keys(multiDocument.definitions) const keyMap = new Map() - for (const key of Object.keys(multiDocument.definitions)) { - const sanitized = sanitizeOpenApiComponentsSchemasKey(key) - if (sanitized !== key) { - keyMap.set(key, sanitized) - } + const usedKeys = new Set(definitionKeys.filter((key) => VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(key))) + const invalidKeys = definitionKeys + .filter((key) => !VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(key)) + .sort() + .map((key) => [key, sanitizeOpenApiComponentsSchemasKey(key)] as const) + for (const [key, base] of invalidKeys) { + if (usedKeys.has(base)) continue + usedKeys.add(base) + keyMap.set(key, base) + } + for (const [key, base] of invalidKeys) { + if (keyMap.has(key)) continue + let candidate: string + let suffix = 0 + do candidate = `${base}_${++suffix}` + while (usedKeys.has(candidate)) + usedKeys.add(candidate) + keyMap.set(key, candidate) } function rewrite(schema: JsonSchema): JsonSchema { - return rewrite_refs(schema, ($ref) => { - const tokens = $ref.split("/") - if (tokens.length > 0) { - const identifier = unescapeToken(tokens[tokens.length - 1]) - const sanitized = keyMap.get(identifier) - if (sanitized !== undefined) { - $ref = tokens.slice(0, -1).join("/") + "/" + sanitized - } - } - return $ref.replace(RE_DEFS, "#/components/schemas") - }) as JsonSchema + return rewriteRefs(schema, ($ref) => { + if (!$ref.startsWith("#/$defs/")) return $ref + + const path = $ref.slice("#/$defs/".length) + const separatorIndex = path.indexOf("/") + const token = separatorIndex === -1 ? path : path.slice(0, separatorIndex) + const rest = separatorIndex === -1 ? "" : path.slice(separatorIndex) + const key = keyMap.get(unescapeToken(token)) ?? token + return `#/components/schemas/${key}${rest}` + }) } return { @@ -710,64 +844,95 @@ export const VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP = /^[a-zA-Z0-9.\-_]+$/ * @internal */ export function sanitizeOpenApiComponentsSchemasKey(s: string): string { - if (s.length === 0) return "_" - if (VALID_OPEN_API_COMPONENTS_SCHEMAS_KEY_REGEXP.test(s)) return s - - const out: Array = [] - - for (const ch of s) { - const code = ch.codePointAt(0) - if ( - code !== undefined && - ((code >= 48 && code <= 57) || // 0-9 - (code >= 65 && code <= 90) || // A-Z - (code >= 97 && code <= 122) || // a-z - code === 46 || // . - code === 45 || // - - code === 95) // _ - ) { - out.push(ch) - } else { - out.push("_") - } - } - - return out.join("") + return s.length === 0 ? "_" : s.replace(/[^a-zA-Z0-9._-]/gu, "_") } -function rewrite_refs(node: unknown, f: ($ref: string) => string): unknown { - if (Array.isArray(node)) return node.map((v) => rewrite_refs(v, f)) - if (!Predicate.isObject(node)) return node - - const out: Record = {} +function transformSchema( + node: unknown, + transform: (schema: Record) => Record +): unknown { + return walk(node) - for (const k of Object.keys(node)) { - const v = node[k] + function walk(node: unknown): unknown { + if (!Predicate.isObject(node)) return node - if (k === "$ref") { - out[k] = typeof v === "string" ? f(v) : v - } else if (Array.isArray(v) || Predicate.isObject(v)) { - out[k] = rewrite_refs(v, f) - } else { - out[k] = v + const out: Record = {} + for (const key of Object.keys(node)) { + const value = node[key] + let transformed = value + switch (key) { + case "$defs": + case "properties": + case "patternProperties": + case "dependentSchemas": + transformed = mapObject(value, walk) ?? value + break + case "allOf": + case "anyOf": + case "oneOf": + case "prefixItems": + transformed = Array.isArray(value) ? value.map(walk) : value + break + case "not": + case "additionalProperties": + case "propertyNames": + case "unevaluatedProperties": + case "items": + case "contains": + case "unevaluatedItems": + case "if": + case "then": + case "else": + case "contentSchema": + transformed = walk(value) + } + InternalRecord.assignProperty(out, key, transformed) } + return transform(out) + } +} + +/** @internal */ +export function rewriteRefs(schema: JsonSchema, rewrite: ($ref: string) => string): JsonSchema { + return transformSchema(schema, (schema) => rewriteSchemaRef(schema, rewrite)) as JsonSchema +} + +function rewriteSchemaRef( + schema: Record, + rewrite: ($ref: string) => string +): Record { + if (typeof schema.$ref === "string") { + InternalRecord.assignProperty(schema, "$ref", rewrite(schema.$ref)) } + return schema +} - return out +function mapObject(value: unknown, f: (node: unknown) => unknown): Record | undefined { + return Predicate.isObject(value) ? Rec.map(value, f) : undefined } -function walk_object( +function rewriteSubschemaKeyword( + out: Record, + key: string, value: unknown, - walk: (node: unknown, isRoot: boolean) => unknown -): Record | undefined { - if (!Predicate.isObject(value)) return undefined - const out: Record = {} - for (const k of Object.keys(value)) out[k] = walk(value[k], false) - return out + rewrite: (node: unknown) => unknown, + singleKeywords: ReadonlySet +): boolean { + if (MAP_SUBSCHEMA_KEYWORDS.has(key)) { + out[key] = mapObject(value, rewrite) ?? value + return true + } + if (ARRAY_SUBSCHEMA_KEYWORDS.has(key)) { + out[key] = Array.isArray(value) ? value.map(rewrite) : value + return true + } + if (!singleKeywords.has(key)) return false + out[key] = rewrite(value) + return true } -function normalize_OpenApi3_0_to_Draft07(node: unknown): unknown { - if (Array.isArray(node)) return node.map(normalize_OpenApi3_0_to_Draft07) +function normalizeOpenApi3_0ToDraft07(node: unknown): unknown { + if (Array.isArray(node)) return node.map(normalizeOpenApi3_0ToDraft07) if (!Predicate.isObject(node)) return node const src = node as Record @@ -776,67 +941,67 @@ function normalize_OpenApi3_0_to_Draft07(node: unknown): unknown { for (const k of Object.keys(src)) { const v = src[k] if (k === "$ref" && typeof v === "string") { - out[k] = v.replace(RE_COMPONENTS_SCHEMAS, "#/definitions") + InternalRecord.assignProperty(out, k, v.replace(RE_COMPONENTS_SCHEMAS, "#/definitions")) } else if (k === "example") { if (src.examples === undefined) { out.examples = [v] } } else if (Array.isArray(v) || Predicate.isObject(v)) { - out[k] = normalize_OpenApi3_0_to_Draft07(v) + InternalRecord.assignProperty(out, k, normalizeOpenApi3_0ToDraft07(v)) } else { - out[k] = v + InternalRecord.assignProperty(out, k, v) } } // Draft-04-style numeric exclusivity booleans - out = adjust_exclusivity(out) + out = adjustExclusivity(out) // OpenAPI 3.0 nullable if (out.nullable === true) { - out = apply_nullable(out) + out = applyNullable(out) } delete out.nullable return out } -function adjust_exclusivity(node: Record): Record { - let out = node - - if (typeof out.exclusiveMinimum === "boolean") { - if (out.exclusiveMinimum === true && typeof out.minimum === "number") { - out = { ...out, exclusiveMinimum: out.minimum } - delete out.minimum - } else { - out = { ...out } - delete out.exclusiveMinimum - } - } +function adjustExclusivity(node: Record): Record { + return adjustExclusiveBound( + adjustExclusiveBound(node, "minimum", "exclusiveMinimum"), + "maximum", + "exclusiveMaximum" + ) +} - if (typeof out.exclusiveMaximum === "boolean") { - if (out.exclusiveMaximum === true && typeof out.maximum === "number") { - out = { ...out, exclusiveMaximum: out.maximum } - delete out.maximum - } else { - out = { ...out } - delete out.exclusiveMaximum - } +function adjustExclusiveBound( + node: Record, + boundKey: "minimum" | "maximum", + exclusiveKey: "exclusiveMinimum" | "exclusiveMaximum" +): Record { + const exclusive = node[exclusiveKey] + if (typeof exclusive !== "boolean") return node + + const out = { ...node } + if (exclusive && typeof node[boundKey] === "number") { + out[exclusiveKey] = node[boundKey] + delete out[boundKey] + } else { + delete out[exclusiveKey] } - return out } -function apply_nullable(node: Record): Record { +function applyNullable(node: Record): Record { // enum widening if (Array.isArray(node.enum)) { - return widen_type({ + return widenType({ ...node, enum: node.enum.includes(null) ? node.enum : [...node.enum, null] }) } // type widening - if (node.type !== undefined) return widen_type(node) + if (node.type !== undefined) return widenType(node) // const === null if (node.const === null) return node @@ -845,7 +1010,7 @@ function apply_nullable(node: Record): Record return { anyOf: [node, { type: "null" }] } } -function widen_type(node: Record): Record { +function widenType(node: Record): Record { const t = node.type if (typeof t === "string") return t === "null" ? node : { ...node, type: [t, "null"] } if (Array.isArray(t)) return t.includes("null") ? node : { ...node, type: [...t, "null"] } @@ -872,18 +1037,15 @@ function widen_type(node: Record): Record { * * **Example** (Resolving a $ref) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const definitions: JsonSchema.Definitions = { * User: { type: "object", properties: { name: { type: "string" } } } * } * - * const result = JsonSchema.resolve$ref("#/$defs/User", definitions) - * console.log(result) // { type: "object", properties: { name: { type: "string" } } } - * - * const missing = JsonSchema.resolve$ref("#/$defs/Unknown", definitions) - * console.log(missing) // undefined + * JsonSchema.resolve$ref("#/$defs/User", definitions) // => { type: "object", properties: { name: { type: "string" } } } + * JsonSchema.resolve$ref("#/$defs/Unknown", definitions) // => undefined * ``` * * @see {@link resolveTopLevel$ref} @@ -893,13 +1055,8 @@ function widen_type(node: Record): Record { */ export function resolve$ref($ref: string, definitions: Definitions): JsonSchema | undefined { const tokens = $ref.split("/") - if (tokens.length > 0) { - const identifier = unescapeToken(tokens[tokens.length - 1]) - const definition = definitions[identifier] - if (definition !== undefined) { - return definition - } - } + const identifier = unescapeToken(tokens[tokens.length - 1]) + if (Object.hasOwn(definitions, identifier)) return definitions[identifier] } /** @@ -917,7 +1074,7 @@ export function resolve$ref($ref: string, definitions: Definitions): JsonSchema * * **Example** (Resolving a top-level $ref) * - * ```ts + * ```ts import.meta.vitest * import { JsonSchema } from "effect" * * const doc: JsonSchema.Document<"draft-2020-12"> = { @@ -929,7 +1086,7 @@ export function resolve$ref($ref: string, definitions: Definitions): JsonSchema * } * * const resolved = JsonSchema.resolveTopLevel$ref(doc) - * console.log(resolved.schema) // { type: "object", properties: { name: { type: "string" } } } + * resolved.schema // => { type: "object", properties: { name: { type: "string" } } } * ``` * * @see {@link resolve$ref} diff --git a/.context/effect/packages/effect/src/Latch.ts b/.context/effect/packages/effect/src/Latch.ts index 418c78e44..62e73d736 100644 --- a/.context/effect/packages/effect/src/Latch.ts +++ b/.context/effect/packages/effect/src/Latch.ts @@ -28,18 +28,18 @@ import * as internal from "./internal/effect.ts" * * **Example** (Coordinating fibers with a latch) * - * ```ts - * import { Effect, Latch } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber, Latch } from "effect" * * // Create and use a latch for coordination between fibers * const program = Effect.gen(function*() { * const latch = yield* Latch.make() - * - * // Wait for the latch to be opened - * yield* latch.await - * - * return "Latch was opened!" + * const waiter = yield* Effect.forkChild(latch.await.pipe(Effect.as("opened"))) + * yield* latch.open + * return yield* Fiber.join(waiter) * }) + * + * await Effect.runPromise(program) // => "opened" * ``` * * @see {@link make} for creating a latch inside Effect code @@ -138,24 +138,19 @@ export interface Latch { * * **Example** (Creating a latch unsafely) * - * ```ts - * import { Effect, Latch } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber, Latch } from "effect" * * const latch = Latch.makeUnsafe(false) + * const waiter = latch.await.pipe(Effect.as("opened")) * - * const waiter = Effect.gen(function*() { - * yield* Effect.log("Waiting for latch to open...") - * yield* latch.await - * yield* Effect.log("Latch opened! Continuing...") - * }) - * - * const opener = Effect.gen(function*() { - * yield* Effect.sleep("2 seconds") - * yield* Effect.log("Opening latch...") + * const program = Effect.gen(function*() { + * const fiber = yield* Effect.forkChild(waiter) * yield* latch.open + * return yield* Fiber.join(fiber) * }) * - * const program = Effect.all([waiter, opener]) + * await Effect.runPromise(program) // => "opened" * ``` * * @see {@link make} for creating a latch inside Effect code @@ -178,26 +173,19 @@ export const makeUnsafe: (open?: boolean | undefined) => Latch = internal.makeLa * * **Example** (Creating a latch) * - * ```ts - * import { Effect, Latch } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber, Latch } from "effect" * * const program = Effect.gen(function*() { * const latch = yield* Latch.make(false) + * const waiter = latch.await.pipe(Effect.as("opened")) * - * const waiter = Effect.gen(function*() { - * yield* Effect.log("Waiting for latch to open...") - * yield* latch.await - * yield* Effect.log("Latch opened! Continuing...") - * }) - * - * const opener = Effect.gen(function*() { - * yield* Effect.sleep("2 seconds") - * yield* Effect.log("Opening latch...") - * yield* latch.open - * }) - * - * yield* Effect.all([waiter, opener]) + * const fiber = yield* Effect.forkChild(waiter) + * yield* latch.open + * return yield* Fiber.join(fiber) * }) + * + * await Effect.runPromise(program) // => "opened" * ``` * * @see {@link makeUnsafe} for synchronous allocation outside Effect code @@ -388,7 +376,7 @@ export const whenOpen: { * * Use to check the state of the latch without suspending or changing its state. * - * @category getters + * @category predicates * @since 4.0.0 */ export const isOpen = (self: Latch): boolean => self.isOpen() diff --git a/.context/effect/packages/effect/src/Layer.ts b/.context/effect/packages/effect/src/Layer.ts index 37e758e94..0d1eb01d8 100644 --- a/.context/effect/packages/effect/src/Layer.ts +++ b/.context/effect/packages/effect/src/Layer.ts @@ -192,7 +192,7 @@ const MemoMapTypeId = "~effect/Layer/MemoMap" * * **Example** (Sharing layer construction with a memo map) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service "result" * ``` * * @category models @@ -251,7 +254,7 @@ const memoMapReuse = ( * * **Example** (Checking whether a value is a layer) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service( * }) * const notALayer = { someProperty: "value" } * - * console.log(Layer.isLayer(dbLayer)) // true - * console.log(Layer.isLayer(notALayer)) // false + * Layer.isLayer(dbLayer) // => true + * Layer.isLayer(notALayer) // => false * ``` * - * @category getters + * @category guards * @since 2.0.0 */ export const isLayer = (u: unknown): u is Layer => hasProperty(u, TypeId) @@ -305,7 +308,7 @@ const fromBuildUnsafe = ( * * **Example** (Constructing a layer from a build function) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service( * }) * ) * ) + * + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, databaseLayer)) // => "result" * ``` * * @category constructors @@ -349,7 +355,7 @@ export const fromBuild = ( * * **Example** (Memoizing layer construction) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service( * }) * ) * ) + * + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, databaseLayer)) // => "result" * ``` * * @category constructors @@ -438,11 +447,13 @@ class MemoMapImpl implements MemoMap { scope: Scope.Scope, build: (memoMap: MemoMap, scope: Scope.Scope) => Effect, E, RIn> ): Effect, E, RIn> { - const existing = this.get(layer, scope) - if (existing) { - return existing - } - return memoMapBuild(this, layer, scope, build) + return internalEffect.suspend(() => { + const existing = this.get(layer, scope) + if (existing) { + return existing + } + return memoMapBuild(this, layer, scope, build) + }) } } @@ -451,7 +462,7 @@ class MemoMapImpl implements MemoMap { * * **Example** (Creating a memo map unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service "result" * ``` * - * @category memo map + * @category constructors * @since 4.0.0 */ export const makeMemoMapUnsafe = (): MemoMap => new MemoMapImpl() @@ -491,7 +505,7 @@ export const makeMemoMapUnsafe = (): MemoMap => new MemoMapImpl() * @see {@link forkMemoMap} for allocating the child memo map inside `Effect` * @see {@link makeMemoMapUnsafe} for creating a root memo map without a parent * - * @category memo map + * @category constructors * @since 4.0.0 */ export const forkMemoMapUnsafe = (parent: MemoMap): MemoMap => new MemoMapImpl(parent) @@ -501,7 +515,7 @@ export const forkMemoMapUnsafe = (parent: MemoMap): MemoMap => new MemoMapImpl(p * * **Example** (Creating a memo map in an effect) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service new MemoMapImpl(p * * return Context.get(context, Database) * }) + * + * const database = Effect.runSync(Effect.scoped(program)) + * Effect.runSync(database.query("SELECT 1")) // => "result" * ``` * - * @category memo map + * @category constructors * @since 2.0.0 */ export const makeMemoMap: Effect = internalEffect.sync(makeMemoMapUnsafe) @@ -541,7 +558,7 @@ export const makeMemoMap: Effect = internalEffect.sync(makeMemoMapUnsaf * @see {@link forkMemoMapUnsafe} for the synchronous constructor variant * @see {@link buildWithMemoMap} for building layers with an explicit memo map * - * @category memo map + * @category constructors * @since 4.0.0 */ export const forkMemoMap = (parent: MemoMap): Effect => internalEffect.sync(() => forkMemoMapUnsafe(parent)) @@ -561,7 +578,7 @@ export const forkMemoMap = (parent: MemoMap): Effect => internalEffect. * * @see {@link MemoMap} the memoization map type wrapped by this service * - * @category models + * @category services * @since 3.13.0 */ export class CurrentMemoMap extends Context.Service()("effect/Layer/CurrentMemoMap") { @@ -577,7 +594,7 @@ export class CurrentMemoMap extends Context.Service()(" * * **Example** (Building layers with an explicit memo map) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service()(" * readonly log: (msg: string) => Effect.Effect * }>()("Logger") {} * + * const logs: Array = [] + * * // Build layers with explicit memoization control * const program = Effect.gen(function*() { * const memoMap = yield* Layer.makeMemoMap @@ -601,7 +620,7 @@ export class CurrentMemoMap extends Context.Service()(" * * // Build logger layer with same memoization (reuses memo if same layer) * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(msg))) * }) * const loggerContext = yield* Layer.buildWithMemoMap( * loggerLayer, @@ -614,9 +633,13 @@ export class CurrentMemoMap extends Context.Service()(" * logger: Context.get(loggerContext, Logger) * } * }) + * + * const services = Effect.runSync(Effect.scoped(program)) + * Effect.runSync(services.logger.log("ready")) + * logs // => ["ready"] * ``` * - * @category memo map + * @category destructors * @since 2.0.0 */ export const buildWithMemoMap: { @@ -645,7 +668,7 @@ export const buildWithMemoMap: { * * **Example** (Building a layer into a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service "result" * ``` * * @category destructors @@ -697,22 +722,24 @@ export const build = ( * * **Example** (Building a layer with an explicit scope) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, Scope } from "effect" * * class Database extends Context.Service Effect.Effect * }>()("Database") {} * + * const logs: Array = [] + * * // Build a layer with explicit scope control * const program = Effect.gen(function*() { * const scope = yield* Effect.scope * * const dbLayer = Layer.effect(Database, Effect.gen(function*() { - * console.log("Initializing database...") + * logs.push("Initializing database...") * yield* Scope.addFinalizer( * scope, - * Effect.sync(() => console.log("Database closed")) + * Effect.sync(() => logs.push("Database closed")) * ) * return { query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) } * })) @@ -724,6 +751,9 @@ export const build = ( * return yield* database.query("SELECT * FROM users") * // Database will be closed when scope is closed * }) + * + * Effect.runSync(Effect.scoped(program)) // => "Result: SELECT * FROM users" + * logs // => ["Initializing database...", "Database closed"] * ``` * * @category destructors @@ -755,7 +785,7 @@ export const buildWithScope: { * * **Example** (Creating a layer from a service implementation) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service Effect.succeed(`Query result: ${sql}`)) * }) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, DatabaseLive)) // => "Query result: SELECT 1" * ``` * * @see {@link sync} for constructing layers from lazy values @@ -798,7 +830,7 @@ export const succeed: { * * **Example** (Providing multiple services from a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service Effect.Effect * }>()("Logger") {} * + * const logs: Array = [] * const context = Context.make(Database, { * query: Effect.fn("Database.query")((sql: string) => Effect.succeed("result")) * }).pipe( * Context.add(Logger, { - * log: (msg: string) => Effect.sync(() => console.log(msg)) + * log: (msg: string) => Effect.sync(() => logs.push(msg)) * }) * ) * * const layer = Layer.succeedContext(context) + * const program = Logger.use((logger) => logger.log("ready")) + * Effect.runSync(Effect.provide(program, layer)) + * logs // => ["ready"] * ``` * * @see {@link succeed} for providing a single service from a value @@ -838,14 +874,12 @@ export const succeedContext = (context: Context.Context): Layer => * * **Example** (Disabling optional lifecycle work) * - * ```ts - * import { Console, Layer } from "effect" - * - * declare const flag: boolean + * ```ts import.meta.vitest + * import { Context, Effect, Layer, Option } from "effect" * - * const StartupLogLive = flag - * ? Layer.effectDiscard(Console.log("application starting")) - * : Layer.empty + * const Service = Context.Service("Service") + * const context = Effect.runSync(Effect.scoped(Layer.build(Layer.empty))) + * Context.getOption(context, Service) // => Option.none() * ``` * * @see {@link effectDiscard} for running an effect while providing no services @@ -870,7 +904,7 @@ export const empty: Layer = succeedContext(Context.empty()) * * **Example** (Lazily providing a service) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service = succeedContext(Context.empty()) * const layer = Layer.sync(Database, () => ({ * query: (sql: string) => Effect.succeed(`Query: ${sql}`) * })) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, layer)) // => "Query: SELECT 1" * ``` * * @see {@link succeed} for constructing layers from static values @@ -912,7 +948,7 @@ export const sync: { * * **Example** (Lazily providing a context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service Effect.succeed(`Query: ${sql}`) * }) * ) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, layer)) // => "Query: SELECT 1" * ``` * * @see {@link sync} for lazily providing a single service @@ -951,7 +989,7 @@ export const syncContext = (evaluate: LazyArg>): Layer * * **Example** (Creating a layer from an effect) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service(evaluate: LazyArg>): Layer * query: (sql: string) => Effect.succeed(`Query: ${sql}`) * })) * ) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, layer)) // => "Query: SELECT 1" * ``` * * @see {@link effectContext} for effectfully providing multiple services @@ -1008,7 +1048,7 @@ const effectImpl = ( * * **Example** (Creating a layer from an effectful context) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service< @@ -1021,6 +1061,8 @@ const effectImpl = ( * query: (sql: string) => Effect.succeed(`Query: ${sql}`) * })) * ) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, layer)) // => "Query: SELECT 1" * ``` * * @see {@link effect} for effectfully providing a single service @@ -1043,14 +1085,17 @@ export const effectContext = ( * * **Example** (Running an effect during layer construction) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Layer } from "effect" * + * const logs: Array = [] * const initLayer = Layer.effectDiscard( * Effect.sync(() => { - * console.log("Initializing application...") + * logs.push("Initializing application...") * }) * ) + * Effect.runSync(Effect.scoped(Layer.build(initLayer))) + * logs // => ["Initializing application..."] * ``` * * @see {@link empty} for a no-op layer that performs no construction work @@ -1071,8 +1116,8 @@ export const effectDiscard = (effect: Effect): Layer()("Config") {} * @@ -1083,6 +1128,7 @@ export const effectDiscard = (effect: Effect): Layer "https://api.example.com" * ``` * * @category constructors @@ -1106,7 +1152,7 @@ export const suspend = (evaluate: LazyArg>): Layer(evaluate: LazyArg>): Layer database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, unwrappedLayer)) // => "result" * ``` * * @category converting @@ -1165,7 +1213,7 @@ const mergeAllEffect = , ...Array, ...Array Effect.succeed("result")) * }) + * const logs: Array = [] * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(msg))) * }) * * const mergedLayer = Layer.mergeAll(dbLayer, loggerLayer) + * const program = Logger.use((logger) => logger.log("ready")) + * Effect.runSync(Effect.provide(program, mergedLayer)) + * logs // => ["ready"] * ``` * * @see {@link merge} for merging one layer with another layer or array @@ -1216,7 +1268,7 @@ export const mergeAll = , ...Array, ...Array Effect.succeed("result")) * }) * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * log: Effect.fn("Logger.log")((_msg: string) => Effect.void) * }) * * const mergedLayer = Layer.merge(dbLayer, loggerLayer) + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, mergedLayer)) // => "result" * ``` * * @see {@link mergeAll} for merging several layers at once @@ -1310,7 +1364,7 @@ const provideWith = ( * * **Example** (Providing layer dependencies) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service Effect.succeed(`DB: ${sql}`)) * }) * + * const logs: Array = [] * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`))) + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`))) * }) * * // UserService depends on Database and Logger @@ -1365,6 +1420,8 @@ const provideWith = ( * }).pipe( * Effect.provide(userServiceWithDependencies) * ) + * Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" } + * logs // => ["[LOG] Looking up user 123"] * ``` * * @see {@link provideMerge} for retaining the dependency services @@ -1419,7 +1476,7 @@ export const provide: { * * **Example** (Providing dependencies while retaining services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service Effect.succeed(`DB: ${sql}`)) * }) * + * const logs: Array = [] * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`))) + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`))) * }) * * // UserService depends on Database and Logger @@ -1480,6 +1538,8 @@ export const provide: { * }).pipe( * Effect.provide(allServicesLayer) * ) + * Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" } + * logs // => ["[LOG] Looking up user 123", "[LOG] Found user: DB: SELECT * FROM users WHERE id = 123"] * ``` * * @see {@link provide} for keeping dependency services private @@ -1529,7 +1589,7 @@ export const provideMerge: { * * **Example** (Creating services from layer output) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Config extends Context.Service Effect.Effect * }>()("Logger") {} * + * const logs: Array = [] + * * // Base config layer * const configLayer = Layer.succeed(Config, { * dbUrl: "postgres://localhost:5432/mydb", @@ -1568,8 +1630,8 @@ export const provideMerge: { * const loggerLayer = Layer.succeed(Logger, { * log: Effect.fn("Logger.log")((msg: string) => * config.logLevel === "debug" - * ? Effect.sync(() => console.log(`[DEBUG] ${msg}`)) - * : Effect.sync(() => console.log(msg)) + * ? Effect.sync(() => logs.push(`[DEBUG] ${msg}`)) + * : Effect.sync(() => logs.push(msg)) * ) * }) * @@ -1590,6 +1652,8 @@ export const provideMerge: { * }).pipe( * Effect.provide(dynamicServiceLayer) * ) + * Effect.runSync(program) // => "Querying postgres://localhost:5432/mydb: SELECT * FROM users" + * logs // => ["[DEBUG] Starting database query"] * ``` * * @category sequencing @@ -1744,8 +1808,8 @@ export const tapCause: { * * **Example** (Converting layer failures to defects) * - * ```ts - * import { Context, Data, Effect, Layer } from "effect" + * ```ts import.meta.vitest + * import { Context, Data, Effect, Exit, Layer } from "effect" * * class DatabaseError extends Data.TaggedError("DatabaseError")<{ * message: string @@ -1756,10 +1820,11 @@ export const tapCause: { * }>()("Database") {} * * // Layer that can fail during construction - * const flakyDatabaseLayer = Layer.effect(Database, Effect.gen(function*() { - * console.log("connecting") - * return yield* new DatabaseError({ message: "Connection failed" }) - * })) + * const error = new DatabaseError({ message: "Connection failed" }) + * const flakyDatabaseLayer = Layer.effect( + * Database, + * Effect.fail(error) + * ) * * // Convert failures to fiber death - removes error from type * const reliableDatabaseLayer = flakyDatabaseLayer.pipe(Layer.orDie) @@ -1772,8 +1837,7 @@ export const tapCause: { * Effect.provide(reliableDatabaseLayer) * ) * - * // Running the program prints "connecting", then the DatabaseError is - * // converted into a fiber defect instead of remaining a typed error. + * Effect.runSync(Effect.exit(program)) // => Exit.die(error) * ``` * * @category error handling @@ -1828,7 +1892,7 @@ export { * * **Example** (Recovering from tagged layer errors) * - * ```ts + * ```ts import.meta.vitest * import { Context, Data, Effect, Layer } from "effect" * * class ConfigError extends Data.TaggedError("ConfigError") {} @@ -1844,6 +1908,8 @@ export { * const recovered = configLayer.pipe( * Layer.catchTag("ConfigError", () => fallbackLayer) * ) + * const program = Config.useSync((config) => config.apiUrl) + * Effect.runSync(Effect.provide(program, recovered)) // => "http://localhost" * ``` * * @see {@link catchCause} for recovering with access to the full cause @@ -1919,7 +1985,7 @@ export const catchTag: { * * **Example** (Recovering from layer failures by cause) * - * ```ts + * ```ts import.meta.vitest * import { Context, Data, Effect, Layer } from "effect" * * class DatabaseError extends Data.TaggedError("DatabaseError")<{ @@ -1944,14 +2010,12 @@ export const catchTag: { * * const program = Effect.gen(function*() { * const database = yield* Database - * const result = yield* database.query("SELECT * FROM users") - * console.log(result) + * return yield* database.query("SELECT * FROM users") * }).pipe( * Effect.provide(databaseWithFallback) * ) * - * Effect.runPromise(program) - * // Memory: SELECT * FROM users + * await Effect.runPromise(program) // => "Memory: SELECT * FROM users" * ``` * * @see {@link catchTag} for recovering from specific tagged errors @@ -2030,7 +2094,7 @@ export const updateService: { * * **Example** (Creating non-shared layer instances) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, Ref } from "effect" * * class Counter extends Context.Service n + 1) - * console.log("constructed Counter") * return { id } * })) * @@ -2075,7 +2138,7 @@ export const updateService: { * Layer.provide(rightLayer, counterLayer) * ) * - * yield* Effect.provide(showIds, shared) + * const sharedResult = yield* Effect.provide(compareIds, shared) * * const freshCounterLayer = Layer.fresh(counterLayer) * const fresh = Layer.merge( @@ -2083,15 +2146,12 @@ export const updateService: { * Layer.provide(rightLayer, freshCounterLayer) * ) * - * yield* Effect.provide(showIds, fresh) + * const freshResult = yield* Effect.provide(compareIds, fresh) + * + * return { shared: sharedResult, fresh: freshResult } * }) * - * Effect.runPromise(program) - * // constructed Counter - * // same Counter: true - * // constructed Counter - * // constructed Counter - * // same Counter: false + * await Effect.runPromise(program) // => { shared: true, fresh: false } * ``` * * @category layers @@ -2115,50 +2175,30 @@ export const fresh = (self: Layer): Layer => * * **Example** (Launching an application layer) * - * ```ts - * import { Console, Context, Effect, Layer } from "effect" + * ```ts import.meta.vitest + * import { Context, Deferred, Effect, Fiber, Layer, Ref } from "effect" * * class HttpServer extends Context.Service Effect.Effect - * readonly stop: () => Effect.Effect + * readonly port: number * }>()("HttpServer") {} * - * class Logger extends Context.Service Effect.Effect - * }>()("Logger") {} - * - * // Server layer that starts an HTTP server - * const serverLayer = Layer.effect(HttpServer, Effect.gen(function*() { - * yield* Console.log("Starting HTTP server...") + * const program = Effect.gen(function*() { + * const events = yield* Ref.make>([]) + * const started = yield* Deferred.make() * - * return { - * start: Effect.fn("HttpServer.start")(function*() { - * yield* Console.log("Server listening on port 3000") - * return "Server started" - * }), - * stop: Effect.fn("HttpServer.stop")(function*() { - * yield* Console.log("Server stopped gracefully") - * return "Server stopped" - * }) - * } - * })) + * const serverLayer = Layer.effect(HttpServer, Effect.gen(function*() { + * yield* Ref.update(events, (events) => [...events, "Starting HTTP server..."]) + * yield* Deferred.succeed(started, undefined) + * return { port: 3000 } + * })) * - * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Console.log(`[LOG] ${msg}`)) + * const fiber = yield* Effect.forkChild(Layer.launch(serverLayer)) + * yield* Deferred.await(started) + * yield* Fiber.interrupt(fiber) + * return yield* Ref.get(events) * }) * - * // Application layer combining all services - * const appLayer = Layer.mergeAll(serverLayer, loggerLayer) - * - * // Launch the application - runs until interrupted - * const application = appLayer.pipe( - * Layer.launch, - * Effect.tapError((error) => Console.log(`Application failed: ${error}`)), - * Effect.tap(() => Console.log("Application completed")) - * ) - * - * // This will run forever until externally interrupted - * // Effect.runFork(application) + * await Effect.runPromise(program) // => ["Starting HTTP server..."] * ``` * * @category converting @@ -2219,7 +2259,7 @@ type AnyEffectOrStream = * * **Example** (Mocking services for tests) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class UserService extends Context.Service "Test User" * ``` * * @category testing @@ -2326,22 +2368,17 @@ const ChannelTypeId: Channel.TypeId = "~effect/Channel" * * **Example** (Constraining layer success types) * - * ```ts - * import { Layer } from "effect" + * ```ts import.meta.vitest + * import { Context, Layer } from "effect" * - * declare const FortyTwoLayer: Layer.Layer<42, never, never> - * declare const StringLayer: Layer.Layer + * const NumberService = Context.Service("Number") + * const numberLayer = Layer.succeed(NumberService, 42) * * // Define a constraint that the success type must be a number * const satisfiesNumber = Layer.satisfiesSuccessType() * * // This works - Layer<42, never, never> extends Layer - * const validLayer = satisfiesNumber(FortyTwoLayer) - * - * // This would cause a TypeScript compilation error: - * // const invalidLayer = satisfiesNumber(StringLayer) - * // ^^^^^^^^^^^ - * // Type 'string' is not assignable to type 'number' + * const validLayer = satisfiesNumber(numberLayer) * ``` * * @category utility types @@ -2360,23 +2397,16 @@ export const satisfiesSuccessType = * * **Example** (Constraining layer error types) * - * ```ts - * import { Layer } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer } from "effect" * - * declare const ErrorLayer: Layer.Layer - * declare const TypeErrorLayer: Layer.Layer - * declare const StringLayer: Layer.Layer + * const typeErrorLayer = Layer.effectDiscard(Effect.fail(new TypeError("boom"))) * * // Define a constraint that the error type must be an Error * const satisfiesError = Layer.satisfiesErrorType() * * // This works - Layer extends Layer - * const validLayer = satisfiesError(TypeErrorLayer) - * - * // This would cause a TypeScript compilation error: - * // const invalidLayer = satisfiesError(StringLayer) - * // ^^^^^^^^^^^ - * // Type 'string' is not assignable to type 'Error' + * const validLayer = satisfiesError(typeErrorLayer) * ``` * * @category utility types @@ -2395,22 +2425,17 @@ export const satisfiesErrorType = * * **Example** (Constraining layer service requirements) * - * ```ts - * import { Layer } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Layer } from "effect" * - * declare const FortyTwoLayer: Layer.Layer - * declare const StringLayer: Layer.Layer + * const NumberService = Context.Service("Number") + * const numberLayer = Layer.effectDiscard(Effect.asVoid(NumberService)) * * // Define a constraint that the service requirements must be numbers * const satisfiesNumber = Layer.satisfiesServicesType() * * // This works - Layer extends Layer - * const validLayer = satisfiesNumber(FortyTwoLayer) - * - * // This would cause a TypeScript compilation error: - * // const invalidLayer = satisfiesNumber(StringLayer) - * // ^^^^^^^^^^^ - * // Type 'string' is not assignable to type 'number' + * const validLayer = satisfiesNumber(numberLayer) * ``` * * @category utility types @@ -2468,38 +2493,37 @@ export interface SpanOptions extends Tracer.SpanOptions { * * **Example** (Tracing layer construction with a span) * - * ```ts - * import { Console, Context, Effect, Layer } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Layer } from "effect" * import type { Tracer } from "effect" * * class Database extends Context.Service Effect.Effect * }>()("Database") {} * + * const logs: Array = [] + * * // Create a traced layer - all operations performed during construction of * // the `Database` service are part of the "database-init" span * const databaseLayer = Layer.effect(Database, Effect.gen(function*() { * // These operations are traced under "database-init" span - * yield* Effect.log("Connecting to database") - * yield* Effect.sleep("100 millis") - * yield* Effect.log("Database connected") + * logs.push("Connecting to database") + * logs.push("Database connected") * * const parentSpan = yield* Effect.currentParentSpan - * yield* Console.log((parentSpan as Tracer.Span).name) // "database-init" + * logs.push((parentSpan as Tracer.Span).name) * * return { * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) * } - * })).pipe(Layer.provide(Layer.span("database-init"))) - * - * // Can also use the `onEnd` callback to execute logic when the span ends - * const tracedLayer = Layer.span("service-initialization", { - * attributes: { version: "1.0.0" }, + * })).pipe(Layer.provide(Layer.span("database-init", { * onEnd: (span, exit) => - * Effect.sync(() => { - * console.log(`Span ${span.name} ended with:`, exit._tag) - * }) - * }) + * Effect.sync(() => logs.push(`Span ${span.name} ended with: ${exit._tag}`)) + * }))) + * + * const program = Database.use((database) => database.query("SELECT 1")) + * Effect.runSync(Effect.provide(program, databaseLayer)) // => "Result: SELECT 1" + * logs // => ["Connecting to database", "Database connected", "database-init", "Span database-init ended with: Success"] * ``` * * @category tracing @@ -2532,10 +2556,11 @@ export const span = ( * * **Example** (Referencing an existing parent span) * - * ```ts - * import { Console, Context, Effect, Layer, Tracer } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Layer, Tracer } from "effect" * * class Database extends Context.Service Effect.Effect * }>()("Database") {} * @@ -2543,12 +2568,10 @@ export const span = ( * const databaseLayer = Layer.effect( * Database, * Effect.gen(function*() { - * yield* Effect.log("Initializing database") - * * const parentSpan = yield* Effect.currentParentSpan - * yield* Console.log(parentSpan.spanId) // "42" * * return { + * spanId: parentSpan.spanId, * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) * } * }) @@ -2556,6 +2579,9 @@ export const span = ( * spanId: "42", * traceId: "000" * })))) + * const program = Database.use((database) => + * Effect.map(database.query("SELECT 1"), (result) => ({ spanId: database.spanId, result }))) + * Effect.runSync(Effect.provide(program, databaseLayer)) // => { spanId: "42", result: "Result: SELECT 1" } * ``` * * @category tracing @@ -2576,7 +2602,7 @@ export const parentSpan = (span: Tracer.AnySpan): Layer => * * **Example** (Wrapping a layer with a span) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer } from "effect" * * class Database extends Context.Service => * readonly log: (msg: string) => Effect.Effect * }>()("Logger") {} * + * const logs: Array = [] + * * // Create layers with tracing * const databaseLayer = Layer.effect(Database, Effect.gen(function*() { - * yield* Effect.log("Connecting to database") - * yield* Effect.sleep("100 millis") * return { * query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result: ${sql}`)) * } @@ -2599,16 +2625,14 @@ export const parentSpan = (span: Tracer.AnySpan): Layer => * })) * * const loggerLayer = Layer.succeed(Logger, { - * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => console.log(msg))) + * log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(msg))) * }).pipe(Layer.withSpan("logger-initialization")) * * // Combine traced layers * const appLayer = Layer.mergeAll(databaseLayer, loggerLayer).pipe( * Layer.withSpan("app-initialization", { * onEnd: (span, exit) => - * Effect.sync(() => { - * console.log(`Application initialization completed: ${exit._tag}`) - * }) + * Effect.sync(() => logs.push(`Application initialization completed: ${exit._tag}`)) * }) * ) * @@ -2619,6 +2643,8 @@ export const parentSpan = (span: Tracer.AnySpan): Layer => * yield* logger.log("Application ready") * return yield* database.query("SELECT * FROM users") * }).pipe(Effect.provide(appLayer)) + * Effect.runSync(program) // => "Result: SELECT * FROM users" + * logs // => ["Application ready", "Application initialization completed: Success"] * ``` * * @category tracing @@ -2684,7 +2710,7 @@ export const withSpan: { * * **Example** (Attaching layers to an existing parent span) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, Tracer } from "effect" * * class Database extends Context.Service Effect.succeed(`DB: ${sql}`)) * } * })) * * const CacheLayer = Layer.effect(Cache, Effect.gen(function*() { - * yield* Effect.log("Connecting to cache") * return { * get: Effect.fn("Cache.get")((key: string) => Effect.succeed(`Cache: ${key}`)) * } @@ -2730,6 +2754,7 @@ export const withSpan: { * return { dbResult, cacheResult } * }) * ) + * Effect.runSync(Effect.scoped(program)) // => { dbResult: "DB: SELECT * FROM users", cacheResult: "Cache: user:123" } * ``` * * @category tracing diff --git a/.context/effect/packages/effect/src/LayerMap.ts b/.context/effect/packages/effect/src/LayerMap.ts index ce63bbdba..7b787441e 100644 --- a/.context/effect/packages/effect/src/LayerMap.ts +++ b/.context/effect/packages/effect/src/LayerMap.ts @@ -33,7 +33,7 @@ type IdleTimeToLiveInput = Duration.Input | ((key: K) => Duration.Input) * * **Example** (Managing keyed layers) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerMap } from "effect" * * // Define a service key @@ -53,14 +53,22 @@ type IdleTimeToLiveInput = Duration.Input | ((key: K) => Duration.Input) * const layerMap = yield* createDatabaseLayerMap * * // Get a layer for a specific environment - * const devLayer = layerMap.get("development") + * const development = yield* Effect.provide( + * DatabaseService.use((database) => database.query("SELECT 1")), + * layerMap.get("development") + * ) * * // Get context directly - * const context = yield* layerMap.contextEffect("production") + * const productionContext = yield* layerMap.contextEffect("production") + * const production = yield* Context.get(productionContext, DatabaseService).query("SELECT 1") * * // Invalidate a cached layer * yield* layerMap.invalidate("development") + * + * return { development, production } * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => { development: "development: SELECT 1", production: "production: SELECT 1" } * ``` * * @category models @@ -95,7 +103,7 @@ export interface LayerMap { * * **Example** (Creating a layer map) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerMap } from "effect" * * // Define a service key @@ -117,16 +125,16 @@ export interface LayerMap { * const devLayer = layerMap.get("development") * * // Use the layer to provide the service - * const result = yield* Effect.provide( + * return yield* Effect.provide( * Effect.gen(function*() { * const db = yield* DatabaseService * return yield* db.query("SELECT * FROM users") * }), * devLayer * ) - * - * console.log(result) // "development: SELECT * FROM users" * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => "development: SELECT * FROM users" * ``` * * @category constructors @@ -150,6 +158,7 @@ export const make: < lookup: (key: K) => Layer.Layer, options?: { readonly idleTimeToLive?: IdleTimeToLiveInput | undefined + readonly preloadKeys?: Iterable | undefined } | undefined ) { const context = yield* Effect.context() @@ -163,6 +172,12 @@ export const make: < idleTimeToLive: options?.idleTimeToLive }) + if (options?.preloadKeys) { + for (const key of options.preloadKeys) { + yield* Effect.scoped(RcMap.get(rcMap, key)) + } + } + return identity>({ [TypeId]: TypeId, rcMap, @@ -182,24 +197,20 @@ export const make: < * * **Example** (Creating a layer map from a record) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerMap } from "effect" * - * // Define service keys - * const DevDatabase = Context.Service<{ - * readonly query: (sql: string) => Effect.Effect - * }>("DevDatabase") - * - * const ProdDatabase = Context.Service<{ + * // Define a service key + * const Database = Context.Service<{ * readonly query: (sql: string) => Effect.Effect - * }>("ProdDatabase") + * }>("Database") * * // Create predefined layers * const layers = { - * development: Layer.succeed(DevDatabase)({ + * development: Layer.succeed(Database)({ * query: Effect.fn("DevDatabase.query")((sql) => Effect.succeed(`DEV: ${sql}`)) * }), - * production: Layer.succeed(ProdDatabase)({ + * production: Layer.succeed(Database)({ * query: Effect.fn("ProdDatabase.query")((sql) => Effect.succeed(`PROD: ${sql}`)) * }) * } as const @@ -210,12 +221,19 @@ export const make: < * idleTimeToLive: "10 seconds" * }) * - * // Get layers by key - * const devLayer = layerMap.get("development") - * const prodLayer = layerMap.get("production") + * const development = yield* Effect.provide( + * Database.use((database) => database.query("SELECT 1")), + * layerMap.get("development") + * ) + * const production = yield* Effect.provide( + * Database.use((database) => database.query("SELECT 1")), + * layerMap.get("production") + * ) * - * console.log("LayerMap created from record") + * return { development, production } * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => { development: "DEV: SELECT 1", production: "PROD: SELECT 1" } * ``` * * @category constructors @@ -310,8 +328,8 @@ export interface TagClass< * * **Example** (Defining a layer map service) * - * ```ts - * import { Console, Context, Effect, Layer, LayerMap } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Layer, LayerMap } from "effect" * * // Define a service key * const Greeter = Context.Service<{ @@ -334,7 +352,7 @@ export interface TagClass< * const program = Effect.gen(function*() { * // Access and use the Greeter service * const greeter = yield* Greeter - * yield* Console.log(yield* greeter.greet) + * return yield* greeter.greet * }).pipe( * // Use the GreeterMap service to provide a variant of the Greeter service * Effect.provide(GreeterMap.get("John")) @@ -342,6 +360,8 @@ export interface TagClass< * // Provide the GreeterMap layer * Effect.provide(GreeterMap.layer) * ) + * + * await Effect.runPromise(program) // => "Hello, John!" * ``` * * @category services @@ -424,7 +444,7 @@ export declare namespace Service { /** * Extracts the key type accepted by a `LayerMap.Service` definition. * - * @category services + * @category utility types * @since 3.14.0 */ export type Key = Options extends { readonly lookup: (key: infer K) => any } ? K @@ -434,7 +454,7 @@ export declare namespace Service { /** * Extracts the layer type produced by a `LayerMap.Service` definition. * - * @category services + * @category utility types * @since 3.14.0 */ export type Layers = Options extends { readonly lookup: (key: infer _K) => infer Layers } ? Layers @@ -445,7 +465,7 @@ export declare namespace Service { * Extracts the services provided by the layers in a `LayerMap.Service` * definition. * - * @category services + * @category utility types * @since 3.14.0 */ export type Success = Layers extends Layer.Layer ? _A : never @@ -453,7 +473,7 @@ export declare namespace Service { /** * Extracts the error type of the layers in a `LayerMap.Service` definition. * - * @category services + * @category utility types * @since 3.14.0 */ export type Error = Layers extends Layer.Layer ? _E : never @@ -462,7 +482,7 @@ export declare namespace Service { * Extracts the service requirements of the layers in a `LayerMap.Service` * definition. * - * @category services + * @category utility types * @since 4.0.0 */ export type Services = Layers extends Layer.Layer ? _R : never diff --git a/.context/effect/packages/effect/src/LayerRef.ts b/.context/effect/packages/effect/src/LayerRef.ts index 387c7940a..a96100773 100644 --- a/.context/effect/packages/effect/src/LayerRef.ts +++ b/.context/effect/packages/effect/src/LayerRef.ts @@ -92,7 +92,7 @@ export interface LayerRef { * * **Example** (Sharing one layer-built service) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerRef } from "effect" * * class Database extends Context.Service { * return result * }) * ) + * + * await Effect.runPromise(program) // => "result" * ``` * * @see {@link Service} for defining a reusable service class around a `LayerRef` @@ -270,7 +272,7 @@ export interface TagClass< * * **Example** (Defining a refreshable service) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, LayerRef } from "effect" * * class Database extends Context.Service "result" * ``` * * @see {@link make} for creating a `LayerRef` value without defining a service class diff --git a/.context/effect/packages/effect/src/LogLevel.ts b/.context/effect/packages/effect/src/LogLevel.ts index f8002b848..837e1bd5a 100644 --- a/.context/effect/packages/effect/src/LogLevel.ts +++ b/.context/effect/packages/effect/src/LogLevel.ts @@ -36,8 +36,8 @@ import * as References from "./References.ts" * * **Example** (Using log levels) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, References } from "effect" * * // Using log levels with Effect logging * const program = Effect.gen(function*() { @@ -52,6 +52,13 @@ import * as References from "./References.ts" * // Type-safe log level variables * const errorLevel = "Error" // LogLevel * const debugLevel = "Debug" // LogLevel + * + * await Effect.runPromise( + * Effect.provideService(program, References.MinimumLogLevel, "None") + * ) + * + * const levels = [errorLevel, debugLevel] + * levels // => ["Error", "Debug"] * ``` * * @category models @@ -101,7 +108,7 @@ export type Severity = "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" * @see {@link Severity} for the concrete message severity type that excludes `All` and `None` * @see {@link Order} for comparing these levels by severity order * - * @category models + * @category constants * @since 4.0.0 */ export const values: ReadonlyArray = ["All", "Fatal", "Error", "Warn", "Info", "Debug", "Trace", "None"] @@ -120,13 +127,12 @@ export const values: ReadonlyArray = ["All", "Fatal", "Error", "Warn", * * **Example** (Ordering log levels) * - * ```ts + * ```ts import.meta.vitest * import { LogLevel } from "effect" * - * // Compare log levels using Order - * console.log(LogLevel.Order("Error", "Info")) // 1 (Error > Info) - * console.log(LogLevel.Order("Debug", "Error")) // -1 (Debug < Error) - * console.log(LogLevel.Order("Info", "Info")) // 0 (Info == Info) + * LogLevel.Order("Error", "Info") // => 1 + * LogLevel.Order("Debug", "Error") // => -1 + * LogLevel.Order("Info", "Info") // => 0 * ``` * * @category ordering @@ -148,11 +154,11 @@ export const Order: Ord.Order = effect.LogLevelOrder * * **Example** (Comparing log levels) * - * ```ts + * ```ts import.meta.vitest * import { LogLevel } from "effect" * - * console.log(LogLevel.Equivalence("Error", "Error")) // true - * console.log(LogLevel.Equivalence("Error", "Info")) // false + * LogLevel.Equivalence("Error", "Error") // => true + * LogLevel.Equivalence("Error", "Info") // => false * ``` * * @see {@link Order} for severity ordering rather than exact level equality @@ -204,25 +210,24 @@ export const getOrdinal = (self: LogLevel): number => effect.logLevelToOrder(sel * * **Example** (Checking higher severity) * - * ```ts + * ```ts import.meta.vitest * import { LogLevel } from "effect" * - * // Check if Error is more severe than Info - * console.log(LogLevel.isGreaterThan("Error", "Info")) // true - * console.log(LogLevel.isGreaterThan("Debug", "Error")) // false + * LogLevel.isGreaterThan("Error", "Info") // => true + * LogLevel.isGreaterThan("Debug", "Error") // => false * * // Use with filtering * const isFatal = LogLevel.isGreaterThan("Fatal", "Warn") * const isError = LogLevel.isGreaterThan("Error", "Warn") * const isDebug = LogLevel.isGreaterThan("Debug", "Warn") - * console.log(isFatal) // true - * console.log(isError) // true - * console.log(isDebug) // false + * isFatal // => true + * isError // => true + * isDebug // => false * * // Curried usage * const isMoreSevereThanInfo = LogLevel.isGreaterThan("Info") - * console.log(isMoreSevereThanInfo("Error")) // true - * console.log(isMoreSevereThanInfo("Debug")) // false + * isMoreSevereThanInfo("Error") // => true + * isMoreSevereThanInfo("Debug") // => false * ``` * * @category ordering @@ -247,33 +252,15 @@ export const isGreaterThan: { * * **Example** (Filtering by minimum log level) * - * ```ts - * import { Logger, LogLevel } from "effect" - * - * // Check if level meets minimum threshold - * console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Error")) // true - * console.log(LogLevel.isGreaterThanOrEqualTo("Error", "Info")) // true - * console.log(LogLevel.isGreaterThanOrEqualTo("Debug", "Info")) // false + * ```ts import.meta.vitest + * import { LogLevel } from "effect" * - * // Create a logger that only logs Info and above - * const infoLogger = Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Info")) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) + * LogLevel.isGreaterThanOrEqualTo("Error", "Error") // => true + * LogLevel.isGreaterThanOrEqualTo("Error", "Info") // => true + * LogLevel.isGreaterThanOrEqualTo("Debug", "Info") // => false * - * // Production logger - only Error and Fatal - * const productionLogger = Logger.make((options) => { - * if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Error")) { - * console.error( - * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` - * ) - * } - * }) - * - * // Curried usage for filtering * const isInfoOrAbove = LogLevel.isGreaterThanOrEqualTo("Info") - * const shouldLog = isInfoOrAbove("Error") // true + * isInfoOrAbove("Error") // => true * ``` * * @category ordering @@ -297,25 +284,24 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking lower severity) * - * ```ts + * ```ts import.meta.vitest * import { LogLevel } from "effect" * - * // Check if Debug is less severe than Info - * console.log(LogLevel.isLessThan("Debug", "Info")) // true - * console.log(LogLevel.isLessThan("Error", "Info")) // false + * LogLevel.isLessThan("Debug", "Info") // => true + * LogLevel.isLessThan("Error", "Info") // => false * * // Filter out verbose logs * const isFatalVerbose = LogLevel.isLessThan("Fatal", "Info") * const isErrorVerbose = LogLevel.isLessThan("Error", "Info") * const isTraceVerbose = LogLevel.isLessThan("Trace", "Info") - * console.log(isFatalVerbose) // false (Fatal is not verbose) - * console.log(isErrorVerbose) // false (Error is not verbose) - * console.log(isTraceVerbose) // true (Trace is verbose) + * isFatalVerbose // => false + * isErrorVerbose // => false + * isTraceVerbose // => true * * // Curried usage * const isLessSevereThanError = LogLevel.isLessThan("Error") - * console.log(isLessSevereThanError("Info")) // true - * console.log(isLessSevereThanError("Fatal")) // false + * isLessSevereThanError("Info") // => true + * isLessSevereThanError("Fatal") // => false * ``` * * @category ordering @@ -340,31 +326,15 @@ export const isLessThan: { * * **Example** (Filtering by maximum log level) * - * ```ts - * import { Logger, LogLevel } from "effect" - * - * // Check if level is at or below threshold - * console.log(LogLevel.isLessThanOrEqualTo("Info", "Info")) // true - * console.log(LogLevel.isLessThanOrEqualTo("Debug", "Info")) // true - * console.log(LogLevel.isLessThanOrEqualTo("Error", "Info")) // false - * - * // Create a logger that suppresses verbose logs - * const quietLogger = Logger.make((options) => { - * if (LogLevel.isLessThanOrEqualTo(options.logLevel, "Info")) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) + * ```ts import.meta.vitest + * import { LogLevel } from "effect" * - * // Development logger - suppress trace logs - * const devLogger = Logger.make((options) => { - * if (LogLevel.isLessThanOrEqualTo(options.logLevel, "Debug")) { - * console.log(`[${options.logLevel}] ${options.message}`) - * } - * }) + * LogLevel.isLessThanOrEqualTo("Info", "Info") // => true + * LogLevel.isLessThanOrEqualTo("Debug", "Info") // => true + * LogLevel.isLessThanOrEqualTo("Error", "Info") // => false * - * // Curried usage for filtering * const isInfoOrBelow = LogLevel.isLessThanOrEqualTo("Info") - * const shouldLog = isInfoOrBelow("Debug") // true + * isInfoOrBelow("Debug") // => true * ``` * * @category ordering @@ -390,22 +360,24 @@ export const isLessThanOrEqualTo: { * * **Example** (Checking current fiber log level) * - * ```ts + * ```ts import.meta.vitest * import { Effect, LogLevel, References } from "effect" * * const program = Effect.gen(function*() { * const debugEnabled = yield* LogLevel.isEnabled("Debug") * const errorEnabled = yield* LogLevel.isEnabled("Error") * - * console.log({ debugEnabled, errorEnabled }) + * return { debugEnabled, errorEnabled } * }) * * const warnOnly = program.pipe( * Effect.provideService(References.MinimumLogLevel, "Warn") * ) + * + * await Effect.runPromise(warnOnly) // => { debugEnabled: false, errorEnabled: true } * ``` * - * @category filtering + * @category predicates * @since 4.0.0 */ export const isEnabled = (self: LogLevel): Effect.Effect => diff --git a/.context/effect/packages/effect/src/Logger.ts b/.context/effect/packages/effect/src/Logger.ts index 9c37114c1..ec3554a4e 100644 --- a/.context/effect/packages/effect/src/Logger.ts +++ b/.context/effect/packages/effect/src/Logger.ts @@ -20,6 +20,7 @@ import * as Formatter from "./Formatter.ts" import { dual } from "./Function.ts" import { isEffect, withFiber } from "./internal/core.ts" import * as effect from "./internal/effect.ts" +import * as InternalRecord from "./internal/record.ts" import * as Layer from "./Layer.ts" import type * as LogLevel from "./LogLevel.ts" import type { Pipeable } from "./Pipeable.ts" @@ -41,23 +42,20 @@ const TypeId = "~effect/Logger" * * **Example** (Creating custom loggers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * - * // Create a custom logger that accepts unknown messages and returns void + * const messages: Array = [] * const stringLogger = Logger.make((options) => { - * console.log(`[${options.logLevel}] ${options.message}`) + * messages.push(`[${options.logLevel}] ${options.message}`) * }) * - * // Create a logger that accepts any message type and returns a formatted string - * const formattedLogger = Logger.make((options) => - * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` - * ) - * - * // Use the logger in an Effect program * const program = Effect.log("Hello World").pipe( * Effect.provide(Logger.layer([stringLogger])) * ) + * + * Effect.runSync(program) + * messages // => ["[Info] Hello World"] * ``` * * @category models @@ -77,24 +75,24 @@ export interface Logger extends Pipeable { * * **Example** (Accessing logger options) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * - * // Options interface provides all logging context + * const outputs: Array = [] * const detailedLogger = Logger.make((options) => { - * const output = { + * outputs.push({ * message: options.message, * level: options.logLevel, - * timestamp: options.date.toISOString(), - * fiberId: options.fiber.id, - * hasCause: options.cause !== undefined - * } - * console.log(JSON.stringify(output)) + * hasCause: options.cause.reasons.length > 0 + * }) * }) * * const program = Effect.log("Processing request").pipe( * Effect.provide(Logger.layer([detailedLogger])) * ) + * + * Effect.runSync(program) + * outputs // => [{ message: ["Processing request"], level: "Info", hasCause: false }] * ``` * * @category options @@ -113,16 +111,14 @@ export interface Options { * * **Example** (Checking logger values) * - * ```ts + * ```ts import.meta.vitest * import { Logger } from "effect" * - * const myLogger = Logger.make((options) => { - * console.log(options.message) - * }) + * const myLogger = Logger.make(() => undefined) * - * console.log(Logger.isLogger(myLogger)) // true - * console.log(Logger.isLogger("not a logger")) // false - * console.log(Logger.isLogger({ log: () => {} })) // false + * Logger.isLogger(myLogger) // => true + * Logger.isLogger("not a logger") // => false + * Logger.isLogger({ log: () => {} }) // => false * ``` * * @category guards @@ -141,26 +137,26 @@ export const isLogger = (u: unknown): u is Logger => Predicate * * **Example** (Accessing current loggers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * - * // Access current loggers from fiber context + * const messages: Array = [] + * const customLogger = Logger.make((options) => { + * messages.push(options.message) + * }) * const program = Effect.gen(function*() { * const currentLoggers = yield* Effect.service(Logger.CurrentLoggers) - * console.log(`Number of active loggers: ${currentLoggers.size}`) - * - * // Add a custom logger to the set - * const customLogger = Logger.make((options) => { - * console.log(`Custom: ${options.message}`) - * }) - * * yield* Effect.log("Hello from custom logger").pipe( * Effect.provide(Logger.layer([customLogger])) * ) + * return currentLoggers.has(Logger.defaultLogger) * }) + * + * Effect.runSync(program) // => true + * messages // => [["Hello from custom logger"]] * ``` * - * @category references + * @category services * @since 4.0.0 */ export const CurrentLoggers: Context.Reference>> = effect.CurrentLoggers @@ -183,7 +179,7 @@ export const CurrentLoggers: Context.Reference> * @see {@link consolePretty} for the TTY-mode pretty console logger affected by this reference * @see {@link withConsoleError} for routing a specific formatter logger to `console.error` * - * @category references + * @category services * @since 4.0.0 */ export const LogToStderr: Context.Reference = effect.LogToStderr @@ -198,27 +194,24 @@ export const LogToStderr: Context.Reference = effect.LogToStderr * * **Example** (Transforming logger output) * - * ```ts - * import { Logger } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" * - * // Create a logger that outputs objects + * const outputs: Array = [] * const structuredLogger = Logger.make((options) => ({ - * level: options.logLevel, - * message: options.message, - * timestamp: options.date.toISOString() + * message: options.message * })) * - * // Transform the output to JSON strings - * const jsonStringLogger = Logger.map( - * structuredLogger, - * (output) => JSON.stringify(output) - * ) - * * // Transform to uppercase messages * const uppercaseLogger = Logger.map( * structuredLogger, * (output) => ({ ...output, message: String(output.message).toUpperCase() }) * ) + * + * const collector = Logger.make((options) => outputs.push(uppercaseLogger.log(options))) + * const program = Effect.log("hello").pipe(Effect.provide(Logger.layer([collector]))) + * Effect.runSync(program) + * outputs // => [{ message: "HELLO" }] * ``` * * @category mapping @@ -247,20 +240,23 @@ export const map = dual< * * **Example** (Writing logger output with console.log) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * import { TestConsole } from "effect/testing" * * // Create a custom formatter * const customFormatter = Logger.make((options) => - * `[${options.date.toISOString()}] ${options.logLevel}: ${options.message}` + * `${options.logLevel}: ${options.message}` * ) * - * // Route to console * const consoleLogger = Logger.withConsoleLog(customFormatter) * - * const program = Effect.log("Hello World").pipe( - * Effect.provide(Logger.layer([consoleLogger])) - * ) + * const program = Effect.gen(function*() { + * yield* Effect.log("Hello World").pipe(Effect.provide(Logger.layer([consoleLogger]))) + * return yield* TestConsole.logLines + * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) // => ["Info: Hello World"] * ``` * * @category logging @@ -284,20 +280,23 @@ export const withConsoleLog = ( * * **Example** (Writing logger output with console.error) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * import { TestConsole } from "effect/testing" * * // Create an error-specific formatter * const errorFormatter = Logger.make((options) => - * `ERROR [${options.date.toISOString()}]: ${options.message}` + * `ERROR: ${options.message}` * ) * - * // Route to console.error * const errorLogger = Logger.withConsoleError(errorFormatter) * - * const program = Effect.logError("Database connection failed").pipe( - * Effect.provide(Logger.layer([errorLogger])) - * ) + * const program = Effect.gen(function*() { + * yield* Effect.logError("Database connection failed").pipe(Effect.provide(Logger.layer([errorLogger]))) + * return yield* TestConsole.errorLines + * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) // => ["ERROR: Database connection failed"] * ``` * * @category logging @@ -325,9 +324,15 @@ export const withConsoleError = ( * * **Example** (Writing logs with level-based console methods) * - * ```ts - * import { Effect, Logger } from "effect" + * ```ts import.meta.vitest + * import { Console, Effect, Logger } from "effect" * + * const messages: Array> = [] + * const testConsole: Console.Console = Object.assign(Object.create(console), { + * info: (message: unknown) => messages.push(["info", message]), + * warn: (message: unknown) => messages.push(["warn", message]), + * error: (message: unknown) => messages.push(["error", message]) + * }) * const formatter = Logger.make((options) => * `[${options.logLevel}] ${options.message}` * ) @@ -338,10 +343,14 @@ export const withConsoleError = ( * yield* Effect.logInfo("Info message") // -> console.info * yield* Effect.logWarning("Warning") // -> console.warn * yield* Effect.logError("Error occurred") // -> console.error - * yield* Effect.logDebug("Debug info") // -> console.debug - * }).pipe( - * Effect.provide(Logger.layer([leveledLogger])) - * ) + * }).pipe(Effect.provide(Logger.layer([leveledLogger]))) + * Effect.runSync(Effect.provideService(program, Console.Console, testConsole)) + * const expected = [ + * ["info", "[Info] Info message"], + * ["warn", "[Warn] Warning"], + * ["error", "[Error] Error occurred"] + * ] + * messages // => expected * ``` * * @category logging @@ -404,7 +413,7 @@ const format = ( const append = (label: string, value: string): string => " " + format(label, value) let out = format("timestamp", date.toISOString()) - out += append("level", logLevel) + out += append("level", logLevel.toUpperCase()) out += append("fiber", formatFiberId(fiber.id)) const messages = Array.ensure(message) @@ -440,34 +449,20 @@ const format = ( * * **Example** (Creating loggers from functions) * - * ```ts - * import { Effect, Logger, References } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" * - * // Simple text logger + * const outputs: Array = [] * const textLogger = Logger.make((options) => - * `${options.date.toISOString()} [${options.logLevel}] ${options.message}` + * `${options.logLevel}: ${options.message}` * ) - * - * // Structured object logger - * const objectLogger = Logger.make((options) => ({ - * timestamp: options.date.toISOString(), - * level: options.logLevel, - * message: options.message, - * fiberId: options.fiber.id, - * annotations: options.fiber.getRef(References.CurrentLogAnnotations) - * })) - * - * // Custom filtering logger - * const filteredLogger = Logger.make((options) => { - * if (options.logLevel === "Debug") { - * return // Skip debug messages - * } - * return `${options.logLevel}: ${options.message}` - * }) + * const collector = Logger.make((options) => outputs.push(textLogger.log(options))) * * const program = Effect.log("Hello World").pipe( - * Effect.provide(Logger.layer([textLogger])) + * Effect.provide(Logger.layer([collector])) * ) + * Effect.runSync(program) + * outputs // => ["Info: Hello World"] * ``` * * @category constructors @@ -482,25 +477,10 @@ export const make: ( * * **Example** (Referencing the default logger) * - * ```ts - * import { Effect, Logger } from "effect" - * - * // Use the default logger (automatically used by Effect runtime) - * const program = Effect.gen(function*() { - * yield* Effect.log("This uses the default logger") - * yield* Effect.logInfo("Info message") - * yield* Effect.logError("Error message") - * }) - * - * // Explicitly use the default logger - * const withDefaultLogger = Effect.log("Explicit default").pipe( - * Effect.provide(Logger.layer([Logger.defaultLogger])) - * ) + * ```ts import.meta.vitest + * import { Logger } from "effect" * - * // Compare with custom logger - * const customLogger = Logger.make((options) => { - * console.log(`CUSTOM: ${options.message}`) - * }) + * Logger.isLogger(Logger.defaultLogger) // => true * ``` * * @category constructors @@ -518,24 +498,24 @@ export const defaultLogger: Logger = effect.defaultLogger * * **Example** (Formatting logs as simple strings) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * import { TestConsole } from "effect/testing" * * // Use the simple format logger - * const simpleLoggerProgram = Effect.log("Hello Simple Format").pipe( - * Effect.provide(Logger.layer([Logger.formatSimple])) + * const stableSimple = Logger.map(Logger.formatSimple, (output) => + * output + * .replace(/timestamp=\S+ /, "") + * .replace(/fiber=#\d+ /, "") * ) - * - * // Combine with console output - * const consoleSimpleLogger = Logger.withConsoleLog(Logger.formatSimple) - * * const program = Effect.gen(function*() { - * yield* Effect.log("Application started") - * yield* Effect.logInfo("Processing data") - * yield* Effect.logWarning("Memory usage high") - * }).pipe( - * Effect.provide(Logger.layer([consoleSimpleLogger])) - * ) + * yield* Effect.log("Application started").pipe( + * Effect.provide(Logger.layer([Logger.withConsoleLog(stableSimple)])) + * ) + * return yield* TestConsole.logLines + * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) // => ["level=INFO message=\"Application started\""] * ``` * * @category constructors @@ -554,27 +534,23 @@ export const formatSimple = effect.loggerMake(format(escapeDoubleQuotes)) * * **Example** (Formatting logs as logfmt) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * import { TestConsole } from "effect/testing" * - * // Use the logfmt format logger - * const logfmtLoggerProgram = Effect.log("Hello LogFmt Format").pipe( - * Effect.provide(Logger.layer([Logger.formatLogFmt])) - * ) - * - * // Perfect for structured logging systems - * const structuredProgram = Effect.gen(function*() { - * yield* Effect.log("User login", { userId: 123, method: "OAuth" }) - * yield* Effect.logInfo("Request processed", { - * duration: 45, - * status: "success" - * }) - * }).pipe( - * Effect.provide(Logger.layer([Logger.withConsoleLog(Logger.formatLogFmt)])) + * const stableLogFmt = Logger.map(Logger.formatLogFmt, (output) => + * output + * .replace(/timestamp=\S+ /, "") + * .replace(/fiber=#\d+ /, "") * ) + * const program = Effect.gen(function*() { + * yield* Effect.log("User login").pipe( + * Effect.provide(Logger.layer([Logger.withConsoleLog(stableLogFmt)])) + * ) + * return yield* TestConsole.logLines + * }).pipe(Effect.provide(TestConsole.layer)) * - * // Good for log aggregation systems like Splunk, ELK - * const productionLogger = Logger.formatLogFmt + * await Effect.runPromise(program) // => ["level=INFO message=\"User login\""] * ``` * * @category constructors @@ -594,30 +570,22 @@ export const formatLogFmt = effect.loggerMake(format(JSON.stringify, 0)) * * **Example** (Formatting logs as structured objects) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" + * import { TestConsole } from "effect/testing" * - * // Use the structured format logger - * const structuredLoggerProgram = Effect.log("Hello Structured Format").pipe( - * Effect.provide(Logger.layer([Logger.formatStructured])) - * ) - * - * // Perfect for JSON processing and analytics - * const analyticsProgram = Effect.gen(function*() { - * yield* Effect.log("User action", { action: "click", element: "button" }) - * yield* Effect.logInfo("API call", { endpoint: "/users", duration: 150 }) - * }).pipe( - * Effect.annotateLogs("sessionId", "abc123"), - * Effect.withLogSpan("request"), - * Effect.provide(Logger.layer([Logger.formatStructured])) - * ) + * const stableStructured = Logger.map(Logger.formatStructured, (output) => ({ + * message: output.message, + * level: output.level + * })) + * const program = Effect.gen(function*() { + * yield* Effect.log("User action").pipe( + * Effect.provide(Logger.layer([Logger.withConsoleLog(stableStructured)])) + * ) + * return yield* TestConsole.logLines + * }).pipe(Effect.provide(TestConsole.layer)) * - * // Process structured output - * const processingLogger = Logger.map(Logger.formatStructured, (output) => { - * // Process the structured object - * const enhanced = { ...output, processed: true } - * return enhanced - * }) + * await Effect.runPromise(program) // => [{ message: "User action", level: "INFO" }] * ``` * * @category constructors @@ -637,13 +605,13 @@ export const formatStructured: Logger `{"service":"api-server","entry":${jsonString}}` - * ) + * const stableJson = Logger.map(Logger.formatJson, (json) => { + * const output = JSON.parse(json) + * return Formatter.formatJson({ message: output.message, level: output.level }) + * }) + * const program = Effect.gen(function*() { + * yield* Effect.log("Server started").pipe( + * Effect.provide(Logger.layer([Logger.withConsoleLog(stableJson)])) + * ) + * return yield* TestConsole.logLines + * }).pipe(Effect.provide(TestConsole.layer)) * - * const envelopedConsoleLogger = Logger.withConsoleLog(envelopedJsonLogger) + * await Effect.runPromise(program) // => ["{\"message\":\"Server started\",\"level\":\"INFO\"}"] * ``` * * @category constructors @@ -719,43 +674,26 @@ export const formatJson = map(formatStructured, Formatter.formatJson) * * **Example** (Batching logger output) * - * ```ts - * import { Duration, Effect, Logger } from "effect" + * ```ts import.meta.vitest + * import { Effect, Logger } from "effect" * - * // Create a batched logger that flushes every 5 seconds - * const batchedLogger = Logger.batched(Logger.formatJson, { - * window: Duration.seconds(5), + * const flushed: Array> = [] + * const messageLogger = Logger.make((options) => String(options.message)) + * const batchedLogger = Logger.batched(messageLogger, { + * window: "1 hour", * flush: (messages) => * Effect.sync(() => { - * console.log(`Flushing ${messages.length} log entries:`) - * messages.forEach((msg, i) => console.log(`${i + 1}. ${msg}`)) + * flushed.push(messages) * }) * }) * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const logger = yield* batchedLogger - * - * yield* Effect.provide( - * Effect.all([ - * Effect.log("Event 1"), - * Effect.log("Event 2"), - * Effect.log("Event 3"), - * Effect.sleep(Duration.seconds(6)), // Trigger flush - * Effect.log("Event 4") - * ]), - * Logger.layer([logger]) - * ) - * }) - * - * // Remote batch logging example - * const remoteBatchLogger = Logger.batched(Logger.formatStructured, { - * window: Duration.seconds(10), - * flush: (entries) => - * Effect.sync(() => { - * // Send batch to remote logging service - * console.log(`Sending ${entries.length} log entries to remote service`) - * }) - * }) + * yield* Effect.log("Event 1").pipe(Effect.provide(Logger.layer([logger]))) + * yield* Effect.log("Event 2").pipe(Effect.provide(Logger.layer([logger]))) + * })) + * await Effect.runPromise(program) + * flushed // => [["Event 1", "Event 2"]] * ``` * * @category constructors @@ -824,35 +762,11 @@ export const batched = dual< * * **Example** (Logging with pretty console output) * - * ```ts - * import { Effect, Logger } from "effect" - * - * // Use the pretty console logger with default settings - * const basicPretty = Effect.log("Hello Pretty Format").pipe( - * Effect.provide(Logger.layer([Logger.consolePretty()])) - * ) - * - * // Configure pretty logger options - * const customPretty = Logger.consolePretty({ - * colors: true, - * stderr: false, - * mode: "tty", - * formatDate: (date) => date.toLocaleTimeString() - * }) - * - * // Perfect for development environment - * const developmentProgram = Effect.gen(function*() { - * yield* Effect.log("Application starting") - * yield* Effect.logInfo("Database connected") - * yield* Effect.logWarning("High memory usage detected") - * }).pipe( - * Effect.annotateLogs("environment", "development"), - * Effect.withLogSpan("startup"), - * Effect.provide(Logger.layer([customPretty])) - * ) + * ```ts import.meta.vitest + * import { Logger } from "effect" * - * // Disable colors for CI/CD environments - * const ciLogger = Logger.consolePretty({ colors: false }) + * const prettyLogger = Logger.consolePretty({ colors: false }) + * Logger.isLogger(prettyLogger) // => true * ``` * * @category constructors @@ -878,33 +792,10 @@ export const consolePretty: ( * * **Example** (Logging logfmt output to the console) * - * ```ts - * import { Effect, Logger } from "effect" - * - * // Use the console logfmt logger - * const logfmtProgram = Effect.log("Hello LogFmt Console").pipe( - * Effect.provide(Logger.layer([Logger.consoleLogFmt])) - * ) - * - * // Great for production environments - * const productionProgram = Effect.gen(function*() { - * yield* Effect.log("Server started", { port: 8080, version: "1.0.0" }) - * yield* Effect.logInfo("Request processed", { userId: 123, duration: 45 }) - * yield* Effect.logError("Validation failed", { - * field: "email", - * value: "invalid" - * }) - * }).pipe( - * Effect.annotateLogs("service", "api"), - * Effect.withLogSpan("request-handler"), - * Effect.provide(Logger.layer([Logger.consoleLogFmt])) - * ) + * ```ts import.meta.vitest + * import { Logger } from "effect" * - * // Combine with other loggers - * const multiLoggerLive = Logger.layer([ - * Logger.consoleLogFmt, - * Logger.consolePretty() - * ]) + * Logger.isLogger(Logger.consoleLogFmt) // => true * ``` * * @category constructors @@ -926,41 +817,10 @@ export const consoleLogFmt: Logger = withConsoleLog(formatLogFmt) * * **Example** (Logging structured output to the console) * - * ```ts - * import { Effect, Logger } from "effect" - * - * // Use the console structured logger - * const structuredProgram = Effect.log("Hello Structured Console").pipe( - * Effect.provide(Logger.layer([Logger.consoleStructured])) - * ) - * - * // Perfect for development debugging - * const debugProgram = Effect.gen(function*() { - * yield* Effect.log("User event", { - * userId: 123, - * action: "login", - * ip: "192.168.1.1" - * }) - * yield* Effect.logInfo("API call", { - * endpoint: "/users", - * method: "GET", - * duration: 120 - * }) - * }).pipe( - * Effect.annotateLogs("requestId", "req-123"), - * Effect.withLogSpan("authentication"), - * Effect.provide(Logger.layer([Logger.consoleStructured])) - * ) + * ```ts import.meta.vitest + * import { Logger } from "effect" * - * // Easy to parse and inspect object structure - * const inspectionProgram = Effect.gen(function*() { - * yield* Effect.log("Complex data", { - * user: { id: 1, name: "John" }, - * metadata: { source: "api", version: 2 } - * }) - * }).pipe( - * Effect.provide(Logger.layer([Logger.consoleStructured])) - * ) + * Logger.isLogger(Logger.consoleStructured) // => true * ``` * * @category constructors @@ -980,46 +840,10 @@ export const consoleStructured: Logger = withConsoleLog(formatStr * * **Example** (Logging JSON output to the console) * - * ```ts - * import { Effect, Logger } from "effect" - * - * // Use the console JSON logger - * const jsonProgram = Effect.log("Hello JSON Console").pipe( - * Effect.provide(Logger.layer([Logger.consoleJson])) - * ) - * - * // Perfect for production logging and log aggregation - * const productionProgram = Effect.gen(function*() { - * yield* Effect.log("Server started", { port: 3000, env: "production" }) - * yield* Effect.logInfo("Request", { - * method: "POST", - * url: "/api/users", - * body: { name: "Alice" } - * }) - * yield* Effect.logError("Database error", { - * error: "Connection timeout", - * retryCount: 3 - * }) - * }).pipe( - * Effect.annotateLogs("service", "user-api"), - * Effect.annotateLogs("version", "1.2.3"), - * Effect.withLogSpan("request-processing"), - * Effect.provide(Logger.layer([Logger.consoleJson])) - * ) + * ```ts import.meta.vitest + * import { Logger } from "effect" * - * // Easy to pipe to log aggregation services - * const productionSetup = Logger.layer([ - * Logger.consoleJson, // For stdout JSON logs - * Logger.consolePretty() // For local debugging - * ]) - * - * // Ideal for containerized environments (Docker, Kubernetes) - * const containerProgram = Effect.log("Container ready", { - * containerId: "abc123", - * image: "myapp:latest" - * }).pipe( - * Effect.provide(Logger.layer([Logger.consoleJson])) - * ) + * Logger.isLogger(Logger.consoleJson) // => true * ``` * * @category constructors @@ -1042,40 +866,14 @@ export const consoleJson: Logger = withConsoleLog(formatJson) * * **Example** (Recording logs as trace span events) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * - * // Tracer logger is included by default - logs automatically become span events - * const defaultProgram = Effect.gen(function*() { - * yield* Effect.log("This automatically becomes a span event") - * yield* Effect.logInfo("Processing data") - * }) - * - * // Explicitly combine tracer logger with other loggers - * const observabilityProgram = Effect.gen(function*() { - * yield* Effect.log("Operation started") - * yield* Effect.logInfo("Processing data") - * yield* Effect.logError("Error occurred") - * }).pipe( - * Effect.withLogSpan("data-processing"), - * Effect.provide(Logger.layer([ - * Logger.tracerLogger, - * Logger.consoleJson - * ])) - * ) - * - * // Perfect for correlating logs with traces in distributed systems - * const distributedProgram = Effect.gen(function*() { - * yield* Effect.log("Step 1: Fetching user data") - * yield* Effect.sleep("100 millis") - * yield* Effect.log("Step 2: Processing payment") - * yield* Effect.sleep("200 millis") - * yield* Effect.log("Step 3: Sending confirmation") - * }).pipe( - * Effect.withLogSpan("payment-workflow"), - * Effect.annotateLogs("userId", "user-123"), + * const program = Effect.log("span event").pipe( + * Effect.withSpan("operation"), * Effect.provide(Logger.layer([Logger.tracerLogger])) * ) + * Effect.runSync(program) * ``` * * @category constructors @@ -1094,37 +892,23 @@ export const tracerLogger: Logger = effect.tracerLogger * * **Example** (Providing logger layers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Logger } from "effect" * - * // Single logger layer - * const JsonLoggerLive = Logger.layer([Logger.consoleJson]) - * - * // Multiple loggers layer - * const MultiLoggerLive = Logger.layer([ - * Logger.consoleJson, - * Logger.consolePretty(), - * Logger.formatStructured - * ]) - * - * // Merge with existing loggers - * const AdditionalLoggerLive = Logger.layer( - * [Logger.consoleJson], - * { mergeWithExisting: true } - * ) - * - * // Using multiple logger formats - * const jsonLogger = Logger.consoleJson - * const prettyLogger = Logger.consolePretty() - * - * const CustomLoggerLive = Logger.layer([jsonLogger, prettyLogger]) + * const messages: Array = [] + * const customLogger = Logger.make((options) => { + * messages.push(options.message) + * }) + * const CustomLoggerLive = Logger.layer([customLogger]) * * const program = Effect.log("Application started").pipe( * Effect.provide(CustomLoggerLive) * ) + * Effect.runSync(program) + * messages // => [["Application started"]] * ``` * - * @category context + * @category layers * @since 4.0.0 */ export const layer = < @@ -1162,91 +946,59 @@ export const layer = < * * **Example** (Writing JSON logs to a file) * - * ```ts - * import { Effect, Layer, Logger } from "effect" - * import { NodeFileSystem, NodeRuntime } from "@effect/platform-node" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Logger } from "effect" * - * const fileLogger = Logger.formatJson.pipe( - * Logger.toFile("/tmp/log.txt") - * ) - * const LoggerLive = Logger.layer([fileLogger]).pipe( - * Layer.provide(NodeFileSystem.layer) - * ) - * - * Effect.log("a").pipe( - * Effect.andThen(Effect.log("b")), - * Effect.andThen(Effect.log("c")), - * Effect.provide(LoggerLive), - * NodeRuntime.runMain - * ) + * const writes: Array = [] + * const file = { + * write: (buffer: Uint8Array) => Effect.sync(() => { + * writes.push(new TextDecoder().decode(buffer).trim()) + * return FileSystem.Size(buffer.length) + * }) + * } as unknown as FileSystem.File + * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) + * const messageLogger = Logger.make((options) => String(options.message)) + * + * const program = Effect.scoped(Effect.gen(function*() { + * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/log.txt") + * yield* Effect.log("a").pipe(Effect.provide(Logger.layer([fileLogger]))) + * yield* Effect.log("b").pipe(Effect.provide(Logger.layer([fileLogger]))) + * yield* Effect.log("c").pipe(Effect.provide(Logger.layer([fileLogger]))) + * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) + * + * await Effect.runPromise(program) + * writes // => ["a\nb\nc"] * ``` * * **Example** (Writing logs to files) * - * ```ts - * import { Duration, Effect, Logger } from "effect" - * import { NodeFileSystem } from "@effect/platform-node" - * - * // Basic file logging. The scope keeps the file open while logs are emitted - * // and flushes pending entries when it closes. - * const basicFileLogger = Effect.scoped( - * Effect.gen(function*() { - * const fileLogger = yield* Logger.formatJson.pipe( - * Logger.toFile("/tmp/app.log") - * ) - * - * yield* Effect.log("Application started").pipe( - * Effect.provide(Logger.layer([fileLogger])) - * ) - * }) - * ).pipe( - * Effect.provide(NodeFileSystem.layer) - * ) + * ```ts import.meta.vitest + * import { Effect, FileSystem, Logger } from "effect" * - * // File logger with custom batch window - * const batchedFileLogger = Effect.scoped( - * Effect.gen(function*() { - * const fileLogger = yield* Logger.formatLogFmt.pipe( - * Logger.toFile("/var/log/myapp.log", { - * flag: "a", - * batchWindow: Duration.seconds(5) - * }) - * ) - * - * yield* Effect.all([ - * Effect.log("Event 1"), - * Effect.log("Event 2"), - * Effect.log("Event 3") - * ]).pipe( - * Effect.provide(Logger.layer([fileLogger])) - * ) + * const writes: Array = [] + * const file = { + * write: (buffer: Uint8Array) => Effect.sync(() => { + * writes.push(new TextDecoder().decode(buffer).trim()) + * return FileSystem.Size(buffer.length) * }) - * ).pipe( - * Effect.provide(NodeFileSystem.layer) - * ) + * } as unknown as FileSystem.File + * const fileSystem = FileSystem.makeNoop({ open: () => Effect.succeed(file) }) + * const messageLogger = Logger.make((options) => String(options.message)) * - * // Multiple loggers: console + file - * const multiLogger = Effect.scoped( - * Effect.gen(function*() { - * const fileLogger = yield* Logger.formatJson.pipe( - * Logger.toFile("/tmp/production.log") - * ) - * - * const loggerLive = Logger.layer([ - * Logger.consolePretty(), - * fileLogger - * ]) - * - * yield* Effect.log("Production event").pipe( - * Effect.provide(loggerLive) - * ) + * const program = Effect.scoped(Effect.gen(function*() { + * const fileLogger = yield* Logger.toFile(messageLogger, "/tmp/app.log", { + * batchWindow: "1 hour" * }) - * ).pipe( - * Effect.provide(NodeFileSystem.layer) - * ) + * yield* Effect.log("Application started").pipe( + * Effect.provide(Logger.layer([fileLogger])) + * ) + * })).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)) + * + * await Effect.runPromise(program) + * writes // => ["Application started"] * ``` * - * @category file + * @category logging * @since 4.0.0 */ export const toFile = dual< diff --git a/.context/effect/packages/effect/src/ManagedRuntime.ts b/.context/effect/packages/effect/src/ManagedRuntime.ts index f3252b780..90117459f 100644 --- a/.context/effect/packages/effect/src/ManagedRuntime.ts +++ b/.context/effect/packages/effect/src/ManagedRuntime.ts @@ -207,6 +207,16 @@ export interface ManagedRuntime { */ readonly dispose: () => Promise + /** + * Dispose of the resources associated with the runtime. + * + * **When to use** + * + * Use with the `await using` syntax to automatically dispose the runtime + * when it goes out of scope. + */ + readonly [Symbol.asyncDispose]: () => Promise + /** * Dispose of the resources associated with the runtime. * @@ -239,15 +249,17 @@ export interface ManagedRuntime { * * **Example** (Creating a managed runtime) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Layer, ManagedRuntime } from "effect" * + * const notifications: Array = [] + * * class Notifications extends Context.Service Effect.Effect * }>()("Notifications") { * static readonly layer = Layer.succeed(this)({ * notify: Effect.fn("Notifications.notify")((message) => - * Effect.sync(() => console.log(message)) + * Effect.sync(() => notifications.push(message)) * ) * }) * } @@ -259,15 +271,15 @@ export interface ManagedRuntime { * (_) => _.notify("Hello, world!") * ).pipe(Effect.ensuring(runtime.disposeEffect)) * - * runtime.runPromise(program) - * // Hello, world! + * await runtime.runPromise(program) + * notifications // => ["Hello, world!"] * ``` * * @see {@link ManagedRuntime} for the returned runtime interface * @see {@link Layer.MemoMap} for shared layer memoization * @see {@link Layer.build} for lower-level scoped layer construction * - * @category runtime class + * @category constructors * @since 2.0.0 */ export const make = ( @@ -324,6 +336,9 @@ export const make = ( dispose(): Promise { return Effect.runPromise(self.disposeEffect) }, + [Symbol.asyncDispose](): Promise { + return self.dispose() + }, disposeEffect: Effect.suspend(() => { ;(self as Mutable>).contextEffect = Effect.die("ManagedRuntime disposed") self.cachedContext = undefined diff --git a/.context/effect/packages/effect/src/Match.ts b/.context/effect/packages/effect/src/Match.ts index 2dfe2af23..b97cdd795 100644 --- a/.context/effect/packages/effect/src/Match.ts +++ b/.context/effect/packages/effect/src/Match.ts @@ -30,7 +30,7 @@ const TypeId = internal.TypeId * * **Example** (Matching string and number values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Simulated dynamic input that can be a string or a number @@ -47,8 +47,7 @@ const TypeId = internal.TypeId * Match.exhaustive * ) * - * console.log(result) - * // Output: "string: some input" + * result // => "string: some input" * ``` * * @category models @@ -70,7 +69,7 @@ export type Matcher "String: hello" + * matcher(42) // => "Number: 42" * ``` * * @category models @@ -111,7 +110,7 @@ export interface TypeMatcher "Unknown type") * ) * - * console.log(result) // "User: Alice" + * result // => "User: Alice" * ``` * * @category models @@ -177,7 +176,7 @@ export type Case = When | Not * * **Example** (Creating positive match cases) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // When creates cases that match specific patterns @@ -187,8 +186,8 @@ export type Case = When | Not * Match.exhaustive * ) * - * console.log(stringMatcher("hello")) // "Got string: hello" - * console.log(stringMatcher(42)) // "Got number: 42" + * stringMatcher("hello") // => "Got string: hello" + * stringMatcher(42) // => "Got number: 42" * ``` * * @category models @@ -211,7 +210,7 @@ export interface When { * * **Example** (Creating negative match cases) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Not creates cases that exclude specific patterns @@ -221,8 +220,8 @@ export interface When { * Match.orElse(() => "This string is forbidden") * ) * - * console.log(matcher("hello")) // "Allowed: hello" - * console.log(matcher("forbidden")) // "This string is forbidden" + * matcher("hello") // => "Allowed: hello" + * matcher("forbidden") // => "This string is forbidden" * ``` * * @category models @@ -250,7 +249,7 @@ export interface Not { * * **Example** (Matching Numbers and Strings) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Create a matcher for values that are either strings or numbers @@ -266,11 +265,9 @@ export interface Not { * Match.exhaustive * ) * - * console.log(match(0)) - * // Output: "number: 0" + * match(0) // => "number: 0" * - * console.log(match("hello")) - * // Output: "string: hello" + * match("hello") // => "string: hello" * ``` * * @see {@link value} for creating a matcher from a specific value. @@ -299,7 +296,7 @@ export const type: () => Matcher, I, never, never> = * * **Example** (Matching an Object by Property) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const input = { name: "John", age: 30 } @@ -315,8 +312,7 @@ export const type: () => Matcher, I, never, never> = * Match.orElse(() => "Oh, not John") * ) * - * console.log(result) - * // Output: "John is 30 years old" + * result // => "John is 30 years old" * ``` * * @see {@link type} for creating a matcher from a specific type. @@ -339,7 +335,7 @@ export const value: ( * * **Example** (Matching value tags) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type Status = { readonly _tag: "Success"; readonly data: string } @@ -351,7 +347,7 @@ export const value: ( * Success: (result) => `Success: ${result.data}` * }) * - * console.log(message) // "Success: Hello" + * message // => "Success: Hello" * ``` * * @category constructors @@ -383,7 +379,7 @@ export const valueTags: { * * **Example** (Matching type tags) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type Result = @@ -398,11 +394,9 @@ export const valueTags: { * Loading: () => "Loading..." * }) * - * console.log(formatResult({ _tag: "Success", data: "Hello World" })) - * // Output: "Data: Hello World" + * formatResult({ _tag: "Success", data: "Hello World" }) // => "Data: Hello World" * - * console.log(formatResult({ _tag: "Error", message: "Network failed" })) - * // Output: "Error: Network failed" + * formatResult({ _tag: "Error", message: "Network failed" }) // => "Error: Network failed" * * // Create a matcher with inferred return type * const processResult = Match.typeTags()({ @@ -411,8 +405,7 @@ export const valueTags: { * Loading: () => ({ type: "pending" }) * }) * - * console.log(processResult({ _tag: "Loading" })) - * // Output: { type: "pending" } + * processResult({ _tag: "Loading" }) // => { type: "pending" } * ``` * * @category constructors @@ -453,7 +446,7 @@ export const typeTags: { * * **Example** (Validating return type consistency) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const match = Match.type<{ a: number } | { b: string }>().pipe( @@ -493,7 +486,7 @@ export const withReturnType: () => ( * * **Example** (Matching with values and predicates) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Create a matcher for objects with an "age" property @@ -509,14 +502,11 @@ export const withReturnType: () => ( * Match.orElse((user: { age: number }) => `${user.age} is too young`) * ) * - * console.log(match({ age: 20 })) - * // Output: "Age: 20" + * match({ age: 20 }) // => "Age: 20" * - * console.log(match({ age: 18 })) - * // Output: "You can vote" + * match({ age: 18 }) // => "You can vote" * - * console.log(match({ age: 4 })) - * // Output: "4 is too young" + * match({ age: 4 }) // => "4 is too young" * ``` * * @see {@link whenOr} for handling any one of several patterns with the same handler @@ -524,7 +514,7 @@ export const withReturnType: () => ( * @see {@link not} for handling inputs that do not match a pattern * @see {@link orElse} for providing a fallback when no pattern case matches * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const when: < @@ -561,7 +551,7 @@ export const when: < * * **Example** (Matching one of several patterns) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type ErrorType = @@ -579,14 +569,12 @@ export const when: < * Match.exhaustive * ) * - * console.log(handleError({ _tag: "NetworkError", message: "No connection" })) - * // Output: "Retry the request" + * handleError({ _tag: "NetworkError", message: "No connection" }) // => "Retry the request" * - * console.log(handleError({ _tag: "ValidationError", field: "email" })) - * // Output: "Invalid field: email" + * handleError({ _tag: "ValidationError", field: "email" }) // => "Invalid field: email" * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const whenOr: < @@ -623,7 +611,7 @@ export const whenOr: < * * **Example** (Matching all provided patterns) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type User = { readonly age: number; readonly role: "admin" | "user" } @@ -637,14 +625,12 @@ export const whenOr: < * Match.orElse(() => "Access denied") * ) * - * console.log(checkUser({ age: 20, role: "admin" })) - * // Output: "Admin access granted" + * checkUser({ age: 20, role: "admin" }) // => "Admin access granted" * - * console.log(checkUser({ age: 20, role: "user" })) - * // Output: "Access denied" + * checkUser({ age: 20, role: "user" }) // => "Access denied" * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const whenAnd: < @@ -681,7 +667,7 @@ export const whenAnd: < * * **Example** (Matching on a discriminator field) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -695,12 +681,14 @@ export const whenAnd: < * Match.discriminator("type")("C", (_) => `C(${_.c})`), * Match.exhaustive * ) + * match({ type: "A", a: "ok" }) // => "A or B: A" + * match({ type: "C", c: true }) // => "C(true)" * ``` * * @see {@link discriminators} for defining several discriminator handlers at once * @see {@link discriminatorStartsWith} for matching string discriminator values by prefix * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const discriminator: ( @@ -734,7 +722,7 @@ export const discriminator: ( * * **Example** (Matching discriminator prefixes) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -744,14 +732,14 @@ export const discriminator: ( * Match.orElse((_) => 3 as const) * ) * - * console.log(match({ type: "A" })) // 1 - * console.log(match({ type: "B" })) // 2 - * console.log(match({ type: "A.A" })) // 1 + * match({ type: "A" }) // => 1 + * match({ type: "B" }) // => 2 + * match({ type: "A.A" }) // => 1 * ``` * * @see {@link discriminator} for matching exact discriminator values * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const discriminatorStartsWith: ( @@ -790,7 +778,7 @@ export const discriminatorStartsWith: ( * * **Example** (Mapping discriminator handlers) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -807,12 +795,14 @@ export const discriminatorStartsWith: ( * }), * Match.exhaustive * ) + * match({ type: "A", a: "ok" }) // => "ok" + * match({ type: "B", b: 42 }) // => 42 * ``` * * @see {@link discriminator} for adding one discriminator case to a matcher pipeline * @see {@link discriminatorsExhaustive} for handling every discriminator value and finalizing the matcher * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const discriminators: ( @@ -854,7 +844,7 @@ export const discriminators: ( * * **Example** (Handling all discriminator cases) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -870,11 +860,12 @@ export const discriminators: ( * C: (c) => c.c * }) * ) + * match({ type: "C", c: true }) // => true * ``` * * @see {@link discriminators} for defining discriminator handlers without finalizing the matcher * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const discriminatorsExhaustive: ( @@ -907,7 +898,7 @@ export const discriminatorsExhaustive: ( * * **Example** (Matching a discriminated union by tag) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type Event = @@ -926,14 +917,12 @@ export const discriminatorsExhaustive: ( * Match.exhaustive * ) * - * console.log(match({ _tag: "success", data: "Hello" })) - * // Output: "Ok!" + * match({ _tag: "success", data: "Hello" }) // => "Ok!" * - * console.log(match({ _tag: "error", error: new Error("Oops!") })) - * // Output: "Error: Oops!" + * match({ _tag: "error", error: new Error("Oops!") }) // => "Error: Oops!" * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const tag: < @@ -966,7 +955,7 @@ export const tag: < * * **Example** (Matching tag prefixes) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -976,12 +965,12 @@ export const tag: < * Match.orElse((_) => 3 as const) * ) * - * console.log(match({ _tag: "A" })) // 1 - * console.log(match({ _tag: "B" })) // 2 - * console.log(match({ _tag: "A.A" })) // 1 + * match({ _tag: "A" }) // => 1 + * match({ _tag: "B" }) // => 2 + * match({ _tag: "A.A" }) // => 1 * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const tagStartsWith: < @@ -1016,7 +1005,7 @@ export const tagStartsWith: < * * **Example** (Mapping tag handlers) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -1033,9 +1022,10 @@ export const tagStartsWith: < * }), * Match.exhaustive * ) + * match({ _tag: "A", a: "ok" }) // => "ok" * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const tags: < @@ -1071,7 +1061,7 @@ export const tags: < * * **Example** (Handling all tag cases) * - * ```ts + * ```ts import.meta.vitest * import { Match, pipe } from "effect" * * const match = pipe( @@ -1087,9 +1077,10 @@ export const tags: < * C: (c) => c.c * }) * ) + * match({ _tag: "B", b: 42 }) // => 42 * ``` * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const tagsExhaustive: < @@ -1120,7 +1111,7 @@ export const tagsExhaustive: < * * **Example** (Ignoring a specific value) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Create a matcher for string or number values @@ -1131,16 +1122,14 @@ export const tagsExhaustive: < * Match.orElse(() => "fallback") * ) * - * console.log(match("hello")) - * // Output: "ok" + * match("hello") // => "ok" * - * console.log(match("hi")) - * // Output: "fallback" + * match("hi") // => "fallback" * ``` * * @see {@link when} for adding a positive pattern case * - * @category Defining patterns + * @category defining patterns * @since 4.0.0 */ export const not: < @@ -1176,7 +1165,7 @@ export const not: < * * **Example** (Matching non-empty strings) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const processInput = Match.type() @@ -1185,19 +1174,16 @@ export const not: < * Match.orElse(() => "Input cannot be empty") * ) * - * console.log(processInput("hello")) - * // Output: "Valid input: hello" + * processInput("hello") // => "Valid input: hello" * - * console.log(processInput("")) - * // Output: "Input cannot be empty" + * processInput("") // => "Input cannot be empty" * - * console.log(processInput(" ")) - * // Output: "Valid input: " (whitespace-only strings are considered non-empty) + * processInput(" ") // => "Valid input: " * ``` * * @see {@link string} for matching any string * - * @category predicates + * @category guards * @since 4.0.0 */ export const nonEmptyString: SafeRefinement = internal.nonEmptyString @@ -1216,7 +1202,7 @@ export const nonEmptyString: SafeRefinement = internal.nonEmptySt * * **Example** (Matching literal values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const handleStatus = Match.type() @@ -1227,23 +1213,18 @@ export const nonEmptyString: SafeRefinement = internal.nonEmptySt * Match.orElse((value) => `Unknown status: ${value}`) * ) * - * console.log(handleStatus("success")) - * // Output: "Operation successful" + * handleStatus("success") // => "Operation successful" * - * console.log(handleStatus(200)) - * // Output: "Operation successful" + * handleStatus(200) // => "Operation successful" * - * console.log(handleStatus("failed")) - * // Output: "Operation failed" + * handleStatus("failed") // => "Operation failed" * - * console.log(handleStatus(0)) - * // Output: "Falsy value" + * handleStatus(0) // => "Falsy value" * - * console.log(handleStatus("pending")) - * // Output: "Unknown status: pending" + * handleStatus("pending") // => "Unknown status: pending" * ``` * - * @category predicates + * @category guards * @since 4.0.0 */ export const is: < @@ -1260,7 +1241,7 @@ export const is: < * * **Example** (Matching string values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const processValue = Match.type().pipe( @@ -1270,12 +1251,12 @@ export const is: < * Match.exhaustive * ) * - * console.log(processValue("hello")) // "String: HELLO" - * console.log(processValue(42)) // "Number: 84" - * console.log(processValue(true)) // "Boolean: yes" + * processValue("hello") // => "String: HELLO" + * processValue(42) // => "Number: 84" + * processValue(true) // => "Boolean: yes" * ``` * - * @category predicates + * @category guards * @since 4.0.0 */ export const string: Predicate.Refinement = Predicate.isString @@ -1295,7 +1276,7 @@ export const string: Predicate.Refinement = Predicate.isString * * **Example** (Matching number values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const categorizeNumber = Match.type().pipe( @@ -1308,15 +1289,15 @@ export const string: Predicate.Refinement = Predicate.isString * Match.orElse(() => "Not a number type") * ) * - * console.log(categorizeNumber(42)) // "Integer: 42" - * console.log(categorizeNumber(3.14)) // "Float: 3.14" - * console.log(categorizeNumber(NaN)) // "Not a number" - * console.log(categorizeNumber("hello")) // "Not a number type" + * categorizeNumber(42) // => "Integer: 42" + * categorizeNumber(3.14) // => "Float: 3.14" + * categorizeNumber(NaN) // => "Not a number" + * categorizeNumber("hello") // => "Not a number type" * ``` * * @see {@link bigint} for matching primitive bigint values * - * @category predicates + * @category guards * @since 4.0.0 */ export const number: Predicate.Refinement = Predicate.isNumber @@ -1341,7 +1322,7 @@ export const number: Predicate.Refinement = Predicate.isNumber * * **Example** (Matching any remaining value) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const describeValue = Match.type() @@ -1353,23 +1334,19 @@ export const number: Predicate.Refinement = Predicate.isNumber * Match.exhaustive * ) * - * console.log(describeValue("hello")) - * // Output: "String: hello" + * describeValue("hello") // => "String: hello" * - * console.log(describeValue(42)) - * // Output: "Number: 42" + * describeValue(42) // => "Number: 42" * - * console.log(describeValue([1, 2, 3])) - * // Output: "Other: object" + * describeValue([1, 2, 3]) // => "Other: object" * - * console.log(describeValue(null)) - * // Output: "Other: object" + * describeValue(null) // => "Other: object" * ``` * * @see {@link defined} for matching only non-nullish values * @see {@link orElse} for providing a fallback after earlier cases * - * @category predicates + * @category guards * @since 4.0.0 */ export const any: SafeRefinement = internal.any @@ -1388,7 +1365,7 @@ export const any: SafeRefinement = internal.any * * **Example** (Matching defined values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const processValue = Match.type() @@ -1397,28 +1374,22 @@ export const any: SafeRefinement = internal.any * Match.orElse(() => "Value is null or undefined") * ) * - * console.log(processValue("hello")) - * // Output: "Defined value: hello" + * processValue("hello") // => "Defined value: hello" * - * console.log(processValue(42)) - * // Output: "Defined value: 42" + * processValue(42) // => "Defined value: 42" * - * console.log(processValue(0)) - * // Output: "Defined value: 0" + * processValue(0) // => "Defined value: 0" * - * console.log(processValue("")) - * // Output: "Defined value: " + * processValue("") // => "Defined value: " * - * console.log(processValue(null)) - * // Output: "Value is null or undefined" + * processValue(null) // => "Value is null or undefined" * - * console.log(processValue(undefined)) - * // Output: "Value is null or undefined" + * processValue(undefined) // => "Value is null or undefined" * ``` * * @see {@link any} for matching every value without excluding nullish inputs * - * @category predicates + * @category guards * @since 4.0.0 */ export const defined: (u: A) => u is A & {} = internal.defined @@ -1437,7 +1408,7 @@ export const defined: (u: A) => u is A & {} = internal.defined * * **Example** (Matching boolean values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const describeTruthiness = Match.type().pipe( @@ -1451,15 +1422,15 @@ export const defined: (u: A) => u is A & {} = internal.defined * Match.orElse(() => "Some other truthy value") * ) * - * console.log(describeTruthiness(true)) // "Definitely true" - * console.log(describeTruthiness(false)) // "Definitely false" - * console.log(describeTruthiness(0)) // "Falsy number" - * console.log(describeTruthiness(1)) // "Some other truthy value" + * describeTruthiness(true) // => "Definitely true" + * describeTruthiness(false) // => "Definitely false" + * describeTruthiness(0) // => "Falsy number" + * describeTruthiness(1) // => "Some other truthy value" * ``` * * @see {@link is} for matching specific literal boolean values * - * @category predicates + * @category guards * @since 4.0.0 */ export const boolean: Predicate.Refinement = Predicate.isBoolean @@ -1481,7 +1452,7 @@ export { * @see {@link defined} for matching non-nullish values * @see {@link is} for matching literal values * - * @category predicates + * @category guards * @since 4.0.0 */ _undefined as undefined @@ -1504,7 +1475,7 @@ export { * @see {@link defined} for matching non-nullish values * @see {@link is} for matching literal values * - * @category predicates + * @category guards * @since 4.0.0 */ _null as null @@ -1524,7 +1495,7 @@ export { * * **Example** (Matching bigint values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const processLargeNumber = Match.type().pipe( @@ -1538,15 +1509,15 @@ export { * Match.orElse(() => "Not a numeric type") * ) * - * console.log(processLargeNumber(123n)) // "BigInt: 123" - * console.log(processLargeNumber(9007199254740992n)) // "Large integer: 9007199254740992" - * console.log(processLargeNumber(123)) // "Regular number: 123" - * console.log(processLargeNumber("123")) // "Not a numeric type" + * processLargeNumber(123n) // => "BigInt: 123" + * processLargeNumber(9007199254740992n) // => "Large integer: 9007199254740992" + * processLargeNumber(123) // => "Regular number: 123" + * processLargeNumber("123") // => "Not a numeric type" * ``` * * @see {@link number} for matching primitive number values * - * @category predicates + * @category guards * @since 4.0.0 */ export const bigint: Predicate.Refinement = Predicate.isBigInt @@ -1562,7 +1533,7 @@ export const bigint: Predicate.Refinement = Predicate.isBigInt * * **Example** (Matching symbol values) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const mySymbol = Symbol("my-symbol") @@ -1579,12 +1550,12 @@ export const bigint: Predicate.Refinement = Predicate.isBigInt * Match.orElse(() => "Not a symbol") * ) * - * console.log(handleSymbol(mySymbol)) // "Symbol with description: my-symbol" - * console.log(handleSymbol(Symbol())) // "Symbol without description" - * console.log(handleSymbol("string")) // "Not a symbol" + * handleSymbol(mySymbol) // => "Symbol with description: my-symbol" + * handleSymbol(Symbol()) // => "Symbol without description" + * handleSymbol("string") // => "Not a symbol" * ``` * - * @category predicates + * @category guards * @since 4.0.0 */ export const symbol: Predicate.Refinement = Predicate.isSymbol @@ -1604,7 +1575,7 @@ export const symbol: Predicate.Refinement = Predicate.isSymbol * * **Example** (Matching Date instances) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const processDateValue = Match.type().pipe( @@ -1618,15 +1589,15 @@ export const symbol: Predicate.Refinement = Predicate.isSymbol * Match.orElse(() => "Not a date-related value") * ) * - * console.log(processDateValue(new Date("2024-01-01"))) // "Date: 2024-01-01" - * console.log(processDateValue(new Date("invalid"))) // "Invalid date" - * console.log(processDateValue("2024-01-01")) // "Date string: 2024-01-01" - * console.log(processDateValue(1704067200000)) // "Not a date-related value" + * processDateValue(new Date("2024-01-01")) // => "Date: 2024-01-01" + * processDateValue(new Date("invalid")) // => "Invalid date" + * processDateValue("2024-01-01") // => "Date string: 2024-01-01" + * processDateValue(1704067200000) // => "Not a date-related value" * ``` * * @see {@link instanceOf} for matching instances of any constructor * - * @category predicates + * @category guards * @since 4.0.0 */ export const date: Predicate.Refinement = Predicate.isDate @@ -1647,7 +1618,7 @@ export const date: Predicate.Refinement = Predicate.isDate * * **Example** (Matching record objects) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const analyzeValue = Match.type().pipe( @@ -1663,15 +1634,15 @@ export const date: Predicate.Refinement = Predicate.isDate * Match.orElse(() => "Not an object") * ) * - * console.log(analyzeValue({ name: "Alice", age: 30 })) // "Object with 2 properties: [name, age]" - * console.log(analyzeValue([1, 2, 3])) // "Array with 3 items" - * console.log(analyzeValue(null)) // "Not an object" - * console.log(analyzeValue("hello")) // "Not an object" + * analyzeValue({ name: "Alice", age: 30 }) // => "Object with 2 properties: [name, age]" + * analyzeValue([1, 2, 3]) // => "Array with 3 items" + * analyzeValue(null) // => "Not an object" + * analyzeValue("hello") // => "Not an object" * ``` * * @see {@link instanceOf} for matching a specific constructor * - * @category predicates + * @category guards * @since 4.0.0 */ export const record: Predicate.Refinement = Predicate.isObject @@ -1691,7 +1662,7 @@ export const record: Predicate.Refinement `Other: ${typeof value}`) * ) * - * console.log(handleValue(new CustomError("Failed", 404))) // "Custom error: Failed (code: 404)" - * console.log(handleValue(new Error("Generic error"))) // "Standard error: Generic error" - * console.log(handleValue([1, 2, 3])) // "Array with 3 items" - * console.log(handleValue(new Map([["count", 1]]))) // "Map with 1 entries" + * handleValue(new CustomError("Failed", 404)) // => "Custom error: Failed (code: 404)" + * handleValue(new Error("Generic error")) // => "Standard error: Generic error" + * handleValue([1, 2, 3]) // => "Array with 3 items" + * handleValue(new Map([["count", 1]])) // => "Map with 1 entries" * ``` * * @see {@link instanceOfUnsafe} for constructor matching without the same type-safety guarantee * @see {@link record} for matching broad non-null, non-array objects * - * @category predicates + * @category guards * @since 4.0.0 */ export const instanceOf: any>( @@ -1752,7 +1723,7 @@ export const instanceOf: any>( * * **Example** (Matching class instances unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * class CustomError extends Error { @@ -1770,11 +1741,12 @@ export const instanceOf: any>( * }), * Match.orElse(() => "Not a CustomError") * ) + * handleError(new CustomError("failed", 500)) // => "Custom error 500: failed" * ``` * * @see {@link instanceOf} for type-safe constructor matching * - * @category predicates + * @category guards * @since 4.0.0 */ export const instanceOfUnsafe: any>( @@ -1797,7 +1769,7 @@ export const instanceOfUnsafe: any>( * * **Example** (Providing a default value when no patterns match) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Create a matcher for string or number values @@ -1808,11 +1780,9 @@ export const instanceOfUnsafe: any>( * Match.orElse(() => "fallback") * ) * - * console.log(match("a")) - * // Output: "ok" + * match("a") // => "ok" * - * console.log(match("b")) - * // Output: "fallback" + * match("b") // => "fallback" * ``` * * @see {@link option} for finalizing unmatched input as `Option.none` @@ -1849,7 +1819,7 @@ export const orElse: Ret>( * * **Example** (Throwing on unmatched input) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * const strictMatcher = Match.type<"a" | "b">().pipe( @@ -1859,8 +1829,8 @@ export const orElse: Ret>( * Match.orElseAbsurd * ) * - * console.log(strictMatcher("a")) // "Found A" - * console.log(strictMatcher("b")) // "Found B" + * strictMatcher("a") // => "Found A" + * strictMatcher("b") // => "Found B" * * // This would throw an error at runtime: * // strictMatcher("c" as any) // throws @@ -1893,7 +1863,7 @@ export const orElseAbsurd: ( * * **Example** (Extracting a user role with `Match.result`) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type User = { readonly role: "admin" | "editor" | "viewer" } @@ -1905,11 +1875,9 @@ export const orElseAbsurd: ( * Match.result // Wrap the result in an Result * ) * - * console.log(getRole({ role: "admin" })) - * // Output: { _id: 'Result', _tag: 'Ok', ok: 'Has full access' } + * getRole({ role: "admin" })._tag // => "Success" * - * console.log(getRole({ role: "viewer" })) - * // Output: { _id: 'Result', _tag: 'Err', err: { role: 'viewer' } } + * getRole({ role: "viewer" })._tag // => "Failure" * ``` * * @category completion @@ -1939,7 +1907,7 @@ export const result: ( * * **Example** (Extracting a user role with `Match.option`) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type User = { readonly role: "admin" | "editor" | "viewer" } @@ -1951,11 +1919,9 @@ export const result: ( * Match.option // Wrap the result in an Option * ) * - * console.log(getRole({ role: "admin" })) - * // Output: { _id: 'Option', _tag: 'Some', value: 'Has full access' } + * getRole({ role: "admin" })._tag // => "Some" * - * console.log(getRole({ role: "viewer" })) - * // Output: { _id: 'Option', _tag: 'None' } + * getRole({ role: "viewer" })._tag // => "None" * ``` * * @see {@link result} for preserving unmatched input as a `Result` failure @@ -1983,7 +1949,7 @@ export const option: ( * * **Example** (Ensuring all cases are covered) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Create a matcher for string or number values @@ -2017,7 +1983,7 @@ const SafeRefinementId = "~effect/match/Match/SafeRefinement" * * **Example** (Using safe refinements) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Built-in safe refinements @@ -2028,10 +1994,10 @@ const SafeRefinementId = "~effect/match/Match/SafeRefinement" * Match.orElse(() => "Undefined or null") * ) * - * console.log(processValue("hello")) // "HELLO" - * console.log(processValue(21)) // 42 - * console.log(processValue(true)) // "Defined: true" - * console.log(processValue(null)) // "Undefined or null" + * processValue("hello") // => "HELLO" + * processValue(21) // => 42 + * processValue(true) // => "Defined: true" + * processValue(null) // => "Undefined or null" * ``` * * @category models @@ -2068,7 +2034,7 @@ export declare namespace Types { * * **Example** (Computing matched types) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * // WhenMatch computes the narrowed type after pattern matching @@ -2085,7 +2051,7 @@ export declare namespace Types { * // Result: { type: "user"; name: string } * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type WhenMatch = @@ -2113,7 +2079,7 @@ export declare namespace Types { * * **Example** (Computing unmatched types) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * // NotMatch computes what remains after exclusion @@ -2127,7 +2093,7 @@ export declare namespace Types { * // Result: "b" | "c" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type NotMatch = Exclude>> @@ -2146,7 +2112,7 @@ export declare namespace Types { * * **Example** (Resolving match patterns) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * // PForMatch resolves patterns to their matched types @@ -2157,7 +2123,7 @@ export declare namespace Types { * // Result: { name: string } * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type PForMatch

= [ResolvePred

] extends [infer X] ? X @@ -2174,7 +2140,7 @@ export declare namespace Types { * * **Example** (Computing excluded patterns) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * // PForExclude computes what to exclude from type operations @@ -2185,7 +2151,7 @@ export declare namespace Types { * // Used internally to filter out admin objects * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type PForExclude

= [SafeRefinementR>] extends [infer X] ? X @@ -2239,7 +2205,7 @@ export declare namespace Types { * * **Example** (Describing complex object patterns) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // PatternBase enables complex object patterns @@ -2251,7 +2217,7 @@ export declare namespace Types { * // Allows: { name?: string | Predicate, age?: number | Predicate, ... } * * // Example usage: - * Match.value({ name: "Alice", age: 30, role: "admin" as const }).pipe( + * const result = Match.value({ name: "Alice", age: 30, role: "admin" as const }).pipe( * Match.when( * { age: (n: number) => n >= 18, role: "admin" }, * (user: { name: string; age: number; role: "admin" }) => @@ -2259,9 +2225,10 @@ export declare namespace Types { * ), * Match.orElse(() => "Not an adult admin") * ) + * result // => "Admin: Alice" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type PatternBase = A extends ReadonlyArray ? ReadonlyArray | PatternPrimitive @@ -2279,7 +2246,7 @@ export declare namespace Types { * literal values, and safe refinements. These are the atomic patterns that * can be composed into more complex matching logic. * - * @category types + * @category utility types * @since 4.0.0 */ export type PatternPrimitive = PredicateA | A | SafeRefinement @@ -2295,18 +2262,19 @@ export declare namespace Types { * * **Example** (Tracking excluded types) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Without is used internally when you write: - * Match.type().pipe( + * const match = Match.type().pipe( * Match.not(Match.string, (value) => `not string: ${value}`), * // At this point, type system uses Without to track exclusion * Match.orElse(() => "was a string") * ) + * match(42) // => "not string: 42" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export interface Without { @@ -2325,18 +2293,19 @@ export declare namespace Types { * * **Example** (Tracking included types) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // Only is used internally when you write: - * Match.type().pipe( + * const match = Match.type().pipe( * Match.when(Match.string, (s) => `string: ${s}`), * // At this point, type system uses Only for the match * Match.orElse((value) => `not string: ${value}`) * ) + * match("ok") // => "string: ok" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export interface Only { @@ -2355,19 +2324,20 @@ export declare namespace Types { * * **Example** (Accumulating excluded types) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // AddWithout is used when combining multiple exclusions: - * Match.type().pipe( + * const match = Match.type().pipe( * Match.not(Match.string, () => "not string"), * Match.not(Match.number, () => "not number"), * // Type system uses AddWithout to combine exclusions * Match.orElse(() => "was string or number") * ) + * match(true) // => "not string" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type AddWithout = [A] extends [Without] ? Without @@ -2385,18 +2355,19 @@ export declare namespace Types { * * **Example** (Refining included types) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * // AddOnly is used when refining positive matches: - * Match.type<{ type: "user" | "admin"; name: string }>().pipe( + * const match = Match.type<{ type: "user" | "admin"; name: string }>().pipe( * Match.when({ type: "admin" }, (admin) => admin.name), * // Type system uses AddOnly to refine the constraint * Match.orElse(() => "not admin") * ) + * match({ type: "admin", name: "Alice" }) // => "Alice" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type AddOnly = [A] extends [Without] ? [X] extends [WX] ? never @@ -2416,7 +2387,7 @@ export declare namespace Types { * * **Example** (Applying accumulated filters) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * // ApplyFilters computes the final narrowed type: @@ -2433,7 +2404,7 @@ export declare namespace Types { * // Result: number | boolean * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type ApplyFilters = A extends Only ? X @@ -2451,7 +2422,7 @@ export declare namespace Types { * * **Example** (Extracting discriminator tags) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * type Events = @@ -2470,7 +2441,7 @@ export declare namespace Types { * // Result: "user" | "admin" * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type Tags = P extends Record ? X : never @@ -2486,7 +2457,7 @@ export declare namespace Types { * * **Example** (Converting arrays to intersections) * - * ```ts + * ```ts import.meta.vitest * import type { Match } from "effect" * * type Combined = Match.Types.ArrayToIntersection<[ @@ -2502,7 +2473,7 @@ export declare namespace Types { * // for advanced pattern matching scenarios * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type ArrayToIntersection> = T.UnionToIntersection< @@ -2520,7 +2491,7 @@ export declare namespace Types { * * **Example** (Extracting matched types) * - * ```ts + * ```ts import.meta.vitest * import { Match } from "effect" * * type StringExtract = Match.Types.ExtractMatch< @@ -2540,7 +2511,7 @@ export declare namespace Types { * // ^^^ s is correctly typed as string * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type ExtractMatch = [ExtractAndNarrow] extends [infer EI] ? EI diff --git a/.context/effect/packages/effect/src/Metric.ts b/.context/effect/packages/effect/src/Metric.ts index d1a1cd4d2..adbf13db7 100644 --- a/.context/effect/packages/effect/src/Metric.ts +++ b/.context/effect/packages/effect/src/Metric.ts @@ -19,6 +19,7 @@ import type { Exit } from "./Exit.ts" import { constUndefined, dual } from "./Function.ts" import * as InternalEffect from "./internal/effect.ts" import * as InternalMetric from "./internal/metric.ts" +import * as InternalRecord from "./internal/record.ts" import * as Layer from "./Layer.ts" import * as Order from "./Order.ts" import type { Pipeable } from "./Pipeable.ts" @@ -48,12 +49,8 @@ import type { Contravariant, Covariant } from "./Types.ts" * * **Example** (Using multiple metric types) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricExample extends Data.TaggedError("MetricExample")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of metrics @@ -99,6 +96,13 @@ import type { Contravariant, Covariant } from "./Types.ts" * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [ + * result.counter.state.count, + * result.gauge.state.value, + * result.frequency.state.occurrences.get("200") + * ] // => [1, 128, 1] * ``` * * @category models @@ -127,12 +131,8 @@ export interface Metric extends Pipeable { * * **Example** (Using counter metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class CounterInterfaceError extends Data.TaggedError("CounterInterfaceError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of counters @@ -178,9 +178,12 @@ export interface Metric extends Pipeable { * bytes: { count: bytesState.count, incremental: bytesState.incremental } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const counts = [result.requests.count, result.bytes.count] // => [6, 1024n] * ``` * - * @category metrics + * @category models * @since 2.0.0 */ export interface Counter extends Metric> {} @@ -190,12 +193,8 @@ export interface Counter extends Metric {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of counters @@ -220,7 +219,6 @@ export interface Counter extends Metric = yield* Metric.value( * byteCounter * ) - * * // CounterState contains: * // - count: current count value (number or bigint based on counter type) * // - incremental: whether counter only allows increases @@ -240,9 +238,12 @@ export interface Counter extends Metric [3, 3, 1024000n] * ``` * - * @category Counter + * @category models * @since 4.0.0 */ export interface CounterState { @@ -261,32 +262,16 @@ export interface CounterState { * * **Example** (Using frequency metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class FrequencyInterfaceError - * extends Data.TaggedError("FrequencyInterfaceError")<{ - * readonly operation: string - * }> - * {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * // Function that accepts any Frequency metric - * const logFrequencyMetric = (freq: Metric.Frequency) => + * const analyzeFrequencyMetric = (freq: Metric.Frequency) => * Effect.gen(function*() { * const state = yield* Metric.value(freq) * - * yield* Effect.log(`Frequency Metric: ${freq.id}`) - * yield* Effect.log(`Description: ${freq.description ?? "No description"}`) - * yield* Effect.log(`Type: ${freq.type}`) // "Frequency" - * * // Access the frequency state * const occurrences: ReadonlyMap = state.occurrences - * yield* Effect.log(`Total unique values: ${occurrences.size}`) - * - * // Iterate through all occurrences - * for (const [value, count] of occurrences) { - * yield* Effect.log(` "${value}": ${count} occurrences`) - * } * * // Find most frequent value * let maxCount = 0 @@ -323,14 +308,16 @@ export interface CounterState { * yield* Metric.update(userActions, "login") * * // Use the function with different frequency metrics - * const statusAnalysis = yield* logFrequencyMetric(statusCodes) - * const actionAnalysis = yield* logFrequencyMetric(userActions) - * + * const statusAnalysis = yield* analyzeFrequencyMetric(statusCodes) + * const actionAnalysis = yield* analyzeFrequencyMetric(userActions) * return { statusAnalysis, actionAnalysis } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.statusAnalysis.mostFrequent, result.actionAnalysis.mostFrequent] // => ["200", "login"] * ``` * - * @category metrics + * @category models * @since 2.0.0 */ export interface Frequency extends Metric {} @@ -340,12 +327,8 @@ export interface Frequency extends Metric {} * * **Example** (Reading frequency state) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class FrequencyStateError extends Data.TaggedError("FrequencyStateError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create frequency metrics for different categories @@ -393,7 +376,6 @@ export interface Frequency extends Metric {} * * const topStatus = getMostFrequent(statusState.occurrences) * const topAction = getMostFrequent(actionState.occurrences) - * * return { * statusCodes: { * totalResponses: Array.from(statusState.occurrences.values()).reduce( @@ -413,9 +395,13 @@ export interface Frequency extends Metric {} * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const mostCommon = [result.statusCodes.mostCommon, result.userActions.mostCommon] + * mostCommon.map(({ key, count }) => [key, count]) // => [["200", 3], ["click", 3]] * ``` * - * @category metrics + * @category models * @since 4.0.0 */ export interface FrequencyState { @@ -432,12 +418,8 @@ export interface FrequencyState { * * **Example** (Using gauge metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class GaugeInterfaceError extends Data.TaggedError("GaugeInterfaceError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of gauges @@ -467,7 +449,6 @@ export interface FrequencyState { * const diskState: Metric.GaugeState = yield* Metric.value( * diskSpaceGauge * ) - * * // Gauge state contains: * // - value: current instantaneous value * @@ -476,9 +457,12 @@ export interface FrequencyState { * disk: { currentValue: diskState.value } // 5000000000n * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.memory.currentValue, result.disk.currentValue] // => [704, 5000000000n] * ``` * - * @category metrics + * @category models * @since 2.0.0 */ export interface Gauge extends Metric> {} @@ -488,12 +472,8 @@ export interface Gauge extends Metric {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of gauges @@ -529,7 +509,6 @@ export interface Gauge extends Metric = yield* Metric.value( * queueSizeGauge * ) - * * // GaugeState contains: * // - value: current instantaneous value (number or bigint based on gauge type) * @@ -545,9 +524,13 @@ export interface Gauge extends Metric [23.1, 5000000000n, 15] * ``` * - * @category metrics + * @category models * @since 4.0.0 */ export interface GaugeState { @@ -564,14 +547,8 @@ export interface GaugeState { * * **Example** (Using histogram metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class HistogramInterfaceError - * extends Data.TaggedError("HistogramInterfaceError")<{ - * readonly operation: string - * }> - * {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create histograms with different boundary strategies @@ -579,7 +556,7 @@ export interface GaugeState { * "http_response_time_ms", * { * description: "HTTP response time distribution in milliseconds", - * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) // 0, 50, 100, ..., 950 + * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 20 }) // 50, 100, ..., 900, Infinity * } * ) * @@ -612,7 +589,6 @@ export interface GaugeState { * const fileSizeState: Metric.HistogramState = yield* Metric.value( * fileSizeHistogram * ) - * * // Histogram state contains: * // - buckets: Array of [boundary, cumulativeCount] pairs * // - count: total number of observations @@ -636,9 +612,13 @@ export interface GaugeState { * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.responseTime.totalRequests, result.responseTime.totalTime, result.fileSize.totalBytes] + * values // => [4, 445, 118] * ``` * - * @category metrics + * @category models * @since 2.0.0 */ export interface Histogram extends Metric {} @@ -648,18 +628,14 @@ export interface Histogram extends Metric {} * * **Example** (Reading histogram state) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class HistogramStateError extends Data.TaggedError("HistogramStateError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create histogram with linear boundaries * const responseTimeHistogram = Metric.histogram("api_response_time_ms", { * description: "API response time distribution", - * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) // 0, 100, 200, ..., 900 + * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) // 100, 200, ..., 800, Infinity * }) * * // Record observations @@ -706,7 +682,6 @@ export interface Histogram extends Metric {} * } * * const bucketAnalysis = analyzeBuckets(state.buckets) - * * return { * responseTime: { * totalRequests: state.count, // 5 @@ -723,9 +698,14 @@ export interface Histogram extends Metric {} * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const stats = result.responseTime + * const values = [stats.totalRequests, stats.fastestResponse, stats.slowestResponse, stats.totalTime] + * values // => [5, 50, 750, 1295] * ``` * - * @category metrics + * @category models * @since 4.0.0 */ export interface HistogramState { @@ -747,12 +727,8 @@ export interface HistogramState { * * **Example** (Using summary metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class SummaryInterfaceError extends Data.TaggedError("SummaryInterfaceError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create summaries with different quantile configurations @@ -811,7 +787,6 @@ export interface HistogramState { * const median = getQuantileValue(responseTimeState.quantiles, 0.5) * const p95 = getQuantileValue(responseTimeState.quantiles, 0.95) * const p99 = getQuantileValue(responseTimeState.quantiles, 0.99) - * * return { * responseTime: { * totalRequests: responseTimeState.count, // 5 @@ -829,9 +804,13 @@ export interface HistogramState { * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const counts = [result.responseTime.totalRequests, result.responseTime.totalTime, result.requestSize.totalRequests] + * counts // => [5, 1461, 3] * ``` * - * @category metrics + * @category models * @since 2.0.0 */ export interface Summary extends Metric {} @@ -841,12 +820,8 @@ export interface Summary extends Metric {} * * **Example** (Reading summary state) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class SummaryStateError extends Data.TaggedError("SummaryStateError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create summary with specific quantiles @@ -889,7 +864,6 @@ export interface Summary extends Metric {} * } * * const quantileValues = extractQuantiles(state.quantiles) - * * return { * latencyAnalysis: { * totalRequests: state.count, // 7 @@ -911,9 +885,14 @@ export interface Summary extends Metric {} * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const analysis = result.latencyAnalysis + * const values = [analysis.totalRequests, analysis.fastestResponse, analysis.slowestResponse, analysis.totalLatency] + * values // => [7, 45, 890, 1879] * ``` * - * @category metrics + * @category models * @since 4.0.0 */ export interface SummaryState { @@ -930,12 +909,8 @@ export interface SummaryState { * * **Example** (Collecting application metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricsError extends Data.TaggedError("MetricsError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of metrics @@ -965,6 +940,9 @@ export interface SummaryState { * frequency: frequencyValue * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.counter.count, result.gauge.value] // => [1, 12] * ``` * * @since 2.0.0 @@ -975,78 +953,28 @@ export declare namespace Metric { * * **Example** (Inspecting metric types) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricTypeError extends Data.TaggedError("MetricTypeError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * const program = Effect.gen(function*() { - * // Create different metric types - * const counter = Metric.counter("requests_total") - * const gauge = Metric.gauge("cpu_usage") - * const frequency = Metric.frequency("status_codes") - * const histogram = Metric.histogram("response_time", { + * const metrics: ReadonlyArray> = [ + * Metric.counter("requests_total"), + * Metric.gauge("cpu_usage"), + * Metric.frequency("status_codes"), + * Metric.histogram("response_time", { * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) - * }) - * const summary = Metric.summary("latency", { + * }), + * Metric.summary("latency", { * maxAge: "5 minutes", * maxSize: 1000, * quantiles: [0.5, 0.95, 0.99] * }) + * ] * - * // Function that checks metric type - * const getMetricInfo = (metric: Metric.Metric) => ({ - * name: metric.id, - * type: metric.type - * }) - * - * // Get type information for each metric - * const counterInfo = getMetricInfo(counter) // { name: "requests_total", type: "Counter" } - * const gaugeInfo = getMetricInfo(gauge) // { name: "cpu_usage", type: "Gauge" } - * const frequencyInfo = getMetricInfo(frequency) // { name: "status_codes", type: "Frequency" } - * const histogramInfo = getMetricInfo(histogram) // { name: "response_time", type: "Histogram" } - * const summaryInfo = getMetricInfo(summary) // { name: "latency", type: "Summary" } - * - * // Pattern match on metric type - * const describeMetric = (type: string): string => { - * switch (type) { - * case "Counter": - * return "Cumulative values that increase over time" - * case "Gauge": - * return "Instantaneous values that can go up or down" - * case "Frequency": - * return "Counts of discrete string occurrences" - * case "Histogram": - * return "Distribution of values across buckets" - * case "Summary": - * return "Quantile calculations over time windows" - * default: - * return "Unknown metric type" - * } - * } - * - * return { - * metrics: [ - * counterInfo, - * gaugeInfo, - * frequencyInfo, - * histogramInfo, - * summaryInfo - * ], - * descriptions: { - * Counter: describeMetric("Counter"), - * Gauge: describeMetric("Gauge"), - * Frequency: describeMetric("Frequency"), - * Histogram: describeMetric("Histogram"), - * Summary: describeMetric("Summary") - * } - * } - * }) + * const types: ReadonlyArray = metrics.map((metric) => metric.type) + * const actual = types // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] * ``` * - * @category types + * @category models * @since 4.0.0 */ export type Type = "Counter" | "Frequency" | "Gauge" | "Histogram" | "Summary" @@ -1056,12 +984,8 @@ export declare namespace Metric { * * **Example** (Providing attributes in different formats) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class AttributesError extends Data.TaggedError("AttributesError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Different ways to specify attributes @@ -1110,19 +1034,22 @@ export declare namespace Metric { * // Both formats result in the same internal representation * const normalizedObject = normalizeAttributes(attributesAsObject) * const normalizedArray = normalizeAttributes(attributesAsArray) - * * return { * attributeFormats: { * object: normalizedObject, // { service: "api", environment: "production", version: "1.2.3" } - * array: normalizedArray, // { service: "api", environment: "production", version: "1.2.3" } - * areEqual: - * JSON.stringify(normalizedObject) === JSON.stringify(normalizedArray) // true + * array: normalizedArray // { service: "api", environment: "production", version: "1.2.3" } * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const objectAttributes = result.attributeFormats.object + * const arrayAttributes = result.attributeFormats.array + * const sameAttributes = [objectAttributes, arrayAttributes] + * sameAttributes // => [{ service: "api", environment: "production", version: "1.2.3" }, { service: "api", environment: "production", version: "1.2.3" }] * ``` * - * @category types + * @category models * @since 4.0.0 */ export type Attributes = AttributeSet | ReadonlyArray<[string, string]> @@ -1132,12 +1059,8 @@ export declare namespace Metric { * * **Example** (Combining metric attribute sets) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class AttributeSetError extends Data.TaggedError("AttributeSetError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Define attribute sets for different contexts @@ -1198,9 +1121,12 @@ export declare namespace Metric { * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const validation = [result.attributes.isValid, result.attributes.totalKeys] // => [true, 9] * ``` * - * @category types + * @category models * @since 4.0.0 */ export type AttributeSet = Readonly> @@ -1210,7 +1136,7 @@ export declare namespace Metric { * * **Example** (Extracting metric input types) * - * ```ts + * ```ts import.meta.vitest * import { Metric } from "effect" * * // Create various metric types @@ -1248,9 +1174,11 @@ export declare namespace Metric { * // Metric.update(numberCounter, "abc") // ✗ Type error * // Metric.update(stringFrequency, "ok") // ✓ Valid (string) * // Metric.update(stringFrequency, 404) // ✗ Type error + * const metricIds = metrics.map(({ id, type }) => `${id}:${type}`) + * metricIds // => ["requests:Counter", "bytes:Counter", "status_codes:Frequency", "cpu_usage:Gauge", "response_time:Histogram"] * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type Input = A extends Metric ? _Input @@ -1261,7 +1189,7 @@ export declare namespace Metric { * * **Example** (Extracting metric state types) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * // Create various metric types @@ -1299,7 +1227,6 @@ export declare namespace Metric { * const frequencyState = yield* Metric.value(statusFrequency) * const histogramState = yield* Metric.value(responseHistogram) * const summaryState = yield* Metric.value(latencySummary) - * * return { * counter: { count: counterState.count }, // { count: 10 } * gauge: { value: gaugeState.value }, // { value: 85.5 } @@ -1308,9 +1235,13 @@ export declare namespace Metric { * summary: { observations: summaryState.count } // { observations: 1 } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.counter.count, result.gauge.value, result.frequency.uniqueValues] + * values // => [10, 85.5, 1] * ``` * - * @category types + * @category utility types * @since 4.0.0 */ export type State = A extends Metric ? _State @@ -1321,12 +1252,8 @@ export declare namespace Metric { * * **Example** (Using metric hooks) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class HooksError extends Data.TaggedError("HooksError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a counter metric @@ -1354,9 +1281,12 @@ export declare namespace Metric { * isIncremental: state.incremental // false * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const state = result // => { currentCount: 6, isIncremental: false } * ``` * - * @category interfaces + * @category models * @since 4.0.0 */ export interface Hooks { @@ -1370,12 +1300,8 @@ export declare namespace Metric { * * **Example** (Inspecting metric metadata) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetadataError extends Data.TaggedError("MetadataError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create metrics with different configurations @@ -1420,9 +1346,12 @@ export declare namespace Metric { * } * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const types = [result.counter.type, result.gauge.type, result.frequency.type] // => ["Counter", "Gauge", "Frequency"] * ``` * - * @category interfaces + * @category models * @since 4.0.0 */ export interface Metadata { @@ -1438,12 +1367,8 @@ export declare namespace Metric { * * **Example** (Inspecting metric snapshot protocols) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class SnapshotProtoError extends Data.TaggedError("SnapshotProtoError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create and update metrics @@ -1493,9 +1418,12 @@ export declare namespace Metric { * null * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const counts = [result.counter?.count, result.histogram?.observations] // => [25, 2] * ``` * - * @category interfaces + * @category models * @since 4.0.0 */ export interface SnapshotProto { @@ -1511,12 +1439,8 @@ export declare namespace Metric { * * **Example** (Analyzing metric snapshots) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class SnapshotError extends Data.TaggedError("SnapshotError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create different types of metrics @@ -1570,9 +1494,12 @@ export declare namespace Metric { * analysis * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const types = result.metricTypes // => ["Counter", "Gauge", "Frequency", "Histogram", "Summary"] * ``` * - * @category types + * @category models * @since 4.0.0 */ export type Snapshot = @@ -1588,12 +1515,8 @@ export declare namespace Metric { * * **Example** (Accessing the current metric attributes key) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class AttributesKeyError extends Data.TaggedError("AttributesKeyError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // The key is used internally by the Effect runtime to manage metric attributes @@ -1627,9 +1550,12 @@ export declare namespace Metric { * isConstant: key === "effect/Metric/CurrentMetricAttributes" // true * } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const key = result // => { keyValue: "effect/Metric/CurrentMetricAttributes", keyType: "string", isConstant: true } * ``` * - * @category references + * @category constants * @since 4.0.0 */ export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes" as const @@ -1651,17 +1577,12 @@ export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes * * **Example** (Providing current metric attributes) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class AttributesError extends Data.TaggedError("AttributesError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Access current metric attributes - * const attributes = yield* Metric.CurrentMetricAttributes - * console.log("Current attributes:", attributes) + * yield* Metric.CurrentMetricAttributes * * // Set new attributes context * const newAttributes = { service: "api", version: "1.0" } @@ -1676,9 +1597,12 @@ export const CurrentMetricAttributesKey = "effect/Metric/CurrentMetricAttributes * * return result * }) + * + * const attributes = await Effect.runPromise(program) + * const actual = attributes // => { service: "api", version: "1.0" } * ``` * - * @category references + * @category services * @since 4.0.0 */ export const CurrentMetricAttributes = Context.Reference(CurrentMetricAttributesKey, { @@ -1710,7 +1634,7 @@ const MetricRegistryKey = "~effect/observability/Metric/MetricRegistryKey" * @see {@link snapshot} for reading all registered metrics from the current `Effect` context * @see {@link snapshotUnsafe} for reading all registered metrics from an explicit `Context` * - * @category references + * @category services * @since 4.0.0 */ export const MetricRegistry = Context.Reference>>( @@ -1912,7 +1836,7 @@ class HistogramMetric extends Metric$ { let count = 0 let sum = 0 let min = Number.MAX_VALUE - let max = Number.MIN_VALUE + let max = -Number.MAX_VALUE Arr.map(Arr.sort(bounds, Order.Number), (n, i) => { boundaries[i] = n @@ -1999,7 +1923,7 @@ class SummaryMetric extends Metric$ => { const builder: Array = [] @@ -2021,8 +1945,8 @@ class SummaryMetric extends Metric$ [q, undefined]) } // Compute the value of the quantile in terms of rank: - // > For a given quantile `q`, return the maximum value `v` such that at - // > most `q * n` values are less than or equal to `v`. + // For a given quantile `q`, return the maximum value `v` such that at + // most `q * n` values are less than or equal to `v`. return sortedQuantiles.map((q) => { if (q <= 0) return [q, samples[0]] if (q >= 1) return [q, samples[sampleSize - 1]] @@ -2094,24 +2018,18 @@ class MetricTransform extends Metric$ true + * Metric.isMetric({ name: "requests" }) // => false * ``` * * @category guards * @since 4.0.0 */ -export const isMetric = (u: unknown): u is Metric => - Predicate.hasProperty(u, "~effect/Metric") && u["~effect/Metric"] === "~effect/Metric" +export const isMetric = (u: unknown): u is Metric => + Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId /** * Represents a Counter metric that tracks cumulative numerical values over @@ -2127,12 +2045,8 @@ export const isMetric = (u: unknown): u is Metric => * * **Example** (Creating counter metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class CounterError extends Data.TaggedError("CounterError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a basic counter for tracking requests @@ -2166,6 +2080,9 @@ export const isMetric = (u: unknown): u is Metric => * * return { requestValue, eventValue, bytesValue } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const counts = [result.requestValue.count, result.eventValue.count, result.bytesValue.count] // => [6, 1, 1024n] * ``` * * @category constructors @@ -2209,12 +2126,8 @@ export const counter: { * * **Example** (Creating gauge metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class GaugeError extends Data.TaggedError("GaugeError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a gauge for tracking memory usage @@ -2253,6 +2166,9 @@ export const counter: { * * return { memoryValue, cpuValue, diskValue } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.memoryValue.value, result.cpuValue.value, result.diskValue.value] // => [800, 75, 1024000000n] * ``` * * @category constructors @@ -2288,12 +2204,8 @@ export const gauge: { * * **Example** (Creating frequency metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class FrequencyError extends Data.TaggedError("FrequencyError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a frequency metric for HTTP status codes @@ -2343,6 +2255,14 @@ export const gauge: { * * return { statusCounts, actionCounts, errorCounts } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const counts = [ + * result.statusCounts.occurrences.get("200"), + * result.actionCounts.occurrences.get("login"), + * result.errorCounts.occurrences.get("ValidationError") + * ] + * counts // => [3, 2, 2] * ``` * * @category constructors @@ -2370,19 +2290,15 @@ export const frequency = (name: string, options?: { * * **Example** (Creating histogram metrics) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class HistogramError extends Data.TaggedError("HistogramError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a histogram for API response times * const responseTimeHistogram = Metric.histogram("api_response_time", { * description: "Distribution of API response times in milliseconds", * boundaries: Metric.linearBoundaries({ start: 0, width: 50, count: 10 }) - * // Creates buckets: 0-50ms, 50-100ms, 100-150ms, ..., 400-450ms, 450ms+ + * // Creates buckets: 0-50ms, 50-100ms, 100-150ms, ..., 350-400ms, 400ms+ * }) * * // Create a histogram for request payload sizes @@ -2422,6 +2338,10 @@ export const frequency = (name: string, options?: { * * return { responseTimeState, payloadSizeState } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.responseTimeState.count, result.responseTimeState.sum, result.payloadSizeState.count] + * values // => [5, 500, 3] * ``` * * @category constructors @@ -2451,12 +2371,8 @@ export const histogram = (name: string, options: { * * **Example** (Creating summary metrics) * - * ```ts - * import { Data, Duration, Effect, Metric } from "effect" - * - * class SummaryError extends Data.TaggedError("SummaryError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Duration, Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create a summary for API response times @@ -2492,24 +2408,16 @@ export const histogram = (name: string, options: { * const responseStats = yield* Metric.value(responseTimeSummary) * const payloadStats = yield* Metric.value(payloadSizeSummary) * - * console.log({ - * count: responseStats.count, - * min: responseStats.min, - * max: responseStats.max, - * sum: responseStats.sum - * }) // { count: 8, min: 82, max: 240, sum: 1155 } - * - * console.log({ - * count: payloadStats.count, - * min: payloadStats.min, - * max: payloadStats.max, - * sum: payloadStats.sum - * }) // { count: 4, min: 1.2, max: 15.6, sum: 26 } - * * // Both summaries include quantile information for their configured windows. * * return { responseStats, payloadStats } * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const response = result.responseStats + * const payload = result.payloadStats + * const responseValues = [response.count, response.min, response.max, response.sum] // => [8, 82, 240, 1155] + * const payloadValues = [payload.count, payload.min, payload.max, payload.sum] // => [4, 1.2, 15.6, 26] * ``` * * @category constructors @@ -2549,7 +2457,7 @@ export const summary = (name: string, options: { * * **Example** (Creating summaries with explicit timestamps) * - * ```ts + * ```ts import.meta.vitest * import { Metric } from "effect" * * const responseTimesSummary = Metric.summaryWithTimestamp( @@ -2561,6 +2469,7 @@ export const summary = (name: string, options: { * quantiles: [0.5, 0.9, 0.99] // Calculate 50th, 90th, and 99th quantiles. * } * ) + * const metadata = [responseTimesSummary.id, responseTimesSummary.type] // => ["response_times_summary", "Summary"] * ``` * * @category constructors @@ -2588,12 +2497,8 @@ export const summaryWithTimestamp = (name: string, options: { * * **Example** (Recording durations with a timer) * - * ```ts - * import { Data, Duration, Effect, Metric } from "effect" - * - * class TimerError extends Data.TaggedError("TimerError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Duration, Effect, Metric } from "effect" * * // Create a timer metric to track API request durations * const apiRequestTimer = Metric.timer("api_request_duration", { @@ -2607,13 +2512,17 @@ export const summaryWithTimestamp = (name: string, options: { * yield* Metric.update(apiRequestTimer, duration) * * const state = yield* Metric.value(apiRequestTimer) - * console.log({ + * return { * count: state.count, * min: state.min, * max: state.max, * sum: state.sum - * }) // { count: 1, min: 120, max: 120, sum: 120 } + * } * }) + * + * await Effect.runPromise( + * Effect.provideService(apiOperation, Metric.MetricRegistry, new Map()) + * ) // => { count: 1, min: 120, max: 120, sum: 120 } * ``` * * @category constructors @@ -2646,10 +2555,10 @@ export const timer = (name: string, options?: { * * **Example** (Reading metric state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * - * const requestCounter = Metric.counter("requests") + * const requestCounter = Metric.counter("modify_requests") * const responseTime = Metric.histogram("response_time", { * boundaries: [100, 500, 1000, 2000] * }) @@ -2661,16 +2570,19 @@ export const timer = (name: string, options?: { * * // Get current values * const counterState = yield* Metric.value(requestCounter) - * console.log(`Request count: ${counterState.count}`) - * * const histogramState = yield* Metric.value(responseTime) - * console.log(`Response time stats:`, { + * return { + * requestCount: counterState.count, * count: histogramState.count, * min: histogramState.min, * max: histogramState.max, * average: histogramState.sum / histogramState.count - * }) + * } * }) + * + * await Effect.runPromise( + * Effect.provideService(program, Metric.MetricRegistry, new Map()) + * ) // => { requestCount: 1, count: 1, min: 750, max: 750, average: 750 } * ``` * * @category getters @@ -2697,7 +2609,7 @@ export const value = ( * * **Example** (Modifying metric values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const temperatureGauge = Metric.gauge("temperature") @@ -2717,10 +2629,10 @@ export const value = ( * * const temp = yield* Metric.value(temperatureGauge) * const requests = yield* Metric.value(requestCounter) - * - * console.log(`Temperature: ${temp.value}°C`) // 22°C - * console.log(`Requests: ${requests.count}`) // 15 + * return [temp.value, requests.count] as const * }) + * + * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [22, 15] * ``` * * @category mutations @@ -2751,7 +2663,7 @@ export const modify: { * * **Example** (Updating metric values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const cpuUsage = Metric.gauge("cpu_usage_percent") @@ -2779,11 +2691,10 @@ export const modify: { * const cpu = yield* Metric.value(cpuUsage) * const statuses = yield* Metric.value(httpStatus) * const times = yield* Metric.value(responseTime) - * - * console.log(`CPU Usage: ${cpu.value}%`) - * console.log(`Status 200 count: ${statuses.occurrences.get("200")}`) // 2 - * console.log(`Response time samples: ${times.count}`) // 3 + * return [cpu.value, statuses.occurrences.get("200"), times.count] as const * }) + * + * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => [67.8, 2, 3] * ``` * * @category mutations @@ -2808,36 +2719,30 @@ export const update: { * * **Example** (Mapping metric inputs) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricError extends Data.TaggedError("MetricError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * - * // Create a histogram that expects Duration values * const durationHistogram = Metric.histogram("request_duration_ms", { * description: "Request duration in milliseconds", * boundaries: Metric.linearBoundaries({ start: 0, width: 100, count: 10 }) * }) * - * // Transform to accept number values representing milliseconds - * const numberHistogram = Metric.mapInput( + * // Accept duration strings while recording numeric milliseconds + * const durationStringHistogram = Metric.mapInput( * durationHistogram, - * (ms: number) => ms // Direct mapping from number to expected input + * (input: string) => Number(input) * ) * * const program = Effect.gen(function*() { - * // Now we can update with a plain number - * yield* Metric.update(numberHistogram, 250) - * - * // Get metric value to see the recorded state - * const value = yield* Metric.value(numberHistogram) - * return value + * yield* Metric.update(durationStringHistogram, "250") + * return yield* Metric.value(durationStringHistogram) * }) + * + * const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [value.count, value.sum] // => [1, 250] * ``` * - * @category mapping + * @category annotations * @since 2.0.0 */ export const mapInput: { @@ -2874,12 +2779,8 @@ export const mapInput: { * * **Example** (Ignoring inputs with a constant value) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricError extends Data.TaggedError("MetricError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * // Create a counter that normally expects a number increment * const requestCounter = Metric.counter("total_requests", { @@ -2898,9 +2799,12 @@ export const mapInput: { * const value = yield* Metric.value(simpleRequestCounter) * return value // Counter state will show count: 3 * }) + * + * const value = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const count = value.count // => 3 * ``` * - * @category Input + * @category mapping * @since 2.0.0 */ export const withConstantInput: { @@ -2922,7 +2826,7 @@ export const withConstantInput: { * * **Example** (Applying metric attributes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * * const requestCounter = Metric.counter("http_requests_total", { @@ -2955,18 +2859,17 @@ export const withConstantInput: { * yield* Metric.update(taggedMetric, 1) // http_requests_total{service="user-api", version="v1"} * }) * - * // When taking snapshots, each attribute combination appears as a separate metric - * const viewMetrics = Effect.gen(function*() { - * const snapshots = yield* Metric.snapshot - * for (const metric of snapshots) { - * if (metric.id === "http_requests_total") { - * console.log(`${metric.id}`, metric.attributes, metric.state) - * } - * } + * const result = Effect.gen(function*() { + * yield* program + * const get = yield* Metric.value(getRequests) + * const post = yield* Metric.value(postRequests) + * return [get.count, post.count] as const * }) + * + * await Effect.runPromise(Effect.provideService(result, Metric.MetricRegistry, new Map())) // => [2, 1] * ``` * - * @category Attributes + * @category mapping * @since 4.0.0 */ export const withAttributes: { @@ -2998,12 +2901,8 @@ export const withAttributes: { * * **Example** (Capturing metric snapshots) * - * ```ts - * import { Console, Data, Effect, Metric } from "effect" - * - * class SnapshotError extends Data.TaggedError("SnapshotError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create and update some metrics @@ -3024,19 +2923,16 @@ export const withAttributes: { * // Take a snapshot of all metrics * const snapshots = yield* Metric.snapshot * - * // Examine the snapshots - * for (const snapshot of snapshots) { - * yield* Console.log(`Metric: ${snapshot.id}`) - * yield* Console.log(`Description: ${snapshot.description}`) - * yield* Console.log(`Type: ${snapshot.type}`) - * yield* Console.log(`State:`, snapshot.state) - * } - * * return snapshots * }) + * + * const snapshots = await Effect.runPromise( + * Effect.provideService(program, Metric.MetricRegistry, new Map()) + * ) + * const ids = snapshots.map((snapshot) => snapshot.id).sort() // => ["http_requests", "response_time_ms"] * ``` * - * @category Snapshotting + * @category snapshotting * @since 2.0.0 */ export const snapshot: Effect> = InternalEffect.map( @@ -3056,12 +2952,8 @@ export const snapshot: Effect> = InternalEffect.m * * **Example** (Dumping metrics as text) * - * ```ts - * import { Console, Data, Effect, Metric } from "effect" - * - * class DumpError extends Data.TaggedError("DumpError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Create and update some metrics for demonstration @@ -3085,20 +2977,21 @@ export const snapshot: Effect> = InternalEffect.m * * // Get formatted dump of all metrics * const metricsReport = yield* Metric.dump - * yield* Console.log("Current Metrics:") - * yield* Console.log(metricsReport) - * - * // Output will look like a formatted table: - * // Name Description Type State - * // http_requests_total Total HTTP requests Counter [count: 2] - * // response_time_ms Current response time in milliseconds Gauge [value: 125] - * // http_status_codes Frequency of HTTP status codes Frequency [occurrences: 200 -> 2, 404 -> 1] - * * return metricsReport * }) + * + * const report = await Effect.runPromise( + * Effect.provideService(program, Metric.MetricRegistry, new Map()) + * ) + * const included = [ + * report.includes("http_requests_total"), + * report.includes("response_time_ms"), + * report.includes("http_status_codes") + * ] + * included // => [true, true, true] * ``` * - * @category Debugging + * @category formatting * @since 4.0.0 */ export const dump: Effect = InternalEffect.flatMap(InternalEffect.context(), (context) => { @@ -3153,56 +3046,20 @@ export const dump: Effect = InternalEffect.flatMap(InternalEffect.contex * * **Example** (Capturing snapshots from a context) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class UnsafeSnapshotError extends Data.TaggedError("UnsafeSnapshotError")<{ - * readonly operation: string - * }> {} - * - * // Use unsafeSnapshot in performance-critical scenarios or internal implementations - * const performanceMetricsExporter = Effect.gen(function*() { - * // Create some metrics first - * const requestCounter = Metric.counter("http_requests", { - * description: "Total HTTP requests" - * }) - * const responseTime = Metric.gauge("response_time_ms", { - * description: "Current response time" - * }) + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * - * // Update metrics + * const requestCounter = Metric.counter("http_requests") + * const program = Effect.gen(function*() { * yield* Metric.update(requestCounter, 1) - * yield* Metric.update(responseTime, 150) - * - * // Get services context for unsafe operations - * const services = yield* Effect.context() - * - * // Use snapshotUnsafe for direct, synchronous access - * const snapshots = Metric.snapshotUnsafe(services) - * const exportBatchCreatedAt = 1_700_000_000_000 - * - * // Process snapshots immediately (useful for exporters, debugging tools) - * const exportData = snapshots.map((snapshot) => ({ - * name: snapshot.id, - * type: snapshot.type, - * value: snapshot.state, - * timestamp: exportBatchCreatedAt - * })) - * - * // This is synchronous and doesn't involve Effect overhead - * // Useful for performance-critical metric export operations - * return exportData + * const context = yield* Effect.context() + * return Metric.snapshotUnsafe(context).map((snapshot) => snapshot.id) * }) * - * // For normal application use, prefer the safe snapshot function: - * const safeSnapshotExample = Effect.gen(function*() { - * // This automatically handles the services context - * const snapshots = yield* Metric.snapshot - * return snapshots - * }) + * await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) // => ["http_requests"] * ``` * - * @category Snapshotting + * @category snapshotting * @since 4.0.0 */ export const snapshotUnsafe = (context: Context.Context): ReadonlyArray => { @@ -3285,60 +3142,13 @@ const attributesToString = (attributes: Metric.AttributeSet): string => { * * **Example** (Creating boundaries from values) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class BoundaryError extends Data.TaggedError("BoundaryError")<{ - * readonly operation: string - * }> {} - * - * // Create boundaries from an array of custom values - * const customBoundaries = Metric.boundariesFromIterable([ - * 10, - * 25, - * 50, - * 100, - * 250, - * 500, - * 1000 - * ]) - * console.log(customBoundaries) // [10, 25, 50, 100, 250, 500, 1000, Infinity] - * - * // Automatically removes duplicates and negative values - * const messyBoundaries = Metric.boundariesFromIterable([ - * -5, - * 0, - * 10, - * 10, - * 25, - * 25, - * 50, - * -1 - * ]) - * console.log(messyBoundaries) // [10, 25, 50, Infinity] - * - * // Works with any iterable (Set, generator functions, etc.) - * const setBoundaries = Metric.boundariesFromIterable( - * new Set([100, 200, 300, 200, 100]) - * ) - * console.log(setBoundaries) // [100, 200, 300, Infinity] - * - * // Use with histogram metric - * const responseTimeHistogram = Metric.histogram("response_times", { - * description: "API response time distribution", - * boundaries: customBoundaries - * }) - * - * const program = Effect.gen(function*() { - * yield* Metric.update(responseTimeHistogram, 75) // Goes in 50-100ms bucket - * yield* Metric.update(responseTimeHistogram, 150) // Goes in 100-250ms bucket + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * const value = yield* Metric.value(responseTimeHistogram) - * return value - * }) + * Metric.boundariesFromIterable([-5, 0, 10, 10, 25, 50]) // => [10, 25, 50, Infinity] * ``` * - * @category boundaries + * @category constructors * @since 4.0.0 */ export const boundariesFromIterable = (iterable: Iterable): ReadonlyArray => @@ -3350,46 +3160,20 @@ export const boundariesFromIterable = (iterable: Iterable): ReadonlyArra * * **Details** * - * Generates `count - 1` finite boundaries using `start + width + index` for + * Generates `count - 1` candidate boundaries using `start + index * width` for * each zero-based index, then applies the same normalization as * `boundariesFromIterable`: non-positive values are removed, duplicates are * collapsed, and `Infinity` is appended. * * **Example** (Creating linear boundaries) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class BoundaryError extends Data.TaggedError("BoundaryError")<{ - * readonly operation: string - * }> {} - * - * // Create boundaries for response time histogram - * const responseBoundaries = Metric.linearBoundaries({ - * start: 0, // Starting point - * width: 100, // Offset used for the first boundary - * count: 5 // Creates 4 boundaries + infinity - * }) - * console.log(responseBoundaries) // [100, 101, 102, 103, Infinity] - * - * // Create a histogram using these boundaries - * const responseTimeHistogram = Metric.histogram("api_response_time", { - * description: "API response time distribution", - * boundaries: responseBoundaries - * }) - * - * const program = Effect.gen(function*() { - * // Record some response times - * yield* Metric.update(responseTimeHistogram, 85) - * yield* Metric.update(responseTimeHistogram, 101) - * yield* Metric.update(responseTimeHistogram, 450) + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * const value = yield* Metric.value(responseTimeHistogram) - * return value - * }) + * Metric.linearBoundaries({ start: 10, width: 20, count: 5 }) // => [10, 30, 50, 70, Infinity] * ``` * - * @category boundaries + * @category constructors * @since 4.0.0 */ export const linearBoundaries = (options: { @@ -3397,7 +3181,7 @@ export const linearBoundaries = (options: { readonly width: number readonly count: number }): ReadonlyArray => - boundariesFromIterable(Arr.makeBy(options.count - 1, (n) => options.start + n + options.width)) + boundariesFromIterable(Arr.makeBy(options.count - 1, (n) => options.start + n * options.width)) /** * Creates histogram bucket boundaries with exponentially increasing values. @@ -3409,47 +3193,13 @@ export const linearBoundaries = (options: { * * **Example** (Creating exponential boundaries) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class BoundaryError extends Data.TaggedError("BoundaryError")<{ - * readonly operation: string - * }> {} - * - * // Create exponential boundaries for request size histogram - * // Buckets: 0-1KB, 1-2KB, 2-4KB, 4-8KB, 8KB+ - * const sizeBoundaries = Metric.exponentialBoundaries({ - * start: 1, // Starting at 1KB - * factor: 2, // Each boundary doubles the previous - * count: 5 // Creates 4 boundaries + infinity - * }) - * console.log(sizeBoundaries) // [1, 2, 4, 8, Infinity] - * - * // Create a histogram for tracking request payload sizes - * const requestSizeHistogram = Metric.histogram("request_size_kb", { - * description: "Request payload size distribution in KB", - * boundaries: sizeBoundaries - * }) - * - * // For very wide ranges, use larger factors - * const latencyBoundaries = Metric.exponentialBoundaries({ - * start: 0.1, // Start at 0.1ms - * factor: 10, // Each boundary is 10x larger - * count: 6 // Creates ranges: 0.1ms, 1ms, 10ms, 100ms, 1000ms+ - * }) - * - * const program = Effect.gen(function*() { - * // Record different request sizes - * yield* Metric.update(requestSizeHistogram, 1.5) // Goes in 1-2KB bucket - * yield* Metric.update(requestSizeHistogram, 3.2) // Goes in 2-4KB bucket - * yield* Metric.update(requestSizeHistogram, 12) // Goes in 8KB+ bucket + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * const value = yield* Metric.value(requestSizeHistogram) - * return value - * }) + * Metric.exponentialBoundaries({ start: 1, factor: 2, count: 5 }) // => [1, 2, 4, 8, Infinity] * ``` * - * @category boundaries + * @category constructors * @since 4.0.0 */ export const exponentialBoundaries = (options: { @@ -3482,36 +3232,13 @@ const fiberFailures = counter("child_fiber_failures", { * * **Example** (Accessing the fiber runtime metrics key) * - * ```ts - * import { Data, Effect, Layer, Metric } from "effect" - * - * class MetricsError extends Data.TaggedError("MetricsError")<{ - * readonly operation: string - * }> {} - * - * const program = Effect.gen(function*() { - * // The key is used internally by the Effect runtime to manage fiber metrics - * const key = Metric.FiberRuntimeMetricsKey - * console.log("Fiber metrics key:", key) - * - * // Enable runtime metrics using the key - * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)( - * Metric.FiberRuntimeMetricsImpl - * ) - * - * return yield* Effect.gen(function*() { - * // This Effect will have fiber metrics automatically collected - * yield* Effect.sleep("100 millis") + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * // Create a test counter to demonstrate the key usage - * const testCounter = Metric.counter("test_counter") - * yield* Metric.update(testCounter, 1) - * return yield* Metric.value(testCounter) - * }).pipe(Effect.provide(layer)) - * }) + * Metric.FiberRuntimeMetricsKey // => "effect/observability/Metric/FiberRuntimeMetricsKey" * ``` * - * @category metrics + * @category constants * @since 4.0.0 */ export const FiberRuntimeMetricsKey: "effect/observability/Metric/FiberRuntimeMetricsKey" = @@ -3522,38 +3249,25 @@ export const FiberRuntimeMetricsKey: "effect/observability/Metric/FiberRuntimeMe * * **Example** (Providing a custom fiber metrics service) * - * ```ts - * import { Data, Effect, Layer, Metric } from "effect" - * import type { Context, Exit } from "effect" + * ```ts import.meta.vitest + * import { Context, Exit, Metric } from "effect" * - * class MetricsError extends Data.TaggedError("MetricsError")<{ - * readonly operation: string - * }> {} - * - * // Custom implementation of the metrics service + * const events: Array = [] * const customMetricsService: Metric.FiberRuntimeMetricsService = { - * recordFiberStart: (context: Context.Context) => { - * console.log("Fiber started") - * // Custom logic for tracking fiber starts + * recordFiberStart: () => { + * events.push("start") * }, - * recordFiberEnd: ( - * context: Context.Context, - * exit: Exit.Exit - * ) => { - * console.log("Fiber completed with exit:", exit) - * // Custom logic for tracking fiber completion based on exit status + * recordFiberEnd: (_context, exit) => { + * events.push(Exit.isSuccess(exit) ? "success" : "failure") * } * } * - * const program = Effect.gen(function*() { - * // Use the custom metrics service - * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)(customMetricsService) - * - * return yield* Effect.sleep("100 millis").pipe(Effect.provide(layer)) - * }) + * customMetricsService.recordFiberStart(Context.empty()) + * customMetricsService.recordFiberEnd(Context.empty(), Exit.succeed("ok")) + * events // => ["start", "success"] * ``` * - * @category metrics + * @category services * @since 4.0.0 */ export interface FiberRuntimeMetricsService { @@ -3578,50 +3292,26 @@ export interface FiberRuntimeMetricsService { * * **Example** (Accessing the fiber runtime metrics service) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricsError extends Data.TaggedError("MetricsError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { - * // Access the fiber runtime metrics service * const metricsService = yield* Metric.FiberRuntimeMetrics - * - * if (metricsService) { - * console.log("Runtime metrics are enabled") - * } else { - * console.log("Runtime metrics are disabled") - * } - * - * // Enable runtime metrics for the application - * const enabledLayer = Metric.enableRuntimeMetricsLayer - * - * return yield* Effect.gen(function*() { - * // Create some concurrent fibers to see metrics in action - * yield* Effect.all([ - * Effect.sleep("100 millis"), - * Effect.sleep("200 millis"), - * Effect.sleep("300 millis") - * ], { concurrency: "unbounded" }) - * - * // Create test metrics to demonstrate the service - * const testCounter = Metric.counter("test_counter") - * yield* Metric.update(testCounter, 5) - * const counterValue = yield* Metric.value(testCounter) - * - * return { counterValue, metricsEnabled: true } - * }).pipe(Effect.provide(enabledLayer)) + * return metricsService === Metric.FiberRuntimeMetricsImpl * }) + * + * const result = await Effect.runPromise( + * Effect.provideService(program, Metric.FiberRuntimeMetrics, Metric.FiberRuntimeMetricsImpl) + * ) + * const isDefault = result // => true * ``` * - * @category runtime metrics + * @category services * @since 4.0.0 */ export const FiberRuntimeMetrics = Context.Reference( InternalMetric.FiberRuntimeMetricsKey, - { defaultValue: constUndefined } + { fiberCached: true, defaultValue: constUndefined } ) /** @@ -3629,44 +3319,16 @@ export const FiberRuntimeMetrics = Context.Reference {} - * - * const program = Effect.gen(function*() { - * // Use the default metrics implementation - * const metrics = Metric.FiberRuntimeMetricsImpl - * console.log("Metrics implementation:", metrics) - * - * // Enable runtime metrics using the default implementation - * const layer = Layer.succeed(Metric.FiberRuntimeMetrics)(metrics) - * - * return yield* Effect.gen(function*() { - * // Run some Effects to trigger metric collection - * yield* Effect.forkChild(Effect.sleep("50 millis")) - * yield* Effect.forkChild(Effect.sleep("100 millis")) - * - * // Wait a bit and check the metrics - * yield* Effect.sleep("200 millis") - * - * // Create test metrics to demonstrate the implementation - * const testCounter = Metric.counter("test_counter") - * const testGauge = Metric.gauge("test_gauge") - * yield* Metric.update(testCounter, 3) - * yield* Metric.update(testGauge, 42) - * - * const counterValue = yield* Metric.value(testCounter) - * const gaugeValue = yield* Metric.value(testGauge) + * ```ts import.meta.vitest + * import { Metric } from "effect" * - * return { counter: counterValue, gauge: gaugeValue } - * }).pipe(Effect.provide(layer)) - * }) + * [ + * typeof Metric.FiberRuntimeMetricsImpl.recordFiberStart, + * typeof Metric.FiberRuntimeMetricsImpl.recordFiberEnd + * ] // => ["function", "function"] * ``` * - * @category metrics + * @category services * @since 4.0.0 */ export const FiberRuntimeMetricsImpl: FiberRuntimeMetricsService = { @@ -3695,91 +3357,18 @@ export const FiberRuntimeMetricsImpl: FiberRuntimeMetricsService = { * * **Example** (Enabling runtime metrics with a layer) * - * ```ts - * import { Console, Data, Effect, Layer, Metric } from "effect" - * - * class AppError extends Data.TaggedError("AppError")<{ - * readonly operation: string - * }> {} - * - * // Define your application logic - * const userService = Effect.gen(function*() { - * // Simulate user operations with concurrent processing - * const fetchUser = (id: number) => - * Effect.gen(function*() { - * yield* Effect.sleep(`${50 + id * 10} millis`) - * if (id % 7 === 0) { - * return yield* new AppError({ operation: `fetch-user-${id}` }) - * } - * return { id, name: `User ${id}`, email: `user${id}@example.com` } - * }) - * - * // Process multiple users concurrently (ignoring failures for demo) - * const userIds = Array.from({ length: 10 }, (_, i) => i + 1) - * const userTasks = userIds.map((id) => - * fetchUser(id).pipe(Effect.catchTag("AppError", () => Effect.succeed(null))) - * ) - * const allUsers = yield* Effect.all(userTasks, { concurrency: 4 }) - * const successfulUsers = allUsers.filter((user) => user !== null) - * return successfulUsers - * }) - * - * const analyticsService = Effect.gen(function*() { - * // Simulate analytics processing - * const tasks = Array.from({ length: 8 }, (_, i) => - * Effect.gen(function*() { - * yield* Effect.sleep(`${100 + i * 25} millis`) - * return `Analytics task ${i} completed` - * })) - * return yield* Effect.all(tasks, { concurrency: 3 }) - * }) - * - * // Main application that uses multiple services - * const application = Effect.gen(function*() { - * yield* Console.log("Starting application with runtime metrics...") - * - * // Run services concurrently - * const [users, analytics] = yield* Effect.all([ - * userService, - * analyticsService - * ], { concurrency: 2 }) - * - * yield* Console.log( - * `Processed ${users.length} users and ${analytics.length} analytics tasks` - * ) - * - * // Inspect the automatically collected runtime metrics - * const metrics = yield* Metric.snapshot - * const runtimeMetrics = metrics.filter((m) => m.id.startsWith("child_fiber")) - * - * yield* Console.log("Runtime Metrics Collected:") - * for (const metric of runtimeMetrics) { - * yield* Console.log(` ${metric.id}: ${JSON.stringify(metric.state)}`) - * } + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * - * return { users, analytics, metricsCount: runtimeMetrics.length } + * const program = Effect.gen(function*() { + * const service = yield* Metric.FiberRuntimeMetrics + * return service === Metric.FiberRuntimeMetricsImpl * }) * - * // Create the base application layer - * const AppLayer = Layer.empty // Add your application layers here (database, HTTP, etc.) - * - * // Add runtime metrics layer at the end - * const AppLayerWithMetrics = AppLayer.pipe( - * Layer.provide(Metric.enableRuntimeMetricsLayer) - * ) - * - * // Run the application with runtime metrics enabled - * const program = application.pipe( - * Effect.provide(AppLayerWithMetrics) - * ) - * - * // Alternative: Provide runtime metrics directly to the application - * const programWithDirectMetrics = application.pipe( - * Effect.provide(Metric.enableRuntimeMetricsLayer) - * ) + * await Effect.runPromise(Effect.provide(program, Metric.enableRuntimeMetricsLayer)) // => true * ``` * - * @category metrics + * @category layers * @since 4.0.0 */ export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(FiberRuntimeMetricsImpl) @@ -3789,12 +3378,8 @@ export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(Fibe * * **Example** (Disabling runtime metrics with a layer) * - * ```ts - * import { Data, Effect, Metric } from "effect" - * - * class MetricsError extends Data.TaggedError("MetricsError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { * // Disable runtime metrics collection @@ -3803,7 +3388,6 @@ export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(Fibe * return yield* Effect.gen(function*() { * // Check that metrics service is disabled * const metricsService = yield* Metric.FiberRuntimeMetrics - * console.log("Metrics enabled:", metricsService !== undefined) // false * * // Run some Effects - no metrics will be collected * yield* Effect.forkChild(Effect.sleep("50 millis")) @@ -3818,9 +3402,12 @@ export const enableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(Fibe * return { counterValue, metricsEnabled: metricsService !== undefined } * }).pipe(Effect.provide(disabledLayer)) * }) + * + * const result = await Effect.runPromise(Effect.provideService(program, Metric.MetricRegistry, new Map())) + * const values = [result.counterValue.count, result.metricsEnabled] // => [1, false] * ``` * - * @category metrics + * @category layers * @since 4.0.0 */ export const disableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(undefined) @@ -3836,75 +3423,18 @@ export const disableRuntimeMetricsLayer = Layer.succeed(FiberRuntimeMetrics)(und * * **Example** (Enabling runtime metrics for an effect) * - * ```ts - * import { Console, Data, Effect, Layer, Metric } from "effect" - * - * class RuntimeMetricsError extends Data.TaggedError("RuntimeMetricsError")<{ - * readonly operation: string - * }> {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { - * // Create a concurrent workload to demonstrate fiber metrics - * const heavyWorkload = Effect.gen(function*() { - * // Simulate concurrent operations - * const tasks = Array.from({ length: 10 }, (_, i) => - * Effect.gen(function*() { - * yield* Effect.sleep(`${100 + i * 50} millis`) - * if (i % 4 === 0) { - * // Simulate some failures - * return yield* new RuntimeMetricsError({ operation: `task-${i}` }) - * } - * return `Task ${i} completed` - * }).pipe( - * Effect.catchTag("RuntimeMetricsError", () => - * Effect.succeed(`Task ${i} failed`)) - * )) - * - * // Run tasks concurrently - * const results = yield* Effect.all(tasks, { concurrency: 5 }) - * return results - * }) - * - * // Enable runtime metrics collection for our workload - * const workloadWithMetrics = Metric.enableRuntimeMetrics(heavyWorkload) - * - * // Execute the workload - * const results = yield* workloadWithMetrics - * - * // After execution, we can inspect the runtime metrics - * // The following metrics are automatically collected: - * // - child_fibers_active: Current number of active child fibers (Gauge) - * // - child_fibers_started: Total child fibers started (Counter, incremental) - * // - child_fiber_successes: Total successful child fibers (Counter, incremental) - * // - child_fiber_failures: Total failed child fibers (Counter, incremental) - * - * yield* Console.log(`Workload completed with ${results.length} results`) - * - * // Get all metrics including the runtime metrics - * const allMetrics = yield* Metric.snapshot - * const runtimeMetrics = allMetrics.filter((m) => - * m.id.startsWith("child_fiber") || m.id.includes("fiber") - * ) - * - * yield* Console.log("Runtime Metrics:") - * for (const metric of runtimeMetrics) { - * yield* Console.log(` ${metric.id}: ${JSON.stringify(metric.state)}`) - * } - * - * return results + * const service = yield* Metric.FiberRuntimeMetrics + * return service === Metric.FiberRuntimeMetricsImpl * }) * - * // Alternative: Use the layer version for broader application coverage - * const BaseAppLayer = Layer.empty // Your base application layers - * const AppLayerWithMetrics = BaseAppLayer.pipe( - * Layer.provide(Metric.enableRuntimeMetricsLayer) - * ) - * const programWithLayer = program.pipe( - * Effect.provide(AppLayerWithMetrics) - * ) + * await Effect.runPromise(Metric.enableRuntimeMetrics(program)) // => true * ``` * - * @category metrics + * @category providing services * @since 4.0.0 */ export const enableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService( @@ -3922,76 +3452,18 @@ export const enableRuntimeMetrics: (self: Effect) => Effect {} + * ```ts import.meta.vitest + * import { Effect, Metric } from "effect" * * const program = Effect.gen(function*() { - * // This section will have runtime metrics enabled - * const normalOperation = Effect.gen(function*() { - * const tasks = Array.from({ length: 5 }, (_, i) => - * Effect.gen(function*() { - * yield* Effect.sleep(`${100 + i * 20} millis`) - * return `Normal task ${i} completed` - * })) - * return yield* Effect.all(tasks, { concurrency: 3 }) - * }) - * - * // This section will have runtime metrics disabled for performance - * const highPerformanceOperation = Metric.disableRuntimeMetrics( - * Effect.gen(function*() { - * // Performance-critical code where metrics overhead should be avoided - * const hotPath = Array.from( - * { length: 1000 }, - * (_, i) => - * Effect.gen(function*() { - * // Simulate intensive computation - * const result = i * i + (i % 10) / 10 - * return result - * }) - * ) - * return yield* Effect.all(hotPath, { concurrency: 100 }) - * }) - * ) - * - * yield* Console.log("Running operations with selective metrics...") - * - * // Run both operations - * const [normalResults, performanceResults] = yield* Effect.all([ - * normalOperation, // Will generate fiber metrics - * highPerformanceOperation // Will NOT generate fiber metrics - * ]) - * - * // Check collected metrics - should only see metrics from normalOperation - * const metrics = yield* Metric.snapshot - * const runtimeMetrics = metrics.filter((m) => m.id.startsWith("child_fiber")) - * - * yield* Console.log(`Normal operation results: ${normalResults.length}`) - * yield* Console.log( - * `Performance operation results: ${performanceResults.length}` - * ) - * yield* Console.log(`Runtime metrics collected: ${runtimeMetrics.length}`) - * - * // The runtime metrics will only reflect the fibers from normalOperation - * // The highPerformanceOperation fibers were not tracked due to disableRuntimeMetrics - * - * return { normalResults, performanceResults, runtimeMetrics } + * const service = yield* Metric.FiberRuntimeMetrics + * return service === undefined * }) * - * // Enable runtime metrics globally, then selectively disable where needed - * const BaseAppLayer = Layer.empty // Your base application layers - * const AppLayerWithMetrics = BaseAppLayer.pipe( - * Layer.provide(Metric.enableRuntimeMetricsLayer) - * ) - * const finalProgram = program.pipe( - * Effect.provide(AppLayerWithMetrics) - * ) + * await Effect.runPromise(Metric.disableRuntimeMetrics(program)) // => true * ``` * - * @category metrics + * @category providing services * @since 4.0.0 */ export const disableRuntimeMetrics: (self: Effect) => Effect = InternalEffect.provideService( @@ -4024,11 +3496,7 @@ function makeHooks( } function serializeAttributes(attributes: Metric.Attributes): string { - return serializeEntries(Array.isArray(attributes) ? attributes : Object.entries(attributes)) -} - -function serializeEntries(entries: ReadonlyArray<[string, string]>): string { - return entries.map(([key, value]) => `${key}=${value}`).join(",") + return JSON.stringify(Array.isArray(attributes) ? attributes : Object.entries(attributes)) } function mergeAttributes( @@ -4041,7 +3509,7 @@ function mergeAttributes( function attributesToRecord(attributes?: Metric.Attributes): Metric.AttributeSet | undefined { if (Predicate.isNotUndefined(attributes) && Array.isArray(attributes)) { return attributes.reduce((acc, [key, value]) => { - acc[key] = value + InternalRecord.assignProperty(acc, key, value) return acc }, {} as Metric.AttributeSet) } diff --git a/.context/effect/packages/effect/src/MutableHashMap.ts b/.context/effect/packages/effect/src/MutableHashMap.ts index 25aa08768..c95c6309c 100644 --- a/.context/effect/packages/effect/src/MutableHashMap.ts +++ b/.context/effect/packages/effect/src/MutableHashMap.ts @@ -39,7 +39,7 @@ const TypeId = "~effect/collections/MutableHashMap" * * **Example** (Using a mutable hash map) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * // Create a mutable hash map with string keys and number values @@ -50,17 +50,7 @@ const TypeId = "~effect/collections/MutableHashMap" * MutableHashMap.set(map, "count", 42) * MutableHashMap.set(map, "total", 100) * - * // Use as iterable - * for (const [key, value] of map) { - * console.log(`${key}: ${value}`) - * } - * // Output: - * // count: 42 - * // total: 100 - * - * // Convert to array - * const entries = Array.from(map) - * console.log(entries) // [["count", 42], ["total", 100]] + * Array.from(map) // => [["count", 42], ["total", 100]] * ``` * * @see {@link empty} for creating an empty mutable hash map @@ -93,7 +83,7 @@ export interface MutableHashMap extends Iterable<[K, V]>, Pipeable * * @see {@link MutableHashMap} for the mutable hash map interface * - * @category refinements + * @category guards * @since 4.0.0 */ export const isMutableHashMap = (value: unknown): value is MutableHashMap => hasProperty(value, TypeId) @@ -133,7 +123,7 @@ const MutableHashMapProto: Omit, "backing" | "b * * **Example** (Creating an empty map) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.empty() @@ -142,7 +132,7 @@ const MutableHashMapProto: Omit, "backing" | "b * MutableHashMap.set(map, "key1", 42) * MutableHashMap.set(map, "key2", 100) * - * console.log(MutableHashMap.size(map)) // 2 + * MutableHashMap.size(map) // => 2 * ``` * * @see {@link make} for creating a map from explicit entries @@ -167,8 +157,8 @@ export const empty = (): MutableHashMap => { * * **Example** (Creating a map from entries) * - * ```ts - * import { MutableHashMap } from "effect" + * ```ts import.meta.vitest + * import { MutableHashMap, Option } from "effect" * * const map = MutableHashMap.make( * ["key1", 42], @@ -176,8 +166,8 @@ export const empty = (): MutableHashMap => { * ["key3", 200] * ) * - * console.log(MutableHashMap.get(map, "key1")) // Some(42) - * console.log(MutableHashMap.size(map)) // 3 + * MutableHashMap.get(map, "key1") // => Option.some(42) + * MutableHashMap.size(map) // => 3 * ``` * * @see {@link empty} for creating an empty map @@ -202,8 +192,8 @@ export const make: >( * * **Example** (Creating a map from an iterable) * - * ```ts - * import { MutableHashMap } from "effect" + * ```ts import.meta.vitest + * import { MutableHashMap, Option } from "effect" * * const entries = [ * ["apple", 1], @@ -213,12 +203,12 @@ export const make: >( * * const map = MutableHashMap.fromIterable(entries) * - * console.log(MutableHashMap.get(map, "banana")) // Some(2) - * console.log(MutableHashMap.size(map)) // 3 + * MutableHashMap.get(map, "banana") // => Option.some(2) + * MutableHashMap.size(map) // => 3 * * // Works with any iterable * const fromMap = MutableHashMap.fromIterable(new Map([["x", 10], ["y", 20]])) - * console.log(MutableHashMap.get(fromMap, "x")) // Some(10) + * MutableHashMap.get(fromMap, "x") // => Option.some(10) * ``` * * @see {@link make} for creating a map from explicit entries @@ -249,23 +239,22 @@ export const fromIterable = (entries: Iterable): MutableH * * **Example** (Getting a value) * - * ```ts - * import { MutableHashMap } from "effect" + * ```ts import.meta.vitest + * import { MutableHashMap, Option } from "effect" * * const map = MutableHashMap.make(["key1", 42], ["key2", 100]) * - * console.log(MutableHashMap.get(map, "key1")) // Some(42) - * console.log(MutableHashMap.get(map, "key3")) // None + * MutableHashMap.get(map, "key1") // => Option.some(42) + * MutableHashMap.get(map, "key3") // => Option.none() * * // Pipe-able version - * const getValue = MutableHashMap.get("key1") - * console.log(getValue(map)) // Some(42) + * MutableHashMap.get("key1")(map) // => Option.some(42) * ``` * * @see {@link has} for checking only whether a key is present * @see {@link set} for inserting or replacing a value by key * - * @category elements + * @category getters * @since 2.0.0 */ export const get: { @@ -304,7 +293,7 @@ const isSimpleKey = (u: unknown): boolean => typeof u !== "object" && typeof u ! * * **Example** (Reading keys) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.make( @@ -313,17 +302,13 @@ const isSimpleKey = (u: unknown): boolean => typeof u !== "object" && typeof u ! * ["cherry", 3] * ) * - * const allKeys = Array.from(MutableHashMap.keys(map)) - * console.log(allKeys) // ["apple", "banana", "cherry"] - * - * // Useful for iteration or validation - * const hasRequiredKeys = allKeys.includes("apple") && allKeys.includes("banana") + * Array.from(MutableHashMap.keys(map)) // => ["apple", "banana", "cherry"] * ``` * * @see {@link values} for iterating over stored values * @see {@link has} for checking one key without iterating * - * @category elements + * @category getters * @since 3.8.0 */ export const keys = (self: MutableHashMap): Iterable => self.backing.keys() @@ -337,7 +322,7 @@ export const keys = (self: MutableHashMap): Iterable => self.back * * **Example** (Reading values) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.make( @@ -346,21 +331,18 @@ export const keys = (self: MutableHashMap): Iterable => self.back * ["cherry", 3] * ) * - * const allValues = Array.from(MutableHashMap.values(map)) - * console.log(allValues) // [1, 2, 3] + * const allValues = Array.from(MutableHashMap.values(map)) // => [1, 2, 3] * * // Useful for calculations - * const total = allValues.reduce((sum, value) => sum + value, 0) - * console.log(total) // 6 + * allValues.reduce((sum, value) => sum + value, 0) // => 6 * * // Filter values - * const largeValues = allValues.filter((value) => value > 1) - * console.log(largeValues) // [2, 3] + * allValues.filter((value) => value > 1) // => [2, 3] * ``` * * @see {@link keys} for iterating over stored keys * - * @category elements + * @category getters * @since 3.8.0 */ export const values = (self: MutableHashMap): Iterable => self.backing.values() @@ -390,22 +372,21 @@ const getFromBucket = ( * * **Example** (Checking for a key) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.make(["key1", 42], ["key2", 100]) * - * console.log(MutableHashMap.has(map, "key1")) // true - * console.log(MutableHashMap.has(map, "key3")) // false + * MutableHashMap.has(map, "key1") // => true + * MutableHashMap.has(map, "key3") // => false * * // Pipe-able version - * const hasKey = MutableHashMap.has("key1") - * console.log(hasKey(map)) // true + * MutableHashMap.has("key1")(map) // => true * ``` * * @see {@link get} for reading the value as an `Option` * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -427,8 +408,8 @@ export const has: { * * **Example** (Setting key-value pairs) * - * ```ts - * import { MutableHashMap } from "effect" + * ```ts import.meta.vitest + * import { MutableHashMap, Option } from "effect" * * const map = MutableHashMap.empty() * @@ -436,17 +417,16 @@ export const has: { * MutableHashMap.set(map, "key1", 42) * MutableHashMap.set(map, "key2", 100) * - * console.log(MutableHashMap.get(map, "key1")) // Some(42) - * console.log(MutableHashMap.size(map)) // 2 + * MutableHashMap.get(map, "key1") // => Option.some(42) + * MutableHashMap.size(map) // => 2 * * // Update existing entry * MutableHashMap.set(map, "key1", 999) - * console.log(MutableHashMap.get(map, "key1")) // Some(999) + * MutableHashMap.get(map, "key1") // => Option.some(999) * * // Pipe-able version - * const setKey = MutableHashMap.set("key3", 300) - * setKey(map) - * console.log(MutableHashMap.size(map)) // 3 + * MutableHashMap.set("key3", 300)(map) + * MutableHashMap.size(map) // => 3 * ``` * * @see {@link modify} for updating an existing value with a function @@ -513,26 +493,26 @@ const getRefKey = ( * * **Example** (Modifying existing values) * - * ```ts - * import { MutableHashMap } from "effect" + * ```ts import.meta.vitest + * import { MutableHashMap, Option } from "effect" * * const map = MutableHashMap.make(["count", 5], ["total", 100]) * * // Increment existing value * MutableHashMap.modify(map, "count", (n) => n + 1) - * console.log(MutableHashMap.get(map, "count")) // Some(6) + * MutableHashMap.get(map, "count") // => Option.some(6) * * // Double existing value * MutableHashMap.modify(map, "total", (n) => n * 2) - * console.log(MutableHashMap.get(map, "total")) // Some(200) + * MutableHashMap.get(map, "total") // => Option.some(200) * * // Try to modify non-existent key (no effect) * MutableHashMap.modify(map, "missing", (n) => n + 1) - * console.log(MutableHashMap.has(map, "missing")) // false + * MutableHashMap.has(map, "missing") // => false * * // Pipe-able version - * const increment = MutableHashMap.modify("count", (n: number) => n + 1) - * increment(map) + * MutableHashMap.modify("count", (n: number) => n + 1)(map) + * MutableHashMap.get(map, "count") // => Option.some(7) * ``` * * @see {@link set} for inserting or replacing a value directly @@ -586,7 +566,7 @@ export const modify: { * * **Example** (Updating or removing a key) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap, Option } from "effect" * * const map = MutableHashMap.make(["count", 5]) @@ -597,7 +577,7 @@ export const modify: { * "count", * (option) => Option.map(option, (n) => n * 2) * ) - * console.log(MutableHashMap.get(map, "count")) // Some(10) + * MutableHashMap.get(map, "count") // => Option.some(10) * * // Add new key * MutableHashMap.modifyAt( @@ -605,11 +585,11 @@ export const modify: { * "new", * (option) => Option.isNone(option) ? Option.some(42) : option * ) - * console.log(MutableHashMap.get(map, "new")) // Some(42) + * MutableHashMap.get(map, "new") // => Option.some(42) * * // Remove key by returning None * MutableHashMap.modifyAt(map, "count", () => Option.none()) - * console.log(MutableHashMap.has(map, "count")) // false + * MutableHashMap.get(map, "count") // => Option.none() * * // Conditional update * MutableHashMap.modifyAt( @@ -617,7 +597,7 @@ export const modify: { * "new", * (option) => Option.filter(option, (n) => n > 50) // Remove if <= 50 * ) - * console.log(MutableHashMap.has(map, "new")) // false (42 <= 50) + * MutableHashMap.get(map, "new") // => Option.none() * ``` * * @see {@link modify} for updating only when the key already exists @@ -663,7 +643,7 @@ export const modifyAt: { * * **Example** (Removing a key) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.make( @@ -672,21 +652,20 @@ export const modifyAt: { * ["key3", 200] * ) * - * console.log(MutableHashMap.size(map)) // 3 + * MutableHashMap.size(map) // => 3 * * // Remove existing key * MutableHashMap.remove(map, "key2") - * console.log(MutableHashMap.size(map)) // 2 - * console.log(MutableHashMap.has(map, "key2")) // false + * MutableHashMap.size(map) // => 2 + * MutableHashMap.has(map, "key2") // => false * * // Remove non-existent key (no effect) * MutableHashMap.remove(map, "nonexistent") - * console.log(MutableHashMap.size(map)) // 2 + * MutableHashMap.size(map) // => 2 * * // Pipe-able version - * const removeKey = MutableHashMap.remove("key1") - * removeKey(map) - * console.log(MutableHashMap.size(map)) // 1 + * MutableHashMap.remove("key1")(map) + * MutableHashMap.size(map) // => 1 * ``` * * @see {@link clear} for removing all entries @@ -737,7 +716,7 @@ export const remove: { * * **Example** (Clearing all entries) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.make( @@ -746,17 +725,17 @@ export const remove: { * ["key3", 200] * ) * - * console.log(MutableHashMap.size(map)) // 3 + * MutableHashMap.size(map) // => 3 * * // Clear all entries * MutableHashMap.clear(map) * - * console.log(MutableHashMap.size(map)) // 0 - * console.log(MutableHashMap.has(map, "key1")) // false + * MutableHashMap.size(map) // => 0 + * MutableHashMap.has(map, "key1") // => false * * // Can still add new entries after clearing * MutableHashMap.set(map, "new", 999) - * console.log(MutableHashMap.size(map)) // 1 + * Array.from(map) // => [["new", 999]] * ``` * * @see {@link remove} for deleting one key @@ -780,26 +759,26 @@ export const clear = (self: MutableHashMap) => { * * **Example** (Checking map size) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashMap } from "effect" * * const map = MutableHashMap.empty() - * console.log(MutableHashMap.size(map)) // 0 + * MutableHashMap.size(map) // => 0 * * MutableHashMap.set(map, "key1", 42) * MutableHashMap.set(map, "key2", 100) - * console.log(MutableHashMap.size(map)) // 2 + * MutableHashMap.size(map) // => 2 * * MutableHashMap.remove(map, "key1") - * console.log(MutableHashMap.size(map)) // 1 + * MutableHashMap.size(map) // => 1 * * MutableHashMap.clear(map) - * console.log(MutableHashMap.size(map)) // 0 + * MutableHashMap.size(map) // => 0 * ``` * * @see {@link isEmpty} for checking whether the map has no entries * - * @category elements + * @category getters * @since 2.0.0 */ export const size = (self: MutableHashMap): number => self.backing.size diff --git a/.context/effect/packages/effect/src/MutableHashSet.ts b/.context/effect/packages/effect/src/MutableHashSet.ts index 1c5dc0a02..3b1965c9b 100644 --- a/.context/effect/packages/effect/src/MutableHashSet.ts +++ b/.context/effect/packages/effect/src/MutableHashSet.ts @@ -36,7 +36,7 @@ const TypeId = "~effect/collections/MutableHashSet" * * **Example** (Using a mutable hash set) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * // Create a mutable hash set @@ -49,16 +49,14 @@ const TypeId = "~effect/collections/MutableHashSet" * MutableHashSet.add(set, "cherry") * * // Check if elements exist - * console.log(MutableHashSet.has(set, "apple")) // true - * console.log(MutableHashSet.has(set, "grape")) // false + * MutableHashSet.has(set, "apple") // => true + * MutableHashSet.has(set, "grape") // => false * - * // Iterate over elements - * for (const value of set) { - * console.log(value) // "apple", "banana", "cherry" - * } + * // Collect the iterator values + * Array.from(set) // => ["apple", "banana", "cherry"] * * // Get size - * console.log(MutableHashSet.size(set)) // 3 + * MutableHashSet.size(set) // => 3 * ``` * * @category models @@ -86,7 +84,7 @@ export interface MutableHashSet extends Iterable, Pipeable, Inspectabl * * @see {@link MutableHashSet} for the mutable hash set interface * - * @category refinements + * @category guards * @since 4.0.0 */ export const isMutableHashSet = (value: unknown): value is MutableHashSet => hasProperty(value, TypeId) @@ -132,7 +130,7 @@ const fromHashMap = (keyMap: MutableHashMap.MutableHashMap): Muta * * **Example** (Creating an empty set) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.empty() @@ -142,8 +140,8 @@ const fromHashMap = (keyMap: MutableHashMap.MutableHashMap): Muta * MutableHashSet.add(set, "banana") * MutableHashSet.add(set, "apple") // Duplicate, no effect * - * console.log(MutableHashSet.size(set)) // 2 - * console.log(Array.from(set)) // ["apple", "banana"] + * MutableHashSet.size(set) // => 2 + * Array.from(set) // => ["apple", "banana"] * ``` * * @see {@link make} for creating a set from explicit values @@ -165,22 +163,20 @@ export const empty = (): MutableHashSet => fromHashMap(MutableHash * * **Example** (Creating a set from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const values = ["apple", "banana", "apple", "cherry", "banana"] * const set = MutableHashSet.fromIterable(values) * - * console.log(MutableHashSet.size(set)) // 3 - * console.log(Array.from(set)) // ["apple", "banana", "cherry"] + * MutableHashSet.size(set) // => 3 + * Array.from(set) // => ["apple", "banana", "cherry"] * * // Works with any iterable - * const fromSet = MutableHashSet.fromIterable(new Set([1, 2, 3])) - * console.log(MutableHashSet.size(fromSet)) // 3 + * MutableHashSet.size(MutableHashSet.fromIterable(new Set([1, 2, 3]))) // => 3 * * // From string characters - * const fromString = MutableHashSet.fromIterable("hello") - * console.log(Array.from(fromString)) // ["h", "e", "l", "o"] + * Array.from(MutableHashSet.fromIterable("hello")) // => ["h", "e", "l", "o"] * ``` * * @category constructors @@ -199,22 +195,21 @@ export const fromIterable = (keys: Iterable): MutableHashSet => * * **Example** (Creating a set from values) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.make("apple", "banana", "apple", "cherry") * - * console.log(MutableHashSet.size(set)) // 3 - * console.log(Array.from(set)) // ["apple", "banana", "cherry"] + * MutableHashSet.size(set) // => 3 + * Array.from(set) // => ["apple", "banana", "cherry"] * * // With numbers * const numbers = MutableHashSet.make(1, 2, 3, 2, 1) - * console.log(MutableHashSet.size(numbers)) // 3 - * console.log(Array.from(numbers)) // [1, 2, 3] + * MutableHashSet.size(numbers) // => 3 + * Array.from(numbers) // => [1, 2, 3] * * // Mixed types - * const mixed = MutableHashSet.make("hello", 42, true, "hello") - * console.log(MutableHashSet.size(mixed)) // 3 + * MutableHashSet.size(MutableHashSet.make("hello", 42, true, "hello")) // => 3 * ``` * * @category constructors @@ -234,7 +229,7 @@ export const make = >( * * **Example** (Adding values) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.empty() @@ -243,17 +238,17 @@ export const make = >( * MutableHashSet.add(set, "apple") * MutableHashSet.add(set, "banana") * - * console.log(MutableHashSet.size(set)) // 2 - * console.log(MutableHashSet.has(set, "apple")) // true + * MutableHashSet.size(set) // => 2 + * MutableHashSet.has(set, "apple") // => true * * // Add duplicate (no effect) * MutableHashSet.add(set, "apple") - * console.log(MutableHashSet.size(set)) // 2 + * MutableHashSet.size(set) // => 2 * * // Pipe-able version * const addFruit = MutableHashSet.add("cherry") * addFruit(set) - * console.log(MutableHashSet.size(set)) // 3 + * MutableHashSet.size(set) // => 3 * ``` * * @category mutations @@ -281,27 +276,27 @@ export const add: { * * **Example** (Checking for a value) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.make("apple", "banana", "cherry") * - * console.log(MutableHashSet.has(set, "apple")) // true - * console.log(MutableHashSet.has(set, "grape")) // false + * MutableHashSet.has(set, "apple") // => true + * MutableHashSet.has(set, "grape") // => false * * // Pipe-able version * const hasApple = MutableHashSet.has("apple") - * console.log(hasApple(set)) // true + * hasApple(set) // => true * * // Check after adding * MutableHashSet.add(set, "grape") - * console.log(MutableHashSet.has(set, "grape")) // true + * MutableHashSet.has(set, "grape") // => true * ``` * * @see {@link add} for adding a value to the set * @see {@link remove} for removing a value from the set * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -322,26 +317,26 @@ export const has: { * * **Example** (Removing a value) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.make("apple", "banana", "cherry") * - * console.log(MutableHashSet.size(set)) // 3 + * MutableHashSet.size(set) // => 3 * * // Remove existing value * MutableHashSet.remove(set, "banana") - * console.log(MutableHashSet.size(set)) // 2 - * console.log(MutableHashSet.has(set, "banana")) // false + * MutableHashSet.size(set) // => 2 + * MutableHashSet.has(set, "banana") // => false * * // Remove non-existent value (no effect) * MutableHashSet.remove(set, "grape") - * console.log(MutableHashSet.size(set)) // 2 + * MutableHashSet.size(set) // => 2 * * // Pipe-able version * const removeFruit = MutableHashSet.remove("apple") * removeFruit(set) - * console.log(MutableHashSet.size(set)) // 1 + * MutableHashSet.size(set) // => 1 * ``` * * @category mutations @@ -364,25 +359,25 @@ export const remove: { * * **Example** (Checking set size) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.empty() - * console.log(MutableHashSet.size(set)) // 0 + * MutableHashSet.size(set) // => 0 * * MutableHashSet.add(set, "apple") * MutableHashSet.add(set, "banana") * MutableHashSet.add(set, "apple") // Duplicate - * console.log(MutableHashSet.size(set)) // 2 + * MutableHashSet.size(set) // => 2 * * MutableHashSet.remove(set, "apple") - * console.log(MutableHashSet.size(set)) // 1 + * MutableHashSet.size(set) // => 1 * * MutableHashSet.clear(set) - * console.log(MutableHashSet.size(set)) // 0 + * MutableHashSet.size(set) // => 0 * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const size = (self: MutableHashSet): number => MutableHashMap.size(self.keyMap) @@ -397,23 +392,23 @@ export const size = (self: MutableHashSet): number => MutableHashMap.size( * * **Example** (Clearing all values) * - * ```ts + * ```ts import.meta.vitest * import { MutableHashSet } from "effect" * * const set = MutableHashSet.make("apple", "banana", "cherry") * - * console.log(MutableHashSet.size(set)) // 3 + * MutableHashSet.size(set) // => 3 * * // Clear all values * MutableHashSet.clear(set) * - * console.log(MutableHashSet.size(set)) // 0 - * console.log(MutableHashSet.has(set, "apple")) // false - * console.log(Array.from(set)) // [] + * MutableHashSet.size(set) // => 0 + * MutableHashSet.has(set, "apple") // => false + * Array.from(set) // => [] * * // Can still add new values after clearing * MutableHashSet.add(set, "new") - * console.log(MutableHashSet.size(set)) // 1 + * MutableHashSet.size(set) // => 1 * ``` * * @category mutations diff --git a/.context/effect/packages/effect/src/MutableList.ts b/.context/effect/packages/effect/src/MutableList.ts index 608fe4ada..e40106a5e 100644 --- a/.context/effect/packages/effect/src/MutableList.ts +++ b/.context/effect/packages/effect/src/MutableList.ts @@ -17,26 +17,16 @@ import * as Arr from "./Array.ts" * * **Example** (Creating and consuming a mutable list) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * - * // Create a mutable list * const list: MutableList.MutableList = MutableList.make() - * - * // Add elements * MutableList.append(list, 1) * MutableList.append(list, 2) * MutableList.prepend(list, 0) * - * // Access properties - * console.log(list.length) // 3 - * console.log(list.head?.array) // Contains elements from head bucket - * console.log(list.tail?.array) // Contains elements from tail bucket - * - * // Take elements - * console.log(MutableList.take(list)) // 0 - * console.log(MutableList.take(list)) // 1 - * console.log(MutableList.take(list)) // 2 + * MutableList.takeAll(list) // => [0, 1, 2] + * list.length // => 0 * ``` * * @category models @@ -52,36 +42,6 @@ export interface MutableList { * The MutableList namespace contains type definitions and utilities for working * with mutable linked lists. * - * **Example** (Typing queue processors) - * - * ```ts - * import { MutableList } from "effect" - * - * // Type annotation using the namespace - * const processQueue = (queue: MutableList.MutableList) => { - * while (queue.length > 0) { - * const item = MutableList.take(queue) - * if (item !== MutableList.Empty) { - * console.log("Processing:", item) - * } - * } - * } - * - * // Using the namespace for type definitions - * const createProcessor = (): { - * queue: MutableList.MutableList - * add: (item: T) => void - * process: () => Array - * } => { - * const queue = MutableList.make() - * return { - * queue, - * add: (item) => MutableList.append(queue, item), - * process: () => MutableList.takeAll(queue) - * } - * } - * ``` - * * @since 2.0.0 */ export declare namespace MutableList { @@ -97,27 +57,19 @@ export declare namespace MutableList { * * **Example** (Inspecting buckets) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.append(list, 1) * MutableList.append(list, 2) * - * // Access bucket information (for debugging or advanced usage) - * const inspectBucket = ( - * bucket: MutableList.MutableList.Bucket | undefined - * ) => { - * if (bucket) { - * console.log("Bucket array:", bucket.array) - * console.log("Bucket offset:", bucket.offset) - * console.log("Bucket mutable:", bucket.mutable) - * console.log("Has next bucket:", bucket.next !== undefined) - * } - * } + * const bucket: MutableList.MutableList.Bucket = list.head! * - * inspectBucket(list.head) - * inspectBucket(list.tail) + * bucket.array // => [1, 2] + * bucket.offset // => 0 + * bucket.mutable // => true + * bucket.next === undefined // => true * ``` * * @category models @@ -142,32 +94,12 @@ export declare namespace MutableList { * * **Example** (Checking for empty results) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * - * // Take from empty list returns Empty symbol - * const result = MutableList.take(list) - * console.log(result === MutableList.Empty) // true - * - * // Safe pattern for checking emptiness - * const processNext = (queue: MutableList.MutableList) => { - * const item = MutableList.take(queue) - * if (item === MutableList.Empty) { - * console.log("Queue is empty") - * return null - * } - * return item.toUpperCase() - * } - * - * // Compare with other empty results - * MutableList.append(list, "hello") - * const next = MutableList.take(list) - * console.log(next !== MutableList.Empty) // true, got "hello" - * - * const empty = MutableList.take(list) - * console.log(empty === MutableList.Empty) // true, list is empty + * MutableList.take(list) === MutableList.Empty // => true * ``` * * @category symbols @@ -181,43 +113,19 @@ export const Empty: unique symbol = Symbol.for("effect/MutableList/Empty") * * **Example** (Handling empty results type-safely) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * - * // Type-safe handling of empty results - * const takeAndDouble = ( - * queue: MutableList.MutableList - * ): number | null => { + * const takeAndDouble = (queue: MutableList.MutableList): number | null => { * const item: number | MutableList.Empty = MutableList.take(queue) - * - * if (item === MutableList.Empty) { - * return null - * } - * - * // TypeScript knows item is number here - * return item * 2 + * return item === MutableList.Empty ? null : item * 2 * } * - * console.log(takeAndDouble(list)) // null (empty list) - * + * takeAndDouble(list) // => null * MutableList.append(list, 5) - * console.log(takeAndDouble(list)) // 10 - * - * // Type guard function - * const isEmpty = ( - * result: number | MutableList.Empty - * ): result is MutableList.Empty => { - * return result === MutableList.Empty - * } - * - * const value = MutableList.take(list) - * if (isEmpty(value)) { - * console.log("List is empty") - * } else { - * console.log("Got value:", value) - * } + * takeAndDouble(list) // => 10 * ``` * * @category symbols @@ -230,22 +138,15 @@ export type Empty = typeof Empty * * **Example** (Creating an empty mutable list) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * - * // Add elements + * list.length // => 0 * MutableList.append(list, "first") - * MutableList.append(list, "second") - * MutableList.prepend(list, "beginning") - * - * console.log(list.length) // 3 - * - * // Take elements in FIFO order (from head) - * console.log(MutableList.take(list)) // "beginning" - * console.log(MutableList.take(list)) // "first" - * console.log(MutableList.take(list)) // "second" + * MutableList.take(list) // => "first" + * list.length // => 0 * ``` * * @category constructors @@ -270,27 +171,16 @@ const emptyBucket = (): MutableList.Bucket => ({ * * **Example** (Appending elements) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() - * - * // Append elements one by one * MutableList.append(list, 1) * MutableList.append(list, 2) * MutableList.append(list, 3) * - * console.log(list.length) // 3 - * - * // Elements are taken from head (FIFO) - * console.log(MutableList.take(list)) // 1 - * console.log(MutableList.take(list)) // 2 - * console.log(MutableList.take(list)) // 3 - * - * // High-throughput usage - * for (let i = 0; i < 10000; i++) { - * MutableList.append(list, i) - * } + * MutableList.toArray(list) // => [1, 2, 3] + * list.length // => 3 * ``` * * @category mutations @@ -313,27 +203,16 @@ export const append = (self: MutableList, message: A): void => { * * **Example** (Prepending elements) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() - * - * // Prepend elements (they'll be at the front) + * MutableList.append(list, "last") * MutableList.prepend(list, "third") * MutableList.prepend(list, "second") * MutableList.prepend(list, "first") * - * console.log(list.length) // 3 - * - * // Elements taken from head (most recently prepended first) - * console.log(MutableList.take(list)) // "first" - * console.log(MutableList.take(list)) // "second" - * console.log(MutableList.take(list)) // "third" - * - * // Use case: priority items or stack-like behavior - * MutableList.append(list, "normal") - * MutableList.prepend(list, "priority") // This will be taken first - * console.log(MutableList.take(list)) // "priority" + * MutableList.toArray(list) // => ["first", "second", "third", "last"] * ``` * * @category mutations @@ -346,6 +225,7 @@ export const prepend = (self: MutableList, message: A): void => { offset: 0, next: self.head } + if (!self.tail) self.tail = self.head self.length++ } @@ -356,25 +236,15 @@ export const prepend = (self: MutableList, message: A): void => { * * **Example** (Prepending multiple elements) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.append(list, 4) * MutableList.append(list, 5) - * - * // Prepend multiple elements * MutableList.prependAll(list, [1, 2, 3]) * - * console.log(list.length) // 5 - * - * // Elements are taken in order: [1, 2, 3, 4, 5] - * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5] - * - * // Works with any iterable - * const newList = MutableList.make() - * MutableList.prependAll(newList, "hello") // Prepends each character - * console.log(MutableList.takeAll(newList)) // ["h", "e", "l", "l", "o"] + * MutableList.toArray(list) // => [1, 2, 3, 4, 5] * ``` * * @category mutations @@ -390,32 +260,24 @@ export const prependAll = (self: MutableList, messages: Iterable): void * **When to use** * * Use when prepending a trusted array directly is worth the optimized path and - * you control whether the input may be reused. + * you can transfer ownership of the input when enabling mutation. * * **Gotchas** * - * When mutable=true, the input array may be modified internally. Only use - * mutable=true when you control the array lifecycle. + * When mutable=true, ownership of the input array transfers to the list. Do not + * read or modify the array afterward. * - * **Example** (Prepending arrays with optional mutation) + * **Example** (Transferring an array when prepending) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.append(list, 4) - * - * // Safe usage (default mutable=false) * const items = [1, 2, 3] - * MutableList.prependAllUnsafe(list, items) - * console.log(items) // [1, 2, 3] - unchanged - * - * // Unsafe but efficient usage (mutable=true) - * const mutableItems = [10, 20, 30] - * MutableList.prependAllUnsafe(list, mutableItems, true) - * // mutableItems may be modified internally for efficiency + * MutableList.prependAllUnsafe(list, items, true) * - * console.log(MutableList.takeAll(list)) // [10, 20, 30, 1, 2, 3, 4] + * MutableList.toArray(list) // => [1, 2, 3, 4] * ``` * * @category mutations @@ -437,33 +299,16 @@ export const prependAllUnsafe = (self: MutableList, messages: ReadonlyArra * * **Example** (Appending multiple elements) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.append(list, 1) * MutableList.append(list, 2) * - * // Append multiple elements - * const added = MutableList.appendAll(list, [3, 4, 5]) - * console.log(added) // 3 - * console.log(list.length) // 5 - * - * // Elements maintain order: [1, 2, 3, 4, 5] - * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5] - * - * // Works with any iterable - * const newList = MutableList.make() - * MutableList.appendAll(newList, new Set(["a", "b", "c"])) - * console.log(MutableList.takeAll(newList)) // ["a", "b", "c"] - * - * // Useful for bulk loading - * const bulkList = MutableList.make() - * const count = MutableList.appendAll( - * bulkList, - * Array.from({ length: 1000 }, (_, i) => i) - * ) - * console.log(count) // 1000 + * MutableList.appendAll(list, [3, 4, 5]) // => 3 + * MutableList.toArray(list) // => [1, 2, 3, 4, 5] + * list.length // => 5 * ``` * * @category mutations @@ -480,37 +325,24 @@ export const appendAll = (self: MutableList, messages: Iterable): numbe * **When to use** * * Use when appending a trusted array directly is worth the optimized path and - * you control whether the input may be reused. + * you can transfer ownership of the input when enabling mutation. * * **Gotchas** * - * When mutable=true, the input array may be modified internally. Only use - * mutable=true when you control the array lifecycle. + * When mutable=true, ownership of the input array transfers to the list. Do not + * read or modify the array afterward. * - * **Example** (Appending arrays with optional mutation) + * **Example** (Transferring an array when appending) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.append(list, 1) - * - * // Safe usage (default mutable=false) * const items = [2, 3, 4] - * const added = MutableList.appendAllUnsafe(list, items) - * console.log(added) // 3 - * console.log(items) // [2, 3, 4] - unchanged - * - * // Unsafe but efficient usage (mutable=true) - * const mutableItems = [5, 6, 7] - * MutableList.appendAllUnsafe(list, mutableItems, true) - * // mutableItems may be modified internally for efficiency + * MutableList.appendAllUnsafe(list, items, true) // => 3 * - * console.log(MutableList.takeAll(list)) // [1, 2, 3, 4, 5, 6, 7] - * - * // High-performance bulk operations - * const bigArray = new Array(10000).fill(0).map((_, i) => i) - * MutableList.appendAllUnsafe(list, bigArray, true) // Very efficient + * MutableList.toArray(list) // => [1, 2, 3, 4] * ``` * * @category mutations @@ -541,29 +373,17 @@ export const appendAllUnsafe = (self: MutableList, messages: ReadonlyArray * * **Example** (Clearing a mutable list) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, [1, 2, 3, 4, 5]) * - * console.log(list.length) // 5 - * - * // Clear all elements * MutableList.clear(list) * - * console.log(list.length) // 0 - * console.log(MutableList.take(list)) // Empty - * - * // Can still use the list after clearing - * MutableList.append(list, 42) - * console.log(list.length) // 1 - * - * // Useful for resetting queues or buffers - * function resetBuffer(buffer: MutableList.MutableList) { - * MutableList.clear(buffer) - * console.log("Buffer cleared and ready for reuse") - * } + * MutableList.toArray(list) // => [] + * list.length // => 0 + * MutableList.take(list) === MutableList.Empty // => true * ``` * * @category mutations @@ -581,39 +401,18 @@ export const clear = (self: MutableList): void => { * * **Example** (Taking batches) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) * - * console.log(list.length) // 10 - * - * // Take first 3 elements - * const first3 = MutableList.takeN(list, 3) - * console.log(first3) // [1, 2, 3] - * console.log(list.length) // 7 - * - * // Take more than available - * const remaining = MutableList.takeN(list, 20) - * console.log(remaining) // [4, 5, 6, 7, 8, 9, 10] - * console.log(list.length) // 0 - * - * // Take from empty list - * const empty = MutableList.takeN(list, 5) - * console.log(empty) // [] - * - * // Batch processing pattern - * const queue = MutableList.make() - * MutableList.appendAll(queue, ["task1", "task2", "task3", "task4", "task5"]) - * - * while (queue.length > 0) { - * const batch = MutableList.takeN(queue, 2) // Process 2 at a time - * console.log("Processing batch:", batch) - * } + * MutableList.takeN(list, 3) // => [1, 2, 3] + * MutableList.toArray(list) // => [4, 5, 6, 7, 8, 9, 10] + * list.length // => 7 * ``` * - * @category elements + * @category mutations * @since 4.0.0 */ export const takeN = (self: MutableList, n: number): Array => { @@ -663,7 +462,7 @@ export const takeN = (self: MutableList, n: number): Array => { * @see {@link takeN} for removing up to `n` values and returning them as an array * @see {@link clear} for removing every value from the list * - * @category elements + * @category mutations * @since 4.0.0 */ export const takeNVoid = (self: MutableList, n: number): void => { @@ -696,40 +495,17 @@ export const takeNVoid = (self: MutableList, n: number): void => { * * **Example** (Draining all elements) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, ["apple", "banana", "cherry"]) * - * console.log(list.length) // 3 - * - * // Take all elements - * const allItems = MutableList.takeAll(list) - * console.log(allItems) // ["apple", "banana", "cherry"] - * console.log(list.length) // 0 - * - * // Useful for converting to array and clearing - * const queue = MutableList.make() - * MutableList.appendAll(queue, [1, 2, 3, 4, 5]) - * - * const snapshot = MutableList.takeAll(queue) - * console.log("Queue contents:", snapshot) - * console.log("Queue is now empty:", queue.length === 0) - * - * // Drain pattern for processing - * function drainAndProcess( - * list: MutableList.MutableList, - * processor: (items: Array) => void - * ) { - * if (list.length > 0) { - * const items = MutableList.takeAll(list) - * processor(items) - * } - * } + * MutableList.takeAll(list) // => ["apple", "banana", "cherry"] + * list.length // => 0 * ``` * - * @category elements + * @category mutations * @since 4.0.0 */ export const takeAll = (self: MutableList): Array => takeN(self, self.length) @@ -741,46 +517,18 @@ export const takeAll = (self: MutableList): Array => takeN(self, self.l * * **Example** (Taking one element) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, ["first", "second", "third"]) * - * // Take elements one by one - * console.log(MutableList.take(list)) // "first" - * console.log(list.length) // 2 - * - * console.log(MutableList.take(list)) // "second" - * console.log(MutableList.take(list)) // "third" - * console.log(list.length) // 0 - * - * // Take from empty list - * console.log(MutableList.take(list)) // Empty symbol - * - * // Check for empty using the Empty symbol - * const result = MutableList.take(list) - * if (result === MutableList.Empty) { - * console.log("List is empty") - * } else { - * console.log("Got element:", result) - * } - * - * // Consumer pattern - * function processNext( - * queue: MutableList.MutableList, - * processor: (item: T) => void - * ): boolean { - * const item = MutableList.take(queue) - * if (item !== MutableList.Empty) { - * processor(item) - * return true - * } - * return false - * } + * MutableList.take(list) // => "first" + * MutableList.toArray(list) // => ["second", "third"] + * list.length // => 2 * ``` * - * @category elements + * @category mutations * @since 4.0.0 */ export const take = (self: MutableList): Empty | A => { @@ -810,10 +558,11 @@ export const take = (self: MutableList): Empty | A => { * * @see {@link takeN} for removing up to `n` values and returning them as an array * - * @category elements + * @category converting * @since 4.0.0 */ export const toArrayN = (self: MutableList, n: number): Array => { + if (n <= 0) return [] const length = Math.min(n, self.length) const out = new Array(length) let index = 0 @@ -839,7 +588,7 @@ export const toArrayN = (self: MutableList, n: number): Array => { * * @see {@link takeAll} for converting all elements to an array and clearing the list * - * @category elements + * @category converting * @since 4.0.0 */ export const toArray = (self: MutableList): Array => toArrayN(self, self.length) @@ -850,39 +599,15 @@ export const toArray = (self: MutableList): Array => toArrayN(self, sel * * **Example** (Filtering in place) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) * - * console.log(list.length) // 10 - * - * // Keep only even numbers * MutableList.filter(list, (n) => n % 2 === 0) * - * console.log(MutableList.takeAll(list)) // [2, 4, 6, 8, 10] - * - * // Filter with index - * const indexed = MutableList.make() - * MutableList.appendAll(indexed, ["a", "b", "c", "d", "e"]) - * - * // Keep elements at even indices - * MutableList.filter(indexed, (value, index) => index % 2 === 0) - * console.log(MutableList.takeAll(indexed)) // ["a", "c", "e"] - * - * // Real-world example: filtering a log queue - * const logs = MutableList.make<{ level: string; message: string }>() - * MutableList.appendAll(logs, [ - * { level: "INFO", message: "App started" }, - * { level: "ERROR", message: "Connection failed" }, - * { level: "DEBUG", message: "Cache hit" }, - * { level: "ERROR", message: "Timeout" } - * ]) - * - * // Keep only errors - * MutableList.filter(logs, (log) => log.level === "ERROR") - * console.log(MutableList.takeAll(logs).map((log) => log.message)) // ["Connection failed", "Timeout"] + * MutableList.toArray(list) // => [2, 4, 6, 8, 10] * ``` * * @category mutations @@ -932,37 +657,15 @@ export const filter = (self: MutableList, f: (value: A, i: number) => bool * * **Example** (Removing matching values) * - * ```ts + * ```ts import.meta.vitest * import { MutableList } from "effect" * * const list = MutableList.make() * MutableList.appendAll(list, ["apple", "banana", "apple", "cherry", "apple"]) * - * console.log(list.length) // 5 - * - * // Remove all occurrences of "apple" * MutableList.remove(list, "apple") * - * console.log(MutableList.takeAll(list)) // ["banana", "cherry"] - * - * // Remove non-existent value (no effect) - * const colors = MutableList.make() - * MutableList.appendAll(colors, ["red", "blue"]) - * MutableList.remove(colors, "green") - * console.log(MutableList.takeAll(colors)) // ["red", "blue"] - * - * // Real-world example: removing completed tasks - * const tasks = MutableList.make<{ id: number; status: string }>() - * MutableList.appendAll(tasks, [ - * { id: 1, status: "pending" }, - * { id: 2, status: "completed" }, - * { id: 3, status: "pending" }, - * { id: 4, status: "completed" } - * ]) - * - * // Remove completed tasks by filtering status - * MutableList.filter(tasks, (task) => task.status !== "completed") - * console.log(MutableList.takeAll(tasks).map((task) => task.id)) // [1, 3] + * MutableList.toArray(list) // => ["banana", "cherry"] * ``` * * @category mutations diff --git a/.context/effect/packages/effect/src/MutableRef.ts b/.context/effect/packages/effect/src/MutableRef.ts index b8a042e84..acd270380 100644 --- a/.context/effect/packages/effect/src/MutableRef.ts +++ b/.context/effect/packages/effect/src/MutableRef.ts @@ -32,19 +32,20 @@ const TypeId = "~effect/MutableRef" * * **Example** (Creating and updating refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * // Create a mutable reference * const ref: MutableRef.MutableRef = MutableRef.make(42) * * // Read the current value - * console.log(ref.current) // 42 - * console.log(MutableRef.get(ref)) // 42 + * ref.current // => 42 + * MutableRef.get(ref) // => 42 * * // Update the value * ref.current = 100 - * console.log(MutableRef.get(ref)) // 100 + * + * MutableRef.get(ref) // => 100 * * // Use with complex types * interface Config { @@ -59,7 +60,8 @@ const TypeId = "~effect/MutableRef" * * // Update through the interface * config.current = { timeout: 10000, retries: 5 } - * console.log(config.current.timeout) // 10000 + * + * config.current // => { timeout: 10000, retries: 5 } * ``` * * @category models @@ -90,21 +92,24 @@ const MutableRefProto: Omit, "current"> = { * * **Example** (Creating mutable refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * // Create a counter reference * const counter = MutableRef.make(0) - * console.log(MutableRef.get(counter)) // 0 + * + * MutableRef.get(counter) // => 0 * * // Create a configuration reference * const config = MutableRef.make({ debug: false, timeout: 5000 }) - * console.log(MutableRef.get(config)) // { debug: false, timeout: 5000 } + * + * MutableRef.get(config) // => { debug: false, timeout: 5000 } * * // Create a string reference * const status = MutableRef.make("idle") * MutableRef.set(status, "running") - * console.log(MutableRef.get(status)) // "running" + * + * MutableRef.get(status) // => "running" * ``` * * @category constructors @@ -128,20 +133,22 @@ export const make = (value: T): MutableRef => { * * **Example** (Comparing and setting values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const ref = MutableRef.make("initial") * * // Successful compare and set * const updated = MutableRef.compareAndSet(ref, "initial", "updated") - * console.log(updated) // true - * console.log(MutableRef.get(ref)) // "updated" + * + * updated // => true + * MutableRef.get(ref) // => "updated" * * // Failed compare and set (value doesn't match) * const failed = MutableRef.compareAndSet(ref, "initial", "failed") - * console.log(failed) // false - * console.log(MutableRef.get(ref)) // "updated" (unchanged) + * + * failed // => false + * MutableRef.get(ref) // => "updated" * * // Thread-safe counter increment * const counter = MutableRef.make(5) @@ -150,12 +157,16 @@ export const make = (value: T): MutableRef => { * current = MutableRef.get(counter) * } while (!MutableRef.compareAndSet(counter, current, current + 1)) * + * MutableRef.get(counter) // => 6 + * * // Pipe-able version * const casUpdate = MutableRef.compareAndSet("updated", "final") - * console.log(casUpdate(ref)) // true + * + * casUpdate(ref) // => true + * MutableRef.get(ref) // => "final" * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const compareAndSet: { @@ -182,29 +193,32 @@ export const compareAndSet: { * * **Example** (Decrementing numeric refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Decrement the counter * MutableRef.decrement(counter) - * console.log(MutableRef.get(counter)) // 4 + * + * MutableRef.get(counter) // => 4 * * // Chain operations * MutableRef.decrement(counter) * MutableRef.decrement(counter) - * console.log(MutableRef.get(counter)) // 2 + * + * MutableRef.get(counter) // => 2 * * // Useful for countdown scenarios * const countdown = MutableRef.make(10) * while (MutableRef.get(countdown) > 0) { - * console.log(MutableRef.get(countdown)) * MutableRef.decrement(countdown) * } + * + * MutableRef.get(countdown) // => 0 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const decrement = (self: MutableRef): MutableRef => update(self, (n) => n - 1) @@ -219,29 +233,35 @@ export const decrement = (self: MutableRef): MutableRef => updat * * **Example** (Decrementing and reading refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Decrement and get the new value * const newValue = MutableRef.decrementAndGet(counter) - * console.log(newValue) // 4 - * console.log(MutableRef.get(counter)) // 4 + * + * newValue // => 4 + * MutableRef.get(counter) // => 4 * * // Use in expressions * const lives = MutableRef.make(3) - * console.log(`Lives remaining: ${MutableRef.decrementAndGet(lives)}`) // "Lives remaining: 2" + * const message = `Lives remaining: ${MutableRef.decrementAndGet(lives)}` + * + * message // => "Lives remaining: 2" * * // Conditional logic based on decremented value * const attempts = MutableRef.make(3) + * let retries = 0 * while (MutableRef.decrementAndGet(attempts) >= 0) { - * console.log("Retrying...") - * // retry logic + * retries += 1 * } + * + * retries // => 3 + * MutableRef.get(attempts) // => -1 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const decrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n - 1) @@ -255,27 +275,31 @@ export const decrementAndGet = (self: MutableRef): number => updateAndGe * * **Example** (Reading current values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const ref = MutableRef.make("hello") - * console.log(MutableRef.get(ref)) // "hello" + * + * MutableRef.get(ref) // => "hello" * * MutableRef.set(ref, "world") - * console.log(MutableRef.get(ref)) // "world" + * + * MutableRef.get(ref) // => "world" * * // Reading complex objects * const config = MutableRef.make({ port: 3000, host: "localhost" }) * const currentConfig = MutableRef.get(config) - * console.log(currentConfig.port) // 3000 + * + * currentConfig // => { port: 3000, host: "localhost" } * * // Multiple reads return the same value * const value1 = MutableRef.get(ref) * const value2 = MutableRef.get(ref) - * console.log(value1 === value2) // true + * + * value1 === value2 // => true * ``` * - * @category general + * @category getters * @since 2.0.0 */ export const get = (self: MutableRef): T => self.current @@ -289,30 +313,38 @@ export const get = (self: MutableRef): T => self.current * * **Example** (Reading before decrementing) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Get current value and then decrement * const previousValue = MutableRef.getAndDecrement(counter) - * console.log(previousValue) // 5 - * console.log(MutableRef.get(counter)) // 4 + * + * previousValue // => 5 + * MutableRef.get(counter) // => 4 * * // Useful for processing where you need the original value * const itemsLeft = MutableRef.make(10) + * const processedItems: Array = [] * while (MutableRef.get(itemsLeft) > 0) { * const currentItem = MutableRef.getAndDecrement(itemsLeft) - * console.log(`Processing item ${currentItem}`) + * processedItems.push(currentItem) * } * + * processedItems // => [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + * MutableRef.get(itemsLeft) // => 0 + * * // Post-decrement semantics (like i-- in other languages) * const index = MutableRef.make(3) * const currentIndex = MutableRef.getAndDecrement(index) - * console.log(`Current: ${currentIndex}, Next: ${MutableRef.get(index)}`) // "Current: 3, Next: 2" + * const nextIndex = MutableRef.get(index) + * + * currentIndex // => 3 + * nextIndex // => 2 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const getAndDecrement = (self: MutableRef): number => getAndUpdate(self, (n) => n - 1) @@ -326,38 +358,45 @@ export const getAndDecrement = (self: MutableRef): number => getAndUpdat * * **Example** (Reading before incrementing) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Get current value and then increment * const previousValue = MutableRef.getAndIncrement(counter) - * console.log(previousValue) // 5 - * console.log(MutableRef.get(counter)) // 6 + * + * previousValue // => 5 + * MutableRef.get(counter) // => 6 * * // Useful for ID generation * const idGenerator = MutableRef.make(0) * const getId = () => MutableRef.getAndIncrement(idGenerator) + * const ids = [getId(), getId(), getId()] * - * console.log(getId()) // 0 - * console.log(getId()) // 1 - * console.log(getId()) // 2 + * ids // => [0, 1, 2] * * // Post-increment semantics (like i++ in other languages) * const position = MutableRef.make(0) * const currentPos = MutableRef.getAndIncrement(position) - * console.log(`Was at: ${currentPos}, Now at: ${MutableRef.get(position)}`) // "Was at: 0, Now at: 1" + * const nextPos = MutableRef.get(position) + * + * currentPos // => 0 + * nextPos // => 1 * * // Useful for iteration counters * const iterations = MutableRef.make(0) + * const visited: Array = [] * while (MutableRef.get(iterations) < 5) { * const iteration = MutableRef.getAndIncrement(iterations) - * console.log(`Iteration ${iteration}`) + * visited.push(iteration) * } + * + * visited // => [0, 1, 2, 3, 4] + * MutableRef.get(iterations) // => 5 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const getAndIncrement = (self: MutableRef): number => getAndUpdate(self, (n) => n + 1) @@ -372,34 +411,41 @@ export const getAndIncrement = (self: MutableRef): number => getAndUpdat * * **Example** (Reading before setting) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const ref = MutableRef.make("old") * * // Set new value and get the previous one * const previous = MutableRef.getAndSet(ref, "new") - * console.log(previous) // "old" - * console.log(MutableRef.get(ref)) // "new" + * + * previous // => "old" + * MutableRef.get(ref) // => "new" * * // Swapping values * const counter = MutableRef.make(5) * const oldValue = MutableRef.getAndSet(counter, 10) - * console.log(`Changed from ${oldValue} to ${MutableRef.get(counter)}`) // "Changed from 5 to 10" + * const newValue = MutableRef.get(counter) + * + * oldValue // => 5 + * newValue // => 10 * * // Pipe-able version * const setValue = MutableRef.getAndSet("final") * const previousValue = setValue(ref) - * console.log(previousValue) // "new" + * + * previousValue // => "new" + * MutableRef.get(ref) // => "final" * * // Useful for atomic swaps in algorithms * const buffer = MutableRef.make>(["a", "b", "c"]) * const oldBuffer = MutableRef.getAndSet(buffer, []) - * console.log(oldBuffer) // ["a", "b", "c"] - * console.log(MutableRef.get(buffer)) // [] + * + * oldBuffer // => ["a", "b", "c"] + * MutableRef.get(buffer) // => [] * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const getAndSet: { @@ -425,40 +471,46 @@ export const getAndSet: { * * **Example** (Reading before updating) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Increment and get the old value * const oldValue = MutableRef.getAndUpdate(counter, (n) => n + 1) - * console.log(oldValue) // 5 - * console.log(MutableRef.get(counter)) // 6 + * + * oldValue // => 5 + * MutableRef.get(counter) // => 6 * * // Double the value and get the previous one * const previous = MutableRef.getAndUpdate(counter, (n) => n * 2) - * console.log(previous) // 6 - * console.log(MutableRef.get(counter)) // 12 + * + * previous // => 6 + * MutableRef.get(counter) // => 12 * * // Transform string and get old value * const message = MutableRef.make("hello") * const oldMessage = MutableRef.getAndUpdate(message, (s) => s.toUpperCase()) - * console.log(oldMessage) // "hello" - * console.log(MutableRef.get(message)) // "HELLO" + * + * oldMessage // => "hello" + * MutableRef.get(message) // => "HELLO" * * // Pipe-able version * const addOne = MutableRef.getAndUpdate((n: number) => n + 1) * const result = addOne(counter) - * console.log(result) // Previous value before increment + * + * result // => 12 + * MutableRef.get(counter) // => 13 * * // Useful for implementing atomic operations * const list = MutableRef.make>([1, 2, 3]) * const oldList = MutableRef.getAndUpdate(list, (arr) => [...arr, 4]) - * console.log(oldList) // [1, 2, 3] - * console.log(MutableRef.get(list)) // [1, 2, 3, 4] + * + * oldList // => [1, 2, 3] + * MutableRef.get(list) // => [1, 2, 3, 4] * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const getAndUpdate: { @@ -479,32 +531,37 @@ export const getAndUpdate: { * * **Example** (Incrementing numeric refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Increment the counter * MutableRef.increment(counter) - * console.log(MutableRef.get(counter)) // 6 + * + * MutableRef.get(counter) // => 6 * * // Chain operations * MutableRef.increment(counter) * MutableRef.increment(counter) - * console.log(MutableRef.get(counter)) // 8 + * + * MutableRef.get(counter) // => 8 * * // Useful for simple counting * const visits = MutableRef.make(0) * MutableRef.increment(visits) // User visited * MutableRef.increment(visits) // Another visit - * console.log(MutableRef.get(visits)) // 2 + * + * MutableRef.get(visits) // => 2 * * // Returns the reference for chaining * const result = MutableRef.increment(counter) - * console.log(result === counter) // true + * + * result === counter // => true + * MutableRef.get(counter) // => 9 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const increment = (self: MutableRef): MutableRef => update(self, (n) => n + 1) @@ -519,33 +576,38 @@ export const increment = (self: MutableRef): MutableRef => updat * * **Example** (Incrementing and reading refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Increment and get the new value * const newValue = MutableRef.incrementAndGet(counter) - * console.log(newValue) // 6 - * console.log(MutableRef.get(counter)) // 6 + * + * newValue // => 6 + * MutableRef.get(counter) // => 6 * * // Use in expressions * const score = MutableRef.make(100) - * console.log(`New score: ${MutableRef.incrementAndGet(score)}`) // "New score: 101" + * const message = `New score: ${MutableRef.incrementAndGet(score)}` + * + * message // => "New score: 101" * * // Pre-increment semantics (like ++i in other languages) * const level = MutableRef.make(0) * const nextLevel = MutableRef.incrementAndGet(level) - * console.log(`Reached level ${nextLevel}`) // "Reached level 1" + * + * nextLevel // => 1 * * // Conditional logic based on incremented value * const attempts = MutableRef.make(0) - * if (MutableRef.incrementAndGet(attempts) > 3) { - * console.log("Too many attempts") - * } + * const tooManyAttempts = MutableRef.incrementAndGet(attempts) > 3 + * + * tooManyAttempts // => false + * MutableRef.get(attempts) // => 1 * ``` * - * @category numeric + * @category mutations * @since 2.0.0 */ export const incrementAndGet = (self: MutableRef): number => updateAndGet(self, (n) => n + 1) @@ -560,38 +622,44 @@ export const incrementAndGet = (self: MutableRef): number => updateAndGe * * **Example** (Setting values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const ref = MutableRef.make("initial") * * // Set a new value * MutableRef.set(ref, "updated") - * console.log(MutableRef.get(ref)) // "updated" + * + * MutableRef.get(ref) // => "updated" * * // Chain set operations (since it returns the ref) * const result = MutableRef.set(ref, "final") - * console.log(result === ref) // true (same reference) - * console.log(MutableRef.get(ref)) // "final" + * + * result === ref // => true + * MutableRef.get(ref) // => "final" * * // Set complex objects * const config = MutableRef.make({ debug: false, verbose: false }) * MutableRef.set(config, { debug: true, verbose: true }) - * console.log(MutableRef.get(config)) // { debug: true, verbose: true } + * + * MutableRef.get(config) // => { debug: true, verbose: true } * * // Pipe-able version * const setValue = MutableRef.set("new value") * setValue(ref) - * console.log(MutableRef.get(ref)) // "new value" + * + * MutableRef.get(ref) // => "new value" * * // Useful for state management * const state = MutableRef.make<"idle" | "loading" | "success" | "error">("idle") * MutableRef.set(state, "loading") * // ... perform async operation * MutableRef.set(state, "success") + * + * MutableRef.get(state) // => "success" * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const set: { @@ -615,34 +683,40 @@ export const set: { * * **Example** (Setting and reading values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const ref = MutableRef.make("old") * * // Set and get the new value * const newValue = MutableRef.setAndGet(ref, "new") - * console.log(newValue) // "new" - * console.log(MutableRef.get(ref)) // "new" + * + * newValue // => "new" + * MutableRef.get(ref) // => "new" * * // Useful for assignments that need the value * const counter = MutableRef.make(0) * const currentValue = MutableRef.setAndGet(counter, 42) - * console.log(`Counter set to: ${currentValue}`) // "Counter set to: 42" + * + * currentValue // => 42 * * // Pipe-able version * const setValue = MutableRef.setAndGet("final") * const result = setValue(ref) - * console.log(result) // "final" + * + * result // => "final" * * // Difference from set: returns value instead of reference * const ref1 = MutableRef.make(1) * const returnedRef = MutableRef.set(ref1, 2) // Returns MutableRef * const returnedValue = MutableRef.setAndGet(ref1, 3) // Returns value - * console.log(returnedValue) // 3 + * + * returnedRef === ref1 // => true + * returnedValue // => 3 + * MutableRef.get(ref1) // => 3 * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const setAndGet: { @@ -667,42 +741,48 @@ export const setAndGet: { * * **Example** (Updating values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Increment the counter * MutableRef.update(counter, (n) => n + 1) - * console.log(MutableRef.get(counter)) // 6 + * + * MutableRef.get(counter) // => 6 * * // Chain updates (since it returns the ref) * const result = MutableRef.update(counter, (n) => n * 2) - * console.log(result === counter) // true (same reference) - * console.log(MutableRef.get(counter)) // 12 + * + * result === counter // => true + * MutableRef.get(counter) // => 12 * * // Transform string * const message = MutableRef.make("hello") * MutableRef.update(message, (s) => s.toUpperCase()) - * console.log(MutableRef.get(message)) // "HELLO" + * + * MutableRef.get(message) // => "HELLO" * * // Update complex objects * const user = MutableRef.make({ name: "Alice", age: 30 }) * MutableRef.update(user, (u) => ({ ...u, age: u.age + 1 })) - * console.log(MutableRef.get(user)) // { name: "Alice", age: 31 } + * + * MutableRef.get(user) // => { name: "Alice", age: 31 } * * // Pipe-able version * const double = MutableRef.update((n: number) => n * 2) * double(counter) - * console.log(MutableRef.get(counter)) // 24 + * + * MutableRef.get(counter) // => 24 * * // Array operations * const list = MutableRef.make>([1, 2, 3]) * MutableRef.update(list, (arr) => [...arr, 4]) - * console.log(MutableRef.get(list)) // [1, 2, 3, 4] + * + * MutableRef.get(list) // => [1, 2, 3, 4] * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const update: { @@ -724,44 +804,50 @@ export const update: { * * **Example** (Updating and reading values) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const counter = MutableRef.make(5) * * // Increment and get the new value * const newValue = MutableRef.updateAndGet(counter, (n) => n + 1) - * console.log(newValue) // 6 - * console.log(MutableRef.get(counter)) // 6 + * + * newValue // => 6 + * MutableRef.get(counter) // => 6 * * // Double the value and get the result * const doubled = MutableRef.updateAndGet(counter, (n) => n * 2) - * console.log(doubled) // 12 + * + * doubled // => 12 * * // Transform string and get result * const message = MutableRef.make("hello") * const upperCase = MutableRef.updateAndGet(message, (s) => s.toUpperCase()) - * console.log(upperCase) // "HELLO" + * + * upperCase // => "HELLO" * * // Pipe-able version * const increment = MutableRef.updateAndGet((n: number) => n + 1) * const result = increment(counter) - * console.log(result) // 13 (new value) + * + * result // => 13 * * // Useful for calculations that need the result * const score = MutableRef.make(100) * const bonus = 50 * const newScore = MutableRef.updateAndGet(score, (s) => s + bonus) - * console.log(`New score: ${newScore}`) // "New score: 150" + * + * newScore // => 150 * * // Array transformations * const list = MutableRef.make>([1, 2, 3]) * const newList = MutableRef.updateAndGet(list, (arr) => arr.map((x) => x * 2)) - * console.log(newList) // [2, 4, 6] - * console.log(MutableRef.get(list)) // [2, 4, 6] + * + * newList // => [2, 4, 6] + * MutableRef.get(list) // => [2, 4, 6] * ``` * - * @category general + * @category mutations * @since 2.0.0 */ export const updateAndGet: { @@ -783,40 +869,45 @@ export const updateAndGet: { * * **Example** (Toggling boolean refs) * - * ```ts + * ```ts import.meta.vitest * import { MutableRef } from "effect" * * const flag = MutableRef.make(false) * * // Toggle the flag * MutableRef.toggle(flag) - * console.log(MutableRef.get(flag)) // true + * + * MutableRef.get(flag) // => true * * // Toggle again * MutableRef.toggle(flag) - * console.log(MutableRef.get(flag)) // false + * + * MutableRef.get(flag) // => false * * // Useful for state switches * const isVisible = MutableRef.make(true) * MutableRef.toggle(isVisible) // Hide - * console.log(MutableRef.get(isVisible)) // false + * + * MutableRef.get(isVisible) // => false * * // Toggle button implementation * const darkMode = MutableRef.make(false) * const toggleDarkMode = () => { * MutableRef.toggle(darkMode) - * console.log(`Dark mode: ${MutableRef.get(darkMode) ? "ON" : "OFF"}`) + * return MutableRef.get(darkMode) ? "ON" : "OFF" * } * - * toggleDarkMode() // "Dark mode: ON" - * toggleDarkMode() // "Dark mode: OFF" + * toggleDarkMode() // => "ON" + * toggleDarkMode() // => "OFF" * * // Returns the reference for chaining * const result = MutableRef.toggle(flag) - * console.log(result === flag) // true + * + * result === flag // => true + * MutableRef.get(flag) // => true * ``` * - * @category boolean + * @category mutations * @since 2.0.0 */ export const toggle = (self: MutableRef): MutableRef => update(self, (_) => !_) diff --git a/.context/effect/packages/effect/src/Newtype.ts b/.context/effect/packages/effect/src/Newtype.ts index ea64786d9..4df67075f 100644 --- a/.context/effect/packages/effect/src/Newtype.ts +++ b/.context/effect/packages/effect/src/Newtype.ts @@ -36,14 +36,19 @@ const TypeId = "~effect/Newtype" * * **Example** (Defining a newtype) * - * ```ts + * ```ts import.meta.vitest * import { Newtype } from "effect" * * interface UserId extends Newtype.Newtype<"UserId", number> {} * interface OrderId extends Newtype.Newtype<"OrderId", number> {} * + * const userId = Newtype.makeIso().set(1) * // UserId and OrderId are not assignable to each other * // even though both wrap `number`. + * // @ts-expect-error + * const orderId: OrderId = userId + * + * Newtype.value(userId) // => 1 * ``` * * @see {@link makeIso} — create an iso to wrap and unwrap @@ -125,7 +130,7 @@ export declare namespace Newtype { * * **Example** (Unwrapping a newtype) * - * ```ts + * ```ts import.meta.vitest * import { Newtype } from "effect" * * interface Label extends Newtype.Newtype<"Label", string> {} @@ -133,7 +138,8 @@ export declare namespace Newtype { * const iso = Newtype.makeIso(data: NonEmptyIterable.NonEmptyIterable): A { - * // Safe - guaranteed to have at least one element - * const [first] = NonEmptyIterable.unprepend(data) - * return first - * } - * - * // Works with any non-empty iterable - * const numbers = Array.make( - * 1, - * 2, - * 3 - * ) as unknown as NonEmptyIterable.NonEmptyIterable - * const firstNumber = getFirst(numbers) // 1 - * - * const chars = "hello" as unknown as NonEmptyIterable.NonEmptyIterable - * const firstChar = getFirst(chars) // "h" - * - * const entries = new Map([["a", 1], [ - * "b", - * 2 - * ]]) as unknown as NonEmptyIterable.NonEmptyIterable<[string, number]> - * const firstEntry = getFirst(entries) // ["a", 1] - * - * // Custom generator - * function* countdown(): Generator { - * yield 3 - * yield 2 - * yield 1 - * } - * const firstCount = getFirst( - * Chunk.fromIterable( - * countdown() - * ) as unknown as NonEmptyIterable.NonEmptyIterable - * ) // 3 - * ``` - * * @category models * @since 2.0.0 */ @@ -109,101 +67,13 @@ export interface NonEmptyIterable extends Iterable { * * **Example** (Extracting first and remaining elements) * - * ```ts - * import { Array, Chunk, NonEmptyIterable } from "effect" - * - * // Helper to make iterator iterable for Array.from - * const iteratorToIterable = (iterator: Iterator): Iterable => ({ - * [Symbol.iterator]() { - * return iterator - * } - * }) - * - * // With NonEmptyArray from Array.make (cast to NonEmptyIterable) - * const numbers = Array.make( - * 1, - * 2, - * 3, - * 4, - * 5 - * ) as unknown as NonEmptyIterable.NonEmptyIterable - * const [first, rest] = NonEmptyIterable.unprepend(numbers) - * console.log(first) // 1 - * console.log(globalThis.Array.from(iteratorToIterable(rest))) // [2, 3, 4, 5] - * - * // With strings (assert when known to be non-empty) - * const text = "hello" as unknown as NonEmptyIterable.NonEmptyIterable - * const [firstChar, restChars] = NonEmptyIterable.unprepend(text) - * console.log(firstChar) // "h" - * console.log(globalThis.Array.from(iteratorToIterable(restChars)).join("")) // "ello" - * - * // With Sets (assert when known to be non-empty) - * const uniqueNumbers = new Set([ - * 10, - * 20, - * 30 - * ]) as unknown as NonEmptyIterable.NonEmptyIterable - * const [firstUnique, restUnique] = NonEmptyIterable.unprepend(uniqueNumbers) - * console.log(firstUnique) // 10 (or any element, Set order is not guaranteed) - * console.log(globalThis.Array.from(iteratorToIterable(restUnique))) // [20, 30] (in some order) - * - * // With Maps (assert when known to be non-empty) - * const keyValuePairs = new Map([["a", 1], ["b", 2], [ - * "c", - * 3 - * ]]) as unknown as NonEmptyIterable.NonEmptyIterable<[string, number]> - * const [firstPair, restPairs] = NonEmptyIterable.unprepend(keyValuePairs) - * console.log(firstPair) // ["a", 1] - * console.log(globalThis.Array.from(iteratorToIterable(restPairs))) // [["b", 2], ["c", 3]] - * - * // With custom generators - * function* fibonacci(): Generator { - * let a = 1, b = 1 - * yield a - * for (let i = 0; i < 10; i++) { - * yield b - * const next = a + b - * a = b - * b = next - * } - * } - * - * const generator = Chunk.fromIterable( - * fibonacci() - * ) as unknown as NonEmptyIterable.NonEmptyIterable - * const [firstFib, restFib] = NonEmptyIterable.unprepend(generator) - * console.log(firstFib) // 1 - * console.log(globalThis.Array.from(iteratorToIterable(restFib))) // [1, 2, 3, 5, 8, 13, 21, 34, 55, 89] - * - * // Practical usage: implementing reduce for non-empty iterables - * function reduceNonEmpty( - * data: NonEmptyIterable.NonEmptyIterable, - * f: (acc: B, current: A) => B, - * initial: B - * ): B { - * const [first, rest] = NonEmptyIterable.unprepend(data) - * let result = f(initial, first) - * - * // Convert iterator to iterable for iteration - * const iterable = { - * [Symbol.iterator]() { - * return rest - * } - * } - * for (const item of iterable) { - * result = f(result, item) - * } - * - * return result - * } - * - * const data = Array.make( - * 1, - * 2, - * 3, - * 4 - * ) as unknown as NonEmptyIterable.NonEmptyIterable - * const sum = reduceNonEmpty(data, (acc, x) => acc + x, 0) // 10 + * ```ts import.meta.vitest + * import { Chunk, NonEmptyIterable } from "effect" + * + * const [first, rest] = NonEmptyIterable.unprepend(Chunk.make(1, 2, 3)) + * + * first // => 1 + * globalThis.Array.from({ [Symbol.iterator]: () => rest }) // => [2, 3] * ``` * * @category getters diff --git a/.context/effect/packages/effect/src/Number.ts b/.context/effect/packages/effect/src/Number.ts index 4fefbae81..89194befd 100644 --- a/.context/effect/packages/effect/src/Number.ts +++ b/.context/effect/packages/effect/src/Number.ts @@ -32,14 +32,11 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Coercing values to numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number as N } from "effect" * - * const num = N.Number("42") - * console.log(num) // 42 - * - * const float = N.Number("3.14") - * console.log(float) // 3.14 + * N.Number("42") // => 42 + * N.Number("3.14") // => 3.14 * ``` * * @category constructors @@ -56,12 +53,11 @@ export const Number = globalThis.Number * * **Example** (Checking for numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.isNumber(2), true) - * assert.deepStrictEqual(Number.isNumber("2"), false) + * Number.isNumber(2) // => true + * Number.isNumber("2") // => false * ``` * * @category guards @@ -78,11 +74,10 @@ export const isNumber: (input: unknown) => input is number = predicate.isNumber * * **Example** (Adding numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.sum(2, 3), 5) + * Number.sum(2, 3) // => 5 * ``` * * @see {@link sumAll} for summing an iterable of numbers @@ -104,11 +99,10 @@ export const sum: { * * **Example** (Multiplying numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.multiply(2, 3), 6) + * Number.multiply(2, 3) // => 6 * ``` * * @see {@link multiplyAll} for multiplying an iterable of numbers @@ -130,11 +124,10 @@ export const multiply: { * * **Example** (Subtracting numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.subtract(2, 3), -1) + * Number.subtract(2, 3) // => -1 * ``` * * @category math @@ -154,11 +147,11 @@ export const subtract: { * * **Example** (Dividing numbers safely) * - * ```ts - * import { Number } from "effect" + * ```ts import.meta.vitest + * import { Number, Option } from "effect" * - * Number.divide(6, 3) // Option.some(2) - * Number.divide(6, 0) // Option.none() + * Number.divide(6, 3) // => Option.some(2) + * Number.divide(6, 0) // => Option.none() * ``` * * @see {@link divideUnsafe} for division that throws when the divisor is zero @@ -189,12 +182,16 @@ export const divide: { * * **Example** (Dividing numbers unsafely) * - * ```ts - * import { Number } from "effect" + * ```ts import.meta.vitest + * import { Number, Result } from "effect" * - * console.log(Number.divideUnsafe(6, 3)) // 2 + * Number.divideUnsafe(6, 3) // => 2 * - * // Passing 0 as the divisor throws a RangeError("Division by zero"). + * const failure = Result.try({ + * try: () => Number.divideUnsafe(6, 0), + * catch: (error) => (error as Error).message + * }) + * Result.merge(failure) // => "Division by zero" * ``` * * @see {@link divide} for division that returns `Option.none` when the divisor is zero @@ -220,11 +217,10 @@ export const divideUnsafe: { * * **Example** (Incrementing a number) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.increment(2), 3) + * Number.increment(2) // => 3 * ``` * * @category math @@ -241,11 +237,10 @@ export const increment = (n: number): number => n + 1 * * **Example** (Decrementing a number) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.decrement(3), 2) + * Number.decrement(3) // => 2 * ``` * * @category math @@ -263,12 +258,12 @@ export const decrement = (n: number): number => n - 1 * * **Example** (Comparing numbers) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" * - * console.log(Number.Order(1, 2)) // -1 - * console.log(Number.Order(2, 1)) // 1 - * console.log(Number.Order(1, 1)) // 0 + * Number.Order(1, 2) // => -1 + * Number.Order(2, 1) // => 1 + * Number.Order(1, 1) // => 0 * ``` * * @category instances @@ -286,12 +281,12 @@ export const Order: order.Order = order.Number * * **Example** (Comparing numbers for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" * - * console.log(Number.Equivalence(1, 1)) // true - * console.log(Number.Equivalence(1, 2)) // false - * console.log(Number.Equivalence(NaN, NaN)) // true + * Number.Equivalence(1, 1) // => true + * Number.Equivalence(1, 2) // => false + * Number.Equivalence(NaN, NaN) // => true * ``` * * @category instances @@ -308,13 +303,12 @@ export const Equivalence: Equ.Equivalence = Equ.Number * * **Example** (Checking less-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.isLessThan(2, 3), true) - * assert.deepStrictEqual(Number.isLessThan(3, 3), false) - * assert.deepStrictEqual(Number.isLessThan(4, 3), false) + * Number.isLessThan(2, 3) // => true + * Number.isLessThan(3, 3) // => false + * Number.isLessThan(4, 3) // => false * ``` * * @category predicates @@ -334,13 +328,12 @@ export const isLessThan: { * * **Example** (Checking less-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.isLessThanOrEqualTo(2, 3), true) - * assert.deepStrictEqual(Number.isLessThanOrEqualTo(3, 3), true) - * assert.deepStrictEqual(Number.isLessThanOrEqualTo(4, 3), false) + * Number.isLessThanOrEqualTo(2, 3) // => true + * Number.isLessThanOrEqualTo(3, 3) // => true + * Number.isLessThanOrEqualTo(4, 3) // => false * ``` * * @category predicates @@ -360,13 +353,12 @@ export const isLessThanOrEqualTo: { * * **Example** (Checking greater-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.isGreaterThan(2, 3), false) - * assert.deepStrictEqual(Number.isGreaterThan(3, 3), false) - * assert.deepStrictEqual(Number.isGreaterThan(4, 3), true) + * Number.isGreaterThan(2, 3) // => false + * Number.isGreaterThan(3, 3) // => false + * Number.isGreaterThan(4, 3) // => true * ``` * * @category predicates @@ -386,13 +378,12 @@ export const isGreaterThan: { * * **Example** (Checking greater-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(2, 3), false) - * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(3, 3), true) - * assert.deepStrictEqual(Number.isGreaterThanOrEqualTo(4, 3), true) + * Number.isGreaterThanOrEqualTo(2, 3) // => false + * Number.isGreaterThanOrEqualTo(3, 3) // => true + * Number.isGreaterThanOrEqualTo(4, 3) // => true * ``` * * @category predicates @@ -412,15 +403,14 @@ export const isGreaterThanOrEqualTo: { * * **Example** (Checking inclusive ranges) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * * const between = Number.between({ minimum: 0, maximum: 5 }) * - * assert.deepStrictEqual(between(3), true) - * assert.deepStrictEqual(between(-1), false) - * assert.deepStrictEqual(between(6), false) + * between(3) // => true + * between(-1) // => false + * between(6) // => false * ``` * * @see {@link clamp} for forcing a number into an inclusive range @@ -454,15 +444,14 @@ export const between: { * * **Example** (Clamping to a range) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * * const clamp = Number.clamp({ minimum: 1, maximum: 5 }) * - * assert.equal(clamp(3), 3) - * assert.equal(clamp(0), 1) - * assert.equal(clamp(6), 5) + * clamp(3) // => 3 + * clamp(0) // => 1 + * clamp(6) // => 5 * ``` * * @see {@link between} for checking whether a number is already inside a range @@ -490,11 +479,10 @@ export const clamp: { * * **Example** (Finding the minimum) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.min(2, 3), 2) + * Number.min(2, 3) // => 2 * ``` * * @see {@link max} for selecting the larger value @@ -516,11 +504,10 @@ export const min: { * * **Example** (Finding the maximum) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.max(2, 3), 3) + * Number.max(2, 3) // => 3 * ``` * * @see {@link min} for selecting the smaller value @@ -542,13 +529,12 @@ export const max: { * * **Example** (Determining the sign) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.sign(-5), -1) - * assert.deepStrictEqual(Number.sign(0), 0) - * assert.deepStrictEqual(Number.sign(5), 1) + * Number.sign(-5) // => -1 + * Number.sign(0) // => 0 + * Number.sign(5) // => 1 * ``` * * @category math @@ -565,11 +551,10 @@ export const sign = (n: number): Ordering => Order(n, 0) * * **Example** (Summing an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.sumAll([2, 3, 4]), 9) + * Number.sumAll([2, 3, 4]) // => 9 * ``` * * @see {@link sum} for adding two numbers @@ -595,11 +580,10 @@ export const sumAll = (collection: Iterable): number => { * * **Example** (Multiplying an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.multiplyAll([2, 3, 4]), 24) + * Number.multiplyAll([2, 3, 4]) // => 24 * ``` * * @see {@link multiply} for multiplying two numbers @@ -629,13 +613,12 @@ export const multiplyAll = (collection: Iterable): number => { * * **Example** (Calculating remainders) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.remainder(2, 2), 0) - * assert.deepStrictEqual(Number.remainder(3, 2), 1) - * assert.deepStrictEqual(Number.remainder(-4, 2), -0) + * Number.remainder(2, 2) // => 0 + * Number.remainder(3, 2) // => 1 + * Number.remainder(-4, 2) // => -0 * ``` * * @see {@link divide} for quotient calculation with division-by-zero represented as `Option.none` @@ -694,12 +677,11 @@ function toScientificInteger(n: number): readonly [coefficient: bigint, exponent * * **Example** (Finding the next power of two) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.nextPow2(5), 8) - * assert.deepStrictEqual(Number.nextPow2(17), 32) + * Number.nextPow2(5) // => 8 + * Number.nextPow2(17) // => 32 * ``` * * @category math @@ -720,15 +702,15 @@ export const nextPow2 = (n: number): number => { * * **Example** (Parsing numbers from strings) * - * ```ts - * import { Number } from "effect" + * ```ts import.meta.vitest + * import { Number, Option } from "effect" * - * Number.parse("42") // Option.some(42) - * Number.parse("3.14") // Option.some(3.14) - * Number.parse("NaN") // Option.some(NaN) - * Number.parse("Infinity") // Option.some(Infinity) - * Number.parse("-Infinity") // Option.some(-Infinity) - * Number.parse("not a number") // Option.none() + * Number.parse("42") // => Option.some(42) + * Number.parse("3.14") // => Option.some(3.14) + * Number.parse("NaN") // => Option.some(NaN) + * Number.parse("Infinity") // => Option.some(Infinity) + * Number.parse("-Infinity") // => Option.some(-Infinity) + * Number.parse("not a number") // => Option.none() * ``` * * @see {@link Number} for native constructor coercion @@ -762,12 +744,11 @@ export const parse = (s: string): Option.Option => { * * **Example** (Rounding with precision) * - * ```ts + * ```ts import.meta.vitest * import { Number } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Number.round(1.1234, 2), 1.12) - * assert.deepStrictEqual(Number.round(1.567, 2), 1.57) + * Number.round(1.1234, 2) // => 1.12 + * Number.round(1.567, 2) // => 1.57 * ``` * * @category math diff --git a/.context/effect/packages/effect/src/Optic.ts b/.context/effect/packages/effect/src/Optic.ts index e9b3de8d6..f7fac610a 100644 --- a/.context/effect/packages/effect/src/Optic.ts +++ b/.context/effect/packages/effect/src/Optic.ts @@ -12,14 +12,14 @@ * @since 4.0.0 */ -import { format } from "./Formatter.ts" -import { identity, memoize } from "./Function.ts" +import { identity } from "./Function.ts" +import * as InternalRecord from "./internal/record.ts" import * as Option from "./Option.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" -import type * as SchemaIssue from "./SchemaIssue.ts" +import * as SchemaIssue from "./SchemaIssue.ts" import * as Struct from "./Struct.ts" import type { IsUnion } from "./Types.ts" @@ -41,7 +41,7 @@ import type { IsUnion } from "./Types.ts" * * **Example** (Converting between Celsius and Fahrenheit) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * const fahrenheit = Optic.makeIso( @@ -49,18 +49,16 @@ import type { IsUnion } from "./Types.ts" * (f) => (f - 32) * 5 / 9 * ) * - * console.log(fahrenheit.get(100)) - * // Output: 212 + * fahrenheit.get(100) // => 212 * - * console.log(fahrenheit.set(32)) - * // Output: 0 + * fahrenheit.set(32) // => 0 * ``` * * @see {@link makeIso} — constructor * @see {@link Lens} — when you only need a one-directional focus into a whole * @see {@link Prism} — when the focus may not be present * - * @category Iso + * @category models * @since 4.0.0 */ export interface Iso extends Lens, Prism {} @@ -79,7 +77,7 @@ export interface Iso extends Lens, Prism {} * * **Example** (Wrapping and unwrapping a branded type) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type Meters = { readonly value: number } @@ -88,11 +86,9 @@ export interface Iso extends Lens, Prism {} * (n) => ({ value: n }) * ) * - * console.log(meters.get({ value: 100 })) - * // Output: 100 + * meters.get({ value: 100 }) // => 100 * - * console.log(meters.set(42)) - * // Output: { value: 42 } + * meters.set(42) // => { value: 42 } * ``` * * @see {@link Iso} — the type this function returns @@ -102,7 +98,7 @@ export interface Iso extends Lens, Prism {} * @since 4.0.0 */ export function makeIso(get: (s: S) => A, set: (a: A) => S): Iso { - return make(new IsoNode(get, set)) + return make(primitiveNode("Iso", get, set)) } /** @@ -123,22 +119,21 @@ export function makeIso(get: (s: S) => A, set: (a: A) => S): Iso { * * **Example** (Focusing on a struct field) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type Person = { readonly name: string; readonly age: number } * * const _name = Optic.id().key("name") * - * console.log(_name.get({ name: "Alice", age: 30 })) - * // Output: "Alice" + * _name.get({ name: "Alice", age: 30 }) // => "Alice" * ``` * * @see {@link makeLens} — constructor * @see {@link Iso} — when conversion is lossless in both directions * @see {@link Optional} — when reading can also fail * - * @category Lens + * @category models * @since 4.0.0 */ export interface Lens extends Optional { @@ -160,7 +155,7 @@ export interface Lens extends Optional { * * **Example** (Focusing on the first element of a pair) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * const _first = Optic.makeLens( @@ -168,11 +163,9 @@ export interface Lens extends Optional { * (s, pair) => [s, pair[1]] * ) * - * console.log(_first.get(["hello", 42])) - * // Output: "hello" + * _first.get(["hello", 42]) // => "hello" * - * console.log(_first.replace("world", ["hello", 42])) - * // Output: ["world", 42] + * _first.replace("world", ["hello", 42]) // => ["world", 42] * ``` * * @see {@link Lens} — the type this function returns @@ -182,7 +175,7 @@ export interface Lens extends Optional { * @since 4.0.0 */ export function makeLens(get: (s: S) => A, replace: (a: A, s: S) => S): Lens { - return make(new LensNode(get, replace)) + return make(primitiveNode("Lens", get, replace)) } /** @@ -198,7 +191,7 @@ export function makeLens(get: (s: S) => A, replace: (a: A, s: S) => S): Le * **Details** * * - `getResult(s)` returns `Result.Success` when the focus matches, or - * `Result.Failure` with an error message. + * `Result.Failure` with a structured issue. * - `set(a)` always succeeds and returns a new `S`. * - Extends {@link Optional}. * - Composing two Prisms produces a Prism; composing a Prism with a @@ -206,7 +199,7 @@ export function makeLens(get: (s: S) => A, replace: (a: A, s: S) => S): Le * * **Example** (Narrowing a tagged union) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * type Shape = @@ -215,18 +208,16 @@ export function makeLens(get: (s: S) => A, replace: (a: A, s: S) => S): Le * * const _circle = Optic.id().tag("Circle") * - * console.log(Result.isSuccess(_circle.getResult({ _tag: "Circle", radius: 5 }))) - * // Output: true + * _circle.getResult({ _tag: "Circle", radius: 5 }) // => Result.succeed({ _tag: "Circle", radius: 5 }) * - * console.log(Result.isFailure(_circle.getResult({ _tag: "Rect", width: 10 }))) - * // Output: true + * Result.isFailure(_circle.getResult({ _tag: "Rect", width: 10 })) // => true * ``` * * @see {@link makePrism} — constructor * @see {@link fromChecks} — build a Prism from schema checks * @see {@link Lens} — when reading always succeeds * - * @category Prism + * @category models * @since 4.0.0 */ export interface Prism extends Optional { @@ -243,26 +234,27 @@ export interface Prism extends Optional { * * **Details** * - * - `getResult` should return `Result.fail(message)` on mismatch. + * - `getResult` should return `Result.fail(issue)` on mismatch. + * - Issues are not formatted automatically; callers choose how to render them. * * **Example** (Parsing a string to a number) * - * ```ts - * import { Optic, Result } from "effect" + * ```ts import.meta.vitest + * import { Optic, Result, SchemaIssue } from "effect" * * const numeric = Optic.makePrism( * (s) => { * const n = Number(s) - * return Number.isNaN(n) ? Result.fail("not a number") : Result.succeed(n) + * return Number.isNaN(n) + * ? Result.fail(new SchemaIssue.InvalidValue({ message: "not a number" })) + * : Result.succeed(n) * }, * String * ) * - * console.log(Result.isSuccess(numeric.getResult("42"))) - * // Output: true + * numeric.getResult("42") // => Result.succeed(42) * - * console.log(numeric.set(42)) - * // Output: "42" + * numeric.set(42) // => "42" * ``` * * @see {@link Prism} — the type this function returns @@ -271,8 +263,11 @@ export interface Prism extends Optional { * @category constructors * @since 4.0.0 */ -export function makePrism(getResult: (s: S) => Result.Result, set: (a: A) => S): Prism { - return make(new PrismNode(getResult, set)) +export function makePrism( + getResult: (s: S) => Result.Result, + set: (a: A) => S +): Prism { + return make(primitiveNode("Prism", getResult, set)) } /** @@ -286,13 +281,13 @@ export function makePrism(getResult: (s: S) => Result.Result, s * * **Details** * - * - `getResult` runs all checks; fails with a combined error message when + * - `getResult` runs all checks and preserves their structured issues when * any check fails. * - `set` is identity — the value passes through unchanged. * * **Example** (Creating a positive integer prism) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result, Schema } from "effect" * * const posInt = Optic.fromChecks( @@ -300,11 +295,9 @@ export function makePrism(getResult: (s: S) => Result.Result, s * Schema.isInt() * ) * - * console.log(Result.isSuccess(posInt.getResult(3))) - * // Output: true + * posInt.getResult(3) // => Result.succeed(3) * - * console.log(Result.isFailure(posInt.getResult(-1))) - * // Output: true + * Result.isFailure(posInt.getResult(-1)) // => true * ``` * * @see {@link makePrism} — constructor with custom getter/setter @@ -314,142 +307,95 @@ export function makePrism(getResult: (s: S) => Result.Result, s * @since 4.0.0 */ export function fromChecks(...checks: readonly [SchemaAST.Check, ...Array>]): Prism { - return make(new CheckNode(checks)) + return make([new CheckNode(checks)]) } -type Node = - | IdentityNode - | IsoNode - | LensNode - | PrismNode - | OptionalNode - | PathNode - | CheckNode - | CompositionNode - -class IdentityNode { - readonly _tag = "IdentityNode" -} - -const identityNode = new IdentityNode() - -class CompositionNode { - readonly _tag = "CompositionNode" - readonly nodes: readonly [Node, ...Array] +type Kind = "Iso" | "Lens" | "Prism" | "Optional" - constructor(nodes: readonly [Node, ...Array]) { - this.nodes = nodes - } +type Operation = { + readonly kind: Kind + readonly get: (s: any) => any + readonly set: (a: any, s?: any) => any } -class IsoNode { - readonly _tag = "IsoNode" - readonly get: (s: S) => A - readonly set: (a: A) => S - - constructor(get: (s: S) => A, set: (a: A) => S) { - this.get = get - this.set = set - } +type PrimitiveStep = Operation & { + readonly _tag: "PrimitiveNode" } -class LensNode { - readonly _tag = "LensNode" - readonly get: (s: S) => A - readonly set: (a: A, s: S) => S - - constructor(get: (s: S) => A, set: (a: A, s: S) => S) { - this.get = get - this.set = set - } -} +type Step = PrimitiveStep | PathNode | CheckNode -class PrismNode { - readonly _tag = "PrismNode" - readonly get: (s: S) => Result.Result - readonly set: (a: A) => S +type Node = ReadonlyArray - constructor(get: (s: S) => Result.Result, set: (a: A) => S) { - this.get = get - this.set = set - } +function primitiveNode(kind: Kind, get: (s: any) => any, set: (a: any, s?: any) => any): Node { + return [{ _tag: "PrimitiveNode", kind, get, set }] } -class OptionalNode { - readonly _tag = "OptionalNode" - readonly get: (s: S) => Result.Result - readonly set: (a: A, s: S) => Result.Result - - constructor(get: (s: S) => Result.Result, set: (a: A, s: S) => Result.Result) { - this.get = get - this.set = set - } +const identityOperation: Operation = { + kind: "Iso", + get: identity, + set: identity } class PathNode { readonly _tag = "PathNode" + readonly kind = "Lens" readonly path: ReadonlyArray + readonly get: (s: any) => any + readonly set: (a: any, s?: any) => any constructor(path: ReadonlyArray) { this.path = path + this.get = (s) => { + let out = s + for (let i = 0; i < path.length; i++) { + out = out[path[i]] + } + return out + } + this.set = (a, s) => { + const out = cloneShallow(s) + let current = out + let i = 0 + for (; i < path.length - 1; i++) { + const key = path[i] + InternalRecord.assignProperty(current, key, cloneShallow(current[key])) + current = current[key] + } + InternalRecord.assignProperty(current, path[i], a) + return out + } } } class CheckNode { readonly _tag = "CheckNode" + readonly kind = "Prism" readonly checks: readonly [SchemaAST.Check, ...Array>] + readonly get: (s: T) => Result.Result + readonly set = identity constructor(checks: readonly [SchemaAST.Check, ...Array>]) { this.checks = checks + this.get = (s) => SchemaAST.runChecks(checks, s) } } -// Nodes that can appear in a normalized chain (no Identity/Composition) -type NormalizedNode = Exclude - -// Fuse with tail when possible, else push. -function pushNormalized(acc: Array, node: NormalizedNode): void { - const last = acc[acc.length - 1] - if (last) { +function compose(a: Node, b: Node): Node { + if (a.length === 0) return b + if (b.length === 0) return a + const nodes = a.slice() + for (let i = 0; i < b.length; i++) { + const node = b[i] + const last = nodes[nodes.length - 1] if (last._tag === "PathNode" && node._tag === "PathNode") { - // fuse Path - acc[acc.length - 1] = new PathNode([...last.path, ...node.path]) - return - } - if (last._tag === "CheckNode" && node._tag === "CheckNode") { - // fuse Checks - acc[acc.length - 1] = new CheckNode([...last.checks, ...node.checks]) - return + nodes[nodes.length - 1] = new PathNode([...last.path, ...node.path]) + } else if (last._tag === "CheckNode" && node._tag === "CheckNode") { + nodes[nodes.length - 1] = new CheckNode([...last.checks, ...node.checks]) + } else { + nodes.push(node) } } - acc.push(node) -} - -// Collect nodes from a node into `acc`, flattening & normalizing on the fly. -function collect(node: Node, acc: Array): void { - if (node._tag === "IdentityNode") return - if (node._tag === "CompositionNode") { - // flatten without extra arrays - for (let i = 0; i < node.nodes.length; i++) collect(node.nodes[i], acc) - return - } - // primitive node - pushNormalized(acc, node) -} - -function compose(a: Node, b: Node): Node { - const nodes: Array = [] - collect(a, nodes) - collect(b, nodes) - - switch (nodes.length) { - case 0: - return identityNode - case 1: - return nodes[0] - default: - return new CompositionNode(nodes as [Node, ...Array]) - } + return nodes } type ForbidUnion = IsUnion extends true ? [Message] : [] @@ -466,47 +412,43 @@ type ForbidUnion = IsUnion extends true ? [Message * * **Details** * - * - `getResult(s)` returns `Result.Success` or `Result.Failure`. + * - `getResult(s)` returns `Result.Success` or `Result.Failure`. * - `replaceResult(a, s)` returns `Result.Success` or - * `Result.Failure`. + * `Result.Failure`. * - `replace(a, s)` returns the original `s` on failure (never throws). * - `modify(f)` returns the original `s` on failure (never throws). * - All operations are pure; inputs are never mutated. * * **Example** (Focusing on an optional record key) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * type Env = { [key: string]: string } * const _home = Optic.id().at("HOME") * - * console.log(Result.isSuccess(_home.getResult({ HOME: "/root" }))) - * // Output: true + * _home.getResult({ HOME: "/root" }) // => Result.succeed("/root") * - * console.log(Result.isFailure(_home.getResult({ PATH: "/bin" }))) - * // Output: true + * Result.isFailure(_home.getResult({ PATH: "/bin" })) // => true * * // replace returns original on failure - * console.log(_home.replace("/new", { PATH: "/bin" })) - * // Output: { PATH: "/bin" } + * _home.replace("/new", { PATH: "/bin" }) // => { PATH: "/bin" } * ``` * * @see {@link makeOptional} — constructor * @see {@link Lens} — when reading always succeeds * @see {@link Prism} — when writing always succeeds * - * @category Optional + * @category models * @since 4.0.0 */ export interface Optional { - readonly node: Node /** * Attempts to read the focus `A` from the whole `S`. Returns * `Result.Success` when the focus exists, or - * `Result.Failure` with a descriptive error otherwise. + * `Result.Failure` with a structured issue otherwise. */ - readonly getResult: (s: S) => Result.Result + readonly getResult: (s: S) => Result.Result /** * Replaces the focus in `S` with a new `A`. Returns the original `s` * unchanged when the optic cannot focus (never throws). @@ -516,20 +458,21 @@ export interface Optional { * Like {@link replace}, but returns an explicit `Result` so callers can * detect and handle failure. */ - readonly replaceResult: (a: A, s: S) => Result.Result + readonly replaceResult: (a: A, s: S) => Result.Result /** * Composes this optic with another. The result type is the weakest of * the two: Iso + Iso = Iso, Lens + Prism = Optional, etc. * * **Example** (Composing a lens with a prism) * - * ```ts - * import { Optic, Option } from "effect" + * ```ts import.meta.vitest + * import { Optic, Option, Result } from "effect" * * type State = { value: Option.Option } * * const _inner = Optic.id().key("value").compose(Optic.some()) * // _inner is Optional + * _inner.getResult({ value: Option.some(1) }) // => Result.succeed(1) * ``` * * @see {@link id} — start a composition chain @@ -545,15 +488,14 @@ export interface Optional { * * **Example** (Incrementing a nested field) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly a: { readonly b: number } } * const _b = Optic.id().key("a").key("b") * * const inc = _b.modify((n) => n + 1) - * console.log(inc({ a: { b: 1 } })) - * // Output: { a: { b: 2 } } + * inc({ a: { b: 1 } }) // => { a: { b: 2 } } * ``` */ modify(f: (a: A) => A): (s: S) => S @@ -569,14 +511,13 @@ export interface Optional { * * **Example** (Drilling into nested structs) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly a: { readonly b: number } } * const _b = Optic.id().key("a").key("b") * - * console.log(_b.get({ a: { b: 42 } })) - * // Output: 42 + * _b.get({ a: { b: 42 } }) // => 42 * ``` */ key( @@ -601,17 +542,15 @@ export interface Optional { * * **Example** (Deleting an optional key) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly a?: number } * const _a = Optic.id().optionalKey("a") * - * console.log(_a.replace(undefined, { a: 1 })) - * // Output: {} + * _a.replace(undefined, { a: 1 }) // => {} * - * console.log(_a.replace(2, {})) - * // Output: { a: 2 } + * _a.replace(2, {}) // => { a: 2 } * ``` */ optionalKey( @@ -636,16 +575,14 @@ export interface Optional { * * **Example** (Focusing only on positive numbers) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result, Schema } from "effect" * * const _pos = Optic.id().check(Schema.isGreaterThan(0)) * - * console.log(Result.isSuccess(_pos.getResult(5))) - * // Output: true + * _pos.getResult(5) // => Result.succeed(5) * - * console.log(Result.isFailure(_pos.getResult(-1))) - * // Output: true + * Result.isFailure(_pos.getResult(-1)) // => true * ``` * * @see {@link fromChecks} — standalone prism from checks @@ -667,7 +604,7 @@ export interface Optional { * * **Example** (Narrowing a union) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * type B = { readonly _tag: "b"; readonly b: number } @@ -678,8 +615,7 @@ export interface Optional { * { expected: `"b" tag` } * ) * - * console.log(Result.isSuccess(_b.getResult({ _tag: "b", b: 1 }))) - * // Output: true + * _b.getResult({ _tag: "b", b: 1 }) // => Result.succeed({ _tag: "b", b: 1 }) * ``` * * @see `.tag()` — shorthand for narrowing by `_tag` @@ -704,10 +640,12 @@ export interface Optional { * - On a {@link Prism}, returns a Prism. * - On an {@link Optional}, returns an Optional. * - Shorthand for `.refine(s => s._tag === tag)`. + * - A non-matching value fails with {@link SchemaIssue.InvalidValue} whose + * `expected` annotation is `" tag"`. * * **Example** (Focusing a tagged variant) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * type Shape = @@ -716,11 +654,9 @@ export interface Optional { * * const _radius = Optic.id().tag("Circle").key("radius") * - * console.log(Result.isSuccess(_radius.getResult({ _tag: "Circle", radius: 5 }))) - * // Output: true + * _radius.getResult({ _tag: "Circle", radius: 5 }) // => Result.succeed(5) * - * console.log(Result.isFailure(_radius.getResult({ _tag: "Rect", width: 10 }))) - * // Output: true + * Result.isFailure(_radius.getResult({ _tag: "Rect", width: 10 })) // => true * ``` * * @see `.refine()` — for arbitrary type guards @@ -745,20 +681,21 @@ export interface Optional { * * - Always returns an {@link Optional}. * - Does **not** work on union types (compile error). + * - A missing key fails with a {@link SchemaIssue.Pointer} at that key whose + * inner issue is {@link SchemaIssue.MissingKey}, for both `getResult` and + * `replaceResult`. * * **Example** (Accessing records safely) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * type Env = { [key: string]: number } * const _x = Optic.id().at("x") * - * console.log(Result.isSuccess(_x.getResult({ x: 1 }))) - * // Output: true + * _x.getResult({ x: 1 }) // => Result.succeed(1) * - * console.log(Result.isFailure(_x.getResult({ y: 2 }))) - * // Output: true + * Result.isFailure(_x.getResult({ y: 2 })) // => true * ``` * * @see `.key()` — when the key is always present @@ -780,15 +717,14 @@ export interface Optional { * * **Example** (Picking keys) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly a: string; readonly b: number; readonly c: boolean } * * const _ac = Optic.id().pick(["a", "c"]) * - * console.log(_ac.get({ a: "hi", b: 1, c: true })) - * // Output: { a: "hi", c: true } + * _ac.get({ a: "hi", b: 1, c: true }) // => { a: "hi", c: true } * ``` * * @see `.omit()` — the inverse operation @@ -815,15 +751,14 @@ export interface Optional { * * **Example** (Omitting keys) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly a: string; readonly b: number; readonly c: boolean } * * const _ac = Optic.id().omit(["b"]) * - * console.log(_ac.get({ a: "hi", b: 1, c: true })) - * // Output: { a: "hi", c: true } + * _ac.get({ a: "hi", b: 1, c: true }) // => { a: "hi", c: true } * ``` * * @see `.pick()` — the inverse operation @@ -847,22 +782,20 @@ export interface Optional { * * **Example** (Filtering undefined values) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * const _defined = Optic.id().notUndefined() * - * console.log(Result.isSuccess(_defined.getResult(42))) - * // Output: true + * _defined.getResult(42) // => Result.succeed(42) * - * console.log(Result.isFailure(_defined.getResult(undefined))) - * // Output: true + * Result.isFailure(_defined.getResult(undefined)) // => true * ``` * * @since 4.0.0 */ - notUndefined(): Prism> - notUndefined(): Optional> + notUndefined(this: Prism): Prism> + notUndefined(this: Optional): Optional> /** * Focuses **all elements** of an array-like focus and optionally narrows @@ -877,11 +810,13 @@ export interface Optional { * element. Non-focusable elements are skipped. * - **replaceResult** expects exactly as many values as were collected by * `getResult` and writes them back in order. Fails with a - * length-mismatch error if counts differ. + * {@link SchemaIssue.InvalidValue} if counts differ. If an inner replacement + * fails, its issue is wrapped in a {@link SchemaIssue.Pointer} at the element + * index. * * **Example** (Incrementing liked posts) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Schema } from "effect" * * type Post = { title: string; likes: number } @@ -894,12 +829,10 @@ export interface Optional { * * const addLike = _likes.modifyAll((n) => n + 1) * - * console.log( - * addLike({ - * user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }] } - * }) - * ) - * // Output: { user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 2 }] } } + * const result = addLike({ + * user: { posts: [{ title: "a", likes: 0 }, { title: "b", likes: 1 }] } + * }) + * result.user.posts // => [{ title: "a", likes: 0 }, { title: "b", likes: 2 }] * ``` * * @see {@link getAll} — extract all focused elements as an array @@ -920,7 +853,7 @@ export interface Optional { * * **Example** (Doubling all focused values) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Schema } from "effect" * * type S = { readonly items: ReadonlyArray } @@ -931,8 +864,7 @@ export interface Optional { * * const doubled = _positive.modifyAll((n) => n * 2) * - * console.log(doubled({ items: [1, -2, 3] })) - * // Output: { items: [2, -2, 6] } + * doubled({ items: [1, -2, 3] }) // => { items: [2, -2, 6] } * ``` * * @see `.forEach()` — create a sub-traversal @@ -951,29 +883,31 @@ export interface Optional { * * **Details** * - * - `getResult` should return `Result.fail(message)` on mismatch. - * - `set` should return `Result.fail(message)` when the update cannot be + * - `getResult` should return `Result.fail(issue)` on mismatch. + * - `set` should return `Result.fail(issue)` when the update cannot be * applied. + * - Issues are not formatted automatically; callers choose how to render them. * * **Example** (Accessing record keys safely) * - * ```ts - * import { Optic, Result } from "effect" + * ```ts import.meta.vitest + * import { Optic, Result, SchemaIssue } from "effect" * - * const atKey = (key: string) => - * Optic.makeOptional, number>( + * const atKey = (key: string) => { + * const issue = new SchemaIssue.Pointer([key], new SchemaIssue.MissingKey(undefined)) + * return Optic.makeOptional, number>( * (s) => * Object.hasOwn(s, key) * ? Result.succeed(s[key]) - * : Result.fail(`Key "${key}" not found`), + * : Result.fail(issue), * (a, s) => * Object.hasOwn(s, key) * ? Result.succeed({ ...s, [key]: a }) - * : Result.fail(`Key "${key}" not found`) + * : Result.fail(issue) * ) + * } * - * console.log(Result.isSuccess(atKey("x").getResult({ x: 1 }))) - * // Output: true + * atKey("x").getResult({ x: 1 }) // => Result.succeed(1) * ``` * * @see {@link Optional} — the type this function returns @@ -984,10 +918,10 @@ export interface Optional { * @since 4.0.0 */ export function makeOptional( - getResult: (s: S) => Result.Result, - set: (a: A, s: S) => Result.Result + getResult: (s: S) => Result.Result, + set: (a: A, s: S) => Result.Result ): Optional { - return make(new OptionalNode(getResult, set)) + return make(primitiveNode("Optional", getResult, set)) } /** @@ -1009,7 +943,7 @@ export function makeOptional( * * **Example** (Traversing array elements with a filter) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Schema } from "effect" * * type S = { readonly items: ReadonlyArray } @@ -1020,26 +954,26 @@ export function makeOptional( * * const getPositive = Optic.getAll(_positive) * - * console.log(getPositive({ items: [1, -2, 3] })) - * // Output: [1, 3] + * getPositive({ items: [1, -2, 3] }) // => [1, 3] * ``` * * @see {@link getAll} — extract focused elements * @see {@link Optional} — the base type * - * @category Traversal + * @category models * @since 4.0.0 */ export interface Traversal extends Optional> {} class OptionalImpl implements Optional { + /** @internal */ readonly node: Node - readonly getResult: (s: S) => Result.Result - readonly replaceResult: (a: A, s: S) => Result.Result + readonly getResult: (s: S) => Result.Result + readonly replaceResult: (a: A, s: S) => Result.Result constructor( node: Node, - getResult: (s: S) => Result.Result, - replaceResult: (a: A, s: S) => Result.Result + getResult: (s: S) => Result.Result, + replaceResult: (a: A, s: S) => Result.Result ) { this.node = node this.getResult = getResult @@ -1055,13 +989,14 @@ class OptionalImpl implements Optional { return make(compose(this.node, that.node)) } key(key: PropertyKey): any { - return make(compose(this.node, new PathNode([key]))) + return make(compose(this.node, [new PathNode([key])])) } optionalKey(key: PropertyKey): any { return make( compose( this.node, - new LensNode( + primitiveNode( + "Lens", (s) => s[key], (a, s) => { const copy = cloneShallow(s) @@ -1072,7 +1007,7 @@ class OptionalImpl implements Optional { delete copy[key] } } else { - copy[key] = a + InternalRecord.assignProperty(copy, key, a) } return copy } @@ -1081,36 +1016,38 @@ class OptionalImpl implements Optional { ) } check(...checks: readonly [SchemaAST.Check, ...Array>]): any { - return make(compose(this.node, new CheckNode(checks))) + return make(compose(this.node, [new CheckNode(checks)])) } refine(refinement: (a: A) => a is B, annotations?: Schema.Annotations.Filter): any { - return make(compose(this.node, new CheckNode([SchemaAST.makeFilterByGuard(refinement, annotations)]))) + return make(compose(this.node, [new CheckNode([SchemaAST.makeFilterByGuard(refinement, annotations)])])) } tag(tag: string): any { + const err = Result.fail(new SchemaIssue.InvalidValue({ expected: `${JSON.stringify(tag)} tag` })) return make( compose( this.node, - new PrismNode( - (s) => - s._tag === tag - ? Result.succeed(s) - : Result.fail(`Expected ${format(tag)} tag, got ${format(s._tag)}`), + primitiveNode( + "Prism", + (s) => s._tag === tag ? Result.succeed(s) : err, identity ) ) ) } at(key: PropertyKey, ..._rest: Array): any { - const err = Result.fail(`Key ${format(key)} not found`) + const err = Result.fail( + new SchemaIssue.Pointer([key], new SchemaIssue.MissingKey(undefined)) + ) return make( compose( this.node, - new OptionalNode( + primitiveNode( + "Optional", (s) => Object.hasOwn(s, key) ? Result.succeed(s[key]) : err, (a, s) => { if (Object.hasOwn(s, key)) { const copy = cloneShallow(s) - copy[key] = a + InternalRecord.assignProperty(copy, key, a) return Result.succeed(copy) } else { return err @@ -1126,7 +1063,7 @@ class OptionalImpl implements Optional { omit(keys: any) { return this.compose(makeLens(Struct.omit(keys), (o, a) => ({ ...a, ...o }))) } - notUndefined(): Prism> { + notUndefined(): any { return this.refine(Predicate.isNotUndefined, { expected: "a value other than `undefined`" }) } forEach(this: Traversal, f: (iso: Iso) => Optional): Traversal { @@ -1154,7 +1091,9 @@ class OptionalImpl implements Optional { // 2) arity check if (bs.length !== idxs.length) { return Result.fail( - `each: replacement length mismatch: ${bs.length} !== ${idxs.length}` + new SchemaIssue.InvalidValue({ + message: `each: replacement length mismatch: ${bs.length} !== ${idxs.length}` + }) ) } @@ -1164,7 +1103,7 @@ class OptionalImpl implements Optional { const i = idxs[k] const r = inner.replaceResult(bs[k], as[i]) if (Result.isFailure(r)) { - return Result.fail(`each: could not set element ${i}`) + return Result.fail(new SchemaIssue.Pointer([i], r.failure)) } out[i] = r.success } @@ -1211,7 +1150,7 @@ class LensImpl extends OptionalImpl implements Lens { class PrismImpl extends OptionalImpl implements Prism { readonly set: (a: A) => S - constructor(node: Node, getResult: (s: S) => Result.Result, set: (a: A) => S) { + constructor(node: Node, getResult: (s: S) => Result.Result, set: (a: A) => S) { super(node, getResult, (a, _) => Result.succeed(set(a))) this.set = set } @@ -1224,15 +1163,23 @@ class PrismImpl extends OptionalImpl implements Prism { } function make(node: Node): any { - const op = recur(node) - switch (op._tag) { - case "IsoNode": + let op: Operation = node[0] ?? identityOperation + if (node.length > 1) { + const kind = node.reduce((kind, step) => composeKind(kind, step.kind), "Iso") + op = { + kind, + get: compileGet(node, kind), + set: compileSet(node, kind) + } + } + switch (op.kind) { + case "Iso": return new IsoImpl(node, op.get, op.set) - case "LensNode": + case "Lens": return new LensImpl(node, op.get, op.set) - case "PrismNode": + case "Prism": return new PrismImpl(node, op.get, op.set) - case "OptionalNode": + case "Optional": return new OptionalImpl(node, op.get, op.set) } } @@ -1249,135 +1196,79 @@ function cloneShallow(pojo: T): T { return pojo } -type Op = { - readonly _tag: "IsoNode" | "LensNode" | "PrismNode" | "OptionalNode" - readonly get: (s: unknown) => any - readonly set: (a: unknown, s?: unknown) => any +function compileGet(nodes: Node, kind: Kind): (s: any) => any { + return (s) => { + for (let i = 0; i < nodes.length; i++) { + const op = nodes[i] + const result = op.get(s) + if (hasFailingGet(op.kind)) { + if (Result.isFailure(result)) { + return result + } + s = result.success + } else { + s = result + } + } + return hasFailingGet(kind) ? Result.succeed(s) : s + } } -const recur = memoize((node: Node): Op => { - switch (node._tag) { - case "IdentityNode": - return { _tag: "IsoNode", get: identity, set: identity } - case "IsoNode": - case "LensNode": - case "PrismNode": - case "OptionalNode": - return { _tag: node._tag, get: node.get, set: node.set } - case "PathNode": { - return { - _tag: "LensNode", - get: (s: any) => { - const path = node.path - let out: any = s - for (let i = 0, n = path.length; i < n; i++) { - out = out[path[i]] - } - return out - }, - set: (a: any, s: any) => { - const path = node.path - const out = cloneShallow(s) - - let current = out - let i = 0 - for (; i < path.length - 1; i++) { - const key = path[i] - current[key] = cloneShallow(current[key]) - current = current[key] - } - - const finalKey = path[i] - current[finalKey] = a - - return out - } +function compileSet(nodes: Node, kind: Kind): (a: any, s: any) => any { + if (hasSourceFreeSet(kind)) { + return (a) => { + for (let i = nodes.length - 1; i >= 0; i--) { + a = nodes[i].set(a) } + return a } - case "CheckNode": - return { - _tag: "PrismNode", - get: (s: any) => Result.mapError(SchemaAST.runChecks(node.checks, s), String), - set: identity + } + return (a, s) => { + const len = nodes.length + const sources = new Array(len) + for (let i = 0; i < len; i++) { + sources[i] = s + const op = nodes[i] + if (hasFailingGet(op.kind)) { + const result = op.get(s) + if (Result.isFailure(result)) { + return result + } + s = result.success + } else { + s = op.get(s) } - case "CompositionNode": { - const ops = node.nodes.map(recur) - const _tag = ops.reduce((tag, op) => getCompositionTag(tag, op._tag), "IsoNode") - return { - _tag, - get: (s: any) => { - for (let i = 0; i < ops.length; i++) { - const op = ops[i] - const result = op.get(s) - if (hasFailingGet(op._tag)) { - if (Result.isFailure(result)) { - return result - } - s = result.success - } else { - s = result - } - } - return hasFailingGet(_tag) ? Result.succeed(s) : s - }, - set: (a: any, s: any) => { - const source = s - const len = ops.length - const ss = new Array(len + 1) - ss[0] = s - for (let i = 0; i < len; i++) { - const op = ops[i] - if (hasFailingGet(op._tag)) { - const result = op.get(s) - if (Result.isFailure(result)) { - return _tag === "OptionalNode" ? result : source - } - s = result.success - } else { - s = op.get(s) - } - ss[i + 1] = s - } - for (let i = len - 1; i >= 0; i--) { - const op = ops[i] - if (hasSet(op._tag)) { - a = op.set(a) - } else if (op._tag === "LensNode") { - a = op.set(a, ss[i]) - } else { - const result = op.set(a, ss[i]) - if (Result.isFailure(result)) { - return result - } - a = result.success - } - } - return _tag === "OptionalNode" ? Result.succeed(a) : a + } + for (let i = len - 1; i >= 0; i--) { + const op = nodes[i] + if (hasSourceFreeSet(op.kind)) { + a = op.set(a) + } else if (op.kind === "Lens") { + a = op.set(a, sources[i]) + } else { + const result = op.set(a, sources[i]) + if (Result.isFailure(result)) { + return result } + a = result.success } } + return kind === "Optional" ? Result.succeed(a) : a } -}) +} -function hasFailingGet(tag: Op["_tag"]): boolean { - return tag === "PrismNode" || tag === "OptionalNode" +function hasFailingGet(kind: Kind): boolean { + return kind === "Prism" || kind === "Optional" } -function hasSet(tag: Op["_tag"]): boolean { - return tag === "IsoNode" || tag === "PrismNode" +function hasSourceFreeSet(kind: Kind): boolean { + return kind === "Iso" || kind === "Prism" } -function getCompositionTag(a: Op["_tag"], b: Op["_tag"]): Op["_tag"] { - switch (a) { - case "IsoNode": - return b - case "LensNode": - return hasFailingGet(b) ? "OptionalNode" : "LensNode" - case "PrismNode": - return hasSet(b) ? "PrismNode" : "OptionalNode" - case "OptionalNode": - return "OptionalNode" - } +function composeKind(a: Kind, b: Kind): Kind { + if (a === "Iso") return b + if (b === "Iso" || a === b) return a + return "Optional" } // --------------------------------------------- // Derived APIs @@ -1399,7 +1290,7 @@ function getCompositionTag(a: Op["_tag"], b: Op["_tag"]): Op["_tag"] { * * **Example** (Collecting positive numbers) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Schema } from "effect" * * type S = { readonly values: ReadonlyArray } @@ -1410,16 +1301,14 @@ function getCompositionTag(a: Op["_tag"], b: Op["_tag"]): Op["_tag"] { * * const getPositive = Optic.getAll(_pos) * - * console.log(getPositive({ values: [3, -1, 5] })) - * // Output: [3, 5] + * getPositive({ values: [3, -1, 5] }) // => [3, 5] * - * console.log(getPositive({ values: [-1, -2] })) - * // Output: [] + * getPositive({ values: [-1, -2] }) // => [] * ``` * * @see {@link Traversal} — the optic type this operates on * - * @category Traversal + * @category getters * @since 4.0.0 */ export function getAll(traversal: Traversal): (s: S) => Array { @@ -1434,7 +1323,7 @@ export function getAll(traversal: Traversal): (s: S) => Array { // Built-in Optics // --------------------------------------------- -const identityIso = make(identityNode) +const identityIso = make([]) /** * Iso that focuses on the whole value unchanged. @@ -1451,20 +1340,19 @@ const identityIso = make(identityNode) * * **Example** (Starting an optic chain) * - * ```ts + * ```ts import.meta.vitest * import { Optic } from "effect" * * type S = { readonly x: number } * * const _x = Optic.id().key("x") * - * console.log(_x.get({ x: 42 })) - * // Output: 42 + * _x.get({ x: 42 }) // => 42 * ``` * * @see {@link Iso} — the type this function returns * - * @category Iso + * @category constructors * @since 4.0.0 */ export function id(): Iso { @@ -1488,7 +1376,7 @@ export function id(): Iso { * * **Example** (Traversing record values) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Schema } from "effect" * * const _positiveValues = Optic.entries() @@ -1496,18 +1384,17 @@ export function id(): Iso { * * const inc = _positiveValues.modifyAll((n) => n + 1) * - * console.log(inc({ a: 0, b: 3, c: -1 })) - * // Output: { a: 0, b: 4, c: -1 } + * inc({ a: 0, b: 3, c: -1 }) // => { a: 0, b: 4, c: -1 } * ``` * * @see {@link Iso} — the type this function returns * @see {@link id} — identity iso * - * @category Iso + * @category constructors * @since 4.0.0 */ export function entries(): Iso, ReadonlyArray> { - return make(new IsoNode(Object.entries, Object.fromEntries)) + return make(primitiveNode("Iso", Object.entries, Object.fromEntries)) } /** @@ -1520,40 +1407,33 @@ export function entries(): Iso, ReadonlyArray>().compose(Optic.some()) * - * console.log(Result.isSuccess(_some.getResult(Option.some(42)))) - * // Output: true + * _some.getResult(Option.some(42)) // => Result.succeed(42) * - * console.log(Result.isFailure(_some.getResult(Option.none()))) - * // Output: true + * Result.isFailure(_some.getResult(Option.none())) // => true * - * console.log(_some.set(10)) - * // Output: { _tag: "Some", value: 10 } + * _some.set(10) // => Option.some(10) * ``` * * @see {@link none} — focuses on `None` instead * @see {@link Prism} — the type this function returns * - * @category Prism + * @category constructors * @since 4.0.0 */ export function some(): Prism, A> { const run = runRefinement(Option.isSome, { expected: "a Some value" }) return makePrism( - (s) => - Result.mapBoth(run(s), { - onFailure: String, - onSuccess: (s) => s.value - }), + (s) => Result.map(run(s), (s) => s.value), Option.some ) } @@ -1573,32 +1453,26 @@ export function some(): Prism, A> { * * **Example** (Matching None) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Option, Result } from "effect" * * const _none = Optic.id>().compose(Optic.none()) * - * console.log(Result.isSuccess(_none.getResult(Option.none()))) - * // Output: true + * _none.getResult(Option.none()) // => Result.succeed(undefined) * - * console.log(Result.isFailure(_none.getResult(Option.some(1)))) - * // Output: true + * Result.isFailure(_none.getResult(Option.some(1))) // => true * ``` * * @see {@link some} — focuses on `Some` instead * @see {@link Prism} — the type this function returns * - * @category Prism + * @category constructors * @since 4.0.0 */ export function none(): Prism, undefined> { const run = runRefinement(Option.isNone, { expected: "a None value" }) return makePrism( - (s) => - Result.mapBoth(run(s), { - onFailure: String, - onSuccess: () => undefined - }), + (s) => Result.map(run(s), () => undefined), () => Option.none() ) } @@ -1618,32 +1492,26 @@ export function none(): Prism, undefined> { * * **Example** (Accessing success) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * const _ok = Optic.id>().compose(Optic.success()) * - * console.log(Result.isSuccess(_ok.getResult(Result.succeed(42)))) - * // Output: true + * _ok.getResult(Result.succeed(42)) // => Result.succeed(42) * - * console.log(Result.isFailure(_ok.getResult(Result.fail("err")))) - * // Output: true + * Result.isFailure(_ok.getResult(Result.fail("err"))) // => true * ``` * * @see {@link failure} — focuses on the failure side * @see {@link Prism} — the type this function returns * - * @category Prism + * @category constructors * @since 4.0.0 */ export function success(): Prism, A> { const run = runRefinement(Result.isSuccess, { expected: "a Result.Success value" }) return makePrism( - (s) => - Result.mapBoth(run(s), { - onFailure: String, - onSuccess: (s) => s.success - }), + (s) => Result.map(run(s), (s) => s.success), Result.succeed ) } @@ -1663,32 +1531,26 @@ export function success(): Prism, A> { * * **Example** (Accessing failure) * - * ```ts + * ```ts import.meta.vitest * import { Optic, Result } from "effect" * * const _err = Optic.id>().compose(Optic.failure()) * - * console.log(Result.isSuccess(_err.getResult(Result.fail("oops")))) - * // Output: true + * _err.getResult(Result.fail("oops")) // => Result.succeed("oops") * - * console.log(Result.isFailure(_err.getResult(Result.succeed(42)))) - * // Output: true + * Result.isFailure(_err.getResult(Result.succeed(42))) // => true * ``` * * @see {@link success} — focuses on the success side * @see {@link Prism} — the type this function returns * - * @category Prism + * @category constructors * @since 4.0.0 */ export function failure(): Prism, E> { const run = runRefinement(Result.isFailure, { expected: "a Result.Failure value" }) return makePrism( - (s) => - Result.mapBoth(run(s), { - onFailure: String, - onSuccess: (s) => s.failure - }), + (s) => Result.map(run(s), (s) => s.failure), Result.fail ) } diff --git a/.context/effect/packages/effect/src/Option.ts b/.context/effect/packages/effect/src/Option.ts index e292f0f56..9e94f8562 100644 --- a/.context/effect/packages/effect/src/Option.ts +++ b/.context/effect/packages/effect/src/Option.ts @@ -20,6 +20,7 @@ import type { TypeLambda } from "./HKT.ts" import type { Inspectable } from "./Inspectable.ts" import * as doNotation from "./internal/doNotation.ts" import * as option from "./internal/option.ts" +import * as InternalRecord from "./internal/record.ts" import * as result from "./internal/result.ts" import type { Order } from "./Order.ts" import * as order from "./Order.ts" @@ -177,17 +178,16 @@ export declare namespace Option { * * **Example** (Extracting the value type) * - * ```ts - * import type { Option } from "effect" - * - * declare const myOption: Option.Option + * ```ts import.meta.vitest + * import { Option } from "effect" * - * // ┌─── string - * // ▼ + * const myOption: Option.Option = Option.some("value") * type MyType = Option.Option.Value + * + * const witness: MyType = "value" * ``` * - * @category Type-level Utils + * @category utility types * @since 2.0.0 */ export type Value> = [T] extends [Option] ? _A : never @@ -218,7 +218,7 @@ export interface OptionUnifyIgnore {} * Use when defining higher-kinded abstractions that must accept optional-value * types as one of their type-lambda inputs. * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface OptionTypeLambda extends TypeLambda { @@ -240,15 +240,12 @@ export interface OptionTypeLambda extends TypeLambda { * * **Example** (Creating an empty Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * // ┌─── Option * // ▼ - * const noValue = Option.none() - * - * console.log(noValue) - * // Output: { _id: 'Option', _tag: 'None' } + * const noValue = Option.none() // => Option.none() * ``` * * @see {@link some} for the opposite operation. @@ -273,15 +270,12 @@ export const none = (): Option => option.none * * **Example** (Wrapping a value) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * // ┌─── Option * // ▼ - * const value = Option.some(1) - * - * console.log(value) - * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * const value = Option.some(1) // => Option.some(1) * ``` * * @see {@link none} for the opposite operation. @@ -306,17 +300,12 @@ export const some: (value: A) => Option = option.some * * **Example** (Checking if a value is an Option) * - * ```ts + * ```ts import.meta.vitest * 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 + * Option.isOption(Option.some(1)) // => true + * Option.isOption(Option.none()) // => true + * Option.isOption({}) // => false * ``` * * @see {@link isNone} to check for `None` specifically @@ -340,14 +329,11 @@ export const isOption: (input: unknown) => input is Option = option.isO * * **Example** (Checking for None) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.isNone(Option.some(1))) - * // Output: false - * - * console.log(Option.isNone(Option.none())) - * // Output: true + * Option.isNone(Option.some(1)) // => false + * Option.isNone(Option.none()) // => true * ``` * * @see {@link isSome} for the opposite check. @@ -370,14 +356,11 @@ export const isNone: (self: Option) => self is None = option.isNone * * **Example** (Checking for Some) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.isSome(Option.some(1))) - * // Output: true - * - * console.log(Option.isSome(Option.none())) - * // Output: false + * Option.isSome(Option.some(1)) // => true + * Option.isSome(Option.none()) // => false * ``` * * @see {@link isNone} for the opposite check. @@ -403,16 +386,13 @@ export const isSome: (self: Option) => self is Some = option.isSome * * **Example** (Matching on an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * const message = Option.match(Option.some(1), { + * Option.match(Option.some(1), { * onNone: () => "Option is empty", * onSome: (value) => `Option has a value: ${value}` - * }) - * - * console.log(message) - * // Output: "Option has a value: 1" + * }) // => "Option has a value: 1" * ``` * * @see {@link getOrElse} for unwrapping with a default @@ -453,7 +433,7 @@ export const match: { * * **Example** (Converting a parser to a type guard) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * type MyData = string | number @@ -465,11 +445,8 @@ export const match: { * // ▼ * const isString = Option.toRefinement(parseString) * - * console.log(isString("a")) - * // Output: true - * - * console.log(isString(1)) - * // Output: false + * isString("a") // => true + * isString(1) // => false * ``` * * @see {@link liftPredicate} for the reverse direction @@ -495,14 +472,11 @@ export const toRefinement = (f: (a: A) => Option): (a: A) => * * **Example** (Getting the first element) * - * ```ts + * ```ts import.meta.vitest * 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' } + * Option.fromIterable([1, 2, 3]) // => Option.some(1) + * Option.fromIterable([]) // => Option.none() * ``` * * @see {@link toArray} for the inverse direction @@ -532,14 +506,11 @@ export const fromIterable = (collection: Iterable): Option => { * * **Example** (Extracting the success side) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * console.log(Option.getSuccess(Result.succeed("ok"))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'ok' } - * - * console.log(Option.getSuccess(Result.fail("err"))) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.getSuccess(Result.succeed("ok")) // => Option.some("ok") + * Option.getSuccess(Result.fail("err")) // => Option.none() * ``` * * @see {@link getFailure} for the opposite operation. @@ -564,14 +535,11 @@ export const getSuccess: (self: Result) => Option = result.getSuc * * **Example** (Extracting the failure side) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * console.log(Option.getFailure(Result.succeed("ok"))) - * // Output: { _id: 'Option', _tag: 'None' } - * - * console.log(Option.getFailure(Result.fail("err"))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'err' } + * Option.getFailure(Result.succeed("ok")) // => Option.none() + * Option.getFailure(Result.fail("err")) // => Option.some("err") * ``` * * @see {@link getSuccess} for the opposite operation. @@ -597,14 +565,11 @@ export const getFailure: (self: Result) => Option = result.getFai * * **Example** (Unwrapping with a fallback) * - * ```ts + * ```ts import.meta.vitest * 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 + * Option.some(1).pipe(Option.getOrElse(() => 0)) // => 1 + * Option.none().pipe(Option.getOrElse(() => 0)) // => 0 * ``` * * @see {@link getOrNull} to fall back to `null` @@ -638,14 +603,11 @@ export const getOrElse: { * * **Example** (Providing a fallback Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * 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' } + * Option.none().pipe(Option.orElse(() => Option.some("b"))) // => Option.some("b") + * Option.some("a").pipe(Option.orElse(() => Option.some("b"))) // => Option.some("a") * ``` * * @see {@link orElseSome} to wrap the fallback value in `Some` automatically @@ -677,14 +639,11 @@ export const orElse: { * * **Example** (Providing a fallback value) * - * ```ts + * ```ts import.meta.vitest * 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' } + * Option.none().pipe(Option.orElseSome(() => "b")) // => Option.some("b") + * Option.some("a").pipe(Option.orElseSome(() => "b")) // => Option.some("a") * ``` * * @see {@link orElse} when the fallback is itself an `Option` @@ -716,14 +675,13 @@ export const orElseSome: { * * **Example** (Tracking value source) * - * ```ts - * import { Option } from "effect" + * ```ts import.meta.vitest + * import { Option, Result } from "effect" * - * console.log(Option.orElseResult(Option.some("primary"), () => Option.some("fallback"))) - * // Output: { _id: 'Option', _tag: 'Some', value: { _tag: 'Failure', value: 'primary' } } + * const fallback = () => Option.some("fallback") * - * console.log(Option.orElseResult(Option.none(), () => Option.some("fallback"))) - * // Output: { _id: 'Option', _tag: 'Some', value: { _tag: 'Success', value: 'fallback' } } + * Option.orElseResult(Option.some("primary"), fallback) // => Option.some(Result.fail("primary")) + * Option.orElseResult(Option.none(), fallback) // => Option.some(Result.succeed("fallback")) * ``` * * @see {@link orElse} for the simpler variant without source tracking @@ -755,15 +713,14 @@ export const orElseResult: { * * **Example** (Finding the first Some) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.firstSomeOf([ + * Option.firstSomeOf([ * Option.none(), * Option.some(1), * Option.some(2) - * ])) - * // Output: { _id: 'Option', _tag: 'Some', value: 1 } + * ]) // => Option.some(1) * ``` * * @see {@link orElse} for a two-option fallback @@ -798,17 +755,12 @@ export const firstSomeOf = > = Iterable Option.none() + * Option.fromNullishOr(null) // => Option.none() + * Option.fromNullishOr(1) // => Option.some(1) * ``` * * @see {@link fromNullOr} to only treat `null` as absent @@ -838,17 +790,12 @@ export const fromNullishOr = ( * * **Example** (Converting possibly undefined values to an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.fromUndefinedOr(undefined)) - * // Output: { _id: 'Option', _tag: 'None' } - * - * console.log(Option.fromUndefinedOr(null)) - * // Output: { _id: 'Option', _tag: 'Some', value: null } - * - * console.log(Option.fromUndefinedOr(42)) - * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * Option.fromUndefinedOr(undefined) // => Option.none() + * Option.fromUndefinedOr(null) // => Option.some(null) + * Option.fromUndefinedOr(42) // => Option.some(42) * ``` * * @see {@link fromNullishOr} to treat both `null` and `undefined` as absent @@ -877,17 +824,12 @@ export const fromUndefinedOr = ( * * **Example** (Converting possibly null values to an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.fromNullOr(null)) - * // Output: { _id: 'Option', _tag: 'None' } - * - * console.log(Option.fromNullOr(undefined)) - * // Output: { _id: 'Option', _tag: 'Some', value: undefined } - * - * console.log(Option.fromNullOr(42)) - * // Output: { _id: 'Option', _tag: 'Some', value: 42 } + * Option.fromNullOr(null) // => Option.none() + * Option.fromNullOr(undefined) // => Option.some(undefined) + * Option.fromNullOr(42) // => Option.some(42) * ``` * * @see {@link fromNullishOr} to treat both `null` and `undefined` as absent @@ -915,7 +857,7 @@ export const fromNullOr = ( * * **Example** (Lifting a parser) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const parse = (s: string): number | undefined => { @@ -925,11 +867,8 @@ export const fromNullOr = ( * * const parseOption = Option.liftNullishOr(parse) * - * console.log(parseOption("1")) - * // Output: { _id: 'Option', _tag: 'Some', value: 1 } - * - * console.log(parseOption("not a number")) - * // Output: { _id: 'Option', _tag: 'None' } + * parseOption("1") // => Option.some(1) + * parseOption("not a number") // => Option.none() * ``` * * @see {@link fromNullishOr} for converting a single value @@ -957,14 +896,11 @@ export const liftNullishOr = , B>( * * **Example** (Unwrapping to null) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.getOrNull(Option.some(1))) - * // Output: 1 - * - * console.log(Option.getOrNull(Option.none())) - * // Output: null + * Option.getOrNull(Option.some(1)) // => 1 + * Option.getOrNull(Option.none()) // => null * ``` * * @see {@link getOrUndefined} to return `undefined` instead @@ -990,14 +926,11 @@ export const getOrNull: (self: Option) => A | null = getOrElse(constNull) * * **Example** (Unwrapping to undefined) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.getOrUndefined(Option.some(1))) - * // Output: 1 - * - * console.log(Option.getOrUndefined(Option.none())) - * // Output: undefined + * Option.getOrUndefined(Option.some(1)) // => 1 + * Option.getOrUndefined(Option.none()) // => undefined * ``` * * @see {@link getOrNull} to return `null` instead @@ -1022,16 +955,13 @@ export const getOrUndefined: (self: Option) => A | undefined = getOrElse(c * * **Example** (Lifting JSON.parse) * - * ```ts + * ```ts import.meta.vitest * 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' } + * parse("1") // => Option.some(1) + * parse("") // => Option.none() * ``` * * @see {@link liftNullishOr} for nullable-returning functions @@ -1065,14 +995,16 @@ export const liftThrowable = , B>( * * **Example** (Throwing a custom error) * - * ```ts - * import { Option } from "effect" + * ```ts import.meta.vitest + * import { Option, Result } from "effect" * - * console.log(Option.getOrThrowWith(Option.some(1), () => new Error("missing"))) - * // Output: 1 + * Option.getOrThrowWith(Option.some(1), () => new Error("missing")) // => 1 * - * Option.getOrThrowWith(Option.none(), () => new Error("missing")) - * // throws Error: missing + * const failure = Result.try({ + * try: () => Option.getOrThrowWith(Option.none(), () => new Error("missing")), + * catch: (error) => (error as Error).message + * }) + * Result.getFailure(failure).pipe(Option.getOrElse(() => "no error")) // => "missing" * ``` * * @see {@link getOrThrow} for a version with a default error @@ -1106,14 +1038,16 @@ export const getOrThrowWith: { * * **Example** (Throwing a default error) * - * ```ts - * import { Option } from "effect" + * ```ts import.meta.vitest + * import { Option, Result } from "effect" * - * console.log(Option.getOrThrow(Option.some(1))) - * // Output: 1 + * Option.getOrThrow(Option.some(1)) // => 1 * - * Option.getOrThrow(Option.none()) - * // throws Error: getOrThrow called on a None + * const failure = Result.try({ + * try: () => Option.getOrThrow(Option.none()), + * catch: (error) => (error as Error).message + * }) + * Result.getFailure(failure).pipe(Option.getOrElse(() => "no error")) // => "getOrThrow called on a None" * ``` * * @see {@link getOrThrowWith} for a custom error @@ -1140,14 +1074,11 @@ export const getOrThrow: (self: Option) => A = getOrThrowWith(() => new Er * * **Example** (Mapping over an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.map(Option.some(2), (n) => n * 2)) - * // Output: { _id: 'Option', _tag: 'Some', value: 4 } - * - * console.log(Option.map(Option.none(), (n: number) => n * 2)) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.map(Option.some(2), (n) => n * 2) // => Option.some(4) + * Option.map(Option.none(), (n: number) => n * 2) // => Option.none() * ``` * * @see {@link flatMap} when `f` returns an `Option` @@ -1174,14 +1105,11 @@ export const map: { * * **Example** (Replacing a value) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.as(Option.some(42), "new value")) - * // Output: { _id: 'Option', _tag: 'Some', value: 'new value' } - * - * console.log(Option.as(Option.none(), "new value")) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.as(Option.some(42), "new value") // => Option.some("new value") + * Option.as(Option.none(), "new value") // => Option.none() * ``` * * @see {@link asVoid} to replace with `undefined` @@ -1206,14 +1134,11 @@ export const as: { * * **Example** (Voiding the value) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.asVoid(Option.some(42))) - * // Output: { _id: 'Option', _tag: 'Some', value: undefined } - * - * console.log(Option.asVoid(Option.none())) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.asVoid(Option.some(42)) // => Option.some(undefined) + * Option.asVoid(Option.none()) // => Option.none() * ``` * * @see {@link as} to replace with a specific constant @@ -1234,11 +1159,10 @@ export { * * **Example** (Referencing Option.void) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.void) - * // Output: { _id: 'Option', _tag: 'Some', value: undefined } + * Option.void // => Option.some(undefined) * ``` * * @see {@link asVoid} to convert an existing `Option` to `Option` @@ -1266,7 +1190,7 @@ export { * * **Example** (Chaining optional lookups) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * interface User { @@ -1279,12 +1203,9 @@ export { * address: Option.some({ street: Option.some("123 Main St") }) * } * - * const street = user.address.pipe( + * user.address.pipe( * Option.flatMap((addr) => addr.street) - * ) - * - * console.log(street) - * // Output: { _id: 'Option', _tag: 'Some', value: '123 Main St' } + * ) // => Option.some("123 Main St") * ``` * * @see {@link map} when `f` returns a plain value @@ -1320,20 +1241,17 @@ export const flatMap: { * * **Example** (Chaining with andThen) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * // Chain with a function returning Option - * console.log(Option.andThen(Option.some(5), (x) => Option.some(x * 2))) - * // Output: { _id: 'Option', _tag: 'Some', value: 10 } + * Option.andThen(Option.some(5), (x) => Option.some(x * 2)) // => Option.some(10) * * // Chain with a static value - * console.log(Option.andThen(Option.some(5), "hello")) - * // Output: { _id: 'Option', _tag: 'Some', value: "hello" } + * Option.andThen(Option.some(5), "hello") // => Option.some("hello") * * // Chain with None - skips - * console.log(Option.andThen(Option.none(), (x) => Option.some(x * 2))) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.andThen(Option.none(), (x) => Option.some(x * 2)) // => Option.none() * ``` * * @see {@link flatMap} for the standard monadic bind @@ -1376,7 +1294,7 @@ export const andThen: { * * **Example** (Navigating optional properties) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * interface Employee { @@ -1387,12 +1305,9 @@ export const andThen: { * company: { address: { street: { name: "high street" } } } * } * - * console.log( - * Option.some(emp).pipe( - * Option.flatMapNullishOr((e) => e.company?.address?.street?.name) - * ) - * ) - * // Output: { _id: 'Option', _tag: 'Some', value: 'high street' } + * Option.some(emp).pipe( + * Option.flatMapNullishOr((e) => e.company?.address?.street?.name) + * ) // => Option.some("high street") * ``` * * @see {@link flatMap} when the function already returns `Option` @@ -1425,14 +1340,11 @@ export const flatMapNullishOr: { * * **Example** (Flattening nested Options) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.flatten(Option.some(Option.some("value")))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'value' } - * - * console.log(Option.flatten(Option.some(Option.none()))) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.flatten(Option.some(Option.some("value"))) // => Option.some("value") + * Option.flatten(Option.some(Option.none())) // => Option.none() * ``` * * @see {@link flatMap} which is `map` + `flatten` @@ -1457,14 +1369,11 @@ export const flatten: (self: Option>) => Option = flatMap(identi * * **Example** (Keeping the second value) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.zipRight(Option.some(1), Option.some("hello"))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } - * - * console.log(Option.zipRight(Option.none(), Option.some("hello"))) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.zipRight(Option.some(1), Option.some("hello")) // => Option.some("hello") + * Option.zipRight(Option.none(), Option.some("hello")) // => Option.none() * ``` * * @see {@link zipLeft} to keep the first value instead @@ -1493,14 +1402,11 @@ export const zipRight: { * * **Example** (Keeping the first value) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.zipLeft(Option.some("hello"), Option.some(1))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } - * - * console.log(Option.zipLeft(Option.some("hello"), Option.none())) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.zipLeft(Option.some("hello"), Option.some(1)) // => Option.some("hello") + * Option.zipLeft(Option.some("hello"), Option.none()) // => Option.none() * ``` * * @see {@link zipRight} to keep the second value instead @@ -1530,7 +1436,7 @@ export const zipLeft: { * * **Example** (Composing parsers) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const parse = (s: string): Option.Option => @@ -1541,11 +1447,8 @@ export const zipLeft: { * * 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' } + * parseAndDouble("42") // => Option.some(84) + * parseAndDouble("not a number") // => Option.none() * ``` * * @see {@link flatMap} for single-step chaining @@ -1575,17 +1478,14 @@ export const composeK: { * * **Example** (Validating without transforming) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const getInteger = (n: number) => * Number.isInteger(n) ? Option.some(n) : Option.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' } + * Option.tap(Option.some(1), getInteger) // => Option.some(1) + * Option.tap(Option.some(1.14), getInteger) // => Option.none() * ``` * * @see {@link flatMap} when you want to transform the value @@ -1615,14 +1515,11 @@ export const tap: { * * **Example** (Pairing two Options) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.product(Option.some("hello"), Option.some(42))) - * // Output: { _id: 'Option', _tag: 'Some', value: ['hello', 42] } - * - * console.log(Option.product(Option.none(), Option.some(42))) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.product(Option.some("hello"), Option.some(42)) // => Option.some(["hello", 42]) + * Option.product(Option.none(), Option.some(42)) // => Option.none() * ``` * * @see {@link zipWith} to combine with a function instead of a tuple @@ -1650,17 +1547,14 @@ export const product = (self: Option, that: Option): Option<[A, B]> * * **Example** (Combining many Options) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const first = Option.some(1) * const rest = [Option.some(2), Option.some(3)] * - * console.log(Option.productMany(first, rest)) - * // Output: { _id: 'Option', _tag: 'Some', value: [1, 2, 3] } - * - * console.log(Option.productMany(first, [Option.some(2), Option.none()])) - * // Output: { _id: 'Option', _tag: 'None' } + * Option.productMany(first, rest) // => Option.some([1, 2, 3]) + * Option.productMany(first, [Option.some(2), Option.none()]) // => Option.none() * ``` * * @see {@link product} for combining exactly two @@ -1704,7 +1598,7 @@ export const productMany = ( * * **Example** (Combining a tuple and a struct) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const maybeName: Option.Option = Option.some("John") @@ -1712,17 +1606,11 @@ export const productMany = ( * * // ┌─── Option<[string, number]> * // ▼ - * const tuple = Option.all([maybeName, maybeAge]) - * console.log(tuple) - * // Output: - * // { _id: 'Option', _tag: 'Some', value: [ 'John', 25 ] } + * const tuple = Option.all([maybeName, maybeAge]) // => Option.some(["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 } } + * const struct = Option.all({ name: maybeName, age: maybeAge }) // => Option.some({ name: "John", age: 25 }) * ``` * * @see {@link product} for combining exactly two @@ -1758,7 +1646,7 @@ export const all: > | Record> | Record ({ name: name.toUpperCase(), age }) - * ) - * - * console.log(person) - * // Output: - * // { _id: 'Option', _tag: 'Some', value: { name: 'JOHN', age: 25 } } + * ) // => Option.some({ name: "JOHN", age: 25 }) * ``` * * @see {@link product} to combine into a tuple instead @@ -1823,16 +1707,15 @@ export const zipWith: { * * **Example** (Summing present values) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe } from "effect" * * const items = [Option.some(1), Option.none(), Option.some(2), Option.none()] * - * console.log(pipe(items, Option.reduceCompact(0, (b, a) => b + a))) - * // Output: 3 + * pipe(items, Option.reduceCompact(0, (b, a) => b + a)) // => 3 * ``` * - * @category reducing + * @category folding * @since 2.0.0 */ export const reduceCompact: { @@ -1866,14 +1749,11 @@ export const reduceCompact: { * * **Example** (Converting to an array) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * - * console.log(Option.toArray(Option.some(1))) - * // Output: [1] - * - * console.log(Option.toArray(Option.none())) - * // Output: [] + * Option.toArray(Option.some(1)) // => [1] + * Option.toArray(Option.none()) // => [] * ``` * * @see {@link fromIterable} for the inverse direction @@ -1899,7 +1779,7 @@ export const toArray = (self: Option): Array => isNone(self) ? [] : [se * * **Example** (Partitioning by Result) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * * const parseNumber = (s: string): Result.Result => { @@ -1907,14 +1787,9 @@ export const toArray = (self: Option): Array => isNone(self) ? [] : [se * return isNaN(n) ? Result.fail("Not a number") : Result.succeed(n) * } * - * console.log(Option.partitionMap(Option.some("42"), parseNumber)) - * // Output: [{ _id: 'Option', _tag: 'None' }, { _id: 'Option', _tag: 'Some', value: 42 }] - * - * console.log(Option.partitionMap(Option.some("abc"), parseNumber)) - * // Output: [{ _id: 'Option', _tag: 'Some', value: 'Not a number' }, { _id: 'Option', _tag: 'None' }] - * - * console.log(Option.partitionMap(Option.none(), parseNumber)) - * // Output: [{ _id: 'Option', _tag: 'None' }, { _id: 'Option', _tag: 'None' }] + * Option.partitionMap(Option.some("42"), parseNumber) // => [Option.none(), Option.some(42)] + * Option.partitionMap(Option.some("abc"), parseNumber) // => [Option.some("Not a number"), Option.none()] + * Option.partitionMap(Option.none(), parseNumber) // => [Option.none(), Option.none()] * ``` * * @see {@link filter} for simple predicate-based filtering @@ -1951,14 +1826,13 @@ export const partitionMap: { * * **Example** (Filtering and transforming) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * console.log(Option.filterMap( + * Option.filterMap( * Option.some(2), * (n) => (n % 2 === 0 ? Result.succeed(`Even: ${n}`) : Result.failVoid) - * )) - * // Output: { _id: 'Option', _tag: 'Some', value: 'Even: 2' } + * ) // => Option.some("Even: 2") * ``` * * @see {@link filter} for predicate-based filtering @@ -1995,20 +1869,15 @@ export const filterMap: { * * **Example** (Filtering with a predicate) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const removeEmpty = (input: Option.Option) => * Option.filter(input, (value) => value !== "") * - * console.log(removeEmpty(Option.some("hello"))) - * // Output: { _id: 'Option', _tag: 'Some', value: 'hello' } - * - * console.log(removeEmpty(Option.some(""))) - * // Output: { _id: 'Option', _tag: 'None' } - * - * console.log(removeEmpty(Option.none())) - * // Output: { _id: 'Option', _tag: 'None' } + * removeEmpty(Option.some("hello")) // => Option.some("hello") + * removeEmpty(Option.some("")) // => Option.none() + * removeEmpty(Option.none()) // => Option.none() * ``` * * @see {@link filterMap} to transform and filter simultaneously @@ -2044,19 +1913,14 @@ export const filter: { * * **Example** (Comparing Options) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Option } from "effect" * * const eq = Option.makeEquivalence(Equivalence.strictEqual()) * - * console.log(eq(Option.some(1), Option.some(1))) - * // Output: true - * - * console.log(eq(Option.some(1), Option.some(2))) - * // Output: false - * - * console.log(eq(Option.none(), Option.none())) - * // Output: true + * eq(Option.some(1), Option.some(1)) // => true + * eq(Option.some(1), Option.some(2)) // => false + * eq(Option.none(), Option.none()) // => true * ``` * * @category instances @@ -2082,19 +1946,14 @@ export const makeEquivalence = (isEquivalent: Equivalence.Equivalence): Eq * * **Example** (Ordering Options) * - * ```ts + * ```ts import.meta.vitest * import { Number as N, Option } from "effect" * * const ord = Option.makeOrder(N.Order) * - * console.log(ord(Option.none(), Option.some(1))) - * // Output: -1 - * - * console.log(ord(Option.some(1), Option.none())) - * // Output: 1 - * - * console.log(ord(Option.some(1), Option.some(2))) - * // Output: -1 + * ord(Option.none(), Option.some(1)) // => -1 + * ord(Option.some(1), Option.none()) // => 1 + * ord(Option.some(1), Option.some(2)) // => -1 * ``` * * @category sorting @@ -2118,16 +1977,13 @@ export const makeOrder = (O: Order): Order> => * * **Example** (Lifting addition) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const addOptions = Option.lift2((a: number, b: number) => a + b) * - * console.log(addOptions(Option.some(2), Option.some(3))) - * // Output: { _id: 'Option', _tag: 'Some', value: 5 } - * - * console.log(addOptions(Option.some(2), Option.none())) - * // Output: { _id: 'Option', _tag: 'None' } + * addOptions(Option.some(2), Option.some(3)) // => Option.some(5) + * addOptions(Option.some(2), Option.none()) // => Option.none() * ``` * * @see {@link zipWith} for a non-lifted variant @@ -2157,16 +2013,13 @@ export const lift2 = (f: (a: A, b: B) => C): { * * **Example** (Validating positive numbers) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const parsePositive = Option.liftPredicate((n: number) => n > 0) * - * console.log(parsePositive(1)) - * // Output: { _id: 'Option', _tag: 'Some', value: 1 } - * - * console.log(parsePositive(-1)) - * // Output: { _id: 'Option', _tag: 'None' } + * parsePositive(1) // => Option.some(1) + * parsePositive(-1) // => Option.none() * ``` * * @see {@link filter} to apply a predicate to an existing `Option` @@ -2207,24 +2060,19 @@ export const liftPredicate: { // Note: I intentionally avoid using the NoInfer p * * **Example** (Checking with custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Option } from "effect" * * const check = Option.containsWith(Equivalence.strictEqual()) * - * console.log(Option.some(2).pipe(check(2))) - * // Output: true - * - * console.log(Option.some(1).pipe(check(2))) - * // Output: false - * - * console.log(Option.none().pipe(check(2))) - * // Output: false + * Option.some(2).pipe(check(2)) // => true + * Option.some(1).pipe(check(2)) // => false + * Option.none().pipe(check(2)) // => false * ``` * * @see {@link contains} for a version using default equality * - * @category elements + * @category predicates * @since 2.0.0 */ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { @@ -2248,23 +2096,18 @@ export const containsWith = (isEquivalent: (self: A, that: A) => boolean): { * * **Example** (Checking containment) * - * ```ts + * ```ts import.meta.vitest * 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 + * Option.some(2).pipe(Option.contains(2)) // => true + * Option.some(1).pipe(Option.contains(2)) // => false + * Option.none().pipe(Option.contains(2)) // => false * ``` * * @see {@link containsWith} for custom equality * @see {@link exists} to test with a predicate * - * @category elements + * @category predicates * @since 2.0.0 */ export const contains: { @@ -2288,25 +2131,20 @@ export const contains: { * * **Example** (Testing a condition) * - * ```ts + * ```ts import.meta.vitest * 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 + * Option.some(2).pipe(Option.exists(isEven)) // => true + * Option.some(1).pipe(Option.exists(isEven)) // => false + * Option.none().pipe(Option.exists(isEven)) // => false * ``` * * @see {@link filter} to keep or discard based on a predicate * @see {@link contains} to test for a specific value * - * @category elements + * @category predicates * @since 2.0.0 */ export const exists: { @@ -2335,24 +2173,22 @@ export const exists: { * * **Example** (Starting do notation) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe } from "effect" - * import * as assert from "node:assert" * - * const result = pipe( + * pipe( * Option.some(2), * Option.bindTo("x"), * Option.bind("y", () => Option.some(3)), * Option.let("sum", ({ x, y }) => x + y) - * ) - * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ) // => Option.some({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link Do} for starting with an empty record * @see {@link bind} to add `Option` values * @see {@link let_ let} to add plain values * - * @category do notation + * @category mapping * @since 2.0.0 */ export const bindTo: { @@ -2383,24 +2219,22 @@ export { * * **Example** (Adding a computed value) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe } from "effect" - * import * as assert from "node:assert" * - * const result = pipe( + * pipe( * Option.Do, * Option.bind("x", () => Option.some(2)), * Option.bind("y", () => Option.some(3)), * Option.let("sum", ({ x, y }) => x + y) - * ) - * assert.deepStrictEqual(result, Option.some({ x: 2, y: 3, sum: 5 })) + * ) // => Option.some({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link Do} for starting the chain * @see {@link bind} to add `Option` values * @see {@link bindTo} to start by naming an existing `Option` * - * @category do notation + * @category mapping * @since 2.0.0 */ let_ as let @@ -2416,25 +2250,23 @@ export { * * **Example** (Binding Option values) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe } from "effect" - * import * as assert from "node:assert" * - * const result = pipe( + * 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 })) + * ) // => Option.some({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link Do} for starting the chain * @see {@link let_ let} to add plain values * @see {@link bindTo} to start by naming an existing `Option` * - * @category do notation + * @category sequencing * @since 2.0.0 */ export const bind: { @@ -2460,25 +2292,23 @@ export const bind: { * * **Example** (Building Option pipelines with do notation) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe } from "effect" - * import * as assert from "node:assert" * - * const result = pipe( + * 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 })) + * ) // => Option.some({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link bind} to add `Option` values * @see {@link let_ let} to add plain values * @see {@link bindTo} to start by naming an existing `Option` * - * @category do notation + * @category constructors * @since 2.0.0 */ export const Do: Option<{}> = some({}) @@ -2500,21 +2330,17 @@ export const Do: Option<{}> = some({}) * * **Example** (Sequencing Option computations with generator syntax) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * * const maybeName: Option.Option = Option.some("John") * const maybeAge: Option.Option = Option.some(25) * - * const person = Option.gen(function*() { + * 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 } } + * }) // => Option.some({ name: "JOHN", age: 25 }) * ``` * * @see {@link Do} / {@link bind} for the do notation alternative @@ -2555,17 +2381,16 @@ export const gen: Gen.Gen = (...args) => { * * **Example** (Reducing with first-wins semantics) * - * ```ts + * ```ts import.meta.vitest * import { Number, Option } from "effect" * * const reducer = Option.makeReducer(Number.ReducerSum) - * console.log(reducer.combineAll([Option.some(1), Option.none(), Option.some(2)])) - * // Output: { _id: 'Option', _tag: 'Some', value: 3 } + * reducer.combineAll([Option.some(1), Option.none(), Option.some(2)]) // => Option.some(3) * ``` * * @see {@link makeReducerFailFast} for fail-fast semantics * - * @category Reducer + * @category constructors * @since 4.0.0 */ export function makeReducer(combiner: Combiner.Combiner): Reducer.Reducer> { @@ -2593,20 +2418,17 @@ export function makeReducer(combiner: Combiner.Combiner): Reducer.Reducer< * * **Example** (Fail-fast combining) * - * ```ts + * ```ts import.meta.vitest * import { Number, Option } from "effect" * * const combiner = Option.makeCombinerFailFast(Number.ReducerSum) - * console.log(combiner.combine(Option.some(1), Option.some(2))) - * // Output: { _id: 'Option', _tag: 'Some', value: 3 } - * - * console.log(combiner.combine(Option.some(1), Option.none())) - * // Output: { _id: 'Option', _tag: 'None' } + * combiner.combine(Option.some(1), Option.some(2)) // => Option.some(3) + * combiner.combine(Option.some(1), Option.none()) // => Option.none() * ``` * * @see {@link makeReducerFailFast} to get a full `Reducer` * - * @category Combiner + * @category constructors * @since 4.0.0 */ export function makeCombinerFailFast(combiner: Combiner.Combiner): Combiner.Combiner> { @@ -2633,21 +2455,18 @@ export function makeCombinerFailFast(combiner: Combiner.Combiner): Combine * * **Example** (Fail-fast reducing) * - * ```ts + * ```ts import.meta.vitest * import { Number, Option } from "effect" * * const reducer = Option.makeReducerFailFast(Number.ReducerSum) - * console.log(reducer.combineAll([Option.some(1), Option.some(2)])) - * // Output: { _id: 'Option', _tag: 'Some', value: 3 } - * - * console.log(reducer.combineAll([Option.some(1), Option.none()])) - * // Output: { _id: 'Option', _tag: 'None' } + * reducer.combineAll([Option.some(1), Option.some(2)]) // => Option.some(3) + * reducer.combineAll([Option.some(1), Option.none()]) // => Option.none() * ``` * * @see {@link makeCombinerFailFast} for just the combiner * @see {@link makeReducer} for non-fail-fast semantics * - * @category Reducer + * @category constructors * @since 4.0.0 */ export function makeReducerFailFast(reducer: Reducer.Reducer): Reducer.Reducer> { diff --git a/.context/effect/packages/effect/src/Order.ts b/.context/effect/packages/effect/src/Order.ts index dcc7bf42c..c52ea3604 100644 --- a/.context/effect/packages/effect/src/Order.ts +++ b/.context/effect/packages/effect/src/Order.ts @@ -31,7 +31,7 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Defining a custom Order) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const byAge: Order.Order<{ name: string; age: number }> = (self, that) => { @@ -42,12 +42,12 @@ import * as Reducer from "./Reducer.ts" * * const person1 = { name: "Alice", age: 30 } * const person2 = { name: "Bob", age: 25 } - * console.log(byAge(person1, person2)) // 1 + * byAge(person1, person2) // => 1 * ``` * * @see {@link make} to create an order from a comparison function * @see {@link Ordering} for the result type of comparisons - * @category type class + * @category models * @since 2.0.0 */ export interface Order { @@ -66,7 +66,7 @@ export interface Order { * This is type-level only, has no runtime representation, and is used * internally by the Effect type system. * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface OrderTypeLambda extends TypeLambda { @@ -90,7 +90,7 @@ export interface OrderTypeLambda extends TypeLambda { * * **Example** (Creating an Order) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const byAge = Order.make<{ name: string; age: number }>((self, that) => { @@ -99,8 +99,8 @@ export interface OrderTypeLambda extends TypeLambda { * return 0 * }) * - * console.log(byAge({ name: "Alice", age: 30 }, { name: "Bob", age: 25 })) // 1 - * console.log(byAge({ name: "Alice", age: 25 }, { name: "Bob", age: 30 })) // -1 + * byAge({ name: "Alice", age: 30 }, { name: "Bob", age: 25 }) // => 1 + * byAge({ name: "Alice", age: 25 }, { name: "Bob", age: 30 }) // => -1 * ``` * * @see {@link mapInput} to transform an order by mapping the input type @@ -128,12 +128,12 @@ export function make( * * **Example** (Ordering strings) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * - * console.log(Order.String("apple", "banana")) // -1 - * console.log(Order.String("banana", "apple")) // 1 - * console.log(Order.String("apple", "apple")) // 0 + * Order.String("apple", "banana") // => -1 + * Order.String("banana", "apple") // => 1 + * Order.String("apple", "apple") // => 0 * ``` * * @see {@link mapInput} to compare objects by a string property @@ -158,15 +158,15 @@ export const String: Order = make((self, that) => self < that ? -1 : 1) * * **Example** (Ordering numbers) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * - * console.log(Order.Number(1, 1)) // 0 - * console.log(Order.Number(1, 2)) // -1 - * console.log(Order.Number(2, 1)) // 1 + * Order.Number(1, 1) // => 0 + * Order.Number(1, 2) // => -1 + * Order.Number(2, 1) // => 1 * - * console.log(Order.Number(0, -0)) // 0 - * console.log(Order.Number(NaN, 1)) // -1 + * Order.Number(0, -0) // => 0 + * Order.Number(NaN, 1) // => -1 * ``` * * @see {@link mapInput} to compare objects by a number property @@ -194,12 +194,12 @@ export const Number: Order = make((self, that) => { * * **Example** (Ordering booleans) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * - * console.log(Order.Boolean(false, true)) // -1 - * console.log(Order.Boolean(true, false)) // 1 - * console.log(Order.Boolean(true, true)) // 0 + * Order.Boolean(false, true) // => -1 + * Order.Boolean(true, false) // => 1 + * Order.Boolean(true, true) // => 0 * ``` * * @see {@link mapInput} to compare objects by a boolean property @@ -222,12 +222,12 @@ export const Boolean: Order = make((self, that) => self < that ? -1 : 1 * * **Example** (Ordering BigInts) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * - * console.log(Order.BigInt(1n, 2n)) // -1 - * console.log(Order.BigInt(2n, 1n)) // 1 - * console.log(Order.BigInt(1n, 1n)) // 0 + * Order.BigInt(1n, 2n) // => -1 + * Order.BigInt(2n, 1n) // => 1 + * Order.BigInt(1n, 1n) // => 0 * ``` * * @see {@link Number} for regular number comparisons @@ -252,14 +252,14 @@ export const BigInt: Order = make((self, that) => self < that ? -1 : 1) * * **Example** (Reversing an Order) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const flip = Order.flip(Order.Number) * - * console.log(flip(1, 2)) // 1 - * console.log(flip(2, 1)) // -1 - * console.log(flip(1, 1)) // 0 + * flip(1, 2) // => 1 + * flip(2, 1) // => -1 + * flip(1, 1) // => 0 * ``` * * @see {@link combine} to combine orders for multi-criteria comparison @@ -286,7 +286,7 @@ export function flip(O: Order): Order { * * **Example** (Combining two Orders) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const byAge = Order.mapInput( @@ -303,8 +303,8 @@ export function flip(O: Order): Order { * const person2 = { name: "Bob", age: 30 } * const person3 = { name: "Charlie", age: 25 } * - * console.log(byAgeAndName(person1, person2)) // -1 (Same age, Alice < Bob) - * console.log(byAgeAndName(person1, person3)) // 1 (Alice (30) > Charlie (25)) + * byAgeAndName(person1, person2) // => -1 + * byAgeAndName(person1, person3) // => 1 * ``` * * @see {@link combineAll} to combine multiple orders from a collection @@ -338,14 +338,14 @@ export const combine: { * * **Example** (Ordering with an always-equal Order) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const alwaysEqualOrder = Order.alwaysEqual() * - * console.log(alwaysEqualOrder(1, 2)) // 0 - * console.log(alwaysEqualOrder(2, 1)) // 0 - * console.log(alwaysEqualOrder(1, 1)) // 0 + * alwaysEqualOrder(1, 2) // => 0 + * alwaysEqualOrder(2, 1) // => 0 + * alwaysEqualOrder(1, 1) // => 0 * ``` * * @see {@link combine} to combine with other orders @@ -371,7 +371,7 @@ export function alwaysEqual(): Order { * * **Example** (Combining multiple Orders) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const byAge = Order.mapInput( @@ -388,7 +388,7 @@ export function alwaysEqual(): Order { * const person1 = { name: "Alice", age: 30 } * const person2 = { name: "Bob", age: 30 } * - * console.log(combinedOrder(person1, person2)) // -1 (Same age, Alice < Bob) + * combinedOrder(person1, person2) // => -1 * ``` * * @see {@link combine} to combine two orders @@ -426,14 +426,14 @@ export function combineAll(collection: Iterable>): Order { * * **Example** (Mapping Input) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const byLength = Order.mapInput(Order.Number, (s: string) => s.length) * - * console.log(byLength("a", "bb")) // -1 - * console.log(byLength("bb", "a")) // 1 - * console.log(byLength("aa", "bb")) // 0 + * byLength("a", "bb") // => -1 + * byLength("bb", "a") // => 1 + * byLength("aa", "bb") // => 0 * ``` * * @see {@link combine} to combine mapped orders for multi-criteria comparison @@ -464,15 +464,15 @@ export const mapInput: { * * **Example** (Ordering Dates) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const date1 = new Date("2023-01-01") * const date2 = new Date("2023-01-02") * - * console.log(Order.Date(date1, date2)) // -1 - * console.log(Order.Date(date2, date1)) // 1 - * console.log(Order.Date(date1, date1)) // 0 + * Order.Date(date1, date2) // => -1 + * Order.Date(date2, date1) // => 1 + * Order.Date(date1, date1) // => 0 * ``` * * @see {@link mapInput} to compare objects by a date property @@ -496,14 +496,14 @@ export const Date: Order = mapInput(Number, (date) => date.getTime()) * * **Example** (Ordering tuples) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const tupleOrder = Order.Tuple([Order.Number, Order.String]) * - * console.log(tupleOrder([1, "a"], [2, "b"])) // -1 - * console.log(tupleOrder([1, "b"], [1, "a"])) // 1 - * console.log(tupleOrder([1, "a"], [1, "a"])) // 0 + * tupleOrder([1, "a"], [2, "b"]) // => -1 + * tupleOrder([1, "b"], [1, "a"]) // => 1 + * tupleOrder([1, "a"], [1, "a"]) // => 0 * ``` * * @see {@link Array} to compare arrays with length consideration @@ -560,15 +560,15 @@ export { * * **Example** (Ordering array elements) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const arrayOrder = Order.Array(Order.Number) * - * console.log(arrayOrder([1, 2], [1, 3])) // -1 - * console.log(arrayOrder([1, 2], [1, 2, 3])) // -1 (shorter array is less) - * console.log(arrayOrder([1, 2, 3], [1, 2])) // 1 (longer array is greater) - * console.log(arrayOrder([1, 2], [1, 2])) // 0 + * arrayOrder([1, 2], [1, 3]) // => -1 + * arrayOrder([1, 2], [1, 2, 3]) // => -1 + * arrayOrder([1, 2, 3], [1, 2]) // => 1 + * arrayOrder([1, 2], [1, 2]) // => 0 * ``` * * @see {@link Tuple} for type-safe tuple ordering @@ -593,7 +593,7 @@ export { * * **Example** (Ordering structs) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const personOrder = Order.Struct({ @@ -605,9 +605,9 @@ export { * const person2 = { name: "Bob", age: 25 } * const person3 = { name: "Alice", age: 25 } * - * console.log(personOrder(person1, person2)) // -1 (Alice < Bob) - * console.log(personOrder(person1, person3)) // 1 (same name, 30 > 25) - * console.log(personOrder(person1, person1)) // 0 + * personOrder(person1, person2) // => -1 + * personOrder(person1, person3) // => 1 + * personOrder(person1, person1) // => 0 * ``` * * @see {@link combine} to combine orders manually @@ -644,14 +644,14 @@ export function Struct }>( * * **Example** (Checking less-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const isLessThanNumber = Order.isLessThan(Order.Number) * - * console.log(isLessThanNumber(1, 2)) // true - * console.log(isLessThanNumber(2, 1)) // false - * console.log(isLessThanNumber(1, 1)) // false + * isLessThanNumber(1, 2) // => true + * isLessThanNumber(2, 1) // => false + * isLessThanNumber(1, 1) // => false * ``` * * @see {@link isLessThanOrEqualTo} for non-strict less than or equal @@ -678,14 +678,14 @@ export const isLessThan = (O: Order): { * * **Example** (Checking greater-than comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const isGreaterThanNumber = Order.isGreaterThan(Order.Number) * - * console.log(isGreaterThanNumber(2, 1)) // true - * console.log(isGreaterThanNumber(1, 2)) // false - * console.log(isGreaterThanNumber(1, 1)) // false + * isGreaterThanNumber(2, 1) // => true + * isGreaterThanNumber(1, 2) // => false + * isGreaterThanNumber(1, 1) // => false * ``` * * @see {@link isGreaterThanOrEqualTo} for non-strict greater than or equal @@ -712,14 +712,14 @@ export const isGreaterThan = (O: Order): { * * **Example** (Checking less-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const isLessThanOrEqualToNumber = Order.isLessThanOrEqualTo(Order.Number) * - * console.log(isLessThanOrEqualToNumber(1, 2)) // true - * console.log(isLessThanOrEqualToNumber(1, 1)) // true - * console.log(isLessThanOrEqualToNumber(2, 1)) // false + * isLessThanOrEqualToNumber(1, 2) // => true + * isLessThanOrEqualToNumber(1, 1) // => true + * isLessThanOrEqualToNumber(2, 1) // => false * ``` * * @see {@link isLessThan} for strict less than @@ -747,14 +747,14 @@ export const isLessThanOrEqualTo = (O: Order): { * * **Example** (Checking greater-than-or-equal comparisons) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const isGreaterThanOrEqualToNumber = Order.isGreaterThanOrEqualTo(Order.Number) * - * console.log(isGreaterThanOrEqualToNumber(2, 1)) // true - * console.log(isGreaterThanOrEqualToNumber(1, 1)) // true - * console.log(isGreaterThanOrEqualToNumber(1, 2)) // false + * isGreaterThanOrEqualToNumber(2, 1) // => true + * isGreaterThanOrEqualToNumber(1, 1) // => true + * isGreaterThanOrEqualToNumber(1, 2) // => false * ``` * * @see {@link isGreaterThan} for strict greater than @@ -782,14 +782,14 @@ export const isGreaterThanOrEqualTo = (O: Order): { * * **Example** (Selecting the minimum value) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const minNumber = Order.min(Order.Number) * - * console.log(minNumber(1, 2)) // 1 - * console.log(minNumber(2, 1)) // 1 - * console.log(minNumber(1, 1)) // 1 + * minNumber(1, 2) // => 1 + * minNumber(2, 1) // => 1 + * minNumber(1, 1) // => 1 * ``` * * @see {@link max} for the maximum of two values @@ -817,14 +817,14 @@ export const min = (O: Order): { * * **Example** (Selecting the maximum value) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const maxNumber = Order.max(Order.Number) * - * console.log(maxNumber(1, 2)) // 2 - * console.log(maxNumber(2, 1)) // 2 - * console.log(maxNumber(1, 1)) // 1 + * maxNumber(1, 2) // => 2 + * maxNumber(2, 1) // => 2 + * maxNumber(1, 1) // => 1 * ``` * * @see {@link min} for the minimum of two values @@ -854,14 +854,14 @@ export const max = (O: Order): { * * **Example** (Clamping values) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const clamp = Order.clamp(Order.Number)({ minimum: 1, maximum: 5 }) * - * console.log(clamp(3)) // 3 - * console.log(clamp(0)) // 1 - * console.log(clamp(6)) // 5 + * clamp(3) // => 3 + * clamp(0) // => 1 + * clamp(6) // => 5 * ``` * * @see {@link min} for the minimum of two values @@ -905,16 +905,16 @@ export const clamp = (O: Order): { * * **Example** (Checking ranges) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const betweenNumber = Order.isBetween(Order.Number) * - * console.log(betweenNumber(5, { minimum: 1, maximum: 10 })) // true - * console.log(betweenNumber(1, { minimum: 1, maximum: 10 })) // true - * console.log(betweenNumber(10, { minimum: 1, maximum: 10 })) // true - * console.log(betweenNumber(0, { minimum: 1, maximum: 10 })) // false - * console.log(betweenNumber(11, { minimum: 1, maximum: 10 })) // false + * betweenNumber(5, { minimum: 1, maximum: 10 }) // => true + * betweenNumber(1, { minimum: 1, maximum: 10 }) // => true + * betweenNumber(10, { minimum: 1, maximum: 10 }) // => true + * betweenNumber(0, { minimum: 1, maximum: 10 }) // => false + * betweenNumber(11, { minimum: 1, maximum: 10 }) // => false * ``` * * @see {@link clamp} to clamp a value to a range @@ -957,14 +957,14 @@ export const isBetween = (O: Order): { * * **Example** (Creating a Reducer) * - * ```ts + * ```ts import.meta.vitest * import { Order } from "effect" * * const reducer = Order.makeReducer() * const orders = [Order.Number, Order.flip(Order.Number)] * * const combined = reducer.combineAll(orders) - * console.log(combined(1, 2)) // -1 (uses first order) + * combined(1, 2) // => -1 * ``` * * @see {@link combine} to combine two orders diff --git a/.context/effect/packages/effect/src/Ordering.ts b/.context/effect/packages/effect/src/Ordering.ts index 8afcdb4ed..ff93ffca7 100644 --- a/.context/effect/packages/effect/src/Ordering.ts +++ b/.context/effect/packages/effect/src/Ordering.ts @@ -27,7 +27,7 @@ import * as Reducer_ from "./Reducer.ts" * * **Example** (Defining comparison results) * - * ```ts + * ```ts import.meta.vitest * import type { Ordering } from "effect" * * // Custom comparison function @@ -37,9 +37,9 @@ import * as Reducer_ from "./Reducer.ts" * return 0 * } * - * console.log(compareNumbers(5, 10)) // -1 (5 < 10) - * console.log(compareNumbers(10, 5)) // 1 (10 > 5) - * console.log(compareNumbers(5, 5)) // 0 (5 == 5) + * compareNumbers(5, 10) // => -1 + * compareNumbers(10, 5) // => 1 + * compareNumbers(5, 5) // => 0 * * // Using with string comparison * const compareStrings = (a: string, b: string): Ordering.Ordering => { @@ -63,13 +63,13 @@ export type Ordering = -1 | 0 | 1 * * **Example** (Reversing comparison order) * - * ```ts + * ```ts import.meta.vitest * import { Ordering } from "effect" * * // Basic reversal - * console.log(Ordering.reverse(1)) // -1 (greater becomes less) - * console.log(Ordering.reverse(-1)) // 1 (less becomes greater) - * console.log(Ordering.reverse(0)) // 0 (equal stays equal) + * Ordering.reverse(1) // => -1 + * Ordering.reverse(-1) // => 1 + * Ordering.reverse(0) // => 0 * * // Creating descending sort from ascending comparison * const compareNumbers = (a: number, b: number): Ordering.Ordering => @@ -103,9 +103,8 @@ export const reverse = (o: Ordering): Ordering => (o === -1 ? 1 : o === 1 ? -1 : * * **Example** (Pattern matching on orderings) * - * ```ts + * ```ts import.meta.vitest * import { Function, Ordering } from "effect" - * import * as assert from "node:assert" * * const toMessage = Ordering.match({ * onLessThan: Function.constant("less than"), @@ -113,9 +112,9 @@ export const reverse = (o: Ordering): Ordering => (o === -1 ? 1 : o === 1 ? -1 : * onGreaterThan: Function.constant("greater than") * }) * - * assert.deepStrictEqual(toMessage(-1), "less than") - * assert.deepStrictEqual(toMessage(0), "equal") - * assert.deepStrictEqual(toMessage(1), "greater than") + * toMessage(-1) // => "less than" + * toMessage(0) // => "equal" + * toMessage(1) // => "greater than" * ``` * * @category pattern matching diff --git a/.context/effect/packages/effect/src/PartitionedSemaphore.ts b/.context/effect/packages/effect/src/PartitionedSemaphore.ts index b5c2e8ce8..d700e46ee 100644 --- a/.context/effect/packages/effect/src/PartitionedSemaphore.ts +++ b/.context/effect/packages/effect/src/PartitionedSemaphore.ts @@ -193,7 +193,6 @@ export const makeUnsafe = (options: { } const needed = permits - totalPermits - const taken = permits - needed if (totalPermits > 0) { totalPermits = 0 } @@ -228,9 +227,7 @@ export const makeUnsafe = (options: { return Effect.sync(() => { cleanup() waitingPermits -= entry.permits - if (taken > 0) { - releaseUnsafe(taken) - } + releaseUnsafe(permits - entry.permits) }) }) } diff --git a/.context/effect/packages/effect/src/Path.ts b/.context/effect/packages/effect/src/Path.ts index 8acb1fdbe..13195b6c2 100644 --- a/.context/effect/packages/effect/src/Path.ts +++ b/.context/effect/packages/effect/src/Path.ts @@ -47,38 +47,38 @@ export const TypeId = "~effect/platform/Path" * * **Example** (Using path operations) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Path } from "effect" * * const program = Effect.gen(function*() { * const path = yield* Path.Path * - * // Use various path operations - * const joined = path.join("home", "user", "documents") - * const normalized = path.normalize("./path/../to/file.txt") - * const basename = path.basename("/path/to/file.txt") - * const dirname = path.dirname("/path/to/file.txt") - * const extname = path.extname("file.txt") - * const isAbs = path.isAbsolute("/absolute/path") - * const parsed = path.parse("/path/to/file.txt") - * const relative = path.relative("/from/path", "/to/path") - * const resolved = path.resolve("relative", "path") - * - * console.log({ - * joined, - * normalized, - * basename, - * dirname, - * extname, - * isAbs, - * parsed, - * relative, - * resolved - * }) + * return { + * joined: path.join("home", "user", "documents"), + * normalized: path.normalize("./path/../to/file.txt"), + * basename: path.basename("/path/to/file.txt"), + * dirname: path.dirname("/path/to/file.txt"), + * extname: path.extname("file.txt"), + * isAbsolute: path.isAbsolute("/absolute/path"), + * name: path.parse("/path/to/file.txt").name, + * relative: path.relative("/from/path", "/to/path"), + * resolved: path.resolve("/base", "relative", "path") + * } * }) + * + * const result = Effect.runSync(Effect.provide(program, Path.layer)) + * result.joined // => "home/user/documents" + * result.normalized // => "to/file.txt" + * result.basename // => "file.txt" + * result.dirname // => "/path/to" + * result.extname // => ".txt" + * result.isAbsolute // => true + * result.name // => "file" + * result.relative // => "../../to/path" + * result.resolved // => "/base/relative/path" * ``` * - * @category models + * @category services * @since 4.0.0 */ export interface Path { @@ -108,7 +108,7 @@ export interface Path { * * **Example** (Working with parsed paths) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Path } from "effect" * * // Access types and utilities in the Path namespace @@ -127,8 +127,10 @@ export interface Path { * name: "file" * } * - * console.log(parsed, exampleParsed) + * return [parsed.base, exampleParsed.base] * }) + * + * Effect.runSync(Effect.provide(program, Path.layer)) // => ["file.txt", "file.txt"] * ``` * * @since 4.0.0 @@ -150,7 +152,7 @@ export declare namespace Path { * * **Example** (Parsing and formatting paths) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Path } from "effect" * * const program = Effect.gen(function*() { @@ -158,23 +160,19 @@ export declare namespace Path { * * // Parse a path into its components * const parsed = path.parse("/home/user/documents/file.txt") - * console.log(parsed) - * // { - * // root: "/", - * // dir: "/home/user/documents", - * // base: "file.txt", - * // ext: ".txt", - * // name: "file" - * // } - * * // Format a path from its components * const formatted = path.format({ * dir: "/home/user", * name: "newfile", * ext: ".ts" * }) - * console.log(formatted) // "/home/user/newfile.ts" + * return { dir: parsed.dir, base: parsed.base, formatted } * }) + * + * const result = Effect.runSync(Effect.provide(program, Path.layer)) + * result.dir // => "/home/user/documents" + * result.base // => "file.txt" + * result.formatted // => "/home/user/newfile.ts" * ``` * * @category models @@ -198,7 +196,7 @@ export declare namespace Path { * * **Example** (Providing a custom Path service) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Layer, Path } from "effect" * * // Create a custom path implementation @@ -244,12 +242,11 @@ export declare namespace Path { * * const program = Effect.gen(function*() { * const path = yield* Path.Path - * const joined = path.join("home", "user", "file.txt") - * console.log(joined) // "home/user/file.txt" + * return path.join("home", "user", "file.txt") * }) * * // Run with custom path implementation - * const result = Effect.provide(program, customPathLayer) + * Effect.runSync(Effect.provide(program, customPathLayer)) // => "home/user/file.txt" * ``` * * @category services diff --git a/.context/effect/packages/effect/src/Pipeable.ts b/.context/effect/packages/effect/src/Pipeable.ts index e58dfac8a..6f2697e74 100644 --- a/.context/effect/packages/effect/src/Pipeable.ts +++ b/.context/effect/packages/effect/src/Pipeable.ts @@ -26,15 +26,16 @@ * * **Example** (Chaining operations with pipe) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * * // The Pipeable interface allows Effect values to be chained using the pipe method * const program = Effect.succeed(1).pipe( * Effect.map((x) => x + 1), - * Effect.flatMap((x) => Effect.succeed(x * 2)), - * Effect.tap((x) => Effect.log(`Result: ${x}`)) + * Effect.flatMap((x) => Effect.succeed(x * 2)) * ) + * + * Effect.runSync(program) // => 4 * ``` * * @category models @@ -539,7 +540,7 @@ export interface Pipeable { * * **Example** (Implementing a pipe method) * - * ```ts + * ```ts import.meta.vitest * import { Pipeable } from "effect" * * class NumberBox { @@ -554,7 +555,7 @@ export interface Pipeable { * (n) => n + 2, * (n) => n * 3 * ) - * console.log(result) // 21 + * result // => 21 * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/PlatformError.ts b/.context/effect/packages/effect/src/PlatformError.ts index 8b3dd7cea..25ae0dc9e 100644 --- a/.context/effect/packages/effect/src/PlatformError.ts +++ b/.context/effect/packages/effect/src/PlatformError.ts @@ -30,7 +30,7 @@ const TypeId = "~effect/platform/PlatformError" * @see {@link SystemError} for failures reported by the host platform or operating system * @see {@link PlatformError} for the wrapper used by most platform APIs * - * @category models + * @category errors * @since 4.0.0 */ export class BadArgument extends Data.TaggedError("BadArgument")<{ @@ -69,7 +69,7 @@ export class BadArgument extends Data.TaggedError("BadArgument")<{ * @see {@link SystemError} for the error data that carries this tag on its `_tag` field * @see {@link systemError} for creating a `PlatformError` from a system failure with one of these tags * - * @category models + * @category errors * @since 4.0.0 */ export type SystemErrorTag = @@ -103,7 +103,7 @@ export type SystemErrorTag = * @see {@link BadArgument} for platform API failures caused by rejected caller input before an operation runs * @see {@link SystemErrorTag} for the normalized tag values stored in `_tag` * - * @category models + * @category errors * @since 4.0.0 */ export class SystemError extends Data.Error<{ @@ -151,7 +151,7 @@ export class SystemError extends Data.Error<{ * @see {@link badArgument} for creating this wrapper from rejected caller input * @see {@link systemError} for creating this wrapper from a host or operating-system failure * - * @category models + * @category errors * @since 4.0.0 */ export class PlatformError extends Data.TaggedError("PlatformError")<{ diff --git a/.context/effect/packages/effect/src/Pool.ts b/.context/effect/packages/effect/src/Pool.ts index 66d034fc3..5ef6f5ed8 100644 --- a/.context/effect/packages/effect/src/Pool.ts +++ b/.context/effect/packages/effect/src/Pool.ts @@ -184,7 +184,7 @@ export interface Strategy { * * This predicate narrows the input to `Pool`. * - * @category refinements + * @category guards * @since 2.0.0 */ export const isPool = (u: unknown): u is Pool => hasProperty(u, TypeId) @@ -250,7 +250,7 @@ export const make = (options: { * * **Example** (Creating a connection pool) * - * ```ts + * ```ts import.meta.vitest * import { Duration, Effect, Pool } from "effect" * * interface Connection { @@ -277,6 +277,8 @@ export const make = (options: { * (pool) => Effect.flatMap(Pool.get(pool), (connection) => connection.execute("select 1")) * ) * ) + * + * await Effect.runPromise(program) // => ["executed: select 1"] * ``` * * @category constructors diff --git a/.context/effect/packages/effect/src/Predicate.ts b/.context/effect/packages/effect/src/Predicate.ts index b5ac5040c..f66458aaf 100644 --- a/.context/effect/packages/effect/src/Predicate.ts +++ b/.context/effect/packages/effect/src/Predicate.ts @@ -29,12 +29,12 @@ import type { TupleOf, TupleOfAtLeast } from "./Types.ts" * * **Example** (Defining a predicate) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isPositive: Predicate.Predicate = (n) => n > 0 * - * console.log(isPositive(1)) + * isPositive(1) // => true * ``` * * @see {@link Refinement} @@ -62,15 +62,18 @@ export interface Predicate { * * **Example** (Type-level usage) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type P = Predicate.Predicate * type TL = Predicate.PredicateTypeLambda + * + * const witness: P = (value) => value > 0 + * witness(1) // => true * ``` * * @see {@link Predicate} - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface PredicateTypeLambda extends TypeLambda { @@ -93,14 +96,14 @@ export interface PredicateTypeLambda extends TypeLambda { * * **Example** (Narrowing unknown values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isString: Predicate.Refinement = (u): u is string => typeof u === "string" * * const data: unknown = "hello" * if (isString(data)) { - * console.log(data.toUpperCase()) + * data.toUpperCase() // => "HELLO" * } * ``` * @@ -129,11 +132,13 @@ export interface Refinement { * * **Example** (Extracting predicate input) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type IsString = Predicate.Predicate * type Input = Predicate.Predicate.In + * + * const input: Input = "value" * ``` * * @see {@link Predicate} @@ -156,11 +161,13 @@ export declare namespace Predicate { * * **Example** (Inferring the input type) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type P = Predicate.Predicate * type Input = Predicate.Predicate.In

+ * + * const input: Input = 1 * ``` * * @see {@link Predicate.Any} @@ -183,10 +190,13 @@ export declare namespace Predicate { * * **Example** (Using generic constraints) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type AnyPredicate = Predicate.Predicate.Any + * + * const witness: AnyPredicate = () => true + * witness("value") // => true * ``` * * @see {@link Predicate.In} @@ -211,12 +221,14 @@ export declare namespace Predicate { * * **Example** (Extracting refinement types) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type IsString = Predicate.Refinement * type Input = Predicate.Refinement.In * type Output = Predicate.Refinement.Out + * + * const output: Output = "value" * ``` * * @see {@link Refinement} @@ -238,11 +250,13 @@ export declare namespace Refinement { * * **Example** (Inferring the input type) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type R = Predicate.Refinement * type Input = Predicate.Refinement.In + * + * const input: Input = "value" * ``` * * @see {@link Refinement.Out} @@ -267,11 +281,13 @@ export declare namespace Refinement { * * **Example** (Inferring the output type) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type R = Predicate.Refinement * type Output = Predicate.Refinement.Out + * + * const output: Output = "value" * ``` * * @see {@link Refinement.In} @@ -293,10 +309,13 @@ export declare namespace Refinement { * * **Example** (Using generic constraints) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * type AnyRefinement = Predicate.Refinement.Any + * + * const witness: AnyRefinement = (_): _ is string => true + * witness("value") // => true * ``` * * @see {@link Refinement.In} @@ -322,14 +341,14 @@ export declare namespace Refinement { * * **Example** (Checking string length) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isLongerThan2 = Predicate.mapInput((s: string) => s.length)( * (n: number) => n > 2 * ) * - * console.log(isLongerThan2("hello")) + * isLongerThan2("hello") // => true * ``` * * @see {@link Predicate} @@ -358,12 +377,12 @@ export const mapInput: { * * **Example** (Checking exact length) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isPair = Predicate.isTupleOf(2) * - * console.log(isPair([1, 2])) + * isPair([1, 2]) // => true * ``` * * @see {@link isTupleOfAtLeast} @@ -391,12 +410,12 @@ export const isTupleOf: { * * **Example** (Checking minimum length) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const hasAtLeast2 = Predicate.isTupleOfAtLeast(2) * - * console.log(hasAtLeast2([1, 2, 3])) + * hasAtLeast2([1, 2, 3]) // => true * ``` * * @see {@link isTupleOf} @@ -424,18 +443,16 @@ export const isTupleOfAtLeast: { * * **Example** (Filtering truthy values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const values = [0, 1, "", "ok", false] - * const truthy = values.filter(Predicate.isTruthy) - * - * console.log(truthy) + * const truthy = values.filter(Predicate.isTruthy) // => [1, "ok"] * ``` * * @see {@link isNullish} * @see {@link isNotNullish} - * @category guards + * @category predicates * @since 2.0.0 */ export function isTruthy(input: unknown): boolean { @@ -455,13 +472,13 @@ export function isTruthy(input: unknown): boolean { * * **Example** (Guarding a Set) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = new Set([1, 2]) * * if (Predicate.isSet(data)) { - * console.log(data.size) + * data.size // => 2 * } * ``` * @@ -487,13 +504,13 @@ export function isSet(input: unknown): input is Set { * * **Example** (Guarding a Map) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = new Map([["a", 1]]) * * if (Predicate.isMap(data)) { - * console.log(data.size) + * data.size // => 1 * } * ``` * @@ -520,13 +537,13 @@ export function isMap(input: unknown): input is Map { * * **Example** (Guarding strings) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = "hi" * * if (Predicate.isString(data)) { - * console.log(data.toUpperCase()) + * data.toUpperCase() // => "HI" * } * ``` * @@ -554,13 +571,13 @@ export function isString(input: unknown): input is string { * * **Example** (Guarding numbers) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = 42 * * if (Predicate.isNumber(data)) { - * console.log(data + 1) + * data + 1 // => 43 * } * ``` * @@ -587,13 +604,13 @@ export function isNumber(input: unknown): input is number { * * **Example** (Guarding booleans) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = true * * if (Predicate.isBoolean(data)) { - * console.log(data ? "yes" : "no") + * data ? "yes" : "no" // => "yes" * } * ``` * @@ -620,13 +637,13 @@ export function isBoolean(input: unknown): input is boolean { * * **Example** (Guarding bigints) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = 1n * * if (Predicate.isBigInt(data)) { - * console.log(data + 2n) + * data + 2n // => 3n * } * ``` * @@ -652,13 +669,13 @@ export function isBigInt(input: unknown): input is bigint { * * **Example** (Guarding symbols) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = Symbol.for("id") * * if (Predicate.isSymbol(data)) { - * console.log(data.description) + * data.description // => "id" * } * ``` * @@ -684,14 +701,14 @@ export function isSymbol(input: unknown): input is symbol { * * **Example** (Guarding property keys) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const key: unknown = "name" * const obj: Record = { name: "Ada" } * * if (Predicate.isPropertyKey(key) && key in obj) { - * console.log(obj[key]) + * obj[key] // => "Ada" * } * ``` * @@ -719,13 +736,13 @@ export function isPropertyKey(u: unknown): u is PropertyKey { * * **Example** (Guarding functions) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = () => 1 * * if (Predicate.isFunction(data)) { - * console.log(data()) + * data() // => 1 * } * ``` * @@ -751,12 +768,12 @@ export function isFunction(input: unknown): input is Function { * * **Example** (Guarding undefined values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = undefined * - * console.log(Predicate.isUndefined(data)) + * Predicate.isUndefined(data) // => true * ``` * * @see {@link isNotUndefined} @@ -782,13 +799,11 @@ export function isUndefined(input: unknown): input is undefined { * * **Example** (Filtering undefined values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const values = [1, undefined, 2] - * const defined = values.filter(Predicate.isNotUndefined) - * - * console.log(defined) + * const defined = values.filter(Predicate.isNotUndefined) // => [1, 2] * ``` * * @see {@link isUndefined} @@ -813,12 +828,12 @@ export function isNotUndefined(input: A): input is Exclude { * * **Example** (Guarding null values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = null * - * console.log(Predicate.isNull(data)) + * Predicate.isNull(data) // => true * ``` * * @see {@link isNotNull} @@ -844,13 +859,11 @@ export function isNull(input: unknown): input is null { * * **Example** (Filtering null values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const values = [1, null, 2] - * const nonNull = values.filter(Predicate.isNotNull) - * - * console.log(nonNull) + * const nonNull = values.filter(Predicate.isNotNull) // => [1, 2] * ``` * * @see {@link isNull} @@ -875,13 +888,11 @@ export function isNotNull(input: A): input is Exclude { * * **Example** (Guarding nullish values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const values = [0, null, "", undefined] - * const nullish = values.filter(Predicate.isNullish) - * - * console.log(nullish) + * const nullish = values.filter(Predicate.isNullish) // => [null, undefined] * ``` * * @see {@link isNotNullish} @@ -908,13 +919,11 @@ export function isNullish(input: A): input is A & (null | undefined) { * * **Example** (Filtering non-nullish values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const values = [0, null, "", undefined] - * const present = values.filter(Predicate.isNotNullish) - * - * console.log(present) + * const present = values.filter(Predicate.isNotNullish) // => [0, ""] * ``` * * @see {@link isNullish} @@ -936,10 +945,10 @@ export function isNotNullish(input: A): input is NonNullable { * * **Example** (Matching no values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * - * console.log(Predicate.isNever("anything")) + * Predicate.isNever("anything") // => false * ``` * * @see {@link isUnknown} @@ -959,10 +968,10 @@ export function isNever(_: unknown): _ is never { * * **Example** (Matching every value) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * - * console.log(Predicate.isUnknown(123)) + * Predicate.isUnknown(123) // => true * ``` * * @see {@link isNever} @@ -987,10 +996,10 @@ export function isUnknown(_: unknown): _ is unknown { * * **Example** (Checking objects or arrays) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * - * console.log(Predicate.isObjectOrArray([])) + * Predicate.isObjectOrArray([]) // => true * ``` * * @see {@link isObject} @@ -1018,11 +1027,11 @@ export function isObjectOrArray(input: unknown): input is { [x: PropertyKey]: un * * **Example** (Guarding objects) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * - * console.log(Predicate.isObject({ a: 1 })) - * console.log(Predicate.isObject([1, 2])) + * Predicate.isObject({ a: 1 }) // => true + * Predicate.isObject([1, 2]) // => false * ``` * * @see {@link isObjectOrArray} @@ -1051,12 +1060,12 @@ export function isObject(input: unknown): input is { [x: PropertyKey]: unknown } * * **Example** (Checking readonly objects) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = { a: 1 } * - * console.log(Predicate.isReadonlyObject(data)) + * Predicate.isReadonlyObject(data) // => true * ``` * * @see {@link isObject} @@ -1081,11 +1090,11 @@ export function isReadonlyObject(input: unknown): input is { readonly [x: Proper * * **Example** (Checking object keywords) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * - * console.log(Predicate.isObjectKeyword(() => 1)) - * console.log(Predicate.isObjectKeyword(null)) + * Predicate.isObjectKeyword(() => 1) // => true + * Predicate.isObjectKeyword(null) // => false * ``` * * @see {@link isObject} @@ -1112,14 +1121,14 @@ export function isObjectKeyword(input: unknown): input is object { * * **Example** (Guarding object properties) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const hasName = Predicate.hasProperty("name") * const data: unknown = { name: "Ada" } * * if (hasName(data)) { - * console.log(data.name) + * data.name // => "Ada" * } * ``` * @@ -1151,12 +1160,12 @@ export const hasProperty: { * * **Example** (Guarding tagged values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isOk = Predicate.isTagged("Ok") * - * console.log(isOk({ _tag: "Ok", value: 1 })) + * isOk({ _tag: "Ok", value: 1 }) // => true * ``` * * @see {@link hasProperty} @@ -1184,12 +1193,12 @@ export const isTagged: { * * **Example** (Guarding errors) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = new Error("boom") * - * console.log(Predicate.isError(data)) + * Predicate.isError(data) // => true * ``` * * @see {@link isUnknown} @@ -1213,12 +1222,12 @@ export function isError(input: unknown): input is Error { * * **Example** (Guarding Uint8Array values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = new Uint8Array([1, 2]) * - * console.log(Predicate.isUint8Array(data)) + * Predicate.isUint8Array(data) // => true * ``` * * @see {@link isIterable} @@ -1243,12 +1252,12 @@ export function isUint8Array(input: unknown): input is Uint8Array { * * **Example** (Guarding Date values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = new Date() * - * console.log(Predicate.isDate(data)) + * Predicate.isDate(data) // => true * ``` * * @see {@link isRegExp} @@ -1272,12 +1281,12 @@ export function isDate(input: unknown): input is Date { * * **Example** (Guarding iterables) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = [1, 2, 3] * - * console.log(Predicate.isIterable(data)) + * Predicate.isIterable(data) // => true * ``` * * @see {@link isSet} @@ -1302,12 +1311,12 @@ export function isIterable(input: unknown): input is Iterable { * * **Example** (Guarding promises) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = Promise.resolve(1) * - * console.log(Predicate.isPromise(data)) + * Predicate.isPromise(data) // => true * ``` * * @see {@link isPromiseLike} @@ -1332,12 +1341,12 @@ export function isPromise(input: unknown): input is Promise { * * **Example** (Guarding promise-like values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = { then: () => {} } * - * console.log(Predicate.isPromiseLike(data)) + * Predicate.isPromiseLike(data) // => true * ``` * * @see {@link isPromise} @@ -1361,12 +1370,12 @@ export function isPromiseLike(input: unknown): input is PromiseLike { * * **Example** (Guarding RegExp values) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const data: unknown = /abc/ * - * console.log(Predicate.isRegExp(data)) + * Predicate.isRegExp(data) // => true * ``` * * @see {@link isDate} @@ -1392,7 +1401,7 @@ export function isRegExp(input: unknown): input is RegExp { * * **Example** (Composing refinements) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isNumber: Predicate.Refinement = (u): u is number => typeof u === "number" @@ -1400,7 +1409,7 @@ export function isRegExp(input: unknown): input is RegExp { * * const isIntegerNumber = Predicate.compose(isNumber, isInteger) * - * console.log(isIntegerNumber(1)) + * isIntegerNumber(1) // => true * ``` * * @see {@link and} @@ -1434,12 +1443,12 @@ export const compose: { * * **Example** (Checking tuples) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const tupleCheck = Predicate.Tuple([(n: number) => n > 0, Predicate.isString]) * - * console.log(tupleCheck([1, "ok"])) + * tupleCheck([1, "ok"]) // => true * ``` * * @see {@link Struct} @@ -1480,7 +1489,7 @@ export function Tuple>( * * **Example** (Checking structs) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const userCheck = Predicate.Struct({ @@ -1488,7 +1497,7 @@ export function Tuple>( * name: Predicate.isString * }) * - * console.log(userCheck({ id: 1, name: "Ada" })) + * userCheck({ id: 1, name: "Ada" }) // => true * ``` * * @see {@link Tuple} @@ -1528,12 +1537,12 @@ export function Struct>( * * **Example** (Negating a predicate) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isNotString = Predicate.not(Predicate.isString) * - * console.log(isNotString(1)) + * isNotString(1) // => true * ``` * * @see {@link and} @@ -1561,12 +1570,12 @@ export function not(self: Predicate): Predicate { * * **Example** (Checking either condition) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isStringOrNumber = Predicate.or(Predicate.isString, Predicate.isNumber) * - * console.log(isStringOrNumber("a")) + * isStringOrNumber("a") // => true * ``` * * @see {@link and} @@ -1597,7 +1606,7 @@ export const or: { * * **Example** (Checking both conditions) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const hasAAndB = Predicate.and( @@ -1610,6 +1619,8 @@ export const or: { * // input has both properties at this point * const a = input.a * const b = input.b + * + * const values = [a, b] // => [1, "ok"] * } * ``` * @@ -1638,14 +1649,14 @@ export const and: { * * **Example** (Checking exclusive-or conditions) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isEven = (n: number) => n % 2 === 0 * const isPositive = (n: number) => n > 0 * const either = Predicate.xor(isEven, isPositive) * - * console.log(either(-2)) + * either(-2) // => true * ``` * * @see {@link or} @@ -1671,13 +1682,13 @@ export const xor: { * * **Example** (Defining equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isEven = (n: number) => n % 2 === 0 * const same = Predicate.eqv(isEven, isEven) * - * console.log(same(3)) + * same(3) // => true * ``` * * @see {@link xor} @@ -1704,14 +1715,14 @@ export const eqv: { * * **Example** (Checking implication) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const isAdult = (age: number) => age >= 18 * const canVote = (age: number) => age >= 18 * const implies = Predicate.implies(isAdult, canVote) * - * console.log(implies(16)) + * implies(16) // => true * ``` * * @see {@link and} @@ -1740,12 +1751,12 @@ export const implies: { * * **Example** (Checking NOR conditions) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const neither = Predicate.nor(Predicate.isString, Predicate.isNumber) * - * console.log(neither(true)) + * neither(true) // => true * ``` * * @see {@link or} @@ -1774,12 +1785,12 @@ export const nor: { * * **Example** (Checking NAND conditions) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const notBoth = Predicate.nand(Predicate.isString, Predicate.isNumber) * - * console.log(notBoth("a")) + * notBoth("a") // => true * ``` * * @see {@link and} @@ -1809,17 +1820,17 @@ export const nand: { * * **Example** (Checking all predicates) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const allChecks = Predicate.every([Predicate.isNumber, (n: number) => n > 0]) * - * console.log(allChecks(2)) + * allChecks(2) // => true * ``` * * @see {@link some} * @see {@link and} - * @category elements + * @category combining * @since 2.0.0 */ export function every(collection: Iterable>): Predicate { @@ -1847,17 +1858,17 @@ export function every(collection: Iterable>): Predicate { * * **Example** (Checking any predicate) * - * ```ts + * ```ts import.meta.vitest * import { Predicate } from "effect" * * const anyCheck = Predicate.some([Predicate.isString, Predicate.isNumber]) * - * console.log(anyCheck("ok")) + * anyCheck("ok") // => true * ``` * * @see {@link every} * @see {@link or} - * @category elements + * @category combining * @since 2.0.0 */ export function some(collection: Iterable>): Predicate { diff --git a/.context/effect/packages/effect/src/PrimaryKey.ts b/.context/effect/packages/effect/src/PrimaryKey.ts index 8353ad9df..ed76a3bc2 100644 --- a/.context/effect/packages/effect/src/PrimaryKey.ts +++ b/.context/effect/packages/effect/src/PrimaryKey.ts @@ -41,7 +41,7 @@ export const symbol = "~effect/interfaces/PrimaryKey" * * **Example** (Implementing a primary key) * - * ```ts + * ```ts import.meta.vitest * import { PrimaryKey } from "effect" * * class ProductId implements PrimaryKey.PrimaryKey { @@ -53,7 +53,7 @@ export const symbol = "~effect/interfaces/PrimaryKey" * } * * const productId = new ProductId("electronics", 42) - * console.log(PrimaryKey.value(productId)) // "electronics-42" + * PrimaryKey.value(productId) // => "electronics-42" * ``` * * @category models @@ -81,7 +81,7 @@ export interface PrimaryKey { * @see {@link PrimaryKey} for the protocol being checked * @see {@link value} for extracting the string value after narrowing * - * @category models + * @category guards * @since 4.0.0 */ export const isPrimaryKey = (u: unknown): u is PrimaryKey => hasProperty(u, symbol) @@ -96,7 +96,7 @@ export const isPrimaryKey = (u: unknown): u is PrimaryKey => hasProperty(u, symb * * **Example** (Reading primary key values) * - * ```ts + * ```ts import.meta.vitest * import { PrimaryKey } from "effect" * * class OrderId implements PrimaryKey.PrimaryKey { @@ -108,16 +108,16 @@ export const isPrimaryKey = (u: unknown): u is PrimaryKey => hasProperty(u, symb * } * * const orderId = new OrderId(1640995200000, 1) - * console.log(PrimaryKey.value(orderId)) // "order_1640995200000_1" + * PrimaryKey.value(orderId) // => "order_1640995200000_1" * * // Can also be used with simple string-based implementations * const simpleKey = { * [PrimaryKey.symbol]: () => "simple-key-123" * } - * console.log(PrimaryKey.value(simpleKey)) // "simple-key-123" + * PrimaryKey.value(simpleKey) // => "simple-key-123" * ``` * - * @category accessors + * @category getters * @since 2.0.0 */ export const value = (self: PrimaryKey): string => self[symbol]() diff --git a/.context/effect/packages/effect/src/PubSub.ts b/.context/effect/packages/effect/src/PubSub.ts index dcbfbf154..740003636 100644 --- a/.context/effect/packages/effect/src/PubSub.ts +++ b/.context/effect/packages/effect/src/PubSub.ts @@ -35,26 +35,27 @@ const TypeId = "~effect/PubSub" * * **Example** (Publishing and subscribing to messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * // Create a bounded PubSub with capacity 10 * const pubsub = yield* PubSub.bounded(10) * * // Subscribe and consume messages - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish messages - * yield* PubSub.publish(pubsub, "Hello") - * yield* PubSub.publish(pubsub, "World") + * // Publish messages + * yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.publish(pubsub, "World") * - * const message1 = yield* PubSub.take(subscription) - * const message2 = yield* PubSub.take(subscription) - * console.log(message1, message2) // "Hello", "World" - * })) - * }) + * const message1 = yield* PubSub.take(subscription) + * const message2 = yield* PubSub.take(subscription) + * return [message1, message2] + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => ["Hello", "World"] * ``` * * @category models @@ -139,6 +140,7 @@ export declare namespace PubSub { take(): A | undefined takeN(n: number): Array takeAll(): Array + close(): void readonly remaining: number } @@ -204,31 +206,28 @@ const SubscriptionTypeId = "~effect/PubSub/Subscription" * * **Example** (Taking messages from a subscription) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Subscribe within a scope for automatic cleanup - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription: PubSub.Subscription = yield* PubSub.subscribe( - * pubsub - * ) - * - * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) - * - * // Take individual messages - * const message = yield* PubSub.take(subscription) - * console.log(message) // "msg1" - * - * // Take multiple messages - * const messages = yield* PubSub.takeUpTo(subscription, 1) - * console.log(messages) // ["msg2"] - * const allMessages = yield* PubSub.takeAll(subscription) - * console.log(allMessages) // ["msg3"] - * })) - * }) + * const subscription: PubSub.Subscription = yield* PubSub.subscribe(pubsub) + * + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * + * // Take individual messages + * const message = yield* PubSub.take(subscription) + * + * // Take multiple messages + * const messages = yield* PubSub.takeUpTo(subscription, 1) + * const allMessages = yield* PubSub.takeAll(subscription) + * return { message, messages, allMessages } + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => { message: "msg1", messages: ["msg2"], allMessages: ["msg3"] } * ``` * * @category models @@ -253,7 +252,7 @@ export interface Subscription extends Pipeable { * * **Example** (Creating a PubSub with a custom strategy) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { @@ -264,8 +263,13 @@ export interface Subscription extends Pipeable { * }) * * // Use the created PubSub - * yield* PubSub.publish(pubsub, "Hello") + * const published = yield* PubSub.publish(pubsub, "Hello") + * yield* PubSub.shutdown(pubsub) + * return published * }) + * + * const actual = await Effect.runPromise(program) + * actual // => true * ``` * * @category constructors @@ -301,7 +305,7 @@ export const make = ( * * **Example** (Creating a bounded PubSub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { @@ -313,7 +317,15 @@ export const make = ( * capacity: 100, * replay: 10 // Last 10 messages replayed to new subscribers * }) + * + * const capacities = [PubSub.capacity(pubsub), PubSub.capacity(pubsubWithReplay)] + * yield* PubSub.shutdown(pubsub) + * yield* PubSub.shutdown(pubsubWithReplay) + * return capacities * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [100, 100] * ``` * * @category constructors @@ -340,33 +352,27 @@ export const bounded = ( * * **Example** (Dropping messages when full) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * // Create dropping PubSub that drops new messages when full * const pubsub = yield* PubSub.dropping(3) * - * // With replay buffer for late subscribers - * const pubsubWithReplay = yield* PubSub.dropping({ - * capacity: 3, - * replay: 5 - * }) + * const subscription = yield* PubSub.subscribe(pubsub) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * // Fill the PubSub and see dropping behavior + * yield* PubSub.publish(pubsub, "msg1") // succeeds + * yield* PubSub.publish(pubsub, "msg2") // succeeds + * yield* PubSub.publish(pubsub, "msg3") // succeeds + * const dropped = yield* PubSub.publish(pubsub, "msg4") // returns false (dropped) * - * // Fill the PubSub and see dropping behavior - * yield* PubSub.publish(pubsub, "msg1") // succeeds - * yield* PubSub.publish(pubsub, "msg2") // succeeds - * yield* PubSub.publish(pubsub, "msg3") // succeeds - * const dropped = yield* PubSub.publish(pubsub, "msg4") // returns false (dropped) - * console.log("Message dropped:", !dropped) // true + * const messages = yield* PubSub.takeAll(subscription) + * return { dropped: !dropped, messages } + * })) * - * const messages = yield* PubSub.takeAll(subscription) - * console.log(messages) // ["msg1", "msg2", "msg3"] - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { dropped: true, messages: ["msg1", "msg2", "msg3"] } * ``` * * @category constructors @@ -393,32 +399,26 @@ export const dropping = ( * * **Example** (Sliding old messages when full) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * // Create sliding PubSub that evicts old messages when full * const pubsub = yield* PubSub.sliding(3) * - * // With replay buffer - * const pubsubWithReplay = yield* PubSub.sliding({ - * capacity: 3, - * replay: 2 - * }) + * const subscription = yield* PubSub.subscribe(pubsub) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * // Fill and overflow the PubSub + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") + * yield* PubSub.publish(pubsub, "msg3") + * yield* PubSub.publish(pubsub, "msg4") // "msg1" is evicted * - * // Fill and overflow the PubSub - * yield* PubSub.publish(pubsub, "msg1") - * yield* PubSub.publish(pubsub, "msg2") - * yield* PubSub.publish(pubsub, "msg3") - * yield* PubSub.publish(pubsub, "msg4") // "msg1" is evicted + * return yield* PubSub.takeAll(subscription) + * })) * - * const messages = yield* PubSub.takeAll(subscription) - * console.log(messages) // ["msg2", "msg3", "msg4"] - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => ["msg2", "msg3", "msg4"] * ``` * * @category constructors @@ -440,30 +440,25 @@ export const sliding = ( * * **Example** (Creating an unbounded PubSub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * // Create unbounded PubSub * const pubsub = yield* PubSub.unbounded() * - * // With replay buffer for late subscribers - * const pubsubWithReplay = yield* PubSub.unbounded({ - * replay: 10 - * }) + * const subscription = yield* PubSub.subscribe(pubsub) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * // Can publish unlimited messages + * for (let i = 0; i < 3; i++) { + * yield* PubSub.publish(pubsub, `message-${i}`) + * } * - * // Can publish unlimited messages - * for (let i = 0; i < 3; i++) { - * yield* PubSub.publish(pubsub, `message-${i}`) - * } + * return yield* PubSub.takeAll(subscription) + * })) * - * const message = yield* PubSub.take(subscription) - * console.log("First message:", message) // "message-0" - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => ["message-0", "message-1", "message-2"] * ``` * * @category constructors @@ -552,18 +547,17 @@ export const makeAtomicUnbounded = (options?: { * * **Example** (Getting PubSub capacity) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(100) - * const cap = PubSub.capacity(pubsub) - * console.log("PubSub capacity:", cap) // 100 - * * const unboundedPubsub = yield* PubSub.unbounded() - * const unboundedCap = PubSub.capacity(unboundedPubsub) - * console.log("Unbounded capacity:", unboundedCap) // Number.MAX_SAFE_INTEGER + * return [PubSub.capacity(pubsub), PubSub.capacity(unboundedPubsub)] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [100, Number.MAX_SAFE_INTEGER] * ``` * * @category getters @@ -582,29 +576,28 @@ export const capacity = (self: PubSub): number => self.pubsub.capacity * * **Example** (Getting PubSub size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Initially empty * const initialSize = yield* PubSub.size(pubsub) - * console.log("Initial size:", initialSize) // 0 * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish some messages for the active subscription - * yield* PubSub.publish(pubsub, "msg1") - * yield* PubSub.publish(pubsub, "msg2") + * // Publish some messages for the active subscription + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") * - * const afterPublish = yield* PubSub.size(pubsub) - * console.log("After publishing:", afterPublish) // 2 + * const afterPublish = yield* PubSub.size(pubsub) + * const messages = yield* PubSub.takeAll(subscription) + * return { initialSize, afterPublish, messages } + * })) * - * yield* PubSub.takeAll(subscription) - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { initialSize: 0, afterPublish: 2, messages: ["msg1", "msg2"] } * ``` * * @category getters @@ -627,14 +620,16 @@ export const size = (self: PubSub): Effect.Effect => Effect.sync(( * * **Example** (Reading size synchronously) * - * ```ts - * import { PubSub } from "effect" + * ```ts import.meta.vitest + * import { Effect, PubSub } from "effect" * - * // Unsafe synchronous size check - * declare const pubsub: PubSub.PubSub + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(2) + * return PubSub.sizeUnsafe(pubsub) + * }) * - * const size = PubSub.sizeUnsafe(pubsub) - * console.log("Current size:", size) + * const actual = await Effect.runPromise(program) + * actual // => 0 * ``` * * @category getters @@ -656,29 +651,28 @@ export const sizeUnsafe = (self: PubSub): number => { * * **Example** (Checking whether a PubSub is full) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(2) * * // Initially not full * const initiallyFull = yield* PubSub.isFull(pubsub) - * console.log("Initially full:", initiallyFull) // false * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Fill the PubSub for the active subscription - * yield* PubSub.publish(pubsub, "msg1") - * yield* PubSub.publish(pubsub, "msg2") + * // Fill the PubSub for the active subscription + * yield* PubSub.publish(pubsub, "msg1") + * yield* PubSub.publish(pubsub, "msg2") * - * const nowFull = yield* PubSub.isFull(pubsub) - * console.log("Now full:", nowFull) // true + * const nowFull = yield* PubSub.isFull(pubsub) + * const messages = yield* PubSub.takeAll(subscription) + * return { initiallyFull, nowFull, messages } + * })) * - * yield* PubSub.takeAll(subscription) - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { initiallyFull: false, nowFull: true, messages: ["msg1", "msg2"] } * ``` * * @category predicates @@ -692,28 +686,27 @@ export const isFull = (self: PubSub): Effect.Effect => * * **Example** (Checking whether a PubSub is empty) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Initially empty * const initiallyEmpty = yield* PubSub.isEmpty(pubsub) - * console.log("Initially empty:", initiallyEmpty) // true * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish a message for the active subscription - * yield* PubSub.publish(pubsub, "Hello") + * // Publish a message for the active subscription + * yield* PubSub.publish(pubsub, "Hello") * - * const nowEmpty = yield* PubSub.isEmpty(pubsub) - * console.log("Now empty:", nowEmpty) // false + * const nowEmpty = yield* PubSub.isEmpty(pubsub) + * const message = yield* PubSub.take(subscription) + * return { initiallyEmpty, nowEmpty, message } + * })) * - * yield* PubSub.take(subscription) - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { initiallyEmpty: true, nowEmpty: false, message: "Hello" } * ``` * * @category predicates @@ -733,7 +726,7 @@ export const isEmpty = (self: PubSub): Effect.Effect => Effect.ma * * **Example** (Shutting down a PubSub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { @@ -743,12 +736,14 @@ export const isEmpty = (self: PubSub): Effect.Effect => Effect.ma * yield* PubSub.shutdown(pubsub) * * const isShutdown = yield* PubSub.isShutdown(pubsub) - * console.log("Is shutdown:", isShutdown) // true * * // Publishing after shutdown returns false * const published = yield* PubSub.publish(pubsub, "msg1") - * console.log("Published after shutdown:", published) // false + * return { isShutdown, published } * }) + * + * const actual = await Effect.runPromise(program) + * actual // => { isShutdown: true, published: false } * ``` * * @category lifecycle @@ -770,7 +765,7 @@ export const shutdown = (self: PubSub): Effect.Effect => * * **Example** (Checking whether a PubSub is shut down) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { @@ -778,14 +773,16 @@ export const shutdown = (self: PubSub): Effect.Effect => * * // Initially not shutdown * const initiallyShutdown = yield* PubSub.isShutdown(pubsub) - * console.log("Initially shutdown:", initiallyShutdown) // false * * // Shutdown the PubSub * yield* PubSub.shutdown(pubsub) * * const nowShutdown = yield* PubSub.isShutdown(pubsub) - * console.log("Now shutdown:", nowShutdown) // true + * return [initiallyShutdown, nowShutdown] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [false, true] * ``` * * @category predicates @@ -804,18 +801,18 @@ export const isShutdown = (self: PubSub): Effect.Effect => Effect * * **Example** (Checking shutdown synchronously) * - * ```ts - * import { PubSub } from "effect" + * ```ts import.meta.vitest + * import { Effect, PubSub } from "effect" * - * declare const pubsub: PubSub.PubSub + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(2) + * const initiallyShutdown = PubSub.isShutdownUnsafe(pubsub) + * yield* PubSub.shutdown(pubsub) + * return [initiallyShutdown, PubSub.isShutdownUnsafe(pubsub)] + * }) * - * // Unsafe synchronous shutdown check - * const isDown = PubSub.isShutdownUnsafe(pubsub) - * if (isDown) { - * console.log("PubSub is shutdown, cannot publish") - * } else { - * console.log("PubSub is active") - * } + * const actual = await Effect.runPromise(program) + * actual // => [false, true] * ``` * * @category predicates @@ -830,7 +827,7 @@ export const isShutdownUnsafe = (self: PubSub): boolean => self.shutdownFl * * **Example** (Waiting for shutdown) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, PubSub } from "effect" * * const program = Effect.gen(function*() { @@ -840,19 +837,19 @@ export const isShutdownUnsafe = (self: PubSub): boolean => self.shutdownFl * const waiterFiber = yield* Effect.forkChild( * Effect.gen(function*() { * yield* PubSub.awaitShutdown(pubsub) - * console.log("PubSub has been shutdown!") + * return "PubSub has been shutdown!" * }) * ) * - * // Do some work... - * yield* Effect.sleep("100 millis") - * * // Shutdown the PubSub * yield* PubSub.shutdown(pubsub) * * // The waiter will now complete - * yield* Fiber.join(waiterFiber) + * return yield* Fiber.join(waiterFiber) * }) + * + * const actual = await Effect.runPromise(program) + * actual // => "PubSub has been shutdown!" * ``` * * @category lifecycle @@ -877,24 +874,24 @@ export const awaitShutdown = (self: PubSub): Effect.Effect => self.s * * **Example** (Publishing a message) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Publish a message * const published = yield* PubSub.publish(pubsub, "Hello World") - * console.log("Message published:", published) // true * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * yield* PubSub.publish(pubsub, "Hello") - * const message = yield* PubSub.take(subscription) - * console.log("Received:", message) // "Hello" - * })) - * }) + * yield* PubSub.publish(pubsub, "Hello") + * const message = yield* PubSub.take(subscription) + * return { published, message } + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => { published: true, message: "Hello" } * ``` * * @see {@link publishUnsafe} for a synchronous non-blocking attempt that does not run effectful surplus handling @@ -941,24 +938,16 @@ export const publish: { * * **Example** (Publishing without suspending) * - * ```ts - * import { PubSub } from "effect" - * - * declare const pubsub: PubSub.PubSub + * ```ts import.meta.vitest + * import { Effect, PubSub } from "effect" * - * // Unsafe synchronous publish (non-blocking) - * const published = PubSub.publishUnsafe(pubsub, "Hello") - * if (published) { - * console.log("Message published successfully") - * } else { - * console.log("Message dropped (PubSub full or shutdown)") - * } + * const program = Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(2) + * return PubSub.publishUnsafe(pubsub, "Hello") + * }) * - * // Useful for scenarios where you don't want to suspend - * const messages = ["msg1", "msg2", "msg3"] - * const publishedCount = - * messages.filter((msg) => PubSub.publishUnsafe(pubsub, msg)).length - * console.log(`Published ${publishedCount} out of ${messages.length} messages`) + * const actual = await Effect.runPromise(program) + * actual // => true * ``` * * @see {@link publish} for effectful publishing that honors the configured surplus strategy @@ -984,37 +973,30 @@ export const publishUnsafe: { * * **Example** (Publishing multiple messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Publish multiple messages at once - * const messages = ["Hello", "World", "from", "Effect"] - * const allPublished = yield* PubSub.publishAll(pubsub, messages) - * console.log("All messages published:", allPublished) // true + * const allPublished = yield* PubSub.publishAll(pubsub, ["Hello", "World", "from", "Effect"]) * * // With a smaller capacity and an active subscription * const smallPubsub = yield* PubSub.bounded(2) - * const manyMessages = ["msg1", "msg2", "msg3", "msg4"] - * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(smallPubsub) - * - * // Will suspend until space becomes available for all messages - * const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, manyMessages)) + * const subscription = yield* PubSub.subscribe(smallPubsub) * - * const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2) - * console.log("First batch:", firstBatch) // ["msg1", "msg2"] + * // Will suspend until space becomes available for all messages + * const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, ["msg1", "msg2", "msg3", "msg4"])) * - * const result = yield* Fiber.join(fiber) - * console.log("All messages eventually published:", result) // true + * const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2) + * const result = yield* Fiber.join(fiber) + * const secondBatch = yield* PubSub.takeAll(subscription) + * return { allPublished, firstBatch, result, secondBatch } + * })) * - * const secondBatch = yield* PubSub.takeAll(subscription) - * console.log("Second batch:", secondBatch) // ["msg3", "msg4"] - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { allPublished: true, firstBatch: ["msg1", "msg2"], result: true, secondBatch: ["msg3", "msg4"] } * ``` * * @category publishing @@ -1048,14 +1030,14 @@ export const publishAll: { * * **Example** (Subscribing to messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * * const program = Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * * // Subscribe within a scope for automatic cleanup - * yield* Effect.scoped(Effect.gen(function*() { + * const first = yield* Effect.scoped(Effect.gen(function*() { * const subscription = yield* PubSub.subscribe(pubsub) * * // Publish some messages @@ -1065,25 +1047,28 @@ export const publishAll: { * // Take messages one by one * const msg1 = yield* PubSub.take(subscription) * const msg2 = yield* PubSub.take(subscription) - * console.log(msg1, msg2) // "Hello", "World" * * // Subscription is automatically cleaned up when scope exits + * return [msg1, msg2] * })) * - * yield* Effect.scoped(Effect.gen(function*() { + * const second = yield* Effect.scoped(Effect.gen(function*() { * const sub1 = yield* PubSub.subscribe(pubsub) * const sub2 = yield* PubSub.subscribe(pubsub) * * // Multiple subscribers can receive the same messages * yield* PubSub.publish(pubsub, "Broadcast") * - * const [msg1, msg2] = yield* Effect.all([ + * return yield* Effect.all([ * PubSub.take(sub1), * PubSub.take(sub2) * ]) - * console.log("Both received:", msg1, msg2) // "Broadcast", "Broadcast" * })) + * return [first, second] * }) + * + * const actual = await Effect.runPromise(program) + * actual // => [["Hello", "World"], ["Broadcast", "Broadcast"]] * ``` * * @category subscriptions @@ -1115,6 +1100,7 @@ const unsubscribe = (self: Subscription): Effect.Effect => Effect.sync(() => { self.subscribers.delete(self.subscription) self.subscription.unsubscribe() + self.replayWindow.close() self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers) }) ), @@ -1130,28 +1116,26 @@ const unsubscribe = (self: Subscription): Effect.Effect => * * **Example** (Taking a message) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Start a fiber to take a message (will suspend) - * const takeFiber = yield* Effect.forkChild( - * PubSub.take(subscription) - * ) + * // Start a fiber to take a message (will suspend) + * const takeFiber = yield* Effect.forkChild(PubSub.take(subscription)) * - * // Publish a message - * yield* PubSub.publish(pubsub, "Hello") + * // Publish a message + * yield* PubSub.publish(pubsub, "Hello") * - * // The take will now complete - * const message = yield* Fiber.join(takeFiber) - * console.log("Received:", message) // "Hello" - * })) - * }) + * // The take will now complete + * return yield* Fiber.join(takeFiber) + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => "Hello" * ``` * * @category subscriptions @@ -1183,23 +1167,23 @@ export const take = (self: Subscription): Effect.Effect => * * **Example** (Taking all available messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish multiple messages - * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * // Publish multiple messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) * - * // Take all available messages at once - * const allMessages = yield* PubSub.takeAll(subscription) - * console.log("All messages:", allMessages) // ["msg1", "msg2", "msg3"] - * })) - * }) + * // Take all available messages at once + * return yield* PubSub.takeAll(subscription) + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => ["msg1", "msg2", "msg3"] * ``` * * @category subscriptions @@ -1254,31 +1238,30 @@ const pollForItem = (self: Subscription) => { * * **Example** (Taking up to a maximum number of messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish multiple messages - * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"]) + * // Publish multiple messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"]) * - * // Take up to 3 messages - * const upTo3 = yield* PubSub.takeUpTo(subscription, 3) - * console.log("Up to 3:", upTo3) // ["msg1", "msg2", "msg3"] + * // Take up to 3 messages + * const upTo3 = yield* PubSub.takeUpTo(subscription, 3) * - * // Take up to 5 more (only 2 remaining) - * const upTo5 = yield* PubSub.takeUpTo(subscription, 5) - * console.log("Up to 5:", upTo5) // ["msg4", "msg5"] + * // Take up to 5 more (only 2 remaining) + * const upTo5 = yield* PubSub.takeUpTo(subscription, 5) * - * // No more messages available - * const noMore = yield* PubSub.takeUpTo(subscription, 10) - * console.log("No more:", noMore) // [] - * })) - * }) + * // No more messages available + * const noMore = yield* PubSub.takeUpTo(subscription, 10) + * return [upTo3, upTo5, noMore] + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => [["msg1", "msg2", "msg3"], ["msg4", "msg5"], []] * ``` * * @category subscriptions @@ -1310,28 +1293,26 @@ export const takeUpTo: { * * **Example** (Taking between a minimum and maximum) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Fiber, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Start taking between 2 and 5 messages (will suspend) - * const takeFiber = yield* Effect.forkChild( - * PubSub.takeBetween(subscription, 2, 5) - * ) + * // Start taking between 2 and 5 messages (will suspend) + * const takeFiber = yield* Effect.forkChild(PubSub.takeBetween(subscription, 2, 5)) * - * // Publish 3 messages - * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * // Publish 3 messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) * - * // Now the take will complete with 3 messages - * const messages = yield* Fiber.join(takeFiber) - * console.log("Between 2-5:", messages) // ["msg1", "msg2", "msg3"] - * })) - * }) + * // Now the take will complete with 3 messages + * return yield* Fiber.join(takeFiber) + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => ["msg1", "msg2", "msg3"] * ``` * * @category subscriptions @@ -1395,29 +1376,29 @@ const takeRemainderLoop = ( * * **Example** (Checking remaining messages) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { + * const program = Effect.scoped(Effect.gen(function*() { * const pubsub = yield* PubSub.bounded(10) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish some messages - * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) + * // Publish some messages + * yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"]) * - * // Check how many messages are available - * const count = yield* PubSub.remaining(subscription) - * console.log("Messages available:", count) // 3 + * // Check how many messages are available + * const count = yield* PubSub.remaining(subscription) * - * // Take one message - * yield* PubSub.take(subscription) + * // Take one message + * const message = yield* PubSub.take(subscription) * - * const remaining = yield* PubSub.remaining(subscription) - * console.log("Messages remaining:", remaining) // 2 - * })) - * }) + * const remaining = yield* PubSub.remaining(subscription) + * return { count, message, remaining } + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => { count: 3, message: "msg1", remaining: 2 } * ``` * * @see {@link remainingUnsafe} for a synchronous check that reports shutdown as `Option.none()` @@ -1443,23 +1424,17 @@ export const remaining = (self: Subscription): Effect.Effect => * * **Example** (Checking remaining messages synchronously) * - * ```ts - * import { PubSub } from "effect" - * - * declare const subscription: PubSub.Subscription + * ```ts import.meta.vitest + * import { Effect, Option, PubSub } from "effect" * - * // Unsafe synchronous check for remaining messages - * const remainingOption = PubSub.remainingUnsafe(subscription) - * if (remainingOption._tag === "Some") { - * console.log("Messages available:", remainingOption.value) - * } else { - * console.log("Subscription is shutdown") - * } + * const program = Effect.scoped(Effect.gen(function*() { + * const pubsub = yield* PubSub.bounded(2) + * const subscription = yield* PubSub.subscribe(pubsub) + * return PubSub.remainingUnsafe(subscription) + * })) * - * // Useful for polling or batching scenarios - * if (remainingOption._tag === "Some" && remainingOption.value > 10) { - * // Process messages in batch - * } + * const actual = await Effect.runPromise(program) + * actual // => Option.some(0) * ``` * * @see {@link remaining} for the effectful variant that interrupts on shutdown @@ -1526,6 +1501,7 @@ const makeSubscriptionUnsafe = ( class BoundedPubSubArb implements PubSub.Atomic { array: Array + replayIndices: Array publisherIndex = 0 subscribers: Array subscriberCount = 0 @@ -1538,6 +1514,7 @@ class BoundedPubSubArb implements PubSub.Atomic { this.capacity = capacity this.replayBuffer = replayBuffer this.array = Array.from({ length: capacity }) + this.replayIndices = replayBuffer ? Array.from({ length: capacity }) : [] this.subscribers = Array.from({ length: capacity }) } @@ -1561,15 +1538,16 @@ class BoundedPubSubArb implements PubSub.Atomic { if (this.isFull()) { return false } + const replayIndex = this.replayBuffer?.offer(value) if (this.subscriberCount !== 0) { const index = this.publisherIndex % this.capacity this.array[index] = value + if (replayIndex !== undefined) { + this.replayIndices[index] = replayIndex + } this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 } - if (this.replayBuffer) { - this.replayBuffer.offer(value) - } return true } @@ -1594,11 +1572,12 @@ class BoundedPubSubArb implements PubSub.Atomic { const a = chunk[iteratorIndex++] const index = this.publisherIndex % this.capacity this.array[index] = a + const replayIndex = this.replayBuffer?.offer(a) + if (replayIndex !== undefined) { + this.replayIndices[index] = replayIndex + } this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 - if (this.replayBuffer) { - this.replayBuffer.offer(a) - } } return chunk.slice(iteratorIndex) } @@ -1606,12 +1585,11 @@ class BoundedPubSubArb implements PubSub.Atomic { slide(): void { if (this.subscribersIndex !== this.publisherIndex) { const index = this.subscribersIndex % this.capacity + const value = this.array[index] this.array[index] = AbsentValue as unknown as A this.subscribers[index] = 0 this.subscribersIndex += 1 - } - if (this.replayBuffer) { - this.replayBuffer.slide() + this.replayBuffer?.slide(value, this.replayIndices[index]) } } @@ -1717,6 +1695,7 @@ class BoundedPubSubArbSubscription implements PubSub.BackingSubscripti class BoundedPubSubPow2 implements PubSub.Atomic { array: Array + replayIndices: Array mask: number publisherIndex = 0 subscribers: Array @@ -1730,6 +1709,7 @@ class BoundedPubSubPow2 implements PubSub.Atomic { this.capacity = capacity this.replayBuffer = replayBuffer this.array = Array.from({ length: capacity }) + this.replayIndices = replayBuffer ? Array.from({ length: capacity }) : [] this.mask = capacity - 1 this.subscribers = Array.from({ length: capacity }) } @@ -1754,15 +1734,16 @@ class BoundedPubSubPow2 implements PubSub.Atomic { if (this.isFull()) { return false } + const replayIndex = this.replayBuffer?.offer(value) if (this.subscriberCount !== 0) { const index = this.publisherIndex & this.mask this.array[index] = value + if (replayIndex !== undefined) { + this.replayIndices[index] = replayIndex + } this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 } - if (this.replayBuffer) { - this.replayBuffer.offer(value) - } return true } @@ -1787,11 +1768,12 @@ class BoundedPubSubPow2 implements PubSub.Atomic { const elem = chunk[iteratorIndex++] const index = this.publisherIndex & this.mask this.array[index] = elem + const replayIndex = this.replayBuffer?.offer(elem) + if (replayIndex !== undefined) { + this.replayIndices[index] = replayIndex + } this.subscribers[index] = this.subscriberCount this.publisherIndex += 1 - if (this.replayBuffer) { - this.replayBuffer.offer(elem) - } } return chunk.slice(iteratorIndex) } @@ -1799,12 +1781,11 @@ class BoundedPubSubPow2 implements PubSub.Atomic { slide(): void { if (this.subscribersIndex !== this.publisherIndex) { const index = this.subscribersIndex & this.mask + const value = this.array[index] this.array[index] = AbsentValue as unknown as A this.subscribers[index] = 0 this.subscribersIndex += 1 - } - if (this.replayBuffer) { - this.replayBuffer.slide() + this.replayBuffer?.slide(value, this.replayIndices[index]) } } @@ -1912,6 +1893,7 @@ class BoundedPubSubSingle implements PubSub.Atomic { subscriberCount = 0 subscribers = 0 value: A = AbsentValue as unknown as A + replayIndex = 0 readonly capacity = 1 readonly replayBuffer: ReplayBuffer | undefined @@ -1944,14 +1926,15 @@ class BoundedPubSubSingle implements PubSub.Atomic { if (this.isFull()) { return false } + const replayIndex = this.replayBuffer?.offer(value) if (this.subscriberCount !== 0) { this.value = value + if (replayIndex !== undefined) { + this.replayIndex = replayIndex + } this.subscribers = this.subscriberCount this.publisherIndex += 1 } - if (this.replayBuffer) { - this.replayBuffer.offer(value) - } return true } @@ -1975,11 +1958,10 @@ class BoundedPubSubSingle implements PubSub.Atomic { slide(): void { if (this.isFull()) { + const value = this.value this.subscribers = 0 this.value = AbsentValue as unknown as A - } - if (this.replayBuffer) { - this.replayBuffer.slide() + this.replayBuffer?.slide(value, this.replayIndex) } } @@ -2058,6 +2040,7 @@ class BoundedPubSubSingleSubscription implements PubSub.BackingSubscri interface Node { value: A | AbsentValue + replayIndex: number | undefined subscribers: number next: Node | null } @@ -2065,6 +2048,7 @@ interface Node { class UnboundedPubSub implements PubSub.Atomic { publisherHead: Node = { value: AbsentValue, + replayIndex: undefined, subscribers: 0, next: null } @@ -2096,19 +2080,19 @@ class UnboundedPubSub implements PubSub.Atomic { } publish(value: A): boolean { + const replayIndex = this.replayBuffer?.offer(value) const subscribers = this.publisherTail.subscribers if (subscribers !== 0) { - this.publisherTail.next = { + const node: Node = { value, + replayIndex, subscribers, next: null } + this.publisherTail.next = node this.publisherTail = this.publisherTail.next this.publisherIndex += 1 } - if (this.replayBuffer) { - this.replayBuffer.offer(value) - } return true } @@ -2125,12 +2109,12 @@ class UnboundedPubSub implements PubSub.Atomic { slide(): void { if (this.publisherHead !== this.publisherTail) { + const node = this.publisherHead.next! + const value = node.value as A this.publisherHead = this.publisherHead.next! this.publisherHead.value = AbsentValue this.subscribersIndex += 1 - } - if (this.replayBuffer) { - this.replayBuffer.slide() + this.replayBuffer?.slide(value, node.replayIndex!) } } @@ -2481,34 +2465,30 @@ export class BackPressureStrategy implements PubSub.Strategy { * * **Example** (Applying a dropping strategy) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { - * // Create PubSub with dropping strategy - * const pubsub = yield* PubSub.dropping(2) - * - * // Or explicitly create with dropping strategy - * const customPubsub = yield* PubSub.make({ + * const program = Effect.scoped(Effect.gen(function*() { + * // Explicitly create a PubSub with a dropping strategy + * const pubsub = yield* PubSub.make({ * atomicPubSub: () => PubSub.makeAtomicBounded(2), * strategy: () => new PubSub.DroppingStrategy() * }) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Fill the PubSub - * const pub1 = yield* PubSub.publish(pubsub, "msg1") // true - * const pub2 = yield* PubSub.publish(pubsub, "msg2") // true - * const pub3 = yield* PubSub.publish(pubsub, "msg3") // false (dropped) + * // Fill the PubSub + * const pub1 = yield* PubSub.publish(pubsub, "msg1") // true + * const pub2 = yield* PubSub.publish(pubsub, "msg2") // true + * const pub3 = yield* PubSub.publish(pubsub, "msg3") // false (dropped) * - * console.log("Publication results:", [pub1, pub2, pub3]) // [true, true, false] + * // Subscribers will only see the first two messages + * const messages = yield* PubSub.takeAll(subscription) + * return { published: [pub1, pub2, pub3], messages } + * })) * - * // Subscribers will only see the first two messages - * const messages = yield* PubSub.takeAll(subscription) - * console.log("Received messages:", messages) // ["msg1", "msg2"] - * })) - * }) + * const actual = await Effect.runPromise(program) + * actual // => { published: [true, true, false], messages: ["msg1", "msg2"] } * ``` * * @category models @@ -2568,33 +2548,30 @@ export class DroppingStrategy implements PubSub.Strategy { * * **Example** (Applying a sliding strategy) * - * ```ts + * ```ts import.meta.vitest * import { Effect, PubSub } from "effect" * - * const program = Effect.gen(function*() { - * // Create PubSub with sliding strategy - * const pubsub = yield* PubSub.sliding(2) - * - * // Or explicitly create with sliding strategy - * const customPubsub = yield* PubSub.make({ + * const program = Effect.scoped(Effect.gen(function*() { + * // Explicitly create a PubSub with a sliding strategy + * const pubsub = yield* PubSub.make({ * atomicPubSub: () => PubSub.makeAtomicBounded(2), * strategy: () => new PubSub.SlidingStrategy() * }) * - * yield* Effect.scoped(Effect.gen(function*() { - * const subscription = yield* PubSub.subscribe(pubsub) + * const subscription = yield* PubSub.subscribe(pubsub) * - * // Publish messages that exceed capacity - * yield* PubSub.publish(pubsub, "msg1") // stored - * yield* PubSub.publish(pubsub, "msg2") // stored - * yield* PubSub.publish(pubsub, "msg3") // "msg1" evicted, "msg3" stored - * yield* PubSub.publish(pubsub, "msg4") // "msg2" evicted, "msg4" stored + * // Publish messages that exceed capacity + * yield* PubSub.publish(pubsub, "msg1") // stored + * yield* PubSub.publish(pubsub, "msg2") // stored + * yield* PubSub.publish(pubsub, "msg3") // "msg1" evicted, "msg3" stored + * yield* PubSub.publish(pubsub, "msg4") // "msg2" evicted, "msg4" stored * - * // Subscribers will see the most recent messages - * const messages = yield* PubSub.takeAll(subscription) - * console.log("Recent messages:", messages) // ["msg3", "msg4"] - * })) - * }) + * // Subscribers will see the most recent messages + * return yield* PubSub.takeAll(subscription) + * })) + * + * const actual = await Effect.runPromise(program) + * actual // => ["msg3", "msg4"] * ``` * * @category models @@ -2702,27 +2679,40 @@ const strategyCompleteSubscribersUnsafe = ( interface ReplayNode { value: A | AbsentValue + index: number next: ReplayNode | null } class ReplayBuffer { readonly capacity: number - head: ReplayNode = { value: AbsentValue, next: null } + head: ReplayNode = { value: AbsentValue, index: 0, next: null } tail: ReplayNode = this.head + readonly slideValues: Array<{ + readonly value: A + readonly index: number + }> = [] size = 0 index = 0 + publisherIndex = 0 constructor(capacity: number) { this.capacity = capacity } - slide() { + slide(value: A, publisherIndex: number): void { + this.slideValues[this.index % this.capacity] = { + value, + index: publisherIndex + } this.index++ } - offer(a: A): void { + offer(a: A): number { + const index = this.publisherIndex++ this.tail.value = a + this.tail.index = index this.tail.next = { value: AbsentValue, + index: 0, next: null } this.tail = this.tail.next @@ -2731,6 +2721,7 @@ class ReplayBuffer { } else { this.size += 1 } + return index } offerAll(as: Iterable): void { for (const a of as) { @@ -2740,48 +2731,66 @@ class ReplayBuffer { } class ReplayWindowImpl implements PubSub.ReplayWindow { - head: ReplayNode - index: number - remaining: number readonly buffer: ReplayBuffer + readonly values: Array + index = 0 + remaining: number + slideIndex: number + newestIndex = -1 constructor(buffer: ReplayBuffer) { this.buffer = buffer - 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++ + this.slideIndex = buffer.index + this.values = new Array(this.remaining) + let node = buffer.head + for (let i = 0; i < this.remaining; i++) { + this.values[i] = node.value as A + this.newestIndex = node.index + node = node.next! + } + } + close(): void { + this.values.length = 0 + this.remaining = 0 + } + sync(): void { + const slides = this.buffer.index - this.slideIndex + if (slides === 0 || this.remaining === 0) { + return + } + const count = Math.min(slides, this.buffer.capacity) + const start = this.buffer.index - count + for (let i = 0; i < count; i++) { + const entry = this.buffer.slideValues[(start + i) % this.buffer.capacity] + if (entry.index > this.newestIndex) { + this.index = (this.index + 1) % this.values.length + this.values[(this.index + this.remaining - 1) % this.values.length] = entry.value + this.newestIndex = entry.index + } } + this.slideIndex = this.buffer.index } take(): A | undefined { if (this.remaining === 0) { return undefined - } else if (this.index < this.buffer.index) { - this.fastForward() } + this.sync() + const value = this.values[this.index] + this.values[this.index] = AbsentValue as unknown as A + this.index = (this.index + 1) % this.values.length this.remaining-- - const value = this.head.value - this.head = this.head.next! + if (this.remaining === 0) { + this.close() + } return value as A } takeN(n: number): Array { - if (this.remaining === 0) { - return [] - } 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 + items[i] = this.take()! } - this.remaining -= len return items } takeAll(): Array { @@ -2793,5 +2802,6 @@ const emptyReplayWindow: PubSub.ReplayWindow = { remaining: 0, take: () => undefined, takeN: () => [], - takeAll: () => [] + takeAll: () => [], + close: () => void 0 } diff --git a/.context/effect/packages/effect/src/Pull.ts b/.context/effect/packages/effect/src/Pull.ts index 54092190a..116e26a31 100644 --- a/.context/effect/packages/effect/src/Pull.ts +++ b/.context/effect/packages/effect/src/Pull.ts @@ -53,7 +53,7 @@ export interface Pull * @see {@link Leftover} for extracting the completion leftover type * @see {@link Services} for extracting the required services type instead * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Success

= P extends Effect ? _A : never @@ -71,7 +71,7 @@ export type Success

= P extends Effect ? _A : n * @see {@link Services} for extracting the required services type instead * @see {@link ExcludeDone} for excluding `Cause.Done` from an error union * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Error

= P extends Effect ? _E extends Cause.Done ? never : _E @@ -90,7 +90,7 @@ export type Error

= P extends Effect ? _E exten * @see {@link Error} for extracting the ordinary failure type, excluding `Cause.Done` * @see {@link Services} for extracting the required services type instead * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Leftover

= P extends Effect ? _E extends Cause.Done ? _L : never @@ -108,7 +108,7 @@ export type Leftover

= P extends Effect ? _E ex * @see {@link Error} for extracting the ordinary failure type * @see {@link Leftover} for extracting the completion leftover type * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Services

= P extends Effect ? _R : never @@ -124,7 +124,7 @@ export type Services

= P extends Effect ? _R : * @see {@link Error} for extracting ordinary failures from a `Pull` * @see {@link Leftover} for extracting the completion leftover type * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type ExcludeDone = Exclude> @@ -151,7 +151,7 @@ export type ExcludeDone = Exclude> * @see {@link matchEffect} for handling success, ordinary failure, and done outcomes explicitly * @see {@link filterDoneLeftover} for extracting a done leftover from an existing `Cause` * - * @category Done + * @category error handling * @since 4.0.0 */ export const catchDone: { @@ -180,7 +180,7 @@ export const catchDone: { * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` * @see {@link filterNoDone} for selecting causes with no done failures * - * @category Done + * @category predicates * @since 4.0.0 */ export const isDoneCause = (cause: Cause.Cause): boolean => cause.reasons.some(isDoneFailure) @@ -197,7 +197,7 @@ export const isDoneCause = (cause: Cause.Cause): boolean => cause.reasons. * @see {@link isDoneCause} for checking an entire `Cause` for any done reason * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` * - * @category Done + * @category guards * @since 4.0.0 */ export const isDoneFailure = ( @@ -217,7 +217,7 @@ export const isDoneFailure = ( * Returns a successful `Result` with the `Cause.Done` value when one is * present, otherwise returns a failed `Result` containing the non-done cause. * - * @category Done + * @category filtering * @since 4.0.0 */ export const filterDone: ( @@ -245,7 +245,7 @@ export const filterDone: ( * @see {@link filterDoneLeftover} for extracting only the done leftover value * @see {@link filterNoDone} for the inverse filter that succeeds only when no done failure is present * - * @category Done + * @category filtering * @since 4.0.0 */ export const filterDoneVoid: ( @@ -271,7 +271,7 @@ export const filterDoneVoid: ( * @see {@link filterDone} for the inverse typed done filter * @see {@link filterDoneVoid} for done detection when the payload is not needed * - * @category Done + * @category filtering * @since 4.0.0 */ export const filterNoDone: ( @@ -291,7 +291,7 @@ export const filterNoDone: ( * Use to extract only the leftover value carried by a `Cause.Done` completion * signal. * - * @category Done + * @category filtering * @since 4.0.0 */ export const filterDoneLeftover: ( @@ -319,7 +319,7 @@ export const filterDoneLeftover: ( * @see {@link filterDone} for extracting the done signal without converting the cause to an `Exit` * @see {@link matchEffect} for handling `Pull` success, failure, and done outcomes directly * - * @category Done + * @category converting * @since 4.0.0 */ export const doneExitFromCause = (cause: Cause.Cause): Exit.Exit, ExcludeDone> => { @@ -336,7 +336,7 @@ export const doneExitFromCause = (cause: Cause.Cause): Exit.Exit(cause: Cause.Cause): Exit.Exit Effect.succeed(`Got error: ${cause}`), * onDone: (leftover) => Effect.succeed(`Stream halted with: ${leftover}`) * }) + * + * await Effect.runPromise(result) // => "Stream halted with: stream ended" * ``` * * @category pattern matching diff --git a/.context/effect/packages/effect/src/Queue.ts b/.context/effect/packages/effect/src/Queue.ts index bd0b4a2e9..cad92efc3 100644 --- a/.context/effect/packages/effect/src/Queue.ts +++ b/.context/effect/packages/effect/src/Queue.ts @@ -142,7 +142,7 @@ export const asDequeue: (self: Queue) => Dequeue = identity * * **Example** (Offering through enqueue handles) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * // Function that only needs write access to a queue @@ -155,7 +155,10 @@ export const asDequeue: (self: Queue) => Dequeue = identity * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * yield* producer(queue) + * return yield* Queue.takeAll(queue) * }) + * + * await Effect.runPromise(program) // => ["hello", "world", "!"] * ``` * * @category models @@ -206,7 +209,7 @@ export declare namespace Enqueue { * * **Example** (Taking through dequeue handles) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -220,8 +223,10 @@ export declare namespace Enqueue { * * // Take elements using dequeue interface * const item = yield* Queue.take(dequeue) - * console.log(item) // "a" + * return item * }) + * + * await Effect.runPromise(program) // => "a" * ``` * * @category models @@ -270,7 +275,7 @@ export declare namespace Dequeue { * * **Example** (Offering and taking queue values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -286,8 +291,10 @@ export declare namespace Dequeue { * const item2 = yield* Queue.take(queue) * const item3 = yield* Queue.take(queue) * - * console.log([item1, item2, item3]) // ["hello", "world", "!"] + * return [item1, item2, item3] * }) + * + * await Effect.runPromise(program) // => ["hello", "world", "!"] * ``` * * @category models @@ -408,10 +415,10 @@ const QueueProto = { * * **Example** (Creating queues) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const queue = yield* Queue.make() * * // add messages to the queue @@ -421,18 +428,18 @@ const QueueProto = { * * // take messages from the queue * const messages = yield* Queue.takeAll(queue) - * console.log(messages) // [1, 2, 3, 4, 5] * * // signal that the queue is done * yield* Queue.end(queue) * const done = yield* Effect.flip(Queue.take(queue)) - * console.log(Cause.isDone(done)) // true * * // signal that another queue has failed * const failedQueue = yield* Queue.make() * const failed = yield* Queue.fail(failedQueue, "boom") - * console.log(failed) // true + * return { messages, done, failed } * }) + * + * await Effect.runPromise(program) // => { messages: [1, 2, 3, 4, 5], done: Cause.Done(), failed: true } * ``` * * @category constructors @@ -470,7 +477,7 @@ export const make = ( * * **Example** (Creating bounded queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -481,8 +488,10 @@ export const make = ( * yield* Queue.offer(queue, "second") * * const size = yield* Queue.size(queue) - * console.log(size) // 2 + * return size * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category constructors @@ -501,7 +510,7 @@ export const bounded = (capacity: number): Effect> => * * **Example** (Creating sliding queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -516,8 +525,10 @@ export const bounded = (capacity: number): Effect> => * yield* Queue.offer(queue, 4) * * const all = yield* Queue.takeAll(queue) - * console.log(all) // [2, 3, 4] - oldest element (1) was dropped + * return all * }) + * + * await Effect.runPromise(program) // => [2, 3, 4] * ``` * * @category constructors @@ -536,7 +547,7 @@ export const sliding = (capacity: number): Effect> => * * **Example** (Creating dropping queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -545,15 +556,15 @@ export const sliding = (capacity: number): Effect> => * // Fill the queue to capacity * const success1 = yield* Queue.offer(queue, 1) * const success2 = yield* Queue.offer(queue, 2) - * console.log(success1, success2) // true, true * * // This will be dropped * const success3 = yield* Queue.offer(queue, 3) - * console.log(success3) // false * * const all = yield* Queue.takeAll(queue) - * console.log(all) // [1, 2] - element 3 was dropped + * return [success1, success2, success3, all] * }) + * + * await Effect.runPromise(program) // => [true, true, false, [1, 2]] * ``` * * @category constructors @@ -572,7 +583,7 @@ export const dropping = (capacity: number): Effect> => * * **Example** (Creating unbounded queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -585,12 +596,13 @@ export const dropping = (capacity: number): Effect> => * * // Check current size * const size = yield* Queue.size(queue) - * console.log(size) // 5 * * // Take all messages * const messages = yield* Queue.takeAll(queue) - * console.log(messages) // ["message1", "message2", "message3", "message4", "message5"] + * return { size, messages } * }) + * + * await Effect.runPromise(program) // => { size: 5, messages: ["message1", "message2", "message3", "message4", "message5"] } * ``` * * @category constructors @@ -609,7 +621,7 @@ export const unbounded = (): Effect> => make() * * **Example** (Offering a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -618,15 +630,16 @@ export const unbounded = (): Effect> => make() * // Successfully add messages to queue * const success1 = yield* Queue.offer(queue, 1) * const success2 = yield* Queue.offer(queue, 2) - * console.log(success1, success2) // true, true * * // Queue state * const size = yield* Queue.size(queue) - * console.log(size) // 2 + * return { offered: [success1, success2], size } * }) + * + * await Effect.runPromise(program) // => { offered: [true, true], size: 2 } * ``` * - * @category Offering + * @category offering * @since 2.0.0 */ export const offer = (self: Enqueue, message: Types.NoInfer): Effect => @@ -670,8 +683,8 @@ export const offer = (self: Enqueue, message: Types.NoInfer): Eff * * **Example** (Offering a value synchronously) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Queue } from "effect" * * // Create a queue effect and extract the queue for unsafe operations * const program = Effect.gen(function*() { @@ -680,15 +693,16 @@ export const offer = (self: Enqueue, message: Types.NoInfer): Eff * // Add messages synchronously using unsafe API * const success1 = Queue.offerUnsafe(queue, 1) * const success2 = Queue.offerUnsafe(queue, 2) - * console.log(success1, success2) // true, true * * // Check current size * const size = Queue.sizeUnsafe(queue) - * console.log(size) // 2 + * return { offered: [success1, success2], size } * }) + * + * await Effect.runPromise(program) // => { offered: [true, true], size: 2 } * ``` * - * @category Offering + * @category offering * @since 4.0.0 */ export const offerUnsafe = (self: Enqueue, message: Types.NoInfer): boolean => { @@ -728,7 +742,7 @@ export const offerUnsafe = (self: Enqueue, message: Types.NoInfer * * **Example** (Offering multiple values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -736,11 +750,13 @@ export const offerUnsafe = (self: Enqueue, message: Types.NoInfer * * // Try to add more messages than capacity without suspending * const remaining1 = yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) - * console.log(remaining1) // [4, 5] - couldn't fit the last 2 + * return remaining1 * }) + * + * await Effect.runPromise(program) // => [4, 5] * ``` * - * @category Offering + * @category offering * @since 2.0.0 */ export const offerAll = (self: Enqueue, messages: Iterable): Effect> => @@ -772,8 +788,8 @@ export const offerAll = (self: Enqueue, messages: Iterable): Effe * * **Example** (Offering multiple values synchronously) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Queue } from "effect" * * // Create a bounded queue and use unsafe API * const program = Effect.gen(function*() { @@ -781,15 +797,16 @@ export const offerAll = (self: Enqueue, messages: Iterable): Effe * * // Try to add 5 messages to capacity-3 queue using unsafe API * const remaining = Queue.offerAllUnsafe(queue, [1, 2, 3, 4, 5]) - * console.log(remaining) // [4, 5] - couldn't fit the last 2 * * // Check what's in the queue * const size = Queue.sizeUnsafe(queue) - * console.log(size) // 3 + * return { remaining, size } * }) + * + * await Effect.runPromise(program) // => { remaining: [4, 5], size: 3 } * ``` * - * @category Offering + * @category offering * @since 4.0.0 */ export const offerAllUnsafe = (self: Enqueue, messages: Iterable): Array => { @@ -832,20 +849,21 @@ export const offerAllUnsafe = (self: Enqueue, messages: Iterable) * * **Example** (Failing queues with an error) * - * ```ts - * import { Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * * // Fail the queue with an error * const failed = yield* Queue.fail(queue, "Something went wrong") - * console.log(failed) // true * * // Taking from the failed queue fails with the error - * const error = yield* Effect.flip(Queue.take(queue)) - * console.log(error) // "Something went wrong" + * const exit = yield* Effect.exit(Queue.take(queue)) + * return [failed, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.fail("Something went wrong")] * ``` * * @category completion @@ -859,8 +877,8 @@ export const fail = (self: Enqueue, error: E) => failCause(self, cor * * **Example** (Failing queues with a cause) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -868,11 +886,13 @@ export const fail = (self: Enqueue, error: E) => failCause(self, cor * // Create a cause and fail the queue * const cause = Cause.fail("Queue processing failed") * const failed = yield* Queue.failCause(queue, cause) - * console.log(failed) // true * * // The queue is now done with the specified failure cause - * console.log(queue.state._tag) // "Done" + * const exit = yield* Effect.exit(Queue.take(queue)) + * return [failed, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.failCause(Cause.fail("Queue processing failed"))] * ``` * * @category completion @@ -902,8 +922,8 @@ export const failCause: { * * **Example** (Failing queues with a cause synchronously) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -911,11 +931,13 @@ export const failCause: { * // Create a cause and fail the queue synchronously * const cause = Cause.fail("Processing error") * const failed = Queue.failCauseUnsafe(queue, cause) - * console.log(failed) // true * * // The queue is now done with the specified failure cause - * console.log(queue.state._tag) // "Done" + * const exit = Queue.takeUnsafe(queue) + * return [failed, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.failCause(Cause.fail("Processing error"))] * ``` * * @category completion @@ -952,7 +974,7 @@ export const failCauseUnsafe = (self: Enqueue, cause: Cause): boo * * **Example** (Ending queues) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -964,16 +986,16 @@ export const failCauseUnsafe = (self: Enqueue, cause: Cause): boo * * // Signal completion - no more messages will be accepted * const ended = yield* Queue.end(queue) - * console.log(ended) // true * * // Trying to offer more messages will return false * const offerResult = yield* Queue.offer(queue, 3) - * console.log(offerResult) // false * * // But we can still take existing messages * const message = yield* Queue.take(queue) - * console.log(message) // 1 + * return [ended, offerResult, message] * }) + * + * await Effect.runPromise(program) // => [true, false, 1] * ``` * * @category completion @@ -999,7 +1021,7 @@ export const end = (self: Enqueue): Effect => failCa * * **Example** (Ending queues synchronously) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * // Create a queue and use unsafe operations @@ -1012,17 +1034,19 @@ export const end = (self: Enqueue): Effect => failCa * * // End the queue synchronously * const ended = Queue.endUnsafe(queue) - * console.log(ended) // true * * // Existing messages can still be consumed while the queue is closing - * console.log(queue.state._tag) // "Closing" + * const states = [queue.state._tag] * * Queue.takeUnsafe(queue) * Queue.takeUnsafe(queue) * * // After buffered messages are consumed, the queue is done - * console.log(queue.state._tag) // "Done" + * states.push(queue.state._tag) + * return { ended, states } * }) + * + * await Effect.runPromise(program) // => { ended: true, states: ["Closing", "Done"] } * ``` * * @category completion @@ -1040,7 +1064,7 @@ export const endUnsafe = (self: Enqueue) => failCauseUnsafe(s * * **Example** (Interrupting queues gracefully) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1052,23 +1076,21 @@ export const endUnsafe = (self: Enqueue) => failCauseUnsafe(s * * // Interrupt gracefully - no more offers accepted, but messages can be consumed * const interrupted = yield* Queue.interrupt(queue) - * console.log(interrupted) // true * * // Trying to offer more messages will return false * const offerResult = yield* Queue.offer(queue, 3) - * console.log(offerResult) // false * * // But we can still take existing messages * const message1 = yield* Queue.take(queue) - * console.log(message1) // 1 * * const message2 = yield* Queue.take(queue) - * console.log(message2) // 2 * * // After all messages are consumed, queue is done * const isDone = queue.state._tag === "Done" - * console.log(isDone) // true + * return { interrupted, offerResult, messages: [message1, message2], isDone } * }) + * + * await Effect.runPromise(program) // => { interrupted: true, offerResult: false, messages: [1, 2], isDone: true } * ``` * * @category completion @@ -1088,7 +1110,7 @@ export const interrupt = (self: Enqueue): Effect => * * **Example** (Shutting down queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1100,12 +1122,13 @@ export const interrupt = (self: Enqueue): Effect => * * // Shutdown clears buffered messages and prevents further offers * const wasShutdown = yield* Queue.shutdown(queue) - * console.log(wasShutdown) // true * * // Queue is now done and cleared * const size = yield* Queue.size(queue) - * console.log(size) // 0 + * return { wasShutdown, size } * }) + * + * await Effect.runPromise(program) // => { wasShutdown: true, size: 0 } * ``` * * @category completion @@ -1142,8 +1165,8 @@ export const shutdown = (self: Enqueue): Effect => * * **Example** (Clearing queued values) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) @@ -1153,16 +1176,16 @@ export const shutdown = (self: Enqueue): Effect => * * // Clear all messages from the queue * const messages = yield* Queue.clear(queue) - * console.log(messages) // [1, 2, 3, 4, 5] * * // Queue is now empty * const size = yield* Queue.size(queue) - * console.log(size) // 0 * * // Clearing empty queue returns empty array * const empty = yield* Queue.clear(queue) - * console.log(empty) // [] + * return { messages, size, empty } * }) + * + * await Effect.runPromise(program) // => { messages: [1, 2, 3, 4, 5], size: 0, empty: [] } * ``` * * @category taking @@ -1197,7 +1220,7 @@ export const clear = (self: Dequeue): Effect, Pull.ExcludeD * * **Example** (Taking all available values) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1208,8 +1231,10 @@ export const clear = (self: Dequeue): Effect, Pull.ExcludeD * * // Take all available messages * const messages1 = yield* Queue.takeAll(queue) - * console.log(messages1) // [1, 2, 3, 4, 5] + * return messages1 * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5] * ``` * * @category taking @@ -1223,7 +1248,7 @@ export const takeAll = (self: Dequeue): Effect, * * **Example** (Collecting values until completion) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1231,13 +1256,13 @@ export const takeAll = (self: Dequeue): Effect, * * // Add several messages * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) - * // Some time later, end the queue - * yield* Effect.forkChild(Queue.end(queue)) + * yield* Queue.end(queue) * * // Collect all available messages - * const messages = yield* Queue.collect(queue) - * console.log(messages) // [1, 2, 3, 4, 5] + * return yield* Queue.collect(queue) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5] * ``` * * @category taking @@ -1275,7 +1300,7 @@ export const collect = (self: Dequeue): Effect, Pull * * **Example** (Taking a fixed number of values) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1286,16 +1311,16 @@ export const collect = (self: Dequeue): Effect, Pull * * // Take exactly 3 messages * const first3 = yield* Queue.takeN(queue, 3) - * console.log(first3) // [1, 2, 3] * * // Take exactly 2 more messages * const next2 = yield* Queue.takeN(queue, 2) - * console.log(next2) // [4, 5] * * // Take remaining messages * const remaining = yield* Queue.takeN(queue, 2) - * console.log(remaining) // [6, 7] + * return [first3, next2, remaining] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3], [4, 5], [6, 7]] * ``` * * @category taking @@ -1318,7 +1343,7 @@ export const takeN = ( * * **Example** (Taking a bounded batch of values) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1329,15 +1354,16 @@ export const takeN = ( * * // Take between 2 and 5 messages * const batch1 = yield* Queue.takeBetween(queue, 2, 5) - * console.log(batch1) // [1, 2, 3, 4, 5] - took 5 (up to max) * * // Take between 1 and 10 messages (but only 3 remain) * const batch2 = yield* Queue.takeBetween(queue, 1, 10) - * console.log(batch2) // [6, 7, 8] - took 3 (all remaining) * * // No more messages available, will wait or return done * // const batch3 = yield* Queue.takeBetween(queue, 1, 3) + * return [batch1, batch2] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3, 4, 5], [6, 7, 8]] * ``` * * @category taking @@ -1363,8 +1389,8 @@ export const takeBetween = ( * * **Example** (Taking one value) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(3) @@ -1376,18 +1402,16 @@ export const takeBetween = ( * // Take messages one by one * const msg1 = yield* Queue.take(queue) * const msg2 = yield* Queue.take(queue) - * console.log(msg1, msg2) // "first", "second" * * // End the queue * yield* Queue.end(queue) * * // Taking from an ended queue fails with Done - * const result = yield* Effect.match(Queue.take(queue), { - * onFailure: (error: Cause.Done) => true, - * onSuccess: (value: string) => false - * }) - * console.log("Queue ended:", result) // true + * const result = yield* Effect.exit(Queue.take(queue)) + * return [[msg1, msg2], result] * }) + * + * await Effect.runPromise(program) // => [["first", "second"], Exit.fail(Cause.Done())] * ``` * * @category taking @@ -1409,7 +1433,7 @@ export const take = (self: Dequeue): Effect => * * **Example** (Polling without blocking) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1417,15 +1441,16 @@ export const take = (self: Dequeue): Effect => * * // Poll returns Option.none if empty * const maybe1 = yield* Queue.poll(queue) - * console.log(Option.isNone(maybe1)) // true * * // Add an item * yield* Queue.offer(queue, 42) * * // Poll returns Option.some with the item * const maybe2 = yield* Queue.poll(queue) - * console.log(Option.getOrNull(maybe2)) // 42 + * return [maybe1, maybe2] * }) + * + * await Effect.runPromise(program) // => [Option.none(), Option.some(42)] * ``` * * @category taking @@ -1452,7 +1477,7 @@ export const poll = (self: Dequeue): Effect> => * * **Example** (Peeking at the next value) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { @@ -1461,8 +1486,10 @@ export const poll = (self: Dequeue): Effect> => * * // Peek at the next item without removing it * const item = yield* Queue.peek(queue) - * console.log(item) // 42 + * return item * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category taking @@ -1495,8 +1522,8 @@ export const peek = (self: Dequeue): Effect => * * **Example** (Taking one value synchronously) * - * ```ts - * import { Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Queue } from "effect" * * // Create a queue and use unsafe operations * const program = Effect.gen(function*() { @@ -1508,15 +1535,15 @@ export const peek = (self: Dequeue): Effect => * * // Take a message synchronously * const result1 = Queue.takeUnsafe(queue) - * console.log(result1) // Success(1) or Exit containing value 1 * * const result2 = Queue.takeUnsafe(queue) - * console.log(result2) // Success(2) * * // No more messages - returns undefined * const result3 = Queue.takeUnsafe(queue) - * console.log(result3) // undefined + * return [result1, result2, result3] * }) + * + * await Effect.runPromise(program) // => [Exit.succeed(1), Exit.succeed(2), undefined] * ``` * * @category taking @@ -1543,16 +1570,14 @@ export const takeUnsafe = (self: Dequeue): Exit | undefined => const await_ = (self: Dequeue): Effect> => internalEffect.callback>((resume) => { + const awaiter = (effect: Effect) => resume(Pull.catchDone(effect, () => internalEffect.exitVoid)) if (self.state._tag === "Done") { - if (Pull.isDoneCause(self.state.exit.cause)) { - return resume(internalEffect.exitVoid) - } - return resume(self.state.exit) + return awaiter(self.state.exit) } - self.state.awaiters.add(resume) + self.state.awaiters.add(awaiter) return internalEffect.sync(() => { if (self.state._tag !== "Done") { - self.state.awaiters.delete(resume) + self.state.awaiters.delete(awaiter) } }) }) @@ -1594,34 +1619,36 @@ export { * * **Details** * - * Completed queues report a size of `0`. + * After `end`, a queue remains `Closing` while buffered messages are drained, + * and its size continues to include those messages. A `Done` queue reports a + * size of `0`. * * **Example** (Checking queue size) * - * ```ts - * import { Cause, Effect, Option, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * * // Check size of empty queue * const size1 = yield* Queue.size(queue) - * console.log(size1) // 0 * * // Add some messages * yield* Queue.offerAll(queue, [1, 2, 3, 4, 5]) * * // Check size after adding messages * const size2 = yield* Queue.size(queue) - * console.log(size2) // 5 * * // End the queue * yield* Queue.end(queue) * - * // Size of ended queue is 0 + * // Ending retains the buffered size while the queue is Closing * const size3 = yield* Queue.size(queue) - * console.log(size3) // 0 + * return [size1, size2, size3] * }) + * + * await Effect.runPromise(program) // => [0, 5, 5] * ``` * * @category sizes @@ -1634,22 +1661,25 @@ export const size = (self: Dequeue): Effect => internalEffec * * **Example** (Checking if queues are full) * - * ```ts - * import { Cause, Effect, Option, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(3) * - * console.log(yield* Queue.isFull(queue)) // false + * const before = yield* Queue.isFull(queue) * * // Add some messages * yield* Queue.offerAll(queue, [1, 2, 3]) * - * console.log(yield* Queue.isFull(queue)) // true + * const after = yield* Queue.isFull(queue) + * return [before, after] * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category sizes + * @category predicates * @since 2.0.0 */ export const isFull = (self: Dequeue): Effect => internalEffect.sync(() => isFullUnsafe(self)) @@ -1664,20 +1694,21 @@ export const isFull = (self: Dequeue): Effect => internalEf * * **Details** * - * Completed queues report a size of `0`. This unsafe operation reads the queue - * state directly without Effect wrapping. + * After `endUnsafe`, a queue remains `Closing` while buffered messages are + * drained, and its size continues to include those messages. A `Done` queue + * reports a size of `0`. This unsafe operation reads the queue state directly + * without Effect wrapping. * * **Example** (Checking queue size synchronously) * - * ```ts - * import { Cause, Effect, Option, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * * // Check size of empty queue * const size1 = Queue.sizeUnsafe(queue) - * console.log(size1) // 0 * * // Add some messages * Queue.offerUnsafe(queue, 1) @@ -1686,15 +1717,16 @@ export const isFull = (self: Dequeue): Effect => internalEf * * // Check size after adding messages * const size2 = Queue.sizeUnsafe(queue) - * console.log(size2) // 3 * * // End the queue * Queue.endUnsafe(queue) * - * // Size of ended queue is 0 + * // Ending retains the buffered size while the queue is Closing * const size3 = Queue.sizeUnsafe(queue) - * console.log(size3) // 0 + * return [size1, size2, size3] * }) + * + * await Effect.runPromise(program) // => [0, 3, 3] * ``` * * @category sizes @@ -1712,22 +1744,25 @@ export const sizeUnsafe = (self: Dequeue): number => self.state._tag * * **Example** (Checking fullness synchronously) * - * ```ts - * import { Cause, Effect, Option, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(3) * - * console.log(Queue.isFullUnsafe(queue)) // false + * const before = Queue.isFullUnsafe(queue) * * // Add some messages * yield* Queue.offerAll(queue, [1, 2, 3]) * - * console.log(Queue.isFullUnsafe(queue)) // true + * const after = Queue.isFullUnsafe(queue) + * return [before, after] * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category sizes + * @category predicates * @since 4.0.0 */ export const isFullUnsafe = (self: Dequeue): boolean => sizeUnsafe(self) === self.capacity @@ -1738,15 +1773,15 @@ export const isFullUnsafe = (self: Dequeue): boolean => sizeUnsafe(s * * **Example** (Running effects into queues) * - * ```ts - * import { Cause, Effect, Queue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Queue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* Queue.bounded(10) * * // Create an effect that succeeds * const dataProcessing = Effect.gen(function*() { - * yield* Effect.sleep("100 millis") + * yield* Effect.yieldNow * return "Processing completed successfully" * }) * @@ -1756,11 +1791,11 @@ export const isFullUnsafe = (self: Dequeue): boolean => sizeUnsafe(s * const effectIntoQueue = Queue.into(queue)(dataProcessing) * * const wasCompleted = yield* effectIntoQueue - * console.log("Queue operation completed:", wasCompleted) // true - * - * // Queue state now reflects the effect's outcome - * console.log("Queue state:", queue.state._tag) // "Done" + * const exit = yield* Effect.exit(Queue.take(queue)) + * return [wasCompleted, exit] * }) + * + * await Effect.runPromise(program) // => [true, Exit.fail(Cause.Done())] * ``` * * @category completion diff --git a/.context/effect/packages/effect/src/Random.ts b/.context/effect/packages/effect/src/Random.ts index 53078f1d2..2afe37ea1 100644 --- a/.context/effect/packages/effect/src/Random.ts +++ b/.context/effect/packages/effect/src/Random.ts @@ -33,21 +33,20 @@ import * as Predicate from "./Predicate.ts" * * **Example** (Accessing the random service) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * * const program = Effect.gen(function*() { * const float = yield* Random.next * const integer = yield* Random.nextInt * const inRange = yield* Random.nextIntBetween(1, 100) - * - * console.log("Float:", float) - * console.log("Integer:", integer) - * console.log("In range:", inRange) + * return [float, integer, inRange] as const * }) + * + * await Effect.runPromise(program.pipe(Random.withSeed("example"))) // => [0.1633802591287037, 3434461687501127, 1] * ``` * - * @category Random Number Generators + * @category services * @since 2.0.0 */ export const Random: Context.Reference<{ @@ -68,16 +67,13 @@ const randomWith = (f: (random: typeof Random["Service"]) => A): Effect.Effec * * **Example** (Generating a random number) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const randomDouble = yield* Random.next - * console.log("Random double:", randomDouble) - * }) + * await Effect.runPromise(Random.next.pipe(Random.withSeed("example"))) // => 0.1633802591287037 * ``` * - * @category Random Number Generators + * @category generators * @since 2.0.0 */ export const next: Effect.Effect = randomWith((r) => r.nextDoubleUnsafe()) @@ -91,16 +87,13 @@ export const next: Effect.Effect = randomWith((r) => r.nextDoubleUnsafe( * * **Example** (Generating a random boolean) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const value = yield* Random.nextBoolean - * console.log("Random boolean:", value) - * }) + * await Effect.runPromise(Random.nextBoolean.pipe(Random.withSeed("example"))) // => false * ``` * - * @category Random Number Generators + * @category generators * @since 2.0.0 */ export const nextBoolean: Effect.Effect = randomWith((r) => r.nextDoubleUnsafe() > 0.5) @@ -116,16 +109,13 @@ export const nextBoolean: Effect.Effect = randomWith((r) => r.nextDoubl * * **Example** (Generating a random integer) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const randomInt = yield* Random.nextInt - * console.log("Random integer:", randomInt) - * }) + * await Effect.runPromise(Random.nextInt.pipe(Random.withSeed("example"))) // => -6064002158214091 * ``` * - * @category Random Number Generators + * @category generators * @since 2.0.0 */ export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe()) @@ -139,16 +129,13 @@ export const nextInt: Effect.Effect = randomWith((r) => r.nextIntUnsafe( * * **Example** (Generating a bounded random number) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const randomDouble = yield* Random.nextBetween(0, 1) - * console.log("Random double: ", randomDouble) - * }) + * await Effect.runPromise(Random.nextBetween(0, 1).pipe(Random.withSeed("example"))) // => 0.1633802591287037 * ``` * - * @category Random Number Generators + * @category generators * @since 4.0.0 */ export const nextBetween = (min: number, max: number): Effect.Effect => @@ -169,7 +156,7 @@ export const nextBetween = (min: number, max: number): Effect.Effect => * * **Example** (Generating a bounded random integer) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * * const program = Effect.gen(function*() { @@ -178,10 +165,13 @@ export const nextBetween = (min: number, max: number): Effect.Effect => * halfOpen: true * }) * const diceRoll3 = yield* Random.nextIntBetween(0, 10) + * return [diceRoll1, diceRoll2, diceRoll3] * }) + * + * await Effect.runPromise(program.pipe(Random.withSeed("example"))) // => [1, 4, 0] * ``` * - * @category Random Number Generators + * @category generators * @since 2.0.0 */ export const nextIntBetween = (min: number, max: number, options?: { @@ -204,16 +194,13 @@ export const nextIntBetween = (min: number, max: number, options?: { * * **Example** (Shuffling values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const values = yield* Random.shuffle([1, 2, 3, 4, 5]) - * console.log(values) - * }) + * await Effect.runPromise(Random.shuffle([1, 2, 3, 4, 5]).pipe(Random.withSeed("example"))) // => [4, 2, 5, 3, 1] * ``` * - * @category Random Number Generators + * @category generators * @since 2.0.0 */ export const shuffle = (elements: Iterable): Effect.Effect> => @@ -243,16 +230,13 @@ export const shuffle = (elements: Iterable): Effect.Effect> => * * **Example** (Choosing a random value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * - * const program = Effect.gen(function*() { - * const value = yield* Random.choice(["red", "green", "blue"] as const) - * console.log(value) - * }) + * await Effect.runPromise(Random.choice(["red", "green", "blue"] as const).pipe(Random.withSeed("example"))) // => "red" * ``` * - * @category Random Number Generators + * @category generators * @since 3.6.0 */ export const choice: >( @@ -285,25 +269,22 @@ export const choice: >( * * **Example** (Seeding random generation) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Random } from "effect" * * const program = Effect.gen(function*() { * const value1 = yield* Random.next * const value2 = yield* Random.next - * console.log(value1, value2) + * return [value1, value2] * }) * - * // Same seed produces same sequence - * const seeded1 = program.pipe(Random.withSeed("my-seed")) - * const seeded2 = program.pipe(Random.withSeed("my-seed")) - * - * // Both will output identical values - * Effect.runPromise(seeded1) - * Effect.runPromise(seeded2) + * await Effect.runPromise(Effect.all([ + * program.pipe(Random.withSeed("my-seed")), + * program.pipe(Random.withSeed("my-seed")) + * ])) // => [[0.018368576514773527, 0.4010840628128671], [0.018368576514773527, 0.4010840628128671]] * ``` * - * @category Seeding + * @category providing services * @since 4.0.0 */ export const withSeed: { diff --git a/.context/effect/packages/effect/src/RcMap.ts b/.context/effect/packages/effect/src/RcMap.ts index 18892480c..f1de43441 100644 --- a/.context/effect/packages/effect/src/RcMap.ts +++ b/.context/effect/packages/effect/src/RcMap.ts @@ -39,17 +39,14 @@ const TypeId = "~effect/RcMap" * * **Example** (Inspecting a reference-counted map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * // Create an RcMap that manages database connections * const dbConnectionMap = yield* RcMap.make({ * lookup: (dbName: string) => - * Effect.acquireRelease( - * Effect.succeed(`Connection to ${dbName}`), - * (conn) => Effect.log(`Closing ${conn}`) - * ), + * Effect.acquireRelease(Effect.succeed(`Connection to ${dbName}`), () => Effect.void), * capacity: 10, * idleTimeToLive: "5 minutes" * }) @@ -60,8 +57,10 @@ const TypeId = "~effect/RcMap" * // - idleTimeToLive: Time before idle resources are released * // - state: Current state of the map * - * console.log(`Capacity: ${dbConnectionMap.capacity}`) - * }).pipe(Effect.scoped) + * return dbConnectionMap.capacity + * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => 10 * ``` * * @see {@link make} for creating an `RcMap` @@ -205,15 +204,17 @@ const makeUnsafe = (options: { * * **Example** (Creating a reference-counted map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const events: Array = [] + * + * const program = Effect.gen(function*() { * const map = yield* RcMap.make({ * lookup: (key: string) => * Effect.acquireRelease( * Effect.succeed(`acquired ${key}`), - * () => Effect.log(`releasing ${key}`) + * () => Effect.sync(() => events.push(`released ${key}`)) * ) * }) * @@ -224,12 +225,15 @@ const makeUnsafe = (options: { * Effect.scoped * ) * }) + * + * await Effect.runPromise(Effect.scoped(program)) + * events // => ["released foo"] * ``` * * @see {@link get} for acquiring or retaining a resource by key * @see {@link invalidate} for removing a resource from the map * - * @category models + * @category constructors * @since 3.5.0 */ export const make: { @@ -299,22 +303,26 @@ export const make: { * * **Example** (Acquiring a resource) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const events: Array = [] + * + * const program = Effect.gen(function*() { * const map = yield* RcMap.make({ * lookup: (key: string) => * Effect.acquireRelease( * Effect.succeed(`Resource: ${key}`), - * () => Effect.log(`Released ${key}`) + * () => Effect.sync(() => events.push(`released ${key}`)) * ) * }) * * // Get a resource - it will be acquired on first access * const resource = yield* RcMap.get(map, "database") - * console.log(resource) // "Resource: database" - * }).pipe(Effect.scoped) + * return [resource, events] as const + * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => ["Resource: database", ["released database"]] * ``` * * @see {@link make} for creating the reference-counted map @@ -427,10 +435,10 @@ const release = (self: RcMap, key: K, entry: State.Entry * * **Example** (Listing keys) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const map = yield* RcMap.make({ * lookup: (key: string) => Effect.succeed(`value-${key}`) * }) @@ -442,8 +450,10 @@ const release = (self: RcMap, key: K, entry: State.Entry * * // Get all keys currently in the map * const allKeys = yield* RcMap.keys(map) - * console.log(allKeys) // ["foo", "bar", "baz"] - * }).pipe(Effect.scoped) + * return Array.from(allKeys) + * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => ["foo", "bar", "baz"] * ``` * * @see {@link has} for checking one key without enumerating all keys @@ -467,15 +477,17 @@ export const keys = (self: RcMap): Effect.Effect> * * **Example** (Invalidating a resource) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const events: Array = [] + * + * const program = Effect.gen(function*() { * const map = yield* RcMap.make({ * lookup: (key: string) => * Effect.acquireRelease( * Effect.succeed(`Resource: ${key}`), - * () => Effect.log(`Released ${key}`) + * () => Effect.sync(() => events.push(`released ${key}`)) * ) * }) * @@ -488,7 +500,10 @@ export const keys = (self: RcMap): Effect.Effect> * * // Next access will create a new resource * yield* RcMap.get(map, "cache") - * }).pipe(Effect.scoped) + * }) + * + * await Effect.runPromise(Effect.scoped(program)) + * events // => ["released cache", "released cache"] * ``` * * @see {@link get} for acquiring or retaining the resource for a key @@ -561,15 +576,17 @@ export const has: { * * **Example** (Extending resource idle time) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcMap } from "effect" * - * Effect.gen(function*() { + * const events: Array = [] + * + * const program = Effect.gen(function*() { * const map = yield* RcMap.make({ * lookup: (key: string) => * Effect.acquireRelease( * Effect.succeed(`Resource: ${key}`), - * () => Effect.log(`Released ${key}`) + * () => Effect.sync(() => events.push(`released ${key}`)) * ), * idleTimeToLive: "10 seconds" * }) @@ -583,7 +600,10 @@ export const has: { * * // The resource will now live for another 10 seconds * // from the time it was touched - * }).pipe(Effect.scoped) + * }) + * + * await Effect.runPromise(Effect.scoped(program)) + * events // => ["released session"] * ``` * * @see {@link invalidate} for removing the resource instead of extending it diff --git a/.context/effect/packages/effect/src/RcRef.ts b/.context/effect/packages/effect/src/RcRef.ts index fab9307ec..3d64d5890 100644 --- a/.context/effect/packages/effect/src/RcRef.ts +++ b/.context/effect/packages/effect/src/RcRef.ts @@ -33,15 +33,17 @@ const TypeId = "~effect/RcRef" * * **Example** (Sharing a lazily acquired resource) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcRef } from "effect" * + * const events: Array = [] + * * // Create an RcRef for a database connection * const createConnectionRef = (connectionString: string) => * RcRef.make({ * acquire: Effect.acquireRelease( * Effect.succeed(`Connected to ${connectionString}`), - * (connection) => Effect.log(`Closing connection: ${connection}`) + * (connection) => Effect.sync(() => events.push(`closed ${connection}`)) * ) * }) * @@ -53,8 +55,10 @@ const TypeId = "~effect/RcRef" * const connection1 = yield* RcRef.get(connectionRef) * const connection2 = yield* RcRef.get(connectionRef) * - * return [connection1, connection2] + * return [connection1 === connection2, events] as const * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => [true, ["closed Connected to postgres://localhost"]] * ``` * * @category models @@ -69,12 +73,13 @@ export interface RcRef extends Pipeable { * * **Example** (Referencing namespace types) * - * ```ts + * ```ts import.meta.vitest * import type { RcRef } from "effect" * * // Use RcRef namespace types * type MyRcRef = RcRef.RcRef * type MyVariance = RcRef.RcRef.Variance + * * ``` * * @since 3.5.0 @@ -121,14 +126,16 @@ export declare namespace RcRef { * * **Example** (Creating a reference-counted resource) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcRef } from "effect" * - * Effect.gen(function*() { + * const events: Array = [] + * + * const program = Effect.gen(function*() { * const ref = yield* RcRef.make({ * acquire: Effect.acquireRelease( * Effect.succeed("foo"), - * () => Effect.log("release foo") + * () => Effect.sync(() => events.push("released foo")) * ) * }) * @@ -139,6 +146,9 @@ export declare namespace RcRef { * Effect.scoped * ) * }) + * + * await Effect.runPromise(Effect.scoped(program)) + * events // => ["released foo"] * ``` * * @category constructors @@ -172,15 +182,17 @@ export const make: ( * * **Example** (Sharing one acquired value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, RcRef } from "effect" * + * const events: Array = [] + * * const program = Effect.gen(function*() { * // Create an RcRef with a resource * const ref = yield* RcRef.make({ * acquire: Effect.acquireRelease( * Effect.succeed("shared resource"), - * (resource) => Effect.log(`Releasing ${resource}`) + * (resource) => Effect.sync(() => events.push(`released ${resource}`)) * ) * }) * @@ -188,11 +200,10 @@ export const make: ( * const value1 = yield* RcRef.get(ref) * const value2 = yield* RcRef.get(ref) * - * // Both values are the same instance - * console.log(value1 === value2) // true - * - * return value1 + * return [value1 === value2, events] as const * }) + * + * await Effect.runPromise(Effect.scoped(program)) // => [true, ["released shared resource"]] * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/Record.ts b/.context/effect/packages/effect/src/Record.ts index 99d006ce2..7ac2341a8 100644 --- a/.context/effect/packages/effect/src/Record.ts +++ b/.context/effect/packages/effect/src/Record.ts @@ -15,6 +15,7 @@ import * as Equal from "./Equal.ts" import type { Equivalence } from "./Equivalence.ts" import { dual, identity } from "./Function.ts" import type { TypeLambda } from "./HKT.ts" +import * as InternalRecord from "./internal/record.ts" import * as Option from "./Option.ts" import * as Reducer from "./Reducer.ts" import type { Result } from "./Result.ts" @@ -27,7 +28,7 @@ import type { NoInfer } from "./Types.ts" * * **Example** (Defining a readonly record type) * - * ```ts + * ```ts import.meta.vitest * import type { Record } from "effect" * * // Creating a readonly record type @@ -37,6 +38,7 @@ import type { NoInfer } from "./Types.ts" * name: "John", * age: 30 * } + * user // => { name: "John", age: 30 } * ``` * * @category models @@ -52,7 +54,7 @@ export type ReadonlyRecord = { * * **Example** (Using readonly record helper types) * - * ```ts + * ```ts import.meta.vitest * import type { Record } from "effect" * * // Using NonLiteralKey to convert literal keys to generic types @@ -60,6 +62,9 @@ export type ReadonlyRecord = { * * // Using IntersectKeys to find common keys between record types * type CommonKeys = Record.ReadonlyRecord.IntersectKeys<"a" | "b", "b" | "c"> // "b" + * + * "key" satisfies GenericKey + * "b" satisfies CommonKeys * ``` * * @since 2.0.0 @@ -76,7 +81,7 @@ export declare namespace ReadonlyRecord { * * **Example** (Converting literal keys to non-literal keys) * - * ```ts + * ```ts import.meta.vitest * import type { Record } from "effect" * * // For literal string keys, this becomes 'string' @@ -84,6 +89,10 @@ export declare namespace ReadonlyRecord { * * // For symbol keys, this becomes 'symbol' * type Example2 = Record.ReadonlyRecord.NonLiteralKey // symbol + * + * const symbol: Example2 = Symbol.for("key") + * "key" satisfies Example1 + * symbol * ``` * * @category models @@ -98,7 +107,7 @@ export declare namespace ReadonlyRecord { * * **Example** (Intersecting record keys) * - * ```ts + * ```ts import.meta.vitest * import type { Record } from "effect" * * // Intersection of literal keys @@ -106,6 +115,9 @@ export declare namespace ReadonlyRecord { * * // Intersection with generic string * type Example2 = Record.ReadonlyRecord.IntersectKeys // string + * + * "b" satisfies Example1 + * "a" satisfies Example2 * ``` * * @category models @@ -122,7 +134,7 @@ export declare namespace ReadonlyRecord { * * **Example** (Applying a readonly record type lambda) * - * ```ts + * ```ts import.meta.vitest * import type { HKT, Record } from "effect" * * type Settings = HKT.Kind< @@ -137,9 +149,10 @@ export declare namespace ReadonlyRecord { * port: 3000, * retries: 3 * } + * defaults // => { port: 3000, retries: 3 } * ``` * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface ReadonlyRecordTypeLambda extends TypeLambda { @@ -151,16 +164,15 @@ export interface ReadonlyRecordTypeLambda extends Typ * * **Example** (Creating an empty record) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" * * // Create an empty record * const emptyRecord = Record.empty() - * console.log(emptyRecord) // {} + * emptyRecord // => {} * * // The type ensures type safety for future operations - * const withValue = Record.set(emptyRecord, "count", 42) - * console.log(withValue) // { count: 42 } + * Record.set(emptyRecord, "count", 42) // => { count: 42 } * ``` * * @category constructors @@ -176,12 +188,11 @@ export const empty = (): Record< * * **Example** (Checking for an empty record) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.isEmptyRecord({}), true) - * assert.deepStrictEqual(Record.isEmptyRecord({ a: 3 }), false) + * Record.isEmptyRecord({}) // => true + * Record.isEmptyRecord({ a: 3 }) // => false * ``` * * @category guards @@ -195,12 +206,11 @@ export const isEmptyRecord = (self: Record): self is * * **Example** (Checking for an empty readonly record) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.isEmptyReadonlyRecord({}), true) - * assert.deepStrictEqual(Record.isEmptyReadonlyRecord({ a: 3 }), false) + * Record.isEmptyReadonlyRecord({}) // => true + * Record.isEmptyReadonlyRecord({ a: 3 }) // => false * ``` * * @category guards @@ -216,16 +226,10 @@ export const isEmptyReadonlyRecord: ( * * **Example** (Building a record from mapped iterable values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" - * - * const input = [1, 2, 3, 4] * - * assert.deepStrictEqual( - * Record.fromIterableWith(input, (a) => [String(a), a * 2]), - * { "1": 2, "2": 4, "3": 6, "4": 8 } - * ) + * Record.fromIterableWith([1, 2, 3, 4], (a) => [String(a), a * 2]) // => { "1": 2, "2": 4, "3": 6, "4": 8 } * ``` * * @category constructors @@ -248,7 +252,7 @@ export const fromIterableWith: { const out: Record = empty() for (const a of self) { const [k, b] = f(a) - out[k] = b + InternalRecord.assignProperty(out, k, b) } return out } @@ -259,31 +263,38 @@ export const fromIterableWith: { * * **Example** (Building a record keyed by iterable values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * * const users = [ * { id: "2", name: "name2" }, * { id: "1", name: "name1" } * ] * - * assert.deepStrictEqual( - * Record.fromIterableBy(users, (user) => user.id), - * { - * "2": { id: "2", name: "name2" }, - * "1": { id: "1", name: "name1" } - * } - * ) + * Record.fromIterableBy( + * users, + * (user) => user.id + * ) // => { "1": { id: "1", name: "name1" }, "2": { id: "2", name: "name2" } } * ``` * * @category constructors * @since 2.0.0 */ -export const fromIterableBy = ( - items: Iterable, - f: (a: A) => K -): Record, A> => fromIterableWith(items, (a) => [f(a), a]) +export const fromIterableBy: { + ( + f: (a: A) => K + ): (items: Iterable) => Record, A> + ( + items: Iterable, + f: (a: A) => K + ): Record, A> +} = dual( + 2, + ( + items: Iterable, + f: (a: A) => K + ): Record, A> => fromIterableWith(items, (a) => [f(a), a]) +) /** * Builds a record from an iterable of key-value pairs. @@ -295,13 +306,10 @@ export const fromIterableBy = ( * * **Example** (Building a record from entries) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" - * - * const input: Array<[string, number]> = [["a", 1], ["b", 2]] * - * assert.deepStrictEqual(Record.fromEntries(input), { a: 1, b: 2 }) + * Record.fromEntries([["a", 1], ["b", 2]]) // => { a: 1, b: 2 } * ``` * * @category constructors @@ -316,15 +324,11 @@ export const fromEntries: ( * * **Example** (Collecting mapped record values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3 } - * assert.deepStrictEqual(Record.collect(x, (key, n) => [key, n]), [["a", 1], [ - * "b", - * 2 - * ], ["c", 3]]) + * Record.collect(x, (key, n) => [key, n]) // => [["a", 1], ["b", 2], ["c", 3]] * ``` * * @category converting @@ -349,12 +353,11 @@ export const collect: { * * **Example** (Converting a record to entries) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3 } - * assert.deepStrictEqual(Record.toEntries(x), [["a", 1], ["b", 2], ["c", 3]]) + * Record.toEntries(x) // => [["a", 1], ["b", 2], ["c", 3]] * ``` * * @category converting @@ -370,11 +373,10 @@ export const toEntries: (self: ReadonlyRecord) => Arr * * **Example** (Getting the record size) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.size({ a: "a", b: 1, c: true }), 3) + * Record.size({ a: "a", b: 1, c: true }) // => 3 * ``` * * @category getters @@ -387,15 +389,14 @@ export const size = (self: ReadonlyRecord): number => * * **Example** (Checking key membership) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.has({ a: 1, b: 2 }, "a"), true) - * assert.deepStrictEqual(Record.has(Record.empty(), "c"), false) + * Record.has({ a: 1, b: 2 }, "a") // => true + * Record.has(Record.empty(), "c") // => false * ``` * - * @category guards + * @category predicates * @since 2.0.0 */ export const has: { @@ -419,14 +420,13 @@ export const has: { * * **Example** (Getting a value as an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option, Record as R } from "effect" - * import * as assert from "node:assert" * * 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()) + * R.get(person, "name") // => Option.some("John Doe") + * R.get(person, "email") // => Option.none() * ``` * * @category getters @@ -438,7 +438,7 @@ export const get: { } = dual( 2, (self: ReadonlyRecord, key: NoInfer): Option.Option => - has(self, key) ? Option.some(self[key]) : Option.none() + Object.hasOwn(self, key) ? Option.some(self[key]) : Option.none() ) /** @@ -447,15 +447,15 @@ export const get: { * * **Example** (Modifying a value at a key) * - * ```ts - * import { Record } from "effect" + * ```ts import.meta.vitest + * import { Option, Record } from "effect" * * const f = (x: number) => x * 2 * * const input: Record = { a: 3 } * - * Record.modify(input, "a", f) // Option.some({ a: 6 }) - * Record.modify(input, "b", f) // Option.none() + * Record.modify(input, "a", f) // => Option.some({ a: 6 }) + * Record.modify(input, "b", f) // => Option.none() * ``` * * @category mutations @@ -494,11 +494,11 @@ export const modify: { * * **Example** (Replacing a value at a key) * - * ```ts - * import { Record } from "effect" + * ```ts import.meta.vitest + * import { Option, Record } from "effect" * - * Record.replace({ a: 1, b: 2, c: 3 }, "a", 10) // Option.some({ a: 10, b: 2, c: 3 }) - * Record.replace(Record.empty(), "a", 10) // Option.none() + * Record.replace({ a: 1, b: 2, c: 3 }, "a", 10) // => Option.some({ a: 10, b: 2, c: 3 }) + * Record.replace(Record.empty(), "a", 10) // => Option.none() * ``` * * @category mutations @@ -537,11 +537,10 @@ export const replace: { * * **Example** (Removing a key) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.remove({ a: 1, b: 2 }, "a"), { b: 2 }) + * Record.remove({ a: 1, b: 2 }, "a") // => { b: 2 } * ``` * * @category mutations @@ -569,13 +568,13 @@ export const remove: { * * **Example** (Popping a value and removing its key) * - * ```ts - * import { Record } from "effect" + * ```ts import.meta.vitest + * import { Option, Record } from "effect" * * const input: Record = { a: 1, b: 2 } * - * Record.pop(input, "a") // Option.some([1, { b: 2 }]) - * Record.pop(input, "c") // Option.none() + * Record.pop(input, "a") // => Option.some([1, { b: 2 }]) + * Record.pop(input, "c") // => Option.none() * ``` * * @category mutations @@ -600,17 +599,16 @@ export const pop: { * * **Example** (Mapping record values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * * const f = (n: number) => `-${n}` * - * assert.deepStrictEqual(Record.map({ a: 3, b: 5 }, f), { a: "-3", b: "-5" }) + * Record.map({ a: 3, b: 5 }, f) // => { a: "-3", b: "-5" } * * const g = (n: number, key: string) => `${key.toUpperCase()}-${n}` * - * assert.deepStrictEqual(Record.map({ a: 3, b: 5 }, g), { a: "A-3", b: "B-5" }) + * Record.map({ a: 3, b: 5 }, g) // => { a: "A-3", b: "B-5" } * ``` * * @category mapping @@ -624,7 +622,7 @@ export const map: { (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) + InternalRecord.assignProperty(out, key, f(self[key], key)) } return out } @@ -635,14 +633,10 @@ export const map: { * * **Example** (Mapping record keys) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.mapKeys({ a: 3, b: 5 }, (key) => key.toUpperCase()), - * { A: 3, B: 5 } - * ) + * Record.mapKeys({ a: 3, b: 5 }, (key) => key.toUpperCase()) // => { A: 3, B: 5 } * ``` * * @category mapping @@ -665,7 +659,7 @@ export const mapKeys: { const out: Record = {} as any for (const key of keys(self)) { const a = self[key] - out[f(key, a)] = a + InternalRecord.assignProperty(out, f(key, a), a) } return out } @@ -676,14 +670,10 @@ export const mapKeys: { * * **Example** (Mapping record entries) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.mapEntries({ a: 3, b: 5 }, (a, key) => [key.toUpperCase(), a + 1]), - * { A: 4, B: 6 } - * ) + * Record.mapEntries({ a: 3, b: 5 }, (a, key) => [key.toUpperCase(), a + 1]) // => { A: 4, B: 6 } * ``` * * @category mapping @@ -706,7 +696,7 @@ export const mapEntries: { const out = {} as Record for (const key of keys(self)) { const [k, b] = f(self[key], key) - out[k] = b + InternalRecord.assignProperty(out, k, b) } return out } @@ -718,13 +708,12 @@ export const mapEntries: { * * **Example** (Filtering and mapping with Result) * - * ```ts + * ```ts import.meta.vitest * import { Record, Result } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3 } * const f = (a: number, key: string) => a > 2 ? Result.succeed(a * 2) : Result.failVoid - * assert.deepStrictEqual(Record.filterMap(x, f), { c: 6 }) + * Record.filterMap(x, f) // => { c: 6 } * ``` * * @category filtering @@ -748,7 +737,7 @@ export const filterMap: { for (const key of keys(self)) { const result = f(self[key], key) if (R.isSuccess(result)) { - out[key] = result.success + InternalRecord.assignProperty(out, key, result.success) } } return out @@ -760,12 +749,11 @@ export const filterMap: { * * **Example** (Filtering record values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3, d: 4 } - * assert.deepStrictEqual(Record.filter(x, (n) => n > 2), { c: 3, d: 4 }) + * Record.filter(x, (n) => n > 2) // => { c: 3, d: 4 } * ``` * * @category filtering @@ -795,7 +783,7 @@ export const filter: { const out: Record = empty() for (const key of keys(self)) { if (predicate(self[key], key)) { - out[key] = self[key] + InternalRecord.assignProperty(out, key, self[key]) } } return out @@ -808,14 +796,10 @@ export const filter: { * * **Example** (Extracting Some values) * - * ```ts + * ```ts import.meta.vitest * import { Option, Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.getSomes({ a: Option.some(1), b: Option.none(), c: Option.some(2) }), - * { a: 1, c: 2 } - * ) + * Record.getSomes({ a: Option.some(1), b: Option.none(), c: Option.some(2) }) // => { a: 1, c: 2 } * ``` * * @category filtering @@ -830,7 +814,7 @@ export const getSomes: ( for (const key of keys(self)) { const option = self[key] if (Option.isSome(option)) { - out[key] = option.value + InternalRecord.assignProperty(out, key, option.value) } } return out @@ -842,18 +826,14 @@ export const getSomes: ( * * **Example** (Extracting Result failures) * - * ```ts + * ```ts import.meta.vitest * import { Record, Result } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.getFailures({ + * Record.getFailures({ * a: Result.succeed(1), * b: Result.fail("err"), * c: Result.succeed(2) - * }), - * { b: "err" } - * ) + * }) // => { b: "err" } * ``` * * @category filtering @@ -866,7 +846,7 @@ export const getFailures = ( for (const key of keys(self)) { const value = self[key] if (R.isFailure(value)) { - out[key] = value.failure + InternalRecord.assignProperty(out, key, value.failure) } } @@ -879,18 +859,14 @@ export const getFailures = ( * * **Example** (Extracting Result successes) * - * ```ts + * ```ts import.meta.vitest * import { Record, Result } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.getSuccesses({ + * Record.getSuccesses({ * a: Result.succeed(1), * b: Result.fail("err"), * c: Result.succeed(2) - * }), - * { a: 1, c: 2 } - * ) + * }) // => { a: 1, c: 2 } * ``` * * @category filtering @@ -903,7 +879,7 @@ export const getSuccesses = ( for (const key of keys(self)) { const value = self[key] if (R.isSuccess(value)) { - out[key] = value.success + InternalRecord.assignProperty(out, key, value.success) } } @@ -921,13 +897,12 @@ export const getSuccesses = ( * * **Example** (Partitioning with Result) * - * ```ts + * ```ts import.meta.vitest * import { Record, Result } from "effect" - * import * as assert from "node:assert" * * const x = { a: 1, b: 2, c: 3 } * const f = (n: number) => (n % 2 === 0 ? Result.succeed(n) : Result.fail(n)) - * assert.deepStrictEqual(Record.partition(x, f), [{ a: 1, c: 3 }, { b: 2 }]) + * Record.partition(x, f) // => [{ a: 1, c: 3 }, { b: 2 }] * ``` * * @category filtering @@ -954,9 +929,9 @@ export const partition: { for (const key of keys(self)) { const e = f(self[key], key) if (R.isFailure(e)) { - left[key] = e.failure + InternalRecord.assignProperty(left, key, e.failure) } else { - right[key] = e.success + InternalRecord.assignProperty(right, key, e.success) } } return [left, right] @@ -969,14 +944,10 @@ export const partition: { * * **Example** (Separating Result values) * - * ```ts + * ```ts import.meta.vitest * import { Record, Result } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.separate({ a: Result.fail("e"), b: Result.succeed(1) }), - * [{ a: "e" }, { b: 1 }] - * ) + * Record.separate({ a: Result.fail("e"), b: Result.succeed(1) }) // => [{ a: "e" }, { b: 1 }] * ``` * * @category filtering @@ -991,11 +962,10 @@ export const separate: ( * * **Example** (Getting record keys) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.keys({ a: 1, b: 2, c: 3 }), ["a", "b", "c"]) + * Record.keys({ a: 1, b: 2, c: 3 }) // => ["a", "b", "c"] * ``` * * @category getters @@ -1009,11 +979,10 @@ export const keys = (self: ReadonlyRecord): * * **Example** (Getting record values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.values({ a: 1, b: 2, c: 3 }), [1, 2, 3]) + * Record.values({ a: 1, b: 2, c: 3 }) // => [1, 2, 3] * ``` * * @category getters @@ -1026,12 +995,11 @@ export const values = (self: ReadonlyRecord): Array { a: 5, b: 2 } + * Record.set("c", 5)({ a: 1, b: 2 }) // => { a: 1, b: 2, c: 5 } * ``` * * @category mutations @@ -1058,13 +1026,50 @@ export const set: { } ) +/** + * Mutates a record by assigning a value to a property. + * + * **When to use** + * + * Use when incrementally constructing a new record and copying it for every + * property would be unnecessary. + * + * **Gotchas** + * + * This function mutates `self`. When `key` is `"__proto__"`, it creates an + * own data property instead of changing the object's prototype. + * + * **Example** (Assigning an external key safely) + * + * ```ts import.meta.vitest + * import { Record } from "effect" + * + * const key: string = "__proto__" // Assume this comes from external input + * const value = { polluted: true } + * + * const unsafe: Record = {} + * unsafe[key] = value + * Object.getPrototypeOf(unsafe) === value // => true + * + * const safe: Record = {} + * Record.assignProperty(safe, key, value) + * Object.getPrototypeOf(safe) === Object.prototype // => true + * safe[key] === value // => true + * ``` + * + * @see {@link set} for an immutable update + * @category mutations + * @since 4.0.0 + */ +export const assignProperty: (self: object, key: PropertyKey, value: unknown) => void = InternalRecord.assignProperty + /** * Checks whether all the keys and values in one record are also found in another record. * Uses the provided equivalence function to compare values. * * **Example** (Checking subrecords with a custom equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Record } from "effect" * * const isSubrecord = Record.isSubrecordBy( @@ -1077,15 +1082,9 @@ export const set: { * status: "active" * } * - * console.log( - * isSubrecord(required, available) - * ) // true - * console.log( - * isSubrecord({ role: "Admin", status: "inactive" }, available) - * ) // false - * console.log( - * isSubrecord(required, { role: "editor", status: "active" }) - * ) // false + * isSubrecord(required, available) // => true + * isSubrecord({ role: "Admin", status: "inactive" }, available) // => false + * isSubrecord(required, { role: "editor", status: "active" }) // => false * ``` * * @category predicates @@ -1114,18 +1113,11 @@ export const isSubrecordBy = (equivalence: Equivalence): { * * **Example** (Checking subrecords) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.isSubrecord({ a: 1 } as Record, { a: 1, b: 2 }), - * true - * ) - * assert.deepStrictEqual( - * Record.isSubrecord({ a: 1, b: 2 }, { a: 1 } as Record), - * false - * ) + * Record.isSubrecord({ a: 1 } as Record, { a: 1, b: 2 }) // => true + * Record.isSubrecord({ a: 1, b: 2 }, { a: 1 } as Record) // => false * ``` * * @category predicates @@ -1141,14 +1133,10 @@ export const isSubrecord: { * * **Example** (Reducing record values) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.reduce({ a: 1, b: 2, c: 3 }, 0, (acc, value, key) => acc + value), - * 6 - * ) + * Record.reduce({ a: 1, b: 2, c: 3 }, 0, (acc, value) => acc + value) // => 6 * ``` * * @category folding @@ -1180,15 +1168,14 @@ export const reduce: { * * **Example** (Checking every record value) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.every({ a: 1, b: 2 }, (n) => n > 0), true) - * assert.deepStrictEqual(Record.every({ a: 1, b: -1 }, (n) => n > 0), false) + * Record.every({ a: 1, b: 2 }, (n) => n > 0) // => true + * Record.every({ a: 1, b: -1 }, (n) => n > 0) // => false * ``` * - * @category predicates + * @category guards * @since 2.0.0 */ export const every: { @@ -1221,12 +1208,11 @@ export const every: { * * **Example** (Checking for any matching value) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.some({ a: 1, b: 2 }, (n) => n > 1), true) - * assert.deepStrictEqual(Record.some({ a: 1, b: 2 }, (n) => n > 2), false) + * Record.some({ a: 1, b: 2 }, (n) => n > 1) // => true + * Record.some({ a: 1, b: 2 }, (n) => n > 2) // => false * ``` * * @category predicates @@ -1253,14 +1239,10 @@ export const some: { * * **Example** (Merging records with union) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.union({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b), - * { a: 1, b: 5, c: 4 } - * ) + * Record.union({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b) // => { a: 1, b: 5, c: 4 } * ``` * * @category combining @@ -1292,14 +1274,14 @@ export const union: { 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]) + InternalRecord.assignProperty(out, key, combine(self[key], that[key as unknown as K1])) } else { - out[key] = self[key] + InternalRecord.assignProperty(out, key, self[key]) } } for (const key of keys(that)) { if (!has(out, key)) { - out[key] = that[key] + InternalRecord.assignProperty(out, key, that[key]) } } return out @@ -1312,14 +1294,10 @@ export const union: { * * **Example** (Merging intersecting keys) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.intersection({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b), - * { b: 5 } - * ) + * Record.intersection({ a: 1, b: 2 }, { b: 3, c: 4 }, (a, b) => a + b) // => { b: 5 } * ``` * * @category combining @@ -1348,7 +1326,7 @@ export const intersection: { } for (const key of keys(self)) { if (has(that, key as any)) { - out[key] = combine(self[key], that[key as unknown as K1]) + InternalRecord.assignProperty(out, key, combine(self[key], that[key as unknown as K1])) } } return out @@ -1361,14 +1339,10 @@ export const intersection: { * * **Example** (Keeping keys unique to each record) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual( - * Record.difference({ a: 1, b: 2 }, { b: 3, c: 4 }), - * { a: 1, c: 4 } - * ) + * Record.difference({ a: 1, b: 2 }, { b: 3, c: 4 }) // => { a: 1, c: 4 } * ``` * * @category combining @@ -1395,12 +1369,12 @@ export const difference: { const out = {} as Record for (const key of keys(self)) { if (!has(that, key as any)) { - out[key] = self[key] + InternalRecord.assignProperty(out, key, self[key]) } } for (const key of keys(that)) { if (!has(self, key as any)) { - out[key] = that[key] + InternalRecord.assignProperty(out, key, that[key]) } } return out @@ -1412,14 +1386,13 @@ export const difference: { * * **Example** (Comparing records with a value equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equal, Record } from "effect" - * import * as assert from "node:assert" * * const recordEquivalence = Record.makeEquivalence(Equal.asEquivalence()) * - * assert.deepStrictEqual(recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 2 }), true) - * assert.deepStrictEqual(recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 3 }), false) + * recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 2 }) // => true + * recordEquivalence({ a: 1, b: 2 }, { a: 1, b: 3 }) // => false * ``` * * @category instances @@ -1437,11 +1410,10 @@ export const makeEquivalence = ( * * **Example** (Creating a singleton record) * - * ```ts + * ```ts import.meta.vitest * import { Record } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(Record.singleton("a", 1), { a: 1 }) + * Record.singleton("a", 1) // => { a: 1 } * ``` * * @category constructors @@ -1516,18 +1488,17 @@ export function makeReducerIntersection( * * **Example** (Finding the first matching entry) * - * ```ts - * import { Record } from "effect" + * ```ts import.meta.vitest + * import { Option, Record } from "effect" * * const record = { a: 1, b: 2, c: 3 } - * const result = Record.findFirst( + * Record.findFirst( * record, * (value, key) => value > 1 && key !== "b" - * ) - * console.log(result) // Option.Some(["c", 3]) + * ) // => Option.some(["c", 3]) * ``` * - * @category elements + * @category searching * @since 3.14.0 */ export const findFirst: { diff --git a/.context/effect/packages/effect/src/Redactable.ts b/.context/effect/packages/effect/src/Redactable.ts index 23aeb2605..b02eb30f2 100644 --- a/.context/effect/packages/effect/src/Redactable.ts +++ b/.context/effect/packages/effect/src/Redactable.ts @@ -29,7 +29,7 @@ import { hasProperty } from "./Predicate.ts" * * **Example** (Masking an API key) * - * ```ts + * ```ts import.meta.vitest * import { Context, Redactable } from "effect" * * class ApiKey { @@ -39,6 +39,8 @@ import { hasProperty } from "./Predicate.ts" * return this.raw.slice(0, 4) + "..." * } * } + * + * Redactable.redact(new ApiKey("secret-key")) // => "secr..." * ``` * * @see {@link Redactable} for the interface this symbol belongs to @@ -63,7 +65,7 @@ export const symbolRedactable: unique symbol = Symbol.for("~effect/Redactable") * * **Example** (Masking an API key) * - * ```ts + * ```ts import.meta.vitest * import { Context, Redactable } from "effect" * * class ApiKey { @@ -73,6 +75,8 @@ export const symbolRedactable: unique symbol = Symbol.for("~effect/Redactable") * return this.raw.slice(0, 4) + "..." * } * } + * + * Redactable.redact(new ApiKey("secret-key")) // => "secr..." * ``` * * @see {@link symbolRedactable} for the symbol key to implement @@ -159,9 +163,12 @@ export function getRedacted(redactable: Redactable): unknown { /** @internal */ export const currentFiberTypeId = "~effect/Fiber/currentFiber" +const emptyMap = new Map() const emptyContext: Context.Context = { "~effect/Context": {} as any, - mapUnsafe: new Map(), + base: emptyMap, + depth: 0, + mapUnsafe: emptyMap, pipe() { return pipeArguments(this, arguments) } diff --git a/.context/effect/packages/effect/src/Redacted.ts b/.context/effect/packages/effect/src/Redacted.ts index afb1eb698..881ec810d 100644 --- a/.context/effect/packages/effect/src/Redacted.ts +++ b/.context/effect/packages/effect/src/Redacted.ts @@ -39,7 +39,7 @@ const TypeId = "~effect/data/Redacted" * * **Example** (Creating redacted values) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" * * // Create a redacted value to protect sensitive information @@ -47,6 +47,7 @@ const TypeId = "~effect/data/Redacted" * const userPassword = Redacted.make("user-password") * * // TypeScript will infer the types as Redacted + * Array.of(String(apiKey), String(userPassword)) // => ["", ""] * ``` * * @category models @@ -65,14 +66,14 @@ export interface Redacted extends Redacted.Variance, Equal.Eq * * **Example** (Using namespace utilities) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" * * // Use the Redacted namespace for type-level operations * const secret = Redacted.make("my-secret") * * // The namespace contains utilities for working with Redacted values - * const isRedacted = Redacted.isRedacted(secret) // true + * Redacted.isRedacted(secret) // => true * ``` * * @since 3.3.0 @@ -109,7 +110,7 @@ export declare namespace Redacted { * * **Example** (Extracting the redacted value type) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" * * type ApiKey = Redacted.Redacted<{ readonly token: string }> @@ -119,7 +120,7 @@ export declare namespace Redacted { * token: `${value.token}:rotated` * }) * - * console.log(rotate({ token: "secret" })) // { token: "secret:rotated" } + * rotate({ token: "secret" }) // => { token: "secret:rotated" } * ``` * * @category utility types @@ -142,17 +143,17 @@ export declare namespace Redacted { * * **Example** (Checking for redacted values) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" * * const secret = Redacted.make("my-secret") * const plainString = "not-secret" * - * console.log(Redacted.isRedacted(secret)) // true - * console.log(Redacted.isRedacted(plainString)) // false + * Redacted.isRedacted(secret) // => true + * Redacted.isRedacted(plainString) // => false * ``` * - * @category refinements + * @category guards * @since 3.3.0 */ export const isRedacted = (u: unknown): u is Redacted => hasProperty(u, TypeId) @@ -173,10 +174,11 @@ export const isRedacted = (u: unknown): u is Redacted => hasProperty(u, * * **Example** (Creating a redacted value) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" * * const API_KEY = Redacted.make("1234567890") + * String(API_KEY) // => "" * ``` * * @category constructors @@ -229,13 +231,12 @@ const Proto = { * * **Example** (Retrieving a redacted value) * - * ```ts + * ```ts import.meta.vitest * import { Redacted } from "effect" - * import * as assert from "node:assert" * * const API_KEY = Redacted.make("1234567890") * - * assert.equal(Redacted.value(API_KEY), "1234567890") + * Redacted.value(API_KEY) // => "1234567890" * ``` * * @category getters @@ -260,20 +261,20 @@ export const value: (self: Redacted) => T = redacted.value * * **Example** (Wiping a redacted value) * - * ```ts - * import { Redacted } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Redacted, Result } from "effect" * * const API_KEY = Redacted.make("1234567890") * - * assert.equal(Redacted.value(API_KEY), "1234567890") + * Redacted.value(API_KEY) // => "1234567890" * * Redacted.wipeUnsafe(API_KEY) * - * assert.throws( - * () => Redacted.value(API_KEY), - * new Error("Unable to get redacted value") - * ) + * const failure = Result.try({ + * try: () => Redacted.value(API_KEY), + * catch: (error) => (error as Error).message + * }) + * failure // => Result.fail("Unable to get redacted value") * ``` * * @category unsafe @@ -293,9 +294,8 @@ export const wipeUnsafe = (self: Redacted): boolean => redacted.redactedRe * * **Example** (Comparing redacted values) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Redacted } from "effect" - * import * as assert from "node:assert" * * const API_KEY1 = Redacted.make("1234567890") * const API_KEY2 = Redacted.make("1-34567890") @@ -303,8 +303,8 @@ export const wipeUnsafe = (self: Redacted): boolean => redacted.redactedRe * * const equivalence = Redacted.makeEquivalence(Equivalence.strictEqual()) * - * assert.equal(equivalence(API_KEY1, API_KEY2), false) - * assert.equal(equivalence(API_KEY1, API_KEY3), true) + * equivalence(API_KEY1, API_KEY2) // => false + * equivalence(API_KEY1, API_KEY3) // => true * ``` * * @category instances diff --git a/.context/effect/packages/effect/src/Reducer.ts b/.context/effect/packages/effect/src/Reducer.ts index cff3de051..6f1a6e42b 100644 --- a/.context/effect/packages/effect/src/Reducer.ts +++ b/.context/effect/packages/effect/src/Reducer.ts @@ -37,13 +37,12 @@ import type * as Combiner from "./Combiner.ts" * * **Example** (String concatenation reducer) * - * ```ts + * ```ts import.meta.vitest * import { Reducer } from "effect" * * const Concat = Reducer.make((a, b) => a + b, "") * - * console.log(Concat.combineAll(["hello", " ", "world"])) - * // Output: "hello world" + * Concat.combineAll(["hello", " ", "world"]) // => "hello world" * ``` * * @see {@link make} – create a `Reducer` from a function and initial value @@ -89,7 +88,7 @@ export interface Reducer extends Combiner.Combiner { * * **Example** (Multiplying with short-circuit) * - * ```ts + * ```ts import.meta.vitest * import { Reducer } from "effect" * * const Product = Reducer.make( @@ -105,11 +104,8 @@ export interface Reducer extends Combiner.Combiner { * } * ) * - * console.log(Product.combineAll([2, 3, 4])) - * // Output: 24 - * - * console.log(Product.combineAll([2, 0, 4])) - * // Output: 0 + * Product.combineAll([2, 3, 4]) // => 24 + * Product.combineAll([2, 0, 4]) // => 0 * ``` * * @see {@link Reducer} – the interface this creates @@ -155,16 +151,13 @@ export function make( * * **Example** (Reversing string concatenation) * - * ```ts + * ```ts import.meta.vitest * import { Reducer, String } from "effect" * * const Prepend = Reducer.flip(String.ReducerConcat) * - * console.log(Prepend.combine("a", "b")) - * // Output: "ba" - * - * console.log(Prepend.combineAll(["a", "b", "c"])) - * // Output: "cba" + * Prepend.combine("a", "b") // => "ba" + * Prepend.combineAll(["a", "b", "c"]) // => "cba" * ``` * * @see {@link make} diff --git a/.context/effect/packages/effect/src/Ref.ts b/.context/effect/packages/effect/src/Ref.ts index d3569010c..a5c783fca 100644 --- a/.context/effect/packages/effect/src/Ref.ts +++ b/.context/effect/packages/effect/src/Ref.ts @@ -35,24 +35,18 @@ const TypeId = "~effect/Ref" * * **Example** (Reading and updating a ref) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { - * // Create a ref with initial value * const counter = yield* Ref.make(0) - * - * // Read the current value * const value = yield* Ref.get(counter) - * console.log(value) // 0 - * - * // Update the value atomically * yield* Ref.update(counter, (n) => n + 1) - * - * // Read the updated value * const newValue = yield* Ref.get(counter) - * console.log(newValue) // 1 + * return [value, newValue] * }) + * + * await Effect.runPromise(program) // => [0, 1] * ``` * * @see {@link make} for creating a `Ref` @@ -85,18 +79,17 @@ export declare namespace Ref { * * **Example** (Using invariant refs) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * - * // This interface defines the invariant nature of Ref's type parameter - * // A Ref is both a producer and consumer of A * const program = Effect.gen(function*() { * const ref = yield* Ref.make(42) - * - * // Ref is invariant - it can both produce and consume numbers - * const value = yield* Ref.get(ref) // produces number - * yield* Ref.set(ref, value + 1) // consumes number + * const value = yield* Ref.get(ref) + * yield* Ref.set(ref, value + 1) + * return yield* Ref.get(ref) * }) + * + * await Effect.runPromise(program) // => 43 * ``` * * @category models @@ -136,18 +129,11 @@ const RefProto = { * * **Example** (Creating a ref unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Ref } from "effect" * - * // Create a ref directly without Effect * const counter = Ref.makeUnsafe(0) - * - * // Get the current value - * const value = Ref.getUnsafe(counter) - * console.log(value) // 0 - * - * // Note: This is unsafe and should be used carefully - * // Prefer Ref.make for Effect-wrapped creation + * Ref.getUnsafe(counter) // => 0 * ``` * * @category constructors @@ -168,14 +154,15 @@ export const makeUnsafe = (value: A): Ref => { * * **Example** (Creating a ref) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* Ref.make(42) - * const value = yield* Ref.get(ref) - * console.log(value) // 42 + * return yield* Ref.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @see {@link makeUnsafe} for synchronous construction outside Effect code @@ -194,14 +181,15 @@ export const make = (value: A): Effect.Effect> => Effect.sync(() => ma * * **Example** (Getting the current value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* Ref.make(42) - * const value = yield* Ref.get(ref) - * console.log(value) // 42 + * return yield* Ref.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @see {@link set} for replacing the current value @@ -220,29 +208,29 @@ export const get = (self: Ref) => Effect.sync(() => self.ref.current) * * **Example** (Setting a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* Ref.make(0) * yield* Ref.set(ref, 42) - * const value = yield* Ref.get(ref) - * console.log(value) // 42 + * return yield* Ref.get(ref) * }) * - * // Using multiple operations * const program2 = Effect.gen(function*() { * const ref = yield* Ref.make(0) * yield* Ref.set(ref, 100) - * const value = yield* Ref.get(ref) - * console.log(value) // 100 + * return yield* Ref.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 + * await Effect.runPromise(program2) // => 100 * ``` * * @see {@link getAndSet} for setting while returning the previous value * @see {@link setAndGet} for setting while returning the new value * - * @category setters + * @category mutations * @since 2.0.0 */ export const set = dual< @@ -259,19 +247,18 @@ export const set = dual< * * **Example** (Replacing a value atomically) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* Ref.make("initial") * - * // Get current value and set new value atomically * const previous = yield* Ref.getAndSet(ref, "updated") - * console.log(previous) // "initial" - * * const current = yield* Ref.get(ref) - * console.log(current) // "updated" + * return [previous, current] * }) + * + * await Effect.runPromise(program) // => ["initial", "updated"] * ``` * * @see {@link set} for setting without returning the previous value @@ -299,19 +286,18 @@ export const getAndSet = dual< * * **Example** (Updating and returning the previous value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(10) * - * // Get current value and update it atomically * const previous = yield* Ref.getAndUpdate(counter, (n) => n * 2) - * console.log(previous) // 10 - * * const current = yield* Ref.get(counter) - * console.log(current) // 20 + * return [previous, current] * }) + * + * await Effect.runPromise(program) // => [10, 20] * ``` * * @see {@link update} for updating without returning the previous value @@ -345,32 +331,26 @@ export const getAndUpdate = dual< * * **Example** (Conditionally updating and returning the previous value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(5) * - * // Only update if value is greater than 3 * const previous1 = yield* Ref.getAndUpdateSome( * counter, * (n) => n > 3 ? Option.some(n * 2) : Option.none() * ) - * console.log(previous1) // 5 - * * const current1 = yield* Ref.get(counter) - * console.log(current1) // 10 - * - * // Try to update again (won't update since 10 > 3 is true but let's say condition is n < 3) * const previous2 = yield* Ref.getAndUpdateSome( * counter, * (n) => n < 3 ? Option.some(n * 2) : Option.none() * ) - * console.log(previous2) // 10 - * * const current2 = yield* Ref.get(counter) - * console.log(current2) // 10 (unchanged) + * return [previous1, current1, previous2, current2] * }) + * + * await Effect.runPromise(program) // => [5, 10, 10, 10] * ``` * * @see {@link getAndUpdate} for always applying an update @@ -402,28 +382,24 @@ export const getAndUpdateSome = dual< * * **Example** (Setting and returning the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* Ref.make(10) * - * // Set new value and get it back in one operation * const newValue = yield* Ref.setAndGet(ref, 42) - * console.log(newValue) // 42 - * - * // Verify the ref contains the new value * const current = yield* Ref.get(ref) - * console.log(current) // 42 + * return [newValue, current] * }) * - * // Useful for sequential operations * const program2 = Effect.gen(function*() { * const counter = yield* Ref.make(0) - * - * const newValue = yield* Ref.setAndGet(counter, 20) - * console.log(newValue) // 20 + * return yield* Ref.setAndGet(counter, 20) * }) + * + * await Effect.runPromise(program) // => [42, 42] + * await Effect.runPromise(program2) // => 20 * ``` * * @category mutations @@ -450,41 +426,36 @@ export const setAndGet = dual< * * **Example** (Modifying a value atomically) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(10) * - * // Modify the ref and return some computation result * const result = yield* Ref.modify(counter, (n) => [ - * `Previous value was ${n}`, // Return value - * n * 2 // New ref value + * `Previous value was ${n}`, + * n * 2 * ]) - * - * console.log(result) // "Previous value was 10" - * * const current = yield* Ref.get(counter) - * console.log(current) // 20 + * return [result, current] * }) * - * // Example with more complex computation * const program2 = Effect.gen(function*() { * const state = yield* Ref.make({ count: 0, total: 0 }) - * - * const incremented = yield* Ref.modify(state, (s) => [ - * s.count, // Return previous count - * { count: s.count + 1, total: s.total + s.count + 1 } // New state + * return yield* Ref.modify(state, (s) => [ + * s.count, + * { count: s.count + 1, total: s.total + s.count + 1 } * ]) - * - * console.log(incremented) // 0 * }) + * + * await Effect.runPromise(program) // => ["Previous value was 10", 20] + * await Effect.runPromise(program2) // => 0 * ``` * * @see {@link updateAndGet} for returning the new stored value * @see {@link modifySome} for optionally updating while returning a separate result * - * @category setters + * @category mutations * @since 2.0.0 */ export const modify = dual< @@ -513,13 +484,12 @@ export const modify = dual< * * **Example** (Conditionally modifying a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(5) * - * // Only modify if value is greater than 3 * const result1 = yield* Ref.modifySome( * counter, * (n) => @@ -527,13 +497,7 @@ export const modify = dual< * ? [`incremented ${n}`, Option.some(n + 10)] * : ["no change", Option.none()] * ) - * - * console.log(result1) // "incremented 5" - * * const current1 = yield* Ref.get(counter) - * console.log(current1) // 15 - * - * // Try to modify with a condition that fails * const result2 = yield* Ref.modifySome( * counter, * (n) => @@ -541,18 +505,17 @@ export const modify = dual< * ? [`decremented ${n}`, Option.some(n - 5)] * : ["no change", Option.none()] * ) - * - * console.log(result2) // "no change" - * * const current2 = yield* Ref.get(counter) - * console.log(current2) // 15 (unchanged) + * return [result1, current1, result2, current2] * }) + * + * await Effect.runPromise(program) // => ["incremented 5", 15, "no change", 15] * ``` * * @see {@link modify} for always storing a new value * @see {@link updateSome} for optional updates without a separate return value * - * @category setters + * @category mutations * @since 2.0.0 */ export const modifySome: { @@ -581,32 +544,30 @@ export const modifySome: { * * **Example** (Updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(5) * - * // Update the value * yield* Ref.update(counter, (n) => n * 2) - * - * const value = yield* Ref.get(counter) - * console.log(value) // 10 + * return yield* Ref.get(counter) * }) * - * // Using multiple operations * const program2 = Effect.gen(function*() { * const counter = yield* Ref.make(5) * yield* Ref.update(counter, (n: number) => n + 10) - * const value = yield* Ref.get(counter) - * console.log(value) // 15 + * return yield* Ref.get(counter) * }) + * + * await Effect.runPromise(program) // => 10 + * await Effect.runPromise(program2) // => 15 * ``` * * @see {@link updateAndGet} for returning the new value * @see {@link getAndUpdate} for returning the previous value * - * @category setters + * @category mutations * @since 2.0.0 */ export const update = dual< @@ -626,20 +587,18 @@ export const update = dual< * * **Example** (Updating and returning the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(5) * - * // Update and get the new value in one operation * const newValue = yield* Ref.updateAndGet(counter, (n) => n * 3) - * console.log(newValue) // 15 - * - * // Verify the ref contains the new value * const current = yield* Ref.get(counter) - * console.log(current) // 15 + * return [newValue, current] * }) + * + * await Effect.runPromise(program) // => [15, 15] * ``` * * @see {@link update} for updating without returning the new value @@ -667,37 +626,33 @@ export const updateAndGet = dual< * * **Example** (Conditionally updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(5) * - * // Only update if value is even * yield* Ref.updateSome( * counter, * (n) => n % 2 === 0 ? Option.some(n * 2) : Option.none() * ) - * - * let current = yield* Ref.get(counter) - * console.log(current) // 5 (unchanged because 5 is odd) - * - * // Set to even number and try again + * const before = yield* Ref.get(counter) * yield* Ref.set(counter, 6) * yield* Ref.updateSome( * counter, * (n) => n % 2 === 0 ? Option.some(n * 2) : Option.none() * ) - * - * current = yield* Ref.get(counter) - * console.log(current) // 12 (updated because 6 is even) + * const after = yield* Ref.get(counter) + * return [before, after] * }) + * + * await Effect.runPromise(program) // => [5, 12] * ``` * * @see {@link update} for always applying an update * @see {@link updateSomeAndGet} for returning the resulting current value * - * @category setters + * @category mutations * @since 2.0.0 */ export const updateSome = dual< @@ -727,26 +682,24 @@ export const updateSome = dual< * * **Example** (Conditionally updating and returning the current value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Ref } from "effect" * * const program = Effect.gen(function*() { * const counter = yield* Ref.make(10) * - * // Only update if value is greater than 5 * const result1 = yield* Ref.updateSomeAndGet( * counter, * (n) => n > 5 ? Option.some(n / 2) : Option.none() * ) - * console.log(result1) // 5 (updated and returned) - * - * // Try to update again with same condition * const result2 = yield* Ref.updateSomeAndGet( * counter, * (n) => n > 5 ? Option.some(n / 2) : Option.none() * ) - * console.log(result2) // 5 (unchanged because 5 is not > 5) + * return [result1, result2] * }) + * + * await Effect.runPromise(program) // => [5, 5] * ``` * * @see {@link updateSome} for conditional updates without returning a value @@ -781,18 +734,11 @@ export const updateSomeAndGet = dual< * * **Example** (Reading a ref unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Ref } from "effect" * - * // Create a ref directly * const counter = Ref.makeUnsafe(42) - * - * // Get the value synchronously - * const value = Ref.getUnsafe(counter) - * console.log(value) // 42 - * - * // Note: This is unsafe and should be used carefully - * // Prefer Ref.get for Effect-wrapped access + * Ref.getUnsafe(counter) // => 42 * ``` * * @category getters diff --git a/.context/effect/packages/effect/src/References.ts b/.context/effect/packages/effect/src/References.ts index bc7926e3e..5d5e51dd9 100644 --- a/.context/effect/packages/effect/src/References.ts +++ b/.context/effect/packages/effect/src/References.ts @@ -124,52 +124,6 @@ export { Tracer } -/** - * Context reference for controlling the current concurrency limit. Can be set to "unbounded" - * for unlimited concurrency or a specific number to limit concurrent operations. - * - * **When to use** - * - * Use to configure the default concurrency limit for operations that read - * concurrency from the current context. - * - * **Example** (Setting current concurrency) - * - * ```ts - * import { Effect, References } from "effect" - * - * const limitConcurrency = Effect.gen(function*() { - * // Get current setting - * const current = yield* References.CurrentConcurrency - * console.log(current) // "unbounded" (default) - * - * // Run with limited concurrency - * yield* Effect.provideService( - * Effect.gen(function*() { - * const limited = yield* References.CurrentConcurrency - * console.log(limited) // 10 - * }), - * References.CurrentConcurrency, - * 10 - * ) - * - * // Run with unlimited concurrency - * yield* Effect.provideService( - * Effect.gen(function*() { - * const unlimited = yield* References.CurrentConcurrency - * console.log(unlimited) // "unbounded" - * }), - * References.CurrentConcurrency, - * "unbounded" - * ) - * }) - * ``` - * - * @category references - * @since 4.0.0 - */ -export const CurrentConcurrency: Context.Reference = references.CurrentConcurrency - /** * Context reference for managing log annotations that are automatically added to all log entries. * These annotations provide contextual metadata that appears in every log message. @@ -181,23 +135,19 @@ export const CurrentConcurrency: Context.Reference = refer * * **Example** (Managing log annotations) * - * ```ts - * import { Console, Effect, References } from "effect" + * ```ts import.meta.vitest + * import { Effect, References } from "effect" * * const logAnnotationExample = Effect.gen(function*() { * // Get current annotations (empty by default) * const current = yield* References.CurrentLogAnnotations - * console.log(current) // {} + * const defaultCount = Object.keys(current).length * * // Run with custom log annotations - * yield* Effect.provideService( + * const custom = yield* Effect.provideService( * Effect.gen(function*() { * const annotations = yield* References.CurrentLogAnnotations - * console.log(annotations) // { requestId: "req-123", userId: "user-456", version: "1.0.0" } - * - * // All log entries will include these annotations - * yield* Console.log("Starting operation") - * yield* Console.info("Processing data") + * return [annotations.requestId, annotations.userId, annotations.version] * }), * References.CurrentLogAnnotations, * { @@ -208,12 +158,10 @@ export const CurrentConcurrency: Context.Reference = refer * ) * * // Run with extended annotations - * yield* Effect.provideService( + * const extended = yield* Effect.provideService( * Effect.gen(function*() { - * const extended = yield* References.CurrentLogAnnotations - * console.log(extended) // { requestId: "req-123", userId: "user-456", version: "1.0.0", operation: "data-sync", timestamp: 1234567890 } - * - * yield* Console.log("Operation completed with extended context") + * const annotations = yield* References.CurrentLogAnnotations + * return [annotations.operation, annotations.timestamp] * }), * References.CurrentLogAnnotations, * { @@ -224,7 +172,11 @@ export const CurrentConcurrency: Context.Reference = refer * timestamp: 1234567890 * } * ) + * + * return [defaultCount, custom, extended] * }) + * + * await Effect.runPromise(logAnnotationExample) // => [0, ["req-123", "user-456", "1.0.0"], ["data-sync", 1234567890]] * ``` * * @category references @@ -246,39 +198,25 @@ export const CurrentLogAnnotations: Context.Reference = [] + * const logger = Logger.make(({ logLevel }) => { + * levels.push(logLevel) + * }) * - * // Change to Error level to reduce noise - * yield* Effect.provideService( - * Effect.gen(function*() { - * const level = yield* References.CurrentLogLevel - * console.log(level) // "Error" - * yield* Console.info("This info message will be filtered out") - * yield* Console.error("This error message will be shown") - * }), - * References.CurrentLogLevel, - * "Error" + * const program = Effect.gen(function*() { + * yield* Effect.log("uses the default level") + * yield* Effect.log("uses the provided level").pipe( + * Effect.provideService(References.CurrentLogLevel, "Error") * ) * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(Logger.layer([logger])))) + * levels // => ["Info", "Error"] * ``` * * @category references @@ -297,24 +235,20 @@ export const CurrentLogLevel: Context.Reference = references.CurrentLo * * **Example** (Tracking log spans) * - * ```ts - * import { Console, Effect, References } from "effect" + * ```ts import.meta.vitest + * import { Effect, References } from "effect" * * const logSpanExample = Effect.gen(function*() { * // Get current spans (empty by default) * const current = yield* References.CurrentLogSpans - * console.log(current.length) // 0 + * const defaultCount = current.length * * // Add a log span manually * const databaseConnectionStartedAt = 0 - * yield* Effect.provideService( + * const database = yield* Effect.provideService( * Effect.gen(function*() { - * // Simulate some work - * yield* Effect.sleep("100 millis") - * yield* Console.log("Database operation in progress") - * * const spans = yield* References.CurrentLogSpans - * console.log("Active spans:", spans.map(([label]) => label)) // ["database-connection"] + * return spans.map(([label]) => label) * }), * References.CurrentLogSpans, * [["database-connection", databaseConnectionStartedAt]] @@ -322,12 +256,10 @@ export const CurrentLogLevel: Context.Reference = references.CurrentLo * * // Add another span * const dataProcessingStartedAt = 100 - * yield* Effect.provideService( + * const processing = yield* Effect.provideService( * Effect.gen(function*() { * const spans = yield* References.CurrentLogSpans - * console.log("Active spans:", spans.map(([label]) => label)) // ["database-connection", "data-processing"] - * - * yield* Console.log("Multiple operations in progress") + * return spans.map(([label]) => label) * }), * References.CurrentLogSpans, * [ @@ -337,15 +269,19 @@ export const CurrentLogLevel: Context.Reference = references.CurrentLo * ) * * // Clear spans when operations complete - * yield* Effect.provideService( + * const cleared = yield* Effect.provideService( * Effect.gen(function*() { * const spans = yield* References.CurrentLogSpans - * console.log("Active spans:", spans.length) // 0 + * return spans.length * }), * References.CurrentLogSpans, * [] * ) + * + * return [defaultCount, database, processing, cleared] * }) + * + * await Effect.runPromise(logSpanExample) // => [0, ["database-connection"], ["database-connection", "data-processing"], 0] * ``` * * @category references @@ -384,47 +320,27 @@ export const CurrentStackFrame: Context.Reference = refe * * Use to filter out log entries below a severity threshold. * - * **Example** (Setting the minimum log level) - * - * ```ts - * import { Console, Effect, References } from "effect" - * - * const configureMinimumLogging = Effect.gen(function*() { - * // Get current minimum level (default is "Info") - * const current = yield* References.MinimumLogLevel - * console.log(current) // "Info" + * **Example** (Filtering logs below the minimum level) * - * // Set minimum level to Warn - Debug and Info will be filtered - * yield* Effect.provideService( - * Effect.gen(function*() { - * const minLevel = yield* References.MinimumLogLevel - * console.log(minLevel) // "Warn" - * - * // These won't be processed at all - * yield* Console.debug("Debug message") // Filtered out - * yield* Console.info("Info message") // Filtered out + * ```ts import.meta.vitest + * import { Effect, Logger, References } from "effect" * - * // These will be processed - * yield* Console.warn("Warning message") // Shown - * yield* Console.error("Error message") // Shown - * }), - * References.MinimumLogLevel, - * "Warn" - * ) - * - * // Reset to default Info level - * yield* Effect.provideService( - * Effect.gen(function*() { - * const minLevel = yield* References.MinimumLogLevel - * console.log(minLevel) // "Info" + * const levels: Array = [] + * const logger = Logger.make(({ logLevel }) => { + * levels.push(logLevel) + * }) * - * // Now info messages will be processed - * yield* Console.info("Info message") // Shown - * }), - * References.MinimumLogLevel, - * "Info" - * ) + * const program = Effect.gen(function*() { + * yield* Effect.logInfo("filtered out") + * yield* Effect.logWarning("included at the threshold") + * yield* Effect.logError("included above the threshold") * }) + * + * await Effect.runPromise(program.pipe( + * Effect.provideService(References.MinimumLogLevel, "Warn"), + * Effect.provide(Logger.layer([logger])) + * )) + * levels // => ["Warn", "Error"] * ``` * * @category references @@ -442,40 +358,31 @@ export const MinimumLogLevel: Context.Reference = references.MinimumLo * * **Example** (Toggling tracing) * - * ```ts + * ```ts import.meta.vitest * import { Effect, References } from "effect" * * const tracingControl = Effect.gen(function*() { * // Check if tracing is enabled (default is true) * const current = yield* References.TracerEnabled - * console.log(current) // true * * // Disable tracing globally - * yield* Effect.provideService( - * Effect.gen(function*() { - * const isEnabled = yield* References.TracerEnabled - * console.log(isEnabled) // false - * - * // Spans will not be traced in this context - * yield* Effect.log("This will not be traced") - * }), + * const disabled = yield* Effect.provideService( + * References.TracerEnabled, * References.TracerEnabled, * false * ) * * // Re-enable tracing - * yield* Effect.provideService( - * Effect.gen(function*() { - * const isEnabled = yield* References.TracerEnabled - * console.log(isEnabled) // true - * - * // All subsequent spans will be traced - * yield* Effect.log("This will be traced") - * }), + * const enabled = yield* Effect.provideService( + * References.TracerEnabled, * References.TracerEnabled, * true * ) + * + * return [current, disabled, enabled] * }) + * + * await Effect.runPromise(tracingControl) // => [true, false, true] * ``` * * @category references @@ -493,27 +400,20 @@ export const TracerEnabled: Context.Reference = references.TracerEnable * * **Example** (Managing span annotations) * - * ```ts + * ```ts import.meta.vitest * import { Effect, References } from "effect" * * const spanAnnotationExample = Effect.gen(function*() { * // Get current annotations (empty by default) * const current = yield* References.TracerSpanAnnotations - * console.log(current) // {} + * const defaultCount = Object.keys(current).length * * // Set global span annotations - * yield* Effect.provideService( + * const configured = yield* Effect.provideService( * Effect.gen(function*() { * // Get current annotations * const annotations = yield* References.TracerSpanAnnotations - * console.log(annotations) // { service: "user-service", version: "1.2.3", environment: "production" } - * - * // All spans created will include these annotations - * yield* Effect.gen(function*() { - * // Add more specific annotations for this span - * yield* Effect.annotateCurrentSpan("userId", "123") - * yield* Effect.log("Processing user") - * }) + * return [annotations.service, annotations.version, annotations.environment] * }), * References.TracerSpanAnnotations, * { @@ -524,15 +424,19 @@ export const TracerEnabled: Context.Reference = references.TracerEnable * ) * * // Clear annotations - * yield* Effect.provideService( + * const cleared = yield* Effect.provideService( * Effect.gen(function*() { * const annotations = yield* References.TracerSpanAnnotations - * console.log(annotations) // {} + * return Object.keys(annotations).length * }), * References.TracerSpanAnnotations, * {} * ) + * + * return [defaultCount, configured, cleared] * }) + * + * await Effect.runPromise(spanAnnotationExample) // => [0, ["user-service", "1.2.3", "production"], 0] * ``` * * @category references @@ -551,13 +455,13 @@ export const TracerSpanAnnotations: Context.Reference links.length), * References.TracerSpanLinks, * [spanLink] * ) * * // Clear links - * yield* Effect.provideService( - * Effect.gen(function*() { - * const links = yield* References.TracerSpanLinks - * console.log(links.length) // 0 - * }), + * const clearedCount = yield* Effect.provideService( + * Effect.map(References.TracerSpanLinks, (links) => links.length), * References.TracerSpanLinks, * [] * ) + * + * return [defaultCount, configuredCount, clearedCount] * }) + * + * await Effect.runPromise(spanLinksExample) // => [0, 1, 0] * ``` * * @category references @@ -619,36 +514,31 @@ export const TracerSpanLinks: Context.Reference> = refer * * **Example** (Toggling trace timing) * - * ```ts + * ```ts import.meta.vitest * import { Effect, References } from "effect" * * const tracingControl = Effect.gen(function*() { * // Check if trace timing is enabled (default is true) * const current = yield* References.TracerTimingEnabled - * console.log(current) // true * * // Disable trace timing globally - * yield* Effect.provideService( - * Effect.gen(function*() { - * // Spans will not having timing information in this context - * const isEnabled = yield* References.TracerTimingEnabled - * console.log(isEnabled) // false - * }), + * const disabled = yield* Effect.provideService( + * References.TracerTimingEnabled, * References.TracerTimingEnabled, * false * ) * * // Re-enable trace timing - * yield* Effect.provideService( - * Effect.gen(function*() { - * // Spans will have timing information in this context - * const isEnabled = yield* References.TracerTimingEnabled - * console.log(isEnabled) // true - * }), + * const enabled = yield* Effect.provideService( + * References.TracerTimingEnabled, * References.TracerTimingEnabled, * true * ) + * + * return [current, disabled, enabled] * }) + * + * await Effect.runPromise(tracingControl) // => [true, false, true] * ``` * * @category references @@ -756,27 +646,25 @@ export { * * **Example** (Providing a custom scheduler) * - * ```ts + * ```ts import.meta.vitest * import { Effect, References, Scheduler } from "effect" * * const customScheduling = Effect.gen(function*() { * // Get current scheduler (default is MixedScheduler) * const current = yield* References.Scheduler - * console.log(current) // MixedScheduler instance + * const isDefaultMixed = current instanceof Scheduler.MixedScheduler * * // Use a custom scheduler - * yield* Effect.provideService( - * Effect.gen(function*() { - * const scheduler = yield* References.Scheduler - * console.log(scheduler) // Custom scheduler instance - * - * // Effects will use the custom scheduler in this context - * yield* Effect.log("Using custom scheduler") - * }), + * const isCustomMixed = yield* Effect.provideService( + * Effect.map(References.Scheduler, (scheduler) => scheduler instanceof Scheduler.MixedScheduler), * References.Scheduler, * new Scheduler.MixedScheduler() * ) + * + * return [isDefaultMixed, isCustomMixed] * }) + * + * await Effect.runPromise(customScheduling) // => [true, true] * ``` * * @category references diff --git a/.context/effect/packages/effect/src/RegExp.ts b/.context/effect/packages/effect/src/RegExp.ts index 0123914f8..c1c97ea3e 100644 --- a/.context/effect/packages/effect/src/RegExp.ts +++ b/.context/effect/packages/effect/src/RegExp.ts @@ -22,15 +22,13 @@ import * as predicate from "./Predicate.ts" * * **Example** (Creating a regular expression) * - * ```ts + * ```ts import.meta.vitest * import { RegExp } from "effect" * - * // Create a regular expression using Effect's RegExp constructor * const pattern = new RegExp.RegExp("hello", "i") - * - * // Test the pattern - * console.log(pattern.test("Hello World")) // true - * console.log(pattern.test("goodbye")) // false + * pattern // => /hello/i + * pattern.test("Hello World") // => true + * pattern.test("goodbye") // => false * ``` * * @category constructors @@ -47,12 +45,11 @@ export const RegExp = globalThis.RegExp * * **Example** (Checking for regular expressions) * - * ```ts + * ```ts import.meta.vitest * import { RegExp } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(RegExp.isRegExp(/a/), true) - * assert.deepStrictEqual(RegExp.isRegExp("a"), false) + * RegExp.isRegExp(/a/) // => true + * RegExp.isRegExp("a") // => false * ``` * * @category guards @@ -69,14 +66,13 @@ export const isRegExp: (input: unknown) => input is RegExp = predicate.isRegExp * * **Example** (Escaping a pattern string) * - * ```ts + * ```ts import.meta.vitest * import { RegExp } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(RegExp.escape("a*b"), "a\\*b") + * RegExp.escape("a*b") // => "a\\*b" * ``` * - * @category RegExp + * @category transforming * @since 2.0.0 */ export const escape = (string: string): string => string.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&") diff --git a/.context/effect/packages/effect/src/Request.ts b/.context/effect/packages/effect/src/Request.ts index 5eb39ffb0..5ccb1032e 100644 --- a/.context/effect/packages/effect/src/Request.ts +++ b/.context/effect/packages/effect/src/Request.ts @@ -17,6 +17,7 @@ import type * as Exit from "./Exit.ts" import { dual } from "./Function.ts" import * as core from "./internal/core.ts" import * as internalEffect from "./internal/effect.ts" +import * as InternalRecord from "./internal/record.ts" import { hasProperty } from "./Predicate.ts" import type * as Types from "./Types.ts" @@ -28,7 +29,7 @@ const TypeId = "~effect/Request" * * **Example** (Defining typed requests) * - * ```ts + * ```ts import.meta.vitest * import type { Request } from "effect" * * // Define a request that fetches a user by ID @@ -41,6 +42,7 @@ const TypeId = "~effect/Request" * interface GetAllUsers extends Request.Request, Error> { * readonly _tag: "GetAllUsers" * } + * * ``` * * @category models @@ -63,7 +65,7 @@ export interface Request extends Variance @@ -98,7 +100,7 @@ export interface Variance { * * **Example** (Using generated request constructors) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * interface GetUser extends Request.Request { @@ -108,7 +110,10 @@ export interface Variance { * * // Constructor type is used internally by Request.of() and Request.tagged() * const GetUser = Request.tagged("GetUser") - * const userRequest = GetUser({ id: 123 }) + * const request = GetUser({ id: 123 }) + * + * request._tag // => "GetUser" + * request.id // => 123 * ``` * * @category models @@ -123,7 +128,7 @@ export interface Constructor, T extends keyof R * * **Example** (Extracting a request error type) * - * ```ts + * ```ts import.meta.vitest * import type { Request } from "effect" * * interface GetUser extends Request.Request { @@ -132,6 +137,7 @@ export interface Constructor, T extends keyof R * * // Extract the error type from a Request using the utility * type UserError = Request.Error // Error + * * ``` * * @category utility types @@ -144,7 +150,7 @@ export type Error> = [T] extends [Request { @@ -154,6 +160,7 @@ export type Error> = [T] extends [Request // string + * * ``` * * @category utility types @@ -176,7 +183,7 @@ export type Services> = [T] extends [Request { @@ -186,6 +193,7 @@ export type Services> = [T] extends [Request // Exit.Exit + * * ``` * * @category utility types @@ -226,7 +234,7 @@ export const RequestPrototype: Request = { * * **Example** (Checking request values) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * declare const User: unique symbol @@ -241,8 +249,8 @@ export const RequestPrototype: Request = { * const GetUser = Request.tagged("GetUser") * * const request = GetUser({ id: "123" }) - * console.log(Request.isRequest(request)) // true - * console.log(Request.isRequest("not a request")) // false + * Request.isRequest(request) // => true + * Request.isRequest("not a request") // => false * ``` * * @category guards @@ -255,7 +263,7 @@ export const isRequest = (u: unknown): u is Request = * * **Example** (Creating untagged request constructors) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * declare const UserProfile: unique symbol @@ -274,13 +282,16 @@ export const isRequest = (u: unknown): u is Request = * id: "user-123", * includeSettings: true * }) + * + * request.id // => "user-123" + * request.includeSettings // => true * ``` * * @category constructors * @since 2.0.0 */ export const of = >(): Constructor => (args) => - Object.assign(Object.create(RequestPrototype), args) + Object.setPrototypeOf({ ...(args as R) }, RequestPrototype) /** * Creates a constructor function for a tagged Request type. The tag is automatically @@ -288,7 +299,7 @@ export const of = >(): Constructor => (args) * * **Example** (Creating tagged request constructors) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * declare const User: unique symbol @@ -317,8 +328,7 @@ export const of = >(): Constructor => (args) * const postRequest = GetPost({ id: "post-456" }) * * // _tag is automatically set - * console.log(userRequest._tag) // "GetUser" - * console.log(postRequest._tag) // "GetPost" + * Array.of(userRequest._tag, postRequest._tag) // => ["GetUser", "GetPost"] * ``` * * @category constructors @@ -328,10 +338,7 @@ export const tagged = & { _tag: string }>( tag: R["_tag"] ): Constructor => (args) => { - const request = Object.create(RequestPrototype) - if (args) Object.assign(request, args) - request._tag = tag - return request + return Object.setPrototypeOf({ ...(args as R), _tag: tag }, RequestPrototype) } /** @@ -344,7 +351,7 @@ export const tagged = & { _tag: string }>( * * **Example** (Defining request classes) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * class GetUser extends Request.Class<{ id: number }, string, Error> { @@ -354,7 +361,7 @@ export const tagged = & { _tag: string }>( * } * * const getUserRequest = new GetUser(123) - * console.log(getUserRequest.id) // 123 + * getUserRequest.id // => 123 * ``` * * @category constructors @@ -364,9 +371,9 @@ export const Class: new, Success, Error = never, C args: Types.Equals>, {}> extends true ? void : { readonly [P in keyof A as P extends keyof Request ? never : P]: A[P] } ) => Request & Readonly = (function() { - function Class(this: any, args: any) { + function Class(this: object, args: object | undefined) { if (args) { - Object.assign(this, args) + InternalRecord.assignProperties(this, args) } } Class.prototype = RequestPrototype @@ -383,7 +390,7 @@ export const Class: new, Success, Error = never, C * * **Example** (Defining tagged request classes) * - * ```ts + * ```ts import.meta.vitest * import { Request } from "effect" * * class GetUserById @@ -391,8 +398,9 @@ export const Class: new, Success, Error = never, C * {} * * const request = new GetUserById({ id: 123 }) - * console.log(request._tag) // "GetUserById" - * console.log(request.id) // 123 + * + * request._tag // => "GetUserById" + * request.id // => 123 * ``` * * @category constructors @@ -554,7 +562,7 @@ export const succeed: { * an `uninterruptible` flag used by batching and caching internals, and the * `completeUnsafe` callback used by resolvers to supply the final `Exit`. * - * @category entry + * @category models * @since 2.0.0 */ export interface Entry { @@ -580,7 +588,7 @@ export interface Entry { * most application code receives entries from a `RequestResolver` instead of * constructing them directly. * - * @category entry + * @category constructors * @since 2.0.0 */ export const makeEntry = (options: { diff --git a/.context/effect/packages/effect/src/RequestResolver.ts b/.context/effect/packages/effect/src/RequestResolver.ts index 33cce7167..2061d4b86 100644 --- a/.context/effect/packages/effect/src/RequestResolver.ts +++ b/.context/effect/packages/effect/src/RequestResolver.ts @@ -52,14 +52,14 @@ const TypeId = "~effect/RequestResolver" * * **Example** (Defining a request resolver) * - * ```ts - * import { Effect, Exit, RequestResolver } from "effect" - * import type { Request } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { * readonly _tag: "GetUserRequest" * readonly id: number * } + * const GetUserRequest = Request.tagged("GetUserRequest") * * // In practice, you would typically use RequestResolver.make() instead * const resolver = RequestResolver.make((entries) => @@ -69,6 +69,9 @@ const TypeId = "~effect/RequestResolver" * } * }) * ) + * + * const program = Effect.request(GetUserRequest({ id: 1 }), resolver) + * await Effect.runPromise(program) // => "User 1" * ``` * * @category models @@ -202,7 +205,7 @@ const defaultKey = (_request: unknown): unknown => defaultKeyObject * * **Example** (Creating a request resolver) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * // Define a request type @@ -224,6 +227,7 @@ const defaultKey = (_request: unknown): unknown => defaultKeyObject * * // Use the resolver to handle requests * const getUserEffect = Effect.request(GetUserRequest({ id: 123 }), UserResolver) + * await Effect.runPromise(getUserEffect) // => "User 123" * ``` * * @category constructors @@ -248,7 +252,7 @@ export const make = ( * * **Example** (Grouping requests by key) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserByRole extends Request.Request { @@ -258,12 +262,14 @@ export const make = ( * } * const GetUserByRole = Request.tagged("GetUserByRole") * + * const batches: Array<[role: string, size: number]> = [] + * * // Group requests by role for efficient batch processing * const UserByRoleResolver = RequestResolver.makeGrouped({ * key: ({ request }) => request.role, * resolver: (entries, role) => * Effect.sync(() => { - * console.log(`Processing ${entries.length} requests for role: ${role}`) + * batches.push([role, entries.length]) * for (const entry of entries) { * entry.completeUnsafe( * Exit.succeed(`User ${entry.request.id} with role ${role}`) @@ -271,6 +277,15 @@ export const make = ( * } * }) * }) + * + * const program = Effect.all([ + * Effect.request(GetUserByRole({ role: "admin", id: 1 }), UserByRoleResolver), + * Effect.request(GetUserByRole({ role: "admin", id: 2 }), UserByRoleResolver) + * ] as const, { concurrency: "unbounded" }) + * const result = await Effect.runPromise(program) + * + * batches // => [["admin", 2]] + * result // => ["User 1 with role admin", "User 2 with role admin"] * ``` * * @category constructors @@ -305,7 +320,7 @@ const hashGroupKey = (get: (entry: Request.Entry) => K) => { * * **Example** (Creating a resolver from a pure function) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Request, RequestResolver } from "effect" * * interface GetSquareRequest extends Request.Request { @@ -324,7 +339,7 @@ const hashGroupKey = (get: (entry: Request.Entry) => K) => { * GetSquareRequest({ value: 5 }), * SquareResolver * ) - * // Will resolve to 25 + * await Effect.runPromise(getSquareEffect) // => 25 * ``` * * @category constructors @@ -350,7 +365,7 @@ export const fromFunction = ( * * **Example** (Batching pure request handling) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Request, RequestResolver } from "effect" * * interface GetDoubleRequest extends Request.Request { @@ -368,7 +383,8 @@ export const fromFunction = ( * const effects = [1, 2, 3].map((value) => * Effect.request(GetDoubleRequest({ value }), DoubleResolver) * ) - * const batchedEffect = Effect.all(effects) // [2, 4, 6] + * const batchedEffect = Effect.all(effects) + * await Effect.runPromise(batchedEffect) // => [2, 4, 6] * ``` * * @category constructors @@ -393,7 +409,7 @@ export const fromFunctionBatched = ( * * **Example** (Creating a resolver from an effectful function) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Request, RequestResolver } from "effect" * * interface GetUserFromAPIRequest extends Request.Request { @@ -406,13 +422,7 @@ export const fromFunctionBatched = ( * * // Create a resolver that uses effects (like HTTP calls) * const UserAPIResolver = RequestResolver.fromEffect( - * (entry) => - * Effect.gen(function*() { - * // Simulate an API call - * yield* Effect.sleep("100 millis") - * // Just return the result without error handling for simplicity - * return `User ${entry.request.id} from API` - * }) + * (entry) => Effect.succeed(`User ${entry.request.id} from API`) * ) * * // Usage @@ -420,6 +430,7 @@ export const fromFunctionBatched = ( * GetUserFromAPIRequest({ id: 123 }), * UserAPIResolver * ) + * await Effect.runPromise(getUserEffect) // => "User 123 from API" * ``` * * @category constructors @@ -456,9 +467,8 @@ export const fromEffect = ( * * **Example** (Handling tagged request batches) * - * ```ts - * import { Effect, RequestResolver } from "effect" - * import type { Request } from "effect" + * ```ts import.meta.vitest + * import { Effect, Request, RequestResolver } from "effect" * * interface GetUser extends Request.Request { * readonly _tag: "GetUser" @@ -471,6 +481,8 @@ export const fromEffect = ( * } * * type MyRequest = GetUser | GetPost + * const GetUser = Request.tagged("GetUser") + * const GetPost = Request.tagged("GetPost") * * // Create a resolver that handles different request types * const MyResolver = RequestResolver.fromEffectTagged()({ @@ -479,6 +491,12 @@ export const fromEffect = ( * GetPost: (requests) => * Effect.succeed(requests.map((req) => `Post ${req.request.id}`)) * }) + * + * const program = Effect.all([ + * Effect.request(GetUser({ id: 1 }), MyResolver), + * Effect.request(GetPost({ id: 2 }), MyResolver) + * ] as const) + * await Effect.runPromise(program) // => ["User 1", "Post 2"] * ``` * * @category constructors @@ -534,7 +552,7 @@ export const fromEffectTagged = { @@ -550,17 +568,21 @@ export const fromEffectTagged = { + * delayRan = true * }) * ) + * + * await Effect.runPromise(resolverWithCustomDelay.delay) + * Array.of(delayRan, RequestResolver.isRequestResolver(resolverWithCustomDelay)) // => [true, true] * ``` * - * @category delay + * @category delays & timeouts * @since 4.0.0 */ export const setDelayEffect: { @@ -580,7 +602,7 @@ export const setDelayEffect: { * * **Example** (Setting a batch delay) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { @@ -599,11 +621,11 @@ export const setDelayEffect: { * // Add a 100ms delay to batch requests together * const delayedResolver = RequestResolver.setDelay(resolver, "100 millis") * - * // Can also use number for milliseconds - * const delayedResolver2 = RequestResolver.setDelay(resolver, 100) + * const program = Effect.request(GetDataRequest(), delayedResolver) + * await Effect.runPromise(program) // => "data" * ``` * - * @category delay + * @category delays & timeouts * @since 4.0.0 */ export const setDelay: { @@ -623,7 +645,7 @@ export const setDelay: { * * **Example** (Running effects around request resolution) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { @@ -631,6 +653,8 @@ export const setDelay: { * } * const GetDataRequest = Request.tagged("GetDataRequest") * + * const events: Array = [] + * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { * for (const entry of entries) { @@ -644,16 +668,20 @@ export const setDelay: { * resolver, * (entries) => * Effect.gen(function*() { - * yield* Effect.log(`Starting batch of ${entries.length} requests`) + * events.push(`Starting batch of ${entries.length} requests`) * return entries.length * }), * (entries, initialSize) => - * Effect.gen(function*() { - * yield* Effect.log( - * `Batch completed with ${entries.length} requests (started with ${initialSize})` - * ) + * Effect.sync(() => { + * events.push(`Batch completed with ${entries.length} requests (started with ${initialSize})`) * }) * ) + * + * const program = Effect.request(GetDataRequest(), resolverWithAround) + * const result = await Effect.runPromise(program) + * + * events // => ["Starting batch of 1 requests", "Batch completed with 1 requests (started with 1)"] + * result // => "data" * ``` * * @category combinators @@ -716,7 +744,7 @@ export const never: RequestResolver = make(() => Effect.never) * * **Example** (Limiting parallel request batches) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { @@ -725,9 +753,11 @@ export const never: RequestResolver = make(() => Effect.never) * } * const GetDataRequest = Request.tagged("GetDataRequest") * + * const batchSizes: Array = [] + * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { - * console.log(`Processing batch of ${entries.length} requests`) + * batchSizes.push(entries.length) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`data-${entry.request.id}`)) * } @@ -742,6 +772,13 @@ export const never: RequestResolver = make(() => Effect.never) * { length: 12 }, * (_, i) => Effect.request(GetDataRequest({ id: i }), limitedResolver) * ) + * + * const result = await Effect.runPromise(Effect.all(requests, { concurrency: "unbounded" })) + * batchSizes // => [5, 5, 2] + * + * result.length // => 12 + * + * Array.of(result[0], result[11]) // => ["data-0", "data-11"] * ``` * * @category combinators @@ -762,7 +799,7 @@ export const batchN: { * * **Example** (Grouping resolver requests) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetUserRequest extends Request.Request { @@ -772,9 +809,11 @@ export const batchN: { * } * const GetUserRequest = Request.tagged("GetUserRequest") * + * const batchSizes: Array = [] + * * const resolver = RequestResolver.make((entries) => * Effect.sync(() => { - * console.log(`Processing ${entries.length} users`) + * batchSizes.push(entries.length) * for (const entry of entries) { * entry.completeUnsafe(Exit.succeed(`User ${entry.request.userId}`)) * } @@ -802,6 +841,13 @@ export const batchN: { * groupedResolver * ) * ] + * + * const result = await Effect.runPromise(Effect.all(requests, { concurrency: "unbounded" })) + * batchSizes.sort() + * + * batchSizes // => [1, 2] + * + * result // => ["User 1", "User 2", "User 3"] * ``` * * @category combinators @@ -830,7 +876,7 @@ export const grouped: { * * **Example** (Racing request resolvers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { @@ -861,6 +907,8 @@ export const grouped: { * * // Race resolvers - will use whichever completes first * const racingResolver = RequestResolver.race(fastResolver, slowResolver) + * const program = Effect.request(GetDataRequest({ id: 1 }), racingResolver) + * await Effect.runPromise(program) // => "fast-1" * ``` * * @category combinators @@ -888,7 +936,7 @@ export const race: { * * **Example** (Adding a tracing span) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Request, RequestResolver } from "effect" * * interface GetDataRequest extends Request.Request { @@ -919,6 +967,7 @@ export const race: { * * // Spans will automatically include batch size and request links * const effect = Effect.request(GetDataRequest({ id: 123 }), tracedResolver) + * await Effect.runPromise(effect) // => "data-123" * ``` * * @category combinators @@ -1159,7 +1208,7 @@ export const withCache: { * @see {@link withCache} for in-memory resolver caching that does not require persistable request values or a persistence store * @see {@link asCache} for exposing resolver results through a `Cache` instead of returning another resolver * - * @category Persistence + * @category caching * @since 4.0.0 */ export const persisted: { diff --git a/.context/effect/packages/effect/src/Result.ts b/.context/effect/packages/effect/src/Result.ts index c1c105247..eb7a4e014 100644 --- a/.context/effect/packages/effect/src/Result.ts +++ b/.context/effect/packages/effect/src/Result.ts @@ -17,6 +17,7 @@ import type { TypeLambda } from "./HKT.ts" import type { Inspectable } from "./Inspectable.ts" import * as doNotation from "./internal/doNotation.ts" import * as option_ from "./internal/option.ts" +import * as InternalRecord from "./internal/record.ts" import * as result from "./internal/result.ts" import type { Option } from "./Option.ts" import type { Pipeable } from "./Pipeable.ts" @@ -46,18 +47,13 @@ const TypeId = "~effect/data/Result" * * **Example** (Creating and matching a Result) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const success = Result.succeed(42) - * const failure = Result.fail("something went wrong") - * - * const message = Result.match(success, { + * Result.match(Result.succeed(42), { * onSuccess: (value) => `Success: ${value}`, * onFailure: (error) => `Error: ${error}` - * }) - * console.log(message) - * // Output: "Success: 42" + * }) // => "Success: 42" * ``` * * @see {@link succeed} / {@link fail} to create values @@ -80,14 +76,13 @@ export type Result = Success | Failure * * **Example** (Accessing the failure value) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * * const failure = Result.fail("Network error") * * if (Result.isFailure(failure)) { - * console.log(failure.failure) - * // Output: "Network error" + * failure.failure // => "Network error" * } * ``` * @@ -143,14 +138,13 @@ export interface ResultIterator> { * * **Example** (Accessing the success value) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * * const success = Result.succeed(42) * * if (Result.isSuccess(success)) { - * console.log(success.success) - * // Output: 42 + * success.success // => 42 * } * ``` * @@ -212,7 +206,7 @@ export interface ResultUnifyIgnore {} * (e.g., `map`, `flatMap` abstractions). You typically do not need to * reference this directly. * - * @category type lambdas + * @category utility types * @since 4.0.0 */ export interface ResultTypeLambda extends TypeLambda { @@ -225,8 +219,8 @@ export interface ResultTypeLambda extends TypeLambda { * * **Example** (Extracting inner types) * - * ```ts - * import type { Result } from "effect" + * ```ts import.meta.vitest + * import { Result } from "effect" * * type R = Result.Result * @@ -235,6 +229,9 @@ export interface ResultTypeLambda extends TypeLambda { * * // string * type E = Result.Result.Failure + * + * const success: A = 42 + * const failure: E = "error" * ``` * * @since 4.0.0 @@ -243,14 +240,14 @@ export declare namespace Result { /** * Extracts the failure type `E` from `Result`. * - * @category Type Level + * @category utility types * @since 4.0.0 */ export type Failure> = [T] extends [Result] ? _E : never /** * Extracts the success type `A` from `Result`. * - * @category Type Level + * @category utility types * @since 4.0.0 */ export type Success> = [T] extends [Result] ? _A : never @@ -266,13 +263,10 @@ export declare namespace Result { * * **Example** (Wrapping a value) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const result = Result.succeed(42) - * - * console.log(Result.isSuccess(result)) - * // Output: true + * Result.succeed(42) // => Result.succeed(42) * ``` * * @see {@link fail} to create a Failure @@ -296,13 +290,10 @@ export const succeed: (right: A) => Result = result.succeed * * **Example** (Creating a failure) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const result = Result.fail("Something went wrong") - * - * console.log(Result.isFailure(result)) - * // Output: true + * Result.fail("Something went wrong") // => Result.fail("Something went wrong") * ``` * * @see {@link succeed} to create a Success @@ -330,13 +321,10 @@ export { * * **Example** (Referencing void results) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const result: Result.Result = Result.void - * - * console.log(Result.isSuccess(result)) - * // Output: true + * const result: Result.Result = Result.void // => Result.succeed(undefined) * ``` * * @see {@link succeed} to create a Success with a specific value @@ -363,13 +351,10 @@ export { * * **Example** (Failing without a payload) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const result = Result.failVoid - * - * console.log(Result.isFailure(result)) - * // Output: true + * Result.failVoid // => Result.fail(undefined) * ``` * * @see {@link fail} to create a Failure with a specific value @@ -396,14 +381,12 @@ export const failVoid: Result = fail(void 0) * * **Example** (Handling nullable values) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.fromNullishOr(1, () => "fallback")) - * // Output: { _tag: "Success", success: 1, ... } + * Result.fromNullishOr(1, () => "fallback") // => Result.succeed(1) * - * console.log(Result.fromNullishOr(null, () => "fallback")) - * // Output: { _tag: "Failure", failure: "fallback", ... } + * Result.fromNullishOr(null, () => "fallback") // => Result.fail("fallback") * ``` * * @see {@link fromOption} to convert from an Option @@ -437,16 +420,12 @@ export const fromNullishOr: { * * **Example** (Converting an Option to a Result) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * const some = Result.fromOption(Option.some(1), () => "missing") - * console.log(some) - * // Output: { _tag: "Success", success: 1, ... } + * Result.fromOption(Option.some(1), () => "missing") // => Result.succeed(1) * - * const none = Result.fromOption(Option.none(), () => "missing") - * console.log(none) - * // Output: { _tag: "Failure", failure: "missing", ... } + * Result.fromOption(Option.none(), () => "missing") // => Result.fail("missing") * ``` * * @see {@link getSuccess} to extract the success value as an Option @@ -503,19 +482,16 @@ export { * * **Example** (Catching JSON parse errors) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const ok = Result.try(() => JSON.parse('{"name": "Alice"}')) - * console.log(ok) - * // Output: { _tag: "Success", success: { name: "Alice" }, ... } + * Result.try(() => JSON.parse('{"name": "Alice"}')) // => Result.succeed({ name: "Alice" }) * * const err = Result.try({ * try: () => JSON.parse("not json"), * catch: (e) => `Parse failed: ${e}` * }) - * console.log(Result.isFailure(err)) - * // Output: true + * Result.isFailure(err) // => true * ``` * * @see {@link succeed} / {@link fail} for direct construction @@ -541,14 +517,12 @@ export { * * **Example** (Checking if a value is a Result) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.isResult(Result.succeed(1))) - * // Output: true + * Result.isResult(Result.succeed(1)) // => true * - * console.log(Result.isResult({ value: 1 })) - * // Output: false + * Result.isResult({ value: 1 }) // => false * ``` * * @see {@link isSuccess} / {@link isFailure} to narrow to a specific variant @@ -572,14 +546,13 @@ export const isResult: (input: unknown) => input is Result = r * * **Example** (Narrowing to failure) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * * const result = Result.fail("oops") * * if (Result.isFailure(result)) { - * console.log(result.failure) - * // Output: "oops" + * result.failure // => "oops" * } * ``` * @@ -605,14 +578,13 @@ export const isFailure: (self: Result) => self is Failure = re * * **Example** (Narrowing to success) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * * const result = Result.succeed(42) * * if (Result.isSuccess(result)) { - * console.log(result.success) - * // Output: 42 + * result.success // => 42 * } * ``` * @@ -639,14 +611,12 @@ export const isSuccess: (self: Result) => self is Success = re * * **Example** (Extracting the success as an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * console.log(Result.getSuccess(Result.succeed("ok"))) - * // Output: { _tag: "Some", value: "ok" } + * Result.getSuccess(Result.succeed("ok")) // => Option.some("ok") * - * console.log(Result.getSuccess(Result.fail("err"))) - * // Output: { _tag: "None" } + * Result.getSuccess(Result.fail("err")) // => Option.none() * ``` * * @see {@link getFailure} to extract the error instead @@ -672,14 +642,12 @@ export const getSuccess: (self: Result) => Option = result.getSuc * * **Example** (Extracting the failure as an Option) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * console.log(Result.getFailure(Result.succeed("ok"))) - * // Output: { _tag: "None" } + * Result.getFailure(Result.succeed("ok")) // => Option.none() * - * console.log(Result.getFailure(Result.fail("err"))) - * // Output: { _tag: "Some", value: "err" } + * Result.getFailure(Result.fail("err")) // => Option.some("err") * ``` * * @see {@link getSuccess} to extract the success instead @@ -701,7 +669,7 @@ export const getFailure: (self: Result) => Option = result.getFai * * **Example** (Comparing Results for equality) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Result } from "effect" * * const eq = Result.makeEquivalence( @@ -709,11 +677,9 @@ export const getFailure: (self: Result) => Option = result.getFai * Equivalence.strictEqual() * ) * - * console.log(eq(Result.succeed(1), Result.succeed(1))) - * // Output: true + * eq(Result.succeed(1), Result.succeed(1)) // => true * - * console.log(eq(Result.succeed(1), Result.fail("x"))) - * // Output: false + * eq(Result.succeed(1), Result.fail("x")) // => false * ``` * * @category instances @@ -744,18 +710,16 @@ export const makeEquivalence = ( * * **Example** (Mapping both channels) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.succeed(1), * Result.mapBoth({ * onSuccess: (n) => n + 1, * onFailure: (e) => `Error: ${e}` * }) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: 2, ... } + * ) // => Result.succeed(2) * ``` * * @see {@link map} to transform only the success value @@ -796,15 +760,13 @@ export const mapBoth: { * * **Example** (Adding context to an error) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.fail("not found"), * Result.mapError((e) => `Error: ${e}`) - * ) - * console.log(result) - * // Output: { _tag: "Failure", failure: "Error: not found", ... } + * ) // => Result.fail("Error: not found") * ``` * * @see {@link map} to transform only the success value @@ -819,7 +781,7 @@ export const mapError: { } = dual( 2, (self: Result, f: (err: E) => E2): Result => - isFailure(self) ? fail(f(self.failure)) : succeed(self.success) + isFailure(self) ? fail(f(self.failure)) : self as unknown as Result ) /** @@ -838,15 +800,13 @@ export const mapError: { * * **Example** (Doubling the success value) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.succeed(3), * Result.map((n) => n * 2) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: 6, ... } + * ) // => Result.succeed(6) * ``` * * @see {@link mapError} to transform only the error value @@ -862,7 +822,7 @@ export const map: { } = dual( 2, (self: Result, f: (ok: A) => A2): Result => - isSuccess(self) ? succeed(f(self.success)) : fail(self.failure) + isSuccess(self) ? succeed(f(self.success)) : self as unknown as Result ) /** @@ -881,7 +841,7 @@ export const map: { * * **Example** (Folding to a string) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * * const format = Result.match({ @@ -889,11 +849,9 @@ export const map: { * onFailure: (e: string) => `Err: ${e}` * }) * - * console.log(format(Result.succeed(42))) - * // Output: "Got 42" + * format(Result.succeed(42)) // => "Got 42" * - * console.log(format(Result.fail("timeout"))) - * // Output: "Err: timeout" + * format(Result.fail("timeout")) // => "Err: timeout" * ``` * * @see {@link merge} to extract `A | E` without mapping @@ -936,18 +894,16 @@ export const match: { * * **Example** (Validating a number) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const ensurePositive = pipe( + * pipe( * 5, * Result.liftPredicate( * (n: number) => n > 0, * (n) => `${n} is not positive` * ) - * ) - * console.log(ensurePositive) - * // Output: { _tag: "Success", success: 5, ... } + * ) // => Result.succeed(5) * ``` * * @see {@link filterOrFail} to validate a value that is already in a `Result` @@ -997,18 +953,16 @@ export const liftPredicate: { * * **Example** (Filtering a success value) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.succeed(0), * Result.filterOrFail( * (n) => n > 0, * (n) => `${n} is not positive` * ) - * ) - * console.log(result) - * // Output: { _tag: "Failure", failure: "0 is not positive", ... } + * ) // => Result.fail("0 is not positive") * ``` * * @see {@link liftPredicate} to create a `Result` from a raw value with a predicate @@ -1050,14 +1004,12 @@ export const filterOrFail: { * * **Example** (Extracting the inner value) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.merge(Result.succeed(42))) - * // Output: 42 + * Result.merge(Result.succeed(42)) // => 42 * - * console.log(Result.merge(Result.fail("error"))) - * // Output: "error" + * Result.merge(Result.fail("error")) // => "error" * ``` * * @see {@link match} to map each branch to a common type @@ -1084,14 +1036,12 @@ export const merge: (self: Result) => E | A = match({ onFailure: ide * * **Example** (Providing a fallback) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.getOrElse(Result.succeed(1), () => 0)) - * // Output: 1 + * Result.getOrElse(Result.succeed(1), () => 0) // => 1 * - * console.log(Result.getOrElse(Result.fail("err"), () => 0)) - * // Output: 0 + * Result.getOrElse(Result.fail("err"), () => 0) // => 0 * ``` * * @see {@link getOrNull} / {@link getOrUndefined} for simpler fallbacks @@ -1126,14 +1076,12 @@ export const getOrElse: { * * **Example** (Unwrapping to nullable) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.getOrNull(Result.succeed(1))) - * // Output: 1 + * Result.getOrNull(Result.succeed(1)) // => 1 * - * console.log(Result.getOrNull(Result.fail("err"))) - * // Output: null + * Result.getOrNull(Result.fail("err")) // => null * ``` * * @see {@link getOrUndefined} to return `undefined` instead @@ -1159,14 +1107,12 @@ export const getOrNull: (self: Result) => A | null = getOrElse(const * * **Example** (Unwrapping to optional) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.getOrUndefined(Result.succeed(1))) - * // Output: 1 + * Result.getOrUndefined(Result.succeed(1)) // => 1 * - * console.log(Result.getOrUndefined(Result.fail("err"))) - * // Output: undefined + * Result.getOrUndefined(Result.fail("err")) // => undefined * ``` * * @see {@link getOrNull} to return `null` instead @@ -1192,19 +1138,19 @@ export const getOrUndefined: (self: Result) => A | undefined = getOr * * **Example** (Throwing a custom error) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log( - * Result.getOrThrowWith(Result.succeed(1), () => new Error("fail")) - * ) - * // Output: 1 + * Result.getOrThrowWith(Result.succeed(1), () => new Error("fail")) // => 1 * - * // This would throw: new Error("Unexpected: oops") - * // Result.getOrThrowWith( - * // Result.fail("oops"), - * // (err) => new Error(`Unexpected: ${err}`) - * // ) + * const failure = Result.try({ + * try: () => Result.getOrThrowWith( + * Result.fail("oops"), + * (error) => new Error(`Unexpected: ${error}`) + * ), + * catch: (error) => (error as Error).message + * }) + * Result.merge(failure) // => "Unexpected: oops" * ``` * * @see {@link getOrThrow} to throw the raw failure value @@ -1238,14 +1184,13 @@ export const getOrThrowWith: { * * **Example** (Unwrapping or throwing) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * console.log(Result.getOrThrow(Result.succeed(1))) - * // Output: 1 + * Result.getOrThrow(Result.succeed(1)) // => 1 * - * // This would throw the string "error": - * // Result.getOrThrow(Result.fail("error")) + * const failure = Result.try(() => Result.getOrThrow(Result.fail("error"))) + * Result.merge(failure) // => "error" * ``` * * @see {@link getOrThrowWith} for custom error mapping @@ -1272,15 +1217,13 @@ export const getOrThrow: (self: Result) => A = getOrThrowWith(identi * * **Example** (Recovering from a failure) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.fail("primary failed"), * Result.orElse(() => Result.succeed(99)) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: 99, ... } + * ) // => Result.succeed(99) * ``` * * @see {@link getOrElse} to unwrap with a fallback value (not a Result) @@ -1315,17 +1258,15 @@ export const orElse: { * * **Example** (Validating sequentially) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.succeed(5), * Result.flatMap((n) => * n > 0 ? Result.succeed(n * 2) : Result.fail("not positive") * ) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: 10, ... } + * ) // => Result.succeed(10) * ``` * * @see {@link andThen} for a more flexible variant that also accepts plain values @@ -1363,25 +1304,23 @@ export const flatMap: { * * **Example** (Chaining Result values with different argument types) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * * // With a function returning a Result * const a = pipe( * Result.succeed(1), * Result.andThen((n) => Result.succeed(n + 1)) - * ) + * ) // => Result.succeed(2) * * // With a plain mapping function * const b = pipe( * Result.succeed(1), * Result.andThen((n) => n + 1) - * ) + * ) // => Result.succeed(2) * * // With a constant value - * const c = pipe(Result.succeed(1), Result.andThen("done")) - * - * console.log(a, b, c) + * const c = pipe(Result.succeed(1), Result.andThen("done")) // => Result.succeed("done") * ``` * * @see {@link flatMap} for the stricter variant (function returning Result only) @@ -1430,18 +1369,14 @@ export const andThen: { * * **Example** (Collecting a tuple and a struct) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * * // Tuple - * const tuple = Result.all([Result.succeed(1), Result.succeed("two")]) - * console.log(tuple) - * // Output: { _tag: "Success", success: [1, "two"], ... } + * Result.all([Result.succeed(1), Result.succeed("two")]) // => Result.succeed([1, "two"]) * * // Struct - * const struct = Result.all({ x: Result.succeed(1), y: Result.fail("err") }) - * console.log(struct) - * // Output: { _tag: "Failure", failure: "err", ... } + * Result.all({ x: Result.succeed(1), y: Result.fail("err") }) // => Result.fail("err") * ``` * * @see {@link flatMap} for chaining two Results sequentially @@ -1481,7 +1416,7 @@ export const all: > | Record> | Record Result.fail(42) * - * console.log(Result.flip(Result.fail("error"))) - * // Output: { _tag: "Success", success: "error", ... } + * Result.flip(Result.fail("error")) // => Result.succeed("error") * ``` * * @see {@link mapError} to transform the error without swapping @@ -1538,17 +1471,14 @@ export const flip = (self: Result): Result => * * **Example** (Composing multiple Results) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * - * const result = Result.gen(function*() { + * Result.gen(function*() { * const a = yield* Result.succeed(1) * const b = yield* Result.succeed(2) * return a + b - * }) - * - * console.log(result) - * // Output: { _tag: "Success", success: 3, ... } + * }) // => Result.succeed(3) * ``` * * @see {@link flatMap} for point-free sequential composition @@ -1592,17 +1522,15 @@ export const gen: Gen.Gen = (...args) => { * * **Example** (Building an object step by step) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.Do, * Result.bind("x", () => Result.succeed(2)), * Result.bind("y", () => Result.succeed(3)), * Result.let("sum", ({ x, y }) => x + y) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: { x: 2, y: 3, sum: 5 }, ... } + * ) // => Result.succeed({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link bind} to add Result-producing fields @@ -1610,7 +1538,7 @@ export const gen: Gen.Gen = (...args) => { * @see {@link gen} for an alternative generator-based syntax * @see {@link bindTo} for starting a do-notation chain from an existing Result * - * @category do notation + * @category constructors * @since 2.0.0 */ export const Do: Result<{}> = succeed({}) @@ -1633,23 +1561,21 @@ export const Do: Result<{}> = succeed({}) * * **Example** (Binding Result values) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.Do, * Result.bind("x", () => Result.succeed(2)), * Result.bind("y", ({ x }) => Result.succeed(x + 3)) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: { x: 2, y: 5 }, ... } + * ) // => Result.succeed({ x: 2, y: 5 }) * ``` * * @see {@link Do} to start the do-notation chain * @see {@link let_ let} for pure computed fields * @see {@link bindTo} to wrap an initial Result into a named field * - * @category do notation + * @category sequencing * @since 2.0.0 */ export const bind: { @@ -1680,21 +1606,19 @@ export const bind: { * * **Example** (Wrapping a value into a named field) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.succeed(42), * Result.bindTo("answer") - * ) - * console.log(result) - * // Output: { _tag: "Success", success: { answer: 42 }, ... } + * ) // => Result.succeed({ answer: 42 }) * ``` * * @see {@link Do} to start from an empty object * @see {@link bind} to add more fields * - * @category do notation + * @category mapping * @since 2.0.0 */ export const bindTo: { @@ -1731,23 +1655,21 @@ export { * * **Example** (Adding a computed field) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * - * const result = pipe( + * pipe( * Result.Do, * Result.bind("x", () => Result.succeed(2)), * Result.bind("y", () => Result.succeed(3)), * Result.let("sum", ({ x, y }) => x + y) - * ) - * console.log(result) - * // Output: { _tag: "Success", success: { x: 2, y: 3, sum: 5 }, ... } + * ) // => Result.succeed({ x: 2, y: 3, sum: 5 }) * ``` * * @see {@link Do} to start the do-notation chain * @see {@link bind} for Result-producing fields * - * @category do notation + * @category mapping * @since 2.0.0 */ let_ as let @@ -1769,21 +1691,17 @@ export { * * **Example** (Transposing an Option of a Result) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * - * const some = Option.some(Result.succeed(42)) - * console.log(Result.transposeOption(some)) - * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * Result.transposeOption(Option.some(Result.succeed(42))) // => Result.succeed(Option.some(42)) * - * const none = Option.none>() - * console.log(Result.transposeOption(none)) - * // Output: { _tag: "Success", success: { _tag: "None" }, ... } + * Result.transposeOption(Option.none>()) // => Result.succeed(Option.none()) * ``` * * @see {@link transposeMapOption} to map and transpose in one step * - * @category Transposing + * @category transposing * @since 3.14.0 */ export const transposeOption = ( @@ -1809,7 +1727,7 @@ export const transposeOption = ( * * **Example** (Mapping and transposing in one step) * - * ```ts + * ```ts import.meta.vitest * import { Option, Result } from "effect" * * const parse = (s: string) => @@ -1817,16 +1735,14 @@ export const transposeOption = ( * ? Result.fail("not a number" as const) * : Result.succeed(Number(s)) * - * console.log(Result.transposeMapOption(Option.some("42"), parse)) - * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * Result.transposeMapOption(Option.some("42"), parse) // => Result.succeed(Option.some(42)) * - * console.log(Result.transposeMapOption(Option.none(), parse)) - * // Output: { _tag: "Success", success: { _tag: "None" }, ... } + * Result.transposeMapOption(Option.none(), parse) // => Result.succeed(Option.none()) * ``` * * @see {@link transposeOption} when the Option already contains a Result * - * @category Transposing + * @category transposing * @since 3.15.0 */ export const transposeMapOption = dual< @@ -1854,11 +1770,10 @@ export const transposeMapOption = dual< * * **Example** (Succeeding with None) * - * ```ts - * import { Result } from "effect" + * ```ts import.meta.vitest + * import { Option, Result } from "effect" * - * console.log(Result.isSuccess(Result.succeedNone)) - * // Output: true + * Result.succeedNone // => Result.succeed(Option.none()) * ``` * * @see {@link succeedSome} for the `Some` counterpart @@ -1880,12 +1795,10 @@ export const succeedNone = succeed(option_.none) * * **Example** (Wrapping a value in Some inside a Result) * - * ```ts - * import { Result } from "effect" + * ```ts import.meta.vitest + * import { Option, Result } from "effect" * - * const result = Result.succeedSome(42) - * console.log(result) - * // Output: { _tag: "Success", success: { _tag: "Some", value: 42 }, ... } + * Result.succeedSome(42) // => Result.succeed(Option.some(42)) * ``` * * @see {@link succeedNone} for the `None` counterpart @@ -1907,17 +1820,17 @@ export const succeedSome = (a: A): Result, E> => succeed * * **Example** (Logging a success value) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Result } from "effect" * + * const values: Array = [] * const result = pipe( * Result.succeed(42), - * Result.tap((n) => console.log("Got:", n)) + * Result.tap((n) => values.push(n)) * ) - * // Output: "Got: 42" * - * console.log(Result.isSuccess(result)) - * // Output: true + * values // => [42] + * result // => Result.succeed(42) * ``` * * @see {@link map} to transform the success value diff --git a/.context/effect/packages/effect/src/Runtime.ts b/.context/effect/packages/effect/src/Runtime.ts index 001c81a72..ab7ad6809 100644 --- a/.context/effect/packages/effect/src/Runtime.ts +++ b/.context/effect/packages/effect/src/Runtime.ts @@ -31,31 +31,27 @@ import type * as Fiber from "./Fiber.ts" * * **Example** (Customizing teardown behavior) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Runtime } from "effect" * - * // Custom teardown that logs completion status + * // Custom teardown that maps completion status to an exit code * const customTeardown: Runtime.Teardown = (exit, onExit) => { - * if (Exit.isSuccess(exit)) { - * console.log("Program completed successfully with value:", exit.value) - * onExit(0) - * } else { - * console.log("Program failed with cause:", exit.cause) - * onExit(1) - * } + * onExit(Exit.isSuccess(exit) ? 0 : 1) * } * + * const completed = new Promise, number]>((resolve) => { * // Use with makeRunMain - * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { - * fiber.addObserver((exit) => { - * teardown(exit, (code) => { - * console.log(`Exiting with code: ${code}`) + * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { + * fiber.addObserver((exit) => { + * teardown(exit, (code) => resolve([exit, code])) * }) * }) + * + * const program = Effect.succeed("Hello, World!") + * runMain(program, { teardown: customTeardown }) * }) * - * const program = Effect.succeed("Hello, World!") - * runMain(program, { teardown: customTeardown }) + * await completed // => [Exit.succeed("Hello, World!"), 0] * ``` * * @category models @@ -90,23 +86,18 @@ export interface Teardown { * * **Example** (Referencing default teardown) * - * ```ts + * ```ts import.meta.vitest * import { Exit, Runtime } from "effect" * - * const logExitCode = (exit: Exit.Exit) => { - * Runtime.defaultTeardown(exit, (code) => { - * console.log(`Exit code: ${code}`) - * }) - * } - * - * logExitCode(Exit.succeed(42)) - * // Output: Exit code: 0 + * const exitCodes: Array = [] + * const collectExitCode = (exit: Exit.Exit) => + * Runtime.defaultTeardown(exit, (code) => exitCodes.push(code)) * - * logExitCode(Exit.fail("error")) - * // Output: Exit code: 1 + * collectExitCode(Exit.succeed(42)) + * collectExitCode(Exit.fail("error")) + * collectExitCode(Exit.interrupt(123)) * - * logExitCode(Exit.interrupt(123)) - * // Output: Exit code: 130 + * exitCodes // => [0, 1, 130] * ``` * * @see {@link errorExitCode} for customizing failure exit codes @@ -152,47 +143,36 @@ export const defaultTeardown: Teardown = ( * * **Example** (Creating platform runners) * - * ```ts - * import { Effect, Fiber, Runtime } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Runtime } from "effect" * + * const events: Array = [] + * const completed = new Promise, number]>((resolve) => { * // Create a simple runner for a hypothetical platform - * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { - * // Set up signal handling - * const handleSignal = () => { - * Effect.runSync(Fiber.interrupt(fiber)) - * } - * - * // Add signal listeners (platform-specific) - * // process.on('SIGINT', handleSignal) - * // process.on('SIGTERM', handleSignal) - * - * // Handle fiber completion - * fiber.addObserver((exit) => { - * teardown(exit, (code) => { - * console.log(`Program finished with exit code: ${code}`) - * // process.exit(code) + * const runMain = Runtime.makeRunMain(({ fiber, teardown }) => { + * // Handle fiber completion + * fiber.addObserver((exit) => { + * teardown(exit, (code) => resolve([exit, code])) * }) * }) - * }) * - * // Use the runner - * const program = Effect.gen(function*() { - * yield* Effect.log("Starting program") - * yield* Effect.sleep(1000) - * yield* Effect.log("Program completed") - * return "success" - * }) - * - * // Run with default options - * runMain(program) + * // Use the runner + * const program = Effect.sync(() => { + * events.push("Starting program", "Program completed") + * return "success" + * }) * - * // Run with custom teardown - * runMain(program, { - * teardown: (exit, onExit) => { - * console.log("Custom teardown logic") - * Runtime.defaultTeardown(exit, onExit) - * } + * runMain(program, { + * teardown: (exit, onExit) => { + * events.push("Custom teardown logic") + * Runtime.defaultTeardown(exit, onExit) + * } + * }) * }) + * + * const result = await completed + * result // => [Exit.succeed("success"), 0] + * events // => ["Starting program", "Program completed", "Custom teardown logic"] * ``` * * @category running @@ -285,16 +265,14 @@ export type errorExitCode = "~effect/Runtime/errorExitCode" * * **Example** (Setting a process exit code) * - * ```ts - * import { Data, Effect, Runtime } from "effect" - * import { NodeRuntime } from "@effect/platform-node" + * ```ts import.meta.vitest + * import { Data, Runtime } from "effect" * * class MyError extends Data.TaggedError("MyError") { * readonly [Runtime.errorExitCode] = 42 * } * - * // If the program fails with MyError, the process will exit with code 42 - * NodeRuntime.runMain(Effect.fail(new MyError())) + * Runtime.getErrorExitCode(new MyError()) // => 42 * ``` * * @see {@link errorReported} for controlling automatic error logging @@ -327,7 +305,7 @@ export const errorExitCode: errorExitCode = "~effect/Runtime/errorExitCode" * * @see {@link errorExitCode} for the marker read by this function * - * @category accessors + * @category getters * @since 4.0.0 */ export const getErrorExitCode = (u: unknown): number => { @@ -377,17 +355,14 @@ export type errorReported = "~effect/Runtime/errorReported" * * **Example** (Suppressing error reporting) * - * ```ts - * import { Data, Effect, Runtime } from "effect" - * import { NodeRuntime } from "@effect/platform-node" + * ```ts import.meta.vitest + * import { Data, Runtime } from "effect" * * class MyError extends Data.TaggedError("MyError") { * readonly [Runtime.errorReported] = false * } * - * // If the program fails with MyError, the process will exit with code 1 but - * // no error will be logged. - * NodeRuntime.runMain(Effect.fail(new MyError())) + * Runtime.getErrorReported(new MyError()) // => false * ``` * * @see {@link errorExitCode} for controlling failure exit codes @@ -418,7 +393,7 @@ export const errorReported: errorReported = "~effect/Runtime/errorReported" * * @see {@link errorReported} for the marker read by this function * - * @category accessors + * @category getters * @since 4.0.0 */ export const getErrorReported = (u: unknown): boolean => { diff --git a/.context/effect/packages/effect/src/Schedule.ts b/.context/effect/packages/effect/src/Schedule.ts index b311b483d..a85e5d4ed 100644 --- a/.context/effect/packages/effect/src/Schedule.ts +++ b/.context/effect/packages/effect/src/Schedule.ts @@ -34,42 +34,17 @@ const randomNext: Effect = random.Random.useSync((random) => random.next * * **Example** (Defining retry and repeat schedules) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class NetworkError extends Data.TaggedError("NetworkError")<{ - * readonly attempt: number - * }> {} - * - * // Basic retry schedule - retry up to 3 times with exponential backoff - * const retrySchedule = Schedule.max([ - * Schedule.exponential("100 millis"), - * Schedule.recurs(3) - * ]) + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * import { TestClock } from "effect/testing" * - * // Basic repeat schedule - repeat every 30 seconds forever - * const repeatSchedule: Schedule.Schedule = Schedule - * .spaced("30 seconds") + * const executions: Array = [] + * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( + * Effect.repeat(Schedule.recurs(2)), + * Effect.as(executions) + * ) * - * const program = Effect.gen(function*() { - * let attempts = 0 - * - * const result1 = yield* Effect.retry( - * Effect.gen(function*() { - * attempts++ - * if (attempts < 3) { - * return yield* Effect.fail(new NetworkError({ attempt: attempts })) - * } - * return "Success" - * }), - * retrySchedule - * ) - * console.log(result1) // "Success" - * - * yield* Console.log("heartbeat").pipe( - * Effect.repeat(repeatSchedule.pipe(Schedule.upTo({ times: 5 }))) - * ) - * }) + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @category models @@ -115,7 +90,7 @@ export interface Metadata extends InputMetada * input and output values, zero duration, and zeroed timing fields before any * schedule step has produced metadata. * - * @category metadata + * @category services * @since 4.0.0 */ export const CurrentMetadata = Context.Reference("effect/Schedule/CurrentMetadata", { @@ -142,27 +117,11 @@ export declare namespace Schedule { * * **Example** (Understanding schedule variance) * - * ```ts - * import { Effect, Schedule } from "effect" - * - * // Understanding Schedule variance: - * // - Output: covariant (can be a subtype) - * // - Input: contravariant (can accept supertypes) - * // - Error: covariant (can be a subtype) - * // - Env: covariant (can be a subtype) - * - * // Schedule that produces strings, accepts any input - * const stringSchedule = Schedule.spaced("1 second").pipe( - * Schedule.map(() => Effect.succeed("tick")) - * ) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * // Schedule that only accepts Error inputs - * const errorSchedule = Schedule.exponential("100 millis").pipe( - * Schedule.upTo({ times: 5 }) - * ) - * - * // Schedule requiring a service environment - * const serviceSchedule = Schedule.spaced("5 seconds") + * const schedule: Schedule.Schedule = Schedule.recurs(2) + * Schedule.isSchedule(schedule) // => true * ``` * * @category models @@ -195,7 +154,7 @@ export declare namespace Schedule { /** * Extracts the output type from a `Schedule`. * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Output = S extends Schedule ? Output : never @@ -203,7 +162,7 @@ export type Output = S extends Schedule ? Output /** * Extracts the input type from a `Schedule`. * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Input = S extends Schedule ? Input : never @@ -211,7 +170,7 @@ export type Input = S extends Schedule ? Input : /** * Extracts the error type from a `Schedule`. * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Error = S extends Schedule ? Error : never @@ -219,7 +178,7 @@ export type Error = S extends Schedule ? Error : /** * Extracts the service requirements from a `Schedule`. * - * @category type extractors + * @category utility types * @since 4.0.0 */ export type Env = S extends Schedule ? Env : never @@ -240,16 +199,16 @@ const ScheduleProto = { * * **Example** (Checking for schedules) * - * ```ts + * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.exponential("100 millis") * const notSchedule = { foo: "bar" } * - * console.log(Schedule.isSchedule(schedule)) // true - * console.log(Schedule.isSchedule(notSchedule)) // false - * console.log(Schedule.isSchedule(null)) // false - * console.log(Schedule.isSchedule(undefined)) // false + * Schedule.isSchedule(schedule) // => true + * Schedule.isSchedule(notSchedule) // => false + * Schedule.isSchedule(null) // => false + * Schedule.isSchedule(undefined) // => false * ``` * * @category guards @@ -262,7 +221,7 @@ export const isSchedule = (u: unknown): u is Schedule { @@ -275,6 +234,14 @@ export const isSchedule = (u: unknown): u is Schedule 0 * ``` * * @category constructors @@ -310,7 +277,7 @@ const metadataFn = () => { * * **Example** (Creating a metadata-aware schedule) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Duration, Effect, Schedule } from "effect" * * const firstThreeInputs = Schedule.fromStepWithMetadata(Effect.succeed((metadata: Schedule.InputMetadata) => { @@ -323,6 +290,14 @@ const metadataFn = () => { * Duration.millis(250) * ] as [string, Duration.Duration]) * })) + * + * const program = Effect.gen(function*() { + * const step = yield* Schedule.toStep(firstThreeInputs) + * const [output] = yield* step(0, "input") + * return output + * }) + * + * await Effect.runPromise(program) // => "attempt 1: input" * ``` * * @category constructors @@ -345,8 +320,8 @@ export const fromStepWithMetadata = ( * * **Example** (Extracting a schedule step function) * - * ```ts - * import { Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * * // Extract step function from an existing schedule * const schedule = Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) @@ -357,10 +332,10 @@ export const fromStepWithMetadata = ( * // Use the step function directly for custom logic. The timestamp is * // supplied by the caller, so tests can pass a deterministic value. * const now = 0 - * const result = yield* stepFn(now, "input") - * - * console.log(`Step result: ${result}`) + * return yield* stepFn(now, "input") * }) + * + * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @category destructors @@ -433,26 +408,19 @@ export const toStepWithMetadata = ( * * **Example** (Extracting a sleeping step function) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schedule } from "effect" + * import { TestClock } from "effect/testing" * - * // Convert schedule to step function with automatic sleeping - * const schedule = Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 3 })) + * const schedule = Schedule.recurs(3) * * const program = Effect.gen(function*() { * const stepWithSleep = yield* Schedule.toStepWithSleep(schedule) * - * // Each call will automatically sleep for the scheduled delay - * console.log("Starting...") - * const result1 = yield* stepWithSleep("first") - * console.log(`First result: ${result1}`) - * - * const result2 = yield* stepWithSleep("second") - * console.log(`Second result: ${result2}`) - * - * const result3 = yield* stepWithSleep("third") - * console.log(`Third result: ${result3}`) + * return [yield* stepWithSleep("first"), yield* stepWithSleep("second")] * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [0, 1] * ``` * * @category destructors @@ -476,94 +444,19 @@ export const toStepWithSleep = ( * * **Example** (Adding extra delay to a schedule) * - * ```ts - * import { Console, Data, Duration, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * // Add a deterministic extra delay based on the schedule metadata - * const delayedSchedule = Schedule.addDelay( - * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 5 })), - * ({ output }) => - * Effect.succeed(Duration.millis(Duration.toMillis(output) * 0.25)) - * ) - * - * const repeatProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.succeed("delayed task"), - * delayedSchedule.pipe( - * Schedule.tap(({ output: delay }) => - * Console.log(`Base delay: ${delay}`) - * ) - * ) - * ) - * }) + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * - * // Add adaptive delay based on execution count - * const adaptiveSchedule = Schedule.addDelay( - * Schedule.recurs(6), - * ({ output: executionCount }) => - * // Increase delay as execution count grows - * Effect.succeed(Duration.millis(executionCount * 200)) + * const schedule = Schedule.recurs(1).pipe( + * Schedule.addDelay(() => Effect.succeed("25 millis")) * ) - * - * const adaptiveProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Adaptive delay task") - * return "adaptive" - * }), - * adaptiveSchedule.pipe( - * Schedule.tap(({ output: count }) => - * Console.log(`Execution ${count + 1} with adaptive delay`) - * ) - * ) - * ) - * }) - * - * // Add effectful delay computation from deterministic service data - * const loadByExecution = [1, 3, 2, 4] as const - * - * const dynamicSchedule = Schedule.addDelay( - * Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 4 })), - * ({ output: executionNumber }) => { - * const load = loadByExecution[executionNumber] ?? 1 - * return Effect.succeed(Duration.millis(load * 100)) - * } - * ) - * - * const dynamicProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Dynamic delay task") - * return "dynamic" - * }), - * dynamicSchedule - * ) + * const program = Effect.gen(function*() { + * const step = yield* Schedule.toStep(schedule) + * const [, delay] = yield* step(0, undefined) + * return delay * }) * - * // Combine with retry for progressive backoff - * const progressiveRetrySchedule = Schedule.addDelay( - * Schedule.exponential("50 millis").pipe(Schedule.upTo({ times: 4 })), - * () => Effect.succeed(Duration.millis(100)) // Fixed additional delay - * ) - * - * const retryProgram = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * if (attempt < 5) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * return `Success on attempt ${attempt}` - * }), - * progressiveRetrySchedule - * ) - * - * yield* Console.log(`Final result: ${result}`) - * }) + * await Effect.runPromise(program) // => Duration.millis(25) * ``` * * @category delays & timeouts @@ -594,41 +487,17 @@ export const addDelay: { * * **Example** (Sequencing quick and slow retries) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * // First retry 3 times quickly, then switch to slower retries - * const quickRetries = Schedule.exponential("100 millis").pipe( - * Schedule.upTo({ times: 3 }) - * ) - * const slowRetries = Schedule.exponential("1 second").pipe( - * Schedule.upTo({ times: 2 }) - * ) - * - * const combinedRetries = Schedule.andThen(quickRetries, slowRetries) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * const program = Effect.gen(function*() { - * let attempt = 0 - * yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Attempt ${attempt}`) - * if (attempt < 6) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Failure ${attempt}` })) - * } - * return `Success on attempt ${attempt}` - * }), - * combinedRetries - * ) - * }) + * const schedule = Schedule.concat(Schedule.recurs(1), Schedule.recurs(2)) + * Schedule.isSchedule(schedule) // => true * ``` * * @category sequencing * @since 2.0.0 */ -export const andThen: { +export const concat: { ( other: Schedule ): ( @@ -642,7 +511,7 @@ export const andThen: { self: Schedule, other: Schedule ): Schedule => - map(andThenResult(self, other), ({ output }) => effect.succeed(Result.merge(output)))) + map(concatResult(self, other), ({ output }) => effect.succeed(Result.merge(output)))) /** * Returns a schedule that runs `self` to completion, then runs `other`, and @@ -656,37 +525,17 @@ export const andThen: { * * **Example** (Tracking sequential schedule phases) * - * ```ts - * import { Console, Effect, Result, Schedule } from "effect" - * - * // Track which phase of the schedule we're in - * const phaseTracker = Schedule.andThenResult( - * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 2 })), - * Schedule.spaced("500 millis").pipe(Schedule.upTo({ times: 2 })) - * ) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * const program = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Task executed") - * return "task-result" - * }), - * phaseTracker.pipe( - * Schedule.tap(({ output: result }) => - * Result.match(result, { - * onFailure: (phase1Output) => Console.log(`Phase 1: ${phase1Output}`), - * onSuccess: (phase2Output) => Console.log(`Phase 2: ${phase2Output}`) - * }) - * ) - * ) - * ) - * }) + * const schedule = Schedule.concatResult(Schedule.recurs(1), Schedule.recurs(2)) + * Schedule.isSchedule(schedule) // => true * ``` * * @category sequencing * @since 4.0.0 */ -export const andThenResult: { +export const concatResult: { ( other: Schedule ): ( @@ -756,36 +605,11 @@ export const andThenResult: { * * **Example** (Combining retry schedules by their maximum delay) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * const retrySchedule = Schedule.max([ - * Schedule.fixed("5 seconds"), - * Schedule.exponential("5 seconds"), - * Schedule.spaced("10 seconds") - * ]) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * const program = Effect.gen(function*() { - * let attempt = 0 - * - * yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Retry attempt ${attempt}`) - * if (attempt < 3) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * return "success" - * }), - * retrySchedule.pipe( - * Schedule.tap(({ output: duration }) => - * Console.log(`Waiting for the slowest schedule: ${duration}`) - * ) - * ) - * ) - * }) + * const schedule = Schedule.max([Schedule.fixed("5 seconds"), Schedule.spaced("10 seconds")]) + * Schedule.isSchedule(schedule) // => true * ``` * * @category combining @@ -841,126 +665,11 @@ const maxDuration = (results: ReadonlyArray): Dur * * **Example** (Scheduling work with cron expressions) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class ScheduledTaskError extends Data.TaggedError("ScheduledTaskError")<{ readonly message: string }> {} + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * // Run every minute * const everyMinute = Schedule.cron("* * * * *") - * - * const minutelyProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Running minutely task") - * return "minute" - * }), - * everyMinute.pipe( - * Schedule.upTo({ times: 3 }), // Run only 3 times for demo - * Schedule.tap(({ output: duration }) => - * Console.log(`Next execution in: ${duration}`) - * ) - * ) - * ) - * }) - * - * // Run every day at 2:30 AM - * const dailyBackup = Schedule.cron("30 2 * * *") - * - * const backupProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Running daily backup...") - * // Simulate backup process - * yield* Effect.sleep("2 seconds") - * yield* Console.log("Backup completed") - * return "backup-done" - * }), - * dailyBackup.pipe( - * Schedule.upTo({ times: 2 }) // Run 2 times for demo - * ) - * ) - * }) - * - * // Run every Monday at 9:00 AM with timezone - * const weeklyReport = Schedule.cron("0 9 * * 1", "America/New_York") - * - * const reportProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Generating weekly report...") - * const report = { - * week: 42, - * status: "ready" as const - * } - * yield* Console.log(`Report generated: ${JSON.stringify(report)}`) - * return report - * }), - * weeklyReport.pipe(Schedule.upTo({ times: 1 })) - * ) - * }) - * - * // Run every 15 minutes during business hours (9 AM - 5 PM) - * const businessHoursCheck = Schedule.cron("0,15,30,45 9-17 * * 1-5") - * - * const businessProgram = Effect.gen(function*() { - * const statuses = ["healthy", "healthy", "degraded", "healthy"] as const - * let index = 0 - * - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Business hours health check...") - * const status = statuses[index++] - * yield* Console.log(`System status: ${status}`) - * return status - * }), - * businessHoursCheck.pipe( - * Schedule.upTo({ times: 4 }) // Demo with 4 checks - * ) - * ) - * }) - * - * // Run on specific days of the month - * const monthlyInvoice = Schedule.cron("0 10 1,15 * *") // 1st and 15th at 10 AM - * - * const invoiceProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Processing monthly invoices...") - * const invoiceCount = 72 - * yield* Console.log(`Processed ${invoiceCount} invoices`) - * return { count: invoiceCount, batch: "2024-01-a" } - * }), - * monthlyInvoice.pipe(Schedule.upTo({ times: 1 })) - * ) - * }) - * - * // Complex cron with error handling - * const complexCron = Schedule.cron("0 2,4,6 * * *").pipe( - * Schedule.tap(({ output: duration }) => - * Console.log(`Scheduled to run again in ${duration}`) - * ) - * ) - * - * const robustProgram = Effect.gen(function*() { - * let attempt = 0 - * - * yield* Effect.repeat( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log("Complex scheduled task...") - * if (attempt === 1) { - * return yield* Effect.fail(new ScheduledTaskError({ message: "Scheduled task failed" })) - * } - * return "success" - * }), - * complexCron.pipe(Schedule.upTo({ times: 3 })) - * ).pipe( - * Effect.catch((error: unknown) => - * Console.log(`Cron task error: ${String(error)}`) - * ) - * ) - * }) + * Schedule.isSchedule(everyMinute) // => true * ``` * * @category constructors @@ -997,13 +706,10 @@ export const cron: { * * **Example** (Recurring once after a duration) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * const program = Effect.repeat( - * Console.log("runs again after one second"), - * Schedule.duration("1 second") - * ) + * Schedule.isSchedule(Schedule.duration("1 second")) // => true * ``` * * @see {@link during} for recurring until a duration has elapsed @@ -1030,82 +736,10 @@ export const duration = (durationInput: Duration.Input): Schedule {} - * - * // Run a task for exactly 5 seconds, regardless of how many iterations - * const fiveSecondSchedule = Schedule.during("5 seconds") - * - * const timedProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Task executed inside the time window") - * yield* Effect.sleep("500 millis") // Each task takes 500ms - * return "task done" - * }), - * fiveSecondSchedule.pipe( - * Schedule.tap(({ output: elapsedDuration }) => - * Console.log(`Total elapsed: ${elapsedDuration}`) - * ) - * ) - * ) - * - * yield* Console.log("Time limit reached!") - * }) - * - * // Combine with other schedules for time-bounded execution - * const timeAndCountLimited = Schedule.max([ - * Schedule.spaced("1 second"), - * Schedule.during("10 seconds"), // Stop after 10 seconds OR - * Schedule.recurs(15) // 15 attempts, whichever comes first - * ]) - * - * // Burst execution within time window - * const burstWindow = Schedule.during("3 seconds") - * - * const burstProgram = Effect.gen(function*() { - * yield* Console.log("Starting burst execution...") - * - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Burst task") - * return "burst" - * }), - * burstWindow - * ) - * - * yield* Console.log("Burst window completed") - * }) - * - * // Timed retry window - retry for up to 30 seconds - * const timedRetry = Schedule.max([ - * Schedule.exponential("200 millis"), - * Schedule.during("30 seconds") - * ]) - * - * const retryProgram = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Retry attempt ${attempt}`) - * - * if (attempt < 4) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * - * return `Success on attempt ${attempt}` - * }), - * timedRetry - * ) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * yield* Console.log(`Result: ${result}`) - * }).pipe( - * Effect.catch((error: unknown) => Console.log(`Timed out: ${String(error)}`)) - * ) + * Schedule.isSchedule(Schedule.during("5 seconds")) // => true * ``` * * @see {@link duration} for one delayed recurrence @@ -1119,8 +753,8 @@ export const during = (duration: Duration.Input): Schedule => effect.succeed((meta) => { const elapsed = Duration.millis(meta.elapsed) return meta.elapsed > durationMillis - ? effect.succeed([elapsed, Duration.zero]) - : Cause.done(elapsed) + ? Cause.done(elapsed) + : effect.succeed([elapsed, Duration.zero]) }) ) } @@ -1136,36 +770,11 @@ export const during = (duration: Duration.Input): Schedule => * * **Example** (Combining retry schedules by their minimum delay) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * const retrySchedule = Schedule.min([ - * Schedule.fixed("5 seconds"), - * Schedule.exponential("5 seconds"), - * Schedule.spaced("10 seconds") - * ]) + * ```ts import.meta.vitest + * import { Schedule } from "effect" * - * const program = Effect.gen(function*() { - * let attempt = 0 - * - * yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Retry attempt ${attempt}`) - * if (attempt < 3) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * return "success" - * }), - * retrySchedule.pipe( - * Schedule.tap(({ output: duration }) => - * Console.log(`Waiting for the fastest schedule: ${duration}`) - * ) - * ) - * ) - * }) + * const schedule = Schedule.min([Schedule.fixed("5 seconds"), Schedule.spaced("10 seconds")]) + * Schedule.isSchedule(schedule) // => true * ``` * * @category combining @@ -1224,44 +833,15 @@ const minDuration = (results: ReadonlyArray): Dur * * **Example** (Retrying with exponential backoff) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryFailure extends Data.TaggedError("RetryFailure")<{ readonly message: string }> {} - * - * // Basic exponential backoff with default factor of 2 - * const basicExponential = Schedule.exponential("100 millis") - * // Delays: 100ms, 200ms, 400ms, 800ms, 1600ms, ... - * - * // Custom exponential backoff with factor 1.5 - * const gentleExponential = Schedule.exponential("200 millis", 1.5) - * // Delays: 200ms, 300ms, 450ms, 675ms, 1012ms, ... - * - * // Retry with exponential backoff (limited to 5 attempts) - * const retryPolicy = Schedule.max([ - * Schedule.exponential("50 millis"), - * Schedule.recurs(5) - * ]) + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * if (attempt < 4) { - * yield* Console.log(`Attempt ${attempt} failed, retrying...`) - * return yield* Effect.fail(new RetryFailure({ message: `Failure ${attempt}` })) - * } - * return `Success on attempt ${attempt}` - * }), - * retryPolicy - * ) - * - * yield* Console.log(`Final result: ${result}`) + * const step = yield* Schedule.toStep(Schedule.exponential("100 millis")) + * return yield* step(0, undefined) * }) * - * // Will retry with delays: 50ms, 100ms, 200ms before success + * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @category constructors @@ -1285,63 +865,15 @@ export const exponential = ( * * **Example** (Retrying with Fibonacci backoff) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * // Basic Fibonacci schedule starting with 100ms - * const fibSchedule = Schedule.fibonacci("100 millis") - * // Delays: 100ms, 100ms, 200ms, 300ms, 500ms, 800ms, 1300ms, ... + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * - * // Retry with Fibonacci backoff for gradual increase - * const retryWithFib = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Attempt ${attempt}`) - * - * if (attempt < 5) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * - * return `Success on attempt ${attempt}` - * }), - * Schedule.max([ - * Schedule.fibonacci("50 millis"), - * Schedule.recurs(6) // Maximum 6 retries - * ]).pipe( - * Schedule.tap(({ output: delay }) => Console.log(`Next retry in ${delay}`)) - * ) - * ) - * - * yield* Console.log(`Final result: ${result}`) + * const program = Effect.gen(function*() { + * const step = yield* Schedule.toStep(Schedule.fibonacci("100 millis")) + * return yield* step(0, undefined) * }) * - * // Heartbeat with Fibonacci intervals (starts fast, gets slower) - * const adaptiveHeartbeat = Effect.gen(function*() { - * yield* Console.log("Heartbeat") - * return "pulse" - * }).pipe( - * Effect.repeat( - * Schedule.fibonacci("200 millis").pipe( - * Schedule.upTo({ times: 8 }) // First 8 heartbeats - * ) - * ) - * ) - * - * // Fibonacci vs exponential comparison - * const compareSchedules = Effect.gen(function*() { - * yield* Console.log("=== Fibonacci Delays ===") - * // 100ms, 100ms, 200ms, 300ms, 500ms, 800ms - * - * yield* Console.log("=== Exponential Delays ===") - * // 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms - * - * // Fibonacci grows more slowly than exponential - * }) + * await Effect.runPromise(program) // => [Duration.millis(100), Duration.millis(100)] * ``` * * @category constructors @@ -1382,50 +914,15 @@ export const fibonacci = (one: Duration.Input): Schedule => { * * **Example** (Repeating on fixed intervals) * - * ```ts - * import { Console, Effect, Schedule } from "effect" - * - * // Fixed interval schedule - recurs on a one-second cadence - * const everySecond = Schedule.fixed("1 second") - * - * // Health check that runs at fixed intervals - * const healthCheck = Effect.gen(function*() { - * yield* Console.log("Health check") - * yield* Effect.sleep("200 millis") // simulate health check work - * return "healthy" - * }).pipe( - * Effect.repeat(Schedule.fixed("2 seconds").pipe(Schedule.upTo({ times: 5 }))) - * ) - * - * // Difference between fixed and spaced: - * // - fixed: maintains constant rate regardless of action duration - * // - spaced: waits for the duration AFTER each action completes - * - * const longRunningTask = Effect.gen(function*() { - * yield* Console.log("Task started") - * yield* Effect.sleep("1.5 seconds") // Longer than interval - * yield* Console.log("Task completed") - * return "done" - * }) - * - * // Fixed schedule: if task takes 1.5s but interval is 1s, - * // next execution happens immediately (no pile-up) - * const fixedSchedule = longRunningTask.pipe( - * Effect.repeat(Schedule.fixed("1 second").pipe(Schedule.upTo({ times: 3 }))) - * ) - * - * // Comparing with spaced (waits 1s AFTER each task) - * const spacedSchedule = longRunningTask.pipe( - * Effect.repeat(Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 3 }))) - * ) + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { - * yield* Console.log("=== Fixed Schedule Demo ===") - * yield* fixedSchedule - * - * yield* Console.log("=== Spaced Schedule Demo ===") - * yield* spacedSchedule + * const step = yield* Schedule.toStep(Schedule.fixed("1 second")) + * return yield* step(0, undefined) * }) + * + * await Effect.runPromise(program) // => [0, Duration.seconds(1)] * ``` * * @see {@link spaced} for delaying after each action completes @@ -1469,59 +966,19 @@ export const fixed = (interval: Duration.Input): Schedule => { * * **Example** (Mapping schedule outputs) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" * - * // Transform schedule output from number to string * const countSchedule = Schedule.recurs(5).pipe( * Schedule.map(({ output: count }) => Effect.succeed(`Execution #${count + 1}`)) * ) - * - * // Map schedule delays to human-readable format - * const readableDelays = Schedule.exponential("100 millis").pipe( - * Schedule.map(({ output: delay }) => Effect.succeed(`Next retry in ${delay}`)) - * ) - * - * // Transform numeric output to structured data - * const structuredSchedule = Schedule.spaced("1 second").pipe( - * Schedule.map(({ output: recurrence }) => Effect.succeed({ - * iteration: recurrence + 1, - * phase: recurrence < 5 ? "warmup" as const : "steady" as const - * })) - * ) - * * const program = Effect.gen(function*() { - * const results = yield* Effect.repeat( - * Effect.succeed("task completed"), - * structuredSchedule.pipe( - * Schedule.upTo({ times: 8 }), - * Schedule.tap(({ output: info }) => - * Console.log( - * `${info.phase} phase - iteration ${info.iteration}` - * ) - * ) - * ) - * ) - * - * yield* Console.log(`Completed iterations`) + * const step = yield* Schedule.toStep(countSchedule) + * const [output] = yield* step(0, undefined) + * return output * }) * - * // Map with effectful transformation - * const effectfulMap = Schedule.fixed("2 seconds").pipe( - * Schedule.map(({ output: count }) => - * Effect.gen(function*() { - * yield* Console.log(`Processing count: ${count}`) - * return count * 10 - * }) - * ) - * ) - * - * // Use timing metadata in the mapped output - * const complexSchedule = Schedule.fibonacci("100 millis").pipe( - * Schedule.map(({ output: delay, attempt }) => - * Effect.succeed(`Attempt ${attempt} delay: ${delay}`) - * ) - * ) + * await Effect.runPromise(program) // => "Execution #1" * ``` * * @category mapping @@ -1565,28 +1022,19 @@ export const map: { * * **Example** (Modifying delays from schedule metadata) * - * ```ts - * import { Console, Duration, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * - * // Modify delays based on output - increase delay on high iteration counts - * const adaptiveDelay = Schedule.recurs(10).pipe( - * Schedule.modifyDelay(({ output, duration }) => { - * // Double the delay if we're seeing high iteration counts - * return Effect.succeed(output > 5 ? Duration.times(duration, 2) : duration) - * }) + * const schedule = Schedule.spaced("10 millis").pipe( + * Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.times(duration, 2))) * ) - * * const program = Effect.gen(function*() { - * let counter = 0 - * yield* Effect.repeat( - * Effect.gen(function*() { - * counter++ - * yield* Console.log(`Attempt ${counter}`) - * return counter - * }), - * adaptiveDelay.pipe(Schedule.upTo({ times: 8 })) - * ) + * const step = yield* Schedule.toStep(schedule) + * const [, delay] = yield* step(0, undefined) + * return delay * }) + * + * await Effect.runPromise(program) // => Duration.millis(20) * ``` * * @category delays & timeouts @@ -1656,25 +1104,19 @@ export const jittered = ( * * **Example** (Passing inputs through as outputs) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" * - * // Create a schedule that outputs the inputs instead of original outputs * const inputSchedule = Schedule.passthrough( * Schedule.exponential("100 millis").pipe(Schedule.upTo({ times: 3 })) * ) - * * const program = Effect.gen(function*() { - * let counter = 0 - * yield* Effect.repeat( - * Effect.gen(function*() { - * counter++ - * yield* Console.log(`Task ${counter} executed`) - * return `result-${counter}` - * }), - * inputSchedule - * ) + * const step = yield* Schedule.toStep(inputSchedule) + * const [output] = yield* step(0, "input") + * return output * }) + * + * await Effect.runPromise(program) // => "input" * ``` * * @category mapping @@ -1706,53 +1148,17 @@ export const passthrough = ( * * **Example** (Limiting recurrences) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * // Basic recurs - retry at most 3 times - * const maxThreeAttempts = Schedule.recurs(3) - * - * // Retry a failing operation at most 5 times - * const program = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Attempt ${attempt}`) - * - * if (attempt < 4) { - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * - * return `Success on attempt ${attempt}` - * }), - * Schedule.recurs(5) // Will retry up to 5 times - * ) - * - * yield* Console.log(`Final result: ${result}`) - * }) + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * import { TestClock } from "effect/testing" * - * // Combining recurs with other schedules for sophisticated retry logic - * const complexRetry = Schedule.max([ - * Schedule.exponential("100 millis"), - * Schedule.recurs(3) // At most 3 retries - * ]) - * - * // Allow ten recurrences after the initial run - * const tenRecurrences = Effect.gen(function*() { - * yield* Console.log("Executing task...") - * return "completed" - * }).pipe( - * Effect.repeat(Schedule.recurs(10)) + * const executions: Array = [] + * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( + * Effect.repeat(Schedule.recurs(3)), + * Effect.as(executions) * ) * - * // The schedule outputs the current recurrence count (0-based) - * const countingSchedule = Schedule.recurs(3).pipe( - * Schedule.tap(({ output: count }) => Console.log(`Execution #${count + 1}`)) - * ) + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3, 4] * ``` * * @see {@link upTo} for limiting an existing schedule @@ -1773,46 +1179,15 @@ export const recurs = (times: number): Schedule => * * **Example** (Repeating with fixed spacing) * - * ```ts - * import { Console, Effect, Schedule } from "effect" - * - * // Basic spaced schedule - runs every 2 seconds - * const everyTwoSeconds = Schedule.spaced("2 seconds") - * - * // Heartbeat that runs indefinitely with fixed spacing - * const heartbeat = Effect.gen(function*() { - * yield* Console.log("Heartbeat") - * }).pipe( - * Effect.repeat(everyTwoSeconds) - * ) - * - * // Limited repeat - run only 5 times with 1-second spacing - * const limitedTask = Effect.gen(function*() { - * yield* Console.log("Executing scheduled task...") - * yield* Effect.sleep("500 millis") // simulate work - * return "Task completed" - * }).pipe( - * Effect.repeat( - * Schedule.spaced("1 second").pipe(Schedule.upTo({ times: 5 })) - * ) - * ) - * - * // Simple spaced schedule with limited repetitions - * const limitedSpaced = Schedule.max([ - * Schedule.spaced("100 millis"), - * Schedule.recurs(5) // at most 5 times - * ]) + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { - * yield* Console.log("Starting spaced execution...") - * - * yield* Effect.repeat( - * Effect.succeed("work item"), - * limitedSpaced - * ) - * - * yield* Console.log("Completed executions") + * const step = yield* Schedule.toStep(Schedule.spaced("2 seconds")) + * return yield* step(0, undefined) * }) + * + * await Effect.runPromise(program) // => [0, Duration.seconds(2)] * ``` * * @see {@link fixed} for recurrence aligned to a regular cadence @@ -1837,23 +1212,20 @@ export const spaced = (duration: Duration.Input): Schedule => { * * **Example** (Tapping schedule metadata) * - * ```ts - * import { Console, Effect, Schedule } from "effect" - * - * const monitoredSchedule = Schedule.exponential("100 millis").pipe( - * Schedule.upTo({ times: 5 }), - * Schedule.tap((metadata) => - * Console.log( - * `Attempt ${metadata.attempt} produced ${metadata.output} ` + - * `after ${metadata.elapsed}ms; next delay is ${metadata.duration}` - * ) - * ) - * ) + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" * - * const program = Effect.retry( - * Effect.fail("transient error"), - * monitoredSchedule + * const attempts: Array = [] + * const monitoredSchedule = Schedule.recurs(2).pipe( + * Schedule.tap((metadata) => Effect.sync(() => attempts.push(metadata.attempt))) * ) + * const program = Effect.gen(function*() { + * const step = yield* Schedule.toStep(monitoredSchedule) + * const [output] = yield* step(0, undefined) + * return { attempts, output } + * }) + * + * await Effect.runPromise(program) // => { attempts: [1], output: 0 } * ``` * * @category sequencing @@ -1902,64 +1274,18 @@ export const tap: { * * **Example** (Limiting by duration and recurrence count) * - * ```ts - * import { Console, Data, Effect, Schedule } from "effect" - * - * class RetryAttemptError extends Data.TaggedError("RetryAttemptError")<{ readonly message: string }> {} - * - * // Limit an infinite schedule to five recurrences - * const limitedHeartbeat = Schedule.spaced("1 second").pipe( - * Schedule.upTo({ times: 5 }) - * ) - * - * const heartbeatProgram = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Heartbeat") - * return "pulse" - * }), - * limitedHeartbeat - * ) - * - * yield* Console.log("Heartbeat sequence completed") - * }) - * - * // Limit retry attempts by both count and elapsed time - * const limitedRetry = Schedule.exponential("100 millis").pipe( - * Schedule.upTo({ - * duration: "5 seconds", - * times: 3 - * }) - * ) - * - * const retryProgram = Effect.gen(function*() { - * let attempt = 0 - * - * const result = yield* Effect.retry( - * Effect.gen(function*() { - * attempt++ - * yield* Console.log(`Attempt ${attempt}`) - * - * if (attempt < 5) { // Will fail more than 3 times - * return yield* Effect.fail(new RetryAttemptError({ message: `Attempt ${attempt} failed` })) - * } - * - * return `Success on attempt ${attempt}` - * }), - * limitedRetry - * ) + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * import { TestClock } from "effect/testing" * - * yield* Console.log(`Result: ${result}`) - * }).pipe( - * Effect.catch((error: unknown) => - * Console.log(`Failed after limited retries: ${String(error)}`) - * ) + * const executions: Array = [] + * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) + * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( + * Effect.repeat(schedule), + * Effect.as(executions) * ) * - * // Empty options leave the schedule unchanged - * const unchanged = Schedule.fixed("500 millis").pipe( - * Schedule.upTo({}) - * ) + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @category filtering @@ -2067,21 +1393,15 @@ export { * * **Example** (Repeating on aligned windows) * - * ```ts - * import { Console, Effect, Schedule } from "effect" - * - * // Execute tasks at regular intervals aligned to window boundaries - * const windowSchedule = Schedule.windowed("5 seconds") + * ```ts import.meta.vitest + * import { Duration, Effect, Schedule } from "effect" * * const program = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Window task executed") - * return "window-task" - * }), - * windowSchedule.pipe(Schedule.upTo({ times: 4 })) - * ) + * const step = yield* Schedule.toStep(Schedule.windowed("5 seconds")) + * return yield* step(0, undefined) * }) + * + * await Effect.runPromise(program) // => [0, Duration.seconds(5)] * ``` * * @category constructors @@ -2107,21 +1427,18 @@ export const windowed = (interval: Duration.Input): Schedule => { * * **Example** (Repeating forever) * - * ```ts - * import { Console, Effect, Schedule } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule } from "effect" + * import { TestClock } from "effect/testing" * - * // A schedule that runs forever with no delay - * const infiniteSchedule = Schedule.forever + * const executions: Array = [] + * const schedule = Schedule.forever.pipe(Schedule.upTo({ times: 2 })) + * const program = Effect.sync(() => executions.push(executions.length + 1)).pipe( + * Effect.repeat(schedule), + * Effect.as(executions) + * ) * - * const program = Effect.gen(function*() { - * yield* Effect.repeat( - * Effect.gen(function*() { - * yield* Console.log("Running forever...") - * return "continuous-task" - * }), - * infiniteSchedule.pipe(Schedule.upTo({ times: 5 })) // Limit for demo - * ) - * }) + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) // => [1, 2, 3] * ``` * * @category constructors @@ -2171,12 +1488,13 @@ export { * * **Example** (Setting a schedule input type) * - * ```ts + * ```ts import.meta.vitest * import { Schedule } from "effect" * * const schedule = Schedule.recurs(3).pipe( * Schedule.setInputType() * ) + * Schedule.isSchedule(schedule) // => true * ``` * * @category utility types diff --git a/.context/effect/packages/effect/src/Scheduler.ts b/.context/effect/packages/effect/src/Scheduler.ts index 4e076de55..1a6b7f87d 100644 --- a/.context/effect/packages/effect/src/Scheduler.ts +++ b/.context/effect/packages/effect/src/Scheduler.ts @@ -26,7 +26,7 @@ import type * as Fiber from "./Fiber.ts" * priorities, and decides when fibers should yield control after consuming * their operation budget. * - * @category models + * @category services * @since 2.0.0 */ export interface Scheduler { @@ -72,10 +72,11 @@ export interface SchedulerDispatcher { * The default value creates a `MixedScheduler`. Provide this service to * customize execution mode, task dispatching, or yield behavior. * - * @category references + * @category services * @since 2.0.0 */ export const Scheduler: Context.Reference = Context.Reference("effect/Scheduler", { + fiberCached: true, defaultValue: () => new MixedScheduler() }) @@ -91,6 +92,16 @@ const setImmediate = "setImmediate" in globalThis return (): void => clearTimeout(timer) } +const setMicrotask = (f: () => void) => { + let cancelled = false + Promise.resolve().then(() => { + if (!cancelled) f() + }) + return (): void => { + cancelled = true + } +} + class PriorityBuckets { buckets: Array<[priority: number, tasks: Array<() => void>]> = [] @@ -135,7 +146,7 @@ class PriorityBuckets { * operation counts to decide when fibers should yield, and is the default * scheduler implementation. * - * @category schedulers + * @category models * @since 2.0.0 */ export class MixedScheduler implements Scheduler { @@ -144,10 +155,10 @@ export class MixedScheduler implements Scheduler { constructor( executionMode: "sync" | "async" = "async", - setImmediateFn: (f: () => void) => () => void = setImmediate + setImmediateFn?: (f: () => void) => () => void ) { this.executionMode = executionMode - this.setImmediate = setImmediateFn + this.setImmediate = setImmediateFn ?? (executionMode === "sync" ? setMicrotask : setImmediate) } /** @@ -252,10 +263,11 @@ class MixedSchedulerDispatcher implements SchedulerDispatcher { * * @see {@link PreventSchedulerYield} for bypassing scheduler yield checks entirely rather than tuning the operation budget * - * @category references + * @category services * @since 4.0.0 */ export const MaxOpsBeforeYield = Context.Reference("effect/Scheduler/MaxOpsBeforeYield", { + fiberCached: true, defaultValue: () => 2048 }) @@ -277,9 +289,10 @@ export const MaxOpsBeforeYield = Context.Reference("effect/Scheduler/Max * @see {@link MaxOpsBeforeYield} for tuning yield frequency without disabling yield checks * @see {@link Scheduler} for providing custom scheduler yield behavior * - * @category references + * @category services * @since 4.0.0 */ export const PreventSchedulerYield = Context.Reference("effect/Scheduler/PreventSchedulerYield", { + fiberCached: true, defaultValue: () => false }) diff --git a/.context/effect/packages/effect/src/Schema.ts b/.context/effect/packages/effect/src/Schema.ts index b1bf63f3a..dbeb45e35 100644 --- a/.context/effect/packages/effect/src/Schema.ts +++ b/.context/effect/packages/effect/src/Schema.ts @@ -19,7 +19,6 @@ import * as BigDecimal_ from "./BigDecimal.ts" import type * as Brand from "./Brand.ts" import * as Cause_ from "./Cause.ts" import * as Chunk_ from "./Chunk.ts" -import type * as Combiner from "./Combiner.ts" import * as Data from "./Data.ts" import * as DateTime from "./DateTime.ts" import type { Differ } from "./Differ.ts" @@ -37,10 +36,11 @@ import * as HashSet_ from "./HashSet.ts" import * as core from "./internal/core.ts" import * as InternalRecord from "./internal/record.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" -import * as InternalArbitrary from "./internal/schema/arbitrary.ts" -import * as InternalEquivalence from "./internal/schema/equivalence.ts" -import * as InternalStandard from "./internal/schema/representation.ts" import * as InternalSchema from "./internal/schema/schema.ts" +import * as InternalArbitrary from "./internal/schema/toArbitrary.ts" +import * as InternalEquivalence from "./internal/schema/toEquivalence.ts" +import * as InternalToJsonSchemaDocument from "./internal/schema/toJsonSchemaDocument.ts" +import * as InternalToRepresentation from "./internal/schema/toRepresentation.ts" import * as JsonPatch from "./JsonPatch.ts" import * as JsonSchema from "./JsonSchema.ts" import { remainder } from "./Number.ts" @@ -51,10 +51,10 @@ import * as Pipeable from "./Pipeable.ts" import * as Predicate from "./Predicate.ts" import * as Record_ from "./Record.ts" import * as Redacted_ from "./Redacted.ts" +import * as RegExp_ from "./RegExp.ts" import * as Result_ from "./Result.ts" import * as Scheduler from "./Scheduler.ts" import * as SchemaAST from "./SchemaAST.ts" -import { isSchemaError, SchemaError } from "./SchemaError.ts" import * as SchemaGetter from "./SchemaGetter.ts" import * as SchemaIssue from "./SchemaIssue.ts" import * as SchemaParser from "./SchemaParser.ts" @@ -62,7 +62,7 @@ import type * as SchemaRepresentation from "./SchemaRepresentation.ts" import * as SchemaTransformation from "./SchemaTransformation.ts" import type { Assign, Lambda, Mutable, Simplify } from "./Struct.ts" import * as Struct_ from "./Struct.ts" -import * as FastCheck from "./testing/FastCheck.ts" +import type * as FastCheck from "./testing/FastCheck.ts" import type { RequiredKeys, UnionToIntersection } from "./Types.ts" import type { Unify } from "./Unify.ts" @@ -108,8 +108,8 @@ export type ConstructorDefault = "no-default" | "with-default" * Use when passing `disableChecks: true` to skip validation when you trust the data. * - Pass `parseOptions` to control error reporting behavior. * - * @see {@link Bottom.makeEffect} - * @see {@link Bottom.make} + * @see {@link BottomWithoutNew.makeEffect} + * @see {@link BottomWithoutNew.make} * * @category options * @since 3.13.4 @@ -132,19 +132,19 @@ export interface MakeOptions { } /** - * The fully-parameterized base interface for all schemas. Exposes all 14 type - * parameters controlling type inference, mutability, optionality, services, and - * transformation behavior. + * The fully-parameterized schema interface without a construct signature. + * Exposes all 14 type parameters controlling type inference, mutability, + * optionality, services, and transformation behavior. * * **When to use** * - * Use when you are writing advanced generic schema utilities or performing - * schema introspection. + * Use as the base for schema interfaces that provide a specialized construct + * signature. * * @category models * @since 4.0.0 */ -export interface Bottom< +export interface BottomWithoutNew< out T, out E, out RD, @@ -202,12 +202,14 @@ export interface Bottom< * **Gotchas** * * Throws an `Error` with the schema issue in its `cause` when validation - * fails. + * fails. Schema validation failures use the generic message + * `"Schema validation failed"`; format the `cause` explicitly with + * `SchemaIssue.makeFormatterDefault()` when human-readable details are needed. * Causes that contain defects, interruptions, or other non-schema reasons * throw with the underlying `Cause` attached instead. * - * @see {@link Bottom.makeOption} — construct synchronously and discard validation details - * @see {@link Bottom.makeEffect} — construct through `Effect` when validation failure should stay in the error channel + * @see {@link BottomWithoutNew.makeOption} — construct synchronously and discard validation details + * @see {@link BottomWithoutNew.makeEffect} — construct through `Effect` when validation failure should stay in the error channel */ make(input: this["~type.make.in"], options?: MakeOptions): this["Type"] /** @@ -230,8 +232,8 @@ export interface Bottom< * that contain defects, interruptions, or other non-schema reasons throw * instead. * - * @see {@link Bottom.make} — construct synchronously when validation failure should throw - * @see {@link Bottom.makeEffect} — construct through `Effect` when validation details should stay in the error channel + * @see {@link BottomWithoutNew.make} — construct synchronously when validation failure should throw + * @see {@link BottomWithoutNew.makeEffect} — construct through `Effect` when validation details should stay in the error channel */ makeOption(input: this["~type.make.in"], options?: MakeOptions): Option_.Option /** @@ -243,38 +245,100 @@ export interface Bottom< * Use when constructor input may fail validation and you want to * compose that failure with other `Effect` operations instead of throwing. * - * @see {@link Bottom.make} — construct synchronously when validation failure should throw - * @see {@link Bottom.makeOption} — construct synchronously and discard validation details + * **Details** + * + * Validation failures are returned directly as `SchemaIssue.Issue` values + * and are not wrapped in `SchemaError`. + * + * @see {@link BottomWithoutNew.make} — construct synchronously when validation failure should throw + * @see {@link BottomWithoutNew.makeOption} — construct synchronously and discard validation details */ - makeEffect(input: this["~type.make.in"], options?: MakeOptions): Effect.Effect + makeEffect(input: this["~type.make.in"], options?: MakeOptions): Effect.Effect +} + +/** + * Fully-parameterized base interface for schemas that can be extended directly + * by TypeScript classes. + * + * **When to use** + * + * Use as the base for concrete schema interfaces whose runtime values support + * `class ... extends schema`. + * + * **Details** + * + * Extends {@link BottomWithoutNew} with a construct signature that accepts `never`. The + * signature enables class extension without making ordinary schemas directly + * constructible. + * + * @see {@link BottomWithoutNew} for the schema protocol without a construct signature + * + * @category utility types + * @since 4.0.0 + */ +export interface Bottom< + out T, + out E, + out RD, + out RE, + out Ast extends SchemaAST.AST, + out Rebuild extends Top, + out TypeMakeIn = T, + out Iso = T, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMake = TypeMakeIn, + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends + BottomWithoutNew< + T, + E, + RD, + RE, + Ast, + Rebuild, + TypeMakeIn, + Iso, + TypeParameters, + TypeMake, + TypeMutability, + TypeOptionality, + TypeConstructorDefault, + EncodedMutability, + EncodedOptionality + > +{ + new(_: never): {} } /** - * Lazy `Bottom` variant for schema implementations that compute their public - * views on demand. + * Lazy `BottomWithoutNew` variant for schema implementations that + * compute their public views on demand. * * **When to use** * - * Use as an implementation base for schema interfaces that must expose - * `Bottom` behavior without forcing TypeScript to eagerly evaluate expensive - * `Type`, `Encoded`, or service views. + * Use as the base for lazy schema interfaces that provide a specialized + * construct signature. * * **Details** * * The laziness is purely type-level; runtime behavior is unchanged. - * `BottomLazy` keeps the structural operations inherited from `Bottom`, but - * erases the expensive schema views to `unknown`. Concrete schema interfaces can - * then redeclare the precise views they expose. This keeps wide schemas such as - * `Struct` and `Union` cheaper when generic code reads a single view, while - * preserving their exact public types. + * `BottomLazyWithoutNew` keeps the structural operations inherited from + * `BottomWithoutNew`, but erases the expensive schema views to + * `unknown`. Concrete schema interfaces can then redeclare the precise views + * they expose. This keeps wide schemas such as `Struct` and `Union` cheaper when + * generic code reads a single view, while preserving their exact public types. * - * @see {@link Bottom} for the fully parameterized schema interface when every + * @see {@link BottomWithoutNew} for the fully parameterized schema interface when every * view must be supplied directly. * * @category utility types * @since 4.0.0 */ -export interface BottomLazy< +export interface BottomLazyWithoutNew< out Ast extends SchemaAST.AST, out Rebuild extends Top, in out TypeParameters extends ReadonlyArray = readonly [], @@ -284,7 +348,7 @@ export interface BottomLazy< out EncodedMutability extends Mutability = "readonly", out EncodedOptionality extends Optionality = "required" > extends - Bottom< + BottomWithoutNew< unknown, unknown, unknown, @@ -303,6 +367,50 @@ export interface BottomLazy< > {} +/** + * Lazy `Bottom` variant for schemas that can be extended directly by TypeScript + * classes. + * + * **When to use** + * + * Use as the base for concrete lazy schema interfaces whose runtime values + * support `class ... extends schema`. + * + * **Details** + * + * Extends {@link BottomLazyWithoutNew} with a construct signature that accepts `never`. + * The signature enables class extension without making ordinary schemas + * directly constructible. + * + * @see {@link BottomLazyWithoutNew} for the lazy schema protocol without a construct signature + * + * @category utility types + * @since 4.0.0 + */ +export interface BottomLazy< + out Ast extends SchemaAST.AST, + out Rebuild extends Top, + in out TypeParameters extends ReadonlyArray = readonly [], + out TypeMutability extends Mutability = "readonly", + out TypeOptionality extends Optionality = "required", + out TypeConstructorDefault extends ConstructorDefault = "no-default", + out EncodedMutability extends Mutability = "readonly", + out EncodedOptionality extends Optionality = "required" +> extends + BottomLazyWithoutNew< + Ast, + Rebuild, + TypeParameters, + TypeMutability, + TypeOptionality, + TypeConstructorDefault, + EncodedMutability, + EncodedOptionality + > +{ + new(_: never): {} +} + /** * Type-level representation returned by {@link declareConstructor}. * @@ -346,8 +454,8 @@ export interface declareConstructor` type) * - * ```ts - * import { Effect, Option, Schema, SchemaIssue as Issue, SchemaParser } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schema, SchemaIssue, SchemaParser } from "effect" * * interface Box { * readonly value: A @@ -362,7 +470,7 @@ export interface declareConstructor * (u, ast, options) => { * if (!isBox(u)) { - * return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(u))) + * return Effect.fail(new SchemaIssue.InvalidType(ast, u, options)) * } * return Effect.map( * SchemaParser.decodeUnknownEffect(itemCodec)(u.value, options), @@ -372,6 +480,7 @@ export interface declareConstructor { value: 1 } * ``` * * @category constructors @@ -423,7 +532,7 @@ export interface declare extends declareConstructor extends declareConstructor "user_123" * ``` * * @see {@link declareConstructor} for creating schemas for parametric types. @@ -448,10 +558,10 @@ export function declare( ): declare { return declareConstructor()( [], - () => (input, ast) => + () => (input, ast, options) => is(input) ? Effect.succeed(input) : - Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))), + Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), annotations ) } @@ -468,7 +578,7 @@ export function declare( * * **Example** (Inspecting all type parameters of a schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.String @@ -519,15 +629,16 @@ export function revealBottom( * * **Example** (Adding a title and description) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * - * const Age = Schema.Number.pipe( + * const Age = Schema.Natural.pipe( * Schema.annotate({ * title: "Age", * description: "A non-negative integer representing age in years" * }) * ) + * Schema.resolveAnnotations(Age)?.title // => "Age" * ``` * * @see {@link annotateEncoded} to annotate the encoded side instead. @@ -551,7 +662,7 @@ export function annotate(annotations: Annotations.Bottom(annotations: Annotations.Bottom "my title" * ``` * * @see {@link annotate} to annotate the type side instead. @@ -587,7 +697,7 @@ export function annotateEncoded(annotations: Annotations.Bottom(annotations: Annotations.Bottom "Username is required" * ``` * * @category annotations @@ -779,7 +890,7 @@ export declare namespace Schema { * * **Example** (Extracting the decoded type) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) @@ -808,13 +919,13 @@ export declare namespace Schema { * * **Example** (Accepting any schema decoding to `string`) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * - * declare function print(schema: Schema.Schema): void + * const accept = (_schema: Schema.Schema): void => {} * - * print(Schema.String) // ok - * print(Schema.NonEmptyString) // ok + * accept(Schema.String) + * accept(Schema.NonEmptyString) * ``` * * @see {@link Codec} — also tracks Encoded, DecodingServices, EncodingServices @@ -839,7 +950,7 @@ export declare namespace Codec { * * **Example** (Extracting the encoded type) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.NumberFromString @@ -857,7 +968,7 @@ export declare namespace Codec { * * **Example** (Checking decoding service requirements) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.String @@ -875,7 +986,7 @@ export declare namespace Codec { * * **Example** (Checking encoding service requirements) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.String @@ -906,12 +1017,13 @@ export declare namespace Codec { * * **Example** (Accepting a codec that decodes to `number` from `string`) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * - * declare function serialize(codec: Schema.Codec): string + * const serialize = (codec: Schema.Codec, value: T): string => + * Schema.encodeSync(codec)(value) * - * serialize(Schema.NumberFromString) // ok — decodes number, encoded as string + * serialize(Schema.NumberFromString, 42) // => "42" * ``` * * @see {@link Codec.Encoded} — extract the encoded type @@ -987,7 +1099,7 @@ export interface Encoder extends Schema { * * **Example** (Recovering encoded type from a schema variable) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema: Schema.Schema = Schema.NumberFromString @@ -1029,60 +1141,75 @@ export interface Optic extends Schema { readonly "Rebuild": Optic } -export { - /** - * Returns `true` if `u` is a {@link SchemaError}. - * - * **Example** (Narrowing Schema errors in a catch block) - * - * ```ts - * import { Schema } from "effect" - * - * try { - * Schema.decodeUnknownSync(Schema.Number)("oops") - * } catch (err) { - * if (Schema.isSchemaError(err)) { - * console.log(err._tag) // "SchemaError" - * } - * } - * ``` - * - * @category guards - * @since 4.0.0 - */ - isSchemaError, - /** - * Error thrown (or returned as the error channel value) when schema decoding - * or encoding fails. - * - * **Details** - * - * The `issue` field contains a structured {@link SchemaIssue.Issue} tree describing - * every validation failure, including the path to the problematic value, - * expected types, and actual values received. `message` renders the issue tree - * as a human-readable string. - * - * Use {@link isSchemaError} to narrow an unknown value to `SchemaError`. - * - * **Example** (Catching a SchemaError) - * - * ```ts - * import { Schema } from "effect" - * - * try { - * Schema.decodeUnknownSync(Schema.Number)("not a number") - * } catch (err) { - * if (Schema.isSchemaError(err)) { - * console.log(err.message) - * // Expected number, actual "not a number" - * } - * } - * ``` - * - * @category errors - * @since 4.0.0 - */ - SchemaError +const SchemaErrorTypeId = "~effect/SchemaError/SchemaError" + +/** + * Error thrown or returned when schema decoding or encoding fails. + * + * **Details** + * + * The `issue` field contains a structured {@link SchemaIssue.Issue} tree describing + * every validation failure, including the path to the problematic value and + * the expected type or constraint. The `message` field renders the issue tree + * with the default formatter. + * + * **Gotchas** + * + * Parsing with `reportInput: true` adds an enumerable `input` field to + * value-bearing issues. Built-in messages may include reported input, and + * custom annotations or messages are not sanitized. + * + * **Example** (Inspecting a SchemaError) + * + * ```ts import.meta.vitest + * import { Result, Schema } from "effect" + * + * const result = Schema.decodeUnknownResult(Schema.Number)("not a number") + * const message = Result.isFailure(result) ? result.failure.message : "" + * message // => "Expected number" + * ``` + * + * @see {@link isSchemaError} for narrowing unknown values + * @category errors + * @since 4.0.0 + */ +export class SchemaError extends Data.TaggedError("SchemaError")<{ + readonly issue: SchemaIssue.Issue +}> { + readonly [SchemaErrorTypeId]: typeof SchemaErrorTypeId = SchemaErrorTypeId + constructor(issue: SchemaIssue.Issue) { + super({ issue }) + } + override get message() { + return SchemaIssue.defaultFormatter(this.issue) + } + override toString() { + return `SchemaError(${this.message})` + } +} + +/** + * Returns `true` if `u` is a {@link SchemaError}. + * + * **When to use** + * + * Use when you need to narrow an unknown value to `SchemaError`. + * + * **Example** (Narrowing Schema errors) + * + * ```ts import.meta.vitest + * import { Result, Schema } from "effect" + * + * const result = Result.try(() => Schema.decodeUnknownSync(Schema.Number)("oops")) + * const error: unknown = Result.isFailure(result) ? result.failure : undefined + * Schema.isSchemaError(error) // => true + * ``` + * + * @category guards + * @since 4.0.0 + */ +export function isSchemaError(u: unknown): u is SchemaError { + return Predicate.hasProperty(u, SchemaErrorTypeId) && u[SchemaErrorTypeId] === SchemaErrorTypeId } function makeStandardResult(exit: Exit_.Exit>): StandardSchemaV1.Result { @@ -1104,7 +1231,7 @@ function makeStandardResult(exit: Exit_.Exit>): St * * **Example** (Creating a standard schema from a regular schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * // Define custom hook functions for error formatting @@ -1130,7 +1257,7 @@ function makeStandardResult(exit: Exit_.Exit>): St * // Create a standard schema from a regular schema * const PersonSchema = Schema.Struct({ * name: Schema.NonEmptyString, - * age: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 150 })) + * age: Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 150 })) * }) * * const standardSchema = Schema.toStandardSchemaV1(PersonSchema, { @@ -1142,16 +1269,21 @@ function makeStandardResult(exit: Exit_.Exit>): St * name: "Alice", * age: 30 * }) - * console.log(validResult) // { value: { name: "Alice", age: 30 } } - * * const invalidResult = standardSchema["~standard"].validate({ * name: "", * age: 200 * }) - * console.log(invalidResult) // { issues: [{ path: ["name"], message: "..." }, { path: ["age"], message: "..." }] } + * + * if (validResult instanceof Promise || invalidResult instanceof Promise) { + * throw new Error("Expected synchronous validation") + * } + * if ("value" in validResult) { + * validResult.value // => { name: "Alice", age: 30 } + * } + * invalidResult.issues?.map((issue) => issue.path) // => [["name"], ["age"]] * ``` * - * @category Standard Schema + * @category converting * @since 4.0.0 */ export function toStandardSchemaV1>( @@ -1169,7 +1301,7 @@ export function toStandardSchemaV1>( const parseOptions: SchemaAST.ParseOptions = { errors: "all", ...options?.parseOptions } const formatter = SchemaIssue.makeFormatterStandardSchemaV1(options) const validate: StandardSchemaV1["~standard"]["validate"] = (value: unknown) => { - const scheduler = new Scheduler.MixedScheduler() + const scheduler = new Scheduler.MixedScheduler("sync") const fiber = Effect.runFork( Effect.match(decodeUnknownEffect(value, parseOptions), { onFailure: formatter, @@ -1230,7 +1362,7 @@ function toBaseStandardJSONSchemaV1(self: Constraint, target: StandardJSONSchema * * https://github.com/standard-schema/standard-schema/pull/134 * - * @category Standard Schema + * @category converting * @since 4.0.0 */ export function toStandardJSONSchemaV1( @@ -1278,19 +1410,19 @@ export function toStandardJSONSchemaV1( * * **Example** (Defining a basic type guard) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const isString = Schema.is(Schema.String) * - * console.log(isString("hello")) // true - * console.log(isString(42)) // false + * isString("hello") // => true + * isString(42) // => false * * // Type narrowing in action * const value: unknown = "hello" * if (isString(value)) { * // value is now typed as string - * console.log(value.toUpperCase()) // "HELLO" + * value.toUpperCase() // => "HELLO" * } * ``` * @@ -1312,6 +1444,9 @@ export const is = SchemaParser.is * * The input is narrowed if the assertion succeeds. If schema validation fails, * the assertion throws an `Error` whose cause is `SchemaIssue.Issue`. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -1321,21 +1456,23 @@ export const is = SchemaParser.is * * **Example** (Asserting and narrowing an input) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Schema, SchemaIssue } from "effect" * * const input: unknown = "hello" * * // This will pass silently (no return value) and narrow input to string * Schema.asserts(Schema.String, input) - * console.log(input.toUpperCase()) + * input.toUpperCase() // => "HELLO" * * // This will throw an error * try { * const invalid: unknown = 123 * Schema.asserts(Schema.String, invalid) * } catch (error) { - * console.log("Non-string assertion failed as expected") + * if (error instanceof Error) { + * SchemaIssue.isIssue(error.cause) // => true + * } * } * ``` * @@ -1372,10 +1509,19 @@ export function decodeUnknownEffect(schema: S, options?: S input: unknown, options?: SchemaAST.ParseOptions ): Effect.Effect => { - return InternalSchema.fromIssueEffect(parser(input, options)) + return fromIssueEffect(parser(input, options)) } } +function fromIssueEffect( + self: Effect.Effect +): Effect.Effect { + return Effect.catchCause( + self, + (cause) => Effect.failCauseSync(() => Cause_.map(cause, (issue) => new SchemaError(issue))) + ) +} + /** * Decodes a typed input (the schema's `Encoded` type) against a schema, * returning an `Effect` that succeeds with the decoded value or fails with a @@ -1482,7 +1628,7 @@ export function decodeUnknownExit>(schema: function fromIssueExit(exit: Exit_.Exit): Exit_.Exit { return Exit_.isSuccess(exit) - ? Exit_.succeed(exit.value) + ? exit as unknown as Exit_.Exit : Exit_.failCause(Cause_.map(exit.cause, (issue) => new SchemaError(issue))) } @@ -1745,19 +1891,12 @@ export const decodePromise: >( * * **Example** (Decoding with a transformation schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const NumberFromString = Schema.NumberFromString * - * console.log(Schema.decodeUnknownSync(NumberFromString)("42")) - * // Output: 42 - * - * Schema.decodeUnknownSync(NumberFromString)("not a number") - * // throws SchemaError: NumberFromString - * // └─ Encoded side transformation failure - * // └─ NumberFromString - * // └─ Expected a numeric string, actual "not a number" + * Schema.decodeUnknownSync(NumberFromString)("42") // => 42 * ``` * * @see {@link SchemaParser.decodeUnknownSync} for the adapter that throws an `Error` whose cause is `SchemaIssue.Issue` @@ -1821,13 +1960,12 @@ export const decodeSync: >( * * **Example** (Encoding a value to a string) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * * const NumberFromString = Schema.NumberFromString * - * Effect.runPromise(Schema.encodeUnknownEffect(NumberFromString)(42)).then(console.log) - * // Output: "42" + * await Effect.runPromise(Schema.encodeUnknownEffect(NumberFromString)(42)) // => "42" * ``` * * @see {@link SchemaParser.encodeUnknownEffect} for the adapter that fails with `SchemaIssue.Issue` directly @@ -1841,7 +1979,7 @@ export function encodeUnknownEffect(schema: S, options?: S input: unknown, options?: SchemaAST.ParseOptions ): Effect.Effect => { - return InternalSchema.fromIssueEffect(parser(input, options)) + return fromIssueEffect(parser(input, options)) } } @@ -2225,33 +2363,6 @@ export const encodeSync: >( */ export const make: (ast: S["ast"], options?: object) => S = InternalSchema.make -/** - * Transforms a schema into a class that can be extended with `extends`. The - * resulting class inherits the full schema API (e.g. `annotate`) and can define - * static methods that reference `this`. - * - * **Example** (Wrapping a primitive schema) - * - * ```ts - * import { Schema } from "effect" - * - * class MyString extends Schema.asClass(Schema.String) { - * static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) - * } - * - * console.log(MyString.decodeUnknownSync("a")) - * // "a" - * ``` - * - * @category constructors - * @since 4.0.0 - */ -export function asClass(schema: S): S & { new(_: never): {} } { - // oxlint-disable-next-line @typescript-eslint/no-extraneous-class - class Class {} - return Object.setPrototypeOf(Class, schema) -} - /** * Checks whether a value is a `Schema`. * @@ -2302,7 +2413,7 @@ interface optionalKeyLambda extends Lambda { * * **Example** (Creating a struct with optional key) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Struct({ @@ -2369,7 +2480,7 @@ interface optionalLambda extends Lambda { * * **Example** (Defining an optional field accepting undefined) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Struct({ @@ -2384,7 +2495,10 @@ interface optionalLambda extends Lambda { * @category combinators * @since 3.10.0 */ -export const optional = Struct_.lambda((self) => optionalKey(UndefinedOr(self))) +export const optional = Struct_.lambda((self) => { + const schema = UndefinedOr(self) + return make(SchemaAST.optional(self.ast), { schema }) +}) interface requiredLambda extends Lambda { (self: optional): S @@ -2604,12 +2718,12 @@ function isFlip$(schema: Top): schema is flip { * * **Example** (Flipping a number-from-string schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * // NumberFromString: decodes string → number * const flipped = Schema.flip(Schema.NumberFromString) - * // flipped: decodes number → string + * Schema.decodeSync(flipped)(42) // => "42" * ``` * * @category transforming @@ -2641,11 +2755,12 @@ export interface Literal * * **Example** (Defining a string literal) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Literal("hello") * // Type: Schema.Literal<"hello"> + * Schema.decodeSync(schema)("hello") // => "hello" * ``` * * @see {@link Literals} for a schema that represents a union of literals. @@ -2773,11 +2888,11 @@ function templateLiteralFromParts(parts: Pa * * **Example** (Defining a URL path pattern) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.TemplateLiteral(["/user/", Schema.Number]) - * // matches strings like "/user/123", "/user/42", etc. + * Schema.is(schema)("/user/123") // => true * ``` * * @see {@link TemplateLiteralParser} for a schema that also parses matched parts into a tuple. @@ -2852,11 +2967,11 @@ export interface TemplateLiteralParser exte * * **Example** (Parsing path parameters) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.TemplateLiteralParser(["/user/", Schema.NumberFromString]) - * // decodes "/user/42" => readonly ["/user/", 42] + * Schema.decodeSync(schema)("/user/42") // => ["/user/", 42] * ``` * * @see {@link TemplateLiteral} for a validation-only version that keeps the string encoded. @@ -2886,7 +3001,7 @@ export interface Enum * * **Example** (Defining a direction enum) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * enum Direction { @@ -2895,7 +3010,7 @@ export interface Enum * } * * const schema = Schema.Enum(Direction) - * // accepts "Up" or "Down" + * Schema.decodeSync(schema)(Direction.Up) // => "Up" * ``` * * @category constructors @@ -3058,7 +3173,7 @@ export interface Boolean extends Bottom * * **Example** (Defining a specific symbol) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const mySymbol = Symbol.for("mySymbol") * const schema = Schema.UniqueSymbol(mySymbol) + * Schema.decodeSync(schema)(mySymbol) === mySymbol // => true * ``` * * @see {@link Symbol} for a schema that accepts any symbol. @@ -3325,7 +3441,7 @@ export declare namespace Struct { type MakeInView< F extends Fields, O extends keyof F = TypeOptionalKeys | TypeConstructorDefaultedKeys - > = [O] extends [never] ? ReadonlyMakeIn : Simplify, O>> + > = [O] extends [never] ? Simplify> : Simplify, O>> /** * Computes the input object type accepted when constructing a struct value. @@ -3361,7 +3477,7 @@ export interface Struct extends BottomLazy extends BottomLazy ["createdAt", "updatedAt", "name", "email"] * ``` */ readonly fields: Fields @@ -3430,7 +3547,7 @@ function makeStruct(ast: SchemaAST.Objects, * * **Example** (Defining a basic struct) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const Person = Schema.Struct({ @@ -3442,9 +3559,7 @@ function makeStruct(ast: SchemaAST.Objects, * // { readonly name: string; readonly age: number; readonly email?: string } * type Person = typeof Person.Type * - * const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) - * console.log(alice) - * // { name: 'Alice', age: 30 } + * Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 } * ``` * * @category constructors @@ -3477,7 +3592,7 @@ interface fieldsAssign extends Lambda { * * **Example** (Adding fields to a union of structs) * - * ```ts + * ```ts import.meta.vitest * import { Schema, Tuple } from "effect" * * // Add a new field to all members of a union of structs @@ -3485,6 +3600,7 @@ interface fieldsAssign extends Lambda { * Schema.Struct({ a: Schema.String }), * Schema.Struct({ b: Schema.Number }) * ]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number }))) + * Schema.decodeSync(schema)({ a: "a", c: 1 }) // => { a: "a", c: 1 } * ``` * * @category combinators @@ -3532,16 +3648,14 @@ const canonicalPropertyKey = (key: PropertyKey): string | symbol => * * **Example** (Renaming `name` to `full_name` in the encoded form) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const Person = Schema.Struct({ name: Schema.String, age: Schema.Number }) * const Encoded = Person.pipe(Schema.encodeKeys({ name: "full_name" })) * * // Decodes { full_name: "Alice", age: 30 } → { name: "Alice", age: 30 } - * const alice = Schema.decodeUnknownSync(Encoded)({ full_name: "Alice", age: 30 }) - * console.log(alice) - * // { name: 'Alice', age: 30 } + * Schema.decodeUnknownSync(Encoded)({ full_name: "Alice", age: 30 }) // => { name: "Alice", age: 30 } * ``` * * @category transforming @@ -3553,8 +3667,8 @@ export function encodeKeys< >(mapping: M) { return function(self: S): encodeKeys { const fields: any = {} - const appliedMapping: any = {} - const reverseMapping: any = {} + const appliedMapping: any = Object.create(null) + const reverseMapping: any = Object.create(null) const seenEncodedKeys = new Set() for (const k of Reflect.ownKeys(self.fields)) { const encoded = toEncoded(self.fields[k]) @@ -3565,7 +3679,7 @@ export function encodeKeys< throw new globalThis.Error(`Duplicate encoded keys: ${formatPropertyKey(encodedKey)}`) } seenEncodedKeys.add(canonical) - fields[encodedKey] = encoded + InternalRecord.assignProperty(fields, encodedKey, encoded) if (hasMapping) { appliedMapping[k] = encodedKey reverseMapping[encodedKey] = k @@ -3593,7 +3707,7 @@ export function encodeKeys< * * **Example** (Adding a computed `fullName` field) * - * ```ts + * ```ts import.meta.vitest * import { Option, Schema } from "effect" * * const Person = Schema.Struct({ first: Schema.String, last: Schema.String }) @@ -3605,8 +3719,7 @@ export function encodeKeys< * ) * * const alice = Schema.decodeUnknownSync(Extended)({ first: "Alice", last: "Smith" }) - * console.log(alice.fullName) - * // Alice Smith + * alice.fullName // => "Alice Smith" * ``` * * @category transforming @@ -3632,7 +3745,7 @@ export function extendTo, const Fields extends S const f = derive[k] const o = f(input) if (Option_.isSome(o)) { - out[k] = o.value + InternalRecord.assignProperty(out, k, o.value) } } return out @@ -3809,9 +3922,16 @@ export interface $Record exten * For transformed key schemas, property selection is based on encoded property * names before the selected key is decoded. * + * **Gotchas** + * + * When decoded or encoded key transformations produce the same property key, + * sequential parsing applies selected own properties in selection order, so + * the later selected property overwrites the earlier value. With concurrency + * greater than `1`, completion order determines which value is retained. + * * **Example** (Defining a string-keyed record of numbers) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Record(Schema.String, Schema.Number) @@ -3819,9 +3939,7 @@ export interface $Record exten * // { readonly [x: string]: number } * type R = typeof schema.Type * - * const result = Schema.decodeUnknownSync(schema)({ a: 1, b: 2 }) - * console.log(result) - * // { a: 1, b: 2 } + * Schema.decodeUnknownSync(schema)({ a: 1, b: 2 }) // => { a: 1, b: 2 } * ``` * * @category constructors @@ -3829,18 +3947,9 @@ export interface $Record exten */ export function Record( key: Key, - value: Value, - options?: { - readonly keyValueCombiner: { - readonly decode?: Combiner.Combiner | undefined - readonly encode?: Combiner.Combiner | undefined - } - } + value: Value ): $Record { - const keyValueCombiner = options?.keyValueCombiner?.decode || options?.keyValueCombiner?.encode - ? new SchemaAST.KeyValueCombiner(options.keyValueCombiner.decode, options.keyValueCombiner.encode) - : undefined - return make(SchemaAST.record(key.ast, value.ast, keyValueCombiner), { key, value }) + return make(SchemaAST.record(key.ast, value.ast), { key, value }) } /** @@ -3984,7 +4093,7 @@ export declare namespace StructWithRest { * * **Example** (Checking record compatibility) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const user = Schema.Struct({ id: Schema.String }) @@ -4055,7 +4164,7 @@ export interface StructWithRest< * * **Example** (Defining structs with string-indexed extra keys) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.StructWithRest( @@ -4276,14 +4385,12 @@ function makeTuple(ast: SchemaAST.Arrays, eleme * * **Example** (Defining a pair of string and number) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Tuple([Schema.String, Schema.Number]) * - * const pair = Schema.decodeUnknownSync(schema)(["hello", 42]) - * console.log(pair) - * // [ 'hello', 42 ] + * Schema.decodeUnknownSync(schema)(["hello", 42]) // => ["hello", 42] * ``` * * @category constructors @@ -4454,7 +4561,7 @@ export interface TupleWithRest< * * **Example** (Defining tuples with rest elements) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * // [string, number, ...boolean[]] @@ -4463,9 +4570,7 @@ export interface TupleWithRest< * [Schema.Boolean] * ) * - * const result = Schema.decodeUnknownSync(schema)(["hello", 1, true, false]) - * console.log(result) - * // [ 'hello', 1, true, false ] + * Schema.decodeUnknownSync(schema)(["hello", 1, true, false]) // => ["hello", 1, true, false] * ``` * * @category constructors @@ -4519,14 +4624,12 @@ export { * * **Example** (Defining an array of strings) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Array(Schema.String) * - * const result = Schema.decodeUnknownSync(schema)(["a", "b", "c"]) - * console.log(result) - * // [ 'a', 'b', 'c' ] + * Schema.decodeUnknownSync(schema)(["a", "b", "c"]) // => ["a", "b", "c"] * ``` * * @category constructors @@ -4568,13 +4671,12 @@ interface NonEmptyArrayLambda extends Lambda { * * **Example** (Defining a non-empty array of numbers) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.NonEmptyArray(Schema.Number) * - * Schema.decodeUnknownSync(schema)([1, 2, 3]) // ok - * Schema.decodeUnknownSync(schema)([]) // throws + * Schema.decodeUnknownSync(schema)([1, 2, 3]) // => [1, 2, 3] * ``` * * @category constructors @@ -4696,13 +4798,16 @@ interface mutableLambda extends Lambda { * * **Example** (Defining mutable arrays) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.mutable(Schema.Array(Schema.Number)) * * // number[] (mutable) * type T = typeof schema.Type + * const value: T = [1, 2] + * value.push(3) + * value // => [1, 2, 3] * ``` * * @category transforming @@ -4790,13 +4895,13 @@ function makeUnion>( * * **Example** (Defining a string or number union) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Union([Schema.String, Schema.Number]) * - * Schema.decodeUnknownSync(schema)("hello") // "hello" - * Schema.decodeUnknownSync(schema)(42) // 42 + * Schema.decodeUnknownSync(schema)("hello") // => "hello" + * Schema.decodeUnknownSync(schema)(42) // => 42 * ``` * * @category constructors @@ -4837,11 +4942,11 @@ export interface Literals> * * **Example** (Defining status codes) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.Literals(["active", "inactive", "pending"]) - * // accepts "active", "inactive", or "pending" + * Schema.decodeSync(schema)("active") // => "active" * ``` * * @see {@link Literal} for a schema that represents a single literal. @@ -4973,7 +5078,7 @@ export interface suspend extends * * **Example** (Defining recursive tree schemas) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * interface Tree { @@ -4985,6 +5090,7 @@ export interface suspend extends * value: Schema.Number, * children: Schema.Array(Schema.suspend((): Schema.Codec => Tree)) * }) + * Schema.decodeSync(Tree)({ value: 1, children: [] }) // => { value: 1, children: [] } * ``` * * @category constructors @@ -5000,12 +5106,14 @@ export function suspend(f: () => S): suspend { * * **Example** (Adding checks to a schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * - * const AgeSchema = Schema.Number.pipe( + * const AgeSchema = Schema.Finite.pipe( * Schema.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(120)) * ) + * Schema.is(AgeSchema)(42) // => true + * Schema.is(AgeSchema)(121) // => false * ``` * * @category filtering @@ -5177,14 +5285,17 @@ export interface middlewareDecoding extends * * **Example** (Logging decode failures) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * + * const events: Array = [] * const Logged = Schema.String.pipe( * Schema.middlewareDecoding((effect) => - * Effect.tapError(effect, (issue) => Effect.log("decode failed", issue)) + * Effect.tapError(effect, () => Effect.sync(() => events.push("decode failed"))) * ) * ) + * Effect.runSync(Effect.result(Schema.decodeUnknownEffect(Logged)(42))) + * events // => ["decode failed"] * ``` * * @see {@link catchDecoding} for a simpler error-recovery variant @@ -5243,14 +5354,17 @@ export interface middlewareEncoding extends * * **Example** (Logging encode failures) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * + * const events: Array = [] * const Logged = Schema.String.pipe( * Schema.middlewareEncoding((effect) => - * Effect.tapError(effect, (issue) => Effect.log("encode failed", issue)) + * Effect.tapError(effect, () => Effect.sync(() => events.push("encode failed"))) * ) * ) + * Effect.runSync(Effect.result(Schema.encodeUnknownEffect(Logged)(42))) + * events // => ["encode failed"] * ``` * * @see {@link catchEncoding} for a simpler error-recovery variant @@ -5280,12 +5394,13 @@ export function middlewareEncoding( * * **Example** (Returning a default on decode failure) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Schema } from "effect" * * const schema = Schema.Number.pipe( * Schema.catchDecoding((_issue) => Effect.succeed(Option.some(0))) * ) + * Effect.runSync(Schema.decodeUnknownEffect(schema)("invalid")) // => 0 * ``` * * @see {@link catchDecodingWithContext} to add service requirements to the handler @@ -5435,7 +5550,7 @@ export interface compose extends * * **Example** (Transforming strings to numbers with a schema transformation) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * const NumberFromString = Schema.String.pipe( @@ -5448,8 +5563,7 @@ export interface compose extends * ) * ) * - * const result = Schema.decodeUnknownSync(NumberFromString)("123") - * // result: 123 + * Schema.decodeUnknownSync(NumberFromString)("123") // => 123 * ``` * * @category transforming @@ -5511,7 +5625,7 @@ export function decodeTo "hello" * ``` * * @category transforming - * @since 3.10.0 + * @since 4.0.0 */ export function decode(transformation: { readonly decode: SchemaGetter.Getter @@ -5552,7 +5665,7 @@ export function decode(transformat * * **Example** (Encoding a number back to a string) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * const NumberFromString = Schema.Number.pipe( @@ -5561,6 +5674,7 @@ export function decode(transformat * encode: SchemaGetter.transform((n: number) => String(n)) * }) * ) + * Schema.decodeSync(NumberFromString)("42") // => 42 * ``` * * @category transforming @@ -5601,7 +5715,7 @@ export function encodeTo s.toUpperCase()) * }) * ) + * Schema.encodeSync(UpperFromLower)("hello") // => "HELLO" * ``` * * @category transforming - * @since 3.10.0 + * @since 4.0.0 */ export function encode(transformation: { readonly decode: SchemaGetter.Getter @@ -5672,11 +5787,11 @@ export interface withConstructorDefault "anonymous" * ``` * * @category constructors @@ -5696,10 +5810,10 @@ export interface withConstructorDefault( // `S["~type.make.in"]` instead of `S["Type"]` is intentional here because // it makes easier to define the default value if there are nested defaults - defaultValue: Effect.Effect + defaultValue: Effect.Effect ) { return (schema: S): withConstructorDefault => - make(SchemaAST.withConstructorDefault(schema.ast, toIssueEffect(defaultValue)), { schema }) + make(SchemaAST.withConstructorDefault(schema.ast, defaultValue), { schema }) } function toIssueEffect( @@ -5754,15 +5868,14 @@ export type DecodingDefaultOptions = { * * **Example** (Providing a default for a missing struct key) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * * const MySchema = Schema.Struct({ * name: Schema.String.pipe(Schema.withDecodingDefaultKey(Effect.succeed("anonymous"))) * }) * - * const result = Schema.decodeUnknownSync(MySchema)({}) - * // result: { name: "anonymous" } + * Schema.decodeUnknownSync(MySchema)({}).name // => "anonymous" * ``` * * @see {@link withDecodingDefault} for the value-level variant (key absent **or** `undefined`) @@ -5862,15 +5975,14 @@ export interface withDecodingDefault extends de * * **Example** (Providing a default for an optional field value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * * const MySchema = Schema.Struct({ * name: Schema.String.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed("anonymous"))) * }) * - * const result = Schema.decodeUnknownSync(MySchema)({ name: undefined }) - * // result: { name: "anonymous" } + * Schema.decodeUnknownSync(MySchema)({ name: undefined }).name // => "anonymous" * ``` * * @see {@link withDecodingDefaultKey} for the key-level variant (key absent only, not `undefined`) @@ -5957,14 +6069,14 @@ export interface tag extends withConstructor * * **Example** (Defining a discriminated union tag) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const A = Schema.Struct({ _tag: Schema.tag("A"), value: Schema.Number }) * * // _tag is optional in make, auto-filled to "A" * const a = A.make({ value: 42 }) - * // a: { _tag: "A", value: 42 } + * a // => { _tag: "A", value: 42 } * ``` * * @see {@link tagDefaultOmit} to also omit the tag during encoding @@ -5991,7 +6103,7 @@ export function tag(literal: Tag): tag * * **Example** (Omitting tags during encoding) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const A = Schema.Struct({ @@ -6000,8 +6112,7 @@ export function tag(literal: Tag): tag * }) * * // Encode strips the _tag field - * const encoded = Schema.encodeUnknownSync(A)({ _tag: "A", value: 1 }) - * // encoded: { value: 1 } + * Schema.encodeUnknownSync(A)({ _tag: "A", value: 1 }) // => { value: 1 } * ``` * * @see {@link tag} for the variant that keeps the tag during encoding @@ -6037,7 +6148,7 @@ export type TaggedStruct "A" * ``` * * @category constructors @@ -6137,7 +6249,7 @@ export type toTaggedUnion< * * **Example** (Adding tagged-union utilities to an existing union) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const A = Schema.TaggedStruct("A", { value: Schema.Number }) @@ -6150,6 +6262,7 @@ export type toTaggedUnion< * A: (a) => `number: ${a.value}`, * B: (b) => `name: ${b.name}` * }) + * result // => "number: 1" * ``` * * @see {@link TaggedUnion} for a shorthand that builds the union from scratch @@ -6190,8 +6303,8 @@ export function toTaggedUnion(tag: Tag) { } discriminantKeys.add(key) discriminants.push(literal) - InternalRecord.set(cases, literal, schema) - InternalRecord.set(guards, literal, is(toType(schema))) + InternalRecord.assignProperty(cases, literal, schema) + InternalRecord.assignProperty(guards, literal, is(toType(schema))) return } } @@ -6203,12 +6316,16 @@ export function toTaggedUnion(tag: Tag) { if (arguments.length === 1) { const cases = arguments[0] return function(value: any) { - return cases[value[tag]](value) + const key = value[tag] + const handler = Object.hasOwn(cases, key) ? cases[key] : undefined + return handler(value) } } const value = arguments[0] const cases = arguments[1] - return cases[value[tag]](value) + const key = value[tag] + const handler = Object.hasOwn(cases, key) ? cases[key] : undefined + return handler(value) } } } @@ -6255,7 +6372,7 @@ export interface TaggedUnion> extends * * **Example** (Pattern matching a discriminated union) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const Shape = Schema.TaggedUnion({ @@ -6268,6 +6385,7 @@ export interface TaggedUnion> extends * Circle: (c) => Math.PI * c.radius ** 2, * Rectangle: (r) => r.width * r.height * }) + * Math.round(area * 100) / 100 // => 78.54 * ``` * * @see {@link toTaggedUnion} to augment an existing union instead @@ -6280,7 +6398,9 @@ export function TaggedUnion extends - BottomLazy< + BottomLazyWithoutNew< S["ast"], S["Rebuild"], S["~type.parameters"], @@ -6322,7 +6442,7 @@ export interface Opaque extends * * **Example** (Defining opaque structs) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * class Person extends Schema.Opaque()( @@ -6333,7 +6453,7 @@ export interface Opaque extends * * // Decoded value is Person, not { name: string } * const person = Schema.decodeUnknownSync(Person)({ name: "Alice" }) - * // person: Person + * person.name // => "Alice" * ``` * * @category constructors @@ -6341,9 +6461,7 @@ export interface Opaque extends */ export function Opaque() { return (schema: S): Opaque & Omit => { - // oxlint-disable-next-line @typescript-eslint/no-extraneous-class - class Opaque {} - return Object.setPrototypeOf(Opaque, schema) + return schema as any } } @@ -6363,13 +6481,13 @@ export interface instanceOf extends declare { * * **Example** (Defining a schema for a built-in class) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const DateSchema = Schema.instanceOf(Date) * * const decoded = Schema.decodeUnknownSync(DateSchema)(new Date("2024-01-01")) - * // decoded: Date + * decoded.toISOString() // => "2024-01-01T00:00:00.000Z" * ``` * * @category constructors @@ -6421,8 +6539,8 @@ export function link() { * * **Example** (Reporting failure at a nested path) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Result, Schema } from "effect" * * const schema = Schema.Struct({ password: Schema.String, confirmPassword: Schema.String }).check( * Schema.makeFilter((o) => @@ -6432,15 +6550,16 @@ export function link() { * ) * ) * - * console.log(String(Schema.decodeUnknownExit(schema)({ password: "123456", confirmPassword: "1234567" }))) - * // Failure(Cause([Fail(SchemaError: password and confirmPassword must match - * // at ["password"])])) + * const result = Schema.decodeUnknownResult(schema)({ password: "123456", confirmPassword: "1234567" }) + * if (Result.isFailure(result) && result.failure.issue._tag === "Filter" && result.failure.issue.issue._tag === "Pointer") { + * result.failure.issue.issue.path // => ["password"] + * } * ``` * * **Example** (Reporting multiple failures at once) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Result, Schema } from "effect" * * const schema = Schema.Struct({ a: Schema.Finite, b: Schema.Finite, c: Schema.Finite }).check( * Schema.makeFilter((o) => { @@ -6453,11 +6572,10 @@ export function link() { * }) * ) * - * console.log(String(Schema.decodeUnknownExit(schema)({ a: 1, b: 0, c: 0 }))) - * // Failure(Cause([Fail(SchemaError: b must be greater than 0 - * // at ["b"] - * // c must be greater than 0 - * // at ["c"])])) + * const result = Schema.decodeUnknownResult(schema)({ a: 1, b: 0, c: 0 }) + * if (Result.isFailure(result) && result.failure.issue._tag === "Filter" && result.failure.issue.issue._tag === "Composite") { + * result.failure.issue.issue.issues.map((issue) => issue._tag === "Pointer" ? issue.path : []) // => [["b"], ["c"]] + * } * ``` * * @category constructors @@ -6476,13 +6594,14 @@ export const makeFilter: ( * **Details** * * - `string`: failure with that string as the message. Produces an - * {@link SchemaIssue.InvalidValue} wrapping the input, with the string used as - * the issue's `message` annotation. - * - {@link SchemaIssue.Issue}: a fully-formed issue, returned as-is. + * {@link SchemaIssue.InvalidValue} with the string used as the issue's + * `message` annotation and honors `reportInput`. + * - {@link SchemaIssue.Issue}: a fully-formed issue, returned as-is. It is not + * enriched when `reportInput` is enabled. * - `{ path, issue }`: failure attached to a nested path. `issue` is either - * a `string` (wrapped in an {@link SchemaIssue.InvalidValue}) or a full - * {@link SchemaIssue.Issue}; the result is wrapped in an {@link SchemaIssue.Pointer} - * at the given `path`. + * a `string` (wrapped in an {@link SchemaIssue.InvalidValue} that honors + * `reportInput`) or a full {@link SchemaIssue.Issue} (returned unchanged); + * the result is wrapped in an {@link SchemaIssue.Pointer} at the given `path`. * * @category models * @since 3.10.0 @@ -6503,8 +6622,8 @@ export type FilterIssue = string | SchemaIssue.Issue | { * - `undefined`: success. The input satisfies the filter. * - `true`: success. Equivalent to `undefined`, useful when the predicate is * a plain boolean expression. - * - `false`: generic failure. Produces an {@link SchemaIssue.InvalidValue} wrapping - * the input, with no custom message. + * - `false`: generic failure. Produces an {@link SchemaIssue.InvalidValue} + * with no custom message and honors `reportInput`. * - {@link FilterIssue}: a single failure. See {@link FilterIssue} for the * shapes (`string`, {@link SchemaIssue.Issue}, or `{ path, issue }`). * - `ReadonlyArray`: several failures reported together. An @@ -6535,6 +6654,17 @@ export function makeFilterGroup( return new SchemaAST.FilterGroup(checks, annotations) } +function makeFixedDeclarationReviver( + id: string, + schema: Top +): SchemaRepresentation.DeclarationReviver { + return InternalSchema.makeDeclarationReviver( + id, + Null, + ({ annotations }) => annotations === undefined ? schema : schema.annotate(annotations) + ) +} + const TRIMMED_PATTERN = "^\\S[\\s\\S]*\\S$|^\\S$|^$" /** @@ -6552,18 +6682,21 @@ const TRIMMED_PATTERN = "^\\S[\\s\\S]*\\S$|^\\S$|^$" * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the trimmed pattern. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isTrimmed(annotations?: Annotations.Filter) { + const regExp = new globalThis.RegExp(TRIMMED_PATTERN) return makeFilter( (s: string) => s.trim() === s, { expected: "a string with no leading or trailing whitespace", - meta: { - _tag: "isTrimmed", - regExp: new globalThis.RegExp(TRIMMED_PATTERN) + representation: { + id: "effect/schema/isTrimmed", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isTrimmed()" }), arbitrary: { constraint: { patterns: [TRIMMED_PATTERN] @@ -6574,6 +6707,24 @@ export function isTrimmed(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isTrimmed` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isTrimmed}. + * + * @see {@link isTrimmed} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isTrimmedReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isTrimmed", + Null, + ({ annotations }) => isTrimmed(annotations) +) + /** * Validates that a string matches the specified regular expression pattern. * @@ -6588,11 +6739,54 @@ export function isTrimmed(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the specified RegExp pattern. * - * @category String checks + * @category validation + * @since 4.0.0 + */ +export function isPattern( + regExp: globalThis.RegExp, + annotations?: Annotations.Filter +): SchemaAST.Filter { + const source = regExp.source + const flags = regExp.flags + const runtimeRegExp = flags === "" + ? `new RegExp(${format(source)})` + : `new RegExp(${format(source)}, ${format(flags)})` + return SchemaAST.isPattern(regExp, { + toCode: () => ({ runtime: `Schema.isPattern(${runtimeRegExp})` }), + ...annotations + }) +} + +const IsPatternPayload = Struct({ + source: String, + flags: String +}).check(makeFilter((payload: { readonly source: string; readonly flags: string }) => { + const result = Result_.try(() => new globalThis.RegExp(payload.source, payload.flags)) + return Result_.isSuccess(result) && + result.success.source === payload.source && + result.success.flags === payload.flags +})) + +/** + * Reviver for persisted `isPattern` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isPattern}. + * + * @see {@link isPattern} for creating the corresponding check + * + * @category validation * @since 4.0.0 */ -export const isPattern: (regExp: globalThis.RegExp, annotations?: Annotations.Filter) => SchemaAST.Filter = - SchemaAST.isPattern +export const isPatternReviver: SchemaRepresentation.FilterReviver<{ + readonly source: string + readonly flags: string +}> = { + id: "effect/schema/isPattern", + payloadSchema: IsPatternPayload, + revive: ({ annotations, payload }) => isPattern(new globalThis.RegExp(payload.source, payload.flags), annotations) +} /** * Validates that a string represents a finite number. @@ -6609,10 +6803,33 @@ export const isPattern: (regExp: globalThis.RegExp, annotations?: Annotations.Fi * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the number string pattern. * - * @category String checks + * @category validation + * @since 4.0.0 + */ +export function isStringFinite(annotations?: Annotations.Filter): SchemaAST.Filter { + return SchemaAST.isStringFinite({ + toCode: () => ({ runtime: "Schema.isStringFinite()" }), + ...annotations + }) +} + +/** + * Reviver for persisted `isStringFinite` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isStringFinite}. + * + * @see {@link isStringFinite} for creating the corresponding check + * + * @category validation * @since 4.0.0 */ -export const isStringFinite: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringFinite +export const isStringFiniteReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isStringFinite", + Null, + ({ annotations }) => isStringFinite(annotations) +) /** * Validates that a string is a signed base-10 integer literal for Effect's @@ -6628,10 +6845,33 @@ export const isStringFinite: (annotations?: Annotations.Filter) => SchemaAST.Fil * This check corresponds to a `pattern` constraint with the same signed * base-10 integer pattern. * - * @category String checks + * @category validation + * @since 4.0.0 + */ +export function isStringBigInt(annotations?: Annotations.Filter): SchemaAST.Filter { + return SchemaAST.isStringBigInt({ + toCode: () => ({ runtime: "Schema.isStringBigInt()" }), + ...annotations + }) +} + +/** + * Reviver for persisted `isStringBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isStringBigInt}. + * + * @see {@link isStringBigInt} for creating the corresponding check + * + * @category validation * @since 4.0.0 */ -export const isStringBigInt: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringBigInt +export const isStringBigIntReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isStringBigInt", + Null, + ({ annotations }) => isStringBigInt(annotations) +) /** * Validates that a string has the `Symbol(description)` format used by Effect's @@ -6642,10 +6882,33 @@ export const isStringBigInt: (annotations?: Annotations.Filter) => SchemaAST.Fil * The check uses the pattern `^Symbol\((.*)\)$`. It is not a general test for * whether a string can be passed to JavaScript's `Symbol()` function. * - * @category String checks + * @category validation * @since 4.0.0 */ -export const isStringSymbol: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isStringSymbol +export function isStringSymbol(annotations?: Annotations.Filter): SchemaAST.Filter { + return SchemaAST.isStringSymbol({ + toCode: () => ({ runtime: "Schema.isStringSymbol()" }), + ...annotations + }) +} + +/** + * Reviver for persisted `isStringSymbol` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isStringSymbol}. + * + * @see {@link isStringSymbol} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isStringSymbolReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isStringSymbol", + Null, + ({ annotations }) => isStringSymbol(annotations) +) /** * Returns a RegExp for validating an RFC 9562 / RFC 4122 UUID. @@ -6688,7 +6951,7 @@ const getUUIDRegExp = (version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8): globalThis.RegE * constraint to ensure generated strings match the UUID pattern. * * @see {@link isGUID} for shape-only GUID validation. - * @category String checks + * @category validation * @since 4.0.0 */ export function isUUID(version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, annotations?: Annotations.Filter) { @@ -6697,24 +6960,45 @@ export function isUUID(version?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8, annotations?: An regExp, { expected: version ? `a UUID v${version}` : "a UUID", - meta: { - _tag: "isUUID", - regExp, - version + representation: { + id: "effect/schema/isUUID", + payload: { version: version ?? null } }, + toJsonSchema: () => ({ pattern: regExp.source, format: "uuid" }), + toCode: () => ({ runtime: version === undefined ? "Schema.isUUID()" : `Schema.isUUID(${version})` }), ...annotations } ) } -const GUID_REGEXP = /^([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})$/ - /** - * Validates that a string has the GUID / UUID textual shape. + * Reviver for persisted `isUUID` checks. * * **When to use** * - * Use when you need to accept dashed hexadecimal identifiers without enforcing + * Use when reconstructing documents that may contain checks created by {@link isUUID}. + * + * @see {@link isUUID} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isUUIDReviver: SchemaRepresentation.FilterReviver<{ + readonly version: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null +}> = InternalSchema.makeFilterReviver( + "effect/schema/isUUID", + Struct({ version: Union([Literals([1, 2, 3, 4, 5, 6, 7, 8]), Null]) }), + ({ annotations, payload }) => isUUID(payload.version ?? undefined, annotations) +) + +const GUID_REGEXP = /^([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})$/ + +/** + * Validates that a string has the GUID / UUID textual shape. + * + * **When to use** + * + * Use when you need to accept dashed hexadecimal identifiers without enforcing * UUID version or variant bits. * * **Details** @@ -6730,7 +7014,7 @@ const GUID_REGEXP = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{ * constraint to ensure generated strings match the GUID pattern. * * @see {@link isUUID} for strict UUID validation. - * @category String checks + * @category validation * @since 4.0.0 */ export function isGUID(annotations?: Annotations.Filter) { @@ -6738,15 +7022,35 @@ export function isGUID(annotations?: Annotations.Filter) { GUID_REGEXP, { expected: "a GUID", - meta: { - _tag: "isGUID", - regExp: GUID_REGEXP + representation: { + id: "effect/schema/isGUID", + payload: null }, + toJsonSchema: () => ({ pattern: GUID_REGEXP.source }), + toCode: () => ({ runtime: "Schema.isGUID()" }), ...annotations } ) } +/** + * Reviver for persisted `isGUID` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGUID}. + * + * @see {@link isGUID} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGUIDReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isGUID", + Null, + ({ annotations }) => isGUID(annotations) +) + /** * Validates that a string is a valid ULID (Universally Unique Lexicographically * Sortable Identifier). @@ -6763,7 +7067,7 @@ export function isGUID(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the ULID pattern. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isULID(annotations?: Annotations.Filter) { @@ -6771,15 +7075,35 @@ export function isULID(annotations?: Annotations.Filter) { return isPattern( regExp, { - meta: { - _tag: "isULID", - regExp + representation: { + id: "effect/schema/isULID", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isULID()" }), ...annotations } ) } +/** + * Reviver for persisted `isULID` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isULID}. + * + * @see {@link isULID} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isULIDReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isULID", + Null, + ({ annotations }) => isULID(annotations) +) + /** * Validates that a string is valid Base64 encoded data. * @@ -6795,7 +7119,7 @@ export function isULID(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the Base64 pattern. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isBase64(annotations?: Annotations.Filter) { @@ -6804,15 +7128,35 @@ export function isBase64(annotations?: Annotations.Filter) { regExp, { expected: "a base64 encoded string", - meta: { - _tag: "isBase64", - regExp + representation: { + id: "effect/schema/isBase64", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isBase64()" }), ...annotations } ) } +/** + * Reviver for persisted `isBase64` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isBase64}. + * + * @see {@link isBase64} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isBase64Reviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isBase64", + Null, + ({ annotations }) => isBase64(annotations) +) + /** * Validates that a string is valid Base64URL encoded data (Base64 with URL-safe * characters). @@ -6829,7 +7173,7 @@ export function isBase64(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies a `patterns` * constraint to ensure generated strings match the Base64URL pattern. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isBase64Url(annotations?: Annotations.Filter) { @@ -6838,42 +7182,62 @@ export function isBase64Url(annotations?: Annotations.Filter) { regExp, { expected: "a base64url encoded string", - meta: { - _tag: "isBase64Url", - regExp + representation: { + id: "effect/schema/isBase64Url", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isBase64Url()" }), ...annotations } ) } +/** + * Reviver for persisted `isBase64Url` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isBase64Url}. + * + * @see {@link isBase64Url} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isBase64UrlReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isBase64Url", + Null, + ({ annotations }) => isBase64Url(annotations) +) + /** * Validates at runtime that a string starts with the specified literal prefix. * * **Details** * - * Notes: - * The JSON Schema and arbitrary metadata are built from `^${startsWith}` without - * escaping regexp metacharacters. If the prefix contains regexp syntax, generated - * patterns may not be equivalent to the runtime `startsWith` check. + * RegExp metacharacters in the prefix are escaped in JSON Schema and arbitrary + * metadata so that the generated patterns retain literal `startsWith` semantics. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isStartsWith(startsWith: string, annotations?: Annotations.Filter) { const formatted = JSON.stringify(startsWith) + const regExp = new globalThis.RegExp(`^${RegExp_.escape(startsWith)}`) return makeFilter( (s: string) => s.startsWith(startsWith), { expected: `a string starting with ${formatted}`, - meta: { - _tag: "isStartsWith", - startsWith, - regExp: new globalThis.RegExp(`^${startsWith}`) + representation: { + id: "effect/schema/isStartsWith", + payload: { startsWith } }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: `Schema.isStartsWith(${format(startsWith)})` }), arbitrary: { constraint: { - patterns: [`^${startsWith}`] + patterns: [regExp.source] } }, ...annotations @@ -6881,33 +7245,53 @@ export function isStartsWith(startsWith: string, annotations?: Annotations.Filte ) } +/** + * Reviver for persisted `isStartsWith` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isStartsWith}. + * + * @see {@link isStartsWith} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isStartsWithReviver: SchemaRepresentation.FilterReviver<{ + readonly startsWith: string +}> = InternalSchema.makeFilterReviver( + "effect/schema/isStartsWith", + Struct({ startsWith: String }), + ({ annotations, payload }) => isStartsWith(payload.startsWith, annotations) +) + /** * Validates at runtime that a string ends with the specified literal suffix. * * **Details** * - * Notes: - * The JSON Schema and arbitrary metadata are built from `${endsWith}$` without - * escaping regexp metacharacters. If the suffix contains regexp syntax, generated - * patterns may not be equivalent to the runtime `endsWith` check. + * RegExp metacharacters in the suffix are escaped in JSON Schema and arbitrary + * metadata so that the generated patterns retain literal `endsWith` semantics. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isEndsWith(endsWith: string, annotations?: Annotations.Filter) { const formatted = JSON.stringify(endsWith) + const regExp = new globalThis.RegExp(`${RegExp_.escape(endsWith)}$`) return makeFilter( (s: string) => s.endsWith(endsWith), { expected: `a string ending with ${formatted}`, - meta: { - _tag: "isEndsWith", - endsWith, - regExp: new globalThis.RegExp(`${endsWith}$`) + representation: { + id: "effect/schema/isEndsWith", + payload: { endsWith } }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: `Schema.isEndsWith(${format(endsWith)})` }), arbitrary: { constraint: { - patterns: [`${endsWith}$`] + patterns: [regExp.source] } }, ...annotations @@ -6915,33 +7299,54 @@ export function isEndsWith(endsWith: string, annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isEndsWith` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isEndsWith}. + * + * @see {@link isEndsWith} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isEndsWithReviver: SchemaRepresentation.FilterReviver<{ + readonly endsWith: string +}> = InternalSchema.makeFilterReviver( + "effect/schema/isEndsWith", + Struct({ endsWith: String }), + ({ annotations, payload }) => isEndsWith(payload.endsWith, annotations) +) + /** * Validates at runtime that a string contains the specified literal substring. * * **Details** * - * Notes: - * The JSON Schema and arbitrary metadata use the substring as a raw regexp - * pattern. If the substring contains regexp syntax, generated patterns may not be - * equivalent to the runtime `includes` check. + * RegExp metacharacters in the substring are escaped in JSON Schema and + * arbitrary metadata so that the generated patterns retain literal `includes` + * semantics. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isIncludes(includes: string, annotations?: Annotations.Filter) { const formatted = JSON.stringify(includes) + const regExp = new globalThis.RegExp(RegExp_.escape(includes)) return makeFilter( (s: string) => s.includes(includes), { expected: `a string including ${formatted}`, - meta: { - _tag: "isIncludes", - includes, - regExp: new globalThis.RegExp(includes) + representation: { + id: "effect/schema/isIncludes", + payload: { includes } }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: `Schema.isIncludes(${format(includes)})` }), arbitrary: { constraint: { - patterns: [includes] + patterns: [regExp.source] } }, ...annotations @@ -6949,6 +7354,26 @@ export function isIncludes(includes: string, annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isIncludes` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isIncludes}. + * + * @see {@link isIncludes} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isIncludesReviver: SchemaRepresentation.FilterReviver<{ + readonly includes: string +}> = InternalSchema.makeFilterReviver( + "effect/schema/isIncludes", + Struct({ includes: String }), + ({ annotations, payload }) => isIncludes(payload.includes, annotations) +) + const UPPERCASED_PATTERN = "^[^a-z]*$" /** @@ -6960,18 +7385,21 @@ const UPPERCASED_PATTERN = "^[^a-z]*$" * such as digits, punctuation, and whitespace. It rejects strings that would * change when uppercased. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isUppercased(annotations?: Annotations.Filter) { + const regExp = new globalThis.RegExp(UPPERCASED_PATTERN) return makeFilter( (s: string) => s.toUpperCase() === s, { expected: "a string with all characters in uppercase", - meta: { - _tag: "isUppercased", - regExp: new globalThis.RegExp(UPPERCASED_PATTERN) + representation: { + id: "effect/schema/isUppercased", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isUppercased()" }), arbitrary: { constraint: { patterns: [UPPERCASED_PATTERN] @@ -6982,6 +7410,24 @@ export function isUppercased(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isUppercased` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isUppercased}. + * + * @see {@link isUppercased} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isUppercasedReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isUppercased", + Null, + ({ annotations }) => isUppercased(annotations) +) + const LOWERCASED_PATTERN = "^[^A-Z]*$" /** @@ -6993,18 +7439,21 @@ const LOWERCASED_PATTERN = "^[^A-Z]*$" * such as digits, punctuation, and whitespace. It rejects strings that would * change when lowercased. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isLowercased(annotations?: Annotations.Filter) { + const regExp = new globalThis.RegExp(LOWERCASED_PATTERN) return makeFilter( (s: string) => s.toLowerCase() === s, { expected: "a string with all characters in lowercase", - meta: { - _tag: "isLowercased", - regExp: new globalThis.RegExp(LOWERCASED_PATTERN) + representation: { + id: "effect/schema/isLowercased", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isLowercased()" }), arbitrary: { constraint: { patterns: [LOWERCASED_PATTERN] @@ -7015,6 +7464,24 @@ export function isLowercased(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isLowercased` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLowercased}. + * + * @see {@link isLowercased} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLowercasedReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isLowercased", + Null, + ({ annotations }) => isLowercased(annotations) +) + const CAPITALIZED_PATTERN = "^[^a-z]?.*$" /** @@ -7026,18 +7493,21 @@ const CAPITALIZED_PATTERN = "^[^a-z]?.*$" * Empty strings pass. Strings whose first character has no lowercase form, such * as a digit, punctuation mark, or whitespace, also pass. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isCapitalized(annotations?: Annotations.Filter) { + const regExp = new globalThis.RegExp(CAPITALIZED_PATTERN) return makeFilter( (s: string) => s.charAt(0).toUpperCase() === s.charAt(0), { expected: "a string with the first character in uppercase", - meta: { - _tag: "isCapitalized", - regExp: new globalThis.RegExp(CAPITALIZED_PATTERN) + representation: { + id: "effect/schema/isCapitalized", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isCapitalized()" }), arbitrary: { constraint: { patterns: [CAPITALIZED_PATTERN] @@ -7048,6 +7518,24 @@ export function isCapitalized(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isCapitalized` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isCapitalized}. + * + * @see {@link isCapitalized} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isCapitalizedReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isCapitalized", + Null, + ({ annotations }) => isCapitalized(annotations) +) + const UNCAPITALIZED_PATTERN = "^[^A-Z]?.*$" /** @@ -7059,18 +7547,21 @@ const UNCAPITALIZED_PATTERN = "^[^A-Z]?.*$" * Empty strings pass. Strings whose first character has no uppercase form, such * as a digit, punctuation mark, or whitespace, also pass. * - * @category String checks + * @category validation * @since 4.0.0 */ export function isUncapitalized(annotations?: Annotations.Filter) { + const regExp = new globalThis.RegExp(UNCAPITALIZED_PATTERN) return makeFilter( (s: string) => s.charAt(0).toLowerCase() === s.charAt(0), { expected: "a string with the first character in lowercase", - meta: { - _tag: "isUncapitalized", - regExp: new globalThis.RegExp(UNCAPITALIZED_PATTERN) + representation: { + id: "effect/schema/isUncapitalized", + payload: null }, + toJsonSchema: () => ({ pattern: regExp.source }), + toCode: () => ({ runtime: "Schema.isUncapitalized()" }), arbitrary: { constraint: { patterns: [UNCAPITALIZED_PATTERN] @@ -7081,6 +7572,42 @@ export function isUncapitalized(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isUncapitalized` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isUncapitalized}. + * + * @see {@link isUncapitalized} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isUncapitalizedReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isUncapitalized", + Null, + ({ annotations }) => isUncapitalized(annotations) +) + +/** + * Type-level representation of {@link Finite}. + * + * @category models + * @since 3.10.0 + */ +export interface Finite extends Number { + readonly "Rebuild": Finite +} + +/** + * Schema for finite numbers, rejecting `NaN`, `Infinity`, and `-Infinity`. + * + * @category schemas + * @since 3.10.0 + */ +export const Finite: Finite = make(SchemaAST.finite) + /** * Validates that a number is finite (not `Infinity`, `-Infinity`, or `NaN`). * @@ -7096,33 +7623,34 @@ export function isUncapitalized(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies `noNaN: true` and * `noInfinity: true` constraints to ensure generated numbers are finite. * - * @category Number checks + * @category validation * @since 4.0.0 */ -export function isFinite(annotations?: Annotations.Filter) { - return makeFilter( - (n: number) => globalThis.Number.isFinite(n), - { - expected: "a finite number", - meta: { - _tag: "isFinite" - }, - arbitrary: { - constraint: { - noInfinity: true, - noNaN: true - } - }, - ...annotations - } - ) -} +export const isFinite: (annotations?: Annotations.Filter) => SchemaAST.Filter = SchemaAST.isFinite + +/** + * Reviver for persisted `isFinite` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isFinite}. + * + * @see {@link isFinite} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isFiniteReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isFinite", + Null, + ({ annotations }) => isFinite(annotations) +) /** * Creates a greater-than (`>`) check for any ordered type from an * `Order.Order` instance. * - * @category Order checks + * @category validation * @since 4.0.0 */ export function makeIsGreaterThan(options: { @@ -7157,7 +7685,7 @@ export function makeIsGreaterThan(options: { * Creates a greater-than-or-equal-to (`>=`) check for any ordered type from an * `Order.Order` instance. * - * @category Order checks + * @category validation * @since 4.0.0 */ export function makeIsGreaterThanOrEqualTo(options: { @@ -7191,7 +7719,7 @@ export function makeIsGreaterThanOrEqualTo(options: { * Creates a less-than (`<`) check for any ordered type from an `Order.Order` * instance. * - * @category Order checks + * @category validation * @since 4.0.0 */ export function makeIsLessThan(options: { @@ -7226,7 +7754,7 @@ export function makeIsLessThan(options: { * Creates a less-than-or-equal-to (`<=`) check for any ordered type from an * `Order.Order` instance. * - * @category Order checks + * @category validation * @since 4.0.0 */ export function makeIsLessThanOrEqualTo(options: { @@ -7260,7 +7788,7 @@ export function makeIsLessThanOrEqualTo(options: { * Creates an inclusive or exclusive range check for any ordered type from an * `Order.Order` instance. * - * @category Order checks + * @category validation * @since 4.0.0 */ export function makeIsBetween(deriveOptions: { @@ -7316,7 +7844,7 @@ export function makeIsBetween(deriveOptions: { * Creates a divisibility check for any numeric type from a remainder function * and a zero value. * - * @category Numeric checks + * @category validation * @since 4.0.0 */ export function makeIsMultipleOf(options: { @@ -7338,6 +7866,13 @@ export function makeIsMultipleOf(options: { } } +function encodeNumberPayload(number: number): number { + if (!globalThis.Number.isFinite(number)) { + throw new globalThis.RangeError(`Expected a finite number, got ${format(number)}`) + } + return number +} + /** * Validates that a number is greater than the specified value (exclusive). * @@ -7353,19 +7888,41 @@ export function makeIsMultipleOf(options: { * `exclusiveMinimum` constraint to ensure generated numbers are greater than * the specified value. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isGreaterThan = makeIsGreaterThan({ order: Order.Number, annotate: (exclusiveMinimum) => ({ - meta: { - _tag: "isGreaterThan", - exclusiveMinimum - } + representation: { + id: "effect/schema/isGreaterThan", + payload: { exclusiveMinimum: encodeNumberPayload(exclusiveMinimum) } + }, + toJsonSchema: () => ({ exclusiveMinimum }), + toCode: () => ({ runtime: `Schema.isGreaterThan(${format(exclusiveMinimum)})` }) }) }) +/** + * Reviver for persisted `isGreaterThan` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThan}. + * + * @see {@link isGreaterThan} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMinimum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThan", + Struct({ exclusiveMinimum: Finite }), + ({ annotations, payload }) => isGreaterThan(payload.exclusiveMinimum, annotations) +) + /** * Validates that a number is greater than or equal to the specified value * (inclusive). @@ -7381,19 +7938,41 @@ export const isGreaterThan = makeIsGreaterThan({ * When generating test data with fast-check, this applies a `minimum` constraint * to ensure generated numbers are greater than or equal to the specified value. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isGreaterThanOrEqualTo = makeIsGreaterThanOrEqualTo({ order: Order.Number, annotate: (minimum) => ({ - meta: { - _tag: "isGreaterThanOrEqualTo", - minimum - } + representation: { + id: "effect/schema/isGreaterThanOrEqualTo", + payload: { minimum: encodeNumberPayload(minimum) } + }, + toJsonSchema: () => ({ minimum }), + toCode: () => ({ runtime: `Schema.isGreaterThanOrEqualTo(${format(minimum)})` }) }) }) +/** + * Reviver for persisted `isGreaterThanOrEqualTo` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThanOrEqualTo}. + * + * @see {@link isGreaterThanOrEqualTo} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThanOrEqualTo", + Struct({ minimum: Finite }), + ({ annotations, payload }) => isGreaterThanOrEqualTo(payload.minimum, annotations) +) + /** * Validates that a number is less than the specified value (exclusive). * @@ -7409,19 +7988,41 @@ export const isGreaterThanOrEqualTo = makeIsGreaterThanOrEqualTo({ * `exclusiveMaximum` constraint to ensure generated numbers are less than the * specified value. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isLessThan = makeIsLessThan({ order: Order.Number, annotate: (exclusiveMaximum) => ({ - meta: { - _tag: "isLessThan", - exclusiveMaximum - } + representation: { + id: "effect/schema/isLessThan", + payload: { exclusiveMaximum: encodeNumberPayload(exclusiveMaximum) } + }, + toJsonSchema: () => ({ exclusiveMaximum }), + toCode: () => ({ runtime: `Schema.isLessThan(${format(exclusiveMaximum)})` }) }) }) +/** + * Reviver for persisted `isLessThan` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThan}. + * + * @see {@link isLessThan} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMaximum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThan", + Struct({ exclusiveMaximum: Finite }), + ({ annotations, payload }) => isLessThan(payload.exclusiveMaximum, annotations) +) + /** * Validates that a number is less than or equal to the specified value * (inclusive). @@ -7437,19 +8038,41 @@ export const isLessThan = makeIsLessThan({ * When generating test data with fast-check, this applies a `maximum` constraint * to ensure generated numbers are less than or equal to the specified value. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isLessThanOrEqualTo = makeIsLessThanOrEqualTo({ order: Order.Number, annotate: (maximum) => ({ - meta: { - _tag: "isLessThanOrEqualTo", - maximum - } + representation: { + id: "effect/schema/isLessThanOrEqualTo", + payload: { maximum: encodeNumberPayload(maximum) } + }, + toJsonSchema: () => ({ maximum }), + toCode: () => ({ runtime: `Schema.isLessThanOrEqualTo(${format(maximum)})` }) }) }) +/** + * Reviver for persisted `isLessThanOrEqualTo` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThanOrEqualTo}. + * + * @see {@link isLessThanOrEqualTo} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanOrEqualToReviver: SchemaRepresentation.FilterReviver<{ + readonly maximum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThanOrEqualTo", + Struct({ maximum: Finite }), + ({ annotations, payload }) => isLessThanOrEqualTo(payload.maximum, annotations) +) + /** * Validates that a number is within a specified range. The range boundaries can * be inclusive or exclusive based on the provided options. @@ -7468,21 +8091,66 @@ export const isLessThanOrEqualTo = makeIsLessThanOrEqualTo({ * `exclusiveMaximum` flags to ensure generated numbers fall within the * specified range. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isBetween = makeIsBetween({ order: Order.Number, annotate: (options) => { + const exclusiveMinimum = options.exclusiveMinimum ? true : undefined + const exclusiveMaximum = options.exclusiveMaximum ? true : undefined + const payload = { + minimum: encodeNumberPayload(options.minimum), + maximum: encodeNumberPayload(options.maximum), + ...(exclusiveMinimum && { exclusiveMinimum }), + ...(exclusiveMaximum && { exclusiveMaximum }) + } return { - meta: { - _tag: "isBetween", - ...options - } + representation: { + id: "effect/schema/isBetween", + payload + }, + toJsonSchema: () => ({ + [exclusiveMinimum ? "exclusiveMinimum" : "minimum"]: options.minimum, + [exclusiveMaximum ? "exclusiveMaximum" : "maximum"]: options.maximum + }), + toCode: () => ({ + runtime: `Schema.isBetween({ minimum: ${format(options.minimum)}, maximum: ${ + format(options.maximum) + }, exclusiveMinimum: ${format(exclusiveMinimum)}, exclusiveMaximum: ${format(exclusiveMaximum)} })` + }) } } }) +/** + * Reviver for persisted `isBetween` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isBetween}. + * + * @see {@link isBetween} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isBetweenReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined +}> = InternalSchema.makeFilterReviver( + "effect/schema/isBetween", + Struct({ + minimum: Finite, + maximum: Finite, + exclusiveMinimum: optional(Literal(true)), + exclusiveMaximum: optional(Literal(true)) + }), + ({ annotations, payload }) => isBetween(payload, annotations) +) + /** * Validates that a number is a multiple of the specified divisor. * @@ -7497,7 +8165,7 @@ export const isBetween = makeIsBetween({ * When generating test data with fast-check, this applies constraints to ensure * generated numbers are multiples of the specified divisor. * - * @category Number checks + * @category validation * @since 4.0.0 */ export const isMultipleOf = makeIsMultipleOf({ @@ -7505,13 +8173,35 @@ export const isMultipleOf = makeIsMultipleOf({ zero: 0, annotate: (divisor) => ({ expected: `a value that is a multiple of ${divisor}`, - meta: { - _tag: "isMultipleOf", - divisor - } + representation: { + id: "effect/schema/isMultipleOf", + payload: { divisor } + }, + toJsonSchema: () => ({ multipleOf: divisor }), + toCode: () => ({ runtime: `Schema.isMultipleOf(${format(divisor)})` }) }) }) +/** + * Reviver for persisted `isMultipleOf` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMultipleOf}. + * + * @see {@link isMultipleOf} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMultipleOfReviver: SchemaRepresentation.FilterReviver<{ + readonly divisor: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMultipleOf", + Struct({ divisor: Finite }), + ({ annotations, payload }) => isMultipleOf(payload.divisor, annotations) +) + /** * Validates that a number is a safe integer (within the safe integer range * that can be exactly represented in JavaScript). @@ -7520,33 +8210,96 @@ export const isMultipleOf = makeIsMultipleOf({ * * JSON Schema: * - * This check corresponds to the `type: "integer"` constraint in JSON Schema. + * This check corresponds to the `type: "integer"` constraint in JSON Schema. + * + * Arbitrary: + * + * When generating test data with fast-check, this applies an `integer: true` + * constraint to ensure generated numbers are integers. + * + * @category validation + * @since 4.0.0 + */ +export function isInt(annotations?: Annotations.Filter) { + return makeFilter( + (n: number) => globalThis.Number.isSafeInteger(n), + { + expected: "an integer", + representation: { + id: "effect/schema/isInt", + payload: null + }, + toJsonSchema: () => ({ type: "integer" }), + toCode: () => ({ runtime: "Schema.isInt()" }), + arbitrary: { + constraint: { + integer: true + } + }, + ...annotations + } + ) +} + +/** + * Reviver for persisted `isInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isInt}. + * + * @see {@link isInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isIntReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isInt", + Null, + ({ annotations }) => isInt(annotations) +) + +/** + * Type-level representation of {@link Int}. + * + * @category models + * @since 3.10.0 + */ +export interface Int extends Number { + readonly "Rebuild": Int +} + +/** + * Schema for integers, rejecting `NaN`, `Infinity`, and `-Infinity`. + * + * @category schemas + * @since 3.10.0 + */ +export const Int: Int = Number.check(isInt()) + +/** + * Type-level representation of {@link Natural}. + * + * @category models + * @since 4.0.0 + */ +export interface Natural extends Int { + readonly "Rebuild": Natural +} + +/** + * Schema for non-negative safe integers, including zero. + * + * **When to use** * - * Arbitrary: + * Use when you need a count, index, or size that cannot be negative. * - * When generating test data with fast-check, this applies an `integer: true` - * constraint to ensure generated numbers are integers. + * @see {@link Int} for safe integers that may be negative * - * @category Integer checks + * @category schemas * @since 4.0.0 */ -export function isInt(annotations?: Annotations.Filter) { - return makeFilter( - (n: number) => globalThis.Number.isSafeInteger(n), - { - expected: "an integer", - meta: { - _tag: "isInt" - }, - arbitrary: { - constraint: { - integer: true - } - }, - ...annotations - } - ) -} +export const Natural: Natural = Int.check(isGreaterThanOrEqualTo(0)) /** * Validates that a number is a 32-bit signed integer (range: -2,147,483,648 to @@ -7564,7 +8317,7 @@ export function isInt(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies integer and range * constraints to ensure generated numbers are 32-bit signed integers. * - * @category Integer checks + * @category validation * @since 4.0.0 */ export function isInt32(annotations?: Annotations.Filter) { @@ -7596,7 +8349,7 @@ export function isInt32(annotations?: Annotations.Filter) { * When generating test data with fast-check, this applies integer and range * constraints to ensure generated numbers are 32-bit unsigned integers. * - * @category Integer checks + * @category validation * @since 4.0.0 */ export function isUint32(annotations?: Annotations.Filter) { @@ -7612,41 +8365,15 @@ export function isUint32(annotations?: Annotations.Filter) { ) } -/** - * Validates that a Date object represents a valid date (not an invalid date - * like `new Date("invalid")`). - * - * **Details** - * - * JSON Schema: - * - * This check does not have a direct JSON Schema equivalent, as JSON Schema - * validates date strings, not Date objects. - * - * Arbitrary: - * - * When generating test data with fast-check, this applies a `valid: true` - * constraint to ensure generated Date objects are valid. - * - * @category Date checks - * @since 4.0.0 - */ -export function isDateValid(annotations?: Annotations.Filter) { - return makeFilter( - (date) => !isNaN(date.getTime()), - { - expected: "a valid date", - meta: { - _tag: "isDateValid" - }, - arbitrary: { - constraint: { - valid: true - } - }, - ...annotations - } - ) +function encodeDatePayload(date: globalThis.Date): string { + if (globalThis.Number.isNaN(date.getTime())) { + throw new globalThis.RangeError(`Expected a valid Date, got ${format(date)}`) + } + return date.toISOString() +} + +function formatDateRuntime(date: globalThis.Date): string { + return `new Date(${format(date.getTime())})` } /** @@ -7660,17 +8387,22 @@ export function isDateValid(annotations?: Annotations.Filter) { * one millisecond after the specified value to ensure generated Date objects are * greater than it. * - * @category Date checks + * @category validation * @since 4.0.0 */ export const isGreaterThanDate = makeIsGreaterThan({ order: Order.Date, - annotate: (exclusiveMinimum) => ({ - meta: { - _tag: "isGreaterThanDate", - exclusiveMinimum + annotate: (exclusiveMinimum) => { + const encoded = encodeDatePayload(exclusiveMinimum) + return { + representation: { + id: "effect/schema/isGreaterThanDate", + payload: { exclusiveMinimum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isGreaterThanDate(${formatDateRuntime(exclusiveMinimum)})` }) } - }) + } }) /** @@ -7690,17 +8422,22 @@ export const isGreaterThanDate = makeIsGreaterThan({ * to ensure generated Date objects are greater than or equal to the specified * date. * - * @category Date checks + * @category validation * @since 4.0.0 */ export const isGreaterThanOrEqualToDate = makeIsGreaterThanOrEqualTo({ order: Order.Date, - annotate: (minimum) => ({ - meta: { - _tag: "isGreaterThanOrEqualToDate", - minimum + annotate: (minimum) => { + const encoded = encodeDatePayload(minimum) + return { + representation: { + id: "effect/schema/isGreaterThanOrEqualToDate", + payload: { minimum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isGreaterThanOrEqualToDate(${formatDateRuntime(minimum)})` }) } - }) + } }) /** @@ -7714,17 +8451,22 @@ export const isGreaterThanOrEqualToDate = makeIsGreaterThanOrEqualTo({ * one millisecond before the specified value to ensure generated Date objects * are less than it. * - * @category Date checks + * @category validation * @since 4.0.0 */ export const isLessThanDate = makeIsLessThan({ order: Order.Date, - annotate: (exclusiveMaximum) => ({ - meta: { - _tag: "isLessThanDate", - exclusiveMaximum + annotate: (exclusiveMaximum) => { + const encoded = encodeDatePayload(exclusiveMaximum) + return { + representation: { + id: "effect/schema/isLessThanDate", + payload: { exclusiveMaximum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isLessThanDate(${formatDateRuntime(exclusiveMaximum)})` }) } - }) + } }) /** @@ -7744,17 +8486,22 @@ export const isLessThanDate = makeIsLessThan({ * to ensure generated Date objects are less than or equal to the specified * date. * - * @category Date checks + * @category validation * @since 4.0.0 */ export const isLessThanOrEqualToDate = makeIsLessThanOrEqualTo({ order: Order.Date, - annotate: (maximum) => ({ - meta: { - _tag: "isLessThanOrEqualToDate", - maximum + annotate: (maximum) => { + const encoded = encodeDatePayload(maximum) + return { + representation: { + id: "effect/schema/isLessThanOrEqualToDate", + payload: { maximum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isLessThanOrEqualToDate(${formatDateRuntime(maximum)})` }) } - }) + } }) /** @@ -7774,17 +8521,33 @@ export const isLessThanOrEqualToDate = makeIsLessThanOrEqualTo({ * constraints to ensure generated Date objects fall within the specified range, * shifting exclusive bounds by one millisecond. * - * @category Date checks + * @category validation * @since 4.0.0 */ export const isBetweenDate = makeIsBetween({ order: Order.Date, - annotate: (options) => ({ - meta: { - _tag: "isBetweenDate", - ...options + annotate: (options) => { + const exclusiveMinimum = options.exclusiveMinimum ? true : undefined + const exclusiveMaximum = options.exclusiveMaximum ? true : undefined + const payload = { + minimum: encodeDatePayload(options.minimum), + maximum: encodeDatePayload(options.maximum), + ...(exclusiveMinimum && { exclusiveMinimum }), + ...(exclusiveMaximum && { exclusiveMaximum }) } - }) + return { + representation: { + id: "effect/schema/isBetweenDate", + payload + }, + toJsonSchema: () => ({}), + toCode: () => ({ + runtime: `Schema.isBetweenDate({ minimum: ${formatDateRuntime(options.minimum)}, maximum: ${ + formatDateRuntime(options.maximum) + }, exclusiveMinimum: ${format(exclusiveMinimum)}, exclusiveMaximum: ${format(exclusiveMaximum)} })` + }) + } + } }) /** @@ -7798,17 +8561,22 @@ export const isBetweenDate = makeIsBetween({ * `exclusiveMinimum + 1n` to ensure generated BigInts are greater than the * specified value. * - * @category BigInt checks + * @category validation * @since 4.0.0 */ export const isGreaterThanBigInt = makeIsGreaterThan({ order: Order.BigInt, - annotate: (exclusiveMinimum) => ({ - meta: { - _tag: "isGreaterThanBigInt", - exclusiveMinimum + annotate: (exclusiveMinimum) => { + const encoded = exclusiveMinimum.toString(10) + return { + representation: { + id: "effect/schema/isGreaterThanBigInt", + payload: { exclusiveMinimum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isGreaterThanBigInt(${format(exclusiveMinimum)})` }) } - }) + } }) /** @@ -7823,17 +8591,22 @@ export const isGreaterThanBigInt = makeIsGreaterThan({ * to ensure generated BigInt values are greater than or equal to the specified * value. * - * @category BigInt checks + * @category validation * @since 4.0.0 */ export const isGreaterThanOrEqualToBigInt = makeIsGreaterThanOrEqualTo({ order: Order.BigInt, - annotate: (minimum) => ({ - meta: { - _tag: "isGreaterThanOrEqualToBigInt", - minimum + annotate: (minimum) => { + const encoded = minimum.toString(10) + return { + representation: { + id: "effect/schema/isGreaterThanOrEqualToBigInt", + payload: { minimum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isGreaterThanOrEqualToBigInt(${format(minimum)})` }) } - }) + } }) /** @@ -7847,17 +8620,22 @@ export const isGreaterThanOrEqualToBigInt = makeIsGreaterThanOrEqualTo({ * `exclusiveMaximum - 1n` to ensure generated BigInts are less than the * specified value. * - * @category BigInt checks + * @category validation * @since 4.0.0 */ export const isLessThanBigInt = makeIsLessThan({ order: Order.BigInt, - annotate: (exclusiveMaximum) => ({ - meta: { - _tag: "isLessThanBigInt", - exclusiveMaximum + annotate: (exclusiveMaximum) => { + const encoded = exclusiveMaximum.toString(10) + return { + representation: { + id: "effect/schema/isLessThanBigInt", + payload: { exclusiveMaximum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isLessThanBigInt(${format(exclusiveMaximum)})` }) } - }) + } }) /** @@ -7872,17 +8650,22 @@ export const isLessThanBigInt = makeIsLessThan({ * to ensure generated BigInt values are less than or equal to the specified * value. * - * @category BigInt checks + * @category validation * @since 4.0.0 */ export const isLessThanOrEqualToBigInt = makeIsLessThanOrEqualTo({ order: Order.BigInt, - annotate: (maximum) => ({ - meta: { - _tag: "isLessThanOrEqualToBigInt", - maximum + annotate: (maximum) => { + const encoded = maximum.toString(10) + return { + representation: { + id: "effect/schema/isLessThanOrEqualToBigInt", + payload: { maximum: encoded } + }, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isLessThanOrEqualToBigInt(${format(maximum)})` }) } - }) + } }) /** @@ -7897,23 +8680,39 @@ export const isLessThanOrEqualToBigInt = makeIsLessThanOrEqualTo({ * constraints to ensure generated BigInt values fall within the specified * range. * - * @category BigInt checks + * @category validation * @since 4.0.0 */ export const isBetweenBigInt = makeIsBetween({ order: Order.BigInt, - annotate: (options) => ({ - meta: { - _tag: "isBetweenBigInt", - ...options + annotate: (options) => { + const exclusiveMinimum = options.exclusiveMinimum ? true : undefined + const exclusiveMaximum = options.exclusiveMaximum ? true : undefined + const payload = { + minimum: options.minimum.toString(10), + maximum: options.maximum.toString(10), + ...(exclusiveMinimum && { exclusiveMinimum }), + ...(exclusiveMaximum && { exclusiveMaximum }) } - }) + return { + representation: { + id: "effect/schema/isBetweenBigInt", + payload + }, + toJsonSchema: () => ({}), + toCode: () => ({ + runtime: `Schema.isBetweenBigInt({ minimum: ${format(options.minimum)}, maximum: ${ + format(options.maximum) + }, exclusiveMinimum: ${format(exclusiveMinimum)}, exclusiveMaximum: ${format(exclusiveMaximum)} })` + }) + } + } }) /** * Validates that a BigDecimal is greater than the specified value (exclusive). * - * @category BigDecimal checks + * @category validation * @since 4.0.0 */ export const isGreaterThanBigDecimal = makeIsGreaterThan({ @@ -7925,7 +8724,7 @@ export const isGreaterThanBigDecimal = makeIsGreaterThan({ * Validates that a BigDecimal is greater than or equal to the specified value * (inclusive). * - * @category BigDecimal checks + * @category validation * @since 4.0.0 */ export const isGreaterThanOrEqualToBigDecimal = makeIsGreaterThanOrEqualTo({ @@ -7936,7 +8735,7 @@ export const isGreaterThanOrEqualToBigDecimal = makeIsGreaterThanOrEqualTo({ /** * Validates that a BigDecimal is less than the specified value (exclusive). * - * @category BigDecimal checks + * @category validation * @since 4.0.0 */ export const isLessThanBigDecimal = makeIsLessThan({ @@ -7948,7 +8747,7 @@ export const isLessThanBigDecimal = makeIsLessThan({ * Validates that a BigDecimal is less than or equal to the specified value * (inclusive). * - * @category BigDecimal checks + * @category validation * @since 4.0.0 */ export const isLessThanOrEqualToBigDecimal = makeIsLessThanOrEqualTo({ @@ -7964,7 +8763,7 @@ export const isLessThanOrEqualToBigDecimal = makeIsLessThanOrEqualTo({ * The minimum and maximum boundaries are inclusive by default. Pass * `exclusiveMinimum` or `exclusiveMaximum` to exclude either boundary. * - * @category BigDecimal checks + * @category validation * @since 4.0.0 */ export const isBetweenBigDecimal = makeIsBetween({ @@ -7991,14 +8790,16 @@ export const isBetweenBigDecimal = makeIsBetween({ * * **Example** (Checking minimum length) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const NonEmptyStringSchema = Schema.String.check(Schema.isMinLength(1)) * const NonEmptyArraySchema = Schema.Array(Schema.Number).check(Schema.isMinLength(1)) + * Schema.is(NonEmptyStringSchema)("a") // => true + * Schema.is(NonEmptyArraySchema)([1]) // => true * ``` * - * @category Length checks + * @category validation * @since 4.0.0 */ export function isMinLength(minLength: number, annotations?: Annotations.Filter) { @@ -8007,11 +8808,13 @@ export function isMinLength(minLength: number, annotations?: Annotations.Filter) (input) => input.length >= minLength, { expected: `a value with a length of at least ${minLength}`, - meta: { - _tag: "isMinLength", - minLength + representation: { + id: "effect/schema/isMinLength", + payload: { minLength } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: ({ type }) => type === "array" ? { minItems: minLength } : { minLength }, + toCode: () => ({ runtime: `Schema.isMinLength(${minLength})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength @@ -8022,6 +8825,26 @@ export function isMinLength(minLength: number, annotations?: Annotations.Filter) ) } +/** + * Reviver for persisted `isMinLength` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMinLength}. + * + * @see {@link isMinLength} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMinLengthReviver: SchemaRepresentation.FilterReviver<{ + readonly minLength: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMinLength", + Struct({ minLength: Natural }), + ({ annotations, payload }) => isMinLength(payload.minLength, annotations) +) + /** * Validates that a value has at least one element. Works with strings and arrays. * This is equivalent to `isMinLength(1)`. @@ -8038,7 +8861,7 @@ export function isMinLength(minLength: number, annotations?: Annotations.Filter) * When generating test data with fast-check, this applies a `minLength: 1` * constraint to ensure generated strings or arrays are non-empty. * - * @category Length checks + * @category validation * @since 4.0.0 */ export function isNonEmpty(annotations?: Annotations.Filter) { @@ -8062,7 +8885,7 @@ export function isNonEmpty(annotations?: Annotations.Filter) { * constraint to ensure generated strings or arrays have at most the required * length. * - * @category Length checks + * @category validation * @since 4.0.0 */ export function isMaxLength(maxLength: number, annotations?: Annotations.Filter) { @@ -8071,11 +8894,13 @@ export function isMaxLength(maxLength: number, annotations?: Annotations.Filter) (input) => input.length <= maxLength, { expected: `a value with a length of at most ${maxLength}`, - meta: { - _tag: "isMaxLength", - maxLength + representation: { + id: "effect/schema/isMaxLength", + payload: { maxLength } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: ({ type }) => type === "array" ? { maxItems: maxLength } : { maxLength }, + toCode: () => ({ runtime: `Schema.isMaxLength(${maxLength})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { maxLength @@ -8086,6 +8911,26 @@ export function isMaxLength(maxLength: number, annotations?: Annotations.Filter) ) } +/** + * Reviver for persisted `isMaxLength` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMaxLength}. + * + * @see {@link isMaxLength} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMaxLengthReviver: SchemaRepresentation.FilterReviver<{ + readonly maxLength: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMaxLength", + Struct({ maxLength: Natural }), + ({ annotations, payload }) => isMaxLength(payload.maxLength, annotations) +) + /** * Validates that a value's length is within the specified range. Works with * strings and arrays. @@ -8103,7 +8948,7 @@ export function isMaxLength(maxLength: number, annotations?: Annotations.Filter) * `maxLength` constraints to ensure generated strings or arrays have a length * within the specified range. * - * @category Length checks + * @category validation * @since 4.0.0 */ export function isLengthBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { @@ -8115,12 +8960,17 @@ export function isLengthBetween(minimum: number, maximum: number, annotations?: expected: minimum === maximum ? `a value with a length of ${minimum}` : `a value with a length between ${minimum} and ${maximum}`, - meta: { - _tag: "isLengthBetween", - minimum, - maximum + + representation: { + id: "effect/schema/isLengthBetween", + payload: { minimum, maximum } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: ({ type }) => + type === "array" + ? { allOf: [{ minItems: minimum }, { maxItems: maximum }] } + : { allOf: [{ minLength: minimum }, { maxLength: maximum }] }, + toCode: () => ({ runtime: `Schema.isLengthBetween(${minimum}, ${maximum})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength: minimum, @@ -8132,6 +8982,27 @@ export function isLengthBetween(minimum: number, maximum: number, annotations?: ) } +/** + * Reviver for persisted `isLengthBetween` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLengthBetween}. + * + * @see {@link isLengthBetween} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLengthBetweenReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLengthBetween", + Struct({ minimum: Natural, maximum: Natural }), + ({ annotations, payload }) => isLengthBetween(payload.minimum, payload.maximum, annotations) +) + /** * Validates that a value has at least the specified size. Works with values * that have a `size` property, such as `Set` or `Map`. @@ -8149,7 +9020,7 @@ export function isLengthBetween(minimum: number, maximum: number, annotations?: * `minLength` constraint. Generators for values with a final `.size`, such as * sets and maps, interpret it as final cardinality. * - * @category Size checks + * @category validation * @since 4.0.0 */ export function isMinSize(minSize: number, annotations?: Annotations.Filter) { @@ -8158,11 +9029,13 @@ export function isMinSize(minSize: number, annotations?: Annotations.Filter) { (input) => input.size >= minSize, { expected: `a value with a size of at least ${minSize}`, - meta: { - _tag: "isMinSize", - minSize + representation: { + id: "effect/schema/isMinSize", + payload: { minSize } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isMinSize(${minSize})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength: minSize @@ -8173,6 +9046,26 @@ export function isMinSize(minSize: number, annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isMinSize` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMinSize}. + * + * @see {@link isMinSize} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMinSizeReviver: SchemaRepresentation.FilterReviver<{ + readonly minSize: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMinSize", + Struct({ minSize: Natural }), + ({ annotations, payload }) => isMinSize(payload.minSize, annotations) +) + /** * Validates that a value has at most the specified size. Works with values * that have a `size` property, such as `Set` or `Map`. @@ -8190,7 +9083,7 @@ export function isMinSize(minSize: number, annotations?: Annotations.Filter) { * `maxLength` constraint. Generators for values with a final `.size`, such as * sets and maps, interpret it as final cardinality. * - * @category Size checks + * @category validation * @since 4.0.0 */ export function isMaxSize(maxSize: number, annotations?: Annotations.Filter) { @@ -8199,11 +9092,13 @@ export function isMaxSize(maxSize: number, annotations?: Annotations.Filter) { (input) => input.size <= maxSize, { expected: `a value with a size of at most ${maxSize}`, - meta: { - _tag: "isMaxSize", - maxSize + representation: { + id: "effect/schema/isMaxSize", + payload: { maxSize } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isMaxSize(${maxSize})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { maxLength: maxSize @@ -8214,6 +9109,26 @@ export function isMaxSize(maxSize: number, annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isMaxSize` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMaxSize}. + * + * @see {@link isMaxSize} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMaxSizeReviver: SchemaRepresentation.FilterReviver<{ + readonly maxSize: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMaxSize", + Struct({ maxSize: Natural }), + ({ annotations, payload }) => isMaxSize(payload.maxSize, annotations) +) + /** * Validates that a value's size is within the specified range. Works with * values that have a `size` property, such as `Set` or `Map`. @@ -8231,7 +9146,7 @@ export function isMaxSize(maxSize: number, annotations?: Annotations.Filter) { * `minLength` and `maxLength` constraints. Generators for values with a final * `.size`, such as sets and maps, interpret them as final cardinality. * - * @category Size checks + * @category validation * @since 4.0.0 */ export function isSizeBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { @@ -8243,12 +9158,14 @@ export function isSizeBetween(minimum: number, maximum: number, annotations?: An expected: minimum === maximum ? `a value with a size of ${minimum}` : `a value with a size between ${minimum} and ${maximum}`, - meta: { - _tag: "isSizeBetween", - minimum, - maximum + + representation: { + id: "effect/schema/isSizeBetween", + payload: { minimum, maximum } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({}), + toCode: () => ({ runtime: `Schema.isSizeBetween(${minimum}, ${maximum})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength: minimum, @@ -8260,6 +9177,27 @@ export function isSizeBetween(minimum: number, maximum: number, annotations?: An ) } +/** + * Reviver for persisted `isSizeBetween` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isSizeBetween}. + * + * @see {@link isSizeBetween} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isSizeBetweenReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isSizeBetween", + Struct({ minimum: Natural, maximum: Natural }), + ({ annotations, payload }) => isSizeBetween(payload.minimum, payload.maximum, annotations) +) + /** * Validates that an object contains at least the specified number of * properties. This includes both string and symbol keys when counting @@ -8277,7 +9215,7 @@ export function isSizeBetween(minimum: number, maximum: number, annotations?: An * `minLength` constraint. Object generators interpret it as the final number * of own properties. * - * @category Object checks + * @category validation * @since 4.0.0 */ export function isMinProperties(minProperties: number, annotations?: Annotations.Filter) { @@ -8286,11 +9224,13 @@ export function isMinProperties(minProperties: number, annotations?: Annotations (input) => Reflect.ownKeys(input).length >= minProperties, { expected: `a value with at least ${minProperties === 1 ? "1 entry" : `${minProperties} entries`}`, - meta: { - _tag: "isMinProperties", - minProperties + representation: { + id: "effect/schema/isMinProperties", + payload: { minProperties } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({ minProperties }), + toCode: () => ({ runtime: `Schema.isMinProperties(${minProperties})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength: minProperties @@ -8301,6 +9241,26 @@ export function isMinProperties(minProperties: number, annotations?: Annotations ) } +/** + * Reviver for persisted `isMinProperties` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMinProperties}. + * + * @see {@link isMinProperties} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMinPropertiesReviver: SchemaRepresentation.FilterReviver<{ + readonly minProperties: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMinProperties", + Struct({ minProperties: Natural }), + ({ annotations, payload }) => isMinProperties(payload.minProperties, annotations) +) + /** * Validates that an object contains at most the specified number of properties. * This includes both string and symbol keys when counting properties. @@ -8317,7 +9277,7 @@ export function isMinProperties(minProperties: number, annotations?: Annotations * `maxLength` constraint. Object generators interpret it as the final number * of own properties. * - * @category Object checks + * @category validation * @since 4.0.0 */ export function isMaxProperties(maxProperties: number, annotations?: Annotations.Filter) { @@ -8326,11 +9286,13 @@ export function isMaxProperties(maxProperties: number, annotations?: Annotations (input) => Reflect.ownKeys(input).length <= maxProperties, { expected: `a value with at most ${maxProperties === 1 ? "1 entry" : `${maxProperties} entries`}`, - meta: { - _tag: "isMaxProperties", - maxProperties + representation: { + id: "effect/schema/isMaxProperties", + payload: { maxProperties } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({ maxProperties }), + toCode: () => ({ runtime: `Schema.isMaxProperties(${maxProperties})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { maxLength: maxProperties @@ -8341,6 +9303,26 @@ export function isMaxProperties(maxProperties: number, annotations?: Annotations ) } +/** + * Reviver for persisted `isMaxProperties` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isMaxProperties}. + * + * @see {@link isMaxProperties} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isMaxPropertiesReviver: SchemaRepresentation.FilterReviver<{ + readonly maxProperties: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isMaxProperties", + Struct({ maxProperties: Natural }), + ({ annotations, payload }) => isMaxProperties(payload.maxProperties, annotations) +) + /** * Validates that an object contains between `minimum` and `maximum` properties (inclusive). * This includes both string and symbol keys when counting properties. @@ -8358,7 +9340,7 @@ export function isMaxProperties(maxProperties: number, annotations?: Annotations * `minLength` and `maxLength` constraints. Object generators interpret them as * the final number of own properties. * - * @category Object checks + * @category validation * @since 4.0.0 */ export function isPropertiesLengthBetween(minimum: number, maximum: number, annotations?: Annotations.Filter) { @@ -8370,12 +9352,14 @@ export function isPropertiesLengthBetween(minimum: number, maximum: number, anno expected: minimum === maximum ? `a value with exactly ${minimum === 1 ? "1 entry" : `${minimum} entries`}` : `a value with between ${minimum} and ${maximum} entries`, - meta: { - _tag: "isPropertiesLengthBetween", - minimum, - maximum + + representation: { + id: "effect/schema/isPropertiesLengthBetween", + payload: { minimum, maximum } }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: () => ({ minProperties: minimum, maxProperties: maximum }), + toCode: () => ({ runtime: `Schema.isPropertiesLengthBetween(${minimum}, ${maximum})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, arbitrary: { constraint: { minLength: minimum, @@ -8387,6 +9371,27 @@ export function isPropertiesLengthBetween(minimum: number, maximum: number, anno ) } +/** + * Reviver for persisted `isPropertiesLengthBetween` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isPropertiesLengthBetween}. + * + * @see {@link isPropertiesLengthBetween} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isPropertiesLengthBetweenReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number +}> = InternalSchema.makeFilterReviver( + "effect/schema/isPropertiesLengthBetween", + Struct({ minimum: Natural, maximum: Natural }), + ({ annotations, payload }) => isPropertiesLengthBetween(payload.minimum, payload.maximum, annotations) +) + /** * Validates that every own property key of an object satisfies the encoded side * of the provided key schema. @@ -8400,7 +9405,7 @@ export function isPropertiesLengthBetween(minimum: number, maximum: number, anno * For string property names, this corresponds to the `propertyNames` constraint * in JSON Schema. * - * @category Object checks + * @category validation * @since 4.0.0 */ export function isPropertyNames(keySchema: Constraint, annotations?: Annotations.Filter) { @@ -8418,22 +9423,43 @@ export function isPropertyNames(keySchema: Constraint, annotations?: Annotations } } if (Arr.isArrayNonEmpty(issues)) { - return new SchemaIssue.Composite(ast, Option_.some(input), issues) + return new SchemaIssue.Composite(ast, issues, input, options) } return true }, { expected: "an object with property names matching the schema", - meta: { - _tag: "isPropertyNames", - propertyNames: propertyNames.ast + representation: { + id: "effect/schema/isPropertyNames", + payload: null, + schemas: [propertyNames.ast] }, - [SchemaAST.STRUCTURAL_ANNOTATION_KEY]: true, + toJsonSchema: ({ schemas }) => ({ propertyNames: schemas[0] }), + toCode: ({ schemas }) => ({ runtime: `Schema.isPropertyNames(${schemas[0].runtime})` }), + [InternalAnnotations.STRUCTURAL_ANNOTATION_KEY]: true, ...annotations } ) } +/** + * Reviver for persisted `isPropertyNames` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isPropertyNames}. + * + * @see {@link isPropertyNames} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isPropertyNamesReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isPropertyNames", + Null, + ({ annotations, schemas }) => isPropertyNames(schemas[0], annotations) +) + /** * Validates that all items in an array are unique according to Effect equality. * @@ -8447,18 +9473,20 @@ export function isPropertyNames(keySchema: Constraint, annotations?: Annotations * `unique: true` constraint. Array generators translate it to `fast-check` * `uniqueArray` using Effect equality. * - * @category Array checks + * @category validation * @since 4.0.0 */ export function isUnique(annotations?: Annotations.Filter) { - const equivalence = Equal.asEquivalence() return makeFilter>( - (input) => Arr.dedupeWith(input, equivalence).length === input.length, + (input) => Arr.dedupe(input).length === input.length, { expected: "an array with unique items", - meta: { - _tag: "isUnique" + representation: { + id: "effect/schema/isUnique", + payload: null }, + toJsonSchema: () => ({ uniqueItems: true }), + toCode: () => ({ runtime: "Schema.isUnique()" }), arbitrary: { constraint: { unique: true @@ -8469,6 +9497,24 @@ export function isUnique(annotations?: Annotations.Filter) { ) } +/** + * Reviver for persisted `isUnique` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isUnique}. + * + * @see {@link isUnique} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isUniqueReviver: SchemaRepresentation.FilterReviver = InternalSchema.makeFilterReviver( + "effect/schema/isUnique", + Null, + ({ annotations }) => isUnique(annotations) +) + // ----------------------------------------------------------------------------- // Built-in Schemas // ----------------------------------------------------------------------------- @@ -8476,7 +9522,7 @@ export function isUnique(annotations?: Annotations.Filter) { /** * Type-level representation of {@link NonEmptyString}. * - * @category string + * @category models * @since 3.10.0 */ export interface NonEmptyString extends String { @@ -8487,7 +9533,7 @@ export interface NonEmptyString extends String { * Schema for non-empty strings. Validates that a string has at least one * character. * - * @category string + * @category schemas * @since 3.10.0 */ export const NonEmptyString: NonEmptyString = String.check(isNonEmpty()) @@ -8495,7 +9541,7 @@ export const NonEmptyString: NonEmptyString = String.check(isNonEmpty()) /** * Type-level representation of {@link Char}. * - * @category string + * @category models * @since 3.10.0 */ export interface Char extends String { @@ -8518,7 +9564,7 @@ export interface Char extends String { * @see {@link NonEmptyString} for strings with length greater than zero * @see {@link isLengthBetween} for the underlying length check * - * @category string + * @category schemas * @since 3.10.0 */ export const Char: Char = String.check(isLengthBetween(1, 1)) @@ -8526,7 +9572,7 @@ export const Char: Char = String.check(isLengthBetween(1, 1)) /** * Type-level representation returned by {@link Option}. * - * @category Option + * @category models * @since 3.10.0 */ export interface Option extends @@ -8549,7 +9595,7 @@ export interface Option extends * `None` is represented as `{ _tag: "None" }`, while `Some` is represented as * `{ _tag: "Some", value }` using the wrapped schema's `Iso` type. * - * @category Option + * @category utility types * @since 4.0.0 */ export type OptionIso = @@ -8559,7 +9605,7 @@ export type OptionIso = /** * Schema for `Option` values. * - * @category Option + * @category schemas * @since 3.10.0 */ export function Option(value: A): Option { @@ -8578,22 +9624,22 @@ export function Option(value: A): Option { SchemaParser.decodeUnknownEffect(value)(input.value, options), { onSuccess: Option_.some, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["value"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "value", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) }, { - typeConstructor: { - _tag: "effect/Option" - }, - generation: { - runtime: `Schema.Option(?)`, - Type: `Option.Option`, - importDeclaration: `import * as Option from "effect/Option"` + representation: { + id: "effect/schema/Option", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.Option(${typeParameters[0].runtime})`, + Type: `Option.Option<${typeParameters[0].Type}>`, + importDeclarations: [`import * as Option from "effect/Option"`] + }), expected: "Option", toCodec: ([value]) => link>()( @@ -8625,10 +9671,31 @@ export function Option(value: A): Option { return make(schema.ast, { value }) } +/** + * Reviver for persisted `Option` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link Option}. + * + * @see {@link Option} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const OptionReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Option", + Null, + ({ annotations, typeParameters }) => { + const schema = Option(typeParameters[0]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link OptionFromNullOr}. * - * @category Option + * @category models * @since 3.10.0 */ export interface OptionFromNullOr extends decodeTo>, NullOr> { @@ -8643,7 +9710,7 @@ export interface OptionFromNullOr extends decodeTo(schema: S): OptionFromNullOr { @@ -8656,7 +9723,7 @@ export function OptionFromNullOr(schema: S): OptionFromNul /** * Type-level representation returned by {@link OptionFromUndefinedOr}. * - * @category Option + * @category models * @since 3.10.0 */ export interface OptionFromUndefinedOr extends decodeTo>, UndefinedOr> { @@ -8672,7 +9739,7 @@ export interface OptionFromUndefinedOr extends decodeTo(schema: S): OptionFromUndefinedOr { @@ -8685,7 +9752,7 @@ export function OptionFromUndefinedOr(schema: S): OptionFr /** * Type-level representation returned by {@link OptionFromNullishOr}. * - * @category Option + * @category models * @since 3.10.0 */ export interface OptionFromNullishOr extends decodeTo>, NullishOr> { @@ -8702,7 +9769,7 @@ export interface OptionFromNullishOr extends decodeTo( @@ -8720,7 +9787,7 @@ export function OptionFromNullishOr( /** * Type-level representation returned by {@link OptionFromOptionalKey}. * - * @category Option + * @category models * @since 4.0.0 */ export interface OptionFromOptionalKey extends decodeTo>, optionalKey> { @@ -8735,7 +9802,7 @@ export interface OptionFromOptionalKey extends decodeTo(schema: S): OptionFromOptionalKey { @@ -8748,7 +9815,7 @@ export function OptionFromOptionalKey(schema: S): OptionFr /** * Type-level representation returned by {@link OptionFromOptional}. * - * @category Option + * @category models * @since 4.0.0 */ export interface OptionFromOptional extends decodeTo>, optional> { @@ -8765,7 +9832,7 @@ export interface OptionFromOptional extends decodeTo(schema: S): OptionFromOptional { @@ -8778,7 +9845,7 @@ export function OptionFromOptional(schema: S): OptionFromO /** * Type-level representation returned by {@link OptionFromOptionalNullOr}. * - * @category Option + * @category models * @since 4.0.0 */ export interface OptionFromOptionalNullOr @@ -8798,7 +9865,7 @@ export interface OptionFromOptionalNullOr * according to `options.onNoneEncoding`: `"omit"` encodes a missing key, * `null` encodes `null`, and `undefined` encodes `undefined`. * - * @category Option + * @category schemas * @since 4.0.0 */ export function OptionFromOptionalNullOr( @@ -8825,7 +9892,7 @@ export function OptionFromOptionalNullOr( /** * Type-level representation returned by {@link Result}. * - * @category schemas + * @category models * @since 4.0.0 */ export interface Result extends @@ -8849,7 +9916,7 @@ export interface Result extends * Successful results are represented as `{ _tag: "Success", success }`, while * failed results are represented as `{ _tag: "Failure", failure }`. * - * @category schemas + * @category utility types * @since 4.0.0 */ export type ResultIso = @@ -8874,32 +9941,31 @@ export function Result( [success, failure], ([success, failure]) => (input, ast, options) => { if (!Result_.isResult(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } switch (input._tag) { case "Success": return Effect.mapBothEager(SchemaParser.decodeEffect(success)(input.success, options), { onSuccess: Result_.succeed, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["success"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "success", issue, input, options) }) case "Failure": return Effect.mapBothEager(SchemaParser.decodeEffect(failure)(input.failure, options), { onSuccess: Result_.fail, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["failure"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "failure", issue, input, options) }) } }, { - typeConstructor: { - _tag: "effect/Result" - }, - generation: { - runtime: `Schema.Result(?, ?)`, - Type: `Result.Result`, - importDeclaration: `import * as Result from "effect/Result"` + representation: { + id: "effect/schema/Result", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.Result(${typeParameters[0].runtime}, ${typeParameters[1].runtime})`, + Type: `Result.Result<${typeParameters[0].Type}, ${typeParameters[1].Type}>`, + importDeclarations: [`import * as Result from "effect/Result"`] + }), expected: "Result", toCodec: ([success, failure]) => link>()( @@ -8939,10 +10005,31 @@ export function Result( return make(schema.ast, { success, failure }) } +/** + * Reviver for persisted {@link Result} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link Result}. + * + * @see {@link Result} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const ResultReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Result", + Null, + ({ annotations, typeParameters }) => { + const schema = Result(typeParameters[0], typeParameters[1]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link Redacted}. * - * @category Redacted + * @category models * @since 3.10.0 */ export interface Redacted extends @@ -8956,14 +10043,39 @@ export interface Redacted extends readonly value: S } +type RedactedRepresentationOptions = { + readonly label?: string | undefined + readonly disallowJsonEncode?: true | undefined +} + +type NormalizedRedactedOptions = + | { readonly label: string } + | { readonly disallowJsonEncode: true } + | { readonly label: string; readonly disallowJsonEncode: true } + +type RedactedRepresentationPayload = RedactedRepresentationOptions | null + +const RedactedOptionsPayload = declare((input): input is RedactedRepresentationOptions => { + if (!Predicate.isObject(input)) { + return false + } + const keys = globalThis.Object.keys(input) + return keys.length > 0 && keys.every((key) => { + switch (key) { + case "label": + return typeof input[key] === "string" + case "disallowJsonEncode": + return input[key] === true + default: + return false + } + }) +}) + +const RedactedRepresentationPayload: Decoder = Union([Null, RedactedOptionsPayload]) + /** - * Schema for values that hide sensitive information from error output and - * inspection. - * - * **Details** - * - * If the wrapped schema fails, the issue will be redacted to prevent both - * the actual value and the schema details from being exposed. + * Schema for `Redacted` values, which hide their contents from inspection. * * Options: * @@ -8977,15 +10089,22 @@ export interface Redacted extends * sensitive and should not be exposed in JSON. * * @see {@link RedactedFromValue} for decoding raw values and wrapping them in `Redacted`. - * @category Redacted + * @category schemas * @since 3.10.0 */ export function Redacted(value: S, options?: { readonly label?: string | undefined readonly disallowJsonEncode?: boolean | undefined }): Redacted { - const decodeLabel = typeof options?.label === "string" - ? SchemaParser.decodeUnknownEffect(Literal(options.label)) + const label = typeof options?.label === "string" ? options.label : undefined + const disallowJsonEncode = options?.disallowJsonEncode === true + const normalizedOptions: NormalizedRedactedOptions | undefined = label !== undefined + ? disallowJsonEncode ? { label, disallowJsonEncode: true } : { label } + : disallowJsonEncode + ? { disallowJsonEncode: true } + : undefined + const decodeLabel = label !== undefined + ? SchemaParser.decodeUnknownEffect(Literal(label)) : undefined const schema = declareConstructor, Redacted_.Redacted>()( [value], @@ -9004,35 +10123,44 @@ export function Redacted(value: S, options?: { SchemaParser.decodeUnknownEffect(value)(Redacted_.value(input), poptions), { onSuccess: () => input, - onFailure: (/** ignore the actual issue because of security reasons */) => { - const oinput = Option_.some(input) - return new SchemaIssue.Composite(ast, oinput, [ - new SchemaIssue.Pointer(["value"], new SchemaIssue.InvalidValue(oinput)) - ]) + onFailure: (/** ignore the issue because of security reasons */) => { + return new SchemaIssue.Composite( + ast, + [ + new SchemaIssue.Pointer( + ["value"], + new SchemaIssue.InvalidValue(undefined, input, poptions) + ) + ], + input, + poptions + ) } } ) ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, poptions)) }, { - typeConstructor: { - _tag: "effect/Redacted", - options - }, - generation: { - runtime: options !== undefined ? `Schema.Redacted(?, ${format(options)})` : `Schema.Redacted(?)`, - Type: `Redacted.Redacted`, - importDeclaration: `import * as Redacted from "effect/Redacted"` + representation: { + id: "effect/schema/Redacted", + payload: normalizedOptions ?? null }, + toCode: ({ typeParameters }) => ({ + runtime: normalizedOptions !== undefined + ? `Schema.Redacted(${typeParameters[0].runtime}, ${format(normalizedOptions)})` + : `Schema.Redacted(${typeParameters[0].runtime})`, + Type: `Redacted.Redacted<${typeParameters[0].Type}>`, + importDeclarations: [`import * as Redacted from "effect/Redacted"`] + }), expected: "Redacted", toCodecJson: ([value]) => link>()( - redact(value), + value, { - decode: SchemaGetter.transform((e) => Redacted_.make(e, { label: options?.label })), - encode: options?.disallowJsonEncode ? + decode: SchemaGetter.transform((e) => Redacted_.make(e, { label })), + encode: disallowJsonEncode ? SchemaGetter.forbidden((oe) => "Cannot serialize Redacted" + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") @@ -9041,8 +10169,8 @@ export function Redacted(value: S, options?: { } ), toArbitrary: ([value]) => () => ({ - arbitrary: value.arbitrary.map((a) => Redacted_.make(a, { label: options?.label })), - terminal: value.terminal?.map((a) => Redacted_.make(a, { label: options?.label })) + arbitrary: value.arbitrary.map((a) => Redacted_.make(a, { label })), + terminal: value.terminal?.map((a) => Redacted_.make(a, { label })) }), toFormatter: () => globalThis.String, toEquivalence: ([value]) => Redacted_.makeEquivalence(value) @@ -9052,26 +10180,34 @@ export function Redacted(value: S, options?: { } /** - * Type-level representation returned by {@link RedactedFromValue}. + * Reviver for persisted {@link Redacted} declarations. * - * @category Redacted + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link Redacted}. + * + * @see {@link Redacted} for creating the corresponding schema + * + * @category schemas * @since 4.0.0 */ -export interface RedactedFromValue - extends decodeTo>, middlewareDecoding> -{ - readonly "Rebuild": RedactedFromValue -} +export const RedactedReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Redacted", + RedactedRepresentationPayload, + ({ annotations, payload, typeParameters }) => { + const schema = Redacted(typeParameters[0], payload ?? undefined) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) /** - * Middleware that wraps decoded errors in `Redacted`, preventing sensitive - * schema details from leaking in error messages. + * Type-level representation returned by {@link RedactedFromValue}. * - * @category Redacted + * @category models * @since 4.0.0 */ -export function redact(schema: S): middlewareDecoding { - return middlewareDecoding(Effect.mapErrorEager(SchemaIssue.redact))(schema) +export interface RedactedFromValue extends decodeTo>, S> { + readonly "Rebuild": RedactedFromValue } /** @@ -9080,36 +10216,34 @@ export function redact(schema: S): middlewareDecoding(value: S, options?: { readonly label?: string | undefined readonly disallowEncode?: boolean | undefined }): RedactedFromValue { - return redact(value).pipe( - decodeTo( - Redacted(toType(value), { - label: options?.label, - disallowJsonEncode: options?.disallowEncode - }), - { - decode: SchemaGetter.transform((t) => Redacted_.make(t, { label: options?.label })), - encode: options?.disallowEncode ? - SchemaGetter.forbidden((oe) => - "Cannot encode Redacted" + - (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") - ) : - SchemaGetter.transform(Redacted_.value) - } - ) - ) + return decodeTo>, S>( + Redacted(toType(value), { + label: options?.label, + disallowJsonEncode: options?.disallowEncode + }), + { + decode: SchemaGetter.transform((t) => Redacted_.make(t, { label: options?.label })), + encode: options?.disallowEncode ? + SchemaGetter.forbidden((oe) => + "Cannot encode Redacted" + + (Option_.isSome(oe) && typeof oe.value.label === "string" ? ` with label: "${oe.value.label}"` : "") + ) : + SchemaGetter.transform(Redacted_.value) + } + )(value) } /** * Type-level representation returned by {@link CauseReason}. * - * @category CauseReason + * @category models * @since 4.0.0 */ export interface CauseReason extends @@ -9133,7 +10267,7 @@ export interface CauseReason extends * Failures are represented with a `Fail` tag and encoded error, defects with a * `Die` tag and encoded defect, and interrupts with an optional `fiberId`. * - * @category CauseReason + * @category utility types * @since 4.0.0 */ export type CauseReasonIso = { @@ -9164,7 +10298,7 @@ export type CauseReasonIso = { * @see {@link Cause} for constructing schemas for full Cause values * @see {@link CauseReasonIso} for the ISO shape of each cause reason * - * @category CauseReason + * @category schemas * @since 4.0.0 */ export function CauseReason(error: E, defect: D): CauseReason { @@ -9172,7 +10306,7 @@ export function CauseReason(error: E [error, defect], ([error, defect]) => (input, ast, options) => { if (!Cause_.isReason(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } switch (input._tag) { case "Fail": @@ -9180,8 +10314,7 @@ export function CauseReason(error: E SchemaParser.decodeUnknownEffect(error)(input.error, options), { onSuccess: Cause_.makeFailReason, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["error"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "error", issue, input, options) } ) case "Die": @@ -9189,8 +10322,7 @@ export function CauseReason(error: E SchemaParser.decodeUnknownEffect(defect)(input.defect, options), { onSuccess: Cause_.makeDieReason, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["defect"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "defect", issue, input, options) } ) case "Interrupt": @@ -9198,14 +10330,15 @@ export function CauseReason(error: E } }, { - typeConstructor: { - _tag: "effect/Cause/Failure" - }, - generation: { - runtime: `Schema.CauseReason(?, ?)`, - Type: `Cause.Failure`, - importDeclaration: `import * as Cause from "effect/Cause"` + representation: { + id: "effect/schema/CauseReason", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.CauseReason(${typeParameters[0].runtime}, ${typeParameters[1].runtime})`, + Type: `Cause.Failure<${typeParameters[0].Type}, ${typeParameters[1].Type}>`, + importDeclarations: [`import * as Cause from "effect/Cause"`] + }), expected: "Cause.Failure", toCodec: ([error, defect]) => link>()( @@ -9236,6 +10369,27 @@ export function CauseReason(error: E return make(schema.ast, { error, defect }) } +/** + * Reviver for persisted `CauseReason` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link CauseReason}. + * + * @see {@link CauseReason} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const CauseReasonReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/CauseReason", + Null, + ({ annotations, typeParameters }) => { + const schema = CauseReason(typeParameters[0], typeParameters[1]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + function causeReasonToArbitrary( error: Annotations.ToArbitrary.TypeParameter, defect: Annotations.ToArbitrary.TypeParameter @@ -9282,7 +10436,7 @@ function causeReasonToFormatter(error: Formatter, defect: Formatter extends @@ -9310,7 +10464,7 @@ export interface Cause extends * @see {@link Cause} for constructing schemas for full Cause values * @see {@link CauseReasonIso} for the ISO shape of each array element * - * @category Cause + * @category utility types * @since 4.0.0 */ export type CauseIso = ReadonlyArray> @@ -9333,7 +10487,7 @@ export type CauseIso = ReadonlyArray * @see {@link CauseReason} for the schema used by each individual cause reason * @see {@link CauseIso} for the ordered array representation used by the schema ISO * - * @category Cause + * @category schemas * @since 3.10.0 */ export function Cause(error: E, defect: D): Cause { @@ -9343,24 +10497,24 @@ export function Cause(error: E, defe const failures = ArraySchema(CauseReason(error, defect)) return (input, ast, options) => { if (!Cause_.isCause(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } return Effect.mapBothEager(SchemaParser.decodeUnknownEffect(failures)(input.reasons, options), { onSuccess: Cause_.fromReasons, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["failures"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "failures", issue, input, options) }) } }, { - typeConstructor: { - _tag: "effect/Cause" - }, - generation: { - runtime: `Schema.Cause(?, ?)`, - Type: `Cause.Cause`, - importDeclaration: `import * as Cause from "effect/Cause"` + representation: { + id: "effect/schema/Cause", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.Cause(${typeParameters[0].runtime}, ${typeParameters[1].runtime})`, + Type: `Cause.Cause<${typeParameters[0].Type}, ${typeParameters[1].Type}>`, + importDeclarations: [`import * as Cause from "effect/Cause"`] + }), expected: "Cause", toCodec: ([error, defect]) => link>()( @@ -9378,6 +10532,27 @@ export function Cause(error: E, defe return make(schema.ast, { error, defect }) } +/** + * Reviver for persisted `Cause` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link Cause}. + * + * @see {@link Cause} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const CauseReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Cause", + Null, + ({ annotations, typeParameters }) => { + const schema = Cause(typeParameters[0], typeParameters[1]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + function causeToArbitrary( error: Annotations.ToArbitrary.TypeParameter, defect: Annotations.ToArbitrary.TypeParameter @@ -9401,17 +10576,17 @@ function causeToFormatter(error: Formatter, defect: Formatter) { } /** - * Type-level representation of {@link Error}. + * Type-level representation of {@link ErrorInstance}. * - * @category Error + * @category models * @since 4.0.0 */ -export interface Error extends instanceOf { - readonly "Rebuild": Error +export interface ErrorInstance extends instanceOf { + readonly "Rebuild": ErrorInstance } /** - * Options for {@link Error} and {@link Defect}. + * Options for {@link ErrorInstance} and {@link Defect}. * * @category options * @since 4.0.0 @@ -9432,13 +10607,36 @@ export interface ErrorOptions { readonly excludeCause?: boolean | undefined } +type ErrorRepresentationOptions = { + readonly includeStack?: true | undefined + readonly excludeCause?: true | undefined +} + +type NormalizedErrorOptions = + | { readonly includeStack: true } + | { readonly excludeCause: true } + | { readonly includeStack: true; readonly excludeCause: true } + +type ErrorRepresentationPayload = ErrorRepresentationOptions | null + +const ErrorOptionsPayload = declare((input): input is ErrorRepresentationOptions => { + if (!Predicate.isObject(input)) { + return false + } + const keys = globalThis.Object.keys(input) + return keys.length > 0 && + keys.every((key) => (key === "includeStack" || key === "excludeCause") && input[key] === true) +}) + +const ErrorRepresentationPayload: Decoder = Union([Null, ErrorOptionsPayload]) + type ErrorOptionsKey = 0 | 1 | 2 | 3 const getErrorOptionsKey = (options?: ErrorOptions): ErrorOptionsKey => ((options?.includeStack === true ? 1 : 0) | (options?.excludeCause === true ? 2 : 0)) as ErrorOptionsKey -const getErrorOptions = (key: ErrorOptionsKey): ErrorOptions | undefined => { +const getErrorOptions = (key: ErrorOptionsKey): NormalizedErrorOptions | undefined => { switch (key) { case 0: return undefined @@ -9451,7 +10649,7 @@ const getErrorOptions = (key: ErrorOptionsKey): ErrorOptions | undefined => { } } -const errorSchemaCache: Array = [] +const errorSchemaCache: Array = [] /** * Schema for JavaScript `Error` objects. @@ -9465,10 +10663,10 @@ const errorSchemaCache: Array = [] * traces are omitted by default for security. Pass `{ includeStack: true }` to * include stack traces, or `{ excludeCause: true }` to omit causes. * - * @category constructors + * @category schemas * @since 4.0.0 */ -export function Error(options?: ErrorOptions): Error { +export function ErrorInstance(options?: ErrorOptions): ErrorInstance { const key = getErrorOptionsKey(options) const cached = errorSchemaCache[key] if (cached !== undefined) { @@ -9476,14 +10674,16 @@ export function Error(options?: ErrorOptions): Error { } const normalizedOptions = getErrorOptions(key) const schema = instanceOf(globalThis.Error, { - typeConstructor: { - _tag: "Error", - ...(normalizedOptions === undefined ? {} : { options: normalizedOptions }) + representation: { + id: "effect/schema/Error", + payload: normalizedOptions ?? null }, - generation: { - runtime: normalizedOptions !== undefined ? `Schema.Error(${format(normalizedOptions)})` : `Schema.Error()`, + toCode: () => ({ + runtime: normalizedOptions !== undefined + ? `Schema.ErrorInstance(${format(normalizedOptions)})` + : `Schema.ErrorInstance()`, Type: `globalThis.Error` - }, + }), expected: "Error", toCodecJson: () => link()(JsonError, SchemaTransformation.errorFromJsonError(normalizedOptions)), toArbitrary: () => (fc) => fc.string().map((message) => new globalThis.Error(message)) @@ -9492,10 +10692,31 @@ export function Error(options?: ErrorOptions): Error { return schema } +/** + * Reviver for persisted {@link ErrorInstance} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link ErrorInstance}. + * + * @see {@link ErrorInstance} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const ErrorInstanceReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Error", + ErrorRepresentationPayload, + ({ annotations, payload }) => { + const schema = ErrorInstance(payload ?? undefined) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation of {@link Defect}. * - * @category Defect + * @category models * @since 3.10.0 */ export interface Defect extends decodeTo { @@ -9541,8 +10762,8 @@ const defectSchemaCache: Array = [] * - Values that cannot be represented as JSON fall back to Effect's formatted * string representation. * - * @see {@link Error} for a schema that only accepts JavaScript `Error` values. - * @category constructors + * @see {@link ErrorInstance} for a schema that only accepts JavaScript `Error` values. + * @category schemas * @since 4.0.0 */ export function Defect(options?: ErrorOptions): Defect { @@ -9559,7 +10780,7 @@ export function Defect(options?: ErrorOptions): Defect { /** * Type-level representation returned by {@link Exit}. * - * @category Exit + * @category models * @since 3.10.0 */ export interface Exit extends @@ -9584,7 +10805,7 @@ export interface Exit = { @@ -9604,7 +10825,7 @@ export type ExitIso( @@ -9622,7 +10843,7 @@ export function Exit { if (!Exit_.isExit(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } switch (input._tag) { case "Success": @@ -9630,8 +10851,7 @@ export function Exit - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["value"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "value", issue, input, options) } ) case "Failure": @@ -9639,22 +10859,24 @@ export function Exit - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["cause"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "cause", issue, input, options) } ) } } }, { - typeConstructor: { - _tag: "effect/Exit" - }, - generation: { - runtime: `Schema.Exit(?, ?, ?)`, - Type: `Exit.Exit`, - importDeclaration: `import * as Exit from "effect/Exit"` + representation: { + id: "effect/schema/Exit", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.Exit(${typeParameters[0].runtime}, ${typeParameters[1].runtime}, ${ + typeParameters[2].runtime + })`, + Type: `Exit.Exit<${typeParameters[0].Type}, ${typeParameters[1].Type}, ${typeParameters[2].Type}>`, + importDeclarations: [`import * as Exit from "effect/Exit"`] + }), expected: "Exit", toCodec: ([value, error, defect]) => link>()( @@ -9712,10 +10934,31 @@ export function Exit { + const schema = Exit(typeParameters[0], typeParameters[1], typeParameters[2]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link ReadonlyMap}. * - * @category ReadonlyMap + * @category models * @since 4.0.0 */ export interface $ReadonlyMap extends @@ -9735,7 +10978,7 @@ export interface $ReadonlyMap * Iso representation used for `ReadonlyMap` schemas: an array of readonly * `[key, value]` tuples using each entry schema's `Iso` type. * - * @category ReadonlyMap + * @category utility types * @since 4.0.0 */ export type ReadonlyMapIso = ReadonlyArray< @@ -9836,7 +11079,7 @@ function entriesArbitrary( * Schema for readonly maps whose keys and values conform to the provided * schemas. * - * @category ReadonlyMap + * @category schemas * @since 3.10.0 */ export function ReadonlyMap( @@ -9857,22 +11100,22 @@ export function ReadonlyMap( SchemaParser.decodeUnknownEffect(array)([...input], options), { onSuccess: (array: ReadonlyArray) => new globalThis.Map(array), - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["entries"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "entries", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } }, { - typeConstructor: { - _tag: "ReadonlyMap" - }, - generation: { - runtime: `Schema.ReadonlyMap(?, ?)`, - Type: `globalThis.ReadonlyMap` + representation: { + id: "effect/schema/ReadonlyMap", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.ReadonlyMap(${typeParameters[0].runtime}, ${typeParameters[1].runtime})`, + Type: `globalThis.ReadonlyMap<${typeParameters[0].Type}, ${typeParameters[1].Type}>` + }), expected: "ReadonlyMap", toCodec: ([key, value]) => link>()( @@ -9897,10 +11140,31 @@ export function ReadonlyMap( return make(schema.ast, { key, value }) } +/** + * Reviver for persisted {@link ReadonlyMap} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link ReadonlyMap}. + * + * @see {@link ReadonlyMap} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const ReadonlyMapReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/ReadonlyMap", + Null, + ({ annotations, typeParameters }) => { + const schema = ReadonlyMap(typeParameters[0], typeParameters[1]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link HashMap}. * - * @category HashMap + * @category models * @since 3.10.0 */ export interface HashMap extends @@ -9920,7 +11184,7 @@ export interface HashMap exten * Iso representation used for `HashMap` schemas: an array of readonly * `[key, value]` tuples using each entry schema's `Iso` type. * - * @category HashMap + * @category utility types * @since 4.0.0 */ export type HashMapIso = ReadonlyArray< @@ -9930,7 +11194,7 @@ export type HashMapIso = Reado /** * Schema for hash maps whose keys and values conform to the provided schemas. * - * @category HashMap + * @category schemas * @since 3.10.0 */ export function HashMap(key: Key, value: Value): HashMap { @@ -9948,23 +11212,23 @@ export function HashMap(key: K SchemaParser.decodeUnknownEffect(entries)(HashMap_.toEntries(input), options), { onSuccess: HashMap_.fromIterable, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["entries"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "entries", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } }, { - typeConstructor: { - _tag: "effect/HashMap" - }, - generation: { - runtime: `Schema.HashMap(?, ?)`, - Type: `HashMap.HashMap`, - importDeclaration: `import * as HashMap from "effect/HashMap"` + representation: { + id: "effect/schema/HashMap", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.HashMap(${typeParameters[0].runtime}, ${typeParameters[1].runtime})`, + Type: `HashMap.HashMap<${typeParameters[0].Type}, ${typeParameters[1].Type}>`, + importDeclarations: [`import * as HashMap from "effect/HashMap"`] + }), expected: "HashMap", toCodec: ([key, value]) => link>()( @@ -9989,10 +11253,31 @@ export function HashMap(key: K return make(schema.ast, { key, value }) } +/** + * Reviver for persisted `HashMap` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link HashMap}. + * + * @see {@link HashMap} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const HashMapReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/HashMap", + Null, + ({ annotations, typeParameters }) => { + const schema = HashMap(typeParameters[0], typeParameters[1]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link ReadonlySet}. * - * @category ReadonlySet + * @category models * @since 4.0.0 */ export interface $ReadonlySet extends @@ -10011,7 +11296,7 @@ export interface $ReadonlySet extends * Iso representation used for `ReadonlySet` schemas: an array of element values * using the element schema's `Iso` type. * - * @category ReadonlySet + * @category utility types * @since 4.0.0 */ export type ReadonlySetIso = ReadonlyArray @@ -10019,7 +11304,7 @@ export type ReadonlySetIso = ReadonlyArray(value: Value): $ReadonlySet { @@ -10037,22 +11322,22 @@ export function ReadonlySet(value: Value): $ReadonlySe SchemaParser.decodeUnknownEffect(array)([...input], options), { onSuccess: (array: ReadonlyArray) => new globalThis.Set(array), - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "values", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } }, { - typeConstructor: { - _tag: "ReadonlySet" - }, - generation: { - runtime: `Schema.ReadonlySet(?)`, - Type: `globalThis.ReadonlySet` + representation: { + id: "effect/schema/ReadonlySet", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.ReadonlySet(${typeParameters[0].runtime})`, + Type: `globalThis.ReadonlySet<${typeParameters[0].Type}>` + }), expected: "ReadonlySet", toCodec: ([value]) => link>()( @@ -10078,10 +11363,31 @@ export function ReadonlySet(value: Value): $ReadonlySe return make(schema.ast, { value }) } +/** + * Reviver for persisted {@link ReadonlySet} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link ReadonlySet}. + * + * @see {@link ReadonlySet} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const ReadonlySetReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/ReadonlySet", + Null, + ({ annotations, typeParameters }) => { + const schema = ReadonlySet(typeParameters[0]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link HashSet}. * - * @category HashSet + * @category models * @since 3.10.0 */ export interface HashSet extends @@ -10100,7 +11406,7 @@ export interface HashSet extends * Iso representation used for `HashSet` schemas: an array of element values * using the element schema's `Iso` type. * - * @category HashSet + * @category utility types * @since 4.0.0 */ export type HashSetIso = ReadonlyArray @@ -10108,7 +11414,7 @@ export type HashSetIso = ReadonlyArray /** * Schema for hash sets whose values conform to the provided element schema. * - * @category HashSet + * @category schemas * @since 3.10.0 */ export function HashSet(value: Value): HashSet { @@ -10126,22 +11432,22 @@ export function HashSet(value: Value): HashSet SchemaParser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), { onSuccess: HashSet_.fromIterable, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "values", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } }, { - typeConstructor: { - _tag: "effect/HashSet" - }, - generation: { - runtime: `Schema.HashSet(?)`, - Type: `HashSet.HashSet` + representation: { + id: "effect/schema/HashSet", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.HashSet(${typeParameters[0].runtime})`, + Type: `HashSet.HashSet<${typeParameters[0].Type}>` + }), expected: "HashSet", toCodec: ([value]) => link>()( @@ -10167,10 +11473,31 @@ export function HashSet(value: Value): HashSet return make(schema.ast, { value }) } +/** + * Reviver for persisted `HashSet` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link HashSet}. + * + * @see {@link HashSet} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const HashSetReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/HashSet", + Null, + ({ annotations, typeParameters }) => { + const schema = HashSet(typeParameters[0]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation returned by {@link Chunk}. * - * @category Chunk + * @category models * @since 3.10.0 */ export interface Chunk extends @@ -10196,7 +11523,7 @@ export interface Chunk extends * * @see {@link Chunk} for the schema interface and constructor that use this ISO representation * - * @category Chunk + * @category utility types * @since 4.0.0 */ export type ChunkIso = ReadonlyArray @@ -10204,7 +11531,7 @@ export type ChunkIso = ReadonlyArray /** * Schema for chunks whose values conform to the provided element schema. * - * @category Chunk + * @category schemas * @since 3.10.0 */ export function Chunk(value: Value): Chunk { @@ -10222,22 +11549,22 @@ export function Chunk(value: Value): Chunk { SchemaParser.decodeUnknownEffect(values)(Arr.fromIterable(input), options), { onSuccess: Chunk_.fromIterable, - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option_.some(input), [new SchemaIssue.Pointer(["values"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "values", issue, input, options) } ) } - return Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } }, { - typeConstructor: { - _tag: "effect/Chunk" - }, - generation: { - runtime: `Schema.Chunk(?)`, - Type: `Chunk.Chunk` + representation: { + id: "effect/schema/Chunk", + payload: null }, + toCode: ({ typeParameters }) => ({ + runtime: `Schema.Chunk(${typeParameters[0].runtime})`, + Type: `Chunk.Chunk<${typeParameters[0].Type}>` + }), expected: "Chunk", toCodec: ([value]) => link>()( @@ -10263,10 +11590,31 @@ export function Chunk(value: Value): Chunk { return make(schema.ast, { value }) } +/** + * Reviver for persisted {@link Chunk} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain schemas created by {@link Chunk}. + * + * @see {@link Chunk} for creating the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const ChunkReviver = InternalSchema.makeDeclarationReviver( + "effect/schema/Chunk", + Null, + ({ annotations, typeParameters }) => { + const schema = Chunk(typeParameters[0]) + return annotations === undefined ? schema : schema.annotate(annotations) + } +) + /** * Type-level representation of {@link RegExp}. * - * @category RegExp + * @category models * @since 4.0.0 */ export interface RegExp extends instanceOf { @@ -10280,19 +11628,20 @@ export interface RegExp extends instanceOf { * * The default JSON serializer encodes a `RegExp` as `{ source, flags }`. * - * @category RegExp + * @category schemas * @since 4.0.0 */ export const RegExp: RegExp = instanceOf( globalThis.RegExp, { - typeConstructor: { - _tag: "RegExp" + representation: { + id: "effect/schema/RegExp", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.RegExp`, Type: `globalThis.RegExp` - }, + }), expected: "RegExp", toCodecJson: () => link()( @@ -10301,10 +11650,15 @@ export const RegExp: RegExp = instanceOf( flags: String }), SchemaTransformation.transformOrFail({ - decode: (e) => + decode: (e, options) => Effect.try({ try: () => new globalThis.RegExp(e.source, e.flags), - catch: (e) => new SchemaIssue.InvalidValue(Option_.some(e), { message: globalThis.String(e) }) + catch: () => + new SchemaIssue.InvalidValue( + { expected: "valid RegExp source and flags" }, + e, + options + ) }), encode: (regExp) => Effect.succeed({ @@ -10339,10 +11693,27 @@ export const RegExp: RegExp = instanceOf( } ) +/** + * Reviver for persisted `RegExp` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link RegExp} schema. + * + * @see {@link RegExp} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const RegExpReviver = makeFixedDeclarationReviver( + "effect/schema/RegExp", + RegExp +) + /** * Type-level representation of {@link URL}. * - * @category URL + * @category models * @since 4.0.0 */ export interface URL extends instanceOf { @@ -10360,19 +11731,20 @@ const URLString = String.annotate({ expected: "a string that will be decoded as * * - encodes `URL` as a `string` * - * @category URL + * @category schemas * @since 4.0.0 */ export const URL: URL = instanceOf( globalThis.URL, { - typeConstructor: { - _tag: "URL" + representation: { + id: "effect/schema/URL", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.URL`, Type: `globalThis.URL` - }, + }), expected: "URL", toCodecJson: () => link()( @@ -10384,10 +11756,27 @@ export const URL: URL = instanceOf( } ) +/** + * Reviver for persisted `URL` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link URL} schema. + * + * @see {@link URL} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const URLReviver = makeFixedDeclarationReviver( + "effect/schema/URL", + URL +) + /** * Type-level representation of {@link URLFromString}. * - * @category URL + * @category models * @since 4.0.0 */ export interface URLFromString extends decodeTo { @@ -10405,7 +11794,7 @@ export interface URLFromString extends decodeTo { * Encoding: * - A `URL` is encoded as a `string` * - * @category URL + * @category schemas * @since 4.0.0 */ export const URLFromString: URLFromString = URLString.pipe(decodeTo(URL, SchemaTransformation.urlFromString)) @@ -10413,28 +11802,19 @@ export const URLFromString: URLFromString = URLString.pipe(decodeTo(URL, SchemaT /** * Type-level representation of {@link Date}. * - * @category Date + * @category models * @since 4.0.0 */ -export interface Date extends instanceOf { +export interface Date extends declare { readonly "Rebuild": Date } -type DateArbitraryConstraints = FastCheck.DateConstraints & { - readonly valid?: boolean | undefined -} - function dateArbitraryConstraints( - constraint: Annotations.ToArbitrary.GenerationConstraint | undefined, ordered: Annotations.ToArbitrary.OrderedConstraint | undefined, - base?: DateArbitraryConstraints | undefined, + base?: FastCheck.DateConstraints | undefined, toDate?: (value: T) => globalThis.Date ): FastCheck.DateConstraints { const out: FastCheck.DateConstraints = { ...base } - delete (out as any).valid - if (base?.valid || constraint?.valid) { - out.noInvalidDate = true - } if (ordered?.minimum !== undefined) { const minimum = toDate === undefined ? ordered.minimum as globalThis.Date : toDate(ordered.minimum) const nextMin = ordered.exclusiveMinimum ? new globalThis.Date(minimum.getTime() + 1) : minimum @@ -10452,49 +11832,48 @@ function dateArbitraryConstraints( return out } -const DateString = String.annotate({ expected: "a string in ISO 8601 format that will be decoded as a Date" }) +const DateString = String.annotate({ expected: "a string that will be decoded as a Date" }) /** - * Schema for JavaScript `Date` objects. + * Schema for valid JavaScript `Date` objects. * * **When to use** * - * Use to validate in-memory values that must already be JavaScript date + * Use to validate in-memory values that must already be valid JavaScript date * objects. * * **Details** * - * This schema accepts any `Date` instance, including invalid dates. The default - * JSON serializer encodes valid dates as ISO 8601 strings; invalid dates encode - * as `"Invalid Date"`. + * This schema accepts `Date` instances whose timestamp is not `NaN`. The + * default JSON serializer encodes dates as ISO 8601 strings. * * **Example** (Defining a Date schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * - * Schema.decodeUnknownSync(Schema.Date)(new Date("2024-01-01")) - * // => Date { 2024-01-01T00:00:00.000Z } + * const date = Schema.decodeUnknownSync(Schema.Date)(new Date("2024-01-01")) + * date.toISOString() // => "2024-01-01T00:00:00.000Z" * ``` * - * @see {@link DateValid} for accepting only valid Date instances * @see {@link DateFromString} for decoding strings into Date instances * @see {@link DateFromMillis} for decoding epoch milliseconds into Date instances * - * @category Date + * @category schemas * @since 4.0.0 */ -export const Date: Date = instanceOf( - globalThis.Date, +export const Date: Date = declare( + (input): input is globalThis.Date => input instanceof globalThis.Date && !globalThis.Number.isNaN(input.getTime()), { - typeConstructor: { - _tag: "Date" + representation: { + id: "effect/schema/Date", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.Date`, Type: `globalThis.Date` - }, - expected: "Date", + }), + expected: "a valid Date", toCodecJson: () => link()( DateString, @@ -10502,16 +11881,33 @@ export const Date: Date = instanceOf( ), toArbitrary: () => (fc, ctx) => fc.date(dateArbitraryConstraints( - ctx?.constraint, - ctx?.constraint?.ordered?.order === Order.Date ? ctx.constraint.ordered : undefined + ctx?.constraint?.ordered?.order === Order.Date ? ctx.constraint.ordered : undefined, + { noInvalidDate: true } )) } ) +/** + * Reviver for persisted `Date` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link Date} schema. + * + * @see {@link Date} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const DateReviver = makeFixedDeclarationReviver( + "effect/schema/Date", + Date +) + /** * Type-level representation of {@link DateFromString}. * - * @category Date + * @category models * @since 3.10.0 */ export interface DateFromString extends decodeTo { @@ -10532,19 +11928,15 @@ export interface DateFromString extends decodeTo { * The string is passed to JavaScript `Date` construction. * * Encoding: - * A valid `Date` is encoded as an ISO string; an invalid `Date` is encoded as - * `"Invalid Date"`. + * A `Date` is encoded as an ISO string. * - * **Gotchas** - * - * Invalid date strings can decode to invalid `Date` instances. + * Invalid date strings fail decoding. * * @see {@link DateFromMillis} for decoding epoch milliseconds into Date instances * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values * @see {@link Date} for accepting Date instances directly - * @see {@link DateValid} for rejecting invalid Date instances * - * @category Date + * @category schemas * @since 3.10.0 */ export const DateFromString: DateFromString = DateString.pipe(decodeTo(Date, SchemaTransformation.dateFromString)) @@ -10552,10 +11944,10 @@ export const DateFromString: DateFromString = DateString.pipe(decodeTo(Date, Sch /** * Type-level representation of {@link DateFromMillis}. * - * @category Date + * @category models * @since 4.0.0 */ -export interface DateFromMillis extends decodeTo { +export interface DateFromMillis extends decodeTo { readonly "Rebuild": DateFromMillis } @@ -10570,53 +11962,31 @@ export interface DateFromMillis extends decodeTo { * **Details** * * Decoding: - * A number of milliseconds since the Unix epoch is decoded as a `Date`. + * A safe integer number of milliseconds since the Unix epoch is decoded as a + * `Date`. * * Encoding: * A `Date` is encoded as its millisecond timestamp. * * **Gotchas** * - * This schema accepts any number, including `NaN`, `Infinity`, and `-Infinity`. - * Those values decode to invalid `Date` instances. + * JavaScript `Date` supports a narrower range than safe integers, so integers + * outside the supported `Date` range fail decoding. * * @see {@link DateFromString} for decoding string-encoded dates * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values * - * @category Date + * @category schemas * @since 4.0.0 */ -export const DateFromMillis: DateFromMillis = Number.pipe( +export const DateFromMillis: DateFromMillis = Int.pipe( decodeTo(Date, SchemaTransformation.dateFromMillis) ) -/** - * Type-level representation of {@link DateValid}. - * - * @category Date - * @since 4.0.0 - */ -export interface DateValid extends Date { - readonly "Rebuild": DateValid -} - -/** - * Schema for **valid** JavaScript `Date` objects. - * - * **Details** - * - * This schema accepts `Date` instances but rejects invalid dates (such as `new - * Date("invalid")`). - * - * @category Date - * @since 4.0.0 - */ -export const DateValid: DateValid = Date.check(isDateValid()) - /** * Type-level representation of {@link Duration}. * - * @category Duration + * @category models * @since 3.10.0 */ export interface Duration extends declare { @@ -10633,28 +12003,28 @@ export interface Duration extends declare { * * **Example** (Defining a Duration schema) * - * ```ts + * ```ts import.meta.vitest * import { Duration, Schema } from "effect" * - * Schema.decodeUnknownSync(Schema.Duration)(Duration.seconds(5)) - * // => Duration(5s) + * Schema.decodeUnknownSync(Schema.Duration)(Duration.seconds(5)) // => Duration.seconds(5) * ``` * - * @category Duration + * @category schemas * * @since 3.10.0 */ export const Duration: Duration = declare( Duration_.isDuration, { - typeConstructor: { - _tag: "effect/Duration" + representation: { + id: "effect/schema/Duration", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.Duration`, Type: `Duration.Duration`, - importDeclaration: `import * as Duration from "effect/Duration"` - }, + importDeclarations: [`import * as Duration from "effect/Duration"`] + }), expected: "Duration", toCodecJson: () => link()( @@ -10703,12 +12073,29 @@ export const Duration: Duration = declare( } ) +/** + * Reviver for persisted {@link Duration} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link Duration} schema. + * + * @see {@link Duration} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const DurationReviver = makeFixedDeclarationReviver( + "effect/schema/Duration", + Duration +) + const DurationString = String.annotate({ expected: "a string that will be decoded as a Duration" }) /** * Type-level representation of {@link DurationFromString}. * - * @category Duration + * @category models * @since 4.0.0 */ export interface DurationFromString extends decodeTo { @@ -10727,7 +12114,7 @@ export interface DurationFromString extends decodeTo { * Encoding: * - A `Duration` is encoded as a parseable `string`. * - * @category Duration + * @category schemas * @since 4.0.0 */ export const DurationFromString: DurationFromString = DurationString.pipe( @@ -10737,40 +12124,38 @@ export const DurationFromString: DurationFromString = DurationString.pipe( /** * Type-level representation of {@link DurationFromNanos}. * - * @category Duration + * @category models * @since 3.10.0 */ export interface DurationFromNanos extends decodeTo { readonly "Rebuild": DurationFromNanos } -const bigint0 = globalThis.BigInt(0) - /** - * Schema that decodes a non-negative `bigint` into a - * `Duration`, treating the bigint as nanoseconds. + * Schema that decodes a `bigint` into a `Duration`, treating the bigint as + * nanoseconds. * * **Details** * * Decoding: - * A non-negative `bigint` representing nanoseconds is decoded as a `Duration`. + * A `bigint` representing nanoseconds is decoded as a `Duration`. * * Encoding: - * Finite durations are encoded as a non-negative `bigint` number of nanoseconds. - * Encoding fails when the duration cannot be represented as nanoseconds, such as - * `Duration.infinity`. + * Finite durations are encoded as a `bigint` number of nanoseconds. Encoding + * fails when the duration cannot be represented as nanoseconds, such as + * `Duration.infinity` or `Duration.negativeInfinity`. * - * @category Duration + * @category schemas * @since 3.10.0 */ -export const DurationFromNanos: DurationFromNanos = BigInt.check(isGreaterThanOrEqualToBigInt(bigint0)).pipe( +export const DurationFromNanos: DurationFromNanos = BigInt.pipe( decodeTo(Duration, SchemaTransformation.durationFromNanos) ) /** * Type-level representation of {@link DurationFromMillis}. * - * @category Duration + * @category models * @since 3.10.0 */ export interface DurationFromMillis extends decodeTo { @@ -10778,31 +12163,32 @@ export interface DurationFromMillis extends decodeTo { } /** - * Schema that decodes a non-negative (possibly infinite) - * integer into a `Duration`, treating the integer value as the duration in + * Schema that decodes a number into a `Duration`, treating the number as * milliseconds. * * **Details** * * Decoding: - * - A non-negative (possibly infinite) integer representing milliseconds is - * decoded as a `Duration` + * - A finite or infinite number is decoded as a `Duration` * * Encoding: - * - A `Duration` is encoded to a non-negative (possibly infinite) integer - * representing milliseconds + * - A `Duration` is encoded to a finite or infinite number of milliseconds + * + * **Gotchas** + * + * `NaN` is decoded as `Duration.zero`, matching `Duration.millis`. * - * @category Duration + * @category schemas * @since 3.10.0 */ -export const DurationFromMillis: DurationFromMillis = Number.check(isGreaterThanOrEqualTo(0)).pipe( +export const DurationFromMillis: DurationFromMillis = Number.pipe( decodeTo(Duration, SchemaTransformation.durationFromMillis) ) /** * Type-level representation of {@link BigDecimal}. * - * @category BigDecimal + * @category models * @since 3.10.0 */ export interface BigDecimal extends declare { @@ -10902,20 +12288,21 @@ function bigDecimalScaleConstraints( * * @see {@link BigDecimalFromString} for parsing string input into a BigDecimal * - * @category BigDecimal + * @category schemas * @since 3.10.0 */ export const BigDecimal: BigDecimal = declare( BigDecimal_.isBigDecimal, { - typeConstructor: { - _tag: "effect/BigDecimal" + representation: { + id: "effect/schema/BigDecimal", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.BigDecimal`, Type: `BigDecimal.BigDecimal`, - importDeclaration: `import * as BigDecimal from "effect/BigDecimal"` - }, + importDeclarations: [`import * as BigDecimal from "effect/BigDecimal"`] + }), expected: "BigDecimal", toCodecJson: () => link()( @@ -10944,10 +12331,27 @@ export const BigDecimal: BigDecimal = declare( } ) +/** + * Reviver for persisted {@link BigDecimal} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link BigDecimal} schema. + * + * @see {@link BigDecimal} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const BigDecimalReviver = makeFixedDeclarationReviver( + "effect/schema/BigDecimal", + BigDecimal +) + /** * Type-level representation of {@link BigDecimalFromString}. * - * @category BigDecimal + * @category models * @since 4.0.0 */ export interface BigDecimalFromString extends decodeTo { @@ -10978,49 +12382,17 @@ export interface BigDecimalFromString extends decodeTo { * @see {@link BigIntFromString} for parsing base-10 integer strings into bigint values * @see {@link NumberFromString} for parsing JavaScript number strings * - * @category BigDecimal + * @category schemas * @since 4.0.0 */ export const BigDecimalFromString: BigDecimalFromString = BigDecimalString.pipe( decodeTo(BigDecimal, SchemaTransformation.bigDecimalFromString) ) -/** - * Type-level representation of {@link UnknownFromJsonString}. - * - * @category models - * @since 4.0.0 - */ -export interface UnknownFromJsonString extends fromJsonString { - readonly "Rebuild": UnknownFromJsonString -} - -/** - * Schema that decodes a JSON-encoded string into an `unknown` value. - * - * **Details** - * - * Decoding: - * - A `string` is decoded as an `unknown` value. - * - If the string is not valid JSON, decoding fails. - * - * Encoding: - * - Any value is encoded as a JSON string using `JSON.stringify`. - * - If the value is not a valid JSON value, encoding fails. - * - * **Example** (Decoding unknown JSON strings) - * - * ```ts - * import { Schema } from "effect" - * - * Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(`{"a":1,"b":2}`) - * // => { a: 1, b: 2 } - * ``` - * - * @category schemas - * @since 4.0.0 - */ -export const UnknownFromJsonString: UnknownFromJsonString = fromJsonString(Unknown) +const JsonString = String.annotate({ + expected: "a string that will be decoded as JSON", + contentMediaType: "application/json" +}) /** * Type-level representation returned by {@link fromJsonString}. @@ -11041,78 +12413,44 @@ export interface fromJsonString extends decodeTo { a: 1 } - * ``` - * - * **Example** (Emitting JSON Schema for a JSON string decoder) + * const schemaFromJsonString = Schema.fromJsonString(schema, { space: 2 }) * - * ```ts - * import { Schema } from "effect" - * - * const original = Schema.Struct({ a: Schema.String }) - * const schema = Schema.fromJsonString(original) - * - * const document = Schema.toJsonSchemaDocument(schema) - * - * console.log(JSON.stringify(document, null, 2)) - * // { - * // "source": "draft-2020-12", - * // "schema": { - * // "type": "string", - * // "contentMediaType": "application/json", - * // "contentSchema": { - * // "type": "object", - * // "properties": { - * // "a": { - * // "type": "string" - * // } - * // }, - * // "required": [ - * // "a" - * // ], - * // "additionalProperties": false - * // } - * // }, - * // "definitions": {} - * // } + * Schema.encodeSync(schemaFromJsonString)({ a: 1 }) // => "{\n \"a\": 1\n}" * ``` * - * @category constructors + * @category schemas * @since 4.0.0 */ -export function fromJsonString(schema: S): fromJsonString { - const identifier = SchemaAST.resolveIdentifier(schema.ast) - return String.annotate({ - // Give the transport wrapper its own name so the decoded payload keeps its identifier. - identifier: identifier === undefined ? undefined : `${identifier}JsonString`, - expected: "a string that will be decoded as JSON", - contentMediaType: "application/json", - contentSchema: SchemaAST.toEncoded(schema.ast) - }).pipe(decodeTo(schema, SchemaTransformation.fromJsonString)) +export function fromJsonString( + schema: S, + options?: { + readonly reviver?: Parameters[1] | undefined + readonly replacer?: SchemaGetter.JsonReplacer | undefined + readonly space?: Parameters[2] | undefined + } +): fromJsonString { + return JsonString.pipe(decodeTo(schema, SchemaTransformation.fromJsonString(options))) } +/** @internal */ +export const UnknownFromJsonString: fromJsonString = fromJsonString(Unknown) + /** * Type-level representation of {@link File}. * - * @category file + * @category models * @since 4.0.0 */ export interface File extends instanceOf { @@ -11127,17 +12465,18 @@ export interface File extends instanceOf { * The default JSON serializer encodes a `File` as `{ data, type, name, lastModified }` * where `data` is base64-encoded. * - * @category file + * @category schemas * @since 4.0.0 */ export const File: File = instanceOf(globalThis.File, { - typeConstructor: { - _tag: "File" + representation: { + id: "effect/schema/File", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.File`, Type: `globalThis.File` - }, + }), expected: "File", toCodecJson: () => link()( @@ -11145,16 +12484,18 @@ export const File: File = instanceOf(globalThis.File, { data: String.check(isBase64()), type: String, name: String, - lastModified: Number + lastModified: Int }), SchemaTransformation.transformOrFail({ - decode: (e) => + decode: (e, options) => Result_.match(Encoding.decodeBase64(e.data), { - onFailure: (error) => + onFailure: () => Effect.fail( - new SchemaIssue.InvalidValue(Option_.some(e.data), { - message: error.message - }) + new SchemaIssue.InvalidValue( + { expected: "a valid Base64 string" }, + e.data, + options + ) ), onSuccess: (bytes) => { const buffer = new globalThis.Uint8Array(bytes) @@ -11163,7 +12504,7 @@ export const File: File = instanceOf(globalThis.File, { ) } }), - encode: (file) => + encode: (file, options) => Effect.tryPromise({ try: async () => { const bytes = new globalThis.Uint8Array(await file.arrayBuffer()) @@ -11174,19 +12515,38 @@ export const File: File = instanceOf(globalThis.File, { lastModified: file.lastModified } }, - catch: (e) => - new SchemaIssue.InvalidValue(Option_.some(file), { - message: globalThis.String(e) - }) + catch: () => + new SchemaIssue.InvalidValue( + { expected: "a readable File" }, + file, + options + ) }) }) ) }) +/** + * Reviver for persisted `File` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link File} schema. + * + * @see {@link File} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const FileReviver = makeFixedDeclarationReviver( + "effect/schema/File", + File +) + /** * Type-level representation of {@link FormData}. * - * @category FormData + * @category models * @since 4.0.0 */ export interface FormData extends instanceOf { @@ -11201,17 +12561,18 @@ export interface FormData extends instanceOf { * The default JSON serializer encodes a `FormData` as an array of `[key, entry]` * pairs where each entry is tagged as `"String"` or `"File"`. * - * @category FormData + * @category schemas * @since 4.0.0 */ export const FormData: FormData = instanceOf(globalThis.FormData, { - typeConstructor: { - _tag: "FormData" + representation: { + id: "effect/schema/FormData", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.FormData`, Type: `globalThis.FormData` - }, + }), expected: "FormData", toCodecJson: () => link()( @@ -11247,10 +12608,27 @@ export const FormData: FormData = instanceOf(globalThis.FormData, { ) }) +/** + * Reviver for persisted `FormData` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link FormData} schema. + * + * @see {@link FormData} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const FormDataReviver = makeFixedDeclarationReviver( + "effect/schema/FormData", + FormData +) + /** * Type-level representation returned by {@link fromFormData}. * - * @category FormData + * @category models * @since 4.0.0 */ export interface fromFormData extends decodeTo { @@ -11278,7 +12656,7 @@ export interface fromFormData extends decodeTo extends decodeTo { a: "1" } * ``` * * **Example** (Decoding nested fields) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.fromFormData( @@ -11315,13 +12692,12 @@ export interface fromFormData extends decodeTo { a: "1", b: { c: "2", d: "3" } } * ``` * * **Example** (Parsing non-string values) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.fromFormData( @@ -11335,8 +12711,7 @@ export interface fromFormData extends decodeTo { a: 1 } * ``` * * @category decoding @@ -11349,7 +12724,7 @@ export function fromFormData(schema: S): fromFormData { /** * Type-level representation of {@link URLSearchParams}. * - * @category search params + * @category models * @since 4.0.0 */ export interface URLSearchParams extends instanceOf { @@ -11363,17 +12738,18 @@ export interface URLSearchParams extends instanceOf * * The default JSON serializer encodes a `URLSearchParams` as a query string. * - * @category search params + * @category schemas * @since 4.0.0 */ export const URLSearchParams: URLSearchParams = instanceOf(globalThis.URLSearchParams, { - typeConstructor: { - _tag: "URLSearchParams" + representation: { + id: "effect/schema/URLSearchParams", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.URLSearchParams`, Type: `globalThis.URLSearchParams` - }, + }), expected: "URLSearchParams", toCodecJson: () => link()( @@ -11385,10 +12761,27 @@ export const URLSearchParams: URLSearchParams = instanceOf(globalThis.URLSearchP ) }) +/** + * Reviver for persisted `URLSearchParams` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link URLSearchParams} schema. + * + * @see {@link URLSearchParams} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const URLSearchParamsReviver = makeFixedDeclarationReviver( + "effect/schema/URLSearchParams", + URLSearchParams +) + /** * Type-level representation returned by {@link fromURLSearchParams}. * - * @category search params + * @category models * @since 4.0.0 */ export interface fromURLSearchParams extends decodeTo { @@ -11417,7 +12810,7 @@ export interface fromURLSearchParams extends decodeTo extends decodeTo { a: "1" } * ``` * * **Example** (Decoding nested fields) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.fromURLSearchParams( @@ -11449,13 +12841,12 @@ export interface fromURLSearchParams extends decodeTo { a: "1", b: { c: "2", d: "3" } } * ``` * * **Example** (Parsing non-string values) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.fromURLSearchParams( @@ -11468,8 +12859,7 @@ export interface fromURLSearchParams extends decodeTo { a: 1 } * ``` * * @category decoding @@ -11479,49 +12869,13 @@ export function fromURLSearchParams(schema: S): fromURLSea return URLSearchParams.pipe(decodeTo(schema, SchemaTransformation.fromURLSearchParams)) } -/** - * Type-level representation of {@link Finite}. - * - * @category Number - * @since 3.10.0 - */ -export interface Finite extends Number { - readonly "Rebuild": Finite -} - -/** - * Schema for finite numbers, rejecting `NaN`, `Infinity`, and `-Infinity`. - * - * @category Number - * @since 3.10.0 - */ -export const Finite: Finite = Number.check(isFinite()) - -/** - * Type-level representation of {@link Int}. - * - * @category Number - * @since 3.10.0 - */ -export interface Int extends Number { - readonly "Rebuild": Int -} - -/** - * Schema for integers, rejecting `NaN`, `Infinity`, and `-Infinity`. - * - * @category Number - * @since 3.10.0 - */ -export const Int: Int = Number.check(isInt()) - /** * Type-level representation of {@link NumberFromString}. * - * @category Number + * @category models * @since 3.10.0 */ -export interface NumberFromString extends decodeTo { +export interface NumberFromString extends decodeTo { readonly "Rebuild": NumberFromString } @@ -11539,7 +12893,7 @@ export interface NumberFromString extends decodeTo { * Encoding: * A number is encoded as a `string`. * - * @category Number + * @category schemas * @since 3.10.0 */ export const NumberFromString: NumberFromString = String.annotate({ @@ -11549,7 +12903,7 @@ export const NumberFromString: NumberFromString = String.annotate({ /** * Type-level representation of {@link FiniteFromString}. * - * @category Number + * @category models * @since 4.0.0 */ export interface FiniteFromString extends decodeTo { @@ -11568,7 +12922,7 @@ export interface FiniteFromString extends decodeTo { * Encoding: * - A finite number is encoded as a `string`. * - * @category Number + * @category schemas * @since 4.0.0 */ export const FiniteFromString: FiniteFromString = String.annotate({ @@ -11578,7 +12932,7 @@ export const FiniteFromString: FiniteFromString = String.annotate({ /** * Type-level representation of {@link BigIntFromString}. * - * @category BigInt + * @category models * @since 4.0.0 */ export interface BigIntFromString extends decodeTo { @@ -11610,7 +12964,7 @@ export interface BigIntFromString extends decodeTo { * @see {@link NumberFromString} for parsing JavaScript number strings, including non-finite values * @see {@link BigDecimalFromString} for parsing decimal number strings * - * @category BigInt + * @category schemas * @since 4.0.0 */ export const BigIntFromString: BigIntFromString = make(SchemaAST.bigIntString).pipe( @@ -11620,7 +12974,7 @@ export const BigIntFromString: BigIntFromString = make(SchemaAST.bigIntS /** * Type-level representation of {@link Trimmed}. * - * @category string + * @category models * @since 3.10.0 */ export interface Trimmed extends String { @@ -11630,7 +12984,7 @@ export interface Trimmed extends String { /** * Schema for strings that contains no leading or trailing whitespaces. * - * @category string + * @category schemas * @since 3.10.0 */ export const Trimmed: Trimmed = String.check(isTrimmed()) @@ -11638,7 +12992,7 @@ export const Trimmed: Trimmed = String.check(isTrimmed()) /** * Type-level representation of {@link Trim}. * - * @category string + * @category models * @since 3.10.0 */ export interface Trim extends decodeTo { @@ -11656,7 +13010,7 @@ export interface Trim extends decodeTo { * Encoding: * - The trimmed string is encoded as is. * - * @category string + * @category schemas * @since 3.10.0 */ export const Trim: Trim = String.annotate({ @@ -11666,7 +13020,7 @@ export const Trim: Trim = String.annotate({ /** * Type-level representation of {@link StringFromBase64}. * - * @category string + * @category models * @since 3.10.0 */ export interface StringFromBase64 extends decodeTo { @@ -11684,7 +13038,7 @@ export interface StringFromBase64 extends decodeTo { * Encoding: * - A `string` is encoded as a base64-encoded string. * - * @category string + * @category schemas * @since 3.10.0 */ export const StringFromBase64: StringFromBase64 = String.annotate({ @@ -11696,7 +13050,7 @@ export const StringFromBase64: StringFromBase64 = String.annotate({ /** * Type-level representation of {@link StringFromBase64Url}. * - * @category string + * @category models * @since 3.10.0 */ export interface StringFromBase64Url extends decodeTo { @@ -11714,7 +13068,7 @@ export interface StringFromBase64Url extends decodeTo { * Encoding: * - A `string` is encoded as a base64 (URL) encoded string. * - * @category string + * @category schemas * @since 3.10.0 */ export const StringFromBase64Url: StringFromBase64Url = String.annotate({ @@ -11726,7 +13080,7 @@ export const StringFromBase64Url: StringFromBase64Url = String.annotate({ /** * Type-level representation of {@link StringFromHex}. * - * @category string + * @category models * @since 3.10.0 */ export interface StringFromHex extends decodeTo { @@ -11744,7 +13098,7 @@ export interface StringFromHex extends decodeTo { * Encoding: * - A `string` is encoded as a hex string. * - * @category string + * @category schemas * @since 3.10.0 */ export const StringFromHex: StringFromHex = String.annotate({ @@ -11756,7 +13110,7 @@ export const StringFromHex: StringFromHex = String.annotate({ /** * Type-level representation of {@link StringFromUriComponent}. * - * @category string + * @category models * @since 3.12.0 */ export interface StringFromUriComponent extends decodeTo { @@ -11777,7 +13131,7 @@ export interface StringFromUriComponent extends decodeTo { * * **Example** (Decoding URI component strings) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const PaginationSchema = Schema.Struct({ @@ -11789,11 +13143,10 @@ export interface StringFromUriComponent extends decodeTo { * Schema.decodeTo(Schema.fromJsonString(PaginationSchema)) * ) * - * console.log(Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 })) - * // %7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D + * Schema.encodeSync(UrlSchema)({ maxItemPerPage: 10, page: 1 }) // => "%7B%22maxItemPerPage%22%3A10%2C%22page%22%3A1%7D" * ``` * - * @category string + * @category schemas * @since 3.12.0 */ export const StringFromUriComponent: StringFromUriComponent = String.annotate({ @@ -11806,7 +13159,7 @@ export const StringFromUriComponent: StringFromUriComponent = String.annotate({ * Schema for property keys accepted by Effect schemas: finite `number`, * `symbol`, or `string`. * - * @category PropertyKey + * @category schemas * @since 4.0.0 */ export const PropertyKey = Union([Finite, Symbol, String]) @@ -11819,7 +13172,7 @@ export const PropertyKey = Union([Finite, Symbol, String]) * The result contains an `issues` array where each issue has a message and an * optional path made of property keys or keyed path segments. * - * @category Standard Schema + * @category schemas * @since 4.0.0 */ export const StandardSchemaV1FailureResult = Struct({ @@ -11832,7 +13185,7 @@ export const StandardSchemaV1FailureResult = Struct({ /** * Type-level representation of {@link BooleanFromBit}. * - * @category boolean + * @category models * @since 4.0.0 */ export interface BooleanFromBit extends decodeTo> { @@ -11855,7 +13208,7 @@ export interface BooleanFromBit extends decodeTo> { @@ -11893,17 +13246,18 @@ const Base64String = String.annotate({ * * The default JSON serializer encodes Uint8Array as a Base64 encoded string. * - * @category Uint8Array + * @category schemas * @since 4.0.0 */ export const Uint8Array: Uint8Array = instanceOf(globalThis.Uint8Array, { - typeConstructor: { - _tag: "Uint8Array" + representation: { + id: "effect/schema/Uint8Array", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.Uint8Array`, Type: `globalThis.Uint8Array` - }, + }), expected: "Uint8Array", toCodecJson: () => link>()( @@ -11913,10 +13267,27 @@ export const Uint8Array: Uint8Array = instanceOf(globalThis.Uint8Array (fc) => fc.uint8Array() }) +/** + * Reviver for persisted `Uint8Array` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link Uint8Array} schema. + * + * @see {@link Uint8Array} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const Uint8ArrayReviver = makeFixedDeclarationReviver( + "effect/schema/Uint8Array", + Uint8Array +) + /** * Type-level representation of {@link Uint8ArrayFromBase64}. * - * @category Uint8Array + * @category models * @since 3.10.0 */ export interface Uint8ArrayFromBase64 extends decodeTo { @@ -11935,7 +13306,7 @@ export interface Uint8ArrayFromBase64 extends decodeTo { * Encoding: * - A `Uint8Array` is encoded as a base64-encoded string. * - * @category Uint8Array + * @category schemas * @since 3.10.0 */ export const Uint8ArrayFromBase64: Uint8ArrayFromBase64 = Base64String.pipe( @@ -11945,7 +13316,7 @@ export const Uint8ArrayFromBase64: Uint8ArrayFromBase64 = Base64String.pipe( /** * Type-level representation of {@link Uint8ArrayFromBase64Url}. * - * @category Uint8Array + * @category models * @since 3.10.0 */ export interface Uint8ArrayFromBase64Url extends decodeTo { @@ -11964,7 +13335,7 @@ export interface Uint8ArrayFromBase64Url extends decodeTo { * Encoding: * - A `Uint8Array` is encoded as a base64 (URL) encoded string. * - * @category Uint8Array + * @category schemas * @since 3.10.0 */ export const Uint8ArrayFromBase64Url: Uint8ArrayFromBase64Url = String.annotate({ @@ -11979,7 +13350,7 @@ export const Uint8ArrayFromBase64Url: Uint8ArrayFromBase64Url = String.annotate( /** * Type-level representation of {@link Uint8ArrayFromHex}. * - * @category Uint8Array + * @category models * @since 3.10.0 */ export interface Uint8ArrayFromHex extends decodeTo { @@ -11998,7 +13369,7 @@ export interface Uint8ArrayFromHex extends decodeTo { * Encoding: * - A `Uint8Array` is encoded as a hex encoded string. * - * @category Uint8Array + * @category schemas * @since 3.10.0 */ export const Uint8ArrayFromHex: Uint8ArrayFromHex = String.annotate({ @@ -12013,7 +13384,7 @@ export const Uint8ArrayFromHex: Uint8ArrayFromHex = String.annotate({ /** * Type-level representation of {@link DateTimeUtc}. * - * @category DateTime + * @category models * @since 3.10.0 */ export interface DateTimeUtc extends declare { @@ -12038,20 +13409,21 @@ export interface DateTimeUtc extends declare { * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values * @see {@link DateTimeZoned} for preserving zoned DateTime values * - * @category DateTime + * @category schemas * @since 3.10.0 */ export const DateTimeUtc: DateTimeUtc = declare( (u) => DateTime.isDateTime(u) && DateTime.isUtc(u), { - typeConstructor: { - _tag: "effect/DateTime.Utc" + representation: { + id: "effect/schema/DateTimeUtc", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.DateTimeUtc`, Type: `DateTime.Utc`, - importDeclaration: `import * as DateTime from "effect/DateTime"` - }, + importDeclarations: [`import * as DateTime from "effect/DateTime"`] + }), expected: "DateTime.Utc", toCodecJson: () => link()( @@ -12060,9 +13432,8 @@ export const DateTimeUtc: DateTimeUtc = declare( ), toArbitrary: () => (fc, ctx) => fc.date(dateArbitraryConstraints( - ctx?.constraint, ctx?.constraint?.ordered?.order === DateTime.Order ? ctx.constraint.ordered : undefined, - { valid: true }, + { noInvalidDate: true }, DateTime.toDateUtc )) .map((date) => DateTime.fromDateUnsafe(date)), @@ -12071,10 +13442,27 @@ export const DateTimeUtc: DateTimeUtc = declare( } ) +/** + * Reviver for persisted {@link DateTimeUtc} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link DateTimeUtc} schema. + * + * @see {@link DateTimeUtc} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const DateTimeUtcReviver = makeFixedDeclarationReviver( + "effect/schema/DateTimeUtc", + DateTimeUtc +) + /** * Type-level representation of {@link DateTimeUtcFromDate}. * - * @category DateTime + * @category models * @since 3.12.0 */ export interface DateTimeUtcFromDate extends decodeTo { @@ -12100,12 +13488,12 @@ export interface DateTimeUtcFromDate extends decodeTo { * @see {@link DateTimeUtc} for validating values that are already `DateTime.Utc` * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values - * @see {@link DateValid} for validating Date instances without converting them + * @see {@link Date} for validating Date instances without converting them * - * @category DateTime + * @category schemas * @since 3.12.0 */ -export const DateTimeUtcFromDate: DateTimeUtcFromDate = DateValid.pipe( +export const DateTimeUtcFromDate: DateTimeUtcFromDate = Date.pipe( decodeTo(DateTimeUtc, { decode: SchemaGetter.dateTimeUtcFromInput(), encode: SchemaGetter.transform(DateTime.toDateUtc) @@ -12115,7 +13503,7 @@ export const DateTimeUtcFromDate: DateTimeUtcFromDate = DateValid.pipe( /** * Type-level representation of {@link DateTimeUtcFromString}. * - * @category DateTime + * @category models * @since 4.0.0 */ export interface DateTimeUtcFromString extends decodeTo { @@ -12140,7 +13528,7 @@ export interface DateTimeUtcFromString extends decodeTo { * @see {@link DateTimeUtcFromMillis} for decoding epoch milliseconds into UTC values * @see {@link DateFromString} for decoding strings into JavaScript Date instances * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeUtcFromString: DateTimeUtcFromString = String.annotate({ @@ -12155,10 +13543,10 @@ export const DateTimeUtcFromString: DateTimeUtcFromString = String.annotate({ /** * Type-level representation of {@link DateTimeUtcFromMillis}. * - * @category DateTime + * @category models * @since 4.0.0 */ -export interface DateTimeUtcFromMillis extends decodeTo, Number> { +export interface DateTimeUtcFromMillis extends decodeTo, Int> { readonly "Rebuild": DateTimeUtcFromMillis } @@ -12177,10 +13565,10 @@ export interface DateTimeUtcFromMillis extends decodeTo * @see {@link DateTimeUtcFromString} for decoding date-time strings into UTC values * @see {@link DateFromMillis} for decoding epoch milliseconds into JavaScript Date instances * - * @category DateTime + * @category schemas * @since 4.0.0 */ -export const DateTimeUtcFromMillis: DateTimeUtcFromMillis = Number.pipe( +export const DateTimeUtcFromMillis: DateTimeUtcFromMillis = Int.pipe( decodeTo(DateTimeUtc, { decode: SchemaGetter.dateTimeUtcFromInput(), encode: SchemaGetter.transform(DateTime.toEpochMillis) @@ -12190,7 +13578,7 @@ export const DateTimeUtcFromMillis: DateTimeUtcFromMillis = Number.pipe( /** * Type-level representation of {@link TimeZoneOffset}. * - * @category DateTime + * @category models * @since 3.10.0 */ export interface TimeZoneOffset extends declare { @@ -12206,24 +13594,25 @@ export interface TimeZoneOffset extends declare { * * - encodes `DateTime.TimeZone.Offset` as a number (offset in milliseconds) * - * @category DateTime + * @category schemas * @since 3.10.0 */ export const TimeZoneOffset: TimeZoneOffset = declare( DateTime.isTimeZoneOffset, { - typeConstructor: { - _tag: "effect/DateTime.TimeZone.Offset" + representation: { + id: "effect/schema/TimeZoneOffset", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.TimeZoneOffset`, Type: `DateTime.TimeZone.Offset`, - importDeclaration: `import * as DateTime from "effect/DateTime"` - }, + importDeclarations: [`import * as DateTime from "effect/DateTime"`] + }), expected: "DateTime.TimeZone.Offset", toCodecJson: () => link()( - Number, + Int, SchemaTransformation.timeZoneOffsetFromNumber ), toArbitrary: () => (fc) => @@ -12233,10 +13622,27 @@ export const TimeZoneOffset: TimeZoneOffset = declare( } ) +/** + * Reviver for persisted {@link TimeZoneOffset} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link TimeZoneOffset} schema. + * + * @see {@link TimeZoneOffset} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const TimeZoneOffsetReviver = makeFixedDeclarationReviver( + "effect/schema/TimeZoneOffset", + TimeZoneOffset +) + /** * Type-level representation of {@link TimeZoneNamed}. * - * @category DateTime + * @category models * @since 3.10.0 */ export interface TimeZoneNamed extends declare { @@ -12254,20 +13660,21 @@ const TimeZoneNamedString = String.annotate({ expected: "an IANA time zone ident * * - encodes `DateTime.TimeZone.Named` as a string (IANA time zone identifier) * - * @category DateTime + * @category schemas * @since 3.10.0 */ export const TimeZoneNamed: TimeZoneNamed = declare( DateTime.isTimeZoneNamed, { - typeConstructor: { - _tag: "effect/DateTime.TimeZone.Named" + representation: { + id: "effect/schema/TimeZoneNamed", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.TimeZoneNamed`, Type: `DateTime.TimeZone.Named`, - importDeclaration: `import * as DateTime from "effect/DateTime"` - }, + importDeclarations: [`import * as DateTime from "effect/DateTime"`] + }), expected: "DateTime.TimeZone.Named", toCodecJson: () => link()( @@ -12285,10 +13692,27 @@ export const TimeZoneNamed: TimeZoneNamed = declare( } ) +/** + * Reviver for persisted {@link TimeZoneNamed} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link TimeZoneNamed} schema. + * + * @see {@link TimeZoneNamed} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const TimeZoneNamedReviver = makeFixedDeclarationReviver( + "effect/schema/TimeZoneNamed", + TimeZoneNamed +) + /** * Type-level representation of {@link TimeZoneNamedFromString}. * - * @category DateTime + * @category models * @since 4.0.0 */ export interface TimeZoneNamedFromString extends decodeTo { @@ -12306,7 +13730,7 @@ export interface TimeZoneNamedFromString extends decodeTo * Encoding: * - A `DateTime.TimeZone.Named` is encoded as a `string`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const TimeZoneNamedFromString: TimeZoneNamedFromString = TimeZoneNamedString.pipe( @@ -12316,7 +13740,7 @@ export const TimeZoneNamedFromString: TimeZoneNamedFromString = TimeZoneNamedStr /** * Type-level representation of {@link TimeZone}. * - * @category DateTime + * @category models * @since 3.10.0 */ export interface TimeZone extends declare { @@ -12337,20 +13761,21 @@ const TimeZoneString = String.annotate({ * - encodes `DateTime.TimeZone` as a string (IANA identifier or offset like * `+03:00`) * - * @category DateTime + * @category schemas * @since 3.10.0 */ export const TimeZone: TimeZone = declare( DateTime.isTimeZone, { - typeConstructor: { - _tag: "effect/DateTime.TimeZone" + representation: { + id: "effect/schema/TimeZone", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.TimeZone`, Type: `DateTime.TimeZone`, - importDeclaration: `import * as DateTime from "effect/DateTime"` - }, + importDeclarations: [`import * as DateTime from "effect/DateTime"`] + }), expected: "DateTime.TimeZone", toCodecJson: () => link()( @@ -12371,10 +13796,27 @@ export const TimeZone: TimeZone = declare( } ) +/** + * Reviver for persisted {@link TimeZone} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link TimeZone} schema. + * + * @see {@link TimeZone} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const TimeZoneReviver = makeFixedDeclarationReviver( + "effect/schema/TimeZone", + TimeZone +) + /** * Type-level representation of {@link TimeZoneFromString}. * - * @category DateTime + * @category models * @since 4.0.0 */ export interface TimeZoneFromString extends decodeTo { @@ -12392,7 +13834,7 @@ export interface TimeZoneFromString extends decodeTo { * Encoding: * - A `DateTime.TimeZone` is encoded as a `string`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const TimeZoneFromString: TimeZoneFromString = TimeZoneString.pipe( @@ -12402,7 +13844,7 @@ export const TimeZoneFromString: TimeZoneFromString = TimeZoneString.pipe( /** * Type-level representation of {@link DateTimeZoned}. * - * @category DateTime + * @category models * @since 3.10.0 */ export interface DateTimeZoned extends declare { @@ -12425,20 +13867,21 @@ const DateTimeZonedString = String.annotate({ * - encodes named zones by appending the IANA identifier in brackets, such as * `YYYY-MM-DDTHH:mm:ss.sss+HH:MM[Time/Zone]` * - * @category DateTime + * @category schemas * @since 3.10.0 */ export const DateTimeZoned: DateTimeZoned = declare( (u) => DateTime.isDateTime(u) && DateTime.isZoned(u), { - typeConstructor: { - _tag: "effect/DateTime.Zoned" + representation: { + id: "effect/schema/DateTimeZoned", + payload: null }, - generation: { + toCode: () => ({ runtime: `Schema.DateTimeZoned`, Type: `DateTime.Zoned`, - importDeclaration: `import * as DateTime from "effect/DateTime"` - }, + importDeclarations: [`import * as DateTime from "effect/DateTime"`] + }), expected: "DateTime.Zoned", toCodecJson: () => link()( @@ -12448,12 +13891,11 @@ export const DateTimeZoned: DateTimeZoned = declare( toArbitrary: () => (fc, ctx) => fc.tuple( fc.date(dateArbitraryConstraints( - ctx?.constraint, ctx?.constraint?.ordered?.order === DateTime.Order ? ctx.constraint.ordered : undefined, { max: new globalThis.Date(8640000000000000 - 14 * 60 * 60 * 1000), min: new globalThis.Date(-8640000000000000 + 14 * 60 * 60 * 1000), - valid: true + noInvalidDate: true }, DateTime.toDateUtc )), @@ -12464,10 +13906,27 @@ export const DateTimeZoned: DateTimeZoned = declare( } ) +/** + * Reviver for persisted {@link DateTimeZoned} declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link DateTimeZoned} schema. + * + * @see {@link DateTimeZoned} for the corresponding schema + * + * @category schemas + * @since 4.0.0 + */ +export const DateTimeZonedReviver = makeFixedDeclarationReviver( + "effect/schema/DateTimeZoned", + DateTimeZoned +) + /** * Type-level representation of {@link DateTimeZonedFromString}. * - * @category DateTime + * @category models * @since 4.0.0 */ export interface DateTimeZonedFromString extends decodeTo { @@ -12485,7 +13944,7 @@ export interface DateTimeZonedFromString extends decodeTo * Encoding: * - A `DateTime.Zoned` is encoded as a `string`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeZonedFromString: DateTimeZonedFromString = DateTimeZonedString.pipe( @@ -12502,17 +13961,18 @@ export const DateTimeZonedFromString: DateTimeZonedFromString = DateTimeZonedStr * @category models * @since 3.10.0 */ -export interface Class extends - BottomLazy< - SchemaAST.Declaration, - decodeTo, S>, - readonly [S], - S["~type.mutability"], - S["~type.optionality"], - S["~type.constructor.default"], - S["~encoded.mutability"], - S["~encoded.optionality"] - > +export interface Class + extends + BottomLazyWithoutNew< + SchemaAST.Declaration, + decodeTo, S>, + readonly [S], + S["~type.mutability"], + S["~type.optionality"], + S["~type.constructor.default"], + S["~encoded.mutability"], + S["~encoded.optionality"] + > { readonly "Type": Self readonly "Encoded": S["Encoded"] @@ -12654,7 +14114,7 @@ function makeClass< static makeOption(input: S["~type.make.in"], options?: MakeOptions): Option_.Option { return SchemaParser.makeOption(getClassSchema(this) as any)(input ?? {}, options) as any } - static makeEffect(input: S["~type.make.in"], options?: MakeOptions): Effect.Effect { + static makeEffect(input: S["~type.make.in"], options?: MakeOptions): Effect.Effect { return (getClassSchema(this) as any).makeEffect(input ?? {}, options) } static annotate(annotations: Annotations.Declaration) { @@ -12704,7 +14164,14 @@ function makeClass< function getClassTransformation(self: new(...args: ReadonlyArray) => any) { return new SchemaTransformation.Transformation( - SchemaGetter.transform((input) => new self(input)), + SchemaGetter.transform((input) => + new self(input, { + "~payload": { + token: payloadToken, + value: input + } + }) + ), SchemaGetter.passthrough() ) } @@ -12725,19 +14192,26 @@ function getClassSchemaFactory( if (memo !== undefined) { return memo } + const ClassTypeId = getClassTypeId(identifier) + const isClassValue: Predicate.Predicate = (input) => + input instanceof self || Predicate.hasProperty(input, ClassTypeId) const transformation = getClassTransformation(self) const to = make>( new SchemaAST.Declaration( [from.ast], - () => (input, ast) => { - return input instanceof self || - Predicate.hasProperty(input, getClassTypeId(identifier)) ? + () => (input, ast, options) => { + return isClassValue(input) ? Effect.succeed(input) : - Effect.fail(new SchemaIssue.InvalidType(ast, Option_.some(input))) + Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) }, { identifier, - [SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation), + [InternalAnnotations.CONSTRUCTOR_ANNOTATION_KEY]: ( + [from]: readonly [SchemaAST.AST] + ): SchemaAST.ConstructorDescriptor => ({ + isConstructed: isClassValue, + link: new SchemaAST.Link(from, transformation) + }), toCodec: ([from]: readonly [ConstraintCodec]) => new SchemaAST.Link(from.ast, transformation), toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter]) => () => ({ @@ -12745,7 +14219,7 @@ function getClassSchemaFactory( terminal: from.terminal?.map((args: S["Type"]) => new self(args)) }), toFormatter: ([from]: readonly [Formatter]) => (t: Self) => `${self.identifier}(${from(t)})`, - "~sentinels": SchemaAST.collectSentinels(from.ast), + [InternalAnnotations.SENTINELS_ANNOTATION_KEY]: SchemaAST.collectSentinels(from.ast), ...annotations } ) @@ -12763,8 +14237,8 @@ type MissingSelfGeneric = /** * Creates a schema-backed class whose constructor validates input against a - * {@link Struct} schema. Construction throws a {@link SchemaError} on invalid - * input. + * {@link Struct} schema. Construction throws an `Error` with a + * `SchemaIssue.Issue` in its `cause` on invalid input. * * **When to use** * @@ -12776,13 +14250,23 @@ type MissingSelfGeneric = * Pass the desired class type as the first type parameter. The second optional * type parameter can be used to add nominal brands. * + * The `identifier` is the schema's stable runtime name. It is exposed on the + * class, stored in the schema AST, and used to label diagnostics and generated + * references as well as to format class instances. + * + * It also derives a runtime marker that recognizes instances across hot module + * reloads, where `instanceof` can fail because the constructor has been + * replaced. The identifier is explicit because the outer JavaScript class name + * is not available while the `extends` expression is evaluated and may change + * through renaming or minification. + * * **Gotchas** * * Passing `disableChecks` in the options skips constructor validation. * * **Example** (Defining a basic class) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * class Person extends Schema.Class("Person")({ @@ -12791,13 +14275,13 @@ type MissingSelfGeneric = * }) {} * * const alice = new Person({ name: "Alice", age: 30 }) - * console.log(alice.name) // "Alice" - * console.log(`${alice}`) // "Person({ name: Alice, age: 30 })" + * alice.name // => "Alice" + * String(alice) // => "Person({\"name\":\"Alice\",\"age\":30})" * ``` * * **Example** (Extending a class) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * class Animal extends Schema.Class("Animal")({ @@ -12809,13 +14293,13 @@ type MissingSelfGeneric = * }) {} * * const dog = new Dog({ name: "Rex", breed: "Labrador" }) - * console.log(dog.name) // "Rex" - * console.log(dog.breed) // "Labrador" + * dog.name // => "Rex" + * dog.breed // => "Labrador" * ``` * * @see {@link TaggedClass} for adding a `_tag` literal field to the class schema - * @see {@link ErrorClass} for defining schema-backed error classes - * @see {@link TaggedErrorClass} for defining tagged schema-backed error classes + * @see {@link Error} for defining schema-backed error classes + * @see {@link TaggedError} for defining tagged schema-backed error classes * * @category constructors * @since 3.10.0 @@ -12865,7 +14349,7 @@ export const Class: { * * **Example** (Defining a tagged class) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * class Circle extends Schema.TaggedClass()("Circle", { @@ -12873,8 +14357,8 @@ export const Class: { * }) {} * * const c = new Circle({ radius: 5 }) - * console.log(c._tag) // "Circle" - * console.log(c.radius) // 5 + * c._tag // => "Circle" + * c.radius // => 5 * ``` * * @category constructors @@ -12923,38 +14407,40 @@ export const TaggedClass: { * * **Example** (Schema-backed error) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * - * class NotFound extends Schema.ErrorClass("NotFound")({ + * class NotFound extends Schema.Error("NotFound")({ * id: Schema.Number * }) {} * * const program = Effect.gen(function*() { * yield* new NotFound({ id: 1 }) * }) + * const error = await Effect.runPromise(Effect.flip(program)) + * error.id // => 1 * ``` * * @category constructors * @since 4.0.0 */ -export const ErrorClass: { +export const Error: { (identifier: string): { ( fields: Fields, annotations?: Annotations.Declaration]> - ): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Error"> : Class, Cause_.YieldableError & Brand> >( schema: S, annotations?: Annotations.Declaration - ): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> : Class + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.Error"> : Class } } = (identifier: string) => ( schema: Struct.Fields | Struct, annotations?: Annotations.Declaration]> -): [Self] extends [never] ? MissingSelfGeneric<"Schema.ErrorClass"> +): [Self] extends [never] ? MissingSelfGeneric<"Schema.Error"> : Class, Cause_.YieldableError & Brand> => { const struct = isStruct(schema) ? schema : Struct(schema) @@ -12981,28 +14467,31 @@ export const ErrorClass: { * * **Example** (Defining a tagged error class) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Schema } from "effect" * - * class NotFound extends Schema.TaggedErrorClass()("NotFound", { + * class NotFound extends Schema.TaggedError()("NotFound", { * id: Schema.Number * }) {} * * const program = Effect.gen(function*() { * yield* new NotFound({ id: 42 }) * }) + * const error = await Effect.runPromise(Effect.flip(program)) + * error._tag // => "NotFound" + * error.id // => 42 * ``` * * @category constructors * @since 3.10.0 */ -export const TaggedErrorClass: { +export const TaggedError: { (identifier?: string): { ( tag: Tag, fields: Fields, annotations?: Annotations.Declaration]> - ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedErrorClass"> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedError"> : Class, Cause_.YieldableError & Brand> >( tag: Tag, @@ -13011,7 +14500,7 @@ export const TaggedErrorClass: { Self, readonly [Struct } & S["fields"]>>] > - ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedErrorClass"> + ): [Self] extends [never] ? MissingSelfGeneric<"Schema.TaggedError"> : Class } & S["fields"]>>, Cause_.YieldableError & Brand> } } = (identifier?: string) => { @@ -13025,7 +14514,7 @@ export const TaggedErrorClass: { unsafePreserveChecks: true }) : TaggedStruct(tagValue, schema) - return ErrorClass(identifier ?? tagValue)( + return Error(identifier ?? tagValue)( struct, annotations as Annotations.Declaration ) @@ -13037,82 +14526,53 @@ export const TaggedErrorClass: { // ----------------------------------------------------------------------------- /** - * A thunk that, given the `fast-check` module, returns an `Arbitrary`. - * Use this type when you need to defer instantiation of the arbitrary, for - * example to support recursive schemas. + * Represents a function that builds a fast-check `Arbitrary` from the + * `fast-check` module. * - * @category Arbitrary - * @since 4.0.0 - */ -export type LazyArbitrary = (fc: typeof FastCheck) => FastCheck.Arbitrary - -/** - * Derives a {@link LazyArbitrary} from a schema. The result is memoized so - * repeated calls with the same schema are cheap. - * - * **Details** + * **When to use** * - * Prefer {@link toArbitrary} when you need the arbitrary directly, or when you - * want derivation diagnostics via `{ report: true }`. Unsupported schema - * nodes, impossible constraints, invalid candidates, and recursive schemas - * without a finite terminal path fail immediately. + * Use as the result type of schema arbitrary derivation. * - * @category Arbitrary + * @category utility types * @since 4.0.0 */ -export function toArbitraryLazy(schema: S): LazyArbitrary { - const lawc = InternalArbitrary.memoized(schema.ast) - return (fc) => lawc(fc, {}) -} +export type Arbitrary = (fc: typeof FastCheck) => FastCheck.Arbitrary /** - * Derives a `fast-check` `Arbitrary` from a schema for property-based - * testing. The derived arbitrary generates values that satisfy the schema. + * Returns an {@link Arbitrary} factory derived from a schema. The generated + * values satisfy the schema and use its decoded `Type`. + * + * **When to use** + * + * Use when you need a fast-check generator for values accepted by a schema. * * **Details** * * Constraints refine base generators; candidates add weighted sources while - * filters still validate every value. `{ report: true }` returns warnings such - * as `OpaqueFilter`, while derivation errors remain fail-fast. Recursive - * schemas use terminal branches and fail when no finite terminal path exists. + * filters still validate every value. Recursive schemas use terminal branches + * and fail when no finite terminal path exists. The result is memoized so + * repeated calls with the same schema are cheap. * * **Example** (Generating arbitrary values) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import * as FastCheck from "fast-check" * - * const PersonArb = Schema.toArbitrary( + * const makePersonArbitrary = Schema.toArbitrary( * Schema.Struct({ name: Schema.String, age: Schema.Number }) * ) * - * // Sample a random value - * const sample = FastCheck.sample(PersonArb, 1)[0] - * console.log(typeof sample.name) // "string" + * const PersonArbitrary = makePersonArbitrary(FastCheck) + * FastCheck.sample(PersonArbitrary, 1) * ``` * - * @category Arbitrary + * @category generators * @since 4.0.0 */ -export function toArbitrary(schema: S): FastCheck.Arbitrary -export function toArbitrary( - schema: S, - options: { readonly report: true } -): Annotations.ToArbitrary.WithReport> -export function toArbitrary( - schema: S, - options?: { readonly report?: boolean } -): FastCheck.Arbitrary | Annotations.ToArbitrary.WithReport> { - if (options?.report === true) { - const lawc = InternalArbitrary.memoized(schema.ast) - const report = InternalArbitrary.makeReport() - InternalArbitrary.collectReport(schema.ast, report) - return { - value: lawc(FastCheck, {}), - report: InternalArbitrary.toReport(report) - } - } - return toArbitraryLazy(schema)(FastCheck) +export function toArbitrary(schema: S): Arbitrary { + const lawc = InternalArbitrary.memoized(schema.ast) + return (fc) => lawc(fc, {}) } // ----------------------------------------------------------------------------- @@ -13128,7 +14588,7 @@ export function toArbitrary( * The annotation is applied through this helper because adding it directly to * `Annotations.Bottom` would make schemas invariant. * - * @category Formatter + * @category formatting * @since 4.0.0 */ export function overrideToFormatter(toFormatter: () => Formatter) { @@ -13147,7 +14607,7 @@ export function overrideToFormatter(toFormatter: () => Formatter< * The optional `onBefore` hook lets you intercept specific AST nodes before * the default formatting logic runs. * - * @category Formatter + * @category formatting * @since 4.0.0 */ export function toFormatter(schema: S, options?: { @@ -13264,14 +14724,17 @@ export function toFormatter(schema: S, options?: { } } case "Union": { - const getCandidates = (t: any) => SchemaAST.getCandidates(t, ast.types) + const types = SchemaAST.toType(ast).types + const getCandidates = (t: any) => SchemaAST.getCandidates(t, types) + const compiled = new Map( + types.map((candidate, i) => [candidate, [SchemaParser._is(candidate), recur(ast.types[i])] as const] as const) + ) return (t) => { const candidates = getCandidates(t) - const refinements = candidates.map(SchemaParser._is) for (let i = 0; i < candidates.length; i++) { - const is = refinements[i] + const [is, formatter] = compiled.get(candidates[i])! if (is(t)) { - return recur(candidates[i])(t) + return formatter(t) } } return format(t) @@ -13312,13 +14775,13 @@ export function overrideToEquivalence(toEquivalence: () => Equiva * * **Example** (Comparing structs) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const eq = Schema.toEquivalence(Schema.Struct({ id: Schema.Number, name: Schema.String })) * - * console.log(eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" })) // true - * console.log(eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" })) // false + * eq({ id: 1, name: "Alice" }, { id: 1, name: "Alice" }) // => true + * eq({ id: 1, name: "Alice" }, { id: 2, name: "Alice" }) // => false * ``` * * @category instances @@ -13333,15 +14796,18 @@ export function toEquivalence(schema: Schema): Equivalence.Equivalence // ----------------------------------------------------------------------------- /** - * Derives an intermediate `SchemaRepresentation.Document` from a schema. This - * document is used internally by {@link toJsonSchemaDocument} and related - * functions to produce JSON Schema output. + * Derives an intermediate `SchemaRepresentation.Document` from the encoded + * side of a schema. * - * @category Representation + * **Details** + * + * Use {@link toType} before this function to represent the type side instead. + * + * @category converting * @since 4.0.0 */ export function toRepresentation(schema: Constraint): SchemaRepresentation.Document { - return InternalStandard.fromAST(schema.ast) + return InternalToRepresentation.toRepresentation(schema.ast) } // ----------------------------------------------------------------------------- @@ -13398,7 +14864,7 @@ export interface ToJsonSchemaOptions { * * **Example** (Including custom annotations) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * const schema = Schema.String.annotate({ @@ -13411,12 +14877,7 @@ export interface ToJsonSchemaOptions { * key === "markdownDescription" || key.startsWith("x-") * }) * - * console.log(doc.schema) - * // { - * // type: "string", - * // description: "A name", - * // markdownDescription: "The **name** field" - * // } + * doc.schema // => { type: "string", description: "A name", markdownDescription: "The **name** field" } * ``` */ readonly includeAnnotationKey?: ((key: string) => boolean) | undefined @@ -13429,14 +14890,16 @@ export interface ToJsonSchemaOptions { * * The `options` parameter controls generation details such as additional * properties and synthesized check descriptions; it does not change the draft - * target. + * target. Declarations are lowered through their `toCodecJson` or `toCodec` + * annotation when available before the representation document is compiled. * * **Gotchas** * * JSON Schema generation is best-effort. Some Effect schema semantics cannot * be represented exactly in JSON Schema, and importing an emitted JSON Schema * may produce an equivalent approximation rather than the original schema - * shape. + * shape. Opaque declarations without a structural codec are represented by an + * unconstrained JSON Schema. * * @category converting * @since 4.0.0 @@ -13445,13 +14908,11 @@ export function toJsonSchemaDocument( schema: Constraint, options?: ToJsonSchemaOptions ): JsonSchema.Document<"draft-2020-12"> { - const sd = toRepresentation(schema) - const jd = InternalStandard.toJsonSchemaDocument(sd, options) - return { - dialect: "draft-2020-12", - schema: jd.schema, - definitions: jd.definitions - } + const document = InternalToRepresentation.toRepresentation( + toCodecJsonAST(schema.ast), + InternalToJsonSchemaDocument.toRepresentationOptions + ) + return InternalToJsonSchemaDocument.toJsonSchemaDocument(document, options) } // ----------------------------------------------------------------------------- @@ -13461,7 +14922,7 @@ export function toJsonSchemaDocument( /** * Type-level representation returned by {@link toCodecJson}. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export interface toCodecJson extends @@ -13490,35 +14951,94 @@ export interface toCodecJson extends * Derives a canonical JSON codec from a schema. The encoded form is `Json`, and * decoding produces the schema's `Type`. * - * @category Canonical Codecs + * **Gotchas** + * + * Declarations without a `toCodecJson` or `toCodec` annotation use `Json` as + * their encoded schema. This keeps codec construction total, but encoding or + * decoding can still fail when declaration values are not JSON values. A + * `toCodecJson` callback can return `undefined` when the declaration is already + * in canonical JSON form. + * + * @category converting * @since 4.0.0 */ export function toCodecJson(schema: S): toCodecJson { - return make(toCodecJsonTop(schema.ast), { schema }) + return make(toCodecJsonAST(schema.ast), { schema }) +} + +/** @internal */ +export const toCodecJsonAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => { + const out = toCodecJsonASTStep(ast, toCodecJsonAST) + const context = ast.context + if (out === ast || context === undefined) return out + return SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(context)) +}) + +function withoutConstructorDefault(context: SchemaAST.Context): SchemaAST.Context { + return context.constructorDefault === undefined ? + context : + new SchemaAST.Context(context.isOptional, context.isMutable, undefined, context.annotations) +} + +function validateCanonicalObjectPropertyNames(ast: SchemaAST.Objects): void { + if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) { + throw new globalThis.Error("Objects property names must be strings", { cause: ast }) + } +} + +function makeReorder(getPriority: (ast: SchemaAST.AST) => number) { + return (types: ReadonlyArray): ReadonlyArray => { + // Create a map of original indices for O(1) lookup + const indexMap = new Map() + for (let i = 0; i < types.length; i++) { + indexMap.set(SchemaAST.toEncoded(types[i]), i) + } + + // Create a sorted copy of the types array + const sortedTypes = [...types].sort((a, b) => { + a = SchemaAST.toEncoded(a) + b = SchemaAST.toEncoded(b) + const pa = getPriority(a) + const pb = getPriority(b) + if (pa !== pb) return pa - pb + // If priorities are equal, maintain original order (stable sort) + return indexMap.get(a)! - indexMap.get(b)! + }) + + // Check if order changed by comparing arrays + const orderChanged = sortedTypes.some((ast, index) => ast !== types[index]) + + if (!orderChanged) return types + return sortedTypes + } } -const toCodecJsonTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { - const out = toCodecJsonBase(ast, toCodecJsonTop) - return out !== ast && SchemaAST.isOptional(ast) ? SchemaAST.optionalKeyLastLink(out) : out +const toCodecJsonReorder = makeReorder((ast: SchemaAST.AST) => { + switch (ast._tag) { + case "BigInt": + case "Symbol": + case "UniqueSymbol": + return 0 + default: + return 1 + } }) -function toCodecJsonBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { +function toCodecJsonASTStep(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { switch (ast._tag) { case "Declaration": { const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec - if (Predicate.isFunction(getLink)) { - const tps = SchemaAST.isDeclaration(ast) - ? ast.typeParameters.map((tp) => InternalSchema.make(SchemaAST.toEncoded(tp))) - : [] - const link = getLink(tps) - const to = recur(link.to) - return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + if (!Predicate.isFunction(getLink)) { + return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToJson]) } - return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToNull]) + const typeParameters = ast.typeParameters.map((tp) => InternalSchema.make(SchemaAST.toEncoded(tp))) + const link = getLink(typeParameters) + return link === undefined ? ast : SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)]) } case "Unknown": - case "ObjectKeyword": return SchemaAST.replaceEncoding(ast, [SchemaAST.unknownToJson]) + case "ObjectKeyword": + return SchemaAST.replaceEncoding(ast, [SchemaAST.objectKeywordToJson]) case "Undefined": case "Void": case "Literal": @@ -13529,13 +15049,11 @@ function toCodecJsonBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Sche case "BigInt": return ast.toCodecStringTree() case "Objects": { - if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) { - throw new globalThis.Error("Objects property names must be strings", { cause: ast }) - } + validateCanonicalObjectPropertyNames(ast) return ast.recur(recur, SchemaAST.parameterFromString) } case "Union": { - const sortedTypes = InternalSchema.jsonReorder(ast.types) + const sortedTypes = toCodecJsonReorder(ast.types) if (sortedTypes !== ast.types) { return new SchemaAST.Union( sortedTypes, @@ -13561,26 +15079,27 @@ function toCodecJsonBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Sche * Derives an isomorphism codec from a schema. The encoded form is the * schema's `Iso` type — the intermediate representation used for round-tripping. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export function toCodecIso(schema: S): Codec { - return make(toCodecIsoTop(SchemaAST.toType(schema.ast))) + return make(toCodecIsoAST(SchemaAST.toType(schema.ast))) } -const toCodecIsoTop = memoize((ast: SchemaAST.AST): SchemaAST.AST => { - const out = toCodecIsoBase(ast, toCodecIsoTop) - return out !== ast && SchemaAST.isOptional(ast) ? SchemaAST.optionalKeyLastLink(out) : out +const toCodecIsoAST = memoize((ast: SchemaAST.AST): SchemaAST.AST => { + const out = toCodecIsoASTStep(ast, toCodecIsoAST) + return out !== ast && ast.context !== undefined ? + SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(ast.context)) : + out }) -function toCodecIsoBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { +function toCodecIsoASTStep(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { switch (ast._tag) { case "Declaration": { const getLink = ast.annotations?.toCodecIso ?? ast.annotations?.toCodec if (Predicate.isFunction(getLink)) { const link = getLink(ast.typeParameters.map((tp) => InternalSchema.make(tp))) - const to = recur(link.to) - return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + return SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)]) } return ast } @@ -13597,7 +15116,7 @@ function toCodecIsoBase(ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => Schem * A {@link Tree} of `string | undefined` nodes. Leaf values are either a * string representation or `undefined` for opaque/declaration types. * - * @category Canonical Codecs + * @category models * @since 4.0.0 */ export type StringTree = Tree @@ -13605,7 +15124,7 @@ export type StringTree = Tree /** * Type-level representation returned by {@link toCodecStringTree}. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export interface toCodecStringTree extends @@ -13634,22 +15153,23 @@ export interface toCodecStringTree extends * Converts a schema to the StringTree canonical codec, where every leaf value * becomes a string while preserving the original structure. * - * **Details** + * **Gotchas** * - * Declarations are converted to `undefined` (unless they have a - * `toCodecJson` or `toCodec` annotation). + * Declarations must provide a structural `toCodecStringTree`, `toCodecJson`, or + * `toCodec` encoding. A callback can return `undefined` when the declaration is + * already in canonical StringTree form. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export function toCodecStringTree(schema: S): toCodecStringTree { - return make(serializerStringTree(schema.ast), { schema }) + return make(toCodecStringTreeAST(schema.ast), { schema }) } /** * Type-level representation returned by {@link toCodecArrayFromSingle}. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export interface toCodecArrayFromSingle extends @@ -13688,11 +15208,11 @@ export interface toCodecArrayFromSingle extends * decoding convenience rather than a canonical StringTree representation. It * does not parse comma-separated strings. * - * @category Canonical Codecs + * @category converting * @since 4.0.0 */ export function toCodecArrayFromSingle(schema: S): toCodecArrayFromSingle { - return make(toCodecArrayFromSingleTop(schema.ast)) + return make(toCodecArrayFromSingleAST(schema.ast)) } type XmlEncoderOptions = { @@ -13717,7 +15237,7 @@ type XmlEncoderOptions = { * an `Effect` that succeeds with the XML string or fails with `SchemaError` if * codec encoding fails. * - * @category Canonical Codecs + * @category encoding * @since 4.0.0 */ export function toEncoderXml( @@ -13815,7 +15335,7 @@ const xml = { } } -function getStringTreePriority(ast: SchemaAST.AST): number { +const toStringTreeReorder = makeReorder((ast: SchemaAST.AST) => { switch (ast._tag) { case "Null": case "Boolean": @@ -13827,27 +15347,29 @@ function getStringTreePriority(ast: SchemaAST.AST): number { default: return 1 } -} - -const treeReorder = InternalSchema.makeReorder(getStringTreePriority) +}) -function serializerTree( +function toCodecStringTreeASTStep( ast: SchemaAST.AST, recur: (ast: SchemaAST.AST) => SchemaAST.AST, onMissingAnnotation: (ast: SchemaAST.AST) => SchemaAST.AST ): SchemaAST.AST { switch (ast._tag) { case "Declaration": { - const getLink = ast.annotations?.toCodecJson ?? ast.annotations?.toCodec - if (Predicate.isFunction(getLink)) { - const tps = SchemaAST.isDeclaration(ast) - ? ast.typeParameters.map((tp) => make(recur(SchemaAST.toEncoded(tp)))) - : [] - const link = getLink(tps) - const to = recur(link.to) - return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) + const typeParameters = ast.typeParameters.map((tp) => make(recur(SchemaAST.toEncoded(tp)))) + const getStringTreeLink = ast.annotations?.toCodecStringTree + if (Predicate.isFunction(getStringTreeLink)) { + const link = getStringTreeLink(typeParameters) + if (link === undefined) return ast + return SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)]) } - return onMissingAnnotation(ast) + const getJsonLink = ast.annotations?.toCodecJson + const jsonLink = Predicate.isFunction(getJsonLink) ? getJsonLink(typeParameters) : undefined + const getLink = jsonLink === undefined ? ast.annotations?.toCodec : undefined + const link = jsonLink ?? (Predicate.isFunction(getLink) ? getLink(typeParameters) : undefined) + return link === undefined + ? onMissingAnnotation(ast) + : SchemaAST.replaceEncoding(ast, [SchemaAST.mapLink(link, recur)]) } case "Null": return SchemaAST.replaceEncoding(ast, [nullToString]) @@ -13864,13 +15386,11 @@ function serializerTree( case "BigInt": return ast.toCodecStringTree() case "Objects": { - if (ast.propertySignatures.some((ps) => typeof ps.name !== "string")) { - throw new globalThis.Error("Objects property names must be strings", { cause: ast }) - } + validateCanonicalObjectPropertyNames(ast) return ast.recur(recur, SchemaAST.parameterFromString) } case "Union": { - const sortedTypes = treeReorder(ast.types) + const sortedTypes = toStringTreeReorder(ast.types) if (sortedTypes !== ast.types) { return new SchemaAST.Union( sortedTypes, @@ -13900,80 +15420,281 @@ const nullToString = new SchemaAST.Link( ) ) -const booleanToString = new SchemaAST.Link( - new SchemaAST.Union([new SchemaAST.Literal("true"), new SchemaAST.Literal("false")], "anyOf"), - new SchemaTransformation.Transformation( - SchemaGetter.transform((s) => s === "true"), - SchemaGetter.String() - ) +const booleanToString = new SchemaAST.Link( + new SchemaAST.Union([new SchemaAST.Literal("true"), new SchemaAST.Literal("false")], "anyOf"), + new SchemaTransformation.Transformation( + SchemaGetter.transform((s) => s === "true"), + SchemaGetter.String() + ) +) + +const arrayFromSingleTransformation = new SchemaTransformation.Transformation( + SchemaGetter.transform((input: ReadonlyArray | string) => typeof input === "string" ? [input] : input), + SchemaGetter.passthrough() +) + +const isCodecArrayFromSingleLink = (link: SchemaAST.Link): boolean => + link.transformation === arrayFromSingleTransformation + +const toCodecStringTreeAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => { + const out = toCodecStringTreeASTStep(ast, toCodecStringTreeAST, (ast) => { + throw new globalThis.Error("Missing structural codec for StringTree", { cause: ast }) + }) + if (out !== ast && ast.context !== undefined) { + return SchemaAST.replaceContextLastLink(out, withoutConstructorDefault(ast.context)) + } + return out +}, { stopAt: isCodecArrayFromSingleLink }) + +const toArrayFromSingleInputElement = (ast: SchemaAST.AST): SchemaAST.AST => + SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(SchemaAST.unknown) : SchemaAST.unknown + +const toCodecArrayFromSingleAST = SchemaAST.applyToSelfOrLastLinkEncodingIdempotent((ast) => { + const out = toCodecArrayFromSingleASTStep(ast) + if (SchemaAST.isArrays(out)) { + const ensure = SchemaAST.decodeTo( + new SchemaAST.Union( + [ + new SchemaAST.Arrays( + out.isMutable, + out.elements.map(toArrayFromSingleInputElement), + out.rest.map(toArrayFromSingleInputElement) + ), + SchemaAST.string + ], + "anyOf" + ), + out, + arrayFromSingleTransformation + ) + return SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(ensure) : ensure + } + return out +}, { stopAt: isCodecArrayFromSingleLink }) + +function toCodecArrayFromSingleASTStep(ast: SchemaAST.AST): SchemaAST.AST { + return ast._tag === "Declaration" || ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union" || + ast._tag === "Suspend" + ? ast.recur(toCodecArrayFromSingleAST) + : ast +} + +/** + * Reviver for persisted `isGreaterThanDate` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThanDate}. + * + * @see {@link isGreaterThanDate} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanDateReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMinimum: globalThis.Date +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThanDate", + Struct({ exclusiveMinimum: Date }), + ({ annotations, payload }) => isGreaterThanDate(payload.exclusiveMinimum, annotations) +) + +/** + * Reviver for persisted `isGreaterThanOrEqualToDate` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThanOrEqualToDate}. + * + * @see {@link isGreaterThanOrEqualToDate} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToDateReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: globalThis.Date +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThanOrEqualToDate", + Struct({ minimum: Date }), + ({ annotations, payload }) => isGreaterThanOrEqualToDate(payload.minimum, annotations) +) + +/** + * Reviver for persisted `isLessThanDate` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThanDate}. + * + * @see {@link isLessThanDate} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanDateReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMaximum: globalThis.Date +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThanDate", + Struct({ exclusiveMaximum: Date }), + ({ annotations, payload }) => isLessThanDate(payload.exclusiveMaximum, annotations) +) + +/** + * Reviver for persisted `isLessThanOrEqualToDate` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThanOrEqualToDate}. + * + * @see {@link isLessThanOrEqualToDate} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanOrEqualToDateReviver: SchemaRepresentation.FilterReviver<{ + readonly maximum: globalThis.Date +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThanOrEqualToDate", + Struct({ maximum: Date }), + ({ annotations, payload }) => isLessThanOrEqualToDate(payload.maximum, annotations) +) + +/** + * Reviver for persisted `isBetweenDate` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isBetweenDate}. + * + * @see {@link isBetweenDate} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isBetweenDateReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: globalThis.Date + readonly maximum: globalThis.Date + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined +}> = InternalSchema.makeFilterReviver( + "effect/schema/isBetweenDate", + Struct({ + minimum: Date, + maximum: Date, + exclusiveMinimum: optional(Literal(true)), + exclusiveMaximum: optional(Literal(true)) + }), + ({ annotations, payload }) => isBetweenDate(payload, annotations) +) + +/** + * Reviver for persisted `isGreaterThanBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThanBigInt}. + * + * @see {@link isGreaterThanBigInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanBigIntReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMinimum: bigint +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThanBigInt", + Struct({ exclusiveMinimum: BigInt }), + ({ annotations, payload }) => isGreaterThanBigInt(payload.exclusiveMinimum, annotations) +) + +/** + * Reviver for persisted `isGreaterThanOrEqualToBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isGreaterThanOrEqualToBigInt}. + * + * @see {@link isGreaterThanOrEqualToBigInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isGreaterThanOrEqualToBigIntReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: bigint +}> = InternalSchema.makeFilterReviver( + "effect/schema/isGreaterThanOrEqualToBigInt", + Struct({ minimum: BigInt }), + ({ annotations, payload }) => isGreaterThanOrEqualToBigInt(payload.minimum, annotations) +) + +/** + * Reviver for persisted `isLessThanBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThanBigInt}. + * + * @see {@link isLessThanBigInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanBigIntReviver: SchemaRepresentation.FilterReviver<{ + readonly exclusiveMaximum: bigint +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThanBigInt", + Struct({ exclusiveMaximum: BigInt }), + ({ annotations, payload }) => isLessThanBigInt(payload.exclusiveMaximum, annotations) ) -const SERIALIZER_ENSURE_ARRAY = "~effect/Schema/SERIALIZER_ENSURE_ARRAY" - -const isSerializerArrayFromSingle = (ast: SchemaAST.AST): boolean => - SchemaAST.isUnion(ast) && ast.annotations?.[SERIALIZER_ENSURE_ARRAY] === true - -const serializerStringTree = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { - if (isSerializerArrayFromSingle(ast)) { - return ast - } - const out = serializerTree(ast, serializerStringTree, (ast) => SchemaAST.replaceEncoding(ast, [unknownToUndefined])) - if (out !== ast && SchemaAST.isOptional(ast)) { - return SchemaAST.optionalKeyLastLink(out) - } - return out -}) - -const unknownToUndefined = new SchemaAST.Link( - SchemaAST.undefined, - new SchemaTransformation.Transformation( - SchemaGetter.passthrough(), - SchemaGetter.transform(() => undefined) - ) +/** + * Reviver for persisted `isLessThanOrEqualToBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isLessThanOrEqualToBigInt}. + * + * @see {@link isLessThanOrEqualToBigInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isLessThanOrEqualToBigIntReviver: SchemaRepresentation.FilterReviver<{ + readonly maximum: bigint +}> = InternalSchema.makeFilterReviver( + "effect/schema/isLessThanOrEqualToBigInt", + Struct({ maximum: BigInt }), + ({ annotations, payload }) => isLessThanOrEqualToBigInt(payload.maximum, annotations) ) -const toArrayFromSingleInputElement = (ast: SchemaAST.AST): SchemaAST.AST => - SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(SchemaAST.unknown) : SchemaAST.unknown - -const arrayFromSingleTransformation = new SchemaTransformation.Transformation( - SchemaGetter.transform((input: ReadonlyArray | string) => typeof input === "string" ? [input] : input), - SchemaGetter.passthrough() +/** + * Reviver for persisted `isBetweenBigInt` checks. + * + * **When to use** + * + * Use when reconstructing documents that may contain checks created by {@link isBetweenBigInt}. + * + * @see {@link isBetweenBigInt} for creating the corresponding check + * + * @category validation + * @since 4.0.0 + */ +export const isBetweenBigIntReviver: SchemaRepresentation.FilterReviver<{ + readonly minimum: bigint + readonly maximum: bigint + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined +}> = InternalSchema.makeFilterReviver( + "effect/schema/isBetweenBigInt", + Struct({ + minimum: BigInt, + maximum: BigInt, + exclusiveMinimum: optional(Literal(true)), + exclusiveMaximum: optional(Literal(true)) + }), + ({ annotations, payload }) => isBetweenBigInt(payload, annotations) ) -const toCodecArrayFromSingleTop = SchemaAST.applyToSelfOrLastLinkEncoding((ast) => { - if (isSerializerArrayFromSingle(ast)) { - return ast - } - const out = onSerializerArrayFromSingle(ast) - if (SchemaAST.isArrays(out)) { - const ensure = SchemaAST.decodeTo( - new SchemaAST.Union( - [ - new SchemaAST.Arrays( - out.isMutable, - out.elements.map(toArrayFromSingleInputElement), - out.rest.map(toArrayFromSingleInputElement) - ), - SchemaAST.string - ], - "anyOf", - { [SERIALIZER_ENSURE_ARRAY]: true } - ), - out, - arrayFromSingleTransformation - ) - return SchemaAST.isOptional(ast) ? SchemaAST.optionalKey(ensure) : ensure - } - return out -}) - -function onSerializerArrayFromSingle(ast: SchemaAST.AST): SchemaAST.AST { - return ast._tag === "Declaration" || ast._tag === "Arrays" || ast._tag === "Objects" || ast._tag === "Union" || - ast._tag === "Suspend" - ? ast.recur(toCodecArrayFromSingleTop) - : ast -} - // ----------------------------------------------------------------------------- // Optic APIs // ----------------------------------------------------------------------------- @@ -13982,7 +15703,19 @@ function onSerializerArrayFromSingle(ast: SchemaAST.AST): SchemaAST.AST { * Derives an `Iso` optic from a schema that isomorphically converts between * the schema's `Type` and its `Iso` (intermediate / serialized form). * - * @category Optic + * **Details** + * + * Reading through the `Iso` encodes the schema value, while replacing through + * it decodes the new focus. + * + * **Gotchas** + * + * Either direction can throw an `Error` with the generic message + * `"Schema validation failed"` and a `SchemaIssue.Issue` in its `cause`. Format + * the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. + * + * @category converting * @since 4.0.0 */ export function toIso(schema: S): Optic_.Iso { @@ -13993,7 +15726,7 @@ export function toIso(schema: S): Optic_.Iso(_: S): Optic_.Iso { @@ -14003,7 +15736,7 @@ export function toIsoSource(_: S): Optic_.Iso(_: S): Optic_.Iso { @@ -14013,7 +15746,7 @@ export function toIsoFocus(_: S): Optic_.Iso extends @@ -14052,7 +15785,7 @@ export interface overrideToCodecIso extends * provided `decode` and `encode` getters to transform between the schema type * and the target codec. * - * @category Optic + * @category transforming * @since 4.0.0 */ export function overrideToCodecIso( @@ -14081,6 +15814,19 @@ export function overrideToCodecIso( * {@link toCodecJson}), computes RFC 6902 JSON Patch operations between old * and new values, and can apply patches back to the typed value. * + * **Details** + * + * `diff` encodes both values before computing the patch. `patch` encodes the old + * value, applies the patch to its JSON representation, and decodes the result. + * + * **Gotchas** + * + * Schema encoding or decoding failures throw an `Error` with the generic message + * `"Schema validation failed"` and a `SchemaIssue.Issue` in its `cause`. Format + * the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. Errors produced by {@link JsonPatch.apply} + * for invalid patch operations are separate from schema validation failures. + * * @category converting * @since 4.0.0 */ @@ -14104,7 +15850,7 @@ export function toDifferJsonPatch(schema: ConstraintCodec): Diffe * Recursive tree type whose leaves are `Node` values and whose branches are * readonly arrays or string-keyed records of child trees. * - * @category Tree + * @category models * @since 4.0.0 */ export type Tree = Node | TreeRecord | ReadonlyArray> @@ -14113,7 +15859,7 @@ export type Tree = Node | TreeRecord | ReadonlyArray> * A record node in a {@link Tree}: an object mapping string keys to child * `Tree` nodes. * - * @category Tree + * @category models * @since 4.0.0 */ export interface TreeRecord { @@ -14125,7 +15871,7 @@ export interface TreeRecord { * The resulting schema accepts a single node value, an array of trees, or an * object whose values are trees. * - * @category Tree + * @category schemas * @since 4.0.0 */ export function Tree(node: S) { @@ -14177,17 +15923,38 @@ export interface JsonObject { * * **Example** (Validating a JSON value) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema } from "effect" * - * const result = Schema.decodeUnknownOption(Schema.Json)({ key: [1, true, null] }) - * console.log(result._tag) // "Some" + * Schema.decodeUnknownOption(Schema.Json)({ key: [1, true, null] }) // => Option.some({ key: [1, true, null] }) * ``` * * @category schemas * @since 4.0.0 */ -export const Json: Codec = make(SchemaAST.Json) +export const Json: Codec = make(SchemaAST.annotate(SchemaAST.Json, { + toCode: () => ({ + runtime: "Schema.Json", + Type: "Schema.Json" + }) +})) + +/** + * Reviver for persisted `Json` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link Json} schema. + * + * @see {@link Json} for the corresponding immutable JSON schema + * + * @category schemas + * @since 4.0.0 + */ +export const JsonReviver = makeFixedDeclarationReviver( + "effect/schema/Json", + Json +) const JsonError = Struct({ message: String, @@ -14230,7 +15997,29 @@ export interface MutableJsonObject { * @category schemas * @since 4.0.0 */ -export const MutableJson: Codec = make(SchemaAST.MutableJson) +export const MutableJson: Codec = make(SchemaAST.annotate(SchemaAST.MutableJson, { + toCode: () => ({ + runtime: "Schema.MutableJson", + Type: "Schema.MutableJson" + }) +})) + +/** + * Reviver for persisted `MutableJson` declarations. + * + * **When to use** + * + * Use when reconstructing documents that may contain the {@link MutableJson} schema. + * + * @see {@link MutableJson} for the corresponding mutable JSON schema + * + * @category schemas + * @since 4.0.0 + */ +export const MutableJsonReviver = makeFixedDeclarationReviver( + "effect/schema/MutableJson", + MutableJson +) // ----------------------------------------------------------------------------- // Annotations @@ -14242,7 +16031,7 @@ export const MutableJson: Codec = make(SchemaAST.MutableJson) * annotations are taken from the last check; otherwise they are taken from * the base schema instance. * - * @category Schema Resolvers + * @category getters * @since 4.0.0 */ export function resolveAnnotations( @@ -14256,7 +16045,7 @@ export function resolveAnnotations( * annotations are those attached via `annotateKey` and live on the AST's * `context` rather than on the schema node itself. * - * @category Schema Resolvers + * @category getters * @since 4.0.0 */ export function resolveAnnotationsKey(schema: S): Annotations.Key | undefined { @@ -14289,7 +16078,7 @@ export declare namespace Annotations { * * **Example** (Defining your own annotations) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * * // Extend the Annotations interface with a custom `version` annotation @@ -14311,8 +16100,7 @@ export declare namespace Annotations { * * if (version) { * // Access individual parts of the version - * console.log(version[1]) - * // Output: 2 + * version[1] // => 2 * } * ``` * @@ -14340,7 +16128,7 @@ export declare namespace Annotations { * `message` first, then `expected`, and finally falls back to ``. * * Use this to name a failed filter in the default message: - * `Expected , got `. + * `Expected `. */ readonly expected?: string | undefined readonly title?: string | undefined @@ -14351,6 +16139,7 @@ export declare namespace Annotations { readonly format?: string | undefined readonly contentEncoding?: string | undefined readonly contentMediaType?: string | undefined + readonly contentSchema?: Json | undefined } /** @@ -14398,7 +16187,7 @@ export declare namespace Annotations { * only changing the expected label. For a filter or refinement failure, * annotate the filter with `message` to replace the whole filter failure * message, or `expected` to keep the default - * `Expected , got ` shape. + * `Expected ` shape. */ readonly message?: string | undefined /** @@ -14413,7 +16202,7 @@ export declare namespace Annotations { * Identifiers are used by schema tooling, including JSON Schema * generation, to name references. The default formatter also uses * `identifier` as the expected label for type-level failures, such as - * `Expected UserId, got null`. + * `Expected UserId`. * * `identifier` does not name a failed filter or refinement. If the base * type matches and a filter fails, put `expected` or `message` on the @@ -14421,10 +16210,6 @@ export declare namespace Annotations { */ readonly identifier?: string | undefined readonly parseOptions?: SchemaAST.ParseOptions | undefined - /** - * Optional metadata used to identify or extend the filter with custom data. - */ - readonly meta?: Meta | undefined /** * Accumulated brands when multiple brands are added with `Schema.brand`. */ @@ -14475,11 +16260,17 @@ export declare namespace Annotations { export interface Declaration = readonly []> extends Bottom { + readonly representation?: + | SchemaRepresentation.RepresentationAnnotation + | undefined readonly toCodec?: | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link) | undefined readonly toCodecJson?: - | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link) + | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link | undefined) + | undefined + readonly toCodecStringTree?: + | ((typeParameters: TypeParameters.Encoded) => SchemaAST.Link | undefined) | undefined readonly toCodecIso?: | ((typeParameters: TypeParameters.Type) => SchemaAST.Link) @@ -14487,16 +16278,7 @@ export declare namespace Annotations { readonly toArbitrary?: ToArbitrary.Declaration | undefined readonly toEquivalence?: ToEquivalence.Declaration | undefined readonly toFormatter?: ToFormatter.Declaration | undefined - readonly typeConstructor?: { - readonly _tag: string - readonly [key: string]: unknown - } | undefined - readonly generation?: { - readonly runtime: string - readonly Type: string - readonly Encoded?: string | undefined - readonly importDeclaration?: string | undefined - } | undefined + readonly toCode?: SchemaRepresentation.Generation.Declaration | undefined /** * Used to collect sentinels from a Declaration SchemaAST. * @@ -14514,6 +16296,20 @@ export declare namespace Annotations { * @since 4.0.0 */ export interface Filter extends Augment { + readonly representation?: + | SchemaRepresentation.CheckRepresentationAnnotation + | undefined + /** + * Compiles this filter to a JSON Schema fragment. + * + * **Gotchas** + * + * Treat the input schemas as immutable. The returned value must be a valid JSON Schema object graph and must not be + * mutated after this function returns. Return a new object graph to produce different output during a later + * compilation. + */ + readonly toJsonSchema?: SchemaRepresentation.ToJsonSchema.Check | undefined + readonly toCode?: SchemaRepresentation.Generation.Check | undefined /** * Complete message to use when this filter or refinement fails. * @@ -14534,10 +16330,6 @@ export declare namespace Annotations { * `message`. */ readonly identifier?: string | undefined - /** - * Optional metadata used to identify or extend the filter with custom data. - */ - readonly meta?: Meta | undefined /** * Optional hints used by arbitrary derivation for this filter. * @@ -14557,6 +16349,8 @@ export declare namespace Annotations { * * **Details** * + * Reserved to internal use only. + * * Example: `minLength` on an array is a structural filter. */ readonly "~structural"?: boolean | undefined @@ -14564,8 +16358,7 @@ export declare namespace Annotations { /** * Types used by arbitrary-derivation annotations to configure `toArbitrary` - * hooks, filter hints, candidate sources, diagnostics, and merged generation - * constraints. + * hooks, filter hints, candidate sources, and merged generation constraints. * * @since 4.0.0 */ @@ -14578,8 +16371,7 @@ export declare namespace Annotations { * `constraint` refines the schema node's base generator. `candidate` adds a * weighted source before all filters run. If neither hint is provided, the * filter does not guide generation; generated values are still checked by - * the filter predicate. With `{ report: true }`, this is reported as - * `OpaqueFilter`. + * the filter predicate. * * @category models * @since 4.0.0 @@ -14646,7 +16438,7 @@ export declare namespace Annotations { * length for strings, array length for arrays, final own-property count for * objects, and final size/cardinality for sets, maps, hash collections, and * chunks. `patterns` are concatenated and used by string generators. - * `integer`, `noNaN`, `noInfinity`, `valid`, and `unique` are true when any + * `integer`, `noNaN`, `noInfinity`, and `unique` are true when any * contributing filter sets them. Range bounds live in `ordered` so ordered * values can share the same representation. * @@ -14660,7 +16452,6 @@ export declare namespace Annotations { readonly integer?: boolean | undefined readonly noInfinity?: boolean | undefined readonly noNaN?: boolean | undefined - readonly valid?: boolean | undefined readonly unique?: boolean | undefined readonly ordered?: OrderedConstraint | undefined } @@ -14768,58 +16559,6 @@ export declare namespace Annotations { typeParameters: { readonly [K in keyof TypeParameters]: TypeParameter } ): (fc: typeof FastCheck, context: Context) => Output } - - /** - * Wraps a derived value together with arbitrary-derivation diagnostics. - * - * @category models - * @since 4.0.0 - */ - export interface WithReport { - readonly value: A - readonly report: Report - } - - /** - * Diagnostics collected while deriving an arbitrary. - * - * **Details** - * - * Reports contain warnings only. Unsupported schema nodes, impossible - * constraints, invalid candidate weights, and throwing candidate factories - * fail immediately. - * - * @category models - * @since 4.0.0 - */ - export interface Report { - readonly warnings: ReadonlyArray - } - - /** - * Non-fatal arbitrary-derivation warning. - * - * @category models - * @since 4.0.0 - */ - export type Warning = OpaqueFilterWarning - - /** - * Warning emitted when a filter is handled only by the final `.filter`. - * - * **Details** - * - * The filter is still enforced. The warning means it did not contribute - * a constraint or candidate, so generation may rely on fast-check discards. - * - * @category models - * @since 4.0.0 - */ - export interface OpaqueFilterWarning { - readonly _tag: "OpaqueFilter" - readonly path: ReadonlyArray - readonly description?: string | undefined - } } /** @@ -14877,262 +16616,22 @@ export declare namespace Annotations { * * **Details** * - * The optional `message` field overrides the default issue message. + * For `InvalidValue` issues, `message` overrides the complete formatted + * message. When `message` is absent, `expected` uses the default expected + * value policy, including reported input when available. Other issue types + * ignore `expected`. * * @category models * @since 4.0.0 */ export interface Issue extends Annotations { + /** + * The expected value description for an `InvalidValue` issue. + */ + readonly expected?: string | undefined + /** + * The complete formatted message for the issue. + */ readonly message?: string | undefined } - - /** - * Registry of metadata payloads emitted by built-in schema filters and checks. - * - * **Details** - * - * Do not augment this interface with custom metadata; extend `MetaDefinitions` - * instead. - * - * @category models - * @since 4.0.0 - */ - export interface BuiltInMetaDefinitions { - // String Meta - readonly isStringFinite: { - readonly _tag: "isStringFinite" - readonly regExp: globalThis.RegExp - } - readonly isStringBigInt: { - readonly _tag: "isStringBigInt" - readonly regExp: globalThis.RegExp - } - readonly isStringSymbol: { - readonly _tag: "isStringSymbol" - readonly regExp: globalThis.RegExp - } - readonly isMinLength: { - readonly _tag: "isMinLength" - readonly minLength: number - } - readonly isMaxLength: { - readonly _tag: "isMaxLength" - readonly maxLength: number - } - readonly isLengthBetween: { - readonly _tag: "isLengthBetween" - readonly minimum: number - readonly maximum: number - } - readonly isPattern: { - readonly _tag: "isPattern" - readonly regExp: globalThis.RegExp - } - readonly isTrimmed: { - readonly _tag: "isTrimmed" - readonly regExp: globalThis.RegExp - } - readonly isUUID: { - readonly _tag: "isUUID" - readonly regExp: globalThis.RegExp - readonly version: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | undefined - } - readonly isGUID: { - readonly _tag: "isGUID" - readonly regExp: globalThis.RegExp - } - readonly isULID: { - readonly _tag: "isULID" - readonly regExp: globalThis.RegExp - } - readonly isBase64: { - readonly _tag: "isBase64" - readonly regExp: globalThis.RegExp - } - readonly isBase64Url: { - readonly _tag: "isBase64Url" - readonly regExp: globalThis.RegExp - } - readonly isStartsWith: { - readonly _tag: "isStartsWith" - readonly startsWith: string - readonly regExp: globalThis.RegExp - } - readonly isEndsWith: { - readonly _tag: "isEndsWith" - readonly endsWith: string - readonly regExp: globalThis.RegExp - } - readonly isIncludes: { - readonly _tag: "isIncludes" - readonly includes: string - readonly regExp: globalThis.RegExp - } - readonly isUppercased: { - readonly _tag: "isUppercased" - readonly regExp: globalThis.RegExp - } - readonly isLowercased: { - readonly _tag: "isLowercased" - readonly regExp: globalThis.RegExp - } - readonly isCapitalized: { - readonly _tag: "isCapitalized" - readonly regExp: globalThis.RegExp - } - readonly isUncapitalized: { - readonly _tag: "isUncapitalized" - readonly regExp: globalThis.RegExp - } - // Number Meta - readonly isFinite: { - readonly _tag: "isFinite" - } - readonly isInt: { - readonly _tag: "isInt" - } - readonly isMultipleOf: { - readonly _tag: "isMultipleOf" - readonly divisor: number - } - readonly isGreaterThan: { - readonly _tag: "isGreaterThan" - readonly exclusiveMinimum: number - } - readonly isGreaterThanOrEqualTo: { - readonly _tag: "isGreaterThanOrEqualTo" - readonly minimum: number - } - readonly isLessThan: { - readonly _tag: "isLessThan" - readonly exclusiveMaximum: number - } - readonly isLessThanOrEqualTo: { - readonly _tag: "isLessThanOrEqualTo" - readonly maximum: number - } - readonly isBetween: { - readonly _tag: "isBetween" - readonly minimum: number - readonly maximum: number - readonly exclusiveMinimum?: boolean | undefined - readonly exclusiveMaximum?: boolean | undefined - } - // BigInt Meta - readonly isGreaterThanBigInt: { - readonly _tag: "isGreaterThanBigInt" - readonly exclusiveMinimum: bigint - } - readonly isGreaterThanOrEqualToBigInt: { - readonly _tag: "isGreaterThanOrEqualToBigInt" - readonly minimum: bigint - } - readonly isLessThanBigInt: { - readonly _tag: "isLessThanBigInt" - readonly exclusiveMaximum: bigint - } - readonly isLessThanOrEqualToBigInt: { - readonly _tag: "isLessThanOrEqualToBigInt" - readonly maximum: bigint - } - readonly isBetweenBigInt: { - readonly _tag: "isBetweenBigInt" - readonly minimum: bigint - readonly maximum: bigint - readonly exclusiveMinimum?: boolean | undefined - readonly exclusiveMaximum?: boolean | undefined - } - // Date Meta - readonly isDateValid: { - readonly _tag: "isDateValid" - } - readonly isGreaterThanDate: { - readonly _tag: "isGreaterThanDate" - readonly exclusiveMinimum: globalThis.Date - } - readonly isGreaterThanOrEqualToDate: { - readonly _tag: "isGreaterThanOrEqualToDate" - readonly minimum: globalThis.Date - } - readonly isLessThanDate: { - readonly _tag: "isLessThanDate" - readonly exclusiveMaximum: globalThis.Date - } - readonly isLessThanOrEqualToDate: { - readonly _tag: "isLessThanOrEqualToDate" - readonly maximum: globalThis.Date - } - readonly isBetweenDate: { - readonly _tag: "isBetweenDate" - readonly minimum: globalThis.Date - readonly maximum: globalThis.Date - readonly exclusiveMinimum?: boolean | undefined - readonly exclusiveMaximum?: boolean | undefined - } - // Objects Meta - readonly isMinProperties: { - readonly _tag: "isMinProperties" - readonly minProperties: number - } - readonly isMaxProperties: { - readonly _tag: "isMaxProperties" - readonly maxProperties: number - } - readonly isPropertiesLengthBetween: { - readonly _tag: "isPropertiesLengthBetween" - readonly minimum: number - readonly maximum: number - } - readonly isPropertyNames: { - readonly _tag: "isPropertyNames" - readonly propertyNames: SchemaAST.AST - } - // Arrays Meta - readonly isUnique: { - readonly _tag: "isUnique" - } - // Declaration Meta - readonly isMinSize: { - readonly _tag: "isMinSize" - readonly minSize: number - } - readonly isMaxSize: { - readonly _tag: "isMaxSize" - readonly maxSize: number - } - readonly isSizeBetween: { - readonly _tag: "isSizeBetween" - readonly minimum: number - readonly maximum: number - } - } - - /** - * Union of all metadata payloads defined by `BuiltInMetaDefinitions`. - * - * @category utility types - * @since 4.0.0 - */ - export type BuiltInMeta = BuiltInMetaDefinitions[keyof BuiltInMetaDefinitions] - - /** - * Augmentable registry of schema filter metadata payloads. - * - * **Details** - * - * Extend this interface to add custom values accepted by annotation `meta` - * fields. - * - * @category models - * @since 4.0.0 - */ - export interface MetaDefinitions extends BuiltInMetaDefinitions {} - - /** - * Union of built-in and user-augmented schema filter metadata payloads. - * - * @category utility types - * @since 4.0.0 - */ - export type Meta = MetaDefinitions[keyof MetaDefinitions] } diff --git a/.context/effect/packages/effect/src/SchemaAST.ts b/.context/effect/packages/effect/src/SchemaAST.ts index fb4913ea3..cf8f4c5f1 100644 --- a/.context/effect/packages/effect/src/SchemaAST.ts +++ b/.context/effect/packages/effect/src/SchemaAST.ts @@ -13,16 +13,15 @@ import * as Arr from "./Array.ts" import * as Cause from "./Cause.ts" -import type * as Combiner from "./Combiner.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { format, formatPropertyKey } from "./Formatter.ts" -import { memoize } from "./Function.ts" +import { identity, memoize, memoizeIdempotent } from "./Function.ts" import { effectIsExit, iterateEager } from "./internal/effect.ts" import * as InternalRecord from "./internal/record.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" import * as InternalSchemaCause from "./internal/schema/cause.ts" -import * as Option from "./Option.ts" +import * as InternalParser from "./internal/schema/parser.ts" import * as Pipeable from "./Pipeable.ts" import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" @@ -451,6 +450,8 @@ export type Encoding = readonly [Link, ...Array] * transformations. * - `concurrency` — maximum number of async parse effects to run concurrently; * defaults to `1`, or use `"unbounded"`. + * - `reportInput` — includes rejected input values in value-bearing schema + * issues. * * @category options * @since 3.10.0 @@ -517,6 +518,35 @@ export interface ParseOptions { * @default 1 */ readonly concurrency?: number | "unbounded" | undefined + + /** + * Whether schema issues should retain and report rejected input values. + * + * **Details** + * + * When enabled, value-bearing issues created by the parser expose an `input` + * field. Built-in formatters may include reported input in default messages. + * The input is retained by reference rather than copied. + * + * **Gotchas** + * + * Enabling this option can retain or disclose secrets, personally + * identifiable information, and large object graphs. The `input` field is + * enumerable and may be included by object enumeration, spread, or + * serialization. Disabling it on a nested schema does not redact that value + * from an ancestor issue whose input reporting remains enabled. Issues + * returned directly by user-defined declarations, checks, transformations, + * and middleware are not modified; their authors decide whether to retain an + * input. To respect this option, pass the callback's input and parse options + * directly to a value-bearing issue constructor. Custom messages and + * annotations remain the caller's responsibility regardless of this option. + * Formatting an issue with `SchemaIssue.makeFormatterDefault()`, reading + * `SchemaError.message`, or formatting a Standard Schema failure can disclose + * retained input. + * + * @default false + */ + readonly reportInput?: boolean | undefined } /** @internal */ @@ -533,7 +563,7 @@ export const defaultParseOptions: ParseOptions = {} * * - `isOptional` — the property key may be absent from the input. * - `isMutable` — the property is `readonly` when `false`. - * - `defaultValue` — an {@link Encoding} applied during construction to + * - `constructorDefault` — a {@link Link} applied during construction to * supply missing values. * - `annotations` — key-level annotations (e.g. description of the key * itself). @@ -547,19 +577,19 @@ export class Context { readonly isOptional: boolean readonly isMutable: boolean /** Used for constructor default values (e.g. `withConstructorDefault` API) */ - readonly defaultValue: Encoding | undefined + readonly constructorDefault: Link | undefined readonly annotations: Schema.Annotations.Key | undefined constructor( isOptional: boolean, isMutable: boolean, /** Used for constructor default values (e.g. `withConstructorDefault` API) */ - defaultValue: Encoding | undefined = undefined, + constructorDefault: Link | undefined = undefined, annotations: Schema.Annotations.Key | undefined = undefined ) { this.isOptional = isOptional this.isMutable = isMutable - this.defaultValue = defaultValue + this.constructorDefault = constructorDefault this.annotations = annotations } } @@ -672,10 +702,10 @@ export class Declaration extends Base { } /** @internal */ getParser(): SchemaParser.Parser { - const run = this.run(this.typeParameters) - return (oinput, options) => { - if (Option.isNone(oinput)) return Effect.succeedNone - return Effect.mapEager(run(oinput.value, this, options), Option.some) + let run: ReturnType + return (input, options) => { + if (input === InternalParser.missing) return InternalParser.missingExit + return (run ??= this.run(this.typeParameters))(input, this, options) } } private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { @@ -816,7 +846,8 @@ export class Void extends Base { readonly _tag = "Void" /** @internal */ getParser() { - return fromAnyToConst(undefined) + const succeed = InternalParser.succeed(undefined) + return (input: unknown) => input === InternalParser.missing ? InternalParser.missingExit : succeed } /** @internal */ toCodecJson(): AST { @@ -1080,9 +1111,9 @@ function isTemplateLiteralPart(ast: AST): ast is TemplateLiteralPart { return true case "Literal": case "TemplateLiteral": - return ast.checks === undefined + return !ast.checks case "Union": - return ast.checks === undefined && ast.types.every(isTemplateLiteralPart) + return !ast.checks && ast.types.every(isTemplateLiteralPart) default: return false } @@ -1106,6 +1137,10 @@ export class TemplateLiteral extends Base { readonly parts: ReadonlyArray /** @internal */ readonly encodedParts: ReadonlyArray + /** @internal */ + readonly literals: ReadonlyArray + /** @internal */ + readonly suffixLengths: ReadonlyArray constructor( parts: ReadonlyArray, @@ -1116,25 +1151,40 @@ export class TemplateLiteral extends Base { ) { super(annotations, checks, encoding, context) const encodedParts: Array = [] + const literals: Array = [] for (const part of parts) { const encoded = toEncoded(part) if (isTemplateLiteralPart(encoded)) { encodedParts.push(encoded) + literals.push(encoded._tag === "Literal" ? globalThis.String(encoded.literal) : undefined) } else { throw new Error(`Invalid TemplateLiteral part ${encoded._tag}`) } } + const suffixLengths = new Array(encodedParts.length + 1) + suffixLengths[encodedParts.length] = 0 + for (let i = encodedParts.length - 1; i >= 0; i--) { + suffixLengths[i] = suffixLengths[i + 1] + (literals[i]?.length ?? 0) + } this.parts = parts this.encodedParts = encodedParts + this.literals = literals + this.suffixLengths = suffixLengths } /** @internal */ - getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { - const parser = recur(this.asTemplateLiteralParser()) - return (oinput: Option.Option, options: ParseOptions) => - Effect.mapBothEager(parser(oinput, options), { - onSuccess: () => oinput, - onFailure: (issue) => new SchemaIssue.Composite(this, oinput, [issue]) + getParser(compile: SchemaParser.Compiler): SchemaParser.Parser { + const parser = compile(this.asTemplateLiteralParser()) + return (input, options) => { + if (input === InternalParser.missing) return InternalParser.missingExit + const result = parser(input, options) + if ((result as Exit.Exit)._tag === "Success") { + return InternalParser.sameExit + } + return Effect.mapBothEager(result, { + onSuccess: () => input, + onFailure: (issue) => new SchemaIssue.Composite(this, [issue], input, options) }) + } } /** @internal */ getExpected(): string { @@ -1142,7 +1192,7 @@ export class TemplateLiteral extends Base { } /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined { - return segmentTemplateLiteralParts(this.encodedParts, s, options) === undefined ? undefined : s + return segmentTemplateLiteralParts(this, s, options) === undefined ? undefined : s } /** @internal */ asTemplateLiteralParser(): Arrays { @@ -1152,12 +1202,14 @@ export class TemplateLiteral extends Base { tuple, new SchemaTransformation.Transformation( SchemaGetter.transformOrFail((s: string, options) => { - const segments = segmentTemplateLiteralParts(this.encodedParts, s, options) - if (segments !== undefined) return Effect.succeed(segments) + const segments = segmentTemplateLiteralParts(this, s, options) + if (segments) return Effect.succeed(segments) return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(s), { - message: `Expected a string matching template literal parts, got ${format(s)}` - }) + new SchemaIssue.InvalidValue( + { expected: "a string matching template literal parts" }, + s, + options + ) ) }), SchemaGetter.transform((parts) => parts.join("")) @@ -1228,11 +1280,11 @@ export type LiteralValue = string | number | boolean | bigint * * **Example** (Creating a literal AST) * - * ```ts + * ```ts import.meta.vitest * import { SchemaAST } from "effect" * * const ast = new SchemaAST.Literal("active") - * console.log(ast.literal) // "active" + * ast.literal // => "active" * ``` * * @see {@link LiteralValue} @@ -1309,7 +1361,8 @@ export class String extends Base { } /** @internal */ matchPart(s: string, options: ParseOptions): string | undefined { - return applyTemplateLiteralPartChecks(this, s, options) + const checks = this.checks + return checks && !options.disableChecks && collectIssues(checks, s, undefined, this, options) ? undefined : s } /** @internal */ getExpected(): string { @@ -1366,20 +1419,24 @@ export class Number extends Base { return this._match(isStringFiniteRegExp, s, options) } private _match(regexp: RegExp, s: string, options: ParseOptions): number | undefined { - return regexp.test(s) - ? applyTemplateLiteralPartChecks(this, globalThis.Number(s), options) - : undefined + if (!regexp.test(s)) return undefined + const value = globalThis.Number(s) + if (options.disableChecks || !this.checks) return value + return collectIssues(this.checks, value, undefined, this, options) ? undefined : value } /** @internal */ toCodecJson(): AST { - if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) { + if ( + this.checks && + (hasCheck(this.checks, "effect/schema/isFinite") || hasCheck(this.checks, "effect/schema/isInt")) + ) { return this } - return replaceEncoding(this, [numberToJson]) + return replaceEncoding(this, [numberToJson(this.checks)]) } /** @internal */ toCodecStringTree(): AST { - if (this.checks && (hasCheck(this.checks, "isFinite") || hasCheck(this.checks, "isInt"))) { + if (this.toCodecJson() === this) { return replaceEncoding(this, [finiteToString]) } return replaceEncoding(this, [numberToString]) @@ -1390,16 +1447,25 @@ export class Number extends Base { } } -// oxlint-disable-next-line only-used-in-recursion - @gcanti what's this? :-) -function hasCheck(checks: ReadonlyArray>, tag: string): boolean { - return checks.some((c) => { - switch (c._tag) { - case "Filter": - return c.annotations?.meta?._tag === tag - case "FilterGroup": - return hasCheck(c.checks, tag) - } - }) +function hasCheck(checks: ReadonlyArray>, id: string): boolean { + return checks.some((check) => + check.annotations?.representation?.id === id || + (check._tag === "FilterGroup" && hasCheck(check.checks, id)) + ) +} + +function numberToJson(checks: Checks | undefined): Link { + const encodedFinite = !checks + ? finite + : appendChecks(finite, checks) + + return new Link( + new Union([encodedFinite, nonFiniteLiterals], "anyOf"), + new SchemaTransformation.Transformation( + SchemaGetter.Number(), + SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n)) + ) + ) } /** @@ -1481,7 +1547,8 @@ export class Symbol extends Base { } /** @internal */ matchKey(s: symbol, options: ParseOptions): symbol | undefined { - return applyTemplateLiteralPartChecks(this, s, options) + if (options.disableChecks || !this.checks) return s + return collectIssues(this.checks, s, undefined, this, options) ? undefined : s } /** @internal */ toCodecStringTree(): AST { @@ -1534,9 +1601,10 @@ export class BigInt extends Base { } /** @internal */ matchPart(s: string, options: ParseOptions): bigint | undefined { - return isStringBigIntRegExp.test(s) - ? applyTemplateLiteralPartChecks(this, globalThis.BigInt(s), options) - : undefined + if (!isStringBigIntRegExp.test(s)) return undefined + const value = globalThis.BigInt(s) + if (options.disableChecks || !this.checks) return value + return collectIssues(this.checks, value, undefined, this, options) ? undefined : value } /** @internal */ toCodecStringTree(): AST { @@ -1590,15 +1658,14 @@ export const bigInt = new BigInt() * * **Example** (Inspecting a tuple AST) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Tuple([Schema.String, Schema.Number]) * const ast = schema.ast * * if (SchemaAST.isArrays(ast)) { - * console.log(ast.elements.length) // 2 - * console.log(ast.rest.length) // 0 + * [ast.elements.length, ast.rest.length] // => [2, 0] * } * ``` * @@ -1630,58 +1697,71 @@ export class Arrays extends Base { this.rest = rest this.encodingChecks = encodingChecks - // A required element cannot follow an optional element. ts(1257) - const i = elements.findIndex(isOptional) - if (i !== -1 && (elements.slice(i + 1).some((e) => !isOptional(e)) || rest.length > 1)) { + let hasOptional = false + for (let i = 0; i < elements.length; i++) { + if (isOptional(elements[i])) { + hasOptional = true + } else if (hasOptional) { + throw new Error("A required element cannot follow an optional element. ts(1257)") + } + } + if (hasOptional && rest.length > 1) { throw new Error("A required element cannot follow an optional element. ts(1257)") } // An optional element cannot follow a rest element.ts(1266) - if (rest.length > 1 && rest.slice(1).some(isOptional)) { - throw new Error("An optional element cannot follow a rest element. ts(1266)") + for (let i = 1; i < rest.length; i++) { + if (isOptional(rest[i])) { + throw new Error("An optional element cannot follow a rest element. ts(1266)") + } } } /** @internal */ - getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + getParser( + compile: SchemaParser.Compiler, + compileConstructorDefault: SchemaParser.Compiler = compile + ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this - const elements = ast.elements.map((ast) => ({ ast, parser: recur(ast) })) - const rest = ast.rest.map((ast) => ({ ast, parser: recur(ast) })) - const elementLen = elements.length - - const [head, ...tail] = rest - const tailLen = tail.length + type ElementParser = { readonly ast: AST; readonly parser: SchemaParser.Parser } + let elements: Array | undefined + let rest: Array | undefined + const elementLen = ast.elements.length + const tailLen = Math.max(0, ast.rest.length - 1) function getParser( tailThreshold: number, index: number ): { readonly ast: AST; readonly parser: SchemaParser.Parser } { if (index < elementLen) { - return elements[index] + return elements![index] } else if (index >= tailThreshold) { - return tail[index - tailThreshold] + return rest![index - tailThreshold + 1] } - return head + return rest![0] } - return Effect.fnUntracedEager(function*(oinput, options) { - if (oinput._tag === "None") { - return oinput + return Effect.fnUntracedEager(function*(input, options) { + if (input === InternalParser.missing) { + return InternalParser.missing } - const input = oinput.value // If the input is not an array, return early with an error if (!Array.isArray(input)) { - return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) + } + if (!elements) { + elements = ast.elements.map((ast) => ({ ast, parser: compileConstructorDefault(ast) })) + rest = ast.rest.map((ast) => ({ ast, parser: compileConstructorDefault(ast) })) } const len = input.length const state = { ast, getParser, - oinput, + input, len, - tailThreshold: resolveTailThreshold(len, elementLen, tailLen), + tailThreshold: Math.max(elementLen, len - tailLen), output: new globalThis.Array(len), issues: undefined as Arr.NonEmptyArray | undefined, options @@ -1698,19 +1778,24 @@ export class Arrays extends Base { // --------------------------------------------- if (ast.rest.length === 0 && len > elementLen) { for (let i = elementLen; i <= len - 1; i++) { - const issue = new SchemaIssue.Pointer([i], new SchemaIssue.UnexpectedKey(ast, input[i])) + const unexpected = new SchemaIssue.UnexpectedKey(ast, input[i], options) + const issue = new SchemaIssue.Pointer([i], unexpected) if (options.errors === "all") { if (state.issues) state.issues.push(issue) else state.issues = [issue] } else { - return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue])) + return yield* Effect.fail( + new SchemaIssue.Composite(ast, [issue], input, options) + ) } } } if (state.issues) { - return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues)) + return yield* Effect.fail( + new SchemaIssue.Composite(ast, state.issues, input, options) + ) } - return Option.some(state.output) + return state.output }) } private _rebuild(recur: (ast: AST) => AST, checks: Checks | undefined, encodingChecks: Checks | undefined) { @@ -1745,7 +1830,7 @@ export class Arrays extends Base { } const parseArray = iterateEager<{ readonly ast: AST - readonly oinput: Option.Option + readonly input: unknown readonly len: number readonly getParser: ( tailThreshold: number, @@ -1757,14 +1842,18 @@ const parseArray = iterateEager<{ issues: Array | undefined }, unknown>()({ onItem(s, item, i) { - const value = i < s.len ? Option.some(item) : Option.none() + const value = i < s.len ? item : InternalParser.missing return s.getParser(s.tailThreshold, i).parser(value, s.options) }, - step(s, _, exit, i) { + step(s, item, exit, i) { if (exit._tag === "Failure") { return wrapPropertyKeyIssue(s, s.ast, i, exit) - } else if (exit.value._tag === "Some") { - s.output[i] = exit.value.value + } + const value = exit === InternalParser.sameExit + ? item + : (exit as InternalParser.Success)[InternalParser.args] + if (value !== InternalParser.missing) { + s.output[i] = value } else { const p = s.getParser(s.tailThreshold, i) if (isOptional(p.ast)) return @@ -1773,20 +1862,14 @@ const parseArray = iterateEager<{ if (s.issues) s.issues.push(issue) else s.issues = [issue] } else { - return Exit.fail(new SchemaIssue.Composite(s.ast, s.oinput, [issue])) + return Exit.fail( + new SchemaIssue.Composite(s.ast, [issue], s.input, s.options) + ) } } } }) -function resolveTailThreshold( - inputLen: number, - elementLen: number, - tailLen: number -) { - return Math.max(elementLen, inputLen - tailLen) -} - const resolveConcurrency = (value: number | "unbounded" | undefined) => { value = value === "unbounded" ? Infinity : value ?? 1 return value > 1 ? { concurrency: value } : undefined @@ -1794,7 +1877,7 @@ const resolveConcurrency = (value: number | "unbounded" | undefined) => { const wrapPropertyKeyIssue = ( s: { - readonly oinput: Option.Option + readonly input: unknown readonly options: ParseOptions issues: Array | undefined }, @@ -1810,7 +1893,13 @@ const wrapPropertyKeyIssue = ( return Exit.failCause( Cause.map( exit.cause, - (issue) => new SchemaIssue.Composite(ast, s.oinput, [new SchemaIssue.Pointer([key], issue)]) + (issue) => + new SchemaIssue.Composite( + ast, + [new SchemaIssue.Pointer([key], issue)], + s.input, + s.options + ) ) ) } @@ -1819,7 +1908,9 @@ const wrapPropertyKeyIssue = ( if (s.issues) s.issues.push(pointer) else s.issues = [pointer] } else { - return Exit.fail(new SchemaIssue.Composite(ast, s.oinput, [pointer])) + return Exit.fail( + new SchemaIssue.Composite(ast, [pointer], s.input, s.options) + ) } } @@ -1887,37 +1978,6 @@ export class PropertySignature { } } -/** - * Represents a bidirectional merge strategy for index signature key-value pairs. - * - * **Details** - * - * Used by {@link IndexSignature} when the same key appears multiple times - * (e.g. from `Schema.extend` or overlapping records). Provides separate - * `decode` and `encode` combiners that determine how duplicate entries are - * merged. - * - * @see {@link IndexSignature} - * @category models - * @since 4.0.0 - */ -export class KeyValueCombiner { - readonly decode: Combiner.Combiner | undefined - readonly encode: Combiner.Combiner | undefined - - constructor( - decode: Combiner.Combiner | undefined, - encode: Combiner.Combiner | undefined - ) { - this.decode = decode - this.encode = encode - } - /** @internal */ - flip(): KeyValueCombiner { - return new KeyValueCombiner(this.encode, this.decode) - } -} - type IndexSignatureParameter = | String | Number @@ -1956,7 +2016,6 @@ function isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter { * - `parameter` — the key type AST (e.g. {@link String} for `string` keys, * {@link TemplateLiteral} for patterned keys). * - `type` — the value type SchemaAST. - * - `merge` — optional {@link KeyValueCombiner} for handling duplicate keys. * * **Gotchas** * @@ -1971,19 +2030,16 @@ function isIndexSignatureParameter(ast: AST): ast is IndexSignatureParameter { export class IndexSignature { readonly parameter: IndexSignatureParameter readonly type: AST - readonly merge: KeyValueCombiner | undefined constructor( parameter: AST, - type: AST, - merge: KeyValueCombiner | undefined + type: AST ) { if (!isIndexSignatureParameter(parameter)) { throw new Error(`Invalid index signature parameter ${parameter._tag}`) } this.parameter = parameter this.type = type - this.merge = merge if (isOptional(type) && !containsUndefined(type)) { throw new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.") } @@ -2014,17 +2070,14 @@ export class IndexSignature { * * **Example** (Inspecting a struct AST) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Struct({ name: Schema.String }) * const ast = schema.ast * * if (SchemaAST.isObjects(ast)) { - * for (const ps of ast.propertySignatures) { - * console.log(ps.name, ps.type._tag) - * } - * // "name" "String" + * ast.propertySignatures.map((ps) => [ps.name, ps.type._tag]) // => [["name", "String"]] * } * ``` * @@ -2062,101 +2115,126 @@ export class Objects extends Base { } } /** @internal */ - getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + getParser( + compile: SchemaParser.Compiler, + compileConstructorDefault: SchemaParser.Compiler = compile + ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this const expectedKeys: Array = [] - const expectedKeysSet = new Set() - const properties: Array<{ - readonly ps: PropertySignature | IndexSignature - readonly parser: SchemaParser.Parser - readonly name: PropertyKey - readonly type: AST - }> = [] for (const ps of ast.propertySignatures) { expectedKeys.push(ps.name) - expectedKeysSet.add(ps.name) - properties.push({ - ps, - parser: recur(ps.type), - name: ps.name, - type: ps.type - }) } + const hasProperties = expectedKeys.length const indexCount = ast.indexSignatures.length + let expectedKeysSet = hasProperties && indexCount ? new Set(expectedKeys) : undefined // --------------------------------------------- // handle empty struct // --------------------------------------------- - if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + if (!hasProperties && !indexCount) { return fromRefinement(ast, Predicate.isNotNullish) } - const parseIndexes = indexCount > 0 ? - iterateEager<{ - readonly oinput: Option.Option - readonly input: Record - readonly options: ParseOptions - readonly out: Record - issues: Array | undefined - }, [key: PropertyKey, is: IndexSignature]>()({ - onItem: Effect.fnUntracedEager(function*( - s, - [key, is] - ) { - const parserKey = recur(parameterFromPropertyKey(is.parameter)) - const effKey = parserKey(Option.some(key), s.options) - const exitKey = (effectIsExit(effKey) ? effKey : yield* Effect.exit(effKey)) as Exit.Exit< - Option.Option, - SchemaIssue.Issue - > - if (exitKey._tag === "Failure") { - const eff = wrapPropertyKeyIssue(s, ast, key, exitKey) - if (eff) yield* eff - return - } - - const value: Option.Option = Option.some(s.input[key]) - const parserValue = recur(is.type) - const effValue = parserValue(value, s.options) - const exitValue = effectIsExit(effValue) ? effValue : yield* Effect.exit(effValue) - if (exitValue._tag === "Failure") { - const eff = wrapPropertyKeyIssue(s, ast, key, exitValue) - if (eff) yield* eff - return - } else if (exitKey.value._tag === "Some" && exitValue.value._tag === "Some") { - const k2 = exitKey.value.value - if (expectedKeysSet.has(key) || expectedKeysSet.has(k2)) { - return - } - const v2 = exitValue.value.value - if (is.merge && is.merge.decode && Object.hasOwn(s.out, k2)) { - const [k, v] = is.merge.decode.combine([k2, s.out[k2]], [k2, v2]) - InternalRecord.set(s.out, k, v) - } else { - InternalRecord.set(s.out, k2, v2) - } - } - }), + let properties: Array | undefined + let indexes: + | Array<{ + readonly is: IndexSignature + readonly parserKey: SchemaParser.Parser + readonly parserValue: SchemaParser.Parser + }> + | undefined + type Index = NonNullable[number] + const finishIndex = ( + s: ObjectParserState, + key: PropertyKey, + k2: PropertyKey | typeof InternalParser.missing, + inputValue: unknown, + exitValue: Exit.Exit + ): Effect.Effect => { + if (exitValue._tag === "Failure") { + return wrapPropertyKeyIssue(s, ast, key, exitValue) ?? Exit.void + } + const value = exitValue === InternalParser.sameExit + ? inputValue + : (exitValue as InternalParser.Success)[InternalParser.args] + if (k2 !== InternalParser.missing && value !== InternalParser.missing) { + if (hasProperties && (expectedKeysSet!.has(key) || expectedKeysSet!.has(k2))) return Exit.void + InternalRecord.assignProperty(s.out, k2, value) + } + return Exit.void + } + const parseIndex = ( + s: ObjectParserState, + key: PropertyKey, + index: Index, + exitKey?: Exit.Exit + ): Effect.Effect => { + if (!exitKey) { + const eff = index.parserKey(key, s.options) + if (!effectIsExit(eff)) { + return Effect.flatMap(Effect.exit(eff), (exit) => parseIndex(s, key, index, exit)) + } + exitKey = eff + } + if (exitKey._tag === "Failure") { + return wrapPropertyKeyIssue(s, ast, key, exitKey) ?? Exit.void + } + const k2 = exitKey === InternalParser.sameExit + ? key + : (exitKey as InternalParser.Success)[InternalParser.args] + const inputValue = s.input[key] + const result = index.parserValue(inputValue, s.options) + return effectIsExit(result) + ? finishIndex(s, key, k2, inputValue, result) + : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, k2, inputValue, exit)) + } + const parseStringIndex = ( + s: ObjectParserState, + key: PropertyKey, + index: Index + ): Effect.Effect => { + const inputValue = s.input[key] + const result = index.parserValue(inputValue, s.options) + return effectIsExit(result) + ? finishIndex(s, key, key, inputValue, result) + : Effect.flatMap(Effect.exit(result), (exit) => finishIndex(s, key, key, inputValue, exit)) + } + const parseIndexes = indexCount ? + iterateEager()({ + onItem: (s, [key, index]) => parseIndex(s, key, index), step: (_s, _, exit: Exit.Exit) => exit._tag === "Failure" ? exit : undefined }) : undefined - return Effect.fnUntracedEager(function*(oinput, options) { - if (oinput._tag === "None") { - return oinput + return Effect.fnUntracedEager(function*(input, options) { + if (input === InternalParser.missing) { + return InternalParser.missing } - const input = oinput.value as Record // If the input is not a record, return early with an error if (!(typeof input === "object" && input !== null && !Array.isArray(input))) { - return yield* Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + return yield* Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) + } + if (!properties) { + properties = ast.propertySignatures.map((ps) => ({ + parser: compileConstructorDefault(ps.type), + name: ps.name, + type: ps.type + })) + indexes = indexCount + ? ast.indexSignatures.map((is) => ({ + is, + parserKey: compile(parameterFromPropertyKey(is.parameter)), + parserValue: compileConstructorDefault(is.type) + })) + : undefined } + const record = input as Record const out: Record = {} const state = { ast, - oinput, - input, + input: record, out, issues: undefined as Arr.NonEmptyArray | undefined, options @@ -2169,14 +2247,16 @@ export class Objects extends Base { // handle excess properties // --------------------------------------------- let inputKeys: Array | undefined - if (ast.indexSignatures.length === 0 && (onExcessPropertyError || onExcessPropertyPreserve)) { - inputKeys = Reflect.ownKeys(input) + if (!indexCount && (onExcessPropertyError || onExcessPropertyPreserve)) { + expectedKeysSet ??= new Set(expectedKeys) + inputKeys = Reflect.ownKeys(record) for (let i = 0; i < inputKeys.length; i++) { const key = inputKeys[i] if (!expectedKeysSet.has(key)) { // key is unexpected if (onExcessPropertyError) { - const issue = new SchemaIssue.Pointer([key], new SchemaIssue.UnexpectedKey(ast, input[key])) + const unexpected = new SchemaIssue.UnexpectedKey(ast, record[key], options) + const issue = new SchemaIssue.Pointer([key], unexpected) if (errorsAllOption) { if (state.issues) { state.issues.push(issue) @@ -2185,11 +2265,13 @@ export class Objects extends Base { } continue } else { - return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, [issue])) + return yield* Effect.fail( + new SchemaIssue.Composite(ast, [issue], input, options) + ) } } else { // preserve key - InternalRecord.set(out, key, input[key]) + InternalRecord.assignProperty(out, key, record[key]) } } } @@ -2200,20 +2282,34 @@ export class Objects extends Base { // --------------------------------------------- // handle property signatures // --------------------------------------------- - const eff = parseProperties(state, properties, concurrency) - if (eff) yield* eff + if (hasProperties) { + const eff = parseProperties(state, properties!, concurrency) + if (eff) yield* eff + } // --------------------------------------------- // handle index signatures // --------------------------------------------- - if (parseIndexes) { - const keyPairs = Arr.empty<[PropertyKey, IndexSignature]>() + if (indexCount && !concurrency) { + for (let i = 0; i < indexCount; i++) { + const index = indexes![i] + const parse = index.is.parameter === string ? parseStringIndex : parseIndex + const keys = index.is.parameter === string + ? Object.keys(record) + : getIndexSignatureKeys(record, index.is.parameter, options) + for (let j = 0; j < keys.length; j++) { + const eff = parse(state, keys[j], index) + if (!effectIsExit(eff)) yield* eff + else if (eff._tag === "Failure") return yield* eff as Exit.Exit + } + } + } else if (parseIndexes) { + const keyPairs = Arr.empty<[PropertyKey, Index]>() for (let i = 0; i < indexCount; i++) { - const is = ast.indexSignatures[i] - const keys = getIndexSignatureKeys(input, is.parameter, options) + const index = indexes![i] + const keys = getIndexSignatureKeys(record, index.is.parameter, options) for (let j = 0; j < keys.length; j++) { - const key = keys[j] - keyPairs.push([key, is]) + keyPairs.push([keys[j], index]) } } const eff = parseIndexes(state, keyPairs, concurrency) @@ -2221,26 +2317,27 @@ export class Objects extends Base { } if (state.issues) { - return yield* Effect.fail(new SchemaIssue.Composite(ast, oinput, state.issues)) + return yield* Effect.fail( + new SchemaIssue.Composite(ast, state.issues, input, options) + ) } if (options.propertyOrder === "original") { // preserve input keys order - const keys = (inputKeys ?? Reflect.ownKeys(input)).concat(expectedKeys) + const keys = (inputKeys ?? Reflect.ownKeys(record)).concat(expectedKeys) const preserved: Record = {} for (const key of keys) { if (Object.hasOwn(out, key)) { - InternalRecord.set(preserved, key, out[key]) + InternalRecord.assignProperty(preserved, key, out[key]) } } - return Option.some(preserved) + return preserved } - return Option.some(out) + return out }) } private _rebuild( recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST, - flipMerge: boolean, checks: Checks | undefined, encodingChecks: Checks | undefined ): Objects { @@ -2252,10 +2349,9 @@ export class Objects extends Base { const indexes = mapOrSame(this.indexSignatures, (is) => { const p = recurParameter(is.parameter) const t = recur(is.type) - const merge = flipMerge ? is.merge?.flip() : is.merge - return p === is.parameter && t === is.type && merge === is.merge + return p === is.parameter && t === is.type ? is - : new IndexSignature(p, t, merge) + : new IndexSignature(p, t) }) return props === this.propertySignatures && indexes === this.indexSignatures && checks === this.checks && @@ -2273,11 +2369,11 @@ export class Objects extends Base { } /** @internal */ flip(recur: (ast: AST) => AST): AST { - return this._rebuild(recur, recur, true, this.encodingChecks, this.checks) + return this._rebuild(recur, recur, this.encodingChecks, this.checks) } /** @internal */ recur(recur: (ast: AST) => AST, recurParameter: (ast: AST) => AST = recur): AST { - return this._rebuild(recur, recurParameter, false, this.checks, this.encodingChecks) + return this._rebuild(recur, recurParameter, this.checks, this.encodingChecks) } /** @internal */ getExpected(): string { @@ -2286,42 +2382,41 @@ export class Objects extends Base { } } +type ObjectParserState = { + readonly ast: Objects + readonly input: Record + readonly options: ParseOptions + readonly out: Record + issues: Array | undefined +} + type ParsedProperty = { - readonly ps: PropertySignature | IndexSignature readonly parser: SchemaParser.Parser readonly name: PropertyKey readonly type: AST } -const parseProperties = iterateEager<{ - readonly ast: AST - readonly oinput: Option.Option - readonly input: Record - readonly options: ParseOptions - readonly out: Record - issues: Array | undefined -}, ParsedProperty>()({ - onItem( - s: { - readonly oinput: Option.Option - readonly input: Record - readonly options: ParseOptions - readonly out: Record - issues: Array | undefined - }, - p - ) { - const value: Option.Option = Object.hasOwn(s.input, p.name) - ? Option.some(s.input[p.name]) - : Option.none() +const parseProperties = iterateEager()({ + onItem(s, p) { + if (!Object.hasOwn(s.input, p.name)) { + return p.parser(InternalParser.missing, s.options) + } + const value = s.input[p.name] + InternalRecord.assignProperty(s.out, p.name, value) return p.parser(value, s.options) }, step(s, p, exit) { if (exit._tag === "Failure") { return wrapPropertyKeyIssue(s, s.ast, p.name, exit) - } else if (exit.value._tag === "Some") { - InternalRecord.set(s.out, p.name, exit.value.value) - } else if (!isOptional(p.type)) { + } + if (exit === InternalParser.sameExit) return + const value = (exit as InternalParser.Success)[InternalParser.args] + if (value !== InternalParser.missing) { + InternalRecord.assignProperty(s.out, p.name, value) + return + } + delete s.out[p.name] + if (!isOptional(p.type)) { const issue = new SchemaIssue.Pointer([p.name], new SchemaIssue.MissingKey(p.type.context?.annotations)) if (s.options.errors === "all") { if (s.issues) s.issues.push(issue) @@ -2329,7 +2424,7 @@ const parseProperties = iterateEager<{ return } else { return Exit.fail( - new SchemaIssue.Composite(s.ast, s.oinput, [issue]) + new SchemaIssue.Composite(s.ast, [issue], s.input, s.options) ) } } @@ -2422,6 +2517,21 @@ export type Sentinel = { readonly literal: LiteralValue | symbol } +const toCandidate = memoizeIdempotent((ast: AST): AST => { + while (true) { + if (isSuspend(ast)) return unknown + const encoding = ast.encoding + if (!encoding) { + // Index signature parameters do not participate in union selection. + return (ast as any).recur?.(toCandidate, identity) ?? ast + } + if ( + encoding.some((link) => link.transformation._tag === "Middleware" && link.transformation.decode !== identity) + ) return unknown + ast = encoding[encoding.length - 1].to + } +}) + function getCandidateTypes(ast: AST): ReadonlyArray { switch (ast._tag) { case "Null": @@ -2447,7 +2557,7 @@ function getCandidateTypes(ast: AST): ReadonlyArray { case "Objects": return ast.propertySignatures.length || ast.indexSignatures.length ? ["object"] - : ["object", "array"] + : ["string", "number", "boolean", "symbol", "bigint", "object", "array", "function"] case "Enum": return Array.from(new Set(ast.enums.map(([, v]) => typeof v))) case "Literal": @@ -2471,12 +2581,12 @@ function getCandidateTypes(ast: AST): ReadonlyArray { } /** @internal */ -export function collectSentinels(ast: AST): Array { +export function collectSentinels(ast: AST): ReadonlyArray { switch (ast._tag) { default: return [] case "Declaration": { - const s = ast.annotations?.["~sentinels"] + const s = ast.annotations?.[InternalAnnotations.SENTINELS_ANNOTATION_KEY] return Array.isArray(s) ? s : [] } case "Objects": @@ -2493,63 +2603,182 @@ export function collectSentinels(ast: AST): Array { return [] }) case "Arrays": - return ast.elements.flatMap((e, i) => { - return isLiteral(e) && !isOptional(e) - ? [{ key: i, literal: e.literal }] - : [] + return ast.elements.flatMap((e, i): Array => { + if (!isOptional(e)) { + if (isLiteral(e)) { + return [{ key: i, literal: e.literal }] + } + if (isUniqueSymbol(e)) { + return [{ key: i, literal: e.symbol }] + } + } + return [] }) + case "Union": { + if (ast.types.length === 0) return [] + const members = ast.types.map((type) => collectSentinels(toCandidate(type))) + return members[0].filter((s) => + members.every((sentinels) => sentinels.some((o) => o.key === s.key && o.literal === s.literal)) + ) + } case "Suspend": return collectSentinels(ast.thunk()) } } -type CandidateIndex = { - byType?: { [K in Type]?: Array } - bySentinel?: Map>> - otherwise?: { [K in Type]?: Array } -} +type CandidateIndex = (input: any, isConstructor: boolean) => ReadonlyArray +type SentinelEntry = readonly [ + byValue: Map>, + all: Set +] +type SentinelIndex = Map const candidateIndexCache = new WeakMap, CandidateIndex>() +const emptyCandidates: ReadonlyArray = Object.freeze([]) function getIndex(types: ReadonlyArray): CandidateIndex { - let idx = candidateIndexCache.get(types) - if (idx) return idx - - idx = {} + let index = candidateIndexCache.get(types) + if (index) return index + + let bySentinel: SentinelIndex | undefined + let sentinelCandidateCount = 0 + let otherwise: { [K in Type]?: Array } | undefined + let literalCandidates: Map> | undefined + let onlyLiterals = true for (let i = 0; i < types.length; i++) { const a = types[i] - const encoded = toEncoded(a) + const encoded = toCandidate(a) if (isNever(encoded)) continue - const candidateTypes = getCandidateTypes(encoded) - const sentinels = collectSentinels(encoded) + if (onlyLiterals) { + if (isLiteral(encoded) || isUniqueSymbol(encoded)) { + literalCandidates ??= new Map() + const literal = isLiteral(encoded) ? encoded.literal : encoded.symbol + let arr = literalCandidates.get(literal) + if (!arr) literalCandidates.set(literal, arr = []) + arr.push(a) + } else { + onlyLiterals = false + } + } - // by-type (always filled – cheap primary filter) - idx.byType ??= {} - for (const t of candidateTypes) (idx.byType[t] ??= []).push(i) + const sentinels = collectSentinels(encoded) - if (sentinels.length > 0) { // discriminated variants - idx.bySentinel ??= new Map() + if (sentinels.length) { // discriminated variants + bySentinel ??= new Map() + sentinelCandidateCount++ for (const { key, literal } of sentinels) { - let m = idx.bySentinel.get(key) - if (!m) idx.bySentinel.set(key, m = new Map()) - let arr = m.get(literal) - if (!arr) m.set(literal, arr = []) - arr.push(i) + let entry = bySentinel.get(key) + if (!entry) bySentinel.set(key, entry = [new Map(), new Set()]) + entry[1].add(i) + let indexes = entry[0].get(literal) + if (!indexes) entry[0].set(literal, indexes = new Set()) + indexes.add(i) } } else { // non-discriminated - idx.otherwise ??= {} - for (const t of candidateTypes) (idx.otherwise[t] ??= []).push(i) + otherwise ??= {} + const candidateTypes = getCandidateTypes(encoded) + for (const t of candidateTypes) (otherwise[t] ??= []).push(i) } } - candidateIndexCache.set(types, idx) - return idx + if (onlyLiterals && literalCandidates) { + literalCandidates.forEach(Object.freeze) + index = (input) => literalCandidates.get(input) ?? emptyCandidates + } else if (bySentinel?.size === 1 && !otherwise) { + const [key, [byValue]] = bySentinel.entries().next().value! + const candidates = byValue as unknown as Map> + for (const [literal, indexes] of byValue) { + candidates.set(literal, Object.freeze(Array.from(indexes, (index) => types[index]))) + } + index = (input, isConstructor) => { + if (Predicate.isObjectKeyword(input)) { + const value = Object.hasOwn(input, key) ? (input as any)[key] : undefined + if (value !== undefined) return candidates.get(value) ?? emptyCandidates + if (isConstructor) return types + } + return emptyCandidates + } + } else if (bySentinel) { + // A key owned by every discriminated candidate is safe to use as the initial selector: no candidate can + // be excluded merely because it uses a different sentinel key. Prefer the key with the most distinct values + // to minimize the matching bucket. + let commonSentinel: [PropertyKey, SentinelEntry] | undefined + for (const entry of bySentinel) { + if ( + (!commonSentinel || entry[1][0].size > commonSentinel[1][0].size) && + entry[1][1].size === sentinelCandidateCount + ) { + commonSentinel = entry + } + } + + index = (input, isConstructor) => { + const runtimeType: Type = input === null ? "null" : Array.isArray(input) ? "array" : typeof input + const base = otherwise?.[runtimeType] ?? emptyCandidates + if (!Predicate.isObjectKeyword(input)) return base.map((i) => types[i]) + + // Non-discriminated candidates are runtime-type fallbacks and are never removed by sentinel checks. + const selected = new Set(base) + let directKey: PropertyKey | undefined + // An observed common key can seed the selection directly; an unknown value rules out every + // discriminated candidate. + if (commonSentinel) { + const [key, [byValue]] = commonSentinel + const hasKey = Object.hasOwn(input, key) + const value = hasKey ? (input as any)[key] : undefined + if (hasKey && (!isConstructor || value !== undefined)) { + const match = byValue.get(value) + if (!match) return base.map((i) => types[i]) + for (const i of match) selected.add(i) + directKey = key + } + } + + // Without an observed common key, collect positive matches from every sentinel. Constructor mode treats + // absent and undefined keys as unconstrained and therefore selects every candidate that owns the key. + if (directKey === undefined) { + for (const [key, [byValue, all]] of bySentinel) { + const hasKey = Object.hasOwn(input, key) + const value = hasKey ? (input as any)[key] : undefined + if (hasKey && (!isConstructor || value !== undefined)) { + const match = byValue.get(value) + if (match) { + for (const i of match) selected.add(i) + } + } else if (isConstructor) { + for (const i of all) selected.add(i) + } + } + } + // Missing keys are neutral. An observed key rejects only selected candidates that own it and do not match. + for (const [key, [byValue, all]] of bySentinel) { + if (key === directKey) continue + const hasKey = Object.hasOwn(input, key) + const value = hasKey ? (input as any)[key] : undefined + if (hasKey && (!isConstructor || value !== undefined)) { + const match = byValue.get(value) + for (const i of selected) { + if (all.has(i) && !match?.has(i)) selected.delete(i) + } + } + } + return Array.from(selected).sort((a, b) => a - b).map((i) => types[i]) + } + } else { + index = (input) => { + const runtimeType: Type = input === null ? "null" : Array.isArray(input) ? "array" : typeof input + return (otherwise?.[runtimeType] ?? emptyCandidates).map((i) => types[i]).filter(filterLiterals(input)) + } + } + + candidateIndexCache.set(types, index) + return index } function filterLiterals(input: any) { return (ast: AST) => { - const encoded = toEncoded(ast) + const encoded = toCandidate(ast) return encoded._tag === "Literal" ? encoded.literal === input : encoded._tag === "UniqueSymbol" ? @@ -2564,30 +2793,12 @@ function filterLiterals(input: any) { * * @internal */ -export function getCandidates(input: any, types: ReadonlyArray): ReadonlyArray { - const idx = getIndex(types) - const runtimeType: Type = input === null ? "null" : Array.isArray(input) ? "array" : typeof input - - // 1. Try sentinel-based dispatch (most selective) - if (idx.bySentinel) { - const base = idx.otherwise?.[runtimeType] ?? [] - if (runtimeType === "object" || runtimeType === "array") { - const selected = new Set(base) - for (const [k, m] of idx.bySentinel) { - if (Object.hasOwn(input, k)) { - const match = m.get((input as any)[k]) - if (match) { - for (const candidate of match) selected.add(candidate) - } - } - } - return Array.from(selected).sort((a, b) => a - b).map((i) => types[i]).filter(filterLiterals(input)) - } - return base.map((i) => types[i]) - } - - // 2. Fallback: runtime-type dispatch only - return (idx.byType?.[runtimeType] ?? []).map((i) => types[i]).filter(filterLiterals(input)) +export function getCandidates( + input: any, + types: ReadonlyArray, + isConstructor = false +): ReadonlyArray { + return getIndex(types)(input, isConstructor) } /** @@ -2605,15 +2816,14 @@ export function getCandidates(input: any, types: ReadonlyArray): ReadonlyAr * * **Example** (Inspecting a union AST) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.Union([Schema.String, Schema.Number]) * const ast = schema.ast * * if (SchemaAST.isUnion(ast)) { - * console.log(ast.types.length) // 2 - * console.log(ast.mode) // "anyOf" + * [ast.types.length, ast.mode] // => [2, "anyOf"] * } * ``` * @@ -2642,38 +2852,46 @@ export class Union extends Base { this.encodingChecks = encodingChecks } /** @internal */ - getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { + getParser( + compile: SchemaParser.Compiler, + compileConstructorDefault?: SchemaParser.Compiler + ): SchemaParser.Parser { // oxlint-disable-next-line @typescript-eslint/no-this-alias const ast = this - return (oinput, options) => { - if (oinput._tag === "None") { - return Effect.succeed(oinput) + return (input, options) => { + if (input === InternalParser.missing) { + return InternalParser.missingExit + } + const candidates = getCandidates(input, ast.types, compileConstructorDefault !== undefined) + + if (candidates.length === 1) { + const result = compile(candidates[0])(input, options) + if ((result as Exit.Exit)._tag === "Success") return result + return effectIsExit(result) + ? failSingleUnionCandidate(ast, (result as Exit.Failure).cause, input, options) + : Effect.catchCause(result, (cause) => failSingleUnionCandidate(ast, cause, input, options)) } - const input = oinput.value - const candidates = getCandidates(input, ast.types) const state = { ast, - recur, - oinput, + compile, input, out: undefined, - successes: [], + successes: ast.mode === "oneOf" ? [] : undefined, issues: undefined as Arr.NonEmptyArray | undefined, options } const concurrency = resolveConcurrency(options?.concurrency) const eff = parseUnion(state, candidates, concurrency ? { ...concurrency, orderedStep: true } : undefined) if (!eff) { - return state.out - ? Effect.succeed(state.out) - : Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? [])) + if (state.out) return state.out + return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options)) } - return Effect.flatMap(eff, (_) => { - return state.out - ? Effect.succeed(state.out) - : Effect.fail(new SchemaIssue.AnyOf(ast, input, state.issues ?? [])) + return Effect.flatMapEager(eff, (_) => { + if (state.out === InternalParser.sameExit) return Effect.succeed(input) + if (state.out) return state.out + return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options)) }) } } @@ -2738,19 +2956,29 @@ export class Union extends Base { } } +function failSingleUnionCandidate( + ast: Union, + cause: Cause.Cause, + input: unknown, + options: ParseOptions +) { + const issue = InternalSchemaCause.getSchemaIssue(cause) + if (!issue) return Exit.failCause(cause) + return Exit.fail(new SchemaIssue.AnyOf(ast, [issue], input, options)) +} + const parseUnion = iterateEager<{ - readonly recur: (ast: AST) => SchemaParser.Parser + readonly compile: (ast: AST) => SchemaParser.Parser readonly ast: Union - readonly oinput: Option.Option readonly input: unknown readonly options: ParseOptions - out: Option.Option | undefined - successes: Array + out: Exit.Success | undefined + readonly successes: Array | undefined issues: Array | undefined }, AST>()({ onItem(s, ast) { - const parser = s.recur(ast) - return parser(s.oinput, s.options) + const parser = s.compile(ast) + return parser(s.input, s.options) }, step(s, candidate, exit) { if (exit._tag === "Failure") { @@ -2761,13 +2989,14 @@ const parseUnion = iterateEager<{ if (s.issues) s.issues.push(issue) else s.issues = [issue] } else { - if (s.out && s.ast.mode === "oneOf") { + if (s.out && s.successes) { s.successes.push(candidate) - return Exit.fail(new SchemaIssue.OneOf(s.ast, s.input, s.successes)) + return Exit.fail(new SchemaIssue.OneOf(s.ast, s.successes, s.input, s.options)) } - s.out = exit.value - s.successes.push(candidate) - if (s.ast.mode === "anyOf") { + s.out = exit + if (s.successes) { + s.successes.push(candidate) + } else { return Exit.void } } @@ -2780,14 +3009,6 @@ const nonFiniteLiterals = new Union([ new Literal("NaN") ], "anyOf") -const numberToJson = new Link( - new Union([number, nonFiniteLiterals], "anyOf"), - new SchemaTransformation.Transformation( - SchemaGetter.Number(), - SchemaGetter.transform((n) => globalThis.Number.isFinite(n) ? n : globalThis.String(n)) - ) -) - function formatIsMutable(isMutable: boolean | undefined): string { return isMutable ? "" : "readonly " } @@ -2821,7 +3042,7 @@ export function memoizeThunk(f: () => A): () => A { * * **Example** (Defining recursive schema ASTs) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * interface Category { @@ -2834,7 +3055,7 @@ export function memoizeThunk(f: () => A): () => A { * children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) * }) * - * // The recursive branch is a Suspend node + * SchemaAST.isObjects(Category.ast) // => true * ``` * * @see {@link isSuspend} @@ -2852,15 +3073,16 @@ export class Suspend extends Base { encoding?: Encoding, context?: Context ) { - if (checks !== undefined) { + if (checks) { throw new Error("Cannot add checks to Suspend") } super(annotations, undefined, encoding, context) this.thunk = memoizeThunk(thunk) } /** @internal */ - getParser(recur: (ast: AST) => SchemaParser.Parser): SchemaParser.Parser { - return recur(this.thunk()) + getParser(compile: SchemaParser.Compiler): SchemaParser.Parser { + let parser: SchemaParser.Parser + return (input, options) => (parser ??= compile(this.thunk()))(input, options) } /** @internal */ recur(recur: (ast: AST) => AST) { @@ -2889,8 +3111,8 @@ export class Suspend extends Base { * * - `run` — the validation function. Returns `undefined` on success, or an * `Issue` on failure. - * - `annotations` — optional filter-level metadata (expected message, meta - * tags, arbitrary constraint hints). + * - `annotations` — optional filter-level annotations (expected message, + * representation, arbitrary constraint hints). * - `aborted` — when `true`, parsing stops immediately after this filter * fails (no further checks run). * @@ -2995,7 +3217,7 @@ export function makeFilter( aborted: boolean = false ): Filter { return new Filter( - (input, ast, options) => SchemaIssue.make(input, ast, filter(input, ast, options)), + (input, ast, options) => SchemaIssue.normalizeFilterOutput(ast, filter(input, ast, options), input, options), annotations, aborted ) @@ -3007,12 +3229,38 @@ export function makeFilterByGuard( annotations?: Schema.Annotations.Filter ): Filter { return new Filter( - (input: E) => is(input) ? undefined : new SchemaIssue.InvalidValue(Option.some(input)), + (input: E, _ast, options) => is(input) ? undefined : new SchemaIssue.InvalidValue(undefined, input, options), annotations, true // after a guard, we always want to abort ) } +/** @internal */ +export function isFinite(annotations?: Schema.Annotations.Filter) { + return makeFilter( + (n: number) => globalThis.Number.isFinite(n), + { + expected: "a finite number", + representation: { + id: "effect/schema/isFinite", + payload: null + }, + toJsonSchema: () => ({ type: "number" }), + toCode: () => ({ runtime: "Schema.isFinite()" }), + arbitrary: { + constraint: { + noInfinity: true, + noNaN: true + } + }, + ...annotations + } + ) +} + +/** @internal */ +export const finite = appendChecks(number, [isFinite()]) + /** * Creates a {@link Filter} that validates strings by running `RegExp.test`. * @@ -3024,21 +3272,25 @@ export function makeFilterByGuard( * **Details** * * The filter can be used with `Schema.filter` or attached directly to a - * `String` AST node through checks. The regular expression source is stored in - * annotations for serialization and arbitrary generation. + * `String` AST node through checks. The regular expression is cloned and its + * `lastIndex` is reset before each test, so global and sticky expressions are + * deterministic and the provided regular expression is not mutated. The + * regular expression source is stored in annotations for serialization and + * arbitrary generation. * * **Gotchas** * - * Use a non-global, non-sticky regular expression, or reset `lastIndex` - * yourself, because `RegExp.test` is stateful for expressions with the `g` or - * `y` flag. + * When deriving an arbitrary, only `regExp.source` is used. Regular expression + * flags are ignored because fast-check does not support them. * * **Example** (Validating an email pattern) * - * ```ts + * ```ts import.meta.vitest * import { SchemaAST } from "effect" * * const emailFilter = SchemaAST.isPattern(/^[^@]+@[^@]+$/) + * emailFilter.run("alice@example.com", SchemaAST.string, {}) // => undefined + * emailFilter.run("invalid", SchemaAST.string, {})?._tag // => "InvalidValue" * ``` * * @see {@link Filter} @@ -3047,14 +3299,19 @@ export function makeFilterByGuard( */ export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annotations.Filter) { const source = regExp.source + const pattern = new globalThis.RegExp(source, regExp.flags) return makeFilter( - (s: string) => regExp.test(s), + (s: string) => { + pattern.lastIndex = 0 + return pattern.test(s) + }, { expected: `a string matching the RegExp ${source}`, - meta: { - _tag: "isPattern", - regExp + representation: { + id: "effect/schema/isPattern", + payload: { source, flags: regExp.flags } }, + toJsonSchema: () => ({ pattern: source }), arbitrary: { constraint: { patterns: [regExp.source] @@ -3076,6 +3333,13 @@ function modifyOwnPropertyDescriptors( return Object.create(Object.getPrototypeOf(ast), d) } +const contextOwners = new WeakMap() + +/** @internal */ +export function getContextOwner(ast: AST): AST { + return contextOwners.get(ast) ?? ast +} + /** @internal */ export function replaceEncoding(ast: A, encoding: Encoding | undefined): A { if (ast.encoding === encoding) { @@ -3091,9 +3355,15 @@ export function replaceContext(ast: A, context: Context | undefin if (ast.context === context) { return ast } - return modifyOwnPropertyDescriptors(ast, (d) => { + const owner = getContextOwner(ast) + if (owner.context === context) { + return owner as A + } + const out = modifyOwnPropertyDescriptors(ast, (d) => { d.context.value = context }) + contextOwners.set(out, owner) + return out } /** @internal */ @@ -3114,7 +3384,7 @@ export function annotate(ast: A, annotations: Schema.Annotations. /** @internal */ export function replaceChecks(ast: A, checks: Checks | undefined): A { - if (ast._tag === "Suspend" && checks !== undefined) { + if (ast._tag === "Suspend" && checks) { throw new Error("Cannot add checks to Suspend") } if (ast.checks === checks) { @@ -3130,14 +3400,17 @@ export function appendChecks(ast: A, checks: Checks | undefined): return replaceChecks(ast, combineChecks(ast.checks, checks)) } +/** @internal */ +export function mapLink(link: Link, f: (ast: AST) => AST): Link { + const to = f(link.to) + return to === link.to ? link : new Link(to, link.transformation) +} + function updateLastLink(encoding: Encoding, f: (ast: AST) => AST): Encoding { const links = encoding const last = links[links.length - 1] - const to = f(last.to) - if (to !== last.to) { - return Arr.append(encoding.slice(0, encoding.length - 1), new Link(to, last.transformation)) - } - return encoding + const out = mapLink(last, f) + return out === last ? encoding : Arr.append(encoding.slice(0, encoding.length - 1), out) } /** @internal */ @@ -3145,6 +3418,11 @@ export function applyToLastLink(f: (ast: AST) => AST) { return (ast: A): A => ast.encoding ? replaceEncoding(ast, updateLastLink(ast.encoding, f)) : ast } +/** @internal */ +export function replaceContextLastLink(ast: A, context: Context): A { + return applyToLastLink((ast) => replaceContext(ast, context))(ast) +} + /** @internal */ export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) { function out(ast: AST): AST { @@ -3153,6 +3431,21 @@ export function applyToSelfOrLastLinkEncoding(f: (ast: AST) => AST) { return memoize(out) } +/** @internal */ +export function applyToSelfOrLastLinkEncodingIdempotent( + f: (ast: AST) => AST, + options?: { readonly stopAt?: (link: Link) => boolean } +) { + function out(ast: AST): AST { + if (ast.encoding) { + const last = ast.encoding[ast.encoding.length - 1] + return options?.stopAt?.(last) ? ast : replaceEncoding(ast, updateLastLink(ast.encoding, out)) + } + return f(ast) + } + return memoizeIdempotent(out) +} + /** @internal */ export function middlewareDecoding( ast: AST, @@ -3213,7 +3506,7 @@ export function annotateKey(ast: A, annotations: Schema.Annotatio new Context( ast.context.isOptional, ast.context.isMutable, - ast.context.defaultValue, + ast.context.constructorDefault, { ...ast.context.annotations, ...annotations } ) : new Context(false, false, undefined, annotations) @@ -3221,42 +3514,33 @@ export function annotateKey(ast: A, annotations: Schema.Annotatio } /** @internal */ -export const optionalKeyLastLink = applyToLastLink(optionalKey) - -/** - * Marks an AST node's property key as optional by setting - * {@link Context.isOptional} to `true`. - * - * **Details** - * - * Also propagates the optional flag through the last link of the encoding - * chain if present. - * - * @see {@link isOptional} - * @see {@link Context} - * @category transforming - * @since 4.0.0 - */ -export function optionalKey(ast: A): A { +export const optionalKey: (ast: A) => A = memoizeIdempotent((ast: A): A => { const context = ast.context ? ast.context.isOptional === false ? - new Context(true, ast.context.isMutable, ast.context.defaultValue, ast.context.annotations) : + new Context(true, ast.context.isMutable, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(true, false) return optionalKeyLastLink(replaceContext(ast, context)) -} +}) -const mutableKeyLastLink = applyToLastLink(mutableKey) +const optionalKeyLastLink = applyToLastLink(optionalKey) + +/** @internal */ +export const optional = memoize((ast: A): Union => + optionalKey(new Union([ast, undefined_], "anyOf")) +) /** @internal */ -export function mutableKey(ast: A): A { +export const mutableKey = memoizeIdempotent((ast: A): A => { const context = ast.context ? ast.context.isMutable === false ? - new Context(ast.context.isOptional, true, ast.context.defaultValue, ast.context.annotations) : + new Context(ast.context.isOptional, true, ast.context.constructorDefault, ast.context.annotations) : ast.context : new Context(false, true) return mutableKeyLastLink(replaceContext(ast, context)) -} +}) + +const mutableKeyLastLink = applyToLastLink(mutableKey) /** @internal */ export function withConstructorDefault( @@ -3267,10 +3551,10 @@ export function withConstructorDefault( SchemaGetter.withDefault(defaultValue), SchemaGetter.passthrough() ) - const encoding: Encoding = [new Link(unknown, transformation)] + const constructorDefault = new Link(unknown, transformation) const context = ast.context ? - new Context(ast.context.isOptional, ast.context.isMutable, encoding, ast.context.annotations) : - new Context(false, false, encoding) + new Context(ast.context.isOptional, ast.context.isMutable, constructorDefault, ast.context.annotations) : + new Context(false, false, constructorDefault) return replaceContext(ast, context) } @@ -3332,11 +3616,11 @@ function parseParameter(ast: AST): { } /** @internal */ -export function record(key: AST, value: AST, keyValueCombiner: KeyValueCombiner | undefined): Objects { +export function record(key: AST, value: AST): Objects { const { literals, parameters: indexSignatures } = parseParameter(key) return new Objects( literals.map((literal) => new PropertySignature(literal, value)), - indexSignatures.map((parameter) => new IndexSignature(parameter, value, keyValueCombiner)) + indexSignatures.map((parameter) => new IndexSignature(parameter, value)) ) } @@ -3366,6 +3650,20 @@ export function isMutable(ast: AST): boolean { return ast.context?.isMutable ?? false } +function isStructuralCheck(check: Check): boolean { + return check.annotations?.[InternalAnnotations.STRUCTURAL_ANNOTATION_KEY] === true || + check._tag === "FilterGroup" && check.checks.every(isStructuralCheck) +} + +function extractStructuralChecks(checks: Checks): Checks | undefined { + function extract(check: Check): Array> { + if (isStructuralCheck(check)) return [check] + return check._tag === "FilterGroup" ? check.checks.flatMap(extract) : [] + } + const out = checks.flatMap(extract) + return Arr.isArrayNonEmpty(out) ? out : undefined +} + /** * Strips all encoding transformations from an AST, returning the decoded * (type-level) representation. @@ -3378,12 +3676,12 @@ export function isMutable(ast: AST): boolean { * * **Example** (Getting the type AST) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.NumberFromString * const typeAst = SchemaAST.toType(schema.ast) - * console.log(typeAst._tag) // "Number" + * typeAst._tag // => "Number" * ``` * * @see {@link toEncoded} @@ -3391,19 +3689,22 @@ export function isMutable(ast: AST): boolean { * @category transforming * @since 4.0.0 */ -export const toType = memoize((ast: A): A => { +export const toType = memoizeIdempotent((ast: A): A => { if (ast.encoding) { return toType(replaceEncoding(ast, undefined)) } const out: any = ast const type = out.recur?.(toType) ?? out - const encodingChecks = type.encodingChecks + const encodingChecks: Checks | undefined = type.encodingChecks if (encodingChecks) { + const checks = type === ast + ? encodingChecks + : isArrays(type) || isObjects(type) || isDeclaration(type) && type.typeParameters.length > 0 + ? extractStructuralChecks(encodingChecks) + : undefined return modifyOwnPropertyDescriptors(type, (d) => { d.encodingChecks.value = undefined - if (type === ast) { - d.checks.value = combineChecks(type.checks, encodingChecks) - } + d.checks.value = combineChecks(type.checks, checks) }) } return type @@ -3422,12 +3723,12 @@ export const toType = memoize((ast: A): A => { * * **Example** (Getting the encoded AST) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.NumberFromString * const encodedAst = SchemaAST.toEncoded(schema.ast) - * console.log(encodedAst._tag) // "String" + * encodedAst._tag // => "String" * ``` * * @see {@link toType} @@ -3435,7 +3736,7 @@ export const toType = memoize((ast: A): A => { * @category transforming * @since 4.0.0 */ -export const toEncoded = memoize((ast: AST): AST => { +export const toEncoded = memoizeIdempotent((ast: AST): AST => { return toType(flip(ast)) }) @@ -3498,54 +3799,44 @@ function fromConst( ast: AST, value: T ): SchemaParser.Parser { - const succeed = Effect.succeedSome(value) - return (oinput) => { - if (oinput._tag === "None") { - return Effect.succeedNone - } - return oinput.value === value - ? succeed - : Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + const succeed = InternalParser.succeed(value) + return (input, options) => { + if (input === InternalParser.missing) return InternalParser.missingExit + if (input === value) return succeed + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } } -function fromAnyToConst(value: T): SchemaParser.Parser { - const succeed = Effect.succeedSome(value) - return (oinput) => oinput._tag === "None" ? Effect.succeedNone : succeed -} - function fromRefinement( ast: AST, refinement: (input: unknown) => input is T ): SchemaParser.Parser { - return (oinput) => { - if (oinput._tag === "None") { - return Effect.succeedNone - } - return refinement(oinput.value) - ? Effect.succeed(oinput) - : Effect.fail(new SchemaIssue.InvalidType(ast, oinput)) + return (input, options) => { + if (input === InternalParser.missing) return InternalParser.missingExit + if (refinement(input)) return InternalParser.sameExit + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } } -function applyTemplateLiteralPartChecks(ast: AST, value: A, options: ParseOptions): A | undefined { - if (options?.disableChecks || ast.checks === undefined) return value - const issues: Array = [] - collectIssues(ast.checks, value, issues, ast, options) - return issues.length === 0 ? value : undefined -} - function segmentTemplateLiteralParts( - parts: ReadonlyArray, + ast: TemplateLiteral, input: string, options: ParseOptions ): Array | undefined { + const parts = ast.encodedParts + const literals = ast.literals + const inputLength = input.length + for (let i = 0; i < literals.length; i++) { + const literal = literals[i] + if (literal && !input.includes(literal)) return undefined + } + if (ast.suffixLengths[0] > inputLength) return undefined + const out = new Array(parts.length) - const failures = new Set() + let failures: Set | undefined function go(i: number, pos: number): boolean { - if (i === parts.length) return pos === input.length - const key = `${i}/${pos}` - if (failures.has(key)) return false + if (i === parts.length) return pos === inputLength + if (failures?.has(i * (inputLength + 1) + pos)) return false const part = parts[i] if (i === parts.length - 1) { const s = input.slice(pos) @@ -3554,21 +3845,28 @@ function segmentTemplateLiteralParts( return true } } else if (part._tag === "Literal") { - const s = globalThis.String(part.literal) + const s = literals[i]! if (input.startsWith(s, pos) && go(i + 1, pos + s.length)) { out[i] = s return true } } else { - for (let end = input.length; end >= pos; end--) { + const maximumEnd = inputLength - ast.suffixLengths[i + 1] + // Splits preceding a literal only need to consider occurrences of that literal. + const anchor = literals[i + 1] + let end = anchor === undefined ? maximumEnd : input.lastIndexOf(anchor, maximumEnd) + while (end >= pos) { const s = input.slice(pos, end) if (part.matchPart(s, options) !== undefined && go(i + 1, end)) { out[i] = s return true } + if (end === 0) break + end = anchor === undefined ? end - 1 : input.lastIndexOf(anchor, end - 1) } } - failures.add(key) + failures ??= new Set() + failures.add(i * (inputLength + 1) + pos) return false } return go(0, 0) ? out : undefined @@ -3582,7 +3880,7 @@ export const enumsToLiterals = memoize((ast: Enum): Union => { ) }) -const parameterFromPropertyKey = applyToSelfOrLastLinkEncoding((ast) => { +const parameterFromPropertyKey = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast @@ -3594,7 +3892,7 @@ const parameterFromPropertyKey = applyToSelfOrLastLinkEncoding((ast) => { }) /** @internal */ -export const parameterFromString = applyToSelfOrLastLinkEncoding((ast) => { +export const parameterFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast @@ -3606,7 +3904,7 @@ export const parameterFromString = applyToSelfOrLastLinkEncoding((ast) => { } }) -const partFromString = applyToSelfOrLastLinkEncoding((ast) => { +const partFromString = applyToSelfOrLastLinkEncodingIdempotent((ast) => { switch (ast._tag) { default: return ast @@ -3627,7 +3925,7 @@ export const STRING_PATTERN = "[\\s\\S]*?" const isStringFiniteRegExp = new globalThis.RegExp(`^${FINITE_PATTERN}$`) -const isStringNumberRegExp = new globalThis.RegExp(`(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)`) +const isStringNumberRegExp = new globalThis.RegExp(`^(?:${FINITE_PATTERN}|Infinity|-Infinity|NaN)$`) /** @internal */ export function isStringFinite(annotations?: Schema.Annotations.Filter) { @@ -3635,10 +3933,11 @@ export function isStringFinite(annotations?: Schema.Annotations.Filter) { isStringFiniteRegExp, { expected: "a string representing a finite number", - meta: { - _tag: "isStringFinite", - regExp: isStringFiniteRegExp + representation: { + id: "effect/schema/isStringFinite", + payload: null }, + toJsonSchema: () => ({ pattern: isStringFiniteRegExp.source }), ...annotations } ) @@ -3669,10 +3968,11 @@ export function isStringBigInt(annotations?: Schema.Annotations.Filter) { isStringBigIntRegExp, { expected: "a string representing a bigint", - meta: { - _tag: "isStringBigInt", - regExp: isStringBigIntRegExp + representation: { + id: "effect/schema/isStringBigInt", + payload: null }, + toJsonSchema: () => ({ pattern: isStringBigIntRegExp.source }), ...annotations } ) @@ -3702,13 +4002,17 @@ const symbolToString = new Link( symbolString, new SchemaTransformation.Transformation( SchemaGetter.transform((description) => globalThis.Symbol.for(isStringSymbolRegExp.exec(description)![1])), - SchemaGetter.transformOrFail((sym: symbol) => { + SchemaGetter.transformOrFail((sym: symbol, options) => { const key = globalThis.Symbol.keyFor(sym) if (key !== undefined) { return Effect.succeed(globalThis.String(sym)) } return Effect.fail( - new SchemaIssue.Forbidden(Option.some(sym), { message: "cannot serialize to string, Symbol is not registered" }) + new SchemaIssue.Forbidden( + { message: "cannot serialize to string, Symbol is not registered" }, + sym, + options + ) ) }) ) @@ -3720,10 +4024,11 @@ export function isStringSymbol(annotations?: Schema.Annotations.Filter) { isStringSymbolRegExp, { expected: "a string representing a symbol", - meta: { - _tag: "isStringSymbol", - regExp: isStringSymbolRegExp + representation: { + id: "effect/schema/isStringSymbol", + payload: null }, + toJsonSchema: () => ({ pattern: isStringSymbolRegExp.source }), ...annotations } ) @@ -3733,24 +4038,33 @@ export function isStringSymbol(annotations?: Schema.Annotations.Filter) { export function collectIssues( checks: ReadonlyArray>, value: T, - issues: Array, + issues: Arr.NonEmptyArray | undefined, ast: AST, options: ParseOptions -) { +): Arr.NonEmptyArray | undefined { for (let i = 0; i < checks.length; i++) { const check = checks[i] if (check._tag === "FilterGroup") { - collectIssues(check.checks, value, issues, ast, options) + issues = collectIssues(check.checks, value, issues, ast, options) + if ( + issues && + (options.errors !== "all" || (issues[issues.length - 1] as SchemaIssue.Filter).filter.aborted) + ) { + return issues + } } else { const issue = check.run(value, ast, options) if (issue) { - issues.push(new SchemaIssue.Filter(value, check, issue)) - if (check.aborted || options?.errors !== "all") { - return + const filter = new SchemaIssue.Filter(check, issue, value, options) + if (issues) issues.push(filter) + else issues = [filter] + if (options.errors !== "all" || check.aborted) { + return issues } } } } + return issues } /** @internal */ @@ -3758,20 +4072,26 @@ export function runChecks( checks: readonly [Check, ...Array>], s: T ): Result.Result { - const issues: Array = [] - collectIssues(checks, s, issues, unknown, { errors: "all" }) - if (Arr.isArrayNonEmpty(issues)) { - const issue = new SchemaIssue.Composite(unknown, Option.some(s), issues) + const issues = collectIssues(checks, s, undefined, unknown, { errors: "all" }) + if (issues) { + const issue = new SchemaIssue.Composite(unknown, issues) return Result.fail(issue) } return Result.succeed(s) } /** @internal */ -export const ClassTypeId = "~effect/Schema/Class" +export interface ConstructorDescriptor { + readonly isConstructed: Predicate.Predicate + readonly link: Link +} /** @internal */ -export const STRUCTURAL_ANNOTATION_KEY = "~structural" +export function getConstructorDescriptor(ast: AST): ConstructorDescriptor | undefined { + if (!isDeclaration(ast)) return undefined + const getDescriptor = ast.annotations?.[InternalAnnotations.CONSTRUCTOR_ANNOTATION_KEY] + return Predicate.isFunction(getDescriptor) ? getDescriptor(ast.typeParameters) : undefined +} /** * Returns all annotations from the AST node. @@ -3784,12 +4104,12 @@ export const STRUCTURAL_ANNOTATION_KEY = "~structural" * * **Example** (Reading annotations) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaAST } from "effect" * * const schema = Schema.String.annotate({ title: "Name" }) * const annotations = SchemaAST.resolve(schema.ast) - * console.log(annotations?.title) // "Name" + * annotations?.title // => "Name" * ``` * * @see {@link resolveAt} @@ -3854,6 +4174,82 @@ export const resolveTitle: (ast: AST) => string | undefined = InternalAnnotation */ export const resolveDescription: (ast: AST) => string | undefined = InternalAnnotations.resolveDescription +type TreeFrame = { + readonly value: object + // Object keys or an array length snapshot. + readonly keys: ReadonlyArray | number + index: number +} + +function isJsonLeaf(u: unknown): boolean { + return u === null || typeof u === "string" || typeof u === "boolean" || + typeof u === "number" && globalThis.Number.isFinite(u) +} + +function isStringTreeLeaf(u: unknown): boolean { + return u === undefined || typeof u === "string" +} + +function isTree(u: unknown, isLeaf: (u: unknown) => boolean): boolean { + const cache = new WeakMap() + const stack: Array = [] + outer: while (true) { + if (typeof u !== "object" || u === null) { + if (!isLeaf(u)) { + return false + } + } else { + const value = u + const cached = cache.get(value) + // `false` marks a node on the current path, while `true` marks a fully + // validated node that can be safely reused by a DAG. + if (cached === false) { + return false + } + if (cached === undefined) { + const isArray = Array.isArray(value) + if (!isArray) { + const prototype = Object.getPrototypeOf(value) + // A plain object from another realm has a different Object.prototype, + // but that prototype still has a null prototype. + if ( + prototype !== null && + prototype !== Object.prototype && + Object.getPrototypeOf(prototype) !== null + ) { + return false + } + } + cache.set(value, false) + stack.push({ + value, + keys: isArray ? value.length : Object.keys(value), + index: 0 + }) + } + } + + while (stack.length > 0) { + const frame = stack[stack.length - 1] + const keys = frame.keys + if (typeof keys === "number") { + if (frame.index < keys) { + // A sparse slot is read as `undefined`; the leaf predicate determines + // whether that is valid for the current tree. + u = (frame.value as ReadonlyArray)[frame.index++] + continue outer + } + } else if (frame.index < keys.length) { + u = (frame.value as Record)[keys[frame.index++]] + continue outer + } + cache.set(frame.value, true) + stack.pop() + } + return true + } +} + /** * Returns true if the value is a JSON value. * @@ -3862,97 +4258,48 @@ export const resolveDescription: (ast: AST) => string | undefined = InternalAnno * @internal */ export function isJson(u: unknown): u is Schema.Json { - // `onPath` is the current recursion stack: nodes between the root and the - // one being visited. A hit here means we looped back to an ancestor — a - // real cycle, not a DAG — so the value is not JSON. - const onPath = new Set() - // `validated` memoizes subtrees we've already fully checked. Without it, a - // diamond-shaped DAG (same node reached through multiple parents) would be - // re-traversed once per parent, which is exponential in the nesting depth. - const validated = new Set() - return recur(u) - - function recur(u: unknown): boolean { - if (u === null || typeof u === "string" || typeof u === "boolean") { - return true - } - if (typeof u === "number") { - return globalThis.Number.isFinite(u) - } - if (typeof u !== "object" || u === undefined) { - return false - } - if (onPath.has(u)) { - return false - } - if (validated.has(u)) { - return true - } - const isArray = Array.isArray(u) - if (!isArray) { - const prototype = Object.getPrototypeOf(u) - if (prototype !== null && Object.getPrototypeOf(prototype) !== null) { - return false - } - } - onPath.add(u) - const ok = isArray - ? u.every(recur) - : Object.keys(u).every((key) => recur((u as Record)[key])) - // Pop on exit so siblings reaching the same node via a different path - // don't see it as an ancestor (that would reject valid DAGs). - onPath.delete(u) - if (ok) { - validated.add(u) - } - return ok - } + return isTree(u, isJsonLeaf) } /** @internal */ export const Json = new Declaration( [], - () => (input, ast) => + () => (input, ast, options) => isJson(input) ? - Effect.succeed(input) : - Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), + InternalParser.sameExit : + Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), { - typeConstructor: { - _tag: "effect/Json" - }, - generation: { - runtime: `Schema.Json`, - Type: `Schema.Json` + representation: { + id: "effect/schema/Json", + payload: null }, expected: "JSON value", - toCodecJson: () => new Link(unknown, SchemaTransformation.passthrough()), + toCodecJson: () => undefined, + toCodecStringTree: () => unknownToStringTree, toArbitrary: () => (fc: typeof FastCheck) => fc.jsonValue() } ) /** @internal */ export const MutableJson = annotate(Json, { - typeConstructor: { - _tag: "effect/MutableJson" - }, - generation: { - runtime: `Schema.MutableJson`, - Type: `Schema.MutableJson` + representation: { + id: "effect/schema/MutableJson", + payload: null } }) /** @internal */ -export const unknownToNull = new Link( - null_, - new SchemaTransformation.Transformation( - SchemaGetter.passthrough(), - SchemaGetter.transform(() => null) - ) +export const unknownToJson = new Link( + Json, + SchemaTransformation.passthrough() ) /** @internal */ -export const unknownToJson = new Link( - Json, +export const objectKeywordToJson = new Link( + new Union([ + new Arrays(false, [], [Json]), + new Objects([], [new IndexSignature(string, Json)]) + ], "anyOf"), SchemaTransformation.passthrough() ) @@ -3964,34 +4311,16 @@ export const unknownToJson = new Link( * @internal */ export function isStringTree(u: unknown): u is Schema.StringTree { - const seen = new Set() - return recur(u) - - function recur(u: unknown): boolean { - if (u === undefined || typeof u === "string") { - return true - } - if (typeof u !== "object" || u === null) { - return false - } - if (seen.has(u)) { - return false - } - seen.add(u) - if (Array.isArray(u)) { - return u.every(recur) - } - return Object.keys(u).every((key) => recur((u as Record)[key])) - } + return isTree(u, isStringTreeLeaf) } const StringTree = new Declaration( [], - () => (input, ast) => + () => (input, ast, options) => isStringTree(input) ? - Effect.succeed(input) : - Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), - { expected: "StringTree" } + InternalParser.sameExit : + Effect.fail(new SchemaIssue.InvalidType(ast, input, options)), + { expected: "StringTree", toCodecStringTree: () => undefined } ) /** @internal */ diff --git a/.context/effect/packages/effect/src/SchemaGetter.ts b/.context/effect/packages/effect/src/SchemaGetter.ts index dff48854e..321096f0a 100644 --- a/.context/effect/packages/effect/src/SchemaGetter.ts +++ b/.context/effect/packages/effect/src/SchemaGetter.ts @@ -45,13 +45,13 @@ import * as Str from "./String.ts" * * **Example** (Creating and composing getters) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const parseNumber = SchemaGetter.transform((s) => Number(s)) * const double = SchemaGetter.transform((n) => n * 2) * const composed = parseNumber.compose(double) - * // composed: Getter — parses then doubles + * await Effect.runPromise(composed.run(Option.some("21"), {})) // => Option.some(42) * ``` * * @see {@link transform} to create a getter from a pure function @@ -105,11 +105,11 @@ export class Getter extends Pipeable.Class { * * **Example** (Returning a constant getter) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const alwaysZero = SchemaGetter.succeed(0) - * // alwaysZero: Getter<0, unknown> — always produces 0 + * await Effect.runPromise(alwaysZero.run(Option.none(), {})) // => Option.some(0) * ``` * * @see {@link transform} when you need to use the input value @@ -133,16 +133,19 @@ export function succeed(t: T): Getter { * **Details** * * - Always fails with the `Issue` returned by `f`. - * - The failure function receives the original `Option` input for error context. + * - The failure function receives the original `Option` input and the + * effective `ParseOptions` for error context. * * **Example** (Defining an always-failing getter) * - * ```ts - * import { Option, SchemaGetter, SchemaIssue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect" * * const rejectAll = SchemaGetter.fail( - * (oe) => new SchemaIssue.InvalidValue(oe, { message: "not allowed" }) + * () => new SchemaIssue.InvalidValue({ message: "not allowed" }) * ) + * const issue = await Effect.runPromise(Effect.flip(rejectAll.run(Option.some("x"), {}))) + * issue._tag // => "InvalidValue" * ``` * * @see {@link forbidden} for a convenience helper for `Forbidden` issues @@ -151,8 +154,10 @@ export function succeed(t: T): Getter { * @category constructors * @since 4.0.0 */ -export function fail(f: (oe: Option.Option) => SchemaIssue.Issue): Getter { - return new Getter((oe) => Effect.fail(f(oe))) +export function fail( + f: (oe: Option.Option, options: SchemaAST.ParseOptions) => SchemaIssue.Issue +): Getter { + return new Getter((oe, options) => Effect.fail(f(oe, options))) } /** @@ -171,12 +176,14 @@ export function fail(f: (oe: Option.Option) => SchemaIssue.Issue): Gett * * **Example** (Forbidding a decode direction) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const noEncode = SchemaGetter.forbidden( * () => "encoding is not supported" * ) + * const issue = await Effect.runPromise(Effect.flip(noEncode.run(Option.some(1), {}))) + * issue._tag // => "Forbidden" * ``` * * @see {@link fail} to fail with a custom issue type @@ -185,7 +192,12 @@ export function fail(f: (oe: Option.Option) => SchemaIssue.Issue): Gett * @since 4.0.0 */ export function forbidden(message: (oe: Option.Option) => string): Getter { - return fail((oe) => new SchemaIssue.Forbidden(oe, { message: message(oe) })) + return fail((oe, options) => { + const annotations = { message: message(oe) } + return Option.isSome(oe) + ? new SchemaIssue.Forbidden(annotations, oe.value, options) + : new SchemaIssue.Forbidden(annotations) + }) } const passthrough_ = new Getter(Effect.succeed) @@ -211,7 +223,7 @@ function isPassthrough(getter: Getter): getter is typeof passt * * **Example** (Passing through identity transformations) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * // No transformation needed — types already match @@ -221,6 +233,7 @@ function isPassthrough(getter: Getter): getter is typeof passt * encode: SchemaGetter.passthrough() * }) * ) + * Schema.decodeSync(StringToString)("hello") // => "hello" * ``` * * @see {@link passthroughSupertype} when `T extends E` @@ -250,11 +263,12 @@ export function passthrough(): Getter { * * **Example** (Passing through supertypes) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * // string extends string, so this is valid * const g = SchemaGetter.passthroughSupertype() + * await Effect.runPromise(g.run(Option.some("hello"), {})) // => Option.some("hello") * ``` * * @see {@link passthrough} when types are identical @@ -282,11 +296,12 @@ export function passthroughSupertype(): Getter { * * **Example** (Passing through subtypes) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * // "hello" extends string, so E extends T * const g = SchemaGetter.passthroughSubtype() + * await Effect.runPromise(g.run(Option.some("hello"), {})) // => Option.some("hello") * ``` * * @see {@link passthrough} when types are identical @@ -317,19 +332,20 @@ export function passthroughSubtype(): Getter { * * **Example** (Providing a default timestamp for a missing field) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const withTimestamp = SchemaGetter.onNone(() => - * Effect.succeed(Option.some(Date.now())) + * Effect.succeed(Option.some(0)) * ) + * await Effect.runPromise(withTimestamp.run(Option.none(), {})) // => Option.some(0) * ``` * * @see {@link required} when absent input should fail * @see {@link withDefault} for a simpler default value for undefined inputs * @see {@link onSome} to handle only present values * - * @category constructors + * @category transforming * @since 4.0.0 */ export function onNone( @@ -354,16 +370,18 @@ export function onNone( * * **Example** (Defining a required struct field) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const mustExist = SchemaGetter.required() + * const issue = await Effect.runPromise(Effect.flip(mustExist.run(Option.none(), {}))) + * issue._tag // => "MissingKey" * ``` * * @see {@link onNone} to provide a fallback instead of failing * @see {@link withDefault} to substitute a default for undefined values * - * @category constructors + * @category validation * @since 4.0.0 */ export function required(annotations?: Schema.Annotations.Key): Getter { @@ -387,19 +405,20 @@ export function required(annotations?: Schema.Annotations.Ke * * **Example** (Transforming only present values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter } from "effect" * * const parseIfPresent = SchemaGetter.onSome( * (s) => Effect.succeed(Option.some(Number(s))) * ) + * await Effect.runPromise(parseIfPresent.run(Option.some("42"), {})) // => Option.some(42) * ``` * * @see {@link onNone} to handle only absent values * @see {@link transform} for a simpler pure transformation of present values * @see {@link transformOrFail} for fallible transformation of present values * - * @category constructors + * @category transforming * @since 4.0.0 */ export function onSome( @@ -430,18 +449,19 @@ export function onSome( * * **Example** (Validating effectfully) * - * ```ts - * import { Effect, SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const nonNegative = SchemaGetter.checkEffect((n) => * Effect.succeed(n >= 0 ? undefined : "must be non-negative") * ) + * await Effect.runPromise(nonNegative.run(Option.some(1), {})) // => Option.some(1) * ``` * * @see {@link transform} when you need to change the value, not just validate * @see {@link fail} for unconditional failure * - * @category constructors + * @category validation * @since 4.0.0 */ export function checkEffect( @@ -453,7 +473,7 @@ export function checkEffect( ): Getter { return onSome((t, options) => { return f(t, options).pipe(Effect.flatMapEager((out) => { - const issue = SchemaIssue.makeSingle(t, out) + const issue = SchemaIssue.makeSingle(out, t, options) return issue ? Effect.fail(issue) : Effect.succeed(Option.some(t)) @@ -479,7 +499,7 @@ export function checkEffect( * * **Example** (Transforming strings to numbers) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaGetter } from "effect" * * const NumberFromString = Schema.String.pipe( @@ -488,13 +508,14 @@ export function checkEffect( * encode: SchemaGetter.transform((n) => String(n)) * }) * ) + * Schema.decodeSync(NumberFromString)("42") // => 42 * ``` * * @see {@link transformOrFail} when the transformation can fail * @see {@link transformOptional} when you need to handle `None` inputs * @see {@link passthrough} when no transformation is needed * - * @category constructors + * @category transforming * @since 4.0.0 */ export function transform(f: (e: E) => T): Getter { @@ -517,23 +538,24 @@ export function transform(f: (e: E) => T): Getter { * * **Example** (Parsing with failure) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SchemaGetter, SchemaIssue } from "effect" * * const safeParseInt = SchemaGetter.transformOrFail( - * (s) => { + * (s, options) => { * const n = parseInt(s, 10) * return isNaN(n) - * ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "not an integer" })) + * ? Effect.fail(new SchemaIssue.InvalidValue({ message: "not an integer" }, s, options)) * : Effect.succeed(n) * } * ) + * await Effect.runPromise(safeParseInt.run(Option.some("42"), {})) // => Option.some(42) * ``` * * @see {@link transform} when transformation cannot fail * @see {@link onSome} when you need full `Option` control over the output * - * @category constructors + * @category transforming * @since 4.0.0 */ export function transformOrFail( @@ -557,18 +579,19 @@ export function transformOrFail( * * **Example** (Filtering out empty strings) * - * ```ts - * import { Option, SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const skipEmpty = SchemaGetter.transformOptional((o) => * Option.filter(o, (s) => s.length > 0) * ) + * await Effect.runPromise(skipEmpty.run(Option.some(""), {})) // => Option.none() * ``` * * @see {@link transform} when you only need to transform present values * @see {@link omit} when you always want `None` * - * @category constructors + * @category transforming * @since 4.0.0 */ export function transformOptional(f: (oe: Option.Option) => Option.Option): Getter { @@ -590,16 +613,17 @@ export function transformOptional(f: (oe: Option.Option) => Option.Opti * * **Example** (Omitting a field during encoding) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const omitField = SchemaGetter.omit() + * await Effect.runPromise(omitField.run(Option.some("hidden"), {})) // => Option.none() * ``` * * @see {@link transformOptional} when you want conditional omission * @see {@link forbidden} when you want to fail instead of silently omit * - * @category constructors + * @category filtering * @since 4.0.0 */ export function omit(): Getter { @@ -622,17 +646,17 @@ export function omit(): Getter { * * **Example** (Providing a default value for an optional field) * - * ```ts - * import { Effect, SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const withZero = SchemaGetter.withDefault(Effect.succeed(0)) - * // Getter + * await Effect.runPromise(withZero.run(Option.some(undefined), {})) // => Option.some(0) * ``` * * @see {@link onNone} to handle only absent keys (not `undefined` values) * @see {@link required} when absent input should fail instead of using a default * - * @category constructors + * @category transforming * @since 4.0.0 */ export function withDefault( @@ -658,16 +682,16 @@ export function withDefault( * * **Example** (Coercing to a string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toString = SchemaGetter.String() - * // Getter + * await Effect.runPromise(toString.run(Option.some(42), {})) // => Option.some("42") * ``` * * @see {@link transform} for custom string conversions * - * @category Coercions + * @category converting * @since 4.0.0 */ export function String(): Getter { @@ -689,16 +713,16 @@ export function String(): Getter { * * **Example** (Coercing to a number) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toNumber = SchemaGetter.Number() - * // Getter + * await Effect.runPromise(toNumber.run(Option.some("42"), {})) // => Option.some(42) * ``` * * @see {@link transformOrFail} for validated number parsing * - * @category Coercions + * @category converting * @since 4.0.0 */ export function Number(): Getter { @@ -719,14 +743,14 @@ export function Number(): Getter { * * **Example** (Coercing to a boolean) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toBool = SchemaGetter.Boolean() - * // Getter + * await Effect.runPromise(toBool.run(Option.some("true"), {})) // => Option.some(true) * ``` * - * @category Coercions + * @category converting * @since 4.0.0 */ export function Boolean(): Getter { @@ -748,14 +772,14 @@ export function Boolean(): Getter { * * **Example** (Coercing to a bigint) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toBigInt = SchemaGetter.BigInt() - * // Getter + * await Effect.runPromise(toBigInt.run(Option.some("42"), {})) // => Option.some(42n) * ``` * - * @category Coercions + * @category converting * @since 4.0.0 */ export function BigInt(): Getter { @@ -777,16 +801,17 @@ export function BigInt(): Getter() - * // Getter + * const result = await Effect.runPromise(toDate.run(Option.some("1970-01-01"), {})) + * Option.map(result, (date) => date.toISOString()) // => Option.some("1970-01-01T00:00:00.000Z") * ``` * * @see {@link dateTimeUtcFromInput} for validated DateTime parsing * - * @category Coercions + * @category converting * @since 4.0.0 */ export function Date(): Getter { @@ -802,13 +827,14 @@ export function Date(): Getter { * * **Example** (Trimming whitespace) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const trimmed = SchemaGetter.trim() + * await Effect.runPromise(trimmed.run(Option.some(" hello "), {})) // => Option.some("hello") * ``` * - * @category string + * @category transforming * @since 4.0.0 */ export function trim(): Getter { @@ -824,13 +850,14 @@ export function trim(): Getter { * * **Example** (Capitalizing a string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const cap = SchemaGetter.capitalize() + * await Effect.runPromise(cap.run(Option.some("hello"), {})) // => Option.some("Hello") * ``` * - * @category string + * @category transforming * @since 4.0.0 */ export function capitalize(): Getter { @@ -846,13 +873,14 @@ export function capitalize(): Getter { * * **Example** (Uncapitalizing a string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const uncap = SchemaGetter.uncapitalize() + * await Effect.runPromise(uncap.run(Option.some("Hello"), {})) // => Option.some("hello") * ``` * - * @category string + * @category transforming * @since 4.0.0 */ export function uncapitalize(): Getter { @@ -868,15 +896,16 @@ export function uncapitalize(): Getter { * * **Example** (Converting snake case to camel case) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toCamel = SchemaGetter.snakeToCamel() + * await Effect.runPromise(toCamel.run(Option.some("user_name"), {})) // => Option.some("userName") * ``` * * @see {@link camelToSnake} for the inverse operation * - * @category string + * @category transforming * @since 4.0.0 */ export function snakeToCamel(): Getter { @@ -892,15 +921,16 @@ export function snakeToCamel(): Getter { * * **Example** (Converting camel case to snake case) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const toSnake = SchemaGetter.camelToSnake() + * await Effect.runPromise(toSnake.run(Option.some("userName"), {})) // => Option.some("user_name") * ``` * * @see {@link snakeToCamel} for the inverse operation * - * @category string + * @category transforming * @since 4.0.0 */ export function camelToSnake(): Getter { @@ -916,15 +946,16 @@ export function camelToSnake(): Getter { * * **Example** (Converting to lowercase) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const lower = SchemaGetter.toLowerCase() + * await Effect.runPromise(lower.run(Option.some("HELLO"), {})) // => Option.some("hello") * ``` * * @see {@link toUpperCase} for the inverse operation * - * @category string + * @category transforming * @since 4.0.0 */ export function toLowerCase(): Getter { @@ -940,15 +971,16 @@ export function toLowerCase(): Getter { * * **Example** (Converting to uppercase) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const upper = SchemaGetter.toUpperCase() + * await Effect.runPromise(upper.run(Option.some("hello"), {})) // => Option.some("HELLO") * ``` * * @see {@link toLowerCase} for the inverse operation * - * @category string + * @category transforming * @since 4.0.0 */ export function toUpperCase(): Getter { @@ -972,35 +1004,53 @@ type ParseJsonOptions = { * - Skips `None` inputs. * - Without `reviver`: returns `Schema.MutableJson` (typed JSON). * - With `reviver`: returns `unknown` (reviver may produce arbitrary values). - * - On parse failure, fails with `SchemaIssue.InvalidValue` containing the error message. + * - On parse failure, fails with `SchemaIssue.InvalidValue` whose `expected` + * annotation is `"a valid JSON string"`. Its default message includes the + * reported input when `reportInput` is enabled. * * **Example** (Parsing JSON) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const parse = SchemaGetter.parseJson() - * // Getter + * await Effect.runPromise(parse.run(Option.some("{\"a\":1}"), {})) // => Option.some({ a: 1 }) * ``` * * @see {@link stringifyJson} for the inverse operation * - * @category JSON getters + * @category decoding * @since 4.0.0 */ export function parseJson(): Getter export function parseJson(options: ParseJsonOptions): Getter export function parseJson(options?: ParseJsonOptions | undefined): Getter { - return onSome((input) => + return onSome((input, parseOptions) => Effect.try({ try: () => Option.some(JSON.parse(input, options?.reviver)), - catch: (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) + catch: () => + new SchemaIssue.InvalidValue( + { expected: "a valid JSON string" }, + input, + parseOptions + ) }) ) } +/** + * Replacer function or property allowlist accepted by `JSON.stringify`. + * + * @category utility types + * @since 4.0.0 + */ +export type JsonReplacer = + | ((this: any, key: string, value: any) => any) + | Array + | null + type StringifyJsonOptions = { - readonly replacer?: Parameters[1] + readonly replacer?: JsonReplacer | undefined readonly space?: Parameters[2] } @@ -1015,33 +1065,41 @@ type StringifyJsonOptions = { * **Details** * * - Skips `None` inputs. - * - On thrown stringify failures, such as circular references, fails with + * - If `JSON.stringify` throws or returns `undefined`, fails with * `SchemaIssue.InvalidValue`. * - Supports optional `replacer` and `space` options, matching * `JSON.stringify`. - * - If `JSON.stringify` returns `undefined`, such as for `undefined`, - * functions, symbols, or a replacer that removes the root value, that - * `undefined` result is returned rather than converted into an `Issue`. * * **Example** (Stringifying JSON) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const stringify = SchemaGetter.stringifyJson() - * // Getter + * await Effect.runPromise(stringify.run(Option.some({ a: 1 }), {})) // => Option.some("{\"a\":1}") * ``` * * @see {@link parseJson} for the inverse operation * - * @category JSON getters + * @category encoding * @since 4.0.0 */ export function stringifyJson(options?: StringifyJsonOptions): Getter { - return onSome((input) => + return onSome((input, parseOptions) => Effect.try({ - try: () => Option.some(JSON.stringify(input, options?.replacer, options?.space)), - catch: (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: globalThis.String(e) }) + try: () => { + const output = JSON.stringify(input, options?.replacer as any, options?.space) + if (output === undefined) { + throw new TypeError("Value cannot be represented as JSON") + } + return Option.some(output) + }, + catch: () => + new SchemaIssue.InvalidValue( + { expected: "a JSON-serializable value" }, + input, + parseOptions + ) }) ) } @@ -1062,17 +1120,17 @@ export function stringifyJson(options?: StringifyJsonOptions): Getter() - * // "a=1,b=2" -> { a: "1", b: "2" } + * await Effect.runPromise(parse.run(Option.some("a=1,b=2"), {})) // => Option.some({ a: "1", b: "2" }) * ``` * * @see {@link joinKeyValue} for the inverse operation * @see {@link split} to split into an array of strings * - * @category string + * @category splitting * @since 4.0.0 */ export function splitKeyValue(options?: { @@ -1085,7 +1143,7 @@ export function splitKeyValue(options?: { input.split(separator).reduce((acc, pair) => { const [key, value] = pair.split(keyValueSeparator) if (key && value) { - acc[key] = value + InternalRecord.assignProperty(acc, key, value) } return acc }, {} as Record) @@ -1108,16 +1166,16 @@ export function splitKeyValue(options?: { * * **Example** (Joining key-value records) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const join = SchemaGetter.joinKeyValue() - * // { a: "1", b: "2" } -> "a=1,b=2" + * await Effect.runPromise(join.run(Option.some({ a: "1", b: "2" }), {})) // => Option.some("a=1,b=2") * ``` * * @see {@link splitKeyValue} for the inverse operation * - * @category string + * @category combining * @since 4.0.0 */ export function joinKeyValue>(options?: { @@ -1146,17 +1204,16 @@ export function joinKeyValue>(options?: { * * **Example** (Splitting a comma-separated string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const splitComma = SchemaGetter.split() - * // "a,b,c" -> ["a", "b", "c"] - * // "" -> [] + * await Effect.runPromise(splitComma.run(Option.some("a,b,c"), {})) // => Option.some(["a", "b", "c"]) * ``` * * @see {@link splitKeyValue} when values are key-value pairs * - * @category string + * @category splitting * @since 4.0.0 */ export function split(options?: { @@ -1175,17 +1232,18 @@ export function split(options?: { * * **Example** (Encoding to Base64) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64() + * await Effect.runPromise(encode.run(Option.some(new Uint8Array([1, 2, 3])), {})) // => Option.some("AQID") * ``` * * @see {@link decodeBase64} for the inverse operation to `Uint8Array` * @see {@link decodeBase64String} for the inverse operation to `string` * @see {@link encodeBase64Url} for the URL-safe variant * - * @category Base64 getters + * @category encoding * @since 4.0.0 */ export function encodeBase64(): Getter { @@ -1201,17 +1259,18 @@ export function encodeBase64(): Getter * * **Example** (Encoding to Base64Url) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeBase64Url() + * await Effect.runPromise(encode.run(Option.some(new Uint8Array([251, 255])), {})) // => Option.some("-_8") * ``` * * @see {@link decodeBase64Url} for the inverse operation to `Uint8Array` * @see {@link decodeBase64UrlString} for the inverse operation to `string` * @see {@link encodeBase64} for the standard Base64 variant * - * @category Base64 getters + * @category encoding * @since 4.0.0 */ export function encodeBase64Url(): Getter { @@ -1227,16 +1286,17 @@ export function encodeBase64Url(): Getter() + * await Effect.runPromise(encode.run(Option.some(new Uint8Array([1, 2, 3])), {})) // => Option.some("010203") * ``` * * @see {@link decodeHex} for the inverse operation to `Uint8Array` * @see {@link decodeHexString} for the inverse operation to `string` * - * @category Hex getters + * @category encoding * @since 4.0.0 */ export function encodeHex(): Getter { @@ -1252,24 +1312,30 @@ export function encodeHex(): Getter { * * **Example** (Decoding Base64 to bytes) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64() - * // Getter + * const result = await Effect.runPromise(decode.run(Option.some("AQID"), {})) + * Option.map(result, Array.from) // => Option.some([1, 2, 3]) * ``` * * @see {@link decodeBase64String} to decode to `string` instead * @see {@link encodeBase64} for the inverse operation * - * @category Base64 getters + * @category decoding * @since 4.0.0 */ export function decodeBase64(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Effect.mapErrorEager( Effect.fromResult(Encoding.decodeBase64(input)), - (e) => new SchemaIssue.InvalidValue(Option.some(input), { message: e.message }) + () => + new SchemaIssue.InvalidValue( + { expected: "a valid Base64 string" }, + input, + options + ) ) ) } @@ -1283,23 +1349,30 @@ export function decodeBase64(): Getter { * * **Example** (Decoding Base64 to string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64String() - * // Getter + * await Effect.runPromise(decode.run(Option.some("aGVsbG8="), {})) // => Option.some("hello") * ``` * * @see {@link decodeBase64} to decode to `Uint8Array` instead * @see {@link encodeBase64} for the inverse operation * - * @category Base64 getters + * @category decoding * @since 4.0.0 */ export function decodeBase64String(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Result.match(Encoding.decodeBase64String(input), { - onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid Base64 string" }, + input, + options + ) + ), onSuccess: Effect.succeed }) ) @@ -1314,23 +1387,31 @@ export function decodeBase64String(): Getter { * * **Example** (Decoding Base64Url to bytes) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64Url() - * // Getter + * const result = await Effect.runPromise(decode.run(Option.some("-_8="), {})) + * Option.map(result, Array.from) // => Option.some([251, 255]) * ``` * * @see {@link decodeBase64UrlString} to decode to `string` instead * @see {@link encodeBase64Url} for the inverse operation * - * @category Base64 getters + * @category decoding * @since 4.0.0 */ export function decodeBase64Url(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Result.match(Encoding.decodeBase64Url(input), { - onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid Base64Url string" }, + input, + options + ) + ), onSuccess: Effect.succeed }) ) @@ -1345,23 +1426,30 @@ export function decodeBase64Url(): Getter { * * **Example** (Decoding Base64Url to string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeBase64UrlString() - * // Getter + * await Effect.runPromise(decode.run(Option.some("aGVsbG8"), {})) // => Option.some("hello") * ``` * * @see {@link decodeBase64Url} to decode to `Uint8Array` instead * @see {@link encodeBase64Url} for the inverse operation * - * @category Base64 getters + * @category decoding * @since 4.0.0 */ export function decodeBase64UrlString(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Result.match(Encoding.decodeBase64UrlString(input), { - onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid Base64Url string" }, + input, + options + ) + ), onSuccess: Effect.succeed }) ) @@ -1376,23 +1464,31 @@ export function decodeBase64UrlString(): Getter { * * **Example** (Decoding hex to bytes) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHex() - * // Getter + * const result = await Effect.runPromise(decode.run(Option.some("010203"), {})) + * Option.map(result, Array.from) // => Option.some([1, 2, 3]) * ``` * * @see {@link decodeHexString} to decode to `string` instead * @see {@link encodeHex} for the inverse operation * - * @category Hex getters + * @category decoding * @since 4.0.0 */ export function decodeHex(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Result.match(Encoding.decodeHex(input), { - onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid hexadecimal string" }, + input, + options + ) + ), onSuccess: Effect.succeed }) ) @@ -1407,23 +1503,30 @@ export function decodeHex(): Getter { * * **Example** (Decoding hex to string) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeHexString() - * // Getter + * await Effect.runPromise(decode.run(Option.some("68656c6c6f"), {})) // => Option.some("hello") * ``` * * @see {@link decodeHex} to decode to `Uint8Array` instead * @see {@link encodeHex} for the inverse operation * - * @category Hex getters + * @category decoding * @since 4.0.0 */ export function decodeHexString(): Getter { - return transformOrFail((input) => + return transformOrFail((input, options) => Result.match(Encoding.decodeHexString(input), { - onFailure: (e) => Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: e.message })), + onFailure: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid hexadecimal string" }, + input, + options + ) + ), onSuccess: Effect.succeed }) ) @@ -1440,15 +1543,16 @@ export function decodeHexString(): Getter { * * **Example** (Encoding a URI component) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeUriComponent() + * await Effect.runPromise(encode.run(Option.some("hello world"), {})) // => Option.some("hello%20world") * ``` * * @see {@link decodeUriComponent} for the inverse operation * - * @category URI + * @category encoding * @since 4.0.0 */ export function encodeUriComponent(): Getter { @@ -1464,27 +1568,29 @@ export function encodeUriComponent(): Getter { * * **Example** (Decoding a URI component) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeUriComponent() - * // Getter + * await Effect.runPromise(decode.run(Option.some("hello%20world"), {})) // => Option.some("hello world") * ``` * * @see {@link encodeUriComponent} for the inverse operation * - * @category URI + * @category decoding * @since 4.0.0 */ export function decodeUriComponent(): Getter { - return transformOrFail((input) => { + return transformOrFail((input, options) => { try { return Effect.succeed(globalThis.decodeURIComponent(input)) - } catch (e) { + } catch { return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(input), { - message: e instanceof URIError ? e.message : "Invalid URI component" - }) + new SchemaIssue.InvalidValue( + { expected: "a valid URI component" }, + input, + options + ) ) } }) @@ -1509,23 +1615,26 @@ export function decodeUriComponent(): Getter { * * **Example** (Parsing DateTime) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { DateTime, Effect, Option, SchemaGetter } from "effect" * * const parseDate = SchemaGetter.dateTimeUtcFromInput() - * // Getter + * const result = await Effect.runPromise(parseDate.run(Option.some("2024-01-01T00:00:00Z"), {})) + * Option.map(result, DateTime.toEpochMillis) // => Option.some(1704067200000) * ``` * * @see {@link Date} for a simpler coercion to `Date` (no validation) * - * @category DateTime + * @category converting * @since 4.0.0 */ export function dateTimeUtcFromInput(): Getter { - return transformOrFail((input) => { + return transformOrFail((input, options) => { return Option.match(DateTime.make(input), { onNone: () => - Effect.fail(new SchemaIssue.InvalidValue(Option.some(input), { message: "Invalid DateTime input" })), + Effect.fail( + new SchemaIssue.InvalidValue({ message: "Invalid DateTime input" }, input, options) + ), onSome: (dt) => Effect.succeed(DateTime.toUtc(dt)) }) }) @@ -1547,18 +1656,20 @@ export function dateTimeUtcFromInput(): Gette * * **Example** (Decoding FormData) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeFormData() - * // Getter, FormData> + * const formData = new FormData() + * formData.append("user[name]", "Alice") + * await Effect.runPromise(decode.run(Option.some(formData), {})) // => Option.some({ user: { name: "Alice" } }) * ``` * - * @see {@link encodeFormData} for the inverse operation + * @see {@link encodeFormData} for the corresponding encoder * @see {@link makeTreeRecord} for the underlying bracket-path parser * @see {@link decodeURLSearchParams} for the URLSearchParams variant * - * @category FormData + * @category decoding * @since 4.0.0 */ export function decodeFormData(): Getter, FormData> { @@ -1585,18 +1696,19 @@ const collectFormDataEntries = collectBracketPathEntries((value): value is strin * * **Example** (Encoding to FormData) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeFormData() - * // Getter + * const result = await Effect.runPromise(encode.run(Option.some({ name: "Alice" }), {})) + * Option.map(result, (formData) => formData.get("name")) // => Option.some("Alice") * ``` * - * @see {@link decodeFormData} for the inverse operation + * @see {@link decodeFormData} for the corresponding decoder * @see {@link collectBracketPathEntries} for the underlying flattener * @see {@link encodeURLSearchParams} for the URLSearchParams variant * - * @category FormData + * @category encoding * @since 4.0.0 */ export function encodeFormData(): Getter { @@ -1628,18 +1740,19 @@ export function encodeFormData(): Getter { * * **Example** (Decoding URLSearchParams) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const decode = SchemaGetter.decodeURLSearchParams() - * // Getter, URLSearchParams> + * const params = new URLSearchParams("user[name]=Alice") + * await Effect.runPromise(decode.run(Option.some(params), {})) // => Option.some({ user: { name: "Alice" } }) * ``` * - * @see {@link encodeURLSearchParams} for the inverse operation + * @see {@link encodeURLSearchParams} for the corresponding encoder * @see {@link makeTreeRecord} for the underlying bracket-path parser * @see {@link decodeFormData} for the FormData variant * - * @category search params + * @category decoding * @since 4.0.0 */ export function decodeURLSearchParams(): Getter, URLSearchParams> { @@ -1663,18 +1776,19 @@ const collectURLSearchParamsEntries = collectBracketPathEntries(Predicate.isStri * * **Example** (Encoding to URLSearchParams) * - * ```ts - * import { SchemaGetter } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaGetter } from "effect" * * const encode = SchemaGetter.encodeURLSearchParams() - * // Getter + * const result = await Effect.runPromise(encode.run(Option.some({ name: "Alice" }), {})) + * Option.map(result, (params) => params.toString()) // => Option.some("name=Alice") * ``` * - * @see {@link decodeURLSearchParams} for the inverse operation + * @see {@link decodeURLSearchParams} for the corresponding decoder * @see {@link collectBracketPathEntries} for the underlying flattener * @see {@link encodeFormData} for the FormData variant * - * @category search params + * @category encoding * @since 4.0.0 */ export function encodeURLSearchParams(): Getter { @@ -1704,20 +1818,6 @@ function bracketPathToTokens(bracketPath: string): Array { .map((part) => (INDEX_REGEXP.test(part) ? globalThis.Number(part) : part)) } -function getOrCreateContainer( - self: any, - key: PropertyKey, - shouldBeArray: boolean -): any { - const current = Object.hasOwn(self, key) ? self[key] : undefined - if (current !== undefined) { - return current - } - const container = shouldBeArray ? [] : {} - InternalRecord.set(self, key, container) - return container -} - /** * Builds a nested tree object from a list of bracket-path entries. * @@ -1740,31 +1840,47 @@ function getOrCreateContainer( * - `"foo[]"` → append to array `foo` * - `""` → real empty key * - Duplicate keys for the same path are merged into arrays. + * - If a structural path conflicts with a previous leaf or a different container + * type, the later structural path replaces the conflicting value. + * - The notation has no escaping for `.`, `[` or `]`, so keys containing these + * delimiters cannot be round-tripped without changing their structure. * * **Example** (Building a tree from bracket paths) * - * ```ts + * ```ts import.meta.vitest * import { SchemaGetter } from "effect" * - * const tree = SchemaGetter.makeTreeRecord([ + * SchemaGetter.makeTreeRecord([ * ["user[name]", "Alice"], * ["user[tags][]", "admin"], * ["user[tags][]", "editor"] - * ]) - * // { user: { name: "Alice", tags: ["admin", "editor"] } } + * ]) // => { user: { name: "Alice", tags: ["admin", "editor"] } } * ``` * - * @see {@link collectBracketPathEntries} for the inverse operation (tree to flat entries) + * @see {@link collectBracketPathEntries} for flattening trees into bracket-path entries * @see {@link decodeFormData} for a higher-level FormData decoder * @see {@link decodeURLSearchParams} for a higher-level URLSearchParams decoder * - * @category Tree + * @category constructors * @since 4.0.0 */ export function makeTreeRecord( bracketPathEntries: ReadonlyArray ): Schema.TreeRecord { const out: any = {} + const containers = new WeakSet() + + function getOrCreateContainer(self: any, key: PropertyKey, shouldBeArray: boolean): any { + const current = Object.hasOwn(self, key) ? self[key] : undefined + if (containers.has(current) && Array.isArray(current) === shouldBeArray) { + return current + } + const container = shouldBeArray ? [] : {} + containers.add(container) + InternalRecord.assignProperty(self, key, container) + return container + } + bracketPathEntries.forEach(([key, value]) => { const tokens = bracketPathToTokens(key) let cur: any = out @@ -1789,9 +1905,9 @@ export function makeTreeRecord( if (hasOwn && Array.isArray(cur[token])) { cur[token].push(value) } else if (hasOwn) { - InternalRecord.set(cur, token, [cur[token], value]) + InternalRecord.assignProperty(cur, token, [cur[token], value]) } else { - InternalRecord.set(cur, token, value) + InternalRecord.assignProperty(cur, token, value) } } else { const next = tokens[i + 1] @@ -1815,7 +1931,6 @@ export function makeTreeRecord( * * **Details** * - * - This is the inverse of {@link makeTreeRecord}. * - Takes a nested object and produces flat `[bracketPath, value]` pairs suitable for * `FormData` or `URLSearchParams`. * - Returns a curried function: first call provides the leaf type guard, second call provides the object. @@ -1823,22 +1938,25 @@ export function makeTreeRecord( * - If all elements of an array are leaves, encodes them as multiple entries with the same key * (e.g. `tags=a&tags=b`). Otherwise uses indexed bracket paths (e.g. `items[0]`, `items[1]`). * - Non-leaf values that aren't objects or arrays are silently skipped. + * - Empty arrays and objects produce no entries, and path delimiters in property + * names are not escaped. The resulting format is therefore lossy. * * **Example** (Flattening an object to bracket paths) * - * ```ts + * ```ts import.meta.vitest * import { Predicate, SchemaGetter } from "effect" * * const collectStrings = SchemaGetter.collectBracketPathEntries(Predicate.isString) * const entries = collectStrings({ user: { name: "Alice", tags: ["admin", "editor"] } }) - * // [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]] + * + * entries // => [["user[name]", "Alice"], ["user[tags]", "admin"], ["user[tags]", "editor"]] * ``` * - * @see {@link makeTreeRecord} for the inverse operation (flat entries to tree) + * @see {@link makeTreeRecord} for building trees from bracket-path entries * @see {@link encodeFormData} for a higher-level FormData encoder * @see {@link encodeURLSearchParams} for a higher-level URLSearchParams encoder * - * @category Tree + * @category converting * @since 4.0.0 */ export function collectBracketPathEntries(isLeaf: (value: unknown) => value is A) { diff --git a/.context/effect/packages/effect/src/SchemaIssue.ts b/.context/effect/packages/effect/src/SchemaIssue.ts index 365500b5f..16381f3aa 100644 --- a/.context/effect/packages/effect/src/SchemaIssue.ts +++ b/.context/effect/packages/effect/src/SchemaIssue.ts @@ -5,8 +5,7 @@ * An `Issue` records what failed and, for nested data, where the failure * happened. The Schema system uses these values for missing keys, unexpected * keys, invalid types, invalid values, failed filters, failed transformations, - * and alternatives that did not match. This module also formats issues and - * supports redaction for sensitive values. + * and alternatives that did not match. This module also formats issues. * * @since 4.0.0 */ @@ -14,9 +13,8 @@ import type { StandardSchemaV1 } from "@standard-schema/spec" import * as Arr from "./Array.ts" import { format, formatPath, type Formatter as FormatterI } from "./Formatter.ts" import * as InternalAnnotations from "./internal/schema/annotations.ts" -import * as Option from "./Option.ts" +import * as InternalParser from "./internal/schema/parser.ts" import { hasProperty } from "./Predicate.ts" -import * as Redacted from "./Redacted.ts" import type * as Schema from "./Schema.ts" import type * as SchemaAST from "./SchemaAST.ts" @@ -37,14 +35,12 @@ const TypeId = "~effect/SchemaIssue/Issue" * * **Example** (Type-guarding an unknown error) * - * ```ts + * ```ts import.meta.vitest * import { SchemaIssue } from "effect" * * const issue = new SchemaIssue.MissingKey(undefined) - * console.log(SchemaIssue.isIssue(issue)) - * // true - * console.log(SchemaIssue.isIssue("not an issue")) - * // false + * SchemaIssue.isIssue(issue) // => true + * SchemaIssue.isIssue("not an issue") // => false * ``` * * @see {@link Issue} @@ -53,7 +49,40 @@ const TypeId = "~effect/SchemaIssue/Issue" * @since 4.0.0 */ export function isIssue(u: unknown): u is Issue { - return hasProperty(u, TypeId) + return hasProperty(u, TypeId) && u[TypeId] === TypeId +} + +/** + * Returns `true` when an issue contains an input reported by the schema parser. + * + * **When to use** + * + * Use when reading `Issue.input`, especially when `undefined` is a valid input + * value. + * + * **Details** + * + * Reported input is stored as an own property. This guard checks for that + * property and narrows `input` from optional to required. + * + * **Example** (Reading a reported input) + * + * ```ts import.meta.vitest + * import { Result, Schema, SchemaIssue } from "effect" + * + * const result = Schema.decodeUnknownResult(Schema.String)(1, { reportInput: true }) + * if (Result.isFailure(result) && SchemaIssue.hasInput(result.failure.issue)) { + * result.failure.issue.input // => 1 + * } + * ``` + * + * @see {@link Issue} for the complete issue model + * + * @category guards + * @since 4.0.0 + */ +export function hasInput(issue: Issue): issue is Issue & { readonly input: unknown } { + return Object.hasOwn(issue, "input") } /** @@ -96,13 +125,17 @@ export type Leaf = * Every node has a `_tag` field for pattern-matching. The union includes both * terminal {@link Leaf} types and composite types that wrap inner issues: * {@link Filter}, {@link Encoding}, {@link Pointer}, {@link Composite}, - * {@link AnyOf}. All `Issue` instances have a `toString()` that delegates to - * the default formatter, so `String(issue)` produces a human-readable message. + * {@link AnyOf}. Use {@link makeFormatterDefault} when a human-readable + * representation is needed. When parsing with `reportInput: true`, + * value-bearing issues expose the rejected value through an enumerable `input` + * field. Built-in formatters may include reported input in default messages. This + * is not a general sanitization boundary: paths, ASTs, union successes, and + * custom annotations or messages are preserved as supplied and remain the + * caller's responsibility. * * @see {@link Leaf} — the terminal subset * @see {@link isIssue} — type guard - * @see {@link getActual} — extract the actual value from any issue - * + * @see {@link hasInput} — checks whether an issue reports an input * @category models * @since 4.0.0 */ @@ -117,8 +150,15 @@ export type Issue = class Base { readonly [TypeId] = TypeId - toString(this: Issue): string { - return defaultFormatter(this) + /** + * The input reported by the schema parser, when input reporting is enabled + * and the issue is associated with a present value. + */ + declare readonly input?: unknown + constructor(input?: unknown, options?: SchemaAST.ParseOptions) { + if (options?.reportInput === true && input !== InternalParser.missing) { + this.input = input + } } } @@ -132,22 +172,28 @@ class Base { * * **Details** * - * - `actual` is the raw input value that was tested (plain `unknown`, not - * wrapped in `Option`). * - `filter` is the AST filter node that produced this issue. * - `issue` is the inner issue describing the failure reason. * * **Example** (Matching a Filter issue) * - * ```ts - * import { SchemaIssue } from "effect" + * ```ts import.meta.vitest + * import { SchemaAST, SchemaIssue } from "effect" + * + * const formatIssue = SchemaIssue.makeFormatterDefault() * * function describe(issue: SchemaIssue.Issue): string { * if (issue._tag === "Filter") { - * return `Filter failed on: ${JSON.stringify(issue.actual)}` + * return `Filter failed: ${formatIssue(issue.issue)}` * } - * return String(issue) + * return formatIssue(issue) * } + * + * const issue = new SchemaIssue.Filter( + * SchemaAST.isPattern(/^valid$/), + * new SchemaIssue.InvalidValue() + * ) + * describe(issue) // => `Filter failed: Expected a valid value` * ``` * * @see {@link Leaf} — terminal issue types that commonly appear as the inner `issue` @@ -158,10 +204,6 @@ class Base { */ export class Filter extends Base { readonly _tag = "Filter" - /** - * The input value that caused the issue. - */ - readonly actual: unknown /** * The filter that failed. */ @@ -172,10 +214,6 @@ export class Filter extends Base { readonly issue: Issue constructor( - /** - * The input value that caused the issue. - */ - actual: unknown, /** * The filter that failed. */ @@ -183,10 +221,18 @@ export class Filter extends Base { /** * The issue that occurred. */ - issue: Issue + issue: Issue, + /** + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. + */ + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() - this.actual = actual + super(input, options) this.filter = filter this.issue = issue } @@ -203,8 +249,6 @@ export class Filter extends Base { * **Details** * * - `ast` is the AST node for the transformation that failed. - * - `actual` is `Option.some(value)` when the input was present, or - * `Option.none()` when it was absent. * - `issue` is the inner issue describing the failure. * * @see {@link Filter} — failure from a refinement check (not a transformation) @@ -219,10 +263,6 @@ export class Encoding extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.AST - /** - * The input value that caused the issue. - */ - readonly actual: Option.Option /** * The issue that occurred. */ @@ -234,17 +274,21 @@ export class Encoding extends Base { */ ast: SchemaAST.AST, /** - * The input value that caused the issue. + * The issue that occurred. */ - actual: Option.Option, + issue: Issue, /** - * The issue that occurred. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - issue: Issue + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual this.issue = issue } } @@ -261,11 +305,9 @@ export class Encoding extends Base { * **Details** * * - `path` is an array of property keys (strings, numbers, or symbols). - * - Has no `actual` value — {@link getActual} returns `Option.none()`. * - Formatters concatenate nested `Pointer` paths into a single path like * `["a"]["b"][0]`. * - * @see {@link getActual} — returns `Option.none()` for `Pointer` * @see {@link Composite} — groups multiple issues under one schema node * * @category models @@ -307,7 +349,6 @@ export class Pointer extends Base { * * **Details** * - * - Has no `actual` value — {@link getActual} returns `Option.none()`. * - `annotations` may contain a custom `messageMissingKey` for formatting. * * @see {@link Pointer} — wraps this issue with the missing key's path @@ -345,9 +386,10 @@ export class MissingKey extends Base { * * **Details** * - * - `actual` is the raw value at the unexpected key (plain `unknown`). * - `ast` is the schema that was being validated against. * - `annotations` on `ast` may contain a custom `messageUnexpectedKey`. + * - The default formatter renders this as `"Expected no excess property"`, or + * `"Unexpected key with value "` when the issue reports an input. * * @see {@link MissingKey} — the opposite case (required key absent) * @see {@link Pointer} — wraps this issue with the unexpected key's path @@ -361,24 +403,23 @@ export class UnexpectedKey extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.AST - /** - * The input value that caused the issue. - */ - readonly actual: unknown - constructor( /** * The schema that caused the issue. */ ast: SchemaAST.AST, /** - * The input value that caused the issue. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. + */ + input?: unknown, + /** + * The effective parse options controlling input retention. */ - actual: unknown + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual } } @@ -393,8 +434,6 @@ export class UnexpectedKey extends Base { * **Details** * * - `issues` is a non-empty readonly array (at least one child). - * - `actual` is `Option.some(value)` when the input was present, or - * `Option.none()` when absent. * - Formatters flatten `Composite` by recursing into each child. * * @see {@link AnyOf} — used for union no-match errors (similar but different semantics) @@ -409,10 +448,6 @@ export class Composite extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.AST - /** - * The input value that caused the issue. - */ - readonly actual: Option.Option /** * The issues that occurred. */ @@ -424,24 +459,28 @@ export class Composite extends Base { */ ast: SchemaAST.AST, /** - * The input value that caused the issue. + * The issues that occurred. */ - actual: Option.Option, + issues: readonly [Issue, ...Array], /** - * The issues that occurred. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - issues: readonly [Issue, ...Array] + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual this.issues = issues } } /** * Represents a schema issue produced when the runtime type of the input does not match the type - * expected by the schema (e.g. got `null` when `string` was expected). + * expected by the schema. * * **When to use** * @@ -451,23 +490,17 @@ export class Composite extends Base { * **Details** * * - `ast` is the schema node that expected a different type. - * - `actual` is `Option.some(value)` when the input was present, or - * `Option.none()` when no value was provided. - * - The default formatter renders this as `"Expected , got "`. + * - The default formatter renders this as `"Expected "`, adding + * `", got "` when the issue reports an input. * - * **Example** (Formatting output) + * **Example** (Formatting a type mismatch) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Schema, SchemaIssue } from "effect" * - * try { - * Schema.decodeUnknownSync(Schema.String)(42) - * } catch (e) { - * if (Schema.isSchemaError(e)) { - * console.log(String(e.issue)) - * // "Expected string, got 42" - * } - * } + * const formatIssue = SchemaIssue.makeFormatterDefault() + * const issue = new SchemaIssue.InvalidType(Schema.String.ast) + * formatIssue(issue) // => "Expected string" * ``` * * @see {@link InvalidValue} — the input has the right type but fails a value constraint @@ -481,24 +514,23 @@ export class InvalidType extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.AST - /** - * The input value that caused the issue. - */ - readonly actual: Option.Option - constructor( /** * The schema that caused the issue. */ ast: SchemaAST.AST, /** - * The input value that caused the issue. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - actual: Option.Option + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual } } @@ -513,23 +545,22 @@ export class InvalidType extends Base { * * **Details** * - * - `actual` is `Option.some(value)` when the failing value is known, or - * `Option.none()` when absent. - * - `annotations` optionally carries a `message` string for formatting. - * - The default formatter renders this as `"Invalid data "` unless a - * custom `message` annotation is provided. + * - A `message` annotation is returned unchanged and takes precedence over all + * other default formatting. + * - Without `message`, an `expected` annotation is formatted as + * `"Expected "`, adding `", got "` when input is reported. + * - Without either annotation, the default formatter renders + * `"Expected a valid value"`, or `"Invalid data "` when input is + * reported. * * **Example** (Returning InvalidValue from a custom filter) * - * ```ts - * import { Option, SchemaIssue } from "effect" + * ```ts import.meta.vitest + * import { SchemaIssue } from "effect" * - * const issue = new SchemaIssue.InvalidValue( - * Option.some(""), - * { message: "must not be empty" } - * ) - * console.log(String(issue)) - * // "must not be empty" + * const formatIssue = SchemaIssue.makeFormatterDefault() + * const issue = new SchemaIssue.InvalidValue({ message: "must not be empty" }) + * formatIssue(issue) // => "must not be empty" * ``` * * @see {@link InvalidType} — the input has the wrong type entirely @@ -540,10 +571,6 @@ export class InvalidType extends Base { */ export class InvalidValue extends Base { readonly _tag = "InvalidValue" - /** - * The value that caused the issue. - */ - readonly actual: Option.Option /** * The metadata for the issue. */ @@ -551,20 +578,40 @@ export class InvalidValue extends Base { constructor( /** - * The value that caused the issue. + * The metadata for the issue. */ - actual: Option.Option, + annotations?: Schema.Annotations.Issue | undefined, /** - * The metadata for the issue. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - annotations?: Schema.Annotations.Issue | undefined + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() - this.actual = actual + super(input, options) this.annotations = annotations } } +/** @internal */ +export function makeCompositeAtKey( + compositeAst: SchemaAST.AST, + pointerKey: PropertyKey, + pointerIssue: Issue, + compositeInput: unknown, + parseOptions?: SchemaAST.ParseOptions +): Composite { + return new Composite( + compositeAst, + [new Pointer([pointerKey], pointerIssue)], + compositeInput, + parseOptions + ) +} + /** * Represents a schema issue produced when a forbidden operation is encountered during parsing, * such as an asynchronous Effect running inside `Schema.decodeUnknownSync`. @@ -576,22 +623,19 @@ export class InvalidValue extends Base { * * **Details** * - * - `actual` is `Option.some(value)` when the input is known, or - * `Option.none()` when absent. * - `annotations` optionally carries a `message` string. * - The default formatter renders this as `"Forbidden operation"`. * * **Example** (Creating a Forbidden issue) * - * ```ts - * import { Option, SchemaIssue } from "effect" + * ```ts import.meta.vitest + * import { SchemaIssue } from "effect" * + * const formatIssue = SchemaIssue.makeFormatterDefault() * const issue = new SchemaIssue.Forbidden( - * Option.none(), * { message: "async operation not allowed in sync context" } * ) - * console.log(String(issue)) - * // "async operation not allowed in sync context" + * formatIssue(issue) // => "async operation not allowed in sync context" * ``` * * @see {@link InvalidValue} — for value-constraint failures (not operation failures) @@ -601,10 +645,6 @@ export class InvalidValue extends Base { */ export class Forbidden extends Base { readonly _tag = "Forbidden" - /** - * The input value that caused the issue. - */ - readonly actual: Option.Option /** * The metadata for the issue. */ @@ -612,16 +652,20 @@ export class Forbidden extends Base { constructor( /** - * The input value that caused the issue. + * The metadata for the issue. */ - actual: Option.Option, + annotations: Schema.Annotations.Issue | undefined, /** - * The metadata for the issue. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - annotations: Schema.Annotations.Issue | undefined + input?: unknown, + /** + * The effective parse options controlling input retention. + */ + options?: SchemaAST.ParseOptions ) { - super() - this.actual = actual + super(input, options) this.annotations = annotations } } @@ -637,9 +681,13 @@ export class Forbidden extends Base { * **Details** * * - `ast` is the `Union` AST node. - * - `actual` is the raw input value (plain `unknown`). - * - `issues` contains per-member failures. When empty, the formatter falls - * back to the union's `expected` annotation. + * - `issues` contains the per-member failures. + * + * **Gotchas** + * + * `issues` is empty when no union member was applicable. In that case, the + * default formatter reports the expected type for the union and appends + * `", got "` when input is reported. * * @see {@link OneOf} — the opposite: *too many* members matched * @see {@link Composite} — groups multiple issues under a non-union schema @@ -653,10 +701,6 @@ export class AnyOf extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.Union - /** - * The input value that caused the issue. - */ - readonly actual: unknown /** * The issues that occurred. */ @@ -668,17 +712,21 @@ export class AnyOf extends Base { */ ast: SchemaAST.Union, /** - * The input value that caused the issue. + * The issues that occurred. + */ + issues: ReadonlyArray, + /** + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. */ - actual: unknown, + input?: unknown, /** - * The issues that occurred. + * The effective parse options controlling input retention. */ - issues: ReadonlyArray + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual this.issues = issues } } @@ -695,10 +743,11 @@ export class AnyOf extends Base { * **Details** * * - `ast` is the `Union` AST node. - * - `actual` is the raw input value (plain `unknown`). * - `successes` lists the AST nodes of each member that accepted the input. * - The default formatter renders this as - * `"Expected exactly one member to match the input "`. + * `"Expected exactly one member to match"`, or + * `"Expected exactly one member to match the input "` when input is + * reported. * * @see {@link AnyOf} — the opposite: *no* members matched * @@ -711,10 +760,6 @@ export class OneOf extends Base { * The schema that caused the issue. */ readonly ast: SchemaAST.Union - /** - * The input value that caused the issue. - */ - readonly actual: unknown /** * The schemas that were successful. */ @@ -726,110 +771,73 @@ export class OneOf extends Base { */ ast: SchemaAST.Union, /** - * The input value that caused the issue. + * The schemas that were successful. */ - actual: unknown, + successes: ReadonlyArray, /** - * The schemas that were successful. + * The present input associated with the issue. It is retained only when + * `options.reportInput` is `true`. + */ + input?: unknown, + /** + * The effective parse options controlling input retention. */ - successes: ReadonlyArray + options?: SchemaAST.ParseOptions ) { - super() + super(input, options) this.ast = ast - this.actual = actual this.successes = successes } } -/** - * Extracts the actual input value from any {@link Issue} variant. - * - * **When to use** - * - * Use when you need to retrieve an `Issue`'s offending input value for logging - * or custom error rendering. - * - * **Details** - * - * - Returns `Option.none()` for `Pointer` and `MissingKey` (they carry no - * value). - * - Returns the existing `Option` for variants that already store `actual` as - * `Option` (`InvalidType`, `InvalidValue`, `Forbidden`, `Encoding`, - * `Composite`). - * - Wraps `actual` with `Option.some` for variants that store it as plain - * `unknown` (`AnyOf`, `UnexpectedKey`, `OneOf`, `Filter`). - * - * **Example** (Extracting the actual value) - * - * ```ts - * import { Option, SchemaIssue } from "effect" - * - * const issue = new SchemaIssue.MissingKey(undefined) - * console.log(SchemaIssue.getActual(issue)) - * // { _tag: "None" } - * ``` - * - * @see {@link Issue} - * @see {@link isIssue} - * - * @category getters - * @since 4.0.0 - */ -export function getActual(issue: Issue): Option.Option { - switch (issue._tag) { - case "Pointer": - case "MissingKey": - return Option.none() - case "InvalidType": - case "InvalidValue": - case "Forbidden": - case "Encoding": - case "Composite": - return issue.actual - case "AnyOf": - case "UnexpectedKey": - case "OneOf": - case "Filter": - return Option.some(issue.actual) - } -} - -function makeFilterIssue(input: unknown, entry: Schema.FilterIssue): Issue { +function makeFilterIssue( + entry: Schema.FilterIssue, + input?: unknown, + options?: SchemaAST.ParseOptions +): Issue { if (isIssue(entry)) { return entry } if (typeof entry === "string") { - return new InvalidValue(Option.some(input), { message: entry }) + return new InvalidValue({ message: entry }, input, options) } const inner = typeof entry.issue === "string" - ? new InvalidValue(Option.some(input), { message: entry.issue }) + ? new InvalidValue({ message: entry.issue }, input, options) : entry.issue return new Pointer(entry.path, inner) } /** @internal */ -export function makeSingle(input: unknown, out: undefined | boolean | Schema.FilterIssue): Issue | undefined { +export function makeSingle( + out: undefined | boolean | Schema.FilterIssue, + input?: unknown, + options?: SchemaAST.ParseOptions +): Issue | undefined { if (out === undefined) { return undefined } if (typeof out === "boolean") { - return out ? undefined : new InvalidValue(Option.some(input)) + return out ? undefined : new InvalidValue(undefined, input, options) } - return makeFilterIssue(input, out) + return makeFilterIssue(out, input, options) } /** @internal */ -export function make(input: unknown, ast: SchemaAST.AST, out: Schema.FilterOutput): Issue | undefined { +export function normalizeFilterOutput( + ast: SchemaAST.AST, + out: Schema.FilterOutput, + input?: unknown, + options?: SchemaAST.ParseOptions +): Issue | undefined { if (Array.isArray(out)) { - if (Arr.isReadonlyArrayNonEmpty(out)) { - if (out.length === 1) { - return makeFilterIssue(input, out[0]) - } - return new Composite(ast, Option.some(input), Arr.map(out, (entry) => makeFilterIssue(input, entry))) + if (!Arr.isReadonlyArrayNonEmpty(out)) { + return undefined } - return undefined + return out.length === 1 + ? makeFilterIssue(out[0], input, options) + : new Composite(ast, Arr.map(out, (entry) => makeFilterIssue(entry, input, options)), input, options) } - return makeSingle(input, out as undefined | boolean | Schema.FilterIssue) + return makeSingle(out as undefined | boolean | Schema.FilterIssue, input, options) } /** @@ -840,7 +848,7 @@ export function make(input: unknown, ast: SchemaAST.AST, out: Schema.FilterOutpu * @see {@link makeFormatterDefault} — creates a `Formatter` * @see {@link makeFormatterStandardSchemaV1} — creates a `Formatter` * - * @category Formatter + * @category formatting * @since 4.0.0 */ export interface Formatter extends FormatterI {} @@ -856,7 +864,7 @@ export interface Formatter extends FormatterI {} * @see {@link defaultLeafHook} — the built-in implementation * @see {@link Leaf} — the union of terminal issue types * - * @category Formatter + * @category formatting * @since 4.0.0 */ export type LeafHook = (issue: Leaf) => string @@ -871,28 +879,34 @@ export type LeafHook = (issue: Leaf) => string * **Details** * * - Checks for a `message` annotation first; returns it if present. - * - Otherwise generates a default message per `_tag`: - * - `InvalidType` → `"Expected , got "` - * - `InvalidValue` → `"Invalid data "` + * - For `InvalidValue`, an `expected` annotation uses the standard expected + * value message and includes reported input when available. + * - Otherwise generates a default message per `_tag`. When the issue reports + * input, the message includes its formatted value where applicable: + * - `InvalidType` → `"Expected "` or `"Expected , got "` + * - `InvalidValue` → `"Expected a valid value"` or `"Invalid data "` * - `MissingKey` → `"Missing key"` - * - `UnexpectedKey` → `"Unexpected key with value "` + * - `UnexpectedKey` → `"Expected no excess property"` or + * `"Unexpected key with value "` * - `Forbidden` → `"Forbidden operation"` - * - `OneOf` → `"Expected exactly one member to match the input "` + * - `OneOf` → `"Expected exactly one member to match"` or + * `"Expected exactly one member to match the input "` * * **Example** (Formatting Standard Schema issues with defaultLeafHook) * - * ```ts + * ```ts import.meta.vitest * import { SchemaIssue } from "effect" * * const formatter = SchemaIssue.makeFormatterStandardSchemaV1({ * leafHook: SchemaIssue.defaultLeafHook * }) + * formatter(new SchemaIssue.MissingKey(undefined)) // => { issues: [{ path: [], message: "Missing key" }] } * ``` * * @see {@link LeafHook} * @see {@link makeFormatterStandardSchemaV1} * - * @category Formatter + * @category formatting * @since 4.0.0 */ export const defaultLeafHook: LeafHook = (issue): string => { @@ -900,17 +914,27 @@ export const defaultLeafHook: LeafHook = (issue): string => { if (message !== undefined) return message switch (issue._tag) { case "InvalidType": - return getExpectedMessage(InternalAnnotations.getExpected(issue.ast), formatOption(issue.actual)) - case "InvalidValue": - return `Invalid data ${formatOption(issue.actual)}` + return getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) + case "InvalidValue": { + const expected = findExpected(issue) + if (expected !== undefined) return getExpectedMessage(expected, issue) + const input = formatInput(issue) + return input === undefined ? "Expected a valid value" : `Invalid data ${input}` + } case "MissingKey": return "Missing key" - case "UnexpectedKey": - return `Unexpected key with value ${format(issue.actual)}` + case "UnexpectedKey": { + const input = formatInput(issue) + return input === undefined ? "Expected no excess property" : `Unexpected key with value ${input}` + } case "Forbidden": return "Forbidden operation" - case "OneOf": - return `Expected exactly one member to match the input ${format(issue.actual)}` + case "OneOf": { + const input = formatInput(issue) + return input === undefined + ? "Expected exactly one member to match" + : `Expected exactly one member to match the input ${input}` + } } } @@ -926,11 +950,10 @@ export const defaultLeafHook: LeafHook = (issue): string => { * * - Returns `string` to override the message, or `undefined` to fall back to * the default formatting. - * * @see {@link defaultCheckHook} — the built-in implementation * @see {@link Filter} — the issue type this hook formats * - * @category Formatter + * @category formatting * @since 4.0.0 */ export type CheckHook = (issue: Filter) => string | undefined @@ -947,17 +970,16 @@ export type CheckHook = (issue: Filter) => string | undefined * - Looks for a `message` annotation on the inner issue first, then on the * filter itself. * - Returns `undefined` when no annotation is found, causing the formatter to - * fall back to `"Expected , got "`. + * fall back to `"Expected "` or, when the filter reports input, + * `"Expected , got "`. * * @see {@link CheckHook} * @see {@link makeFormatterStandardSchemaV1} * - * @category Formatter + * @category formatting * @since 4.0.0 */ -export const defaultCheckHook: CheckHook = (issue): string | undefined => { - return findMessage(issue.issue) ?? findMessage(issue) -} +export const defaultCheckHook: CheckHook = (issue): string | undefined => findMessage(issue.issue) ?? findMessage(issue) /** * Creates a {@link Formatter} that produces a `StandardSchemaV1.FailureResult`. @@ -975,20 +997,30 @@ export const defaultCheckHook: CheckHook = (issue): string | undefined => { * - `Pointer` paths are accumulated to produce full property paths. * - Falls back to {@link defaultLeafHook} / {@link defaultCheckHook} when no * hooks are provided. + * - Default messages include reported input when the issue that produces the + * message has an `input` field. The returned Standard Schema issues do not + * receive an `input` field. + * + * **Gotchas** + * + * Reported input can appear inside the Standard Schema `message` string even + * though it is not exposed as a separate property. Custom hooks control their + * complete message and are not modified. * * **Example** (Creating a Standard Schema V1 formatter) * - * ```ts + * ```ts import.meta.vitest * import { SchemaIssue } from "effect" * * const formatter = SchemaIssue.makeFormatterStandardSchemaV1() + * formatter(new SchemaIssue.MissingKey(undefined)).issues[0].message // => "Missing key" * ``` * * @see {@link makeFormatterDefault} — produces a plain string instead * @see {@link LeafHook} * @see {@link CheckHook} * - * @category Formatter + * @category formatting * @since 4.0.0 */ export function makeFormatterStandardSchemaV1(options?: { @@ -1006,8 +1038,18 @@ type DefaultIssue = { readonly path: ReadonlyArray } -function getExpectedMessage(expected: string, actual: string): string { - return `Expected ${expected}, got ${actual}` +function formatInput(issue: Issue): string | undefined { + return hasInput(issue) ? format(issue.input) : undefined +} + +function findExpected(issue: InvalidValue): string | undefined { + const expected = issue.annotations?.expected + return typeof expected === "string" ? expected : undefined +} + +function getExpectedMessage(expected: string, issue: Issue): string { + const input = formatInput(issue) + return input === undefined ? `Expected ${expected}` : `Expected ${expected}, got ${input}` } function toDefaultIssues( @@ -1022,15 +1064,16 @@ function toDefaultIssues( if (message !== undefined) { return [{ path, message }] } - switch (issue.issue._tag) { - case "InvalidValue": - return [{ - path, - message: getExpectedMessage(formatCheck(issue.filter), format(issue.actual)) - }] - default: - return toDefaultIssues(issue.issue, path, leafHook, checkHook) + if (issue.issue._tag !== "InvalidValue") { + return toDefaultIssues(issue.issue, path, leafHook, checkHook) } + const expected = findExpected(issue.issue) + return [{ + path, + message: expected === undefined + ? getExpectedMessage(formatCheck(issue.filter), issue) + : getExpectedMessage(expected, issue.issue) + }] } case "Encoding": return toDefaultIssues(issue.issue, path, leafHook, checkHook) @@ -1039,12 +1082,11 @@ function toDefaultIssues( case "Composite": return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) case "AnyOf": { - const message = findMessage(issue) if (issue.issues.length === 0) { - if (message !== undefined) return [{ path, message }] - - const expected = getExpectedMessage(InternalAnnotations.getExpected(issue.ast), format(issue.actual)) - return [{ path, message: expected }] + return [{ + path, + message: findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) + }] } return issue.issues.flatMap((issue) => toDefaultIssues(issue, path, leafHook, checkHook)) } @@ -1076,101 +1118,92 @@ function formatCheck(check: SchemaAST.Check): string { * * **Details** * - * This is the default formatter used by `SchemaIssue.toString()`. - * * - Flattens the issue tree into `{ message, path }` entries using * {@link defaultLeafHook} and {@link defaultCheckHook}. + * - Includes reported input in default messages when the node producing the + * message has an `input` field. * - Each entry is rendered as `""` or `"\n at "`. * - Multiple entries are joined with newlines. * + * **Gotchas** + * + * Formatting an issue can disclose input retained with `reportInput: true`. + * Wrapper inputs are not inherited by child messages, and custom messages are + * returned unchanged. + * * **Example** (Formatting an issue as a string) * - * ```ts + * ```ts import.meta.vitest * import { SchemaIssue } from "effect" * * const formatter = SchemaIssue.makeFormatterDefault() + * formatter(new SchemaIssue.MissingKey(undefined)) // => "Missing key" * ``` * * @see {@link makeFormatterStandardSchemaV1} — produces Standard Schema V1 format instead * @see {@link Formatter} * - * @category Formatter + * @category formatting * @since 4.0.0 */ export function makeFormatterDefault(): Formatter { - return (issue) => - toDefaultIssues(issue, [], defaultLeafHook, defaultCheckHook) - .map(formatDefaultIssue) - .join("\n") + return (issue) => formatIssue(issue, "") } /** @internal */ export const defaultFormatter = makeFormatterDefault() -function formatDefaultIssue(issue: DefaultIssue): string { - let out = issue.message - if (issue.path && issue.path.length > 0) { - const path = formatPath(issue.path as ReadonlyArray) - out += `\n at ${path}` - } - return out -} - -function findMessage(issue: Issue): string | undefined { +function formatIssue(issue: Issue, path: string): string { + let message: string switch (issue._tag) { - case "InvalidType": - case "OneOf": - case "Composite": - case "AnyOf": - return getMessageAnnotation(issue.ast.annotations) - case "InvalidValue": - case "Forbidden": - return getMessageAnnotation(issue.annotations) - case "MissingKey": - return getMessageAnnotation(issue.annotations, "messageMissingKey") - case "UnexpectedKey": - return getMessageAnnotation(issue.ast.annotations, "messageUnexpectedKey") - case "Filter": - return getMessageAnnotation(issue.filter.annotations) + case "Filter": { + const annotated = defaultCheckHook(issue) + if (annotated !== undefined) { + message = annotated + } else { + if (issue.issue._tag !== "InvalidValue") { + return formatIssue(issue.issue, path) + } + const expected = findExpected(issue.issue) + message = expected === undefined + ? getExpectedMessage(formatCheck(issue.filter), issue) + : getExpectedMessage(expected, issue.issue) + } + break + } case "Encoding": - return findMessage(issue.issue) + return formatIssue(issue.issue, path) + case "Pointer": + return formatIssue(issue.issue, path + formatPath(issue.path)) + case "Composite": + case "AnyOf": { + if (issue._tag === "Composite" || issue.issues.length > 0) { + return issue.issues.map((issue) => formatIssue(issue, path)).join("\n") + } + message = findMessage(issue) ?? getExpectedMessage(InternalAnnotations.getExpected(issue.ast), issue) + break + } + default: + message = defaultLeafHook(issue) + break } + return path ? `${message}\n at ${path}` : message } -function getMessageAnnotation( - annotations: Schema.Annotations.Annotations | undefined, - type: "message" | "messageMissingKey" | "messageUnexpectedKey" = "message" -): string | undefined { - const message = annotations?.[type] +function findMessage(issue: Issue): string | undefined { + if (issue._tag === "Pointer") return + if (issue._tag === "Encoding") return findMessage(issue.issue) + const annotations = issue._tag === "Filter" + ? issue.filter.annotations + : "annotations" in issue + ? issue.annotations + : issue.ast.annotations + const message = annotations?.[ + issue._tag === "MissingKey" + ? "messageMissingKey" + : issue._tag === "UnexpectedKey" + ? "messageUnexpectedKey" + : "message" + ] if (typeof message === "string") return message } - -function formatOption(actual: Option.Option): string { - if (Option.isNone(actual)) return "no value provided" - return format(actual.value) -} - -/** @internal */ -export function redact(issue: Issue): Issue { - switch (issue._tag) { - case "MissingKey": - return issue - case "Forbidden": - return new Forbidden(Option.map(issue.actual, Redacted.make), issue.annotations) - case "Filter": - return new Filter(Redacted.make(issue.actual), issue.filter, redact(issue.issue)) - case "Pointer": - return new Pointer(issue.path, redact(issue.issue)) - - case "Encoding": - case "InvalidType": - case "InvalidValue": - case "Composite": - return new InvalidValue(Option.map(issue.actual, Redacted.make)) - - case "AnyOf": - case "OneOf": - case "UnexpectedKey": - return new InvalidValue(Option.some(Redacted.make(issue.actual))) - } -} diff --git a/.context/effect/packages/effect/src/SchemaParser.ts b/.context/effect/packages/effect/src/SchemaParser.ts index 017471fcd..7cc35ebff 100644 --- a/.context/effect/packages/effect/src/SchemaParser.ts +++ b/.context/effect/packages/effect/src/SchemaParser.ts @@ -10,49 +10,19 @@ * * @since 4.0.0 */ -import * as Arr from "./Array.ts" import * as Cause from "./Cause.ts" import * as Effect from "./Effect.ts" import * as Exit from "./Exit.ts" import { memoize } from "./Function.ts" +import { effectIsExit } from "./internal/effect.ts" import * as InternalSchemaCause from "./internal/schema/cause.ts" +import * as InternalParser from "./internal/schema/parser.ts" import * as Option from "./Option.ts" -import * as Predicate from "./Predicate.ts" import * as Result from "./Result.ts" import type * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaIssue from "./SchemaIssue.ts" -// Converts a type-side AST into its constructor form by recursively restoring -// constructor encodings for nested classes and fields with constructor defaults. -const toConstructorAST = memoize((ast: SchemaAST.AST): SchemaAST.AST => { - switch (ast._tag) { - case "Declaration": { - const getLink = ast.annotations?.[SchemaAST.ClassTypeId] - if (Predicate.isFunction(getLink)) { - const link = getLink(ast.typeParameters) - const to = toConstructorAST(link.to) - return SchemaAST.replaceEncoding(ast, to === link.to ? [link] : [new SchemaAST.Link(to, link.transformation)]) - } - return ast - } - case "Objects": - case "Arrays": - return ast.recur((ast) => { - const defaultValue = ast.context?.defaultValue - if (defaultValue) { - const out = toConstructorAST(ast) - return SchemaAST.replaceEncoding(out, out.encoding ? [...out.encoding, ...defaultValue] : defaultValue) - } - return toConstructorAST(ast) - }) - case "Suspend": - return ast.recur(toConstructorAST) - default: - return ast - } -}) - /** * Creates an effectful maker for the schema's decoded type side. * @@ -71,8 +41,7 @@ const toConstructorAST = memoize((ast: SchemaAST.AST): SchemaAST.AST => { * @since 4.0.0 */ export function makeEffect(schema: S) { - const ast = toConstructorAST(SchemaAST.toType(schema.ast)) - const parser = run(ast) + const parser = runWithCompiler(constructorCompiler, SchemaAST.toType(schema.ast)) return (input: S["~type.make.in"], options?: Schema.MakeOptions): Effect.Effect => { return parser( input, @@ -125,6 +94,9 @@ export function makeOption(schema: S) { * * The returned function constructs a value from constructor input and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` when construction fails. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -146,7 +118,7 @@ export function make(schema: S) { exit.cause, "Constructor adapter can only throw schema issues" ) - throw new Error(issue.toString(), { cause: issue }) + throw new Error("Schema validation failed", { cause: issue }) } } @@ -170,7 +142,7 @@ export function make(schema: S) { * that contain defects, interruptions, or asynchronous work at this synchronous * boundary throw an `Error` whose cause is the underlying `Cause`. * - * @category Asserting + * @category guards * @since 3.10.0 */ export function is(schema: S): (input: I) => input is I & S["Type"] { @@ -215,6 +187,9 @@ export function _issue(ast: SchemaAST.AST) { * The assertion returns normally when validation succeeds. When the input does * not satisfy the schema with a schema-only failure, it throws an `Error` with * the `SchemaIssue.Issue` in its `cause`. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -222,7 +197,7 @@ export function _issue(ast: SchemaAST.AST) { * synchronous boundary throw an `Error` whose cause is the underlying `Cause`, * instead of being converted to a schema validation error. * - * @category Asserting + * @category guards * @since 4.0.0 */ export function asserts(schema: S, input: I): asserts input is I & S["Type"] { @@ -233,7 +208,7 @@ export function asserts(schema: S, input: I): as exit.cause, "Assertion adapter can only throw schema issues" ) - throw new Error(issue.toString(), { cause: issue }) + throw new Error("Schema validation failed", { cause: issue }) } } @@ -313,6 +288,9 @@ export const decodeEffect: ( * * The returned function resolves with the decoded `Type` on success and rejects * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -345,6 +323,9 @@ export function decodeUnknownPromise * * The returned function resolves with the decoded `Type` on success and rejects * with an `Error` whose cause is a `SchemaIssue.Issue` on decoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -523,6 +504,9 @@ export const decodeResult: >( * * The returned function returns the decoded `Type` on success and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -557,6 +541,9 @@ export function decodeUnknownSync>( * * The returned function returns the decoded `Type` on success and throws an * `Error` with the `SchemaIssue.Issue` in its `cause` on decoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -651,6 +638,9 @@ export const encodeEffect: ( * * The returned function resolves with the schema's `Encoded` value on success and * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -682,6 +672,9 @@ export const encodeUnknownPromise = * * The returned function resolves with the schema's `Encoded` value on success and * rejects with an `Error` whose cause is a `SchemaIssue.Issue` on encoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -857,6 +850,9 @@ export const encodeResult: >( * * The returned function returns the schema's `Encoded` value on success and throws * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -890,6 +886,9 @@ export function encodeUnknownSync>( * * The returned function returns the schema's `Encoded` value on success and throws * an `Error` with the `SchemaIssue.Issue` in its `cause` on encoding failure. + * Schema validation failures use the generic message `"Schema validation failed"`. + * Format the `cause` explicitly with `SchemaIssue.makeFormatterDefault()` when + * human-readable details are needed. * * **Gotchas** * @@ -912,18 +911,38 @@ export const encodeSync: >( const mergeParseOptions = ( options: SchemaAST.ParseOptions, overrideOptions: SchemaAST.ParseOptions | undefined -): SchemaAST.ParseOptions => overrideOptions === undefined ? options : { ...options, ...overrideOptions } +): SchemaAST.ParseOptions => overrideOptions ? { ...options, ...overrideOptions } : options + +const getValue = (value: unknown): Effect.Effect => { + if (value === InternalParser.missing) { + return Effect.fail(new SchemaIssue.InvalidValue()) + } + return Effect.succeed(value) +} /** @internal */ export function run(ast: SchemaAST.AST) { - const parser = recur(ast) - return (input: unknown, options?: SchemaAST.ParseOptions): Effect.Effect => - Effect.flatMapEager(parser(Option.some(input), options ?? SchemaAST.defaultParseOptions), (oa) => { - if (oa._tag === "None") { - return Effect.fail(new SchemaIssue.InvalidValue(oa)) - } - return Effect.succeed(oa.value as T) - }) + return runWithCompiler(normalCompiler, ast) +} + +function runWithCompiler(compiler: Compiler, ast: SchemaAST.AST) { + let parser: Parser + return (input: unknown, options?: SchemaAST.ParseOptions): Effect.Effect => { + const result = (parser ??= compiler(ast))( + input, + options ?? SchemaAST.defaultParseOptions + ) + if (result === InternalParser.sameExit) { + return Effect.succeed(input) as Effect.Effect + } + if (!effectIsExit(result)) { + return Effect.flatMapEager(result, getValue) + } + return (result as InternalParser.Success)[InternalParser.args] === + InternalParser.missing + ? getValue(InternalParser.missing) + : result as Effect.Effect + } } function asPromise( @@ -938,7 +957,7 @@ function asPromise( exit.cause, "Promise adapter can only reject schema issues" ) - throw new Error(issue.toString(), { cause: issue }) + throw new Error("Schema validation failed", { cause: issue }) }) } @@ -988,129 +1007,213 @@ function asSync( return exit.value } const issue = InternalSchemaCause.getSchemaIssueOrThrow(exit.cause, "Sync adapter can only throw schema issues") - throw new Error(issue.toString(), { cause: issue }) + throw new Error("Schema validation failed", { cause: issue }) } } -function mapSchemaIssueEffect( - self: Effect.Effect, - f: (issue: SchemaIssue.Issue) => SchemaIssue.Issue -): Effect.Effect { - return Effect.catchCause(self, (cause) => Effect.failCauseSync(() => Cause.map(cause, f))) -} - /** @internal */ export interface Parser { ( - input: Option.Option, + input: unknown, options: SchemaAST.ParseOptions - ): Effect.Effect, SchemaIssue.Issue, any> + ): Effect.Effect } -const recur = memoize( - (ast: SchemaAST.AST): Parser => { - let parser: Parser - const checks = ast.checks - const encoding = ast.encoding - const links = encoding - const len = links?.length ?? 0 - const encodingChecks = (ast as any).encodingChecks - const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations) - ?.["parseOptions"] - if (!ast.context && !encoding && !checks && !encodingChecks) { - return (ou, options) => { - parser ??= ast.getParser(recur) - if (astOptions) { - options = { ...options, ...astOptions } - } - return parser(ou, options) - } - } - const isStructural = SchemaAST.isArrays(ast) || SchemaAST.isObjects(ast) || - (SchemaAST.isDeclaration(ast) && ast.typeParameters.length > 0) - const structuralChecks = checks && isStructural ? - checks.filter((check) => check.annotations?.[SchemaAST.STRUCTURAL_ANNOTATION_KEY]) : - undefined - return (ou, options) => { - if (astOptions) { - options = { ...options, ...astOptions } - } - let srou: Effect.Effect, SchemaIssue.Issue, unknown> | undefined - if (links) { - for (let i = len - 1; i >= 0; i--) { - const link = links[i] - const to = link.to - const parser = recur(to) - srou = srou ? Effect.flatMapEager(srou, (ou) => parser(ou, options)) : parser(ou, options) - if (link.transformation._tag === "Transformation") { - const getter = link.transformation.decode - srou = Effect.flatMapEager(srou, (ou) => getter.run(ou, options)) - } else { - srou = link.transformation.decode(srou, options) - } - } - srou = mapSchemaIssueEffect(srou!, (issue) => new SchemaIssue.Encoding(ast, ou, issue)) - } +/** @internal */ +export interface Compiler { + (ast: SchemaAST.AST): Parser +} - parser ??= ast.getParser(recur) - const parseLocal = (localOu: Option.Option) => { - let sroa = parser(localOu, options) +const normalCompiler: Compiler = memoize((ast) => makeParser(ast, normalCompiler)) +const constructorCompiler: Compiler = memoize((ast) => makeParser(ast, constructorCompiler, compileConstructorDefault)) +const compileDefaulted = memoize((ast: SchemaAST.AST) => + makeParser(ast, constructorCompiler, compileConstructorDefault, ast.context?.constructorDefault) +) - if (encodingChecks && !options?.disableChecks) { - sroa = Effect.flatMapEager(sroa, (oa) => { - if (Option.isSome(localOu) && Option.isSome(oa)) { - const issues: Array = [] +function compileConstructorDefault(ast: SchemaAST.AST): Parser { + return ast.context?.constructorDefault ? compileDefaulted(ast) : constructorCompiler(ast) +} - SchemaAST.collectIssues(encodingChecks, localOu.value, issues, ast, options) +function applyTransformation( + result: Effect.Effect, + current: unknown, + transformation: SchemaAST.Link["transformation"], + options: SchemaAST.ParseOptions +): Effect.Effect { + let transformed: Effect.Effect, SchemaIssue.Issue, unknown> + if (effectIsExit(result) && result._tag === "Success") { + const optional = InternalParser.toOption( + result === InternalParser.sameExit + ? current + : (result as InternalParser.Success)[InternalParser.args] + ) + transformed = transformation._tag === "Transformation" + ? transformation.decode.run(optional, options) + : transformation.decode(InternalParser.succeed(optional), options) + } else if (transformation._tag === "Transformation") { + transformed = Effect.flatMapEager( + result, + (value) => transformation.decode.run(InternalParser.toOption(value), options) + ) + } else { + transformed = transformation.decode( + Effect.mapEager(result, InternalParser.toOption), + options + ) + } + return effectIsExit(transformed) && transformed._tag === "Success" + ? InternalParser.fromOptionExit( + (transformed as InternalParser.Success, SchemaIssue.Issue>)[InternalParser.args] + ) + : Effect.flatMapEager(transformed, InternalParser.fromOptionExit) +} - if (Arr.isArrayNonEmpty(issues)) { - return Effect.fail(new SchemaIssue.Composite(ast, localOu, issues)) - } +function makeConstructorParser(descriptor: SchemaAST.ConstructorDescriptor, compile: Compiler): Parser { + let sourceParser: Parser + return (input, options) => { + if (input === InternalParser.missing) return InternalParser.missingExit + if (descriptor.isConstructed(input)) return InternalParser.sameExit + const result = (sourceParser ??= compile(descriptor.link.to))(input, options) + return applyTransformation(result, input, descriptor.link.transformation, options) + } +} + +function makeParser( + ast: SchemaAST.AST, + compile: Compiler, + compileConstructorDefault?: Compiler, + constructorDefault?: SchemaAST.Link +): Parser { + const descriptor = compileConstructorDefault ? SchemaAST.getConstructorDescriptor(ast) : undefined + const parser = descriptor + ? makeConstructorParser(descriptor, compile) + : ast.getParser(compile, compileConstructorDefault) + const checks = ast.checks + const links = constructorDefault + ? ast.encoding ? [...ast.encoding, constructorDefault] : [constructorDefault] + : ast.encoding + const encodingChecks = (ast as any).encodingChecks + const astOptions = (checks ? checks[checks.length - 1].annotations : ast.annotations) + ?.["parseOptions"] + if (!links && !checks && !encodingChecks) { + if (!astOptions) { + return parser + } + return (input, options) => parser(input, mergeParseOptions(options, astOptions)) + } + let encodingParsers: ReadonlyArray | undefined + const parseLocal = ( + input: unknown, + options: SchemaAST.ParseOptions + ) => { + let result = parser(input, options) + if (encodingChecks && !options.disableChecks) { + if (effectIsExit(result)) { + if (result._tag === "Success") { + const output = result === InternalParser.sameExit + ? input + : (result as InternalParser.Success)[InternalParser.args] + if (input !== InternalParser.missing && output !== InternalParser.missing) { + const issues = SchemaAST.collectIssues(encodingChecks, input, undefined, ast, options) + if (issues) { + result = Effect.fail(new SchemaIssue.Composite(ast, issues, input, options)) } - return Effect.succeed(oa) - }) + } } - - if (checks && !options?.disableChecks) { - if (options?.errors === "all" && structuralChecks && structuralChecks.length > 0 && Option.isSome(localOu)) { - sroa = mapSchemaIssueEffect(sroa, (issue) => { - const issues: Array = [] - SchemaAST.collectIssues( - structuralChecks, - localOu.value, - issues, - ast, - options - ) - const out: SchemaIssue.Issue = Arr.isArrayNonEmpty(issues) - ? issue._tag === "Composite" && issue.ast === ast - ? new SchemaIssue.Composite(ast, issue.actual, [...issue.issues, ...issues]) - : new SchemaIssue.Composite(ast, localOu, [issue, ...issues]) - : issue - return out - }) + } else { + result = Effect.flatMap(result, (value) => { + if (input !== InternalParser.missing && value !== InternalParser.missing) { + const issues = SchemaAST.collectIssues(encodingChecks, input, undefined, ast, options) + if (issues) { + return Effect.fail(new SchemaIssue.Composite(ast, issues, input, options)) + } } - sroa = Effect.flatMapEager(sroa, (oa) => { - if (Option.isSome(oa)) { - const value = oa.value - const issues: Array = [] - - SchemaAST.collectIssues(checks, value, issues, ast, options) + return Effect.succeed(value) + }) + } + } - if (Arr.isArrayNonEmpty(issues)) { - return Effect.fail(new SchemaIssue.Composite(ast, oa, issues)) - } + if (checks && !options.disableChecks) { + if (effectIsExit(result)) { + if (result._tag === "Success") { + const value = result === InternalParser.sameExit + ? input + : (result as InternalParser.Success)[InternalParser.args] + if (value === InternalParser.missing) return result + const issues = SchemaAST.collectIssues(checks, value, undefined, ast, options) + if (issues) { + result = Effect.fail(new SchemaIssue.Composite(ast, issues, value, options)) + } + } + } else { + result = Effect.flatMap(result, (value) => { + if (value !== InternalParser.missing) { + const issues = SchemaAST.collectIssues(checks, value, undefined, ast, options) + if (issues) { + return Effect.fail(new SchemaIssue.Composite(ast, issues, value, options)) } - return Effect.succeed(oa) + } + return Effect.succeed(value) + }) + } + } + + return result + } + if (!links) { + return astOptions + ? (input, options) => parseLocal(input, mergeParseOptions(options, astOptions)) + : parseLocal + } + return ( + input: unknown, + options: SchemaAST.ParseOptions + ) => { + if (astOptions) { + options = mergeParseOptions(options, astOptions) + } + const parsers = encodingParsers ??= links.map((link) => compile(link.to)) + let current = input + let result = parsers[parsers.length - 1](input, options) + for (let i = links.length - 1; i >= 0; i--) { + result = applyTransformation(result, current, links[i].transformation, options) + if (i !== 0) { + const next = parsers[i - 1] + if ((result as Exit.Exit)._tag === "Success") { + current = (result as InternalParser.Success)[InternalParser.args] + result = next(current, options) + } else { + result = Effect.flatMapEager(result, (value) => { + const nextResult = next(value, options) + return nextResult === InternalParser.sameExit ? InternalParser.succeed(value) : nextResult }) } - - return sroa } - - const sroa = srou ? Effect.flatMapEager(srou, parseLocal) : parseLocal(ou) - - return sroa } + if ((result as Exit.Exit)._tag === "Success") { + const value = (result as InternalParser.Success)[InternalParser.args] + const local = parseLocal(value, options) + return local === InternalParser.sameExit ? result : local + } + result = Effect.catchCause( + result, + (cause) => + Effect.failCauseSync(() => + Cause.map( + cause, + (issue) => + new SchemaIssue.Encoding( + ast, + issue, + input, + options + ) + ) + ) + ) + return Effect.flatMapEager(result, (value) => { + const local = parseLocal(value, options) + return local === InternalParser.sameExit ? InternalParser.succeed(value) : local + }) } -) +} diff --git a/.context/effect/packages/effect/src/SchemaRepresentation.ts b/.context/effect/packages/effect/src/SchemaRepresentation.ts index 547ea6e3f..8818ccc81 100644 --- a/.context/effect/packages/effect/src/SchemaRepresentation.ts +++ b/.context/effect/packages/effect/src/SchemaRepresentation.ts @@ -1,72 +1,156 @@ /** - * Plain data structures for describing schemas in a serializable form. A - * `Representation` is not the original `Schema` object; it is a JSON-friendly - * description of the schema's types, fields, unions, checks, annotations, and - * references. - * - * This module defines the representation node types, document types, and - * codecs used to validate those documents. It can build representation - * documents from schema ASTs, turn representation documents back into schemas, - * convert them to and from JSON Schema documents, and generate TypeScript code - * artifacts for schema definitions. + * Open, compiler-extensible representation of Effect schemas. * * @since 4.0.0 */ -import * as Arr from "./Array.ts" -import { format, formatPropertyKey } from "./Formatter.ts" -import { collectBrands } from "./internal/schema/annotations.ts" -import * as InternalRepresentation from "./internal/schema/representation.ts" -import { unescapeToken } from "./JsonPointer.ts" +import * as InternalRecord from "./internal/record.ts" +import * as InternalFromJsonSchemaDocument from "./internal/schema/fromJsonSchemaDocument.ts" +import * as InternalFromRepresentation from "./internal/schema/fromRepresentation.ts" +import * as InternalSchema from "./internal/schema/schema.ts" +import * as InternalToCodeDocument from "./internal/schema/toCodeDocument.ts" +import * as InternalToJsonSchemaDocument from "./internal/schema/toJsonSchemaDocument.ts" +import * as InternalToRepresentation from "./internal/schema/toRepresentation.ts" import type * as JsonSchema from "./JsonSchema.ts" -import { remainder } from "./Number.ts" import * as Option from "./Option.ts" -import * as Predicate from "./Predicate.ts" -import * as Rec from "./Record.ts" import * as Schema from "./Schema.ts" import * as SchemaAST from "./SchemaAST.ts" import * as SchemaGetter from "./SchemaGetter.ts" -// ----------------------------------------------------------------------------- -// specification -// ----------------------------------------------------------------------------- - /** - * A custom type declaration, such as `Date`, `Option`, or `ReadonlySet`. - * - * **When to use** + * Open persistence identity carried by declarations and opaque checks. * - * Use when inspecting or transforming non-primitive schema types. + * @category annotations + * @since 4.0.0 + */ +export interface RepresentationAnnotation { + readonly id: string + readonly payload: Schema.Json +} + +/** + * Open persistence identity and schema dependencies carried by opaque checks. * - * **Details** + * @category annotations + * @since 4.0.0 + */ +export interface CheckRepresentationAnnotation extends RepresentationAnnotation { + readonly schemas?: ReadonlyArray | undefined +} + +/** + * Input passed to JSON Schema compiler annotations. * - * `typeParameters` holds the inner type arguments, such as the `A` in - * `Option`. `encodedSchema` is the fallback representation when no - * {@link Reviver} recognizes this declaration. `annotations.typeConstructor` - * identifies the declaration kind, such as `{ _tag: "effect/Option" }`. + * @since 4.0.0 + */ +export declare namespace ToJsonSchema { + /** + * Input for a check compiler. + * + * @category models + * @since 4.0.0 + */ + export interface CheckInput { + readonly type: JsonSchema.Type | undefined + readonly schemas: ReadonlyArray + } + + /** + * Compiles a check to a JSON Schema fragment. + * + * **Gotchas** + * + * Treat the input schemas as immutable. The returned value must be a valid JSON Schema object graph and must not be + * mutated after this function returns. Return a new object graph to produce different output during a later + * compilation. + * + * @category models + * @since 4.0.0 + */ + export type Check = (input: CheckInput) => JsonSchema.JsonSchema +} + +/** + * Input and output contracts for code generation annotations. * - * @see {@link Reviver} - * @see {@link toSchemaDefaultReviver} + * @since 4.0.0 + */ +export declare namespace Generation { + /** + * Input for declaration code generation. + * + * @category models + * @since 4.0.0 + */ + export interface DeclarationInput { + readonly typeParameters: ReadonlyArray + } + + /** + * Output of declaration code generation. + * + * @category models + * @since 4.0.0 + */ + export interface DeclarationOutput { + readonly runtime: string + readonly Type: string + readonly importDeclarations?: ReadonlyArray | undefined + } + + /** + * Declaration code generator. + * + * @category models + * @since 4.0.0 + */ + export type Declaration = (input: DeclarationInput) => DeclarationOutput + + /** + * Input for check code generation. + * + * @category models + * @since 4.0.0 + */ + export interface CheckInput { + readonly schemas: ReadonlyArray + } + + /** + * Output of check code generation. + * + * @category models + * @since 4.0.0 + */ + export interface CheckOutput { + readonly runtime: string + readonly importDeclarations?: ReadonlyArray | undefined + } + + /** + * Check code generator. + * + * @category models + * @since 4.0.0 + */ + export type Check = (input: CheckInput) => CheckOutput +} + +/** + * A custom opaque declaration. * * @category models * @since 4.0.0 */ export interface Declaration { readonly _tag: "Declaration" + readonly representation?: RepresentationAnnotation | undefined readonly annotations?: Schema.Annotations.Annotations | undefined readonly typeParameters: ReadonlyArray - readonly checks: ReadonlyArray> - readonly encodedSchema: Representation + readonly checks: ReadonlyArray } /** - * A lazily resolved representation used for recursive schemas. - * - * **Details** - * - * `thunk` points to the actual representation, possibly via a - * {@link Reference}. `checks` is always empty on `Suspend` nodes. - * - * @see {@link Reference} + * A lazily resolved representation. * * @category models * @since 4.0.0 @@ -79,25 +163,7 @@ export interface Suspend { } /** - * A named reference to a definition in the {@link References} map. - * - * **When to use** - * - * Use when a representation should point to a named definition instead of - * embedding the definition inline. - * - * **Details** - * - * `$ref` is the key into `Document.references` or `MultiDocument.references`. - * References are resolved lazily by {@link toSchema} and - * {@link toCodeDocument}. - * - * **Gotchas** - * - * Resolution throws at runtime if the key is not found in the references map. - * - * @see {@link References} - * @see {@link Document} + * A named reference. * * @category models * @since 4.0.0 @@ -107,244 +173,152 @@ export interface Reference { readonly $ref: string } -/** - * The `null` type. - * - * @category models - * @since 4.0.0 - */ -export interface Null { - readonly _tag: "Null" +interface Keyword { + readonly _tag: Tag readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: ReadonlyArray } /** - * The `undefined` type. + * The null keyword representation. * * @category models * @since 4.0.0 */ -export interface Undefined { - readonly _tag: "Undefined" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Null extends Keyword<"Null"> {} /** - * The `void` type. + * The undefined keyword representation. * * @category models * @since 4.0.0 */ -export interface Void { - readonly _tag: "Void" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Undefined extends Keyword<"Undefined"> {} /** - * The `never` type (no valid values). + * The void keyword representation. * * @category models * @since 4.0.0 */ -export interface Never { - readonly _tag: "Never" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Void extends Keyword<"Void"> {} /** - * The `unknown` type (any value accepted). + * The never keyword representation. * * @category models * @since 4.0.0 */ -export interface Unknown { - readonly _tag: "Unknown" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Never extends Keyword<"Never"> {} /** - * The `any` type. + * The unknown keyword representation. * * @category models * @since 4.0.0 */ -export interface Any { - readonly _tag: "Any" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Unknown extends Keyword<"Unknown"> {} /** - * The `string` type with optional validation checks. - * - * **Details** - * - * `checks` holds string-specific constraints, such as min/max length, pattern, - * and UUID checks. `contentMediaType` and `contentSchema` indicate that the - * string contains encoded data, such as `"application/json"` with a nested - * schema. - * - * @see {@link StringMeta} - * @see {@link Check} + * The any keyword representation. * * @category models * @since 4.0.0 */ -export interface String { - readonly _tag: "String" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly checks: ReadonlyArray> - readonly contentMediaType?: string | undefined - readonly contentSchema?: Representation | undefined -} +export interface Any extends Keyword<"Any"> {} /** - * The `number` type with optional validation checks. - * - * **Details** - * - * `checks` holds number-specific constraints, such as int, finite, min, max, - * multipleOf, and between checks. - * - * @see {@link NumberMeta} + * A string representation. * * @category models * @since 4.0.0 */ -export interface Number { - readonly _tag: "Number" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly checks: ReadonlyArray> -} +export interface String extends Keyword<"String"> {} /** - * The `boolean` type. + * A number representation. * * @category models * @since 4.0.0 */ -export interface Boolean { - readonly _tag: "Boolean" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface Number extends Keyword<"Number"> {} /** - * The `bigint` type with optional validation checks. - * - * @see {@link BigIntMeta} + * A boolean representation. * * @category models * @since 4.0.0 */ -export interface BigInt { - readonly _tag: "BigInt" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly checks: ReadonlyArray> -} - +export interface Boolean extends Keyword<"Boolean"> {} /** - * The `symbol` type. + * A bigint representation. * * @category models * @since 4.0.0 */ -export interface Symbol { - readonly _tag: "Symbol" - readonly annotations?: Schema.Annotations.Annotations | undefined -} - +export interface BigInt extends Keyword<"BigInt"> {} /** - * A specific literal value (`string`, `number`, `boolean`, or `bigint`). + * A symbol representation. * * @category models * @since 4.0.0 */ -export interface Literal { - readonly _tag: "Literal" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly literal: string | number | boolean | bigint -} +export interface Symbol extends Keyword<"Symbol"> {} /** - * A specific unique `symbol` value. + * A literal representation. + * + * **Details** + * + * The live representation stores the native literal value. Persistent codecs + * add an explicit type discriminator when encoding it. * * @category models * @since 4.0.0 */ -export interface UniqueSymbol { - readonly _tag: "UniqueSymbol" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly symbol: symbol +export interface Literal extends Keyword<"Literal"> { + readonly literal: SchemaAST.LiteralValue } /** - * The `object` keyword type (matches any non-primitive). + * A unique global symbol representation. * * @category models * @since 4.0.0 */ -export interface ObjectKeyword { - readonly _tag: "ObjectKeyword" - readonly annotations?: Schema.Annotations.Annotations | undefined +export interface UniqueSymbol extends Keyword<"UniqueSymbol"> { + readonly symbol: symbol } /** - * A TypeScript-style enum. Each entry is a `[name, value]` pair. + * The object keyword representation. * * @category models * @since 4.0.0 */ -export interface Enum { - readonly _tag: "Enum" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly enums: ReadonlyArray -} +export interface ObjectKeyword extends Keyword<"ObjectKeyword"> {} /** - * A template literal type composed of a sequence of parts (literals, strings, - * numbers, etc.). + * An enum representation. + * + * **Details** + * + * Enum members are stored as native string or number values. Persistent + * codecs add an explicit type discriminator when encoding them. * * @category models * @since 4.0.0 */ -export interface TemplateLiteral { - readonly _tag: "TemplateLiteral" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly parts: ReadonlyArray +export interface Enum extends Keyword<"Enum"> { + readonly enums: ReadonlyArray } /** - * An array or tuple type. - * - * **Details** - * - * `elements` are the fixed positional elements, or tuple prefix, and each may - * be optional. `rest` contains the variadic tail types; a single-element - * `rest` with no `elements` produces a plain `Array`. `checks` holds - * array-specific constraints, such as minLength, maxLength, and unique checks. - * - * @see {@link Element} - * @see {@link ArraysMeta} + * A template literal representation. * * @category models * @since 4.0.0 */ -export interface Arrays { - readonly _tag: "Arrays" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly elements: ReadonlyArray - readonly rest: ReadonlyArray - readonly checks: ReadonlyArray> +export interface TemplateLiteral extends Keyword<"TemplateLiteral"> { + readonly parts: ReadonlyArray } /** - * A positional element within an {@link Arrays} tuple. - * - * **Details** - * - * `isOptional` indicates whether this element can be absent. `type` is the - * schema representation for this element's value. - * - * @see {@link Arrays} + * A tuple element. * * @category models * @since 4.0.0 @@ -356,39 +330,28 @@ export interface Element { } /** - * An object/struct type with named properties and optional index signatures. - * - * **Details** - * - * `propertySignatures` are the explicitly named fields. `indexSignatures` - * define catch-all key/value types, such as `Record`. `checks` - * holds object-specific constraints, such as minProperties and maxProperties. - * - * @see {@link PropertySignature} - * @see {@link IndexSignature} - * @see {@link ObjectsMeta} + * An array or tuple representation. * * @category models * @since 4.0.0 */ -export interface Objects { - readonly _tag: "Objects" - readonly annotations?: Schema.Annotations.Annotations | undefined - readonly propertySignatures: ReadonlyArray - readonly indexSignatures: ReadonlyArray - readonly checks: ReadonlyArray> +export interface Arrays extends Keyword<"Arrays"> { + readonly elements: ReadonlyArray + readonly rest: ReadonlyArray } /** - * A named property within an {@link Objects} representation. + * A property signature. * * **Details** * - * `name` is the property key, which can be a string, number, or symbol. - * `isOptional` indicates whether the key can be absent. `isMutable` indicates - * whether the property is mutable rather than readonly. + * The live representation stores the native property key. Persistent codecs + * add an explicit type discriminator when encoding it. + * + * **Gotchas** * - * @see {@link Objects} + * Local symbols can be represented while the schema is live, but persistent + * codecs reject them because they cannot be reconstructed by identity. * * @category models * @since 4.0.0 @@ -402,15 +365,7 @@ export interface PropertySignature { } /** - * An index signature, such as `[key: string]: number`, within an - * {@link Objects}. - * - * **Details** - * - * `parameter` is the key type representation. `type` is the value type - * representation. - * - * @see {@link Objects} + * An index signature. * * @category models * @since 4.0.0 @@ -421,34 +376,29 @@ export interface IndexSignature { } /** - * A union of multiple representations. - * - * **Details** + * An object representation. * - * `types` are the union members. `mode` controls JSON Schema output as either - * `"anyOf"` (the default) or mutually exclusive `"oneOf"`. + * @category models + * @since 4.0.0 + */ +export interface Objects extends Keyword<"Objects"> { + readonly propertySignatures: ReadonlyArray + readonly indexSignatures: ReadonlyArray +} + +/** + * A union representation. * * @category models * @since 4.0.0 */ -export interface Union { - readonly _tag: "Union" - readonly annotations?: Schema.Annotations.Annotations | undefined +export interface Union extends Keyword<"Union"> { readonly types: ReadonlyArray readonly mode: "anyOf" | "oneOf" } /** - * The core tagged union of all supported schema shapes. - * - * **Details** - * - * Each variant has a `_tag` discriminator. Switch on `_tag` to handle each - * shape. Most variants carry optional `annotations` and some carry `checks` - * for validation constraints. - * - * @see {@link Document} - * @see {@link fromAST} + * The structural schema representation. * * @category models * @since 4.0.0 @@ -478,3430 +428,798 @@ export type Representation = | Union /** - * A validation constraint attached to a type. Either a single {@link Filter} - * or a {@link FilterGroup} combining multiple checks. - * - * @see {@link Filter} - * @see {@link FilterGroup} + * A structural check. * * @category models * @since 4.0.0 */ -export type Check = Filter | FilterGroup +export type Check = Filter | FilterGroup /** - * A single validation constraint with typed metadata describing the check - * (e.g. `{ _tag: "isMinLength", minLength: 3 }`). - * - * @see {@link Check} + * An opaque leaf check. * * @category models * @since 4.0.0 */ -export interface Filter { +export interface Filter { readonly _tag: "Filter" - readonly annotations?: Schema.Annotations.Filter | undefined - readonly meta: M + readonly representation?: CheckRepresentationAnnotation | undefined + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly aborted: boolean } /** - * A group of validation constraints that are logically combined. Contains - * at least one {@link Check}. - * - * @see {@link Check} + * A non-empty group of checks. * * @category models * @since 4.0.0 */ -export interface FilterGroup { +export interface FilterGroup { readonly _tag: "FilterGroup" - readonly annotations?: Schema.Annotations.Filter | undefined - readonly checks: readonly [Check, ...Array>] + readonly representation?: CheckRepresentationAnnotation | undefined + readonly annotations?: Schema.Annotations.Annotations | undefined + readonly checks: readonly [Check, ...Array] } /** - * Metadata union for string-specific validation checks (minLength, maxLength, - * pattern, UUID, trimmed, etc.). - * - * @see {@link String} - * @see {@link Check} - * - * @category models - * @since 4.0.0 - */ -export type StringMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isStringFinite" - | "isStringBigInt" - | "isStringSymbol" - | "isMinLength" - | "isMaxLength" - | "isPattern" - | "isLengthBetween" - | "isTrimmed" - | "isUUID" - | "isGUID" - | "isULID" - | "isBase64" - | "isBase64Url" - | "isStartsWith" - | "isEndsWith" - | "isIncludes" - | "isUppercased" - | "isLowercased" - | "isCapitalized" - | "isUncapitalized" -] - -/** - * Metadata union for number-specific validation checks (int, finite, - * min, max, multipleOf, between). - * - * @see {@link Number} - * @see {@link Check} + * Named representation definitions. * * @category models * @since 4.0.0 */ -export type NumberMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isInt" - | "isFinite" - | "isMultipleOf" - | "isGreaterThanOrEqualTo" - | "isLessThanOrEqualTo" - | "isGreaterThan" - | "isLessThan" - | "isBetween" -] +export interface References { + readonly [$ref: string]: Representation +} /** - * Metadata union for bigint-specific validation checks (min, max, between). - * - * @see {@link BigInt} - * @see {@link Check} + * A single representation and its definitions. * * @category models * @since 4.0.0 */ -export type BigIntMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isGreaterThanOrEqualToBigInt" - | "isLessThanOrEqualToBigInt" - | "isGreaterThanBigInt" - | "isLessThanBigInt" - | "isBetweenBigInt" -] +export interface Document { + readonly representation: Representation + readonly references: References +} /** - * Metadata union for array-specific validation checks (minLength, maxLength, - * length, unique). - * - * @see {@link Arrays} - * @see {@link Check} + * Multiple representations sharing definitions. * * @category models * @since 4.0.0 */ -export type ArraysMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isMinLength" - | "isMaxLength" - | "isLengthBetween" - | "isUnique" -] +export interface MultiDocument { + readonly representations: readonly [Representation, ...Array] + readonly references: References +} /** - * Metadata union for object-specific validation checks (minProperties, - * maxProperties, propertiesLength, propertyNames). - * - * @see {@link Objects} - * @see {@link Check} + * Reviver for a declaration. * * @category models * @since 4.0.0 */ -export type ObjectsMeta = - | Schema.Annotations.BuiltInMetaDefinitions[ - | "isMinProperties" - | "isMaxProperties" - | "isPropertiesLengthBetween" - ] - | { readonly _tag: "isPropertyNames"; readonly propertyNames: Representation } +export interface DeclarationReviver

{ + readonly id: string + readonly payloadSchema: Schema.Decoder

+ readonly revive: (input: { + readonly payload: P + readonly typeParameters: ReadonlyArray + readonly annotations: Schema.Annotations.Annotations | undefined + }) => Schema.Top +} /** - * Metadata union for Date-specific validation checks (valid, min, max, between). - * - * @see {@link Declaration} - * @see {@link DeclarationMeta} + * Reviver for a leaf check. * * @category models * @since 4.0.0 */ -export type DateMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isDateValid" - | "isGreaterThanDate" - | "isGreaterThanOrEqualToDate" - | "isLessThanDate" - | "isLessThanOrEqualToDate" - | "isBetweenDate" -] +export interface FilterReviver

{ + readonly id: string + readonly payloadSchema: Schema.Decoder

+ readonly revive: (input: { + readonly payload: P + readonly schemas: ReadonlyArray + readonly annotations: Schema.Annotations.Filter | undefined + }) => SchemaAST.Filter +} /** - * Metadata union for size-based validation checks (minSize, maxSize, size). - * Used for collection types like `Set`, `Map`. - * - * @see {@link Declaration} - * @see {@link DeclarationMeta} + * Reviver for a check group. * * @category models * @since 4.0.0 */ -export type SizeMeta = Schema.Annotations.BuiltInMetaDefinitions[ - | "isMinSize" - | "isMaxSize" - | "isSizeBetween" -] +export interface FilterGroupReviver

{ + readonly id: string + readonly payloadSchema: Schema.Decoder

+ readonly revive: (input: { + readonly payload: P + readonly schemas: ReadonlyArray + readonly annotations: Schema.Annotations.Filter | undefined + }) => SchemaAST.FilterGroup +} /** - * Metadata union for {@link Declaration} checks — either {@link DateMeta} - * or {@link SizeMeta}. + * A check reviver. * * @category models * @since 4.0.0 */ -export type DeclarationMeta = DateMeta | SizeMeta - -/** @internal */ -export type Meta = StringMeta | NumberMeta | BigIntMeta | ArraysMeta | ObjectsMeta | DeclarationMeta +export type CheckReviver

= FilterReviver

| FilterGroupReviver

/** - * A string-keyed map of named {@link Representation} definitions. Used by - * {@link Document} and {@link MultiDocument} for `$ref` resolution (analogous - * to JSON Schema `$defs`). - * - * @see {@link Reference} - * @see {@link Document} + * A typed reviver. * * @category models * @since 4.0.0 */ -export interface References { - readonly [$ref: string]: Representation -} +export type Reviver

= DeclarationReviver

| CheckReviver

/** - * A single {@link Representation} together with its named {@link References}. - * - * **When to use** - * - * Use when representing a single Schema AST together with its named references - * before reconstructing a runtime Schema, converting to JSON Schema, or - * wrapping it as a {@link MultiDocument}. - * - * @see {@link MultiDocument} - * @see {@link fromAST} + * A reviver erased only at collection boundaries. * * @category models * @since 4.0.0 */ -export type Document = { - readonly representation: Representation - readonly references: References -} +export type AnyReviver = Reviver /** - * One or more {@link Representation}s sharing a common {@link References} map. - * - * **When to use** - * - * Use when you use {@link fromASTs} to create this from multiple Schema ASTs, - * {@link toCodeDocument} to generate TypeScript code, and - * {@link toJsonSchemaMultiDocument} to convert to JSON Schema. + * Creates a declaration reviver while inferring its payload type from `payloadSchema`. * - * @see {@link Document} - * @see {@link fromASTs} - * - * @category models + * @category constructors * @since 4.0.0 */ -export type MultiDocument = { - readonly representations: readonly [Representation, ...Array] - readonly references: References -} - -// ----------------------------------------------------------------------------- -// schemas -// ----------------------------------------------------------------------------- - -const Representation$ref = Schema.suspend(() => $Representation) - -const toJsonAnnotationsBlacklist: Set = new Set([ - ...InternalRepresentation.fromASTBlacklist, - "expected", - "contentMediaType", - "contentSchema" -]) +export const makeDeclarationReviver:

( + id: string, + payloadSchema: Schema.Decoder

, + revive: DeclarationReviver

["revive"] +) => DeclarationReviver

= InternalSchema.makeDeclarationReviver /** - * A tree of primitive values used to serialize annotations to JSON. + * Creates a filter reviver while inferring its payload type from `payloadSchema`. * - * @category Tree + * @category constructors * @since 4.0.0 */ -export type PrimitiveTree = Schema.Tree +export const makeFilterReviver:

( + id: string, + payloadSchema: Schema.Decoder

, + revive: FilterReviver

["revive"] +) => FilterReviver

= InternalSchema.makeFilterReviver /** - * Schema for {@link PrimitiveTree}. - * - * **When to use** - * - * Use to validate recursive annotation metadata trees whose leaves are `null`, - * `number`, `boolean`, `bigint`, `symbol`, or `string`. + * Creates a filter group reviver while inferring its payload type from `payloadSchema`. * - * @see {@link PrimitiveTree} for the recursive tree type accepted by this codec - * @see {@link $Annotations} for the annotation codec that filters values through this codec - * - * @category schemas + * @category constructors * @since 4.0.0 */ -export const $PrimitiveTree: Schema.Codec = Schema.Tree( - Schema.Union([ - Schema.Null, - Schema.Number, // allows NaN, Infinity, -Infinity - Schema.Boolean, - Schema.BigInt, - Schema.Symbol, - Schema.String - ]) -) - -const isPrimitiveTree = Schema.is($PrimitiveTree) +export const makeFilterGroupReviver:

( + id: string, + payloadSchema: Schema.Decoder

, + revive: FilterGroupReviver

["revive"] +) => FilterGroupReviver

= InternalSchema.makeFilterGroupReviver /** - * Schema for serializing public `Schema.Annotations.Annotations` values. It - * filters out internal annotation keys and non-primitive values during - * encoding. + * Options for importing JSON Schema Draft 2020-12 documents. * * **When to use** * - * Use to serialize schema annotations in representation schemas while retaining - * only primitive-tree metadata. + * Use when you need to configure pattern handling or transform each JSON Schema node before translation. * * **Details** * - * Decoding is passthrough. Encoding removes internal annotation keys and values - * that are not accepted by `$PrimitiveTree`. + * `patterns` controls pattern constraints reached during best-effort translation, including `pattern`, the keys of + * `patternProperties`, and patterns nested in `propertyNames`: + * + * - `"error"` rejects the document and is the default. + * - `"ignore"` skips the constraint. + * - `"apply"` compiles and enforces the constraint with the runtime's native regular expression engine. + * + * **Gotchas** * - * @see {@link $PrimitiveTree} for the codec used to filter annotation values + * Use `patterns: "apply"` only for trusted documents because regular expression evaluation may block for an unbounded + * amount of time. `patterns: "ignore"` weakens validation by accepting values that the source document may reject. + * Ignoring `patternProperties` also skips its value constraints and `additionalProperties`, because matching keys cannot + * be determined without evaluating the patterns. + * `onEnter` must return a JSON Schema object. Its result is used directly, and exceptions raised by the callback pass + * through unchanged. * - * @category schemas + * @category models * @since 4.0.0 */ -export const $Annotations = Schema.Record(Schema.String, Schema.Unknown).pipe( - Schema.encodeTo(Schema.Record(Schema.String, $PrimitiveTree), { - decode: SchemaGetter.passthrough(), - encode: SchemaGetter.transformOptional(Option.flatMap((r) => { - const out: Record = {} - for (const [k, v] of Object.entries(r)) { - if (!toJsonAnnotationsBlacklist.has(k) && isPrimitiveTree(v)) { - out[k] = v - } - } - return Rec.isEmptyRecord(out) ? Option.none() : Option.some(out) - })) - }) -).annotate({ identifier: "Annotations" }) +export interface FromJsonSchemaOptions { + readonly onEnter?: ((schema: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined + /** + * Controls how reached JSON Schema regular expression patterns are imported. + * + * @default "error" + */ + readonly patterns?: "error" | "ignore" | "apply" | undefined +} /** - * Schema for the {@link Null} representation node. + * Runtime and TypeScript source generated for one schema. * - * @category schemas + * @category models * @since 4.0.0 */ -export const $Null = Schema.Struct({ - _tag: Schema.tag("Null"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Null" }) +export interface Code { + readonly runtime: string + readonly Type: string +} /** - * Schema for the {@link Undefined} representation node. + * Creates generated runtime and TypeScript source strings for a schema. * - * @category schemas + * @category constructors * @since 4.0.0 */ -export const $Undefined = Schema.Struct({ - _tag: Schema.tag("Undefined"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Undefined" }) +export const makeCode: (runtime: string, Type: string) => Code = InternalToCodeDocument.makeCode /** - * Schema for the {@link Void} representation node. + * Auxiliary source artifact emitted while generating schema code. * - * @category schemas + * @category models * @since 4.0.0 */ -export const $Void = Schema.Struct({ - _tag: Schema.tag("Void"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Void" }) +export type Artifact = + | { + readonly _tag: "Symbol" + readonly identifier: string + readonly code: Code + } + | { + readonly _tag: "Enum" + readonly identifier: string + readonly code: Code + } + | { + readonly _tag: "Import" + readonly importDeclaration: string + } /** - * Schema for the {@link Never} representation node. + * Generated schema code together with named references and auxiliary artifacts. * - * @category schemas + * @category models * @since 4.0.0 */ -export const $Never = Schema.Struct({ - _tag: Schema.tag("Never"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Never" }) +export interface CodeDocument { + readonly codes: ReadonlyArray + readonly references: { + readonly nonRecursives: ReadonlyArray<{ + readonly $ref: string + readonly code: Code + }> + readonly recursives: Readonly> + } + readonly artifacts: ReadonlyArray +} /** - * Schema for the {@link Unknown} representation node. + * Lowers the encoded side of an AST to a live representation document. * - * @category schemas - * @since 4.0.0 - */ -export const $Unknown = Schema.Struct({ - _tag: Schema.tag("Unknown"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Unknown" }) - -/** - * Schema for the {@link Any} representation node. + * **Details** + * + * Apply `SchemaAST.toType` to the AST first to lower its type side instead. * - * @category schemas + * @category constructors * @since 4.0.0 */ -export const $Any = Schema.Struct({ - _tag: Schema.tag("Any"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Any" }) - -const $IsStringFinite = Schema.Struct({ - _tag: Schema.tag("isStringFinite"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsStringFinite" }) - -const $IsStringBigInt = Schema.Struct({ - _tag: Schema.tag("isStringBigInt"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsStringBigInt" }) - -const $IsStringSymbol = Schema.Struct({ - _tag: Schema.tag("isStringSymbol"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsStringSymbol" }) - -const $IsTrimmed = Schema.Struct({ - _tag: Schema.tag("isTrimmed"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsTrimmed" }) - -const $IsUUID = Schema.Struct({ - _tag: Schema.tag("isUUID"), - regExp: Schema.RegExp, - version: Schema.UndefinedOr(Schema.Literals([1, 2, 3, 4, 5, 6, 7, 8])) -}).annotate({ identifier: "IsUUID" }) - -const $IsGUID = Schema.Struct({ - _tag: Schema.tag("isGUID"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsGUID" }) - -const $IsULID = Schema.Struct({ - _tag: Schema.tag("isULID"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsULID" }) - -const $IsBase64 = Schema.Struct({ - _tag: Schema.tag("isBase64"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsBase64" }) - -const $IsBase64Url = Schema.Struct({ - _tag: Schema.tag("isBase64Url"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsBase64Url" }) - -const $IsStartsWith = Schema.Struct({ - _tag: Schema.tag("isStartsWith"), - startsWith: Schema.String, - regExp: Schema.RegExp -}).annotate({ identifier: "IsStartsWith" }) - -const $IsEndsWith = Schema.Struct({ - _tag: Schema.tag("isEndsWith"), - endsWith: Schema.String, - regExp: Schema.RegExp -}).annotate({ identifier: "IsEndsWith" }) - -const $IsIncludes = Schema.Struct({ - _tag: Schema.tag("isIncludes"), - includes: Schema.String, - regExp: Schema.RegExp -}).annotate({ identifier: "IsIncludes" }) - -const $IsUppercased = Schema.Struct({ - _tag: Schema.tag("isUppercased"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsUppercased" }) - -const $IsLowercased = Schema.Struct({ - _tag: Schema.tag("isLowercased"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsLowercased" }) - -const $IsCapitalized = Schema.Struct({ - _tag: Schema.tag("isCapitalized"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsCapitalized" }) - -const $IsUncapitalized = Schema.Struct({ - _tag: Schema.tag("isUncapitalized"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsUncapitalized" }) - -const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) - -const $IsMinLength = Schema.Struct({ - _tag: Schema.tag("isMinLength"), - minLength: NonNegativeInt -}).annotate({ identifier: "IsMinLength" }) - -const $IsMaxLength = Schema.Struct({ - _tag: Schema.tag("isMaxLength"), - maxLength: NonNegativeInt -}).annotate({ identifier: "IsMaxLength" }) - -const $IsLengthBetween = Schema.Struct({ - _tag: Schema.tag("isLengthBetween"), - minimum: NonNegativeInt, - maximum: NonNegativeInt -}).annotate({ identifier: "IsLengthBetween" }) - -const $IsPattern = Schema.Struct({ - _tag: Schema.tag("isPattern"), - regExp: Schema.RegExp -}).annotate({ identifier: "IsPattern" }) - -/** - * Schema for {@link StringMeta}. - * - * @category schemas - * @since 4.0.0 - */ -export const $StringMeta = Schema.Union([ - $IsStringFinite, - $IsStringBigInt, - $IsStringSymbol, - $IsTrimmed, - $IsUUID, - $IsGUID, - $IsULID, - $IsBase64, - $IsBase64Url, - $IsStartsWith, - $IsEndsWith, - $IsIncludes, - $IsUppercased, - $IsLowercased, - $IsCapitalized, - $IsUncapitalized, - $IsMinLength, - $IsMaxLength, - $IsPattern, - $IsLengthBetween -]).annotate({ identifier: "StringMeta" }) - -function makeCheck(meta: Schema.Codec, identifier: string) { - const Check$ref = Schema.suspend(() => Check) - const Check: Schema.Codec> = Schema.Union([ - Schema.Struct({ - _tag: Schema.tag("Filter"), - annotations: Schema.optional($Annotations), - meta - }).annotate({ identifier: `${identifier}Filter` }), - Schema.Struct({ - _tag: Schema.tag("FilterGroup"), - annotations: Schema.optional($Annotations), - checks: Schema.NonEmptyArray(Check$ref) - }).annotate({ identifier: `${identifier}FilterGroup` }) - ]).annotate({ identifier: `${identifier}Check` }) - return Check +export function toRepresentation(ast: SchemaAST.AST): Document { + return InternalToRepresentation.toRepresentation(ast) } /** - * Schema for the {@link String} representation node. + * Lowers one or more AST encoded sides in a shared reference environment. * - * @category schemas - * @since 4.0.0 - */ -export const $String = Schema.Struct({ - _tag: Schema.tag("String"), - annotations: Schema.optional($Annotations), - checks: Schema.Array(makeCheck($StringMeta, "String")), - contentMediaType: Schema.optional(Schema.String), - contentSchema: Schema.optional(Representation$ref) -}).annotate({ identifier: "String" }) - -const $IsInt = Schema.Struct({ - _tag: Schema.tag("isInt") -}).annotate({ identifier: "IsInt" }) - -const $IsMultipleOf = Schema.Struct({ - _tag: Schema.tag("isMultipleOf"), - divisor: Schema.Finite -}).annotate({ identifier: "IsMultipleOf" }) - -const $IsFinite = Schema.Struct({ - _tag: Schema.tag("isFinite") -}).annotate({ identifier: "IsFinite" }) - -const $IsGreaterThan = Schema.Struct({ - _tag: Schema.tag("isGreaterThan"), - exclusiveMinimum: Schema.Finite -}).annotate({ identifier: "IsGreaterThan" }) - -const $IsGreaterThanOrEqualTo = Schema.Struct({ - _tag: Schema.tag("isGreaterThanOrEqualTo"), - minimum: Schema.Finite -}).annotate({ identifier: "IsGreaterThanOrEqualTo" }) - -const $IsLessThan = Schema.Struct({ - _tag: Schema.tag("isLessThan"), - exclusiveMaximum: Schema.Finite -}).annotate({ identifier: "IsLessThan" }) - -const $IsLessThanOrEqualTo = Schema.Struct({ - _tag: Schema.tag("isLessThanOrEqualTo"), - maximum: Schema.Finite -}).annotate({ identifier: "IsLessThanOrEqualTo" }) - -const $IsBetween = Schema.Struct({ - _tag: Schema.tag("isBetween"), - minimum: Schema.Finite, - maximum: Schema.Finite, - exclusiveMinimum: Schema.optional(Schema.Boolean), - exclusiveMaximum: Schema.optional(Schema.Boolean) -}).annotate({ identifier: "IsBetween" }) - -/** - * Schema for {@link NumberMeta}. + * **Details** * - * @category schemas - * @since 4.0.0 - */ -export const $NumberMeta = Schema.Union([ - $IsInt, - $IsMultipleOf, - $IsFinite, - $IsGreaterThan, - $IsGreaterThanOrEqualTo, - $IsLessThan, - $IsLessThanOrEqualTo, - $IsBetween -]).annotate({ identifier: "NumberMeta" }) - -/** - * Schema for the {@link Number} representation node. + * Apply `SchemaAST.toType` to an AST first to lower its type side instead. * - * @category schemas + * @category constructors * @since 4.0.0 */ -export const $Number = Schema.Struct({ - _tag: Schema.tag("Number"), - annotations: Schema.optional($Annotations), - checks: Schema.Array(makeCheck($NumberMeta, "Number")) -}).annotate({ identifier: "Number" }) +export function toRepresentations( + asts: readonly [SchemaAST.AST, ...Array] +): MultiDocument { + return InternalToRepresentation.toRepresentations(asts) +} /** - * Schema for the {@link Boolean} representation node. + * Wraps a single representation document as a multi-document with one root. + * + * **When to use** * - * @category schemas + * Use when an API such as `toCodeDocument` requires a `MultiDocument`. + * + * @category transforming * @since 4.0.0 */ -export const $Boolean = Schema.Struct({ - _tag: Schema.tag("Boolean"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Boolean" }) - -const $IsGreaterThanBigInt = Schema.Struct({ - _tag: Schema.tag("isGreaterThanBigInt"), - exclusiveMinimum: Schema.BigInt -}).annotate({ identifier: "IsGreaterThanBigInt" }) - -const $IsGreaterThanOrEqualToBigInt = Schema.Struct({ - _tag: Schema.tag("isGreaterThanOrEqualToBigInt"), - minimum: Schema.BigInt -}).annotate({ identifier: "IsGreaterThanOrEqualToBigInt" }) - -const $IsLessThanBigInt = Schema.Struct({ - _tag: Schema.tag("isLessThanBigInt"), - exclusiveMaximum: Schema.BigInt -}).annotate({ identifier: "IsLessThanBigInt" }) - -const $IsLessThanOrEqualToBigInt = Schema.Struct({ - _tag: Schema.tag("isLessThanOrEqualToBigInt"), - maximum: Schema.BigInt -}).annotate({ identifier: "IsLessThanOrEqualToBigInt" }) - -const $IsBetweenBigInt = Schema.Struct({ - _tag: Schema.tag("isBetweenBigInt"), - minimum: Schema.BigInt, - maximum: Schema.BigInt, - exclusiveMinimum: Schema.optional(Schema.Boolean), - exclusiveMaximum: Schema.optional(Schema.Boolean) -}).annotate({ identifier: "IsBetweenBigInt" }) - -const $BigIntMeta = Schema.Union([ - $IsGreaterThanBigInt, - $IsGreaterThanOrEqualToBigInt, - $IsLessThanBigInt, - $IsLessThanOrEqualToBigInt, - $IsBetweenBigInt -]).annotate({ identifier: "BigIntMeta" }) +export function toMultiDocument(document: Document): MultiDocument { + return { + representations: [document.representation], + references: document.references + } +} /** - * Schema for the {@link BigInt} representation node. + * Compiles a live representation document to JSON Schema Draft 2020-12. * * **When to use** * - * Use to encode, decode, or validate serialized `BigInt` representation nodes, - * not application `bigint` values. + * Use when you need JSON Schema output from a representation whose checks carry compiler annotations. * - * **Details** + * **Gotchas** * - * Accepts representation nodes with `_tag: "BigInt"`, optional annotations, - * and bigint-specific validation metadata in `checks`. + * Opaque declarations are represented by an unconstrained JSON Schema. Check callback results are used directly, and + * exceptions raised by a callback pass through unchanged. Callbacks must treat their input schemas as immutable. Each + * returned value must be a valid JSON Schema object graph and must not be mutated after the callback returns. Local + * definition references returned by callbacks are resolved together with compiler-generated references. * - * @see {@link BigIntMeta} for the metadata accepted by the `checks` array + * @see {@link toJsonSchemaMultiDocument} for multiple roots sharing definitions * - * @category schemas + * @category transforming * @since 4.0.0 */ -export const $BigInt = Schema.Struct({ - _tag: Schema.tag("BigInt"), - annotations: Schema.optional($Annotations), - checks: Schema.Array(makeCheck($BigIntMeta, "BigInt")) -}).annotate({ identifier: "BigInt" }) +export function toJsonSchemaDocument( + document: Document, + options?: Schema.ToJsonSchemaOptions +): JsonSchema.Document<"draft-2020-12"> { + return InternalToJsonSchemaDocument.toJsonSchemaDocument(document, options) +} /** - * Schema for the {@link Symbol} representation node. + * Compiles multiple live representations to a shared JSON Schema Draft 2020-12 document. * - * @category schemas - * @since 4.0.0 - */ -export const $Symbol = Schema.Struct({ - _tag: Schema.tag("Symbol"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Symbol" }) - -/** - * Schema for the literal value types allowed in a {@link Literal} node - * (string, finite number, boolean, or bigint). + * **When to use** * - * @category schemas - * @since 4.0.0 - */ -export const $LiteralValue = Schema.Union([ - Schema.String, - Schema.Finite, - Schema.Boolean, - Schema.BigInt -]).annotate({ identifier: "LiteralValue" }) - -/** - * Schema for the {@link Literal} representation node. + * Use when several representation roots must share the same JSON Schema definitions. * - * @category schemas - * @since 4.0.0 - */ -export const $Literal = Schema.Struct({ - _tag: Schema.tag("Literal"), - annotations: Schema.optional($Annotations), - literal: $LiteralValue -}).annotate({ identifier: "Literal" }) - -/** - * Schema for the {@link UniqueSymbol} representation node. + * **Gotchas** * - * @category schemas - * @since 4.0.0 - */ -export const $UniqueSymbol = Schema.Struct({ - _tag: Schema.tag("UniqueSymbol"), - annotations: Schema.optional($Annotations), - symbol: Schema.Symbol -}).annotate({ identifier: "UniqueSymbol" }) - -/** - * Schema for the {@link ObjectKeyword} representation node. + * Every definition is compiled, including definitions that are not reachable from a root. Check callbacks must treat + * their input schemas as immutable. Each returned value must be a valid JSON Schema object graph and must not be + * mutated after the callback returns. Local definition references returned by callbacks are resolved together with + * compiler-generated references. * - * @category schemas - * @since 4.0.0 - */ -export const $ObjectKeyword = Schema.Struct({ - _tag: Schema.tag("ObjectKeyword"), - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "ObjectKeyword" }) - -/** - * Schema for the {@link Enum} representation node. + * @see {@link toJsonSchemaDocument} for a single root * - * @category schemas + * @category transforming * @since 4.0.0 */ -export const $Enum = Schema.Struct({ - _tag: Schema.tag("Enum"), - annotations: Schema.optional($Annotations), - enums: Schema.Array( - Schema.Tuple([ - Schema.String, - Schema.Union([ - Schema.String, - Schema.Number // NaN, Infinity, -Infinity are allowed enum values - ]) - ]) - ) -}).annotate({ identifier: "Enum" }) +export function toJsonSchemaMultiDocument( + document: MultiDocument, + options?: Schema.ToJsonSchemaOptions +): JsonSchema.MultiDocument<"draft-2020-12"> { + return InternalToJsonSchemaDocument.toJsonSchemaMultiDocument(document, options) +} /** - * Schema for the {@link TemplateLiteral} representation node. + * Generates TypeScript source for live schema representations and their definitions. * - * @category schemas - * @since 4.0.0 - */ -export const $TemplateLiteral = Schema.Struct({ - _tag: Schema.tag("TemplateLiteral"), - annotations: Schema.optional($Annotations), - parts: Schema.Array(Representation$ref) -}).annotate({ identifier: "TemplateLiteral" }) - -/** - * Schema for the {@link Element} type (positional tuple element). + * **When to use** * - * @category schemas - * @since 4.0.0 - */ -export const $Element = Schema.Struct({ - isOptional: Schema.Boolean, - type: Representation$ref, - annotations: Schema.optional($Annotations) -}).annotate({ identifier: "Element" }) - -const $IsUnique = Schema.Struct({ - _tag: Schema.tag("isUnique") -}).annotate({ identifier: "IsUnique" }) - -const $ArraysMeta = Schema.Union([ - $IsMinLength, - $IsMaxLength, - $IsLengthBetween, - $IsUnique -]).annotate({ identifier: "ArraysMeta" }) - -/** - * Schema for the {@link Arrays} representation node. + * Use when custom declarations and checks provide `toCode` callbacks and must be emitted without a central handler registry. * - * @category schemas - * @since 4.0.0 - */ -export const $Arrays = Schema.Struct({ - _tag: Schema.tag("Arrays"), - annotations: Schema.optional($Annotations), - elements: Schema.Array($Element), - rest: Schema.Array(Representation$ref), - checks: Schema.Array(makeCheck($ArraysMeta, "Arrays")) -}).annotate({ identifier: "Arrays" }) - -/** - * Schema for the {@link PropertySignature} type. + * **Gotchas** * - * @category schemas - * @since 4.0.0 - */ -export const $PropertySignature = Schema.Struct({ - annotations: Schema.optional($Annotations), - name: Schema.PropertyKey, - type: Representation$ref, - isOptional: Schema.Boolean, - isMutable: Schema.Boolean -}).annotate({ identifier: "PropertySignature" }) - -/** - * Schema for the {@link IndexSignature} type. + * Opaque declarations and leaf checks require `toCode` callbacks. Callback results are used directly, and exceptions raised by a callback pass through unchanged. * - * @category schemas + * @category transforming * @since 4.0.0 */ -export const $IndexSignature = Schema.Struct({ - parameter: Representation$ref, - type: Representation$ref -}).annotate({ identifier: "IndexSignature" }) +export function toCodeDocument(document: MultiDocument): CodeDocument { + return InternalToCodeDocument.toCodeDocument(document) +} -const $IsMinProperties = Schema.Struct({ - _tag: Schema.tag("isMinProperties"), - minProperties: NonNegativeInt -}).annotate({ identifier: "IsMinProperties" }) +const RepresentationSchema = Schema.suspend( + (): Schema.Codec => RepresentationUnion +) +const RepresentationsSchema = Schema.Array(RepresentationSchema) -const $IsMaxProperties = Schema.Struct({ - _tag: Schema.tag("isMaxProperties"), - maxProperties: NonNegativeInt -}).annotate({ identifier: "IsMaxProperties" }) +const RepresentationAnnotationSchema = Schema.Struct({ + id: Schema.NonEmptyString, + payload: Schema.Json +}) -const $IsPropertiesLengthBetween = Schema.Struct({ - _tag: Schema.tag("isPropertiesLengthBetween"), - minimum: NonNegativeInt, - maximum: NonNegativeInt -}).annotate({ identifier: "IsPropertiesLengthBetween" }) +const CheckRepresentationAnnotationSchema = Schema.Struct({ + ...RepresentationAnnotationSchema.fields, + schemas: Schema.optional(RepresentationsSchema) +}) -const $IsPropertyNames = Schema.Struct({ - _tag: Schema.tag("isPropertyNames"), - propertyNames: Representation$ref -}).annotate({ identifier: "IsPropertyNames" }) +const LiveAnnotationsSchema = Schema.Record(Schema.String, Schema.Unknown) +const JsonAnnotationsSchema = Schema.Record(Schema.String, Schema.Json) -/** - * Schema for {@link ObjectsMeta}. - * - * @category schemas - * @since 4.0.0 - */ -export const $ObjectsMeta = Schema.Union([ - $IsMinProperties, - $IsMaxProperties, - $IsPropertiesLengthBetween, - $IsPropertyNames -]).annotate({ identifier: "ObjectsMeta" }) +function pruneAnnotations( + annotations: Readonly> +): Option.Option>> { + const out: Record = {} + for (const [key, value] of Object.entries(annotations)) { + if (SchemaAST.isJson(value)) { + InternalRecord.assignProperty(out, key, value) + } + } + return Object.keys(out).length === 0 ? Option.none() : Option.some(out) +} -/** - * Schema for the {@link Objects} representation node. - * - * @category schemas - * @since 4.0.0 - */ -export const $Objects = Schema.Struct({ - _tag: Schema.tag("Objects"), - annotations: Schema.optional($Annotations), - propertySignatures: Schema.Array($PropertySignature), - indexSignatures: Schema.Array($IndexSignature), - checks: Schema.Array(makeCheck($ObjectsMeta, "Objects")) -}).annotate({ identifier: "Objects" }) +const AnnotationsSchema = Schema.optional(LiveAnnotationsSchema).pipe( + Schema.encodeTo(Schema.optionalKey(JsonAnnotationsSchema), { + decode: SchemaGetter.passthroughSubtype(), + encode: SchemaGetter.transformOptional((annotations) => + Option.isNone(annotations) || annotations.value === undefined + ? Option.none() + : pruneAnnotations(annotations.value) + ) + }) +) -/** - * Schema for the {@link Union} representation node. - * - * @category schemas - * @since 4.0.0 - */ -export const $Union = Schema.Struct({ +const CheckSchema = Schema.suspend((): Schema.Codec => CheckUnion) +const ChecksSchema = Schema.Array(CheckSchema) +const KeywordFields = { + annotations: AnnotationsSchema, + checks: ChecksSchema +} +const FilterSchema = Schema.Struct({ + _tag: Schema.tag("Filter"), + representation: CheckRepresentationAnnotationSchema, + annotations: AnnotationsSchema, + aborted: Schema.Boolean +}) +const FilterGroupSchema = Schema.Struct({ + _tag: Schema.tag("FilterGroup"), + representation: Schema.optional(CheckRepresentationAnnotationSchema), + annotations: AnnotationsSchema, + checks: Schema.NonEmptyArray(CheckSchema) +}) +const CheckUnion = Schema.Union([FilterSchema, FilterGroupSchema]) + +function makeKeywordSchema>(tag: Tag) { + return Schema.Struct({ + _tag: Schema.tag(tag), + ...KeywordFields + }) +} + +const DeclarationSchema = Schema.Struct({ + _tag: Schema.tag("Declaration"), + representation: RepresentationAnnotationSchema, + annotations: AnnotationsSchema, + typeParameters: RepresentationsSchema, + checks: ChecksSchema +}) +const SuspendSchema = Schema.Struct({ + _tag: Schema.tag("Suspend"), + annotations: AnnotationsSchema, + checks: Schema.Tuple([]), + thunk: RepresentationSchema +}) +function makeValueSchema(type: Type, value: Schema.Codec) { + return value.pipe( + Schema.encodeTo(Schema.Struct({ type: Schema.tag(type), value }), { + decode: SchemaGetter.transform((encoded: { readonly type: Type; readonly value: Value }) => encoded.value), + encode: SchemaGetter.transform((value: Value) => ({ type, value })) + }) + ) +} +const StringValueCodec = makeValueSchema("string", Schema.String) +const NumberValueCodec = makeValueSchema("number", Schema.Number) +const LiteralSchema = Schema.Struct({ + _tag: Schema.tag("Literal"), + ...KeywordFields, + literal: Schema.Union([ + StringValueCodec, + makeValueSchema("number", Schema.Finite), + makeValueSchema("bigint", Schema.BigInt), + makeValueSchema("boolean", Schema.Boolean) + ]) +}) +const UniqueSymbolSchema = Schema.Struct({ + _tag: Schema.tag("UniqueSymbol"), + ...KeywordFields, + symbol: Schema.Symbol +}) +const EnumSchema = Schema.Struct({ + _tag: Schema.tag("Enum"), + ...KeywordFields, + enums: Schema.Array(Schema.Tuple([ + Schema.String, + Schema.Union([StringValueCodec, NumberValueCodec]) + ])) +}) +const TemplateLiteralSchema = Schema.Struct({ + _tag: Schema.tag("TemplateLiteral"), + ...KeywordFields, + parts: RepresentationsSchema +}) +const ElementSchema = Schema.Struct({ + isOptional: Schema.Boolean, + type: RepresentationSchema, + annotations: AnnotationsSchema +}) +const ArraysSchema = Schema.Struct({ + _tag: Schema.tag("Arrays"), + ...KeywordFields, + elements: Schema.Array(ElementSchema), + rest: RepresentationsSchema +}) +const PropertySignatureSchema = Schema.Struct({ + name: Schema.Union([ + StringValueCodec, + NumberValueCodec, + makeValueSchema("symbol", Schema.Symbol) + ]), + type: RepresentationSchema, + isOptional: Schema.Boolean, + isMutable: Schema.Boolean, + annotations: AnnotationsSchema +}) +const IndexSignatureSchema = Schema.Struct({ + parameter: RepresentationSchema, + type: RepresentationSchema +}) +const ObjectsSchema = Schema.Struct({ + _tag: Schema.tag("Objects"), + ...KeywordFields, + propertySignatures: Schema.Array(PropertySignatureSchema), + indexSignatures: Schema.Array(IndexSignatureSchema) +}) +const UnionSchema = Schema.Struct({ _tag: Schema.tag("Union"), - annotations: Schema.optional($Annotations), - types: Schema.Array(Representation$ref), + ...KeywordFields, + types: RepresentationsSchema, mode: Schema.Literals(["anyOf", "oneOf"]) -}).annotate({ identifier: "Union" }) - -/** - * Schema for the {@link Reference} representation node. - * - * @category schemas - * @since 4.0.0 - */ -export const $Reference = Schema.Struct({ +}) +const ReferenceSchema = Schema.Struct({ _tag: Schema.tag("Reference"), - $ref: Schema.String -}).annotate({ identifier: "Reference" }) - -const $IsDateValid = Schema.Struct({ - _tag: Schema.tag("isDateValid") -}).annotate({ identifier: "IsDateValid" }) - -const $IsGreaterThanDate = Schema.Struct({ - _tag: Schema.tag("isGreaterThanDate"), - exclusiveMinimum: Schema.Date -}).annotate({ identifier: "IsGreaterThanDate" }) + $ref: Schema.NonEmptyString +}) + +const RepresentationUnion = Schema.Union([ + DeclarationSchema, + ReferenceSchema, + SuspendSchema, + makeKeywordSchema("Null"), + makeKeywordSchema("Undefined"), + makeKeywordSchema("Void"), + makeKeywordSchema("Never"), + makeKeywordSchema("Unknown"), + makeKeywordSchema("Any"), + makeKeywordSchema("String"), + makeKeywordSchema("Number"), + makeKeywordSchema("Boolean"), + makeKeywordSchema("BigInt"), + makeKeywordSchema("Symbol"), + makeKeywordSchema("ObjectKeyword"), + LiteralSchema, + UniqueSymbolSchema, + EnumSchema, + TemplateLiteralSchema, + ArraysSchema, + ObjectsSchema, + UnionSchema +]) -const $IsGreaterThanOrEqualToDate = Schema.Struct({ - _tag: Schema.tag("isGreaterThanOrEqualToDate"), - minimum: Schema.Date -}).annotate({ identifier: "IsGreaterThanOrEqualToDate" }) +const ReferencesSchema = Schema.Record(Schema.String, RepresentationSchema) -const $IsLessThanDate = Schema.Struct({ - _tag: Schema.tag("isLessThanDate"), - exclusiveMaximum: Schema.Date -}).annotate({ identifier: "IsLessThanDate" }) +const DocumentFromJson: Schema.Codec = Schema.toCodecJson( + Schema.Struct({ + representation: RepresentationSchema, + references: ReferencesSchema + }) +) -const $IsLessThanOrEqualToDate = Schema.Struct({ - _tag: Schema.tag("isLessThanOrEqualToDate"), - maximum: Schema.Date -}).annotate({ identifier: "IsLessThanOrEqualToDate" }) +const MultiDocumentFromJson: Schema.Codec = Schema.toCodecJson( + Schema.Struct({ + representations: Schema.NonEmptyArray(RepresentationSchema), + references: ReferencesSchema + }) +) -const $IsBetweenDate = Schema.Struct({ - _tag: Schema.tag("isBetweenDate"), - minimum: Schema.Date, - maximum: Schema.Date, - exclusiveMinimum: Schema.optional(Schema.Boolean), - exclusiveMaximum: Schema.optional(Schema.Boolean) -}).annotate({ identifier: "IsBetweenDate" }) +const encodeDocument = Schema.encodeSync(DocumentFromJson) +const encodeMultiDocument = Schema.encodeSync(MultiDocumentFromJson) +const decodeDocument = Schema.decodeSync(DocumentFromJson) +const decodeMultiDocument = Schema.decodeSync(MultiDocumentFromJson) /** - * Schema for {@link DateMeta}. + * Projects a live single-root representation document and encodes it as JSON. * - * @category schemas - * @since 4.0.0 - */ -export const $DateMeta = Schema.Union([ - $IsDateValid, - $IsGreaterThanDate, - $IsGreaterThanOrEqualToDate, - $IsLessThanDate, - $IsLessThanOrEqualToDate, - $IsBetweenDate -]).annotate({ identifier: "DateMeta" }) - -const $IsMinSize = Schema.Struct({ - _tag: Schema.tag("isMinSize"), - minSize: NonNegativeInt -}).annotate({ identifier: "IsMinSize" }) - -const $IsMaxSize = Schema.Struct({ - _tag: Schema.tag("isMaxSize"), - maxSize: NonNegativeInt -}).annotate({ identifier: "IsMaxSize" }) - -const $IsSizeBetween = Schema.Struct({ - _tag: Schema.tag("isSizeBetween"), - minimum: NonNegativeInt, - maximum: NonNegativeInt -}).annotate({ identifier: "IsSizeBetween" }) - -/** - * Schema for {@link SizeMeta}. + * **When to use** * - * @category schemas - * @since 4.0.0 - */ -export const $SizeMeta = Schema.Union([ - $IsMinSize, - $IsMaxSize, - $IsSizeBetween -]).annotate({ identifier: "SizeMeta" }) - -/** - * Schema for {@link DeclarationMeta}. + * Use when you need a stable JSON value for storage or transport after calling `toRepresentation`. * - * @category schemas - * @since 4.0.0 - */ -export const $DeclarationMeta = Schema.Union([ - $DateMeta, - $SizeMeta -]).annotate({ identifier: "DeclarationMeta" }) - -/** - * Schema for the {@link Declaration} representation node. + * **Gotchas** * - * @category schemas - * @since 4.0.0 - */ -export const $Declaration = Schema.Struct({ - _tag: Schema.tag("Declaration"), - annotations: Schema.optional($Annotations), - typeParameters: Schema.Array(Representation$ref), - checks: Schema.Array(makeCheck($DeclarationMeta, "Declaration")), - encodedSchema: Representation$ref -}).annotate({ identifier: "Declaration" }) - -/** - * Schema for the {@link Suspend} representation node. + * Generic annotations that are not JSON are omitted. Invalid persistence identities and unsupported structural values throw an `Error` containing their representation path. * - * @category schemas - * @since 4.0.0 - */ -export const $Suspend = Schema.Struct({ - _tag: Schema.tag("Suspend"), - annotations: Schema.optional($Annotations), - checks: Schema.Tuple([]), - thunk: Representation$ref -}).annotate({ identifier: "Suspend" }) - -/** - * Type-level helper for the recursive {@link $Representation} codec. + * @see {@link toRepresentation} for constructing the live document + * @see {@link toJsonMultiDocument} for documents with multiple roots * - * @category schemas + * @category encoding * @since 4.0.0 */ -export interface $Representation extends Schema.Codec {} +export function toJson(document: Document): Schema.Json { + return encodeDocument(document) +} /** - * Schema for the full {@link Representation} union. It recursively validates - * and encodes any representation node. + * Projects a live multi-root representation document and encodes it as JSON. + * + * **When to use** + * + * Use when you need one JSON value for multiple live roots that share a reference environment. * - * @category schemas + * **Gotchas** + * + * The root order and shared reference keys are preserved, while non-JSON generic annotations are omitted. + * + * @see {@link toRepresentations} for constructing the live multi-document + * @see {@link toJson} for a single-root document + * + * @category encoding * @since 4.0.0 */ -export const $Representation: $Representation = Schema.Union([ - $Null, - $Undefined, - $Void, - $Never, - $Unknown, - $Any, - $String, - $Number, - $Boolean, - $BigInt, - $Symbol, - $Literal, - $UniqueSymbol, - $ObjectKeyword, - $Enum, - $TemplateLiteral, - $Arrays, - $Objects, - $Union, - $Reference, - $Declaration, - $Suspend -]).annotate({ identifier: "Schema" }) +export function toJsonMultiDocument(document: MultiDocument): Schema.Json { + return encodeMultiDocument(document) +} /** - * Schema for {@link Document}. + * Decodes a persisted single-root representation document from JSON. * * **When to use** * - * Use to validate or serialize a single schema representation document with - * `Schema.decodeUnknownSync` or `Schema.encodeSync`. + * Use when reading a representation document from storage or transport before inspecting it or passing it to `fromRepresentation`. * * **Gotchas** * - * This codec validates document structure but does not resolve `$ref` keys - * against `references`. + * Invalid documents throw a schema decoding error. Decoding does not reconstruct runtime callbacks. * - * @see {@link DocumentFromJson} for the JSON-string codec wrapper - * @see {@link $MultiDocument} for validating documents with multiple root representations + * @see {@link toJson} for encoding a document + * @see {@link fromRepresentation} for reconstructing a runtime schema + * @see {@link fromJsonMultiDocument} for multiple roots sharing references * - * @category schemas + * @category decoding * @since 4.0.0 */ -export const $Document = Schema.Struct({ - representation: $Representation, - references: Schema.Record(Schema.String, $Representation) -}).annotate({ identifier: "Document" }) +export function fromJson(input: Schema.Json): Document { + return decodeDocument(input) +} /** - * Schema for {@link MultiDocument}. + * Decodes a persisted multi-root representation document from JSON. + * + * **When to use** * - * @category schemas + * Use when reading multiple representation roots that share references before inspecting them or passing them to `fromRepresentations`. + * + * **Gotchas** + * + * Invalid documents throw a schema decoding error. Decoding does not reconstruct runtime callbacks. + * + * @see {@link toJsonMultiDocument} for encoding a multi-document + * @see {@link fromRepresentations} for reconstructing runtime schemas + * @see {@link fromJson} for a single root + * + * @category decoding * @since 4.0.0 */ -export const $MultiDocument = Schema.Struct({ - representations: Schema.NonEmptyArray($Representation), - references: Schema.Record(Schema.String, $Representation) -}).annotate({ identifier: "MultiDocument" }) - -// ----------------------------------------------------------------------------- -// APIs -// ----------------------------------------------------------------------------- +export function fromJsonMultiDocument(input: Schema.Json): MultiDocument { + return decodeMultiDocument(input) +} /** - * Converts a Schema AST into a {@link Document}. + * Reconstructs a runtime schema from a representation document. * * **When to use** * - * Use when you have a single Schema AST and need a schema representation - * document. + * Use when you have decoded or constructed a document whose declaration and check annotations may require revivers. * - * **Details** + * **Gotchas** * - * Shared/recursive sub-schemas are extracted into the `references` map. + * Revivers are resolved locally by `id`; none are installed implicitly. Reviver results are used directly, and exceptions raised by a reviver pass through unchanged. * - * **Example** (Converting a Schema to a Document) + * **Example** (Restoring a persisted schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaRepresentation } from "effect" * - * const Person = Schema.Struct({ - * name: Schema.String, - * age: Schema.Number - * }) + * const document = SchemaRepresentation.toRepresentation(Schema.Struct({ name: Schema.String }).ast) + * const persisted = SchemaRepresentation.toJson(document) + * const restored = SchemaRepresentation.fromJson(persisted) + * const schema = SchemaRepresentation.fromRepresentation(restored, { revivers: [] }) + * const Person = Schema.make>(schema.ast) * - * const doc = SchemaRepresentation.fromAST(Person.ast) - * console.log(doc.representation._tag) - * // "Objects" + * Schema.decodeUnknownSync(Person)({ name: "Ada" }) // => { name: "Ada" } * ``` * - * @see {@link Document} - * @see {@link fromASTs} + * @see {@link fromJson} for decoding a persisted document + * @see {@link fromRepresentations} for multiple roots sharing references * - * @category constructors + * @category transforming * @since 4.0.0 */ -export const fromAST: (ast: SchemaAST.AST) => Document = InternalRepresentation.fromAST +export function fromRepresentation( + document: Document, + options: { readonly revivers: ReadonlyArray } +): Schema.Top { + return InternalFromRepresentation.fromRepresentation(document, options.revivers) +} /** - * Converts one or more Schema ASTs into a {@link MultiDocument}. + * Reconstructs multiple runtime schemas from a representation multi-document. * * **When to use** * - * Use when you have multiple Schema ASTs and need one schema representation - * `MultiDocument` with shared references. + * Use when multiple roots must be rebuilt in one shared reference environment. * - * **Details** + * **Gotchas** * - * All schemas share a single `references` map. + * Only references reachable from a root are revived. Revivers are resolved locally by `id`; none are installed implicitly. * - * @see {@link MultiDocument} - * @see {@link fromAST} + * @see {@link fromJsonMultiDocument} for decoding a persisted multi-document + * @see {@link fromRepresentation} for a single root * - * @category constructors + * @category transforming * @since 4.0.0 */ -export const fromASTs: (asts: readonly [SchemaAST.AST, ...Array]) => MultiDocument = - InternalRepresentation.fromASTs +export function fromRepresentations( + document: MultiDocument, + options: { readonly revivers: ReadonlyArray } +): readonly [Schema.Top, ...Array] { + return InternalFromRepresentation.fromRepresentations(document, options.revivers) +} /** - * Schema that decodes a {@link Document} from JSON and encodes it back. + * Imports a JSON Schema Draft 2020-12 document as a runtime schema. * * **When to use** * - * Use when you need a JSON codec for schema representation documents with - * `Schema.decodeUnknownSync` or `Schema.encodeSync`. - * - * **Example** (Round-tripping a Document through JSON) + * Use when you need to validate or transform values described by an external JSON Schema document. * - * ```ts - * import { Schema, SchemaRepresentation } from "effect" + * **Gotchas** * - * const doc = SchemaRepresentation.fromAST(Schema.String.ast) - * const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)(doc) - * const back = Schema.decodeUnknownSync(SchemaRepresentation.DocumentFromJson)(json) - * ``` + * Import is best-effort. Built-in declarations and checks are reconstructed with importer-owned revivers. Pattern + * constraints reached during translation cause an error by default. Use `patterns: "apply"` only for trusted documents, + * or `patterns: "ignore"` to weaken validation explicitly. Callback results are used directly, and exceptions raised by a + * callback pass through unchanged. * - * @see {@link $Document} - * @see {@link MultiDocumentFromJson} + * @see {@link fromJsonSchemaMultiDocument} for multiple roots sharing definitions + * @see {@link toRepresentation} for converting the result to a representation document * - * @category schemas + * @category constructors * @since 4.0.0 */ -export const DocumentFromJson: Schema.Codec = Schema.toCodecJson($Document) +export function fromJsonSchemaDocument( + document: JsonSchema.Document<"draft-2020-12">, + options?: FromJsonSchemaOptions +): Schema.Top { + return InternalFromJsonSchemaDocument.fromJsonSchemaDocument(document, options) +} /** - * Schema for `MultiDocument` values encoded as JSON. + * Imports multiple JSON Schema Draft 2020-12 roots as runtime schemas with shared definitions. * - * @see {@link $MultiDocument} - * @see {@link DocumentFromJson} + * **When to use** * - * @category schemas - * @since 4.0.0 - */ -export const MultiDocumentFromJson: Schema.Codec = Schema.toCodecJson($MultiDocument) - -/** - * Wraps a single {@link Document} as a {@link MultiDocument} with one - * representation. + * Use when multiple imported roots share reachable definitions, aliases, or recursion. * - * **When to use** + * **Gotchas** * - * Use when you need to pass a single schema representation `Document` where an - * API expects a `MultiDocument`. + * Only definitions reachable from a root are translated. Pattern constraints reached during translation cause an error + * by default. Use `patterns: "apply"` only for trusted documents, or `patterns: "ignore"` to weaken validation explicitly. + * Callback results are used directly, and exceptions raised by a callback pass through unchanged. * - * @see {@link Document} - * @see {@link MultiDocument} + * @see {@link fromJsonSchemaDocument} for a single root + * @see {@link toRepresentations} for converting the returned schema ASTs to a representation document * - * @category transforming + * @category constructors * @since 4.0.0 */ -export function toMultiDocument(document: Document): MultiDocument { - return { - representations: [document.representation], - references: document.references - } -} - -/** - * A callback that handles {@link Declaration} nodes during reconstruction - * ({@link toSchema}) or code generation ({@link toCodeDocument}). - * - * **Details** - * - * Return a value to handle the declaration. Return `undefined` to fall back to - * default behavior, which uses `encodedSchema` for `toSchema` or the - * `generation` annotation for `toCodeDocument`. `recur` processes child - * representations recursively. - * - * @see {@link toSchema} - * @see {@link toSchemaDefaultReviver} - * @see {@link toCodeDocument} - * - * @category models - * @since 4.0.0 - */ -export type Reviver = (declaration: Declaration, recur: (representation: Representation) => T) => T | undefined - -/** - * Default {@link Reviver} for {@link toSchema} that handles built-in Effect - * types, including Option, Result, Redacted, Cause, Exit, ReadonlyMap, HashMap, - * ReadonlySet, Date, Duration, URL, and RegExp. - * - * **When to use** - * - * Use when you need the default `options.reviver` for {@link toSchema} to - * reconstruct runtime schemas for built-in Effect declarations. - * - * **Details** - * - * The reviver returns `undefined` for unrecognized declarations, causing - * fallback to `encodedSchema`. - * - * @see {@link toSchema} - * @see {@link Reviver} - * - * @category transforming - * @since 4.0.0 - */ -export const toSchemaDefaultReviver: Reviver = (s, recur) => { - const typeConstructor = s.annotations?.typeConstructor - if (Predicate.isObject(typeConstructor) && typeof typeConstructor._tag === "string") { - const typeParameters = s.typeParameters.map(recur) - switch (typeConstructor._tag) { - // built-in types - case "Date": - return Schema.Date - case "Error": - return Schema.Error(typeConstructor.options as Schema.ErrorOptions | undefined) - case "File": - return Schema.File - case "FormData": - return Schema.FormData - case "ReadonlyMap": - return Schema.ReadonlyMap(typeParameters[0], typeParameters[1]) - case "ReadonlySet": - return Schema.ReadonlySet(typeParameters[0]) - case "RegExp": - return Schema.RegExp - case "Uint8Array": - return Schema.Uint8Array - case "URL": - return Schema.URL - case "URLSearchParams": - return Schema.URLSearchParams - // effect types - case "effect/Option": - return Schema.Option(typeParameters[0]) - case "effect/Result": - return Schema.Result(typeParameters[0], typeParameters[1]) - case "effect/Redacted": - return Schema.Redacted(typeParameters[0], typeConstructor.options as any) - case "effect/DateTime.TimeZone": - return Schema.TimeZone - case "effect/DateTime.TimeZone.Named": - return Schema.TimeZoneNamed - case "effect/DateTime.TimeZone.Offset": - return Schema.TimeZoneOffset - case "effect/DateTime.Utc": - return Schema.DateTimeUtc - case "effect/DateTime.Zoned": - return Schema.DateTimeZoned - case "effect/BigDecimal": - return Schema.BigDecimal - case "effect/Chunk": - return Schema.Chunk(typeParameters[0]) - case "effect/Cause": - return Schema.Cause(typeParameters[0], typeParameters[1]) - case "effect/Cause/Failure": - return Schema.CauseReason(typeParameters[0], typeParameters[1]) - case "effect/Duration": - return Schema.Duration - case "effect/Exit": - return Schema.Exit(typeParameters[0], typeParameters[1], typeParameters[2]) - case "effect/Json": - return Schema.Json - case "effect/MutableJson": - return Schema.MutableJson - case "effect/HashMap": - return Schema.HashMap(typeParameters[0], typeParameters[1]) - case "effect/HashSet": - return Schema.HashSet(typeParameters[0]) - } - } -} - -/** - * Creates a runtime Schema from a {@link Document}. - * - * **When to use** - * - * Use when you have a serialized or computed schema representation document and - * need a runtime Schema for decoding/encoding. - * - * **Details** - * - * Pass `options.reviver`, such as {@link toSchemaDefaultReviver}, to handle - * {@link Declaration} nodes for types like `Date` and `Option`. Without a - * reviver, declarations fall back to their `encodedSchema`. Circular references - * are handled via lazy `Schema.suspend`. - * - * **Gotchas** - * - * This throws if a `$ref` is not found in `document.references`. - * - * **Example** (Reconstructing a Schema) - * - * ```ts - * import { Schema, SchemaRepresentation } from "effect" - * - * const doc = SchemaRepresentation.fromAST( - * Schema.Struct({ name: Schema.String }).ast - * ) - * - * const schema = SchemaRepresentation.toSchema(doc) - * console.log(JSON.stringify(Schema.toJsonSchemaDocument(schema), null, 2)) - * ``` - * - * @see {@link Document} - * @see {@link Reviver} - * @see {@link toSchemaDefaultReviver} - * - * @category Runtime Generation - * @since 4.0.0 - */ -export function toSchema(document: Document, options?: { - readonly reviver?: Reviver | undefined -}): S { - type Slot = { - // 0 = not started, 1 = building, 2 = done - state: 0 | 1 | 2 - value: Schema.Top | undefined - ref: Schema.Top - } - - const slots = new Map() - - return recur(document.representation) as S - - function recur(r: Representation): Schema.Top { - let out = on(r) - if ("annotations" in r && r.annotations) out = out.annotate(r.annotations) - out = toSchemaChecks(out, r) - return out - } - - function getSlot(identifier: string): Slot { - const existing = slots.get(identifier) - if (existing) return existing - - // Create the slot *before* resolving, so self-references can see it. - const slot: Slot = { - state: 0, - value: undefined, - ref: Schema.suspend(() => { - if (slot.value === undefined) { - return Schema.Unknown - } - return slot.value - }) - } - slots.set(identifier, slot) - return slot - } - - function resolveReference($ref: string): Schema.Top { - const definition = document.references[$ref] - if (definition === undefined) { - throw new Error(`Reference ${$ref} not found`) - } - - const slot = getSlot($ref) - - if (slot.state === 2) { - // Already built: return the built schema directly - return slot.value! - } - - if (slot.state === 1) { - // Circular: we're currently building this identifier. - return slot.ref - } - - // First time: build it. - slot.state = 1 - try { - slot.value = recur(definition) - slot.state = 2 - return slot.value - } catch (e) { - // Leave the slot in a safe state so future thunks don't silently succeed. - slot.state = 0 - slot.value = undefined - throw e - } - } - - function on(r: Representation): Schema.Top { - switch (r._tag) { - case "Declaration": - return options?.reviver?.(r, recur) ?? recur(r.encodedSchema) - case "Reference": - return resolveReference(r.$ref) - case "Suspend": - return recur(r.thunk) - case "Null": - return Schema.Null - case "Undefined": - return Schema.Undefined - case "Void": - return Schema.Void - case "Never": - return Schema.Never - case "Unknown": - return Schema.Unknown - case "Any": - return Schema.Any - case "String": { - const contentMediaType = r.contentMediaType - const contentSchema = r.contentSchema - if (contentMediaType === "application/json" && contentSchema !== undefined) { - return Schema.fromJsonString(recur(contentSchema)) - } - return Schema.String - } - case "Number": - return Schema.Number - case "Boolean": - return Schema.Boolean - case "BigInt": - return Schema.BigInt - case "Symbol": - return Schema.Symbol - case "Literal": - return Schema.Literal(r.literal) - case "UniqueSymbol": - return Schema.UniqueSymbol(r.symbol) - case "ObjectKeyword": - return Schema.ObjectKeyword - case "Enum": - return Schema.Enum(Object.fromEntries(r.enums)) - case "TemplateLiteral": { - const parts = r.parts.map(recur) as Schema.TemplateLiteral.Parts - return Schema.TemplateLiteral(parts) - } - case "Arrays": { - const elements = r.elements.map((e) => { - const s = recur(e.type) - return e.isOptional ? Schema.optionalKey(s) : s - }) - const rest = r.rest.map(recur) - if (Arr.isArrayNonEmpty(rest)) { - if (r.elements.length === 0 && r.rest.length === 1) { - return Schema.Array(rest[0]) - } - return Schema.TupleWithRest(Schema.Tuple(elements), rest) - } - return Schema.Tuple(elements) - } - case "Objects": { - const fields: Record = {} - - for (const ps of r.propertySignatures) { - const s = recur(ps.type) - const withOptional = ps.isOptional ? Schema.optionalKey(s) : s - fields[ps.name] = ps.isMutable ? Schema.mutableKey(withOptional) : withOptional - } - - const indexSignatures = r.indexSignatures.map((is) => - Schema.Record(recur(is.parameter) as Schema.Record.Key, recur(is.type)) - ) - - if (Arr.isArrayNonEmpty(indexSignatures)) { - if (r.propertySignatures.length === 0 && indexSignatures.length === 1) { - return indexSignatures[0] - } - return Schema.StructWithRest(Schema.Struct(fields), indexSignatures) - } - - return Schema.Struct(fields) - } - case "Union": { - if (r.types.length === 0) return Schema.Never - if (r.types.every((t) => t._tag === "Literal")) { - if (r.types.length === 1) { - return Schema.Literal(r.types[0].literal) - } - return Schema.Literals(r.types.map((t) => t.literal)) - } - return Schema.Union(r.types.map(recur), { mode: r.mode }) - } - } - } - - function toSchemaChecks(top: Schema.Top, schema: Representation): Schema.Top { - switch (schema._tag) { - default: - return top - case "String": - case "Number": - case "BigInt": - case "Arrays": - case "Objects": - case "Declaration": { - const checks = schema.checks.map(toSchemaCheck) - return Arr.isArrayNonEmpty(checks) ? top.check(...checks) : top - } - } - } - - function toSchemaCheck(check: Check): SchemaAST.Check { - switch (check._tag) { - case "Filter": - return toSchemaFilter(check) - case "FilterGroup": { - return Schema.makeFilterGroup(Arr.map(check.checks, toSchemaCheck), check.annotations) - } - } - } - - function toSchemaFilter(filter: Filter): SchemaAST.Check { - const a = filter.annotations - switch (filter.meta._tag) { - // String Meta - case "isStringFinite": - return Schema.isStringFinite(a) - case "isStringBigInt": - return Schema.isStringBigInt(a) - case "isStringSymbol": - return Schema.isStringSymbol(a) - case "isMinLength": - return Schema.isMinLength(filter.meta.minLength, a) - case "isMaxLength": - return Schema.isMaxLength(filter.meta.maxLength, a) - case "isLengthBetween": - return Schema.isLengthBetween(filter.meta.minimum, filter.meta.maximum, a) - case "isPattern": - return Schema.isPattern(filter.meta.regExp, a) - case "isTrimmed": - return Schema.isTrimmed(a) - case "isUUID": - return Schema.isUUID(filter.meta.version, a) - case "isGUID": - return Schema.isGUID(a) - case "isULID": - return Schema.isULID(a) - case "isBase64": - return Schema.isBase64(a) - case "isBase64Url": - return Schema.isBase64Url(a) - case "isStartsWith": - return Schema.isStartsWith(filter.meta.startsWith, a) - case "isEndsWith": - return Schema.isEndsWith(filter.meta.endsWith, a) - case "isIncludes": - return Schema.isIncludes(filter.meta.includes, a) - case "isUppercased": - return Schema.isUppercased(a) - case "isLowercased": - return Schema.isLowercased(a) - case "isCapitalized": - return Schema.isCapitalized(a) - case "isUncapitalized": - return Schema.isUncapitalized(a) - - // Number Meta - case "isFinite": - return Schema.isFinite(a) - case "isInt": - return Schema.isInt(a) - case "isMultipleOf": - return Schema.isMultipleOf(filter.meta.divisor, a) - case "isGreaterThan": - return Schema.isGreaterThan(filter.meta.exclusiveMinimum, a) - case "isGreaterThanOrEqualTo": - return Schema.isGreaterThanOrEqualTo(filter.meta.minimum, a) - case "isLessThan": - return Schema.isLessThan(filter.meta.exclusiveMaximum, a) - case "isLessThanOrEqualTo": - return Schema.isLessThanOrEqualTo(filter.meta.maximum, a) - case "isBetween": - return Schema.isBetween(filter.meta, a) - - // BigInt Meta - case "isGreaterThanBigInt": - return Schema.isGreaterThanBigInt(filter.meta.exclusiveMinimum, a) - case "isGreaterThanOrEqualToBigInt": - return Schema.isGreaterThanOrEqualToBigInt(filter.meta.minimum, a) - case "isLessThanBigInt": - return Schema.isLessThanBigInt(filter.meta.exclusiveMaximum, a) - case "isLessThanOrEqualToBigInt": - return Schema.isLessThanOrEqualToBigInt(filter.meta.maximum, a) - case "isBetweenBigInt": - return Schema.isBetweenBigInt(filter.meta, a) - - // Object Meta - case "isMinProperties": - return Schema.isMinProperties(filter.meta.minProperties, a) - case "isMaxProperties": - return Schema.isMaxProperties(filter.meta.maxProperties, a) - case "isPropertiesLengthBetween": - return Schema.isPropertiesLengthBetween(filter.meta.minimum, filter.meta.maximum, a) - case "isPropertyNames": - return Schema.isPropertyNames(recur(filter.meta.propertyNames) as Schema.Record.Key, a) - - // Arrays Meta - case "isUnique": - return Schema.isUnique(a) - - // Date Meta - case "isDateValid": - return Schema.isDateValid(a) - case "isGreaterThanDate": - return Schema.isGreaterThanDate(filter.meta.exclusiveMinimum, a) - case "isGreaterThanOrEqualToDate": - return Schema.isGreaterThanOrEqualToDate(filter.meta.minimum, a) - case "isLessThanDate": - return Schema.isLessThanDate(filter.meta.exclusiveMaximum, a) - case "isLessThanOrEqualToDate": - return Schema.isLessThanOrEqualToDate(filter.meta.maximum, a) - case "isBetweenDate": - return Schema.isBetweenDate(filter.meta, a) - - // Size Meta - case "isMinSize": - return Schema.isMinSize(filter.meta.minSize, a) - case "isMaxSize": - return Schema.isMaxSize(filter.meta.maxSize, a) - case "isSizeBetween": - return Schema.isSizeBetween(filter.meta.minimum, filter.meta.maximum, a) - } - } -} - -/** - * Converts a {@link Document} to a Draft 2020-12 JSON Schema document. - * - * **When to use** - * - * Use when you need to produce a standard JSON Schema document from a schema - * representation `Document`. - * - * **Gotchas** - * - * JSON Schema generation is best-effort. Some Effect schema representation - * semantics cannot be represented exactly in JSON Schema, and importing an - * emitted JSON Schema may produce an equivalent approximation rather than the - * original representation shape. - * - * **Example** (Generating JSON Schema) - * - * ```ts - * import { Schema, SchemaRepresentation } from "effect" - * - * const doc = SchemaRepresentation.fromAST(Schema.String.ast) - * const jsonSchema = SchemaRepresentation.toJsonSchemaDocument(doc) - * console.log(jsonSchema.schema.type) - * // "string" - * ``` - * - * @see {@link Document} - * @see {@link toJsonSchemaMultiDocument} - * @see {@link fromJsonSchemaDocument} - * - * @category transforming - * @since 4.0.0 - */ -export const toJsonSchemaDocument: ( - document: Document, - options?: Schema.ToJsonSchemaOptions -) => JsonSchema.Document<"draft-2020-12"> = InternalRepresentation.toJsonSchemaDocument - -/** - * Converts a {@link MultiDocument} to a Draft 2020-12 JSON Schema - * multi-document. - * - * **When to use** - * - * Use when you need to export related schema representation documents together - * so shared definitions stay in multi-document JSON Schema form. - * - * **Gotchas** - * - * JSON Schema generation is best-effort. Some Effect schema representation - * semantics cannot be represented exactly in JSON Schema, and importing an - * emitted JSON Schema may produce equivalent approximations rather than the - * original representation shapes. - * - * @see {@link MultiDocument} - * @see {@link toJsonSchemaDocument} - * @see {@link fromJsonSchemaMultiDocument} - * - * @category transforming - * @since 4.0.0 - */ -export const toJsonSchemaMultiDocument: ( - document: MultiDocument, - options?: Schema.ToJsonSchemaOptions -) => JsonSchema.MultiDocument<"draft-2020-12"> = InternalRepresentation.toJsonSchemaMultiDocument - -/** - * A pair of TypeScript source strings for a schema: `runtime` is the - * executable Schema expression, `Type` is the corresponding TypeScript type. - * - * @see {@link makeCode} - * @see {@link CodeDocument} - * - * @category Code Generation - * @since 4.0.0 - */ -export type Code = { - readonly runtime: string - readonly Type: string -} - -/** - * Constructs a {@link Code} value from a runtime expression string and a - * TypeScript type string. - * - * @see {@link Code} - * - * @category Code Generation - * @since 4.0.0 - */ -export function makeCode(runtime: string, Type: string): Code { - return { runtime, Type } -} - -/** - * An auxiliary code artifact produced during code generation — a symbol - * declaration, an enum declaration, or an import statement. - * - * @see {@link CodeDocument} - * @see {@link toCodeDocument} - * - * @category Code Generation - * @since 4.0.0 - */ -export type Artifact = - | { - readonly _tag: "Symbol" - readonly identifier: string - readonly generation: Code - } - | { - readonly _tag: "Enum" - readonly identifier: string - readonly generation: Code - } - | { - readonly _tag: "Import" - readonly importDeclaration: string - } - -/** - * The output of {@link toCodeDocument}: generated TypeScript code for one or - * more schemas plus their shared references and auxiliary artifacts. - * - * **Details** - * - * `codes` contains one {@link Code} per input representation. - * `references.nonRecursives` contains topologically sorted non-recursive - * definitions. `references.recursives` contains definitions involved in cycles. - * `artifacts` contains symbols, enums, and import statements needed by the - * code. - * - * @see {@link toCodeDocument} - * @see {@link Code} - * @see {@link Artifact} - * - * @category Code Generation - * @since 4.0.0 - */ -export type CodeDocument = { - readonly codes: ReadonlyArray - readonly references: { - readonly nonRecursives: ReadonlyArray<{ - readonly $ref: string - readonly code: Code - }> - readonly recursives: { - readonly [$ref: string]: Code - } - } - readonly artifacts: ReadonlyArray -} - -/** - * Generates TypeScript code strings from a {@link MultiDocument}. - * - * **When to use** - * - * Use when you need to produce source code for Effect Schema definitions from a - * schema representation `MultiDocument`. - * - * **Details** - * - * `options.reviver` can customize code generation for {@link Declaration} - * nodes. Return `undefined` to fall back to the default logic, which uses - * `generation` annotations or the encoded schema. References are - * topologically sorted so non-recursive definitions are emitted before their - * dependents. `$ref` keys are converted to sanitized JavaScript identifiers. - * - * **Example** (Generating TypeScript code) - * - * ```ts - * import { Schema, SchemaRepresentation } from "effect" - * - * const Person = Schema.Struct({ - * name: Schema.String, - * age: Schema.Int - * }) - * - * const multi = SchemaRepresentation.toMultiDocument( - * SchemaRepresentation.fromAST(Person.ast) - * ) - * const codeDoc = SchemaRepresentation.toCodeDocument(multi) - * console.log(codeDoc.codes[0].runtime) - * // Schema.Struct({ ... }) - * ``` - * - * @see {@link CodeDocument} - * @see {@link MultiDocument} - * @see {@link Reviver} - * - * @category Code Generation - * @since 4.0.0 - */ -export function toCodeDocument(multiDocument: MultiDocument, options?: { - /** - * The reviver can return `undefined` to indicate that the generation should be generated by the default logic - */ - readonly reviver?: Reviver | undefined -}): CodeDocument { - const artifacts: Array = [] - - const ts = topologicalSort(multiDocument.references) - - // Phase 1: Build sanitization map with collision handling - const sanitizedReferenceMap = new Map() - const uniqueSanitizedReferences = new Set() - const referenceCount = new Map() - - // Process all references first to build the map - const allRefs = [ - ...ts.nonRecursives.map(({ $ref }) => $ref), - ...Object.keys(ts.recursives) - ] - - for (const ref of allRefs) { - ensureUniqueSanitized(ref) - } - - // Phase 2: Use the map when processing references - const nonRecursives = ts.nonRecursives.map(({ $ref, representation }) => ({ - $ref: sanitizedReferenceMap.get($ref)!, - code: recur(representation) - })) - const recursives = Rec.mapEntries(ts.recursives, (representation, $ref) => [ - sanitizedReferenceMap.get($ref)!, - recur(representation) - ]) - - const codes = multiDocument.representations.map(recur) - - return { - codes, - references: { - nonRecursives: nonRecursives.filter(({ $ref }) => (referenceCount.get($ref) ?? 0) > 0), - recursives: Rec.filter(recursives, (_, $ref) => (referenceCount.get($ref) ?? 0) > 0) - }, - artifacts - } - - function ensureUniqueSanitized(originalRef: string): string { - // Check if already mapped (consistency) - const sanitized = sanitizedReferenceMap.get(originalRef) - if (sanitized !== undefined) { - return sanitized - } - - // Find unique sanitized name - const seed = sanitizeJavaScriptIdentifier(originalRef) - let candidate = seed - let suffix = 0 - - while (uniqueSanitizedReferences.has(candidate)) { - candidate = `${seed}${++suffix}` - } - - uniqueSanitizedReferences.add(candidate) - sanitizedReferenceMap.set(originalRef, candidate) - return candidate - } - - function addSymbol(s: symbol): string { - const identifier = ensureUniqueSanitized("_symbol") - const key = globalThis.Symbol.keyFor(s) - const description = s.description - const generation = key === undefined - ? makeCode(`Symbol(${description === undefined ? "" : format(description)})`, `typeof ${identifier}`) - : makeCode(`Symbol.for(${format(key)})`, `typeof ${identifier}`) - artifacts.push({ _tag: "Symbol", identifier, generation }) - return identifier - } - - function addEnum(s: Enum): string { - const identifier = ensureUniqueSanitized("_Enum") - artifacts.push({ - _tag: "Enum", - identifier, - generation: makeCode( - `enum ${identifier} { ${s.enums.map(([name, value]) => `${format(name)}: ${format(value)}`).join(", ")} }`, - `typeof ${identifier}` - ) - }) - return identifier - } - - function addImport(importDeclaration: string) { - if (!artifacts.some((a) => a._tag === "Import" && a.importDeclaration === importDeclaration)) { - artifacts.push({ _tag: "Import", importDeclaration }) - } - } - - function recur(s: Representation): Code { - const g = on(s) - switch (s._tag) { - default: - return makeCode( - g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations), - g.Type + toTypeBrand(s.annotations) - ) - case "Reference": - return g - case "Declaration": - case "String": - case "Number": - case "BigInt": - case "Arrays": - case "Objects": - case "Suspend": - return makeCode( - g.runtime + toRuntimeAnnotate(s.annotations) + toRuntimeBrand(s.annotations) + toRuntimeChecks(s.checks), - g.Type + toTypeBrand(s.annotations) + toTypeChecks(s.checks) - ) - } - } - - function on(s: Representation): Code { - switch (s._tag) { - case "Declaration": { - // if there is a reviver, use it to generate the generation - if (options?.reviver !== undefined) { - // the reviver can return `undefined` to indicate that the generation should be generated by the default logic - const out = options.reviver(s, recur) - if (out !== undefined) { - return out - } - } - // otherwise, use the generation from the annotations - const generation = s.annotations?.generation - if ( - Predicate.isObject(generation) && typeof generation.runtime === "string" && - typeof generation.Type === "string" - ) { - const typeParameters = s.typeParameters.map(recur) - if (typeof generation.importDeclaration === "string") { - addImport(generation.importDeclaration) - } - return makeCode( - replacePlaceholders(generation.runtime, typeParameters.map((p) => p.runtime)), - replacePlaceholders(generation.Type, typeParameters.map((p) => p.Type)) - ) - } - // otherwise, use the generation from the encoded schema - return recur(s.encodedSchema) - } - case "Reference": { - const sanitized = ensureUniqueSanitized(s.$ref) - referenceCount.set(sanitized, (referenceCount.get(sanitized) ?? 0) + 1) - return makeCode(sanitized, sanitized) - } - case "Suspend": { - const thunk = recur(s.thunk) - return makeCode( - `Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`, - thunk.Type - ) - } - case "Null": - return makeCode(`Schema.Null`, "null") - case "Undefined": - return makeCode(`Schema.Undefined`, "undefined") - case "Void": - return makeCode(`Schema.Void`, "void") - case "Never": - return makeCode(`Schema.Never`, "never") - case "Unknown": - return makeCode(`Schema.Unknown`, "unknown") - case "Any": - return makeCode(`Schema.Any`, "any") - case "Number": - return makeCode(`Schema.Number`, "number") - case "Boolean": - return makeCode(`Schema.Boolean`, "boolean") - case "BigInt": - return makeCode(`Schema.BigInt`, "bigint") - case "Symbol": - return makeCode(`Schema.Symbol`, "symbol") - case "String": { - const contentMediaType = s.contentMediaType - const contentSchema = s.contentSchema - if (contentMediaType === "application/json" && contentSchema !== undefined) { - return makeCode(`Schema.fromJsonString(${recur(contentSchema)})`, "string") - } else { - return makeCode(`Schema.String`, "string") - } - } - case "Literal": { - const literal = format(s.literal) - return makeCode(`Schema.Literal(${literal})`, literal) - } - case "UniqueSymbol": { - const identifier = addSymbol(s.symbol) - return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`) - } - case "ObjectKeyword": - return makeCode(`Schema.ObjectKeyword`, "object") - case "Enum": { - const identifier = addEnum(s) - return makeCode(`Schema.Enum(${identifier})`, `typeof ${identifier}`) - } - case "TemplateLiteral": { - const parts = s.parts.map(recur) - const type = toTypeParts(s.parts).map((p) => "`" + p + "`").join(" | ") - return makeCode(`Schema.TemplateLiteral([${parts.map((p) => p.runtime).join(", ")}])`, type) - } - case "Arrays": { - const elements = s.elements.map((e) => { - return { - isOptional: e.isOptional, - type: recur(e.type), - annotations: e.annotations - } - }) - - const rest = s.rest.map(recur) - - if (Arr.isArrayNonEmpty(rest)) { - const item = rest[0] - if (elements.length === 0 && rest.length === 1) { - return makeCode( - `Schema.Array(${item.runtime})`, - `ReadonlyArray<${item.Type}>` - ) - } - const post = rest.slice(1) - return makeCode( - `Schema.TupleWithRest(Schema.Tuple([${ - elements.map((e) => - toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations) - ).join(", ") - }]), [${rest.map((r) => r.runtime).join(", ")}])`, - `readonly [${ - elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ") - }, ...Array<${item.Type}>${post.length > 0 ? `, ${post.map((p) => p.Type).join(", ")}` : ""}]` - ) - } - return makeCode( - `Schema.Tuple([${ - elements.map((e) => toRuntimeIsOptional(e.isOptional, e.type.runtime) + toRuntimeAnnotateKey(e.annotations)) - .join(", ") - }])`, - `readonly [${elements.map((e) => toTypeIsOptional(e.isOptional, e.type.Type)).join(", ")}]` - ) - } - case "Objects": { - const pss = s.propertySignatures.map((p) => { - const isSymbol = typeof p.name === "symbol" - const name = isSymbol ? addSymbol(p.name) : formatPropertyKey(p.name) - const nameType = toTypeIsOptional( - p.isOptional, - toTypeIsMutable(p.isMutable, isSymbol ? `[typeof ${name}]` : name) - ) - const type = recur(p.type) - return makeCode( - `${isSymbol ? `[${name}]` : name}: ${ - toRuntimeIsOptional(p.isOptional, toRuntimeIsMutable(p.isMutable, type.runtime)) - }` + - toRuntimeAnnotateKey(p.annotations), - `${nameType}: ${type.Type}` - ) - }) - - const iss = s.indexSignatures.map((is) => { - return { - parameter: recur(is.parameter), - type: recur(is.type) - } - }) - - if (iss.length === 0) { - // 1) Only properties -> Struct - return makeCode( - `Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} })`, - `{ ${pss.map((p) => p.Type).join(", ")} }` - ) - } else if (pss.length === 0 && iss.length === 1) { - // 2) Only one index signature and no properties -> Record - return makeCode( - `Schema.Record(${iss[0].parameter.runtime}, ${iss[0].type.runtime})`, - `{ readonly [x: ${iss[0].parameter.Type}]: ${iss[0].type.Type} }` - ) - } else { - // 3) Properties + index signatures -> StructWithRest - return makeCode( - `Schema.StructWithRest(Schema.Struct({ ${pss.map((p) => p.runtime).join(", ")} }), [${ - iss.map((is) => `Schema.Record(${is.parameter.runtime}, ${is.type.runtime})`).join(", ") - }])`, - `{ ${pss.map((p) => p.Type).join(", ")}, ${ - iss.map((is) => `readonly [x: ${is.parameter.Type}]: ${is.type.Type}`).join(", ") - } }` - ) - } - } - case "Union": { - if (s.types.length === 0) { - return makeCode("Schema.Never", "never") - } - if (s.types.every((t) => t._tag === "Literal")) { - const literals = s.types.map((l) => format(l.literal)) - if (literals.length === 1) { - return makeCode(`Schema.Literal(${literals[0]})`, literals[0]) - } - return makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | ")) - } - const mode = s.mode === "anyOf" ? "" : `, { mode: "oneOf" }` - const types = s.types.map((t) => recur(t)) - return makeCode( - `Schema.Union([${types.map((t) => t.runtime).join(", ")}]${mode})`, - types.map((t) => t.Type).join(" | ") - ) - } - } - } - - function toTypeBrand(annotations: Schema.Annotations.Annotations | undefined): string { - const brands = collectBrands(annotations) - if (brands.length === 0) return "" - addImport(`import type * as Brand from "effect/Brand"`) - return brands.map((b) => ` & Brand.Brand<${format(b)}>`).join("") - } - - function toTypeChecks(checks: ReadonlyArray>): string { - return checks.map((c) => toTypeCheck(c)).join("") - } - - function toTypeCheck(check: Check): string { - switch (check._tag) { - case "Filter": - return toTypeBrand(check.annotations) - case "FilterGroup": { - return toTypeChecks(check.checks) - } - } - } - - function toRuntimeChecks(checks: ReadonlyArray>): string { - return checks.map((c) => `.check(${toRuntimeCheck(c)})` + toRuntimeBrand(c.annotations)).join("") - } - - function toRuntimeCheck(check: Check): string { - switch (check._tag) { - case "Filter": - return toRuntimeFilter(check) - case "FilterGroup": { - const a = toRuntimeAnnotations(check.annotations) - const ca = a === "" ? "" : `, ${a}` - return `Schema.makeFilterGroup([${check.checks.map((c) => toRuntimeCheck(c)).join(", ")}]${ca})` - } - } - } - - function toRuntimeFilter(filter: Filter): string { - const a = toRuntimeAnnotations(filter.annotations) - const ca = a === "" ? "" : `, ${a}` - switch (filter.meta._tag) { - case "isTrimmed": - case "isGUID": - case "isULID": - case "isBase64": - case "isBase64Url": - case "isUppercased": - case "isLowercased": - case "isCapitalized": - case "isUncapitalized": - case "isFinite": - case "isInt": - case "isUnique": - case "isDateValid": - return `Schema.${filter.meta._tag}(${a})` - - case "isStringFinite": - case "isStringBigInt": - case "isStringSymbol": - case "isPattern": - return `Schema.${filter.meta._tag}(${toRuntimeRegExp(filter.meta.regExp)}${ca})` - - case "isMinLength": - return `Schema.isMinLength(${filter.meta.minLength}${ca})` - case "isMaxLength": - return `Schema.isMaxLength(${filter.meta.maxLength}${ca})` - case "isLengthBetween": - return `Schema.isLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` - case "isUUID": - return `Schema.isUUID(${filter.meta.version}${ca})` - case "isStartsWith": - return `Schema.isStartsWith(${format(filter.meta.startsWith)}${ca})` - case "isEndsWith": - return `Schema.isEndsWith(${format(filter.meta.endsWith)}${ca})` - case "isIncludes": - return `Schema.isIncludes(${format(filter.meta.includes)}${ca})` - - case "isGreaterThan": - case "isGreaterThanBigInt": - case "isGreaterThanDate": - return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMinimum)}${ca})` - case "isGreaterThanOrEqualTo": - case "isGreaterThanOrEqualToBigInt": - case "isGreaterThanOrEqualToDate": - return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.minimum)}${ca})` - case "isLessThan": - case "isLessThanBigInt": - case "isLessThanDate": - return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.exclusiveMaximum)}${ca})` - case "isLessThanOrEqualTo": - case "isLessThanOrEqualToBigInt": - case "isLessThanOrEqualToDate": - return `Schema.${filter.meta._tag}(${toRuntimeValue(filter.meta.maximum)}${ca})` - case "isBetween": - case "isBetweenBigInt": - case "isBetweenDate": - return `Schema.${filter.meta._tag}({ minimum: ${toRuntimeValue(filter.meta.minimum)}, maximum: ${ - toRuntimeValue(filter.meta.maximum) - }, exclusiveMinimum: ${toRuntimeValue(filter.meta.exclusiveMinimum)}, exclusiveMaximum: ${ - toRuntimeValue(filter.meta.exclusiveMaximum) - }${ca})` - - case "isMultipleOf": - return `Schema.isMultipleOf(${filter.meta.divisor}${ca})` - - case "isMinProperties": - return `Schema.isMinProperties(${filter.meta.minProperties}${ca})` - case "isMaxProperties": - return `Schema.isMaxProperties(${filter.meta.maxProperties}${ca})` - case "isPropertiesLengthBetween": - return `Schema.isPropertiesLengthBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` - case "isPropertyNames": - return `Schema.isPropertyNames(${recur(filter.meta.propertyNames).runtime}${ca})` - - case "isMinSize": - return `Schema.isMinSize(${filter.meta.minSize}${ca})` - case "isMaxSize": - return `Schema.isMaxSize(${filter.meta.maxSize}${ca})` - case "isSizeBetween": - return `Schema.isSizeBetween(${filter.meta.minimum}, ${filter.meta.maximum}${ca})` - } - } -} - -const VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP = /^[A-Z_$][A-Za-z0-9_$]*$/ - -/** - * Converts an arbitrary string into a valid (ASCII) JavaScript identifier - * starting with an uppercase letter, `$`, or `_`. - * - * - Replaces invalid identifier characters with `_` - * - Uppercases a leading ASCII letter - * - If the first character is a digit, prefixes `_` - * - Empty input becomes `_` - * - * @internal - */ -export function sanitizeJavaScriptIdentifier(s: string): string { - if (s.length === 0) return "_" - if (VALID_ASCII_UPPER_JAVASCRIPT_IDENTIFIER_REGEXP.test(s)) return s - - const out: Array = [] - let needsPrefix = false - let i = 0 - - for (const ch of s) { - if (i === 0) { - if (ch === "_" || ch === "$" || (ch >= "A" && ch <= "Z")) { - out.push(ch) - } else if (ch >= "a" && ch <= "z") { - out.push(ch.toUpperCase()) - } else if (ch >= "0" && ch <= "9") { - out.push(ch) - needsPrefix = true - } else { - out.push("_") - } - } else { - out.push(isAsciiIdPart(ch) ? ch : "_") - } - i++ - } - - return needsPrefix ? "_" + out.join("") : out.join("") -} - -function isAsciiIdStart(ch: string): boolean { - return ( - ch === "_" || - ch === "$" || - (ch >= "A" && ch <= "Z") || - (ch >= "a" && ch <= "z") - ) -} - -function isAsciiIdPart(ch: string): boolean { - return isAsciiIdStart(ch) || (ch >= "0" && ch <= "9") -} - -function replacePlaceholders(template: string, items: ReadonlyArray) { - let i = 0 - return template.replace(/\?/g, () => items[i++]) -} - -function toTypeParts(parts: ReadonlyArray): ReadonlyArray { - if (parts.length === 0) { - return [""] - } - const [first, ...rest] = parts - const restPatterns = toTypeParts(rest) - return toTypePart(first).flatMap((f) => restPatterns.map((r) => f + r)) -} - -function toTypePart(r: Representation): ReadonlyArray { - switch (r._tag) { - case "Literal": - return [globalThis.String(r.literal)] - case "String": - return ["${string}"] - case "Number": - return ["${number}"] - case "BigInt": - return ["${bigint}"] - case "TemplateLiteral": - return toTypeParts(r.parts) - case "Union": - return r.types.flatMap(toTypePart) - default: - return [] - } -} - -const toCodeAnnotationsBlacklist: Set = new Set([ - ...toJsonAnnotationsBlacklist, - "typeConstructor", - "generation", - "brands" -]) - -function toRuntimeAnnotations(annotations: Schema.Annotations.Annotations | undefined): string { - if (!annotations) return "" - const entries: Array = [] - for (const [key, value] of Object.entries(annotations)) { - if (toCodeAnnotationsBlacklist.has(key)) continue - entries.push(`${formatPropertyKey(key)}: ${format(value)}`) - } - if (entries.length === 0) return "" - return `{ ${entries.join(", ")} }` -} - -function toRuntimeBrand(annotations: Schema.Annotations.Annotations | undefined): string { - const brands = collectBrands(annotations) - return brands.length > 0 ? `.pipe(${brands.map((b) => `Schema.brand(${format(b)})`).join(", ")})` : "" -} - -function toRuntimeAnnotate(annotations: Schema.Annotations.Annotations | undefined): string { - const s = toRuntimeAnnotations(annotations) - return s === "" ? "" : `.annotate(${s})` -} - -function toRuntimeAnnotateKey(annotations: Schema.Annotations.Annotations | undefined): string { - const s = toRuntimeAnnotations(annotations) - return s === "" ? "" : `.annotateKey(${s})` -} - -function toRuntimeIsOptional(isOptional: boolean, runtime: string): string { - return isOptional ? `Schema.optionalKey(${runtime})` : runtime -} - -function toTypeIsOptional(isOptional: boolean, type: string): string { - return isOptional ? `${type}?` : type -} - -function toRuntimeIsMutable(isMutable: boolean, runtime: string): string { - return isMutable ? `Schema.mutableKey(${runtime})` : runtime -} - -function toTypeIsMutable(isMutable: boolean, type: string): string { - return isMutable ? type : `readonly ${type}` -} - -function toRuntimeValue(value: undefined | number | boolean | bigint | Date): string { - if (value instanceof Date) { - return `new Date(${value.getTime()})` - } - return format(value) -} - -function toRuntimeRegExp(regExp: RegExp): string { - const args = [format(regExp.source)] - const flags = regExp.flags.trim() - if (flags !== "") { - args.push(format(flags)) - } - return `new RegExp(${args.join(", ")})` -} - -/** - * Parses a Draft 2020-12 JSON Schema document into a {@link Document}. - * - * **When to use** - * - * Use when you need to import a Draft 2020-12 JSON Schema document into the - * Effect schema representation system. - * - * **Details** - * - * `options.onEnter` is an optional hook called on each JSON Schema node before - * processing, allowing pre-transformation. - * - * **Gotchas** - * - * JSON Schema import is best-effort. Some JSON Schema constructs do not map - * exactly to Effect schema representations, and importing a schema previously - * emitted by `toJsonSchemaDocument` may produce an equivalent approximation - * rather than the original representation shape. - * - * This throws if a `$ref` cannot be resolved within the document's definitions. - * Circular `$ref`s are detected and cause an error. - * - * @see {@link Document} - * @see {@link toJsonSchemaDocument} - * @see {@link fromJsonSchemaMultiDocument} - * - * @category constructors - * @since 4.0.0 - */ -export function fromJsonSchemaDocument(document: JsonSchema.Document<"draft-2020-12">, options?: { - readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined -}): Document { - const { references, representations: schemas } = fromJsonSchemaMultiDocument({ - dialect: document.dialect, - schemas: [document.schema], - definitions: document.definitions - }, options) - return { - representation: schemas[0], - references - } -} - -/** - * Parses a Draft 2020-12 JSON Schema multi-document into a - * {@link MultiDocument}. - * - * **When to use** - * - * Use when you need to import a Draft 2020-12 JSON Schema multi-document whose - * schemas share definitions. - * - * **Details** - * - * `options.onEnter` is an optional hook called on each JSON Schema node before - * processing. - * - * **Gotchas** - * - * JSON Schema import is best-effort. Some JSON Schema constructs do not map - * exactly to Effect schema representations, and importing schemas previously - * emitted by `toJsonSchemaMultiDocument` may produce equivalent approximations - * rather than the original representation shapes. - * - * This throws if a `$ref` cannot be resolved. - * - * @see {@link MultiDocument} - * @see {@link toJsonSchemaMultiDocument} - * @see {@link fromJsonSchemaDocument} - * - * @category constructors - * @since 4.0.0 - */ -export function fromJsonSchemaMultiDocument(document: JsonSchema.MultiDocument<"draft-2020-12">, options?: { - readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined -}): MultiDocument { - let definitionIdentifier: string | undefined - const references: Record = {} - - type ResolvedReference = Exclude - const resolvedReferences = new Map() - - function resolveReference($ref: string): ResolvedReference { - const definition = document.definitions[$ref] - if (definition === undefined) { - throw new Error(`Reference ${$ref} not found`) - } - - const resolved = resolvedReferences.get($ref) - if (resolved === null) { - throw new Error(`Circular reference detected: ${$ref}`) - } - if (resolved !== undefined) return resolved - - resolvedReferences.set($ref, null) - const value = recur(definition) - const out = value._tag === "Reference" ? resolveReference(value.$ref) : value - resolvedReferences.set($ref, out) - return out - } - - for (const [identifier, definition] of Object.entries(document.definitions)) { - definitionIdentifier = identifier - references[identifier] = unknownToJson(recur(definition)) - } - - definitionIdentifier = undefined - const representations = Arr.map(document.schemas, (schema) => unknownToJson(recur(schema))) - return { - representations, - references - } - - function recur(u: unknown): Representation { - if (u === false) return never - if (!Predicate.isObject(u)) return unknown - - let js: JsonSchema.JsonSchema = options?.onEnter?.(u) ?? u - if (Array.isArray(js.type)) { - if (js.type.every(isType)) { - const { type, ...rest } = js - js = { - anyOf: type.map((type) => ({ type })), - ...rest - } - } else { - js = {} - } - } - - let out = on(js) - - const annotations = collectAnnotations(js) - if (annotations !== undefined) { - out = combine(out, { _tag: "Unknown", annotations }) - } - - if (Array.isArray(js.allOf)) { - out = js.allOf.reduce((acc, curr) => combine(acc, recur(curr)), out) - } - if (Array.isArray(js.anyOf)) { - out = combine({ _tag: "Union", types: js.anyOf.map((type) => recur(type)), mode: "anyOf" }, out) - } - if (Array.isArray(js.oneOf)) { - out = combine({ _tag: "Union", types: js.oneOf.map((type) => recur(type)), mode: "oneOf" }, out) - } - - return out - } - - function on(js: JsonSchema.JsonSchema): Representation { - if (typeof js.$ref === "string") { - const $ref = js.$ref.slice(2).split("/").at(-1) - if ($ref !== undefined) { - const reference: Reference = { _tag: "Reference", $ref: unescapeToken($ref) } - if (definitionIdentifier === $ref) { - return { _tag: "Suspend", thunk: reference, checks: [] } - } else { - return reference - } - } - } else if ("const" in js) { - if (isLiteralValue(js.const)) { - return { _tag: "Literal", literal: js.const } - } else if (js.const === null) { - return null_ - } - } else if (Array.isArray(js.enum)) { - const types: Array = [] - for (const e of js.enum) { - if (isLiteralValue(e)) { - types.push({ _tag: "Literal", literal: e }) - } else if (e === null) { - types.push(null_) - } else { - types.push(recur(e)) - } - } - if (types.length === 1) { - return types[0] - } else { - return { _tag: "Union", types, mode: "anyOf" } - } - } - - const type = isType(js.type) ? js.type : getType(js) - if (type !== undefined) { - switch (type) { - case "null": - return null_ - case "string": { - const checks = collectStringChecks(js) - if (checks.length > 0) { - return { ...string, checks } - } - return string - } - case "number": - return { - _tag: "Number", - checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }, ...collectNumberChecks(js)] - } - case "integer": - return { - _tag: "Number", - checks: [{ _tag: "Filter", meta: { _tag: "isInt" } }, ...collectNumberChecks(js)] - } - case "boolean": - return boolean - case "array": { - const minItems = typeof js.minItems === "number" ? js.minItems : 0 - - const elements: Array = (Array.isArray(js.prefixItems) ? js.prefixItems : []).map((e, i) => ({ - isOptional: i + 1 > minItems, - type: recur(e) - })) - - const rest: Array = js.items !== undefined ? - [recur(js.items)] - : js.prefixItems !== undefined && typeof js.maxItems === "number" - ? [] - : [unknown] - - return { _tag: "Arrays", elements, rest, checks: collectArraysChecks(js) } - } - case "object": { - return { - _tag: "Objects", - propertySignatures: collectProperties(js), - indexSignatures: collectIndexSignatures(js), - checks: collectObjectsChecks(js) - } - } - } - } - - return { _tag: "Unknown" } - } - - function collectObjectsChecks(js: JsonSchema.JsonSchema): Array> { - const checks: Array> = [] - if (typeof js.minProperties === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMinProperties", minProperties: js.minProperties } }) - } - if (typeof js.maxProperties === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMaxProperties", maxProperties: js.maxProperties } }) - } - if (js.propertyNames !== undefined) { - const propertyNames = recur(js.propertyNames) - checks.push({ _tag: "Filter", meta: { _tag: "isPropertyNames", propertyNames } }) - } - return checks - } - - function combine(a: Representation, b: Representation): Representation { - switch (a._tag) { - default: - return never - case "Reference": - return combine(resolveReference(a.$ref), b) - case "Never": - return a - case "Unknown": { - const resolved = b._tag === "Reference" ? resolveReference(b.$ref) : b - return { ...resolved, ...combineAnnotations(a.annotations, resolved.annotations) } - } - case "Null": - case "String": - case "Number": - case "Boolean": - case "Literal": - case "Arrays": - case "Objects": - case "Union": - break - } - - if (b._tag === "Reference") { - return combine(a, resolveReference(b.$ref)) - } - if (b._tag === "Unknown") { - return { ...a, ...combineAnnotations(a.annotations, b.annotations) } - } - if (a._tag === "Union") { - const types = a.types.map((s) => combine(s, b)).filter((s) => s !== never) - if (types.length === 0) return never - return { - _tag: "Union", - types, - mode: a.mode, - ...makeAnnotations(a.annotations) - } - } - if (b._tag === "Union") { - return combine(b, a) - } - - switch (a._tag) { - case "Null": - return b._tag === "Null" ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never - case "String": { - if (b._tag === "Literal") { - return satisfiesLiteral(a, b) ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never - } - if (b._tag !== "String") return never - const checks = combineChecks(a.checks, b.checks, b.annotations) - return { - _tag: "String", - checks: checks ?? a.checks, - ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) - } - } - case "Number": { - if (b._tag === "Literal") { - return satisfiesLiteral(a, b) ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } : never - } - if (b._tag !== "Number") return never - const checks = combineNumberChecks(a.checks, b.checks, b.annotations) - return { - _tag: "Number", - checks: checks ?? a.checks, - ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) - } - } - case "Boolean": - if (b._tag === "Boolean") { - return { _tag: "Boolean", ...combineAnnotations(a.annotations, b.annotations) } - } - return b._tag === "Literal" && typeof b.literal === "boolean" - ? { ...b, ...combineAnnotations(a.annotations, b.annotations) } - : never - case "Literal": - switch (b._tag) { - case "Literal": - return a.literal === b.literal - ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } - : never - case "String": - case "Number": - return satisfiesLiteral(b, a) ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } : never - case "Boolean": - return typeof a.literal === "boolean" - ? { ...a, ...combineAnnotations(a.annotations, b.annotations) } - : never - default: - return never - } - case "Arrays": { - if (b._tag !== "Arrays") return never - const arrays = combineArrays(a, b) - if (arrays === undefined) return never - const checks = combineArraysChecks(a.checks, b.checks, b.annotations) - return { - _tag: "Arrays", - elements: arrays.elements, - rest: arrays.rest, - checks: checks ?? a.checks, - ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) - } - } - case "Objects": { - if (b._tag !== "Objects") return never - const checks = combineChecks(a.checks, b.checks, b.annotations) - return { - _tag: "Objects", - propertySignatures: combinePropertySignatures(a.propertySignatures, b.propertySignatures), - indexSignatures: combineIndexSignatures(a.indexSignatures, b.indexSignatures), - checks: checks ?? a.checks, - ...combineAnnotations(a.annotations, checks ? undefined : b.annotations) - } - } - default: - return never - } - } - - function satisfiesPrimitiveCheck(check: Check, value: unknown): boolean { - if (check._tag === "FilterGroup") { - return check.checks.every((check) => satisfiesPrimitiveCheck(check, value)) - } - const meta = check.meta - switch (meta._tag) { - case "isMinLength": - return typeof value === "string" && value.length >= meta.minLength - case "isMaxLength": - return typeof value === "string" && value.length <= meta.maxLength - case "isPattern": - return typeof value === "string" && meta.regExp.test(value) - case "isFinite": - return typeof value === "number" && globalThis.Number.isFinite(value) - case "isInt": - return typeof value === "number" && globalThis.Number.isSafeInteger(value) - case "isMultipleOf": - return typeof value === "number" && remainder(value, meta.divisor) === 0 - case "isGreaterThan": - return typeof value === "number" && value > meta.exclusiveMinimum - case "isGreaterThanOrEqualTo": - return typeof value === "number" && value >= meta.minimum - case "isLessThan": - return typeof value === "number" && value < meta.exclusiveMaximum - case "isLessThanOrEqualTo": - return typeof value === "number" && value <= meta.maximum - default: - return false - } - } - - function satisfiesLiteral(type: String | Number, literal: Literal): boolean { - const value = literal.literal - if (type._tag === "String" ? typeof value !== "string" : typeof value !== "number") { - return false - } - return type.checks.every((check) => satisfiesPrimitiveCheck(check, value)) - } - - function collectProperties(js: JsonSchema.JsonSchema): Array { - const properties: Record = Predicate.isObject(js.properties) ? js.properties : {} - const required = Array.isArray(js.required) ? js.required : [] - required.forEach((key) => { - if (!Object.hasOwn(properties, key)) { - properties[key] = {} - } - }) - return Object.entries(properties).map(([key, v]) => ({ - name: key, - type: recur(v), - isOptional: !required.includes(key), - isMutable: false - })) - } - - function collectIndexSignatures(js: JsonSchema.JsonSchema): Array { - const out: Array = [] - - if (Predicate.isObject(js.patternProperties)) { - for (const [pattern, value] of Object.entries(js.patternProperties)) { - out.push({ parameter: recur({ pattern }), type: recur(value) }) - } - } - - if (js.additionalProperties === undefined || js.additionalProperties === true) { - out.push({ parameter: string, type: unknown }) - } else if (Predicate.isObject(js.additionalProperties)) { - out.push({ parameter: string, type: recur(js.additionalProperties) }) - } - - return out - } - - function combineArrays(a: Arrays, b: Arrays): Pick | undefined { - const elements: Array = [] - const len = Math.max(a.elements.length, b.elements.length) - for (let i = 0; i < len; i++) { - const ae = a.elements[i] - const be = b.elements[i] - const isOptional = ae?.isOptional !== false && be?.isOptional !== false - const at = ae?.type ?? a.rest[0] - const bt = be?.type ?? b.rest[0] - if (at === undefined || bt === undefined) { - return isOptional ? { elements, rest: [] } : undefined - } - const type = combine(at, bt) - if (type === never) { - return isOptional ? { elements, rest: [] } : undefined - } - elements.push({ isOptional, type }) - } - - const ar = a.rest[0] - const br = b.rest[0] - if (ar === undefined || br === undefined) { - return { elements, rest: [] } - } - const rest = combine(ar, br) - return { elements, rest: rest === never ? [] : [rest] } - } - - function combinePropertySignatures( - a: ReadonlyArray, - b: ReadonlyArray - ): Array { - const propertySignatures: Array = [] - const thatPropertiesMap: Record = {} - for (const p of b) { - thatPropertiesMap[p.name] = p - } - const keys = new Set() - for (const p of a) { - keys.add(p.name) - const thatp = thatPropertiesMap[p.name] - if (thatp) { - propertySignatures.push( - { - name: p.name, - type: combine(p.type, thatp.type), - isOptional: p.isOptional && thatp.isOptional, - isMutable: p.isMutable - } - ) - } else { - propertySignatures.push(p) - } - } - for (const p of b) { - if (!keys.has(p.name)) propertySignatures.push(p) - } - return propertySignatures - } - - function combineIndexSignatures( - a: ReadonlyArray, - b: ReadonlyArray - ): Array { - if (a.length === 0 || b.length === 0) return [] - const out: Array = [...a] - for (const is of b) { - if (is.parameter === string) { - const i = a.findIndex((is) => is.parameter === string) - if (i !== -1) { - out[i] = { parameter: string, type: combine(a[i].type, is.type) } - } else { - out.push(is) - } - } else { - out.push(is) - } - } - return out - } - - function unknownToJson(representation: Representation): Representation { - switch (representation._tag) { - case "Unknown": - return representation.annotations === undefined ? - json : - { - ...json, - annotations: { - ...json.annotations, - ...representation.annotations - } - } - case "Suspend": { - const thunk = unknownToJson(representation.thunk) - return thunk === representation.thunk ? representation : { ...representation, thunk } - } - case "String": { - if (representation.contentSchema === undefined) return representation - const contentSchema = unknownToJson(representation.contentSchema) - return contentSchema === representation.contentSchema ? representation : { ...representation, contentSchema } - } - case "Arrays": { - const elements = SchemaAST.mapOrSame(representation.elements, (element) => { - const type = unknownToJson(element.type) - return type === element.type ? element : { ...element, type } - }) - const rest = SchemaAST.mapOrSame(representation.rest, unknownToJson) - return elements === representation.elements && rest === representation.rest ? - representation : - { ...representation, elements, rest } - } - case "Objects": { - const propertySignatures = SchemaAST.mapOrSame(representation.propertySignatures, (propertySignature) => { - const type = unknownToJson(propertySignature.type) - return type === propertySignature.type ? propertySignature : { ...propertySignature, type } - }) - const indexSignatures = SchemaAST.mapOrSame(representation.indexSignatures, (indexSignature) => { - const type = unknownToJson(indexSignature.type) - return type === indexSignature.type ? indexSignature : { ...indexSignature, type } - }) - return propertySignatures === representation.propertySignatures && - indexSignatures === representation.indexSignatures ? - representation : - { ...representation, propertySignatures, indexSignatures } - } - case "Union": { - const types = SchemaAST.mapOrSame(representation.types, unknownToJson) - return types === representation.types ? representation : { ...representation, types } - } - default: - return representation - } - } -} - -function asChecks( - checks: ReadonlyArray>, - annotations: Schema.Annotations.Annotations | undefined -): ReadonlyArray> | undefined { - if (Arr.isReadonlyArrayNonEmpty(checks)) { - if (annotations !== undefined) { - if (checks.length === 1) { - const check = checks[0] - if (check.annotations === undefined) { - return [{ ...check, annotations }] - } else { - return [{ _tag: "FilterGroup", checks, annotations }] - } - } else { - return [{ _tag: "FilterGroup", checks, annotations }] - } - } - return checks - } -} - -function combineChecks( - a: ReadonlyArray>, - b: ReadonlyArray>, - annotations: Schema.Annotations.Annotations | undefined -): Array> | undefined { - const checks = asChecks(b, annotations) - if (checks) { - return [...a, ...checks] - } -} - -function combineNumberChecks( - a: ReadonlyArray>, - b: ReadonlyArray>, - annotations: Schema.Annotations.Annotations | undefined -): Array> | undefined { - if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isFinite")) { - b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isFinite") - } - if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isInt")) { - b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isInt") - } - return combineChecks(a, b, annotations) -} - -function combineArraysChecks( - a: ReadonlyArray>, - b: ReadonlyArray>, - annotations: Schema.Annotations.Annotations | undefined -): Array> | undefined { - if (a.some((c) => c._tag === "Filter" && c.meta._tag === "isUnique")) { - b = b.filter((c) => c._tag !== "Filter" || c.meta._tag !== "isUnique") - } - return combineChecks(a, b, annotations) -} - -function makeAnnotations( - annotations: Schema.Annotations.Annotations | undefined -): { annotations: Schema.Annotations.Annotations } | undefined { - return annotations ? { annotations } : undefined -} - -function combineAnnotations( - a: Schema.Annotations.Annotations | undefined, - b: Schema.Annotations.Annotations | undefined -): { annotations: Schema.Annotations.Annotations } | undefined { - if (a === undefined) return makeAnnotations(b) - if (b === undefined) return makeAnnotations(a) - return { annotations: { ...a, ...b } } // TODO: better merge -} - -function collectStringChecks(js: JsonSchema.JsonSchema): Array> { - const checks: Array> = [] - if (typeof js.minLength === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minLength } }) - } - if (typeof js.maxLength === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxLength } }) - } - if (typeof js.pattern === "string") { - checks.push({ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp(js.pattern) } }) - } - return checks -} - -function collectNumberChecks(js: JsonSchema.JsonSchema): Array> { - const checks: Array> = [] - if (typeof js.minimum === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: js.minimum } }) - } - if (typeof js.maximum === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: js.maximum } }) - } - if (typeof js.exclusiveMinimum === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isGreaterThan", exclusiveMinimum: js.exclusiveMinimum } }) - } - if (typeof js.exclusiveMaximum === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isLessThan", exclusiveMaximum: js.exclusiveMaximum } }) - } - if (typeof js.multipleOf === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMultipleOf", divisor: js.multipleOf } }) - } - return checks -} - -function collectArraysChecks(js: JsonSchema.JsonSchema): Array> { - const checks: Array> = [] - if (js.prefixItems === undefined) { - if (typeof js.minItems === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMinLength", minLength: js.minItems } }) - } - if (typeof js.maxItems === "number") { - checks.push({ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: js.maxItems } }) - } - } - if (typeof js.uniqueItems === "boolean") { - checks.push({ _tag: "Filter", meta: { _tag: "isUnique" } }) - } - return checks -} - -const unknown: Unknown = { _tag: "Unknown" } -const json: Declaration = { - _tag: "Declaration", - annotations: { - expected: "JSON value", - generation: { - Type: "Schema.Json", - runtime: "Schema.Json" - }, - typeConstructor: { - _tag: "effect/Json" - } - }, - checks: [], - encodedSchema: unknown, - typeParameters: [] -} -const never: Never = { _tag: "Never" } -const null_: Null = { _tag: "Null" } -const string: String = { _tag: "String", checks: [] } -const boolean: Boolean = { _tag: "Boolean" } - -function collectAnnotations( - schema: JsonSchema.JsonSchema -): Schema.Annotations.Annotations | undefined { - const as: Record = {} - - if (typeof schema.title === "string") as.title = schema.title - if (typeof schema.description === "string") as.description = schema.description - if (schema.default !== undefined) as.default = schema.default - if (Array.isArray(schema.examples)) as.examples = schema.examples - if (typeof schema.readOnly === "boolean") as.readOnly = schema.readOnly - if (typeof schema.writeOnly === "boolean") as.writeOnly = schema.writeOnly - if (typeof schema.format === "string") as.format = schema.format - if (typeof schema.contentEncoding === "string") as.contentEncoding = schema.contentEncoding - if (typeof schema.contentMediaType === "string") as.contentMediaType = schema.contentMediaType - - return Rec.isEmptyRecord(as) ? undefined : as -} - -function isLiteralValue(value: unknown): value is SchemaAST.LiteralValue { - return typeof value === "string" || typeof value === "number" || typeof value === "boolean" -} - -const stringKeys = ["minLength", "maxLength", "pattern", "format", "contentMediaType", "contentSchema"] -const numberKeys = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] -const objectKeys = [ - "properties", - "required", - "additionalProperties", - "patternProperties", - "propertyNames", - "minProperties", - "maxProperties" -] -const arrayKeys = ["items", "prefixItems", "additionalItems", "minItems", "maxItems", "uniqueItems"] - -function getType(js: JsonSchema.JsonSchema): JsonSchema.Type | undefined { - if (stringKeys.some((key) => js[key] !== undefined)) { - return "string" - } - if (numberKeys.some((key) => js[key] !== undefined)) { - return "number" - } - if (objectKeys.some((key) => js[key] !== undefined)) { - return "object" - } - if (arrayKeys.some((key) => js[key] !== undefined)) { - return "array" - } -} - -const types = ["null", "string", "number", "integer", "boolean", "object", "array"] - -function isType(type: unknown): type is JsonSchema.Type { - return typeof type === "string" && types.includes(type) -} - -/** @internal */ -export type TopologicalSort = { - /** - * The definitions that are not recursive. - * The definitions that depends on other definitions are placed after the definitions they depend on - */ - readonly nonRecursives: ReadonlyArray<{ - readonly $ref: string - readonly representation: Representation - }> - /** - * The recursive definitions (with no particular order). - */ - readonly recursives: { - readonly [$ref: string]: Representation - } -} - -/** @internal */ -export function topologicalSort(references: References): TopologicalSort { - const identifiers = Object.keys(references) - const identifierSet = new Set(identifiers) - - const collectRefs = (root: Representation): Set => { - const refs = new Set() - const visited = new WeakSet() - const stack: Array = [root] - - while (stack.length > 0) { - const r = stack.pop()! - if (visited.has(r)) continue - visited.add(r) - - if (r._tag === "Reference") { - if (identifierSet.has(r.$ref)) { - refs.add(r.$ref) - } - } - - // Push nested Representation schemas onto the stack - switch (r._tag) { - case "Declaration": - for (const typeParam of r.typeParameters) stack.push(typeParam) - stack.push(r.encodedSchema) - break - case "Suspend": - stack.push(r.thunk) - break - case "String": - if (r.contentSchema !== undefined) stack.push(r.contentSchema) - break - case "TemplateLiteral": - for (const part of r.parts) stack.push(part) - break - case "Arrays": - for (const element of r.elements) stack.push(element.type) - for (const rest of r.rest) stack.push(rest) - break - case "Objects": - for (const propertySignature of r.propertySignatures) stack.push(propertySignature.type) - for (const indexSignature of r.indexSignatures) { - stack.push(indexSignature.parameter) - stack.push(indexSignature.type) - } - break - case "Union": - for (const type of r.types) stack.push(type) - break - } - } - - return refs - } - - // identifier -> internal identifiers it depends on - const dependencies = new Map>( - identifiers.map((id) => [id, collectRefs(references[id])]) - ) - - // Mark only nodes that are part of cycles - const recursive = new Set() - const state = new Map() // 0 = new, 1 = visiting, 2 = done - const stack: Array = [] - const indexInStack = new Map() - - const dfs = (id: string): void => { - const s = state.get(id) ?? 0 - if (s === 1) { - const start = indexInStack.get(id) - if (start !== undefined) { - for (let i = start; i < stack.length; i++) { - recursive.add(stack[i]) - } - } - return - } - if (s === 2) return - - state.set(id, 1) - indexInStack.set(id, stack.length) - stack.push(id) - - for (const dep of dependencies.get(id) ?? []) { - dfs(dep) - } - - stack.pop() - indexInStack.delete(id) - state.set(id, 2) - } - - for (const id of identifiers) dfs(id) - - // Topologically sort the non-recursive nodes (ignoring edges to recursive nodes) - const inDegree = new Map() - const dependents = new Map>() // dep -> nodes that depend on it - - for (const id of identifiers) { - if (!recursive.has(id)) { - inDegree.set(id, 0) - dependents.set(id, new Set()) - } - } - - for (const [id, deps] of dependencies) { - if (recursive.has(id)) continue - for (const dep of deps) { - if (recursive.has(dep)) continue - inDegree.set(id, (inDegree.get(id) ?? 0) + 1) - dependents.get(dep)?.add(id) - } - } - - const queue: Array = [] - for (const [id, deg] of inDegree) { - if (deg === 0) queue.push(id) - } - - const nonRecursives: Array<{ readonly $ref: string; readonly representation: Representation }> = [] - for (let i = 0; i < queue.length; i++) { - const $ref = queue[i] - nonRecursives.push({ $ref, representation: references[$ref] }) - - for (const next of dependents.get($ref) ?? []) { - const deg = (inDegree.get(next) ?? 0) - 1 - inDegree.set(next, deg) - if (deg === 0) queue.push(next) - } - } - - const recursives: Record = {} - for (const $ref of recursive) { - recursives[$ref] = references[$ref] - } - - return { nonRecursives, recursives } +export function fromJsonSchemaMultiDocument( + document: JsonSchema.MultiDocument<"draft-2020-12">, + options?: FromJsonSchemaOptions +): readonly [Schema.Top, ...Array] { + return InternalFromJsonSchemaDocument.fromJsonSchemaMultiDocument(document, options) } diff --git a/.context/effect/packages/effect/src/SchemaTransformation.ts b/.context/effect/packages/effect/src/SchemaTransformation.ts index a9709e1ca..e90c1a653 100644 --- a/.context/effect/packages/effect/src/SchemaTransformation.ts +++ b/.context/effect/packages/effect/src/SchemaTransformation.ts @@ -52,13 +52,15 @@ import * as SchemaIssue from "./SchemaIssue.ts" * * **Example** (Creating a middleware that falls back on decode failure) * - * ```ts - * import { Effect, Option, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, SchemaIssue, SchemaTransformation } from "effect" * - * const fallback = new SchemaTransformation.Middleware( + * const fallback = new SchemaTransformation.Middleware( * (effect) => Effect.catch(effect, () => Effect.succeed(Option.some("fallback"))), * (effect) => effect * ) + * const issue = new SchemaIssue.InvalidValue({ message: "Missing value" }) + * await Effect.runPromise(fallback.decode(Effect.fail(issue), {})) // => Option.some("fallback") * ``` * * @see {@link Transformation} — value-level bidirectional transformation @@ -121,14 +123,13 @@ const TypeId = "~effect/SchemaTransformation/Transformation" * * **Example** (Composing two transformations) * - * ```ts + * ```ts import.meta.vitest * import { SchemaTransformation } from "effect" * * const trimAndLower = SchemaTransformation.trim().compose( * SchemaTransformation.toLowerCase() * ) - * // decode: trim then lowercase - * // encode: passthrough (both directions) + * trimAndLower._tag // => "Transformation" * ``` * * @see {@link make} — construct from `{ decode, encode }` getters @@ -178,14 +179,11 @@ export class Transformation { * * **Example** (Checking a value) * - * ```ts + * ```ts import.meta.vitest * import { SchemaTransformation } from "effect" * - * SchemaTransformation.isTransformation(SchemaTransformation.trim()) - * // true - * - * SchemaTransformation.isTransformation({ decode: null, encode: null }) - * // false + * SchemaTransformation.isTransformation(SchemaTransformation.trim()) // => true + * SchemaTransformation.isTransformation({ decode: null, encode: null }) // => false * ``` * * @see {@link Transformation} @@ -195,7 +193,7 @@ export class Transformation { * @since 4.0.0 */ export function isTransformation(u: unknown): u is Transformation { - return Predicate.hasProperty(u, TypeId) + return Predicate.hasProperty(u, TypeId) && u[TypeId] === TypeId } /** @@ -214,13 +212,14 @@ export function isTransformation(u: unknown): u is Transformation((s) => Number(s)), * encode: SchemaGetter.transform((n) => String(n)) * }) + * t._tag // => "Transformation" * ``` * * @see {@link transform} — simpler constructor from pure functions @@ -257,30 +256,31 @@ export const make = (options: { * * **Example** (Parsing a date string that can fail) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect" * * const DateFromString = Schema.String.pipe( * Schema.decodeTo( * Schema.Date, * SchemaTransformation.transformOrFail({ - * decode: (s) => { + * decode: (s, options) => { * const d = new Date(s) * return isNaN(d.getTime()) - * ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: "Invalid date" })) + * ? Effect.fail(new SchemaIssue.InvalidValue({ message: "Invalid date" }, s, options)) * : Effect.succeed(d) * }, * encode: (d) => Effect.succeed(d.toISOString()) * }) * ) * ) + * Schema.decodeSync(DateFromString)("2024-01-01").toISOString() // => "2024-01-01T00:00:00.000Z" * ``` * * @see {@link transform} — for infallible, pure transformations * @see {@link transformOptional} — for transformations that handle missing keys * @see {@link make} — for transformations from existing Getters * - * @category constructors + * @category transforming * @since 3.10.0 */ export function transformOrFail(options: { @@ -310,7 +310,7 @@ export function transformOrFail(options: { * * **Example** (Converting between cents and dollars) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const CentsFromDollars = Schema.Number.pipe( @@ -322,13 +322,14 @@ export function transformOrFail(options: { * }) * ) * ) + * Schema.decodeSync(CentsFromDollars)(2.5) // => 250 * ``` * * @see {@link transformOrFail} — for fallible or effectful transformations * @see {@link transformOptional} — for transformations that handle missing keys * @see {@link passthrough} — when no conversion is needed * - * @category constructors + * @category transforming * @since 3.10.0 */ export function transform(options: { @@ -360,7 +361,7 @@ export function transform(options: { * * **Example** (Converting an optional key to Option) * - * ```ts + * ```ts import.meta.vitest * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.Struct({ @@ -374,13 +375,14 @@ export function transform(options: { * ) * ) * }) + * Schema.decodeSync(schema)({}).a // => Option.none() * ``` * * @see {@link transform} — when you don't need Option-level control * @see {@link optionFromOptionalKey} — built-in for the common optional-key-to-Option pattern * @see {@link optionFromOptional} — built-in for optional (undefined) to Option * - * @category constructors + * @category transforming * @since 4.0.0 */ export function transformOptional(options: { @@ -410,19 +412,20 @@ export function transformOptional(options: { * * **Example** (Trimming on decode) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Trimmed = Schema.String.pipe( * Schema.decode(SchemaTransformation.trim()) * ) + * Schema.decodeSync(Trimmed)(" hello ") // => "hello" * ``` * * @see {@link toLowerCase} * @see {@link toUpperCase} * @see {@link snakeToCamel} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function trim(): Transformation { @@ -449,18 +452,19 @@ export function trim(): Transformation { * * **Example** (Converting snake case to camel case) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const SnakeToCamel = Schema.String.pipe( * Schema.decode(SchemaTransformation.snakeToCamel()) * ) + * Schema.decodeSync(SnakeToCamel)("user_name") // => "userName" * ``` * * @see {@link trim} * @see {@link toLowerCase} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function snakeToCamel(): Transformation { @@ -486,18 +490,19 @@ export function snakeToCamel(): Transformation { * * **Example** (Lowercasing on decode) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Lowered = Schema.String.pipe( * Schema.decode(SchemaTransformation.toLowerCase()) * ) + * Schema.decodeSync(Lowered)("HELLO") // => "hello" * ``` * * @see {@link toUpperCase} * @see {@link trim} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function toLowerCase(): Transformation { @@ -523,18 +528,19 @@ export function toLowerCase(): Transformation { * * **Example** (Uppercasing on decode) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Uppered = Schema.String.pipe( * Schema.decode(SchemaTransformation.toUpperCase()) * ) + * Schema.decodeSync(Uppered)("hello") // => "HELLO" * ``` * * @see {@link toLowerCase} * @see {@link trim} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function toUpperCase(): Transformation { @@ -560,18 +566,19 @@ export function toUpperCase(): Transformation { * * **Example** (Capitalizing on decode) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Capitalized = Schema.String.pipe( * Schema.decode(SchemaTransformation.capitalize()) * ) + * Schema.decodeSync(Capitalized)("hello") // => "Hello" * ``` * * @see {@link uncapitalize} * @see {@link toUpperCase} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function capitalize(): Transformation { @@ -597,18 +604,19 @@ export function capitalize(): Transformation { * * **Example** (Uncapitalizing on decode) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Uncapitalized = Schema.String.pipe( * Schema.decode(SchemaTransformation.uncapitalize()) * ) + * Schema.decodeSync(Uncapitalized)("Hello") // => "hello" * ``` * * @see {@link capitalize} * @see {@link toLowerCase} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function uncapitalize(): Transformation { @@ -636,7 +644,7 @@ export function uncapitalize(): Transformation { * * **Example** (Parsing key-value pairs) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const Config = Schema.String.pipe( @@ -645,13 +653,13 @@ export function uncapitalize(): Transformation { * SchemaTransformation.splitKeyValue({ separator: ";", keyValueSeparator: ":" }) * ) * ) - * // "host:localhost;port:3000" → { host: "localhost", port: "3000" } + * Schema.decodeSync(Config)("host:localhost;port:3000") // => { host: "localhost", port: "3000" } * ``` * * @see {@link trim} * @see {@link snakeToCamel} * - * @category String transformations + * @category transforming * @since 4.0.0 */ export function splitKeyValue(options?: { @@ -687,12 +695,13 @@ const passthrough_ = new Transformation( * * **Example** (Chaining schemas with no conversion) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.Trim.pipe( * Schema.decodeTo(Schema.FiniteFromString, SchemaTransformation.passthrough()) * ) + * Schema.decodeSync(schema)("1") // => 1 * ``` * * @see {@link passthroughSupertype} @@ -724,10 +733,11 @@ export function passthrough(): Transformation { * * **Example** (Passing through supertypes) * - * ```ts + * ```ts import.meta.vitest * import { SchemaTransformation } from "effect" * - * const t = SchemaTransformation.passthroughSupertype<"a" | "b", string>() + * const t: SchemaTransformation.Transformation<"a" | "b", string> = + * SchemaTransformation.passthroughSupertype<"a" | "b", string>() * ``` * * @see {@link passthrough} @@ -757,10 +767,11 @@ export function passthroughSupertype(): Transformation { * * **Example** (Passing through subtypes) * - * ```ts + * ```ts import.meta.vitest * import { SchemaTransformation } from "effect" * - * const t = SchemaTransformation.passthroughSubtype() + * const t: SchemaTransformation.Transformation = + * SchemaTransformation.passthroughSubtype() * ``` * * @see {@link passthrough} @@ -792,18 +803,19 @@ export function passthroughSubtype(): Transformation { * * **Example** (Converting a string to a number) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.Number, SchemaTransformation.numberFromString) * ) + * Schema.decodeSync(schema)("42") // => 42 * ``` * * @see {@link bigintFromString} * @see {@link transform} * - * @category Coercions + * @category converting * @since 4.0.0 */ export const numberFromString = new Transformation( @@ -828,18 +840,19 @@ export const numberFromString = new Transformation( * * **Example** (Converting a string to a BigInt) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.BigInt, SchemaTransformation.bigintFromString) * ) + * Schema.decodeSync(schema)("42") // => 42n * ``` * * @see {@link numberFromString} * @see {@link transform} * - * @category Coercions + * @category converting * @since 4.0.0 */ export const bigintFromString = new Transformation( @@ -852,8 +865,8 @@ export const bigintFromString = new Transformation( * * **When to use** * - * Use when you need a schema transformation to parse ISO 8601 date strings from - * APIs or user input. + * Use when you need a schema transformation to parse date strings from APIs or + * user input. * * **Details** * @@ -863,18 +876,19 @@ export const bigintFromString = new Transformation( * * **Example** (Converting a string to a Date) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString) * ) + * Schema.decodeSync(schema)("2024-01-01").toISOString() // => "2024-01-01T00:00:00.000Z" * ``` * * @see {@link dateFromMillis} * @see {@link dateTimeUtcFromString} * - * @category Coercions + * @category converting * @since 4.0.0 */ export const dateFromString: Transformation = new Transformation( @@ -903,18 +917,19 @@ export const dateFromString: Transformation = new Trans * * **Example** (Converting milliseconds to a Date) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.Number.pipe( * Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromMillis) * ) + * Schema.decodeSync(schema)(0).toISOString() // => "1970-01-01T00:00:00.000Z" * ``` * * @see {@link dateFromString} * @see {@link SchemaGetter.dateTimeUtcFromInput} * - * @category Coercions + * @category converting * @since 4.0.0 */ export const dateFromMillis: Transformation = new Transformation( @@ -940,12 +955,13 @@ export const dateFromMillis: Transformation = new Trans * * **Example** (Converting a string to a Duration) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromString) * ) + * String(Schema.decodeSync(schema)("5 seconds")) // => "5000 millis" * ``` * * @see {@link durationFromNanos} @@ -958,10 +974,16 @@ export const durationFromString: Transformation = tra Duration.Duration, string >({ - decode: (s) => + decode: (s, options) => Option.match(Duration.fromInput(s as Duration.Input), { onNone: () => - Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid Duration string: ${s}` })), + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid Duration string" }, + s, + options + ) + ), onSome: Effect.succeed }), encode: (duration) => Effect.succeed(globalThis.String(duration)) @@ -984,12 +1006,13 @@ export const durationFromString: Transformation = tra * * **Example** (Converting nanoseconds to a Duration) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.BigInt.pipe( * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromNanos) * ) + * String(Schema.decodeSync(schema)(5n)) // => "5 nanos" * ``` * * @see {@link durationFromMillis} @@ -999,11 +1022,15 @@ export const durationFromString: Transformation = tra */ export const durationFromNanos: Transformation = transformOrFail({ decode: (i) => Effect.succeed(Duration.nanos(i)), - encode: (a) => + encode: (a, options) => Option.match(Duration.toNanos(a), { onNone: () => Effect.fail( - new SchemaIssue.InvalidValue(Option.some(a), { message: `Unable to encode ${a} into a bigint` }) + new SchemaIssue.InvalidValue( + { expected: "a Duration representable as a bigint" }, + a, + options + ) ), onSome: (nanos) => Effect.succeed(nanos) }) @@ -1025,12 +1052,13 @@ export const durationFromNanos: Transformation = tran * * **Example** (Converting milliseconds to a Duration) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.Number.pipe( * Schema.decodeTo(Schema.Duration, SchemaTransformation.durationFromMillis) * ) + * String(Schema.decodeSync(schema)(5000)) // => "5000 millis" * ``` * * @see {@link durationFromNanos} @@ -1140,8 +1168,8 @@ export const defectFromJson = (options?: ErrorOptions) => * * **Example** (Converting nullable values to an Option) * - * ```ts - * import { Schema, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.NullOr(Schema.String).pipe( * Schema.decodeTo( @@ -1149,6 +1177,7 @@ export const defectFromJson = (options?: ErrorOptions) => * SchemaTransformation.optionFromNullOr() * ) * ) + * Schema.decodeSync(schema)(null) // => Option.none() * ``` * * @see {@link optionFromNullishOr} @@ -1180,8 +1209,8 @@ export function optionFromNullOr(): Transformation, T | null * * **Example** (Converting undefined-or values to an Option) * - * ```ts - * import { Schema, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.UndefinedOr(Schema.String).pipe( * Schema.decodeTo( @@ -1189,6 +1218,7 @@ export function optionFromNullOr(): Transformation, T | null * SchemaTransformation.optionFromUndefinedOr() * ) * ) + * Schema.decodeSync(schema)(undefined) // => Option.none() * ``` * * @see {@link optionFromOptionalKey} @@ -1223,8 +1253,8 @@ export function optionFromUndefinedOr(): Transformation, T | * * **Example** (Converting nullish values to an Option and encoding None as null) * - * ```ts - * import { Schema, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.NullishOr(Schema.String).pipe( * Schema.decodeTo( @@ -1232,6 +1262,7 @@ export function optionFromUndefinedOr(): Transformation, T | * SchemaTransformation.optionFromNullishOr({ onNoneEncoding: null }) * ) * ) + * Schema.encodeSync(schema)(Option.none()) // => null * ``` * * @see {@link optionFromNullOr} @@ -1269,8 +1300,8 @@ export function optionFromNullishOr( * * **Example** (Converting an optional key to an Option) * - * ```ts - * import { Schema, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.Struct({ * name: Schema.optionalKey(Schema.String).pipe( @@ -1280,6 +1311,7 @@ export function optionFromNullishOr( * ) * ) * }) + * Schema.decodeSync(schema)({}).name // => Option.none() * ``` * * @see {@link optionFromOptional} @@ -1314,8 +1346,8 @@ export function optionFromOptionalKey(): Transformation, T> * * **Example** (Converting an optional value to an Option) * - * ```ts - * import { Schema, SchemaTransformation } from "effect" + * ```ts import.meta.vitest + * import { Option, Schema, SchemaTransformation } from "effect" * * const schema = Schema.Struct({ * age: Schema.optional(Schema.Number).pipe( @@ -1325,6 +1357,7 @@ export function optionFromOptionalKey(): Transformation, T> * ) * ) * }) + * Schema.decodeSync(schema)({ age: undefined }).age // => Option.none() * ``` * * @see {@link optionFromOptionalKey} @@ -1357,12 +1390,13 @@ export function optionFromOptional(): Transformation, T | un * * **Example** (Converting a string to a URL) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.URL, SchemaTransformation.urlFromString) * ) + * Schema.decodeSync(schema)("https://example.com/path").href // => "https://example.com/path" * ``` * * @see {@link numberFromString} @@ -1372,10 +1406,16 @@ export function optionFromOptional(): Transformation, T | un * @since 4.0.0 */ export const urlFromString: Transformation = transformOrFail({ - decode: (s) => + decode: (s, options) => URL.canParse(s) ? Effect.succeed(new URL(s)) - : Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid URL string: ${s}` })), + : Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid URL string" }, + s, + options + ) + ), encode: (url) => Effect.succeed(url.href) }) @@ -1401,10 +1441,16 @@ export const bigDecimalFromString: Transformation BigDecimal.BigDecimal, string >({ - decode: (s) => { + decode: (s, options) => { const result = BigDecimal.fromString(s) return Option.isNone(result) - ? Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid BigDecimal string: ${s}` })) + ? Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid BigDecimal string" }, + s, + options + ) + ) : Effect.succeed(result.value) }, encode: (bd) => Effect.succeed(BigDecimal.format(bd)) @@ -1426,12 +1472,13 @@ export const bigDecimalFromString: Transformation * * **Example** (Converting Base64 to a Uint8Array) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.Uint8Array, SchemaTransformation.uint8ArrayFromBase64String) * ) + * Array.from(Schema.decodeSync(schema)("AQID")) // => [1, 2, 3] * ``` * * @see {@link fromJsonString} @@ -1461,12 +1508,13 @@ export const uint8ArrayFromBase64String: Transformation = new Transf * * **Example** (Converting Base64Url to a string) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromBase64UrlString) * ) + * Schema.decodeSync(schema)("aGVsbG8") // => "hello" * ``` * * @see {@link stringFromBase64String} @@ -1529,12 +1578,13 @@ export const stringFromBase64UrlString: Transformation = new Tra * * **Example** (Converting hex to a string) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromHexString) * ) + * Schema.decodeSync(schema)("68656c6c6f") // => "hello" * ``` * * @see {@link stringFromBase64String} @@ -1565,12 +1615,13 @@ export const stringFromHexString: Transformation = new Transform * * **Example** (Defining a URI component schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( * Schema.decodeTo(Schema.String, SchemaTransformation.stringFromUriComponent) * ) + * Schema.decodeSync(schema)("hello%20world") // => "hello world" * ``` * * @see {@link stringFromBase64String} @@ -1596,17 +1647,20 @@ export const stringFromUriComponent: Transformation = new Transf * * **Details** * - * Decode fails with `InvalidValue` for invalid JSON, and encode can fail with - * `InvalidValue` when `JSON.stringify` cannot serialize the value. + * The `reviver` option is passed to `JSON.parse` during decoding. The + * `replacer` and `space` options are passed to `JSON.stringify` during + * encoding. Decode fails with `InvalidValue` for invalid JSON, and encode can + * fail with `InvalidValue` when `JSON.stringify` cannot serialize the value. * * **Example** (Parsing JSON) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.String.pipe( - * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromJsonString) + * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromJsonString()) * ) + * Schema.decodeSync(schema)("{\"ok\":true}") // => { ok: true } * ``` * * @see {@link uint8ArrayFromBase64String} @@ -1615,10 +1669,16 @@ export const stringFromUriComponent: Transformation = new Transf * @category decoding * @since 4.0.0 */ -export const fromJsonString = new Transformation( - SchemaGetter.parseJson(), - SchemaGetter.stringifyJson() -) +export function fromJsonString(options?: { + readonly reviver?: Parameters[1] | undefined + readonly replacer?: SchemaGetter.JsonReplacer | undefined + readonly space?: Parameters[2] | undefined +}): Transformation { + return new Transformation( + SchemaGetter.parseJson(options ?? {}), + SchemaGetter.stringifyJson(options) + ) +} /** * Decodes a `FormData` instance into a nested record using bracket-path keys and @@ -1637,12 +1697,15 @@ export const fromJsonString = new Transformation( * * **Example** (Decoding FormData) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.instanceOf(FormData).pipe( * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromFormData) * ) + * const formData = new FormData() + * formData.append("user[name]", "Alice") + * Schema.decodeSync(schema)(formData) // => { user: { name: "Alice" } } * ``` * * @see {@link fromURLSearchParams} @@ -1673,12 +1736,13 @@ export const fromFormData = new Transformation( * * **Example** (Decoding URLSearchParams) * - * ```ts + * ```ts import.meta.vitest * import { Schema, SchemaTransformation } from "effect" * * const schema = Schema.instanceOf(URLSearchParams).pipe( * Schema.decodeTo(Schema.Unknown, SchemaTransformation.fromURLSearchParams) * ) + * Schema.decodeSync(schema)(new URLSearchParams("user[name]=Alice")) // => { user: { name: "Alice" } } * ``` * * @see {@link fromFormData} @@ -1743,10 +1807,16 @@ export const timeZoneNamedFromString: Transformation({ - decode: (s) => { + decode: (s, options) => { return Option.match(DateTime.zoneMakeNamed(s), { onNone: () => - Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid IANA time zone: ${s}` })), + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid IANA time zone" }, + s, + options + ) + ), onSome: Effect.succeed }) }, @@ -1778,9 +1848,16 @@ export const timeZoneFromString: Transformation = tra DateTime.TimeZone, string >({ - decode: (s) => { + decode: (s, options) => { return Option.match(DateTime.zoneFromString(s), { - onNone: () => Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid time zone: ${s}` })), + onNone: () => + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid time zone" }, + s, + options + ) + ), onSome: Effect.succeed }) }, @@ -1812,10 +1889,16 @@ export const dateTimeUtcFromString: Transformation = trans DateTime.Utc, string >({ - decode: (s) => { + decode: (s, options) => { return Option.match(DateTime.make(s), { onNone: () => - Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid UTC DateTime string: ${s}` })), + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid UTC DateTime string" }, + s, + options + ) + ), onSome: (result) => Effect.succeed(DateTime.toUtc(result)) }) }, @@ -1846,10 +1929,16 @@ export const dateTimeZonedFromString: Transformation = t DateTime.Zoned, string >({ - decode: (s) => { + decode: (s, options) => { return Option.match(DateTime.makeZonedFromString(s), { onNone: () => - Effect.fail(new SchemaIssue.InvalidValue(Option.some(s), { message: `Invalid Zoned DateTime string: ${s}` })), + Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a valid Zoned DateTime string" }, + s, + options + ) + ), onSome: Effect.succeed }) }, diff --git a/.context/effect/packages/effect/src/SchemaUtils.ts b/.context/effect/packages/effect/src/SchemaUtils.ts deleted file mode 100644 index 1746d087a..000000000 --- a/.context/effect/packages/effect/src/SchemaUtils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Small helpers for schema patterns that are too specialized for the main - * `Schema` module. The current helper builds a schema for an existing class: - * the encoded input is checked with a struct schema, decoding calls the class - * constructor with the decoded properties, and the final value remains an - * instance of that class. - * - * @since 4.0.0 - */ -import { identity } from "./Function.ts" -import * as Schema from "./Schema.ts" -import * as SchemaTransformation from "./SchemaTransformation.ts" - -/** - * Builds an experimental schema for instances of a native class using a struct - * schema as the encoded representation. - * - * **When to use** - * - * Use when you need a schema for an existing native class while keeping a - * `Struct` schema as its encoded representation. - * - * **Details** - * - * Decoding constructs `new constructor(props)` from the encoded fields. - * Encoding uses the instance as the encoded shape, so the class should expose - * properties compatible with the provided encoding schema. - * - * @see {@link Schema.instanceOf} for validating existing class instances without a struct encoding - * @see {@link Schema.Class} for defining schema-backed classes directly - * @see {@link Schema.ErrorClass} for defining schema-backed error classes - * - * @category schemas - * @since 4.0.0 - */ -export function getNativeClassSchema any, S extends Schema.Struct>( - constructor: C, - options: { - readonly encoding: S - readonly annotations?: Schema.Annotations.Declaration> - } -): Schema.decodeTo, S["Iso"]>, S> { - const transformation = SchemaTransformation.transform, S["Type"]>({ - decode: (props) => new constructor(props), - encode: identity - }) - return Schema.instanceOf(constructor, { - toCodec: () => Schema.link>()(options.encoding, transformation), - ...options.annotations - }).pipe(Schema.encodeTo(options.encoding, transformation)) -} diff --git a/.context/effect/packages/effect/src/Scope.ts b/.context/effect/packages/effect/src/Scope.ts index 49a6dc271..a7268975c 100644 --- a/.context/effect/packages/effect/src/Scope.ts +++ b/.context/effect/packages/effect/src/Scope.ts @@ -25,23 +25,21 @@ const CloseableTypeId = effect.ScopeCloseableTypeId * * **Example** (Managing scoped resources) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Scope } from "effect" * * const program = Effect.gen(function*() { * const scope = yield* Scope.make("sequential") * - * // Scope has a strategy and state - * console.log(scope.strategy) // "sequential" - * console.log(scope.state._tag) // "Open" - * - * // Close the scope + * const initial = [scope.strategy, scope.state._tag] * yield* Scope.close(scope, Exit.void) - * console.log(scope.state._tag) // "Closed" + * return [initial, scope.state._tag] * }) + * + * Effect.runSync(program) // => [["sequential", "Empty"], "Closed"] * ``` * - * @category models + * @category services * @since 2.0.0 */ export interface Scope { @@ -55,18 +53,18 @@ export interface Scope { * * **Example** (Closing a scope) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const cleanups: Array = [] * const program = Effect.gen(function*() { * const scope = yield* Scope.make() - * - * // Add a finalizer - * yield* Scope.addFinalizer(scope, Console.log("Cleanup!")) - * - * // Scope can be closed + * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup!"))) * yield* Scope.close(scope, Exit.void) * }) + * + * Effect.runSync(program) + * cleanups // => ["Cleanup!"] * ``` * * @category models @@ -83,25 +81,17 @@ export interface Closeable extends Scope { * * **Example** (Checking scope states) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Scope } from "effect" * - * // Example of checking scope states * const program = Effect.gen(function*() { * const scope = yield* Scope.make() - * - * // When open, the scope accepts finalizers - * if (scope.state._tag === "Open") { - * console.log("Scope is open") - * } - * + * const before = scope.state._tag * yield* Scope.close(scope, Exit.void) - * - * // When closed, the scope no longer accepts finalizers - * if (scope.state._tag === "Closed") { - * console.log("Scope is closed") - * } + * return [before, scope.state._tag] * }) + * + * Effect.runSync(program) // => ["Empty", "Closed"] * ``` * * @since 4.0.0 @@ -118,16 +108,12 @@ export declare namespace State { * * **Example** (Inspecting an empty scope state) * - * ```ts + * ```ts import.meta.vitest * import { Scope } from "effect" * * const scope = Scope.makeUnsafe() * - * // When scope is open, you can check its state - * if (scope.state._tag === "Open") { - * console.log("Scope is open and accepting finalizers") - * console.log(scope.state.finalizers.size) // Number of registered finalizers - * } + * scope.state._tag // => "Empty" * ``` * * @category models @@ -142,16 +128,17 @@ export declare namespace State { * * **Example** (Inspecting an open scope state) * - * ```ts - * import { Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Scope } from "effect" * * const scope = Scope.makeUnsafe() * - * // When scope is open, you can check its state - * if (scope.state._tag === "Open") { - * console.log("Scope is open and accepting finalizers") - * console.log(scope.state.finalizers.size) // Number of registered finalizers - * } + * Effect.runSync(Scope.addFinalizer(scope, Effect.void)) + * const state = scope.state + * if (state._tag !== "Open") throw new Error("unexpected state") + * + * state._tag // => "Open" + * state.finalizers.size // => 1 * ``` * * @category models @@ -167,21 +154,20 @@ export declare namespace State { * * **Example** (Inspecting a closed scope state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Exit, Scope } from "effect" * * const program = Effect.gen(function*() { * const scope = yield* Scope.make() * - * // Close the scope * yield* Scope.close(scope, Exit.succeed("Done")) - * - * // Check if scope is closed * if (scope.state._tag === "Closed") { - * console.log("Scope is closed") - * console.log(scope.state.exit) // The exit value used to close the scope + * return scope.state.exit * } + * return Exit.die("unexpected state") * }) + * + * Effect.runSync(program) // => Exit.succeed("Done") * ``` * * @category models @@ -203,19 +189,17 @@ export declare namespace State { * * **Example** (Accessing the scope service) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Scope } from "effect" * + * const cleanups: Array = [] * const program = Effect.gen(function*() { - * // Access the scope from the context * const scope = yield* Scope.Scope - * - * // Use the scope for resource management - * yield* Scope.addFinalizer(scope, Effect.log("Cleanup")) + * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup"))) * }) * - * // Provide a scope to the program - * const scoped = Effect.scoped(program) + * Effect.runSync(Effect.scoped(program)) + * cleanups // => ["Cleanup"] * ``` * * @category services @@ -228,21 +212,19 @@ export const Scope: Context.Service = effect.scopeTag * * **Example** (Creating a scope) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const cleanups: Array = [] * const program = Effect.gen(function*() { - * // Create a scope with sequential cleanup * const scope = yield* Scope.make("sequential") - * - * // Add finalizers - * yield* Scope.addFinalizer(scope, Console.log("Cleanup 1")) - * yield* Scope.addFinalizer(scope, Console.log("Cleanup 2")) - * - * // Close the scope (finalizers run in reverse order) + * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup 1"))) + * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup 2"))) * yield* Scope.close(scope, Exit.void) - * // Output: "Cleanup 2", then "Cleanup 1" * }) + * + * Effect.runSync(program) + * cleanups // => ["Cleanup 2", "Cleanup 1"] * ``` * * @category constructors @@ -262,17 +244,18 @@ export const make: (finalizerStrategy?: "sequential" | "parallel") => Effect = [] * const program = Effect.gen(function*() { - * yield* Scope.addFinalizer(scope, Console.log("Cleanup")) + * yield* Scope.addFinalizer(scope, Effect.sync(() => cleanups.push("Cleanup"))) * yield* Scope.close(scope, Exit.void) * }) + * + * Effect.runSync(program) + * cleanups // => ["Cleanup"] * ``` * * @category constructors @@ -294,21 +277,24 @@ export const makeUnsafe: (finalizerStrategy?: "sequential" | "parallel") => Clos * * **Example** (Providing a scope) * - * ```ts - * import { Console, Effect, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * - * // An effect that requires a Scope + * const events: Array = [] * const program = Effect.gen(function*() { * const scope = yield* Scope.Scope - * yield* Scope.addFinalizer(scope, Console.log("Cleanup")) - * yield* Console.log("Working...") + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup"))) + * events.push("working") * }) * - * // Provide a scope to the program * const withScope = Effect.gen(function*() { * const scope = yield* Scope.make() * yield* Scope.provide(scope)(program) + * yield* Scope.close(scope, Exit.void) * }) + * + * Effect.runSync(withScope) + * events // => ["working", "cleanup"] * ``` * * @category combinators @@ -335,29 +321,18 @@ export const provide: { * * **Example** (Adding an exit-aware finalizer) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const exits: Array> = [] * const withResource = Effect.gen(function*() { * const scope = yield* Scope.make() - * - * // Add a finalizer for cleanup - * yield* Scope.addFinalizerExit( - * scope, - * (exit) => - * Console.log( - * `Cleaning up resource. Exit: ${ - * Exit.isSuccess(exit) ? "Success" : "Failure" - * }` - * ) - * ) - * - * // Use the resource - * yield* Console.log("Using resource") - * - * // Close the scope + * yield* Scope.addFinalizerExit(scope, (exit) => Effect.sync(() => exits.push(exit))) * yield* Scope.close(scope, Exit.void) * }) + * + * Effect.runSync(withResource) + * exits // => [Exit.void] * ``` * * @category combinators @@ -377,23 +352,21 @@ export const addFinalizerExit: (scope: Scope, finalizer: (exit: Exit) * * **Example** (Adding finalizers) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const events: Array = [] * const program = Effect.gen(function*() { * const scope = yield* Scope.make() - * - * // Add simple finalizers - * yield* Scope.addFinalizer(scope, Console.log("Cleanup task 1")) - * yield* Scope.addFinalizer(scope, Console.log("Cleanup task 2")) - * yield* Scope.addFinalizer(scope, Effect.log("Cleanup task 3")) - * - * // Do some work - * yield* Console.log("Doing work...") - * - * // Close the scope + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 1"))) + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 2"))) + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("cleanup 3"))) + * events.push("work") * yield* Scope.close(scope, Exit.void) * }) + * + * Effect.runSync(program) + * events // => ["work", "cleanup 3", "cleanup 2", "cleanup 1"] * ``` * * @category combinators @@ -412,25 +385,21 @@ export const addFinalizer: (scope: Scope, finalizer: Effect) => Effect< * * **Example** (Creating a child scope) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const cleanups: Array = [] * const nestedScopes = Effect.gen(function*() { * const parentScope = yield* Scope.make("sequential") - * - * // Add finalizer to parent - * yield* Scope.addFinalizer(parentScope, Console.log("Parent cleanup")) - * - * // Create child scope + * yield* Scope.addFinalizer(parentScope, Effect.sync(() => cleanups.push("parent"))) * const childScope = yield* Scope.fork(parentScope, "parallel") - * - * // Add finalizer to child - * yield* Scope.addFinalizer(childScope, Console.log("Child cleanup")) - * - * // Close child first, then parent + * yield* Scope.addFinalizer(childScope, Effect.sync(() => cleanups.push("child"))) * yield* Scope.close(childScope, Exit.void) * yield* Scope.close(parentScope, Exit.void) * }) + * + * Effect.runSync(nestedScopes) + * cleanups // => ["child", "parent"] * ``` * * @category combinators @@ -457,21 +426,21 @@ export const fork: ( * * **Example** (Creating a child scope synchronously) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const cleanups: Array = [] * const program = Effect.gen(function*() { * const parentScope = Scope.makeUnsafe("sequential") * const childScope = Scope.forkUnsafe(parentScope, "parallel") - * - * // Add finalizers to both scopes - * yield* Scope.addFinalizer(parentScope, Console.log("Parent cleanup")) - * yield* Scope.addFinalizer(childScope, Console.log("Child cleanup")) - * - * // Close child first, then parent + * yield* Scope.addFinalizer(parentScope, Effect.sync(() => cleanups.push("parent"))) + * yield* Scope.addFinalizer(childScope, Effect.sync(() => cleanups.push("child"))) * yield* Scope.close(childScope, Exit.void) * yield* Scope.close(parentScope, Exit.void) * }) + * + * Effect.runSync(program) + * cleanups // => ["child", "parent"] * ``` * * @category combinators @@ -494,24 +463,21 @@ export const forkUnsafe: (scope: Scope, finalizerStrategy?: "sequential" | "para * * **Example** (Running scope finalizers) * - * ```ts - * import { Console, Effect, Exit, Scope } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Scope } from "effect" * + * const events: Array = [] * const resourceManagement = Effect.gen(function*() { * const scope = yield* Scope.make("sequential") - * - * // Add multiple finalizers - * yield* Scope.addFinalizer(scope, Console.log("Close database connection")) - * yield* Scope.addFinalizer(scope, Console.log("Close file handle")) - * yield* Scope.addFinalizer(scope, Console.log("Release memory")) - * - * // Do some work... - * yield* Console.log("Performing operations...") - * - * // Close scope - finalizers run in reverse order of registration + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("database"))) + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("file"))) + * yield* Scope.addFinalizer(scope, Effect.sync(() => events.push("memory"))) + * events.push("work") * yield* Scope.close(scope, Exit.succeed("Success!")) - * // Output: "Release memory", "Close file handle", "Close database connection" * }) + * + * Effect.runSync(resourceManagement) + * events // => ["work", "memory", "file", "database"] * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/ScopedCache.ts b/.context/effect/packages/effect/src/ScopedCache.ts index c80da9db9..664be8138 100644 --- a/.context/effect/packages/effect/src/ScopedCache.ts +++ b/.context/effect/packages/effect/src/ScopedCache.ts @@ -211,7 +211,7 @@ export const make = < > => makeWith({ ...options, - timeToLive: options.timeToLive ? () => options.timeToLive! : defaultTimeToLive + timeToLive: options.timeToLive !== undefined ? () => options.timeToLive! : defaultTimeToLive }) const Proto = { diff --git a/.context/effect/packages/effect/src/ScopedRef.ts b/.context/effect/packages/effect/src/ScopedRef.ts index f708463cd..8cbb2609e 100644 --- a/.context/effect/packages/effect/src/ScopedRef.ts +++ b/.context/effect/packages/effect/src/ScopedRef.ts @@ -165,7 +165,7 @@ export const make = (evaluate: LazyArg): Effect.Effect, never * changed to the new value, with old resources released, or until the attempt * to acquire a new value fails. * - * @category setters + * @category mutations * @since 2.0.0 */ export const set: { @@ -178,12 +178,14 @@ export const set: { self: ScopedRef, acquire: Effect.Effect ) { - yield* Scope.close(self.backing.backing.ref.current[0], Exit.void) const scope = Scope.makeUnsafe() const value = yield* acquire.pipe( Scope.provide(scope), Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) ) + yield* Scope.close(self.backing.backing.ref.current[0], Exit.void).pipe( + Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) + ) self.backing.backing.ref.current = [scope, value] }, Effect.uninterruptible, diff --git a/.context/effect/packages/effect/src/Semaphore.ts b/.context/effect/packages/effect/src/Semaphore.ts index 12c0758da..217e80920 100644 --- a/.context/effect/packages/effect/src/Semaphore.ts +++ b/.context/effect/packages/effect/src/Semaphore.ts @@ -33,7 +33,7 @@ import type * as Option from "./Option.ts" * * **Example** (Controlling concurrent access) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Semaphore } from "effect" * * // Create and use a semaphore for controlling concurrent access @@ -44,6 +44,8 @@ import type * as Option from "./Option.ts" * Effect.succeed("Resource accessed") * ) * }) + * + * await Effect.runPromise(program) // => "Resource accessed" * ``` * * @see {@link make} for creating a semaphore inside Effect code @@ -118,9 +120,11 @@ export interface Semaphore { ): (self: Effect.Effect) => 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. + * Acquires the specified number of permits and returns the acquired permit + * count, suspending the task if they are not yet available. Pending `take` + * calls are scanned in registration order, but a request is served only when + * enough permits are available, so a smaller later request may overtake a + * larger earlier request. * * **When to use** * @@ -128,6 +132,16 @@ export interface Semaphore { */ take(this: Semaphore, permits: number): Effect.Effect + /** + * Acquires the specified number of permits only if they are immediately + * available. + * + * **When to use** + * + * Use to manually acquire permits without waiting, paired with `release`. + */ + takeIfAvailable(this: Semaphore, permits: number): Effect.Effect + /** * Releases the specified number of permits and returns the resulting * available permits. @@ -160,7 +174,7 @@ export interface Semaphore { * * **Example** (Creating an unsafe semaphore) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Semaphore } from "effect" * * const semaphore = Semaphore.makeUnsafe(3) @@ -168,9 +182,8 @@ export interface Semaphore { * const task = (id: number) => * semaphore.withPermits(1)( * Effect.gen(function*() { - * yield* Effect.log(`Task ${id} started`) - * yield* Effect.sleep("1 second") - * yield* Effect.log(`Task ${id} completed`) + * yield* Effect.yieldNow + * return id * }) * ) * @@ -182,6 +195,8 @@ export interface Semaphore { * task(4), * task(5) * ], { concurrency: "unbounded" }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5] * ``` * * @category constructors @@ -189,6 +204,24 @@ export interface Semaphore { */ export const makeUnsafe = (permits: number): Semaphore => new SemaphoreImpl(permits) +const waitForPermits = ( + self: SemaphoreImpl, + n: number, + effect: Effect.Effect +): Effect.Effect => + internal.callback((resume) => { + if (self.free >= n) return resume(effect) + const observer = () => { + if (self.free < n) return + self.waiters.delete(observer) + resume(effect) + } + self.waiters.add(observer) + return internal.sync(() => { + self.waiters.delete(observer) + }) + }) + class SemaphoreImpl implements Semaphore { public waiters = new Set<() => void>() public taken = 0 @@ -205,18 +238,7 @@ class SemaphoreImpl implements Semaphore { take(n: number): Effect.Effect { const take: Effect.Effect = internal.suspend(() => { if (this.free < n) { - return internal.callback((resume) => { - if (this.free >= n) return resume(take) - const observer = () => { - if (this.free < n) return - this.waiters.delete(observer) - resume(take) - } - this.waiters.add(observer) - return internal.sync(() => { - this.waiters.delete(observer) - }) - }) + return waitForPermits(this, n, take) } this.taken += n return internal.succeed(n) @@ -224,58 +246,64 @@ class SemaphoreImpl implements Semaphore { return take } - updateTakenUnsafe(fiber: Fiber, f: (n: number) => number): number { - this.taken = f(this.taken) + takeIfAvailable(n: number): Effect.Effect { + return internal.suspend(() => { + if (this.free < n) return internal.succeed(false) + this.taken += n + return internal.succeed(true) + }) + } + + releaseUnsafe(fiber: Fiber, n: number): number { + this.taken -= n if (this.waiters.size > 0) { fiber.currentDispatcher.scheduleTask(() => { - const iter = this.waiters.values() - let item = iter.next() - while (item.done === false && this.free > 0) { - item.value() - item = iter.next() + for (const observer of this.waiters) { + if (this.free <= 0) break + observer() } }, 0) } return this.free } - updateTaken(f: (n: number) => number): Effect.Effect { - return core.withFiber((fiber) => internal.succeed(this.updateTakenUnsafe(fiber, f))) - } - resize(permits: number) { return core.withFiber((fiber) => { this.permits = permits if (this.free < 0) return internal.void - this.updateTakenUnsafe(fiber, (taken) => taken) + this.releaseUnsafe(fiber, 0) return internal.void }) } release(n: number): Effect.Effect { - return this.updateTaken((taken) => taken - n) + return core.withFiber((fiber) => internal.succeed(this.releaseUnsafe(fiber, n))) } get releaseAll(): Effect.Effect { - return this.updateTaken((_) => 0) + return core.withFiber((fiber) => internal.succeed(this.releaseUnsafe(fiber, this.taken))) } withPermits(n: number) { return (self: Effect.Effect) => - internal.uninterruptibleMask((restore) => - internal.flatMap( - restore(this.take(n)), - (permits) => - internal.onExitPrimitive( - restore(self), - () => { - this.updateTakenUnsafe(internal.getCurrentFiber()!, (taken) => taken - permits) - return undefined - }, - true - ) - ) - ) + internal.uninterruptibleMask((restore) => { + const acquire: Effect.Effect = internal.suspend(() => { + if (this.free < n) { + const wait = waitForPermits(this, n, internal.void) + return internal.flatMap(restore(wait), () => acquire) + } + this.taken += n + return internal.onExitPrimitive( + restore(self), + () => { + this.releaseUnsafe(internal.getCurrentFiber()!, n) + return undefined + }, + true + ) + }) + return acquire + }) } readonly withPermit = this.withPermits(1) @@ -286,7 +314,7 @@ class SemaphoreImpl implements Semaphore { if (this.free < n) return internal.succeedNone this.taken += n return internal.onExitPrimitive(restore(internal.asSome(self)), () => { - this.updateTakenUnsafe(internal.getCurrentFiber()!, (taken) => taken - n) + this.releaseUnsafe(internal.getCurrentFiber()!, n) return undefined }, true) }) @@ -303,7 +331,7 @@ class SemaphoreImpl implements Semaphore { * * **Example** (Creating a semaphore) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Semaphore } from "effect" * * const program = Effect.gen(function*() { @@ -312,15 +340,16 @@ class SemaphoreImpl implements Semaphore { * const task = (id: number) => * semaphore.withPermits(1)( * Effect.gen(function*() { - * yield* Effect.log(`Task ${id} acquired permit`) - * yield* Effect.sleep("1 second") - * yield* Effect.log(`Task ${id} releasing permit`) + * yield* Effect.yieldNow + * return id * }) * ) * * // Run 4 tasks, but only 2 can run concurrently - * yield* Effect.all([task(1), task(2), task(3), task(4)]) + * return yield* Effect.all([task(1), task(2), task(3), task(4)]) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4] * ``` * * @category constructors @@ -456,6 +485,7 @@ export const withPermitsIfAvailable: { * * @see {@link withPermit} for automatically acquiring and releasing one permit around an effect * @see {@link withPermits} for automatically acquiring and releasing multiple permits around an effect + * @see {@link takeIfAvailable} for manually acquiring permits without waiting * @see {@link release} for returning manually acquired permits * * @category combinators @@ -466,6 +496,33 @@ export const take: { (self: Semaphore, permits: number): Effect.Effect } = dual(2, (self: Semaphore, permits: number) => self.take(permits)) +/** + * Acquires the specified number of permits only if they are immediately + * available. + * + * **When to use** + * + * Use when you need fail-fast manual permit acquisition for a lower-level + * protocol with explicit acquisition and release control. + * + * **Details** + * + * If enough permits are available, they are acquired and the effect returns + * `true`. Otherwise, the effect returns `false` immediately without acquiring + * any permits. + * + * @see {@link take} for the variant that waits until permits are available + * @see {@link release} for returning manually acquired permits + * @see {@link withPermitsIfAvailable} for automatic acquisition and release around an effect + * + * @category combinators + * @since 4.0.0 + */ +export const takeIfAvailable: { + (permits: number): (self: Semaphore) => Effect.Effect + (self: Semaphore, permits: number): Effect.Effect +} = dual(2, (self: Semaphore, permits: number) => self.takeIfAvailable(permits)) + /** * Releases the specified number of permits and returns the resulting available * permits. diff --git a/.context/effect/packages/effect/src/Sink.ts b/.context/effect/packages/effect/src/Sink.ts index 8dd5bd6ef..2d5b7ddd8 100644 --- a/.context/effect/packages/effect/src/Sink.ts +++ b/.context/effect/packages/effect/src/Sink.ts @@ -46,7 +46,7 @@ const TypeId = "~effect/Sink" * * **Example** (Running a sink with a stream) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Sink, Stream } from "effect" * * // Create a simple sink that always succeeds with a value @@ -54,10 +54,7 @@ const TypeId = "~effect/Sink" * * // Use the sink to consume a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program).then(console.log) - * // Output: 42 + * await Effect.runPromise(Stream.run(stream, sink)) // => 42 * ``` * * @category models @@ -194,14 +191,14 @@ const SinkProto = { * * **Example** (Checking for a sink) * - * ```ts + * ```ts import.meta.vitest * import { Sink } from "effect" * * const sink = Sink.never * const notStream = { data: [1, 2, 3] } * - * console.log(Sink.isSink(sink)) // true - * console.log(Sink.isSink(notStream)) // false + * Sink.isSink(sink) // => true + * Sink.isSink(notStream) // => false * ``` * * @category guards @@ -217,6 +214,20 @@ export const isSink = (u: unknown): u is Sink], never, void>().pipe( + * Channel.drain, + * Channel.mapDone(() => ["consumed"] as const) + * ) + * const sink = Sink.fromChannel(channel) + * + * await Effect.runPromise(Stream.run(Stream.make(1, 2, 3), sink)) // => "consumed" + * ``` + * * @see {@link toChannel} for converting a `Sink` back to a `Channel` * @category constructors * @since 2.0.0 @@ -239,6 +250,43 @@ export const fromChannel = ( ) as Effect.Effect, E, R> ) +/** + * Creates a sink that writes its input to a Web `WritableStream`. + * + * **Example** (Collecting values in a Web stream) + * + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" + * + * const written: Array = [] + * const sink = Sink.fromWritableStream({ + * evaluate: () => new WritableStream({ + * write(value) { + * written.push(value) + * } + * }), + * onError: (cause) => new Error(String(cause)) + * }) + * + * await Effect.runPromise(Stream.run(Stream.make(1, 2, 3), sink)) + * written // => [1, 2, 3] + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export const fromWritableStream = (options: { + readonly evaluate: LazyArg> + readonly onError: (error: unknown) => E + readonly closeOnDone?: boolean | undefined +}): Sink => + fromChannel( + Channel.mapDone( + Channel.fromWritableStream(options), + (_) => [_] + ) + ) + /** * Creates a `Sink` from a low-level transform function. * @@ -265,14 +313,16 @@ export const fromTransform = ( /** * Creates a `Channel` from a Sink. * - * **Example** (Converting a sink to a channel) + * **Example** (Running a sink as a channel) * - * ```ts - * import { Sink } from "effect" + * ```ts import.meta.vitest + * import { Channel, Effect, Sink, Stream } from "effect" * - * // Create a sink and extract its channel - * const sink = Sink.succeed(42) - * const channel = Sink.toChannel(sink) + * const channel = Stream.toChannel(Stream.make(1, 2, 3)).pipe( + * Channel.pipeTo(Sink.toChannel(Sink.sum)) + * ) + * + * await Effect.runPromise(Channel.runDone(channel)) // => [6] * ``` * * @category constructors @@ -494,7 +544,7 @@ export const fromPubSub = ( * * **Example** (Succeeding with a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Sink, Stream } from "effect" * * // Create a sink that always yields the same value @@ -502,10 +552,7 @@ export const fromPubSub = ( * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program).then(console.log) - * // Output: 42 + * await Effect.runPromise(Stream.run(stream, sink)) // => 42 * ``` * * @category constructors @@ -536,18 +583,15 @@ export const suspend = (evaluate: LazyArg>) * * **Example** (Failing with an error) * - * ```ts - * import { Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Sink, Stream } from "effect" * * // Create a sink that always fails - * const sink = Sink.fail(new Error("Sink failed")) + * const sink = Sink.fail("Sink failed") * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program).catch(console.log) - * // Output: Error: Sink failed + * await Effect.runPromiseExit(Stream.run(stream, sink)) // => Exit.fail("Sink failed") * ``` * * @category constructors @@ -560,18 +604,15 @@ export const fail = (e: E): Sink => fromEffectEnd(E * * **Example** (Failing with a lazy error) * - * ```ts - * import { Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Sink, Stream } from "effect" * * // Create a sink that fails with a lazy error - * const sink = Sink.failSync(() => new Error("Lazy error")) + * const sink = Sink.failSync(() => "Lazy error") * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program).catch(console.log) - * // Output: Error: Lazy error + * await Effect.runPromiseExit(Stream.run(stream, sink)) // => Exit.fail("Lazy error") * ``` * * @category constructors @@ -585,18 +626,15 @@ export const failSync = (evaluate: LazyArg): Sink Exit.fail("Custom cause") * ``` * * @category constructors @@ -610,18 +648,15 @@ export const failCause = (cause: Cause.Cause): Sink Cause.fail(new Error("Lazy cause"))) + * const sink = Sink.failCauseSync(() => Cause.fail("Lazy cause")) * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program).catch(console.log) - * // Output: Error: Lazy cause + * await Effect.runPromiseExit(Stream.run(stream, sink)) // => Exit.fail("Lazy cause") * ``` * * @category constructors @@ -635,18 +670,15 @@ export const failCauseSync = (evaluate: LazyArg>): Sink Exit.die("Defect error") * ``` * * @category constructors @@ -1115,7 +1147,7 @@ export const mapLeftover: { * If more elements are pulled than needed, the remaining elements from the same * array are returned as leftovers. * - * @category collecting + * @category constructors * @since 2.0.0 */ export const take = (n: number): Sink, In, In> => @@ -1212,7 +1244,7 @@ export const flatMap: { * A sink that reduces input elements from the provided `initial` state with * `f` while the specified `predicate` returns `true`. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceWhile = ( @@ -1248,7 +1280,7 @@ export const reduceWhile = ( * A sink that effectfully reduces input elements from the provided `initial` * state with `f` while the specified `predicate` returns `true`. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceWhileEffect = ( @@ -1289,7 +1321,7 @@ export const reduceWhileEffect = ( * A sink that reduces non-empty input arrays from the provided `initial` state * with `f` while the specified `predicate` returns `true`. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceWhileArray = ( @@ -1304,11 +1336,9 @@ export const reduceWhileArray = ( } return upstream.pipe( Effect.flatMap((arr) => { - for (let i = 0; i < arr.length; i++) { - state = f(state, arr) - if (!contFn(state)) { - return Cause.done() - } + state = f(state, arr) + if (!contFn(state)) { + return Cause.done() } return Effect.void }), @@ -1321,7 +1351,7 @@ export const reduceWhileArray = ( * A sink that effectfully reduces non-empty input arrays from the provided * `initial` state with `f` while the specified `predicate` returns `true`. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceWhileArrayEffect = ( @@ -1352,7 +1382,7 @@ export const reduceWhileArrayEffect = ( * A sink that reduces its inputs using the provided function `f` starting from * the provided `initial` state. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduce = (initial: LazyArg, f: (s: S, input: In) => S): Sink => @@ -1367,7 +1397,7 @@ export const reduce = (initial: LazyArg, f: (s: S, input: In) => S): S * A sink that reduces its inputs using the provided function `f` starting from * the specified `initial` state. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceArray = ( @@ -1390,7 +1420,7 @@ export const reduceArray = ( * A sink that reduces its inputs using the provided effectful function `f` * starting from the specified `initial` state. * - * @category reducing + * @category folding * @since 4.0.0 */ export const reduceEffect = ( @@ -1756,21 +1786,16 @@ export const takeUntilEffect = ( * * **Example** (Running effects for each item) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * - * // Create a sink that logs each item - * const sink = Sink.forEach((item: number) => Console.log(`Processing: ${item}`)) + * const processed: Array = [] + * const sink = Sink.forEach((item: number) => Effect.sync(() => processed.push(item))) * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program) - * // Output: - * // Processing: 1 - * // Processing: 2 - * // Processing: 3 + * await Effect.runPromise(Stream.run(stream, sink)) + * processed // => [1, 2, 3] * ``` * * @category constructors @@ -1786,22 +1811,16 @@ export const forEach = ( * * **Example** (Running effects for each chunk) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * - * // Create a sink that processes chunks - * const sink = Sink.forEachArray((chunk: ReadonlyArray) => - * Console.log( - * `Processing chunk of ${chunk.length} items: [${chunk.join(", ")}]` - * ) - * ) + * const processed: Array> = [] + * const sink = Sink.forEachArray((chunk: ReadonlyArray) => Effect.sync(() => processed.push([...chunk]))) * * // Use it with a stream * const stream = Stream.make(1, 2, 3, 4, 5) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program) - * // Output: Processing chunk of 5 items: [1, 2, 3, 4, 5] + * await Effect.runPromise(Stream.run(stream, sink)) + * processed // => [[1, 2, 3, 4, 5]] * ``` * * @category constructors @@ -1869,24 +1888,20 @@ export const forEachWhileArray = ( * * **Example** (Unwrapping a sink effect) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * * // Create a sink from an effect that produces a sink + * const processed: Array = [] * const sinkEffect = Effect.succeed( - * Sink.forEach((item: number) => Console.log(`Item: ${item}`)) + * Sink.forEach((item: number) => Effect.sync(() => processed.push(item))) * ) * const sink = Sink.unwrap(sinkEffect) * * // Use it with a stream * const stream = Stream.make(1, 2, 3) - * const program = Stream.run(stream, sink) - * - * Effect.runPromise(program) - * // Output: - * // Item: 1 - * // Item: 2 - * // Item: 3 + * await Effect.runPromise(Stream.run(stream, sink)) + * processed // => [1, 2, 3] * ``` * * @category constructors @@ -1933,7 +1948,7 @@ export const summarized: { export const withDuration = ( self: Sink ): Sink<[A, Duration.Duration], In, L, E, R> => - summarized(self, Clock.currentTimeNanos, (start, end) => Duration.nanos(end - start)) + summarized(self, Clock.monotonicTimeNanos, (start, end) => Duration.nanos(end - start)) /** * A sink that drains all input and returns the elapsed duration. @@ -1951,7 +1966,7 @@ export const timed: Sink = map(withDuration(drain), * Services contained in the provided context are removed from the sink's * service requirements. * - * @category services + * @category providing services * @since 2.0.0 */ export const provideContext: { @@ -1980,7 +1995,7 @@ export const provideContext: { * The service identified by `key` is removed from the sink's service * requirements. * - * @category services + * @category providing services * @since 4.0.0 */ export const provideService: { @@ -2089,7 +2104,7 @@ export const catchCause: { const catch_: { ( f: (error: Types.NoInfer) => Effect.Effect - ): (self: Sink) => Sink + ): (self: Sink) => Sink ( self: Sink, f: (error: E) => Effect.Effect @@ -2129,7 +2144,7 @@ export { * The effect receives the sink's `Exit` for the result value. The original * sink result and leftovers are preserved unless the finalizer itself fails. * - * @category Finalization + * @category resource management * @since 4.0.0 */ export const onExit: { @@ -2157,7 +2172,7 @@ export const onExit: { * The original sink result and leftovers are preserved unless the finalizer * itself fails. * - * @category Finalization + * @category resource management * @since 2.0.0 */ export const ensuring: { diff --git a/.context/effect/packages/effect/src/Stdio.ts b/.context/effect/packages/effect/src/Stdio.ts index 899c75e62..d51caa541 100644 --- a/.context/effect/packages/effect/src/Stdio.ts +++ b/.context/effect/packages/effect/src/Stdio.ts @@ -57,12 +57,24 @@ export const TypeId: TypeId = "~effect/Stdio" * standard error, and a stream of standard input bytes. I/O operations can fail * with `PlatformError`. * - * @category models + * @category services * @since 4.0.0 */ export interface Stdio { readonly [TypeId]: TypeId readonly args: Effect.Effect> + /** + * Whether standard input is attached to a terminal. + * + * @since 4.0.0 + */ + readonly stdinIsTerminal: Effect.Effect + /** + * Whether standard output is attached to a terminal. + * + * @since 4.0.0 + */ + readonly stdoutIsTerminal: Effect.Effect stdout(options?: { readonly endOnDone?: boolean | undefined }): Sink.Sink @@ -98,16 +110,23 @@ export const Stdio: Context.Service = Context.Service(TypeI * * **Details** * - * The returned service reuses the supplied fields unchanged and only adds the - * `Stdio` type identifier; it does not create a `Layer` or provide defaults. + * The returned service reuses the supplied fields unchanged and adds the + * `Stdio` type identifier. Omitted terminal-detection fields default to + * effects that succeed with `false`. * * @see {@link layerTest} for a test layer with default fields that can be overridden * * @category constructors * @since 4.0.0 */ -export const make = (options: Omit): Stdio => ({ +export const make = ( + options: + & Omit + & Partial> +): Stdio => ({ [TypeId]: TypeId, + stdinIsTerminal: Effect.succeed(false), + stdoutIsTerminal: Effect.succeed(false), ...options }) @@ -123,7 +142,7 @@ export const make = (options: Omit): Stdio => ({ * * Any provided fields override defaults. By default, arguments are empty, * standard output and error are draining sinks, and standard input is an empty - * stream. + * stream, and terminal-detection effects succeed with `false`. * * @see {@link make} for constructing a `Stdio` service directly without a `Layer` or defaults * diff --git a/.context/effect/packages/effect/src/Stream.ts b/.context/effect/packages/effect/src/Stream.ts index a29269975..4c2f749ac 100644 --- a/.context/effect/packages/effect/src/Stream.ts +++ b/.context/effect/packages/effect/src/Stream.ts @@ -22,6 +22,7 @@ import * as Equal from "./Equal.ts" import * as ExecutionPlan from "./ExecutionPlan.ts" import * as Exit from "./Exit.ts" import * as Fiber from "./Fiber.ts" +import type { SizeInput } from "./FileSystem.ts" import type * as Filter from "./Filter.ts" import type { LazyArg } from "./Function.ts" import { constant, constTrue, constVoid, dual, identity } from "./Function.ts" @@ -103,21 +104,16 @@ export const TypeId: TypeId = "~effect/Stream" * * **Example** (Creating and consuming streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * const program = Effect.gen(function*() { - * yield* Stream.make(1, 2, 3).pipe( + * const values = await Effect.runPromise( + * Stream.make(1, 2, 3).pipe( * Stream.map((n) => n * 2), - * Stream.runForEach((n) => Console.log(n)) + * Stream.runCollect * ) - * }) - * - * Effect.runPromise(program) - * // Output: - * // 2 - * // 4 - * // 6 + * ) + * values // => [2, 4, 6] * ``` * * @category models @@ -155,15 +151,17 @@ export interface StreamUnifyIgnore { * * **Example** (Using the stream type lambda) * - * ```ts - * import type { HKT, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, HKT, Stream } from "effect" * * // Create a Stream type using the type lambda - * type NumberStream = HKT.Kind + * type NumberStream = HKT.Kind * // Equivalent to: Stream + * const stream: NumberStream = Stream.make(1, 2, 3) + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3] * ``` * - * @category type lambdas + * @category utility types * @since 2.0.0 */ export interface StreamTypeLambda extends TypeLambda { @@ -207,12 +205,12 @@ export interface VarianceStruct { * * **Example** (Extracting the success type from a Stream type) * - * ```ts - * import type { Stream } from "effect" + * ```ts import.meta.vitest + * import { Stream } from "effect" * * type NumberStream = Stream.Stream * type SuccessType = Stream.Success - * // SuccessType is number + * const value: SuccessType = 42 * ``` * * @category utility types @@ -225,12 +223,12 @@ export type Success> = [T] extends [Stream * type ErrorType = Stream.Error - * // ErrorType is string + * const error: ErrorType = "boom" * ``` * * @category utility types @@ -243,15 +241,16 @@ export type Error> = [T] extends [Stream unknown * } * type NumberStream = Stream.Stream * type RequiredServices = Stream.Services - * // RequiredServices is { db: Database } + * const services: RequiredServices = { db: { query: (sql) => sql } } + * services.db.query("SELECT 1") // => "SELECT 1" * ``` * * @category utility types @@ -265,20 +264,11 @@ export type Services> = [T] extends [Stream true + * Stream.isStream({ data: [1, 2, 3] }) // => false * ``` * * @category guards @@ -291,15 +281,10 @@ export const isStream = (u: unknown): u is Stream => * * **Example** (Reading the default chunk size) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * yield* Console.log(Stream.DefaultChunkSize) - * }) + * ```ts import.meta.vitest + * import { Stream } from "effect" * - * Effect.runPromise(program) - * // Output: 4096 + * Stream.DefaultChunkSize // => 4096 * ``` * * @category constants @@ -320,17 +305,12 @@ export type HaltStrategy = Channel.HaltStrategy * * **Example** (Creating a stream from an array-emitting channel) * - * ```ts - * import { Channel, Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const channel = Channel.succeed([1, 2, 3] as const) - * const stream = Stream.fromChannel(channel) - * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) - * }) + * ```ts import.meta.vitest + * import { Channel, Effect, Stream } from "effect" * - * // Output: [ 1, 2, 3 ] + * const channel = Channel.succeed([1, 2, 3] as const) + * const stream = Stream.fromChannel(channel) + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3] * ``` * * @category constructors @@ -345,17 +325,11 @@ export const fromChannel: , E, R>( * * **Example** (Creating a stream from an effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const stream = Stream.fromEffect(Effect.succeed(42)) - * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ 42 ] + * const stream = Stream.fromEffect(Effect.succeed(42)) + * await Effect.runPromise(Stream.runCollect(stream)) // => [42] * ``` * * @category constructors @@ -369,7 +343,7 @@ export const fromEffect = (effect: Effect.Effect): Stream(effect: Effect.Effect): Stream greeter.greet("World")) * ) * - * const program = Effect.gen(function*() { - * return yield* stream.pipe( + * await Effect.runPromise( + * stream.pipe( * Stream.provideService(Greeter, { * greet: (name) => `Hello, ${name}!` * }), * Stream.runCollect * ) - * }) - * - * Effect.runPromise(program) - * // Output: [ "Hello, World!" ] + * ) // => ["Hello, World!"] * ``` * - * @category context + * @category accessors * @since 4.0.0 */ export const service = (service: Context.Key): Stream => fromEffect(Effect.service(service)) @@ -409,7 +380,7 @@ export const service = (service: Context.Key): Stream = * * **Example** (Accessing an optional service as a stream) * - * ```ts + * ```ts import.meta.vitest * import { Context, Effect, Option, Stream } from "effect" * * class Greeter extends Context.Service(service: Context.Key): Stream = * ) * ) * - * const program = Effect.gen(function*() { - * return yield* stream.pipe( + * await Effect.runPromise( + * stream.pipe( * Stream.provideService(Greeter, { * greet: (name) => `Hello, ${name}!` * }), * Stream.runCollect * ) - * }) - * - * Effect.runPromise(program) - * // Output: [ "Hello, World!" ] + * ) // => ["Hello, World!"] * ``` * - * @category context + * @category accessors * @since 4.0.0 */ export const serviceOption = (service: Context.Key): Stream> => @@ -449,17 +417,16 @@ export const serviceOption = (service: Context.Key): Stream { + * drained = true + * })).pipe(Stream.runDrain) + * ) + * drained // => true * ``` * * @category constructors @@ -473,19 +440,12 @@ export const fromEffectDrain = (effect: Effect.Effect): Stream * * **Example** (Repeating an effect forever) * - * ```ts - * import { Console, Effect, Random, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const stream = Stream.fromEffectRepeat(Random.nextInt).pipe( - * Stream.take(5) - * ) - * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ 3891571149, 4239494205, 2352981603, 2339111046, 1488052210 ] + * let n = 0 + * const stream = Stream.fromEffectRepeat(Effect.sync(() => ++n)).pipe(Stream.take(5)) + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3, 4, 5] * ``` * * @category constructors @@ -500,20 +460,11 @@ export const fromEffectRepeat = (effect: Effect.Effect): Strea * * **Example** (Repeating an effect with a schedule) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const stream = Stream.fromEffectSchedule( - * Effect.succeed("ping"), - * Schedule.recurs(2) - * ) - * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) - * }) + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ "ping", "ping", "ping" ] + * const stream = Stream.fromEffectSchedule(Effect.succeed("ping"), Schedule.recurs(2)) + * await Effect.runPromise(Stream.runCollect(stream)) // => ["ping", "ping", "ping"] * ``` * * @category constructors @@ -549,19 +500,10 @@ export const fromEffectSchedule = ( * * **Example** (Emitting ticks on an interval) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const ticks = yield* Stream.tick("200 millis").pipe( - * Stream.take(3), - * Stream.runCollect - * ) - * yield* Console.log(ticks) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ undefined, undefined, undefined ] + * await Effect.runPromise(Stream.tick(0).pipe(Stream.take(3), Stream.runCollect)) // => [undefined, undefined, undefined] * ``` * * @category constructors @@ -591,21 +533,19 @@ export const tick = (interval: Duration.Input): Stream => * * **Example** (Creating a stream from a pull effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.scoped( * Effect.gen(function*() { * const source = Stream.make(1, 2, 3) * const pull = yield* Stream.toPull(source) * const stream = Stream.fromPull(Effect.succeed(pull)) - * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * return yield* Stream.runCollect(stream) * }) * ) * - * Effect.runPromise(program) - * // Output: [1, 2, 3] + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category constructors @@ -620,20 +560,14 @@ export const fromPull = ( * * **Example** (Transforming a pull effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * * const transformed = Stream.transformPull(stream, (pull) => Effect.succeed(pull)) * - * const program = Effect.gen(function*() { - * const values = yield* Stream.runCollect(transformed) - * yield* Console.log(values) - * }) - * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(Stream.runCollect(transformed)) // => [1, 2, 3] * ``` * * @category constructors @@ -663,28 +597,23 @@ export const transformPull = ( * * **Example** (Transforming a stream by effectfully transforming its pull effect) * - * ```ts - * import { Console, Effect, Scope, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Scope, Stream } from "effect" * + * const finalized: Array = [] * const stream = Stream.make(1, 2, 3) * * const transformed = Stream.transformPullBracket( * stream, * (pull, _scope, forkedScope) => * Effect.gen(function*() { - * yield* Scope.addFinalizer(forkedScope, Console.log("Releasing scope")) + * yield* Scope.addFinalizer(forkedScope, Effect.sync(() => finalized.push(true))) * return pull * }) * ) * - * const program = Effect.gen(function*() { - * const values = yield* Stream.runCollect(transformed) - * yield* Console.log(values) - * }) - * - * Effect.runPromise(program) - * // Output: [1, 2, 3] - * // Releasing scope + * await Effect.runPromise(Stream.runCollect(transformed)) // => [1, 2, 3] + * finalized // => [true] * ``` * * @category constructors @@ -713,18 +642,12 @@ export const transformPullBracket = ( * * **Example** (Converting a stream to a channel) * - * ```ts - * import { Channel, Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const stream = Stream.make(1, 2, 3) - * const channel = Stream.toChannel(stream) - * const values = yield* Channel.runCollect(channel) - * yield* Console.log(values.flat()) - * }) + * ```ts import.meta.vitest + * import { Channel, Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * const channel = Stream.toChannel(Stream.make(1, 2, 3)) + * const values = await Effect.runPromise(Channel.runCollect(channel)) + * values.flat() // => [1, 2, 3] * ``` * * @category constructors @@ -748,8 +671,8 @@ export const toChannel = ( * * **Example** (Creating a stream from a callback that can emit values into a queue) * - * ```ts - * import { Console, Effect, Queue, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Queue, Stream } from "effect" * * const stream = Stream.callback((queue) => * Effect.sync(() => { @@ -762,13 +685,7 @@ export const toChannel = ( * }) * ) * - * const program = Effect.gen(function*() { - * const values = yield* stream.pipe(Stream.runCollect) - * yield* Console.log(values) - * // [ 1, 2, 3 ] - * }) - * - * Effect.runPromise(program) + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3] * ``` * * @category constructors @@ -787,16 +704,10 @@ export const callback = ( * * **Example** (Creating an empty stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const values = yield* Stream.empty.pipe(Stream.runCollect) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // [] + * await Effect.runPromise(Stream.runCollect(Stream.empty)) // => [] * ``` * * @category constructors @@ -809,16 +720,10 @@ export const empty: Stream = fromChannel(Channel.empty) * * **Example** (Creating a single-valued pure stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const values = yield* Stream.succeed(3).pipe(Stream.runCollect) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // [ 3 ] + * await Effect.runPromise(Stream.runCollect(Stream.succeed(3))) // => [3] * ``` * * @category constructors @@ -831,17 +736,12 @@ export const succeed = (value: A): Stream => fromChannel(Channel.succeed(A * * **Example** (Creating a stream from a sequence of values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * - * const program = Effect.gen(function*() { - * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) // [ 1, 2, 3 ] - * }) - * - * Effect.runPromise(program) + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 2, 3] * ``` * * @category constructors @@ -858,16 +758,10 @@ export const make = >(...values: As): Stream * * **Example** (Evaluating a value synchronously) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const values = yield* Stream.sync(() => 2 + 1).pipe(Stream.runCollect) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ 3 ] + * await Effect.runPromise(Stream.sync(() => 2 + 1).pipe(Stream.runCollect)) // => [3] * ``` * * @category constructors @@ -884,16 +778,10 @@ export const sync = (evaluate: LazyArg): Stream => fromChannel(Channel. * * **Example** (Creating a lazily constructed stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" - * - * const program = Effect.gen(function*() { - * const values = yield* Stream.suspend(() => Stream.make(1, 2, 3)).pipe(Stream.runCollect) - * yield* Console.log(values) - * }) + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(Stream.suspend(() => Stream.make(1, 2, 3)).pipe(Stream.runCollect)) // => [1, 2, 3] * ``` * * @category constructors @@ -907,17 +795,10 @@ export const suspend = (stream: LazyArg>): Stream Exit.fail("Uh oh!") * ``` * * @category constructors @@ -930,19 +811,12 @@ export const fail = (error: E): Stream => fromChannel(Channel.fail( * * **Example** (Failing a stream lazily) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Stream } from "effect" * * const stream = Stream.failSync(() => "Uh oh!") * - * const program = Effect.gen(function*() { - * const exit = yield* Stream.runCollect(stream).pipe(Effect.exit) - * yield* Console.log(exit) - * }) - * - * Effect.runPromise(program) - * // Output: - * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Uh oh!' } } + * await Effect.runPromise(Stream.runCollect(stream).pipe(Effect.exit)) // => Exit.fail("Uh oh!") * ``` * * @category constructors @@ -955,20 +829,14 @@ export const failSync = (evaluate: LazyArg): Stream => fromChann * * **Example** (Failing with a cause) * - * ```ts - * import { Cause, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Stream } from "effect" * * const stream = Stream.failCause(Cause.fail("Database connection failed")).pipe( * Stream.catchCause(() => Stream.succeed("recovered")) * ) * - * const program = Effect.gen(function*() { - * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * // Output: [ "recovered" ] - * }) - * - * Effect.runPromise(program) + * await Effect.runPromise(Stream.runCollect(stream)) // => ["recovered"] * ``` * * @category constructors @@ -981,27 +849,13 @@ export const failCause = (cause: Cause.Cause): Stream => fromCha * * **Example** (Dying with a defect) * - * ```ts - * import { Cause, Console, Effect, Exit, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Stream } from "effect" * * const defect = new Error("Boom") * const stream = Stream.die(defect) * - * const program = Effect.gen(function*() { - * const exit = yield* Effect.exit(Stream.runCollect(stream)) - * const message = Exit.match(exit, { - * onSuccess: () => "Exit.Success", - * onFailure: (cause) => { - * const reason = cause.reasons[0] - * const defect = Cause.isDieReason(reason) ? String(reason.defect) : "Unexpected reason" - * return `Exit.Failure(${defect})` - * } - * }) - * yield* Console.log(message) - * }) - * - * Effect.runPromise(program) - * // Output: Exit.Failure(Error: Boom) + * await Effect.runPromise(Effect.exit(Stream.runCollect(stream))) // => Exit.failCause(Cause.die(defect)) * ``` * * @category constructors @@ -1014,21 +868,14 @@ export const die = (defect: unknown): Stream => fromChannel(Channel.die(d * * **Example** (Failing with a lazy cause) * - * ```ts - * import { Cause, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Stream } from "effect" * * const stream = Stream.failCauseSync(() => * Cause.fail("Connection timeout after retries") * ) * - * const program = Effect.gen(function*() { - * const exit = yield* Stream.runCollect(stream).pipe(Effect.exit) - * yield* Console.log(exit) - * }) - * - * Effect.runPromise(program) - * // Output: - * // { _id: 'Exit', _tag: 'Failure', cause: { _id: 'Cause', _tag: 'Fail', failure: 'Connection timeout after retries' } } + * await Effect.runPromise(Stream.runCollect(stream).pipe(Effect.exit)) // => Exit.fail("Connection timeout after retries") * ``` * * @category constructors @@ -1046,8 +893,8 @@ export const failCauseSync = (evaluate: LazyArg>): Stream(evaluate: LazyArg>): Stream [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1081,19 +927,18 @@ export const fromIteratorSucceed = (iterator: IterableIterator, maxChunkSi * * **Example** (Creating a stream from an iterable) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const numbers = [1, 2, 3] * * const program = Effect.gen(function*() { * const stream = Stream.fromIterable(numbers) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1119,8 +964,8 @@ export const fromIterable = ( * * **Example** (Creating a stream from an iterable effect) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class UserRepo extends Context.Service> @@ -1139,11 +984,10 @@ export const fromIterable = ( * }), * Stream.runCollect * ) - * yield* Console.log(users) + * users // => ["user1", "user2"] * }) * - * Effect.runPromise(program) - * // Output: [ "user1", "user2" ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1157,19 +1001,18 @@ export const fromIterableEffect = (iterable: Effect.Effect, * * **Example** (Repeating an iterable effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.fromIterableEffectRepeat(Effect.succeed([1, 2])).pipe( * Stream.take(5) * ) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 1, 2, 1] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 1, 2, 1 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1184,17 +1027,16 @@ export const fromIterableEffectRepeat = ( * * **Example** (Creating a stream from an array of values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.fromArray([1, 2, 3]) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1213,17 +1055,16 @@ export const fromArray = (array: ReadonlyArray): Stream => * * **Example** (Creating a stream from an effect that produces an array of values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.fromArrayEffect(Effect.succeed(["Ada", "Grace"])) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => ["Ada", "Grace"] * }) * - * Effect.runPromise(program) - * // Output: [ "Ada", "Grace" ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1238,17 +1079,16 @@ export const fromArrayEffect = ( * * **Example** (Creating a stream from an arbitrary number of arrays) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.fromArrays([1, 2], [3, 4]) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3, 4] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1268,23 +1108,22 @@ export const fromArrays = >>( * * **Example** (Creating a stream from a queue of values) * - * ```ts - * import { Console, Effect, Queue, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Queue, Stream } from "effect" * * const program = Effect.gen(function*() { - * const queue = yield* Queue.unbounded() + * const queue = yield* Queue.unbounded() * yield* Queue.offer(queue, 1) * yield* Queue.offer(queue, 2) * yield* Queue.offer(queue, 3) - * yield* Queue.shutdown(queue) + * yield* Queue.end(queue) * * const stream = Stream.fromQueue(queue) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1298,11 +1137,11 @@ export const fromQueue = (queue: Queue.Dequeue): Stream() + * const pubsub = yield* PubSub.unbounded({ replay: 3 }) * * const fiber = yield* Stream.fromPubSub(pubsub).pipe( * Stream.take(3), @@ -1315,11 +1154,10 @@ export const fromQueue = (queue: Queue.Dequeue): Stream [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1336,8 +1174,8 @@ export const fromPubSub = (pubsub: PubSub.PubSub): Stream => fromChanne * * **Example** (Creating a stream from PubSub takes) * - * ```ts - * import { Console, Effect, Exit, PubSub, Stream, Take } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, PubSub, Stream, Take } from "effect" * * const program = Effect.gen(function*() { * const pubsub = yield* PubSub.unbounded>({ @@ -1349,11 +1187,10 @@ export const fromPubSub = (pubsub: PubSub.PubSub): Stream => fromChanne * yield* PubSub.publish(pubsub, Exit.succeed(undefined)) * * const values = yield* Stream.fromPubSubTake(pubsub).pipe(Stream.runCollect) - * yield* Console.log(values) + * values // => [1, 2] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1373,8 +1210,8 @@ export const fromPubSubTake = (pubsub: PubSub.PubSub>): St * * **Example** (Creating a stream from a ReadableStream) * - * ```ts - * import { Console, Data, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Stream } from "effect" * * class StreamError extends Data.TaggedError("StreamError")<{ readonly cause: unknown }> {} * @@ -1393,11 +1230,10 @@ export const fromPubSubTake = (pubsub: PubSub.PubSub>): St * onError: (cause) => new StreamError({ cause }) * }) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1409,30 +1245,14 @@ export const fromReadableStream = ( readonly onError: (error: unknown) => E readonly releaseLockOnEnd?: boolean | undefined } -): Stream => - fromChannel(Channel.fromTransform(Effect.fnUntraced(function*(_, scope) { - const reader = options.evaluate().getReader() - yield* Scope.addFinalizer( - scope, - options.releaseLockOnEnd - ? Effect.sync(() => reader.releaseLock()) - : Effect.promise(() => reader.cancel().catch(constVoid)) - ) - return Effect.flatMap( - Effect.tryPromise({ - try: () => reader.read(), - catch: (reason) => options.onError(reason) - }), - ({ done, value }) => done ? Cause.done() : Effect.succeed(Arr.of(value)) - ) - }))) +): Stream => fromChannel(Channel.fromReadableStream(options)) /** * Creates a stream from an AsyncIterable. * * **Example** (Creating a stream from an AsyncIterable) * - * ```ts + * ```ts import.meta.vitest * import { Data, Effect, Stream } from "effect" * * class StreamError extends Data.TaggedError("StreamError")<{ readonly cause: unknown }> {} @@ -1443,13 +1263,12 @@ export const fromReadableStream = ( * yield 3 * })() * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const stream = Stream.fromAsyncIterable(iterable, (cause) => new StreamError({ cause })) * const values = yield* Stream.runCollect(stream) - * yield* Effect.sync(() => console.log(values)) + * values // => [1, 2, 3] * })) * - * // [ 1, 2, 3 ] * ``` * * @category constructors @@ -1466,20 +1285,17 @@ export const fromAsyncIterable = ( * * **Example** (Creating a stream from a schedule) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function*() { - * const schedule = Schedule.spaced("50 millis").pipe( - * Schedule.upTo({ times: 3 }) - * ) + * const schedule = Schedule.recurs(3) * const stream = Stream.fromSchedule(schedule) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [0, 1, 2] * }) * - * Effect.runPromise(program) - * // Output: [ 0, 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1504,8 +1320,8 @@ export const fromSchedule = (schedule: Schedule.Schedule() @@ -1516,11 +1332,10 @@ export const fromSchedule = (schedule: Schedule.Schedule [1, 2] * })) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1560,7 +1375,7 @@ export interface EventListener { * * **Example** (Creating a stream from an event listener) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * * class NumberTarget implements Stream.EventListener { @@ -1574,15 +1389,14 @@ export interface EventListener { * removeEventListener(_event: string, _f: (event: number) => void) {} * } * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const stream = Stream.fromEventListener(new NumberTarget(), "data").pipe( * Stream.take(3) * ) * const values = yield* Stream.runCollect(stream) - * yield* Effect.sync(() => console.log(values)) + * values // => [1, 2, 3] * })) * - * // [ 1, 2, 3 ] * ``` * * @category constructors @@ -1619,17 +1433,16 @@ export const fromEventListener = ( * * **Example** (Unfolding stream state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.unfold(1, (n) => Effect.succeed([n, n + 1] as const)) * const values = yield* Stream.runCollect(stream.pipe(Stream.take(5))) - * yield* Console.log(values) + * values // => [ 1, 2, 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1663,8 +1476,8 @@ export const unfold = ( * * **Example** (Paginating stream state) * - * ```ts - * import { Console, Effect, Option, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Stream } from "effect" * * const stream = Stream.paginate(0, (n: number) => * Effect.succeed( @@ -1674,8 +1487,7 @@ export const unfold = ( * ] as const * )) * - * Effect.runPromise(Stream.runCollect(stream)).then(console.log) - * // Output: [ 0, 1, 2, 3 ] + * await Effect.runPromise(Stream.runCollect(stream)) // => [0, 1, 2, 3] * ``` * * @category constructors @@ -1709,18 +1521,17 @@ export const paginate = ( * * **Example** (Iterating from a seed value) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.iterate(1, (n) => n + 1).pipe(Stream.take(3)) * * const program = Effect.gen(function* () { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1739,16 +1550,15 @@ export const iterate = (value: A, next: (value: A) => A): Stream => * * **Example** (Creating a numeric range) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.range(1, 5).pipe(Stream.runCollect) - * yield* Console.log(values) + * values // => [ 1, 2, 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1760,14 +1570,15 @@ export const range = ( chunkSize = Channel.DefaultChunkSize ): Stream => min > max ? empty : fromPull(Effect.sync(() => { + const size = Math.max(1, chunkSize) let start = min let done = false return Effect.suspend(() => { if (done) return Cause.done() const remaining = max - start + 1 - if (remaining > chunkSize) { - const chunk = Arr.range(start, start + chunkSize - 1) - start += chunkSize + if (remaining > size) { + const chunk = Arr.range(start, start + size - 1) + start += size return Effect.succeed(chunk) } const chunk = Arr.range(start, start + remaining - 1) @@ -1781,7 +1592,7 @@ export const range = ( * * **Example** (Creating a never-ending stream) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * * const program = Stream.never.pipe( @@ -1789,8 +1600,7 @@ export const range = ( * Stream.runCollect * ) * - * Effect.runPromise(program).then(console.log) - * // [] + * await Effect.runPromise(program) // => [] * ``` * * @category constructors @@ -1803,8 +1613,8 @@ export const never: Stream = fromChannel(Channel.never) * * **Example** (Unwrapping a stream effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const effect = Effect.succeed(Stream.make(1, 2, 3)) * @@ -1812,9 +1622,9 @@ export const never: Stream = fromChannel(Channel.never) * * const program = Effect.gen(function*() { * const chunk = yield* Stream.runCollect(stream) - * yield* Console.log(chunk) + * chunk // => [ 1, 2, 3 ] * }) - * // [1, 2, 3] + * await Effect.runPromise(program) * ``` * * @category constructors @@ -1830,22 +1640,24 @@ export const unwrap = ( * * **Example** (Scoping a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const stream = Stream.scoped( * Stream.fromEffect( * Effect.acquireRelease( - * Console.log("acquire").pipe(Effect.as("resource")), - * () => Console.log("release") + * Effect.sync(() => { + * events.push("acquire") + * return "resource" + * }), + * () => Effect.sync(() => events.push("release")) * ) * ) * ) * - * Effect.runPromise(Stream.runCollect(stream)).then(console.log) - * // acquire - * // release - * // [ "resource" ] + * await Effect.runPromise(Stream.runCollect(stream)) // => ["resource"] + * events // => ["acquire", "release"] * ``` * * @category constructors @@ -1860,16 +1672,11 @@ export const scoped = ( * * **Example** (Mapping stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Stream } from "effect" * * const stream = Stream.fromArray([1, 2, 3]).pipe(Stream.map((n, i) => n + i)) - * const program = Stream.runCollect(stream).pipe( - * Effect.tap((values) => Console.log(values)) - * ) - * - * Effect.runPromise(program) - * // [ 1, 3, 5 ] + * await Effect.runPromise(Stream.runCollect(stream)) // => [1, 3, 5] * ``` * * @category mapping @@ -1892,8 +1699,8 @@ export const map: { * * **Example** (Mapping both the failure and success channels of a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const mapper = { * onFailure: (error: string) => `error: ${error}`, @@ -1905,19 +1712,17 @@ export const map: { * Stream.mapBoth(mapper), * Stream.runCollect * ) - * yield* Console.log(success) + * success // => [ 2, 4 ] * * const failure = yield* Stream.fail("boom").pipe( * Stream.mapBoth(mapper), * Stream.catch((error: string) => Stream.succeed(error)), * Stream.runCollect * ) - * yield* Console.log(failure) + * failure // => [ 'error: boom' ] * }) * - * Effect.runPromise(program) - * // Output: [ 2, 4 ] - * // Output: [ "error: boom" ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -1945,8 +1750,8 @@ export const mapBoth: { * * **Example** (Mapping stream chunks) * - * ```ts - * import { Array, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3, 4).pipe( @@ -1954,11 +1759,10 @@ export const mapBoth: { * Stream.mapArray((chunk, index) => Array.map(chunk, (n) => n + index)), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 1, 2, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -1987,15 +1791,16 @@ export const mapArray: { * * **Example** (Effectfully mapping stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const stream = Stream.make(1, 2, 3) * * const mappedStream = stream.pipe( * Stream.mapEffect((n) => - * Effect.gen(function*() { - * yield* Console.log(`Processing: ${n}`) + * Effect.sync(() => { + * events.push(`Processing: ${n}`) * return n * 2 * }) * ) @@ -2003,15 +1808,11 @@ export const mapArray: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(mappedStream) - * yield* Console.log(result) + * result // => [2, 4, 6] * }) * - * Effect.runPromise(program) - * // Output: - * // Processing: 1 - * // Processing: 2 - * // Processing: 3 - * // [2, 4, 6] + * await Effect.runPromise(program) + * events // => ["Processing: 1", "Processing: 2", "Processing: 3"] * ``` * * @category mapping @@ -2058,18 +1859,17 @@ export const mapEffect: { * * **Example** (Flattening a stream of Effect values into a stream of their results) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(Effect.succeed(1), Effect.succeed(2), Effect.succeed(3)) * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream.pipe(Stream.flattenEffect())) - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [1, 2, 3] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -2112,8 +1912,8 @@ export const flattenEffect: < * * **Example** (Effectfully mapping stream chunks) * - * ```ts - * import { Array, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.fromArray([1, 2, 3, 4]).pipe( @@ -2123,11 +1923,10 @@ export const flattenEffect: < * ), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 1, 2, 13, 14 ] * }) * - * Effect.runPromise(program) - * // Output: [1, 2, 13, 14] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -2155,8 +1954,8 @@ export const mapArrayEffect: { * * **Example** (Converting failures to results) * - * ```ts - * import { Console, Effect, Result, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Result, Stream } from "effect" * * const program = Effect.gen(function*() { * const results = yield* Stream.make(1, 2).pipe( @@ -2168,11 +1967,10 @@ export const mapArrayEffect: { * })), * Stream.runCollect * ) - * yield* Console.log(results) + * results // => [ 'success: 1', 'success: 2', 'failure: boom' ] * }) * - * Effect.runPromise(program) - * // Output: [ "success: 1", "success: 2", "failure: boom" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -2189,29 +1987,23 @@ export const result = (self: Stream): Stream = [] * const program = Effect.gen(function*() { * const result = yield* Stream.fromArray([1, 2, 3]).pipe( - * Stream.tap((n) => Console.log(`before mapping: ${n}`)), + * Stream.tap((n) => Effect.sync(() => events.push(`before mapping: ${n}`))), * Stream.map((n) => n * 2), - * Stream.tap((n) => Console.log(`after mapping: ${n}`)), + * Stream.tap((n) => Effect.sync(() => events.push(`after mapping: ${n}`))), * Stream.runCollect * ) * - * yield* Console.log(result) + * result // => [2, 4, 6] * }) * - * Effect.runPromise(program) - * // Output: - * // before mapping: 1 - * // after mapping: 2 - * // before mapping: 2 - * // after mapping: 4 - * // before mapping: 3 - * // after mapping: 6 - * // [ 2, 4, 6 ] + * await Effect.runPromise(program) + * events // => ["before mapping: 1", "after mapping: 2", "before mapping: 2", "after mapping: 4", "before mapping: 3", "after mapping: 6"] * ``` * * @category sequencing @@ -2249,28 +2041,25 @@ export const tap: { * * **Example** (Tapping values and errors) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const program = Effect.gen(function*() { * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail("boom")), * Stream.tapBoth({ - * onElement: (value) => Console.log(`seen: ${value}`), - * onError: (error) => Console.log(`error: ${error}`) + * onElement: (value) => Effect.sync(() => events.push(`seen: ${value}`)), + * onError: (error) => Effect.sync(() => events.push(`error: ${error}`)) * }), * Stream.catch(() => Stream.make(3)) * ) * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: - * // seen: 1 - * // seen: 2 - * // error: boom - * // [ 1, 2, 3 ] + * await Effect.runPromise(program) + * events // => ["seen: 1", "seen: 2", "error: boom"] * ``` * * @category sequencing @@ -2310,8 +2099,8 @@ export const tapBoth: { * * **Example** (Tapping values with a sink) * - * ```ts - * import { Console, Effect, Ref, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Ref, Sink, Stream } from "effect" * * const program = Effect.gen(function*() { * const seen = yield* Ref.make>([]) @@ -2323,13 +2112,11 @@ export const tapBoth: { * Stream.runCollect * ) * const tapped = yield* Ref.get(seen) - * yield* Console.log(tapped) - * yield* Console.log(result) + * tapped // => [ 1, 2, 3 ] + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [1, 2, 3] - * // Output: [1, 2, 3] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2415,19 +2202,18 @@ export const tapSink: { * * **Example** (Flat mapping stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( * Stream.flatMap((n) => Stream.make(n, n * 2)), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2, 2, 4, 3, 6 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 2, 4, 3, 6 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -2469,19 +2255,18 @@ export const flatMap: { * * **Example** (Switching to the latest stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Stream.make(1, 2, 3).pipe( * Stream.switchMap((n) => (n === 3 ? Stream.make(n) : Stream.never)), * Stream.runCollect * ) * - * Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const result = yield* program - * yield* Console.log(result) - * // Output: [ 3 ] - * }) + * result // => [ 3 ] + * })) * ``` * * @category sequencing @@ -2529,8 +2314,8 @@ export const switchMap: { * * **Example** (Flattening nested streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const streamOfStreams = Stream.make( * Stream.make(1, 2), @@ -2540,11 +2325,10 @@ export const switchMap: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(Stream.flatten(streamOfStreams)) - * yield* Console.log(values) + * values // => [ 1, 2, 3, 4, 5, 6 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4, 5, 6 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -2581,18 +2365,17 @@ export const flatten: < * * **Example** (Flattening a stream of non-empty arrays into a stream of elements) * - * ```ts - * import { Array, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Effect, Stream } from "effect" * * const stream = Stream.make(Array.make(1, 2), Array.make(3)) * * const program = Effect.gen(function* () { * const result = yield* Stream.runCollect(Stream.flattenArray(stream)) - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2606,16 +2389,15 @@ export const flattenArray = (self: Stream, * * **Example** (Draining stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.range(1, 6).pipe(Stream.drain, Stream.runCollect) - * yield* Console.log(result) + * result // => [] * }) * - * Effect.runPromise(program) - * // Output: [] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2629,23 +2411,23 @@ export const drain = (self: Stream): Stream => fr * * **Example** (Draining a stream in the background) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const foreground = Stream.make(1, 2) - * const background = Stream.fromEffect(Console.log("background task")) + * const background = Stream.fromEffect(Effect.sync(() => events.push("background task"))) * * const program = Effect.gen(function*() { * const values = yield* foreground.pipe( * Stream.drainFork(background), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [1, 2] * }) * - * Effect.runPromise(program) - * // Output: background task - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) + * events // => ["background task"] * ``` * * @category sequencing @@ -2665,8 +2447,8 @@ export const drainFork: { * * **Example** (Repeating a stream on a schedule) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function* () { * const result = yield* Stream.make(1).pipe( @@ -2674,11 +2456,10 @@ export const drainFork: { * Stream.runCollect * ) * - * yield* Console.log(result) + * result // => [ 1, 1, 1, 1, 1 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 1, 1, 1, 1 ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2714,20 +2495,19 @@ export const repeat: { * * **Example** (Scheduling stream elements) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3).pipe( - * Stream.schedule(Schedule.spaced("10 millis")), + * Stream.schedule(Schedule.recurs(3)), * Stream.runCollect * ) * - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category rate limiting @@ -2757,8 +2537,8 @@ export const schedule: { * * **Example** (Timing out a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1).pipe( @@ -2766,14 +2546,13 @@ export const schedule: { * Stream.timeout("1 second"), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1 ] + * await Effect.runPromise(program) * ``` * - * @category rate limiting + * @category delays & timeouts * @since 2.0.0 */ export const timeout: { @@ -2809,7 +2588,7 @@ export const timeout: { * * @see {@link timeout} for ending the stream instead of switching to a fallback stream * - * @category rate limiting + * @category delays & timeouts * @since 4.0.0 */ export const timeoutOrElse: { @@ -2887,19 +2666,18 @@ export const timeoutOrElse: { * * **Example** (Repeating stream elements) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make("A", "B", "C").pipe( * Stream.repeatElements(Schedule.recurs(1)), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 'A', 'A', 'B', 'B', 'C', 'C' ] * }) * - * Effect.runPromise(program) - * // Output: [ "A", "A", "B", "B", "C", "C" ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2955,8 +2733,8 @@ export const repeatElements: { * * **Example** (Repeating a stream forever) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make("A", "B").pipe( * Stream.forever, @@ -2965,11 +2743,10 @@ export const repeatElements: { * * const program = Effect.gen(function*() { * const output = yield* Stream.runCollect(stream) - * yield* Console.log(output) + * output // => [ 'A', 'B', 'A', 'B', 'A' ] * }) * - * Effect.runPromise(program) - * // Output: [ "A", "B", "A", "B", "A" ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -2982,17 +2759,16 @@ export const forever = (self: Stream): Stream => from * * **Example** (Flattening iterable values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.make([1, 2], [3, 4]).pipe(Stream.flattenIterable) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 1, 2, 3, 4 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -3007,8 +2783,8 @@ export const flattenIterable = (self: Stream, E, R>): Strea * * **Example** (Flattening Take values) * - * ```ts - * import { Array, Console, Effect, Exit, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Effect, Exit, Stream } from "effect" * * const program = Effect.gen(function*() { * const takes = Stream.make( @@ -3018,11 +2794,10 @@ export const flattenIterable = (self: Stream, E, R>): Strea * ) * * const values = yield* Stream.flattenTake(takes).pipe(Stream.runCollect) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -3041,16 +2816,15 @@ export const flattenTake = (self: Stream, E2, R>): * * **Example** (Concatenating streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.concat(Stream.make(1, 2, 3), Stream.make(4, 5, 6)) * - * Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * }) - * // Output: [ 1, 2, 3, 4, 5, 6 ] + * values // => [ 1, 2, 3, 4, 5, 6 ] + * })) * ``` * * @category sequencing @@ -3070,8 +2844,8 @@ export const concat: { * * **Example** (Prepending values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(3, 4).pipe( @@ -3079,11 +2853,10 @@ export const concat: { * Stream.runCollect * ) * - * yield* Console.log(values) - * // Output: [ 1, 2, 3, 4 ] + * values // => [ 1, 2, 3, 4 ] * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -3107,19 +2880,18 @@ export const prepend: { * * **Example** (Merging stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const fast = Stream.make(1, 2, 3) * const slow = Stream.fromEffect(Effect.delay(Effect.succeed(4), "50 millis")) * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(Stream.merge(fast, slow)) - * yield* Console.log(result) + * result // => [ 1, 2, 3, 4 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3, 4 ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -3165,21 +2937,21 @@ export const merge: { * * **Example** (Merging with a background effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( - * Stream.mergeEffect(Console.log("side task")), + * Stream.mergeEffect(Effect.sync(() => events.push("side task"))), * Stream.runCollect * ) * - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: side task - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) + * events // => ["side task"] * ``` * * @category merging @@ -3209,8 +2981,8 @@ export const mergeEffect: { * * **Example** (Merging streams into results) * - * ```ts - * import { Console, Effect, Result, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Result, Stream } from "effect" * * const left = Stream.fromEffect(Effect.succeed("left")) * const right = Stream.fromEffect(Effect.delay(Effect.succeed("right"), "10 millis")) @@ -3227,11 +2999,10 @@ export const mergeEffect: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(merged) - * yield* Console.log(result) + * result // => [ 'left:left', 'right:right' ] * }) * - * Effect.runPromise(program) - * // Output: [ "left:left", "right:right" ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -3270,18 +3041,17 @@ export const mergeResult: { * * **Example** (Merging streams while keeping left values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const left = Stream.make(1, 2) * const right = Stream.make("a", "b") * const values = yield* left.pipe(Stream.mergeLeft(right), Stream.runCollect) - * yield* Console.log(values) + * values // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -3312,8 +3082,8 @@ export const mergeLeft: { * * **Example** (Merging streams while keeping right values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const left = Stream.make("left-1", "left-2").pipe( * Stream.tap(() => Effect.sync(() => undefined)) @@ -3324,11 +3094,10 @@ export const mergeLeft: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(merged) - * yield* Console.log(result) + * result // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -3359,8 +3128,8 @@ export const mergeRight: { * * **Example** (Merging streams with bounded concurrency) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const streams = [ * Stream.fromEffect(Effect.delay(Effect.succeed("A"), "20 millis")), @@ -3371,11 +3140,10 @@ export const mergeRight: { * const values = yield* Stream.mergeAll(streams, { concurrency: 2 }).pipe( * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 'B', 'A' ] * }) * - * Effect.runPromise(program) - * // Output: [ "B", "A" ] + * await Effect.runPromise(program) * ``` * * @see {@link merge} for merging exactly two streams and choosing a halt strategy @@ -3416,18 +3184,17 @@ export const mergeAll: { * * **Example** (Computing cartesian products) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const left = Stream.make(1, 2) * const right = Stream.make("a", "b") * const values = yield* Stream.runCollect(Stream.cross(left, right)) - * yield* Console.log(values) + * values // => [ [ 1, 'a' ], [ 1, 'b' ], [ 2, 'a' ], [ 2, 'b' ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, "a" ], [ 1, "b" ], [ 2, "a" ], [ 2, "b" ] ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3452,19 +3219,18 @@ export const cross: { * * **Example** (Combining cartesian products) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const left = Stream.make(1, 2) * const right = Stream.make("a", "b") * const combined = Stream.crossWith(left, right, (n, s) => `${n}-${s}`) * const result = yield* Stream.runCollect(combined) - * yield* Console.log(result) + * result // => [ '1-a', '1-b', '2-a', '2-b' ] * }) * - * Effect.runPromise(program) - * // Output: [ "1-a", "1-b", "2-a", "2-b" ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3491,8 +3257,8 @@ export const crossWith: { * * **Example** (Zipping streams with a function) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream1 = Stream.make(1, 2, 3, 4, 5, 6) * const stream2 = Stream.make("a", "b", "c") @@ -3501,11 +3267,10 @@ export const crossWith: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(zipped) - * yield* Console.log(result) + * result // => [ '1-a', '2-b', '3-c' ] * }) * - * Effect.runPromise(program) - * // Output: [ "1-a", "2-b", "3-c" ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3553,8 +3318,8 @@ const zipArrays = ( * * **Example** (Zipping stream chunks) * - * ```ts - * import { Array, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Effect, Stream } from "effect" * * const left = Stream.fromArrays([1, 2, 3], [4, 5]) * const right = Stream.fromArrays(["a", "b"], ["c", "d", "e"]) @@ -3568,11 +3333,10 @@ const zipArrays = ( * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(zipped) - * yield* Console.log(result) + * result // => [ [ 1, 'a' ], [ 2, 'b' ], [ 3, 'c' ], [ 4, 'd' ], [ 5, 'e' ] ] * }) * - * Effect.runPromise(program) - * // Output: [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, "e"]] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3662,8 +3426,8 @@ export const zipWithArray: { * * **Example** (Zipping streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream1 = Stream.make(1, 2, 3) * const stream2 = Stream.make("a", "b", "c") @@ -3672,11 +3436,10 @@ export const zipWithArray: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(zipped) - * yield* Console.log(result) + * result // => [ [ 1, 'a' ], [ 2, 'b' ], [ 3, 'c' ] ] * }) * - * Effect.runPromise(program) - * // Output: [[1, "a"], [2, "b"], [3, "c"]] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3703,19 +3466,18 @@ export const zip: { * * **Example** (Zipping streams while keeping left values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream1 = Stream.make(1, 2, 3, 4) * const stream2 = Stream.make("a", "b") * * const program = Effect.gen(function*() { * const result = yield* Stream.zipLeft(stream1, stream2).pipe(Stream.runCollect) - * yield* Console.log(result) + * result // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [1, 2] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3745,19 +3507,18 @@ export const zipLeft: { * * **Example** (Zipping streams while keeping right values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream1 = Stream.make(1, 2) * const stream2 = Stream.make("a", "b", "c", "d") * * const program = Effect.gen(function*() { * const result = yield* Stream.zipRight(stream1, stream2).pipe(Stream.runCollect) - * yield* Console.log(result) + * result // => [ 'a', 'b' ] * }) * - * Effect.runPromise(program) - * // Output: ["a", "b"] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3792,8 +3553,8 @@ export const zipRight: { * * **Example** (Zipping and flattening tuples) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream1 = Stream.make( @@ -3804,11 +3565,10 @@ export const zipRight: { * const stream2 = Stream.make("x", "y", "z") * const result = yield* Stream.zipFlatten(stream1, stream2).pipe(Stream.runCollect) * - * yield* Console.log(result) + * result // => [ [ 1, 'a', 'x' ], [ 2, 'b', 'y' ], [ 3, 'c', 'z' ] ] * }) * - * Effect.runPromise(program) - * // Output: [[1, "a", "x"], [2, "b", "y"], [3, "c", "z"]] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3835,19 +3595,18 @@ export const zipFlatten: { * * **Example** (Zipping elements with indices) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const indexed = yield* Stream.make("a", "b", "c", "d").pipe( * Stream.zipWithIndex, * Stream.runCollect * ) - * yield* Console.log(indexed) + * indexed // => [ [ 'a', 0 ], [ 'b', 1 ], [ 'c', 2 ], [ 'd', 3 ] ] * }) * - * Effect.runPromise(program) - * // Output: [["a", 0], ["b", 1], ["c", 2], ["d", 3]] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3861,21 +3620,15 @@ export const zipWithIndex = (self: Stream): Stream<[A, number] * * **Example** (Zipping elements with next values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Stream } from "effect" * * const stream = Stream.zipWithNext(Stream.make(1, 2, 3, 4)) * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [[1, Option.some(2)], [2, Option.some(3)], [3, Option.some(4)], [4, Option.none()]] * })) - * // Output: [ - * // [ 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' } ] - * // ] * ``` * * @category zipping @@ -3906,23 +3659,17 @@ export const zipWithNext = (self: Stream): Stream<[A, Option.O * * **Example** (Zipping elements with previous values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Stream } from "effect" * * const stream = Stream.zipWithPrevious(Stream.make(1, 2, 3, 4)) * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [[Option.none(), 1], [Option.some(1), 2], [Option.some(2), 3], [Option.some(3), 4]] * }) * - * Effect.runPromise(program) - * // Output: [ - * // [ { _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 ] - * // ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -3944,7 +3691,7 @@ export const zipWithPrevious = (self: Stream): Stream<[Option. * * **Example** (Zipping elements with neighbors) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect, Option, Stream } from "effect" * * const program = Effect.gen(function*() { @@ -3952,11 +3699,10 @@ export const zipWithPrevious = (self: Stream): Stream<[Option. * Stream.zipWithPreviousAndNext, * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [[Option.none(), 1, Option.some(2)], [Option.some(1), 2, Option.some(3)], [Option.some(2), 3, Option.none()]] * }) * - * Effect.runPromise(program) - * // Output: [ [Option.none(), 1, Option.some(2)], [Option.some(1), 2, Option.some(3)], [Option.some(2), 3, Option.none()] ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -4010,8 +3756,8 @@ export const zipWithPreviousAndNext = ( * * **Example** (Zipping latest values from many streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.zipLatestAll( * Stream.make(1, 2, 3).pipe(Stream.rechunk(1)), @@ -4021,11 +3767,10 @@ export const zipWithPreviousAndNext = ( * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [[1, "a", true], [2, "a", true], [2, "b", true], [2, "b", false], [3, "b", false], [3, "c", false], [3, "c", true]] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, "a", true ], [ 2, "a", true ], [ 3, "a", true ], [ 3, "b", true ], [ 3, "c", true ], [ 3, "c", false ], [ 3, "c", true ] ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -4086,8 +3831,8 @@ export const zipLatestAll = >>( * * **Example** (Zipping latest values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.zipLatest( @@ -4095,9 +3840,9 @@ export const zipLatestAll = >>( * Stream.make("a") * ).pipe(Stream.runCollect) * - * yield* Console.log(result) + * result // => [ [ 1, 'a' ] ] * }) - * // Output: [ [1, "a"] ] + * await Effect.runPromise(program) * ``` * * @category zipping @@ -4136,10 +3881,10 @@ export const zipLatest: { * * **Example** (Zipping latest values with a function) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3).pipe( * Stream.rechunk(1), * Stream.zipLatestWith( @@ -4149,9 +3894,8 @@ export const zipLatest: { * Stream.runCollect * ) * - * yield* Console.log(result) - * // Output: [ 11, 12, 22, 23 ] - * }) + * result // => [ 11, 12, 22, 23 ] + * })) * ``` * * @category zipping @@ -4188,19 +3932,18 @@ export const zipLatestWith: { * * **Example** (Racing multiple streams) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.raceAll( - * Stream.fromSchedule(Schedule.spaced("1 second")), + * Stream.empty, * Stream.make(0, 1, 2) * ).pipe(Stream.runCollect) - * yield* Console.log(result) + * result // => [ 0, 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 0, 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category racing @@ -4247,21 +3990,20 @@ export const raceAll = >>( * * **Example** (Racing two streams) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.race( - * Stream.make(0, 1, 2), - * Stream.fromSchedule(Schedule.spaced("1 second")) + * Stream.empty, + * Stream.make(0, 1, 2) * ) * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 0, 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 0, 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category racing @@ -4285,19 +4027,18 @@ export const race: { * * **Example** (Filtering stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.make(1, 2, 3, 4).pipe( * Stream.filter((n) => n % 2 === 0) * ) * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 2, 4 ] * }) * - * Effect.runPromise(program) - * // Output: [ 2, 4 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -4359,18 +4100,17 @@ export const filterMap: { * * **Example** (Effectfully filtering stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4).pipe(Stream.filterEffect((n) => Effect.succeed(n > 2))) * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 3, 4 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 4 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -4440,8 +4180,8 @@ export const filterMapEffect: { * * **Example** (Partitioning a stream into queues) * - * ```ts - * import { Console, Effect, Result, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Result, Stream } from "effect" * * const program = Effect.gen(function*() { * const [passes, fails] = yield* Stream.make(1, 2, 3, 4).pipe( @@ -4451,13 +4191,11 @@ export const filterMapEffect: { * const passValues = yield* Stream.fromQueue(passes).pipe(Stream.runCollect) * const failValues = yield* Stream.fromQueue(fails).pipe(Stream.runCollect) * - * yield* Console.log(passValues) - * // Output: [ 2, 4 ] - * yield* Console.log(failValues) - * // Output: [ 1, 3 ] + * passValues // => [ 2, 4 ] + * failValues // => [ 1, 3 ] * }) * - * Effect.runPromise(Effect.scoped(program)) + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category filtering @@ -4641,8 +4379,8 @@ export const partitionEffect: { * * **Example** (Partitioning a stream) * - * ```ts - * import { Console, Effect, Result, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Result, Stream } from "effect" * * const program = Effect.gen(function*() { * const [excluded, satisfying] = yield* Stream.partition( @@ -4651,11 +4389,10 @@ export const partitionEffect: { * ) * const left = yield* Stream.runCollect(excluded) * const right = yield* Stream.runCollect(satisfying) - * yield* Console.log(left) - * // Output: [ 1, 3 ] - * yield* Console.log(right) - * // Output: [ 2, 4 ] + * left // => [ 1, 3 ] + * right // => [ 2, 4 ] * }) + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category filtering @@ -4704,18 +4441,17 @@ export const partition: { * * **Example** (Conditionally keeping a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect( * Stream.when(Stream.make(1, 2, 3), Effect.succeed(false)) * ) - * yield* Console.log(result) + * result // => [] * }) * - * Effect.runPromise(program) - * // Output: [] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -4748,8 +4484,8 @@ export const when: { * * **Example** (Peeling a stream with a sink) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * * const stream = Stream.fromArrays([1, 2, 3], [4, 5, 6]) * const sink = Sink.take(3) @@ -4758,12 +4494,11 @@ export const when: { * Effect.gen(function*() { * const [peeled, rest] = yield* Stream.peel(stream, sink) * const remaining = yield* Stream.runCollect(rest) - * yield* Console.log([peeled, remaining]) + * const result = [peeled, remaining] // => [[1, 2, 3], [4, 5, 6]] * }) * ) * - * Effect.runPromise(program) - * // Output: [ [1, 2, 3], [4, 5, 6] ] + * await Effect.runPromise(program) * ``` * * @category destructors @@ -4815,22 +4550,21 @@ export const peel: { * * **Example** (Buffering stream elements) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( * Stream.buffer({ capacity: 1 }), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * - * @category rate limiting + * @category buffering * @since 2.0.0 */ export const buffer: { @@ -4868,21 +4602,21 @@ export const buffer: { * * **Example** (Buffering stream chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.fromArrays([1, 2], [3, 4]).pipe( * Stream.bufferArray({ capacity: 2 }), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 1, 2, 3, 4 ] * }) * - * // Output: [ 1, 2, 3, 4 ] + * await Effect.runPromise(program) * ``` * - * @category rate limiting + * @category buffering * @since 4.0.0 */ export const bufferArray: { @@ -4914,8 +4648,8 @@ export const bufferArray: { * * **Example** (Catching stream causes) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail("Oops!")), @@ -4928,11 +4662,10 @@ export const bufferArray: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(recovered) - * yield* Console.log(values) + * values // => [ 1, 2, 999 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 999 ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -4961,23 +4694,23 @@ export const catchCause: { * * **Example** (Tapping stream causes) * - * ```ts - * import { Cause, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Stream } from "effect" * + * const observations: Array = [] * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail("boom")), - * Stream.tapCause((cause) => Console.log(Cause.isReason(cause))), + * Stream.tapCause((cause) => Effect.sync(() => observations.push(Cause.isReason(cause)))), * Stream.catch(() => Stream.succeed(0)) * ) * * const program = Effect.gen(function* () { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [1, 2, 0] * }) * - * Effect.runPromise(program) - * // Output: true - * // Output: [ 1, 2, 0 ] + * await Effect.runPromise(program) + * observations // => [false] * ``` * * @category error handling @@ -5019,8 +4752,8 @@ export { * * **Example** (Catching stream failures) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail("Oops!")), @@ -5029,11 +4762,10 @@ export { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 1, 2, 999 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 999 ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5047,24 +4779,23 @@ export { * * **Example** (Effectfully peeking at errors) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const errors: Array = [] * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail("boom")), - * Stream.tapError((error) => Console.log(`tapError: ${error}`)), + * Stream.tapError((error) => Effect.sync(() => errors.push(error))), * Stream.catch(() => Stream.make(999)) * ) * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 999] * }) * - * Effect.runPromise(program) - * // Output: - * // tapError: boom - * // [ 1, 2, 999 ] + * await Effect.runPromise(program) + * errors // => ["boom"] * ``` * * @category error handling @@ -5098,8 +4829,8 @@ export const tapError: { * * **Example** (Catching matching failures) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2).pipe( * Stream.concat(Stream.fail(42)), @@ -5111,11 +4842,10 @@ export const tapError: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * // Output: [ 1, 2, 999 ] + * values // => [ 1, 2, 999 ] * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5247,8 +4977,8 @@ export const catchFilter: { * * **Example** (Catching tagged failures) * - * ```ts - * import { Console, Data, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Stream } from "effect" * * class HttpError extends Data.TaggedError("HttpError")<{ message: string }> {} * @@ -5260,11 +4990,10 @@ export const catchFilter: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(recovered) - * yield* Console.log(values) - * // Output: [ "Recovered: timeout" ] + * values // => [ 'Recovered: timeout' ] * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5355,8 +5084,8 @@ export const catchTag: { * * **Example** (Catching tagged failures with handlers) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * class NotFound { * readonly _tag = "NotFound" @@ -5378,10 +5107,10 @@ export const catchTag: { * }), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 'fallback' ] * }) * - * // Output: [ "fallback" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5478,8 +5207,8 @@ export const catchTags: { * * **Example** (Catching a tagged error reason) * - * ```ts - * import { Console, Data, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Stream } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ * retryAfter: number @@ -5504,11 +5233,10 @@ export const catchTags: { * ), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 'retry: 60' ] * }) * - * Effect.runPromise(program) - * // Output: [ "retry: 60" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5613,8 +5341,8 @@ export const catchReason: { * * **Example** (Catching tagged error reasons) * - * ```ts - * import { Console, Data, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Data, Effect, Stream } from "effect" * * class RateLimitError extends Data.TaggedError("RateLimitError")<{ * retryAfter: number @@ -5640,11 +5368,10 @@ export const catchReason: { * }), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 'retry: 60' ] * }) * - * Effect.runPromise(program) - * // Output: [ "retry: 60" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5733,7 +5460,8 @@ export const catchReasons: { }[keyof Cases] > } = dual((args) => isStream(args[0]), (self, errorTag, cases, orElse) => { - const handlers: Record Channel.Channel> = {} + const handlers: Record Channel.Channel> = + Object.create(null) for (const key of Object.keys(cases)) { const handler = (cases as any)[key] handlers[key] = (reason, error) => handler(reason, error).channel @@ -5757,8 +5485,8 @@ export const catchReasons: { * * **Example** (Mapping stream errors) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.fail("bad").pipe( @@ -5766,11 +5494,10 @@ export const catchReasons: { * Stream.catch((error) => Stream.make(`recovered from ${error}`)), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 'recovered from mapped: bad' ] * }) * - * Effect.runPromise(program) - * // Output: [ "recovered from mapped: bad" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5790,8 +5517,8 @@ export const mapError: { * * **Example** (Catching matching causes) * - * ```ts - * import { Cause, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const failingStream = Stream.fail("NetworkError") @@ -5802,11 +5529,10 @@ export const mapError: { * ) * * const output = yield* Stream.runCollect(recovered) - * yield* Console.log(output) + * output // => [ 'Recovered: NetworkError' ] * }) * - * Effect.runPromise(program) - * // Output: [ "Recovered: NetworkError" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5890,19 +5616,18 @@ export const catchCauseFilter: { * * **Example** (Switching on empty streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.empty.pipe( * Stream.orElseIfEmpty(() => Stream.make(1, 2)), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5930,8 +5655,8 @@ export const orElseIfEmpty: { * * **Example** (Recovering with a fallback value) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.fail("NetworkError").pipe( @@ -5939,11 +5664,10 @@ export const orElseIfEmpty: { * ) * * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 'Recovered: NetworkError' ] * }) * - * Effect.runPromise(program) - * // Output: [ "Recovered: NetworkError" ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -5967,8 +5691,8 @@ export const orElseSucceed: { * * **Example** (Turning failures into defects) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( @@ -5976,11 +5700,10 @@ export const orElseSucceed: { * Stream.runCollect * ) * - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -6003,8 +5726,8 @@ export const orDie = (self: Stream): Stream => fr * * **Example** (Ignoring stream failures) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( @@ -6012,27 +5735,25 @@ export const orDie = (self: Stream): Stream => fr * Stream.ignore, * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * **Example** (Configuring ignore logging) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const values = yield* Stream.fail("boom").pipe( * Stream.ignore({ log: false }), * Stream.runCollect * ) - * yield* Effect.sync(() => console.log(values)) + * values // => [] * })) * - * // [] * ``` * * @see {@link ignoreCause} for a variant that also ignores defects, not just typed failures @@ -6070,19 +5791,18 @@ export const ignore: < * * **Example** (Ignoring stream failure causes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const values = yield* Stream.make(1, 2).pipe( * Stream.concat(Stream.die("boom")), * Stream.ignoreCause({ log: false }), * Stream.runCollect * ) - * yield* Effect.sync(() => console.log(values)) + * values // => [1, 2] * })) * - * // [ 1, 2 ] * ``` * * @see {@link ignore} to ignore only typed failures without suppressing defects @@ -6121,8 +5841,8 @@ export const ignoreCause: < * * **Example** (Retrying stream failures) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1).pipe( @@ -6132,11 +5852,10 @@ export const ignoreCause: < * Stream.runCollect * ) * - * yield* Console.log(values) + * values // => [ 1, 1 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 1 ] + * await Effect.runPromise(program) * ``` * * @category error handling @@ -6170,6 +5889,27 @@ export const retry: { ): Stream => fromChannel(Channel.retry(self.channel, policy)) ) +const retryWithoutReset = ( + self: Stream, + schedule: Schedule.Schedule +): Stream => + unwrap(Effect.map(Schedule.toStepWithMetadata(schedule), (step) => { + let meta = Schedule.CurrentMetadata.defaultValue() + const loop = (): Stream => + catch_( + provideServiceEffect(self, Schedule.CurrentMetadata, Effect.sync(() => meta)), + (error) => + unwrap(Pull.catchDone( + Effect.map(step(error), (meta_) => { + meta = meta_ + return unwrap(Effect.as(Effect.yieldNow, loop())) + }), + () => Effect.succeed(fail(error)) + )) + ) + return loop() + })) + /** * Applies an `ExecutionPlan` to a stream, retrying with step-provided resources * until it succeeds or the plan is exhausted. @@ -6180,10 +5920,17 @@ export const retry: { * `preventFallbackOnPartialStream` to fail instead of mixing partial output with * a later fallback. * + * Attempts can be observed from outside the stream by passing + * `options.onEvent`, which receives an `ExecutionPlan.Event` before each + * attempt and after it settles; see `Effect.withExecutionPlan` for the handler + * semantics. When a downstream consumer stops pulling early (for example + * `Stream.take` outside the plan), the truncated attempt reports + * `AttemptSuccess`: the consumer stopped, not the source. + * * **Example** (Applying an execution plan) * - * ```ts - * import { Console, Context, Effect, ExecutionPlan, Layer, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, ExecutionPlan, Layer, Stream } from "effect" * * class Service extends Context.Service()("Service", { * make: Effect.succeed({ @@ -6203,27 +5950,32 @@ export const retry: { * * const program = Effect.gen(function*() { * const items = yield* stream.pipe(Stream.withExecutionPlan(plan), Stream.runCollect) - * yield* Console.log(items) + * items // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category error handling * @since 3.16.0 */ export const withExecutionPlan: { - ( + ( policy: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, - options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } - ): (self: Stream) => Stream> - ( + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + readonly onEvent?: ((event: ExecutionPlan.Event) => Effect.Effect) | undefined + } + ): (self: Stream) => Stream | RX> + ( self: Stream, policy: ExecutionPlan.ExecutionPlan<{ provides: Provides; input: Input; error: PolicyE; requirements: R2 }>, - options?: { readonly preventFallbackOnPartialStream?: boolean | undefined } - ): Stream> -} = dual((args) => isStream(args[0]), ( + options?: { + readonly preventFallbackOnPartialStream?: boolean | undefined + readonly onEvent?: ((event: ExecutionPlan.Event) => Effect.Effect) | undefined + } + ): Stream | RX> +} = dual((args) => isStream(args[0]), ( self: Stream, policy: ExecutionPlan.ExecutionPlan<{ provides: Provides @@ -6233,8 +5985,9 @@ export const withExecutionPlan: { }>, options?: { readonly preventFallbackOnPartialStream?: boolean | undefined + readonly onEvent?: ((event: ExecutionPlan.Event) => Effect.Effect) | undefined } -): Stream> => +): Stream | RX> => suspend(() => { const preventFallbackOnPartialStream = options?.preventFallbackOnPartialStream ?? false let i = 0 @@ -6252,6 +6005,28 @@ export const withExecutionPlan: { return meta }) ) + const emitter = options?.onEvent === undefined + ? undefined + : internalExecutionPlan.makeEventEmitter(options.onEvent, () => meta) + let attemptState: internalExecutionPlan.AttemptState | undefined + const instrument: (attempt: Stream) => Stream = emitter === undefined + ? identity + : (attempt) => + onExit( + onStart( + attempt, + Effect.map(emitter.begin, (state) => { + attemptState = state + }) + ), + (exit) => + Effect.suspend(() => { + if (attemptState === undefined) return Effect.void + const state = attemptState + attemptState = undefined + return emitter.end(state, exit) + }) + ) let lastError = Option.none() const loop: Stream< A, @@ -6263,7 +6038,9 @@ export const withExecutionPlan: { return fail(Option.getOrThrow(lastError)) } - let nextStream: Stream> = provideMeta(provide(self, step.provide)) + let nextStream: Stream> = provideMeta( + instrument(provide(self, step.provide)) + ) let receivedElements = false if (Option.isSome(lastError)) { @@ -6276,10 +6053,10 @@ export const withExecutionPlan: { attempted = true return fail(error) }) - nextStream = retry(nextStream, internalExecutionPlan.scheduleFromStep(step, false) as any) + nextStream = retryWithoutReset(nextStream, internalExecutionPlan.scheduleFromStep(step, false) as any) } else { const schedule = internalExecutionPlan.scheduleFromStep(step, true) - nextStream = schedule ? retry(nextStream, schedule as any) : nextStream + nextStream = schedule ? retryWithoutReset(nextStream, schedule as any) : nextStream } return catch_( @@ -6307,19 +6084,18 @@ export const withExecutionPlan: { * * **Example** (Taking values from the left) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3, 4, 5).pipe( * Stream.take(3), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6334,24 +6110,80 @@ export const take: { n < 1 ? empty : takeUntil(self, (_, i) => i === (n - 1)) ) +/** + * Emits byte chunks until the configured limit would be exceeded, then drops + * the crossing chunk and switches to a fallback stream. + * + * **Example** (Truncating at a byte limit) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * + * const program = Stream.make( + * new Uint8Array([1, 2]), + * new Uint8Array([3, 4, 5]) + * ).pipe( + * Stream.limitBytes(4, () => Stream.empty), + * Stream.runCollect, + * Effect.map((chunks) => chunks.map((chunk) => [...chunk])) + * ) + * + * await Effect.runPromise(program) // => [[1, 2]] + * ``` + * + * @category filtering + * @since 4.0.0 + */ +export const limitBytes: { + ( + bytes: SizeInput, + onLimitReached: LazyArg> + ): (self: Stream) => Stream + ( + self: Stream, + bytes: SizeInput, + onLimitReached: LazyArg> + ): Stream +} = dual(3, ( + self: Stream, + bytes: SizeInput, + onLimitReached: LazyArg> +): Stream => + suspend(() => { + const limit = BigInt(bytes) + let size = BigInt(0) + let limitReached = false + return concat( + takeWhile(self, (chunk) => { + const nextSize = size + BigInt(chunk.length) + if (nextSize > limit) { + limitReached = true + return false + } + size = nextSize + return true + }), + suspend(() => limitReached ? onLimitReached() : empty) + ) + })) + /** * Keeps the last `n` elements from this stream. * * **Example** (Taking elements from the right) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.range(1, 6).pipe( * Stream.takeRight(3), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 4, 5, 6 ] * }) * - * Effect.runPromise(program) - * // Output: [ 4, 5, 6 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6385,8 +6217,8 @@ export const takeRight: { * * **Example** (Taking until a predicate matches) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.range(1, 5) * @@ -6395,16 +6227,15 @@ export const takeRight: { * Stream.takeUntil((n) => n % 3 === 0), * Stream.runCollect * ) - * yield* Console.log(inclusive) - * // Output: [ 1, 2, 3 ] + * inclusive // => [ 1, 2, 3 ] * * const exclusive = yield* stream.pipe( * Stream.takeUntil((n) => n % 3 === 0, { excludeLast: true }), * Stream.runCollect * ) - * yield* Console.log(exclusive) - * // Output: [ 1, 2 ] + * exclusive // => [ 1, 2 ] * }) + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6452,19 +6283,18 @@ export const takeUntil: { * * **Example** (Taking until an effectful predicate matches) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.range(1, 5).pipe( * Stream.takeUntilEffect((n) => Effect.succeed(n % 3 === 0)), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6514,8 +6344,8 @@ export const takeUntilEffect: { * * **Example** (Taking while a predicate holds) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.range(1, 5).pipe( * Stream.takeWhile((n) => n % 3 !== 0) @@ -6523,11 +6353,10 @@ export const takeUntilEffect: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6627,19 +6456,18 @@ export const takeWhileFilter: { * * **Example** (Effectfully taking while a predicate holds) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.range(1, 5).pipe( * Stream.takeWhileEffect((n) => Effect.succeed(n % 3 !== 0)), * Stream.runCollect * ) - * Console.log(result) + * result // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6668,19 +6496,18 @@ export const takeWhileEffect: { * * **Example** (Dropping values from the left) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) * const result = Stream.drop(stream, 2) * * const program = Effect.gen(function*() { * const items = yield* Stream.runCollect(result) - * yield* Console.log(items) + * items // => [ 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6713,16 +6540,16 @@ export const drop: { * * **Example** (Dropping until a predicate matches) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) * const result = Stream.dropUntil(stream, (n) => n >= 3) * - * Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const output = yield* Stream.runCollect(result) - * yield* Console.log(output) // Output: [ 4, 5 ] - * }) + * output // => [ 4, 5 ] + * })) * ``` * * @category filtering @@ -6747,19 +6574,18 @@ export const dropUntil: { * * **Example** (Dropping until an effectful predicate matches) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.range(1, 5).pipe( * Stream.dropUntilEffect((n) => Effect.succeed(n % 3 === 0)), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6790,19 +6616,18 @@ export const dropUntilEffect: { * * **Example** (Dropping while a predicate holds) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3, 4, 5).pipe( * Stream.dropWhile((n) => n < 3), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6873,19 +6698,18 @@ export const dropWhileFilter: { * * **Example** (Effectfully dropping while a predicate holds) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3, 4, 5).pipe( * Stream.dropWhileEffect((n) => Effect.succeed(n < 3)), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 4, 5 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6932,19 +6756,18 @@ export const dropWhileEffect: { * * **Example** (Dropping values from the right) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3, 4, 5).pipe( * Stream.dropRight(2), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category filtering @@ -6976,8 +6799,8 @@ export const dropRight: { * * **Example** (Exposing stream chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const chunks = yield* Stream.make(1, 2, 3, 4).pipe( @@ -6985,11 +6808,10 @@ export const dropRight: { * Stream.chunks, * Stream.runCollect * ) - * yield* Console.log(chunks) + * chunks // => [ [ 1, 2 ], [ 3, 4 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, 2 ], [ 3, 4 ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -7010,8 +6832,8 @@ export const chunks = (self: Stream): Stream(self: Stream): Stream [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -7083,18 +6904,17 @@ export const rechunk: { * * **Example** (Emitting sliding windows) * - * ```ts - * import { Console, Effect, pipe, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, pipe, Stream } from "effect" * - * Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const result = yield* pipe( * Stream.make(1, 2, 3, 4, 5), * Stream.sliding(2), * Stream.runCollect * ) - * yield* Console.log(result) - * }) - * // Output: [ [1, 2], [2, 3], [3, 4], [4, 5] ] + * result // => [ [ 1, 2 ], [ 2, 3 ], [ 3, 4 ], [ 4, 5 ] ] + * })) * ``` * * @category grouping @@ -7114,19 +6934,18 @@ export const sliding: { * * **Example** (Emitting sliding windows with a step size) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const chunks = yield* Stream.make(1, 2, 3, 4, 5).pipe( * Stream.slidingSize(3, 2), * Stream.runCollect * ) - * yield* Console.log(chunks) + * chunks // => [ [ 1, 2, 3 ], [ 3, 4, 5 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, 2, 3 ], [ 3, 4, 5 ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -7143,12 +6962,18 @@ export const slidingSize: { let cause: Cause.Cause | null = null const list = MutableList.make() let emitted = false + let skip = 0 const pull: Pull.Pull< Arr.NonEmptyReadonlyArray>, E | Cause.Done > = Effect.matchCauseEffect(upstream, { onSuccess(arr) { MutableList.appendAllUnsafe(list, arr) + if (skip > 0) { + const length = list.length + MutableList.takeNVoid(list, skip) + skip = Math.max(0, skip - length) + } if (list.length < chunkSize) return pull emitted = true const chunks = [] as any as Arr.NonEmptyArray> @@ -7157,10 +6982,12 @@ export const slidingSize: { chunks.push(MutableList.takeN(list, chunkSize) as any) } else { chunks.push(MutableList.toArrayN(list, chunkSize) as any) - if (chunkSize === 1) { + if (chunkSize === 1 && stepSize <= 0) { MutableList.take(list) } else { + const length = list.length MutableList.takeNVoid(list, stepSize) + skip = Math.max(0, stepSize - length) } } } @@ -7187,19 +7014,18 @@ export const slidingSize: { * * **Example** (Splitting on matching values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.range(0, 9).pipe( * Stream.split((n) => n % 4 === 0), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ [ 1, 2, 3 ], [ 5, 6, 7 ], [ 9 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [1, 2, 3], [5, 6, 7], [9] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -7249,8 +7075,8 @@ export const split: { * * **Example** (Combining streams with state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.combine( * Stream.make("A", "B", "C"), @@ -7264,11 +7090,10 @@ export const split: { * * const program = Effect.gen(function*() { * const output = yield* Stream.runCollect(stream) - * yield* Console.log(output) + * output // => [ 'L:A', 'R:1', 'L:B', 'R:2', 'L:C', 'R:3' ] * }) * - * Effect.runPromise(program) - * // Output: [ "L:A", "R:1", "L:B", "R:2", "L:C", "R:3" ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -7330,8 +7155,8 @@ export const combine: { * * **Example** (Combining stream chunks with state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2).pipe( * Stream.combineArray( @@ -7347,11 +7172,10 @@ export const combine: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 1, 2, 10, 20 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 10, 20 ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -7399,7 +7223,7 @@ export const combineArray: { * * **Example** (Statefully mapping stream values) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { @@ -7411,11 +7235,10 @@ export const combineArray: { * Stream.runCollect * ) * - * yield* Console.log(totals) + * totals // => [0, 1, 3, 6, 10, 15, 21] * }) * - * Effect.runPromise(program) - * // Output: [ 0, 1, 3, 6, 10, 15, 21 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -7476,8 +7299,8 @@ export const mapAccum: { * * **Example** (Statefully mapping stream chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const output = yield* Stream.make(1, 2, 3, 4, 5, 6).pipe( @@ -7488,11 +7311,10 @@ export const mapAccum: { * }), * Stream.runCollect * ) - * yield* Console.log(output) + * output // => [ 3, 10, 21 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 10, 21 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -7559,8 +7381,8 @@ const emptyArr = Arr.empty() * * **Example** (Effectfully mapping stream values with state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const result = yield* Stream.make(1, 1, 1).pipe( @@ -7570,11 +7392,10 @@ const emptyArr = Arr.empty() * Stream.runCollect * ) * - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // Output: [ 1, 2, 3 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -7644,8 +7465,8 @@ export const mapAccumEffect: { * * **Example** (Effectfully mapping stream chunks with state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const totals = yield* Stream.make(1, 2, 3, 4).pipe( @@ -7658,11 +7479,10 @@ export const mapAccumEffect: { * ), * Stream.runCollect * ) - * yield* Console.log(totals) + * totals // => [ 3, 10 ] * }) * - * Effect.runPromise(program) - * // Output: [ 3, 10 ] + * await Effect.runPromise(program) * ``` * * @category mapping @@ -7684,7 +7504,7 @@ export const mapAccumArrayEffect: { readonly onHalt?: ((state: S) => ReadonlyArray) | undefined } ): Stream -} = dual((args) => isStream(args), ( +} = dual((args) => isStream(args[0]), ( self: Stream, initial: LazyArg, f: (s: S, a: Arr.NonEmptyReadonlyArray) => Effect.Effect], E2, R2>, @@ -7720,22 +7540,21 @@ export const mapAccumArrayEffect: { * * **Example** (Scanning stream state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( * Stream.scan(0, (acc, n) => acc + n), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 0, 1, 3, 6 ] * }) * - * Effect.runPromise(program) - * // Output: [ 0, 1, 3, 6 ] + * await Effect.runPromise(program) * ``` * - * @category Accumulation + * @category accumulation * @since 2.0.0 */ export const scan: { @@ -7774,20 +7593,20 @@ export const scan: { * * **Example** (Effectfully scanning stream state) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const states = yield* Stream.make(1, 2, 3).pipe( * Stream.scanEffect(0, (sum, n) => Effect.succeed(sum + n)), * Stream.runCollect * ) - * yield* Console.log(states) - * // Output: [ 0, 1, 3, 6 ] + * states // => [ 0, 1, 3, 6 ] * }) + * await Effect.runPromise(program) * ``` * - * @category Accumulation + * @category accumulation * @since 2.0.0 */ export const scanEffect: { @@ -7817,20 +7636,16 @@ export const scanEffect: { * * **Example** (Debouncing stream elements) * - * ```ts - * import { Console, Duration, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Duration, Effect, Stream } from "effect" * - * const stream = Stream.make(1, 2, 3).pipe( - * Stream.concat(Stream.fromEffect(Effect.sleep(Duration.millis(50)).pipe(Effect.as(4)))), - * Stream.concat(Stream.make(5)), - * Stream.debounce(Duration.millis(30)) - * ) + * const stream = Stream.make(1, 2, 3).pipe(Stream.debounce(Duration.zero)) * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * // Output: [ 3, 5 ] + * values // => [ 3 ] * }) + * await Effect.runPromise(program) * ``` * * @category rate limiting @@ -7929,24 +7744,23 @@ export const debounce: { * * **Example** (Throttling stream chunks effectfully) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( - * Stream.take(6), + * const stream = Stream.range(0, 5).pipe( + * Stream.rechunk(1), * Stream.throttleEffect({ * cost: (arr) => Effect.succeed(arr.length), * units: 1, - * duration: "100 millis", + * duration: 0, * strategy: "shape" * }) * ) * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 0, 1, 2, 3, 4, 5 ] * })) - * // Output: [0, 1, 2, 3, 4, 5] * ``` * * @category rate limiting @@ -8093,24 +7907,24 @@ const throttleShapeEffect = ( * * **Example** (Throttling stream chunks) * - * ```ts - * import { Console, Effect, Schedule, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * const stream = Stream.fromSchedule(Schedule.spaced("50 millis")).pipe( - * Stream.take(6), + * const stream = Stream.range(0, 5).pipe( + * Stream.rechunk(1), * Stream.throttle({ * cost: (arr) => arr.length, * units: 1, - * duration: "100 millis", + * duration: 0, * strategy: "shape" * }) * ) * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) - * // Output: [ 0, 1, 2, 3, 4, 5 ] + * values // => [ 0, 1, 2, 3, 4, 5 ] * }) + * await Effect.runPromise(program) * ``` * * @category rate limiting @@ -8161,19 +7975,18 @@ export const throttle: { * * **Example** (Grouping elements by size) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const grouped = yield* Stream.range(1, 8).pipe( * Stream.grouped(3), * Stream.runCollect * ) - * yield* Console.log(grouped) + * grouped // => [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8 ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -8193,19 +8006,18 @@ export const grouped: { * * **Example** (Grouping elements by size or time) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( * Stream.groupedWithin(2, "5 seconds"), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ [ 1, 2 ], [ 3 ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ 1, 2 ], [ 3 ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -8233,8 +8045,8 @@ export const groupedWithin: { * * **Example** (Grouping elements into keyed substreams using an effectful classifier) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const grouped = yield* Stream.make(1, 2, 3, 4, 5).pipe( @@ -8250,11 +8062,10 @@ export const groupedWithin: { * Stream.runCollect * ) * - * yield* Console.log(grouped) + * grouped // => [ [ 'odd', [ 1, 3, 5 ] ], [ 'even', [ 2, 4 ] ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ "odd", [ 1, 3, 5 ] ], [ "even", [ 2, 4 ] ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -8305,8 +8116,8 @@ export const groupBy: { * * **Example** (Grouping elements by key) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const grouped = yield* Stream.make(1, 2, 3, 4, 5).pipe( @@ -8320,11 +8131,10 @@ export const groupBy: { * ), * Stream.runCollect * ) - * yield* Console.log(grouped) + * grouped // => [ [ 'odd', [ 1, 3, 5 ] ], [ 'even', [ 2, 4 ] ] ] * }) * - * Effect.runPromise(program) - * // Output: [ [ "odd", [ 1, 3, 5 ] ], [ "even", [ 2, 4 ] ] ] + * await Effect.runPromise(program) * ``` * * @category grouping @@ -8511,8 +8321,8 @@ export const groupAdjacentBy: { * * **Example** (Transducing with a sink) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * * const program = Effect.gen(function* () { * const result = yield* Stream.make(1, 2, 3, 4).pipe( @@ -8520,12 +8330,12 @@ export const groupAdjacentBy: { * Stream.runCollect * ) * - * yield* Console.log(result) - * // Output: [ [ 1, 2 ], [ 3, 4 ] ] + * result // => [ [ 1, 2 ], [ 3, 4 ], [] ] * }) + * await Effect.runPromise(program) * ``` * - * @category Aggregation + * @category aggregation * @since 2.0.0 */ export const transduce = dual< @@ -8585,10 +8395,10 @@ export const transduce = dual< * * **Example** (Aggregating with a sink) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function* () { + * await Effect.runPromise(Effect.gen(function* () { * const aggregated = yield* Stream.runCollect( * Stream.make(1, 2, 3, 4, 5, 6).pipe( * Stream.aggregate( @@ -8596,12 +8406,11 @@ export const transduce = dual< * ) * ) * ) - * yield* Console.log(aggregated) + * aggregated // => [ 6, 15 ] * })) - * // [ 6, 15 ] * ``` * - * @category Aggregation + * @category aggregation * @since 2.0.0 */ export const aggregate: { @@ -8626,24 +8435,23 @@ export const aggregate: { * * **Example** (Aggregating with a sink and schedule) * - * ```ts - * import { Console, Effect, Schedule, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schedule, Sink, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function* () { + * await Effect.runPromise(Effect.gen(function* () { * const aggregated = yield* Stream.runCollect( * Stream.make(1, 2, 3, 4, 5, 6).pipe( * Stream.aggregateWithin( * Sink.foldUntil(() => 0, 3, (sum, n) => Effect.succeed(sum + n)), - * Schedule.spaced("1 minute") + * Schedule.forever * ) * ) * ) - * yield* Console.log(aggregated) + * aggregated // => [ 6, 15 ] * })) - * // Output: [ 6, 15 ] * ``` * - * @category Aggregation + * @category aggregation * @since 2.0.0 */ export const aggregateWithin: { @@ -8687,13 +8495,13 @@ export const aggregateWithin: { let leftover: Arr.NonEmptyReadonlyArray | undefined let sinkHasInput = false const step = yield* Schedule.toStepWithSleep(schedule) - const stepToBuffer = Effect.suspend(function loop(): Pull.Pull { - return step(lastOutput).pipe( - Effect.flatMap(() => !sinkHasInput ? loop() : Queue.offer(buffer, scheduleStep)), - Effect.flatMap(() => Effect.never), - Pull.catchDone(() => Cause.done()) - ) + const stepLoop = Effect.suspend(function loop(): Pull.Pull { + return Effect.flatMap(step(lastOutput), () => !sinkHasInput ? loop() : Queue.offer(buffer, scheduleStep)) }) + const stepToBuffer: Pull.Pull = stepLoop.pipe( + Effect.flatMap(() => Effect.never), + Pull.catchDone(() => Cause.done()) + ) // buffer -> sink const pullFromBuffer: Pull.Pull< @@ -8753,8 +8561,8 @@ export const aggregateWithin: { * * **Example** (Broadcasting to two consumers) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.scoped( * Effect.gen(function*() { @@ -8767,15 +8575,14 @@ export const aggregateWithin: { * Stream.runCollect(right) * ], { concurrency: "unbounded" }) * - * yield* Console.log(values) + * values // => [ [ 1, 2, 3 ], [ 1, 2, 3 ] ] * }) * ) * - * Effect.runPromise(program) - * // Output: [[1, 2, 3], [1, 2, 3]] + * await Effect.runPromise(program) * ``` * - * @category Broadcast + * @category broadcasting * @since 4.0.0 */ export const broadcastN: { @@ -8870,8 +8677,8 @@ const makePubSub = ( * * **Example** (Broadcasting a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.scoped( * Effect.gen(function* () { @@ -8885,15 +8692,14 @@ const makePubSub = ( * Stream.runCollect(broadcasted) * ], { concurrency: "unbounded" }) * - * yield* Console.log([left, right]) + * const result = [left, right] // => [[1, 2, 3], [1, 2, 3]] * }) * ) * - * Effect.runPromise(program) - * // Output: [[1, 2, 3], [1, 2, 3]] + * await Effect.runPromise(program) * ``` * - * @category Broadcast + * @category broadcasting * @since 2.0.0 */ export const broadcast: { @@ -8941,27 +8747,40 @@ export const broadcast: { * * **Example** (Sharing a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Ref, Stream } from "effect" * - * Effect.runPromise( + * const result = await Effect.runPromise( * Effect.scoped( * Effect.gen(function*() { - * const shared = yield* Stream.make(1, 2, 3).pipe( - * Stream.share({ capacity: 16 }) + * const firstReady = yield* Deferred.make() + * const secondReady = yield* Deferred.make() + * const acquisitions = yield* Ref.make(0) + * const source = Stream.fromEffect(Ref.update(acquisitions, (n) => n + 1)).pipe( + * Stream.drain, + * Stream.concat(Stream.make(0)), + * Stream.concat( + * Stream.fromEffect(Effect.all([Deferred.await(firstReady), Deferred.await(secondReady)])).pipe(Stream.drain) + * ), + * Stream.concat(Stream.make(1, 2, 3)) * ) - * - * const first = yield* shared.pipe(Stream.take(1), Stream.runCollect) - * const second = yield* shared.pipe(Stream.take(1), Stream.runCollect) - * - * yield* Console.log([first, second]) + * const shared = yield* Stream.share(source, { capacity: 16, replay: 1 }) + * const consume = (ready: Deferred.Deferred) => + * shared.pipe( + * Stream.tap((value) => value === 0 ? Deferred.succeed(ready, void 0) : Effect.void), + * Stream.filter((value) => value !== 0), + * Stream.runCollect + * ) + * + * const values = yield* Effect.all([consume(firstReady), consume(secondReady)], { concurrency: "unbounded" }) + * return { values, acquisitions: yield* Ref.get(acquisitions) } * }) * ) * ) - * // output: [[1], [1]] + * result // => { values: [[1, 2, 3], [1, 2, 3]], acquisitions: 1 } * ``` * - * @category Broadcast + * @category broadcasting * @since 3.8.0 */ export const share: { @@ -9021,8 +8840,8 @@ export const share: { * * **Example** (Piping through a channel) * - * ```ts - * import { Array, Channel, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Array, Channel, Effect, Stream } from "effect" * * type NumberChunk = readonly [number, ...Array] * @@ -9036,14 +8855,13 @@ export const share: { * Stream.pipeThroughChannel(doubleChunks), * Stream.runCollect * ) - * yield* Console.log(result) + * result // => [ 2, 4, 6 ] * }) * - * Effect.runPromise(program) - * // => [2, 4, 6] + * await Effect.runPromise(program) * ``` * - * @category Pipe + * @category sequencing * @since 2.0.0 */ export const pipeThroughChannel: { @@ -9070,7 +8888,7 @@ export const pipeThroughChannel: { * * **Example** (Piping through a channel with failures) * - * ```ts + * ```ts import.meta.vitest * import { Array, Channel, Effect, Stream } from "effect" * * type NumberChunk = readonly [number, ...Array] @@ -9079,19 +8897,18 @@ export const pipeThroughChannel: { * Channel.map((chunk) => Array.map(chunk, String)) * ) * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * const result = yield* Stream.make(1, 2, 3).pipe( * Stream.rechunk(2), * Stream.pipeThroughChannelOrFail(stringifyChunks), * Stream.runCollect * ) * - * yield* Effect.sync(() => console.log(result)) + * result // => ["1", "2", "3"] * })) - * // [ "1", "2", "3" ] * ``` * - * @category Pipe + * @category sequencing * @since 2.0.0 */ export const pipeThroughChannelOrFail: { @@ -9116,8 +8933,8 @@ export const pipeThroughChannelOrFail: { * * **Example** (Piping through a sink) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * * const program = Effect.gen(function*() { * const leftovers = yield* Stream.make(1, 2, 3, 4).pipe( @@ -9125,14 +8942,13 @@ export const pipeThroughChannelOrFail: { * Stream.runCollect * ) * - * yield* Console.log(leftovers) + * leftovers // => [ 3, 4 ] * }) * - * Effect.runPromise(program) - * //=> [ 3, 4 ] + * await Effect.runPromise(program) * ``` * - * @category Pipe + * @category sequencing * @since 2.0.0 */ export const pipeThrough: { @@ -9153,21 +8969,20 @@ export const pipeThrough: { * * **Example** (Collecting values into a stream element) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * * const program = Effect.gen(function*() { * const collected = yield* stream.pipe(Stream.collect, Stream.runCollect) - * yield* Console.log(collected[0]) + * collected[0] // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // [1, 2, 3] + * await Effect.runPromise(program) * ``` * - * @category Accumulation + * @category accumulation * @since 4.0.0 */ export const collect = (self: Stream): Stream, E, R> => fromEffect(runCollect(self)) @@ -9177,8 +8992,8 @@ export const collect = (self: Stream): Stream, E, R> * * **Example** (Accumulating stream elements) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const accumulated = yield* Stream.runCollect( @@ -9187,14 +9002,13 @@ export const collect = (self: Stream): Stream, E, R> * Stream.accumulate * ) * ) - * yield* Console.log(accumulated) + * accumulated // => [ [ 1 ], [ 1, 2 ], [ 1, 2, 3 ] ] * }) * - * Effect.runPromise(program) - * //=> { _id: 'Chunk', values: [ [ 1 ], [ 1, 2 ], [ 1, 2, 3 ] ] } + * await Effect.runPromise(program) * ``` * - * @category Accumulation + * @category accumulation * @since 2.0.0 */ export const accumulate = (self: Stream): Stream, E, R> => @@ -9208,8 +9022,8 @@ export const accumulate = (self: Stream): Stream(self: Stream): Stream [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // [1, 2, 3] + * await Effect.runPromise(program) * ``` * - * @category Deduplication + * @category deduplication * @since 2.0.0 */ export const changes = (self: Stream): Stream => changesWith(self, Equal.equals) @@ -9234,23 +9047,22 @@ export const changes = (self: Stream): Stream => chan * * **Example** (Emitting values that changed by equivalence) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make("A", "a", "B", "b", "b").pipe( * Stream.changesWith((left, right) => left.toLowerCase() === right.toLowerCase()) * ) * - * Effect.runPromise( + * await Effect.runPromise( * Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 'A', 'B' ] * }) * ) - * // ["A", "B"] * ``` * - * @category Deduplication + * @category deduplication * @since 2.0.0 */ export const changesWith: { @@ -9292,22 +9104,21 @@ export const changesWith: { * * **Example** (Effectfully emitting changed values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const stream = Stream.make(1, 1, 2, 2, 3, 3).pipe( * Stream.changesWithEffect((a, b) => Effect.succeed(a === b)) * ) * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 1, 2, 3 ] * }) * - * Effect.runPromise(program) - * // { _id: "Chunk", values: [ 1, 2, 3 ] } + * await Effect.runPromise(program) * ``` * - * @category Deduplication + * @category deduplication * @since 2.0.0 */ export const changesWithEffect: { @@ -9360,8 +9171,8 @@ export const changesWithEffect: { * * **Example** (Decoding Uint8Array chunks into strings using TextDecoder with an optional encoding) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const encoder = new TextEncoder() * const stream = Stream.make( @@ -9374,14 +9185,13 @@ export const changesWithEffect: { * Stream.decodeText, * Stream.runCollect * ) - * yield* Console.log(decoded) + * decoded // => [ 'Hello', ' World' ] * }) * - * Effect.runPromise(program) - * // ["Hello", " World"] + * await Effect.runPromise(program) * ``` * - * @category encoding + * @category decoding * @since 2.0.0 */ export const decodeText: < @@ -9412,19 +9222,18 @@ export const decodeText: < * * **Example** (Encoding a stream of strings into UTF-8 Uint8Array chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make("Hello", " ", "World") * const program = Effect.gen(function*() { * const encoded = Stream.encodeText(stream) * const chunks = yield* Stream.runCollect(encoded) * const bytes = chunks.map((chunk) => [...chunk]) - * yield* Console.log(bytes) + * bytes // => [ [ 72, 101, 108, 108, 111 ], [ 32 ], [ 87, 111, 114, 108, 100 ] ] * }) * - * Effect.runPromise(program) - * // [[72, 101, 108, 108, 111], [32], [87, 111, 114, 108, 100]] + * await Effect.runPromise(program) * ``` * * @category encoding @@ -9441,19 +9250,18 @@ export const encodeText = (self: Stream): Stream [ 'a', 'b', 'c' ] * })) - * // ["a", "b", "c"] * ``` * - * @category encoding + * @category splitting * @since 2.0.0 */ export const splitLines = (self: Stream): Stream => @@ -9467,17 +9275,16 @@ export const splitLines = (self: Stream): Stream [1, 0, 2, 0, 3, 0, 4] * }) * - * Effect.runPromise(program) - * // [1, 0, 2, 0, 3, 0, 4] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -9509,7 +9316,7 @@ export const intersperse: { * * **Example** (Interspersing stream affixes) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect, Stream } from "effect" * * const stream = Stream.make("a", "b", "c").pipe( @@ -9518,11 +9325,10 @@ export const intersperse: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => ["[", "a", ",", "b", ",", "c", "]"] * }) * - * Effect.runPromise(program) - * // [ "[", "a", ",", "b", ",", "c", "]" ] + * await Effect.runPromise(program) * ``` * * @category sequencing @@ -9552,8 +9358,8 @@ export const intersperseAffixes: { * * **Example** (Interleaving streams) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.interleave( * Stream.make(2, 3), @@ -9562,11 +9368,10 @@ export const intersperseAffixes: { * * const program = Effect.gen(function*() { * const collected = yield* Stream.runCollect(stream) - * yield* Console.log(collected) + * collected // => [ 2, 5, 3, 6, 7 ] * }) * - * Effect.runPromise(program) - * // [2, 5, 3, 6, 7] + * await Effect.runPromise(program) * ``` * * @category merging @@ -9595,8 +9400,8 @@ export const interleave: { * * **Example** (Interleaving two streams deterministically by following a boolean decider stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const left = Stream.make(1, 3, 5) @@ -9607,11 +9412,10 @@ export const interleave: { * Stream.interleaveWith(left, right, decider) * ) * - * yield* Console.log(values) + * values // => [ 1, 2, 4, 3, 5 ] * }) * - * Effect.runPromise(program) - * // [ 1, 2, 4, 3, 5 ] + * await Effect.runPromise(program) * ``` * * @category merging @@ -9685,8 +9489,8 @@ export const interleaveWith: { * * **Example** (Interrupting when an effect completes) * - * ```ts - * import { Console, Deferred, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const interrupt = yield* Deferred.make() @@ -9700,11 +9504,10 @@ export const interleaveWith: { * ) * * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ 1 ] * }) * - * Effect.runPromise(program) - * // => [1, 2] + * await Effect.runPromise(program) * ``` * * @category interruption @@ -9720,7 +9523,7 @@ export const interruptWhen: { ) /** - * Stops a stream after the current element when an effect completes. + * Stops a stream after the current pull when an effect completes. * * **When to use** * @@ -9733,13 +9536,15 @@ export const interruptWhen: { * * **Gotchas** * - * This does not interrupt an in-progress pull. Use {@link interruptWhen} when - * the stream should be interrupted immediately. + * This does not interrupt or truncate an in-progress pull. A pull may emit + * multiple elements in a single chunk, in which case the entire chunk is + * emitted. Use {@link interruptWhen} when the stream should be interrupted + * immediately. * * **Example** (Halting a stream after an effect completes) * - * ```ts - * import { Console, Deferred, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Deferred, Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const halt = yield* Deferred.make() @@ -9748,12 +9553,10 @@ export const interruptWhen: { * Stream.haltWhen(Deferred.await(halt)), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [ 1, 2 ] * }) * - * Effect.runPromise(program) - * // Output: - * // [1, 2] + * await Effect.runPromise(program) * ``` * * @category interruption @@ -9773,25 +9576,25 @@ export const haltWhen: { * * **Example** (Running a finalizer on exit) * - * ```ts - * import { Console, Effect, Exit, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Stream } from "effect" * + * const exits: Array = [] * const stream = Stream.make(1, 2, 3).pipe( * Stream.onExit((exit) => * Exit.isSuccess(exit) - * ? Console.log("Stream completed successfully") - * : Console.log("Stream failed") + * ? Effect.sync(() => exits.push("success")) + * : Effect.sync(() => exits.push("failure")) * ) * ) * - * Effect.runPromise(Effect.gen(function*() { + * await Effect.runPromise(Effect.gen(function*() { * yield* Stream.runCollect(stream) * })) - * // Output: - * // Stream completed successfully + * exits // => ["success"] * ``` * - * @category Finalization + * @category resource management * @since 4.0.0 */ export const onExit: { @@ -9817,21 +9620,21 @@ export const onExit: { * * **Example** (Running an effect on errors) * - * ```ts - * import { Cause, Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Stream } from "effect" * + * const errors: Array = [] * const program = Effect.gen(function*() { * const stream = Stream.make(1, 2, 3).pipe( * Stream.concat(Stream.fail("boom")), - * Stream.onError((cause) => Console.log(`Stream failed: ${Cause.squash(cause)}`)) + * Stream.onError((cause) => Effect.sync(() => errors.push(String(Cause.squash(cause))))) * ) * * yield* Stream.runCollect(stream) * }) * - * Effect.runPromiseExit(program) - * // Output: - * // Stream failed: boom + * await Effect.runPromise(Effect.exit(program)) + * errors // => ["boom"] * ``` * * @category error handling @@ -9855,22 +9658,21 @@ export const onError: { * * **Example** (Running an effect on start) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const program = Effect.gen(function*() { * const stream = Stream.fromArray([1, 2, 3]).pipe( - * Stream.onStart(Console.log("Stream started")) + * Stream.onStart(Effect.sync(() => events.push("started"))) * ) * * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Output: - * // Stream started - * // [1, 2, 3] + * await Effect.runPromise(program) + * events // => ["started"] * ``` * * @category sequencing @@ -9894,16 +9696,17 @@ export const onStart: { * * **Example** (Running an effect on the first value) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * - * Effect.runPromise(Effect.gen(function* () { + * const first: Array = [] + * await Effect.runPromise(Effect.gen(function* () { * yield* Stream.fromArray([1, 2, 3]).pipe( - * Stream.onFirst((value) => Console.log(`first=${value}`)), + * Stream.onFirst((value) => Effect.sync(() => first.push(value))), * Stream.runDrain * ) * })) - * // Output: first=1 + * first // => [1] * ``` * * @category sequencing @@ -9927,20 +9730,20 @@ export const onFirst: { * * **Example** (Running an effect on end) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const program = Effect.gen(function*() { * const values = yield* Stream.make(1, 2, 3).pipe( - * Stream.onEnd(Console.log("Stream ended")), + * Stream.onEnd(Effect.sync(() => events.push("ended"))), * Stream.runCollect * ) - * yield* Console.log(values) + * values // => [1, 2, 3] * }) * - * Effect.runPromise(program) - * // Stream ended - * // [1, 2, 3] + * await Effect.runPromise(program) + * events // => ["ended"] * ``` * * @category sequencing @@ -9964,24 +9767,24 @@ export const onEnd: { * * **Example** (Ensuring finalization) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const events: Array = [] * const stream = Stream.fromArray([1, 2]).pipe( - * Stream.ensuring(Effect.orDie(Console.log("cleanup"))) + * Stream.ensuring(Effect.sync(() => events.push("cleanup"))) * ) * * const program = Effect.gen(function*() { * const collected = yield* Stream.runCollect(stream) - * yield* Console.log(collected) + * collected // => [1, 2] * }) * - * Effect.runPromise(program) - * //=> cleanup - * //=> [1, 2] + * await Effect.runPromise(program) + * events // => ["cleanup"] * ``` * - * @category Finalization + * @category resource management * @since 2.0.0 */ export const ensuring: { @@ -10000,7 +9803,7 @@ export const ensuring: { * * **Example** (Providing stream requirements) * - * ```ts + * ```ts import.meta.vitest * import { Console, Context, Effect, Layer, Stream } from "effect" * * class Env extends Context.Service()("Env") {} @@ -10016,16 +9819,10 @@ export const ensuring: { * * const withEnv = stream.pipe(Stream.provide(layer)) * - * const program = Stream.runCollect(withEnv).pipe( - * Effect.flatMap((values) => Console.log(values)) - * ) - * - * Effect.runPromise(program) - * // Output: - * // ["Hello, Ada"] + * await Effect.runPromise(Stream.runCollect(withEnv)) // => ["Hello, Ada"] * ``` * - * @category services + * @category providing services * @since 4.0.0 */ export const provide: { @@ -10057,8 +9854,8 @@ export const provide: { * * **Example** (Providing multiple services to the stream using a context) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class Config extends Context.Service()("Config") {} * class Greeter extends Context.Service string }>()("Greeter") {} @@ -10077,14 +9874,13 @@ export const provide: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(Stream.provideContext(stream, context)) - * yield* Console.log(result) + * result // => [ 'Hello!' ] * }) * - * Effect.runPromise(program) - * // ["Hello!"] + * await Effect.runPromise(program) * ``` * - * @category services + * @category providing services * @since 2.0.0 */ export const provideContext: { @@ -10102,8 +9898,8 @@ export const provideContext: { * * **Example** (Providing a stream service) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class Greeter extends Context.Service string @@ -10123,14 +9919,13 @@ export const provideContext: { * }) * ) * ) - * yield* Console.log(collected) + * collected // => [ 'Hello, Ada' ] * }) * - * Effect.runPromise(program) - * //=> ["Hello, Ada"] + * await Effect.runPromise(program) * ``` * - * @category services + * @category providing services * @since 2.0.0 */ export const provideService: { @@ -10156,8 +9951,8 @@ export const provideService: { * * **Example** (Providing a stream service effectfully) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class ApiConfig extends Context.Service()("ApiConfig") {} * @@ -10168,26 +9963,21 @@ export const provideService: { * }) * ) * + * const events: Array = [] * const withConfig = stream.pipe( * Stream.provideServiceEffect( * ApiConfig, * Effect.succeed({ baseUrl: "https://example.com" }).pipe( - * Effect.tap(() => Console.log("Loading config...")) + * Effect.tap(() => Effect.sync(() => events.push("loading"))) * ) * ) * ) * - * const program = Stream.runCollect(withConfig).pipe( - * Effect.flatMap((values) => Console.log(values)) - * ) - * - * Effect.runPromise(program) - * // Output: - * // Loading config... - * // ["https://example.com"] + * await Effect.runPromise(Stream.runCollect(withConfig)) // => ["https://example.com"] + * events // => ["loading"] * ``` * - * @category services + * @category providing services * @since 2.0.0 */ export const provideServiceEffect: { @@ -10214,8 +10004,8 @@ export const provideServiceEffect: { * * **Example** (Updating the stream context) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class Logger extends Context.Service()("Logger") {} * class Config extends Context.Service()("Config") {} @@ -10236,16 +10026,15 @@ export const provideServiceEffect: { * * const program = Effect.gen(function*() { * const values = yield* Stream.runCollect(updated) - * yield* Console.log(values) + * values // => [ 'Hello World' ] * }) * - * Effect.runPromise( + * await Effect.runPromise( * Effect.provideService(program, Logger, { prefix: "Hello " }) * ) - * //=> [ "Hello World" ] * ``` * - * @category services + * @category providing services * @since 4.0.0 */ export const updateContext: { @@ -10268,8 +10057,8 @@ export const updateContext: { * * **Example** (Updating a stream service) * - * ```ts - * import { Console, Context, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Context, Effect, Stream } from "effect" * * class Counter extends Context.Service()("Counter") {} * @@ -10279,14 +10068,13 @@ export const updateContext: { * * const program = Effect.gen(function*() { * const counters = yield* Stream.runCollect(stream) - * yield* Console.log(`Updated count: ${counters[0].count}`) + * const message = `Updated count: ${counters[0].count}` // => "Updated count: 1" * }) * - * Effect.runPromise(Effect.provideService(program, Counter, { count: 0 })) - * // Output: Updated count: 1 + * await Effect.runPromise(Effect.provideService(program, Counter, { count: 0 })) * ``` * - * @category services + * @category providing services * @since 2.0.0 */ export const updateService: { @@ -10318,18 +10106,17 @@ export const updateService: { * * **Example** (Wrapping a stream in a span) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.fromArray([1, 2, 3]).pipe(Stream.withSpan("numbers")) * - * Effect.runPromise( + * await Effect.runPromise( * Effect.gen(function*() { * const values = yield* Stream.runCollect(stream) - * yield* Console.log(values) + * values // => [ 1, 2, 3 ] * }) * ) - * // [1, 2, 3] * ``` * * @category tracing @@ -10354,8 +10141,8 @@ export const withSpan: { * * **Example** (Starting stream do notation) * - * ```ts - * import { Console, Effect, pipe, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, pipe, Stream } from "effect" * * const program = pipe( * Stream.Do, @@ -10365,14 +10152,13 @@ export const withSpan: { * * const effect = Effect.gen(function*() { * const collected = yield* Stream.runCollect(program) - * yield* Console.log(collected) + * collected // => [ { value: 1, next: 2 }, { value: 2, next: 3 } ] * }) * - * Effect.runPromise(effect) - * //=> [{ value: 1, next: 2 }, { value: 2, next: 3 }] + * await Effect.runPromise(effect) * ``` * - * @category do notation + * @category constructors * @since 2.0.0 */ export const Do: Stream<{}> = succeed({}) @@ -10399,8 +10185,8 @@ export { * * **Example** (Adding a computed field) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.Do.pipe( * Stream.let("x", () => 2), @@ -10409,14 +10195,13 @@ export { * * const program = Effect.gen(function*() { * const records = yield* Stream.runCollect(stream) - * yield* Console.log(records) + * records // => [ { x: 2, y: 6 } ] * }) * - * Effect.runPromise(program) - * // [{ x: 2, y: 6 }] + * await Effect.runPromise(program) * ``` * - * @category do notation + * @category mapping * @since 2.0.0 */ let_ as let @@ -10427,8 +10212,8 @@ export { * * **Example** (Binding a stream value) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Stream.Do.pipe( * Stream.bind("a", () => Stream.make(1, 2)), @@ -10437,11 +10222,10 @@ export { * * const result = Stream.runCollect(program) * - * Effect.runPromise(Effect.flatMap(result, Console.log)) - * // [{ a: 1, b: 2 }, { a: 2, b: 3 }] + * await Effect.runPromise(result) // => [{ a: 1, b: 2 }, { a: 2, b: 3 }] * ``` * - * @category do notation + * @category sequencing * @since 2.0.0 */ export const bind: { @@ -10478,8 +10262,8 @@ export const bind: { * * **Example** (Binding an effect value) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.Do.pipe( * Stream.bind("value", () => Stream.make(1, 2)), @@ -10488,14 +10272,13 @@ export const bind: { * * const program = Effect.gen(function*() { * const result = yield* Stream.runCollect(stream) - * yield* Console.log(result) + * result // => [ { value: 1, double: 2 }, { value: 2, double: 4 } ] * }) * - * Effect.runPromise(program) - * // [{ value: 1, double: 2 }, { value: 2, double: 4 }] + * await Effect.runPromise(program) * ``` * - * @category do notation + * @category sequencing * @since 2.0.0 */ export const bindEffect: { @@ -10535,18 +10318,15 @@ export const bindEffect: { * * **Example** (Binding values to a record key) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3).pipe(Stream.bindTo("value")) * - * const program = Stream.runCollect(stream).pipe(Effect.flatMap(Console.log)) - * - * Effect.runPromise(program) - * // [{ value: 1 }, { value: 2 }, { value: 3 }] + * await Effect.runPromise(Stream.runCollect(stream)) // => [{ value: 1 }, { value: 2 }, { value: 3 }] * ``` * - * @category do notation + * @category mapping * @since 2.0.0 */ export const bindTo: { @@ -10562,13 +10342,12 @@ export const bindTo: { * * **Example** (Running a stream with a sink) * - * ```ts - * import { Console, Effect, Sink, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Sink, Stream } from "effect" * * const program = Stream.run(Stream.make(1, 2, 3), Sink.sum) * - * Effect.runPromise(Effect.flatMap(program, Console.log)) - * // 6 + * await Effect.runPromise(program) // => 6 * ``` * * @category destructors @@ -10598,18 +10377,17 @@ export const run: { * * **Example** (Collecting stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) * * const program = Effect.gen(function*() { * const collected = yield* Stream.runCollect(stream) - * yield* Console.log(collected) + * collected // => [ 1, 2, 3, 4, 5 ] * }) * - * Effect.runPromise(program) - * // [1, 2, 3, 4, 5] + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10632,18 +10410,17 @@ export const runCollect = (self: Stream): Effect.Effect 5 * }) * - * Effect.runPromise(program) - * // 5 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10657,16 +10434,15 @@ export const runCount = (self: Stream): Effect.Effect 6 * }) * - * Effect.runPromise(program) - * // 6 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10685,8 +10461,8 @@ export const runSum = (self: Stream): Effect.Effect(self: Stream): Effect.Effect 0, * (acc, n) => acc + n * ) - * yield* Console.log(total) + * total // => 6 * }) * - * Effect.runPromise(program) - * // 6 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10738,8 +10513,8 @@ export const runFold: { * * **Example** (Effectfully folding stream values) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const program = Effect.gen(function*() { * const total = yield* Stream.runFoldEffect( @@ -10747,11 +10522,10 @@ export const runFold: { * () => 0, * (acc, n) => Effect.succeed(acc + n) * ) - * yield* Console.log(total) + * total // => 6 * }) * - * Effect.runPromise(program) - * // 6 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10795,16 +10569,15 @@ export const runFoldEffect: { * * **Example** (Getting the first stream value) * - * ```ts - * import { Console, Effect, Option, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Stream } from "effect" * * const program = Effect.gen(function*() { * const head = yield* Stream.runHead(Stream.make(1, 2, 3)) - * yield* Console.log(Option.getOrThrow(head)) + * Option.getOrThrow(head) // => 1 * }) * - * Effect.runPromise(program) - * // 1 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -10845,19 +10618,18 @@ export const runLast = (self: Stream): Effect.Effect = [] * * const program = Effect.gen(function*() { - * yield* Stream.runForEach(stream, (n) => Console.log(`Processing: ${n}`)) + * yield* Stream.runForEach(stream, (n) => Effect.sync(() => values.push(`Processing: ${n}`))) * }) * - * Effect.runPromise(program) - * // Processing: 1 - * // Processing: 2 - * // Processing: 3 + * await Effect.runPromise(program) + * values // => ["Processing: 1", "Processing: 2", "Processing: 3"] * ``` * * @category destructors @@ -10890,24 +10662,23 @@ export const runForEach: { * * **Example** (Running effects while a predicate holds) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const values: Array = [] * const program = Effect.gen(function*() { * const stream = Stream.make(1, 2, 3, 4, 5) * * yield* Stream.runForEachWhile(stream, (n) => * Effect.gen(function*() { - * yield* Console.log(`Processing: ${n}`) + * yield* Effect.sync(() => values.push(n)) * return n < 3 * }) * ) * }) * - * Effect.runPromise(program) - * // Processing: 1 - * // Processing: 2 - * // Processing: 3 + * await Effect.runPromise(program) + * values // => [1, 2, 3] * ``` * * @category destructors @@ -10946,19 +10717,20 @@ export const runForEachWhile: { * * **Example** (Consuming stream chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) + * const chunks: Array = [] * const program = Effect.gen(function*() { * yield* Stream.runForEachArray( * stream, - * (chunk) => Console.log(`Processing chunk: ${chunk.join(", ")}`) + * (chunk) => Effect.sync(() => chunks.push(chunk.join(", "))) * ) * }) * - * Effect.runPromise(program) - * // Processing chunk: 1, 2, 3, 4, 5 + * await Effect.runPromise(program) + * chunks // => ["1, 2, 3, 4, 5"] * ``` * * @category destructors @@ -10982,21 +10754,20 @@ export const runForEachArray: { * * **Example** (Draining a stream run) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * + * const values: Array = [] * const program = Effect.gen(function*() { * const stream = Stream.make(1, 2, 3).pipe( - * Stream.mapEffect((n) => Console.log(`Processing: ${n}`)) + * Stream.mapEffect((n) => Effect.sync(() => values.push(n))) * ) * * yield* Stream.runDrain(stream) * }) * - * Effect.runPromise(program) - * // Processing: 1 - * // Processing: 2 - * // Processing: 3 + * await Effect.runPromise(program) + * values // => [1, 2, 3] * ``` * * @category destructors @@ -11014,8 +10785,8 @@ export const runDrain = (self: Stream): Effect.Effect(self: Stream): Effect.Effect [ 1, 2, 3 ] * }) * ) * - * Effect.runPromise(program) - * // [1, 2, 3] + * await Effect.runPromise(program) * ``` * * @category destructors @@ -11043,17 +10813,16 @@ export const toPull = ( * * **Example** (Joining strings from a stream) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make("Hello", " ", "World", "!") * const program = Effect.gen(function*() { * const text = yield* Stream.mkString(stream) - * yield* Console.log(text) + * text // => "Hello World!" * }) * - * Effect.runPromise(program) - * // Hello World! + * await Effect.runPromise(program) * ``` * * @category destructors @@ -11066,57 +10835,63 @@ export const mkString = (self: Stream): Effect.Effect acc + chunk.join("") ) +/** + * Concatenates the stream's `Uint8Array` chunks into a single `ArrayBuffer`. + * + * **Example** (Joining byte chunks into an ArrayBuffer) + * + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" + * + * const program = Stream.make( + * new Uint8Array([1, 2]), + * new Uint8Array([3, 4]) + * ).pipe( + * Stream.mkArrayBuffer, + * Effect.map((buffer) => [...new Uint8Array(buffer)]) + * ) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4] + * ``` + * + * **Gotchas** + * + * This materializes the full content in memory. The source stream must not + * reuse or mutate emitted buffers, which are retained until collection completes. + * + * @category destructors + * @since 4.0.0 + */ +export const mkArrayBuffer = (self: Stream): Effect.Effect => + Effect.map(Channel.mkUint8Array(self.channel), (bytes) => bytes.buffer) + /** * Concatenates the stream's `Uint8Array` chunks into a single `Uint8Array`. * * **Example** (Joining Uint8Array chunks) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(new Uint8Array([1, 2]), new Uint8Array([3, 4])) * const program = Effect.gen(function*() { * const bytes = yield* Stream.mkUint8Array(stream) - * yield* Console.log([...bytes]) + * const values = Array.from(bytes) // => [1, 2, 3, 4] * }) * - * Effect.runPromise(program) - * // [1, 2, 3, 4] + * await Effect.runPromise(program) * ``` * + * **Gotchas** + * + * This materializes the full content in memory. The source stream must not + * reuse or mutate emitted buffers, which are retained until collection completes. + * * @category destructors * @since 4.0.0 */ export const mkUint8Array = (self: Stream): Effect.Effect => - Effect.map( - Channel.runFold( - self.channel, - (): { - bytes: number - readonly arrays: Array - } => ({ - bytes: 0, - arrays: [] - }), - (acc, chunk) => { - for (let i = 0; i < chunk.length; i++) { - acc.bytes += chunk[i].length - acc.arrays.push(chunk[i]) - } - return acc - } - ), - ({ arrays, bytes }) => { - const result = new Uint8Array(bytes) - let offset = 0 - for (let i = 0; i < arrays.length; i++) { - const array = arrays[i] - result.set(array, offset) - offset += array.length - } - return result - } - ) + Channel.mkUint8Array(self.channel) /** * Converts the stream to a `ReadableStream` using the provided services. @@ -11132,11 +10907,13 @@ export const mkUint8Array = (self: Stream): Effect.Effec * * **Example** (Converting to a ReadableStream with services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) * const readableStream = Stream.toReadableStreamWith(stream, Context.empty()) + * const values = await Array.fromAsync(readableStream) + * values // => [ 1, 2, 3, 4, 5 ] * ``` * * @category destructors @@ -11208,11 +10985,12 @@ export const toReadableStreamWith = dual< * * **Example** (Converting a stream to a ReadableStream) * - * ```ts + * ```ts import.meta.vitest * import { Stream } from "effect" * * const readableStream = Stream.toReadableStream(Stream.make(1, 2, 3)) - * const reader = readableStream.getReader() + * const values = await Array.fromAsync(readableStream) + * values // => [ 1, 2, 3 ] * ``` * * @category destructors @@ -11250,17 +11028,17 @@ export const toReadableStream: { * * **Example** (Creating a ReadableStream effect) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3, 4, 5) * * const effect = Effect.gen(function*() { * const readableStream = yield* Stream.toReadableStreamEffect(stream) - * yield* Console.log(readableStream instanceof ReadableStream) // true + * readableStream instanceof ReadableStream // => true * }) * - * Effect.runPromise(effect) + * await Effect.runPromise(effect) * ``` * * @category destructors @@ -11298,22 +11076,13 @@ export const toReadableStreamEffect: { * * **Example** (Converting to an AsyncIterable with services) * - * ```ts + * ```ts import.meta.vitest * import { Context, Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * const iterable = Stream.toAsyncIterableWith(stream, Context.empty()) * - * const collect = async () => { - * const results: Array = [] - * for await (const value of iterable) { - * results.push(value) - * } - * console.log(results) - * } - * - * collect() - * // [ 1, 2, 3 ] + * await Array.fromAsync(iterable) // => [1, 2, 3] * ``` * * @category destructors @@ -11333,32 +11102,69 @@ export const toAsyncIterableWith: { ): AsyncIterable => ({ [Symbol.asyncIterator]() { const runPromise = Effect.runPromiseWith(context) - const runPromiseExit = Effect.runPromiseExitWith(context) + const runFork = Effect.runForkWith(context) const scope = Scope.makeUnsafe() let pull: Pull.Pull, E, void, R> | undefined let currentIter: Iterator | undefined + let currentFiber: Fiber.Fiber, E | Cause.Done> | undefined + let closePromise: Promise> | undefined + const close = (exit: Exit.Exit): Promise> => { + if (closePromise) return closePromise + const fiber = currentFiber + closePromise = runPromise(Effect.as( + Effect.andThen( + fiber ? Fiber.interrupt(fiber) : Effect.void, + Scope.close(scope, exit) + ), + { done: true, value: undefined } + )) + return closePromise + } + const closeAndReportError = async (exit: Exit.Exit): Promise => { + try { + await close(exit) + } catch (error) { + await runPromise(Effect.logError("Suppressed error while closing Stream async iterator", error)) + } + } return { async next(): Promise> { + if (closePromise) return closePromise if (currentIter) { const next = currentIter.next() if (!next.done) return next currentIter = undefined } - pull ??= await runPromise(Channel.toPullScoped(self.channel, scope)) - const exit = await runPromiseExit(pull) + const fiber = runFork( + pull ?? + Effect.flatMap(Channel.toPullScoped(self.channel, scope), (nextPull) => { + pull = nextPull + return nextPull + }) + ) + currentFiber = fiber + const exit = await runPromise(Fiber.await(fiber)) + if (currentFiber === fiber) { + currentFiber = undefined + } if (Exit.isSuccess(exit)) { currentIter = exit.value[Symbol.iterator]() return currentIter.next() } else if (Pull.isDoneCause(exit.cause)) { - return { done: true, value: undefined } + return close(Exit.void) } + if (closePromise && Cause.hasInterruptsOnly(exit.cause)) { + return closePromise + } + await closeAndReportError(exit) throw Cause.squash(exit.cause) }, - return(_) { - return runPromise(Effect.as( - Scope.close(scope, Exit.void), - { done: true, value: undefined } - )) + return() { + return close(Exit.void) + }, + async throw(error) { + await closeAndReportError(Exit.die(error)) + throw error } } } @@ -11375,25 +11181,17 @@ export const toAsyncIterableWith: { * * **Example** (Creating an AsyncIterable effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * * const program = Effect.gen(function*() { * const iterable = yield* Stream.toAsyncIterableEffect(stream) - * const values = yield* Effect.promise(async () => { - * const collected: Array = [] - * for await (const value of iterable) { - * collected.push(value) - * } - * return collected - * }) - * yield* Effect.sync(() => console.log(values)) + * return yield* Effect.promise(() => Array.fromAsync(iterable)) * }) * - * Effect.runPromise(program) - * // [ 1, 2, 3 ] + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category destructors @@ -11410,22 +11208,12 @@ export const toAsyncIterableEffect = (self: Stream): Effect.Ef * * **Example** (Converting to an async iterable) * - * ```ts + * ```ts import.meta.vitest * import { Stream } from "effect" * * const stream = Stream.make(1, 2, 3) * - * const collect = async () => { - * const iterable = Stream.toAsyncIterable(stream) - * const values: Array = [] - * for await (const value of iterable) { - * values.push(value) - * } - * console.log(values) - * } - * - * collect() - * // [ 1, 2, 3 ] + * await Array.fromAsync(Stream.toAsyncIterable(stream)) // => [1, 2, 3] * ``` * * @category destructors @@ -11444,8 +11232,8 @@ export const toAsyncIterable = (self: Stream): AsyncIterable => * * **Example** (Running a stream into a PubSub) * - * ```ts - * import { Console, Effect, PubSub, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, PubSub, Stream } from "effect" * * const program = Effect.scoped(Effect.gen(function* () { * const pubsub = yield* PubSub.unbounded() @@ -11456,13 +11244,11 @@ export const toAsyncIterable = (self: Stream): AsyncIterable => * const first = yield* PubSub.take(subscription) * const second = yield* PubSub.take(subscription) * - * yield* Console.log(first) - * yield* Console.log(second) + * first // => 1 + * second // => 2 * })) * - * Effect.runPromise(program) - * //=> 1 - * //=> 2 + * await Effect.runPromise(program) * ``` * * @category destructors @@ -11500,8 +11286,8 @@ export const runIntoPubSub: { * * **Example** (Converting a stream to a PubSub for concurrent consumption) * - * ```ts - * import { Console, Effect, PubSub, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, PubSub, Stream } from "effect" * * const program = Effect.scoped(Effect.gen(function* () { * const pubsub = yield* Stream.fromArray([1, 2]).pipe( @@ -11510,8 +11296,9 @@ export const runIntoPubSub: { * const subscription = yield* PubSub.subscribe(pubsub) * const first = yield* PubSub.take(subscription) * - * yield* Console.log(first) + * first // => 1 * })) + * await Effect.runPromise(program) * ``` * * @category destructors @@ -11569,8 +11356,8 @@ export const toPubSub: { * * **Example** (Converting to a PubSub of takes) * - * ```ts - * import { Console, Effect, PubSub, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, PubSub, Stream } from "effect" * * const program = Effect.gen(function* () { * const pubsub = yield* Stream.fromArray([1, 2, 3]).pipe( @@ -11580,9 +11367,10 @@ export const toPubSub: { * const take = yield* PubSub.take(subscription) * * if (Array.isArray(take)) { - * yield* Console.log(take) + * take // => [ 1, 2, 3 ] * } * }) + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category destructors @@ -11638,14 +11426,15 @@ export const toPubSubTake: { * * **Example** (Converting a stream to a Queue for concurrent consumption) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Queue, Stream } from "effect" * * const program = Effect.gen(function* () { * const queue = yield* Stream.toQueue(Stream.fromIterable([1, 2, 3]), { capacity: 8 }) * const chunk = yield* Queue.takeBetween(queue, 1, 3) - * return chunk + * chunk // => [ 1, 2, 3 ] * }) + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category destructors @@ -11689,7 +11478,7 @@ export const toQueue: { * * **Example** (Running a stream into a queue) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, Queue, Stream } from "effect" * * const program = Effect.gen(function*() { @@ -11706,8 +11495,10 @@ export const toQueue: { * ] * const done = yield* Effect.flip(Queue.take(queue)) * - * return { values, done } + * values // => [ 1, 2, 3 ] + * done._tag === "Done" // => true * }) + * await Effect.runPromise(program) * ``` * * @category destructors diff --git a/.context/effect/packages/effect/src/String.ts b/.context/effect/packages/effect/src/String.ts index 615ec1d7b..671ba710f 100644 --- a/.context/effect/packages/effect/src/String.ts +++ b/.context/effect/packages/effect/src/String.ts @@ -46,12 +46,11 @@ export const String = globalThis.String * * **Example** (Checking for strings) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.isString("a"), true) - * assert.deepStrictEqual(String.isString(1), false) + * String.isString("a") // => true + * String.isString(1) // => false * ``` * * @category guards @@ -65,12 +64,12 @@ export const isString: Refinement = predicate.isString * * **Example** (Comparing strings lexicographically) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.Order("apple", "banana")) // -1 - * console.log(String.Order("banana", "apple")) // 1 - * console.log(String.Order("apple", "apple")) // 0 + * String.Order("apple", "banana") // => -1 + * String.Order("banana", "apple") // => 1 + * String.Order("apple", "apple") // => 0 * ``` * * @category instances @@ -83,11 +82,11 @@ export const Order: order.Order = order.String * * **Example** (Comparing strings for equality) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.Equivalence("hello", "hello")) // true - * console.log(String.Equivalence("hello", "world")) // false + * String.Equivalence("hello", "hello") // => true + * String.Equivalence("hello", "world") // => false * ``` * * @category instances @@ -104,11 +103,11 @@ export const Equivalence: Equ.Equivalence = Equ.String * * **Example** (Referencing the empty string) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.empty) // "" - * console.log(String.isEmpty(String.empty)) // true + * String.empty // => "" + * String.isEmpty(String.empty) // => true * ``` * * @category constants @@ -121,11 +120,13 @@ export const empty: "" = "" as const * * **Example** (Concatenating string literal types) * - * ```ts + * ```ts import.meta.vitest * import type { String } from "effect" * * // Type-level concatenation * type Result = String.Concat<"hello", "world"> // "helloworld" + * + * const witness: Result = "helloworld" * ``` * * @category models @@ -138,14 +139,11 @@ export type Concat = `${A}${B}` * * **Example** (Concatenating strings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" * - * const result1 = String.concat("hello", "world") - * console.log(result1) // "helloworld" - * - * const result2 = pipe("hello", String.concat("world")) - * console.log(result2) // "helloworld" + * String.concat("hello", "world") // => "helloworld" + * pipe("hello", String.concat("world")) // => "helloworld" * ``` * * @category combining @@ -161,12 +159,11 @@ export const concat: { * * **Example** (Converting strings to uppercase) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("a", String.toUpperCase), "A") - * assert.deepStrictEqual(String.toUpperCase("hello"), "HELLO") + * pipe("a", String.toUpperCase) // => "A" + * String.toUpperCase("hello") // => "HELLO" * ``` * * @category transforming @@ -179,12 +176,11 @@ export const toUpperCase = (self: S): Uppercase => self.toU * * **Example** (Converting strings to lowercase) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("A", String.toLowerCase), "a") - * assert.deepStrictEqual(String.toLowerCase("HELLO"), "hello") + * pipe("A", String.toLowerCase) // => "a" + * String.toLowerCase("HELLO") // => "hello" * ``` * * @category transforming @@ -197,12 +193,11 @@ export const toLowerCase = (self: T): Lowercase => self.toL * * **Example** (Capitalizing a string) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("abc", String.capitalize), "Abc") - * assert.deepStrictEqual(String.capitalize("hello"), "Hello") + * pipe("abc", String.capitalize) // => "Abc" + * String.capitalize("hello") // => "Hello" * ``` * * @category transforming @@ -219,12 +214,11 @@ export const capitalize = (self: T): Capitalize => { * * **Example** (Uncapitalizing a string) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("ABC", String.uncapitalize), "aBC") - * assert.deepStrictEqual(String.uncapitalize("Hello"), "hello") + * pipe("ABC", String.uncapitalize) // => "aBC" + * String.uncapitalize("Hello") // => "hello" * ``` * * @category transforming @@ -246,15 +240,11 @@ export const uncapitalize = (self: T): Uncapitalize => { * * **Example** (Replacing a substring) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("abc", String.replace("b", "d")), "adc") - * assert.deepStrictEqual( - * pipe("hello world", String.replace("world", "Effect")), - * "hello Effect" - * ) + * pipe("abc", String.replace("b", "d")) // => "adc" + * pipe("hello world", String.replace("world", "Effect")) // => "hello Effect" * ``` * * @category transforming @@ -268,10 +258,12 @@ export const replace = (searchValue: string | RegExp, replaceValue: string) => ( * * **Example** (Trimming whitespace at the type level) * - * ```ts + * ```ts import.meta.vitest * import type { String } from "effect" * * type Result = String.Trim<" hello "> // "hello" + * + * const witness: Result = "hello" * ``` * * @category models @@ -284,12 +276,11 @@ export type Trim = TrimEnd> * * **Example** (Trimming whitespace) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.trim(" a "), "a") - * assert.deepStrictEqual(String.trim(" hello world "), "hello world") + * String.trim(" a ") // => "a" + * String.trim(" hello world ") // => "hello world" * ``` * * @category transforming @@ -302,10 +293,12 @@ export const trim = (self: A): Trim => self.trim() as Trim< * * **Example** (Trimming leading whitespace at the type level) * - * ```ts + * ```ts import.meta.vitest * import type { String } from "effect" * * type Result = String.TrimStart<" hello"> // "hello" + * + * const witness: Result = "hello" * ``` * * @category models @@ -318,12 +311,11 @@ export type TrimStart = A extends `${" " | "\n" | "\t" | "\r"} * * **Example** (Trimming leading whitespace) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.trimStart(" a "), "a ") - * assert.deepStrictEqual(String.trimStart(" hello world"), "hello world") + * String.trimStart(" a ") // => "a " + * String.trimStart(" hello world") // => "hello world" * ``` * * @category transforming @@ -336,10 +328,12 @@ export const trimStart = (self: A): TrimStart => self.trimS * * **Example** (Trimming trailing whitespace at the type level) * - * ```ts + * ```ts import.meta.vitest * import type { String } from "effect" * * type Result = String.TrimEnd<"hello "> // "hello" + * + * const witness: Result = "hello" * ``` * * @category models @@ -352,12 +346,11 @@ export type TrimEnd = A extends `${infer B}${" " | "\n" | "\t" * * **Example** (Trimming trailing whitespace) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.trimEnd(" a "), " a") - * assert.deepStrictEqual(String.trimEnd("hello world "), "hello world") + * String.trimEnd(" a ") // => " a" + * String.trimEnd("hello world ") // => "hello world" * ``` * * @category transforming @@ -370,12 +363,11 @@ export const trimEnd = (self: A): TrimEnd => self.trimEnd() * * **Example** (Slicing strings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("abcd", String.slice(1, 3)), "bc") - * assert.deepStrictEqual(pipe("hello world", String.slice(0, 5)), "hello") + * pipe("abcd", String.slice(1, 3)) // => "bc" + * pipe("hello world", String.slice(0, 5)) // => "hello" * ``` * * @category transforming @@ -388,15 +380,14 @@ export const slice = (start?: number, end?: number) => (self: string): string => * * **Example** (Checking for empty strings) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.isEmpty(""), true) - * assert.deepStrictEqual(String.isEmpty("a"), false) + * String.isEmpty("") // => true + * String.isEmpty("a") // => false * ``` * - * @category predicates + * @category guards * @since 2.0.0 */ export const isEmpty = (self: string): self is "" => self.length === 0 @@ -406,15 +397,14 @@ export const isEmpty = (self: string): self is "" => self.length === 0 * * **Example** (Checking for non-empty strings) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.isNonEmpty(""), false) - * assert.deepStrictEqual(String.isNonEmpty("a"), true) + * String.isNonEmpty("") // => false + * String.isNonEmpty("a") // => true * ``` * - * @category guards + * @category predicates * @since 2.0.0 */ export const isNonEmpty = (self: string): boolean => self.length > 0 @@ -424,11 +414,10 @@ export const isNonEmpty = (self: string): boolean => self.length > 0 * * **Example** (Getting string length) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.length("abc"), 3) + * String.length("abc") // => 3 * ``` * * @category getters @@ -441,13 +430,12 @@ export const length = (self: string): number => self.length * * **Example** (Splitting strings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("abc", String.split("")), ["a", "b", "c"]) - * assert.deepStrictEqual(pipe("", String.split("")), [""]) - * assert.deepStrictEqual(String.split("hello,world", ","), ["hello", "world"]) + * pipe("abc", String.split("")) // => ["a", "b", "c"] + * pipe("", String.split("")) // => [""] + * String.split("hello,world", ",") // => ["hello", "world"] * ``` * * @category transforming @@ -467,12 +455,11 @@ export const split: { * * **Example** (Checking for substrings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("hello world", String.includes("world")), true) - * assert.deepStrictEqual(pipe("hello world", String.includes("foo")), false) + * pipe("hello world", String.includes("world")) // => true + * pipe("hello world", String.includes("foo")) // => false * ``` * * @category predicates @@ -486,12 +473,11 @@ export const includes = (searchString: string, position?: number) => (self: stri * * **Example** (Checking string prefixes) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("hello world", String.startsWith("hello")), true) - * assert.deepStrictEqual(pipe("hello world", String.startsWith("world")), false) + * pipe("hello world", String.startsWith("hello")) // => true + * pipe("hello world", String.startsWith("world")) // => false * ``` * * @category predicates @@ -505,12 +491,11 @@ export const startsWith = (searchString: string, position?: number) => (self: st * * **Example** (Checking string suffixes) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("hello world", String.endsWith("world")), true) - * assert.deepStrictEqual(pipe("hello world", String.endsWith("hello")), false) + * pipe("hello world", String.endsWith("world")) // => true + * pipe("hello world", String.endsWith("hello")) // => false * ``` * * @category predicates @@ -524,14 +509,14 @@ export const endsWith = (searchString: string, position?: number) => (self: stri * * **Example** (Reading character codes) * - * ```ts - * import { String } from "effect" + * ```ts import.meta.vitest + * import { Option, String } from "effect" * - * String.charCodeAt("abc", 1) // Option.some(98) - * String.charCodeAt("abc", 4) // Option.none() + * String.charCodeAt("abc", 1) // => Option.some(98) + * String.charCodeAt("abc", 4) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const charCodeAt: { @@ -548,11 +533,11 @@ export const charCodeAt: { * * **Example** (Extracting substrings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" * - * pipe("abcd", String.substring(1)) // "bcd" - * pipe("abcd", String.substring(1, 3)) // "bc" + * pipe("abcd", String.substring(1)) // => "bcd" + * pipe("abcd", String.substring(1, 3)) // => "bc" * ``` * * @category transforming @@ -565,14 +550,14 @@ export const substring = (start: number, end?: number) => (self: string): string * * **Example** (Accessing characters safely) * - * ```ts - * import { pipe, String } from "effect" + * ```ts import.meta.vitest + * import { Option, pipe, String } from "effect" * - * pipe("abc", String.at(1)) // Option.some("b") - * pipe("abc", String.at(4)) // Option.none() + * pipe("abc", String.at(1)) // => Option.some("b") + * pipe("abc", String.at(4)) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const at: { @@ -585,14 +570,14 @@ export const at: { * * **Example** (Reading characters safely) * - * ```ts - * import { pipe, String } from "effect" + * ```ts import.meta.vitest + * import { Option, pipe, String } from "effect" * - * pipe("abc", String.charAt(1)) // Option.some("b") - * pipe("abc", String.charAt(4)) // Option.none() + * pipe("abc", String.charAt(1)) // => Option.some("b") + * pipe("abc", String.charAt(4)) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const charAt: { @@ -608,14 +593,14 @@ export const charAt: { * * **Example** (Reading code points) * - * ```ts - * import { pipe, String } from "effect" + * ```ts import.meta.vitest + * import { Option, pipe, String } from "effect" * - * pipe("abc", String.codePointAt(1)) // Option.some(98) - * pipe("abc", String.codePointAt(10)) // Option.none() + * pipe("abc", String.codePointAt(1)) // => Option.some(98) + * pipe("abc", String.codePointAt(10)) // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const codePointAt: { @@ -628,11 +613,11 @@ export const codePointAt: { * * **Example** (Finding the first substring index) * - * ```ts - * import { pipe, String } from "effect" + * ```ts import.meta.vitest + * import { Option, pipe, String } from "effect" * - * pipe("abbbc", String.indexOf("b")) // Option.some(1) - * pipe("abbbc", String.indexOf("z")) // Option.none() + * pipe("abbbc", String.indexOf("b")) // => Option.some(1) + * pipe("abbbc", String.indexOf("z")) // => Option.none() * ``` * * @category searching @@ -646,11 +631,11 @@ export const indexOf = (searchString: string) => (self: string): Option.Option Option.some(3) + * pipe("abbbc", String.lastIndexOf("d")) // => Option.none() * ``` * * @category searching @@ -666,16 +651,15 @@ export const lastIndexOf = (searchString: string) => (self: string): Option.Opti * * **Example** (Comparing strings by locale) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("a", String.localeCompare("b")), -1) - * assert.deepStrictEqual(pipe("b", String.localeCompare("a")), 1) - * assert.deepStrictEqual(pipe("a", String.localeCompare("a")), 0) + * pipe("a", String.localeCompare("b")) // => -1 + * pipe("b", String.localeCompare("a")) // => 1 + * pipe("a", String.localeCompare("a")) // => 0 * ``` * - * @category comparing + * @category comparisons * @since 2.0.0 */ export const localeCompare = @@ -688,16 +672,15 @@ export const localeCompare = * * **Example** (Matching regular expressions) * - * ```ts + * ```ts import.meta.vitest * import { Option, pipe, String } from "effect" * - * const match = pipe("hello", String.match(/l+/)) - * - * if (Option.isSome(match)) { - * console.log(`${match.value[0]}@${match.value.index}`) // "ll@2" - * } - * - * console.log(Option.isNone(pipe("hello", String.match(/x/)))) // true + * pipe( + * "hello", + * String.match(/l+/), + * Option.map((match) => [match[0], match.index]) + * ) // => Option.some(["ll", 2]) + * pipe("hello", String.match(/x/)) // => Option.none() * ``` * * @category searching @@ -712,13 +695,12 @@ export const match = (regExp: RegExp | string) => (self: string): Option.Option< * * **Example** (Iterating regular expression matches) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" * * const matches = pipe("hello world", String.matchAll(/l/g)) - * console.log( - * Array.from(matches, (match) => `${match[0]}@${match.index}`).join(", ") - * ) // "l@2, l@3, l@9" + * + * Array.from(matches, (match) => [match[0], match.index]) // => [["l", 2], ["l", 3], ["l", 9]] * ``` * * @category searching @@ -731,19 +713,21 @@ export const matchAll = (regExp: RegExp) => (self: string): IterableIterator character.codePointAt(0)) // => [0x1e9b, 0x323] + * Array.from(pipe(str, String.normalize("NFC")), (character) => character.codePointAt(0)) // => [0x1e9b, 0x323] + * Array.from( + * pipe(str, String.normalize("NFD")), + * (character) => character.codePointAt(0) + * ) // => [0x17f, 0x323, 0x307] + * Array.from(pipe(str, String.normalize("NFKC")), (character) => character.codePointAt(0)) // => [0x1e69] + * Array.from( * pipe(str, String.normalize("NFKD")), - * "\u0073\u0323\u0307" - * ) + * (character) => character.codePointAt(0) + * ) // => [0x73, 0x323, 0x307] * ``` * * @category transforming @@ -756,12 +740,11 @@ export const normalize = (form?: "NFC" | "NFD" | "NFKC" | "NFKD") => (self: stri * * **Example** (Padding strings at the end) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("a", String.padEnd(5)), "a ") - * assert.deepStrictEqual(pipe("a", String.padEnd(5, "_")), "a____") + * pipe("a", String.padEnd(5)) // => "a " + * pipe("a", String.padEnd(5, "_")) // => "a____" * ``` * * @category transforming @@ -775,12 +758,11 @@ export const padEnd = (maxLength: number, fillString?: string) => (self: string) * * **Example** (Padding strings at the start) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("a", String.padStart(5)), " a") - * assert.deepStrictEqual(pipe("a", String.padStart(5, "_")), "____a") + * pipe("a", String.padStart(5)) // => " a" + * pipe("a", String.padStart(5, "_")) // => "____a" * ``` * * @category transforming @@ -794,12 +776,11 @@ export const padStart = (maxLength: number, fillString?: string) => (self: strin * * **Example** (Repeating strings) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("a", String.repeat(5)), "aaaaa") - * assert.deepStrictEqual(pipe("hello", String.repeat(3)), "hellohellohello") + * pipe("a", String.repeat(5)) // => "aaaaa" + * pipe("hello", String.repeat(3)) // => "hellohellohello" * ``` * * @category transforming @@ -812,12 +793,11 @@ export const repeat = (count: number) => (self: string): string => self.repeat(c * * **Example** (Replacing all matches) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(pipe("ababb", String.replaceAll("b", "c")), "acacc") - * assert.deepStrictEqual(pipe("ababb", String.replaceAll(/ba/g, "cc")), "accbb") + * pipe("ababb", String.replaceAll("b", "c")) // => "acacc" + * pipe("ababb", String.replaceAll(/ba/g, "cc")) // => "accbb" * ``` * * @category transforming @@ -832,12 +812,12 @@ export const replaceAll = (searchValue: string | RegExp, replaceValue: string) = * * **Example** (Searching strings) * - * ```ts - * import { String } from "effect" + * ```ts import.meta.vitest + * import { Option, String } from "effect" * - * String.search("ababb", "b") // Option.some(1) - * String.search("ababb", /abb/) // Option.some(2) - * String.search("ababb", "d") // Option.none() + * String.search("ababb", "b") // => Option.some(1) + * String.search("ababb", /abb/) // => Option.some(2) + * String.search("ababb", "d") // => Option.none() * ``` * * @category searching @@ -857,12 +837,11 @@ export const search: { * * **Example** (Lowercasing strings by locale) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * * const str = "\u0130" - * assert.deepStrictEqual(pipe(str, String.toLocaleLowerCase("tr")), "i") + * pipe(str, String.toLocaleLowerCase("tr")) // => "i" * ``` * * @category transforming @@ -876,12 +855,11 @@ export const toLocaleLowerCase = (locale?: string | Array) => (self: str * * **Example** (Uppercasing strings by locale) * - * ```ts + * ```ts import.meta.vitest * import { pipe, String } from "effect" - * import * as assert from "node:assert" * * const str = "i\u0307" - * assert.deepStrictEqual(pipe(str, String.toLocaleUpperCase("lt-LT")), "I") + * pipe(str, String.toLocaleUpperCase("lt-LT")) // => "I" * ``` * * @category transforming @@ -904,11 +882,10 @@ export const toLocaleUpperCase = (locale?: string | Array) => (self: str * * **Example** (Taking characters from the start) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.takeLeft("Hello World", 5), "Hello") + * String.takeLeft("Hello World", 5) // => "Hello" * ``` * * @category transforming @@ -933,11 +910,10 @@ export const takeLeft: { * * **Example** (Taking characters from the end) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" - * import * as assert from "node:assert" * - * assert.deepStrictEqual(String.takeRight("Hello World", 5), "World") + * String.takeRight("Hello World", 5) // => "World" * ``` * * @category transforming @@ -960,11 +936,10 @@ const LF = 0x0a * * **Example** (Iterating lines without separators) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * const lines = String.linesIterator("hello\nworld\n") - * console.log(Array.from(lines)) // ["hello", "world"] + * Array.from(String.linesIterator("hello\nworld\n")) // => ["hello", "world"] * ``` * * @category splitting @@ -978,11 +953,10 @@ export const linesIterator = (self: string): LinesIterator => linesSeparated(sel * * **Example** (Iterating lines with separators) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * const lines = String.linesWithSeparators("hello\nworld\n") - * console.log(Array.from(lines)) // ["hello\n", "world\n"] + * Array.from(String.linesWithSeparators("hello\nworld\n")) // => ["hello\n", "world\n"] * ``` * * @category splitting @@ -996,12 +970,10 @@ export const linesWithSeparators = (s: string): LinesIterator => linesSeparated( * * **Example** (Stripping custom margins) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * const text = " |hello\n |world" - * const result = String.stripMarginWith(text, "|") - * console.log(result) // "hello\nworld" + * String.stripMarginWith(" |hello\n |world", "|") // => "hello\nworld" * ``` * * @category transforming @@ -1035,12 +1007,10 @@ export const stripMarginWith: { * * **Example** (Stripping pipe margins) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * const text = " |hello\n |world" - * const result = String.stripMargin(text) - * console.log(result) // "hello\nworld" + * String.stripMargin(" |hello\n |world") // => "hello\nworld" * ``` * * @category transforming @@ -1053,17 +1023,18 @@ export const stripMargin = (self: string): string => stripMarginWith(self, "|") * * **Example** (Converting snake_case to camelCase) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.snakeToCamel("hello_world")) // "helloWorld" - * console.log(String.snakeToCamel("foo_bar_baz")) // "fooBarBaz" + * String.snakeToCamel("hello_world") // => "helloWorld" + * String.snakeToCamel("foo_bar_baz") // => "fooBarBaz" * ``` * * @category transforming * @since 2.0.0 */ export const snakeToCamel = (self: string): string => { + if (self.length === 0) return self let str = self[0] for (let i = 1; i < self.length; i++) { str += self[i] === "_" ? self[++i].toUpperCase() : self[i] @@ -1076,17 +1047,18 @@ export const snakeToCamel = (self: string): string => { * * **Example** (Converting snake_case to PascalCase) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.snakeToPascal("hello_world")) // "HelloWorld" - * console.log(String.snakeToPascal("foo_bar_baz")) // "FooBarBaz" + * String.snakeToPascal("hello_world") // => "HelloWorld" + * String.snakeToPascal("foo_bar_baz") // => "FooBarBaz" * ``` * * @category transforming * @since 2.0.0 */ export const snakeToPascal = (self: string): string => { + if (self.length === 0) return self let str = self[0].toUpperCase() for (let i = 1; i < self.length; i++) { str += self[i] === "_" ? self[++i].toUpperCase() : self[i] @@ -1099,11 +1071,11 @@ export const snakeToPascal = (self: string): string => { * * **Example** (Converting snake_case to kebab-case) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.snakeToKebab("hello_world")) // "hello-world" - * console.log(String.snakeToKebab("foo_bar_baz")) // "foo-bar-baz" + * String.snakeToKebab("hello_world") // => "hello-world" + * String.snakeToKebab("foo_bar_baz") // => "foo-bar-baz" * ``` * * @category transforming @@ -1116,11 +1088,11 @@ export const snakeToKebab = (self: string): string => self.replace(/_/g, "-") * * **Example** (Converting camelCase to snake_case) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.camelToSnake("helloWorld")) // "hello_world" - * console.log(String.camelToSnake("fooBarBaz")) // "foo_bar_baz" + * String.camelToSnake("helloWorld") // => "hello_world" + * String.camelToSnake("fooBarBaz") // => "foo_bar_baz" * ``` * * @category transforming @@ -1133,11 +1105,11 @@ export const camelToSnake = (self: string): string => self.replace(/([A-Z])/g, " * * **Example** (Converting PascalCase to snake_case) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.pascalToSnake("HelloWorld")) // "hello_world" - * console.log(String.pascalToSnake("FooBarBaz")) // "foo_bar_baz" + * String.pascalToSnake("HelloWorld") // => "hello_world" + * String.pascalToSnake("FooBarBaz") // => "foo_bar_baz" * ``` * * @category transforming @@ -1151,11 +1123,11 @@ export const pascalToSnake = (self: string): string => * * **Example** (Converting kebab-case to snake_case) * - * ```ts + * ```ts import.meta.vitest * import { String } from "effect" * - * console.log(String.kebabToSnake("hello-world")) // "hello_world" - * console.log(String.kebabToSnake("foo-bar-baz")) // "foo_bar_baz" + * String.kebabToSnake("hello-world") // => "hello_world" + * String.kebabToSnake("foo-bar-baz") // => "foo_bar_baz" * ``` * * @category transforming @@ -1264,15 +1236,20 @@ export const noCase: { readonly delimiter?: string | undefined readonly transform?: (part: string, index: number, parts: ReadonlyArray) => string }): string => { + const splitRegExp = toRegExpArray(options?.splitRegExp ?? SPLIT_REGEXP) + const stripRegExp = toRegExpArray(options?.stripRegExp ?? STRIP_REGEXP) const delimiter = options?.delimiter ?? " " const transform = options?.transform ?? toLowerCase - return normalizeCase(input, SPLIT_REGEXP, STRIP_REGEXP, delimiter, transform) + return normalizeCase(input, splitRegExp, stripRegExp, delimiter, transform) }) +const toRegExpArray = (regexp: RegExp | ReadonlyArray): ReadonlyArray => + predicate.isRegExp(regexp) ? [regexp] : regexp + const normalizeCase = ( input: string, splitRegExp: ReadonlyArray, - stripRegExp: RegExp, + stripRegExp: ReadonlyArray, delimiter: string, transform: (part: string, index: number, parts: ReadonlyArray) => string ): string => { @@ -1280,7 +1257,9 @@ const normalizeCase = ( for (const regexp of splitRegExp) { result = result.replace(regexp, "$1\0$2") } - result = result.replace(stripRegExp, "\0") + for (const regexp of stripRegExp) { + result = result.replace(regexp, "\0") + } let start = 0 let end = result.length // Trim the delimiter from around the output string. @@ -1400,7 +1379,7 @@ export const constantCase: (self: string) => string = noCase({ * @since 4.0.0 */ export const configCase: (self: string) => string = (self) => - normalizeCase(self, CONFIG_SPLIT_REGEXP, STRIP_REGEXP, "_", toUpperCase) + normalizeCase(self, CONFIG_SPLIT_REGEXP, [STRIP_REGEXP], "_", toUpperCase) /** * Converts a string to kebab-case (lowercase with hyphens). diff --git a/.context/effect/packages/effect/src/Struct.ts b/.context/effect/packages/effect/src/Struct.ts index 9f0318f55..14416763e 100644 --- a/.context/effect/packages/effect/src/Struct.ts +++ b/.context/effect/packages/effect/src/Struct.ts @@ -14,6 +14,7 @@ import * as Combiner from "./Combiner.ts" import * as Equivalence from "./Equivalence.ts" import { dual } from "./Function.ts" +import * as InternalRecord from "./internal/record.ts" import * as order from "./Order.ts" import * as Reducer from "./Reducer.ts" @@ -31,7 +32,7 @@ import * as Reducer from "./Reducer.ts" * * **Example** (Flattening an intersection) * - * ```ts + * ```ts import.meta.vitest * import type { Struct } from "effect" * * type Original = { a: string } & { b: number } @@ -39,6 +40,8 @@ import * as Reducer from "./Reducer.ts" * // Without Simplify, the type displays as `{ a: string } & { b: number }` * type Simplified = Struct.Simplify * // { a: string; b: number } + * + * const witness: Simplified = { a: "value", b: 1 } * ``` * * @see {@link Mutable} – also flattens but removes `readonly` @@ -62,12 +65,16 @@ export type Simplify = { [K in keyof T]: T[K] } & {} * * **Example** (Making a readonly type mutable) * - * ```ts + * ```ts import.meta.vitest * import type { Struct } from "effect" * * type ReadOnly = { readonly a: string; readonly b: number } * type Writable = Struct.Mutable * // { a: string; b: number } + * + * const witness: Writable = { a: "value", b: 1 } + * witness.b = 2 + * witness // => { a: "value", b: 2 } * ``` * * @see {@link Simplify} – flattens intersections without removing `readonly` @@ -91,13 +98,15 @@ export type Mutable = { -readonly [K in keyof T]: T[K] } & {} * * **Example** (Merging two types with overlapping keys) * - * ```ts + * ```ts import.meta.vitest * import type { Struct } from "effect" * * type A = { a: string; b: number } * type B = { b: boolean; c: string } * type Merged = Struct.Assign * // { a: string; b: boolean; c: string } + * + * const witness: Merged = { a: "value", b: true, c: "other" } * ``` * * @see {@link assign} – the runtime equivalent @@ -120,11 +129,10 @@ export type Assign = Simplify "Alice" * ``` * * @see {@link keys} – list all string keys of a struct @@ -151,18 +159,18 @@ export const get: { * * **Example** (Reading typed keys) * - * ```ts + * ```ts import.meta.vitest * import { Struct } from "effect" * * const user = { name: "Alice", age: 30, [Symbol.for("id")]: 1 } * * const k: Array<"name" | "age"> = Struct.keys(user) - * console.log(k) // ["name", "age"] + * k // => ["name", "age"] * ``` * * @see {@link get} – access a single key's value * @see {@link pick} – select a subset of keys into a new struct - * @category Key utilities + * @category getters * @since 3.6.0 */ export const keys = (self: S): Array<(keyof S) & string> => @@ -181,12 +189,11 @@ export const keys = (self: S): Array<(keyof S) & string> => * * **Example** (Selecting specific properties) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const user = { name: "Alice", age: 30, admin: true } - * const nameAndAge = pipe(user, Struct.pick(["name", "age"])) - * console.log(nameAndAge) // { name: "Alice", age: 30 } + * pipe(user, Struct.pick(["name", "age"])) // => { name: "Alice", age: 30 } * ``` * * @see {@link omit} – the inverse (exclude keys instead) @@ -219,12 +226,11 @@ export const pick: { * * **Example** (Removing a property) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const user = { name: "Alice", age: 30, password: "secret" } - * const safe = pipe(user, Struct.omit(["password"])) - * console.log(safe) // { name: "Alice", age: 30 } + * pipe(user, Struct.omit(["password"])) // => { name: "Alice", age: 30 } * ``` * * @see {@link pick} – the inverse (keep only specified keys) @@ -257,13 +263,12 @@ export const omit: { * * **Example** (Merging structs with overlapping keys) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const defaults = { theme: "light", lang: "en" } * const overrides = { theme: "dark", fontSize: 14 } - * const config = pipe(defaults, Struct.assign(overrides)) - * console.log(config) // { theme: "dark", lang: "en", fontSize: 14 } + * pipe(defaults, Struct.assign(overrides)) // => { theme: "dark", lang: "en", fontSize: 14 } * ``` * * @see {@link Assign} – the type-level equivalent @@ -302,7 +307,7 @@ type Evolved = Simplify< * * **Example** (Transforming selected values) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const result = pipe( @@ -312,7 +317,7 @@ type Evolved = Simplify< * age: (n) => n + 1 * }) * ) - * console.log(result) // { name: "ALICE", age: 31, active: true } + * result // => { name: "ALICE", age: 31, active: true } * ``` * * @see {@link evolveKeys} – transform keys instead of values @@ -352,7 +357,7 @@ type KeyEvolved = Simplify< * * **Example** (Renaming keys with functions) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const result = pipe( @@ -361,13 +366,13 @@ type KeyEvolved = Simplify< * name: (k) => k.toUpperCase() * }) * ) - * console.log(result) // { NAME: "Alice", age: 30 } + * result // => { NAME: "Alice", age: 30 } * ``` * * @see {@link renameKeys} – rename keys with a static mapping * @see {@link evolve} – transform values instead of keys * @see {@link evolveEntries} – transform both keys and values - * @category Key utilities + * @category transforming * @since 4.0.0 */ export const evolveKeys: { @@ -407,7 +412,7 @@ type EntryEvolved = { * * **Example** (Transforming keys and values together) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const result = pipe( @@ -417,7 +422,7 @@ type EntryEvolved = { * label: (k, v) => [k, v.toUpperCase()] * }) * ) - * console.log(result) // { amountCents: 10000, label: "TOTAL" } + * result // => { amountCents: 10000, label: "TOTAL" } * ``` * * @see {@link evolve} – transform values only @@ -449,19 +454,19 @@ export const evolveEntries: { * * **Example** (Renaming keys) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * const result = pipe( * { firstName: "Alice", lastName: "Smith", age: 30 }, * Struct.renameKeys({ firstName: "first", lastName: "last" }) * ) - * console.log(result) // { first: "Alice", last: "Smith", age: 30 } + * result // => { first: "Alice", last: "Smith", age: 30 } * ``` * * @see {@link evolveKeys} – rename keys using functions * @see {@link evolveEntries} – rename keys and transform values - * @category Key utilities + * @category transforming * @since 4.0.0 */ export const renameKeys: { @@ -494,7 +499,7 @@ export const renameKeys: { * * **Example** (Comparing structs for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Struct } from "effect" * * const PersonEquivalence = Struct.makeEquivalence({ @@ -502,10 +507,8 @@ export const renameKeys: { * age: Equivalence.strictEqual() * }) * - * console.log(PersonEquivalence({ name: "Alice", age: 30 }, { name: "Alice", age: 30 })) - * // true - * console.log(PersonEquivalence({ name: "Alice", age: 30 }, { name: "Bob", age: 30 })) - * // false + * PersonEquivalence({ name: "Alice", age: 30 }, { name: "Alice", age: 30 }) // => true + * PersonEquivalence({ name: "Alice", age: 30 }, { name: "Bob", age: 30 }) // => false * ``` * * @see {@link makeOrder} – create an `Order` for structs @@ -531,7 +534,7 @@ export const makeEquivalence = Equivalence.Struct * * **Example** (Ordering structs by name then age) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Struct } from "effect" * * const PersonOrder = Struct.makeOrder({ @@ -539,8 +542,7 @@ export const makeEquivalence = Equivalence.Struct * age: Number.Order * }) * - * console.log(PersonOrder({ name: "Alice", age: 30 }, { name: "Bob", age: 25 })) - * // -1 (Alice comes before Bob) + * PersonOrder({ name: "Alice", age: 30 }, { name: "Bob", age: 25 }) // => -1 * ``` * * @see {@link makeEquivalence} – create an `Equivalence` for structs @@ -566,18 +568,20 @@ export const makeOrder = order.Struct * * **Example** (Defining a lambda type) * - * ```ts + * ```ts import.meta.vitest * import type { Struct } from "effect" * * interface ToString extends Struct.Lambda { * readonly "~lambda.out": string * } + * + * const witness: ToString = { "~lambda.in": 1, "~lambda.out": "1" } * ``` * * @see {@link Apply} – apply a Lambda to a concrete type * @see {@link lambda} – create a runtime lambda value * @see {@link map} – use a lambda to transform all struct values - * @category Lambda + * @category utility types * @since 4.0.0 */ export interface Lambda { @@ -601,19 +605,21 @@ export interface Lambda { * * **Example** (Computing the output type of a lambda) * - * ```ts + * ```ts import.meta.vitest * import type { Struct } from "effect" * * interface ToString extends Struct.Lambda { * readonly "~lambda.out": string * } * - * // Result is `string` + * // string * type Result = Struct.Apply + * + * const witness: Result = "value" * ``` * * @see {@link Lambda} – the base interface - * @category Lambda + * @category utility types * @since 4.0.0 */ export type Apply = (L & { readonly "~lambda.in": V })["~lambda.out"] @@ -636,7 +642,7 @@ export type Apply = (L & { readonly "~lambda.in": V })["~la * * **Example** (Wrapping values in arrays) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * interface AsArray extends Struct.Lambda { @@ -646,12 +652,12 @@ export type Apply = (L & { readonly "~lambda.in": V })["~la * * const asArray = Struct.lambda((a) => [a]) * const result = pipe({ x: 1, y: "hello" }, Struct.map(asArray)) - * console.log(result) // { x: [1], y: ["hello"] } + * result // => { x: [1], y: ["hello"] } * ``` * * @see {@link Lambda} – the type-level interface * @see {@link map} – apply a lambda to all struct values - * @category Lambda + * @category constructors * @since 4.0.0 */ export const lambda = any>( @@ -672,7 +678,7 @@ export const lambda = any>( * * **Example** (Wrapping every value in an array) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * interface AsArray extends Struct.Lambda { @@ -682,7 +688,7 @@ export const lambda = any>( * * const asArray = Struct.lambda((a) => [a]) * const result = pipe({ width: 10, height: 20 }, Struct.map(asArray)) - * console.log(result) // { width: [10], height: [20] } + * result // => { width: [10], height: [20] } * ``` * * @see {@link mapPick} – apply a lambda only to selected keys @@ -716,7 +722,7 @@ export const map: { * * **Example** (Wrapping only selected values in arrays) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * interface AsArray extends Struct.Lambda { @@ -729,7 +735,7 @@ export const map: { * { x: 1, y: 2, z: 3 }, * Struct.mapPick(["x", "z"], asArray) * ) - * console.log(result) // { x: [1], y: 2, z: [3] } + * result // => { x: [1], y: 2, z: [3] } * ``` * * @see {@link map} – apply a lambda to all keys @@ -770,7 +776,7 @@ export const mapPick: { * * **Example** (Wrapping all values except one in arrays) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct } from "effect" * * interface AsArray extends Struct.Lambda { @@ -783,7 +789,7 @@ export const mapPick: { * { x: 1, y: 2, z: 3 }, * Struct.mapOmit(["y"], asArray) * ) - * console.log(result) // { x: [1], y: 2, z: [3] } + * result // => { x: [1], y: 2, z: [3] } * ``` * * @see {@link map} – apply a lambda to all keys @@ -836,7 +842,7 @@ function buildStruct< const res = f(k, source[k]) if (res) { const [nk, nv] = res - out[nk] = nv + InternalRecord.assignProperty(out, nk, nv) } } return out @@ -859,7 +865,7 @@ function buildStruct< * * **Example** (Combining struct properties) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Struct } from "effect" * * const C = Struct.makeCombiner<{ readonly n: number; readonly s: string }>({ @@ -867,8 +873,7 @@ function buildStruct< * s: String.ReducerConcat * }) * - * const result = C.combine({ n: 1, s: "hello" }, { n: 2, s: " world" }) - * console.log(result) // { n: 3, s: "hello world" } + * C.combine({ n: 1, s: "hello" }, { n: 2, s: " world" }) // => { n: 3, s: "hello world" } * ``` * * @see {@link makeReducer} – like `makeCombiner` but with an initial value @@ -888,7 +893,7 @@ export function makeCombiner( for (const key of keys) { const merge = combiners[key].combine(self[key], that[key]) if (omitKeyWhen(merge)) continue - out[key] = merge + InternalRecord.assignProperty(out as object, key, merge) } return out }) @@ -912,7 +917,7 @@ export function makeCombiner( * * **Example** (Reducing a collection of structs) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Struct } from "effect" * * const R = Struct.makeReducer<{ readonly n: number; readonly s: string }>({ @@ -925,7 +930,7 @@ export function makeCombiner( * { n: 2, s: "b" }, * { n: 3, s: "c" } * ]) - * console.log(result) // { n: 6, s: "abc" } + * result // => { n: 6, s: "abc" } * ``` * * @see {@link makeCombiner} – like `makeReducer` but without an initial value @@ -943,7 +948,7 @@ export function makeReducer( for (const key of Reflect.ownKeys(reducers) as Array) { const iv = reducers[key].initialValue if (options?.omitKeyWhen?.(iv)) continue - initialValue[key] = iv + InternalRecord.assignProperty(initialValue as object, key, iv) } return Reducer.make(combine, initialValue) } @@ -957,11 +962,10 @@ export function makeReducer( * * **Example** (Creating a record) * - * ```ts + * ```ts import.meta.vitest * import { Struct } from "effect" * - * const record = Struct.Record(["a", "b"], "value") - * console.log(record) // { a: "value", b: "value" } + * Struct.Record(["a", "b"], "value") // => { a: "value", b: "value" } * ``` * * @category constructors @@ -973,7 +977,7 @@ export function Record, Value> ): Record { const out: any = {} for (const key of keys) { - out[key] = value + InternalRecord.assignProperty(out, key, value) } return out } diff --git a/.context/effect/packages/effect/src/SubscriptionRef.ts b/.context/effect/packages/effect/src/SubscriptionRef.ts index 57a9f487b..1a4adf732 100644 --- a/.context/effect/packages/effect/src/SubscriptionRef.ts +++ b/.context/effect/packages/effect/src/SubscriptionRef.ts @@ -129,7 +129,7 @@ export const make = (value: A): Effect.Effect> => * * **Example** (Streaming changes) * - * ```ts + * ```ts import.meta.vitest * import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -148,13 +148,13 @@ export const make = (value: A): Effect.Effect> => * yield* SubscriptionRef.set(ref, 2) * * const values = yield* Fiber.join(fiber) - * console.log(values) // [ 0, 1, 2 ] + * return Array.from(values) * }) * - * Effect.runPromise(program) + * await Effect.runPromise(program) // => [0, 1, 2] * ``` * - * @category changes + * @category subscriptions * @since 4.0.0 */ export const changes = (self: SubscriptionRef): Stream.Stream => Stream.fromPubSub(self.pubsub) @@ -175,15 +175,16 @@ export const changes = (self: SubscriptionRef): Stream.Stream => Stream * * **Example** (Reading the current value unsafely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(42) * - * const value = SubscriptionRef.getUnsafe(ref) - * console.log(value) + * return SubscriptionRef.getUnsafe(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category getters @@ -196,15 +197,16 @@ export const getUnsafe = (self: SubscriptionRef): A => self.value * * **Example** (Reading the current value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(42) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category getters @@ -218,18 +220,18 @@ export const get = (self: SubscriptionRef): Effect.Effect => Effect.syn * * **Example** (Getting and setting a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * * const oldValue = yield* SubscriptionRef.getAndSet(ref, 20) - * console.log("Old value:", oldValue) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [oldValue, newValue] * }) + * + * await Effect.runPromise(program) // => [10, 20] * ``` * * @category getters @@ -256,18 +258,18 @@ const setUnsafe = (self: SubscriptionRef, value: A) => { * * **Example** (Getting and updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * * const oldValue = yield* SubscriptionRef.getAndUpdate(ref, (n) => n * 2) - * console.log("Old value:", oldValue) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [oldValue, newValue] * }) + * + * await Effect.runPromise(program) // => [10, 20] * ``` * * @category getters @@ -290,7 +292,7 @@ export const getAndUpdate: { * * **Example** (Getting and updating with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -300,11 +302,11 @@ export const getAndUpdate: { * ref, * (n) => Effect.succeed(n + 5) * ) - * console.log("Old value:", oldValue) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [oldValue, newValue] * }) + * + * await Effect.runPromise(program) // => [10, 15] * ``` * * @category getters @@ -317,7 +319,7 @@ export const getAndUpdateEffect: { self: SubscriptionRef, update: (a: A) => Effect.Effect ) => - self.semaphore.withPermit(Effect.sync(() => { + self.semaphore.withPermit(Effect.suspend(() => { const current = self.value return Effect.map(update(current), (newValue) => { setUnsafe(self, newValue) @@ -341,7 +343,7 @@ export const getAndUpdateEffect: { * * **Example** (Getting and conditionally updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -351,11 +353,11 @@ export const getAndUpdateEffect: { * ref, * (n) => n > 5 ? Option.some(n * 2) : Option.none() * ) - * console.log("Old value:", oldValue) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [oldValue, newValue] * }) + * + * await Effect.runPromise(program) // => [10, 20] * ``` * * @category getters @@ -372,7 +374,7 @@ export const getAndUpdateSome: { const current = self.value const option = update(current) if (Option.isNone(option)) { - return Effect.succeed(current) + return current } setUnsafe(self, option.value) return current @@ -394,7 +396,7 @@ export const getAndUpdateSome: { * * **Example** (Getting and conditionally updating with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -404,11 +406,11 @@ export const getAndUpdateSome: { * ref, * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) * ) - * console.log("Old value:", oldValue) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [oldValue, newValue] * }) + * + * await Effect.runPromise(program) // => [10, 13] * ``` * * @category getters @@ -441,7 +443,7 @@ export const getAndUpdateSomeEffect: { * * **Example** (Modifying a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -451,14 +453,14 @@ export const getAndUpdateSomeEffect: { * `Old value was ${n}`, * n * 2 * ]) - * console.log(result) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [result, newValue] * }) + * + * await Effect.runPromise(program) // => ["Old value was 10", 20] * ``` * - * @category modifications + * @category mutations * @since 2.0.0 */ export const modify: { @@ -481,7 +483,7 @@ export const modify: { * * **Example** (Modifying with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -491,14 +493,14 @@ export const modify: { * ref, * (n) => Effect.succeed([`Doubled from ${n}`, n * 2] as const) * ) - * console.log(result) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [result, newValue] * }) + * + * await Effect.runPromise(program) // => ["Doubled from 10", 20] * ``` * - * @category modifications + * @category mutations * @since 2.0.0 */ export const modifyEffect: { @@ -536,7 +538,7 @@ export const modifyEffect: { * * **Example** (Conditionally modifying a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -547,14 +549,14 @@ export const modifyEffect: { * (n) => * n > 5 ? ["Updated", Option.some(n * 2)] : ["Not updated", Option.none()] * ) - * console.log(result) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [result, newValue] * }) + * + * await Effect.runPromise(program) // => ["Updated", 20] * ``` * - * @category modifications + * @category mutations * @since 2.0.0 */ export const modifySome: { @@ -592,7 +594,7 @@ export const modifySome: { * * **Example** (Conditionally modifying with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -607,14 +609,14 @@ export const modifySome: { * : (["Not updated", Option.none()] as const) * ) * ) - * console.log(result) - * * const newValue = yield* SubscriptionRef.get(ref) - * console.log("New value:", newValue) + * return [result, newValue] * }) + * + * await Effect.runPromise(program) // => ["Updated", 15] * ``` * - * @category modifications + * @category mutations * @since 2.0.0 */ export const modifySomeEffect: { @@ -643,7 +645,7 @@ export const modifySomeEffect: { * * **Example** (Setting a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -651,12 +653,13 @@ export const modifySomeEffect: { * * yield* SubscriptionRef.set(ref, 42) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * - * @category setters + * @category mutations * @since 2.0.0 */ export const set: { @@ -673,18 +676,19 @@ export const set: { * * **Example** (Setting and reading the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(0) * - * const newValue = yield* SubscriptionRef.setAndGet(ref, 42) - * console.log("New value:", newValue) + * return yield* SubscriptionRef.setAndGet(ref, 42) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * - * @category setters + * @category mutations * @since 2.0.0 */ export const setAndGet: { @@ -702,7 +706,7 @@ export const setAndGet: { * * **Example** (Updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -710,12 +714,13 @@ export const setAndGet: { * * yield* SubscriptionRef.update(ref, (n) => n * 2) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 20 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const update: { @@ -733,7 +738,7 @@ export const update: { * * **Example** (Updating with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -741,12 +746,13 @@ export const update: { * * yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5)) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 15 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateEffect: { @@ -766,18 +772,19 @@ export const updateEffect: { * * **Example** (Updating and reading the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * - * const newValue = yield* SubscriptionRef.updateAndGet(ref, (n) => n * 2) - * console.log("New value:", newValue) + * return yield* SubscriptionRef.updateAndGet(ref, (n) => n * 2) * }) + * + * await Effect.runPromise(program) // => 20 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateAndGet: { @@ -797,21 +804,22 @@ export const updateAndGet: { * * **Example** (Updating with an effect and reading the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * - * const newValue = yield* SubscriptionRef.updateAndGetEffect( + * return yield* SubscriptionRef.updateAndGetEffect( * ref, * (n) => Effect.succeed(n + 5) * ) - * console.log("New value:", newValue) * }) + * + * await Effect.runPromise(program) // => 15 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateAndGetEffect: { @@ -835,7 +843,7 @@ export const updateAndGetEffect: { * * **Example** (Conditionally updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -846,12 +854,13 @@ export const updateAndGetEffect: { * (n) => n > 5 ? Option.some(n * 2) : Option.none() * ) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 20 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateSome: { @@ -883,7 +892,7 @@ export const updateSome: { * * **Example** (Conditionally updating with an effect) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -894,12 +903,13 @@ export const updateSome: { * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) * ) * - * const value = yield* SubscriptionRef.get(ref) - * console.log(value) + * return yield* SubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 13 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateSomeEffect: { @@ -937,21 +947,22 @@ export const updateSomeEffect: { * * **Example** (Conditionally updating and reading the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * - * const newValue = yield* SubscriptionRef.updateSomeAndGet( + * return yield* SubscriptionRef.updateSomeAndGet( * ref, * (n) => n > 5 ? Option.some(n * 2) : Option.none() * ) - * console.log("New value:", newValue) * }) + * + * await Effect.runPromise(program) // => 20 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateSomeAndGet: { @@ -985,21 +996,22 @@ export const updateSomeAndGet: { * * **Example** (Conditionally updating with an effect and reading the new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, SubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* SubscriptionRef.make(10) * - * const newValue = yield* SubscriptionRef.updateSomeAndGetEffect( + * return yield* SubscriptionRef.updateSomeAndGetEffect( * ref, * (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) * ) - * console.log("New value:", newValue) * }) + * + * await Effect.runPromise(program) // => 13 * ``` * - * @category updating + * @category mutations * @since 2.0.0 */ export const updateSomeAndGetEffect: { diff --git a/.context/effect/packages/effect/src/Symbol.ts b/.context/effect/packages/effect/src/Symbol.ts index b359115da..de444c25d 100644 --- a/.context/effect/packages/effect/src/Symbol.ts +++ b/.context/effect/packages/effect/src/Symbol.ts @@ -18,11 +18,11 @@ import * as predicate from "./Predicate.ts" * * **Example** (Checking for symbols) * - * ```ts + * ```ts import.meta.vitest * import { Symbol } from "effect" * - * console.log(Symbol.isSymbol(globalThis.Symbol.for("a"))) // true - * console.log(Symbol.isSymbol("a")) // false + * Symbol.isSymbol(globalThis.Symbol.for("a")) // => true + * Symbol.isSymbol("a") // => false * ``` * * @category guards diff --git a/.context/effect/packages/effect/src/SynchronizedRef.ts b/.context/effect/packages/effect/src/SynchronizedRef.ts index 282849626..5bbf95662 100644 --- a/.context/effect/packages/effect/src/SynchronizedRef.ts +++ b/.context/effect/packages/effect/src/SynchronizedRef.ts @@ -226,7 +226,7 @@ export const getAndUpdateSome: { } = dual( 2, (self: SynchronizedRef, pf: (a: A) => Option.Option): Effect.Effect => - self.semaphore.withPermit(Ref.getAndUpdateSome(self, pf)) + self.semaphore.withPermit(Ref.getAndUpdateSome(self.backing, pf)) ) /** diff --git a/.context/effect/packages/effect/src/Terminal.ts b/.context/effect/packages/effect/src/Terminal.ts index f59175692..c2311d184 100644 --- a/.context/effect/packages/effect/src/Terminal.ts +++ b/.context/effect/packages/effect/src/Terminal.ts @@ -25,7 +25,7 @@ const TypeId = "~effect/platform/Terminal" * A `Terminal` represents a command-line interface which can read input from a * user and display messages to a user. * - * @category models + * @category services * @since 4.0.0 */ export interface Terminal { @@ -118,10 +118,10 @@ const QuitErrorTypeId = "effect/platform/Terminal/QuitError" * * @see {@link isQuitError} for checking unknown errors when handling terminal cancellation * - * @category QuitError + * @category errors * @since 4.0.0 */ -export class QuitError extends Schema.ErrorClass("QuitError")({ +export class QuitError extends Schema.Error("QuitError")({ _tag: Schema.tag("QuitError") }) { /** diff --git a/.context/effect/packages/effect/src/Tracer.ts b/.context/effect/packages/effect/src/Tracer.ts index 028829ded..967109401 100644 --- a/.context/effect/packages/effect/src/Tracer.ts +++ b/.context/effect/packages/effect/src/Tracer.ts @@ -22,7 +22,7 @@ import * as Option from "./Option.ts" * `span` to allocate a span from the supplied name, parent, annotations, * links, start time, kind, root flag, and sampling decision. * - * @category models + * @category services * @since 2.0.0 */ export interface Tracer { @@ -61,7 +61,7 @@ export interface EffectPrimitive { * * **Example** (Creating span statuses) * - * ```ts + * ```ts import.meta.vitest * import { Exit } from "effect" * import type { Tracer } from "effect" * @@ -80,8 +80,8 @@ export interface EffectPrimitive { * exit: Exit.succeed("result") * } * - * console.log(startedStatus._tag) // "Started" - * console.log(endedStatus.endTime - endedStatus.startTime) // 500000000n + * startedStatus._tag // => "Started" + * endedStatus.endTime - endedStatus.startTime // => 500_000_000n * ``` * * @category models @@ -103,20 +103,19 @@ export type SpanStatus = { * * **Example** (Accepting any span) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Tracer } from "effect" * * // Function that accepts any span type - * const logSpan = (span: Tracer.AnySpan) => { - * console.log(`Span ID: ${span.spanId}, Trace ID: ${span.traceId}`) - * return Effect.succeed(span) - * } + * const getSpanIds = (span: Tracer.AnySpan) => Effect.succeed([span.spanId, span.traceId]) * * // Works with both Span and ExternalSpan * const externalSpan = Tracer.externalSpan({ * spanId: "span-123", * traceId: "trace-456" * }) + * + * await Effect.runPromise(getSpanIds(externalSpan)) // => ["span-123", "trace-456"] * ``` * * @category models @@ -134,11 +133,11 @@ export type AnySpan = Span | ExternalSpan * * **Example** (Reading the parent span key) * - * ```ts + * ```ts import.meta.vitest * import { Tracer } from "effect" * * // The key used to identify parent spans in the context - * console.log(Tracer.ParentSpanKey) // "effect/Tracer/ParentSpan" + * Tracer.ParentSpanKey // => "effect/Tracer/ParentSpan" * ``` * * @category constants @@ -152,20 +151,23 @@ export const ParentSpanKey = "effect/Tracer/ParentSpan" * * **Example** (Accessing the parent span) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Tracer } from "effect" * * // Access the parent span from the context * const program = Effect.gen(function*() { * const parentSpan = yield* Effect.service(Tracer.ParentSpan) - * console.log(`Parent span: ${parentSpan.spanId}`) + * return parentSpan.spanId * }) + * + * const parent = Tracer.externalSpan({ spanId: "span-123", traceId: "trace-456" }) + * await Effect.runPromise(Effect.provideService(program, Tracer.ParentSpan, parent)) // => "span-123" * ``` * * @category services * @since 2.0.0 */ -export class ParentSpan extends Context.Service()(ParentSpanKey) {} +export class ParentSpan extends Context.Service()(ParentSpanKey, { fiberCached: true }) {} /** * Represents a span created outside Effect's tracer, carrying trace and span @@ -174,7 +176,7 @@ export class ParentSpan extends Context.Service()(ParentSpa * * **Example** (Creating an external span value) * - * ```ts + * ```ts import.meta.vitest * import { Context } from "effect" * import type { Tracer } from "effect" * @@ -187,7 +189,7 @@ export class ParentSpan extends Context.Service()(ParentSpa * annotations: Context.empty() * } * - * console.log(`External span: ${externalSpan.spanId}`) + * externalSpan.spanId // => "span-abc-123" * ``` * * @category models @@ -208,9 +210,8 @@ export interface ExternalSpan { * * **Example** (Configuring span options) * - * ```ts - * import { Effect } from "effect" - * import type { Tracer } from "effect" + * ```ts import.meta.vitest + * import { Effect, Tracer } from "effect" * * // Create an effect with span options * const options: Tracer.SpanOptions = { @@ -223,6 +224,19 @@ export interface ExternalSpan { * const program = Effect.succeed("Hello World").pipe( * Effect.withSpan("my-operation", options) * ) + * + * const spans: Array = [] + * const tracer = Tracer.make({ + * span(options) { + * const span = new Tracer.NativeSpan(options) + * spans.push(span) + * return span + * } + * }) + * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "Hello World" + * + * spans[0]?.attributes.get("user.id") // => "123" + * spans[0]?.status._tag // => "Ended" * ``` * * @category options @@ -266,22 +280,27 @@ export interface TraceOptions { * * **Example** (Configuring span kinds) * - * ```ts - * import { Effect } from "effect" - * import type { Tracer } from "effect" + * ```ts import.meta.vitest + * import { Effect, Tracer } from "effect" * * // Different span kinds for different operations - * const serverSpan = Effect.withSpan("handle-request", { - * kind: "server" as Tracer.SpanKind - * }) + * const program = Effect.succeed("handled").pipe( + * Effect.withSpan("handle-request", { + * kind: "server" as Tracer.SpanKind + * }) + * ) * - * const clientSpan = Effect.withSpan("api-call", { - * kind: "client" as Tracer.SpanKind + * const spans: Array = [] + * const tracer = Tracer.make({ + * span(options) { + * const span = new Tracer.NativeSpan(options) + * spans.push(span) + * return span + * } * }) + * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "handled" * - * const internalSpan = Effect.withSpan("internal-process", { - * kind: "internal" as Tracer.SpanKind - * }) + * spans[0]?.kind // => "server" * ``` * * @category models @@ -296,12 +315,13 @@ export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer * * **Example** (Working with spans) * - * ```ts + * ```ts import.meta.vitest * import { Context, Exit, Option } from "effect" * import type { Tracer } from "effect" * * const attributes = new Map() * const links: Array = [] + * const events: Array<[name: string, startTime: bigint, attributes: Record]> = [] * let status: Tracer.SpanStatus = { * _tag: "Started", * startTime: 1_000_000_000n @@ -328,7 +348,7 @@ export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer * attributes.set(key, value) * }, * event(name, startTime, eventAttributes = {}) { - * console.log(`${name} at ${startTime} with ${Object.keys(eventAttributes).length} attributes`) + * events.push([name, startTime, eventAttributes]) * }, * addLinks(newLinks) { * links.push(...newLinks) @@ -336,11 +356,13 @@ export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer * } * * span.attribute("user.id", "123") + * span.event("loaded", 1_250_000_000n, { "cache.hit": true }) * span.end(1_500_000_000n, Exit.succeed("user")) * - * console.log(span.name) // "load-user" - * console.log(span.attributes.get("user.id")) // "123" - * console.log(span.status._tag) // "Ended" + * span.name // => "load-user" + * span.attributes.get("user.id") // => "123" + * span.status._tag // => "Ended" + * events // => [["loaded", 1_250_000_000n, { "cache.hit": true }]] * ``` * * @category models @@ -370,7 +392,7 @@ export interface Span { * * **Example** (Linking spans) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Tracer } from "effect" * * // Create a span link to connect spans @@ -387,6 +409,19 @@ export interface Span { * const program = Effect.succeed("result").pipe( * Effect.withSpan("linked-operation", { links: [link] }) * ) + * + * const spans: Array = [] + * const tracer = Tracer.make({ + * span(options) { + * const span = new Tracer.NativeSpan(options) + * spans.push(span) + * return span + * } + * }) + * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) // => "result" + * + * spans[0]?.links[0]?.span.spanId // => "external-span-123" + * spans[0]?.links[0]?.attributes["link.type"] // => "follows-from" * ``` * * @category models @@ -425,8 +460,8 @@ export const make = (options: Tracer): Tracer => options * * **Example** (Creating an external span) * - * ```ts - * import { Effect, Tracer } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, Tracer } from "effect" * * // Create an external span from another tracing system * const span = Tracer.externalSpan({ @@ -439,6 +474,19 @@ export const make = (options: Tracer): Tracer => options * const program = Effect.succeed("Hello").pipe( * Effect.withSpan("child-operation", { parent: span }) * ) + * + * const spans: Array = [] + * const tracer = Tracer.make({ + * span(options) { + * const span = new Tracer.NativeSpan(options) + * spans.push(span) + * return span + * } + * }) + * const value = await Effect.runPromise(Effect.provideService(program, Tracer.Tracer, tracer)) + * + * value // => "Hello" + * spans.map((span) => Option.getOrUndefined(span.parent)?.spanId) // => ["span-abc-123"] * ``` * * @category constructors @@ -474,18 +522,18 @@ export const externalSpan = ( * * **Example** (Disabling span propagation) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Tracer } from "effect" * * // Disable span propagation for a specific effect - * const program = Effect.gen(function*() { - * yield* Effect.log("This will not propagate parent span") - * }).pipe( + * const program = Tracer.DisablePropagation.pipe( * Effect.provideService(Tracer.DisablePropagation, true) * ) + * + * await Effect.runPromise(program) // => true * ``` * - * @category references + * @category services * @since 3.12.0 */ export const DisablePropagation = Context.Reference( @@ -508,7 +556,7 @@ export const DisablePropagation = Context.Reference( * * @see {@link MinimumTraceLevel} for the threshold that decides whether spans at that level are sampled * - * @category references + * @category services * @since 4.0.0 */ export const CurrentTraceLevel: Context.Reference = Context.Reference( @@ -537,7 +585,7 @@ export const CurrentTraceLevel: Context.Reference = Context.Reference< * * @see {@link CurrentTraceLevel} for the default span level used when options do not specify one * - * @category references + * @category services * @since 4.0.0 */ export const MinimumTraceLevel = Context.Reference< @@ -552,7 +600,7 @@ export const MinimumTraceLevel = Context.Reference< * Use when you need the raw context key for active tracer lookup in lower-level * tracing code. * - * @category references + * @category constants * @since 4.0.0 */ export const TracerKey = "effect/Tracer" @@ -563,26 +611,25 @@ export const TracerKey = "effect/Tracer" * * **Example** (Accessing the current tracer) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Tracer } from "effect" * * // Access the current tracer from the context * const program = Effect.gen(function*() { * const tracer = yield* Effect.service(Tracer.Tracer) - * console.log("Using current tracer") + * // Or use the built-in tracer effect + * const tracerFromAccessor = yield* Effect.tracer + * return tracer === tracerFromAccessor * }) * - * // Or use the built-in tracer effect - * const tracerEffect = Effect.gen(function*() { - * const tracer = yield* Effect.tracer - * console.log("Current tracer obtained") - * }) + * await Effect.runPromise(program) // => true * ``` * - * @category references + * @category services * @since 2.0.0 */ export const Tracer: Context.Reference = Context.Reference(TracerKey, { + fiberCached: true, defaultValue: () => make({ span: (options) => new NativeSpan(options) @@ -602,7 +649,7 @@ export const Tracer: Context.Reference = Context.Reference(Trace * * @see {@link Span} for the interface implemented by native spans * - * @category native tracer + * @category models * @since 4.0.0 */ export class NativeSpan implements Span { diff --git a/.context/effect/packages/effect/src/Trie.ts b/.context/effect/packages/effect/src/Trie.ts index 025fc0411..c9203a880 100644 --- a/.context/effect/packages/effect/src/Trie.ts +++ b/.context/effect/packages/effect/src/Trie.ts @@ -26,8 +26,8 @@ const TypeId = TR.TrieTypeId * * **Example** (Using a trie for prefix search) * - * ```ts - * import { Trie } from "effect" + * ```ts import.meta.vitest + * import { Option, Trie } from "effect" * * // Create a trie with string-to-number mappings * const trie: Trie.Trie = Trie.make( @@ -38,24 +38,20 @@ const TypeId = TR.TrieTypeId * ) * * // Get values by exact key - * console.log(Trie.get(trie, "apple")) // Some(1) - * console.log(Trie.get(trie, "grape")) // None + * Trie.get(trie, "apple") // => Option.some(1) + * Trie.get(trie, "grape") // => Option.none() * * // Find all keys with a prefix - * console.log(Array.from(Trie.keysWithPrefix(trie, "app"))) - * // ["app", "apple", "application"] + * Array.from(Trie.keysWithPrefix(trie, "app")) // => ["app", "apple", "application"] * * // Iterate over all entries (sorted alphabetically) - * for (const [key, value] of trie) { - * console.log(`${key}: ${value}`) - * } - * // Output: "app: 2", "apple: 1", "application: 3", "banana: 4" + * Array.from(trie) // => [["app", 2], ["apple", 1], ["application", 3], ["banana", 4]] * * // Check if key exists - * console.log(Trie.has(trie, "app")) // true + * Trie.has(trie, "app") // => true * * // Get size - * console.log(Trie.size(trie)) // 4 + * Trie.size(trie) // => 4 * ``` * * @category models @@ -72,14 +68,13 @@ export interface Trie extends Iterable<[string, Value]>, Equal, Pi * * **Example** (Creating an empty trie) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty() * - * assert.equal(Trie.size(trie), 0) - * assert.deepStrictEqual(Array.from(trie), []) + * Trie.size(trie) // => 0 + * Array.from(trie) // => [] * ``` * * @category constructors @@ -92,9 +87,8 @@ export const empty: () => Trie = TR.empty * * **Example** (Creating a trie from entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } from "effect" * * const iterable: Array = [["call", 0], ["me", 1], [ * "mind", @@ -103,17 +97,8 @@ export const empty: () => Trie = TR.empty * 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 - * ) + * Array.from(trie) // => [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]] + * trie // => Trie.make(["call", 0], ["me", 1], ["mind", 2], ["mid", 3]) * ``` * * @category constructors @@ -126,17 +111,13 @@ export const fromIterable: (entries: Iterable) => Trie< * * **Example** (Constructing a trie from entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } 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 - * ) + * Array.from(trie) // => [["ca", 0], ["me", 1]] + * trie // => Trie.fromIterable([["ca", 0], ["me", 1]]) * ``` * * @category constructors @@ -151,9 +132,8 @@ export const make: >( * * **Example** (Inserting entries) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie1 = Trie.empty().pipe( * Trie.insert("call", 0) @@ -162,13 +142,10 @@ export const make: >( * 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 - * ]]) + * Array.from(trie1) // => [["call", 0]] + * Array.from(trie2) // => [["call", 0], ["me", 1]] + * Array.from(trie3) // => [["call", 0], ["me", 1], ["mind", 2]] + * Array.from(trie4) // => [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]] * ``` * * @category mutations @@ -188,9 +165,8 @@ export const insert: { * * **Example** (Reading keys in alphabetical order) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("cab", 0), @@ -198,8 +174,7 @@ export const insert: { * Trie.insert("bca", 2) * ) * - * const result = Array.from(Trie.keys(trie)) - * assert.deepStrictEqual(result, ["abc", "bca", "cab"]) + * Array.from(Trie.keys(trie)) // => ["abc", "bca", "cab"] * ``` * * @category getters @@ -216,9 +191,8 @@ export const keys: (self: Trie) => IterableIterator = TR.keys * * **Example** (Reading values by key order) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("call", 0), @@ -226,8 +200,7 @@ export const keys: (self: Trie) => IterableIterator = TR.keys * Trie.insert("and", 2) * ) * - * const result = Array.from(Trie.values(trie)) - * assert.deepStrictEqual(result, [2, 0, 1]) + * Array.from(Trie.values(trie)) // => [2, 0, 1] * ``` * * @category getters @@ -244,17 +217,15 @@ export const values: (self: Trie) => IterableIterator = TR.values * * **Example** (Reading entries in alphabetical order) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * 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]]) + * Array.from(Trie.entries(trie)) // => [["call", 0], ["me", 1]] * ``` * * @category getters @@ -271,17 +242,14 @@ export const entries: (self: Trie) => IterableIterator<[string, V]> = TR.e * * **Example** (Converting entries to an array) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * 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]]) + * Trie.toEntries(trie) // => [["call", 0], ["me", 1]] * ``` * * @category getters @@ -295,9 +263,8 @@ export const toEntries = (self: Trie): Array<[string, V]> => Array.from(en * * **Example** (Finding keys with a prefix) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("she", 0), @@ -306,8 +273,7 @@ export const toEntries = (self: Trie): Array<[string, V]> => Array.from(en * Trie.insert("shore", 3) * ) * - * const result = Array.from(Trie.keysWithPrefix(trie, "she")) - * assert.deepStrictEqual(result, ["she", "shells"]) + * Array.from(Trie.keysWithPrefix(trie, "she")) // => ["she", "shells"] * ``` * * @category getters @@ -324,9 +290,8 @@ export const keysWithPrefix: { * * **Example** (Finding values with a prefix) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("she", 0), @@ -335,10 +300,7 @@ export const keysWithPrefix: { * Trie.insert("shore", 3) * ) * - * const result = Array.from(Trie.valuesWithPrefix(trie, "she")) - * - * // 0: "she", 1: "shells" - * assert.deepStrictEqual(result, [0, 1]) + * Array.from(Trie.valuesWithPrefix(trie, "she")) // => [0, 1] * ``` * * @category getters @@ -355,9 +317,8 @@ export const valuesWithPrefix: { * * **Example** (Finding entries with a prefix) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("she", 0), @@ -366,8 +327,7 @@ export const valuesWithPrefix: { * Trie.insert("shore", 3) * ) * - * const result = Array.from(Trie.entriesWithPrefix(trie, "she")) - * assert.deepStrictEqual(result, [["she", 0], ["shells", 1]]) + * Array.from(Trie.entriesWithPrefix(trie, "she")) // => [["she", 0], ["shells", 1]] * ``` * * @category getters @@ -384,9 +344,8 @@ export const entriesWithPrefix: { * * **Example** (Converting prefixed entries to an array) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -395,8 +354,7 @@ export const entriesWithPrefix: { * Trie.insert("she", 3) * ) * - * const result = Trie.toEntriesWithPrefix(trie, "she") - * assert.deepStrictEqual(result, [["she", 3], ["shells", 0]]) + * Trie.toEntriesWithPrefix(trie, "she") // => [["she", 3], ["shells", 0]] * ``` * * @category getters @@ -413,9 +371,8 @@ export const toEntriesWithPrefix: { * * **Example** (Finding the longest prefix) * - * ```ts - * import { Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Option, Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -423,14 +380,8 @@ export const toEntriesWithPrefix: { * Trie.insert("she", 2) * ) * - * const none = Trie.longestPrefixOf(trie, "sell") - * const some = Trie.longestPrefixOf(trie, "sells") - * - * assert.equal(none._tag, "None") - * assert.equal(some._tag, "Some") - * if (some._tag === "Some") { - * assert.deepStrictEqual(some.value, ["sells", 1]) - * } + * Trie.longestPrefixOf(trie, "sell") // => Option.none() + * Trie.longestPrefixOf(trie, "sells") // => Option.some(["sells", 1]) * ``` * * @category getters @@ -446,16 +397,15 @@ export const longestPrefixOf: { * * **Example** (Getting the size) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("a", 0), * Trie.insert("b", 1) * ) * - * assert.equal(Trie.size(trie), 2) + * Trie.size(trie) // => 2 * ``` * * @category getters @@ -468,9 +418,8 @@ export const size: (self: Trie) => number = TR.size * * **Example** (Looking up values safely) * - * ```ts + * ```ts import.meta.vitest * import { Option, Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("call", 0), @@ -479,17 +428,17 @@ export const size: (self: Trie) => number = TR.size * 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()) + * Trie.get(trie, "call") // => Option.some(0) + * Trie.get(trie, "me") // => Option.some(1) + * Trie.get(trie, "mind") // => Option.some(2) + * Trie.get(trie, "mid") // => Option.some(3) + * Trie.get(trie, "cale") // => Option.none() + * Trie.get(trie, "ma") // => Option.none() + * Trie.get(trie, "midn") // => Option.none() + * Trie.get(trie, "mea") // => Option.none() * ``` * - * @category elements + * @category getters * @since 2.0.0 */ export const get: { @@ -502,9 +451,8 @@ export const get: { * * **Example** (Checking key membership) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("call", 0), @@ -513,17 +461,17 @@ export const get: { * 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) + * Trie.has(trie, "call") // => true + * Trie.has(trie, "me") // => true + * Trie.has(trie, "mind") // => true + * Trie.has(trie, "mid") // => true + * Trie.has(trie, "cale") // => false + * Trie.has(trie, "ma") // => false + * Trie.has(trie, "midn") // => false + * Trie.has(trie, "mea") // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -536,18 +484,17 @@ export const has: { * * **Example** (Checking whether a trie is empty) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty() * const trie1 = trie.pipe(Trie.insert("ma", 0)) * - * assert.equal(Trie.isEmpty(trie), true) - * assert.equal(Trie.isEmpty(trie1), false) + * Trie.isEmpty(trie) // => true + * Trie.isEmpty(trie1) // => false * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const isEmpty: (self: Trie) => boolean = TR.isEmpty @@ -567,16 +514,18 @@ export const isEmpty: (self: Trie) => boolean = TR.isEmpty * * **Example** (Looking up values unsafely) * - * ```ts - * import { Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Result, Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("call", 0), * Trie.insert("me", 1) * ) * - * assert.throws(() => Trie.getUnsafe(trie, "mae")) + * Result.try({ + * try: () => Trie.getUnsafe(trie, "mae"), + * catch: (error) => (error as Error).message + * }) // => Result.fail("Expected trie to contain key") * ``` * * @category unsafe @@ -592,9 +541,8 @@ export const getUnsafe: { * * **Example** (Removing entries) * - * ```ts + * ```ts import.meta.vitest * import { Option, Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("call", 0), @@ -606,9 +554,9 @@ export const getUnsafe: { * 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()) + * Trie.get(trie, "call") // => Option.some(0) + * Trie.get(trie1, "call") // => Option.none() + * Trie.get(trie2, "call") // => Option.none() * ``` * * @category mutations @@ -624,9 +572,8 @@ export const remove: { * * **Example** (Reducing entries) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -634,24 +581,9 @@ export const remove: { * 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" - * ) + * trie.pipe(Trie.reduce(0, (acc, n) => acc + n)) // => 3 + * trie.pipe(Trie.reduce(10, (acc, n) => acc + n)) // => 13 + * trie.pipe(Trie.reduce("", (acc, _, key) => acc + key)) // => "sellssheshells" * ``` * * @category folding @@ -667,9 +599,8 @@ export const reduce: { * * **Example** (Mapping entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -677,20 +608,8 @@ export const reduce: { * 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) + * Trie.map(trie, (v) => v + 1) // => Trie.make(["shells", 1], ["sells", 2], ["she", 3]) + * Trie.map(trie, (_, k) => k.length) // => Trie.make(["shells", 6], ["sells", 5], ["she", 3]) * ``` * * @category folding @@ -706,9 +625,8 @@ export const map: { * * **Example** (Filtering entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -716,20 +634,8 @@ export const map: { * 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 - * ) + * Trie.filter(trie, (v) => v > 1) // => Trie.make(["she", 2]) + * Trie.filter(trie, (_, k) => k.length > 3) // => Trie.make(["shells", 0], ["sells", 1]) * ``` * * @category filtering @@ -748,9 +654,8 @@ export const filter: { * * **Example** (Filtering and mapping entries) * - * ```ts - * import { Equal, Result, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Result, Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -758,32 +663,11 @@ export const filter: { * 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 ? Result.succeed(v) : Result.failVoid), - * trieMapV - * ), - * true - * ) - * assert.equal( - * Equal.equals( - * Trie.filterMap( - * trie, - * (v, k) => k.length > 3 ? Result.succeed(v) : Result.failVoid - * ), - * trieMapK - * ), - * true - * ) + * Trie.filterMap(trie, (v) => v > 1 ? Result.succeed(v) : Result.failVoid) // => Trie.make(["she", 2]) + * Trie.filterMap( + * trie, + * (v, k) => k.length > 3 ? Result.succeed(v) : Result.failVoid + * ) // => Trie.make(["shells", 0], ["sells", 1]) * ``` * * @category filtering @@ -799,9 +683,8 @@ export const filterMap: { * * **Example** (Compacting optional values) * - * ```ts - * import { Equal, Option, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Option, Trie } from "effect" * * const trie = Trie.empty>().pipe( * Trie.insert("shells", Option.some(0)), @@ -809,12 +692,7 @@ export const filterMap: { * 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) + * Trie.compact(trie) // => Trie.make(["shells", 0], ["she", 2]) * ``` * * @category filtering @@ -827,9 +705,8 @@ export const compact: (self: Trie>) => Trie = TR.compact * * **Example** (Iterating over entries) * - * ```ts + * ```ts import.meta.vitest * import { Trie } from "effect" - * import * as assert from "node:assert" * * let value = 0 * @@ -842,7 +719,7 @@ export const compact: (self: Trie>) => Trie = TR.compact * }) * ) * - * assert.equal(value, 17) + * value // => 17 * ``` * * @category traversing @@ -858,9 +735,8 @@ export const forEach: { * * **Example** (Modifying an existing value) * - * ```ts - * import { Equal, Option, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Option, Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -868,12 +744,8 @@ export const forEach: { * 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) + * trie.pipe(Trie.modify("she", (v) => v + 10), Trie.get("she")) // => Option.some(12) + * trie.pipe(Trie.modify("me", (v) => v)) // => trie * ``` * * @category mutations @@ -889,9 +761,8 @@ export const modify: { * * **Example** (Removing multiple entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } from "effect" * * const trie = Trie.empty().pipe( * Trie.insert("shells", 0), @@ -899,13 +770,7 @@ export const modify: { * Trie.insert("she", 2) * ) * - * assert.equal( - * Equal.equals( - * trie.pipe(Trie.removeMany(["she", "sells"])), - * Trie.empty().pipe(Trie.insert("shells", 0)) - * ), - * true - * ) + * trie.pipe(Trie.removeMany(["she", "sells"])) // => Trie.make(["shells", 0]) * ``` * * @category mutations @@ -921,27 +786,16 @@ export const removeMany: { * * **Example** (Inserting multiple entries) * - * ```ts - * import { Equal, Trie } from "effect" - * import * as assert from "node:assert" + * ```ts import.meta.vitest + * import { Trie } from "effect" * * const trie = Trie.empty().pipe( - * Trie.insert("shells", 0), - * Trie.insert("sells", 1), - * Trie.insert("she", 2) + * Trie.insert("shells", 0) * ) * - * const trieInsert = Trie.empty().pipe( - * Trie.insert("shells", 0), - * Trie.insertMany( - * [["sells", 1], ["she", 2]] - * ) - * ) - * - * assert.equal( - * Equal.equals(trie, trieInsert), - * true - * ) + * trie.pipe( + * Trie.insertMany([["sells", 1], ["she", 2]]) + * ) // => Trie.make(["shells", 0], ["sells", 1], ["she", 2]) * ``` * * @category mutations diff --git a/.context/effect/packages/effect/src/Tuple.ts b/.context/effect/packages/effect/src/Tuple.ts index 8e4251fe4..f929c9f0a 100644 --- a/.context/effect/packages/effect/src/Tuple.ts +++ b/.context/effect/packages/effect/src/Tuple.ts @@ -32,11 +32,10 @@ import type { Apply, Lambda } from "./Struct.ts" * * **Example** (Creating a tuple) * - * ```ts + * ```ts import.meta.vitest * import { Tuple } from "effect" * - * const point = Tuple.make(10, 20, "red") - * console.log(point) // [10, 20, "red"] + * Tuple.make(10, 20, "red") // => [10, 20, "red"] * ``` * * @see {@link get} – access a single element by index @@ -44,7 +43,8 @@ import type { Apply, Lambda } from "./Struct.ts" * @category constructors * @since 2.0.0 */ -export const make = >(...elements: Elements): Elements => elements +export const make = >(...elements: [...Elements]): [...Elements] => + elements type Indices> = Exclude["length"], T["length"]> @@ -61,11 +61,10 @@ type Indices> = Exclude["length"], T * * **Example** (Extracting an element by index) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Tuple } from "effect" * - * const last = pipe(Tuple.make(1, true, "hello"), Tuple.get(2)) - * console.log(last) // "hello" + * pipe(Tuple.make(1, true, "hello"), Tuple.get(2)) // => "hello" * ``` * * @see {@link make} – create a tuple @@ -92,7 +91,9 @@ type _BuildTuple< [...I, unknown] > -type PickTuple, K> = _BuildTuple +type PickTuple, I extends ReadonlyArray>> = { + -readonly [K in keyof I]: T[I[K] & keyof T] +} /** * Creates a new tuple containing only the elements at the specified indices. @@ -107,11 +108,10 @@ type PickTuple, K> = _BuildTuple * * **Example** (Selecting elements by index) * - * ```ts + * ```ts import.meta.vitest * import { Tuple } from "effect" * - * const result = Tuple.pick(["a", "b", "c", "d"], [0, 2, 3]) - * console.log(result) // ["a", "c", "d"] + * Tuple.pick(["a", "b", "c", "d"], [0, 2, 3]) // => ["a", "c", "d"] * ``` * * @see {@link omit} – the inverse (exclude indices instead) @@ -122,11 +122,11 @@ type PickTuple, K> = _BuildTuple export const pick: { , const I extends ReadonlyArray>>( indices: I - ): (self: T) => PickTuple + ): (self: T) => PickTuple , const I extends ReadonlyArray>>( self: T, indices: I - ): PickTuple + ): PickTuple } = dual( 2, >( @@ -152,11 +152,10 @@ type OmitTuple, K> = _BuildTuple ["a", "c"] * ``` * * @see {@link pick} – the inverse (keep only specified indices) @@ -196,11 +195,10 @@ export const omit: { * * **Example** (Appending an element) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Tuple } from "effect" * - * const result = pipe(Tuple.make(1, 2), Tuple.appendElement("end")) - * console.log(result) // [1, 2, "end"] + * pipe(Tuple.make(1, 2), Tuple.appendElement("end")) // => [1, 2, "end"] * ``` * * @see {@link appendElements} – append multiple elements (another tuple) @@ -226,11 +224,10 @@ export const appendElement: { * * **Example** (Concatenating tuples) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Tuple } from "effect" * - * const result = pipe(Tuple.make(1, 2), Tuple.appendElements(["a", "b"] as const)) - * console.log(result) // [1, 2, "a", "b"] + * pipe(Tuple.make(1, 2), Tuple.appendElements(["a", "b"] as const)) // => [1, 2, "a", "b"] * ``` * * @see {@link appendElement} – append a single element @@ -270,17 +267,16 @@ type Evolved = { [I in keyof T]: I extends keyof E ? (E[I] extends (...a: * * **Example** (Transforming selected elements) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Tuple } from "effect" * - * const result = pipe( + * pipe( * Tuple.make("hello", 42, true), * Tuple.evolve([ * (s) => s.toUpperCase(), * (n) => n * 2 * ]) - * ) - * console.log(result) // ["HELLO", 84, true] + * ) // => ["HELLO", 84, true] * ``` * * @see {@link map} – apply the same transformation to all elements @@ -317,18 +313,17 @@ export const evolve: { * * **Example** (Swapping elements) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Tuple } from "effect" * - * const result = pipe( + * pipe( * Tuple.make("a", "b", "c"), * Tuple.renameIndices(["2", "1", "0"]) - * ) - * console.log(result) // ["c", "b", "a"] + * ) // => ["c", "b", "a"] * ``` * * @see {@link evolve} – transform element values instead of positions - * @category Index utilities + * @category transforming * @since 4.0.0 */ export const renameIndices: { @@ -367,7 +362,7 @@ export const renameIndices: { * * **Example** (Wrapping every element in an array) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct, Tuple } from "effect" * * interface AsArray extends Struct.Lambda { @@ -376,8 +371,7 @@ export const renameIndices: { * } * * const asArray = Struct.lambda((a) => [a]) - * const result = pipe(Tuple.make(1, "hello", true), Tuple.map(asArray)) - * console.log(result) // [[1], ["hello"], [true]] + * pipe(Tuple.make(1, "hello", true), Tuple.map(asArray)) // => [[1], ["hello"], [true]] * ``` * * @see {@link mapPick} – apply a lambda only to selected indices @@ -414,7 +408,7 @@ export const map: { * * **Example** (Wrapping only selected elements in arrays) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct, Tuple } from "effect" * * interface AsArray extends Struct.Lambda { @@ -423,11 +417,10 @@ export const map: { * } * * const asArray = Struct.lambda((a) => [a]) - * const result = pipe( + * pipe( * Tuple.make(1, "hello", true), * Tuple.mapPick([0, 2], asArray) - * ) - * console.log(result) // [[1], "hello", [true]] + * ) // => [[1], "hello", [true]] * ``` * * @see {@link map} – apply a lambda to all elements @@ -470,7 +463,7 @@ export const mapPick: { * * **Example** (Wrapping all elements except one in arrays) * - * ```ts + * ```ts import.meta.vitest * import { pipe, Struct, Tuple } from "effect" * * interface AsArray extends Struct.Lambda { @@ -479,11 +472,10 @@ export const mapPick: { * } * * const asArray = Struct.lambda((a) => [a]) - * const result = pipe( + * pipe( * Tuple.make(1, "hello", true), * Tuple.mapOmit([1], asArray) - * ) - * console.log(result) // [[1], "hello", [true]] + * ) // => [[1], "hello", [true]] * ``` * * @see {@link map} – apply a lambda to all elements @@ -530,7 +522,7 @@ export const mapOmit: { * * **Example** (Comparing tuples for equivalence) * - * ```ts + * ```ts import.meta.vitest * import { Equivalence, Tuple } from "effect" * * const eq = Tuple.makeEquivalence([ @@ -538,8 +530,8 @@ export const mapOmit: { * Equivalence.strictEqual() * ]) * - * console.log(eq(["Alice", 30], ["Alice", 30])) // true - * console.log(eq(["Alice", 30], ["Bob", 30])) // false + * eq(["Alice", 30], ["Alice", 30]) // => true + * eq(["Alice", 30], ["Bob", 30]) // => false * ``` * * @see {@link makeOrder} – create an `Order` for tuples @@ -564,13 +556,13 @@ export const makeEquivalence = Equivalence.Tuple * * **Example** (Ordering tuples) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Tuple } from "effect" * * const ord = Tuple.makeOrder([String.Order, Number.Order]) * - * console.log(ord(["Alice", 30], ["Bob", 25])) // -1 - * console.log(ord(["Alice", 30], ["Alice", 30])) // 0 + * ord(["Alice", 30], ["Bob", 25]) // => -1 + * ord(["Alice", 30], ["Alice", 30]) // => 0 * ``` * * @see {@link makeEquivalence} – create an `Equivalence` for tuples @@ -600,13 +592,12 @@ export { * * **Example** (Checking exact length) * - * ```ts + * ```ts import.meta.vitest * import { Tuple } from "effect" * * const arr: Array = [1, 2, 3] * if (Tuple.isTupleOf(arr, 3)) { - * console.log(arr) - * // ^? [number, number, number] + * arr // => [1, 2, 3] * } * ``` * @@ -635,13 +626,12 @@ export { * * **Example** (Checking minimum length) * - * ```ts + * ```ts import.meta.vitest * import { Tuple } from "effect" * * const arr: Array = [1, 2, 3, 4] * if (Tuple.isTupleOfAtLeast(arr, 3)) { - * console.log(arr) - * // ^? [number, number, number, ...number[]] + * arr // => [1, 2, 3, 4] * } * ``` * @@ -664,7 +654,7 @@ export { * * **Example** (Combining tuple elements) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Tuple } from "effect" * * const C = Tuple.makeCombiner([ @@ -672,8 +662,7 @@ export { * String.ReducerConcat * ]) * - * const result = C.combine([1, "hello"], [2, " world"]) - * console.log(result) // [3, "hello world"] + * C.combine([1, "hello"], [2, " world"]) // => [3, "hello world"] * ``` * * @see {@link makeReducer} – like `makeCombiner` but with an initial value @@ -705,7 +694,7 @@ export function makeCombiner>( * * **Example** (Reducing a collection of tuples) * - * ```ts + * ```ts import.meta.vitest * import { Number, String, Tuple } from "effect" * * const R = Tuple.makeReducer([ @@ -713,12 +702,11 @@ export function makeCombiner>( * String.ReducerConcat * ]) * - * const result = R.combineAll([ + * R.combineAll([ * [1, "a"], * [2, "b"], * [3, "c"] - * ]) - * console.log(result) // [6, "abc"] + * ]) // => [6, "abc"] * ``` * * @see {@link makeCombiner} – like `makeReducer` but without an initial value diff --git a/.context/effect/packages/effect/src/TxChunk.ts b/.context/effect/packages/effect/src/TxChunk.ts index ffabde747..e83aa2c09 100644 --- a/.context/effect/packages/effect/src/TxChunk.ts +++ b/.context/effect/packages/effect/src/TxChunk.ts @@ -35,7 +35,7 @@ const TypeId = "~effect/transactions/TxChunk" * * **Example** (Using a transactional chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -49,7 +49,6 @@ const TypeId = "~effect/transactions/TxChunk" * // Single operations - no explicit transaction needed * yield* TxChunk.append(txChunk, 4) * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] * * // Multi-step atomic operation - use explicit transaction * yield* Effect.tx( @@ -60,8 +59,10 @@ const TypeId = "~effect/transactions/TxChunk" * ) * * const finalResult = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(finalResult)) // [0, 1, 2, 3, 4, 5] + * return [Chunk.toArray(result), Chunk.toArray(finalResult)] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3, 4], [0, 1, 2, 3, 4, 5]] * ``` * * @category models @@ -100,7 +101,7 @@ const TxChunkProto = { * * **Example** (Creating a TxChunk from a chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -110,8 +111,10 @@ const TxChunkProto = { * * // Read the value - automatically transactional * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category constructors @@ -130,7 +133,7 @@ export const make = (initial: Chunk.Chunk): Effect.Effect> => * * **Example** (Creating an empty TxChunk) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -139,14 +142,15 @@ export const make = (initial: Chunk.Chunk): Effect.Effect> => * * // Check if it's empty - automatically transactional * const isEmpty = yield* TxChunk.isEmpty(txChunk) - * console.log(isEmpty) // true * * // Add elements - automatically transactional * yield* TxChunk.append(txChunk, 42) * * const isStillEmpty = yield* TxChunk.isEmpty(txChunk) - * console.log(isStillEmpty) // false + * return [isEmpty, isStillEmpty] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category constructors @@ -165,7 +169,7 @@ export const empty = (): Effect.Effect> => * * **Example** (Creating from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -174,7 +178,6 @@ export const empty = (): Effect.Effect> => * * // Read the contents - automatically transactional * const chunk = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(chunk)) // [1, 2, 3, 4, 5] * * // Multi-step atomic modification - use explicit transaction * yield* Effect.tx( @@ -185,8 +188,10 @@ export const empty = (): Effect.Effect> => * ) * * const updated = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(updated)) // [0, 1, 2, 3, 4, 5, 6] + * return [Chunk.toArray(chunk), Chunk.toArray(updated)] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5, 6]] * ``` * * @category constructors @@ -205,12 +210,13 @@ export const fromIterable = (iterable: Iterable): Effect.Effect * * **Example** (Wrapping an existing TxRef) * - * ```ts - * import { Chunk, TxChunk, TxRef } from "effect" + * ```ts import.meta.vitest + * import { Chunk, Effect, TxChunk, TxRef } from "effect" * * // Create a TxChunk from an existing TxRef (advanced usage) * const ref = TxRef.makeUnsafe(Chunk.fromIterable([1, 2, 3])) * const txChunk = TxChunk.makeUnsafe(ref) + * Chunk.toArray(await Effect.runPromise(TxChunk.get(txChunk))) // => [1, 2, 3] * ``` * * @category constructors @@ -233,7 +239,7 @@ export const makeUnsafe = (ref: TxRef.TxRef>): TxChunk => { * * **Example** (Modifying while returning a value) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -245,11 +251,11 @@ export const makeUnsafe = (ref: TxRef.TxRef>): TxChunk => { * Chunk.append(chunk, 4) // new value * ]) * - * console.log(oldSize) // 3 - * * const newChunk = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(newChunk)) // [1, 2, 3, 4] + * return [oldSize, Chunk.toArray(newChunk)] * }) + * + * await Effect.runPromise(program) // => [3, [1, 2, 3, 4]] * ``` * * @category combinators @@ -281,7 +287,7 @@ export const modify: { * * **Example** (Updating the stored chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -291,8 +297,10 @@ export const modify: { * yield* TxChunk.update(txChunk, (chunk) => Chunk.reverse(chunk)) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [3, 2, 1] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [3, 2, 1] * ``` * * @category combinators @@ -314,7 +322,7 @@ export const update: { * * **Example** (Reading the current chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -322,12 +330,10 @@ export const update: { * * // Read the current value within a transaction * const chunk = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(chunk)) // [1, 2, 3] - * - * // The value is tracked for conflict detection - * const size = Chunk.size(chunk) - * console.log(size) // 3 + * return [Chunk.toArray(chunk), Chunk.size(chunk)] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3], 3] * ``` * * @category combinators @@ -345,7 +351,7 @@ export const get = (self: TxChunk): Effect.Effect> => TxRef * * **Example** (Replacing the stored chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -356,8 +362,10 @@ export const get = (self: TxChunk): Effect.Effect> => TxRef * yield* TxChunk.set(txChunk, newChunk) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [10, 20, 30, 40] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [10, 20, 30, 40] * ``` * * @category combinators @@ -381,7 +389,7 @@ export const set: { * * **Example** (Appending an element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -391,8 +399,10 @@ export const set: { * yield* TxChunk.append(txChunk, 4) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4] * ``` * * @category combinators @@ -416,7 +426,7 @@ export const append: { * * **Example** (Prepending an element) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -426,8 +436,10 @@ export const append: { * yield* TxChunk.prepend(txChunk, 1) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4] * ``` * * @category combinators @@ -446,7 +458,7 @@ export const prepend: { * * **Example** (Getting the size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -454,13 +466,14 @@ export const prepend: { * * // Get the current size - automatically transactional * const currentSize = yield* TxChunk.size(txChunk) - * console.log(currentSize) // 5 * * // Size is tracked for conflict detection * yield* TxChunk.append(txChunk, 6) * const newSize = yield* TxChunk.size(txChunk) - * console.log(newSize) // 6 + * return [currentSize, newSize] * }) + * + * await Effect.runPromise(program) // => [5, 6] * ``` * * @category combinators @@ -474,7 +487,7 @@ export const size = (self: TxChunk): Effect.Effect => * * **Example** (Checking for an empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -485,12 +498,13 @@ export const size = (self: TxChunk): Effect.Effect => * const isEmpty1 = yield* TxChunk.isEmpty(emptyChunk) * const isEmpty2 = yield* TxChunk.isEmpty(nonEmptyChunk) * - * console.log(isEmpty1) // true - * console.log(isEmpty2) // false + * return [isEmpty1, isEmpty2] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isEmpty = (self: TxChunk): Effect.Effect => @@ -501,7 +515,7 @@ export const isEmpty = (self: TxChunk): Effect.Effect => * * **Example** (Checking for a non-empty chunk) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -512,12 +526,13 @@ export const isEmpty = (self: TxChunk): Effect.Effect => * const isNonEmpty1 = yield* TxChunk.isNonEmpty(emptyChunk) * const isNonEmpty2 = yield* TxChunk.isNonEmpty(nonEmptyChunk) * - * console.log(isNonEmpty1) // false - * console.log(isNonEmpty2) // true + * return [isNonEmpty1, isNonEmpty2] * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isNonEmpty = (self: TxChunk): Effect.Effect => @@ -533,7 +548,7 @@ export const isNonEmpty = (self: TxChunk): Effect.Effect => * * **Example** (Taking leading elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -543,8 +558,10 @@ export const isNonEmpty = (self: TxChunk): Effect.Effect => * yield* TxChunk.take(txChunk, 3) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category combinators @@ -568,7 +585,7 @@ export const take: { * * **Example** (Dropping leading elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -578,8 +595,10 @@ export const take: { * yield* TxChunk.drop(txChunk, 2) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [3, 4, 5] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [3, 4, 5] * ``` * * @category combinators @@ -603,7 +622,7 @@ export const drop: { * * **Example** (Taking a slice) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -613,8 +632,10 @@ export const drop: { * yield* TxChunk.slice(txChunk, 2, 5) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [3, 4, 5] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [3, 4, 5] * ``` * * @category combinators @@ -640,7 +661,7 @@ export const slice: { * * **Example** (Mapping elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -650,8 +671,10 @@ export const slice: { * yield* TxChunk.map(txChunk, (n) => n * 2) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [2, 4, 6, 8] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [2, 4, 6, 8] * ``` * * @category combinators @@ -675,7 +698,7 @@ export const map: { * * **Example** (Filtering elements) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -685,8 +708,10 @@ export const map: { * yield* TxChunk.filter(txChunk, (n) => n % 2 === 0) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [2, 4, 6] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [2, 4, 6] * ``` * * @category combinators @@ -713,7 +738,7 @@ export const filter: { * * **Example** (Appending another chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -724,8 +749,10 @@ export const filter: { * yield* TxChunk.appendAll(txChunk, otherChunk) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5, 6] * ``` * * @category combinators @@ -750,7 +777,7 @@ export const appendAll: { * * **Example** (Prepending another chunk) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -761,8 +788,10 @@ export const appendAll: { * yield* TxChunk.prependAll(txChunk, otherChunk) * * const result = yield* TxChunk.get(txChunk) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] + * return Chunk.toArray(result) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3, 4, 5, 6] * ``` * * @category combinators @@ -787,7 +816,7 @@ export const prependAll: { * * **Example** (Concatenating TxChunks) * - * ```ts + * ```ts import.meta.vitest * import { Chunk, Effect, TxChunk } from "effect" * * const program = Effect.gen(function*() { @@ -798,12 +827,13 @@ export const prependAll: { * yield* TxChunk.concat(txChunk1, txChunk2) * * const result = yield* TxChunk.get(txChunk1) - * console.log(Chunk.toReadonlyArray(result)) // [1, 2, 3, 4, 5, 6] * * // Original txChunk2 is unchanged * const original = yield* TxChunk.get(txChunk2) - * console.log(Chunk.toReadonlyArray(original)) // [4, 5, 6] + * return [Chunk.toArray(result), Chunk.toArray(original)] * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3, 4, 5, 6], [4, 5, 6]] * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/TxDeferred.ts b/.context/effect/packages/effect/src/TxDeferred.ts index 851f811b0..637e61494 100644 --- a/.context/effect/packages/effect/src/TxDeferred.ts +++ b/.context/effect/packages/effect/src/TxDeferred.ts @@ -37,7 +37,7 @@ const TypeId = "~effect/transactions/TxDeferred" * * **Example** (Completing a transactional deferred) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxDeferred } from "effect" * * const program = Effect.gen(function*() { @@ -45,16 +45,16 @@ const TypeId = "~effect/transactions/TxDeferred" * * // Complete the deferred * const first = yield* TxDeferred.succeed(deferred, 42) - * console.log(first) // true * * // Second write is a no-op * const second = yield* TxDeferred.succeed(deferred, 99) - * console.log(second) // false * * // Read the value * const value = yield* TxDeferred.await(deferred) - * console.log(value) // 42 + * return [first, second, value] * }) + * + * await Effect.runPromise(program) // => [true, false, 42] * ``` * * @category models @@ -95,14 +95,15 @@ const makeTxDeferred = (ref: TxRef.TxRef>>): TxDeferre * * **Example** (Creating a transactional deferred) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() - * const state = yield* TxDeferred.poll(deferred) - * console.log(Option.isNone(state)) // true + * return yield* TxDeferred.poll(deferred) * }) + * + * await Effect.runPromise(program) // => Option.none() * ``` * * @category constructors @@ -117,15 +118,16 @@ export const make = (): Effect.Effect> => * * **Example** (Awaiting a deferred value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() * yield* TxDeferred.succeed(deferred, 42) - * const value = yield* TxDeferred.await(deferred) - * console.log(value) // 42 + * return yield* TxDeferred.await(deferred) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category getters @@ -170,18 +172,19 @@ export { * * **Example** (Polling a deferred) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Result, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() * const before = yield* TxDeferred.poll(deferred) - * console.log(Option.isNone(before)) // true * * yield* TxDeferred.succeed(deferred, 42) * const after = yield* TxDeferred.poll(deferred) - * console.log(after) // Some(Success(42)) + * return [before, after] * }) + * + * await Effect.runPromise(program) // => [Option.none(), Option.some(Result.succeed(42))] * ``` * * @category getters @@ -199,16 +202,17 @@ export const poll = (self: TxDeferred): Effect.Effect() * const first = yield* TxDeferred.done(deferred, Result.succeed(42)) - * console.log(first) // true * const second = yield* TxDeferred.done(deferred, Result.succeed(99)) - * console.log(second) // false + * return [first, second] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category mutations @@ -238,16 +242,17 @@ export const done: { * * **Example** (Completing with a success value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() * const first = yield* TxDeferred.succeed(deferred, 42) - * console.log(first) // true * const second = yield* TxDeferred.succeed(deferred, 99) - * console.log(second) // false + * return [first, second] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category mutations @@ -271,16 +276,18 @@ export const succeed: { * * **Example** (Completing with a failure) * - * ```ts - * import { Effect, TxDeferred } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, Option, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() * const first = yield* TxDeferred.fail(deferred, "boom") - * console.log(first) // true * const second = yield* TxDeferred.fail(deferred, "boom2") - * console.log(second) // false + * const exit = yield* Effect.exit(TxDeferred.await(deferred)) + * return [first, second, exit, Exit.getCause(exit)] * }) + * + * await Effect.runPromise(program) // => [true, false, Exit.fail("boom"), Option.some(Cause.fail("boom"))] * ``` * * @category mutations @@ -303,14 +310,15 @@ export const fail: { * * **Example** (Checking transactional deferreds) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxDeferred } from "effect" * * const program = Effect.gen(function*() { * const deferred = yield* TxDeferred.make() - * console.log(TxDeferred.isTxDeferred(deferred)) // true - * console.log(TxDeferred.isTxDeferred("not a deferred")) // false + * return [TxDeferred.isTxDeferred(deferred), TxDeferred.isTxDeferred("not a deferred")] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category guards diff --git a/.context/effect/packages/effect/src/TxHashMap.ts b/.context/effect/packages/effect/src/TxHashMap.ts index c18ff1ef4..9671b41da 100644 --- a/.context/effect/packages/effect/src/TxHashMap.ts +++ b/.context/effect/packages/effect/src/TxHashMap.ts @@ -51,8 +51,8 @@ const TxHashMapProto = { * * **Example** (Using transactional hash maps) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a transactional hash map @@ -60,8 +60,7 @@ const TxHashMapProto = { * * // Single operations are automatically transactional * yield* TxHashMap.set(txMap, "user3", "Charlie") - * const user = yield* TxHashMap.get(txMap, "user1") - * console.log(user) // Option.some("Alice") + * yield* TxHashMap.get(txMap, "user1") // => Option.some("Alice") * * // Multi-step atomic operations * yield* Effect.tx( @@ -74,9 +73,10 @@ const TxHashMapProto = { * }) * ) * - * const size = yield* TxHashMap.size(txMap) - * console.log(size) // 2 + * return yield* TxHashMap.size(txMap) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category models @@ -93,8 +93,8 @@ export interface TxHashMap extends Inspectable, Pipeable { * * **Example** (Reusing extracted TxHashMap types) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a transactional inventory map @@ -117,7 +117,10 @@ export interface TxHashMap extends Inspectable, Pipeable { * ) * * yield* updateStock("laptop", 3) + * return yield* TxHashMap.get(inventory, "laptop") * }) + * + * await Effect.runPromise(program) // => Option.some({ stock: 3, price: 999 }) * ``` * * @since 4.0.0 @@ -128,8 +131,8 @@ export declare namespace TxHashMap { * * **Example** (Extracting key types) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a user map to extract key type from @@ -143,8 +146,10 @@ export declare namespace TxHashMap { * * // Use the extracted type in functions * const getUserById = (id: UserKey) => TxHashMap.get(userMap, id) - * const alice = yield* getUserById("alice") // Option<{ name: string, age: number }> + * return yield* getUserById("alice") * }) + * + * await Effect.runPromise(program) // => Option.some({ name: "Alice", age: 30 }) * ``` * * @category utility types @@ -157,8 +162,8 @@ export declare namespace TxHashMap { * * **Example** (Extracting value types) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a product catalog TxHashMap @@ -175,9 +180,10 @@ export declare namespace TxHashMap { * return `${product.category}: $${product.price}` * } * - * const laptop = yield* TxHashMap.get(catalog, "laptop") - * // laptop has type Option thanks to type extraction + * return Option.map(yield* TxHashMap.get(catalog, "laptop"), processProduct) * }) + * + * await Effect.runPromise(program) // => Option.some("electronics: $999") * ``` * * @category utility types @@ -190,7 +196,7 @@ export declare namespace TxHashMap { * * **Example** (Extracting entry types) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -210,10 +216,10 @@ export declare namespace TxHashMap { * } * * // Get all entries and process them - * const entries = yield* TxHashMap.entries(config) - * const configLines = entries.map(processEntry) - * console.log(configLines) // ["api_url=https://api.example.com", ...] + * return (yield* TxHashMap.entries(config)).map(processEntry).sort() * }) + * + * await Effect.runPromise(program) // => ["api_url=https://api.example.com", "retries=3", "timeout=5000"] * ``` * * @category utility types @@ -227,7 +233,7 @@ export declare namespace TxHashMap { * * **Example** (Creating an empty map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -235,17 +241,15 @@ export declare namespace TxHashMap { * const emptyMap = yield* TxHashMap.empty() * * // Verify it's empty - * const isEmpty = yield* TxHashMap.isEmpty(emptyMap) - * console.log(isEmpty) // true - * - * const size = yield* TxHashMap.size(emptyMap) - * console.log(size) // 0 + * yield* TxHashMap.isEmpty(emptyMap) // => true + * yield* TxHashMap.size(emptyMap) // => 0 * * // Start adding elements * yield* TxHashMap.set(emptyMap, "first", 1) - * const newSize = yield* TxHashMap.size(emptyMap) - * console.log(newSize) // 1 + * return yield* TxHashMap.size(emptyMap) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category constructors @@ -262,8 +266,8 @@ export const empty = (): Effect.Effect> => * * **Example** (Creating a map from entries) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a user directory @@ -274,16 +278,14 @@ export const empty = (): Effect.Effect> => * ) * * // Check the initial size - * const size = yield* TxHashMap.size(userMap) - * console.log(size) // 3 + * yield* TxHashMap.size(userMap) // => 3 * * // Access users - * const alice = yield* TxHashMap.get(userMap, "alice") - * console.log(alice) // Option.some({ name: "Alice Smith", role: "admin" }) - * - * const nonExistent = yield* TxHashMap.get(userMap, "david") - * console.log(nonExistent) // Option.none() + * yield* TxHashMap.get(userMap, "alice") // => Option.some({ name: "Alice Smith", role: "admin" }) + * return yield* TxHashMap.get(userMap, "david") * }) + * + * await Effect.runPromise(program) // => Option.none() * ``` * * @category constructors @@ -303,8 +305,8 @@ export const make = ( * * **Example** (Creating a map from an iterable) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create from various iterable sources @@ -318,16 +320,15 @@ export const make = ( * const configMap = yield* TxHashMap.fromIterable(configEntries) * * // Verify the configuration was loaded - * const size = yield* TxHashMap.size(configMap) - * console.log(size) // 4 - * - * const dbHost = yield* TxHashMap.get(configMap, "database.host") - * console.log(dbHost) // Option.some("localhost") + * yield* TxHashMap.size(configMap) // => 4 + * yield* TxHashMap.get(configMap, "database.host") // => Option.some("localhost") * * // Can also create from Map, Set of tuples, etc. * const jsMap = new Map([["key1", "value1"], ["key2", "value2"]]) - * const txMapFromJs = yield* TxHashMap.fromIterable(jsMap) + * return yield* TxHashMap.fromIterable(jsMap) * }) + * + * await Effect.runPromise(program) * ``` * * @category constructors @@ -347,7 +348,7 @@ export const fromIterable = ( * * **Example** (Looking up values safely) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -357,18 +358,14 @@ export const fromIterable = ( * ) * * // Safe lookup - returns Option - * const alice = yield* TxHashMap.get(userMap, "alice") - * console.log(alice) // Option.some({ name: "Alice", role: "admin" }) - * - * const nonExistent = yield* TxHashMap.get(userMap, "charlie") - * console.log(nonExistent) // Option.none() + * yield* TxHashMap.get(userMap, "alice") // => Option.some({ name: "Alice", role: "admin" }) + * yield* TxHashMap.get(userMap, "charlie") // => Option.none() * * // Use with pipe syntax for type-safe access - * const bobRole = yield* TxHashMap.get(userMap, "bob") - * if (bobRole._tag === "Some") { - * console.log(bobRole.value.role) // "user" - * } + * return yield* TxHashMap.get(userMap, "bob") * }) + * + * await Effect.runPromise(program) // => Option.some({ name: "Bob", role: "user" }) * ``` * * @category combinators @@ -396,8 +393,8 @@ export const get: { * * **Example** (Setting values) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const inventory = yield* TxHashMap.make( @@ -407,17 +404,18 @@ export const get: { * * // Update existing item * yield* TxHashMap.set(inventory, "laptop", 3) - * const laptopStock = yield* TxHashMap.get(inventory, "laptop") - * console.log(laptopStock) // Option.some(3) + * yield* TxHashMap.get(inventory, "laptop") // => Option.some(3) * * // Add new item * yield* TxHashMap.set(inventory, "keyboard", 15) - * const keyboardStock = yield* TxHashMap.get(inventory, "keyboard") - * console.log(keyboardStock) // Option.some(15) + * yield* TxHashMap.get(inventory, "keyboard") // => Option.some(15) * * // Use with pipe syntax * yield* TxHashMap.set("tablet", 8)(inventory) + * return yield* TxHashMap.get(inventory, "tablet") * }) + * + * await Effect.runPromise(program) // => Option.some(8) * ``` * * @category combinators @@ -437,7 +435,7 @@ export const set: { * * **Example** (Checking for keys) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -448,19 +446,17 @@ export const set: { * ) * * // Check if users exist - * const hasAlice = yield* TxHashMap.has(permissions, "alice") - * console.log(hasAlice) // true - * - * const hasDavid = yield* TxHashMap.has(permissions, "david") - * console.log(hasDavid) // false + * yield* TxHashMap.has(permissions, "alice") // => true + * yield* TxHashMap.has(permissions, "david") // => false * * // Use direct method call for type-safe access - * const hasBob = yield* TxHashMap.has(permissions, "bob") - * console.log(hasBob) // true + * return yield* TxHashMap.has(permissions, "bob") * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category combinators + * @category predicates * @since 2.0.0 */ export const has: { @@ -485,7 +481,7 @@ export const has: { * * **Example** (Removing keys) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -496,20 +492,17 @@ export const has: { * ) * * // Remove expired user - * const removed = yield* TxHashMap.remove(cache, "user:3") - * console.log(removed) // true (key existed and was removed) + * yield* TxHashMap.remove(cache, "user:3") // => true * * // Try to remove non-existent key - * const notRemoved = yield* TxHashMap.remove(cache, "user:999") - * console.log(notRemoved) // false (key didn't exist) + * yield* TxHashMap.remove(cache, "user:999") // => false * * // Verify removal - * const hasUser3 = yield* TxHashMap.has(cache, "user:3") - * console.log(hasUser3) // false - * - * const size = yield* TxHashMap.size(cache) - * console.log(size) // 2 + * yield* TxHashMap.has(cache, "user:3") // => false + * return yield* TxHashMap.size(cache) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category combinators @@ -541,7 +534,7 @@ export const remove: { * * **Example** (Clearing all entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -552,19 +545,17 @@ export const remove: { * ) * * // Check initial state - * const initialSize = yield* TxHashMap.size(sessionMap) - * console.log(initialSize) // 3 + * yield* TxHashMap.size(sessionMap) // => 3 * * // Clear all sessions (e.g., during maintenance) * yield* TxHashMap.clear(sessionMap) * * // Verify cleared - * const finalSize = yield* TxHashMap.size(sessionMap) - * console.log(finalSize) // 0 - * - * const isEmpty = yield* TxHashMap.isEmpty(sessionMap) - * console.log(isEmpty) // true + * yield* TxHashMap.size(sessionMap) // => 0 + * return yield* TxHashMap.isEmpty(sessionMap) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category combinators @@ -577,7 +568,7 @@ export const clear = (self: TxHashMap): Effect.Effect => TxRef * * **Example** (Counting entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -587,19 +578,18 @@ export const clear = (self: TxHashMap): Effect.Effect => TxRef * ["users", 50] * ) * - * const count = yield* TxHashMap.size(metrics) - * console.log(count) // 3 + * yield* TxHashMap.size(metrics) // => 3 * * // Add more metrics * yield* TxHashMap.set(metrics, "response_time", 250) - * const newCount = yield* TxHashMap.size(metrics) - * console.log(newCount) // 4 + * yield* TxHashMap.size(metrics) // => 4 * * // Remove a metric * yield* TxHashMap.remove(metrics, "errors") - * const finalCount = yield* TxHashMap.size(metrics) - * console.log(finalCount) // 3 + * return yield* TxHashMap.size(metrics) * }) + * + * await Effect.runPromise(program) // => 3 * ``` * * @category combinators @@ -616,28 +606,27 @@ export const size = (self: TxHashMap): Effect.Effect => * * **Example** (Checking for an empty map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Start with empty map * const cache = yield* TxHashMap.empty() - * const empty = yield* TxHashMap.isEmpty(cache) - * console.log(empty) // true + * yield* TxHashMap.isEmpty(cache) // => true * * // Add an item * yield* TxHashMap.set(cache, "key1", "value1") - * const stillEmpty = yield* TxHashMap.isEmpty(cache) - * console.log(stillEmpty) // false + * yield* TxHashMap.isEmpty(cache) // => false * * // Clear and check again * yield* TxHashMap.clear(cache) - * const emptyAgain = yield* TxHashMap.isEmpty(cache) - * console.log(emptyAgain) // true + * return yield* TxHashMap.isEmpty(cache) * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category combinators + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: TxHashMap): Effect.Effect => @@ -651,23 +640,23 @@ export const isEmpty = (self: TxHashMap): Effect.Effect => * * **Example** (Checking for a non-empty map) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const inventory = yield* TxHashMap.make(["laptop", 5]) * - * const hasItems = yield* TxHashMap.isNonEmpty(inventory) - * console.log(hasItems) // true + * yield* TxHashMap.isNonEmpty(inventory) // => true * * // Clear inventory * yield* TxHashMap.clear(inventory) - * const stillHasItems = yield* TxHashMap.isNonEmpty(inventory) - * console.log(stillHasItems) // false + * return yield* TxHashMap.isNonEmpty(inventory) * }) + * + * await Effect.runPromise(program) // => false * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isNonEmpty = (self: TxHashMap): Effect.Effect => @@ -683,8 +672,8 @@ export const isNonEmpty = (self: TxHashMap): Effect.Effect * * **Example** (Updating existing values) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const counters = yield* TxHashMap.make( @@ -698,10 +687,9 @@ export const isNonEmpty = (self: TxHashMap): Effect.Effect * "downloads", * (count) => count + 1 * ) - * console.log(oldDownloads) // Option.some(100) + * oldDownloads // => Option.some(100) * - * const newDownloads = yield* TxHashMap.get(counters, "downloads") - * console.log(newDownloads) // Option.some(101) + * yield* TxHashMap.get(counters, "downloads") // => Option.some(101) * * // Try to modify non-existent key * const nonExistent = yield* TxHashMap.modify( @@ -709,11 +697,14 @@ export const isNonEmpty = (self: TxHashMap): Effect.Effect * "clicks", * (count) => count + 1 * ) - * console.log(nonExistent) // Option.none() + * nonExistent // => Option.none() * * // Update views counter with direct method call * yield* TxHashMap.modify(counters, "views", (views) => views * 2) + * return yield* TxHashMap.get(counters, "views") * }) + * + * await Effect.runPromise(program) // => Option.some(500) * ``` * * @category combinators @@ -755,42 +746,34 @@ export const modify: { * * **Example** (Updating values with Option) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const storage = yield* TxHashMap.make([ * "file1.txt", * "content1" * ], ["access_count", 0]) + * const increment = Option.map((value: string | number) => typeof value === "number" ? value + 1 : value) * * // Increment existing counter - * yield* TxHashMap.modifyAt(storage, "access_count", (current) => - * current._tag === "Some" && typeof current.value === "number" - * ? { ...current, value: current.value + 1 } - * : current - * ) - * const count1 = yield* TxHashMap.get(storage, "access_count") - * console.log(count1) // Option.some(1) + * yield* TxHashMap.modifyAt(storage, "access_count", increment) + * yield* TxHashMap.get(storage, "access_count") // => Option.some(1) * * // Increment existing counter again - * yield* TxHashMap.modifyAt(storage, "access_count", (current) => - * current._tag === "Some" && typeof current.value === "number" - * ? { ...current, value: current.value + 1 } - * : current - * ) - * const count2 = yield* TxHashMap.get(storage, "access_count") - * console.log(count2) // Option.some(2) + * yield* TxHashMap.modifyAt(storage, "access_count", increment) + * yield* TxHashMap.get(storage, "access_count") // => Option.some(2) * * // Update an existing string entry - * yield* TxHashMap.modifyAt(storage, "file1.txt", (current) => - * current._tag === "Some" && typeof current.value === "string" - * ? { ...current, value: `${current.value}.bak` } - * : current + * yield* TxHashMap.modifyAt( + * storage, + * "file1.txt", + * Option.map((value) => typeof value === "string" ? `${value}.bak` : value) * ) - * const backup = yield* TxHashMap.get(storage, "file1.txt") - * console.log(backup) // Option.some("content1.bak") + * return yield* TxHashMap.get(storage, "file1.txt") * }) + * + * await Effect.runPromise(program) // => Option.some("content1.bak") * ``` * * @category combinators @@ -831,7 +814,7 @@ export const modifyAt: { * * **Example** (Reading keys) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -841,17 +824,21 @@ export const modifyAt: { * ["charlie", "moderator"] * ) * - * const usernames = yield* TxHashMap.keys(userRoles) - * console.log(usernames.sort()) // ["alice", "bob", "charlie"] + * const usernames = (yield* TxHashMap.keys(userRoles)).sort() + * usernames // => ["alice", "bob", "charlie"] * * // Useful for iteration + * const assignments: Array = [] * for (const username of usernames) { * const role = yield* TxHashMap.get(userRoles, username) * if (role._tag === "Some") { - * console.log(`${username}: ${role.value}`) + * assignments.push(`${username}: ${role.value}`) * } * } + * return assignments * }) + * + * await Effect.runPromise(program) // => ["alice: admin", "bob: user", "charlie: moderator"] * ``` * * @category combinators @@ -868,7 +855,7 @@ export const keys = (self: TxHashMap): Effect.Effect> => * * **Example** (Reading values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -878,18 +865,19 @@ export const keys = (self: TxHashMap): Effect.Effect> => * ["charlie", 92] * ) * - * const allScores = yield* TxHashMap.values(scores) - * console.log(allScores.sort((a, b) => a - b)) // [87, 92, 95] + * const allScores = (yield* TxHashMap.values(scores)).sort((a, b) => a - b) + * allScores // => [87, 92, 95] * * // Calculate average * const average = allScores.reduce((sum, score) => sum + score, 0) / * allScores.length - * console.log(average.toFixed(2)) // "91.33" + * average.toFixed(2) // => "91.33" * * // Find maximum - * const maxScore = Math.max(...allScores) - * console.log(maxScore) // 95 + * return Math.max(...allScores) * }) + * + * await Effect.runPromise(program) // => 95 * ``` * * @category combinators @@ -906,7 +894,7 @@ export const values = (self: TxHashMap): Effect.Effect> => * * **Example** (Reading entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -916,19 +904,10 @@ export const values = (self: TxHashMap): Effect.Effect> => * ["ssl", "false"] * ) * - * const allEntries = yield* TxHashMap.entries(config) - * const sortedEntries = allEntries.toSorted(([left], [right]) => left.localeCompare(right)) - * console.log(sortedEntries) - * // [["host", "localhost"], ["port", "3000"], ["ssl", "false"]] - * - * // Process configuration entries - * for (const [key, value] of sortedEntries) { - * console.log(`${key}=${value}`) - * } - * // host=localhost - * // port=3000 - * // ssl=false + * return (yield* TxHashMap.entries(config)).toSorted(([left], [right]) => left.localeCompare(right)) * }) + * + * await Effect.runPromise(program) // => [["host", "localhost"], ["port", "3000"], ["ssl", "false"]] * ``` * * @category combinators @@ -947,8 +926,8 @@ export const entries = ( * * **Example** (Taking immutable snapshots) * - * ```ts - * import { Effect, HashMap, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, HashMap, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const liveData = yield* TxHashMap.make( @@ -965,13 +944,14 @@ export const entries = ( * yield* TxHashMap.set(liveData, "wind_speed", 5.3) * * // Snapshot remains unchanged - * console.log(HashMap.size(snapshot)) // 3 - * console.log(HashMap.get(snapshot, "temperature")) // Option.some(22.5) + * HashMap.size(snapshot) // => 3 + * HashMap.get(snapshot, "temperature") // => Option.some(22.5) * * // Can use regular HashMap operations on snapshot - * const tempReading = HashMap.get(snapshot, "temperature") - * const humidityReading = HashMap.get(snapshot, "humidity") + * return HashMap.get(snapshot, "humidity") * }) + * + * await Effect.runPromise(program) // => Option.some(45.2) * ``` * * @category combinators @@ -992,8 +972,8 @@ export const snapshot = ( * * **Example** (Merging HashMaps) * - * ```ts - * import { Effect, HashMap, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, HashMap, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create initial user preferences @@ -1014,18 +994,13 @@ export const snapshot = ( * yield* TxHashMap.union(userPrefs, newSettings) * * // Check the merged result - * const theme = yield* TxHashMap.get(userPrefs, "theme") - * console.log(theme) // Option.some("dark") - overridden - * - * const language = yield* TxHashMap.get(userPrefs, "language") - * console.log(language) // Option.some("en") - preserved - * - * const timezone = yield* TxHashMap.get(userPrefs, "timezone") - * console.log(timezone) // Option.some("UTC") - newly added - * - * const size = yield* TxHashMap.size(userPrefs) - * console.log(size) // 5 total settings + * yield* TxHashMap.get(userPrefs, "theme") // => Option.some("dark") + * yield* TxHashMap.get(userPrefs, "language") // => Option.some("en") + * yield* TxHashMap.get(userPrefs, "timezone") // => Option.some("UTC") + * return yield* TxHashMap.size(userPrefs) * }) + * + * await Effect.runPromise(program) // => 5 * ``` * * @category combinators @@ -1057,8 +1032,8 @@ export const union: { * * **Example** (Removing multiple keys) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a cache with temporary data @@ -1070,23 +1045,24 @@ export const union: { * ["temp_data_2", { value: "also_temporary" }] * ) * - * console.log(yield* TxHashMap.size(cache)) // 5 + * yield* TxHashMap.size(cache) // => 5 * * // Remove expired sessions and temporary data * const keysToRemove = ["session_1", "session_2", "temp_data_1", "temp_data_2"] * yield* TxHashMap.removeMany(cache, keysToRemove) * - * console.log(yield* TxHashMap.size(cache)) // 1 + * yield* TxHashMap.size(cache) // => 1 * * // Verify only the valid session remains - * const remainingSession = yield* TxHashMap.get(cache, "session_3") - * console.log(remainingSession) // Option.some({ user: "charlie", expires: "2024-12-31" }) + * yield* TxHashMap.get(cache, "session_3") // => Option.some({ user: "charlie", expires: "2024-12-31" }) * * // Can also remove from Set, Array, or any iterable * const moreKeysToRemove = new Set(["session_3"]) * yield* TxHashMap.removeMany(cache, moreKeysToRemove) - * console.log(yield* TxHashMap.isEmpty(cache)) // true + * return yield* TxHashMap.isEmpty(cache) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category combinators @@ -1111,8 +1087,8 @@ export const removeMany: { * * **Example** (Setting multiple entries) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create an empty product catalog @@ -1133,7 +1109,7 @@ export const removeMany: { * * yield* TxHashMap.setMany(catalog, initialProducts) * - * console.log(yield* TxHashMap.size(catalog)) // 4 + * yield* TxHashMap.size(catalog) // => 4 * * // Update prices with a new batch * const priceUpdates: Array< @@ -1146,16 +1122,18 @@ export const removeMany: { * * yield* TxHashMap.setMany(catalog, priceUpdates) * - * console.log(yield* TxHashMap.size(catalog)) // 5 (4 original + 1 new) + * yield* TxHashMap.size(catalog) // => 5 * * // Verify the updates - * const laptop = yield* TxHashMap.get(catalog, "laptop") - * console.log(laptop) // Option.some({ price: 899, stock: 5 }) + * yield* TxHashMap.get(catalog, "laptop") // => Option.some({ price: 899, stock: 5 }) * * // Can also use Map, Set of tuples, or any iterable of entries * const jsMap = new Map([["tablet", { price: 399, stock: 3 }]]) * yield* TxHashMap.setMany(catalog, jsMap) + * return yield* TxHashMap.get(catalog, "tablet") * }) + * + * await Effect.runPromise(program) // => Option.some({ price: 399, stock: 3 }) * ``` * * @category combinators @@ -1182,16 +1160,16 @@ export const setMany: { * * **Example** (Checking TxHashMap values) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * const txMap = yield* TxHashMap.make(["key", "value"]) * - * console.log(TxHashMap.isTxHashMap(txMap)) // true - * console.log(TxHashMap.isTxHashMap({})) // false - * console.log(TxHashMap.isTxHashMap(null)) // false - * console.log(TxHashMap.isTxHashMap("not a map")) // false + * TxHashMap.isTxHashMap(txMap) // => true + * TxHashMap.isTxHashMap({}) // => false + * TxHashMap.isTxHashMap(null) // => false + * TxHashMap.isTxHashMap("not a map") // => false * * // Useful for type guards in runtime checks * const validateInput = (value: unknown) => { @@ -1201,7 +1179,12 @@ export const setMany: { * } * return Effect.fail("Invalid input") * } + * + * yield* Effect.exit(validateInput(null)) // => Exit.fail("Invalid input") + * return yield* Effect.exit(validateInput(txMap)) * }) + * + * await Effect.runPromise(program) // => Exit.succeed("Valid TxHashMap") * ``` * * @category guards @@ -1222,8 +1205,8 @@ export const isTxHashMap = (value: unknown): value is TxHashMap => { * * **Example** (Looking up values with precomputed hashes) * - * ```ts - * import { Effect, Hash, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Hash, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a cache with user sessions @@ -1238,16 +1221,17 @@ export const isTxHashMap = (value: unknown): value is TxHashMap => { * * // Use hash-optimized lookup for performance in hot paths * const session = yield* TxHashMap.getHash(cache, sessionId, precomputedHash) - * console.log(session) // Option.some({ userId: "user1", lastActive: ... }) + * session // => Option.some({ userId: "user1", lastActive: 1_700_000_000_000 }) * * // This avoids recomputing the hash when you already have it - * const invalidSession = yield* TxHashMap.getHash( + * return yield* TxHashMap.getHash( * cache, * "invalid", * Hash.string("invalid") * ) - * console.log(invalidSession) // Option.none() * }) + * + * await Effect.runPromise(program) // => Option.none() * ``` * * @category combinators @@ -1283,7 +1267,7 @@ export const getHash: { * * **Example** (Checking keys with precomputed hashes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Hash, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1298,29 +1282,30 @@ export const getHash: { * const roleHash = Hash.string(role) * * // Use hash-optimized existence check - * const hasAdminRole = yield* TxHashMap.hasHash(permissions, role, roleHash) - * console.log(hasAdminRole) // true + * yield* TxHashMap.hasHash(permissions, role, roleHash) // => true * * // Check non-existent role - * const hasGuestRole = yield* TxHashMap.hasHash( + * yield* TxHashMap.hasHash( * permissions, * "guest", * Hash.string("guest") - * ) - * console.log(hasGuestRole) // false + * ) // => false * * // Useful in hot paths where hash is computed once and reused * const roles = ["admin", "user", "moderator"] * const roleHashes = roles.map((role) => [role, Hash.string(role)] as const) - * + * const results: Array = [] * for (const [role, hash] of roleHashes) { * const exists = yield* TxHashMap.hasHash(permissions, role, hash) - * console.log(`Role ${role}: ${exists}`) + * results.push(`Role ${role}: ${exists}`) * } + * return results * }) + * + * await Effect.runPromise(program) // => ["Role admin: true", "Role user: true", "Role moderator: false"] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const hasHash: { @@ -1348,8 +1333,8 @@ export const hasHash: { * * **Example** (Mapping values) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a user profile map @@ -1366,21 +1351,20 @@ export const hasHash: { * ) * * // Check the transformed values - * const aliceGreeting = yield* TxHashMap.get(greetings, "alice") - * console.log(aliceGreeting) // Option.some("Hello, Alice! (User: alice)") + * yield* TxHashMap.get(greetings, "alice") // => Option.some("Hello, Alice! (User: alice)") * * // Data-last usage with pipe * const ages = yield* profiles.pipe( * TxHashMap.map((profile) => profile.age) * ) * - * const aliceAge = yield* TxHashMap.get(ages, "alice") - * console.log(aliceAge) // Option.some(30) + * yield* TxHashMap.get(ages, "alice") // => Option.some(30) * * // Original map is unchanged - * const originalAlice = yield* TxHashMap.get(profiles, "alice") - * console.log(originalAlice) // Option.some({ name: "Alice", age: 30, active: true }) + * return yield* TxHashMap.get(profiles, "alice") * }) + * + * await Effect.runPromise(program) // => Option.some({ name: "Alice", age: 30, active: true }) * ``` * * @category combinators @@ -1417,7 +1401,7 @@ export const map: { * * **Example** (Filtering entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1435,24 +1419,24 @@ export const map: { * (product) => product.category === "electronics" && product.stock > 0 * ) * - * const size = yield* TxHashMap.size(electronicsInStock) - * console.log(size) // 2 (laptop and mouse) + * yield* TxHashMap.size(electronicsInStock) // => 2 * * // Data-last usage with pipe * const expensiveItems = yield* inventory.pipe( * TxHashMap.filter((product) => product.price > 500) * ) * - * const expensiveSize = yield* TxHashMap.size(expensiveItems) - * console.log(expensiveSize) // 2 (laptop and phone) + * yield* TxHashMap.size(expensiveItems) // => 2 * * // Type guard usage - * const highValueItems = yield* TxHashMap.filter( + * return yield* TxHashMap.filter( * inventory, * (product): product is typeof product & { price: number } => * product.price > 50 * ) * }) + * + * await Effect.runPromise(program) * ``` * * @category combinators @@ -1492,7 +1476,7 @@ export const filter: { * * **Example** (Reducing entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1508,12 +1492,9 @@ export const filter: { * const totalSales = yield* TxHashMap.reduce( * sales, * 0, - * (total, amount, quarter) => { - * console.log(`Adding ${quarter}: ${amount}`) - * return total + amount - * } + * (total, amount) => total + amount * ) - * console.log(`Total sales: ${totalSales}`) // 80000 + * totalSales // => 80000 * * // Data-last usage with pipe * const quarterlyReport = yield* sales.pipe( @@ -1526,16 +1507,10 @@ export const filter: { * }) * ) * ) - * console.log(quarterlyReport) // { quarters: 4, total: 80000, max: 25000 } - * - * // Build a summary string - * const summary = yield* TxHashMap.reduce( - * sales, - * "", - * (acc, amount, quarter) => acc + `${quarter}: $${amount.toLocaleString()}\n` - * ) - * console.log(summary) + * return quarterlyReport * }) + * + * await Effect.runPromise(program) // => { quarters: 4, total: 80000, max: 25000 } * ``` * * @category combinators @@ -1571,7 +1546,7 @@ export const reduce: { * * **Example** (Filtering and mapping entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Result, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1599,10 +1574,8 @@ export const reduce: { * ) * * const aliceData = yield* TxHashMap.get(activeAdminAges, "alice") - * console.log(aliceData) // Option.some({ username: "alice", age: 30, seniority: "senior" }) - * - * const charlieData = yield* TxHashMap.get(activeAdminAges, "charlie") - * console.log(charlieData) // Option.none() (not active) + * aliceData // => Option.some({ username: "alice", age: 30, seniority: "senior" }) + * yield* TxHashMap.get(activeAdminAges, "charlie") // => Option.none() * * // Data-last usage with pipe * const validAges = yield* userData.pipe( @@ -1612,9 +1585,10 @@ export const reduce: { * }) * ) * - * const size = yield* TxHashMap.size(validAges) - * console.log(size) // 3 (alice, charlie, diana have valid ages) + * return yield* TxHashMap.size(validAges) * }) + * + * await Effect.runPromise(program) // => 3 * ``` * * @category combinators @@ -1646,7 +1620,7 @@ export const filterMap: { * * **Example** (Checking entries with a predicate) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1659,28 +1633,27 @@ export const filterMap: { * ) * * // Check if any users are online - * const hasOnlineUsers = yield* TxHashMap.hasBy( + * yield* TxHashMap.hasBy( * userStatuses, * (user) => user.status === "online" - * ) - * console.log(hasOnlineUsers) // true + * ) // => true * * // Check if any users have specific username pattern - * const hasAdminUser = yield* TxHashMap.hasBy( + * yield* TxHashMap.hasBy( * userStatuses, * (user, username) => username.startsWith("admin") - * ) - * console.log(hasAdminUser) // false + * ) // => false * * // Data-last usage with pipe - * const hasRecentActivity = yield* userStatuses.pipe( + * return yield* userStatuses.pipe( * TxHashMap.hasBy((user) => currentTime - user.lastSeen < 1_800_000) // 30 minutes * ) - * console.log(hasRecentActivity) // true * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const hasBy: { @@ -1705,8 +1678,8 @@ export const hasBy: { * * **Example** (Finding the first matching entry) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a task priority map @@ -1722,21 +1695,15 @@ export const hasBy: { * (task) => task.priority >= 2 && !task.completed * ) * - * if (highPriorityTask._tag === "Some") { - * const [taskId, task] = highPriorityTask.value - * console.log(`Found task: ${taskId}, priority: ${task.priority}`) - * // "Found task: task3, priority: 2" - * } + * highPriorityTask // => Option.some(["task3", { priority: 2, assignee: "alice", completed: false }]) * * // Find first task assigned to specific user - * const aliceTask = yield* tasks.pipe( + * return yield* tasks.pipe( * TxHashMap.findFirst((task) => task.assignee === "alice") * ) - * - * if (aliceTask._tag === "Some") { - * console.log(`Alice's task: ${aliceTask.value[0]}`) - * } * }) + * + * await Effect.runPromise(program) // => Option.some(["task1", { priority: 1, assignee: "alice", completed: false }]) * ``` * * @category combinators @@ -1764,7 +1731,7 @@ export const findFirst: { * * **Example** (Checking whether some entries match) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1776,28 +1743,27 @@ export const findFirst: { * ) * * // Check if any products are expensive - * const hasExpensiveProducts = yield* TxHashMap.some( + * yield* TxHashMap.some( * inventory, * (product) => product.price > 500 - * ) - * console.log(hasExpensiveProducts) // true + * ) // => true * * // Check if any products are out of stock - * const hasOutOfStock = yield* TxHashMap.some( + * yield* TxHashMap.some( * inventory, * (product) => product.stock === 0 - * ) - * console.log(hasOutOfStock) // true + * ) // => true * * // Data-last usage with pipe - * const hasAffordableItems = yield* inventory.pipe( + * return yield* inventory.pipe( * TxHashMap.some((product) => product.price < 50) * ) - * console.log(hasAffordableItems) // true (mouse is $29) * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const some: { @@ -1821,7 +1787,7 @@ export const some: { * * **Example** (Checking whether every entry matches) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -1833,28 +1799,27 @@ export const some: { * ) * * // Check if all users can read - * const allCanRead = yield* TxHashMap.every( + * yield* TxHashMap.every( * permissions, * (perms) => perms.canRead - * ) - * console.log(allCanRead) // true + * ) // => true * * // Check if all users can write - * const allCanWrite = yield* TxHashMap.every( + * yield* TxHashMap.every( * permissions, * (perms) => perms.canWrite - * ) - * console.log(allCanWrite) // false + * ) // => false * * // Data-last usage with pipe - * const allHaveBasicAccess = yield* permissions.pipe( + * return yield* permissions.pipe( * TxHashMap.every((perms, username) => perms.canRead && username.length > 2) * ) - * console.log(allHaveBasicAccess) // true * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const every: { @@ -1879,8 +1844,8 @@ export const every: { * * **Example** (Running effects for each entry) * - * ```ts - * import { Console, Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a log processing map @@ -1890,26 +1855,17 @@ export const every: { * ["debug.log", { size: 512, level: "debug" }] * ) * - * // Process each log file with side effects + * const messages: Array = [] * yield* TxHashMap.forEach(logs, (logInfo, filename) => - * Effect.gen(function*() { - * yield* Console.log( - * `Processing ${filename}: ${logInfo.size} bytes, level: ${logInfo.level}` - * ) - * if (logInfo.level === "error") { - * yield* Console.log(`⚠️ Error log detected: ${filename}`) - * } + * Effect.sync(() => { + * messages.push(`${filename}: ${logInfo.size} bytes (${logInfo.level})`) * })) * - * // Data-last usage with pipe - * yield* logs.pipe( - * TxHashMap.forEach((logInfo) => - * logInfo.size > 1000 - * ? Console.log(`Large log file: ${logInfo.size} bytes`) - * : Effect.void - * ) - * ) + * return messages.sort() * }) + * + * const result = await Effect.runPromise(program) + * result // => ["access.log: 2048 bytes (info)", "debug.log: 512 bytes (debug)", "error.log: 1024 bytes (error)"] * ``` * * @category combinators @@ -1946,8 +1902,8 @@ export const forEach: { * * **Example** (Flat mapping entries) * - * ```ts - * import { Effect, TxHashMap } from "effect" + * ```ts import.meta.vitest + * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { * // Create a department-employee map @@ -1975,15 +1931,12 @@ export const forEach: { * ) * * // Check the flattened result - * const alice = yield* TxHashMap.get(employeeDetails, "alice") - * console.log(alice) // Option.some({ department: "engineering", role: "lead" }) - * - * const charlie = yield* TxHashMap.get(employeeDetails, "charlie") - * console.log(charlie) // Option.some({ department: "marketing", role: "lead" }) - * - * const size = yield* TxHashMap.size(employeeDetails) - * console.log(size) // 4 (all employees) + * yield* TxHashMap.get(employeeDetails, "alice") // => Option.some({ department: "engineering", role: "lead" }) + * yield* TxHashMap.get(employeeDetails, "charlie") // => Option.some({ department: "marketing", role: "lead" }) + * return yield* TxHashMap.size(employeeDetails) * }) + * + * await Effect.runPromise(program) // => 4 * ``` * * @category combinators @@ -2028,7 +1981,7 @@ export const flatMap: { * * **Example** (Compacting optional values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -2047,21 +2000,17 @@ export const flatMap: { * // Remove all None values and unwrap Some values * const validUsers = yield* TxHashMap.compact(userData) * - * const size = yield* TxHashMap.size(validUsers) - * console.log(size) // 3 (alice, charlie, eve) + * yield* TxHashMap.size(validUsers) // => 3 * - * const alice = yield* TxHashMap.get(validUsers, "alice") - * console.log(alice) // Option.some({ age: 30, email: "alice@example.com" }) - * - * const bob = yield* TxHashMap.get(validUsers, "bob") - * console.log(bob) // Option.none() (removed from map) + * yield* TxHashMap.get(validUsers, "alice") // => Option.some({ age: 30, email: "alice@example.com" }) + * yield* TxHashMap.get(validUsers, "bob") // => Option.none() * * // Useful for cleaning up optional data processing results * const userAges = yield* TxHashMap.map(validUsers, (user) => user.age) - * const ageEntries = yield* TxHashMap.entries(userAges) - * const sortedAgeEntries = ageEntries.toSorted(([left], [right]) => left.localeCompare(right)) - * console.log(sortedAgeEntries) // [["alice", 30], ["charlie", 25], ["eve", 28]] + * return (yield* TxHashMap.entries(userAges)).toSorted(([left], [right]) => left.localeCompare(right)) * }) + * + * await Effect.runPromise(program) // => [["alice", 30], ["charlie", 25], ["eve", 28]] * ``` * * @category combinators @@ -2082,7 +2031,7 @@ export const compact = ( * * **Example** (Converting to entries) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -2093,24 +2042,18 @@ export const compact = ( * ) * * // Get all entries as an array - * const allEntries = yield* TxHashMap.toEntries(settings) - * const sortedEntries = allEntries.toSorted(([left], [right]) => left.localeCompare(right)) - * console.log(sortedEntries) - * // [["language", "en-US"], ["theme", "dark"], ["timezone", "UTC"]] - * - * // Process entries - * for (const [setting, value] of sortedEntries) { - * console.log(`${setting}: ${value}`) - * } + * const sortedEntries = (yield* TxHashMap.toEntries(settings)) + * .toSorted(([left], [right]) => left.localeCompare(right)) + * sortedEntries // => [["language", "en-US"], ["theme", "dark"], ["timezone", "UTC"]] * - * // Convert to object for JSON serialization - * const settingsObj = Object.fromEntries(sortedEntries) - * console.log(JSON.stringify(settingsObj)) - * // {"language":"en-US","theme":"dark","timezone":"UTC"} + * // Convert to an object + * return Object.fromEntries(sortedEntries) * }) + * + * await Effect.runPromise(program) // => { language: "en-US", theme: "dark", timezone: "UTC" } * ``` * - * @category combinators + * @category getters * @since 4.0.0 */ export const toEntries = ( @@ -2123,7 +2066,7 @@ export const toEntries = ( * * **Example** (Converting to values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashMap } from "effect" * * const program = Effect.gen(function*() { @@ -2135,22 +2078,23 @@ export const toEntries = ( * * // Get all product information * const products = yield* TxHashMap.toValues(inventory) - * console.log(products.length) // 3 + * products.length // => 3 * * // Calculate total inventory value * const totalValue = products.reduce( * (sum, product) => sum + (product.price * product.stock), * 0 * ) - * console.log(`Total inventory value: $${totalValue}`) // Total inventory value: $8025 + * totalValue // => 8025 * * // Find products with low stock - * const lowStockProducts = products.filter((product) => product.stock < 10) - * console.log(`${lowStockProducts.length} product with low stock`) // 1 product with low stock + * return products.filter((product) => product.stock < 10).length * }) + * + * await Effect.runPromise(program) // => 1 * ``` * - * @category combinators + * @category getters * @since 4.0.0 */ export const toValues = (self: TxHashMap): Effect.Effect> => values(self) diff --git a/.context/effect/packages/effect/src/TxHashSet.ts b/.context/effect/packages/effect/src/TxHashSet.ts index 3e6e0e0fa..5ed9e793b 100644 --- a/.context/effect/packages/effect/src/TxHashSet.ts +++ b/.context/effect/packages/effect/src/TxHashSet.ts @@ -55,7 +55,7 @@ const TxHashSetProto = { * * **Example** (Using transactional hash sets) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -64,8 +64,7 @@ const TxHashSetProto = { * * // Single operations are automatically transactional * yield* TxHashSet.add(txSet, "grape") - * const hasApple = yield* TxHashSet.has(txSet, "apple") - * console.log(hasApple) // true + * yield* TxHashSet.has(txSet, "apple") // => true * * // Multi-step atomic operations * yield* Effect.tx( @@ -78,9 +77,10 @@ const TxHashSetProto = { * }) * ) * - * const size = yield* TxHashSet.size(txSet) - * console.log(size) // 4 + * yield* TxHashSet.size(txSet) // => 4 * }) + * + * await Effect.runPromise(program) * ``` * * @category models @@ -97,7 +97,7 @@ export interface TxHashSet extends Inspectable, Pipeable { * * **Example** (Extracting value types inside transactions) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -111,7 +111,10 @@ export interface TxHashSet extends Inspectable, Pipeable { * const addColor = (color: Color) => TxHashSet.add(colors, color) * * yield* addColor("yellow") + * yield* TxHashSet.has(colors, "yellow") // => true * }) + * + * await Effect.runPromise(program) * ``` * * @since 4.0.0 @@ -122,7 +125,7 @@ export declare namespace TxHashSet { * * **Example** (Extracting a TxHashSet value type) * - * ```ts + * ```ts import.meta.vitest * import type { TxHashSet } from "effect" * * type FruitSet = TxHashSet.TxHashSet<"apple" | "banana" | "cherry"> @@ -134,7 +137,7 @@ export declare namespace TxHashSet { * return `Processing ${fruit}` * } * - * console.log(processFruit("apple")) // Processing apple + * processFruit("apple") // => "Processing apple" * ``` * * @category utility types @@ -154,20 +157,22 @@ const makeTxHashSet = (ref: TxRef.TxRef>): TxHashSet => * * **Example** (Creating an empty transactional hash set) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.empty() * - * console.log(yield* TxHashSet.size(txSet)) // 0 - * console.log(yield* TxHashSet.isEmpty(txSet)) // true + * yield* TxHashSet.size(txSet) // => 0 + * yield* TxHashSet.isEmpty(txSet) // => true * * // Add some values * yield* TxHashSet.add(txSet, "hello") * yield* TxHashSet.add(txSet, "world") - * console.log(yield* TxHashSet.size(txSet)) // 2 + * yield* TxHashSet.size(txSet) // => 2 * }) + * + * await Effect.runPromise(program) * ``` * * @category constructors @@ -184,19 +189,21 @@ export const empty = (): Effect.Effect> => * * **Example** (Creating transactional hash sets from values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const fruits = yield* TxHashSet.make("apple", "banana", "cherry") - * console.log(yield* TxHashSet.size(fruits)) // 3 + * yield* TxHashSet.size(fruits) // => 3 * * const numbers = yield* TxHashSet.make(1, 2, 3, 2, 1) // Duplicates ignored - * console.log(yield* TxHashSet.size(numbers)) // 3 + * yield* TxHashSet.size(numbers) // => 3 * * const mixed = yield* TxHashSet.make("hello", 42, true) - * console.log(yield* TxHashSet.size(mixed)) // 3 + * yield* TxHashSet.size(mixed) // => 3 * }) + * + * await Effect.runPromise(program) * ``` * * @category constructors @@ -216,20 +223,21 @@ export const make = >( * * **Example** (Creating a transactional hash set from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const fromArray = yield* TxHashSet.fromIterable(["a", "b", "c", "b", "a"]) - * console.log(yield* TxHashSet.size(fromArray)) // 3 + * yield* TxHashSet.size(fromArray) // => 3 * * const fromSet = yield* TxHashSet.fromIterable(new Set([1, 2, 3])) - * console.log(yield* TxHashSet.size(fromSet)) // 3 + * yield* TxHashSet.size(fromSet) // => 3 * * const fromString = yield* TxHashSet.fromIterable("hello") - * const values = yield* TxHashSet.toHashSet(fromString) - * console.log(Array.from(values).sort()) // ["e", "h", "l", "o"] + * Array.from(yield* TxHashSet.toHashSet(fromString)).sort() // => ["e", "h", "l", "o"] * }) + * + * await Effect.runPromise(program) * ``` * * @category constructors @@ -247,21 +255,23 @@ export const fromIterable = (values: Iterable): Effect.Effect * * **Example** (Creating a transactional hash set from a HashSet) * - * ```ts + * ```ts import.meta.vitest * import { Effect, HashSet, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const hashSet = HashSet.make("x", "y", "z") * const txSet = yield* TxHashSet.fromHashSet(hashSet) * - * console.log(yield* TxHashSet.size(txSet)) // 3 - * console.log(yield* TxHashSet.has(txSet, "y")) // true + * yield* TxHashSet.size(txSet) // => 3 + * yield* TxHashSet.has(txSet, "y") // => true * * // Original hashSet is unchanged when txSet is modified * yield* TxHashSet.add(txSet, "w") - * console.log(HashSet.size(hashSet)) // 3 (original unchanged) - * console.log(yield* TxHashSet.size(txSet)) // 4 + * HashSet.size(hashSet) // => 3 + * yield* TxHashSet.size(txSet) // => 4 * }) + * + * await Effect.runPromise(program) * ``` * * @category constructors @@ -278,7 +288,7 @@ export const fromHashSet = (hashSet: HashSet.HashSet): Effect.Effect(hashSet: HashSet.HashSet): Effect.Effect true + * TxHashSet.isTxHashSet(hashSet) // => false + * TxHashSet.isTxHashSet(array) // => false + * TxHashSet.isTxHashSet(null) // => false * }) + * + * await Effect.runPromise(program) * ``` * * @category guards @@ -307,20 +319,22 @@ export const isTxHashSet = (u: unknown): u is TxHashSet => hasProperty( * * **Example** (Adding values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.make("a", "b") * * yield* TxHashSet.add(txSet, "c") - * console.log(yield* TxHashSet.size(txSet)) // 3 - * console.log(yield* TxHashSet.has(txSet, "c")) // true + * yield* TxHashSet.size(txSet) // => 3 + * yield* TxHashSet.has(txSet, "c") // => true * * // Adding existing value has no effect * yield* TxHashSet.add(txSet, "a") - * console.log(yield* TxHashSet.size(txSet)) // 3 (unchanged) + * yield* TxHashSet.size(txSet) // => 3 * }) + * + * await Effect.runPromise(program) * ``` * * @category mutations @@ -343,21 +357,21 @@ export const add: { * * **Example** (Removing values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.make("a", "b", "c") * - * const removed = yield* TxHashSet.remove(txSet, "b") - * console.log(removed) // true (value existed and was removed) - * console.log(yield* TxHashSet.size(txSet)) // 2 - * console.log(yield* TxHashSet.has(txSet, "b")) // false + * yield* TxHashSet.remove(txSet, "b") // => true + * yield* TxHashSet.size(txSet) // => 2 + * yield* TxHashSet.has(txSet, "b") // => false * * // Removing non-existent value returns false - * const notRemoved = yield* TxHashSet.remove(txSet, "d") - * console.log(notRemoved) // false + * yield* TxHashSet.remove(txSet, "d") // => false * }) + * + * await Effect.runPromise(program) * ``` * * @category mutations @@ -384,14 +398,14 @@ export const remove: { * * **Example** (Checking membership) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Equal, Hash, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.make("apple", "banana", "cherry") * - * console.log(yield* TxHashSet.has(txSet, "apple")) // true - * console.log(yield* TxHashSet.has(txSet, "grape")) // false + * yield* TxHashSet.has(txSet, "apple") // => true + * yield* TxHashSet.has(txSet, "grape") // => false * * // Works with any type that implements Equal * class Person implements Equal.Equal { @@ -407,11 +421,13 @@ export const remove: { * } * * const people = yield* TxHashSet.make(new Person("Alice"), new Person("Bob")) - * console.log(yield* TxHashSet.has(people, new Person("Alice"))) // true + * yield* TxHashSet.has(people, new Person("Alice")) // => true * }) + * + * await Effect.runPromise(program) * ``` * - * @category elements + * @category predicates * @since 2.0.0 */ export const has: { @@ -431,19 +447,21 @@ export const has: { * * **Example** (Getting the set size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const empty = yield* TxHashSet.empty() - * console.log(yield* TxHashSet.size(empty)) // 0 + * yield* TxHashSet.size(empty) // => 0 * * const small = yield* TxHashSet.make("a", "b") - * console.log(yield* TxHashSet.size(small)) // 2 + * yield* TxHashSet.size(small) // => 2 * * const fromIterable = yield* TxHashSet.fromIterable(["x", "y", "z", "x", "y"]) - * console.log(yield* TxHashSet.size(fromIterable)) // 3 (duplicates ignored) + * yield* TxHashSet.size(fromIterable) // => 3 * }) + * + * await Effect.runPromise(program) * ``` * * @category getters @@ -460,19 +478,21 @@ export const size = (self: TxHashSet): Effect.Effect => * * **Example** (Checking whether a set is empty) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const empty = yield* TxHashSet.empty() - * console.log(yield* TxHashSet.isEmpty(empty)) // true + * yield* TxHashSet.isEmpty(empty) // => true * * const nonEmpty = yield* TxHashSet.make("a") - * console.log(yield* TxHashSet.isEmpty(nonEmpty)) // false + * yield* TxHashSet.isEmpty(nonEmpty) // => false * }) + * + * await Effect.runPromise(program) * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: TxHashSet): Effect.Effect => @@ -490,17 +510,19 @@ export const isEmpty = (self: TxHashSet): Effect.Effect => * * **Example** (Clearing all values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.make("a", "b", "c") - * console.log(yield* TxHashSet.size(txSet)) // 3 + * yield* TxHashSet.size(txSet) // => 3 * * yield* TxHashSet.clear(txSet) - * console.log(yield* TxHashSet.size(txSet)) // 0 - * console.log(yield* TxHashSet.isEmpty(txSet)) // true + * yield* TxHashSet.size(txSet) // => 0 + * yield* TxHashSet.isEmpty(txSet) // => true * }) + * + * await Effect.runPromise(program) * ``` * * @category mutations @@ -513,7 +535,7 @@ export const clear = (self: TxHashSet): Effect.Effect => TxRef.set(s * * **Example** (Combining sets with union) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -521,10 +543,11 @@ export const clear = (self: TxHashSet): Effect.Effect => TxRef.set(s * const set2 = yield* TxHashSet.make("b", "c") * const combined = yield* TxHashSet.union(set1, set2) * - * const values = yield* TxHashSet.toHashSet(combined) - * console.log(Array.from(values).sort()) // ["a", "b", "c"] - * console.log(yield* TxHashSet.size(combined)) // 3 + * Array.from(yield* TxHashSet.toHashSet(combined)).sort() // => ["a", "b", "c"] + * yield* TxHashSet.size(combined) // => 3 * }) + * + * await Effect.runPromise(program) * ``` * * @category combinators @@ -554,7 +577,7 @@ export const union: { * * **Example** (Finding common values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -562,10 +585,11 @@ export const union: { * const set2 = yield* TxHashSet.make("b", "c", "d") * const common = yield* TxHashSet.intersection(set1, set2) * - * const values = yield* TxHashSet.toHashSet(common) - * console.log(Array.from(values).sort()) // ["b", "c"] - * console.log(yield* TxHashSet.size(common)) // 2 + * Array.from(yield* TxHashSet.toHashSet(common)).sort() // => ["b", "c"] + * yield* TxHashSet.size(common) // => 2 * }) + * + * await Effect.runPromise(program) * ``` * * @category combinators @@ -595,7 +619,7 @@ export const intersection: { * * **Example** (Finding values absent from another set) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -603,10 +627,11 @@ export const intersection: { * const set2 = yield* TxHashSet.make("b", "d") * const diff = yield* TxHashSet.difference(set1, set2) * - * const values = yield* TxHashSet.toHashSet(diff) - * console.log(Array.from(values).sort()) // ["a", "c"] - * console.log(yield* TxHashSet.size(diff)) // 2 + * Array.from(yield* TxHashSet.toHashSet(diff)).sort() // => ["a", "c"] + * yield* TxHashSet.size(diff) // => 2 * }) + * + * await Effect.runPromise(program) * ``` * * @category combinators @@ -636,7 +661,7 @@ export const difference: { * * **Example** (Checking subset relationships) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { @@ -644,14 +669,16 @@ export const difference: { * const large = yield* TxHashSet.make("a", "b", "c", "d") * const other = yield* TxHashSet.make("x", "y") * - * console.log(yield* TxHashSet.isSubset(small, large)) // true - * console.log(yield* TxHashSet.isSubset(large, small)) // false - * console.log(yield* TxHashSet.isSubset(small, other)) // false - * console.log(yield* TxHashSet.isSubset(small, small)) // true + * yield* TxHashSet.isSubset(small, large) // => true + * yield* TxHashSet.isSubset(large, small) // => false + * yield* TxHashSet.isSubset(small, other) // => false + * yield* TxHashSet.isSubset(small, small) // => true * }) + * + * await Effect.runPromise(program) * ``` * - * @category elements + * @category predicates * @since 4.0.0 */ export const isSubset: { @@ -672,21 +699,23 @@ export const isSubset: { * * **Example** (Testing whether some values match) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5) * - * console.log(yield* TxHashSet.some(numbers, (n) => n > 3)) // true - * console.log(yield* TxHashSet.some(numbers, (n) => n > 10)) // false + * yield* TxHashSet.some(numbers, (n) => n > 3) // => true + * yield* TxHashSet.some(numbers, (n) => n > 10) // => false * * const empty = yield* TxHashSet.empty() - * console.log(yield* TxHashSet.some(empty, (n) => n > 0)) // false + * yield* TxHashSet.some(empty, (n) => n > 0) // => false * }) + * + * await Effect.runPromise(program) * ``` * - * @category elements + * @category predicates * @since 4.0.0 */ export const some: { @@ -706,21 +735,23 @@ export const some: { * * **Example** (Testing whether every value matches) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const numbers = yield* TxHashSet.make(2, 4, 6, 8) * - * console.log(yield* TxHashSet.every(numbers, (n) => n % 2 === 0)) // true - * console.log(yield* TxHashSet.every(numbers, (n) => n > 5)) // false + * yield* TxHashSet.every(numbers, (n) => n % 2 === 0) // => true + * yield* TxHashSet.every(numbers, (n) => n > 5) // => false * * const empty = yield* TxHashSet.empty() - * console.log(yield* TxHashSet.every(empty, (n) => n > 0)) // true (vacuously true) + * yield* TxHashSet.every(empty, (n) => n > 0) // => true * }) + * + * await Effect.runPromise(program) * ``` * - * @category elements + * @category predicates * @since 4.0.0 */ export const every: { @@ -740,23 +771,23 @@ export const every: { * * **Example** (Mapping values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const numbers = yield* TxHashSet.make(1, 2, 3) * const doubled = yield* TxHashSet.map(numbers, (n) => n * 2) * - * const values = yield* TxHashSet.toHashSet(doubled) - * console.log(Array.from(values).sort()) // [2, 4, 6] - * console.log(yield* TxHashSet.size(doubled)) // 3 + * Array.from(yield* TxHashSet.toHashSet(doubled)).sort() // => [2, 4, 6] + * yield* TxHashSet.size(doubled) // => 3 * * // Mapping can reduce size if function produces duplicates * const strings = yield* TxHashSet.make("apple", "banana", "cherry") * const lengths = yield* TxHashSet.map(strings, (s) => s.length) - * const lengthValues = yield* TxHashSet.toHashSet(lengths) - * console.log(Array.from(lengthValues).sort()) // [5, 6] (apple=5, banana=6, cherry=6) + * Array.from(yield* TxHashSet.toHashSet(lengths)).sort() // => [5, 6] * }) + * + * await Effect.runPromise(program) * ``` * * @category mapping @@ -780,17 +811,18 @@ export const map: { * * **Example** (Filtering values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5, 6) * const evens = yield* TxHashSet.filter(numbers, (n) => n % 2 === 0) * - * const values = yield* TxHashSet.toHashSet(evens) - * console.log(Array.from(values).sort()) // [2, 4, 6] - * console.log(yield* TxHashSet.size(evens)) // 3 + * Array.from(yield* TxHashSet.toHashSet(evens)).sort() // => [2, 4, 6] + * yield* TxHashSet.size(evens) // => 3 * }) + * + * await Effect.runPromise(program) * ``` * * @category filtering @@ -836,19 +868,18 @@ export const filter: { * * **Example** (Reducing values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const numbers = yield* TxHashSet.make(1, 2, 3, 4, 5) - * const sum = yield* TxHashSet.reduce(numbers, 0, (acc, n) => acc + n) - * - * console.log(sum) // 15 + * yield* TxHashSet.reduce(numbers, 0, (acc, n) => acc + n) // => 15 * * const strings = yield* TxHashSet.make("a", "b", "c") - * const concatenated = yield* TxHashSet.reduce(strings, "", (acc, s) => acc + s) - * console.log(concatenated) // Order may vary: "abc", "bac", etc. + * String(yield* TxHashSet.reduce(strings, "", (acc, s) => acc + s)).split("").sort().join("") // => "abc" * }) + * + * await Effect.runPromise(program) * ``` * * @category folding @@ -885,21 +916,23 @@ export const reduce: { * * **Example** (Taking a HashSet snapshot) * - * ```ts + * ```ts import.meta.vitest * import { Effect, HashSet, TxHashSet } from "effect" * * const program = Effect.gen(function*() { * const txSet = yield* TxHashSet.make("x", "y", "z") * const hashSet = yield* TxHashSet.toHashSet(txSet) * - * console.log(HashSet.size(hashSet)) // 3 - * console.log(HashSet.has(hashSet, "y")) // true + * HashSet.size(hashSet) // => 3 + * HashSet.has(hashSet, "y") // => true * * // hashSet is a snapshot - modifications to txSet don't affect it * yield* TxHashSet.add(txSet, "w") - * console.log(HashSet.size(hashSet)) // 3 (unchanged) - * console.log(yield* TxHashSet.size(txSet)) // 4 + * HashSet.size(hashSet) // => 3 + * yield* TxHashSet.size(txSet) // => 4 * }) + * + * await Effect.runPromise(program) * ``` * * @category converting diff --git a/.context/effect/packages/effect/src/TxPriorityQueue.ts b/.context/effect/packages/effect/src/TxPriorityQueue.ts index 6f5b410ed..179e7e096 100644 --- a/.context/effect/packages/effect/src/TxPriorityQueue.ts +++ b/.context/effect/packages/effect/src/TxPriorityQueue.ts @@ -39,7 +39,7 @@ const TypeId = "~effect/transactions/TxPriorityQueue" * * **Example** (Dequeuing values by priority) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { @@ -47,9 +47,10 @@ const TypeId = "~effect/transactions/TxPriorityQueue" * yield* TxPriorityQueue.offer(pq, 3) * yield* TxPriorityQueue.offer(pq, 1) * yield* TxPriorityQueue.offer(pq, 2) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category models @@ -107,14 +108,15 @@ const insertSorted = (chunk: Chunk, value: A, ord: Order): Chunk => * * **Example** (Creating an empty priority queue) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) - * const empty = yield* TxPriorityQueue.isEmpty(pq) - * console.log(empty) // true + * return yield* TxPriorityQueue.isEmpty(pq) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category constructors @@ -128,14 +130,15 @@ export const empty = (order: Order): Effect.Effect> => * * **Example** (Creating a priority queue from an iterable) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category constructors @@ -160,14 +163,15 @@ export const fromIterable: { * * **Example** (Creating a priority queue from variadic values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.make(Order.Number)(3, 1, 2) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category constructors @@ -181,14 +185,15 @@ export const make = (order: Order) => (...elements: Array): Effect.Effe * * **Example** (Getting the queue size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3]) - * const s = yield* TxPriorityQueue.size(pq) - * console.log(s) // 3 + * return yield* TxPriorityQueue.size(pq) * }) + * + * await Effect.runPromise(program) // => 3 * ``` * * @category getters @@ -201,17 +206,18 @@ export const size = (self: TxPriorityQueue): Effect.Effect => Effe * * **Example** (Checking whether a queue is empty) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) - * const empty = yield* TxPriorityQueue.isEmpty(pq) - * console.log(empty) // true + * return yield* TxPriorityQueue.isEmpty(pq) * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: TxPriorityQueue): Effect.Effect => Effect.map(size(self), (n) => n === 0) @@ -221,17 +227,18 @@ export const isEmpty = (self: TxPriorityQueue): Effect.Effect => * * **Example** (Checking whether a queue has elements) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1]) - * const nonEmpty = yield* TxPriorityQueue.isNonEmpty(pq) - * console.log(nonEmpty) // true + * return yield* TxPriorityQueue.isNonEmpty(pq) * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isNonEmpty = (self: TxPriorityQueue): Effect.Effect => Effect.map(size(self), (n) => n > 0) @@ -246,14 +253,15 @@ export const isNonEmpty = (self: TxPriorityQueue): Effect.Effect * * **Example** (Peeking at the next value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) - * const top = yield* TxPriorityQueue.peek(pq) - * console.log(top) // 1 + * return yield* TxPriorityQueue.peek(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category getters @@ -279,14 +287,15 @@ export const peek = (self: TxPriorityQueue): Effect.Effect => * * **Example** (Peeking without retrying) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) - * const result = yield* TxPriorityQueue.peekOption(pq) - * console.log(Option.isNone(result)) // true + * return yield* TxPriorityQueue.peekOption(pq) * }) + * + * await Effect.runPromise(program) // => Option.none() * ``` * * @category getters @@ -300,16 +309,17 @@ export const peekOption = (self: TxPriorityQueue): Effect.Effect * * **Example** (Offering a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) * yield* TxPriorityQueue.offer(pq, 2) * yield* TxPriorityQueue.offer(pq, 1) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category mutations @@ -329,15 +339,16 @@ export const offer: { * * **Example** (Offering multiple values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) * yield* TxPriorityQueue.offerAll(pq, [3, 1, 2]) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category mutations @@ -360,14 +371,15 @@ export const offerAll: { * * **Example** (Taking the next value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) - * const first = yield* TxPriorityQueue.take(pq) - * console.log(first) // 1 + * return yield* TxPriorityQueue.take(pq) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category mutations @@ -389,14 +401,15 @@ export const take = (self: TxPriorityQueue): Effect.Effect => * * **Example** (Taking all values in priority order) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) - * const all = yield* TxPriorityQueue.takeAll(pq) - * console.log(all) // [1, 2, 3] + * return yield* TxPriorityQueue.takeAll(pq) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category mutations @@ -413,14 +426,15 @@ export const takeAll = (self: TxPriorityQueue): Effect.Effect> => * * **Example** (Taking without retrying) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) - * const result = yield* TxPriorityQueue.takeOption(pq) - * console.log(Option.isNone(result)) // true + * return yield* TxPriorityQueue.takeOption(pq) * }) + * + * await Effect.runPromise(program) // => Option.none() * ``` * * @category mutations @@ -440,14 +454,15 @@ export const takeOption = (self: TxPriorityQueue): Effect.Effect * * **Example** (Taking up to a limit) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [5, 3, 1, 4, 2]) - * const top2 = yield* TxPriorityQueue.takeUpTo(pq, 2) - * console.log(top2) // [1, 2] + * return yield* TxPriorityQueue.takeUpTo(pq, 2) * }) + * + * await Effect.runPromise(program) // => [1, 2] * ``` * * @category mutations @@ -474,15 +489,16 @@ export const takeUpTo: { * * **Example** (Removing matching values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3, 4, 5]) * yield* TxPriorityQueue.removeIf(pq, (n) => n % 2 === 0) - * const all = yield* TxPriorityQueue.takeAll(pq) - * console.log(all) // [1, 3, 5] + * return yield* TxPriorityQueue.takeAll(pq) * }) + * + * await Effect.runPromise(program) // => [1, 3, 5] * ``` * * @category filtering @@ -502,15 +518,16 @@ export const removeIf: { * * **Example** (Retaining matching values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [1, 2, 3, 4, 5]) * yield* TxPriorityQueue.retainIf(pq, (n) => n % 2 === 0) - * const all = yield* TxPriorityQueue.takeAll(pq) - * console.log(all) // [2, 4] + * return yield* TxPriorityQueue.takeAll(pq) * }) + * + * await Effect.runPromise(program) // => [2, 4] * ``` * * @category filtering @@ -530,14 +547,15 @@ export const retainIf: { * * **Example** (Reading values in priority order) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.fromIterable(Order.Number, [3, 1, 2]) - * const all = yield* TxPriorityQueue.toArray(pq) - * console.log(all) // [1, 2, 3] + * return yield* TxPriorityQueue.toArray(pq) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category converting @@ -551,14 +569,15 @@ export const toArray = (self: TxPriorityQueue): Effect.Effect> => * * **Example** (Checking for a TxPriorityQueue) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Order, TxPriorityQueue } from "effect" * * const program = Effect.gen(function*() { * const pq = yield* TxPriorityQueue.empty(Order.Number) - * console.log(TxPriorityQueue.isTxPriorityQueue(pq)) // true - * console.log(TxPriorityQueue.isTxPriorityQueue("nope")) // false + * return [TxPriorityQueue.isTxPriorityQueue(pq), TxPriorityQueue.isTxPriorityQueue("nope")] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category guards diff --git a/.context/effect/packages/effect/src/TxPubSub.ts b/.context/effect/packages/effect/src/TxPubSub.ts index e1ecbc9ea..697bc59ad 100644 --- a/.context/effect/packages/effect/src/TxPubSub.ts +++ b/.context/effect/packages/effect/src/TxPubSub.ts @@ -9,6 +9,7 @@ * * @since 4.0.0 */ +import * as Arr from "./Array.ts" import * as Effect from "./Effect.ts" import { dual } from "./Function.ts" import type { Inspectable } from "./Inspectable.ts" @@ -28,21 +29,22 @@ const TypeId = "~effect/transactions/TxPubSub" * * **Example** (Subscribing to a transactional pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, "hello") - * const msg = yield* TxQueue.take(sub) - * console.log(msg) // "hello" + * return yield* TxQueue.take(sub) * }) * ) * }) + * + * await Effect.runPromise(program) // => "hello" * ``` * * @category models @@ -102,21 +104,22 @@ const makeTxPubSub = ( * * **Example** (Creating a bounded pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.bounded(16) * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, 42) - * const value = yield* TxQueue.take(sub) - * console.log(value) // 42 + * return yield* TxQueue.take(sub) * }) * ) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category constructors @@ -135,13 +138,13 @@ export const bounded = (capacity: number): Effect.Effect> * * **Example** (Creating a dropping pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.dropping(2) * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, 1) @@ -149,10 +152,12 @@ export const bounded = (capacity: number): Effect.Effect> * yield* TxPubSub.publish(hub, 3) // dropped * const v1 = yield* TxQueue.take(sub) * const v2 = yield* TxQueue.take(sub) - * console.log(v1, v2) // 1 2 + * return [v1, v2] * }) * ) * }) + * + * await Effect.runPromise(program) // => [1, 2] * ``` * * @category constructors @@ -171,23 +176,24 @@ export const dropping = (capacity: number): Effect.Effect * * **Example** (Creating a sliding pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.sliding(2) * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, 1) * yield* TxPubSub.publish(hub, 2) * yield* TxPubSub.publish(hub, 3) // evicts 1 - * const v1 = yield* TxQueue.take(sub) - * console.log(v1) // 2 + * return yield* TxQueue.take(sub) * }) * ) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category constructors @@ -205,21 +211,22 @@ export const sliding = (capacity: number): Effect.Effect> * * **Example** (Creating an unbounded pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, "msg") - * const msg = yield* TxQueue.take(sub) - * console.log(msg) // "msg" + * return yield* TxQueue.take(sub) * }) * ) * }) + * + * await Effect.runPromise(program) // => "msg" * ``` * * @category constructors @@ -241,13 +248,15 @@ export const unbounded = (): Effect.Effect> => * * **Example** (Reading pub/sub capacity) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.bounded(16) - * console.log(TxPubSub.capacity(hub)) // 16 + * return TxPubSub.capacity(hub) * }) + * + * await Effect.runPromise(program) // => 16 * ``` * * @category getters @@ -260,22 +269,23 @@ export const capacity = (self: TxPubSub): number => self.capacity * * **Example** (Reading subscriber queue size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, 1) * yield* TxPubSub.publish(hub, 2) - * const s = yield* TxPubSub.size(hub) - * console.log(s) // 2 + * return yield* TxPubSub.size(hub) * }) * ) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category getters @@ -297,17 +307,18 @@ export const size = (self: TxPubSub): Effect.Effect => * * **Example** (Checking whether a pub/sub is empty) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() - * const empty = yield* TxPubSub.isEmpty(hub) - * console.log(empty) // true + * return yield* TxPubSub.isEmpty(hub) * }) + * + * await Effect.runPromise(program) // => true * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: TxPubSub): Effect.Effect => Effect.map(size(self), (s) => s === 0) @@ -317,17 +328,18 @@ export const isEmpty = (self: TxPubSub): Effect.Effect => Effect. * * **Example** (Checking whether a pub/sub is full) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.bounded(2) - * const full = yield* TxPubSub.isFull(hub) - * console.log(full) // false + * return yield* TxPubSub.isFull(hub) * }) + * + * await Effect.runPromise(program) // => false * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isFull = (self: TxPubSub): Effect.Effect => @@ -345,18 +357,20 @@ export const isFull = (self: TxPubSub): Effect.Effect => * * **Example** (Checking whether a pub/sub is shut down) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() - * console.log(yield* TxPubSub.isShutdown(hub)) // false + * const before = yield* TxPubSub.isShutdown(hub) * yield* TxPubSub.shutdown(hub) - * console.log(yield* TxPubSub.isShutdown(hub)) // true + * return [before, yield* TxPubSub.isShutdown(hub)] * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category getters + * @category predicates * @since 2.0.0 */ export const isShutdown = (self: TxPubSub): Effect.Effect => TxRef.get(self.shutdownRef) @@ -374,7 +388,7 @@ export const isShutdown = (self: TxPubSub): Effect.Effect => TxRe * * **Example** (Publishing a message to subscribers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -382,17 +396,18 @@ export const isShutdown = (self: TxPubSub): Effect.Effect => TxRe * * // No subscribers - publish is a no-op * const r1 = yield* TxPubSub.publish(hub, "no one listening") - * console.log(r1) // true * - * yield* Effect.scoped( + * const msg = yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publish(hub, "hello") - * const msg = yield* TxQueue.take(sub) - * console.log(msg) // "hello" + * return yield* TxQueue.take(sub) * }) * ) + * return [r1, msg] * }) + * + * await Effect.runPromise(program) // => [true, "hello"] * ``` * * @category mutations @@ -428,23 +443,25 @@ export const publish: { * * **Example** (Publishing multiple messages to subscribers) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxPubSub.subscribe(hub) * yield* TxPubSub.publishAll(hub, [1, 2, 3]) * const v1 = yield* TxQueue.take(sub) * const v2 = yield* TxQueue.take(sub) * const v3 = yield* TxQueue.take(sub) - * console.log(v1, v2, v3) // 1 2 3 + * return [v1, v2, v3] * }) * ) * }) + * + * await Effect.runPromise(program) // => [1, 2, 3] * ``` * * @category mutations @@ -455,17 +472,19 @@ export const publishAll: { (self: TxPubSub, values: Iterable): Effect.Effect } = dual( 2, - (self: TxPubSub, values: Iterable): Effect.Effect => - Effect.gen(function*() { + (self: TxPubSub, values: Iterable): Effect.Effect => { + const valuesArray = Arr.fromIterable(values) + return Effect.gen(function*() { if (yield* TxRef.get(self.shutdownRef)) return false let allAccepted = true - for (const value of values) { + for (const value of valuesArray) { const accepted = yield* publish(self, value) if (!accepted) allAccepted = false } return allAccepted }).pipe(Effect.tx) + } ) /** @@ -477,13 +496,13 @@ export const publishAll: { * * **Example** (Subscribing multiple queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub1 = yield* TxPubSub.subscribe(hub) * const sub2 = yield* TxPubSub.subscribe(hub) @@ -492,10 +511,12 @@ export const publishAll: { * * const msg1 = yield* TxQueue.take(sub1) * const msg2 = yield* TxQueue.take(sub2) - * console.log(msg1, msg2) // "broadcast" "broadcast" + * return [msg1, msg2] * }) * ) * }) + * + * await Effect.runPromise(program) // => ["broadcast", "broadcast"] * ``` * * @category mutations @@ -602,7 +623,7 @@ const makeSubscriberQueue = ( * * **Example** (Shutting down a pub/sub) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxPubSub } from "effect" * * const program = Effect.gen(function*() { @@ -610,11 +631,11 @@ const makeSubscriberQueue = ( * yield* TxPubSub.shutdown(hub) * * const shut = yield* TxPubSub.isShutdown(hub) - * console.log(shut) // true - * * const accepted = yield* TxPubSub.publish(hub, 1) - * console.log(accepted) // false + * return [shut, accepted] * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @category mutations @@ -637,16 +658,19 @@ export const shutdown = (self: TxPubSub): Effect.Effect => * * **Example** (Waiting for shutdown) * - * ```ts - * import { Effect, TxPubSub } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber, TxPubSub } from "effect" * * const program = Effect.gen(function*() { * const hub = yield* TxPubSub.unbounded() * * const fiber = yield* Effect.forkChild(TxPubSub.awaitShutdown(hub)) * yield* TxPubSub.shutdown(hub) - * yield* fiber.await + * yield* Fiber.await(fiber) + * return yield* TxPubSub.isShutdown(hub) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category mutations @@ -668,14 +692,11 @@ export const awaitShutdown = (self: TxPubSub): Effect.Effect => * * **Example** (Checking for a TxPubSub) * - * ```ts + * ```ts import.meta.vitest * import { TxPubSub } from "effect" * - * declare const someValue: unknown - * - * if (TxPubSub.isTxPubSub(someValue)) { - * console.log("This is a TxPubSub") - * } + * const someValue: unknown = {} + * TxPubSub.isTxPubSub(someValue) // => false * ``` * * @category guards diff --git a/.context/effect/packages/effect/src/TxQueue.ts b/.context/effect/packages/effect/src/TxQueue.ts index 5e9f7cd1f..e4e7f0c80 100644 --- a/.context/effect/packages/effect/src/TxQueue.ts +++ b/.context/effect/packages/effect/src/TxQueue.ts @@ -36,19 +36,11 @@ import type * as Types from "./Types.ts" * * **Example** (Inspecting queue lifecycle states) * - * ```ts + * ```ts import.meta.vitest * import type { TxQueue } from "effect" * - * // State progression example - * declare const state: TxQueue.State - * - * if (state._tag === "Open") { - * console.log("Queue is accepting new items") - * } else if (state._tag === "Closing") { - * console.log("Queue is draining, cause:", state.cause) - * } else { - * console.log("Queue is done, cause:", state.cause) - * } + * const state: TxQueue.State = { _tag: "Open" } + * state._tag // => "Open" * ``` * * @category models @@ -146,7 +138,7 @@ export interface TxQueueState extends Inspectable { * * **Example** (Offering values through enqueue handles) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * import type { Cause } from "effect" * @@ -167,7 +159,11 @@ export interface TxQueueState extends Inspectable { * >(5) * yield* TxQueue.offer(completableQueue, "task") * yield* TxQueue.end(completableQueue) + * + * return accepted * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category models @@ -183,7 +179,7 @@ export interface TxEnqueue extends TxQueueState { * * **Example** (Taking values through dequeue handles) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -191,7 +187,6 @@ export interface TxEnqueue extends TxQueueState { * const queue = yield* TxQueue.bounded(10) * yield* TxQueue.offer(queue, 42) * const item = yield* TxQueue.take(queue) - * console.log(item) // 42 * * // Queue with error channel - errors propagate through E-channel * const faultTolerantQueue = yield* TxQueue.bounded(10) @@ -200,7 +195,10 @@ export interface TxEnqueue extends TxQueueState { * // All dequeue operations now fail with the error directly * const takeResult = yield* Effect.flip(TxQueue.take(faultTolerantQueue)) // "processing failed" * const peekResult = yield* Effect.flip(TxQueue.peek(faultTolerantQueue)) // "processing failed" + * return [item, takeResult, peekResult] as const * }) + * + * await Effect.runPromise(program) // => [42, "processing failed", "processing failed"] * ``` * * @category models @@ -216,7 +214,7 @@ export interface TxDequeue extends TxQueueState { * * **Example** (Combining enqueue and dequeue operations) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -226,7 +224,6 @@ export interface TxDequeue extends TxQueueState { * // Single operations - automatically transactional * const accepted = yield* TxQueue.offer(queue, 42) * const item = yield* TxQueue.take(queue) // Effect - * console.log(item) // 42 * * // Queue with error channel * const faultTolerantQueue = yield* TxQueue.bounded(10) @@ -234,8 +231,10 @@ export interface TxDequeue extends TxQueueState { * // Operations can handle queue-level failures * yield* TxQueue.fail(faultTolerantQueue, "queue failed") * const result = yield* Effect.flip(TxQueue.take(faultTolerantQueue)) - * console.log(result) // "queue failed" + * return [accepted, item, result] as const * }) + * + * await Effect.runPromise(program) // => [true, 42, "queue failed"] * ``` * * @category models @@ -250,15 +249,11 @@ export interface TxQueue extends TxEnqueue, Tx * * **Example** (Checking enqueue handles) * - * ```ts + * ```ts import.meta.vitest * import { TxQueue } from "effect" * - * declare const someValue: unknown - * - * if (TxQueue.isTxEnqueue(someValue)) { - * // someValue is now typed as TxEnqueue - * console.log("This is a TxEnqueue") - * } + * const someValue: unknown = {} + * TxQueue.isTxEnqueue(someValue) // => false * ``` * * @category guards @@ -271,15 +266,11 @@ export const isTxEnqueue = (u: unknown): u is TxEnqueu * * **Example** (Checking dequeue handles) * - * ```ts + * ```ts import.meta.vitest * import { TxQueue } from "effect" * - * declare const someValue: unknown - * - * if (TxQueue.isTxDequeue(someValue)) { - * // someValue is now typed as TxDequeue - * console.log("This is a TxDequeue") - * } + * const someValue: unknown = {} + * TxQueue.isTxDequeue(someValue) // => false * ``` * * @category guards @@ -292,15 +283,11 @@ export const isTxDequeue = (u: unknown): u is TxDequeu * * **Example** (Checking queue handles) * - * ```ts + * ```ts import.meta.vitest * import { TxQueue } from "effect" * - * declare const someValue: unknown - * - * if (TxQueue.isTxQueue(someValue)) { - * // someValue is now typed as TxQueue - * console.log("This is a TxQueue") - * } + * const someValue: unknown = {} + * TxQueue.isTxQueue(someValue) // => false * ``` * * @category guards @@ -344,7 +331,7 @@ const TxQueueProto = { * * **Example** (Creating bounded queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -358,9 +345,10 @@ const TxQueueProto = { * yield* TxQueue.offer(queue, 1) * yield* TxQueue.offer(queue, 2) * - * const item = yield* TxQueue.take(queue) - * console.log(item) // 1 + * return yield* TxQueue.take(queue) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category constructors @@ -390,7 +378,7 @@ export const bounded = ( * * **Example** (Creating unbounded queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -404,9 +392,10 @@ export const bounded = ( * yield* TxQueue.offer(queue, "hello") * yield* TxQueue.offer(queue, "world") * - * const size = yield* TxQueue.size(queue) - * console.log(size) // 2 + * return yield* TxQueue.size(queue) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category constructors @@ -434,7 +423,7 @@ export const unbounded = (): Effect.Effect> * * **Example** (Creating dropping queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -446,9 +435,10 @@ export const unbounded = (): Effect.Effect> * yield* TxQueue.offer(queue, 2) * * // This will be dropped (returns false) - * const accepted = yield* TxQueue.offer(queue, 3) - * console.log(accepted) // false + * return yield* TxQueue.offer(queue, 3) * }) + * + * await Effect.runPromise(program) // => false * ``` * * @category constructors @@ -478,7 +468,7 @@ export const dropping = ( * * **Example** (Creating sliding queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -492,9 +482,10 @@ export const dropping = ( * // This will evict item 1 and add 3 * yield* TxQueue.offer(queue, 3) * - * const item = yield* TxQueue.take(queue) - * console.log(item) // 2 (item 1 was evicted) + * return yield* TxQueue.take(queue) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @category constructors @@ -528,16 +519,17 @@ export const sliding = ( * * **Example** (Offering a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * // Offer an item - returns true if accepted - * const accepted = yield* TxQueue.offer(queue, 42) - * console.log(accepted) // true + * return yield* TxQueue.offer(queue, 42) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category combinators @@ -595,17 +587,17 @@ export const offer: { * * **Example** (Offering multiple values) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * // Offer multiple items - returns rejected items as array - * const rejected = yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) - * console.log(rejected) // [] if all accepted - * console.log(rejected.length) // 0 + * return yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) * }) + * + * await Effect.runPromise(program) // => [] * ``` * * @category combinators @@ -616,11 +608,13 @@ export const offerAll: { (self: TxEnqueue, values: Iterable): Effect.Effect> } = dual( 2, - (self: TxEnqueue, values: Iterable): Effect.Effect> => - Effect.gen(function*() { + (self: TxEnqueue, values: Iterable): Effect.Effect> => { + const valuesArray = Array.from(values) + + return Effect.gen(function*() { const rejected: Array = [] - for (const value of values) { + for (const value of valuesArray) { const accepted = yield* offer(self, value) if (!accepted) { rejected.push(value) @@ -629,6 +623,7 @@ export const offerAll: { return rejected }).pipe(Effect.tx) + } ) /** @@ -641,8 +636,8 @@ export const offerAll: { * * **Example** (Taking a value) * - * ```ts - * import { Effect, TxQueue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) @@ -650,13 +645,14 @@ export const offerAll: { * * // Take an item - blocks if empty * const item = yield* TxQueue.take(queue) - * console.log(item) // 42 * * // When queue fails, take fails with the same error * yield* TxQueue.fail(queue, "queue error") - * const result = yield* Effect.flip(TxQueue.take(queue)) - * console.log(result) // "queue error" + * const result = yield* Effect.exit(TxQueue.take(queue)) + * return [item, result] as const * }) + * + * await Effect.runPromise(program) // => [42, Exit.fail("queue error")] * ``` * * @category combinators @@ -698,7 +694,7 @@ export const take = (self: TxDequeue): Effect.Effect => * * **Example** (Polling without blocking) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Option, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -706,12 +702,13 @@ export const take = (self: TxDequeue): Effect.Effect => * * // Poll returns Option.none if empty * const maybe = yield* TxQueue.poll(queue) - * console.log(Option.isNone(maybe)) // true * * yield* TxQueue.offer(queue, 42) * const item = yield* TxQueue.poll(queue) - * console.log(Option.getOrNull(item)) // 42 + * return [maybe, item] as const * }) + * + * await Effect.runPromise(program) // => [Option.none(), Option.some(42)] * ``` * * @category combinators @@ -731,6 +728,11 @@ export const poll = (self: TxDequeue): Effect.Effect(self: TxDequeue): Effect.Effect(10) * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) * * // Take all items atomically - returns NonEmptyArray - * const items = yield* TxQueue.takeAll(queue) - * console.log(items) // [1, 2, 3, 4, 5] - * console.log(Array.isArrayNonEmpty(items)) // true + * return yield* TxQueue.takeAll(queue) * }) * * // Error propagation example @@ -763,9 +763,11 @@ export const poll = (self: TxDequeue): Effect.Effect [1, 2, 3, 4, 5] + * await Effect.runPromise(errorExample) // => Exit.fail("processing error") * ``` * * @category combinators @@ -808,7 +810,7 @@ export const takeAll = (self: TxDequeue): Effect.Effect(self: TxDequeue): Effect.Effect [[1, 2, 3, 4], [5, 6, 7, 8, 9]] * ``` * * @category combinators @@ -898,7 +901,7 @@ export const takeN: { * * **Example** (Taking batches within bounds) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -907,15 +910,16 @@ export const takeN: { * * // Take between 2 and 5 items * const batch1 = yield* TxQueue.takeBetween(queue, 2, 5) - * console.log(batch1) // [1, 2, 3, 4, 5] - took 5 (up to max) * * // Take between 1 and 10 items (but only 3 remain) * const batch2 = yield* TxQueue.takeBetween(queue, 1, 10) - * console.log(batch2) // [6, 7, 8] - took 3 (all remaining) * * // Would wait for at least 1 item to be available * // const batch3 = yield* TxQueue.takeBetween(queue, 1, 3) + * return [batch1, batch2] as const * }) + * + * await Effect.runPromise(program) // => [[1, 2, 3, 4, 5], [6, 7, 8]] * ``` * * @category taking @@ -986,8 +990,8 @@ export const takeBetween: { * * **Example** (Peeking without removing values) * - * ```ts - * import { Effect, TxQueue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) @@ -995,11 +999,10 @@ export const takeBetween: { * * // Peek at the next item without removing it * const item = yield* TxQueue.peek(queue) - * console.log(item) // 42 * * // Item is still in the queue * const size = yield* TxQueue.size(queue) - * console.log(size) // 1 + * return [item, size] as const * }) * * // Error handling example @@ -1008,9 +1011,11 @@ export const takeBetween: { * yield* TxQueue.fail(queue, "queue failed") * * // peek() propagates the queue error through E-channel - * const result = yield* Effect.flip(TxQueue.peek(queue)) - * console.log(result) // "queue failed" + * return yield* Effect.exit(TxQueue.peek(queue)) * }) + * + * await Effect.runPromise(program) // => [42, 1] + * await Effect.runPromise(errorExample) // => Exit.fail("queue failed") * ``` * * @category combinators @@ -1037,16 +1042,17 @@ export const peek = (self: TxDequeue): Effect.Effect => * * **Example** (Reading queue size) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * yield* TxQueue.offerAll(queue, [1, 2, 3]) * - * const size = yield* TxQueue.size(queue) - * console.log(size) // 3 + * return yield* TxQueue.size(queue) * }) + * + * await Effect.runPromise(program) // => 3 * ``` * * @category combinators @@ -1059,22 +1065,23 @@ export const size = (self: TxQueueState): Effect.Effect => TxChunk.size( * * **Example** (Checking whether a queue is empty) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * const empty = yield* TxQueue.isEmpty(queue) - * console.log(empty) // true * * yield* TxQueue.offer(queue, 42) * const stillEmpty = yield* TxQueue.isEmpty(queue) - * console.log(stillEmpty) // false + * return [empty, stillEmpty] as const * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * - * @category combinators + * @category predicates * @since 2.0.0 */ export const isEmpty = (self: TxQueueState): Effect.Effect => TxChunk.isEmpty(self.items) @@ -1084,22 +1091,23 @@ export const isEmpty = (self: TxQueueState): Effect.Effect => TxChunk.i * * **Example** (Checking whether a queue is full) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(2) * * const full = yield* TxQueue.isFull(queue) - * console.log(full) // false * * yield* TxQueue.offerAll(queue, [1, 2]) * const nowFull = yield* TxQueue.isFull(queue) - * console.log(nowFull) // true + * return [full, nowFull] as const * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category combinators + * @category predicates * @since 2.0.0 */ export const isFull = (self: TxQueueState): Effect.Effect => @@ -1116,7 +1124,7 @@ export const isFull = (self: TxQueueState): Effect.Effect => * * **Example** (Interrupting queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -1124,9 +1132,10 @@ export const isFull = (self: TxQueueState): Effect.Effect => * yield* TxQueue.offer(queue, 42) * * // Interrupt gracefully - allows remaining items to be consumed - * const result = yield* TxQueue.interrupt(queue) - * console.log(result) // true + * return yield* TxQueue.interrupt(queue) * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category combinators @@ -1144,16 +1153,17 @@ export const interrupt = (self: TxEnqueue): Effect.Effect = * * **Example** (Failing queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * // Fail the queue with an error - * const result = yield* TxQueue.fail(queue, "connection lost") - * console.log(result) // true + * return yield* TxQueue.fail(queue, "connection lost") * }) + * + * await Effect.runPromise(program) // => true * ``` * * @category combinators @@ -1189,7 +1199,7 @@ export const fail: { * * **Example** (Failing queues with causes) * - * ```ts + * ```ts import.meta.vitest * import { Cause, Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -1198,8 +1208,10 @@ export const fail: { * // Complete with specific cause * const cause = Cause.interrupt() * const result = yield* TxQueue.failCause(queue, cause) - * console.log(result) // true + * return [cause, result] as const * }) + * + * await Effect.runPromise(program) // => [Cause.interrupt(), true] * ``` * * @category combinators @@ -1239,23 +1251,23 @@ export const failCause: { * * **Example** (Ending queues) * - * ```ts - * import { Cause, Effect, TxQueue } from "effect" + * ```ts import.meta.vitest + * import { Cause, Effect, Exit, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * // Signal the end of the queue * const result = yield* TxQueue.end(queue) - * console.log(result) // true * * // All operations will now fail with Done - * const takeResult = yield* Effect.flip(TxQueue.take(queue)) - * console.log(Cause.isDone(takeResult)) // true + * const takeResult = yield* Effect.exit(TxQueue.take(queue)) * - * const peekResult = yield* Effect.flip(TxQueue.peek(queue)) - * console.log(Cause.isDone(peekResult)) // true + * const peekResult = yield* Effect.exit(TxQueue.peek(queue)) + * return [result, takeResult, peekResult] as const * }) + * + * await Effect.runPromise(program) // => [true, Exit.fail(Cause.Done()), Exit.fail(Cause.Done())] * ``` * * @category combinators @@ -1265,16 +1277,15 @@ export const end = (self: TxEnqueue): Effect.Effect(self: TxEnqueue): Effect.Effect [5, [1, 2, 3, 4, 5], 0] * ``` * * @category combinators @@ -1307,6 +1318,9 @@ export const clear = (self: TxEnqueue): Effect.Effect, Excl } const chunk = yield* TxChunk.get(self.items) yield* TxChunk.set(self.items, Chunk.empty()) + if (state._tag === "Closing") { + yield* TxRef.set(self.stateRef, { _tag: "Done", cause: state.cause }) + } return Chunk.toArray(chunk) }).pipe(Effect.tx) @@ -1319,7 +1333,7 @@ export const clear = (self: TxEnqueue): Effect.Effect, Excl * * **Example** (Shutting down queues) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -1327,16 +1341,16 @@ export const clear = (self: TxEnqueue): Effect.Effect, Excl * yield* TxQueue.offerAll(queue, [1, 2, 3, 4, 5]) * * const sizeBefore = yield* TxQueue.size(queue) - * console.log(sizeBefore) // 5 * * yield* TxQueue.shutdown(queue) * * const sizeAfter = yield* TxQueue.size(queue) - * console.log(sizeAfter) // 0 (cleared) * * const isShutdown = yield* TxQueue.isShutdown(queue) - * console.log(isShutdown) // true (interrupted) + * return [sizeBefore, sizeAfter, isShutdown] as const * }) + * + * await Effect.runPromise(program) // => [5, 0, true] * ``` * * @category combinators @@ -1344,7 +1358,7 @@ export const clear = (self: TxEnqueue): Effect.Effect, Excl */ export const shutdown = (self: TxEnqueue): Effect.Effect => Effect.gen(function*() { - yield* Effect.ignore(clear(self)) + yield* Effect.ignoreCause(clear(self)) return yield* interrupt(self) }).pipe(Effect.tx) @@ -1353,22 +1367,23 @@ export const shutdown = (self: TxEnqueue): Effect.Effect => * * **Example** (Checking open state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * const open = yield* TxQueue.isOpen(queue) - * console.log(open) // true * * yield* TxQueue.interrupt(queue) * const stillOpen = yield* TxQueue.isOpen(queue) - * console.log(stillOpen) // false + * return [open, stillOpen] as const * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isOpen = (self: TxQueueState): Effect.Effect => @@ -1379,7 +1394,7 @@ export const isOpen = (self: TxQueueState): Effect.Effect => * * **Example** (Checking closing state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { @@ -1387,15 +1402,16 @@ export const isOpen = (self: TxQueueState): Effect.Effect => * yield* TxQueue.offer(queue, 42) * * const closing = yield* TxQueue.isClosing(queue) - * console.log(closing) // false * * yield* TxQueue.interrupt(queue) * const nowClosing = yield* TxQueue.isClosing(queue) - * console.log(nowClosing) // true + * return [closing, nowClosing] as const * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isClosing = (self: TxQueueState): Effect.Effect => @@ -1406,22 +1422,23 @@ export const isClosing = (self: TxQueueState): Effect.Effect => * * **Example** (Checking done state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * const done = yield* TxQueue.isDone(queue) - * console.log(done) // false * * yield* TxQueue.interrupt(queue) * const nowDone = yield* TxQueue.isDone(queue) - * console.log(nowDone) // true + * return [done, nowDone] as const * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category combinators + * @category predicates * @since 4.0.0 */ export const isDone = (self: TxQueueState): Effect.Effect => @@ -1432,22 +1449,23 @@ export const isDone = (self: TxQueueState): Effect.Effect => * * **Example** (Checking shutdown state) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * * const isShutdown = yield* TxQueue.isShutdown(queue) - * console.log(isShutdown) // false * * yield* TxQueue.shutdown(queue) * const nowShutdown = yield* TxQueue.isShutdown(queue) - * console.log(nowShutdown) // true + * return [isShutdown, nowShutdown] as const * }) + * + * await Effect.runPromise(program) // => [false, true] * ``` * - * @category combinators + * @category predicates * @since 2.0.0 */ export const isShutdown = (self: TxQueueState): Effect.Effect => isDone(self) @@ -1457,19 +1475,20 @@ export const isShutdown = (self: TxQueueState): Effect.Effect => isDone * * **Example** (Awaiting queue completion) * - * ```ts - * import { Effect, TxQueue } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber, TxQueue } from "effect" * * const program = Effect.gen(function*() { * const queue = yield* TxQueue.bounded(10) * - * // In another fiber, end the queue - * yield* Effect.forkChild(Effect.delay(TxQueue.interrupt(queue), "100 millis")) + * const waiter = yield* Effect.forkChild(TxQueue.awaitCompletion(queue)) + * yield* TxQueue.interrupt(queue) * - * // Wait for completion - succeeds when queue ends - * yield* TxQueue.awaitCompletion(queue) - * console.log("Queue completed successfully") + * yield* Fiber.join(waiter) + * return "Queue completed successfully" * }) + * + * await Effect.runPromise(program) // => "Queue completed successfully" * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/TxReentrantLock.ts b/.context/effect/packages/effect/src/TxReentrantLock.ts index f4dfc2e90..0e0f90ec6 100644 --- a/.context/effect/packages/effect/src/TxReentrantLock.ts +++ b/.context/effect/packages/effect/src/TxReentrantLock.ts @@ -44,18 +44,21 @@ const emptyState: LockState = { * * **Example** (Using read and write locks) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * * // Multiple readers can proceed concurrently - * yield* TxReentrantLock.withReadLock(lock, Effect.succeed("reading")) + * const read = yield* TxReentrantLock.withReadLock(lock, Effect.succeed("reading")) * * // Writer gets exclusive access - * yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("writing")) + * const write = yield* TxReentrantLock.withWriteLock(lock, Effect.succeed("writing")) + * return [read, write] * }) + * + * await Effect.runPromise(program) // => ["reading", "writing"] * ``` * * @category models @@ -91,14 +94,15 @@ const TxReentrantLockProto: Omit = * * **Example** (Creating a reentrant lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const isLocked = yield* TxReentrantLock.locked(lock) - * console.log(isLocked) // false + * return yield* TxReentrantLock.locked(lock) * }) + * + * await Effect.runPromise(program) // => false * ``` * * @category constructors @@ -124,15 +128,17 @@ export const make = (): Effect.Effect => * * **Example** (Acquiring a read lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * const count = yield* TxReentrantLock.acquireRead(lock) - * console.log(count) // 1 * yield* TxReentrantLock.releaseRead(lock) + * return count * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category mutations @@ -178,15 +184,17 @@ export const acquireRead = (self: TxReentrantLock): Effect.Effect => * * **Example** (Acquiring a write lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * const count = yield* TxReentrantLock.acquireWrite(lock) - * console.log(count) // 1 * yield* TxReentrantLock.releaseWrite(lock) + * return count * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category mutations @@ -243,38 +251,39 @@ export const acquireWrite = (self: TxReentrantLock): Effect.Effect => * * **Example** (Releasing a read lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * yield* TxReentrantLock.acquireRead(lock) - * const remaining = yield* TxReentrantLock.releaseRead(lock) - * console.log(remaining) // 0 + * return yield* TxReentrantLock.releaseRead(lock) * }) + * + * await Effect.runPromise(program) // => 0 * ``` * * @category mutations * @since 2.0.0 */ -export const releaseRead = (self: TxReentrantLock): Effect.Effect => - Effect.withFiber((fiber) => - Effect.gen(function*() { - const state = yield* TxRef.get(self.stateRef) - const fiberId = fiber.id - const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0) +const releaseReadFor = (self: TxReentrantLock, fiberId: number): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) + const currentCount = Option.getOrElse(HashMap.get(state.readers, fiberId), () => 0) - if (currentCount <= 0) return 0 + if (currentCount <= 0) return 0 - const newCount = currentCount - 1 - const newReaders = newCount === 0 - ? HashMap.remove(state.readers, fiberId) - : HashMap.set(state.readers, fiberId, newCount) + const newCount = currentCount - 1 + const newReaders = newCount === 0 + ? HashMap.remove(state.readers, fiberId) + : HashMap.set(state.readers, fiberId, newCount) - yield* TxRef.set(self.stateRef, { ...state, readers: newReaders }) - return newCount - }).pipe(Effect.tx) - ) + yield* TxRef.set(self.stateRef, { ...state, readers: newReaders }) + return newCount + }).pipe(Effect.tx) + +export const releaseRead = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => releaseReadFor(self, fiber.id)) /** * Releases one write lock held by the current fiber. @@ -289,37 +298,38 @@ export const releaseRead = (self: TxReentrantLock): Effect.Effect => * * **Example** (Releasing a write lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * yield* TxReentrantLock.acquireWrite(lock) - * const remaining = yield* TxReentrantLock.releaseWrite(lock) - * console.log(remaining) // 0 + * return yield* TxReentrantLock.releaseWrite(lock) * }) + * + * await Effect.runPromise(program) // => 0 * ``` * * @category mutations * @since 2.0.0 */ -export const releaseWrite = (self: TxReentrantLock): Effect.Effect => - Effect.withFiber((fiber) => - Effect.gen(function*() { - const state = yield* TxRef.get(self.stateRef) - const fiberId = fiber.id +const releaseWriteFor = (self: TxReentrantLock, fiberId: number): Effect.Effect => + Effect.gen(function*() { + const state = yield* TxRef.get(self.stateRef) - if (Option.isNone(state.writer) || state.writer.value[0] !== fiberId) return 0 + if (Option.isNone(state.writer) || state.writer.value[0] !== fiberId) return 0 - const newCount = state.writer.value[1] - 1 - const newWriter = newCount <= 0 - ? Option.none() - : Option.some([fiberId, newCount] as const) + const newCount = state.writer.value[1] - 1 + const newWriter = newCount <= 0 + ? Option.none() + : Option.some([fiberId, newCount] as const) - yield* TxRef.set(self.stateRef, { ...state, writer: newWriter }) - return newCount - }).pipe(Effect.tx) - ) + yield* TxRef.set(self.stateRef, { ...state, writer: newWriter }) + return newCount + }).pipe(Effect.tx) + +export const releaseWrite = (self: TxReentrantLock): Effect.Effect => + Effect.withFiber((fiber) => releaseWriteFor(self, fiber.id)) /** * Acquires a read lock for the duration of the scope. @@ -327,29 +337,35 @@ export const releaseWrite = (self: TxReentrantLock): Effect.Effect => * * **Example** (Holding a scoped read lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * - * yield* Effect.scoped( + * const held = yield* Effect.scoped( * Effect.gen(function*() { * yield* TxReentrantLock.readLock(lock) * // read lock is held for the duration of the scope + * return yield* TxReentrantLock.readLocks(lock) * }) * ) * // read lock is released + * return [held, yield* TxReentrantLock.readLocks(lock)] * }) + * + * await Effect.runPromise(program) // => [1, 0] * ``` * * @category mutations * @since 2.0.0 */ export const readLock = (self: TxReentrantLock): Effect.Effect => - Effect.acquireRelease( - acquireRead(self), - () => releaseRead(self) + Effect.withFiber((fiber) => + Effect.acquireRelease( + acquireRead(self), + () => releaseReadFor(self, fiber.id) + ) ) /** @@ -358,29 +374,35 @@ export const readLock = (self: TxReentrantLock): Effect.Effect [1, 0] * ``` * * @category mutations * @since 2.0.0 */ export const writeLock = (self: TxReentrantLock): Effect.Effect => - Effect.acquireRelease( - acquireWrite(self), - () => releaseWrite(self) + Effect.withFiber((fiber) => + Effect.acquireRelease( + acquireWrite(self), + () => releaseWriteFor(self, fiber.id) + ) ) /** @@ -389,17 +411,18 @@ export const writeLock = (self: TxReentrantLock): Effect.Effect "read data" * ``` * * @category mutations @@ -432,17 +455,18 @@ export const withReadLock: { * * **Example** (Running an effect with a write lock) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const result = yield* TxReentrantLock.withWriteLock( + * return yield* TxReentrantLock.withWriteLock( * lock, * Effect.succeed("wrote data") * ) - * console.log(result) // "wrote data" * }) + * + * await Effect.runPromise(program) // => "wrote data" * ``` * * @category mutations @@ -479,17 +503,18 @@ export const withWriteLock: { * * **Example** (Running an effect with exclusive access) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const result = yield* TxReentrantLock.withLock( + * return yield* TxReentrantLock.withLock( * lock, * Effect.succeed("exclusive operation") * ) - * console.log(result) // "exclusive operation" * }) + * + * await Effect.runPromise(program) // => "exclusive operation" * ``` * * @category mutations @@ -509,16 +534,18 @@ export const withLock: { * * **Example** (Counting read locks) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() * yield* TxReentrantLock.acquireRead(lock) * const count = yield* TxReentrantLock.readLocks(lock) - * console.log(count) // 1 * yield* TxReentrantLock.releaseRead(lock) + * return count * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category getters @@ -539,14 +566,15 @@ export const readLocks = (self: TxReentrantLock): Effect.Effect => * * **Example** (Counting write locks) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const count = yield* TxReentrantLock.writeLocks(lock) - * console.log(count) // 0 + * return yield* TxReentrantLock.writeLocks(lock) * }) + * + * await Effect.runPromise(program) // => 0 * ``` * * @category getters @@ -563,14 +591,15 @@ export const writeLocks = (self: TxReentrantLock): Effect.Effect => * * **Example** (Checking whether a lock is held) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const isLocked = yield* TxReentrantLock.locked(lock) - * console.log(isLocked) // false + * return yield* TxReentrantLock.locked(lock) * }) + * + * await Effect.runPromise(program) // => false * ``` * * @category getters @@ -587,14 +616,15 @@ export const locked = (self: TxReentrantLock): Effect.Effect => * * **Example** (Checking whether a read lock is held) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const isReadLocked = yield* TxReentrantLock.readLocked(lock) - * console.log(isReadLocked) // false + * return yield* TxReentrantLock.readLocked(lock) * }) + * + * await Effect.runPromise(program) // => false * ``` * * @category getters @@ -611,14 +641,15 @@ export const readLocked = (self: TxReentrantLock): Effect.Effect => * * **Example** (Checking whether a write lock is held) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxReentrantLock } from "effect" * * const program = Effect.gen(function*() { * const lock = yield* TxReentrantLock.make() - * const isWriteLocked = yield* TxReentrantLock.writeLocked(lock) - * console.log(isWriteLocked) // false + * return yield* TxReentrantLock.writeLocked(lock) * }) + * + * await Effect.runPromise(program) // => false * ``` * * @category getters @@ -639,14 +670,12 @@ export const writeLocked = (self: TxReentrantLock): Effect.Effect => * * **Example** (Checking for TxReentrantLock values) * - * ```ts + * ```ts import.meta.vitest * import { TxReentrantLock } from "effect" * - * declare const someValue: unknown + * const someValue: unknown = {} * - * if (TxReentrantLock.isTxReentrantLock(someValue)) { - * console.log("This is a TxReentrantLock") - * } + * TxReentrantLock.isTxReentrantLock(someValue) // => false * ``` * * @category guards diff --git a/.context/effect/packages/effect/src/TxRef.ts b/.context/effect/packages/effect/src/TxRef.ts index 94e7891f6..8a2364fe9 100644 --- a/.context/effect/packages/effect/src/TxRef.ts +++ b/.context/effect/packages/effect/src/TxRef.ts @@ -36,7 +36,7 @@ const TypeId = "~effect/transactions/TxRef" * * **Example** (Using a transactional reference) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -49,9 +49,10 @@ const TypeId = "~effect/transactions/TxRef" * yield* TxRef.set(ref, current + 1) * })) * - * const final = yield* TxRef.get(ref) - * console.log(final) // 1 + * return yield* TxRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 1 * ``` * * @category models @@ -74,7 +75,7 @@ export interface TxRef extends Pipeable { * * **Example** (Creating transactional references) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -88,9 +89,10 @@ export interface TxRef extends Pipeable { * yield* TxRef.set(name, "Bob") * })) * - * console.log(yield* TxRef.get(counter)) // 42 - * console.log(yield* TxRef.get(name)) // "Bob" + * return [yield* TxRef.get(counter), yield* TxRef.get(name)] * }) + * + * await Effect.runPromise(program) // => [42, "Bob"] * ``` * * @category constructors @@ -108,7 +110,7 @@ export const make = (initial: A) => Effect.sync(() => makeUnsafe(initial)) * * **Example** (Creating transactional references unsafely) * - * ```ts + * ```ts import.meta.vitest * import { TxRef } from "effect" * * // Create a TxRef synchronously (unsafe - use make instead in Effect contexts) @@ -116,8 +118,8 @@ export const make = (initial: A) => Effect.sync(() => makeUnsafe(initial)) * const config = TxRef.makeUnsafe({ timeout: 5000, retries: 3 }) * * // These are now ready to use in transactions - * console.log(counter.value) // 0 - * console.log(config.value) // { timeout: 5000, retries: 3 } + * counter.value // => 0 + * config.value // => { timeout: 5000, retries: 3 } * ``` * * @category constructors @@ -143,7 +145,7 @@ export const makeUnsafe = (initial: A): TxRef => ({ * * **Example** (Modifying transactional references) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -152,9 +154,10 @@ export const makeUnsafe = (initial: A): TxRef => ({ * // Modify and return both old and new value * const result = yield* TxRef.modify(counter, (current) => [current * 2, current + 1]) * - * console.log(result) // 0 (the return value: current * 2) - * console.log(yield* TxRef.get(counter)) // 1 (the new value: current + 1) + * return [result, yield* TxRef.get(counter)] * }) + * + * await Effect.runPromise(program) // => [0, 1] * ``` * * @category combinators @@ -191,7 +194,7 @@ export const modify: { * * **Example** (Updating transactional references) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -202,8 +205,10 @@ export const modify: { * TxRef.update(counter, (current) => current * 2) * ) * - * console.log(yield* TxRef.get(counter)) // 20 + * return yield* TxRef.get(counter) * }) + * + * await Effect.runPromise(program) // => 20 * ``` * * @category combinators @@ -226,7 +231,7 @@ export const update: { * * **Example** (Reading transactional references) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -237,8 +242,10 @@ export const update: { * TxRef.get(counter) * ) * - * console.log(value) // 42 + * return value * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @category combinators @@ -255,7 +262,7 @@ export const get = (self: TxRef): Effect.Effect => modify(self, (curren * * **Example** (Setting transactional references) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxRef } from "effect" * * const program = Effect.gen(function*() { @@ -266,8 +273,10 @@ export const get = (self: TxRef): Effect.Effect => modify(self, (curren * TxRef.set(counter, 100) * ) * - * console.log(yield* TxRef.get(counter)) // 100 + * return yield* TxRef.get(counter) * }) + * + * await Effect.runPromise(program) // => 100 * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/TxSemaphore.ts b/.context/effect/packages/effect/src/TxSemaphore.ts index 7886e2d78..65f3b4819 100644 --- a/.context/effect/packages/effect/src/TxSemaphore.ts +++ b/.context/effect/packages/effect/src/TxSemaphore.ts @@ -34,7 +34,7 @@ const TypeId = "~effect/transactions/TxSemaphore" * * **Example** (Managing permits transactionally) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSemaphore } from "effect" * * // Create a semaphore with 3 permits for managing concurrent database connections @@ -43,14 +43,17 @@ const TypeId = "~effect/transactions/TxSemaphore" * * // Acquire a permit before accessing the database * yield* TxSemaphore.acquire(dbSemaphore) - * console.log("Database connection acquired") + * const acquired = yield* TxSemaphore.available(dbSemaphore) * * // Perform database operations... * * // Release the permit when done * yield* TxSemaphore.release(dbSemaphore) - * console.log("Database connection released") + * const released = yield* TxSemaphore.available(dbSemaphore) + * return [acquired, released] as const * }) + * + * await Effect.runPromise(program) // => [2, 3] * ``` * * @see {@link make} for creating a transactional semaphore @@ -98,8 +101,8 @@ const makeTxSemaphore = (permitsRef: TxRef.TxRef, capacity: number): TxS * * **Example** (Creating a semaphore) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * // Create a semaphore for managing concurrent access to a resource pool * const program = Effect.gen(function*() { @@ -109,12 +112,10 @@ const makeTxSemaphore = (permitsRef: TxRef.TxRef, capacity: number): TxS * // Check initial state * const available = yield* TxSemaphore.available(connectionSemaphore) * const capacity = yield* TxSemaphore.capacity(connectionSemaphore) - * - * yield* Console.log( - * `Created semaphore with ${capacity} permits, ${available} available` - * ) - * // Output: "Created semaphore with 3 permits, 3 available" + * return [capacity, available] as const * }) + * + * await Effect.runPromise(program) // => [3, 3] * ``` * * @see {@link available} for reading the current available permit count @@ -142,15 +143,14 @@ export const make = (permits: number): Effect.Effect => * * **Example** (Checking available permits) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(5) * * // Check available permits before acquiring * const before = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`Available permits: ${before}`) // 5 * * // Acquire some permits * yield* TxSemaphore.acquire(semaphore) @@ -158,8 +158,10 @@ export const make = (permits: number): Effect.Effect => * * // Check available permits after acquiring * const after = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`Available permits: ${after}`) // 3 + * return [before, after] as const * }) + * + * await Effect.runPromise(program) // => [5, 3] * ``` * * @see {@link capacity} for reading the fixed total permit count @@ -178,20 +180,21 @@ export const available = (self: TxSemaphore): Effect.Effect => TxRef.get * * **Example** (Checking semaphore capacity) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(10) * * const capacity = yield* TxSemaphore.capacity(semaphore) - * yield* Console.log(`Semaphore capacity: ${capacity}`) // 10 * * // Capacity remains constant regardless of current permits * yield* TxSemaphore.acquire(semaphore) * const stillSame = yield* TxSemaphore.capacity(semaphore) - * yield* Console.log(`Capacity after acquire: ${stillSame}`) // 10 + * return [capacity, stillSame] as const * }) + * + * await Effect.runPromise(program) // => [10, 10] * ``` * * @see {@link available} for reading the current available permit count @@ -212,23 +215,20 @@ export const capacity = (self: TxSemaphore): Effect.Effect => Effect.suc * * **Example** (Acquiring a permit) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(2) * - * yield* Console.log("Acquiring first permit...") * yield* TxSemaphore.acquire(semaphore) - * yield* Console.log("First permit acquired") * - * yield* Console.log("Acquiring second permit...") * yield* TxSemaphore.acquire(semaphore) - * yield* Console.log("Second permit acquired") * - * const available = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`Available permits: ${available}`) // 0 + * return yield* TxSemaphore.available(semaphore) * }) + * + * await Effect.runPromise(program) // => 0 * ``` * * @see {@link tryAcquire} for a non-blocking single-permit attempt @@ -267,19 +267,18 @@ export const acquire = (self: TxSemaphore): Effect.Effect => * * **Example** (Acquiring multiple permits) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(5) * - * yield* Console.log("Acquiring 3 permits...") * yield* TxSemaphore.acquireN(semaphore, 3) - * yield* Console.log("3 permits acquired") * - * const available = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`Available permits: ${available}`) // 2 + * return yield* TxSemaphore.available(semaphore) * }) + * + * await Effect.runPromise(program) // => 2 * ``` * * @see {@link tryAcquireN} for a non-blocking multi-permit attempt @@ -313,20 +312,21 @@ export const acquireN = (self: TxSemaphore, n: number): Effect.Effect => { * * **Example** (Trying to acquire a permit) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(1) * * // First try should succeed * const first = yield* TxSemaphore.tryAcquire(semaphore) - * yield* Console.log(`First try: ${first}`) // true * * // Second try should fail (no permits left) * const second = yield* TxSemaphore.tryAcquire(semaphore) - * yield* Console.log(`Second try: ${second}`) // false + * return [first, second] as const * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @see {@link acquire} for waiting until one permit is available @@ -355,20 +355,21 @@ export const tryAcquire = (self: TxSemaphore): Effect.Effect => * * **Example** (Trying to acquire multiple permits) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(3) * * // Try to acquire 2 permits (should succeed) * const first = yield* TxSemaphore.tryAcquireN(semaphore, 2) - * yield* Console.log(`First try (2 permits): ${first}`) // true * * // Try to acquire 2 more permits (should fail, only 1 left) * const second = yield* TxSemaphore.tryAcquireN(semaphore, 2) - * yield* Console.log(`Second try (2 permits): ${second}`) // false + * return [first, second] as const * }) + * + * await Effect.runPromise(program) // => [true, false] * ``` * * @see {@link acquireN} for waiting until all requested permits are available @@ -404,22 +405,23 @@ export const tryAcquireN = (self: TxSemaphore, n: number): Effect.Effect [1, 2] * ``` * * @see {@link acquire} for manually acquiring one permit @@ -448,22 +450,23 @@ export const release = (self: TxSemaphore): Effect.Effect => * * **Example** (Releasing multiple permits) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(5) * * // Acquire 3 permits * yield* TxSemaphore.acquireN(semaphore, 3) - * let available = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`After acquire: ${available}`) // 2 + * const afterAcquire = yield* TxSemaphore.available(semaphore) * * // Release 2 permits * yield* TxSemaphore.releaseN(semaphore, 2) - * available = yield* TxSemaphore.available(semaphore) - * yield* Console.log(`After release: ${available}`) // 4 + * const afterRelease = yield* TxSemaphore.available(semaphore) + * return [afterAcquire, afterRelease] as const * }) + * + * await Effect.runPromise(program) // => [2, 4] * ``` * * @see {@link acquireN} for manually acquiring multiple permits @@ -499,26 +502,30 @@ export const releaseN = (self: TxSemaphore, n: number): Effect.Effect => { * * **Example** (Running an effect with a permit) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(2) + * const events: Array = [] * * // Execute database operation with automatic permit management * const result = yield* TxSemaphore.withPermit( * semaphore, * Effect.gen(function*() { - * yield* Console.log("Permit acquired, accessing database...") - * yield* Effect.sleep("100 millis") // Simulate database work - * yield* Console.log("Database operation complete") + * events.push("permit acquired") + * yield* Effect.yieldNow + * events.push("operation complete") * return "query result" * }) * ) * - * yield* Console.log(`Result: ${result}`) * // Permit is automatically released here + * const available = yield* TxSemaphore.available(semaphore) + * return [events, result, available] as const * }) + * + * await Effect.runPromise(program) // => [["permit acquired", "operation complete"], "query result", 2] * ``` * * @see {@link withPermits} for automatically acquiring and releasing multiple permits @@ -570,26 +577,30 @@ export const withPermit: { * * **Example** (Running an effect with multiple permits) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(5) + * const events: Array = [] * * // Execute batch operation with 3 permits * const results = yield* TxSemaphore.withPermits( * semaphore, * 3, * Effect.gen(function*() { - * yield* Console.log("3 permits acquired, processing batch...") - * yield* Effect.sleep("200 millis") // Simulate batch processing + * events.push("3 permits acquired") + * yield* Effect.yieldNow * return ["result1", "result2", "result3"] * }) * ) * - * yield* Console.log(`Batch results: ${results.join(", ")}`) * // All 3 permits are automatically released here + * const available = yield* TxSemaphore.available(semaphore) + * return [events, results, available] as const * }) + * + * await Effect.runPromise(program) // => [["3 permits acquired"], ["result1", "result2", "result3"], 5] * ``` * * @see {@link withPermit} for automatically acquiring and releasing one permit @@ -636,28 +647,32 @@ export const withPermits: { * * **Example** (Acquiring a scoped permit) * - * ```ts - * import { Console, Effect, TxSemaphore } from "effect" + * ```ts import.meta.vitest + * import { Effect, TxSemaphore } from "effect" * * const program = Effect.gen(function*() { * const semaphore = yield* TxSemaphore.make(3) + * const events: Array = [] * * yield* Effect.scoped( * Effect.gen(function*() { * // Acquire permit for the duration of this scope * yield* TxSemaphore.withPermitScoped(semaphore) - * yield* Console.log("Permit acquired for scope") + * events.push("permit acquired for scope") * * // Do work within the scope - * yield* Effect.sleep("500 millis") - * yield* Console.log("Work completed") + * yield* Effect.yieldNow + * events.push("work completed") * * // Permit will be automatically released when scope closes * }) * ) * - * yield* Console.log("Scope closed, permit released") + * const available = yield* TxSemaphore.available(semaphore) + * return [events, available] as const * }) + * + * await Effect.runPromise(program) // => [["permit acquired for scope", "work completed"], 3] * ``` * * @see {@link withPermit} for acquiring one permit around a single effect @@ -681,22 +696,25 @@ export const withPermitScoped = (self: TxSemaphore): Effect.Effect [true, false, 5] * ``` * * @see {@link make} for creating a `TxSemaphore` diff --git a/.context/effect/packages/effect/src/TxSubscriptionRef.ts b/.context/effect/packages/effect/src/TxSubscriptionRef.ts index aa4a8942f..668c0de89 100644 --- a/.context/effect/packages/effect/src/TxSubscriptionRef.ts +++ b/.context/effect/packages/effect/src/TxSubscriptionRef.ts @@ -36,24 +36,25 @@ const TypeId = "~effect/transactions/TxSubscriptionRef" * * **Example** (Subscribing to transactional changes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(0) * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxSubscriptionRef.changes(ref) * const initial = yield* TxQueue.take(sub) - * console.log(initial) // 0 * * yield* TxSubscriptionRef.set(ref, 1) * const next = yield* TxQueue.take(sub) - * console.log(next) // 1 + * return [initial, next] * }) * ) * }) + * + * await Effect.runPromise(program) // => [0, 1] * ``` * * @see {@link make} for creating a transactional subscription reference @@ -100,14 +101,15 @@ const TxSubscriptionRefProto: Omit, typeof TypeId | "ref" * * **Example** (Creating a transactional subscription reference) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(42) - * const value = yield* TxSubscriptionRef.get(ref) - * console.log(value) // 42 + * return yield* TxSubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @see {@link changes} for subscribing to the created reference @@ -140,14 +142,15 @@ export const make = (value: A): Effect.Effect> => * * **Example** (Reading the current value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make("hello") - * const value = yield* TxSubscriptionRef.get(ref) - * console.log(value) // "hello" + * return yield* TxSubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => "hello" * ``` * * @see {@link changes} for reading the current value and subsequent updates @@ -172,15 +175,16 @@ export const get = (self: TxSubscriptionRef): Effect.Effect => TxRef.ge * * **Example** (Modifying and returning a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(10) * const result = yield* TxSubscriptionRef.modify(ref, (n) => [`was ${n}`, n + 1]) - * console.log(result) // "was 10" - * console.log(yield* TxSubscriptionRef.get(ref)) // 11 + * return [result, yield* TxSubscriptionRef.get(ref)] * }) + * + * await Effect.runPromise(program) // => ["was 10", 11] * ``` * * @see {@link update} for deriving the next value without a separate return value @@ -222,14 +226,16 @@ export const modify: { * * **Example** (Setting a new value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(0) * yield* TxSubscriptionRef.set(ref, 42) - * console.log(yield* TxSubscriptionRef.get(ref)) // 42 + * return yield* TxSubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 42 * ``` * * @see {@link update} for deriving the new value from the current value @@ -257,14 +263,16 @@ export const set: { * * **Example** (Updating a value) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(5) * yield* TxSubscriptionRef.update(ref, (n) => n * 2) - * console.log(yield* TxSubscriptionRef.get(ref)) // 10 + * return yield* TxSubscriptionRef.get(ref) * }) + * + * await Effect.runPromise(program) // => 10 * ``` * * @see {@link set} for replacing the value directly @@ -293,15 +301,16 @@ export const update: { * * **Example** (Getting and setting atomically) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make("a") * const old = yield* TxSubscriptionRef.getAndSet(ref, "b") - * console.log(old) // "a" - * console.log(yield* TxSubscriptionRef.get(ref)) // "b" + * return [old, yield* TxSubscriptionRef.get(ref)] * }) + * + * await Effect.runPromise(program) // => ["a", "b"] * ``` * * @see {@link set} for setting without returning the previous value @@ -329,15 +338,16 @@ export const getAndSet: { * * **Example** (Getting and updating atomically) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(1) * const old = yield* TxSubscriptionRef.getAndUpdate(ref, (n) => n + 10) - * console.log(old) // 1 - * console.log(yield* TxSubscriptionRef.get(ref)) // 11 + * return [old, yield* TxSubscriptionRef.get(ref)] * }) + * + * await Effect.runPromise(program) // => [1, 11] * ``` * * @see {@link update} for updating without returning the previous value @@ -366,14 +376,15 @@ export const getAndUpdate: { * * **Example** (Updating and reading atomically) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(3) - * const result = yield* TxSubscriptionRef.updateAndGet(ref, (n) => n * 3) - * console.log(result) // 9 + * return yield* TxSubscriptionRef.updateAndGet(ref, (n) => n * 3) * }) + * + * await Effect.runPromise(program) // => 9 * ``` * * @see {@link update} for updating without returning the new value @@ -409,24 +420,25 @@ export const updateAndGet: { * * **Example** (Subscribing to changes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, TxQueue, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { * const ref = yield* TxSubscriptionRef.make(0) * - * yield* Effect.scoped( + * return yield* Effect.scoped( * Effect.gen(function*() { * const sub = yield* TxSubscriptionRef.changes(ref) * const initial = yield* TxQueue.take(sub) - * console.log(initial) // 0 * * yield* TxSubscriptionRef.set(ref, 1) * const next = yield* TxQueue.take(sub) - * console.log(next) // 1 + * return [initial, next] * }) * ) * }) + * + * await Effect.runPromise(program) // => [0, 1] * ``` * * @see {@link changesStream} for subscribing through a `Stream` @@ -459,7 +471,7 @@ export const changes = ( * * **Example** (Streaming changes) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Stream, TxSubscriptionRef } from "effect" * * const program = Effect.gen(function*() { @@ -470,8 +482,10 @@ export const changes = ( * const values = yield* Stream.runCollect( * TxSubscriptionRef.changesStream(ref).pipe(Stream.take(1)) * ) - * console.log(values) // [2] + * return Array.from(values) * }) + * + * await Effect.runPromise(program) // => [2] * ``` * * @see {@link changes} for subscribing through a transactional queue @@ -500,14 +514,11 @@ export const changesStream = (self: TxSubscriptionRef): Stream.Stream false * ``` * * @see {@link make} for creating a `TxSubscriptionRef` diff --git a/.context/effect/packages/effect/src/Types.ts b/.context/effect/packages/effect/src/Types.ts index 94abd3a8f..a403f5c48 100644 --- a/.context/effect/packages/effect/src/Types.ts +++ b/.context/effect/packages/effect/src/Types.ts @@ -10,7 +10,7 @@ */ /** - * @category tuples + * @category utility types * @since 2.0.0 */ type TupleOf_> = `${N}` extends `-${number}` ? never @@ -33,7 +33,7 @@ type TupleOf_> = `${N}` extends `- * * **Example** (Checking fixed-length tuples) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * // Exactly 3 numbers @@ -48,7 +48,7 @@ type TupleOf_> = `${N}` extends `- * * @see {@link TupleOfAtLeast} * - * @category tuples + * @category utility types * @since 3.3.0 */ export type TupleOf = N extends N ? number extends N ? Array : TupleOf_ : never @@ -68,7 +68,7 @@ export type TupleOf = N extends N ? number extends N ? Arra * * **Example** (Checking minimum-length tuples) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * // At least 2 strings @@ -81,7 +81,7 @@ export type TupleOf = N extends N ? number extends N ? Arra * * @see {@link TupleOf} * - * @category tuples + * @category utility types * @since 3.3.0 */ export type TupleOfAtLeast = [...TupleOf, ...Array] @@ -99,7 +99,7 @@ export type TupleOfAtLeast = [...TupleOf, ...Array * * **Example** (Extracting tags) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type MyError = @@ -109,12 +109,14 @@ export type TupleOfAtLeast = [...TupleOf, ...Array * * type Result = Types.Tags * // "NotFound" | "Timeout" + * + * const witness: Result = "NotFound" * ``` * * @see {@link ExtractTag} * @see {@link ExcludeTag} * - * @category types + * @category utility types * @since 2.0.0 */ export type Tags = E extends { readonly _tag: string } ? E["_tag"] : never @@ -133,7 +135,7 @@ export type Tags = E extends { readonly _tag: string } ? E["_tag"] : never * * **Example** (Removing a variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type MyError = @@ -143,12 +145,14 @@ export type Tags = E extends { readonly _tag: string } ? E["_tag"] : never * * type WithoutTimeout = Types.ExcludeTag * // { readonly _tag: "NotFound"; readonly id: string } | string + * + * const witness: WithoutTimeout = { _tag: "NotFound", id: "1" } * ``` * * @see {@link ExtractTag} * @see {@link Tags} * - * @category types + * @category utility types * @since 2.0.0 */ export type ExcludeTag = Exclude @@ -167,7 +171,7 @@ export type ExcludeTag = Exclude * * **Example** (Extracting a variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type MyError = @@ -176,12 +180,14 @@ export type ExcludeTag = Exclude * * type TimeoutError = Types.ExtractTag * // { readonly _tag: "Timeout"; readonly ms: number } + * + * const witness: TimeoutError = { _tag: "Timeout", ms: 100 } * ``` * * @see {@link ExcludeTag} * @see {@link Tags} * - * @category types + * @category utility types * @since 2.0.0 */ export type ExtractTag = E extends { readonly _tag: infer T } ? K extends T ? E : never : never @@ -203,17 +209,19 @@ export type ExtractTag = E extends { readonly _tag: infer T * * **Example** (Converting a union to an intersection) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Union = { a: string } | { b: number } * type Result = Types.UnionToIntersection * // { a: string } & { b: number } + * + * const witness: Result = { a: "value", b: 1 } * ``` * * @see {@link IsUnion} * - * @category types + * @category utility types * @since 2.0.0 */ export type UnionToIntersection = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R @@ -233,18 +241,20 @@ export type UnionToIntersection = (T extends any ? (x: T) => any : never) ext * * **Example** (Simplifying an intersection) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * // Without Simplify: IDE shows { a: number } & { b: string } * // With Simplify: IDE shows { a: number; b: string } * type Clean = Types.Simplify<{ a: number } & { b: string }> + * + * const witness: Clean = { a: 1, b: "value" } * ``` * * @see {@link MergeLeft} * @see {@link MergeRight} * - * @category types + * @category utility types * @since 2.0.0 */ export type Simplify = { @@ -266,7 +276,7 @@ export type Simplify = { * * **Example** (Checking type equality) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Yes = Types.Equals<{ a: number }, { a: number }> // true @@ -276,7 +286,7 @@ export type Simplify = { * * @see {@link EqualsWith} * - * @category models + * @category utility types * @since 2.0.0 */ export type Equals = (() => T extends X ? 1 : 2) extends < @@ -297,7 +307,7 @@ export type Equals = (() => T extends X ? 1 : 2) extends < * * **Example** (Choosing a conditional type based on equality) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type R1 = Types.EqualsWith // "same" @@ -306,7 +316,7 @@ export type Equals = (() => T extends X ? 1 : 2) extends < * * @see {@link Equals} * - * @category models + * @category utility types * @since 3.15.0 */ export type EqualsWith = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? Y : N @@ -326,14 +336,14 @@ export type EqualsWith = (() => T extends A ? 1 : 2) extends ( * * **Example** (Checking key presence) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Yes = Types.Has<{ a: number; b: string }, "a" | "c"> // true * type No = Types.Has<{ a: number }, "b" | "c"> // false * ``` * - * @category models + * @category utility types * @since 2.0.0 */ export type Has = (Key extends infer K ? K extends keyof A ? true : never : never) extends never @@ -354,7 +364,7 @@ export type Has = (Key extends infer K ? K extends keyof * * **Example** (Merging with left bias) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Result = Types.MergeLeft< @@ -362,12 +372,14 @@ export type Has = (Key extends infer K ? K extends keyof * { a: string; c: boolean } * > * // { a: number; b: number; c: boolean } + * + * const witness: Result = { a: 1, b: 2, c: true } * ``` * * @see {@link MergeRight} * @see {@link Simplify} * - * @category models + * @category utility types * @since 2.0.0 */ export type MergeLeft = MergeRight @@ -386,7 +398,7 @@ export type MergeLeft = MergeRight * * **Example** (Right-biased merge) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Result = Types.MergeRight< @@ -394,12 +406,14 @@ export type MergeLeft = MergeRight * { a: string; c: boolean } * > * // { a: string; b: number; c: boolean } + * + * const witness: Result = { a: "value", b: 2, c: true } * ``` * * @see {@link MergeLeft} * @see {@link Simplify} * - * @category models + * @category utility types * @since 2.0.0 */ export type MergeRight = Simplify< @@ -421,23 +435,21 @@ export type MergeRight = Simplify< * * - `number` — run at most N effects concurrently. * - `"unbounded"` — run all effects concurrently with no limit. - * - `"inherit"` — inherit the concurrency from the surrounding context. * * **Example** (Setting concurrency values) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * const sequential: Types.Concurrency = 1 * const limited: Types.Concurrency = 5 * const unbounded: Types.Concurrency = "unbounded" - * const inherit: Types.Concurrency = "inherit" * ``` * * @category models * @since 2.0.0 */ -export type Concurrency = number | "unbounded" | "inherit" +export type Concurrency = number | "unbounded" /** * Removes `readonly` from all properties of `T`. Supports arrays, tuples, @@ -453,7 +465,7 @@ export type Concurrency = number | "unbounded" | "inherit" * * **Example** (Converting shallowly to mutable types) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Obj = Types.Mutable<{ @@ -468,11 +480,14 @@ export type Concurrency = number | "unbounded" | "inherit" * * type Tup = Types.Mutable * // [string, number] + * + * const tuple: Tup = ["value", 1] + * tuple[1] = 2 * ``` * * @see {@link DeepMutable} * - * @category types + * @category utility types * @since 2.0.0 */ export type Mutable = { @@ -494,7 +509,7 @@ export type Mutable = { * * **Example** (Converting deeply to mutable types) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Deep = Types.DeepMutable<{ @@ -502,11 +517,14 @@ export type Mutable = { * readonly b: ReadonlyArray<{ readonly c: number }> * }> * // { a: string; b: Array<{ c: number }> } + * + * const witness: Deep = { a: "value", b: [{ c: 1 }] } + * witness.b[0].c = 2 * ``` * * @see {@link Mutable} * - * @category types + * @category utility types * @since 3.1.0 */ export type DeepMutable = T extends ReadonlyMap ? Map, DeepMutable> @@ -529,16 +547,18 @@ export type DeepMutable = T extends ReadonlyMap ? Map(value: T, fallback: Types.NoInfer): T + * function withDefault(value: T, _fallback: Types.NoInfer): T { + * return value + * } * * // T is inferred as "a" | "b" from the first argument only * const result = withDefault<"a" | "b">("a", "b") * ``` * - * @category models + * @category utility types * @since 2.0.0 */ export type NoInfer = [A][A extends any ? 0 : never] @@ -559,20 +579,22 @@ export type NoInfer = [A][A extends any ? 0 : never] * * **Example** (Defining an invariant phantom type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * interface Container { * readonly _phantom: Types.Invariant * readonly value: T * } + * + * const container: Container = { _phantom: (value) => value, value: 1 } * ``` * * @see {@link Invariant.Type} * @see {@link Covariant} * @see {@link Contravariant} * - * @category models + * @category utility types * @since 2.0.0 */ export type Invariant = (_: A) => A @@ -596,16 +618,18 @@ export declare namespace Invariant { * * **Example** (Extracting the inner type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Inner = Types.Invariant.Type> * // number + * + * const witness: Inner = 1 * ``` * * @see {@link Invariant} * - * @category models + * @category utility types * @since 3.9.0 */ export type Type = A extends Invariant ? U : never @@ -627,20 +651,22 @@ export declare namespace Invariant { * * **Example** (Defining a covariant phantom type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * interface Producer { * readonly _phantom: Types.Covariant * readonly get: () => T * } + * + * const producer: Producer = { _phantom: () => "value", get: () => "value" } * ``` * * @see {@link Covariant.Type} * @see {@link Contravariant} * @see {@link Invariant} * - * @category models + * @category utility types * @since 2.0.0 */ export type Covariant = (_: never) => A @@ -664,16 +690,18 @@ export declare namespace Covariant { * * **Example** (Extracting the inner type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Inner = Types.Covariant.Type> * // string + * + * const witness: Inner = "value" * ``` * * @see {@link Covariant} * - * @category models + * @category utility types * @since 3.9.0 */ export type Type = A extends Covariant ? U : never @@ -695,20 +723,25 @@ export declare namespace Covariant { * * **Example** (Defining a contravariant phantom type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * interface Consumer { * readonly _phantom: Types.Contravariant * readonly accept: (value: T) => void * } + * + * const consumer: Consumer = { + * _phantom: () => {}, + * accept: (_value) => {} + * } * ``` * * @see {@link Contravariant.Type} * @see {@link Covariant} * @see {@link Invariant} * - * @category models + * @category utility types * @since 2.0.0 */ export type Contravariant = (_: A) => void @@ -732,16 +765,18 @@ export declare namespace Contravariant { * * **Example** (Extracting the inner type) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Inner = Types.Contravariant.Type> * // string + * + * const witness: Inner = "value" * ``` * * @see {@link Contravariant} * - * @category models + * @category utility types * @since 3.9.0 */ export type Type = A extends Contravariant ? U : never @@ -755,7 +790,7 @@ export declare namespace Contravariant { * * Use to erase an empty object type from an API result or parameter position. * - * @category types + * @category utility types * @since 3.19.20 */ export type VoidIfEmpty = keyof S extends never ? void : S @@ -773,14 +808,16 @@ export type VoidIfEmpty = keyof S extends never ? void : S * * **Example** (Filtering out functions) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Result = Types.NotFunction void) | number> * // string | number + * + * const witness: Result = "value" * ``` * - * @category types + * @category utility types * @since 2.0.0 */ export type NotFunction = T extends Function ? never : T @@ -798,7 +835,7 @@ export type NotFunction = T extends Function ? never : T * * **Example** (Preventing extra properties) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type Expected = { a: number; b: string } @@ -806,9 +843,11 @@ export type NotFunction = T extends Function ? never : T * * type Result = Types.NoExcessProperties * // { a: number; b: string; readonly c: never } + * + * const accepted: Types.NoExcessProperties = { a: 1, b: "value" } * ``` * - * @category types + * @category utility types * @since 3.9.0 */ export type NoExcessProperties = T & Readonly, never>> @@ -828,7 +867,7 @@ export type NoExcessProperties = T & Readonly // true @@ -882,7 +921,7 @@ export interface unhandled { * * @see {@link UnionToIntersection} * - * @category types + * @category utility types * @since 4.0.0 */ export type IsUnion = [T] extends [UnionToIntersection] ? false : true @@ -901,7 +940,7 @@ export type IsUnion = [T] extends [UnionToIntersection] ? false : true * * **Example** (Extracting reason types) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -910,13 +949,15 @@ export type IsUnion = [T] extends [UnionToIntersection] ? false : true * * type Reasons = Types.ReasonOf * // RateLimitError | QuotaError + * + * const witness: Reasons = { _tag: "QuotaError", limit: 10 } * ``` * * @see {@link ReasonTags} * @see {@link ExtractReason} * @see {@link ExcludeReason} * - * @category types + * @category utility types * @since 4.0.0 */ export type ReasonOf = E extends { readonly reason: infer R } ? R : never @@ -936,7 +977,7 @@ export type ReasonOf = E extends { readonly reason: infer R } ? R : never * * **Example** (Getting reason tags) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -945,12 +986,14 @@ export type ReasonOf = E extends { readonly reason: infer R } ? R : never * * type Result = Types.ReasonTags * // "RateLimitError" | "QuotaError" + * + * const witness: Result = "RateLimitError" * ``` * * @see {@link ReasonOf} * @see {@link ExtractReason} * - * @category types + * @category utility types * @since 4.0.0 */ export type ReasonTags = E extends { readonly reason: { readonly _tag: string } } ? E["reason"]["_tag"] @@ -971,7 +1014,7 @@ export type ReasonTags = E extends { readonly reason: { readonly _tag: string * * **Example** (Extracting a reason variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -980,13 +1023,15 @@ export type ReasonTags = E extends { readonly reason: { readonly _tag: string * * type Result = Types.ExtractReason * // { readonly _tag: "RateLimitError"; readonly retryAfter: number } + * + * const witness: Result = { _tag: "RateLimitError", retryAfter: 30 } * ``` * * @see {@link ExcludeReason} * @see {@link ReasonOf} * @see {@link ReasonTags} * - * @category types + * @category utility types * @since 4.0.0 */ export type ExtractReason = E extends { readonly reason: infer R } @@ -1009,7 +1054,7 @@ export type ExtractReason = E extends { readonly reason: in * * **Example** (Narrowing a reason variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -1018,13 +1063,18 @@ export type ExtractReason = E extends { readonly reason: in * * type Result = Types.NarrowReason * // ApiError & { readonly reason: { readonly _tag: "RateLimitError"; readonly retryAfter: number } } + * + * const witness: Result = { + * _tag: "ApiError", + * reason: { _tag: "RateLimitError", retryAfter: 30 } + * } * ``` * * @see {@link ExcludeReason} * @see {@link ReasonOf} * @see {@link ReasonTags} * - * @category types + * @category utility types * @since 4.0.0 */ export type NarrowReason = E extends { readonly reason: infer R } @@ -1047,7 +1097,7 @@ export type NarrowReason = E extends { readonly reason: inf * * **Example** (Omitting a reason variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -1056,6 +1106,11 @@ export type NarrowReason = E extends { readonly reason: inf * * type Result = Types.OmitReason * // ApiError & { readonly reason: { readonly _tag: "QuotaError"; readonly limit: number } } + * + * const witness: Result = { + * _tag: "ApiError", + * reason: { _tag: "QuotaError", limit: 10 } + * } * ``` * * @see {@link NarrowReason} @@ -1063,7 +1118,7 @@ export type NarrowReason = E extends { readonly reason: inf * @see {@link ReasonOf} * @see {@link ReasonTags} * - * @category types + * @category utility types * @since 4.0.0 */ export type OmitReason = E extends { readonly reason: infer R } @@ -1086,7 +1141,7 @@ export type OmitReason = E extends { readonly reason: infer * * **Example** (Excluding a reason variant) * - * ```ts + * ```ts import.meta.vitest * import type { Types } from "effect" * * type RateLimitError = { readonly _tag: "RateLimitError"; readonly retryAfter: number } @@ -1095,13 +1150,15 @@ export type OmitReason = E extends { readonly reason: infer * * type Result = Types.ExcludeReason * // { readonly _tag: "QuotaError"; readonly limit: number } + * + * const witness: Result = { _tag: "QuotaError", limit: 10 } * ``` * * @see {@link ExtractReason} * @see {@link ReasonOf} * @see {@link ReasonTags} * - * @category types + * @category utility types * @since 4.0.0 */ export type ExcludeReason = E extends { readonly reason: infer R } @@ -1115,7 +1172,7 @@ export type ExcludeReason = E extends { readonly reason: in * * Use to derive the keys whose properties must be present on an object type. * - * @category types + * @category utility types * @since 4.0.0 */ export type RequiredKeys = { [K in keyof T]-?: {} extends Pick ? never : K }[keyof T] diff --git a/.context/effect/packages/effect/src/Unify.ts b/.context/effect/packages/effect/src/Unify.ts index 080b1bec9..a2e280985 100644 --- a/.context/effect/packages/effect/src/Unify.ts +++ b/.context/effect/packages/effect/src/Unify.ts @@ -178,7 +178,7 @@ type FilterOut = A extends any ? typeSymbol extends keyof A ? never : A : nev * * **Example** (Unifying protocol types) * - * ```ts + * ```ts import.meta.vitest * import type { Unify } from "effect" * * // Example of types that can be unified @@ -196,7 +196,8 @@ type FilterOut = A extends any ? typeSymbol extends keyof A ? never : A : nev * * // Unify automatically handles the union * type Unified = Unify.Unify - * // Results in a properly unified type + * + * const witness: Unified = "value" * ``` * * @see {@link unify} for applying this normalization to a value or function @@ -243,27 +244,24 @@ export type Unify = Values< * * **Example** (Unifying values and function results) * - * ```ts + * ```ts import.meta.vitest * import { Unify } from "effect" * * // Unify a simple value - * const unifiedValue = Unify.unify("hello") + * const unifiedValue = Unify.unify("hello") // => "hello" * // Type: string * * // Unify a function result - * const createUnifiableValue = () => ({ - * value: "test", - * [Unify.typeSymbol]: "string" as const, - * [Unify.unifySymbol]: { String: () => "test" as const } - * }) + * const createValue = () => ({ value: "test" }) * - * const unifiedFunction = Unify.unify(createUnifiableValue) - * // The result will be properly unified + * const unifiedFunction = Unify.unify(createValue) + * unifiedFunction().value // => "test" * * // Unify with curried functions * const curriedFunction = (a: string) => (b: number) => ({ result: a + b }) * const unifiedCurried = Unify.unify(curriedFunction) * // Type: (a: string) => (b: number) => Unify<{ result: string }> + * unifiedCurried("value-")(1).result // => "value-1" * ``` * * @see {@link Unify} for the type-level normalization applied by this helper diff --git a/.context/effect/packages/effect/src/Utils.ts b/.context/effect/packages/effect/src/Utils.ts index 0886e5f6d..448c79b16 100644 --- a/.context/effect/packages/effect/src/Utils.ts +++ b/.context/effect/packages/effect/src/Utils.ts @@ -31,18 +31,14 @@ import type * as Types from "./Types.ts" * * **Example** (Yielding a wrapped value in a generator) * - * ```ts + * ```ts import.meta.vitest * import { Utils } from "effect" * * const gen = new Utils.SingleShotGen("hello") * - * // First call yields the wrapped value - * console.log(gen.next(0)) - * // { value: "hello", done: false } + * gen.next(0) // => { value: "hello", done: false } * - * // Second call signals completion with the provided value - * console.log(gen.next(42)) - * // { value: 42, done: true } + * gen.next(42) // => { value: 42, done: true } * ``` * * @see {@link Gen} for the type-level signature that relies on `SingleShotGen` @@ -113,15 +109,21 @@ export class SingleShotGen implements IterableIterator { * * **Example** (Declaring variance for a TypeLambda) * - * ```ts + * ```ts import.meta.vitest * import type { Option, Utils } from "effect" * - * declare const variance: Utils.Variance< + * const variance: Utils.Variance< * Option.OptionTypeLambda, - * never, - * never, - * never - * > + * unknown, + * string, + * string + * > = { + * _F: (value) => value, + * _R: () => {}, + * _O: () => "output", + * _E: () => "error" + * } + * Array.of(variance._O(undefined as never), variance._E(undefined as never)) // => ["output", "error"] * ``` * * @see {@link Gen} for the type-level signature that uses `Variance` @@ -152,10 +154,15 @@ export interface Variance { * * **Example** (Typing a gen function for Option) * - * ```ts - * import type { Option, Utils } from "effect" + * ```ts import.meta.vitest + * import { Option } from "effect" + * import type { Utils } from "effect" * - * declare const gen: Utils.Gen + * const gen: Utils.Gen = Option.gen + * const result = gen(function*() { + * return yield* Option.some(1) + * }) + * result // => Option.some(1) * ``` * * @see {@link Variance} for encoding the variance used for inference diff --git a/.context/effect/packages/effect/src/index.ts b/.context/effect/packages/effect/src/index.ts index 8b2a9e35c..d1774a4d0 100644 --- a/.context/effect/packages/effect/src/index.ts +++ b/.context/effect/packages/effect/src/index.ts @@ -556,11 +556,6 @@ export * as SchemaRepresentation from "./SchemaRepresentation.ts" */ export * as SchemaTransformation from "./SchemaTransformation.ts" -/** - * @since 4.0.0 - */ -export * as SchemaUtils from "./SchemaUtils.ts" - /** * @since 2.0.0 */ diff --git a/.context/effect/packages/effect/src/internal/core.ts b/.context/effect/packages/effect/src/internal/core.ts index de598d12e..021d72dd1 100644 --- a/.context/effect/packages/effect/src/internal/core.ts +++ b/.context/effect/packages/effect/src/internal/core.ts @@ -13,6 +13,7 @@ import type { StackFrame } from "../References.ts" import type * as Types from "../Types.ts" import { SingleShotGen } from "../Utils.ts" import type { FiberImpl } from "./effect.ts" +import * as InternalRecord from "./record.ts" /** @internal */ export const EffectTypeId = `~effect/Effect` as const @@ -238,7 +239,7 @@ export abstract class ReasonBase implements Cause.Cause.Reas } /** @internal */ -export const constEmptyAnnotations = new Map() +export const constEmptyAnnotations: ReadonlyMap = new Map() /** @internal */ export class Fail extends ReasonBase<"Fail"> implements Cause.Fail { @@ -470,12 +471,12 @@ export const makeExit = < ) => Primitive | Yield }): Fn => { const Proto = { - ...makePrimitiveProto(options), [ExitTypeId]: ExitTypeId, _tag: options.op, get [options.prop](): any { return (this as any)[args] }, + ...makePrimitiveProto(options), toString(this: any) { return `${options.op}(${format(this[args])})` }, @@ -587,10 +588,10 @@ export const Error: new = {}>( ) => Cause.YieldableError & Readonly = (function() { const plainArgsSymbol = Symbol.for("effect/Data/Error/plainArgs") return class Base extends YieldableError { - constructor(args: any) { + constructor(args: Record | undefined) { super(args?.message, args?.cause ? { cause: args.cause } : undefined) if (args) { - Object.assign(this, args) + InternalRecord.assignProperties(this, args) // @effect-diagnostics-next-line floatingEffect:off Object.defineProperty(this, plainArgsSymbol, { value: args, diff --git a/.context/effect/packages/effect/src/internal/dateTime.ts b/.context/effect/packages/effect/src/internal/dateTime.ts index 63a9c2cd8..650b1230b 100644 --- a/.context/effect/packages/effect/src/internal/dateTime.ts +++ b/.context/effect/packages/effect/src/internal/dateTime.ts @@ -221,7 +221,7 @@ export const makeUnsafe = (input: A): DateTim return fromDateUnsafe(input) as DateTime.DateTime.PreserveZone } else if (typeof input === "object") { if ("epochMilliseconds" in input) { - return makeUtc(input.epochMilliseconds) as DateTime.DateTime.PreserveZone + return fromDateUnsafe(new Date(input.epochMilliseconds)) as DateTime.DateTime.PreserveZone } const date = new Date(0) setPartsDate(date, input) @@ -600,6 +600,12 @@ export const zonedOffsetIso = (self: DateTime.Zoned): string => offsetToString(z /** @internal */ export const toEpochMillis = (self: DateTime.DateTime): number => self.epochMilliseconds +/** @internal */ +export const toEpochSeconds = (self: DateTime.DateTime): number => Math.floor(self.epochMilliseconds / 1000) + +/** @internal */ +export const fromEpochSeconds = (seconds: number): DateTime.Utc => makeUtc(seconds * 1000) + /** @internal */ export const removeTime = (self: DateTime.DateTime): DateTime.Utc => withDate(self, (date) => { diff --git a/.context/effect/packages/effect/src/internal/effect.ts b/.context/effect/packages/effect/src/internal/effect.ts index 56902a914..99c3632ff 100644 --- a/.context/effect/packages/effect/src/internal/effect.ts +++ b/.context/effect/packages/effect/src/internal/effect.ts @@ -83,8 +83,8 @@ import { } from "./core.ts" import * as doNotation from "./doNotation.ts" import * as InternalMetric from "./metric.ts" +import * as InternalRecord from "./record.ts" import { - CurrentConcurrency, CurrentErrorReporters, CurrentLogAnnotations, CurrentLogLevel, @@ -98,7 +98,6 @@ import { } from "./references.ts" import { getStackTraceLimit, setStackTraceLimit } from "./stackTraceLimit.ts" import { addSpanStackTrace, makeStackCleaner } from "./tracer.ts" -import { version } from "./version.ts" // ---------------------------------------------------------------------------- // Cause @@ -269,7 +268,7 @@ export const causeMap: { const failures = self.reasons.map((failure) => { if (isFailReason(failure)) { hasFail = true - return new Fail(f(failure.error)) + return new Fail(f(failure.error), failure.annotations) } return failure }) @@ -490,7 +489,7 @@ const renderErrorCause = (cause: Error, prefix: string) => { // ---------------------------------------------------------------------------- /** @internal */ -export const FiberTypeId = `~effect/Fiber/${version}` as const +export const FiberTypeId = "~effect/Fiber" as const const fiberVariance = { _A: identity, @@ -556,7 +555,7 @@ export class FiberImpl implements Fiber.Fiber { } getRef(ref: Context.Reference): X { - return Context.getReferenceUnsafe(this.context, ref) + return Context.get(this.context, ref) } addObserver(cb: (exit: Exit.Exit) => void): () => void { if (this._exit) { @@ -707,20 +706,26 @@ export class FiberImpl implements Fiber.Fiber { return pipeArguments(this, arguments) } setContext(context: Context.Context): void { + const previous = this.context this.context = context + // Every key cached below opts in to Context caching, so contexts related + // only by non-caching adds cannot have changed any of them + if (previous !== undefined && Context.hasSameCache(previous, context)) return const scheduler = this.getRef(Scheduler.Scheduler) if (scheduler !== this.currentScheduler) { this.currentScheduler = scheduler this._dispatcher = undefined } - this.currentSpan = context.mapUnsafe.get(Tracer.ParentSpanKey) + // The string-keyed lookups keep the Tracer key values (and the native + // tracer behind Tracer.Tracer's default) out of every bundle + this.currentSpan = Context.getOrUndefinedUnsafe(context, Tracer.ParentSpanKey) this.currentLogLevel = this.getRef(CurrentLogLevel) this.minimumLogLevel = this.getRef(MinimumLogLevel) - this.currentStackFrame = context.mapUnsafe.get(CurrentStackFrame.key) + this.currentStackFrame = this.getRef(CurrentStackFrame) this.maxOpsBeforeYield = this.getRef(Scheduler.MaxOpsBeforeYield) this.currentPreventYield = this.getRef(Scheduler.PreventSchedulerYield) - this.runtimeMetrics = context.mapUnsafe.get(InternalMetric.FiberRuntimeMetricsKey) - const currentTracer = context.mapUnsafe.get(Tracer.TracerKey) + this.runtimeMetrics = Context.getOrUndefinedUnsafe(context, InternalMetric.FiberRuntimeMetricsKey) + const currentTracer = Context.getOrUndefinedUnsafe(context, Tracer.TracerKey) this.currentTracerContext = currentTracer ? currentTracer["context"] : undefined } get currentSpanLocal(): Tracer.Span | undefined { @@ -817,7 +822,7 @@ export const fiberJoin = (self: Fiber.Fiber): Effect.Effect => /** @internal */ export const fiberJoinAll = >>(self: A): Effect.Effect< Arr.ReadonlyArray.With> ? _A : never>, - A extends Fiber.Fiber ? _E : never + A extends Iterable> ? _E : never > => callback((resume) => { const fibers = Array.from(self) @@ -2111,6 +2116,32 @@ export const updateService: { }) ) +/** @internal */ +export const updateServiceScoped = ( + service: Context.Key, + update: (value: A) => A, + options?: { + readonly reset?: ((original: A, updated: A, current: A) => A) | undefined + } | undefined +): Effect.Effect => + uninterruptible(withFiber((fiber) => { + const original = Context.getUnsafe(fiber.context, service) + const updated = update(original) + fiber.setContext(Context.add(fiber.context, service, updated)) + return scopeAddFinalizerExit(Context.getUnsafe(fiber.context, scopeTag), (_) => { + const current = Context.getUnsafe(fiber.context, service) + let next: A + if (options?.reset === undefined) { + if (current !== updated) return void_ + next = original + } else { + next = options.reset(original, updated, current) + } + fiber.setContext(Context.add(fiber.context, service, next)) + return void_ + }) + })) + /** @internal */ export const context = (): Effect.Effect> => getContext as any const getContext = withFiber((fiber) => succeed(fiber.context)) @@ -2222,17 +2253,6 @@ export const provideServiceEffect: { flatMap(acquire, (implementation) => provideService(self, service, implementation)) ) -/** @internal */ -export const withConcurrency: { - ( - concurrency: "unbounded" | number - ): (self: Effect.Effect) => Effect.Effect - ( - self: Effect.Effect, - concurrency: "unbounded" | number - ): Effect.Effect -} = provideService(CurrentConcurrency) - // ---------------------------------------------------------------------------- // zipping // ---------------------------------------------------------------------------- @@ -3733,8 +3753,8 @@ export const timed = ( self: Effect.Effect ): Effect.Effect<[duration: Duration.Duration, result: A], E, R> => clockWith((clock) => { - const start = clock.currentTimeNanosUnsafe() - return map(self, (a) => [Duration.nanos(clock.currentTimeNanosUnsafe() - start), a]) + const start = clock.monotonicTimeNanosUnsafe() + return map(self, (a) => [Duration.nanos(clock.monotonicTimeNanosUnsafe() - start), a]) }) // ---------------------------------------------------------------------------- @@ -4353,7 +4373,7 @@ export const all = < Object.entries(arg), ([key, effect]) => map(options?.mode === "result" ? result(effect) : effect, (value) => { - out[key] = value + InternalRecord.assignProperty(out, key, value) }), { discard: true, @@ -4615,10 +4635,8 @@ export const forEach: { readonly discard?: boolean | undefined } ): Effect.Effect => - withFiber((parent) => { - const concurrencyOption = options?.concurrency === "inherit" - ? parent.getRef(CurrentConcurrency) - : (options?.concurrency ?? 1) + suspend(() => { + const concurrencyOption = options?.concurrency ?? 1 const concurrency = concurrencyOption === "unbounded" ? Number.POSITIVE_INFINITY : Math.max(1, concurrencyOption) @@ -4667,7 +4685,6 @@ const forEachSequential = ( type IterateEagerOptions = { readonly concurrency?: number | undefined - readonly start?: number | undefined readonly end?: number | undefined readonly orderedStep?: boolean | undefined } @@ -4683,14 +4700,37 @@ const iterateEagerImpl = (options: { const onItem = options.onItem const step = options.step + const runSequential = ( + state: S, + items: ReadonlyArray, + index: number, + end: number + ): Effect.Effect | undefined => { + for (; index < end; index++) { + const item = items[index] + const effect = onItem(state, item, index) + if (!effectIsExit(effect)) { + return flatMap( + exit(effect), + (itemExit) => step(state, item, itemExit, index) ?? runSequential(state, items, index + 1, end) ?? void_ + ) + } + const terminal = step(state, item, effect, index) + if (terminal) return terminal._tag === "Failure" ? terminal : undefined + } + } + return ( state: S, items: ReadonlyArray, opts: IterateEagerOptions | undefined ): Effect.Effect | undefined => { - let index = opts?.start ?? 0 + let index = 0 const end = opts?.end ?? items.length const concurrency = opts?.concurrency ?? 1 + if (concurrency === 1) { + return runSequential(state, items, 0, end) + } const orderedStep = opts?.orderedStep === true && concurrency > 1 let done = false let parentFiber: Fiber.Fiber | undefined @@ -4737,14 +4777,6 @@ const iterateEagerImpl = (options: { terminal = runStep(item, eff, index) if (terminal) break - // Use flatMap for concurrency of 1 - } else if (concurrency === 1) { - return flatMap(exit(eff), (exit) => { - terminal = runStep(item, exit, index) - index++ - return terminal ?? go() ?? void_ - }) - // We have an effect, so enter "async" mode } else if (!parentFiber) { return callback((cb) => { @@ -5191,16 +5223,17 @@ export const forkUnsafe = ( daemon = false, uninterruptible: boolean | "inherit" = false ): FiberImpl => { - const interruptible = uninterruptible === "inherit" ? parent.interruptible : !uninterruptible - const child = new FiberImpl(parent.context, interruptible) + const parentRuntime = parent as FiberImpl + const interruptible = uninterruptible === "inherit" ? parentRuntime.interruptible : !uninterruptible + const child = new FiberImpl(parentRuntime.context, interruptible) if (immediate) { child.evaluate(effect as any) } else { - parent.currentDispatcher.scheduleTask(() => child.evaluate(effect as any), 0) + parentRuntime.currentDispatcher.scheduleTask(() => child.evaluate(effect as any), 0) } if (!daemon && !child._exit) { - parent.children().add(child) - child.addObserver(() => parent._children!.delete(child)) + parentRuntime.children().add(child) + child.addObserver(() => parentRuntime._children!.delete(child)) } return child } @@ -5489,7 +5522,7 @@ const succeedFalse = succeed(false) class Latch implements _Latch.Latch { waiters: Array<(_: Effect.Effect) => void> = [] - scheduled = false + scheduled: Array<(_: Effect.Effect) => void> | undefined = undefined private _isOpen: boolean constructor(isOpen: boolean) { @@ -5497,17 +5530,35 @@ class Latch implements _Latch.Latch { } private scheduleUnsafe(fiber: Fiber.Fiber) { - if (this.scheduled || this.waiters.length === 0) { + if (this.waiters.length === 0) { return succeedTrue } - this.scheduled = true - fiber.currentDispatcher.scheduleTask(this.flushWaiters, 0) + if (this.scheduled === undefined) { + this.scheduled = this.waiters + fiber.currentDispatcher.scheduleTask(this.flushScheduled, 0) + } else { + for (let i = 0; i < this.waiters.length; i++) { + this.scheduled.push(this.waiters[i]) + } + } + this.waiters = [] return succeedTrue } - private flushWaiters = () => { - this.scheduled = false + private flushScheduled = () => { + if (this.scheduled === undefined) return + const waiters = this.scheduled + this.scheduled = undefined + for (let i = 0; i < waiters.length; i++) { + waiters[i](exitVoid) + } + } + private flushWaiters() { + // swap both arrays out before any resume runs: a resumed waiter can + // reentrantly close the latch and register new waiters, which must not + // be drained by this flush const waiters = this.waiters this.waiters = [] + this.flushScheduled() for (let i = 0; i < waiters.length; i++) { waiters[i](exitVoid) } @@ -5531,9 +5582,14 @@ class Latch implements _Latch.Latch { } this.waiters.push(resume) return sync(() => { - const index = this.waiters.indexOf(resume) + let index = this.waiters.indexOf(resume) if (index !== -1) { this.waiters.splice(index, 1) + } else if (this.scheduled !== undefined) { + index = this.scheduled.indexOf(resume) + if (index !== -1) { + this.scheduled.splice(index, 1) + } } }) }) @@ -5806,11 +5862,8 @@ export const useSpan: { return withFiber((fiber) => { const span = makeSpanUnsafe(fiber, name, options) const clock = fiber.getRef(ClockRef) - return onExit(internalCall(() => evaluate(span)), (exit) => - sync(() => { - if (span.status._tag === "Ended") return - span.end(clock.currentTimeNanosUnsafe(), exit) - })) + const timingEnabled = fiber.getRef(TracerTimingEnabled) + return onExit(internalCall(() => evaluate(span)), (exit) => endSpan(span, exit, clock, timingEnabled)) }) } @@ -5885,11 +5938,11 @@ export const annotateSpans: { ...args: [Record] | [key: string, value: unknown] ): Effect.Effect => updateService(effect, TracerSpanAnnotations, (annotations) => { - const newAnnotations = { ...annotations } + const newAnnotations = args.length === 1 ? { ...annotations, ...args[0] } : { ...annotations } if (args.length === 1) { - Object.assign(newAnnotations, args[0]) + return newAnnotations } else { - newAnnotations[args[0]] = args[1] + InternalRecord.assignProperty(newAnnotations, args[0], args[1]) } return newAnnotations }) @@ -5942,9 +5995,13 @@ class ClockImpl implements Clock.Clock { } readonly currentTimeMillis: Effect.Effect = sync(() => this.currentTimeMillisUnsafe()) currentTimeNanosUnsafe(): bigint { - return processOrPerformanceNow() + return wallTimeNanos() } readonly currentTimeNanos: Effect.Effect = sync(() => this.currentTimeNanosUnsafe()) + monotonicTimeNanosUnsafe(): bigint { + return monotonicNowNanos() + } + readonly monotonicTimeNanos: Effect.Effect = sync(() => this.monotonicTimeNanosUnsafe()) sleep(duration: Duration.Duration): Effect.Effect { return this.sleepMillis(Duration.toMillis(duration)) } @@ -5961,27 +6018,45 @@ class ClockImpl implements Clock.Clock { } } -const performanceNowNanos = (function() { - const bigint1e6 = BigInt(1_000_000) - if (typeof performance === "undefined" || typeof performance.now === "undefined") { - return () => BigInt(Date.now()) * bigint1e6 +const nanosPerMilli = BigInt(1_000_000) + +const monotonicNowNanos = (function() { + const processHrtime = (globalThis as { + readonly process?: { readonly hrtime?: { readonly bigint?: () => bigint } } + }).process?.hrtime + if (typeof processHrtime?.bigint === "function") { + return () => processHrtime.bigint!() } - let origin: bigint + if (typeof performance !== "undefined" && typeof performance.now === "function") { + return () => BigInt(Math.round(performance.now() * 1_000_000)) + } + let previous = BigInt(0) return () => { - origin ??= (BigInt(Date.now()) * bigint1e6) - BigInt(Math.round(performance.now() * 1_000_000)) - return origin + BigInt(Math.round(performance.now() * 1_000_000)) + const current = BigInt(Date.now()) * nanosPerMilli + if (current > previous) { + previous = current + } + return previous } })() -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 = (BigInt(Date.now()) * BigInt(1e6)) - processHrtime.bigint() - return () => origin + processHrtime.bigint() + +const wallTimeNanos = (function() { + const reanchorThresholdNanos = BigInt(1_000_000_000) + let origin: bigint | undefined + return () => { + const monotonic = monotonicNowNanos() + const wall = BigInt(Date.now()) * nanosPerMilli + if (origin === undefined) { + origin = wall - monotonic + } else { + const projected = origin + monotonic + const skew = wall > projected ? wall - projected : projected - wall + if (skew > reanchorThresholdNanos) { + origin = wall - monotonic + } + } + return origin + monotonic + } })() /** @internal */ @@ -5998,6 +6073,9 @@ export const currentTimeMillis: Effect.Effect = clockWith((clock) => clo /** @internal */ export const currentTimeNanos: Effect.Effect = clockWith((clock) => clock.currentTimeNanos) +/** @internal */ +export const monotonicTimeNanos: Effect.Effect = clockWith((clock) => clock.monotonicTimeNanos) + // ---------------------------------------------------------------------------- // Errors // ---------------------------------------------------------------------------- @@ -6160,7 +6238,7 @@ export const annotateLogsScoped: { const next = { ...prev } for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i] - next[key] = value + InternalRecord.assignProperty(next, key, value) } fiber.setContext(Context.add(fiber.context, CurrentLogAnnotations, next)) return scopeAddFinalizerExit(Context.getUnsafe(fiber.context, scopeTag), (_) => { @@ -6169,8 +6247,8 @@ export const annotateLogsScoped: { for (let i = 0; i < entries.length; i++) { const [key, value] = entries[i] if (current[key] !== value) continue - if (key in prev) { - next[key] = prev[key] + if (Object.hasOwn(prev, key)) { + InternalRecord.assignProperty(next, key, prev[key]) } else { delete next[key] } @@ -6224,7 +6302,6 @@ export const formatLogSpan = (self: [label: string, timestamp: number], now: num /** @internal */ export const structuredMessage = (u: unknown): unknown => { switch (typeof u) { - case "bigint": case "function": case "symbol": { return String(u) @@ -6334,10 +6411,10 @@ export const consolePretty = (options?: { }) => { // evaluated lazily so the module-level bundle stays free of `process` // property accesses, which bundlers must retain as possible side effects - const hasProcessStdout = typeof process === "object" && - process !== null && - typeof process.stdout === "object" && - process.stdout !== null + const process = (globalThis as { + readonly process?: { readonly stdout?: { readonly isTTY?: boolean } } + }).process + const hasProcessStdout = typeof process?.stdout === "object" && process.stdout !== null const processStdoutIsTTY = hasProcessStdout && process.stdout.isTTY === true const hasProcessStdoutOrDeno = hasProcessStdout || "Deno" in globalThis @@ -6355,7 +6432,7 @@ const prettyLoggerTty = (options: { readonly colors: boolean readonly formatDate: (date: Date) => string }) => { - const processIsBun = typeof process === "object" && "isBun" in process && process.isBun === true + const processIsBun = (globalThis as { readonly process?: { readonly isBun?: boolean } }).process?.isBun === true const color = options.colors ? withColor : withColorNoop return loggerMake( ({ cause, date, fiber, logLevel, message: message_ }) => { @@ -6514,7 +6591,7 @@ export const tracerLogger = loggerMake(({ cause, fiber, logLevel, if (span === undefined || span._tag === "ExternalSpan") return const attributes: Record = {} for (const [key, value] of Object.entries(annotations)) { - attributes[key] = value + InternalRecord.assignProperty(attributes, key, value) } attributes["effect.fiberId"] = fiber.id attributes["effect.logLevel"] = logLevel.toUpperCase() diff --git a/.context/effect/packages/effect/src/internal/executionPlan.ts b/.context/effect/packages/effect/src/internal/executionPlan.ts index 2ae8498b5..fb8cd6c16 100644 --- a/.context/effect/packages/effect/src/internal/executionPlan.ts +++ b/.context/effect/packages/effect/src/internal/executionPlan.ts @@ -1,47 +1,132 @@ +import * as Duration from "../Duration.ts" import type { Effect } from "../Effect.ts" import * as Api from "../ExecutionPlan.ts" -import { dual } from "../Function.ts" +import type * as Exit from "../Exit.ts" +import { dual, identity } from "../Function.ts" import * as Result from "../Result.ts" import * as Schedule from "../Schedule.ts" +import { isEffect } from "./core.ts" import * as effect from "./effect.ts" import * as internalLayer from "./layer.ts" import * as internalSchedule from "./schedule.ts" +/** @internal */ +export interface AttemptState { + readonly attempt: number + readonly stepAttempt: number + readonly stepIndex: number + readonly startNanos: bigint +} + +/** @internal */ +export interface EventEmitter { + readonly begin: Effect + readonly end: (state: AttemptState, exit: Exit.Exit) => Effect +} + +/** @internal */ +export const makeEventEmitter = ( + onEvent: (event: Api.Event) => Effect, + currentMetadata: () => Api.Metadata +): EventEmitter => { + let lastStepIndex = -1 + let stepAttempt = 0 + const emit = (event: Api.Event) => effect.ignoreCause(onEvent(event)) + return { + begin: effect.clockWith((clock) => + effect.suspend(() => { + const meta = currentMetadata() + if (meta.stepIndex !== lastStepIndex) { + lastStepIndex = meta.stepIndex + stepAttempt = 0 + } + stepAttempt++ + const state: AttemptState = { + attempt: meta.attempt, + stepAttempt, + stepIndex: meta.stepIndex, + startNanos: clock.monotonicTimeNanosUnsafe() + } + return effect.as( + emit({ + _tag: "AttemptStart", + attempt: state.attempt, + stepAttempt: state.stepAttempt, + stepIndex: state.stepIndex + }), + state + ) + }) + ), + end: (state, exit) => + effect.clockWith((clock) => { + const duration = Duration.nanos(clock.monotonicTimeNanosUnsafe() - state.startNanos) + return emit( + exit._tag === "Success" + ? { + _tag: "AttemptSuccess", + attempt: state.attempt, + stepAttempt: state.stepAttempt, + stepIndex: state.stepIndex, + duration + } + : { + _tag: "AttemptFailure", + attempt: state.attempt, + stepAttempt: state.stepAttempt, + stepIndex: state.stepIndex, + duration, + cause: exit.cause + } + ) + }) + } +} + /** @internal */ export const withExecutionPlan: { - ( + ( plan: Api.ExecutionPlan<{ provides: Provides input: Input error: PlanE requirements: PlanR - }> + }>, + options?: { + readonly onEvent?: ((event: Api.Event) => Effect) | undefined + } ): (effect: Effect) => Effect< A, E | PlanE, - Exclude | PlanR + Exclude | PlanR | RX > - ( + ( effect: Effect, plan: Api.ExecutionPlan<{ provides: Provides input: Input error: PlanE requirements: PlanR - }> + }>, + options?: { + readonly onEvent?: ((event: Api.Event) => Effect) | undefined + } ): Effect< A, E | PlanE, - Exclude | PlanR + Exclude | PlanR | RX > -} = dual(2, ( +} = dual((args) => isEffect(args[0]), ( self: Effect, plan: Api.ExecutionPlan<{ provides: Provides input: Input error: PlanE requirements: PlanR - }> + }>, + options?: { + readonly onEvent?: ((event: Api.Event) => Effect) | undefined + } ) => effect.suspend(() => { let i = 0 @@ -59,13 +144,24 @@ export const withExecutionPlan: { return meta }) ) + const emitter = options?.onEvent === undefined + ? undefined + : makeEventEmitter(options.onEvent, () => meta) + const instrument: (attempt: Effect) => Effect = emitter === undefined + ? identity + : (attempt) => + effect.uninterruptibleMask((restore) => + effect.flatMap(emitter.begin, (state) => effect.onExit(restore(attempt), (exit) => emitter.end(state, exit))) + ) let result: Result.Result | undefined return effect.flatMap( effect.whileLoop({ while: () => i < plan.steps.length && (result === undefined || Result.isFailure(result)), body() { const step = plan.steps[i] - let nextEffect: Effect = provideMeta(internalLayer.provide(self, step.provide as any)) + let nextEffect: Effect = provideMeta( + instrument(internalLayer.provide(self, step.provide as any)) + ) if (result) { let attempted = false const wrapped = nextEffect diff --git a/.context/effect/packages/effect/src/internal/hashMap.ts b/.context/effect/packages/effect/src/internal/hashMap.ts index aa62c937e..ad6933162 100644 --- a/.context/effect/packages/effect/src/internal/hashMap.ts +++ b/.context/effect/packages/effect/src/internal/hashMap.ts @@ -977,13 +977,8 @@ export const hasBy = dual< return false }) -/** @internal */ -export const set = dual< - (key: K, value: V) => (self: HashMap) => HashMap, - (self: HashMap, key: K, value: V) => HashMap ->(3, (self: HashMap, key: K, value: V): HashMap => { +const setHash = (self: HashMap, key: K, hash: number, value: V): HashMap => { const impl = self as HashMapImpl - const hash = Hash.hash(key) const added = { value: false } // Pass edit context: use current edit if editable, otherwise NaN (never matches any edit) @@ -1005,6 +1000,14 @@ export const set = dual< } return new HashMapImpl(false, impl._edit, newRoot, impl._size + (added.value ? 1 : 0)) +} + +/** @internal */ +export const set = dual< + (key: K, value: V) => (self: HashMap) => HashMap, + (self: HashMap, key: K, value: V) => HashMap +>(3, (self: HashMap, key: K, value: V): HashMap => { + return setHash(self, key, Hash.hash(key), value) }) /** @internal */ @@ -1104,10 +1107,10 @@ export const modifyHash = dual< const updated = f(current) if (Option.isNone(updated)) { - return hasHash(self, key, hash) ? remove(self, key) : self + return hasHash(self, key, hash) ? removeHash(self, key, hash) : self } - return set(self, key, updated.value) + return setHash(self, key, hash, updated.value) }) /** @internal */ @@ -1130,13 +1133,8 @@ export const union = dual< return result }) -/** @internal */ -export const remove = dual< - (key: K) => (self: HashMap) => HashMap, - (self: HashMap, key: K) => HashMap ->(2, (self: HashMap, key: K): HashMap => { +const removeHash = (self: HashMap, key: K, hash: number): HashMap => { const impl = self as HashMapImpl - const hash = Hash.hash(key) const removed = { value: false } const edit = impl._editable ? impl._edit : NaN @@ -1157,6 +1155,14 @@ export const remove = dual< } return new HashMapImpl(false, impl._edit, newRoot, impl._size - 1) +} + +/** @internal */ +export const remove = dual< + (key: K) => (self: HashMap) => HashMap, + (self: HashMap, key: K) => HashMap +>(2, (self: HashMap, key: K): HashMap => { + return removeHash(self, key, Hash.hash(key)) }) /** @internal */ diff --git a/.context/effect/packages/effect/src/internal/matcher.ts b/.context/effect/packages/effect/src/internal/matcher.ts index a9d20eebf..3a031787e 100644 --- a/.context/effect/packages/effect/src/internal/matcher.ts +++ b/.context/effect/packages/effect/src/internal/matcher.ts @@ -124,8 +124,8 @@ const makePredicate = (pattern: unknown): Predicate.Predicate => { return true } } else if (pattern !== null && typeof pattern === "object") { - const keysAndPredicates = Object.entries(pattern).map( - ([k, p]) => [k, makePredicate(p)] as const + const keysAndPredicates = Reflect.ownKeys(pattern).map( + (key) => [key, makePredicate((pattern as any)[key])] as const ) const len = keysAndPredicates.length @@ -396,7 +396,7 @@ export const discriminators = (field: D) => fields: P ) => { const predicate = makeWhen( - (arg: any) => arg != null && arg[field] in fields, + (arg: any) => arg != null && Object.hasOwn(fields, arg[field]), (data: any) => (fields as any)[data[field]](data) ) diff --git a/.context/effect/packages/effect/src/internal/persistence.ts b/.context/effect/packages/effect/src/internal/persistence.ts new file mode 100644 index 000000000..5d222c257 --- /dev/null +++ b/.context/effect/packages/effect/src/internal/persistence.ts @@ -0,0 +1,2 @@ +/** @internal */ +export const sqlCleanupBatchSize = 1000 diff --git a/.context/effect/packages/effect/src/internal/rcRef.ts b/.context/effect/packages/effect/src/internal/rcRef.ts index 7013b2c2a..56245e27d 100644 --- a/.context/effect/packages/effect/src/internal/rcRef.ts +++ b/.context/effect/packages/effect/src/internal/rcRef.ts @@ -94,7 +94,7 @@ export const make = (options: { }) const getState = (self: RcRefImpl) => - Effect.uninterruptibleMask((restore) => { + Effect.uninterruptibleMask(function loop(restore): Effect.Effect, E> { switch (self.state._tag) { case "Closed": { return Effect.interrupt @@ -107,22 +107,30 @@ const getState = (self: RcRefImpl) => } case "Empty": { const scope = Scope.makeUnsafe() - return self.semaphore.withPermits(1)( - restore(Effect.provideContext( - self.acquire as Effect.Effect, - Context.add(self.context, Scope.Scope, scope) - )).pipe(Effect.map((value) => { - const state: State.Acquired = { - _tag: "Acquired", - value, - scope, - fiber: undefined, - refCount: 1, - invalidated: false + return self.semaphore.withPermit( + Effect.suspend(() => { + if (self.state._tag !== "Empty") { + return loop(restore) } - self.state = state - return state - })) + return restore(Effect.provideContext( + self.acquire as Effect.Effect, + Context.add(self.context, Scope.Scope, scope) + )).pipe( + Effect.map((value) => { + const state: State.Acquired = { + _tag: "Acquired", + value, + scope, + fiber: undefined, + refCount: 1, + invalidated: false + } + self.state = state + return state + }), + Effect.onExit((exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void) + ) + }) ) } } diff --git a/.context/effect/packages/effect/src/internal/record.ts b/.context/effect/packages/effect/src/internal/record.ts index 91319df89..ee4de5a19 100644 --- a/.context/effect/packages/effect/src/internal/record.ts +++ b/.context/effect/packages/effect/src/internal/record.ts @@ -1,9 +1,5 @@ -/** - * @since 4.0.0 - */ - /** @internal */ -export function set(self: Record, key: K, value: A): Record { +export function assignProperty(self: object, key: PropertyKey, value: unknown): void { if (key === "__proto__") { Object.defineProperty(self, key, { value, @@ -12,7 +8,15 @@ export function set(self: Record, key: K, value: configurable: true }) } else { - self[key] = value + ;(self as any)[key] = value + } +} + +/** @internal */ +export function assignProperties(self: object, source: object): void { + for (const key of Reflect.ownKeys(source)) { + if (Object.prototype.propertyIsEnumerable.call(source, key)) { + assignProperty(self, key, (source as any)[key]) + } } - return self } diff --git a/.context/effect/packages/effect/src/internal/references.ts b/.context/effect/packages/effect/src/internal/references.ts index 8fdd284f6..6cb160019 100644 --- a/.context/effect/packages/effect/src/internal/references.ts +++ b/.context/effect/packages/effect/src/internal/references.ts @@ -6,11 +6,6 @@ import type { ReadonlyRecord } from "../Record.ts" import type { StackFrame } from "../References.ts" import type { SpanLink } from "../Tracer.ts" -/** @internal */ -export const CurrentConcurrency = Context.Reference<"unbounded" | number>("effect/References/CurrentConcurrency", { - defaultValue: () => "unbounded" -}) - /** @internal */ export const CurrentErrorReporters = Context.Reference>( "effect/ErrorReporter/CurrentErrorReporters", @@ -19,6 +14,7 @@ export const CurrentErrorReporters = Context.Reference("effect/References/CurrentStackFrame", { + fiberCached: true, defaultValue: constUndefined }) @@ -52,13 +48,13 @@ export const CurrentLogAnnotations = Context.Reference = Context.Reference( "effect/References/CurrentLogLevel", - { defaultValue: () => "Info" } + { fiberCached: true, defaultValue: () => "Info" } ) /** @internal */ export const MinimumLogLevel = Context.Reference< LogLevel ->("effect/References/MinimumLogLevel", { defaultValue: () => "Info" }) +>("effect/References/MinimumLogLevel", { fiberCached: true, defaultValue: () => "Info" }) /** @internal */ export const UnhandledLogLevel: Context.Reference = Context.Reference( diff --git a/.context/effect/packages/effect/src/internal/schedule.ts b/.context/effect/packages/effect/src/internal/schedule.ts index bb405f633..ecb1c3743 100644 --- a/.context/effect/packages/effect/src/internal/schedule.ts +++ b/.context/effect/packages/effect/src/internal/schedule.ts @@ -184,17 +184,17 @@ export const scheduleFrom = dual< schedule: Schedule.Schedule ) => ( self: Effect - ) => Effect, + ) => Effect, ( self: Effect, initial: Input, schedule: Schedule.Schedule - ) => Effect + ) => Effect >(3, ( self: Effect, initial: Input, schedule: Schedule.Schedule -): Effect => +): Effect => effect.flatMap(Schedule.toStepWithMetadata(schedule), (step) => { let meta = Schedule.CurrentMetadata.defaultValue() const selfWithMeta = effect.suspend(() => effect.provideService(self, Schedule.CurrentMetadata, meta)) @@ -213,7 +213,7 @@ export const scheduleFrom = dual< }) as Effect } ), - (error) => core.isDone(error) ? effect.succeed(error.value as Output) : effect.fail(error as E) + (error) => core.isDone(error) ? effect.succeed(error.value as Output) : effect.fail(error as E | Error) ) })) diff --git a/.context/effect/packages/effect/src/internal/schema/annotations.ts b/.context/effect/packages/effect/src/internal/schema/annotations.ts index 174494ae6..d5fb684f7 100644 --- a/.context/effect/packages/effect/src/internal/schema/annotations.ts +++ b/.context/effect/packages/effect/src/internal/schema/annotations.ts @@ -12,9 +12,38 @@ export function resolveAt(key: string) { return (ast: SchemaAST.AST): A | undefined => resolve(ast)?.[key] as A | undefined } +/** @internal */ +export const STRUCTURAL_ANNOTATION_KEY = "~structural" + +/** @internal */ +export const IDENTIFIER_FALLBACK_KEY = "~identifier" + +/** @internal */ +export const SENTINELS_ANNOTATION_KEY = "~sentinels" + +/** @internal */ +export const CONSTRUCTOR_ANNOTATION_KEY = "~constructor" + +/** @internal */ +export const jsonSchemaAnnotationKeys = [ + "title", + "description", + "default", + "examples", + "readOnly", + "writeOnly", + "format", + "contentEncoding", + "contentMediaType", + "contentSchema" +] as const + /** @internal */ export const resolveIdentifier = resolveAt("identifier") +/** @internal */ +export const resolveIdentifierFallback = resolveAt(IDENTIFIER_FALLBACK_KEY) + /** @internal */ export const resolveTitle = resolveAt("title") @@ -26,7 +55,7 @@ export const resolveBrands = resolveAt>("brands") /** @internal */ export const getExpected = memoize((ast: SchemaAST.AST): string => { - const identifier = resolveIdentifier(ast) + const identifier = resolve(ast)?.identifier if (typeof identifier === "string") return identifier return ast.getExpected(getExpected) }) @@ -35,3 +64,21 @@ export const getExpected = memoize((ast: SchemaAST.AST): string => { export function collectBrands(annotations: Schema.Annotations.Annotations | undefined): ReadonlyArray { return annotations !== undefined && Array.isArray(annotations.brands) ? annotations.brands : [] } + +/** @internal */ +export const annotationExcludedKeys = new Set([ + SENTINELS_ANNOTATION_KEY, + STRUCTURAL_ANNOTATION_KEY, + "representation", + "arbitrary", + "brands", + "toJsonSchema", + "toCode", + "toArbitrary", + "toEquivalence", + "toFormatter", + "toCodec", + "toCodecJson", + "toCodecStringTree", + "toCodecIso" +]) diff --git a/.context/effect/packages/effect/src/internal/schema/arbitrary.ts b/.context/effect/packages/effect/src/internal/schema/arbitrary.ts deleted file mode 100644 index 8fe969071..000000000 --- a/.context/effect/packages/effect/src/internal/schema/arbitrary.ts +++ /dev/null @@ -1,915 +0,0 @@ -import * as Array from "../../Array.ts" -import * as Boolean from "../../Boolean.ts" -import type * as Combiner from "../../Combiner.ts" -import * as Equal from "../../Equal.ts" -import { memoize } from "../../Function.ts" -import * as Number from "../../Number.ts" -import * as Option from "../../Option.ts" -import * as Order from "../../Order.ts" -import * as Predicate from "../../Predicate.ts" -import type * as Schema from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import * as Struct from "../../Struct.ts" -import type * as FastCheck from "../../testing/FastCheck.ts" -import * as UndefinedOr from "../../UndefinedOr.ts" -import { errorWithPath } from "../errors.ts" -import * as InternalAnnotations from "./annotations.ts" - -const arbitraryMemoMap = new WeakMap>() -const suspendDepthIdentifierMap = new WeakMap() -const emptyRecursionStack: RecursionStack = [] - -type RecursionStack = ReadonlyArray - -type Context = Schema.Annotations.ToArbitrary.Context -type Constraint = Schema.Annotations.ToArbitrary.GenerationConstraint -type OrderedConstraint = Schema.Annotations.ToArbitrary.OrderedConstraint -type ArbitraryFilter = Schema.Annotations.ToArbitrary.Filter - -type Lazy = (fc: typeof FastCheck, ctx: Context, recursionStack: RecursionStack) => FastCheck.Arbitrary -type LazyOption = ( - fc: typeof FastCheck, - ctx: Context, - recursionStack: RecursionStack -) => FastCheck.Arbitrary | undefined - -export interface MutableReport { - readonly warnings: Array -} - -/** @internal */ -export function makeReport(): MutableReport { - return { warnings: [] } -} - -/** @internal */ -export function toReport(report: MutableReport): Schema.Annotations.ToArbitrary.Report { - return { warnings: report.warnings.slice() } -} - -function arbitraryError(what: string) { - return new Error(`Unable to derive an arbitrary for ${what}`) -} - -const entryComparator = ([a]: readonly [any, any], [b]: readonly [any, any]) => Equal.equals(a, b) - -function applyChecks(ast: SchemaAST.AST, filters: Array>, arbitrary: FastCheck.Arbitrary) { - return filters.reduce( - (acc, filter) => acc.filter((a) => filter.run(a, ast, SchemaAST.defaultParseOptions) === undefined), - arbitrary - ) -} - -function validateArrayConstraints(constraint: FastCheck.ArrayConstraints | undefined, label: string) { - if ( - constraint?.minLength !== undefined && constraint.maxLength !== undefined && - constraint.minLength > constraint.maxLength - ) { - throw arbitraryError(`${label} constraints`) - } -} - -function lengthToFastCheckConstraints( - constraint: { readonly minLength?: number | undefined; readonly maxLength?: number | undefined } | undefined -) { - return constraint === undefined || (constraint.minLength === undefined && constraint.maxLength === undefined) - ? undefined - : { - ...(constraint.minLength !== undefined ? { minLength: constraint.minLength } : {}), - ...(constraint.maxLength !== undefined ? { maxLength: constraint.maxLength } : {}) - } -} - -function arrayWithConstraints( - fc: typeof FastCheck, - item: FastCheck.Arbitrary, - constraint: FastCheck.ArrayConstraints | undefined, - comparator?: ((a: any, b: any) => boolean) | undefined -) { - return comparator - ? fc.uniqueArray(item, { ...constraint, comparator }) - : fc.array(item, constraint) -} - -function array(fc: typeof FastCheck, ctx: Context, item: FastCheck.Arbitrary, terminal = false) { - const constraint = ctx.constraint - const arrayConstraints = lengthToFastCheckConstraints(constraint) - validateArrayConstraints(arrayConstraints, "array") - return arrayWithConstraints( - fc, - item, - terminal ? { ...arrayConstraints, maxLength: arrayConstraints?.minLength ?? 0 } : arrayConstraints, - constraint?.unique ? Equal.equals : undefined - ) -} - -function appendArray( - fc: typeof FastCheck, - out: FastCheck.Arbitrary>, - len: number, - rest: FastCheck.Arbitrary> -) { - return out.chain((as) => as.length < len ? fc.constant(as) : rest.map((rest) => [...as, ...rest])) -} - -function appendObjectEntries( - out: FastCheck.Arbitrary, - entries: FastCheck.Arbitrary> -) { - return out.chain((o) => entries.map((entries) => ({ ...Object.fromEntries(entries), ...o }))) -} - -const max = UndefinedOr.makeReducer(Number.ReducerMax) -const min = UndefinedOr.makeReducer(Number.ReducerMin) -const or = UndefinedOr.makeReducer(Boolean.ReducerOr) -const concat = UndefinedOr.makeReducer(Array.makeReducerConcat()) - -const combiner: Combiner.Combiner = Struct.makeCombiner({ - integer: or, - maxLength: min, - minLength: max, - noInfinity: or, - noNaN: or, - patterns: concat, - unique: or, - valid: or -}, { - omitKeyWhen: Predicate.isUndefined -}) - -function mergeOrderedBound( - order: Order.Order, - self: T | undefined, - selfExclusive: boolean | undefined, - that: T | undefined, - thatExclusive: boolean | undefined, - takeComparison: -1 | 1 -): readonly [T | undefined, boolean | undefined] { - if (that === undefined || self === undefined) { - return that === undefined ? [self, selfExclusive] : [that, thatExclusive] - } - const comparison = order(self, that) - return comparison === takeComparison - ? [that, thatExclusive] - : comparison === 0 - ? [self, selfExclusive || thatExclusive] - : [self, selfExclusive] -} - -function mergeOrderedConstraints(self: OrderedConstraint | undefined, that: OrderedConstraint) { - if (self === undefined) { - return that - } - if (self.order !== that.order) { - throw new Error("Cannot merge ordered arbitrary constraints with different Order instances") - } - - const [minimum, exclusiveMinimum] = mergeOrderedBound( - self.order, - self.minimum, - self.exclusiveMinimum, - that.minimum, - that.exclusiveMinimum, - -1 - ) - const [maximum, exclusiveMaximum] = mergeOrderedBound( - self.order, - self.maximum, - self.exclusiveMaximum, - that.maximum, - that.exclusiveMaximum, - 1 - ) - - return { - order: self.order, - ...(minimum !== undefined ? { minimum } : {}), - ...(exclusiveMinimum !== undefined ? { exclusiveMinimum } : {}), - ...(maximum !== undefined ? { maximum } : {}), - ...(exclusiveMaximum !== undefined ? { exclusiveMaximum } : {}) - } -} - -function mergeConstraint(self: Constraint | undefined, that: Constraint): Constraint { - const { ordered: selfOrdered, ...selfRest } = self ?? {} - const { ordered: thatOrdered, ...thatRest } = that - const ordered = thatOrdered === undefined - ? selfOrdered - : mergeOrderedConstraints(selfOrdered, thatOrdered) - const out = combiner.combine(selfRest, thatRest) - return { - ...out, - ...(ordered === undefined ? {} : { ordered }) - } -} - -function collectChecks(checks: SchemaAST.Checks | undefined) { - const filters: Array> = [] - const arbitraries: Array = [] - function visit(check: SchemaAST.Check) { - if (check.annotations?.arbitrary) { - arbitraries.push(check.annotations.arbitrary) - } - if (check._tag !== "Filter") { - for (const child of check.checks) { - visit(child) - } - } else { - filters.push(check) - } - } - checks?.forEach(visit) - return { filters, arbitraries } -} - -function constraintContext(arbitraries: Array): (ctx: Context) => Context { - const constraintAnnotations = arbitraries.map(({ constraint }) => constraint).filter(Predicate.isNotUndefined) - return (ctx) => { - const constraint = constraintAnnotations.reduce( - (acc: Constraint | undefined, c) => mergeConstraint(acc, c), - ctx.constraint - ) - return { ...ctx, constraint } - } -} - -function resetContext(ctx: Context) { - return { ...ctx, constraint: undefined } -} - -function objectEntriesConstraints(ast: SchemaAST.Objects, constraint: Constraint | undefined, requiredKeys: number) { - if (constraint === undefined || (constraint.minLength === undefined && constraint.maxLength === undefined)) { - return undefined - } - if ( - constraint.minLength !== undefined && - ast.indexSignatures.length === 0 && - constraint.minLength > ast.propertySignatures.length - ) { - throw arbitraryError("object property constraints") - } - const out: FastCheck.ArrayConstraints = {} - if (constraint.minLength !== undefined) { - out.minLength = Math.max(0, constraint.minLength - requiredKeys) - } - if (constraint.maxLength !== undefined) { - out.maxLength = constraint.maxLength - requiredKeys - if (out.maxLength < 0) { - throw arbitraryError("object property constraints") - } - } - validateArrayConstraints(out, "object property") - return out -} - -function objectWithOptionalCount( - fc: typeof FastCheck, - pss: Record>, - orderedNames: ReadonlyArray, - requiredKeys: ReadonlyArray, - optionalNames: ReadonlyArray, - constraint: Constraint -) { - const requiredCount = requiredKeys.length - if (constraint.maxLength !== undefined && constraint.maxLength < requiredCount) { - throw arbitraryError("object property constraints") - } - const minOptional = constraint.minLength === undefined ? 0 : Math.max(0, constraint.minLength - requiredCount) - const maxOptional = constraint.maxLength === undefined - ? optionalNames.length - : Math.min(optionalNames.length, constraint.maxLength - requiredCount) - if (minOptional > maxOptional) { - throw arbitraryError("object property constraints") - } - const full = fc.record(pss, { requiredKeys: [...requiredKeys, ...optionalNames] }) - const chosen = fc.shuffledSubarray([...optionalNames], { minLength: minOptional, maxLength: maxOptional }) - return fc.tuple(full, chosen).map(([base, names]) => { - const keep = new Set([...requiredKeys, ...names]) - const out: Record = {} - for (const name of orderedNames) { - if (keep.has(name)) { - out[name] = base[name] - } - } - return out - }) -} - -function toRangeConstraints( - ordered: OrderedConstraint | undefined, - min: (value: T, excluded: boolean) => T, - max: (value: T, excluded: boolean) => T, - error: string -) { - const out: { min?: T; max?: T } = {} - if (ordered?.minimum !== undefined) { - out.min = min(ordered.minimum as T, ordered.exclusiveMinimum === true) - } - if (ordered?.maximum !== undefined) { - out.max = max(ordered.maximum as T, ordered.exclusiveMaximum === true) - } - if (out.min !== undefined && out.max !== undefined && out.min > out.max) { - throw arbitraryError(error) - } - return out -} - -function toIntegerConstraints(ordered: OrderedConstraint | undefined) { - return toRangeConstraints( - ordered, - (minimum, excluded) => excluded ? Math.floor(minimum) + 1 : Math.ceil(minimum), - (maximum, excluded) => excluded ? Math.ceil(maximum) - 1 : Math.floor(maximum), - "integer constraints" - ) -} - -function toFloatConstraints(constraint: Constraint | undefined, ordered: OrderedConstraint | undefined) { - const out: FastCheck.FloatConstraints = { - ...(constraint?.noInfinity ? { noDefaultInfinity: true } : {}), - ...(constraint?.noNaN ? { noNaN: true } : {}), - ...(ordered?.minimum !== undefined ? { min: ordered.minimum as number } : {}), - ...(ordered?.exclusiveMinimum !== undefined ? { minExcluded: ordered.exclusiveMinimum } : {}), - ...(ordered?.maximum !== undefined ? { max: ordered.maximum as number } : {}), - ...(ordered?.exclusiveMaximum !== undefined ? { maxExcluded: ordered.exclusiveMaximum } : {}) - } - if ( - out.min !== undefined && - out.max !== undefined && - (out.min > out.max || (out.min === out.max && (out.minExcluded || out.maxExcluded))) - ) { - throw arbitraryError("number constraints") - } - return out -} - -function toBigIntConstraints(ordered: OrderedConstraint | undefined) { - return toRangeConstraints( - ordered, - (minimum, excluded) => excluded ? minimum + BigInt(1) : minimum, - (maximum, excluded) => excluded ? maximum - BigInt(1) : maximum, - "the ordered bigint constraints" - ) -} - -interface LazyArbitraryWithContext { - (fc: typeof FastCheck, ctx: Context, recursionStack?: RecursionStack): FastCheck.Arbitrary - readonly terminal: ( - fc: typeof FastCheck, - ctx: Context, - recursionStack?: RecursionStack - ) => FastCheck.Arbitrary | undefined -} - -function makeLazy(normal: Lazy, terminal: LazyOption): LazyArbitraryWithContext { - const out = - ((fc, ctx, recursionStack = emptyRecursionStack) => normal(fc, ctx, recursionStack)) as LazyArbitraryWithContext - ;(out as { terminal: LazyOption }).terminal = (fc, ctx, recursionStack = emptyRecursionStack) => - terminal(fc, ctx, recursionStack) - return out -} - -function same(f: Lazy) { - return makeLazy(f, f) -} - -function getSuspendRecursion(fc: typeof FastCheck, ast: SchemaAST.Suspend) { - const depthIdentifier = suspendDepthIdentifierMap.get(ast) ?? fc.createDepthIdentifier() - suspendDepthIdentifierMap.set(ast, depthIdentifier) - return { maxDepth: 2, depthIdentifier } -} - -function oneOf(fc: typeof FastCheck, arbitraries: ReadonlyArray>) { - return arbitraries.length === 0 ? undefined : arbitraries.length === 1 ? arbitraries[0] : fc.oneof(...arbitraries) -} - -const finiteNumberConstraint: Constraint = { - noInfinity: true, - noNaN: true -} - -function finiteNumberContext(ctx: Context): Context { - return { - ...ctx, - constraint: finiteNumberConstraint - } -} - -function reportChecks(report: MutableReport, checks: SchemaAST.Checks | undefined, path: ReadonlyArray) { - function visit(check: SchemaAST.Check, covered: boolean) { - const arbitrary = check.annotations?.arbitrary - const nextCovered = covered || arbitrary?.constraint !== undefined || arbitrary?.candidate !== undefined - if (check._tag !== "Filter") { - for (const child of check.checks) { - visit(child, nextCovered) - } - } else if (!nextCovered) { - const meta = check.annotations?.meta - const description = typeof meta === "object" && meta !== null && "_tag" in meta && typeof meta._tag === "string" - ? meta._tag - : check.annotations?.identifier ?? check.annotations?.expected - report.warnings.push({ _tag: "OpaqueFilter", path, ...(description === undefined ? {} : { description }) }) - } - } - checks?.forEach((check) => visit(check, false)) -} - -/** @internal */ -export function collectReport(ast: SchemaAST.AST, report: MutableReport) { - const stack = new WeakSet() - function visit(ast: SchemaAST.AST, path: ReadonlyArray) { - if (stack.has(ast)) { - return - } - stack.add(ast) - reportChecks(report, ast.checks, path) - switch (ast._tag) { - case "Declaration": - ast.typeParameters.forEach((tp) => visit(tp, path)) - break - case "Arrays": { - for (const [i, type] of [...ast.elements, ...ast.rest].entries()) { - visit(type, [...path, i]) - } - break - } - case "Objects": - ast.propertySignatures.forEach((ps) => visit(ps.type, [...path, ps.name])) - ast.indexSignatures.forEach((is) => { - visit(is.parameter, path) - visit(is.type, path) - }) - break - case "Union": - ast.types.forEach((type) => visit(type, path)) - break - case "TemplateLiteral": - ast.parts.forEach((part, i) => visit(SchemaAST.toEncoded(part), [...path, i])) - break - case "Suspend": - visit(ast.thunk(), path) - break - } - stack.delete(ast) - } - visit(ast, []) -} - -function applyCandidates( - fc: typeof FastCheck, - ctx: Context, - arbitraries: Array, - base: FastCheck.Arbitrary | undefined -) { - const weighted: Array> = base === undefined - ? [] - : [{ arbitrary: base, weight: 1 }] - for (const { candidate } of arbitraries) { - if (!candidate) { - continue - } - const arbitrary = candidate.make(fc, ctx) - if (arbitrary === undefined) { - continue - } - const weight = candidate.weight ?? 1 - if (!globalThis.Number.isInteger(weight) || weight <= 0) { - throw arbitraryError("a candidate with an invalid weight") - } - weighted.push({ arbitrary, weight }) - } - return weighted.length === 0 ? undefined : weighted.length === 1 ? weighted[0].arbitrary : fc.oneof(...weighted) -} - -function applyFilterLayer( - ast: SchemaAST.AST, - checks: ReturnType, - fc: typeof FastCheck, - ctx: Context, - base: FastCheck.Arbitrary | undefined -) { - const out = applyCandidates(fc, ctx, checks.arbitraries, base) - return out === undefined ? undefined : applyChecks(ast, checks.filters, out) -} - -function normalizeDerivation( - output: Schema.Annotations.ToArbitrary.Output, - hasTypeParameters: boolean -) { - if (!(typeof output === "object" && output !== null && "arbitrary" in output)) { - return { arbitrary: output, terminal: hasTypeParameters ? undefined : output } - } - const terminal = "terminal" in output ? output.terminal : hasTypeParameters ? undefined : output.arbitrary - return { - arbitrary: output.arbitrary, - terminal - } -} - -function makeTypeParameters( - typeParameters: ReadonlyArray>, - fc: typeof FastCheck, - ctx: Context, - recursionStack: RecursionStack, - lazyNormal: boolean -) { - return typeParameters.map((tp) => ({ - arbitrary: lazyNormal ? fc.constant(null).chain(() => tp(fc, ctx, recursionStack)) : tp(fc, ctx, recursionStack), - terminal: tp.terminal(fc, ctx, recursionStack) - })) -} - -type BaseBuilder = ( - fc: typeof FastCheck, - ctx: Context, - nextCtx: Context, - recursionStack: RecursionStack -) => FastCheck.Arbitrary | undefined - -function filterLayer( - ast: SchemaAST.AST, - checks: ReturnType, - normalBase: BaseBuilder, - terminalBase: BaseBuilder -): LazyArbitraryWithContext { - const f = constraintContext(checks.arbitraries) - return makeLazy((fc, ctx, recursionStack) => { - const nextCtx = f(ctx) - return applyFilterLayer(ast, checks, fc, nextCtx, normalBase(fc, ctx, nextCtx, recursionStack))! - }, (fc, ctx, recursionStack) => { - const nextCtx = f(ctx) - return applyFilterLayer(ast, checks, fc, nextCtx, terminalBase(fc, ctx, nextCtx, recursionStack)) - }) -} - -/** @internal */ -export const memoized = memoize((ast: SchemaAST.AST) => recur(ast, [])) - -function recur(ast: SchemaAST.AST, path: ReadonlyArray): LazyArbitraryWithContext { - // --------------------------------------------- - // handle annotations - // --------------------------------------------- - const annotation = InternalAnnotations.resolve(ast)?.toArbitrary as - | Schema.Annotations.ToArbitrary.Declaration> - | undefined - if (annotation) { - const typeParameters = SchemaAST.isDeclaration(ast) ? ast.typeParameters.map((tp) => recur(tp, path)) : [] - const checks = collectChecks(ast.checks) - const derive = (lazyNormal: boolean): BaseBuilder => (fc, ctx, nextCtx, recursionStack) => - normalizeDerivation( - annotation(makeTypeParameters(typeParameters, fc, resetContext(ctx), recursionStack, lazyNormal))(fc, nextCtx), - typeParameters.length > 0 - )[lazyNormal ? "terminal" : "arbitrary"] - return filterLayer(ast, checks, derive(false), derive(true)) - } - if (ast.checks) { - const checks = collectChecks(ast.checks) - const lawc = recur(SchemaAST.replaceChecks(ast, undefined), path) - return filterLayer( - ast, - checks, - (fc, _ctx, nextCtx, recursionStack) => lawc(fc, nextCtx, recursionStack), - (fc, _ctx, nextCtx, recursionStack) => lawc.terminal(fc, nextCtx, recursionStack) - ) - } - return base(ast, path) -} - -function base(ast: SchemaAST.AST, path: ReadonlyArray): LazyArbitraryWithContext { - switch (ast._tag) { - case "Never": - case "Declaration": - throw errorWithPath(`Unsupported AST ${ast._tag}`, path) - case "Null": - return same((fc) => fc.constant(null)) - case "Void": - case "Undefined": - return same((fc) => fc.constant(undefined)) - case "Unknown": - case "Any": - return same((fc) => fc.anything()) - case "String": - return same((fc, ctx) => { - const constraint = ctx.constraint - const patterns = constraint?.patterns - return patterns - ? fc.oneof(...patterns.map((pattern) => fc.stringMatching(new RegExp(pattern)))) - : fc.string(lengthToFastCheckConstraints(constraint)) - }) - case "Number": - return same((fc, ctx) => { - const constraint = ctx.constraint - const ordered = constraint?.ordered?.order === Order.Number ? constraint.ordered : undefined - return constraint?.integer - ? fc.integer(toIntegerConstraints(ordered)) - : fc.float(toFloatConstraints(constraint, ordered)) - }) - case "Boolean": - return same((fc) => fc.boolean()) - case "BigInt": - return same((fc, ctx) => { - const ordered = ctx.constraint?.ordered?.order === Order.BigInt ? ctx.constraint.ordered : undefined - return fc.bigInt(toBigIntConstraints(ordered)) - }) - case "Symbol": - return same((fc) => fc.string().map(Symbol.for)) - case "Literal": - return same((fc) => fc.constant(ast.literal)) - case "UniqueSymbol": - return same((fc) => fc.constant(ast.symbol)) - case "ObjectKeyword": - return same((fc) => fc.oneof(fc.object(), fc.array(fc.anything()))) - case "Enum": - return recur(SchemaAST.enumsToLiterals(ast), path) - case "TemplateLiteral": { - const parts = ast.parts.map((part, i) => recur(SchemaAST.toEncoded(part), [...path, i])) - return same((fc, ctx, recursionStack) => - fc.tuple(...parts.map((part) => part(fc, finiteNumberContext(ctx), recursionStack))).map((segments) => - segments.map((segment) => globalThis.String(segment)).join("") - ) - ) - } - case "Arrays": { - const elements = ast.elements.map((ast, i) => ({ - ast, - arbitrary: recur(ast, [...path, i]) - })) - const len = ast.elements.length - const rest = ast.rest.map((ast, i) => ({ - ast, - arbitrary: recur(ast, [...path, len + i]) - })) - const terminal: LazyOption = (fc, ctx, recursionStack) => { - const reset = resetContext(ctx) - const elementArbitraries: Array>> = [] - const optionals: Array | undefined> = [] - let length = 0 - for (const element of elements) { - const out = element.arbitrary.terminal(fc, reset, recursionStack) - if (SchemaAST.isOptional(element.ast)) { - optionals.push(out) - continue - } - if (out === undefined) { - return undefined - } - length++ - elementArbitraries.push(out.map(Option.some)) - } - const minLength = ctx.constraint?.minLength ?? 0 - const needsRest = Array.isReadonlyArrayNonEmpty(rest) && minLength > length + optionals.length - const optionalTarget = needsRest ? optionals.length : Math.max(0, minLength - length) - let includedOptionals = 0 - for (const out of optionals) { - if (includedOptionals >= optionalTarget || out === undefined) { - elementArbitraries.push(fc.constant(Option.none())) - continue - } - includedOptionals++ - length++ - elementArbitraries.push(out.map(Option.some)) - } - if (includedOptionals < optionalTarget) { - return undefined - } - let out = fc.tuple(...elementArbitraries).map(Array.getSomes) - if (Array.isReadonlyArrayNonEmpty(rest)) { - const [head, ...tail] = rest - const restCtx = ast.elements.length === 0 ? ctx : reset - const minRestLength = Math.max(0, minLength - length - tail.length) - const headArbitrary = minRestLength === 0 - ? undefined - : head.arbitrary.terminal(fc, reset, recursionStack) - if (minRestLength > 0 && headArbitrary === undefined) { - return undefined - } - const restArbitrary = minRestLength === 0 - ? fc.constant([]) - : array( - fc, - { ...restCtx, constraint: { ...restCtx.constraint, minLength: minRestLength } }, - headArbitrary!, - true - ) - out = appendArray(fc, out, len, restArbitrary) - if (tail.length > 0) { - const tailArbitraries: Array> = [] - for (const element of tail) { - const out = element.arbitrary.terminal(fc, reset, recursionStack) - if (out === undefined) { - return undefined - } - tailArbitraries.push(out) - } - const t = fc.tuple(...tailArbitraries) - out = appendArray(fc, out, len, t) - } - } - return out - } - return makeLazy((fc, ctx, recursionStack) => { - const reset = resetContext(ctx) - // --------------------------------------------- - // handle elements - // --------------------------------------------- - const elementArbitraries: Array>> = elements.map( - ({ ast, arbitrary }) => { - const out = arbitrary(fc, reset, recursionStack) - return SchemaAST.isOptional(ast) - ? out.chain((a) => fc.boolean().map((b) => b ? Option.some(a) : Option.none())) - : out.map(Option.some) - } - ) - let out = fc.tuple(...elementArbitraries).map(Array.getSomes) - // --------------------------------------------- - // handle rest element - // --------------------------------------------- - if (Array.isReadonlyArrayNonEmpty(rest)) { - const [head, ...tail] = rest.map(({ arbitrary }) => arbitrary(fc, reset, recursionStack)) - - const restArbitrary = array(fc, ast.elements.length === 0 ? ctx : reset, head) - out = appendArray(fc, out, len, restArbitrary) - // --------------------------------------------- - // handle post rest elements - // --------------------------------------------- - if (tail.length > 0) { - const t = fc.tuple(...tail) - out = appendArray(fc, out, len, t) - } - } - if (ctx.recursion) { - const terminalOut = terminal(fc, ctx, recursionStack) - if (terminalOut !== undefined) { - return fc.oneof(ctx.recursion, terminalOut, out) - } - } - return out - }, terminal) - } - case "Objects": { - const propertySignatures = ast.propertySignatures.map((ps) => ({ - ps, - arbitrary: recur(ps.type, [...path, ps.name]) - })) - const indexSignatures = ast.indexSignatures.map((is) => ({ - is, - parameter: recur(is.parameter, path), - type: recur(is.type, path) - })) - const terminal: LazyOption = (fc, ctx, recursionStack) => { - const reset = resetContext(ctx) - const pss: any = {} - const requiredKeys: Array = [] - const optionals: Array]> = [] - for (const { ps, arbitrary } of propertySignatures) { - const name = ps.name - const out = arbitrary.terminal(fc, reset, recursionStack) - if (SchemaAST.isOptional(ps.type)) { - if (out !== undefined) { - optionals.push([name, out]) - } - continue - } - if (out === undefined) { - return undefined - } - requiredKeys.push(name) - pss[name] = out - } - let optionalCount = Math.max(0, (ctx.constraint?.minLength ?? 0) - requiredKeys.length) - for (const [name, out] of optionals) { - if (optionalCount === 0) { - break - } - optionalCount-- - requiredKeys.push(name) - pss[name] = out - } - if (optionalCount > 0 && ast.indexSignatures.length === 0) { - return undefined - } - let out = fc.record(pss, { requiredKeys }) - const entriesConstraints = objectEntriesConstraints(ast, ctx.constraint, requiredKeys.length) - const minEntries = entriesConstraints?.minLength ?? 0 - for (const { parameter, type } of indexSignatures) { - let entries: FastCheck.Arbitrary> - if (minEntries === 0) { - entries = fc.constant([]) - } else { - const key = parameter.terminal(fc, reset, recursionStack) - const value = type.terminal(fc, reset, recursionStack) - if (key === undefined || value === undefined) { - return undefined - } - entries = arrayWithConstraints( - fc, - fc.tuple(key, value), - { ...entriesConstraints, maxLength: minEntries }, - entryComparator - ) - } - out = appendObjectEntries(out, entries) - } - return out - } - return makeLazy((fc, ctx, recursionStack) => { - const reset = resetContext(ctx) - // --------------------------------------------- - // handle property signatures - // --------------------------------------------- - const pss: any = {} - const orderedNames: Array = [] - const requiredKeys: Array = [] - const optionalNames: Array = [] - for (const { ps, arbitrary } of propertySignatures) { - const name = ps.name - orderedNames.push(name) - if (SchemaAST.isOptional(ps.type)) { - optionalNames.push(name) - } else { - requiredKeys.push(name) - } - pss[name] = arbitrary(fc, reset, recursionStack) - } - // When property-count constraints must be satisfied by selecting - // optional keys (no index signatures are available to fill the gap), - // generate a count-controlled subset of optional keys instead of - // relying on fast-check's independent inclusion plus discards. This - // enforces both bounds precisely while still varying which optionals - // appear. - const constraint = ctx.constraint - if ( - optionalNames.length > 0 && - indexSignatures.length === 0 && - constraint !== undefined && - (constraint.minLength !== undefined || constraint.maxLength !== undefined) - ) { - return objectWithOptionalCount(fc, pss, orderedNames, requiredKeys, optionalNames, constraint) - } - let out = fc.record(pss, { requiredKeys }) - const entriesConstraints = objectEntriesConstraints(ast, ctx.constraint, requiredKeys.length) - // --------------------------------------------- - // handle index signatures - // --------------------------------------------- - for (const { parameter, type } of indexSignatures) { - const entry = fc.tuple(parameter(fc, reset, recursionStack), type(fc, reset, recursionStack)) - const entries = arrayWithConstraints(fc, entry, entriesConstraints, entryComparator) - out = appendObjectEntries(out, entries) - } - return out - }, terminal) - } - case "Union": { - const types = ast.types.map((ast) => recur(ast, path)) - const terminal: LazyOption = (fc, ctx, recursionStack) => - oneOf(fc, types.map((type) => type.terminal(fc, ctx, recursionStack)).filter(Predicate.isNotUndefined)) - return makeLazy((fc, ctx, recursionStack) => { - const arbitraries = types.map((type) => type(fc, ctx, recursionStack)) - if (ctx.recursion) { - const terminalOut = terminal(fc, ctx, recursionStack) - if (terminalOut !== undefined) { - return fc.oneof(ctx.recursion, terminalOut, ...arbitraries) - } - } - const out = oneOf(fc, arbitraries) - if (out === undefined) { - throw arbitraryError("a union with no members") - } - return out - }, terminal) - } - case "Suspend": { - const memo = arbitraryMemoMap.get(ast) - - if (memo) return memo - - const get = SchemaAST.memoizeThunk(() => recur(ast.thunk(), path)) - const out = makeLazy((fc, ctx, recursionStack) => { - const recursion = getSuspendRecursion(fc, ast) - const nextCtx = { ...ctx, recursion } - const nextStack = recursionStack.includes(ast) ? recursionStack : [...recursionStack, ast] - const terminal = get().terminal(fc, nextCtx, nextStack) - if (terminal === undefined) { - throw errorWithPath( - "Unable to derive an arbitrary for a recursive schema without a finite generation path", - path - ) - } - return fc.oneof( - recursion, - terminal, - fc.constant(null).chain(() => get()(fc, nextCtx, nextStack)) - ) - }, (fc, ctx, recursionStack) => { - if (recursionStack.includes(ast)) { - return undefined - } - const recursion = getSuspendRecursion(fc, ast) - return get().terminal(fc, { ...ctx, recursion }, [...recursionStack, ast]) - }) - - arbitraryMemoMap.set(ast, out) - - return out - } - } -} diff --git a/.context/effect/packages/effect/src/internal/schema/equivalence.ts b/.context/effect/packages/effect/src/internal/schema/equivalence.ts deleted file mode 100644 index 32818d55a..000000000 --- a/.context/effect/packages/effect/src/internal/schema/equivalence.ts +++ /dev/null @@ -1,153 +0,0 @@ -import * as Equal from "../../Equal.ts" -import * as Equivalence from "../../Equivalence.ts" -import { memoize } from "../../Function.ts" -import * as Predicate from "../../Predicate.ts" -import type * as Schema from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import * as SchemaParser from "../../SchemaParser.ts" -import { errorWithPath } from "../errors.ts" -import * as InternalAnnotations from "./annotations.ts" - -/** @internal */ -export const toEquivalence = memoize((ast: SchemaAST.AST): Equivalence.Equivalence => { - return recur(ast, []) -}) - -function recur(ast: SchemaAST.AST, path: ReadonlyArray): Equivalence.Equivalence { - // --------------------------------------------- - // handle annotations - // --------------------------------------------- - const annotation = InternalAnnotations.resolve(ast)?.["toEquivalence"] as - | Schema.Annotations.ToEquivalence.Declaration> - | undefined - if (annotation) { - return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map((tp) => recur(tp, path)) : []) - } - switch (ast._tag) { - case "Never": - throw errorWithPath(`Unsupported AST ${ast._tag}`, path) - case "Declaration": - case "Null": - case "Undefined": - case "Void": - case "Unknown": - case "Any": - case "String": - case "Number": - case "Boolean": - case "BigInt": - case "Symbol": - case "Literal": - case "UniqueSymbol": - case "ObjectKeyword": - case "Enum": - case "TemplateLiteral": - return Equal.equals - case "Arrays": { - const elements = ast.elements.map((e, i) => recur(e, [...path, i])) - const len = ast.elements.length - const rest = ast.rest.map((r, i) => recur(r, [...path, len + i])) - 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 (rest.length > 0) { - 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++) { - if (!tail[j](a[i + j], b[i + j])) { - return false - } - } - } - return true - }) - } - case "Objects": { - if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { - return Equal.equals - } - const propertySignatures = ast.propertySignatures.map((ps) => recur(ps.type, [...path, ps.name])) - const indexSignatures = ast.indexSignatures.map((is) => recur(is.type, path)) - return Equivalence.make((a, b) => { - if (!Predicate.isObject(a) || !Predicate.isObject(b)) { - return false - } - // --------------------------------------------- - // handle property signatures - // --------------------------------------------- - for (let i = 0; i < propertySignatures.length; i++) { - const ps = ast.propertySignatures[i] - const name = ps.name - const aHas = Object.hasOwn(a, name) - const bHas = Object.hasOwn(b, name) - if (SchemaAST.isOptional(ps.type)) { - if (aHas !== bHas) { - return false - } - } - if (aHas && bHas && !propertySignatures[i](a[name], b[name])) { - return false - } - } - // --------------------------------------------- - // handle index signatures - // --------------------------------------------- - for (let i = 0; i < indexSignatures.length; i++) { - const is = ast.indexSignatures[i] - const aKeys = SchemaAST.getIndexSignatureKeys(a, is.parameter) - const bKeys = SchemaAST.getIndexSignatureKeys(b, is.parameter) - - if (aKeys.length !== bKeys.length) return false - - for (let j = 0; j < aKeys.length; j++) { - const key = aKeys[j] - if (!Object.hasOwn(b, key) || !indexSignatures[i](a[key], b[key])) { - return false - } - } - } - return true - }) - } - case "Union": - return Equivalence.make((a, b) => { - const candidates = SchemaAST.getCandidates(a, ast.types) - const types = candidates.map(SchemaParser._is) - for (let i = 0; i < candidates.length; i++) { - const is = types[i] - if (is(a) && is(b)) { - return recur(candidates[i], path)(a, b) - } - } - return false - }) - case "Suspend": { - const get = SchemaAST.memoizeThunk(() => recur(ast.thunk(), path)) - return Equivalence.make((a, b) => get()(a, b)) - } - } -} diff --git a/.context/effect/packages/effect/src/internal/schema/fromJsonSchemaDocument.ts b/.context/effect/packages/effect/src/internal/schema/fromJsonSchemaDocument.ts new file mode 100644 index 000000000..590802eff --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/fromJsonSchemaDocument.ts @@ -0,0 +1,977 @@ +import { unescapeToken } from "../../JsonPointer.ts" +import type * as JsonSchema from "../../JsonSchema.ts" +import { remainder } from "../../Number.ts" +import * as Schema from "../../Schema.ts" +import * as SchemaAST from "../../SchemaAST.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" +import { errorWithPath } from "../errors.ts" +import * as InternalRecord from "../record.ts" +import * as InternalAnnotations from "./annotations.ts" +import { fromRepresentation, fromRepresentations } from "./fromRepresentation.ts" + +type Path = ReadonlyArray +type Representation = SchemaRepresentation.Representation +type Check = SchemaRepresentation.Check + +type ImportedJsonSchemaRepresentation = Extract + +const never: ImportedJsonSchemaRepresentation = { _tag: "Never", checks: [] } +const unknown: ImportedJsonSchemaRepresentation = { _tag: "Unknown", checks: [] } +const string: ImportedJsonSchemaRepresentation = { _tag: "String", checks: [] } + +function makeLiteral(literal: string | number | boolean): SchemaRepresentation.Literal { + return { _tag: "Literal", literal, checks: [] } +} + +function annotate( + representation: ImportedJsonSchemaRepresentation, + annotations: Schema.Annotations.Annotations | undefined +): ImportedJsonSchemaRepresentation { + if (annotations === undefined) return representation + if (representation._tag === "Reference") { + return { + _tag: "Suspend", + annotations, + checks: [], + thunk: representation + } + } + return { + ...representation, + annotations: { + ...representation.annotations, + ...annotations + } + } +} + +const jsonSchemaTypes = new Set([ + "null", + "string", + "number", + "integer", + "boolean", + "object", + "array" +]) + +const jsonSchemaStringKeys = ["minLength", "maxLength", "pattern", "format", "contentMediaType", "contentSchema"] +const jsonSchemaNumberKeys = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"] +const jsonSchemaObjectKeys = [ + "properties", + "required", + "additionalProperties", + "patternProperties", + "propertyNames", + "minProperties", + "maxProperties" +] +const jsonSchemaArrayKeys = ["items", "prefixItems", "additionalItems", "minItems", "maxItems", "uniqueItems"] +function isImportedJsonSchemaType(input: unknown): input is JsonSchema.Type { + return typeof input === "string" && jsonSchemaTypes.has(input) +} + +function inferJsonSchemaType(schema: JsonSchema.JsonSchema): JsonSchema.Type | undefined { + if (jsonSchemaStringKeys.some((key) => schema[key] !== undefined)) return "string" + if (jsonSchemaNumberKeys.some((key) => schema[key] !== undefined)) return "number" + if (jsonSchemaObjectKeys.some((key) => schema[key] !== undefined)) return "object" + if (jsonSchemaArrayKeys.some((key) => schema[key] !== undefined)) return "array" +} + +function jsonSchemaReferenceKey($ref: string): string | undefined { + const token = $ref.slice($ref.lastIndexOf("/") + 1) + return token.length === 0 ? undefined : unescapeToken(token) +} + +function jsonSchemaFilter( + id: string, + payload: Schema.Json, + schemas?: ReadonlyArray +): Check { + return { + _tag: "Filter", + aborted: false, + representation: { + id, + payload, + ...(schemas === undefined ? undefined : { schemas }) + } + } +} + +function addNumberCheck( + checks: Array, + value: unknown, + id: string, + key: string +): void { + if (typeof value === "number") { + checks.push(jsonSchemaFilter(id, { [key]: value })) + } +} + +function jsonSchemaAnnotations( + schema: JsonSchema.JsonSchema +): Schema.Annotations.Annotations | undefined { + const annotations: Record = {} + if (typeof schema.title === "string") annotations.title = schema.title + if (typeof schema.description === "string") annotations.description = schema.description + if (Object.hasOwn(schema, "default")) annotations.default = schema.default as Schema.Json + if (Array.isArray(schema.examples)) annotations.examples = schema.examples as ReadonlyArray + if (typeof schema.readOnly === "boolean") annotations.readOnly = schema.readOnly + if (typeof schema.writeOnly === "boolean") annotations.writeOnly = schema.writeOnly + if (typeof schema.format === "string") annotations.format = schema.format + if (typeof schema.contentEncoding === "string") annotations.contentEncoding = schema.contentEncoding + if (typeof schema.contentMediaType === "string") annotations.contentMediaType = schema.contentMediaType + if (SchemaAST.isJson(schema.contentSchema)) annotations.contentSchema = schema.contentSchema + return Object.keys(annotations).length === 0 ? undefined : annotations +} + +function jsonDeclaration( + annotations: Schema.Annotations.Annotations | undefined +): Representation { + return { + _tag: "Declaration", + representation: { + id: "effect/schema/Json", + payload: null + }, + annotations: { + ...annotations, + expected: "JSON value" + }, + checks: [], + typeParameters: [] + } +} + +function unknownJsonSchemas(representation: Representation): Representation { + switch (representation._tag) { + case "Unknown": + return jsonDeclaration(representation.annotations) + case "Suspend": + return { ...representation, thunk: unknownJsonSchemas(representation.thunk) } + case "Arrays": + return { + ...representation, + elements: representation.elements.map((element) => ({ + ...element, + type: unknownJsonSchemas(element.type) + })), + rest: representation.rest.map(unknownJsonSchemas) + } + case "Objects": + return { + ...representation, + propertySignatures: representation.propertySignatures.map((property) => ({ + ...property, + type: unknownJsonSchemas(property.type) + })), + indexSignatures: representation.indexSignatures.map((indexSignature) => ({ + parameter: unknownJsonSchemas(indexSignature.parameter), + type: unknownJsonSchemas(indexSignature.type) + })), + checks: representation.checks.map(unknownJsonSchemaCheck) + } + case "Union": + return { ...representation, types: representation.types.map(unknownJsonSchemas) } + default: + return representation + } +} + +function unknownJsonSchemaCheck(check: Check): Check { + const representation = check.representation + const schemas = representation?.schemas + if (representation === undefined || schemas === undefined) { + return check + } + return { + ...check, + representation: { + ...representation, + schemas: schemas.map(unknownJsonSchemas) + } + } +} + +function translateJsonSchemaMultiDocument( + document: JsonSchema.MultiDocument<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions, + singleRoot = false +): SchemaRepresentation.MultiDocument { + const definitionCache = new Map() + const reachableDefinitions = new Map() + const annotatedReferences: Array<{ + readonly reference: SchemaRepresentation.Reference + readonly path: Path + }> = [] + + function translateDefinition( + key: string, + path: Path, + recursiveReferenceError?: string + ): ImportedJsonSchemaRepresentation { + const cached = definitionCache.get(key) + if (cached !== undefined) { + if (cached === null) { + throw errorWithPath(recursiveReferenceError ?? `Invalid reference ${key}`, [...path, "$ref"]) + } + return cached + } + if (!Object.hasOwn(document.definitions, key)) { + throw errorWithPath(`Invalid reference ${key}`, [...path, "$ref"]) + } + definitionCache.set(key, null) + const representation = recur(document.definitions[key], ["definitions", key]) + definitionCache.set(key, representation) + return representation + } + + function resolveReference( + reference: SchemaRepresentation.Reference, + path: Path, + options?: { readonly recursiveReferenceError?: string }, + seen: ReadonlySet = new Set() + ): ImportedJsonSchemaRepresentation { + if (seen.has(reference.$ref)) { + throw errorWithPath(`Invalid reference ${reference.$ref}`, [...path, "$ref"]) + } + const nextSeen = new Set(seen).add(reference.$ref) + const representation = translateDefinition(reference.$ref, path, options?.recursiveReferenceError) + if (representation._tag === "Reference") { + return resolveReference(representation, path, options, nextSeen) + } + if (representation._tag === "Suspend" && representation.thunk._tag === "Reference") { + return annotate( + resolveReference(representation.thunk, path, options, nextSeen), + representation.annotations + ) + } + return representation + } + + function annotationsOf( + representation: ImportedJsonSchemaRepresentation + ): Schema.Annotations.Annotations | undefined { + return representation._tag === "Reference" ? undefined : representation.annotations + } + + function mergeAnnotations( + left: Schema.Annotations.Annotations | undefined, + right: Schema.Annotations.Annotations | undefined + ): Schema.Annotations.Annotations | undefined { + if (left === undefined) return right + if (right === undefined) return left + return { ...left, ...right } + } + + function combinedAnnotations( + representation: ImportedJsonSchemaRepresentation, + left: ImportedJsonSchemaRepresentation, + right: ImportedJsonSchemaRepresentation + ): ImportedJsonSchemaRepresentation { + return annotate( + representation, + mergeAnnotations(annotationsOf(left), annotationsOf(right)) + ) + } + + function asChecks( + checks: ReadonlyArray, + annotations: Schema.Annotations.Annotations | undefined + ): ReadonlyArray | undefined { + if (checks.length === 0) return undefined + if (annotations === undefined) return checks + if (checks.length === 1 && checks[0].annotations === undefined) { + return [{ + ...checks[0], + annotations + }] + } + return [{ + _tag: "FilterGroup", + checks: checks as [Check, ...Array], + annotations + }] + } + + function combineChecks( + left: ReadonlyArray, + right: ReadonlyArray, + annotations: Schema.Annotations.Annotations | undefined, + deduplicate: ReadonlyArray = [] + ): ReadonlyArray | undefined { + for (const id of deduplicate) { + if (left.some((check) => checkId(check) === id)) { + right = right.filter((check) => checkId(check) !== id) + } + } + const checks = asChecks(right, annotations) + return checks === undefined ? undefined : [...left, ...checks] + } + + function checkId(check: Check): string | undefined { + return check._tag === "Filter" ? check.representation?.id : undefined + } + + function satisfiesPrimitiveCheck(check: Check, value: string | number): boolean | undefined { + if (check._tag === "FilterGroup") { + return check.checks.every((check) => satisfiesPrimitiveCheck(check, value)) + } + const representation = check.representation! + const payload = representation.payload as Record + switch (representation.id) { + case "effect/schema/isMinLength": + return (value as string).length >= payload.minLength + case "effect/schema/isMaxLength": + return (value as string).length <= payload.maxLength + case "effect/schema/isPattern": + return new RegExp(payload.source as string, payload.flags as string).test(value as string) + case "effect/schema/isFinite": + return globalThis.Number.isFinite(value as number) + case "effect/schema/isInt": + return globalThis.Number.isSafeInteger(value as number) + case "effect/schema/isMultipleOf": + return remainder(value as number, payload.divisor) === 0 + case "effect/schema/isGreaterThan": + return (value as number) > payload.exclusiveMinimum + case "effect/schema/isGreaterThanOrEqualTo": + return (value as number) >= payload.minimum + case "effect/schema/isLessThan": + return (value as number) < payload.exclusiveMaximum + case "effect/schema/isLessThanOrEqualTo": + return (value as number) <= payload.maximum + } + } + + function satisfiesLiteral( + representation: + | SchemaRepresentation.String + | SchemaRepresentation.Number, + literal: SchemaRepresentation.Literal + ): boolean { + const value = literal.literal + if (representation._tag === "String" ? typeof value !== "string" : typeof value !== "number") { + return false + } + return representation.checks.every((check) => satisfiesPrimitiveCheck(check, value as string | number)) + } + + function combinePrimitiveWithLiteral( + primitive: + | SchemaRepresentation.String + | SchemaRepresentation.Number + | SchemaRepresentation.Boolean, + literal: SchemaRepresentation.Literal + ): ImportedJsonSchemaRepresentation { + const satisfies = primitive._tag === "Boolean" + ? typeof literal.literal === "boolean" + : satisfiesLiteral(primitive, literal) + return satisfies ? combinedAnnotations(literal, primitive, literal) : never + } + + function combineArrays( + left: SchemaRepresentation.Arrays, + right: SchemaRepresentation.Arrays, + path: Path + ): Pick | undefined { + const elements: Array = [] + const length = Math.max(left.elements.length, right.elements.length) + for (let index = 0; index < length; index++) { + const leftElement = left.elements[index] + const rightElement = right.elements[index] + const isOptional = leftElement?.isOptional !== false && rightElement?.isOptional !== false + const leftType = leftElement?.type ?? left.rest[0] + const rightType = rightElement?.type ?? right.rest[0] + if (leftType === undefined || rightType === undefined) { + return isOptional ? { elements, rest: [] } : undefined + } + const type = combine( + leftType as ImportedJsonSchemaRepresentation, + rightType as ImportedJsonSchemaRepresentation, + [...path, "elements", index, "type"] + ) + if (type._tag === "Never") { + return isOptional ? { elements, rest: [] } : undefined + } + elements.push({ + isOptional, + type + }) + } + + const leftRest = left.rest[0] + const rightRest = right.rest[0] + if (leftRest === undefined || rightRest === undefined) { + return { elements, rest: [] } + } + const rest = combine( + leftRest as ImportedJsonSchemaRepresentation, + rightRest as ImportedJsonSchemaRepresentation, + [...path, "rest", 0] + ) + return { elements, rest: rest._tag === "Never" ? [] : [rest] } + } + + function combineProperties( + left: ReadonlyArray, + right: ReadonlyArray, + path: Path + ): Array { + const rightByName = new Map(right.map((property) => [property.name, property])) + const names = new Set() + const properties = left.map((property) => { + const name = property.name + names.add(name) + const other = rightByName.get(name) + if (other === undefined) return property + return { + name: property.name, + type: combine( + property.type as ImportedJsonSchemaRepresentation, + other.type as ImportedJsonSchemaRepresentation, + [...path, "properties", globalThis.String(name)] + ), + isOptional: property.isOptional && other.isOptional, + isMutable: false + } + }) + for (const property of right) { + if (!names.has(property.name)) properties.push(property) + } + return properties + } + + function isUnconstrainedString(representation: Representation): boolean { + return representation._tag === "String" && representation.checks.length === 0 && + representation.annotations === undefined + } + + function combineIndexSignatures( + left: ReadonlyArray, + right: ReadonlyArray, + path: Path + ): Array { + if (left.length === 0 || right.length === 0) return [] + const signatures = [...left] + for (const signature of right) { + if (isUnconstrainedString(signature.parameter)) { + const index = signatures.findIndex((candidate) => isUnconstrainedString(candidate.parameter)) + if (index !== -1) { + signatures[index] = { + parameter: signatures[index].parameter, + type: combine( + signatures[index].type as ImportedJsonSchemaRepresentation, + signature.type as ImportedJsonSchemaRepresentation, + [...path, "indexSignatures", index, "type"] + ) + } + } else { + signatures.push(signature) + } + } else { + signatures.push(signature) + } + } + return signatures + } + + function combine( + left: ImportedJsonSchemaRepresentation, + right: ImportedJsonSchemaRepresentation, + path: Path + ): ImportedJsonSchemaRepresentation { + if (left._tag === "Never") return left + if (right._tag === "Never") return right + if (left._tag === "Unknown") return combinedAnnotations(right, left, right) + if (right._tag === "Unknown") return combinedAnnotations(left, left, right) + if (left._tag === "Reference") return combine(resolveReference(left, path), right, path) + if (right._tag === "Reference") return combine(left, resolveReference(right, path), path) + if (left._tag === "Suspend") { + return annotate( + combine(left.thunk as ImportedJsonSchemaRepresentation, right, path), + left.annotations + ) + } + if (right._tag === "Suspend") { + return annotate( + combine(left, right.thunk as ImportedJsonSchemaRepresentation, path), + right.annotations + ) + } + if (left._tag === "Union") { + const types = left.types + .map((type, index) => combine(type as ImportedJsonSchemaRepresentation, right, [...path, "types", index])) + .filter((type) => type._tag !== "Never") + if (types.length === 0) return never + return annotate({ + _tag: "Union", + types, + mode: left.mode, + checks: left.checks + }, left.annotations) + } + if (right._tag === "Union") return combine(right, left, path) + + switch (left._tag) { + case "Null": + return right._tag === "Null" + ? combinedAnnotations({ _tag: "Null", checks: [...left.checks, ...right.checks] }, left, right) + : never + case "String": + if (right._tag === "Literal") { + return combinePrimitiveWithLiteral(left, right) + } + if (right._tag !== "String") return never + const stringChecks = combineChecks(left.checks, right.checks, right.annotations) + return annotate( + { + _tag: "String", + checks: stringChecks ?? left.checks + }, + mergeAnnotations(left.annotations, stringChecks === undefined ? right.annotations : undefined) + ) + case "Number": + if (right._tag === "Literal") { + return combinePrimitiveWithLiteral(left, right) + } + if (right._tag !== "Number") return never + const numberChecks = combineChecks(left.checks, right.checks, right.annotations, [ + "effect/schema/isFinite", + "effect/schema/isInt" + ]) + return annotate( + { + _tag: "Number", + checks: numberChecks ?? left.checks + }, + mergeAnnotations(left.annotations, numberChecks === undefined ? right.annotations : undefined) + ) + case "Boolean": + if (right._tag === "Literal") { + return combinePrimitiveWithLiteral(left, right) + } + return right._tag === "Boolean" + ? combinedAnnotations( + { + _tag: "Boolean", + checks: [...left.checks, ...right.checks] + }, + left, + right + ) + : never + case "Literal": + if (right._tag === "Literal") { + return left.literal === right.literal + ? combinedAnnotations( + { + ...left, + checks: [...left.checks, ...right.checks] + }, + left, + right + ) + : never + } + if ( + (right._tag === "String" || right._tag === "Number") && satisfiesLiteral(right, left) || + right._tag === "Boolean" && typeof left.literal === "boolean" + ) { + return combinedAnnotations(left, left, right) + } + return never + case "Arrays": { + if (right._tag !== "Arrays") return never + const arrays = combineArrays(left, right, path) + if (arrays === undefined) return never + const arrayChecks = combineChecks(left.checks, right.checks, right.annotations, ["effect/schema/isUnique"]) + return annotate( + { + _tag: "Arrays", + elements: arrays.elements, + rest: arrays.rest, + checks: arrayChecks ?? left.checks + }, + mergeAnnotations(left.annotations, arrayChecks === undefined ? right.annotations : undefined) + ) + } + case "Objects": { + if (right._tag !== "Objects") return never + const objectChecks = combineChecks(left.checks, right.checks, right.annotations) + return annotate( + { + _tag: "Objects", + propertySignatures: combineProperties(left.propertySignatures, right.propertySignatures, path), + indexSignatures: combineIndexSignatures(left.indexSignatures, right.indexSignatures, path), + checks: objectChecks ?? left.checks + }, + mergeAnnotations(left.annotations, objectChecks === undefined ? right.annotations : undefined) + ) + } + } + } + + function enter(input: unknown): JsonSchema.JsonSchema | undefined { + if (typeof input !== "object" || input === null || Array.isArray(input)) { + return undefined + } + const schema = input as JsonSchema.JsonSchema + return options?.onEnter === undefined ? schema : options.onEnter(schema) + } + + function recur(input: unknown, path: Path): ImportedJsonSchemaRepresentation { + if (input === false) { + return never + } + const schema = enter(input) + if (schema === undefined) { + return unknown + } + + let representation = on(schema, path) + if (representation._tag === "Reference") { + const siblingSchema: JsonSchema.JsonSchema = { ...schema, $ref: undefined } + for (const key of InternalAnnotations.jsonSchemaAnnotationKeys) { + delete siblingSchema[key] + } + const sibling = on(siblingSchema, path) + if (sibling._tag !== "Unknown") { + const reference = representation + representation = combine( + resolveReference(reference, path, { + recursiveReferenceError: `Unsupported assertion siblings on recursive reference ${reference.$ref}` + }), + sibling, + path + ) + } + } + const annotations = jsonSchemaAnnotations(schema) + if (annotations !== undefined && representation._tag === "Reference") { + annotatedReferences.push({ reference: representation, path }) + } + representation = annotate(representation, annotations) + + if (Array.isArray(schema.allOf)) { + for (let index = 0; index < schema.allOf.length; index++) { + representation = combine( + representation, + recur(schema.allOf[index], [...path, "allOf", index]), + [...path, "allOf", index] + ) + } + } + + for (const mode of ["anyOf", "oneOf"] as const) { + const members = schema[mode] + if (Array.isArray(members)) { + const union: ImportedJsonSchemaRepresentation = { + _tag: "Union", + types: members.map((member, index) => recur(member, [...path, mode, index])), + mode, + checks: [] + } + representation = combine(union, representation, [...path, mode]) + } + } + return representation + } + + function on(schema: JsonSchema.JsonSchema, path: Path): ImportedJsonSchemaRepresentation { + if (typeof schema.$ref === "string") { + const $ref = jsonSchemaReferenceKey(schema.$ref) + if ($ref !== undefined) { + if (!reachableDefinitions.has($ref)) reachableDefinitions.set($ref, path) + return { _tag: "Reference", $ref } + } + } + if (Object.hasOwn(schema, "const")) { + if (schema.const === null) { + return { _tag: "Null", checks: [] } + } + if (typeof schema.const === "string" || typeof schema.const === "number" || typeof schema.const === "boolean") { + return makeLiteral(schema.const) + } + } + if (Array.isArray(schema.enum)) { + const types: Array = [] + for (let index = 0; index < schema.enum.length; index++) { + const value = schema.enum[index] + if (value === null) { + types.push({ _tag: "Null", checks: [] }) + } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + types.push(makeLiteral(value)) + } else { + types.push(recur(value, [...path, "enum", index])) + } + } + if (types.length === 1) { + return types[0] + } + return { _tag: "Union", types, mode: "anyOf", checks: [] } + } + + if (Array.isArray(schema.type) && schema.type.every(isImportedJsonSchemaType)) { + return { + _tag: "Union", + types: schema.type.map((type) => on({ ...schema, type }, path)), + mode: "anyOf", + checks: [] + } + } + + const type = isImportedJsonSchemaType(schema.type) ? schema.type : inferJsonSchemaType(schema) + switch (type) { + case "null": + return { _tag: "Null", checks: [] } + case "string": + return { + _tag: "String", + checks: collectStringChecks(schema, path) + } + case "number": + case "integer": + return { + _tag: "Number", + checks: [ + jsonSchemaFilter(type === "number" ? "effect/schema/isFinite" : "effect/schema/isInt", null), + ...collectNumberChecks(schema) + ] + } + case "boolean": + return { _tag: "Boolean", checks: [] } + case "array": { + const prefixItems = Array.isArray(schema.prefixItems) ? schema.prefixItems : undefined + const minItems = typeof schema.minItems === "number" ? schema.minItems : 0 + const elements = prefixItems?.map((element, index) => ({ + isOptional: index + 1 > minItems, + type: recur(element, [...path, "prefixItems", index]) + })) ?? [] + const isTupleClosed = schema.items === false || + (schema.items === undefined && + prefixItems !== undefined && + schema.maxItems === prefixItems.length) + const isMaxItemsRedundant = isTupleClosed && + typeof schema.maxItems === "number" && + schema.maxItems >= elements.length + return { + _tag: "Arrays", + elements, + rest: isTupleClosed + ? [] + : [schema.items === undefined ? unknown : recur(schema.items, [...path, "items"])], + checks: collectArrayChecks(schema, isMaxItemsRedundant) + } + } + case "object": + return { + _tag: "Objects", + propertySignatures: collectProperties(schema, path), + indexSignatures: collectIndexSignatures(schema, path), + checks: collectObjectChecks(schema, path) + } + default: + return unknown + } + } + + function importPatternChecks(pattern: string, path: Path): Array { + switch (options?.patterns ?? "error") { + case "error": + throw errorWithPath(`Pattern encountered while patterns is set to "error"`, path) + case "ignore": + return [] + case "apply": + return [jsonSchemaFilter("effect/schema/isPattern", { source: pattern, flags: "" })] + } + } + + function collectStringChecks(schema: JsonSchema.JsonSchema, path: Path): Array { + const checks: Array = [] + addNumberCheck(checks, schema.minLength, "effect/schema/isMinLength", "minLength") + addNumberCheck(checks, schema.maxLength, "effect/schema/isMaxLength", "maxLength") + if (typeof schema.pattern === "string") { + checks.push(...importPatternChecks(schema.pattern, [...path, "pattern"])) + } + return checks + } + + function collectNumberChecks(schema: JsonSchema.JsonSchema): Array { + const checks: Array = [] + addNumberCheck(checks, schema.minimum, "effect/schema/isGreaterThanOrEqualTo", "minimum") + addNumberCheck(checks, schema.maximum, "effect/schema/isLessThanOrEqualTo", "maximum") + addNumberCheck(checks, schema.exclusiveMinimum, "effect/schema/isGreaterThan", "exclusiveMinimum") + addNumberCheck(checks, schema.exclusiveMaximum, "effect/schema/isLessThan", "exclusiveMaximum") + addNumberCheck(checks, schema.multipleOf, "effect/schema/isMultipleOf", "divisor") + return checks + } + + function collectArrayChecks(schema: JsonSchema.JsonSchema, isMaxItemsRedundant: boolean): Array { + const checks: Array = [] + if (schema.prefixItems === undefined) { + addNumberCheck(checks, schema.minItems, "effect/schema/isMinLength", "minLength") + } + if (!isMaxItemsRedundant) { + addNumberCheck(checks, schema.maxItems, "effect/schema/isMaxLength", "maxLength") + } + if (schema.uniqueItems === true) { + checks.push(jsonSchemaFilter("effect/schema/isUnique", null)) + } + return checks + } + + function collectProperties( + schema: JsonSchema.JsonSchema, + path: Path + ): Array { + const properties = + typeof schema.properties === "object" && schema.properties !== null && !Array.isArray(schema.properties) + ? schema.properties as Record + : {} + const required = Array.isArray(schema.required) + ? schema.required.filter((key): key is string => typeof key === "string") + : [] + const keys = new Set([...Object.keys(properties), ...required]) + return Array.from(keys, (name) => ({ + name, + type: recur(properties[name], [...path, "properties", name]), + isOptional: !required.includes(name), + isMutable: false + })) + } + + function collectIndexSignatures( + schema: JsonSchema.JsonSchema, + path: Path + ): Array { + const signatures: Array = [] + if ( + typeof schema.patternProperties === "object" && + schema.patternProperties !== null && + !Array.isArray(schema.patternProperties) + ) { + for (const [pattern, value] of Object.entries(schema.patternProperties)) { + const checks = importPatternChecks(pattern, [...path, "patternProperties", pattern]) + if (checks.length === 0) return [{ parameter: string, type: unknown }] + signatures.push({ + parameter: { + _tag: "String", + checks + }, + type: recur(value, [...path, "patternProperties", pattern]) + }) + } + } + if (schema.additionalProperties === undefined || schema.additionalProperties === true) { + signatures.push({ + parameter: string, + type: unknown + }) + } else if (typeof schema.additionalProperties === "object" && schema.additionalProperties !== null) { + signatures.push({ + parameter: string, + type: recur(schema.additionalProperties, [...path, "additionalProperties"]) + }) + } + return signatures + } + + function collectObjectChecks( + schema: JsonSchema.JsonSchema, + path: Path + ): Array { + const checks: Array = [] + addNumberCheck(checks, schema.minProperties, "effect/schema/isMinProperties", "minProperties") + addNumberCheck(checks, schema.maxProperties, "effect/schema/isMaxProperties", "maxProperties") + if (schema.propertyNames !== undefined) { + checks.push(jsonSchemaFilter( + "effect/schema/isPropertyNames", + null, + [recur(schema.propertyNames, [...path, "propertyNames"])] + )) + } + return checks + } + + const references: Record = {} + const representations = document.schemas.map((schema, index) => + unknownJsonSchemas(recur(schema, singleRoot ? ["schema"] : ["schemas", index])) + ) as [Representation, ...Array] + for (const [key, path] of reachableDefinitions) { + InternalRecord.assignProperty(references, key, unknownJsonSchemas(translateDefinition(key, path))) + } + for (const { reference, path } of annotatedReferences) { + resolveReference(reference, path) + } + return { representations, references } +} + +/** @internal */ +function toRepresentation( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions +): SchemaRepresentation.Document { + const translated = translateJsonSchemaMultiDocument( + { + dialect: document.dialect, + schemas: [document.schema], + definitions: document.definitions + }, + options, + true + ) + return { + representation: translated.representations[0], + references: translated.references + } +} + +const jsonSchemaRevivers: ReadonlyArray = [ + Schema.JsonReviver, + Schema.isPatternReviver, + Schema.isFiniteReviver, + Schema.isGreaterThanReviver, + Schema.isGreaterThanOrEqualToReviver, + Schema.isLessThanReviver, + Schema.isLessThanOrEqualToReviver, + Schema.isMultipleOfReviver, + Schema.isIntReviver, + Schema.isMinLengthReviver, + Schema.isMaxLengthReviver, + Schema.isMinPropertiesReviver, + Schema.isMaxPropertiesReviver, + Schema.isPropertyNamesReviver, + Schema.isUniqueReviver +] + +/** @internal */ +export function fromJsonSchemaDocument( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions +): Schema.Top { + return fromRepresentation(toRepresentation(document, options), jsonSchemaRevivers) +} + +/** @internal */ +export function fromJsonSchemaMultiDocument( + document: JsonSchema.MultiDocument<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions +): readonly [Schema.Top, ...Array] { + return fromRepresentations(translateJsonSchemaMultiDocument(document, options), jsonSchemaRevivers) +} diff --git a/.context/effect/packages/effect/src/internal/schema/fromRepresentation.ts b/.context/effect/packages/effect/src/internal/schema/fromRepresentation.ts new file mode 100644 index 000000000..30b647beb --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/fromRepresentation.ts @@ -0,0 +1,339 @@ +import * as Arr from "../../Array.ts" +import * as Result from "../../Result.ts" +import * as Schema from "../../Schema.ts" +import type * as SchemaAST from "../../SchemaAST.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" +import { errorWithPath } from "../errors.ts" +import * as InternalRecord from "../record.ts" + +type Path = ReadonlyArray + +/** @internal */ +export function fromRepresentations( + document: SchemaRepresentation.MultiDocument, + revivers: ReadonlyArray +): readonly [Schema.Top, ...Array] { + return revivePersisted(document.representations, document.references, makeReviverMap(revivers), false) +} + +class ReferenceSlot { + body: Schema.Top | undefined + resolving = false + readonly wrapper: Schema.Top + + constructor(key: string) { + this.wrapper = Schema.suspend(() => { + if (this.body === undefined) { + throw new Error(`Reference ${key} was evaluated before it was resolved`) + } + return this.body + }) + } +} + +function makeReviverMap( + revivers: ReadonlyArray +): Map { + const out = new Map() + + for (let index = 0; index < revivers.length; index++) { + const reviver = revivers[index] + if (out.has(reviver.id)) { + throw errorWithPath(`Duplicate reviver for ${reviver.id}`, ["revivers", index, "id"]) + } + out.set(reviver.id, { + ...reviver, + payloadSchema: Schema.toCodecJson(reviver.payloadSchema) + }) + } + + return out +} + +function revivePersisted( + representations: readonly [ + SchemaRepresentation.Representation, + ...Array + ], + references: SchemaRepresentation.References, + reviverMap: ReadonlyMap, + singleRoot: boolean +): readonly [Schema.Top, ...Array] { + const slots = new Map() + + function resolveReference(key: string, path: Path): Schema.Top { + let slot = slots.get(key) + if (slot === undefined) { + if (!Object.hasOwn(references, key)) { + throw errorWithPath(`Invalid reference ${key}`, [...path, "$ref"]) + } + slot = new ReferenceSlot(key) + slots.set(key, slot) + } + if (slot.body !== undefined) { + return slot.body + } + if (slot.resolving) { + return slot.wrapper + } + + slot.resolving = true + try { + slot.body = annotate(recur(references[key], ["references", key]), { identifier: key }) + return slot.body + } finally { + slot.resolving = false + } + } + + function resolveReviver( + representation: SchemaRepresentation.RepresentationAnnotation, + path: Path + ): R { + const reviver = reviverMap.get(representation.id) + if (reviver === undefined) { + throw errorWithPath(`Missing reviver for ${representation.id}`, path) + } + return reviver as R + } + + function decodePayload( + representation: SchemaRepresentation.RepresentationAnnotation, + reviver: SchemaRepresentation.AnyReviver, + path: Path + ): any { + const decoded = Schema.decodeUnknownResult(reviver.payloadSchema)(representation.payload) + if (Result.isFailure(decoded)) { + throw errorWithPath(`Invalid representation payload for ${representation.id}`, path) + } + return decoded.success + } + + function reviveSchemas( + representations: ReadonlyArray, + path: Path + ): ReadonlyArray { + return representations.map((representation, index) => recur(representation, [...path, index])) + } + + function reviveDeclaration( + declaration: SchemaRepresentation.Declaration, + path: Path + ): Schema.Top { + const representationPath = [...path, "representation"] + const representation = declaration.representation + if (representation === undefined) { + throw errorWithPath("Missing representation annotation", representationPath) + } + const reviver = resolveReviver>(representation, representationPath) + const payload = decodePayload(representation, reviver, [...representationPath, "payload"]) + const typeParameters = reviveSchemas(declaration.typeParameters, [...path, "typeParameters"]) + const schema = reviver.revive({ payload, typeParameters, annotations: declaration.annotations }) + return appendChecks(schema, declaration.checks, [...path, "checks"]) + } + + function reviveFilter( + filter: SchemaRepresentation.Filter, + path: Path + ): SchemaAST.Filter { + const representationPath = [...path, "representation"] + const representation = filter.representation + if (representation === undefined) { + throw errorWithPath("Missing representation annotation", representationPath) + } + const reviver = resolveReviver>(representation, representationPath) + const payload = decodePayload(representation, reviver, [...representationPath, "payload"]) + const schemas = reviveSchemas(representation.schemas ?? [], [...representationPath, "schemas"]) + const check = reviver.revive({ payload, schemas, annotations: filter.annotations }) + return filter.aborted ? check.abort() : check + } + + function reviveFilterGroup( + group: SchemaRepresentation.FilterGroup, + path: Path + ): SchemaAST.FilterGroup { + const representationPath = [...path, "representation"] + const representation = group.representation + if (representation === undefined) { + const checks = group.checks.map((check, index) => reviveCheck(check, [...path, "checks", index])) + return Schema.makeFilterGroup( + checks as [SchemaAST.Check, ...Array>], + group.annotations as Schema.Annotations.Filter | undefined + ) + } + + const reviver = resolveReviver>(representation, representationPath) + const payload = decodePayload(representation, reviver, [...representationPath, "payload"]) + const schemas = reviveSchemas(representation.schemas ?? [], [...representationPath, "schemas"]) + return reviver.revive({ payload, schemas, annotations: group.annotations }) + } + + function reviveCheck( + check: SchemaRepresentation.Check, + path: Path + ): SchemaAST.Check { + return check._tag === "Filter" + ? reviveFilter(check, path) + : reviveFilterGroup(check, path) + } + + function appendChecks( + schema: S, + checks: ReadonlyArray, + path: Path + ): S["Rebuild"] { + const revived = checks.map((check, index) => reviveCheck(check, [...path, index])) + return Arr.isArrayNonEmpty(revived) ? schema.check(...revived) : schema as S["Rebuild"] + } + + function annotate( + schema: Schema.Top, + annotations: Schema.Annotations.Annotations | undefined + ): Schema.Top { + return annotations === undefined ? schema : schema.annotate(annotations) + } + + function finishStructural( + schema: Schema.Top, + representation: Exclude, + path: Path + ): Schema.Top { + return appendChecks( + annotate(schema, representation.annotations), + representation.checks, + [...path, "checks"] + ) + } + + function recur( + representation: SchemaRepresentation.Representation, + path: Path + ): Schema.Top { + switch (representation._tag) { + case "Reference": + return resolveReference(representation.$ref, path) + case "Declaration": + return reviveDeclaration(representation, path) + case "Suspend": { + const thunkPath = [...path, "thunk"] + if (representation.thunk._tag === "Reference") { + const key = representation.thunk.$ref + resolveReference(key, thunkPath) + const slot = slots.get(key)! + return annotate(slot.wrapper, representation.annotations) + } + const thunk = recur(representation.thunk, thunkPath) + return annotate(Schema.suspend(() => thunk), representation.annotations) + } + case "Null": + return finishStructural(Schema.Null, representation, path) + case "Undefined": + return finishStructural(Schema.Undefined, representation, path) + case "Void": + return finishStructural(Schema.Void, representation, path) + case "Never": + return finishStructural(Schema.Never, representation, path) + case "Unknown": + return finishStructural(Schema.Unknown, representation, path) + case "Any": + return finishStructural(Schema.Any, representation, path) + case "String": + return finishStructural(Schema.String, representation, path) + case "Number": + return finishStructural(Schema.Number, representation, path) + case "Boolean": + return finishStructural(Schema.Boolean, representation, path) + case "BigInt": + return finishStructural(Schema.BigInt, representation, path) + case "Symbol": + return finishStructural(Schema.Symbol, representation, path) + case "Literal": + return finishStructural(Schema.Literal(representation.literal), representation, path) + case "UniqueSymbol": + return finishStructural(Schema.UniqueSymbol(representation.symbol), representation, path) + case "ObjectKeyword": + return finishStructural(Schema.ObjectKeyword, representation, path) + case "Enum": + return finishStructural( + Schema.Enum(Object.fromEntries(representation.enums)), + representation, + path + ) + case "TemplateLiteral": { + const parts = representation.parts.map((part, index) => recur(part, [...path, "parts", index])) + return finishStructural( + Schema.TemplateLiteral(parts as unknown as Schema.TemplateLiteral.Parts), + representation, + path + ) + } + case "Arrays": { + const elements = representation.elements.map((element, index) => { + let schema = recur(element.type, [...path, "elements", index, "type"]) + if (element.annotations !== undefined) { + schema = schema.annotateKey(element.annotations as Schema.Annotations.Key) + } + return element.isOptional ? Schema.optionalKey(schema) : schema + }) + const rest = representation.rest.map((item, index) => recur(item, [...path, "rest", index])) + const schema = Arr.isArrayNonEmpty(rest) + ? elements.length === 0 && rest.length === 1 + ? Schema.Array(rest[0]) + : Schema.TupleWithRest(Schema.Tuple(elements), rest) + : Schema.Tuple(elements) + return finishStructural(schema, representation, path) + } + case "Objects": { + const fields: Record = {} + for (let index = 0; index < representation.propertySignatures.length; index++) { + const property = representation.propertySignatures[index] + let schema = recur(property.type, [...path, "propertySignatures", index, "type"]) + if (property.annotations !== undefined) { + schema = schema.annotateKey(property.annotations as Schema.Annotations.Key) + } + if (property.isOptional) { + schema = Schema.optionalKey(schema) + } + if (property.isMutable) { + schema = Schema.mutableKey(schema) + } + InternalRecord.assignProperty(fields, property.name, schema) + } + const records = representation.indexSignatures.map((indexSignature, index) => + Schema.Record( + recur(indexSignature.parameter, [...path, "indexSignatures", index, "parameter"]) as Schema.Record.Key, + recur(indexSignature.type, [...path, "indexSignatures", index, "type"]) + ) + ) + const schema = Arr.isArrayNonEmpty(records) + ? representation.propertySignatures.length === 0 && records.length === 1 + ? records[0] + : Schema.StructWithRest(Schema.Struct(fields), records) + : Schema.Struct(fields) + return finishStructural(schema, representation, path) + } + case "Union": { + const members = representation.types.map((member, index) => recur(member, [...path, "types", index])) + return finishStructural(Schema.Union(members, { mode: representation.mode }), representation, path) + } + } + } + + const schemas = representations.map((representation, index) => + recur(representation, singleRoot ? ["representation"] : ["representations", index]) + ) as [Schema.Top, ...Array] + return schemas +} + +/** @internal */ +export function fromRepresentation( + document: SchemaRepresentation.Document, + revivers: ReadonlyArray +): Schema.Top { + return revivePersisted( + [document.representation], + document.references, + makeReviverMap(revivers), + true + )[0] +} diff --git a/.context/effect/packages/effect/src/internal/schema/parser.ts b/.context/effect/packages/effect/src/internal/schema/parser.ts new file mode 100644 index 000000000..77bd15057 --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/parser.ts @@ -0,0 +1,22 @@ +import * as Exit from "../../Exit.ts" +import * as Option from "../../Option.ts" +import { args } from "../core.ts" + +export const missing = Symbol() + +export { args } + +export type Success = Exit.Success & { readonly [args]: A } + +export const succeed = Exit.succeed as (value: A) => Success + +export const missingExit = succeed(missing) + +// Shared success for a present input returned unchanged. It must be resolved +// before crossing an asynchronous or transformation boundary. +export const sameExit: Success = succeed(missing) + +export const toOption = (value: A): Option.Option => value === missing ? Option.none() : Option.some(value as A) + +export const fromOptionExit = (option: Option.Option): Success => + option._tag === "None" ? missingExit : succeed(option.value) diff --git a/.context/effect/packages/effect/src/internal/schema/representation.ts b/.context/effect/packages/effect/src/internal/schema/representation.ts deleted file mode 100644 index b63ea2150..000000000 --- a/.context/effect/packages/effect/src/internal/schema/representation.ts +++ /dev/null @@ -1,795 +0,0 @@ -import * as Arr from "../../Array.ts" -import * as Equal from "../../Equal.ts" -import { format } from "../../Formatter.ts" -import { escapeToken } from "../../JsonPointer.ts" -import type * as JsonSchema from "../../JsonSchema.ts" -import * as Predicate from "../../Predicate.ts" -import * as Rec from "../../Record.ts" -import * as RegEx from "../../RegExp.ts" -import type * as Schema from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" -import * as InternalAnnotations from "./annotations.ts" -import * as InternalSchema from "./schema.ts" - -/** @internal */ -export function fromAST(ast: SchemaAST.AST): SchemaRepresentation.Document { - const { references, representations: schemas } = fromASTs([ast]) - return { representation: schemas[0], references } -} - -/** @internal */ -export function fromASTs(asts: readonly [SchemaAST.AST, ...Array]): SchemaRepresentation.MultiDocument { - const references: Record = {} - - const referenceMap = new Map() - const uniqueReferences = new Set() - const visiting = new Set() - - const schemas = Arr.map(asts, (ast) => recur(ast)) - - return { - representations: schemas, - references - } - - function gen(prefix: string): string { - let candidate = prefix - let suffix = 0 - - while (uniqueReferences.has(candidate)) { - candidate = `${prefix}${++suffix}` - } - - uniqueReferences.add(candidate) - return candidate - } - - function recur(ast: SchemaAST.AST, prefix?: string): SchemaRepresentation.Representation { - const found = referenceMap.get(ast) - if (found !== undefined) { - return { _tag: "Reference", $ref: found } - } - - const last = SchemaAST.getLastEncoding(ast) - const identifier = InternalAnnotations.resolveIdentifier(ast) ?? prefix - - if (ast !== last) { - return recur(last, identifier) - } - - // Has identifier → always create reference - if (identifier !== undefined) { - const reference = gen(identifier) - referenceMap.set(ast, reference) - const out = on(ast) - const found = references[identifier] - // Reuse existing references when duplicate identifiers have the same representation - if (found !== undefined && Equal.equals(out, found)) { - referenceMap.set(ast, identifier) - return { _tag: "Reference", $ref: identifier } - } - references[reference] = out - return { _tag: "Reference", $ref: reference } - } - - // Recursion detected → create reference - if (visiting.has(ast)) { - const reference = gen(`${ast._tag}_`) - referenceMap.set(ast, reference) - return { _tag: "Reference", $ref: reference } - } - - // Normal case → inline - visiting.add(ast) - const out = on(ast) - visiting.delete(ast) - - // A descendant triggered reference creation (recursion) - const ref = referenceMap.get(ast) - if (ref !== undefined) { - references[ref] = out - return { _tag: "Reference", $ref: ref } - } - - return out - } - - function getEncodedSchema(last: SchemaAST.Declaration): SchemaAST.Declaration | SchemaAST.Null { - const getLink = last.annotations?.toCodecJson ?? last.annotations?.toCodec - if (Predicate.isFunction(getLink)) { - return SchemaAST.replaceEncoding(last, [ - getLink(last.typeParameters.map((tp) => InternalSchema.make(SchemaAST.toEncoded(tp)))) - ]) - } - return SchemaAST.null - } - - function on(last: SchemaAST.AST): SchemaRepresentation.Representation { - const annotations = fromASTAnnotations(last.annotations) - switch (last._tag) { - case "Declaration": { - // this must be executed before transforming the type parameters - const encodedSchema = recur(getEncodedSchema(last)) - return { - _tag: "Declaration", - typeParameters: last.typeParameters.map((ast) => recur(ast)), - encodedSchema, - checks: fromASTChecks(last.checks), - ...annotations - } - } - case "Null": - case "Undefined": - case "Void": - case "Never": - case "Unknown": - case "Any": - case "Boolean": - case "Symbol": - case "ObjectKeyword": - return { _tag: last._tag, ...annotations } - case "String": { - const contentMediaType = last.annotations?.contentMediaType - const contentSchema = last.annotations?.contentSchema - return { - _tag: last._tag, - checks: fromASTChecks(last.checks), - ...annotations, - ...(typeof contentMediaType === "string" && SchemaAST.isAST(contentSchema) - ? { contentSchema: recur(contentSchema) } - : undefined) - } - } - case "Number": - case "BigInt": - return { - _tag: last._tag, - checks: fromASTChecks(last.checks), - ...annotations - } - case "Literal": - return { - _tag: last._tag, - literal: last.literal, - ...annotations - } - case "UniqueSymbol": - return { - _tag: last._tag, - symbol: last.symbol, - ...annotations - } - case "Enum": - return { - _tag: last._tag, - enums: last.enums, - ...annotations - } - case "TemplateLiteral": - return { - _tag: last._tag, - parts: last.parts.map((ast) => recur(ast)), - ...annotations - } - case "Arrays": - return { - _tag: last._tag, - elements: last.elements.map((e) => { - const last = SchemaAST.getLastEncoding(e) - return { - isOptional: SchemaAST.isOptional(last), - type: recur(e), - ...fromASTAnnotations(last.context?.annotations) - } - }), - rest: last.rest.map((ast) => recur(ast)), - checks: fromASTChecks(last.checks), - ...annotations - } - case "Objects": - return { - _tag: last._tag, - propertySignatures: last.propertySignatures.map((ps) => { - const last = SchemaAST.getLastEncoding(ps.type) - return { - name: ps.name, - type: recur(ps.type), - isOptional: SchemaAST.isOptional(last), - isMutable: SchemaAST.isMutable(last), - ...fromASTAnnotations(last.context?.annotations) - } - }), - indexSignatures: last.indexSignatures.map((is) => ({ - parameter: recur(is.parameter), - type: recur(is.type) - })), - checks: fromASTChecks(last.checks), - ...annotations - } - case "Union": { - const types = InternalSchema.jsonReorder(last.types) - return { - _tag: last._tag, - types: types.map((ast) => recur(ast)), - mode: last.mode, - ...annotations - } - } - case "Suspend": { - return { - _tag: "Suspend", - checks: [], - thunk: recur(last.thunk()), - ...annotations - } - } - } - } - - function fromASTChecks( - checks: readonly [SchemaAST.Check, ...Array>] | undefined - ): Array> { - if (!checks) return [] - return checks.map(getCheck).filter((c) => c !== undefined) - - function getCheck(c: SchemaAST.Check): SchemaRepresentation.Check | undefined { - switch (c._tag) { - case "Filter": { - const meta = c.annotations?.meta - if (meta) { - return { - _tag: "Filter", - meta: meta._tag === "isPropertyNames" - ? { - _tag: "isPropertyNames", - propertyNames: recur(meta.propertyNames) - } - : meta, - ...fromASTAnnotations(c.annotations) - } - } - return undefined - } - case "FilterGroup": { - const checks = fromASTChecks(c.checks) - if (Arr.isArrayNonEmpty(checks)) { - return { - _tag: "FilterGroup", - checks, - ...fromASTAnnotations(c.annotations) - } - } - } - } - } - } -} - -/** @internal */ -export const fromASTBlacklist: Set = new Set([ - // `expected` is preserved because is useful to generate descriptions in JSON Schemas - "~structural", - "~sentinels", - "meta", - "arbitrary", - "toArbitrary", - "toEquivalence", - "toFormatter", - "toCodec", - "toCodecJson", - "toCodecIso", - SchemaAST.ClassTypeId -]) - -const standardJsonSchemaAnnotationKeys: ReadonlySet = new Set([ - "title", - "description", - "default", - "examples", - "readOnly", - "writeOnly", - "format", - "contentEncoding", - "contentMediaType", - "contentSchema" -]) - -function fromASTAnnotations( - annotations: Schema.Annotations.Annotations | undefined -): { annotations: Schema.Annotations.Annotations } | undefined { - if (annotations !== undefined) { - const filtered = Rec.filter(annotations, (_, k) => !fromASTBlacklist.has(k)) - if (!Rec.isEmptyRecord(filtered)) { - return { annotations: filtered } - } - } - return undefined -} - -/** @internal */ -export function toJsonSchemaDocument( - document: SchemaRepresentation.Document, - options?: Schema.ToJsonSchemaOptions -): JsonSchema.Document<"draft-2020-12"> { - const { definitions, dialect: source, schemas } = toJsonSchemaMultiDocument({ - representations: [document.representation], - references: document.references - }, options) - const schema = schemas[0] - return { dialect: source, schema, definitions } -} - -/** @internal */ -export function toJsonSchemaMultiDocument( - multiDocument: SchemaRepresentation.MultiDocument, - options?: Schema.ToJsonSchemaOptions -): JsonSchema.MultiDocument<"draft-2020-12"> { - const generateDescriptions = options?.generateDescriptions ?? false - const additionalProperties = options?.additionalProperties ?? false - const includeAnnotationKey = options?.includeAnnotationKey - - const definitions = Rec.map(multiDocument.references, (d) => recur(d)) - - return { - dialect: "draft-2020-12", - schemas: Arr.map(multiDocument.representations, (s) => recur(s)), - definitions - } - - function recur(s: SchemaRepresentation.Representation): JsonSchema.JsonSchema { - let js: JsonSchema.JsonSchema = on(s) - if ("annotations" in s) { - const a = collectJsonSchemaAnnotations(s.annotations) - if (a) { - js = { ...js, ...a } - } - } - if ("checks" in s) { - const checks = collectJsonSchemaChecks(s.checks, js.type) - for (const check of checks) { - js = appendJsonSchema(js, check) - } - } - return js - } - - function on(schema: SchemaRepresentation.Representation): JsonSchema.JsonSchema { - switch (schema._tag) { - case "Any": - case "Unknown": - return {} - case "ObjectKeyword": - return { anyOf: [{ type: "object" }, { type: "array" }] } - case "Void": - case "Undefined": - return { type: "null" } - case "BigInt": - return { - "type": "string", - "allOf": [ - { "pattern": "^-?\\d+$" } - ] - } - case "Symbol": - case "UniqueSymbol": - return { - "type": "string", - "allOf": [ - { "pattern": "^Symbol\\((.*)\\)$" } - ] - } - case "Declaration": - return recur(schema.encodedSchema) - case "Suspend": - return recur(schema.thunk) - case "Reference": - return { $ref: `#/$defs/${escapeToken(schema.$ref)}` } - case "Null": - return { type: "null" } - case "Never": - return { not: {} } - case "String": { - const out: JsonSchema.JsonSchema = { type: "string" } - if (schema.contentMediaType !== undefined) { - out.contentMediaType = schema.contentMediaType - } - if (schema.contentSchema !== undefined) { - out.contentSchema = recur(schema.contentSchema) - } - return out - } - case "Number": - return hasCheck(schema.checks, "isInt") ? - { type: "integer" } : - hasCheck(schema.checks, "isFinite") ? - { type: "number" } : - { - "anyOf": [ - { type: "number" }, - { type: "string", enum: ["NaN"] }, - { type: "string", enum: ["Infinity"] }, - { type: "string", enum: ["-Infinity"] } - ] - } - case "Boolean": - return { type: "boolean" } - case "Literal": { - const literal = schema.literal - if (typeof literal === "string") { - return { type: "string", enum: [literal] } - } - if (typeof literal === "number") { - return { type: "number", enum: [literal] } - } - if (typeof literal === "boolean") { - return { type: "boolean", enum: [literal] } - } - // bigint literals are not supported - return { type: "string", enum: [String(literal)] } - } - case "Enum": { - return recur({ - _tag: "Union", - types: schema.enums.map(([title, value]) => ({ - _tag: "Literal", - literal: value, - annotations: { title } - })), - mode: "anyOf", - annotations: schema.annotations - }) - } - case "TemplateLiteral": { - const pattern = schema.parts.map(getPartPattern).join("") - return { type: "string", pattern: `^${pattern}$` } - } - case "Arrays": { - // --------------------------------------------- - // handle post rest elements - // --------------------------------------------- - if (schema.rest.length > 1) { - throw new globalThis.Error("Generating a JSON Schema for post-rest elements is not supported") - } - const out: JsonSchema.JsonSchema = { type: "array" } - let minItems = schema.elements.length - const prefixItems: Array = schema.elements.map((e) => { - if (e.isOptional) { - minItems-- - } - const v = recur(e.type) - const a = collectJsonSchemaAnnotations(e.annotations) - return a ? appendJsonSchema(v, a) : v - }) - if (prefixItems.length > 0) { - out.prefixItems = prefixItems - out.maxItems = schema.elements.length - if (minItems > 0) { - out.minItems = minItems - } - } else { - out.items = false - } - if (schema.rest.length > 0) { - delete out.maxItems - const rest = recur(schema.rest[0]) - if (Object.keys(rest).length > 0) { - out.items = rest - } else { - delete out.items - } - } - return out - } - case "Objects": { - if (schema.propertySignatures.length === 0 && schema.indexSignatures.length === 0) { - return { anyOf: [{ type: "object" }, { type: "array" }] } - } - const out: JsonSchema.JsonSchema = { type: "object" } - const properties: Record = {} - const required: Array = [] - - for (const ps of schema.propertySignatures) { - const name = ps.name - if (typeof name !== "string") { - throw new globalThis.Error(`Unsupported property signature name: ${format(name)}`) - } - const v = recur(ps.type) - const a = collectJsonSchemaAnnotations(ps.annotations) - properties[name] = a ? appendJsonSchema(v, a) : v - // Property is required only if it's not explicitly optional AND doesn't contain Undefined - if (!ps.isOptional) { - required.push(name) - } - } - - if (Object.keys(properties).length > 0) { - out.properties = properties - } - if (required.length > 0) { - out.required = required - } - - out.additionalProperties = additionalProperties - const patternProperties: Record = {} - // Handle index signatures - for (const is of schema.indexSignatures) { - let type: JsonSchema.JsonSchema | false = recur(is.type) - // Collapse unannotated Never ({ not: {} }) to false, but keep annotated schemas as objects. - if (Object.keys(type).length === 1 && "not" in type) { - type = false - } - const patterns = getParameterPatterns(is.parameter) - if (patterns.length > 0) { - for (const pattern of patterns) { - patternProperties[pattern] = type - } - } else { - out.additionalProperties = type - } - } - if (Object.keys(patternProperties).length > 0) { - out.patternProperties = patternProperties - delete out.additionalProperties - } - if (Predicate.isObject(out.additionalProperties) && Rec.isEmptyRecord(out.additionalProperties)) { - delete out.additionalProperties - } - - return out - } - case "Union": { - const types = schema.types.map(recur) - if (types.length === 0) { - // anyOf MUST be a non-empty array - return { not: {} } - } - if (types.length > 1) { - const compacted = compactEnums(types) - if (compacted) return compacted - } - return schema.mode === "anyOf" ? { anyOf: types } : { oneOf: types } - } - } - } - - // Collapses [{type:"string",enum:["a"]},{type:"string",enum:["b"]}] into {type:"string",enum:["a","b"]}. - // Returns undefined if members have different types, extra keys (e.g. title), or empty enums. - function compactEnums( - types: ReadonlyArray - ): JsonSchema.JsonSchema | undefined { - let sharedType: string | undefined - const values: Array = [] - for (const t of types) { - const keys = Object.keys(t) - if (keys.length !== 2 || t.type === undefined || !Array.isArray(t.enum) || t.enum.length === 0) { - return undefined - } - if (sharedType === undefined) { - sharedType = t.type as string - } else if (t.type !== sharedType) { - return undefined - } - for (const v of t.enum) { - values.push(v) - } - } - return { type: sharedType, enum: values } - } - - function collectJsonSchemaAnnotations( - annotations: Schema.Annotations.Annotations | undefined - ): JsonSchema.JsonSchema | undefined { - if (annotations === undefined) return undefined - - const out: JsonSchema.JsonSchema = {} - if (typeof annotations.title === "string") out.title = annotations.title - if (typeof annotations.description === "string") out.description = annotations.description - else if (generateDescriptions && typeof annotations.expected === "string") out.description = annotations.expected - if (annotations.default !== undefined) out.default = annotations.default - if (Array.isArray(annotations.examples)) out.examples = annotations.examples - if (typeof annotations.readOnly === "boolean") out.readOnly = annotations.readOnly - if (typeof annotations.writeOnly === "boolean") out.writeOnly = annotations.writeOnly - if (typeof annotations.format === "string") out.format = annotations.format - if (typeof annotations.contentEncoding === "string") out.contentEncoding = annotations.contentEncoding - if (typeof annotations.contentMediaType === "string") out.contentMediaType = annotations.contentMediaType - - if (includeAnnotationKey) { - for (const [key, value] of Object.entries(annotations)) { - if (value === undefined) continue - if (standardJsonSchemaAnnotationKeys.has(key)) continue - if (!includeAnnotationKey(key)) continue - out[key] = value - } - } - - if (Object.keys(out).length > 0) return out - } - - function collectJsonSchemaChecks( - checks: ReadonlyArray>, - type: unknown - ): Array { - return checks.map(collectJsonSchemaCheck).filter((c) => c !== undefined) - - function collectJsonSchemaCheck(check: SchemaRepresentation.Check): JsonSchema.JsonSchema | undefined { - switch (check._tag) { - case "Filter": - return filterToJsonSchema(check, type) - case "FilterGroup": { - const checks = check.checks.map(collectJsonSchemaCheck).filter((c) => c !== undefined) - if (checks.length === 0) return undefined - let out = { allOf: checks } - const a = collectJsonSchemaAnnotations(check.annotations) - if (a) { - out = { ...out, ...a } - } - return out - } - } - } - } - - function filterToJsonSchema( - filter: SchemaRepresentation.Filter, - type: unknown - ): JsonSchema.JsonSchema | undefined { - const meta = filter.meta as SchemaRepresentation.Meta - if (!meta) return undefined - - let out = on(meta) - const a = collectJsonSchemaAnnotations(filter.annotations) - if (a) { - out = { ...out, ...a } - } - return out - - function on( - meta: SchemaRepresentation.Meta - ): JsonSchema.JsonSchema | undefined { - switch (meta._tag) { - case "isMinLength": - return type === "array" ? { minItems: meta.minLength } : { minLength: meta.minLength } - case "isMaxLength": - return type === "array" ? { maxItems: meta.maxLength } : { maxLength: meta.maxLength } - case "isLengthBetween": - return type === "array" - ? { allOf: [{ minItems: meta.minimum }, { maxItems: meta.maximum }] } - : { allOf: [{ minLength: meta.minimum }, { maxLength: meta.maximum }] } - case "isPattern": - case "isGUID": - case "isULID": - case "isBase64": - case "isBase64Url": - case "isStartsWith": - case "isEndsWith": - case "isIncludes": - case "isUppercased": - case "isLowercased": - case "isCapitalized": - case "isUncapitalized": - case "isTrimmed": - case "isStringFinite": - case "isStringBigInt": - case "isStringSymbol": - return { pattern: meta.regExp.source } - case "isUUID": - return { pattern: meta.regExp.source, format: "uuid" } - - case "isFinite": - case "isInt": - return undefined - case "isMultipleOf": - return { multipleOf: meta.divisor } - case "isGreaterThanOrEqualTo": - return { minimum: meta.minimum } - case "isLessThanOrEqualTo": - return { maximum: meta.maximum } - case "isGreaterThan": - return { exclusiveMinimum: meta.exclusiveMinimum } - case "isLessThan": - return { exclusiveMaximum: meta.exclusiveMaximum } - case "isBetween": { - return { - [meta.exclusiveMinimum ? "exclusiveMinimum" : "minimum"]: meta.minimum, - [meta.exclusiveMaximum ? "exclusiveMaximum" : "maximum"]: meta.maximum - } - } - - case "isUnique": - return { uniqueItems: true } - - case "isMinProperties": - return { minProperties: meta.minProperties } - case "isMaxProperties": - return { maxProperties: meta.maxProperties } - case "isPropertiesLengthBetween": - return { minProperties: meta.minimum, maxProperties: meta.maximum } - case "isPropertyNames": - return { propertyNames: recur(meta.propertyNames) } - - case "isDateValid": - return { format: "date-time" } - } - } - } - - function getParameterPatterns(parameter: SchemaRepresentation.Representation): Array { - switch (parameter._tag) { - default: - throw new globalThis.Error(`Unsupported index signature parameter: ${parameter._tag}`) - case "Reference": - return getParameterPatterns(multiDocument.references[parameter.$ref]) - case "String": - return getPatterns(parameter) - case "TemplateLiteral": - return [`^${parameter.parts.map(getPartPattern).join("")}$`] - case "Union": - return parameter.types.flatMap(getParameterPatterns) - } - } -} - -function getPatterns(s: SchemaRepresentation.String): Array { - return recur(s.checks) - - function recur(checks: ReadonlyArray>): Array { - return checks.flatMap((c) => { - switch (c._tag) { - case "Filter": { - if ("regExp" in c.meta) { - return [c.meta.regExp.source] - } - return [] - } - case "FilterGroup": - return recur(c.checks) - } - }) - } -} - -function hasCheck(checks: ReadonlyArray>, tag: string): boolean { - return checks.some((c) => { - switch (c._tag) { - case "Filter": - return c.meta._tag === tag - case "FilterGroup": - return hasCheck(c.checks, tag) - } - }) -} - -function appendJsonSchema(a: JsonSchema.JsonSchema, b: JsonSchema.JsonSchema): JsonSchema.JsonSchema { - if (Object.keys(a).length === 0) return b - const len = Object.keys(b).length - if (len === 0) return a - const members = Array.isArray(b.allOf) && len === 1 ? b.allOf : [b] - - if (Array.isArray(a.allOf)) { - return { ...a, allOf: [...a.allOf, ...members] } - } - - if (typeof a.$ref === "string") { - return { allOf: [a, ...members] } - } - - return { ...a, allOf: members } -} - -function getPartPattern(part: SchemaRepresentation.Representation): string { - switch (part._tag) { - case "Literal": - return RegEx.escape(globalThis.String(part.literal)) - case "String": - return SchemaAST.STRING_PATTERN - case "Number": - return SchemaAST.FINITE_PATTERN - case "TemplateLiteral": - return part.parts.map(getPartPattern).join("") - case "Union": - return part.types.map(getPartPattern).join("|") - default: - throw new globalThis.Error("Unsupported part", { cause: part }) - } -} diff --git a/.context/effect/packages/effect/src/internal/schema/schema.ts b/.context/effect/packages/effect/src/internal/schema/schema.ts index 752d3fc97..cdbb85ff5 100644 --- a/.context/effect/packages/effect/src/internal/schema/schema.ts +++ b/.context/effect/packages/effect/src/internal/schema/schema.ts @@ -1,15 +1,51 @@ -import * as Cause from "../../Cause.ts" -import * as Effect from "../../Effect.ts" import * as Pipeable from "../../Pipeable.ts" import type * as Schema from "../../Schema.ts" import * as SchemaAST from "../../SchemaAST.ts" -import { SchemaError } from "../../SchemaError.ts" -import type { Issue } from "../../SchemaIssue.ts" import * as SchemaParser from "../../SchemaParser.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" /** @internal */ export const TypeId = "~effect/Schema/Schema" +/** @internal */ +export function makeDeclarationReviver

( + id: string, + payloadSchema: Schema.Decoder

, + revive: SchemaRepresentation.DeclarationReviver

["revive"] +): SchemaRepresentation.DeclarationReviver

{ + return { + id, + payloadSchema, + revive + } +} + +/** @internal */ +export function makeFilterReviver

( + id: string, + payloadSchema: Schema.Decoder

, + revive: SchemaRepresentation.FilterReviver

["revive"] +): SchemaRepresentation.FilterReviver

{ + return { + id, + payloadSchema, + revive + } +} + +/** @internal */ +export function makeFilterGroupReviver

( + id: string, + payloadSchema: Schema.Decoder

, + revive: SchemaRepresentation.FilterGroupReviver

["revive"] +): SchemaRepresentation.FilterGroupReviver

{ + return { + id, + payloadSchema, + revive + } +} + const SchemaProto = { [TypeId]: TypeId, pipe() { @@ -28,68 +64,15 @@ const SchemaProto = { /** @internal */ export function make(ast: S["ast"], options?: object): S { - const self = Object.create(SchemaProto) - if (options) { - Object.assign(self, options) - } + function Schema() {} + const self = Object.defineProperties( + Object.setPrototypeOf(Schema, SchemaProto), + Object.getOwnPropertyDescriptors({ ...options }) + ) self.ast = ast self.rebuild = (ast: SchemaAST.AST) => make(ast, options) - const makeEffect = SchemaParser.makeEffect(self) - self.makeEffect = (input: S["~type.make.in"], options?: Schema.MakeOptions) => - fromIssueEffect(makeEffect(input, options)) + self.makeEffect = SchemaParser.makeEffect(self) self.make = SchemaParser.make(self) self.makeOption = SchemaParser.makeOption(self) return self } - -/** @internal */ -export function fromIssueEffect( - self: Effect.Effect -): Effect.Effect { - return Effect.catchCause( - self, - (cause) => Effect.failCauseSync(() => Cause.map(cause, (issue) => new SchemaError(issue))) - ) -} - -/** @internal */ -export const jsonReorder = makeReorder(getJsonPriority) - -function getJsonPriority(ast: SchemaAST.AST): number { - switch (ast._tag) { - case "BigInt": - case "Symbol": - case "UniqueSymbol": - return 0 - default: - return 1 - } -} - -/** @internal */ -export function makeReorder(getPriority: (ast: SchemaAST.AST) => number) { - return (types: ReadonlyArray): ReadonlyArray => { - // Create a map of original indices for O(1) lookup - const indexMap = new Map() - for (let i = 0; i < types.length; i++) { - indexMap.set(SchemaAST.toEncoded(types[i]), i) - } - - // Create a sorted copy of the types array - const sortedTypes = [...types].sort((a, b) => { - a = SchemaAST.toEncoded(a) - b = SchemaAST.toEncoded(b) - const pa = getPriority(a) - const pb = getPriority(b) - if (pa !== pb) return pa - pb - // If priorities are equal, maintain original order (stable sort) - return indexMap.get(a)! - indexMap.get(b)! - }) - - // Check if order changed by comparing arrays - const orderChanged = sortedTypes.some((ast, index) => ast !== types[index]) - - if (!orderChanged) return types - return sortedTypes - } -} diff --git a/.context/effect/packages/effect/src/internal/schema/toArbitrary.ts b/.context/effect/packages/effect/src/internal/schema/toArbitrary.ts new file mode 100644 index 000000000..c6d2ae27c --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/toArbitrary.ts @@ -0,0 +1,843 @@ +import * as Array from "../../Array.ts" +import * as Boolean from "../../Boolean.ts" +import type * as Combiner from "../../Combiner.ts" +import * as Equal from "../../Equal.ts" +import { memoize } from "../../Function.ts" +import * as Number from "../../Number.ts" +import * as Option from "../../Option.ts" +import * as Order from "../../Order.ts" +import * as Predicate from "../../Predicate.ts" +import type * as Schema from "../../Schema.ts" +import * as SchemaAST from "../../SchemaAST.ts" +import * as Struct from "../../Struct.ts" +import type * as FastCheck from "../../testing/FastCheck.ts" +import * as UndefinedOr from "../../UndefinedOr.ts" +import { errorWithPath } from "../errors.ts" +import * as InternalRecord from "../record.ts" +import * as InternalAnnotations from "./annotations.ts" + +const arbitraryMemoMap = new WeakMap>() +const suspendDepthIdentifierMap = new WeakMap() +const emptyRecursionStack: RecursionStack = [] + +type RecursionStack = ReadonlyArray + +type Context = Schema.Annotations.ToArbitrary.Context +type Constraint = Schema.Annotations.ToArbitrary.GenerationConstraint +type OrderedConstraint = Schema.Annotations.ToArbitrary.OrderedConstraint +type ArbitraryFilter = Schema.Annotations.ToArbitrary.Filter + +type Lazy = (fc: typeof FastCheck, ctx: Context, recursionStack: RecursionStack) => FastCheck.Arbitrary +type LazyOption = ( + fc: typeof FastCheck, + ctx: Context, + recursionStack: RecursionStack +) => FastCheck.Arbitrary | undefined + +function arbitraryError(what: string) { + return new Error(`Unable to derive an arbitrary for ${what}`) +} + +const entryComparator = ([a]: readonly [any, any], [b]: readonly [any, any]) => Equal.equals(a, b) + +function applyChecks(ast: SchemaAST.AST, filters: Array>, arbitrary: FastCheck.Arbitrary) { + return filters.reduce( + (acc, filter) => acc.filter((a) => filter.run(a, ast, SchemaAST.defaultParseOptions) === undefined), + arbitrary + ) +} + +function validateArrayConstraints(constraint: FastCheck.ArrayConstraints | undefined, label: string) { + if ( + constraint?.minLength !== undefined && constraint.maxLength !== undefined && + constraint.minLength > constraint.maxLength + ) { + throw arbitraryError(`${label} constraints`) + } +} + +function lengthToFastCheckConstraints( + constraint: { readonly minLength?: number | undefined; readonly maxLength?: number | undefined } | undefined +) { + return constraint === undefined || (constraint.minLength === undefined && constraint.maxLength === undefined) + ? undefined + : { + ...(constraint.minLength !== undefined ? { minLength: constraint.minLength } : {}), + ...(constraint.maxLength !== undefined ? { maxLength: constraint.maxLength } : {}) + } +} + +function arrayWithConstraints( + fc: typeof FastCheck, + item: FastCheck.Arbitrary, + constraint: FastCheck.ArrayConstraints | undefined, + comparator?: ((a: any, b: any) => boolean) | undefined +) { + return comparator + ? fc.uniqueArray(item, { ...constraint, comparator }) + : fc.array(item, constraint) +} + +function array(fc: typeof FastCheck, ctx: Context, item: FastCheck.Arbitrary, terminal = false) { + const constraint = ctx.constraint + const arrayConstraints = lengthToFastCheckConstraints(constraint) + validateArrayConstraints(arrayConstraints, "array") + return arrayWithConstraints( + fc, + item, + terminal ? { ...arrayConstraints, maxLength: arrayConstraints?.minLength ?? 0 } : arrayConstraints, + constraint?.unique ? Equal.equals : undefined + ) +} + +function appendArray( + fc: typeof FastCheck, + out: FastCheck.Arbitrary>, + len: number, + rest: FastCheck.Arbitrary> +) { + return out.chain((as) => as.length < len ? fc.constant(as) : rest.map((rest) => [...as, ...rest])) +} + +function appendObjectEntries( + out: FastCheck.Arbitrary, + entries: FastCheck.Arbitrary> +) { + return out.chain((o) => entries.map((entries) => ({ ...Object.fromEntries(entries), ...o }))) +} + +const max = UndefinedOr.makeReducer(Number.ReducerMax) +const min = UndefinedOr.makeReducer(Number.ReducerMin) +const or = UndefinedOr.makeReducer(Boolean.ReducerOr) +const concat = UndefinedOr.makeReducer(Array.makeReducerConcat()) + +const combiner: Combiner.Combiner = Struct.makeCombiner({ + integer: or, + maxLength: min, + minLength: max, + noInfinity: or, + noNaN: or, + patterns: concat, + unique: or +}, { + omitKeyWhen: Predicate.isUndefined +}) + +function mergeOrderedBound( + order: Order.Order, + self: T | undefined, + selfExclusive: boolean | undefined, + that: T | undefined, + thatExclusive: boolean | undefined, + takeComparison: -1 | 1 +): readonly [T | undefined, boolean | undefined] { + if (that === undefined || self === undefined) { + return that === undefined ? [self, selfExclusive] : [that, thatExclusive] + } + const comparison = order(self, that) + return comparison === takeComparison + ? [that, thatExclusive] + : comparison === 0 + ? [self, selfExclusive || thatExclusive] + : [self, selfExclusive] +} + +function mergeOrderedConstraints(self: OrderedConstraint | undefined, that: OrderedConstraint) { + if (self === undefined) { + return that + } + if (self.order !== that.order) { + throw new Error("Cannot merge ordered arbitrary constraints with different Order instances") + } + + const [minimum, exclusiveMinimum] = mergeOrderedBound( + self.order, + self.minimum, + self.exclusiveMinimum, + that.minimum, + that.exclusiveMinimum, + -1 + ) + const [maximum, exclusiveMaximum] = mergeOrderedBound( + self.order, + self.maximum, + self.exclusiveMaximum, + that.maximum, + that.exclusiveMaximum, + 1 + ) + + return { + order: self.order, + ...(minimum !== undefined ? { minimum } : {}), + ...(exclusiveMinimum !== undefined ? { exclusiveMinimum } : {}), + ...(maximum !== undefined ? { maximum } : {}), + ...(exclusiveMaximum !== undefined ? { exclusiveMaximum } : {}) + } +} + +function mergeConstraint(self: Constraint | undefined, that: Constraint): Constraint { + const { ordered: selfOrdered, ...selfRest } = self ?? {} + const { ordered: thatOrdered, ...thatRest } = that + const ordered = thatOrdered === undefined + ? selfOrdered + : mergeOrderedConstraints(selfOrdered, thatOrdered) + const out = combiner.combine(selfRest, thatRest) + return { + ...out, + ...(ordered === undefined ? {} : { ordered }) + } +} + +function collectChecks(checks: SchemaAST.Checks | undefined) { + const filters: Array> = [] + const arbitraries: Array = [] + function visit(check: SchemaAST.Check) { + if (check.annotations?.arbitrary) { + arbitraries.push(check.annotations.arbitrary) + } + if (check._tag !== "Filter") { + for (const child of check.checks) { + visit(child) + } + } else { + filters.push(check) + } + } + checks?.forEach(visit) + return { filters, arbitraries } +} + +function constraintContext(arbitraries: Array): (ctx: Context) => Context { + const constraintAnnotations = arbitraries.map(({ constraint }) => constraint).filter(Predicate.isNotUndefined) + return (ctx) => { + const constraint = constraintAnnotations.reduce( + (acc: Constraint | undefined, c) => mergeConstraint(acc, c), + ctx.constraint + ) + return { ...ctx, constraint } + } +} + +function resetContext(ctx: Context) { + return { ...ctx, constraint: undefined } +} + +function objectEntriesConstraints(ast: SchemaAST.Objects, constraint: Constraint | undefined, requiredKeys: number) { + if (constraint === undefined || (constraint.minLength === undefined && constraint.maxLength === undefined)) { + return undefined + } + if ( + constraint.minLength !== undefined && + ast.indexSignatures.length === 0 && + constraint.minLength > ast.propertySignatures.length + ) { + throw arbitraryError("object property constraints") + } + const out: FastCheck.ArrayConstraints = {} + if (constraint.minLength !== undefined) { + out.minLength = Math.max(0, constraint.minLength - requiredKeys) + } + if (constraint.maxLength !== undefined) { + out.maxLength = constraint.maxLength - requiredKeys + if (out.maxLength < 0) { + throw arbitraryError("object property constraints") + } + } + validateArrayConstraints(out, "object property") + return out +} + +function objectWithOptionalCount( + fc: typeof FastCheck, + pss: Record>, + orderedNames: ReadonlyArray, + requiredKeys: ReadonlyArray, + optionalNames: ReadonlyArray, + constraint: Constraint +) { + const requiredCount = requiredKeys.length + if (constraint.maxLength !== undefined && constraint.maxLength < requiredCount) { + throw arbitraryError("object property constraints") + } + const minOptional = constraint.minLength === undefined ? 0 : Math.max(0, constraint.minLength - requiredCount) + const maxOptional = constraint.maxLength === undefined + ? optionalNames.length + : Math.min(optionalNames.length, constraint.maxLength - requiredCount) + if (minOptional > maxOptional) { + throw arbitraryError("object property constraints") + } + const full = fc.record(pss, { requiredKeys: [...requiredKeys, ...optionalNames] }) + const chosen = fc.shuffledSubarray([...optionalNames], { minLength: minOptional, maxLength: maxOptional }) + return fc.tuple(full, chosen).map(([base, names]) => { + const keep = new Set([...requiredKeys, ...names]) + const out: Record = {} + for (const name of orderedNames) { + if (keep.has(name)) { + InternalRecord.assignProperty(out, name, base[name]) + } + } + return out + }) +} + +function toRangeConstraints( + ordered: OrderedConstraint | undefined, + min: (value: T, excluded: boolean) => T, + max: (value: T, excluded: boolean) => T, + error: string +) { + const out: { min?: T; max?: T } = {} + if (ordered?.minimum !== undefined) { + out.min = min(ordered.minimum as T, ordered.exclusiveMinimum === true) + } + if (ordered?.maximum !== undefined) { + out.max = max(ordered.maximum as T, ordered.exclusiveMaximum === true) + } + if (out.min !== undefined && out.max !== undefined && out.min > out.max) { + throw arbitraryError(error) + } + return out +} + +function toIntegerConstraints(ordered: OrderedConstraint | undefined) { + return toRangeConstraints( + ordered, + (minimum, excluded) => excluded ? Math.floor(minimum) + 1 : Math.ceil(minimum), + (maximum, excluded) => excluded ? Math.ceil(maximum) - 1 : Math.floor(maximum), + "integer constraints" + ) +} + +function toFloatConstraints(constraint: Constraint | undefined, ordered: OrderedConstraint | undefined) { + const out: FastCheck.FloatConstraints = { + ...(constraint?.noInfinity ? { noDefaultInfinity: true } : {}), + ...(constraint?.noNaN ? { noNaN: true } : {}), + ...(ordered?.minimum !== undefined ? { min: ordered.minimum as number } : {}), + ...(ordered?.exclusiveMinimum !== undefined ? { minExcluded: ordered.exclusiveMinimum } : {}), + ...(ordered?.maximum !== undefined ? { max: ordered.maximum as number } : {}), + ...(ordered?.exclusiveMaximum !== undefined ? { maxExcluded: ordered.exclusiveMaximum } : {}) + } + if ( + out.min !== undefined && + out.max !== undefined && + (out.min > out.max || (out.min === out.max && (out.minExcluded || out.maxExcluded))) + ) { + throw arbitraryError("number constraints") + } + return out +} + +function toBigIntConstraints(ordered: OrderedConstraint | undefined) { + return toRangeConstraints( + ordered, + (minimum, excluded) => excluded ? minimum + BigInt(1) : minimum, + (maximum, excluded) => excluded ? maximum - BigInt(1) : maximum, + "the ordered bigint constraints" + ) +} + +interface LazyArbitraryWithContext { + (fc: typeof FastCheck, ctx: Context, recursionStack?: RecursionStack): FastCheck.Arbitrary + readonly terminal: ( + fc: typeof FastCheck, + ctx: Context, + recursionStack?: RecursionStack + ) => FastCheck.Arbitrary | undefined +} + +function makeLazy(normal: Lazy, terminal: LazyOption): LazyArbitraryWithContext { + const out = + ((fc, ctx, recursionStack = emptyRecursionStack) => normal(fc, ctx, recursionStack)) as LazyArbitraryWithContext + ;(out as { terminal: LazyOption }).terminal = (fc, ctx, recursionStack = emptyRecursionStack) => + terminal(fc, ctx, recursionStack) + return out +} + +function same(f: Lazy) { + return makeLazy(f, f) +} + +function getSuspendRecursion(fc: typeof FastCheck, ast: SchemaAST.Suspend) { + const depthIdentifier = suspendDepthIdentifierMap.get(ast) ?? fc.createDepthIdentifier() + suspendDepthIdentifierMap.set(ast, depthIdentifier) + return { maxDepth: 2, depthIdentifier } +} + +function oneOf(fc: typeof FastCheck, arbitraries: ReadonlyArray>) { + return arbitraries.length === 0 ? undefined : arbitraries.length === 1 ? arbitraries[0] : fc.oneof(...arbitraries) +} + +const finiteNumberConstraint: Constraint = { + noInfinity: true, + noNaN: true +} + +function finiteNumberContext(ctx: Context): Context { + return { + ...ctx, + constraint: finiteNumberConstraint + } +} + +function applyCandidates( + fc: typeof FastCheck, + ctx: Context, + arbitraries: Array, + base: FastCheck.Arbitrary | undefined +) { + const weighted: Array> = base === undefined + ? [] + : [{ arbitrary: base, weight: 1 }] + for (const { candidate } of arbitraries) { + if (!candidate) { + continue + } + const arbitrary = candidate.make(fc, ctx) + if (arbitrary === undefined) { + continue + } + const weight = candidate.weight ?? 1 + if (!globalThis.Number.isInteger(weight) || weight <= 0) { + throw arbitraryError("a candidate with an invalid weight") + } + weighted.push({ arbitrary, weight }) + } + return weighted.length === 0 ? undefined : weighted.length === 1 ? weighted[0].arbitrary : fc.oneof(...weighted) +} + +function applyFilterLayer( + ast: SchemaAST.AST, + checks: ReturnType, + fc: typeof FastCheck, + ctx: Context, + base: FastCheck.Arbitrary | undefined +) { + const out = applyCandidates(fc, ctx, checks.arbitraries, base) + return out === undefined ? undefined : applyChecks(ast, checks.filters, out) +} + +function normalizeDerivation( + output: Schema.Annotations.ToArbitrary.Output, + hasTypeParameters: boolean +) { + if (!(typeof output === "object" && output !== null && "arbitrary" in output)) { + return { arbitrary: output, terminal: hasTypeParameters ? undefined : output } + } + const terminal = "terminal" in output ? output.terminal : hasTypeParameters ? undefined : output.arbitrary + return { + arbitrary: output.arbitrary, + terminal + } +} + +function makeTypeParameters( + typeParameters: ReadonlyArray>, + fc: typeof FastCheck, + ctx: Context, + recursionStack: RecursionStack, + lazyNormal: boolean +) { + return typeParameters.map((tp) => ({ + arbitrary: lazyNormal ? fc.constant(null).chain(() => tp(fc, ctx, recursionStack)) : tp(fc, ctx, recursionStack), + terminal: tp.terminal(fc, ctx, recursionStack) + })) +} + +type BaseBuilder = ( + fc: typeof FastCheck, + ctx: Context, + nextCtx: Context, + recursionStack: RecursionStack +) => FastCheck.Arbitrary | undefined + +function filterLayer( + ast: SchemaAST.AST, + checks: ReturnType, + normalBase: BaseBuilder, + terminalBase: BaseBuilder +): LazyArbitraryWithContext { + const f = constraintContext(checks.arbitraries) + return makeLazy((fc, ctx, recursionStack) => { + const nextCtx = f(ctx) + return applyFilterLayer(ast, checks, fc, nextCtx, normalBase(fc, ctx, nextCtx, recursionStack))! + }, (fc, ctx, recursionStack) => { + const nextCtx = f(ctx) + return applyFilterLayer(ast, checks, fc, nextCtx, terminalBase(fc, ctx, nextCtx, recursionStack)) + }) +} + +/** @internal */ +export const memoized = memoize((ast: SchemaAST.AST) => recur(ast, [])) + +function recur(ast: SchemaAST.AST, path: ReadonlyArray): LazyArbitraryWithContext { + // --------------------------------------------- + // handle annotations + // --------------------------------------------- + const annotation = InternalAnnotations.resolve(ast)?.toArbitrary as + | Schema.Annotations.ToArbitrary.Declaration> + | undefined + if (annotation) { + const typeParameters = SchemaAST.isDeclaration(ast) ? ast.typeParameters.map((tp) => recur(tp, path)) : [] + const checks = collectChecks(ast.checks) + const derive = (lazyNormal: boolean): BaseBuilder => (fc, ctx, nextCtx, recursionStack) => + normalizeDerivation( + annotation(makeTypeParameters(typeParameters, fc, resetContext(ctx), recursionStack, lazyNormal))(fc, nextCtx), + typeParameters.length > 0 + )[lazyNormal ? "terminal" : "arbitrary"] + return filterLayer(ast, checks, derive(false), derive(true)) + } + if (ast.checks) { + const checks = collectChecks(ast.checks) + const lawc = recur(SchemaAST.replaceChecks(ast, undefined), path) + return filterLayer( + ast, + checks, + (fc, _ctx, nextCtx, recursionStack) => lawc(fc, nextCtx, recursionStack), + (fc, _ctx, nextCtx, recursionStack) => lawc.terminal(fc, nextCtx, recursionStack) + ) + } + return base(ast, path) +} + +function base(ast: SchemaAST.AST, path: ReadonlyArray): LazyArbitraryWithContext { + switch (ast._tag) { + case "Never": + case "Declaration": + throw errorWithPath(`Unsupported AST ${ast._tag}`, path) + case "Null": + return same((fc) => fc.constant(null)) + case "Void": + case "Undefined": + return same((fc) => fc.constant(undefined)) + case "Unknown": + case "Any": + return same((fc) => fc.anything()) + case "String": + return same((fc, ctx) => { + const constraint = ctx.constraint + const patterns = constraint?.patterns + return patterns + ? fc.oneof(...patterns.map((pattern) => fc.stringMatching(new RegExp(pattern)))) + : fc.string(lengthToFastCheckConstraints(constraint)) + }) + case "Number": + return same((fc, ctx) => { + const constraint = ctx.constraint + const ordered = constraint?.ordered?.order === Order.Number ? constraint.ordered : undefined + return constraint?.integer + ? fc.integer(toIntegerConstraints(ordered)) + : fc.float(toFloatConstraints(constraint, ordered)) + }) + case "Boolean": + return same((fc) => fc.boolean()) + case "BigInt": + return same((fc, ctx) => { + const ordered = ctx.constraint?.ordered?.order === Order.BigInt ? ctx.constraint.ordered : undefined + return fc.bigInt(toBigIntConstraints(ordered)) + }) + case "Symbol": + return same((fc) => fc.string().map(Symbol.for)) + case "Literal": + return same((fc) => fc.constant(ast.literal)) + case "UniqueSymbol": + return same((fc) => fc.constant(ast.symbol)) + case "ObjectKeyword": + return same((fc) => fc.oneof(fc.object(), fc.array(fc.anything()))) + case "Enum": + return recur(SchemaAST.enumsToLiterals(ast), path) + case "TemplateLiteral": { + const parts = ast.parts.map((part, i) => recur(SchemaAST.toEncoded(part), [...path, i])) + return same((fc, ctx, recursionStack) => + fc.tuple(...parts.map((part) => part(fc, finiteNumberContext(ctx), recursionStack))).map((segments) => + segments.map((segment) => globalThis.String(segment)).join("") + ) + ) + } + case "Arrays": { + const elements = ast.elements.map((ast, i) => ({ + ast, + arbitrary: recur(ast, [...path, i]) + })) + const len = ast.elements.length + const rest = ast.rest.map((ast, i) => ({ + ast, + arbitrary: recur(ast, [...path, len + i]) + })) + const terminal: LazyOption = (fc, ctx, recursionStack) => { + const reset = resetContext(ctx) + const elementArbitraries: Array>> = [] + const optionals: Array | undefined> = [] + let length = 0 + for (const element of elements) { + const out = element.arbitrary.terminal(fc, reset, recursionStack) + if (SchemaAST.isOptional(element.ast)) { + optionals.push(out) + continue + } + if (out === undefined) { + return undefined + } + length++ + elementArbitraries.push(out.map(Option.some)) + } + const minLength = ctx.constraint?.minLength ?? 0 + const needsRest = Array.isReadonlyArrayNonEmpty(rest) && minLength > length + optionals.length + const optionalTarget = needsRest ? optionals.length : Math.max(0, minLength - length) + let includedOptionals = 0 + for (const out of optionals) { + if (includedOptionals >= optionalTarget || out === undefined) { + elementArbitraries.push(fc.constant(Option.none())) + continue + } + includedOptionals++ + length++ + elementArbitraries.push(out.map(Option.some)) + } + if (includedOptionals < optionalTarget) { + return undefined + } + let out = fc.tuple(...elementArbitraries).map(Array.getSomes) + if (Array.isReadonlyArrayNonEmpty(rest)) { + const [head, ...tail] = rest + const restCtx = ast.elements.length === 0 ? ctx : reset + const minRestLength = Math.max(0, minLength - length - tail.length) + const headArbitrary = minRestLength === 0 + ? undefined + : head.arbitrary.terminal(fc, reset, recursionStack) + if (minRestLength > 0 && headArbitrary === undefined) { + return undefined + } + const restArbitrary = minRestLength === 0 + ? fc.constant([]) + : array( + fc, + { ...restCtx, constraint: { ...restCtx.constraint, minLength: minRestLength } }, + headArbitrary!, + true + ) + out = appendArray(fc, out, len, restArbitrary) + if (tail.length > 0) { + const tailArbitraries: Array> = [] + for (const element of tail) { + const out = element.arbitrary.terminal(fc, reset, recursionStack) + if (out === undefined) { + return undefined + } + tailArbitraries.push(out) + } + const t = fc.tuple(...tailArbitraries) + out = appendArray(fc, out, len, t) + } + } + return out + } + return makeLazy((fc, ctx, recursionStack) => { + const reset = resetContext(ctx) + // --------------------------------------------- + // handle elements + // --------------------------------------------- + const elementArbitraries: Array>> = elements.map( + ({ ast, arbitrary }) => { + const out = arbitrary(fc, reset, recursionStack) + return SchemaAST.isOptional(ast) + ? out.chain((a) => fc.boolean().map((b) => b ? Option.some(a) : Option.none())) + : out.map(Option.some) + } + ) + let out = fc.tuple(...elementArbitraries).map((elements) => + Array.getSomes(Array.takeWhile(elements, Option.isSome)) + ) + // --------------------------------------------- + // handle rest element + // --------------------------------------------- + if (Array.isReadonlyArrayNonEmpty(rest)) { + const [head, ...tail] = rest.map(({ arbitrary }) => arbitrary(fc, reset, recursionStack)) + + const restArbitrary = array(fc, ast.elements.length === 0 ? ctx : reset, head) + out = appendArray(fc, out, len, restArbitrary) + // --------------------------------------------- + // handle post rest elements + // --------------------------------------------- + if (tail.length > 0) { + const t = fc.tuple(...tail) + out = appendArray(fc, out, len, t) + } + } + if (ctx.recursion) { + const terminalOut = terminal(fc, ctx, recursionStack) + if (terminalOut !== undefined) { + return fc.oneof(ctx.recursion, terminalOut, out) + } + } + return out + }, terminal) + } + case "Objects": { + const propertySignatures = ast.propertySignatures.map((ps) => ({ + ps, + arbitrary: recur(ps.type, [...path, ps.name]) + })) + const indexSignatures = ast.indexSignatures.map((is) => ({ + is, + parameter: recur(is.parameter, path), + type: recur(is.type, path) + })) + const terminal: LazyOption = (fc, ctx, recursionStack) => { + const reset = resetContext(ctx) + const pss: any = {} + const requiredKeys: Array = [] + const optionals: Array]> = [] + for (const { ps, arbitrary } of propertySignatures) { + const name = ps.name + const out = arbitrary.terminal(fc, reset, recursionStack) + if (SchemaAST.isOptional(ps.type)) { + if (out !== undefined) { + optionals.push([name, out]) + } + continue + } + if (out === undefined) { + return undefined + } + requiredKeys.push(name) + InternalRecord.assignProperty(pss, name, out) + } + let optionalCount = Math.max(0, (ctx.constraint?.minLength ?? 0) - requiredKeys.length) + for (const [name, out] of optionals) { + if (optionalCount === 0) { + break + } + optionalCount-- + requiredKeys.push(name) + InternalRecord.assignProperty(pss, name, out) + } + if (optionalCount > 0 && ast.indexSignatures.length === 0) { + return undefined + } + let out = fc.record(pss, { requiredKeys }) + const entriesConstraints = objectEntriesConstraints(ast, ctx.constraint, requiredKeys.length) + const minEntries = entriesConstraints?.minLength ?? 0 + for (const { parameter, type } of indexSignatures) { + let entries: FastCheck.Arbitrary> + if (minEntries === 0) { + entries = fc.constant([]) + } else { + const key = parameter.terminal(fc, reset, recursionStack) + const value = type.terminal(fc, reset, recursionStack) + if (key === undefined || value === undefined) { + return undefined + } + entries = arrayWithConstraints( + fc, + fc.tuple(key, value), + { ...entriesConstraints, maxLength: minEntries }, + entryComparator + ) + } + out = appendObjectEntries(out, entries) + } + return out + } + return makeLazy((fc, ctx, recursionStack) => { + const reset = resetContext(ctx) + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + const pss: any = {} + const orderedNames: Array = [] + const requiredKeys: Array = [] + const optionalNames: Array = [] + for (const { ps, arbitrary } of propertySignatures) { + const name = ps.name + orderedNames.push(name) + if (SchemaAST.isOptional(ps.type)) { + optionalNames.push(name) + } else { + requiredKeys.push(name) + } + InternalRecord.assignProperty(pss, name, arbitrary(fc, reset, recursionStack)) + } + // When property-count constraints must be satisfied by selecting + // optional keys (no index signatures are available to fill the gap), + // generate a count-controlled subset of optional keys instead of + // relying on fast-check's independent inclusion plus discards. This + // enforces both bounds precisely while still varying which optionals + // appear. + const constraint = ctx.constraint + if ( + optionalNames.length > 0 && + indexSignatures.length === 0 && + constraint !== undefined && + (constraint.minLength !== undefined || constraint.maxLength !== undefined) + ) { + return objectWithOptionalCount(fc, pss, orderedNames, requiredKeys, optionalNames, constraint) + } + let out = fc.record(pss, { requiredKeys }) + const entriesConstraints = objectEntriesConstraints(ast, ctx.constraint, requiredKeys.length) + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + for (const { parameter, type } of indexSignatures) { + const entry = fc.tuple(parameter(fc, reset, recursionStack), type(fc, reset, recursionStack)) + const entries = arrayWithConstraints(fc, entry, entriesConstraints, entryComparator) + out = appendObjectEntries(out, entries) + } + return out + }, terminal) + } + case "Union": { + const types = ast.types.map((ast) => recur(ast, path)) + const terminal: LazyOption = (fc, ctx, recursionStack) => + oneOf(fc, types.map((type) => type.terminal(fc, ctx, recursionStack)).filter(Predicate.isNotUndefined)) + return makeLazy((fc, ctx, recursionStack) => { + const arbitraries = types.map((type) => type(fc, ctx, recursionStack)) + if (ctx.recursion) { + const terminalOut = terminal(fc, ctx, recursionStack) + if (terminalOut !== undefined) { + return fc.oneof(ctx.recursion, terminalOut, ...arbitraries) + } + } + const out = oneOf(fc, arbitraries) + if (out === undefined) { + throw arbitraryError("a union with no members") + } + return out + }, terminal) + } + case "Suspend": { + const memo = arbitraryMemoMap.get(ast) + + if (memo) return memo + + const get = SchemaAST.memoizeThunk(() => recur(ast.thunk(), path)) + const out = makeLazy((fc, ctx, recursionStack) => { + const recursion = getSuspendRecursion(fc, ast) + const nextCtx = { ...ctx, recursion } + const nextStack = recursionStack.includes(ast) ? recursionStack : [...recursionStack, ast] + const terminal = get().terminal(fc, nextCtx, nextStack) + if (terminal === undefined) { + throw errorWithPath( + "Unable to derive an arbitrary for a recursive schema without a finite generation path", + path + ) + } + return fc.oneof( + recursion, + terminal, + fc.constant(null).chain(() => get()(fc, nextCtx, nextStack)) + ) + }, (fc, ctx, recursionStack) => { + if (recursionStack.includes(ast)) { + return undefined + } + const recursion = getSuspendRecursion(fc, ast) + return get().terminal(fc, { ...ctx, recursion }, [...recursionStack, ast]) + }) + + arbitraryMemoMap.set(ast, out) + + return out + } + } +} diff --git a/.context/effect/packages/effect/src/internal/schema/toCodeDocument.ts b/.context/effect/packages/effect/src/internal/schema/toCodeDocument.ts new file mode 100644 index 000000000..a59a5c57f --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/toCodeDocument.ts @@ -0,0 +1,572 @@ +import * as Arr from "../../Array.ts" +import { format, formatPropertyKey } from "../../Formatter.ts" +import type * as Schema from "../../Schema.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" +import { errorWithPath } from "../errors.ts" +import * as InternalRecord from "../record.ts" +import * as InternalAnnotations from "./annotations.ts" + +type Path = ReadonlyArray +type CheckRepresentationAnnotation = SchemaRepresentation.CheckRepresentationAnnotation< + SchemaRepresentation.Representation +> + +/** @internal */ +export function makeCode(runtime: string, Type: string): SchemaRepresentation.Code { + return { runtime, Type } +} + +function renderEmittableAnnotation(input: unknown): string | undefined { + if ( + input === null || + typeof input === "string" || + typeof input === "boolean" || + typeof input === "number" || + typeof input === "bigint" + ) return format(input) + if (typeof input === "symbol") { + const key = globalThis.Symbol.keyFor(input) + return key === undefined ? undefined : `Symbol.for(${format(key)})` + } + if (typeof input !== "object") return undefined + if (Array.isArray(input)) { + const values: Array = [] + for (const value of input) { + const rendered = renderEmittableAnnotation(value) + if (rendered === undefined) return undefined + values.push(rendered) + } + return `[${values.join(", ")}]` + } + + const entries: Array = [] + for (const [key, value] of Object.entries(input)) { + const rendered = renderEmittableAnnotation(value) + if (rendered === undefined) return undefined + entries.push(`${formatPropertyKey(key)}: ${rendered}`) + } + return `{ ${entries.join(", ")} }` +} + +function renderAnnotations( + annotations: Schema.Annotations.Annotations | undefined +): string | undefined { + if (annotations === undefined) return undefined + const entries: Array = [] + for (const [key, value] of Object.entries(annotations)) { + if (InternalAnnotations.annotationExcludedKeys.has(key)) continue + const rendered = renderEmittableAnnotation(value) + if (rendered !== undefined) entries.push(`${formatPropertyKey(key)}: ${rendered}`) + } + return entries.length === 0 ? undefined : `{ ${entries.join(", ")} }` +} + +/** @internal */ +export function sanitizeJavaScriptIdentifier(input: string): string { + if (input.length === 0) return "_" + const out = input.replace(/[^A-Za-z0-9_$]/gu, "_") + const first = out[0] + return first >= "a" && first <= "z" + ? first.toUpperCase() + out.slice(1) + : first >= "0" && first <= "9" + ? `_${out}` + : out +} + +function isSimpleLiveLiteral( + representation: SchemaRepresentation.Representation +): representation is SchemaRepresentation.Literal { + return representation._tag === "Literal" && representation.checks.length === 0 && + representation.annotations === undefined +} + +/** @internal */ +export interface TopologicalSort { + readonly nonRecursives: ReadonlyArray<{ + readonly $ref: string + readonly representation: SchemaRepresentation.Representation + }> + readonly recursives: Readonly> +} + +/** @internal */ +export function topologicalSort( + references: SchemaRepresentation.References +): TopologicalSort { + const identifiers = Object.keys(references) + const identifierSet = new Set(identifiers) + + function collectRefs(root: SchemaRepresentation.Representation): ReadonlySet { + const refs = new Set() + const visited = new WeakSet() + const stack: Array = [root] + + function pushRepresentationSchemas(representation: CheckRepresentationAnnotation | undefined): void { + if (representation?.schemas !== undefined) stack.push(...representation.schemas) + } + + function pushChecks( + checks: ReadonlyArray + ): void { + for (const check of checks) { + pushRepresentationSchemas(check.representation) + if (check._tag === "FilterGroup") pushChecks(check.checks) + } + } + + while (stack.length > 0) { + const representation = stack.pop()! + if (visited.has(representation)) continue + visited.add(representation) + if (representation._tag === "Reference") { + if (identifierSet.has(representation.$ref)) refs.add(representation.$ref) + continue + } + + pushChecks(representation.checks) + switch (representation._tag) { + case "Declaration": + pushRepresentationSchemas(representation.representation) + stack.push(...representation.typeParameters) + break + case "Suspend": + stack.push(representation.thunk) + break + case "TemplateLiteral": + stack.push(...representation.parts) + break + case "Arrays": + for (const element of representation.elements) stack.push(element.type) + stack.push(...representation.rest) + break + case "Objects": + for (const property of representation.propertySignatures) stack.push(property.type) + for (const signature of representation.indexSignatures) { + stack.push(signature.parameter, signature.type) + } + break + case "Union": + stack.push(...representation.types) + break + } + } + return refs + } + + const dependencies = new Map>( + identifiers.map((identifier) => [identifier, collectRefs(references[identifier])]) + ) + const recursive = new Set() + const state = new Map() + const stack: Array = [] + + function visit(identifier: string): void { + const current = state.get(identifier) ?? 0 + if (current === 1) { + const start = stack.indexOf(identifier) + for (let index = start; index < stack.length; index++) recursive.add(stack[index]) + return + } + if (current === 2) return + state.set(identifier, 1) + stack.push(identifier) + for (const dependency of dependencies.get(identifier)!) visit(dependency) + stack.pop() + state.set(identifier, 2) + } + + for (const identifier of identifiers) visit(identifier) + + const inDegree = new Map() + const dependents = new Map>() + for (const identifier of identifiers) { + if (!recursive.has(identifier)) { + inDegree.set(identifier, 0) + dependents.set(identifier, new Set()) + } + } + for (const [identifier, internalDependencies] of dependencies) { + if (recursive.has(identifier)) continue + for (const dependency of internalDependencies) { + if (recursive.has(dependency)) continue + inDegree.set(identifier, inDegree.get(identifier)! + 1) + dependents.get(dependency)!.add(identifier) + } + } + + const queue: Array = [] + for (const [identifier, degree] of inDegree) { + if (degree === 0) queue.push(identifier) + } + const nonRecursives: Array<{ + readonly $ref: string + readonly representation: SchemaRepresentation.Representation + }> = [] + for (let index = 0; index < queue.length; index++) { + const $ref = queue[index] + nonRecursives.push({ $ref, representation: references[$ref] }) + for (const dependent of dependents.get($ref)!) { + const degree = inDegree.get(dependent)! - 1 + inDegree.set(dependent, degree) + if (degree === 0) queue.push(dependent) + } + } + const recursives: Record = {} + for (const identifier of recursive) InternalRecord.assignProperty(recursives, identifier, references[identifier]) + return { nonRecursives, recursives } +} + +/** @internal */ +export function toCodeDocument( + document: SchemaRepresentation.MultiDocument +): SchemaRepresentation.CodeDocument { + const artifacts: Array = [] + const sorted = topologicalSort(document.references) + const sanitizedReferences = new Map() + const uniqueIdentifiers = new Set() + let compilingRecursiveDefinition = false + let explicitSuspendDepth = 0 + + for (const { $ref } of sorted.nonRecursives) ensureUniqueIdentifier($ref) + for (const $ref of Object.keys(sorted.recursives)) ensureUniqueIdentifier($ref) + + const nonRecursives = sorted.nonRecursives.map(({ $ref, representation }) => ({ + $ref: ensureUniqueIdentifier($ref), + code: recur(representation, ["references", $ref]) + })) + const recursives: Record = {} + for (const [$ref, representation] of Object.entries(sorted.recursives)) { + compilingRecursiveDefinition = true + InternalRecord.assignProperty(recursives, ensureUniqueIdentifier($ref), recur(representation, ["references", $ref])) + compilingRecursiveDefinition = false + } + const codes = document.representations.map((representation, index) => + recur(representation, ["representations", index]) + ) + + return { + codes, + references: { nonRecursives, recursives }, + artifacts + } + + function ensureUniqueIdentifier(original: string): string { + const existing = sanitizedReferences.get(original) + if (existing !== undefined) return existing + const candidate = freshIdentifier(original) + sanitizedReferences.set(original, candidate) + return candidate + } + + function freshIdentifier(seed: string): string { + const sanitized = sanitizeJavaScriptIdentifier(seed) + let candidate = sanitized + let suffix = 0 + while (uniqueIdentifiers.has(candidate)) candidate = `${sanitized}${++suffix}` + uniqueIdentifiers.add(candidate) + return candidate + } + + function addImport(importDeclaration: string): void { + if (!artifacts.some((artifact) => artifact._tag === "Import" && artifact.importDeclaration === importDeclaration)) { + artifacts.push({ _tag: "Import", importDeclaration }) + } + } + + function addSymbol(symbol: symbol): string { + const identifier = freshIdentifier("_symbol") + const key = globalThis.Symbol.keyFor(symbol) + const description = symbol.description + artifacts.push({ + _tag: "Symbol", + identifier, + code: makeCode( + key === undefined + ? `Symbol(${description === undefined ? "" : format(description)})` + : `Symbol.for(${format(key)})`, + `typeof ${identifier}` + ) + }) + return identifier + } + + function annotationSchemas( + representation: CheckRepresentationAnnotation | undefined, + path: Path + ): ReadonlyArray { + return representation?.schemas?.map((schema, index) => recur(schema, [...path, "schemas", index])) ?? [] + } + + function checkBrands( + check: SchemaRepresentation.Check + ): ReadonlyArray { + const own = InternalAnnotations.collectBrands(check.annotations) + if ( + check._tag === "FilterGroup" && + check.annotations?.toCode === undefined + ) { + return [...own, ...check.checks.flatMap(checkBrands)] + } + return own + } + + function runtimeBrands(brands: ReadonlyArray): string { + return brands.length === 0 + ? "" + : `.pipe(${brands.map((brand) => `Schema.brand(${format(brand)})`).join(", ")})` + } + + function typeBrands(brands: ReadonlyArray): string { + if (brands.length === 0) return "" + addImport(`import type * as Brand from "effect/Brand"`) + return brands.map((brand) => ` & Brand.Brand<${format(brand)}>`).join("") + } + + function runtimeAnnotate( + annotations: Schema.Annotations.Annotations | undefined, + method: "annotate" | "annotateKey" = "annotate" + ): string { + const rendered = renderAnnotations(annotations) + return rendered === undefined ? "" : `.${method}(${rendered})` + } + + function compileCheck( + check: SchemaRepresentation.Check, + path: Path + ): string { + const callback = check.annotations?.toCode + let runtime: string + if (callback !== undefined) { + const schemas = annotationSchemas(check.representation, [...path, "representation"]) + const output = (callback as SchemaRepresentation.Generation.Check)({ schemas }) + for (const importDeclaration of output.importDeclarations ?? []) addImport(importDeclaration) + runtime = output.runtime + } else if (check._tag === "Filter") { + throw errorWithPath("Missing toCode callback", [...path, "annotations", "toCode"]) + } else { + runtime = `Schema.makeFilterGroup([${ + check.checks.map((child, index) => compileCheck(child, [...path, "checks", index])).join(", ") + }])` + } + runtime += runtimeAnnotate(check.annotations) + if (check._tag === "Filter" && check.aborted) runtime += ".abort()" + return runtime + } + + function applyNode( + base: SchemaRepresentation.Code, + representation: Exclude, + path: Path, + includeTypeBrands: boolean = true + ): SchemaRepresentation.Code { + const nodeBrands = InternalAnnotations.collectBrands(representation.annotations) + let runtime = base.runtime + runtimeAnnotate(representation.annotations) + runtimeBrands(nodeBrands) + let Type = base.Type + (includeTypeBrands ? typeBrands(nodeBrands) : "") + for (let index = 0; index < representation.checks.length; index++) { + const check = representation.checks[index] + const brands = checkBrands(check) + runtime += `.check(${compileCheck(check, [...path, "checks", index])})${runtimeBrands(brands)}` + if (includeTypeBrands) Type += typeBrands(brands) + } + return makeCode(runtime, Type) + } + + function recur( + representation: SchemaRepresentation.Representation, + path: Path, + includeTypeBrands: boolean = true + ): SchemaRepresentation.Code { + if (representation._tag === "Reference") { + if (!Object.hasOwn(document.references, representation.$ref)) { + throw errorWithPath(`Invalid reference ${representation.$ref}`, [...path, "$ref"]) + } + const identifier = ensureUniqueIdentifier(representation.$ref) + if ( + compilingRecursiveDefinition && explicitSuspendDepth === 0 && + Object.hasOwn(sorted.recursives, representation.$ref) + ) { + return makeCode(`Schema.suspend((): Schema.Codec<${identifier}> => ${identifier})`, identifier) + } + const Type = includeTypeBrands + ? identifier + : recur(document.references[representation.$ref], ["references", representation.$ref], false).Type + return makeCode(identifier, Type) + } + return applyNode(on(representation, path, includeTypeBrands), representation, path, includeTypeBrands) + } + + function on( + representation: Exclude, + path: Path, + includeTypeBrands: boolean + ): SchemaRepresentation.Code { + switch (representation._tag) { + case "Declaration": { + const callback = representation.annotations?.toCode + if (callback === undefined) { + throw errorWithPath("Missing toCode callback", [...path, "annotations", "toCode"]) + } + const typeParameters = representation.typeParameters.map((typeParameter, index) => + recur(typeParameter, [...path, "typeParameters", index]) + ) + const output = (callback as SchemaRepresentation.Generation.Declaration)({ typeParameters }) + for (const importDeclaration of output.importDeclarations ?? []) addImport(importDeclaration) + return makeCode(output.runtime, output.Type) + } + case "Suspend": { + explicitSuspendDepth++ + const thunk = recur(representation.thunk, [...path, "thunk"]) + explicitSuspendDepth-- + return makeCode(`Schema.suspend((): Schema.Codec<${thunk.Type}> => ${thunk.runtime})`, thunk.Type) + } + case "Null": + return makeCode("Schema.Null", "null") + case "Undefined": + return makeCode("Schema.Undefined", "undefined") + case "Void": + return makeCode("Schema.Void", "void") + case "Never": + return makeCode("Schema.Never", "never") + case "Unknown": + return makeCode("Schema.Unknown", "unknown") + case "Any": + return makeCode("Schema.Any", "any") + case "String": + return makeCode("Schema.String", "string") + case "Number": + return makeCode("Schema.Number", "number") + case "Boolean": + return makeCode("Schema.Boolean", "boolean") + case "BigInt": + return makeCode("Schema.BigInt", "bigint") + case "Symbol": + return makeCode("Schema.Symbol", "symbol") + case "Literal": { + const literal = format(representation.literal) + return makeCode(`Schema.Literal(${literal})`, literal) + } + case "UniqueSymbol": { + const identifier = addSymbol(representation.symbol) + return makeCode(`Schema.UniqueSymbol(${identifier})`, `typeof ${identifier}`) + } + case "ObjectKeyword": + return makeCode("Schema.ObjectKeyword", "object") + case "Enum": { + const identifier = freshIdentifier("_Enum") + artifacts.push({ + _tag: "Enum", + identifier, + code: makeCode( + `enum ${identifier} { ${ + representation.enums.map(([name, value]) => `${format(name)} = ${format(value)}`).join( + ", " + ) + } }`, + `typeof ${identifier}` + ) + }) + return makeCode(`Schema.Enum(${identifier})`, identifier) + } + case "TemplateLiteral": { + const parts = representation.parts.map((part, index) => recur(part, [...path, "parts", index], false)) + const Type = `\`${parts.map((part) => `\${${part.Type}}`).join("")}\`` + return makeCode(`Schema.TemplateLiteral([${parts.map((part) => part.runtime).join(", ")}])`, Type) + } + case "Arrays": { + const elements = representation.elements.map((element, index) => { + const type = recur(element.type, [...path, "elements", index, "type"]) + return makeCode( + `${element.isOptional ? "Schema.optionalKey(" : ""}${type.runtime}${element.isOptional ? ")" : ""}${ + runtimeAnnotate(element.annotations, "annotateKey") + }`, + `${type.Type}${element.isOptional ? "?" : ""}` + ) + }) + const rest = representation.rest.map((item, index) => recur(item, [...path, "rest", index])) + if (Arr.isArrayNonEmpty(rest)) { + const item = rest[0] + if (elements.length === 0 && rest.length === 1) { + return makeCode(`Schema.Array(${item.runtime})`, `ReadonlyArray<${item.Type}>`) + } + const post = rest.slice(1) + return makeCode( + `Schema.TupleWithRest(Schema.Tuple([${elements.map((element) => element.runtime).join(", ")}]), [${ + rest.map((item) => item.runtime).join(", ") + }])`, + `readonly [${elements.map((element) => element.Type).join(", ")}, ...Array<${item.Type}>${ + post.length > 0 ? `, ${post.map((item) => item.Type).join(", ")}` : "" + }]` + ) + } + return makeCode( + `Schema.Tuple([${elements.map((element) => element.runtime).join(", ")}])`, + `readonly [${elements.map((element) => element.Type).join(", ")}]` + ) + } + case "Objects": { + const properties = representation.propertySignatures.map((property, index) => { + const isSymbol = typeof property.name === "symbol" + const name = isSymbol + ? addSymbol(property.name) + : formatPropertyKey(property.name) + const type = recur(property.type, [...path, "propertySignatures", index, "type"]) + let runtime = type.runtime + if (property.isMutable) runtime = `Schema.mutableKey(${runtime})` + if (property.isOptional) runtime = `Schema.optionalKey(${runtime})` + const runtimeName = isSymbol ? `[${name}]` : name + const typeName = `${property.isMutable ? "" : "readonly "}${runtimeName}${property.isOptional ? "?" : ""}` + return makeCode( + `${runtimeName}: ${runtime}${runtimeAnnotate(property.annotations, "annotateKey")}`, + `${typeName}: ${type.Type}` + ) + }) + const indexSignatures = representation.indexSignatures.map((signature, index) => ({ + parameter: recur(signature.parameter, [...path, "indexSignatures", index, "parameter"]), + type: recur(signature.type, [...path, "indexSignatures", index, "type"]) + })) + const propertyRuntimes = properties.map((property) => property.runtime).join(", ") + const propertyTypes = properties.map((property) => property.Type).join(", ") + if (indexSignatures.length === 0) { + return makeCode( + `Schema.Struct({ ${propertyRuntimes} })`, + `{ ${propertyTypes} }` + ) + } + if (properties.length === 0 && indexSignatures.length === 1) { + const signature = indexSignatures[0] + return makeCode( + `Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})`, + `{ readonly [x: ${signature.parameter.Type}]: ${signature.type.Type} }` + ) + } + const indexRuntimes = indexSignatures.map((signature) => + `Schema.Record(${signature.parameter.runtime}, ${signature.type.runtime})` + ).join(", ") + const indexTypes = indexSignatures.map((signature) => + `readonly [x: ${signature.parameter.Type}]: ${signature.type.Type}` + ).join(", ") + return makeCode( + `Schema.StructWithRest(Schema.Struct({ ${propertyRuntimes} }), [${indexRuntimes}])`, + `{ ${propertyTypes}${properties.length > 0 ? ", " : ""}${indexTypes} }` + ) + } + case "Union": { + if (representation.types.length === 0) return makeCode("Schema.Never", "never") + if (representation.types.every(isSimpleLiveLiteral)) { + const literals = representation.types.map((literal) => format(literal.literal)) + return literals.length === 1 + ? makeCode(`Schema.Literal(${literals[0]})`, literals[0]) + : makeCode(`Schema.Literals([${literals.join(", ")}])`, literals.join(" | ")) + } + const types = representation.types.map((type, index) => + recur(type, [...path, "types", index], includeTypeBrands) + ) + const mode = representation.mode === "anyOf" ? "" : `, { mode: "oneOf" }` + return makeCode( + `Schema.Union([${types.map((type) => type.runtime).join(", ")}]${mode})`, + types.map((type) => type.Type).join(" | ") + ) + } + } + } +} diff --git a/.context/effect/packages/effect/src/internal/schema/toEquivalence.ts b/.context/effect/packages/effect/src/internal/schema/toEquivalence.ts new file mode 100644 index 000000000..1cce02ffd --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/toEquivalence.ts @@ -0,0 +1,158 @@ +import * as Equal from "../../Equal.ts" +import * as Equivalence from "../../Equivalence.ts" +import { memoize } from "../../Function.ts" +import * as Predicate from "../../Predicate.ts" +import type * as Schema from "../../Schema.ts" +import * as SchemaAST from "../../SchemaAST.ts" +import * as SchemaParser from "../../SchemaParser.ts" +import * as InternalAnnotations from "./annotations.ts" + +/** @internal */ +export const toEquivalence = memoize((ast: SchemaAST.AST): Equivalence.Equivalence => { + return recur(ast, []) +}) + +function recur(ast: SchemaAST.AST, path: ReadonlyArray): Equivalence.Equivalence { + // --------------------------------------------- + // handle annotations + // --------------------------------------------- + const annotation = InternalAnnotations.resolve(ast)?.["toEquivalence"] as + | Schema.Annotations.ToEquivalence.Declaration> + | undefined + if (annotation) { + return annotation(SchemaAST.isDeclaration(ast) ? ast.typeParameters.map((tp) => recur(tp, path)) : []) + } + switch (ast._tag) { + case "Never": + return Equivalence.strictEqual() + case "Declaration": + case "Null": + case "Undefined": + case "Void": + case "Unknown": + case "Any": + case "String": + case "Number": + case "Boolean": + case "BigInt": + case "Symbol": + case "Literal": + case "UniqueSymbol": + case "ObjectKeyword": + case "Enum": + case "TemplateLiteral": + return Equal.equals + case "Arrays": { + const elements = ast.elements.map((e, i) => recur(e, [...path, i])) + const len = ast.elements.length + const rest = ast.rest.map((r, i) => recur(r, [...path, len + i])) + 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 (rest.length > 0) { + 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++) { + if (!tail[j](a[i + j], b[i + j])) { + return false + } + } + } + return true + }) + } + case "Objects": { + if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) { + return Equal.equals + } + const propertySignatures = ast.propertySignatures.map((ps) => recur(ps.type, [...path, ps.name])) + const indexSignatures = ast.indexSignatures.map((is) => recur(is.type, path)) + return Equivalence.make((a, b) => { + if (!Predicate.isObject(a) || !Predicate.isObject(b)) { + return false + } + // --------------------------------------------- + // handle property signatures + // --------------------------------------------- + for (let i = 0; i < propertySignatures.length; i++) { + const ps = ast.propertySignatures[i] + const name = ps.name + const aHas = Object.hasOwn(a, name) + const bHas = Object.hasOwn(b, name) + if (SchemaAST.isOptional(ps.type)) { + if (aHas !== bHas) { + return false + } + } + if (aHas && bHas && !propertySignatures[i](a[name], b[name])) { + return false + } + } + // --------------------------------------------- + // handle index signatures + // --------------------------------------------- + for (let i = 0; i < indexSignatures.length; i++) { + const is = ast.indexSignatures[i] + const aKeys = SchemaAST.getIndexSignatureKeys(a, is.parameter) + const bKeys = SchemaAST.getIndexSignatureKeys(b, is.parameter) + + if (aKeys.length !== bKeys.length) return false + + for (let j = 0; j < aKeys.length; j++) { + const key = aKeys[j] + if (!Object.hasOwn(b, key) || !indexSignatures[i](a[key], b[key])) { + return false + } + } + } + return true + }) + } + case "Union": { + const types = SchemaAST.toType(ast).types + const compiled = new Map( + types.map((candidate, i) => + [candidate, [SchemaParser._is(candidate), recur(ast.types[i], path)] as const] as const + ) + ) + return Equivalence.make((a, b) => { + const candidates = SchemaAST.getCandidates(a, types) + for (let i = 0; i < candidates.length; i++) { + const [is, equivalence] = compiled.get(candidates[i])! + if (is(a) && is(b)) { + return equivalence(a, b) + } + } + return false + }) + } + case "Suspend": { + const get = SchemaAST.memoizeThunk(() => recur(ast.thunk(), path)) + return Equivalence.make((a, b) => get()(a, b)) + } + } +} diff --git a/.context/effect/packages/effect/src/internal/schema/toJsonSchemaDocument.ts b/.context/effect/packages/effect/src/internal/schema/toJsonSchemaDocument.ts new file mode 100644 index 000000000..89900ee49 --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/toJsonSchemaDocument.ts @@ -0,0 +1,549 @@ +import * as Arr from "../../Array.ts" +import * as Equal from "../../Equal.ts" +import { escapeToken, unescapeToken } from "../../JsonPointer.ts" +import type * as JsonSchema from "../../JsonSchema.ts" +import { rewriteRefs } from "../../JsonSchema.ts" +import * as RegEx from "../../RegExp.ts" +import type * as Schema from "../../Schema.ts" +import * as SchemaAST from "../../SchemaAST.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" +import { errorWithPath } from "../errors.ts" +import * as InternalRecord from "../record.ts" +import * as InternalAnnotations from "./annotations.ts" + +type Path = ReadonlyArray +type CheckRepresentationAnnotation = SchemaRepresentation.CheckRepresentationAnnotation< + SchemaRepresentation.Representation +> + +const jsonSchemaAnnotationExcludedKeys = new Set([ + ...InternalAnnotations.annotationExcludedKeys, + InternalAnnotations.IDENTIFIER_FALLBACK_KEY, + ...InternalAnnotations.jsonSchemaAnnotationKeys +]) + +/** @internal */ +export const toRepresentationOptions = { + isAnonymousReferenceAllowed: (ast: SchemaAST.AST): boolean => !SchemaAST.isDeclaration(ast) +} + +function collectJsonSchemaAnnotations( + annotations: Schema.Annotations.Annotations | undefined, + options: Schema.ToJsonSchemaOptions | undefined +): JsonSchema.JsonSchema | undefined { + if (annotations === undefined) return undefined + + const out: JsonSchema.JsonSchema = {} + const title = annotations.title + if (typeof title === "string") out.title = title + const description = annotations.description + const expected = annotations.expected + if (typeof description === "string") out.description = description + else if (options?.generateDescriptions === true && typeof expected === "string") out.description = expected + + const defaultValue = annotations.default + if (SchemaAST.isJson(defaultValue)) out.default = defaultValue + const examples = annotations.examples + if (Array.isArray(examples) && SchemaAST.isJson(examples)) out.examples = examples + const readOnly = annotations.readOnly + if (typeof readOnly === "boolean") out.readOnly = readOnly + const writeOnly = annotations.writeOnly + if (typeof writeOnly === "boolean") out.writeOnly = writeOnly + const format = annotations.format + if (typeof format === "string") out.format = format + const contentEncoding = annotations.contentEncoding + if (typeof contentEncoding === "string") out.contentEncoding = contentEncoding + const contentMediaType = annotations.contentMediaType + if (typeof contentMediaType === "string") out.contentMediaType = contentMediaType + const contentSchema = annotations.contentSchema + if (SchemaAST.isJson(contentSchema)) out.contentSchema = contentSchema + + if (options?.includeAnnotationKey !== undefined) { + for (const [key, value] of Object.entries(annotations)) { + if ( + jsonSchemaAnnotationExcludedKeys.has(key) || + !options.includeAnnotationKey(key) + ) { + continue + } + if (SchemaAST.isJson(value)) InternalRecord.assignProperty(out, key, value) + } + } + + return Object.keys(out).length === 0 ? undefined : out +} + +type JsonSchemaNumberType = "number" | "integer" + +function extractJsonSchemaNumberType(schema: JsonSchema.JsonSchema): { + readonly type: JsonSchemaNumberType | undefined + readonly schema: JsonSchema.JsonSchema +} { + let type: JsonSchemaNumberType | undefined = schema.type === "number" || schema.type === "integer" + ? schema.type + : undefined + let out = schema + if (type !== undefined) { + out = { ...schema } + delete out.type + } + if (Array.isArray(out.allOf)) { + const members: Array = [] + let changed = false + for (const member of out.allOf) { + const extracted = extractJsonSchemaNumberType(member) + if (extracted.type !== undefined) { + changed = true + if (type === undefined || extracted.type === "integer") type = extracted.type + } + if (Object.keys(extracted.schema).length > 0) members.push(extracted.schema) + } + if (changed) { + const { allOf: _, ...rest } = out + out = members.length === 0 ? rest : { ...rest, allOf: members } + } + } + return { type, schema: out } +} + +function isJsonSchemaNumberEncoding(schema: JsonSchema.JsonSchema): boolean { + return Array.isArray(schema.anyOf) && schema.anyOf.length === 4 && schema.anyOf[0]?.type === "number" && + schema.anyOf.slice(1).every((member) => member.type === "string") +} + +function appendJsonSchema( + left: JsonSchema.JsonSchema, + right: JsonSchema.JsonSchema +): JsonSchema.JsonSchema { + if (Object.keys(left).length === 0) return right + const rightKeys = Object.keys(right) + if (rightKeys.length === 0) return left + const leftType = left.type === "number" || left.type === "integer" ? left.type : undefined + const isNumberEncoding = isJsonSchemaNumberEncoding(left) + if (leftType !== undefined || isNumberEncoding) { + const extracted = extractJsonSchemaNumberType(right) + if (extracted.type !== undefined) { + const type = leftType === "integer" || extracted.type === "integer" ? "integer" : "number" + const base: JsonSchema.JsonSchema = { ...left, type } + if (isNumberEncoding) delete base.anyOf + return Object.keys(extracted.schema).length === 0 ? base : appendJsonSchema(base, extracted.schema) + } + } + const members = Array.isArray(right.allOf) && rightKeys.length === 1 ? right.allOf : [right] + if (Array.isArray(left.allOf)) { + return { ...left, allOf: [...left.allOf, ...members] } + } + if (typeof left.$ref === "string") { + return { allOf: [left, ...members] } + } + return { ...left, allOf: members } +} + +function compileJsonSchema( + representations: readonly [ + SchemaRepresentation.Representation, + ...Array + ], + rootPaths: ReadonlyArray, + references: SchemaRepresentation.References, + options: Schema.ToJsonSchemaOptions | undefined +): JsonSchema.MultiDocument<"draft-2020-12"> { + // null = compiling, string = canonical key, object = compiled schema + const definitionStates = new Map() + const compiledRepresentations = new WeakMap() + const fallbackDefinitions = new Map>() + let hasAliases = false + const referenceKeys = Object.keys(references) + for (const key of referenceKeys) { + compileDefinition(key, ["references", key]) + } + const schemas = Arr.map( + representations, + (representation, index) => finalizeJsonSchema(recur(representation, rootPaths[index])) + ) + const definitions: Record = {} + for (const key of referenceKeys) { + const compiled = definitionStates.get(key)! + if (typeof compiled !== "string") { + InternalRecord.assignProperty(definitions, key, finalizeJsonSchema(compiled)) + } + } + return { dialect: "draft-2020-12", schemas, definitions } + + function compileDefinition(key: string, path: Path): string { + const compiled = definitionStates.get(key) + if (compiled !== undefined) return typeof compiled === "string" ? compiled : key + if (!Object.hasOwn(references, key)) { + throw errorWithPath(`Invalid reference ${key}`, [...path, "$ref"]) + } + + definitionStates.set(key, null) + const representation = references[key] + const schema = recur(representation, ["references", key]) + + const fallback = getIdentifierFallback(representation) + if (fallback !== undefined) { + const candidates = fallbackDefinitions.get(fallback) + const match = candidates?.find((candidate) => Equal.equals(definitionStates.get(candidate), schema)) + if (match === undefined) { + if (candidates === undefined) fallbackDefinitions.set(fallback, [key]) + else candidates.push(key) + } else { + hasAliases = true + definitionStates.set(key, match) + return match + } + } + definitionStates.set(key, schema) + return key + } + + function finalizeJsonSchema(schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { + if (!hasAliases) return schema + return rewriteRefs(schema, ($ref) => + $ref.replace(/^#\/\$defs\/([^/]*)/, (match, token) => { + const canonical = definitionStates.get(unescapeToken(token)) + return typeof canonical === "string" ? `#/$defs/${escapeToken(canonical)}` : match + })) + } + + function getIdentifierFallback( + representation: SchemaRepresentation.Representation + ): string | undefined { + if (representation._tag === "Reference") return undefined + const annotations = representation.checks.length === 0 + ? representation.annotations + : representation.checks[representation.checks.length - 1].annotations + return typeof annotations?.identifier !== "string" && + typeof annotations?.[InternalAnnotations.IDENTIFIER_FALLBACK_KEY] === "string" + ? annotations[InternalAnnotations.IDENTIFIER_FALLBACK_KEY] + : undefined + } + + function annotationSchemas( + representation: CheckRepresentationAnnotation | undefined, + path: Path + ): ReadonlyArray { + return representation?.schemas?.map((schema, index) => recur(schema, [...path, "schemas", index])) ?? [] + } + + function compileCheck( + check: SchemaRepresentation.Check, + type: JsonSchema.Type | undefined, + path: Path + ): JsonSchema.JsonSchema | undefined { + const annotations = check.annotations + const callback = annotations?.toJsonSchema + if (callback !== undefined) { + const schemas = annotationSchemas(check.representation, [...path, "representation"]) + const fragment = (callback as SchemaRepresentation.ToJsonSchema.Check)({ type, schemas }) + const ordinary = collectJsonSchemaAnnotations(annotations, options) + return ordinary === undefined ? fragment : { ...fragment, ...ordinary } + } + if (check._tag === "Filter") return undefined + + const children = check.checks + .map((child, index) => compileCheck(child, type, [...path, "checks", index])) + .filter((child): child is JsonSchema.JsonSchema => child !== undefined) + if (children.length === 0) return undefined + const ordinary = collectJsonSchemaAnnotations(annotations, options) + return ordinary === undefined ? { allOf: children } : { allOf: children, ...ordinary } + } + + function recur( + representation: SchemaRepresentation.Representation, + path: Path + ): JsonSchema.JsonSchema { + if (representation._tag === "Reference") { + const canonical = compileDefinition(representation.$ref, path) + return { $ref: `#/$defs/${escapeToken(canonical)}` } + } + const cached = compiledRepresentations.get(representation) + if (cached !== undefined) return cached + + let output = on(representation, path) + const ordinary = collectJsonSchemaAnnotations(representation.annotations, options) + if (ordinary !== undefined) { + output = { ...output, ...ordinary } + } + for (let index = 0; index < representation.checks.length; index++) { + const type = typeof output.type === "string" && isJsonSchemaType(output.type) ? output.type : undefined + const check = compileCheck(representation.checks[index], type, [...path, "checks", index]) + if (check !== undefined) { + output = appendJsonSchema(output, check) + } + } + compiledRepresentations.set(representation, output) + return output + } + + function on( + representation: Exclude, + path: Path + ): JsonSchema.JsonSchema { + switch (representation._tag) { + case "Any": + case "Unknown": + return {} + case "ObjectKeyword": + return { anyOf: [{ type: "object" }, { type: "array" }] } + case "Void": + case "Undefined": + case "Null": + return { type: "null" } + case "BigInt": + return { type: "string", allOf: [{ pattern: "^-?\\d+$" }] } + case "Symbol": + case "UniqueSymbol": + return { type: "string", allOf: [{ pattern: "^Symbol\\((.*)\\)$" }] } + case "Declaration": { + return {} + } + case "Suspend": + return recur(representation.thunk, [...path, "thunk"]) + case "Never": + return { not: {} } + case "String": + return { type: "string" } + case "Number": + return { + anyOf: [ + { type: "number" }, + { type: "string", enum: ["NaN"] }, + { type: "string", enum: ["Infinity"] }, + { type: "string", enum: ["-Infinity"] } + ] + } + case "Boolean": + return { type: "boolean" } + case "Literal": { + const literal = representation.literal + return typeof literal === "bigint" + ? { type: "string", enum: [globalThis.String(literal)] } + : { type: typeof literal, enum: [literal] } + } + case "Enum": { + const types = representation.enums.map(([title, literal]) => + typeof literal === "number" && !globalThis.Number.isFinite(literal) + ? { type: "string", enum: [globalThis.String(literal)], title } + : { type: typeof literal, enum: [literal], title } + ) + return types.length === 0 ? { not: {} } : { anyOf: types } + } + case "TemplateLiteral": + return { type: "string", pattern: `^${representation.parts.map(getPartPattern).join("")}$` } + case "Arrays": { + if (representation.rest.length > 1) { + throw errorWithPath("Invalid schema representation document", [...path, "rest"]) + } + const out: JsonSchema.JsonSchema = { type: "array" } + let minItems = representation.elements.length + const prefixItems = representation.elements.map((element, index) => { + if (element.isOptional) minItems-- + const compiled = recur(element.type, [...path, "elements", index, "type"]) + const annotations = collectJsonSchemaAnnotations(element.annotations, options) + return annotations === undefined ? compiled : appendJsonSchema(compiled, annotations) + }) + if (prefixItems.length > 0) { + out.prefixItems = prefixItems + out.maxItems = representation.elements.length + if (minItems > 0) out.minItems = minItems + } else { + out.items = false + } + if (representation.rest.length === 1) { + delete out.maxItems + const rest = recur(representation.rest[0], [...path, "rest", 0]) + if (Object.keys(rest).length > 0) out.items = rest + else delete out.items + } + return out + } + case "Objects": { + if (representation.propertySignatures.length === 0 && representation.indexSignatures.length === 0) { + return { anyOf: [{ type: "object" }, { type: "array" }] } + } + const out: JsonSchema.JsonSchema = { type: "object" } + const properties: Record = {} + const required: Array = [] + for (let index = 0; index < representation.propertySignatures.length; index++) { + const property = representation.propertySignatures[index] + if (typeof property.name !== "string") { + throw errorWithPath("Invalid schema representation document", [ + ...path, + "propertySignatures", + index, + "name" + ]) + } + const name = property.name + const compiled = recur(property.type, [...path, "propertySignatures", index, "type"]) + const annotations = collectJsonSchemaAnnotations(property.annotations, options) + InternalRecord.assignProperty( + properties, + name, + annotations === undefined ? compiled : appendJsonSchema(compiled, annotations) + ) + if (!property.isOptional) required.push(name) + } + if (representation.propertySignatures.length > 0) out.properties = properties + if (required.length > 0) out.required = required + out.additionalProperties = options?.additionalProperties ?? false + const patternProperties: Record = {} + for (let index = 0; index < representation.indexSignatures.length; index++) { + const signature = representation.indexSignatures[index] + let type: JsonSchema.JsonSchema | false = recur( + signature.type, + [...path, "indexSignatures", index, "type"] + ) + if (Object.keys(type).length === 1 && "not" in type) type = false + const patterns = getParameterPatterns( + signature.parameter, + [...path, "indexSignatures", index, "parameter"], + new Set() + ) + if (patterns.length === 0) { + out.additionalProperties = type + } else { + for (const pattern of patterns) InternalRecord.assignProperty(patternProperties, pattern, type) + } + } + if (Object.keys(patternProperties).length > 0) { + out.patternProperties = patternProperties + delete out.additionalProperties + } + if ( + typeof out.additionalProperties === "object" && + out.additionalProperties !== null && + Object.keys(out.additionalProperties).length === 0 + ) { + delete out.additionalProperties + } + return out + } + case "Union": { + const types = representation.types.map((type, index) => recur(type, [...path, "types", index])) + if (types.length === 0) return { not: {} } + if (types.length > 1) { + const compacted = compactEnums(types) + if (compacted !== undefined) return compacted + } + return representation.mode === "anyOf" ? { anyOf: types } : { oneOf: types } + } + } + } + + function getParameterPatterns( + parameter: SchemaRepresentation.Representation, + path: Path, + seenReferences: ReadonlySet + ): ReadonlyArray { + switch (parameter._tag) { + case "Reference": { + if (!Object.hasOwn(references, parameter.$ref)) { + throw errorWithPath(`Invalid reference ${parameter.$ref}`, [...path, "$ref"]) + } + compileDefinition(parameter.$ref, path) + if (seenReferences.has(parameter.$ref)) return [] + const next = new Set(seenReferences).add(parameter.$ref) + return getParameterPatterns(references[parameter.$ref], ["references", parameter.$ref], next) + } + case "String": + return collectPatterns(recur(parameter, path)) + case "TemplateLiteral": + return [`^${parameter.parts.map(getPartPattern).join("")}$`] + case "Union": + return parameter.types.flatMap((type, index) => + getParameterPatterns(type, [...path, "types", index], seenReferences) + ) + default: + throw errorWithPath("Invalid schema representation document", path) + } + } +} + +function isJsonSchemaType(input: string): input is JsonSchema.Type { + return input === "string" || input === "number" || input === "boolean" || input === "array" || + input === "object" || input === "null" || input === "integer" +} + +function compactEnums( + schemas: ReadonlyArray +): JsonSchema.JsonSchema | undefined { + let sharedType: unknown = undefined + const values: Array = [] + for (const schema of schemas) { + const keys = Object.keys(schema) + if (keys.length !== 2 || schema.type === undefined || !Array.isArray(schema.enum) || schema.enum.length === 0) { + return undefined + } + if (sharedType === undefined) sharedType = schema.type + else if (schema.type !== sharedType) return undefined + values.push(...schema.enum) + } + return { type: sharedType, enum: values } +} + +function collectPatterns(schema: JsonSchema.JsonSchema): ReadonlyArray { + const patterns: Array = [] + if (typeof schema.pattern === "string") patterns.push(schema.pattern) + for (const key of ["allOf", "anyOf", "oneOf"] as const) { + const members = schema[key] + if (Array.isArray(members)) { + for (const member of members) { + if (typeof member === "object" && member !== null && !Array.isArray(member)) { + patterns.push(...collectPatterns(member)) + } + } + } + } + return patterns +} + +function getPartPattern(part: SchemaRepresentation.Representation): string { + switch (part._tag) { + case "Literal": + return RegEx.escape(globalThis.String(part.literal)) + case "String": + return SchemaAST.STRING_PATTERN + case "Number": + return SchemaAST.FINITE_PATTERN + case "TemplateLiteral": + return part.parts.map(getPartPattern).join("") + case "Union": + return part.types.map(getPartPattern).join("|") + default: + throw errorWithPath("Invalid schema representation document", []) + } +} + +/** @internal */ +export function toJsonSchemaDocument( + document: SchemaRepresentation.Document, + options?: Schema.ToJsonSchemaOptions +): JsonSchema.Document<"draft-2020-12"> { + const output = compileJsonSchema( + [document.representation], + [["representation"]], + document.references, + options + ) + return { + dialect: output.dialect, + schema: output.schemas[0], + definitions: output.definitions + } +} + +/** @internal */ +export function toJsonSchemaMultiDocument( + document: SchemaRepresentation.MultiDocument, + options?: Schema.ToJsonSchemaOptions +): JsonSchema.MultiDocument<"draft-2020-12"> { + return compileJsonSchema( + document.representations, + document.representations.map((_, index) => ["representations", index]), + document.references, + options + ) +} diff --git a/.context/effect/packages/effect/src/internal/schema/toRepresentation.ts b/.context/effect/packages/effect/src/internal/schema/toRepresentation.ts new file mode 100644 index 000000000..0816d04e3 --- /dev/null +++ b/.context/effect/packages/effect/src/internal/schema/toRepresentation.ts @@ -0,0 +1,404 @@ +import * as Arr from "../../Array.ts" +import type * as Schema from "../../Schema.ts" +import * as SchemaAST from "../../SchemaAST.ts" +import type * as SchemaRepresentation from "../../SchemaRepresentation.ts" +import * as InternalRecord from "../record.ts" +import * as InternalAnnotations from "./annotations.ts" + +/** @internal */ +export function toRepresentation( + ast: SchemaAST.AST, + options?: Options +): SchemaRepresentation.Document { + const { references, representations } = toRepresentations([ast], options) + return { representation: representations[0], references } +} + +/** @internal */ +export function toRepresentations( + asts: readonly [SchemaAST.AST, ...Array], + options?: Options +): SchemaRepresentation.MultiDocument { + return fromASTs(asts, options) +} + +/** @internal */ +export interface Options { + readonly isAnonymousReferenceAllowed?: ((ast: SchemaAST.AST) => boolean) | undefined +} + +type CheckRepresentationAnnotation = SchemaRepresentation.CheckRepresentationAnnotation< + SchemaRepresentation.Representation +> + +function annotationsField(annotations: A | undefined): { readonly annotations: A } | undefined { + return annotations === undefined ? undefined : { annotations } +} + +function hasShareableStructure( + ast: SchemaAST.AST, + isAnonymousReferenceAllowed: Options["isAnonymousReferenceAllowed"] +): boolean { + if (isAnonymousReferenceAllowed?.(ast) === false) return false + switch (ast._tag) { + case "Arrays": + case "Objects": + case "Suspend": + return true + case "Declaration": + return true + case "Union": + return ast.types.some((ast) => hasShareableStructure(ast, isAnonymousReferenceAllowed)) + default: + return false + } +} + +function isWorthReferencing(bodyCost: number, occurrences: number): boolean { + return occurrences * bodyCost > bodyCost + occurrences + 1 +} + +function isAnonymousReferenceEligible( + ast: SchemaAST.AST, + occurrences: number, + isAnonymousReferenceAllowed: Options["isAnonymousReferenceAllowed"] +): boolean { + if (isAnonymousReferenceAllowed?.(ast) === false) return false + if (hasShareableStructure(ast, isAnonymousReferenceAllowed)) return true + switch (ast._tag) { + case "Union": + return isWorthReferencing(ast.types.length + 1, occurrences) + case "Enum": + return isWorthReferencing(ast.enums.length + 1, occurrences) + case "TemplateLiteral": + return isWorthReferencing(ast.parts.length + 1, occurrences) + case "Literal": + return typeof ast.literal === "string" && + isWorthReferencing(ast.literal.length / 32 + 1, occurrences) + default: + return false + } +} + +interface ReferenceIdentifier { + readonly identifier: string + readonly fallback?: string | undefined +} + +function resolveReferenceIdentifier( + input: SchemaAST.AST, + encoded: SchemaAST.AST +): ReferenceIdentifier | undefined { + const identifier = InternalAnnotations.resolveIdentifier(encoded) + if (identifier !== undefined) return { identifier } + const fallback = (encoded !== input ? InternalAnnotations.resolveIdentifier(input) : undefined) ?? + InternalAnnotations.resolveIdentifierFallback(encoded) + return fallback === undefined + ? undefined + : { identifier: `${fallback}Encoded`, fallback } +} + +function fromASTs( + asts: readonly [SchemaAST.AST, ...Array], + options: Options | undefined +): SchemaRepresentation.MultiDocument { + const references: Record = {} + const anonymousReferences = new Map() + const referenceOwners = new Map() + const buildingReferences = new Set() + const visiting = new Set() + const occurrences = new Map() + const shared = new Set() + + for (const ast of asts) visit(ast) + + const representations = Arr.map(asts, (ast) => recur(ast)) + + return { representations, references } + + function getReference(prefix: string, owner: SchemaAST.AST, separator = "_"): string { + let candidate = prefix + let suffix = 0 + while (referenceOwners.has(candidate)) { + if (referenceOwners.get(candidate) === owner) return candidate + candidate = `${prefix}${separator}${++suffix}` + } + referenceOwners.set(candidate, owner) + return candidate + } + + function annotateReference( + ast: SchemaAST.AST, + referenceIdentifier: ReferenceIdentifier, + reference: string + ): SchemaAST.AST { + const fallback = referenceIdentifier.fallback + if (fallback !== undefined) { + return InternalAnnotations.resolveIdentifierFallback(ast) === fallback + ? ast + : SchemaAST.annotate(ast, { + [InternalAnnotations.IDENTIFIER_FALLBACK_KEY]: fallback + }) + } + return reference === referenceIdentifier.identifier + ? ast + : SchemaAST.annotate(ast, { identifier: reference }) + } + + function makeReference(reference: string, ast: SchemaAST.AST): SchemaRepresentation.Reference { + if (!Object.hasOwn(references, reference) && !buildingReferences.has(reference)) { + buildingReferences.add(reference) + const representation = on(ast) + buildingReferences.delete(reference) + InternalRecord.assignProperty(references, reference, representation) + } + return { _tag: "Reference", $ref: reference } + } + + function visit(input: SchemaAST.AST): void { + const ast = SchemaAST.getLastEncoding(input) + const owner = SchemaAST.getContextOwner(ast) + const count = (occurrences.get(owner) ?? 0) + 1 + occurrences.set(owner, count) + if (count > 1) { + if ( + !shared.has(owner) && + isAnonymousReferenceEligible(owner, count, options?.isAnonymousReferenceAllowed) + ) shared.add(owner) + return + } + visitChecks(ast.checks) + switch (ast._tag) { + case "Declaration": + case "Arrays": + case "Objects": + case "Union": + ast.recur((child) => { + visit(child) + return child + }) + break + case "TemplateLiteral": + ast.parts.forEach(visit) + break + case "Suspend": + visit(ast.thunk()) + break + } + } + + function visitChecks(checks: SchemaAST.Checks | undefined): void { + checks?.forEach((check) => { + check.annotations?.representation?.schemas?.forEach((schema) => visit(SchemaAST.toType(schema))) + if (check._tag === "FilterGroup") visitChecks(check.checks) + }) + } + + function recur(input: SchemaAST.AST): SchemaRepresentation.Representation { + const ast = SchemaAST.getLastEncoding(input) + const owner = SchemaAST.getContextOwner(ast) + const referenceIdentifier = resolveReferenceIdentifier(input, ast) + if (referenceIdentifier !== undefined) { + const reference = getReference(referenceIdentifier.identifier, owner) + return makeReference(reference, annotateReference(ast, referenceIdentifier, reference)) + } + + const found = anonymousReferences.get(owner) + if (found !== undefined) { + return { _tag: "Reference", $ref: found } + } + + const isShared = shared.has(owner) + if (isShared || visiting.has(owner)) { + const reference = getReference(`${ast._tag}_`, owner, "") + anonymousReferences.set(owner, reference) + return isShared + ? makeReference(reference, ast) + : { _tag: "Reference", $ref: reference } + } + + visiting.add(owner) + const representation = on(ast) + visiting.delete(owner) + + const reference = anonymousReferences.get(owner) + if (reference !== undefined) { + InternalRecord.assignProperty(references, reference, representation) + return { _tag: "Reference", $ref: reference } + } + + return representation + } + + function on(ast: SchemaAST.AST): SchemaRepresentation.Representation { + const checks = fromChecks(ast.checks) + switch (ast._tag) { + case "Declaration": + return { + _tag: "Declaration", + typeParameters: ast.typeParameters.map((ast) => recur(ast)), + checks, + ...fromDeclarationAnnotations(ast.annotations) + } + case "Null": + case "Undefined": + case "Void": + case "Never": + case "Unknown": + case "Any": + case "String": + case "Boolean": + case "Number": + case "BigInt": + case "Symbol": + case "ObjectKeyword": + return { + _tag: ast._tag, + checks, + ...annotationsField(ast.annotations) + } + case "Literal": + return { + _tag: "Literal", + literal: ast.literal, + checks, + ...annotationsField(ast.annotations) + } + case "UniqueSymbol": + return { + _tag: "UniqueSymbol", + symbol: ast.symbol, + checks, + ...annotationsField(ast.annotations) + } + case "Enum": + return { + _tag: "Enum", + enums: ast.enums, + checks, + ...annotationsField(ast.annotations) + } + case "TemplateLiteral": + return { + _tag: "TemplateLiteral", + parts: ast.parts.map((ast) => recur(ast)), + checks, + ...annotationsField(ast.annotations) + } + case "Arrays": + return { + _tag: "Arrays", + elements: ast.elements.map((element) => { + const projected = SchemaAST.getLastEncoding(element) + const annotations = projected.context?.annotations + return { + isOptional: SchemaAST.isOptional(projected), + type: recur(element), + ...annotationsField(annotations) + } + }), + rest: ast.rest.map((ast) => recur(ast)), + checks, + ...annotationsField(ast.annotations) + } + case "Objects": + return { + _tag: "Objects", + propertySignatures: ast.propertySignatures.map((property) => { + const projected = SchemaAST.getLastEncoding(property.type) + const annotations = projected.context?.annotations + return { + name: property.name, + type: recur(property.type), + isOptional: SchemaAST.isOptional(projected), + isMutable: SchemaAST.isMutable(projected), + ...annotationsField(annotations) + } + }), + indexSignatures: ast.indexSignatures.map((index) => ({ + parameter: recur(index.parameter), + type: recur(index.type) + })), + checks, + ...annotationsField(ast.annotations) + } + case "Union": + return { + _tag: "Union", + types: ast.types.map((ast) => recur(ast)), + mode: ast.mode, + checks, + ...annotationsField(ast.annotations) + } + case "Suspend": + return { + _tag: "Suspend", + checks: [], + thunk: recur(ast.thunk()), + ...annotationsField(ast.annotations) + } + } + } + + function fromChecks( + checks: readonly [SchemaAST.Check, ...Array>] | undefined + ): Array { + return checks?.map(fromCheck) ?? [] + } + + function fromCheck( + check: SchemaAST.Check + ): SchemaRepresentation.Check { + switch (check._tag) { + case "Filter": + return { + _tag: "Filter", + aborted: check.aborted, + ...fromCheckAnnotations(check.annotations) + } + case "FilterGroup": + return { + _tag: "FilterGroup", + checks: Arr.map(check.checks, fromCheck), + ...fromCheckAnnotations(check.annotations) + } + } + } + + function fromDeclarationAnnotations< + A extends Schema.Annotations.Annotations & { + readonly representation?: SchemaRepresentation.RepresentationAnnotation | undefined + } + >(annotations: A | undefined): { + readonly representation?: SchemaRepresentation.RepresentationAnnotation | undefined + readonly annotations?: Omit | undefined + } | undefined { + if (annotations === undefined) return undefined + const { representation, ...ordinary } = annotations + return { + ...(representation === undefined ? undefined : { representation }), + ...(Object.keys(ordinary).length === 0 ? undefined : { annotations: ordinary }) + } + } + + function fromCheckAnnotations< + A extends Schema.Annotations.Annotations & { + readonly representation?: SchemaRepresentation.CheckRepresentationAnnotation | undefined + } + >(annotations: A | undefined): { + readonly representation?: CheckRepresentationAnnotation | undefined + readonly annotations?: Omit | undefined + } | undefined { + if (annotations === undefined) return undefined + const { representation, ...ordinary } = annotations + const projected = representation === undefined + ? undefined + : representation.schemas === undefined + ? representation as CheckRepresentationAnnotation + : { ...representation, schemas: representation.schemas.map((schema) => recur(SchemaAST.toType(schema))) } + return { + ...(projected === undefined ? undefined : { representation: projected }), + ...(Object.keys(ordinary).length === 0 ? undefined : { annotations: ordinary }) + } + } +} diff --git a/.context/effect/packages/effect/src/internal/stackTraceLimit.ts b/.context/effect/packages/effect/src/internal/stackTraceLimit.ts index 440e18044..7a7e7df8d 100644 --- a/.context/effect/packages/effect/src/internal/stackTraceLimit.ts +++ b/.context/effect/packages/effect/src/internal/stackTraceLimit.ts @@ -16,10 +16,6 @@ */ import type { ErrorWithStackTraceLimit } from "./tracer.ts" -const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor -const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty -const ObjectIsExtensible = Object.isExtensible - /** * Check if `Error.stackTraceLimit` is writable. * Returns `false` if the property is frozen, non-writable, or `Error` is non-extensible. @@ -27,12 +23,12 @@ const ObjectIsExtensible = Object.isExtensible * @internal */ export const isStackTraceLimitWritable = (): boolean => { - const desc = ObjectGetOwnPropertyDescriptor(Error, "stackTraceLimit") + const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit") if (desc === undefined) { - return ObjectIsExtensible(Error) + return Object.isExtensible(Error) } - return ObjectPrototypeHasOwnProperty.call(desc, "writable") + return Object.hasOwn(desc, "writable") ? desc.writable === true : desc.set !== undefined } diff --git a/.context/effect/packages/effect/src/internal/trie.ts b/.context/effect/packages/effect/src/internal/trie.ts index b9e412dc4..03db686cb 100644 --- a/.context/effect/packages/effect/src/internal/trie.ts +++ b/.context/effect/packages/effect/src/internal/trie.ts @@ -42,7 +42,7 @@ const TrieProto: TR.Trie = { return hash }, [Equal.symbol](this: TrieImpl, that: unknown): boolean { - if (isTrie(that)) { + if (isTrie(that) && size(this) === size(that)) { const entries = Array.from(that) return Array.from(this).every((itemSelf, i) => { const itemThat = entries[i] @@ -103,8 +103,8 @@ class TrieIterator implements IterableIterator { 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) } + if (this.filter(key, value.value)) { + return { done: false, value: this.f(key, value.value) } } } } else { @@ -172,7 +172,7 @@ export const insert = dual< key: key[0], count: 0 } - const count = n.count + 1 + let count = n.count + 1 let cIndex = 0 while (cIndex < key.length) { @@ -194,7 +194,17 @@ export const insert = dual< } } else { if (cIndex === key.length - 1) { - n.value = value + if (n.value !== undefined) { + count -= 1 + } + nStack[nStack.length - 1] = { + key: n.key, + count, + value: { value }, + left: n.left, + mid: n.mid, + right: n.right + } } else if (n.mid === undefined) { dStack.push(0) n = { key: key[cIndex + 1], count } @@ -403,7 +413,7 @@ export const get = dual< } } else { if (cIndex === key.length - 1) { - return Option.fromUndefinedOr(n.value) + return n.value === undefined ? Option.none() : Option.some(n.value.value) } else { if (n.mid === undefined) { return Option.none() @@ -632,7 +642,7 @@ export const modify = dual< nStack[nStack.length - 1] = { key: updateNode.key, count: updateNode.count, - value: f(updateNode.value), // Update + value: { value: f(updateNode.value.value) }, // Update left: updateNode.left, mid: updateNode.mid, right: updateNode.right @@ -693,10 +703,6 @@ export const longestPrefixOf = dual< let cIndex = 0 while (cIndex < key.length) { const c = key[cIndex] - if (n.value !== undefined) { - longestPrefixNode = Option.some([key.slice(0, cIndex + 1), n.value]) - } - if (c > n.key) { if (n.right === undefined) { break @@ -710,6 +716,9 @@ export const longestPrefixOf = dual< n = n.left } } else { + if (n.value !== undefined) { + longestPrefixNode = Option.some([key.slice(0, cIndex + 1), n.value.value]) + } if (n.mid === undefined) { break } else { @@ -726,7 +735,7 @@ export const longestPrefixOf = dual< interface Node { key: string count: number - value?: V | undefined + value?: { readonly value: V } | undefined left?: Node | undefined mid?: Node | undefined right?: Node | undefined diff --git a/.context/effect/packages/effect/src/internal/version.ts b/.context/effect/packages/effect/src/internal/version.ts deleted file mode 100644 index 31a362ac6..000000000 --- a/.context/effect/packages/effect/src/internal/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const version: version = "dev" -export type version = "dev" diff --git a/.context/effect/packages/effect/src/testing/FastCheck.ts b/.context/effect/packages/effect/src/testing/FastCheck.ts index 1ce177c61..d8a7c2245 100644 --- a/.context/effect/packages/effect/src/testing/FastCheck.ts +++ b/.context/effect/packages/effect/src/testing/FastCheck.ts @@ -20,7 +20,7 @@ * * **Example** (Checking an array reversal property) * - * ```ts + * ```ts import.meta.vitest * import { FastCheck } from "effect/testing" * * // Property: reverse of reverse should equal original @@ -29,17 +29,18 @@ * (arr: Array) => { * const reversed = arr.slice().reverse() * const doubleReversed = reversed.slice().reverse() - * return JSON.stringify(arr) === JSON.stringify(doubleReversed) + * return arr.length === doubleReversed.length && + * arr.every((value, index) => value === doubleReversed[index]) * } * ) * * // Run the property test - * FastCheck.assert(reverseProp) + * FastCheck.assert(reverseProp, { seed: 1, numRuns: 100 }) // => undefined * ``` * * **Example** (Checking string concatenation properties) * - * ```ts + * ```ts import.meta.vitest * import { FastCheck } from "effect/testing" * * // Test string concatenation properties @@ -54,12 +55,12 @@ * } * ) * - * FastCheck.assert(concatProp) + * FastCheck.assert(concatProp, { seed: 2, numRuns: 100 }) // => undefined * ``` * * **Example** (Generating record data for properties) * - * ```ts + * ```ts import.meta.vitest * import { FastCheck } from "effect/testing" * * // Generate random data for testing @@ -80,7 +81,7 @@ * } * ) * - * FastCheck.assert(validPersonProp) + * FastCheck.assert(validPersonProp, { seed: 3, numRuns: 100 }) // => undefined * ``` * * @category re-exports diff --git a/.context/effect/packages/effect/src/testing/TestClock.ts b/.context/effect/packages/effect/src/testing/TestClock.ts index 0740d41b3..0cff8aafa 100644 --- a/.context/effect/packages/effect/src/testing/TestClock.ts +++ b/.context/effect/packages/effect/src/testing/TestClock.ts @@ -44,27 +44,28 @@ import * as Semaphore from "../Semaphore.ts" * * Tests `Effect.timeout` using `TestClock`. * - * ```ts - * import { Effect, Fiber, Option, pipe } from "effect" + * ```ts import.meta.vitest + * import { Effect, Exit, Fiber, pipe } from "effect" * import { TestClock } from "effect/testing" - * import * as assert from "node:assert" * - * Effect.gen(function*() { + * const program = Effect.gen(function*() { * const fiber = yield* pipe( * Effect.sleep("5 minutes"), * Effect.timeout("1 minute"), * Effect.forkChild * ) * yield* TestClock.adjust("1 minute") - * const result = yield* Fiber.join(fiber) - * assert.deepStrictEqual(result, Option.none()) + * const exit = yield* Fiber.await(fiber) + * Exit.isFailure(exit) // => true * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * **Example** (Advancing time deterministically) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * import { TestClock } from "effect/testing" * * const program = Effect.gen(function*() { @@ -78,10 +79,13 @@ import * as Semaphore from "../Semaphore.ts" * * // Advance the test clock by 1 hour * yield* TestClock.adjust("1 hour") + * yield* Fiber.join(fiber) * * // The effect should now be executed - * console.log(executed) // true + * executed // => true * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * @category models @@ -111,7 +115,7 @@ export interface TestClock extends Clock.Clock { * * **Example** (Configuring a test clock) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -123,8 +127,10 @@ export interface TestClock extends Clock.Clock { * * // Access the current state * const currentTime = testClock.currentTimeMillisUnsafe() - * console.log(currentTime) // 0 (starts at epoch) + * currentTime // => 0 * }) + * + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @since 2.0.0 @@ -137,7 +143,7 @@ export declare namespace TestClock { * * **Example** (Configuring the warning delay) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -149,7 +155,10 @@ export declare namespace TestClock { * * // Use the TestClock in your test * yield* testClock.adjust("1 hour") + * testClock.currentTimeMillisUnsafe() // => 3_600_000 * }) + * + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category options @@ -194,12 +203,20 @@ const SleepOrder = Order.flip(Order.Struct({ sequence: Order.Number })) +const nanosPerMilli = BigInt(1_000_000) + +const millisToNanos = (millis: number): bigint => { + const wholeMillis = Math.floor(millis) + const fractionalNanos = Math.floor((millis - wholeMillis) * 1_000_000) + return BigInt(wholeMillis) * nanosPerMilli + BigInt(fractionalNanos) +} + /** * Creates a `TestClock` with optional configuration. * * **Example** (Creating a test clock) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -215,8 +232,10 @@ const SleepOrder = Order.flip(Order.Struct({ * // Use the TestClock to control time in tests * yield* testClock.adjust("1 hour") * const currentTime = testClock.currentTimeMillisUnsafe() - * console.log(currentTime) // Time advanced by 1 hour + * currentTime // => 3_600_000 * }) + * + * await Effect.runPromise(Effect.scoped(program)) * ``` * * @category constructors @@ -225,7 +244,7 @@ const SleepOrder = Order.flip(Order.Struct({ export const make = Effect.fnUntraced(function*( options?: TestClock.Options ) { - const config = Object.assign({}, defaultOptions, options) + const config = { ...defaultOptions, ...options } let sequence = 0 const sleeps: Array<{ readonly sequence: number @@ -236,6 +255,8 @@ export const make = Effect.fnUntraced(function*( const warningSemaphore = yield* Semaphore.make(1) let currentTimestamp: number = new Date(0).getTime() + let currentWallNanos = BigInt(0) + let currentMonotonicNanos = BigInt(0) let warningState: WarningState = WarningState.Start() function currentTimeMillisUnsafe(): number { @@ -243,11 +264,16 @@ export const make = Effect.fnUntraced(function*( } function currentTimeNanosUnsafe(): bigint { - return BigInt(Math.floor(currentTimestamp * 1000000)) + return currentWallNanos + } + + function monotonicTimeNanosUnsafe(): bigint { + return currentMonotonicNanos } const currentTimeMillis = Effect.sync(currentTimeMillisUnsafe) const currentTimeNanos = Effect.sync(currentTimeNanosUnsafe) + const monotonicTimeNanos = Effect.sync(monotonicTimeNanosUnsafe) function withLive(effect: Effect.Effect) { return Effect.provideService(effect, Clock.Clock, liveClock) @@ -317,14 +343,24 @@ export const make = Effect.fnUntraced(function*( const run = Effect.fnUntraced(function*(step: (currentTimestamp: number) => number) { yield* Fiber.await(yield* Effect.forkChild(Effect.yieldNow)) const endTimestamp = step(currentTimestamp) + const advanceTo = (timestamp: number) => { + const deltaMillis = timestamp - currentTimestamp + if (deltaMillis > 0 && Number.isFinite(deltaMillis)) { + currentMonotonicNanos += BigInt(Math.round(deltaMillis * 1_000_000)) + } + if (Number.isFinite(timestamp)) { + currentWallNanos = millisToNanos(timestamp) + } + currentTimestamp = timestamp + } while (Arr.isArrayNonEmpty(sleeps)) { if (Arr.lastNonEmpty(sleeps).timestamp > endTimestamp) break const entry = sleeps.pop()! - currentTimestamp = entry.timestamp + advanceTo(entry.timestamp) entry.latch.openUnsafe() yield* Effect.yieldNow } - currentTimestamp = endTimestamp + advanceTo(endTimestamp) }, runSemaphore.withPermits(1)) function adjust(duration: Duration.Input) { @@ -341,8 +377,10 @@ export const make = Effect.fnUntraced(function*( return { currentTimeMillisUnsafe, currentTimeNanosUnsafe, + monotonicTimeNanosUnsafe, currentTimeMillis, currentTimeNanos, + monotonicTimeNanos, adjust, setTime, sleep, @@ -355,7 +393,7 @@ export const make = Effect.fnUntraced(function*( * * **Example** (Providing a test clock layer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -370,7 +408,13 @@ export const make = Effect.fnUntraced(function*( * const program = Effect.gen(function*() { * // Use the layer in your program * yield* TestClock.adjust("1 hour") - * }).pipe(Effect.provide(testClockLayer)) + * return yield* TestClock.testClockWith((testClock) => + * Effect.succeed(testClock.currentTimeMillisUnsafe()) + * ) + * }) + * + * await Effect.runPromise(Effect.provide(program, testClockLayer)) // => 3_600_000 + * await Effect.runPromise(Effect.provide(program, customTestClockLayer)) // => 3_600_000 * ``` * * @category layers @@ -387,7 +431,7 @@ export const layer: (options?: TestClock.Options) => Layer.Layer = fl * * **Example** (Accessing the test clock) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestClock } from "effect/testing" * @@ -400,8 +444,10 @@ export const layer: (options?: TestClock.Options) => Layer.Layer = fl * // Adjust time using the TestClock instance * yield* TestClock.testClockWith((testClock) => testClock.adjust("2 hours")) * - * console.log(currentTime) // Initial time + * currentTime // => 0 * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * @category testing @@ -418,8 +464,8 @@ export const testClockWith = ( * * **Example** (Advancing the test clock) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Fiber } from "effect" * import { TestClock } from "effect/testing" * * const program = Effect.gen(function*() { @@ -433,10 +479,13 @@ export const testClockWith = ( * * // Advance the clock by 30 minutes * yield* TestClock.adjust("30 minutes") + * yield* Fiber.join(fiber) * * // The effect should now be executed - * console.log(executed) // true + * executed // => true * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * @category testing @@ -451,8 +500,8 @@ export const adjust = (duration: Duration.Input): Effect.Effect => * * **Example** (Setting the test clock time) * - * ```ts - * import { Duration, Effect } from "effect" + * ```ts import.meta.vitest + * import { Duration, Effect, Fiber } from "effect" * import { TestClock } from "effect/testing" * * const program = Effect.gen(function*() { @@ -467,10 +516,13 @@ export const adjust = (duration: Duration.Input): Effect.Effect => * // Set the clock to a specific timestamp (2 hours from epoch) * const targetTime = Duration.toMillis(Duration.hours(2)) * yield* TestClock.setTime(targetTime) + * yield* Fiber.join(fiber) * * // The effect should now be executed - * console.log(executed) // true + * executed // => true * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * @category testing @@ -485,26 +537,28 @@ export const setTime = (timestamp: number): Effect.Effect => * * **Example** (Running with the live clock) * - * ```ts + * ```ts import.meta.vitest * import { Clock, Effect } from "effect" * import { TestClock } from "effect/testing" * * const program = Effect.gen(function*() { * // Get the current test time (starts at epoch) * const testTime = yield* Clock.currentTimeMillis - * console.log(testTime) // 0 + * testTime // => 0 * * // Get the actual system time using withLive * const realTime = yield* TestClock.withLive(Clock.currentTimeMillis) - * console.log(realTime) // Actual system timestamp + * Number.isFinite(realTime) // => true * * // Advance test time * yield* TestClock.adjust("1 hour") * * // Test time is now 1 hour ahead * const newTestTime = yield* Clock.currentTimeMillis - * console.log(newTestTime) // 3600000 (1 hour in milliseconds) + * newTestTime // => 3_600_000 * }) + * + * await Effect.runPromise(Effect.provide(program, TestClock.layer())) * ``` * * @category testing diff --git a/.context/effect/packages/effect/src/testing/TestConsole.ts b/.context/effect/packages/effect/src/testing/TestConsole.ts index aa85d3e50..b8a5a52f6 100644 --- a/.context/effect/packages/effect/src/testing/TestConsole.ts +++ b/.context/effect/packages/effect/src/testing/TestConsole.ts @@ -31,7 +31,7 @@ import * as Layer from "../Layer.ts" * * **Example** (Capturing console output in tests) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -42,9 +42,11 @@ import * as Layer from "../Layer.ts" * const logs = yield* TestConsole.logLines * const errors = yield* TestConsole.errorLines * - * console.log(logs) // [["Hello, World!"]] - * console.log(errors) // [["An error occurred"]] + * logs // => ["Hello, World!"] + * errors // => ["An error occurred"] * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link layer} for providing `TestConsole` to an effect @@ -99,12 +101,10 @@ export declare namespace TestConsole { * * **Example** (Typing captured console methods) * - * ```ts + * ```ts import.meta.vitest * import type { TestConsole } from "effect/testing" * * const method: TestConsole.TestConsole.Method = "log" - * - * console.log(method) // "log" * ``` * * @category models @@ -122,7 +122,7 @@ export declare namespace TestConsole { * * **Example** (Typing captured console entries) * - * ```ts + * ```ts import.meta.vitest * import type { TestConsole } from "effect/testing" * * const entry: TestConsole.TestConsole.Entry = { @@ -130,8 +130,7 @@ export declare namespace TestConsole { * parameters: ["not found"] * } * - * console.log(entry.method) // "error" - * console.log(entry.parameters) // ["not found"] + * entry // => { method: "error", parameters: ["not found"] } * ``` * * @category models @@ -154,7 +153,7 @@ export declare namespace TestConsole { * * **Example** (Creating a test console) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -165,9 +164,11 @@ export declare namespace TestConsole { * const logs = yield* TestConsole.logLines * const errors = yield* TestConsole.errorLines * - * console.log("Captured logs:", logs) - * console.log("Captured errors:", errors) + * logs // => ["Debug message"] + * errors // => ["Error occurred"] * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link layer} for providing a `TestConsole` as a `Layer` @@ -225,7 +226,7 @@ export const make = Effect.gen(function*() { * * **Example** (Accessing the test console service) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -237,10 +238,12 @@ export const make = Effect.gen(function*() { * const logs = yield* testConsole.logLines * const errors = yield* testConsole.errorLines * - * console.log("Logs:", logs) // [["Test message"]] - * console.log("Errors:", errors) // [["Test error"]] + * logs // => ["Test message"] + * errors // => ["Test error"] * }) * ).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link layer} for providing the test console service @@ -264,7 +267,7 @@ export const testConsoleWith = (f: (console: TestConsole) => Effect.Eff * * **Example** (Providing a test console layer) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -275,9 +278,11 @@ export const testConsoleWith = (f: (console: TestConsole) => Effect.Eff * const logs = yield* TestConsole.logLines * const errors = yield* TestConsole.errorLines * - * console.log("Captured logs:", logs) - * console.log("Captured errors:", errors) + * logs // => ["This will be captured"] + * errors // => ["This error will be captured"] * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link make} for constructing the service value directly @@ -299,7 +304,7 @@ export const layer: Layer.Layer = Layer.effect(Console.Console)(mak * * **Example** (Reading captured log lines) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -310,13 +315,10 @@ export const layer: Layer.Layer = Layer.effect(Console.Console)(mak * * const logs = yield* TestConsole.logLines * - * console.log(logs) - * // [ - * // ["First message"], - * // ["Second message", { key: "value" }], - * // ["Third message", 42, true] - * // ] + * logs // => ["First message", "Second message", { key: "value" }, "Third message", 42, true] * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link errorLines} for reading captured `Console.error` output @@ -340,7 +342,7 @@ export const logLines: Effect.Effect, never, never> = tes * * **Example** (Reading captured error lines) * - * ```ts + * ```ts import.meta.vitest * import { Console, Effect } from "effect" * import { TestConsole } from "effect/testing" * @@ -350,12 +352,11 @@ export const logLines: Effect.Effect, never, never> = tes * * const errors = yield* TestConsole.errorLines * - * console.log(errors) - * // [ - * // ["Error message"], - * // ["Another error", Error: Something went wrong] - * // ] + * const messages = [errors[0], errors[1], errors[2] instanceof Error ? errors[2].message : undefined] + * messages // => ["Error message", "Another error", "Something went wrong"] * }).pipe(Effect.provide(TestConsole.layer)) + * + * await Effect.runPromise(program) * ``` * * @see {@link logLines} for reading captured `Console.log` output diff --git a/.context/effect/packages/effect/src/testing/TestSchema.ts b/.context/effect/packages/effect/src/testing/TestSchema.ts index 2a46d6993..88e55f97f 100644 --- a/.context/effect/packages/effect/src/testing/TestSchema.ts +++ b/.context/effect/packages/effect/src/testing/TestSchema.ts @@ -17,7 +17,7 @@ import * as Record from "../Record.ts" import * as Result from "../Result.ts" import * as Schema from "../Schema.ts" import * as SchemaAST from "../SchemaAST.ts" -import type * as SchemaIssue from "../SchemaIssue.ts" +import * as SchemaIssue from "../SchemaIssue.ts" import * as SchemaParser from "../SchemaParser.ts" import * as FastCheck from "../testing/FastCheck.ts" @@ -30,7 +30,7 @@ import * as FastCheck from "../testing/FastCheck.ts" * * **Example** (Decoding and encoding a struct) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * @@ -38,10 +38,10 @@ import * as FastCheck from "../testing/FastCheck.ts" * const asserts = new TestSchema.Asserts(schema) * * // decoding - * await asserts.decoding().succeed({ name: "Alice" }) + * await asserts.decoding().succeed({ name: "Alice" }) // => undefined * * // encoding - * await asserts.encoding().succeed({ name: "Alice" }) + * await asserts.encoding().succeed({ name: "Alice" }) // => undefined * ``` * * @see {@link Decoding} @@ -64,13 +64,13 @@ export class Asserts { * * **Example** (Comparing struct fields) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const fieldsA = { name: Schema.String } * const fieldsB = { name: Schema.String } - * TestSchema.Asserts.ast.fields.equals(fieldsA, fieldsB) // no error + * TestSchema.Asserts.ast.fields.equals(fieldsA, fieldsB) // => undefined * ``` */ static ast = { @@ -104,13 +104,13 @@ export class Asserts { * * **Example** (Testing make) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const schema = Schema.String * const asserts = new TestSchema.Asserts(schema) - * await asserts.make().succeed("hello") + * await asserts.make().succeed("hello") // => undefined * ``` * * @see {@link decoding} for assertions against decoded input @@ -123,7 +123,7 @@ export class Asserts { async function succeed(input: S["~type.make.in"], expected?: S["Type"]) { const r = await Effect.runPromise( makeEffect(input, options).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -135,7 +135,7 @@ export class Asserts { async fail(input: unknown, message: string) { const r = await Effect.runPromise( makeEffect(input, options).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -157,12 +157,12 @@ export class Asserts { * * **Example** (Verifying round trips) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * - * const asserts = new TestSchema.Asserts(Schema.NumberFromString) - * await asserts.verifyLosslessTransformation() + * const asserts = new TestSchema.Asserts(Schema.String) + * await asserts.verifyLosslessTransformation({ params: { seed: 1, numRuns: 20 } }) // => undefined * ``` * * @see {@link arbitrary} for checking that generated values satisfy the schema @@ -172,13 +172,13 @@ export class Asserts { }) { const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(this.schema) const encodeEffect = SchemaParser.encodeEffect(this.schema) - const arbitrary = Schema.toArbitrary(this.schema) + const arbitrary = Schema.toArbitrary(this.schema)(FastCheck) return FastCheck.assert( FastCheck.asyncProperty(arbitrary, async (t) => { const r = await Effect.runPromise( encodeEffect(t).pipe( Effect.flatMapEager((e) => decodeUnknownEffect(e)), - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -200,14 +200,14 @@ export class Asserts { * * **Example** (Decoding assertions) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.NumberFromString) * const decoding = asserts.decoding() - * await decoding.succeed("42", 42) - * await decoding.fail(null, "Expected string, got null") + * await decoding.succeed("42", 42) // => undefined + * await decoding.fail(null, "Expected string") // => undefined * ``` * * @see {@link Decoding} @@ -231,13 +231,13 @@ export class Asserts { * * **Example** (Encoding assertions) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.NumberFromString) * const encoding = asserts.encoding() - * await encoding.succeed(42, "42") + * await encoding.succeed(42, "42") // => undefined * ``` * * @see {@link Encoding} @@ -262,12 +262,12 @@ export class Asserts { * * **Example** (Verifying arbitrary generation) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.String) - * asserts.arbitrary().verifyGeneration() + * asserts.arbitrary().verifyGeneration({ params: { seed: 1, numRuns: 20 } }) // => undefined * ``` * * @see {@link verifyLosslessTransformation} for property-based round-trip checks @@ -280,7 +280,7 @@ export class Asserts { }) { const params = options?.params const is = Schema.is(schema) - const arb = Schema.toArbitrary(schema) + const arb = Schema.toArbitrary(schema)(FastCheck) FastCheck.assert(FastCheck.property(arb, (a) => is(a)), { numRuns: 20, ...params }) } } @@ -300,13 +300,13 @@ export class Asserts { * * **Example** (Decoding with service provision) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const asserts = new TestSchema.Asserts(Schema.String) * const decoding = asserts.decoding() - * await decoding.succeed("hello") + * await decoding.succeed("hello") // => undefined * ``` * * @see {@link Asserts} @@ -341,12 +341,12 @@ export class Decoding { * * **Example** (Testing identity and transformed decoding) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const decoding = new TestSchema.Asserts(Schema.NumberFromString).decoding() - * await decoding.succeed("1", 1) // transformed + * await decoding.succeed("1", 1) // => undefined * ``` * * @see {@link fail} for asserting decoding failures @@ -367,7 +367,7 @@ export class Decoding { ) { const r = await Effect.runPromise( this.decodeUnknownEffect(input, this.options?.parseOptions).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -384,12 +384,12 @@ export class Decoding { * * **Example** (Asserting a decoding failure) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const decoding = new TestSchema.Asserts(Schema.String).decoding() - * await decoding.fail(42, "Expected string, got 42") + * await decoding.fail(42, "Expected string") // => undefined * ``` * * @see {@link succeed} for asserting successful decoding @@ -401,7 +401,7 @@ export class Decoding { ) { const r = await Effect.runPromise( this.decodeUnknownEffect(input, this.options?.parseOptions).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -440,12 +440,12 @@ export class Decoding { * * **Example** (Encoding assertions) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() - * await encoding.succeed(42, "42") + * await encoding.succeed(42, "42") // => undefined * ``` * * @see {@link Asserts} @@ -459,7 +459,7 @@ export class Encoding { readonly encodeUnknownEffect: ( input: unknown, options?: SchemaAST.ParseOptions - ) => Effect.Effect + ) => Effect.Effect readonly options?: { readonly parseOptions?: SchemaAST.ParseOptions | undefined } | undefined @@ -481,12 +481,12 @@ export class Encoding { * * **Example** (Testing identity and transformed encoding) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() - * await encoding.succeed(1, "1") // transformed + * await encoding.succeed(1, "1") // => undefined * ``` * * @see {@link fail} for asserting encoding failures @@ -507,7 +507,7 @@ export class Encoding { ) { const r = await Effect.runPromise( this.encodeUnknownEffect(input, this.options?.parseOptions).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) @@ -524,12 +524,12 @@ export class Encoding { * * **Example** (Asserting an encoding failure) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { TestSchema } from "effect/testing" * * const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() - * await encoding.fail("not-a-number", "Expected number, got \"not-a-number\"") + * await encoding.fail("not-a-number", "Expected number") // => undefined * ``` * * @see {@link succeed} for asserting successful encoding @@ -541,7 +541,7 @@ export class Encoding { ) { const r = await Effect.runPromise( this.encodeUnknownEffect(input, this.options?.parseOptions).pipe( - Effect.mapErrorEager((issue) => issue.toString()), + Effect.mapErrorEager(SchemaIssue.defaultFormatter), Effect.result ) ) diff --git a/.context/effect/packages/effect/src/unstable/ai/AiError.ts b/.context/effect/packages/effect/src/unstable/ai/AiError.ts index b5a7005ad..bac73a0df 100644 --- a/.context/effect/packages/effect/src/unstable/ai/AiError.ts +++ b/.context/effect/packages/effect/src/unstable/ai/AiError.ts @@ -13,6 +13,7 @@ */ import * as Duration from "../../Duration.ts" import * as Effect from "../../Effect.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Option from "../../Option.ts" import * as Predicate from "../../Predicate.ts" import { redact } from "../../Redactable.ts" @@ -34,7 +35,7 @@ const redactHeaders = (headers: Record): Record const result: Record = {} for (const key in redacted) { const value = redacted[key] - result[key] = Redacted.isRedacted(value) ? value.toString() : value + InternalRecord.assignProperty(result, key, Redacted.isRedacted(value) ? value.toString() : value) } return result } @@ -54,7 +55,7 @@ const redactHeaders = (headers: Record): Record * * **Example** (Creating a network error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const error = new AiError.NetworkError({ @@ -69,15 +70,13 @@ const redactHeaders = (headers: Record): Record * description: "Connection timeout after 30 seconds" * }) * - * console.log(error.isRetryable) // true - * console.log(error.message) - * // "Transport: Connection timeout after 30 seconds (POST https://api.openai.com/v1/completions)" + * const result = [error.reason, error.isRetryable] // => ["TransportError", true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class NetworkError extends Schema.ErrorClass( +export class NetworkError extends Schema.Error( "effect/ai/AiError/NetworkError" )({ _tag: Schema.tag("NetworkError"), @@ -106,13 +105,17 @@ export class NetworkError extends Schema.ErrorClass( * * **Example** (Creating a network error from a request error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" - * import type { HttpClientError } from "effect/unstable/http" + * import { HttpClientError, HttpClientRequest } from "effect/unstable/http" * - * declare const platformError: HttpClientError.RequestError + * const platformError = new HttpClientError.TransportError({ + * request: HttpClientRequest.get("https://example.com/models"), + * description: "Connection refused" + * }) * * const aiError = AiError.NetworkError.fromRequestError(platformError) + * aiError.reason // => "TransportError" * ``` * * @since 4.0.0 @@ -183,7 +186,7 @@ export class NetworkError extends Schema.ErrorClass( * * **Example** (Inspecting metadata shape) * - * ```ts + * ```ts import.meta.vitest * const metadata = { * openai: { * errorCode: "rate_limit_exceeded", @@ -191,6 +194,8 @@ export class NetworkError extends Schema.ErrorClass( * }, * anthropic: null * } + * + * Array.of(metadata.openai.errorCode, metadata.anthropic) // => ["rate_limit_exceeded", null] * ``` * * @category schemas @@ -306,9 +311,9 @@ export interface UnknownErrorMetadata extends ProviderMetadata {} * @since 4.0.0 */ export const UsageInfo = Schema.Struct({ - promptTokens: Schema.optional(Schema.Number), - completionTokens: Schema.optional(Schema.Number), - totalTokens: Schema.optional(Schema.Number) + promptTokens: Schema.optional(Schema.Int), + completionTokens: Schema.optional(Schema.Int), + totalTokens: Schema.optional(Schema.Int) }).annotate({ identifier: "UsageInfo" }) /** @@ -350,7 +355,7 @@ export const HttpContext = Schema.Struct({ * * **Example** (Creating a rate limit error) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * import { AiError } from "effect/unstable/ai" * @@ -358,14 +363,13 @@ export const HttpContext = Schema.Struct({ * retryAfter: Duration.seconds(60) * }) * - * console.log(rateLimitError.isRetryable) // true - * console.log(rateLimitError.message) // "Rate limit exceeded. Retry after 1 minute" + * const result = [rateLimitError._tag, rateLimitError.isRetryable] // => ["RateLimitError", true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class RateLimitError extends Schema.ErrorClass( +export class RateLimitError extends Schema.Error( "effect/ai/AiError/RateLimitError" )({ _tag: Schema.tag("RateLimitError"), @@ -405,20 +409,18 @@ export class RateLimitError extends Schema.ErrorClass( * * **Example** (Creating a quota exhausted error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const quotaError = new AiError.QuotaExhaustedError({}) * - * console.log(quotaError.isRetryable) // false - * console.log(quotaError.message) - * // "Quota exhausted. Check your account billing and usage limits." + * const result = [quotaError._tag, quotaError.isRetryable] // => ["QuotaExhaustedError", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class QuotaExhaustedError extends Schema.ErrorClass( +export class QuotaExhaustedError extends Schema.Error( "effect/ai/AiError/QuotaExhaustedError" )({ _tag: Schema.tag("QuotaExhaustedError"), @@ -458,22 +460,20 @@ export class QuotaExhaustedError extends Schema.ErrorClass( * * **Example** (Creating an authentication error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const authError = new AiError.AuthenticationError({ * kind: "InvalidKey" * }) * - * console.log(authError.isRetryable) // false - * console.log(authError.message) - * // "InvalidKey: Verify your API key is correct" + * const result = [authError.kind, authError.isRetryable] // => ["InvalidKey", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class AuthenticationError extends Schema.ErrorClass( +export class AuthenticationError extends Schema.Error( "effect/ai/AiError/AuthenticationError" )({ _tag: Schema.tag("AuthenticationError"), @@ -518,22 +518,20 @@ export class AuthenticationError extends Schema.ErrorClass( * * **Example** (Creating a content policy error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const policyError = new AiError.ContentPolicyError({ * description: "Input contains prohibited content" * }) * - * console.log(policyError.isRetryable) // false - * console.log(policyError.message) - * // "Content policy violation: Input contains prohibited content" + * const result = [policyError.description, policyError.isRetryable] // => ["Input contains prohibited content", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ContentPolicyError extends Schema.ErrorClass( +export class ContentPolicyError extends Schema.Error( "effect/ai/AiError/ContentPolicyError" )({ _tag: Schema.tag("ContentPolicyError"), @@ -571,7 +569,7 @@ export class ContentPolicyError extends Schema.ErrorClass( * * **Example** (Creating an invalid request error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const invalidRequestError = new AiError.InvalidRequestError({ @@ -580,15 +578,13 @@ export class ContentPolicyError extends Schema.ErrorClass( * description: "Temperature value 5 is out of range" * }) * - * console.log(invalidRequestError.isRetryable) // false - * console.log(invalidRequestError.message) - * // "Invalid request: parameter 'temperature' must be between 0 and 2. Temperature value 5 is out of range" + * const result = [invalidRequestError.parameter, invalidRequestError.isRetryable] // => ["temperature", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class InvalidRequestError extends Schema.ErrorClass( +export class InvalidRequestError extends Schema.Error( "effect/ai/AiError/InvalidRequestError" )({ _tag: Schema.tag("InvalidRequestError"), @@ -632,22 +628,20 @@ export class InvalidRequestError extends Schema.ErrorClass( * * **Example** (Creating an internal provider error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const providerError = new AiError.InternalProviderError({ * description: "Server encountered an unexpected error" * }) * - * console.log(providerError.isRetryable) // true - * console.log(providerError.message) - * // "Internal provider error: Server encountered an unexpected error" + * const result = [providerError.description, providerError.isRetryable] // => ["Server encountered an unexpected error", true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class InternalProviderError extends Schema.ErrorClass( +export class InternalProviderError extends Schema.Error( "effect/ai/AiError/InternalProviderError" )({ _tag: Schema.tag("InternalProviderError"), @@ -685,22 +679,20 @@ export class InternalProviderError extends Schema.ErrorClass ["Expected a string but received a number", true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class InvalidOutputError extends Schema.ErrorClass( +export class InvalidOutputError extends Schema.Error( "effect/ai/AiError/InvalidOutputError" )({ _tag: Schema.tag("InvalidOutputError"), @@ -729,13 +721,15 @@ export class InvalidOutputError extends Schema.ErrorClass( * * **Example** (Creating an invalid output error from a schema error) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schema } from "effect" * import { AiError } from "effect/unstable/ai" * - * declare const schemaError: Schema.SchemaError - * + * const schemaError = await Effect.runPromise( + * Schema.decodeUnknownEffect(Schema.Number)("not a number").pipe(Effect.flip) + * ) * const parseError = AiError.InvalidOutputError.fromSchemaError(schemaError) + * parseError.description // => "Expected number" * ``` * * @since 4.0.0 @@ -761,7 +755,7 @@ export class InvalidOutputError extends Schema.ErrorClass( * * **Example** (Creating a structured output error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const error = new AiError.StructuredOutputError({ @@ -769,15 +763,13 @@ export class InvalidOutputError extends Schema.ErrorClass( * responseText: "{\"foo\":}" * }) * - * console.log(error.isRetryable) // true - * console.log(error.message) - * // "Structured output validation failed: Expected a valid JSON object" + * const result = [error.description, error.responseText, error.isRetryable] // => ["Expected a valid JSON object", '{"foo":}', true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class StructuredOutputError extends Schema.ErrorClass( +export class StructuredOutputError extends Schema.Error( "effect/ai/AiError/StructuredOutputError" )({ _tag: Schema.tag("StructuredOutputError"), @@ -807,14 +799,15 @@ export class StructuredOutputError extends Schema.ErrorClass "{}" * ``` * * @since 4.0.0 @@ -842,22 +835,20 @@ export class StructuredOutputError extends Schema.ErrorClass ["Unions are not supported in Anthropic structured output", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class UnsupportedSchemaError extends Schema.ErrorClass( +export class UnsupportedSchemaError extends Schema.Error( "effect/ai/AiError/UnsupportedSchemaError" )({ _tag: Schema.tag("UnsupportedSchemaError"), @@ -894,22 +885,20 @@ export class UnsupportedSchemaError extends Schema.ErrorClass ["An unexpected error occurred", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class UnknownError extends Schema.ErrorClass( +export class UnknownError extends Schema.Error( "effect/ai/AiError/UnknownError" )({ _tag: Schema.tag("UnknownError"), @@ -952,7 +941,7 @@ export class UnknownError extends Schema.ErrorClass( * * **Example** (Creating a tool not found error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const error = new AiError.ToolNotFoundError({ @@ -960,15 +949,13 @@ export class UnknownError extends Schema.ErrorClass( * availableTools: ["GetWeather", "GetTime"] * }) * - * console.log(error.isRetryable) // true - * console.log(error.message) - * // "Tool 'unknownTool' not found. Available tools: GetWeather, GetTime" + * const result = [error.toolName, error.availableTools, error.isRetryable] // => ["unknownTool", ["GetWeather", "GetTime"], true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ToolNotFoundError extends Schema.ErrorClass( +export class ToolNotFoundError extends Schema.Error( "effect/ai/AiError/ToolNotFoundError" )({ _tag: Schema.tag("ToolNotFoundError"), @@ -1007,7 +994,7 @@ export class ToolNotFoundError extends Schema.ErrorClass( * * **Example** (Creating a tool parameter validation error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const error = new AiError.ToolParameterValidationError({ @@ -1016,15 +1003,13 @@ export class ToolNotFoundError extends Schema.ErrorClass( * description: "Expected string, got number" * }) * - * console.log(error.isRetryable) // true - * console.log(error.message) - * // "Invalid parameters for tool 'GetWeather': Expected string, got number" + * const result = [error.toolName, error.description, error.isRetryable] // => ["GetWeather", "Expected string, got number", true] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ToolParameterValidationError extends Schema.ErrorClass( +export class ToolParameterValidationError extends Schema.Error( "effect/ai/AiError/ToolParameterValidationError" )({ _tag: Schema.tag("ToolParameterValidationError"), @@ -1064,7 +1049,7 @@ export class ToolParameterValidationError extends Schema.ErrorClass ["GetWeather", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class InvalidToolResultError extends Schema.ErrorClass( +export class InvalidToolResultError extends Schema.Error( "effect/ai/AiError/InvalidToolResultError" )({ _tag: Schema.tag("InvalidToolResultError"), @@ -1118,7 +1101,7 @@ export class InvalidToolResultError extends Schema.ErrorClass ["GetWeather", "Cannot encode bigint values as JSON", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ToolResultEncodingError extends Schema.ErrorClass( +export class ToolResultEncodingError extends Schema.Error( "effect/ai/AiError/ToolResultEncodingError" )({ _tag: Schema.tag("ToolResultEncodingError"), @@ -1174,7 +1155,7 @@ export class ToolResultEncodingError extends Schema.ErrorClass ["OpenAiCodeInterpreter", "Invalid container ID format", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ToolConfigurationError extends Schema.ErrorClass( +export class ToolConfigurationError extends Schema.Error( "effect/ai/AiError/ToolConfigurationError" )({ _tag: Schema.tag("ToolConfigurationError"), @@ -1228,22 +1207,20 @@ export class ToolConfigurationError extends Schema.ErrorClass [["GetWeather", "SendEmail"], false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class ToolkitRequiredError extends Schema.ErrorClass( +export class ToolkitRequiredError extends Schema.Error( "effect/ai/AiError/ToolkitRequiredError" )({ _tag: Schema.tag("ToolkitRequiredError"), @@ -1283,22 +1260,20 @@ export class ToolkitRequiredError extends Schema.ErrorClass ["InvalidUserInputError", false] * ``` * - * @category reason + * @category errors * @since 4.0.0 */ -export class InvalidUserInputError extends Schema.ErrorClass( +export class InvalidUserInputError extends Schema.Error( "effect/ai/AiError/InvalidUserInputError" )({ _tag: Schema.tag("InvalidUserInputError"), @@ -1338,7 +1313,7 @@ export class InvalidUserInputError extends Schema.ErrorClass + * const aiOperation = Effect.fail(new AiError.AiError({ + * module: "OpenAI", + * method: "generateText", + * reason: new AiError.RateLimitError({ retryAfter: Duration.seconds(30) }) + * })) * * // Handle specific reason types * const handled = aiOperation.pipe( @@ -1453,12 +1432,14 @@ const TypeId = "~effect/unstable/ai/AiError/AiError" as const * return Effect.fail(error) * }) * ) + * + * await Effect.runPromise(handled) // => "Retry after 30000 millis" * ``` * * @category schemas * @since 4.0.0 */ -export class AiError extends Schema.ErrorClass( +export class AiError extends Schema.Error( "effect/ai/AiError/AiError" )({ _tag: Schema.tag("AiError"), @@ -1505,7 +1486,7 @@ export type AiErrorEncoded = typeof AiError["Encoded"] * * **Example** (Checking for an AI error) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const someError = new Error("generic error") @@ -1515,8 +1496,7 @@ export type AiErrorEncoded = typeof AiError["Encoded"] * reason: new AiError.RateLimitError({}) * }) * - * console.log(AiError.isAiError(someError)) // false - * console.log(AiError.isAiError(aiError)) // true + * const result = [AiError.isAiError(someError), AiError.isAiError(aiError)] // => [false, true] * ``` * * @category guards @@ -1529,14 +1509,13 @@ export const isAiError = (u: unknown): u is AiError => Predicate.hasProperty(u, * * **Example** (Checking for an AI error reason) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const rateLimitError = new AiError.RateLimitError({}) * const genericError = new Error("generic error") * - * console.log(AiError.isAiErrorReason(rateLimitError)) // true - * console.log(AiError.isAiErrorReason(genericError)) // false + * const result = [AiError.isAiErrorReason(rateLimitError), AiError.isAiErrorReason(genericError)] // => [true, false] * ``` * * @category guards @@ -1549,7 +1528,7 @@ export const isAiErrorReason = (u: unknown): u is AiErrorReason => Predicate.has * * **Example** (Creating an AI error) * - * ```ts + * ```ts import.meta.vitest * import { Duration } from "effect" * import { AiError } from "effect/unstable/ai" * @@ -1561,8 +1540,7 @@ export const isAiErrorReason = (u: unknown): u is AiErrorReason => Predicate.has * }) * }) * - * console.log(error.message) - * // "OpenAI.completion: Rate limit exceeded. Retry after 1 minute" + * const result = [error.module, error.method, error.reason._tag] // => ["OpenAI", "completion", "RateLimitError"] * ``` * * @category constructors @@ -1584,7 +1562,7 @@ export const make = (params: { * * **Example** (Mapping an HTTP status to a reason) * - * ```ts + * ```ts import.meta.vitest * import { AiError } from "effect/unstable/ai" * * const reason = AiError.reasonFromHttpStatus({ @@ -1592,7 +1570,7 @@ export const make = (params: { * body: { error: "Rate limit exceeded" } * }) * - * console.log(reason._tag) // "RateLimitError" + * reason._tag // => "RateLimitError" * ``` * * @category constructors diff --git a/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts b/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts index 634f9d3d9..6b9034449 100644 --- a/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts +++ b/.context/effect/packages/effect/src/unstable/ai/AnthropicStructuredOutput.ts @@ -3,24 +3,18 @@ * structured output. * * The main entry point returns the JSON Schema to send to Anthropic and a codec - * for decoding the model response back into the original application type. When - * Anthropic cannot express the original schema shape directly, the conversion - * rewrites supported cases such as tuples, records, optional properties, and - * `oneOf` unions. Schema kinds that cannot be represented throw during - * conversion instead of producing a lossy schema. + * for decoding the model response back into the original application type. + * Unsupported constraints can be omitted from the provider schema and remain + * enforced by the returned codec. * * @since 4.0.0 */ -import * as Arr from "../../Array.ts" import * as JsonSchema from "../../JsonSchema.ts" -import * as Option from "../../Option.ts" -import * as Predicate from "../../Predicate.ts" +import * as Rec from "../../Record.ts" import * as Schema from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import * as SchemaTransformation from "../../SchemaTransformation.ts" +import * as InternalStructuredOutput from "./internal/structured-output.ts" import * as LanguageModel from "./LanguageModel.ts" import * as OpenAiStructuredOutput from "./OpenAiStructuredOutput.ts" -import * as Tool from "./Tool.ts" /** * Converts a `Schema.Codec` to Anthropic structured-output JSON Schema and a @@ -35,22 +29,26 @@ import * as Tool from "./Tool.ts" * **Details** * * Returns the JSON Schema to include in the request and the codec to use when - * decoding the model response. If the input schema already fits Anthropic's - * supported JSON Schema subset, the original codec is returned unchanged. + * decoding the model response. The codec remains authoritative: the provider + * JSON Schema can be a lossy, less restrictive representation when Anthropic + * cannot express an Effect Schema constraint. * * **Gotchas** * * - Some schemas use a provider-safe encoded shape: tuples become objects with - * numeric string keys, records become arrays of `[key, value]` pairs, and - * optional properties become required nullable properties. + * numeric string keys, objects with index signatures become arrays of + * `[key, value]` pairs, and optional properties become required nullable + * properties. * - `oneOf` unions are emitted as `anyOf` unions. - * - Unsupported schema kinds throw during conversion instead of producing a - * lossy schema. + * - Unsupported constraints are removed from the provider schema and are still + * checked while decoding with the returned codec. + * - Recursive schemas throw during conversion because Anthropic structured + * output does not support recursive references. * * @see {@link LanguageModel.CodecTransformer} for the structured-output transformer contract * @see {@link OpenAiStructuredOutput.toCodecOpenAI} for the OpenAI-specific transformer * - * @category Codec Transformation + * @category transforming * @since 4.0.0 */ export function toCodecAnthropic( @@ -59,349 +57,120 @@ export function toCodecAnthropic( readonly codec: Schema.ConstraintCodec readonly jsonSchema: JsonSchema.JsonSchema } { - const to = schema.ast - const from = recur(SchemaAST.toEncoded(to)) - const codec = from === to - ? schema - : Schema.make(SchemaAST.decodeTo(from, to, SchemaTransformation.passthrough())) - const document = JsonSchema.resolveTopLevel$ref(Schema.toJsonSchemaDocument(codec)) - const jsonSchema = { ...document.schema } + const codec = InternalStructuredOutput.toCodec(schema) + const unresolvedDocument = Schema.toJsonSchemaDocument(codec, { generateDescriptions: true }) + if (hasReferenceCycle(unresolvedDocument.schema, unresolvedDocument.definitions)) { + throw new Error("AnthropicStructuredOutput: Recursive schemas are not supported") + } + const document = JsonSchema.resolveTopLevel$ref(unresolvedDocument) + const jsonSchema = rewriteAnthropic(document.schema) if (Object.keys(document.definitions).length > 0) { - jsonSchema.$defs = document.definitions + jsonSchema.$defs = Rec.map(document.definitions, rewriteAnthropic) } return { codec, jsonSchema } } -function recur(ast: SchemaAST.AST): SchemaAST.AST { - switch (ast._tag) { - case "Declaration": - case "Void": - case "Never": - case "Unknown": - case "Any": - case "BigInt": - case "Symbol": - case "UniqueSymbol": - case "ObjectKeyword": - case "Enum": - case "TemplateLiteral": - case "Suspend": - return unsupportedAst( - ast, - "Anthropic structured output does not support this schema kind; consider transforming the schema or using a different provider" - ) - case "Undefined": - return unsupportedAst( - ast, - "Anthropic structured output does not support undefined; consider transforming the schema or using a different provider; if using `Schema.optional`, consider using `Schema.optionalKey` instead" - ) - case "Null": - return ast - case "String": { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.String(annotations, filters) - } - return ast - } - case "Number": { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.Number(annotations, filters) - } - return ast - } - case "Boolean": - return ast - case "Literal": { - const literal = ast.literal - if (typeof literal === "string" || typeof literal === "number" || typeof literal === "boolean") { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.Literal(ast.literal, annotations, filters) - } - return ast - } - throw new Error( - `${errorPrefix}: Unsupported literal type ${typeof literal} (value: ${ - String(literal) - }) (supported: string | number | boolean)` - ) - } - case "Union": { - if (ast.mode === "oneOf") { - return new SchemaAST.Union(ast.types, "anyOf", ast.annotations, ast.checks) - } - const types = SchemaAST.mapOrSame(ast.types, recur) - const { annotations, filters } = get(ast) - if (types !== ast.types || annotations !== undefined || filters !== undefined) { - return new SchemaAST.Union(types, "anyOf", annotations, filters) - } - return ast - } - case "Arrays": { - if (ast.rest.length > 1) { - throw new Error( - `${errorPrefix}: Post-rest elements are not supported for arrays (rest length: ${ast.rest.length})` - ) - } - let { annotations, filters } = get(ast) - if (ast.elements.length > 0) { - // tuples are not supported by Anthropic, we translate them to objects with string keys - if (annotations !== undefined && typeof annotations.description === "string") { - annotations.description = `${TUPLE_DESCRIPTION}; ${annotations.description}` - } else { - annotations ??= {} - annotations.description = TUPLE_DESCRIPTION - } - const propertySignatures = ast.elements.map((e, i) => { - return new SchemaAST.PropertySignature(String(i), e) - }) - if (ast.rest.length === 1) { - propertySignatures.push( - new SchemaAST.PropertySignature(REST_PROPERTY_NAME, new SchemaAST.Arrays(false, [], ast.rest)) - ) - } - return SchemaAST.decodeTo( - recur(new SchemaAST.Objects(propertySignatures, [], annotations, filters)), - ast, - SchemaTransformation.transform({ - decode: (o) => { - let t: Array = [] - for (let i = 0; i < ast.elements.length; i++) { - const k = String(i) - if (o[k] !== undefined) { - t.push(o[k]) - } - } - if (REST_PROPERTY_NAME in o) { - t = [...t, ...o[REST_PROPERTY_NAME]] - } - return t - }, - encode: (t) => { - const o: Record = {} - for (let i = 0; i < ast.elements.length; i++) { - if (t.length >= i) { - o[String(i)] = t[i] - } - } - if (ast.rest.length === 1) { - o[REST_PROPERTY_NAME] = t.length >= ast.elements.length ? t.slice(ast.elements.length) : [] - } - return o - } - }) - ) - } else { - const rest = SchemaAST.mapOrSame(ast.rest, recur) - if (rest !== ast.rest || annotations !== undefined || filters !== undefined) { - return new SchemaAST.Arrays(false, [], rest, annotations, filters) - } - return ast - } - } - case "Objects": { - let { annotations, filters } = get(ast) - if (ast.indexSignatures.length === 0) { - const propertySignatures = SchemaAST.mapOrSame(ast.propertySignatures, (ps) => { - if (typeof ps.name !== "string") { - throw new Error( - `${errorPrefix}: Property names must be strings (got ${typeof ps.name})` - ) - } - let type = recur(ps.type) - // opttional properties are not supported by Anthropic, so we translate them to nullable unions - if (SchemaAST.isOptional(ps.type)) { - type = SchemaAST.decodeTo( - new SchemaAST.Union([type, SchemaAST.null], "anyOf"), - SchemaAST.optionalKey(type), - SchemaTransformation.transformOptional({ - decode: Option.filter(Predicate.isNotNull), - encode: Option.orElseSome(() => null) - }) - ) - } - if (type === ps.type) { - return ps - } - return new SchemaAST.PropertySignature(ps.name, type) - }) - if ( - propertySignatures !== ast.propertySignatures || annotations !== undefined || filters !== undefined - ) { - return new SchemaAST.Objects(propertySignatures, [], annotations, filters) - } - } else if (ast.indexSignatures.length === 1 && ast.propertySignatures.length === 0) { - const is = ast.indexSignatures[0] - if (Tool.isEmptyParamsRecord(is)) { - return ast - } - // records are not supported by Anthropic, so we translate them to arrays of key-value pairs - if (annotations !== undefined && typeof annotations.description === "string") { - annotations.description = `${RECORD_DESCRIPTION}; ${annotations.description}` - } else { - annotations ??= {} - annotations.description = RECORD_DESCRIPTION - } - return SchemaAST.decodeTo( - recur( - new SchemaAST.Arrays(false, [], [new SchemaAST.Arrays(false, [is.parameter, is.type], [])], annotations) - ), - ast, - SchemaTransformation.transform({ - decode: Object.fromEntries, - encode: Object.entries - }) - ) - } else { - throw new Error( - `${errorPrefix}: unsupported object schema shape (properties: ${ast.propertySignatures.length}, indexSignatures: ${ast.indexSignatures.length}). Supported: plain objects (properties only) or records (single index signature, no properties)` - ) +function hasReferenceCycle( + root: JsonSchema.JsonSchema, + definitions: JsonSchema.Definitions +): boolean { + const visiting = new Set() + const visited = new Set() + + function visit(schema: JsonSchema.JsonSchema): boolean { + if (visiting.has(schema)) return true + if (visited.has(schema)) return false + + visiting.add(schema) + let cycle = false + InternalStructuredOutput.walkJsonSchema(schema, (node) => { + if (!cycle && typeof node.$ref === "string") { + const target = node.$ref === "#" ? root : JsonSchema.resolve$ref(node.$ref, definitions) + if (target !== undefined && visit(target)) cycle = true } - return ast - } + return node + }) + visiting.delete(schema) + visited.add(schema) + return cycle } -} - -const errorPrefix = "AnthropicStructuredOutput" -function unsupportedAst(ast: SchemaAST.AST, details?: string): never { - const base = `Unsupported AST ${ast._tag}` - const full = `${errorPrefix}: ${base}` - throw new Error(details !== undefined ? `${full} (${details})` : full) + return visit(root) } -const REST_PROPERTY_NAME = "__rest__" - -const RECORD_DESCRIPTION = - "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object" - -const TUPLE_DESCRIPTION = - "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements" - -type Annotation = - | { readonly _tag: "description"; readonly description: string } - | { readonly _tag: "format"; readonly format: string } - -type Filter = - | Annotation - | { readonly _tag: "filter"; readonly filter: SchemaAST.Filter } - -const get = (ast: SchemaAST.AST): { - annotations: Record | undefined - filters: [SchemaAST.Check, ...SchemaAST.Check[]] | undefined -} => { - const annotations: Record = {} - const filters: Array> = [] - const checks = getChecks(ast) - if (checks.length > 0) { - for (const check of checks) { - switch (check._tag) { - case "description": { - if (annotations.description !== undefined) { - annotations.description += ` and ${check.description}` - } else { - annotations.description = check.description - } - break - } - case "format": { - annotations.format = check.format - break - } - case "filter": { - filters.push(check.filter) - break - } +function rewriteAnthropic(schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { + return InternalStructuredOutput.walkJsonSchema(schema, (schema) => { + const normalized = hoistAllOfDescriptions(schema) + const out: JsonSchema.JsonSchema = {} + let unsupportedFormat: string | undefined + for (const [key, value] of Object.entries(normalized)) { + if (key === "format") { + if ( + typeof value === "string" && formats.has(value) && + (normalized.type === undefined || normalized.type === "string") + ) { + out.format = value + } else if (typeof value === "string") unsupportedFormat = value + } else if (supportedKeywords.has(key)) { + if (key !== "additionalProperties" || value === false) out[key] = value } } - } - return { - annotations: Object.keys(annotations).length > 0 ? annotations : undefined, - filters: Arr.isArrayNonEmpty(filters) ? filters : undefined - } + if (unsupportedFormat !== undefined) { + InternalStructuredOutput.appendDescription(out, `a value with a format of ${unsupportedFormat}`) + } + return out + }) } -const getChecks = (ast: SchemaAST.AST): Array => [ - ...(ast.checks !== undefined ? getFilters(ast.checks) : []), - ...getAnnotations(ast.annotations) -] +function hoistAllOfDescriptions(schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { + if (!Array.isArray(schema.allOf)) return schema -const getAnnotations = (annotations: Schema.Annotations.Filter | undefined): Array => { - const out: Array = [] - if (annotations !== undefined) { - const description = annotations?.description - ?? (annotations.meta?._tag === "isInt" || annotations.meta?._tag === "isFinite" - ? undefined - : annotations?.expected) - if (typeof description === "string") { - out.push({ _tag: "description", description }) - } - const format = annotations?.format - if (typeof format === "string") { - if (formats.includes(format)) { - out.push({ _tag: "format", format }) - } else { - out.push({ _tag: "description", description: `a value with a format of ${format}` }) - } - } + const out: JsonSchema.JsonSchema = {} + for (const [key, value] of Object.entries(schema)) { + if (key !== "allOf") out[key] = value } - return out -} - -function getFilter(filter: SchemaAST.Filter): Array { - let out: Array = [] - const annotations = getAnnotations(filter.annotations) - const meta = filter.annotations?.meta - if (meta !== undefined) { - switch (meta._tag) { - case "isInt": - case "isFinite": { - out = out.concat(annotations) - out.push({ _tag: "filter", filter: resetFilter(filter) }) - break - } - default: { - out = out.concat(annotations) - break - } - } - if ("regExp" in meta && meta.regExp instanceof RegExp) { - out.push({ _tag: "filter", filter: resetFilter(filter) }) + const members: Array = [] + for (const member of schema.allOf) { + if (!InternalStructuredOutput.isJsonSchema(member)) continue + const { description, format, title, ...memberRest } = member + const rest = { ...memberRest } + if (schema.type !== undefined && schema.type !== "string") delete rest.pattern + if (typeof description === "string") { + InternalStructuredOutput.appendDescription(out, description) } + if (out.format === undefined && typeof format === "string") out.format = format + if (out.title === undefined && typeof title === "string") out.title = title + if (Object.keys(rest).length > 0) members.push(rest) } + if (members.length > 0) out.allOf = members return out } -function resetFilter(filter: SchemaAST.Filter): SchemaAST.Filter { - return filter.annotate({ - description: undefined, - expected: undefined, - title: undefined, - format: undefined - }) -} - -function getFilters(checks: readonly [SchemaAST.Check, ...SchemaAST.Check[]]): Array { - return checks.flatMap((check) => { - switch (check._tag) { - case "Filter": - return getFilter(check) - case "FilterGroup": - return getFilters(check.checks) - } - }) -} - -const formats = [ +const supportedKeywords = new Set([ + "$ref", + "type", + "title", + "description", + "enum", + "const", + "anyOf", + "allOf", + "properties", + "required", + "additionalProperties", + "items", + "pattern" +]) + +const formats = new Set([ "date-time", "time", "date", "duration", "email", "hostname", + "uri", "ipv4", "ipv6", "uuid" -] +]) diff --git a/.context/effect/packages/effect/src/unstable/ai/Chat.ts b/.context/effect/packages/effect/src/unstable/ai/Chat.ts index cd43d462a..4ac38dd5c 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Chat.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Chat.ts @@ -48,17 +48,35 @@ import type * as Tool from "./Tool.ts" * * **Example** (Accessing the Chat service) * - * ```ts - * import { Effect } from "effect" - * import { Chat } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" + * import { Chat, LanguageModel } from "effect/unstable/ai" + * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => + * Effect.succeed([{ + * type: "text", + * text: "Quantum computers use quantum states to process information." + * }]), + * streamText: () => Stream.empty + * }) + * ) + * + * const ChatLayer = Layer.effect(Chat.Chat, Chat.empty) * * const program = Effect.gen(function*() { - * const chat = yield* Chat.empty + * const chat = yield* Chat.Chat * const response = yield* chat.generateText({ * prompt: "Explain quantum computing in simple terms" * }) - * return response.content + * return response.text * }) + * + * await Effect.runPromise( + * program.pipe(Effect.provide(Layer.merge(ChatLayer, FakeLanguageModel))) + * ) // => "Quantum computers use quantum states to process information." * ``` * * @category services @@ -94,16 +112,17 @@ export interface Service { * * **Example** (Inspecting chat history) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * import { Chat } from "effect/unstable/ai" * * const inspectHistory = Effect.gen(function*() { - * const chat = yield* Chat.empty + * const chat = yield* Chat.fromPrompt("Hello") * const currentHistory = yield* Ref.get(chat.history) - * console.log("Current conversation:", currentHistory) - * return currentHistory + * return currentHistory.content.length * }) + * + * await Effect.runPromise(inspectHistory) // => 1 * ``` */ readonly history: Ref.Ref @@ -118,19 +137,17 @@ export interface Service { * * **Example** (Exporting chat history) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Chat } from "effect/unstable/ai" * * const saveChat = Effect.gen(function*() { - * const chat = yield* Chat.empty - * yield* chat.generateText({ prompt: "Hello!" }) - * + * const chat = yield* Chat.fromPrompt("Hello!") * const exportedData = yield* chat.export - * - * // Save to database or file system - * return exportedData + * return typeof exportedData * }) + * + * await Effect.runPromise(saveChat) // => "object" * ``` */ readonly export: Effect.Effect @@ -145,21 +162,17 @@ export interface Service { * * **Example** (Exporting chat history as JSON) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Chat } from "effect/unstable/ai" * * const backupChat = Effect.gen(function*() { - * const chat = yield* Chat.empty - * - * yield* chat.generateText({ prompt: "Explain photosynthesis" }) - * + * const chat = yield* Chat.fromPrompt("Explain photosynthesis") * const jsonBackup = yield* chat.exportJson - * - * yield* Effect.sync(() => localStorage.setItem("chat-backup", jsonBackup)) - * - * return jsonBackup + * return JSON.parse(jsonBackup).content.length * }) + * + * await Effect.runPromise(backupChat) // => 1 * ``` */ readonly exportJson: Effect.Effect @@ -175,9 +188,23 @@ export interface Service { * * **Example** (Generating chat responses) * - * ```ts - * import { Effect } from "effect" - * import { Chat } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" + * import { Chat, LanguageModel } from "effect/unstable/ai" + * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: (options) => + * Effect.succeed([{ + * type: "text", + * text: options.prompt.content.length === 1 + * ? "The capital of France is Paris." + * : "Paris has about 2.1 million residents." + * }]), + * streamText: () => Stream.empty + * }) + * ) * * const chatWithAI = Effect.gen(function*() { * const chat = yield* Chat.empty @@ -185,13 +212,13 @@ export interface Service { * 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] + * return [response1.text, response2.text] * }) + * + * await Effect.runPromise(chatWithAI.pipe(Effect.provide(FakeLanguageModel))) // => ["The capital of France is Paris.", "Paris has about 2.1 million residents."] * ``` */ readonly generateText: { @@ -242,22 +269,35 @@ export interface Service { * * **Example** (Streaming chat responses) * - * ```ts - * import { Effect, Stream } from "effect" - * import { Chat } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" + * import { Chat, LanguageModel } from "effect/unstable/ai" + * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => Effect.succeed([]), + * streamText: () => + * Stream.make( + * { type: "text-delta", id: "story", delta: "A small probe reached orbit." }, + * { type: "text-delta", id: "story", delta: " It sent back a picture of Earth." } + * ) + * }) + * ) * * const streamingChat = Effect.gen(function*() { * const chat = yield* Chat.empty - * - * const stream = yield* chat.streamText({ + * const story = 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) + * }).pipe( + * Stream.runFold(() => "", (text, part) => + * part.type === "text-delta" ? text + part.delta : text) + * ) + * return story * }) + * + * const story = await Effect.runPromise(streamingChat.pipe(Effect.provide(FakeLanguageModel))) + * story // => "A small probe reached orbit. It sent back a picture of Earth." * ``` */ readonly streamText: { @@ -309,9 +349,9 @@ export interface Service { * * **Example** (Generating structured objects) * - * ```ts - * import { Effect, Schema } from "effect" - * import { Chat } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Effect, Layer, Schema, Stream } from "effect" + * import { Chat, LanguageModel } from "effect/unstable/ai" * * const ContactSchema = Schema.Struct({ * name: Schema.String, @@ -319,19 +359,28 @@ export interface Service { * phone: Schema.optional(Schema.String) * }) * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => + * Effect.succeed([{ + * type: "text", + * text: '{"name":"John Doe","email":"john@example.com","phone":"555-1234"}' + * }]), + * streamText: () => Stream.empty + * }) + * ) + * * 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 + * return [contact.value.name, contact.value.email, contact.value.phone] * }) + * + * await Effect.runPromise(extractContact.pipe(Effect.provide(FakeLanguageModel))) // => ["John Doe", "john@example.com", "555-1234"] * ``` */ readonly generateObject: < @@ -464,21 +513,17 @@ const makeUnsafe = (history: Ref.Ref) => { * * **Example** (Creating an empty chat) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Chat } from "effect/unstable/ai" * * 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 + * const history = yield* chat.export + * return (history as { content: ReadonlyArray }).content.length * }) + * + * await Effect.runPromise(freshChat) // => 0 * ``` * * @category constructors @@ -496,7 +541,7 @@ export const empty: Effect.Effect = Effect.sync(() => makeUnsafe(Ref.ma * * **Example** (Creating a chat from a system prompt) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Chat } from "effect/unstable/ai" * @@ -506,17 +551,16 @@ export const empty: Effect.Effect = Effect.sync(() => makeUnsafe(Ref.ma * content: "You are a helpful assistant specialized in mathematics." * }]) * - * const response = yield* chat.generateText({ - * prompt: "What is 2+2?" - * }) - * - * return response.content + * const history = yield* chat.export + * return (history as { content: ReadonlyArray }).content.length * }) + * + * await Effect.runPromise(chatWithSystemPrompt) // => 1 * ``` * * **Example** (Restoring chat history from a prompt) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Chat } from "effect/unstable/ai" * @@ -537,12 +581,11 @@ export const empty: Effect.Effect = Effect.sync(() => makeUnsafe(Ref.ma * } * ]) * - * const response = yield* chat.generateText({ - * prompt: "I need help with TypeScript" - * }) - * - * return response + * const history = yield* chat.export + * return (history as { content: ReadonlyArray }).content.length * }) + * + * await Effect.runPromise(existingChat) // => 3 * ``` * * @category constructors @@ -562,7 +605,7 @@ export const fromPrompt = (prompt: Prompt.RawInput) => * * **Example** (Restoring chat data) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Ref } from "effect" * import { Chat } from "effect/unstable/ai" * @@ -582,18 +625,20 @@ export const fromPrompt = (prompt: Prompt.RawInput) => * const restoredChat = yield* Chat.fromExport(exported) * const restoredHistory = yield* Ref.get(restoredChat.history) * - * console.log(restoredHistory.content.map((message) => message.role)) - * // ["user", "assistant"] - * * const restoredResponse = restoredHistory.content[1] * if (restoredResponse?.role === "assistant") { * const restoredText = restoredResponse.content[0] * if (restoredText?.type === "text") { - * console.log(restoredText.text) - * // "The project uses Effect." + * return { + * roles: restoredHistory.content.map((message) => message.role), + * text: restoredText.text + * } * } * } + * return undefined * }) + * + * await Effect.runPromise(restoreChat) // => { roles: ["user", "assistant"], text: "The project uses Effect." } * ``` * * @category constructors @@ -615,29 +660,19 @@ export const fromExport = (data: unknown): Effect.Effect< * * **Example** (Restoring chat history from JSON) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Ref } from "effect" * import { Chat } from "effect/unstable/ai" * * 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 original = yield* Chat.fromPrompt("Hello") + * const jsonData = yield* original.exportJson * const restoredChat = yield* Chat.fromJson(jsonData) + * const history = yield* Ref.get(restoredChat.history) + * return history.content.length + * }) * - * // Chat history is now restored - * const response = yield* restoredChat.generateText({ - * prompt: "What were we talking about?" - * }) - * - * return response - * }).pipe( - * Effect.catchTag("SchemaError", (error) => { - * console.log("Invalid JSON format:", error.message) - * return Chat.empty // Fallback to empty chat - * }) - * ) + * await Effect.runPromise(restoreFromJson) // => 1 * ``` * * @category constructors @@ -664,7 +699,7 @@ export const fromJson = (data: string): Effect.Effect< * @category errors * @since 4.0.0 */ -export class ChatNotFoundError extends Schema.ErrorClass( +export class ChatNotFoundError extends Schema.Error( "effect/ai/Chat/ChatNotFoundError" )({ _tag: Schema.tag("ChatNotFoundError"), @@ -923,7 +958,7 @@ export const makePersisted = Effect.fnUntraced(function*(options: { * * @see {@link makePersisted} for the effect constructor when building the service directly instead of providing it as a layer * - * @category constructors + * @category layers * @since 4.0.0 */ export const layerPersisted = (options: { diff --git a/.context/effect/packages/effect/src/unstable/ai/EmbeddingModel.ts b/.context/effect/packages/effect/src/unstable/ai/EmbeddingModel.ts index c08bdd4c0..acf6fa7f8 100644 --- a/.context/effect/packages/effect/src/unstable/ai/EmbeddingModel.ts +++ b/.context/effect/packages/effect/src/unstable/ai/EmbeddingModel.ts @@ -69,7 +69,7 @@ export class Dimensions extends Context.Service()( export class EmbeddingUsage extends Schema.Class( "effect/ai/EmbeddingModel/EmbeddingUsage" )({ - inputTokens: Schema.UndefinedOr(Schema.Finite) + inputTokens: Schema.optional(Schema.Finite) }) {} /** diff --git a/.context/effect/packages/effect/src/unstable/ai/IdGenerator.ts b/.context/effect/packages/effect/src/unstable/ai/IdGenerator.ts index ba2217cdb..60bb102a9 100644 --- a/.context/effect/packages/effect/src/unstable/ai/IdGenerator.ts +++ b/.context/effect/packages/effect/src/unstable/ai/IdGenerator.ts @@ -32,7 +32,7 @@ import * as Random from "../../Random.ts" * * **Example** (Accessing the ID generator service) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { IdGenerator } from "effect/unstable/ai" * @@ -41,6 +41,13 @@ import * as Random from "../../Random.ts" * const newId = yield* idGenerator.generateId() * return newId * }) + * + * const program = useIdGenerator.pipe( + * Effect.provideService(IdGenerator.IdGenerator, { + * generateId: () => Effect.succeed("id-1") + * }) + * ) + * await Effect.runPromise(program) // => "id-1" * ``` * * @category services @@ -61,7 +68,7 @@ export class IdGenerator extends Context.Service()( * * **Example** (Implementing a custom ID generator) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import type { IdGenerator } from "effect/unstable/ai" * @@ -71,11 +78,9 @@ export class IdGenerator extends Context.Service()( * generateId: () => Effect.sync(() => `custom_${++nextId}`) * } * - * const program = Effect.gen(function*() { - * const id = yield* customService.generateId() - * console.log(id) // "custom_1" - * return id - * }) + * const program = customService.generateId() + * + * await Effect.runPromise(program) // => "custom_1" * ``` * * @category models @@ -90,7 +95,7 @@ export interface Service { * * **Example** (Configuring generated IDs) * - * ```ts + * ```ts import.meta.vitest * import type { IdGenerator } from "effect/unstable/ai" * * // Configuration for tool call IDs @@ -102,6 +107,7 @@ export interface Service { * } * * // This will generate IDs like: "tool_A1B2C3D4" + * const result = [toolCallOptions.prefix, toolCallOptions.size] // => ["tool", 8] * ``` * * @category options @@ -162,13 +168,12 @@ const makeGenerator = ({ * * **Example** (Generating default IDs) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { IdGenerator } from "effect/unstable/ai" * * const program = Effect.gen(function*() { * const id = yield* IdGenerator.defaultIdGenerator.generateId() - * console.log(id) // "id_A7xK9mP2qR5tY8uV" * return id * }) * @@ -179,6 +184,9 @@ const makeGenerator = ({ * IdGenerator.defaultIdGenerator * ) * ) + * + * const id = await Effect.runPromise(withDefault) + * const result = [id.startsWith("id_"), id.length] // => [true, 19] * ``` * * @category constructors @@ -198,7 +206,7 @@ export const defaultIdGenerator: Service = { * * **Example** (Creating a custom generator) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { IdGenerator } from "effect/unstable/ai" * @@ -211,15 +219,16 @@ export const defaultIdGenerator: Service = { * size: 10 * }) * - * const messageId = yield* messageIdGen.generateId() - * console.log(messageId) // "msg-A7X9K2M5P8" - * return messageId + * return yield* messageIdGen.generateId() * }) + * + * const messageId = await Effect.runPromise(program) + * const result = [messageId.startsWith("msg-"), messageId.length] // => [true, 14] * ``` * * **Example** (Handling invalid generator options) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { IdGenerator } from "effect/unstable/ai" * @@ -231,14 +240,8 @@ export const defaultIdGenerator: Service = { * size: 8 * }) * - * const program = Effect.gen(function*() { - * const generator = yield* invalidConfig - * return generator - * }).pipe( - * Effect.catch((error) => - * Effect.succeed(`Configuration error: ${error.message}`) - * ) - * ) + * const error = await Effect.runPromise(Effect.flip(invalidConfig)) + * error.message // => 'The separator "A" must not be part of the alphabet "ABC123".' * ``` * * @category constructors @@ -273,7 +276,7 @@ export const make = Effect.fnUntraced(function*({ * * **Example** (Providing an ID generator layer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { IdGenerator } from "effect/unstable/ai" * @@ -287,13 +290,14 @@ export const make = Effect.fnUntraced(function*({ * * const program = Effect.gen(function*() { * const idGen = yield* IdGenerator.IdGenerator - * const toolCallId = yield* idGen.generateId() - * console.log(toolCallId) // "tool_call_A7XK9MP2QR5T" - * return toolCallId + * return yield* idGen.generateId() * }).pipe(Effect.provide(toolCallIdLayer)) + * + * const toolCallId = await Effect.runPromise(program) + * const result = [toolCallId.startsWith("tool_call_"), toolCallId.length] // => [true, 22] * ``` * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = (options: MakeOptions): Layer.Layer => diff --git a/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts b/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts index 8e6a0b2ef..0f5f544b0 100644 --- a/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts +++ b/.context/effect/packages/effect/src/unstable/ai/LanguageModel.ts @@ -20,9 +20,9 @@ import type * as JsonSchema from "../../JsonSchema.ts" import * as Option from "../../Option.ts" import * as Predicate from "../../Predicate.ts" import * as Queue from "../../Queue.ts" -import { CurrentConcurrency } from "../../References.ts" import * as Schema from "../../Schema.ts" import * as SchemaAST from "../../SchemaAST.ts" +import * as Semaphore from "../../Semaphore.ts" import * as Sink from "../../Sink.ts" import * as Stream from "../../Stream.ts" import type { Span } from "../../Tracer.ts" @@ -52,10 +52,22 @@ import * as Toolkit from "./Toolkit.ts" * * **Example** (Accessing the language model service) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" * import { LanguageModel } from "effect/unstable/ai" * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => + * Effect.succeed([{ + * type: "text", + * text: "Machine learning finds patterns in data." + * }]), + * streamText: () => Stream.empty + * }) + * ) + * * const program = Effect.gen(function*() { * const model = yield* LanguageModel.LanguageModel * const response = yield* model.generateText({ @@ -63,6 +75,8 @@ import * as Toolkit from "./Toolkit.ts" * }) * return response.text * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(FakeLanguageModel))) // => "Machine learning finds patterns in data." * ``` * * @category services @@ -186,9 +200,12 @@ export interface Service { * * Different language model providers have varying constraints on the JSON * schemas they accept. A `CodecTransformer` rewrites a codec's encoded side to - * satisfy those constraints while preserving the decoded type. + * satisfy those constraints while preserving the decoded type. A provider + * schema may be less restrictive than the codec when the provider cannot + * express every constraint; the returned codec remains authoritative for + * validating model output. * - * @category models + * @category utility types * @since 4.0.0 */ export type CodecTransformer = (schema: Schema.ConstraintCodec) => { @@ -334,21 +351,14 @@ export type ToolChoice = * * **Example** (Inspecting a text response) * - * ```ts - * import { Effect } from "effect" - * import { LanguageModel } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { LanguageModel, Response } from "effect/unstable/ai" * - * const program = Effect.gen(function*() { - * const response = yield* LanguageModel.generateText({ - * prompt: "Explain photosynthesis" - * }) + * const response = new LanguageModel.GenerateTextResponse([ + * Response.makePart("text", { text: "Plants convert light into energy." }) + * ]) * - * console.log(response.text) // Generated text content - * console.log(response.finishReason) // "stop", "length", etc. - * console.log(response.usage) // Usage information - * - * return response - * }) + * const result = [response.text, response.finishReason] // => ["Plants convert light into energy.", "unknown"] * ``` * * @category models @@ -445,26 +455,16 @@ export class GenerateTextResponse> { * * **Example** (Inspecting an object response) * - * ```ts - * import { Effect, Schema } from "effect" - * import { LanguageModel } from "effect/unstable/ai" - * - * 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 - * }) + * ```ts import.meta.vitest + * import { LanguageModel, Response } from "effect/unstable/ai" * - * console.log(response.value) // { name: "John Doe", email: "john@example.com" } - * console.log(response.text) // Raw generated text + * const response = new LanguageModel.GenerateObjectResponse( + * { name: "John Doe", email: "john@example.com" }, + * [Response.makePart("text", { text: '{"name":"John Doe","email":"john@example.com"}' })] + * ) * - * return response.value - * }) + * response.value // => { name: "John Doe", email: "john@example.com" } + * response.text // => '{"name":"John Doe","email":"john@example.com"}' * ``` * * @category models @@ -1004,6 +1004,8 @@ export const make: (params: { ) { const tracker = Option.getOrUndefined(yield* Effect.serviceOption(ResponseIdTracker.ResponseIdTracker)) const toolChoice = options.toolChoice ?? "auto" + const concurrency = options.concurrency ?? "unbounded" + providerOptions.span.attribute("concurrency", concurrency) const generateWithNonIncrementalFallback = () => { const requestOptions: ProviderOptions = { @@ -1121,7 +1123,7 @@ export const make: (params: { const approvedResults = yield* executeApprovedToolCalls( approved, toolkit, - options.concurrency + concurrency ) const deniedResults = createDenialResults(denied) const preResolvedResults = [...approvedResults, ...deniedResults] @@ -1189,7 +1191,7 @@ export const make: (params: { rawContent, toolkit, providerOptions.prompt.content, - options.concurrency + concurrency ).pipe( Stream.filter( (result) => @@ -1239,6 +1241,8 @@ export const make: (params: { ) { const tracker = Option.getOrUndefined(yield* Effect.serviceOption(ResponseIdTracker.ResponseIdTracker)) const toolChoice = options.toolChoice ?? "auto" + const concurrency = options.concurrency ?? "unbounded" + providerOptions.span.attribute("concurrency", concurrency) const streamWithNonIncrementalFallback = () => { const requestOptions: ProviderOptions = { @@ -1379,7 +1383,7 @@ export const make: (params: { const approvedResults = yield* executeApprovedToolCalls( pendingApproved, toolkit, - options.concurrency + concurrency ) const deniedResults = createDenialResults(pendingDenied) const preResolvedResults = [...approvedResults, ...deniedResults] @@ -1486,6 +1490,9 @@ export const make: (params: { // FiberSet to track concurrent tool call handlers const toolCallFibers = yield* FiberSet.make() + const toolCallSemaphore = concurrency === "unbounded" + ? undefined + : yield* Semaphore.make(concurrency) // Helper function to handle tool calls with approval logic const handleToolCall = Effect.fnUntraced(function*(part: Response.ToolCallPartEncoded) { @@ -1509,7 +1516,7 @@ export const make: (params: { return } - yield* toolkit.handle(part.name, part.params as any).pipe( + yield* toolkit.handle(part.name, part.params as any, part.id).pipe( Stream.unwrap, Stream.runForEach((result) => { const toolResultPart = Response.makePart("tool-result", { @@ -1550,7 +1557,11 @@ export const make: (params: { // Fork tool call handlers - use the raw chunk for encoded params for (const part of chunk) { if (part.type === "tool-call" && part.providerExecuted !== true) { - yield* FiberSet.run(toolCallFibers, handleToolCall(part)) + const effect = handleToolCall(part) + yield* FiberSet.run( + toolCallFibers, + toolCallSemaphore ? toolCallSemaphore.withPermit(effect) : effect + ) } } }) @@ -1592,21 +1603,43 @@ export const make: (params: { * * **Example** (Generating text with options) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" * import { LanguageModel } from "effect/unstable/ai" * - * const program = Effect.gen(function*() { - * const response = yield* LanguageModel.generateText({ - * prompt: "Write a haiku about programming", - * toolChoice: "none" + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: (options) => + * Effect.succeed([ + * { + * type: "text", + * text: options.toolChoice === "none" + * ? "Code flows through types / Errors become values / Programs stay composed" + * : "Unexpected tool choice" + * }, + * { + * type: "finish", + * reason: "stop", + * usage: { + * inputTokens: { total: 6 }, + * outputTokens: { total: 12 } + * } + * } + * ]), + * streamText: () => Stream.empty * }) + * ) * - * console.log(response.text) - * console.log(response.usage.inputTokens.total) + * const program = LanguageModel.generateText({ + * prompt: "Write a haiku about programming", + * toolChoice: "none" + * }).pipe( + * Effect.map((response) => [response.text, response.usage.inputTokens.total]), + * Effect.provide(FakeLanguageModel) + * ) * - * return response - * }) + * await Effect.runPromise(program) // => ["Code flows through types / Errors become values / Programs stay composed", 6] * ``` * * @category text generation @@ -1661,8 +1694,8 @@ export const generateText: { * * **Example** (Generating an object) * - * ```ts - * import { Effect, Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer, Schema, Stream } from "effect" * import { LanguageModel } from "effect/unstable/ai" * * const EventSchema = Schema.Struct({ @@ -1671,22 +1704,31 @@ export const generateText: { * 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" + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => + * Effect.succeed([{ + * type: "text", + * text: '{"title":"Tech Conference","date":"March 15th","location":"San Francisco"}' + * }]), + * streamText: () => Stream.empty * }) - * - * console.log(response.value) - * // { title: "Tech Conference", date: "March 15th", location: "San Francisco" } - * - * return response.value - * }) + * ) + * + * const program = LanguageModel.generateObject({ + * prompt: "Extract event info: Tech Conference on March 15th in San Francisco", + * schema: EventSchema, + * objectName: "event" + * }).pipe( + * Effect.map((response) => response.value), + * Effect.provide(FakeLanguageModel) + * ) + * + * await Effect.runPromise(program) // => { title: "Tech Conference", date: "March 15th", location: "San Francisco" } * ``` * - * @category object generation + * @category generators * @since 4.0.0 */ export const generateObject = < @@ -1718,18 +1760,30 @@ export const generateObject = < * * **Example** (Streaming text deltas) * - * ```ts - * import { Console, Effect, Stream } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer, Stream } from "effect" * import { LanguageModel } from "effect/unstable/ai" * + * const FakeLanguageModel = Layer.effect( + * LanguageModel.LanguageModel, + * LanguageModel.make({ + * generateText: () => Effect.succeed([]), + * streamText: () => + * Stream.make( + * { type: "text-delta", id: "story", delta: "The explorer reached orbit." }, + * { type: "text-delta", id: "story", delta: " Earth glowed below." } + * ) + * }) + * ) + * * 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 - * })) + * }).pipe( + * Stream.runFold(() => "", (text, part) => part.type === "text-delta" ? text + part.delta : text), + * Effect.provide(FakeLanguageModel) + * ) + * + * await Effect.runPromise(program) // => "The explorer reached orbit. Earth glowed below." * ``` * * @category text generation @@ -1954,7 +2008,7 @@ const isApprovalNeeded = Effect.fnUntraced(function*( const executeApprovedToolCalls = >( approvals: ReadonlyArray, toolkit: Toolkit.WithHandler, - concurrency: Concurrency | undefined + concurrency: Concurrency ): Effect.Effect< Array, Tool.HandlerError | AiError.AiError, @@ -1982,7 +2036,8 @@ const executeApprovedToolCalls = >( const resultStream = yield* toolkit.handle( toolCall.name, - toolCall.params as any + toolCall.params as any, + approval.toolCallId ) const terminalResult = yield* resultStream.pipe( @@ -2000,18 +2055,13 @@ const executeApprovedToolCalls = >( id: approval.toolCallId, name: toolCall.name, isFailure: terminalResult.isFailure, - result: terminalResult.encodedResult + result: terminalResult.encodedResult, + providerExecuted: false }) }) - return Effect.gen(function*() { - const resolveConcurrency = concurrency === "inherit" - ? yield* Effect.service(CurrentConcurrency) - : (concurrency ?? "unbounded") - - return yield* Effect.forEach(approvals, executeTool, { - concurrency: resolveConcurrency - }) + return Effect.forEach(approvals, executeTool, { + concurrency }) } @@ -2026,7 +2076,8 @@ const createDenialResults = ( id: denial.toolCallId, name: denial.toolCall.name, isFailure: true, - result: { type: "execution-denied", reason: denial.reason } + result: { type: "execution-denied", reason: denial.reason }, + providerExecuted: false }) ) } @@ -2050,7 +2101,7 @@ const resolveToolCalls = >( content: ReadonlyArray, toolkit: Toolkit.WithHandler, messages: ReadonlyArray, - concurrency: Concurrency | undefined + concurrency: Concurrency ): Stream.Stream< ToolResolutionResult, Tool.HandlerError | AiError.AiError, @@ -2098,7 +2149,7 @@ const resolveToolCalls = >( } if (approvedToolCallIds.has(toolCall.id)) { - return toolkit.handle(toolCall.name, toolCall.params as any).pipe( + return toolkit.handle(toolCall.name, toolCall.params as any, toolCall.id).pipe( Stream.unwrap, Stream.map( (result) => @@ -2124,7 +2175,7 @@ const resolveToolCalls = >( ) } - return toolkit.handle(toolCall.name, toolCall.params as any).pipe( + return toolkit.handle(toolCall.name, toolCall.params as any, toolCall.id).pipe( Stream.unwrap, Stream.map( (result) => @@ -2139,14 +2190,7 @@ const resolveToolCalls = >( }).pipe(Stream.unwrap) ) - const resolveConcurrency = concurrency === "inherit" - ? Effect.service(CurrentConcurrency) - : Effect.succeed(concurrency ?? "unbounded") - - return resolveConcurrency.pipe( - Effect.map((concurrency) => Stream.mergeAll(streams, { concurrency })), - Stream.unwrap - ) + return Stream.mergeAll(streams, { concurrency }) } // ============================================================================= diff --git a/.context/effect/packages/effect/src/unstable/ai/McpProtocol.ts b/.context/effect/packages/effect/src/unstable/ai/McpProtocol.ts new file mode 100644 index 000000000..046f2aa48 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/ai/McpProtocol.ts @@ -0,0 +1,48 @@ +/** + * Defines the MCP protocol implementations that an `McpServer` can support. + * + * @since 4.0.0 + */ +import type * as RpcGroup from "../rpc/RpcGroup.ts" +import * as Internal from "./internal/mcpProtocol.ts" +import * as McpSchema from "./McpSchema.ts" + +/** + * The MCP 2025-06-18 protocol implementation. + * + * @category protocols + * @since 4.0.0 + */ +export const v2025_06_18: ProtocolAdapter = Internal.make({ + protocolVersion: "2025-06-18", + transport: { + acceptsJsonRpcBatches: false, + requiresVersionHeader: true + }, + clientRpcs: McpSchema.ClientRpcs, + clientNotificationRpcs: McpSchema.ClientNotificationRpcs, + serverRequestRpcs: McpSchema.ServerRequestRpcs, + serverNotificationRpcs: McpSchema.ServerNotificationRpcs +}) + +/** + * An implemented MCP protocol that can be supplied to `McpServer`. + * + * @category models + * @since 4.0.0 + */ +export type ProtocolAdapter = Internal.ProtocolAdapter< + "2025-06-18", + RpcGroup.Rpcs, + RpcGroup.Rpcs, + RpcGroup.Rpcs, + RpcGroup.Rpcs +> + +/** + * The MCP protocol versions implemented by this release. + * + * @category models + * @since 4.0.0 + */ +export type ProtocolVersion = ProtocolAdapter["protocolVersion"] diff --git a/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts b/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts index 34c3f42cb..f88bcccbf 100644 --- a/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts +++ b/.context/effect/packages/effect/src/unstable/ai/McpSchema.ts @@ -23,6 +23,7 @@ import type * as RpcClient from "../rpc/RpcClient.ts" import type { RpcClientError } from "../rpc/RpcClientError.ts" import * as RpcGroup from "../rpc/RpcGroup.ts" import * as RpcMiddleware from "../rpc/RpcMiddleware.ts" +import type * as McpProtocol from "./McpProtocol.ts" /** * Schema type returned by `optionalWithDefault`. @@ -100,8 +101,8 @@ export const optional = ( */ export const RequestId: Schema.Union<[ typeof Schema.String, - typeof Schema.Number -]> = Schema.Union([Schema.String, Schema.Number]) + typeof Schema.Finite +]> = Schema.Union([Schema.String, Schema.Finite]) /** * Type represented by the JSON-RPC request identifier schema. @@ -120,8 +121,8 @@ export type RequestId = typeof RequestId.Type */ export const ProgressToken: Schema.Union<[ typeof Schema.String, - typeof Schema.Number -]> = Schema.Union([Schema.String, Schema.Number]) + typeof Schema.Finite +]> = Schema.Union([Schema.String, Schema.Finite]) /** * Type represented by the MCP progress token schema. @@ -299,7 +300,7 @@ export class Annotations extends Schema.Opaque()(Schema.Struct({ * effectively required, while 0 means "least important," and indicates that * the data is entirely optional. */ - priority: optional(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))) + priority: optional(Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 }))) })) {} /** @@ -449,7 +450,7 @@ export class McpErrorBase extends Schema.Class( /** * The error type that occurred. */ - code: Schema.Number, + code: Schema.Int, /** * A short description of the error. The message SHOULD be limited to a * concise single sentence. @@ -539,7 +540,7 @@ export const PARSE_ERROR_CODE = -32700 as const * @category errors * @since 4.0.0 */ -export class ParseError extends Schema.ErrorClass("effect/ai/McpSchema/ParseError")({ +export class ParseError extends Schema.Error("effect/ai/McpSchema/ParseError")({ ...McpErrorBase.fields, _tag: Schema.tag("ParseError"), code: Schema.tag(PARSE_ERROR_CODE) @@ -560,7 +561,7 @@ export class ParseError extends Schema.ErrorClass("effect/ai/McpSche * @category errors * @since 4.0.0 */ -export class InvalidRequest extends Schema.ErrorClass("effect/ai/McpSchema/InvalidRequest")({ +export class InvalidRequest extends Schema.Error("effect/ai/McpSchema/InvalidRequest")({ ...McpErrorBase.fields, _tag: Schema.tag("InvalidRequest"), code: Schema.tag(INVALID_REQUEST_ERROR_CODE) @@ -580,7 +581,7 @@ export class InvalidRequest extends Schema.ErrorClass("effect/ai * @category errors * @since 4.0.0 */ -export class MethodNotFound extends Schema.ErrorClass("effect/ai/McpSchema/MethodNotFound")({ +export class MethodNotFound extends Schema.Error("effect/ai/McpSchema/MethodNotFound")({ ...McpErrorBase.fields, _tag: Schema.tag("MethodNotFound"), code: Schema.tag(METHOD_NOT_FOUND_ERROR_CODE) @@ -601,7 +602,7 @@ export class MethodNotFound extends Schema.ErrorClass("effect/ai * @category errors * @since 4.0.0 */ -export class InvalidParams extends Schema.ErrorClass("effect/ai/McpSchema/InvalidParams")({ +export class InvalidParams extends Schema.Error("effect/ai/McpSchema/InvalidParams")({ ...McpErrorBase.fields, _tag: Schema.tag("InvalidParams"), code: Schema.tag(INVALID_PARAMS_ERROR_CODE) @@ -623,7 +624,7 @@ export class InvalidParams extends Schema.ErrorClass("effect/ai/M * @category errors * @since 4.0.0 */ -export class InternalError extends Schema.ErrorClass("effect/ai/McpSchema/InternalError")({ +export class InternalError extends Schema.Error("effect/ai/McpSchema/InternalError")({ ...McpErrorBase.fields, _tag: Schema.tag("InternalError"), code: Schema.tag(INTERNAL_ERROR_CODE) @@ -662,7 +663,7 @@ export const McpError = Schema.Union([ * * The receiver should respond promptly; otherwise the sender may disconnect. * - * @category ping + * @category protocols * @since 4.0.0 */ export class Ping extends Rpc.make("ping", { @@ -678,7 +679,7 @@ export class Ping extends Rpc.make("ping", { /** * Schema for the server's response to an initialize request from the client. * - * @category initialization + * @category schemas * @since 4.0.0 */ export class InitializeResult extends Schema.Opaque()(Schema.Struct({ @@ -705,7 +706,7 @@ export class InitializeResult extends Schema.Opaque()(Schema.S * Sent from the client to the server when it first connects, asking it to begin * initialization. * - * @category initialization + * @category protocols * @since 4.0.0 */ export class Initialize extends Rpc.make("initialize", { @@ -734,7 +735,7 @@ export class Initialize extends Rpc.make("initialize", { /** * Sent from the client to the server after initialization has finished. * - * @category initialization + * @category protocols * @since 4.0.0 */ export class InitializedNotification extends Rpc.make("notifications/initialized", { @@ -754,7 +755,7 @@ export class InitializedNotification extends Rpc.make("notifications/initialized * The payload identifies the request to cancel and may include a * human-readable reason. * - * @category cancellation + * @category protocols * @since 4.0.0 */ export class CancelledNotification extends Rpc.make("notifications/cancelled", { @@ -782,7 +783,7 @@ export class CancelledNotification extends Rpc.make("notifications/cancelled", { /** * Sent from either peer to report progress for a long-running request. * - * @category progress + * @category protocols * @since 4.0.0 */ export class ProgressNotification extends Rpc.make("notifications/progress", { @@ -797,11 +798,11 @@ export class ProgressNotification extends Rpc.make("notifications/progress", { * The progress thus far. This should increase every time progress is made, * even if the total is unknown. */ - progress: optional(Schema.Number), + progress: optional(Schema.Finite), /** * Total number of items to process (or total progress required), if known. */ - total: optional(Schema.Number), + total: optional(Schema.Finite), /** * An optional message describing the current progress. */ @@ -816,7 +817,7 @@ export class ProgressNotification extends Rpc.make("notifications/progress", { /** * Schema for a known resource that the server is capable of reading. * - * @category resources + * @category schemas * @since 4.0.0 */ export class Resource extends Schema.Class( @@ -855,7 +856,7 @@ export class Resource extends Schema.Class( * This can be used by Hosts to display file sizes and estimate context * window usage. */ - size: optional(Schema.Number), + size: optional(Schema.Int), /** * Optional additional metadata for the client. * @@ -868,7 +869,7 @@ export class Resource extends Schema.Class( /** * Schema for a template description of resources available on the server. * - * @category resources + * @category schemas * @since 4.0.0 */ export class ResourceTemplate extends Schema.Class( @@ -914,7 +915,7 @@ export class ResourceTemplate extends Schema.Class( /** * Schema for the contents of a specific resource or sub-resource. * - * @category resources + * @category schemas * @since 4.0.0 */ export class ResourceContents extends Schema.Opaque()(Schema.Struct({ @@ -935,7 +936,7 @@ export class ResourceContents extends Schema.Opaque()(Schema.S /** * Schema for text resource contents represented as a string. * - * @category resources + * @category schemas * @since 4.0.0 */ export class TextResourceContents extends Schema.Opaque()(Schema.Struct({ @@ -950,7 +951,7 @@ export class TextResourceContents extends Schema.Opaque()( /** * Schema for binary resource contents represented as a `Uint8Array`. * - * @category resources + * @category schemas * @since 4.0.0 */ export class BlobResourceContents extends Schema.Opaque()(Schema.Struct({ @@ -964,7 +965,7 @@ export class BlobResourceContents extends Schema.Opaque()( /** * Schema for the server's response to a resources/list request from the client. * - * @category resources + * @category schemas * @since 4.0.0 */ export class ListResourcesResult extends Schema.Class( @@ -977,7 +978,7 @@ export class ListResourcesResult extends Schema.Class( /** * Sent from the client to request a list of resources the server has. * - * @category resources + * @category protocols * @since 4.0.0 */ export class ListResources extends Rpc.make("resources/list", { @@ -990,7 +991,7 @@ export class ListResources extends Rpc.make("resources/list", { * Schema for the server's response to a resources/templates/list request from * the client. * - * @category resources + * @category schemas * @since 4.0.0 */ export class ListResourceTemplatesResult extends Schema.Class( @@ -1003,7 +1004,7 @@ export class ListResourceTemplatesResult extends Schema.Class()(Schema.Struct({ @@ -1026,7 +1027,7 @@ export class ReadResourceResult extends Schema.Opaque()(Sche /** * Sent from the client to the server, to read a specific resource URI. * - * @category resources + * @category protocols * @since 4.0.0 */ export class ReadResource extends Rpc.make("resources/read", { @@ -1053,7 +1054,7 @@ export class ReadResource extends Rpc.make("resources/read", { * * Servers may send this notification without a previous client subscription. * - * @category resources + * @category protocols * @since 4.0.0 */ export class ResourceListChangedNotification extends Rpc.make("notifications/resources/list_changed", { @@ -1064,10 +1065,11 @@ export class ResourceListChangedNotification extends Rpc.make("notifications/res * Sent from the client to request resources/updated notifications from the * server whenever a particular resource changes. * - * @category resources + * @category protocols * @since 4.0.0 */ export class Subscribe extends Rpc.make("resources/subscribe", { + success: Schema.Struct({}), error: McpError, payload: { ...RequestMeta.fields, @@ -1084,10 +1086,11 @@ export class Subscribe extends Rpc.make("resources/subscribe", { * notifications from the server. This should follow a previous * resources/subscribe request. * - * @category resources + * @category protocols * @since 4.0.0 */ export class Unsubscribe extends Rpc.make("resources/unsubscribe", { + success: Schema.Struct({}), error: McpError, payload: { ...RequestMeta.fields, @@ -1107,7 +1110,7 @@ export class Unsubscribe extends Rpc.make("resources/unsubscribe", { * The URI may identify a sub-resource of the resource that the client * originally subscribed to. * - * @category resources + * @category protocols * @since 4.0.0 */ export class ResourceUpdatedNotification extends Rpc.make("notifications/resources/updated", { @@ -1403,7 +1406,7 @@ export class PromptListChangedNotification extends Rpc.make("notifications/promp * Clients should never make tool use decisions based on ToolAnnotations * received from untrusted servers. * - * @category tools + * @category schemas * @since 4.0.0 */ export class ToolAnnotations extends Schema.Opaque()(Schema.Struct({ @@ -1449,7 +1452,7 @@ export class ToolAnnotations extends Schema.Opaque()(Schema.Str /** * Schema for the definition of a tool the client can call. * - * @category tools + * @category schemas * @since 4.0.0 */ export class Tool extends Schema.Class( @@ -1470,6 +1473,10 @@ export class Tool extends Schema.Class( * A JSON Schema object defining the expected parameters for the tool. */ inputSchema: Schema.Any, + /** + * An optional JSON Schema object defining the expected output of the tool. + */ + outputSchema: optional(Schema.Any), /** * Optional additional tool information. */ @@ -1486,7 +1493,7 @@ export class Tool extends Schema.Class( /** * Schema for the server's response to a tools/list request from the client. * - * @category tools + * @category schemas * @since 4.0.0 */ export class ListToolsResult extends Schema.Class( @@ -1499,7 +1506,7 @@ export class ListToolsResult extends Schema.Class( /** * Sent from the client to request a list of tools the server has. * - * @category tools + * @category protocols * @since 4.0.0 */ export class ListTools extends Rpc.make("tools/list", { @@ -1520,7 +1527,7 @@ export class ListTools extends Rpc.make("tools/list", { * indicating that the server does not support tool calls, or any other * exceptional conditions, should be reported as an MCP error response. * - * @category tools + * @category schemas * @since 4.0.0 */ export class CallToolResult extends Schema.Class("@effect/ai/McpSchema/CallToolResult")({ @@ -1546,7 +1553,7 @@ export class CallToolResult extends Schema.Class("@effect/ai/Mcp * @see {@link ListTools} for discovering available tools before calling one * @see {@link CallToolResult} for the successful tool-call result shape * - * @category tools + * @category protocols * @since 4.0.0 */ export class CallTool extends Rpc.make("tools/call", { @@ -1555,9 +1562,12 @@ export class CallTool extends Rpc.make("tools/call", { payload: { ...RequestMeta.fields, name: Schema.String, - arguments: Schema.Record( - Schema.String, - Schema.Any + arguments: optionalWithDefault( + Schema.Record( + Schema.String, + Schema.Any + ), + () => ({}) ) } }) {} @@ -1573,7 +1583,7 @@ export class CallTool extends Rpc.make("tools/call", { * * Servers may send this notification without a previous client subscription. * - * @category tools + * @category protocols * @since 4.0.0 */ export class ToolListChangedNotification extends Rpc.make("notifications/tools/list_changed", { @@ -1638,6 +1648,7 @@ export class SetLevel extends Rpc.make("logging/setLevel", { */ level: LoggingLevel }, + success: Schema.Struct({}), error: McpError }) {} @@ -1678,7 +1689,7 @@ export class LoggingMessageNotification extends Rpc.make("notifications/message" /** * Describes a message issued to or received from an LLM API. * - * @category sampling + * @category schemas * @since 4.0.0 */ export class SamplingMessage extends Schema.Opaque()(Schema.Struct({ @@ -1694,7 +1705,7 @@ export class SamplingMessage extends Schema.Opaque()(Schema.Str * Keys not declared here are currently left unspecified by the spec and are up * to the client to interpret. * - * @category sampling + * @category schemas * @since 4.0.0 */ export class ModelHint extends Schema.Opaque()(Schema.Struct({ @@ -1731,7 +1742,7 @@ export class ModelHint extends Schema.Opaque()(Schema.Struct({ * up to the client to decide how to interpret these preferences and how to * balance them against other considerations. * - * @category sampling + * @category schemas * @since 4.0.0 */ export class ModelPreferences extends Schema.Class( @@ -1752,19 +1763,19 @@ export class ModelPreferences extends Schema.Class( * is not important, while a value of 1 means cost is the most important * factor. */ - costPriority: optional(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))), + costPriority: optional(Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 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: optional(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))), + speedPriority: optional(Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 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: optional(Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))) + intelligencePriority: optional(Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 }))) }) {} /** @@ -1779,12 +1790,13 @@ export class ModelPreferences extends Schema.Class( * The client should let the user inspect the sampled message before returning * it to the server. * - * @category sampling + * @category schemas * @since 4.0.0 */ export class CreateMessageResult extends Schema.Class( "@effect/ai/McpSchema/CreateMessageResult" )({ + ...SamplingMessage.fields, /** * The name of the model that generated the message. */ @@ -1808,7 +1820,7 @@ export class CreateMessageResult extends Schema.Class( * The client chooses the model and should ask the user to approve the sampling * request before it begins. * - * @category sampling + * @category protocols * @since 4.0.0 */ export class CreateMessage extends Rpc.make("sampling/createMessage", { @@ -1820,7 +1832,7 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { * The server's preferences for which model to select. The client MAY ignore * these preferences. */ - modelPreferences: optional(ModelPreferences), + modelPreferences: optional(Schema.Struct(ModelPreferences.fields)), /** * An optional system prompt the server wants to use for sampling. The * client MAY modify or omit this prompt. @@ -1831,18 +1843,18 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { * caller), to be attached to the prompt. The client MAY ignore this request. */ includeContext: optional(Schema.Literals(["none", "thisServer", "allServers"])), - temperature: optional(Schema.Number), + temperature: optional(Schema.Finite), /** * 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, + maxTokens: Schema.Int, stopSequences: optional(Schema.Array(Schema.String)), /** * Optional metadata to pass through to the LLM provider. The format of * this metadata is provider-specific. */ - metadata: Schema.Any + metadata: optional(Schema.Record(Schema.String, Schema.Unknown)) } }) {} @@ -1853,7 +1865,7 @@ export class CreateMessage extends Rpc.make("sampling/createMessage", { /** * Schema for a reference to a resource or resource template definition. * - * @category autocomplete + * @category schemas * @since 4.0.0 */ export class ResourceReference extends Schema.Opaque()(Schema.Struct({ @@ -1867,7 +1879,7 @@ export class ResourceReference extends Schema.Opaque()(Schema /** * Schema for a prompt reference used in autocomplete requests. * - * @category autocomplete + * @category schemas * @since 4.0.0 */ export class PromptReference extends Schema.Opaque()(Schema.Struct({ @@ -1882,7 +1894,7 @@ export class PromptReference extends Schema.Opaque()(Schema.Str /** * Schema for the server's response to a completion/complete request. * - * @category autocomplete + * @category schemas * @since 4.0.0 */ export class CompleteResult extends Schema.Opaque()(Schema.Struct({ @@ -1895,7 +1907,7 @@ export class CompleteResult extends Schema.Opaque()(Schema.Struc * The total number of completion options available. This can exceed the * number of values actually sent in the response. */ - total: optional(Schema.Number), + total: optional(Schema.Int), /** * Indicates whether there are additional completion options beyond those * provided in the current response, even if the exact total is unknown. @@ -1920,7 +1932,7 @@ export class CompleteResult extends Schema.Opaque()(Schema.Struc /** * Sent from the client to the server to ask for completion options. * - * @category autocomplete + * @category protocols * @since 4.0.0 */ export class Complete extends Rpc.make("completion/complete", { @@ -1966,7 +1978,7 @@ export class Complete extends Rpc.make("completion/complete", { /** * Represents a root directory or file that the server can operate on. * - * @category roots + * @category schemas * @since 4.0.0 */ export class Root extends Schema.Class( @@ -1993,7 +2005,7 @@ export class Root extends Schema.Class( * * Use to return the directories or files that an MCP server may operate on. * - * @category roots + * @category schemas * @since 4.0.0 */ export class ListRootsResult extends Schema.Class( @@ -2014,7 +2026,7 @@ export class ListRootsResult extends Schema.Class( * system structure or access specific locations that the client has permission * to read from. * - * @category roots + * @category protocols * @since 4.0.0 */ export class ListRoots extends Rpc.make("roots/list", { @@ -2034,7 +2046,7 @@ export class ListRoots extends Rpc.make("roots/list", { * * Send this when the client adds, removes, or modifies a root. * - * @category roots + * @category protocols * @since 4.0.0 */ export class RootsListChangedNotification extends Rpc.make("notifications/roots/list_changed", { @@ -2048,7 +2060,7 @@ export class RootsListChangedNotification extends Rpc.make("notifications/roots/ /** * Schema for an accepted client response to an elicitation request. * - * @category elicitation + * @category schemas * @since 4.0.0 */ export class ElicitAcceptResult extends Schema.Class( @@ -2072,7 +2084,7 @@ export class ElicitAcceptResult extends Schema.Class( /** * Schema for a declined or canceled client response to an elicitation request. * - * @category elicitation + * @category schemas * @since 4.0.0 */ export class ElicitDeclineResult extends Schema.Class( @@ -2091,7 +2103,7 @@ export class ElicitDeclineResult extends Schema.Class( /** * Schema for every client response to an elicitation request. * - * @category elicitation + * @category schemas * @since 4.0.0 */ export const ElicitResult = Schema.Union([ @@ -2108,7 +2120,7 @@ export const ElicitResult = Schema.Union([ * The client responds with accepted content, an explicit decline, or a * cancellation. * - * @category elicitation + * @category protocols * @since 4.0.0 */ export class Elicit extends Rpc.make("elicitation/create", { @@ -2137,16 +2149,14 @@ export class Elicit extends Rpc.make("elicitation/create", { * The error stores the original elicitation request and, when available, the * underlying cause. * - * @category elicitation + * @category schemas * @since 4.0.0 */ -export class ElicitationDeclined - extends Schema.ErrorClass("@effect/ai/McpSchema/ElicitationDeclined")({ - _tag: Schema.tag("ElicitationDeclined"), - request: Elicit.payloadSchema, - cause: optional(Schema.Defect()) - }) -{} +export class ElicitationDeclined extends Schema.Error("@effect/ai/McpSchema/ElicitationDeclined")({ + _tag: Schema.tag("ElicitationDeclined"), + request: Elicit.payloadSchema, + cause: optional(Schema.Defect()) +}) {} // ============================================================================= // McpServerClient @@ -2160,11 +2170,12 @@ export class ElicitationDeclined * It exposes the current client id, the client's initialize payload, and a * scoped RPC client for server-initiated requests back to that client. * - * @category client + * @category services * @since 4.0.0 */ export class McpServerClient extends Context.Service, RpcClientError>, @@ -2459,7 +2470,7 @@ const ParamSchemaTypeId = "~effect/ai/McpSchema/ParamSchema" * Returns `true` when a schema was created with `param` and therefore carries * a resource URI template parameter name. * - * @category parameters + * @category guards * @since 4.0.0 */ export function isParam(schema: Schema.Constraint): schema is Param { @@ -2519,7 +2530,7 @@ export function param( * Annotation to conditionally enable or disable tools based on client * information. * - * @category annotations + * @category services * @since 4.0.0 */ export class EnabledWhen diff --git a/.context/effect/packages/effect/src/unstable/ai/McpServer.ts b/.context/effect/packages/effect/src/unstable/ai/McpServer.ts index a3e90016f..4256efd96 100644 --- a/.context/effect/packages/effect/src/unstable/ai/McpServer.ts +++ b/.context/effect/packages/effect/src/unstable/ai/McpServer.ts @@ -13,11 +13,14 @@ import * as Arr from "../../Array.ts" import * as Cause from "../../Cause.ts" import * as Context from "../../Context.ts" +import * as Data from "../../Data.ts" import * as Effect from "../../Effect.ts" import * as Exit from "../../Exit.ts" import * as Fiber from "../../Fiber.ts" import * as Layer from "../../Layer.ts" +import * as LogLevel from "../../LogLevel.ts" import * as Option from "../../Option.ts" +import * as Predicate from "../../Predicate.ts" import * as Queue from "../../Queue.ts" import * as RcMap from "../../RcMap.ts" import { CurrentLogLevel } from "../../References.ts" @@ -39,29 +42,37 @@ import type * as RpcGroup from "../rpc/RpcGroup.ts" import * as RpcMessage from "../rpc/RpcMessage.ts" import * as RpcSerialization from "../rpc/RpcSerialization.ts" import * as RpcServer from "../rpc/RpcServer.ts" +import * as AiError from "./AiError.ts" +import * as McpProtocolRegistry from "./internal/mcpProtocolRegistry.ts" +import type * as McpProtocol from "./McpProtocol.ts" import { CallToolResult, - ClientNotificationRpcs, + CancelledNotification, ClientRpcs, - CompleteResult, Elicit, ElicitationDeclined, EnabledWhen, GetPromptResult, InternalError, + INVALID_REQUEST_ERROR_CODE, InvalidParams, + InvalidRequest, isParam, ListPromptsResult, ListResourcesResult, ListResourceTemplatesResult, ListToolsResult, + LoggingMessageNotification, + McpErrorBase, McpServerClient, McpServerClientMiddleware, + MethodNotFound, + ParseError, Prompt, Resource, ResourceTemplate, + ResourceUpdatedNotification, ServerNotificationRpcs, - ServerRequestRpcs, TextContent, Tool as McpTool } from "./McpSchema.ts" @@ -69,8 +80,10 @@ import type { CallTool, ClientCapabilities, Complete, + CompleteResult, GetPrompt, Initialize, + LoggingLevel, Param, PromptArgument, PromptMessage, @@ -80,6 +93,8 @@ import type { import * as Tool from "./Tool.ts" import type * as Toolkit from "./Toolkit.ts" +type CompletionContext = typeof Complete.payloadSchema.Type["context"] + /** * Service that stores and serves an MCP server's registered tools, resources, * prompts, completions, and outgoing notifications. @@ -89,7 +104,7 @@ import type * as Toolkit from "./Toolkit.ts" * Handlers use this service to register capabilities and resolve incoming MCP * requests. * - * @category server + * @category services * @since 4.0.0 */ export class McpServer extends Context.Service - readonly handle: (payload: any) => Effect.Effect + readonly handle: (payload: any) => Effect.Effect }) => Effect.Effect readonly callTool: ( requests: typeof CallTool.payloadSchema.Type @@ -129,17 +144,27 @@ export class McpServer extends Context.Service readonly routerPath: string - readonly completions: Record Effect.Effect> + readonly completions: Record< + string, + ( + input: string, + context: CompletionContext + ) => Effect.Effect + > readonly handle: ( uri: string, params: Array - ) => Effect.Effect + ) => Effect.Effect< + typeof ReadResourceResult.Type, + InvalidParams | InternalError, + McpServerClient + > } ) => Effect.Effect readonly findResource: ( uri: string - ) => Effect.Effect + ) => Effect.Effect readonly prompts: ReadonlyArray<{ readonly prompt: Prompt @@ -150,7 +175,10 @@ export class McpServer extends Context.Service readonly completions: Record< string, - (input: string) => Effect.Effect + ( + input: string, + context: CompletionContext + ) => Effect.Effect > readonly handle: ( params: Record @@ -162,7 +190,7 @@ export class McpServer extends Context.Service Effect.Effect + ) => Effect.Effect }>()("effect/ai/McpServer") { /** * Builds an MCP server service from registered tools, prompts, resources, and completions. @@ -176,7 +204,11 @@ export class McpServer extends Context.Service - ) => Effect.Effect + ) => Effect.Effect< + typeof ReadResourceResult.Type, + InternalError | InvalidParams, + McpServerClient + > } | { readonly _tag: "Resource" readonly effect: Effect.Effect @@ -186,7 +218,10 @@ export class McpServer extends Context.Service }>() - const toolMap = new Map Effect.Effect>() + const toolMap = new Map< + string, + (payload: any) => Effect.Effect + >() const resources: Array<{ readonly resource: Resource readonly annotations: Context.Context @@ -205,7 +240,10 @@ export class McpServer extends Context.Service() const completionsMap = new Map< string, - (input: string) => Effect.Effect + ( + input: string, + context: CompletionContext + ) => Effect.Effect >() const notificationsQueue = yield* Queue.make>() const listChangedHandles = new Map() @@ -258,7 +296,9 @@ export class McpServer extends Context.Service Effect.fail(new InternalError({ message: "Internal error" }))) + ) }), get resources() { return resources @@ -285,7 +325,7 @@ export class McpServer extends Context.Service { const match = matcher.find(uri) if (!match) { - return Effect.succeed({ contents: [] }) + return Effect.fail(new McpErrorBase({ code: -32002, message: `Resource '${uri}' not found` })) } else if (match.handler._tag === "Resource") { return match.handler.effect } @@ -320,7 +360,19 @@ export class McpServer extends Context.Service = Layer.effect(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" -] -const mcpSessionIdHeader = "mcp-session-id" -const mcpProtocolVersionHeader = "mcp-protocol-version" +const MCP_SESSION_ID_HEADER = "mcp-session-id" +const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" +const MCP_INVALID_BATCH_METHOD = "invalid/json-rpc-batch" +const decodeCancelledNotification = Schema.decodeUnknownEffect( + Schema.toCodecJson(CancelledNotification.payloadSchema) +) + +const requestKey = (requestId: string | number): string => `${typeof requestId}:${requestId}` + +type SessionLogLevel = + | { + readonly _tag: "Effect" + readonly level: LogLevel.LogLevel + } + | { + readonly _tag: "Mcp" + readonly level: LoggingLevel + } + +interface Session { + readonly initializePayload: typeof Initialize.payloadSchema.Type + readonly protocol: McpProtocol.ProtocolAdapter + readonly resourceSubscriptions: Set | undefined + logLevel: SessionLogLevel +} + +interface Sessions { + readonly bySessionId: Map + readonly byClientId: Map +} + +class McpClientKey extends Data.Class<{ + readonly clientId: number + readonly protocolVersion: string +}> {} + +class McpProtocolState extends Context.Service +}>()("effect/ai/McpServer/McpProtocolState") {} + +const makeMcpProtocolState = Effect.fnUntraced(function*( + protocols: Arr.NonEmptyReadonlyArray +) { + return McpProtocolState.of({ + sessions: { + bySessionId: new Map(), + byClientId: new Map() + }, + protocolRegistry: yield* McpProtocolRegistry.make(protocols) + }) +}) + +const layerMcpProtocolState = ( + protocols: Arr.NonEmptyReadonlyArray +): Layer.Layer => + Layer.effect(McpProtocolState)(makeMcpProtocolState(protocols)) /** * Runs an MCP server over the current `RpcServer.Protocol`. @@ -352,31 +452,54 @@ const mcpProtocolVersionHeader = "mcp-protocol-version" * tools, resources, and prompts, and forwards queued server notifications to * initialized clients. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: (options: { readonly name: string readonly version: string + readonly protocols: Arr.NonEmptyReadonlyArray readonly extensions?: Record<`${string}/${string}`, unknown> | undefined }) => Effect.Effect< never, - never, + Cause.IllegalArgumentError, McpServer | RpcServer.Protocol > = Effect.fnUntraced(function*(options: { readonly name: string readonly version: string + readonly protocols: Arr.NonEmptyReadonlyArray + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined }) { + const protocolStateOption = yield* Effect.serviceOption(McpProtocolState) + const protocolState = Option.isSome(protocolStateOption) + ? protocolStateOption.value + : yield* makeMcpProtocolState(options.protocols) + return yield* runWithProtocolState(options, protocolState) +}) + +const runWithProtocolState = Effect.fnUntraced(function*(options: { + readonly name: string + readonly version: string + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined +}, protocolState: McpProtocolState["Service"]) { + const serverScope = yield* Effect.scope + const protocolRegistry = protocolState.protocolRegistry const protocol = yield* RpcServer.Protocol const server = yield* McpServer const isHttp = Option.isSome(yield* Effect.serviceOption(HttpRouter.HttpRouter)) - const clientSessions = new Map() - const handlers = yield* Layer.build(layerHandlers(options, { clientSessions })) + const sessions = protocolState.sessions + const clientProtocols = new Map() + const activeRequests = new Map>() + const handlers = yield* Layer.build(layerHandlers(options, { + sessions, + protocolRegistry + })) const clients = yield* RcMap.make({ - lookup: Effect.fnUntraced(function*(clientId: number) { + lookup: Effect.fnUntraced(function*(key: McpClientKey) { + const selectedProtocol = protocolRegistry.select(key.protocolVersion) let write!: (message: RpcMessage.FromServerEncoded) => Effect.Effect - const client = yield* RpcClient.make(ServerRequestRpcs, { + const client = yield* RpcClient.make(selectedProtocol.serverRequestRpcs, { spanPrefix: "McpServer/Client" }).pipe( Effect.provideServiceEffect( @@ -387,7 +510,7 @@ export const run: (options: { return { send(id, request, _transferables) { cid = id - return protocol.send(clientId, { + return protocol.send(key.clientId, { ...request, headers: undefined, traceId: undefined, @@ -408,84 +531,253 @@ export const run: (options: { idleTimeToLive: 10000 }) - const clientMiddleware = McpServerClientMiddleware.of((effect, { client, headers, rpc }) => { - const initializePayload = getInitializedClient(clientSessions, client.id, headers) - const isInitialize = rpc._tag === "initialize" - if (!isInitialize && !initializePayload) { + const clientMiddleware = McpServerClientMiddleware.of((effect, { client, headers, payload, rpc }) => { + const session = getClientSession(sessions, client.id, headers) + const isInitialize = rpc._tag.endsWith("/initialize") + if (!isInitialize && !session) { const fiber = Fiber.getCurrent()! const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) if (httpRequest) { appendPreResponseHandlerUnsafe( httpRequest, - () => Effect.succeed(HttpServerResponse.empty({ status: 404 })) + () => + Effect.succeed( + HttpServerResponse.empty({ + status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 + }) + ) ) } return Effect.die(new Error(`Mcp-Session-Id does not exist`)) } - return Effect.provideService( - effect, - McpServerClient, - McpServerClient.of({ - clientId: client.id, - initializePayload: initializePayload!, - getClient: RcMap.get(clients, client.id).pipe( - Effect.map(({ client }) => client) - ) - }) + const selectedProtocol = session?.protocol ?? protocolForInternalTag(protocolRegistry, rpc._tag) + return effect.pipe( + Effect.provideService( + McpServerClient, + McpServerClient.of({ + clientId: client.id, + protocolVersion: selectedProtocol.protocolVersion, + initializePayload: session?.initializePayload ?? payload as typeof Initialize.payloadSchema.Type, + getClient: RcMap.get( + clients, + new McpClientKey({ + clientId: client.id, + protocolVersion: selectedProtocol.protocolVersion + }) + ).pipe( + Effect.map(({ client }) => client) + ) + }) + ), + Effect.provideService(CurrentLogLevel, effectLogLevel(session?.logLevel)) ) }) const patchedProtocol = RpcServer.Protocol.of({ ...protocol, + send: (clientId, response) => { + if (response._tag === "Exit") { + const requests = activeRequests.get(clientId) + const key = requestKey(response.requestId) + const cancelled = requests?.get(key) + if (requests !== undefined && requests.delete(key) && requests.size === 0) { + activeRequests.delete(clientId) + } + if (cancelled === true) { + return Effect.void + } + } + return protocol.send(clientId, response) + }, run: (f) => protocol.run((clientId, request_) => { - const request = request_ as any as + const fiber = Fiber.getCurrent()! + const request = request_ as unknown as | RpcMessage.FromServerEncoded | RpcMessage.FromClientEncoded + const httpRequest = isHttp + ? Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) + : undefined + if (httpRequest !== undefined && request._tag !== "Eof") { + appendPreResponseHandlerUnsafe(httpRequest, (_, response) => + Effect.succeed( + response.status === 200 && + response.body._tag === "Uint8Array" && + response.body.contentLength === 0 + ? HttpServerResponse.empty({ + headers: Headers.remove(response.headers, "content-type"), + status: 202 + }) + : response + )) + } switch (request._tag) { case "Request": { + const headers = isHttp + ? Context.getUnsafe( + Fiber.getCurrent()!.context, + HttpServerRequest.HttpServerRequest + ).headers + : Headers.fromInput(request.headers) + const session = getClientSession(sessions, clientId, headers) + const selectedProtocol = session?.protocol ?? + (request.tag === "initialize" + ? protocolRegistry.select(getOfferedProtocolVersion(request.payload)) + : protocolRegistry.protocols[0]) + clientProtocols.set(clientId, selectedProtocol) + if (request.tag === MCP_INVALID_BATCH_METHOD) { + return protocol.send(clientId, { + _tag: "Exit", + requestId: request.id, + exit: { + _tag: "Failure", + cause: [{ + _tag: "Fail", + error: new InvalidRequest({ message: "JSON-RPC batches are not supported" }) + }] + } + }) + } if (isHttp) { const fiber = Fiber.getCurrent()! const httpRequest = Context.getUnsafe(fiber.context, HttpServerRequest.HttpServerRequest) - const client = getInitializedClient(clientSessions, clientId, httpRequest.headers) - if (client) { + if (session) { appendPreResponseHandlerUnsafe(httpRequest, (_, res) => Effect.succeed( - HttpServerResponse.setHeader(res, mcpProtocolVersionHeader, client.protocolVersion) + HttpServerResponse.setHeader( + res, + MCP_PROTOCOL_VERSION_HEADER, + session.protocol.protocolVersion + ) )) } } - const rpc = ClientNotificationRpcs.requests.get(request.tag) - if (rpc) { + const routedRequest = protocolRegistry.routeClientRequest(selectedProtocol, request) + const rpc = protocolRegistry.clientRpcs.requests.get(routedRequest.tag) + if (rpc && selectedProtocol.clientNotificationRpcs.requests.has(request.tag)) { + if (!session) { + if (httpRequest) { + appendPreResponseHandlerUnsafe( + httpRequest, + () => + Effect.succeed( + HttpServerResponse.empty({ + status: headers[MCP_SESSION_ID_HEADER] === undefined ? 400 : 404 + }) + ) + ) + } + return Effect.void + } if (request.tag === "notifications/cancelled") { - return f(clientId, { - _tag: "Interrupt", - requestId: String((request.payload as any).requestId) - }) + return decodeCancelledNotification(request.payload).pipe( + Effect.flatMap(({ requestId }) => { + const key = requestKey(requestId) + const requests = activeRequests.get(clientId) + if (requests?.has(key) !== true) { + return Effect.void + } + requests.set(key, true) + return f(clientId, { + _tag: "Interrupt", + requestId: RpcMessage.RequestId(requestId) + }) + }), + Effect.catchCause(() => Effect.void) + ) } - const handler = handlers.mapUnsafe.get(request.tag) as Rpc.Handler - return handler - ? handler.handler(request.payload, { - rpc, - requestId: RpcMessage.RequestId(request.id), - client: new Rpc.ServerClient(clientId), - headers: Headers.fromInput(request.headers) - }) as any as Effect.Effect - : Effect.void + return selectedProtocol.payloadCodecs(rpc).decode(request.payload).pipe( + Effect.flatMap((payload) => { + if ( + request.tag === "notifications/roots/list_changed" && + session.initializePayload.capabilities.roots?.listChanged === true + ) { + if (httpRequest !== undefined) { + return Effect.void + } + return RcMap.get( + clients, + new McpClientKey({ + clientId, + protocolVersion: selectedProtocol.protocolVersion + }) + ).pipe( + Effect.flatMap(({ client }) => client["roots/list"](undefined)), + Effect.scoped, + Effect.ignoreCause, + Effect.forkIn(serverScope), + Effect.asVoid + ) + } + const handler = handlers.mapUnsafe.get(rpc.key) as Rpc.Handler | undefined + return handler + ? handler.handler(payload, { + rpc, + requestId: RpcMessage.RequestId(request.id), + client: new Rpc.ServerClient(clientId), + headers + }) as any as Effect.Effect + : Effect.void + }), + Effect.catchCause(() => Effect.void) + ) } - return f(clientId, request) + if (!rpc) { + if (request.isNotification) { + return Effect.void + } + return protocol.send(clientId, { + _tag: "Exit", + requestId: request.id, + exit: { + _tag: "Failure", + cause: [{ _tag: "Fail", error: new MethodNotFound({ message: `Method not found: ${request.tag}` }) }] + } + }) + } + return selectedProtocol.payloadCodecs(rpc).decode(request.payload).pipe( + Effect.matchEffect({ + onSuccess: () => { + if (request.isNotification !== true) { + const requests = activeRequests.get(clientId) ?? new Map() + requests.set(requestKey(request.id), false) + activeRequests.set(clientId, requests) + } + return f(clientId, routedRequest) + }, + onFailure: () => + request.isNotification + ? Effect.void + : protocol.send(clientId, { + _tag: "Exit", + requestId: request.id, + exit: { + _tag: "Failure", + cause: [{ _tag: "Fail", error: new InvalidParams({ message: "Invalid method parameters" }) }] + } + }) + }) + ) } case "Ping": case "Ack": case "Interrupt": + return f(clientId, request) case "Eof": + activeRequests.delete(clientId) return f(clientId, request) case "Pong": case "Exit": case "Chunk": case "ClientProtocolError": case "Defect": - return RcMap.get(clients, clientId).pipe( + return RcMap.get( + clients, + new McpClientKey({ + clientId, + protocolVersion: getProtocolForClient(clientProtocols, clientId, protocolRegistry).protocolVersion + }) + ).pipe( Effect.flatMap(({ write }) => write(request)), Effect.scoped ) @@ -493,23 +785,52 @@ export const run: (options: { }) }) - const encodeNotification = Schema.encodeUnknownEffect( - Schema.Union(Array.from(ServerNotificationRpcs.requests.values(), (rpc) => rpc.payloadSchema)) - ) yield* Queue.take(server.notificationsQueue).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 clientProtocols.keys()) { + if (!clientIds.has(clientId)) { + clientProtocols.delete(clientId) + sessions.byClientId.delete(clientId) + } + } for (const clientId of server.initializedClients.keys()) { if (!clientIds.has(clientId)) { server.initializedClients.delete(clientId) continue } + const selectedProtocol = clientProtocols.get(clientId) + if (!selectedProtocol) { + continue + } + const rpc = selectedProtocol.serverNotificationRpcs.requests.get(request.tag) + if (!rpc) { + continue + } + if (request.tag === "notifications/message") { + const { level } = yield* Schema.decodeUnknownEffect( + LoggingMessageNotification.payloadSchema + )(request.payload) + if (!isMcpLogLevelEnabled(level, sessions.byClientId.get(clientId)?.logLevel)) { + continue + } + } + if (request.tag === "notifications/resources/updated") { + const { uri } = yield* Schema.decodeUnknownEffect( + ResourceUpdatedNotification.payloadSchema + )(request.payload) + if (sessions.byClientId.get(clientId)?.resourceSubscriptions?.has(uri) !== true) { + continue + } + } + const encoded = yield* selectedProtocol.payloadCodecs(rpc).encode(request.payload) + // TODO: Extend RpcServer.Protocol's outbound message contract with server-originated + // notifications so MCP does not need to treat this notification as an RPC response. + const message: RpcMessage.RequestEncoded = { + _tag: "Request", + tag: request.tag, + payload: encoded + } as any yield* patchedProtocol.send(clientId, message as any) } })), @@ -518,7 +839,7 @@ export const run: (options: { Effect.forkScoped ) - return yield* RpcServer.make(ClientRpcs, { + return yield* RpcServer.make(protocolRegistry.clientRpcs, { spanPrefix: "McpServer", disableFatalDefects: true }).pipe( @@ -560,65 +881,145 @@ export const run: (options: { export const layer = (options: { readonly name: string readonly version: string + readonly protocols: Arr.NonEmptyReadonlyArray readonly extensions?: Record<`${string}/${string}`, unknown> | undefined -}): Layer.Layer => - Layer.effectDiscard(Effect.forkScoped(run(options))).pipe( +}): Layer.Layer => + layerWithProtocolState(options).pipe( + Layer.provide(layerMcpProtocolState(options.protocols)) + ) + +const layerWithProtocolState = (options: { + readonly name: string + readonly version: string + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined +}): Layer.Layer => + Layer.effectDiscard( + Effect.gen(function*() { + const protocolState = yield* McpProtocolState + yield* Effect.forkScoped(runWithProtocolState(options, protocolState)) + }) + ).pipe( Layer.provideMerge(McpServer.layer) ) +const StdioInitializeRequest = Schema.Struct({ + method: Schema.Literal("initialize"), + params: Schema.Struct({ + protocolVersion: Schema.String + }) +}) + +const StdioInvalidBatchExit = Schema.Struct({ + _tag: Schema.Literal("Exit"), + requestId: Schema.Null, + exit: Schema.Struct({ + cause: Schema.Unknown + }) +}) + +const decodeStdioInitializeRequest = Schema.decodeUnknownOption(StdioInitializeRequest) +const decodeStdioInvalidBatchExit = Schema.decodeUnknownOption(StdioInvalidBatchExit) + +const makeStdioSerialization = ( + protocols: Arr.NonEmptyReadonlyArray +): RpcSerialization.RpcSerialization["Service"] => + RpcSerialization.RpcSerialization.of({ + contentType: "application/json-rpc", + includesFraming: true, + makeUnsafe: () => { + const framing = RpcSerialization.ndjson.makeUnsafe() + const jsonRpc = RpcSerialization.jsonRpc().makeUnsafe() + const protocolsByVersion = new Map( + protocols.map((protocol) => [protocol.protocolVersion, protocol]) + ) + let selectedProtocol = protocols[0] + return { + decode: (data) => { + const frames = framing.decode(data) + const messages: Array = [] + for (const frame of frames) { + const entries = Array.isArray(frame) ? frame : [frame] + const initialize = Arr.findFirst(entries, (entry) => decodeStdioInitializeRequest(entry)) + selectedProtocol = Option.match(initialize, { + onNone: () => selectedProtocol, + onSome: ({ params }) => protocolsByVersion.get(params.protocolVersion) ?? protocols[0] + }) + if (Array.isArray(frame) && !selectedProtocol.transport.acceptsJsonRpcBatches) { + messages.push({ + _tag: "Request", + id: null, + tag: MCP_INVALID_BATCH_METHOD, + payload: null, + headers: [] + }) + } else { + messages.push(...jsonRpc.decode(JSON.stringify(frame))) + } + } + return messages + }, + encode: (response) => { + const invalidBatchExit = decodeStdioInvalidBatchExit(response) + if (Option.isSome(invalidBatchExit)) { + return framing.encode({ + jsonrpc: "2.0", + id: null, + error: { + _tag: "Cause", + code: INVALID_REQUEST_ERROR_CODE, + message: "JSON-RPC batches are not supported", + data: invalidBatchExit.value.exit.cause + } + }) + } + const encoded = jsonRpc.encode(response) + return encoded === undefined ? undefined : `${encoded}\n` + } + } + } + }) + /** * Runs the McpServer, using stdio for input and output. * - * **Example** (Running an MCP server over stdio) + * **Example** (Configuring an MCP server over stdio) * - * ```ts - * import { Effect, Layer, Logger, Schema } from "effect" - * import { NodeRuntime, NodeStdio } from "@effect/platform-node" - * import { McpSchema, McpServer } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Effect, Layer, Schema } from "effect" + * import { McpProtocol, McpSchema, McpServer } from "effect/unstable/ai" * * const idParam = McpSchema.param("id", Schema.Number) * - * // 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]) + * id: () => Effect.succeed([1, 2, 3]) * }, - * content: Effect.fn(function*(_uri, id) { - * return `# MCP Server Demo - ID: ${id}` - * }) + * content: (_uri, id) => Effect.succeed(`# 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", + * description: "Looks up flight booking details", * parameters: { * flightNumber: Schema.String * }, * completion: { - * flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"]) + * flightNumber: () => Effect.succeed(["FL123", "FL456"]) * }, * 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 + * const ServerLayer = Layer.mergeAll(ReadmeTemplate, TestPrompt).pipe( * Layer.provide(McpServer.layerStdio({ * name: "Demo Server", * version: "1.0.0", - * })), - * Layer.provide(NodeStdio.layer), - * Layer.provide(Layer.succeed(Logger.LogToStderr)(true)) + * protocols: [McpProtocol.v2025_06_18] + * })) * ) * - * Layer.launch(ServerLayer).pipe(NodeRuntime.runMain) + * Layer.isLayer(ServerLayer) // => true * ``` * * @category layers @@ -627,16 +1028,18 @@ export const layer = (options: { export const layerStdio = (options: { readonly name: string readonly version: string + readonly protocols: Arr.NonEmptyReadonlyArray readonly extensions?: Record<`${string}/${string}`, unknown> | undefined -}): Layer.Layer => +}): Layer.Layer => layer(options).pipe( Layer.provide(RpcServer.layerProtocolStdio), - Layer.provide(RpcSerialization.layerNdJsonRpc()) + Layer.provide( + Layer.succeed(RpcSerialization.RpcSerialization)(makeStdioSerialization(options.protocols)) + ) ) /** - * Registers an HTTP POST JSON-RPC route at `options.path` on the current - * `HttpRouter`. + * Registers a Streamable HTTP MCP endpoint at `options.path`. * * **When to use** * @@ -644,8 +1047,9 @@ export const layerStdio = (options: { * * **Details** * - * This layer composes `layer(options)`, `RpcServer.layerProtocolHttp(options)`, - * and `RpcSerialization.layerJsonRpc()`. + * POST serves JSON-RPC and accepted notification-only requests return `202`. + * Unsupported protocol versions return `400`; methods without MCP handlers + * return `405`. Browser Origins are rejected unless listed in `allowedOrigins`. * * @see {@link layerStdio} for exposing the server over stdio * @see {@link layer} for the base MCP server layer without a transport protocol @@ -657,17 +1061,201 @@ export const layerHttp = (options: { readonly name: string readonly version: string readonly path: HttpRouter.PathInput + readonly protocols: Arr.NonEmptyReadonlyArray + readonly allowedOrigins?: ReadonlyArray | undefined readonly extensions?: Record<`${string}/${string}`, unknown> | undefined -}): Layer.Layer => - layer(options).pipe( - Layer.provide(RpcServer.layerProtocolHttp(options)), +}): Layer.Layer => { + const protocolState = layerMcpProtocolState(options.protocols) + const methodNotAllowedResponse = HttpServerResponse.empty({ + status: 405, + headers: { allow: "POST" } + }) + const methodNotAllowed = (request: HttpServerRequest.HttpServerRequest) => + Effect.succeed( + request.headers.origin !== undefined && + !options.allowedOrigins?.includes(request.headers.origin) + ? HttpServerResponse.empty({ status: 403 }) + : methodNotAllowedResponse + ) + const routes = Layer.mergeAll( + HttpRouter.add("GET", options.path, methodNotAllowed), + HttpRouter.add("PUT", options.path, methodNotAllowed), + HttpRouter.add("PATCH", options.path, methodNotAllowed), + HttpRouter.add("DELETE", options.path, methodNotAllowed), + HttpRouter.add("OPTIONS", options.path, methodNotAllowed) + ) + return Layer.merge(layerWithProtocolState(options), routes).pipe( + Layer.provide(layerMcpProtocolHttp(options)), + Layer.provide(protocolState), Layer.provide(RpcSerialization.layerJsonRpc()) ) +} + +const layerMcpProtocolHttp = (options: { + readonly path: HttpRouter.PathInput + readonly allowedOrigins?: ReadonlyArray | undefined +}): Layer.Layer< + RpcServer.Protocol, + never, + McpProtocolState | RpcSerialization.RpcSerialization | HttpRouter.HttpRouter +> => + Layer.effect(RpcServer.Protocol)(Effect.gen(function*() { + const state = yield* McpProtocolState + const { httpEffect, protocol } = yield* RpcServer.makeProtocolWithHttpEffect + const router = yield* HttpRouter.HttpRouter + yield* router.add("POST", options.path, (request) => { + if ( + request.headers.origin !== undefined && + !options.allowedOrigins?.includes(request.headers.origin) + ) { + return Effect.succeed(HttpServerResponse.empty({ status: 403 })) + } + const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase() + if (contentType !== "application/json") { + return Effect.succeed(HttpServerResponse.empty({ status: 415 })) + } + const accepted = new Set() + for (const entry of request.headers.accept?.split(",") ?? []) { + const [mediaType, ...parameters] = entry.split(";").map((part) => part.trim().toLowerCase()) + let quality = 1 + for (const parameter of parameters) { + const [name, value] = parameter.split("=", 2).map((part) => part.trim()) + if (name === "q") { + quality = value === undefined ? Number.NaN : Number(value) + } + } + if (mediaType !== undefined && quality > 0 && quality <= 1) { + accepted.add(mediaType) + } + } + if (!accepted.has("application/json") || !accepted.has("text/event-stream")) { + return Effect.succeed(HttpServerResponse.empty({ status: 406 })) + } + const protocolVersion = request.headers[MCP_PROTOCOL_VERSION_HEADER] + if ( + protocolVersion !== undefined && + !state.protocolRegistry.protocols.some((protocol) => protocol.protocolVersion === protocolVersion) + ) { + return Effect.succeed(HttpServerResponse.empty({ status: 400 })) + } + return request.text.pipe( + Effect.matchEffect({ + onFailure: () => + Effect.succeed( + HttpServerResponse.jsonUnsafe({ + jsonrpc: "2.0", + id: null, + error: new ParseError({ + message: "Parse error" + }) + }) + ), + onSuccess: (body) => { + return Effect.match(Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(body), { + onFailure: () => ({ + _tag: "Error" as const, + id: null, + error: new ParseError({ message: "Parse error" }) + }), + onSuccess: (input) => { + if (Array.isArray(input)) { + const sessionId = request.headers[MCP_SESSION_ID_HEADER] + const session = sessionId === undefined ? undefined : state.sessions.bySessionId.get(sessionId) + let selectedProtocol = session?.protocol ?? state.protocolRegistry.protocols[0] + for (const entry of input) { + if ( + Predicate.hasProperty(entry, "method") && + entry.method === "initialize" && + Predicate.hasProperty(entry, "params") && + Predicate.hasProperty(entry.params, "protocolVersion") && + typeof entry.params.protocolVersion === "string" + ) { + selectedProtocol = state.protocolRegistry.select(entry.params.protocolVersion) + break + } + } + if (!selectedProtocol.transport.acceptsJsonRpcBatches) { + return { _tag: "HttpError" as const, status: 400 } + } + } + const hasId = Predicate.hasProperty(input, "id") + const id = hasId && (typeof input.id === "string" || typeof input.id === "number") + ? input.id + : null + const isJsonRpc = Predicate.hasProperty(input, "jsonrpc") && input.jsonrpc === "2.0" + const hasValidRequestId = hasId === false || typeof input.id === "string" || + typeof input.id === "number" + const isRequest = isJsonRpc && hasValidRequestId && + Predicate.hasProperty(input, "method") && typeof input.method === "string" + const hasValidResponseId = hasId && + (typeof input.id === "string" || typeof input.id === "number" || input.id === null) + const hasResult = Predicate.hasProperty(input, "result") + const hasError = Predicate.hasProperty(input, "error") + const isResponse = isJsonRpc && hasValidResponseId && hasResult !== hasError + const isInitialize = isRequest && input.method === "initialize" + const sessionId = request.headers[MCP_SESSION_ID_HEADER] + const session = sessionId === undefined ? undefined : state.sessions.bySessionId.get(sessionId) + if (isInitialize && sessionId !== undefined) { + return { + _tag: "HttpError" as const, + status: session === undefined ? 404 : 400 + } + } + if (!isInitialize && isRequest && session === undefined) { + return { + _tag: "HttpError" as const, + status: sessionId === undefined ? 400 : 404 + } + } + if ( + session !== undefined && + session.protocol.transport.requiresVersionHeader && + protocolVersion !== session.protocol.protocolVersion + ) { + return { _tag: "HttpError" as const, status: 400 } + } + return isRequest || isResponse + ? { _tag: "Success" as const } + : { + _tag: "Error" as const, + id, + error: new InvalidRequest({ message: "Invalid Request" }) + } + } + }).pipe( + Effect.flatMap((decoded) => + decoded._tag === "HttpError" + ? Effect.succeed(HttpServerResponse.empty({ status: decoded.status })) + : decoded._tag === "Error" + ? Effect.succeed( + HttpServerResponse.jsonUnsafe({ + jsonrpc: "2.0", + id: decoded.id, + error: decoded.error + }) + ) + : httpEffect + ) + ) + } + }) + ) + }) + return protocol + })) + +const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error." + +const toolErrorResult = (message: string): CallToolResult => + new CallToolResult({ + isError: true, + content: [{ type: "text", text: message }] + }) /** * Registers a `Toolkit` with the `McpServer`. * - * @category tools + * @category handlers * @since 4.0.0 */ export const registerToolkit: >( @@ -689,10 +1277,13 @@ export const registerToolkit: >( for (const tool of Object.values(built.tools)) { const annotations = tool.annotations const toolMeta = Context.getOrUndefined(annotations, Tool.Meta) + const isDeclaredFailure = Schema.is(tool.failureSchema) + const outputSchema = Tool.getJsonSchemaFromSchema(tool.successSchema) const mcpTool = new McpTool({ name: tool.name, description: Tool.getDescription(tool), inputSchema: Tool.getJsonSchema(tool), + ...(outputSchema.type === "object" ? { outputSchema } : {}), annotations: { ...(Context.getOption(tool.annotations, Tool.Title).pipe( Option.map((title) => ({ title })), @@ -709,32 +1300,41 @@ export const registerToolkit: >( tool: mcpTool, annotations, handle(payload) { - return built.handle(tool.name as any, payload).pipe( + return built.handle(tool.name as keyof Tools, payload).pipe( Stream.unwrap, Stream.run(Sink.last()), Effect.flatMap(Effect.fromOption), - Effect.provideContext(services as Context.Context), - Effect.matchCause({ - onFailure: (cause) => - new CallToolResult({ - isError: true, - content: [{ - type: "text", - text: Cause.pretty(cause) - }] - }), - onSuccess: (result: any) => - new CallToolResult({ - isError: false, - structuredContent: typeof result.encodedResult === "object" ? result.encodedResult : undefined, - content: [{ - type: "text", - text: JSON.stringify(result.encodedResult) - }] - }) + Effect.provideContext( + services as Context.Context> + ), + Effect.map((result) => + new CallToolResult({ + isError: false, + structuredContent: typeof result.encodedResult === "object" ? result.encodedResult : undefined, + content: result.encodedResult === undefined ? [] : [{ + type: "text", + text: JSON.stringify(result.encodedResult) + }] + }) + ), + Effect.tapCause(Effect.logError), + Effect.catch((error) => { + if (AiError.isAiError(error)) { + const reason = (error as AiError.AiError).reason + return reason._tag === "ToolParameterValidationError" + ? Effect.fail(new InvalidParams({ message: reason.message })) + : Effect.succeed(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)) + } + if (isDeclaredFailure(error)) { + const message = error instanceof Error + ? error.message + : INTERNAL_TOOL_ERROR_MESSAGE + return Effect.succeed(toolErrorResult(message)) + } + return Effect.succeed(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE)) }), - Effect.tapCause(Effect.log) - ) as any + Effect.catchDefect(() => Effect.succeed(toolErrorResult(INTERNAL_TOOL_ERROR_MESSAGE))) + ) } }) } @@ -743,7 +1343,7 @@ export const registerToolkit: >( /** * Registers an `AiToolkit` with the `McpServer`. * - * @category tools + * @category layers * @since 4.0.0 */ export const toolkit = >( @@ -767,7 +1367,11 @@ export const toolkit = >( export type ValidateCompletions = & Completions & { - readonly [K in keyof Completions]: K extends Keys ? (input: string) => any : never + readonly [K in keyof Completions]: K extends Keys ? ( + input: string, + context: CompletionContext + ) => any + : never } /** @@ -786,7 +1390,10 @@ export type ResourceCompletions readonly [ K in Extract as Schemas[K] extends Param ? Id : `param${K}` - ]: (input: string) => Effect.Effect, any, any> + ]: ( + input: string, + context: CompletionContext + ) => Effect.Effect, any, any> } /** @@ -800,7 +1407,7 @@ export type ResourceCompletions * * @see {@link resource} for the layer-based resource registration wrapper * - * @category resources + * @category handlers * @since 4.0.0 */ export const registerResource: { @@ -893,7 +1500,15 @@ export const registerResource: { readonly mimeType?: string | undefined readonly audience?: ReadonlyArray<"user" | "assistant"> | undefined readonly priority?: number | undefined - readonly completion?: Record Effect.Effect> | undefined + readonly completion?: + | Record< + string, + ( + input: string, + context: CompletionContext + ) => Effect.Effect + > + | undefined readonly content: (uri: string, ...params: Array) => Effect.Effect< typeof ReadResourceResult.Type | string | Uint8Array, E, @@ -909,11 +1524,20 @@ export const registerResource: { uriTemplate: uriPath, annotations: options! }) - const completions: Record Effect.Effect> = {} + const completions: Record< + string, + ( + input: string, + context: CompletionContext + ) => Effect.Effect + > = Object.create(null) for (const [param, handle] of Object.entries(options.completion ?? {})) { const encodeArray = Schema.encodeUnknownEffect(Schema.Array(params[param])) - const handler = (input: string) => - handle(input).pipe( + const handler = ( + input: string, + context: CompletionContext + ) => + handle(input, context).pipe( Effect.flatMap(encodeArray), Effect.map((values) => ({ completion: { @@ -962,7 +1586,7 @@ export const registerResource: { * * @see {@link registerResource} for the Effect-level resource registration API * - * @category resources + * @category layers * @since 4.0.0 */ export const resource: { @@ -1035,7 +1659,7 @@ export const resource: { * * @see {@link prompt} for the layer-based prompt registration wrapper * - * @category prompts + * @category handlers * @since 4.0.0 */ export const registerPrompt = < @@ -1043,7 +1667,10 @@ export const registerPrompt = < R, Params extends Schema.Struct.Fields = {}, const Completions extends { - readonly [K in keyof Params]?: (input: string) => Effect.Effect, any, any> + readonly [K in keyof Params]?: ( + input: string, + context: CompletionContext + ) => Effect.Effect, any, any> } = {} >( options: { @@ -1072,18 +1699,30 @@ export const registerPrompt = < const decode = options.parameters ? Schema.decodeEffect(Schema.Struct(props)) : () => Effect.succeed({} as Params) - const completion: Record Effect.Effect> = options.completion ?? {} + const completion: Record< + string, + ( + input: string, + context: CompletionContext + ) => Effect.Effect + > = options.completion ?? {} return Effect.gen(function*() { const registry = yield* McpServer const services = yield* Effect.context, McpServerClient>>() const completions: Record< string, - (input: string) => Effect.Effect - > = {} + ( + input: string, + context: CompletionContext + ) => Effect.Effect + > = Object.create(null) for (const [param, handle] of Object.entries(completion)) { const encodeArray = Schema.encodeEffect(Schema.Array(props[param])) - const handler = (input: string) => - handle(input).pipe( + const handler = ( + input: string, + context: CompletionContext + ) => + handle(input, context).pipe( Effect.flatMap(encodeArray), Effect.map((values) => ({ completion: { @@ -1107,20 +1746,23 @@ export const registerPrompt = < handle: (params) => decode(params).pipe( Effect.mapError((error) => new InvalidParams({ message: error.message })), - Effect.flatMap((params) => options.content(params as any)), - Effect.map((messages) => { - messages = typeof messages === "string" ? - [{ - role: "user", - content: TextContent.make({ text: messages }) - }] : - messages - return new GetPromptResult({ messages, description: prompt.description }) - }), - Effect.catchCause((cause) => { - const prettyError = Cause.prettyErrors(cause)[0] - return Effect.fail(new InternalError({ message: prettyError.message })) - }), + Effect.flatMap((params) => + options.content(params as any).pipe( + Effect.map((messages) => { + messages = typeof messages === "string" ? + [{ + role: "user", + content: TextContent.make({ text: messages }) + }] : + messages + return new GetPromptResult({ messages, description: prompt.description }) + }), + Effect.catchCause((cause) => { + const prettyError = Cause.prettyErrors(cause)[0] + return Effect.fail(new InternalError({ message: prettyError.message })) + }) + ) + ), Effect.provideContext(services as Context.Context) ) }) @@ -1142,7 +1784,7 @@ export const registerPrompt = < * * @see {@link registerPrompt} for the Effect-level prompt registration API * - * @category prompts + * @category layers * @since 4.0.0 */ export const prompt = < @@ -1150,7 +1792,10 @@ export const prompt = < R, Params extends Schema.Struct.Fields = {}, const Completions extends { - readonly [K in keyof Params]?: (input: string) => Effect.Effect, any, any> + readonly [K in keyof Params]?: ( + input: string, + context: CompletionContext + ) => Effect.Effect, any, any> } = {} >( options: { @@ -1177,7 +1822,7 @@ export const prompt = < * Accepted content is decoded with the supplied schema, declined requests fail * with `ElicitationDeclined`, and canceled requests interrupt the effect. * - * @category elicitation + * @category accessors * @since 4.0.0 */ export const elicit: , unknown>>(options: { @@ -1214,7 +1859,7 @@ export const elicit: /** * Accesses the current client's capabilities. * - * @category capabilities + * @category accessors * @since 4.0.0 */ export const clientCapabilities: Effect.Effect< @@ -1244,7 +1889,7 @@ const makeUriMatcher = () => { const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: ReadonlyArray) => { let routerPath = segments[0].replace(":", "::") let uriPath = segments[0] - const params: Record = {} + const params: Record = Object.create(null) let pathSchema = Schema.Tuple([]) as Schema.Top if (schemas.length > 0) { const arr: Array = [] @@ -1274,141 +1919,190 @@ const layerHandlers = (serverInfo: { readonly version: string readonly extensions?: Record<`${string}/${string}`, unknown> | undefined }, options: { - readonly clientSessions: Map + readonly sessions: Sessions + readonly protocolRegistry: McpProtocolRegistry.ProtocolRegistry }) => - ClientRpcs.toLayer( + Layer.effectContext( Effect.gen(function*() { const server = yield* McpServer - let currentLogLevel = yield* CurrentLogLevel - - return ClientRpcs.of({ - // Requests - ping: () => Effect.succeed({}), - initialize(params, { client }) { - const requestedVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(params.protocolVersion) - ? params.protocolVersion - : LATEST_PROTOCOL_VERSION - if (requestedVersion !== params.protocolVersion) { - params = { - ...params, - protocolVersion: requestedVersion + const currentLogLevel = yield* CurrentLogLevel + const contextMap = new Map() + + for (const protocol of options.protocolRegistry.protocols) { + const selectedProtocol = protocol + const wireHandlers = ClientRpcs.of({ + // Requests + ping: () => Effect.succeed({}), + initialize(params, { client }) { + const capabilities: Types.DeepMutable = { + completions: {}, + logging: {} } - } - const capabilities: Types.DeepMutable = { - 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.tools.length > 0) { + capabilities.tools = { listChanged: true } } - } - if (server.prompts.length > 0) { - capabilities.prompts = { listChanged: true } - } - if (serverInfo.extensions) { - capabilities.extensions = serverInfo.extensions as any - } - return Effect.withFiber((fiber) => { - const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) - if (httpRequest) { - const sessionId = crypto.randomUUID() - options.clientSessions.set(sessionId, params) - appendPreResponseHandlerUnsafe(httpRequest, (_req, res) => - Effect.succeed(HttpServerResponse.setHeaders(res, { - [mcpSessionIdHeader]: sessionId, - [mcpProtocolVersionHeader]: requestedVersion - }))) - } else { - options.clientSessions.set(String(client.id), params) + if (server.resources.length > 0 || server.resourceTemplates.length > 0) { + capabilities.resources = { + listChanged: true, + subscribe: true + } } - return Effect.succeed({ - capabilities, - serverInfo, - protocolVersion: requestedVersion - }) - }) - }, - "completion/complete": (r) => - server.completion(r).pipe( - Effect.provideService(CurrentLogLevel, currentLogLevel) - ), - "logging/setLevel": ({ level }) => - Effect.sync(() => { - switch (level) { - case "notice": - case "info": - currentLogLevel = "Info" - break - case "error": - currentLogLevel = "Error" - break - case "debug": - currentLogLevel = "Debug" - break - case "warning": - currentLogLevel = "Warn" - break - case "critical": - case "alert": - case "emergency": - currentLogLevel = "Fatal" - break + if (server.prompts.length > 0) { + capabilities.prompts = { listChanged: true } } - }), - "prompts/get": (r) => - server.getPromptResult(r).pipe( - Effect.provideService(CurrentLogLevel, currentLogLevel) - ), - "prompts/list": (_, { client, headers }) => - Effect.sync(() => { - const initialized = getInitializedClient(options.clientSessions, client.id, headers) - return new ListPromptsResult({ prompts: filterByClient(initialized, server.prompts, "prompt") }) - }), - "resources/list": (_, { client, headers }) => - Effect.sync(() => { - const initialized = getInitializedClient(options.clientSessions, client.id, headers) - return new ListResourcesResult({ resources: filterByClient(initialized, server.resources, "resource") }) - }), - "resources/read": ({ uri }) => - server.findResource(uri).pipe( - Effect.provideService(CurrentLogLevel, currentLogLevel) - ), - "resources/subscribe": () => - InternalError.notImplemented, - "resources/unsubscribe": () => - InternalError.notImplemented, - "resources/templates/list": (_, { client, headers }) => - Effect.sync(() => { - const initialized = getInitializedClient(options.clientSessions, client.id, headers) - return new ListResourceTemplatesResult({ - resourceTemplates: filterByClient(initialized, server.resourceTemplates, "template") - }) - }), - "tools/call": (r) => - server.callTool(r).pipe( - Effect.provideService(CurrentLogLevel, currentLogLevel) - ), - "tools/list": (_, { client, headers }) => - Effect.sync(() => { - const initialized = getInitializedClient(options.clientSessions, client.id, headers) - return new ListToolsResult({ - tools: filterByClient(initialized, server.tools, "tool") + if (serverInfo.extensions) { + capabilities.extensions = serverInfo.extensions as any + } + return Effect.withFiber((fiber) => { + const httpRequest = Context.getOrUndefined(fiber.context, HttpServerRequest.HttpServerRequest) + if (httpRequest !== undefined && capabilities.resources !== undefined) { + capabilities.resources.subscribe = false + } + const session: Session = { + initializePayload: params, + protocol: selectedProtocol, + resourceSubscriptions: capabilities.resources?.subscribe === true ? new Set() : undefined, + logLevel: { _tag: "Effect", level: currentLogLevel } + } + if (httpRequest) { + const sessionId = crypto.randomUUID() + options.sessions.bySessionId.set(sessionId, session) + appendPreResponseHandlerUnsafe(httpRequest, (_req, res) => + Effect.succeed(HttpServerResponse.setHeaders(res, { + [MCP_SESSION_ID_HEADER]: sessionId, + [MCP_PROTOCOL_VERSION_HEADER]: selectedProtocol.protocolVersion + }))) + } else { + options.sessions.byClientId.set(client.id, session) + } + return Effect.succeed({ + capabilities, + serverInfo, + protocolVersion: selectedProtocol.protocolVersion + }) }) - }), + }, + "completion/complete": (r) => + server.completion(r), + "logging/setLevel": ({ level }, { client, headers }) => + Effect.sync(() => { + const session = getClientSession(options.sessions, client.id, headers) + if (session) { + session.logLevel = { _tag: "Mcp", level } + } + return {} + }), + "prompts/get": (r) => + server.getPromptResult(r), + "prompts/list": (_, { client, headers }) => + Effect.sync(() => { + const initialized = getClientSession(options.sessions, client.id, headers)?.initializePayload + return new ListPromptsResult({ prompts: filterByClient(initialized, server.prompts, "prompt") }) + }), + "resources/list": (_, { client, headers }) => + Effect.sync(() => { + const initialized = getClientSession(options.sessions, client.id, headers)?.initializePayload + return new ListResourcesResult({ resources: filterByClient(initialized, server.resources, "resource") }) + }), + "resources/read": ({ uri }) => server.findResource(uri), + "resources/subscribe": ({ uri }, { client, headers }) => + Effect.gen(function*() { + const subscriptions = getClientSession( + options.sessions, + client.id, + headers + )?.resourceSubscriptions + if (subscriptions === undefined) { + return yield* new MethodNotFound({ + message: "Resource subscriptions are not supported" + }) + } + subscriptions.add(uri) + return {} + }), + "resources/unsubscribe": ({ uri }, { client, headers }) => + Effect.gen(function*() { + const subscriptions = getClientSession( + options.sessions, + client.id, + headers + )?.resourceSubscriptions + if (subscriptions === undefined) { + return yield* new MethodNotFound({ + message: "Resource subscriptions are not supported" + }) + } + subscriptions.delete(uri) + return {} + }), + "resources/templates/list": (_, { client, headers }) => + Effect.sync(() => { + const initialized = getClientSession(options.sessions, client.id, headers)?.initializePayload + return new ListResourceTemplatesResult({ + resourceTemplates: filterByClient(initialized, server.resourceTemplates, "template") + }) + }), + "tools/call": (r) => server.callTool(r), + "tools/list": (_, { client, headers }) => + Effect.sync(() => { + const initialized = getClientSession(options.sessions, client.id, headers)?.initializePayload + return new ListToolsResult({ + tools: filterByClient(initialized, server.tools, "tool") + }) + }), - // Notifications - "notifications/cancelled": (_) => Effect.void, - "notifications/initialized": (_) => Effect.void, - "notifications/progress": (_) => Effect.void, - "notifications/roots/list_changed": (_) => Effect.void - }) + // Notifications + "notifications/cancelled": (_) => Effect.void, + "notifications/initialized": (_, { client, headers }) => + Effect.sync(() => { + server.initializedClients.add(client.id) + const session = getClientSession(options.sessions, client.id, headers) + if (session) { + options.sessions.byClientId.set(client.id, session) + } + }), + "notifications/progress": (_) => Effect.void, + "notifications/roots/list_changed": (_) => Effect.void + }) + yield* addProtocolHandlers( + options.protocolRegistry, + selectedProtocol, + selectedProtocol.clientRpcs, + wireHandlers, + contextMap + ) + } + return Context.makeUnsafe(contextMap) }) ) +const addProtocolHandlers = Effect.fnUntraced(function*< + ClientRpcs extends Rpc.Any +>( + registry: McpProtocolRegistry.ProtocolRegistry, + protocol: McpProtocol.ProtocolAdapter, + clientRpcs: RpcGroup.RpcGroup, + handlers: RpcGroup.HandlersFrom, + contextMap: Map +) { + const handlerContext = yield* clientRpcs.toHandlers(handlers) + for (const rpcDefinition of clientRpcs.requests.values()) { + const routed = registry.routeClientRequest(protocol, { + _tag: "Request", + id: 0, + tag: rpcDefinition._tag, + payload: undefined, + headers: [] + }) + const namespacedRpc = registry.clientRpcs.requests.get(routed.tag) + const handler = handlerContext.mapUnsafe.get(rpcDefinition.key) + if (namespacedRpc === undefined || handler === undefined) { + return yield* Effect.die(`MCP handler registration invariant failed for ${routed.tag}`) + } + contextMap.set(namespacedRpc.key, handler) + } +}) + const resolveResourceContent = ( uri: string, content: typeof ReadResourceResult.Type | string | Uint8Array @@ -1455,14 +2149,74 @@ const filterByClient = < return out } -const getInitializedClient = ( - sessions: Map, +const getClientSession = ( + sessions: Sessions, clientId: number, headers: Headers.Headers ) => { - const sessionId = headers[mcpSessionIdHeader] + const sessionId = headers[MCP_SESSION_ID_HEADER] if (sessionId === undefined) { - return sessions.get(String(clientId)) + return sessions.byClientId.get(clientId) } - return sessions.get(sessionId) + return sessions.bySessionId.get(sessionId) +} + +const mcpLogLevels: Record = { + debug: { effect: "Debug", order: 0 }, + info: { effect: "Info", order: 1 }, + notice: { effect: "Info", order: 2 }, + warning: { effect: "Warn", order: 3 }, + error: { effect: "Error", order: 4 }, + critical: { effect: "Fatal", order: 5 }, + alert: { effect: "Fatal", order: 6 }, + emergency: { effect: "Fatal", order: 7 } } + +const effectLogLevel = (logLevel: SessionLogLevel | undefined): LogLevel.LogLevel => + logLevel?._tag === "Mcp" ? mcpLogLevels[logLevel.level].effect : logLevel?.level ?? "Info" + +const isMcpLogLevelEnabled = ( + level: LoggingLevel, + minimum: SessionLogLevel | undefined +): boolean => + minimum?._tag === "Mcp" + ? mcpLogLevels[level].order >= mcpLogLevels[minimum.level].order + : LogLevel.isGreaterThanOrEqualTo(mcpLogLevels[level].effect, minimum?.level ?? "Info") + +const getOfferedProtocolVersion = (payload: unknown): string => + typeof payload === "object" && + payload !== null && + "protocolVersion" in payload && + typeof payload.protocolVersion === "string" + ? payload.protocolVersion + : "" + +const protocolForInternalTag = ( + registry: McpProtocolRegistry.ProtocolRegistry, + tag: string +): McpProtocol.ProtocolAdapter => { + for (const protocol of registry.protocols) { + const routed = registry.routeClientRequest(protocol, { + _tag: "Request", + id: 0, + tag: "", + payload: undefined, + headers: [] + }) + if (tag.startsWith(routed.tag)) { + return protocol + } + } + return registry.protocols[0] +} + +const getProtocolForClient = ( + clientProtocols: Map, + clientId: number, + registry: McpProtocolRegistry.ProtocolRegistry +): McpProtocol.ProtocolAdapter => + clientProtocols.get(clientId) ?? + registry.protocols[0] diff --git a/.context/effect/packages/effect/src/unstable/ai/Model.ts b/.context/effect/packages/effect/src/unstable/ai/Model.ts index 6e78123c6..fb440e762 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Model.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Model.ts @@ -84,7 +84,6 @@ export class ModelName extends Context.Service()( ) {} const Proto = { - ...PipeInspectableProto, [TypeId]: TypeId, ["~effect/Layer"]: { _ROut: identity, @@ -97,6 +96,7 @@ const Proto = { Effect.succeed(Layer.provide(self, Layer.succeedContext(context))) ) }, + ...PipeInspectableProto, toJSON(this: Model): unknown { return { _id: "effect/ai/Model", @@ -110,29 +110,18 @@ const Proto = { * * **Example** (Providing model metadata) * - * ```ts - * import { Effect } from "effect" - * import type { Layer } from "effect" - * import { LanguageModel, Model } from "effect/unstable/ai" - * - * declare const bedrockLayer: Layer.Layer + * ```ts import.meta.vitest + * import { Effect, Layer } from "effect" + * import { Model } from "effect/unstable/ai" * - * // Model automatically provides ProviderName and ModelName services - * const checkProviderAndGenerate = Effect.gen(function*() { + * const model = Model.make("amazon-bedrock", "claude-3-5-haiku", Layer.empty) + * const program = Effect.gen(function*() { * const provider = yield* Model.ProviderName * const modelName = yield* Model.ModelName + * return { provider, modelName } + * }).pipe(Effect.provide(model)) * - * console.log(`Generating with: ${provider}/${modelName}`) - * - * return yield* LanguageModel.generateText({ - * prompt: `Hello from ${provider}!` - * }) - * }) - * - * const program = checkProviderAndGenerate.pipe( - * Effect.provide(Model.make("amazon-bedrock", "claude-3-5-haiku", bedrockLayer)) - * ) - * // Will log: "Generating with: amazon-bedrock/claude-3-5-haiku" + * await Effect.runPromise(program) // => { provider: "amazon-bedrock", modelName: "claude-3-5-haiku" } * ``` * * @category constructors diff --git a/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts b/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts index 54defd5d9..522b0aa88 100644 --- a/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts +++ b/.context/effect/packages/effect/src/unstable/ai/OpenAiStructuredOutput.ts @@ -4,21 +4,15 @@ * OpenAI structured output accepts only a subset of JSON Schema. This module * converts an Effect `Schema.Codec` into a provider-compatible JSON Schema and * a matching codec for decoding the model response back into the original - * application type. When possible, unsupported schema shapes are rewritten into - * supported ones; schema kinds that cannot be represented safely fail during - * conversion. + * application type. Unsupported constraints can be omitted from the provider + * schema and remain enforced by the returned codec. * * @since 4.0.0 */ -import * as Arr from "../../Array.ts" import * as JsonSchema from "../../JsonSchema.ts" -import * as Option from "../../Option.ts" -import * as Predicate from "../../Predicate.ts" import * as Rec from "../../Record.ts" import * as Schema from "../../Schema.ts" -import * as SchemaAST from "../../SchemaAST.ts" -import * as SchemaTransformation from "../../SchemaTransformation.ts" -import * as Tool from "./Tool.ts" +import * as InternalStructuredOutput from "./internal/structured-output.ts" /** * Converts a `Schema.Codec` to OpenAI structured-output JSON Schema and a @@ -27,27 +21,38 @@ import * as Tool from "./Tool.ts" * **When to use** * * Use when you send Effect Schema-backed structured output requests to OpenAI - * and need provider-compatible JSON Schema without losing the decoded - * application type. + * standard models and need provider-compatible JSON Schema without losing the + * decoded application type. * * **Details** * * Returns the JSON Schema to include in the request and the codec to use when - * decoding the model response. If the input schema already fits OpenAI's - * supported JSON Schema subset, the original codec is returned unchanged. + * decoding the model response. The codec remains authoritative: the provider + * JSON Schema can be a lossy, less restrictive representation when OpenAI + * cannot express an Effect Schema constraint. Conversion throws when the + * resulting root is not an object or contains `anyOf`. * * **Gotchas** * * - Some schemas use a provider-safe encoded shape: tuples become objects with - * numeric string keys, records become arrays of `[key, value]` pairs, and - * optional properties become required nullable properties. + * numeric string keys, objects with index signatures become arrays of + * `[key, value]` pairs, and optional properties become required nullable + * properties. * - `oneOf` unions are emitted as `anyOf` unions. - * - Regex patterns from multiple filters are merged into one `pattern` because - * OpenAI structured output does not support `allOf`. - * - Unsupported schema kinds throw during conversion instead of producing a - * lossy schema. + * - Compatible regex patterns are merged because OpenAI structured output does + * not support `allOf`. + * - The root JSON Schema must be an object and cannot use `anyOf`. + * - Constraints inside `allOf` are retained only when they have an explicit, + * semantics-preserving normalization rule. + * - Structural constraints inside `allOf`, such as `properties`, `required`, + * `additionalProperties`, and `items`, are omitted instead of being merged + * with a different meaning. + * - Unsupported constraints are removed from the provider schema and are still + * checked while decoding with the returned codec. + * - Compatibility targets standard OpenAI models. Fine-tuned models support a + * smaller JSON Schema subset. * - * @category Codec Transformation + * @category transforming * @since 4.0.0 */ export function toCodecOpenAI( @@ -56,421 +61,251 @@ export function toCodecOpenAI( codec: Schema.ConstraintCodec jsonSchema: JsonSchema.JsonSchema } { - const to = schema.ast - const from = recurOpenAI(SchemaAST.toEncoded(to)) - const codec = from === to - ? schema - : Schema.make(SchemaAST.decodeTo(from, to, SchemaTransformation.passthrough())) - const document = JsonSchema.resolveTopLevel$ref(Schema.toJsonSchemaDocument(codec)) + const codec = InternalStructuredOutput.toCodec(schema) + const document = JsonSchema.resolveTopLevel$ref( + Schema.toJsonSchemaDocument(codec, { generateDescriptions: true }) + ) const jsonSchema = rewriteOpenAI(document.schema) + if (jsonSchema.type !== "object" || jsonSchema.anyOf !== undefined) { + throw new Error( + `OpenAiStructuredOutput: Root JSON Schema must have type "object" and must not use "anyOf"` + ) + } if (Object.keys(document.definitions).length > 0) { jsonSchema.$defs = Rec.map(document.definitions, rewriteOpenAI) } return { codec, jsonSchema } } -/** - * Post-processes the JSON schema produced by `Schema.toJsonSchemaDocument`, - * recursively flattening `allOf` arrays by merging each member's keys into - * the parent object. This is necessary because OpenAI structured output does - * not support `allOf`. - */ function rewriteOpenAI(schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { - const out: JsonSchema.JsonSchema = {} - for (const [k, v] of Object.entries(schema)) { - if (k === "allOf" && Array.isArray(v)) { - for (const member of v) { - Object.assign(out, rewriteOpenAI(member as JsonSchema.JsonSchema)) + return InternalStructuredOutput.walkJsonSchema(schema, (schema) => { + const normalized = normalizeAllOf(schema) + const out: JsonSchema.JsonSchema = {} + let unsupportedFormat: string | undefined + for (const [key, value] of Object.entries(normalized)) { + if (key === "format") { + if (typeof value === "string" && formats.has(value) && supportsType(key, normalized.type)) out.format = value + else if (typeof value === "string") unsupportedFormat = value + } else if (supportedKeywords.has(key) && supportsType(key, normalized.type)) { + if (key !== "additionalProperties" || value === false) out[key] = value } - } else if (Array.isArray(v)) { - out[k] = v.map((item) => - typeof item === "object" && item !== null && !Array.isArray(item) - ? rewriteOpenAI(item as JsonSchema.JsonSchema) - : item - ) - } else if (typeof v === "object" && v !== null) { - out[k] = rewriteOpenAI(v as JsonSchema.JsonSchema) - } else { - out[k] = v } + if (unsupportedFormat !== undefined) { + InternalStructuredOutput.appendDescription(out, `a value with a format of ${unsupportedFormat}`) + } + if (out.type === "object" && out.properties === undefined && out.additionalProperties === false) { + out.properties = {} + } + return out + }) +} + +function supportsType(key: string, type: unknown): boolean { + if (typeof type !== "string") return true + switch (key) { + case "pattern": + case "format": + return type === "string" + case "multipleOf": + case "minimum": + case "exclusiveMinimum": + case "maximum": + case "exclusiveMaximum": + return type === "number" || type === "integer" + case "items": + case "minItems": + case "maxItems": + return type === "array" + case "properties": + case "patternProperties": + case "required": + case "additionalProperties": + return type === "object" + default: + return true } - if (out.type === "object" && out.properties === undefined && out.additionalProperties === false) { - out.properties = {} +} + +function normalizeAllOf(schema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { + if (!Array.isArray(schema.allOf)) return schema + + const out: JsonSchema.JsonSchema = {} + const patterns: Array = [] + const baseKeywords = new Set() + const memberKeywords = new Set() + const ambiguousKeywords = new Set() + for (const [key, value] of Object.entries(schema)) { + if (key === "allOf") continue + baseKeywords.add(key) + if (key === "pattern" && typeof value === "string") patterns.push(value) + else out[key] = value + } + for (const member of schema.allOf) { + if (!InternalStructuredOutput.isJsonSchema(member)) continue + for (const [key, value] of Object.entries(member)) { + mergeAllOfKeyword(out, key, value, patterns, baseKeywords, memberKeywords, ambiguousKeywords) + } + } + const uniquePatterns = Array.from(new Set(patterns)) + if (uniquePatterns.length === 1) { + out.pattern = uniquePatterns[0] + } else if (uniquePatterns.length > 1) { + const combined = uniquePatterns.map((source) => `(?=[\\s\\S]*?(?:${source}))`).join("") + out.pattern = `^${combined}` } return out } -function recurOpenAI(ast: SchemaAST.AST): SchemaAST.AST { - switch (ast._tag) { - case "Declaration": - case "Void": - case "Never": - case "Unknown": - case "Any": - case "BigInt": - case "Symbol": - case "UniqueSymbol": - case "ObjectKeyword": - case "Enum": - case "TemplateLiteral": - return unsupportedAst( - ast, - "OpenAI structured output does not support this schema kind; consider transforming the schema or using a different provider" - ) - case "Undefined": - return unsupportedAst( - ast, - "OpenAI structured output does not support undefined; consider transforming the schema or using a different provider; if using `Schema.optional`, consider using `Schema.optionalKey` instead" - ) - case "Null": - return ast - case "String": { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.String(annotations, filters) - } - return ast - } - case "Number": { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.Number(annotations, filters) - } - return ast - } - case "Boolean": - return ast - case "Literal": { - const literal = ast.literal - if (typeof literal === "string" || typeof literal === "number" || typeof literal === "boolean") { - const { annotations, filters } = get(ast) - if (annotations !== undefined || filters !== undefined) { - return new SchemaAST.Literal(ast.literal, annotations, filters) - } - return ast - } - throw new Error( - `${errorPrefix}: Unsupported literal type ${typeof literal} (value: ${ - String(literal) - }) (supported: string | number | boolean)` - ) - } - case "Union": { - if (ast.mode === "oneOf") { - return new SchemaAST.Union(ast.types, "anyOf", ast.annotations, ast.checks) +function mergeAllOfKeyword( + out: JsonSchema.JsonSchema, + key: string, + value: unknown, + patterns: Array, + baseKeywords: ReadonlySet, + memberKeywords: Set, + ambiguousKeywords: Set +): void { + switch (key) { + case "description": + if (typeof value === "string") InternalStructuredOutput.appendDescription(out, value) + return + case "pattern": + if (typeof value === "string") patterns.push(value) + return + case "minimum": + case "exclusiveMinimum": + mergeLowerBound(out, key, value) + return + case "maximum": + case "exclusiveMaximum": + mergeUpperBound(out, key, value) + return + case "minItems": + mergeMinimum(out, key, value) + return + case "maxItems": + mergeMaximum(out, key, value) + return + default: + if (!normalizableAllOfKeywords.has(key) || baseKeywords.has(key) || ambiguousKeywords.has(key)) return + if (!memberKeywords.has(key)) { + out[key] = value + memberKeywords.add(key) + } else if (out[key] !== value) { + delete out[key] + memberKeywords.delete(key) + ambiguousKeywords.add(key) } - const types = SchemaAST.mapOrSame(ast.types, recurOpenAI) - const { annotations, filters } = get(ast) - if (types !== ast.types || annotations !== undefined || filters !== undefined) { - return new SchemaAST.Union(types, "anyOf", annotations, filters) - } - return ast - } - case "Arrays": { - if (ast.rest.length > 1) { - throw new Error( - `${errorPrefix}: Post-rest elements are not supported for arrays (rest length: ${ast.rest.length})` - ) - } - let { annotations, filters } = get(ast) - if (ast.elements.length > 0) { - // tuples are not supported by OpenAI, we translate them to objects with string keys - if (annotations !== undefined && typeof annotations.description === "string") { - annotations.description = `${TUPLE_DESCRIPTION}; ${annotations.description}` - } else { - annotations ??= {} - annotations.description = TUPLE_DESCRIPTION - } - const propertySignatures = ast.elements.map((e, i) => { - return new SchemaAST.PropertySignature(String(i), e) - }) - if (ast.rest.length === 1) { - propertySignatures.push( - new SchemaAST.PropertySignature(REST_PROPERTY_NAME, new SchemaAST.Arrays(false, [], ast.rest)) - ) - } - return SchemaAST.decodeTo( - recurOpenAI(new SchemaAST.Objects(propertySignatures, [], annotations, filters)), - ast, - SchemaTransformation.transform({ - decode: (o) => { - let t: Array = [] - for (let i = 0; i < ast.elements.length; i++) { - const k = String(i) - if (o[k] !== undefined) { - t.push(o[k]) - } - } - if (REST_PROPERTY_NAME in o) { - t = [...t, ...o[REST_PROPERTY_NAME]] - } - return t - }, - encode: (t) => { - const o: Record = {} - for (let i = 0; i < ast.elements.length; i++) { - if (t.length >= i) { - o[String(i)] = t[i] - } - } - if (ast.rest.length === 1) { - o[REST_PROPERTY_NAME] = t.length >= ast.elements.length ? t.slice(ast.elements.length) : [] - } - return o - } - }) - ) - } else { - const rest = SchemaAST.mapOrSame(ast.rest, recurOpenAI) - if (rest !== ast.rest || annotations !== undefined || filters !== undefined) { - return new SchemaAST.Arrays(false, [], rest, annotations, filters) - } - return ast - } - } - case "Objects": { - let { annotations, filters } = get(ast) - if (ast.indexSignatures.length === 0) { - const propertySignatures = SchemaAST.mapOrSame(ast.propertySignatures, (ps) => { - if (typeof ps.name !== "string") { - throw new Error( - `${errorPrefix}: Property names must be strings (got ${typeof ps.name})` - ) - } - let type = recurOpenAI(ps.type) - // optional properties are not supported by OpenAI, so we translate them to nullable unions - if (SchemaAST.isOptional(ps.type)) { - type = SchemaAST.decodeTo( - new SchemaAST.Union([type, SchemaAST.null], "anyOf"), - SchemaAST.optionalKey(type), - SchemaTransformation.transformOptional({ - decode: Option.filter(Predicate.isNotNull), - encode: Option.orElseSome(() => null) - }) - ) - } - if (type === ps.type) { - return ps - } - return new SchemaAST.PropertySignature(ps.name, type) - }) - if ( - propertySignatures !== ast.propertySignatures || annotations !== undefined || filters !== undefined - ) { - return new SchemaAST.Objects(propertySignatures, [], annotations, filters) - } - } else if (ast.indexSignatures.length === 1 && ast.propertySignatures.length === 0) { - const is = ast.indexSignatures[0] - if (Tool.isEmptyParamsRecord(is)) { - return ast - } - // records are not supported by OpenAI, so we translate them to arrays of key-value pairs - if (annotations !== undefined && typeof annotations.description === "string") { - annotations.description = `${RECORD_DESCRIPTION}; ${annotations.description}` - } else { - annotations ??= {} - annotations.description = RECORD_DESCRIPTION - } - return SchemaAST.decodeTo( - recurOpenAI( - new SchemaAST.Arrays(false, [], [new SchemaAST.Arrays(false, [is.parameter, is.type], [])], annotations) - ), - ast, - SchemaTransformation.transform({ - decode: Object.fromEntries, - encode: Object.entries - }) - ) - } else { - throw new Error( - `${errorPrefix}: unsupported object schema shape (properties: ${ast.propertySignatures.length}, indexSignatures: ${ast.indexSignatures.length}). Supported: plain objects (properties only) or records (single index signature, no properties)` - ) - } - return ast - } - case "Suspend": { - const cached = cache.get(ast) - if (cached) return cached - const { annotations } = get(ast) - const out = new SchemaAST.Suspend(() => recurOpenAI(ast.thunk()), annotations) - cache.set(ast, out) - return out - } + return } } -const cache = new Map() - -const errorPrefix = "OpenAiStructuredOutput" - -function unsupportedAst(ast: SchemaAST.AST, details?: string): never { - const base = `Unsupported AST ${ast._tag}` - const full = `${errorPrefix}: ${base}` - throw new Error(details !== undefined ? `${full} (${details})` : full) +function mergeMinimum(schema: JsonSchema.JsonSchema, key: string, value: unknown): void { + if (typeof value !== "number") return + const current = schema[key] + if (typeof current !== "number" || value > current) schema[key] = value } -const REST_PROPERTY_NAME = "__rest__" - -const RECORD_DESCRIPTION = - "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object" - -const TUPLE_DESCRIPTION = - "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements" - -type Annotation = - | { readonly _tag: "description"; readonly description: string } - | { readonly _tag: "format"; readonly format: string } - -type Filter = - | Annotation - | { readonly _tag: "filter"; readonly filter: SchemaAST.Filter } - | { readonly _tag: "regex"; readonly source: string } +function mergeMaximum(schema: JsonSchema.JsonSchema, key: string, value: unknown): void { + if (typeof value !== "number") return + const current = schema[key] + if (typeof current !== "number" || value < current) schema[key] = value +} -const get = (ast: SchemaAST.AST): { - annotations: Record | undefined - filters: [SchemaAST.Check, ...SchemaAST.Check[]] | undefined -} => { - const annotations: Record = {} - const filters: Array> = [] - const regexSources: Array = [] - const checks = getChecks(ast, SchemaAST.isArrays(ast)) - if (checks.length > 0) { - for (const check of checks) { - switch (check._tag) { - case "description": { - if (annotations.description !== undefined) { - annotations.description += ` and ${check.description}` - } else { - annotations.description = check.description - } - break - } - case "format": { - annotations.format = check.format - break - } - case "filter": { - filters.push(check.filter) - break - } - case "regex": { - regexSources.push(check.source) - break - } - } - } +function mergeLowerBound(schema: JsonSchema.JsonSchema, key: string, value: unknown): void { + if (typeof value !== "number") { + if (!Object.hasOwn(schema, key)) schema[key] = value + return } - // OpenAI does not support allOf, so we merge multiple regex patterns into a single isPattern filter - if (regexSources.length === 1) { - filters.push(SchemaAST.isPattern(new RegExp(regexSources[0]))) - } else if (regexSources.length > 1) { - const combined = regexSources.map((s) => `(?=[\\s\\S]*?(?:${s}))`).join("") - filters.push(SchemaAST.isPattern(new RegExp(`^${combined}`))) + const minimum = typeof schema.minimum === "number" ? schema.minimum : undefined + const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : undefined + let current: { readonly value: number; readonly exclusive: boolean } | undefined + if (minimum !== undefined) current = { value: minimum, exclusive: false } + if ( + exclusiveMinimum !== undefined && + (current === undefined || exclusiveMinimum >= current.value) + ) { + current = { value: exclusiveMinimum, exclusive: true } } - return { - annotations: Object.keys(annotations).length > 0 ? annotations : undefined, - filters: Arr.isArrayNonEmpty(filters) ? filters : undefined + const candidate = { value, exclusive: key === "exclusiveMinimum" } + if ( + current === undefined || + candidate.value > current.value || + (candidate.value === current.value && candidate.exclusive) + ) { + current = candidate } + delete schema.minimum + delete schema.exclusiveMinimum + schema[current.exclusive ? "exclusiveMinimum" : "minimum"] = current.value } -const getChecks = (ast: SchemaAST.AST, isArray: boolean): Array => [ - ...(ast.checks !== undefined ? getFilters(ast.checks, isArray) : []), - ...getAnnotations(ast.annotations) -] - -const getAnnotations = (annotations: Schema.Annotations.Filter | undefined): Array => { - const out: Array = [] - if (annotations !== undefined) { - const description = annotations?.description - ?? (annotations.meta?._tag === "isInt" || annotations.meta?._tag === "isFinite" - ? undefined - : annotations?.expected) - if (typeof description === "string") { - out.push({ _tag: "description", description }) - } - const format = annotations?.format - if (typeof format === "string") { - if (formats.includes(format)) { - out.push({ _tag: "format", format }) - } else { - out.push({ _tag: "description", description: `a value with a format of ${format}` }) - } - } +function mergeUpperBound(schema: JsonSchema.JsonSchema, key: string, value: unknown): void { + if (typeof value !== "number") { + if (!Object.hasOwn(schema, key)) schema[key] = value + return } - return out -} - -function getFilter(filter: SchemaAST.Filter, isArray: boolean): Array { - let out: Array = [] - const annotations = getAnnotations(filter.annotations) - const meta = filter.annotations?.meta - if (meta !== undefined) { - switch (meta._tag) { - case "isMinLength": - case "isMaxLength": - case "isLengthBetween": { - out = out.concat(annotations) - if (isArray) { - out.push({ _tag: "filter", filter: resetFilter(filter) }) - } - break - } - case "isInt": - case "isFinite": - case "isGreaterThan": - case "isGreaterThanOrEqualTo": - case "isLessThan": - case "isLessThanOrEqualTo": - case "isBetween": - case "isMultipleOf": { - out = out.concat(annotations) - out.push({ _tag: "filter", filter: resetFilter(filter) }) - break - } - default: { - out = out.concat(annotations) - break - } - } - if ("regExp" in meta && meta.regExp instanceof RegExp) { - out.push({ _tag: "regex", source: meta.regExp.source }) - } + const maximum = typeof schema.maximum === "number" ? schema.maximum : undefined + const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : undefined + let current: { readonly value: number; readonly exclusive: boolean } | undefined + if (maximum !== undefined) current = { value: maximum, exclusive: false } + if ( + exclusiveMaximum !== undefined && + (current === undefined || exclusiveMaximum <= current.value) + ) { + current = { value: exclusiveMaximum, exclusive: true } } - return out + const candidate = { value, exclusive: key === "exclusiveMaximum" } + if ( + current === undefined || + candidate.value < current.value || + (candidate.value === current.value && candidate.exclusive) + ) { + current = candidate + } + delete schema.maximum + delete schema.exclusiveMaximum + schema[current.exclusive ? "exclusiveMaximum" : "maximum"] = current.value } -function resetFilter(filter: SchemaAST.Filter): SchemaAST.Filter { - return filter.annotate({ - description: undefined, - expected: undefined, - title: undefined, - format: undefined - }) -} +const supportedKeywords = new Set([ + "$ref", + "type", + "title", + "description", + "enum", + "anyOf", + "properties", + "required", + "additionalProperties", + "items", + "pattern", + "multipleOf", + "minimum", + "exclusiveMinimum", + "maximum", + "exclusiveMaximum", + "minItems", + "maxItems" +]) -function getFilters( - checks: readonly [SchemaAST.Check, ...SchemaAST.Check[]], - isArray: boolean -): Array { - return checks.flatMap((check) => { - switch (check._tag) { - case "Filter": - return getFilter(check, isArray) - case "FilterGroup": - return getFilters(check.checks, isArray) - } - }) -} +const normalizableAllOfKeywords = new Set([ + "$ref", + "title", + "enum", + "anyOf", + "format", + "multipleOf" +]) -const formats = [ +const formats = new Set([ "date-time", "time", "date", "duration", "email", "hostname", - "uri", "ipv4", "ipv6", "uuid" -] +]) diff --git a/.context/effect/packages/effect/src/unstable/ai/Prompt.ts b/.context/effect/packages/effect/src/unstable/ai/Prompt.ts index 45855c3c1..7680780fb 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Prompt.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Prompt.ts @@ -12,7 +12,6 @@ import * as Arr from "../../Array.ts" import * as Effect from "../../Effect.ts" import { dual } from "../../Function.ts" -import * as Option from "../../Option.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" import * as Schema from "../../Schema.ts" @@ -153,7 +152,7 @@ const BasePart = Schema.Struct({ * * **Example** (Creating content parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const textPart = Prompt.makePart("text", { @@ -165,6 +164,8 @@ const BasePart = Schema.Struct({ * fileName: "screenshot.png", * data: new Uint8Array([1, 2, 3]) * }) + * + * const result = [textPart.type, filePart.type] // => ["text", "file"] * ``` * * @category constructors @@ -219,12 +220,13 @@ export type PartConstructorParams

= Omit "Hello, how can I help you today?" * ``` * * @category models @@ -301,13 +303,14 @@ export const textPart = (params: PartConstructorParams): TextPart => m * * **Example** (Creating reasoning parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const reasoningPart: Prompt.ReasoningPart = Prompt.makePart("reasoning", { * text: * "Summary: the response compares the requested options by price and availability." * }) + * reasoningPart.type // => "reasoning" * ``` * * @category models @@ -387,7 +390,7 @@ export const reasoningPart = (params: PartConstructorParams): Rea * * **Example** (Creating file parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const imagePart: Prompt.FilePart = Prompt.makePart("file", { @@ -401,6 +404,8 @@ export const reasoningPart = (params: PartConstructorParams): Rea * fileName: "report.pdf", * data: new Uint8Array([1, 2, 3]) * }) + * + * const result = [imagePart.mediaType, documentPart.fileName] // => ["image/jpeg", "report.pdf"] * ``` * * @category models @@ -501,7 +506,7 @@ export const filePart = (params: PartConstructorParams): FilePart => m * * **Example** (Creating tool call parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const toolCallPart: Prompt.ToolCallPart = Prompt.makePart("tool-call", { @@ -510,6 +515,7 @@ export const filePart = (params: PartConstructorParams): FilePart => m * params: { city: "San Francisco", units: "celsius" }, * providerExecuted: false * }) + * toolCallPart.name // => "get_weather" * ``` * * @category models @@ -614,7 +620,7 @@ export const toolCallPart = (params: PartConstructorParams): ToolC * * **Example** (Creating tool result parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const toolResultPart: Prompt.ToolResultPart = Prompt.makePart("tool-result", { @@ -625,8 +631,10 @@ export const toolCallPart = (params: PartConstructorParams): ToolC * temperature: 22, * condition: "sunny", * humidity: 65 - * } + * }, + * providerExecuted: false * }) + * const result = [toolResultPart.name, toolResultPart.isFailure] // => ["get_weather", false] * ``` * * @category models @@ -649,6 +657,10 @@ export interface ToolResultPart extends BasePart<"tool-result", ToolResultPartOp * 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 } /** @@ -674,6 +686,10 @@ export interface ToolResultPartEncoded extends BasePartEncoded<"tool-result", To * 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 | undefined } /** @@ -697,6 +713,7 @@ export const ToolResultPart: Schema.Struct<{ readonly name: Schema.String readonly isFailure: Schema.Boolean readonly result: Schema.Unknown + readonly providerExecuted: Schema.withDecodingDefault readonly "~effect/ai/Prompt/Part": Schema.withDecodingDefaultKey> readonly options: Schema.withDecodingDefault< Schema.$Record< @@ -710,7 +727,8 @@ export const ToolResultPart: Schema.Struct<{ id: Schema.String, name: Schema.String, isFailure: Schema.Boolean, - result: Schema.Unknown + result: Schema.Unknown, + providerExecuted: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))) }).annotate({ identifier: "ToolResultPart" }) /** @@ -736,7 +754,7 @@ export const toolResultPart = (params: PartConstructorParams): T * * **Example** (Creating tool approval responses) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const approvalResponse: Prompt.ToolApprovalResponsePart = Prompt.makePart( @@ -755,6 +773,8 @@ export const toolResultPart = (params: PartConstructorParams): T * reason: "Operation not allowed" * } * ) + * + * const result = [approvalResponse.approved, denialResponse.approved] // => [true, false] * ``` * * @category models @@ -858,7 +878,7 @@ export const toolApprovalResponsePart = ( * * **Example** (Creating tool approval requests) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const approvalRequest: Prompt.ToolApprovalRequestPart = Prompt.makePart( @@ -868,6 +888,7 @@ export const toolApprovalResponsePart = ( * toolCallId: "call_456" * } * ) + * const result = [approvalRequest.approvalId, approvalRequest.toolCallId] // => ["approval_123", "call_456"] * ``` * * @category models @@ -946,6 +967,32 @@ export const toolApprovalRequestPart = ( params: PartConstructorParams ): ToolApprovalRequestPart => makePart("tool-approval-request", params as any) +/** + * Schema for validation and encoding of content parts. + * + * @category schemas + * @since 4.0.0 + */ +export const Part: Schema.Union< + readonly [ + typeof TextPart, + typeof ReasoningPart, + typeof FilePart, + typeof ToolCallPart, + typeof ToolResultPart, + typeof ToolApprovalResponsePart, + typeof ToolApprovalRequestPart + ] +> = Schema.Union([ + TextPart, + ReasoningPart, + FilePart, + ToolCallPart, + ToolResultPart, + ToolApprovalResponsePart, + ToolApprovalRequestPart +]) + // ============================================================================= // Base Message // ============================================================================= @@ -1012,7 +1059,7 @@ const BaseMessage = Schema.Struct({ * * **Example** (Creating messages) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const textPart = Prompt.makePart("text", { @@ -1022,6 +1069,7 @@ const BaseMessage = Schema.Struct({ * const userMessage = Prompt.makeMessage("user", { * content: [textPart] * }) + * const result = [userMessage.role, userMessage.content.length] // => ["user", 1] * ``` * * @category constructors @@ -1096,13 +1144,14 @@ export const ContentFromString: Schema.decodeTo< * * **Example** (Creating system messages) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const systemMessage: Prompt.SystemMessage = Prompt.makeMessage("system", { * content: "You are a helpful assistant specialized in mathematics. " + * "Always show your work step by step." * }) + * systemMessage.role // => "system" * ``` * * @category models @@ -1177,7 +1226,7 @@ export const systemMessage = (params: MessageConstructorParams): * * **Example** (Creating user messages) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const textUserMessage: Prompt.UserMessage = Prompt.makeMessage("user", { @@ -1200,6 +1249,8 @@ export const systemMessage = (params: MessageConstructorParams): * }) * ] * }) + * + * const result = [textUserMessage.content.length, multimodalUserMessage.content.length] // => [1, 2] * ``` * * @category models @@ -1241,6 +1292,17 @@ export interface UserMessageEncoded extends BaseMessageEncoded<"user", UserMessa */ export type UserMessagePartEncoded = TextPartEncoded | FilePartEncoded +/** + * Schema for validation and encoding of user message content parts. + * + * @category schemas + * @since 4.0.0 + */ +export const UserMessagePart: Schema.Union = Schema.Union([ + TextPart, + FilePart +]) + /** * Represents provider-specific options that can be associated with a * `UserMessage` through module augmentation. @@ -1345,7 +1407,7 @@ export const userMessage = (params: MessageConstructorParams): User * * **Example** (Creating assistant messages) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const assistantMessage: Prompt.AssistantMessage = Prompt.makeMessage( @@ -1369,7 +1431,8 @@ export const userMessage = (params: MessageConstructorParams): User * result: { * temperature: 72, * condition: "sunny" - * } + * }, + * providerExecuted: true * }), * Prompt.makePart("text", { * text: "The weather in San Francisco is currently 72°F and sunny." @@ -1377,6 +1440,7 @@ export const userMessage = (params: MessageConstructorParams): User * ] * } * ) + * assistantMessage.content.map((part) => part.type) // => ["text", "tool-call", "tool-result", "text"] * ``` * * @category models @@ -1427,6 +1491,30 @@ export type AssistantMessagePartEncoded = | ToolResultPartEncoded | ToolApprovalRequestPartEncoded +/** + * Schema for validation and encoding of assistant message content parts. + * + * @category schemas + * @since 4.0.0 + */ +export const AssistantMessagePart: Schema.Union< + readonly [ + typeof TextPart, + typeof FilePart, + typeof ReasoningPart, + typeof ToolCallPart, + typeof ToolResultPart, + typeof ToolApprovalRequestPart + ] +> = Schema.Union([ + TextPart, + FilePart, + ReasoningPart, + ToolCallPart, + ToolResultPart, + ToolApprovalRequestPart +]) + /** * Represents provider-specific options that can be associated with a * `AssistantMessage` through module augmentation. @@ -1536,7 +1624,7 @@ export const assistantMessage = (params: MessageConstructorParams ["tool", "tool-result"] * ``` * * @category models @@ -1596,6 +1686,19 @@ export interface ToolMessageEncoded extends BaseMessageEncoded<"tool", ToolMessa */ export type ToolMessagePartEncoded = ToolResultPartEncoded | ToolApprovalResponsePartEncoded +/** + * Schema for validation and encoding of tool message content parts. + * + * @category schemas + * @since 4.0.0 + */ +export const ToolMessagePart: Schema.Union< + readonly [typeof ToolResultPart, typeof ToolApprovalResponsePart] +> = Schema.Union([ + ToolResultPart, + ToolApprovalResponsePart +]) + /** * Represents provider-specific options that can be associated with a * `ToolMessage` through module augmentation. @@ -1737,22 +1840,30 @@ export const Prompt: Schema.Codec = Schema.Struct({ Schema.decodeTo( $Prompt, SchemaTransformation.transformOrFail({ - decode: (input) => + decode: (input, options) => Effect.mapBothEager( SchemaParser.decodeEffect(Schema.Array(Message))(input.content), { onSuccess: makePrompt, onFailure: () => - new SchemaIssue.InvalidValue(Option.some(input.content), { message: "Invalid Prompt messages" }) + new SchemaIssue.InvalidValue( + { message: "Invalid Prompt messages" }, + input.content, + options + ) } ), - encode: (prompt) => + encode: (prompt, options) => Effect.mapBothEager( SchemaParser.encodeEffect(Schema.Array(Message))(prompt.content), { onSuccess: (messages) => ({ content: messages }), onFailure: () => - new SchemaIssue.InvalidValue(Option.some(prompt.content), { message: "Invalid Prompt messages" }) + new SchemaIssue.InvalidValue( + { message: "Invalid Prompt messages" }, + prompt.content, + options + ) } ) }) @@ -1765,8 +1876,8 @@ export const Prompt: Schema.Codec = Schema.Struct({ * * **Example** (Accepting raw prompt input) * - * ```ts - * import type { Prompt } from "effect/unstable/ai" + * ```ts import.meta.vitest + * import { Prompt } from "effect/unstable/ai" * * // String input - creates a user message * const stringInput: Prompt.RawInput = "Hello, world!" @@ -1777,9 +1888,9 @@ export const Prompt: Schema.Codec = Schema.Struct({ * { role: "user", content: [{ type: "text", text: "Hi!" }] } * ] * - * // Existing prompt - * declare const existingPrompt: Prompt.Prompt - * const promptInput: Prompt.RawInput = existingPrompt + * const promptInput: Prompt.RawInput = Prompt.empty + * + * const result = [typeof stringInput, Array.isArray(messagesInput), promptInput.content.length] // => ["string", true, 0] * ``` * * @category models @@ -1809,11 +1920,11 @@ const decodeMessagesSync = Schema.decodeSync(Schema.Array(Message)) * * **Example** (Creating an empty prompt) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const emptyPrompt = Prompt.empty - * console.log(emptyPrompt.content) // [] + * emptyPrompt.content // => [] * ``` * * @category constructors @@ -1831,7 +1942,7 @@ export const empty: Prompt = makePrompt([]) * * **Example** (Creating prompts from inputs) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * // From string - creates a user message @@ -1843,9 +1954,9 @@ export const empty: Prompt = makePrompt([]) * { role: "user", content: [{ type: "text", text: "Hi!" }] } * ]) * - * // From existing prompt - * declare const existingPrompt: Prompt.Prompt - * const copiedPrompt = Prompt.make(existingPrompt) + * const copiedPrompt = Prompt.make(Prompt.empty) + * + * const result = [textPrompt.content[0].role, structuredPrompt.content.length, copiedPrompt.content.length] // => ["user", 2, 0] * ``` * * @category constructors @@ -1872,7 +1983,7 @@ export const make = (input: RawInput): Prompt => { * * **Example** (Creating prompts from messages) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const messages: ReadonlyArray = [ @@ -1885,6 +1996,7 @@ export const make = (input: RawInput): Prompt => { * ] * * const prompt = Prompt.fromMessages(messages) + * prompt.content.length // => 2 * ``` * * @category constructors @@ -1892,15 +2004,27 @@ export const make = (input: RawInput): Prompt => { */ export const fromMessages = (messages: ReadonlyArray): Prompt => makePrompt(messages) +const mergeOptions = (left: ProviderOptions, right: ProviderOptions): ProviderOptions => { + const result: Record = { ...left } + for (const [provider, metadata] of Object.entries(right)) { + const previous = result[provider] + result[provider] = Predicate.isObject(previous) && Predicate.isObject(metadata) + ? Object.assign({}, previous, metadata) + : metadata + } + return result +} + /** * Creates a `Prompt` from response parts by folding completed text and - * reasoning streams into assistant parts, placing tool calls and approval - * requests in an assistant message, and placing non-preliminary tool results - * in a tool message using their encoded results. + * reasoning streams into assistant parts, preserving provider metadata as + * prompt options, placing tool calls and approval requests in an assistant + * message, and placing non-preliminary tool results in a tool message using + * their encoded results. * * **Example** (Creating prompts from response parts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt, Response } from "effect/unstable/ai" * * const responseParts: ReadonlyArray = [ @@ -1926,6 +2050,7 @@ export const fromMessages = (messages: ReadonlyArray): Prompt => makePr * * const prompt = Prompt.fromResponseParts(responseParts) * // Creates an assistant message with the response content + * prompt.content.map((message) => message.role) // => ["assistant", "tool"] * ``` * * @category constructors @@ -1939,55 +2064,63 @@ export const fromResponseParts = (parts: ReadonlyArray): Promp const assistantParts: Array = [] const toolParts: Array = [] - const activeTextDeltas = new Map() - const activeReasoningDeltas = new Map() + 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 })) + assistantParts.push(makePart("text", { text: part.text, options: part.metadata })) break } // Text Parts (streaming) case "text-start": { - activeTextDeltas.set(part.id, { text: "" }) + activeTextDeltas.set(part.id, { text: "", options: part.metadata }) break } case "text-delta": { if (activeTextDeltas.has(part.id)) { - activeTextDeltas.get(part.id)!.text += part.delta + const active = activeTextDeltas.get(part.id)! + active.text += part.delta + active.options = mergeOptions(active.options, part.metadata) } break } case "text-end": { if (activeTextDeltas.has(part.id)) { - assistantParts.push(makePart("text", activeTextDeltas.get(part.id)!)) + const active = activeTextDeltas.get(part.id)! + active.options = mergeOptions(active.options, part.metadata) + assistantParts.push(makePart("text", active)) } break } // Reasoning Parts case "reasoning": { - assistantParts.push(makePart("reasoning", { text: part.text })) + assistantParts.push(makePart("reasoning", { text: part.text, options: part.metadata })) break } // Reasoning Parts (streaming) case "reasoning-start": { - activeReasoningDeltas.set(part.id, { text: "" }) + activeReasoningDeltas.set(part.id, { text: "", options: part.metadata }) break } case "reasoning-delta": { if (activeReasoningDeltas.has(part.id)) { - activeReasoningDeltas.get(part.id)!.text += part.delta + const active = activeReasoningDeltas.get(part.id)! + active.text += part.delta + active.options = mergeOptions(active.options, part.metadata) } break } case "reasoning-end": { if (activeReasoningDeltas.has(part.id)) { - assistantParts.push(makePart("reasoning", activeReasoningDeltas.get(part.id)!)) + const active = activeReasoningDeltas.get(part.id)! + active.options = mergeOptions(active.options, part.metadata) + assistantParts.push(makePart("reasoning", active)) } break } @@ -1998,7 +2131,8 @@ export const fromResponseParts = (parts: ReadonlyArray): Promp id: part.id, name: part.name, params: part.params, - providerExecuted: part.providerExecuted ?? false + providerExecuted: part.providerExecuted ?? false, + options: part.metadata })) break } @@ -2006,11 +2140,14 @@ export const fromResponseParts = (parts: ReadonlyArray): Promp // Tool Result Parts (skip preliminary results) case "tool-result": { if (part.preliminary !== true) { - toolParts.push(makePart("tool-result", { + const target = part.providerExecuted === true ? assistantParts : toolParts + target.push(makePart("tool-result", { id: part.id, name: part.name, isFailure: part.isFailure, - result: part.encodedResult + result: part.encodedResult, + providerExecuted: part.providerExecuted ?? false, + options: part.metadata })) } break @@ -2058,7 +2195,7 @@ export const fromResponseParts = (parts: ReadonlyArray): Promp * * **Example** (Concatenating prompts) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const systemPrompt = Prompt.make([{ @@ -2067,6 +2204,7 @@ export const fromResponseParts = (parts: ReadonlyArray): Promp * }]) * * const merged = Prompt.concat(systemPrompt, "Hello, world!") + * merged.content.map((message) => message.role) // => ["system", "user"] * ``` * * @category combinators @@ -2101,7 +2239,7 @@ export const concat: { * * **Example** (Replacing system instructions) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const systemPrompt = Prompt.make([{ @@ -2117,6 +2255,7 @@ export const concat: { * prompt, * "You are an expert in programming" * ) + * replaced.content[0].content // => "You are an expert in programming" * ``` * * @category combinators @@ -2143,7 +2282,7 @@ export const setSystem: { * * **Example** (Prepending system instructions) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const systemPrompt = Prompt.make([{ @@ -2160,6 +2299,7 @@ export const setSystem: { * "You are a helpful assistant. " * ) * // result content: "You are a helpful assistant. You are an expert in programming." + * replaced.content[0].content // => "You are a helpful assistant. You are an expert in programming." * ``` * * @category combinators @@ -2192,7 +2332,7 @@ export const prependSystem: { * * **Example** (Appending system instructions) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/ai" * * const systemPrompt = Prompt.make([{ @@ -2209,6 +2349,7 @@ export const prependSystem: { * " You are a helpful assistant." * ) * // result content: "You are an expert in programming. You are a helpful assistant." + * replaced.content[0].content // => "You are an expert in programming. You are a helpful assistant." * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/unstable/ai/Response.ts b/.context/effect/packages/effect/src/unstable/ai/Response.ts index c14811ffa..fab6fa0ce 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Response.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Response.ts @@ -155,7 +155,7 @@ export type AllPartsEncoded = * * **Example** (Building a response parts schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Response, Tool, Toolkit } from "effect/unstable/ai" * @@ -167,6 +167,7 @@ export type AllPartsEncoded = * ) * * const allPartsSchema = Response.AllParts(myToolkit) + * Schema.isSchema(allPartsSchema) // => true * ``` * * @category schemas @@ -493,7 +494,7 @@ const BasePart = Schema.Struct({ * * **Example** (Creating response content parts) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const textPart = Response.makePart("text", { @@ -506,6 +507,8 @@ const BasePart = Schema.Struct({ * params: { city: "San Francisco" }, * providerExecuted: false * }) + * + * const result = [textPart.type, toolCallPart.name] // => ["text", "get_weather"] * ``` * * @category constructors @@ -557,12 +560,13 @@ export type ConstructorParams = * * **Example** (Creating a text part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const textPart: Response.TextPart = Response.makePart("text", { * text: "The answer to your question is 42." * }) + * textPart.text // => "The answer to your question is 42." * ``` * * @category models @@ -813,13 +817,14 @@ export const TextEndPart: Schema.Struct<{ * * **Example** (Creating a reasoning part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const reasoningPart: Response.ReasoningPart = Response.makePart("reasoning", { * text: * "Let me think step by step: First I need to analyze the user's question..." * }) + * reasoningPart.type // => "reasoning" * ``` * * @category models @@ -1295,7 +1300,7 @@ export const ToolParamsEndPart: Schema.Struct<{ * * **Example** (Creating a tool call part) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Response } from "effect/unstable/ai" * @@ -1316,6 +1321,7 @@ export const ToolParamsEndPart: Schema.Struct<{ * params: { city: "San Francisco", units: "celsius" }, * providerExecuted: false * }) + * const result = [toolCallPart.name, toolCallPart.params.city] // => ["get_weather", "San Francisco"] * ``` * * @category models @@ -1503,7 +1509,7 @@ export interface ToolResultFailure extends BaseToo * * **Example** (Creating a tool result part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * interface WeatherData { @@ -1533,6 +1539,7 @@ export interface ToolResultFailure extends BaseToo * providerExecuted: false, * preliminary: false * }) + * const result = [toolResultPart.name, toolResultPart.result.temperature] // => ["get_weather", 22] * ``` * * @category models @@ -1723,7 +1730,7 @@ export const toolResultPart = ["approval_123", "call_456"] * ``` * * @category models @@ -1824,13 +1832,14 @@ export const toolApprovalRequestPart = ( * * **Example** (Creating a file part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const imagePart: Response.FilePart = Response.makePart("file", { * mediaType: "image/jpeg", * data: new Uint8Array([1, 2, 3]) * }) + * const result = [imagePart.mediaType, imagePart.data] // => ["image/jpeg", new Uint8Array([1, 2, 3])] * ``` * * @category models @@ -2116,7 +2125,7 @@ export const UrlSourcePart: Schema.Struct<{ * * **Example** (Describing an HTTP request) * - * ```ts + * ```ts import.meta.vitest * import type { Response } from "effect/unstable/ai" * * const requestDetails: typeof Response.HttpRequestDetails.Type = { @@ -2126,6 +2135,7 @@ export const UrlSourcePart: Schema.Struct<{ * hash: undefined, * headers: { "Content-Type": "application/json" } * } + * const result = [requestDetails.method, requestDetails.urlParams] // => ["POST", []] * ``` * * @category schemas @@ -2135,7 +2145,7 @@ export const HttpRequestDetails = Schema.Struct({ method: Schema.Literals(["GET", "POST", "PATCH", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE"]), url: Schema.String, urlParams: Schema.Array(Schema.Tuple([Schema.String, Schema.String])), - hash: Schema.UndefinedOr(Schema.String), + hash: Schema.optional(Schema.String), headers: Schema.Record( Schema.String, Schema.Union([ @@ -2156,7 +2166,7 @@ export const HttpRequestDetails = Schema.Struct({ * * **Example** (Describing an HTTP response) * - * ```ts + * ```ts import.meta.vitest * import type { Response } from "effect/unstable/ai" * * const responseDetails: typeof Response.HttpResponseDetails.Type = { @@ -2166,13 +2176,14 @@ export const HttpRequestDetails = Schema.Struct({ * "X-Request-Id": "req_abc123" * } * } + * const result = [responseDetails.status, responseDetails.headers["X-Request-Id"]] // => [200, "req_abc123"] * ``` * * @category schemas * @since 4.0.0 */ export const HttpResponseDetails = Schema.Struct({ - status: Schema.Number, + status: Schema.Int, headers: Schema.Record( Schema.String, Schema.Union([ @@ -2191,7 +2202,7 @@ export const HttpResponseDetails = Schema.Struct({ * * **Example** (Creating a metadata part) * - * ```ts + * ```ts import.meta.vitest * import { DateTime } from "effect" * import { Response } from "effect/unstable/ai" * @@ -2200,10 +2211,11 @@ export const HttpResponseDetails = Schema.Struct({ * { * id: "resp_123", * modelId: "gpt-4", - * timestamp: DateTime.nowUnsafe(), + * timestamp: DateTime.makeUnsafe("2024-01-01T00:00:00Z"), * request: undefined * } * ) + * const result = [metadataPart.id, metadataPart.modelId] // => ["resp_123", "gpt-4"] * ``` * * @category models @@ -2213,19 +2225,19 @@ export interface ResponseMetadataPart extends BasePart<"response-metadata", Resp /** * Optional unique identifier for this specific response. */ - readonly id: string | undefined + readonly id?: string | undefined /** * Optional identifier of the AI model that generated the response. */ - readonly modelId: string | undefined + readonly modelId?: string | undefined /** * Optional timestamp when the response was generated. */ - readonly timestamp: DateTime.Utc | undefined + readonly timestamp?: DateTime.Utc | undefined /** * Optional HTTP request details for the request made to the AI provider. */ - readonly request: typeof HttpRequestDetails.Type | undefined + readonly request?: typeof HttpRequestDetails.Type | undefined } /** @@ -2272,10 +2284,10 @@ export interface ResponseMetadataPartMetadata extends ProviderMetadata {} */ export const ResponseMetadataPart: Schema.Struct<{ readonly type: Schema.tag<"response-metadata"> - readonly id: Schema.UndefinedOr - readonly modelId: Schema.UndefinedOr - readonly timestamp: Schema.UndefinedOr - readonly request: Schema.UndefinedOr + readonly id: Schema.optional + readonly modelId: Schema.optional + readonly timestamp: Schema.optional + readonly request: Schema.optional readonly "~effect/ai/Content/Part": Schema.withDecodingDefaultKey> readonly metadata: Schema.withDecodingDefault< Schema.$Record> @@ -2283,10 +2295,10 @@ export const ResponseMetadataPart: Schema.Struct<{ }> = Schema.Struct({ ...BasePart.fields, type: Schema.tag("response-metadata"), - id: Schema.UndefinedOr(Schema.String), - modelId: Schema.UndefinedOr(Schema.String), - timestamp: Schema.UndefinedOr(Schema.DateTimeUtcFromString), - request: Schema.UndefinedOr(HttpRequestDetails) + id: Schema.optional(Schema.String), + modelId: Schema.optional(Schema.String), + timestamp: Schema.optional(Schema.DateTimeUtcFromString), + request: Schema.optional(HttpRequestDetails) }).annotate({ identifier: "ResponseMetadataPart" }) satisfies Schema.Codec< ResponseMetadataPart, ResponseMetadataPartEncoded @@ -2368,19 +2380,19 @@ export class Usage extends Schema.Class("effect/ai/AiResponse/Usage")({ /** * The number of non-cached input (i.e. prompt) tokens used. */ - uncached: Schema.UndefinedOr(Schema.Number), + uncached: Schema.optional(Schema.Int), /** * The total of number of input (i.e. prompt) tokens used. */ - total: Schema.UndefinedOr(Schema.Number), + total: Schema.optional(Schema.Int), /** * The number of cached input (i.e. prompt) tokens read. */ - cacheRead: Schema.UndefinedOr(Schema.Number), + cacheRead: Schema.optional(Schema.Int), /** * The number of cached input (i.e. prompt) tokens written. */ - cacheWrite: Schema.UndefinedOr(Schema.Number) + cacheWrite: Schema.optional(Schema.Int) }), /** * Information about the output (i.e. response) tokens used. @@ -2389,15 +2401,15 @@ export class Usage extends Schema.Class("effect/ai/AiResponse/Usage")({ /** * The total of number of output (i.e. response) tokens used. */ - total: Schema.UndefinedOr(Schema.Number), + total: Schema.optional(Schema.Int), /** * The number of text tokens used. */ - text: Schema.UndefinedOr(Schema.Number), + text: Schema.optional(Schema.Int), /** * The number of reasoning tokens used. */ - reasoning: Schema.UndefinedOr(Schema.Number) + reasoning: Schema.optional(Schema.Int) }) }) {} @@ -2406,7 +2418,7 @@ export class Usage extends Schema.Class("effect/ai/AiResponse/Usage")({ * * **Example** (Creating a finish part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const finishPart: Response.FinishPart = Response.makePart("finish", { @@ -2426,6 +2438,7 @@ export class Usage extends Schema.Class("effect/ai/AiResponse/Usage")({ * }), * response: undefined * }) + * const result = [finishPart.reason, finishPart.usage.inputTokens.total] // => ["stop", 50] * ``` * * @category models @@ -2443,7 +2456,7 @@ export interface FinishPart extends BasePart<"finish", FinishPartMetadata> { /** * Optional HTTP response details from the AI provider. */ - readonly response: typeof HttpResponseDetails.Type | undefined + readonly response?: typeof HttpResponseDetails.Type | undefined } /** @@ -2500,7 +2513,7 @@ export const FinishPart: Schema.Struct<{ "unknown" ]> readonly usage: typeof Usage - readonly response: Schema.UndefinedOr + readonly response: Schema.optional readonly "~effect/ai/Content/Part": Schema.withDecodingDefaultKey> readonly metadata: Schema.withDecodingDefault< Schema.$Record> @@ -2510,7 +2523,7 @@ export const FinishPart: Schema.Struct<{ type: Schema.tag("finish"), reason: FinishReason, usage: Usage, - response: Schema.UndefinedOr(HttpResponseDetails) + response: Schema.optional(HttpResponseDetails) }).annotate({ identifier: "FinishPart" }) satisfies Schema.Codec // ============================================================================= @@ -2522,12 +2535,13 @@ export const FinishPart: Schema.Struct<{ * * **Example** (Creating an error part) * - * ```ts + * ```ts import.meta.vitest * import { Response } from "effect/unstable/ai" * * const errorPart: Response.ErrorPart = Response.makePart("error", { * error: new Error("boom") * }) + * const result = [errorPart.type, errorPart.error instanceof Error] // => ["error", true] * ``` * * @category models diff --git a/.context/effect/packages/effect/src/unstable/ai/Telemetry.ts b/.context/effect/packages/effect/src/unstable/ai/Telemetry.ts index 2b8b31cd5..a7aed63c1 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Telemetry.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Telemetry.ts @@ -229,7 +229,7 @@ export type WellKnownSystem = * * **Example** (Prefixing telemetry attributes) * - * ```ts + * ```ts import.meta.vitest * import type { Telemetry } from "effect/unstable/ai" * * type RequestAttrs = { @@ -245,6 +245,11 @@ export type WellKnownSystem = * // "gen_ai.request.model_name": string * // "gen_ai.request.max_tokens": number * // } + * const attributes: PrefixedAttrs = { + * "gen_ai.request.model_name": "gpt-4", + * "gen_ai.request.max_tokens": 1000 + * } + * Object.keys(attributes) // => ["gen_ai.request.model_name", "gen_ai.request.max_tokens"] * ``` * * @category utility types @@ -264,12 +269,19 @@ export type AttributesWithPrefix, Prefix * * **Example** (Formatting attribute names) * - * ```ts + * ```ts import.meta.vitest * import type { Telemetry } from "effect/unstable/ai" * * type Formatted1 = Telemetry.FormatAttributeName<"modelName"> // "model_name" * type Formatted2 = Telemetry.FormatAttributeName<"maxTokens"> // "max_tokens" * type Formatted3 = Telemetry.FormatAttributeName<"temperature"> // "temperature" + * + * const formatted: [Formatted1, Formatted2, Formatted3] = [ + * "model_name", + * "max_tokens", + * "temperature" + * ] + * formatted // => ["model_name", "max_tokens", "temperature"] * ``` * * @category utility types @@ -292,9 +304,8 @@ export type FormatAttributeName = T extends * * **Example** (Adding prefixed span attributes) * - * ```ts - * import { String } from "effect" - * import type { Tracer } from "effect" + * ```ts import.meta.vitest + * import { Context, Option, String, Tracer } from "effect" * import { Telemetry } from "effect/unstable/ai" * * const addCustomAttributes = Telemetry.addSpanAttributes( @@ -302,13 +313,22 @@ export type FormatAttributeName = T extends * String.camelToSnake * ) * - * // Usage with a span - * declare const span: Tracer.Span + * const span = new Tracer.NativeSpan({ + * name: "request", + * parent: Option.none(), + * annotations: Context.empty(), + * links: [], + * startTime: 0n, + * kind: "internal", + * sampled: true + * }) + * * addCustomAttributes(span, { * modelName: "gpt-4", * maxTokens: 1000 * }) - * // Results in attributes: "custom.ai.model_name" and "custom.ai.max_tokens" + * + * Array.from(span.attributes.keys()) // => ["custom.ai.model_name", "custom.ai.max_tokens"] * ``` * * @category annotations @@ -358,7 +378,7 @@ const addSpanUsageAttributes = addSpanAttributes("gen_ai.usage", String.camelToS * * **Example** (Configuring GenAI telemetry attributes) * - * ```ts + * ```ts import.meta.vitest * import type { Telemetry } from "effect/unstable/ai" * * const telemetryOptions: Telemetry.GenAITelemetryAttributeOptions = { @@ -381,6 +401,8 @@ const addSpanUsageAttributes = addSpanAttributes("gen_ai.usage", String.camelToS * outputTokens: 25 * } * } + * + * const result = [telemetryOptions.system, telemetryOptions.usage?.inputTokens] // => ["openai", 50] * ``` * * @category options @@ -428,7 +450,7 @@ export type GenAITelemetryAttributeOptions = BaseAttributes & { * * **Example** (Adding GenAI telemetry annotations) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Telemetry } from "effect/unstable/ai" * @@ -440,7 +462,10 @@ export type GenAITelemetryAttributeOptions = BaseAttributes & { * request: { model: "gpt-4", temperature: 0.7 }, * usage: { inputTokens: 100, outputTokens: 50 } * }) + * return (span as { attributes: ReadonlyMap }).attributes.size * }) + * + * await Effect.runPromise(Effect.withSpan(directUsage, "example")) // => 5 * ``` * * @category annotations @@ -468,7 +493,7 @@ export const addGenAIAnnotations: { * * **Example** (Transforming AI spans) * - * ```ts + * ```ts import.meta.vitest * import type { Telemetry } from "effect/unstable/ai" * * const customTransformer: Telemetry.SpanTransformer = ({ response, span }) => { @@ -480,6 +505,8 @@ export const addGenAIAnnotations: { * ) * span.attribute("total_text_length", totalTextLength) * } + * + * typeof customTransformer // => "function" * ``` * * @category models diff --git a/.context/effect/packages/effect/src/unstable/ai/Tokenizer.ts b/.context/effect/packages/effect/src/unstable/ai/Tokenizer.ts index 4764ce539..a919d9b17 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Tokenizer.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Tokenizer.ts @@ -31,7 +31,7 @@ import * as Prompt from "./Prompt.ts" * * **Example** (Accessing the Tokenizer service) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Tokenizer } from "effect/unstable/ai" * @@ -40,6 +40,12 @@ import * as Prompt from "./Prompt.ts" * const tokens = yield* tokenizer.tokenize("Hello, world!") * return tokens.length * }) + * + * const tokenizer = Tokenizer.make({ + * tokenize: (prompt) => Effect.succeed(prompt.content.map((_, index) => index)) + * }) + * const result = useTokenizer.pipe(Effect.provideService(Tokenizer.Tokenizer, tokenizer)) + * await Effect.runPromise(result) // => 1 * ``` * * @category services @@ -60,7 +66,7 @@ export class Tokenizer extends Context.Service()( * * **Example** (Implementing a custom tokenizer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Prompt } from "effect/unstable/ai" * import type { Tokenizer } from "effect/unstable/ai" @@ -71,6 +77,9 @@ export class Tokenizer extends Context.Service()( * truncate: (input, maxTokens) => * Effect.succeed(Prompt.make(input.toString().slice(0, maxTokens * 5))) * } + * + * const tokenCount = (await Effect.runPromise(customTokenizer.tokenize("one two three"))).length // => 3 + * const messageCount = (await Effect.runPromise(customTokenizer.truncate("hello world", 1))).content.length // => 1 * ``` * * @category models @@ -112,7 +121,7 @@ export interface Service { * * **Example** (Creating a word tokenizer) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { Tokenizer } from "effect/unstable/ai" * @@ -131,6 +140,8 @@ export interface Service { * .map((_, index) => index) * ) * }) + * + * await Effect.runPromise(wordTokenizer.tokenize("hello effect world")) // => [0, 1, 2] * ``` * * @category constructors diff --git a/.context/effect/packages/effect/src/unstable/ai/Tool.ts b/.context/effect/packages/effect/src/unstable/ai/Tool.ts index 775e9a136..39aa8f824 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Tool.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Tool.ts @@ -171,7 +171,7 @@ export type NeedsApproval = * * **Example** (Defining a weather lookup tool) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -188,6 +188,7 @@ export type NeedsApproval = * humidity: Schema.Number * }) * }) + * const result = [GetWeather.name, GetWeather.failureMode] // => ["GetWeather", "error"] * ``` * * @category models @@ -273,6 +274,13 @@ export interface Tool< */ readonly needsApproval?: boolean | NeedsApprovalFunction | undefined + /** + * Set whether user approval is required before executing this tool. + */ + setNeedsApproval( + needsApproval: NeedsApproval + ): Tool + /** * Adds a _request-level_ dependency which must be provided before the tool * call handler can be executed. @@ -358,7 +366,7 @@ export interface Tool< * * **Example** (Defining a provider-defined web search tool) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -377,7 +385,8 @@ export interface Tool< * snippet: Schema.String * })) * }) - * }) + * })({ query: "Effect" }) + * const result = [WebSearch.name, WebSearch.providerName] // => ["OpenAiWebSearch", "web_search"] * ``` * * @category models @@ -450,7 +459,7 @@ export interface ProviderDefined< * * **Example** (Defining dynamic tools) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -473,6 +482,8 @@ export interface ProviderDefined< * required: ["query"] * } * }) + * + * const result = [Calculator.name, McpTool.name] // => ["Calculator", "McpTool"] * ``` * * @category models @@ -517,7 +528,7 @@ export interface Dynamic< * * **Example** (Checking for user-defined tools) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -547,8 +558,7 @@ export interface Dynamic< * }) * }) * - * console.log(Tool.isUserDefined(UserDefinedTool)) // true - * console.log(Tool.isUserDefined(ProviderDefinedTool)) // false + * const result = [Tool.isUserDefined(UserDefinedTool), Tool.isUserDefined(ProviderDefinedTool)] // => [true, false] * ``` * * @category guards @@ -562,7 +572,7 @@ export const isUserDefined = (u: unknown): u is Tool => * * **Example** (Checking for provider-defined tools) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -592,8 +602,7 @@ export const isUserDefined = (u: unknown): u is Tool => * }) * }) * - * console.log(Tool.isProviderDefined(UserDefinedTool)) // false - * console.log(Tool.isProviderDefined(ProviderDefinedTool)) // true + * const result = [Tool.isProviderDefined(UserDefinedTool), Tool.isProviderDefined(ProviderDefinedTool)] // => [false, false] * ``` * * @category guards @@ -608,7 +617,7 @@ export const isProviderDefined = ( * * **Example** (Checking for dynamic tools) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -621,8 +630,7 @@ export const isProviderDefined = ( * success: Schema.Number * }) * - * console.log(Tool.isDynamic(DynamicTool)) // true - * console.log(Tool.isDynamic(UserDefinedTool)) // false + * const result = [Tool.isDynamic(DynamicTool), Tool.isDynamic(UserDefinedTool)] // => [true, false] * ``` * * @category guards @@ -1051,6 +1059,9 @@ const Proto = { setFailure(this: Any, failureSchema: Schema.Constraint) { return clone(this, { failureSchema }) }, + setNeedsApproval(this: Any, needsApproval: NeedsApproval) { + return clone(this, { needsApproval }) + }, annotate(this: Any, tag: Context.Key, value: S) { return clone(this, { annotations: Context.add(this.annotations, tag, value) }) }, @@ -1175,7 +1186,7 @@ const dynamicProto = < * * **Example** (Creating a tool without parameters) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -1184,6 +1195,7 @@ const dynamicProto = < * description: "Returns the current timestamp", * success: Schema.Number * }) + * GetCurrentTime.name // => "GetCurrentTime" * ``` * * @category constructors @@ -1281,7 +1293,7 @@ export const make = < * * **Example** (Creating a dynamic tool) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -1304,6 +1316,8 @@ export const make = < * required: ["query"] * } * }) + * + * const result = [Calculator.name, McpTool.name] // => ["Calculator", "McpTool"] * ``` * * @category constructors @@ -1379,7 +1393,7 @@ export const dynamic: { * * **Example** (Creating a provider-defined tool) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -1398,7 +1412,8 @@ export const dynamic: { * content: Schema.String * })) * }) - * }) + * })({ query: "Effect" }) + * const result = [WebSearch.name, WebSearch.providerName] // => ["OpenAiWebSearch", "web_search"] * ``` * * @category constructors @@ -1576,7 +1591,7 @@ export class NameMapper> { * * **Example** (Reading a tool description) * - * ```ts + * ```ts import.meta.vitest * import { Tool } from "effect/unstable/ai" * * const myTool = Tool.make("example", { @@ -1584,7 +1599,7 @@ export class NameMapper> { * }) * * const description = Tool.getDescription(myTool) - * console.log(description) // "This is an example tool" + * description // => "This is an example tool" * ``` * * @category getters @@ -1615,7 +1630,7 @@ export const getDescription = (tool: Tool): string | undefined * * **Example** (Generating a tool JSON schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Tool } from "effect/unstable/ai" * @@ -1627,15 +1642,10 @@ export const getDescription = (tool: Tool): string | undefined * }) * * const jsonSchema = Tool.getJsonSchema(weatherTool) - * console.log(jsonSchema) - * // { - * // type: "object", - * // properties: { - * // location: { type: "string" }, - * // units: { type: "string", enum: ["celsius", "fahrenheit"] } - * // }, - * // required: ["location", "units"] - * // } + * jsonSchema.type // => "object" + * if (typeof jsonSchema.properties === "object" && jsonSchema.properties !== null) { + * Object.keys(jsonSchema.properties) // => ["location", "units"] + * } * ``` * * @category getters @@ -1666,10 +1676,18 @@ export const getJsonSchema = (tool: Tool, options?: { export const getJsonSchemaFromSchema = (schema: S, options?: { readonly transformer?: CodecTransformer }): JsonSchema.JsonSchema => { + return getJsonSchemaFromSchemaWith(schema, Schema.toJsonSchemaDocument, options) +} + +const getJsonSchemaFromSchemaWith = ( + schema: S, + toJsonSchemaDocument: (schema: Schema.Constraint) => JsonSchema.Document<"draft-2020-12">, + options?: { readonly transformer?: CodecTransformer } +): JsonSchema.JsonSchema => { if (Predicate.isNotUndefined(options?.transformer)) { return options.transformer(schema).jsonSchema } - const document = Schema.toJsonSchemaDocument(schema) + const document = toJsonSchemaDocument(schema) if (Object.keys(document.definitions).length > 0) { document.schema.$defs = document.definitions } @@ -1685,14 +1703,16 @@ export const getJsonSchemaFromSchema = (schema: S, * * **Example** (Annotating a tool title) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const myTool = Tool.make("calculate_tip") * .annotate(Tool.Title, "Tip Calculator") + * Context.getUnsafe(myTool.annotations, Tool.Title) // => "Tip Calculator" * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export class Title extends Context.Service()("effect/ai/Tool/Title") {} @@ -1702,14 +1722,16 @@ export class Title extends Context.Service()("effect/ai/Tool/Titl * * **Example** (Annotating MCP metadata) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const myCalculatorUi = Tool.make("calculator_ui", {}) * .annotate(Tool.Meta, { ui: { resourceUri: "ui://example/calculator-ui" } }) + * "ui" in Context.getUnsafe(myCalculatorUi.annotations, Tool.Meta) // => true * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export class Meta extends Context.Service>()("effect/ai/Tool/Meta") {} @@ -1724,14 +1746,16 @@ export class Meta extends Context.Service>()("effe * * **Example** (Marking a tool as read-only) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const readOnlyTool = Tool.make("get_user_info") * .annotate(Tool.Readonly, true) + * Context.get(readOnlyTool.annotations, Tool.Readonly) // => true * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export const Readonly = Context.Reference("effect/ai/Tool/Readonly", { @@ -1748,14 +1772,16 @@ export const Readonly = Context.Reference("effect/ai/Tool/Readonly", { * * **Example** (Marking a tool as non-destructive) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const safeTool = Tool.make("search_database") * .annotate(Tool.Destructive, false) + * Context.get(safeTool.annotations, Tool.Destructive) // => false * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export const Destructive = Context.Reference("effect/ai/Tool/Destructive", { @@ -1773,14 +1799,16 @@ export const Destructive = Context.Reference("effect/ai/Tool/Destructiv * * **Example** (Marking a tool as idempotent) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const idempotentTool = Tool.make("get_current_time") * .annotate(Tool.Idempotent, true) + * Context.get(idempotentTool.annotations, Tool.Idempotent) // => true * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export const Idempotent = Context.Reference("effect/ai/Tool/Idempotent", { @@ -1798,14 +1826,16 @@ export const Idempotent = Context.Reference("effect/ai/Tool/Idempotent" * * **Example** (Disabling open-world access) * - * ```ts + * ```ts import.meta.vitest + * import { Context } from "effect" * import { Tool } from "effect/unstable/ai" * * const restrictedTool = Tool.make("internal_operation") * .annotate(Tool.OpenWorld, false) + * Context.get(restrictedTool.annotations, Tool.OpenWorld) // => false * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export const OpenWorld = Context.Reference("effect/ai/Tool/OpenWorld", { @@ -1827,14 +1857,15 @@ export const OpenWorld = Context.Reference("effect/ai/Tool/OpenWorld", * * **Example** (Disabling strict JSON schema mode) * - * ```ts + * ```ts import.meta.vitest * import { Tool } from "effect/unstable/ai" * * const flexibleTool = Tool.make("search") * .annotate(Tool.Strict, false) + * Tool.getStrictMode(flexibleTool) // => false * ``` * - * @category annotations + * @category services * @since 4.0.0 */ export const Strict = Context.Reference("effect/ai/Tool/Strict", { @@ -1915,13 +1946,13 @@ function filter(obj: any) { next = [] for (const node of nodes) { - if (Object.prototype.hasOwnProperty.call(node, "__proto__")) { + if (Object.hasOwn(node, "__proto__")) { throw new SyntaxError("Object contains forbidden prototype property") } if ( - Object.prototype.hasOwnProperty.call(node, "constructor") && - Object.prototype.hasOwnProperty.call(node.constructor, "prototype") + Object.hasOwn(node, "constructor") && + Object.hasOwn(node.constructor, "prototype") ) { throw new SyntaxError("Object contains forbidden prototype property") } diff --git a/.context/effect/packages/effect/src/unstable/ai/Toolkit.ts b/.context/effect/packages/effect/src/unstable/ai/Toolkit.ts index af3be989f..1cedb040c 100644 --- a/.context/effect/packages/effect/src/unstable/ai/Toolkit.ts +++ b/.context/effect/packages/effect/src/unstable/ai/Toolkit.ts @@ -15,6 +15,7 @@ import * as Effect from "../../Effect.ts" import * as Effectable from "../../Effectable.ts" import * as Fiber from "../../Fiber.ts" import { identity } from "../../Function.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Layer from "../../Layer.ts" import * as Predicate from "../../Predicate.ts" import * as Queue from "../../Queue.ts" @@ -32,8 +33,8 @@ const TypeId = "~effect/ai/Toolkit" as const * * **Example** (Defining AI toolkits) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schema } from "effect" * import { Tool, Toolkit } from "effect/unstable/ai" * * const SearchDocs = Tool.make("SearchDocs", { @@ -50,8 +51,12 @@ const TypeId = "~effect/ai/Toolkit" as const * * const AiToolkit = Toolkit.make(SearchDocs, SummarizeText) * - * console.log(Object.keys(AiToolkit.tools)) - * // ["SearchDocs", "SummarizeText"] + * const ready = AiToolkit.pipe(Effect.provide(AiToolkit.toLayer({ + * SearchDocs: ({ query }) => Effect.succeed([query]), + * SummarizeText: ({ text }) => Effect.succeed(text) + * }))) + * + * Object.keys((await Effect.runPromise(ready)).tools) // => ["SearchDocs", "SummarizeText"] * ``` * * @category models @@ -105,6 +110,10 @@ export interface Toolkit> extends * @since 4.0.0 */ export interface HandlerContext { + /** + * The unique identifier of the tool call, when available. + */ + readonly toolCallId?: string | undefined /** * Emit a preliminary result during long-running tool calls. * @@ -199,7 +208,11 @@ export interface WithHandler> { /** * Parameters to pass to the tool handler. */ - params: Tool.Parameters + params: Tool.Parameters, + /** + * The unique identifier of the tool call. + */ + toolCallId?: string ) => Effect.Effect< Stream.Stream< Tool.HandlerResult, @@ -257,8 +270,8 @@ const Proto = { return schemas } - const handle = Effect.fnUntraced(function*(name: string, params: unknown) { - const tool = tools[name] + const handle = Effect.fnUntraced(function*(name: string, params: unknown, toolCallId?: string) { + const tool = Object.hasOwn(tools, name) ? tools[name] : undefined yield* Effect.annotateCurrentSpan({ tool: name, @@ -302,6 +315,7 @@ const Proto = { readonly preliminary: boolean }, Cause.Done>() const context: HandlerContext = { + toolCallId, preliminary: (result) => Effect.asVoid(Queue.offer(queue, { result, @@ -390,8 +404,10 @@ const Proto = { const handlers = Effect.isEffect(build) ? yield* build : build const context = new Map() for (const [name, handler] of Object.entries(handlers)) { - const tool = this.tools[name]! - context.set(tool.id, { name, handler, context: services }) + const tool = Object.hasOwn(this.tools, name) ? this.tools[name] : undefined + if (tool !== undefined) { + context.set(tool.id, { name, handler, context: services }) + } } return Context.makeUnsafe(context) }) @@ -418,7 +434,7 @@ const resolveInput = >( ): Record => { const output = {} as Record for (const tool of tools) { - output[tool.name] = tool + InternalRecord.assignProperty(output, tool.name, tool) } return output } @@ -447,8 +463,8 @@ export const empty: Toolkit<{}> = makeProto({}) * * **Example** (Creating a toolkit) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schema } from "effect" * import { Tool, Toolkit } from "effect/unstable/ai" * * const GetCurrentTime = Tool.make("GetCurrentTime", { @@ -466,6 +482,12 @@ export const empty: Toolkit<{}> = makeProto({}) * }) * * const toolkit = Toolkit.make(GetCurrentTime, GetWeather) + * const ready = toolkit.pipe(Effect.provide(toolkit.toLayer({ + * GetCurrentTime: () => Effect.succeed(0), + * get_weather: () => Effect.succeed({ temperature: 20, condition: "clear" }) + * }))) + * + * Object.keys((await Effect.runPromise(ready)).tools) // => ["GetCurrentTime", "get_weather"] * ``` * * @category constructors @@ -518,8 +540,8 @@ export type MergedTools> = SimplifyRecord< * * **Example** (Merging toolkits) * - * ```ts - * import { Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, Schema } from "effect" * import { Tool, Toolkit } from "effect/unstable/ai" * * const mathToolkit = Toolkit.make( @@ -533,6 +555,14 @@ export type MergedTools> = SimplifyRecord< * ) * * const combined = Toolkit.merge(mathToolkit, utilityToolkit) + * const ready = combined.pipe(Effect.provide(combined.toLayer({ + * add: () => Effect.succeed(1), + * subtract: () => Effect.succeed(0), + * get_time: () => Effect.succeed(0), + * get_weather: () => Effect.succeed("clear") + * }))) + * + * Object.keys((await Effect.runPromise(ready)).tools) // => ["add", "subtract", "get_time", "get_weather"] * ``` * * @category constructors @@ -547,7 +577,7 @@ export const merge = >( const tools = {} as Record for (const toolkit of toolkits) { for (const [name, tool] of Object.entries(toolkit.tools)) { - tools[name] = tool + InternalRecord.assignProperty(tools, name, tool) } } return makeProto(tools) as any diff --git a/.context/effect/packages/effect/src/unstable/ai/index.ts b/.context/effect/packages/effect/src/unstable/ai/index.ts index abea583e3..0c99d8153 100644 --- a/.context/effect/packages/effect/src/unstable/ai/index.ts +++ b/.context/effect/packages/effect/src/unstable/ai/index.ts @@ -34,6 +34,11 @@ export * as IdGenerator from "./IdGenerator.ts" */ export * as LanguageModel from "./LanguageModel.ts" +/** + * @since 4.0.0 + */ +export * as McpProtocol from "./McpProtocol.ts" + /** * @since 4.0.0 */ diff --git a/.context/effect/packages/effect/src/unstable/ai/internal/codec-transformer.ts b/.context/effect/packages/effect/src/unstable/ai/internal/codec-transformer.ts index f1d15f1b7..7d3b858b7 100644 --- a/.context/effect/packages/effect/src/unstable/ai/internal/codec-transformer.ts +++ b/.context/effect/packages/effect/src/unstable/ai/internal/codec-transformer.ts @@ -2,12 +2,18 @@ import * as JsonSchema from "../../../JsonSchema.ts" import * as Schema from "../../../Schema.ts" import type { CodecTransformer } from "../LanguageModel.ts" -/** @internal */ -export const defaultCodecTransformer: CodecTransformer = (codec) => { - const document = JsonSchema.resolveTopLevel$ref(Schema.toJsonSchemaDocument(codec)) - const jsonSchema = { ...document.schema } - if (Object.keys(document.definitions).length > 0) { - jsonSchema.$defs = document.definitions +const makeDefaultCodecTransformer = ( + toJsonSchemaDocument: (schema: Schema.Constraint) => JsonSchema.Document<"draft-2020-12"> +): CodecTransformer => { + return (codec) => { + const document = JsonSchema.resolveTopLevel$ref(toJsonSchemaDocument(codec)) + const jsonSchema = { ...document.schema } + if (Object.keys(document.definitions).length > 0) { + jsonSchema.$defs = document.definitions + } + return { codec, jsonSchema } } - return { codec, jsonSchema } } + +/** @internal */ +export const defaultCodecTransformer = makeDefaultCodecTransformer(Schema.toJsonSchemaDocument) diff --git a/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocol.ts b/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocol.ts new file mode 100644 index 000000000..fe4dee034 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocol.ts @@ -0,0 +1,85 @@ +import type * as Effect from "../../../Effect.ts" +import * as Schema from "../../../Schema.ts" +import type * as Rpc from "../../rpc/Rpc.ts" +import type * as RpcGroup from "../../rpc/RpcGroup.ts" + +export interface PayloadCodecs { + readonly decode: (input: unknown) => Effect.Effect + readonly encode: (input: unknown) => Effect.Effect +} + +/** @internal */ +export interface AnyProtocolAdapter { + readonly protocolVersion: string + readonly transport: { + readonly acceptsJsonRpcBatches: boolean + readonly requiresVersionHeader: boolean + } + readonly clientRpcs: RpcGroup.Any + readonly clientNotificationRpcs: RpcGroup.Any + readonly serverRequestRpcs: RpcGroup.Any + readonly serverNotificationRpcs: RpcGroup.Any + readonly payloadCodecs: (rpc: Rpc.AnyWithProps) => PayloadCodecs +} + +export interface ProtocolAdapter< + out Version extends string = string, + ClientRpcs extends Rpc.Any = Rpc.Any, + ClientNotificationRpcs extends ClientRpcs = ClientRpcs, + ServerRequestRpcs extends Rpc.Any = Rpc.Any, + ServerNotificationRpcs extends Rpc.Any = Rpc.Any +> { + readonly protocolVersion: Version + readonly transport: { + readonly acceptsJsonRpcBatches: boolean + readonly requiresVersionHeader: boolean + } + readonly clientRpcs: RpcGroup.RpcGroup + readonly clientNotificationRpcs: RpcGroup.RpcGroup + readonly serverRequestRpcs: RpcGroup.RpcGroup + readonly serverNotificationRpcs: RpcGroup.RpcGroup + readonly payloadCodecs: (rpc: Rpc.AnyWithProps) => PayloadCodecs +} + +/** @internal */ +export const make = < + const Version extends string, + ClientRpcs extends Rpc.Any, + ClientNotificationRpcs extends ClientRpcs, + ServerRequestRpcs extends Rpc.Any, + ServerNotificationRpcs extends Rpc.Any +>(options: { + readonly protocolVersion: Version + readonly transport: { + readonly acceptsJsonRpcBatches: boolean + readonly requiresVersionHeader: boolean + } + readonly clientRpcs: RpcGroup.RpcGroup + readonly clientNotificationRpcs: RpcGroup.RpcGroup + readonly serverRequestRpcs: RpcGroup.RpcGroup + readonly serverNotificationRpcs: RpcGroup.RpcGroup +}): ProtocolAdapter< + Version, + ClientRpcs, + ClientNotificationRpcs, + ServerRequestRpcs, + ServerNotificationRpcs +> => { + const payloadCodecsCache = new WeakMap() + const payloadCodecs = (rpc: Rpc.AnyWithProps): PayloadCodecs => { + let codecs = payloadCodecsCache.get(rpc) + if (codecs === undefined) { + const schema = Schema.toCodecJson(rpc.payloadSchema) + codecs = { + decode: Schema.decodeUnknownEffect(schema) as PayloadCodecs["decode"], + encode: Schema.encodeUnknownEffect(schema) as PayloadCodecs["encode"] + } + payloadCodecsCache.set(rpc, codecs) + } + return codecs + } + return { + ...options, + payloadCodecs + } +} diff --git a/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocolRegistry.ts b/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocolRegistry.ts new file mode 100644 index 000000000..818bba60c --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/ai/internal/mcpProtocolRegistry.ts @@ -0,0 +1,70 @@ +import type { NonEmptyReadonlyArray } from "../../../Array.ts" +import * as Cause from "../../../Cause.ts" +import * as Effect from "../../../Effect.ts" +import type * as RpcGroup from "../../rpc/RpcGroup.ts" +import type * as RpcMessage from "../../rpc/RpcMessage.ts" +import type * as McpProtocol from "./mcpProtocol.ts" + +type AnyRpcGroup = RpcGroup.RpcGroup + +const prefix = (protocol: McpProtocol.AnyProtocolAdapter): string => + `@effect/mcp/${encodeURIComponent(protocol.protocolVersion)}/` + +const asRpcGroup = (group: RpcGroup.Any): AnyRpcGroup => group as unknown as AnyRpcGroup + +/** @internal */ +export interface ProtocolRegistry< + Protocol extends McpProtocol.AnyProtocolAdapter = McpProtocol.AnyProtocolAdapter +> { + readonly protocols: NonEmptyReadonlyArray + readonly clientRpcs: AnyRpcGroup + readonly select: (offeredVersion: string) => Protocol + readonly routeClientRequest: ( + protocol: Protocol, + request: RpcMessage.RequestEncoded + ) => RpcMessage.RequestEncoded +} + +/** @internal */ +export const make = Effect.fnUntraced(function*< + const Protocols extends NonEmptyReadonlyArray +>( + protocols: Protocols +) { + type Protocol = Protocols[number] + + if (protocols.length === 0) { + return yield* new Cause.IllegalArgumentError( + "MCP protocol declaration must contain at least one MCP protocol" + ) + } + + const snapshot = Object.freeze(Array.from(protocols)) as NonEmptyReadonlyArray + const byVersion = new Map() + for (const protocol of snapshot) { + if (byVersion.has(protocol.protocolVersion)) { + return yield* new Cause.IllegalArgumentError( + `Duplicate MCP protocol version: ${protocol.protocolVersion}` + ) + } + byVersion.set(protocol.protocolVersion, protocol) + } + + let clientRpcs = asRpcGroup(snapshot[0].clientRpcs).prefix(prefix(snapshot[0])) + for (let i = 1; i < snapshot.length; i++) { + clientRpcs = clientRpcs.merge(asRpcGroup(snapshot[i].clientRpcs).prefix(prefix(snapshot[i]))) + } + + return { + protocols: snapshot, + clientRpcs, + select: (offeredVersion: string) => byVersion.get(offeredVersion) ?? snapshot[0], + routeClientRequest: ( + protocol: Protocol, + request: RpcMessage.RequestEncoded + ) => ({ + ...request, + tag: `${prefix(protocol)}${request.tag}` + }) + } satisfies ProtocolRegistry +}) diff --git a/.context/effect/packages/effect/src/unstable/ai/internal/structured-output.ts b/.context/effect/packages/effect/src/unstable/ai/internal/structured-output.ts new file mode 100644 index 000000000..2d090d162 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/ai/internal/structured-output.ts @@ -0,0 +1,453 @@ +import * as Arr from "../../../Array.ts" +import * as InternalRecord from "../../../internal/record.ts" +import type * as JsonSchema from "../../../JsonSchema.ts" +import * as Option from "../../../Option.ts" +import * as Predicate from "../../../Predicate.ts" +import * as Schema from "../../../Schema.ts" +import * as SchemaAST from "../../../SchemaAST.ts" +import * as SchemaTransformation from "../../../SchemaTransformation.ts" +import * as Tool from "../Tool.ts" + +const REST_PROPERTY_NAME = "__rest__" +const TAIL_PROPERTY_PREFIX = "__tail_" + +const RECORD_DESCRIPTION = + "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object" + +const TUPLE_DESCRIPTION = + "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements" +const TUPLE_TAIL_DESCRIPTION = `${TUPLE_DESCRIPTION}. Post-rest elements use '__tail_0__', '__tail_1__', and so on` + +/** @internal */ +export function toCodec( + schema: Schema.ConstraintCodec +): Schema.ConstraintCodec { + const jsonCodec = Schema.toCodecJson(schema) + const encoded = SchemaAST.toEncoded(jsonCodec.ast) + const from = transform(encoded) + if (from === encoded) { + return jsonCodec.ast === schema.ast ? schema : jsonCodec + } + return Schema.make( + SchemaAST.decodeTo(from, jsonCodec.ast, SchemaTransformation.passthrough()) + ) +} + +function transform(root: SchemaAST.AST): SchemaAST.AST { + let cache: Map | undefined + + function recur(ast: SchemaAST.AST): SchemaAST.AST { + switch (ast._tag) { + case "Union": { + const types = SchemaAST.mapOrSame(ast.types, recur) + const checks = prepareChecks(ast.checks) + const mode = ast.mode === "oneOf" ? "anyOf" : ast.mode + if (types === ast.types && checks === ast.checks && mode === ast.mode) return ast + return new SchemaAST.Union( + types, + mode, + ast.annotations, + checks, + ast.encoding, + ast.context, + ast.encodingChecks + ) + } + case "Arrays": { + if (ast.elements.length > 0 || ast.rest.length > 1) { + return tupleToObject(ast, recur) + } + const rest = SchemaAST.mapOrSame(ast.rest, recur) + const checks = prepareChecks(ast.checks) + if (rest === ast.rest && checks === ast.checks) return ast + return new SchemaAST.Arrays( + ast.isMutable, + ast.elements, + rest, + ast.annotations, + checks, + ast.encoding, + ast.context, + ast.encodingChecks + ) + } + case "Objects": { + if (ast.indexSignatures.length === 1 && ast.propertySignatures.length === 0) { + const indexSignature = ast.indexSignatures[0] + if (Tool.isEmptyParamsRecord(indexSignature)) return ast + } + if (ast.indexSignatures.length > 0) { + return objectToEntries(ast, recur) + } + + const propertySignatures = SchemaAST.mapOrSame(ast.propertySignatures, (propertySignature) => { + let type = recur(propertySignature.type) + if (SchemaAST.isOptional(propertySignature.type)) { + type = optionalToNullable(type) + } + return type === propertySignature.type + ? propertySignature + : new SchemaAST.PropertySignature(propertySignature.name, type) + }) + const checks = prepareChecks(ast.checks) + if ( + propertySignatures === ast.propertySignatures && + checks === ast.checks + ) { + return ast + } + return new SchemaAST.Objects( + propertySignatures, + ast.indexSignatures, + ast.annotations, + checks, + ast.encoding, + ast.context, + ast.encodingChecks + ) + } + case "Suspend": { + const cached = cache?.get(ast) + if (cached !== undefined) return cached + const out = new SchemaAST.Suspend( + () => recur(ast.thunk()), + ast.annotations, + undefined, + ast.encoding, + ast.context + ) + if (cache === undefined) cache = new Map() + cache.set(ast, out) + return out + } + default: { + const checks = prepareChecks(ast.checks) + return checks === ast.checks ? ast : SchemaAST.replaceChecks(ast, checks) + } + } + } + + return recur(root) +} + +function tupleToObject(ast: SchemaAST.Arrays, recur: (ast: SchemaAST.AST) => SchemaAST.AST): SchemaAST.AST { + const propertySignatures = ast.elements.map((element, index) => + new SchemaAST.PropertySignature(String(index), element) + ) + if (ast.rest.length === 1) { + propertySignatures.push( + new SchemaAST.PropertySignature(REST_PROPERTY_NAME, new SchemaAST.Arrays(false, [], ast.rest)) + ) + } else if (ast.rest.length > 1) { + propertySignatures.push( + new SchemaAST.PropertySignature(REST_PROPERTY_NAME, new SchemaAST.Arrays(false, [], [ast.rest[0]])) + ) + for (let index = 1; index < ast.rest.length; index++) { + propertySignatures.push( + new SchemaAST.PropertySignature(`${TAIL_PROPERTY_PREFIX}${index - 1}__`, ast.rest[index]) + ) + } + } + const from = recur( + new SchemaAST.Objects( + propertySignatures, + [], + structuralAnnotations(ast, ast.rest.length > 1 ? TUPLE_TAIL_DESCRIPTION : TUPLE_DESCRIPTION), + compilerChecks(ast.checks) + ) + ) + return SchemaAST.decodeTo( + from, + ast, + SchemaTransformation.transform({ + decode: (object) => { + const tuple: Array = [] + for (let index = 0; index < ast.elements.length; index++) { + const key = String(index) + if (object[key] !== undefined) tuple.push(object[key]) + } + if (REST_PROPERTY_NAME in object) { + const rest = object[REST_PROPERTY_NAME] + for (let index = 0; index < rest.length; index++) { + tuple.push(rest[index]) + } + } + for (let index = 1; index < ast.rest.length; index++) { + tuple.push(object[`${TAIL_PROPERTY_PREFIX}${index - 1}__`]) + } + return tuple + }, + encode: (tuple) => { + const object: Record = {} + for (let index = 0; index < ast.elements.length; index++) { + if (index < tuple.length) object[String(index)] = tuple[index] + } + if (ast.rest.length >= 1) { + const tailLength = ast.rest.length - 1 + const restEnd = Math.max(ast.elements.length, tuple.length - tailLength) + object[REST_PROPERTY_NAME] = tuple.slice(ast.elements.length, restEnd) + for (let index = 0; index < tailLength; index++) { + object[`${TAIL_PROPERTY_PREFIX}${index}__`] = tuple[restEnd + index] + } + } + return object + } + }) + ) +} + +function objectToEntries( + ast: SchemaAST.Objects, + recur: (ast: SchemaAST.AST) => SchemaAST.AST +): SchemaAST.AST { + const checks = combineChecks(recordChecks(ast.checks), compilerChecks(ast.checks)) + const key = unionOrSingle([ + ...ast.propertySignatures.map((propertySignature) => new SchemaAST.Literal(propertySignature.name as string)), + ...ast.indexSignatures.map((indexSignature) => indexSignature.parameter) + ]) + const value = unionOrSingle([ + ...ast.propertySignatures.map((propertySignature) => propertySignature.type), + ...ast.indexSignatures.map((indexSignature) => indexSignature.type) + ]) + const from = recur( + new SchemaAST.Arrays( + false, + [], + [new SchemaAST.Arrays(false, [key, value], [])], + structuralAnnotations(ast, RECORD_DESCRIPTION), + checks + ) + ) + return SchemaAST.decodeTo( + from, + ast, + SchemaTransformation.transform({ + decode: Object.fromEntries, + encode: Object.entries + }) + ) +} + +function unionOrSingle(types: ReadonlyArray): SchemaAST.AST { + if (types.length === 1) return types[0] + const unique = Array.from(new Set(types)) + return unique.length === 1 ? unique[0] : new SchemaAST.Union(unique, "anyOf") +} + +function combineChecks( + left: SchemaAST.Checks | undefined, + right: SchemaAST.Checks | undefined +): SchemaAST.Checks | undefined { + if (left === undefined) return right + if (right === undefined) return left + return [...left, ...right] +} + +function compilerChecks(checks: SchemaAST.Checks | undefined): SchemaAST.Checks | undefined { + if (checks === undefined) return undefined + const out = checks.flatMap(compilerCheck) + return Arr.isArrayNonEmpty(out) ? out : undefined +} + +function compilerCheck(check: SchemaAST.Check): Array> { + // Structural rewrites change the value seen by provider-side checks. A + // no-op proxy lets the generic compiler call `toJsonSchema` with the real + // provider type and dependency schemas without running the original filter + // against the wrong runtime shape. + const annotations = compilerAnnotations(check.annotations) + if (check._tag === "Filter") { + return annotations === undefined ? [] : [new SchemaAST.Filter(() => undefined, annotations)] + } + const checks = check.checks.flatMap(compilerCheck) + if (Arr.isArrayNonEmpty(checks)) { + return [new SchemaAST.FilterGroup(checks, annotations)] + } + return annotations === undefined ? [] : [new SchemaAST.Filter(() => undefined, annotations)] +} + +function compilerAnnotations( + annotations: Schema.Annotations.Filter | undefined +): Schema.Annotations.Filter | undefined { + if (annotations?.toJsonSchema === undefined) return undefined + return { + representation: annotations.representation, + toJsonSchema: annotations.toJsonSchema + } +} + +function optionalToNullable(type: SchemaAST.AST): SchemaAST.AST { + return SchemaAST.decodeTo( + new SchemaAST.Union([type, SchemaAST.null], "anyOf"), + SchemaAST.optionalKey(type), + SchemaTransformation.transformOptional({ + decode: Option.filter(Predicate.isNotNull), + encode: Option.orElseSome(() => null) + }) + ) +} + +function prepareChecks(checks: SchemaAST.Checks | undefined): SchemaAST.Checks | undefined { + if (checks === undefined) return undefined + const out = SchemaAST.mapOrSame(checks, prepareCheck) + return out as SchemaAST.Checks +} + +function prepareCheck(check: SchemaAST.Check): SchemaAST.Check { + switch (check._tag) { + case "Filter": { + const id = check.annotations?.representation?.id + return id === "effect/schema/isFinite" || id === "effect/schema/isInt" + ? check.annotate({ expected: undefined }) + : check + } + case "FilterGroup": { + const checks = SchemaAST.mapOrSame(check.checks, prepareCheck) + return checks === check.checks + ? check + : new SchemaAST.FilterGroup(checks, check.annotations) + } + } +} + +function recordChecks(checks: SchemaAST.Checks | undefined): SchemaAST.Checks | undefined { + if (checks === undefined) return undefined + const out = checks.flatMap(recordCheck) + return Arr.isArrayNonEmpty(out) ? out : undefined +} + +function recordCheck(check: SchemaAST.Check): Array> { + if (check._tag === "FilterGroup") return check.checks.flatMap(recordCheck) + const representation = check.annotations?.representation + const payload = representation?.payload + if (!isJsonObject(payload)) return [] + // A decoded record with at least N properties necessarily came from at least + // N entries. Upper bounds are not transported because duplicate provider + // keys can collapse during `Object.fromEntries`, so `maxItems` could reject + // an input that the returned codec accepts. + switch (representation?.id) { + case "effect/schema/isMinProperties": + return typeof payload.minProperties === "number" + ? [withoutDescription(Schema.isMinLength(payload.minProperties))] + : [] + case "effect/schema/isPropertiesLengthBetween": + return typeof payload.minimum === "number" + ? [withoutDescription(Schema.isMinLength(payload.minimum))] + : [] + default: + return [] + } +} + +function isJsonObject(input: Schema.Json | undefined): input is Schema.JsonObject { + return typeof input === "object" && input !== null && !Array.isArray(input) +} + +function withoutDescription(check: SchemaAST.Filter): SchemaAST.Filter { + return check.annotate({ + description: undefined, + expected: undefined, + title: undefined + }) +} + +function structuralAnnotations( + ast: SchemaAST.AST, + structuralDescription: string +): Schema.Annotations.Annotations { + const descriptions: Array = [] + appendAnnotationDescription(descriptions, ast.annotations) + if (ast.checks !== undefined) { + for (const check of ast.checks) appendCheckDescriptions(descriptions, check) + } + return { + description: descriptions.length === 0 + ? structuralDescription + : `${structuralDescription}; ${descriptions.join(" and ")}` + } +} + +function appendCheckDescriptions(descriptions: Array, check: SchemaAST.Check): void { + appendAnnotationDescription(descriptions, check.annotations) + if (check._tag === "FilterGroup") { + for (const child of check.checks) appendCheckDescriptions(descriptions, child) + } +} + +function appendAnnotationDescription( + descriptions: Array, + annotations: Schema.Annotations.Annotations | undefined +): void { + const description = annotations?.description ?? annotations?.expected + if (typeof description === "string" && !descriptions.includes(description)) { + descriptions.push(description) + } +} + +type JsonSchemaVisitor = (schema: JsonSchema.JsonSchema) => JsonSchema.JsonSchema + +/** @internal */ +export function walkJsonSchema( + schema: JsonSchema.JsonSchema, + visitor: JsonSchemaVisitor +): JsonSchema.JsonSchema { + const out: JsonSchema.JsonSchema = {} + for (const [key, value] of Object.entries(schema)) { + switch (key) { + case "properties": + case "patternProperties": { + if (isJsonSchema(value)) { + const properties: Record = {} + for (const [name, property] of Object.entries(value)) { + InternalRecord.assignProperty( + properties, + name, + isJsonSchema(property) ? walkJsonSchema(property, visitor) : property + ) + } + InternalRecord.assignProperty(out, key, properties) + } else { + InternalRecord.assignProperty(out, key, value) + } + break + } + case "additionalProperties": + case "items": + case "propertyNames": { + InternalRecord.assignProperty(out, key, isJsonSchema(value) ? walkJsonSchema(value, visitor) : value) + break + } + case "prefixItems": + case "allOf": + case "anyOf": + case "oneOf": { + InternalRecord.assignProperty( + out, + key, + Array.isArray(value) + ? value.map((member) => isJsonSchema(member) ? walkJsonSchema(member, visitor) : member) + : value + ) + break + } + default: + InternalRecord.assignProperty(out, key, value) + break + } + } + return visitor(out) +} + +/** @internal */ +export function appendDescription(schema: JsonSchema.JsonSchema, description: string): void { + if (typeof schema.description === "string") { + const descriptions = schema.description.split(" and ") + if (!descriptions.includes(description)) schema.description += ` and ${description}` + } else { + schema.description = description + } +} + +/** @internal */ +export function isJsonSchema(input: unknown): input is JsonSchema.JsonSchema { + return typeof input === "object" && input !== null && !Array.isArray(input) +} diff --git a/.context/effect/packages/effect/src/unstable/cli/Argument.ts b/.context/effect/packages/effect/src/unstable/cli/Argument.ts index 7eb40ac80..36928fbae 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Argument.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Argument.ts @@ -49,10 +49,11 @@ export interface Argument extends Param.Param { * * **Example** (Creating a string argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const filename = Argument.string("filename") + * filename.kind // => "argument" * ``` * * @category constructors @@ -65,10 +66,11 @@ export const string = (name: string): Argument => Param.string(Param.arg * * **Example** (Creating an integer argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const count = Argument.integer("count") + * count.kind // => "argument" * ``` * * @category constructors @@ -81,11 +83,12 @@ export const integer = (name: string): Argument => Param.integer(Param.a * * **Example** (Creating file path arguments) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const inputFile = Argument.file("input", { mustExist: true }) // Must exist * const outputFile = Argument.file("output", { mustExist: false }) // Must not exist + * const kinds = [inputFile.kind, outputFile.kind] // => ["argument", "argument"] * ``` * * @category constructors @@ -100,10 +103,11 @@ export const file = (name: string, options?: { * * **Example** (Creating a directory path argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const workspace = Argument.directory("workspace", { mustExist: true }) // Must exist + * workspace.kind // => "argument" * ``` * * @category constructors @@ -118,10 +122,11 @@ export const directory = (name: string, options?: { * * **Example** (Creating a float argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const ratio = Argument.float("ratio") + * ratio.kind // => "argument" * ``` * * @category constructors @@ -134,10 +139,11 @@ export const float = (name: string): Argument => Param.float(Param.argum * * **Example** (Creating a date argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const startDate = Argument.date("start-date") + * startDate.kind // => "argument" * ``` * * @category constructors @@ -150,10 +156,11 @@ export const date = (name: string): Argument => Param.date(Param.argumentK * * **Example** (Creating a choice argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const environment = Argument.choice("environment", ["dev", "staging", "prod"]) + * environment.kind // => "argument" * ``` * * @category constructors @@ -169,10 +176,11 @@ export const choice = >( * * **Example** (Creating a path argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const configPath = Argument.path("config") + * configPath.kind // => "argument" * ``` * * @category constructors @@ -188,10 +196,11 @@ export const path = (name: string, options?: { * * **Example** (Creating a redacted argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const secret = Argument.redacted("secret") + * secret.kind // => "argument" * ``` * * @category constructors @@ -204,10 +213,11 @@ export const redacted = (name: string): Argument> => P * * **Example** (Reading file text) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const config = Argument.fileText("config-file") + * config.kind // => "argument" * ``` * * @category constructors @@ -226,10 +236,11 @@ export const fileText = (name: string): Argument => Param.fileText(Param * * **Example** (Parsing file content) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const config = Argument.fileParse("config", { format: "json" }) + * config.kind // => "argument" * ``` * * @category constructors @@ -245,7 +256,7 @@ export const fileParse = ( * * **Example** (Validating file content with a schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Argument } from "effect/unstable/cli" * @@ -255,6 +266,7 @@ export const fileParse = ( * }) * * const config = Argument.fileSchema("config", ConfigSchema) + * config.kind // => "argument" * ``` * * @category constructors @@ -271,11 +283,12 @@ export const fileSchema = ( * * **Example** (Creating a sentinel argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * // Used as a placeholder or default in combinators * const noArg = Argument.none + * noArg.kind // => "argument" * ``` * * @category constructors @@ -292,10 +305,11 @@ export const none: Argument = Param.none(Param.argumentKind) * * **Example** (Making an argument optional) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const optionalVersion = Argument.string("version").pipe(Argument.optional) + * optionalVersion.kind // => "argument" * ``` * * @category combinators @@ -308,12 +322,13 @@ export const optional = (arg: Argument): Argument> => Par * * **Example** (Adding an argument description) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const filename = Argument.string("filename").pipe( * Argument.withDescription("The input file to process") * ) + * filename.kind // => "argument" * ``` * * @category combinators @@ -329,10 +344,11 @@ export const withDescription: { * * **Example** (Providing a default value) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const port = Argument.integer("port").pipe(Argument.withDefault(8080)) + * port.kind // => "argument" * ``` * * @category combinators @@ -353,13 +369,14 @@ export const withDefault: { * * **Example** (Loading a fallback config) * - * ```ts + * ```ts import.meta.vitest * import { Config } from "effect" * import { Argument } from "effect/unstable/cli" * * const repository = Argument.string("repository").pipe( * Argument.withFallbackConfig(Config.string("REPOSITORY")) * ) + * repository.kind // => "argument" * ``` * * @category combinators @@ -375,12 +392,13 @@ export const withFallbackConfig: { * * **Example** (Showing a fallback prompt) * - * ```ts + * ```ts import.meta.vitest * import { Argument, Prompt } from "effect/unstable/cli" * * const filename = Argument.string("filename").pipe( * Argument.withFallbackPrompt(Prompt.text({ message: "Filename" })) * ) + * filename.kind // => "argument" * ``` * * @category combinators @@ -396,7 +414,7 @@ export const withFallbackPrompt: { * * **Example** (Accepting multiple values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * // Accept any number of files @@ -411,6 +429,8 @@ export const withFallbackPrompt: { * const limitedFiles = Argument.string("files").pipe( * Argument.variadic({ min: 1, max: 5 }) * ) + * + * const kinds = [anyFiles.kind, atLeastOneFile.kind, limitedFiles.kind] // => ["argument", "argument", "argument"] * ``` * * @category combinators @@ -429,12 +449,13 @@ export const variadic: { * * **Example** (Mapping parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const port = Argument.integer("port").pipe( * Argument.map((p) => ({ port: p, url: `http://localhost:${p}` })) * ) + * port.kind // => "argument" * ``` * * @category combinators @@ -450,9 +471,27 @@ export const map: { * * **Example** (Validating values effectfully) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Argument, CliError } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const files = Argument.string("files").pipe( * Argument.mapEffect((file) => @@ -460,11 +499,17 @@ export const map: { * ? Effect.succeed(file) * : Effect.fail( * new CliError.UserError({ - * cause: new Error("Only .txt files allowed") + * cause: new Error(`Unsupported file extension: ${file}`), + * userMessage: "Only .txt files allowed" * }) * ) * ) * ) + * + * const [, value] = await Effect.runPromise( + * files.parse({ arguments: ["notes.txt"], flags: {} }).pipe(Effect.provide(CliTestLayer)) + * ) + * value // => "notes.txt" * ``` * * @category combinators @@ -488,8 +533,27 @@ export const mapEffect: { * * **Example** (Mapping values that may throw) * - * ```ts + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Argument } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const json = Argument.string("data").pipe( * Argument.mapTryCatch( @@ -498,6 +562,11 @@ export const mapEffect: { * `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` * ) * ) + * + * const [, value] = await Effect.runPromise( + * json.parse({ arguments: ['{"enabled":true}'], flags: {} }).pipe(Effect.provide(CliTestLayer)) + * ) + * value // => { enabled: true } * ``` * * @category combinators @@ -520,10 +589,11 @@ export const mapTryCatch: { * * **Example** (Requiring a minimum number of values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const files = Argument.string("files").pipe(Argument.atLeast(1)) + * files.kind // => "argument" * ``` * * @category combinators @@ -539,10 +609,11 @@ export const atLeast: { * * **Example** (Limiting the maximum number of values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const files = Argument.string("files").pipe(Argument.atMost(5)) + * files.kind // => "argument" * ``` * * @category combinators @@ -558,10 +629,11 @@ export const atMost: { * * **Example** (Requiring a range of values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const files = Argument.string("files").pipe(Argument.between(1, 5)) + * files.kind // => "argument" * ``` * * @category combinators @@ -577,13 +649,14 @@ export const between: { * * **Example** (Validating parsed values with a schema) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Argument } from "effect/unstable/cli" * * const input = Argument.string("input").pipe( * Argument.withSchema(Schema.NonEmptyString) * ) + * input.kind // => "argument" * ``` * * @category combinators @@ -603,7 +676,7 @@ export const withSchema: { * * **Example** (Mapping choices to values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const logLevel = Argument.choiceWithValue("level", [ @@ -612,6 +685,7 @@ export const withSchema: { * ["warn", 2], * ["error", 3] * ]) + * logLevel.kind // => "argument" * ``` * * @category constructors @@ -636,12 +710,13 @@ export const choiceWithValue = "argument" * ``` * * @category metadata @@ -657,7 +732,7 @@ export const withMetavar: { * * **Example** (Filtering parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const positiveInt = Argument.integer("count").pipe( @@ -666,6 +741,7 @@ export const withMetavar: { * (n) => `Expected positive integer, got ${n}` * ) * ) + * positiveInt.kind // => "argument" * ``` * * @category combinators @@ -686,7 +762,7 @@ export const filter: { * * **Example** (Filtering and mapping parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * import { Argument } from "effect/unstable/cli" * @@ -696,6 +772,7 @@ export const filter: { * (n) => `Expected positive integer, got ${n}` * ) * ) + * positiveInt.kind // => "argument" * ``` * * @category combinators @@ -715,12 +792,13 @@ export const filterMap: { * * **Example** (Providing a fallback argument) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const value = Argument.integer("value").pipe( * Argument.orElse(() => Argument.string("value")) * ) + * value.kind // => "argument" * ``` * * @category combinators @@ -736,13 +814,14 @@ export const orElse: { * * **Example** (Returning which fallback succeeded) * - * ```ts + * ```ts import.meta.vitest * import { Argument } from "effect/unstable/cli" * * const source = Argument.file("source").pipe( * Argument.orElseResult(() => Argument.string("url")) * ) * // Returns Result + * source.kind // => "argument" * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/unstable/cli/CliError.ts b/.context/effect/packages/effect/src/unstable/cli/CliError.ts index 2ff929399..ac8a9c877 100644 --- a/.context/effect/packages/effect/src/unstable/cli/CliError.ts +++ b/.context/effect/packages/effect/src/unstable/cli/CliError.ts @@ -2,11 +2,11 @@ * Defines structured errors for the unstable CLI parser and runner. * * CLI errors describe problems such as unknown or duplicate flags, missing - * flags or arguments, invalid values, unknown subcommands, user handler - * failures, and requests to show command help. This module includes the - * `CliError` union, the `isCliError` guard, schema-backed error classes with - * display messages, and the `NonShowHelpErrors` union used when parse or - * validation errors should be shown with help output. + * flags or arguments, unexpected positional arguments, invalid values, unknown + * subcommands, user handler failures, and requests to show command help. This + * module includes the `CliError` union, the `isCliError` guard, schema-backed + * error classes with display messages, and the `NonShowHelpErrors` union used + * when parse or validation errors should be shown with help output. * * @since 4.0.0 */ @@ -25,26 +25,16 @@ const TypeId = "~effect/cli/CliError" * * **Example** (Checking CLI errors) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliError } from "effect/unstable/cli" * - * const handleError = (error: unknown) => { - * if (CliError.isCliError(error)) { - * console.log("CLI Error:", error.message) - * return Effect.succeed("Handled CLI error") - * } - * return Effect.fail("Unknown error") - * } + * const error = new CliError.MissingOption({ option: "api-key" }) + * const program = CliError.isCliError(error) + * ? Effect.succeed(error.message) + * : Effect.fail("Unknown error") * - * // Example usage in error handling - * const program = Effect.gen(function*() { - * const result = yield* Effect.try({ - * try: () => ({ success: true }), - * catch: (error) => error - * }) - * handleError(result) - * }) + * await Effect.runPromise(program) // => "Missing required flag: --api-key" * ``` * * @category guards @@ -57,31 +47,28 @@ export const isCliError = (u: unknown): u is CliError => Predicate.hasProperty(u * * **Example** (Handling CLI errors) * - * ```ts - * import type { CliError } from "effect/unstable/cli" + * ```ts import.meta.vitest + * import { CliError } from "effect/unstable/cli" * - * const handleCliError = (error: CliError.CliError): void => { + * const describe = (error: CliError.CliError): string => { * switch (error._tag) { * case "UnrecognizedOption": - * console.log(`Unknown flag: ${error.option}`) - * break + * return `Unknown flag: ${error.option}` * case "MissingOption": - * console.log(`Required flag missing: ${error.option}`) - * break + * return `Required flag missing: ${error.option}` * case "InvalidValue": - * console.log(`Invalid value: ${error.value} for ${error.option}`) - * break + * return `Invalid value: ${error.value} for ${error.option}` * case "ShowHelp": - * // Display help for the command path - * console.log(`Help requested for: ${error.commandPath.join(" ")}`) - * break + * return `Help requested for: ${error.commandPath.join(" ")}` * default: - * console.log(error.message) + * return error.message * } * } + * + * describe(new CliError.MissingOption({ option: "token" })) // => "Required flag missing: token" * ``` * - * @category models + * @category errors * @since 4.0.0 */ export type CliError = @@ -89,6 +76,7 @@ export type CliError = | DuplicateOption | MissingOption | MissingArgument + | UnexpectedArgument | InvalidValue | UnknownSubcommand | ShowHelp @@ -99,7 +87,7 @@ export type CliError = * * **Example** (Creating unrecognized option errors) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliError } from "effect/unstable/cli" * @@ -110,24 +98,24 @@ export type CliError = * suggestions: ["--verbose", "--force"] * }) * - * console.log(unrecognizedError.message) - * // "Unrecognized flag: --unknown-flag in command deploy production - * // - * // Did you mean this? - * // --verbose - * // --force" + * unrecognizedError._tag // => "UnrecognizedOption" + * unrecognizedError.option // => "--unknown-flag" + * unrecognizedError.command // => ["deploy", "production"] * * // In CLI parsing context * const parseCommand = Effect.gen(function*() { * // If parsing encounters unknown flag * return yield* unrecognizedError * }) + * + * const parseError = await Effect.runPromise(Effect.flip(parseCommand)) + * parseError._tag // => "UnrecognizedOption" * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class UnrecognizedOption extends Schema.TaggedErrorClass( +export class UnrecognizedOption extends Schema.TaggedError( `${TypeId}/UnrecognizedOption` )("UnrecognizedOption", { option: Schema.String, @@ -162,7 +150,7 @@ export class UnrecognizedOption extends Schema.TaggedErrorClass "DuplicateOption" + * duplicateError.option // => "--verbose" + * duplicateError.parentCommand // => "myapp" + * duplicateError.childCommand // => "deploy" * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class DuplicateOption extends Schema.TaggedErrorClass( +export class DuplicateOption extends Schema.TaggedError( `${TypeId}/DuplicateOption` )("DuplicateOption", { option: Schema.String, @@ -209,7 +198,7 @@ export class DuplicateOption extends Schema.TaggedErrorClass( * * **Example** (Creating missing option errors) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliError } from "effect/unstable/cli" * @@ -217,8 +206,7 @@ export class DuplicateOption extends Schema.TaggedErrorClass( * option: "api-key" * }) * - * console.log(missingOptionError.message) - * // "Missing required flag: --api-key" + * const details = [missingOptionError._tag, missingOptionError.option] // => ["MissingOption", "api-key"] * * // In validation context * const validateRequiredOptions = (options: Record) => @@ -229,12 +217,15 @@ export class DuplicateOption extends Schema.TaggedErrorClass( * } * return apiKey * }) + * + * const validationError = await Effect.runPromise(Effect.flip(validateRequiredOptions({}))) + * validationError._tag // => "MissingOption" * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class MissingOption extends Schema.TaggedErrorClass( +export class MissingOption extends Schema.TaggedError( `${TypeId}/MissingOption` )("MissingOption", { option: Schema.String @@ -261,7 +252,7 @@ export class MissingOption extends Schema.TaggedErrorClass( * * **Example** (Creating missing argument errors) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliError } from "effect/unstable/cli" * @@ -269,8 +260,7 @@ export class MissingOption extends Schema.TaggedErrorClass( * argument: "target" * }) * - * console.log(missingArgError.message) - * // "Missing required argument: target" + * const details = [missingArgError._tag, missingArgError.argument] // => ["MissingArgument", "target"] * * // In argument parsing * const parseArguments = (args: Array) => @@ -280,12 +270,15 @@ export class MissingOption extends Schema.TaggedErrorClass( * } * return args[0] * }) + * + * const parseError = await Effect.runPromise(Effect.flip(parseArguments([]))) + * parseError._tag // => "MissingArgument" * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class MissingArgument extends Schema.TaggedErrorClass( +export class MissingArgument extends Schema.TaggedError( `${TypeId}/MissingArgument` )("MissingArgument", { argument: Schema.String @@ -307,13 +300,54 @@ export class MissingArgument extends Schema.TaggedErrorClass( } } +/** + * Error thrown when positional arguments remain after a command has parsed all + * of its parameters. + * + * **Example** (Reporting unexpected arguments) + * + * ```ts import.meta.vitest + * import { CliError } from "effect/unstable/cli" + * + * const error = new CliError.UnexpectedArgument({ + * arguments: ["extra.txt"] + * }) + * + * const details = [error._tag, error.arguments] // => ["UnexpectedArgument", ["extra.txt"]] + * ``` + * + * @category errors + * @since 4.0.0 + */ +export class UnexpectedArgument extends Schema.TaggedError( + `${TypeId}/UnexpectedArgument` +)("UnexpectedArgument", { + arguments: Schema.Array(Schema.String) +}) { + /** + * Marks this value as an unexpected CLI argument error for runtime guards. + * + * @since 4.0.0 + */ + readonly [TypeId] = TypeId + + /** + * Formats the unexpected positional arguments for display. + * + * @since 4.0.0 + */ + override get message() { + const label = this.arguments.length === 1 ? "argument" : "arguments" + return `Unexpected positional ${label}: ${this.arguments.map((value) => JSON.stringify(value)).join(", ")}` + } +} + /** * Error thrown when an option or argument value is invalid. * * **Example** (Creating invalid value errors) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest * import { CliError } from "effect/unstable/cli" * * const invalidValueError = new CliError.InvalidValue({ @@ -323,8 +357,10 @@ export class MissingArgument extends Schema.TaggedErrorClass( * kind: "flag" * }) * - * console.log(invalidValueError.message) - * // "Invalid value for flag --port: "abc123". Expected: integer between 1 and 65535" + * invalidValueError._tag // => "InvalidValue" + * invalidValueError.kind // => "flag" + * invalidValueError.option // => "port" + * invalidValueError.value // => "abc123" * * // For positional arguments * const invalidArgError = new CliError.InvalidValue({ @@ -334,14 +370,13 @@ export class MissingArgument extends Schema.TaggedErrorClass( * kind: "argument" * }) * - * console.log(invalidArgError.message) - * // "Invalid value for argument : "abc". Expected: integer" + * const details = [invalidArgError.kind, invalidArgError.option, invalidArgError.value] // => ["argument", "count", "abc"] * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class InvalidValue extends Schema.TaggedErrorClass( +export class InvalidValue extends Schema.TaggedError( `${TypeId}/InvalidValue` )("InvalidValue", { option: Schema.String, @@ -380,7 +415,7 @@ export class InvalidValue extends Schema.TaggedErrorClass( * * **Example** (Creating unknown subcommand errors) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliError } from "effect/unstable/cli" * @@ -390,12 +425,9 @@ export class InvalidValue extends Schema.TaggedErrorClass( * suggestions: ["deploy", "destroy"] * }) * - * console.log(unknownSubcommandError.message) - * // "Unknown subcommand "deplyo" for "myapp" - * // - * // Did you mean this? - * // deploy - * // destroy" + * unknownSubcommandError._tag // => "UnknownSubcommand" + * unknownSubcommandError.subcommand // => "deplyo" + * unknownSubcommandError.parent // => ["myapp"] * * // In subcommand parsing * const parseSubcommand = (subcommand: string) => @@ -406,14 +438,17 @@ export class InvalidValue extends Schema.TaggedErrorClass( * } * return subcommand * }) + * + * const parseError = await Effect.runPromise(Effect.flip(parseSubcommand("deplyo"))) + * parseError._tag // => "UnknownSubcommand" * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class UnknownSubcommand extends Schema.TaggedErrorClass( +export class UnknownSubcommand extends Schema.TaggedError( `${TypeId}/UnknownSubcommand` -)("UnknownSubcomand", { +)("UnknownSubcommand", { subcommand: Schema.String, parent: Schema.optional(Schema.Array(Schema.String)), suggestions: Schema.Array(Schema.String) @@ -443,15 +478,20 @@ export class UnknownSubcommand extends Schema.TaggedErrorClass => { * if (error._tag === "UserError") { - * console.log("Command failed:", error.cause) * return Effect.succeed(1) // Exit code 1 * } * return Effect.succeed(0) * } + * + * await Effect.runPromise(deployCommand) // => { deployed: true } + * await Effect.runPromise(handleError(userError)) // => 1 * ``` * - * @category models + * @category errors * @since 4.0.0 */ -export class UserError extends Schema.TaggedErrorClass( +export class UserError extends Schema.TaggedError( `${TypeId}/UserError` )("UserError", { - cause: Schema.Defect() + cause: Schema.Defect(), + userMessage: Schema.optionalKey(Schema.String) }) { /** * Marks this value as a user handler error for runtime guards. @@ -487,6 +530,26 @@ export class UserError extends Schema.TaggedErrorClass( * @since 4.0.0 */ readonly [TypeId] = TypeId + + /** + * Controls whether the runtime logger should report this error. The CLI + * runner sets this to `false` after rendering the error itself. + * + * @since 4.0.0 + */ + override [Runtime.errorReported] = true + + /** + * Returns the explicit user-facing message or a safe fallback from `cause`. + * + * @since 4.0.0 + */ + override get message() { + if (this.userMessage) return this.userMessage + if (typeof this.cause === "string" && this.cause) return this.cause + if (this.cause instanceof Error && this.cause.message) return this.cause.message + return "An error occurred" + } } /** @@ -497,7 +560,7 @@ export class UserError extends Schema.TaggedErrorClass( * This excludes `ShowHelp` itself, allowing parse and validation errors to be * stored in `ShowHelp.errors` without nesting another help-control value. * - * @category models + * @category schemas * @since 4.0.0 */ export const NonShowHelpErrors: Schema.Union< @@ -506,6 +569,7 @@ export const NonShowHelpErrors: Schema.Union< typeof DuplicateOption, typeof MissingOption, typeof MissingArgument, + typeof UnexpectedArgument, typeof InvalidValue, typeof UnknownSubcommand, typeof UserError @@ -515,6 +579,7 @@ export const NonShowHelpErrors: Schema.Union< DuplicateOption, MissingOption, MissingArgument, + UnexpectedArgument, InvalidValue, UnknownSubcommand, UserError @@ -529,7 +594,7 @@ export const NonShowHelpErrors: Schema.Union< * runner should display help along with the underlying parse or validation * failures. * - * @category models + * @category errors * @since 4.0.0 */ export type NonShowHelpErrors = typeof NonShowHelpErrors.Type @@ -543,10 +608,10 @@ export type NonShowHelpErrors = typeof NonShowHelpErrors.Type * that should be shown with help text. When `errors` is non-empty, the runtime * exit code is `1`; otherwise it is `0`. * - * @category models + * @category errors * @since 4.0.0 */ -export class ShowHelp extends Schema.TaggedErrorClass( +export class ShowHelp extends Schema.TaggedError( `${TypeId}/ShowHelp` )("ShowHelp", { commandPath: Schema.Array(Schema.String), diff --git a/.context/effect/packages/effect/src/unstable/cli/CliOutput.ts b/.context/effect/packages/effect/src/unstable/cli/CliOutput.ts index e65ccdb58..53aa8eaf2 100644 --- a/.context/effect/packages/effect/src/unstable/cli/CliOutput.ts +++ b/.context/effect/packages/effect/src/unstable/cli/CliOutput.ts @@ -22,7 +22,7 @@ import type { HelpDoc } from "./HelpDoc.ts" * * **Example** (Customizing CLI output formatting) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliOutput } from "effect/unstable/cli" * @@ -38,11 +38,12 @@ import type { HelpDoc } from "./HelpDoc.ts" * // Use the custom formatter in a program * const program = Effect.gen(function*() { * const formatter = yield* CliOutput.Formatter - * const helpText = formatter.formatVersion("myapp", "1.0.0") - * console.log(helpText) + * return formatter.formatVersion("myapp", "1.0.0") * }).pipe( * Effect.provide(CliOutput.layer(customFormatter)) * ) + * + * await Effect.runPromise(program) // => "myapp (1.0.0)" * ``` * * @category models @@ -54,7 +55,7 @@ export interface Formatter { * * **Example** (Formatting help documents) * - * ```ts + * ```ts import.meta.vitest * import { Option as O } from "effect" * import { CliOutput } from "effect/unstable/cli" * import type { HelpDoc } from "effect/unstable/cli" @@ -84,8 +85,7 @@ export interface Formatter { * * const formatter = CliOutput.defaultFormatter() * const helpText = formatter.formatHelpDoc(helpDoc) - * console.log(helpText) - * // Outputs formatted help with sections: DESCRIPTION, USAGE, ARGUMENTS, FLAGS + * const sectionsPresent = [helpText.includes("DESCRIPTION"), helpText.includes("FLAGS")] // => [true, true] * ``` * * @since 4.0.0 @@ -97,7 +97,7 @@ export interface Formatter { * * **Example** (Formatting CLI errors) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * import { CliOutput } from "effect/unstable/cli" * @@ -108,7 +108,7 @@ export interface Formatter { * const formatter = CliOutput.defaultFormatter() * const error = new InvalidOption({ message: "Unknown flag '--invalid'" }) * const errorMessage = formatter.formatCliError(error) - * console.log(errorMessage) // "Unknown flag '--invalid'" + * errorMessage // => "Unknown flag '--invalid'" * ``` * * @since 4.0.0 @@ -120,7 +120,7 @@ export interface Formatter { * * **Example** (Formatting error sections) * - * ```ts + * ```ts import.meta.vitest * import { Data } from "effect" * import { CliOutput } from "effect/unstable/cli" * @@ -134,10 +134,10 @@ export interface Formatter { * const error = new ValidationError({ message: "Value must be positive" }) * * const coloredError = colorFormatter.formatError(error) - * console.log(coloredError) // "\n\x1b[1m\x1b[31mERROR\x1b[0m\n Value must be positive\x1b[0m" + * coloredError.includes("\u001b[31mERROR") // => true * * const plainError = noColorFormatter.formatError(error) - * console.log(plainError) // "\nERROR\n Value must be positive" + * plainError // => "\nERROR\n Value must be positive" * ``` * * @since 4.0.0 @@ -149,7 +149,7 @@ export interface Formatter { * * **Example** (Formatting version output) * - * ```ts + * ```ts import.meta.vitest * import { CliOutput } from "effect/unstable/cli" * * const colorFormatter = CliOutput.defaultFormatter({ colors: true }) @@ -159,10 +159,10 @@ export interface Formatter { * const version = "1.2.3" * * const coloredVersion = colorFormatter.formatVersion(appName, version) - * console.log(coloredVersion) // "\x1b[1mmy-awesome-tool\x1b[0m \x1b[2mv\x1b[0m\x1b[1m1.2.3\x1b[0m" + * coloredVersion // => "\u001b[1mmy-awesome-tool\u001b[0m \u001b[2mv\u001b[0m\u001b[1m1.2.3\u001b[0m" * * const plainVersion = noColorFormatter.formatVersion(appName, version) - * console.log(plainVersion) // "my-awesome-tool v1.2.3" + * plainVersion // => "my-awesome-tool v1.2.3" * ``` * * @since 4.0.0 @@ -174,7 +174,7 @@ export interface Formatter { * * **Example** (Formatting grouped errors) * - * ```ts + * ```ts import.meta.vitest * import { CliError, CliOutput } from "effect/unstable/cli" * * const formatter = CliOutput.defaultFormatter({ colors: false }) @@ -189,7 +189,7 @@ export interface Formatter { * ] * * const output = formatter.formatErrors(errors) - * // Groups errors by type and displays all at once + * const optionsPresent = [output.includes("--foo"), output.includes("--required")] // => [true, true] * ``` * * @since 4.0.0 @@ -203,7 +203,7 @@ export interface Formatter { * * **Example** (Accessing the output formatter) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" * import { CliOutput } from "effect/unstable/cli" * @@ -212,14 +212,13 @@ export interface Formatter { * const formatter = yield* CliOutput.Formatter * * // Format version information - * const versionText = formatter.formatVersion("my-cli", "2.1.0") - * console.log(versionText) // "my-cli v2.1.0" (with colors if supported) - * - * return versionText + * return formatter.formatVersion("my-cli", "2.1.0") * }) * * // Run with default formatter - * const result = Effect.runSync(program) + * await Effect.runPromise(program.pipe( + * Effect.provide(CliOutput.layer(CliOutput.defaultFormatter({ colors: false }))) + * )) // => "my-cli v2.1.0" * ``` * * @category services @@ -235,8 +234,8 @@ export const Formatter: Context.Reference = Context.Reference( * * **Example** (Providing a custom formatter) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect } from "effect" * import { CliOutput } from "effect/unstable/cli" * * // Create a custom formatter without colors @@ -246,22 +245,12 @@ export const Formatter: Context.Reference = Context.Reference( * // Create a program that uses the custom formatter * const program = Effect.gen(function*() { * const formatter = yield* CliOutput.Formatter - * const versionText = formatter.formatVersion("my-cli", "1.0.0") - * yield* Console.log(`Using custom formatter: ${versionText}`) + * return formatter.formatVersion("my-cli", "1.0.0") * }).pipe( * Effect.provide(NoColorLayer) * ) * - * // You can also create completely custom formatters - * const jsonFormatter: CliOutput.Formatter = { - * formatHelpDoc: (doc) => JSON.stringify(doc, null, 2), - * formatCliError: (error) => JSON.stringify({ error: error.message }), - * formatError: (error) => - * JSON.stringify({ type: "error", message: error.message }), - * formatVersion: (name, version) => JSON.stringify({ name, version }), - * formatErrors: (errors) => JSON.stringify(errors.map((error) => error.message)) - * } - * const JsonLayer = CliOutput.layer(jsonFormatter) + * await Effect.runPromise(program) // => "my-cli v1.0.0" * ``` * * @category layers @@ -269,13 +258,17 @@ export const Formatter: Context.Reference = Context.Reference( */ export const layer = (formatter: Formatter): Layer.Layer => Layer.succeed(Formatter)(formatter) +const escapeControlCharacters = (text: string): string => + // oxlint-disable-next-line no-control-regex + text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, (character) => + `\\x${character.charCodeAt(0).toString(16).padStart(2, "0")}`) + /** * Creates a default formatter with configurable options. * * **Example** (Creating default formatters) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest * import { CliError, CliOutput } from "effect/unstable/cli" * * // Create a formatter without colors for tests or CI environments @@ -287,23 +280,16 @@ export const layer = (formatter: Formatter): Layer.Layer => Layer.succeed * // Auto-detect colors based on terminal support (default behavior) * const autoFormatter = CliOutput.defaultFormatter() * - * const program = Effect.gen(function*() { - * const formatter = colorFormatter - * - * // Format an error with proper styling - * const error = new CliError.InvalidValue({ - * option: "foo", - * value: "bar", - * expected: "baz", - * kind: "flag" - * }) - * const errorText = formatter.formatError(error) - * console.log(errorText) - * - * // Format version information - * const versionText = formatter.formatVersion("my-tool", "1.2.3") - * console.log(versionText) + * const error = new CliError.InvalidValue({ + * option: "foo", + * value: "bar", + * expected: "baz", + * kind: "flag" * }) + * + * noColorFormatter.formatError(error).includes("Invalid value") // => true + * colorFormatter.formatVersion("my-tool", "1.2.3").includes("my-tool") // => true + * autoFormatter.formatVersion("my-tool", "1.2.3").includes("1.2.3") // => true * ``` * * @category constructors @@ -349,14 +335,14 @@ export const defaultFormatter = (options?: { colors?: boolean }): Formatter => { return { formatHelpDoc: (doc: HelpDoc): string => formatHelpDocImpl(doc, colors), - formatCliError: (error): string => error.message, + formatCliError: (error): string => escapeControlCharacters(error.message), formatError: (error): string => { - return `\n${bold}${red}ERROR${reset}\n ${error.message}${reset}` + return `\n${bold}${red}ERROR${reset}\n ${escapeControlCharacters(error.message)}${reset}` }, formatErrors: (errors): string => { if (errors.length === 0) return "" if (errors.length === 1) { - return `\n${bold}${red}ERROR${reset}\n ${errors[0].message}${reset}` + return `\n${bold}${red}ERROR${reset}\n ${escapeControlCharacters(errors[0].message)}${reset}` } // Group errors by _tag @@ -373,7 +359,7 @@ export const defaultFormatter = (options?: { colors?: boolean }): Formatter => { for (const [, group] of grouped) { for (const error of group) { - sections.push(` ${error.message}${reset}`) + sections.push(` ${escapeControlCharacters(error.message)}${reset}`) } } diff --git a/.context/effect/packages/effect/src/unstable/cli/Command.ts b/.context/effect/packages/effect/src/unstable/cli/Command.ts index 2223a30e0..d4027c882 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Command.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Command.ts @@ -22,6 +22,7 @@ import type * as Path from "../../Path.ts" import * as Predicate from "../../Predicate.ts" import * as References from "../../References.ts" import * as Result from "../../Result.ts" +import * as Runtime from "../../Runtime.ts" import * as Stdio from "../../Stdio.ts" import * as Terminal from "../../Terminal.ts" import type { Contravariant, Covariant, NoInfer, Simplify } from "../../Types.ts" @@ -57,9 +58,27 @@ import * as Prompt from "./Prompt.ts" * * **Example** (Defining CLI commands) * - * ```ts - * import { Console } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Argument, Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * // Simple command with no configuration * const version: Command.Command<"version", {}, {}, never, never> = Command.make( @@ -84,9 +103,15 @@ import * as Prompt from "./Prompt.ts" * }) * * // Command with handler + * const output: Array = [] * const greet = Command.make("greet", { * name: Flag.string("name") - * }, (config) => Console.log(`Hello, ${config.name}!`)) + * }, (config) => Effect.sync(() => output.push(`Hello, ${config.name}!`)).pipe(Effect.asVoid)) + * + * await Effect.runPromise( + * Command.runWith(greet, { version: "1.0.0" })(["--name", "Alice"]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Hello, Alice!"] * ``` * * @category models @@ -141,11 +166,11 @@ export interface Command /** - * Whether this command is hidden from parent help output, shell - * completions, and unknown-subcommand suggestions. Hidden commands still + * Whether this command is omitted from parent help output, shell + * completions, and unknown-subcommand suggestions. Unlisted commands still * parse and execute normally when invoked by exact name. */ - readonly hidden: boolean + readonly unlisted: boolean } /** @@ -194,19 +219,19 @@ export declare namespace Command { * * **Example** (Configuring command input) * - * ```ts + * ```ts import.meta.vitest * import { Argument, Flag } from "effect/unstable/cli" * import type { Command as CliCommand } from "effect/unstable/cli" * * // Simple flat configuration - * const simpleConfig: CliCommand.Command.Config = { + * const simpleConfig = { * name: Flag.string("name"), * age: Flag.integer("age"), * file: Argument.string("file") - * } + * } satisfies CliCommand.Command.Config * * // Nested configuration for organization - * const nestedConfig: CliCommand.Command.Config = { + * const nestedConfig = { * user: { * name: Flag.string("name"), * email: Flag.string("email") @@ -215,7 +240,9 @@ export declare namespace Command { * host: Flag.string("host"), * port: Flag.integer("port") * } - * } + * } satisfies CliCommand.Command.Config + * + * [simpleConfig.name.kind, nestedConfig.server.port.kind] // => ["flag", "flag"] * ``` * * @category models @@ -261,7 +288,7 @@ export declare namespace Command { * * **Example** (Inferring command input) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * import type { Command as CliCommand } from "effect/unstable/cli" * @@ -281,6 +308,12 @@ export declare namespace Command { * // readonly port: number * // } * // } + * + * const inferred: Result = { + * name: "Alice", + * server: { host: "localhost", port: 8080 } + * } + * inferred // => { name: "Alice", server: { host: "localhost", port: 8080 } } * ``` * * @category models @@ -320,7 +353,7 @@ export declare namespace Command { readonly commands: NonEmptyReadonlyArray }> readonly annotations: Context.Context - readonly hidden: boolean + readonly unlisted: boolean } /** @@ -399,9 +432,27 @@ export type Services = C extends Command< * * **Example** (Accessing parent command context) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const parent = Command.make("app").pipe( * Command.withSharedFlags({ @@ -410,19 +461,31 @@ export type Services = C extends Command< * }) * ) * + * const output: Array = [] * const child = Command.make("deploy", { * target: Flag.string("target") * }, (config) => * Effect.gen(function*() { * // Access parent's config by yielding the parent command * const parentConfig = yield* parent - * yield* Console.log(`Verbose: ${parentConfig.verbose}`) - * yield* Console.log(`Config: ${parentConfig.config}`) - * yield* Console.log(`Target: ${config.target}`) + * yield* Effect.sync(() => output.push(`Verbose: ${parentConfig.verbose}`)) + * yield* Effect.sync(() => output.push(`Config: ${parentConfig.config}`)) + * yield* Effect.sync(() => output.push(`Target: ${config.target}`)) * })) * * const app = parent.pipe(Command.withSubcommands([child])) - * // Usage: app --verbose --config prod.json deploy --target staging + * + * await Effect.runPromise( + * Command.runWith(app, { version: "1.0.0" })([ + * "--verbose", + * "--config", + * "prod.json", + * "deploy", + * "--target", + * "staging" + * ]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Verbose: true", "Config: prod.json", "Target: staging"] * ``` * * @category models @@ -478,9 +541,27 @@ export const isCommand = (u: unknown): u is Command.Any => Predicate.hasProperty * * **Example** (Creating commands) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Argument, Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * // Simple command with no configuration * const version = Command.make("version") @@ -505,19 +586,29 @@ export const isCommand = (u: unknown): u is Command.Any => Predicate.hasProperty * }) * * // Command with handler + * const output: Array = [] * const deployWithHandler = Command.make("deploy", { * environment: Flag.string("env"), * force: Flag.boolean("force") * }, (config) => * Effect.gen(function*() { - * yield* Console.log(`Starting deployment to ${config.environment}`) + * yield* Effect.sync(() => output.push(`Starting deployment to ${config.environment}`)) * * if (!config.force && config.environment === "production") { * return yield* Effect.fail("Production deployments require --force flag") * } * - * yield* Console.log("Deployment completed successfully") + * yield* Effect.sync(() => output.push("Deployment completed successfully")) * })) + * + * await Effect.runPromise( + * Command.runWith(deployWithHandler, { version: "1.0.0" })([ + * "--env", + * "staging", + * "--force" + * ]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Starting deployment to staging", "Deployment completed successfully"] * ``` * * @category constructors @@ -558,9 +649,27 @@ export const make: { * * **Example** (Adding command handlers) * - * ```ts - * import { Console } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * // Command without initial handler * const greet = Command.make("greet", { @@ -568,11 +677,19 @@ export const make: { * }) * * // Add handler later + * const output: Array = [] * const greetWithHandler = greet.pipe( * Command.withHandler((config: { readonly name: string }) => - * Console.log(`Hello, ${config.name}!`) + * Effect.sync(() => output.push(`Hello, ${config.name}!`)).pipe(Effect.asVoid) + * ) + * ) + * + * await Effect.runPromise( + * Command.runWith(greetWithHandler, { version: "1.0.0" })(["--name", "Alice"]).pipe( + * Effect.provide(CliTestLayer) * ) * ) + * output // => ["Hello, Alice!"] * ``` * * @category combinators @@ -656,9 +773,27 @@ const normalizeSubcommandEntries = ( * * **Example** (Adding subcommands) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * // Parent command with shared flags * const git = Command.make("git").pipe( @@ -668,19 +803,29 @@ const normalizeSubcommandEntries = ( * ) * * // Subcommand that accesses parent config + * const output: Array = [] * const clone = Command.make("clone", { * repository: Flag.string("repo") * }, (config) => * Effect.gen(function*() { * const parent = yield* git // Access parent's parsed config * if (parent.verbose) { - * yield* Console.log("Verbose mode enabled") + * yield* Effect.sync(() => output.push("Verbose mode enabled")) * } - * yield* Console.log(`Cloning ${config.repository}`) + * yield* Effect.sync(() => output.push(`Cloning ${config.repository}`)) * })) * * const app = git.pipe(Command.withSubcommands([clone])) - * // Usage: git --verbose clone --repo github.com/foo/bar + * + * await Effect.runPromise( + * Command.runWith(app, { version: "1.0.0" })([ + * "--verbose", + * "clone", + * "--repo", + * "github.com/foo/bar" + * ]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Verbose mode enabled", "Cloning github.com/foo/bar"] * ``` * * @category combinators @@ -755,7 +900,7 @@ export const withSubcommands: { const context = yield* impl.parseContext(raw) const result = yield* sub.parse(raw.subcommand.value.parsedInput) - return Object.assign({}, context, { [SubcommandStateSymbol]: { name: sub.name, result } }) as NextInput + return { ...context, [SubcommandStateSymbol]: { name: sub.name, result } } as NextInput }) const handle = Effect.fnUntraced(function*(input: NextInput, path: ReadonlyArray) { @@ -781,6 +926,7 @@ export const withSubcommands: { description: impl.description, shortDescription: impl.shortDescription, alias: impl.alias, + unlisted: impl.unlisted, annotations: impl.annotations, globalFlags: impl.globalFlags, examples: impl.examples, @@ -852,20 +998,20 @@ export const withSharedFlags: { type NextInput = Simplify type NextContextInput = Simplify - const parseShared = makeParser(sharedConfig) as ( + const parseShared = makeParser(sharedConfig, { allowLeftovers: true }) as ( input: ParsedTokens ) => Effect.Effect const parse = Effect.fnUntraced(function*(raw: ParsedTokens) { const base = yield* impl.parse(raw) const shared = yield* parseShared(raw) - return Object.assign({}, base, shared) as NextInput + return { ...(base as object), ...(shared as object) } as NextInput }) const parseContext = Effect.fnUntraced(function*(raw: ParsedTokens) { const base = yield* impl.parseContext(raw) const shared = yield* parseShared(raw) - return Object.assign({}, base, shared) as NextContextInput + return { ...(base as object), ...(shared as object) } as NextContextInput }) const handle = ( @@ -880,6 +1026,7 @@ export const withSharedFlags: { description: impl.description, shortDescription: impl.shortDescription, alias: impl.alias, + unlisted: impl.unlisted, annotations: impl.annotations, globalFlags: impl.globalFlags, examples: impl.examples, @@ -960,18 +1107,42 @@ type ExtractSubcommandContext> * * **Example** (Setting descriptions) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * + * const output: Array = [] * const deploy = Command.make("deploy", { * environment: Flag.string("env") * }, (config) => * Effect.gen(function*() { - * yield* Console.log(`Deploying to ${config.environment}`) + * yield* Effect.sync(() => output.push(`Deploying to ${config.environment}`)) * })).pipe( * Command.withDescription("Deploy the application to a specified environment") * ) + * + * await Effect.runPromise( + * Command.runWith(deploy, { version: "1.0.0" })(["--env", "staging"]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Deploying to staging"] * ``` * * @category combinators @@ -1040,7 +1211,7 @@ export const withAlias: { ) => makeCommand({ ...toImpl(self), alias })) /** - * Hides a subcommand from parent help output, shell completions, and + * Omits a subcommand from parent help output, shell completions, and * "did you mean?" suggestions while keeping it fully invocable by exact name. * * **When to use** @@ -1048,29 +1219,31 @@ export const withAlias: { * Use when experimental or internal subcommands should be accepted but not advertised on * the public CLI surface. * - * **Example** (Hiding a subcommand) + * **Example** (Unlisting a subcommand) * - * ```ts + * ```ts import.meta.vitest * import { Command } from "effect/unstable/cli" * * // `experimental` still runs when invoked as `mycli experimental`, * // but it does not appear under SUBCOMMANDS in `mycli --help`. * const experimental = Command.make("experimental").pipe( - * Command.withHidden + * Command.unlisted * ) * * const root = Command.make("mycli").pipe( * Command.withSubcommands([experimental]) * ) + * + * root.subcommands[0].commands[0].unlisted // => true * ``` * * @category combinators * @since 4.0.0 */ -export const withHidden = ( +export const unlisted = ( self: Command ): Command => - makeCommand({ ...toImpl(self), hidden: true }) as Command + makeCommand({ ...toImpl(self), unlisted: true }) as Command /** * Adds a custom annotation to a command. @@ -1166,7 +1339,7 @@ export const annotateMerge: { * * **Example** (Adding usage examples) * - * ```ts + * ```ts import.meta.vitest * import { Command } from "effect/unstable/cli" * * const login = Command.make("login").pipe( @@ -1175,6 +1348,8 @@ export const annotateMerge: { * { command: "myapp login --token sbp_abc123", description: "Log in with a token" } * ]) * ) + * + * login.examples.map((example) => example.command) // => ["myapp login", "myapp login --token sbp_abc123"] * ``` * * @category combinators @@ -1212,16 +1387,35 @@ const mapHandler = ( * * **Example** (Providing command services) * - * ```ts - * import { Effect, FileSystem, PlatformError } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, PlatformError, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * + * const output: Array = [] * const deploy = Command.make("deploy", { * env: Flag.string("env") * }, (config) => * Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem - * // Use fs... + * yield* Effect.sync(() => output.push(`Using file system for ${config.env}`)) * })).pipe( * // Provide FileSystem based on the --env flag * Command.provide((config) => @@ -1238,6 +1432,11 @@ const mapHandler = ( * }) * ) * ) + * + * await Effect.runPromise( + * Command.runWith(deploy, { version: "1.0.0" })(["--env", "local"]).pipe(Effect.provide(CliTestLayer)) + * ) + * output // => ["Using file system for local"] * ``` * * @category providing services @@ -1381,19 +1580,42 @@ export const provideEffectDiscard: { * * **Example** (Constructing command arguments) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Console, Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const command = Command.make("app") - * - * const program = Effect.gen(function*() { - * const args = yield* Command.wizard(command) - * yield* Console.log(args.join(" ")) + * const silentConsole: Console.Console = Object.assign(Object.create(console), { + * log: () => {} * }) + * + * const program = Command.wizard(command).pipe( + * Effect.provideService(Console.Console, silentConsole), + * Effect.provide(CliTestLayer) + * ) + * + * await Effect.runPromise(program) // => ["app"] * ``` * - * @category command execution + * @category running * @since 4.0.0 */ export const wizard = ( @@ -1444,18 +1666,26 @@ const getOutOfScopeGlobalFlagErrors = ( const showHelp = ( command: Command, - error: CliError.ShowHelp + error: CliError.ShowHelp, + renderErrors: boolean ): Effect.Effect => Effect.gen(function*() { const { builtIns } = yield* CliConfig.CliConfig const formatter = yield* CliOutput.Formatter const helpDoc = yield* getHelpForCommandPath(command, error.commandPath, builtIns) yield* Console.log(formatter.formatHelpDoc(helpDoc)) - if (error.errors.length > 0) { + if (renderErrors && error.errors.length > 0) { yield* Console.error(formatter.formatErrors(error.errors as any)) } }) +const showUserError = (error: CliError.UserError): Effect.Effect => + Effect.gen(function*() { + const formatter = yield* CliOutput.Formatter + yield* Console.error(formatter.formatError(error)) + error[Runtime.errorReported] = false + }) + /** * Runs a command using the arguments supplied by the `Stdio` service. * @@ -1464,33 +1694,63 @@ const showHelp = ( * Use when command-line arguments should come from `Stdio` at the application * entry point. * + * Help documents are always rendered. By default, parse error details and + * `CliError.UserError` failures are also rendered with the installed + * `CliOutput.Formatter` before the error is rethrown. Set `renderErrors` to + * `false` when the host application owns error rendering. + * * **Example** (Running commands with standard input) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({ + * args: Effect.succeed(["--name", "Alice"]) + * }), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * + * const output: Array = [] * const greetCommand = Command.make("greet", { * name: Flag.string("name") * }, (config) => * Effect.gen(function*() { - * yield* Console.log(`Hello, ${config.name}!`) + * yield* Effect.sync(() => output.push(`Hello, ${config.name}!`)) * })) * * // Automatically gets args from the Stdio service * const program = Command.run(greetCommand, { * version: "1.0.0" * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) + * output // => ["Hello, Alice!"] * ``` * * @see {@link runWith} for running a command with an explicit argument array * - * @category command execution + * @category running * @since 4.0.0 */ export const run: { (config: { readonly version: string + readonly renderErrors?: boolean | undefined }): ( command: Command ) => Effect.Effect @@ -1498,12 +1758,14 @@ export const run: { command: Command, config: { readonly version: string + readonly renderErrors?: boolean | undefined } ): Effect.Effect } = dual(2, ( command: Command, config: { readonly version: string + readonly renderErrors?: boolean | undefined } ) => Stdio.Stdio.use(({ args }) => @@ -1521,19 +1783,43 @@ export const run: { * Use when you need to test CLI applications or programmatically execute * commands with specific arguments. * + * Help documents are always rendered. By default, parse error details and + * `CliError.UserError` failures are also rendered with the installed + * `CliOutput.Formatter` before the error is rethrown. Set `renderErrors` to + * `false` when the host application owns error rendering. + * * **Example** (Running commands with explicit arguments) * - * ```ts - * import { Console, Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Command, Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * + * const output: Array = [] * const greet = Command.make("greet", { * name: Flag.string("name"), * count: Flag.integer("count").pipe(Flag.withDefault(1)) * }, (config) => * Effect.gen(function*() { * for (let i = 0; i < config.count; i++) { - * yield* Console.log(`Hello, ${config.name}!`) + * yield* Effect.sync(() => output.push(`Hello, ${config.name}!`)) * } * })) * @@ -1541,24 +1827,21 @@ export const run: { * const testProgram = Effect.gen(function*() { * const runCommand = Command.runWith(greet, { version: "1.0.0" }) * - * // Test normal execution * yield* runCommand(["--name", "Alice", "--count", "2"]) - * - * // Test help display - * yield* runCommand(["--help"]) - * - * // Test version display - * yield* runCommand(["--version"]) * }) + * + * await Effect.runPromise(testProgram.pipe(Effect.provide(CliTestLayer))) + * output // => ["Hello, Alice!", "Hello, Alice!"] * ``` * - * @category command execution + * @category running * @since 4.0.0 */ export const runWith = ( command: Command, config: { readonly version: string + readonly renderErrors?: boolean | undefined } ): ( input: ReadonlyArray @@ -1625,7 +1908,7 @@ export const runWith = ( })) if (shouldRun) { yield* Console.log() - yield* runWith(command, config)(wizardArgs.slice(1)) + yield* runWith(command, { ...config, renderErrors: false })(wizardArgs.slice(1)) } }).pipe( Effect.catchTag("QuitError", () => Console.log(Wizard.renderQuit())) @@ -1673,7 +1956,14 @@ export const runWith = ( CliError.isCliError(error) && error._tag === "ShowHelp" ? Result.succeed(error) : Result.fail(error), - (error) => Effect.andThen(showHelp(command, error), Effect.fail(error)) + (error) => Effect.andThen(showHelp(command, error, config.renderErrors !== false), Effect.fail(error)) + ), + Effect.catchFilter( + (error) => + config.renderErrors !== false && CliError.isCliError(error) && error._tag === "UserError" + ? Result.succeed(error) + : Result.fail(error), + (error) => Effect.andThen(showUserError(error), Effect.fail(error)) ), Effect.catchFilter( (e) => diff --git a/.context/effect/packages/effect/src/unstable/cli/Flag.ts b/.context/effect/packages/effect/src/unstable/cli/Flag.ts index e4c683220..e467479be 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Flag.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Flag.ts @@ -43,11 +43,12 @@ export interface Flag extends Param.Param {} * * **Example** (Creating string flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const nameFlag = Flag.string("name") * // Usage: --name "John Doe" + * nameFlag.kind // => "flag" * ``` * * @category constructors @@ -60,11 +61,12 @@ export const string = (name: string): Flag => Param.string(Param.flagKin * * **Example** (Creating boolean flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const verboseFlag = Flag.boolean("verbose") * // Usage: --verbose (true) or --no-verbose (false) + * verboseFlag.kind // => "flag" * ``` * * @category constructors @@ -77,11 +79,12 @@ export const boolean = (name: string): Flag => Param.boolean(Param.flag * * **Example** (Creating integer flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const portFlag = Flag.integer("port") * // Usage: --port 8080 + * portFlag.kind // => "flag" * ``` * * @category constructors @@ -94,11 +97,12 @@ export const integer = (name: string): Flag => Param.integer(Param.flagK * * **Example** (Creating float flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const rateFlag = Flag.float("rate") * // Usage: --rate 3.14 + * rateFlag.kind // => "flag" * ``` * * @category constructors @@ -111,11 +115,12 @@ export const float = (name: string): Flag => Param.float(Param.flagKind, * * **Example** (Creating date flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const startDateFlag = Flag.date("start-date") * // Usage: --start-date 2023-12-25 + * startDateFlag.kind // => "flag" * ``` * * @category constructors @@ -129,7 +134,7 @@ export const date = (name: string): Flag => Param.date(Param.flagKind, nam * * **Example** (Creating flag choices with values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // simple enum like choice mapping directly to string union @@ -141,6 +146,7 @@ export const date = (name: string): Flag => Param.date(Param.flagKind, nam * ["info", "Info" as const], * ["error", "Error" as const] * ]) + * const kinds = [color.kind, logLevel.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -179,7 +185,7 @@ export const choice = >( * * **Example** (Creating path flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Basic path flag @@ -196,6 +202,7 @@ export const choice = >( * pathType: "directory", * typeName: "OUTPUT_DIRECTORY" * }) + * const kinds = [pathFlag.kind, fileFlag.kind, dirFlag.kind] // => ["flag", "flag", "flag"] * ``` * * @category constructors @@ -212,7 +219,7 @@ export const path = (name: string, options?: { * * **Example** (Creating file flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Basic file flag @@ -222,6 +229,7 @@ export const path = (name: string, options?: { * // File that must exist * const configFlag = Flag.file("config", { mustExist: true }) * // Usage: --config ./config.yaml (file must exist) + * const kinds = [inputFlag.kind, configFlag.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -236,7 +244,7 @@ export const file = (name: string, options?: { * * **Example** (Creating directory flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Basic directory flag @@ -246,6 +254,7 @@ export const file = (name: string, options?: { * // Directory that must exist * const sourceFlag = Flag.directory("source", { mustExist: true }) * // Usage: --source ./src (directory must exist) + * const kinds = [outputFlag.kind, sourceFlag.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -266,20 +275,39 @@ export const directory = (name: string, options?: { * * **Example** (Creating redacted flags) * - * ```ts - * import { Effect, Redacted } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Redacted, Stdio, Terminal } from "effect" * import { Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const passwordFlag = Flag.redacted("password") * * const program = Effect.gen(function*() { - * const [leftover, password] = yield* passwordFlag.parse({ + * const [, password] = yield* passwordFlag.parse({ * arguments: [], * flags: { "password": ["abc123"] } * }) - * const value = Redacted.value(password) // Access the underlying value - * console.log("Password length:", value.length) + * return Redacted.value(password).length * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) // => 6 * ``` * * @category constructors @@ -292,11 +320,12 @@ export const redacted = (name: string): Flag> => Param * * **Example** (Reading file text) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const config = Flag.fileText("config-file") * // --config-file ./app.json will read the file content + * config.kind // => "flag" * ``` * * @category constructors @@ -314,7 +343,7 @@ export const fileText = (name: string): Flag => Param.fileText(Param.fla * * **Example** (Parsing file contents) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Will use the extension of the file passed on the command line to determine @@ -323,6 +352,7 @@ export const fileText = (name: string): Flag => Param.fileText(Param.fla * * // Will use the JSON parser * const jsonConfig = Flag.fileParse("json-config", { format: "json" }) + * const kinds = [config.kind, jsonConfig.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -339,7 +369,7 @@ export const fileParse = ( * * **Example** (Validating file contents) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Flag } from "effect/unstable/cli" * @@ -349,6 +379,7 @@ export const fileParse = ( * }) * * const config = Flag.fileSchema("config", ConfigSchema, { format: "json" }) + * config.kind // => "flag" * ``` * * @category constructors @@ -375,11 +406,12 @@ export const fileSchema = ( * * **Example** (Parsing key-value pairs) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const env = Flag.keyValuePair("env") * // --env FOO=bar --env BAZ=qux will parse to { FOO: "bar", BAZ: "qux" } + * env.kind // => "flag" * ``` * * @category constructors @@ -393,14 +425,14 @@ export const keyValuePair = (name: string): Flag> => Para * * **Example** (Creating sentinel flags) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const makeValueFlag = (includeValue: boolean) => * includeValue ? Flag.string("value") : Flag.none * - * console.log(makeValueFlag(true) === Flag.none) // false - * console.log(makeValueFlag(false) === Flag.none) // true + * makeValueFlag(true) === Flag.none // => false + * makeValueFlag(false) === Flag.none // => true * ``` * * @category constructors @@ -417,7 +449,7 @@ export const none: Flag = Param.none(Param.flagKind) * * **Example** (Adding flag aliases) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Flag can be used as both --verbose and -v @@ -430,6 +462,7 @@ export const none: Flag = Param.none(Param.flagKind) * Flag.withAlias("h"), * Flag.withAlias("?") * ) + * const kinds = [verboseFlag.kind, helpFlag.kind] // => ["flag", "flag"] * ``` * * @category aliasing @@ -445,7 +478,7 @@ export const withAlias: { * * **Example** (Adding help descriptions) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const portFlag = Flag.integer("port").pipe( @@ -455,9 +488,10 @@ export const withAlias: { * const configFlag = Flag.file("config").pipe( * Flag.withDescription("Path to the configuration file") * ) + * const kinds = [portFlag.kind, configFlag.kind] // => ["flag", "flag"] * ``` * - * @category help documentation + * @category metadata * @since 4.0.0 */ export const withDescription: { @@ -479,7 +513,7 @@ export const withDescription: { * * **Example** (Setting metavars) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const databaseFlag = Flag.string("database-url").pipe( @@ -492,6 +526,7 @@ export const withDescription: { * Flag.withMetavar("SECONDS") * ) * // In help: --timeout SECONDS + * const kinds = [databaseFlag.kind, timeoutFlag.kind] // => ["flag", "flag"] * ``` * * @category metadata @@ -514,13 +549,14 @@ export const withMetavar: { * * **Example** (Hiding a flag from help) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Flag still parses --experimental-foo, but it does not appear in --help. * const experimental = Flag.boolean("experimental-foo").pipe( * Flag.withHidden * ) + * experimental.kind // => "flag" * ``` * * @category metadata @@ -533,23 +569,39 @@ export const withHidden = (self: Flag): Flag => Param.withHidden(self) * * **Example** (Making flags optional) * - * ```ts - * import { Effect, Option } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Option, Path, Stdio, Terminal } from "effect" * import { Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const optionalPort = Flag.optional(Flag.integer("port")) * * const program = Effect.gen(function*() { - * const [leftover, port] = yield* optionalPort.parse({ + * const [, port] = yield* optionalPort.parse({ * arguments: [], * flags: { "port": ["4000"] } * }) - * if (Option.isSome(port)) { - * console.log("Port specified:", port.value) - * } else { - * console.log("No port specified, using default") - * } + * return port * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) // => Option.some(4000) * ``` * * @category optionality @@ -562,7 +614,7 @@ export const optional = (param: Flag): Flag> => Param.opt * * **Example** (Providing default values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const portFlag = Flag.integer("port").pipe( @@ -574,6 +626,7 @@ export const optional = (param: Flag): Flag> => Param.opt * Flag.withDefault("localhost") * ) * // If --host is not provided, defaults to "localhost" + * const kinds = [portFlag.kind, hostFlag.kind] // => ["flag", "flag"] * ``` * * @category optionality @@ -589,13 +642,14 @@ export const withDefault: { * * **Example** (Falling back to config) * - * ```ts + * ```ts import.meta.vitest * import { Config } from "effect" * import { Flag } from "effect/unstable/cli" * * const verbose = Flag.boolean("verbose").pipe( * Flag.withFallbackConfig(Config.boolean("VERBOSE")) * ) + * verbose.kind // => "flag" * ``` * * @category combinators @@ -611,12 +665,13 @@ export const withFallbackConfig: { * * **Example** (Falling back to prompts) * - * ```ts + * ```ts import.meta.vitest * import { Flag, Prompt } from "effect/unstable/cli" * * const name = Flag.string("name").pipe( * Flag.withFallbackPrompt(Prompt.text({ message: "Name" })) * ) + * name.kind // => "flag" * ``` * * @category combinators @@ -632,7 +687,7 @@ export const withFallbackPrompt: { * * **Example** (Mapping parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Convert string to uppercase @@ -644,6 +699,7 @@ export const withFallbackPrompt: { * const urlFlag = Flag.integer("port").pipe( * Flag.map((port) => `http://localhost:${port}`) * ) + * const kinds = [nameFlag.kind, urlFlag.kind] // => ["flag", "flag"] * ``` * * @category mapping @@ -659,18 +715,39 @@ export const map: { * * **Example** (Mapping parsed values effectfully) * - * ```ts - * import { Effect, FileSystem } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) + * + * const upperName = Flag.string("name").pipe( + * Flag.mapEffect((name) => Effect.succeed(name.toUpperCase())) + * ) * - * // Read file size from path flag - * const fileSizeFlag = Flag.file("input").pipe( - * Flag.mapEffect(Effect.fnUntraced(function*(path) { - * const fs = yield* FileSystem.FileSystem - * const stats = yield* Effect.orDie(fs.stat(path)) - * return stats.size - * })) + * const [, value] = await Effect.runPromise( + * upperName.parse({ + * arguments: [], + * flags: { name: ["alice"] } + * }).pipe(Effect.provide(CliTestLayer)) * ) + * value // => "ALICE" * ``` * * @category mapping @@ -694,8 +771,27 @@ export const mapEffect: { * * **Example** (Mapping thrown errors) * - * ```ts + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * // Parse JSON string with error handling * const jsonFlag = Flag.string("config").pipe( @@ -712,6 +808,14 @@ export const mapEffect: { * (error) => `Invalid URL: ${error}` * ) * ) + * + * const [, value] = await Effect.runPromise( + * jsonFlag.parse({ + * arguments: [], + * flags: { config: ['{"enabled":true}'] } + * }).pipe(Effect.provide(CliTestLayer)) + * ) + * value // => { enabled: true } * ``` * * @category mapping @@ -731,7 +835,7 @@ export const mapTryCatch: { * * **Example** (Requiring repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const sourceFlag = Flag.atLeast(Flag.file("source"), 2) @@ -742,6 +846,7 @@ export const mapTryCatch: { * Flag.atLeast(1) * ) * // Requires at least 1 tag + * const kinds = [sourceFlag.kind, tagFlag.kind] // => ["flag", "flag"] * ``` * * @category repetition @@ -757,7 +862,7 @@ export const atLeast: { * * **Example** (Limiting repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const warningFlag = Flag.atMost(Flag.string("warning"), 3) @@ -768,6 +873,7 @@ export const atLeast: { * Flag.atMost(1) * ) * // Allows at most 1 debug flag + * const kinds = [warningFlag.kind, debugFlag.kind] // => ["flag", "flag"] * ``` * * @category repetition @@ -783,7 +889,7 @@ export const atMost: { * * **Example** (Bounding repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * const hostFlag = Flag.between(Flag.string("host"), 1, 3) @@ -794,6 +900,7 @@ export const atMost: { * Flag.between(0, 5) * ) * // Allows 0-5 exclude patterns + * const kinds = [hostFlag.kind, excludeFlag.kind] // => ["flag", "flag"] * ``` * * @category repetition @@ -809,7 +916,7 @@ export const between: { * * **Example** (Filtering and transforming values) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * import { Flag } from "effect/unstable/cli" * @@ -828,6 +935,7 @@ export const between: { * (email) => `Invalid email address: ${email}` * ) * ) + * const kinds = [positiveInt.kind, emailFlag.kind] // => ["flag", "flag"] * ``` * * @category filtering @@ -847,7 +955,7 @@ export const filterMap: { * * **Example** (Filtering parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Ensure port is in valid range @@ -865,6 +973,7 @@ export const filterMap: { * () => "Name cannot be empty" * ) * ) + * const kinds = [portFlag.kind, nameFlag.kind] // => ["flag", "flag"] * ``` * * @category filtering @@ -884,7 +993,7 @@ export const filter: { * * **Example** (Falling back to another flag) * - * ```ts + * ```ts import.meta.vitest * import { Flag } from "effect/unstable/cli" * * // Try parsing as integer, fallback to string @@ -898,6 +1007,7 @@ export const filter: { * Flag.file("config"), * () => Flag.string("config-url") * ) + * const kinds = [valueFlag.kind, configFlag.kind] // => ["flag", "flag"] * ``` * * @category alternatives @@ -913,27 +1023,42 @@ export const orElse: { * * **Example** (Returning fallback results) * - * ```ts - * import { Effect, Result } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Result, Stdio, Terminal } from "effect" * import { Flag } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * // Try file path, fallback to URL * const sourceFlag = Flag.orElseResult( - * Flag.file("source"), + * Flag.string("source"), * () => Flag.string("source-url") * ) * * const program = Effect.gen(function*() { - * const [leftover, source] = yield* sourceFlag.parse({ + * const [, source] = yield* sourceFlag.parse({ * arguments: [], - * flags: { "source-url": ["https://google.com"] } + * flags: { "source-url": ["https://example.com"] } * }) - * if (Result.isSuccess(source)) { - * console.log("Using file:", source.success) - * } else { - * console.log("Using URL:", source.failure) - * } + * return source * }) + * + * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) // => Result.fail("https://example.com") * ``` * * @category alternatives @@ -949,7 +1074,7 @@ export const orElseResult: { * * **Example** (Validating with schemas) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Flag } from "effect/unstable/cli" * @@ -976,6 +1101,7 @@ export const orElseResult: { * const configFlag = Flag.string("config").pipe( * Flag.withSchema(ConfigSchema) * ) + * const kinds = [emailFlag.kind, configFlag.kind] // => ["flag", "flag"] * ``` * * @category schemas diff --git a/.context/effect/packages/effect/src/unstable/cli/GlobalFlag.ts b/.context/effect/packages/effect/src/unstable/cli/GlobalFlag.ts index a300d4f41..d9c1484af 100644 --- a/.context/effect/packages/effect/src/unstable/cli/GlobalFlag.ts +++ b/.context/effect/packages/effect/src/unstable/cli/GlobalFlag.ts @@ -58,7 +58,7 @@ export interface Action { /** * Setting flag: configure command handler's environment (--log-level, --config). * - * @category models + * @category services * @since 4.0.0 */ export interface Setting extends Context.Service, A> { diff --git a/.context/effect/packages/effect/src/unstable/cli/HelpDoc.ts b/.context/effect/packages/effect/src/unstable/cli/HelpDoc.ts index e1dc75846..6618c91a7 100644 --- a/.context/effect/packages/effect/src/unstable/cli/HelpDoc.ts +++ b/.context/effect/packages/effect/src/unstable/cli/HelpDoc.ts @@ -21,7 +21,7 @@ import type * as Option from "../../Option.ts" * * **Example** (Defining command help documentation) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option as O } from "effect" * import type { HelpDoc } from "effect/unstable/cli" * @@ -55,6 +55,8 @@ import type * as Option from "../../Option.ts" * } * ] * } + * + * deployCommandHelp.usage // => "myapp deploy [options] " * ``` * * @category models @@ -126,7 +128,7 @@ export interface ExampleDoc { * * **Example** (Documenting command flags) * - * ```ts + * ```ts import.meta.vitest * import { Option as O } from "effect" * import type { HelpDoc } from "effect/unstable/cli" * @@ -145,6 +147,8 @@ export interface ExampleDoc { * description: O.some("Port number to use"), * required: true * } + * + * const names = [verboseFlag.name, portFlag.name] // => ["verbose", "port"] * ``` * * @category models @@ -182,7 +186,7 @@ export interface FlagDoc { * * **Example** (Documenting subcommands) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option as O } from "effect" * import type { HelpDoc } from "effect/unstable/cli" * @@ -211,6 +215,8 @@ export interface FlagDoc { * commands: [deploySubcommand, buildSubcommand] * }] * } + * + * mainCommandHelp.subcommands?.[0].commands.map((command) => command.name) // => ["deploy", "build"] * ``` * * @category models @@ -262,7 +268,7 @@ export interface SubcommandGroupDoc { * * **Example** (Documenting positional arguments) * - * ```ts + * ```ts import.meta.vitest * import { Context, Option as O } from "effect" * import type { HelpDoc } from "effect/unstable/cli" * @@ -290,6 +296,8 @@ export interface SubcommandGroupDoc { * flags: [], * args: [sourceArg, filesArg] * } + * + * copyCommandHelp.args?.map((arg) => arg.name) // => ["source", "files"] * ``` * * @category models diff --git a/.context/effect/packages/effect/src/unstable/cli/Param.ts b/.context/effect/packages/effect/src/unstable/cli/Param.ts index 7f2e582e9..d9ed6ded9 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Param.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Param.ts @@ -12,7 +12,7 @@ */ import * as Config from "../../Config.ts" import * as Effect from "../../Effect.ts" -import { dual, identity } from "../../Function.ts" +import { dual, identity, type LazyArg } from "../../Function.ts" import * as Option from "../../Option.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" @@ -226,7 +226,11 @@ export interface Transform extends Para readonly _tag: "Transform" readonly kind: Kind readonly param: Param - readonly f: (parse: Parse) => Parse + readonly alternatives: ReadonlyArray>> + readonly f: ( + parse: Parse, + alternatives: ReadonlyArray>> + ) => Parse } /** @@ -272,17 +276,15 @@ const Proto = { * * **Example** (Checking for params) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const maybeParam = Param.string(Param.flagKind, "name") * - * if (Param.isParam(maybeParam)) { - * console.log("This is a Param") - * } + * Param.isParam(maybeParam) // => true * ``` * - * @category refinements + * @category guards * @since 4.0.0 */ export const isParam = (u: unknown): u is Param => Predicate.hasProperty(u, TypeId) @@ -292,17 +294,17 @@ export const isParam = (u: unknown): u is Param => Predicate.has * * **Example** (Checking for single params) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const nameParam = Param.string(Param.flagKind, "name") * const optionalParam = Param.optional(nameParam) * - * console.log(Param.isSingle(nameParam)) // true - * console.log(Param.isSingle(optionalParam)) // false + * Param.isSingle(nameParam) // => true + * Param.isSingle(optionalParam) // => false * ``` * - * @category refinements + * @category guards * @since 4.0.0 */ export const isSingle = ( @@ -343,14 +345,14 @@ export const makeSingle = (params: { params.kind === argumentKind ? parsePositional(params.name, params.primitiveType, args) : parseFlag(params.name, params.primitiveType, args) - return Object.assign(Object.create(Proto), { + return Object.setPrototypeOf({ _tag: "Single", ...params, description: params.description ?? Option.none(), aliases: params.aliases ?? [], hidden: params.hidden ?? false, parse - }) + }, Proto) } /** @@ -358,7 +360,7 @@ export const makeSingle = (params: { * * **Example** (Creating string parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create a string flag @@ -368,6 +370,7 @@ export const makeSingle = (params: { * const fileArg = Param.string(Param.argumentKind, "file") * * // Usage in CLI: --name "John Doe" or as positional argument + * const kinds = [nameFlag.kind, fileArg.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -388,7 +391,7 @@ export const string = ( * * **Example** (Creating boolean parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create a boolean flag @@ -399,6 +402,7 @@ export const string = ( * * // Usage in CLI: --verbose (defaults to true when present, false when absent) * // or as positional: true/false + * const kinds = [verboseFlag.kind, enableArg.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -419,7 +423,7 @@ export const boolean = ( * * **Example** (Creating integer parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create an integer flag @@ -429,6 +433,7 @@ export const boolean = ( * const countArg = Param.integer(Param.argumentKind, "count") * * // Usage in CLI: --port 8080 or as positional argument: 42 + * const kinds = [portFlag.kind, countArg.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -449,7 +454,7 @@ export const integer = ( * * **Example** (Creating float parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create a float flag @@ -459,6 +464,7 @@ export const integer = ( * const thresholdArg = Param.float(Param.argumentKind, "threshold") * * // Usage in CLI: --rate 0.95 or as positional argument: 3.14159 + * const kinds = [rateFlag.kind, thresholdArg.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -479,7 +485,7 @@ export const float = ( * * **Example** (Creating date parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create a date flag @@ -490,6 +496,7 @@ export const float = ( * * // Usage in CLI: --start-date "2023-12-25" or as positional: "2023-01-01" * // Parses to JavaScript Date object + * const kinds = [startFlag.kind, dueDateArg.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -511,7 +518,7 @@ export const date = ( * * **Example** (Creating valued choices) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * type Animal = Dog | Cat @@ -528,6 +535,7 @@ export const date = ( * ["dog", { _tag: "Dog" }], * ["cat", { _tag: "Cat" }] * ]) + * animal.kind // => "flag" * ``` * * @category constructors @@ -549,7 +557,7 @@ export const choiceWithValue = < * * **Example** (Creating string choices) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const logLevel = Param.choice(Param.flagKind, "log-level", [ @@ -558,6 +566,7 @@ export const choiceWithValue = < * "warn", * "error" * ]) + * logLevel.kind // => "flag" * ``` * * @category constructors @@ -576,7 +585,7 @@ export const choice = < * * **Example** (Creating path parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Basic path parameter @@ -591,6 +600,7 @@ export const choice = < * mustExist: true, * typeName: "config-file" * }) + * const kinds = [outputPath.kind, inputPath.kind, configFile.kind] // => ["flag", "flag", "flag"] * ``` * * @category constructors @@ -622,7 +632,7 @@ export const path = ( * * **Example** (Creating directory parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Basic directory parameter @@ -632,6 +642,7 @@ export const path = ( * const sourceDir = Param.directory(Param.flagKind, "source", { mustExist: true }) * * // Usage: --output-dir /path/to/dir --source /existing/dir + * const kinds = [outputDir.kind, sourceDir.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -660,7 +671,7 @@ export const directory = ( * * **Example** (Creating file parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Basic file parameter @@ -670,6 +681,7 @@ export const directory = ( * const inputFile = Param.file(Param.flagKind, "input", { mustExist: true }) * * // Usage: --output result.txt --input existing-file.txt + * const kinds = [outputFile.kind, inputFile.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -694,7 +706,7 @@ export const file = ( * * **Example** (Creating redacted parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create a password parameter @@ -704,6 +716,7 @@ export const file = ( * const apiKey = Param.redacted(Param.argumentKind, "api-key") * * // Usage: --password (value will be hidden in help/logs) + * const kinds = [password.kind, apiKey.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -724,7 +737,7 @@ export const redacted = ( * * **Example** (Reading file text) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Read a config file as string @@ -734,6 +747,7 @@ export const redacted = ( * const templateContent = Param.fileText(Param.argumentKind, "template") * * // Usage: --config config.txt (reads file content into string) + * const kinds = [configContent.kind, templateContent.kind] // => ["flag", "argument"] * ``` * * @category constructors @@ -756,7 +770,7 @@ export const fileText = (kind: Kind, name: string): Para * * **Example** (Parsing file contents) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Will use the extension of the file passed on the command line to determine @@ -767,6 +781,7 @@ export const fileText = (kind: Kind, name: string): Para * const jsonConfig = Param.fileParse(Param.flagKind, "json-config", { * format: "json" * }) + * const kinds = [config.kind, jsonConfig.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -788,7 +803,7 @@ export const fileParse = ( * * **Example** (Validating file contents) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Param } from "effect/unstable/cli" * // Parse JSON config file @@ -807,6 +822,7 @@ export const fileParse = ( * }) * * // Usage: --config config.json (reads and validates file content) + * const kinds = [config.kind, yamlConfig.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -839,7 +855,7 @@ export const fileSchema = ( * * **Example** (Parsing key-value pairs) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const env = Param.keyValuePair(Param.flagKind, "env") @@ -847,6 +863,7 @@ export const fileSchema = ( * * const props = Param.keyValuePair(Param.flagKind, "property") * // --property name=value --property debug=true + * const kinds = [env.kind, props.kind] // => ["flag", "flag"] * ``` * * @category constructors @@ -865,7 +882,7 @@ export const keyValuePair = ( }), { min: 1 } ), - (objects) => Object.assign({}, ...objects) + (objects) => Object.fromEntries(objects.flatMap(Object.entries)) ) /** @@ -878,7 +895,7 @@ export const keyValuePair = ( * * **Example** (Creating sentinel parameters) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const disabledDebugParam = Param.none(Param.flagKind) @@ -886,8 +903,8 @@ export const keyValuePair = ( * const makeDebugParam = (enableDebug: boolean) => * enableDebug ? Param.string(Param.flagKind, "debug") : disabledDebugParam * - * console.log(makeDebugParam(true) === disabledDebugParam) // false - * console.log(makeDebugParam(false) === disabledDebugParam) // true + * makeDebugParam(true) === disabledDebugParam // => false + * makeDebugParam(false) === disabledDebugParam // => true * ``` * * @category constructors @@ -917,7 +934,7 @@ const FLAG_DASH_REGEXP = /^-+/ * * **Example** (Adding parameter aliases) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const force = Param.boolean(Param.flagKind, "force").pipe( @@ -930,6 +947,7 @@ const FLAG_DASH_REGEXP = /^-+/ * Param.optional, * Param.withAlias("-c") // finds the underlying Single and adds alias * ) + * const kinds = [force.kind, count.kind] // => ["flag", "flag"] * ``` * * @category combinators @@ -956,13 +974,14 @@ export const withAlias: { * * **Example** (Adding help descriptions) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const verbose = Param.boolean(Param.flagKind, "verbose").pipe( * Param.withAlias("-v"), * Param.withDescription("Enable verbose output") * ) + * verbose.kind // => "flag" * ``` * * @category combinators @@ -990,12 +1009,13 @@ export const withDescription: { * * **Example** (Hiding a flag from help) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const experimental = Param.boolean(Param.flagKind, "experimental-foo").pipe( * Param.withHidden * ) + * experimental.kind // => "flag" * ``` * * @category metadata @@ -1013,12 +1033,13 @@ export const withHidden = (self: Param): Par * * **Example** (Mapping parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const port = Param.integer(Param.flagKind, "port").pipe( * Param.map((n) => ({ port: n, url: `http://localhost:${n}` })) * ) + * port.kind // => "flag" * ``` * * @category combinators @@ -1044,24 +1065,49 @@ export const map: { const transform = ( self: Param, - f: (parse: Parse) => Parse -) => - Object.assign(Object.create(Proto), { + f: ( + parse: Parse, + alternatives: ReadonlyArray>> + ) => Parse, + alternatives: ReadonlyArray>> = [] +): Transform => { + const alternativeParsers = alternatives.map((alternative) => () => alternative().parse) + return Object.assign(Object.create(Proto), { _tag: "Transform", kind: self.kind, param: self, + alternatives, f, - parse: f(self.parse) + parse: f(self.parse, alternativeParsers) }) +} /** * Transforms the parsed value of an option using an effectful mapping function. * * **Example** (Mapping parsed values effectfully) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { CliError, Param } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const validatedEmail = Param.string(Param.flagKind, "email").pipe( * Param.mapEffect((email) => @@ -1077,6 +1123,14 @@ const transform = ( * ) * ) * ) + * + * const [, value] = await Effect.runPromise( + * validatedEmail.parse({ + * arguments: [], + * flags: { email: ["alice@example.com"] } + * }).pipe(Effect.provide(CliTestLayer)) + * ) + * value // => "alice@example.com" * ``` * * @category combinators @@ -1109,8 +1163,27 @@ export const mapEffect: { * * **Example** (Mapping thrown errors) * - * ```ts + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Param } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const parsedJson = Param.string(Param.flagKind, "config").pipe( * Param.mapTryCatch( @@ -1119,6 +1192,14 @@ export const mapEffect: { * `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` * ) * ) + * + * const [, value] = await Effect.runPromise( + * parsedJson.parse({ + * arguments: [], + * flags: { config: ['{"enabled":true}'] } + * }).pipe(Effect.provide(CliTestLayer)) + * ) + * value // => { enabled: true } * ``` * * @category combinators @@ -1174,13 +1255,14 @@ export const mapTryCatch: { * * **Example** (Making parameters optional) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Create an optional port option * // - When not provided: returns Option.none() * // - When provided: returns Option.some(parsedValue) * const port = Param.optional(Param.integer(Param.flagKind, "port")) + * port.kind // => "flag" * ``` * * @category combinators @@ -1230,7 +1312,7 @@ export const optional = ( * * **Example** (Providing default values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Using the pipe operator to make an option optional @@ -1244,6 +1326,7 @@ export const optional = ( * Param.withDescription("Enable verbose output"), * Param.withDefault(false) * ) + * const kinds = [port.kind, verbose.kind] // => ["flag", "flag"] * ``` * * @category combinators @@ -1405,7 +1488,7 @@ export type VariadicParamOptions = { * * **Example** (Accepting multiple values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Basic variadic parameter (0 to infinity) @@ -1422,6 +1505,7 @@ export type VariadicParamOptions = { * min: 2, // at least 2 times * max: 2 // at most 2 times * }) + * const kinds = [tags.kind, inputs.kind, limited.kind] // => ["flag", "flag", "flag"] * ``` * * @category combinators @@ -1459,7 +1543,7 @@ export const variadic = ( * * **Example** (Bounding repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Allow 1-3 file inputs @@ -1478,6 +1562,7 @@ export const variadic = ( * * // Parse: --tag dev --tag staging --tag v1.0 * // Result: ["dev", "staging", "v1.0"] + * const kinds = [files.kind, tags.kind] // => ["flag", "flag"] * ``` * * @category combinators @@ -1507,7 +1592,7 @@ export const between: { * * **Example** (Limiting repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Allow at most 3 warning suppressions @@ -1517,6 +1602,7 @@ export const between: { * * // Parse: --suppress warning1 --suppress warning2 * // Result: ["warning1", "warning2"] + * suppressions.kind // => "flag" * ``` * * @category combinators @@ -1542,7 +1628,7 @@ export const atMost: { * * **Example** (Requiring repeated values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * // Require at least 2 input files @@ -1553,6 +1639,7 @@ export const atMost: { * * // Parse: --input file1.txt --input file2.txt --input file3.txt * // Result: ["file1.txt", "file2.txt", "file3.txt"] + * inputs.kind // => "flag" * ``` * * @category combinators @@ -1579,7 +1666,7 @@ export const atLeast: { * * **Example** (Filtering and transforming values) * - * ```ts + * ```ts import.meta.vitest * import { Option } from "effect" * import { Param } from "effect/unstable/cli" * const positiveInt = Param.integer(Param.flagKind, "count").pipe( @@ -1588,6 +1675,7 @@ export const atLeast: { * (n) => `Expected positive integer, got ${n}` * ) * ) + * positiveInt.kind // => "flag" * ``` * * @category combinators @@ -1630,7 +1718,7 @@ export const filterMap: { * * **Example** (Filtering parsed values) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const evenNumber = Param.integer(Param.flagKind, "num").pipe( @@ -1639,6 +1727,7 @@ export const filterMap: { * (n) => `Expected even number, got ${n}` * ) * ) + * evenNumber.kind // => "flag" * ``` * * @category combinators @@ -1670,7 +1759,7 @@ export const filter: { * * **Example** (Setting metavars) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const port = Param.integer(Param.flagKind, "port").pipe( @@ -1680,6 +1769,7 @@ export const filter: { * () => "Port must be between 1 and 65535" * ) * ) + * port.kind // => "flag" * ``` * * @category metadata @@ -1703,7 +1793,7 @@ export const withMetavar: { * * **Example** (Validating with schemas) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Param } from "effect/unstable/cli" * const isEmail = Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) @@ -1715,6 +1805,7 @@ export const withMetavar: { * const email = Param.string(Param.flagKind, "email").pipe( * Param.withSchema(Email) * ) + * email.kind // => "flag" * ``` * * @category combinators @@ -1752,32 +1843,33 @@ export const withSchema: { * * **Example** (Falling back to another parameter) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const config = Param.file(Param.flagKind, "config").pipe( * Param.orElse(() => Param.string(Param.flagKind, "config-url")) * ) + * config.kind // => "flag" * ``` * * @category combinators * @since 4.0.0 */ export const orElse: { - ( - orElse: (error: CliError.CliError) => Param - ): (self: Param) => Param + (orElse: LazyArg>): (self: Param) => Param ( self: Param, - orElse: (error: CliError.CliError) => Param + orElse: LazyArg> ): Param } = dual(2, ( self: Param, - orElse: (error: CliError.CliError) => Param + orElse: LazyArg> ) => transform( self, - (parse: Parse): Parse => (args: ParsedArgs) => Effect.catch(parse(args), (err) => orElse(err).parse(args)) + (parse: Parse, alternatives): Parse => (args: ParsedArgs) => + Effect.catch(parse(args), () => (alternatives[0]!() as Parse)(args)), + [orElse] )) /** @@ -1791,13 +1883,14 @@ export const orElse: { * * **Example** (Returning fallback results) * - * ```ts + * ```ts import.meta.vitest * import { Param } from "effect/unstable/cli" * * const configSource = Param.file(Param.flagKind, "config").pipe( * Param.orElseResult(() => Param.string(Param.flagKind, "config-url")) * ) * // Returns Result + * configSource.kind // => "flag" * ``` * * @category combinators @@ -1805,27 +1898,28 @@ export const orElse: { */ export const orElseResult: { ( - orElse: (error: CliError.CliError) => Param + orElse: LazyArg> ): (self: Param) => Param> ( self: Param, - orElse: (error: CliError.CliError) => Param + orElse: LazyArg> ): Param> } = dual(2, ( self: Param, - orElse: (error: CliError.CliError) => Param + orElse: LazyArg> ) => { return transform( self, - (parse: Parse): Parse> => (args: ParsedArgs) => + (parse: Parse, alternatives): Parse> => (args: ParsedArgs) => Effect.catch( Effect.map(parse(args), ([leftover, value]) => [leftover, Result.succeed(value)] as const), - (err) => + () => Effect.map( - orElse(err).parse(args), + (alternatives[0]!() as Parse)(args), ([leftover, value]) => [leftover, Result.fail(value)] as const ) - ) + ), + [orElse] ) }) @@ -2042,7 +2136,12 @@ const transformSingle = ( return matchParam(param, { Single: (single) => f(single), Map: (mapped) => map(transformSingle(mapped.param, f), mapped.f), - Transform: (mapped) => transform(transformSingle(mapped.param, f), mapped.f), + Transform: (mapped) => + transform( + transformSingle(mapped.param, f), + mapped.f, + mapped.alternatives.map((alternative) => () => transformSingle(alternative(), f)) + ), Optional: (p) => optional(transformSingle(p.param, f)) as Param, Variadic: (p) => variadic(transformSingle(p.param, f), { @@ -2064,7 +2163,10 @@ export const extractSingleParams = ( return matchParam(param, { Single: (single) => [single as Single], Map: (mapped) => extractSingleParams(mapped.param), - Transform: (mapped) => extractSingleParams(mapped.param), + Transform: (mapped) => [ + ...extractSingleParams(mapped.param), + ...mapped.alternatives.flatMap((alternative) => extractSingleParams(alternative())) + ], Optional: (optional) => extractSingleParams(optional.param), Variadic: (variadic) => extractSingleParams(variadic.param) }) diff --git a/.context/effect/packages/effect/src/unstable/cli/Primitive.ts b/.context/effect/packages/effect/src/unstable/cli/Primitive.ts index 4526a81af..e5ef22d30 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Primitive.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Primitive.ts @@ -10,9 +10,6 @@ * * @since 4.0.0 */ -import * as Ini from "ini" -import * as Toml from "toml" -import * as Yaml from "yaml" import * as Config from "../../Config.ts" import * as Effect from "../../Effect.ts" import * as FileSystem from "../../FileSystem.ts" @@ -24,6 +21,9 @@ import * as Schema from "../../Schema.ts" import type { Formatter } from "../../SchemaIssue.ts" import type * as Struct from "../../Struct.ts" import type { Covariant } from "../../Types.ts" +import * as Ini from "../encoding/Ini.ts" +import * as Toml from "../encoding/Toml.ts" +import * as Yaml from "../encoding/Yaml.ts" import type { Environment } from "./Command.ts" const TypeId = "~effect/cli/Primitive" @@ -33,25 +33,36 @@ const TypeId = "~effect/cli/Primitive" * * **Example** (Parsing values with primitives) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * // Using built-in primitives - * const parseString = Effect.gen(function*() { + * const program = Effect.gen(function*() { * const stringResult = yield* Primitive.string.parse("hello") * const numberResult = yield* Primitive.integer.parse("42") * const boolResult = yield* Primitive.boolean.parse("true") - * - * return { stringResult, numberResult, boolResult } + * return [stringResult, numberResult, boolResult] as const * }) * - * // All primitives provide parsing functionality - * const parseDate = Effect.gen(function*() { - * const dateResult = yield* Primitive.date.parse("2023-12-25") - * const pathResult = yield* Primitive.path("file", true).parse("./package.json") - * return { dateResult, pathResult } - * }) + * await Effect.runPromise(program.pipe(Effect.provide(CliTestLayer))) // => ["hello", 42, true] * ``` * * @category models @@ -125,23 +136,36 @@ const makeSchemaPrimitive = ( * * **Example** (Parsing boolean values) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const parseBoolean = Effect.gen(function*() { - * const result1 = yield* Primitive.boolean.parse("true") - * console.log(result1) // true - * - * const result2 = yield* Primitive.boolean.parse("yes") - * console.log(result2) // true - * - * const result3 = yield* Primitive.boolean.parse("false") - * console.log(result3) // false + * const parseBoolean = Effect.all([ + * Primitive.boolean.parse("true"), + * Primitive.boolean.parse("yes"), + * Primitive.boolean.parse("false"), + * Primitive.boolean.parse("0") + * ]) * - * const result4 = yield* Primitive.boolean.parse("0") - * console.log(result4) // false - * }) + * await Effect.runPromise(parseBoolean.pipe(Effect.provide(CliTestLayer))) // => [true, true, false, false] * ``` * * @category constructors @@ -157,20 +181,35 @@ export const boolean: Primitive = makeSchemaPrimitive( * * **Example** (Parsing floating-point numbers) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const parseFloat = Effect.gen(function*() { - * const result1 = yield* Primitive.float.parse("3.14") - * console.log(result1) // 3.14 - * - * const result2 = yield* Primitive.float.parse("-42.5") - * console.log(result2) // -42.5 + * const parseFloat = Effect.all([ + * Primitive.float.parse("3.14"), + * Primitive.float.parse("-42.5"), + * Primitive.float.parse("0") + * ]) * - * const result3 = yield* Primitive.float.parse("0") - * console.log(result3) // 0 - * }) + * await Effect.runPromise(parseFloat.pipe(Effect.provide(CliTestLayer))) // => [3.14, -42.5, 0] * ``` * * @category constructors @@ -186,20 +225,35 @@ export const float: Primitive = makeSchemaPrimitive( * * **Example** (Parsing integer values) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const parseInteger = Effect.gen(function*() { - * const result1 = yield* Primitive.integer.parse("42") - * console.log(result1) // 42 - * - * const result2 = yield* Primitive.integer.parse("-123") - * console.log(result2) // -123 + * const parseInteger = Effect.all([ + * Primitive.integer.parse("42"), + * Primitive.integer.parse("-123"), + * Primitive.integer.parse("0") + * ]) * - * const result3 = yield* Primitive.integer.parse("0") - * console.log(result3) // 0 - * }) + * await Effect.runPromise(parseInteger.pipe(Effect.provide(CliTestLayer))) // => [42, -123, 0] * ``` * * @category constructors @@ -215,20 +269,34 @@ export const integer: Primitive = makeSchemaPrimitive( * * **Example** (Parsing date values) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const parseDate = Effect.gen(function*() { - * const result1 = yield* Primitive.date.parse("2023-12-25") - * console.log(result1) // Date object for December 25, 2023 - * - * const result2 = yield* Primitive.date.parse("2023-12-25T10:30:00Z") - * console.log(result2) // Date object with time - * - * const result3 = yield* Primitive.date.parse("Dec 25, 2023") - * console.log(result3) // Date object parsed from natural format + * const result = yield* Primitive.date.parse("2023-12-25") + * return result.toISOString() * }) + * + * await Effect.runPromise(parseDate.pipe(Effect.provide(CliTestLayer))) // => "2023-12-25T00:00:00.000Z" * ``` * * @category constructors @@ -236,7 +304,7 @@ export const integer: Primitive = makeSchemaPrimitive( */ export const date: Primitive = makeSchemaPrimitive( "Date", - Schema.DateValid + Schema.Date ) /** @@ -244,20 +312,35 @@ export const date: Primitive = makeSchemaPrimitive( * * **Example** (Parsing string values) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const parseString = Effect.gen(function*() { - * const result1 = yield* Primitive.string.parse("hello world") - * console.log(result1) // "hello world" - * - * const result2 = yield* Primitive.string.parse("") - * console.log(result2) // "" + * const parseString = Effect.all([ + * Primitive.string.parse("hello world"), + * Primitive.string.parse(""), + * Primitive.string.parse("123") + * ]) * - * const result3 = yield* Primitive.string.parse("123") - * console.log(result3) // "123" - * }) + * await Effect.runPromise(parseString.pipe(Effect.provide(CliTestLayer))) // => ["hello world", "", "123"] * ``` * * @category constructors @@ -270,9 +353,27 @@ export const string: Primitive = makePrimitive("String", (value) => Effe * * **Example** (Parsing choices) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * type LogLevel = "debug" | "info" | "warn" | "error" * @@ -283,13 +384,12 @@ export const string: Primitive = makePrimitive("String", (value) => Effe * ["error", "error"] * ]) * - * const parseLogLevel = Effect.gen(function*() { - * const result1 = yield* logLevelPrimitive.parse("info") - * console.log(result1) // "info" + * const parseLogLevel = Effect.all([ + * logLevelPrimitive.parse("info"), + * logLevelPrimitive.parse("debug") + * ]) * - * const result2 = yield* logLevelPrimitive.parse("debug") - * console.log(result2) // "debug" - * }) + * await Effect.runPromise(parseLogLevel.pipe(Effect.provide(CliTestLayer))) // => ["info", "debug"] * ``` * * @category constructors @@ -314,7 +414,7 @@ export const choice = ( * * **Example** (Choosing path validation) * - * ```ts + * ```ts import.meta.vitest * import { Primitive } from "effect/unstable/cli" * * // Only accept files @@ -325,6 +425,8 @@ export const choice = ( * * // Accept either files or directories * const anyPath = Primitive.path("either", false) + * + * const tags = [filePath._tag, dirPath._tag, anyPath._tag] // => ["Path", "Path", "Path"] * ``` * * @category models @@ -337,26 +439,38 @@ export type PathType = "file" | "directory" | "either" * * **Example** (Parsing file system paths) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const services = Layer.mergeAll( + * Path.layer, + * FileSystem.layerNoop({ + * exists: () => Effect.succeed(true), + * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info) + * }), + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const program = Effect.gen(function*() { - * // Parse a file path that must exist * const filePrimitive = Primitive.path("file", true) * const filePath = yield* filePrimitive.parse("./package.json") - * console.log(filePath) // Absolute path to package.json + * return filePath.endsWith("/package.json") + * }).pipe(Effect.provide(services)) * - * // Parse a directory path - * const dirPrimitive = Primitive.path("directory", false) - * const dirPath = yield* dirPrimitive.parse("./src") - * console.log(dirPath) // Absolute path to src directory - * - * // Parse any path type - * const anyPrimitive = Primitive.path("either", false) - * const anyPath = yield* anyPrimitive.parse("./some/path") - * console.log(anyPath) // Absolute path - * }) + * await Effect.runPromise(program) // => true * ``` * * @category constructors @@ -365,8 +479,8 @@ export type PathType = "file" | "directory" | "either" export const path = ( pathType: PathType, mustExist?: boolean -): Primitive => - makePrimitive( +): Primitive => { + const primitive = makePrimitive( "Path", Effect.fnUntraced(function*(value) { const fs = yield* FileSystem.FileSystem @@ -404,6 +518,8 @@ export const path = ( return absolutePath }) ) + return Object.assign(primitive, { pathType }) +} /** * Creates a primitive that wraps string input in `Redacted`. @@ -415,15 +531,34 @@ export const path = ( * * **Example** (Parsing redacted values) * - * ```ts - * import { Effect, Redacted } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Redacted, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const parseRedacted = Effect.gen(function*() { * const result = yield* Primitive.redacted.parse("secret-password") - * console.log(Redacted.value(result)) // "secret-password" - * console.log(String(result)) // "" + * return [Redacted.value(result), String(result)] as const * }) + * + * await Effect.runPromise(parseRedacted.pipe(Effect.provide(CliTestLayer))) // => ["secret-password", ""] * ``` * * @category constructors @@ -439,27 +574,38 @@ export const redacted: Primitive> = makePrimitive( * * **Example** (Reading file text) * - * ```ts - * import { Effect, Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" - * - * const ConfigSchema = Schema.Struct({ - * name: Schema.String, - * version: Schema.String, - * port: Schema.Number - * }) - * const decodeConfig = Schema.decodeUnknownEffect( - * Schema.fromJsonString(ConfigSchema) + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const services = Layer.mergeAll( + * Path.layer, + * FileSystem.layerNoop({ + * exists: () => Effect.succeed(true), + * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info), + * readFileString: () => Effect.succeed('{"private":true}') + * }), + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) * ) * * const readConfigFile = Effect.gen(function*() { - * const content = yield* Primitive.fileText.parse("./config.json") - * console.log(content) // {"name":"my-app","version":"1.0.0","port":3000} + * const content = yield* Primitive.fileText.parse("./package.json") + * return JSON.parse(content) as { private: boolean } + * }).pipe(Effect.provide(services)) * - * const config = yield* decodeConfig(content) - * console.log(config) // { name: "my-app", version: "1.0.0", port: 3000 } - * return config - * }) + * await Effect.runPromise(readConfigFile) // => { private: true } * ``` * * @category constructors @@ -536,17 +682,40 @@ const fileParsers: Record unknown> = { * * **Example** (Parsing file content) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const services = Layer.mergeAll( + * Path.layer, + * FileSystem.layerNoop({ + * exists: () => Effect.succeed(true), + * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info), + * readFileString: () => Effect.succeed('{"private":true}') + * }), + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const tomlFilePrimitive = Primitive.fileParse({ format: "toml" }) + * const jsonFilePrimitive = Primitive.fileParse({ format: "json" }) * * const loadConfig = Effect.gen(function*() { - * const config = yield* tomlFilePrimitive.parse("./config.toml") - * console.log(config) // { name: "my-app", version: "1.0.0", port: 3000 } - * return config - * }) + * const config = yield* jsonFilePrimitive.parse("./package.json") + * return config as { private: boolean } + * }).pipe(Effect.provide(services)) + * + * await Effect.runPromise(loadConfig) // => { private: true } * ``` * * @category constructors @@ -588,14 +757,34 @@ export type FileSchemaOptions = Struct.Simplify< * * **Example** (Parsing file content with a schema) * - * ```ts - * import { Effect, Schema } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Schema, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const services = Layer.mergeAll( + * Path.layer, + * FileSystem.layerNoop({ + * exists: () => Effect.succeed(true), + * stat: () => Effect.succeed({ type: "File" } as FileSystem.File.Info), + * readFileString: () => Effect.succeed('{"private":true}') + * }), + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const ConfigSchema = Schema.Struct({ - * name: Schema.String, - * version: Schema.String, - * port: Schema.Number + * private: Schema.Boolean * }) * * const jsonConfigPrimitive = Primitive.fileSchema(ConfigSchema, { @@ -603,10 +792,10 @@ export type FileSchemaOptions = Struct.Simplify< * }) * * const loadConfig = Effect.gen(function*() { - * const config = yield* jsonConfigPrimitive.parse("./config.json") - * console.log(config) // { name: "my-app", version: "1.0.0", port: 3000 } - * return config - * }) + * return yield* jsonConfigPrimitive.parse("./package.json") + * }).pipe(Effect.provide(services)) + * + * await Effect.runPromise(loadConfig) // => { private: true } * ``` * * @category constructors @@ -634,20 +823,36 @@ export const fileSchema = ( * * **Example** (Parsing key-value pairs) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * - * const parseKeyValue = Effect.gen(function*() { - * const result1 = yield* Primitive.keyValuePair.parse("name=john") - * console.log(result1) // { name: "john" } - * - * const result2 = yield* Primitive.keyValuePair.parse("port=3000") - * console.log(result2) // { port: "3000" } + * const parseKeyValue = Effect.all([ + * Primitive.keyValuePair.parse("name=john"), + * Primitive.keyValuePair.parse("port=3000"), + * Primitive.keyValuePair.parse("debug=true") + * ]) * - * const result3 = yield* Primitive.keyValuePair.parse("debug=true") - * console.log(result3) // { debug: "true" } - * }) + * const result = await Effect.runPromise(parseKeyValue.pipe(Effect.provide(CliTestLayer))) + * result // => [{ name: "john" }, { port: "3000" }, { debug: "true" }] * ``` * * @category constructors @@ -681,16 +886,34 @@ export const keyValuePair: Primitive> = makePrimitive( * * **Example** (Rejecting option values) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect" * import { Primitive } from "effect/unstable/cli" + * import { ChildProcessSpawner } from "effect/unstable/process" + * + * const CliTestLayer = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Stdio.layerTest({}), + * Layer.succeed(Terminal.Terminal, Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.die("unused"), + * readLine: Effect.die("unused"), + * display: () => Effect.void + * })), + * Layer.succeed( + * ChildProcessSpawner.ChildProcessSpawner, + * ChildProcessSpawner.make(() => Effect.die("unused")) + * ) + * ) * * const program = Effect.gen(function*() { * // This will always fail - useful for boolean flags * return yield* Primitive.none.parse("any-value") * }) * - * // The above effect will fail with "This option does not accept values" + * await Effect.runPromise(Effect.flip(program).pipe(Effect.provide(CliTestLayer))) // => "This option does not accept values" * ``` * * @category constructors @@ -708,20 +931,20 @@ export const none: Primitive = makePrimitive("None", () => Effect.fail("T * * **Example** (Getting primitive type names) * - * ```ts + * ```ts import.meta.vitest * import { Primitive } from "effect/unstable/cli" * - * console.log(Primitive.getTypeName(Primitive.string)) // "string" - * console.log(Primitive.getTypeName(Primitive.integer)) // "integer" - * console.log(Primitive.getTypeName(Primitive.boolean)) // "boolean" - * console.log(Primitive.getTypeName(Primitive.date)) // "date" - * console.log(Primitive.getTypeName(Primitive.keyValuePair)) // "key=value" + * Primitive.getTypeName(Primitive.string) // => "string" + * Primitive.getTypeName(Primitive.integer) // => "integer" + * Primitive.getTypeName(Primitive.boolean) // => "boolean" + * Primitive.getTypeName(Primitive.date) // => "date" + * Primitive.getTypeName(Primitive.keyValuePair) // => "key=value" * * const logLevelChoice = Primitive.choice([ * ["debug", "debug"], * ["info", "info"] * ]) - * console.log(Primitive.getTypeName(logLevelChoice)) // "choice" + * Primitive.getTypeName(logLevelChoice) // => "choice" * ``` * * @category getters @@ -763,3 +986,7 @@ export const getTypeName = (primitive: Primitive): string => { /** @internal */ export const getChoiceKeys = (primitive: Primitive): ReadonlyArray | undefined => primitive._tag === "Choice" ? (primitive as any).choiceKeys : undefined + +/** @internal */ +export const getPathType = (primitive: Primitive): PathType | undefined => + primitive._tag === "Path" ? (primitive as any).pathType : undefined diff --git a/.context/effect/packages/effect/src/unstable/cli/Prompt.ts b/.context/effect/packages/effect/src/unstable/cli/Prompt.ts index 1a0443ba1..f21b1f010 100644 --- a/.context/effect/packages/effect/src/unstable/cli/Prompt.ts +++ b/.context/effect/packages/effect/src/unstable/cli/Prompt.ts @@ -29,6 +29,11 @@ import type { Covariant } from "../../Types.ts" import * as Ansi from "./internal/ansi.ts" import type * as Primitive from "./Primitive.ts" +declare const process: { + readonly platform: string + readonly cwd: () => string +} + const TypeId = "~effect/cli/Prompt" /** @@ -651,28 +656,35 @@ export declare namespace All { * * **Example** (Collecting prompt results) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path, Terminal } from "effect" * import { Prompt } from "effect/unstable/cli" * - * const username = Prompt.text({ - * message: "Enter your username: " + * const terminal = Terminal.make({ + * columns: Effect.succeed(80), + * rows: Effect.succeed(24), + * readInput: Effect.succeed({} as never), + * readLine: Effect.die("unused"), + * display: () => Effect.void * }) + * const services = Layer.mergeAll( + * FileSystem.layerNoop({}), + * Path.layer, + * Layer.succeed(Terminal.Terminal, terminal) + * ) * - * const password = Prompt.password({ - * message: "Enter your password: ", - * validate: (value) => - * value.length === 0 - * ? Effect.fail("Password cannot be empty") - * : Effect.succeed(value) - * }) + * const username = Prompt.succeed("alice") + * const password = Prompt.succeed("secret") * * const allWithTuple = Prompt.all([username, password]) * * const allWithRecord = Prompt.all({ username, password }) + * + * await Effect.runPromise(Effect.provide(allWithTuple, services)) // => ["alice", "secret"] + * await Effect.runPromise(Effect.provide(allWithRecord, services)) // => { username: "alice", password: "secret" } * ``` * - * @category collecting & elements + * @category combining * @since 4.0.0 */ export const all: < @@ -681,10 +693,13 @@ export const all: < 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 if (Predicate.isIterable(arguments[0])) { + return allTupled(Arr.fromIterable(arguments[0] as Iterable>)) as any } else { const entries = Object.entries(arguments[0] as Readonly<{ [K: string]: Prompt }>) + if (entries.length === 0) { + return succeed({}) as any + } let result = map(entries[0][1], (value) => ({ [entries[0][0]]: value })) if (entries.length === 1) { return result as any @@ -1070,7 +1085,7 @@ export const password = ( * The returned effect may fail with `Terminal.QuitError` if terminal input ends * or the prompt is quit. * - * @category execution + * @category running * @since 4.0.0 */ export const run: ( @@ -1136,7 +1151,7 @@ export const select = (options: SelectOptions): Prompt => { * * **Example** (Filtering choices with autocomplete) * - * ```ts + * ```ts import.meta.vitest * import { Prompt } from "effect/unstable/cli" * * const language = Prompt.autoComplete({ @@ -1147,6 +1162,8 @@ export const select = (options: SelectOptions): Prompt => { * { title: "Kotlin", value: "kt" } * ] * }) + * + * Prompt.isPrompt(language) // => true * ``` * * @category constructors @@ -1202,7 +1219,7 @@ export const multiSelect = ( const initialSelected = new Set() for (let i = 0; i < opts.choices.length; i++) { const choice = opts.choices[i] as SelectChoice - if (choice.selected === true) { + if (choice.selected === true && !choice.disabled) { initialSelected.add(i) } } @@ -1985,6 +2002,9 @@ class Day extends DatePart { } private ordinalIndicator(day: number): string { + if (day >= 11 && day <= 13) { + return "th" + } switch (day % 10) { case 1: return "st" @@ -2043,7 +2063,7 @@ class Year extends DatePart { override toString() { const year = `${this.date.getFullYear()}`.padStart(4, "0") return this.token.length === 2 - ? year.substring(-2) + ? year.slice(-2) : year } } @@ -2060,7 +2080,7 @@ class Meridiem extends DatePart { setValue(_value: string): void {} override toString() { - const meridiem = this.date.getHours() > 12 ? "pm" : "am" + const meridiem = this.date.getHours() >= 12 ? "pm" : "am" return /A/.test(this.token) ? meridiem.toUpperCase() : meridiem @@ -2547,9 +2567,9 @@ const renderMultiSelectChoices = ( renderOptions?: RenderOptions | undefined ) => { const choices = options.choices - const totalChoices = choices.length - const selectedCount = state.selectedIndices.size - const allSelected = selectedCount === totalChoices + const selectableCount = choices.filter((choice) => !choice.disabled).length + const selectedCount = Array.from(state.selectedIndices).filter((index) => !choices[index].disabled).length + const allSelected = selectedCount === selectableCount const selectAllText = allSelected ? options?.selectNone ?? "Select None" @@ -2585,8 +2605,9 @@ const renderMultiSelectChoices = ( const annotatedCheckbox = isHighlighted && renderOptions?.plain !== true ? Ansi.annotate(checkbox, Ansi.cyanBright) : checkbox - const title = renderMultiSelectTitle(choice.title, isHighlighted, renderOptions) - const description = renderChoiceDescription(choice as SelectChoice, isHighlighted, renderOptions) + const selectChoice = choice as SelectChoice + const title = renderChoiceTitle(selectChoice, isHighlighted, renderOptions) + const description = renderChoiceDescription(selectChoice, isHighlighted, renderOptions) documents.push(prefix + " " + annotatedCheckbox + " " + title + " " + description) } } @@ -2635,16 +2656,20 @@ const processSpace = ( ) => { const selectedIndices = new Set(state.selectedIndices) if (state.index === 0) { - if (state.selectedIndices.size === options.choices.length) { + const selectableCount = options.choices.filter((choice) => !choice.disabled).length + const selectedCount = Array.from(state.selectedIndices).filter((index) => !options.choices[index].disabled).length + if (selectedCount === selectableCount) { selectedIndices.clear() } else { for (let i = 0; i < options.choices.length; i++) { - selectedIndices.add(i) + if (!options.choices[i].disabled) { + selectedIndices.add(i) + } } } } else if (state.index === 1) { for (let i = 0; i < options.choices.length; i++) { - if (state.selectedIndices.has(i)) { + if (options.choices[i].disabled || state.selectedIndices.has(i)) { selectedIndices.delete(i) } else { selectedIndices.add(i) @@ -2652,7 +2677,9 @@ const processSpace = ( } } else { const choiceIndex = state.index - metaOptionsCount - if (selectedIndices.has(choiceIndex)) { + if (options.choices[choiceIndex].disabled) { + return Effect.succeed(Action.Beep()) + } else if (selectedIndices.has(choiceIndex)) { selectedIndices.delete(choiceIndex) } else { selectedIndices.add(choiceIndex) @@ -2692,7 +2719,8 @@ const handleMultiSelectProcess = (options: SelectOptionsReq & MultiSelectO } case "enter": case "return": { - const selectedCount = state.selectedIndices.size + const selectedIndices = Array.from(state.selectedIndices).filter((index) => !options.choices[index].disabled) + const selectedCount = selectedIndices.length if (options.min !== undefined && selectedCount < options.min) { return Effect.succeed( Action.NextFrame({ state: { ...state, error: Option.some(`At least ${options.min} are required`) } }) @@ -2703,9 +2731,7 @@ const handleMultiSelectProcess = (options: SelectOptionsReq & MultiSelectO Action.NextFrame({ state: { ...state, error: Option.some(`At most ${options.max} choices are allowed`) } }) ) } - const selectedValues = Array.from(state.selectedIndices).sort(EffectNumber.Order).map((index) => - options.choices[index].value - ) + const selectedValues = selectedIndices.sort(EffectNumber.Order).map((index) => options.choices[index].value) return Effect.succeed(Action.Submit({ value: selectedValues })) } default: { @@ -2861,7 +2887,11 @@ const defaultFloatProcessor = (input: string, state: NumberState) => { return Effect.succeed(Action.NextFrame({ state: { ...state, - value: input === "." ? `${parsed}.` : `${parsed}`, + value: input === "." + ? `${parsed}.` + : state.value.includes(".") && /^\d$/.test(input) + ? state.value + input + : `${parsed}`, error: Option.none() } })) diff --git a/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts b/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts index 560010228..bfc4392ab 100644 --- a/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts +++ b/.context/effect/packages/effect/src/unstable/cli/internal/ansi.ts @@ -79,9 +79,9 @@ export const combine = (...styles: Array): Array => styles /** @internal */ export const cursorTo = (column: number, row?: number): string => { if (row === undefined) { - return `\x1b${Math.max(column + 1, 0)} G` + return `${ESC}${Math.max(column + 1, 0)}G` } - return `\x1b${row + 1}${SEP}${Math.max(column + 1, 0)} H` + return `${ESC}${row + 1}${SEP}${Math.max(column + 1, 0)}H` } /** @internal */ diff --git a/.context/effect/packages/effect/src/unstable/cli/internal/command.ts b/.context/effect/packages/effect/src/unstable/cli/internal/command.ts index b35279b1c..543f283d6 100644 --- a/.context/effect/packages/effect/src/unstable/cli/internal/command.ts +++ b/.context/effect/packages/effect/src/unstable/cli/internal/command.ts @@ -98,7 +98,7 @@ export const makeCommand = | undefined readonly subcommands?: ReadonlyArray | undefined readonly parse?: ((input: ParsedTokens) => Effect.Effect) | undefined @@ -125,7 +125,7 @@ export const makeCommand = ): HelpDoc => { const args: Array = [] @@ -139,7 +139,8 @@ export const makeCommand = min > 0)), variadic: metadata.isVariadic }) } @@ -147,8 +148,8 @@ export const makeCommand = 0 ? commandPath.join(" ") : options.name // Only render `` in usage when at least one visible subcommand - // exists; an all-hidden subcommand tree should look like a leaf command. - if (subcommands.some((group) => group.commands.some((c) => !c.hidden))) { + // exists; an all-unlisted subcommand tree should look like a leaf command. + if (subcommands.some((group) => group.commands.some((c) => !c.unlisted))) { usage += " " } usage += " [flags]" @@ -159,21 +160,22 @@ export const makeCommand = = { @@ -429,7 +429,7 @@ export type EncodedUnprocessedOptions = { * The fields distinguish existing requests from new requests and carry the * driver-specific pagination cursor. * - * @category Encoded + * @category models * @since 4.0.0 */ export type EncodedRepliesOptions = { @@ -606,11 +606,7 @@ export const makeEncoded: (encoded: Encoded) => Effect.Effect< ), Effect.asVoid ), - saveReply: (reply) => - Effect.flatMap( - Reply.serialize(reply), - encoded.saveReply - ), + saveReply: (reply) => Effect.flatMap(Reply.serializeOrDefect(reply), encoded.saveReply), clearReplies: encoded.clearReplies, repliesFor: Effect.fnUntraced(function*(messages) { const requestIds = Arr.empty() @@ -793,7 +789,7 @@ export const noop: MessageStorage["Service"] = Effect.runSync(make({ * It stores the encoded envelope, last acknowledged chunk, accumulated replies, * and optional delivery time. * - * @category memory + * @category models * @since 4.0.0 */ export type MemoryEntry = { @@ -806,7 +802,7 @@ export type MemoryEntry = { /** * Provides a context reference used in tests to simulate a transaction. * - * @category memory + * @category services * @since 4.0.0 */ export const MemoryTransaction = Context.Reference("effect/cluster/MessageStorage/MemoryTransaction", { @@ -822,7 +818,7 @@ export const MemoryTransaction = Context.Reference("effect/cluster/Mess * maps used to track requests, primary keys, unprocessed envelopes, reply IDs, * and the journal. * - * @category memory + * @category services * @since 4.0.0 */ export class MemoryDriver extends Context.Service()("effect/cluster/MessageStorage/MemoryDriver", { @@ -995,6 +991,14 @@ export class MemoryDriver extends Context.Service()("effect/cluste resetAddress: () => Effect.void, clearAddress: (address) => Effect.sync(() => { + for (const [primaryKey, entry] of requestsByPrimaryKey) { + const envelope = entry.envelope + const sameAddress = address.entityType === envelope.address.entityType && + address.entityId === envelope.address.entityId + if (sameAddress) { + requestsByPrimaryKey.delete(primaryKey) + } + } for (let i = journal.length - 1; i >= 0; i--) { const envelope = journal[i] const sameAddress = address.entityType === envelope.address.entityType && diff --git a/.context/effect/packages/effect/src/unstable/cluster/Reply.ts b/.context/effect/packages/effect/src/unstable/cluster/Reply.ts index fea424f5f..2f8b310e6 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Reply.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Reply.ts @@ -242,11 +242,10 @@ export class Chunk extends Data.TaggedClass("Chunk")<{ [success], ([success]) => (input, ast, options) => { if (!isReply(input) || input._tag !== "Chunk") { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } return Effect.mapBothEager(SchemaParser.decodeEffect(Schema.NonEmptyArray(success))(input.values, options), { - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option.some(input), [new SchemaIssue.Pointer(["values"], issue)]), + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "values", issue, input, options), onSuccess: (values) => new Chunk({ ...input, values } as any) }) }, @@ -258,7 +257,7 @@ export class Chunk extends Data.TaggedClass("Chunk")<{ _tag: Schema.Literal("Chunk"), requestId: SnowflakeFromBigInt, id: SnowflakeFromBigInt, - sequence: Schema.Number, + sequence: Schema.Int, values: Schema.NonEmptyArray(success) }), SchemaTransformation.transform({ @@ -352,11 +351,10 @@ export class WithExit extends Data.TaggedClass("WithExit")<{ [exitSchema], ([exit]) => (input, ast, options) => { if (!isReply(input) || input._tag !== "WithExit") { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } return Effect.mapBothEager(SchemaParser.decodeEffect(exit)(input.exit, options), { - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option.some(input), [new SchemaIssue.Pointer(["exit"], issue)]), + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "exit", issue, input, options), onSuccess: (exit) => new WithExit({ ...input, exit: exit as any }) }) }, @@ -435,6 +433,27 @@ export const serialize = ( ) } +/** + * Serializes a `ReplyWithContext`, falling back to a serializable defect reply + * when the original reply cannot be encoded. + * + * @category serialization + * @since 4.0.0 + */ +export const serializeOrDefect = ( + self: ReplyWithContext +): Effect.Effect => + Effect.catchTag( + serialize(self), + "MalformedMessage", + (error) => + Effect.orDie(serialize(ReplyWithContext.fromDefect({ + id: self.reply.id, + requestId: self.reply.requestId, + defect: error + }))) + ) + /** * Serializes an outgoing request's last received reply when one exists, returning * `None` when no reply has been received and refailing encoding errors as diff --git a/.context/effect/packages/effect/src/unstable/cluster/Runner.ts b/.context/effect/packages/effect/src/unstable/cluster/Runner.ts index d5a54e787..1cbe895aa 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Runner.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Runner.ts @@ -29,7 +29,7 @@ const TypeId = "~effect/cluster/Runner" export class Runner extends Schema.Class(TypeId)({ address: RunnerAddress, groups: Schema.Array(Schema.String), - weight: Schema.Number + weight: Schema.Finite }) { /** * Formatter for rendering runner values consistently. diff --git a/.context/effect/packages/effect/src/unstable/cluster/RunnerAddress.ts b/.context/effect/packages/effect/src/unstable/cluster/RunnerAddress.ts index 69ce6b239..308bb6d7f 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/RunnerAddress.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/RunnerAddress.ts @@ -28,7 +28,7 @@ const TypeId = "~effect/cluster/RunnerAddress" */ export class RunnerAddress extends Schema.Class(TypeId)({ host: Schema.String, - port: Schema.Number + port: Schema.Int }) { /** * Marks this value as a cluster runner address for runtime guards. diff --git a/.context/effect/packages/effect/src/unstable/cluster/RunnerHealth.ts b/.context/effect/packages/effect/src/unstable/cluster/RunnerHealth.ts index 51ddcb5fa..966d0d8cd 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/RunnerHealth.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/RunnerHealth.ts @@ -27,7 +27,7 @@ import * as Runners from "./Runners.ts" * still be processing messages. If a Runner is not responsive, then its * associated shards can and will be re-assigned to a different Runner. * - * @category models + * @category services * @since 4.0.0 */ export class RunnerHealth extends Context.Service< diff --git a/.context/effect/packages/effect/src/unstable/cluster/RunnerServer.ts b/.context/effect/packages/effect/src/unstable/cluster/RunnerServer.ts index cea221b38..e076b7324 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/RunnerServer.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/RunnerServer.ts @@ -10,6 +10,7 @@ * * @since 4.0.0 */ +import type * as Cause from "../../Cause.ts" import * as Effect from "../../Effect.ts" import type * as Exit from "../../Exit.ts" import * as Fiber from "../../Fiber.ts" @@ -17,6 +18,7 @@ import { constant } from "../../Function.ts" import * as Layer from "../../Layer.ts" import * as Option from "../../Option.ts" import * as Queue from "../../Queue.ts" +import type * as Rpc from "../rpc/Rpc.ts" import * as RpcServer from "../rpc/RpcServer.ts" import type * as ClusterError from "./ClusterError.ts" import * as Message from "./Message.ts" @@ -30,6 +32,16 @@ import { ShardingConfig } from "./ShardingConfig.ts" const constVoid = constant(Effect.void) +const serializeDefectReply = ( + reply: Reply.ReplyWithContext, + defect: unknown +): Effect.Effect => + Effect.orDie(Reply.serialize(Reply.ReplyWithContext.fromDefect({ + id: reply.reply.id, + requestId: reply.reply.requestId, + defect + }))) + /** * Layer that handles runner protocol RPCs by forwarding requests to `Sharding` * and `MessageStorage`. @@ -43,16 +55,16 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() { return { Ping: () => Effect.void, - Notify: ({ envelope }) => - sharding.notify( - envelope._tag === "Request" - ? new Message.IncomingRequest({ - envelope, - respond: constVoid, - lastSentReply: Option.none() - }) - : new Message.IncomingEnvelope({ envelope }) - ), + Notify: ({ envelope, persisted }) => { + const message = envelope._tag === "Request" + ? new Message.IncomingRequest({ + envelope, + respond: constVoid, + lastSentReply: Option.none() + }) + : new Message.IncomingEnvelope({ envelope }) + return persisted ? sharding.notify(message) : sharding.send(message) + }, Effect: ({ persisted, request }) => { let replyEncoded: Option.Option> = Option .none() @@ -63,7 +75,7 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() { envelope: request, lastSentReply: Option.none(), respond(reply) { - resume(Effect.orDie(Reply.serialize(reply))) + resume(Reply.serializeOrDefect(reply)) return Effect.void } }) @@ -108,23 +120,37 @@ export const layerHandlers = Runners.Rpcs.toLayer(Effect.gen(function*() { }, Stream: ({ persisted, request }) => Effect.flatMap( - Queue.make(), + Queue.make(), (queue) => { const message = new Message.IncomingRequest({ envelope: request, lastSentReply: Option.none(), respond(reply) { - return Effect.flatMap(Reply.serialize(reply), (reply) => { - Queue.offerUnsafe(queue, reply) - return Effect.void - }) + return Reply.serialize(reply).pipe( + Effect.flatMap((reply) => { + Queue.offerUnsafe(queue, reply) + if (reply._tag === "WithExit") { + Queue.endUnsafe(queue) + } + return Effect.void + }), + Effect.catchTag("MalformedMessage", (error) => + Effect.flatMap(serializeDefectReply(reply, error), (reply) => { + // the fallback defect reply is terminal, so end the stream + Queue.offerUnsafe(queue, reply) + Queue.endUnsafe(queue) + return Effect.void + })) + ) } }) return Effect.as( persisted ? Effect.andThen( storage.registerReplyHandler(message).pipe( - Effect.onError((cause) => Queue.failCause(queue, cause)), + Effect.onError((cause) => + Queue.failCause(queue, cause) + ), Effect.forkScoped ), sharding.notify(message, constWaitUntilRead) @@ -168,7 +194,8 @@ export const layer: Layer.Layer< RpcServer.Protocol | Sharding.Sharding | MessageStorage.MessageStorage > = RpcServer.layer(Runners.Rpcs, { spanPrefix: "RunnerServer", - disableTracing: true + disableTracing: true, + disableFatalDefects: true }).pipe(Layer.provide(layerHandlers)) /** diff --git a/.context/effect/packages/effect/src/unstable/cluster/RunnerStorage.ts b/.context/effect/packages/effect/src/unstable/cluster/RunnerStorage.ts index f1ac2bbe7..e06689bfc 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/RunnerStorage.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/RunnerStorage.ts @@ -24,7 +24,7 @@ import * as ShardId from "./ShardId.ts" * Represents a generic interface to the persistent storage required by the * cluster. * - * @category models + * @category services * @since 4.0.0 */ export class RunnerStorage extends Context.Service * * **Details** * - * Registered runners are treated as healthy and shard acquisition is kept only in - * process memory. + * Runner health and shard acquisition are kept only in process memory. * * @category constructors * @since 4.0.0 */ export const makeMemory = Effect.gen(function*() { - const runners = MutableHashMap.empty() + 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) => + getRunners: Effect.sync(() => Array.from(MutableHashMap.values(runners))), + register: (runner, healthy) => Effect.sync(() => { - MutableHashMap.set(runners, runner.address, runner) + MutableHashMap.set(runners, runner.address, [runner, healthy]) return MachineId.make(id++) }), unregister: (address) => Effect.sync(() => { MutableHashMap.remove(runners, address) }), - setRunnerHealth: () => Effect.void, + setRunnerHealth: (address, healthy) => + Effect.sync(() => { + MutableHashMap.modify(runners, address, ([runner]) => [runner, healthy] as const) + }), acquire: (_address, shardIds) => { acquired = Array.from(shardIds) return Effect.succeed(acquired) diff --git a/.context/effect/packages/effect/src/unstable/cluster/Runners.ts b/.context/effect/packages/effect/src/unstable/cluster/Runners.ts index fea620c40..6289d0f10 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Runners.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Runners.ts @@ -41,7 +41,7 @@ import * as Snowflake from "./Snowflake.ts" * sending and notifying messages, coordinating persisted replies, and marking * runners unavailable. * - * @category context + * @category services * @since 4.0.0 */ export class Runners extends Context.Service /** - * Notify a Runner that a message is available, then read replies from storage. + * Notify a Runner that a message is available. Persisted messages recover + * replies from storage, while volatile messages complete after delivery. */ readonly notify: ( options: { @@ -98,7 +99,14 @@ export class Runners extends Context.Service readonly discard: boolean } - ) => Effect.Effect + ) => Effect.Effect< + void, + | EntityNotAssignedToRunner + | RunnerUnavailable + | MailboxFull + | AlreadyProcessingMessage + | PersistenceError + > /** * Notify the current Runner that a message is available, then read replies from @@ -144,12 +152,6 @@ export class Runners extends Context.Service {} @@ -516,7 +519,7 @@ export interface RpcClient extends RpcClient_.FromGroup Effect.fail(new RunnerUnavailable({ address }))) + Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address }))) ) } + // Persisted requests can recover their reply from storage via the + // `RunnerUnavailable` path, volatile requests receive the defect as their reply. + const respondDefect = (defect: unknown) => + isPersisted + ? Effect.fail(new RunnerUnavailable({ address })) + : message.respond( + new Reply.WithExit({ + id: snowflakeGen.nextUnsafe(), + requestId: message.envelope.requestId, + exit: Exit.die(defect) + }) + ) const isStream = RpcSchema.isStreamSchema(rpc.successSchema) if (!isStream) { return Effect.matchEffect(Message.serializeRequest(message), { @@ -590,7 +604,6 @@ export const makeRpc: Effect.Effect< persisted: isPersisted }) ), - Effect.catchTag("RpcClientError", Effect.die), Effect.flatMap((reply) => Schema.decodeEffect(Reply.Reply(message.rpc))(reply).pipe( Effect.provideContext(message.context), @@ -599,7 +612,8 @@ export const makeRpc: Effect.Effect< ), Effect.flatMap(message.respond), Effect.scoped, - Effect.catchDefect(() => Effect.fail(new RunnerUnavailable({ address }))) + Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address }))), + Effect.catchDefect(respondDefect) ), onFailure: (error) => message.respond( @@ -626,10 +640,10 @@ export const makeRpc: Effect.Effect< Effect.flatMap((reply) => Effect.orDie(decode(reply))), Effect.flatMap(message.respond), Effect.forever, - Effect.catchTag("RpcClientError", Effect.die), Effect.provideContext(message.context), Effect.catchTag("Done", (_) => Effect.void), - Effect.catchDefect(() => Effect.fail(new RunnerUnavailable({ address }))) + Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address }))), + Effect.catchDefect(respondDefect) ) }), Effect.scoped @@ -648,15 +662,23 @@ export const makeRpc: Effect.Effect< if (Option.isNone(address)) { return Effect.void } + const rpc = message.rpc as any as Rpc.AnyWithProps + const isPersisted = Context.get(rpc.annotations, Persisted) const envelope = message.envelope const encode: Effect.Effect = message._tag === "OutgoingRequest" ? Effect.orDie(Message.serializeRequest(message)) : Effect.succeed(envelope) - return Effect.flatMap(encode, (envelope) => + const notify = Effect.flatMap(encode, (envelope) => RcMap.get(clients, address.value).pipe( - Effect.flatMap((client) => client.Notify({ envelope })), + Effect.flatMap((client) => + client.Notify({ + envelope, + persisted: isPersisted + }) + ), Effect.scoped, - Effect.ignore + Effect.catchTag("RpcClientError", () => Effect.fail(new RunnerUnavailable({ address: address.value }))) )) + return isPersisted ? Effect.ignore(notify) : notify }, onRunnerUnavailable: (address) => RcMap.invalidate(clients, address) }) @@ -681,7 +703,7 @@ export const layerRpc: Layer.Layer< * Service that creates an RPC client protocol for communicating with a runner at a * given address. * - * @category client + * @category services * @since 4.0.0 */ export class RpcClientProtocol extends Context.Service< diff --git a/.context/effect/packages/effect/src/unstable/cluster/ShardId.ts b/.context/effect/packages/effect/src/unstable/cluster/ShardId.ts index c70b22e4e..559968284 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/ShardId.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/ShardId.ts @@ -49,7 +49,7 @@ export const ShardId = S.declare(isShardId, { S.link()( S.Struct({ group: S.String, - id: S.Number + id: S.Int }), { decode: SchemaGetter.transform(({ group, id }) => make(group, id)), diff --git a/.context/effect/packages/effect/src/unstable/cluster/Sharding.ts b/.context/effect/packages/effect/src/unstable/cluster/Sharding.ts index a202047cf..ab40a7cfe 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Sharding.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Sharding.ts @@ -23,6 +23,7 @@ import * as Fiber from "../../Fiber.ts" import * as FiberMap from "../../FiberMap.ts" import { constant, flow } from "../../Function.ts" import * as HashRing from "../../HashRing.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Latch from "../../Latch.ts" import * as Layer from "../../Layer.ts" import * as MutableHashMap from "../../MutableHashMap.ts" @@ -55,6 +56,7 @@ import { EntityReaper } from "./internal/entityReaper.ts" import { hashString } from "./internal/hash.ts" import { internalInterruptors } from "./internal/interruptors.ts" import { ResourceMap } from "./internal/resourceMap.ts" +import { effectiveInterval } from "./internal/shardLock.ts" import * as Message from "./Message.ts" import * as MessageStorage from "./MessageStorage.ts" import * as Reply from "./Reply.ts" @@ -122,7 +124,7 @@ export class Sharding extends Context.Service RpcClient.RpcClient.From< Rpcs, - MailboxFull | AlreadyProcessingMessage | PersistenceError + MailboxFull | AlreadyProcessingMessage | PersistenceError | EntityNotAssignedToRunner > > @@ -176,7 +178,7 @@ export class Sharding extends Context.Service Effect.Effect< void, - MailboxFull | AlreadyProcessingMessage | PersistenceError + MailboxFull | AlreadyProcessingMessage | PersistenceError | EntityNotAssignedToRunner > /** @@ -217,6 +219,7 @@ interface EntityManagerState { const make = Effect.gen(function*() { const config = yield* ShardingConfig + const shardLockInterval = effectiveInterval(config) const shardGroups = shardGroupConfig(config) const getRunnerAddress = () => Option.getOrUndefined(config.runnerAddress) const clock = yield* Clock @@ -237,9 +240,13 @@ const make = Effect.gen(function*() { const runnerStorage = yield* RunnerStorage const entityManagers = new Map() + let entityRegistrationStartMillis: number | undefined + let entityRegistrationFallbackStartMillis: number | undefined const shardAssignments = MutableHashMap.empty() const selfShards = MutableHashSet.empty() + // open while shard lock storage is healthy + const shardLocksHealthyLatch = Latch.makeUnsafe(true) // the active shards are the ones that we have acquired the lock for const acquiredShards = MutableHashSet.empty() @@ -277,6 +284,9 @@ const make = Effect.gen(function*() { // allow them to move to another runner. const releasingShards = MutableHashSet.empty() + // Shards whose entities must be force interrupted and locks fully released + // before normal reacquisition. + const forceReleasingShards = MutableHashSet.empty() const initialRunnerAddress = getRunnerAddress() if (initialRunnerAddress) { const selfAddress = initialRunnerAddress @@ -286,37 +296,88 @@ const make = Effect.gen(function*() { }) const releaseShardsMap = yield* FiberMap.make() - const releaseShard = Effect.fnUntraced( - function*(shardId: ShardId) { - const fibers = Arr.empty>() + let forcedShardReleaseRunning = false + // Interrupt the shards' entities, wait for lock health, run the storage + // release, then clear the shards' bookkeeping. + const runShardRelease = Effect.fnUntraced(function*( + shardIds: ReadonlyArray, + force: boolean, + release: Effect.Effect + ) { + const fibers = Arr.empty>() + for (const shardId of shardIds) { for (const state of entityManagers.values()) { if (state.status === "closed") continue - fibers.push(yield* Effect.forkScoped(state.manager.interruptShard(shardId))) + fibers.push(yield* Effect.forkScoped(state.manager.interruptShard(shardId, { force }))) } - yield* Fiber.joinAll(fibers) - yield* runnerStorage.release(selfAddress, shardId) + } + yield* Fiber.joinAll(fibers) + yield* shardLocksHealthyLatch.await + yield* release + for (const shardId of shardIds) { MutableHashSet.remove(releasingShards, shardId) + MutableHashSet.remove(forceReleasingShards, shardId) yield* storage.unregisterShardReplyHandlers(shardId) - }, - Effect.sandbox, - (effect, shardId) => + } + }) + const retryShardRelease = + (annotations: { readonly fiber: string; readonly shardId?: ShardId }) => + (effect: Effect.Effect) => effect.pipe( + Effect.sandbox, Effect.tapError((cause) => - Effect.logDebug(`Could not release shard, retrying`, cause).pipe( + Effect.logDebug(`Could not release shards, retrying`, cause).pipe( Effect.annotateLogs({ module: "effect/cluster/Sharding", - fiber: "releaseShard", runner: selfAddress, - shardId - }) + ...annotations + }), + // Effect.eventually retries immediately, so space failures to + // avoid hot-looping while storage is unavailable. + Effect.andThen(Effect.sleep(50)) ) ), - Effect.eventually, + Effect.eventually + ) + const releaseShard = Effect.fnUntraced( + function*(shardId: ShardId) { + yield* runShardRelease( + [shardId], + MutableHashSet.has(forceReleasingShards, shardId), + runnerStorage.release(selfAddress, shardId) + ) + }, + (effect, shardId) => + effect.pipe( + retryShardRelease({ fiber: "releaseShard", shardId }), FiberMap.run(releaseShardsMap, shardId, { onlyIfMissing: true }) ) ) + // The forced release ends with `runnerStorage.releaseAll`, which drops + // every lock held by this runner. Shards must not be reacquired through the + // normal path while it is pending, otherwise the bulk release wipes a lock + // the runner already considers acquired. + const forcedShardReleasePending = () => forcedShardReleaseRunning || MutableHashSet.size(forceReleasingShards) > 0 + const releaseForcedShards = Effect.suspend(() => { + if (forcedShardReleaseRunning || MutableHashSet.size(forceReleasingShards) === 0) { + return Effect.void + } + forcedShardReleaseRunning = true + const shardIds = [...forceReleasingShards] + return runShardRelease(shardIds, true, runnerStorage.releaseAll(selfAddress)).pipe( + retryShardRelease({ fiber: "releaseForcedShards" }), + Effect.ensuring(Effect.sync(() => { + forcedShardReleaseRunning = false + activeShardsLatch.openUnsafe() + })), + Effect.forkIn(shardingScope), + Effect.asVoid + ) + }) const releaseShards = Effect.gen(function*() { + yield* releaseForcedShards for (const shardId of releasingShards) { + if (MutableHashSet.has(forceReleasingShards, shardId)) continue if (FiberMap.hasUnsafe(releaseShardsMap, shardId)) continue yield* releaseShard(shardId) } @@ -336,11 +397,21 @@ const make = Effect.gen(function*() { MutableHashSet.add(releasingShards, shardId) } - if (MutableHashSet.size(releasingShards) > 0) { + if (MutableHashSet.size(releasingShards) > 0 || MutableHashSet.size(forceReleasingShards) > 0) { yield* Effect.forkIn(syncSingletons, shardingScope) yield* releaseShards } + if (!shardLocksHealthyLatch.isOpen()) { + continue + } + + // Wait for the pending bulk release before reacquiring, so it cannot + // drop a lock acquired here. `releaseForcedShards` reopens the latch. + if (forcedShardReleasePending()) { + continue + } + // if a shard has been assigned to this runner, we acquire it const unacquiredShards = MutableHashSet.empty() for (const shardId of selfShards) { @@ -353,7 +424,7 @@ const make = Effect.gen(function*() { } const oacquired = yield* runnerStorage.acquire(selfAddress, unacquiredShards).pipe( - Effect.timeoutOption(config.shardLockRefreshInterval) + Effect.timeoutOption(shardLockInterval) ) if (Option.isNone(oacquired)) { activeShardsLatch.openUnsafe() @@ -363,10 +434,19 @@ const make = Effect.gen(function*() { const acquired = oacquired.value yield* storage.resetShards(acquired).pipe( Effect.ignore, - Effect.timeoutOption(config.shardLockRefreshInterval) + Effect.timeoutOption(shardLockInterval) ) + // A forced release can start while `acquire` is in flight, so re-check + // it here as well as before acquiring. + const forcedReleasePending = forcedShardReleasePending() for (const shardId of acquired) { - if (MutableHashSet.has(releasingShards, shardId) || !MutableHashSet.has(selfShards, shardId)) { + if ( + !shardLocksHealthyLatch.isOpen() || + forcedReleasePending || + MutableHashSet.has(releasingShards, shardId) || + !MutableHashSet.has(selfShards, shardId) + ) { + MutableHashSet.add(releasingShards, shardId) continue } MutableHashSet.add(acquiredShards, shardId) @@ -392,8 +472,52 @@ const make = Effect.gen(function*() { Effect.forkIn(shardingScope) ) - // refresh the shard locks every `shardLockRefreshInterval` - yield* Effect.suspend(() => + const markShardLocksUnhealthy = (cause: Cause.Cause) => + Effect.suspend(() => { + if (!shardLocksHealthyLatch.closeUnsafe()) return Effect.void + + const affectedShards = MutableHashSet.fromIterable([...acquiredShards, ...releasingShards]) + MutableHashSet.clear(selfShards) + MutableHashSet.clear(acquiredShards) + for (const shardId of affectedShards) { + MutableHashSet.add(releasingShards, shardId) + MutableHashSet.add(forceReleasingShards, shardId) + } + ClusterMetrics.shards.updateUnsafe(BigInt(0), Context.empty()) + activeShardsLatch.openUnsafe() + + return Effect.gen(function*() { + yield* Effect.logError("Shard lock storage is unhealthy", cause) + yield* Effect.forkIn(syncSingletons, shardingScope, { startImmediately: true }) + + for (const shardId of affectedShards) { + for (const state of entityManagers.values()) { + if (state.status === "closed") continue + yield* Effect.forkIn( + state.manager.interruptShard(shardId, { force: true }), + shardingScope, + { startImmediately: true } + ) + } + } + activeShardsLatch.openUnsafe() + }) + }) + + const markShardLocksHealthy = Effect.suspend(() => { + if (!shardLocksHealthyLatch.openUnsafe()) return Effect.void + + MutableHashSet.clear(selfShards) + MutableHashMap.forEach(shardAssignments, (runner, shardId) => { + if (isLocalRunner(runner)) { + MutableHashSet.add(selfShards, shardId) + } + }) + activeShardsLatch.openUnsafe() + return Effect.logInfo("Shard lock storage has recovered") + }) + + const refreshShardLocks = Effect.suspend(() => runnerStorage.refresh(selfAddress, [ ...acquiredShards, ...releasingShards @@ -421,12 +545,20 @@ const make = Effect.gen(function*() { times: 5, schedule: Schedule.spaced(50) }), - Effect.catchCause((cause) => - Effect.logError("Could not refresh shard locks", cause).pipe( - Effect.andThen(clearSelfShards) - ) - ), - Effect.repeat(Schedule.fixed(config.shardLockRefreshInterval)), + Effect.timeout(shardLockInterval), + Effect.catchCause(markShardLocksUnhealthy) + ) + + const probeShardLocks = runnerStorage.refresh(selfAddress, []).pipe( + Effect.timeout(shardLockInterval), + Effect.andThen(markShardLocksHealthy), + Effect.catchCause(() => Effect.void) + ) + + // Refresh shard locks at the lease-safe interval, or probe storage while + // lock ownership is uncertain. + yield* Effect.suspend(() => shardLocksHealthyLatch.isOpen() ? refreshShardLocks : probeShardLocks).pipe( + Effect.repeat(Schedule.fixed(shardLockInterval)), Effect.forever, Effect.forkIn(shardingScope) ) @@ -439,11 +571,6 @@ const make = Effect.gen(function*() { ) } - const clearSelfShards = Effect.sync(() => { - MutableHashSet.clear(selfShards) - activeShardsLatch.openUnsafe() - }) - // --- Storage inbox --- // // Responsible for reading unprocessed messages from storage and sending them @@ -466,7 +593,6 @@ const make = Effect.gen(function*() { const entityRegistrationTimeoutMillis = Duration.toMillis( Duration.fromInputUnsafe(config.entityRegistrationTimeout) ) - const storageStartMillis = clock.currentTimeMillisUnsafe() yield* Effect.gen(function*() { yield* Effect.logDebug("Starting") @@ -492,8 +618,15 @@ const make = Effect.gen(function*() { } const state = entityManagers.get(address.entityType) if (!state) { - const sinceStart = clock.currentTimeMillisUnsafe() - storageStartMillis - if (sinceStart < entityRegistrationTimeoutMillis) { + const now = clock.currentTimeMillisUnsafe() + const registrationStarted = entityRegistrationStartMillis !== undefined + const timeoutStartMillis = entityRegistrationStartMillis ?? + (entityRegistrationFallbackStartMillis ??= now) + // If registration never starts, allow two intervals from the first missing read before failing. + const timeoutMillis = registrationStarted + ? entityRegistrationTimeoutMillis + : entityRegistrationTimeoutMillis * 2 + if (now - timeoutStartMillis < timeoutMillis) { // reset address in the case that the entity is slow to register MutableHashSet.add(resetAddresses, address) return Effect.void @@ -646,7 +779,7 @@ const make = Effect.gen(function*() { const resumptionState = Option.getOrThrow(MutableHashMap.get(entityResumptionState, address)) let done = false - while (!done) { + while (!done) { // oxlint-disable-line no-unmodified-loop-condition // if the shard is no longer assigned to this runner, we stop if (!MutableHashSet.has(acquiredShards, address.shardId)) { return @@ -834,18 +967,40 @@ const make = Effect.gen(function*() { retries?: number ): Effect.Effect< void, - MailboxFull | AlreadyProcessingMessage | PersistenceError + MailboxFull | AlreadyProcessingMessage | PersistenceError | EntityNotAssignedToRunner > { + const isPersisted = Context.get( + message._tag === "OutgoingRequest" ? message.annotations : message.rpc.annotations, + Persisted + ) + const shouldFail = !discard && + (message._tag === "OutgoingRequest" || message.envelope._tag === "AckChunk") + const abandon = (error: EntityNotAssignedToRunner) => { + if (!isPersisted) { + return shouldFail + ? Effect.fail(error) + : Effect.logDebug("Abandoning outgoing message during shutdown", message.envelope.address) + } + const persist = message._tag === "OutgoingRequest" + ? storage.saveRequest(message) + : storage.saveEnvelope(message) + return Effect.catchTag(persist, "MalformedMessage", Effect.die).pipe( + Effect.andThen( + shouldFail + ? Effect.fail(error) + : Effect.logWarning("Persisting outgoing message abandoned during shutdown", message.envelope.address) + ) + ) + } return Effect.catchFilter( Effect.suspend(() => { const address = message.envelope.address - const isPersisted = Context.get( - message._tag === "OutgoingRequest" ? message.annotations : message.rpc.annotations, - Persisted - ) if (isPersisted && !storageEnabled) { return Effect.die("Sharding.sendOutgoing: Persisted messages require MessageStorage") } + if (shouldFail && MutableRef.get(isShutdown)) { + return Effect.fail(new EntityNotAssignedToRunner({ address })) + } const maybeRunner = MutableHashMap.get(shardAssignments, address.shardId) const runnerIsLocal = Option.isSome(maybeRunner) && isLocalRunner(maybeRunner.value) if (isPersisted) { @@ -857,6 +1012,8 @@ const make = Effect.gen(function*() { } return runnerIsLocal ? sendLocal(message) + : discard + ? runnersService.notify({ address: maybeRunner, message, discard }) : runnersService.send({ address: maybeRunner.value, message }) }), (error) => @@ -864,6 +1021,15 @@ const make = Effect.gen(function*() { ? Result.succeed(error) : Result.fail(error), (error) => { + // Abandon the message during teardown: retrying would loop forever once the runner is shutting down + if (error._tag === "EntityNotAssignedToRunner") { + const targetManager = entityManagers.get(message.envelope.address.entityType) + const cannotRecover = MutableRef.get(isShutdown) || + (targetManager !== undefined && targetManager.status !== "alive") + if (cannotRecover) { + return abandon(error) + } + } if (retries === 0) { return Effect.die(error) } @@ -977,7 +1143,7 @@ const make = Effect.gen(function*() { if (newAssignments) { const runner = newAssignments[i] MutableHashMap.set(shardAssignments, shard, runner) - if (isLocalRunner(runner)) { + if (shardLocksHealthyLatch.isOpen() && isLocalRunner(runner)) { MutableHashSet.add(selfShards, shard) } } else { @@ -1031,7 +1197,7 @@ const make = Effect.gen(function*() { Entity, (entityId: string) => RpcClient.RpcClient< any, - MailboxFull | AlreadyProcessingMessage + MailboxFull | AlreadyProcessingMessage | EntityNotAssignedToRunner >, never > = yield* ResourceMap.make( @@ -1044,7 +1210,7 @@ const make = Effect.gen(function*() { flatten: true, onFromClient(options): Effect.Effect< void, - MailboxFull | AlreadyProcessingMessage | PersistenceError + MailboxFull | AlreadyProcessingMessage | PersistenceError | EntityNotAssignedToRunner > { const address = Context.getUnsafe(options.context, ClientAddressTag) switch (options.message._tag) { @@ -1077,7 +1243,7 @@ const make = Effect.gen(function*() { if (!options.discard) { const entry: ClientRequestEntry = { rpc: rpc as any, - context: fiber.currentContext, + context: fiber.context, message } clientRequests.set(id, entry) @@ -1167,12 +1333,14 @@ const make = Effect.gen(function*() { return entity.protocol.requests.has(p as string) }, get(target, p) { - if (p in target) { + if (Object.hasOwn(target, p)) { 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 method = (payload: any, options?: {}) => clientFn(p as string, payload, options) + InternalRecord.assignProperty(target, p, method) + return method } }) } @@ -1183,7 +1351,7 @@ const make = Effect.gen(function*() { const makeClient = (entity: Entity): Effect.Effect< ( entityId: string - ) => RpcClient.RpcClient.From + ) => RpcClient.RpcClient.From > => clients.get(entity) as any const clientRespondDiscard = (_reply: Reply.Reply) => Effect.void @@ -1309,12 +1477,11 @@ const make = Effect.gen(function*() { runnerAddress, sharding }).pipe( - Effect.provideContext(Context.mutate(services, (services) => - services.pipe( - Context.add(EntityReaper, reaper), - Context.add(Scope.Scope, scope), - Context.add(Snowflake.Generator, snowflakeGen) - ))) + Effect.provideContext(services.pipe( + Context.add(EntityReaper, reaper), + Context.add(Scope.Scope, scope), + Context.add(Snowflake.Generator, snowflakeGen) + )) ) as Effect.Effect const state: EntityManagerState = { entity, @@ -1335,6 +1502,7 @@ const make = Effect.gen(function*() { // register entities while storage is idle // this ensures message order is preserved yield* withStorageReadLock(Effect.sync(() => { + entityRegistrationStartMillis ??= clock.currentTimeMillisUnsafe() entityManagers.set(entity.type, state) if (entityManagerLatches.has(entity.type)) { entityManagerLatches.get(entity.type)!.openUnsafe() diff --git a/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts b/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts index 5b5c18ae1..e9a465d63 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/ShardingConfig.ts @@ -23,7 +23,7 @@ import { RunnerAddress } from "./RunnerAddress.ts" /** * Represents the configuration for the `Sharding` service on a given runner. * - * @category models + * @category services * @since 4.0.0 */ export class ShardingConfig extends Context.Service): Layer.Layer /** * Layer that provides the default `ShardingConfig` values. * - * @category defaults + * @category layers * @since 4.0.0 */ export const layerDefaults: Layer.Layer = layer() @@ -349,7 +353,7 @@ export const layerFromEnv = (options?: Partial | unde * Normalizes the provided `ShardingConfig` to calculate the `available` and * `assigned` shard groups. * - * @category Shard groups + * @category converting * @since 4.0.0 */ export const shardGroupConfig = (config: ShardingConfig["Service"]): { diff --git a/.context/effect/packages/effect/src/unstable/cluster/SingleRunner.ts b/.context/effect/packages/effect/src/unstable/cluster/SingleRunner.ts index 3b150c9a7..3f3cdb906 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/SingleRunner.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/SingleRunner.ts @@ -12,6 +12,7 @@ */ import * as Layer from "effect/Layer" import type { ConfigError } from "../../Config.ts" +import type * as Crypto from "../../Crypto.ts" import type * as SqlClient from "../sql/SqlClient.ts" import type * as MessageStorage from "./MessageStorage.ts" import * as RunnerHealth from "./RunnerHealth.ts" @@ -42,7 +43,8 @@ import * as SqlRunnerStorage from "./SqlRunnerStorage.ts" * **Gotchas** * * - Even when `runnerStorage` is `"memory"`, message storage remains - * SQL-backed, so callers must still provide `SqlClient`. + * SQL-backed, so callers must still provide `SqlClient` and `Crypto.Crypto` + * (used to hash over-length message deduplication keys). * - Runner communication and runner health are no-op services, so this layer is * for single-process use rather than multi-runner coordination. * @@ -62,7 +64,7 @@ export const layer = (options?: { | Runners.Runners | MessageStorage.MessageStorage, ConfigError, - SqlClient.SqlClient + SqlClient.SqlClient | Crypto.Crypto > => Sharding.layer.pipe( Layer.provideMerge(Runners.layerNoop), diff --git a/.context/effect/packages/effect/src/unstable/cluster/SingletonAddress.ts b/.context/effect/packages/effect/src/unstable/cluster/SingletonAddress.ts index 5479ad742..2261e6e75 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/SingletonAddress.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/SingletonAddress.ts @@ -17,7 +17,7 @@ const TypeId = "~effect/cluster/SingletonAddress" /** * Represents the unique address of an singleton within the cluster. * - * @category address + * @category schemas * @since 4.0.0 */ export class SingletonAddress extends Schema.Class(TypeId)({ diff --git a/.context/effect/packages/effect/src/unstable/cluster/Snowflake.ts b/.context/effect/packages/effect/src/unstable/cluster/Snowflake.ts index 37c638f9e..8adf7576e 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/Snowflake.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/Snowflake.ts @@ -170,7 +170,7 @@ export const make = (options: { /** * Extracts the Unix timestamp in milliseconds from a snowflake id. * - * @category parts + * @category getters * @since 4.0.0 */ export const timestamp = (snowflake: Snowflake): number => Number(snowflake >> constBigInt22) + sinceUnixEpoch @@ -178,7 +178,7 @@ export const timestamp = (snowflake: Snowflake): number => Number(snowflake >> c /** * Extracts the timestamp from a snowflake id as a `DateTime.Utc`. * - * @category parts + * @category getters * @since 4.0.0 */ export const dateTime = (snowflake: Snowflake): DateTime.Utc => DateTime.makeUnsafe(timestamp(snowflake)) @@ -186,7 +186,7 @@ export const dateTime = (snowflake: Snowflake): DateTime.Utc => DateTime.makeUns /** * Extracts the machine id component from a snowflake id. * - * @category parts + * @category getters * @since 4.0.0 */ export const machineId = (snowflake: Snowflake): MachineId => @@ -195,7 +195,7 @@ export const machineId = (snowflake: Snowflake): MachineId => /** * Extracts the per-machine sequence component from a snowflake id. * - * @category parts + * @category getters * @since 4.0.0 */ export const sequence = (snowflake: Snowflake): number => Number(snowflake % constBigInt4096) @@ -203,7 +203,7 @@ export const sequence = (snowflake: Snowflake): number => Number(snowflake % con /** * Decomposes a snowflake id into its timestamp, machine id, and sequence parts. * - * @category parts + * @category converting * @since 4.0.0 */ export const toParts = (snowflake: Snowflake): Snowflake.Parts => ({ @@ -221,7 +221,7 @@ export const toParts = (snowflake: Snowflake): Snowflake.Parts => ({ * backward, resets the sequence each millisecond, and advances the timestamp when * more than 4096 ids are requested in the same millisecond. * - * @category Generator + * @category constructors * @since 4.0.0 */ export const makeGenerator: Effect.Effect = Effect.gen(function*() { @@ -265,7 +265,7 @@ export const makeGenerator: Effect.Effect = Effect.gen(func /** * Context service for a stateful snowflake id generator. * - * @category Generator + * @category services * @since 4.0.0 */ export class Generator extends Context.Service< @@ -276,7 +276,7 @@ export class Generator extends Context.Service< /** * Layer that provides the default snowflake `Generator` service. * - * @category Generator + * @category layers * @since 4.0.0 */ export const layerGenerator: Layer.Layer = Layer.effect(Generator)(makeGenerator) diff --git a/.context/effect/packages/effect/src/unstable/cluster/SqlMessageStorage.ts b/.context/effect/packages/effect/src/unstable/cluster/SqlMessageStorage.ts index 63c1c38c4..e8c2ed4c0 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/SqlMessageStorage.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/SqlMessageStorage.ts @@ -8,13 +8,21 @@ * storage constructor, layers, migrations, optional table prefixes, and the row * mapping needed by encoded message storage. * + * Request deduplication keys that exceed the 255-character `message_id` + * column are hashed with the `Crypto` service before they are written, so + * composed keys of any length are supported; shorter keys are stored as + * plaintext, byte-compatible with rows written by previous versions. + * * @since 4.0.0 */ // eslint-disable effect/no-bigint-literals import * as Arr from "../../Array.ts" +import * as Crypto from "../../Crypto.ts" import * as Effect from "../../Effect.ts" +import * as Encoding from "../../Encoding.ts" import * as Layer from "../../Layer.ts" import * as Option from "../../Option.ts" +import type * as PlatformError from "../../PlatformError.ts" import * as Schedule from "../../Schedule.ts" import * as Migrator from "../sql/Migrator.ts" import * as SqlClient from "../sql/SqlClient.ts" @@ -61,9 +69,10 @@ export const make: (options?: { }) => Effect.Effect< MessageStorage.MessageStorage["Service"], never, - SqlClient.SqlClient | Snowflake.Generator + SqlClient.SqlClient | Snowflake.Generator | Crypto.Crypto > = Effect.fnUntraced(function*(options) { const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const crypto = yield* Crypto.Crypto const prefix = options?.prefix ?? "cluster" const table = (name: string) => `${prefix}_${name}` @@ -84,6 +93,30 @@ export const make: (options?: { const repliesTable = table("replies") const repliesTableSql = sql(repliesTable) + // The composed primary key (`entityType/entityId/tag/id`) can legally exceed + // the 255-character `message_id` column: entity_type(150) + entity_id(255) + + // tag(50) alone total 458 characters before the RPC primary key is appended. + // Keys that fit are stored as-is, keeping `message_id` byte-compatible with + // rows written by previous versions; longer keys are stored as a SHA-256 + // digest (64 hex characters, collision probability negligible at a 2^128 + // birthday bound). Digests never contain "/" while composed keys always do, + // so the two encodings cannot collide. + const encoder = new TextEncoder() + const messageIdForPrimaryKey = (primaryKey: string): Effect.Effect => + primaryKey.length <= 255 + ? Effect.succeed(primaryKey) + : Effect.map(crypto.digest("SHA-256", encoder.encode(primaryKey)), Encoding.encodeHex) + + const messageIdEnforcesWidth = sql.onDialectOrElse({ + mssql: () => true, + mysql: () => true, + pg: () => true, + orElse: () => false + }) + // sqlite's TEXT message_id column stored over-length plaintext keys before + // digests were introduced; those legacy rows need a plaintext fallback read + const mayHaveLegacyRow = (primaryKey: string): boolean => !messageIdEnforcesWidth && primaryKey.length > 255 + const envelopeToRow = ( envelope: Envelope.Encoded, message_id: string | null, @@ -238,6 +271,14 @@ export const make: (options?: { const sqlFalse = sql.literal(supportsBooleans ? "FALSE" : "0") const sqlTrue = sql.literal(supportsBooleans ? "TRUE" : "1") + const selectByMessageId = (message_id: string): Effect.Effect, SqlError> => + 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} + ` + const insertEnvelope: ( row: MessageRow, message_id: string @@ -250,12 +291,7 @@ export const make: (options?: { `.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} - ` + return selectByMessageId(message_id) })), mysql: () => (row, message_id) => Effect.flatMap( @@ -264,12 +300,7 @@ export const make: (options?: { 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} - ` + return selectByMessageId(message_id) } ), mssql: () => (row, message_id) => @@ -311,12 +342,7 @@ export const make: (options?: { 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( + selectByMessageId(message_id).pipe( Effect.tap(sql`INSERT OR IGNORE INTO ${messagesTableSql} ${sql.insert(row)}`), sql.withTransaction, Effect.retry({ times: 3 }) @@ -406,18 +432,30 @@ export const make: (options?: { 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 - ) + let insert: Effect.Effect, SqlError | PlatformError.PlatformError> + if (primaryKey !== null) { + insert = Effect.flatMap(messageIdForPrimaryKey(primaryKey), (messageId) => { + const row = envelopeToRow(envelope, messageId, deliverAt) + if (!mayHaveLegacyRow(primaryKey)) { + return insertEnvelope(row, messageId) + } + return Effect.flatMap( + selectByMessageId(primaryKey), + (rows) => rows.length > 0 ? Effect.succeed(rows) : insertEnvelope(row, messageId) + ) + }) + } else { + const row = envelopeToRow(envelope, null, deliverAt) + insert = 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) => { @@ -488,7 +526,15 @@ export const make: (options?: { ), requestIdForPrimaryKey: (primaryKey) => - sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}`.pipe( + messageIdForPrimaryKey(primaryKey).pipe( + Effect.flatMap((messageId) => + sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${messageId}` + ), + Effect.flatMap((rows) => + rows.length === 0 && mayHaveLegacyRow(primaryKey) + ? sql<{ id: string | bigint }>`SELECT id FROM ${messagesTableSql} WHERE message_id = ${primaryKey}` + : Effect.succeed(rows) + ), Effect.map((rows) => Option.map(Option.fromNullishOr(rows[0]?.id), Snowflake.Snowflake)), Effect.provideService(SqlClient.SafeIntegers, true), PersistenceError.refail, @@ -645,7 +691,9 @@ export const make: (options?: { * * The layer runs the SQL migrations through `make`, provides `MessageStorage`, * and supplies `Snowflake.layerGenerator` internally. Callers still provide - * `SqlClient` and `ShardingConfig`. + * `SqlClient`, `ShardingConfig`, and `Crypto.Crypto`, which is used to hash + * message deduplication keys that would overflow the fixed-width + * `message_id` column. * * **Gotchas** * @@ -662,7 +710,7 @@ export const make: (options?: { export const layer: Layer.Layer< MessageStorage.MessageStorage, never, - SqlClient.SqlClient | ShardingConfig + SqlClient.SqlClient | ShardingConfig | Crypto.Crypto > = Layer.effect(MessageStorage.MessageStorage, make()).pipe( Layer.provide(Snowflake.layerGenerator) ) @@ -675,7 +723,7 @@ export const layer: Layer.Layer< */ export const layerWith = (options: { readonly prefix?: string | undefined -}): Layer.Layer => +}): Layer.Layer => Layer.effect(MessageStorage.MessageStorage, make(options)).pipe( Layer.provide(Snowflake.layerGenerator) ) @@ -993,7 +1041,7 @@ const replyKind = { } as const satisfies Record["_tag"], number | null> const replyFromRow = (row: ReplyRow): Reply.Encoded => - Number(row.kind) === replyKind.WithExit ? + row.kind !== null && Number(row.kind) === replyKind.WithExit ? { _tag: "WithExit", id: String(row.id), diff --git a/.context/effect/packages/effect/src/unstable/cluster/SqlRunnerStorage.ts b/.context/effect/packages/effect/src/unstable/cluster/SqlRunnerStorage.ts index c57874754..c5f6f8875 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/SqlRunnerStorage.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/SqlRunnerStorage.ts @@ -12,6 +12,8 @@ import * as Arr from "../../Array.ts" import * as Duration from "../../Duration.ts" import * as Effect from "../../Effect.ts" +import * as Exit from "../../Exit.ts" +import * as Fiber from "../../Fiber.ts" import * as Layer from "../../Layer.ts" import * as Scope from "../../Scope.ts" import * as SqlClient from "../sql/SqlClient.ts" @@ -19,12 +21,24 @@ import type { SqlError } from "../sql/SqlError.ts" import type * as Statement from "../sql/Statement.ts" import { PersistenceError } from "./ClusterError.ts" import { ResourceRef } from "./internal/resourceRef.ts" +import { effectiveInterval } from "./internal/shardLock.ts" import * as RunnerStorage from "./RunnerStorage.ts" import * as ShardId from "./ShardId.ts" import * as ShardingConfig from "./ShardingConfig.ts" const withTracerDisabled = Effect.withTracerEnabled(false) +// This exact FNV-1a hash, including its tag and UTF-8 encoding, is a persistent +// advisory-lock wire format and must never change. +const postgresLockNamespace = (prefix: string): number => { + const bytes = new TextEncoder().encode(`effect-cluster:${prefix}`) + let hash = 0x811c9dc5 + for (let i = 0; i < bytes.length; i++) { + hash = Math.imul(hash ^ bytes[i], 0x01000193) + } + return hash | 0 +} + /** * Creates a SQL-backed `RunnerStorage` implementation for registered runners and * shard locks, using the configured table prefix and advisory locks where @@ -60,18 +74,34 @@ export const make = Effect.fnUntraced(function*(options: { const shardGroups = ShardingConfig.shardGroupConfig(config) const availableShardGroups = Array.from(shardGroups.available) const disableAdvisoryLocks = config.shardLockDisableAdvisory + const lockOperationInterval = effectiveInterval(config) const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const layerScope = yield* Effect.scope const prefix = options?.prefix ?? "cluster" const table = (name: string) => `${prefix}_${name}` + const pgLockNamespace = postgresLockNamespace(prefix) + // PostgreSQL exposes the signed int4 lock key through pg_locks as an unsigned oid. + const pgLockNamespaceOid = pgLockNamespace >>> 0 + // Keep all PostgreSQL and MySQL shard-lock operations on a rebuildable + // reserved connection, including when advisory locks are disabled. const acquireLockConn = sql.onDialectOrElse({ pg: () => Effect.fnUntraced(function*(scope: Scope.Scope) { const conn = yield* Effect.orDie(sql.reserve).pipe( Scope.provide(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()", []))) + const pid = disableAdvisoryLocks + ? 0 + : (yield* conn.executeValues("SELECT pg_backend_pid()", []))[0][0] as number + if (!disableAdvisoryLocks) { + yield* Scope.addFinalizerExit(scope, () => + conn.executeRaw("SELECT pg_advisory_unlock_all()", []).pipe( + Effect.timeout(lockOperationInterval), + Effect.interruptible, + Effect.ignoreCause + )) + } return [conn, pid] as const }, Effect.orDie), mysql: () => @@ -79,6 +109,7 @@ export const make = Effect.fnUntraced(function*(options: { const conn = yield* Effect.orDie(sql.reserve).pipe( Scope.provide(scope) ) + if (disableAdvisoryLocks) return [conn, 0] as const // we need to get the connection id using IS_USED_LOCK to properly // support vitess let pid: number | undefined = undefined @@ -91,12 +122,79 @@ export const make = Effect.fnUntraced(function*(options: { if (taken[0] === null) continue pid = taken[1] } - yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", []))) + if (!disableAdvisoryLocks) { + yield* Scope.addFinalizerExit(scope, () => + conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", []).pipe( + Effect.timeout(lockOperationInterval), + Effect.interruptible, + Effect.ignoreCause + )) + } return [conn, pid] as const }, Effect.orDie), orElse: () => undefined }) - const lockConn = acquireLockConn && (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn)) + const lockConn = acquireLockConn && (yield* ResourceRef.from(layerScope, acquireLockConn)) + + // `Effect.timeout` waits for the timed-out effect to finish interrupting, so + // an operation stuck in an uninterruptible region (such as a scope finalizer + // releasing an unresponsive connection) can outlive its deadline. Fork the + // operation and timeout the join instead, leaving stalled cleanup to finish + // detached in the layer scope. + const withDeadline = Effect.fnUntraced(function*(operation: Effect.Effect) { + const fiber = yield* Effect.forkIn(operation, layerScope, { startImmediately: true }) + return yield* Fiber.join(fiber).pipe( + Effect.timeout(lockOperationInterval), + Effect.ensuring(Effect.suspend(() => + fiber.pollUnsafe() !== undefined ? Effect.void : Fiber.interrupt(fiber).pipe( + Effect.forkIn(layerScope, { startImmediately: true }), + Effect.asVoid + ) + )) + ) + }) + + let lockConnRebuilding = false + // Incremented every time the reserved connection is replaced, so failures + // from operations that ran on an already replaced connection do not trigger + // another rebuild. + let lockConnGeneration = 0 + const rebuildLockConn = (generation: number) => { + if ( + !lockConn || + lockConnRebuilding || + generation !== lockConnGeneration || + lockConn.state.current._tag === "Closed" + ) return Effect.void + lockConnRebuilding = true + // The rebuild starts by closing the previous scope, releasing the + // unresponsive connection back to the pool. Bound it with `withDeadline` + // so a release that never completes cannot leave `lockConnRebuilding` set + // forever, which would disable every subsequent rebuild. + return withDeadline(lockConn.rebuildUnsafe()).pipe( + Effect.exit, + Effect.tap((exit) => + Effect.sync(() => { + if (Exit.isSuccess(exit) && lockConn.state.current._tag === "Acquired") { + lockConnGeneration++ + } + }) + ), + Effect.ensuring(Effect.sync(() => { + lockConnRebuilding = false + })), + Effect.forkIn(layerScope, { startImmediately: true }), + Effect.asVoid + ) + } + // Rebuild the reserved connection when `effect` fails on it. Failures keep + // scheduling rebuilds, so a rebuilt connection that is also unresponsive is + // replaced again. + const onErrorRebuildLockConn = (effect: Effect.Effect): Effect.Effect => + Effect.suspend(() => { + const generation = lockConnGeneration + return Effect.onError(effect, () => rebuildLockConn(generation)) + }) const runnersTable = table("runners") const runnersTableSql = sql(runnersTable) @@ -276,7 +374,7 @@ export const make = Effect.fnUntraced(function*(options: { const [query, params] = effect.compile() return lockConn.await.pipe( Effect.flatMap(([conn]) => conn.executeRaw(query, params)), - Effect.onError(() => lockConn.rebuildUnsafe()) + onErrorRebuildLockConn ) } const execWithLockConnUnprepared = ( @@ -286,7 +384,7 @@ export const make = Effect.fnUntraced(function*(options: { const [query, params] = effect.compile() return lockConn.await.pipe( Effect.flatMap(([conn]) => conn.executeUnprepared(query, params, undefined)), - Effect.onError(() => lockConn.rebuildUnsafe()) + onErrorRebuildLockConn ) } const execWithLockConnValues = ( @@ -296,7 +394,7 @@ export const make = Effect.fnUntraced(function*(options: { const [query, params] = effect.compile() return lockConn.await.pipe( Effect.flatMap(([conn]) => conn.executeValues(query, params)), - Effect.onError(() => lockConn.rebuildUnsafe()) + onErrorRebuildLockConn ) } @@ -314,6 +412,7 @@ export const make = Effect.fnUntraced(function*(options: { WHERE ${locksTableSql}.address = ${address} OR ${locksTableSql}.acquired_at < ${lockExpiresAt} `.pipe( + execWithLockConn, Effect.andThen(acquiredLocks(address, shardIds)) ) } @@ -323,12 +422,14 @@ export const make = Effect.fnUntraced(function*(options: { 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`, + `SELECT objid FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND classid = ${pgLockNamespaceOid} AND objsubid = 2 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)!) + const shardId = lockNumbersReverse.get(lockNum) + if (shardId === undefined) continue + acquiredShardIds.push(shardId) toAcquire.delete(lockNum) } if (toAcquire.size === 0) { @@ -342,7 +443,7 @@ export const make = Effect.fnUntraced(function*(options: { } } return acquiredShardIds - }, Effect.onError(() => lockConn!.rebuildUnsafe())) + }, onErrorRebuildLockConn) }, mysql: () => { @@ -356,7 +457,8 @@ export const make = Effect.fnUntraced(function*(options: { 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( +`.pipe( + execWithLockConnUnprepared, Effect.andThen(acquiredLocks(address, shardIds)) ) } @@ -385,7 +487,7 @@ export const make = Effect.fnUntraced(function*(options: { } } return acquiredShardIds - }, Effect.onError(() => lockConn!.rebuildUnsafe())) + }, onErrorRebuildLockConn) }, mssql: () => (address: string, shardIds: ReadonlyArray) => { @@ -460,7 +562,7 @@ export const make = Effect.fnUntraced(function*(options: { const pgLocks = (shardIdsMap: Map) => Array.from( shardIdsMap.entries(), - ([lockNum, shardId]) => `pg_try_advisory_lock(${lockNum}) AS "${shardId}"` + ([lockNum, shardId]) => `pg_try_advisory_lock(${pgLockNamespace}, ${lockNum}) AS "${shardId}"` ).join(", ") const mysqlLocks = (shardIds: ReadonlyArray) => @@ -477,7 +579,8 @@ export const make = Effect.fnUntraced(function*(options: { WHERE address = ${address} AND acquired_at >= ${lockExpiresAt} AND shard_id IN ${stringLiteralArr(shardIds)} - `.values.pipe( + `.pipe( + execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string)) ) @@ -533,6 +636,72 @@ export const make = Effect.fnUntraced(function*(options: { `.pipe(execWithLockConnValues, Effect.map((rows) => rows.map((row) => row[0] as string))) }) + const withLockOperationDeadline = (operation: Effect.Effect) => + onErrorRebuildLockConn(withDeadline(operation)) + + const releaseShard = sql.onDialectOrElse({ + pg: () => { + if (disableAdvisoryLocks) { + return (address: string, shardId: string) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(execWithLockConn) + } + 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(${pgLockNamespace}, ${lockNum})`, []) + const takenLocks = yield* conn.executeValues( + `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND classid = ${pgLockNamespaceOid} AND objid = ${lockNum} AND objsubid = 2 AND pid = pg_backend_pid()`, + [] + ) + if (takenLocks.length === 0) return + } + const [conn] = yield* lockConn!.await + yield* conn.executeRaw(`SELECT pg_advisory_unlock_all()`, []) + }, + onErrorRebuildLockConn, + Effect.asVoid + ) + }, + mysql: () => { + if (disableAdvisoryLocks) { + return (address: string, shardId: string) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(execWithLockConn) + } + 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 + } + }, + onErrorRebuildLockConn, + Effect.asVoid + ) + }, + orElse: () => (address: string, shardId: string) => + sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}` + }) + + const releaseAllShards = sql.onDialectOrElse({ + pg: () => (address: string) => + disableAdvisoryLocks + ? sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(execWithLockConn) + : sql`SELECT pg_advisory_unlock_all()`.pipe(execWithLockConn, Effect.asVoid), + mysql: () => (address: string) => + disableAdvisoryLocks + ? sql`DELETE FROM ${locksTableSql} WHERE address = ${address}`.pipe(execWithLockConn) + : sql`SELECT RELEASE_ALL_LOCKS()`.pipe(execWithLockConn, Effect.asVoid), + orElse: () => (address: string) => sql`DELETE FROM ${locksTableSql} WHERE address = ${address}` + }) + return RunnerStorage.makeEncoded({ getRunners: sql`SELECT runner, healthy FROM ${runnersTableSql} WHERE last_heartbeat > ${lockExpiresAt}`.values.pipe( PersistenceError.refail, @@ -563,120 +732,37 @@ export const make = Effect.fnUntraced(function*(options: { ), acquire: (address, shardIds) => - acquireLock(address, shardIds).pipe( + withLockOperationDeadline(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([]), + withLockOperationDeadline( + sql`UPDATE ${runnersTableSql} SET last_heartbeat = ${sqlNow} WHERE address = ${address}`.pipe( + execWithLockConn, + shardIds.length > 0 ? + Effect.andThen(refreshShards(address, shardIds)) : + Effect.as([]) + ) + ).pipe( PersistenceError.refail, withTracerDisabled ), - 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, - withTracerDisabled - ) - } - 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!.rebuildUnsafe()), - Effect.asVoid, - PersistenceError.refail, - withTracerDisabled - ) - }, - mysql: () => { - if (disableAdvisoryLocks) { - return (address: string, shardId: string) => - sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe( - PersistenceError.refail, - withTracerDisabled - ) - } - 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!.rebuildUnsafe()), - Effect.asVoid, - PersistenceError.refail, - withTracerDisabled - ) - }, - orElse: () => (address, shardId) => - sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe( - PersistenceError.refail, - withTracerDisabled - ) - }), - - 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 - ) - }) + release: (address, shardId) => + withLockOperationDeadline(releaseShard(address, shardId)).pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ), + + releaseAll: (address) => + withLockOperationDeadline(releaseAllShards(address)).pipe( + Effect.asVoid, + PersistenceError.refail, + withTracerDisabled + ) }) }, withTracerDisabled) diff --git a/.context/effect/packages/effect/src/unstable/cluster/internal/entityManager.ts b/.context/effect/packages/effect/src/unstable/cluster/internal/entityManager.ts index ad655d89a..e568d3d07 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/internal/entityManager.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/internal/entityManager.ts @@ -56,7 +56,9 @@ export interface EntityManager { }) => boolean readonly clearProcessed: () => void - readonly interruptShard: (shardId: ShardId) => Effect.Effect + readonly interruptShard: (shardId: ShardId, options?: { + readonly force?: boolean + }) => Effect.Effect readonly activeEntityCount: Effect.Effect } @@ -108,7 +110,7 @@ export const make = Effect.fnUntraced(function*< const clock = yield* Clock const context = yield* Effect.context | Rpc.Middleware | RX>() const defectRetryPolicy = options.defectRetryPolicy - ? Schedule.andThen(options.defectRetryPolicy, defaultRetryPolicy) + ? Schedule.concat(options.defectRetryPolicy, defaultRetryPolicy) : defaultRetryPolicy const retryDriver = yield* Schedule.toStepWithSleep(defectRetryPolicy) const entityRpcs = new Map(entity.protocol.requests) @@ -117,7 +119,10 @@ export const make = Effect.fnUntraced(function*< entityRpcs.set(KeepAliveRpc._tag, KeepAliveRpc as any) const activeServers = new Map() - const serverCloseLatches = new Map() + const serverCloseLatches = new Map() const processedRequestIds = new Set() const entities: ResourceMap< @@ -132,12 +137,16 @@ export const make = Effect.fnUntraced(function*< const scope = yield* Effect.scope const endLatch = Latch.makeUnsafe() const keepAliveLatch = Latch.makeUnsafe() + const closeLatches = { + closed: Latch.makeUnsafe(), + force: Latch.makeUnsafe() + } // on shutdown, reset the storage for the entity yield* Scope.addFinalizerExit( scope, () => { - serverCloseLatches.get(address)?.openUnsafe() + serverCloseLatches.get(address)?.closed.openUnsafe() serverCloseLatches.delete(address) return Effect.void } @@ -154,14 +163,13 @@ export const make = Effect.fnUntraced(function*< Effect.fnUntraced(function*(scope) { let isShuttingDown = false - const handlerContext = Context.mutate(context, (context) => - context.pipe( - Context.add(CurrentAddress, address), - Context.add(CurrentRunnerAddress, options.runnerAddress), - Context.add(KeepAliveLatch, keepAliveLatch), - Context.add(Scope.Scope, scope), - Context.add(CurrentLogAnnotations, {}) - )) + const handlerContext = context.pipe( + Context.add(CurrentAddress, address), + Context.add(CurrentRunnerAddress, options.runnerAddress), + Context.add(KeepAliveLatch, keepAliveLatch), + Context.add(Scope.Scope, scope), + Context.add(CurrentLogAnnotations, {}) + ) // Initiate the behavior for the entity const handlers = yield* (entity.protocol.toHandlers(buildHandlers as any).pipe( @@ -373,14 +381,19 @@ export const make = Effect.fnUntraced(function*< scope, Effect.withFiber((fiber) => { activeServers.delete(address.entityId) - serverCloseLatches.set(address, Latch.makeUnsafe()) internalInterruptors.add(fiber.id) - return state.write(0, { _tag: "Eof" }).pipe( - Effect.andThen(Effect.interruptible(endLatch.await)), - Effect.timeoutOption(config.entityTerminationTimeout) + return Effect.raceFirst( + state.write(0, { _tag: "Eof" }).pipe( + Effect.andThen(endLatch.await), + Effect.timeoutOption(config.entityTerminationTimeout), + Effect.interruptible + ), + Effect.interruptible(closeLatches.force.await) ) }) ) + // Do not make shard interruption wait for an entity that is still building. + serverCloseLatches.set(address, closeLatches) activeServers.set(address.entityId, state) return state @@ -532,22 +545,29 @@ export const make = Effect.fnUntraced(function*< ) } - const decodeMessage = makeMessageDecode(entity, entityRpcs) + const decodeMessage = makeMessageDecode(entityRpcs) const runFork = Effect.runForkWith(context) return identity({ - interruptShard: (shardId: ShardId) => + interruptShard: (shardId: ShardId, options) => Effect.suspend(function loop(): Effect.Effect { const fibers = Arr.empty>() + if (options?.force === true) { + serverCloseLatches.forEach((latches, address) => { + if (shardId[Equal.symbol](address.shardId)) { + latches.force.openUnsafe() + } + }) + } activeServers.forEach((state) => { if (shardId[Equal.symbol](state.address.shardId)) { fibers.push(runFork(entities.removeIgnore(state.address))) } }) - serverCloseLatches.forEach((latch, address) => { + serverCloseLatches.forEach((latches, address) => { if (shardId[Equal.symbol](address.shardId)) { - fibers.push(runFork(latch.await)) + fibers.push(runFork(latches.closed.await)) } }) if (fibers.length === 0) return Effect.void @@ -629,10 +649,7 @@ const defaultRetryPolicy = Schedule.min([ Schedule.spaced("10 seconds") ]) -const makeMessageDecode = ( - entity: Entity, - entityRpcs: Map -) => { +const makeMessageDecode = (entityRpcs: Map) => { const decodeRequest = Effect.fnUntracedEager(function*( message: Message.IncomingRequest, rpc: Rpc.AnyWithProps @@ -670,8 +687,8 @@ const makeMessageDecode = ( if (!rpc) { return Effect.fail( new Schema.SchemaError( - new SchemaIssue.InvalidValue(Option.some(message), { - message: `Unknown tag ${message.envelope.tag} for entity type ${entity.type}` + new SchemaIssue.InvalidValue({ + message: "Expected a known entity RPC tag" }) ) ) diff --git a/.context/effect/packages/effect/src/unstable/cluster/internal/resourceMap.ts b/.context/effect/packages/effect/src/unstable/cluster/internal/resourceMap.ts index 427b8b43e..13377199b 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/internal/resourceMap.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/internal/resourceMap.ts @@ -65,7 +65,7 @@ export class ResourceMap { if (existing) { return Deferred.await(existing.deferred) } - const scope = Effect.runSync(Scope.make()) + const scope = Scope.makeUnsafe() const deferred = Deferred.makeUnsafe() backingSet(this.entries, key, { scope, deferred }) return Effect.onExit(this.lookup(key, scope), (exit) => { @@ -73,7 +73,10 @@ export class ResourceMap { return Deferred.done(deferred, exit) } backingDelete(this.entries, key) - return Deferred.done(deferred, exit) + return Effect.andThen( + Deferred.done(deferred, exit), + Scope.close(scope, exit) + ) }) }) } diff --git a/.context/effect/packages/effect/src/unstable/cluster/internal/resourceRef.ts b/.context/effect/packages/effect/src/unstable/cluster/internal/resourceRef.ts index 00ed414e6..e0976d042 100644 --- a/.context/effect/packages/effect/src/unstable/cluster/internal/resourceRef.ts +++ b/.context/effect/packages/effect/src/unstable/cluster/internal/resourceRef.ts @@ -1,3 +1,4 @@ +import type * as Cause from "../../../Cause.ts" import * as Effect from "../../../Effect.ts" import * as Exit from "../../../Exit.ts" import * as Latch from "../../../Latch.ts" @@ -7,7 +8,7 @@ import * as Scope from "../../../Scope.ts" import { internalInterruptors } from "./interruptors.ts" /** @internal */ -export type State = { +export type State = { readonly _tag: "Closed" } | { readonly _tag: "Acquiring" @@ -16,6 +17,10 @@ export type State = { readonly _tag: "Acquired" readonly scope: Scope.Closeable readonly value: A +} | { + readonly _tag: "Failed" + readonly scope: Scope.Closeable + readonly cause: Cause.Cause } /** @internal */ @@ -24,7 +29,7 @@ export class ResourceRef { parentScope: Scope.Scope, acquire: (scope: Scope.Scope) => Effect.Effect ) { - const state = MutableRef.make>({ _tag: "Closed" }) + const state = MutableRef.make>({ _tag: "Closed" }) yield* Scope.addFinalizerExit(parentScope, (exit) => { const s = MutableRef.get(state) @@ -44,10 +49,10 @@ export class ResourceRef { return new ResourceRef(state, acquire) }) - readonly state: MutableRef.MutableRef> + readonly state: MutableRef.MutableRef> readonly acquire: (scope: Scope.Scope) => Effect.Effect constructor( - state: MutableRef.MutableRef>, + state: MutableRef.MutableRef>, acquire: (scope: Scope.Scope) => Effect.Effect ) { this.state = state @@ -84,15 +89,32 @@ export class ResourceRef { MutableRef.set(this.state, { _tag: "Acquired", scope, value }) return this.latch.open }) + ).pipe( + Effect.onExit((exit) => { + if (Exit.isSuccess(exit)) { + return Effect.void + } + return Scope.close(scope, exit).pipe( + Effect.ensuring(Effect.sync(() => { + const state = this.state.current + if (state._tag === "Acquiring" && state.scope === scope) { + MutableRef.set(this.state, { _tag: "Failed", scope, cause: exit.cause }) + this.latch.openUnsafe() + } + })) + ) + }) ) } - await: Effect.Effect = Effect.suspend(() => { + 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) + } else if (s._tag === "Failed") { + return Effect.failCause(s.cause) } return Effect.flatMap(this.latch.await, () => this.await) }) diff --git a/.context/effect/packages/effect/src/unstable/cluster/internal/shardLock.ts b/.context/effect/packages/effect/src/unstable/cluster/internal/shardLock.ts new file mode 100644 index 000000000..7e37eed5e --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/cluster/internal/shardLock.ts @@ -0,0 +1,9 @@ +import * as Duration from "../../../Duration.ts" +import type { ShardingConfig } from "../ShardingConfig.ts" + +/** @internal */ +export const effectiveInterval = (config: ShardingConfig["Service"]): Duration.Duration => + Duration.min( + Duration.fromInputUnsafe(config.shardLockRefreshInterval), + Duration.divideUnsafe(Duration.fromInputUnsafe(config.shardLockExpiration), 3) + ) diff --git a/.context/effect/packages/effect/src/unstable/devtools/DevToolsClient.ts b/.context/effect/packages/effect/src/unstable/devtools/DevToolsClient.ts index ff7d7f3de..84bf14adc 100644 --- a/.context/effect/packages/effect/src/unstable/devtools/DevToolsClient.ts +++ b/.context/effect/packages/effect/src/unstable/devtools/DevToolsClient.ts @@ -184,7 +184,8 @@ const makeTracerEffect = Effect.gen(function*() { return Tracer.make({ span(options) { const span = currentTracer.span(options) - client.sendUnsafe(span) + // the span is mutated in place, so send a snapshot of its current state + client.sendUnsafe({ ...span }) const oldEvent = span.event span.event = function(this: Tracer.Span, name, startTime, attributes) { client.sendUnsafe({ @@ -201,7 +202,7 @@ const makeTracerEffect = Effect.gen(function*() { const oldEnd = span.end span.end = function(this: Tracer.Span, endTime, exit) { oldEnd.call(this, endTime, exit) - client.sendUnsafe(span) + client.sendUnsafe({ ...span }) } return span diff --git a/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts b/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts index 4778114e7..bdfc47f2d 100644 --- a/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts +++ b/.context/effect/packages/effect/src/unstable/devtools/DevToolsSchema.ts @@ -311,7 +311,7 @@ export type Counter = Schema.Schema.Type export const Frequency = metric( "Frequency", Schema.Struct({ - occurrences: Schema.ReadonlyMap(Schema.String, Schema.Number) + occurrences: Schema.ReadonlyMap(Schema.String, Schema.Natural) }) ) @@ -370,8 +370,8 @@ export type Gauge = Schema.Schema.Type export const Histogram = metric( "Histogram", Schema.Struct({ - buckets: Schema.Array(Schema.Tuple([Schema.Number, Schema.Number])), - count: Schema.Number, + buckets: Schema.Array(Schema.Tuple([Schema.Number, Schema.Natural])), + count: Schema.Natural, min: Schema.Number, max: Schema.Number, sum: Schema.Number @@ -405,8 +405,13 @@ export type Histogram = Schema.Schema.Type export const Summary = metric( "Summary", Schema.Struct({ - quantiles: Schema.Array(Schema.Tuple([Schema.Number, Schema.UndefinedOr(Schema.Number)])), - count: Schema.Number, + quantiles: Schema.Array( + Schema.Tuple([ + Schema.Finite.check(Schema.isBetween({ minimum: 0, maximum: 1 })), + Schema.UndefinedOr(Schema.Number) + ]) + ), + count: Schema.Natural, min: Schema.Number, max: Schema.Number, sum: Schema.Number diff --git a/.context/effect/packages/effect/src/unstable/encoding/Ini.ts b/.context/effect/packages/effect/src/unstable/encoding/Ini.ts new file mode 100644 index 000000000..34e3728cb --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/encoding/Ini.ts @@ -0,0 +1,174 @@ +/** + * Parses INI configuration files. + * + * This module contains the decoding surface used by Effect's CLI without + * pulling in the complete `ini` package. + * + * @since 4.0.0 + */ + +/* + * The parser is adapted from `ini` 7.0.0. + * + * Copyright (c) Isaac Z. Schlueter and Contributors + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +const hasOwn = Object.prototype.hasOwnProperty + +const splitSections = (value: string): Array => { + const sections: Array = [] + let start = 0 + let index = 0 + while ((index = value.indexOf(".", index)) !== -1) { + if (index === 0 || value[index - 1] !== "\\") { + sections.push(value.slice(start, index)) + start = index + 1 + } + index++ + } + sections.push(value.slice(start)) + return sections +} + +const isQuoted = (value: string): boolean => + value.length >= 2 && + ((value.startsWith("\"") && value.endsWith("\"")) || + (value.startsWith("'") && value.endsWith("'"))) + +const unquote = (input: string | undefined): string => { + let value = (input ?? "").trim() + if (isQuoted(value)) { + if (value[0] === "'") { + value = value.slice(1, -1) + } + try { + return JSON.parse(value) + } catch { + return value + } + } + + let escaped = false + let output = "" + for (const character of value) { + if (escaped) { + output += character === ";" || character === "#" || character === "\\" + ? character + : `\\${character}` + escaped = false + } else if (character === ";" || character === "#") { + break + } else if (character === "\\") { + escaped = true + } else { + output += character + } + } + return output.trim() +} + +/** + * Parses an INI document into a null-prototype record. + * + * Section names separated by dots become nested records, repeated keys ending + * in `[]` become arrays, and the scalar values `true`, `false`, and `null` are + * decoded. Other scalar values remain strings. + * + * @category decoding + * @since 4.0.0 + */ +export const parse = (input: string): Record => { + const output: Record = Object.create(null) + let current = output + const sectionPattern = /^\[([^\]]*)\]\s*$/ + const propertyPattern = /^([^=]+)(=(.*))?$/ + + for (const line of input.split(/[\r\n]+/g)) { + if (line.length === 0 || /^\s*(?:[;#]|$)/.test(line)) { + continue + } + + const sectionMatch = sectionPattern.exec(line) + if (sectionMatch !== null) { + const section = unquote(sectionMatch[1]) + if (section === "__proto__") { + current = Object.create(null) + } else { + current = output[section] ??= Object.create(null) + } + continue + } + + const propertyMatch = propertyPattern.exec(line) + if (propertyMatch === null) { + continue + } + + const rawKey = unquote(propertyMatch[1]) + const array = rawKey.length > 2 && rawKey.endsWith("[]") + const key = array ? rawKey.slice(0, -2) : rawKey + if (key === "__proto__") { + continue + } + + const rawValue = propertyMatch[2] === undefined ? true : unquote(propertyMatch[3]) + const value = rawValue === "true" || rawValue === "false" || rawValue === "null" + ? JSON.parse(rawValue) + : rawValue + + if (array) { + if (!hasOwn.call(current, key)) { + current[key] = [] + } else if (!Array.isArray(current[key])) { + current[key] = [current[key]] + } + } + if (Array.isArray(current[key])) { + current[key].push(value) + } else { + current[key] = value + } + } + + const remove: Array = [] + for (const section of Object.keys(output)) { + if (typeof output[section] !== "object" || output[section] === null || Array.isArray(output[section])) { + continue + } + + const parts = splitSections(section) + const last = parts.pop()! + const key = last.replace(/\\\./g, ".") + current = output + for (const part of parts) { + if (part === "__proto__") { + continue + } + if (!hasOwn.call(current, part) || typeof current[part] !== "object" || current[part] === null) { + current[part] = Object.create(null) + } + current = current[part] + } + if (current === output && key === last) { + continue + } + current[key] = output[section] + remove.push(section) + } + for (const section of remove) { + delete output[section] + } + return output +} diff --git a/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts b/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts index 3cc7fa6b9..ceb40f05a 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Msgpack.ts @@ -12,14 +12,13 @@ import { Packr, Unpackr } from "msgpackr" import * as Msgpackr from "msgpackr" import * as Arr from "../../Array.ts" +import * as Cause from "../../Cause.ts" import * as Channel from "../../Channel.ts" import * as ChannelSchema from "../../ChannelSchema.ts" import * as Data from "../../Data.ts" import * as Effect from "../../Effect.ts" import { dual } from "../../Function.ts" -import * as Option from "../../Option.ts" -import * as Predicate from "../../Predicate.ts" -import type * as Pull from "../../Pull.ts" +import * as Pull from "../../Pull.ts" import * as Schema from "../../Schema.ts" import * as SchemaIssue from "../../SchemaIssue.ts" import * as SchemaTransformation from "../../SchemaTransformation.ts" @@ -136,37 +135,55 @@ export const decode = (): Channel.Channel< Channel.fromTransform((upstream, _scope) => Effect.sync(() => { const unpackr = new Unpackr() - let incomplete: Uint8Array | undefined = undefined - return Effect.flatMap( - upstream, - function loop(chunk): Pull.Pull, IE | MsgPackError, Done> { - const out = Arr.empty() - for (let i = 0; i < chunk.length; i++) { - let buf = chunk[i] - if (incomplete !== undefined) { - const prev = buf - buf = new Uint8Array(incomplete.length + buf.length) - buf.set(incomplete) - buf.set(prev, incomplete.length) - incomplete = undefined - } - try { - out.push(...unpackr.unpackMultiple(buf)) - } catch (cause) { - const error: any = cause - if (error.incomplete) { - incomplete = buf.subarray(error.lastPosition) - if (error.values) { - out.push(...error.values) - } - } else { - return Effect.fail(new MsgPackError({ kind: "Unpack", cause })) + let incomplete: { + readonly bytes: Uint8Array + readonly cause: unknown + } | undefined = undefined + + const pull = Effect.suspend((): Pull.Pull, IE | MsgPackError, Done> => + Pull.matchEffect(upstream, { + onSuccess: loop, + onFailure: Effect.failCause, + onDone: (done): Pull.Pull => + incomplete === undefined + ? Cause.done(done) + : Effect.fail(new MsgPackError({ kind: "Unpack", cause: incomplete.cause })) + }) + ) + + function loop(chunk: Arr.NonEmptyReadonlyArray>): Pull.Pull< + Arr.NonEmptyReadonlyArray, + IE | MsgPackError, + Done + > { + const out = Arr.empty() + for (let i = 0; i < chunk.length; i++) { + let buf = chunk[i] + if (incomplete !== undefined) { + const prev = buf + buf = new Uint8Array(incomplete.bytes.length + buf.length) + buf.set(incomplete.bytes) + buf.set(prev, incomplete.bytes.length) + incomplete = undefined + } + try { + out.push(...unpackr.unpackMultiple(buf)) + } catch (cause) { + const error: any = cause + if (error.incomplete) { + incomplete = { bytes: buf.subarray(error.lastPosition), cause } + if (error.values) { + out.push(...error.values) } + } else { + return Effect.fail(new MsgPackError({ kind: "Unpack", cause })) } } - return Arr.isReadonlyArrayNonEmpty(out) ? Effect.succeed(out) : Effect.flatMap(upstream, loop) } - ) + return Arr.isReadonlyArrayNonEmpty(out) ? Effect.succeed(out) : pull + } + + return pull }) ) @@ -344,25 +361,29 @@ export const transformation: SchemaTransformation.Transformation< unknown, Uint8Array > = SchemaTransformation.transformOrFail({ - decode(e, _options) { + decode(e, options) { try { return Effect.succeed(Msgpackr.decode(e)) - } catch (cause) { + } catch { return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(e), { - message: Predicate.hasProperty(cause, "message") ? String(cause.message) : String(cause) - }) + new SchemaIssue.InvalidValue( + { expected: "valid MessagePack bytes" }, + e, + options + ) ) } }, - encode(t, _options) { + encode(t, options) { try { return Effect.succeed(Msgpackr.encode(t) as Uint8Array) - } catch (cause) { + } catch { return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(t), { - message: Predicate.hasProperty(cause, "message") ? String(cause.message) : String(cause) - }) + new SchemaIssue.InvalidValue( + { expected: "a MessagePack-serializable value" }, + t, + options + ) ) } } diff --git a/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts b/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts index 87bcce197..ce9e33b42 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Ndjson.ts @@ -73,7 +73,15 @@ export const encodeString = (): Channel.Channel< Channel.fromTransform((upstream, _scope) => Effect.succeed(Effect.flatMap(upstream, (input) => { try { - return Effect.succeed(Arr.of(input.map((item) => JSON.stringify(item)).join("\n") + "\n")) + return Effect.succeed(Arr.of( + input.map((item) => { + const output = JSON.stringify(item) + if (output === undefined) { + throw new TypeError("Value cannot be represented as JSON") + } + return output + }).join("\n") + "\n" + )) } catch (cause) { return Effect.fail(new NdjsonError({ kind: "Pack", cause })) } diff --git a/.context/effect/packages/effect/src/unstable/encoding/Sse.ts b/.context/effect/packages/effect/src/unstable/encoding/Sse.ts index 2adcf6322..6c0c0fde0 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/Sse.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/Sse.ts @@ -22,6 +22,72 @@ import * as Result from "../../Result.ts" import * as Schema from "../../Schema.ts" import * as SchemaTransformation from "../../SchemaTransformation.ts" +const SseErrorTypeId = "~effect/encoding/Sse/SseError" + +/** + * Error reason raised when pending Server-Sent Events state exceeds the + * configured maximum size. + * + * @category errors + * @since 4.0.0 + */ +export class EventTooLarge extends Data.TaggedError("EventTooLarge")<{ + readonly maxEventSize: number +}> { + override get message() { + return `Pending SSE event exceeded the maximum size of ${this.maxEventSize}` + } +} + +/** + * Union of Server-Sent Events decoding error reasons. + * + * @category errors + * @since 4.0.0 + */ +export type SseErrorReason = EventTooLarge + +/** + * Error raised when decoding a Server-Sent Events stream fails. + * + * @category errors + * @since 4.0.0 + */ +export class SseError extends Data.TaggedError("SseError")<{ + readonly reason: SseErrorReason +}> { + /** + * Marks this value as an SSE decoding error. + * + * @since 4.0.0 + */ + readonly [SseErrorTypeId] = SseErrorTypeId + + /** + * Delegates the public message to the underlying SSE error reason. + * + * @since 4.0.0 + */ + override get message() { + return this.reason.message + } +} + +/** + * Options for decoding Server-Sent Events streams. + * + * @category decoding + * @since 4.0.0 + */ +export interface DecodeOptions { + /** + * Maximum number of string code units retained for a pending event. The default is 10 MiB. + */ + readonly maxEventSize?: number | undefined +} + +const defaultMaxEventSize = 10 * 1024 * 1024 + /** * Creates a channel that parses Server-Sent Events text chunks into `Event` values. * @@ -33,9 +99,9 @@ import * as SchemaTransformation from "../../SchemaTransformation.ts" * @category decoding * @since 4.0.0 */ -export const decode = (): Channel.Channel< +export const decode = (options?: DecodeOptions): Channel.Channel< NonEmptyReadonlyArray, - IE | Retry, + IE | Retry | SseError, Done, NonEmptyReadonlyArray, IE, @@ -51,16 +117,19 @@ export const decode = (): Channel.Channel< } else { buffer.push(event) } - }) + }, options) const pump = Effect.flatMap(upstream, (arr) => { for (let i = 0; i < arr.length; i++) { - parser.feed(arr[i]) + const error = parser.feed(arr[i]) + if (error !== undefined) { + return Effect.fail(error) + } } return Effect.void }) - return Effect.suspend(function loop(): Pull.Pull, IE | Retry, Done> { + return Effect.suspend(function loop(): Pull.Pull, IE | Retry | SseError, Done> { if (Arr.isArrayNonEmpty(buffer)) { const out = buffer buffer = [] @@ -108,10 +177,11 @@ export const decodeSchema = < IE, Done >( - schema: S + schema: S, + options?: DecodeOptions ): Channel.Channel< NonEmptyReadonlyArray, - IE | Retry | Schema.SchemaError, + IE | Retry | SseError | Schema.SchemaError, Done, NonEmptyReadonlyArray, IE, @@ -119,7 +189,7 @@ export const decodeSchema = < S["DecodingServices"] > => Channel.pipeTo( - decode(), + decode(options), ChannelSchema.decode(EventEncoded.pipe( Schema.decodeTo(schema) ))() @@ -137,14 +207,15 @@ export const decodeSchema = < * @since 4.0.0 */ export const decodeDataSchema = ( - schema: Schema.ConstraintDecoder + schema: Schema.ConstraintDecoder, + options?: DecodeOptions ): Channel.Channel< NonEmptyReadonlyArray<{ readonly event: string readonly id: string | undefined readonly data: Type }>, - IE | Retry | Schema.SchemaError, + IE | Retry | SseError | Schema.SchemaError, Done, NonEmptyReadonlyArray, IE, @@ -156,7 +227,7 @@ export const decodeDataSchema = ( data: Schema.fromJsonString(schema) }) return Channel.pipeTo( - decode(), + decode(options), Channel.map( ChannelSchema.decode(eventSchema)(), Arr.map((event) => ({ ...event, id: event.id })) @@ -170,20 +241,23 @@ export const decodeDataSchema = ( * **Details** * * Call `feed` with text chunks to parse `Event` and `Retry` values through the - * callback, and call `reset` to clear any buffered event state. + * callback, and call `reset` to clear any buffered event state. `feed` returns + * an `SseError` if the pending event exceeds `maxEventSize`. * * @category decoding * @since 4.0.0 */ -export function makeParser(onParse: (event: AnyEvent) => void): Parser { +export function makeParser(onParse: (event: AnyEvent) => void, options?: DecodeOptions): Parser { + const maxEventSize = options?.maxEventSize ?? defaultMaxEventSize + // Processing state let isFirstChunk: boolean let buffer: string let startingPosition: number let startingFieldLength: number + let discardTrailingNewline: boolean // Event state - let eventId: string | undefined let lastEventId: string | undefined let eventName: string | undefined let data: string @@ -196,19 +270,20 @@ export function makeParser(onParse: (event: AnyEvent) => void): Parser { buffer = "" startingPosition = 0 startingFieldLength = -1 + discardTrailingNewline = false - eventId = undefined + lastEventId = undefined eventName = undefined data = "" } - function feed(chunk: string): void { + function feed(chunk: string): SseError | undefined { buffer = buffer ? buffer + chunk : chunk - // Strip any UTF8 byte order mark (BOM) at the start of the stream. + // Strip any UTF-8 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)) { + if (isFirstChunk && buffer.startsWith(BOM)) { buffer = buffer.slice(BOM.length) } @@ -217,7 +292,6 @@ export function makeParser(onParse: (event: AnyEvent) => void): Parser { // 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) { @@ -271,6 +345,12 @@ export function makeParser(onParse: (event: AnyEvent) => void): Parser { // portion of the buffer only buffer = buffer.slice(position) } + + if (buffer.length + data.length > maxEventSize) { + const error = new SseError({ reason: new EventTooLarge({ maxEventSize }) }) + reset() + return error + } } function parseEventStreamLine( @@ -284,12 +364,11 @@ export function makeParser(onParse: (event: AnyEvent) => void): Parser { if (data.length > 0) { onParse({ _tag: "Event", - id: eventId, - event: eventName ?? "message", + id: lastEventId, + event: eventName || "message", data: data.slice(0, -1) // remove trailing newline }) data = "" - eventId = undefined } eventName = undefined return @@ -316,35 +395,30 @@ export function makeParser(onParse: (event: AnyEvent) => void): Parser { } else if (field === "event") { eventName = value } else if (field === "id" && !value.includes("\u0000")) { - eventId = value lastEventId = value - } else if (field === "retry") { + } else if (field === "retry" && /^\d+$/.test(value)) { const retry = parseInt(value, 10) - if (!Number.isNaN(retry)) { - onParse(new Retry({ duration: Duration.millis(retry), lastEventId })) - } + 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) -} +const BOM = "\uFEFF" /** * Stateful Server-Sent Events parser returned by `makeParser`. * * **Details** * - * `feed` accepts additional text chunks and `reset` clears buffered parser state. + * `feed` accepts additional text chunks and returns an `SseError` when the + * configured pending event size is exceeded. `reset` clears buffered parser + * state. * * @category decoding * @since 4.0.0 */ export interface Parser { - feed(chunk: string): void + feed(chunk: string): SseError | undefined reset(): void } @@ -580,9 +654,7 @@ export const encoder: Encoder = { if (event.event !== "message") { data += `event: ${event.event}\n` } - if (event.data !== "") { - data += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n` - } + data += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n` return data + "\n" } case "Retry": { diff --git a/.context/effect/packages/effect/src/unstable/encoding/Toml.ts b/.context/effect/packages/effect/src/unstable/encoding/Toml.ts new file mode 100644 index 000000000..a6d09dc8f --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/encoding/Toml.ts @@ -0,0 +1,504 @@ +/** + * Parses TOML configuration files. + * + * The implementation covers the TOML values and table forms used by Effect's + * configuration-file CLI primitive while avoiding a generated parser runtime. + * + * @since 4.0.0 + */ + +/* + * The behavior is based on `toml` 4.1.2. + * + * Copyright (c) 2012 Michelle Tilley + * + * 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. + */ + +type Table = Record + +const hasOwn = Object.prototype.hasOwnProperty +const makeTable = (): Table => Object.create(null) +const isTable = (value: unknown): value is Table => + typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) + +class TomlParser { + readonly root = makeTable() + private readonly input: string + private current = this.root + private index = 0 + private line = 1 + private column = 1 + private readonly explicitTables = new Set() + + constructor(input: string) { + this.input = input + } + + parse(): Table { + while (true) { + this.skipDocumentWhitespace() + if (this.done) { + return this.root + } + if (this.peek() === "[") { + this.parseHeader() + } else { + const keys = this.parseKeyPath("=") + this.skipInlineWhitespace() + this.expect("=") + this.skipInlineWhitespace() + this.assign(this.current, keys, this.parseValue()) + this.finishStatement() + } + } + } + + private parseHeader(): void { + this.expect("[") + const array = this.peek() === "[" + if (array) { + this.advance() + } + this.skipInlineWhitespace() + const path = this.parseKeyPath("]") + this.skipInlineWhitespace() + this.expect("]") + if (array) { + this.expect("]") + } + this.finishStatement() + + const pathKey = JSON.stringify(path) + if (!array && this.explicitTables.has(pathKey)) { + this.fail(`Cannot redefine table '${path.join(".")}'`) + } + if (!array) { + this.explicitTables.add(pathKey) + } + this.current = this.resolveTable(path, array) + } + + private resolveTable(path: ReadonlyArray, array: boolean): Table { + let table = this.root + for (let index = 0; index < path.length; index++) { + const key = path[index] + const last = index === path.length - 1 + let value = table[key] + + if (last && array) { + if (value === undefined) { + value = [] + table[key] = value + } + if (!Array.isArray(value)) { + this.fail(`Cannot redefine existing key '${path.slice(0, index + 1).join(".")}'`) + } + const next = makeTable() + value.push(next) + return next + } + + if (value === undefined) { + value = makeTable() + table[key] = value + } + if (Array.isArray(value)) { + value = value[value.length - 1] + } + if (!isTable(value)) { + this.fail(`Cannot redefine existing key '${path.slice(0, index + 1).join(".")}'`) + } + table = value + } + return table + } + + private assign(target: Table, keys: ReadonlyArray, value: unknown): void { + let table = target + for (let index = 0; index < keys.length - 1; index++) { + const key = keys[index] + const existing = table[key] + if (existing === undefined) { + const child = makeTable() + table[key] = child + table = child + } else if (isTable(existing)) { + table = existing + } else { + this.fail(`Cannot redefine existing key '${keys.slice(0, index + 1).join(".")}'`) + } + } + const key = keys[keys.length - 1] + if (hasOwn.call(table, key)) { + this.fail(`Cannot redefine existing key '${keys.join(".")}'`) + } + table[key] = value + } + + private parseKeyPath(stop: "=" | "]"): Array { + const keys: Array = [] + while (true) { + this.skipInlineWhitespace() + const character = this.peek() + let key: string + if (character === "\"") { + key = this.parseBasicString(false) + } else if (character === "'") { + key = this.parseLiteralString(false) + } else { + const start = this.index + while (/[A-Za-z0-9_-]/.test(this.peek())) { + this.advance() + } + key = this.input.slice(start, this.index) + } + if (key.length === 0) { + this.fail("Expected a key") + } + keys.push(key) + this.skipInlineWhitespace() + if (this.peek() === ".") { + this.advance() + continue + } + if (this.peek() !== stop) { + this.fail(`Expected '${stop}'`) + } + return keys + } + } + + private parseValue(): unknown { + const character = this.peek() + if (character === "\"") { + return this.parseBasicString(this.input.startsWith("\"\"\"", this.index)) + } + if (character === "'") { + return this.parseLiteralString(this.input.startsWith("'''", this.index)) + } + if (character === "[") { + return this.parseArray() + } + if (character === "{") { + return this.parseInlineTable() + } + + const spaceSeparatedDateTime = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?)/ + .exec(this.input.slice(this.index)) + if (spaceSeparatedDateTime !== null) { + this.advance(spaceSeparatedDateTime[0].length) + return this.parseDate(`${spaceSeparatedDateTime[1]}T${spaceSeparatedDateTime[2]}`)! + } + + const start = this.index + while (!this.done && !/[\s,#\]}]/.test(this.peek())) { + this.advance() + } + const token = this.input.slice(start, this.index) + if (token === "true") return true + if (token === "false") return false + if (token.length === 0) this.fail("Expected a value") + + const date = this.parseDate(token) + if (date !== undefined) { + return date + } + + const number = this.parseNumber(token) + if (number !== undefined) { + return number + } + this.fail(`Invalid value '${token}'`) + } + + private parseDate(token: string): Date | string | undefined { + const date = "\\d{4}-\\d{2}-\\d{2}" + const time = "\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?" + if (new RegExp(`^${date}[Tt]${time}(?:[Zz]|[+-]\\d{2}:\\d{2})$`).test(token)) { + const value = new Date(token.replace("t", "T").replace("z", "Z")) + if (Number.isNaN(value.getTime())) { + this.fail(`Invalid date-time '${token}'`) + } + return value + } + if ( + new RegExp(`^${date}[Tt]${time}$`).test(token) || + new RegExp(`^${date}$`).test(token) || + new RegExp(`^${time}$`).test(token) + ) { + return token.replace("t", "T") + } + return undefined + } + + private parseNumber(token: string): number | undefined { + const normalized = token.replace(/_/g, "") + if (/^[+-]?(?:inf|nan)$/.test(token)) { + if (normalized.endsWith("nan")) return Number.NaN + return normalized[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY + } + if (/^0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/.test(token)) { + return Number.parseInt(normalized.slice(2), 16) + } + if (/^0o[0-7](?:_?[0-7])*$/.test(token)) { + return Number.parseInt(normalized.slice(2), 8) + } + if (/^0b[01](?:_?[01])*$/.test(token)) { + return Number.parseInt(normalized.slice(2), 2) + } + if (/^[+-]?(?:0|[1-9](?:_?\d)*)$/.test(token)) { + return Number(normalized) + } + if ( + /^[+-]?(?:(?:0|[1-9](?:_?\d)*)\.\d(?:_?\d)*(?:[eE][+-]?\d(?:_?\d)*)?|(?:0|[1-9](?:_?\d)*)[eE][+-]?\d(?:_?\d)*)$/ + .test( + token + ) + ) { + return Number(normalized) + } + return undefined + } + + private parseBasicString(multiline: boolean): string { + this.expect("\"") + if (multiline) { + this.expect("\"") + this.expect("\"") + if (this.peek() === "\n") this.advance() + } + let output = "" + while (!this.done) { + if (multiline && this.input.startsWith("\"\"\"", this.index)) { + this.advance(3) + return output + } + const character = this.peek() + if (!multiline && character === "\"") { + this.advance() + return output + } + if (!multiline && (character === "\n" || character === "\r")) { + this.fail("Basic strings cannot contain newlines") + } + if (character !== "\\") { + output += character + this.advance() + continue + } + + this.advance() + if (multiline && /[ \t\r\n]/.test(this.peek())) { + while (/[ \t]/.test(this.peek())) this.advance() + if (this.peek() !== "\n" && this.peek() !== "\r") { + this.fail("Invalid multiline string continuation") + } + while (/[ \t\r\n]/.test(this.peek())) this.advance() + continue + } + const escape = this.peek() + this.advance() + const escapes: Record = { + b: "\b", + t: "\t", + n: "\n", + f: "\f", + r: "\r", + "\"": "\"", + "\\": "\\" + } + if (hasOwn.call(escapes, escape)) { + output += escapes[escape] + } else if (escape === "u" || escape === "U") { + const length = escape === "u" ? 4 : 8 + const hex = this.input.slice(this.index, this.index + length) + if (!new RegExp(`^[0-9A-Fa-f]{${length}}$`).test(hex)) { + this.fail("Invalid unicode escape") + } + output += String.fromCodePoint(Number.parseInt(hex, 16)) + this.advance(length) + } else { + this.fail(`Invalid escape '\\${escape}'`) + } + } + this.fail("Unterminated basic string") + } + + private parseLiteralString(multiline: boolean): string { + this.expect("'") + if (multiline) { + this.expect("'") + this.expect("'") + if (this.peek() === "\n") this.advance() + } + const start = this.index + while (!this.done) { + if (multiline && this.input.startsWith("'''", this.index)) { + const output = this.input.slice(start, this.index) + this.advance(3) + return output + } + if (!multiline && this.peek() === "'") { + const output = this.input.slice(start, this.index) + this.advance() + return output + } + if (!multiline && (this.peek() === "\n" || this.peek() === "\r")) { + this.fail("Literal strings cannot contain newlines") + } + this.advance() + } + this.fail("Unterminated literal string") + } + + private parseArray(): Array { + this.expect("[") + const output: Array = [] + while (true) { + this.skipArrayWhitespace() + if (this.peek() === "]") { + this.advance() + return output + } + output.push(this.parseValue()) + this.skipArrayWhitespace() + if (this.peek() === "]") { + this.advance() + return output + } + this.expect(",") + } + } + + private parseInlineTable(): Table { + this.expect("{") + const output = makeTable() + this.skipInlineWhitespace() + if (this.peek() === "}") { + this.advance() + return output + } + while (true) { + const keys = this.parseKeyPath("=") + this.skipInlineWhitespace() + this.expect("=") + this.skipInlineWhitespace() + this.assign(output, keys, this.parseValue()) + this.skipInlineWhitespace() + if (this.peek() === "}") { + this.advance() + return output + } + this.expect(",") + this.skipInlineWhitespace() + if (this.peek() === "}") { + this.fail("Inline tables cannot end with a trailing comma") + } + } + } + + private skipDocumentWhitespace(): void { + while (!this.done) { + if (/[ \t\r\n]/.test(this.peek())) { + this.advance() + } else if (this.peek() === "#") { + this.skipComment() + } else { + return + } + } + } + + private skipInlineWhitespace(): void { + while (this.peek() === " " || this.peek() === "\t") { + this.advance() + } + } + + private skipArrayWhitespace(): void { + while (!this.done) { + if (/[ \t\r\n]/.test(this.peek())) { + this.advance() + } else if (this.peek() === "#") { + this.skipComment() + } else { + return + } + } + } + + private finishStatement(): void { + this.skipInlineWhitespace() + if (this.peek() === "#") { + this.skipComment() + } + if (!this.done && this.peek() !== "\n" && this.peek() !== "\r") { + this.fail("Expected the end of the line") + } + } + + private skipComment(): void { + while (!this.done && this.peek() !== "\n") { + this.advance() + } + } + + private expect(character: string): void { + if (this.peek() !== character) { + this.fail(`Expected '${character}'`) + } + this.advance() + } + + private advance(count = 1): void { + for (let offset = 0; offset < count; offset++) { + if (this.input[this.index] === "\n") { + this.line++ + this.column = 1 + } else { + this.column++ + } + this.index++ + } + } + + private peek(): string { + return this.input[this.index] ?? "" + } + + private get done(): boolean { + return this.index >= this.input.length + } + + private fail(message: string): never { + throw new SyntaxError(`${message} at line ${this.line}, column ${this.column}`) + } +} + +/** + * Parses a TOML document into a null-prototype record. + * + * Offset date-times are represented by `Date`, matching the package this + * parser replaces. Local dates and times remain strings. + * + * @category decoding + * @since 4.0.0 + */ +export const parse = (input: string): Record => new TomlParser(input.replace(/^\uFEFF/, "")).parse() diff --git a/.context/effect/packages/effect/src/unstable/encoding/Yaml.ts b/.context/effect/packages/effect/src/unstable/encoding/Yaml.ts new file mode 100644 index 000000000..750db7b2f --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/encoding/Yaml.ts @@ -0,0 +1,541 @@ +/** + * Parses YAML configuration files. + * + * This is a focused YAML 1.2 configuration parser. It supports block and flow + * collections, quoted and block scalars, anchors, and aliases. + * + * @since 4.0.0 + */ + +/* + * The behavior is based on `yaml` 2.9.0. + * + * Copyright Eemeli Aro + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +type YamlRecord = Record + +type Line = { + readonly raw: string + readonly text: string + readonly indent: number + readonly number: number +} + +const hasOwn = Object.prototype.hasOwnProperty + +const setProperty = (record: YamlRecord, key: string, value: unknown): void => { + Object.defineProperty(record, key, { + configurable: true, + enumerable: true, + writable: true, + value + }) +} + +const stripComment = (input: string): string => { + let quote: "'" | "\"" | undefined + let escaped = false + for (let index = 0; index < input.length; index++) { + const character = input[index] + if (quote === "\"") { + if (escaped) { + escaped = false + } else if (character === "\\") { + escaped = true + } else if (character === quote) { + quote = undefined + } + } else if (quote === "'") { + if (character === quote && input[index + 1] === quote) { + index++ + } else if (character === quote) { + quote = undefined + } + } else if (character === "'" || character === "\"") { + quote = character + } else if (character === "#" && (index === 0 || /\s/.test(input[index - 1]))) { + return input.slice(0, index).trimEnd() + } + } + return input.trimEnd() +} + +const mappingSeparator = (input: string): number => { + let quote: "'" | "\"" | undefined + let escaped = false + let depth = 0 + for (let index = 0; index < input.length; index++) { + const character = input[index] + if (quote === "\"") { + if (escaped) escaped = false + else if (character === "\\") escaped = true + else if (character === quote) quote = undefined + } else if (quote === "'") { + if (character === quote && input[index + 1] === quote) index++ + else if (character === quote) quote = undefined + } else if (character === "'" || character === "\"") { + quote = character + } else if (character === "[" || character === "{") { + depth++ + } else if (character === "]" || character === "}") { + depth-- + } else if (character === ":" && depth === 0 && (input[index + 1] === undefined || /\s/.test(input[index + 1]))) { + return index + } + } + return -1 +} + +const parseDoubleQuoted = (input: string): string => { + if (!input.endsWith("\"") || input.length < 2) { + throw new SyntaxError("Unterminated double-quoted YAML string") + } + let output = "" + for (let index = 1; index < input.length - 1; index++) { + const character = input[index] + if (character !== "\\") { + output += character + continue + } + const escape = input[++index] + const escapes: Record = { + "0": "\0", + a: "\x07", + b: "\b", + t: "\t", + n: "\n", + v: "\v", + f: "\f", + r: "\r", + e: "\x1b", + " ": " ", + "\"": "\"", + "/": "/", + "\\": "\\", + N: "\u0085", + _: "\u00a0", + L: "\u2028", + P: "\u2029" + } + if (hasOwn.call(escapes, escape)) { + output += escapes[escape] + continue + } + if (escape === "x" || escape === "u" || escape === "U") { + const length = escape === "x" ? 2 : escape === "u" ? 4 : 8 + const hex = input.slice(index + 1, index + 1 + length) + if (!new RegExp(`^[0-9A-Fa-f]{${length}}$`).test(hex)) { + throw new SyntaxError("Invalid unicode escape in YAML string") + } + output += String.fromCodePoint(Number.parseInt(hex, 16)) + index += length + continue + } + throw new SyntaxError(`Invalid YAML escape '\\${escape}'`) + } + return output +} + +const parseScalar = (input: string): unknown => { + const value = input.trim() + if (value.length === 0) return null + if (value.startsWith("\"") && value.endsWith("\"")) { + return parseDoubleQuoted(value) + } + if (value.startsWith("'") && value.endsWith("'")) { + return value.slice(1, -1).replace(/''/g, "'") + } + if (/^(?:null|~)$/i.test(value)) return null + if (/^true$/i.test(value)) return true + if (/^false$/i.test(value)) return false + if (/^[+-]?\.inf$/i.test(value)) return value[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY + if (/^\.nan$/i.test(value)) return Number.NaN + + const normalized = value.replace(/_/g, "") + if (/^[+-]?0x[0-9a-f]+$/i.test(normalized)) { + const sign = normalized[0] === "-" ? -1 : 1 + return sign * Number.parseInt(normalized.replace(/^[+-]?0x/i, ""), 16) + } + if (/^[+-]?0o[0-7]+$/i.test(normalized)) { + const sign = normalized[0] === "-" ? -1 : 1 + return sign * Number.parseInt(normalized.replace(/^[+-]?0o/i, ""), 8) + } + if ( + /^[+-]?(?:0|[1-9]\d*)$/.test(normalized) || + /^[+-]?(?:(?:0|[1-9]\d*)?\.\d+|(?:0|[1-9]\d*)\.?\d*[eE][+-]?\d+)$/.test(normalized) + ) { + return Number(normalized) + } + return value +} + +const parseKey = (input: string): string => { + const value = parseScalar(input) + return value === null || value === undefined ? "" : String(value) +} + +class FlowParser { + private readonly input: string + private readonly anchors: ReadonlyMap + private index = 0 + + constructor(input: string, anchors: ReadonlyMap) { + this.input = input + this.anchors = anchors + } + + parse(): unknown { + const value = this.parseValue() + this.skipWhitespace() + if (this.index !== this.input.length) { + this.fail("Unexpected flow collection content") + } + return value + } + + private parseValue(): unknown { + this.skipWhitespace() + const character = this.peek() + if (character === "[") return this.parseSequence() + if (character === "{") return this.parseMapping() + if (character === "\"" || character === "'") return parseScalar(this.readQuoted(character)) + if (character === "*") { + this.index++ + const name = this.readUntil(/[,\]}\s]/) + if (!this.anchors.has(name)) this.fail(`Unknown alias '*${name}'`) + return this.anchors.get(name) + } + return parseScalar(this.readUntil(/[,\]}]/).trim()) + } + + private parseSequence(): Array { + this.expect("[") + const output: Array = [] + this.skipWhitespace() + if (this.peek() === "]") { + this.index++ + return output + } + while (true) { + output.push(this.parseValue()) + this.skipWhitespace() + if (this.peek() === "]") { + this.index++ + return output + } + this.expect(",") + this.skipWhitespace() + if (this.peek() === "]") { + this.index++ + return output + } + } + } + + private parseMapping(): YamlRecord { + this.expect("{") + const output: YamlRecord = {} + this.skipWhitespace() + if (this.peek() === "}") { + this.index++ + return output + } + while (true) { + const key = this.parseKey() + this.skipWhitespace() + this.expect(":") + if (hasOwn.call(output, key)) this.fail(`Duplicate key '${key}'`) + setProperty(output, key, this.parseValue()) + this.skipWhitespace() + if (this.peek() === "}") { + this.index++ + return output + } + this.expect(",") + this.skipWhitespace() + if (this.peek() === "}") { + this.index++ + return output + } + } + } + + private parseKey(): string { + this.skipWhitespace() + const character = this.peek() + if (character === "\"" || character === "'") { + return parseKey(this.readQuoted(character)) + } + return this.readUntil(/:/).trim() + } + + private readQuoted(quote: string): string { + const start = this.index++ + let escaped = false + while (this.index < this.input.length) { + const character = this.input[this.index++] + if (quote === "\"" && escaped) escaped = false + else if (quote === "\"" && character === "\\") escaped = true + else if (quote === "'" && character === quote && this.input[this.index] === quote) this.index++ + else if (character === quote) return this.input.slice(start, this.index) + } + this.fail("Unterminated quoted scalar") + } + + private readUntil(stop: RegExp): string { + const start = this.index + while (this.index < this.input.length && !stop.test(this.peek())) this.index++ + return this.input.slice(start, this.index) + } + + private skipWhitespace(): void { + while (/\s/.test(this.peek())) this.index++ + } + + private expect(character: string): void { + if (this.peek() !== character) this.fail(`Expected '${character}'`) + this.index++ + } + + private peek(): string { + return this.input[this.index] ?? "" + } + + private fail(message: string): never { + throw new SyntaxError(`${message} at flow offset ${this.index}`) + } +} + +class YamlParser { + private readonly lines: ReadonlyArray + private index = 0 + private readonly anchors = new Map() + + constructor(lines: ReadonlyArray) { + this.lines = lines + } + + parse(): unknown { + this.skipIgnored() + if (this.index >= this.lines.length) return null + const value = this.parseNode(this.lines[this.index].indent) + this.skipIgnored() + if (this.index < this.lines.length) { + this.fail(this.lines[this.index], "Unexpected content") + } + return value + } + + private parseNode(indent: number): unknown { + this.skipIgnored() + const line = this.lines[this.index] + if (line === undefined) return null + if (line.indent !== indent) this.fail(line, `Expected indentation of ${indent} spaces`) + if (line.text === "-" || line.text.startsWith("- ")) return this.parseSequence(indent) + if (mappingSeparator(line.text) !== -1) return this.parseMapping(indent) + this.index++ + return this.parseInlineValue(line.text) + } + + private parseMapping(indent: number): YamlRecord { + const output: YamlRecord = {} + while (true) { + this.skipIgnored() + const line = this.lines[this.index] + if (line === undefined || line.indent < indent) return output + if (line.indent > indent) this.fail(line, `Unexpected indentation of ${line.indent} spaces`) + const separator = mappingSeparator(line.text) + if (separator === -1) return output + this.index++ + this.parseMappingEntry(output, line.text, separator, indent, line) + } + } + + private parseMappingEntry( + output: YamlRecord, + text: string, + separator: number, + indent: number, + line: Line + ): void { + const key = parseKey(text.slice(0, separator).trim()) + const rawValue = text.slice(separator + 1).trim() + const value = this.parseNodeValue(rawValue, indent) + if (hasOwn.call(output, key)) this.fail(line, `Duplicate key '${key}'`) + setProperty(output, key, value) + } + + private parseSequence(indent: number): Array { + const output: Array = [] + while (true) { + this.skipIgnored() + const line = this.lines[this.index] + if (line === undefined || line.indent < indent) return output + if (line.indent > indent) this.fail(line, `Unexpected indentation of ${line.indent} spaces`) + if (line.text !== "-" && !line.text.startsWith("- ")) return output + this.index++ + const item = line.text.slice(1).trimStart() + const separator = mappingSeparator(item) + if (separator === -1) { + output.push(this.parseNodeValue(item, indent)) + continue + } + + const mapping: YamlRecord = {} + this.parseMappingEntry(mapping, item, separator, indent + 2, line) + while (true) { + this.skipIgnored() + const continuation = this.lines[this.index] + if (continuation === undefined || continuation.indent <= indent) break + if (continuation.indent !== indent + 2) { + this.fail(continuation, `Expected indentation of ${indent + 2} spaces`) + } + const nextSeparator = mappingSeparator(continuation.text) + if (nextSeparator === -1) this.fail(continuation, "Expected a mapping entry") + this.index++ + this.parseMappingEntry(mapping, continuation.text, nextSeparator, indent + 2, continuation) + } + output.push(mapping) + } + } + + private parseNodeValue(rawValue: string, parentIndent: number): unknown { + let value = rawValue + let anchor: string | undefined + const anchorMatch = /^&([^\s,[\]{}]+)(?:\s+(.*))?$/.exec(value) + if (anchorMatch !== null) { + anchor = anchorMatch[1] + value = anchorMatch[2] ?? "" + } + + let parsed: unknown + if (value.length === 0) { + this.skipIgnored() + const next = this.lines[this.index] + parsed = next !== undefined && next.indent > parentIndent ? this.parseNode(next.indent) : null + } else if (/^[|>](?:[1-9]?[+-]?|[+-]?[1-9]?)$/.test(value)) { + parsed = this.parseBlockScalar(value, parentIndent) + } else { + parsed = this.parseInlineValue(value) + } + if (anchor !== undefined) this.anchors.set(anchor, parsed) + return parsed + } + + private parseInlineValue(value: string): unknown { + if (value.startsWith("*")) { + const name = value.slice(1).trim() + if (!this.anchors.has(name)) throw new SyntaxError(`Unknown YAML alias '*${name}'`) + return this.anchors.get(name) + } + if (value.startsWith("[") || value.startsWith("{")) { + return new FlowParser(value, this.anchors).parse() + } + return parseScalar(value) + } + + private parseBlockScalar(indicator: string, parentIndent: number): string { + const style = indicator[0] + const chomp = indicator.includes("-") ? "strip" : indicator.includes("+") ? "keep" : "clip" + const explicitIndent = Number.parseInt(indicator.replace(/[^1-9]/g, ""), 10) + const start = this.index + let end = start + let contentIndent = Number.isNaN(explicitIndent) ? Number.POSITIVE_INFINITY : parentIndent + explicitIndent + + while (end < this.lines.length) { + const line = this.lines[end] + if (line.raw.trim().length !== 0 && line.indent <= parentIndent) break + if (line.raw.trim().length !== 0) contentIndent = Math.min(contentIndent, line.indent) + end++ + } + if (contentIndent === Number.POSITIVE_INFINITY) contentIndent = parentIndent + 1 + + const content: Array = [] + for (let index = start; index < end; index++) { + const line = this.lines[index] + if (line.raw.trim().length === 0) { + content.push("") + } else if (line.indent < contentIndent) { + this.fail(line, `Expected block scalar indentation of ${contentIndent} spaces`) + } else { + content.push(line.raw.slice(contentIndent)) + } + } + this.index = end + + let output = "" + if (style === "|") { + output = content.join("\n") + } else { + for (let index = 0; index < content.length; index++) { + output += content[index] + if (index < content.length - 1) { + output += content[index].length === 0 || content[index + 1].length === 0 ? "\n" : " " + } + } + } + if (chomp === "keep") return `${output}\n` + output = output.replace(/\n+$/, "") + return chomp === "strip" ? output : `${output}\n` + } + + private skipIgnored(): void { + while (this.index < this.lines.length) { + const line = this.lines[this.index] + if (line.text.length === 0 || line.text.startsWith("%") || line.text === "---") { + this.index++ + } else if (line.text === "...") { + this.index++ + while (this.index < this.lines.length && this.lines[this.index].text.length === 0) this.index++ + if (this.index < this.lines.length) { + this.fail(this.lines[this.index], "Multiple YAML documents are not supported") + } + } else { + return + } + } + } + + private fail(line: Line, message: string): never { + throw new SyntaxError(`${message} at line ${line.number}`) + } +} + +/** + * Parses one YAML document into JavaScript values. + * + * The core YAML 1.2 scalar schema is used, so booleans, nulls, and numbers are + * decoded while date-like values remain strings. + * + * @category decoding + * @since 4.0.0 + */ +export const parse = (input: string): unknown => { + const source = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n") + const lines = source.split("\n").map((raw, index): Line => { + const indentation = /^( *)/.exec(raw)![1].length + if (raw.slice(0, indentation + 1).includes("\t")) { + throw new SyntaxError(`Tabs cannot be used for YAML indentation at line ${index + 1}`) + } + return { + raw, + text: stripComment(raw.slice(indentation)), + indent: indentation, + number: index + 1 + } + }) + return new YamlParser(lines).parse() +} diff --git a/.context/effect/packages/effect/src/unstable/encoding/index.ts b/.context/effect/packages/effect/src/unstable/encoding/index.ts index d6c7fe41b..da43977da 100644 --- a/.context/effect/packages/effect/src/unstable/encoding/index.ts +++ b/.context/effect/packages/effect/src/unstable/encoding/index.ts @@ -4,6 +4,11 @@ // @barrel: Auto-generated exports. Do not edit manually. +/** + * @since 4.0.0 + */ +export * as Ini from "./Ini.ts" + /** * @since 4.0.0 */ @@ -18,3 +23,13 @@ export * as Ndjson from "./Ndjson.ts" * @since 4.0.0 */ export * as Sse from "./Sse.ts" + +/** + * @since 4.0.0 + */ +export * as Toml from "./Toml.ts" + +/** + * @since 4.0.0 + */ +export * as Yaml from "./Yaml.ts" diff --git a/.context/effect/packages/effect/src/unstable/eventlog/Event.ts b/.context/effect/packages/effect/src/unstable/eventlog/Event.ts index 4129f4c1a..00dd6f329 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/Event.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/Event.ts @@ -114,7 +114,7 @@ export interface AnyWithProps extends Any {} /** * Derives the handler service marker for an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type ToService = A extends Event< @@ -128,7 +128,7 @@ export type ToService = A extends Event< /** * Extracts the tag string from an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type Tag = A extends Event< @@ -142,7 +142,7 @@ export type Tag = A extends Event< /** * Extracts the error schema from an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorSchema = A extends Event< @@ -156,7 +156,7 @@ export type ErrorSchema = A extends Event< /** * Decoded error value type for an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = Schema.Schema.Type> @@ -165,7 +165,7 @@ export type Error = Schema.Schema.Type> * Returns an event definition type whose error schema also includes the provided * error schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddError = A extends Event< @@ -179,7 +179,7 @@ export type AddError = A extends Event< /** * Extracts the payload schema from an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type PayloadSchema = A extends Event< @@ -193,7 +193,7 @@ export type PayloadSchema = A extends Event< /** * Extracts the payload schema for the event in a union with the specified tag. * - * @category models + * @category utility types * @since 4.0.0 */ export type PayloadSchemaWithTag = A extends Event< @@ -207,7 +207,7 @@ export type PayloadSchemaWithTag = A extends /** * Decoded payload value type for an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type Payload = Schema.Schema.Type> @@ -220,7 +220,7 @@ export type Payload = Schema.Schema.Type> * The result contains `_tag` set to the event tag and `payload` set to the * decoded payload value. * - * @category models + * @category utility types * @since 4.0.0 */ export type TaggedPayload = A extends Event< @@ -237,7 +237,7 @@ export type TaggedPayload = A extends Event< /** * Extracts the success schema from an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type SuccessSchema = A extends Event< @@ -251,7 +251,7 @@ export type SuccessSchema = A extends Event< /** * Decoded success value type for an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type Success = Schema.Schema.Type> @@ -264,7 +264,7 @@ export type Success = Schema.Schema.Type> * This includes payload encoding services plus success and error decoding * services. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesClient = A extends Event< @@ -286,7 +286,7 @@ export type ServicesClient = A extends Event< * This includes payload decoding services plus success and error encoding * services. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesServer = A extends Event< @@ -304,7 +304,7 @@ export type ServicesServer = A extends Event< * All schema services required to encode and decode the payload, success, and * error schemas for an event definition. * - * @category models + * @category utility types * @since 4.0.0 */ export type Services = A extends Event< @@ -324,7 +324,7 @@ export type Services = A extends Event< /** * Extracts the event definition with the specified tag from an event union. * - * @category models + * @category utility types * @since 4.0.0 */ export type WithTag = Extract @@ -332,7 +332,7 @@ export type WithTag = Extract = Exclude @@ -340,7 +340,7 @@ export type ExcludeTag = Exclude = Payload> @@ -348,7 +348,7 @@ export type PayloadWithTag = Payload = Success> @@ -356,7 +356,7 @@ export type SuccessWithTag = Success = Error> @@ -365,7 +365,7 @@ export type ErrorWithTag = Error = ServicesClient> diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventGroup.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventGroup.ts index d814c006c..d5c475a36 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventGroup.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventGroup.ts @@ -100,7 +100,7 @@ export type AnyWithProps = EventGroup /** * Derives the handler service markers required for all events in an event group. * - * @category models + * @category utility types * @since 4.0.0 */ export type ToService = A extends EventGroup ? Event.ToService<_Events> @@ -109,7 +109,7 @@ export type ToService = A extends EventGroup ? Event.ToService /** * Extracts the union of event definitions contained in an event group. * - * @category models + * @category utility types * @since 4.0.0 */ export type Events = Group extends EventGroup ? _Events @@ -118,7 +118,7 @@ export type Events = Group extends EventGroup ? _Events /** * Client-side schema services required by all events in an event group. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesClient = Event.ServicesClient> @@ -126,7 +126,7 @@ export type ServicesClient = Event.ServicesClient> /** * Server-side schema services required by all events in an event group. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesServer = Event.ServicesServer> diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventJournal.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventJournal.ts index f89b50df5..c80bf95bc 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventJournal.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventJournal.ts @@ -34,7 +34,7 @@ import type { StoreId } from "./EventLogMessage.ts" * The service writes local entries, imports entries from remote journals, exposes * a stream of local changes, and provides per-store locking. * - * @category context + * @category services * @since 4.0.0 */ export class EventJournal extends Context.Service Effect.Effect /** - * Retrieve the last known sequence number for a remote source. + * Retrieve the first unused sequence number for a remote source. */ readonly nextRemoteSequence: (remoteId: RemoteId) => Effect.Effect @@ -149,7 +149,7 @@ export const RemoteIdTypeId: RemoteIdTypeId = "effect/eventlog/EventJournal/Remo /** * Branded byte identifier for a remote event journal source. * - * @category remote + * @category models * @since 4.0.0 */ export type RemoteId = Uint8Array & Brand @@ -157,7 +157,7 @@ export type RemoteId = Uint8Array & Brand /** * Schema for branded remote event journal identifiers. * - * @category remote + * @category schemas * @since 4.0.0 */ export const RemoteId = Schema.Uint8Array.pipe(Schema.brand(RemoteIdTypeId)) @@ -175,7 +175,7 @@ export const RemoteId = Schema.Uint8Array.pipe(Schema.brand(RemoteIdTypeId)) * This is unsafe because the generated UUID bytes are cast to the brand without * schema validation. * - * @category remote + * @category unsafe * @since 4.0.0 */ export const makeRemoteIdUnsafe = (): RemoteId => Uuid.v4({}, new globalThis.Uint8Array(16)) as RemoteId @@ -199,7 +199,7 @@ export type EntryIdTypeId = "effect/eventlog/EventJournal/EntryId" /** * Branded byte identifier for an event journal entry. * - * @category entry + * @category models * @since 4.0.0 */ export type EntryId = Uint8Array & Brand @@ -207,7 +207,7 @@ export type EntryId = Uint8Array & Brand /** * Schema for branded event journal entry identifiers. * - * @category entry + * @category schemas * @since 4.0.0 */ export const EntryId = (Schema.Uint8Array as Schema.instanceOf>).pipe( @@ -217,7 +217,7 @@ export const EntryId = (Schema.Uint8Array as Schema.instanceOf((a, b) => { @@ -243,7 +243,7 @@ export const EntryIdOrder = Order.make((a, b) => { * This is unsafe because the generated UUID bytes are cast to the brand without * schema validation. * - * @category entry + * @category unsafe * @since 4.0.0 */ export const makeEntryIdUnsafe = (options: { msecs?: number } = {}): EntryId => @@ -252,7 +252,7 @@ export const makeEntryIdUnsafe = (options: { msecs?: number } = {}): EntryId => /** * Extracts the millisecond timestamp encoded in a UUID v7 `EntryId`. * - * @category entry + * @category getters * @since 4.0.0 */ export const entryIdMillis = (entryId: EntryId): number => { @@ -269,7 +269,7 @@ export const entryIdMillis = (entryId: EntryId): number => { * An entry records its ID, event tag, primary key, and MessagePack-encoded * payload, with helpers for array MessagePack encoding and creation timestamps. * - * @category entry + * @category schemas * @since 4.0.0 */ export class Entry extends Schema.Class("effect/eventlog/EventJournal/Entry")({ @@ -341,11 +341,11 @@ export class Entry extends Schema.Class("effect/eventlog/EventJournal/Ent * * It pairs the remote sequence number with the journal entry payload. * - * @category entry + * @category schemas * @since 4.0.0 */ export class RemoteEntry extends Schema.Class("effect/eventlog/EventJournal/RemoteEntry")({ - remoteSequence: Schema.Number, + remoteSequence: Schema.Natural, entry: Entry }) {} @@ -357,7 +357,7 @@ export class RemoteEntry extends Schema.Class("effect/eventlog/Even * Entries, remote tracking state, and locks live only in the current process and * are lost when the service is discarded. * - * @category memory + * @category constructors * @since 4.0.0 */ export const makeMemory: Effect.Effect = Effect.gen(function*() { @@ -417,8 +417,8 @@ export const makeMemory: Effect.Effect = Effect.gen(fun for (const remoteEntry of options.entries) { if (byId.has(remoteEntry.entry.idString)) { duplicateEntries.push(remoteEntry.entry) - if (remoteEntry.remoteSequence > remote.sequence) { - remote.sequence = remoteEntry.remoteSequence + if (remoteEntry.remoteSequence >= remote.sequence) { + remote.sequence = remoteEntry.remoteSequence + 1 } continue } @@ -438,7 +438,7 @@ export const makeMemory: Effect.Effect = Effect.gen(fun if (entry !== undefined && entry.createdAtMillis > entryMillis) { continue } - for (let j = i + 2; j < journal.length; j++) { + for (let j = i + 1; j < journal.length; j++) { const scannedEntry = journal[j]! if (scannedEntry.event === originEntry.event && scannedEntry.primaryKey === originEntry.primaryKey) { conflicts.push(scannedEntry) @@ -451,11 +451,19 @@ export const makeMemory: Effect.Effect = Effect.gen(fun for (const remoteEntry of uncommittedRemotes) { journal.push(remoteEntry.entry) byId.set(remoteEntry.entry.idString, remoteEntry.entry) - if (remoteEntry.remoteSequence > remote.sequence) { - remote.sequence = remoteEntry.remoteSequence + remotes.forEach((target) => { + if (target !== remote) { + target.missing.push(remoteEntry.entry) + } + }) + if (remoteEntry.remoteSequence >= remote.sequence) { + remote.sequence = remoteEntry.remoteSequence + 1 } } journal.sort((a, b) => a.createdAtMillis - b.createdAtMillis) + remotes.forEach((remote) => { + remote.missing.sort((a, b) => a.createdAtMillis - b.createdAtMillis) + }) return { duplicateEntries } @@ -497,7 +505,7 @@ export const makeMemory: Effect.Effect = Effect.gen(fun * All journal data is stored in process memory and is not persisted across layer * lifetimes. * - * @category memory + * @category layers * @since 4.0.0 */ export const layerMemory: Layer.Layer = Layer.effect(EventJournal, makeMemory) @@ -511,7 +519,7 @@ export const layerMemory: Layer.Layer = Layer.effect(EventJournal, * browser database, publishes local changes, and requires `Scope` so the database * connection can be closed when the scope ends. * - * @category indexed db + * @category constructors * @since 4.0.0 */ export const makeIndexedDb = (options?: { @@ -772,7 +780,7 @@ const decodeEntryIdbArray = Schema.decodeUnknownEffect(EntryIdbArray) * Provides `EventJournal` using the IndexedDB-backed implementation created by * `makeIndexedDb`. * - * @category indexed db + * @category layers * @since 4.0.0 */ export const layerIndexedDb = (options?: { diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLog.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLog.ts index fc161ce8f..faa5a3b0c 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLog.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLog.ts @@ -13,6 +13,7 @@ import * as Context from "../../Context.ts" import * as Effect from "../../Effect.ts" import * as FiberMap from "../../FiberMap.ts" import { constant, identity } from "../../Function.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Layer from "../../Layer.ts" import type { Pipeable } from "../../Pipeable.ts" import { pipeArguments } from "../../Pipeable.ts" @@ -159,7 +160,9 @@ export const layerRegistry = Layer.effect( }, registerReactivity: (keys) => Effect.sync(() => { - Object.assign(reactivityKeys, keys) + for (const [key, value] of Object.entries(keys)) { + InternalRecord.assignProperty(reactivityKeys, key, value) + } }), reactivityKeys }) @@ -202,7 +205,7 @@ export const SchemaTypeId: SchemaTypeId = "~effect/eventlog/EventLog/Schema" /** * Returns `true` when a value carries the `EventLogSchema` marker. * - * @category schemas + * @category guards * @since 4.0.0 */ export const isEventLogSchema = (u: unknown): u is EventLogSchema => @@ -407,7 +410,7 @@ export declare namespace Handlers { * * Defaults to the branded store id `"default"`. * - * @category models + * @category services * @since 4.0.0 */ export class CurrentStoreId extends Context.Reference("effect/eventlog/EventLog/CurrentStoreId", { @@ -498,7 +501,7 @@ const handlersProto = { handlers: { ...this.handlers, [tag]: { - event: this.group.events[tag], + event: Object.hasOwn(this.group.events, tag) ? this.group.events[tag] : undefined!, context: this.context, handler } @@ -547,7 +550,7 @@ export const group = ( const handlers = Effect.isEffect(result) ? (yield* (result as unknown as Effect.Effect>)) : (result as unknown as Handlers) - for (const tag in handlers.handlers) { + for (const tag of Object.keys(handlers.handlers)) { registry.registerHandlerUnsafe({ event: tag, handler: handlers.handlers[tag] }) } }) @@ -584,7 +587,7 @@ export const groupCompaction = ( yield* registry.registerCompaction({ events: Object.keys(group.events), effect: Effect.fnUntraced(function*({ entries, write }): Effect.fn.Return { - const isEventTag = (tag: string): tag is Event.Tag => tag in group.events + const isEventTag = (tag: string): tag is Event.Tag => Object.hasOwn(group.events, tag) const decodePayload = >(tag: Tag, payload: Uint8Array) => Schema.decodeUnknownEffect(group.events[tag].payloadMsgPack)(payload).pipe( Effect.updateContext((input) => Context.merge(services, input)), @@ -595,7 +598,7 @@ export const groupCompaction = ( tag: Tag, payload: Event.PayloadWithTag ): Effect.fn.Return["EncodingServices"]> { - const event = group.events[tag] + const event = Object.hasOwn(group.events, tag) ? group.events[tag] : undefined! const entry = new Entry({ id: makeEntryIdUnsafe({ msecs: timestamp }), event: tag, @@ -674,8 +677,8 @@ export const groupReactivity = ( yield* registry.registerReactivity(keys as Record.ReadonlyRecord>) return } - const obj: Record> = {} - for (const tag in group.events) { + const obj: Record> = Object.create(null) + for (const tag of Object.keys(group.events)) { obj[tag] = keys } yield* registry.registerReactivity(obj) @@ -741,7 +744,9 @@ export const makeReplayFromRemote = (options: { Effect.asVoid ) as any - const keys = options.reactivityKeys[entry.event] + const keys = Object.hasOwn(options.reactivityKeys, entry.event) + ? options.reactivityKeys[entry.event] + : undefined if (keys) { for (const key of keys) { options.reactivity.invalidateUnsafe({ @@ -780,7 +785,9 @@ const make = Effect.gen(function*() { const invalidateReactivityEntries = (entries: ReadonlyArray) => Effect.sync(() => { for (const entry of entries) { - const keys = registry.reactivityKeys[entry.event] + const keys = Object.hasOwn(registry.reactivityKeys, entry.event) + ? registry.reactivityKeys[entry.event] + : undefined if (!keys) { continue } @@ -903,8 +910,11 @@ const make = Effect.gen(function*() { Effect.updateContext((input) => Context.merge(handler.context, input)), Effect.provideService(Identity, identity), Effect.tap(() => { - if (registry.reactivityKeys[entry.event]) { - for (const key of registry.reactivityKeys[entry.event]) { + const keys = Object.hasOwn(registry.reactivityKeys, entry.event) + ? registry.reactivityKeys[entry.event] + : undefined + if (keys) { + for (const key of keys) { reactivity.invalidateUnsafe({ [key]: [entry.primaryKey] }) @@ -1002,7 +1012,7 @@ export const layer = ( * The returned function delegates to the `EventLog` service and preserves each * event's success and error types. * - * @category client + * @category constructors * @since 4.0.0 */ export const makeClient = ( diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogEncryption.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogEncryption.ts index 052628a72..053517f5b 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogEncryption.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogEncryption.ts @@ -19,14 +19,15 @@ import type { Identity } from "./EventLog.ts" import { makeGetIdentityRootSecretMaterial } from "./internal/identityRootSecretDerivation.ts" /** - * Schema for an encrypted journal entry paired with the id of the original - * entry. + * Schema for an encrypted journal entry paired with its initialization vector + * and the id of the original entry. * * @category models * @since 4.0.0 */ export const EncryptedEntry = Schema.Struct({ entryId: EntryId, + iv: Transferable.Uint8Array, encryptedEntry: Transferable.Uint8Array }) @@ -46,7 +47,7 @@ export interface EncryptedRemoteEntry extends Schema.Schema.Type - ) => Effect.Effect<{ - readonly iv: Uint8Array - readonly encryptedEntries: ReadonlyArray> - }> + ) => Effect.Effect< + ReadonlyArray<{ + readonly iv: Uint8Array + readonly encryptedEntry: Uint8Array + }> + > readonly decrypt: ( identity: Identity["Service"], entries: ReadonlyArray @@ -104,22 +107,18 @@ export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect + return yield* Effect.promise(() => Promise.all( - data.map((entry) => - crypto.subtle.encrypt( + data.map((entry) => { + const iv = crypto.getRandomValues(new Uint8Array(12)) + return crypto.subtle.encrypt( { name: "AES-GCM", iv: toBufferSource(iv), tagLength: 128 }, key, toBufferSource(entry) - ) - ) + ).then((encryptedEntry) => ({ iv, encryptedEntry: new Uint8Array(encryptedEntry) })) + }) ) ) - return { - iv, - encryptedEntries: encryptedEntries.map((entry) => new Uint8Array(entry)) - } }), decrypt: Effect.fnUntraced(function*(identity, entries) { const key = (yield* getIdentityRootSecretMaterial(identity)).encryptionKey @@ -160,7 +159,7 @@ export const makeEncryptionSubtle = (crypto: Crypto): Effect.Effect = Layer.effect( diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogMessage.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogMessage.ts index 0f5cb629c..c9aec0b91 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogMessage.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogMessage.ts @@ -39,7 +39,7 @@ export const StoreIdTypeId: StoreIdTypeId = "effect/eventlog/EventLog/StoreId" /** * Branded string identifying a logical event-log store. * - * @category StoreId + * @category models * @since 4.0.0 */ export type StoreId = string & Brand @@ -47,7 +47,7 @@ export type StoreId = string & Brand /** * Schema for branded event-log store ids. * - * @category StoreId + * @category schemas * @since 4.0.0 */ export const StoreId = Schema.String.pipe(Schema.brand(StoreIdTypeId)) @@ -63,7 +63,7 @@ export const StoreId = Schema.String.pipe(Schema.brand(StoreIdTypeId)) * @category protocols * @since 4.0.0 */ -export class EventLogProtocolError extends Schema.TaggedErrorClass( +export class EventLogProtocolError extends Schema.TaggedError( "effect/eventlog/EventLogRemote/ProtocolError" )("EventLogProtocolError", { requestTag: Schema.String, @@ -163,8 +163,12 @@ export class SingleMessage */ export class ChunkedMessage extends Schema.TaggedClass("effect/eventlog/EventLogRemote/ChunkedMessage")("Chunked", { - id: Schema.Number, - part: Schema.Tuple([Schema.Number, Schema.Number]), + id: Schema.Int, + part: Schema.Tuple([Schema.Natural, Schema.Natural]).check( + Schema.makeFilter(([index, total]) => index < total, { + expected: "a chunk part with an index less than its total" + }) + ), data: Transferable.Uint8Array }) { @@ -221,6 +225,9 @@ export class ChunkedMessage } map.set(part.id, entry) } + if (entry.parts[index] !== undefined) { + return + } entry.parts[index] = part.data entry.count++ entry.bytes += part.data.byteLength @@ -254,8 +261,8 @@ export class WriteChunkedRpc extends Rpc.make("EventLog.WriteChunked", { * * **Details** * - * It includes the client public key, target store id, AES-GCM initialization - * vector, and encrypted entries. + * It includes the client public key, target store id, and encrypted entries + * with their AES-GCM initialization vectors. * * @category protocols * @since 4.0.0 @@ -263,7 +270,6 @@ export class WriteChunkedRpc extends Rpc.make("EventLog.WriteChunked", { export class WriteEntries extends Schema.Class("effect/eventlog/EventLogRemote/WriteEntries")({ publicKey: Schema.String, storeId: StoreId, - iv: Transferable.Uint8Array, encryptedEntries: Schema.Array(EncryptedEntry) }) { static FromMsgpack = Msgpack.schema(WriteEntries) @@ -324,7 +330,7 @@ export class ChangesRpc extends Rpc.make("EventLog.Changes", { payload: { publicKey: Schema.String, storeId: StoreId, - startSequence: Schema.Number + startSequence: Schema.Natural }, success: Schema.Union([SingleMessage, ChunkedMessage]), error: EventLogProtocolError, diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogRemote.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogRemote.ts index c45a835a8..dc76e650e 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogRemote.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogRemote.ts @@ -119,7 +119,7 @@ const makeAuthenticate = Effect.fnUntraced(function*(options: { * Use to provide the RPC client used by remote event-log replicas to * authenticate, write entries, and subscribe to changes. * - * @category RPC client + * @category services * @since 4.0.0 */ export class EventLogRemoteClient extends Context.Service< @@ -307,14 +307,14 @@ export const makeEncrypted = Effect.gen(function*(): Effect.fn.Return< return yield* makeWith({ encodeWrite: (options) => encryption.encrypt(options.identity, options.entries).pipe( - Effect.flatMap((msg) => + Effect.flatMap((encryptedEntries) => new WriteEntries({ publicKey: options.identity.publicKey, storeId: options.storeId, - iv: msg.iv, - encryptedEntries: msg.encryptedEntries.map((entry, i) => ({ + encryptedEntries: encryptedEntries.map((entry, i) => ({ entryId: options.entries[i].id, - encryptedEntry: entry + iv: entry.iv, + encryptedEntry: entry.encryptedEntry })) }).encoded ) diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServer.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServer.ts index b41a6731c..ed39ad80d 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServer.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServer.ts @@ -80,7 +80,8 @@ export const layerRpcHandlers = (options: { signingPublicKey: Uint8Array ) => Effect.Effect> readonly onWrite: ( - data: Uint8Array + data: Uint8Array, + authenticatedPublicKeys: ReadonlySet ) => Effect.Effect readonly changes: (options: { readonly publicKey: string @@ -161,24 +162,46 @@ export const layerRpcHandlers = (options: { }) } + const authenticatedIdentities = new Set( + Context.getOrUndefined(client.annotations, AuthenticatedIdentities) + ) + authenticatedIdentities.add(request.publicKey) void client .annotate(EventLog.Identity, { publicKey: request.publicKey, privateKey: constEmptyPrivateKey }) + .annotate(AuthenticatedIdentities, authenticatedIdentities) .annotate(ChunkedMessageState, new Map()) }), - "EventLog.WriteSingle": Effect.fnUntraced(function*(request) { - yield* options.onWrite(request.data) + "EventLog.WriteSingle": Effect.fnUntraced(function*(request, { client }) { + yield* options.onWrite( + request.data, + Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set() + ) }), "EventLog.WriteChunked": Effect.fnUntraced(function*(request, { client }) { const state = Context.get(client.annotations, ChunkedMessageState) const data = ChunkedMessage.join(state, request) if (!data) return - yield* options.onWrite(data) + yield* options.onWrite( + data, + Context.getOrUndefined(client.annotations, AuthenticatedIdentities) ?? new Set() + ) }), - "EventLog.Changes": (request) => - options.changes({ + "EventLog.Changes": (request, { client }) => { + const authenticatedIdentities = Context.getOrUndefined(client.annotations, AuthenticatedIdentities) + if (!authenticatedIdentities?.has(request.publicKey)) { + return Stream.fail( + new EventLogProtocolError({ + requestTag: "Changes", + publicKey: request.publicKey, + code: "Forbidden", + message: "Identity is not authenticated" + }) + ) + } + return options.changes({ publicKey: request.publicKey, storeId: request.storeId, startSequence: request.startSequence @@ -200,6 +223,7 @@ export const layerRpcHandlers = (options: { ) ) ) + } }) })).pipe( Layer.merge(layerAuthMiddleware) @@ -214,7 +238,7 @@ export const layerRpcHandlers = (options: { * Use to keep per-client chunk assembly state while handling chunked event-log * writes. * - * @category chunked message state + * @category services * @since 4.0.0 */ export class ChunkedMessageState extends Context.Reference< @@ -227,6 +251,16 @@ export class ChunkedMessageState extends Context.Reference< defaultValue: () => new Map() }) {} +/** + * Annotation containing the public keys authenticated on an RPC connection. + * + * @category services + * @since 4.0.0 + */ +export class AuthenticatedIdentities extends Context.Service>()( + "effect/eventlog/EventLogServer/AuthenticatedIdentities" +) {} + class SessionAuthCacheKey extends Data.Class<{ readonly publicKey: string readonly signingPublicKey: Uint8Array diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerEncrypted.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerEncrypted.ts index 0b23b69e2..642306b4c 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerEncrypted.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerEncrypted.ts @@ -47,7 +47,7 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { remoteId, getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => storage.getOrCreateSessionAuthBinding(publicKey, signingPublicKey), - onWrite: Effect.fnUntraced(function*(data) { + onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys) { const request = yield* WriteEntries.decode(data).pipe( Effect.mapError((_) => new EventLogProtocolError({ @@ -58,11 +58,19 @@ export const layerRpcHandlers = Layer.unwrap(Effect.gen(function*() { }) ) ) + if (!authenticatedPublicKeys.has(request.publicKey)) { + return yield* new EventLogProtocolError({ + requestTag: "WriteEntries", + publicKey: request.publicKey, + code: "Forbidden", + message: "Identity is not authenticated" + }) + } if (request.encryptedEntries.length === 0) return - const entries = request.encryptedEntries.map(({ encryptedEntry, entryId }) => + const entries = request.encryptedEntries.map(({ encryptedEntry, entryId, iv }) => new PersistedEntry({ entryId, - iv: request.iv, + iv, encryptedEntry }) ) @@ -116,7 +124,7 @@ export const layer: Layer.Layer = Rp /** * Schema for encrypted entries persisted by the encrypted event-log server. * - * @category storage + * @category models * @since 4.0.0 */ export class PersistedEntry extends Schema.Class( @@ -150,7 +158,7 @@ export class PersistedEntry extends Schema.Class( * persists encrypted entries, and streams encrypted changes for a public key and * store id. * - * @category storage + * @category services * @since 4.0.0 */ export class Storage extends Context.Service = Effect.gen(function*() { @@ -255,7 +263,7 @@ export const makeStorageMemory: Effect.Effect = Layer.effect(Storage)(makeStorageMemory) diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerUnencrypted.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerUnencrypted.ts index 0f9278a9c..6965ab154 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerUnencrypted.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogServerUnencrypted.ts @@ -74,7 +74,7 @@ export class EventLogServerUnencrypted extends Context.Service( @@ -146,7 +146,7 @@ export const layerRpcHandlers: Layer.Layer< remoteId, getOrCreateSessionAuthBinding: (publicKey, signingPublicKey) => storage.getOrCreateSessionAuthBinding(publicKey, signingPublicKey), - onWrite: Effect.fnUntraced(function*(data) { + onWrite: Effect.fnUntraced(function*(data, authenticatedPublicKeys) { const request = yield* WriteEntriesUnencrypted.decode(data).pipe( Effect.mapError((_) => new EventLogProtocolError({ @@ -157,6 +157,14 @@ export const layerRpcHandlers: Layer.Layer< }) ) ) + if (!authenticatedPublicKeys.has(request.publicKey)) { + return yield* new EventLogProtocolError({ + requestTag: "WriteEntries", + publicKey: request.publicKey, + code: "Forbidden", + message: "Identity is not authenticated" + }) + } if (!Arr.isReadonlyArrayNonEmpty(request.entries)) return const resolvedStoreId = yield* mapping.resolve({ @@ -322,7 +330,7 @@ const toStoreNotFoundError = (options: { * Provides a `StoreMapping` that accepts only one configured store id and fails * all other store ids as not found. * - * @category store + * @category layers * @since 4.0.0 */ export const layerStoreMappingStatic = (options: { @@ -352,7 +360,7 @@ export const layerStoreMappingStatic = (options: { * allocates remote sequence numbers, persists entries, streams changes, and * exposes a transaction boundary. * - * @category storage + * @category services * @since 4.0.0 */ export class Storage extends Context.Service = [] for (let j = 0; j < newHistory.length; j++) { - const scannedEntry = history[j]! + const scannedEntry = newHistory[j]! if (scannedEntry.event === originEntry.event && scannedEntry.primaryKey === originEntry.primaryKey) { conflicts.push(scannedEntry) } @@ -556,7 +564,7 @@ export const compactBacklog = Effect.fnUntraced(function*(options: { * in memory, publishes live changes, and serializes transactions with a * semaphore. * - * @category storage + * @category constructors * @since 4.0.0 */ export const makeStorageMemory: Effect.Effect = Effect.gen(function*() { @@ -663,7 +671,7 @@ export const makeStorageMemory: Effect.Effect = Layer.effect(Storage)(makeStorageMemory) diff --git a/.context/effect/packages/effect/src/unstable/eventlog/EventLogSessionAuth.ts b/.context/effect/packages/effect/src/unstable/eventlog/EventLogSessionAuth.ts index e74995544..2e0182f5e 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/EventLogSessionAuth.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/EventLogSessionAuth.ts @@ -479,7 +479,7 @@ export const verifySessionAuthPayload = ( /** * Generates a random session authentication challenge using `globalThis.crypto`. * - * @category challenge + * @category constructors * @since 4.0.0 */ export const makeSessionAuthChallenge: Effect.Effect< diff --git a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventJournal.ts b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventJournal.ts index d60357457..23beb8a37 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventJournal.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventJournal.ts @@ -220,10 +220,12 @@ export const make = (options?: { primaryKey, payload }, { disableChecks: true }) - yield* insertEntry(toEntryRow(entry)) - const value = yield* effect(entry) - yield* PubSub.publish(pubsub, entry) - return value + return yield* Effect.uninterruptibleMask((restore) => + restore(effect(entry)).pipe( + Effect.tap(insertEntry(toEntryRow(entry))), + Effect.tap(PubSub.publish(pubsub, entry)) + ) + ) }, withTracerDisabled, Effect.mapError((cause) => new EventJournal.EventJournalError({ cause, method: "write" })) @@ -319,7 +321,7 @@ const EntryRow = Schema.Struct({ event: Schema.String, primary_key: Schema.String, payload: Schema.Uint8Array, - timestamp: Schema.Number + timestamp: Schema.Int }) const EntryRowArray = Schema.Array(EntryRow) @@ -345,7 +347,7 @@ const toEntryRow = (entry: EventJournal.Entry): EntryRow => ({ const RemoteRow = Schema.Struct({ remote_id: EventJournal.RemoteId, entry_id: EventJournal.EntryId, - sequence: Schema.Number + sequence: Schema.Natural }) const RemoteRowArray = Schema.Array(RemoteRow) diff --git a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerEncrypted.ts b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerEncrypted.ts index 892391aa7..d1b8972a1 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerEncrypted.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerEncrypted.ts @@ -269,7 +269,7 @@ export const makeStorage = (options?: { }).pipe(withTracerDisabled) const EncryptedRemoteEntrySql = Schema.Struct({ - sequence: Schema.Number, + sequence: Schema.Natural, iv: Schema.Uint8Array, entry_id: EntryId, encrypted_entry: Schema.Uint8Array diff --git a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts index ba38af32b..8c7e44fbe 100644 --- a/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts +++ b/.context/effect/packages/effect/src/unstable/eventlog/SqlEventLogServerUnencrypted.ts @@ -461,17 +461,20 @@ const EntrySql = Schema.Struct({ type EntrySql = Schema.Schema.Type -const SqlNumber = Schema.Union([Schema.Number, Schema.NumberFromString]) +const SqlNatural = Schema.Union([ + Schema.Natural, + Schema.FiniteFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)) +]) const RemoteEntrySql = Schema.Struct({ ...EntrySql.fields, - sequence: SqlNumber + sequence: SqlNatural }) type RemoteEntrySql = Schema.Schema.Type const StoreSequenceSql = Schema.Struct({ - next_sequence: SqlNumber + next_sequence: SqlNatural }) const SessionAuthBindingSql = Schema.Struct({ diff --git a/.context/effect/packages/effect/src/unstable/http/Cookies.ts b/.context/effect/packages/effect/src/unstable/http/Cookies.ts index 2da2fe004..580570471 100644 --- a/.context/effect/packages/effect/src/unstable/http/Cookies.ts +++ b/.context/effect/packages/effect/src/unstable/http/Cookies.ts @@ -13,6 +13,7 @@ import * as Data from "../../Data.ts" import * as Duration from "../../Duration.ts" import { dual } from "../../Function.ts" import * as Inspectable from "../../Inspectable.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Option from "../../Option.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" @@ -27,7 +28,7 @@ const TypeId = "~effect/http/Cookies" /** * Returns `true` when a value is a `Cookies` collection. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isCookies = (u: unknown): u is Cookies => Predicate.hasProperty(u, TypeId) @@ -65,15 +66,15 @@ export interface CookiesSchema extends Schema.declare ({ + runtime: "Cookies.CookiesSchema", + Type: "Cookies.Cookies", + importDeclarations: [`import * as Cookies from "effect/unstable/http/Cookies"`] + }), expected: "Cookies", toCodecJson: () => Schema.link()( @@ -146,14 +147,15 @@ export interface CookieSchema extends Schema.declare {} export const CookieSchema: CookieSchema = Schema.declare( isCookie, { - typeConstructor: { - _tag: "effect/http/Cookie" - }, - generation: { - runtime: `Cookies.CookieSchema`, - Type: `Cookies.Cookie`, - importDeclaration: `import * as Cookie from "effect/unstable/http/Cookies"` + representation: { + id: "effect/http/Cookie", + payload: null }, + toCode: () => ({ + runtime: "Cookies.CookieSchema", + Type: "Cookies.Cookie", + importDeclarations: [`import * as Cookies from "effect/unstable/http/Cookies"`] + }), expected: "Cookie" } ) @@ -251,7 +253,7 @@ export const fromReadonlyRecord = (cookies: Record.ReadonlyRecord): Cookies => { const record: Record = {} for (const cookie of cookies) { - record[cookie.name] = cookie + InternalRecord.assignProperty(record, cookie.name, cookie) } return fromReadonlyRecord(record) } @@ -417,13 +419,17 @@ export const empty: Cookies = fromIterable([]) /** * Returns `true` when the `Cookies` collection contains no cookies. * - * @category refinements + * @category predicates * @since 4.0.0 */ export const isEmpty = (self: Cookies): boolean => Record.isEmptyRecord(self.cookies) // oxlint-disable-next-line no-control-regex const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ +const cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ +// oxlint-disable-next-line no-control-regex +const cookieDomainRegExp = /^[\u0009\u0020-\u003a\u003c-\u007e\u0080-\u00ff]+$/ +const cookiePathRegExp = /^[\u0020-\u003a\u003c-\u007e]+$/ const CookieProto = { [CookieTypeId]: CookieTypeId, @@ -453,26 +459,10 @@ export function makeCookie( value: string, options?: Cookie["options"] | undefined ): Result.Result { - if (!fieldContentRegExp.test(name)) { - return Result.fail(CookiesError.fromReason("InvalidCookieName")) - } const encodedValue = encodeURIComponent(value) - if (encodedValue && !fieldContentRegExp.test(encodedValue)) { - return Result.fail(CookiesError.fromReason("InvalidCookieValue")) - } - - if (options !== undefined) { - if (options.domain !== undefined && !fieldContentRegExp.test(options.domain)) { - return Result.fail(CookiesError.fromReason("InvalidCookieDomain")) - } - - if (options.path !== undefined && !fieldContentRegExp.test(options.path)) { - return Result.fail(CookiesError.fromReason("InvalidCookiePath")) - } - - if (options.maxAge !== undefined && !Duration.isFinite(Duration.fromInputUnsafe(options.maxAge))) { - return Result.fail(CookiesError.fromReason("CookieInfinityMaxAge")) - } + const error = validateCookie(name, encodedValue, options) + if (error !== undefined) { + return Result.fail(error) } return Result.succeed(Object.assign(Object.create(CookieProto), { @@ -483,6 +473,28 @@ export function makeCookie( })) } +function validateCookie( + name: string, + encodedValue: string, + options: Cookie["options"] | undefined +): CookiesError | undefined { + if (!cookieNameRegExp.test(name)) { + return CookiesError.fromReason("InvalidCookieName") + } + if (encodedValue && !fieldContentRegExp.test(encodedValue)) { + return CookiesError.fromReason("InvalidCookieValue") + } + if (options?.domain !== undefined && !cookieDomainRegExp.test(options.domain)) { + return CookiesError.fromReason("InvalidCookieDomain") + } + if (options?.path !== undefined && !cookiePathRegExp.test(options.path)) { + return CookiesError.fromReason("InvalidCookiePath") + } + if (options?.maxAge !== undefined && !Duration.isFinite(Duration.fromInputUnsafe(options.maxAge))) { + return CookiesError.fromReason("CookieInfinityMaxAge") + } +} + /** * Create a new cookie, throwing an error if invalid * @@ -526,7 +538,7 @@ export const setAllCookie: { } = dual(2, (self: Cookies, cookies: Iterable) => { const record = { ...self.cookies } for (const cookie of cookies) { - record[cookie.name] = cookie + InternalRecord.assignProperty(record, cookie.name, cookie) } return fromReadonlyRecord(record) }) @@ -568,7 +580,8 @@ export const get: { (self: Cookies, name: string): Option.Option } = dual( (args) => isCookies(args[0]), - (self: Cookies, name: string): Option.Option => Option.fromUndefinedOr(self.cookies[name]) + (self: Cookies, name: string): Option.Option => + Option.fromUndefinedOr(Object.hasOwn(self.cookies, name) ? self.cookies[name] : undefined) ) /** @@ -742,7 +755,7 @@ export const setAll: { if (Result.isFailure(result)) { return result as Result.Failure } - record[name] = result.success + InternalRecord.assignProperty(record, name, result.success) } return Result.succeed(fromReadonlyRecord(record)) } @@ -776,6 +789,10 @@ export const setAllUnsafe: { * @since 4.0.0 */ export function serializeCookie(self: Cookie): string { + const error = validateCookie(self.name, self.valueEncoded, self.options) + if (error !== undefined) { + throw error + } let str = self.name + "=" + self.valueEncoded if (self.options === undefined) { @@ -865,7 +882,7 @@ export const toRecord = (self: Cookies): Record => { const cookies = Object.values(self.cookies) for (let index = 0; index < cookies.length; index++) { const cookie = cookies[index] - record[cookie.name] = cookie.value + InternalRecord.assignProperty(record, cookie.name, cookie.value) } return record } @@ -926,14 +943,16 @@ export function parseHeader(header: string): Record { } const key = header.substring(pos, eqIdx++).trim() - if (result[key] === undefined) { + if (!Object.hasOwn(result, key)) { 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 + InternalRecord.assignProperty( + result, + key, + !(val.indexOf("%") === -1) ? tryDecodeURIComponent(val) : val + ) } pos = terminatorPos + 1 diff --git a/.context/effect/packages/effect/src/unstable/http/Etag.ts b/.context/effect/packages/effect/src/unstable/http/Etag.ts index 0a8545b60..c35f0ba33 100644 --- a/.context/effect/packages/effect/src/unstable/http/Etag.ts +++ b/.context/effect/packages/effect/src/unstable/http/Etag.ts @@ -71,7 +71,7 @@ export const toString = (self: Etag): string => { /** * Service for generating ETags from filesystem file information or Web `File`-like metadata. * - * @category models + * @category services * @since 4.0.0 */ export class Generator extends Context.Service { const fetch = fiber.getRef(Fetch) const options: globalThis.RequestInit = fiber.context.mapUnsafe.get(RequestInit.key) ?? {} - let headers = options.headers ? Headers.merge(Headers.fromInput(options.headers), request.headers) : request.headers + let headers = options.headers + ? Headers.merge(Headers.fromInput(options.headers as Headers.Input), request.headers) + : request.headers if (headers["content-length"]) { headers = Headers.remove(headers, "content-length") } diff --git a/.context/effect/packages/effect/src/unstable/http/FindMyWay.ts b/.context/effect/packages/effect/src/unstable/http/FindMyWay.ts index 7507cb4fc..c4a1c92e4 100644 --- a/.context/effect/packages/effect/src/unstable/http/FindMyWay.ts +++ b/.context/effect/packages/effect/src/unstable/http/FindMyWay.ts @@ -1,15 +1,74 @@ /** - * Re-exports the `find-my-way-ts` router package used by the unstable HTTP - * routing modules. + * A radix-tree HTTP router used by the unstable HTTP routing modules. * - * This module keeps the router types and helpers available from the Effect HTTP - * namespace without wrapping or changing them. + * @since 4.0.0 + */ +import * as internal from "./FindMyWay/internal/router.ts" + +/* + * MIT License + * + * Copyright (c) 2017-2019 Tomas Della Vedova + * + * 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. + */ + +/** + * @since 4.0.0 + * @category models + */ +export interface RouterConfig { + readonly ignoreTrailingSlash: boolean + readonly ignoreDuplicateSlashes: boolean + readonly caseSensitive: boolean + readonly maxParamLength: number +} + +/** + * @since 4.0.0 + * @category models + */ +export type PathInput = `/${string}` | "*" + +/** + * @since 4.0.0 + * @category models + */ +export interface Router { + readonly on: (method: string | Iterable, path: PathInput, handler: A) => void + readonly all: (path: PathInput, handler: A) => void + readonly find: (method: string, url: string) => FindResult | undefined + readonly has: (method: string, url: string) => boolean +} + +/** * @since 4.0.0 + * @category models */ +export interface FindResult { + readonly handler: A + readonly params: Record + readonly searchParams: Record> +} /** - * @category re-exports * @since 4.0.0 + * @category constructors */ -export * from "find-my-way-ts" +export const make: (options?: Partial) => Router = internal.make diff --git a/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/queryString.ts b/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/queryString.ts new file mode 100644 index 000000000..eeda1cb2b --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/queryString.ts @@ -0,0 +1,420 @@ +/* + * MIT License + * + * Copyright (c) 2017-2019 Tomas Della Vedova + * + * 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. + */ + +/** + * @since 1.0.0 + */ +// Taken from https://github.com/anonrig/fast-querystring under MIT License +const plusRegex = /\+/g +const Empty: new() => Record = function() {} as any +Empty.prototype = Object.create(null) + +/** + * @category parsing + * @since 1.0.0 + */ +export function parse(input: string) { + // Optimization: Use new Empty() instead of Object.create(null) for performance + // v8 has a better optimization for initializing functions compared to Object + const result = new Empty() + + if (typeof input !== "string") { + return result + } + + const inputLength = input.length + let key = "" + let value = "" + let startingIndex = -1 + let equalityIndex = -1 + let shouldDecodeKey = false + let shouldDecodeValue = false + let keyHasPlus = false + let valueHasPlus = false + let hasBothKeyValuePair = false + let c = 0 + + // Have a boundary of input.length + 1 to access last pair inside the loop. + for (let i = 0; i < inputLength + 1; i++) { + c = i !== inputLength ? input.charCodeAt(i) : 38 + + // Handle '&' and end of line to pass the current values to result + if (c === 38) { + hasBothKeyValuePair = equalityIndex > startingIndex + + // Optimization: Reuse equality index to store the end of key + if (!hasBothKeyValuePair) { + equalityIndex = i + } + + key = input.slice(startingIndex + 1, equalityIndex) + + // Add key/value pair only if the range size is greater than 1; a.k.a. contains at least "=" + if (hasBothKeyValuePair || key.length > 0) { + // Optimization: Replace '+' with space + if (keyHasPlus) { + key = key.replace(plusRegex, " ") + } + + // Optimization: Do not decode if it's not necessary. + if (shouldDecodeKey) { + try { + key = decodeURIComponent(key) || key + } catch {} + } + + if (hasBothKeyValuePair) { + value = input.slice(equalityIndex + 1, i) + + if (valueHasPlus) { + value = value.replace(plusRegex, " ") + } + + if (shouldDecodeValue) { + try { + value = decodeURIComponent(value) || value + } catch {} + } + } + const currentValue = result[key] + + if (currentValue === undefined) { + result[key] = value + } else { + // Optimization: value.pop is faster than Array.isArray(value) + if (currentValue.pop) { + currentValue.push(value) + } else { + result[key] = [currentValue, value] + } + } + } + + // Reset reading key value pairs + value = "" + startingIndex = i + equalityIndex = i + shouldDecodeKey = false + shouldDecodeValue = false + keyHasPlus = false + valueHasPlus = false + } // Check '=' + else if (c === 61) { + if (equalityIndex <= startingIndex) { + equalityIndex = i + } // If '=' character occurs again, we should decode the input. + else { + shouldDecodeValue = true + } + } // Check '+', and remember to replace it with empty space. + else if (c === 43) { + if (equalityIndex > startingIndex) { + valueHasPlus = true + } else { + keyHasPlus = true + } + } // Check '%' character for encoding + else if (c === 37) { + if (equalityIndex > startingIndex) { + shouldDecodeValue = true + } else { + shouldDecodeKey = true + } + } + } + + return result +} + +function getAsPrimitive(value: any) { + const type = typeof value + + if (type === "string") { + // Length check is handled inside encodeString function + return encodeString(value) + } else if (type === "bigint" || type === "boolean") { + return "" + value + } else if (type === "number" && Number.isFinite(value)) { + return value < 1e21 ? "" + value : encodeString("" + value) + } + + return "" +} + +/** + * @category encoding + * @since 1.0.0 + */ +export function stringify(input: Record): string { + let result = "" + + if (input === null || typeof input !== "object") { + return result + } + + const separator = "&" + const keys = Object.keys(input) + const keyLength = keys.length + let valueLength = 0 + + for (let i = 0; i < keyLength; i++) { + const key = keys[i] + const value = input[key] + const encodedKey = encodeString(key) + "=" + + if (i) { + result += separator + } + + if (Array.isArray(value)) { + valueLength = value.length + for (let j = 0; j < valueLength; j++) { + if (j) { + result += separator + } + + // Optimization: Dividing into multiple lines improves the performance. + // Since v8 does not need to care about the '+' character if it was one-liner. + result += encodedKey + result += getAsPrimitive(value[j]) + } + } else { + result += encodedKey + result += getAsPrimitive(value) + } + } + + return result +} + +// ----------------------------------------------------------------------------- + +// This has been taken from Node.js project. +// Full implementation can be found from https://github.com/nodejs/node/blob/main/lib/internal/querystring.js + +const hexTable = Array.from( + { length: 256 }, + (_, i) => "%" + ((i < 16 ? "0" : "") + i.toString(16)).toUpperCase() +) + +// These characters do not need escaping when generating query strings: +// ! - . _ ~ +// ' ( ) * +// digits +// alpha (uppercase) +// alpha (lowercase) +// biome-ignore format: the array should not be formatted +const noEscape = new Int8Array([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 0 - 15 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, // 16 - 31 + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 0, + 0, + 1, + 1, + 0, // 32 - 47 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, // 48 - 63 + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 64 - 79 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 1, // 80 - 95 + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, // 96 - 111 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 1, + 0 // 112 - 127 +]) + +function encodeString(str: string) { + const len = str.length + if (len === 0) return "" + + let out = "" + let lastPos = 0 + let i = 0 + + outer: for (; i < len; i++) { + let c = str.charCodeAt(i) + + // ASCII + while (c < 0x80) { + if (noEscape[c] !== 1) { + if (lastPos < i) out += str.slice(lastPos, i) + lastPos = i + 1 + out += hexTable[c] + } + + if (++i === len) break outer + + c = str.charCodeAt(i) + } + + if (lastPos < i) out += str.slice(lastPos, i) + + // Multi-byte characters ... + if (c < 0x800) { + lastPos = i + 1 + out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)] + continue + } + if (c < 0xd800 || c >= 0xe000) { + lastPos = i + 1 + out += hexTable[0xe0 | (c >> 12)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)] + continue + } + // Surrogate pair + ++i + + // This branch should never happen because all URLSearchParams entries + // should already be converted to USVString. But, included for + // completion's sake anyway. + if (i >= len) { + throw new Error("URI malformed") + } + + const c2 = str.charCodeAt(i) & 0x3ff + + lastPos = i + 1 + c = 0x10000 + (((c & 0x3ff) << 10) | c2) + out += hexTable[0xf0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3f)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)] + } + if (lastPos === 0) return str + if (lastPos < len) return out + str.slice(lastPos) + return out +} diff --git a/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/router.ts b/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/router.ts new file mode 100644 index 000000000..8008f9f5f --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/FindMyWay/internal/router.ts @@ -0,0 +1,950 @@ +/* + * MIT License + * + * Copyright (c) 2017-2019 Tomas Della Vedova + * + * 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. + */ + +import type * as Router from "../../FindMyWay.ts" +import * as QS from "./queryString.ts" + +const FULL_PATH_REGEXP = /^https?:\/\/.*?\// +const OPTIONAL_PARAM_REGEXP = /(\/:[^/()]*?)\?(\/?)/ + +interface Route { + readonly method: string + readonly path: Router.PathInput + readonly pattern: string + readonly handler: A + readonly params: ReadonlyArray +} + +/** @internal */ +export const make = ( + options: Partial = {} +): Router.Router => new RouterImpl(options) + +class RouterImpl implements Router.Router { + constructor(options: Partial = {}) { + this.options = { + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: true, + caseSensitive: false, + maxParamLength: 100, + ...options + } + } + + readonly options: Router.RouterConfig + routes: Array = [] + trees: Record = Object.create(null) + + on( + method: string | Iterable, + path: Router.PathInput, + handler: A + ): void { + const optionalParamMatch = path.match(OPTIONAL_PARAM_REGEXP) + if (optionalParamMatch && optionalParamMatch.index !== undefined) { + assert( + path.length === optionalParamMatch.index + optionalParamMatch[0].length, + "Optional Parameter needs to be the last parameter of the path" + ) + + const pathFull = path.replace( + OPTIONAL_PARAM_REGEXP, + "$1$2" + ) as Router.PathInput + const pathOptional = (path.replace( + OPTIONAL_PARAM_REGEXP, + "$2" + ) || "/") as Router.PathInput + + this.on(method, pathFull, handler) + this.on(method, pathOptional, handler) + return + } + + if (this.options.ignoreDuplicateSlashes) { + path = removeDuplicateSlashes(path) + } + + if (this.options.ignoreTrailingSlash) { + path = trimLastSlash(path) + } + + const methods = typeof method === "string" ? [method] : method + for (const method of methods) { + this._on(method, path, handler) + } + } + + all(path: Router.PathInput, handler: A) { + this.on(httpMethods, path, handler) + } + + private _on(method: string, path: Router.PathInput, handler: A): void { + if (this.trees[method] === undefined) { + this.trees[method] = new StaticNode("/") + } + + let pattern = path + if (pattern === "*" && this.trees[method].prefix.length !== 0) { + const currentRoot = this.trees[method] + this.trees[method] = new StaticNode("") + this.trees[method].staticChildren["/"] = currentRoot + } + + let parentNodePathIndex = this.trees[method].prefix.length + let currentNode: Node = this.trees[method] + + const params = [] + for (let i = 0; i <= pattern.length; i++) { + if (pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) === 58) { + // It's a double colon + i++ + continue + } + + const isParametricNode = pattern.charCodeAt(i) === 58 && pattern.charCodeAt(i + 1) !== 58 + const isWildcardNode = pattern.charCodeAt(i) === 42 + + if ( + isParametricNode || + isWildcardNode || + (i === pattern.length && i !== parentNodePathIndex) + ) { + let staticNodePath = pattern.slice(parentNodePathIndex, i) + if (!this.options.caseSensitive) { + staticNodePath = staticNodePath.toLowerCase() + } + staticNodePath = staticNodePath.split("::").join(":") + staticNodePath = staticNodePath.split("%").join("%25") + // add the static part of the route to the tree + currentNode = (currentNode as StaticNode).createStaticChild( + staticNodePath + ) + } + + if (isParametricNode) { + let isRegexNode = false + let isParamSafe = true + let backtrack = "" + const regexps = [] + let nodePatternParts = "" + + let lastParamStartIndex = i + 1 + for (let j = lastParamStartIndex;; j++) { + const charCode = pattern.charCodeAt(j) + + const isRegexParam = charCode === 40 + const isStaticPart = charCode === 45 || charCode === 46 + const isEndOfNode = charCode === 47 || j === pattern.length + + if (isRegexParam || isStaticPart || isEndOfNode) { + const paramName = pattern.slice(lastParamStartIndex, j) + params.push(paramName) + + isRegexNode = isRegexNode || isRegexParam || isStaticPart + + if (isRegexParam) { + const endOfRegexIndex = getClosingParenthensePosition(pattern, j) + const regexString = pattern.slice(j, endOfRegexIndex + 1) + + regexps.push(trimRegExpStartAndEnd(regexString)) + + j = endOfRegexIndex + 1 + isParamSafe = true + } else { + regexps.push(isParamSafe ? "(.*?)" : `(${backtrack}|(?:(?!${backtrack}).)*)`) + isParamSafe = false + } + + const staticPartStartIndex = j + for (; j < pattern.length; j++) { + const charCode = pattern.charCodeAt(j) + if (charCode === 47) break + if (charCode === 58) { + const nextCharCode = pattern.charCodeAt(j + 1) + if (nextCharCode === 58) j++ + else break + } + } + + let staticPart = pattern.slice(staticPartStartIndex, j) + if (staticPart) { + staticPart = staticPart.split("::").join(":") + staticPart = staticPart.split("%").join("%25") + regexps.push(backtrack = escapeRegExp(staticPart)) + } + + lastParamStartIndex = j + 1 + nodePatternParts += "()" + staticPart + + if ( + isEndOfNode || + pattern.charCodeAt(j) === 47 || + j === pattern.length + ) { + const nodePattern = isRegexNode ? nodePatternParts : staticPart + const nodePath = pattern.slice(i, j) + + pattern = pattern.slice(0, i + 1) + nodePattern + pattern.slice(j) + i += nodePattern.length + + const regex = isRegexNode + ? new RegExp("^" + regexps.join("") + "$") + : undefined + currentNode = (currentNode as StaticNode).createParametricChild( + regex, + staticPart, + nodePath + ) + parentNodePathIndex = i + 1 + break + } + } + } + } else if (isWildcardNode) { + // add the wildcard parameter + params.push("*") + currentNode = (currentNode as StaticNode).createWildcardChild() + parentNodePathIndex = i + 1 + + if (i !== pattern.length - 1) { + throw new Error("Wildcard must be the last character in the route") + } + } + } + + if (!this.options.caseSensitive) { + pattern = pattern.toLowerCase() as Router.PathInput + } + + if (pattern === "*") { + pattern = "/*" + } + + for (const existRoute of this.routes) { + if (existRoute.method === method && existRoute.pattern === pattern) { + throw new Error( + `Method '${method}' already declared for route '${pattern}'` + ) + } + } + + const route = { method, path, pattern, params, handler } + this.routes.push(route) + currentNode.addRoute(route) + } + + has(method: string, path: string): boolean { + const node = this.trees[method] + if (node === undefined) { + return false + } + + const staticNode = node.getStaticChild(path) + if (staticNode === undefined) { + return false + } + + return staticNode.isLeafNode + } + + find(method: string, path: string): Router.FindResult | undefined { + let currentNode: Node | undefined = this.trees[method] + if (currentNode === undefined) return undefined + + if (path.charCodeAt(0) !== 47) { + // 47 is '/' + path = path.replace(FULL_PATH_REGEXP, "/") + } + + // This must be run before sanitizeUrl as the resulting function + // .sliceParameter must be constructed with same URL string used + // throughout the rest of this function. + if (this.options.ignoreDuplicateSlashes) { + path = removeDuplicateSlashes(path) + } + + let sanitizedUrl + let querystring + let shouldDecodeParam + + try { + sanitizedUrl = safeDecodeURI(path) + path = sanitizedUrl.path + querystring = sanitizedUrl.querystring + shouldDecodeParam = sanitizedUrl.shouldDecodeParam + } catch (error) { + return undefined + } + + if (this.options.ignoreTrailingSlash) { + path = trimLastSlash(path) + } + + const originPath = path + + if (this.options.caseSensitive === false) { + path = path.toLowerCase() + } + + const maxParamLength = this.options.maxParamLength + + let pathIndex = (currentNode as StaticNode).prefix.length + const params = [] + const pathLen = path.length + + const brothersNodesStack: Array = [] + + while (true) { + if (pathIndex === pathLen && currentNode.isLeafNode) { + const handle = currentNode.handlerStorage?.find() + if (handle !== undefined) { + return { + handler: handle.handler as A, + params: handle.createParams(params), + searchParams: QS.parse(querystring) + } as const + } + } + + let node: Node | undefined = currentNode.getNextNode( + path, + pathIndex, + brothersNodesStack, + params.length + ) + + if (node === undefined) { + if (brothersNodesStack.length === 0) { + return undefined + } + + const brotherNodeState = brothersNodesStack.pop()! + pathIndex = brotherNodeState.brotherPathIndex + params.splice(brotherNodeState.paramsCount) + node = brotherNodeState.brotherNode + } + + currentNode = node + + while (true) { + if (currentNode._tag === "StaticNode") { + pathIndex += currentNode.prefix.length + break + } + + if (currentNode._tag === "WildcardNode") { + let param = originPath.slice(pathIndex) + if (shouldDecodeParam) { + param = safeDecodeURIComponent(param) + } + + params.push(param) + pathIndex = pathLen + break + } + + let paramEndIndex = originPath.indexOf("/", pathIndex) + if (paramEndIndex === -1) { + paramEndIndex = pathLen + } + + let param = originPath.slice(pathIndex, paramEndIndex) + if (shouldDecodeParam) { + param = safeDecodeURIComponent(param) + } + + if (currentNode.regex !== undefined) { + const matchedParameters: RegExpExecArray | null = currentNode.regex.exec(param) + if (matchedParameters === null) { + if (brothersNodesStack.length === 0) { + return undefined + } + + const brotherNodeState = brothersNodesStack.pop()! + pathIndex = brotherNodeState.brotherPathIndex + params.splice(brotherNodeState.paramsCount) + currentNode = brotherNodeState.brotherNode + continue + } + + let maxParamLengthExceeded = false + for (let i = 1; i < matchedParameters.length; i++) { + const matchedParam = matchedParameters[i] ?? "" + if (matchedParam.length > maxParamLength) { + maxParamLengthExceeded = true + break + } + } + + if (maxParamLengthExceeded) { + if (brothersNodesStack.length === 0) { + return undefined + } + + const brotherNodeState = brothersNodesStack.pop()! + pathIndex = brotherNodeState.brotherPathIndex + params.splice(brotherNodeState.paramsCount) + currentNode = brotherNodeState.brotherNode + continue + } + + for (let i = 1; i < matchedParameters.length; i++) { + params.push(matchedParameters[i] ?? "") + } + } else { + if (param.length > maxParamLength) { + if (brothersNodesStack.length === 0) { + return undefined + } + + const brotherNodeState = brothersNodesStack.pop()! + pathIndex = brotherNodeState.brotherPathIndex + params.splice(brotherNodeState.paramsCount) + currentNode = brotherNodeState.brotherNode + continue + } + params.push(param) + } + + pathIndex = paramEndIndex + break + } + } + } +} + +// ---------------------------------------------------------------------------- +// Handler storage +// ---------------------------------------------------------------------------- + +interface Handler { + readonly params: ReadonlyArray + readonly handler: unknown + readonly createParams: ( + paramsArray: ReadonlyArray + ) => Record +} + +class HandlerStorage { + readonly handlers: Array = [] + unconstrainedHandler: Handler | undefined + + find() { + return this.unconstrainedHandler + } + + add(route: Route) { + const handler: Handler = { + params: route.params, + handler: route.handler, + createParams: compileCreateParams(route.params) + } + this.handlers.push(handler) + this.unconstrainedHandler = this.handlers[0] + } +} + +// ---------------------------------------------------------------------------- +// Nodes +// ---------------------------------------------------------------------------- + +interface BrotherNode { + readonly paramsCount: number + readonly brotherPathIndex: number + readonly brotherNode: Node +} + +type Node = StaticNode | ParametricNode | WildcardNode + +abstract class NodeBase { + isLeafNode = false + routes: Array | undefined + handlerStorage: HandlerStorage | undefined + + addRoute(route: Route) { + if (this.routes === undefined) { + this.routes = [route] + } else { + this.routes.push(route) + } + + if (this.handlerStorage === undefined) { + this.handlerStorage = new HandlerStorage() + } + this.isLeafNode = true + this.handlerStorage.add(route) + } + + abstract getNextNode( + path: string, + pathIndex: number, + nodeStack: any, + paramsCount: number + ): Node | undefined +} + +abstract class ParentNode extends NodeBase { + readonly staticChildren: Record = Object.create(null) + + findStaticMatchingChild( + path: string, + pathIndex: number + ): StaticNode | undefined { + const staticChild = this.staticChildren[path.charAt(pathIndex)] + if ( + staticChild === undefined || + !staticChild.matchPrefix(path, pathIndex) + ) { + return undefined + } + return staticChild + } + + getStaticChild(path: string, pathIndex = 0): StaticNode | undefined { + if (path.length === pathIndex) { + return this as any + } + + const staticChild = this.findStaticMatchingChild(path, pathIndex) + if (staticChild === undefined) { + return undefined + } + + return staticChild.getStaticChild( + path, + pathIndex + staticChild.prefix.length + ) + } + + createStaticChild(path: string): StaticNode { + if (path.length === 0) { + return this as any + } + + let staticChild = this.staticChildren[path.charAt(0)] + if (staticChild) { + let i = 1 + for (; i < staticChild.prefix.length; i++) { + if (path.charCodeAt(i) !== staticChild.prefix.charCodeAt(i)) { + staticChild = staticChild.split(this, i) + break + } + } + return staticChild.createStaticChild(path.slice(i)) + } + + const label = path.charAt(0) + this.staticChildren[label] = new StaticNode(path) + return this.staticChildren[label] + } +} + +class StaticNode extends ParentNode { + readonly _tag = "StaticNode" + constructor(prefix: string) { + super() + this.setPrefix(prefix) + } + + prefix!: string + matchPrefix!: (path: string, pathIndex: number) => boolean + readonly parametricChildren: Array = [] + + wildcardChild: WildcardNode | undefined + + private setPrefix(prefix: string) { + this.prefix = prefix + + if (prefix.length === 1) { + this.matchPrefix = (_path, _pathIndex) => true + } else { + const len = prefix.length + this.matchPrefix = function(path, pathIndex) { + for (let i = 1; i < len; i++) { + if (path.charCodeAt(pathIndex + i) !== this.prefix.charCodeAt(i)) { + return false + } + } + return true + } + } + } + + getParametricChild(regex: RegExp | undefined): ParametricNode | undefined { + if (regex === undefined) { + return this.parametricChildren.find((child) => child.isRegex === false) + } + + const source = regex.source + return this.parametricChildren.find((child) => { + if (child.regex === undefined) { + return false + } + return child.regex.source === source + }) + } + + createParametricChild( + regex: RegExp | undefined, + staticSuffix: string | undefined, + nodePath: string + ) { + let child = this.getParametricChild(regex) + if (child !== undefined) { + child.nodePaths.add(nodePath) + return child + } + + child = new ParametricNode(regex, staticSuffix, nodePath) + this.parametricChildren.push(child) + this.parametricChildren.sort((child1, child2) => { + if (!child1.isRegex) return 1 + if (!child2.isRegex) return -1 + + if (child1.staticSuffix === undefined) return 1 + if (child2.staticSuffix === undefined) return -1 + + if (child2.staticSuffix.endsWith(child1.staticSuffix)) return 1 + if (child1.staticSuffix.endsWith(child2.staticSuffix)) return -1 + + return 0 + }) + + return child + } + + createWildcardChild() { + if (this.wildcardChild === undefined) { + this.wildcardChild = new WildcardNode() + } + return this.wildcardChild + } + + split(parentNode: ParentNode, length: number) { + const parentPrefix = this.prefix.slice(0, length) + const childPrefix = this.prefix.slice(length) + + this.setPrefix(childPrefix) + + const staticNode = new StaticNode(parentPrefix) + staticNode.staticChildren[childPrefix.charAt(0)] = this + parentNode.staticChildren[parentPrefix.charAt(0)] = staticNode + + return staticNode + } + + getNextNode( + path: string, + pathIndex: number, + nodeStack: Array, + paramsCount: number + ): Node | undefined { + let node: Node | undefined = this.findStaticMatchingChild(path, pathIndex) + let parametricBrotherNodeIndex = 0 + + if (node === undefined) { + if (this.parametricChildren.length === 0) { + return this.wildcardChild + } + + node = this.parametricChildren[0] + parametricBrotherNodeIndex = 1 + } + + if (this.wildcardChild !== undefined) { + nodeStack.push({ + paramsCount, + brotherPathIndex: pathIndex, + brotherNode: this.wildcardChild + }) + } + + for ( + let i = this.parametricChildren.length - 1; + i >= parametricBrotherNodeIndex; + i-- + ) { + nodeStack.push({ + paramsCount, + brotherPathIndex: pathIndex, + brotherNode: this.parametricChildren[i] + }) + } + + return node + } +} + +class ParametricNode extends ParentNode { + readonly _tag = "ParametricNode" + readonly regex: RegExp | undefined + readonly staticSuffix: string | undefined + constructor( + regex: RegExp | undefined, + staticSuffix: string | undefined, + nodePath: string + ) { + super() + this.regex = regex + this.staticSuffix = staticSuffix + this.isRegex = !!regex + this.nodePaths = new Set([nodePath]) + } + + readonly isRegex: boolean + readonly nodePaths: Set + + getNextNode(path: string, pathIndex: number) { + return this.findStaticMatchingChild(path, pathIndex) + } +} + +class WildcardNode extends NodeBase { + readonly _tag = "WildcardNode" + getNextNode( + _path: string, + _pathIndex: number, + _nodeStack: any, + _paramsCount: number + ): Node | undefined { + return undefined + } +} + +// -- + +interface Assert { + (condition: any, message?: string): asserts condition +} +const assert: Assert = (condition, message) => { + if (!condition) { + throw new Error(message) + } +} + +function removeDuplicateSlashes(path: string): Router.PathInput { + return path.replace(/\/\/+/g, "/") as Router.PathInput +} + +function trimLastSlash(path: string): Router.PathInput { + if (path.length > 1 && path.charCodeAt(path.length - 1) === 47) { + return path.slice(0, -1) as Router.PathInput + } + return path as Router.PathInput +} + +function compileCreateParams( + params: ReadonlyArray +): (paramsArray: ReadonlyArray) => Record { + const len = params.length + return function(paramsArray) { + const paramsObject: Record = Object.create(null) + for (let i = 0; i < len; i++) { + paramsObject[params[i]] = paramsArray[i] + } + return paramsObject + } +} + +function getClosingParenthensePosition(path: string, idx: number) { + // `path.indexOf()` will always return the first position of the closing parenthese, + // but it's inefficient for grouped or wrong regexp expressions. + // see issues #62 and #63 for more info + + let parentheses = 1 + + while (idx < path.length) { + idx++ + + // ignore skipped chars + if (path[idx] === "\\") { + idx++ + continue + } + + if (path[idx] === ")") { + parentheses-- + } else if (path[idx] === "(") { + parentheses++ + } + + if (!parentheses) return idx + } + + throw new TypeError("Invalid regexp expression in \"" + path + "\"") +} + +function trimRegExpStartAndEnd(regexString: string) { + // removes chars that marks start "^" and end "$" of regexp + if (regexString.charCodeAt(1) === 94) { + regexString = regexString.slice(0, 1) + regexString.slice(2) + } + + if (regexString.charCodeAt(regexString.length - 2) === 36) { + regexString = regexString.slice(0, regexString.length - 2) + + regexString.slice(regexString.length - 1) + } + + return regexString +} + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") +} + +// It must spot all the chars where decodeURIComponent(x) !== decodeURI(x) +// The chars are: # $ & + , / : ; = ? @ +function decodeComponentChar(highCharCode: number, lowCharCode: number) { + if (highCharCode === 50) { + if (lowCharCode === 53) return "%" + + if (lowCharCode === 51) return "#" + if (lowCharCode === 52) return "$" + if (lowCharCode === 54) return "&" + if (lowCharCode === 66) return "+" + if (lowCharCode === 98) return "+" + if (lowCharCode === 67) return "," + if (lowCharCode === 99) return "," + if (lowCharCode === 70) return "/" + if (lowCharCode === 102) return "/" + return undefined + } + if (highCharCode === 51) { + if (lowCharCode === 65) return ":" + if (lowCharCode === 97) return ":" + if (lowCharCode === 66) return ";" + if (lowCharCode === 98) return ";" + if (lowCharCode === 68) return "=" + if (lowCharCode === 100) return "=" + if (lowCharCode === 70) return "?" + if (lowCharCode === 102) return "?" + return undefined + } + if (highCharCode === 52 && lowCharCode === 48) { + return "@" + } + return undefined +} + +function safeDecodeURI(path: string) { + let shouldDecode = false + let shouldDecodeParam = false + + let querystring = "" + + for (let i = 1; i < path.length; i++) { + const charCode = path.charCodeAt(i) + + if (charCode === 37) { + const highCharCode = path.charCodeAt(i + 1) + const lowCharCode = path.charCodeAt(i + 2) + + if (decodeComponentChar(highCharCode, lowCharCode) === undefined) { + shouldDecode = true + } else { + shouldDecodeParam = true + // %25 - encoded % char. We need to encode one more time to prevent double decoding + if (highCharCode === 50 && lowCharCode === 53) { + shouldDecode = true + path = path.slice(0, i + 1) + "25" + path.slice(i + 1) + i += 2 + } + i += 2 + } + // Some systems do not follow RFC and separate the path and query + // string with a `;` character (code 59), e.g. `/foo;jsessionid=123456`. + // Thus, we need to split on `;` as well as `?` and `#`. + } else if (charCode === 63 || charCode === 59 || charCode === 35) { + querystring = path.slice(i + 1) + path = path.slice(0, i) + break + } + } + const decodedPath = shouldDecode ? decodeURI(path) : path + return { path: decodedPath, querystring, shouldDecodeParam } as const +} + +function safeDecodeURIComponent(uriComponent: string) { + const startIndex = uriComponent.indexOf("%") + if (startIndex === -1) return uriComponent + + let decoded = "" + let lastIndex = startIndex + + for (let i = startIndex; i < uriComponent.length; i++) { + if (uriComponent.charCodeAt(i) === 37) { + if (i + 2 >= uriComponent.length) break + + const highCharCode = uriComponent.charCodeAt(i + 1) + const lowCharCode = uriComponent.charCodeAt(i + 2) + + const decodedChar = decodeComponentChar(highCharCode, lowCharCode) + decoded += uriComponent.slice(lastIndex, i) + decodedChar + + lastIndex = i + 3 + } + } + return ( + uriComponent.slice(0, startIndex) + decoded + uriComponent.slice(lastIndex) + ) +} + +const httpMethods = [ + "ACL", + "BIND", + "CHECKOUT", + "CONNECT", + "COPY", + "DELETE", + "GET", + "HEAD", + "LINK", + "LOCK", + "M-SEARCH", + "MERGE", + "MKACTIVITY", + "MKCALENDAR", + "MKCOL", + "MOVE", + "NOTIFY", + "OPTIONS", + "PATCH", + "POST", + "PROPFIND", + "PROPPATCH", + "PURGE", + "PUT", + "QUERY", + "REBIND", + "REPORT", + "SEARCH", + "SOURCE", + "SUBSCRIBE", + "TRACE", + "UNBIND", + "UNLINK", + "UNLOCK", + "UNSUBSCRIBE" +] as const diff --git a/.context/effect/packages/effect/src/unstable/http/Headers.ts b/.context/effect/packages/effect/src/unstable/http/Headers.ts index f0fb26744..d46772524 100644 --- a/.context/effect/packages/effect/src/unstable/http/Headers.ts +++ b/.context/effect/packages/effect/src/unstable/http/Headers.ts @@ -42,7 +42,7 @@ export type TypeId = typeof TypeId /** * Returns `true` if the provided value is a `Headers` value. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isHeaders = (u: unknown): u is Headers => Predicate.hasProperty(u, TypeId) @@ -130,15 +130,15 @@ export interface HeadersSchema extends Schema.declare ({ + runtime: "Headers.HeadersSchema", + Type: "Headers.Headers", + importDeclarations: [`import * as Headers from "effect/unstable/http/Headers"`] + }), expected: "Headers", toEquivalence: () => Equivalence, toCodec: () => @@ -423,7 +423,7 @@ export const redact: { * * Defaults include `authorization`, `cookie`, `set-cookie`, and `x-api-key`. * - * @category fiber refs + * @category services * @since 4.0.0 */ export const CurrentRedactedNames = Context.Reference< diff --git a/.context/effect/packages/effect/src/unstable/http/HttpBody.ts b/.context/effect/packages/effect/src/unstable/http/HttpBody.ts index c43a60d4b..ae9db9374 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpBody.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpBody.ts @@ -30,7 +30,7 @@ const TypeId = "~effect/http/HttpBody" /** * Returns `true` if the provided value is an `HttpBody`. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isHttpBody = (u: unknown): u is HttpBody => Predicate.hasProperty(u, TypeId) @@ -484,12 +484,26 @@ export const stream = ( contentLength?: number ): Stream => new Stream(body, contentType ?? "application/octet-stream", contentLength) +const fileContentLength = ( + size: FileSystem.SizeInput, + options?: { + readonly bytesToRead?: FileSystem.SizeInput | undefined + readonly offset?: FileSystem.SizeInput | undefined + } +): number => { + const available = Math.max(0, Number(size) - Number(options?.offset ?? 0)) + return options?.bytesToRead === undefined + ? available + : Math.min(available, Math.max(0, Number(options.bytesToRead))) +} + /** * Creates a streaming HTTP body for a file path. * * **Details** * - * The effect requires `FileSystem`, stats the file to set the content length, and can fail with `PlatformError`. + * The effect requires `FileSystem`, stats the file to set the selected content length, and can fail with + * `PlatformError`. * * @category constructors * @since 4.0.0 @@ -510,7 +524,7 @@ export const file = ( stream( fs.stream(path, options), options?.contentType, - Number(info.size) + fileContentLength(info.size, options) )) ) @@ -519,7 +533,8 @@ export const file = ( * * **Details** * - * The effect requires `FileSystem`, uses the provided file size as the content length, and can fail with `PlatformError`. + * The effect requires `FileSystem`, uses the provided file size to determine the selected content length, and can + * fail with `PlatformError`. * * @category constructors * @since 4.0.0 @@ -540,6 +555,6 @@ export const fileFromInfo = ( stream( fs.stream(path, options), options?.contentType, - Number(info.size) + fileContentLength(info.size, options) ) ) diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClient.ts b/.context/effect/packages/effect/src/unstable/http/HttpClient.ts index 102237da2..c3a1a37c1 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClient.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClient.ts @@ -34,6 +34,7 @@ import type { EqualsWith, ExcludeTag, ExtractTag, NoExcessProperties, NoInfer, T import type * as RateLimiter from "../persistence/RateLimiter.ts" import * as Cookies from "./Cookies.ts" import * as Headers from "./Headers.ts" +import * as HttpBody from "./HttpBody.ts" import * as Error from "./HttpClientError.ts" import * as HttpClientRequest from "./HttpClientRequest.ts" import * as HttpClientResponse from "./HttpClientResponse.ts" @@ -257,7 +258,7 @@ export const options: (url: string | URL, options?: HttpClientRequest.Options.No * * The transformation receives both the response effect and the original request, allowing it to change success, error, and environment behavior. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const transform: { @@ -289,7 +290,7 @@ export const transform: { /** * Transforms a client by applying an effectful transformation to each response effect. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const transformResponse: { @@ -476,7 +477,7 @@ export const catchTags: { /** * Filters the result of a response, or runs an alternative effect if the predicate fails. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterOrElse: { @@ -521,7 +522,7 @@ export const filterOrElse: { /** * Filters successful responses, or fails with the error produced by `orFailWith` when the predicate does not match. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterOrFail: { @@ -548,7 +549,7 @@ export const filterOrFail: { /** * Filters responses by HTTP status code. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterStatus: { @@ -563,7 +564,7 @@ export const filterStatus: { /** * Filters responses that return a 2xx status code. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterStatusOk: (self: HttpClient.With) => HttpClient.With = @@ -686,8 +687,10 @@ export const make = ( span.attribute("url.query", query) } const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) + const headerFilter = fiber.getRef(TracerHeaderFilter) const redactedHeaders = Headers.redact(request.headers, redactedHeaderNames) for (const name in redactedHeaders) { + if (!headerFilter(name, "request")) continue span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])) } request = fiber.getRef(TracerPropagationEnabled) @@ -701,6 +704,7 @@ export const make = ( span.attribute("http.response.status_code", response.status) const redactedHeaders = Headers.redact(response.headers, redactedHeaderNames) for (const name in redactedHeaders) { + if (!headerFilter(name, "response")) continue span.attribute(`http.response.header.${name}`, String(redactedHeaders[name])) } @@ -724,7 +728,7 @@ export const make = ( /** * Appends a transformation of the request object before sending it. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const mapRequest: { @@ -746,7 +750,7 @@ export const mapRequest: { /** * Appends an effectful transformation of the request object before sending it. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const mapRequestEffect: { @@ -769,7 +773,7 @@ export const mapRequestEffect: { /** * Prepends a transformation of the request object before sending it. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const mapRequestInput: { @@ -791,7 +795,7 @@ export const mapRequestInput: { /** * Prepends an effectful transformation of the request object before sending it. * - * @category mapping & sequencing + * @category mapping * @since 4.0.0 */ export const mapRequestInputEffect: { @@ -988,7 +992,8 @@ export declare namespace WithRateLimiter { * * **Details** * - * They define the backing limiter, initial limit window, keying strategy, algorithm, token cost, and whether response headers update future limits. + * They define the backing limiter, initial limit window, keying strategy, + * algorithm, token cost, retry count, and response header inspection. * * @category rate limiting * @since 4.0.0 @@ -1021,11 +1026,48 @@ export declare namespace WithRateLimiter { */ readonly tokens?: number | ((request: HttpClientRequest.HttpClientRequest) => number) | undefined /** - * Disable automatic limits updates from response headers. + * The maximum number of automatic retries after an HTTP `429` response. + * Use a non-negative integer. Defaults to an unlimited number of retries. + * Set to `0` to disable automatic retries. + */ + readonly times?: number | undefined + /** + * Overrides the response header names used for rate limit inspection. + * Header names are matched case-insensitively. An omitted field continues + * using the built-in names, while a configured field replaces them for + * that value. Header value formats are unchanged. + */ + readonly responseHeaders?: { + /** + * Header containing the maximum number of requests in a window. + */ + readonly limit?: string | undefined + /** + * Header containing the number of requests remaining in a window. + */ + readonly remaining?: string | undefined + /** + * Header containing the rate limit reset time. + */ + readonly reset?: string | undefined + /** + * Header containing the number of seconds until the rate limit resets. + */ + readonly resetAfter?: string | undefined + /** + * Header containing the retry delay for an HTTP `429` response. + */ + readonly retryAfter?: string | undefined + } | undefined + /** + * Disables automatic limit updates, `Retry-After` delays, and adaptive + * feedback from response headers. This does not disable automatic HTTP + * `429` retries. Set `times` to `0` to disable them. */ readonly disableResponseInspection?: boolean | undefined /** - * Disable adaptive learning from `Retry-After` responses. + * Disables adaptive learning from `Retry-After` responses. Response + * inspection and direct `Retry-After` delays remain enabled. */ readonly disableAdaptiveLearning?: boolean | undefined } @@ -1040,6 +1082,12 @@ export declare namespace WithRateLimiter { * automatically retries HTTP `429` responses (or `HttpClientError` values * wrapping a `429` response) by forcing the retry back through the limiter. * + * **Gotchas** + * + * Automatic HTTP `429` retries are unlimited unless `times` is specified. + * Disabling response inspection does not disable retries; set `times` to `0` + * to return or fail with the first `429`. + * * @category rate limiting * @since 4.0.0 */ @@ -1071,6 +1119,7 @@ export const withRateLimiter: { ? tokensOption : constant(tokensOption ?? 1) const adaptiveLearningEnabled = !options.disableAdaptiveLearning + const headerNames = resolveRateLimiterHeaderNames(options.responseHeaders) const getState = (key: string): RateLimiterState => { const current = states.get(key) @@ -1085,13 +1134,13 @@ export const withRateLimiter: { ? undefined : (clock: Clock, key: string, headers: Headers.Headers, tokens: number) => { const current = getState(key) - const next = parseRateLimiterState(current, clock, headers, tokens) + const next = parseRateLimiterState(current, clock, headers, tokens, headerNames) if (next.limit !== current.limit || !Duration.equals(next.window, current.window)) { states.set(key, next) } } - return transform(self, function loop(effect, request): Effect.Effect< + return transform(self, function loop(effect, request, retries = 0): Effect.Effect< HttpClientResponse.HttpClientResponse, E | RateLimiter.RateLimiterError, R @@ -1101,11 +1150,11 @@ export const withRateLimiter: { const key = resolveKey(request) const tokens = Math.max(resolveTokens(request), 1) const current = getState(key) + const canRetry = options.times === undefined || retries < options.times function retry(retryAfter: Duration.Duration | undefined) { - if (options.disableResponseInspection) return loop(effect, request) return retryAfter - ? Effect.flatMap(Effect.sleep(retryAfter), () => loop(effect, request)) - : loop(effect, request) + ? Effect.flatMap(Effect.sleep(retryAfter), () => loop(effect, request, retries + 1)) + : loop(effect, request, retries + 1) } const inspectResponse = ( response: HttpClientResponse.HttpClientResponse, @@ -1115,11 +1164,11 @@ export const withRateLimiter: { if (options.disableResponseInspection || response.status !== 429) { return Effect.succeed(undefined) } - const retryAfter = parseRetryAfter(clock, getHeader(response.headers, "retry-after")) + const retryAfter = parseRetryAfter(clock, getHeader(response.headers, ...headerNames.retryAfter)) if (retryAfter === undefined) { return Effect.succeed(undefined) } - const delay = parseRateLimitWindow(clock, response.headers) ?? retryAfter + const delay = parseRateLimitWindow(clock, response.headers, headerNames) ?? retryAfter if (adaptive === undefined) { return Effect.succeed(delay) } @@ -1153,7 +1202,7 @@ export const withRateLimiter: { const request = Effect.matchEffect(effect, { onSuccess(response) { return Effect.flatMap(inspectResponse(response, adaptive), (retryAfter) => { - if (response.status !== 429) return Effect.succeed(response) + if (response.status !== 429 || !canRetry) return Effect.succeed(response) return retry(retryAfter) }) }, @@ -1161,7 +1210,7 @@ export const withRateLimiter: { if (isTooManyRequestsHttpClientError(error)) { return Effect.flatMap( inspectResponse(error.reason.response, adaptive), - (retryAfter) => retry(retryAfter) + (retryAfter) => canRetry ? retry(retryAfter) : Effect.fail(error) ) } return Effect.fail(error) @@ -1195,6 +1244,34 @@ export const withRateLimiter: { }) }) +interface RateLimiterHeaderNames { + readonly limit: ReadonlyArray + readonly remaining: ReadonlyArray + readonly reset: ReadonlyArray + readonly resetAfter: ReadonlyArray + readonly retryAfter: ReadonlyArray +} + +const resolveRateLimiterHeaderNames = ( + responseHeaders: WithRateLimiter.Options["responseHeaders"] +): RateLimiterHeaderNames => ({ + limit: responseHeaders?.limit === undefined + ? ["ratelimit-limit", "x-ratelimit-limit"] + : [responseHeaders.limit.toLowerCase()], + remaining: responseHeaders?.remaining === undefined + ? ["ratelimit-remaining", "x-ratelimit-remaining"] + : [responseHeaders.remaining.toLowerCase()], + reset: responseHeaders?.reset === undefined + ? ["ratelimit-reset", "x-ratelimit-reset"] + : [responseHeaders.reset.toLowerCase()], + resetAfter: responseHeaders?.resetAfter === undefined + ? ["ratelimit-reset-after", "x-ratelimit-reset-after"] + : [responseHeaders.resetAfter.toLowerCase()], + retryAfter: responseHeaders?.retryAfter === undefined + ? ["retry-after"] + : [responseHeaders.retryAfter.toLowerCase()] +}) + interface RateLimiterState { readonly limit: number readonly window: Duration.Duration @@ -1205,10 +1282,11 @@ const parseRateLimiterState = ( state: RateLimiterState, clock: Clock, headers: Headers.Headers, - tokens: number + tokens: number, + headerNames: RateLimiterHeaderNames ): RateLimiterState => { - const limit = parseRateLimitLimit(state, headers, tokens) ?? state.limit - const window = parseRateLimitWindow(clock, headers) ?? state.window + const limit = parseRateLimitLimit(state, headers, tokens, headerNames) ?? state.limit + const window = parseRateLimitWindow(clock, headers, headerNames) ?? state.window if (limit === state.limit && Duration.equals(window, state.window)) { return state } @@ -1218,35 +1296,40 @@ const parseRateLimiterState = ( const parseRateLimitLimit = ( state: RateLimiterState, headers: Headers.Headers, - tokens: number + tokens: number, + headerNames: RateLimiterHeaderNames ): number | undefined => { - const raw = getHeader(headers, "ratelimit-limit", "x-ratelimit-limit") + const raw = getHeader(headers, ...headerNames.limit) const value = parseNumberHeader(raw) if (value !== undefined && value > 0) { return value } - const remaining = parseRateLimitRemaining(headers) + const remaining = parseRateLimitRemaining(headers, headerNames) if (remaining === undefined) { return undefined } return state.initial ? remaining + tokens : Math.max(remaining + tokens, state.limit) } -const parseRateLimitRemaining = (headers: Headers.Headers): number | undefined => { - const raw = getHeader(headers, "ratelimit-remaining", "x-ratelimit-remaining") +const parseRateLimitRemaining = ( + headers: Headers.Headers, + headerNames: RateLimiterHeaderNames +): number | undefined => { + const raw = getHeader(headers, ...headerNames.remaining) const value = parseNumberHeader(raw) return value !== undefined && value >= 0 ? value : undefined } const parseRateLimitWindow = ( clock: Clock, - headers: Headers.Headers + headers: Headers.Headers, + headerNames: RateLimiterHeaderNames ): Duration.Duration | undefined => { - const resetAfter = parseResetAfter(getHeader(headers, "ratelimit-reset-after", "x-ratelimit-reset-after")) + const resetAfter = parseResetAfter(getHeader(headers, ...headerNames.resetAfter)) if (resetAfter !== undefined) { return resetAfter } - return parseResetHeader(clock, getHeader(headers, "ratelimit-reset", "x-ratelimit-reset")) + return parseResetHeader(clock, getHeader(headers, ...headerNames.reset)) } const parseRetryAfter = ( @@ -1322,7 +1405,7 @@ const getHeader = (headers: Headers.Headers, ...keys: Array): string | u /** * Performs an additional effect after a successful request. * - * @category mapping & sequencing + * @category sequencing * @since 4.0.0 */ export const tap: { @@ -1344,7 +1427,7 @@ export const tap: { /** * Performs an additional effect after an unsuccessful request. * - * @category mapping & sequencing + * @category sequencing * @since 4.0.0 */ export const tapError: { @@ -1366,7 +1449,7 @@ export const tapError: { /** * Performs an additional effect on the request before sending it. * - * @category mapping & sequencing + * @category sequencing * @since 4.0.0 */ export const tapRequest: { @@ -1466,17 +1549,29 @@ export const followRedirects: { ): Effect.Effect => Effect.flatMap( self.postprocess(Effect.succeed(request)), - (response) => - response.status >= 300 && response.status < 400 && response.headers.location && - redirects < (maxRedirects ?? 10) - ? loop( - HttpClientRequest.setUrl( - request, - new URL(response.headers.location, response.request.url) - ), - redirects + 1 - ) - : Effect.succeed(response) + (response) => { + if ( + response.status < 300 || response.status >= 400 || !response.headers.location || + redirects >= (maxRedirects ?? 10) + ) { + return Effect.succeed(response) + } + const url = new URL(response.headers.location, response.request.url) + let nextRequest = request + if ( + ((response.status === 301 || response.status === 302) && request.method === "POST") || + (response.status === 303 && request.method !== "GET" && request.method !== "HEAD") + ) { + nextRequest = HttpClientRequest.setMethod(nextRequest, "GET") + nextRequest = HttpClientRequest.setBody(nextRequest, HttpBody.empty) + } + if (url.origin !== new URL(response.request.url).origin) { + nextRequest = HttpClientRequest.removeHeader(nextRequest, "authorization") + nextRequest = HttpClientRequest.removeHeader(nextRequest, "proxy-authorization") + nextRequest = HttpClientRequest.removeHeader(nextRequest, "cookie") + } + return loop(HttpClientRequest.setUrl(nextRequest, url), redirects + 1) + } ) return Effect.flatMap(request, (request) => loop(request, 0)) }, @@ -1486,7 +1581,7 @@ export const followRedirects: { /** * Context reference for a predicate that disables client-side tracing for matching outgoing requests. * - * @category references + * @category services * @since 4.0.0 */ export const TracerDisabledWhen = Context.Reference< @@ -1495,10 +1590,22 @@ export const TracerDisabledWhen = Context.Reference< defaultValue: () => constFalse }) +/** + * Context reference for filtering request and response headers added to client spans. + * + * @category services + * @since 4.0.0 + */ +export const TracerHeaderFilter = Context.Reference< + (headerName: string, phase: "request" | "response") => boolean +>("effect/http/HttpClient/TracerHeaderFilter", { + defaultValue: () => constTrue +}) + /** * Context reference that controls whether outgoing client spans are propagated to request headers. * - * @category references + * @category services * @since 4.0.0 */ export const TracerPropagationEnabled = Context.Reference("effect/HttpClient/TracerPropagationEnabled", { @@ -1508,7 +1615,7 @@ export const TracerPropagationEnabled = Context.Reference("effect/HttpC /** * Context reference for generating the span name used for outgoing client request spans. * - * @category references + * @category services * @since 4.0.0 */ export const SpanNameGenerator = Context.Reference< diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClientError.ts b/.context/effect/packages/effect/src/unstable/http/HttpClientError.ts index e4909af4f..52135c5da 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClientError.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClientError.ts @@ -299,7 +299,7 @@ export type HttpClientErrorReason = RequestError | ResponseError * @category schemas * @since 4.0.0 */ -export class HttpClientErrorSchema extends Schema.ErrorClass(TypeId)({ +export class HttpClientErrorSchema extends Schema.Error(TypeId)({ _tag: Schema.tag("HttpError"), kind: Schema.Literals( [ diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts b/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts index feabe9b1e..46751c3b3 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClientRequest.ts @@ -28,6 +28,7 @@ import * as Stream from "../../Stream.ts" import * as Headers from "./Headers.ts" import * as HttpBody from "./HttpBody.ts" import { hasBody, type HttpMethod } from "./HttpMethod.ts" +import * as bodyInternal from "./internal/httpBody.ts" import * as Url from "./Url.ts" import * as UrlParams from "./UrlParams.ts" @@ -327,6 +328,39 @@ export const setHeaders: { self.body )) +/** + * Transforms the request headers with the provided function, returning a new request. + * + * @category combinators + * @since 4.0.0 + */ +export const updateHeaders: { + (f: (headers: Headers.Headers) => Headers.Headers): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, f: (headers: Headers.Headers) => Headers.Headers): HttpClientRequest +} = dual(2, (self: HttpClientRequest, f: (headers: Headers.Headers) => Headers.Headers): HttpClientRequest => + makeWith( + self.method, + self.url, + self.urlParams, + self.hash, + f(self.headers), + self.body + )) + +/** + * Removes a single request header by name, returning a new request. + * + * @category combinators + * @since 4.0.0 + */ +export const removeHeader: { + (key: string): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, key: string): HttpClientRequest +} = dual( + 2, + (self: HttpClientRequest, key: string): HttpClientRequest => updateHeaders(self, Headers.remove(key)) +) + /** * Sets the `Authorization` header using HTTP Basic authentication credentials. * @@ -616,23 +650,12 @@ export const setBody: { (body: HttpBody.HttpBody): (self: HttpClientRequest) => HttpClientRequest (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest } = dual(2, (self: HttpClientRequest, body: HttpBody.HttpBody): HttpClientRequest => { - let headers = self.headers - if (body._tag === "Empty" || body._tag === "FormData") { - headers = Headers.remove(Headers.remove(headers, "Content-Type"), "Content-length") - } else { - if (body.contentType) { - headers = Headers.set(headers, "content-type", body.contentType) - } - if (body.contentLength !== undefined) { - headers = Headers.set(headers, "content-length", body.contentLength.toString()) - } - } return makeWith( self.method, self.url, self.urlParams, self.hash, - headers, + bodyInternal.updateHeaders(self.headers, body), body ) }) diff --git a/.context/effect/packages/effect/src/unstable/http/HttpClientResponse.ts b/.context/effect/packages/effect/src/unstable/http/HttpClientResponse.ts index 41eb60883..f070fe645 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpClientResponse.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpClientResponse.ts @@ -201,7 +201,7 @@ export const matchStatus: { /** * Succeeds with the response when its status satisfies the predicate, otherwise fails with `HttpClientError`. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterStatus: { @@ -228,7 +228,7 @@ export const filterStatus: { /** * Succeeds with the response only when its status is in the 2xx range, otherwise fails with `HttpClientError`. * - * @category filters + * @category filtering * @since 4.0.0 */ export const filterStatusOk = (self: HttpClientResponse): Effect.Effect => @@ -333,22 +333,7 @@ class WebHttpClientResponse extends Inspectable.Class implements HttpClientRespo private textBody?: Effect.Effect get text(): Effect.Effect { - if (this.textBody) { - return this.textBody - } - this.textBody = Effect.tryPromise({ - try: () => this.source.text(), - catch: (cause) => - new Error.HttpClientError({ - reason: new Error.DecodeError({ - request: this.request, - response: this, - cause - }) - }) - }).pipe(Effect.cached, Effect.runSync) - this.arrayBufferBody = Effect.map(this.textBody, (_) => new TextEncoder().encode(_).buffer) - return this.textBody + return this.textBody ??= Effect.map(this.arrayBuffer, (_) => new TextDecoder().decode(_)) } get urlParamsBody(): Effect.Effect { @@ -397,7 +382,6 @@ class WebHttpClientResponse extends Inspectable.Class implements HttpClientRespo }) }) }).pipe(Effect.cached, Effect.runSync) - this.textBody = Effect.map(this.arrayBufferBody, (_) => new TextDecoder().decode(_)) return this.arrayBufferBody } diff --git a/.context/effect/packages/effect/src/unstable/http/HttpEffect.ts b/.context/effect/packages/effect/src/unstable/http/HttpEffect.ts index e4bad7d8f..872493ea5 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpEffect.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpEffect.ts @@ -172,7 +172,7 @@ const scoped = (effect: Effect.Effect) => /** * Function run with the current request and response just before the response is sent, allowing the response to be replaced or failing with `HttpServerError`. * - * @category Pre-response handlers + * @category handlers * @since 4.0.0 */ export type PreResponseHandler = ( @@ -183,7 +183,7 @@ export type PreResponseHandler = ( /** * Registers an additional pre-response handler for the current HTTP server request. * - * @category fiber refs + * @category handlers * @since 4.0.0 */ export const appendPreResponseHandler = (handler: PreResponseHandler): Effect.Effect => @@ -195,7 +195,7 @@ export const appendPreResponseHandler = (handler: PreResponseHandler): Effect.Ef /** * Registers a pre-response handler for the supplied HTTP server request. * - * @category fiber refs + * @category unsafe * @since 4.0.0 */ export const appendPreResponseHandlerUnsafe: ( @@ -206,7 +206,7 @@ export const appendPreResponseHandlerUnsafe: ( /** * Runs an effect after registering a pre-response handler for the current HTTP server request. * - * @category fiber refs + * @category handlers * @since 4.0.0 */ export const withPreResponseHandler: { diff --git a/.context/effect/packages/effect/src/unstable/http/HttpMethod.ts b/.context/effect/packages/effect/src/unstable/http/HttpMethod.ts index b491a5271..0b87104ec 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpMethod.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpMethod.ts @@ -51,7 +51,7 @@ export declare namespace HttpMethod { /** * Returns `true` when a method can carry a request body and narrows it to `HttpMethod.WithBody`. * - * @category predicates + * @category guards * @since 4.0.0 */ export const hasBody = (method: HttpMethod): method is HttpMethod.WithBody => @@ -107,18 +107,15 @@ export const allShort = [ * * **Example** (Checking HTTP method values) * - * ```ts + * ```ts import.meta.vitest * import { HttpMethod } from "effect/unstable/http" * - * console.log(HttpMethod.isHttpMethod("GET")) - * // true - * console.log(HttpMethod.isHttpMethod("get")) - * // false - * console.log(HttpMethod.isHttpMethod(1)) - * // false + * HttpMethod.isHttpMethod("GET") // => true + * HttpMethod.isHttpMethod("get") // => false + * HttpMethod.isHttpMethod(1) // => false * ``` * - * @category refinements + * @category guards * @since 4.0.0 */ export const isHttpMethod = (u: unknown): u is HttpMethod => all.has(u as HttpMethod) diff --git a/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts b/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts index 4d09d91a2..e30ef3f1c 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpMiddleware.ts @@ -23,12 +23,15 @@ import type { ReadonlyRecord } from "../../Record.ts" import { TracerEnabled } from "../../References.ts" import { ParentSpan } from "../../Tracer.ts" import * as Headers from "./Headers.ts" +import type { CompressionAlgorithm } from "./HttpPlatform.ts" +import { HttpPlatform } from "./HttpPlatform.ts" import { causeResponseStripped } from "./HttpServerError.ts" import { HttpServerRequest } from "./HttpServerRequest.ts" import * as Request from "./HttpServerRequest.ts" import * as Response from "./HttpServerResponse.ts" import type { HttpServerResponse } from "./HttpServerResponse.ts" import * as TraceContext from "./HttpTraceContext.ts" +import * as compressionInternal from "./internal/compression.ts" import { appendPreResponseHandlerUnsafe } from "./internal/preResponseHandler.ts" /** @@ -84,7 +87,7 @@ const stripSearchAndHash = (url: string): string => { /** * Runs an effect with HTTP response logging disabled for the current server request. * - * @category Logger + * @category logging * @since 4.0.0 */ export const withLoggerDisabled = (self: Effect.Effect): Effect.Effect => @@ -97,7 +100,7 @@ export const withLoggerDisabled = (self: Effect.Effect): Effec /** * Context reference for a predicate that disables server-side tracing for matching requests. * - * @category Tracer + * @category services * @since 4.0.0 */ export const TracerDisabledWhen = Context.Reference>( @@ -108,7 +111,7 @@ export const TracerDisabledWhen = Context.Reference /** * Creates a layer that disables server-side tracing for requests whose URL exactly matches one of the supplied URLs. * - * @category Tracer + * @category layers * @since 4.0.0 */ export const layerTracerDisabledForUrls = ( @@ -118,7 +121,7 @@ export const layerTracerDisabledForUrls = ( /** * Context reference for generating server span names from HTTP server requests. * - * @category Tracer + * @category services * @since 4.0.0 */ export const SpanNameGenerator = Context.Reference<(request: HttpServerRequest) => string>( @@ -129,7 +132,7 @@ export const SpanNameGenerator = Context.Reference<(request: HttpServerRequest) /** * Middleware that logs sent HTTP responses with request method, request URL, and response status annotations. * - * @category Logger + * @category logging * @since 4.0.0 */ export const logger: ( @@ -170,7 +173,7 @@ export const logger: ( /** * Middleware that creates a server trace span for each request and records request and response HTTP attributes. * - * @category Tracer + * @category tracing * @since 4.0.0 */ export const tracer: ( @@ -193,32 +196,6 @@ export const tracer: ( fiber.setContext(prevServices) const endTime = fiber.getRef(Clock).currentTimeNanosUnsafe() fiber.currentDispatcher.scheduleTask(() => { - const url = Request.toURL(request) - if (Option.isSome(url) && (url.value.username !== "" || url.value.password !== "")) { - url.value.username = "REDACTED" - url.value.password = "REDACTED" - } - const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) - const requestHeaders = Headers.redact(request.headers, redactedHeaderNames) - span.attribute("http.request.method", request.method) - if (Option.isSome(url)) { - span.attribute("url.full", url.value.toString()) - span.attribute("url.path", url.value.pathname) - const query = url.value.search.slice(1) - if (query !== "") { - span.attribute("url.query", url.value.search.slice(1)) - } - span.attribute("url.scheme", url.value.protocol.slice(0, -1)) - } - if (request.headers["user-agent"] !== undefined) { - span.attribute("user_agent.original", request.headers["user-agent"]) - } - for (const name in requestHeaders) { - span.attribute(`http.request.header.${name}`, String(requestHeaders[name])) - } - if (Option.isSome(request.remoteAddress)) { - span.attribute("client.address", request.remoteAddress.value) - } let response: HttpServerResponse let spanExit = exit if (Exit.isFailure(exit)) { @@ -228,10 +205,38 @@ export const tracer: ( } else { response = exit.value } - span.attribute("http.response.status_code", response.status) - const responseHeaders = Headers.redact(response.headers, redactedHeaderNames) - for (const name in responseHeaders) { - span.attribute(`http.response.header.${name}`, String(responseHeaders[name])) + if (span.sampled) { + const url = Request.toURL(request) + if (Option.isSome(url) && (url.value.username !== "" || url.value.password !== "")) { + url.value.username = "REDACTED" + url.value.password = "REDACTED" + } + const redactedHeaderNames = fiber.getRef(Headers.CurrentRedactedNames) + const requestHeaders = Headers.redact(request.headers, redactedHeaderNames) + span.attribute("http.request.method", request.method) + if (Option.isSome(url)) { + span.attribute("url.full", url.value.toString()) + span.attribute("url.path", url.value.pathname) + const query = url.value.search.slice(1) + if (query !== "") { + span.attribute("url.query", url.value.search.slice(1)) + } + span.attribute("url.scheme", url.value.protocol.slice(0, -1)) + } + if (request.headers["user-agent"] !== undefined) { + span.attribute("user_agent.original", request.headers["user-agent"]) + } + for (const name in requestHeaders) { + span.attribute(`http.request.header.${name}`, String(requestHeaders[name])) + } + if (Option.isSome(request.remoteAddress)) { + span.attribute("client.address", request.remoteAddress.value) + } + span.attribute("http.response.status_code", response.status) + const responseHeaders = Headers.redact(response.headers, redactedHeaderNames) + for (const name in responseHeaders) { + span.attribute(`http.response.header.${name}`, String(responseHeaders[name])) + } } span.end(endTime, spanExit) }, 0) @@ -243,7 +248,7 @@ export const tracer: ( /** * Middleware that trusts `X-Forwarded-Host` and `X-Forwarded-For`, updating the request host header and remote address. * - * @category Proxying + * @category proxying * @since 4.0.0 */ export const xForwardedHeaders = make((httpApp) => @@ -263,7 +268,7 @@ export const xForwardedHeaders = make((httpApp) => /** * Middleware that parses the current request URL's search parameters and provides them as `ParsedSearchParams`. * - * @category search params + * @category parsing * @since 4.0.0 */ export const searchParamsParser = ( @@ -283,7 +288,7 @@ export const searchParamsParser = ( /** * Middleware that handles CORS preflight requests and adds configured CORS headers to HTTP responses. * - * @category CORS + * @category middleware * @since 4.0.0 */ export const cors = (options?: { @@ -401,3 +406,161 @@ export const cors = (options?: { return httpApp }) } + +/** + * Middleware that compresses HTTP response bodies based on the request's + * `Accept-Encoding` header. + * + * **Details** + * + * Content negotiation follows RFC 9110: the first algorithm in server + * preference order that the client accepts with a positive q-value and the + * platform supports is used. When no algorithm is acceptable the response is + * sent uncompressed. + * + * The body transform is performed by the `HttpPlatform` service. + * + * Responses are skipped when the status is 1xx, 204, 206, or 304, when a + * `Content-Encoding` is already present, when `Cache-Control: no-transform` + * is set, when the content type is absent or not compressible, when a known + * body length is below `minSize`, or when the body is empty or `FormData`. A + * response carrying `Content-Encoding: identity` opts out of compression and + * has the header stripped before sending. + * + * `Vary: Accept-Encoding` is set on every response that was eligible by + * status and content type, including ones skipped by negotiation or + * `minSize`. + * + * Do not combine this middleware with Deno's automatic response compression. + * On edge runtimes that already apply automatic compression, the middleware + * is unnecessary. Platforms backed by Web `CompressionStream` also cannot + * explicitly flush each input chunk, so incremental delivery depends on the + * runtime's implementation. + * + * **Security** + * + * Compression can expose secrets through BREACH-style attacks when one + * response contains both secret data and attacker-controlled input and an + * attacker can observe the compressed response length. For affected routes, + * disable compression with `Content-Encoding: identity` or + * `Cache-Control: no-transform`, or use `compressible` to restrict which + * response content types can be compressed. + * + * @category compression + * @since 4.0.0 + */ +export const compression = ( + options?: { + /** + * Server preference order. Negotiation picks the first accepted algorithm + * supported by the platform. Defaults to `["br", "gzip", "deflate"]`; + * `zstd` must be explicitly opted into. + */ + readonly algorithms?: ReadonlyArray | undefined + /** + * Minimum body size in bytes when the length is known. Unknown-length bodies + * are always compressed. Defaults to `1024`. + */ + readonly minSize?: number | undefined + /** Replaces the default content-type predicate. */ + readonly compressible?: ((contentType: string) => boolean) | undefined + /** Per-algorithm levels. Platforms without a level knob ignore them. */ + readonly levels?: { + readonly gzip?: number | undefined + readonly deflate?: number | undefined + readonly br?: number | undefined + readonly zstd?: number | undefined + } | undefined + } | undefined +): ( + httpApp: Effect.Effect +) => Effect.Effect => { + const preferred = options?.algorithms ?? defaultAlgorithms + const minSize = options?.minSize ?? 1024 + const compressible = options?.compressible ?? compressionInternal.defaultCompressible + const levels = { ...defaultLevels, ...options?.levels } + const levelOptions: Record = { + gzip: { level: levels.gzip }, + deflate: { level: levels.deflate }, + br: { level: levels.br }, + zstd: { level: levels.zstd } + } + const transform = ( + compression: HttpPlatform["Service"]["compression"], + acceptEncoding: string | undefined, + response: HttpServerResponse + ): Effect.Effect => { + if ( + response.status < 200 || + response.status === 204 || + response.status === 206 || + response.status === 304 + ) { + return Effect.succeed(response) + } + const currentEncoding = response.headers["content-encoding"] + if (currentEncoding !== undefined) { + return Effect.succeed( + currentEncoding.trim().toLowerCase() === "identity" + ? Response.removeHeader(response, "content-encoding") + : response + ) + } + const body = response.body + if (body._tag === "Empty" || body._tag === "FormData") { + return Effect.succeed(response) + } + const cacheControl = response.headers["cache-control"] + if (cacheControl !== undefined && noTransformRegex.test(cacheControl)) { + return Effect.succeed(response) + } + const contentType = response.headers["content-type"] ?? body.contentType + if (contentType === undefined || !compressible(contentType)) { + return Effect.succeed(response) + } + const algorithm = compressionInternal.negotiate(acceptEncoding, preferred, compression.algorithms) + if (algorithm === undefined) { + return Effect.succeed(withVary(response)) + } + const contentLength = body.contentLength ?? contentLengthHeader(response.headers) + if (contentLength !== undefined && contentLength < minSize) { + return Effect.succeed(withVary(response)) + } + return compression.compressResponse(response, algorithm, levelOptions[algorithm]) + } + return ( + httpApp: Effect.Effect + ): Effect.Effect => + Effect.withFiber((fiber) => { + const request = Context.getUnsafe(fiber.context, HttpServerRequest) + const compression = Context.getUnsafe(fiber.context, HttpPlatform).compression + appendPreResponseHandlerUnsafe(request, (request, response) => + transform(compression, request.headers["accept-encoding"], response)) + return httpApp + }) +} + +const withVary = (response: HttpServerResponse): HttpServerResponse => { + const vary = compressionInternal.varyAcceptEncoding(response.headers) + return vary === undefined ? response : Response.setHeader(response, "vary", vary) +} + +const defaultAlgorithms: ReadonlyArray = ["br", "gzip", "deflate"] + +const defaultLevels = { + gzip: 6, + deflate: 6, + br: 4, + zstd: 3 +} as const + +const noTransformRegex = /(?:^|[\s,])no-transform(?:$|[\s,;])/i + +const contentLengthHeader = (headers: Headers.Headers): number | undefined => { + const value = headers["content-length"] + if (value === undefined) { + return undefined + } + const parsed = Number(value) + return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined +} diff --git a/.context/effect/packages/effect/src/unstable/http/HttpPlatform.ts b/.context/effect/packages/effect/src/unstable/http/HttpPlatform.ts index d4c8b2d7a..7c687888f 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpPlatform.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpPlatform.ts @@ -21,6 +21,7 @@ import * as Etag from "./Etag.ts" import * as Headers from "./Headers.ts" import type * as Body from "./HttpBody.ts" import * as Response from "./HttpServerResponse.ts" +import * as internal from "./internal/compression.ts" /** * Service for platform-specific HTTP response helpers, including file-backed server responses. @@ -29,6 +30,8 @@ import * as Response from "./HttpServerResponse.ts" * @since 4.0.0 */ export class HttpPlatform extends Context.Service make({ + platform: "web", + compression: internal.compressionWeb, fileResponse(path, status, statusText, headers, start, end, contentLength) { return Response.stream( fs.stream(path, { @@ -152,14 +161,128 @@ export const layer = Layer.effect(HttpPlatform)( { contentLength, headers, status, statusText } ) }, - fileWebResponse(file, status, statusText, headers, _options) { - return Response.stream( - Stream.fromReadableStream({ + fileWebResponse(file, status, statusText, headers, options) { + const offset = Number(options?.offset ?? 0) + const bytesToRead = options?.bytesToRead !== undefined ? Number(options.bytesToRead) : undefined + const chunkSize = options?.chunkSize !== undefined ? Math.max(1, Number(options.chunkSize)) : Infinity + const end = offset + (bytesToRead ?? Infinity) + const stream = end <= offset + ? Stream.empty + : Stream.fromReadableStream({ evaluate: () => file.stream() as ReadableStream, onError: identity - }), - { headers, status, statusText } - ) + }).pipe( + Stream.mapAccum( + () => 0, + (position, bytes) => { + const next = position + bytes.length + const start = Math.min(Math.max(offset - position, 0), bytes.length) + const stop = Math.min(Math.max(end - position, 0), bytes.length) + const chunks: Array<{ readonly bytes: Uint8Array; readonly done: boolean }> = [] + for (let index = start; index < stop; index += chunkSize) { + chunks.push({ + bytes: bytes.subarray(index, Math.min(index + chunkSize, stop)), + done: next >= end && index + chunkSize >= stop + }) + } + return [next, chunks] + } + ), + Stream.takeUntil((chunk) => chunk.done), + Stream.map((chunk) => chunk.bytes) + ) + return Response.stream(stream, { + contentLength: bytesToRead ?? file.size - offset, + headers, + status, + statusText + }) } })) ).pipe(Layer.provide(Etag.layerWeak)) + +/** + * Content codings that HTTP response compression can apply. + * + * @category compression + * @since 4.0.0 + */ +export type CompressionAlgorithm = "gzip" | "deflate" | "br" | "zstd" + +/** + * Options passed to a platform when compressing a response body. + * + * **Details** + * + * The `level` scale depends on the algorithm. Platforms without a level knob, + * such as the Web `CompressionStream` implementation, ignore it. + * + * @category compression + * @since 4.0.0 + */ +export interface CompressionOptions { + readonly level?: number | undefined +} + +/** + * Platform primitive for HTTP response compression. + * + * **Details** + * + * `algorithms` advertises what the platform can encode; content negotiation + * happens in the shared `HttpMiddleware.compression` middleware. + * + * `compressResponse` is only called when compression is definitely happening — + * all skip logic runs in the shared middleware first. The platform owns the + * body transform and removes `Content-Length` when the compressed size is not + * known in advance. The `make` wrapper owns the `Content-Encoding` and `Vary` + * headers. + * + * @category compression + * @since 4.0.0 + */ +export interface Compression { + readonly algorithms: ReadonlySet + readonly compressResponse: ( + response: Response.HttpServerResponse, + algorithm: CompressionAlgorithm, + options?: CompressionOptions | undefined + ) => Effect.Effect +} + +/** + * Creates a compression body transform backed by the Web `CompressionStream` + * API, for use with `makeCompressionWeb`. + * + * **Details** + * + * The format string is passed through to the runtime, so runtime-specific + * formats such as Bun's `"brotli"` and `"zstd"` are usable. `CompressionStream` + * has no compression level knob, so `CompressionOptions.level` does not apply. + * + * @category compression + * @since 4.0.0 + */ +export const compressionTransformWeb: ( + format: string +) => (stream: ReadableStream) => ReadableStream = internal.compressionTransformWeb + +/** + * Creates a `Compression` implementation from Web `ReadableStream` + * transforms. + * + * **Details** + * + * All supported bodies are transformed as streams. The `Content-Length` + * header is dropped in every case. + * + * @category compression + * @since 4.0.0 + */ +export const makeCompressionWeb: (options: { + readonly algorithms: Iterable + readonly transform: ( + algorithm: CompressionAlgorithm, + options?: CompressionOptions | undefined + ) => (stream: ReadableStream) => ReadableStream +}) => Compression = internal.makeCompressionWeb diff --git a/.context/effect/packages/effect/src/unstable/http/HttpRouter.ts b/.context/effect/packages/effect/src/unstable/http/HttpRouter.ts index 6181e2fcd..ea79e2429 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpRouter.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpRouter.ts @@ -41,7 +41,7 @@ const TypeId = "~effect/http/HttpRouter" * and expose the registered routes as an Effect that handles the current server * request. * - * @category HttpRouter + * @category services * @since 4.0.0 */ export interface HttpRouter { @@ -97,7 +97,7 @@ export interface HttpRouter { * Route and middleware layers require this service to register themselves with * the router. * - * @category HttpRouter + * @category services * @since 4.0.0 */ export const HttpRouter: Context.Service = Context.Service( @@ -112,7 +112,7 @@ export const HttpRouter: Context.Service = Context.Servi * The returned router accepts route and middleware registrations and later routes * the current `HttpServerRequest` to the matching `HttpServerResponse`. * - * @category HttpRouter + * @category constructors * @since 4.0.0 */ export const make = Effect.gen(function*() { @@ -190,8 +190,8 @@ export const make = Effect.gen(function*() { }), asHttpEffect() { let handler = Effect.withFiber((fiber) => { - const contextMap = new Map(fiber.context.mapUnsafe) - const request = contextMap.get(HttpServerRequest.HttpServerRequest.key) as HttpServerRequest.HttpServerRequest + let context = fiber.context + const request = Context.getUnsafe(context, HttpServerRequest.HttpServerRequest) let result = router.find(request.method, request.url) if (result === undefined && request.method === "HEAD") { result = router.find("GET", request.url) @@ -205,26 +205,30 @@ export const make = Effect.gen(function*() { } const route = result.handler if (Option.isSome(route.prefix)) { - contextMap.set(HttpServerRequest.HttpServerRequest.key, sliceRequestUrl(request, route.prefix.value)) + context = Context.add( + context, + HttpServerRequest.HttpServerRequest, + sliceRequestUrl(request, route.prefix.value) + ) } - contextMap.set(HttpServerRequest.ParsedSearchParams.key, result.searchParams) - contextMap.set(RouteContext.key, { + context = Context.add(context, HttpServerRequest.ParsedSearchParams, result.searchParams) + context = Context.add(context, RouteContext, { route, params: result.params }) - const span = contextMap.get(Tracer.ParentSpan.key) as Tracer.Span | undefined + const span = Context.getOrUndefined(context, Tracer.ParentSpan) if (span && span._tag === "Span") { span.attribute("http.route", route.path) } - return Effect.provideContext( + return Effect.updateContext( (route.uninterruptible ? route.handler : Effect.interruptible(route.handler)) as Effect.Effect< HttpServerResponse.HttpServerResponse, unknown >, - Context.makeUnsafe(contextMap) + () => context ) }) if (middleware.size === 0) return handler @@ -249,7 +253,7 @@ function sliceRequestUrl(request: HttpServerRequest.HttpServerRequest, prefix: s * The value is passed to the route matcher when an `HttpRouter` is created and * defaults to an empty configuration. * - * @category configuration + * @category services * @since 4.0.0 */ export const RouterConfig = Context.Reference>( @@ -446,18 +450,29 @@ export const schemaPathParams = + * router.add("GET", "/health", HttpServerResponse.text("ready")) + * ) + * + * const program = Effect.acquireUseRelease( + * Effect.sync(() => HttpRouter.toWebHandler(Routes, { disableLogger: true })), + * ({ handler }) => + * Effect.gen(function*() { + * const response = yield* Effect.promise(() => handler(new Request("http://localhost/health"))) + * const body = yield* Effect.promise(() => response.text()) + * return body + * }), + * ({ dispose }) => Effect.promise(dispose) + * ) * - * // then use `yield* router.add(...)` to add a route - * })) + * await Effect.runPromise(program) // => "ready" * ``` * - * @category HttpRouter + * @category layers * @since 4.0.0 */ export const use = ( @@ -469,18 +484,19 @@ export const use = ( * * **Example** (Adding a GET route) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Layer } from "effect" * import { HttpRouter, HttpServerResponse } from "effect/unstable/http" * * const Route = HttpRouter.add( * "GET", * "/hello", - * Effect.succeed(HttpServerResponse.text("Hello, World!")) + * HttpServerResponse.text("Hello, World!") * ) + * Layer.isLayer(Route) // => true * ``` * - * @category HttpRouter + * @category layers * @since 4.0.0 */ export const add = ( @@ -501,20 +517,21 @@ export const add = ( * * **Example** (Adding multiple routes) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Layer } from "effect" * import { HttpRouter, HttpServerResponse } from "effect/unstable/http" * * const Routes = HttpRouter.addAll([ * HttpRouter.route( * "GET", * "/hello", - * Effect.succeed(HttpServerResponse.text("Hello, World!")) + * HttpServerResponse.text("Hello, World!") * ) * ]) + * Layer.isLayer(Routes) // => true * ``` * - * @category HttpRouter + * @category layers * @since 4.0.0 */ export const addAll = >, EX = never, RX = never>( @@ -542,7 +559,7 @@ export const addAll = >, EX = never /** * Layer that provides a newly constructed `HttpRouter`. * - * @category HttpRouter + * @category layers * @since 4.0.0 */ export const layer: Layer.Layer = Layer.effect(HttpRouter)(make) @@ -557,7 +574,7 @@ export const layer: Layer.Layer = Layer.effect(HttpRouter)(make) * `Scope`; route request markers are converted into the ordinary requirements of * the returned handler. * - * @category HttpRouter + * @category converting * @since 4.0.0 */ export const toHttpEffect = ( @@ -565,7 +582,7 @@ export const toHttpEffect = ( ): Effect.Effect< Effect.Effect< HttpServerResponse.HttpServerResponse, - Request.Only<"Error", R> | Request.Only<"GlobalRequires", R> | HttpServerError.HttpServerError, + Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.HttpServerError, Scope.Scope | HttpServerRequest.HttpServerRequest | Request.Only<"Requires", R> | Request.Only<"GlobalRequires", R> >, Request.Without, @@ -588,7 +605,7 @@ const RouteTypeId = "~effect/http/HttpRouter/Route" * A route pairs an HTTP method and path pattern with a response handler, plus * metadata used for prefix handling and interruptibility. * - * @category Route + * @category routes * @since 4.0.0 */ export interface Route { @@ -610,7 +627,7 @@ export declare namespace Route { /** * Extracts the error type produced by a `Route` handler. * - * @category Route + * @category routes * @since 4.0.0 */ export type Error> = R extends Route ? E : never @@ -618,7 +635,7 @@ export declare namespace Route { /** * Extracts the context requirements of a `Route` handler. * - * @category Route + * @category routes * @since 4.0.0 */ export type Context> = T extends Route ? R : never @@ -646,7 +663,7 @@ const makeRoute = (options: { * function from the current request to a response effect. Set `uninterruptible` to * prevent the route handler from being made interruptible while it runs. * - * @category Route + * @category routes * @since 4.0.0 */ export const route = ( @@ -676,7 +693,7 @@ export const route = ( * Path pattern accepted by the router. Routes must use an absolute path * beginning with `/` or the wildcard `*`. * - * @category PathInput + * @category models * @since 4.0.0 */ export type PathInput = `/${string}` | "*" @@ -693,7 +710,7 @@ const removeTrailingSlash = ( * Trailing slashes are removed from the prefix; `/` becomes the prefix itself and * `*` becomes a wildcard route under the prefix. * - * @category PathInput + * @category transforming * @since 4.0.0 */ export const prefixPath: { @@ -715,7 +732,7 @@ export const prefixPath: { * request, the matched prefix can be removed from the request URL seen by the * handler. * - * @category Route + * @category routes * @since 4.0.0 */ export const prefixRoute: { @@ -735,7 +752,7 @@ export const prefixRoute: { * Represents a request-level dependency, that needs to be provided by * middleware. * - * @category Request types + * @category utility types * @since 4.0.0 */ export interface Request { @@ -754,7 +771,7 @@ export declare namespace Request { /** * Wraps a type in a request-level marker of the supplied kind. * - * @category Request types + * @category utility types * @since 4.0.0 */ export type From = R extends infer T ? Request : never @@ -763,7 +780,7 @@ export declare namespace Request { * Extracts the payload types from request-level markers that have the supplied * kind. * - * @category Request types + * @category utility types * @since 4.0.0 */ export type Only = A extends Request ? T : never @@ -772,7 +789,7 @@ export declare namespace Request { * Removes request-level markers from a union, leaving only ordinary requirement * or error types. * - * @category Request types + * @category utility types * @since 4.0.0 */ export type Without = A extends Request ? never : A @@ -782,7 +799,7 @@ export declare namespace Request { * Services provided by the HTTP router, which are available in the * request context. * - * @category Request types + * @category utility types * @since 4.0.0 */ export type Provided = @@ -794,7 +811,7 @@ export type Provided = /** * Services provided to global middleware. * - * @category Request types + * @category utility types * @since 4.0.0 */ export type GlobalProvided = @@ -828,7 +845,8 @@ export interface Middleware< readonly [MiddlewareTypeId]: Config readonly layer: [Config["requires"]] extends [never] ? Layer.Layer< - Request.From<"Requires", Config["provides"]>, + | Request.From<"Requires", Config["provides"]> + | Request.From<"Error", Config["handles"]>, Config["layerError"], | Config["layerRequires"] | Request.From<"Requires", Config["requires"]> @@ -867,50 +885,35 @@ export interface Middleware< * * **Example** (Applying route and global middleware) * - * ```ts - * import { Context, Effect, Layer } from "effect" - * import { HttpMiddleware, HttpRouter, HttpServerResponse } from "effect/unstable/http" - * - * // Here we are defining a CORS middleware - * const CorsMiddleware = HttpRouter.middleware(HttpMiddleware.cors()).layer - * // You can also use HttpRouter.cors() to create a CORS middleware - * - * class CurrentSession extends Context.Service()("CurrentSession") {} - * - * // You can create middleware that provides a service to the HTTP requests. - * const SessionMiddleware = HttpRouter.middleware<{ - * provides: CurrentSession - * }>()( - * Effect.gen(function*() { - * yield* Effect.log("SessionMiddleware initialized") - * - * return (httpEffect) => - * Effect.provideService(httpEffect, CurrentSession, { - * token: "dummy-token" - * }) - * }) + * ```ts import.meta.vitest + * import { Effect, Layer } from "effect" + * import { HttpRouter, HttpServerResponse } from "effect/unstable/http" + * + * const RouteMiddleware = HttpRouter.middleware((httpEffect) => + * Effect.map(httpEffect, HttpServerResponse.setHeader("x-route", "route")) * ).layer * - * Effect.gen(function*() { - * const router = yield* HttpRouter.HttpRouter - * yield* router.add( - * "GET", - * "/hello", + * const GlobalMiddleware = HttpRouter.middleware( + * (httpEffect) => Effect.map(httpEffect, HttpServerResponse.setHeader("x-global", "global")), + * { global: true } + * ) + * + * const Routes = HttpRouter.add("GET", "/hello", HttpServerResponse.text("Hello")).pipe( + * Layer.provide(RouteMiddleware) + * ) + * const App = Layer.mergeAll(Routes, GlobalMiddleware) + * + * const program = Effect.acquireUseRelease( + * Effect.sync(() => HttpRouter.toWebHandler(App, { disableLogger: true })), + * ({ handler }) => * 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]) + * const response = yield* Effect.promise(() => handler(new Request("http://localhost/hello"))) + * return [response.headers.get("x-route"), response.headers.get("x-global")] + * }), + * ({ dispose }) => Effect.promise(dispose) * ) + * + * await Effect.runPromise(program) // => ["route", "global"] * ``` * * @category middleware @@ -1147,7 +1150,7 @@ export declare namespace middleware { /** * Middleware that applies CORS headers to the HTTP response. * - * @category middleware + * @category layers * @since 4.0.0 */ export const cors = ( @@ -1166,21 +1169,22 @@ export const cors = ( * * **Example** (Disabling route logging) * - * ```ts - * import { Effect, Layer } from "effect" + * ```ts import.meta.vitest + * import { Layer } from "effect" * import { HttpRouter, HttpServerResponse } from "effect/unstable/http" * * const Route = HttpRouter.add( * "GET", * "/hello", - * Effect.succeed(HttpServerResponse.text("Hello, World!")) + * HttpServerResponse.text("Hello, World!") * ).pipe( * // disable the logger for this route * Layer.provide(HttpRouter.disableLogger) * ) + * Layer.isLayer(Route) // => true * ``` * - * @category middleware + * @category layers * @since 4.0.0 */ export const disableLogger: Layer.Layer = middleware(HttpMiddleware.withLoggerDisabled).layer @@ -1188,7 +1192,7 @@ export const disableLogger: Layer.Layer = middleware(HttpMiddleware.withL /** * Provides request-level dependencies to some routes. * - * @category middleware + * @category layers * @since 4.0.0 */ export const provideRequest = @@ -1213,7 +1217,7 @@ export const provideRequest = /** * Runs the provided application layer as an HTTP server. * - * @category server + * @category layers * @since 4.0.0 */ export const serve = | Request.Only<"GlobalRequires", R>>( @@ -1277,7 +1281,7 @@ export const serve = | Request.On * Web `Response` values and a `dispose` function for releasing the layer * resources. * - * @category server + * @category converting * @since 4.0.0 */ export const toWebHandler = < @@ -1290,7 +1294,8 @@ export const toWebHandler = < | Request<"Error", any> | Request<"GlobalError", any>, HE, - HR = Exclude | Request.Only<"GlobalRequires", R>, A> + HR = Exclude | Request.Only<"GlobalRequires", R>, A>, + ReqR = Exclude >( appLayer: Layer.Layer, options?: { @@ -1319,11 +1324,11 @@ export const toWebHandler = < ) => Effect.Effect } ): { - readonly handler: [Exclude] extends [never] + readonly handler: [ReqR] extends [never] ? ((request: globalThis.Request, context?: Context.Context | undefined) => Promise) : (( request: globalThis.Request, - context: Context.Context> + context: Context.Context ) => Promise) readonly dispose: () => Promise } => { diff --git a/.context/effect/packages/effect/src/unstable/http/HttpServer.ts b/.context/effect/packages/effect/src/unstable/http/HttpServer.ts index dc40fb9d8..f8de5dbb3 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpServer.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpServer.ts @@ -32,7 +32,7 @@ import type { HttpServerResponse } from "./HttpServerResponse.ts" * The service can serve an HTTP response effect and exposes the address where the * server is listening. * - * @category models + * @category services * @since 4.0.0 */ export class HttpServer extends Context.Service { @@ -226,7 +226,7 @@ export const formatAddress = (address: Address): string => { * Reads the current server address, formats it with `formatAddress`, and passes * the formatted address to the supplied effectful function. * - * @category address + * @category accessors * @since 4.0.0 */ export const addressFormattedWith = ( @@ -240,7 +240,7 @@ export const addressFormattedWith = ( /** * Logs the formatted address of the current HTTP server. * - * @category address + * @category logging * @since 4.0.0 */ export const logAddress: Effect.Effect = addressFormattedWith((_) => @@ -250,7 +250,7 @@ export const logAddress: Effect.Effect = addressFormatt /** * Adds address logging to a layer that provides an `HttpServer`. * - * @category address + * @category layers * @since 4.0.0 */ export const withLogAddress = ( diff --git a/.context/effect/packages/effect/src/unstable/http/HttpServerError.ts b/.context/effect/packages/effect/src/unstable/http/HttpServerError.ts index 7fb6aeffa..ad39dbef9 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpServerError.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpServerError.ts @@ -178,7 +178,7 @@ export class InternalError extends Data.TaggedError("InternalError")<{ /** * Returns `true` when the supplied value is an `HttpServerError`. * - * @category predicates + * @category guards * @since 4.0.0 */ export const isHttpServerError = (u: unknown): u is HttpServerError => hasProperty(u, TypeId) @@ -249,7 +249,7 @@ export class ServeError extends Data.TaggedError("ServeError")<{ * `causeResponse` uses this annotation to map a pure client abort to a `499` * response instead of a server abort response. * - * @category annotations + * @category services * @since 4.0.0 */ export class ClientAbort extends Context.Service()("effect/http/HttpServerError/ClientAbort") { diff --git a/.context/effect/packages/effect/src/unstable/http/HttpServerRequest.ts b/.context/effect/packages/effect/src/unstable/http/HttpServerRequest.ts index d04626ede..ff4904116 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpServerRequest.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpServerRequest.ts @@ -16,6 +16,7 @@ import * as Context from "../../Context.ts" import * as Effect from "../../Effect.ts" import type * as FileSystem from "../../FileSystem.ts" import * as Inspectable from "../../Inspectable.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Option from "../../Option.ts" import type * as Path from "../../Path.ts" import type { ReadonlyRecord } from "../../Record.ts" @@ -44,7 +45,7 @@ export { * Use to configure the maximum body size accepted while reading server * request bodies. * - * @category fiber refs + * @category references * @since 4.0.0 */ MaxBodySize @@ -104,7 +105,7 @@ export interface HttpServerRequest extends HttpIncomingMessage.HttpIncomingMessa * Use to access the request currently being handled by HTTP server routes and * middleware. * - * @category context + * @category services * @since 4.0.0 */ export const HttpServerRequest: Context.Service = Context.Service( @@ -124,7 +125,7 @@ export const HttpServerRequest: Context.Service> => { const out: Record> = {} for (const [key, value] of url.searchParams.entries()) { - const entry = out[key] - if (entry !== undefined) { + if (Object.hasOwn(out, key)) { + const entry = out[key] if (Array.isArray(entry)) { entry.push(value) } else { - out[key] = [entry, value] + InternalRecord.assignProperty(out, key, [entry, value]) } } else { - out[key] = value + InternalRecord.assignProperty(out, key, value) } } return out @@ -435,14 +436,19 @@ export const toClientRequest = (request: HttpServerRequest): HttpClientRequest.H Option.getOrElse(toURL(request), () => request.url) ) -const toClientBody = (request: HttpServerRequest): HttpBody.HttpBody => - hasBody(request.method) +const toClientBody = (request: HttpServerRequest): HttpBody.HttpBody => { + if (!hasBody(request.method)) { + return HttpBody.empty + } + const formData = getFormDataBody(request) + return formData === undefined ? HttpBody.stream( request.stream, request.headers["content-type"], parseContentLength(request.headers["content-length"]) ) - : HttpBody.empty + : HttpBody.formData(formData) +} const parseContentLength = (contentLength: string | undefined): number | undefined => { if (contentLength === undefined) { @@ -862,25 +868,25 @@ const rawBodyStream = (request: HttpServerRequest, body: unknown): Stream.Stream if (body instanceof Request) { return streamFromReadable(request, body.body) } - if (isFormData(body)) { - return streamFromReadable(request, new Response(body).body) - } if (isReadableStream(body)) { return streamFromReadable(request, body) } + if (isBodyInit(body)) { + return streamFromReadable(request, new Response(body).body) + } return Stream.fail(requestParseError(request, "Unsupported body type")) } const rawBodyBytes = (request: HttpServerRequest, body: unknown): Effect.Effect => { - if (body instanceof Blob) { - return bytesFromBodyInit(request, body) - } if (body instanceof Request) { return Effect.tryPromise({ try: () => body.arrayBuffer().then((buffer) => new Uint8Array(buffer)), catch: (cause) => requestParseError(request, undefined, cause) }) } + if (isBodyInit(body)) { + return bytesFromBodyInit(request, body) + } return Effect.fail(requestParseError(request, "Unsupported body type")) } @@ -994,6 +1000,14 @@ const isReadableStream = (u: unknown): u is ReadableStream => const isFormData = (u: unknown): u is FormData => typeof FormData !== "undefined" && u instanceof FormData +const isBodyInit = (u: unknown): u is BodyInit => + typeof u === "string" || + (typeof ArrayBuffer !== "undefined" && (u instanceof ArrayBuffer || ArrayBuffer.isView(u))) || + (typeof Blob !== "undefined" && u instanceof Blob) || + isFormData(u) || + (typeof URLSearchParams !== "undefined" && u instanceof URLSearchParams) || + isReadableStream(u) + const textDecoder = new TextDecoder() /** diff --git a/.context/effect/packages/effect/src/unstable/http/HttpServerResponse.ts b/.context/effect/packages/effect/src/unstable/http/HttpServerResponse.ts index eef398bea..14c8c427d 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpServerResponse.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpServerResponse.ts @@ -34,6 +34,7 @@ import * as HttpClientRequest from "./HttpClientRequest.ts" import * as HttpClientResponse from "./HttpClientResponse.ts" import * as HttpIncomingMessage from "./HttpIncomingMessage.ts" import type { HttpPlatform } from "./HttpPlatform.ts" +import * as bodyInternal from "./internal/httpBody.ts" import * as Template from "./Template.ts" import * as UrlParams from "./UrlParams.ts" @@ -533,6 +534,24 @@ export const setHeader: { makeResponse({ ...self, headers: Headers.set(self.headers, key, value) + }, true) +) + +/** + * Returns a response with the specified header removed. + * + * @category combinators + * @since 4.0.0 + */ +export const removeHeader: { + (key: string): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, key: string): HttpServerResponse +} = dual( + 2, + (self: HttpServerResponse, key: string): HttpServerResponse => + makeResponse({ + ...self, + headers: Headers.remove(self.headers, key) }) ) @@ -551,7 +570,7 @@ export const setHeaders: { makeResponse({ ...self, headers: Headers.setAll(self.headers, input) - }) + }, true) ) /** @@ -913,7 +932,8 @@ export const setBody: { (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse } = dual( 2, - (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => makeResponse({ ...self, body }) + (self: HttpServerResponse, body: Body.HttpBody): HttpServerResponse => + makeResponse({ ...self, headers: bodyInternal.updateHeaders(self.headers, body), body }) ) /** @@ -1319,7 +1339,7 @@ const makeResponse = (options: { readonly headers?: Headers.Headers | undefined readonly cookies?: Cookies.Cookies | undefined readonly body?: Body.HttpBody | undefined -}) => { +}, preferHeaders = false) => { const self = Object.create(Proto) as Mutable self.status = options.status self.statusText = options.statusText @@ -1327,13 +1347,13 @@ const makeResponse = (options: { self.body = options.body ?? Body.empty if ( self.body._tag !== "Empty" && - (self.body.contentType || self.body.contentLength) + (self.body.contentType || self.body.contentLength !== undefined) ) { const newHeaders = Headers.fromRecordUnsafe({ ...options.headers }) as any - if (self.body.contentType) { + if (self.body.contentType && (!preferHeaders || newHeaders["content-type"] === undefined)) { newHeaders["content-type"] = self.body.contentType } - if (self.body.contentLength) { + if (self.body.contentLength !== undefined && (!preferHeaders || newHeaders["content-length"] === undefined)) { newHeaders["content-length"] = self.body.contentLength.toString() } self.headers = newHeaders diff --git a/.context/effect/packages/effect/src/unstable/http/HttpStaticServer.ts b/.context/effect/packages/effect/src/unstable/http/HttpStaticServer.ts index ee3c5c6c7..b1f047634 100644 --- a/.context/effect/packages/effect/src/unstable/http/HttpStaticServer.ts +++ b/.context/effect/packages/effect/src/unstable/http/HttpStaticServer.ts @@ -26,14 +26,41 @@ import * as HttpServerResponse from "./HttpServerResponse.ts" * * **Example** (Serving files from a directory) * - * ```ts - * import { Effect } from "effect" - * import { HttpStaticServer } from "effect/unstable/http" + * ```ts import.meta.vitest + * import { Effect, FileSystem, Layer, Path } from "effect" + * import { + * HttpEffect, + * HttpPlatform, + * HttpServerResponse, + * HttpStaticServer + * } from "effect/unstable/http" * - * const program = Effect.gen(function*() { - * const app = yield* HttpStaticServer.make({ root: "./public" }) - * return app + * const TestFileSystem = FileSystem.layerNoop({ + * stat: () => + * Effect.succeed({ + * type: "File", + * size: FileSystem.Size(20) + * } as FileSystem.File.Info) * }) + * const TestHttpPlatform = Layer.succeed( + * HttpPlatform.HttpPlatform, + * HttpPlatform.HttpPlatform.of({ + * platform: "web", + * fileResponse: (path) => Effect.succeed(HttpServerResponse.text(`Serving ${path}`)), + * fileWebResponse: () => Effect.die("unused") + * }) + * ) + * const TestServices = Layer.mergeAll(Path.layer, TestFileSystem, TestHttpPlatform) + * + * const program = Effect.gen(function*() { + * const app = yield* HttpStaticServer.make({ root: "/public" }) + * const handler = HttpEffect.toWebHandler(app) + * const response = yield* Effect.promise(() => handler(new Request("http://localhost/guide.txt"))) + * const body = yield* Effect.promise(() => response.text()) + * return body + * }).pipe(Effect.provide(TestServices)) + * + * await Effect.runPromise(program) // => "Serving /public/guide.txt" * ``` * * @category constructors @@ -179,7 +206,7 @@ export const make: (options: { * * **Example** (Mounting static files on a router) * - * ```ts + * ```ts import.meta.vitest * import { Layer } from "effect" * import { HttpRouter, HttpServerResponse, HttpStaticServer } from "effect/unstable/http" * @@ -191,6 +218,7 @@ export const make: (options: { * }) * * const AppLayer = Layer.mergeAll(ApiLayer, StaticFilesLayer) + * Layer.isLayer(AppLayer) // => true * ``` * * @category layers diff --git a/.context/effect/packages/effect/src/unstable/http/Multipart.ts b/.context/effect/packages/effect/src/unstable/http/Multipart.ts index 8df44a064..344efc4bf 100644 --- a/.context/effect/packages/effect/src/unstable/http/Multipart.ts +++ b/.context/effect/packages/effect/src/unstable/http/Multipart.ts @@ -34,7 +34,7 @@ import * as UndefinedOr from "../../UndefinedOr.ts" import * as IncomingMessage from "./HttpIncomingMessage.ts" import * as HttpServerRespondable from "./HttpServerRespondable.ts" import * as HttpServerResponse from "./HttpServerResponse.ts" -import * as MP from "./Multipasta.ts" +import * as MP from "./MultipartParser.ts" /** * Type identifier used to brand multipart part values. @@ -276,6 +276,13 @@ export class MultipartError extends Data.TaggedError("MultipartError")<{ */ export interface PersistedFileSchema extends Schema.declare {} +const PersistedFileEncoded = Schema.Struct({ + key: Schema.String, + name: Schema.String, + contentType: Schema.String.annotate({ contentEncoding: "binary" }), + path: Schema.String +}) + /** * Schema for persisted multipart files. * @@ -290,23 +297,19 @@ export interface PersistedFileSchema extends Schema.declare {} export const PersistedFileSchema: PersistedFileSchema = Schema.declare( isPersistedFile, { - typeConstructor: { - _tag: "effect/http/PersistedFile" - }, - generation: { - runtime: `Multipart.PersistedFileSchema`, - Type: `Multipart.PersistedFile`, - importDeclaration: `import * as Multipart from "effect/unstable/http/Multipart"` + representation: { + id: "effect/http/PersistedFile", + payload: null }, + toCode: () => ({ + runtime: "Multipart.PersistedFileSchema", + Type: "Multipart.PersistedFile", + importDeclarations: [`import * as Multipart from "effect/unstable/http/Multipart"`] + }), expected: "PersistedFile", toCodecJson: () => Schema.link()( - Schema.Struct({ - key: Schema.String, - name: Schema.String, - contentType: Schema.String.annotate({ contentEncoding: "binary" }), - path: Schema.String - }), + PersistedFileEncoded, SchemaTransformation.transform({ decode: ({ contentType, key, name, path }) => new PersistedFileImpl(key, name, contentType, path), encode: (file) => ({ @@ -435,7 +438,7 @@ export const makeConfig = ( * non-empty batches of parsed `Part` values, failing with `MultipartError` for * parser and limit failures. * - * @category Parsers + * @category parsing * @since 4.0.0 */ export const makeChannel = (headers: Record): Channel.Channel< @@ -599,7 +602,7 @@ class FileImpl extends PartBase implements File { this.contentType = info.contentType this.content = Stream.fromChannel(channel) this.contentEffect = channel.pipe( - collectUint8Array, + Channel.mkUint8Array, Effect.mapError((cause) => MultipartError.fromReason("InternalError", cause)) ) } @@ -632,24 +635,15 @@ const defaultWriteFile = (path: string, file: File) => * **Gotchas** * * This materializes the full content in memory. + * The source channel must not reuse or mutate emitted buffers, which are retained + * until collection completes. * * @category converting * @since 4.0.0 */ export const collectUint8Array = ( self: Channel.Channel, OE, OD, unknown, unknown, unknown, R> -): Effect.Effect, OE, R> => - Channel.runFold(self, constant(new Uint8Array(0)), (accumulator, chunk) => { - const totalLength = chunk.reduce((sum, element) => sum + element.length, accumulator.length) - const newAccumulator = new Uint8Array(totalLength) - newAccumulator.set(accumulator, 0) - let offset = accumulator.length - for (const element of chunk) { - newAccumulator.set(element, offset) - offset += element.length - } - return newAccumulator - }) +): Effect.Effect, OE, R> => Channel.mkUint8Array(self) /** * Persists a stream of multipart parts into a record. @@ -675,6 +669,8 @@ export const toPersisted = ( const path_ = yield* Path.Path const dir = yield* fs.makeTempDirectoryScoped() const persisted: Record | Array | string> = Object.create(null) + const usedPaths = new Set() + let fileIndex = 0 yield* Stream.runForEach(stream, (part) => { if (part._tag === "Field") { if (!(part.key in persisted)) { @@ -689,7 +685,12 @@ export const toPersisted = ( return Effect.void } const file = part - const path = path_.join(dir, path_.basename(file.name).slice(-128)) + const fileName = path_.basename(file.name).slice(-128) + let path = path_.join(dir, fileName) + while (usedPaths.has(path)) { + path = path_.join(dir, `${fileIndex++}-${fileName}`) + } + usedPaths.add(path) const filePart = new PersistedFileImpl( file.key, file.name, @@ -791,7 +792,7 @@ export declare namespace withLimits { * These settings control maximum part count, field size, file size, total body * size, and MIME types that should be treated as fields instead of files. * - * @category fiber refs + * @category options * @since 4.0.0 */ export type Options = { @@ -810,7 +811,7 @@ export declare namespace withLimits { * * The default is `undefined`, meaning no explicit part-count limit. * - * @category references + * @category services * @since 4.0.0 */ export const MaxParts = Context.Reference("effect/http/Multipart/MaxParts", { @@ -824,7 +825,7 @@ export const MaxParts = Context.Reference("effect/http/Multi * * The default limit is 10 MiB. * - * @category references + * @category services * @since 4.0.0 */ export const MaxFieldSize = Context.Reference("effect/http/Multipart/MaxFieldSize", { @@ -838,7 +839,7 @@ export const MaxFieldSize = Context.Reference("effect/http * * The default is `undefined`, meaning no explicit per-file limit. * - * @category references + * @category services * @since 4.0.0 */ export const MaxFileSize = Context.Reference( @@ -854,7 +855,7 @@ export const MaxFileSize = Context.Reference( * * The default treats `application/json` parts as fields. * - * @category references + * @category services * @since 4.0.0 */ export const FieldMimeTypes = Context.Reference>("effect/http/Multipart/FieldMimeTypes", { diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser.ts new file mode 100644 index 000000000..01cf7d1c5 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser.ts @@ -0,0 +1,115 @@ +/** + * Low-level parser for HTTP `multipart/form-data` bodies. + * + * @since 4.0.0 + */ +import type * as HeadersParser from "./MultipartParser/HeadersParser.ts" +import * as internal from "./MultipartParser/internal/multipart.ts" + +/** + * Metadata describing a multipart form part. + * + * @category models + * @since 4.0.0 + */ +export interface PartInfo { + readonly name: string + readonly filename?: string | undefined + readonly contentType: string + readonly contentTypeParameters: Record + readonly contentDisposition: string + readonly contentDispositionParameters: Record + readonly headers: Record> +} + +/** + * An error produced while parsing a multipart body. + * + * @category errors + * @since 4.0.0 + */ +export type MultipartError = + | { + readonly _tag: "InvalidBoundary" + } + | { + readonly _tag: "BadHeaders" + readonly error: HeadersParser.Failure + } + | { + readonly _tag: "InvalidDisposition" + } + | { + readonly _tag: "ReachedLimit" + readonly limit: + | "MaxParts" + | "MaxTotalSize" + | "MaxPartSize" + | "MaxFieldSize" + } + | { + readonly _tag: "EndNotReached" + } + +/** + * Shared multipart parser configuration. + * + * @category models + * @since 4.0.0 + */ +export type BaseConfig = { + readonly headers: Record + readonly isFile?: ((info: PartInfo) => boolean) | undefined + readonly maxParts?: number | undefined + readonly maxTotalSize?: number | undefined + readonly maxPartSize?: number | undefined + readonly maxFieldSize?: number | undefined +} + +/** + * Multipart parser configuration with event callbacks. + * + * @category models + * @since 4.0.0 + */ +export type Config = BaseConfig & { + readonly onField: (info: PartInfo, value: Uint8Array) => void + readonly onFile: (info: PartInfo) => (chunk: Uint8Array | null) => void + readonly onError: (error: MultipartError) => void + readonly onDone: () => void +} + +/** + * A streaming multipart parser. + * + * @category models + * @since 4.0.0 + */ +export interface Parser { + readonly write: (chunk: Uint8Array) => void + readonly end: () => void +} + +/** + * Creates a streaming multipart parser. + * + * @category constructors + * @since 4.0.0 + */ +export const make: (options: Config) => Parser = internal.make + +/** + * Determines whether a multipart part should be treated as a file. + * + * @category utilities + * @since 4.0.0 + */ +export const defaultIsFile: (info: PartInfo) => boolean = internal.defaultIsFile + +/** + * Decodes a multipart field using its declared character set. + * + * @category utilities + * @since 4.0.0 + */ +export const decodeField: (info: PartInfo, value: Uint8Array) => string = internal.decodeField diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/HeadersParser.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/HeadersParser.ts new file mode 100644 index 000000000..c42d56125 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/HeadersParser.ts @@ -0,0 +1,68 @@ +/** + * Low-level parser for multipart header blocks. + * + * @since 4.0.0 + */ +import * as internal from "./internal/headers.ts" + +/** + * The reason a multipart header block could not be parsed. + * + * @category errors + * @since 4.0.0 + */ +export type FailureReason = + | "TooManyHeaders" + | "HeaderTooLarge" + | "InvalidHeaderName" + | "InvalidHeaderValue" + +/** + * Indicates that the parser needs more input. + * + * @category models + * @since 4.0.0 + */ +export interface Continue { + readonly _tag: "Continue" +} + +/** + * A multipart header parsing failure. + * + * @category errors + * @since 4.0.0 + */ +export interface Failure { + readonly _tag: "Failure" + readonly reason: FailureReason + readonly headers: Record> +} + +/** + * A successfully parsed multipart header block. + * + * @category models + * @since 4.0.0 + */ +export interface Headers { + readonly _tag: "Headers" + readonly headers: Record> + readonly endPosition: number +} + +/** + * The result of parsing a multipart header block. + * + * @category models + * @since 4.0.0 + */ +export type ReturnValue = Continue | Failure | Headers + +/** + * Creates an incremental multipart header parser. + * + * @category constructors + * @since 4.0.0 + */ +export const make: () => (chunk: Uint8Array, start: number) => ReturnValue = internal.make diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/Search.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/Search.ts new file mode 100644 index 000000000..47ca06d33 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/Search.ts @@ -0,0 +1,17 @@ +/** + * Low-level byte search used by the multipart parser. + * + * @since 4.0.0 + */ +import * as internal from "./internal/search.ts" + +/** + * Creates an incremental byte search for a string boundary. + * + * @category constructors + * @since 4.0.0 + */ +export const make: ( + needle: string, + callback: (index: number, chunk: Uint8Array) => void +) => { readonly write: (chunk: Uint8Array) => void; readonly end: () => void } = internal.make diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/contentType.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/contentType.ts new file mode 100644 index 000000000..2ea96a197 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/contentType.ts @@ -0,0 +1,103 @@ +// taken from https://github.com/fastify/fast-content-type-parse +// under the MIT license + +/** + * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1 + * + * parameter = token "=" ( token / quoted-string ) + * token = 1*tchar + * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + * / DIGIT / ALPHA + * ; any VCHAR, except delimiters + * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + * obs-text = %x80-FF + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + */ +const paramRE = + /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u{10ffff}]|\\[\v\u0020-\u{10ffff}])*"|[!#$%&'*+.^\w`|~-]+) */gu + +/** + * RegExp to match quoted-pair in RFC 7230 sec 3.2.6 + * + * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + * obs-text = %x80-FF + */ +const quotedPairRE = /\\([\v\u0020-\u{10ffff}])/gu + +/** + * RegExp to match type in RFC 7231 sec 3.1.1.1 + * + * media-type = type "/" subtype + * type = token + * subtype = token + */ +const mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u +const mediaTypeRENoSlash = /^[!#$%&'*+.^\w|~-]+$/u + +// default ContentType to prevent repeated object creation +const defaultContentType = { value: "", parameters: Object.create(null) } + +export function parse( + header: string | undefined, + withoutSlash = false +): { + readonly value: string + readonly parameters: Record +} { + if (typeof header !== "string") { + return defaultContentType + } + + let index = header.indexOf(";") + const type = index !== -1 ? header.slice(0, index).trim() : header.trim() + const mediaRE = withoutSlash ? mediaTypeRENoSlash : mediaTypeRE + + if (mediaRE.test(type) === false) { + return defaultContentType + } + + const result = { + value: type.toLowerCase(), + parameters: Object.create(null) + } + + // parse parameters + if (index === -1) { + return result + } + + let key: string + let match: RegExpExecArray | null + let value: string + + paramRE.lastIndex = index + + while ((match = paramRE.exec(header))) { + if (match.index !== index) { + return defaultContentType + } + + index += match[0].length + key = match[1].toLowerCase() + value = match[2] + + if (value[0] === "\"") { + // remove quotes and escapes + value = value.slice(1, value.length - 1) + + if (!withoutSlash && quotedPairRE.test(value)) { + value = value.replace(quotedPairRE, "$1") + } + } + + result.parameters[key] = value + } + + if (index !== header.length) { + return defaultContentType + } + + return result +} diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/headers.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/headers.ts new file mode 100644 index 000000000..312d2dc4a --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/headers.ts @@ -0,0 +1,244 @@ +import type { Continue, FailureReason, ReturnValue } from "../HeadersParser.ts" + +const constMaxPairs = 100 +const constMaxSize = 16 * 1024 + +const State = { + key: 0, + whitespace: 1, + value: 2 +} as const +type State = (typeof State)[keyof typeof State] + +const constContinue: Continue = { _tag: "Continue" } + +// RFC 7230 token characters, allowed in header names +// The previous table stopped at byte 126; zero-filled entries for bytes 127-255 +// preserve its rejection behavior because callers accept only entries equal to 1. +const constNameChars = new Uint8Array(256) +for (const char of "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") { + constNameChars[char.charCodeAt(0)] = 1 +} + +// HTAB, visible ASCII and obs-text, allowed in header values +const constValueChars = new Uint8Array(256) +constValueChars[9] = 1 +for (let i = 32; i <= 126; i++) { + constValueChars[i] = 1 +} +for (let i = 128; i <= 255; i++) { + constValueChars[i] = 1 +} + +export function make() { + const decoder = new TextDecoder() + const state = { + state: State.key as State, + headers: Object.create(null) as Record>, + key: "", + value: undefined as undefined | Uint8Array, + crlf: 0, + previousChunk: undefined as undefined | Uint8Array, + pairs: 0, + size: 0 + } + + function reset(value: ReturnValue): ReturnValue { + state.state = State.key + state.headers = Object.create(null) + state.key = "" + state.value = undefined + state.crlf = 0 + state.previousChunk = undefined + state.pairs = 0 + state.size = 0 + return value + } + + function concatUint8Array(a: Uint8Array, b: Uint8Array): Uint8Array { + const newUint8Array = new Uint8Array(a.length + b.length) + newUint8Array.set(a) + newUint8Array.set(b, a.length) + return newUint8Array + } + + function error(reason: FailureReason) { + return reset({ _tag: "Failure", reason, headers: state.headers }) + } + + return function write(chunk: Uint8Array, start: number): ReturnValue { + let endOffset = 0 + let previousCursor: number | undefined + if (state.previousChunk !== undefined) { + endOffset = state.previousChunk.length + previousCursor = endOffset + + const newChunk = new Uint8Array(chunk.length + endOffset) + newChunk.set(state.previousChunk) + newChunk.set(chunk, endOffset) + state.previousChunk = undefined + chunk = newChunk + } + const end = chunk.length + + outer: while (start < end) { + if (state.state === State.key) { + let i = start + for (; i < end; i++) { + if (state.size++ > constMaxSize) { + return error("HeaderTooLarge") + } + + if (chunk[i] === 58) { + state.key += decoder.decode(chunk.subarray(start, i)).toLowerCase() + if (state.key.length === 0) { + return error("InvalidHeaderName") + } + + if ( + chunk[i + 1] === 32 && + chunk[i + 2] !== 32 && + chunk[i + 2] !== 9 + ) { + start = i + 2 + state.state = State.value + state.size++ + } else if (chunk[i + 1] !== 32 && chunk[i + 1] !== 9) { + start = i + 1 + state.state = State.value + } else { + start = i + 1 + state.state = State.whitespace + } + + break + } else if (constNameChars[chunk[i]] !== 1) { + return error("InvalidHeaderName") + } + } + if (i === end) { + state.key += decoder.decode(chunk.subarray(start, end)).toLowerCase() + return constContinue + } + } + + if (state.state === State.whitespace) { + for (; start < end; start++) { + if (state.size++ > constMaxSize) { + return error("HeaderTooLarge") + } + + if (chunk[start] !== 32 && chunk[start] !== 9) { + state.state = State.value + break + } + } + if (start === end) { + return constContinue + } + } + + if (state.state === State.value) { + let i = start + if (previousCursor !== undefined) { + i = previousCursor + previousCursor = undefined + } + for (; i < end; i++) { + if (state.size++ > constMaxSize) { + return error("HeaderTooLarge") + } + + if (chunk[i] === 13 || state.crlf > 0) { + let byte = chunk[i] + + if (byte === 13 && state.crlf === 0) { + state.crlf = 1 + i++ + state.size++ + byte = chunk[i] + } + if (byte === 10 && state.crlf === 1) { + state.crlf = 2 + i++ + state.size++ + byte = chunk[i] + } + if (byte === 13 && state.crlf === 2) { + state.crlf = 3 + i++ + state.size++ + byte = chunk[i] + } + if (byte === 10 && state.crlf === 3) { + state.crlf = 4 + i++ + state.size++ + } + + if (state.crlf < 4 && i >= end) { + state.previousChunk = chunk.subarray(start) + return constContinue + } else if (state.crlf >= 2) { + state.value = state.value === undefined + ? chunk.subarray(start, i - state.crlf) + : concatUint8Array( + state.value, + chunk.subarray(start, i - state.crlf) + ) + const value = decoder.decode(state.value) + if (state.headers[state.key] === undefined) { + state.headers[state.key] = value + } else if (typeof state.headers[state.key] === "string") { + state.headers[state.key] = [ + state.headers[state.key] as string, + value + ] + } else { + ;(state.headers[state.key] as Array).push(value) + } + + start = i + state.size-- + + if (state.crlf !== 4 && state.pairs === constMaxPairs) { + return error("TooManyHeaders") + } else if (state.crlf === 3) { + return error("InvalidHeaderValue") + } else if (state.crlf === 4) { + return reset({ + _tag: "Headers", + headers: state.headers, + endPosition: start - endOffset + }) + } + + state.pairs++ + state.key = "" + state.value = undefined + state.crlf = 0 + state.state = State.key + + continue outer + } + } else if (constValueChars[chunk[i]] !== 1) { + return error("InvalidHeaderValue") + } + } + + if (i === end) { + state.value = state.value === undefined + ? chunk.subarray(start, end) + : concatUint8Array(state.value, chunk.subarray(start, end)) + return constContinue + } + } + } + + if (start > end) { + state.size += end - start + } + + return constContinue + } +} diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/multipart.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/multipart.ts new file mode 100644 index 000000000..78d7fb077 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/multipart.ts @@ -0,0 +1,279 @@ +import type { Config, MultipartError, PartInfo } from "../../MultipartParser.ts" +import * as CT from "./contentType.ts" +import * as HP from "./headers.ts" +import * as Search from "./search.ts" + +const State = { + headers: 0, + body: 1 +} as const +type State = (typeof State)[keyof typeof State] + +const errInvalidDisposition: MultipartError = { _tag: "InvalidDisposition" } +const errEndNotReached: MultipartError = { _tag: "EndNotReached" } +const errMaxParts: MultipartError = { _tag: "ReachedLimit", limit: "MaxParts" } +const errMaxTotalSize: MultipartError = { + _tag: "ReachedLimit", + limit: "MaxTotalSize" +} +const errMaxPartSize: MultipartError = { + _tag: "ReachedLimit", + limit: "MaxPartSize" +} +const errMaxFieldSize: MultipartError = { + _tag: "ReachedLimit", + limit: "MaxFieldSize" +} + +const constCR = new TextEncoder().encode("\r\n") + +export function defaultIsFile(info: PartInfo) { + return ( + info.filename !== undefined || + info.contentType === "application/octet-stream" + ) +} + +function parseBoundary(headers: Record) { + const contentType = CT.parse(headers["content-type"]) + return contentType.parameters.boundary +} + +function noopOnChunk(_chunk: Uint8Array | null) {} + +export function make({ + headers, + onFile: onPart, + onField, + onError, + onDone, + isFile = defaultIsFile, + maxParts = Infinity, + maxTotalSize = Infinity, + maxPartSize = Infinity, + maxFieldSize = 1024 * 1024 +}: Config) { + const boundary = parseBoundary(headers) + if (boundary === undefined) { + onError({ _tag: "InvalidBoundary" }) + return { + write: noopOnChunk, + end() {} + } + } + + const state = { + state: State.headers as State, + index: 0, + parts: 0, + onChunk: noopOnChunk, + info: undefined as any as PartInfo, + headerSkip: 0, + partSize: 0, + totalSize: 0, + isFile: false, + fieldChunks: [] as Array, + fieldSize: 0, + done: false, + stopped: false + } + + function skipBody() { + state.state = State.body + state.isFile = true + state.onChunk = noopOnChunk + } + + function stop(error: MultipartError) { + state.stopped = true + if (state.state === State.body && state.isFile) { + state.onChunk(null) + } + onError(error) + } + + const headerParser = HP.make() + + const split = Search.make( + `\r\n--${boundary}`, + function(index, chunk) { + if (state.stopped) { + return + } + + if (index === 0) { + // data before the first boundary + skipBody() + return + } else if (index !== state.index) { + if (state.index > 0) { + if (state.isFile) { + state.onChunk(null) + } else { + if (state.fieldChunks.length === 1) { + onField(state.info, state.fieldChunks[0]) + } else { + const buf = new Uint8Array(state.fieldSize) + let offset = 0 + for (let i = 0; i < state.fieldChunks.length; i++) { + const chunk = state.fieldChunks[i] + buf.set(chunk, offset) + offset += chunk.length + } + onField(state.info, buf) + } + state.fieldSize = 0 + state.fieldChunks = [] + } + } + state.partSize = 0 + + state.state = State.headers + state.index = index + state.headerSkip = 2 // skip the first \r\n + + // trailing -- + if (chunk[0] === 45 && chunk[1] === 45) { + state.done = true + return onDone() + } + + state.parts++ + if (state.parts > maxParts) { + return stop(errMaxParts) + } + } + + if ((state.partSize += chunk.length) > maxPartSize) { + return stop(errMaxPartSize) + } + + if (state.state === State.headers) { + const result = headerParser(chunk, state.headerSkip) + state.headerSkip = 0 + + if (result._tag === "Continue") { + return + } else if (result._tag === "Failure") { + skipBody() + return onError({ _tag: "BadHeaders", error: result }) + } + + const contentType = CT.parse(result.headers["content-type"] as string) + const contentDisposition = CT.parse( + result.headers["content-disposition"] as string, + true + ) + + if ( + "form-data" === contentDisposition.value && + !("name" in contentDisposition.parameters) + ) { + skipBody() + return onError(errInvalidDisposition) + } + + let encodedFilename: string | undefined + if ("filename*" in contentDisposition.parameters) { + const parts = contentDisposition.parameters["filename*"].split("''") + if (parts.length === 2) { + try { + encodedFilename = decodeURIComponent(parts[1]) + } catch { + encodedFilename = parts[1] + } + } + } + + state.info = { + name: contentDisposition.parameters.name ?? "", + filename: encodedFilename ?? contentDisposition.parameters.filename, + contentType: contentType.value === "" + ? contentDisposition.parameters.filename !== undefined + ? "application/octet-stream" + : "text/plain" + : contentType.value, + contentTypeParameters: contentType.parameters, + contentDisposition: contentDisposition.value, + contentDispositionParameters: contentDisposition.parameters as any, + headers: result.headers + } + + state.state = State.body + state.isFile = isFile(state.info) + + if (state.isFile) { + state.onChunk = onPart(state.info) + } + + if (result.endPosition < chunk.length) { + if (state.isFile) { + state.onChunk(chunk.subarray(result.endPosition)) + } else { + const buf = chunk.subarray(result.endPosition) + if ((state.fieldSize += buf.length) > maxFieldSize) { + return stop(errMaxFieldSize) + } + state.fieldChunks.push(buf) + } + } + } else if (state.isFile) { + state.onChunk(chunk) + } else { + if ((state.fieldSize += chunk.length) > maxFieldSize) { + return stop(errMaxFieldSize) + } + state.fieldChunks.push(chunk) + } + }, + constCR, + 2 + ) + + return { + write(chunk: Uint8Array) { + if (state.stopped) { + return + } + if ((state.totalSize += chunk.length) > maxTotalSize) { + return stop(errMaxTotalSize) + } + return split.write(chunk) + }, + end() { + split.end() + if (!state.done && !state.stopped) { + stop(errEndNotReached) + } + + state.state = State.headers + state.index = 0 + state.parts = 0 + state.onChunk = noopOnChunk + state.info = undefined as any as PartInfo + state.totalSize = 0 + state.partSize = 0 + state.fieldChunks = [] + state.fieldSize = 0 + state.done = false + state.stopped = false + } + } as const +} + +const utf8Decoder = new TextDecoder("utf-8") +function getDecoder(charset: string) { + if (charset === "utf-8" || charset === "utf8" || charset === "") { + return utf8Decoder + } + + try { + return new TextDecoder(charset) + } catch (error) { + return utf8Decoder + } +} + +export function decodeField(info: PartInfo, value: Uint8Array): string { + return getDecoder(info.contentTypeParameters.charset ?? "utf-8").decode(value) +} diff --git a/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/search.ts b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/search.ts new file mode 100644 index 000000000..be7530e22 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/MultipartParser/internal/search.ts @@ -0,0 +1,167 @@ +interface SearchState { + readonly needle: Uint8Array + readonly needleLength: number + readonly indexes: Record> + readonly firstByte: number + + previousChunk: Uint8Array | undefined + previousChunkLength: number + matchIndex: number +} + +function makeState(needle_: string): SearchState { + const needle = new TextEncoder().encode(needle_) + const needleLength = needle.length + + const indexes: Record> = {} + for (let i = 0; i < needleLength; i++) { + const b = needle[i] + if (indexes[b] === undefined) indexes[b] = [] + indexes[b].push(i) + } + + return { + needle, + needleLength, + indexes, + firstByte: needle[0], + previousChunk: undefined, + previousChunkLength: 0, + matchIndex: 0 + } +} + +export function make( + needle: string, + callback: (index: number, chunk: Uint8Array) => void, + seed?: Uint8Array, + minimumChunkLength?: number +) { + const state = makeState(needle) + const minChunkLength = minimumChunkLength ?? state.needleLength + if (seed !== undefined) { + state.previousChunk = seed + state.previousChunkLength = seed.length + } + + function makeIndexOf(): ( + chunk: Uint8Array, + needle: Uint8Array, + fromIndex: number + ) => number { + // on node.js use the Buffer api + if ( + "Buffer" in globalThis && + !("Bun" in globalThis || "Deno" in globalThis) + ) { + return function(chunk, needle, fromIndex) { + return Buffer.prototype.indexOf.call(chunk, needle, fromIndex) + } + } + + const skipTable = new Uint8Array(256).fill(state.needle.length) + for (let i = 0, lastIndex = state.needle.length - 1; i < lastIndex; ++i) { + skipTable[state.needle[i]] = lastIndex - i + } + + return function(chunk, needle, fromIndex) { + const lengthTotal = chunk.length + let i = fromIndex + state.needleLength - 1 + + while (i < lengthTotal) { + for ( + let j = state.needleLength - 1, k = i; + j >= 0 && chunk[k] === needle[j]; + j--, k-- + ) { + if (j === 0) return k + } + i += skipTable[chunk[i]] + } + + return -1 + } + } + + const indexOf = makeIndexOf() + + function write(chunk: Uint8Array): void { + let chunkLength = chunk.length + + if (state.previousChunk !== undefined) { + const newChunk = new Uint8Array(state.previousChunkLength + chunkLength) + newChunk.set(state.previousChunk) + newChunk.set(chunk, state.previousChunkLength) + chunk = newChunk + chunkLength = state.previousChunkLength + chunkLength + state.previousChunk = undefined + } + + let pos = 0 + while (pos < chunkLength) { + const remaining = chunkLength - pos + if (remaining < minChunkLength) { + state.previousChunk = chunk.subarray(pos) + state.previousChunkLength = remaining + return + } + + const match = indexOf(chunk, state.needle, pos) + + if (match > -1) { + if (match > pos) { + callback(state.matchIndex, chunk.subarray(pos, match)) + } + state.matchIndex += 1 + pos = match + state.needleLength + continue + } else if (chunk[chunkLength - 1] in state.indexes) { + const indexes = state.indexes[chunk[chunkLength - 1]] + let earliestIndex = -1 + for (let i = 0, len = indexes.length; i < len; i++) { + const index = indexes[i] + if ( + chunk[chunkLength - 1 - index] === state.firstByte && + i > earliestIndex + ) { + earliestIndex = index + } + } + if (earliestIndex === -1) { + if (pos === 0) { + callback(state.matchIndex, chunk) + } else { + callback(state.matchIndex, chunk.subarray(pos)) + } + } else { + if (chunkLength - 1 - earliestIndex > pos) { + callback( + state.matchIndex, + chunk.subarray(pos, chunkLength - 1 - earliestIndex) + ) + } + state.previousChunk = chunk.subarray(chunkLength - 1 - earliestIndex) + state.previousChunkLength = earliestIndex + 1 + } + } else if (pos === 0) { + callback(state.matchIndex, chunk) + } else { + callback(state.matchIndex, chunk.subarray(pos)) + } + + break + } + } + + function end(): void { + if (state.previousChunk !== undefined && state.previousChunk !== seed) { + callback(state.matchIndex, state.previousChunk) + } + + state.previousChunk = seed + state.previousChunkLength = seed?.length ?? 0 + state.matchIndex = 0 + } + + return { write, end } as const +} diff --git a/.context/effect/packages/effect/src/unstable/http/Multipasta.ts b/.context/effect/packages/effect/src/unstable/http/Multipasta.ts deleted file mode 100644 index 53d5ff8b2..000000000 --- a/.context/effect/packages/effect/src/unstable/http/Multipasta.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Re-exports the `multipasta` multipart parser used by the unstable HTTP - * modules. - * - * This module keeps the parser types and helpers available from the Effect HTTP - * namespace without wrapping or changing them. - * - * @since 4.0.0 - */ - -/** - * @category re-exports - * @since 4.0.0 - */ -export * from "multipasta" diff --git a/.context/effect/packages/effect/src/unstable/http/Multipasta/HeadersParser.ts b/.context/effect/packages/effect/src/unstable/http/Multipasta/HeadersParser.ts deleted file mode 100644 index 094cae5f5..000000000 --- a/.context/effect/packages/effect/src/unstable/http/Multipasta/HeadersParser.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Re-exports the `multipasta` header parser used by multipart HTTP parsing. - * - * This module keeps the parser available from the Effect HTTP namespace without - * wrapping or changing it. - * - * @since 4.0.0 - */ - -/** - * @category re-exports - * @since 4.0.0 - */ -export * from "multipasta/HeadersParser" diff --git a/.context/effect/packages/effect/src/unstable/http/Multipasta/Node.ts b/.context/effect/packages/effect/src/unstable/http/Multipasta/Node.ts deleted file mode 100644 index effc1f465..000000000 --- a/.context/effect/packages/effect/src/unstable/http/Multipasta/Node.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Re-exports the Node.js multipart helpers from `multipasta`. - * - * This module keeps the Node-specific parser helpers available from the Effect - * HTTP namespace without wrapping or changing them. - * - * @since 4.0.0 - */ - -/** - * @category re-exports - * @since 4.0.0 - */ -export * from "multipasta/node" diff --git a/.context/effect/packages/effect/src/unstable/http/Multipasta/Search.ts b/.context/effect/packages/effect/src/unstable/http/Multipasta/Search.ts deleted file mode 100644 index 4a1c3013a..000000000 --- a/.context/effect/packages/effect/src/unstable/http/Multipasta/Search.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Re-exports the multipart search helpers from `multipasta`. - * - * This module keeps the search helpers available from the Effect HTTP namespace - * without wrapping or changing them. - * - * @since 4.0.0 - */ - -/** - * @category re-exports - * @since 4.0.0 - */ -export * from "multipasta/Search" diff --git a/.context/effect/packages/effect/src/unstable/http/Multipasta/Web.ts b/.context/effect/packages/effect/src/unstable/http/Multipasta/Web.ts deleted file mode 100644 index 30e60a5af..000000000 --- a/.context/effect/packages/effect/src/unstable/http/Multipasta/Web.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Re-exports the Web multipart helpers from `multipasta`. - * - * This module keeps the Web-specific parser helpers available from the Effect - * HTTP namespace without wrapping or changing them. - * - * @since 4.0.0 - */ - -/** - * @category re-exports - * @since 4.0.0 - */ -export * from "multipasta/web" diff --git a/.context/effect/packages/effect/src/unstable/http/Template.ts b/.context/effect/packages/effect/src/unstable/http/Template.ts index 38d3f95e9..994ecd461 100644 --- a/.context/effect/packages/effect/src/unstable/http/Template.ts +++ b/.context/effect/packages/effect/src/unstable/http/Template.ts @@ -154,7 +154,7 @@ export function make>( values[index] = primitiveToString(value) })), { - concurrency: "inherit", + concurrency: "unbounded", discard: true } ), @@ -215,11 +215,12 @@ export function stream>( buffer = "" } - return Stream.flatMap( - Stream.fromIterable(chunks), - (chunk) => - typeof chunk === "string" ? Stream.succeed(chunk) : Effect.isEffect(chunk) ? Stream.fromEffect(chunk) : chunk, - { concurrency: "unbounded" } + return Stream.fromIterable(chunks).pipe( + Stream.mapEffect( + (chunk) => Effect.isEffect(chunk) ? chunk : Effect.succeed(chunk), + { concurrency: "unbounded" } + ), + Stream.flatMap((chunk) => typeof chunk === "string" ? Stream.succeed(chunk) : chunk) ) } diff --git a/.context/effect/packages/effect/src/unstable/http/Url.ts b/.context/effect/packages/effect/src/unstable/http/Url.ts index 8c73a9f07..4b1e63cf2 100644 --- a/.context/effect/packages/effect/src/unstable/http/Url.ts +++ b/.context/effect/packages/effect/src/unstable/http/Url.ts @@ -88,7 +88,7 @@ const baseUrl = (): string | undefined => { * * **Example** (Parsing absolute and relative URLs) * - * ```ts + * ```ts import.meta.vitest * import { Result } from "effect" * import { Url } from "effect/unstable/http" * @@ -98,22 +98,12 @@ const baseUrl = (): string | undefined => { * // ▼ * const parsed = Url.fromString("https://example.com/path") * - * if (Result.isSuccess(parsed)) { - * console.log("Parsed URL:", parsed.success.toString()) - * } else { - * console.log("Error:", parsed.failure.message) - * } - * // Output: Parsed URL: https://example.com/path + * Result.map(parsed, (url) => url.toString()) // => Result.succeed("https://example.com/path") * * // Parse a relative URL with a base * const relativeParsed = Url.fromString("/relative-path", "https://example.com") * - * if (Result.isSuccess(relativeParsed)) { - * console.log("Parsed relative URL:", relativeParsed.success.toString()) - * } else { - * console.log("Error:", relativeParsed.failure.message) - * } - * // Output: Parsed relative URL: https://example.com/relative-path + * Result.map(relativeParsed, (url) => url.toString()) // => Result.succeed("https://example.com/relative-path") * ``` * * @category constructors @@ -133,7 +123,7 @@ export const fromString: { * * **Example** (Mutating URL credentials) * - * ```ts + * ```ts import.meta.vitest * import { Url } from "effect/unstable/http" * * const myUrl = new URL("https://example.com") @@ -143,11 +133,10 @@ export const fromString: { * url.password = "pass" * }) * - * console.log("Mutated:", mutatedUrl.toString()) - * // Output: Mutated: https://user:pass@example.com/ + * mutatedUrl.toString() // => "https://user:pass@example.com/" * ``` * - * @category modifiers + * @category transforming * @since 4.0.0 */ export const mutate: { @@ -295,7 +284,7 @@ export const setUsername: { * * **Example** (Replacing query parameters) * - * ```ts + * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/unstable/http" * * const myUrl = new URL("https://example.com?foo=bar") @@ -306,8 +295,7 @@ export const setUsername: { * UrlParams.fromInput([["key", "value"]]) * ) * - * console.log(updatedUrl.toString()) - * // Output: https://example.com/?key=value + * updatedUrl.toString() // => "https://example.com/?key=value" * ``` * * @category setters @@ -332,16 +320,15 @@ export const setUrlParams: { * * **Example** (Reading query parameters) * - * ```ts - * import { Url } from "effect/unstable/http" + * ```ts import.meta.vitest + * import { Url, UrlParams } from "effect/unstable/http" * * const myUrl = new URL("https://example.com?foo=bar") * * // Read parameters * const params = Url.urlParams(myUrl) * - * console.log(params) - * // Output: [ [ 'foo', 'bar' ] ] + * UrlParams.toString(params) // => "foo=bar" * ``` * * @category getters @@ -361,18 +348,17 @@ export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput( * * **Example** (Modifying query parameters) * - * ```ts + * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/unstable/http" * * 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 + * changedUrl.toString() // => "https://example.com/?foo=bar&key=value" * ``` * - * @category modifiers + * @category transforming * @since 4.0.0 */ export const modifyUrlParams: { diff --git a/.context/effect/packages/effect/src/unstable/http/UrlParams.ts b/.context/effect/packages/effect/src/unstable/http/UrlParams.ts index c2483a24e..d83dfcea2 100644 --- a/.context/effect/packages/effect/src/unstable/http/UrlParams.ts +++ b/.context/effect/packages/effect/src/unstable/http/UrlParams.ts @@ -16,6 +16,7 @@ import { dual } from "../../Function.ts" import * as Hash from "../../Hash.ts" import type { Inspectable } from "../../Inspectable.ts" import { PipeInspectableProto } from "../../internal/core.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Option from "../../Option.ts" import type { Pipeable } from "../../Pipeable.ts" import { hasProperty } from "../../Predicate.ts" @@ -236,15 +237,15 @@ export interface UrlParamsSchema extends Schema.declare ({ + runtime: "UrlParams.UrlParamsSchema", + Type: "UrlParams.UrlParams", + importDeclarations: [`import * as UrlParams from "effect/unstable/http/UrlParams"`] + }), expected: "UrlParams", toEquivalence: () => Equivalence, toCodec: () => @@ -463,9 +464,8 @@ export const toString = (input: Input): string => new URLSearchParams(fromInput( * * **Example** (Converting parameters to a record) * - * ```ts + * ```ts import.meta.vitest * import { UrlParams } from "effect/unstable/http" - * import * as assert from "node:assert" * * const urlParams = UrlParams.fromInput({ * a: 1, @@ -473,12 +473,7 @@ export const toString = (input: Input): string => new URLSearchParams(fromInput( * c: "string", * e: [1, 2, 3] * }) - * const result = UrlParams.toRecord(urlParams) - * - * assert.deepStrictEqual( - * result, - * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] } - * ) + * UrlParams.toRecord(urlParams) // => { a: "1", b: "true", c: "string", e: ["1", "2", "3"] } * ``` * * @category converting @@ -487,13 +482,15 @@ export const toString = (input: Input): string => new URLSearchParams(fromInput( export const toRecord = (self: UrlParams): Record> => { const out: Record> = {} for (const [k, value] of self.params) { - const curr = out[k] - if (curr === undefined) { - out[k] = value - } else if (typeof curr === "string") { - out[k] = [curr, value] + if (!Object.hasOwn(out, k)) { + InternalRecord.assignProperty(out, k, value) } else { - curr.push(value) + const current = out[k] + if (typeof current === "string") { + InternalRecord.assignProperty(out, k, [current, value]) + } else { + current.push(value) + } } } return out @@ -519,7 +516,7 @@ export const toReadonlyRecord: (self: UrlParams) => ReadonlyRecord {} +export interface schemaJsonField extends Schema.decodeTo, UrlParamsSchema> {} /** * Extracts a JSON value from the first occurrence of the given `field` in the @@ -527,7 +524,7 @@ export interface schemaJsonField extends Schema.decodeTo ["bar", 42] * ``` * * @category schemas @@ -589,7 +585,7 @@ export interface schemaRecord extends * * **Example** (Decoding URL parameters to a record) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { UrlParams } from "effect/unstable/http" * @@ -600,12 +596,11 @@ export interface schemaRecord extends * })) * ) * - * console.log( - * Schema.decodeSync(toStruct)(UrlParams.fromInput({ + * const decoded = Schema.decodeSync(toStruct)(UrlParams.fromInput({ * some: "value", * number: 42 * })) - * ) + * const result = [decoded.some, decoded.number] // => ["value", 42] * ``` * * @category schemas diff --git a/.context/effect/packages/effect/src/unstable/http/index.ts b/.context/effect/packages/effect/src/unstable/http/index.ts index 462637685..30cbeead0 100644 --- a/.context/effect/packages/effect/src/unstable/http/index.ts +++ b/.context/effect/packages/effect/src/unstable/http/index.ts @@ -127,7 +127,7 @@ export * as Multipart from "./Multipart.ts" /** * @since 4.0.0 */ -export * as Multipasta from "./Multipasta.ts" +export * as MultipartParser from "./MultipartParser.ts" /** * @since 4.0.0 diff --git a/.context/effect/packages/effect/src/unstable/http/internal/compression.ts b/.context/effect/packages/effect/src/unstable/http/internal/compression.ts new file mode 100644 index 000000000..0dcdf222b --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/internal/compression.ts @@ -0,0 +1,196 @@ +import * as Effect from "../../../Effect.ts" +import { identity } from "../../../Function.ts" +import * as Stream from "../../../Stream.ts" +import type * as Headers from "../Headers.ts" +import * as HttpBody from "../HttpBody.ts" +import type { Compression, CompressionAlgorithm, CompressionOptions } from "../HttpPlatform.ts" +import * as Response from "../HttpServerResponse.ts" + +/** @internal */ +export const varyAcceptEncoding = (headers: Headers.Headers): string | undefined => { + const vary = headers["vary"] + if (vary === undefined) { + return "Accept-Encoding" + } + const members = vary.split(",").map((member) => member.trim().toLowerCase()) + return members.includes("*") || members.includes("accept-encoding") + ? undefined + : `${vary}, Accept-Encoding` +} + +/** @internal */ +export const wrapCompression = (impl: Compression): Compression => ({ + algorithms: impl.algorithms, + compressResponse(response, algorithm, options) { + return Effect.map(impl.compressResponse(response, algorithm, options), (compressed) => { + if (compressed === response) { + return response + } + const headers: Record = { "content-encoding": algorithm } + const vary = varyAcceptEncoding(compressed.headers) + if (vary !== undefined) { + headers["vary"] = vary + } + const etag = compressed.headers["etag"] + if (etag !== undefined && !etag.startsWith("W/")) { + headers["etag"] = `W/${etag}` + } + return Response.setHeaders(compressed, headers) + }) + } +}) + +/** @internal */ +export const compressionTransformWeb = + (format: string) => (stream: ReadableStream): ReadableStream => + stream.pipeThrough( + new CompressionStream(format as CompressionFormat) as unknown as ReadableWritablePair + ) + +/** @internal */ +export const setBodyWithoutLength = ( + response: Response.HttpServerResponse, + body: HttpBody.HttpBody +): Response.HttpServerResponse => Response.removeHeader(Response.setBody(response, body), "content-length") + +/** @internal */ +export const makeCompressionWeb = (options: { + readonly algorithms: Iterable + readonly transform: ( + algorithm: CompressionAlgorithm, + options?: CompressionOptions | undefined + ) => (stream: ReadableStream) => ReadableStream +}): Compression => ({ + algorithms: new Set(options.algorithms), + compressResponse(response, algorithm, opts) { + const body = response.body + switch (body._tag) { + case "Uint8Array": { + const data = body.body + return Effect.succeed(streamBody( + response, + () => options.transform(algorithm, opts)(singleChunkStream(data)), + body.contentType + )) + } + case "Stream": { + const stream = body.stream + return Effect.succeed(streamBody( + response, + () => options.transform(algorithm, opts)(Stream.toReadableStream(stream)), + body.contentType + )) + } + case "Raw": { + const readable = rawReadableStream(body.body) + if (readable === undefined) { + return Effect.succeed(response) + } + return Effect.succeed(setBodyWithoutLength( + response, + HttpBody.raw(options.transform(algorithm, opts)(readable), { contentType: body.contentType }) + )) + } + default: { + return Effect.succeed(response) + } + } + } +}) + +const streamBody = ( + response: Response.HttpServerResponse, + evaluate: () => ReadableStream, + contentType: string | undefined +): Response.HttpServerResponse => + setBodyWithoutLength( + response, + HttpBody.stream(Stream.fromReadableStream({ evaluate, onError: identity }), contentType) + ) + +const singleChunkStream = (data: Uint8Array): ReadableStream => + new ReadableStream({ + start(controller) { + controller.enqueue(data) + controller.close() + } + }) + +const rawReadableStream = (raw: unknown): ReadableStream | undefined => { + if (typeof ReadableStream !== "undefined" && raw instanceof ReadableStream) { + return raw + } else if (raw instanceof globalThis.Response) { + return raw.body ?? undefined + } + return new globalThis.Response(raw as BodyInit).body ?? undefined +} + +/** @internal */ +export const compressionWeb: Compression = makeCompressionWeb({ + algorithms: ["gzip", "deflate"], + transform: (algorithm) => compressionTransformWeb(algorithm) +}) + +/** @internal */ +export const defaultCompressible = (contentType: string): boolean => { + const semi = contentType.indexOf(";") + const type = (semi === -1 ? contentType : contentType.slice(0, semi)).trim().toLowerCase() + if (type.startsWith("text/")) { + return true + } + switch (type) { + case "application/json": + case "application/javascript": + case "application/xml": + case "image/svg+xml": + case "application/wasm": { + return true + } + } + return type.endsWith("+json") || type.endsWith("+xml") +} + +const acceptMember = /^([a-z0-9!#$%&'*+.^_`|~-]+)(?:;q=(0(?:\.[0-9]{0,3})?|1(?:\.0{0,3})?))?$/ + +/** @internal */ +export const parseAcceptEncoding = (header: string): ReadonlyMap | undefined => { + const trimmed = header.trim() + if (trimmed === "") { + return undefined + } + const accepted = new Map() + for (const part of trimmed.split(",")) { + const member = part.trim().toLowerCase().replace(/[ \t]*;[ \t]*/g, ";") + const match = acceptMember.exec(member) + if (match === null) { + return undefined + } + accepted.set(match[1], match[2] === undefined ? 1 : Number(match[2])) + } + return accepted +} + +/** @internal */ +export const negotiate = ( + header: string | undefined, + preferred: ReadonlyArray, + supported: ReadonlySet +): CompressionAlgorithm | undefined => { + if (header === undefined) { + return undefined + } + const accepted = parseAcceptEncoding(header) + if (accepted === undefined) { + return undefined + } + for (const algorithm of preferred) { + if (!supported.has(algorithm)) { + continue + } + const quality = accepted.get(algorithm) ?? accepted.get("*") + if (quality !== undefined && quality > 0) { + return algorithm + } + } + return undefined +} diff --git a/.context/effect/packages/effect/src/unstable/http/internal/httpBody.ts b/.context/effect/packages/effect/src/unstable/http/internal/httpBody.ts new file mode 100644 index 000000000..e033e4415 --- /dev/null +++ b/.context/effect/packages/effect/src/unstable/http/internal/httpBody.ts @@ -0,0 +1,15 @@ +import * as Headers from "../Headers.ts" +import type * as HttpBody from "../HttpBody.ts" + +/** @internal */ +export const updateHeaders = (headers: Headers.Headers, body: HttpBody.HttpBody): Headers.Headers => { + if (body._tag === "Empty" || body._tag === "FormData") { + return Headers.remove(Headers.remove(headers, "content-type"), "content-length") + } + headers = body.contentType === undefined + ? Headers.remove(headers, "content-type") + : Headers.set(headers, "content-type", body.contentType) + return body.contentLength === undefined + ? Headers.remove(headers, "content-length") + : Headers.set(headers, "content-length", body.contentLength.toString()) +} diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts index a5497a12b..ab23c6e88 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApi.ts @@ -15,7 +15,6 @@ import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" import * as Record from "../../Record.ts" import type * as Schema from "../../Schema.ts" -import type * as SchemaAST from "../../SchemaAST.ts" import type { PathInput } from "../http/HttpRouter.ts" import * as HttpApiEndpoint from "./HttpApiEndpoint.ts" import type * as HttpApiGroup from "./HttpApiGroup.ts" @@ -145,7 +144,7 @@ const Proto = { ) { const groups = { ...this.groups } for (const group of toAdd) { - InternalRecord.set(groups, group.identifier, group) + InternalRecord.assignProperty(groups, group.identifier, group) } return makeProto({ ...optionsFromApi(this), @@ -157,9 +156,9 @@ const Proto = { api: Top ) { const newGroups = { ...this.groups } - for (const key in api.groups) { + for (const key of Object.keys(api.groups)) { const group = api.groups[key] - InternalRecord.set( + InternalRecord.assignProperty( newGroups, key, group.annotateMerge(Context.merge(api.annotations, group.annotations)) @@ -290,11 +289,11 @@ export const reflect = , - getStatus: (ast: SchemaAST.AST) => number + getStatus: (schema: Schema.Constraint) => number ): ReadonlyMap]> => { const map = new Map]>() @@ -314,9 +313,9 @@ const extractResponseContent = ( return map function add(schema: Schema.Top) { - if (HttpApiSchema.isStreamSchema(schema)) return - const ast = schema.ast - const status = getStatus(ast) + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + if (HttpApiSchema.isStreamSchema(body)) return + const status = getStatus(schema) const schemas = map.get(status) if (schemas === undefined) { map.set(status, [schema]) diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts index ff54fcb82..062dc839e 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiBuilder.ts @@ -18,7 +18,6 @@ import type { FileSystem } from "../../FileSystem.ts" import { identity } from "../../Function.ts" import { stringOrRedacted } from "../../internal/redacted.ts" import * as Layer from "../../Layer.ts" -import * as Option from "../../Option.ts" import type { Path } from "../../Path.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import { hasProperty } from "../../Predicate.ts" @@ -58,7 +57,7 @@ import * as OpenApi from "./OpenApi.ts" /** * Registers an `HttpApi` with a `HttpRouter`. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( @@ -724,10 +723,10 @@ function decodePayload( } try { return decode(JSON.parse(text)) - } catch (cause) { + } catch { return Effect.fail( new Schema.SchemaError( - new SchemaIssue.InvalidValue(Option.some(text), { message: `Invalid JSON: ${cause}` }) + new SchemaIssue.InvalidValue({ message: "Expected a valid JSON body" }) ) ) } @@ -759,8 +758,12 @@ function handlerToHttpEffect( const encodeError = Schema.encodeUnknownEffect(makeErrorSchema(endpoint)) const decodeParams = UndefinedOr.map(endpoint.params, Schema.decodeUnknownEffect) const decodeHeaders = UndefinedOr.map(endpoint.headers, Schema.decodeUnknownEffect) - const decodeQuery = UndefinedOr.map(endpoint.query, Schema.decodeUnknownEffect) + const decodeQuery = UndefinedOr.map( + endpoint.query, + (schema) => Schema.decodeUnknownEffect(Schema.toCodecArrayFromSingle(schema)) + ) const encodeStream = makeStreamEncoder(endpoint) + const encodeWithHeaders = makeWithHeadersEncoder(endpoint) const shouldParsePayload = endpoint.payload.size > 0 && !isRaw const payloadBy = shouldParsePayload ? buildPayloadDecoders(endpoint.payload) : undefined @@ -798,15 +801,26 @@ function handlerToHttpEffect( request.payload = yield* HttpApiSchemaError.wrap("Payload", result) } } - const response = yield* handler(request) + let response = yield* handler(request) if (Response.isHttpServerResponse(response)) { return response } - const streamResponse = encodeStream?.(response, context) - if (streamResponse !== undefined) { - return yield* HttpApiSchemaError.wrap("Body", streamResponse) + let responseHeaders: unknown | undefined + if (encodeWithHeaders !== undefined && HttpApiSchema.isWithHeadersValue(response)) { + responseHeaders = response.headers + response = response.body } - return yield* HttpApiSchemaError.wrap("Body", encodeSuccess(response)) + const encoded = yield* HttpApiSchemaError.wrap( + "Body", + encodeStream?.(response, context) ?? + (responseHeaders !== undefined ? encodeWithHeaders!.encodeBody(response) : encodeSuccess(response)) + ) + if (encodeWithHeaders === undefined || responseHeaders === undefined) return encoded + const encodedHeaders = yield* HttpApiSchemaError.wrap( + "ResponseHeaders", + encodeWithHeaders.encodeHeaders.get(encoded.status)!(responseHeaders) + ) + return Response.setHeaders(encoded, encodedHeaders as any) }) ).pipe( Effect.withErrorReporting, @@ -919,6 +933,37 @@ type StreamEncoder = (response: unknown, context: Context.Context) => | Effect.Effect | undefined +type WithHeadersEncoder = (headers: unknown) => Effect.Effect + +interface WithHeadersEncoders { + readonly encodeBody: (body: unknown) => Effect.Effect + readonly encodeHeaders: ReadonlyMap +} + +function makeWithHeadersEncoder(endpoint: HttpApiEndpoint.Top): WithHeadersEncoders | undefined { + const encodeHeaders = new Map() + const bodySchemas: Array> = [] + for (const schema of endpoint.success) { + if (!HttpApiSchema.isWithHeaders(schema)) continue + encodeHeaders.set(HttpApiSchema.getStatusSuccessSchema(schema), Schema.encodeUnknownEffect(schema.headers)) + if (!HttpApiSchema.isStreamSchema(schema.schema)) { + bodySchemas.push(toResponseSuccessSchema(schema)) + } + } + if (encodeHeaders.size === 0) return undefined + // Branded response bodies encode against the header-carrying members only, so + // the encoded status always has a matching header schema. + const bodySchema: Schema.ConstraintEncoder = bodySchemas.length === 0 + ? Schema.Never + : bodySchemas.length === 1 + ? bodySchemas[0] + : Schema.Union(bodySchemas) + return { + encodeBody: Schema.encodeUnknownEffect(bodySchema), + encodeHeaders + } +} + function makeStreamEncoder(endpoint: HttpApiEndpoint.Top): StreamEncoder | undefined { const streamSchema = getStreamSuccessSchema(endpoint) if (streamSchema === undefined) { @@ -933,7 +978,7 @@ function makeStreamEncoder(endpoint: HttpApiEndpoint.Top): StreamEncoder | undef return (response, context) => { if (!Stream.isStream(response)) { return hasBuffered ? undefined : new Schema.SchemaError( - new SchemaIssue.InvalidValue(Option.some(response), { message: "Expected a streaming response" }) + new SchemaIssue.InvalidValue({ message: "Expected a streaming response" }) ) } @@ -952,7 +997,7 @@ function makeStreamEncoder(endpoint: HttpApiEndpoint.Top): StreamEncoder | undef return (response, context) => { if (!Stream.isStream(response)) { return hasBuffered ? undefined : new Schema.SchemaError( - new SchemaIssue.InvalidValue(Option.some(response), { message: "Expected a streaming response" }) + new SchemaIssue.InvalidValue({ message: "Expected a streaming response" }) ) } @@ -968,15 +1013,17 @@ function makeStreamEncoder(endpoint: HttpApiEndpoint.Top): StreamEncoder | undef function getStreamSuccessSchema(endpoint: HttpApiEndpoint.Top) { for (const schema of endpoint.success) { - if (HttpApiSchema.isStreamSchema(schema)) { - return schema + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + if (HttpApiSchema.isStreamSchema(body)) { + return body } } } function hasBufferedSuccess(endpoint: HttpApiEndpoint.Top): boolean { for (const schema of endpoint.success) { - if (Schema.isSchema(schema) && !HttpApiSchema.isStreamSchema(schema)) return true + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + if (Schema.isSchema(body) && !HttpApiSchema.isStreamSchema(body)) return true } return endpoint.success.size === 0 } @@ -1041,8 +1088,31 @@ function renderSseEvent(event: Sse.EventEncoded) { }) } -const toResponseSuccessSchema = toResponseSchema(HttpApiSchema.getStatusSuccess) -const toResponseErrorSchema = toResponseSchema(HttpApiSchema.getStatusError) +const toResponseSuccessSchema = toResponseSchema(HttpApiSchema.getStatusSuccessSchema) +const toResponseErrorSchemaPlain = toResponseSchema(HttpApiSchema.getStatusErrorSchema) + +function toResponseErrorSchema( + schema: Schema.Constraint +): Schema.ConstraintEncoder { + if (!HttpApiSchema.isWithHeaders(schema)) return toResponseErrorSchemaPlain(schema) + + const encodeBody = Schema.encodeUnknownEffect(schema.schema) + const encodeHeaders = Schema.encodeUnknownEffect(schema.headers) + const encodeResponse = getResponseEncode( + HttpApiSchema.getStatusErrorSchema(schema), + HttpApiSchema.getResponseEncodingSchema(schema), + HttpApiSchema.isNoContent(schema.schema.ast) + ) + const transformation = withHeadersTransformation>( + (body, options) => + encodeBody(body, options).pipe( + Effect.mapError((error) => error.issue), + Effect.flatMap((body) => encodeResponse(body, options)) + ), + encodeHeaders + ) + return $HttpServerResponse.pipe(Schema.decodeTo(schema, transformation)) +} function makeSuccessSchema( endpoint: HttpApiEndpoint.Top @@ -1059,55 +1129,106 @@ function makeErrorSchema( return schemas.length === 1 ? schemas[0] : Schema.Union(schemas) } -function toResponseSchema(getStatus: (ast: SchemaAST.AST) => number) { - const cache = new WeakMap() +function toResponseSchema(getStatus: (schema: Schema.Constraint) => number) { + // WithHeaders wrappers share a single declaration AST, so they are cached by instance + const cache = new WeakMap() return (schema: Schema.Constraint): Schema.ConstraintEncoder => { - const cached = cache.get(schema.ast) + const key = HttpApiSchema.isWithHeaders(schema) ? schema : schema.ast + const cached = cache.get(key) if (cached !== undefined) { return cached as any } + const bodySchema = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema const responseSchema = $HttpServerResponse.pipe( - Schema.decodeTo(schema, getResponseTransformation(getStatus, schema)) + Schema.decodeTo(bodySchema, getResponseTransformation(getStatus, schema)) ) - cache.set(schema.ast, responseSchema) + cache.set(key, responseSchema) return responseSchema } } function getResponseTransformation( - getStatus: (ast: SchemaAST.AST) => number, + getStatus: (schema: Schema.Constraint) => number, schema: Schema.Constraint -): SchemaTransformation.Transformation { - const ast = schema.ast +): SchemaTransformation.Transformation { + const withHeaders = HttpApiSchema.getWithHeadersAnnotation(schema.ast) + if (withHeaders !== undefined) { + const encodeBody = getResponseEncode( + getStatus(withHeaders.body), + HttpApiSchema.getResponseEncoding(withHeaders.body.ast), + HttpApiSchema.isNoContent(withHeaders.body.ast) + ) + return withHeadersTransformation(encodeBody, Schema.encodeUnknownEffect(withHeaders.headersCodec)) + } + + const bodySchema = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema const encode = getResponseEncode( - getStatus(ast), - HttpApiSchema.getResponseEncoding(ast), - HttpApiSchema.isNoContent(ast) + getStatus(schema), + HttpApiSchema.getResponseEncodingSchema(schema), + HttpApiSchema.isNoContent(bodySchema.ast) ) return SchemaTransformation.transformOrFail({ - decode: (res) => Effect.fail(new SchemaIssue.Forbidden(Option.some(res), { message: "Encode only schema" })), + decode: (input, options) => + Effect.fail( + new SchemaIssue.Forbidden({ message: "Encode only schema" }, input, options) + ), encode }) } +function withHeadersTransformation( + encodeBody: ( + body: unknown, + options?: SchemaAST.ParseOptions + ) => Effect.Effect, + encodeHeaders: ( + headers: unknown, + options?: SchemaAST.ParseOptions + ) => Effect.Effect +): SchemaTransformation.Transformation { + return SchemaTransformation.transformOrFail({ + decode: (input, options) => + Effect.fail( + new SchemaIssue.Forbidden({ message: "Encode only schema" }, input, options) + ), + encode: (value, options) => { + const pair = value as { readonly body: unknown; readonly headers: unknown } + return Effect.flatMap(encodeBody(pair.body, options), (response) => + Effect.map( + encodeHeaders(pair.headers, options).pipe(Effect.mapError((error) => error.issue)), + (headers) => Response.setHeaders(response, headers as any) + )) + } + }) +} + function getResponseEncode( status: number, encoding: HttpApiSchema.ResponseEncoding, isNoContent: boolean -): (e: E) => Effect.Effect { +): ( + e: E, + options?: SchemaAST.ParseOptions +) => Effect.Effect { switch (encoding._tag) { case "Json": { - return ((e) => { + return ((e, options) => { if (e === undefined || isNoContent) { return Effect.succeed(Response.empty({ status })) } try { const s = JSON.stringify(e) return Effect.succeed(Response.text(s, { status, contentType: encoding.contentType })) - } catch (error) { - return Effect.fail(new SchemaIssue.InvalidValue(Option.some(e), { message: globalThis.String(error) })) + } catch { + return Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a JSON-serializable response body" }, + e, + options + ) + ) } }) } diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts index 2c3c08211..365329af4 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiClient.ts @@ -16,7 +16,6 @@ import type * as Context from "../../Context.ts" import * as Effect from "../../Effect.ts" import { identity } from "../../Function.ts" import * as InternalRecord from "../../internal/record.ts" -import * as Option from "../../Option.ts" import * as Predicate from "../../Predicate.ts" import * as Schema from "../../Schema.ts" import * as SchemaAST from "../../SchemaAST.ts" @@ -62,22 +61,26 @@ export type Client * Derives the typed client interface for an `HttpApi`, preserving any additional * client error and service requirements supplied by the caller. * - * @category models + * @category utility types * @since 4.0.0 */ export type ForApi = Api extends HttpApi.HttpApi ? Client : never -type SuccessType = S extends HttpApiSchema.StreamSse< - infer _Events, - infer _Error, - infer _Value -> ? Stream.Stream< - _Value, - _Error["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry, - never - > +type SuccessType = S extends HttpApiSchema.WithHeaders< + infer _Inner, + infer _Headers +> ? HttpApiSchema.withHeaders, _Headers["Type"]> + : S extends HttpApiSchema.StreamSse< + infer _Events, + infer _Error, + infer _Value + > ? Stream.Stream< + _Value, + _Error["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError, + never + > : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream : S extends Schema.Constraint ? S["Type"] : never @@ -367,10 +370,11 @@ export const makeClient = group.identifier === options.group, onEndpoint({ endpoint, endpointFn }) { - InternalRecord.set(client, endpoint.identifier, endpointFn) + InternalRecord.assignProperty(client, endpoint.identifier, endpointFn) } }).pipe(Effect.map(() => client)) as any } @@ -629,7 +633,7 @@ export const endpoint = < * * **Example** (Building typed URLs) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" * @@ -647,8 +651,7 @@ export const endpoint = < * * buildUrl.users.getUser({ * params: { id: "123" } - * }) - * //=> "https://api.example.com/users/123" + * }) // => "https://api.example.com/users/123" * ``` * * @category constructors @@ -662,7 +665,7 @@ export const urlBuilder = (api: Api, options?: { HttpApi.reflect(api as unknown as HttpApi.Top, { onGroup({ group }) { if (group.topLevel) return - InternalRecord.set(builder, group.identifier, {}) + InternalRecord.assignProperty(builder, group.identifier, {}) }, onEndpoint({ group, endpoint }) { const makeUrl = compilePath(endpoint.path) @@ -688,7 +691,7 @@ export const urlBuilder = (api: Api, options?: { const url = query === "" ? path : `${path}?${query}` return options?.baseUrl === undefined ? url : new URL(url, options.baseUrl.toString()).toString() } - InternalRecord.set( + InternalRecord.assignProperty( group.topLevel ? builder : builder[group.identifier], endpoint.identifier, endpointBuilder @@ -724,9 +727,44 @@ const compilePath = (path: string) => { } function schemasToResponse(schemas: readonly [Schema.Constraint, ...Array]) { - const codec = toCodecArrayBuffer(schemas) + const hasWithHeaders = schemas.some((schema) => + HttpApiSchema.isWithHeaders(schema) || HttpApiSchema.getWithHeadersAnnotation(schema.ast) !== undefined + ) + const codec = hasWithHeaders + ? Schema.Union(schemas.map(toCodecArrayBufferWithHeaders)) + : toCodecArrayBuffer(schemas) const decode = Schema.decodeEffect(codec) - return (response: HttpClientResponse.HttpClientResponse) => Effect.flatMap(response.arrayBuffer, decode) + return (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + response.arrayBuffer, + hasWithHeaders + ? (body) => decode({ body, headers: response.headers }) + : decode + ) +} + +function toCodecArrayBufferWithHeaders(schema: Schema.Constraint): Schema.Top { + const isWithHeaders = HttpApiSchema.isWithHeaders(schema) + const annotation = HttpApiSchema.getWithHeadersAnnotation(schema.ast) + if (annotation !== undefined) { + return Schema.Struct({ + body: fromArrayBuffer(annotation.body), + headers: annotation.headersCodec + }).pipe(Schema.decodeTo(schema)) + } + const body = isWithHeaders ? schema.schema : schema + return Schema.Struct({ + body: fromArrayBuffer(body).pipe(Schema.decodeTo(body)), + headers: isWithHeaders ? schema.headers : Schema.Unknown + }).pipe( + Schema.decodeTo( + isWithHeaders ? schema : Schema.toType(schema), + SchemaTransformation.transform({ + decode: (value) => isWithHeaders ? HttpApiSchema.withHeaders(value) : value.body, + encode: (value: any) => isWithHeaders ? value : { body: value, headers: undefined } + }) as any + ) + ) } type ResponseDecoder = (response: HttpClientResponse.HttpClientResponse) => Effect.Effect @@ -770,9 +808,10 @@ function groupSchemasByContentType( ): Map> { const grouped = new Map]>() for (const schema of schemas) { - const contentType = HttpApiSchema.isNoContent(schema.ast) + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + const contentType = HttpApiSchema.isNoContent(body.ast) ? "" - : MediaType.normalize(HttpApiSchema.getResponseEncoding(schema.ast).contentType) + : MediaType.normalize(HttpApiSchema.getResponseEncodingSchema(schema).contentType) const existing = grouped.get(contentType) if (existing === undefined) { grouped.set(contentType, [schema]) @@ -804,24 +843,35 @@ function failUnsupportedContentType( const reservedStreamFailureEvent = "effect/httpapi/stream/failure" -function getStreamSuccessSchemas(endpoint: HttpApiEndpoint.Top): Array { - const schemas: Array = [] +type StreamSuccessSchema = + | HttpApiSchema.StreamSchema + | HttpApiSchema.WithHeaders + +const isWithHeadersStreamSuccess = ( + schema: StreamSuccessSchema +): schema is HttpApiSchema.WithHeaders => HttpApiSchema.isWithHeaders(schema) + +function getStreamSuccessSchemas(endpoint: HttpApiEndpoint.Top): Array { + const schemas: Array = [] for (const schema of endpoint.success) { - if (HttpApiSchema.isStreamSchema(schema)) { - schemas.push(schema) + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + if (HttpApiSchema.isStreamSchema(body)) { + schemas.push(schema as StreamSuccessSchema) } } return schemas } -function streamToResponse(streamSchema: HttpApiSchema.StreamSchema) { +function streamToResponse(successSchema: StreamSuccessSchema) { + const isWithHeaders = isWithHeadersStreamSuccess(successSchema) + const streamSchema = isWithHeaders ? successSchema.schema : successSchema const sse = HttpApiSchema.isStreamUint8Array(streamSchema) ? undefined : { declaration: streamSchema, decoder: makeSseDecoder(streamSchema) } - return (response: HttpClientResponse.HttpClientResponse) => + const toStream = (response: HttpClientResponse.HttpClientResponse) => Effect.map(Effect.context(), (context) => Stream.provideContext( sse === undefined ? @@ -829,6 +879,14 @@ function streamToResponse(streamSchema: HttpApiSchema.StreamSchema) { decodeSseStream(response.stream, sse.declaration, sse.decoder), context as Context.Context )) + if (!isWithHeaders) return toStream + + const decodeHeaders = Schema.decodeUnknownEffect(successSchema.headers) + return (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + decodeHeaders(response.headers), + (headers) => Effect.map(toStream(response), (body) => HttpApiSchema.withHeaders({ body, headers })) + ) } function makeSseDecoder( @@ -942,31 +1000,34 @@ function toCodecArrayBuffer(schemas: readonly [Schema.Constraint, ...Array a === undefined ? null : a, encode: (a) => a === null ? undefined : a - }) as any : - undefined - )) - } - case "FormUrlEncoded": - return StringFromArrayBuffer.pipe( - Schema.decodeTo(UrlParams.schemaRecord), - Schema.decodeTo(schema) + }) as any + ) ) - case "Uint8Array": - return Uint8ArrayFromArrayBuffer.pipe(Schema.decodeTo(schema)) - case "Text": - return StringFromArrayBuffer.pipe(Schema.decodeTo(schema)) + : UnknownFromArrayBuffer } + case "FormUrlEncoded": + return StringFromArrayBuffer.pipe(Schema.decodeTo(UrlParams.schemaRecord)) + case "Uint8Array": + return Uint8ArrayFromArrayBuffer + case "Text": + return StringFromArrayBuffer } } @@ -1004,39 +1065,59 @@ function getEncodePayloadSchemaFromBody( const out = $HttpBody.pipe(Schema.decodeTo( schema, SchemaTransformation.transformOrFail({ - decode(httpBody) { - return Effect.fail(new SchemaIssue.Forbidden(Option.some(httpBody), { message: "Encode only schema" })) + decode(input, options) { + return Effect.fail( + new SchemaIssue.Forbidden({ message: "Encode only schema" }, input, options) + ) }, - encode(t) { + encode(t, options) { switch (encoding._tag) { case "Multipart": - return Effect.fail(new SchemaIssue.Forbidden(Option.some(t), { message: "Payload must be a FormData" })) + return Effect.fail( + new SchemaIssue.Forbidden( + { message: "Payload must be a FormData" }, + t, + options + ) + ) case "Json": { try { const body = JSON.stringify(t) return Effect.succeed(HttpBody.text(body, encoding.contentType)) - } catch (error) { - return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), { message: globalThis.String(error) })) + } catch { + return Effect.fail( + new SchemaIssue.InvalidValue( + { expected: "a JSON-serializable request body" }, + t, + options + ) + ) } } case "Text": { if (typeof t !== "string") { return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(t), { message: "Expected a string" }) + new SchemaIssue.InvalidValue({ message: "Expected a string" }, t, options) ) } return Effect.succeed(HttpBody.text(t, encoding.contentType)) } case "FormUrlEncoded": { if (!Predicate.isObject(t)) { - return Effect.fail(new SchemaIssue.InvalidValue(Option.some(t), { message: "Expected a record" })) + return Effect.fail( + new SchemaIssue.InvalidValue({ message: "Expected a record" }, t, options) + ) } return Effect.succeed(HttpBody.urlParams(UrlParams.fromInput(t as any), encoding.contentType)) } case "Uint8Array": { if (!(t instanceof Uint8Array)) { return Effect.fail( - new SchemaIssue.InvalidValue(Option.some(t), { message: "Expected a Uint8Array" }) + new SchemaIssue.InvalidValue( + { message: "Expected a Uint8Array" }, + t, + options + ) ) } return Effect.succeed(HttpBody.uint8Array(t, encoding.contentType)) diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts index d5b4a89e9..331b4ff84 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiEndpoint.ts @@ -44,11 +44,15 @@ const TypeId = "~effect/httpapi/HttpApiEndpoint" */ export const isHttpApiEndpoint = (u: unknown): u is Top => Predicate.hasProperty(u, TypeId) -type SuccessType = S extends HttpApiSchema.StreamSse< - infer _Events, - infer _Error, - infer _Value -> ? Stream.Stream<_Value, _Error["Type"], never> +type SuccessType = S extends HttpApiSchema.WithHeaders< + infer _Inner, + infer _Headers +> ? HttpApiSchema.withHeaders, _Headers["Type"]> + : S extends HttpApiSchema.StreamSse< + infer _Events, + infer _Error, + infer _Value + > ? Stream.Stream<_Value, _Error["Type"], never> : S extends HttpApiSchema.StreamUint8Array ? Stream.Stream : S extends Schema.Constraint ? S["Type"] : never @@ -75,15 +79,24 @@ type UnwrapReadonlyArray = S extends ReadonlyArray ? A : S type ExtractBufferedSuccess = Exclude< Extract, Schema.Top>, - HttpApiSchema.StreamSchema + HttpApiSchema.StreamSchema | HttpApiSchema.WithHeaders > type ExtractStreamSuccess = UnwrapReadonlyArray extends infer Success ? Success extends HttpApiSchema.StreamSchema ? Success : never : never -type ToSuccessCodec = [ExtractBufferedSuccess] extends [never] ? ExtractStreamSuccess - : Schema.toCodecJson> | ExtractStreamSuccess +type ExtractWithHeadersSuccess = UnwrapReadonlyArray extends infer Success ? + Success extends HttpApiSchema.WithHeaders ? HttpApiSchema.WithHeaders< + _Inner extends HttpApiSchema.StreamSchema ? _Inner : Schema.toCodecJson<_Inner>, + Schema.toCodecStringTree<_Headers> + > : + never + : never + +type ToSuccessCodec = [ExtractBufferedSuccess] extends [never] ? + ExtractStreamSuccess | ExtractWithHeadersSuccess + : Schema.toCodecJson> | ExtractStreamSuccess | ExtractWithHeadersSuccess type ToJsonCodec = [S] extends [never] ? never : [S] extends [Schema.Constraint] ? Schema.toCodecJson @@ -334,7 +347,7 @@ export interface Top extends /** * Extracts the endpoint identifier literal from an `HttpApiEndpoint`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Identifier = Endpoint extends Constraint ? Endpoint["identifier"] : never @@ -342,7 +355,7 @@ export type Identifier = Endpoint extends Constraint ? Endpoint["ident /** * Extracts the success schema associated with an endpoint. * - * @category models + * @category utility types * @since 4.0.0 */ export type Success = Endpoint extends Constraint ? Endpoint["~Success"] : never @@ -350,7 +363,7 @@ export type Success = Endpoint extends Constraint ? Endpoint["~Success /** * Extracts the error schema associated with an endpoint. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = Endpoint extends Constraint ? Endpoint["~Error"] : never @@ -358,7 +371,7 @@ export type Error = Endpoint extends Constraint ? Endpoint["~Error"] : /** * Extracts the schema used for an endpoint's path parameters. * - * @category models + * @category utility types * @since 4.0.0 */ export type Params = Endpoint extends ConstraintRequest ? Endpoint["~Params"] @@ -367,7 +380,7 @@ export type Params = Endpoint extends ConstraintRequest ? Endpoint["~P /** * Extracts the schema used for an endpoint's query parameters. * - * @category models + * @category utility types * @since 4.0.0 */ export type Query = Endpoint extends ConstraintRequest ? Endpoint["~Query"] @@ -376,7 +389,7 @@ export type Query = Endpoint extends ConstraintRequest ? Endpoint["~Qu /** * Extracts the schema used for an endpoint's request payload. * - * @category models + * @category utility types * @since 4.0.0 */ export type Payload = Endpoint extends ConstraintRequest ? Endpoint["~Payload"] @@ -385,7 +398,7 @@ export type Payload = Endpoint extends ConstraintRequest ? Endpoint["~ /** * Extracts the schema used for an endpoint's request headers. * - * @category models + * @category utility types * @since 4.0.0 */ export type Headers = Endpoint extends ConstraintRequest ? Endpoint["~Headers"] @@ -394,7 +407,7 @@ export type Headers = Endpoint extends ConstraintRequest ? Endpoint["~ /** * Extracts the middleware identifiers attached to an endpoint. * - * @category models + * @category utility types * @since 4.0.0 */ export type Middleware = Endpoint extends { readonly "~Middleware": infer M } ? M @@ -403,7 +416,7 @@ export type Middleware = Endpoint extends { readonly "~Middleware": in /** * Computes the services provided by the middleware attached to an endpoint. * - * @category models + * @category utility types * @since 4.0.0 */ export type MiddlewareProvides = HttpApiMiddleware.Provides> @@ -411,7 +424,7 @@ export type MiddlewareProvides = HttpApiMiddleware.Provides = HttpApiMiddleware.MiddlewareClient> @@ -420,7 +433,7 @@ export type MiddlewareClient = HttpApiMiddleware.MiddlewareClient = HttpApiMiddleware.Error> @@ -429,7 +442,7 @@ export type MiddlewareError = HttpApiMiddleware.Error = Endpoint extends ConstraintRequest ? @@ -440,7 +453,7 @@ export type Errors = Endpoint extends ConstraintRequest ? * Computes the services required to encode an endpoint's error responses, * including services required by middleware error encoders. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesEncode = Endpoint extends ConstraintRequest ? @@ -453,7 +466,7 @@ export type ErrorServicesEncode = Endpoint extends ConstraintRequest ? * available params, query, payload, headers, the raw request, endpoint, and group. * Multipart stream payloads are exposed as streams of parts. * - * @category models + * @category utility types * @since 4.0.0 */ export type Request = Endpoint extends ConstraintRequest ? Endpoint["~Request"] @@ -464,7 +477,7 @@ export type Request = Endpoint extends ConstraintRequest ? Endpoint["~ * params, query, and headers plus the raw request, endpoint, and group, while * leaving payload handling to the raw request. * - * @category models + * @category utility types * @since 4.0.0 */ export type RequestRaw = Endpoint extends ConstraintRequest ? Endpoint["~RequestRaw"] @@ -475,7 +488,7 @@ export type RequestRaw = Endpoint extends ConstraintRequest ? Endpoint * the params, query, headers, payload, and response mode fields required by the * endpoint. Multipart payloads are supplied as `FormData`. * - * @category models + * @category utility types * @since 4.0.0 */ export type ClientRequest< @@ -511,7 +524,7 @@ export type ClientResponseMode = "decoded-only" | "decoded-and-response" | "resp * Computes the services required on the server to decode endpoint inputs and * encode endpoint success, error, and middleware error responses. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServerServices = Endpoint extends ConstraintRequest ? @@ -528,7 +541,7 @@ export type ServerServices = Endpoint extends ConstraintRequest ? * Computes the services required on the client to encode endpoint requests and * decode endpoint success or error responses. * - * @category models + * @category utility types * @since 4.0.0 */ export type ClientServices = Endpoint extends ConstraintRequest ? @@ -543,7 +556,7 @@ export type ClientServices = Endpoint extends ConstraintRequest ? /** * Extracts the additional services required by middleware applied to an endpoint. * - * @category models + * @category utility types * @since 4.0.0 */ export type MiddlewareServices = Endpoint extends { readonly "~MiddlewareServices": infer R } ? R @@ -553,7 +566,7 @@ export type MiddlewareServices = Endpoint extends { readonly "~Middlew * Computes the services required to decode an endpoint's error responses, * including services required by middleware error decoders. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesDecode = Endpoint extends ConstraintRequest ? @@ -565,7 +578,7 @@ export type ErrorServicesDecode = Endpoint extends ConstraintRequest ? * The normal server handler for an endpoint, accepting the decoded request shape * and returning either the endpoint success value or a custom `HttpServerResponse`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Handler = ( @@ -576,7 +589,7 @@ export type Handler = ( * The raw server handler for an endpoint, receiving a request shape without a * decoded payload so the handler can read the raw `HttpServerRequest` directly. * - * @category models + * @category utility types * @since 4.0.0 */ export type HandlerRaw = ( @@ -586,7 +599,7 @@ export type HandlerRaw = ( /** * Selects the endpoint with the specified identifier from a union of endpoints. * - * @category models + * @category utility types * @since 4.0.0 */ export type WithIdentifier = Extract< @@ -597,7 +610,7 @@ export type WithIdentifier = Extract< /** * Removes endpoints with the specified identifier from a union of endpoints. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExcludeIdentifier = Exclude< @@ -609,7 +622,7 @@ export type ExcludeIdentifier = Exclude< * Derives the normal handler type for the endpoint with the specified identifier * in an endpoint union. * - * @category models + * @category utility types * @since 4.0.0 */ export type HandlerWithIdentifier = Handler< @@ -622,7 +635,7 @@ export type HandlerWithIdentifier = HandlerRaw< @@ -635,7 +648,7 @@ export type HandlerRawWithIdentifier = Success< @@ -646,7 +659,7 @@ export type SuccessWithIdentifier = Errors< @@ -657,7 +670,7 @@ export type ErrorsWithIdentifier = ServerServices< @@ -668,7 +681,7 @@ export type ServerServicesWithIdentifier = Middleware< @@ -679,7 +692,7 @@ export type MiddlewareWithIdentifier = @@ -689,7 +702,7 @@ export type MiddlewareServicesWithIdentifier = ExcludeProvided< @@ -701,7 +714,7 @@ export type ExcludeProvidedWithIdentifier = Exclude< @@ -714,7 +727,7 @@ export type ExcludeProvided = Exclude< * Returns an endpoint type with the supplied path prefix prepended while * preserving the endpoint's schemas, method, errors, and middleware. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddPrefix = Endpoint extends HttpApiEndpoint< @@ -748,7 +761,7 @@ export type AddPrefix = Endpoint * Returns an endpoint type with additional middleware applied and the endpoint's * middleware service requirements updated accordingly. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddMiddleware = Endpoint extends HttpApiEndpoint< @@ -869,7 +882,7 @@ function makeProto< * Constraint for path parameter schemas: each parameter must encode to * `string | undefined`, or the schema must encode to a record of those values. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type ParamsConstraint = @@ -880,7 +893,7 @@ export type ParamsConstraint = * Constraint for header schemas: each header must encode to `string | undefined`, * or the schema must encode to a record of those values. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type HeadersConstraint = @@ -891,7 +904,7 @@ export type HeadersConstraint = * Constraint for query schemas: each field must encode to `string`, an array of * strings, or `undefined`, or the schema must encode to a record of those values. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type QueryConstraint = @@ -906,7 +919,7 @@ export type QueryConstraint = * - for body methods, payload may be any `Schema.Top` (or content-type keyed * schemas) and OpenAPI uses `requestBody` instead of `parameters` * - * @category constraints + * @category utility types * @since 4.0.0 */ export type PayloadConstraint = Method extends HttpMethod.NoBody ? Record< @@ -920,7 +933,7 @@ export type PayloadConstraint = Method extends HttpMe * accept field records for query-style encoding, while body methods accept one or * more schemas. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type PayloadConstraintCodecs = Method extends HttpMethod.NoBody ? @@ -931,7 +944,7 @@ export type PayloadConstraintCodecs = Method extends * Constraint for success response schemas, allowing either a single schema or a * readonly array of schemas. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type SuccessConstraint = Schema.Top | ReadonlyArray @@ -940,14 +953,16 @@ export type SuccessConstraint = Schema.Top | ReadonlyArray * Constraint for error response schemas, allowing either a single schema or a * readonly array of schemas. * - * @category constraints + * @category utility types * @since 4.0.0 */ export type ErrorConstraint = Schema.Top | ReadonlyArray +type ErrorSchema = S extends HttpApiSchema.WithHeaders ? Inner : S + type ErrorNoStream = [ Extract< - S extends ReadonlyArray ? S[number] : S, + ErrorSchema ? S[number] : S>, HttpApiSchema.StreamSchema > ] extends [never] ? S : never @@ -1139,11 +1154,18 @@ function getSuccessResponse( if (success === undefined) return new Set() const schemas = Arr.ensure(success) validateSuccessResponse(schemas, method) - return new Set( - disableCodecs ? - schemas : - schemas.map((schema) => HttpApiSchema.isStreamSchema(schema) ? schema : transformResponse(schema)) - ) + return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) +} + +function transformResponseSchema(schema: Schema.Top): Schema.Top { + if (HttpApiSchema.isStreamSchema(schema)) return schema + if (HttpApiSchema.isWithHeaders(schema)) { + const inner = HttpApiSchema.isStreamSchema(schema.schema) + ? schema.schema + : applyResponseEncoding(schema.schema, HttpApiSchema.getResponseEncodingSchema(schema)) + return HttpApiSchema.rebuildWithHeaders(schema, inner, Schema.toCodecStringTree(schema.headers)) + } + return transformResponse(schema) } function getErrorResponse( @@ -1153,14 +1175,17 @@ function getErrorResponse( if (error === undefined) return new Set() const schemas = Arr.ensure(error) for (const schema of schemas) { - if (HttpApiSchema.isStreamSchema(schema)) { + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + if (HttpApiSchema.isStreamSchema(body)) { throw new Error("Streaming schemas are not supported in error responses") } } - return new Set(disableCodecs ? schemas : schemas.map(transformResponse)) + validateResponseExclusivity(schemas, HttpApiSchema.getStatusErrorSchema) + return new Set(disableCodecs ? schemas : schemas.map(transformResponseSchema)) } function validateSuccessResponse(schemas: ReadonlyArray, method: HttpMethod) { + let hasStream = false const statuses = new Map @@ -1168,31 +1193,32 @@ function validateSuccessResponse(schemas: ReadonlyArray, meth }>() for (const schema of schemas) { - if (HttpApiSchema.isStreamSchema(schema)) { - validateStreamSuccess(schema, method) - const status = HttpApiSchema.getStatusStream(schema) - const entry = getStatusEntry(statuses, status) - if (entry.stream !== undefined) { - throw new Error(`Multiple streaming success responses for status: ${status}`) + const inner = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema + const status = HttpApiSchema.getStatusSuccessSchema(schema) + if (HttpApiSchema.isStreamSchema(inner)) { + validateStreamSuccess(inner, method) + if (hasStream) { + throw new Error("Multiple streaming success responses are not supported") } + hasStream = true + const entry = getStatusEntry(statuses, status) if (entry.noContent) { throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`) } - if (entry.bufferedContentTypes.has(MediaType.normalize(schema.contentType))) { + if (entry.bufferedContentTypes.has(MediaType.normalize(inner.contentType))) { throw new Error( - `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${schema.contentType}` + `Cannot combine buffered and streaming success responses for status ${status} and content-type: ${inner.contentType}` ) } - statuses.set(status, { ...entry, stream: schema }) + statuses.set(status, { ...entry, stream: inner }) } else { - const status = HttpApiSchema.getStatusSuccess(schema.ast) const entry = getStatusEntry(statuses, status) - const noContent = HttpApiSchema.isNoContent(schema.ast) + const noContent = HttpApiSchema.isNoContent(inner.ast) if (entry.stream !== undefined) { if (noContent) { throw new Error(`Cannot combine no-content and streaming success responses for status: ${status}`) } - const encoding = HttpApiSchema.getResponseEncoding(schema.ast) + const encoding = HttpApiSchema.getResponseEncodingSchema(schema) if ( MediaType.normalize(encoding.contentType) === MediaType.normalize(entry.stream.contentType) ) { @@ -1203,12 +1229,14 @@ function validateSuccessResponse(schemas: ReadonlyArray, meth } if (!noContent) { entry.bufferedContentTypes.add( - MediaType.normalize(HttpApiSchema.getResponseEncoding(schema.ast).contentType) + MediaType.normalize(HttpApiSchema.getResponseEncodingSchema(schema).contentType) ) } entry.noContent = entry.noContent || noContent } } + + validateResponseExclusivity(schemas, HttpApiSchema.getStatusSuccessSchema) } function getStatusEntry( @@ -1227,6 +1255,49 @@ function getStatusEntry( return entry } +function validateResponseExclusivity( + schemas: ReadonlyArray, + getStatus: (schema: Schema.Constraint) => number +) { + const statuses = new Map + }>() + for (const schema of schemas) { + const status = getStatus(schema) + const withHeadersAnnotation = HttpApiSchema.getWithHeadersAnnotation(schema.ast) + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : withHeadersAnnotation?.body ?? schema + const contentType = HttpApiSchema.isNoContent(body.ast) + ? "" + : MediaType.normalize( + HttpApiSchema.isStreamSchema(body) + ? body.contentType + : HttpApiSchema.getResponseEncodingSchema(schema).contentType + ) + let entry = statuses.get(status) + if (entry === undefined) { + entry = { headerContentType: undefined, plainContentTypes: new Set() } + statuses.set(status, entry) + } + const combineError = () => + new Error( + `Cannot combine a response with headers with another response for status ${status} and content-type: ${ + contentType || "" + }` + ) + if (HttpApiSchema.isWithHeaders(schema) || withHeadersAnnotation !== undefined) { + if (entry.headerContentType !== undefined) { + throw new Error(`Cannot declare multiple responses with headers for status ${status}`) + } + if (entry.plainContentTypes.has(contentType)) throw combineError() + entry.headerContentType = contentType + } else { + if (entry.headerContentType === contentType) throw combineError() + entry.plainContentTypes.add(contentType) + } + } +} + function validateStreamSuccess(schema: HttpApiSchema.StreamSchema, method: HttpMethod) { if (method === "HEAD") { throw new Error("HEAD endpoints cannot declare streaming success responses") @@ -1275,6 +1346,23 @@ function hasReservedEventLiteral(ast: AST.AST, seen: Set): boolean { function transformResponse(schema: Schema.Top): Schema.Top { const encoding = HttpApiSchema.getResponseEncoding(schema.ast) + const withHeaders = HttpApiSchema.getWithHeadersAnnotation(schema.ast) + if (withHeaders === undefined) { + return applyResponseEncoding(schema, encoding) + } + const headers = Schema.toEncoded(withHeaders.headers) + return Schema.Struct({ + body: applyResponseEncoding(Schema.toEncoded(withHeaders.body), encoding), + headers + }).pipe(Schema.decodeTo(schema)).annotate({ + "~httpApiWithHeaders": { + ...withHeaders, + headersCodec: Schema.toCodecStringTree(headers) + } + }) +} + +function applyResponseEncoding(schema: Schema.Top, encoding: HttpApiSchema.ResponseEncoding): Schema.Top { switch (encoding._tag) { case "Json": return Schema.toCodecJson(schema) diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiError.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiError.ts index 33b40c07e..20f90066d 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiError.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiError.ts @@ -1,7 +1,7 @@ /** * Built-in error schemas for common HTTP API failure responses. * - * This module provides reusable `Schema.ErrorClass` values for common HTTP + * This module provides reusable `Schema.Error` values for common HTTP * status codes, plus `HttpApiSchemaError` for request decoding failures raised * by the HTTP API runtime. The status errors can be used in endpoint or * middleware error declarations and are understood by builders, generated @@ -39,7 +39,7 @@ const serviceUnavailableResponse = HttpServerResponse.empty({ status: 503 }) * @category errors * @since 4.0.0 */ -export class BadRequest extends Schema.ErrorClass("effect/HttpApiError/BadRequest")({ +export class BadRequest extends Schema.Error("effect/HttpApiError/BadRequest")({ _tag: Schema.tag("BadRequest") }, { description: "BadRequest", @@ -56,7 +56,7 @@ export class BadRequest extends Schema.ErrorClass("effect/HttpApiErr * No-content schema variant for `BadRequest`, decoding an empty 400 response into * a `BadRequest` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const BadRequestNoContent = BadRequest.pipe(HttpApiSchema.asNoContent({ @@ -70,7 +70,7 @@ export const BadRequestNoContent = BadRequest.pipe(HttpApiSchema.asNoContent({ * @category errors * @since 4.0.0 */ -export class Unauthorized extends Schema.ErrorClass("effect/HttpApiError/Unauthorized")({ +export class Unauthorized extends Schema.Error("effect/HttpApiError/Unauthorized")({ _tag: Schema.tag("Unauthorized") }, { description: "Unauthorized", @@ -86,7 +86,7 @@ export class Unauthorized extends Schema.ErrorClass("effect/HttpAp * No-content schema variant for `Unauthorized`, decoding an empty 401 response * into an `Unauthorized` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const UnauthorizedNoContent = Unauthorized.pipe(HttpApiSchema.asNoContent({ @@ -100,7 +100,7 @@ export const UnauthorizedNoContent = Unauthorized.pipe(HttpApiSchema.asNoContent * @category errors * @since 4.0.0 */ -export class Forbidden extends Schema.ErrorClass("effect/HttpApiError/Forbidden")({ +export class Forbidden extends Schema.Error("effect/HttpApiError/Forbidden")({ _tag: Schema.tag("Forbidden") }, { description: "Forbidden", @@ -116,7 +116,7 @@ export class Forbidden extends Schema.ErrorClass("effect/HttpApiError * No-content schema variant for `Forbidden`, decoding an empty 403 response into a * `Forbidden` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const ForbiddenNoContent = Forbidden.pipe(HttpApiSchema.asNoContent({ @@ -130,7 +130,7 @@ export const ForbiddenNoContent = Forbidden.pipe(HttpApiSchema.asNoContent({ * @category errors * @since 4.0.0 */ -export class NotFound extends Schema.ErrorClass("effect/HttpApiError/NotFound")({ +export class NotFound extends Schema.Error("effect/HttpApiError/NotFound")({ _tag: Schema.tag("NotFound") }, { description: "NotFound", @@ -146,7 +146,7 @@ export class NotFound extends Schema.ErrorClass("effect/HttpApiError/N * No-content schema variant for `NotFound`, decoding an empty 404 response into a * `NotFound` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const NotFoundNoContent = NotFound.pipe(HttpApiSchema.asNoContent({ @@ -160,7 +160,7 @@ export const NotFoundNoContent = NotFound.pipe(HttpApiSchema.asNoContent({ * @category errors * @since 4.0.0 */ -export class MethodNotAllowed extends Schema.ErrorClass("effect/HttpApiError/MethodNotAllowed")({ +export class MethodNotAllowed extends Schema.Error("effect/HttpApiError/MethodNotAllowed")({ _tag: Schema.tag("MethodNotAllowed") }, { description: "MethodNotAllowed", @@ -176,7 +176,7 @@ export class MethodNotAllowed extends Schema.ErrorClass("effec * No-content schema variant for `MethodNotAllowed`, decoding an empty 405 response * into a `MethodNotAllowed` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const MethodNotAllowedNoContent = MethodNotAllowed.pipe(HttpApiSchema.asNoContent({ @@ -190,7 +190,7 @@ export const MethodNotAllowedNoContent = MethodNotAllowed.pipe(HttpApiSchema.asN * @category errors * @since 4.0.0 */ -export class NotAcceptable extends Schema.ErrorClass("effect/HttpApiError/NotAcceptable")({ +export class NotAcceptable extends Schema.Error("effect/HttpApiError/NotAcceptable")({ _tag: Schema.tag("NotAcceptable") }, { description: "NotAcceptable", @@ -206,7 +206,7 @@ export class NotAcceptable extends Schema.ErrorClass("effect/Http * No-content schema variant for `NotAcceptable`, decoding an empty 406 response * into a `NotAcceptable` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const NotAcceptableNoContent = NotAcceptable.pipe(HttpApiSchema.asNoContent({ @@ -220,7 +220,7 @@ export const NotAcceptableNoContent = NotAcceptable.pipe(HttpApiSchema.asNoConte * @category errors * @since 4.0.0 */ -export class RequestTimeout extends Schema.ErrorClass("effect/HttpApiError/RequestTimeout")({ +export class RequestTimeout extends Schema.Error("effect/HttpApiError/RequestTimeout")({ _tag: Schema.tag("RequestTimeout") }, { description: "RequestTimeout", @@ -236,7 +236,7 @@ export class RequestTimeout extends Schema.ErrorClass("effect/Ht * No-content schema variant for `RequestTimeout`, decoding an empty 408 response * into a `RequestTimeout` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const RequestTimeoutNoContent = RequestTimeout.pipe(HttpApiSchema.asNoContent({ @@ -250,7 +250,7 @@ export const RequestTimeoutNoContent = RequestTimeout.pipe(HttpApiSchema.asNoCon * @category errors * @since 4.0.0 */ -export class Conflict extends Schema.ErrorClass("effect/HttpApiError/Conflict")({ +export class Conflict extends Schema.Error("effect/HttpApiError/Conflict")({ _tag: Schema.tag("Conflict") }, { description: "Conflict", @@ -266,7 +266,7 @@ export class Conflict extends Schema.ErrorClass("effect/HttpApiError/C * No-content schema variant for `Conflict`, decoding an empty 409 response into a * `Conflict` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const ConflictNoContent = Conflict.pipe(HttpApiSchema.asNoContent({ @@ -280,7 +280,7 @@ export const ConflictNoContent = Conflict.pipe(HttpApiSchema.asNoContent({ * @category errors * @since 4.0.0 */ -export class Gone extends Schema.ErrorClass("effect/HttpApiError/Gone")({ +export class Gone extends Schema.Error("effect/HttpApiError/Gone")({ _tag: Schema.tag("Gone") }, { description: "Gone", @@ -296,7 +296,7 @@ export class Gone extends Schema.ErrorClass("effect/HttpApiError/Gone")({ * No-content schema variant for `Gone`, decoding an empty 410 response into a * `Gone` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const GoneNoContent = Gone.pipe(HttpApiSchema.asNoContent({ @@ -310,14 +310,12 @@ export const GoneNoContent = Gone.pipe(HttpApiSchema.asNoContent({ * @category errors * @since 4.0.0 */ -export class UnprocessableEntity - extends Schema.ErrorClass("effect/HttpApiError/UnprocessableEntity")({ - _tag: Schema.tag("UnprocessableEntity") - }, { - description: "UnprocessableEntity", - httpApiStatus: 422 - }) -{ +export class UnprocessableEntity extends Schema.Error("effect/HttpApiError/UnprocessableEntity")({ + _tag: Schema.tag("UnprocessableEntity") +}, { + description: "UnprocessableEntity", + httpApiStatus: 422 +}) { override readonly [ErrorReporter.ignore] = true; [HttpServerRespondable.symbol]() { return Effect.succeed(unprocessableEntityResponse) @@ -328,7 +326,7 @@ export class UnprocessableEntity * No-content schema variant for `UnprocessableEntity`, decoding an empty 422 * response into an `UnprocessableEntity` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const UnprocessableEntityNoContent = UnprocessableEntity.pipe(HttpApiSchema.asNoContent({ @@ -342,14 +340,12 @@ export const UnprocessableEntityNoContent = UnprocessableEntity.pipe(HttpApiSche * @category errors * @since 4.0.0 */ -export class InternalServerError - extends Schema.ErrorClass("effect/HttpApiError/InternalServerError")({ - _tag: Schema.tag("InternalServerError") - }, { - description: "InternalServerError", - httpApiStatus: 500 - }) -{ +export class InternalServerError extends Schema.Error("effect/HttpApiError/InternalServerError")({ + _tag: Schema.tag("InternalServerError") +}, { + description: "InternalServerError", + httpApiStatus: 500 +}) { [HttpServerRespondable.symbol]() { return Effect.succeed(internalServerErrorResponse) } @@ -359,7 +355,7 @@ export class InternalServerError * No-content schema variant for `InternalServerError`, decoding an empty 500 * response into an `InternalServerError` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const InternalServerErrorNoContent = InternalServerError.pipe(HttpApiSchema.asNoContent({ @@ -373,7 +369,7 @@ export const InternalServerErrorNoContent = InternalServerError.pipe(HttpApiSche * @category errors * @since 4.0.0 */ -export class NotImplemented extends Schema.ErrorClass("effect/HttpApiError/NotImplemented")({ +export class NotImplemented extends Schema.Error("effect/HttpApiError/NotImplemented")({ _tag: Schema.tag("NotImplemented") }, { description: "NotImplemented", @@ -388,7 +384,7 @@ export class NotImplemented extends Schema.ErrorClass("effect/Ht * No-content schema variant for `NotImplemented`, decoding an empty 501 response * into a `NotImplemented` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const NotImplementedNoContent = NotImplemented.pipe(HttpApiSchema.asNoContent({ @@ -402,14 +398,12 @@ export const NotImplementedNoContent = NotImplemented.pipe(HttpApiSchema.asNoCon * @category errors * @since 4.0.0 */ -export class ServiceUnavailable - extends Schema.ErrorClass("effect/HttpApiError/ServiceUnavailable")({ - _tag: Schema.tag("ServiceUnavailable") - }, { - description: "ServiceUnavailable", - httpApiStatus: 503 - }) -{ +export class ServiceUnavailable extends Schema.Error("effect/HttpApiError/ServiceUnavailable")({ + _tag: Schema.tag("ServiceUnavailable") +}, { + description: "ServiceUnavailable", + httpApiStatus: 503 +}) { [HttpServerRespondable.symbol]() { return Effect.succeed(serviceUnavailableResponse) } @@ -419,7 +413,7 @@ export class ServiceUnavailable * No-content schema variant for `ServiceUnavailable`, decoding an empty 503 * response into a `ServiceUnavailable` error value. * - * @category NoContent errors + * @category schemas * @since 4.0.0 */ export const ServiceUnavailableNoContent = ServiceUnavailable.pipe(HttpApiSchema.asNoContent({ @@ -443,15 +437,15 @@ export type HttpApiSchemaErrorTypeId = "~effect/httpapi/HttpApiError/HttpApiSche export const HttpApiSchemaErrorTypeId: HttpApiSchemaErrorTypeId = "~effect/httpapi/HttpApiError/HttpApiSchemaError" /** - * Error raised when an HTTP API request component fails schema decoding. It records - * which component failed and responds as an empty `400 Bad Request` when rendered - * as a server response. + * Error raised when an HTTP API request or response component fails schema + * decoding or encoding. It records which component failed and responds as an + * empty `400 Bad Request` when rendered as a server response. * * @category errors * @since 4.0.0 */ export class HttpApiSchemaError extends Data.TaggedClass("HttpApiSchemaError")<{ - readonly kind: "Params" | "Headers" | "Query" | "Body" | "Payload" + readonly kind: "Params" | "Headers" | "Query" | "Body" | "Payload" | "ResponseHeaders" readonly cause: Schema.SchemaError }> { readonly [HttpApiSchemaErrorTypeId]: HttpApiSchemaErrorTypeId = HttpApiSchemaErrorTypeId diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiGroup.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiGroup.ts index fb6103f76..66feb7486 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiGroup.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiGroup.ts @@ -136,7 +136,7 @@ export interface HttpApiGroup< * id and the group identifier so the relationship between an API and its * implemented groups is checked at compile time. * - * @category models + * @category services * @since 4.0.0 */ export interface Service { @@ -154,7 +154,7 @@ export interface Service { * When given an API id and a group or union of groups, this type maps each group * to the `Service` identity that must be provided by `HttpApiBuilder.group`. * - * @category models + * @category utility types * @since 4.0.0 */ export type ToService = Group extends Constraint ? @@ -187,7 +187,7 @@ export interface Top extends HttpApiGroup /** * Selects the group with the specified identifier from a union of groups. * - * @category models + * @category utility types * @since 4.0.0 */ export type WithIdentifier = Extract @@ -195,7 +195,7 @@ export type WithIdentifier = Extract = Group extends Constraint ? Group["identifier"] : never @@ -203,7 +203,7 @@ export type Identifier = Group extends Constraint ? Group["identifier"] : /** * Extracts the endpoint union contained in an `HttpApiGroup`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Endpoints = Group extends HttpApiGroup ? @@ -214,7 +214,7 @@ export type Endpoints = Group extends HttpApiGroup = HttpApiEndpoint.ErrorServicesEncode> @@ -223,7 +223,7 @@ export type ErrorServicesEncode = HttpApiEndpoint.ErrorServicesEncode = HttpApiEndpoint.ErrorServicesDecode> @@ -231,7 +231,7 @@ export type ErrorServicesDecode = HttpApiEndpoint.ErrorServicesDecode = HttpApiEndpoint.MiddlewareError> @@ -240,7 +240,7 @@ export type MiddlewareError = HttpApiEndpoint.MiddlewareError = HttpApiEndpoint.MiddlewareProvides> @@ -248,7 +248,7 @@ export type MiddlewareProvides = HttpApiEndpoint.MiddlewareProvides = HttpApiEndpoint.MiddlewareClient> @@ -256,7 +256,7 @@ export type MiddlewareClient = HttpApiEndpoint.MiddlewareClient = HttpApiEndpoint.MiddlewareServices> @@ -264,7 +264,7 @@ export type MiddlewareServices = HttpApiEndpoint.MiddlewareServices = Endpoints< @@ -274,7 +274,7 @@ export type EndpointsWithIdentifier = Group extends HttpApiGroup ? @@ -284,7 +284,7 @@ export type ClientServices = Group extends HttpApiGroup = Group extends @@ -295,7 +295,7 @@ export type AddPrefix = Group extends /** * Returns the type of a group after applying a middleware identifier to every endpoint in the group. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddMiddleware = Group extends @@ -308,7 +308,7 @@ const Proto = { add(this: Top, ...toAdd: NonEmptyReadonlyArray) { const endpoints = { ...this.endpoints } for (const endpoint of toAdd) { - InternalRecord.set(endpoints, endpoint.identifier, endpoint) + InternalRecord.assignProperty(endpoints, endpoint.identifier, endpoint) } return makeProto({ ...optionsFromGroup(this), diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts index d5f31846a..24b3f0fb6 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiMiddleware.ts @@ -125,7 +125,7 @@ export interface HttpApiMiddlewareClient<_E, CE, R> { /** * Client-side service marker required when a middleware declares `requiredForClient`. * - * @category models + * @category utility types * @since 4.0.0 */ export interface ForClient { @@ -136,7 +136,7 @@ export interface ForClient { /** * Base service key shape for HTTP API middleware services, including provided services, declared error schemas, and client requirements. * - * @category models + * @category services * @since 4.0.0 */ export interface AnyService extends Context.Key { @@ -150,7 +150,7 @@ export interface AnyService extends Context.Key { /** * Middleware service key shape for security middleware, including the security schemes handled by the service. * - * @category models + * @category services * @since 4.0.0 */ export interface AnyServiceSecurity extends AnyService { @@ -161,7 +161,7 @@ export interface AnyServiceSecurity extends AnyService { /** * Type-level identifier carried by middleware services to track provided services, required services, errors, client errors, and client requirements. * - * @category models + * @category utility types * @since 4.0.0 */ export interface AnyId { @@ -177,7 +177,7 @@ export interface AnyId { /** * Extracts the services provided by a middleware identifier. * - * @category models + * @category utility types * @since 4.0.0 */ export type Provides = A extends { readonly [TypeId]: { readonly provides: infer P } } ? P : never @@ -185,7 +185,7 @@ export type Provides = A extends { readonly [TypeId]: { readonly provides: in /** * Extracts the services required to run a middleware implementation. * - * @category models + * @category utility types * @since 4.0.0 */ export type Requires = A extends { readonly [TypeId]: { readonly requires: infer R } } ? R : never @@ -193,7 +193,7 @@ export type Requires = A extends { readonly [TypeId]: { readonly requires: in /** * Applies a middleware's service changes to an existing requirement type by removing services it provides and adding services it requires. * - * @category models + * @category utility types * @since 4.0.0 */ export type ApplyServices = Exclude> | Requires @@ -201,7 +201,7 @@ export type ApplyServices = Exclude> | Requir /** * Extracts the schema or schema union used for errors declared by a middleware identifier. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorSchema = A extends { readonly [TypeId]: { readonly error: infer E } } ? ErrorSchemaFromConstraint @@ -210,7 +210,7 @@ export type ErrorSchema = A extends { readonly [TypeId]: { readonly error: in /** * Extracts the decoded error type declared by a middleware identifier. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = ErrorSchema["Type"] @@ -218,7 +218,7 @@ export type Error = ErrorSchema["Type"] /** * Extracts the client-side error type for middleware that is required on generated clients. * - * @category models + * @category utility types * @since 4.0.0 */ export type ClientError = A extends { @@ -232,7 +232,7 @@ export type ClientError = A extends { /** * Computes the client-side service marker required for middleware that must also run in generated clients. * - * @category models + * @category utility types * @since 4.0.0 */ export type MiddlewareClient = A extends { @@ -245,7 +245,7 @@ export type MiddlewareClient = A extends { /** * Extracts the schema services required to encode errors declared by a middleware identifier. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesEncode = ErrorSchema["EncodingServices"] @@ -253,7 +253,7 @@ export type ErrorServicesEncode = ErrorSchema["EncodingServices"] /** * Extracts the schema services required to decode errors declared by a middleware identifier. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesDecode = ErrorSchema["DecodingServices"] @@ -266,7 +266,7 @@ export type ErrorServicesDecode = ErrorSchema["DecodingServices"] * It combines a `Context.Service` class with the middleware metadata used by * endpoints, builders, and generated clients. * - * @category schemas + * @category services * @since 4.0.0 */ export type ServiceClass< @@ -314,7 +314,7 @@ export type ServiceClass< * required services, provided services, typed error schemas, security schemes, * client errors, or a matching client middleware requirement. * - * @category schemas + * @category constructors * @since 4.0.0 */ export const Service = < @@ -394,26 +394,73 @@ function getError(error: ErrorConstraint | undefined): ReadonlySet { * * **Example** (Mapping schema errors to custom errors) * - * ```ts - * import { Effect, Schema } from "effect" - * import { HttpApiMiddleware } from "effect/unstable/httpapi" + * ```ts import.meta.vitest + * import { Effect, Schema, type Types } from "effect" + * import { HttpRouter, HttpServerResponse } from "effect/unstable/http" + * import { + * HttpApiEndpoint, + * HttpApiError, + * HttpApiGroup, + * HttpApiMiddleware + * } from "effect/unstable/httpapi" * - * export class CustomError extends Schema.TaggedErrorClass()("CustomError", {}) {} + * class CustomError extends Schema.TaggedError()("CustomError", {}) {} * - * export class ErrorHandler extends HttpApiMiddleware.Service()("api/ErrorHandler", { + * class ErrorHandler extends HttpApiMiddleware.Service()("api/ErrorHandler", { * error: CustomError * }) {} * - * export const ErrorHandlerLayer = HttpApiMiddleware.layerSchemaErrorTransform( + * const messages: Array = [] + * const ErrorHandlerLayer = HttpApiMiddleware.layerSchemaErrorTransform( * ErrorHandler, * (schemaError) => - * Effect.log("Got SchemaError", schemaError).pipe( + * Effect.sync(() => messages.push(`Mapping ${schemaError.kind} schema error`)).pipe( * Effect.andThen(Effect.fail(new CustomError())) * ) * ) + * + * const endpoint = HttpApiEndpoint.get("example", "/") + * const group = HttpApiGroup.make("examples").add(endpoint) + * const middlewareContext = { + * endpoint: endpoint as unknown as HttpApiEndpoint.Top, + * group: group as unknown as HttpApiGroup.Top + * } + * const Routes = HttpRouter.add( + * "GET", + * "/", + * Effect.gen(function*() { + * const applySchemaErrorTransform = yield* ErrorHandler + * const schemaError = yield* HttpApiError.HttpApiSchemaError.wrap( + * "Body", + * Schema.decodeUnknownEffect(Schema.String)(42) + * ).pipe(Effect.flip) + * const failingResponse = Effect.fail(schemaError as unknown as Types.unhandled) + * const result = yield* applySchemaErrorTransform(failingResponse, middlewareContext).pipe( + * Effect.match({ + * onFailure: (error) => error instanceof CustomError ? error._tag : "UnexpectedError", + * onSuccess: () => "Success" + * }) + * ) + * return HttpServerResponse.text(result) + * }) + * ).pipe(HttpRouter.provideRequest(ErrorHandlerLayer)) + * + * const program = Effect.acquireUseRelease( + * Effect.sync(() => HttpRouter.toWebHandler(Routes, { disableLogger: true })), + * ({ handler }) => + * Effect.gen(function*() { + * const response = yield* Effect.promise(() => handler(new Request("http://localhost/"))) + * const body = yield* Effect.promise(() => response.text()) + * return body + * }), + * ({ dispose }) => Effect.promise(dispose) + * ) + * + * const body = await Effect.runPromise(program) + * const result = [messages, body] // => [["Mapping Body schema error"], "CustomError"] * ``` * - * @category SchemaError transform + * @category layers * @since 4.0.0 */ export const layerSchemaErrorTransform = ( @@ -451,7 +498,7 @@ export const layerSchemaErrorTransform = ( diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiScalar.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiScalar.ts index c7228adfb..22900eede 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiScalar.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiScalar.ts @@ -87,12 +87,6 @@ export type ScalarConfig = { /** * Path to a favicon image. * - * **Example** (Setting a relative favicon) - * - * ```ts - * const favicon = "/favicon.svg" - * ``` - * * @default undefined */ favicon?: string @@ -107,12 +101,6 @@ export type ScalarConfig = { * Browsers can derive the origin from `window.location.origin`; server * rendering needs this value supplied explicitly. * - * **Example** (Setting a local server URL) - * - * ```ts - * const baseServerURL = "http://localhost:3000" - * ``` - * * @default undefined */ baseServerURL?: string diff --git a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts index a02ec20d7..43769ccd9 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/HttpApiSchema.ts @@ -29,10 +29,27 @@ declare module "../../Schema.ts" { * @internal */ readonly "~httpApiEncoding"?: Encoding | undefined + /** + * Marks schemas produced by `encodeToWithHeaders`, carrying the body and + * headers schemas so integrations can split the encoded pair. + * @internal + */ + readonly "~httpApiWithHeaders"?: WithHeadersAnnotation | undefined } } } +/** + * Annotation payload attached by `encodeToWithHeaders`. + * + * @internal + */ +export interface WithHeadersAnnotation { + readonly body: Schema.Top + readonly headers: Schema.Top + readonly headersCodec: Schema.Top +} + /** * HTTP API body encoding metadata used by payloads and responses. * @@ -140,7 +157,7 @@ const StreamSchemaTypeId = "~effect/httpapi/HttpApiSchema/Stream" /** * Common HTTP status code literals accepted by {@link status}. * - * @category status + * @category models * @since 4.0.0 */ export type StatusLiteral = keyof typeof statusCodeByLiteral @@ -154,7 +171,7 @@ export type StatusLiteral = keyof typeof statusCodeByLiteral * schema. You can pass either a numeric status code (for example, `201`) or a * common literal name (for example, `"Created"`). * - * @category status + * @category schemas * @since 4.0.0 */ export function status(code: number): { @@ -174,7 +191,7 @@ export function status(code: number | StatusLiteral) { * * @see {@link NoContent} for the predefined 204 no content schema. * - * @category Empty + * @category constructors * @since 4.0.0 */ export const Empty = (code: number): Schema.Void => Schema.Void.pipe(status(code)) @@ -190,7 +207,7 @@ export interface NoContent extends Schema.Void {} /** * Schema for empty HTTP responses with status code 204. * - * @category Empty + * @category schemas * @since 4.0.0 */ export const NoContent: NoContent = Empty(204) @@ -206,7 +223,7 @@ export interface Created extends Schema.Void {} /** * Schema for empty HTTP responses with status code 201. * - * @category Empty + * @category schemas * @since 4.0.0 */ export const Created: Created = Empty(201) @@ -222,7 +239,7 @@ export interface Accepted extends Schema.Void {} /** * Schema for empty HTTP responses with status code 202. * - * @category Empty + * @category schemas * @since 4.0.0 */ export const Accepted: Accepted = Empty(202) @@ -467,6 +484,315 @@ function defaultStreamContentType(mode: StreamMode): string { } } +/** + * Runtime brand key used to mark `WithHeaders` response schemas. + * + * @category type IDs + * @since 4.0.0 + */ +export const WithHeadersTypeId = "~effect/httpapi/HttpApiSchema/WithHeaders" + +/** + * Type-level brand identifier used by `WithHeaders`. + * + * @category type IDs + * @since 4.0.0 + */ +export type WithHeadersTypeId = typeof WithHeadersTypeId + +/** + * Runtime brand key used to mark `WithHeaders` response values. + * + * @category type IDs + * @since 4.0.0 + */ +export const WithHeadersValueTypeId = "~effect/httpapi/HttpApiSchema/WithHeadersValue" + +/** + * Type-level brand identifier used by `WithHeaders` response values. + * + * @category type IDs + * @since 4.0.0 + */ +export type WithHeadersValueTypeId = typeof WithHeadersValueTypeId + +/** + * A response schema wrapping a body schema together with a response headers + * schema. + * + * **Details** + * + * `WithHeaders` is a branded declaration schema: it carries the inner success + * schema and the headers schema as properties, and server, client, and OpenAPI + * integrations detect the brand and handle body and headers separately. It is + * supported for error responses, though {@link encodeToWithHeaders} is usually + * more convenient there because handlers can fail with the domain error value. + * + * - `schema` is the inner response schema. Success responses may wrap + * `StreamSse` and `StreamUint8Array`; error responses remain non-streaming. + * Nesting `WithHeaders` is rejected at construction. + * - `headers` is any schema; endpoint construction applies + * `Schema.toCodecStringTree` unless codecs are disabled, so leaves become + * `string | undefined` on the wire and `undefined` leaves are omitted from the response. + * - Status and response-encoding annotations are resolved from the wrapper + * first, falling through to the inner schema. + * - A header-carrying response cannot share its status and content type with + * another response in the same success or error union. Endpoint construction + * rejects ambiguous declarations, including those made with + * {@link encodeToWithHeaders}. + * - `Rebuild` preserves the brand and both parts, so `.annotate` keeps the + * wrapper intact. + * + * @category models + * @since 4.0.0 + */ +export interface WithHeaders extends + Schema.Bottom< + withHeaders, + withHeaders, + S["DecodingServices"] | H["DecodingServices"], + S["EncodingServices"] | H["EncodingServices"], + SchemaAST.Declaration, + WithHeaders + > +{ + readonly "Rebuild": WithHeaders + readonly [WithHeadersTypeId]: typeof WithHeadersTypeId + readonly schema: S + readonly headers: H +} + +/** + * The Type of a `WithHeaders` schema: what handlers return and what the + * client resolves to, constructed via {@link withHeaders}. + * + * `body` is the inner success value. For stream success schemas it is the + * `Stream` itself, so headers are decided before the body starts streaming. + * + * @category models + * @since 4.0.0 + */ +export interface withHeaders { + readonly [WithHeadersValueTypeId]: WithHeadersValueTypeId + readonly body: A + readonly headers: H +} + +/** @internal */ +export const isWithHeadersValue = (u: unknown): u is withHeaders => + Predicate.hasProperty(u, WithHeadersValueTypeId) + +const withHeadersValueSchema = Schema.declare(isWithHeadersValue) + +/** + * Wraps a success schema with a response headers schema. + * + * Headers accept either a schema or a fields shorthand, mirroring the + * request-side headers option. + * + * ```ts import.meta.vitest + * import { Schema } from "effect" + * import { HttpApiSchema } from "effect/unstable/httpapi" + * + * const schema = HttpApiSchema.WithHeaders(Schema.String, { + * "x-total-count": Schema.FiniteFromString + * }) + * const response: typeof schema.Type = HttpApiSchema.withHeaders({ + * body: "created", + * headers: { "x-total-count": 1 } + * }) + * + * HttpApiSchema.isWithHeaders(schema) // => true + * response.body // => "created" + * response.headers // => { "x-total-count": 1 } + * ``` + * + * @category constructors + * @since 4.0.0 + */ +export function WithHeaders( + schema: S, + headers: H +): WithHeaders> +export function WithHeaders( + schema: S, + headers: H +): WithHeaders +export function WithHeaders( + schema: Schema.Top, + headers: Schema.Top | Schema.Struct.Fields +): WithHeaders { + if (isWithHeaders(schema)) { + throw new Error("WithHeaders schemas cannot be nested") + } + return Schema.make>(withHeadersValueSchema.ast, { + [WithHeadersTypeId]: WithHeadersTypeId, + schema, + headers: Schema.isSchema(headers) ? headers : Schema.Struct(headers) + }) +} + +/** + * Constructs a `WithHeaders` response value from a body and headers. + * + * The returned value is branded so servers and clients can detect it exactly, + * including in mixed success unions. The same shape is used on both sides: a + * value received from a client can be returned from another handler unchanged. + * + * See {@link WithHeaders} for an example that constructs a schema and its + * corresponding response value. + * + * @category constructors + * @since 4.0.0 + */ +export const withHeaders = (options: { + readonly body: A + readonly headers: H +}): withHeaders => ({ + [WithHeadersValueTypeId]: WithHeadersValueTypeId, + body: options.body, + headers: options.headers +}) + +/** + * Returns `true` when a schema is a `WithHeaders` response schema. + * + * ```ts import.meta.vitest + * import { Schema } from "effect" + * import { HttpApiSchema } from "effect/unstable/httpapi" + * + * const schema = HttpApiSchema.WithHeaders(Schema.String, { + * "x-request-id": Schema.String + * }) + * + * HttpApiSchema.isWithHeaders(schema) // => true + * HttpApiSchema.isWithHeaders(Schema.String) // => false + * ``` + * + * @category predicates + * @since 4.0.0 + */ +export const isWithHeaders = (u: unknown): u is WithHeaders => + Schema.isSchema(u) && Predicate.hasProperty(u, WithHeadersTypeId) + +/** @internal */ +export function rebuildWithHeaders( + self: WithHeaders, + schema: Schema.Top, + headers: Schema.Top +): WithHeaders { + return Schema.make>(self.ast, { + [WithHeadersTypeId]: WithHeadersTypeId, + schema, + headers + }) +} + +/** + * Schema type returned by `encodeToWithHeaders`, encoding as a `{ body, headers }` + * pair while decoding to the source schema type. + * + * @category schemas + * @since 4.0.0 + */ +export interface encodeToWithHeaders< + S extends Schema.Top, + Body extends Schema.Top, + Headers extends Schema.Struct.Fields +> extends + Schema.decodeTo< + Schema.toType, + Schema.Struct<{ + readonly body: Body + readonly headers: Schema.Struct + }> + > +{} + +/** + * Encodes a schema as a `{ body, headers }` pair, folding response headers into + * an opaque domain type such as an error class. + * + * **Details** + * + * The encoded side is the pair of the body schema and the headers fields; the + * Type stays the source schema's Type. The body schema is authoritative for + * everything wire-level: status, content type, and response encoding resolve + * from the body schema's annotations. + * + * The mappings are pure total functions: validation lives in the body and + * header schemas, the mappings only reshape valid data. + * + * Streams used as the body schema turn mid-stream transport errors into + * defects on the client. Stream responses should use {@link WithHeaders}, + * which preserves the body stream's error channel in the generated client. + * + * ```ts import.meta.vitest + * import { Schema } from "effect" + * import { HttpApiSchema } from "effect/unstable/httpapi" + * + * class UserNotFound extends Schema.TaggedError()("UserNotFound", { + * userId: Schema.Int + * }) {} + * + * const UserNotFoundWithHeaders = UserNotFound.pipe( + * HttpApiSchema.encodeToWithHeaders({ + * body: HttpApiSchema.Empty(404), + * headers: { + * "x-user-id": Schema.Int + * } + * }, { + * decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }), + * encode: (error) => ({ + * headers: { "x-user-id": error.userId }, + * body: undefined + * }) + * }) + * ) + * + * const encoded = Schema.encodeSync(UserNotFoundWithHeaders)(new UserNotFound({ userId: 123 })) + * encoded // => { body: undefined, headers: { "x-user-id": 123 } } + * ``` + * + * @see {@link WithHeaders} for the structural wrapper recommended for success + * responses, including streams. + * + * @category encoding + * @since 4.0.0 + */ +export function encodeToWithHeaders< + S extends Schema.Top, + Body extends Schema.Top, + Headers extends Schema.Struct.Fields +>(options: { + readonly body: Body + readonly headers: Headers +}, transformation: { + readonly decode: ( + value: Schema.Struct.Type<{ readonly body: Body; readonly headers: Schema.Struct }> + ) => S["Type"] + readonly encode: ( + value: S["Type"] + ) => Schema.Struct.Type<{ readonly body: Body; readonly headers: Schema.Struct }> +}) { + return (self: S): encodeToWithHeaders => { + const body = options.body + const headers = Schema.Struct(options.headers) + const status = resolveHttpApiStatus(body.ast) + const encoding = resolveHttpApiEncoding(body.ast) + return Schema.Struct({ body, headers }).pipe( + Schema.decodeTo( + Schema.toType(self), + SchemaTransformation.transform(transformation) + ) + ).annotate({ + "~httpApiWithHeaders": { body, headers, headersCodec: Schema.toEncoded(headers) }, + ...(status !== undefined ? { httpApiStatus: status } : undefined), + ...(encoding !== undefined ? { "~httpApiEncoding": encoding } : undefined) + }) + } +} + /** * Runtime brand key used to mark schemas as buffered multipart payloads. * @@ -667,6 +993,9 @@ export const isNoContent = (ast: SchemaAST.AST): boolean => { const resolveHttpApiEncoding = SchemaAST.resolveAt("~httpApiEncoding") +/** @internal */ +export const getWithHeadersAnnotation = SchemaAST.resolveAt("~httpApiWithHeaders") + const resolveHttpApiStatus = SchemaAST.resolveAt("httpApiStatus") const defaultJsonEncoding: Encoding = { @@ -703,6 +1032,22 @@ export function getStatusSuccess(self: SchemaAST.AST): number { return resolveHttpApiStatus(self) ?? 200 } +/** @internal */ +export function getStatusSuccessSchema(schema: Schema.Constraint): number { + if (isWithHeaders(schema)) { + return resolveHttpApiStatus(schema.ast) ?? getStatusSuccess(schema.schema.ast) + } + return getStatusSuccess(schema.ast) +} + +/** @internal */ +export function getResponseEncodingSchema(schema: Schema.Constraint): ResponseEncoding { + if (isWithHeaders(schema) && resolveHttpApiEncoding(schema.ast) === undefined) { + return getResponseEncoding(schema.schema.ast) + } + return getResponseEncoding(schema.ast) +} + /** @internal */ export function getStatusStream(self: StreamSchema): number { return getStatusSuccess(self.ast) @@ -712,3 +1057,11 @@ export function getStatusStream(self: StreamSchema): number { export function getStatusError(self: SchemaAST.AST): number { return resolveHttpApiStatus(self) ?? 500 } + +/** @internal */ +export function getStatusErrorSchema(schema: Schema.Constraint): number { + if (isWithHeaders(schema)) { + return resolveHttpApiStatus(schema.ast) ?? getStatusError(schema.schema.ast) + } + return getStatusError(schema.ast) +} diff --git a/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts b/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts index 31a709ef5..c7ff5bff4 100644 --- a/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts +++ b/.context/effect/packages/effect/src/unstable/httpapi/OpenApi.ts @@ -14,13 +14,14 @@ import * as Context from "../../Context.ts" import * as Equal from "../../Equal.ts" import { constFalse } from "../../Function.ts" import * as InternalRecord from "../../internal/record.ts" +import * as InternalToJsonSchemaDocument from "../../internal/schema/toJsonSchemaDocument.ts" +import * as InternalToRepresentation from "../../internal/schema/toRepresentation.ts" import * as JsonPatch from "../../JsonPatch.ts" import { escapeToken } from "../../JsonPointer.ts" import * as JsonSchema from "../../JsonSchema.ts" import * as Option from "../../Option.ts" import * as Schema from "../../Schema.ts" import * as SchemaAST from "../../SchemaAST.ts" -import * as SchemaRepresentation from "../../SchemaRepresentation.ts" import * as HttpMethod from "../http/HttpMethod.ts" import * as HttpApi from "./HttpApi.ts" import * as HttpApiEndpoint from "./HttpApiEndpoint.ts" @@ -32,7 +33,7 @@ import type { HttpApiSecurity } from "./HttpApiSecurity.ts" /** * OpenAPI annotation for overriding generated identifiers, including operation ids. * - * @category annotations + * @category services * @since 4.0.0 */ export class Identifier extends Context.Service()("effect/httpapi/OpenApi/Identifier") {} @@ -40,7 +41,7 @@ export class Identifier extends Context.Service()("effect/ht /** * OpenAPI annotation for setting the API title or group tag name. * - * @category annotations + * @category services * @since 4.0.0 */ export class Title extends Context.Service()("effect/httpapi/OpenApi/Title") {} @@ -48,7 +49,7 @@ export class Title extends Context.Service()("effect/httpapi/Open /** * OpenAPI annotation for setting the generated API version. * - * @category annotations + * @category services * @since 4.0.0 */ export class Version extends Context.Service()("effect/httpapi/OpenApi/Version") {} @@ -56,7 +57,7 @@ export class Version extends Context.Service()("effect/httpapi/ /** * OpenAPI annotation for setting generated descriptions on APIs, groups, endpoints, or security schemes. * - * @category annotations + * @category services * @since 4.0.0 */ export class Description extends Context.Service()("effect/httpapi/OpenApi/Description") {} @@ -64,7 +65,7 @@ export class Description extends Context.Service()("effect/ /** * OpenAPI annotation for setting the generated API license metadata. * - * @category annotations + * @category services * @since 4.0.0 */ export class License extends Context.Service()("effect/httpapi/OpenApi/License") {} @@ -72,7 +73,7 @@ export class License extends Context.Service()("eff /** * OpenAPI annotation for adding external documentation metadata to groups or endpoints. * - * @category annotations + * @category services * @since 4.0.0 */ export class ExternalDocs @@ -82,7 +83,7 @@ export class ExternalDocs /** * OpenAPI annotation for setting the generated API server list. * - * @category annotations + * @category services * @since 4.0.0 */ export class Servers @@ -92,7 +93,7 @@ export class Servers /** * OpenAPI annotation for setting the format metadata, such as a bearer token format on security schemes. * - * @category annotations + * @category services * @since 4.0.0 */ export class Format extends Context.Service()("effect/httpapi/OpenApi/Format") {} @@ -100,7 +101,7 @@ export class Format extends Context.Service()("effect/httpapi/Op /** * OpenAPI annotation for setting generated summary text. * - * @category annotations + * @category services * @since 4.0.0 */ export class Summary extends Context.Service()("effect/httpapi/OpenApi/Summary") {} @@ -108,7 +109,7 @@ export class Summary extends Context.Service()("effect/httpapi/ /** * OpenAPI annotation for marking a generated endpoint operation as deprecated. * - * @category annotations + * @category services * @since 4.0.0 */ export class Deprecated extends Context.Service()("effect/httpapi/OpenApi/Deprecated") {} @@ -116,7 +117,7 @@ export class Deprecated extends Context.Service()("effect/h /** * OpenAPI annotation for shallowly merging additional fields into a generated OpenAPI object. * - * @category annotations + * @category services * @since 4.0.0 */ export class Override extends Context.Service>()("effect/httpapi/OpenApi/Override") {} @@ -130,7 +131,7 @@ export class Override extends Context.Service> * Use to hide internal, experimental, or otherwise undocumented HTTP API groups * and endpoints from generated OpenAPI output. * - * @category annotations + * @category services * @since 4.0.0 */ export const Exclude = Context.Reference("effect/httpapi/OpenApi/Exclude", { @@ -145,7 +146,7 @@ export const Exclude = Context.Reference("effect/httpapi/OpenApi/Exclud * The function is applied during generation to the annotated API, group tag, or * endpoint operation. * - * @category annotations + * @category services * @since 4.0.0 */ export class Transform extends Context.Service< @@ -212,6 +213,34 @@ export const annotations: ( const apiCache = new WeakMap() +type CompileSchemas = ( + asts: readonly [SchemaAST.AST, ...Array] +) => JsonSchema.MultiDocument<"openapi-3.1"> + +const compileSchemas: CompileSchemas = (asts) => + JsonSchema.toMultiDocumentOpenApi3_1( + InternalToJsonSchemaDocument.toJsonSchemaMultiDocument( + InternalToRepresentation.toRepresentations( + Arr.map(asts, Schema.toCodecJsonAST), + InternalToJsonSchemaDocument.toRepresentationOptions + ) + ) + ) + +const cloneOpenAPISpec = (value: A): A => { + if (Array.isArray(value)) { + return value.map(cloneOpenAPISpec) as A + } + if (value !== null && typeof value === "object") { + const out: Record = {} + for (const key of Object.keys(value)) { + InternalRecord.assignProperty(out, key, cloneOpenAPISpec((value as Record)[key])) + } + return out as A + } + return value +} + /** * 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 @@ -251,9 +280,17 @@ function processAnnotation( export function fromApi( api: HttpApi.HttpApi ): OpenAPISpec { - const cached = apiCache.get(api) + return fromApiWith(api, apiCache, compileSchemas) +} + +function fromApiWith( + api: HttpApi.HttpApi, + cache: WeakMap, + compileSchemas: CompileSchemas +): OpenAPISpec { + const cached = cache.get(api) if (cached !== undefined) { - return cached + return cloneOpenAPISpec(cached) } let spec: OpenAPISpec = { openapi: "3.1.0", @@ -318,7 +355,10 @@ export function fromApi { - Object.assign(tag, override) + // OpenAPI documents are JSON, so symbol keys are intentionally ignored. + for (const [key, value] of Object.entries(override)) { + InternalRecord.assignProperty(tag as any, key, value) + } }) processAnnotation(group.annotations, Transform, (transformFn) => { tag = transformFn(tag) as OpenAPISpecTag @@ -346,10 +386,29 @@ export function fromApi string) { - for (const [status, { content, descriptions, streamContent }] of bodies) { + for (const [status, { content, descriptions, headers, streamContent }] of bodies) { const description = descriptions.size > 0 ? Array.from(descriptions).join(" | ") : defaultDescription() - op.responses[status] = { + InternalRecord.assignProperty(op.responses, status, { description + }) + for (const schema of headers) { + const ast = SchemaAST.getLastEncoding(schema.ast) + if (SchemaAST.isObjects(ast)) { + for (const ps of ast.propertySignatures) { + const name = String(ps.name).toLowerCase() + if (name === "content-type") continue + op.responses[status].headers ??= {} + InternalRecord.assignProperty(op.responses[status].headers, name, { + schema: {}, + required: !SchemaAST.isOptional(ps.type) + }) + pathOps.push({ + _tag: "parameter", + ast: ps.type, + path: ["paths", path, method, "responses", String(status), "headers", name, "schema"] + }) + } + } } if (content !== undefined) { content.forEach((map, encoding) => { @@ -363,9 +422,9 @@ export function fromApi "Error" ) processAnnotation(endpoint.annotations, Override, (override) => { - Object.assign(op, override) + // OpenAPI documents are JSON, so symbol keys are intentionally ignored. + for (const [key, value] of Object.entries(override)) { + InternalRecord.assignProperty(op as any, key, value) + } }) processAnnotation(endpoint.annotations, Transform, (transformFn) => { op = transformFn(op) as OpenAPISpecOperation @@ -570,8 +632,8 @@ export function fromApi { const identifier = SchemaAST.resolveIdentifier(componentSchema.ast) if (identifier !== undefined) { - if (identifier in spec.components.schemas) { + if (Object.hasOwn(spec.components.schemas, identifier)) { throw new globalThis.Error(`Duplicate component schema identifier: ${identifier}`) } - spec.components.schemas[identifier] = {} + InternalRecord.assignProperty(spec.components.schemas, identifier, {}) pathOps.push({ _tag: "schema", ast: componentSchema.ast, @@ -599,12 +661,7 @@ export function fromApi op.ast) - ) - const jsonSchemaMultiDocument = JsonSchema.toMultiDocumentOpenApi3_1( - SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument) - ) + const jsonSchemaMultiDocument = compileSchemas(Arr.map(pathOps, (op) => op.ast)) const patchOps: Array = pathOps.map((op, i) => { const oppath = escapePath(op.path) const value = jsonSchemaMultiDocument.schemas[i] @@ -633,13 +690,16 @@ export function fromApi { - Object.assign(spec, override) + // OpenAPI documents are JSON, so symbol keys are intentionally ignored. + for (const [key, value] of Object.entries(override)) { + InternalRecord.assignProperty(spec as any, key, value) + } }) processAnnotation(api.annotations, Transform, (transformFn) => { spec = transformFn(spec) as OpenAPISpec }) - apiCache.set(api, spec) + cache.set(api, cloneOpenAPISpec(spec)) return spec } @@ -649,6 +709,7 @@ type ResponseBodies = Map< { descriptions: Set content: Content | undefined // undefined means no content + headers: Array streamContent: StreamContent | undefined } > @@ -658,19 +719,20 @@ const reservedStreamFailureEvent = "effect/httpapi/stream/failure" function extractSuccessResponseBodies(endpoint: HttpApiEndpoint.Top): ResponseBodies { return extractResponseBodies( HttpApiEndpoint.getSuccessSchemas(endpoint), - HttpApiSchema.getStatusSuccess, + HttpApiSchema.getStatusSuccessSchema, resolveDescriptionOrIdentifier ) } function extractResponseBodies( schemas: Array, - getStatus: (ast: SchemaAST.AST) => number, + getStatus: (schema: Schema.Constraint) => number, getDescription: (ast: SchemaAST.AST) => string | undefined ): ResponseBodies { const map = new Map content: Content | undefined + headers: Array streamContent: StreamContent | undefined }>() @@ -679,16 +741,25 @@ function extractResponseBodies( return map function process(schema: Schema.Constraint) { - if (HttpApiSchema.isStreamSchema(schema)) { - addStreamContent(schema) - return - } - const ast = schema.ast - const status = getStatus(ast) - if (HttpApiSchema.isNoContent(ast)) { - addNoContent(status, getDescription(schema.ast) ?? "") + const annotation = HttpApiSchema.getWithHeadersAnnotation(schema.ast) + const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : annotation?.body ?? schema + const headers = HttpApiSchema.isWithHeaders(schema) ? schema.headers : annotation?.headersCodec + const status = getStatus(schema) + const ast = body.ast + if (HttpApiSchema.isStreamSchema(body)) { + addStreamContent(body, status) + } else if (HttpApiSchema.isNoContent(ast)) { + addNoContent(status, getDescription(schema.ast) ?? getDescription(ast) ?? "") } else { - addContent(schema, status, HttpApiSchema.getResponseEncoding(ast)) + addContent( + body, + status, + HttpApiSchema.getResponseEncodingSchema(schema), + getDescription(schema.ast) ?? getDescription(ast) + ) + } + if (headers !== undefined) { + map.get(status)!.headers.push(headers) } } @@ -698,6 +769,7 @@ function extractResponseBodies( map.set(status, { descriptions: new Set([description]), content: undefined, + headers: [], streamContent: undefined }) } else { @@ -707,14 +779,19 @@ function extractResponseBodies( } } - function addContent(schema: Schema.Constraint, status: number, encoding: HttpApiSchema.Encoding) { - const description = getDescription(schema.ast) + function addContent( + schema: Schema.Constraint, + status: number, + encoding: HttpApiSchema.Encoding, + description: string | undefined + ) { const statusMap = map.get(status) const { _tag, contentType } = encoding if (statusMap === undefined) { map.set(status, { descriptions: new Set(description !== undefined ? [description] : []), content: new Map([[_tag, new Map([[contentType, new Set([schema])]])]]), + headers: [], streamContent: undefined }) } else { @@ -741,19 +818,24 @@ function extractResponseBodies( } } - function addStreamContent(stream: HttpApiSchema.StreamSchema) { - const status = HttpApiSchema.getStatusStream(stream) + function addStreamContent( + stream: HttpApiSchema.StreamSchema, + status: number + ) { const statusMap = map.get(status) if (statusMap === undefined) { map.set(status, { descriptions: new Set(), content: undefined, + headers: [], streamContent: new Map([[stream.contentType, stream]]) }) - } else if (statusMap.streamContent === undefined) { - statusMap.streamContent = new Map([[stream.contentType, stream]]) } else { - statusMap.streamContent.set(stream.contentType, stream) + if (statusMap.streamContent === undefined) { + statusMap.streamContent = new Map([[stream.contentType, stream]]) + } else { + statusMap.streamContent.set(stream.contentType, stream) + } } } } @@ -793,7 +875,7 @@ function toEncodingAST(ast: SchemaAST.AST, _tag: HttpApiSchema.Encoding["_tag"]) function persistedFileToBinaryEncoding(ast: SchemaAST.AST): SchemaAST.AST { if ( SchemaAST.isDeclaration(ast) && - ((ast.annotations as (Schema.Annotations.Declaration | undefined))?.typeConstructor?._tag === + ((ast.annotations as (Schema.Annotations.Declaration | undefined))?.representation?.id === "effect/http/PersistedFile") ) { return Uint8ArrayEncoding.ast @@ -1018,8 +1100,17 @@ export type OpenApiSpecContent = { export interface OpenApiSpecResponse { description: string content?: OpenApiSpecContent + headers?: Record } +/** + * Generated OpenAPI response header object. + * + * @category models + * @since 4.0.0 + */ +export type OpenAPISpecHeader = Omit + /** * Generated OpenAPI media type object containing the JSON Schema for a request or response body. * diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpExporter.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpExporter.ts index eee7a6d79..92b593080 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpExporter.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpExporter.ts @@ -13,6 +13,9 @@ import * as Context from "../../Context.ts" import * as Duration from "../../Duration.ts" import * as Effect from "../../Effect.ts" import * as Fiber from "../../Fiber.ts" +import * as FiberSet from "../../FiberSet.ts" +import { identity } from "../../Function.ts" +import * as Layer from "../../Layer.ts" import * as Num from "../../Number.ts" import * as Option from "../../Option.ts" import * as Schedule from "../../Schedule.ts" @@ -23,6 +26,24 @@ import * as HttpClientError from "../../unstable/http/HttpClientError.ts" import * as HttpClientRequest from "../../unstable/http/HttpClientRequest.ts" import type { HttpBody } from "../http/HttpBody.ts" +const retryAfterDelay = (value: string | undefined): Effect.Effect => { + const seconds = Option.fromUndefinedOr(value).pipe(Option.flatMap(Num.parse)) + if (Option.isSome(seconds)) { + return Effect.succeed(Duration.seconds(seconds.value)) + } + if (value === undefined) { + return Effect.succeed(Duration.seconds(5)) + } + const timestamp = Date.parse(value) + if (Number.isNaN(timestamp)) { + return Effect.succeed(Duration.seconds(5)) + } + return Effect.map( + Clock, + (clock) => Duration.millis(Math.max(timestamp - clock.currentTimeMillisUnsafe(), 1)) + ) +} + const policy = Schedule.forever.pipe( Schedule.passthrough, Schedule.addDelay(({ output: error }) => { @@ -31,16 +52,101 @@ const policy = Schedule.forever.pipe( && error.reason._tag === "StatusCodeError" && error.reason.response.status === 429 ) { - const retryAfter = Option.fromUndefinedOr(error.reason.response.headers["retry-after"]).pipe( - Option.flatMap(Num.parse), - Option.getOrElse(() => 5) - ) - return Effect.succeed(Duration.seconds(retryAfter)) + return retryAfterDelay(error.reason.response.headers["retry-after"]) } return Effect.succeed(Duration.seconds(1)) }) ) +/** + * Registry of exporter flush operations, used to manually drain buffered + * telemetry before the surrounding scope closes. + * + * **Details** + * + * Every exporter created by `make` registers its export operation here, so a + * single `flush` drains all signals sharing the registry. `flush` returns only + * after the exports it initiated have settled, cannot fail, and respects each + * exporter's temporary-disable window. Wrap it with `Effect.timeoutOption` to + * bound its duration at the call site. + * + * @category services + * @since 4.0.0 + */ +export class Flusher extends Context.Service "flushed" + * ``` + */ + readonly flush: Effect.Effect + readonly register: (run: Effect.Effect) => Effect.Effect +}>()( + "effect/observability/OtlpExporter/Flusher" +) {} + +/** + * Provides a `Flusher` backed by a fresh registry. + * + * **Details** + * + * This is intentionally a single module-level constant rather than a factory: + * layer memoization is keyed by layer instance, so every signal layer + * referencing this same constant shares one registry per layer build, and one + * `flush` drains traces, logs and metrics together. A factory returning a new + * layer per call would silently create one registry per signal. + * + * Registration is scoped — an exporter is removed from the registry when its + * own scope closes. Flushing with an empty registry is a no-op. + * + * Note that `flush` cannot await an export that was already in flight when it + * was called (for example one started by the export interval); it only waits + * for the exports it initiates. + * + * @category layers + * @since 4.0.0 + */ +export const layerFlusher: Layer.Layer = Layer.sync(Flusher, () => { + const registry = new Set>() + return { + flush: Effect.suspend(() => { + if (registry.size === 0) { + return Effect.void + } + return Effect.forEach(registry, identity, { + concurrency: "unbounded", + discard: true + }) + }), + register: (run) => + Effect.flatMap(Scope.Scope, (scope) => { + registry.add(run) + return Scope.addFinalizer( + scope, + Effect.sync(() => registry.delete(run)) + ) + }) + } +}) + /** * Creates a scoped OTLP batch exporter. * @@ -61,18 +167,17 @@ export const make: ( readonly label: string readonly exportInterval: Duration.Input readonly maxBatchSize: number | "disabled" - readonly body: (data: Array) => HttpBody + readonly body: (data: Array) => readonly [body: HttpBody, onSuccess: Effect.Effect] readonly shutdownTimeout: Duration.Input } ) => Effect.Effect< { readonly push: (data: unknown) => void }, never, - HttpClient.HttpClient | Scope.Scope + Flusher | HttpClient.HttpClient | Scope.Scope > = Effect.fnUntraced(function*(options) { const services = yield* Effect.context() const clock = Context.get(services, Clock) const scope = Context.get(services, Scope.Scope) - const runFork = Effect.runForkWith(services) const exportInterval = Duration.max(Duration.fromInputUnsafe(options.exportInterval), Duration.zero) let disabledUntil: number | undefined = undefined @@ -103,9 +208,11 @@ export const make: ( } buffer = [] } + const [body, onSuccess] = options.body(items) return client.execute( - HttpClientRequest.setBody(request, options.body(items)) + HttpClientRequest.setBody(request, body) ).pipe( + Effect.andThen(onSuccess), Effect.asVoid, Effect.withTracerEnabled(false) ) @@ -122,9 +229,19 @@ export const make: ( }) ) + const exportFibers = yield* FiberSet.make() + const runExportFork = yield* FiberSet.runtime(exportFibers)() + + const flusher = yield* Flusher + yield* flusher.register(runExport) + yield* Scope.addFinalizer( scope, - runExport.pipe( + Effect.suspend(() => { + if (disabledUntil !== undefined) return Effect.void + runExportFork(runExport) + return FiberSet.awaitEmpty(exportFibers) + }).pipe( Effect.ignore, Effect.interruptible, Effect.timeoutOption(options.shutdownTimeout) @@ -132,7 +249,8 @@ export const make: ( ) yield* Effect.sleep(exportInterval).pipe( - Effect.andThen(runExport), + Effect.andThen(FiberSet.run(exportFibers, runExport)), + Effect.flatMap(Fiber.await), Effect.forever, Effect.forkIn(scope) ) @@ -142,7 +260,7 @@ export const make: ( if (disabledUntil !== undefined) return buffer.push(data) if (options.maxBatchSize !== "disabled" && buffer.length >= options.maxBatchSize) { - Fiber.runIn(runFork(runExport), scope) + runExportFork(runExport) } } } diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpLogger.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpLogger.ts index c80d5caef..6a3a201dd 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpLogger.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpLogger.ts @@ -57,7 +57,7 @@ export const make: ( ) => Effect.Effect< Logger.Logger, never, - OtlpSerialization | HttpClient.HttpClient | Scope.Scope + Exporter.Flusher | OtlpSerialization | HttpClient.HttpClient | Scope.Scope > = Effect.fnUntraced(function*(options) { const serialization = yield* OtlpSerialization const otelResource = yield* OtlpResource.fromConfig(options.resource) @@ -71,7 +71,7 @@ export const make: ( headers: options.headers, maxBatchSize: options.maxBatchSize ?? 1000, exportInterval: options.exportInterval ?? Duration.seconds(1), - body: (data) => + body: (data) => [ serialization.logs({ resourceLogs: [{ resource: otelResource, @@ -81,6 +81,8 @@ export const make: ( }] }] }), + Effect.void + ], shutdownTimeout: options.shutdownTimeout ?? Duration.seconds(3) }) @@ -116,10 +118,10 @@ export const layer = (options: { readonly shutdownTimeout?: Duration.Input | undefined readonly excludeLogSpans?: boolean | undefined readonly mergeWithExisting?: boolean | undefined -}): Layer.Layer => +}): Layer.Layer => Logger.layer([make(options)], { mergeWithExisting: options.mergeWithExisting ?? true - }) + }).pipe(Layer.provideMerge(Exporter.layerFlusher)) /** * Creates an OTLP logs layer from OpenTelemetry configuration. @@ -136,7 +138,7 @@ export const layerFromConfig = (options?: { readonly headers?: Headers.Input | undefined readonly excludeLogSpans?: boolean | undefined readonly mergeWithExisting?: boolean | undefined -}): Layer.Layer => +}): Layer.Layer => Effect.gen(function*() { const { disabled, endpoint, exporters } = yield* Config.all({ disabled: Config.boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false)), @@ -145,7 +147,7 @@ export const layerFromConfig = (options?: { }) if (disabled || !endpoint || !exporters.includes("otlp")) { - return Layer.empty + return Exporter.layerFlusher } const { baseTimeout, logsTimeout, exportTimeout, scheduleDelay, maxBatchSize } = yield* Config.all({ diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpMetrics.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpMetrics.ts index 6ec1e9c6e..5dd5c35eb 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpMetrics.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpMetrics.ts @@ -40,7 +40,8 @@ import { OtlpSerialization } from "./OtlpSerialization.ts" * * **Example** (Configuring aggregation temporality) * - * ```ts + * ```ts import.meta.vitest + * import { Layer } from "effect" * import { OtlpMetrics } from "effect/unstable/observability" * * // Use delta temporality for backends that prefer it (e.g., Datadog, Dynatrace) @@ -54,6 +55,8 @@ import { OtlpSerialization } from "./OtlpSerialization.ts" * url: "http://localhost:4318/v1/metrics", * temporality: "cumulative" // This is the default * }) + * + * const result = [Layer.isLayer(metricsLayer), Layer.isLayer(cumulativeLayer)] // => [true, true] * ``` * * @category models @@ -67,6 +70,9 @@ export type AggregationTemporality = "cumulative" | "delta" * **Details** * * The exporter snapshots registered Effect metrics on the configured interval, serializes them with the selected aggregation temporality, and flushes during scope finalization up to `shutdownTimeout`. + * Manual flushing also triggers a snapshot. With delta temporality, each + * successful export advances the previous-export state, so frequent successful + * flushes narrow the delta aggregation windows. * * @category constructors * @since 4.0.0 @@ -85,7 +91,7 @@ export const make: (options: { }) => Effect.Effect< void, never, - HttpClient.HttpClient | OtlpSerialization | Scope.Scope + Exporter.Flusher | HttpClient.HttpClient | OtlpSerialization | Scope.Scope > = Effect.fnUntraced(function*(options) { const clock = yield* Clock const serialization = yield* OtlpSerialization @@ -102,15 +108,22 @@ export const make: (options: { // State for delta temporality tracking let previousExportTimeNanos: bigint = startTimeNanos - const previousCounterState = new Map() - const previousHistogramState = new Map() - const previousFrequencyState = new Map>() - const previousSummaryState = new Map() - - const snapshot = (): HttpBody => { + let previousCounterState = new Map() + let previousHistogramState = new Map() + let previousFrequencyState = new Map>() + let previousSummaryState = new Map() + let snapshotSequence = 0 + let committedSnapshotSequence = -1 + + const snapshot = (): readonly [body: HttpBody, onSuccess: Effect.Effect] => { const snapshot = Metric.snapshotUnsafe(services) + const currentSnapshotSequence = snapshotSequence++ const nowNanos = clock.currentTimeNanosUnsafe() const nowTime = String(nowNanos) + const nextCounterState = new Map(previousCounterState) + const nextHistogramState = new Map(previousHistogramState) + const nextFrequencyState = new Map(previousFrequencyState) + const nextSummaryState = new Map(previousSummaryState) const metricData: Array = [] const metricDataByName = new Map() const addMetricData = (data: IMetric) => { @@ -156,7 +169,7 @@ export const make: (options: { } } } - previousCounterState.set(metricKey, currentCount) + nextCounterState.set(metricKey, currentCount) } const dataPoint: INumberDataPoint = { @@ -247,7 +260,7 @@ export const make: (options: { // Note: This is a limitation - true delta min/max would require tracking // observations within each interval } - previousHistogramState.set(metricKey, { + nextHistogramState.set(metricKey, { count: state.state.count, sum: state.state.sum, bucketCounts: currentBuckets.counts.slice(), @@ -308,7 +321,7 @@ export const make: (options: { } if (isDelta) { - previousFrequencyState.set(metricKey, currentOccurrences) + nextFrequencyState.set(metricKey, currentOccurrences) } if (metricDataByName.has(state.id)) { @@ -360,7 +373,7 @@ export const make: (options: { reportCount = state.state.count - previousState.count reportSum = state.state.sum - previousState.sum } - previousSummaryState.set(metricKey, { + nextSummaryState.set(metricKey, { count: state.state.count, sum: state.state.sum }) @@ -420,12 +433,7 @@ export const make: (options: { } } - // Update the previous export time for delta calculations - if (isDelta) { - previousExportTimeNanos = nowNanos - } - - return serialization.metrics({ + const body = serialization.metrics({ resourceMetrics: [{ resource, scopeMetrics: [{ @@ -434,6 +442,18 @@ export const make: (options: { }] }] }) + const onSuccess = isDelta + ? Effect.sync(() => { + if (currentSnapshotSequence < committedSnapshotSequence) return + previousCounterState = nextCounterState + previousHistogramState = nextHistogramState + previousFrequencyState = nextFrequencyState + previousSummaryState = nextSummaryState + previousExportTimeNanos = nowNanos + committedSnapshotSequence = currentSnapshotSequence + }) + : Effect.void + return [body, onSuccess] } yield* Exporter.make({ @@ -464,7 +484,8 @@ export const layer = (options: { readonly exportInterval?: Duration.Input | undefined readonly shutdownTimeout?: Duration.Input | undefined readonly temporality?: AggregationTemporality | undefined -}): Layer.Layer => Layer.effectDiscard(make(options)) +}): Layer.Layer => + Layer.effectDiscard(make(options)).pipe(Layer.provideMerge(Exporter.layerFlusher)) /** * Creates an OTLP metrics layer from OpenTelemetry configuration. @@ -479,7 +500,7 @@ export const layerFromConfig = (options?: { readonly attributes?: Record } | undefined readonly headers?: Headers.Input | undefined -}): Layer.Layer => +}): Layer.Layer => Effect.gen(function*() { const { disabled, endpoint, exporters } = yield* Config.all({ disabled: Config.boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false)), @@ -487,7 +508,7 @@ export const layerFromConfig = (options?: { exporters: OtlpEnv.exporters("METRICS") }) if (disabled || !endpoint || !exporters.includes("otlp")) { - return Layer.empty + return Exporter.layerFlusher } const { baseTimeout, metricsTimeout, exportTimeout, exportInterval, temporalityPreference } = yield* Config.all({ diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpResource.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpResource.ts index 8602e53ce..f77181645 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpResource.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpResource.ts @@ -70,11 +70,20 @@ export const make = (options: { * Creates an OTLP resource from explicit options and OpenTelemetry * configuration. * + * **When to use** + * + * Use when resource metadata may be configured in code or by the deployment + * environment. To let operators set the service identity, omit `serviceName`, + * `serviceVersion`, and their matching attributes, then use + * `OTEL_SERVICE_NAME` and `OTEL_RESOURCE_ATTRIBUTES`. + * * **Details** * - * `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME`, and - * `OTEL_SERVICE_VERSION` override explicit options; missing required - * configuration is converted to a defect. + * Explicit `serviceName` and `serviceVersion` options take precedence over + * matching explicit attributes. Explicit attributes take precedence over + * environment variables. `OTEL_SERVICE_NAME` and `OTEL_SERVICE_VERSION` take + * precedence over matching attributes in `OTEL_RESOURCE_ATTRIBUTES`. Missing + * required configuration is converted to a defect. * * @category constructors * @since 4.0.0 @@ -91,24 +100,24 @@ export const fromConfig: ( readonly attributes?: Record | undefined }) { const env = yield* Config.schema( - Schema.UndefinedOr(Config.Record(Schema.String, Schema.String)), + Schema.UndefinedOr(Config.Record(Schema.StringFromUriComponent, Schema.StringFromUriComponent)), "OTEL_RESOURCE_ATTRIBUTES" ) - const serviceName = (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME")) - ?? env?.["service.name"] as string | undefined + const serviceName = options?.serviceName ?? options?.attributes?.["service.name"] as string | undefined - ?? options?.serviceName + ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_NAME")) + ?? env?.["service.name"] as string | undefined ?? (yield* Config.string("OTEL_SERVICE_NAME")) - const serviceVersion = (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_VERSION")) - ?? env?.["service.version"] as string | undefined + const serviceVersion = options?.serviceVersion ?? options?.attributes?.["service.version"] as string | undefined - ?? options?.serviceVersion + ?? (yield* Config.schema(Schema.UndefinedOr(Schema.String), "OTEL_SERVICE_VERSION")) + ?? env?.["service.version"] as string | undefined const attributes = { - ...options?.attributes, - ...env + ...env, + ...options?.attributes } delete attributes["service.name"] @@ -133,7 +142,7 @@ export const fromConfig: ( * * Throws if the resource does not contain a string `service.name` attribute. * - * @category Attributes + * @category attributes * @since 4.0.0 */ export const serviceNameUnsafe = (resource: Resource): string => { @@ -149,7 +158,7 @@ export const serviceNameUnsafe = (resource: Resource): string => { /** * Converts key/value entries into OTLP `KeyValue` attributes. * - * @category Attributes + * @category attributes * @since 4.0.0 */ export const entriesToAttributes = (entries: Iterable<[string, unknown]>): Array => { @@ -171,7 +180,7 @@ export const entriesToAttributes = (entries: Iterable<[string, unknown]>): Array * Arrays are converted recursively, primitive values use their matching OTLP * fields, and unsupported values are formatted as strings. * - * @category Attributes + * @category attributes * @since 4.0.0 */ export const unknownToAttributeValue = (value: unknown): AnyValue => { @@ -189,7 +198,7 @@ export const unknownToAttributeValue = (value: unknown): AnyValue => { } case "bigint": return { - intValue: Number(value) + intValue: String(value) } case "number": return Number.isInteger(value) @@ -235,7 +244,7 @@ export interface AnyValue { /** AnyValue boolValue */ boolValue?: boolean | null /** AnyValue intValue */ - intValue?: number | null + intValue?: string | number | null /** AnyValue doubleValue */ doubleValue?: number | null /** AnyValue arrayValue */ diff --git a/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts b/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts index 9a2b515b8..d24aad30c 100644 --- a/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts +++ b/.context/effect/packages/effect/src/unstable/observability/OtlpTracer.ts @@ -59,7 +59,7 @@ export const make: ( ) => Effect.Effect< Tracer.Tracer, never, - OtlpSerialization | HttpClient.HttpClient | Scope.Scope + Exporter.Flusher | OtlpSerialization | HttpClient.HttpClient | Scope.Scope > = Effect.fnUntraced(function*(options) { const otelResource = yield* OtlpResource.fromConfig(options.resource) const serialization = yield* OtlpSerialization @@ -83,7 +83,7 @@ export const make: ( }] }] } - return serialization.traces(data) + return [serialization.traces(data), Effect.void] }, shutdownTimeout: options.shutdownTimeout ?? Duration.seconds(3) }) @@ -134,7 +134,11 @@ export const layer: (options: { readonly maxBatchSize?: number | undefined readonly context?: ((primitive: Tracer.EffectPrimitive, span: Tracer.AnySpan) => X) | undefined readonly shutdownTimeout?: Duration.Input | undefined -}) => Layer.Layer = flow(make, Layer.effect(Tracer.Tracer)) +}) => Layer.Layer = flow( + make, + Layer.effect(Tracer.Tracer), + Layer.provideMerge(Exporter.layerFlusher) +) /** * Creates an OTLP traces layer from OpenTelemetry configuration. @@ -150,7 +154,7 @@ export const layerFromConfig = (options?: { } | undefined readonly headers?: Headers.Input | undefined readonly context?: ((primitive: Tracer.EffectPrimitive, span: Tracer.AnySpan) => X) | undefined -}): Layer.Layer => +}): Layer.Layer => Effect.gen(function*() { const { disabled, endpoint, exporters } = yield* Config.all({ disabled: Config.boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false)), @@ -159,7 +163,7 @@ export const layerFromConfig = (options?: { }) if (disabled || !endpoint || !exporters.includes("otlp")) { - return Layer.empty + return Exporter.layerFlusher } const { baseTimeout, tracesTimeout, exportTimeout, scheduleDelay, maxBatchSize } = yield* Config.all({ diff --git a/.context/effect/packages/effect/src/unstable/observability/PrometheusMetrics.ts b/.context/effect/packages/effect/src/unstable/observability/PrometheusMetrics.ts index 8646a5452..abc55ee28 100644 --- a/.context/effect/packages/effect/src/unstable/observability/PrometheusMetrics.ts +++ b/.context/effect/packages/effect/src/unstable/observability/PrometheusMetrics.ts @@ -19,12 +19,14 @@ import * as HttpServerResponse from "../http/HttpServerResponse.ts" * * **Example** (Mapping metric names) * - * ```ts + * ```ts import.meta.vitest * import type { PrometheusMetrics } from "effect/unstable/observability" * * // Convert camelCase to snake_case * const mapper: PrometheusMetrics.MetricNameMapper = (name) => * name.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase() + * + * mapper("httpRequests") // => "http_requests" * ``` * * @category models @@ -68,7 +70,7 @@ export interface HttpOptions extends FormatOptions { * * **Example** (Formatting metrics) * - * ```ts + * ```ts import.meta.vitest * import { Effect, Metric } from "effect" * import { PrometheusMetrics } from "effect/unstable/observability" * @@ -88,7 +90,11 @@ export interface HttpOptions extends FormatOptions { * * // Format with prefix * const output2 = yield* PrometheusMetrics.format({ prefix: "myapp" }) + * + * return [output1.includes("api_requests_total"), output2.includes("myapp_active_connections")] * }) + * + * Effect.runSync(program) // => [true, true] * ``` * * @category formatting @@ -156,7 +162,8 @@ export const formatUnsafe = ( * * **Example** (Serving metrics over HTTP) * - * ```ts + * ```ts import.meta.vitest + * import { Layer } from "effect" * import { PrometheusMetrics } from "effect/unstable/observability" * * // Create a layer that adds /metrics endpoint to the router @@ -167,9 +174,11 @@ export const formatUnsafe = ( * path: "/prometheus/metrics", * prefix: "myapp" * }) + * + * const result = [Layer.isLayer(PrometheusLayer), Layer.isLayer(CustomPrometheusLayer)] // => [true, true] * ``` * - * @category Http + * @category layers * @since 4.0.0 */ export const layerHttp = ( diff --git a/.context/effect/packages/effect/src/unstable/observability/internal/otlpEnv.ts b/.context/effect/packages/effect/src/unstable/observability/internal/otlpEnv.ts index 1004a83e1..013dd9dc9 100644 --- a/.context/effect/packages/effect/src/unstable/observability/internal/otlpEnv.ts +++ b/.context/effect/packages/effect/src/unstable/observability/internal/otlpEnv.ts @@ -11,7 +11,7 @@ const ExporterList = Config.Array(Schema.String).pipe( }) ) -const HeadersRecord = Config.Record(Schema.String, Schema.String) +const HeadersRecord = Config.Record(Schema.String, Schema.StringFromUriComponent) export const headers = (signal: Signal) => Config.schema(HeadersRecord, `OTEL_EXPORTER_OTLP_${signal}_HEADERS`).pipe( diff --git a/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts b/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts index cf6fa3972..56be450b1 100644 --- a/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts +++ b/.context/effect/packages/effect/src/unstable/observability/internal/protobuf.ts @@ -29,6 +29,9 @@ const encodeTag = (fieldNumber: number, wireType: WireType): number => (fieldNum export const encodeVarint = (value: number | bigint): Uint8Array => { const bytes: Array = [] let n = typeof value === "bigint" ? value : BigInt(value) + if (n < BigInt(0)) { + n = BigInt.asUintN(64, n) + } while (n > BigInt(127)) { bytes.push(Number(n & BigInt(127)) | 0x80) n >>= BigInt(7) diff --git a/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts b/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts index 061536fff..f03bcfc8a 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/KeyValueStore.ts @@ -341,7 +341,12 @@ export const layerMemory: Layer.Layer = Layer.sync(KeyValueStore) * * **Details** * - * The directory is created if needed, and each key is encoded as a file name. + * The directory is created if needed, and each key is percent-encoded as a + * single file name. Empty keys, `.` and `..` are rejected. Keys are only + * guaranteed to be distinct on case-sensitive file systems. + * + * `clear` removes the directory recursively, so it must not be shared with + * unrelated data. * * @category layers * @since 4.0.0 @@ -352,7 +357,20 @@ export const layerFileSystem = ( Layer.effect(KeyValueStore)(Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path - const keyPath = (key: string) => path.join(directory, encodeURIComponent(key)) + const withKeyPath = ( + method: string, + key: string, + f: (path: string) => Effect.Effect + ): Effect.Effect => + key.length === 0 || key === "." || key === ".." + ? Effect.fail( + new KeyValueStoreError({ + method, + key, + message: `Invalid key ${key}` + }) + ) + : f(path.join(directory, encodeURIComponent(key))) if (!(yield* fs.exists(directory))) { yield* fs.makeDirectory(directory, { recursive: true }) @@ -360,60 +378,65 @@ export const layerFileSystem = ( return make({ get: (key: string) => - Effect.catchTag( - fs.readFileString(keyPath(key)), - "PlatformError", - (cause) => - cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail( - new KeyValueStoreError({ - method: "get", - key, - message: `Unable to get item with key ${key}`, - cause - }) - ) - ), + withKeyPath("get", key, (path) => + Effect.catchTag( + fs.readFileString(path), + "PlatformError", + (cause) => + cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail( + new KeyValueStoreError({ + method: "get", + key, + message: `Unable to get item with key ${key}`, + cause + }) + ) + )), getUint8Array: (key: string) => - Effect.catchTag( - fs.readFile(keyPath(key)), - "PlatformError", - (cause) => - cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail( + withKeyPath("getUint8Array", key, (path) => + Effect.catchTag( + fs.readFile(path), + "PlatformError", + (cause) => + cause.reason._tag === "NotFound" ? Effect.undefined : Effect.fail( + new KeyValueStoreError({ + method: "getUint8Array", + key, + message: `Unable to get item with key ${key}`, + cause + }) + ) + )), + set: (key: string, value: string | Uint8Array) => + withKeyPath("set", key, (path) => + Effect.mapError( + typeof value === "string" ? fs.writeFileString(path, value) : fs.writeFile(path, value), + (cause) => new KeyValueStoreError({ - method: "getUint8Array", + method: "set", key, - message: `Unable to get item with key ${key}`, + message: `Unable to set item with key ${key}`, cause }) - ) - ), - set: (key: string, value: string | Uint8Array) => - Effect.mapError( - typeof value === "string" ? fs.writeFileString(keyPath(key), value) : fs.writeFile(keyPath(key), value), - (cause) => + )), + remove: (key: string) => + withKeyPath("remove", key, (path) => + Effect.mapError(fs.remove(path), (cause) => new KeyValueStoreError({ - method: "set", + method: "remove", key, - message: `Unable to set item with key ${key}`, + message: `Unable to remove item with key ${key}`, cause - }) - ), - remove: (key: string) => - Effect.mapError(fs.remove(keyPath(key)), (cause) => - new KeyValueStoreError({ - method: "remove", - key, - message: `Unable to remove item with key ${key}`, - cause - })), + }))), has: (key: string) => - Effect.mapError(fs.exists(keyPath(key)), (cause) => - new KeyValueStoreError({ - method: "has", - key, - message: `Unable to check existence of item with key ${key}`, - cause - })), + withKeyPath("has", key, (path) => + Effect.mapError(fs.exists(path), (cause) => + new KeyValueStoreError({ + method: "has", + key, + message: `Unable to check existence of item with key ${key}`, + cause + }))), clear: Effect.mapError( Effect.andThen( fs.remove(directory, { recursive: true }), @@ -678,7 +701,7 @@ const SchemaStoreTypeId = "~effect/persistence/KeyValueStore/SchemaStore" as con /** * Schema-aware view of a `KeyValueStore` that stores values as encoded JSON. * - * @category SchemaStore + * @category models * @since 4.0.0 */ export interface SchemaStore { @@ -739,7 +762,7 @@ export interface SchemaStore { /** * Adapts a `KeyValueStore` into a `SchemaStore` using the schema's JSON codec. * - * @category SchemaStore + * @category converting * @since 4.0.0 */ export const toSchemaStore = (self: KeyValueStore, schema: S): SchemaStore => { diff --git a/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts b/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts index 82a809f89..7935e7981 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/Persistable.ts @@ -10,6 +10,7 @@ import type * as Duration from "../../Duration.ts" import type * as Effect from "../../Effect.ts" import type * as Exit from "../../Exit.ts" +import * as InternalRecord from "../../internal/record.ts" import * as PrimaryKey from "../../PrimaryKey.ts" import * as Request from "../../Request.ts" import * as Schema from "../../Schema.ts" @@ -55,7 +56,7 @@ export type Any = Persistable /** * Extracts the success schema from a persistable request. * - * @category models + * @category utility types * @since 4.0.0 */ export type SuccessSchema = A["~effect/persistence/Persistable"]["success"] @@ -63,7 +64,7 @@ export type SuccessSchema = A["~effect/persistence/Persistable"][ /** * Extracts the success value type from a persistable request. * - * @category models + * @category utility types * @since 4.0.0 */ export type Success = A["~effect/persistence/Persistable"]["success"]["Type"] @@ -71,7 +72,7 @@ export type Success = A["~effect/persistence/Persistable"]["succe /** * Extracts the error schema from a persistable request. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorSchema = A["~effect/persistence/Persistable"]["error"] @@ -79,7 +80,7 @@ export type ErrorSchema = A["~effect/persistence/Persistable"]["e /** * Extracts the error value type from a persistable request. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = A["~effect/persistence/Persistable"]["error"]["Type"] @@ -88,7 +89,7 @@ export type Error = A["~effect/persistence/Persistable"]["error"] * Services required to decode a persisted success or error value for the * request. * - * @category models + * @category utility types * @since 4.0.0 */ export type DecodingServices = @@ -98,7 +99,7 @@ export type DecodingServices = /** * Services required to encode a success or error value for persistence. * - * @category models + * @category utility types * @since 4.0.0 */ export type EncodingServices = @@ -109,7 +110,7 @@ export type EncodingServices = * All schema services required to encode and decode a persistable request * result. * - * @category models + * @category utility types * @since 4.0.0 */ export type Services = @@ -122,7 +123,7 @@ export type Services = * Computes the time to live for a persisted result from the result `Exit` and * request value. * - * @category models + * @category utility types * @since 4.0.0 */ export type TimeToLiveFn = (exit: Exit.Exit, Error>, request: K) => Duration.Input @@ -179,10 +180,10 @@ export const Class = < | ("requires" extends keyof Config ? Config["requires"] : never) > => { - function Persistable(this: any, props: any) { + function Persistable(this: any, props: object | undefined) { this._tag = tag if (props) { - Object.assign(this, props) + InternalRecord.assignProperties(this, props) } } Persistable.prototype = { diff --git a/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts b/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts index 670cd644e..c8c0ab155 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/PersistedQueue.ts @@ -217,7 +217,7 @@ export type ErrorTypeId = "~@effect/experimental/PersistedQueue/PersistedQueueEr * @category errors * @since 4.0.0 */ -export class PersistedQueueError extends Schema.ErrorClass( +export class PersistedQueueError extends Schema.Error( "effect/persistence/PersistedQueue/PersistedQueueError" )({ _tag: Schema.tag("PersistedQueueError"), @@ -245,7 +245,7 @@ export class PersistedQueueError extends Schema.ErrorClass( * The store persists offered elements and returns taken elements in a scope so * the finalizer can complete or retry them based on the processing exit. * - * @category store + * @category services * @since 4.0.0 */ export class PersistedQueueStore extends Context.Service< @@ -283,7 +283,7 @@ export class PersistedQueueStore extends Context.Service< * The store is process-local and volatile; failed takes are requeued until the * configured maximum attempts is reached. * - * @category store + * @category layers * @since 4.0.0 */ export const layerStoreMemory: Layer.Layer< @@ -294,9 +294,9 @@ export const layerStoreMemory: Layer.Layer< attempts: number readonly element: unknown } - const ids = new Set() const queues = new Map items: Set }>() const getOrCreateQueue = (name: string) => { @@ -304,6 +304,7 @@ export const layerStoreMemory: Layer.Layer< if (!queue) { queue = { latch: Latch.makeUnsafe(false), + ids: new Set(), items: new Set() } queues.set(name, queue) @@ -314,9 +315,9 @@ export const layerStoreMemory: Layer.Layer< return PersistedQueueStore.of({ offer: (options) => Effect.sync(() => { - if (ids.has(options.id)) return - ids.add(options.id) const queue = getOrCreateQueue(options.name) + if (queue.ids.has(options.id)) return + queue.ids.add(options.id) queue.items.add({ id: options.id, attempts: 0, element: options.element }) queue.latch.openUnsafe() }), @@ -357,7 +358,7 @@ export const layerStoreMemory: Layer.Layer< * refreshes locks while items are being processed, and moves exhausted items * to a failed queue. * - * @category store + * @category constructors * @since 4.0.0 */ export const makeStoreRedis = Effect.fnUntraced(function*( @@ -720,7 +721,7 @@ end /** * Provides a Redis-backed `PersistedQueueStore` using `makeStoreRedis`. * - * @category store + * @category layers * @since 4.0.0 */ export const layerStoreRedis: ( @@ -745,7 +746,7 @@ export const layerStoreRedis: ( * per-worker locks, refreshes active locks while scoped takes are running, and * retries or completes rows according to the processing exit. * - * @category store + * @category constructors * @since 4.0.0 */ export const makeStoreSql: ( @@ -857,9 +858,11 @@ export const makeStoreSql: ( yield* sql.onDialectOrElse({ mssql: () => sql`IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = N'idx_${tableName}_id') - CREATE UNIQUE INDEX idx_${tableNameSql}_id ON ${tableNameSql} (id)`, - mysql: () => sql`CREATE UNIQUE INDEX ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id)`.pipe(Effect.ignore), - orElse: () => sql`CREATE UNIQUE INDEX IF NOT EXISTS ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id)` + CREATE UNIQUE INDEX idx_${tableNameSql}_id ON ${tableNameSql} (id, queue_name)`, + mysql: () => + sql`CREATE UNIQUE INDEX ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id, queue_name)`.pipe(Effect.ignore), + orElse: () => + sql`CREATE UNIQUE INDEX IF NOT EXISTS ${sql(`idx_${tableName}_id`)} ON ${tableNameSql} (id, queue_name)` }) yield* sql.onDialectOrElse({ @@ -894,7 +897,7 @@ export const makeStoreSql: ( sql` INSERT INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at) VALUES (${id}, ${name}, ${element}, FALSE, 0, ${sqlNow}, ${sqlNow}) - ON CONFLICT (id) DO NOTHING + ON CONFLICT (id, queue_name) DO NOTHING `, mysql: () => (id: string, name: string, element: string) => sql` @@ -903,7 +906,7 @@ export const makeStoreSql: ( `, mssql: () => (id: string, name: string, element: string) => sql` - IF NOT EXISTS (SELECT 1 FROM ${tableNameSql} WHERE id = ${id}) + IF NOT EXISTS (SELECT 1 FROM ${tableNameSql} WHERE id = ${id} AND queue_name = ${name}) BEGIN INSERT INTO ${tableNameSql} (id, queue_name, element, completed, attempts, created_at, updated_at) VALUES (${id}, ${name}, ${element}, 0, 0, ${sqlNow}, ${sqlNow}) @@ -1181,7 +1184,7 @@ class QueueKey extends Data.Class<{ /** * Provides a SQL-backed `PersistedQueueStore` using `makeStoreSql`. * - * @category store + * @category layers * @since 4.0.0 */ export const layerStoreSql: ( diff --git a/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts b/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts index 975aea833..631df6ed7 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/Persistence.ts @@ -15,8 +15,10 @@ import * as Duration from "../../Duration.ts" import * as Effect from "../../Effect.ts" import * as Exit from "../../Exit.ts" import { identity } from "../../Function.ts" +import { sqlCleanupBatchSize } from "../../internal/persistence.ts" import * as Layer from "../../Layer.ts" import * as PrimaryKey from "../../PrimaryKey.ts" +import * as Schedule from "../../Schedule.ts" import * as Schema from "../../Schema.ts" import type * as Scope from "../../Scope.ts" import * as SqlClient from "../sql/SqlClient.ts" @@ -33,7 +35,7 @@ const ErrorTypeId = "~effect/persistence/Persistence/PersistenceError" as const * @category errors * @since 4.0.0 */ -export class PersistenceError extends Schema.ErrorClass(ErrorTypeId)({ +export class PersistenceError extends Schema.Error(ErrorTypeId)({ _tag: Schema.tag("PersistenceError"), message: Schema.String, cause: Schema.optional(Schema.Defect()) @@ -50,7 +52,7 @@ export class PersistenceError extends Schema.ErrorClass(ErrorT * Service for creating scoped stores of persisted `Persistable` request * results. * - * @category models + * @category services * @since 4.0.0 */ export class Persistence extends Context.Service sql` @@ -322,7 +325,7 @@ export const layerBackingSqlMultiTable: Layer.Layer< `, mssql: () => sql` - IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U') + IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${tableName} AND xtype='U') CREATE TABLE ${table} ( id NVARCHAR(450) PRIMARY KEY, value NVARCHAR(MAX) NOT NULL, @@ -360,6 +363,20 @@ export const layerBackingSqlMultiTable: Layer.Layer< INSERT INTO ${table} ${sql.insert(entries)} ON DUPLICATE KEY UPDATE value=VALUES(value), expires=VALUES(expires) `.unprepared, + mssql: (): UpsertFn => (entries) => + Effect.forEach( + entries, + (entry) => + sql` + MERGE ${table} AS target + USING (SELECT ${entry.id} AS id, ${entry.value} AS value, ${entry.expires} AS expires) AS source + ON target.id = source.id + WHEN MATCHED THEN UPDATE SET value = source.value, expires = source.expires + WHEN NOT MATCHED THEN INSERT (id, value, expires) + VALUES (source.id, source.value, source.expires); + `, + { discard: true } + ), // sqlite orElse: (): UpsertFn => (entries) => sql` @@ -412,18 +429,16 @@ export const layerBackingSqlMultiTable: Layer.Layer< }) ), Effect.flatMap((rows) => { - const out = new Array(keys.length) + const values = new Map() for (let i = 0; i < rows.length; i++) { const row = rows[i] - const index = keys.indexOf(row.id) - if (index === -1) continue try { - out[index] = JSON.parse(row.value) + values.set(row.id, JSON.parse(row.value)) } catch { // ignore } } - return Effect.succeed(out as Arr.NonEmptyArray) + return Effect.succeed(keys.map((key) => values.get(key)) as Arr.NonEmptyArray) }) ), set: (key, value, ttl) => @@ -538,7 +553,7 @@ export const layerBackingSql: Layer.Layer< `, mssql: () => sql` - IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${table} AND xtype='U') + IF NOT EXISTS (SELECT * FROM sysobjects WHERE name=${"effect_persistence"} AND xtype='U') CREATE TABLE ${table} ( store_id NVARCHAR(191) NOT NULL, id NVARCHAR(191) NOT NULL, @@ -560,6 +575,120 @@ export const layerBackingSql: Layer.Layer< ` }).pipe(Effect.orDie) + yield* sql.onDialectOrElse({ + pg: () => + sql`CREATE INDEX IF NOT EXISTS effect_persistence_expires_idx ON ${table} (expires) WHERE expires IS NOT NULL` + .pipe(Effect.orDie, Effect.asVoid), + mysql: () => + Effect.gen(function*() { + const indexExists = sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'effect_persistence' + AND index_name = 'effect_persistence_expires_idx' + `.pipe( + Effect.map((rows) => Number(rows[0].count) > 0) + ) + + yield* sql`CREATE INDEX effect_persistence_expires_idx ON ${table} (expires)`.pipe( + Effect.catch((error) => Effect.flatMap(indexExists, (exists) => exists ? Effect.void : Effect.fail(error))) + ) + }).pipe(Effect.orDie), + mssql: () => + Effect.gen(function*() { + const indexExists = sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM sys.indexes + WHERE name = N'effect_persistence_expires_idx' + AND object_id = OBJECT_ID(N'effect_persistence') + `.pipe( + Effect.map((rows) => Number(rows[0].count) > 0) + ) + + yield* sql`CREATE INDEX effect_persistence_expires_idx ON ${table} (expires) WHERE expires IS NOT NULL`.pipe( + Effect.catch((error) => Effect.flatMap(indexExists, (exists) => exists ? Effect.void : Effect.fail(error))) + ) + }).pipe(Effect.orDie), + // sqlite + orElse: () => + sql`CREATE INDEX IF NOT EXISTS effect_persistence_expires_idx ON ${table} (expires) WHERE expires IS NOT NULL` + .pipe(Effect.orDie, Effect.asVoid) + }) + + const cleanupBatchDelay = Duration.millis(10) + const cleanupInterval = Duration.minutes(5) + + const deleteExpiredBatch = sql.onDialectOrElse({ + pg: () => (expiresAtOrBefore: number) => + sql<{ readonly count: number }>` + WITH deleted_entries AS ( + DELETE FROM ${table} + WHERE ctid IN ( + SELECT ctid FROM ${table} + WHERE expires IS NOT NULL AND expires <= ${expiresAtOrBefore} + LIMIT ${sql.literal(String(sqlCleanupBatchSize))} + ) + RETURNING 1 + ) + SELECT COUNT(*)::INT AS count FROM deleted_entries + `.pipe(Effect.map((rows) => rows[0].count)), + mysql: () => + Effect.fnUntraced( + function*(expiresAtOrBefore: number) { + const connection = yield* sql.reserve + const [statement, parameters] = sql` + DELETE FROM ${table} + WHERE expires IS NOT NULL AND expires <= ${expiresAtOrBefore} + LIMIT ${sql.literal(String(sqlCleanupBatchSize))} + `.compile() + yield* connection.execute(statement, parameters, undefined) + const rows = yield* connection.executeValues("SELECT ROW_COUNT()", []) + return Number(rows[0][0]) + }, + Effect.scoped + ), + mssql: () => (expiresAtOrBefore: number) => + sql<{ readonly store_id: string }>` + WITH expired_entries AS ( + SELECT TOP ${sql.literal(String(sqlCleanupBatchSize))} store_id, id FROM ${table} + WITH (UPDLOCK, READPAST, READCOMMITTEDLOCK) + WHERE expires IS NOT NULL AND expires <= ${expiresAtOrBefore} + ) + DELETE persistence + OUTPUT DELETED.store_id + FROM ${table} AS persistence + INNER JOIN expired_entries + ON persistence.store_id = expired_entries.store_id + AND persistence.id = expired_entries.id + `.pipe(Effect.map((deletedEntries) => deletedEntries.length)), + // Some sqlite clients do not support interactive transactions, so use one bounded statement. + orElse: () => (expiresAtOrBefore: number) => + sql<{ readonly deleted: number }>` + DELETE FROM ${table} + WHERE rowid IN ( + SELECT rowid FROM ${table} + WHERE expires IS NOT NULL AND expires <= ${expiresAtOrBefore} + LIMIT ${sql.literal(String(sqlCleanupBatchSize))} + ) + RETURNING 1 AS deleted + `.pipe(Effect.map((deletedEntries) => deletedEntries.length)) + }) + + const deleteExpired = Effect.gen(function*() { + const expiresAtOrBefore = yield* Clock.currentTimeMillis + return yield* deleteExpiredBatch(expiresAtOrBefore).pipe( + Effect.repeat({ + while: (deletedCount) => deletedCount === sqlCleanupBatchSize, + schedule: Schedule.spaced(cleanupBatchDelay) + }) + ) + }) + + yield* deleteExpired.pipe( + Effect.catch((cause) => Effect.logWarning("Failed to clean up expired persistence entries", cause)), + Effect.repeat(Schedule.spaced(cleanupInterval)), + Effect.forkScoped + ) + type UpsertFn = ( entries: Array<{ store_id: string; id: string; value: string; expires: number | null }> ) => Effect.Effect @@ -606,11 +735,6 @@ export const layerBackingSql: Layer.Layer< make: Effect.fnUntraced(function*(storeId) { const clock = yield* Clock.Clock - // Cleanup expired entries on startup - yield* Effect.ignore( - sql`DELETE FROM ${table} WHERE store_id = ${storeId} AND expires IS NOT NULL AND expires <= ${clock.currentTimeMillisUnsafe()}` - ) - return identity({ get: (key) => sql< @@ -650,18 +774,16 @@ export const layerBackingSql: Layer.Layer< }) ), Effect.flatMap((rows) => { - const out = new Array(keys.length) + const values = new Map() for (let i = 0; i < rows.length; i++) { const row = rows[i] - const index = keys.indexOf(row.id) - if (index === -1) continue try { - out[index] = JSON.parse(row.value) + values.set(row.id, JSON.parse(row.value)) } catch { // ignore } } - return Effect.succeed(out as Arr.NonEmptyArray) + return Effect.succeed(keys.map((key) => values.get(key)) as Arr.NonEmptyArray) }) ), set: (key, value, ttl) => @@ -821,7 +943,13 @@ export const layerBackingRedis: Layer.Layer< Effect.mapError( ttl === undefined ? redis.send("SET", prefixed(key), JSON.stringify(value)) - : redis.send("SET", prefixed(key), JSON.stringify(value), "PX", String(Duration.toMillis(ttl))), + : redis.send( + "SET", + prefixed(key), + JSON.stringify(value), + "PX", + String(Math.ceil(Duration.toMillis(ttl))) + ), ({ cause }) => new PersistenceError({ message: `Failed to set key ${key} in Redis`, @@ -836,7 +964,7 @@ export const layerBackingRedis: Layer.Layer< const pkey = prefixed(key) sets.set(pkey, JSON.stringify(value)) if (ttl) { - expires.set(pkey, Duration.toMillis(ttl)) + expires.set(pkey, Math.ceil(Duration.toMillis(ttl))) } } return Effect.mapError( @@ -854,7 +982,7 @@ export const layerBackingRedis: Layer.Layer< ({ cause }) => new PersistenceError({ message: `Failed to remove key ${key} from Redis`, cause }) ), clear: redis.send>("KEYS", `${prefix}:*`).pipe( - Effect.flatMap((keys) => redis.send("DEL", ...keys)), + Effect.flatMap((keys) => keys.length === 0 ? Effect.void : redis.send("DEL", ...keys)), Effect.mapError(({ cause }) => new PersistenceError({ message: `Failed to clear keys from Redis`, @@ -983,7 +1111,6 @@ export const layerBackingKvs: Layer.Layer< 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( diff --git a/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts b/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts index 15984d9d1..6762b277a 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/RateLimiter.ts @@ -234,16 +234,17 @@ export const layer: Layer.Layer< * * **Example** (Applying rate limits to effects) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer } from "effect" * import { RateLimiter } from "effect/unstable/persistence" * - * Effect.gen(function*() { + * const messages: Array = [] + * const program = 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( + * yield* Effect.sync(() => messages.push("Making a request with rate limiting")).pipe( * withLimiter({ * key: "some-key", * limit: 10, @@ -252,7 +253,12 @@ export const layer: Layer.Layer< * algorithm: "fixed-window" * }) * ) - * }) + * }).pipe( + * Effect.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))) + * ) + * + * await Effect.runPromise(program) + * messages // => ["Making a request with rate limiting"] * ``` * * @category accessors @@ -279,56 +285,84 @@ export const makeWithRateLimiter: Effect.Effect< ) /** - * Accesses a function that sleeps when the rate limit is exceeded. + * Sleeps when the rate limit is exceeded. * * **Example** (Sleeping until rate limit permits) * - * ```ts - * import { Effect } from "effect" + * ```ts import.meta.vitest + * import { Effect, Layer } from "effect" * import { RateLimiter } from "effect/unstable/persistence" * - * 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", + * const program = Effect.gen(function*() { + * const limiter = yield* RateLimiter.RateLimiter + * const partiallyApplied = RateLimiter.sleep(limiter) + * const partial = yield* partiallyApplied({ + * key: "partial", * limit: 10, * window: "5 seconds", * algorithm: "fixed-window" * }) - * }) + * const direct = yield* RateLimiter.sleep(limiter, { + * key: "direct", + * limit: 10, + * window: "5 seconds", + * algorithm: "fixed-window" + * }) + * return [partial.remaining, direct.remaining] + * }).pipe( + * Effect.provide(RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory))) + * ) + * + * await Effect.runPromise(program) // => [9, 9] * ``` * * @category accessors * @since 4.0.0 */ -export const makeSleep: Effect.Effect< - ((options: { +export function sleep(self: RateLimiter): (options: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly window: Duration.Input + readonly limit: number + readonly key: string + readonly tokens?: number | undefined +}) => Effect.Effect +export function sleep(self: RateLimiter, options: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly window: Duration.Input + readonly limit: number + readonly key: string + readonly tokens?: number | undefined +}): Effect.Effect +export function sleep(self: RateLimiter, options?: { + readonly algorithm?: "fixed-window" | "token-bucket" | undefined + readonly window: Duration.Input + readonly limit: number + readonly key: string + readonly tokens?: number | undefined +}): + | Effect.Effect + | ((options: { readonly algorithm?: "fixed-window" | "token-bucket" | undefined readonly window: Duration.Input readonly limit: number readonly key: string readonly tokens?: number | undefined - }) => Effect.Effect), - never, - RateLimiter -> = RateLimiter.use((limiter) => - Effect.succeed((options) => - Effect.flatMap( - limiter.consume({ - ...options, - onExceeded: "delay" - }), - (result) => { - if (Duration.isZero(result.delay)) return Effect.succeed(result) - return Effect.as(Effect.sleep(result.delay), result) - } - ) + }) => Effect.Effect) +{ + if (options === undefined) { + return (options) => sleep(self, options) + } + return Effect.flatMap( + self.consume({ + ...options, + onExceeded: "delay" + }), + (result) => { + if (Duration.isZero(result.delay)) return Effect.succeed(result) + return Effect.as(Effect.sleep(result.delay), result) + } ) -) +} /** * Runtime type identifier for `RateLimiterError`. @@ -356,14 +390,14 @@ export type ErrorTypeId = "~@effect/experimental/RateLimiter/RateLimiterError" * @category errors * @since 4.0.0 */ -export class RateLimitExceeded extends Schema.ErrorClass( +export class RateLimitExceeded extends Schema.Error( "effect/persistence/RateLimiter/RateLimitExceeded" )({ _tag: Schema.tag("RateLimitExceeded"), retryAfter: Schema.DurationFromMillis, key: Schema.String, - limit: Schema.Number, - remaining: Schema.Number + limit: Schema.Finite, + remaining: Schema.Finite }) { /** * Public message used when the rate limiter rejects a request. @@ -381,7 +415,7 @@ export class RateLimitExceeded extends Schema.ErrorClass( * @category errors * @since 4.0.0 */ -export class RateLimitStoreError extends Schema.ErrorClass( +export class RateLimitStoreError extends Schema.Error( "effect/persistence/RateLimiter/RateLimitStoreError" )({ _tag: Schema.tag("RateLimitStoreError"), @@ -415,7 +449,7 @@ export const RateLimiterErrorReason: Schema.Union<[ * @category errors * @since 4.0.0 */ -export class RateLimiterError extends Schema.ErrorClass(ErrorTypeId)({ +export class RateLimiterError extends Schema.Error(ErrorTypeId)({ _tag: Schema.tag("RateLimiterError"), reason: RateLimiterErrorReason }) { @@ -486,7 +520,7 @@ export type AdaptivePhase = "inactive" | "cooldown" | "learning" | "learned" /** * Options for consuming tokens from the adaptive rate limiter store. * - * @category models + * @category options * @since 4.0.0 */ export interface AdaptiveConsumeOptions { @@ -537,7 +571,7 @@ export interface AdaptiveConsumeResult { /** * Options for reporting response feedback to the adaptive rate limiter store. * - * @category models + * @category options * @since 4.0.0 */ export interface AdaptiveFeedbackOptions { @@ -575,7 +609,7 @@ export interface AdaptiveFeedbackOptions { * Use to provide the shared counter storage and adaptive feedback state used by * persistent rate-limit checks. * - * @category store + * @category services * @since 4.0.0 */ export class RateLimiterStore extends Context.Service< @@ -655,7 +689,7 @@ interface AdaptiveState { /** * Provides a process-local in-memory `RateLimiterStore`. * - * @category RateLimiterStore + * @category layers * @since 4.0.0 */ export const layerStoreMemory: Layer.Layer< @@ -874,7 +908,7 @@ export const layerStoreMemory: Layer.Layer< * Creates a Redis-backed `RateLimiterStore` using Lua scripts and the * configured key prefix. * - * @category RateLimiterStore + * @category constructors * @since 4.0.0 */ export const makeStoreRedis = Effect.fnUntraced(function*( diff --git a/.context/effect/packages/effect/src/unstable/persistence/Redis.ts b/.context/effect/packages/effect/src/unstable/persistence/Redis.ts index a0453fa3e..aea3e7513 100644 --- a/.context/effect/packages/effect/src/unstable/persistence/Redis.ts +++ b/.context/effect/packages/effect/src/unstable/persistence/Redis.ts @@ -100,7 +100,7 @@ const ErrorTypeId: ErrorTypeId = "~effect/persistence/Redis/RedisError" * @category errors * @since 4.0.0 */ -export class RedisError extends Schema.ErrorClass(ErrorTypeId)({ +export class RedisError extends Schema.Error(ErrorTypeId)({ _tag: Schema.tag("RedisError"), cause: Schema.Defect() }) { @@ -123,7 +123,7 @@ const ScriptTypeId: ScriptTypeId = "~effect/persistence/Redis/Script" * It defines the Lua source, parameter-to-argument mapping, Redis key count, * and result type used by `Redis.eval`. * - * @category Scripting + * @category scripting * @since 4.0.0 */ export interface Script< @@ -173,7 +173,7 @@ const ScriptProto = { * The result type defaults to `void` and can be refined with * `withReturnType`. * - * @category Scripting + * @category scripting * @since 4.0.0 */ export const script = >( @@ -186,8 +186,8 @@ export const script = >( params: Params result: void }> => - Object.assign(Object.create(ScriptProto), { + Object.setPrototypeOf({ ...options, params: f, numberOfKeys: typeof options.numberOfKeys === "number" ? constant(options.numberOfKeys) : options.numberOfKeys - }) + }, ScriptProto) diff --git a/.context/effect/packages/effect/src/unstable/process/ChildProcess.ts b/.context/effect/packages/effect/src/unstable/process/ChildProcess.ts index 32b8ba29a..1c019b540 100644 --- a/.context/effect/packages/effect/src/unstable/process/ChildProcess.ts +++ b/.context/effect/packages/effect/src/unstable/process/ChildProcess.ts @@ -105,13 +105,14 @@ export type PipeToOption = "stdin" | `fd${number}` * * **Example** (Piping stderr between commands) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * // Pipe stderr instead of stdout * const pipeline = ChildProcess.make`my-program`.pipe( * ChildProcess.pipeTo(ChildProcess.make`grep error`, { from: "stderr" }) * ) + * const result = [pipeline._tag, pipeline.options.from] // => ["PipedCommand", "stderr"] * ``` * * @category options @@ -383,6 +384,11 @@ export interface CommandOptions extends KillOptions { * If `extendEnv` is set to `true`, the value of `env` will be merged with * the value of `globalThis.process.env`, prioritizing the values in `env` * when conflicts exist. + * + * **Gotchas** + * + * Without `extendEnv: true`, providing `env` replaces the inherited child + * environment. The child will not receive `PATH` unless `env` includes it. */ readonly env?: Record | undefined /** @@ -392,7 +398,9 @@ export interface CommandOptions extends KillOptions { * * **Details** * - * If set to `false`, only the value of `env` is used. + * If set to `false` and `env` is provided, only the value of `env` is used. + * + * @default false */ readonly extendEnv?: boolean | undefined /** @@ -424,6 +432,14 @@ export interface CommandOptions extends KillOptions { * Defaults to `true` on non-Windows platforms and `false` on Windows platforms. */ readonly detached?: boolean | undefined + /** + * If set to `true`, prevents the child process's console or GUI window from + * becoming visible on Windows. + * + * Defaults to `true` unless `detached` is set to `true`. This option has no + * effect on non-Windows platforms. + */ + readonly windowsHide?: boolean | undefined /** * Configuration options for the standard input stream for the child process. */ @@ -450,7 +466,7 @@ export interface CommandOptions extends KillOptions { * * **Example** (Configuring additional file descriptors) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * // Output fd3 - read data from child @@ -466,6 +482,8 @@ export interface CommandOptions extends KillOptions { * fd3: { type: "input" } * } * }) + * const result = [cmd1.options.additionalFds?.fd3?.type, cmd2.options.additionalFds?.fd3?.type] + * result // => ["output", "input"] * ``` */ readonly additionalFds?: Record<`fd${number}`, AdditionalFdConfig> | undefined @@ -564,7 +582,7 @@ const makePipedCommand = ( * * **Example** (Creating commands) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * // Template literal form @@ -575,6 +593,8 @@ const makePipedCommand = ( * * // Array form * const cmd3 = ChildProcess.make("git", ["status"]) + * + * const result = [cmd1.command, cmd2.options.cwd, cmd3.args[0]] // => ["echo", "/tmp", "status"] * ``` * * @category constructors @@ -645,7 +665,7 @@ export const make: { * * **Example** (Piping command output) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * // Pipe stdout (default) @@ -662,6 +682,9 @@ export const make: { * const pipeline3 = ChildProcess.make`my-program`.pipe( * ChildProcess.pipeTo(ChildProcess.make`tee output.log`, { from: "all" }) * ) + * + * const result = [pipeline1._tag, pipeline2.options.from, pipeline3.options.from] + * result // => ["PipedCommand", "stderr", "all"] * ``` * * @category combinators @@ -684,7 +707,7 @@ export const pipeTo: { * * **Example** (Prefixing commands) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * const command = ChildProcess.make`echo "foo"` @@ -694,6 +717,8 @@ export const pipeTo: { * ) * * // now prefixed will execute `time echo "foo"` + * const result = prefixed._tag === "StandardCommand" ? `${prefixed.command} ${prefixed.args[0]}` : prefixed._tag + * result // => "time echo" * ``` * * @category combinators @@ -752,12 +777,13 @@ const applyPrefix = (self: Command, prefixSpec: PrefixSpec): Command => { * * **Example** (Setting command working directories) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * const cmd = ChildProcess.make`ls -la`.pipe( * ChildProcess.setCwd("/tmp") * ) + * const result = cmd._tag === "StandardCommand" && cmd.options.cwd // => "/tmp" * ``` * * @category combinators @@ -790,12 +816,13 @@ export const setCwd: { * * **Example** (Setting command environment variables) * - * ```ts + * ```ts import.meta.vitest * import { ChildProcess } from "effect/unstable/process" * * const cmd = ChildProcess.make`node script.js`.pipe( * ChildProcess.setEnv({ NODE_ENV: "test" }) * ) + * const result = cmd._tag === "StandardCommand" && cmd.options.env?.NODE_ENV // => "test" * ``` * * @category combinators diff --git a/.context/effect/packages/effect/src/unstable/process/ChildProcessSpawner.ts b/.context/effect/packages/effect/src/unstable/process/ChildProcessSpawner.ts index f4e8e0a82..5c8d0ac26 100644 --- a/.context/effect/packages/effect/src/unstable/process/ChildProcessSpawner.ts +++ b/.context/effect/packages/effect/src/unstable/process/ChildProcessSpawner.ts @@ -166,20 +166,31 @@ export interface ChildProcessHandle { * * **Example** (Temporarily unreferencing a child process) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" - * import { NodeServices } from "@effect/platform-node" - * import { ChildProcess } from "effect/unstable/process" + * import type { ChildProcessSpawner } from "effect/unstable/process" * - * const program = Effect.gen(function*() { - * const handle = yield* ChildProcess.make`./server` - * const reref = yield* handle.unref + * let referenced = true + * + * const unref: ChildProcessSpawner.ChildProcessHandle["unref"] = Effect.sync(() => { + * referenced = false + * const reref: ChildProcessSpawner.Reref = Effect.sync(() => { + * referenced = true + * }) + * return reref + * }) * - * yield* Effect.sleep("1 second") + * const program = Effect.gen(function*() { + * const states = [] as Array + * const reref = yield* unref + * states.push(referenced) * * yield* reref - * return yield* handle.exitCode - * }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)) + * states.push(referenced) + * return states + * }) + * + * Effect.runSync(program) // => [false, true] * ``` */ readonly unref: Effect.Effect @@ -200,13 +211,13 @@ const HandleProto = { * @since 4.0.0 */ export const makeHandle = (params: Omit): ChildProcessHandle => - Object.assign(Object.create(HandleProto), params) + Object.setPrototypeOf({ ...params }, HandleProto) /** * Creates a `ChildProcessSpawner` service from a `spawn` function, deriving * helpers for exit codes and output collection from that implementation. * - * @category models + * @category constructors * @since 4.0.0 */ export const make = (spawn: ChildProcessSpawner["Service"]["spawn"]): ChildProcessSpawner["Service"] => { diff --git a/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts b/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts index ec28b9d1d..d2ed49385 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/AsyncResult.ts @@ -17,6 +17,7 @@ import * as Exit from "../../Exit.ts" import type { LazyArg } from "../../Function.ts" import { constTrue, dual, identity } from "../../Function.ts" import * as Hash from "../../Hash.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Option from "../../Option.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import type { Predicate, Refinement } from "../../Predicate.ts" @@ -141,7 +142,7 @@ const ResultProto = { /** * Returns whether an `AsyncResult` is currently waiting for an asynchronous computation or refresh to finish. * - * @category refinements + * @category predicates * @since 4.0.0 */ export const isWaiting = (result: AsyncResult): boolean => result.waiting @@ -193,7 +194,7 @@ export const waitingFrom = (previous: Option.Option>): A /** * Returns `true` when an `AsyncResult` is in the `Initial` state. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isInitial = (result: AsyncResult): result is Initial => result._tag === "Initial" @@ -201,7 +202,7 @@ export const isInitial = (result: AsyncResult): result is Initial(result: AsyncResult): result is Success | Failure => @@ -235,7 +236,7 @@ export interface Success extends AsyncResult.Proto { /** * Returns `true` when an `AsyncResult` is a `Success`. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isSuccess = (result: AsyncResult): result is Success => result._tag === "Success" @@ -273,7 +274,7 @@ export interface Failure extends AsyncResult.Proto { /** * Returns `true` when an `AsyncResult` is a `Failure`. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isFailure = (result: AsyncResult): result is Failure => result._tag === "Failure" @@ -281,7 +282,7 @@ export const isFailure = (result: AsyncResult): result is Failure(result: AsyncResult): result is Failure => @@ -694,12 +695,12 @@ export const all = | Record>( for (let i = 0; i < entries.length; i++) { const [key, result] = entries[i] if (!isAsyncResult(result)) { - successes[key] = result + InternalRecord.assignProperty(successes, key, result) continue } else if (!isSuccess(result)) { return result as any } - successes[key] = result.value + InternalRecord.assignProperty(successes, key, result.value) if (result.waiting) { waiting = true } @@ -724,7 +725,7 @@ export const builder = >(self: A): Builder< /** * Type marker used by `Builder` to track whether defect failures still need to be handled. * - * @category models + * @category utility types * @since 4.0.0 */ export interface Defect { @@ -734,7 +735,7 @@ export interface Defect { /** * Type marker used by `Builder` to track whether interrupt failures still need to be handled. * - * @category models + * @category utility types * @since 4.0.0 */ export interface Interrupt { @@ -960,7 +961,7 @@ export const Schema = < [success_, Schema_.Cause(error, Schema_.Defect())], ([value, cause]) => (input, ast, options) => { if (!isAsyncResult(input)) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } switch (input._tag) { case "Initial": @@ -970,8 +971,7 @@ export const Schema = < SchemaParser.decodeUnknownEffect(value)(input.value, options), { onSuccess: (value) => success(value, input), - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option.some(input), [new SchemaIssue.Pointer(["value"], issue)]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "value", issue, input, options) } ) case "Failure": { @@ -982,9 +982,14 @@ export const Schema = < { onSuccess: (value) => Option.some(success(value, ps)), onFailure: (issue) => - new SchemaIssue.Composite(ast, Option.some(input), [ - new SchemaIssue.Pointer(["previousSuccess", "value"], issue) - ]) + new SchemaIssue.Composite( + ast, + [ + new SchemaIssue.Pointer(["previousSuccess", "value"], issue) + ], + input, + options + ) } ) ), @@ -992,7 +997,7 @@ export const Schema = < ) const causeEffect = Effect.mapErrorEager( SchemaParser.decodeUnknownEffect(cause)(input.cause, options), - (issue) => new SchemaIssue.Composite(ast, Option.some(input), [new SchemaIssue.Pointer(["cause"], issue)]) + (issue) => SchemaIssue.makeCompositeAtKey(ast, "cause", issue, input, options) ) return Effect.flatMapEager( prevSuccessEffect, @@ -1009,7 +1014,7 @@ export const Schema = < { expected: "AsyncResult", toCodec([value, cause]) { - const Success = Schema_.TaggedStruct("Success", { value, waiting: Schema_.Boolean, timestamp: Schema_.Number }) + const Success = Schema_.TaggedStruct("Success", { value, waiting: Schema_.Boolean, timestamp: Schema_.Int }) return Schema_.link>()( Schema_.Union([ Schema_.TaggedStruct("Initial", { waiting: Schema_.Boolean }), diff --git a/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts b/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts index b9b95ee2d..d01751a7c 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/Atom.ts @@ -68,6 +68,7 @@ export interface Atom extends Pipeable, Inspectable.Inspectable { readonly keepAlive: boolean readonly lazy: boolean readonly read: (get: AtomContext) => A + equals(value: A, next: A): boolean readonly refresh?: (f: (atom: Atom) => void) => void readonly label?: readonly [name: string, stack: string] readonly idleTTL?: number @@ -229,6 +230,7 @@ const removeTtl = setIdleTTL(0) const AtomProto = { [TypeId]: TypeId, + equals: Object.is, ...PipeInspectableProto, toJSON(this: Atom) { return { @@ -353,7 +355,7 @@ const WritableProto = { /** * Returns `true` when an atom is writable. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isWritable = (atom: Atom): atom is Writable => WritableTypeId in atom @@ -692,7 +694,7 @@ export interface AtomRuntime extends Atom | ((get: AtomContext) => Layer.Layer) ): AtomRuntime - readonly memoMap: Layer.MemoMap readonly addGlobalLayer: (layer: Layer.Layer) => void /** @@ -716,14 +717,40 @@ export interface RuntimeFactory { } /** - * Creates a `RuntimeFactory` backed by the supplied `Layer.MemoMap`. + * A `RuntimeFactory` backed by an atom whose memo map is scoped to each registry. * - * @category constructors + * @category models + * @since 4.0.0 + */ +export interface RegistryRuntimeFactory extends RuntimeFactory { + readonly memoMap: Atom +} + +/** + * A `RuntimeFactory` backed by a concrete memo map shared across registries. + * + * @category models * @since 4.0.0 */ -export const context: (options: { +export interface SharedRuntimeFactory extends RuntimeFactory { readonly memoMap: Layer.MemoMap -}) => RuntimeFactory = (options) => { +} + +/** + * Creates a `RuntimeFactory` backed by a registry-scoped memo map by default, + * or by the supplied atom or concrete `Layer.MemoMap`. + * + * @category constructors + * @since 4.0.0 + */ +export function context(): RegistryRuntimeFactory +export function context(options: { readonly memoMap: Atom }): RegistryRuntimeFactory +export function context(options: { readonly memoMap: Layer.MemoMap }): SharedRuntimeFactory +export function context(options?: { + readonly memoMap: Atom | Layer.MemoMap +}): RegistryRuntimeFactory | SharedRuntimeFactory { + const memoMap = options?.memoMap ?? removeTtl(make(() => Layer.makeMemoMapUnsafe())) + const resolveMemoMap = (get: AtomContext): Layer.MemoMap => isAtom(memoMap) ? get(memoMap) : memoMap let globalLayer: Layer.Layer = Reactivity.layer function factory( create: @@ -745,23 +772,25 @@ export const context: (options: { self.read = function read(get: AtomContext) { const layer = get(layerAtom) - const build = Effect.flatMap(Effect.scope, (scope) => Layer.buildWithMemoMap(layer, options.memoMap, scope)) + const build = Effect.flatMap(Effect.scope, (scope) => Layer.buildWithMemoMap(layer, resolveMemoMap(get), scope)) return effect(get, build, { uninterruptible: true }) } return self } - factory.memoMap = options.memoMap + factory.memoMap = memoMap factory.addGlobalLayer = (layer: Layer.Layer) => { globalLayer = Layer.provideMerge(globalLayer, Layer.provide(layer, Reactivity.layer)) } - const reactivityAtom = removeTtl(make( - Effect.contextWith((services: Context.Context) => - Layer.buildWithMemoMap(Reactivity.layer, options.memoMap, Context.get(services, Scope.Scope)) - ).pipe( - Effect.map(Context.get(Reactivity.Reactivity)) + const reactivityAtom = removeTtl( + make((get) => + Effect.contextWith((services: Context.Context) => + Layer.buildWithMemoMap(Reactivity.layer, resolveMemoMap(get), Context.get(services, Scope.Scope)) + ).pipe( + Effect.map(Context.get(Reactivity.Reactivity)) + ) ) - )) + ) factory.withReactivity = (keys: ReadonlyArray | ReadonlyRecord>) => >(atom: A): A => @@ -773,24 +802,16 @@ export const context: (options: { get.subscribe(atom, (value) => get.setSelf(value)) return get.once(atom) }, { initialValueTarget: atom }) as any as A - return factory + return factory as any } /** - * Default `Layer.MemoMap` used by the module-level `runtime` factory. - * - * @category context - * @since 4.0.0 - */ -export const defaultMemoMap: Layer.MemoMap = Layer.makeMemoMapUnsafe() - -/** - * Default `RuntimeFactory` created with `defaultMemoMap`. + * Default registry-scoped `RuntimeFactory`. * * @category context * @since 4.0.0 */ -export const runtime: RuntimeFactory = context({ memoMap: defaultMemoMap }) +export const runtime: RegistryRuntimeFactory = context() /** * Returns `Rx.runtime.withReactivity` for refreshing an atom whenever the @@ -1505,6 +1526,45 @@ export const setLazy: { lazy })) +/** + * Returns a copy of an atom that uses a custom equality function to detect + * value changes. + * + * **Details** + * + * When an atom's value is rebuilt or written, the registry compares the new + * value against the current one to decide whether dependents and listeners + * should be notified. By default the comparison uses `Object.is`, so a + * structurally equal but referentially distinct value still triggers + * notifications. Providing an equality function lets the atom skip updates + * when the new value is equal to the current one. + * + * **Example** (Comparing values structurally) + * + * ```ts import.meta.vitest + * import { Atom } from "effect/unstable/reactivity" + * + * const point = Atom.make({ x: 0, y: 0 }).pipe( + * Atom.withEquality<{ x: number; y: number }>((a, b) => a.x === b.x && a.y === b.y) + * ) + * point.equals({ x: 1, y: 2 }, { x: 1, y: 2 }) // => true + * ``` + * + * @category combinators + * @since 4.0.0 + */ +export const withEquality: { + (equals: (value: A, next: A) => boolean): >(self: T) => T + >(self: T, equals: (value: Type, next: Type) => boolean): T +} = dual( + 2, + >(self: T, equals: (value: Type, next: Type) => boolean): T => + Object.assign(Object.create(Object.getPrototypeOf(self)), { + ...self, + equals + }) +) + /** * Attaches a diagnostic label to an atom. * @@ -1849,7 +1909,7 @@ const shouldRevalidateSWR = (result: AsyncResult.AsyncResult, staleT * transitions finish, the source atom is refreshed, and failures roll the value * back to the latest source value. * - * @category Optimistic + * @category constructors * @since 4.0.0 */ export const optimistic = (self: Atom): Writable>> => { @@ -1952,7 +2012,7 @@ export const optimistic = (self: Atom): Writable void) => void = Registry.batch * It listens for `visibilitychange` events on `window` and removes the listener * when the atom is disposed. * - * @category Focus + * @category constants * @since 4.0.0 */ export const windowFocusSignal: Atom = readable((get) => { @@ -2062,7 +2122,7 @@ export const windowFocusSignal: Atom = readable((get) => { * The derived atom also subscribes to the source atom so normal source updates are * forwarded to its own value. * - * @category Focus + * @category constructors * @since 4.0.0 */ export const makeRefreshOnSignal = <_>(signal: Atom<_>) => >(self: A): WithoutSerializable => @@ -2081,7 +2141,7 @@ export const makeRefreshOnSignal = <_>(signal: Atom<_>) => > * This helper is browser-only because `windowFocusSignal` depends on `window` and * `document.visibilityState`. * - * @category Focus + * @category combinators * @since 4.0.0 */ export const refreshOnWindowFocus: >(self: A) => WithoutSerializable = makeRefreshOnSignal( @@ -2101,7 +2161,7 @@ export const refreshOnWindowFocus: >(self: A) => WithoutSeri * exposes the decoded value and writes the default value when the key is missing; * in async mode it exposes an `AsyncResult` of the decoded value. * - * @category KeyValueStore + * @category constructors * @since 4.0.0 */ export const kvs = , const Mode extends "sync" | "async" = never>(options: { @@ -2166,7 +2226,7 @@ export const kvs = , const Mode exten * * If you pass a schema, it has to be synchronous and have no context. * - * @category search params + * @category constructors * @since 4.0.0 */ export const searchParam = = never>( @@ -2409,7 +2469,7 @@ export type SerializableTypeId = "~effect-atom/atom/Atom/Serializable" * The key identifies the atom in dehydrated state, and the encode/decode * functions convert between the atom value and the schema encoded value. * - * @category Serializable + * @category models * @since 4.0.0 */ export interface Serializable { @@ -2423,7 +2483,7 @@ export interface Serializable { /** * Returns `true` when an atom carries `Serializable` metadata. * - * @category Serializable + * @category guards * @since 4.0.0 */ export const isSerializable = (self: Atom): self is Atom & Serializable => SerializableTypeId in self @@ -2475,7 +2535,7 @@ export const ServerValueTypeId = "~effect-atom/atom/Atom/ServerValue" as const /** * Sets the value of an Atom when read on the server. * - * @category ServerValue + * @category transforming * @since 4.0.0 */ export const withServerValue: { @@ -2494,7 +2554,7 @@ export const withServerValue: { * Sets an `AsyncResult` atom's server-side value to * `AsyncResult.initial(true)`. * - * @category ServerValue + * @category transforming * @since 4.0.0 */ export const withServerValueInitial = >>(self: A): A => @@ -2508,7 +2568,7 @@ export const withServerValueInitial = @@ -266,6 +266,9 @@ export const Service = HttpClientError.HttpClientError | SchemaError >) })) + if (opts.reactivityKeys) { + atom = self.runtime.factory.withReactivity(opts.reactivityKeys)(atom) + } if (opts.responseMode === "decoded-only" && opts.serializationKey) { const endpoint = groups[opts.group].endpoints[opts.endpoint] atom = Atom.serializable(atom, { @@ -281,9 +284,7 @@ export const Service = ? Atom.setIdleTTL(atom, opts.timeToLive) : Atom.keepAlive(atom) } - return opts.reactivityKeys - ? self.runtime.factory.withReactivity(opts.reactivityKeys)(atom) - : atom + return atom }) self.query = (( diff --git a/.context/effect/packages/effect/src/unstable/reactivity/AtomRegistry.ts b/.context/effect/packages/effect/src/unstable/reactivity/AtomRegistry.ts index 0747de7bd..4c54893f2 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/AtomRegistry.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/AtomRegistry.ts @@ -435,7 +435,15 @@ class RegistryImpl implements AtomRegistry { const encoded = this.preloadedSerializable.get(key) this.preloadedSerializable.delete(key) const decoded = (atom as any as Atom.Serializable)[SerializableTypeId].decode(encoded) - node.setValue(decoded) + let target = atom + while (target.initialValueTarget) { + target = target.initialValueTarget + } + if (target === atom) { + node.setValue(decoded) + } else { + this.ensureNode(target).setInitialValue(decoded) + } } return node } @@ -598,6 +606,8 @@ class NodeImpl { children = new Set>() listeners = new Set<() => void>() skipInvalidation = false + building = false + invalidatedDuringBuild = false currentState() { switch (this.state) { @@ -620,7 +630,9 @@ class NodeImpl { value(): A { if ((this.state & NodeFlags.waitingForValue) !== 0) { this.lifetime = makeLifetime(this) + this.building = true const value = this.atom.read(this.lifetime) + this.building = false if ((this.state & NodeFlags.waitingForValue) !== 0) { if (this.preserveInitialValueOnBuild) { this.preserveInitialValueOnBuild = false @@ -685,7 +697,7 @@ class NodeImpl { } this.state = NodeState.valid - if (Object.is(this._value, value)) { + if (this.atom.equals(this._value, value)) { return } @@ -727,6 +739,9 @@ class NodeImpl { } invalidate(): void { + if (this.building && batchState.phase === BatchPhase.collect) { + this.invalidatedDuringBuild = true + } if (this.state === NodeState.valid) { this.state = NodeState.stale this.disposeLifetime() @@ -852,8 +867,9 @@ const LifetimeProto: Omit, "node" | "finalizers" | "disposed" | "i return this.node.registry.get(atom) } const parent = this.node.registry.ensureNode(atom) + const value = parent.value() this.node.addParent(parent) - return parent.value() + return value }, result(this: Lifetime, atom: Atom.Atom>, options?: { @@ -1102,7 +1118,12 @@ export function batch(f: () => void): void { function batchRebuildNode(node: NodeImpl) { if (node.state === NodeState.valid) { - return + if (!node.invalidatedDuringBuild) { + return + } + node.invalidatedDuringBuild = false + node.state = NodeState.stale + node.disposeLifetime() } for (const parent of node.parents) { diff --git a/.context/effect/packages/effect/src/unstable/reactivity/AtomRpc.ts b/.context/effect/packages/effect/src/unstable/reactivity/AtomRpc.ts index c961729c0..67b084824 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/AtomRpc.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/AtomRpc.ts @@ -37,7 +37,7 @@ import * as Reactivity from "./Reactivity.ts" * It exposes the RPC client, an atom runtime, mutation helpers that return `AtomResultFn`s, and query helpers that * return atoms or pull atoms for RPC results. * - * @category models + * @category services * @since 4.0.0 */ export interface AtomRpcClient extends @@ -234,6 +234,9 @@ export const Service = () => : self.runtime.atom( self.use((client) => client(tag, payload, { headers } as any)) as any ) + if (reactivityKeys) { + atom = self.runtime.factory.withReactivity(reactivityKeys)(atom) + } if (!isStream && key.serializationKey) { atom = Atom.serializable(atom, { key: `AtomRpc:${key.tag}:${key.serializationKey}`, @@ -248,9 +251,7 @@ export const Service = () => ? Atom.setIdleTTL(atom, timeToLive) : Atom.keepAlive(atom) } - return reactivityKeys - ? self.runtime.factory.withReactivity(reactivityKeys)(atom) - : atom + return atom } ) diff --git a/.context/effect/packages/effect/src/unstable/reactivity/Hydration.ts b/.context/effect/packages/effect/src/unstable/reactivity/Hydration.ts index 8de832cbe..182e9959e 100644 --- a/.context/effect/packages/effect/src/unstable/reactivity/Hydration.ts +++ b/.context/effect/packages/effect/src/unstable/reactivity/Hydration.ts @@ -103,7 +103,7 @@ export const dehydrate = ( /** * Returns dehydrated state entries as `DehydratedAtomValue` records. * - * @category dehydration + * @category converting * @since 4.0.0 */ export const toValues = (state: ReadonlyArray): Array => state as any diff --git a/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts b/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts index a5ce20a04..b23fd841a 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/Rpc.ts @@ -47,7 +47,7 @@ export const isRpc = (u: unknown): u is Rpc => Predicate.hasPrope * Defect schemas decode and encode without services and can be constructed from * `null`, `undefined`, or an object value. * - * @category models + * @category schemas * @since 4.0.0 */ export interface DefectSchema extends Schema.Top { @@ -260,7 +260,7 @@ export interface AnyWithProps extends Pipeable { /** * Extracts the tag string from an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Tag = R extends Rpc< @@ -276,7 +276,7 @@ export type Tag = R extends Rpc< /** * Extracts the success schema from an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type SuccessSchema = R extends Rpc< @@ -292,7 +292,7 @@ export type SuccessSchema = R extends Rpc< /** * Extracts the decoded success value type from an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Success = SuccessSchema["Type"] @@ -300,7 +300,7 @@ export type Success = SuccessSchema["Type"] /** * Extracts the encoded success value type from an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type SuccessEncoded = R extends Rpc< @@ -321,7 +321,7 @@ export type SuccessEncoded = R extends Rpc< * For streaming RPCs, this is the stream element schema; otherwise it is the * RPC success schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type SuccessExitSchema = SuccessSchema extends RpcSchema.Stream ? _A : SuccessSchema @@ -334,7 +334,7 @@ export type SuccessExitSchema = SuccessSchema extends RpcSchema.Stream = Success extends infer T ? T extends Stream ? void : T @@ -344,7 +344,7 @@ export type SuccessExit = Success extends infer T ? T extends Stream = Success extends Stream ? _A : never @@ -353,7 +353,7 @@ export type SuccessChunk = Success extends Stream = R extends Rpc< @@ -370,7 +370,7 @@ export type ErrorSchema = R extends Rpc< * Extracts the decoded error value type from an `Rpc`, including middleware * errors. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = Schema.Schema.Type> @@ -383,7 +383,7 @@ export type Error = Schema.Schema.Type> * For streaming RPCs, this includes both the stream error schema and the RPC * error schema; otherwise it is the RPC error schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorExitSchema = SuccessSchema extends RpcSchema.Stream ? _E | ErrorSchema @@ -396,7 +396,7 @@ export type ErrorExitSchema = SuccessSchema extends RpcSchema.Stream = Success extends Stream ? _E | Error : Error @@ -405,7 +405,7 @@ export type ErrorExit = Success extends Stream = Exit_, ErrorExit> @@ -414,7 +414,7 @@ export type Exit = Exit_, ErrorExit> * Extracts the payload constructor input type accepted by the RPC payload * schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type PayloadConstructor = R extends Rpc< @@ -430,7 +430,7 @@ export type PayloadConstructor = R extends Rpc< /** * Extracts the decoded payload type from an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Payload = R extends Rpc< @@ -447,7 +447,7 @@ export type Payload = R extends Rpc< * Extracts all schema services required to encode or decode an RPC's payload, * success, error, and middleware error schemas. * - * @category models + * @category utility types * @since 4.0.0 */ export type Services = R extends Rpc< @@ -476,7 +476,7 @@ export type Services = R extends Rpc< * This includes payload encoding services and success, error, and middleware * error decoding services. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesClient = R extends Rpc< @@ -501,7 +501,7 @@ export type ServicesClient = R extends Rpc< * This includes payload decoding services and success, error, and middleware * error encoding services. * - * @category models + * @category utility types * @since 4.0.0 */ export type ServicesServer = R extends Rpc< @@ -521,7 +521,7 @@ export type ServicesServer = R extends Rpc< /** * Extracts the service identifiers for middleware attached to an `Rpc`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Middleware = R extends Rpc< @@ -538,7 +538,7 @@ export type Middleware = R extends Rpc< * Extracts client-side middleware service requirements for middleware marked as * required on the client. * - * @category models + * @category utility types * @since 4.0.0 */ export type MiddlewareClient = R extends Rpc< @@ -556,7 +556,7 @@ export type MiddlewareClient = R extends Rpc< * Returns an RPC type with an additional error schema unioned into its error * channel. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddError = R extends Rpc< @@ -580,7 +580,7 @@ export type AddError = R extends Rpc< * Returns an RPC type with additional middleware and the corresponding * middleware service requirements applied. * - * @category models + * @category utility types * @since 4.0.0 */ export type AddMiddleware = R extends Rpc< @@ -603,7 +603,7 @@ export type AddMiddleware = R extends Rpc< @@ -624,7 +624,7 @@ export type ToHandler = R extends Rpc< * The function receives the decoded payload and request metadata, and returns * the RPC result shape, optionally wrapped with `Wrapper` options. * - * @category models + * @category utility types * @since 4.0.0 */ export type ToHandlerFn = ( @@ -641,7 +641,7 @@ export type ToHandlerFn = ( * Returns `true` when the RPC with the specified tag has a streaming success * schema, or `never` otherwise. * - * @category models + * @category utility types * @since 4.0.0 */ export type IsStream = R extends Rpc< @@ -657,7 +657,7 @@ export type IsStream = R extends Rpc< /** * Extracts the RPC with the specified tag from an RPC union. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExtractTag = R extends Rpc< @@ -674,7 +674,7 @@ export type ExtractTag = R extends Rpc< * Extracts the services provided by middleware on the RPC with the specified * tag. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExtractProvides = R extends Rpc< @@ -690,7 +690,7 @@ export type ExtractProvides = R extends Rpc< /** * Extracts the service requirements of the RPC with the specified tag. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExtractRequires = R extends Rpc< @@ -707,7 +707,7 @@ export type ExtractRequires = R extends Rpc< * Removes the services provided by middleware for the specified RPC tag from an * environment type. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExcludeProvides = Exclude< @@ -724,7 +724,7 @@ export type ExcludeProvides = Exclude< * RPCs return an effect that succeeds with the success value or a deferred * success value. * - * @category models + * @category utility types * @since 4.0.0 */ export type ResultFrom = R extends Rpc< @@ -756,7 +756,7 @@ export type ResultFrom = R extends Rpc< * Returns an RPC type with the specified string prefix added to its tag while * preserving its payload, success, error, middleware, and requirements. * - * @category models + * @category utility types * @since 4.0.0 */ export type Prefixed = Rpcs extends Rpc< @@ -956,7 +956,7 @@ export const make = < * * **Example** (Defining a paginated RPC constructor) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Rpc } from "effect/unstable/rpc" * @@ -992,6 +992,8 @@ export const make = < * export const listAllRpc = makePaginated("listAll", { * success: Schema.String * }) + * + * const result = [listAllRpc._tag, Schema.isSchema(listAllRpc.successSchema)] // => ["listAll", true] * ``` * * @category constructors @@ -1178,7 +1180,7 @@ export type WrapperOr = A | Wrapper /** * Returns `true` when the value is an RPC `Wrapper`. * - * @category wrapping + * @category guards * @since 4.0.0 */ export const isWrapper = (u: object): u is Wrapper => WrapperTypeId in u diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts index 7673a125d..1812e8f55 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcClient.ts @@ -18,10 +18,12 @@ import * as Effect from "../../Effect.ts" import * as Exit from "../../Exit.ts" import * as Fiber from "../../Fiber.ts" import { constVoid, dual, flow, identity } from "../../Function.ts" +import * as InternalRecord from "../../internal/record.ts" import * as Latch from "../../Latch.ts" import * as Layer from "../../Layer.ts" import * as Option from "../../Option.ts" import * as Pool from "../../Pool.ts" +import * as Predicate from "../../Predicate.ts" import * as Queue from "../../Queue.ts" import * as Result from "../../Result.ts" import * as Schedule from "../../Schedule.ts" @@ -50,11 +52,13 @@ import * as RpcSerialization from "./RpcSerialization.ts" import * as RpcWorker from "./RpcWorker.ts" import { withRunClient } from "./Utils.ts" +const isRpcClientError = (u: unknown): u is RpcClientError => Predicate.isTagged(u, "RpcClientError") + /** * The object-shaped client generated from a union of RPC definitions, with one * method per RPC tag. * - * @category client + * @category utility types * @since 4.0.0 */ export type RpcClient = Struct.Simplify> @@ -71,7 +75,7 @@ export declare namespace RpcClient { * method that accepts the RPC payload and returns either an `Effect` or * `Stream` based on the RPC success schema. * - * @category client + * @category utility types * @since 4.0.0 */ export type From = { @@ -136,7 +140,7 @@ export declare namespace RpcClient { * Builds a flattened RPC client function that accepts an RPC tag and payload, * returning the corresponding `Effect` or `Stream` for that RPC. * - * @category client + * @category utility types * @since 4.0.0 */ export type Flat = < @@ -199,7 +203,7 @@ export declare namespace RpcClient { * Derives the object-shaped RPC client type for all RPCs contained in an * `RpcGroup`. * - * @category client + * @category utility types * @since 4.0.0 */ export type FromGroup = RpcClient, E> @@ -211,7 +215,7 @@ let requestIdCounter = 0 * client API together with a `write` function for delivering server messages * back to the client. * - * @category client + * @category constructors * @since 4.0.0 */ export const makeNoSerialization: ( @@ -608,7 +612,7 @@ export const makeNoSerialization: { - client[rpc._tag] = onRequest(rpc as any) + InternalRecord.assignProperty(client, rpc._tag, onRequest(rpc as any)) }) } @@ -621,7 +625,7 @@ let clientIdCounter = 0 * Creates a schema-aware RPC client for a group using the current client * `Protocol`, encoding requests and decoding server responses. * - * @category client + * @category constructors * @since 4.0.0 */ export const make: ( @@ -806,7 +810,7 @@ const rpcSchemas = (rpc: Rpc.AnyWithProps) => { * Use to set request headers that should be automatically merged into outgoing * RPC client messages. * - * @category headers + * @category services * @since 4.0.0 */ export const CurrentHeaders = Context.Reference("effect/rpc/RpcClient/CurrentHeaders", { @@ -838,7 +842,7 @@ export const withHeaders: { * Use to provide the transport boundary for RPC clients over HTTP, WebSocket, * workers, sockets, or custom protocols. * - * @category protocols + * @category services * @since 4.0.0 */ export class Protocol extends Context.Service cause instanceof RpcClientError ? cause : httpClientError(cause)) + Effect.mapError((cause) => isRpcClientError(cause) ? cause : httpClientError(cause)) ) if (!hasResponse) { return yield* emptyResponseError(request) @@ -979,7 +983,7 @@ export const makeProtocolHttp = (client: HttpClient.HttpClient): Effect.Effect< * Provides a client `Protocol` backed by `HttpClient`, targeting the configured * URL and optionally transforming the client before use. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolHttp = (options: { @@ -1007,6 +1011,13 @@ export const layerProtocolHttp = (options: { export const makeProtocolSocket = (options?: { readonly retryTransientErrors?: boolean | undefined readonly retryPolicy?: Schedule.Schedule | undefined + /** + * Runs for each retried `SocketOpenError` when `retryTransientErrors` is enabled. + * Ping timeouts are also reported because the protocol classifies them as + * `SocketOpenError`. The returned `Effect` cannot fail with a typed error + * or require services; defects are logged and ignored so retries can continue. + */ + readonly onTransientError?: ((error: RpcClientError) => Effect.Effect) | undefined }): Effect.Effect< Protocol["Service"], never, @@ -1031,6 +1042,13 @@ export const makeProtocolSocket = (options?: { const broadcast = (response: FromServerEncoded) => Effect.forEach(clientIds, (clientId) => writeResponse(clientId, response)) + const broadcastError = (error: RpcClientError) => { + currentError = error + return broadcast({ + _tag: "ClientProtocolError", + error + }) + } yield* Effect.suspend(() => { parser = serialization.makeUnsafe() @@ -1048,11 +1066,12 @@ export const makeProtocolSocket = (options?: { pinger.onPong() return Effect.void } - if ("requestId" in response) { - const clientId = requestClientMap.get(response.requestId) + if (Object.hasOwn(response, "requestId")) { + const requestId = (response as FromServerEncoded & { readonly requestId: string | number }).requestId + const clientId = requestClientMap.get(requestId) if (clientId !== undefined) { if (response._tag === "Exit") { - requestClientMap.delete(response.requestId) + requestClientMap.delete(requestId) } return writeResponse(clientId, response) } @@ -1094,24 +1113,29 @@ export const makeProtocolSocket = (options?: { Effect.tapCause((cause) => { const error = Cause.findError(cause) const hasError = Result.isSuccess(error) - if ( - options?.retryTransientErrors && hasError && - error.success.reason._tag === "SocketOpenError" - ) { - return Effect.void - } - currentError = new RpcClientError({ + const rpcError = new RpcClientError({ reason: hasError ? error.success.reason : new RpcClientDefect({ message: "Unknown socket error", cause: Cause.squash(cause) }) }) - return broadcast({ - _tag: "ClientProtocolError", - error: currentError - }) + if ( + options?.retryTransientErrors && hasError && + error.success.reason._tag === "SocketOpenError" + ) { + return (options.onTransientError?.(rpcError) ?? Effect.void).pipe( + Effect.ignoreCause({ + log: true, + message: "RpcClient onTransientError hook failed" + }) + ) + } + return broadcastError(rpcError) }), - Effect.retry(options?.retryPolicy ?? defaultRetryPolicy), + Effect.retryOrElse( + options?.retryPolicy ?? defaultRetryPolicy, + (error) => broadcastError(new RpcClientError({ reason: error.reason })) + ), Effect.annotateLogs({ module: "RpcClient", method: "makeProtocolSocket" @@ -1169,11 +1193,18 @@ const makePinger = Effect.fnUntraced(function*(writePing: Effect.Effect * Provides a client `Protocol` backed by the current `Socket` and * `RpcSerialization` services. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolSocket = (options?: { readonly retryTransientErrors?: boolean | undefined + /** + * Runs for each retried `SocketOpenError` when `retryTransientErrors` is enabled. + * Ping timeouts are also reported because the protocol classifies them as + * `SocketOpenError`. The returned `Effect` cannot fail with a typed error + * or require services; defects are logged and ignored so retries can continue. + */ + readonly onTransientError?: ((error: RpcClientError) => Effect.Effect) | undefined }): Layer.Layer< Protocol, never, @@ -1254,6 +1285,11 @@ export const makeProtocolWorker = ( undefined }).pipe( Effect.tapCause((cause) => { + for (const [requestId, entry] of entries) { + if (entry.worker !== backing) continue + entries.delete(requestId) + entry.latch.openUnsafe() + } const error = Cause.findError(cause) return broadcast({ _tag: "ClientProtocolError", @@ -1350,7 +1386,7 @@ export const makeProtocolWorker = ( * Provides a client `Protocol` backed by a worker pool using the current worker * platform and spawner services. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolWorker: ( @@ -1380,7 +1416,7 @@ export const layerProtocolWorker: ( * Use to run setup or cleanup effects when an RPC client transport opens or * closes. * - * @category connection hooks + * @category services * @since 4.0.0 */ export class ConnectionHooks extends Context.Service("effect/rpc/RpcClientError/RpcClientDefect")({ +export class RpcClientDefect extends Schema.Error("effect/rpc/RpcClientError/RpcClientDefect")({ _tag: Schema.tag("RpcClientDefect"), message: Schema.String, cause: Schema.Defect() @@ -35,7 +35,7 @@ export class RpcClientDefect extends Schema.ErrorClass("effect/ * @category errors * @since 4.0.0 */ -export class RpcClientError extends Schema.ErrorClass(TypeId)({ +export class RpcClientError extends Schema.Error(TypeId)({ _tag: Schema.tag("RpcClientError"), reason: Schema.Union([ WorkerErrorReason, diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcGroup.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcGroup.ts index 8998a6343..2b63742a9 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcGroup.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcGroup.ts @@ -29,7 +29,7 @@ const TypeId = "~effect/rpc/RpcGroup" * A collection of RPC definitions that can be composed, annotated, and * converted into server handlers or layers. * - * @category groups + * @category models * @since 4.0.0 */ export interface RpcGroup extends Pipeable { @@ -170,7 +170,7 @@ export interface RpcGroup extends Pipeable { * An erased `RpcGroup` type for APIs that only need to know that a value is an * RPC group. * - * @category groups + * @category utility types * @since 4.0.0 */ export interface Any { @@ -181,7 +181,7 @@ export interface Any { * Builds the object type of server handler functions required to implement each * RPC in a union. * - * @category groups + * @category utility types * @since 4.0.0 */ export type HandlersFrom = { @@ -192,7 +192,7 @@ export type HandlersFrom = { * Extracts the server handler function type for a specific RPC tag from an RPC * union. * - * @category groups + * @category utility types * @since 4.0.0 */ export type HandlerFrom = Extract extends @@ -202,7 +202,7 @@ export type HandlerFrom = Extract< * Computes the services required by all handlers in a handler object for an RPC * union. * - * @category groups + * @category utility types * @since 4.0.0 */ export type HandlersServices = keyof Handlers extends infer K ? @@ -213,7 +213,7 @@ export type HandlersServices = keyof Handlers ex * Computes the services required by a single RPC handler, excluding services * provided by middleware and `Scope` where the server supplies it. * - * @category groups + * @category utility types * @since 4.0.0 */ export type HandlerServices = true extends @@ -242,7 +242,7 @@ export type HandlerServices = Group extends RpcGroup ? string extends R["_tag"] ? never : R : never @@ -396,7 +396,7 @@ const makeProto = (options: { /** * Creates an `RpcGroup` from one or more RPC definitions. * - * @category groups + * @category constructors * @since 4.0.0 */ export const make = >( diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts index 13a31fc8f..dca41316d 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcMessage.ts @@ -20,7 +20,7 @@ import type { RpcClientError } from "./RpcClientError.ts" /** * Decoded messages that can be sent from an RPC client to a server. * - * @category request + * @category models * @since 4.0.0 */ export type FromClient = Request | Ack | Interrupt | Eof @@ -28,7 +28,7 @@ export type FromClient = Request | Ack | Interrupt | Eof /** * Transport-encoded messages that can be sent from an RPC client to a server. * - * @category request + * @category models * @since 4.0.0 */ export type FromClientEncoded = RequestEncoded | AckEncoded | InterruptEncoded | Ping | Eof @@ -37,7 +37,7 @@ export type FromClientEncoded = RequestEncoded | AckEncoded | InterruptEncoded | * A branded request identifier used to correlate RPC requests, responses, * chunks, acknowledgements, and interrupts. * - * @category request + * @category models * @since 4.0.0 */ export type RequestId = Branded @@ -45,7 +45,7 @@ export type RequestId = Branded id as RequestId @@ -54,7 +54,7 @@ export const RequestId = (id: string | number): RequestId => id as RequestId * The transport-encoded RPC request envelope, including the string request id, * RPC tag, encoded payload, headers, and optional trace context. * - * @category request + * @category models * @since 4.0.0 */ export interface RequestEncoded { @@ -63,6 +63,7 @@ export interface RequestEncoded { readonly tag: string readonly payload: unknown readonly headers: ReadonlyArray<[string, string]> + readonly isNotification?: true readonly traceId?: string readonly spanId?: string readonly sampled?: boolean @@ -72,7 +73,7 @@ export interface RequestEncoded { * The decoded RPC request envelope for an RPC union, carrying a branded request * id, typed RPC tag, decoded payload, headers, and optional trace context. * - * @category request + * @category models * @since 4.0.0 */ export interface Request { @@ -89,7 +90,7 @@ export interface Request { /** * A decoded acknowledgement for a streamed RPC response chunk. * - * @category request + * @category models * @since 4.0.0 */ export interface Ack { @@ -101,7 +102,7 @@ export interface Ack { * A decoded request to interrupt an in-flight RPC, carrying the request id and * interrupting fiber ids. * - * @category request + * @category models * @since 4.0.0 */ export interface Interrupt { @@ -113,7 +114,7 @@ export interface Interrupt { /** * The transport-encoded acknowledgement for a streamed RPC response chunk. * - * @category request + * @category models * @since 4.0.0 */ export interface AckEncoded { @@ -124,7 +125,7 @@ export interface AckEncoded { /** * The transport-encoded request to interrupt an in-flight RPC. * - * @category request + * @category models * @since 4.0.0 */ export interface InterruptEncoded { @@ -136,7 +137,7 @@ export interface InterruptEncoded { * A client-to-server message indicating that the client has finished sending * input for the current connection or request batch. * - * @category request + * @category models * @since 4.0.0 */ export interface Eof { @@ -147,7 +148,7 @@ export interface Eof { * A client-to-server keepalive message used by protocols that monitor * connection liveness. * - * @category request + * @category models * @since 4.0.0 */ export interface Ping { @@ -157,7 +158,7 @@ export interface Ping { /** * Represents the reusable `Eof` message value. * - * @category request + * @category constants * @since 4.0.0 */ export const constEof: Eof = { _tag: "Eof" } @@ -165,7 +166,7 @@ export const constEof: Eof = { _tag: "Eof" } /** * Represents the reusable `Ping` message value. * - * @category request + * @category constants * @since 4.0.0 */ export const constPing: Ping = { _tag: "Ping" } @@ -173,7 +174,7 @@ export const constPing: Ping = { _tag: "Ping" } /** * Decoded messages that can be sent from an RPC server to a client. * - * @category response + * @category models * @since 4.0.0 */ export type FromServer = @@ -185,7 +186,7 @@ export type FromServer = /** * Transport-encoded messages that can be sent from an RPC server to a client. * - * @category response + * @category models * @since 4.0.0 */ export type FromServerEncoded = @@ -214,7 +215,7 @@ export type ResponseIdTypeId = typeof ResponseIdTypeId /** * A branded numeric identifier for server responses. * - * @category response + * @category models * @since 4.0.0 */ export type ResponseId = Branded @@ -223,7 +224,7 @@ export type ResponseId = Branded * The transport-encoded response message containing a non-empty batch of stream * chunk values for a request. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseChunkEncoded { @@ -236,7 +237,7 @@ export interface ResponseChunkEncoded { * The decoded response message containing a non-empty batch of stream chunk * values for a specific client and request. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseChunk { @@ -250,7 +251,7 @@ export interface ResponseChunk { * The transport representation of an RPC `Exit`, encoding success values or a * failure cause made of failures, defects, and interrupts. * - * @category response + * @category models * @since 4.0.0 */ export type ExitEncoded = { @@ -276,7 +277,7 @@ export type ExitEncoded = { * The transport-encoded terminal response for a request, carrying the encoded * `Exit`. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseExitEncoded { @@ -289,7 +290,7 @@ export interface ResponseExitEncoded { * A server-to-client protocol message reporting a client protocol error to all * affected in-flight requests. * - * @category response + * @category models * @since 4.0.0 */ export interface ClientProtocolError { @@ -301,7 +302,7 @@ export interface ClientProtocolError { * The decoded terminal response for a request, carrying the typed `Rpc.Exit` * for the RPC. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseExit { @@ -315,7 +316,7 @@ export interface ResponseExit { * The transport-encoded server defect message used for protocol-level defects * that affect the client connection. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseDefectEncoded { @@ -329,7 +330,7 @@ const encodeDefect = Schema.encodeSync(Schema.Defect()) * Creates an encoded terminal response for a request whose exit is a defect * encoded with `Schema.Defect()`. * - * @category response + * @category constructors * @since 4.0.0 */ export const ResponseExitDieEncoded = (options: { @@ -351,7 +352,7 @@ export const ResponseExitDieEncoded = (options: { * Creates a transport-encoded defect response by encoding the input with * `Schema.Defect()`. * - * @category response + * @category constructors * @since 4.0.0 */ export const ResponseDefectEncoded = (input: unknown): ResponseDefectEncoded => ({ @@ -362,7 +363,7 @@ export const ResponseDefectEncoded = (input: unknown): ResponseDefectEncoded => /** * The decoded server defect message for a client connection. * - * @category response + * @category models * @since 4.0.0 */ export interface ResponseDefect { @@ -374,7 +375,7 @@ export interface ResponseDefect { /** * A server message indicating that the client connection has ended. * - * @category response + * @category models * @since 4.0.0 */ export interface ClientEnd { @@ -385,7 +386,7 @@ export interface ClientEnd { /** * A server-to-client keepalive response to a `Ping` message. * - * @category response + * @category models * @since 4.0.0 */ export interface Pong { @@ -395,7 +396,7 @@ export interface Pong { /** * Represents the reusable `Pong` message value. * - * @category response + * @category constants * @since 4.0.0 */ export const constPong: Pong = { _tag: "Pong" } diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts index 56773544b..1b7578038 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcMiddleware.ts @@ -119,7 +119,7 @@ export interface Any { * A type-level carrier for RPC middleware metadata, including provided * services, required services, error schema, and client error type. * - * @category models + * @category utility types * @since 4.0.0 */ export interface AnyId { @@ -135,7 +135,7 @@ export interface AnyId { * The `Context.Service` class shape created for an RPC middleware, including * its error schema, service metadata, and client-side requirement marker. * - * @category models + * @category services * @since 4.0.0 */ export interface ServiceClass< @@ -164,7 +164,7 @@ export interface ServiceClass< /** * Extracts the services provided by an RPC middleware. * - * @category models + * @category utility types * @since 4.0.0 */ export type Provides = A extends { readonly [TypeId]: { readonly provides: infer P } } ? P : never @@ -172,7 +172,7 @@ export type Provides = A extends { readonly [TypeId]: { readonly provides: in /** * Extracts the services required by an RPC middleware. * - * @category models + * @category utility types * @since 4.0.0 */ export type Requires = A extends { readonly [TypeId]: { readonly requires: infer R } } ? R : never @@ -181,7 +181,7 @@ export type Requires = A extends { readonly [TypeId]: { readonly requires: in * Applies a middleware's service transformation to an RPC environment by * removing services the middleware provides and adding services it requires. * - * @category models + * @category utility types * @since 4.0.0 */ export type ApplyServices = Exclude> | Requires @@ -189,7 +189,7 @@ export type ApplyServices = Exclude> | Requires /** * Extracts the error schema associated with an RPC middleware. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorSchema = A extends { readonly [TypeId]: { readonly error: infer E } } @@ -199,7 +199,7 @@ export type ErrorSchema = A extends { readonly [TypeId]: { readonly error: in /** * Extracts the decoded error type produced by an RPC middleware. * - * @category models + * @category utility types * @since 4.0.0 */ export type Error = ErrorSchema["Type"] @@ -207,7 +207,7 @@ export type Error = ErrorSchema["Type"] /** * Extracts the encoding services required by a middleware's error schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesEncode = ErrorSchema["EncodingServices"] @@ -215,7 +215,7 @@ export type ErrorServicesEncode = ErrorSchema["EncodingServices"] /** * Extracts the decoding services required by a middleware's error schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type ErrorServicesDecode = ErrorSchema["DecodingServices"] @@ -318,7 +318,7 @@ export const Service = < * capturing the layer's environment and merging it into each middleware * invocation. * - * @category client + * @category layers * @since 4.0.0 */ export const layerClient = ( diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts index 5b306dacc..1f85f7d42 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcSchema.ts @@ -23,7 +23,7 @@ const StreamSchemaTypeId = "~effect/rpc/RpcSchema/StreamSchema" * Returns `true` when a schema is an RPC stream schema created by * `RpcSchema.Stream`. * - * @category streams + * @category guards * @since 4.0.0 */ export function isStreamSchema(schema: Schema.Constraint): schema is Stream { @@ -47,7 +47,7 @@ export function getStreamSchemas(schema: Schema.Constraint): Option.Option<{ * A schema marker for RPC streaming responses, storing the success element * schema and stream error schema used for encoding and decoding stream chunks. * - * @category streams + * @category models * @since 4.0.0 */ export interface Stream extends @@ -75,7 +75,7 @@ const schema = Schema.declare(Stream_.isStream) * Creates an RPC stream schema from a stream element success schema and stream * error schema. * - * @category streams + * @category constructors * @since 4.0.0 */ export function Stream(success: A, error: E): Stream { @@ -86,7 +86,7 @@ export function Stream * Annotation that marks interruptions that originate from an RPC client * abort. * - * @category Cause annotations + * @category services * @since 4.0.0 */ export class ClientAbort extends Context.Service()("effect/rpc/RpcSchema/ClientAbort") { diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcSerialization.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcSerialization.ts index 49a591b83..f6aa9c728 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcSerialization.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcSerialization.ts @@ -11,6 +11,7 @@ */ import * as Msgpackr from "msgpackr" import * as Context from "../../Context.ts" +import * as Data from "../../Data.ts" import * as Layer from "../../Layer.ts" import * as Predicate from "../../Predicate.ts" import { hasProperty } from "../../Predicate.ts" @@ -26,7 +27,7 @@ import type * as RpcMessage from "./RpcMessage.ts" * Use to provide the serialization boundary shared by RPC clients and servers * for a chosen wire format. * - * @category serialization + * @category services * @since 4.0.0 */ export class RpcSerialization extends Context.Service Uint8Array | string | undefined } +/** + * Error raised when a streaming parser retains more data than its configured + * buffer limit. + * + * @category errors + * @since 4.0.0 + */ +export class MaxBufferSizeExceeded extends Data.TaggedError("MaxBufferSizeExceeded")<{ + readonly maxBufferSize: number +}> { + override get message() { + return `RPC serialization buffer exceeded the maximum size of ${this.maxBufferSize}` + } +} + +/** + * Options shared by streaming RPC serialization formats. + * + * @category serialization + * @since 4.0.0 + */ +export interface StreamOptions { + /** + * Maximum number of bytes or string code units retained for an incomplete frame. + * The default is 16 MiB. Use `"unbounded"` to disable the limit. + */ + readonly maxBufferSize?: number | "unbounded" | undefined +} + +const defaultMaxBufferSize = 16 * 1024 * 1024 + +const isBufferSizeExceeded = ( + bufferSize: number, + maxBufferSize: number | "unbounded" +): maxBufferSize is number => maxBufferSize !== "unbounded" && bufferSize > maxBufferSize + /** * JSON RPC serialization for whole message payloads. It does not include * message framing, so it is intended for transports that frame responses @@ -77,41 +114,62 @@ export const json: RpcSerialization["Service"] = RpcSerialization.of({ * @category serialization * @since 4.0.0 */ -export const ndjson: RpcSerialization["Service"] = RpcSerialization.of({ - contentType: "application/ndjson", - includesFraming: true, - makeUnsafe: () => { - const decoder = new TextDecoder() - let buffer = "" - return ({ - decode: (bytes) => { - buffer += typeof bytes === "string" ? bytes : decoder.decode(bytes) - let position = 0 - let nlIndex = buffer.indexOf("\n", position) - const items: Array = [] - while (nlIndex !== -1) { - const item = JSON.parse(buffer.slice(position, nlIndex)) - items.push(item) - position = nlIndex + 1 - nlIndex = buffer.indexOf("\n", position) - } - buffer = buffer.slice(position) - return items - }, - encode: (response) => { - if (Array.isArray(response)) { - if (response.length === 0) return undefined - let data = "" - for (let i = 0; i < response.length; i++) { - data += JSON.stringify(response[i]) + "\n" +export const makeNdjson = (options?: StreamOptions): RpcSerialization["Service"] => { + const maxBufferSize = options?.maxBufferSize ?? defaultMaxBufferSize + return RpcSerialization.of({ + contentType: "application/ndjson", + includesFraming: true, + makeUnsafe: () => { + const decoder = new TextDecoder() + let buffer = "" + const failMaxBufferSize = (maxBufferSize: number): never => { + buffer = "" + throw new MaxBufferSizeExceeded({ maxBufferSize }) + } + return ({ + decode: (bytes) => { + buffer += typeof bytes === "string" ? bytes : decoder.decode(bytes, { stream: true }) + let position = 0 + let nlIndex = buffer.indexOf("\n", position) + const items: Array = [] + while (nlIndex !== -1) { + if (isBufferSizeExceeded(nlIndex - position, maxBufferSize)) { + failMaxBufferSize(maxBufferSize) + } + const item = JSON.parse(buffer.slice(position, nlIndex)) + items.push(item) + position = nlIndex + 1 + nlIndex = buffer.indexOf("\n", position) + } + buffer = buffer.slice(position) + if (isBufferSizeExceeded(buffer.length, maxBufferSize)) { + failMaxBufferSize(maxBufferSize) + } + return items + }, + encode: (response) => { + if (Array.isArray(response)) { + if (response.length === 0) return undefined + let data = "" + for (let i = 0; i < response.length; i++) { + data += JSON.stringify(response[i]) + "\n" + } + return data } - return data + return JSON.stringify(response) + "\n" } - return JSON.stringify(response) + "\n" - } - }) - } -}) + }) + } + }) +} + +/** + * Default newline-delimited JSON RPC serialization. + * + * @category serialization + * @since 4.0.0 + */ +export const ndjson: RpcSerialization["Service"] = makeNdjson() /** * Creates a JSON-RPC 2.0 serialization for RPC protocol messages without @@ -156,12 +214,13 @@ export const jsonRpc = (options?: { */ export const ndJsonRpc = (options?: { readonly contentType?: string | undefined + readonly maxBufferSize?: number | "unbounded" | undefined }): RpcSerialization["Service"] => RpcSerialization.of({ contentType: options?.contentType ?? "application/json-rpc", includesFraming: true, makeUnsafe: () => { - const parser = ndjson.makeUnsafe() + const parser = makeNdjson({ maxBufferSize: options?.maxBufferSize }).makeUnsafe() const batches = new Map @@ -212,12 +271,13 @@ function decodeJsonRpcRaw( } function decodeJsonRpcMessage(decoded: JsonRpcMessage): RpcMessage.FromClientEncoded | RpcMessage.FromServerEncoded { - if ("method" in decoded) { - if (Predicate.isNullish(decoded.id) && decoded.method.startsWith("@effect/rpc/")) { - const tag = decoded.method.slice("@effect/rpc/".length) as + if (Object.hasOwn(decoded, "method")) { + const request = decoded as JsonRpcRequest + if (Predicate.isNullish(request.id) && request.method.startsWith("@effect/rpc/")) { + const tag = request.method.slice("@effect/rpc/".length) as | RpcMessage.FromServerEncoded["_tag"] | Exclude - const requestId = (decoded as any).params?.requestId + const requestId = (request as any).params?.requestId return requestId ? { _tag: tag, @@ -227,46 +287,50 @@ function decodeJsonRpcMessage(decoded: JsonRpcMessage): RpcMessage.FromClientEnc } return { _tag: "Request", - id: decoded.id ?? "", - tag: decoded.method, - payload: decoded.params ?? null, - headers: decoded.headers ?? [], - ...(decoded.spanId ? + id: request.id ?? "", + tag: request.method, + payload: request.params ?? null, + headers: request.headers ?? [], + ...(Predicate.hasProperty(request, "id") ? {} : { isNotification: true as const }), + ...(request.spanId ? { - traceId: decoded.traceId, - spanId: decoded.spanId!, - sampled: decoded.sampled! + traceId: request.traceId, + spanId: request.spanId!, + sampled: request.sampled! } : {}) } - } else if (decoded.error && decoded.error._tag === "Defect") { + } + const response = decoded as JsonRpcResponse + const hasError = Object.hasOwn(response, "error") + if (hasError && response.error && response.error._tag === "Defect") { return { _tag: "Defect", - defect: decoded.error.data + defect: response.error.data } - } else if (decoded.chunk === true) { + } else if (Object.hasOwn(response, "chunk") && response.chunk === true) { return { _tag: "Chunk", - requestId: decoded.id ?? "", - values: decoded.result as any + requestId: response.id ?? "", + values: response.result as any } } return { _tag: "Exit", - requestId: decoded.id ?? "", - exit: decoded.error != null ? + requestId: response.id ?? "", + exit: hasError && response.error != null ? { _tag: "Failure", - cause: decoded.error._tag === "Cause" ? - decoded.error.data as any : + cause: response.error._tag === "Cause" ? + response.error.data as any : [{ _tag: "Die", - defect: decoded.error + defect: response.error }] } : { _tag: "Success", - value: decoded.result + value: response.result } } } @@ -372,7 +436,8 @@ function encodeJsonRpcMessage(response: RpcMessage.FromServerEncoded | RpcMessag result: response.exit.value } as any } - const error = response.exit.cause.find((failure) => failure._tag === "Fail") + const failure = response.exit.cause.find((failure) => failure._tag === "Fail") + const error = failure?._tag === "Fail" ? failure.error : undefined return { jsonrpc: "2.0", id: response.requestId ?? undefined, @@ -438,19 +503,29 @@ type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse * @category serialization * @since 4.0.0 */ -export const makeMsgPack = (options?: Msgpackr.Options | undefined): RpcSerialization["Service"] => - RpcSerialization.of({ +export const makeMsgPack = ( + options?: (Msgpackr.Options & StreamOptions) | undefined +): RpcSerialization["Service"] => { + const { maxBufferSize = defaultMaxBufferSize, ...msgpackOptions } = options ?? {} + return RpcSerialization.of({ contentType: "application/msgpack", includesFraming: true, makeUnsafe: () => { - const unpackr = new Msgpackr.Unpackr(options) - const packr = new Msgpackr.Packr(options) + const unpackr = new Msgpackr.Unpackr(msgpackOptions) + const packr = new Msgpackr.Packr(msgpackOptions) const encoder = new TextEncoder() let incomplete: Uint8Array | undefined = undefined + const failMaxBufferSize = (maxBufferSize: number): never => { + incomplete = undefined + throw new MaxBufferSizeExceeded({ maxBufferSize }) + } return { decode(bytes) { let buf = typeof bytes === "string" ? encoder.encode(bytes) : bytes if (incomplete !== undefined) { + if (isBufferSizeExceeded(incomplete.length + buf.length, maxBufferSize)) { + failMaxBufferSize(maxBufferSize) + } const prev = buf bytes = new Uint8Array(incomplete.length + buf.length) bytes.set(incomplete) @@ -464,6 +539,9 @@ export const makeMsgPack = (options?: Msgpackr.Options | undefined): RpcSerializ const error = error_ as any if (error.incomplete) { incomplete = buf.subarray(error.lastPosition) + if (isBufferSizeExceeded(incomplete.length, maxBufferSize)) { + failMaxBufferSize(maxBufferSize) + } return error.values ?? [] } throw error_ @@ -473,6 +551,7 @@ export const makeMsgPack = (options?: Msgpackr.Options | undefined): RpcSerializ } } }) +} /** * Default MessagePack RPC serialization using record support and built-in @@ -492,7 +571,7 @@ export const msgPack: RpcSerialization["Service"] = makeMsgPack({ useRecords: tr * * @see {@link layerNdjson} for transports that need newline-delimited framing * - * @category serialization + * @category layers * @since 4.0.0 */ export const layerJson: Layer.Layer = Layer.succeed(RpcSerialization)(json) @@ -506,15 +585,24 @@ export const layerJson: Layer.Layer = Layer.succeed(RpcSeriali * * @see {@link layerJson} for transports that already provide message framing * - * @category serialization + * @category layers * @since 4.0.0 */ export const layerNdjson: Layer.Layer = Layer.succeed(RpcSerialization)(ndjson) +/** + * RPC serialization layer that uses NDJSON with custom streaming options. + * + * @category layers + * @since 4.0.0 + */ +export const layerNdjsonWith = (options?: StreamOptions): Layer.Layer => + Layer.succeed(RpcSerialization)(makeNdjson(options)) + /** * RPC serialization layer that uses JSON-RPC for serialization. * - * @category serialization + * @category layers * @since 4.0.0 */ export const layerJsonRpc = (options?: { @@ -525,11 +613,12 @@ export const layerJsonRpc = (options?: { * RPC serialization layer that uses newline-delimited JSON-RPC for * serialization. * - * @category serialization + * @category layers * @since 4.0.0 */ export const layerNdJsonRpc = (options?: { readonly contentType?: string | undefined + readonly maxBufferSize?: number | "unbounded" | undefined }): Layer.Layer => Layer.succeed(RpcSerialization)(ndJsonRpc(options)) /** @@ -540,7 +629,17 @@ export const layerNdJsonRpc = (options?: { * MessagePack has a more compact binary format compared to JSON and NDJSON. It * also has better support for binary data. * - * @category serialization + * @category layers * @since 4.0.0 */ export const layerMsgPack: Layer.Layer = Layer.succeed(RpcSerialization)(msgPack) + +/** + * RPC serialization layer that uses MessagePack with custom options. + * + * @category layers + * @since 4.0.0 + */ +export const layerMsgPackWith = ( + options?: (Msgpackr.Options & StreamOptions) | undefined +): Layer.Layer => Layer.succeed(RpcSerialization)(makeMsgPack(options)) diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts index 329db27a8..5a944e153 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcServer.ts @@ -27,6 +27,7 @@ import * as Pull from "../../Pull.ts" import * as Queue from "../../Queue.ts" import * as Schedule from "../../Schedule.ts" import * as Schema from "../../Schema.ts" +import * as SchemaIssue from "../../SchemaIssue.ts" import * as Scope from "../../Scope.ts" import * as Semaphore from "../../Semaphore.ts" import { Stdio } from "../../Stdio.ts" @@ -37,7 +38,7 @@ import * as Headers from "../http/Headers.ts" import * as HttpRouter from "../http/HttpRouter.ts" import * as HttpServerRequest from "../http/HttpServerRequest.ts" import * as HttpServerResponse from "../http/HttpServerResponse.ts" -import type * as Socket from "../socket/Socket.ts" +import * as Socket from "../socket/Socket.ts" import * as SocketServer from "../socket/SocketServer.ts" import * as Transferable from "../workers/Transferable.ts" import type { WorkerError } from "../workers/WorkerError.ts" @@ -63,7 +64,7 @@ import { withRun } from "./Utils.ts" * The decoded RPC server boundary, accepting client messages for a client id * and allowing that client to be disconnected. * - * @category server + * @category models * @since 4.0.0 */ export interface RpcServer { @@ -78,7 +79,7 @@ export interface RpcServer { * handlers for a group and sending decoded server responses through * `onFromServer`. * - * @category server + * @category constructors * @since 4.0.0 */ export const makeNoSerialization: ( @@ -483,7 +484,7 @@ const applyMiddleware = ( * requests, invoking handlers, encoding responses, and managing in-flight * request lifetime. * - * @category server + * @category running * @since 4.0.0 */ export const make: ( @@ -635,7 +636,7 @@ export const make: ( Effect.flatMap((a) => send(client.id, onSuccess(a), collector && collector.clearUnsafe())), Effect.catchCause((cause) => { client.schemas.delete(requestId) - const defect = Cause.squash(Cause.map(cause, (e) => e.issue.toString())) + const defect = Cause.squash(Cause.map(cause, (e) => SchemaIssue.defaultFormatter(e.issue))) return Effect.andThen( sendRequestDefect(client, requestId, encodeDefect, defect), server.write(client.id, { _tag: "Interrupt", requestId, interruptors: [] }) @@ -688,7 +689,7 @@ export const make: ( switch (request._tag) { case "Request": { - const tag = Predicate.hasProperty(request, "tag") ? (request.tag as string) : "" + const tag = Object.hasOwn(request, "tag") ? (request.tag as string) : "" let requestId: RequestId switch (typeof request.id) { case "number": @@ -708,7 +709,8 @@ export const make: ( return Effect.matchEffect( Effect.provideContext(schemas.decode(request.payload), schemas.context), { - onFailure: (error) => sendRequestDefect(client, requestId, schemas.encodeDefect, error.issue.toString()), + onFailure: (error) => + sendRequestDefect(client, requestId, schemas.encodeDefect, SchemaIssue.defaultFormatter(error.issue)), onSuccess: (payload) => { client.schemas.set( requestId, @@ -762,7 +764,7 @@ export const make: ( * Provides a scoped layer that starts an RPC server for a group using the * current server `Protocol`. * - * @category server + * @category layers * @since 4.0.0 */ export const layer = ( @@ -791,7 +793,7 @@ export const layer = ( * Defaults to using websockets for communication, but can be configured to use * HTTP. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerHttp = (options: { @@ -830,7 +832,7 @@ export const layerHttp = (options: { * Use to provide the transport boundary for RPC servers over HTTP, WebSocket, * workers, sockets, or custom protocols. * - * @category protocols + * @category services * @since 4.0.0 */ export class Protocol extends Context.Service< @@ -880,7 +882,7 @@ export const makeProtocolSocketServer = Effect.gen(function*() { /** * RPC protocol that uses `SocketServer` for communication. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolSocketServer: Layer.Layer< @@ -947,7 +949,7 @@ export const makeProtocolWebsocket: (options: { /** * RPC protocol that uses WebSockets for communication. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolWebsocket = (options: { @@ -1152,7 +1154,7 @@ export const makeProtocolHttp: (options: { * Provides a server `Protocol` that uses HTTP POST requests for RPC * communication. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolHttp = (options: { @@ -1165,7 +1167,7 @@ export const layerProtocolHttp = (options: { * Starts an RPC server for a group and returns the HTTP request/response effect * that serves the non-websocket HTTP RPC protocol. * - * @category http app + * @category running * @since 4.0.0 */ export const toHttpEffect: ( @@ -1206,7 +1208,7 @@ export const toHttpEffect: ( * Starts an RPC server for a group and returns the HTTP effect that upgrades * requests to the websocket RPC protocol. * - * @category http app + * @category running * @since 4.0.0 */ export const toHttpEffectWebsocket: ( @@ -1308,7 +1310,7 @@ export const makeProtocolStdio = Effect.gen(function*() { * Provides a server `Protocol` that reads RPC messages from `Stdio.stdin` and * writes encoded responses to `Stdio.stdout`. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolStdio: Layer.Layer< @@ -1379,7 +1381,7 @@ export const makeProtocolWorkerRunner: Effect.Effect< /** * Provides a server `Protocol` backed by the current `WorkerRunnerPlatform`. * - * @category protocols + * @category layers * @since 4.0.0 */ export const layerProtocolWorkerRunner: Layer.Layer< @@ -1466,6 +1468,9 @@ const makeSocketProtocol: Effect.Effect< step: constVoid }) } catch (cause) { + if (Predicate.isTagged(cause, "MaxBufferSizeExceeded")) { + return writeRaw(new Socket.CloseEvent(1009, String(cause))) + } return writeRaw(parser.encode(ResponseDefectEncoded(cause))!) } }).pipe( diff --git a/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts b/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts index 50e0b9f13..f7dd7504f 100644 --- a/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts +++ b/.context/effect/packages/effect/src/unstable/rpc/RpcWorker.ts @@ -21,7 +21,7 @@ import type { Protocol } from "./RpcServer.ts" * Context service that supplies the initial RPC worker message as encoded data * paired with any transferables that should be posted with it. * - * @category initial message + * @category services * @since 4.0.0 */ export class InitialMessage extends Context.Service< @@ -44,7 +44,7 @@ export declare namespace InitialMessage { * Tagged wire representation of an RPC worker initial message after schema * encoding. * - * @category initial message + * @category models * @since 4.0.0 */ export interface Encoded { @@ -61,7 +61,7 @@ const ProtocolTag = Context.Service( * Runs an effect, encodes its result with the schema's JSON codec, and returns * the encoded value together with collected transferables. * - * @category initial message + * @category encoding * @since 4.0.0 */ export const makeInitialMessage = ( @@ -86,7 +86,7 @@ export const makeInitialMessage = ( * Provides the `InitialMessage` service from a schema and build effect, * capturing the layer context and dying if schema encoding fails. * - * @category initial message + * @category layers * @since 4.0.0 */ export const layerInitialMessage = ( @@ -105,7 +105,7 @@ export const layerInitialMessage = ( * Reads the protocol initial message and decodes it with the supplied schema, * failing if no initial message is available or decoding fails. * - * @category initial message + * @category decoding * @since 4.0.0 */ export const initialMessage = ( diff --git a/.context/effect/packages/effect/src/unstable/schema/Model.ts b/.context/effect/packages/effect/src/unstable/schema/Model.ts index effd6423b..59f61695d 100644 --- a/.context/effect/packages/effect/src/unstable/schema/Model.ts +++ b/.context/effect/packages/effect/src/unstable/schema/Model.ts @@ -74,7 +74,7 @@ export { * * **Example** (Defining a variant model class) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" * import { Model } from "effect/unstable/schema" * @@ -107,6 +107,8 @@ export { * return this.name.toUpperCase() * } * } + * + * [Schema.isSchema(Group), Schema.isSchema(Group.insert), Schema.isSchema(Group.json)] // => [true, true, true] * ``` * * @category constructors @@ -116,21 +118,21 @@ export { /** * Extracts a generated variant schema from a model or variant struct. * - * @category extraction + * @category converting * @since 4.0.0 */ extract, /** * Creates a variant field from schemas keyed by variant name. * - * @category fields + * @category constructors * @since 4.0.0 */ Field, /** * Transforms schemas inside a variant field or plain schema by variant name. * - * @category fields + * @category transforming * @since 4.0.0 */ fieldEvolve, @@ -138,14 +140,14 @@ export { * Creates a variant field that applies a schema to every variant except the * supplied keys. * - * @category fields + * @category constructors * @since 4.0.0 */ FieldExcept, /** * Creates a variant field that applies a schema only to the supplied variants. * - * @category fields + * @category constructors * @since 4.0.0 */ FieldOnly, @@ -169,7 +171,7 @@ export { /** * Returns the variant field definitions stored on a model or variant struct. * - * @category fields + * @category getters * @since 4.0.0 */ export const fields: >(self: A) => A[typeof VariantSchema.TypeId] = @@ -179,7 +181,7 @@ export const fields: >(self: A) => A[typeof * Marks a value as an explicit override for fields that otherwise use an * overrideable default. * - * @category overrideable + * @category constructors * @since 4.0.0 */ export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Override @@ -196,7 +198,7 @@ export const Override: (value: A) => A & Brand<"Override"> = VariantSchema.Ov * @see {@link Field} for generated columns that need a custom variant set, such * as primary keys used in update payloads. * - * @category generated + * @category schemas * @since 4.0.0 */ export interface GeneratedByDb extends @@ -218,7 +220,7 @@ export interface GeneratedByDb extends * @see {@link Field} for generated columns that need a custom variant set, such * as primary keys used in update payloads. * - * @category generated + * @category schemas * @since 4.0.0 */ export const GeneratedByDb = ( @@ -234,7 +236,7 @@ export const GeneratedByDb = ( * database variants and read JSON, but omitted from JSON create and update * variants. * - * @category generated + * @category schemas * @since 4.0.0 */ export interface GeneratedByApp extends @@ -251,7 +253,7 @@ export interface GeneratedByApp extends * variants and the read JSON variant, but omitted from JSON create and update * variants. * - * @category generated + * @category schemas * @since 4.0.0 */ export const GeneratedByApp = (schema: S): GeneratedByApp => @@ -266,7 +268,7 @@ export const GeneratedByApp = (schema: S): GeneratedByApp< * Variant field type for a sensitive value that is available to database variants * and omitted from all JSON variants. * - * @category sensitive + * @category schemas * @since 4.0.0 */ export interface Sensitive extends @@ -281,7 +283,7 @@ export interface Sensitive extends * A field that represents a sensitive value that should not be exposed in the * JSON variants. * - * @category sensitive + * @category schemas * @since 4.0.0 */ export const Sensitive = (schema: S): Sensitive => @@ -295,7 +297,7 @@ export const Sensitive = (schema: S): Sensitive => * Schema type for an optional object key whose encoded value may be missing or * null and whose decoded value is an `Option`. * - * @category optional + * @category schemas * @since 4.0.0 */ export interface optionalOption @@ -306,7 +308,7 @@ export interface optionalOption * Creates a schema for optional keys that decodes missing or null encoded values * through `Option` and encodes `Option` values back to optional nullable keys. * - * @category optional + * @category schemas * @since 4.0.0 */ export const optionalOption = (schema: S): optionalOption => @@ -328,7 +330,7 @@ export const optionalOption = (schema: S): optional * For the database variants, it will accept `null`able values. * For the JSON variants, it will also accept missing keys. * - * @category optional + * @category schemas * @since 4.0.0 */ export interface FieldOption extends @@ -350,7 +352,7 @@ export interface FieldOption extends * For the database variants, it will accept `null`able values. * For the JSON variants, it will also accept missing keys. * - * @category optional + * @category schemas * @since 4.0.0 */ export const FieldOption: | Schema.Top>( @@ -376,7 +378,7 @@ export const FieldOption: | Schema.Top>( * Variant field type for SQLite booleans stored as `0 | 1` in database variants * and exposed as `boolean` in JSON variants. * - * @category booleans + * @category schemas * @since 4.0.0 */ export interface BooleanSqlite extends @@ -394,7 +396,7 @@ export interface BooleanSqlite extends * Schema for sqlite booleans that are represented as `0 | 1` in database * variants and `boolean` in JSON variants. * - * @category booleans + * @category schemas * @since 4.0.0 */ export const BooleanSqlite: BooleanSqlite = Field({ @@ -410,7 +412,7 @@ export const BooleanSqlite: BooleanSqlite = Field({ * Schema type for a `DateTime.Utc` date-only value encoded as a `YYYY-MM-DD` * string. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface Date extends Schema.decodeTo, Schema.String> {} @@ -419,7 +421,7 @@ export interface Date extends Schema.decodeTo, S * Schema for a `DateTime.Utc` that is serialized as a date string in the * format `YYYY-MM-DD`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const Date: Date = Schema.String.pipe( @@ -433,7 +435,7 @@ export const Date: Date = Schema.String.pipe( * Schema for an overrideable UTC date-only field whose constructor default is * the current date with the time component removed. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateWithNow = VariantSchema.Overrideable(Date, { @@ -444,7 +446,7 @@ export const DateWithNow = VariantSchema.Overrideable(Date, { * Schema for an overrideable UTC date-time field encoded as a string and * defaulted to the current `DateTime.Utc`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromString, { @@ -455,7 +457,7 @@ export const DateTimeWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFrom * Schema for an overrideable UTC date-time field encoded as a JavaScript `Date` * and defaulted to the current `DateTime.Utc`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeFromDateWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromDate, { @@ -466,7 +468,7 @@ export const DateTimeFromDateWithNow = VariantSchema.Overrideable(Schema.DateTim * Schema for an overrideable UTC date-time field encoded as milliseconds and * defaulted to the current `DateTime.Utc`. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeFromNumberWithNow = VariantSchema.Overrideable(Schema.DateTimeUtcFromMillis, { @@ -477,7 +479,7 @@ export const DateTimeFromNumberWithNow = VariantSchema.Overrideable(Schema.DateT * Variant field type for a UTC date-time stored as a string, defaulted to the * current time on insert, available for selection, and omitted from updates. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeInsert extends @@ -496,7 +498,7 @@ export interface DateTimeInsert extends * * It is omitted from updates and is available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeInsert: DateTimeInsert = Field({ @@ -509,7 +511,7 @@ export const DateTimeInsert: DateTimeInsert = Field({ * Variant field type for a UTC date-time stored as a JavaScript `Date` in * database variants, encoded as a string for JSON, and defaulted on insert. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeInsertFromDate extends @@ -528,7 +530,7 @@ export interface DateTimeInsertFromDate extends * * It is omitted from updates and is available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeInsertFromDate: DateTimeInsertFromDate = Field({ @@ -541,7 +543,7 @@ export const DateTimeInsertFromDate: DateTimeInsertFromDate = Field({ * Variant field type for a UTC date-time encoded as milliseconds and defaulted to * the current time on insert. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeInsertFromNumber extends @@ -560,7 +562,7 @@ export interface DateTimeInsertFromNumber extends * * It is omitted from updates and is available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeInsertFromNumber: DateTimeInsertFromNumber = Field({ @@ -573,7 +575,7 @@ export const DateTimeInsertFromNumber: DateTimeInsertFromNumber = Field({ * Variant field type for a UTC date-time stored as a string and defaulted to the * current time on both inserts and updates. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeUpdate extends @@ -594,7 +596,7 @@ export interface DateTimeUpdate extends * It is set to the current `DateTime.Utc` on updates and inserts and is * available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeUpdate: DateTimeUpdate = Field({ @@ -609,7 +611,7 @@ export const DateTimeUpdate: DateTimeUpdate = Field({ * database variants, encoded as a string for JSON, and defaulted on inserts and * updates. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeUpdateFromDate extends @@ -630,7 +632,7 @@ export interface DateTimeUpdateFromDate extends * It is set to the current `DateTime.Utc` on updates and inserts and is * available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeUpdateFromDate: DateTimeUpdateFromDate = Field({ @@ -644,7 +646,7 @@ export const DateTimeUpdateFromDate: DateTimeUpdateFromDate = Field({ * Variant field type for a UTC date-time encoded as milliseconds and defaulted to * the current time on both inserts and updates. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export interface DateTimeUpdateFromNumber extends @@ -665,7 +667,7 @@ export interface DateTimeUpdateFromNumber extends * It is set to the current `DateTime.Utc` on updates and inserts and is * available for selection. * - * @category DateTime + * @category schemas * @since 4.0.0 */ export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({ @@ -679,7 +681,7 @@ export const DateTimeUpdateFromNumber: DateTimeUpdateFromNumber = Field({ * Variant field type for a JSON value stored as text in database variants and * exposed through the supplied schema in JSON variants. * - * @category models + * @category schemas * @since 4.0.0 */ export interface JsonFromString extends @@ -700,7 +702,7 @@ export interface JsonFromString extends * * The "json" variants will use the object schema directly. * - * @category constructors + * @category schemas * @since 4.0.0 */ export const JsonFromString = ( @@ -721,7 +723,7 @@ export const JsonFromString = ( * Variant field type for a branded binary UUID v4 value whose insert variant * generates a UUID by default. * - * @category uuid + * @category schemas * @since 4.0.0 */ export interface UuidV4BytesInsert extends @@ -736,7 +738,7 @@ export interface UuidV4BytesInsert extends /** * Schema for binary `Uint8Array` values backed by an `ArrayBuffer`. * - * @category Uint8Array + * @category schemas * @since 4.0.0 */ export const Uint8Array: Schema.instanceOf> = Schema.Uint8Array as Schema.instanceOf< @@ -747,7 +749,7 @@ export const Uint8Array: Schema.instanceOf> = Schema.Uin * Adds a constructor default that generates a binary UUID v4 for a branded * `Uint8Array` schema. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV4BytesWithGenerate = ( @@ -758,7 +760,7 @@ export const UuidV4BytesWithGenerate = ( /** * A field that represents a binary UUID v4 that is generated on inserts. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV4BytesInsert = ( @@ -775,7 +777,7 @@ export const UuidV4BytesInsert = ( * Variant field type for a branded string UUID v4 value whose insert variant * generates a UUID by default. * - * @category uuid + * @category schemas * @since 4.0.0 */ export interface UuidV4Insert extends @@ -790,7 +792,7 @@ export interface UuidV4Insert extends /** * Adds a constructor default that generates a string UUID v4. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV4WithGenerate = ( @@ -801,7 +803,7 @@ export const UuidV4WithGenerate = ( /** * A field that represents a string UUID v4 that is generated on inserts. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV4Insert = ( @@ -818,7 +820,7 @@ export const UuidV4Insert = ( * Variant field type for a branded string UUID v7 value whose insert variant * generates a UUID by default. * - * @category uuid + * @category schemas * @since 4.0.0 */ export interface UuidV7Insert extends @@ -833,7 +835,7 @@ export interface UuidV7Insert extends /** * Adds a constructor default that generates a string UUID v7. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV7WithGenerate = ( @@ -848,7 +850,7 @@ export const UuidV7WithGenerate = ( /** * A field that represents a string UUID v7 that is generated on inserts. * - * @category uuid + * @category schemas * @since 4.0.0 */ export const UuidV7Insert = ( diff --git a/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts b/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts index 060671283..895a17ef6 100644 --- a/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts +++ b/.context/effect/packages/effect/src/unstable/schema/VariantSchema.ts @@ -11,10 +11,10 @@ import type { Brand } from "../../Brand.ts" import * as Effect from "../../Effect.ts" import { dual } from "../../Function.ts" +import * as InternalRecord from "../../internal/record.ts" import { type Pipeable, pipeArguments } from "../../Pipeable.ts" import * as Predicate from "../../Predicate.ts" import * as Schema from "../../Schema.ts" -import type * as SchemaAST from "../../SchemaAST.ts" import * as Struct_ from "../../Struct.ts" /** @@ -26,6 +26,7 @@ import * as Struct_ from "../../Struct.ts" export const TypeId = "~effect/schema/VariantSchema" const cacheSymbol = Symbol.for(`${TypeId}/cache`) +const defaultCacheSymbol = Symbol.for(`${TypeId}/defaultCache`) /** * Pipeable container of schema fields that can be extracted into per-variant @@ -38,6 +39,8 @@ export interface Struct extends Pipeable { readonly [TypeId]: A /** @internal */ [cacheSymbol]?: Record + /** @internal */ + [defaultCacheSymbol]?: Record } /** @@ -87,7 +90,7 @@ export declare namespace Struct { 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" - : {} + : unknown } } @@ -144,7 +147,7 @@ export declare namespace Field { * @since 4.0.0 */ export type ConfigWithKeys = { - readonly [P in K]?: Schema.Top + readonly [P in K]?: Schema.Top | undefined } /** @@ -167,7 +170,7 @@ export declare namespace Field { * Computes the `Schema.Struct` field map for a variant by selecting matching * field schemas and recursively extracting nested structs. * - * @category extractors + * @category utility types * @since 4.0.0 */ export type ExtractFields = { @@ -186,7 +189,7 @@ export type ExtractFields, IsDefault = false> = [A] extends [ @@ -214,29 +217,38 @@ const extract: { 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 cache = options?.isDefault === true + ? self[defaultCacheSymbol] ?? (self[defaultCacheSymbol] = Object.create(null)) + : self[cacheSymbol] ?? (self[cacheSymbol] = Object.create(null)) + if (Object.hasOwn(cache, variant)) { + return cache[variant] as any } const fields: Record = {} for (const key of Object.keys(self[TypeId])) { const value = self[TypeId][key] + if (value === undefined) { + continue + } if (TypeId in value) { if (options?.isDefault === true && Schema.isSchema(value)) { - fields[key] = value + InternalRecord.assignProperty(fields, key, value) } else { - fields[key] = extract(value, variant) + InternalRecord.assignProperty(fields, key, extract(value, variant)) } } else if (FieldTypeId in value) { - if (variant in value.schemas) { - fields[key] = value.schemas[variant] + if (Object.hasOwn(value.schemas, variant)) { + const schema = value.schemas[variant] + if (schema !== undefined) { + InternalRecord.assignProperty(fields, key, schema) + } } } else { - fields[key] = value + InternalRecord.assignProperty(fields, key, value) } } - return cache[cacheKey] = Schema.Struct(fields) as any + const schema = Schema.Struct(fields) + cache[variant] = schema + return schema as any } ) @@ -261,19 +273,7 @@ export interface Class< S extends Schema.Top & { readonly fields: Schema.Struct.Fields } -> extends - Schema.BottomLazy< - SchemaAST.Declaration, - Schema.decodeTo, S>, - readonly [S], - S["~type.mutability"], - S["~type.optionality"], - S["~type.constructor.default"], - S["~encoded.mutability"], - S["~encoded.optionality"] - >, - Struct> -{ +> extends Schema.Class, Struct> { readonly "Type": Self readonly "Encoded": S["Encoded"] readonly "DecodingServices": S["DecodingServices"] @@ -306,12 +306,13 @@ type MissingSelfGeneric = * @category models * @since 4.0.0 */ -export interface Union>> extends - Schema.Union< - { - readonly [K in keyof Members]: [Members[K]] extends [Schema.Top] ? Members[K] : never - } - > +export interface Union>, Default extends string = string> + extends + Schema.Union< + { + readonly [K in keyof Members]: Extract + } + > {} /** @@ -420,7 +421,7 @@ export const make = < } readonly Union: >>( members: Members - ) => Union & Union.Variants + ) => Union & Union.Variants readonly extract: { ( variant: V @@ -462,7 +463,7 @@ export const make = < return function(schema: S) { const obj: Record = {} for (const key of keys) { - obj[key] = schema + InternalRecord.assignProperty(obj, key, schema) } return Field(obj) } @@ -472,14 +473,14 @@ export const make = < const obj: Record = {} for (const variant of options.variants) { if (!keys.includes(variant)) { - obj[variant] = schema + InternalRecord.assignProperty(obj, variant, schema) } } return Field(obj) } } function UnionVariants(members: ReadonlyArray>) { - return Union(members, options.variants) + return Union(members, options.defaultVariant, options.variants) } const fieldEvolve = dual( 2, @@ -516,7 +517,7 @@ export const make = < /** * Marks a value as an explicit override for an `Overrideable` schema default. * - * @category overrideable + * @category constructors * @since 4.0.0 */ export const Override = (value: A): A & Brand<"Override"> => value as any @@ -525,7 +526,7 @@ export const Override = (value: A): A & Brand<"Override"> => value as any * Schema type whose constructor can use an effectful default unless a value is * explicitly branded with `Override`. * - * @category overrideable + * @category schemas * @since 4.0.0 */ export interface Overrideable extends @@ -553,7 +554,7 @@ export interface Overrideable( @@ -592,11 +593,18 @@ const Field = (schemas: A): Field => { return self } -const Union = >, Variants extends ReadonlyArray>( +const Union = < + Members extends ReadonlyArray>, + Default extends string, + Variants extends ReadonlyArray +>( members: Members, + defaultVariant: Default, variants: Variants ) => { - const VariantUnion = Schema.Union(members.filter((member) => Schema.isSchema(member))) as any + const VariantUnion = Schema.Union( + members.map((member) => Schema.isSchema(member) ? member : extract(member, defaultVariant, { isDefault: true })) + ) as any for (const variant of variants) { Object.defineProperty(VariantUnion, variant, { value: Schema.Union(members.map((member) => extract(member, variant))) diff --git a/.context/effect/packages/effect/src/unstable/socket/Socket.ts b/.context/effect/packages/effect/src/unstable/socket/Socket.ts index 103b1b9ca..13eba59fd 100644 --- a/.context/effect/packages/effect/src/unstable/socket/Socket.ts +++ b/.context/effect/packages/effect/src/unstable/socket/Socket.ts @@ -185,7 +185,7 @@ export class CloseEvent { /** * Returns `true` when a value is a `CloseEvent`. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isCloseEvent = (u: unknown): u is CloseEvent => Predicate.hasProperty(u, CloseEventTypeId) @@ -209,7 +209,7 @@ export const SocketErrorTypeId: SocketErrorTypeId = "~effect/socket/Socket/Socke /** * Returns `true` when a value is a `SocketError`. * - * @category refinements + * @category guards * @since 4.0.0 */ export const isSocketError = (u: unknown): u is SocketError => Predicate.hasProperty(u, SocketErrorTypeId) @@ -220,7 +220,7 @@ export const isSocketError = (u: unknown): u is SocketError => Predicate.hasProp * @category errors * @since 4.0.0 */ -export class SocketReadError extends Schema.ErrorClass("effect/socket/Socket/SocketReadError")({ +export class SocketReadError extends Schema.Error("effect/socket/Socket/SocketReadError")({ _tag: Schema.tag("SocketReadError"), cause: Schema.Defect() }) { @@ -238,7 +238,7 @@ export class SocketReadError extends Schema.ErrorClass("effect/ * @category errors * @since 4.0.0 */ -export class SocketWriteError extends Schema.ErrorClass("effect/socket/Socket/SocketWriteError")({ +export class SocketWriteError extends Schema.Error("effect/socket/Socket/SocketWriteError")({ _tag: Schema.tag("SocketWriteError"), cause: Schema.Defect() }) { @@ -257,7 +257,7 @@ export class SocketWriteError extends Schema.ErrorClass("effec * @category errors * @since 4.0.0 */ -export class SocketOpenError extends Schema.ErrorClass("effect/socket/Socket/SocketOpenError")({ +export class SocketOpenError extends Schema.Error("effect/socket/Socket/SocketOpenError")({ _tag: Schema.tag("SocketOpenError"), kind: Schema.Literals(["Unknown", "Timeout"]), cause: Schema.Defect() @@ -281,9 +281,9 @@ export class SocketOpenError extends Schema.ErrorClass("effect/ * @category errors * @since 4.0.0 */ -export class SocketCloseError extends Schema.ErrorClass("effect/socket/Socket/SocketCloseError")({ +export class SocketCloseError extends Schema.Error("effect/socket/Socket/SocketCloseError")({ _tag: Schema.tag("SocketCloseError"), - code: Schema.Number, + code: Schema.Int, closeReason: Schema.optional(Schema.String) }) { /** @@ -339,7 +339,7 @@ export type SocketErrorReason = * @category errors * @since 4.0.0 */ -export class SocketError extends Schema.TaggedErrorClass(SocketErrorTypeId)("SocketError", { +export class SocketError extends Schema.TaggedError(SocketErrorTypeId)("SocketError", { _tag: Schema.tag("SocketError"), reason: SocketErrorReason }) { @@ -608,10 +608,19 @@ export const fromWebSocket = ( options?: { readonly closeCodeIsError?: ((code: number) => boolean) | undefined readonly openTimeout?: Duration.Input | undefined + /** + * Replays buffered events on the first run after the socket opens and before + * the run's `onOpen` effect. + * + * @category options + * @since 4.0.0 + */ + readonly onInitialRun?: ((ws: globalThis.WebSocket) => ReadonlyArray) | undefined } | undefined ): Effect.Effect> => Effect.withFiber((fiber) => { let currentWS: globalThis.WebSocket | undefined + let initial = true const latch = Latch.makeUnsafe(false) const acquireContext = fiber.context as Context.Context const closeCodeIsError = options?.closeCodeIsError ?? defaultCloseCodeIsError @@ -638,7 +647,7 @@ export const fromWebSocket = ( ) return run(effect) } - const result = handler(event.data) + const result = handler(event.data instanceof ArrayBuffer ? new Uint8Array(event.data) : event.data) if (Effect.isEffect(result)) { run(result) } @@ -708,6 +717,10 @@ export const fromWebSocket = ( open = true currentWS = ws latch.openUnsafe() + if (initial && options?.onInitialRun) { + initial = false + for (const event of options.onInitialRun(ws)) onMessage(event) + } if (opts?.onOpen) yield* opts.onOpen return yield* Effect.catchFilter( FiberSet.join(fiberSet), @@ -723,14 +736,21 @@ export const fromWebSocket = ( ) 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 as string | Uint8Array) - } - })) + latch.whenOpen( + Effect.suspend(() => { + try { + const ws = currentWS! + if (isCloseEvent(chunk)) { + ws.close(chunk.code, chunk.reason) + } else { + ws.send(chunk as string | Uint8Array) + } + return Effect.void + } catch (cause) { + return Effect.fail(new SocketError({ reason: new SocketWriteError({ cause }) })) + } + }) + ) const writer = Effect.succeed(write) return Effect.succeed(make({ @@ -783,7 +803,7 @@ export const layerWebSocket: ( /** * Context reference for socket send queue capacity, defaulting to `16`. * - * @category fiber refs + * @category services * @since 4.0.0 */ export const SendQueueCapacity = Context.Reference("~effect/socket/Socket/SendQueueCapacity", { @@ -893,7 +913,10 @@ export const fromTransformStream = (acquire: Effect.Effect getWriter(stream).write(typeof chunk === "string" ? encoder.encode(chunk) : chunk)) + return Effect.tryPromise({ + try: () => getWriter(stream).write(typeof chunk === "string" ? encoder.encode(chunk) : chunk), + catch: (cause) => new SocketError({ reason: new SocketWriteError({ cause }) }) + }) })) const writer = Effect.acquireRelease( Effect.succeed(write), diff --git a/.context/effect/packages/effect/src/unstable/sql/Migrator.ts b/.context/effect/packages/effect/src/unstable/sql/Migrator.ts index e500f3f0f..2bf15e682 100644 --- a/.context/effect/packages/effect/src/unstable/sql/Migrator.ts +++ b/.context/effect/packages/effect/src/unstable/sql/Migrator.ts @@ -15,6 +15,7 @@ import { FileSystem } from "../../FileSystem.ts" import { pipe } from "../../Function.ts" import * as Option from "../../Option.ts" import * as Order from "../../Order.ts" +import { Path } from "../../Path.ts" import * as Client from "./SqlClient.ts" import type { SqlError } from "./SqlError.ts" @@ -400,11 +401,19 @@ export const fromRecord = (migrations: Record_.js`, `_.ts`, * `_.mjs`, or `_.mts`, and sorts migrations by id. * + * **Details** + * + * Requires a `Path` service appropriate for the migration directory's path + * syntax. On Windows, prefer a platform-aware implementation such as + * `NodePath.layer`; the core `Path.layer` uses POSIX semantics and does not + * preserve Windows drive-letter paths. + * * @category loaders * @since 4.0.0 */ -export const fromFileSystem: (directory: string) => Loader = Effect.fnUntraced(function*(directory) { +export const fromFileSystem: (directory: string) => Loader = Effect.fnUntraced(function*(directory) { const Fs = yield* FileSystem + const path = yield* Path const files = yield* Effect.mapError( Fs.readDirectory(directory), (cause) => @@ -424,14 +433,18 @@ export const fromFileSystem: (directory: string) => Loader = Effect. [ Number(id), name, - Effect.promise( - () => - import( - /* @vite-ignore */ - /* webpackIgnore: true */ - `${directory}/${basename}` - ) - ) + // `import` needs a file URL: on Windows an absolute path such as + // `D:\migrations\1_init.ts` is rejected by the ESM loader. `orDie` keeps the + // failure a defect so `loadMigration` reports it as an import error. + Effect.flatMap(Effect.orDie(path.toFileUrl(path.join(directory, basename))), (url) => + Effect.promise( + () => + import( + /* @vite-ignore */ + /* webpackIgnore: true */ + url.href + ) + )) ] ] as const }) diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlClient.ts b/.context/effect/packages/effect/src/unstable/sql/SqlClient.ts index 90b7849f2..b2bd66231 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlClient.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlClient.ts @@ -16,6 +16,7 @@ import * as Option from "../../Option.ts" import type * as Queue from "../../Queue.ts" import type { ReadonlyRecord } from "../../Record.ts" import * as Scope from "../../Scope.ts" +import * as Semaphore from "../../Semaphore.ts" import * as Stream from "../../Stream.ts" import * as Tracer from "../../Tracer.ts" import type { NoInfer } from "../../Types.ts" @@ -126,6 +127,7 @@ export declare namespace SqlClient { } let clientIdCounter = 0 +let transactionSemaphoreIdCounter = 0 /** * Constructs a `SqlClient` from connection acquirers, a compiler, transaction @@ -227,67 +229,73 @@ export const makeWithTransaction = (options: { readonly commit: (conn: NoInfer) => Effect.Effect readonly rollback: (conn: NoInfer) => Effect.Effect readonly rollbackSavepoint: (conn: NoInfer, id: number) => Effect.Effect -}) => -(effect: Effect.Effect): Effect.Effect => { - return Effect.uninterruptibleMask((restore) => - Effect.useSpan( - "sql.transaction", - { kind: "client" }, - (span) => - Effect.withFiber((fiber) => { - for (const [key, value] of options.spanAttributes) { - span.attribute(key, value) - } - const services = fiber.context - const clock = fiber.getRef(Clock) - const connOption = Context.getOption(services, options.transactionService) - const conn = connOption._tag === "Some" - ? Effect.succeed([undefined, connOption.value[0]] as const) - : options.acquireConnection - const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 - return Effect.flatMap( - conn, - ( - [scope, conn] - ) => - (id === 0 ? options.begin(conn) : options.savepoint(conn, id)).pipe( - Effect.flatMap(() => - Effect.provideContext( - restore(effect), - Context.mutate(services, (services) => +}) => { + const transactionSemaphore = Context.Service( + `effect/sql/SqlClient/TransactionSemaphore/${transactionSemaphoreIdCounter++}` + ) + return (effect: Effect.Effect): Effect.Effect => + Effect.uninterruptibleMask((restore) => + Effect.useSpan( + "sql.transaction", + { kind: "client" }, + (span) => + Effect.withFiber((fiber) => { + for (const [key, value] of options.spanAttributes) { + span.attribute(key, value) + } + const services = fiber.context + const clock = fiber.getRef(Clock) + const connOption = Context.getOption(services, options.transactionService) + const conn = connOption._tag === "Some" + ? Effect.succeed([undefined, connOption.value[0]] as const) + : options.acquireConnection + const id = connOption._tag === "Some" ? connOption.value[1] + 1 : 0 + const transaction = Effect.flatMap( + conn, + ( + [scope, conn] + ) => + (id === 0 ? options.begin(conn) : options.savepoint(conn, id)).pipe( + Effect.flatMap(() => + Effect.provideContext( + restore(effect), services.pipe( Context.add(options.transactionService, [conn, id]), + Context.add(transactionSemaphore, Semaphore.makeUnsafe(1)), Context.add(Tracer.ParentSpan, span) - )) - ) - ), - Effect.exit, - Effect.flatMap((exit) => { - let effect: Effect.Effect - if (Exit.isSuccess(exit)) { - if (id === 0) { - span.event("db.transaction.commit", clock.currentTimeNanosUnsafe()) - effect = Effect.orDie(options.commit(conn)) + ) + ) + ), + Effect.exit, + Effect.flatMap((exit) => { + let effect: Effect.Effect + if (Exit.isSuccess(exit)) { + if (id === 0) { + span.event("db.transaction.commit", clock.currentTimeNanosUnsafe()) + effect = Effect.orDie(options.commit(conn)) + } else { + span.event("db.transaction.savepoint", clock.currentTimeNanosUnsafe()) + effect = Effect.void + } } else { - span.event("db.transaction.savepoint", clock.currentTimeNanosUnsafe()) - effect = Effect.void + span.event("db.transaction.rollback", clock.currentTimeNanosUnsafe()) + effect = Effect.orDie( + id > 0 + ? options.rollbackSavepoint(conn, id) + : options.rollback(conn) + ) } - } else { - span.event("db.transaction.rollback", clock.currentTimeNanosUnsafe()) - effect = Effect.orDie( - id > 0 - ? options.rollbackSavepoint(conn, id) - : options.rollback(conn) - ) - } - const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect - return Effect.flatMap(withScope, () => exit) - }) - ) - ) - }) + const withScope = scope !== undefined ? Effect.ensuring(effect, Scope.close(scope, exit)) : effect + return Effect.flatMap(withScope, () => exit) + }) + ) + ) + return id === 0 + ? transaction + : Context.getUnsafe(services, transactionSemaphore).withPermit(transaction) + }) + ) ) - ) } /** @@ -333,7 +341,7 @@ export const TransactionConnection = ( * Context reference used by SQL integrations to opt in to safe integer * handling; defaults to `false`. * - * @category references + * @category services * @since 4.0.0 */ export const SafeIntegers = Context.Reference("effect/sql/SqlClient/SafeIntegers", { diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlError.ts b/.context/effect/packages/effect/src/unstable/sql/SqlError.ts index dbaf05ef0..b5f285099 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlError.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlError.ts @@ -28,7 +28,7 @@ const ReasonFields = { * @category errors * @since 4.0.0 */ -export class ConnectionError extends Schema.TaggedErrorClass("effect/sql/SqlError/ConnectionError")( +export class ConnectionError extends Schema.TaggedError("effect/sql/SqlError/ConnectionError")( "ConnectionError", ReasonFields ) { @@ -56,7 +56,7 @@ export class ConnectionError extends Schema.TaggedErrorClass("e * @category errors * @since 4.0.0 */ -export class AuthenticationError extends Schema.TaggedErrorClass( +export class AuthenticationError extends Schema.TaggedError( "effect/sql/SqlError/AuthenticationError" )("AuthenticationError", ReasonFields) { /** @@ -83,7 +83,7 @@ export class AuthenticationError extends Schema.TaggedErrorClass( +export class AuthorizationError extends Schema.TaggedError( "effect/sql/SqlError/AuthorizationError" )("AuthorizationError", ReasonFields) { /** @@ -109,7 +109,7 @@ export class AuthorizationError extends Schema.TaggedErrorClass("effect/sql/SqlError/SqlSyntaxError")( +export class SqlSyntaxError extends Schema.TaggedError("effect/sql/SqlError/SqlSyntaxError")( "SqlSyntaxError", ReasonFields ) { @@ -142,7 +142,7 @@ const UniqueViolationFields = { * @category errors * @since 4.0.0 */ -export class UniqueViolation extends Schema.TaggedErrorClass("effect/sql/SqlError/UniqueViolation")( +export class UniqueViolation extends Schema.TaggedError("effect/sql/SqlError/UniqueViolation")( "UniqueViolation", UniqueViolationFields ) { @@ -169,7 +169,7 @@ export class UniqueViolation extends Schema.TaggedErrorClass("e * @category errors * @since 4.0.0 */ -export class ConstraintError extends Schema.TaggedErrorClass("effect/sql/SqlError/ConstraintError")( +export class ConstraintError extends Schema.TaggedError("effect/sql/SqlError/ConstraintError")( "ConstraintError", ReasonFields ) { @@ -196,7 +196,7 @@ export class ConstraintError extends Schema.TaggedErrorClass("e * @category errors * @since 4.0.0 */ -export class DeadlockError extends Schema.TaggedErrorClass("effect/sql/SqlError/DeadlockError")( +export class DeadlockError extends Schema.TaggedError("effect/sql/SqlError/DeadlockError")( "DeadlockError", ReasonFields ) { @@ -224,7 +224,7 @@ export class DeadlockError extends Schema.TaggedErrorClass("effec * @category errors * @since 4.0.0 */ -export class SerializationError extends Schema.TaggedErrorClass( +export class SerializationError extends Schema.TaggedError( "effect/sql/SqlError/SerializationError" )("SerializationError", ReasonFields) { /** @@ -251,7 +251,7 @@ export class SerializationError extends Schema.TaggedErrorClass("effect/sql/SqlError/LockTimeoutError")( +export class LockTimeoutError extends Schema.TaggedError("effect/sql/SqlError/LockTimeoutError")( "LockTimeoutError", ReasonFields ) { @@ -278,7 +278,7 @@ export class LockTimeoutError extends Schema.TaggedErrorClass( * @category errors * @since 4.0.0 */ -export class StatementTimeoutError extends Schema.TaggedErrorClass( +export class StatementTimeoutError extends Schema.TaggedError( "effect/sql/SqlError/StatementTimeoutError" )("StatementTimeoutError", ReasonFields) { /** @@ -304,7 +304,7 @@ export class StatementTimeoutError extends Schema.TaggedErrorClass("effect/sql/SqlError/UnknownError")( +export class UnknownError extends Schema.TaggedError("effect/sql/SqlError/UnknownError")( "UnknownError", ReasonFields ) { @@ -384,7 +384,7 @@ export const SqlErrorReason: Schema.Union<[ * @category errors * @since 4.0.0 */ -export class SqlError extends Schema.TaggedErrorClass("effect/sql/SqlError")("SqlError", { +export class SqlError extends Schema.TaggedError("effect/sql/SqlError")("SqlError", { reason: SqlErrorReason }) { /** @@ -574,9 +574,9 @@ export const classifySqliteError = ( * @since 4.0.0 */ export class ResultLengthMismatch - extends Schema.TaggedErrorClass("effect/sql/ResultLengthMismatch")("ResultLengthMismatch", { - expected: Schema.Number, - actual: Schema.Number + extends Schema.TaggedError("effect/sql/ResultLengthMismatch")("ResultLengthMismatch", { + expected: Schema.Natural, + actual: Schema.Natural }) { /** diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts b/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts index fd31502f1..e8007e51f 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlModel.ts @@ -27,7 +27,7 @@ import * as SqlSchema from "./SqlSchema.ts" * supplied, reads ignore soft-deleted rows and delete updates that column * instead of removing the row. * - * @category repository + * @category constructors * @since 4.0.0 */ export const makeRepository = < @@ -224,7 +224,7 @@ select * from ${sql(options.tableName)} where ${withSoftDeleteFilter(sql`${sql(i * Creates batched request resolvers for a schema model's insert, insert-void, * find-by-id, and delete operations, honoring the optional soft-delete column. * - * @category repository + * @category constructors * @since 4.0.0 */ export const makeResolvers = < diff --git a/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts b/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts index 385117a08..88a88ecf1 100644 --- a/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts +++ b/.context/effect/packages/effect/src/unstable/sql/SqlResolver.ts @@ -29,7 +29,7 @@ import { ResultLengthMismatch } from "./SqlError.ts" * Request type used by SQL request resolvers, carrying the input payload * together with the resolver's result, error, and environment types. * - * @category requests + * @category models * @since 4.0.0 */ export interface SqlRequest extends Request.Request { @@ -53,7 +53,7 @@ const SqlRequestProto = { * Runs a payload as a `SqlRequest` through a request resolver, either directly * with a payload and resolver or curried by resolver. * - * @category requests + * @category running * @since 4.0.0 */ export const request: { @@ -76,7 +76,7 @@ export const request: { * Constructs a `SqlRequest` from a payload. Equality and hashing are based on * the payload so equal requests can be batched and deduplicated. * - * @category requests + * @category constructors * @since 4.0.0 */ export const SqlRequest = (payload: In): SqlRequest => { @@ -121,8 +121,9 @@ export const ordered = ({ key: transactionKey, resolver: Effect.fnUntraced(function*(entries) { - const inputs = yield* partitionRequests(entries, options.Request) - const results = yield* options.execute(inputs as any).pipe( + const [inputs, encodedEntries] = yield* partitionRequests(entries, options.Request) + if (!Arr.isArrayNonEmpty(inputs)) return + const results = yield* options.execute(inputs).pipe( Effect.provideContext(entries[0].context) ) if (results.length !== inputs.length) { @@ -131,8 +132,8 @@ export const ordered = ({ key: transactionKey, resolver: Effect.fnUntraced(function*(entries) { - const inputs = yield* partitionRequests(entries, options.Request) + const [inputs] = yield* partitionRequests(entries, options.Request) + if (!Arr.isArrayNonEmpty(inputs)) return const resultMap = MutableHashMap.empty>() - const results = yield* options.execute(inputs as any).pipe( + const results = yield* options.execute(inputs).pipe( Effect.provideContext(entries[0].context) ) const decodedResults = yield* decodeResults(results).pipe( @@ -246,7 +248,8 @@ export const findById = ( >({ key: transactionKey, resolver: Effect.fnUntraced(function*(entries) { - const inputs = yield* partitionRequests(entries, options.Request) - yield* options.execute(inputs as any).pipe( + const [inputs] = yield* partitionRequests(entries, options.Request) + if (!Arr.isArrayNonEmpty(inputs)) return + yield* options.execute(inputs).pipe( Effect.provideContext(entries[0].context) ) for (let i = 0; i < entries.length; i++) { @@ -326,6 +330,7 @@ const partitionRequests = function*( ) { const len = requests.length const inputs = Arr.empty() + const encodedEntries = Arr.empty>>() let entry!: Request.Entry> const encode = Schema.encodeEffect(schema) const handle = Effect.matchCauseEager({ @@ -334,6 +339,7 @@ const partitionRequests = function*( }, onSuccess(value: InE) { inputs.push(value) + encodedEntries.push(entry) } }) @@ -342,7 +348,7 @@ const partitionRequests = function*( yield (Effect.provideContext(handle(encode(entry.request.payload)), entry.context) as Effect.Effect) } - return inputs + return [inputs, encodedEntries] as const } const partitionRequestsById = function*( @@ -352,35 +358,39 @@ const partitionRequestsById = function*( const len = requests.length const inputs = Arr.empty() const byIdMap = MutableHashMap.empty>>() - let entry!: Request.Entry> - const encode = Schema.encodeEffect(schema) - const handle = Effect.matchCauseEager({ - onFailure(cause: Cause.Cause) { - entry.completeUnsafe(Exit.failCause(cause)) - }, - onSuccess(value: InE) { - inputs.push(value) - } - }) for (let i = 0; i < len; i++) { - entry = requests[i] + const entry = requests[i] const existing = MutableHashMap.get(byIdMap, entry.request.payload) if (Option.isSome(existing)) { - const duplicate = entry + const previous = existing.value MutableHashMap.set(byIdMap, entry.request.payload, { - ...existing.value, + ...previous, completeUnsafe(exit) { - existing.value.completeUnsafe(exit) - duplicate.completeUnsafe(exit) + previous.completeUnsafe(exit) + entry.completeUnsafe(exit) } }) } else { - yield (Effect.provideContext(handle(encode(entry.request.payload)), entry.context) as Effect.Effect) MutableHashMap.set(byIdMap, entry.request.payload, entry) } } + const encode = Schema.encodeEffect(schema) + for (const [, entry] of byIdMap) { + yield* Effect.provideContext( + Effect.matchCauseEager(encode(entry.request.payload), { + onFailure(cause) { + entry.completeUnsafe(Exit.failCause(cause)) + }, + onSuccess(value) { + inputs.push(value) + } + }), + entry.context + ) + } + return [inputs, byIdMap] as const } diff --git a/.context/effect/packages/effect/src/unstable/sql/Statement.ts b/.context/effect/packages/effect/src/unstable/sql/Statement.ts index 261034c6a..34f4b0026 100644 --- a/.context/effect/packages/effect/src/unstable/sql/Statement.ts +++ b/.context/effect/packages/effect/src/unstable/sql/Statement.ts @@ -17,6 +17,7 @@ import * as Effectable from "../../Effectable.ts" import type * as Fiber from "../../Fiber.ts" import { constUndefined } from "../../Function.ts" import * as internalEffect from "../../internal/effect.ts" +import * as InternalRecord from "../../internal/record.ts" import { hasProperty } from "../../Predicate.ts" import { TracerTimingEnabled } from "../../References.ts" import * as Stream from "../../Stream.ts" @@ -98,7 +99,7 @@ export type Transformer = ( * Context reference for an optional current SQL statement transformer applied * before statement execution. * - * @category transformer + * @category services * @since 4.0.0 */ export const CurrentTransformer = Context.Reference("effect/sql/CurrentTransformer", { @@ -737,7 +738,7 @@ const emptyFragment = fragment([literal("")]) * Dialect-specific compiler that converts a SQL `Fragment` into SQL text and * bind parameters, with a no-transform variant. * - * @category compiler + * @category models * @since 4.0.0 */ export interface Compiler { @@ -753,7 +754,7 @@ export interface Compiler { * Callbacks used by `makeCompiler` to render dialect placeholders, * identifiers, insert helpers, update helpers, and custom SQL segments. * - * @category compiler + * @category models * @since 4.0.0 */ export type CompilerOptions = any> = { @@ -788,7 +789,7 @@ export type CompilerOptions = any> = { /** * Creates a dialect-specific SQL `Compiler` from rendering callbacks. * - * @category compiler + * @category constructors * @since 4.0.0 */ export const makeCompiler = = any>( @@ -798,22 +799,25 @@ export const makeCompiler = = any>( self.options = options self.dialect = options.dialect self.disableTransforms = false + self.statementCache = new WeakMap() + self.statementCacheNoTransform = new WeakMap() return self } +type CompiledStatement = readonly [sql: string, binds: ReadonlyArray] + interface CompilerImpl extends Compiler { readonly options: CompilerOptions readonly disableTransforms: boolean + readonly statementCache: WeakMap + readonly statementCacheNoTransform: WeakMap compile( statement: Fragment, withoutTransform?: boolean, placeholderOverride?: (u: unknown) => string - ): readonly [sql: string, binds: ReadonlyArray] + ): CompiledStatement } -const statementCacheSymbol = Symbol.for("effect/unstable/sql/Statement/statementCache") -const statementCacheNoTransformSymbol = Symbol.for("effect/unstable/sql/Statement/statementCacheNoTransform") - const CompilerProto = { compile( this: CompilerImpl, @@ -823,9 +827,10 @@ const CompilerProto = { ): readonly [sql: string, binds: ReadonlyArray] { const opts = this.options withoutTransform = withoutTransform || this.disableTransforms - const cacheSymbol = withoutTransform ? statementCacheNoTransformSymbol : statementCacheSymbol - if (cacheSymbol in statement) { - return (statement as any)[cacheSymbol] + const cache = withoutTransform ? this.statementCacheNoTransform : this.statementCache + const cached = cache.get(statement) + if (cached !== undefined) { + return cached } const segments = statement.segments @@ -1032,7 +1037,8 @@ const CompilerProto = { if (placeholderOverride !== undefined) { return result } - return (statement as any)[cacheSymbol] = result + cache.set(statement, result) + return result }, get withoutTransform() { @@ -1048,7 +1054,7 @@ const CompilerProto = { * Creates a SQLite compiler that uses `?` placeholders and quoted identifiers, * optionally transforming identifier names before escaping. * - * @category compiler + * @category constructors * @since 4.0.0 */ export const makeCompilerSqlite = (transform?: ((_: string) => string) | undefined): Compiler => @@ -1091,7 +1097,7 @@ export function defaultEscape(c: string) { * Classifies a JavaScript value as a SQL primitive kind, treating `undefined` * as `null` and defaulting unrecognized objects to `string`. * - * @category predicates + * @category converting * @since 4.0.0 */ export const primitiveKind = (value: unknown): PrimitiveKind => { @@ -1146,8 +1152,8 @@ export const defaultTransforms = ( const transformObject = (obj: Record): any => { const newObj: Record = {} - for (const key in obj) { - newObj[transformer(key)] = transformValue(obj[key]) + for (const key of Object.keys(obj)) { + InternalRecord.assignProperty(newObj, transformer(key), transformValue(obj[key])) } return newObj } @@ -1162,8 +1168,8 @@ export const defaultTransforms = ( newRows[i] = transformArrayNested(row) as any } else { const obj: any = {} - for (const key in row) { - obj[transformer(key)] = transformValue(row[key]) + for (const [key, value] of Object.entries(row)) { + InternalRecord.assignProperty(obj, transformer(key), transformValue(value)) } newRows[i] = obj } @@ -1181,8 +1187,8 @@ export const defaultTransforms = ( newRows[i] = transformArray(row) as any } else { const obj: any = {} - for (const key in row) { - obj[transformer(key)] = row[key] + for (const [key, value] of Object.entries(row)) { + InternalRecord.assignProperty(obj, transformer(key), value) } newRows[i] = obj } @@ -1253,23 +1259,6 @@ const StatementProto: Omit< StatementImpl, "segments" | "acquirer" | "compiler" | "spanAttributes" | "transformRows" > = { - ...Effectable.Prototype>({ - label: "Statement", - evaluate(fiber) { - const span = internalEffect.makeSpanUnsafe(fiber, "sql.execute", { kind: "client" }) - const clock = fiber.getRef(Clock) - const timingEnabled = fiber.getRef(TracerTimingEnabled) - return Effect.onExit( - this.withConnectionSpan( - "execute", - (connection, sql, params) => connection.execute(sql, params, this.transformRows), - false, - span - ), - (exit) => internalEffect.endSpan(span, exit, clock, timingEnabled) - ) - } - }), [FragmentTypeId]: FragmentTypeId, withConnection( this: StatementImpl, @@ -1373,6 +1362,24 @@ const StatementProto: Omit< ) }, + ...Effectable.Prototype>({ + label: "Statement", + evaluate(fiber) { + const span = internalEffect.makeSpanUnsafe(fiber, "sql.execute", { kind: "client" }) + const clock = fiber.getRef(Clock) + const timingEnabled = fiber.getRef(TracerTimingEnabled) + return Effect.onExit( + this.withConnectionSpan( + "execute", + (connection, sql, params) => connection.execute(sql, params, this.transformRows), + false, + span + ), + (exit) => internalEffect.endSpan(span, exit, clock, timingEnabled) + ) + } + }), + compile( this: StatementImpl, withoutTransform?: boolean | undefined diff --git a/.context/effect/packages/effect/src/unstable/workers/Transferable.ts b/.context/effect/packages/effect/src/unstable/workers/Transferable.ts index 724db088f..804d27bfe 100644 --- a/.context/effect/packages/effect/src/unstable/workers/Transferable.ts +++ b/.context/effect/packages/effect/src/unstable/workers/Transferable.ts @@ -20,7 +20,7 @@ import * as SchemaGetter from "../../SchemaGetter.ts" * Service for collecting `Transferable` objects while encoding worker messages * so they can be passed to `postMessage` transfer lists. * - * @category models + * @category services * @since 4.0.0 */ export class Collector extends Context.Service { * platform ready/data messages and running the optional `onSpawn` effect when * the worker reports readiness. * - * @category models + * @category constructors * @since 4.0.0 */ export const makeUnsafe = (options: { @@ -165,6 +165,17 @@ export const makePlatform = () => const spawn = (yield* Spawner) as SpawnerFn let currentPort: P | undefined const buffer: Array<[unknown, ReadonlyArray | undefined]> = [] + const sendToPort = (port: P, message: unknown, transfers?: ReadonlyArray) => + Effect.try({ + try: () => port.postMessage([0, message], transfers as any), + catch: (cause) => + new WorkerError({ + reason: new WorkerSendError({ + message: "Failed to send message to worker", + cause + }) + }) + }) const run = ( handler: (_: O) => Effect.Effect, @@ -209,7 +220,7 @@ export const makePlatform = () => currentPort = port if (buffer.length > 0) { for (const [message, transfers] of buffer) { - port.postMessage([0, message], transfers as any) + yield* sendToPort(port, message, transfers) } buffer.length = 0 } @@ -224,19 +235,7 @@ export const makePlatform = () => buffer.push([message, transfers]) return Effect.void } - try { - currentPort.postMessage([0, message], transfers as any) - return Effect.void - } catch (cause) { - return Effect.fail( - new WorkerError({ - reason: new WorkerSendError({ - message: "Failed to send message to worker", - cause - }) - }) - ) - } + return sendToPort(currentPort, message, transfers) }) return { run, send } diff --git a/.context/effect/packages/effect/src/unstable/workers/WorkerError.ts b/.context/effect/packages/effect/src/unstable/workers/WorkerError.ts index 8c5d179d8..978d80f37 100644 --- a/.context/effect/packages/effect/src/unstable/workers/WorkerError.ts +++ b/.context/effect/packages/effect/src/unstable/workers/WorkerError.ts @@ -31,10 +31,10 @@ export const isWorkerError = (u: unknown): u is WorkerError => hasProperty(u, Ty /** * Worker error reason for failures while spawning or setting up a worker. * - * @category models + * @category errors * @since 4.0.0 */ -export class WorkerSpawnError extends Schema.ErrorClass( +export class WorkerSpawnError extends Schema.Error( "effect/workers/WorkerError/WorkerSpawnError" )({ _tag: Schema.tag("WorkerSpawnError"), @@ -45,10 +45,10 @@ export class WorkerSpawnError extends Schema.ErrorClass( /** * Worker error reason for failures while sending a message to a worker. * - * @category models + * @category errors * @since 4.0.0 */ -export class WorkerSendError extends Schema.ErrorClass( +export class WorkerSendError extends Schema.Error( "effect/workers/WorkerError/WorkerSendError" )({ _tag: Schema.tag("WorkerSendError"), @@ -60,10 +60,10 @@ export class WorkerSendError extends Schema.ErrorClass( * Worker error reason for failures while receiving or handling a message from a * worker. * - * @category models + * @category errors * @since 4.0.0 */ -export class WorkerReceiveError extends Schema.ErrorClass( +export class WorkerReceiveError extends Schema.Error( "effect/workers/WorkerError/WorkerReceiveError" )({ _tag: Schema.tag("WorkerReceiveError"), @@ -74,10 +74,10 @@ export class WorkerReceiveError extends Schema.ErrorClass( /** * Worker error reason for an unclassified worker failure. * - * @category models + * @category errors * @since 4.0.0 */ -export class WorkerUnknownError extends Schema.ErrorClass( +export class WorkerUnknownError extends Schema.Error( "effect/workers/WorkerError/WorkerUnknownError" )({ _tag: Schema.tag("WorkerUnknownError"), @@ -88,7 +88,7 @@ export class WorkerUnknownError extends Schema.ErrorClass( /** * Union of the specific failure reasons that can be wrapped by a `WorkerError`. * - * @category models + * @category errors * @since 4.0.0 */ export type WorkerErrorReason = @@ -100,7 +100,7 @@ export type WorkerErrorReason = /** * Schema for decoding and encoding all supported worker error reason variants. * - * @category models + * @category schemas * @since 4.0.0 */ export const WorkerErrorReason: Schema.Union<[ @@ -119,10 +119,10 @@ export const WorkerErrorReason: Schema.Union<[ * Error raised by worker APIs, wrapping a specific `WorkerErrorReason` and * exposing its message and cause. * - * @category models + * @category errors * @since 4.0.0 */ -export class WorkerError extends Schema.ErrorClass(TypeId)({ +export class WorkerError extends Schema.Error(TypeId)({ _tag: Schema.tag("WorkerError"), reason: WorkerErrorReason }) { diff --git a/.context/effect/packages/effect/src/unstable/workers/WorkerRunner.ts b/.context/effect/packages/effect/src/unstable/workers/WorkerRunner.ts index 7668d2581..f6f87652a 100644 --- a/.context/effect/packages/effect/src/unstable/workers/WorkerRunner.ts +++ b/.context/effect/packages/effect/src/unstable/workers/WorkerRunner.ts @@ -49,7 +49,7 @@ export type PlatformMessage = readonly [request: 0, I] | readonly [close: 1] /** * Context service that starts a platform-specific `WorkerRunner`. * - * @category models + * @category services * @since 4.0.0 */ export class WorkerRunnerPlatform extends Context.Service( @@ -240,7 +240,7 @@ export const CurrentAttempt = Context.Reference( * Computes a deterministic activity idempotency key from the current workflow * execution ID, the supplied name, and optionally the current attempt. * - * @category Idempotency + * @category idempotency * @since 4.0.0 */ export const idempotencyKey: ( diff --git a/.context/effect/packages/effect/src/unstable/workflow/DurableClock.ts b/.context/effect/packages/effect/src/unstable/workflow/DurableClock.ts index b35fb47f4..7029c863c 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/DurableClock.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/DurableClock.ts @@ -64,7 +64,7 @@ const InstanceTag = Context.Service< * Waits inside a workflow, using an in-memory activity for durations at or * below the threshold and scheduling a durable clock for longer durations. * - * @category sleeping + * @category delays & timeouts * @since 4.0.0 */ export const sleep: ( diff --git a/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts b/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts index 73c6d8b82..b6fad5e74 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/DurableDeferred.ts @@ -144,6 +144,8 @@ const await_: (self: DurableDeferred) { const engine = yield* EngineTag const instance = yield* InstanceTag + // Register before the read so any later completion can preempt the run. + instance.awaitedDeferreds.add(self.name) const exit = yield* Workflow.wrapActivityResult( engine.deferredResult(self), Option.isNone @@ -225,9 +227,12 @@ export const into: { exit.cause.reasons, Filter.fromPredicate(Cause.isInterruptReason) ) - const hasInterruptsOnly = interrupts.length === exit.cause.reasons.length - if (hasInterruptsOnly && instance.suspended) { - parentInstance.suspended = true + if (interrupts.length === exit.cause.reasons.length) { + // An interrupt-only exit is never a result: the effect was + // suspended, preempted or interrupted, so record nothing. + if (instance.suspended) { + parentInstance.suspended = true + } return } else if (interrupts.length > 0) { exit = Exit.failCause(Cause.fromReasons(reasons)) @@ -309,7 +314,7 @@ export type TokenTypeId = typeof TokenTypeId * Branded string token identifying a durable deferred for a workflow * execution. * - * @category token + * @category models * @since 4.0.0 */ export type Token = Brand.Branded @@ -317,7 +322,7 @@ export type Token = Brand.Branded /** * Schema for branded durable deferred tokens. * - * @category token + * @category schemas * @since 4.0.0 */ export const Token: Schema.brand = Schema.String.pipe(Schema.brand(TokenTypeId)) @@ -326,7 +331,7 @@ export const Token: Schema.brand = Schema.String.pip * Schema for a decoded durable deferred token containing the workflow * name, execution ID, and deferred name. * - * @category token + * @category schemas * @since 4.0.0 */ export class TokenParsed extends Schema.Class( @@ -401,7 +406,7 @@ export class TokenParsed extends Schema.Class( * Creates a token for a durable deferred using the current workflow instance's * workflow name and execution ID. * - * @category token + * @category constructors * @since 4.0.0 */ export const token: ( @@ -419,7 +424,7 @@ export const token: = [] + * const processApiCall = ({ id }: { readonly id: string }) => Effect.sync(() => processed.push(id)) + * + * // Construct the worker layer without starting background workers in this example. + * const ApiWorker = DurableQueue.worker(ApiQueue, processApiCall, { + * concurrency: 5 + * }) + * + * const program = Effect.gen(function*() { + * // Exercise the finite handler directly instead of running a queue worker. + * yield* processApiCall({ id: "api-call-1" }) + * return [Layer.isLayer(MyWorkflowLayer), Layer.isLayer(ApiWorker), processed] as const + * }) + * + * await Effect.runPromise(program) // => [true, true, ["api-call-1"]] * ``` * * @category constructors @@ -170,7 +172,7 @@ const getQueueSchema = ( /** * Adds an item to the queue and wait for a worker to process it. * - * @category Processing + * @category running * @since 4.0.0 */ export const process: < @@ -247,7 +249,7 @@ const defaultRetrySchedule = Schedule.min([ /** * Create a worker effect that processes items from the durable queue. * - * @category Worker + * @category workers * @since 4.0.0 */ export const makeWorker: < @@ -334,7 +336,7 @@ export const makeWorker: < /** * Create a layer that runs workers for the durable queue. * - * @category Worker + * @category workers * @since 4.0.0 */ export const worker: < diff --git a/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts b/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts index eea4a1f01..af541bc40 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/Workflow.ts @@ -20,7 +20,7 @@ import * as Fiber from "../../Fiber.ts" import * as Filter from "../../Filter.ts" import { constFalse, constTrue, dual, identity } from "../../Function.ts" import * as Layer from "../../Layer.ts" -import * as Option from "../../Option.ts" +import type * as Option from "../../Option.ts" import * as Predicate from "../../Predicate.ts" import type * as Schedule from "../../Schedule.ts" import * as Schema from "../../Schema.ts" @@ -200,7 +200,7 @@ export interface AnyStructSchema extends Schema.Top { * Type-level marker for services associated with a specific workflow * execution tag. * - * @category models + * @category utility types * @since 4.0.0 */ export interface Execution { @@ -252,7 +252,7 @@ export interface AnyWithProps extends Any { /** * Extracts the payload schema from a `Workflow`. * - * @category models + * @category utility types * @since 4.0.0 */ export type PayloadSchema = W extends Workflow< @@ -267,7 +267,7 @@ export type PayloadSchema = W extends Workflow< * Computes the schema services required by clients that execute or poll * workflows. * - * @category models + * @category utility types * @since 4.0.0 */ export type RequirementsClient = Workflows extends Workflow< @@ -285,7 +285,7 @@ export type RequirementsClient = Workflows extends Workfl * Computes the schema services required by handlers that decode workflow * payloads and encode workflow results. * - * @category models + * @category utility types * @since 4.0.0 */ export type RequirementsHandler = Workflows extends Workflow< @@ -465,7 +465,7 @@ const ResultTypeId = "~effect/workflow/Workflow/Result" /** * Returns `true` when a value is a workflow `Result`. * - * @category results + * @category guards * @since 4.0.0 */ export const isResult = ( @@ -476,7 +476,7 @@ export const isResult = ( * Result of a workflow execution, either a completed exit or a suspended * workflow state. * - * @category results + * @category models * @since 4.0.0 */ export type Result = Complete | Suspended @@ -484,7 +484,7 @@ export type Result = Complete | Suspended /** * Encoded representation of a workflow `Result`. * - * @category results + * @category models * @since 4.0.0 */ export type ResultEncoded = @@ -495,7 +495,7 @@ export type ResultEncoded = * Encoded representation of a completed workflow result containing an encoded * `Exit`. * - * @category results + * @category models * @since 4.0.0 */ export interface CompleteEncoded { @@ -527,7 +527,7 @@ export interface CompleteSchema< /** * Represents a completed workflow execution with its success or failure `Exit`. * - * @category results + * @category models * @since 4.0.0 */ export class Complete extends Data.TaggedClass("Complete")<{ @@ -557,16 +557,13 @@ export class Complete extends Data.TaggedClass("Complete")<{ [Schema.Exit(options.success, options.error, Schema.Defect())], ([exit]) => (input, ast, options) => { if (!(isResult(input) && input._tag === "Complete")) { - return Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))) + return Effect.fail(new SchemaIssue.InvalidType(ast, input, options)) } return Effect.mapBothEager( SchemaParser.decodeEffect(exit)(input.exit, options), { onSuccess: (exit) => new Complete({ exit }), - onFailure: (issue) => - new SchemaIssue.Composite(ast, Option.some(input), [ - new SchemaIssue.Pointer(["exit"], issue) - ]) + onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "exit", issue, input, options) } ) }, @@ -599,7 +596,7 @@ export class Complete extends Data.TaggedClass("Complete")<{ * Represents a suspended workflow execution, optionally carrying the cause that * triggered suspension. * - * @category results + * @category schemas * @since 4.0.0 */ export class Suspended extends Schema.Class( @@ -620,7 +617,7 @@ export class Suspended extends Schema.Class( * Creates a schema for workflow results using the supplied success and error * schemas. * - * @category results + * @category schemas * @since 4.0.0 */ export const Result = < @@ -636,7 +633,7 @@ const AnyOrVoid = Schema.Union([Schema.Any, Schema.Void]) /** * Schema for encoded workflow results with generic success and error payloads. * - * @category results + * @category schemas * @since 4.0.0 */ export const ResultEncoded: Schema.Codec> = Schema.toEncoded( @@ -653,7 +650,7 @@ export const ResultEncoded: Schema.Codec> = Schema.toEnc * `Result`, handling suspension, defect capture, interruption, and workflow * scope finalization. * - * @category results + * @category converting * @since 4.0.0 */ export const intoResult = ( @@ -716,7 +713,7 @@ export const intoResult = ( * Wraps an activity-like effect so workflow suspension waits for currently * running activities to finish or suspend. * - * @category results + * @category resource management * @since 4.0.0 */ export const wrapActivityResult = ( @@ -825,7 +822,7 @@ export const addFinalizer: ( * * Compensation finalizers are only registered for top-level effects in the workflow and do not work for nested activities. * - * @category Compensation + * @category compensation * @since 4.0.0 */ export const withCompensation: { @@ -853,7 +850,7 @@ export const withCompensation: { * Marks a workflow instance as suspended and interrupts the current fiber to * stop execution until it is resumed. * - * @category results + * @category interruption * @since 4.0.0 */ export const suspend = (instance: WorkflowInstance["Service"]): Effect.Effect => @@ -870,7 +867,7 @@ export const suspend = (instance: WorkflowInstance["Service"]): Effect.Effect( @@ -887,7 +884,7 @@ export const CaptureDefects = Context.Reference( * * The suspended execution can later be resumed with the workflow's `resume` method, for example `MyWorkflow.resume(executionId)`. * - * @category annotations + * @category services * @since 4.0.0 */ export const SuspendOnFailure = Context.Reference( diff --git a/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts b/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts index 7a6a5a3a0..ea13e88fe 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/WorkflowEngine.ts @@ -9,7 +9,7 @@ * * @since 4.0.0 */ -import type * as Cause from "../../Cause.ts" +import * as Cause from "../../Cause.ts" import * as Context from "../../Context.ts" import * as Effect from "../../Effect.ts" import * as Exit from "../../Exit.ts" @@ -261,6 +261,9 @@ export class WorkflowInstance extends Context.Service< */ cause: Cause.Cause | undefined + /** Deferred names this run parked on; their completions preempt the run. */ + readonly awaitedDeferreds: Set + readonly activityState: { count: number readonly latch: Latch.Latch @@ -269,15 +272,17 @@ export class WorkflowInstance extends Context.Service< >()("effect/workflow/WorkflowEngine/WorkflowInstance") { static initial( workflow: Workflow.Any, - executionId: string + executionId: string, + scope = Scope.makeUnsafe() ): WorkflowInstance["Service"] { return WorkflowInstance.of({ executionId, workflow, - scope: Scope.makeUnsafe(), + scope, suspended: false, interrupted: false, cause: undefined, + awaitedDeferreds: new Set(), activityState: { count: 0, latch: Latch.makeUnsafe() @@ -286,11 +291,93 @@ export class WorkflowInstance extends Context.Service< } } +/** + * In-process deferred state for live workflow executions. + * + * @category models + * @since 4.0.0 + */ +export interface DeferredState { + /** Returns a completion not yet durably readable. */ + readonly pendingResult: ( + executionId: string, + name: string + ) => Exit.Exit | undefined + + /** Tracks and provides a run, retaining pending results across suspension. */ + readonly trackRun: ( + instance: WorkflowInstance["Service"], + effect: Effect.Effect + ) => Effect.Effect> + + /** Records a completion, preempting a run parked on that deferred. */ + readonly deferredDone: ( + executionId: string, + name: string, + exit: Exit.Exit + ) => Effect.Effect +} + +/** + * Creates deferred state shared by workflow engines. + * + * @category constructors + * @since 4.0.0 + */ +export const makeDeferredState = (): DeferredState => { + const pending = new Map>>() + const running = new Map + }>() + return { + pendingResult: (executionId, name) => pending.get(executionId)?.get(name), + trackRun: (instance, effect) => + Effect.withFiber((fiber) => { + const run = { instance, fiber: fiber as Fiber.Fiber } + running.set(instance.executionId, run) + return Effect.ensuring( + Effect.provideService(effect, WorkflowInstance, instance), + Effect.sync(() => { + if (!instance.suspended) { + pending.delete(instance.executionId) + } + if (running.get(instance.executionId) === run) { + running.delete(instance.executionId) + } + }) + ) + }), + deferredDone: (executionId, name, exit) => + Effect.withFiber((current) => { + const run = running.get(executionId) + if (!run) return Effect.void + let entries = pending.get(executionId) + if (!entries) { + entries = new Map() + pending.set(executionId, entries) + } + entries.set(name, exit) + if ( + run.fiber === current || + run.fiber.pollUnsafe() || + !run.instance.awaitedDeferreds.has(name) + ) { + return Effect.void + } + // Suspended retains the pending result; the engine re-runs the + // interrupted run and the replay observes the completion. + run.instance.suspended = true + return Fiber.interrupt(run.fiber) + }) + } +} + /** * Low-level workflow engine contract that works with encoded payloads and * results before `makeUnsafe` adds typed schema decoding and encoding. * - * @category Encoded + * @category services * @since 4.0.0 */ export interface Encoded { @@ -603,6 +690,8 @@ export const layerMemory: Layer.Layer = Layer.effect(WorkflowEng } const activities = new Map() + const deferredState = makeDeferredState() + const resume = Effect.fnUntraced(function*(executionId: string): Effect.fn.Return { const state = executions.get(executionId) if (!state) return @@ -614,7 +703,11 @@ export const layerMemory: Layer.Layer = Layer.effect(WorkflowEng } const entry = workflows.get(state.instance.workflow._tag)! - const instance = WorkflowInstance.initial(state.instance.workflow, state.instance.executionId) + const instance = WorkflowInstance.initial( + state.instance.workflow, + state.instance.executionId, + state.instance.scope + ) instance.interrupted = state.instance.interrupted state.instance = instance state.fiber = yield* state.execute(state.payload, state.instance.executionId).pipe( @@ -626,8 +719,8 @@ export const layerMemory: Layer.Layer = Layer.effect(WorkflowEng return Effect.withFiber((fiber) => Effect.interruptible(Fiber.interrupt(fiber))) }), Workflow.intoResult, - Effect.provideService(WorkflowInstance, instance), Effect.provideService(WorkflowEngine, engine), + (effect) => deferredState.trackRun(instance, effect), Effect.tap((result) => { if (!state.parent || result._tag !== "Complete") { return Effect.void @@ -669,7 +762,14 @@ export const layerMemory: Layer.Layer = Layer.effect(WorkflowEng yield* resume(options.executionId) } if (options.discard) return - return (yield* Fiber.join(state.fiber!)) as any + // Capture together so a wake that swaps in a replay cannot desync them. + const instance = state.instance + const exit = yield* Fiber.await(state.fiber!) + if (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) && instance.suspended) { + // A completion preempted the run; the caller retries into the replay. + return new Workflow.Suspended({}) as any + } + return (yield* exit) as any }), interrupt: Effect.fnUntraced(function*(_workflow, executionId) { const state = executions.get(executionId) @@ -738,7 +838,10 @@ export const layerMemory: Layer.Layer = Layer.effect(WorkflowEng const id = `${options.executionId}/${options.deferredName}` if (deferredResults.has(id)) return Effect.void deferredResults.set(id, options.exit) - return resume(options.executionId) + return Effect.andThen( + deferredState.deferredDone(options.executionId, options.deferredName, options.exit), + resume(options.executionId) + ) }), scheduleClock: (workflow, options) => engine.deferredDone(options.clock.deferred, { diff --git a/.context/effect/packages/effect/src/unstable/workflow/WorkflowProxy.ts b/.context/effect/packages/effect/src/unstable/workflow/WorkflowProxy.ts index 6f1708525..a1104b6d4 100644 --- a/.context/effect/packages/effect/src/unstable/workflow/WorkflowProxy.ts +++ b/.context/effect/packages/effect/src/unstable/workflow/WorkflowProxy.ts @@ -22,7 +22,7 @@ import type * as Workflow from "./Workflow.ts" * * **Example** (Deriving RPC endpoints from workflows) * - * ```ts + * ```ts import.meta.vitest * import { Layer, Schema } from "effect" * import { RpcServer } from "effect/unstable/rpc" * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow" @@ -46,6 +46,7 @@ import type * as Workflow from "./Workflow.ts" * const ApiLayer = RpcServer.layer(MyRpcs).pipe( * Layer.provide(WorkflowProxyServer.layerRpcHandlers(myWorkflows)) * ) + * const result = [MyRpcs.requests.size, Layer.isLayer(ApiLayer)] // => [3, true] * ``` * * @category constructors @@ -103,7 +104,7 @@ export type ConvertRpcs = * * **Example** (Deriving HTTP API endpoints from workflows) * - * ```ts + * ```ts import.meta.vitest * import { Layer, Schema } from "effect" * import { HttpApi, HttpApiBuilder } from "effect/unstable/httpapi" * import { Workflow, WorkflowProxy, WorkflowProxyServer } from "effect/unstable/workflow" @@ -131,6 +132,7 @@ export type ConvertRpcs = * WorkflowProxyServer.layerHttpApi(MyApi, "workflows", myWorkflows) * ) * ) + * const result = [Object.keys(MyApi.groups.workflows.endpoints).length, Layer.isLayer(ApiLayer)] // => [3, true] * ``` * * @category constructors diff --git a/.context/effect/packages/effect/test/Array.test.ts b/.context/effect/packages/effect/test/Array.test.ts index 8b1329c1d..5d7265b1b 100644 --- a/.context/effect/packages/effect/test/Array.test.ts +++ b/.context/effect/packages/effect/test/Array.test.ts @@ -1,6 +1,17 @@ import { describe, it } from "@effect/vitest" import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" -import { Array as Arr, Equivalence, Number as Num, Option, Order, type Predicate, Result, String as Str } from "effect" +import { + Array as Arr, + Equal, + Equivalence, + Hash, + Number as Num, + Option, + Order, + type Predicate, + Result, + String as Str +} from "effect" import { identity, pipe } from "effect/Function" import { FastCheck as fc } from "effect/testing" @@ -10,6 +21,16 @@ const symC = Symbol.for("c") const double = (n: number) => n * 2 +class HashCollision implements Equal.Equal { + constructor(readonly value: number) {} + [Hash.symbol](): number { + return 0 + } + [Equal.symbol](that: Equal.Equal): boolean { + return that instanceof HashCollision && this.value === that.value + } +} + describe("Array", () => { it("of", () => { deepStrictEqual(Arr.of(1), [1]) @@ -1414,6 +1435,26 @@ describe("Array", () => { deepStrictEqual(Arr.dedupe([1, 2, 3]), [1, 2, 3]) deepStrictEqual(Arr.dedupe([1, 1, 1]), [1]) deepStrictEqual(Arr.dedupe(["a", "b", "a"]), ["a", "b"]) + deepStrictEqual(Arr.dedupe([NaN, NaN]), [NaN]) + deepStrictEqual(Arr.dedupe([0, -0]), [0]) + deepStrictEqual( + Arr.dedupe([new HashCollision(1), new HashCollision(2), new HashCollision(1)]).map((value) => value.value), + [1, 2] + ) + const sparse = globalThis.Array(2) + sparse[1] = 1 + deepStrictEqual(Arr.dedupe(sparse), [undefined, 1]) + deepStrictEqual(Arr.dedupe(globalThis.Array(1)), [undefined]) + }) + + it("dedupe does not hash a single value", () => { + let value: unknown = null + for (let index = 0; index < 25_000; index++) { + value = { value } + } + const result = Arr.dedupe([value]) + strictEqual(result.length, 1) + strictEqual(result[0], value) }) it("dedupeAdjacent", () => { @@ -1437,6 +1478,13 @@ describe("Array", () => { deepStrictEqual(Arr.union([], []), []) deepStrictEqual(Arr.union([1, 2], [1, 2]), [1, 2]) deepStrictEqual(pipe([1, 2], Arr.union([3, 4])), [1, 2, 3, 4]) + deepStrictEqual( + Arr.union( + [new HashCollision(1), new HashCollision(2)], + [new HashCollision(1), new HashCollision(3)] + ).map((value) => value.value), + [1, 2, 3] + ) }) it("intersection", () => { @@ -1446,6 +1494,14 @@ describe("Array", () => { deepStrictEqual(Arr.intersection([], [1, 2]), []) deepStrictEqual(Arr.intersection([1, 2], []), []) deepStrictEqual(pipe([1, 2, 3], Arr.intersection([2, 3, 4])), [2, 3]) + deepStrictEqual(Arr.intersection([1, 1, 2], [1]), [1, 1]) + deepStrictEqual( + Arr.intersection( + [new HashCollision(1), new HashCollision(2)], + [new HashCollision(1)] + ).map((value) => value.value), + [1] + ) }) it("difference", () => { @@ -1454,6 +1510,16 @@ describe("Array", () => { deepStrictEqual(Arr.difference([1, 2], []), [1, 2]) deepStrictEqual(Arr.difference([], [1, 2]), []) deepStrictEqual(pipe([1, 2, 3], Arr.difference([3])), [1, 2]) + const sparse = globalThis.Array(2) + sparse[1] = 1 + deepStrictEqual(Arr.difference(sparse, []), [1]) + deepStrictEqual( + Arr.difference( + [new HashCollision(1), new HashCollision(2)], + [new HashCollision(1)] + ).map((value) => value.value), + [2] + ) }) it("cartesianWith", () => { diff --git a/.context/effect/packages/effect/test/ArrayNaNIndex.test.ts b/.context/effect/packages/effect/test/ArrayNaNIndex.test.ts new file mode 100644 index 000000000..9c5f258c3 --- /dev/null +++ b/.context/effect/packages/effect/test/ArrayNaNIndex.test.ts @@ -0,0 +1,40 @@ +import { assert, describe, it } from "@effect/vitest" +import { Array as Arr, Option } from "effect" + +const input = [1, 2, 3] + +describe("Array NaN indexes", () => { + it("rejects NaN in get", () => { + assert.deepStrictEqual(Arr.get(input, Number.NaN), Option.none()) + }) + + it("rejects NaN in getUnsafe", () => { + assert.throws(() => Arr.getUnsafe(input, Number.NaN), /Index out of bounds/) + }) + + it("rejects NaN in insertAt", () => { + assert.deepStrictEqual(Arr.insertAt(input, Number.NaN, 4), Option.none()) + }) + + it("rejects NaN in replace", () => { + assert.deepStrictEqual(Arr.replace(input, Number.NaN, 4), Option.none()) + }) + + it("rejects NaN in modify", () => { + assert.deepStrictEqual(Arr.modify(input, Number.NaN, (n) => n * 2), Option.none()) + }) + + it("treats removing NaN as an out-of-bounds no-op", () => { + assert.deepStrictEqual(Arr.remove(input, Number.NaN), input) + }) +}) + +describe("Array fractional indexes", () => { + it("floors the index in replace", () => { + assert.deepStrictEqual(Arr.replace(input, 1.5, 4), Option.some([1, 4, 3])) + }) + + it("floors the index in modify", () => { + assert.deepStrictEqual(Arr.modify(input, 1.5, (n) => n * 2), Option.some([1, 4, 3])) + }) +}) diff --git a/.context/effect/packages/effect/test/BigInt.test.ts b/.context/effect/packages/effect/test/BigInt.test.ts index 28f7e6b28..64e83168a 100644 --- a/.context/effect/packages/effect/test/BigInt.test.ts +++ b/.context/effect/packages/effect/test/BigInt.test.ts @@ -1,17 +1,17 @@ +import { assert, describe, it } from "@effect/vitest" import * as BigInt from "effect/BigInt" -import { describe, it } from "vitest" -import { assertNone, assertSome, strictEqual } from "./utils/assert.ts" +import { assertNone, assertSome } from "./utils/assert.ts" describe("BigInt", () => { it("re-exports the global BigInt constructor", () => { - strictEqual(BigInt.Equivalence(1n, 1n), true) - strictEqual(BigInt.Equivalence(1n, 2n), false) + assert.strictEqual(BigInt.Equivalence(1n, 1n), true) + assert.strictEqual(BigInt.Equivalence(1n, 2n), false) }) it("divide returns some for non-zero divisors in data-first and data-last forms", () => { assertSome(BigInt.divide(6n, 3n), 2n) assertNone(BigInt.divide(6n, 0n)) - strictEqual(BigInt.divideUnsafe(6n, 3n), 2n) + assert.strictEqual(BigInt.divideUnsafe(6n, 3n), 2n) }) it("sqrt returns integer square roots", () => { @@ -36,22 +36,34 @@ describe("BigInt", () => { }) it("fromNumber returns none for unsafe or non-integral numbers", () => { - strictEqual(BigInt.ReducerSum.combine(1n, 2n), 3n) - strictEqual(BigInt.ReducerSum.combine(BigInt.ReducerSum.initialValue, 2n), 2n) - strictEqual(BigInt.ReducerSum.combine(2n, BigInt.ReducerSum.initialValue), 2n) + assert.strictEqual(BigInt.ReducerSum.combine(1n, 2n), 3n) + assert.strictEqual(BigInt.ReducerSum.combine(BigInt.ReducerSum.initialValue, 2n), 2n) + assert.strictEqual(BigInt.ReducerSum.combine(2n, BigInt.ReducerSum.initialValue), 2n) }) it("ReducerMultiply combines values with one as the identity", () => { - strictEqual(BigInt.ReducerMultiply.combine(2n, 3n), 6n) - strictEqual(BigInt.ReducerMultiply.combine(BigInt.ReducerMultiply.initialValue, 2n), 2n) - strictEqual(BigInt.ReducerMultiply.combine(2n, BigInt.ReducerMultiply.initialValue), 2n) + assert.strictEqual(BigInt.ReducerMultiply.combine(2n, 3n), 6n) + assert.strictEqual(BigInt.ReducerMultiply.combine(BigInt.ReducerMultiply.initialValue, 2n), 2n) + assert.strictEqual(BigInt.ReducerMultiply.combine(2n, BigInt.ReducerMultiply.initialValue), 2n) }) it("CombinerMax returns the larger bigint", () => { - strictEqual(BigInt.CombinerMax.combine(1n, 2n), 2n) + assert.strictEqual(BigInt.CombinerMax.combine(1n, 2n), 2n) }) it("CombinerMin returns the smaller bigint", () => { - strictEqual(BigInt.CombinerMin.combine(1n, 2n), 1n) + assert.strictEqual(BigInt.CombinerMin.combine(1n, 2n), 1n) + }) + + it("returns a non-negative greatest common divisor", () => { + assert.strictEqual(BigInt.gcd(-6n, 4n), 2n) + }) + + it("returns a non-negative least common multiple", () => { + assert.strictEqual(BigInt.lcm(6n, -4n), 12n) + }) + + it("returns zero for two zero operands", () => { + assert.strictEqual(BigInt.lcm(0n, 0n), 0n) }) }) diff --git a/.context/effect/packages/effect/test/Brand.test.ts b/.context/effect/packages/effect/test/Brand.test.ts index 811b34c9f..8d2c151de 100644 --- a/.context/effect/packages/effect/test/Brand.test.ts +++ b/.context/effect/packages/effect/test/Brand.test.ts @@ -29,7 +29,7 @@ describe("Brand", () => { const Int = Brand.check(Schema.isInt()) const result = Int.result(1.1) assertTrue(Result.isFailure(result)) - strictEqual(String(result.failure), "BrandError(Expected an integer, got 1.1)") + strictEqual(String(result.failure), "BrandError(Expected an integer)") }) it("creates nominal brands without runtime validation", () => { @@ -94,7 +94,7 @@ describe("Brand", () => { assertSome(Int.option(-1), -1 as Int) assertSuccess(Int, 1) - assertFailure(Int, 1.1, "Expected an integer, got 1.1") + assertFailure(Int, 1.1, "Expected an integer") assertSuccess(Int, -1) }) @@ -103,13 +103,13 @@ describe("Brand", () => { const PositiveInt = Brand.check(Schema.isInt(), Schema.isGreaterThan(0)) assertSuccess(PositiveInt, 1) - assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1") - assertFailure(PositiveInt, -1, "Expected a value greater than 0, got -1") + assertFailure(PositiveInt, 1.1, "Expected an integer") + assertFailure(PositiveInt, -1, "Expected a value greater than 0") assertFailure( PositiveInt, -1.1, - `Expected an integer, got -1.1 -Expected a value greater than 0, got -1.1` + `Expected an integer +Expected a value greater than 0` ) }) @@ -121,9 +121,9 @@ Expected a value greater than 0, got -1.1` ) assertSuccess(PositiveInt, 1) - assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1") - assertFailure(PositiveInt, -1, "Expected a value greater than 0, got -1") - assertFailure(PositiveInt, -1.1, `Expected an integer, got -1.1`) + assertFailure(PositiveInt, 1.1, "Expected an integer") + assertFailure(PositiveInt, -1, "Expected a value greater than 0") + assertFailure(PositiveInt, -1.1, `Expected an integer`) }) }) @@ -149,12 +149,12 @@ Expected a value greater than 0, got -1.1` assertNone(PositiveInt.option(-1)) assertSuccess(PositiveInt, 1) - assertFailure(PositiveInt, 1.1, "Expected an integer, got 1.1") + assertFailure(PositiveInt, 1.1, "Expected an integer") assertFailure( PositiveInt, -1.1, - `Expected an integer, got -1.1 -Expected a value greater than 0, got -1.1` + `Expected an integer +Expected a value greater than 0` ) }) }) diff --git a/.context/effect/packages/effect/test/Cache.test.ts b/.context/effect/packages/effect/test/Cache.test.ts index 512cdddc3..376308b96 100644 --- a/.context/effect/packages/effect/test/Cache.test.ts +++ b/.context/effect/packages/effect/test/Cache.test.ts @@ -43,6 +43,19 @@ describe("Cache", () => { assert.isFalse(yield* Cache.has(cache, "test")) })) + it.effect("make - uses a numeric zero TTL", () => + Effect.gen(function*() { + const cache = yield* Cache.make({ + capacity: 10, + lookup: (key) => Effect.succeed(key.length), + timeToLive: 0 + }) + + yield* Cache.get(cache, "test") + + assert.isFalse(yield* Cache.has(cache, "test")) + })) + it.effect("make - lookup function context is preserved", () => Effect.gen(function*() { class TestService extends Context.Service()("TestService") {} @@ -336,6 +349,28 @@ describe("Cache", () => { yield* lookupInterrupted.await assert.strictEqual(yield* Cache.get(cache, "K1"), 42) })) + + it.effect("concurrent access - interrupted lookup does not remove a newer set value", () => + Effect.gen(function*() { + const lookupStarted = yield* Latch.make() + const lookupInterrupted = yield* Latch.make() + const cache = yield* Cache.make({ + capacity: 1, + lookup: () => + Effect.onInterrupt( + lookupStarted.open.pipe(Effect.andThen(Effect.never)), + () => lookupInterrupted.open + ) + }) + + const getter = yield* Cache.get(cache, "key").pipe(Effect.forkChild({ startImmediately: true })) + yield* lookupStarted.await + yield* Cache.set(cache, "key", 99) + yield* Fiber.interrupt(getter) + yield* lookupInterrupted.await + + assert.deepStrictEqual(yield* Cache.getSuccess(cache, "key"), Option.some(99)) + })) }) describe("getOption", () => { diff --git a/.context/effect/packages/effect/test/CauseMapAnnotations.test.ts b/.context/effect/packages/effect/test/CauseMapAnnotations.test.ts new file mode 100644 index 000000000..fd8f72a40 --- /dev/null +++ b/.context/effect/packages/effect/test/CauseMapAnnotations.test.ts @@ -0,0 +1,15 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Context } from "effect" + +describe("Cause.map", () => { + it("preserves annotations on mapped failures", () => { + class RequestId extends Context.Service()("RequestId") {} + + const cause = Cause.fail("error").pipe( + Cause.annotate(Context.make(RequestId, "request-1")), + Cause.map((error) => error.toUpperCase()) + ) + + assert.strictEqual(Context.getOrUndefined(Cause.annotations(cause), RequestId), "request-1") + }) +}) diff --git a/.context/effect/packages/effect/test/Channel.test.ts b/.context/effect/packages/effect/test/Channel.test.ts index 1516c3274..2d160f810 100644 --- a/.context/effect/packages/effect/test/Channel.test.ts +++ b/.context/effect/packages/effect/test/Channel.test.ts @@ -119,6 +119,44 @@ describe("Channel", () => { assert.deepStrictEqual(resultChunked, [[1, 2, 3, 4], [5]]) })) + it.effect("fromReadableStream", () => + Effect.gen(function*() { + const result = yield* Channel.fromReadableStream({ + evaluate: () => + new ReadableStream({ + start(controller) { + controller.enqueue(1) + controller.enqueue(2) + controller.close() + } + }), + onError: (error) => error + }).pipe(Channel.runCollect) + + assert.deepStrictEqual(result, [[1], [2]]) + })) + + it.effect("fromTransformStream - surfaces write-side errors through the read side", () => + Effect.gen(function*() { + const error = new Error("write failed") + const channel = Channel.fromTransformStream({ + evaluate: () => + new TransformStream({ + transform() { + throw error + } + }), + onError: (cause) => cause as Error + }) + const exit = yield* Channel.fromArray([[1] as [number]]).pipe( + Channel.pipeTo(channel), + Channel.runDrain, + Effect.exit + ) + + assertExitFailure(exit, Cause.fail(error)) + })) + it.effect("acquireRelease", () => Effect.gen(function*() { const acquired = yield* Ref.make(false) @@ -132,6 +170,20 @@ describe("Channel", () => { })) }) + describe("destructors", () => { + it.effect("mkUint8Array", () => + Effect.gen(function*() { + const bytes = yield* Channel.fromArray( + [ + [new Uint8Array([1, 2])], + [new Uint8Array([3]), new Uint8Array([4, 5])] + ] as const + ).pipe(Channel.mkUint8Array) + + assert.deepStrictEqual(bytes, new Uint8Array([1, 2, 3, 4, 5])) + })) + }) + describe("mapping", () => { it.effect("map", () => Effect.gen(function*() { diff --git a/.context/effect/packages/effect/test/Chunk.test.ts b/.context/effect/packages/effect/test/Chunk.test.ts index cc4aaa6a5..8eb4de760 100644 --- a/.context/effect/packages/effect/test/Chunk.test.ts +++ b/.context/effect/packages/effect/test/Chunk.test.ts @@ -388,6 +388,12 @@ describe("Chunk", () => { }) describe("take", () => { + it("produces valid chunks for fractional counts", () => { + const chunk = Chunk.make(1, 2, 3) + deepStrictEqual(Chunk.toArray(Chunk.take(chunk, 1.5)), [1]) + deepStrictEqual(Chunk.toArray(Chunk.drop(chunk, 1.5)), [2, 3]) + }) + describe("Given a Chunk with more elements than the amount taken", () => { it("should return the subset", () => { assertEquals(pipe(Chunk.fromArrayUnsafe([1, 2, 3]), Chunk.take(2)), Chunk.fromArrayUnsafe([1, 2])) diff --git a/.context/effect/packages/effect/test/Clock.test.ts b/.context/effect/packages/effect/test/Clock.test.ts new file mode 100644 index 000000000..6ad6a30c8 --- /dev/null +++ b/.context/effect/packages/effect/test/Clock.test.ts @@ -0,0 +1,37 @@ +import { assert, describe, it, vi } from "@effect/vitest" +import { Clock, Effect } from "effect" + +describe.sequential("Clock", () => { + it.live("keeps wall time aligned while exposing the monotonic source", () => { + let wallMillis = 1_000_000 + let monotonicNanos = 5_000_000_000n + const dateNow = vi.spyOn(Date, "now").mockImplementation(() => wallMillis) + const hrtime = vi.spyOn(process.hrtime, "bigint").mockImplementation(() => monotonicNanos) + return Effect.gen(function*() { + const clock = yield* Clock.Clock + const nanosPerMilli = 1_000_000n + + assert.strictEqual(clock.currentTimeNanosUnsafe(), BigInt(wallMillis) * nanosPerMilli) + assert.strictEqual(clock.monotonicTimeNanosUnsafe(), monotonicNanos) + + wallMillis += 250 + monotonicNanos += 250_000_000n + assert.strictEqual(clock.currentTimeNanosUnsafe(), BigInt(wallMillis) * nanosPerMilli) + + wallMillis += 5_000 + const beforeSuspend = clock.monotonicTimeNanosUnsafe() + assert.strictEqual(clock.currentTimeNanosUnsafe(), BigInt(wallMillis) * nanosPerMilli) + assert.strictEqual(clock.monotonicTimeNanosUnsafe(), beforeSuspend) + + wallMillis -= 3_000 + monotonicNanos += 100_000_000n + assert.strictEqual(clock.currentTimeNanosUnsafe(), BigInt(wallMillis) * nanosPerMilli) + assert.isTrue(clock.monotonicTimeNanosUnsafe() > beforeSuspend) + }).pipe( + Effect.ensuring(Effect.sync(() => { + dateNow.mockRestore() + hrtime.mockRestore() + })) + ) + }) +}) diff --git a/.context/effect/packages/effect/test/Config.test.ts b/.context/effect/packages/effect/test/Config.test.ts index 8ffe5eabe..e2f7c6a96 100644 --- a/.context/effect/packages/effect/test/Config.test.ts +++ b/.context/effect/packages/effect/test/Config.test.ts @@ -1,7 +1,19 @@ -import { describe, it } from "@effect/vitest" -import { deepStrictEqual } from "@effect/vitest/utils" -import { Config, ConfigProvider, Duration, Effect, Option, pipe, Redacted, Result, Schema, SchemaIssue } from "effect" -import * as assert from "node:assert" +import { assert, describe, it } from "@effect/vitest" +import { + Config, + ConfigProvider, + Duration, + Effect, + Option, + pipe, + Redacted, + Result, + Schema, + SchemaIssue, + SchemaTransformation +} from "effect" +import { vi } from "vitest" +import type * as ConfigProviderModule from "../src/ConfigProvider.ts" async function assertSuccess(config: Config.Config, provider: ConfigProvider.ConfigProvider, expected: T) { const r = await config.parse(provider).pipe( @@ -21,150 +33,157 @@ async function assertFailure(config: Config.Config, provider: ConfigProvid } describe("Config", () => { - it("a config is an Effect and can be yielded", () => { - const provider = ConfigProvider.fromEnv({ env: { STRING: "value" } }) - const result = Effect.runSync(Effect.provide( - Config.schema(Schema.Struct({ STRING: Schema.String })), - ConfigProvider.layer(provider) - )) - deepStrictEqual(result, { STRING: "value" }) + it("recognizes SourceError defects from a reloaded module copy", async () => { + vi.resetModules() + const ForeignConfigProvider = await vi.importActual( + "../src/ConfigProvider.ts" + ) + const sourceError = new ForeignConfigProvider.SourceError({ message: "source unavailable" }) + assert.isFalse(sourceError instanceof ConfigProvider.SourceError) + + const provider = ConfigProvider.make(() => Effect.die(sourceError)) + const error = await Config.string("value").parse(provider).pipe( + Effect.flip, + Effect.runPromise + ) + + assert.strictEqual(error.cause, sourceError) }) - describe("schema", () => { - it("should not leak any information about the value", async () => { - const provider = ConfigProvider.fromUnknown({}) - await assertFailure( - Config.schema(Schema.Redacted(Schema.Literal("secret")), "a"), - provider, - `Invalid data - at ["a"]` + it.effect("uses the current ConfigProvider when yielded as an Effect", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromEnv({ env: { STRING: "value" } }) + const result = yield* Effect.provide( + Config.schema(Schema.Struct({ STRING: Schema.String })), + ConfigProvider.layer(provider) ) - }) - }) + + assert.deepStrictEqual(result, { STRING: "value" }) + })) describe("constructors", () => { - it("fail", async () => { + it("fail creates an always-failing config", async () => { await assertFailure( Config.fail( - new Schema.SchemaError(new SchemaIssue.Forbidden(Option.none(), { message: "failure message" })) + new Schema.SchemaError(new SchemaIssue.Forbidden({ message: "failure message" })) ), ConfigProvider.fromUnknown({}), `failure message` ) }) - it("succeed", async () => { + it("succeed creates a provider-independent value", async () => { const provider = ConfigProvider.fromUnknown({}) await assertSuccess(Config.succeed(1), provider, 1) }) - it("string", async () => { + it("string decodes present input and reports absence", async () => { const provider = ConfigProvider.fromUnknown({ a: "value" }) await assertSuccess(Config.string("a"), provider, "value") await assertFailure( Config.string("b"), provider, - `Expected string, got undefined + `Expected string at ["b"]` ) }) - it("nonEmptyString", async () => { + it("nonEmptyString rejects preserved empty input", async () => { const provider = ConfigProvider.fromUnknown({ a: "value", b: "" }, { preserveEmptyStrings: true }) await assertSuccess(Config.nonEmptyString("a"), provider, "value") await assertFailure( Config.nonEmptyString("b"), provider, - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["b"]` ) }) - it("number", async () => { + it("number accepts finite and non-finite numbers", async () => { const provider = ConfigProvider.fromUnknown({ a: "1", c: "c", d: "Infinity" }) await assertSuccess(Config.number("a"), provider, 1) await assertSuccess(Config.number("d"), provider, Infinity) await assertFailure( Config.number("b"), provider, - `Expected string | "Infinity" | "-Infinity" | "NaN", got undefined + `Expected string | "Infinity" | "-Infinity" | "NaN" at ["b"]` ) }) - it("finite", async () => { + it("finite rejects invalid and non-finite numbers", async () => { const provider = ConfigProvider.fromUnknown({ a: "1", b: "a", c: "Infinity" }) await assertSuccess(Config.finite("a"), provider, 1) await assertFailure( Config.finite("b"), provider, - `Expected a string representing a finite number, got "a" + `Expected a string representing a finite number at ["b"]` ) await assertFailure( Config.finite("c"), provider, - `Expected a string representing a finite number, got "Infinity" + `Expected a string representing a finite number at ["c"]` ) }) - it("int", async () => { + it("int rejects non-integer numbers", async () => { const provider = ConfigProvider.fromUnknown({ a: "1", b: "1.2" }) await assertSuccess(Config.int("a"), provider, 1) await assertFailure( Config.int("b"), provider, - `Expected an integer, got 1.2 + `Expected an integer at ["b"]` ) }) - it("literal", async () => { + it("literal accepts only the configured value", async () => { const provider = ConfigProvider.fromUnknown({ a: "L" }) await assertSuccess(Config.literal("L", "a"), provider, "L") await assertFailure( Config.literal("-", "a"), provider, - `Expected "-", got "L" + `Expected "-" at ["a"]` ) }) - it("literals", async () => { + it("literals accepts configured string alternatives", async () => { const provider = ConfigProvider.fromUnknown({ a: "production", b: "staging" }) await assertSuccess(Config.literals(["development", "production"], "a"), provider, "production") await assertFailure( Config.literals(["development", "production"], "b"), provider, - `Expected "development" | "production", got "staging" + `Expected "development" | "production" at ["b"]` ) }) - it("literals (numbers)", async () => { + it("literals accepts configured number alternatives", async () => { const provider = ConfigProvider.fromUnknown({ a: "1", b: "3" }) await assertSuccess(Config.literals([1, 2], "a"), provider, 1) await assertFailure( Config.literals([1, 2], "b"), provider, - `Expected "1" | "2", got "3" + `Expected "1" | "2" at ["b"]` ) }) - it("date", async () => { + it("date rejects invalid dates", async () => { const provider = ConfigProvider.fromUnknown({ a: "2021-01-01", b: "invalid" }) await assertSuccess(Config.date("a"), provider, new Date("2021-01-01")) await assertFailure( Config.date("b"), provider, - `Expected a valid date, got Invalid Date + `Expected a valid Date at ["b"]` ) }) - it("redacted", async () => { + it("redacted creates redacted values and reports missing input", async () => { const provider = ConfigProvider.fromUnknown({ a: "value" }) @@ -173,12 +192,12 @@ describe("Config", () => { await assertFailure( Config.redacted("failure"), provider, - `Invalid data + `Expected string at ["failure"]` ) }) - it("url", async () => { + it("url decodes valid URLs and reports absence", async () => { const provider = ConfigProvider.fromUnknown({ a: "https://example.com" }) @@ -187,14 +206,14 @@ describe("Config", () => { await assertFailure( Config.url("failure"), provider, - `Expected string, got undefined + `Expected string at ["failure"]` ) }) }) describe("combinators", () => { - it("map", async () => { + it("map transforms successful values in data-first and data-last form", async () => { const config = Config.schema(Schema.String) await assertSuccess( @@ -209,13 +228,13 @@ describe("Config", () => { ) }) - it("mapOrFail", async () => { + it("mapOrFail supports effectful validation", async () => { const config = Config.schema(Schema.String) const f = (s: string) => s === "" ? Effect.fail( new Config.ConfigError( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some(s), { message: "empty" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "empty" })) ) ) : Effect.succeed(s.toUpperCase()) @@ -232,7 +251,7 @@ describe("Config", () => { ) }) - it("orElse", async () => { + it("orElse evaluates the fallback after absence", async () => { const config = Config.orElse(Config.string("a"), () => Config.finite("b")) await assertSuccess( @@ -247,64 +266,104 @@ describe("Config", () => { ) }) + it.effect("defers user callbacks until the Config Effect is executed", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromUnknown({}) + let mapCalls = 0 + let mapOrFailCalls = 0 + let orElseCalls = 0 + const mapped = Config.succeed(1).pipe( + Config.map((value) => { + mapCalls++ + return value + 1 + }) + ).parse(provider) + const mappedOrFailed = Config.succeed(1).pipe( + Config.mapOrFail((value) => { + mapOrFailCalls++ + return Effect.succeed(value + 1) + }) + ).parse(provider) + const recovered = Config.fail( + new Schema.SchemaError(new SchemaIssue.Forbidden({ message: "failure" })) + ).pipe( + Config.orElse(() => { + orElseCalls++ + return Config.succeed(1) + }) + ).parse(provider) + + assert.strictEqual(mapCalls, 0) + assert.strictEqual(mapOrFailCalls, 0) + assert.strictEqual(orElseCalls, 0) + + yield* mapped + yield* mappedOrFailed + yield* recovered + + assert.strictEqual(mapCalls, 1) + assert.strictEqual(mapOrFailCalls, 1) + assert.strictEqual(orElseCalls, 1) + })) + describe("all", () => { - it("tuple", async () => { + it("combines tuple inputs and preserves positions", async () => { const config = Config.all([Config.nonEmptyString("a"), Config.finite("b")]) await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a", b: "1" }), ["a", 1]) await assertFailure( config, ConfigProvider.fromUnknown({ a: "", b: "1" }, { preserveEmptyStrings: true }), - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["a"]` ) await assertFailure( config, ConfigProvider.fromUnknown({ a: "a", b: "b" }), - `Expected a string representing a finite number, got "b" + `Expected a string representing a finite number at ["b"]` ) }) - it("iterable", async () => { + it("combines generic iterables in iteration order", async () => { const config = Config.all(new Set([Config.nonEmptyString("a"), Config.finite("b")])) await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a", b: "1" }), ["a", 1]) await assertFailure( config, ConfigProvider.fromUnknown({ a: "", b: "1" }, { preserveEmptyStrings: true }), - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["a"]` ) await assertFailure( config, ConfigProvider.fromUnknown({ a: "a", b: "b" }), - `Expected a string representing a finite number, got "b" + `Expected a string representing a finite number at ["b"]` ) }) - it("struct", async () => { + it("combines named fields and preserves their keys", async () => { const config = Config.all({ a: Config.nonEmptyString("b"), c: Config.finite("d") }) await assertSuccess(config, ConfigProvider.fromUnknown({ b: "b", d: "1" }), { a: "b", c: 1 }) await assertFailure( config, ConfigProvider.fromUnknown({ b: "", d: "1" }, { preserveEmptyStrings: true }), - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["b"]` ) await assertFailure( config, ConfigProvider.fromUnknown({ b: "b", d: "b" }), - `Expected a string representing a finite number, got "b" + `Expected a string representing a finite number at ["d"]` ) }) }) describe("withDefault", () => { - it("value", async () => { + it("uses the parsed value when present and the default when absent", async () => { const defaultValue = 0 const config = Config.finite("a").pipe(Config.withDefault(defaultValue)) @@ -313,12 +372,12 @@ describe("Config", () => { await assertFailure( config, ConfigProvider.fromUnknown({ a: "value" }), - `Expected a string representing a finite number, got "value" + `Expected a string representing a finite number at ["a"]` ) }) - it("redacted", async () => { + it("supports redacted default values", async () => { const defaultValue = Redacted.make("default") const config = Config.redacted("a").pipe(Config.withDefault(defaultValue)) @@ -326,7 +385,7 @@ describe("Config", () => { await assertSuccess(config, ConfigProvider.fromUnknown({}), defaultValue) }) - it("uses default for empty env strings", async () => { + it("treats ignored empty env strings as absent", async () => { const config = Config.string("a").pipe(Config.withDefault("default")) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" } }), "default") @@ -337,55 +396,64 @@ describe("Config", () => { ) }) - it("uses default for empty env numbers", async () => { + it("validates empty env numbers when they are preserved", async () => { const config = Config.number("a").pipe(Config.withDefault(0)) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" } }), 0) await assertFailure( config, ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), - `Expected a string representing a finite number, got "" + `Expected a string representing a finite number at ["a"] -Expected "Infinity" | "-Infinity" | "NaN", got "" +Expected "Infinity" | "-Infinity" | "NaN" at ["a"]` ) }) - it("struct", async () => { + it("defaults wholly absent products and rejects partial products", async () => { const defaultValue = { a: "a", c: 0 } const config = Config.all({ a: Config.nonEmptyString("b"), c: Config.finite("d") }).pipe( Config.withDefault(defaultValue) ) await assertSuccess(config, ConfigProvider.fromUnknown({ b: "b", d: "1" }), { a: "b", c: 1 }) - await assertSuccess(config, ConfigProvider.fromUnknown({ b: "b" }), defaultValue) - await assertSuccess(config, ConfigProvider.fromUnknown({ d: "1" }), defaultValue) + await assertSuccess(config, ConfigProvider.fromUnknown({}), defaultValue) + await assertFailure( + config, + ConfigProvider.fromUnknown({ b: "b" }), + `Expected string + at ["d"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ d: "1" }), + `Expected string + at ["b"]` + ) await assertFailure( config, ConfigProvider.fromUnknown({ b: "", d: "1" }, { preserveEmptyStrings: true }), - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["b"]` ) }) - it("does not recover from invalid union values", async () => { + it("does not recover from invalid union input", async () => { const config = Config.logLevel("LOG_LEVEL").pipe(Config.withDefault("Info")) await assertSuccess(config, ConfigProvider.fromUnknown({}), "Info") await assertFailure( config, ConfigProvider.fromUnknown({ LOG_LEVEL: "debug" }), - `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None", got "debug" + `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None" at ["LOG_LEVEL"]` ) }) - it("does not recover from filter failures", async () => { + it("does not recover from schema refinement failures", async () => { const schema = Schema.String.check( - Schema.makeFilter((s) => - s === "a" ? undefined : new SchemaIssue.InvalidValue(Option.none(), { message: `must be "a"` }) - ) + Schema.makeFilter((s) => s === "a" ? undefined : new SchemaIssue.InvalidValue({ message: `must be "a"` })) ) const config = Config.schema(schema, "a").pipe(Config.withDefault("fallback")) @@ -402,64 +470,98 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" ) }) - it("array", async () => { + it("uses the default unless a plain Array schema receives an array representation", async () => { const config = Config.schema(Schema.Array(Schema.String), "a").pipe(Config.withDefault(["default"])) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "value" } }), ["value"]) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "value" } }), ["default"]) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" } }), ["default"]) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), []) + await assertSuccess( + config, + ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), + ["default"] + ) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "value" } }), ["value"]) await assertSuccess(config, ConfigProvider.fromEnv({ env: {} }), ["default"]) }) - it("schema containers", async () => { - const provider = ConfigProvider.fromEnv({ env: {} }) + it("defaults absent named containers and preserves explicit empty containers", async () => { + const absent = ConfigProvider.fromUnknown({}) await assertSuccess( Config.schema(Schema.Struct({ value: Schema.String }), "a").pipe(Config.withDefault({ value: "default" })), - provider, + absent, { value: "default" } ) await assertSuccess( Config.schema(Schema.Struct({ value: Schema.optionalKey(Schema.String) }), "a").pipe( Config.withDefault({ value: "default" }) ), - provider, + absent, { value: "default" } ) await assertSuccess( Config.schema(Schema.Struct({}), "a").pipe(Config.withDefault({ value: "default" })), - provider, + absent, { value: "default" } ) await assertSuccess( Config.schema(Schema.Record(Schema.String, Schema.String), "a").pipe( Config.withDefault({ value: "default" }) ), - provider, + absent, { value: "default" } ) + await assertSuccess( + Config.schema(Schema.Tuple([]), "a").pipe(Config.withDefault(["default"])), + absent, + ["default"] + ) await assertSuccess( Config.schema(Schema.Tuple([Schema.String]), "a").pipe(Config.withDefault(["default"])), - provider, + absent, ["default"] ) await assertSuccess( Config.schema(Schema.ReadonlySet(Schema.String), "a").pipe(Config.withDefault(new Set(["default"]))), - provider, + absent, new Set(["default"]) ) await assertSuccess( Config.schema(Schema.ReadonlyMap(Schema.String, Schema.String), "a").pipe( Config.withDefault(new Map([["default", "value"]])) ), - provider, + absent, new Map([["default", "value"]]) ) + + await assertSuccess( + Config.schema(Schema.Struct({ value: Schema.optionalKey(Schema.String) }), "a"), + ConfigProvider.fromUnknown({ a: {} }), + {} + ) + await assertSuccess( + Config.schema(Schema.Record(Schema.String, Schema.String), "a"), + ConfigProvider.fromUnknown({ a: {} }), + {} + ) + await assertSuccess( + Config.schema(Schema.Tuple([]), "a"), + ConfigProvider.fromUnknown({ a: [] }), + [] + ) + }) + + it("preserves values successfully decoded from undefined", async () => { + const config = Config.schema(Schema.UndefinedOr(Schema.String), "a").pipe( + Config.withDefault("default") + ) + + await assertSuccess(config, ConfigProvider.fromUnknown({}), undefined) }) }) describe("option", () => { - it("value", async () => { + it("wraps present values and maps absence to None", async () => { const config = Config.finite("a").pipe(Config.option) const stringConfig = Config.string("a").pipe(Config.option) @@ -474,33 +576,418 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromUnknown({ a: "value" }), - `Expected a string representing a finite number, got "value" + `Expected a string representing a finite number at ["a"]` ) }) - it("struct", async () => { + it("returns None for absent products and rejects partial products", async () => { const config = Config.all({ a: Config.nonEmptyString("b"), c: Config.finite("d") }).pipe( Config.option ) await assertSuccess(config, ConfigProvider.fromUnknown({ b: "b", d: "1" }), Option.some({ a: "b", c: 1 })) - await assertSuccess(config, ConfigProvider.fromUnknown({ b: "b" }), Option.none()) - await assertSuccess(config, ConfigProvider.fromUnknown({ d: "1" }), Option.none()) - await assertSuccess(config, ConfigProvider.fromUnknown({ b: "", d: "1" }), Option.none()) + await assertSuccess(config, ConfigProvider.fromUnknown({}), Option.none()) + await assertFailure( + config, + ConfigProvider.fromUnknown({ b: "b" }), + `Expected string + at ["d"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ d: "1" }), + `Expected string + at ["b"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ b: "", d: "1" }), + `Expected string + at ["b"]` + ) await assertFailure( config, ConfigProvider.fromUnknown({ b: "", d: "1" }, { preserveEmptyStrings: true }), - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["b"]` ) }) + + it.effect("wraps successfully decoded undefined in Some", () => + Effect.gen(function*() { + const config = Config.schema(Schema.UndefinedOr(Schema.String), "a").pipe(Config.option) + + assert.deepStrictEqual( + yield* config.parse(ConfigProvider.fromUnknown({})), + Option.some(undefined) + ) + })) + }) + + describe("absence semantics", () => { + describe("schema and all", () => { + const fallback = { host: "fallback", port: 0 } + const schemaConfig = Config.schema( + Schema.Struct({ + host: Schema.String, + port: Schema.Finite + }) + ).pipe(Config.nested("database")) + const allConfig = Config.all({ + host: Config.string("host"), + port: Config.finite("port") + }).pipe(Config.nested("database")) + + it("default wholly absent nested configurations", async () => { + const provider = ConfigProvider.fromUnknown({}) + + await assertSuccess(schemaConfig.pipe(Config.withDefault(fallback)), provider, fallback) + await assertSuccess(allConfig.pipe(Config.withDefault(fallback)), provider, fallback) + }) + + it("distinguish an explicit empty schema container from an absent all group", async () => { + const provider = ConfigProvider.fromUnknown({ database: {} }) + + await assertFailure( + schemaConfig.pipe(Config.withDefault(fallback)), + provider, + `Missing key + at ["database"]["host"]` + ) + await assertSuccess(allConfig.pipe(Config.withDefault(fallback)), provider, fallback) + + await assertFailure( + schemaConfig.pipe(Config.option), + provider, + `Missing key + at ["database"]["host"]` + ) + await assertSuccess(allConfig.pipe(Config.option), provider, Option.none()) + }) + + it("reject partial input for both composition models", async () => { + const provider = ConfigProvider.fromUnknown({ database: { host: "localhost" } }) + + await assertFailure( + schemaConfig.pipe(Config.withDefault(fallback)), + provider, + `Missing key + at ["database"]["port"]` + ) + await assertFailure( + allConfig.pipe(Config.withDefault(fallback)), + provider, + `Expected string + at ["database"]["port"]` + ) + }) + + it.effect("does not count successful undefined child values as provider input", () => + Effect.gen(function*() { + const config = Config.all({ + optional: Config.schema(Schema.UndefinedOr(Schema.String), "optional"), + required: Config.string("required") + }) + const fallback = { optional: "fallback", required: "fallback" } + const provider = ConfigProvider.fromUnknown({}) + + assert.deepStrictEqual( + yield* config.pipe(Config.withDefault(fallback)).parse(provider), + fallback + ) + assert.deepStrictEqual( + yield* config.pipe(Config.option).parse(provider), + Option.none() + ) + })) + }) + + it.effect("rejects partial products independently of field order", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromUnknown({ invalid: "not-a-number" }) + const schemaConfigs = [ + Config.schema( + Schema.Struct({ + missing: Schema.String, + invalid: Schema.Finite + }) + ), + Config.schema( + Schema.Struct({ + invalid: Schema.Finite, + missing: Schema.String + }) + ) + ] + const allConfigs = [ + Config.all({ + missing: Config.string("missing"), + invalid: Config.finite("invalid") + }), + Config.all({ + invalid: Config.finite("invalid"), + missing: Config.string("missing") + }) + ] + + for (const config of [...schemaConfigs, ...allConfigs]) { + const error = yield* config.pipe( + Config.withDefault({ missing: "default", invalid: 0 }), + (config) => config.parse(provider), + Effect.flip + ) + assert.ok(error instanceof Config.ConfigError) + } + })) + + it.effect("does not count child defaults as provider input", () => + Effect.gen(function*() { + const fallback = { required: "fallback", defaulted: 0 } + const config = Config.all({ + required: Config.string("required"), + defaulted: Config.int("defaulted").pipe(Config.withDefault(1)) + }).pipe(Config.withDefault(fallback)) + + assert.deepStrictEqual( + yield* config.parse(ConfigProvider.fromUnknown({})), + fallback + ) + assert.deepStrictEqual( + yield* config.parse(ConfigProvider.fromUnknown({ required: "value" })), + { required: "value", defaulted: 1 } + ) + const error = yield* config.parse( + ConfigProvider.fromUnknown({ defaulted: "2" }) + ).pipe(Effect.flip) + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("preserves provider input evidence recovered by orElse", () => + Effect.gen(function*() { + const config = Config.all({ + recovered: Config.int("recovered").pipe(Config.orElse(() => Config.succeed(1))), + required: Config.string("required") + }).pipe(Config.withDefault({ recovered: 0, required: "default" })) + const error = yield* config.parse( + ConfigProvider.fromUnknown({ recovered: "invalid" }) + ).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("does not invent provider input evidence when orElse recovers absence", () => + Effect.gen(function*() { + const fallback = { recovered: 0, required: "default" } + const config = Config.all({ + recovered: Config.int("recovered").pipe(Config.orElse(() => Config.succeed(1))), + required: Config.string("required") + }).pipe(Config.withDefault(fallback)) + + assert.deepStrictEqual( + yield* config.parse(ConfigProvider.fromUnknown({})), + fallback + ) + })) + + it.effect("does not turn recovered invalid input into absence", () => + Effect.gen(function*() { + const config = Config.int("primary").pipe( + Config.orElse(() => Config.string("fallback")), + Config.withDefault("default") + ) + const error = yield* config.parse( + ConfigProvider.fromUnknown({ primary: "invalid" }) + ).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["fallback"]` + ) + })) + + it.effect("preserves provider input evidence through mapOrFail and orElse", () => + Effect.gen(function*() { + const validationError = new Config.ConfigError( + new Schema.SchemaError(new SchemaIssue.Forbidden({ message: "invalid value" })) + ) + const config = Config.all({ + recovered: Config.string("recovered").pipe( + Config.mapOrFail(() => Effect.fail(validationError)), + Config.orElse(() => Config.succeed("fallback")) + ), + required: Config.string("required") + }).pipe(Config.withDefault({ recovered: "default", required: "default" })) + const error = yield* config.parse( + ConfigProvider.fromUnknown({ recovered: "value" }) + ).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("preserves provider input evidence after a descendant source failure", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make((path) => { + if (path.length === 0) { + return Effect.succeed(ConfigProvider.makeRecord(new Set(["value"]))) + } + return path.length === 1 && path[0] === "value" + ? Effect.fail(sourceError) + : Effect.succeed(undefined) + }) + const config = Config.all({ + recovered: Config.schema(Schema.Struct({ value: Schema.String })).pipe( + Config.orElse(() => Config.succeed({ value: "fallback" })) + ), + required: Config.string("required") + }).pipe(Config.withDefault({ recovered: { value: "default" }, required: "default" })) + const error = yield* config.parse(provider).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("preserves sibling input evidence when recovering an all failure", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make((path) => { + if (path[0] === "failed") return Effect.fail(sourceError) + if (path[0] === "present") return Effect.succeed(ConfigProvider.makeValue("value")) + return Effect.succeed(undefined) + }) + const recovered = Config.all({ + failed: Config.string("failed"), + present: Config.string("present") + }).pipe(Config.orElse(() => Config.succeed({ failed: "recovered", present: "recovered" }))) + const config = Config.all({ + recovered, + required: Config.string("required") + }).pipe(Config.withDefault({ + recovered: { failed: "default", present: "default" }, + required: "default" + })) + const error = yield* config.parse(provider).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("does not invent provider input evidence after an initial source failure", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make((path) => + path.length === 0 ? Effect.fail(sourceError) : Effect.succeed(undefined) + ) + const fallback = { recovered: { value: "default" }, required: "default" } + const config = Config.all({ + recovered: Config.schema(Schema.Struct({ value: Schema.String })).pipe( + Config.orElse(() => Config.succeed({ value: "fallback" })) + ), + required: Config.string("required") + }).pipe(Config.withDefault(fallback)) + + assert.deepStrictEqual( + yield* config.parse(provider), + fallback + ) + })) + + it.effect("normalizes unavailable scalar representations to absence", () => + Effect.gen(function*() { + const provider = ConfigProvider.make((path) => + Effect.succeed( + path.length === 1 && path[0] === "value" + ? ConfigProvider.makeRecord(new Set()) + : undefined + ) + ) + const config = Config.string("value") + + assert.strictEqual( + yield* config.pipe(Config.withDefault("default")).parse(provider), + "default" + ) + assert.deepStrictEqual( + yield* config.pipe(Config.option).parse(provider), + Option.none() + ) + })) + + it.effect("treats incompatible container representations as absent", () => + Effect.gen(function*() { + const struct = Config.schema( + Schema.Struct({ value: Schema.optionalKey(Schema.String) }), + "value" + ) + const array = Config.schema(Schema.Array(Schema.String), "value") + + assert.deepStrictEqual( + yield* struct.pipe(Config.withDefault({ value: "default" })).parse( + ConfigProvider.fromUnknown({ value: [] }) + ), + { value: "default" } + ) + assert.deepStrictEqual( + yield* array.pipe(Config.option).parse( + ConfigProvider.fromUnknown({ value: {} }) + ), + Option.none() + ) + })) + + it.effect("propagates provider failures", () => + Effect.gen(function*() { + const cause = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make(() => Effect.fail(cause)) + const error = yield* Config.string("a").pipe( + Config.withDefault("fallback"), + (config) => config.parse(provider), + Effect.flip + ) + + assert.strictEqual(error.cause, cause) + })) + + it.effect("rejects present containers with incompatible shapes", () => + Effect.gen(function*() { + const wrongStruct = yield* Config.schema( + Schema.Struct({ value: Schema.optionalKey(Schema.String) }), + "value" + ).parse(ConfigProvider.fromUnknown({ value: [] })).pipe(Effect.flip) + assert.ok(wrongStruct instanceof Config.ConfigError) + + const wrongArray = yield* Config.schema( + Schema.Array(Schema.String), + "value" + ).parse(ConfigProvider.fromUnknown({ value: {} })).pipe(Effect.flip) + assert.ok(wrongArray instanceof Config.ConfigError) + })) }) describe("nested", () => { - describe("fromUnknown", () => { - it("nested", async () => { + describe("with fromUnknown", () => { + it("prefixes a root config", async () => { const config = Config.string().pipe(Config.nested("a")) await assertSuccess( @@ -511,12 +998,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromUnknown({}), - `Expected string, got undefined + `Expected string at ["a"]` ) }) - it("name + nested", async () => { + it("composes a constructor path with a prefix", async () => { const config = Config.string("a").pipe(Config.nested("b")) await assertSuccess( @@ -527,12 +1014,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromUnknown({}), - `Expected string, got undefined + `Expected string at ["b"]["a"]` ) }) - it("name + nested + nested", async () => { + it("composes multiple prefixes from outermost to innermost", async () => { const config = Config.string("a").pipe(Config.nested("b"), Config.nested("c")) await assertSuccess( @@ -543,12 +1030,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromUnknown({ c: { b: {} } }), - `Expected string, got undefined + `Expected string at ["c"]["b"]["a"]` ) }) - it("all", async () => { + it("prefixes every child of an all product", async () => { const config = Config.all({ host: Config.string("host"), port: Config.number("port") @@ -562,14 +1049,14 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromUnknown({}), - `Expected string, got undefined + `Expected string at ["database"]["host"]` ) }) }) - describe("fromEnv", () => { - it("nested", async () => { + describe("with fromEnv", () => { + it("prefixes a root config", async () => { const config = Config.string().pipe(Config.nested("a")) await assertSuccess( @@ -580,12 +1067,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined + `Expected string at ["a"]` ) }) - it("name + nested", async () => { + it("composes a constructor path with a prefix", async () => { const config = Config.string("a").pipe(Config.nested("b")) await assertSuccess( @@ -596,12 +1083,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined + `Expected string at ["b"]["a"]` ) }) - it("name + nested + nested", async () => { + it("composes multiple prefixes from outermost to innermost", async () => { const config = Config.string("a").pipe(Config.nested("b"), Config.nested("c")) await assertSuccess( @@ -612,12 +1099,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromEnv({ env: { "c_b": "value" } }), - `Expected string, got undefined + `Expected string at ["c"]["b"]["a"]` ) }) - it("all", async () => { + it("prefixes every child of an all product", async () => { const config = Config.all({ host: Config.string("host"), port: Config.number("port") @@ -631,12 +1118,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined + `Expected string at ["database"]["host"]` ) }) - it("config nested and provider nested compose lookup but not error paths", async () => { + it("composes Config and provider prefixes without leaking provider paths into errors", async () => { const config = Config.string("host").pipe(Config.nested("database")) const provider = ConfigProvider.fromEnv({ env: { app_database_host: "localhost" } @@ -646,12 +1133,12 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( config, ConfigProvider.fromEnv({ env: {} }).pipe(ConfigProvider.nested("app")), - `Expected string, got undefined + `Expected string at ["database"]["host"]` ) }) - it("provider nested over orElse keeps the logical error path", async () => { + it("preserves logical error paths through provider fallback", async () => { const provider = ConfigProvider.fromEnv({ env: { app_port: "abc" } }).pipe( ConfigProvider.orElse(ConfigProvider.fromEnv({ env: {} })), ConfigProvider.nested("app") @@ -660,9 +1147,9 @@ Expected "Infinity" | "-Infinity" | "NaN", got "" await assertFailure( Config.number("port"), provider, - `Expected a string representing a finite number, got "abc" + `Expected a string representing a finite number at ["port"] -Expected "Infinity" | "-Infinity" | "NaN", got "abc" +Expected "Infinity" | "-Infinity" | "NaN" at ["port"]` ) }) @@ -670,7 +1157,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "abc" }) describe("unwrap", () => { - it("plain object", async () => { + it("combines a plain record of configs", async () => { const config = Config.unwrap({ a: Config.schema(Schema.String, "a2") }) @@ -678,7 +1165,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "abc" await assertSuccess(config, ConfigProvider.fromUnknown({ a2: "value" }), { a: "value" }) }) - it("nested", async () => { + it("recursively combines nested records", async () => { const config = Config.unwrap({ a: { b: Config.schema(Schema.String, "b2") @@ -694,831 +1181,1229 @@ Expected "Infinity" | "-Infinity" | "NaN", got "abc" }) }) - describe("Config built-in schemas", () => { - it("Boolean", async () => { - const provider = ConfigProvider.fromUnknown({ - a: "true", - b: "false", - c: "yes", - d: "no", - e: "on", - f: "off", - g: "1", - h: "0", - i: "y", - j: "n", - failure: "value" - }) - - await assertSuccess(Config.boolean("a"), provider, true) - await assertSuccess(Config.boolean("b"), provider, false) - await assertSuccess(Config.boolean("c"), provider, true) - await assertSuccess(Config.boolean("d"), provider, false) - await assertSuccess(Config.boolean("e"), provider, true) - await assertSuccess(Config.boolean("f"), provider, false) - await assertSuccess(Config.boolean("g"), provider, true) - await assertSuccess(Config.boolean("h"), provider, false) - await assertSuccess(Config.boolean("i"), provider, true) - await assertSuccess(Config.boolean("j"), provider, false) + describe("schema", () => { + it("reports missing redacted input", async () => { await assertFailure( - Config.boolean("failure"), - provider, - `Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n", got "value" - at ["failure"]` + Config.schema(Schema.Redacted(Schema.Literal("secret")), "a"), + ConfigProvider.fromUnknown({}), + `Expected "secret" + at ["a"]` ) }) - it("Duration", async () => { - const provider = ConfigProvider.fromUnknown({ - a: "1000 millis", - b: "1 second", - c: "Infinity", - d: "-Infinity", - failure: "value" - }) + describe("built-in schema-backed constructors", () => { + it("decodes supported boolean spellings", async () => { + const provider = ConfigProvider.fromUnknown({ + a: "true", + b: "false", + c: "yes", + d: "no", + e: "on", + f: "off", + g: "1", + h: "0", + i: "y", + j: "n", + failure: "value" + }) - await assertSuccess(Config.duration("a"), provider, Duration.millis(1000)) - await assertSuccess(Config.duration("b"), provider, Duration.seconds(1)) - await assertSuccess(Config.duration("c"), provider, Duration.infinity) - await assertSuccess(Config.duration("d"), provider, Duration.negativeInfinity) - await assertFailure( - Config.duration("failure"), - provider, - `Invalid Duration string: value + await assertSuccess(Config.boolean("a"), provider, true) + await assertSuccess(Config.boolean("b"), provider, false) + await assertSuccess(Config.boolean("c"), provider, true) + await assertSuccess(Config.boolean("d"), provider, false) + await assertSuccess(Config.boolean("e"), provider, true) + await assertSuccess(Config.boolean("f"), provider, false) + await assertSuccess(Config.boolean("g"), provider, true) + await assertSuccess(Config.boolean("h"), provider, false) + await assertSuccess(Config.boolean("i"), provider, true) + await assertSuccess(Config.boolean("j"), provider, false) + await assertFailure( + Config.boolean("failure"), + provider, + `Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n" at ["failure"]` - ) - }) - - it("Port", async () => { - const provider = ConfigProvider.fromUnknown({ - a: "8080", - failure: "-1" + ) }) - await assertSuccess(Config.port("a"), provider, 8080) - await assertFailure( - Config.port("failure"), - provider, - `Expected a value between 1 and 65535, got -1 + it("decodes durations including infinities", async () => { + const provider = ConfigProvider.fromUnknown({ + a: "1000 millis", + b: "1 second", + c: "Infinity", + d: "-Infinity", + failure: "value" + }) + + await assertSuccess(Config.duration("a"), provider, Duration.millis(1000)) + await assertSuccess(Config.duration("b"), provider, Duration.seconds(1)) + await assertSuccess(Config.duration("c"), provider, Duration.infinity) + await assertSuccess(Config.duration("d"), provider, Duration.negativeInfinity) + await assertFailure( + Config.duration("failure"), + provider, + `Expected a valid Duration string at ["failure"]` - ) - }) + ) + }) - it("LogLevel / logLevel", async () => { - const provider = ConfigProvider.fromUnknown({ - a: "Info", - failure_1: "info", - failure_2: "value" + it("validates port ranges", async () => { + const provider = ConfigProvider.fromUnknown({ + a: "8080", + failure: "-1" + }) + + await assertSuccess(Config.port("a"), provider, 8080) + await assertFailure( + Config.port("failure"), + provider, + `Expected a value between 1 and 65535 + at ["failure"]` + ) }) - await assertSuccess(Config.logLevel("a"), provider, "Info") - await assertFailure( - Config.logLevel("failure_1"), - provider, - `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None", got "info" + it("validates log-level literals", async () => { + const provider = ConfigProvider.fromUnknown({ + a: "Info", + failure_1: "info", + failure_2: "value" + }) + + await assertSuccess(Config.logLevel("a"), provider, "Info") + await assertFailure( + Config.logLevel("failure_1"), + provider, + `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None" at ["failure_1"]` - ) - await assertFailure( - Config.logLevel("failure_2"), - provider, - `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None", got "value" + ) + await assertFailure( + Config.logLevel("failure_2"), + provider, + `Expected "All" | "Fatal" | "Error" | "Warn" | "Info" | "Debug" | "Trace" | "None" at ["failure_2"]` - ) - }) + ) + }) - describe("Record", () => { - it("from record", async () => { - const schema = Config.Record(Schema.String, Schema.String) - const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") + describe("Record", () => { + it("decodes object input", async () => { + const schema = Config.Record(Schema.String, Schema.String) + const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") - await assertSuccess( - config, - ConfigProvider.fromUnknown({ - OTEL_RESOURCE_ATTRIBUTES: { + await assertSuccess( + config, + ConfigProvider.fromUnknown({ + OTEL_RESOURCE_ATTRIBUTES: { + "service.name": "my-service", + "service.version": "1.0.0", + "custom.attribute": "value" + } + }), + { "service.name": "my-service", "service.version": "1.0.0", "custom.attribute": "value" } - }), - { - "service.name": "my-service", - "service.version": "1.0.0", - "custom.attribute": "value" - } - ) - }) + ) + }) - it("from string", async () => { - const schema = Config.Record(Schema.String, Schema.String) - const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") + it("decodes separated string input", async () => { + const schema = Config.Record(Schema.String, Schema.String) + const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") - await assertSuccess( - config, - ConfigProvider.fromEnv({ - env: { - OTEL_RESOURCE_ATTRIBUTES: "service.name=my-service,service.version=1.0.0,custom.attribute=value" + await assertSuccess( + config, + ConfigProvider.fromEnv({ + env: { + OTEL_RESOURCE_ATTRIBUTES: "service.name=my-service,service.version=1.0.0,custom.attribute=value" + } + }), + { + "service.name": "my-service", + "service.version": "1.0.0", + "custom.attribute": "value" } - }), - { - "service.name": "my-service", - "service.version": "1.0.0", - "custom.attribute": "value" - } - ) - }) + ) + }) - it("options", async () => { - const schema = Config.Record(Schema.String, Schema.String, { separator: "&", keyValueSeparator: "==" }) - const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") + it("supports custom separators", async () => { + const schema = Config.Record(Schema.String, Schema.String, { separator: "&", keyValueSeparator: "==" }) + const config = Config.schema(schema, "OTEL_RESOURCE_ATTRIBUTES") - await assertSuccess( - config, - ConfigProvider.fromEnv({ - env: { - OTEL_RESOURCE_ATTRIBUTES: "service.name==my-service&service.version==1.0.0&custom.attribute==value" + await assertSuccess( + config, + ConfigProvider.fromEnv({ + env: { + OTEL_RESOURCE_ATTRIBUTES: "service.name==my-service&service.version==1.0.0&custom.attribute==value" + } + }), + { + "service.name": "my-service", + "service.version": "1.0.0", + "custom.attribute": "value" } - }), - { - "service.name": "my-service", - "service.version": "1.0.0", - "custom.attribute": "value" - } - ) + ) + }) }) }) - }) - describe("fromEnv", () => { - it("path argument", async () => { - await assertSuccess( - Config.schema(Schema.String, "a"), - ConfigProvider.fromEnv({ env: { a: "value" } }), - "value" - ) - await assertSuccess( - Config.schema(Schema.String, ["a", "b"]), - ConfigProvider.fromEnv({ env: { "a_b": "value" } }), - "value" - ) - await assertSuccess( - Config.schema(Schema.UndefinedOr(Schema.String)), - ConfigProvider.fromEnv({ env: {} }), - undefined - ) - await assertSuccess( - Config.schema(Schema.UndefinedOr(Schema.String), "a"), - ConfigProvider.fromEnv({ env: {} }), - undefined - ) - }) + describe("materialization", () => { + describe("Encoded shapes", () => { + const scalarToStruct = Schema.String.pipe( + Schema.decodeTo( + Schema.Struct({ value: Schema.String }), + SchemaTransformation.transform({ + decode: (value) => ({ value }), + encode: ({ value }) => value + }) + ) + ) + const structToScalar = Schema.Struct({ value: Schema.String }).pipe( + Schema.decodeTo( + Schema.String, + SchemaTransformation.transform({ + decode: ({ value }) => value, + encode: (value) => ({ value }) + }) + ) + ) - describe("leafs and containers", () => { - it("node can be both leaf and object", async () => { - const schema = Schema.Struct({ a: Schema.Number }) - const config = Config.schema(schema) + it.effect("loads the encoded shape when it differs from the decoded shape", () => + Effect.gen(function*() { + assert.deepStrictEqual( + yield* Config.schema(scalarToStruct, "config").parse( + ConfigProvider.fromUnknown({ config: "value" }) + ), + { value: "value" } + ) + assert.strictEqual( + yield* Config.schema(structToScalar, "config").parse( + ConfigProvider.fromUnknown({ config: { value: "value" } }) + ), + "value" + ) + })) + + it.effect("loads encoded shapes recursively inside objects and arrays", () => + Effect.gen(function*() { + const config = Config.schema( + Schema.Struct({ + fromScalar: scalarToStruct, + fromStruct: structToScalar, + items: Schema.Array(scalarToStruct) + }) + ) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_b": "2" } }), { a: 1 }) - }) + assert.deepStrictEqual( + yield* config.parse( + ConfigProvider.fromUnknown({ + fromScalar: "one", + fromStruct: { value: "two" }, + items: ["three"] + }) + ), + { + fromScalar: { value: "one" }, + fromStruct: "two", + items: [{ value: "three" }] + } + ) + })) - it("node can be both leaf and array", async () => { - const schema = Schema.Struct({ a: Schema.Number }) - const config = Config.schema(schema) + it.effect("loads every union member from its encoded shape", () => + Effect.gen(function*() { + const config = Config.schema( + Schema.Union([scalarToStruct, structToScalar]), + "config" + ) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_0": "2" } }), { a: 1 }) + assert.deepStrictEqual( + yield* config.parse(ConfigProvider.fromUnknown({ config: "scalar" })), + { value: "scalar" } + ) + assert.strictEqual( + yield* config.parse( + ConfigProvider.fromUnknown({ config: { value: "struct" } }) + ), + "struct" + ) + })) }) - it("if a node can be both object and array, it should be an object", async () => { - const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.Number }) }) - const config = Config.schema(schema) - - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_b": "2", "a_0": "3" } }), { - a: { b: 2 } - }) + describe("Objects", () => { + it.effect("loads explicit properties even when only a fallback provider contains the child", () => + Effect.gen(function*() { + const primary = ConfigProvider.make((path) => + Effect.succeed( + path.length === 0 + ? ConfigProvider.makeRecord(new Set()) + : undefined + ) + ) + const fallback = ConfigProvider.make((path) => + Effect.succeed( + path.length === 1 && path[0] === "host" + ? ConfigProvider.makeValue("localhost") + : undefined + ) + ) + const provider = ConfigProvider.orElse(primary, fallback) + + assert.deepStrictEqual( + yield* Config.schema(Schema.Struct({ host: Schema.String })).parse(provider), + { host: "localhost" } + ) + })) + + it.effect("does not load advertised keys that are unrelated to the schema", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "unrelated key was loaded" }) + const provider = ConfigProvider.make((path) => { + if (path.length === 0) { + return Effect.succeed(ConfigProvider.makeRecord(new Set(["wanted", "unrelated"]))) + } + if (path[0] === "wanted") { + return Effect.succeed(ConfigProvider.makeValue("value")) + } + return Effect.fail(sourceError) + }) + + assert.deepStrictEqual( + yield* Config.schema(Schema.Struct({ wanted: Schema.String })).parse(provider), + { wanted: "value" } + ) + })) + + it.effect("loads advertised keys only when they match an index signature", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "non-matching key was loaded" }) + const provider = ConfigProvider.make((path) => { + if (path.length === 0) { + return Effect.succeed(ConfigProvider.makeRecord(new Set(["wanted", "unrelated"]))) + } + if (path[0] === "wanted") { + return Effect.succeed(ConfigProvider.makeValue("value")) + } + return Effect.fail(sourceError) + }) + + assert.deepStrictEqual( + yield* Config.schema(Schema.Record(Schema.Literal("wanted"), Schema.String)).parse(provider), + { wanted: "value" } + ) + })) + + it.effect("leaves separated record parsing to the explicit Config.Record schema", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromEnv({ + env: { + values: "first=1,second=2" + } + }) + + assert.deepStrictEqual( + yield* Config.schema(Config.Record(Schema.String, Schema.Finite), "values").parse(provider), + { first: 1, second: 2 } + ) + })) }) - }) - it("Null", async () => { - const schema = Schema.Null - const config = Config.schema(schema, "a") + describe("Arrays", () => { + it.effect("preserves missing array positions as undefined values", () => + Effect.gen(function*() { + const provider = ConfigProvider.make((path) => { + if (path.length === 0) { + return Effect.succeed(ConfigProvider.makeArray(2)) + } + return Effect.succeed( + path[0] === 0 + ? ConfigProvider.makeValue("value") + : undefined + ) + }) + + assert.deepStrictEqual( + yield* Config.schema(Schema.Array(Schema.UndefinedOr(Schema.String))).parse(provider), + ["value", undefined] + ) + })) + + it.effect("leaves scalar-to-array parsing to the explicit Config.Array schema", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromEnv({ + env: { + values: "1,2", + values_0: "3" + } + }) + + assert.deepStrictEqual( + yield* Config.schema(Config.Array(Schema.Finite), "values").parse(provider), + [1, 2] + ) + assert.deepStrictEqual( + yield* Config.schema(Schema.Array(Schema.Finite), "values").parse(provider), + [3] + ) + })) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "null" } }), null) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected "null", got undefined - at ["a"]` - ) - }) + describe("Opaque schemas", () => { + const unsupported = [ + ["Any", Schema.Any], + ["Unknown", Schema.Unknown], + ["ObjectKeyword", Schema.ObjectKeyword], + ["Json", Schema.Json], + ["MutableJson", Schema.MutableJson] + ] as const + + for (const [name, schema] of unsupported) { + it(`rejects Schema.${name}`, () => { + assert.throws( + () => Config.schema(schema), + /Config\.schema does not support opaque StringTree encodings/ + ) + }) + } - it("String", async () => { - const schema = Schema.String - const config = Config.schema(schema, "a") + it("rejects opaque shapes nested in objects", () => { + assert.throws( + () => Config.schema(Schema.Struct({ value: Schema.Unknown })), + /Config\.schema does not support opaque StringTree encodings/ + ) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined - at ["a"]` - ) - }) + it("rejects opaque union members", () => { + assert.throws( + () => Config.schema(Schema.Union([Schema.String, Schema.Unknown])), + /Config\.schema does not support opaque StringTree encodings/ + ) + }) - it("Number", async () => { - const schema = Schema.Number - const config = Config.schema(schema, "a") + it("rejects opaque shapes behind suspensions", () => { + assert.throws( + () => Config.schema(Schema.suspend(() => Schema.Unknown)), + /Config\.schema does not support opaque StringTree encodings/ + ) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected string | "Infinity" | "-Infinity" | "NaN", got undefined - at ["a"]` - ) - }) + it.effect("supports declarations with concrete StringTree encodings", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromUnknown({ value: "https://example.com" }) + const value = yield* Config.schema(Schema.URL, "value").parse(provider) - it("Finite", async () => { - const schema = Schema.Finite - const config = Config.schema(schema, "a") + assert.strictEqual(value.href, "https://example.com/") + })) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined - at ["a"]` - ) - }) + it.effect("supports arbitrary JSON encoded in a scalar string", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromUnknown({ value: `{"nested":[1,true]}` }) + const value = yield* Config.schema(Schema.fromJsonString(Schema.Json), "value").parse(provider) - it("Int", async () => { - const schema = Schema.Int - const config = Config.schema(schema, "a") + assert.deepStrictEqual(value, { nested: [1, true] }) + })) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected string, got undefined - at ["a"]` - ) - }) + describe("Union", () => { + it("materializes each member independently before applying first-match semantics", async () => { + const config = Config.schema( + Schema.Union([ + Schema.Struct({ child: Schema.String }), + Schema.String + ]), + "value" + ) + const provider = ConfigProvider.fromEnv({ + env: { + value: "scalar", + value_child: "object" + } + }) - it("Boolean", async () => { - const schema = Schema.Boolean - const config = Config.schema(schema, "a") + await assertSuccess(config, provider, { child: "object" }) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "true" } }), true) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "false" } }), false) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Expected "true" | "false", got undefined - at ["a"]` - ) - }) + it("preserves first-match semantics when the scalar member is declared first", async () => { + const config = Config.schema( + Schema.Union([ + Schema.String, + Schema.Struct({ child: Schema.String }) + ]), + "value" + ) + const provider = ConfigProvider.fromEnv({ + env: { + value: "scalar", + value_child: "object" + } + }) - describe("Struct", () => { - it("required properties", async () => { - const schema = Schema.Struct({ a: Schema.Number }) - const config = Config.schema(schema) + await assertSuccess(config, provider, "scalar") + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) - }) + it("reports oneOf ambiguity without exposing the internal cursor", async () => { + const config = Config.schema( + Schema.Union([ + Schema.Struct({ child: Schema.String }), + Schema.String + ], { mode: "oneOf" }), + ["database", "value"] + ) + const provider = ConfigProvider.fromEnv({ + env: { + database_value: "scalar", + database_value_child: "object" + } + }) - it("optionalKey properties", async () => { - const schema = Schema.Struct({ a: Schema.optionalKey(Schema.Number) }) - const config = Config.schema(schema) + await assertFailure( + config, + provider, + `Expected exactly one member to match + at ["database"]["value"]` + ) + }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: {} }), {}) + it.effect("counts input available to any member when composing with Config.all", () => + Effect.gen(function*() { + const config = Config.all({ + selected: Config.schema( + Schema.Union([ + Schema.Undefined, + Schema.Struct({ child: Schema.String }) + ]), + "value" + ), + required: Config.string("required") + }).pipe( + Config.withDefault({ + selected: undefined, + required: "default" + }) + ) + const provider = ConfigProvider.fromEnv({ + env: { + value_child: "present" + } + }) + + const error = yield* config.parse(provider).pipe(Effect.flip) + assert.strictEqual( + error.cause.message, + `Expected string + at ["required"]` + ) + })) + + it.effect("applies checks attached to the original union", () => + Effect.gen(function*() { + const schema = Schema.Union([ + Schema.Struct({ child: Schema.String }), + Schema.String + ]).check( + Schema.makeFilter((value) => + typeof value === "string" + ? new SchemaIssue.InvalidValue({ message: "union check failed" }) + : undefined + ) + ) + const error = yield* Config.schema(schema, "value").parse( + ConfigProvider.fromUnknown({ value: "scalar" }) + ).pipe(Effect.flip) + + assert.strictEqual( + error.cause.message, + `union check failed + at ["value"]` + ) + })) + + it.effect("propagates SourceError defects instead of trying another member", () => + Effect.gen(function*() { + const sourceError = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make((path) => { + if (path.length === 1 && path[0] === "value") { + return Effect.succeed(ConfigProvider.makeRecord(new Set(["child"]), "scalar")) + } + return Effect.fail(sourceError) + }) + const config = Config.schema( + Schema.Union([ + Schema.Struct({ child: Schema.String }), + Schema.String + ]), + "value" + ) + + const error = yield* config.parse(provider).pipe(Effect.flip) + assert.strictEqual(error.cause, sourceError) + })) }) + }) - it("optional properties", async () => { - const config = Config.schema( - Schema.Struct({ a: Schema.optional(Schema.Number) }) + describe("fromEnv provider", () => { + it("loads root, flat, and nested paths", async () => { + await assertSuccess( + Config.schema(Schema.String, "a"), + ConfigProvider.fromEnv({ env: { a: "value" } }), + "value" + ) + await assertSuccess( + Config.schema(Schema.String, ["a", "b"]), + ConfigProvider.fromEnv({ env: { "a_b": "value" } }), + "value" + ) + await assertSuccess( + Config.schema(Schema.UndefinedOr(Schema.String)), + ConfigProvider.fromEnv({ env: {} }), + undefined + ) + await assertSuccess( + Config.schema(Schema.UndefinedOr(Schema.String), "a"), + ConfigProvider.fromEnv({ env: {} }), + undefined ) - - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: {} }), {}) }) - it("literal property", async () => { - const schema = Schema.Struct({ a: Schema.Literals(["b", "c"]) }) - const config = Config.schema(schema) + describe("node precedence", () => { + it("uses a co-located scalar instead of object children for a leaf schema", async () => { + const schema = Schema.Struct({ a: Schema.Number }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_b": "2" } }), { a: 1 }) + }) + + it("uses a co-located scalar instead of array children for a leaf schema", async () => { + const schema = Schema.Struct({ a: Schema.Number }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_0": "2" } }), { a: 1 }) + }) + + it("prefers object children when a node can be both object and array", async () => { + const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.Number }) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "b" } }), { a: "b" }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "c" } }), { a: "c" }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", "a_b": "2", "a_0": "3" } }), { + a: { b: 2 } + }) + }) }) - it("array property", async () => { - const schema = Schema.Struct({ a: Schema.Array(Schema.Number) }) - const config = Config.schema(schema) + it("decodes Null", async () => { + const schema = Schema.Null + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), { a: [] }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: [1] }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1" } }), { a: [1] }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "2" } }), { a: [1, 2] }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", a_0: "2" } }), { a: [1] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "null" } }), null) await assertFailure( config, ConfigProvider.fromEnv({ env: {} }), - `Missing key + `Expected "null" at ["a"]` ) }) - }) - it("Record(String, Finite)", async () => { - const schema = Schema.Record(Schema.String, Schema.Finite) - const config = Config.schema(schema) + it("decodes String and reports absence", async () => { + const schema = Schema.String + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", b: "2" } }), { a: 1, b: 2 }) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: { a: "1", b: "value" } }), - `Expected a string representing a finite number, got "value" - at ["b"]` - ) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") + await assertFailure( + config, + ConfigProvider.fromEnv({ env: {} }), + `Expected string + at ["a"]` + ) + }) - describe("Tuple", () => { - it("empty", async () => { - const schema = Schema.Struct({ a: Schema.Tuple([]) }) - const config = Config.schema(schema) + it("decodes Number and reports absence", async () => { + const schema = Schema.Number + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), { a: [] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: {} }), + `Expected string | "Infinity" | "-Infinity" | "NaN" + at ["a"]` + ) }) - it("ensure array", async () => { - const schema = Schema.Struct({ a: Schema.Tuple([Schema.Number]) }) - const config = Config.schema(schema) + it("decodes Finite and reports absence", async () => { + const schema = Schema.Finite + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: [1] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: {} }), + `Expected string + at ["a"]` + ) }) - it("required elements", async () => { - const schema = Schema.Struct({ a: Schema.Tuple([Schema.String, Schema.Finite]) }) - const config = Config.schema(schema) + it("decodes Int and reports absence", async () => { + const schema = Schema.Int + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "a", a_1: "2" } }), { a: ["a", 2] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) await assertFailure( config, - ConfigProvider.fromEnv({ env: { a: "a" } }), - `Missing key - at ["a"][1]` + ConfigProvider.fromEnv({ env: {} }), + `Expected string + at ["a"]` ) + }) + + it("decodes Boolean and reports absence", async () => { + const schema = Schema.Boolean + const config = Config.schema(schema, "a") + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "true" } }), true) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "false" } }), false) await assertFailure( config, - ConfigProvider.fromEnv({ env: { a_0: "a", a_1: "value" } }), - `Expected a string representing a finite number, got "value" - at ["a"][1]` + ConfigProvider.fromEnv({ env: {} }), + `Expected "true" | "false" + at ["a"]` ) }) - }) - it("Array(Finite)", async () => { - const schema = Schema.Struct({ a: Schema.Array(Schema.Finite) }) - const config = Config.schema(schema) + describe("Struct", () => { + it("decodes required properties", async () => { + const schema = Schema.Struct({ a: Schema.Number }) + const config = Config.schema(schema) - // ensure array - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1,2,3" } }), { a: [1, 2, 3] }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "2" } }), { a: [1, 2] }) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: { a: "a", a_0: "1" } }), - `Expected a string representing a finite number, got "a" - at ["a"][0]` - ) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: { a_0: "1", a_2: "2" } }), - `Expected string, got undefined - at ["a"][1]` - ) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "value" } }), - `Expected a string representing a finite number, got "value" - at ["a"][1]` - ) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) + }) - describe("Union", () => { - describe("Literals", () => { - it("string", async () => { - const schema = Schema.Struct({ a: Schema.Literals(["a", "b"]) }) + it("omits absent optionalKey properties", async () => { + const schema = Schema.Struct({ a: Schema.optionalKey(Schema.Number) }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { unrelated: "value" } }), {}) + }) + + it("omits absent optional properties", async () => { + const config = Config.schema( + Schema.Struct({ a: Schema.optional(Schema.Number) }) + ) + + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { unrelated: "value" } }), {}) + }) + + it("decodes literal properties", async () => { + const schema = Schema.Struct({ a: Schema.Literals(["b", "c"]) }) const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "b" } }), { a: "b" }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "c" } }), { a: "c" }) }) - }) - it("inclusive", async () => { - const schema = Schema.Union([ - Schema.Struct({ a: Schema.String }), - Schema.Struct({ b: Schema.Number }) - ]) - const config = Config.schema(schema) + it("decodes indexed array properties without treating co-located scalars as arrays", async () => { + const schema = Schema.Struct({ a: Schema.Array(Schema.Number) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { b: "1" } }), { b: 1 }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a", b: "1" } }), { a: "a" }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), + `Expected array + at ["a"]` + ) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "1" } }), + `Expected array + at ["a"]` + ) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1" } }), { a: [1] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "2" } }), { a: [1, 2] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", a_0: "2" } }), { a: [2] }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: {} }), + `Expected object` + ) + }) }) - it("exclusive", async () => { - const schema = Schema.Union([ - Schema.Struct({ a: Schema.String }), - Schema.Struct({ b: Schema.Number }) - ], { mode: "oneOf" }) + it("decodes and validates Record values", async () => { + const schema = Schema.Record(Schema.String, Schema.Finite) const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { b: "1" } }), { b: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1", b: "2" } }), { a: 1, b: 2 }) await assertFailure( config, - ConfigProvider.fromEnv({ env: { a: "a", b: "1" } }), - `Expected exactly one member to match the input {"a":"a","b":"1"}` + ConfigProvider.fromEnv({ env: { a: "1", b: "value" } }), + `Expected a string representing a finite number + at ["b"]` ) }) - it("number | string", async () => { - const schema = Schema.Union([Schema.Number, Schema.String]) - const config = Config.schema(schema, "a") - - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") - }) - - it("string | number", async () => { - const schema = Schema.Union([Schema.String, Schema.Number]) - const config = Config.schema(schema, "a") - - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") - }) - }) - - it("Suspend", async () => { - interface A { - readonly a: string - readonly as: ReadonlyArray - } - const schema = Schema.Struct({ - a: Schema.String, - as: Schema.Array(Schema.suspend((): Schema.Codec => schema)) - }) - const config = Config.schema(schema) + describe("Tuple", () => { + it("rejects a scalar where an empty tuple is expected", async () => { + const schema = Schema.Struct({ a: Schema.Tuple([]) }) + const config = Config.schema(schema) - await assertSuccess( - config, - ConfigProvider.fromEnv({ env: { a: "1", as: "" }, preserveEmptyStrings: true }), - { a: "1", as: [] } - ) - await assertSuccess( - config, - ConfigProvider.fromEnv({ env: { a: "1", as_0_a: "2", as_0_as: "" }, preserveEmptyStrings: true }), - { - a: "1", - as: [{ a: "2", as: [] }] - } - ) - }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "" }, preserveEmptyStrings: true }), + `Expected array + at ["a"]` + ) + }) - it("Redacted(Int)", async () => { - const schema = Schema.Redacted(Schema.Int) - const config = Config.schema(schema, "a") + it("rejects scalar tuple input", async () => { + const schema = Schema.Struct({ a: Schema.Tuple([Schema.Number]) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), Redacted.make(1)) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: {} }), - `Invalid data - at ["a"]` - ) - await assertFailure( - config, - ConfigProvider.fromEnv({ env: { a: "1.1" } }), - `Invalid data + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "1" } }), + `Expected array at ["a"]` - ) - }) - }) - - describe("fromUnknown", () => { - it("path argument", async () => { - await assertSuccess( - Config.schema(Schema.String, []), - ConfigProvider.fromUnknown("value"), - "value" - ) - await assertSuccess( - Config.schema(Schema.String, "a"), - ConfigProvider.fromUnknown({ a: "value" }), - "value" - ) - await assertSuccess( - Config.schema(Schema.String, ["a", "b"]), - ConfigProvider.fromUnknown({ a: { b: "value" } }), - "value" - ) - }) + ) + }) - it("Undefined", async () => { - const schema = Schema.Undefined - const config = Config.schema(schema) + it("requires and validates every tuple element", async () => { + const schema = Schema.Struct({ a: Schema.Tuple([Schema.String, Schema.Finite]) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown(undefined), undefined) - await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected undefined, got "a"`) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "a", a_1: "2" } }), { a: ["a", 2] }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "a" } }), + `Expected array + at ["a"]` + ) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a_0: "a", a_1: "value" } }), + `Expected a string representing a finite number + at ["a"][1]` + ) + }) + }) - it("Null", async () => { - const schema = Schema.Null - const config = Config.schema(schema) + it("decodes indexed Array input and rejects scalar input", async () => { + const schema = Schema.Struct({ a: Schema.Array(Schema.Finite) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("null"), null) - await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected "null", got "a"`) - }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "1,2,3" } }), + `Expected array + at ["a"]` + ) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "2" } }), { a: [1, 2] }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a", a_0: "1" } }), { a: [1] }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a_0: "1", a_2: "2" } }), + `Expected string + at ["a"][1]` + ) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a_0: "1", a_1: "value" } }), + `Expected a string representing a finite number + at ["a"][1]` + ) + }) - it("String", async () => { - const schema = Schema.String - const config = Config.schema(schema) + describe("Union", () => { + it("decodes literal unions", async () => { + const schema = Schema.Struct({ a: Schema.Literals(["a", "b"]) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("value"), "value") - await assertFailure(config, ConfigProvider.fromUnknown({}), `Expected string, got undefined`) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "b" } }), { a: "b" }) + }) - it("Number", async () => { - const schema = Schema.Number - const config = Config.schema(schema) + it("uses first-match semantics by default", async () => { + const schema = Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) + ]) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) - await assertFailure( - config, - ConfigProvider.fromUnknown("a"), - `Expected a string representing a finite number, got "a" -Expected "Infinity" | "-Infinity" | "NaN", got "a"` - ) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { b: "1" } }), { b: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a", b: "1" } }), { a: "a" }) + }) - it("Finite", async () => { - const schema = Schema.Finite - const config = Config.schema(schema) + it("enforces exactly one match in oneOf mode", async () => { + const schema = Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) + ], { mode: "oneOf" }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) - await assertFailure( - config, - ConfigProvider.fromUnknown("a"), - `Expected a string representing a finite number, got "a"` - ) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), { a: "a" }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { b: "1" } }), { b: 1 }) + await assertFailure( + config, + ConfigProvider.fromEnv({ env: { a: "a", b: "1" } }), + "Expected exactly one member to match" + ) + }) - it("Int", async () => { - const schema = Schema.Int - const config = Config.schema(schema) + it("decodes Number before String", async () => { + const schema = Schema.Union([Schema.Number, Schema.String]) + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) - await assertFailure( - config, - ConfigProvider.fromUnknown("a"), - `Expected a string representing a finite number, got "a"` - ) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") + }) - it("Boolean", async () => { - const schema = Schema.Boolean - const config = Config.schema(schema) + it("still decodes numeric input when String is listed first", async () => { + const schema = Schema.Union([Schema.String, Schema.Number]) + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromUnknown("true"), true) - await assertSuccess(config, ConfigProvider.fromUnknown("false"), false) - await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected "true" | "false", got "a"`) - }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), 1) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "a" } }), "a") + }) + }) - describe("Struct", () => { - it("required properties", async () => { - const schema = Schema.Struct({ a: Schema.Finite }) - const config = Config.schema(schema) + it("reports Int validation errors", async () => { + const schema = Schema.Redacted(Schema.Int) + const config = Config.schema(schema, "a") - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromEnv({ env: { a: "1" } }), Redacted.make(1)) await assertFailure( config, - ConfigProvider.fromUnknown({}), - `Missing key + ConfigProvider.fromEnv({ env: {} }), + `Expected string at ["a"]` ) await assertFailure( config, - ConfigProvider.fromUnknown({ a: "value" }), - `Expected a string representing a finite number, got "value" + ConfigProvider.fromEnv({ env: { a: "1.1" } }), + `Expected an integer at ["a"]` ) }) + }) - it("optionalKey properties", async () => { - const schema = Schema.Struct({ a: Schema.optionalKey(Schema.Number) }) + describe("fromUnknown provider", () => { + it("loads root, flat, and nested paths", async () => { + await assertSuccess( + Config.schema(Schema.String, []), + ConfigProvider.fromUnknown("value"), + "value" + ) + await assertSuccess( + Config.schema(Schema.String, "a"), + ConfigProvider.fromUnknown({ a: "value" }), + "value" + ) + await assertSuccess( + Config.schema(Schema.String, ["a", "b"]), + ConfigProvider.fromUnknown({ a: { b: "value" } }), + "value" + ) + }) + + it("decodes Undefined", async () => { + const schema = Schema.Undefined const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromUnknown({}), {}) + await assertSuccess(config, ConfigProvider.fromUnknown(undefined), undefined) + await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected undefined`) }) - it("optional properties", async () => { - const config = Config.schema( - Schema.Struct({ a: Schema.optional(Schema.Number) }) - ) + it("decodes Null", async () => { + const schema = Schema.Null + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromUnknown({}), {}) + await assertSuccess(config, ConfigProvider.fromUnknown("null"), null) + await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected "null"`) }) - it("literal property", async () => { - const schema = Schema.Struct({ a: Schema.Literals(["b", "c"]) }) + it("decodes String and rejects object input", async () => { + const schema = Schema.String const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "b" }), { a: "b" }) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "c" }), { a: "c" }) + await assertSuccess(config, ConfigProvider.fromUnknown("value"), "value") + await assertFailure(config, ConfigProvider.fromUnknown({}), `Expected string`) }) - it("array property", async () => { - const schema = Schema.Struct({ a: Schema.Array(Schema.Number) }) + it("decodes Number and rejects invalid input", async () => { + const schema = Schema.Number const config = Config.schema(schema) + await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) await assertFailure( config, - ConfigProvider.fromUnknown({ a: "" }), - `Missing key - at ["a"]` - ) - await assertSuccess( - config, - ConfigProvider.fromUnknown({ a: "" }, { preserveEmptyStrings: true }), - { a: [] } + ConfigProvider.fromUnknown("a"), + `Expected a string representing a finite number +Expected "Infinity" | "-Infinity" | "NaN"` ) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: [1] }) }) - }) - - it("Record(String, Finite)", async () => { - const schema = Schema.Record(Schema.String, Schema.Finite) - const config = Config.schema(schema) - - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", b: "2" }), { a: 1, b: 2 }) - await assertFailure( - config, - ConfigProvider.fromUnknown({ a: "1", b: "value" }), - `Expected a string representing a finite number, got "value" - at ["b"]` - ) - }) - describe("Tuple", () => { - it("ensure array", async () => { - const schema = Schema.Tuple([Schema.Number]) + it("decodes Finite and rejects invalid input", async () => { + const schema = Schema.Finite const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown(["1"]), [1]) - await assertSuccess(config, ConfigProvider.fromUnknown("1"), [1]) + await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) + await assertFailure( + config, + ConfigProvider.fromUnknown("a"), + `Expected a string representing a finite number` + ) }) - it("required elements", async () => { - const schema = Schema.Tuple([Schema.String, Schema.Finite]) + it("decodes Int and rejects invalid input", async () => { + const schema = Schema.Int const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown(["a", "2"]), ["a", 2]) - await assertFailure( - config, - ConfigProvider.fromUnknown(["a"]), - `Missing key - at [1]` - ) + await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) await assertFailure( config, - ConfigProvider.fromUnknown(["a", "value"]), - `Expected a string representing a finite number, got "value" - at [1]` + ConfigProvider.fromUnknown("a"), + `Expected a string representing a finite number` ) }) - }) - it("Array(Finite)", async () => { - const schema = Schema.Array(Schema.Finite) - const config = Config.schema(schema) + it("decodes Boolean and rejects invalid input", async () => { + const schema = Schema.Boolean + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown(["1"]), [1]) - // ensure array - await assertSuccess(config, ConfigProvider.fromUnknown("1"), [1]) - await assertSuccess(config, ConfigProvider.fromUnknown(["1", "2"]), [1, 2]) - await assertFailure( - config, - ConfigProvider.fromUnknown(["1", "value"]), - `Expected a string representing a finite number, got "value" - at [1]` - ) - }) + await assertSuccess(config, ConfigProvider.fromUnknown("true"), true) + await assertSuccess(config, ConfigProvider.fromUnknown("false"), false) + await assertFailure(config, ConfigProvider.fromUnknown("a"), `Expected "true" | "false"`) + }) - describe("Union", () => { - describe("Literals", () => { - it("string", async () => { - const schema = Schema.Literals(["a", "b"]) + describe("Struct", () => { + it("requires and validates required properties", async () => { + const schema = Schema.Struct({ a: Schema.Finite }) const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") - await assertSuccess(config, ConfigProvider.fromUnknown("b"), "b") + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) + await assertFailure( + config, + ConfigProvider.fromUnknown({}), + `Missing key + at ["a"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "value" }), + `Expected a string representing a finite number + at ["a"]` + ) }) - }) - it("inclusive", async () => { - const schema = Schema.Union([ - Schema.Struct({ a: Schema.String }), - Schema.Struct({ b: Schema.Number }) - ]) - const config = Config.schema(schema) + it("omits absent optionalKey properties", async () => { + const schema = Schema.Struct({ a: Schema.optionalKey(Schema.Number) }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromUnknown({}), {}) + }) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a" }), { a: "a" }) - await assertSuccess(config, ConfigProvider.fromUnknown({ b: "1" }), { b: 1 }) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a", b: "1" }), { a: "a" }) + it("omits absent optional properties", async () => { + const config = Config.schema( + Schema.Struct({ a: Schema.optional(Schema.Number) }) + ) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromUnknown({}), {}) + }) + + it("decodes literal properties", async () => { + const schema = Schema.Struct({ a: Schema.Literals(["b", "c"]) }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "b" }), { a: "b" }) + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "c" }), { a: "c" }) + }) + + it("rejects scalar values for array properties", async () => { + const schema = Schema.Struct({ a: Schema.Array(Schema.Number) }) + const config = Config.schema(schema) + + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "" }), + `Missing key + at ["a"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "" }, { preserveEmptyStrings: true }), + `Expected array + at ["a"]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "1" }), + `Expected array + at ["a"]` + ) + }) }) - it("exclusive", async () => { - const schema = Schema.Union([ - Schema.Struct({ a: Schema.String }), - Schema.Struct({ b: Schema.Number }) - ], { mode: "oneOf" }) + it("decodes and validates Record values", async () => { + const schema = Schema.Record(Schema.String, Schema.Finite) const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a" }), { a: "a" }) - await assertSuccess(config, ConfigProvider.fromUnknown({ b: "1" }), { b: 1 }) + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: 1 }) + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", b: "2" }), { a: 1, b: 2 }) await assertFailure( config, - ConfigProvider.fromUnknown({ a: "a", b: "1" }), - `Expected exactly one member to match the input {"a":"a","b":"1"}` + ConfigProvider.fromUnknown({ a: "1", b: "value" }), + `Expected a string representing a finite number + at ["b"]` ) }) - it("number | string", async () => { - const schema = Schema.Union([Schema.Number, Schema.String]) - const config = Config.schema(schema) + describe("Tuple", () => { + it("accepts array tuple input and rejects scalar input", async () => { + const schema = Schema.Tuple([Schema.Number]) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) - await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") + await assertSuccess(config, ConfigProvider.fromUnknown(["1"]), [1]) + await assertFailure(config, ConfigProvider.fromUnknown("1"), `Expected array`) + }) + + it("requires and validates every tuple element", async () => { + const schema = Schema.Tuple([Schema.String, Schema.Finite]) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown(["a", "2"]), ["a", 2]) + await assertFailure( + config, + ConfigProvider.fromUnknown(["a"]), + `Missing key + at [1]` + ) + await assertFailure( + config, + ConfigProvider.fromUnknown(["a", "value"]), + `Expected a string representing a finite number + at [1]` + ) + }) }) - it("string | number", async () => { - const schema = Schema.Union([Schema.String, Schema.Number]) + it("accepts array input and rejects scalar Array input", async () => { + const schema = Schema.Array(Schema.Finite) const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) - await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") + await assertSuccess(config, ConfigProvider.fromUnknown(["1"]), [1]) + await assertFailure(config, ConfigProvider.fromUnknown("1"), `Expected array`) + await assertSuccess(config, ConfigProvider.fromUnknown(["1", "2"]), [1, 2]) + await assertFailure( + config, + ConfigProvider.fromUnknown(["1", "value"]), + `Expected a string representing a finite number + at [1]` + ) }) - }) - it("Suspend", async () => { - interface A { - readonly a: string - readonly as: ReadonlyArray - } - const schema = Schema.Struct({ - a: Schema.String, - as: Schema.Array(Schema.suspend((): Schema.Codec => schema)) + describe("Union", () => { + it("decodes literal unions", async () => { + const schema = Schema.Literals(["a", "b"]) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") + await assertSuccess(config, ConfigProvider.fromUnknown("b"), "b") + }) + + it("uses first-match semantics by default", async () => { + const schema = Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) + ]) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a" }), { a: "a" }) + await assertSuccess(config, ConfigProvider.fromUnknown({ b: "1" }), { b: 1 }) + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a", b: "1" }), { a: "a" }) + }) + + it("enforces exactly one match in oneOf mode", async () => { + const schema = Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.Number }) + ], { mode: "oneOf" }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "a" }), { a: "a" }) + await assertSuccess(config, ConfigProvider.fromUnknown({ b: "1" }), { b: 1 }) + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "a", b: "1" }), + "Expected exactly one member to match" + ) + }) + + it("decodes Number before String", async () => { + const schema = Schema.Union([Schema.Number, Schema.String]) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) + await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") + }) + + it("still decodes numeric input when String is listed first", async () => { + const schema = Schema.Union([Schema.String, Schema.Number]) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown("1"), 1) + await assertSuccess(config, ConfigProvider.fromUnknown("a"), "a") + }) }) - const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", as: [] }), { a: "1", as: [] }) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", as: [{ a: "2", as: [] }] }), { - a: "1", - as: [{ a: "2", as: [] }] + it("decodes recursive suspended schemas", async () => { + interface A { + readonly a: string + readonly as: ReadonlyArray + } + const schema = Schema.Struct({ + a: Schema.String, + as: Schema.Array(Schema.suspend((): Schema.Codec => schema)) + }) + const config = Config.schema(schema) + + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", as: [] }), { a: "1", as: [] }) + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1", as: [{ a: "2", as: [] }] }), { + a: "1", + as: [{ a: "2", as: [] }] + }) }) - }) - it("Redacted(Int)", async () => { - const schema = Schema.Struct({ a: Schema.Redacted(Schema.Int) }) - const config = Config.schema(schema) + it("reports nested Int validation errors", async () => { + const schema = Schema.Struct({ a: Schema.Redacted(Schema.Int) }) + const config = Config.schema(schema) - await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: Redacted.make(1) }) - await assertFailure( - config, - ConfigProvider.fromUnknown({}), - `Missing key + await assertSuccess(config, ConfigProvider.fromUnknown({ a: "1" }), { a: Redacted.make(1) }) + await assertFailure( + config, + ConfigProvider.fromUnknown({}), + `Missing key at ["a"]` - ) - await assertFailure( - config, - ConfigProvider.fromUnknown({ a: "1.1" }), - `Invalid data + ) + await assertFailure( + config, + ConfigProvider.fromUnknown({ a: "1.1" }), + `Expected an integer at ["a"]` - ) - }) + ) + }) - it("URL", async () => { - const schema = Schema.Struct({ a: Schema.URL }) - const config = Config.schema(schema) + it("decodes nested URL values", async () => { + const schema = Schema.Struct({ a: Schema.URL }) + const config = Config.schema(schema) - await assertSuccess( - config, - ConfigProvider.fromUnknown({ a: "https://example.com" }), - { a: new URL("https://example.com") } - ) + await assertSuccess( + config, + ConfigProvider.fromUnknown({ a: "https://example.com" }), + { a: new URL("https://example.com") } + ) + }) }) }) }) diff --git a/.context/effect/packages/effect/test/ConfigProvider.test.ts b/.context/effect/packages/effect/test/ConfigProvider.test.ts index bc5c7207f..c2bcaa9fd 100644 --- a/.context/effect/packages/effect/test/ConfigProvider.test.ts +++ b/.context/effect/packages/effect/test/ConfigProvider.test.ts @@ -2,191 +2,316 @@ import { describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" import { ConfigProvider, Effect, FileSystem, Layer, Path, PlatformError, Result } from "effect" +const notFound = (method: string): PlatformError.PlatformError => + PlatformError.systemError({ + module: "FileSystem", + _tag: "NotFound", + method + }) + async function assertSuccess( provider: ConfigProvider.ConfigProvider, path: ConfigProvider.Path, - expected: ConfigProvider.Node | undefined + expected: ConfigProvider.Node ) { const r = Effect.result(provider.load(path)) deepStrictEqual(await Effect.runPromise(r), Result.succeed(expected)) } -// async function assertFailure( -// provider: ConfigProvider.ConfigProvider, -// path: ConfigProvider.Path, -// expected: ConfigProvider.SourceError -// ) { -// const r = Effect.result(provider.load(path)) -// deepStrictEqual(await Effect.runPromise(r), Result.fail(expected)) -// } +async function assertMissing( + provider: ConfigProvider.ConfigProvider, + path: ConfigProvider.Path +) { + const r = Effect.result(provider.load(path)) + deepStrictEqual(await Effect.runPromise(r), Result.succeed(undefined)) +} + +async function assertFailure( + provider: ConfigProvider.ConfigProvider, + path: ConfigProvider.Path, + expected: ConfigProvider.SourceError +) { + const r = Effect.result(provider.load(path)) + deepStrictEqual(await Effect.runPromise(r), Result.fail(expected)) +} describe("ConfigProvider", () => { - it("orElse", async () => { - const provider1 = ConfigProvider.fromEnv({ - env: { - "A": "value1" - } + describe("make", () => { + it.effect("exposes lookup absence and input transformation as provider behavior", () => + Effect.gen(function*() { + const provider = ConfigProvider.fromUnknown({ + APP: { + PORT: "3000" + } + }) + const nested = provider.mapInput((path) => ["APP", ...path]) + + deepStrictEqual( + yield* nested.load(["PORT"]), + ConfigProvider.makeValue("3000") + ) + deepStrictEqual(yield* nested.load(["MISSING"]), undefined) + })) + + it("creates a provider from a lookup function", async () => { + const provider = ConfigProvider.make((path) => + Effect.succeed( + path.join(".") === "A.B" + ? ConfigProvider.makeValue("value") + : undefined + ) + ) + + await assertSuccess(provider, ["A", "B"], ConfigProvider.makeValue("value")) + await assertMissing(provider, ["missing"]) }) - const provider2 = ConfigProvider.fromEnv({ - env: { - "B": "value2" - } + + it("preserves SourceError failures", async () => { + const error = new ConfigProvider.SourceError({ message: "source unavailable" }) + const provider = ConfigProvider.make(() => Effect.fail(error)) + + await assertFailure(provider, ["A"], error) }) - const provider = provider1.pipe(ConfigProvider.orElse(provider2)) - await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) - await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value2")) }) - it("orElse does not fall back on SourceError", async () => { - const error = new ConfigProvider.SourceError({ message: "io down" }) - const primary = ConfigProvider.make(() => Effect.fail(error)) - const fallback = ConfigProvider.fromEnv({ env: { KEY: "fallback" } }) - const provider = primary.pipe(ConfigProvider.orElse(fallback)) - const r = await Effect.runPromise(Effect.result(provider.load(["KEY"]))) - deepStrictEqual(r, Result.fail(error)) - }) + describe("combinators", () => { + describe("orElse", () => { + it("uses the fallback when the primary provider is missing the path", async () => { + const primary = ConfigProvider.fromEnv({ + env: { + "A": "value1" + } + }) + const fallback = ConfigProvider.fromEnv({ + env: { + "B": "value2" + } + }) + const provider = ConfigProvider.orElse(primary, fallback) + await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) + await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value2")) + }) - it("orElse applies each operand's transformations", async () => { - const primary = ConfigProvider.fromEnv({ - env: { - "DATABASE_HOST": "from-env" - } - }).pipe(ConfigProvider.constantCase) - const fallback = ConfigProvider.fromEnv({ - env: { - "APP_PORT": "3000" - } - }).pipe(ConfigProvider.nested("APP")) - const provider = primary.pipe(ConfigProvider.orElse(fallback)) - await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("from-env")) - await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("3000")) - }) + it("does not use the fallback after a SourceError", async () => { + const error = new ConfigProvider.SourceError({ message: "io down" }) + const primary = ConfigProvider.make(() => Effect.fail(error)) + const fallback = ConfigProvider.fromEnv({ env: { KEY: "fallback" } }) + const provider = primary.pipe(ConfigProvider.orElse(fallback)) + await assertFailure(provider, ["KEY"], error) + }) - it("mapInput distributes over orElse", async () => { - const appendSuffix = ConfigProvider.mapInput((path) => - path.map((seg) => typeof seg === "string" ? `${seg}_SUFFIX` : seg) - ) - const primary = ConfigProvider.fromEnv({ - env: { - "prefix_SUFFIX_KEY_SUFFIX": "primary" - } - }).pipe(ConfigProvider.nested("prefix")) - const fallback = ConfigProvider.fromEnv({ - env: { - "fallback_SUFFIX_KEY_SUFFIX": "fallback", - "fallback_SUFFIX_OTHER_SUFFIX": "fallback" - } - }).pipe(ConfigProvider.nested("fallback")) - const provider = primary.pipe(ConfigProvider.orElse(fallback), appendSuffix) - await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("primary")) - await assertSuccess(provider, ["OTHER"], ConfigProvider.makeValue("fallback")) - }) + it("does not evaluate the fallback after the primary provider finds a node", async () => { + const primary = ConfigProvider.fromUnknown({ KEY: "primary" }) + const fallback = ConfigProvider.make(() => + Effect.fail(new ConfigProvider.SourceError({ message: "fallback evaluated" })) + ) + const provider = primary.pipe(ConfigProvider.orElse(fallback)) - it("nested distributes over orElse", async () => { - const primary = ConfigProvider.fromEnv({ - env: { - "app_DATABASE_HOST": "primary" - } - }).pipe(ConfigProvider.constantCase) - const fallback = ConfigProvider.fromEnv({ - env: { - "app_PORT": "fallback" - } + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("primary")) + }) + + it("preserves each operand's transformations", async () => { + const primary = ConfigProvider.fromEnv({ + env: { + "DATABASE_HOST": "from-env" + } + }).pipe(ConfigProvider.constantCase) + const fallback = ConfigProvider.fromEnv({ + env: { + "APP_PORT": "3000" + } + }).pipe(ConfigProvider.nested("APP")) + const provider = primary.pipe(ConfigProvider.orElse(fallback)) + await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("from-env")) + await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("3000")) + }) + + it("distributes mapInput over both operands", async () => { + const appendSuffix = ConfigProvider.mapInput((path) => + path.map((seg) => typeof seg === "string" ? `${seg}_SUFFIX` : seg) + ) + const primary = ConfigProvider.fromEnv({ + env: { + "prefix_SUFFIX_KEY_SUFFIX": "primary" + } + }).pipe(ConfigProvider.nested("prefix")) + const fallback = ConfigProvider.fromEnv({ + env: { + "fallback_SUFFIX_KEY_SUFFIX": "fallback", + "fallback_SUFFIX_OTHER_SUFFIX": "fallback" + } + }).pipe(ConfigProvider.nested("fallback")) + const provider = primary.pipe(ConfigProvider.orElse(fallback), appendSuffix) + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("primary")) + await assertSuccess(provider, ["OTHER"], ConfigProvider.makeValue("fallback")) + }) + + it("distributes nested over both operands", async () => { + const primary = ConfigProvider.fromEnv({ + env: { + "app_DATABASE_HOST": "primary" + } + }).pipe(ConfigProvider.constantCase) + const fallback = ConfigProvider.fromEnv({ + env: { + "app_PORT": "fallback" + } + }) + const provider = primary.pipe(ConfigProvider.orElse(fallback), ConfigProvider.nested("app")) + await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("primary")) + await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("fallback")) + }) + + it("uses the fallback when the primary value is an empty string", async () => { + const primary = ConfigProvider.fromEnv({ + env: { + "KEY": "" + } + }) + const fallback = ConfigProvider.fromUnknown({ KEY: "fallback" }) + const provider = primary.pipe(ConfigProvider.orElse(fallback)) + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("fallback")) + }) + + it("does not merge a fallback container into an existing primary container", async () => { + const primary = ConfigProvider.fromUnknown({ primary: "value1" }) + const fallback = ConfigProvider.fromUnknown({ fallback: "value2" }) + const provider = primary.pipe(ConfigProvider.orElse(fallback)) + + await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["primary"]))) + await assertSuccess(provider, ["fallback"], ConfigProvider.makeValue("value2")) + }) }) - const provider = primary.pipe(ConfigProvider.orElse(fallback), ConfigProvider.nested("app")) - await assertSuccess(provider, ["databaseHost"], ConfigProvider.makeValue("primary")) - await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("fallback")) - }) - it("orElse falls back when the primary env value is empty", async () => { - const primary = ConfigProvider.fromEnv({ - env: { - "KEY": "" - } + describe("mapInput", () => { + it("composes transformations in application order", async () => { + const appendB = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_B" : sn)) + const provider = ConfigProvider.mapInput( + ConfigProvider.fromEnv({ + env: { + "KEY_A_B": "value" + } + }), + (path) => path.map((sn) => typeof sn === "string" ? sn + "_A" : sn) + ).pipe(appendB) + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) + }) }) - const fallback = ConfigProvider.fromUnknown({ KEY: "fallback" }) - const provider = primary.pipe(ConfigProvider.orElse(fallback)) - await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("fallback")) - }) - it("constantCase", async () => { - const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ - env: { - "CONSTANT_CASE": "value1" - } - })) - await assertSuccess(provider, ["constant.case"], ConfigProvider.makeValue("value1")) - }) + describe("constantCase", () => { + it("converts string path segments to config case", async () => { + const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ + env: { + "CONSTANT_CASE": "value1" + } + })) + await assertSuccess(provider, ["constant.case"], ConfigProvider.makeValue("value1")) + }) - it("constantCase uses config casing for numeric word groups", async () => { - const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ - env: { - "API_V2_XML": "value1", - "FIELD2_VALUE": "value2" - } - })) - await assertSuccess(provider, ["api-v2 xml"], ConfigProvider.makeValue("value1")) - await assertSuccess(provider, ["field2Value"], ConfigProvider.makeValue("value2")) - }) + it("preserves numeric word groups", async () => { + const provider = ConfigProvider.constantCase(ConfigProvider.fromEnv({ + env: { + "API_V2_XML": "value1", + "FIELD2_VALUE": "value2" + } + })) + await assertSuccess(provider, ["api-v2 xml"], ConfigProvider.makeValue("value1")) + await assertSuccess(provider, ["field2Value"], ConfigProvider.makeValue("value2")) + }) - describe("mapInput", () => { - it("two mappings", async () => { - const appendA = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_A" : sn)) - const appendB = ConfigProvider.mapInput((path) => path.map((sn) => typeof sn === "string" ? sn + "_B" : sn)) - const provider = ConfigProvider.fromEnv({ - env: { - "KEY_A_B": "value" - } - }).pipe(appendA, appendB) - await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) - }) - }) + it("leaves numeric path segments unchanged", async () => { + const provider = ConfigProvider.fromUnknown({ + ITEMS: ["value"] + }).pipe(ConfigProvider.constantCase) - describe("nested", () => { - it("should add a prefix to the path", async () => { - const provider = ConfigProvider.fromEnv({ - env: { - "prefix_A": "value" - } - }).pipe(ConfigProvider.nested("prefix")) - await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value")) + await assertSuccess(provider, ["items", 0], ConfigProvider.makeValue("value")) + }) }) - it("constantCase + nested", async () => { - const provider = ConfigProvider.fromEnv({ - env: { - "prefix_KEY_WITH_DOTS": "value" - } - }).pipe(ConfigProvider.constantCase, ConfigProvider.nested("prefix")) - await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) + describe("nested", () => { + it("adds a string prefix to the path", async () => { + const provider = ConfigProvider.fromEnv({ + env: { + "prefix_A": "value" + } + }).pipe(ConfigProvider.nested("prefix")) + await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value")) + }) + + it("adds a Path prefix with string and numeric segments", async () => { + const provider = ConfigProvider.nested( + ConfigProvider.fromUnknown({ + apps: [{ PORT: "3000" }] + }), + ["apps", 0] + ) + + await assertSuccess(provider, ["PORT"], ConfigProvider.makeValue("3000")) + }) + + it("preserves order when constantCase is applied before nested", async () => { + const provider = ConfigProvider.fromEnv({ + env: { + "prefix_KEY_WITH_DOTS": "value" + } + }).pipe(ConfigProvider.constantCase, ConfigProvider.nested("prefix")) + await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) + }) + + it("preserves order when constantCase is applied after nested", async () => { + const provider = ConfigProvider.fromEnv({ + env: { + "PREFIX_WITH_DOTS_KEY_WITH_DOTS": "value" + } + }).pipe(ConfigProvider.nested("prefix.with.dots"), ConfigProvider.constantCase) + await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) + }) + + it("composes multiple prefixes as wrappers", async () => { + const provider = ConfigProvider.fromEnv({ + env: { + "b_a_KEY": "value" + } + }).pipe(ConfigProvider.nested("a"), ConfigProvider.nested("b")) + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) + }) + + it("allows a later mapInput to transform the prefixed path", async () => { + const appendLeaf = ConfigProvider.mapInput((path) => [...path, "leaf"]) + const provider = ConfigProvider.fromEnv({ + env: { + "app_KEY_leaf": "value" + } + }).pipe(ConfigProvider.nested("app"), appendLeaf) + await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) + }) }) + }) - it("nested + constantCase", async () => { - const provider = ConfigProvider.fromEnv({ - env: { - "PREFIX_WITH_DOTS_KEY_WITH_DOTS": "value" - } - }).pipe(ConfigProvider.nested("prefix.with.dots"), ConfigProvider.constantCase) - await assertSuccess(provider, ["key.with.dots"], ConfigProvider.makeValue("value")) + describe("fromEnvRecord", () => { + it("reads defined values and skips undefined values", async () => { + const env: Record = { + DEFINED: "value", + UNDEFINED: undefined + } + const provider = ConfigProvider.fromEnvRecord(env) + + await assertSuccess(provider, ["DEFINED"], ConfigProvider.makeValue("value")) + await assertMissing(provider, ["UNDEFINED"]) + await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["DEFINED"]))) }) - it("multiple nested calls compose as wrappers", async () => { - const provider = ConfigProvider.fromEnv({ - env: { - "b_a_KEY": "value" - } - }).pipe(ConfigProvider.nested("a"), ConfigProvider.nested("b")) - await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) + it("treats empty strings as missing by default", async () => { + await assertMissing(ConfigProvider.fromEnvRecord({ EMPTY: "" }), ["EMPTY"]) }) - it("mapInput after nested transforms the full path", async () => { - const appendLeaf = ConfigProvider.mapInput((path) => [...path, "leaf"]) - const provider = ConfigProvider.fromEnv({ - env: { - "app_KEY_leaf": "value" - } - }).pipe(ConfigProvider.nested("app"), appendLeaf) - await assertSuccess(provider, ["KEY"], ConfigProvider.makeValue("value")) + it("preserves empty strings when requested", async () => { + const provider = ConfigProvider.fromEnvRecord( + { EMPTY: "" }, + { preserveEmptyStrings: true } + ) + + await assertSuccess(provider, ["EMPTY"], ConfigProvider.makeValue("")) }) }) @@ -197,10 +322,25 @@ describe("ConfigProvider", () => { await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) }) - it("missing key returns undefined", async () => { + it("missing key returns None", async () => { const env = { A: "value1" } const provider = ConfigProvider.fromEnv({ env }) - await assertSuccess(provider, ["missing"], undefined) + await assertMissing(provider, ["missing"]) + }) + + it("does not read inherited environment properties", async () => { + const provider = ConfigProvider.fromEnv({ env: {} }) + await assertMissing(provider, ["constructor"]) + }) + + it("does not traverse inherited trie properties", async () => { + delete (Object as any).children + const provider = ConfigProvider.fromEnv({ env: { constructor_X: "value" } }) + const polluted = Object.hasOwn(Object, "children") + delete (Object as any).children + + deepStrictEqual(polluted, false) + await assertSuccess(provider, ["constructor", "X"], ConfigProvider.makeValue("value")) }) it("treats empty string values as missing while preserving structure by default", async () => { @@ -208,7 +348,7 @@ describe("ConfigProvider", () => { const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A", "B"]))) - await assertSuccess(provider, ["A"], undefined) + await assertMissing(provider, ["A"]) await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value1")) }) @@ -249,7 +389,7 @@ describe("ConfigProvider", () => { const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(2)) - await assertSuccess(provider, ["A", 0], undefined) + await assertMissing(provider, ["A", 0]) await assertSuccess(provider, ["A", 1], ConfigProvider.makeValue("value1")) }) @@ -311,7 +451,7 @@ describe("ConfigProvider", () => { // max index is 2 => length 3 (sparse is allowed) await assertSuccess(provider, ["A"], ConfigProvider.makeArray(3, "root")) await assertSuccess(provider, ["A", 0], ConfigProvider.makeValue("value1")) - await assertSuccess(provider, ["A", 1], undefined) + await assertMissing(provider, ["A", 1]) await assertSuccess(provider, ["A", 2], ConfigProvider.makeValue("value3")) }) @@ -375,8 +515,8 @@ describe("ConfigProvider", () => { const provider = ConfigProvider.fromEnv({ env }) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("value1")) - await assertSuccess(provider, ["A", "B"], undefined) - await assertSuccess(provider, ["A_B"], undefined) + await assertMissing(provider, ["A", "B"]) + await assertMissing(provider, ["A_B"]) }) it("direct lookup and nested lookup both work for the same env var", async () => { @@ -500,12 +640,12 @@ describe("ConfigProvider", () => { }) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["a", "b", "record", "array"]))) - await assertSuccess(provider, ["a"], undefined) + await assertMissing(provider, ["a"]) await assertSuccess(provider, ["b"], ConfigProvider.makeValue("value1")) await assertSuccess(provider, ["record"], ConfigProvider.makeRecord(new Set(["key"]))) - await assertSuccess(provider, ["record", "key"], undefined) + await assertMissing(provider, ["record", "key"]) await assertSuccess(provider, ["array"], ConfigProvider.makeArray(1)) - await assertSuccess(provider, ["array", 0], undefined) + await assertMissing(provider, ["array", 0]) }) it("preserves empty string leaves when requested", async () => { @@ -531,14 +671,25 @@ describe("ConfigProvider", () => { await assertSuccess(provider, ["array", 2], ConfigProvider.makeArray(1)) }) - it("should return undefined on non-existing paths", async () => { - await assertSuccess(provider, ["string", "non-existing"], undefined) - await assertSuccess(provider, ["record", "non-existing"], undefined) - await assertSuccess(provider, ["array", 3, "non-existing"], undefined) + it("should return None on non-existing paths", async () => { + await assertMissing(provider, ["string", "non-existing"]) + await assertMissing(provider, ["record", "non-existing"]) + await assertMissing(provider, ["array", 3, "non-existing"]) + }) + + it("does not read inherited object properties", async () => { + const root = Object.assign(Object.create({ inherited: "value1" }), { + own: "value2" + }) + const provider = ConfigProvider.fromUnknown(root) + + await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["own"]))) + await assertMissing(provider, ["inherited"]) + await assertSuccess(provider, ["own"], ConfigProvider.makeValue("value2")) }) it("null values", async () => { - await assertSuccess(provider, ["null"], undefined) + await assertMissing(provider, ["null"]) }) it("number values", async () => { @@ -554,7 +705,7 @@ describe("ConfigProvider", () => { }) it("undefined values", async () => { - await assertSuccess(provider, ["undefined"], undefined) + await assertMissing(provider, ["undefined"]) }) it("unknown values", async () => { @@ -580,12 +731,21 @@ export NODE_ENV=production await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("production")) }) - it("quoting is allowed", async () => { + it("supports single, double, and backtick quoted values", async () => { const provider = ConfigProvider.fromDotEnvContents(` -NODE_ENV="production" +SINGLE='value # one' +DOUBLE="line 1\\nline 2" +BACKTICK=\`value # two\` `) - await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["NODE"]))) - await assertSuccess(provider, ["NODE_ENV"], ConfigProvider.makeValue("production")) + await assertSuccess(provider, ["SINGLE"], ConfigProvider.makeValue("value # one")) + await assertSuccess(provider, ["DOUBLE"], ConfigProvider.makeValue("line 1\nline 2")) + await assertSuccess(provider, ["BACKTICK"], ConfigProvider.makeValue("value # two")) + }) + + it("removes inline comments from unquoted values", async () => { + const provider = ConfigProvider.fromDotEnvContents("VALUE=value # comment") + + await assertSuccess(provider, ["VALUE"], ConfigProvider.makeValue("value")) }) it("objects are supported", async () => { @@ -605,7 +765,7 @@ A= B=value1 `) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A", "B"]))) - await assertSuccess(provider, ["A"], undefined) + await assertMissing(provider, ["A"]) await assertSuccess(provider, ["B"], ConfigProvider.makeValue("value1")) }) @@ -669,7 +829,7 @@ DB_PASS=$PASSWORD const provider = ConfigProvider.fromDotEnvContents( ` PASSWORD="value" -DB_PASS=$PASSWORD +DB_PASS=\${PASSWORD} `, { expandVariables: true } ) @@ -677,6 +837,29 @@ DB_PASS=$PASSWORD await assertSuccess(provider, ["PASSWORD"], ConfigProvider.makeValue("value")) await assertSuccess(provider, ["DB_PASS"], ConfigProvider.makeValue("value")) }) + + it("expansion defaults are used only for empty or unset variables", async () => { + const provider = ConfigProvider.fromDotEnvContents( + ` +SET=actual +EMPTY= +FROM_SET=\${SET:-fallback} +FROM_EMPTY=\${EMPTY:-fallback} +FROM_UNSET=\${UNSET:-fallback} +`, + { expandVariables: true } + ) + await assertSuccess(provider, ["FROM_SET"], ConfigProvider.makeValue("actual")) + await assertSuccess(provider, ["FROM_EMPTY"], ConfigProvider.makeValue("fallback")) + await assertSuccess(provider, ["FROM_UNSET"], ConfigProvider.makeValue("fallback")) + }) + + it("does not expand missing inherited variables", async () => { + const provider = ConfigProvider.fromDotEnvContents("VALUE=$constructor", { + expandVariables: true + }) + await assertMissing(provider, ["VALUE"]) + }) }) describe("fromDotEnv", () => { @@ -734,7 +917,7 @@ DB_PASS=$PASSWORD`) ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["A"]))) - await assertSuccess(provider, ["A"], undefined) + await assertMissing(provider, ["A"]) }) it("should support `preserveEmptyStrings` option", async () => { @@ -748,10 +931,27 @@ DB_PASS=$PASSWORD`) await assertSuccess(provider, ["A"], ConfigProvider.makeValue("")) }) + + it("fails with the FileSystem error when the file cannot be read", async () => { + const error = PlatformError.systemError({ + module: "FileSystem", + _tag: "PermissionDenied", + method: "readFileString" + }) + const result = await Effect.runPromise( + ConfigProvider.fromDotEnv().pipe( + Effect.provide(FileSystem.layerNoop({ + readFileString: () => Effect.fail(error) + })), + Effect.result + ) + ) + + deepStrictEqual(result, Result.fail(error)) + }) }) describe("fromDir", () => { - const provider = ConfigProvider.fromDir({ rootPath: "/" }) const files: Record = { "/secret": "keepitsafe\n", "/SHOUTING": "value", @@ -763,78 +963,24 @@ DB_PASS=$PASSWORD`) if (path in files) { return Effect.succeed(files[path]) } - return Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readFileString" - }) - ) + return Effect.fail(notFound("readFileString")) }, readDirectory(_path) { - // For the test, we only have files, no directories - return Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readDirectory" - }) - ) + return Effect.fail(notFound("readDirectory")) } }) const Platform = Layer.mergeAll(Fs, Path.layer) - const SetLayer = ConfigProvider.layer(provider).pipe( - Layer.provide(Platform), - Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ - env: { secret: "fail" } - }))) - ) - const AddLayer = ConfigProvider.layerAdd(provider).pipe( - Layer.provide(Platform), - Layer.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ - env: { - secret: "shh", - fallback: "value" - } - }))) - ) - - it("reads config", async () => { - const result = await Effect.runPromise( - Effect.gen(function*() { - const provider = yield* ConfigProvider.ConfigProvider - const secret = yield* provider.load(["secret"]) - const shouting = yield* provider.load(["SHOUTING"]) - const integer = yield* provider.load(["integer"]) - const nestedConfig = yield* provider.load(["nested", "config"]) - - return { secret, shouting, integer, nestedConfig } - }).pipe(Effect.provide(SetLayer)) - ) - deepStrictEqual(result.secret, ConfigProvider.makeValue("keepitsafe")) - deepStrictEqual(result.shouting, ConfigProvider.makeValue("value")) - deepStrictEqual(result.integer, ConfigProvider.makeValue("123")) - deepStrictEqual(result.nestedConfig, ConfigProvider.makeValue("hello")) - - const fallback = await Effect.runPromise( - Effect.gen(function*() { - const provider = yield* ConfigProvider.ConfigProvider - return yield* provider.load(["fallback"]) - }).pipe(Effect.provide(SetLayer)) - ) - - deepStrictEqual(fallback, undefined) - }) - - it("orElse falls back when fromDir path is missing", async () => { + it("reads trimmed file contents at root and nested paths", async () => { const provider = await Effect.runPromise( - ConfigProvider.fromDir({ rootPath: "/" }).pipe( - Effect.map((dir) => dir.pipe(ConfigProvider.orElse(ConfigProvider.fromEnv({ env: { fallback: "value" } })))), - Effect.provide(Platform) - ) + ConfigProvider.fromDir({ rootPath: "/" }).pipe(Effect.provide(Platform)) ) - await assertSuccess(provider, ["fallback"], ConfigProvider.makeValue("value")) + + await assertSuccess(provider, ["secret"], ConfigProvider.makeValue("keepitsafe")) + await assertSuccess(provider, ["SHOUTING"], ConfigProvider.makeValue("value")) + await assertSuccess(provider, ["integer"], ConfigProvider.makeValue("123")) + await assertSuccess(provider, ["nested", "config"], ConfigProvider.makeValue("hello")) + await assertMissing(provider, ["missing"]) }) it("treats empty files as missing by default", async () => { @@ -842,24 +988,12 @@ DB_PASS=$PASSWORD`) readFileString(path) { return path === "/empty" ? Effect.succeed("") - : Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readFileString" - }) - ) + : Effect.fail(notFound("readFileString")) }, readDirectory(path) { return path === "/" ? Effect.succeed(["empty"]) - : Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readDirectory" - }) - ) + : Effect.fail(notFound("readDirectory")) } }) const provider = await Effect.runPromise( @@ -867,7 +1001,7 @@ DB_PASS=$PASSWORD`) ) await assertSuccess(provider, [], ConfigProvider.makeRecord(new Set(["empty"]))) - await assertSuccess(provider, ["empty"], undefined) + await assertMissing(provider, ["empty"]) }) it("preserves empty files when requested", async () => { @@ -875,22 +1009,10 @@ DB_PASS=$PASSWORD`) readFileString(path) { return path === "/empty" ? Effect.succeed("\n") - : Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readFileString" - }) - ) + : Effect.fail(notFound("readFileString")) }, readDirectory() { - return Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readDirectory" - }) - ) + return Effect.fail(notFound("readDirectory")) } }) const provider = await Effect.runPromise( @@ -902,64 +1024,19 @@ DB_PASS=$PASSWORD`) await assertSuccess(provider, ["empty"], ConfigProvider.makeValue("")) }) - it("orElse falls back when fromDir file is empty", async () => { - const Fs = FileSystem.layerNoop({ - readFileString(path) { - return path === "/empty" - ? Effect.succeed("") - : Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readFileString" - }) - ) - }, - readDirectory() { - return Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readDirectory" - }) - ) - } - }) - const provider = await Effect.runPromise( - ConfigProvider.fromDir({ rootPath: "/" }).pipe( - Effect.map((dir) => dir.pipe(ConfigProvider.orElse(ConfigProvider.fromUnknown({ empty: "fallback" })))), - Effect.provide(Layer.mergeAll(Fs, Path.layer)) - ) - ) - - await assertSuccess(provider, ["empty"], ConfigProvider.makeValue("fallback")) - }) - it("reads directory entries as a record", async () => { const dirs: Record> = { "/app": ["host", "port"] } const Fs = FileSystem.layerNoop({ readFileString() { - return Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readFileString" - }) - ) + return Effect.fail(notFound("readFileString")) }, readDirectory(path) { const entries = dirs[path] return entries ? Effect.succeed([...entries]) - : Effect.fail( - PlatformError.systemError({ - module: "FileSystem", - _tag: "NotFound", - method: "readDirectory" - }) - ) + : Effect.fail(notFound("readDirectory")) } }) const provider = await Effect.runPromise( @@ -968,21 +1045,114 @@ DB_PASS=$PASSWORD`) await assertSuccess(provider, ["app"], ConfigProvider.makeRecord(new Set(["host", "port"]))) }) - it("layerAdd uses fallback", async () => { + it("wraps non-NotFound failures in SourceError", async () => { + const cause = PlatformError.systemError({ + module: "FileSystem", + _tag: "PermissionDenied", + method: "readFileString" + }) + const Fs = FileSystem.layerNoop({ + readFileString: () => Effect.fail(cause), + readDirectory: () => Effect.fail(notFound("readDirectory")) + }) + const provider = await Effect.runPromise( + ConfigProvider.fromDir({ rootPath: "/config" }).pipe( + Effect.provide(Layer.mergeAll(Fs, Path.layer)) + ) + ) + + await assertFailure( + provider, + ["secret"], + new ConfigProvider.SourceError({ + message: "Failed to read file at /config/secret", + cause + }) + ) + }) + }) + + describe("layers", () => { + const loadFromContext = (path: ConfigProvider.Path) => + Effect.gen(function*() { + const provider = yield* ConfigProvider.ConfigProvider + return yield* provider.load(path) + }) + + it("layer installs a provider", async () => { const result = await Effect.runPromise( - Effect.gen(function*() { - const provider = yield* ConfigProvider.ConfigProvider - const secret = yield* provider.load(["secret"]) - const integer = yield* provider.load(["integer"]) - const fallback = yield* provider.load(["fallback"]) - - return { secret, integer, fallback } - }).pipe(Effect.provide(AddLayer)) + loadFromContext(["KEY"]).pipe( + Effect.provide(ConfigProvider.layer(ConfigProvider.fromUnknown({ KEY: "value" }))) + ) ) - deepStrictEqual(result.secret, ConfigProvider.makeValue("shh")) - deepStrictEqual(result.integer, ConfigProvider.makeValue("123")) - deepStrictEqual(result.fallback, ConfigProvider.makeValue("value")) + deepStrictEqual(result, ConfigProvider.makeValue("value")) + }) + + it("layer accepts an Effect that produces a provider", async () => { + const result = await Effect.runPromise( + loadFromContext(["KEY"]).pipe( + Effect.provide(ConfigProvider.layer(Effect.succeed(ConfigProvider.fromUnknown({ KEY: "value" })))) + ) + ) + + deepStrictEqual(result, ConfigProvider.makeValue("value")) + }) + + it("layerAdd adds an Effect-produced provider as fallback", async () => { + const current = ConfigProvider.fromUnknown({ + CURRENT: "current", + SHARED: "current" + }) + const fallback = ConfigProvider.fromUnknown({ + FALLBACK: "fallback", + SHARED: "fallback" + }) + const Added = ConfigProvider.layerAdd(Effect.succeed(fallback)).pipe( + Layer.provide(ConfigProvider.layer(current)) + ) + + const result = await Effect.runPromise( + Effect.all({ + current: loadFromContext(["CURRENT"]), + fallback: loadFromContext(["FALLBACK"]), + shared: loadFromContext(["SHARED"]) + }).pipe(Effect.provide(Added)) + ) + + deepStrictEqual(result, { + current: ConfigProvider.makeValue("current"), + fallback: ConfigProvider.makeValue("fallback"), + shared: ConfigProvider.makeValue("current") + }) + }) + + it("layerAdd can install the added provider as primary", async () => { + const current = ConfigProvider.fromUnknown({ + CURRENT: "current", + SHARED: "current" + }) + const primary = ConfigProvider.fromUnknown({ + PRIMARY: "primary", + SHARED: "primary" + }) + const Added = ConfigProvider.layerAdd(primary, { asPrimary: true }).pipe( + Layer.provide(ConfigProvider.layer(current)) + ) + + const result = await Effect.runPromise( + Effect.all({ + current: loadFromContext(["CURRENT"]), + primary: loadFromContext(["PRIMARY"]), + shared: loadFromContext(["SHARED"]) + }).pipe(Effect.provide(Added)) + ) + + deepStrictEqual(result, { + current: ConfigProvider.makeValue("current"), + primary: ConfigProvider.makeValue("primary"), + shared: ConfigProvider.makeValue("primary") + }) }) }) }) diff --git a/.context/effect/packages/effect/test/Context.test.ts b/.context/effect/packages/effect/test/Context.test.ts new file mode 100644 index 000000000..6e4bcfc83 --- /dev/null +++ b/.context/effect/packages/effect/test/Context.test.ts @@ -0,0 +1,218 @@ +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Equal from "effect/Equal" +import * as Option from "effect/Option" +import * as Redactable from "effect/Redactable" +import { describe, it } from "vitest" + +describe("Context", () => { + const A = Context.Service("ContextTest/A") + const B = Context.Service("ContextTest/B") + const C = Context.Service("ContextTest/C") + + it("keeps the source immutable across additions", () => { + const source = Context.make(A, 1) + const result = Context.add(source, B, 2) + + deepStrictEqual([...source.mapUnsafe], [[A.key, 1]]) + deepStrictEqual([...result.mapUnsafe], [[A.key, 1], [B.key, 2]]) + }) + + it("removes a service with addOrOmit", () => { + const context = Context.make(A, 1).pipe(Context.addOrOmit(A, Option.none())) + + assertTrue(Context.getOption(context, A)._tag === "None") + }) + + it("preserves Map insertion order for replacements and appends", () => { + const Ref = Context.Reference("ContextTest/OrderRef", { defaultValue: () => 0 }) + const context = Context.empty().pipe( + Context.add(A, 1), + Context.add(Ref, 2), + Context.add(B, 3), + Context.add(A, 4), + Context.add(C, 5) + ) + + deepStrictEqual([...context.mapUnsafe], [ + [A.key, 4], + [Ref.key, 2], + [B.key, 3], + [C.key, 5] + ]) + }) + + it("invalidates the fiber cache only for opted-in keys", () => { + const Cached = Context.Service("ContextTest/Cached", { fiberCached: true }) + class CachedClass extends Context.Service()("ContextTest/CachedClass", { + fiberCached: true + }) {} + const Ref = Context.Reference("ContextTest/Ref", { defaultValue: () => 0 }) + const CachedRef = Context.Reference("ContextTest/CachedRef", { + fiberCached: true, + defaultValue: () => 0 + }) + + const source = Context.make(A, 1) + assertTrue(Context.hasSameCache(source, Context.add(source, B, 1))) + assertTrue(Context.hasSameCache(source, Context.add(source, Ref, 1))) + assertFalse(Context.hasSameCache(source, Context.add(source, CachedRef, 1))) + const context = source.pipe( + Context.add(Cached, 2), + Context.add(CachedClass, 3) + ) + + strictEqual(Context.get(context, Cached), 2) + strictEqual(Context.get(context, CachedClass), 3) + assertTrue(Context.getOption(source, Cached)._tag === "None") + + const replaced = Context.add(context, Cached, 4) + strictEqual(Context.get(replaced, Cached), 4) + strictEqual(Context.get(context, Cached), 2) + + deepStrictEqual([...replaced.mapUnsafe], [ + [A.key, 1], + [Cached.key, 4], + [CachedClass.key, 3] + ]) + deepStrictEqual([...Context.omit(Cached)(replaced).mapUnsafe], [ + [A.key, 1], + [CachedClass.key, 3] + ]) + }) + + it("supports the Redactable fallback context", () => { + const Cached = Context.Service("ContextTest/RedactableCached", { fiberCached: true }) + const context = Redactable.getRedacted({ + [Redactable.symbolRedactable](context: Context.Context) { + return context + } + }) as Context.Context + + strictEqual(context.mapUnsafe.size, 0) + assertTrue(Context.getOption(context, Cached)._tag === "None") + + const added = Context.add(context, Cached, 1) + strictEqual(Context.get(added, Cached), 1) + strictEqual(context.mapUnsafe.size, 0) + }) + + it("distinguishes an undefined service from an absent service", () => { + const Undefined = Context.Service("ContextTest/Undefined", { fiberCached: true }) + const Missing = Context.Service("ContextTest/Missing") + const Ref = Context.Reference("ContextTest/UndefinedRef", { + defaultValue: () => "default", + fiberCached: true + }) + const context = Context.make(Undefined, undefined).pipe(Context.add(Ref, undefined)) + + assertTrue(Context.getOption(context, Undefined)._tag === "Some") + assertTrue(Context.getOption(context, Missing)._tag === "None") + strictEqual(Context.getUnsafe(context, Undefined), undefined) + strictEqual(Context.getOrElse(context, Undefined, () => 1), undefined) + deepStrictEqual(Context.getOption(context, Ref), Option.some(undefined)) + strictEqual(Context.get(context, Ref), undefined) + }) + + it("bounds deep overlay chains without changing values or order", () => { + const keys = Array.from({ length: 20 }, (_, i) => Context.Service(`ContextTest/Deep${i}`)) + let context = Context.empty() + for (let i = 0; i < keys.length; i++) { + context = Context.add(context, keys[i], i) + } + + strictEqual(context.mapUnsafe.size, 20) + deepStrictEqual([...context.mapUnsafe.keys()], keys.map((key) => key.key)) + for (let i = 0; i < keys.length; i++) { + strictEqual(Context.getUnsafe(context, keys[i]), i) + } + // Rebasing on ordinary keys must not invalidate fiber caches + assertTrue(Context.hasSameCache(Context.empty(), context)) + }) + + it("flattens after repeated base fall-throughs", () => { + const context = Context.make(A, 1).pipe(Context.add(B, 2)) + const impl = context as any + + for (let i = 0; i < 7; i++) { + strictEqual(Context.getUnsafe(context, A), 1) + } + strictEqual(impl._flat, undefined) + + strictEqual(Context.getUnsafe(context, A), 1) + assertTrue(impl._flat instanceof Map) + strictEqual(impl.overlay, undefined) + strictEqual(impl.depth, 0) + + const added = Context.add(context, C, 3) as any + strictEqual(added._flat, undefined) + strictEqual(added.baseHits, 0) + }) + + it("supports the ReadonlyMap surface through mapUnsafe", () => { + const context = Context.make(A, 1).pipe(Context.add(B, 2)) + const visited: Array<[string, number]> = [] + context.mapUnsafe.forEach((value, key) => visited.push([key, value])) + + strictEqual(context.mapUnsafe.size, 2) + assertTrue(context.mapUnsafe.has(A.key)) + strictEqual(context.mapUnsafe.get(B.key), 2) + deepStrictEqual([...context.mapUnsafe.keys()], [A.key, B.key]) + deepStrictEqual([...context.mapUnsafe.values()], [1, 2]) + deepStrictEqual([...context.mapUnsafe.entries()], [[A.key, 1], [B.key, 2]]) + deepStrictEqual(visited, [[A.key, 1], [B.key, 2]]) + }) + + it("supports equality and JSON materialization", () => { + const left = Context.make(A, 1).pipe(Context.add(B, 2)) + const right = Context.makeUnsafe(new Map([[A.key, 1], [B.key, 2]])) + + assertTrue(Equal.equals(left, right)) + assertFalse(Equal.equals(left, Context.make(A, 2))) + deepStrictEqual(left.toJSON(), { + _id: "Context", + services: [{ key: A.key, value: 1 }, { key: B.key, value: 2 }] + }) + }) + + it("merges with right bias and preserves empty operand identity", () => { + const left = Context.make(A, 1).pipe(Context.add(B, 2)) + const right = Context.make(A, 3).pipe(Context.add(C, 4)) + const merged = Context.merge(left, right) + + strictEqual(Context.merge(Context.empty(), left), left) + strictEqual(Context.merge(left, Context.empty()), left) + deepStrictEqual([...merged.mapUnsafe], [[A.key, 3], [B.key, 2], [C.key, 4]]) + }) + + it("pick and omit retain source ordering", () => { + const source = Context.make(A, 1).pipe(Context.add(B, 2), Context.add(C, 3)) + + deepStrictEqual([...Context.pick(C, A)(source).mapUnsafe], [[A.key, 1], [C.key, 3]]) + deepStrictEqual([...Context.omit(B)(source).mapUnsafe], [[A.key, 1], [C.key, 3]]) + }) + + it("resolves reference defaults lazily and caches them", () => { + let calls = 0 + const Ref = Context.Reference("ContextTest/LazyRef", { + defaultValue: () => { + calls++ + return {} + } + }) + const context = Context.empty() + + strictEqual(calls, 0) + const first = Context.get(context, Ref) + strictEqual(calls, 1) + strictEqual(Context.get(context, Ref), first) + strictEqual(calls, 1) + }) + + it("retains the makeUnsafe input map", () => { + const map = new Map([[A.key, 1]]) + const context = Context.makeUnsafe(map) + + strictEqual(context.mapUnsafe, map) + }) +}) diff --git a/.context/effect/packages/effect/test/Cron.test.ts b/.context/effect/packages/effect/test/Cron.test.ts index 3a3d1a5d2..d4d942662 100644 --- a/.context/effect/packages/effect/test/Cron.test.ts +++ b/.context/effect/packages/effect/test/Cron.test.ts @@ -218,6 +218,62 @@ describe("Cron", () => { ) }) + it("format preserves compact cron syntax", () => { + strictEqual( + Cron.format(Cron.parseUnsafe("23 0-20/2 * * 0", "Europe/Berlin")), + "23 0-20/2 * * 0" + ) + }) + + it("format can include the default seconds field", () => { + strictEqual( + Cron.format(Cron.parseUnsafe("23 0-20/2 * * 0"), { includeSeconds: true }), + "0 23 0-20/2 * * 0" + ) + }) + + it("format compacts multiple runs within a field", () => { + strictEqual( + Cron.format(Cron.make({ + minutes: [0, 1, 2, 10, 20, 30], + hours: [], + days: [], + months: [], + weekdays: [] + })), + "0-2,10-30/10 * * * *" + ) + }) + + it("format preserves non-uniform values", () => { + strictEqual( + Cron.format(Cron.make({ + minutes: [1, 5, 11], + hours: [], + days: [], + months: [], + weekdays: [] + })), + "1,5,11 * * * *" + ) + }) + + it("format handles default, non-default, and unrestricted seconds", () => { + const format = (seconds?: Iterable) => + Cron.format(Cron.make({ + seconds, + minutes: [], + hours: [], + days: [], + months: [], + weekdays: [] + })) + + strictEqual(format(), "* * * * *") + strictEqual(format([15, 30]), "15,30 * * * * *") + strictEqual(format([]), "* * * * * *") + }) + it("make supports requiring both days and weekdays", () => { const utc = DateTime.zoneMakeNamedUnsafe("UTC") const values = { diff --git a/.context/effect/packages/effect/test/Crypto.test.ts b/.context/effect/packages/effect/test/Crypto.test.ts index b63c66a06..b1d992132 100644 --- a/.context/effect/packages/effect/test/Crypto.test.ts +++ b/.context/effect/packages/effect/test/Crypto.test.ts @@ -9,6 +9,21 @@ const testCrypto = Crypto.make({ digest: (algorithm, data) => Effect.succeed(Uint8Array.of(data.length, algorithm.length)) }) +const makeCrypto = (value: bigint) => + Crypto.make({ + randomBytes: () => + Uint8Array.of( + Number((value >> 48n) & 0x3fn), + Number((value >> 40n) & 0xffn), + Number((value >> 32n) & 0xffn), + Number((value >> 24n) & 0xffn), + Number((value >> 16n) & 0xffn), + Number((value >> 8n) & 0xffn), + Number(value & 0xffn) + ), + digest: (_algorithm, data) => Effect.succeed(data) + }) + describe("Crypto", () => { it("supports string literal digest algorithms", () => { const algorithm: Crypto.DigestAlgorithm = "SHA-256" @@ -34,7 +49,7 @@ describe("Crypto", () => { const randomShuffle = yield* crypto.randomShuffle([1, 2, 3]) assert.strictEqual(random, 0.75) - assert.strictEqual(randomInt, 4503599627370497) + assert.strictEqual(randomInt, -2251799813685247) assert.strictEqual(randomBoolean, true) assert.strictEqual(randomBetween, 17.5) assert.strictEqual(randomBetweenDecimalBounds, 18) @@ -42,6 +57,13 @@ describe("Crypto", () => { assert.deepStrictEqual(randomShuffle, [1, 2, 3]) }).pipe(Effect.provideService(Crypto.Crypto, testCrypto))) + it("maps adjacent random values to adjacent safe integers", () => { + assert.strictEqual(makeCrypto(0n).nextIntUnsafe(), Number.MIN_SAFE_INTEGER) + assert.strictEqual(makeCrypto(1n).nextIntUnsafe(), Number.MIN_SAFE_INTEGER + 1) + assert.strictEqual(makeCrypto(1n << 53n).nextIntUnsafe(), 1) + assert.strictEqual(makeCrypto((1n << 54n) - 2n).nextIntUnsafe(), Number.MAX_SAFE_INTEGER) + }) + it.effect("randomIntBetween excludes the upper bound in half-open ranges", () => Effect.gen(function*() { const crypto = yield* Crypto.Crypto diff --git a/.context/effect/packages/effect/test/DateTime.test.ts b/.context/effect/packages/effect/test/DateTime.test.ts index 365553330..6628344f6 100644 --- a/.context/effect/packages/effect/test/DateTime.test.ts +++ b/.context/effect/packages/effect/test/DateTime.test.ts @@ -1,5 +1,5 @@ import { describe, it } from "@effect/vitest" -import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "@effect/vitest/utils" import { DateTime, Duration, Effect, Option } from "effect" import { TestClock } from "effect/testing" @@ -381,6 +381,32 @@ describe("DateTime", () => { }) }) + describe("toEpochSeconds", () => { + it("returns epoch seconds", () => { + const dt = DateTime.makeUnsafe("2024-01-01T00:00:00Z") + strictEqual(DateTime.toEpochSeconds(dt), 1704067200) + }) + + it("floors to nearest second", () => { + const dt = DateTime.makeUnsafe("2024-01-01T00:00:00.999Z") + strictEqual(DateTime.toEpochSeconds(dt), 1704067200) + }) + }) + + describe("fromEpochSeconds", () => { + it("creates DateTime from epoch seconds", () => { + const dt = DateTime.fromEpochSeconds(1704067200) + strictEqual(dt.toJSON(), "2024-01-01T00:00:00.000Z") + }) + + it("roundtrips with toEpochSeconds", () => { + const original = DateTime.makeUnsafe("2024-06-15T12:30:00Z") + const seconds = DateTime.toEpochSeconds(original) + const restored = DateTime.fromEpochSeconds(seconds) + strictEqual(DateTime.toEpochSeconds(restored), seconds) + }) + }) + describe("makeZonedFromString", () => { it.effect("parses an instant with an offset and IANA zone", () => Effect.gen(function*() { @@ -455,7 +481,19 @@ describe("DateTime", () => { })) }) + describe("make", () => { + it("rejects invalid object instants", () => { + assertNone(DateTime.make({ epochMilliseconds: NaN })) + assertNone(DateTime.make({ epochMilliseconds: 8_640_000_000_000_001 })) + }) + }) + describe("makeUnsafe", () => { + it("throws for invalid object instants", () => { + throws(() => DateTime.makeUnsafe({ epochMilliseconds: NaN })) + throws(() => DateTime.makeUnsafe({ epochMilliseconds: 8_640_000_000_000_001 })) + }) + it("treats strings without zone info as UTC", () => { const dt = DateTime.makeUnsafe("2024-01-01 01:00:00") strictEqual(dt.toJSON(), "2024-01-01T01:00:00.000Z") diff --git a/.context/effect/packages/effect/test/Deferred.test.ts b/.context/effect/packages/effect/test/Deferred.test.ts index 6d244e7ad..3b64745e0 100644 --- a/.context/effect/packages/effect/test/Deferred.test.ts +++ b/.context/effect/packages/effect/test/Deferred.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Deferred, Option } from "effect" +import { Deferred, Fiber, Option } from "effect" import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" @@ -97,6 +97,53 @@ describe("Deferred", () => { const result = yield* Effect.exit(Deferred.await(deferred)) assert.deepStrictEqual(result, Exit.failCause(Cause.interrupt(-1))) })) + + it.effect("await - interrupting a suspended waiter removes it", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const interrupted = yield* Deferred.await(deferred).pipe( + Effect.forkChild({ startImmediately: true }) + ) + const live = yield* Deferred.await(deferred).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + assert.strictEqual(deferred.resumes?.length, 2) + yield* Fiber.interrupt(interrupted) + assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(interrupted))) + assert.strictEqual(deferred.resumes?.length, 1) + + assert.isTrue(yield* Deferred.succeed(deferred, 42)) + assert.strictEqual(yield* Fiber.join(live), 42) + })) + + it.effect("await - interrupting a waiter after completion does not die", () => + Effect.gen(function*() { + const deferred = yield* Deferred.make() + const interrupted = yield* Deferred.await(deferred).pipe( + Effect.forkChild({ startImmediately: true }) + ) + const live = yield* Deferred.await(deferred).pipe( + Effect.forkChild({ startImmediately: true }) + ) + assert.strictEqual(deferred.resumes?.length, 2) + + // Waiters resume by running the completion effect, so a suspension + // inside it leaves the await's cleanup on the waiter's stack after + // completion has already cleared `resumes`. Interrupting the waiter + // from the same tick then runs that cleanup post-completion. + yield* Effect.sync(() => { + Deferred.doneUnsafe(deferred, Effect.as(Effect.yieldNow, 42)) + assert.isUndefined(deferred.resumes) + interrupted.interruptUnsafe() + }) + + const exit = yield* Fiber.await(interrupted) + assert.isTrue(Exit.hasInterrupts(exit)) + assert.isFalse(Exit.hasDies(exit)) + + assert.strictEqual(yield* Fiber.join(live), 42) + })) }) describe("polling", () => { diff --git a/.context/effect/packages/effect/test/Duration.test.ts b/.context/effect/packages/effect/test/Duration.test.ts index aed0c2473..e666f8708 100644 --- a/.context/effect/packages/effect/test/Duration.test.ts +++ b/.context/effect/packages/effect/test/Duration.test.ts @@ -8,7 +8,7 @@ import { strictEqual, throws } from "@effect/vitest/utils" -import { Duration, Equal, pipe } from "effect" +import { Duration, Equal, Hash, HashSet, pipe } from "effect" describe("Duration", () => { it("fromInputUnsafe", () => { @@ -23,10 +23,20 @@ describe("Duration", () => { deepStrictEqual(Duration.fromInputUnsafe("10 nanos"), Duration.nanos(10n)) deepStrictEqual(Duration.fromInputUnsafe("1.5 nanos"), Duration.nanos(2n)) deepStrictEqual(Duration.fromInputUnsafe("-1.5 nanos"), Duration.nanos(-2n)) + deepStrictEqual(Duration.fromInputUnsafe("9007199254740993.1 nanos"), Duration.nanos(9_007_199_254_740_993n)) + deepStrictEqual(Duration.fromInputUnsafe("-9007199254740993.5 nanos"), Duration.nanos(-9_007_199_254_740_994n)) deepStrictEqual(Duration.fromInputUnsafe("1 micro"), Duration.micros(1n)) deepStrictEqual(Duration.fromInputUnsafe("10 micros"), Duration.micros(10n)) deepStrictEqual(Duration.fromInputUnsafe("1.5 micros"), Duration.nanos(1500n)) deepStrictEqual(Duration.fromInputUnsafe("-1.5 micros"), Duration.nanos(-1500n)) + deepStrictEqual( + Duration.fromInputUnsafe("9007199254740993.1 micros"), + Duration.nanos(9_007_199_254_740_993_100n) + ) + deepStrictEqual( + Duration.fromInputUnsafe("-9007199254740993.0005 micros"), + Duration.nanos(-9_007_199_254_740_993_001n) + ) deepStrictEqual(Duration.fromInputUnsafe("1 milli"), Duration.millis(1)) deepStrictEqual(Duration.fromInputUnsafe("10 millis"), Duration.millis(10)) deepStrictEqual(Duration.fromInputUnsafe("1 second"), Duration.seconds(1)) @@ -218,6 +228,27 @@ describe("Duration", () => { assertTrue(pipe(Duration.hours(1), Duration.equals(Duration.minutes(60)))) }) + it("Hash.symbol agrees with equals across Millis/Nanos representations", () => { + const millisTagged = Duration.seconds(5) + const nanosTagged = Duration.nanos(5_000_000_000n) + + assertTrue(Duration.equals(millisTagged, nanosTagged)) + assertTrue(Equal.equals(millisTagged, nanosTagged)) + strictEqual(Hash.hash(millisTagged), Hash.hash(nanosTagged)) + strictEqual(Hash.hash(Duration.millis(-5000)), Hash.hash(Duration.nanos(-5_000_000_000n))) + strictEqual(Hash.hash(Duration.infinity), Hash.hash(Duration.infinity)) + assertFalse(Equal.equals(Duration.infinity, Duration.negativeInfinity)) + + assertTrue(HashSet.has(HashSet.make(millisTagged), nanosTagged)) + }) + + it("Hash.symbol handles finite millis too large to convert to nanos", () => { + const duration = Duration.millis(1e303) + + assertTrue(Equal.equals(duration, Duration.millis(1e303))) + assertTrue(HashSet.has(HashSet.make(duration), Duration.millis(1e303))) + }) + it("between", () => { assertTrue(Duration.between(Duration.hours(1), { minimum: Duration.minutes(59), @@ -281,8 +312,8 @@ describe("Duration", () => { // nanos deepStrictEqual(Duration.divideUnsafe(Duration.nanos(2n), 2), Duration.nanos(1n)) deepStrictEqual(Duration.divideUnsafe(Duration.nanos(1n), 3), Duration.zero) - throws(() => Duration.divideUnsafe(Duration.nanos(1n), 0.5)) - throws(() => Duration.divideUnsafe(Duration.nanos(1n), 1.5)) + deepStrictEqual(Duration.divideUnsafe(Duration.nanos(1n), 0.5), Duration.zero) + deepStrictEqual(Duration.divideUnsafe(Duration.nanos(1n), 1.5), Duration.zero) // infinity deepStrictEqual(Duration.divideUnsafe(Duration.infinity, 2), Duration.infinity) diff --git a/.context/effect/packages/effect/test/Effect.test.ts b/.context/effect/packages/effect/test/Effect.test.ts index 4fc96fe5c..d67aaff24 100644 --- a/.context/effect/packages/effect/test/Effect.test.ts +++ b/.context/effect/packages/effect/test/Effect.test.ts @@ -10,6 +10,7 @@ import { Exit, Fiber, type Filter, + Latch, Layer, Logger, type LogLevel, @@ -181,27 +182,24 @@ describe("Effect", () => { assert.isTrue(release) }) - it("Context.Service", () => + it.effect("Context.Service", () => ATag.pipe( Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))), - Effect.provideService(ATag, "A"), - Effect.runPromise + Effect.provideService(ATag, "A") )) describe("fromOption", () => { - it("from a some", () => + it.effect("from a some", () => Option.some("A").pipe( Effect.fromOption, - Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))), - Effect.runPromise + Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))) )) - it("from a none", () => + it.effect("from a none", () => Option.none().pipe( Effect.fromOption, Effect.flip, - Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError))), - Effect.runPromise + Effect.tap((error) => Effect.sync(() => assert.ok(error instanceof Cause.NoSuchElementError))) )) it.effect("from a none with a custom error", () => @@ -243,19 +241,17 @@ describe("Effect", () => { }) describe("fromResult", () => { - it("from a success", () => + it.effect("from a success", () => Result.succeed("A").pipe( Effect.fromResult, - Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))), - Effect.runPromise + Effect.tap((_) => Effect.sync(() => assert.strictEqual(_, "A"))) )) - it("from a failure", () => + it.effect("from a failure", () => Result.fail("error").pipe( Effect.fromResult, Effect.flip, - Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error"))), - Effect.runPromise + Effect.tap((error) => Effect.sync(() => assert.strictEqual(error, "error"))) )) }) @@ -469,34 +465,22 @@ describe("Effect", () => { }) describe("forEach", () => { - it("sequential", () => + it.effect("sequential", () => Effect.gen(function*() { const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_)) assert.deepStrictEqual(results, [1, 2, 3]) - }).pipe(Effect.runPromise)) + })) - it("unbounded", () => + it.effect("unbounded", () => Effect.gen(function*() { const results = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_), { concurrency: "unbounded" }) assert.deepStrictEqual(results, [1, 2, 3]) - }).pipe(Effect.runPromise)) + })) - it("bounded", () => + it.effect("bounded", () => Effect.gen(function*() { const results = yield* Effect.forEach([1, 2, 3, 4, 5], (_) => Effect.succeed(_), { concurrency: 2 }) assert.deepStrictEqual(results, [1, 2, 3, 4, 5]) - }).pipe(Effect.runPromise)) - - it.effect("inherit unbounded", () => - Effect.gen(function*() { - const handle = yield* Effect.forEach([1, 2, 3], (_) => Effect.succeed(_).pipe(Effect.delay(50)), { - concurrency: "inherit" - }).pipe( - Effect.withConcurrency("unbounded"), - Effect.forkChild - ) - yield* TestClock.adjust(90) - assert.deepStrictEqual(handle.pollUnsafe(), Exit.succeed([1, 2, 3])) })) it.effect("sequential interrupt", () => @@ -616,21 +600,21 @@ describe("Effect", () => { assertExitDefect(exit!, defect) })) - it("length = 0", () => + it.effect("length = 0", () => Effect.gen(function*() { const results = yield* Effect.forEach([], (_) => Effect.succeed(_)) assert.deepStrictEqual(results, []) - }).pipe(Effect.runPromise)) + })) - it("string", () => + it.effect("string", () => Effect.gen(function*() { const results = yield* Effect.forEach("abc", (_) => Effect.succeed(_)) assert.deepStrictEqual(results, ["a", "b", "c"]) - }).pipe(Effect.runPromise)) + })) }) describe("all", () => { - it("tuple", () => + it.effect("tuple", () => Effect.gen(function*() { const results = (yield* Effect.all([ Effect.succeed(1), @@ -642,9 +626,9 @@ describe("Effect", () => { number ] assert.deepStrictEqual(results, [1, 2, 3]) - }).pipe(Effect.runPromise)) + })) - it("record", () => + it.effect("record", () => Effect.gen(function*() { const results = (yield* Effect.all({ a: Effect.succeed(1), @@ -660,7 +644,7 @@ describe("Effect", () => { b: "2", c: true }) - }).pipe(Effect.runPromise)) + })) it.effect("record discard", () => Effect.gen(function*() { @@ -732,6 +716,35 @@ describe("Effect", () => { c: Result.succeed(true) }) })) + + it.effect("concurrency interrupts started siblings on failure", () => + Effect.gen(function*() { + const started: Array = [] + const interrupted: Array = [] + const make = (i: number) => + Effect.suspend(() => { + started.push(i) + return Effect.sleep(500) + }).pipe( + Effect.as(i), + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted.push(i) + }) + ) + ) + const fiber = yield* Effect.all([ + make(1), + Effect.fail("boom").pipe(Effect.delay(100)), + make(3), + make(4) + ], { concurrency: 3 }).pipe(Effect.forkChild) + yield* TestClock.adjust(100) + const result = yield* Fiber.await(fiber) + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.deepStrictEqual(started, [1, 3]) + assert.deepStrictEqual(interrupted, [1, 3]) + })) }) describe("partition", () => { @@ -931,7 +944,7 @@ describe("Effect", () => { }) describe("acquireRelease", () => { - it("releases on interrupt", () => + it.live("releases on interrupt", () => Effect.gen(function*() { let release = false const fiber = yield* Effect.acquireRelease( @@ -947,7 +960,7 @@ describe("Effect", () => { fiber.interruptUnsafe() yield* Fiber.await(fiber) assert.strictEqual(release, true) - }).pipe(Effect.runPromise)) + })) it.effect("supports release dependencies", () => Effect.gen(function*() { @@ -1011,6 +1024,64 @@ describe("Effect", () => { assert.deepStrictEqual(interrupted, [500, 300, 200]) })) + it.effect("race interrupts the loser when the other side succeeds", () => + Effect.gen(function*() { + const interrupted: Array = [] + const onInterrupt = (label: string) => + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted.push(label) + }) + ) + const fiber = yield* Effect.race( + Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")), + Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow")) + ).pipe(Effect.forkChild) + yield* TestClock.adjust("500 millis") + const result = yield* Fiber.join(fiber) + assert.strictEqual(result, "fast") + assert.deepStrictEqual(interrupted, ["slow"]) + })) + + it.effect("raceFirst interrupts the loser when the other side succeeds", () => + Effect.gen(function*() { + const interrupted: Array = [] + const onInterrupt = (label: string) => + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted.push(label) + }) + ) + const fiber = yield* Effect.raceFirst( + Effect.succeed("fast").pipe(Effect.delay(100), onInterrupt("fast")), + Effect.succeed("slow").pipe(Effect.delay(500), onInterrupt("slow")) + ).pipe(Effect.forkChild) + yield* TestClock.adjust("500 millis") + const result = yield* Fiber.join(fiber) + assert.strictEqual(result, "fast") + assert.deepStrictEqual(interrupted, ["slow"]) + })) + + it.effect("raceFirst interrupts the loser when the other side fails", () => + Effect.gen(function*() { + const interrupted: Array = [] + const fiber = yield* Effect.raceFirst( + Effect.fail("boom").pipe(Effect.delay(100)), + Effect.succeed("slow").pipe( + Effect.delay(500), + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted.push("slow") + }) + ) + ) + ).pipe(Effect.exit, Effect.forkChild) + yield* TestClock.adjust("500 millis") + const result = yield* Fiber.join(fiber) + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.deepStrictEqual(interrupted, ["slow"]) + })) + describe("repeat", () => { it.effect("is interruptible", () => Effect.gen(function*() { @@ -1362,6 +1433,21 @@ describe("Effect", () => { })) }) + describe("timed", () => { + it.effect("uses monotonic time when wall time moves backward", () => + Effect.gen(function*() { + yield* TestClock.setTime(1_000) + const [duration, result] = yield* Effect.gen(function*() { + yield* TestClock.adjust("100 millis") + yield* TestClock.setTime(0) + return "done" + }).pipe(Effect.timed) + + assert.strictEqual(result, "done") + assert.strictEqual(Duration.toMillis(duration), 100) + })) + }) + describe("timeoutOption", () => { it.live("timeout a long computation", () => Effect.gen(function*() { @@ -1418,6 +1504,24 @@ describe("Effect", () => { ) assert.deepStrictEqual(result, new Cause.TimeoutError()) })) + it.effect("timeout interrupts the effect", () => + Effect.gen(function*() { + let interrupted = false + const fiber = yield* Effect.never.pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ), + Effect.timeout(10), + Effect.flip, + Effect.forkChild + ) + yield* TestClock.adjust(10) + const result = yield* Fiber.join(fiber) + assert.deepStrictEqual(result, new Cause.TimeoutError()) + assert.isTrue(interrupted) + })) }) describe("interruption", () => { @@ -1515,6 +1619,29 @@ describe("Effect", () => { assert.isTrue(ref) })) + it.effect("acquireUseRelease release runs when use is interrupted", () => + Effect.gen(function*() { + let acquired = false + let releaseExit: Exit.Exit | undefined + const fiber = yield* Effect.acquireUseRelease( + Effect.sync(() => { + acquired = true + return 123 + }), + () => Effect.never, + (resource, exit) => + Effect.sync(() => { + assert.strictEqual(resource, 123) + releaseExit = exit + }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* Fiber.interrupt(fiber) + assert.isTrue(acquired) + assert.isDefined(releaseExit) + assert.isTrue(Exit.hasInterrupts(releaseExit!)) + assert(Exit.hasInterrupts(fiber.pollUnsafe()!)) + })) + it.live("async can be uninterruptible", () => Effect.gen(function*() { let ref = false @@ -1566,6 +1693,154 @@ describe("Effect", () => { yield* Fiber.interrupt(fiber) assert.strictEqual(signal!.aborted, true) })) + + it.effect("callback cleanup effect runs on interrupt", () => + Effect.gen(function*() { + let cleanedUp = false + const fiber = yield* Effect.callback((_resume) => + Effect.sync(() => { + cleanedUp = true + }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* Fiber.interrupt(fiber) + assert.isTrue(cleanedUp) + })) + + describe("uninterruptibleMask", () => { + it.effect("defers a pending interrupt until the masked region completes", () => + Effect.gen(function*() { + const masked = yield* Latch.make() + const resume = yield* Latch.make() + const events: Array = [] + + const child = yield* Effect.uninterruptibleMask(() => + Effect.gen(function*() { + yield* masked.open + yield* resume.await + events.push("masked region completed") + }) + ).pipe(Effect.forkChild({ startImmediately: true })) + + yield* masked.await + events.push("masked") + + yield* Effect.sync(() => { + child.interruptUnsafe(123) + events.push("interrupted") + }) + assert.isUndefined(child.pollUnsafe()) + + yield* resume.open + events.push("resumed") + yield* Effect.yieldNow + yield* Effect.yieldNow + + const exit = child.pollUnsafe() + if (exit === undefined) { + return assert.fail("fiber did not exit after the masked region completed") + } + assert.isTrue(Exit.hasInterrupts(exit)) + if (exit._tag !== "Failure") { + return assert.fail("expected interrupted fiber to exit with failure") + } + assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123])) + assert.deepStrictEqual(events, ["masked", "interrupted", "resumed", "masked region completed"]) + })) + + it.effect("delivers a pending interrupt when restore re-enables interruptibility", () => + Effect.gen(function*() { + const masked = yield* Latch.make() + const resume = yield* Latch.make() + const events: Array = [] + + const child = yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function*() { + yield* masked.open + yield* resume.await + // the pending interrupt is delivered at the restore boundary, so + // the restored effect never runs + return yield* restore(Effect.suspend(() => { + events.push("restored") + return Effect.never + })) + }) + ).pipe(Effect.forkChild({ startImmediately: true })) + + yield* masked.await + events.push("masked") + + yield* Effect.sync(() => { + child.interruptUnsafe(123) + events.push("interrupted") + }) + assert.isUndefined(child.pollUnsafe()) + + yield* resume.open + events.push("resumed") + yield* Effect.yieldNow + yield* Effect.yieldNow + + const exit = child.pollUnsafe() + if (exit === undefined) { + return assert.fail("fiber did not exit after restore re-enabled interruptibility") + } + assert.isTrue(Exit.hasInterrupts(exit)) + if (exit._tag !== "Failure") { + return assert.fail("expected interrupted fiber to exit with failure") + } + assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123])) + assert.deepStrictEqual(events, ["masked", "interrupted", "resumed"]) + })) + + it.effect("region after a restored section stays uninterruptible", () => + Effect.gen(function*() { + const afterRestore = yield* Latch.make() + const resume = yield* Latch.make() + const events: Array = [] + + const child = yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function*() { + yield* restore(Effect.sync(() => { + events.push("restored section completed") + })) + yield* afterRestore.open + yield* resume.await + events.push("masked region completed") + }) + ).pipe(Effect.forkChild({ startImmediately: true })) + + yield* afterRestore.await + events.push("after restore") + + yield* Effect.sync(() => { + child.interruptUnsafe(123) + events.push("interrupted") + }) + assert.isUndefined(child.pollUnsafe()) + + yield* resume.open + events.push("resumed") + yield* Effect.yieldNow + yield* Effect.yieldNow + + const exit = child.pollUnsafe() + if (exit === undefined) { + return assert.fail("fiber did not exit after the masked region completed") + } + assert.isTrue(Exit.hasInterrupts(exit)) + if (exit._tag !== "Failure") { + return assert.fail("expected interrupted fiber to exit with failure") + } + assert.deepStrictEqual(Cause.interruptors(exit.cause), new Set([123])) + assert.deepStrictEqual(events, [ + "restored section completed", + "after restore", + "interrupted", + "resumed", + "masked region completed" + ]) + })) + }) }) describe("awaitAllChildren", () => { @@ -1916,6 +2191,21 @@ describe("Effect", () => { ) assert.isTrue(ref) })) + + it.effect("onExit - callback observes interrupt exit when the fiber is interrupted", () => + Effect.gen(function*() { + let observedInterrupt = false + const fiber = yield* Effect.never.pipe( + Effect.onExit((exit) => + Effect.sync(() => { + observedInterrupt = Exit.hasInterrupts(exit) + }) + ), + Effect.forkChild({ startImmediately: true }) + ) + yield* Fiber.interrupt(fiber) + assert.isTrue(observedInterrupt) + })) }) describe("Effect.ignore", () => { @@ -2234,6 +2524,20 @@ describe("Effect", () => { assert.deepStrictEqual(executionOrder, ["task2", "task1"]) }) }) + it.effect("concurrent: true interrupts the other side on failure", () => { + const interrupted: Array = [] + const task1 = Effect.never.pipe( + Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task1"))) + ) + const task2 = Effect.fail("boom").pipe(Effect.delay(10)) + return Effect.gen(function*() { + const fiber = yield* Effect.forkChild(Effect.zip(task1, task2, { concurrent: true })) + yield* TestClock.adjust(10) + const result = yield* Fiber.await(fiber) + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.deepStrictEqual(interrupted, ["task1"]) + }) + }) }) describe("zipWith", () => { @@ -2273,6 +2577,22 @@ describe("Effect", () => { assert.deepStrictEqual(executionOrder, ["task2", "task1"]) }) }) + it.effect("concurrent: true interrupts the other side on failure", () => { + const interrupted: Array = [] + const task1 = Effect.fail("boom").pipe(Effect.delay(10)) + const task2 = Effect.never.pipe( + Effect.onInterrupt(() => Effect.sync(() => interrupted.push("task2"))) + ) + return Effect.gen(function*() { + const fiber = yield* Effect.forkChild( + Effect.zipWith(task1, task2, (a, b) => `${a}${b}`, { concurrent: true }) + ) + yield* TestClock.adjust(10) + const result = yield* Fiber.await(fiber) + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.deepStrictEqual(interrupted, ["task2"]) + }) + }) }) describe("catchCauseFilter", () => { @@ -3035,6 +3355,75 @@ describe("Effect", () => { })) }) + describe("updateServiceScoped", () => { + class CurrentNumber extends Context.Service()("CurrentNumber") {} + + const CurrentNumberReference = Context.Reference("CurrentNumberReference", { + defaultValue: () => 1 + }) + + const CurrentValues = Context.Reference>("CurrentValues", { + defaultValue: () => [] + }) + + it.effect("updates a Context.Service until the scope closes", () => + Effect.gen(function*() { + const before = yield* CurrentNumber + const during = yield* Effect.scoped( + Effect.gen(function*() { + yield* Effect.updateServiceScoped(CurrentNumber, (value) => value + 1) + return yield* CurrentNumber + }) + ) + const after = yield* CurrentNumber + + assert.deepStrictEqual([before, during, after], [1, 2, 1]) + }).pipe(Effect.provideService(CurrentNumber, 1))) + + it.effect("updates a Context.Reference until the scope closes", () => + Effect.gen(function*() { + const before = yield* CurrentNumberReference + const during = yield* Effect.scoped( + Effect.gen(function*() { + yield* Effect.updateServiceScoped(CurrentNumberReference, (value) => value + 1) + return yield* CurrentNumberReference + }) + ) + const after = yield* CurrentNumberReference + + assert.deepStrictEqual([before, during, after], [1, 2, 1]) + })) + + it.effect("supports merging the current value on reset", () => + Effect.gen(function*() { + const scope = Scope.makeUnsafe() + let resetValues: ReadonlyArray> | undefined + + const result = yield* Effect.gen(function*() { + yield* Effect.updateServiceScoped( + CurrentValues, + (values) => [...values, "scoped"], + { + reset: (original, updated, current) => { + resetValues = [original, updated, current] + return [...original, ...current.filter((value) => !updated.includes(value))] + } + } + ) + yield* Effect.withFiber((fiber) => + Effect.sync(() => { + fiber.setContext(Context.add(fiber.context, CurrentValues, ["scoped", "external"])) + }) + ) + yield* Scope.close(scope, Exit.void) + return yield* CurrentValues + }).pipe(Effect.provideService(Scope.Scope, scope)) + + assert.deepStrictEqual(resetValues, [[], ["scoped"], ["scoped", "external"]]) + assert.deepStrictEqual(result, ["external"]) + })) + }) + describe("provide", () => { class MyNumber extends Context.Service()("MyNumber") {} diff --git a/.context/effect/packages/effect/test/Equal.test.ts b/.context/effect/packages/effect/test/Equal.test.ts index 5b094f043..768fe0f05 100644 --- a/.context/effect/packages/effect/test/Equal.test.ts +++ b/.context/effect/packages/effect/test/Equal.test.ts @@ -1,9 +1,22 @@ +import { assert } from "@effect/vitest" import * as Equal from "effect/Equal" import * as Hash from "effect/Hash" import * as HashMap from "effect/HashMap" import * as Option from "effect/Option" import { describe, expect, it } from "vitest" +class Key implements Equal.Equal, Hash.Hash { + constructor(readonly group: string) {} + + [Equal.symbol](that: unknown) { + return that instanceof Key && this.group === that.group + } + + [Hash.symbol]() { + return 0 + } +} + describe("Equal.equals", () => { describe("plain objects", () => { it("should return true for structurally identical objects (structural equality)", () => { @@ -289,6 +302,18 @@ describe("Equal.equals", () => { const date2 = new Date("2023-01-02T00:00:00.000Z") expect(Equal.equals(date1, date2)).toBe(false) }) + + it("should compare invalid dates without throwing", () => { + expect(Equal.equals(new Date(NaN), new Date(NaN))).toBe(true) + }) + }) + + describe("DataView objects", () => { + it("should compare viewed bytes", () => { + const self = new DataView(Uint8Array.of(1).buffer) + const that = new DataView(Uint8Array.of(2).buffer) + expect(Equal.equals(self, that)).toBe(false) + }) }) describe("Effect data structures", () => { @@ -672,6 +697,19 @@ describe("Equal.equals", () => { }) describe("Map and Set mixed", () => { + it("matches Map and Set entries one-to-one", () => { + const setResult = Equal.equals( + new Set([new Key("x"), new Key("x")]), + new Set([new Key("x"), new Key("y")]) + ) + const mapResult = Equal.equals( + new Map([[new Key("x"), 1], [new Key("x"), 1]]), + new Map([[new Key("x"), 1], [new Key("y"), 1]]) + ) + + assert.deepStrictEqual([setResult, mapResult], [false, false]) + }) + it("should handle objects containing maps and sets", () => { const obj1 = { map: new Map([["a", 1], ["b", 2]]), diff --git a/.context/effect/packages/effect/test/ExecutionPlan.test.ts b/.context/effect/packages/effect/test/ExecutionPlan.test.ts index ff546087e..64edfbb2b 100644 --- a/.context/effect/packages/effect/test/ExecutionPlan.test.ts +++ b/.context/effect/packages/effect/test/ExecutionPlan.test.ts @@ -1,6 +1,19 @@ -import { describe, it } from "@effect/vitest" -import { assertTrue, deepStrictEqual } from "@effect/vitest/utils" -import { Array, Context, Effect, ExecutionPlan, Exit, Layer, Stream } from "effect" +import { assert, describe, it } from "@effect/vitest" +import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { + Array, + Cause, + Context, + Duration, + Effect, + ExecutionPlan, + Exit, + Fiber, + Latch, + Layer, + Scheduler, + Stream +} from "effect" describe("ExecutionPlan", () => { class Service extends Context.Service()("Service", { @@ -45,7 +58,78 @@ describe("ExecutionPlan", () => { provide: Service.C }) + describe("make", () => { + it("rejects zero attempts", () => { + assert.throws( + () => + ExecutionPlan.make({ + provide: Context.empty(), + attempts: 0 + }), + /ExecutionPlan\.make: step\[0\]\.attempts must be greater than 0/ + ) + }) + }) + describe("Stream.withExecutionPlan", () => { + it.effect("limits attempts after partial stream failures", () => + Effect.gen(function*() { + let runs = 0 + const stream = Stream.fromEffect(Effect.sync(() => { + runs++ + return 1 + })).pipe(Stream.concat(Stream.fail("boom"))) + const plan = ExecutionPlan.make({ + provide: Context.empty(), + attempts: 3 + }) + const items = Array.empty() + + const result = yield* stream.pipe( + Stream.withExecutionPlan(plan), + Stream.runForEach((item) => + Effect.sync(() => { + items.push(item) + }) + ), + Effect.exit + ) + + deepStrictEqual(items, [1, 1, 1]) + deepStrictEqual(runs, 3) + deepStrictEqual(result, Exit.fail("boom")) + })) + + it.effect("allows interruption between retries after partial stream failures", () => + Effect.gen(function*() { + let runs = 0 + const retried = yield* Latch.make() + const stream = Stream.fromEffect(Effect.gen(function*() { + runs++ + if (runs > 1) { + yield* retried.open + } + return 1 + })).pipe(Stream.concat(Stream.fail("boom"))) + const plan = ExecutionPlan.make({ + provide: Context.empty(), + attempts: Number.MAX_SAFE_INTEGER + }) + const fiber = yield* stream.pipe( + Stream.withExecutionPlan(plan), + Stream.runDrain, + Effect.provideService(Scheduler.PreventSchedulerYield, true), + Effect.forkChild + ) + + yield* retried.await + yield* Fiber.interrupt(fiber) + const exit = yield* Fiber.await(fiber) + + assertTrue(runs > 1) + assertTrue(Exit.hasInterrupts(exit)) + })) + it.effect("falls back through failing layers and records stream attempt metadata", () => Effect.gen(function*() { const stream = Stream.unwrap(Effect.map(Service, (_) => _.stream)) @@ -114,4 +198,349 @@ describe("ExecutionPlan", () => { deepStrictEqual(result, Exit.fail("Partial")) })) }) + + describe("onEvent", () => { + const makeCollector = () => { + const events = Array.empty>() + const onEvent = (event: ExecutionPlan.Event) => + Effect.sync(() => { + events.push(event) + }) + return { events, onEvent } + } + + const simplify = (event: ExecutionPlan.Event) => + event._tag === "AttemptFailure" + ? { + _tag: event._tag, + attempt: event.attempt, + stepAttempt: event.stepAttempt, + stepIndex: event.stepIndex, + error: Cause.squash(event.cause) + } + : { + _tag: event._tag, + attempt: event.attempt, + stepAttempt: event.stepAttempt, + stepIndex: event.stepIndex + } + + describe("Effect.withExecutionPlan", () => { + it.effect("emits start and success on first attempt", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const result = yield* Effect.succeed(1).pipe( + Effect.withExecutionPlan(ExecutionPlan.make({ provide: Context.empty() }), { onEvent }) + ) + strictEqual(result, 1) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptSuccess", attempt: 1, stepAttempt: 1, stepIndex: 0 } + ]) + assertTrue(events[1]._tag === "AttemptSuccess" && Duration.isDuration(events[1].duration)) + })) + + it.effect("observer defects do not change the outcome or leave attempts unpaired", () => + Effect.gen(function*() { + const events = Array.empty() + const exit = yield* Effect.succeed("source-success").pipe( + Effect.withExecutionPlan(ExecutionPlan.make({ provide: Context.empty() }), { + onEvent: (event) => + Effect.sync(() => events.push(event._tag)).pipe( + Effect.andThen(Effect.die("observer-defect")) + ) + }), + Effect.exit + ) + + deepStrictEqual( + { exit, events }, + { exit: Exit.succeed("source-success"), events: ["AttemptStart", "AttemptSuccess"] }, + "observer defects must not affect the source outcome" + ) + })) + + it.effect("emits an event pair per attempt within a step", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + let runs = 0 + const task = Effect.suspend(() => { + runs++ + return runs < 3 ? Effect.fail(`fail-${runs}`) : Effect.succeed(runs) + }) + const plan = ExecutionPlan.make({ provide: Context.empty(), attempts: 3 }) + const result = yield* Effect.withExecutionPlan(task, plan, { onEvent }) + strictEqual(result, 3) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "fail-1" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 2, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 2, stepAttempt: 2, stepIndex: 0, error: "fail-2" }, + { _tag: "AttemptStart", attempt: 3, stepAttempt: 3, stepIndex: 0 }, + { _tag: "AttemptSuccess", attempt: 3, stepAttempt: 3, stepIndex: 0 } + ]) + })) + + it.effect("fails over to the next step without replaying events", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + let runs = 0 + const task = Effect.suspend(() => { + runs++ + return runs < 3 ? Effect.fail(`fail-${runs}`) : Effect.succeed(runs) + }) + const plan = ExecutionPlan.make( + { provide: Context.empty(), attempts: 2 }, + { provide: Context.empty(), attempts: 2 } + ) + const result = yield* Effect.withExecutionPlan(task, plan, { onEvent }) + strictEqual(result, 3) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "fail-1" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 2, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 2, stepAttempt: 2, stepIndex: 0, error: "fail-2" }, + { _tag: "AttemptStart", attempt: 3, stepAttempt: 1, stepIndex: 1 }, + { _tag: "AttemptSuccess", attempt: 3, stepAttempt: 1, stepIndex: 1 } + ]) + })) + + it.effect("event attempt matches CurrentMetadata across a failover boundary", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const inside = Array.empty() + let runs = 0 + const task = Effect.gen(function*() { + yield* ExecutionPlan.CurrentMetadata.use((meta) => { + inside.push(meta) + return Effect.void + }) + runs++ + return runs < 3 ? yield* Effect.fail("boom") : runs + }) + const plan = ExecutionPlan.make( + { provide: Context.empty(), attempts: 2 }, + { provide: Context.empty(), attempts: 2 } + ) + const result = yield* Effect.withExecutionPlan(task, plan, { onEvent }) + strictEqual(result, 3) + deepStrictEqual(inside, [ + { attempt: 1, stepIndex: 0 }, + { attempt: 2, stepIndex: 0 }, + { attempt: 3, stepIndex: 1 } + ]) + deepStrictEqual( + events.filter((event) => event._tag === "AttemptStart") + .map((event) => ({ attempt: event.attempt, stepIndex: event.stepIndex })), + inside + ) + })) + + it.effect("emits AttemptFailure when a step layer fails to build", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const plan = ExecutionPlan.make( + { provide: Layer.effect(Service, Effect.fail("nope")) }, + { provide: Service.C } + ) + const result = yield* Stream.runCollect(Stream.unwrap(Effect.map(Service, (_) => _.stream))).pipe( + Effect.withExecutionPlan(plan, { onEvent }) + ) + deepStrictEqual(result, [1, 2, 3]) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "nope" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 1, stepIndex: 1 }, + { _tag: "AttemptSuccess", attempt: 2, stepAttempt: 1, stepIndex: 1 } + ]) + })) + + it.effect("emits AttemptFailure when interrupted mid-attempt", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const started = yield* Latch.make() + const task = Effect.gen(function*() { + yield* started.open + return yield* Effect.never + }) + const plan = ExecutionPlan.make({ provide: Context.empty(), attempts: 2 }) + const fiber = yield* task.pipe( + Effect.withExecutionPlan(plan, { onEvent }), + Effect.forkChild + ) + yield* started.await + yield* Fiber.interrupt(fiber) + const exit = yield* Fiber.await(fiber) + assertTrue(Exit.hasInterrupts(exit)) + strictEqual(events.length, 2) + deepStrictEqual(simplify(events[0]), { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }) + const failure = events[1] + assertTrue(failure._tag === "AttemptFailure") + strictEqual(failure.attempt, 1) + assertTrue(Cause.hasInterrupts(failure.cause)) + })) + + it.effect("emits AttemptFailure for defects without retrying", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const plan = ExecutionPlan.make( + { provide: Context.empty(), attempts: 2 }, + { provide: Context.empty() } + ) + const exit = yield* Effect.die("boom").pipe( + Effect.withExecutionPlan(plan, { onEvent }), + Effect.exit + ) + deepStrictEqual(exit, Exit.die("boom")) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "boom" } + ]) + })) + }) + + describe("Stream.withExecutionPlan", () => { + it.effect("observer defects do not change the outcome or leave attempts unpaired", () => + Effect.gen(function*() { + const events = Array.empty() + const exit = yield* Stream.succeed("source-success").pipe( + Stream.withExecutionPlan(ExecutionPlan.make({ provide: Context.empty() }), { + onEvent: (event) => + Effect.sync(() => events.push(event._tag)).pipe( + Effect.andThen(Effect.die("observer-defect")) + ) + }), + Stream.runCollect, + Effect.exit + ) + + deepStrictEqual( + { exit, events }, + { exit: Exit.succeed(["source-success"]), events: ["AttemptStart", "AttemptSuccess"] }, + "observer defects must not affect the source outcome" + ) + })) + + it.effect("emits events for each step attempt", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const stream = Stream.unwrap(Effect.map(Service, (_) => _.stream)) + const items = yield* stream.pipe( + Stream.withExecutionPlan(Plan, { onEvent }), + Stream.runCollect + ) + deepStrictEqual(items, [1, 2, 3]) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "A" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 1, stepIndex: 1 }, + { _tag: "AttemptFailure", attempt: 2, stepAttempt: 1, stepIndex: 1, error: "B" }, + { _tag: "AttemptStart", attempt: 3, stepAttempt: 1, stepIndex: 2 }, + { _tag: "AttemptSuccess", attempt: 3, stepAttempt: 1, stepIndex: 2 } + ]) + })) + + it.effect("emits an event pair per attempt within a step", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const stream = Stream.make(1).pipe(Stream.concat(Stream.fail("boom"))) + const plan = ExecutionPlan.make({ provide: Context.empty(), attempts: 3 }) + const result = yield* stream.pipe( + Stream.withExecutionPlan(plan, { onEvent }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("boom")) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "boom" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 2, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 2, stepAttempt: 2, stepIndex: 0, error: "boom" }, + { _tag: "AttemptStart", attempt: 3, stepAttempt: 3, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 3, stepAttempt: 3, stepIndex: 0, error: "boom" } + ]) + })) + + it.effect("emits events when falling back after a partial stream", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const stream = Stream.unwrap(Effect.map(Service, (_) => _.stream)) + const items = yield* stream.pipe( + Stream.withExecutionPlan(PlanPartial, { onEvent }), + Stream.runCollect + ) + deepStrictEqual(items, [1, 2, 3, 1, 2, 3]) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "Partial" }, + { _tag: "AttemptStart", attempt: 2, stepAttempt: 1, stepIndex: 1 }, + { _tag: "AttemptSuccess", attempt: 2, stepAttempt: 1, stepIndex: 1 } + ]) + })) + + it.effect("emits AttemptFailure when interrupted mid-attempt", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const started = yield* Latch.make() + const stream = Stream.fromEffect(Effect.gen(function*() { + yield* started.open + return yield* Effect.never + })) + const plan = ExecutionPlan.make({ provide: Context.empty(), attempts: 2 }) + const fiber = yield* stream.pipe( + Stream.withExecutionPlan(plan, { onEvent }), + Stream.runDrain, + Effect.forkChild + ) + yield* started.await + yield* Fiber.interrupt(fiber) + const exit = yield* Fiber.await(fiber) + assertTrue(Exit.hasInterrupts(exit)) + strictEqual(events.length, 2) + deepStrictEqual(simplify(events[0]), { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }) + const failure = events[1] + assertTrue(failure._tag === "AttemptFailure") + strictEqual(failure.attempt, 1) + assertTrue(Cause.hasInterrupts(failure.cause)) + })) + + it.effect("emits AttemptFailure for defects without retrying", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const plan = ExecutionPlan.make( + { provide: Context.empty(), attempts: 2 }, + { provide: Context.empty() } + ) + const exit = yield* Stream.fromEffect(Effect.die("boom")).pipe( + Stream.withExecutionPlan(plan, { onEvent }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(exit, Exit.die("boom")) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "boom" } + ]) + })) + + it.effect("emits events when a partial stream fails with fallback disabled", () => + Effect.gen(function*() { + const { events, onEvent } = makeCollector() + const stream = Stream.unwrap(Effect.map(Service, (_) => _.stream)) + const result = yield* stream.pipe( + Stream.withExecutionPlan(PlanPartial, { + preventFallbackOnPartialStream: true, + onEvent + }), + Stream.runDrain, + Effect.exit + ) + deepStrictEqual(result, Exit.fail("Partial")) + deepStrictEqual(events.map(simplify), [ + { _tag: "AttemptStart", attempt: 1, stepAttempt: 1, stepIndex: 0 }, + { _tag: "AttemptFailure", attempt: 1, stepAttempt: 1, stepIndex: 0, error: "Partial" } + ]) + })) + }) + }) }) diff --git a/.context/effect/packages/effect/test/FiberHandle.test.ts b/.context/effect/packages/effect/test/FiberHandle.test.ts index c69e4de0c..5ad0ea88c 100644 --- a/.context/effect/packages/effect/test/FiberHandle.test.ts +++ b/.context/effect/packages/effect/test/FiberHandle.test.ts @@ -87,6 +87,26 @@ describe("FiberHandle", () => { strictEqual(fiberA.pollUnsafe(), undefined) })) + it.effect("clear does not remove a newer fiber installed while interrupting the previous one", () => + Effect.gen(function*() { + const handle = yield* FiberHandle.make() + yield* FiberHandle.run(handle, Effect.uninterruptible(Effect.sleep(200))) + + const clearFiber = yield* Effect.forkChild(FiberHandle.clear(handle), { startImmediately: true }) + yield* TestClock.adjust(50) + const nextFiber = yield* FiberHandle.run(handle, Effect.never) + yield* TestClock.adjust(200) + yield* Fiber.join(clearFiber) + + const current = FiberHandle.getUnsafe(handle) + if (Option.isNone(current)) { + assert.fail("expected FiberHandle.clear to preserve the newer fiber") + return + } + strictEqual(current.value, nextFiber) + strictEqual(nextFiber.pollUnsafe(), undefined) + })) + it.effect("runtime onlyIfMissing", () => Effect.gen(function*() { const run = yield* FiberHandle.makeRuntime() diff --git a/.context/effect/packages/effect/test/FiberSet.test.ts b/.context/effect/packages/effect/test/FiberSet.test.ts index 95475bc81..d2c77a2ed 100644 --- a/.context/effect/packages/effect/test/FiberSet.test.ts +++ b/.context/effect/packages/effect/test/FiberSet.test.ts @@ -4,6 +4,12 @@ import { Array, Deferred, Effect, Exit, Fiber, FiberSet, pipe, Ref, Scope } from import { TestClock } from "effect/testing" describe("FiberSet", () => { + it.effect("identifies FiberSet in JSON", () => + Effect.gen(function*() { + const set = yield* FiberSet.make() + strictEqual((set.toJSON() as { readonly _id: string })._id, "FiberSet") + })) + it.effect("interrupts running fibers when the scope closes", () => Effect.gen(function*() { const ref = yield* Ref.make(0) diff --git a/.context/effect/packages/effect/test/FiberSetRuntimePropagation.test.ts b/.context/effect/packages/effect/test/FiberSetRuntimePropagation.test.ts new file mode 100644 index 000000000..5c414d403 --- /dev/null +++ b/.context/effect/packages/effect/test/FiberSetRuntimePropagation.test.ts @@ -0,0 +1,16 @@ +import { describe, it } from "@effect/vitest" +import { assertTrue } from "@effect/vitest/utils" +import { Deferred, Effect, Fiber, FiberSet } from "effect" + +describe("FiberSet.runtime interruption propagation", () => { + it.effect("records external interruption when enabled", () => + Effect.gen(function*() { + const set = yield* FiberSet.make() + const run = yield* FiberSet.runtime(set)() + const fiber = run(Effect.never, { propagateInterruption: true }) + + yield* Fiber.interrupt(fiber) + + assertTrue(yield* Deferred.isDone(set.deferred), "external interruption should be recorded") + })) +}) diff --git a/.context/effect/packages/effect/test/FileSystem.test-utils.ts b/.context/effect/packages/effect/test/FileSystem.test-utils.ts new file mode 100644 index 000000000..3490d7db9 --- /dev/null +++ b/.context/effect/packages/effect/test/FileSystem.test-utils.ts @@ -0,0 +1,435 @@ +import { assert, expect, it } from "@effect/vitest" +import { Array, Result } from "effect" +import * as Effect from "effect/Effect" +import * as Fs from "effect/FileSystem" +import type * as Layer from "effect/Layer" +import * as Stream from "effect/Stream" + +export interface TestLayerOptions { + /** Whether writable access to a directory is supported. Deno's open-based access check rejects directories. Defaults to `true`. */ + readonly accessOnDirectory?: boolean + /** Whether a scoped temporary file removes its containing directory. Deno removes only the file. Defaults to `true`. */ + readonly tempFileScopedRemovesDirectory?: boolean +} + +export const testLayer = (layer: Layer.Layer, options: TestLayerOptions = {}) => { + const runPromise = (self: Effect.Effect) => + Effect.runPromise( + Effect.provide(self, layer) + ) + + 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* Effect.scoped(Effect.gen(function*() { + dir = yield* fs.makeTempDirectory() + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + })) + 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* Effect.scoped( + Effect.gen(function*() { + dir = yield* fs.makeTempDirectoryScoped() + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + }) + ) + const error = yield* Effect.flip(fs.stat(dir)) + assert(error.reason._tag === "NotFound") + }))) + + it.skipIf(options.accessOnDirectory === false)( + "access on a writable directory", + () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + yield* Effect.scoped(Effect.gen(function*() { + const dir = yield* fs.makeTempDirectoryScoped() + yield* fs.access(dir, { writable: true }) + })) + })) + ) + + it("makeTempFileScoped cleans up", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + yield* Effect.scoped(Effect.gen(function*() { + const root = yield* fs.makeTempDirectoryScoped() + let file = "" + let dir = "" + yield* Effect.scoped(Effect.gen(function*() { + file = yield* fs.makeTempFileScoped({ directory: root }) + const separator = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")) + assert(separator > 0, "Expected temp file path to contain a directory separator") + dir = file.slice(0, separator) + const stat = yield* fs.stat(dir) + expect(stat.type).toEqual("Directory") + })) + const fileError = yield* Effect.flip(fs.stat(file)) + assert(fileError.reason._tag === "NotFound") + if (options.tempFileScopedRemovesDirectory !== false) { + const directoryError = yield* Effect.flip(fs.stat(dir)) + assert(directoryError.reason._tag === "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* Effect.map(fs.readFile(file), (_) => new TextDecoder().decode(_)) + expect(before).toEqual(text) + + yield* fs.truncate(file) + + const after = yield* Effect.map(fs.readFile(file), (_) => new TextDecoder().decode(_)) + expect(after).toEqual("") + }))) + + it("writeFile with r+ overwrites without truncating", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const path = yield* fs.makeTempFile() + + yield* fs.writeFileString(path, "abcdef") + yield* fs.writeFileString(path, "xy", { flag: "r+" }) + + assert.strictEqual(yield* fs.readFileString(path), "xycdef") + }))) + + it("writeFile with empty data honors the flag", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const path = yield* fs.makeTempFile() + + yield* fs.writeFileString(path, "abc") + yield* fs.writeFileString(path, "") + assert.strictEqual(yield* fs.readFileString(path), "") + + yield* fs.writeFileString(path, "abc") + yield* fs.writeFileString(path, "", { flag: "r+" }) + assert.strictEqual(yield* fs.readFileString(path), "abc") + }))) + + it("writeFile with r rejects writes", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const path = yield* fs.makeTempFile() + + const error = yield* fs.writeFileString(path, "data", { flag: "r" }).pipe(Effect.flip) + + assert(error.reason._tag !== "BadArgument") + assert.strictEqual(error.reason.method, "writeFile") + assert.strictEqual(error.reason.pathOrDescriptor, path) + assert.strictEqual(yield* fs.readFileString(path), "") + }))) + + it("writeFile with a appends", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const path = yield* fs.makeTempFile() + + yield* fs.writeFileString(path, "abc") + yield* fs.writeFileString(path, "def", { flag: "a" }) + + assert.strictEqual(yield* fs.readFileString(path), "abcdef") + }))) + + it("writeFile with wx exclusively creates", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const root = yield* fs.makeTempDirectory() + const path = `${root}/file.txt` + + yield* fs.writeFileString(path, "first", { flag: "wx" }) + yield* fs.writeFileString(path, "second", { flag: "wx" }).pipe(Effect.flip) + + assert.strictEqual(yield* fs.readFileString(path), "first") + }))) + + it("copy with overwrite false preserves an existing destination", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + const root = yield* fs.makeTempDirectory() + const source = `${root}/source.txt` + const destination = `${root}/destination.txt` + yield* fs.writeFileString(source, "source") + yield* fs.writeFileString(destination, "destination") + + const result = yield* Effect.result(fs.copy(source, destination, { overwrite: false })) + + if (Result.isFailure(result)) { + assert(result.failure.reason._tag === "AlreadyExists") + assert.strictEqual(result.failure.reason.method, "copy") + assert.strictEqual(result.failure.reason.pathOrDescriptor, source) + } + assert.strictEqual(yield* fs.readFileString(source), "source") + assert.strictEqual(yield* fs.readFileString(destination), "destination") + }))) + + it("should track the cursor position when reading", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.gen(function*() { + let text: string + const file = yield* fs.open(`${__dirname}/fixtures/text.txt`) + + text = yield* file.readAlloc(Fs.Size(5)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("lorem") + + yield* file.seek(Fs.Size(7), "current") + text = yield* file.readAlloc(Fs.Size(5)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("dolar") + + yield* file.seek(Fs.Size(1), "current") + text = yield* file.readAlloc(Fs.Size(8)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("sit amet") + + yield* file.seek(Fs.Size(0), "start") + text = yield* file.readAlloc(Fs.Size(11)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("lorem ipsum") + + text = yield* fs.stream(`${__dirname}/fixtures/text.txt`, { offset: Fs.Size(6), bytesToRead: Fs.Size(5) }).pipe( + Stream.map((_) => new TextDecoder().decode(_)), + Stream.runCollect, + Effect.map(Array.join("")) + ) + expect(text).toBe("ipsum") + }).pipe( + Effect.scoped + ) + }))) + + it("should read from a backwards seek", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.gen(function*() { + const file = yield* fs.open(`${__dirname}/fixtures/text.txt`) + + const first = yield* file.readAlloc(Fs.Size(5)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(first).toBe("lorem") + + yield* file.seek(Fs.Size(-3), "current") + const second = yield* file.readAlloc(Fs.Size(3)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(second).toBe("rem") + }).pipe( + Effect.scoped + ) + }))) + + it("should read sequentially without an intervening seek", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.gen(function*() { + const file = yield* fs.open(`${__dirname}/fixtures/text.txt`) + + const first = yield* file.readAlloc(Fs.Size(5)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(first).toBe("lorem") + + const second = yield* file.readAlloc(Fs.Size(6)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(second).toBe(" ipsum") + }).pipe( + Effect.scoped + ) + }))) + + it("should track the cursor position when writing", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* 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") + }).pipe( + Effect.scoped + ) + }))) + + it("should maintain a read cursor in append mode", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* 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* file.readAlloc(Fs.Size(3)).pipe( + Effect.flatMap(Effect.fromOption), + 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* file.readAlloc(Fs.Size(6)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("barbaz") + }).pipe( + Effect.scoped + ) + }))) + + it("should restore the read cursor after an append write", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.gen(function*() { + 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") + + const first = yield* file.readAlloc(Fs.Size(1)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(first).toBe("f") + + yield* file.write(new TextEncoder().encode("bar")) + const second = yield* file.readAlloc(Fs.Size(2)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(second).toBe("oo") + }).pipe( + Effect.scoped + ) + }))) + + it("should keep the current cursor if truncating doesn't affect it", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* 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)) + }).pipe( + Effect.scoped + ) + }))) + + it("should update the current cursor if truncating affects it", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* 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)) + }).pipe( + Effect.scoped + ) + }))) + + it("should read from the clamped cursor after truncating", () => + runPromise(Effect.gen(function*() { + const fs = yield* Fs.FileSystem + + yield* Effect.gen(function*() { + const path = yield* fs.makeTempFileScoped() + const file = yield* fs.open(path, { flag: "w+" }) + + yield* file.write(new TextEncoder().encode("abcdefghij")) + yield* file.truncate(Fs.Size(5)) + yield* fs.writeFile(path, new TextEncoder().encode("xyz"), { flag: "a" }) + + const text = yield* file.readAlloc(Fs.Size(3)).pipe( + Effect.flatMap(Effect.fromOption), + Effect.map((_) => new TextDecoder().decode(_)) + ) + expect(text).toBe("xyz") + }).pipe( + Effect.scoped + ) + }))) +} diff --git a/.context/effect/packages/effect/test/Formatter.test.ts b/.context/effect/packages/effect/test/Formatter.test.ts index 782dcedb2..f0cb4b0b6 100644 --- a/.context/effect/packages/effect/test/Formatter.test.ts +++ b/.context/effect/packages/effect/test/Formatter.test.ts @@ -1,7 +1,19 @@ -import { Context, Option, Redactable, Redacted, Schema } from "effect" +import { describe, it } from "@effect/vitest" +import { + Context, + Effect, + Inspectable, + Option, + Redactable, + Redacted, + Result, + Schema, + SchemaGetter, + SchemaIssue, + SchemaParser +} from "effect" import { format, formatJson } from "effect/Formatter" -import { describe, it } from "vitest" -import { strictEqual } from "./utils/assert.ts" +import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "./utils/assert.ts" class SensitiveData implements Redactable.Redactable { constructor(private secret: string) {} @@ -10,6 +22,14 @@ class SensitiveData implements Redactable.Redactable { this.secret += s } + toString() { + return this.secret + } + + toJSON() { + return { secret: this.secret } + } + [Redactable.symbolRedactable]() { return { secret: "[REDACTED]" } } @@ -88,6 +108,11 @@ describe("Formatter", () => { strictEqual(format(obj), `{"a":1,"b":[Circular]}`) }) + it("preserves repeated non-circular references", () => { + const shared = { value: 1 } + strictEqual(format({ first: shared, second: shared }), `{"first":{"value":1},"second":{"value":1}}`) + }) + it("object with null prototype", () => { strictEqual(format(Object.create(null)), `{}`) strictEqual(format(Object.create(null, { a: { value: 1 } })), `{"a":1}`) @@ -96,16 +121,13 @@ describe("Formatter", () => { it("function", () => { strictEqual( format(() => {}), - `() => { - }` + `() => {}` ) strictEqual( format(() => { return 1 }), - `() => { - return 1; - }` + `() => {\n\t\t\t\treturn 1;\n\t\t\t}` ) }) @@ -164,8 +186,8 @@ describe("Formatter", () => { strictEqual(format(new A({ a: "a" })), `A({"a":"a"})`) }) - it("Schema.ErrorClass", () => { - class E extends Schema.ErrorClass("E")({ + it("Schema.Error", () => { + class E extends Schema.Error("E")({ a: Schema.String }) {} strictEqual(format(new E({ a: "a" })), `E`) @@ -225,9 +247,50 @@ describe("Formatter", () => { strictEqual(format(data), `{"secret":"[REDACTED]"}`) strictEqual(format({ a: data }), `{"a":{"secret":"[REDACTED]"}}`) }) + + it("redacts before specialized representations", () => { + const array = Object.assign(["secret"], { + [Redactable.symbolRedactable]: () => ["[REDACTED]"] + }) + const date = Object.assign(new Date(0), { + [Redactable.symbolRedactable]: () => "[REDACTED]" + }) + + strictEqual(format(array), `["[REDACTED]"]`) + strictEqual(format(date), `"[REDACTED]"`) + }) + + it("preserves formatting options for redacted representations", () => { + let toStringCalls = 0 + const value = { + [Redactable.symbolRedactable]: () => ({ + a: 1, + b: 2, + toString() { + toStringCalls++ + return "custom" + } + }) + } + + assertTrue(format(value, { space: 2, ignoreToString: true }).startsWith(`{\n "a": 1,\n "b": 2,`)) + strictEqual(toStringCalls, 0) + }) + + it("tracks circular references through redacted representations", () => { + const value: Redactable.Redactable = { + [Redactable.symbolRedactable]: () => ({ value }) + } + + strictEqual(format(value), `{"value":[Circular]}`) + }) }) describe("formatJson", () => { + it("returns valid JSON for undefined input", () => { + strictEqual(formatJson(undefined), `null`) + }) + it("should omit circular references", () => { const obj: any = { a: 1 } obj.self = obj @@ -239,9 +302,402 @@ describe("Formatter", () => { strictEqual(formatJson({ left: shared, right: shared }), `{"left":{"a":1},"right":{"a":1}}`) }) + it("should stringify BigInt values", () => { + strictEqual(formatJson(123n), `"123n"`) + strictEqual(formatJson({ value: 123n }), `{"value":"123n"}`) + strictEqual(formatJson([1n, 2n]), `["1n","2n"]`) + }) + it("should redact sensitive data", () => { + const date = Object.assign(new Date(0), { + [Redactable.symbolRedactable]: () => "[REDACTED]" + }) + strictEqual(formatJson(data), `{"secret":"[REDACTED]"}`) strictEqual(formatJson({ a: data }), `{"a":{"secret":"[REDACTED]"}}`) + strictEqual(formatJson([data]), `[{"secret":"[REDACTED]"}]`) + strictEqual(formatJson(date), `"[REDACTED]"`) + }) + }) + + describe("Inspectable.toJson", () => { + it("redacts before toJSON", () => { + deepStrictEqual( + Inspectable.toJson(data), + { secret: "[REDACTED]" } + ) + }) + + it("preserves plain objects as structured values", () => { + const value: any = { count: 1n } + value.self = value + + strictEqual(Inspectable.toJson(value), value) + }) + }) + + describe("Inspectable.toStringUnknown", () => { + it("should stringify BigInt values", () => { + strictEqual(Inspectable.toStringUnknown(123n), `123n`) + strictEqual( + Inspectable.toStringUnknown({ value: 123n }), + `{ + "value": "123n" +}` + ) + }) + }) + + describe("SchemaIssue reportInput", () => { + const formatIssue = SchemaIssue.makeFormatterDefault() + + it("supports every value-bearing issue constructor", () => { + const input = { value: "secret" } + const options = { reportInput: true } as const + const invalidValue = new SchemaIssue.InvalidValue() + const union = Schema.Union([Schema.String, Schema.Number]).ast + const issues: ReadonlyArray = [ + new SchemaIssue.InvalidType(Schema.String.ast, input, options), + new SchemaIssue.InvalidValue(undefined, input, options), + new SchemaIssue.UnexpectedKey(Schema.String.ast, input, options), + new SchemaIssue.Forbidden(undefined, input, options), + new SchemaIssue.OneOf(union, [Schema.String.ast], input, options), + new SchemaIssue.Filter(Schema.isMinLength(1), invalidValue, input, options), + new SchemaIssue.Encoding(Schema.String.ast, invalidValue, input, options), + new SchemaIssue.Composite(Schema.String.ast, [invalidValue], input, options), + new SchemaIssue.AnyOf(union, [invalidValue], input, options) + ] + + for (const issue of issues) { + assertTrue(SchemaIssue.hasInput(issue)) + strictEqual(issue.input, input) + assertTrue(Object.keys(issue).includes("input")) + } + + assertFalse(SchemaIssue.hasInput(new SchemaIssue.InvalidType(Schema.String.ast))) + assertFalse(SchemaIssue.hasInput(new SchemaIssue.Pointer([], invalidValue))) + assertFalse(SchemaIssue.hasInput(new SchemaIssue.MissingKey(undefined))) + }) + + it("reports input only when enabled", () => { + const input = { secret: "value" } + const disabled = SchemaParser.decodeUnknownResult(Schema.String)(input) + + assertTrue(Result.isFailure(disabled)) + assertTrue(disabled.failure._tag === "InvalidType") + assertFalse(SchemaIssue.hasInput(disabled.failure)) + strictEqual(formatIssue(disabled.failure), "Expected string") + + const enabled = SchemaParser.decodeUnknownResult(Schema.String)(input, { reportInput: true }) + assertTrue(Result.isFailure(enabled)) + assertTrue(enabled.failure._tag === "InvalidType") + assertTrue(SchemaIssue.hasInput(enabled.failure)) + strictEqual(enabled.failure.input, input) + assertTrue(Object.keys(enabled.failure).includes("input")) + assertTrue(JSON.stringify(enabled.failure).includes("secret")) + deepStrictEqual(Object.getOwnPropertyDescriptor(enabled.failure, "input"), { + value: input, + enumerable: true, + writable: true, + configurable: true + }) + strictEqual(formatIssue(enabled.failure), `Expected string, got {"secret":"value"}`) + }) + + it("distinguishes undefined input from missing input", () => { + const undefinedResult = SchemaParser.decodeUnknownResult(Schema.String)(undefined, { reportInput: true }) + assertTrue(Result.isFailure(undefinedResult)) + assertTrue(SchemaIssue.hasInput(undefinedResult.failure)) + strictEqual(undefinedResult.failure.input, undefined) + strictEqual(formatIssue(undefinedResult.failure), "Expected string, got undefined") + + const missingResult = SchemaParser.decodeUnknownResult(Schema.Struct({ value: Schema.String }))( + {}, + { reportInput: true } + ) + assertTrue(Result.isFailure(missingResult)) + assertTrue(missingResult.failure._tag === "Composite") + const pointer = missingResult.failure.issues[0] + assertTrue(pointer._tag === "Pointer") + assertFalse(SchemaIssue.hasInput(pointer)) + assertTrue(pointer.issue._tag === "MissingKey") + assertFalse(SchemaIssue.hasInput(pointer.issue)) + strictEqual(formatIssue(pointer.issue), "Missing key") + }) + + it("reports local inputs for structs", () => { + const input = { value: "not a number", extra: "secret" } + const result = SchemaParser.decodeUnknownResult(Schema.Struct({ value: Schema.Number }))(input, { + errors: "all", + onExcessProperty: "error", + reportInput: true + }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Composite") + strictEqual(result.failure.input, input) + + const extraPointer = result.failure.issues[0] + assertTrue(extraPointer._tag === "Pointer") + assertFalse(SchemaIssue.hasInput(extraPointer)) + assertTrue(extraPointer.issue._tag === "UnexpectedKey") + strictEqual(extraPointer.issue.input, "secret") + + const valuePointer = result.failure.issues[1] + assertTrue(valuePointer._tag === "Pointer") + assertFalse(SchemaIssue.hasInput(valuePointer)) + assertTrue(valuePointer.issue._tag === "InvalidType") + strictEqual(valuePointer.issue.input, "not a number") + }) + + it("reports filter input on parser-created issues", () => { + const result = SchemaParser.decodeUnknownResult(Schema.String.check(Schema.isMinLength(1)))("", { + reportInput: true + }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Composite") + const filter = result.failure.issues[0] + assertTrue(filter._tag === "Filter") + strictEqual(filter.input, "") + assertTrue(filter.issue._tag === "InvalidValue") + strictEqual(filter.issue.input, "") + strictEqual(formatIssue(filter), `Expected a value with a length of at least 1, got ""`) + }) + + it("allows user-provided transformations to report their input", () => { + const schema = Schema.String.pipe( + Schema.decode({ + decode: SchemaGetter.transformOrFail((input, options) => + Effect.fail(new SchemaIssue.InvalidValue(undefined, input, options)) + ), + encode: SchemaGetter.passthrough() + }) + ) + const result = SchemaParser.decodeUnknownResult(schema)("secret", { reportInput: true }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Encoding") + assertTrue(result.failure.issue._tag === "InvalidValue") + strictEqual(result.failure.issue.input, "secret") + strictEqual(formatIssue(result.failure), `Invalid data "secret"`) + }) + + it("reports input from effectful checks", () => { + const schema = Schema.String.pipe( + Schema.decode({ + decode: SchemaGetter.checkEffect(() => Effect.succeed(false)), + encode: SchemaGetter.passthrough() + }) + ) + const result = SchemaParser.decodeUnknownResult(schema)("secret", { reportInput: true }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Encoding") + assertTrue(result.failure.issue._tag === "InvalidValue") + strictEqual(result.failure.issue.input, "secret") + strictEqual(formatIssue(result.failure), `Invalid data "secret"`) + }) + + it("does not infer input through a user-provided pointer", () => { + const inner = new SchemaIssue.InvalidValue() + const pointer = new SchemaIssue.Pointer(["value"], inner) + const schema = Schema.String.check(Schema.makeFilter(() => pointer)) + const result = SchemaParser.decodeUnknownResult(schema)("secret", { reportInput: true }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Composite") + const filter = result.failure.issues[0] + assertTrue(filter._tag === "Filter") + strictEqual(filter.input, "secret") + strictEqual(filter.issue, pointer) + assertFalse(SchemaIssue.hasInput(pointer)) + assertFalse(SchemaIssue.hasInput(inner)) + }) + + it("reports input for union wrappers", () => { + const anyOf = SchemaParser.decodeUnknownResult(Schema.Union([Schema.Literal("a"), Schema.Literal("b")]))( + "c", + { reportInput: true } + ) + assertTrue(Result.isFailure(anyOf)) + assertTrue(anyOf.failure._tag === "AnyOf") + strictEqual(anyOf.failure.input, "c") + strictEqual(formatIssue(anyOf.failure), `Expected "a" | "b", got "c"`) + + const oneOf = SchemaParser.decodeUnknownResult( + Schema.Union([Schema.Literal("a"), Schema.Literal("a")], { mode: "oneOf" }) + )("a", { reportInput: true }) + assertTrue(Result.isFailure(oneOf)) + assertTrue(oneOf.failure._tag === "OneOf") + strictEqual(oneOf.failure.input, "a") + strictEqual(formatIssue(oneOf.failure), `Expected exactly one member to match the input "a"`) + }) + + it("respects annotated parse options", () => { + const enabled = Schema.String.annotate({ parseOptions: { reportInput: true } }) + const enabledResult = SchemaParser.decodeUnknownResult(enabled)(1, { reportInput: false }) + assertTrue(Result.isFailure(enabledResult)) + strictEqual(enabledResult.failure.input, 1) + + const disabled = Schema.String.annotate({ parseOptions: { reportInput: false } }) + const disabledResult = SchemaParser.decodeUnknownResult(disabled)(1, { reportInput: true }) + assertTrue(Result.isFailure(disabledResult)) + assertFalse(SchemaIssue.hasInput(disabledResult.failure)) + + const nestedDisabled = Schema.Struct({ value: disabled }) + const nestedInput = { value: 1 } + const nestedResult = SchemaParser.decodeUnknownResult(nestedDisabled)(nestedInput, { reportInput: true }) + assertTrue(Result.isFailure(nestedResult)) + assertTrue(nestedResult.failure._tag === "Composite") + strictEqual(nestedResult.failure.input, nestedInput) + const pointer = nestedResult.failure.issues[0] + assertTrue(pointer._tag === "Pointer") + assertFalse(SchemaIssue.hasInput(pointer.issue)) + }) + + it.effect("distinguishes present undefined from absent input in forbidden", () => + Effect.gen(function*() { + const getter = SchemaGetter.forbidden(() => "not allowed") + const present = yield* getter.run(Option.some(undefined), { reportInput: true }).pipe(Effect.flip) + assertTrue(present._tag === "Forbidden") + assertTrue(SchemaIssue.hasInput(present)) + strictEqual(present.input, undefined) + strictEqual(formatIssue(present), "not allowed") + + const absent = yield* getter.run(Option.none(), { reportInput: true }).pipe(Effect.flip) + assertTrue(absent._tag === "Forbidden") + assertFalse(SchemaIssue.hasInput(absent)) + })) + + it("formats reported inputs with the historical templates", () => { + const options = { reportInput: true } as const + const invalidValue = new SchemaIssue.InvalidValue() + const union = Schema.Union([Schema.String, Schema.Number]).ast + + strictEqual(formatIssue(new SchemaIssue.InvalidType(Schema.String.ast, 1, options)), "Expected string, got 1") + strictEqual(formatIssue(new SchemaIssue.InvalidValue(undefined, 1, options)), "Invalid data 1") + strictEqual( + formatIssue(new SchemaIssue.UnexpectedKey(Schema.String.ast, 1, options)), + "Unexpected key with value 1" + ) + strictEqual( + formatIssue(new SchemaIssue.OneOf(union, [Schema.String.ast], 1, options)), + "Expected exactly one member to match the input 1" + ) + strictEqual(formatIssue(new SchemaIssue.Forbidden(undefined, 1, options)), "Forbidden operation") + strictEqual( + formatIssue(new SchemaIssue.Filter(Schema.isMinLength(1), invalidValue, 1, options)), + "Expected a value with a length of at least 1, got 1" + ) + strictEqual(formatIssue(new SchemaIssue.AnyOf(union, [], 1, options)), "Expected string | number, got 1") + }) + + it("does not inherit input from wrappers", () => { + const options = { reportInput: true } as const + const inner = new SchemaIssue.InvalidValue() + + strictEqual( + formatIssue(new SchemaIssue.Encoding(Schema.String.ast, inner, "secret", options)), + "Expected a valid value" + ) + strictEqual( + formatIssue(new SchemaIssue.Composite(Schema.String.ast, [inner], "secret", options)), + "Expected a valid value" + ) + strictEqual( + formatIssue( + new SchemaIssue.Filter( + Schema.isMinLength(1), + new SchemaIssue.InvalidValue(undefined, "secret", options) + ) + ), + "Expected a value with a length of at least 1" + ) + }) + + it("uses expected annotations only for InvalidValue", () => { + strictEqual(formatIssue(new SchemaIssue.InvalidValue({ expected: "a valid value" })), "Expected a valid value") + strictEqual( + formatIssue(new SchemaIssue.InvalidValue({ expected: "a valid value" }, "secret", { reportInput: true })), + `Expected a valid value, got "secret"` + ) + strictEqual( + formatIssue( + new SchemaIssue.InvalidValue( + { expected: "ignored", message: "custom message" }, + "secret", + { reportInput: true } + ) + ), + "custom message" + ) + strictEqual( + formatIssue(new SchemaIssue.Forbidden({ expected: "a permitted operation" }, "secret", { reportInput: true })), + "Forbidden operation" + ) + const filter = new SchemaIssue.Filter( + Schema.isMinLength(1), + new SchemaIssue.InvalidValue({ expected: "a custom value" }, "secret", { reportInput: true }) + ) + strictEqual(formatIssue(filter), `Expected a custom value, got "secret"`) + deepStrictEqual( + SchemaIssue.makeFormatterStandardSchemaV1({ checkHook: () => undefined })(filter).issues, + [{ message: `Expected a custom value, got "secret"`, path: [] }] + ) + }) + + it("uses expected annotations in built-ins", () => { + const disabled = SchemaParser.decodeUnknownResult(Schema.URLFromString)("invalid") + assertTrue(Result.isFailure(disabled)) + strictEqual(formatIssue(disabled.failure), "Expected a valid URL string") + + const enabled = SchemaParser.decodeUnknownResult(Schema.URLFromString)("invalid", { reportInput: true }) + assertTrue(Result.isFailure(enabled)) + strictEqual(formatIssue(enabled.failure), `Expected a valid URL string, got "invalid"`) + }) + + it("keeps Redacted input redacted", () => { + const input = Redacted.make("secret", { label: "password" }) + const result = SchemaParser.decodeUnknownResult(Schema.Redacted(Schema.Number))(input, { reportInput: true }) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure._tag === "Composite") + const pointer = result.failure.issues[0] + assertTrue(pointer._tag === "Pointer") + assertTrue(pointer.issue._tag === "InvalidValue") + strictEqual(pointer.issue.input, input) + strictEqual(formatIssue(result.failure), "Invalid data \n at [\"value\"]") + }) + + it("preserves custom messages and hooks", () => { + const options = { reportInput: true } as const + const invalidType = new SchemaIssue.InvalidType( + Schema.String.annotate({ message: "custom message" }).ast, + "secret", + options + ) + strictEqual(formatIssue(invalidType), "custom message") + + const leafFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ leafHook: () => "redacted leaf" }) + deepStrictEqual(leafFormatter(invalidType).issues, [{ message: "redacted leaf", path: [] }]) + + const filter = new SchemaIssue.Filter(Schema.isMinLength(1), new SchemaIssue.InvalidValue(), "secret", options) + const checkFormatter = SchemaIssue.makeFormatterStandardSchemaV1({ checkHook: () => "redacted check" }) + deepStrictEqual(checkFormatter(filter).issues, [{ message: "redacted check", path: [] }]) + }) + + it("reports input in Standard Schema messages without adding an input field", () => { + const standardSchema = Schema.toStandardSchemaV1(Schema.String, { + parseOptions: { reportInput: true } + }) + const result = standardSchema["~standard"].validate(1) + if (result instanceof Promise) { + throw new Error("Expected synchronous validation") + } + deepStrictEqual(result, { + issues: [{ message: "Expected string, got 1", path: [] }] + }) }) }) }) diff --git a/.context/effect/packages/effect/test/Function.test.ts b/.context/effect/packages/effect/test/Function.test.ts index 13ecc61b7..07dcba9cd 100644 --- a/.context/effect/packages/effect/test/Function.test.ts +++ b/.context/effect/packages/effect/test/Function.test.ts @@ -330,4 +330,33 @@ describe("Function", () => { assert.strictEqual(callCount, 2) }) }) + + describe("memoizeIdempotent", () => { + it("caches the output as a fixed point", () => { + let callCount = 0 + const input = { id: "input" } + const output = { id: "output" } + const f = F.memoizeIdempotent((obj: { id: string }) => { + callCount++ + return obj === input ? output : obj + }) + + assert.strictEqual(f(input), output) + assert.strictEqual(f(output), output) + assert.strictEqual(callCount, 1) + }) + + it("caches an input that is already a fixed point", () => { + let callCount = 0 + const f = F.memoizeIdempotent((obj: object) => { + callCount++ + return obj + }) + const input = {} + + assert.strictEqual(f(input), input) + assert.strictEqual(f(input), input) + assert.strictEqual(callCount, 1) + }) + }) }) diff --git a/.context/effect/packages/effect/test/HashMap.test.ts b/.context/effect/packages/effect/test/HashMap.test.ts index 741678194..0ee4d7bbf 100644 --- a/.context/effect/packages/effect/test/HashMap.test.ts +++ b/.context/effect/packages/effect/test/HashMap.test.ts @@ -29,6 +29,23 @@ describe("HashMap", () => { }) describe("basic operations", () => { + it("modifyHash stores a new key under the supplied hash", () => { + const key = {} + const hash = 12345 + const map = HashMap.modifyHash(HashMap.empty(), key, hash, () => Option.some("value")) + + expect(HashMap.getHash(map, key, hash)).toEqual(Option.some("value")) + }) + + it("modifyHash removes a key using the supplied hash", () => { + const key = {} + const hash = 12345 + const map = HashMap.modifyHash(HashMap.empty(), key, hash, () => Option.some("value")) + const removed = HashMap.modifyHash(map, key, hash, () => Option.none()) + + expect(HashMap.getHash(removed, key, hash)).toEqual(Option.none()) + }) + it("get - existing key", () => { const map = HashMap.make(["a", 1], ["b", 2]) expect(HashMap.get(map, "a")).toEqual(Option.some(1)) diff --git a/.context/effect/packages/effect/test/HashRing.test.ts b/.context/effect/packages/effect/test/HashRing.test.ts new file mode 100644 index 000000000..3cc3f6951 --- /dev/null +++ b/.context/effect/packages/effect/test/HashRing.test.ts @@ -0,0 +1,47 @@ +import { assert, describe, it } from "@effect/vitest" +import * as HashRing from "effect/HashRing" +import * as PrimaryKey from "effect/PrimaryKey" + +describe("HashRing", () => { + it("updates the stored node when adding the same primary key", () => { + const first = { + name: "first", + [PrimaryKey.symbol]() { + return "node" + } + } + const updated = { + name: "updated", + [PrimaryKey.symbol]() { + return "node" + } + } + const ring = HashRing.make() + + HashRing.add(ring, first) + HashRing.add(ring, updated) + + assert.strictEqual(HashRing.get(ring, "request"), updated) + }) + + it("updates the stored node when changing its weight", () => { + const first = { + name: "first", + [PrimaryKey.symbol]() { + return "node" + } + } + const updated = { + name: "updated", + [PrimaryKey.symbol]() { + return "node" + } + } + const ring = HashRing.make() + + HashRing.add(ring, first) + HashRing.add(ring, updated, { weight: 2 }) + + assert.strictEqual(HashRing.get(ring, "request"), updated) + }) +}) diff --git a/.context/effect/packages/effect/test/HttpClient.test.ts b/.context/effect/packages/effect/test/HttpClient.test.ts index 5dcb17aa2..0ba4fac52 100644 --- a/.context/effect/packages/effect/test/HttpClient.test.ts +++ b/.context/effect/packages/effect/test/HttpClient.test.ts @@ -1,7 +1,16 @@ import { expect, it } from "@effect/vitest" import { Context, Effect, Layer, Schema, Stream, Struct } from "effect" import { TestClock } from "effect/testing" -import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" +import { + FetchHttpClient, + HttpClient, + HttpClientRequest, + HttpClientResponse, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "effect/unstable/http" const Todo = Schema.Struct({ userId: Schema.Number, @@ -14,10 +23,7 @@ const TodoWithoutId = Schema.Struct({ }) const makeJsonPlaceholder = Effect.gen(function*() { - const defaultClient = yield* HttpClient.HttpClient - const client = defaultClient.pipe( - HttpClient.mapRequest(HttpClientRequest.prependUrl("https://jsonplaceholder.typicode.com")) - ) + const client = yield* HttpClient.HttpClient const createTodo = (todo: typeof TodoWithoutId.Type) => HttpClientRequest.post("/todos").pipe( HttpClientRequest.schemaBodyJson(TodoWithoutId)(todo), @@ -32,43 +38,103 @@ const makeJsonPlaceholder = Effect.gen(function*() { interface JsonPlaceholder extends Effect.Success {} const JsonPlaceholder = Context.Service("test/JsonPlaceholder") const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder) +const TestRoutes = HttpRouter.serve(HttpRouter.use(Effect.fnUntraced(function*(router) { + yield* router.addAll([ + HttpRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("test"))), + HttpRouter.route("GET", "/redirect", Effect.succeed(HttpServerResponse.redirect("/"))), + HttpRouter.route( + "GET", + "/stream", + Effect.succeed(HttpServerResponse.stream(Stream.make("test").pipe(Stream.encodeText))) + ), + HttpRouter.route( + "GET", + "/interrupt", + Effect.succeed(HttpServerResponse.stream( + Stream.make("test").pipe(Stream.concat(Stream.never), Stream.encodeText) + )) + ), + HttpRouter.route( + "GET", + "/todos/1", + Effect.succeed(HttpServerResponse.jsonUnsafe({ + userId: 1, + id: 1, + title: "test", + completed: false + })) + ), + HttpRouter.route( + "POST", + "/todos", + Effect.gen(function*() { + const todo = yield* HttpServerRequest.schemaBodyJson(TodoWithoutId) + return HttpServerResponse.jsonUnsafe({ ...todo, id: 201 }) + }) + ), + HttpRouter.route("HEAD", "/todos", Effect.succeed(HttpServerResponse.empty({ status: 200 }))) + ]) +}))) +const DenoHttpServerUrl = new URL("../../platform/deno/src/DenoHttpServer.ts", import.meta.url).href +const TestServerLive = Layer.unwrap(Effect.promise(() => + "Deno" in globalThis + ? (import(DenoHttpServerUrl) as Promise<{ + readonly layerServer: (options: { + readonly hostname: string + readonly port: number + readonly onListen: () => void + }) => Layer.Layer + }>).then((DenoHttpServer) => DenoHttpServer.layerServer({ hostname: "127.0.0.1", port: 0, onListen: () => {} })) + : Promise.all([ + import("@effect/platform-node/NodeHttpServer"), + import("node:http") + ]).then(([NodeHttpServer, Http]) => NodeHttpServer.layerServer(Http.createServer, { port: 0 })) +)) ;[ { name: "FetchHttpClient", layer: FetchHttpClient.layer } ].forEach(({ layer, name }) => { + const layerTest = HttpServer.layerTestClient.pipe( + Layer.provide(layer), + Layer.provideMerge(TestServerLive) + ) + const testLayer = Layer.merge(JsonPlaceholderLive, TestRoutes).pipe( + Layer.provideMerge(layerTest) + ) + it.layer(layer)(name, (it) => { - it.effect("google", () => - flakyTest(Effect.gen(function*() { - const response = yield* HttpClient.get("https://www.google.com/").pipe( + it.effect("get", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("/").pipe( Effect.flatMap((_) => _.text) ) - expect(response).toContain("Google") - }))) + expect(response).toBe("test") + }).pipe(Effect.provide(testLayer))) - it.effect("google followRedirects", () => - flakyTest(Effect.gen(function*() { + it.effect("followRedirects", () => + Effect.gen(function*() { const client = (yield* HttpClient.HttpClient).pipe( HttpClient.followRedirects() ) - const response = yield* client.get("http://google.com/").pipe( + const response = yield* client.get("/redirect").pipe( Effect.flatMap((_) => _.text) ) - expect(response).toContain("Google") - }))) + expect(response).toBe("test") + }).pipe(Effect.provide(testLayer))) - it.effect("google stream", () => - flakyTest(Effect.gen(function*() { + it.effect("stream", () => + Effect.gen(function*() { const client = yield* HttpClient.HttpClient - const response = yield* client.get("https://www.google.com/").pipe( + const response = yield* client.get("/stream").pipe( Effect.map((_) => _.stream), Stream.unwrap, Stream.decodeText(), Stream.mkString ) - expect(response).toContain("Google") - }))) + expect(response).toBe("test") + }).pipe(Effect.provide(testLayer))) it.effect("jsonplaceholder", () => Effect.gen(function*() { @@ -77,7 +143,7 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder) Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) ) expect(response.id).toBe(1) - }).pipe(flakyTest, Effect.provide(JsonPlaceholderLive))) + }).pipe(Effect.provide(testLayer))) it.effect("jsonplaceholder schemaBodyJson", () => Effect.gen(function*() { @@ -88,23 +154,23 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder) completed: false }) expect(response.title).toBe("test") - }).pipe(Effect.provide(JsonPlaceholderLive), flakyTest)) + }).pipe(Effect.provide(testLayer))) it.effect("head request with schemaJson", () => - flakyTest(Effect.gen(function*() { + Effect.gen(function*() { const client = yield* HttpClient.HttpClient - const response = yield* client.head("https://jsonplaceholder.typicode.com/todos").pipe( + const response = yield* client.head("/todos").pipe( Effect.flatMap( HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) })) ) ) expect(response).toEqual({ status: 200 }) - }))) + }).pipe(Effect.provide(testLayer))) it.effect("interrupt", () => Effect.gen(function*() { const client = yield* HttpClient.HttpClient - const response = yield* client.get("https://www.google.com/").pipe( + const response = yield* client.get("/interrupt").pipe( Effect.flatMap((_) => _.text), Effect.timeout(1), Effect.asSome, @@ -112,20 +178,12 @@ const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder) TestClock.withLive ) expect(response._tag).toEqual("None") - })) + }).pipe(Effect.provide(testLayer))) it.effect("close early", () => - flakyTest(Effect.gen(function*() { - const response = yield* HttpClient.get("https://www.google.com/") + Effect.gen(function*() { + const response = yield* HttpClient.get("/stream") expect(response.status).toBe(200) - }))) + }).pipe(Effect.provide(testLayer))) }) }) - -const flakyTest = (effect: Effect.Effect) => - effect.pipe( - Effect.timeoutOrElse({ - duration: "2 seconds", - orElse: () => Effect.void - }) - ) diff --git a/.context/effect/packages/effect/test/Iterable.test.ts b/.context/effect/packages/effect/test/Iterable.test.ts index 82817e4bf..bb8b59ee0 100644 --- a/.context/effect/packages/effect/test/Iterable.test.ts +++ b/.context/effect/packages/effect/test/Iterable.test.ts @@ -396,6 +396,13 @@ describe("Iterable", () => { deepStrictEqual(toArray(Iter.flatten([[1], [2], [3]])), [1, 2, 3]) }) + it("flatten is stack safe across empty iterables", () => { + const input: Array> = Array.from({ length: 20_000 }, () => []) + input.push([1]) + + deepStrictEqual(toArray(Iter.flatten(input)), [1]) + }) + it("cartesianWith", () => { const right = (function*() { yield* [1, 2, 3] diff --git a/.context/effect/packages/effect/test/JsonPatch.test.ts b/.context/effect/packages/effect/test/JsonPatch.test.ts index 5084ccafb..f25d1d3ce 100644 --- a/.context/effect/packages/effect/test/JsonPatch.test.ts +++ b/.context/effect/packages/effect/test/JsonPatch.test.ts @@ -166,6 +166,15 @@ describe("JsonPatch", () => { { op: "replace", path: "/a~01b", value: 2 } ]) }) + + it("treats '__proto__' as an own key", () => { + const oldValue = JSON.parse(`{"__proto__":{"value":1}}`) + const newValue = JSON.parse(`{"__proto__":{"value":2}}`) + + deepStrictEqual(JsonPatch.get(oldValue, newValue), [ + { op: "replace", path: "/__proto__/value", value: 2 } + ]) + }) }) }) @@ -522,6 +531,10 @@ describe("JsonPatch", () => { () => JsonPatch.apply([{ op: "replace", path: "/-1", value: 1 }], [1, 2, 3]), `Invalid array index` ) + expectMessage( + () => JsonPatch.apply([{ op: "add", path: "/items/abc/value", value: 1 }], { items: [] }), + `Invalid array index: "abc"` + ) }) it("rejects out-of-bounds array access", () => { @@ -562,6 +575,19 @@ describe("JsonPatch", () => { ) }) + it("does not treat inherited properties as document members", () => { + const document = Object.create({ inherited: { value: 1 } }) as Schema.JsonObject + + expectMessage( + () => JsonPatch.apply([{ op: "replace", path: "/inherited/value", value: 2 }], document), + "Cannot replace at" + ) + expectMessage( + () => JsonPatch.apply([{ op: "remove", path: "/inherited" }], document), + "does not exist" + ) + }) + it("rejects add/replace when the parent is missing or not a container", () => { expectMessage( () => JsonPatch.apply([{ op: "add", path: "/a/b", value: 1 }], { a: null }), @@ -587,6 +613,22 @@ describe("JsonPatch", () => { ) }) + it("rejects operations when a nested parent cannot be resolved", () => { + const cases: ReadonlyArray<[JsonPatch.JsonPatchOperation, Schema.Json]> = [ + [{ op: "add", path: "/a/b/c", value: 1 }, { a: null }], + [{ op: "add", path: "/a/0/b", value: 1 }, { a: [] }], + [{ op: "replace", path: "/a/b", value: 1 }, {}], + [{ op: "remove", path: "/a/b" }, {}], + [{ op: "remove", path: "/a/b/c" }, { a: "not-object" }] + ] + for (const [operation, document] of cases) { + expectMessage( + () => JsonPatch.apply([operation], document), + `Cannot ${operation.op} at` + ) + } + }) + it("rejects remove at the root", () => { expectMessage( () => JsonPatch.apply([{ op: "remove", path: "" }], { a: 1 }), @@ -617,6 +659,34 @@ describe("JsonPatch", () => { JsonPatch.apply(patch, { a: 1 }) deepStrictEqual(patch, patchCopy) }) + + it("preserves references outside the modified path", () => { + const unchanged = { value: 1 } + const original = { changed: { value: 1 }, unchanged } + const result = JsonPatch.apply([{ op: "replace", path: "/changed/value", value: 2 }], original) + + strictEqual((result as any).unchanged, unchanged) + deepStrictEqual(original, { changed: { value: 1 }, unchanged: { value: 1 } }) + }) + + it("handles '__proto__' as an own data property", () => { + const original = JSON.parse(`{"nested":{"__proto__":{"value":1}}}`) + const result = JsonPatch.apply( + [ + { op: "replace", path: "/nested/__proto__/value", value: 2 }, + { op: "add", path: "/__proto__", value: { value: 3 } } + ], + original + ) as any + + strictEqual(Object.getPrototypeOf(result), Object.prototype) + strictEqual(Object.getPrototypeOf(result.nested), Object.prototype) + strictEqual(Object.hasOwn(result, "__proto__"), true) + strictEqual(Object.hasOwn(result.nested, "__proto__"), true) + deepStrictEqual(result.__proto__, { value: 3 }) + deepStrictEqual(result.nested.__proto__, { value: 2 }) + deepStrictEqual(original, JSON.parse(`{"nested":{"__proto__":{"value":1}}}`)) + }) }) describe("empty patch optimization", () => { @@ -765,6 +835,13 @@ describe("JsonPatch", () => { }) describe("root operations", () => { + it("applies root add", () => { + deepStrictEqual( + JsonPatch.apply([{ op: "add", path: "", value: { replacement: true } }], { old: true }), + { replacement: true } + ) + }) + it("applies root replace followed by nested operations", () => { const result = JsonPatch.apply( [ diff --git a/.context/effect/packages/effect/test/JsonSchema.test.ts b/.context/effect/packages/effect/test/JsonSchema.test.ts index f5245633e..1473b0631 100644 --- a/.context/effect/packages/effect/test/JsonSchema.test.ts +++ b/.context/effect/packages/effect/test/JsonSchema.test.ts @@ -1,9 +1,116 @@ -import { describe, it } from "@effect/vitest" +import { assert, describe, it } from "@effect/vitest" import { deepStrictEqual } from "@effect/vitest/utils" import * as JsonSchema from "effect/JsonSchema" +import * as Schema from "effect/Schema" + +// oxlint-disable-next-line @typescript-eslint/no-require-imports +const AjvDraft07 = require("ajv") +// oxlint-disable-next-line @typescript-eslint/no-require-imports +const AjvDraft04 = require("ajv-draft-04") + +const ajvDraft07 = new AjvDraft07.default({ allErrors: true, strict: false }) +const ajvDraft04 = new AjvDraft04.default({ allErrors: true, strict: false }) + +function makeSchema(document: JsonSchema.Document<"draft-04" | "draft-07">): JsonSchema.JsonSchema { + return { + $schema: document.dialect === "draft-04" + ? JsonSchema.META_SCHEMA_URI_DRAFT_04 + : JsonSchema.META_SCHEMA_URI_DRAFT_07, + ...document.schema, + ...(Object.keys(document.definitions).length > 0 ? { definitions: document.definitions } : {}) + } +} + +function assertDoesNotMutate(input: A, f: (input: A) => unknown): void { + const before = structuredClone(input) + f(input) + deepStrictEqual(input, before) +} describe("JsonSchema", () => { - describe("sanitizeOpenApiComponentsKey", () => { + describe("meta-schema URIs", () => { + it("exports the URI for every supported dialect", () => { + deepStrictEqual(JsonSchema.META_SCHEMA_URI_DRAFT_04, "http://json-schema.org/draft-04/schema#") + deepStrictEqual(JsonSchema.META_SCHEMA_URI_DRAFT_07, "http://json-schema.org/draft-07/schema#") + deepStrictEqual(JsonSchema.META_SCHEMA_URI_DRAFT_2020_12, "https://json-schema.org/draft/2020-12/schema") + }) + }) + + describe("resolve$ref", () => { + it("resolves a definition", () => { + const definition: JsonSchema.JsonSchema = { type: "string" } + deepStrictEqual(JsonSchema.resolve$ref("#/$defs/A", { A: definition }), definition) + }) + + it("unescapes the referenced JSON Pointer token", () => { + const definition: JsonSchema.JsonSchema = { type: "string" } + deepStrictEqual(JsonSchema.resolve$ref("#/$defs/A~1B~0C", { "A/B~C": definition }), definition) + }) + + it("returns undefined for a missing definition", () => { + deepStrictEqual(JsonSchema.resolve$ref("#/$defs/Missing", {}), undefined) + }) + + it("ignores inherited definitions", () => { + deepStrictEqual(JsonSchema.resolve$ref("#/$defs/constructor", {}), undefined) + }) + + it("resolves __proto__ when it is an own definition", () => { + const definition: JsonSchema.JsonSchema = { type: "string" } + deepStrictEqual( + JsonSchema.resolve$ref("#/$defs/__proto__", { ["__proto__"]: definition }), + definition + ) + }) + }) + + describe("resolveTopLevel$ref", () => { + it("resolves a top-level ref without mutating the document definitions", () => { + const definition: JsonSchema.JsonSchema = { type: "string" } + const document: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { $ref: "#/$defs/A" }, + definitions: { A: definition } + } + const result = JsonSchema.resolveTopLevel$ref(document) + + assert.notStrictEqual(result, document) + assert.strictEqual(result.definitions, document.definitions) + deepStrictEqual(result, { + dialect: "draft-2020-12", + schema: definition, + definitions: document.definitions + }) + }) + + it("returns the same document when the top-level ref cannot be resolved", () => { + const document: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { $ref: "#/$defs/Missing" }, + definitions: {} + } + + assert.strictEqual(JsonSchema.resolveTopLevel$ref(document), document) + }) + + it("returns the same document when there is no string top-level ref", () => { + const withoutRef: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { type: "string" }, + definitions: {} + } + const withNonStringRef: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { $ref: 1 }, + definitions: {} + } + + assert.strictEqual(JsonSchema.resolveTopLevel$ref(withoutRef), withoutRef) + assert.strictEqual(JsonSchema.resolveTopLevel$ref(withNonStringRef), withNonStringRef) + }) + }) + + describe("sanitizeOpenApiComponentsSchemasKey", () => { const sanitizeOpenApiComponentsKey = JsonSchema.sanitizeOpenApiComponentsSchemasKey it("returns '_' for empty input", () => { @@ -71,6 +178,16 @@ describe("JsonSchema", () => { }) describe("fromSchemaDraft07", () => { + it("preserves not", () => { + const input: JsonSchema.JsonSchema = { not: { type: "string" } } + const result = JsonSchema.fromSchemaDraft07(input) + deepStrictEqual(result, { + dialect: "draft-2020-12", + schema: { not: { type: "string" } }, + definitions: {} + }) + }) + it("normalizes a schema without definitions to the canonical document shape", () => { const input: JsonSchema.JsonSchema = { type: "string" @@ -164,7 +281,7 @@ describe("JsonSchema", () => { }) }) - it("should preserve annotations", () => { + it("preserves annotations", () => { const input: JsonSchema.JsonSchema = { type: "string", title: "My String", @@ -192,7 +309,7 @@ describe("JsonSchema", () => { }) }) - it("should handle string constraints", () => { + it("preserves string constraints", () => { const input: JsonSchema.JsonSchema = { type: "string", pattern: "^[a-z]+$", @@ -212,7 +329,7 @@ describe("JsonSchema", () => { }) }) - it("should handle number constraints", () => { + it("preserves number constraints", () => { const input: JsonSchema.JsonSchema = { type: "number", minimum: 0, @@ -236,7 +353,7 @@ describe("JsonSchema", () => { }) }) - it("should handle array constraints", () => { + it("preserves array constraints", () => { const input: JsonSchema.JsonSchema = { type: "array", items: { type: "string" }, @@ -258,7 +375,7 @@ describe("JsonSchema", () => { }) }) - it("should handle object constraints", () => { + it("preserves object constraints", () => { const input: JsonSchema.JsonSchema = { type: "object", properties: { @@ -296,7 +413,7 @@ describe("JsonSchema", () => { }) }) - it("should handle enum, const, allOf, anyOf, oneOf", () => { + it("preserves enum, const, allOf, anyOf, and oneOf", () => { const input: JsonSchema.JsonSchema = { enum: ["a", "b", "c"], const: "constant", @@ -384,6 +501,55 @@ describe("JsonSchema", () => { definitions: {} }) }) + + it("preserves malformed values for recognized keywords", () => { + const input: JsonSchema.JsonSchema = { + $ref: 1, + definitions: { + Invalid: [1, { not: false }] as unknown as JsonSchema.JsonSchema + }, + properties: { + nested: { + definitions: "invalid", + not: [false, { type: "string" }] + } + }, + patternProperties: "invalid", + allOf: "invalid" + } + + deepStrictEqual(JsonSchema.fromSchemaDraft07(input), { + dialect: "draft-2020-12", + schema: { + $ref: 1, + properties: { + nested: { + definitions: "invalid", + not: [false, { type: "string" }] + } + }, + patternProperties: "invalid", + allOf: "invalid" + }, + definitions: { + Invalid: [1, { not: false }] as unknown as JsonSchema.JsonSchema + } + }) + }) + + it("ignores additionalItems when items is not a tuple", () => { + deepStrictEqual( + JsonSchema.fromSchemaDraft07({ + type: "array", + items: { type: "string" }, + additionalItems: false + }).schema, + { + type: "array", + items: { type: "string" } + } + ) + }) }) describe("fromSchemaDraft2020_12", () => { @@ -492,6 +658,20 @@ describe("JsonSchema", () => { }) describe("fromSchemaOpenApi3_1", () => { + it("preserves non-string refs and malformed schema maps", () => { + const input: JsonSchema.JsonSchema = { + $ref: null, + enum: [null], + properties: [{ $ref: "#/components/schemas/Literal" }] + } + + deepStrictEqual(JsonSchema.fromSchemaOpenApi3_1(input), { + dialect: "draft-2020-12", + schema: input, + definitions: {} + }) + }) + it("rewrites OpenAPI component schema refs to $defs refs", () => { const input: JsonSchema.JsonSchema = { type: "object", @@ -514,6 +694,80 @@ describe("JsonSchema", () => { }) }) + it("rewrites refs only in schema positions", () => { + const literal = { $ref: "#/components/schemas/Literal" } + const input: JsonSchema.JsonSchema = { + properties: { value: { $ref: "#/components/schemas/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + } + + deepStrictEqual(JsonSchema.fromSchemaOpenApi3_1(input), { + dialect: "draft-2020-12", + schema: { + properties: { value: { $ref: "#/$defs/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + }, + definitions: {} + }) + }) + + it("rewrites refs throughout Draft 2020-12 subschemas", () => { + const input: JsonSchema.JsonSchema = { + $defs: { Alias: { $ref: "#/components/schemas/Value" } }, + properties: { value: { $ref: "#/components/schemas/Value" } }, + patternProperties: { pattern: { $ref: "#/components/schemas/Value" } }, + dependentSchemas: { dependency: { $ref: "#/components/schemas/Value" } }, + allOf: [{ $ref: "#/components/schemas/Value" }], + anyOf: [{ $ref: "#/components/schemas/Value" }], + oneOf: [{ $ref: "#/components/schemas/Value" }], + prefixItems: [{ $ref: "#/components/schemas/Value" }], + additionalProperties: { $ref: "#/components/schemas/Value" }, + unevaluatedProperties: { $ref: "#/components/schemas/Value" }, + propertyNames: { $ref: "#/components/schemas/Value" }, + items: { $ref: "#/components/schemas/Value" }, + contains: { $ref: "#/components/schemas/Value" }, + unevaluatedItems: { $ref: "#/components/schemas/Value" }, + not: { $ref: "#/components/schemas/Value" }, + if: { $ref: "#/components/schemas/Value" }, + // oxlint-disable-next-line unicorn/no-thenable -- JSON Schema keyword + then: { $ref: "#/components/schemas/Value" }, + else: { $ref: "#/components/schemas/Value" }, + contentSchema: { $ref: "#/components/schemas/Value" } + } + + deepStrictEqual(JsonSchema.fromSchemaOpenApi3_1(input), { + dialect: "draft-2020-12", + schema: { + properties: { value: { $ref: "#/$defs/Value" } }, + patternProperties: { pattern: { $ref: "#/$defs/Value" } }, + dependentSchemas: { dependency: { $ref: "#/$defs/Value" } }, + allOf: [{ $ref: "#/$defs/Value" }], + anyOf: [{ $ref: "#/$defs/Value" }], + oneOf: [{ $ref: "#/$defs/Value" }], + prefixItems: [{ $ref: "#/$defs/Value" }], + additionalProperties: { $ref: "#/$defs/Value" }, + unevaluatedProperties: { $ref: "#/$defs/Value" }, + propertyNames: { $ref: "#/$defs/Value" }, + items: { $ref: "#/$defs/Value" }, + contains: { $ref: "#/$defs/Value" }, + unevaluatedItems: { $ref: "#/$defs/Value" }, + not: { $ref: "#/$defs/Value" }, + if: { $ref: "#/$defs/Value" }, + // oxlint-disable-next-line unicorn/no-thenable -- JSON Schema keyword + then: { $ref: "#/$defs/Value" }, + else: { $ref: "#/$defs/Value" }, + contentSchema: { $ref: "#/$defs/Value" } + }, + definitions: { Alias: { $ref: "#/$defs/Value" } } + }) + }) + it("extracts root $defs after rewriting OpenAPI component refs", () => { const input: JsonSchema.JsonSchema = { type: "object", @@ -682,330 +936,253 @@ describe("JsonSchema", () => { }) }) - describe("nullable", () => { - it("expands nullable schema without other keywords to anyOf", () => { - assertFromSchemaOpenApi3_0( - { nullable: true }, - { - schema: { - anyOf: [ - {}, - { type: "null" } - ] - } - } - ) - }) - - it("adds null to a string type", () => { - const input: JsonSchema.JsonSchema = { + it("prefers examples over a singular example", () => { + assertFromSchemaOpenApi3_0( + { type: "string", - nullable: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", + example: "ignored", + examples: ["kept"] + }, + { schema: { - type: ["string", "null"] - }, - definitions: {} - }) - }) - - it("adds null to a type array", () => { - const input: JsonSchema.JsonSchema = { - type: ["string", "number"], - nullable: true + type: "string", + examples: ["kept"] + } } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: ["string", "number", "null"] - }, - definitions: {} - }) - }) + ) + }) - it("keeps a non-null const while adding null to the type", () => { - const input: JsonSchema.JsonSchema = { - type: "string", - const: "a", - nullable: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: ["string", "null"], - const: "a" - }, - definitions: {} - }) - }) + type OpenApi3_0Case = { + readonly name: string + readonly input: JsonSchema.JsonSchema + readonly expected: JsonSchema.JsonSchema + } - it("wraps a non-null const in anyOf when type is absent", () => { - const input: JsonSchema.JsonSchema = { - const: "a", - nullable: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - anyOf: [ - { const: "a" }, - { type: "null" } - ] - }, - definitions: {} - }) - }) + function testOpenApi3_0Cases(cases: ReadonlyArray): void { + for (const { expected, input, name } of cases) { + it(name, () => assertFromSchemaOpenApi3_0(input, { schema: expected })) + } + } - it("keeps a null const without adding anyOf", () => { - const input: JsonSchema.JsonSchema = { - const: null, - nullable: true + describe("nullable", () => { + testOpenApi3_0Cases([ + { + name: "expands a schema without other keywords to anyOf", + input: { nullable: true }, + expected: { anyOf: [{}, { type: "null" }] } + }, + { + name: "adds null to a string type", + input: { type: "string", nullable: true }, + expected: { type: ["string", "null"] } + }, + { + name: "adds null to a type array", + input: { type: ["string", "number"], nullable: true }, + expected: { type: ["string", "number", "null"] } + }, + { + name: "does not widen the null type", + input: { type: "null", nullable: true }, + expected: { type: "null" } + }, + { + name: "does not duplicate null in a type array", + input: { type: ["string", "null"], nullable: true }, + expected: { type: ["string", "null"] } + }, + { + name: "leaves a malformed type unchanged", + input: { type: 1, nullable: true }, + expected: { type: 1 } + }, + { + name: "keeps a non-null const while adding null to the type", + input: { type: "string", const: "a", nullable: true }, + expected: { type: ["string", "null"], const: "a" } + }, + { + name: "wraps a non-null const in anyOf when type is absent", + input: { const: "a", nullable: true }, + expected: { anyOf: [{ const: "a" }, { type: "null" }] } + }, + { + name: "keeps a null const without adding anyOf", + input: { const: null, nullable: true }, + expected: { const: null } + }, + { + name: "adds null to enum values and type", + input: { type: "string", enum: ["a", "b"], nullable: true }, + expected: { type: ["string", "null"], enum: ["a", "b", null] } + }, + { + name: "does not duplicate null in enum values", + input: { type: "string", enum: ["a", "b", null], nullable: true }, + expected: { type: ["string", "null"], enum: ["a", "b", null] } + }, + { + name: "preserves enum when null is its only value", + input: { type: "string", enum: [null], nullable: true }, + expected: { type: ["string", "null"], enum: [null] } + }, + { + name: "uses anyOf for schemas without type", + input: { nullable: true, minimum: 0 }, + expected: { anyOf: [{ minimum: 0 }, { type: "null" }] } + }, + { + name: "drops nullable false", + input: { type: "string", nullable: false }, + expected: { type: "string" } + }, + { + name: "normalizes nullable inside allOf independently from the parent", + input: { type: "string", allOf: [{ nullable: true }] }, + expected: { + type: "string", + allOf: [{ anyOf: [{}, { type: "null" }] }] + } + }, + { + name: "normalizes nullable on both a parent and its allOf member", + input: { type: "string", nullable: true, allOf: [{ nullable: true }] }, + expected: { + type: ["string", "null"], + allOf: [{ anyOf: [{}, { type: "null" }] }] + } } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - const: null - }, - definitions: {} - }) - }) + ]) + }) - it("adds null to enum values and type", () => { - const input: JsonSchema.JsonSchema = { - type: "string", - enum: ["a", "b"], - nullable: true + describe("exclusivity", () => { + testOpenApi3_0Cases([ + { + name: "turns exclusiveMinimum true into the minimum value", + input: { type: "number", minimum: 10, exclusiveMinimum: true }, + expected: { type: "number", exclusiveMinimum: 10 } + }, + { + name: "turns exclusiveMaximum true into the maximum value", + input: { type: "number", maximum: 100, exclusiveMaximum: true }, + expected: { type: "number", exclusiveMaximum: 100 } + }, + { + name: "drops exclusiveMinimum false", + input: { type: "number", minimum: 10, exclusiveMinimum: false }, + expected: { type: "number", minimum: 10 } + }, + { + name: "drops exclusiveMaximum false", + input: { type: "number", maximum: 100, exclusiveMaximum: false }, + expected: { type: "number", maximum: 100 } + }, + { + name: "drops exclusiveMinimum true when minimum is absent", + input: { type: "number", exclusiveMinimum: true }, + expected: { type: "number" } + }, + { + name: "drops exclusiveMaximum true when maximum is absent", + input: { type: "number", exclusiveMaximum: true }, + expected: { type: "number" } } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: ["string", "null"], - enum: ["a", "b", null] - }, - definitions: {} - }) + ]) + }) + }) + + describe("toDocumentDraft07", () => { + it("preserves Schema.Never", () => { + const result = JsonSchema.toDocumentDraft07(Schema.toJsonSchemaDocument(Schema.Never)) + deepStrictEqual(result, { + dialect: "draft-07", + schema: { not: {} }, + definitions: {} }) + }) - it("does not duplicate null in enum values", () => { - const input: JsonSchema.JsonSchema = { - type: "string", - enum: ["a", "b", null], - nullable: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: ["string", "null"], - enum: ["a", "b", null] - }, - definitions: {} - }) + it("omits an empty required array", () => { + const document = JsonSchema.toDocumentDraft07({ + dialect: "draft-2020-12", + schema: { type: "object", required: [] }, + definitions: {} }) - it("preserves enum when null is the only enum value", () => { - const input: JsonSchema.JsonSchema = { - type: "string", - enum: [null], - nullable: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { + deepStrictEqual(document.schema, { type: "object" }) + }) + + it("preserves every supported annotation and validation keyword", () => { + const schema: JsonSchema.JsonSchema = { + type: "object", + required: ["value"], + enum: ["a", "b"], + const: "a", + title: "title", + description: "description", + default: "a", + examples: ["a"], + format: "custom", + readOnly: true, + writeOnly: true, + pattern: "^a$", + minimum: 0, + maximum: 10, + exclusiveMinimum: 0, + exclusiveMaximum: 10, + minLength: 1, + maxLength: 10, + minItems: 1, + maxItems: 10, + minProperties: 1, + maxProperties: 10, + multipleOf: 2, + uniqueItems: true, + properties: { value: { type: "string" } }, + patternProperties: { "^x-": { type: "string" } }, + not: { type: "null" }, + additionalProperties: false, + propertyNames: { minLength: 1 }, + allOf: [{ type: "object" }], + anyOf: [{ type: "string" }], + oneOf: [{ type: "number" }], + items: { type: "string" } + } + + deepStrictEqual( + JsonSchema.toDocumentDraft07({ dialect: "draft-2020-12", - schema: { - type: ["string", "null"], - enum: [null] - }, + schema, definitions: {} - }) - }) + }).schema, + schema + ) + }) - it("uses anyOf for nullable schemas without type", () => { - const input: JsonSchema.JsonSchema = { - nullable: true, - minimum: 0 - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - anyOf: [ - { - minimum: 0 - }, - { - type: "null" - } - ] + it("preserves validation semantics", () => { + const document = JsonSchema.toDocumentDraft07({ + dialect: "draft-2020-12", + schema: { + type: "object", + properties: { + mode: { const: "on" }, + value: { type: "number", exclusiveMinimum: 0 }, + tuple: { type: "array", prefixItems: [{ type: "string" }], items: false } }, - definitions: {} - }) + required: ["mode", "value", "tuple"], + additionalProperties: false + }, + definitions: {} }) + const schema = makeSchema(document) + deepStrictEqual(ajvDraft07.validateSchema(schema), true) - it("drops nullable: false", () => { - const input: JsonSchema.JsonSchema = { - type: "string", - nullable: false - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "string" - }, - definitions: {} - }) - }) - - it("normalizes nullable inside allOf independently from the parent", () => { - assertFromSchemaOpenApi3_0( - { - type: "string", - allOf: [{ nullable: true }] - }, - { - schema: { - type: "string", - allOf: [{ - anyOf: [ - {}, - { type: "null" } - ] - }] - } - } - ) - assertFromSchemaOpenApi3_0( - { - type: "string", - nullable: true, - allOf: [{ nullable: true }] - }, - { - schema: { - type: ["string", "null"], - allOf: [{ - anyOf: [ - {}, - { type: "null" } - ] - }] - } - } - ) - }) + const validate = ajvDraft07.compile(schema) + deepStrictEqual(validate({ mode: "on", value: 1, tuple: ["a"] }), true) + deepStrictEqual(validate({ mode: "off", value: 1, tuple: ["a"] }), false) + deepStrictEqual(validate({ mode: "on", value: 0, tuple: ["a"] }), false) + deepStrictEqual(validate({ mode: "on", value: 1, tuple: ["a", "b"] }), false) }) - describe("exclusivity", () => { - it("turns exclusiveMinimum: true into exclusiveMinimum: minimum", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - minimum: 10, - exclusiveMinimum: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number", - exclusiveMinimum: 10 - }, - definitions: {} - }) - }) - - it("turns exclusiveMaximum: true into exclusiveMaximum: maximum", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - maximum: 100, - exclusiveMaximum: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number", - exclusiveMaximum: 100 - }, - definitions: {} - }) - }) - - it("drops exclusiveMinimum: false", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - minimum: 10, - exclusiveMinimum: false - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number", - minimum: 10 - }, - definitions: {} - }) - }) - - it("drops exclusiveMaximum: false", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - maximum: 100, - exclusiveMaximum: false - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number", - maximum: 100 - }, - definitions: {} - }) - }) - - it("drops exclusiveMinimum: true when minimum is absent", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - exclusiveMinimum: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number" - }, - definitions: {} - }) - }) - - it("drops exclusiveMaximum: true when maximum is absent", () => { - const input: JsonSchema.JsonSchema = { - type: "number", - exclusiveMaximum: true - } - const result = JsonSchema.fromSchemaOpenApi3_0(input) - deepStrictEqual(result, { - dialect: "draft-2020-12", - schema: { - type: "number" - }, - definitions: {} - }) - }) - }) - }) - - describe("toDocumentDraft07", () => { it("rewrites $defs refs to Draft-07 definitions refs", () => { const input: JsonSchema.Document<"draft-2020-12"> = { dialect: "draft-2020-12", @@ -1039,7 +1216,7 @@ describe("JsonSchema", () => { definitions: { A: { type: "string", - $ref: "#/definitions/B" + allOf: [{ $ref: "#/definitions/B" }] }, B: { type: "number" @@ -1048,6 +1225,60 @@ describe("JsonSchema", () => { }) }) + it("rewrites refs only in schema positions", () => { + const literal = { $ref: "#/$defs/Literal" } + const result = JsonSchema.toDocumentDraft07({ + dialect: "draft-2020-12", + schema: { + properties: { value: { $ref: "#/$defs/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + }, + definitions: { Value: { type: "string" } } + }) + + deepStrictEqual(result, { + dialect: "draft-07", + schema: { + properties: { value: { $ref: "#/definitions/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + }, + definitions: { Value: { type: "string" } } + }) + }) + + it("preserves constraints next to refs and existing allOf", () => { + const document = JsonSchema.toDocumentDraft07({ + dialect: "draft-2020-12", + schema: { + $ref: "#/$defs/S", + minLength: 3, + allOf: [{ maxLength: 5 }] + }, + definitions: { S: { type: "string" } } + }) + + deepStrictEqual(document.schema, { + minLength: 3, + allOf: [ + { $ref: "#/definitions/S" }, + { maxLength: 5 } + ] + }) + + const schema = makeSchema(document) + deepStrictEqual(ajvDraft07.validateSchema(schema), true) + const validate = ajvDraft07.compile(schema) + deepStrictEqual(validate("abc"), true) + deepStrictEqual(validate("a"), false) + deepStrictEqual(validate("abcdef"), false) + }) + it("converts prefixItems to a Draft-07 items tuple", () => { const input: JsonSchema.Document<"draft-2020-12"> = { dialect: "draft-2020-12", @@ -1114,10 +1345,434 @@ describe("JsonSchema", () => { definitions: {} }) }) + + it("preserves malformed values for recognized keywords", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + $ref: 1, + properties: "invalid", + not: [false, { type: "string" }], + allOf: "invalid", + prefixItems: "invalid" + }, + definitions: {} + } + + deepStrictEqual(JsonSchema.toDocumentDraft07(input), { + dialect: "draft-07", + schema: { + $ref: 1, + properties: "invalid", + not: [false, { type: "string" }], + allOf: "invalid", + items: "invalid" + }, + definitions: {} + }) + }) + }) + + describe("toDocumentDraft04", () => { + it("rewrites $defs refs to Draft-04 definitions refs", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + type: "object", + properties: { + value: { $ref: "#/$defs/Value" } + } + }, + definitions: { + Value: { type: "string" } + } + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result, { + dialect: "draft-04", + schema: { + type: "object", + properties: { + value: { $ref: "#/definitions/Value" } + } + }, + definitions: { + Value: { type: "string" } + } + }) + }) + + it("preserves every supported Draft-04 keyword and drops newer annotations", () => { + const input: JsonSchema.JsonSchema = { + type: "object", + required: ["value"], + enum: ["a", "b"], + title: "title", + description: "description", + default: "a", + format: "custom", + pattern: "^a$", + minimum: 0, + maximum: 10, + minLength: 1, + maxLength: 10, + minItems: 1, + maxItems: 10, + minProperties: 1, + maxProperties: 10, + multipleOf: 2, + uniqueItems: true, + properties: { value: { type: "string" } }, + patternProperties: { "^x-": { type: "string" } }, + not: { type: "null" }, + additionalProperties: false, + allOf: [{ type: "object" }], + anyOf: [{ type: "string" }], + oneOf: [{ type: "number" }], + items: { type: "string" }, + examples: ["a"], + readOnly: true, + writeOnly: true, + propertyNames: { minLength: 1 } + } + const expected: JsonSchema.JsonSchema = { + type: "object", + required: ["value"], + enum: ["a", "b"], + title: "title", + description: "description", + default: "a", + format: "custom", + pattern: "^a$", + minimum: 0, + maximum: 10, + minLength: 1, + maxLength: 10, + minItems: 1, + maxItems: 10, + minProperties: 1, + maxProperties: 10, + multipleOf: 2, + uniqueItems: true, + properties: { value: { type: "string" } }, + patternProperties: { "^x-": { type: "string" } }, + not: { type: "null" }, + additionalProperties: false, + allOf: [{ type: "object" }], + anyOf: [{ type: "string" }], + oneOf: [{ type: "number" }], + items: { type: "string" } + } + + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: input, + definitions: {} + }).schema, + expected + ) + }) + + it("omits an empty required array", () => { + const document = JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { type: "object", required: [] }, + definitions: {} + }) + + deepStrictEqual(document.schema, { type: "object" }) + deepStrictEqual(ajvDraft04.validateSchema(makeSchema(document)), true) + }) + + it("converts const to enum", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + enum: ["a", "b"], + const: "b", + allOf: [{ type: "string" }] + }, + definitions: {} + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result.schema, { + enum: ["a", "b"], + allOf: [{ type: "string" }, { enum: ["b"] }] + }) + + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { const: "a" }, + definitions: {} + }).schema, + { enum: ["a"] } + ) + + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { enum: ["a", "b"], const: "b" }, + definitions: {} + }).schema, + { enum: ["a", "b"], allOf: [{ enum: ["b"] }] } + ) + }) + + it("preserves refs in literal values", () => { + const literal = { $ref: "#/$defs/Literal" } + + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { const: literal }, + definitions: {} + }).schema, + { enum: [literal] } + ) + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { enum: [literal], default: literal }, + definitions: {} + }).schema, + { enum: [literal], default: literal } + ) + }) + + it("converts numeric exclusive bounds to Draft-04 boolean exclusivity", () => { + const cases: ReadonlyArray = [ + [{ minimum: 1 }, { minimum: 1 }], + [{ exclusiveMinimum: 1 }, { minimum: 1, exclusiveMinimum: true }], + [{ minimum: 2, exclusiveMinimum: 1 }, { minimum: 2 }], + [{ minimum: 1, exclusiveMinimum: 1 }, { minimum: 1, exclusiveMinimum: true }], + [{ minimum: 1, exclusiveMinimum: 2 }, { minimum: 2, exclusiveMinimum: true }], + [{ maximum: 2 }, { maximum: 2 }], + [{ exclusiveMaximum: 2 }, { maximum: 2, exclusiveMaximum: true }], + [{ maximum: 1, exclusiveMaximum: 2 }, { maximum: 1 }], + [{ maximum: 2, exclusiveMaximum: 2 }, { maximum: 2, exclusiveMaximum: true }], + [{ maximum: 2, exclusiveMaximum: 1 }, { maximum: 1, exclusiveMaximum: true }] + ] + for (const [schema, expected] of cases) { + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema, + definitions: {} + }).schema, + expected + ) + } + }) + + it("converts boolean schemas only in schema positions", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + type: "object", + properties: { + allowed: true, + denied: false, + nested: { not: false } + }, + additionalProperties: false, + allOf: [true], + anyOf: [false] + }, + definitions: {} + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result.schema, { + type: "object", + properties: { + allowed: {}, + denied: { not: {} }, + nested: { not: { not: {} } } + }, + additionalProperties: false, + allOf: [{}], + anyOf: [{ not: {} }] + }) + }) + + it("converts tuple members while preserving additionalItems booleans", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + type: "array", + prefixItems: [true, false], + items: false + }, + definitions: {} + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result.schema, { + type: "array", + items: [{}, { not: {} }], + additionalItems: false + }) + + deepStrictEqual( + JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { type: "array", items: false }, + definitions: {} + }).schema, + { type: "array", items: { not: {} } } + ) + }) + + it("converts schemas in additionalProperties and additionalItems", () => { + const result = JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { + type: "object", + additionalProperties: { const: "value" }, + properties: { + tuple: { + type: "array", + prefixItems: [{ type: "string" }], + items: { const: "rest" } + } + } + }, + definitions: {} + }) + + deepStrictEqual(result.schema, { + type: "object", + additionalProperties: { enum: ["value"] }, + properties: { + tuple: { + type: "array", + items: [{ type: "string" }], + additionalItems: { enum: ["rest"] } + } + } + }) + }) + + it("preserves malformed values for recognized keywords", () => { + const result = JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { + properties: "invalid", + not: [1], + allOf: "invalid" + }, + definitions: {} + }) + + deepStrictEqual(result.schema, { + properties: "invalid", + not: [1], + allOf: "invalid" + }) + }) + + it("preserves allOf, not, and null", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + anyOf: [ + { type: "null" }, + { allOf: [{ not: { type: "string" } }] } + ] + }, + definitions: {} + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result.schema, input.schema) + }) + + it("drops keywords unavailable in Draft-04", () => { + const input: JsonSchema.Document<"draft-2020-12"> = { + dialect: "draft-2020-12", + schema: { + type: "object", + propertyNames: { pattern: "^[a-z]+$" }, + examples: [{ value: 1 }] + }, + definitions: {} + } + const result = JsonSchema.toDocumentDraft04(input) + deepStrictEqual(result.schema, { type: "object" }) + }) + + it("preserves constraints next to refs", () => { + const document = JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { $ref: "#/$defs/S", minLength: 3 }, + definitions: { S: { type: "string" } } + }) + + deepStrictEqual(document.schema, { + allOf: [{ $ref: "#/definitions/S" }], + minLength: 3 + }) + + const schema = makeSchema(document) + deepStrictEqual(ajvDraft04.validateSchema(schema), true) + const validate = ajvDraft04.compile(schema) + deepStrictEqual(validate("abc"), true) + deepStrictEqual(validate("a"), false) + }) + + it("preserves validation semantics for converted constraints", () => { + const document = JsonSchema.toDocumentDraft04({ + dialect: "draft-2020-12", + schema: { + type: "object", + properties: { + mode: { const: "on" }, + value: { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 2 }, + tuple: { type: "array", prefixItems: [{ type: "string" }], items: false } + }, + required: ["mode", "value", "tuple"], + additionalProperties: false + }, + definitions: {} + }) + const schema = makeSchema(document) + deepStrictEqual(ajvDraft04.validateSchema(schema), true) + + const validate = ajvDraft04.compile(schema) + deepStrictEqual(validate({ mode: "on", value: 1, tuple: ["a"] }), true) + deepStrictEqual(validate({ mode: "off", value: 1, tuple: ["a"] }), false) + deepStrictEqual(validate({ mode: "on", value: 0, tuple: ["a"] }), false) + deepStrictEqual(validate({ mode: "on", value: 1, tuple: ["a", "b"] }), false) + }) + + it("converts documents generated from Effect schemas", () => { + const shared = Schema.Struct({ value: Schema.String }) + const schema = Schema.Struct({ + mode: Schema.Literal("enabled"), + threshold: Schema.Finite.check(Schema.isGreaterThan(0)), + tuple: Schema.Tuple([Schema.String, Schema.Boolean]), + left: shared, + right: shared + }) + const document = JsonSchema.toDocumentDraft04(Schema.toJsonSchemaDocument(schema)) + const draft04 = makeSchema(document) + deepStrictEqual(ajvDraft04.validateSchema(draft04), true) + + const validate = ajvDraft04.compile(draft04) + const valid = { + mode: "enabled", + threshold: 1, + tuple: ["a", true], + left: { value: "left" }, + right: { value: "right" } + } + deepStrictEqual(validate(valid), true) + deepStrictEqual(validate({ ...valid, threshold: 0 }), false) + deepStrictEqual(validate({ ...valid, tuple: ["a", true, false] }), false) + }) }) - describe("toDocumentOpenApi3_1", () => { - it("should rewrite `$defs` references to `components/schemas`", () => { + describe("toMultiDocumentOpenApi3_1", () => { + it("rewrites `$defs` references to `components/schemas`", () => { const input: JsonSchema.MultiDocument<"draft-2020-12"> = { dialect: "draft-2020-12", schemas: [ @@ -1163,6 +1818,33 @@ describe("JsonSchema", () => { }) }) + it("rewrites refs only in schema positions", () => { + const literal = { $ref: "#/$defs/Literal" } + const result = JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [{ + properties: { value: { $ref: "#/$defs/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + }], + definitions: { Value: { type: "string" } } + }) + + deepStrictEqual(result, { + dialect: "openapi-3.1", + schemas: [{ + properties: { value: { $ref: "#/components/schemas/Value" } }, + const: literal, + enum: [literal], + default: literal, + examples: [literal] + }], + definitions: { Value: { type: "string" } } + }) + }) + it("sanitizes component schema keys and rewritten refs together", () => { const input: JsonSchema.MultiDocument<"draft-2020-12"> = { dialect: "draft-2020-12", @@ -1196,12 +1878,260 @@ describe("JsonSchema", () => { } }) }) + + it("unescapes a definition key before sanitizing its ref", () => { + const result = JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [{ $ref: "#/$defs/A~1B" }], + definitions: { + "A/B": { type: "string" } + } + }) + + deepStrictEqual(result, { + dialect: "openapi-3.1", + schemas: [{ $ref: "#/components/schemas/A_B" }], + definitions: { + A_B: { type: "string" } + } + }) + }) + + it("suffixes a sanitized key that collides with a valid key", () => { + const result = JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [ + { + allOf: [ + { $ref: "#/$defs/A_B" }, + { $ref: "#/$defs/A~1B" } + ] + } + ], + definitions: { + A_B: { type: "string" }, + "A/B": { type: "number" } + } + }) + + deepStrictEqual(result, { + dialect: "openapi-3.1", + schemas: [ + { + allOf: [ + { $ref: "#/components/schemas/A_B" }, + { $ref: "#/components/schemas/A_B_1" } + ] + } + ], + definitions: { + A_B: { type: "string" }, + A_B_1: { type: "number" } + } + }) + }) + + it("allocates suffixes deterministically and skips occupied keys", () => { + const convert = (definitions: JsonSchema.Definitions) => + JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [ + { + allOf: [ + { $ref: "#/$defs/A_B" }, + { $ref: "#/$defs/A_B_1" }, + { $ref: "#/$defs/A~1B" }, + { $ref: "#/$defs/A?B" } + ] + } + ], + definitions + }) + const expected: JsonSchema.MultiDocument<"openapi-3.1"> = { + dialect: "openapi-3.1", + schemas: [ + { + allOf: [ + { $ref: "#/components/schemas/A_B" }, + { $ref: "#/components/schemas/A_B_1" }, + { $ref: "#/components/schemas/A_B_2" }, + { $ref: "#/components/schemas/A_B_3" } + ] + } + ], + definitions: { + A_B: { type: "string" }, + A_B_1: { type: "boolean" }, + A_B_2: { type: "number" }, + A_B_3: { type: "null" } + } + } + + deepStrictEqual( + convert({ + "A?B": { type: "null" }, + A_B_1: { type: "boolean" }, + "A/B": { type: "number" }, + A_B: { type: "string" } + }), + expected + ) + deepStrictEqual( + convert({ + A_B: { type: "string" }, + "A/B": { type: "number" }, + A_B_1: { type: "boolean" }, + "A?B": { type: "null" } + }), + expected + ) + }) + + it("reserves sanitized bases before allocating suffixes", () => { + const result = JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [ + { + allOf: [ + { $ref: "#/$defs/A~1B" }, + { $ref: "#/$defs/A?B" }, + { $ref: "#/$defs/A?B?1" } + ] + } + ], + definitions: { + "A/B": { type: "number" }, + "A?B": { type: "string" }, + "A?B?1": { type: "boolean" } + } + }) + + deepStrictEqual(result, { + dialect: "openapi-3.1", + schemas: [ + { + allOf: [ + { $ref: "#/components/schemas/A_B" }, + { $ref: "#/components/schemas/A_B_2" }, + { $ref: "#/components/schemas/A_B_1" } + ] + } + ], + definitions: { + A_B: { type: "number" }, + A_B_2: { type: "string" }, + A_B_1: { type: "boolean" } + } + }) + }) + + it("rewrites nested definition refs without changing other refs", () => { + const result = JsonSchema.toMultiDocumentOpenApi3_1({ + dialect: "draft-2020-12", + schemas: [ + { + allOf: [ + { $ref: "#/$defs/A~1B/properties/value" }, + { $ref: "https://example.com/schema#/$defs/A~1B" }, + { $ref: "#/other/A~1B" } + ] + } + ], + definitions: { + "A/B": { + type: "object", + properties: { value: { type: "string" } } + } + } + }) + + deepStrictEqual(result, { + dialect: "openapi-3.1", + schemas: [ + { + allOf: [ + { $ref: "#/components/schemas/A_B/properties/value" }, + { $ref: "https://example.com/schema#/$defs/A~1B" }, + { $ref: "#/other/A~1B" } + ] + } + ], + definitions: { + A_B: { + type: "object", + properties: { value: { type: "string" } } + } + } + }) + }) + }) + + describe("input immutability", () => { + const schema: JsonSchema.JsonSchema = { + type: "object", + properties: { + value: { + type: "array", + prefixItems: [{ type: "string" }], + items: false, + nullable: true + } + }, + $defs: { + Value: { type: "string" } + } + } + + for ( + const [name, convert] of [ + ["fromSchemaDraft07", JsonSchema.fromSchemaDraft07], + ["fromSchemaDraft2020_12", JsonSchema.fromSchemaDraft2020_12], + ["fromSchemaOpenApi3_1", JsonSchema.fromSchemaOpenApi3_1], + ["fromSchemaOpenApi3_0", JsonSchema.fromSchemaOpenApi3_0] + ] as const + ) { + it(`${name} does not mutate its input`, () => { + assertDoesNotMutate(structuredClone(schema), convert) + }) + } + + for ( + const [name, convert] of [ + ["toDocumentDraft07", JsonSchema.toDocumentDraft07], + ["toDocumentDraft04", JsonSchema.toDocumentDraft04] + ] as const + ) { + it(`${name} does not mutate its input`, () => { + assertDoesNotMutate( + { + dialect: "draft-2020-12", + schema: structuredClone(schema), + definitions: { Value: { type: "string" } } + }, + convert + ) + }) + } + + it("toMultiDocumentOpenApi3_1 does not mutate its input", () => { + assertDoesNotMutate( + { + dialect: "draft-2020-12", + schemas: [structuredClone(schema)] as const, + definitions: { Value: { type: "string" } } + }, + JsonSchema.toMultiDocumentOpenApi3_1 + ) + }) }) describe("roundtrip conversions", () => { it("preserves a Draft-07 schema and definitions through canonical form", () => { const original: JsonSchema.JsonSchema = { type: "object", + readOnly: true, + writeOnly: true, + not: { required: ["forbidden"] }, properties: { name: { type: "string" }, items: { @@ -1228,6 +2158,9 @@ describe("JsonSchema", () => { deepStrictEqual(backTo07.schema, { type: "object", + readOnly: true, + writeOnly: true, + not: { required: ["forbidden"] }, properties: { name: { type: "string" }, items: { diff --git a/.context/effect/packages/effect/test/Latch.test.ts b/.context/effect/packages/effect/test/Latch.test.ts index e59b4d810..5f4e3705f 100644 --- a/.context/effect/packages/effect/test/Latch.test.ts +++ b/.context/effect/packages/effect/test/Latch.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Latch } from "effect" +import { Effect, Exit, Fiber, Latch } from "effect" +import * as Scheduler from "effect/Scheduler" describe("Latch", () => { it.effect("release wakes current waiters and keeps the latch closed", () => @@ -30,4 +31,217 @@ describe("Latch", () => { assert.isTrue(latch.isOpen()) })) + + it.effect("open then close does not resume waiters registered after close", () => + Effect.gen(function*() { + const latch = Latch.makeUnsafe(false) + const before = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + + yield* latch.open + yield* latch.close + const after = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + + yield* Effect.yieldNow + yield* Effect.yieldNow + + assert.isDefined(before.pollUnsafe()) + assert.isUndefined(after.pollUnsafe()) + })) + + it.effect("release while a flush is pending covers the new waiters", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + const tasks: Array<() => void> = [] + const scheduler: Scheduler.Scheduler = { + executionMode: "async", + makeDispatcher() { + return { + scheduleTask(task, _priority) { + tasks.push(task) + }, + flush() { + } + } + }, + shouldYield: () => false + } + + const first = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + assert.lengthOf(tasks, 1) + + const second = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + assert.lengthOf(tasks, 1) + + tasks.shift()!() + yield* Fiber.join(first) + yield* Fiber.join(second) + assert.isFalse(latch.isOpen()) + })) + + it.effect("release does not resume waiters registered after the release", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + const covered = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + + yield* latch.release + const late = yield* Effect.forkChild( + Latch.await(latch), + { startImmediately: true } + ) + + yield* Effect.yieldNow + yield* Effect.yieldNow + + assert.isDefined(covered.pollUnsafe()) + assert.isUndefined(late.pollUnsafe()) + })) + + it.effect("openUnsafe does not resume waiters registered after a reentrant close", () => + Effect.gen(function*() { + const latch = Latch.makeUnsafe(false) + const tasks: Array<() => void> = [] + const scheduler: Scheduler.Scheduler = { + executionMode: "async", + makeDispatcher() { + return { + scheduleTask(task, _priority) { + tasks.push(task) + }, + flush() { + } + } + }, + shouldYield: () => false + } + + let second: Fiber.Fiber | undefined + yield* Effect.forkChild( + Effect.gen(function*() { + yield* Latch.await(latch) + latch.closeUnsafe() + second = yield* Effect.forkChild(Latch.await(latch), { startImmediately: true }) + return yield* Effect.never + }), + { startImmediately: true } + ) + + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + latch.openUnsafe() + assert.isUndefined(second!.pollUnsafe()) + assert.lengthOf(tasks, 1) + tasks.shift()!() + assert.isUndefined(second!.pollUnsafe()) + })) + + it.effect("interrupting a waiter removes it from a pending flush", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + const tasks: Array<() => void> = [] + const scheduler: Scheduler.Scheduler = { + executionMode: "async", + makeDispatcher() { + return { + scheduleTask(task, _priority) { + tasks.push(task) + }, + flush() { + } + } + }, + shouldYield: () => false + } + + const resumed: Array = [] + const waiter = yield* Effect.forkChild( + Effect.gen(function*() { + yield* Latch.await(latch) + resumed.push("waiter") + }), + { startImmediately: true } + ) + + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + assert.lengthOf(tasks, 1) + + yield* Fiber.interrupt(waiter) + const exit = yield* Fiber.await(waiter) + assert.isTrue(Exit.hasInterrupts(exit)) + + tasks.shift()!() + assert.deepStrictEqual(resumed, []) + assert.isFalse(latch.isOpen()) + })) + + it.effect("await is interruptible and cleans up interrupted waiters", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + const tasks: Array<() => void> = [] + const scheduler: Scheduler.Scheduler = { + executionMode: "async", + makeDispatcher() { + return { + scheduleTask(task, _priority) { + tasks.push(task) + }, + flush() { + } + } + }, + shouldYield: () => false + } + const resumed: Array = [] + const interrupted = yield* Effect.forkChild( + Effect.gen(function*() { + yield* Latch.await(latch) + resumed.push("interrupted") + }), + { startImmediately: true } + ) + + assert.isUndefined(interrupted.pollUnsafe()) + + yield* Fiber.interrupt(interrupted) + const exit = yield* Fiber.await(interrupted) + assert.isTrue(Exit.hasInterrupts(exit)) + + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + assert.lengthOf(tasks, 0) + + const live = yield* Effect.forkChild( + Effect.gen(function*() { + yield* Latch.await(latch) + resumed.push("live") + }), + { startImmediately: true } + ) + + assert.isUndefined(live.pollUnsafe()) + + yield* latch.release.pipe(Effect.provideService(Scheduler.Scheduler, scheduler)) + assert.lengthOf(tasks, 1) + assert.isUndefined(live.pollUnsafe()) + + tasks.shift()!() + yield* Fiber.join(live) + + assert.deepStrictEqual(resumed, ["live"]) + assert.isFalse(latch.isOpen()) + })) }) diff --git a/.context/effect/packages/effect/test/Layer.test.ts b/.context/effect/packages/effect/test/Layer.test.ts index 7722b4619..a8f4bcd79 100644 --- a/.context/effect/packages/effect/test/Layer.test.ts +++ b/.context/effect/packages/effect/test/Layer.test.ts @@ -452,6 +452,17 @@ describe("Layer", () => { }) describe("MemoMap", () => { + it("does not memoize a build before its Effect executes", () => { + const memoMap = Layer.makeMemoMapUnsafe() + const scope = Scope.makeUnsafe() + const layer = Layer.effectDiscard(Effect.void) + + // @effect-diagnostics-next-line floatingEffect:off + Layer.buildWithMemoMap(layer, memoMap, scope) + + assert.strictEqual((memoMap as any).map.size, 0) + }) + it.effect("memoizes suspend across builds", () => Effect.gen(function*() { const arr: Array = [] diff --git a/.context/effect/packages/effect/test/LayerMap.test.ts b/.context/effect/packages/effect/test/LayerMap.test.ts index e7e064a42..5ff1d4b9b 100644 --- a/.context/effect/packages/effect/test/LayerMap.test.ts +++ b/.context/effect/packages/effect/test/LayerMap.test.ts @@ -16,6 +16,17 @@ const makeLayer = (key: string, acquired: Array, released: Array ) as Layer.Layer describe("LayerMap", () => { + it.effect("make preloads the requested keys", () => + Effect.gen(function*() { + const acquired: Array = [] + yield* LayerMap.make( + (key: string) => Layer.effectDiscard(Effect.sync(() => acquired.push(key))) as Layer.Layer, + { preloadKeys: ["a", "b"] } + ) + + assert.deepStrictEqual(acquired, ["a", "b"]) + })) + it.effect("make supports dynamic idleTimeToLive", () => Effect.gen(function*() { const acquired: Array = [] diff --git a/.context/effect/packages/effect/test/Logger.test.ts b/.context/effect/packages/effect/test/Logger.test.ts index 5871e8752..b121e1f40 100644 --- a/.context/effect/packages/effect/test/Logger.test.ts +++ b/.context/effect/packages/effect/test/Logger.test.ts @@ -90,6 +90,31 @@ describe("Logger", () => { assert.ok(!output.includes("annotation=\"\\\"value with spaces\\\"\"")) })) + it.effect("formats BigInt messages consistently", () => + Effect.gen(function*() { + const simple: Array = [] + const logFmt: Array = [] + const structured: Array<{ readonly message: unknown; readonly level: string }> = [] + const json: Array<{ readonly message: unknown; readonly level: string }> = [] + const loggers = [ + Logger.formatSimple.pipe(Logger.map((output) => void simple.push(output))), + Logger.formatLogFmt.pipe(Logger.map((output) => void logFmt.push(output))), + Logger.formatStructured.pipe(Logger.map((output) => void structured.push(output))), + Logger.formatJson.pipe(Logger.map((output) => void json.push(JSON.parse(output)))) + ] + + yield* Effect.logInfo(123n, { value: 123n }).pipe(Effect.provide(Logger.layer(loggers))) + + assert.include(simple[0], ` level=INFO fiber=`) + assert.include(simple[0], `message=123n message="{\\"value\\":123n}"`) + assert.include(logFmt[0], ` level=INFO fiber=`) + assert.include(logFmt[0], `message=123n message="{\\"value\\":123n}"`) + assert.deepStrictEqual(structured[0].message, [123n, { value: 123n }]) + assert.strictEqual(structured[0].level, "INFO") + assert.deepStrictEqual(json[0].message, ["123n", { value: "123n" }]) + assert.strictEqual(json[0].level, "INFO") + })) + it.effect("annotateLogsScoped applies annotations only while scoped", () => Effect.gen(function*() { const annotations: Array> = [] diff --git a/.context/effect/packages/effect/test/ManagedRuntime.test.ts b/.context/effect/packages/effect/test/ManagedRuntime.test.ts index 91a3dac48..62bdc78b8 100644 --- a/.context/effect/packages/effect/test/ManagedRuntime.test.ts +++ b/.context/effect/packages/effect/test/ManagedRuntime.test.ts @@ -47,6 +47,21 @@ describe("ManagedRuntime", () => { strictEqual(result, "test") }) + test("supports await using", async () => { + let count = 0 + const layer = Layer.effectDiscard(Effect.addFinalizer(() => + Effect.sync(() => { + count++ + }) + )) + { + await using runtime = ManagedRuntime.make(layer) + await runtime.runPromise(Effect.void) + strictEqual(count, 0) + } + strictEqual(count, 1) + }) + it("fibers are interrupted on dispose", async () => { const runtime = ManagedRuntime.make(Layer.empty) const fiber = runtime.runFork(Effect.never) diff --git a/.context/effect/packages/effect/test/Match.test.ts b/.context/effect/packages/effect/test/Match.test.ts index 04507a658..2b14f6e8e 100644 --- a/.context/effect/packages/effect/test/Match.test.ts +++ b/.context/effect/packages/effect/test/Match.test.ts @@ -24,4 +24,17 @@ describe("Match", () => { strictEqual(match({ _tag: "A.one" }), "hit") strictEqual(match(null), "miss") }) + + it("checks symbol-keyed object pattern properties", () => { + const key = Symbol("key") + const match = pipe( + Match.type<{ readonly [key]: "expected" | "other" }>(), + Match.when({ [key]: "expected" }, () => "hit"), + Match.orElse(() => "miss") + ) + + strictEqual(match({ [key]: "expected" }), "hit") + strictEqual(match({ [key]: "other" }), "miss") + strictEqual(match({} as any), "miss") + }) }) diff --git a/.context/effect/packages/effect/test/Metric.test.ts b/.context/effect/packages/effect/test/Metric.test.ts index 66fc1d1f3..5defa99be 100644 --- a/.context/effect/packages/effect/test/Metric.test.ts +++ b/.context/effect/packages/effect/test/Metric.test.ts @@ -6,6 +6,19 @@ import { TestClock } from "effect/testing" const attributes = { x: "a", y: "b" } describe("Metric", () => { + it.effect("keeps distinct attribute sets in separate series", () => + Effect.gen(function*() { + const id = nextId() + const first = Metric.counter(id, { attributes: { a: "b,c=d" } }) + const second = Metric.counter(id, { attributes: { a: "b", c: "d" } }) + + yield* Metric.update(first, 1) + yield* Metric.update(second, 10) + + assert.strictEqual((yield* Metric.value(first)).count, 1) + assert.strictEqual((yield* Metric.value(second)).count, 10) + })) + it.effect("should be referentially transparent", () => Effect.gen(function*() { const id = nextId() @@ -346,7 +359,44 @@ describe("Metric", () => { })) }) + it.effect("uses finite extrema for empty histogram and summary states", () => + Effect.gen(function*() { + const histogram = Metric.histogram(nextId(), { boundaries: [] }) + const summary = Metric.summary(nextId(), { + maxAge: "1 minute", + maxSize: 10, + quantiles: [] + }) + const histogramState = yield* Metric.value(histogram) + const summaryState = yield* Metric.value(summary) + assert.deepStrictEqual( + { min: histogramState.min, max: histogramState.max }, + { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } + ) + assert.deepStrictEqual( + { min: summaryState.min, max: summaryState.max }, + { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } + ) + })) + + it("creates evenly spaced linear boundaries", () => { + assert.deepStrictEqual( + Metric.linearBoundaries({ start: 10, width: 20, count: 5 }), + [10, 30, 50, 70, Number.POSITIVE_INFINITY] + ) + }) + describe("Histogram", () => { + it.effect("reports the maximum for negative-only observations", () => + Effect.gen(function*() { + const histogram = Metric.histogram(nextId(), { boundaries: [-10, -5, 0] }) + yield* Metric.update(histogram, -10) + yield* Metric.update(histogram, -5) + const result = yield* Metric.value(histogram) + assert.strictEqual(result.min, -10) + assert.strictEqual(result.max, -5) + })) + it.effect("custom observe with value", () => Effect.gen(function*() { const id = nextId() @@ -403,6 +453,20 @@ describe("Metric", () => { }) describe("Summary", () => { + it.effect("reports the maximum for negative-only observations", () => + Effect.gen(function*() { + const summary = Metric.summary(nextId(), { + maxAge: "1 minute", + maxSize: 10, + quantiles: [0.5] + }) + yield* Metric.update(summary, -10) + yield* Metric.update(summary, -5) + const result = yield* Metric.value(summary) + assert.strictEqual(result.min, -10) + assert.strictEqual(result.max, -5) + })) + it.effect("custom observe with value", () => Effect.gen(function*() { const id = nextId() @@ -640,6 +704,23 @@ describe("Metric", () => { assert.strictEqual(result.max, Duration.toMillis(Duration.hours(1))) assert.strictEqual(result.sum, Duration.toMillis(Duration.hours(1))) })) + + it.effect("uses monotonic time when wall time moves backward", () => + Effect.gen(function*() { + const id = nextId() + const timer = Metric.timer(id) + yield* TestClock.setTime(1_000) + yield* Effect.gen(function*() { + yield* TestClock.adjust("100 millis") + yield* TestClock.setTime(0) + }).pipe(Effect.trackDuration(timer)) + + const result = yield* Metric.value(timer) + assert.strictEqual(result.count, 1) + assert.strictEqual(result.min, 100) + assert.strictEqual(result.max, 100) + assert.strictEqual(result.sum, 100) + })) }) describe("trackDurationWith", () => { diff --git a/.context/effect/packages/effect/test/Migrator.test.ts b/.context/effect/packages/effect/test/Migrator.test.ts index fb66c1fa2..3a821ac69 100644 --- a/.context/effect/packages/effect/test/Migrator.test.ts +++ b/.context/effect/packages/effect/test/Migrator.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, FileSystem } from "effect" +import { Effect, FileSystem, Layer, Path, PlatformError } from "effect" import * as Migrator from "effect/unstable/sql/Migrator" const migrationNames = ( @@ -55,7 +55,8 @@ describe("Migrator", () => { "0001_first.js", "0005_ignored.cjs" ]) - })) + })), + Effect.provide(Path.layer) ) assert.deepStrictEqual(migrationNames(migrations), [ @@ -65,5 +66,61 @@ describe("Migrator", () => { [4, "fourth"] ]) })) + + it.effect("fromFileSystem imports migrations through the file URL the platform resolves", () => + Effect.gen(function*() { + const posix = yield* Effect.provide(Path.Path, Path.layer) + const migrationUrl = new URL("./fixtures/migrator/0001_first.js", import.meta.url) + let resolvedPath: string | undefined + // stand in for a Windows platform layer, which core has no implementation of + const windowsLike = Layer.succeed(Path.Path)({ + ...posix, + join: (...segments: ReadonlyArray) => segments.join("\\"), + toFileUrl: (path: string) => { + resolvedPath = path + return Effect.succeed(migrationUrl) + } + }) + + const migrations = yield* Migrator.fromFileSystem("C:\\migrations").pipe( + Effect.provide(FileSystem.layerNoop({ + readDirectory: () => Effect.succeed(["0001_first.js"]) + })), + Effect.provide(windowsLike) + ) + + // the loader is lazy, and `ResolvedMigration` declares a `SqlClient` + // requirement this loader never uses + const load = migrations[0]![2] as Effect.Effect + const imported = yield* load + + assert.strictEqual(resolvedPath, "C:\\migrations\\0001_first.js") + assert.strictEqual((imported as { readonly marker: string }).marker, "loaded") + })) + + it.effect("fromFileSystem surfaces file URL failures as import errors", () => + Effect.gen(function*() { + const posix = yield* Effect.provide(Path.Path, Path.layer) + const failingPath = Layer.succeed(Path.Path)({ + ...posix, + toFileUrl: () => Effect.fail(new PlatformError.BadArgument({ module: "Path", method: "toFileUrl" })) + }) + + const migrations = yield* Migrator.fromFileSystem("/migrations").pipe( + Effect.provide(FileSystem.layerNoop({ + readDirectory: () => Effect.succeed(["0001_first.js"]) + })), + Effect.provide(failingPath) + ) + + const load = migrations[0]![2] as Effect.Effect + const outcome = yield* load.pipe( + Effect.catchDefect((defect) => Effect.succeed(`defect: ${defect}`)), + Effect.orElseSucceed(() => "no defect") + ) + + // a typed failure here would escape `make`'s MigrationError | SqlError channel + assert.include(outcome, "defect:") + })) }) }) diff --git a/.context/effect/packages/effect/test/MutableList.test.ts b/.context/effect/packages/effect/test/MutableList.test.ts index a939553fc..99ae7e8b3 100644 --- a/.context/effect/packages/effect/test/MutableList.test.ts +++ b/.context/effect/packages/effect/test/MutableList.test.ts @@ -3,6 +3,22 @@ import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { MutableList } from "effect" describe("MutableList", () => { + it("preserves a prepended element when appending to the list", () => { + const list = MutableList.make() + MutableList.prepend(list, 1) + MutableList.append(list, 2) + + strictEqual(MutableList.toArray(list).join(","), "1,2") + strictEqual(list.length, 2) + }) + + it("returns an empty snapshot for a negative bound", () => { + const list = MutableList.make() + MutableList.append(list, 1) + + deepStrictEqual(MutableList.toArrayN(list, -1), []) + }) + it("appendAll returns 0 and leaves an empty list empty", () => { const list = MutableList.make() diff --git a/.context/effect/packages/effect/test/Optic.test.ts b/.context/effect/packages/effect/test/Optic.test.ts index c15b88255..989f9c3df 100644 --- a/.context/effect/packages/effect/test/Optic.test.ts +++ b/.context/effect/packages/effect/test/Optic.test.ts @@ -1,9 +1,36 @@ -import { Optic, Option, Result, Schema } from "effect" +import { Optic, Option, Result, Schema, SchemaIssue } from "effect" import { describe, it } from "vitest" -import { assertFailure, assertSuccess, assertTrue, deepStrictEqual, strictEqual, throws } from "./utils/assert.ts" +import { assertSuccess, assertTrue, deepStrictEqual, strictEqual, throws } from "./utils/assert.ts" const addOne = (n: number) => n + 1 +function getFailure(result: Result.Result): SchemaIssue.Issue { + assertTrue(Result.isFailure(result)) + return result.failure +} + +function assertFailureMessage(result: Result.Result, message: string) { + strictEqual(SchemaIssue.defaultFormatter(getFailure(result)), message) +} + +function assertFailureIssue(result: Result.Result, issue: SchemaIssue.Issue) { + strictEqual(getFailure(result), issue) +} + +function assertPointerFailure( + result: Result.Result, + path: ReadonlyArray +): SchemaIssue.Issue { + const failure = getFailure(result) + assertTrue(failure._tag === "Pointer") + deepStrictEqual(failure.path, path) + return failure.issue +} + +function assertMissingKeyFailure(result: Result.Result, key: PropertyKey) { + assertTrue(assertPointerFailure(result, [key])._tag === "MissingKey") +} + describe("Optic", () => { it("replace should throw an error if the object has a non-Object constructor or null prototype", () => { { @@ -27,6 +54,22 @@ describe("Optic", () => { } }) + it("preserves __proto__ as an own property", () => { + const value = { polluted: true } + const optics: ReadonlyArray, object]> = [ + [Optic.id>().key("__proto__"), {}], + [Optic.id>().optionalKey("__proto__"), {}], + [Optic.id>().at("__proto__"), { ["__proto__"]: 0 }] + ] + + for (const [optic, source] of optics) { + const out = optic.replace(value, source) + strictEqual(Object.getPrototypeOf(out), Object.prototype) + assertTrue(Object.hasOwn(out, "__proto__")) + strictEqual(out["__proto__"], value) + } + }) + it("id", () => { const iso = Optic.id() @@ -35,6 +78,109 @@ describe("Optic", () => { strictEqual(iso.modify(addOne)(1), 2) }) + describe("compose", () => { + it("sets through composed isos without reading a source", () => { + const value = Optic.makeIso<{ readonly value: number }, number>( + (s) => s.value, + (value) => ({ value }) + ) + const text = Optic.makeIso( + String, + Number + ) + const optic = value.compose(text) + + deepStrictEqual(optic.set("2"), { value: 2 }) + deepStrictEqual(optic.replace("2", { value: 1 }), { value: 2 }) + }) + + it("sets through composed prisms without reading a source", () => { + const value = Optic.makePrism<{ readonly value: number }, number>( + (s) => Result.succeed(s.value), + (value) => ({ value }) + ) + const text = Optic.makePrism( + (n) => Result.succeed(String(n)), + Number + ) + const optic = value.compose(text) + + deepStrictEqual(optic.set("2"), { value: 2 }) + }) + + it("preserves capabilities and behavior across every optic kind", () => { + const optics = { + Iso: Optic.makeIso((n) => n, (n) => n), + Lens: Optic.makeLens((n) => n, (n) => n), + Prism: Optic.makePrism(Result.succeed, (n) => n), + Optional: Optic.makeOptional(Result.succeed, (n) => Result.succeed(n)) + } as const + const expected = { + Iso: { Iso: "Iso", Lens: "Lens", Prism: "Prism", Optional: "Optional" }, + Lens: { Iso: "Lens", Lens: "Lens", Prism: "Optional", Optional: "Optional" }, + Prism: { Iso: "Prism", Lens: "Optional", Prism: "Prism", Optional: "Optional" }, + Optional: { Iso: "Optional", Lens: "Optional", Prism: "Optional", Optional: "Optional" } + } as const + + for (const left of Object.keys(optics) as Array) { + for (const right of Object.keys(optics) as Array) { + const optic: any = optics[left].compose(optics[right] as any) + const kind = expected[left][right] + + strictEqual("get" in optic, kind === "Iso" || kind === "Lens") + strictEqual("set" in optic, kind === "Iso" || kind === "Prism") + assertSuccess(optic.getResult(1), 1) + assertSuccess(optic.replaceResult(2, 1), 2) + } + } + }) + + it("preserves optional setter failures", () => { + const issue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) + const optic = Optic.makeOptional( + Result.succeed, + () => Result.fail(issue) + ).compose(Optic.makeIso((n) => n, (n) => n)) + + assertFailureIssue(optic.replaceResult(2, 1), issue) + strictEqual(optic.replace(2, 1), 1) + strictEqual(optic.modify(addOne)(1), 1) + }) + + it("composes identity in both directions", () => { + const value = Optic.makeIso(String, Number) + const left = Optic.id().compose(value) + const right = value.compose(Optic.id()) + const identity = Optic.id().compose(Optic.id()) + + strictEqual(left.get(1), "1") + strictEqual(right.get(1), "1") + strictEqual(left.set("2"), 2) + strictEqual(right.set("2"), 2) + strictEqual(identity.get(1), 1) + strictEqual(identity.set(2), 2) + }) + + it("is independent of composition grouping", () => { + const wrapped = Optic.makeIso<{ readonly value: number }, number>( + (s) => s.value, + (value) => ({ value }) + ) + const text = Optic.makeIso(String, Number) + const chars = Optic.makeIso>( + (s) => [...s], + (chars) => chars.join("") + ) + const left = wrapped.compose(text).compose(chars) + const right = wrapped.compose(text.compose(chars)) + + deepStrictEqual(left.get({ value: 12 }), ["1", "2"]) + deepStrictEqual(right.get({ value: 12 }), ["1", "2"]) + deepStrictEqual(left.set(["3", "4"]), { value: 34 }) + deepStrictEqual(right.set(["3", "4"]), { value: 34 }) + }) + }) + describe("key", () => { describe("Struct", () => { it("required key", () => { @@ -201,7 +347,7 @@ describe("Optic", () => { type S = number const optic = Optic.id().check(Schema.isGreaterThan(0)) assertSuccess(optic.getResult(1), 1) - assertFailure(optic.getResult(0), `Expected a value greater than 0, got 0`) + assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) strictEqual(optic.set(1), 1) strictEqual(optic.set(0), 0) deepStrictEqual(optic.modify(addOne)(1), 2) @@ -212,18 +358,30 @@ describe("Optic", () => { type S = number const optic = Optic.id().check(Schema.isInt(), Schema.isGreaterThan(0)) assertSuccess(optic.getResult(1), 1) - assertFailure(optic.getResult(0), `Expected a value greater than 0, got 0`) - assertFailure(optic.getResult(1.1), `Expected an integer, got 1.1`) - assertFailure( + assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) + assertFailureMessage(optic.getResult(1.1), `Expected an integer`) + assertFailureMessage( optic.getResult(-1.1), - `Expected an integer, got -1.1 -Expected a value greater than 0, got -1.1` + `Expected an integer +Expected a value greater than 0` ) deepStrictEqual(optic.modify(addOne)(1), 2) deepStrictEqual(optic.modify(addOne)(0), 0) deepStrictEqual(optic.modify(addOne)(1.1), 1.1) deepStrictEqual(optic.modify(addOne)(-1.1), -1.1) }) + + it("combines checks across successive calls", () => { + const optic = Optic.id() + .check(Schema.isInt()) + .check(Schema.isGreaterThan(0)) + + assertFailureMessage( + optic.getResult(-1.1), + `Expected an integer +Expected a value greater than 0` + ) + }) }) it("refine", () => { @@ -235,7 +393,7 @@ Expected a value greater than 0, got -1.1` ).key("b") assertSuccess(optic.getResult({ _tag: "b", b: 1 }), 1) - assertFailure(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag, got {"_tag":"a","a":"value"}`) + assertFailureMessage(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag`) deepStrictEqual(optic.modify(addOne)({ _tag: "a", a: "value" }), { _tag: "a", a: "value" }) deepStrictEqual(optic.modify(addOne)({ _tag: "b", b: 1 }), { _tag: "b", b: 2 }) }) @@ -245,7 +403,7 @@ Expected a value greater than 0, got -1.1` const optic = Optic.id().tag("b").key("b") assertSuccess(optic.getResult({ _tag: "b", b: 1 }), 1) - assertFailure(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag, got "a"`) + assertFailureMessage(optic.getResult({ _tag: "a", a: "value" }), `Expected "b" tag`) deepStrictEqual(optic.modify(addOne)({ _tag: "a", a: "value" }), { _tag: "a", a: "value" }) deepStrictEqual(optic.modify(addOne)({ _tag: "b", b: 1 }), { _tag: "b", b: 2 }) }) @@ -256,9 +414,9 @@ Expected a value greater than 0, got -1.1` const optic = Optic.id().at("a") assertSuccess(optic.getResult({ a: 1, b: 2 }), 1) - assertFailure(optic.getResult({ b: 2 }), `Key "a" not found`) + assertMissingKeyFailure(optic.getResult({ b: 2 }), "a") assertSuccess(optic.replaceResult(2, { a: 1, b: 2 }), { a: 2, b: 2 }) - assertFailure(optic.replaceResult(2, { b: 2 }), `Key "a" not found`) + assertMissingKeyFailure(optic.replaceResult(2, { b: 2 }), "a") deepStrictEqual(optic.replace(2, { a: 1, b: 2 }), { a: 2, b: 2 }) deepStrictEqual(optic.replace(2, { b: 2 }), { b: 2 }) }) @@ -268,9 +426,9 @@ Expected a value greater than 0, got -1.1` const optic = Optic.id().at(0) assertSuccess(optic.getResult([1, 2]), 1) - assertFailure(optic.getResult([]), `Key 0 not found`) + assertMissingKeyFailure(optic.getResult([]), 0) assertSuccess(optic.replaceResult(3, [1, 2]), [3, 2]) - assertFailure(optic.replaceResult(2, []), `Key 0 not found`) + assertMissingKeyFailure(optic.replaceResult(2, []), 0) deepStrictEqual(optic.replace(3, [1, 2]), [3, 2]) deepStrictEqual(optic.replace(2, []), []) }) @@ -280,9 +438,9 @@ Expected a value greater than 0, got -1.1` type S = { readonly a: number } const optic = Optic.id().key("a").check(Schema.isGreaterThan(0)) assertSuccess(optic.getResult({ a: 1 }), 1) - assertFailure(optic.getResult({ a: 0 }), `Expected a value greater than 0, got 0`) + assertFailureMessage(optic.getResult({ a: 0 }), `Expected a value greater than 0`) assertSuccess(optic.replaceResult(2, { a: 1 }), { a: 2 }) - assertFailure(optic.replaceResult(2, { a: 0 }), `Expected a value greater than 0, got 0`) + assertFailureMessage(optic.replaceResult(2, { a: 0 }), `Expected a value greater than 0`) deepStrictEqual(optic.replace(2, { a: 1 }), { a: 2 }) deepStrictEqual(optic.replace(2, { a: 0 }), { a: 0 }) }) @@ -332,6 +490,27 @@ Expected a value greater than 0, got -1.1` { a: 0, b: 2, c: 0 } ) }) + + it("fails when the replacement count does not match", () => { + const optic = Optic.id>().forEach((element) => element) + + assertFailureMessage( + optic.replaceResult([2], [1, 2]), + "each: replacement length mismatch: 1 !== 2" + ) + }) + + it("fails when an inner setter fails", () => { + const issue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) + const optic = Optic.id>().forEach(() => + Optic.makeOptional( + Result.succeed, + () => Result.fail(issue) + ) + ) + + strictEqual(assertPointerFailure(optic.replaceResult([2], [1]), [0]), issue) + }) }) describe("modifyAll", () => { @@ -361,12 +540,24 @@ Expected a value greater than 0, got -1.1` { a: 0, b: 2, c: 0 } ) }) + + it("returns the original source when the traversal fails", () => { + type S = { readonly values?: ReadonlyArray } + const issue = new SchemaIssue.InvalidValue({ message: "missing values" }) + const optic: Optic.Traversal = Optic.makeOptional( + (s) => s.values === undefined ? Result.fail(issue) : Result.succeed(s.values), + (values, s) => s.values === undefined ? Result.fail(issue) : Result.succeed({ ...s, values }) + ) + const source: S = {} + + strictEqual(optic.modifyAll(addOne)(source), source) + }) }) it("notUndefined", () => { const optic = Optic.id().notUndefined() assertSuccess(optic.getResult(1), 1) - assertFailure(optic.getResult(undefined), "Expected a value other than `undefined`, got undefined") + assertFailureMessage(optic.getResult(undefined), "Expected a value other than `undefined`") deepStrictEqual(optic.replace(2, undefined), 2) deepStrictEqual(optic.replace(2, 1), 2) @@ -382,6 +573,17 @@ Expected a value greater than 0, got -1.1` deepStrictEqual(getAll({ a: [1, -2, 3] }), [1, 3]) }) + it("getAll returns an empty array when the traversal fails", () => { + const focusIssue = new SchemaIssue.InvalidValue({ message: "cannot focus" }) + const replaceIssue = new SchemaIssue.InvalidValue({ message: "cannot replace" }) + const optic: Optic.Traversal = Optic.makeOptional>( + () => Result.fail(focusIssue), + () => Result.fail(replaceIssue) + ) + + deepStrictEqual(Optic.getAll(optic)(1), []) + }) + it("replace copies only objects and arrays along the focused path", () => { type Task = { id: number; done: boolean; title: string } type Project = { id: number; name: string; tasks: Array } @@ -418,21 +620,23 @@ Expected a value greater than 0, got -1.1` it("fromChecks", () => { const optic = Optic.id().compose(Optic.fromChecks(Schema.isGreaterThan(0), Schema.isInt())) assertSuccess(optic.getResult(1), 1) - assertFailure(optic.getResult(0), `Expected a value greater than 0, got 0`) - assertFailure(optic.getResult(1.1), `Expected an integer, got 1.1`) + assertFailureMessage(optic.getResult(0), `Expected a value greater than 0`) + assertFailureMessage(optic.getResult(1.1), `Expected an integer`) }) describe("Option", () => { it("some", () => { const optic = Optic.id>().compose(Optic.some()) assertSuccess(optic.getResult(Option.some(1)), 1) - assertFailure(optic.getResult(Option.none()), `Expected a Some value, got none()`) + assertFailureMessage(optic.getResult(Option.none()), `Expected a Some value`) + deepStrictEqual(optic.set(2), Option.some(2)) }) it("none", () => { const optic = Optic.id>().compose(Optic.none()) assertSuccess(optic.getResult(Option.none()), undefined) - assertFailure(optic.getResult(Option.some(1)), `Expected a None value, got some(1)`) + assertFailureMessage(optic.getResult(Option.some(1)), `Expected a None value`) + deepStrictEqual(optic.set(undefined), Option.none()) }) }) @@ -440,13 +644,15 @@ Expected a value greater than 0, got -1.1` it("success", () => { const optic = Optic.id>().compose(Optic.success()) assertSuccess(optic.getResult(Result.succeed(1)), 1) - assertFailure(optic.getResult(Result.fail("error")), `Expected a Result.Success value, got failure("error")`) + assertFailureMessage(optic.getResult(Result.fail("error")), `Expected a Result.Success value`) + deepStrictEqual(optic.set(2), Result.succeed(2)) }) it("failure", () => { const optic = Optic.id>().compose(Optic.failure()) assertSuccess(optic.getResult(Result.fail("error")), "error") - assertFailure(optic.getResult(Result.succeed(1)), `Expected a Result.Failure value, got success(1)`) + assertFailureMessage(optic.getResult(Result.succeed(1)), `Expected a Result.Failure value`) + deepStrictEqual(optic.set("new error"), Result.fail("new error")) }) }) }) diff --git a/.context/effect/packages/effect/test/PartitionedSemaphore.test.ts b/.context/effect/packages/effect/test/PartitionedSemaphore.test.ts index 8078a18d7..c5a547926 100644 --- a/.context/effect/packages/effect/test/PartitionedSemaphore.test.ts +++ b/.context/effect/packages/effect/test/PartitionedSemaphore.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Option, PartitionedSemaphore } from "effect" +import { Effect, Fiber, Option, PartitionedSemaphore } from "effect" describe("PartitionedSemaphore", () => { it.effect("module-level combinators delegate to the instance api", () => @@ -71,4 +71,22 @@ describe("PartitionedSemaphore", () => { assert.deepStrictEqual(result, Option.none()) assert.isFalse(executed) })) + + it.effect("interrupting a partially satisfied waiter releases all acquired permits", () => + Effect.gen(function*() { + const sem = yield* PartitionedSemaphore.make({ permits: 4 }) + + yield* PartitionedSemaphore.take(sem, "a", 3) + const waiter = yield* PartitionedSemaphore.take(sem, "b", 3).pipe(Effect.forkChild) + yield* Effect.yieldNow + + yield* PartitionedSemaphore.release(sem, 1) + assert.strictEqual(yield* PartitionedSemaphore.available(sem), 0) + yield* Fiber.interrupt(waiter) + assert.strictEqual(yield* PartitionedSemaphore.available(sem), 2) + yield* PartitionedSemaphore.release(sem, 2) + + assert.strictEqual(yield* PartitionedSemaphore.available(sem), 4) + yield* PartitionedSemaphore.take(sem, "c", 4) + })) }) diff --git a/.context/effect/packages/effect/test/PubSub.test.ts b/.context/effect/packages/effect/test/PubSub.test.ts index bf1b79172..0b110e31b 100644 --- a/.context/effect/packages/effect/test/PubSub.test.ts +++ b/.context/effect/packages/effect/test/PubSub.test.ts @@ -399,6 +399,48 @@ describe("PubSub", () => { })) describe("replay", () => { + it("does not retain values published after the replay window is drained", () => { + const pubsub = PubSub.makeAtomicUnbounded({ replay: 1 }) + pubsub.publish({}) + const replayWindow = pubsub.replayWindow() + replayWindow.take() + + const slidOut = {} + pubsub.publish(slidOut) + pubsub.publish({}) + + assert.isFalse(retains(replayWindow, slidOut)) + }) + + it("does not retain values published outside an undrained replay window", () => { + const pubsub = PubSub.makeAtomicUnbounded({ replay: 1 }) + const replayed = {} + pubsub.publish(replayed) + const replayWindow = pubsub.replayWindow() + + const slidOut = {} + pubsub.publish(slidOut) + pubsub.publish({}) + + assert.isFalse(retains(replayWindow, slidOut)) + assert.strictEqual(replayWindow.take(), replayed) + }) + + it("preserves replay order across multiple slides", () => { + const pubsub = PubSub.makeAtomicBounded({ capacity: 4, replay: 3 }) + pubsub.publishAll([1, 2, 3, 4, 5]) + const subscription = pubsub.subscribe() + const replayWindow = pubsub.replayWindow() + pubsub.publishAll([6, 7, 8, 9]) + for (const value of [10, 11, 12]) { + pubsub.slide() + pubsub.publish(value) + } + + assert.deepStrictEqual(replayWindow.takeAll(), [6, 7, 8]) + assert.deepStrictEqual(subscription.pollUpTo(Number.POSITIVE_INFINITY), [9, 10, 11, 12]) + }) + it.effect("unbounded", () => Effect.gen(function*() { const messages = [1, 2, 3, 4, 5] @@ -474,21 +516,43 @@ describe("PubSub", () => { const sub3 = yield* PubSub.subscribe(pubsub) assert.deepStrictEqual(yield* PubSub.takeAll(sub3), [14, 15, 16]) })) - }) - it.effect("shutdown interrupts suspended subscribers", () => - Effect.scoped( + it.effect("sliding preserves publish order with a lagging subscriber", () => Effect.gen(function*() { - const pubsub = yield* PubSub.unbounded() + const pubsub = yield* PubSub.sliding({ capacity: 4, replay: 3 }) + yield* PubSub.subscribe(pubsub) + yield* PubSub.publishAll(pubsub, [1, 2]) const subscription = yield* PubSub.subscribe(pubsub) - const fiber = yield* Effect.forkChild(PubSub.take(subscription), { startImmediately: true }) + yield* PubSub.publishAll(pubsub, [3, 4, 5]) - yield* PubSub.shutdown(pubsub) + const values = yield* PubSub.takeAll(subscription) + assert.isTrue(values.every((value, index) => index === 0 || values[index - 1] <= value)) + })) + }) - const exit = yield* Fiber.await(fiber) - assert.isTrue(Exit.hasInterrupts(exit!)) - }) - )) + it.effect("shutdown interrupts suspended subscribers", () => + Effect.gen(function*() { + const pubsub = yield* PubSub.unbounded() + const subscription = yield* PubSub.subscribe(pubsub) + const fiber = yield* Effect.forkChild(PubSub.take(subscription), { startImmediately: true }) + + yield* PubSub.shutdown(pubsub) + + const exit = yield* Fiber.await(fiber) + assert.isTrue(Exit.hasInterrupts(exit!)) + })) + + it.effect("publish succeeds after interrupting a suspended subscriber", () => + Effect.gen(function*() { + const pubsub = yield* PubSub.dropping(1) + const subscription = yield* PubSub.subscribe(pubsub) + const fiber = yield* Effect.forkChild(PubSub.take(subscription), { startImmediately: true }) + + yield* Fiber.interrupt(fiber) + + assert.isTrue(yield* PubSub.publish(pubsub, 42)) + assert.strictEqual(yield* PubSub.take(subscription), 42) + })) it.effect("publish succeeds after interrupting a suspended subscriber", () => Effect.scoped( @@ -505,16 +569,14 @@ describe("PubSub", () => { )) it.effect("shutdown interrupts suspended takeAll subscribers", () => - Effect.scoped( - Effect.gen(function*() { - const pubsub = yield* PubSub.unbounded() - const subscription = yield* PubSub.subscribe(pubsub) - const fiber = yield* Effect.forkChild(PubSub.takeAll(subscription), { startImmediately: true }) - yield* PubSub.shutdown(pubsub) - const exit = yield* Fiber.await(fiber) - assert.isTrue(Exit.hasInterrupts(exit)) - }) - )) + Effect.gen(function*() { + const pubsub = yield* PubSub.unbounded() + const subscription = yield* PubSub.subscribe(pubsub) + const fiber = yield* Effect.forkChild(PubSub.takeAll(subscription), { startImmediately: true }) + yield* PubSub.shutdown(pubsub) + const exit = yield* Fiber.await(fiber) + assert.isTrue(Exit.hasInterrupts(exit)) + })) it.effect("Stream.fromPubSub completes after shutdown", () => Effect.gen(function*() { @@ -531,22 +593,41 @@ describe("PubSub", () => { })) it.effect("publish returns false after shutdown", () => - Effect.scoped( - Effect.gen(function*() { - const pubsub = yield* PubSub.unbounded() - yield* PubSub.shutdown(pubsub) + Effect.gen(function*() { + const pubsub = yield* PubSub.unbounded() + yield* PubSub.shutdown(pubsub) - assert.strictEqual(yield* PubSub.publish(pubsub, 1), false) - }) - )) + assert.strictEqual(yield* PubSub.publish(pubsub, 1), false) + })) it.effect("publishAll returns false after shutdown", () => - Effect.scoped( - Effect.gen(function*() { - const pubsub = yield* PubSub.unbounded() - yield* PubSub.shutdown(pubsub) + Effect.gen(function*() { + const pubsub = yield* PubSub.unbounded() + yield* PubSub.shutdown(pubsub) - assert.strictEqual(yield* PubSub.publishAll(pubsub, [1, 2, 3]), false) - }) - )) + assert.strictEqual(yield* PubSub.publishAll(pubsub, [1, 2, 3]), false) + })) }) + +const retains = (root: object, target: object): boolean => { + const objects = [root] + const seen = new Set() + while (objects.length > 0) { + const current = objects.pop()! + if (current === target) { + return true + } + if (seen.has(current)) { + continue + } + seen.add(current) + for (const key of Reflect.ownKeys(current)) { + const descriptor = Object.getOwnPropertyDescriptor(current, key) + const value = descriptor && "value" in descriptor ? descriptor.value : undefined + if (typeof value === "object" && value !== null) { + objects.push(value) + } + } + } + return false +} diff --git a/.context/effect/packages/effect/test/Queue.test.ts b/.context/effect/packages/effect/test/Queue.test.ts index 3a7a28eab..3806e0b82 100644 --- a/.context/effect/packages/effect/test/Queue.test.ts +++ b/.context/effect/packages/effect/test/Queue.test.ts @@ -130,6 +130,22 @@ describe("Queue", () => { assert.deepStrictEqual(result, [5]) })) + it.effect("take can be interrupted without losing offers", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + const interruptedFiber = yield* Queue.take(queue).pipe(Effect.forkChild) + + yield* Effect.yieldNow + yield* Fiber.interrupt(interruptedFiber) + assert.isTrue(Exit.hasInterrupts(yield* Fiber.await(interruptedFiber))) + + const liveFiber = yield* Queue.take(queue).pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* Queue.offer(queue, 1) + + assert.strictEqual(yield* Fiber.join(liveFiber), 1) + })) + it.effect("done completes takes", () => Effect.gen(function*() { const queue = yield* Queue.bounded(2) @@ -303,6 +319,28 @@ describe("Queue", () => { assert.isNotNull(fiber.pollUnsafe()) })) + it.effect("end preserves Done for take and excludes it from await", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + const takeFiber = yield* Queue.take(queue).pipe(Effect.forkChild) + const awaitFiber = yield* Queue.await(queue).pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* Queue.end(queue) + + assert.deepStrictEqual(yield* Fiber.await(takeFiber), Exit.fail(Cause.Done())) + assert.strictEqual(yield* Fiber.join(awaitFiber), void 0) + })) + + it.effect("await preserves non-Done failures", () => + Effect.gen(function*() { + const queue = yield* Queue.unbounded() + const fiber = yield* Queue.await(queue).pipe(Effect.exit, Effect.forkChild) + yield* Effect.yieldNow + yield* Queue.fail(queue, "boom") + + assert.deepStrictEqual(yield* Fiber.join(fiber), Exit.fail("boom")) + })) + it.effect("bounded 0 capacity", () => Effect.gen(function*() { const queue = yield* Queue.bounded(0) diff --git a/.context/effect/packages/effect/test/RcRef.test.ts b/.context/effect/packages/effect/test/RcRef.test.ts index bcfe03d7e..142d2fc0b 100644 --- a/.context/effect/packages/effect/test/RcRef.test.ts +++ b/.context/effect/packages/effect/test/RcRef.test.ts @@ -1,8 +1,12 @@ import { assert, describe, it } from "@effect/vitest" +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 RcRef from "effect/RcRef" +import * as Ref from "effect/Ref" import * as Scope from "effect/Scope" +import { TestClock } from "effect/testing" describe("RcRef", () => { it.effect("deallocation", () => @@ -56,42 +60,134 @@ describe("RcRef", () => { assert.isTrue(Exit.hasInterrupts(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 - // }) - // - // assert.strictEqual(acquired, 0) - // assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") - // assert.strictEqual(acquired, 1) - // assert.strictEqual(released, 0) - // - // yield* TestClock.adjust(1000) - // assert.strictEqual(released, 1) - // - // assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") - // assert.strictEqual(acquired, 2) - // assert.strictEqual(released, 1) - // - // yield* TestClock.adjust(500) - // assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") - // assert.strictEqual(acquired, 2) - // assert.strictEqual(released, 1) - // - // yield* TestClock.adjust(1000) - // assert.strictEqual(released, 2) - // })) + it.effect("releases resources acquired before acquisition failure", () => + Effect.gen(function*() { + const acquired = yield* Ref.make(0) + const released = yield* Ref.make(0) + const refScope = yield* Scope.make() + const borrowerScope = yield* Scope.make() + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Ref.updateAndGet(acquired, (n) => n + 1), + () => Ref.update(released, (n) => n + 1) + ).pipe(Effect.andThen(Effect.fail("boom"))) + }).pipe(Scope.provide(refScope)) + + const getExit = yield* RcRef.get(ref).pipe(Scope.provide(borrowerScope), Effect.exit) + const borrowerCloseExit = yield* Scope.close(borrowerScope, Exit.void).pipe(Effect.exit) + const refCloseExit = yield* Scope.close(refScope, Exit.void).pipe(Effect.exit) + + assert.deepStrictEqual(getExit, Exit.fail("boom")) + assert.deepStrictEqual(borrowerCloseExit, Exit.void) + assert.deepStrictEqual(refCloseExit, Exit.void) + assert.strictEqual(yield* Ref.get(acquired), 1) + assert.strictEqual(yield* Ref.get(released), 1) + })) + + it.effect("shares one generation between concurrent first borrowers", () => + Effect.gen(function*() { + const acquired = yield* Ref.make(0) + const released = yield* Ref.make(0) + const firstStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const refScope = yield* Scope.make() + const scopeA = yield* Scope.make() + const scopeB = yield* Scope.make() + const ref = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.gen(function*() { + const id = yield* Ref.updateAndGet(acquired, (n) => n + 1) + if (id === 1) { + yield* Deferred.succeed(firstStarted, undefined) + yield* Deferred.await(releaseFirst) + } + return { id } + }), + () => Ref.update(released, (n) => n + 1) + ) + }).pipe(Scope.provide(refScope)) + + const fiberA = yield* RcRef.get(ref).pipe( + Scope.provide(scopeA), + Effect.forkChild({ startImmediately: true }) + ) + yield* Deferred.await(firstStarted) + const fiberB = yield* RcRef.get(ref).pipe( + Scope.provide(scopeB), + Effect.forkChild({ startImmediately: true }) + ) + yield* Deferred.succeed(releaseFirst, undefined) + + const exitA = yield* Fiber.await(fiberA) + const exitB = yield* Fiber.await(fiberB) + const generationA = yield* Fiber.join(fiberA) + const generationB = yield* Fiber.join(fiberB) + const closeAExit = yield* Scope.close(scopeA, Exit.void).pipe(Effect.exit) + const closeBExit = yield* Scope.close(scopeB, Exit.void).pipe(Effect.exit) + const refCloseExit = yield* Scope.close(refScope, Exit.void).pipe(Effect.exit) + + assert.deepStrictEqual( + { + exitA, + exitB, + closeAExit, + closeBExit, + refCloseExit, + sameGeneration: generationA === generationB, + acquired: yield* Ref.get(acquired), + released: yield* Ref.get(released) + }, + { + exitA: Exit.succeed(generationA), + exitB: Exit.succeed(generationA), + closeAExit: Exit.void, + closeBExit: Exit.void, + refCloseExit: Exit.void, + sameGeneration: true, + acquired: 1, + released: 1 + } + ) + })) + + it.effect("idleTimeToLive reuses and releases resources", () => + 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: "10 millis" + }) + + assert.strictEqual(acquired, 0) + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 1) + assert.strictEqual(released, 0) + + yield* TestClock.adjust("5 millis") + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 1) + assert.strictEqual(released, 0) + + yield* TestClock.adjust("9 millis") + assert.strictEqual(released, 0) + yield* TestClock.adjust("1 millis") + assert.strictEqual(released, 1) + + assert.strictEqual(yield* Effect.scoped(RcRef.get(ref)), "foo") + assert.strictEqual(acquired, 2) + assert.strictEqual(released, 1) + + yield* TestClock.adjust("10 millis") + assert.strictEqual(released, 2) + })) }) diff --git a/.context/effect/packages/effect/test/Record.test.ts b/.context/effect/packages/effect/test/Record.test.ts index eaf698362..1ba1ae507 100644 --- a/.context/effect/packages/effect/test/Record.test.ts +++ b/.context/effect/packages/effect/test/Record.test.ts @@ -1,4 +1,4 @@ -import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual } from "@effect/vitest/utils" +import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Equivalence, Number as Num, Option, Record, Result } from "effect" import { pipe } from "effect/Function" import { describe, it } from "vitest" @@ -61,6 +61,11 @@ describe("Record", () => { }) deepStrictEqual(Record.fromIterableBy(["a", symA], (s) => s), { a: "a", [symA]: symA }) + + deepStrictEqual(pipe(users, Record.fromIterableBy((user) => user.id)), { + "2": { id: "2", name: "name2" }, + "1": { id: "1", name: "name1" } + }) }) it("fromEntries", () => { @@ -159,6 +164,20 @@ describe("Record", () => { }) }) + describe("assignProperty", () => { + it("preserves __proto__ as an own property", () => { + const record: Record = {} + const prototype = Object.getPrototypeOf(record) + const value = { polluted: true } + + Record.assignProperty(record, "__proto__", value) + + strictEqual(Object.getPrototypeOf(record), prototype) + assertTrue(Object.hasOwn(record, "__proto__")) + strictEqual(record["__proto__"], value) + }) + }) + it("singleton", () => { deepStrictEqual(Record.singleton("a", 1), { a: 1 }) diff --git a/.context/effect/packages/effect/test/Request.test.ts b/.context/effect/packages/effect/test/Request.test.ts index 58c9b177b..be302cf35 100644 --- a/.context/effect/packages/effect/test/Request.test.ts +++ b/.context/effect/packages/effect/test/Request.test.ts @@ -133,6 +133,26 @@ const provideEnv = flow( ) describe.sequential("Request", () => { + it("preserves __proto__ as an own constructor property", () => { + interface ProtoRequest extends Request.Request { + readonly "__proto__": { readonly polluted: boolean } + } + const make = Request.of() + const value = { polluted: true } + const request = make({ ["__proto__"]: value }) + + assert.isTrue(Request.isRequest(request)) + assert.isTrue(Object.hasOwn(request, "__proto__")) + assert.strictEqual(request["__proto__"], value) + }) + + it("copies enumerable symbol properties in Class", () => { + const key = Symbol() + class SymbolRequest extends Request.Class<{ readonly [key]: string }, void> {} + + assert.strictEqual(new SymbolRequest({ [key]: "value" })[key], "value") + }) + it("compares StructuralProto values when hashes collide", () => { class Req extends Request.Class<{ id: string; account: string }, string> {} diff --git a/.context/effect/packages/effect/test/Result.test.ts b/.context/effect/packages/effect/test/Result.test.ts index 11c44f1fa..26292f835 100644 --- a/.context/effect/packages/effect/test/Result.test.ts +++ b/.context/effect/packages/effect/test/Result.test.ts @@ -196,11 +196,12 @@ describe("Result", () => { describe("Mapping", () => { it("map", () => { const f = Result.map(Str.length) + const failure = Result.fail("s") assertSuccess(pipe(Result.succeed("abc"), f), 3) - assertFailure(pipe(Result.fail("s"), f), "s") + strictEqual(pipe(failure, f), failure) // data-first assertSuccess(Result.map(Result.succeed("abc"), Str.length), 3) - assertFailure(Result.map(Result.fail("s"), Str.length), "s") + strictEqual(Result.map(failure, Str.length), failure) }) it("mapBoth", () => { @@ -229,10 +230,11 @@ describe("Result", () => { it("mapError", () => { const f = Result.mapError((n: number) => n * 2) - assertSuccess(pipe(Result.succeed("a"), f), "a") + const success = Result.succeed("a") + strictEqual(pipe(success, f), success) assertFailure(pipe(Result.fail(1), f), 2) // data-first - assertSuccess(Result.mapError(Result.succeed("a"), (n: number) => n * 2), "a") + strictEqual(Result.mapError(success, (n: number) => n * 2), success) assertFailure(Result.mapError(Result.fail(1), (n) => n * 2), 2) }) diff --git a/.context/effect/packages/effect/test/Schedule.test.ts b/.context/effect/packages/effect/test/Schedule.test.ts index 7200df6f4..99cae3311 100644 --- a/.context/effect/packages/effect/test/Schedule.test.ts +++ b/.context/effect/packages/effect/test/Schedule.test.ts @@ -4,6 +4,19 @@ import { constant, constUndefined } from "effect/Function" import { TestClock } from "effect/testing" describe("Schedule", () => { + describe("constructors", () => { + it.effect("during recurs while the duration has not elapsed", () => + Effect.gen(function*() { + const step = yield* Schedule.toStep(Schedule.during("1 second")) + const completed = yield* Pull.catchDone( + step(0, undefined).pipe(Effect.as(false)), + () => Effect.succeed(true) + ) + + assert.isFalse(completed) + })) + }) + describe("combining", () => { it.effect("max - outputs the slowest schedule duration", () => Effect.gen(function*() { @@ -68,6 +81,28 @@ describe("Schedule", () => { }) describe("sequencing", () => { + it.effect("concat - sequences self then other and merges their outputs", () => + Effect.gen(function*() { + const left = Schedule.identity().pipe( + Schedule.upTo({ times: 2 }), + Schedule.map(({ output }) => `left:${output}`) + ) + const right = Schedule.identity().pipe( + Schedule.map(({ output }) => `right:${output}`) + ) + const step = yield* Schedule.toStep(Schedule.concat(left, right)) + + const first = yield* step(0, "a") + const second = yield* step(0, "b") + const third = yield* step(0, "c") + + assert.deepStrictEqual([first, second, third], [ + ["left:a", Duration.zero], + ["left:b", Duration.zero], + ["right:c", Duration.zero] + ]) + })) + it.effect("tap - provides full metadata", () => Effect.gen(function*() { const observed: Array> = [] @@ -190,13 +225,13 @@ describe("Schedule", () => { ]) })) - it.effect("andThenResult - sequences self then other when collecting delays", () => + it.effect("concatResult - sequences self then other when collecting delays", () => Effect.gen(function*() { const left = Schedule.fixed("500 millis").pipe( Schedule.while(({ attempt }) => Effect.succeed(attempt <= 3)) ) const right = Schedule.fixed("1 second") - const schedule = Schedule.andThenResult(left, right) + const schedule = Schedule.concatResult(left, right) const inputs = Array.makeBy(6, constUndefined) const outputs = yield* runDelays(schedule, inputs) expect(outputs).toEqual([ @@ -209,13 +244,13 @@ describe("Schedule", () => { ]) })) - it.effect("andThenResult - includes finite other completion when collecting delays", () => + it.effect("concatResult - includes finite other completion when collecting delays", () => Effect.gen(function*() { const left = Schedule.fixed("500 millis").pipe( Schedule.while(({ attempt }) => Effect.succeed(attempt <= 2)) ) const right = Schedule.duration("1 second") - const schedule = Schedule.andThenResult(left, right) + const schedule = Schedule.concatResult(left, right) const inputs = Array.makeBy(5, constUndefined) const outputs = yield* runDelays(schedule, inputs) expect(outputs).toEqual([ @@ -226,11 +261,11 @@ describe("Schedule", () => { ]) })) - it.effect("andThenResult - wraps self outputs as Failure and other outputs as Success", () => + it.effect("concatResult - wraps self outputs as Failure and other outputs as Success", () => Effect.gen(function*() { const left = Schedule.identity().pipe(Schedule.upTo({ times: 2 })) const right = Schedule.identity() - const step = yield* Schedule.toStep(Schedule.andThenResult(left, right)) + const step = yield* Schedule.toStep(Schedule.concatResult(left, right)) const first = yield* step(0, "left-1") const second = yield* step(0, "left-2") diff --git a/.context/effect/packages/effect/test/Scheduler.test.ts b/.context/effect/packages/effect/test/Scheduler.test.ts index eb35d6ae0..e82c3a9fb 100644 --- a/.context/effect/packages/effect/test/Scheduler.test.ts +++ b/.context/effect/packages/effect/test/Scheduler.test.ts @@ -19,6 +19,26 @@ describe("Scheduler", () => { assert.deepStrictEqual(exit, Exit.succeed(1)) }) + it("runSyncExit does not schedule timers after yielding", () => { + const setImmediate = vi.spyOn(globalThis, "setImmediate").mockImplementation(() => { + throw new Error("setImmediate is not supported") + }) + const setTimeout = vi.spyOn(globalThis, "setTimeout").mockImplementation(() => { + throw new Error("setTimeout is not supported") + }) + + try { + const exit = Effect.runSyncExit(Effect.as(Effect.yieldNow, 1)) + + assert.deepStrictEqual(exit, Exit.succeed(1)) + assert.strictEqual(setImmediate.mock.calls.length, 0) + assert.strictEqual(setTimeout.mock.calls.length, 0) + } finally { + setImmediate.mockRestore() + setTimeout.mockRestore() + } + }) + it.effect("MixedScheduler orders by priority (sync)", () => Effect.sync(() => { const scheduler = new Scheduler.MixedScheduler("sync").makeDispatcher() diff --git a/.context/effect/packages/effect/test/ScopedCache.test.ts b/.context/effect/packages/effect/test/ScopedCache.test.ts index 45d11be32..91c844e9a 100644 --- a/.context/effect/packages/effect/test/ScopedCache.test.ts +++ b/.context/effect/packages/effect/test/ScopedCache.test.ts @@ -44,6 +44,19 @@ describe("ScopedCache", () => { assert.isFalse(yield* ScopedCache.has(cache, "test")) })) + it.effect("uses a numeric zero TTL", () => + Effect.gen(function*() { + const cache = yield* ScopedCache.make({ + capacity: 10, + lookup: (key: string) => Effect.succeed(key.length), + timeToLive: 0 + }) + + yield* ScopedCache.get(cache, "test") + + assert.isFalse(yield* ScopedCache.has(cache, "test")) + })) + it.effect("lookup function context is preserved", () => Effect.gen(function*() { class TestService extends Context.Service()("TestService") {} diff --git a/.context/effect/packages/effect/test/ScopedRef.test.ts b/.context/effect/packages/effect/test/ScopedRef.test.ts index 8798a6a33..bbe0bc2e4 100644 --- a/.context/effect/packages/effect/test/ScopedRef.test.ts +++ b/.context/effect/packages/effect/test/ScopedRef.test.ts @@ -1,6 +1,6 @@ -import { describe, it } from "@effect/vitest" +import { assert, describe, it } from "@effect/vitest" import { strictEqual } from "@effect/vitest/utils" -import { Effect, identity, pipe, ScopedRef } from "effect" +import { Effect, Exit, identity, pipe, Ref, Scope, ScopedRef } from "effect" import * as Counter from "./utils/counter.ts" describe("ScopedRef", () => { @@ -72,6 +72,53 @@ describe("ScopedRef", () => { strictEqual(acquired, 3) strictEqual(released, 3) })) + it.effect("keeps the current resource when replacement acquisition fails", () => + Effect.gen(function*() { + let released = false + const ref = yield* ScopedRef.fromAcquire( + Effect.acquireRelease(Effect.succeed(1), () => + Effect.sync(() => { + released = true + })) + ) + + yield* ScopedRef.set(ref, Effect.fail("boom")).pipe(Effect.catch(() => Effect.void)) + + strictEqual(released, false, "failed replacement must not release the current resource") + strictEqual(yield* ScopedRef.get(ref), 1) + })) + it.effect("releases a replacement when the old finalizer defects", () => + Effect.gen(function*() { + const oldReleased = yield* Ref.make(0) + const replacementAcquired = yield* Ref.make(0) + const replacementReleased = yield* Ref.make(0) + const ownerScope = yield* Scope.make() + const ref = yield* ScopedRef.fromAcquire( + Effect.acquireRelease( + Effect.succeed(0), + () => + Ref.update(oldReleased, (n) => n + 1).pipe( + Effect.andThen(Effect.die("old-release-defect")) + ) + ) + ).pipe(Scope.provide(ownerScope)) + + const setExit = yield* ScopedRef.set( + ref, + Effect.acquireRelease( + Ref.updateAndGet(replacementAcquired, (n) => n + 1), + () => Ref.update(replacementReleased, (n) => n + 1) + ) + ).pipe(Effect.exit) + strictEqual(yield* ScopedRef.get(ref), 0) + const ownerCloseExit = yield* Scope.close(ownerScope, Exit.void).pipe(Effect.exit) + + assert.deepStrictEqual(setExit, Exit.die("old-release-defect")) + assert.deepStrictEqual(ownerCloseExit, Exit.void) + strictEqual(yield* Ref.get(oldReleased), 1) + strictEqual(yield* Ref.get(replacementAcquired), 1) + strictEqual(yield* Ref.get(replacementReleased), 1) + })) it.effect("fromAcquire tracks the initial resource through replacement and scope close", () => Effect.gen(function*() { const ref = yield* Effect.scoped(ScopedRef.make(() => 0)) diff --git a/.context/effect/packages/effect/test/Semaphore.test.ts b/.context/effect/packages/effect/test/Semaphore.test.ts index 47fb21849..3ce4b16f4 100644 --- a/.context/effect/packages/effect/test/Semaphore.test.ts +++ b/.context/effect/packages/effect/test/Semaphore.test.ts @@ -305,6 +305,183 @@ describe("Semaphore", () => { assert.isTrue(Option.isSome(result)) })) + it.effect("withPermits interruption does not leak permits", () => + Effect.gen(function*() { + const sem = yield* Semaphore.make(1) + let acquired = false + const waiter = yield* sem.withPermits(2)( + Effect.sync(() => { + acquired = true + }) + ).pipe(Effect.forkChild) + + yield* Effect.yieldNow + assert.isUndefined(waiter.pollUnsafe()) + + yield* Fiber.interrupt(waiter) + assert.isFalse(acquired) + + const result = yield* sem.withPermitsIfAvailable(1)(Effect.void) + assert.isTrue(Option.isSome(result)) + + yield* sem.withPermits(1)( + Effect.sync(() => { + acquired = true + }) + ) + assert.isTrue(acquired) + })) + + it.effect("withPermits interrupted at any operation does not leak permits", () => + Effect.gen(function*() { + let operations = 0 + const counted = new Scheduler.MixedScheduler() + const baseline = yield* (yield* Semaphore.make(1)).withPermits(1)(Effect.void).pipe( + Effect.provideService(Scheduler.Scheduler, { + executionMode: counted.executionMode, + makeDispatcher: () => counted.makeDispatcher(), + shouldYield: (fiber) => { + operations++ + return counted.shouldYield(fiber) + } + }), + Effect.forkChild + ) + yield* Fiber.await(baseline) + + const recovered: Array = [] + let ranUnderPermits = false + + for (let at = 1; at <= operations; at++) { + const sem = yield* Semaphore.make(1) + const base = new Scheduler.MixedScheduler() + let seen = 0 + const scheduler: Scheduler.Scheduler = { + executionMode: base.executionMode, + makeDispatcher: () => base.makeDispatcher(), + shouldYield: (fiber) => { + if (++seen === at) fiber.interruptUnsafe() + return base.shouldYield(fiber) + } + } + + const holder = yield* sem.withPermits(1)( + Effect.sync(() => { + ranUnderPermits = true + }) + ).pipe( + Effect.provideService(Scheduler.Scheduler, scheduler), + Effect.forkChild + ) + yield* Fiber.await(holder) + + const result = yield* sem.withPermitsIfAvailable(1)(Effect.void) + recovered.push(Option.isSome(result)) + } + + assert.deepStrictEqual(recovered, Array.from({ length: operations }, () => true)) + assert.isTrue(ranUnderPermits) + })) + + it.effect("queued withPermits interrupted at any operation does not leak permits", () => + Effect.gen(function*() { + const queuedScheduler = ( + tasks: Array<() => void>, + onOperation: (fiber: Fiber.Fiber) => void + ) => { + const scheduler: Scheduler.Scheduler = { + executionMode: "async", + makeDispatcher: () => ({ + scheduleTask(task) { + tasks.push(task) + }, + flush() {} + }), + shouldYield: (fiber) => { + onOperation(fiber) + return false + } + } + return scheduler + } + + let operations = 0 + const baselineSem = yield* Semaphore.make(1) + const baselineTasks: Array<() => void> = [] + const baselineHolder = yield* Effect.forkChild(baselineSem.withPermits(1)(Effect.never)) + yield* Effect.yieldNow + yield* Effect.forkChild( + baselineSem.withPermits(1)(Effect.void).pipe( + Effect.provideService(Scheduler.Scheduler, queuedScheduler(baselineTasks, () => operations++)) + ), + { startImmediately: true } + ) + yield* Fiber.interrupt(baselineHolder) + yield* Effect.yieldNow + while (baselineTasks.length > 0) baselineTasks.shift()!() + + const recovered: Array = [] + let ranUnderPermits = false + + for (let at = 1; at <= operations; at++) { + const sem = yield* Semaphore.make(1) + const tasks: Array<() => void> = [] + let seen = 0 + const scheduler = queuedScheduler(tasks, (fiber) => { + if (++seen === at) fiber.interruptUnsafe() + }) + + const holder = yield* Effect.forkChild(sem.withPermits(1)(Effect.never)) + yield* Effect.yieldNow + + yield* Effect.forkChild( + sem.withPermits(1)( + Effect.sync(() => { + ranUnderPermits = true + }) + ).pipe(Effect.provideService(Scheduler.Scheduler, scheduler)), + { startImmediately: true } + ) + + yield* Fiber.interrupt(holder) + yield* Effect.yieldNow + while (tasks.length > 0) tasks.shift()!() + + const result = yield* sem.withPermitsIfAvailable(1)(Effect.void) + recovered.push(Option.isSome(result)) + } + + assert.deepStrictEqual(recovered, Array.from({ length: operations }, () => true)) + assert.isTrue(ranUnderPermits) + })) + + it.effect("takeIfAvailable acquires permits when they are available", () => + Effect.gen(function*() { + const sem = yield* Semaphore.make(2) + + const acquired = yield* Semaphore.takeIfAvailable(sem, 2) + assert.isTrue(acquired) + + const unavailable = yield* sem.takeIfAvailable(1) + assert.isFalse(unavailable) + + const released = yield* sem.release(2) + assert.strictEqual(released, 2) + })) + + it.effect("takeIfAvailable returns immediately when permits are unavailable", () => + Effect.gen(function*() { + const sem = yield* Semaphore.make(1) + + yield* sem.take(1) + + const acquired = yield* Semaphore.takeIfAvailable(1)(sem) + assert.isFalse(acquired) + + yield* sem.release(1) + assert.isTrue(yield* sem.takeIfAvailable(1)) + })) + it.effect("module-level combinators delegate to the instance api", () => Effect.gen(function*() { const sem = yield* Semaphore.make(1) diff --git a/.context/effect/packages/effect/test/Sink.test.ts b/.context/effect/packages/effect/test/Sink.test.ts index 09b15f2bb..680eb0c83 100644 --- a/.context/effect/packages/effect/test/Sink.test.ts +++ b/.context/effect/packages/effect/test/Sink.test.ts @@ -7,10 +7,71 @@ import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Array, Cause, Effect, Option, Ref, Result, Sink, Stream } from "effect" +import { Array, Cause, Deferred, Duration, Effect, Fiber, Option, Ref, Result, Sink, Stream } from "effect" import { constTrue, pipe } from "effect/Function" +import { TestClock } from "effect/testing" describe("Sink", () => { + describe("constructors", () => { + it.effect("fromWritableStream - aborts instead of closing on upstream failure", () => + Effect.gen(function*() { + const error = new Error("upstream failed") + let abortReason: unknown = undefined + let closes = 0 + const writable = new WritableStream({ + close() { + closes++ + }, + abort(reason) { + abortReason = reason + } + }) + const exit = yield* Stream.make(1).pipe( + Stream.concat(Stream.fail(error)), + Stream.run(Sink.fromWritableStream({ + evaluate: () => writable, + onError: (cause) => cause + })), + Effect.exit + ) + + assertExitFailure(exit, Cause.fail(error)) + strictEqual(closes, 0) + deepStrictEqual(Cause.squash(abortReason as Cause.Cause), error) + })) + + it.effect("fromWritableStream - aborts instead of closing on interruption", () => + Effect.gen(function*() { + const started = yield* Deferred.make() + let abortReason: unknown = undefined + let closes = 0 + const writable = new WritableStream({ + close() { + closes++ + }, + abort(reason) { + abortReason = reason + } + }) + const sink = Sink.fromWritableStream({ + evaluate: () => writable, + onError: (cause) => cause + }) + const fiber = yield* Stream.fromEffect( + Deferred.succeed(started, void 0).pipe(Effect.andThen(Effect.never)) + ).pipe( + Stream.run(sink), + Effect.forkChild + ) + + yield* Deferred.await(started) + yield* Fiber.interrupt(fiber) + + strictEqual(closes, 0) + strictEqual(abortReason === undefined, false) + })) + }) + describe("reduceWhile", () => { it.effect("empty", () => Effect.gen(function*() { @@ -45,6 +106,18 @@ describe("Sink", () => { strictEqual(result, 45) })) }) + + describe("reduceWhileArray", () => { + it.effect("applies the reducer once per non-empty input array", () => + Effect.gen(function*() { + const result = yield* Stream.fromArrays([1, 2, 3]).pipe( + Stream.run(Sink.reduceWhileArray(() => 0, constTrue, (count) => count + 1)) + ) + + strictEqual(result, 1, "the reducer must run once for each input array") + })) + }) + describe("reduceWhileEffect", () => { it.effect("short circuits", () => Effect.gen(function*() { @@ -184,6 +257,23 @@ describe("Sink", () => { })) }) + describe("withDuration", () => { + it.effect("uses monotonic time when wall time moves backward", () => + Effect.gen(function*() { + yield* TestClock.setTime(1_000) + const [, duration] = yield* Stream.empty.pipe( + Stream.run( + Sink.fromEffect(Effect.gen(function*() { + yield* TestClock.adjust("100 millis") + yield* TestClock.setTime(0) + })).pipe(Sink.withDuration) + ) + ) + + strictEqual(Duration.toMillis(duration), 100) + })) + }) + describe("flatMap", () => { it.effect("flatMap - empty input", () => Effect.gen(function*() { diff --git a/.context/effect/packages/effect/test/Stdio.test.ts b/.context/effect/packages/effect/test/Stdio.test.ts new file mode 100644 index 000000000..bfb56fb18 --- /dev/null +++ b/.context/effect/packages/effect/test/Stdio.test.ts @@ -0,0 +1,22 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Stdio from "effect/Stdio" + +describe("Stdio", () => { + it.effect("layerTest defaults terminal state to false", () => + Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + assert.isFalse(yield* stdio.stdinIsTerminal) + assert.isFalse(yield* stdio.stdoutIsTerminal) + }).pipe(Effect.provide(Stdio.layerTest({})))) + + it.effect("layerTest allows terminal state overrides", () => + Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + assert.isTrue(yield* stdio.stdinIsTerminal) + assert.isTrue(yield* stdio.stdoutIsTerminal) + }).pipe(Effect.provide(Stdio.layerTest({ + stdinIsTerminal: Effect.succeed(true), + stdoutIsTerminal: Effect.succeed(true) + })))) +}) diff --git a/.context/effect/packages/effect/test/Stream.test.ts b/.context/effect/packages/effect/test/Stream.test.ts index e196f12b4..0db0749d0 100644 --- a/.context/effect/packages/effect/test/Stream.test.ts +++ b/.context/effect/packages/effect/test/Stream.test.ts @@ -4,6 +4,7 @@ import { assertExitFailure, assertSuccess, assertTrue, deepStrictEqual, strictEq import { Array, Cause, + Channel, Clock, Context, Data, @@ -22,6 +23,7 @@ import { References, Result, Schedule, + Scope, Sink, Stream, String as Str @@ -134,6 +136,29 @@ describe("Stream", () => { }) describe("destructors", () => { + const withScopeFinalizer = ( + self: Stream.Stream, + finalizer: (exit: Exit.Exit) => Effect.Effect + ): Stream.Stream => + Stream.fromChannel( + Channel.fromTransform((upstream, scope) => + Effect.andThen( + Scope.addFinalizerExit(scope, finalizer), + Channel.toTransform(Stream.toChannel(self))(upstream, scope) + ) + ) + ) + + it.effect("mkArrayBuffer - concatenates Uint8Array chunks", () => + Effect.gen(function*() { + const buffer = yield* Stream.make( + new Uint8Array([1, 2]), + new Uint8Array([3, 4]) + ).pipe(Stream.mkArrayBuffer) + + assert.deepStrictEqual([...new Uint8Array(buffer)], [1, 2, 3, 4]) + })) + it.effect("runForEachWhile continues across chunk boundaries", () => Effect.gen(function*() { const seen: Array = [] @@ -161,6 +186,211 @@ describe("Stream", () => { ) assert.deepStrictEqual(seen, [1, 2, 3]) })) + + it.effect("toAsyncIterable - return interrupts an in-flight pull", () => + Effect.gen(function*() { + const started = yield* Deferred.make() + let interrupted = false + const iterator = Stream.toAsyncIterable( + Stream.fromEffect( + Effect.andThen( + Deferred.succeed(started, void 0), + Effect.never + ).pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ) + ) + ) + )[Symbol.asyncIterator]() + + const pending = iterator.next() + yield* Deferred.await(started) + const result = yield* Effect.promise(() => iterator.return!(undefined)) + const pendingResult = yield* Effect.promise(() => pending) + + assert.deepStrictEqual(result, { done: true, value: undefined }) + assert.deepStrictEqual(pendingResult, { done: true, value: undefined }) + assert.isTrue(interrupted) + })) + + it.effect("toAsyncIterable - return does not reject an unawaited in-flight next", () => + Effect.gen(function*() { + let interrupted = false + const iterator = Stream.toAsyncIterable( + Stream.fromEffect( + Effect.never.pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ) + ) + ) + )[Symbol.asyncIterator]() + + iterator.next() + const result = yield* Effect.promise(() => iterator.return!(undefined)) + yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 0))) + + assert.deepStrictEqual(result, { done: true, value: undefined }) + assert.isTrue(interrupted) + })) + + it.effect("toAsyncIterable - early for await exit interrupts the producer", () => + Effect.gen(function*() { + let interrupted = false + const iterable = Stream.toAsyncIterable( + Stream.callback((queue) => + Effect.andThen( + Queue.offer(queue, 1), + Effect.never + ).pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ) + ) + ) + ) + + yield* Effect.promise(async () => { + for await (const _ of iterable) { + break + } + }) + + assert.isTrue(interrupted) + })) + + it.effect("toAsyncIterable - return is idempotent without an in-flight pull", () => + Effect.gen(function*() { + const iterator = Stream.toAsyncIterable(Stream.make(1))[Symbol.asyncIterator]() + + const first = yield* Effect.promise(() => iterator.return!(undefined)) + const second = yield* Effect.promise(() => iterator.return!(undefined)) + const next = yield* Effect.promise(() => iterator.next()) + + assert.deepStrictEqual(first, { done: true, value: undefined }) + assert.deepStrictEqual(second, { done: true, value: undefined }) + assert.deepStrictEqual(next, { done: true, value: undefined }) + })) + + it.effect("toAsyncIterable - natural completion closes the scope with a successful exit", () => + Effect.gen(function*() { + const finalizerExits: Array> = [] + const stream = withScopeFinalizer( + Stream.make(1, 2), + (exit) => + Effect.sync(() => { + finalizerExits.push(exit) + }) + ) + const values = yield* Effect.promise(async () => { + const values: Array = [] + for await (const value of Stream.toAsyncIterable(stream)) { + values.push(value) + } + return values + }) + + assert.deepStrictEqual(values, [1, 2]) + assert.deepStrictEqual(finalizerExits, [Exit.void]) + })) + + it.effect("toAsyncIterable - stream failure is forwarded to the scope", () => + Effect.gen(function*() { + const streamError = new Error("stream failure") + const finalizerError = new Error("finalizer failure") + const finalizerExits: Array> = [] + const capturedLogs: Array = [] + const testLogger = Logger.make((options) => { + capturedLogs.push(options.message) + }) + const stream = withScopeFinalizer( + Stream.fail(streamError), + (exit) => + Effect.andThen( + Effect.sync(() => { + finalizerExits.push(exit) + }), + Effect.die(finalizerError) + ) + ) + + const thrown = yield* Effect.gen(function*() { + const iterable = yield* Stream.toAsyncIterableEffect(stream) + return yield* Effect.promise(() => iterable[Symbol.asyncIterator]().next().catch((error) => error)) + }).pipe(Effect.withLogger(testLogger)) + + assert.strictEqual(thrown, streamError) + assert.deepStrictEqual(finalizerExits, [Exit.fail(streamError)]) + assert.strictEqual(capturedLogs.length, 1) + })) + + it.effect("toAsyncIterable - throw forwards a failure exit and preserves its error", () => + Effect.gen(function*() { + const thrownError = new Error("thrown failure") + const finalizerError = new Error("finalizer failure") + const finalizerExits: Array> = [] + const capturedLogs: Array = [] + const testLogger = Logger.make((options) => { + capturedLogs.push(options.message) + }) + const stream = withScopeFinalizer( + Stream.make(1), + (exit) => + Effect.andThen( + Effect.sync(() => { + finalizerExits.push(exit) + }), + Effect.die(finalizerError) + ) + ) + + const thrown = yield* Effect.gen(function*() { + const iterator = (yield* Stream.toAsyncIterableEffect(stream))[Symbol.asyncIterator]() + yield* Effect.promise(() => iterator.next()) + return yield* Effect.promise(() => iterator.throw!(thrownError).catch((error) => error)) + }).pipe(Effect.withLogger(testLogger)) + + assert.strictEqual(thrown, thrownError) + assert.deepStrictEqual(finalizerExits, [Exit.die(thrownError)]) + assert.strictEqual(capturedLogs.length, 1) + })) + + it.effect("toAsyncIterable - throw interrupts an in-flight pull", () => + Effect.gen(function*() { + const started = yield* Deferred.make() + let interrupted = false + const iterator = Stream.toAsyncIterable( + Stream.fromEffect( + Effect.andThen( + Deferred.succeed(started, void 0), + Effect.never + ).pipe( + Effect.onInterrupt(() => + Effect.sync(() => { + interrupted = true + }) + ) + ) + ) + )[Symbol.asyncIterator]() + const error = new Error("boom") + + const pending = iterator.next() + yield* Deferred.await(started) + const thrown = yield* Effect.promise(() => iterator.throw!(error).catch((error) => error)) + const pendingResult = yield* Effect.promise(() => pending) + + assert.strictEqual(thrown, error) + assert.deepStrictEqual(pendingResult, { done: true, value: undefined }) + assert.isTrue(interrupted) + })) }) describe("constructors", () => { @@ -174,6 +404,16 @@ describe("Stream", () => { assert.deepStrictEqual(result, [1, 2, 3]) })) + it.effect("range - zero chunk size does not change the emitted range", () => + Effect.gen(function*() { + const result = yield* Stream.range(1, 3, 0).pipe( + Stream.take(4), + Stream.runCollect + ) + + assert.deepStrictEqual(result, [1, 2, 3]) + })) + it.effect("service", () => Effect.gen(function*() { const result = yield* Stream.service(Greeter).pipe( @@ -427,6 +667,52 @@ describe("Stream", () => { }) describe("taking", () => { + it.effect("limitBytes - does not evaluate the fallback below the limit", () => + Effect.gen(function*() { + let evaluated = false + const chunks = [new Uint8Array([1, 2]), new Uint8Array([3, 4])] + const result = yield* Stream.fromIterable(chunks).pipe( + Stream.limitBytes(5, () => { + evaluated = true + return Stream.empty + }), + Stream.runCollect + ) + + assert.deepStrictEqual(result, chunks) + assert.isFalse(evaluated) + })) + + it.effect("limitBytes - does not evaluate the fallback at the limit", () => + Effect.gen(function*() { + let evaluated = false + const chunks = [new Uint8Array([1, 2]), new Uint8Array([3, 4])] + const result = yield* Stream.fromIterable(chunks).pipe( + Stream.limitBytes(4, () => { + evaluated = true + return Stream.empty + }), + Stream.runCollect + ) + + assert.deepStrictEqual(result, chunks) + assert.isFalse(evaluated) + })) + + it.effect("limitBytes - drops the crossing chunk and switches to the fallback", () => + Effect.gen(function*() { + const first = new Uint8Array([1, 2]) + const crossing = new Uint8Array([3, 4, 5, 6]) + const after = new Uint8Array([7]) + const fallback = new Uint8Array([8, 9]) + const result = yield* Stream.make(first, crossing, after).pipe( + Stream.limitBytes(5, () => Stream.succeed(fallback)), + Stream.runCollect + ) + + assert.deepStrictEqual(result, [first, fallback]) + })) + it.effect("take - pulls the first `n` values from a stream", () => Effect.gen(function*() { const result = yield* Stream.range(1, 5).pipe( @@ -979,6 +1265,17 @@ describe("Stream", () => { assert.deepStrictEqual(result, Array.scan([1, 2, 3, 4, 5], 0, (acc, curr) => acc + curr)) })) + + it.effect("mapAccumArrayEffect data-first", () => + Effect.gen(function*() { + const result = yield* Stream.mapAccumArrayEffect( + Stream.make(1, 2, 3), + () => 0, + (sum, values) => Effect.succeed([sum + values.length, values] as const) + ).pipe(Stream.runCollect) + + assert.deepStrictEqual(result, [1, 2, 3]) + })) }) describe("grouping", () => { @@ -1031,6 +1328,139 @@ describe("Stream", () => { })) }) + const testOuterFailure = ( + combinator: "flatMap" | "switchMap", + inner: "never" | "slow" + ) => + Effect.gen(function*() { + const started = yield* Latch.make(false) + const failing = yield* Deferred.make() + const finalized = yield* Ref.make(0) + const outer = Stream.concat( + Stream.make(1), + Stream.fromEffect( + started.await.pipe( + Effect.andThen(Deferred.succeed(failing, void 0)), + Effect.andThen(Effect.fail("boom")) + ) + ) + ) + const makeInner = () => { + started.openUnsafe() + return (inner === "never" ? Stream.never : Stream.fromEffect(Effect.sleep(Duration.hours(1)))).pipe( + Stream.ensuring(Ref.update(finalized, (n) => n + 1)) + ) + } + const stream = combinator === "flatMap" + ? Stream.flatMap(outer, makeInner, { concurrency: 2 }) + : Stream.switchMap(outer, makeInner) + const fiber = yield* stream.pipe( + Stream.runDrain, + Effect.forkChild + ) + yield* Deferred.await(failing) + + const result = yield* Fiber.await(fiber) + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.strictEqual(yield* Ref.get(finalized), 1) + }) + + describe("flatMap", () => { + it.effect("interrupts all inner streams when the outer fails at the concurrency limit", () => + Effect.gen(function*() { + const latch = yield* Latch.make() + const finalized = yield* Ref.make(0) + const outer = Stream.concat( + Stream.make(1, 2), + Stream.flatMap(Stream.fromEffect(latch.await), () => Stream.fail("boom")) + ) + const result = yield* Stream.flatMap( + outer, + () => + Stream.flatMap(Stream.fromEffect(latch.open), () => Stream.never).pipe( + Stream.ensuring(Ref.update(finalized, (n) => n + 1)) + ), + { concurrency: 2 } + ).pipe( + Stream.runDrain, + Effect.exit + ) + + assert.deepStrictEqual(result, Exit.fail("boom")) + assert.strictEqual(yield* Ref.get(finalized), 2) + })) + + it.effect("releases a permit after a successful saturated pull", () => + Effect.gen(function*() { + const gate = yield* Deferred.make() + const outer = Stream.concat( + Stream.make(1, 2), + Stream.fromEffect(Deferred.succeed(gate, void 0).pipe(Effect.as(3))) + ) + const result = yield* Stream.flatMap( + outer, + (n) => n === 3 ? Stream.make(3) : Stream.fromEffect(Deferred.await(gate).pipe(Effect.as(n))), + { concurrency: 2 } + ).pipe(Stream.runCollect) + + assert.deepStrictEqual([...result].sort(), [1, 2, 3]) + })) + + it.effect("preserves an outer element when a permit arrives during a pull", () => + Effect.gen(function*() { + const probing = yield* Deferred.make() + const gate = yield* Deferred.make() + const innerGate = yield* Deferred.make() + const innerCompleted = yield* Deferred.make() + const outer = Stream.concat( + Stream.make(1, 2), + Stream.fromEffect( + Deferred.succeed(probing, void 0).pipe( + Effect.andThen(Deferred.await(gate)), + Effect.as(3) + ) + ) + ) + const fiber = yield* Stream.flatMap( + outer, + (n) => + n === 3 + ? Stream.make(3) + : Stream.fromEffect(Deferred.await(innerGate).pipe(Effect.as(n))).pipe( + Stream.ensuring(Deferred.succeed(innerCompleted, void 0)) + ), + { concurrency: 2 } + ).pipe( + Stream.runCollect, + Effect.forkChild + ) + yield* Deferred.await(probing) + yield* Deferred.succeed(innerGate, void 0) + yield* Deferred.await(innerCompleted) + yield* Deferred.succeed(gate, void 0) + + const result = yield* Fiber.join(fiber) + assert.deepStrictEqual([...result].sort(), [1, 2, 3]) + })) + + it.effect( + "fails promptly and interrupts slow inner streams when the outer stream fails", + () => testOuterFailure("flatMap", "slow") + ) + }) + + describe("switchMap", () => { + it.effect( + "interrupts a never-ending inner stream when the outer stream fails", + () => testOuterFailure("switchMap", "never") + ) + + it.effect( + "fails promptly and interrupts a slow inner stream when the outer stream fails", + () => testOuterFailure("switchMap", "slow") + ) + }) + it.effect.prop( "rechunk", { @@ -1831,6 +2261,31 @@ describe("Stream", () => { }) describe("aggregateWithin", () => { + it.effect("does not grow the fiber continuation stack while upstream is idle", () => + Effect.gen(function*() { + const continuationCounts: Array = [] + const schedule = Schedule.spaced("10 millis").pipe( + Schedule.tap(() => + Effect.withFiber((fiber) => + Effect.sync(() => { + continuationCounts.push( + (fiber as unknown as { readonly _stack: ReadonlyArray })._stack.length + ) + }) + ) + ) + ) + const fiber = yield* Stream.never.pipe( + Stream.aggregateWithin(Sink.take(25), schedule), + Stream.runDrain, + Effect.forkChild({ startImmediately: true }) + ) + yield* TestClock.adjust("1 second") + assert.isAbove(continuationCounts.length, 1) + assert.strictEqual(continuationCounts.at(-1), continuationCounts[0]) + yield* Fiber.interrupt(fiber) + })) + it.effect("groupedWithin does not emit empty arrays when upstream is idle", () => Effect.gen(function*() { const fiber = yield* Stream.never.pipe( @@ -2547,13 +3002,11 @@ describe("Stream", () => { assertExitFailure(result, Cause.fail("boom")) })) - // Note: This test is skipped because with sequential pulling (matching zipWith behavior), - // when the left stream ends, we don't pull from the right stream, so errors after - // the left stream ends are not encountered. This is correct behavior. - it.skip("error propagation from right stream", () => + // Keep the left stream alive: once either side ends, later errors from the other side are not observed. + it.effect("error propagation from right stream", () => Effect.gen(function*() { const result = yield* Stream.zipWithArray( - Stream.make(1, 2, 3), + Stream.fromArrays([1, 2, 3], [4]), Stream.make("a", "b").pipe(Stream.concat(Stream.fail("boom"))), (left, right) => { const minLength = Math.min(left.length, right.length) @@ -3421,6 +3874,28 @@ describe("Stream", () => { }) describe("haltWhen", () => { + it.effect("halts after the current element in the documentation example", () => + Effect.gen(function*() { + const halt = yield* Deferred.make() + const values = yield* Stream.fromArray([1, 2, 3]).pipe( + Stream.tap((value) => value === 2 ? Deferred.succeed(halt, void 0) : Effect.void), + Stream.haltWhen(Deferred.await(halt)), + Stream.runCollect + ) + assert.deepStrictEqual(values, [1, 2]) + })) + + it.effect("halts a synchronous upstream before the next chunk", () => + Effect.gen(function*() { + const halt = yield* Deferred.make() + const values = yield* Stream.fromArrays([1], [2], [3], [4]).pipe( + Stream.tap((value) => value === 2 ? Deferred.succeed(halt, void 0) : Effect.void), + Stream.haltWhen(Deferred.await(halt)), + Stream.runCollect + ) + assert.deepStrictEqual(values, [1, 2]) + })) + it.effect("halts after the current element", () => Effect.gen(function*() { const ref = yield* Ref.make(false) @@ -4291,6 +4766,23 @@ describe("Stream", () => { deepStrictEqual(result1, result2) })) + it.effect("slidingSize is independent of upstream chunk boundaries", () => + Effect.gen(function*() { + const result = yield* Effect.all([ + Stream.make(1, 2, 3, 4, 5), + Stream.fromArrays([1, 2], [3, 4, 5]), + Stream.fromArrays([1], [2], [3], [4], [5]), + Stream.fromArrays([1], [2], [3], [4]) + ].map((stream) => stream.pipe(Stream.slidingSize(2, 3), Stream.runCollect))) + + deepStrictEqual(result, [ + [[1, 2], [4, 5]], + [[1, 2], [4, 5]], + [[1, 2], [4, 5]], + [[1, 2], [4]] + ]) + })) + it.effect("sliding - fails if upstream produces an error", () => Effect.gen(function*() { const result = yield* pipe( @@ -4674,7 +5166,7 @@ describe("Stream", () => { describe("broadcastN", () => { it.effect("fans out to a fixed number of streams", () => - Effect.scoped(Effect.gen(function*() { + Effect.gen(function*() { const [left, right] = yield* Stream.make(1, 2, 3).pipe( Stream.broadcastN({ n: 2, capacity: 4 }) ) @@ -4685,10 +5177,10 @@ describe("Stream", () => { ], { concurrency: "unbounded" }) assert.deepStrictEqual(result, [[1, 2, 3], [1, 2, 3]]) - }))) + })) it.effect("propagates failures to all downstream streams", () => - Effect.scoped(Effect.gen(function*() { + Effect.gen(function*() { const [left, right] = yield* Stream.fail("boom").pipe( Stream.broadcastN({ n: 2, capacity: 4 }) ) @@ -4699,7 +5191,7 @@ describe("Stream", () => { ], { concurrency: "unbounded" }) assert.deepStrictEqual(result, [Exit.fail("boom"), Exit.fail("boom")]) - }))) + })) }) }) diff --git a/.context/effect/packages/effect/test/String.test.ts b/.context/effect/packages/effect/test/String.test.ts index b0612ba9d..2cbb018e7 100644 --- a/.context/effect/packages/effect/test/String.test.ts +++ b/.context/effect/packages/effect/test/String.test.ts @@ -587,6 +587,10 @@ describe("String", () => { it("handles single word", () => { strictEqual(S.snakeToCamel("hello"), "hello") }) + + it("handles an empty string", () => { + strictEqual(S.snakeToCamel(""), "") + }) }) describe("snakeToPascal", () => { @@ -594,6 +598,10 @@ describe("String", () => { strictEqual(S.snakeToPascal("hello_world"), "HelloWorld") strictEqual(S.snakeToPascal("foo_bar_baz"), "FooBarBaz") }) + + it("handles an empty string", () => { + strictEqual(S.snakeToPascal(""), "") + }) }) describe("snakeToKebab", () => { @@ -634,6 +642,22 @@ describe("String", () => { strictEqual(pipe("helloWorld", S.noCase({ delimiter: "-" })), "hello-world") }) + it("uses a custom split regular expression", () => { + strictEqual(S.noCase("ab", { splitRegExp: /([a])([b])/g }), "a b") + }) + + it("uses custom split regular expressions", () => { + strictEqual(S.noCase("abc", { splitRegExp: [/([a])([b])/g, /([b])([c])/g] }), "a b c") + }) + + it("uses a custom strip regular expression", () => { + strictEqual(S.noCase("a_b-c", { stripRegExp: /_/g }), "a b-c") + }) + + it("uses custom strip regular expressions", () => { + strictEqual(S.noCase("a_b-c", { stripRegExp: [/_/g, /-/g] }), "a b c") + }) + it("handles underscores and hyphens", () => { strictEqual(S.noCase("hello_world"), "hello world") strictEqual(S.noCase("hello-world"), "hello world") diff --git a/.context/effect/packages/effect/test/SubscriptionRef.test.ts b/.context/effect/packages/effect/test/SubscriptionRef.test.ts index f1ea41dfb..972634c48 100644 --- a/.context/effect/packages/effect/test/SubscriptionRef.test.ts +++ b/.context/effect/packages/effect/test/SubscriptionRef.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Array, Effect, Exit, Fiber, Latch, Number, Pull, Random, Stream, SubscriptionRef } from "effect" +import { Array, Effect, Exit, Fiber, Latch, Number, Option, Pull, Random, Stream, SubscriptionRef } from "effect" describe("SubscriptionRef", () => { it.effect("isSubscriptionRef", () => @@ -9,6 +9,22 @@ describe("SubscriptionRef", () => { assert.isFalse(SubscriptionRef.isSubscriptionRef([0])) })) + it.effect("getAndUpdateEffect", () => + Effect.gen(function*() { + const ref = yield* SubscriptionRef.make(1) + const previous = yield* SubscriptionRef.getAndUpdateEffect(ref, (value) => Effect.succeed(value + 1)) + assert.strictEqual(previous, 1) + assert.strictEqual(yield* SubscriptionRef.get(ref), 2) + })) + + it.effect("getAndUpdateSome returns the current value when no update is selected", () => + Effect.gen(function*() { + const ref = yield* SubscriptionRef.make(1) + const previous = yield* SubscriptionRef.getAndUpdateSome(ref, () => Option.none()) + + assert.strictEqual(previous, 1) + })) + it.effect("multiple subscribers can receive changes", () => Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) diff --git a/.context/effect/packages/effect/test/SynchronizedRef.test.ts b/.context/effect/packages/effect/test/SynchronizedRef.test.ts index 241df928d..ab5e37564 100644 --- a/.context/effect/packages/effect/test/SynchronizedRef.test.ts +++ b/.context/effect/packages/effect/test/SynchronizedRef.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Deferred, Effect, Exit, Fiber, pipe, SynchronizedRef } from "effect" +import { Deferred, Effect, Exit, Fiber, Option, pipe, SynchronizedRef } from "effect" const current = "value" const update = "new value" @@ -102,4 +102,11 @@ describe("SynchronizedRef", () => { const result = yield* SynchronizedRef.updateAndGetEffect(ref, (_) => Effect.succeed(Closed)) deepStrictEqual(result, Closed) })) + it.effect("getAndUpdateSome updates the backing ref", () => + Effect.gen(function*() { + const ref = yield* SynchronizedRef.make(1) + const previous = yield* SynchronizedRef.getAndUpdateSome(ref, (n) => Option.some(n + 1)) + strictEqual(previous, 1) + strictEqual(yield* SynchronizedRef.get(ref), 2) + })) }) diff --git a/.context/effect/packages/effect/test/TestClock.test.ts b/.context/effect/packages/effect/test/TestClock.test.ts index c03abc3e3..d53df56a8 100644 --- a/.context/effect/packages/effect/test/TestClock.test.ts +++ b/.context/effect/packages/effect/test/TestClock.test.ts @@ -61,6 +61,68 @@ describe("TestClock", () => { assert.strictEqual(testClock.currentTimeNanosUnsafe(), 199023438000000n) })) + it.effect("setTime - preserves wall-clock nanoseconds for large timestamps", () => + Effect.gen(function*() { + const testClock = yield* TestClock.make() + const timestamp = 1_000_000_000_001 + yield* testClock.setTime(timestamp) + assert.strictEqual(testClock.currentTimeNanosUnsafe(), BigInt(timestamp) * 1_000_000n) + })) + + it.effect("adjust - advances wall and monotonic time", () => + Effect.gen(function*() { + const testClock = yield* TestClock.make() + yield* testClock.adjust("1 second") + assert.strictEqual(testClock.currentTimeMillisUnsafe(), 1_000) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe(), 1_000_000_000n) + assert.strictEqual(yield* testClock.monotonicTimeNanos, 1_000_000_000n) + })) + + it.effect("adjust - keeps nanosecond access total after infinite durations", () => + Effect.gen(function*() { + for (const duration of [Duration.infinity, Duration.negativeInfinity]) { + const testClock = yield* TestClock.make() + yield* testClock.adjust(duration) + assert.strictEqual(typeof (yield* testClock.currentTimeNanos), "bigint") + } + })) + + it.effect("setTime - advances monotonic time only when moving forward", () => + Effect.gen(function*() { + const testClock = yield* TestClock.make() + yield* testClock.setTime(2_000) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe(), 2_000_000_000n) + yield* testClock.setTime(500) + assert.strictEqual(testClock.currentTimeMillisUnsafe(), 500) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe(), 2_000_000_000n) + yield* testClock.setTime(1_000) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe(), 2_500_000_000n) + })) + + it.effect("setTime - preserves nanosecond precision for far-future timestamps", () => + Effect.gen(function*() { + const testClock = yield* TestClock.make() + const farFuture = 1_000_000_000_000 + yield* testClock.setTime(farFuture) + const before = testClock.monotonicTimeNanosUnsafe() + yield* testClock.setTime(farFuture + 1) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe() - before, 1_000_000n) + })) + + it.effect("adjust - advances monotonic time to intermediate sleep deadlines", () => + Effect.gen(function*() { + const testClock = yield* TestClock.make() + let observed = 0n + yield* Effect.gen(function*() { + yield* testClock.sleep(Duration.seconds(1)) + observed = yield* testClock.monotonicTimeNanos + }).pipe(Effect.forkChild) + yield* testClock.adjust("2 seconds") + assert.strictEqual(observed, 1_000_000_000n) + assert.strictEqual(testClock.monotonicTimeNanosUnsafe(), 2_000_000_000n) + })) + + // `it.effect` and `it.live` provide an ambient Scope, defeating the #2244 regression guard. it("layer - can adjust when provided without an ambient Scope", () => Effect.gen(function*() { yield* TestClock.adjust("1 second") diff --git a/.context/effect/packages/effect/test/Tracer.test.ts b/.context/effect/packages/effect/test/Tracer.test.ts index 690dc07b2..ebd59d940 100644 --- a/.context/effect/packages/effect/test/Tracer.test.ts +++ b/.context/effect/packages/effect/test/Tracer.test.ts @@ -287,6 +287,23 @@ describe("Tracer", () => { strictEqual(span.status.startTime, 0n) })) + + it.effect("should set start and end times to zero when timing is disabled", () => + Effect.gen(function*() { + yield* TestClock.adjust("1 millis") + + const useSpan = yield* Effect.useSpan("useSpan", (span) => Effect.succeed(span)) + const withSpan = yield* Effect.currentSpan.pipe(Effect.withSpan("withSpan")) + + deepStrictEqual( + [useSpan.status, withSpan.status].map((status) => { + strictEqual(status._tag, "Ended") + return status._tag === "Ended" ? [status.startTime, status.endTime] : undefined + }), + [[0n, 0n], [0n, 0n]], + "disabled span timing" + ) + }).pipe(Effect.withTracerTiming(false))) }) describe("Effect.linkSpans", () => { diff --git a/.context/effect/packages/effect/test/Trie.test.ts b/.context/effect/packages/effect/test/Trie.test.ts index 191774c3b..ac4e82759 100644 --- a/.context/effect/packages/effect/test/Trie.test.ts +++ b/.context/effect/packages/effect/test/Trie.test.ts @@ -2,11 +2,23 @@ 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 Hash from "effect/Hash" import * as Option from "effect/Option" import * as Result from "effect/Result" import * as Trie from "effect/Trie" describe("Trie", () => { + it("equality rejects tries with different numbers of entries after a hash collision", () => { + const value = { + [Hash.symbol]: () => Hash.hash("a") * 53 + } + const empty = Trie.empty() + const nonEmpty = Trie.make(["a", value]) + + strictEqual(Hash.hash(empty), Hash.hash(nonEmpty)) + strictEqual(Equal.equals(empty, nonEmpty), false, "tries with different sizes must not be equal") + }) + it("toString renders entries in iteration order", () => { const trie = pipe( Trie.empty(), @@ -68,6 +80,17 @@ describe("Trie", () => { deepStrictEqual(Array.from(trie4), [["call", 0], ["me", 1], ["mid", 3], ["mind", 2]]) }) + it("insert replaces a key without mutating or growing the original", () => { + const before = Trie.make(["a", 1]) + const after = Trie.insert(before, "a", 2) + + deepStrictEqual( + [Array.from(before), Trie.size(before), Array.from(after), Trie.size(after)], + [[["a", 1]], 1, [["a", 2]], 1], + "replacement must be immutable and preserve size" + ) + }) + it("fromIterable preserves an empty iterable", () => { const iterable: Array<[string, number]> = [] const trie = Trie.fromIterable(iterable) @@ -144,6 +167,14 @@ describe("Trie", () => { assertNone(Trie.get(trie, "mea")) }) + it("stores undefined values", () => { + const trie = Trie.make(["a", undefined]) + + strictEqual(Trie.size(trie), 1) + strictEqual(Option.isSome(Trie.get(trie, "a")), true, "an inserted undefined value must remain present") + deepStrictEqual(Array.from(trie), [["a", undefined]]) + }) + it("get distinguishes complete keys from prefixes", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), @@ -348,6 +379,16 @@ describe("Trie", () => { assertSome(Trie.longestPrefixOf(trie, "shellsort"), ["shells", 0]) }) + it("longestPrefixOf ignores valued sibling nodes that do not match the input", () => { + const trie = Trie.make(["a", 1], ["b", 2]) + + strictEqual( + Option.isNone(Trie.longestPrefixOf(trie, "c")), + true, + "a non-matching sibling must not be reported as a prefix" + ) + }) + it("map transforms values and can use keys", () => { const trie = Trie.empty().pipe( Trie.insert("shells", 0), diff --git a/.context/effect/packages/effect/test/TxPubSub.test.ts b/.context/effect/packages/effect/test/TxPubSub.test.ts index fe8e1d390..53306efe1 100644 --- a/.context/effect/packages/effect/test/TxPubSub.test.ts +++ b/.context/effect/packages/effect/test/TxPubSub.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Fiber, TxPubSub, TxQueue } from "effect" +import { Effect, Exit, Fiber, Option, Scope, TxPubSub, TxQueue } from "effect" describe("TxPubSub", () => { describe("constructors", () => { @@ -95,6 +95,33 @@ describe("TxPubSub", () => { ) })) + it.effect("publishAll preserves a one-shot iterable across retries", () => + Effect.gen(function*() { + const hub = yield* TxPubSub.bounded(1) + + yield* Effect.scoped( + Effect.gen(function*() { + const sub = yield* TxPubSub.subscribe(hub) + yield* TxPubSub.publish(hub, 1) + + let iterations = 0 + const hasIterated = () => iterations > 0 + const values = (function*() { + iterations++ + yield 2 + })() + const fiber = yield* Effect.forkChild(TxPubSub.publishAll(hub, values)) + while (!hasIterated()) { + yield* Effect.yieldNow + } + + assert.strictEqual(yield* TxQueue.take(sub), 1) + assert.strictEqual(yield* Fiber.join(fiber), true) + assert.deepStrictEqual(yield* TxQueue.poll(sub), Option.some(2)) + }) + ) + })) + it.effect("subscriber only receives messages published after subscription", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) @@ -304,6 +331,17 @@ describe("TxPubSub", () => { }) describe("scope cleanup", () => { + it.effect("releases a subscriber after hub shutdown without interruption", () => + Effect.gen(function*() { + const hub = yield* Effect.tx(TxPubSub.unbounded()) + const scope = yield* Scope.make() + yield* TxPubSub.subscribe(hub).pipe(Scope.provide(scope)) + yield* Effect.tx(TxPubSub.shutdown(hub)) + + const release = yield* Effect.exit(Scope.close(scope, Exit.void)) + assert(Exit.isSuccess(release)) + })) + it.effect("closing scope removes subscriber", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) diff --git a/.context/effect/packages/effect/test/TxQueue.test.ts b/.context/effect/packages/effect/test/TxQueue.test.ts index 8c9100ba9..163729720 100644 --- a/.context/effect/packages/effect/test/TxQueue.test.ts +++ b/.context/effect/packages/effect/test/TxQueue.test.ts @@ -219,6 +219,26 @@ describe("TxQueue", () => { assert.deepStrictEqual(maybe, Option.some(42)) }))) + it.effect("poll completes a closing queue after removing the last item", () => + Effect.tx(Effect.gen(function*() { + const queue = yield* TxQueue.bounded(1) + yield* TxQueue.offer(queue, 42) + yield* TxQueue.interrupt(queue) + + assert.deepStrictEqual(yield* TxQueue.poll(queue), Option.some(42)) + assert.strictEqual(yield* TxQueue.isDone(queue), true) + }))) + + it.effect("clear completes a closing queue after removing all items", () => + Effect.tx(Effect.gen(function*() { + const queue = yield* TxQueue.bounded(1) + yield* TxQueue.offer(queue, 42) + yield* TxQueue.interrupt(queue) + + assert.deepStrictEqual(yield* TxQueue.clear(queue), [42]) + assert.strictEqual(yield* TxQueue.isDone(queue), true) + }))) + it.effect("offerAll works correctly", () => Effect.tx(Effect.gen(function*() { const queue = yield* TxQueue.bounded(10) @@ -230,6 +250,25 @@ describe("TxQueue", () => { assert.strictEqual(size, 5) }))) + it.effect("offerAll preserves a one-shot iterable across retries and runs", () => + Effect.gen(function*() { + const queue = yield* TxQueue.bounded(1) + yield* TxQueue.offer(queue, 1) + + const values = (function*() { + yield 2 + })() + const offer = TxQueue.offerAll(queue, values) + const fiber = yield* Effect.forkChild(offer, { startImmediately: true }) + + assert.strictEqual(yield* TxQueue.take(queue), 1) + assert.deepStrictEqual(yield* Fiber.join(fiber), []) + assert.deepStrictEqual(yield* TxQueue.poll(queue), Option.some(2)) + + assert.deepStrictEqual(yield* offer, []) + assert.deepStrictEqual(yield* TxQueue.poll(queue), Option.some(2)) + })) + it.effect("takeAll works correctly with new signature", () => Effect.tx(Effect.gen(function*() { const queue = yield* TxQueue.bounded(10) diff --git a/.context/effect/packages/effect/test/TxReentrantLock.test.ts b/.context/effect/packages/effect/test/TxReentrantLock.test.ts index e9872b7f5..c9884240d 100644 --- a/.context/effect/packages/effect/test/TxReentrantLock.test.ts +++ b/.context/effect/packages/effect/test/TxReentrantLock.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Exit, Fiber, TxReentrantLock } from "effect" +import { Effect, Exit, Fiber, Scope, TxReentrantLock } from "effect" describe("TxReentrantLock", () => { describe("constructors", () => { @@ -410,4 +410,30 @@ describe("TxReentrantLock", () => { assert.strictEqual(result, 0) }))) }) + + it.effect("releases a scoped read lock when another fiber closes the scope", () => + Effect.gen(function*() { + const lock = yield* TxReentrantLock.make() + const scope = yield* Scope.make() + const fiber = yield* TxReentrantLock.readLock(lock).pipe( + Effect.provideService(Scope.Scope, scope), + Effect.forkChild + ) + yield* Fiber.join(fiber) + yield* Scope.close(scope, Exit.void) + assert.isFalse(yield* TxReentrantLock.readLocked(lock)) + })) + + it.effect("releases a scoped write lock when another fiber closes the scope", () => + Effect.gen(function*() { + const lock = yield* TxReentrantLock.make() + const scope = yield* Scope.make() + const fiber = yield* TxReentrantLock.writeLock(lock).pipe( + Effect.provideService(Scope.Scope, scope), + Effect.forkChild + ) + yield* Fiber.join(fiber) + yield* Scope.close(scope, Exit.void) + assert.isFalse(yield* TxReentrantLock.writeLocked(lock)) + })) }) diff --git a/.context/effect/packages/effect/test/cluster/ClusterWorkflowEngine.test.ts b/.context/effect/packages/effect/test/cluster/ClusterWorkflowEngine.test.ts index 0a5567857..150e6c043 100644 --- a/.context/effect/packages/effect/test/cluster/ClusterWorkflowEngine.test.ts +++ b/.context/effect/packages/effect/test/cluster/ClusterWorkflowEngine.test.ts @@ -1,5 +1,5 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Cause, Context, DateTime, Duration, Effect, Exit, Fiber, Layer, Option, Result, Schema } from "effect" +import { Cause, Context, DateTime, Duration, Effect, Exit, Fiber, Layer, Option, Result, Schema, Tracer } from "effect" import { TestClock } from "effect/testing" import { ClusterSchema, @@ -176,6 +176,17 @@ describe.concurrent("ClusterWorkflowEngine", () => { expect(flags.get("interrupt3")).toBeFalsy() }).pipe(Effect.provide(TestWorkflowLayer))) + it.effect("Activity.raceAll ignores a failure when another activity can succeed", () => + Effect.gen(function*() { + const fiber = yield* FailureRaceWorkflow.execute({ + id: "failure-race" + }).pipe(Effect.forkChild({ startImmediately: true })) + + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("slow") + }).pipe(Effect.provide(TestWorkflowLayer))) + it.effect("Activity.raceAll replays the first durable activity", () => Effect.gen(function*() { const flags = yield* Flags @@ -204,6 +215,306 @@ describe.concurrent("ClusterWorkflowEngine", () => { expect(result).toEqual("Activity3") }).pipe(Effect.provide(TestWorkflowLayer))) + it.effect("DurableDeferred.raceAll lets a deferred win while another branch is active", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const executionId = yield* MixedRaceWorkflow.executionId({ id: "mixed-race" }) + const fiber = yield* MixedRaceWorkflow.execute({ id: "mixed-race" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(MixedRaceGate, { + workflow: MixedRaceWorkflow, + executionId + }) + yield* DurableDeferred.succeed(MixedRaceGate, { token, value: "signal" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("signal") + }).pipe(Effect.provide(TestWorkflowLayer)), 20_000) + + it.effect( + "DurableDeferred.raceAll lets an active branch win while the deferred stays pending", + () => + Effect.gen(function*() { + const fiber = yield* MixedRaceWorkflow.execute({ id: "mixed-race-activity" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + for (let i = 0; i < 4; i++) { + yield* TestClock.adjust("1 second") + } + + expect(yield* Fiber.join(fiber)).toEqual("activity") + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect( + "DurableDeferred.raceAll replays the run when a losing deferred completes late", + () => + Effect.gen(function*() { + const flags = yield* Flags + const sharding = yield* Sharding.Sharding + const executionId = yield* LosingDeferredWorkflow.executionId({ id: "losing-deferred" }) + const fiber = yield* LosingDeferredWorkflow.execute({ id: "losing-deferred" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + yield* TestClock.adjust("1 second") + while (flags.get("losing-deferred-tail-runs") !== 1) { + yield* Effect.yieldNow + } + + const token = DurableDeferred.tokenFromExecutionId(LosingDeferredGate, { + workflow: LosingDeferredWorkflow, + executionId + }) + yield* DurableDeferred.succeed(LosingDeferredGate, { token, value: "signal" }) + for (let i = 0; i < 4; i++) { + yield* sharding.pollStorage + yield* TestClock.adjust("10 seconds") + } + + expect(yield* Fiber.join(fiber)).toEqual("activity:tail") + // The late completion preempts the tail; the replay re-executes it. + expect(flags.get("losing-deferred-tail-runs")).toEqual(2) + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect("DurableDeferred.raceAll wakes the active run by replaying it", () => + Effect.gen(function*() { + const flags = yield* Flags + const sharding = yield* Sharding.Sharding + const executionId = yield* InPlaceWakeWorkflow.executionId({ id: "in-place-wake" }) + const fiber = yield* InPlaceWakeWorkflow.execute({ id: "in-place-wake" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(InPlaceWakeGate, { + workflow: InPlaceWakeWorkflow, + executionId + }) + yield* DurableDeferred.succeed(InPlaceWakeGate, { token, value: "signal" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("signal") + // Usually the completion preempts the parked run (2 runs); under load + // it can land before the branch parks and is read directly (1 run). + assert([1, 2].includes(flags.get("in-place-wake-runs") as number)) + }).pipe(Effect.provide(TestWorkflowLayer)), 20_000) + + it.effect("DurableDeferred.raceAll wakes a branch wrapped in DurableDeferred.into", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const executionId = yield* IntoWrapWorkflow.executionId({ id: "into-wrap" }) + const fiber = yield* IntoWrapWorkflow.execute({ id: "into-wrap" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(IntoWrapGate, { + workflow: IntoWrapWorkflow, + executionId + }) + yield* DurableDeferred.succeed(IntoWrapGate, { token, value: "signal" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("signal") + }).pipe(Effect.provide(TestWorkflowLayer)), 20_000) + + it.effect( + "DurableDeferred.raceAll does not preempt for deferreds awaited inside activity bodies", + () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const fiber = yield* ClockCaptureWorkflow.execute({ id: "clock-capture" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + // Only workflow-level awaits preempt; the clock inside the activity + // does not, so the other branch wins. + yield* TestClock.adjust(1) + yield* TestClock.adjust(5000) + yield* sharding.pollStorage + yield* TestClock.adjust(1000) + yield* TestClock.adjust("60 seconds") + + expect(yield* Fiber.join(fiber)).toEqual("slow") + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect( + "DurableDeferred.raceAll lets a bare durable clock branch win while another branch is active", + () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const fiber = yield* BareClockWorkflow.execute({ id: "bare-clock" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + for (let i = 0; i < 8; i++) { + yield* sharding.pollStorage + yield* TestClock.adjust("5 seconds") + } + + expect(yield* Fiber.join(fiber)).toEqual("clock") + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect("DurableDeferred.raceAll runs a branch's transformations on a deferred wake", () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const executionId = yield* MappedGateWorkflow.executionId({ id: "mapped-gate" }) + const fiber = yield* MappedGateWorkflow.execute({ id: "mapped-gate" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(MappedGate, { + workflow: MappedGateWorkflow, + executionId + }) + yield* DurableDeferred.succeed(MappedGate, { token, value: "signal" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("signal!") + }).pipe(Effect.provide(TestWorkflowLayer)), 20_000) + + it.effect( + "DurableDeferred.raceAll wakes a branch that ran an activity before its await", + () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const executionId = yield* PreGateWorkflow.executionId({ id: "pre-gate" }) + const fiber = yield* PreGateWorkflow.execute({ id: "pre-gate" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(PreGate, { + workflow: PreGateWorkflow, + executionId + }) + yield* DurableDeferred.succeed(PreGate, { token, value: "signal" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("act:signal") + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect("DurableDeferred.raceAll re-runs a multi-await branch once per completion", () => + Effect.gen(function*() { + const flags = yield* Flags + const sharding = yield* Sharding.Sharding + const executionId = yield* TwoStepWorkflow.executionId({ id: "two-step" }) + const fiber = yield* TwoStepWorkflow.execute({ id: "two-step" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(1) + const tokenA = DurableDeferred.tokenFromExecutionId(TwoStepGateA, { + workflow: TwoStepWorkflow, + executionId + }) + yield* DurableDeferred.succeed(TwoStepGateA, { token: tokenA, value: "a" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + // The run parks on the second gate, usually after one wake replay; + // under load the first completion can be read directly (1 run). + assert([1, 2].includes(flags.get("two-step-branch-runs") as number)) + + const tokenB = DurableDeferred.tokenFromExecutionId(TwoStepGateB, { + workflow: TwoStepWorkflow, + executionId + }) + yield* DurableDeferred.succeed(TwoStepGateB, { token: tokenB, value: "b" }) + yield* sharding.pollStorage + yield* TestClock.adjust("1 second") + + expect(yield* Fiber.join(fiber)).toEqual("a:b") + assert([2, 3].includes(flags.get("two-step-branch-runs") as number)) + }).pipe(Effect.provide(TestWorkflowLayer)), 20_000) + + it.effect( + "DurableDeferred.raceAll delivers a completion that lands while a suspension commits", + () => + Effect.gen(function*() { + const flags = yield* Flags + const sharding = yield* Sharding.Sharding + const executionId = yield* SlowUnwindWorkflow.executionId({ id: "slow-unwind" }) + const fiber = yield* SlowUnwindWorkflow.execute({ id: "slow-unwind" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + // both branches park, the suspension commits, and the ensuring sleep + // keeps the run from settling + yield* TestClock.adjust(1) + yield* TestClock.adjust(1) + + const token = DurableDeferred.tokenFromExecutionId(SlowUnwindGateB, { + workflow: SlowUnwindWorkflow, + executionId + }) + yield* DurableDeferred.succeed(SlowUnwindGateB, { token, value: "signal-b" }) + // Finish the unwind, then the replay and its own ensuring sleep. + for (let i = 0; i < 4; i++) { + yield* TestClock.adjust("10 seconds") + yield* sharding.pollStorage + } + + expect(yield* Fiber.join(fiber)).toEqual("signal-b") + expect(flags.get("slow-unwind-runs")).toEqual(2) + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + + it.effect( + "DurableDeferred.raceAll suspends when every branch is pending and resumes with the winner", + () => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const executionId = yield* TwoGateWorkflow.executionId({ id: "two-gates" }) + const fiber = yield* TwoGateWorkflow.execute({ id: "two-gates" }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + // Wait for the race to suspend with both gates pending. + yield* TestClock.adjust(1) + let polled = yield* TwoGateWorkflow.poll(executionId) + while (Option.isNone(polled) || polled.value._tag !== "Suspended") { + yield* Effect.yieldNow + polled = yield* TwoGateWorkflow.poll(executionId) + } + + const token = DurableDeferred.tokenFromExecutionId(TwoGateB, { + workflow: TwoGateWorkflow, + executionId + }) + yield* DurableDeferred.succeed(TwoGateB, { token, value: "signal-b" }) + yield* sharding.pollStorage + yield* TestClock.adjust("5 seconds") + + expect(yield* Fiber.join(fiber)).toEqual("signal-b") + }).pipe(Effect.provide(TestWorkflowLayer)), + 20_000 + ) + it.effect("nested workflows", () => Effect.gen(function*() { const flags = yield* Flags @@ -214,6 +525,9 @@ describe.concurrent("ClusterWorkflowEngine", () => { id: "123" }).pipe(Effect.forkChild) yield* TestClock.adjust(1000) + while (flags.get("parent-suspended") === undefined) { + yield* Effect.yieldNow + } assert.isUndefined(flags.get("parent-end")) assert.isUndefined(flags.get("child-end")) @@ -231,10 +545,11 @@ describe.concurrent("ClusterWorkflowEngine", () => { assert.isTrue(flags.get("child-end")) }).pipe(Effect.provide(TestWorkflowLayer))) - it.effect("routes durable clock wakeups to the workflow shard group", () => + it.effect("routes fractional millisecond durable clock wakeups to the workflow shard group", () => Effect.gen(function*() { const driver = yield* MessageStorage.MemoryDriver const sharding = yield* Sharding.Sharding + const startedAt = yield* DateTime.now const fiber = yield* ShardedClockWorkflow.execute({ id: "sharded-clock" @@ -247,8 +562,11 @@ describe.concurrent("ClusterWorkflowEngine", () => { ) assert(envelope) assert.strictEqual(envelope.address.shardId.group, "workflow") + const deliverAt = driver.requests.get(envelope.requestId)?.deliverAt + assert.isNumber(deliverAt) + assert.strictEqual(deliverAt, DateTime.toEpochMillis(startedAt) + 10001) - yield* TestClock.adjust("10 seconds") + yield* TestClock.adjust(10001) yield* sharding.pollStorage yield* TestClock.adjust(5000) yield* Fiber.join(fiber) @@ -290,7 +608,50 @@ describe.concurrent("ClusterWorkflowEngine", () => { ) assert(envelope) assert.strictEqual(envelope.address.shardId.group, "workflow") - }).pipe(Effect.scoped, Effect.provide(TestWorkflowEngine))) + }).pipe(Effect.provide(TestWorkflowEngine))) + + it.effect("propagates trace context to persisted workflow requests", () => { + let callerSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + const span = new Tracer.NativeSpan(options) + if (options.name === "WorkflowEngine.deferredDone") { + callerSpan = span + } + return span + } + }) + return Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + const engine = yield* WorkflowEngine + yield* engine.register(ShardedDeferredWorkflow, () => Effect.void) + + const executionId = yield* ShardedDeferredWorkflow.executionId({ id: "trace-context" }) + const token = DurableDeferred.tokenFromExecutionId(ShardedDeferred, { + workflow: ShardedDeferredWorkflow, + executionId + }) + const journalLength = driver.journal.length + yield* DurableDeferred.done(ShardedDeferred, { + token, + exit: Exit.void + }) + + const envelope = driver.journal.slice(journalLength).find((envelope) => + envelope._tag === "Request" && + envelope.address.entityType === "Workflow/ShardedDeferredWorkflow" && + envelope.tag === "deferred" + ) + assert(envelope?._tag === "Request") + assert(callerSpan) + assert.strictEqual(envelope.traceId, callerSpan.traceId) + assert.strictEqual(envelope.spanId, callerSpan.spanId) + assert.strictEqual(envelope.sampled, callerSpan.sampled) + }).pipe( + Effect.provideService(Tracer.Tracer, tracer), + Effect.provide(TestWorkflowEngine) + ) + }) it.effect("routes activities to the workflow shard group after a partial client is cached", () => Effect.gen(function*() { @@ -325,7 +686,7 @@ describe.concurrent("ClusterWorkflowEngine", () => { ) assert(envelope) assert.strictEqual(envelope.address.shardId.group, "workflow") - }).pipe(Effect.scoped, Effect.provide(TestWorkflowEngine))) + }).pipe(Effect.provide(TestWorkflowEngine))) it.effect("SuspendOnFailure", () => Effect.gen(function*() { @@ -337,6 +698,9 @@ describe.concurrent("ClusterWorkflowEngine", () => { }).pipe(Effect.forkChild({ startImmediately: true })) yield* TestClock.adjust(2000) + while (!flags.has("suspended")) { + yield* Effect.yieldNow + } assert.isTrue(flags.get("suspended")) assert.include(flags.get("cause"), "boom") @@ -391,7 +755,7 @@ const TestWorkflowEngine = ClusterWorkflowEngine.layer.pipe( Layer.provide(TestShardingConfig) ) -class SendEmailError extends Schema.ErrorClass("SendEmailError")({ +class SendEmailError extends Schema.Error("SendEmailError")({ _tag: Schema.tag("SendEmailError"), message: Schema.String }) {} @@ -408,7 +772,7 @@ const EmailWorkflow = Workflow.make("EmailWorkflow", { }) class Flags extends Context.Service()("Flags", { - make: Effect.sync(() => new Map()) + make: Effect.sync(() => new Map()) }) { static readonly layer = Layer.effect(Flags, this.make) } @@ -528,6 +892,30 @@ const RaceWorkflowLayer = RaceWorkflow.toLayer(Effect.fnUntraced(function*() { ]) })) +const FailureRaceWorkflow = Workflow.make("FailureRaceWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + error: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const FailureRaceWorkflowLayer = FailureRaceWorkflow.toLayer(() => + Activity.raceAll("failure-race", [ + Activity.make({ + name: "failure-race-fast", + success: Schema.String, + error: Schema.String, + execute: Effect.fail("boom") + }), + Activity.make({ + name: "failure-race-slow", + success: Schema.String, + error: Schema.String, + execute: Effect.sleep("1 second").pipe(Effect.as("slow")) + }) + ]) +) + const DurableRaceWorkflow = Workflow.make("DurableRaceWorkflow", { payload: { id: Schema.String @@ -590,6 +978,332 @@ const DurableRaceWorkflowLayer = DurableRaceWorkflow.toLayer(Effect.fnUntraced(f return result })) +const MixedRaceWorkflow = Workflow.make("MixedRaceWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const MixedRaceGate = DurableDeferred.make("MixedRaceGate", { + success: Schema.String +}) + +const MixedRaceWorkflowLayer = MixedRaceWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "mixed-race", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(MixedRaceGate), + Activity.make({ + name: "mixed-race-activity", + success: Schema.String, + execute: Effect.sleep("1 second").pipe(Effect.as("activity")) + }) + ] + }) +) + +const LosingDeferredWorkflow = Workflow.make("LosingDeferredWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const LosingDeferredGate = DurableDeferred.make("LosingDeferredGate", { + success: Schema.String +}) + +const LosingDeferredWorkflowLayer = LosingDeferredWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + const winner = yield* DurableDeferred.raceAll({ + name: "losing-deferred", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(LosingDeferredGate), + Activity.make({ + name: "losing-deferred-activity", + success: Schema.String, + execute: Effect.sleep("1 second").pipe(Effect.as("activity")) + }) + ] + }) + const tail = yield* Activity.make({ + name: "losing-deferred-tail", + success: Schema.String, + execute: Effect.suspend(() => { + const runs = flags.get("losing-deferred-tail-runs") + flags.set("losing-deferred-tail-runs", typeof runs === "number" ? runs + 1 : 1) + return Effect.sleep("10 seconds").pipe(Effect.as("tail")) + }) + }) + return `${winner}:${tail}` +})) + +const InPlaceWakeWorkflow = Workflow.make("InPlaceWakeWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const InPlaceWakeGate = DurableDeferred.make("InPlaceWakeGate", { + success: Schema.String +}) + +const InPlaceWakeWorkflowLayer = InPlaceWakeWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + const runs = flags.get("in-place-wake-runs") + flags.set("in-place-wake-runs", typeof runs === "number" ? runs + 1 : 1) + return yield* DurableDeferred.raceAll({ + name: "in-place-wake", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(InPlaceWakeGate), + Activity.make({ + name: "in-place-wake-activity", + success: Schema.String, + execute: Effect.sleep("5 seconds").pipe(Effect.as("activity")) + }) + ] + }) +})) + +const IntoWrapWorkflow = Workflow.make("IntoWrapWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const IntoWrapGate = DurableDeferred.make("IntoWrapGate", { + success: Schema.String +}) + +const IntoWrapAux = DurableDeferred.make("IntoWrapAux", { + success: Schema.String +}) + +const IntoWrapWorkflowLayer = IntoWrapWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "into-wrap", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.into(DurableDeferred.await(IntoWrapGate), IntoWrapAux), + Activity.make({ + name: "into-wrap-activity", + success: Schema.String, + execute: Effect.sleep("30 seconds").pipe(Effect.as("activity")) + }) + ] + }) +) + +const ClockCaptureWorkflow = Workflow.make("ClockCaptureWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const ClockCaptureWorkflowLayer = ClockCaptureWorkflow.toLayer(() => + Activity.raceAll("clock-capture", [ + Activity.make({ + name: "clock-capture-durable", + success: Schema.String, + execute: DurableClock.sleep({ + name: "clock-capture-durable", + duration: 5000, + inMemoryThreshold: Duration.zero + }).pipe(Effect.as("clock")) + }), + Activity.make({ + name: "clock-capture-slow", + success: Schema.String, + execute: Effect.sleep("30 seconds").pipe(Effect.as("slow")) + }) + ]) +) + +const BareClockWorkflow = Workflow.make("BareClockWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const BareClockWorkflowLayer = BareClockWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "bare-clock", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableClock.sleep({ + name: "bare-clock-timer", + duration: 5000, + inMemoryThreshold: Duration.zero + }).pipe(Effect.as("clock")), + Activity.make({ + name: "bare-clock-slow", + success: Schema.String, + execute: Effect.sleep("30 seconds").pipe(Effect.as("slow")) + }) + ] + }) +) + +const MappedGateWorkflow = Workflow.make("MappedGateWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const MappedGate = DurableDeferred.make("MappedGate", { + success: Schema.String +}) + +const MappedGateWorkflowLayer = MappedGateWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "mapped-gate", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(MappedGate).pipe(Effect.map((s) => `${s}!`)), + Activity.make({ + name: "mapped-gate-slow", + success: Schema.String, + execute: Effect.sleep("30 seconds").pipe(Effect.as("slow")) + }) + ] + }) +) + +const PreGateWorkflow = Workflow.make("PreGateWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const PreGate = DurableDeferred.make("PreGate", { + success: Schema.String +}) + +const PreGateWorkflowLayer = PreGateWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "pre-gate", + success: Schema.String, + error: Schema.Never, + effects: [ + Effect.gen(function*() { + const a = yield* Activity.make({ + name: "pre-gate-activity", + success: Schema.String, + execute: Effect.succeed("act") + }) + const s = yield* DurableDeferred.await(PreGate) + return `${a}:${s}` + }), + Activity.make({ + name: "pre-gate-slow", + success: Schema.String, + execute: Effect.sleep("30 seconds").pipe(Effect.as("slow")) + }) + ] + }) +) + +const TwoStepWorkflow = Workflow.make("TwoStepWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const TwoStepGateA = DurableDeferred.make("TwoStepGateA", { + success: Schema.String +}) + +const TwoStepGateB = DurableDeferred.make("TwoStepGateB", { + success: Schema.String +}) + +const TwoStepWorkflowLayer = TwoStepWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "two-step", + success: Schema.String, + error: Schema.Never, + effects: [ + Effect.gen(function*() { + const flags = yield* Flags + const runs = flags.get("two-step-branch-runs") + flags.set("two-step-branch-runs", typeof runs === "number" ? runs + 1 : 1) + const a = yield* DurableDeferred.await(TwoStepGateA) + const b = yield* DurableDeferred.await(TwoStepGateB) + return `${a}:${b}` + }), + // a live branch that holds no activity slot, so wake re-runs are + // processed while the race stays active + Effect.never + ] + }) +) + +const SlowUnwindWorkflow = Workflow.make("SlowUnwindWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const SlowUnwindGateA = DurableDeferred.make("SlowUnwindGateA", { + success: Schema.String +}) + +const SlowUnwindGateB = DurableDeferred.make("SlowUnwindGateB", { + success: Schema.String +}) + +const SlowUnwindWorkflowLayer = SlowUnwindWorkflow.toLayer(Effect.fnUntraced(function*() { + const flags = yield* Flags + const runs = flags.get("slow-unwind-runs") + flags.set("slow-unwind-runs", typeof runs === "number" ? runs + 1 : 1) + return yield* DurableDeferred.raceAll({ + name: "slow-unwind", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(SlowUnwindGateA), + DurableDeferred.await(SlowUnwindGateB) + ] + }).pipe( + // slows the unwind so completions can land while a suspension commits + Effect.ensuring(Effect.sleep("10 seconds")) + ) +})) + +const TwoGateWorkflow = Workflow.make("TwoGateWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const TwoGateA = DurableDeferred.make("TwoGateA", { + success: Schema.String +}) + +const TwoGateB = DurableDeferred.make("TwoGateB", { + success: Schema.String +}) + +const TwoGateWorkflowLayer = TwoGateWorkflow.toLayer(() => + DurableDeferred.raceAll({ + name: "two-gates", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(TwoGateA), + DurableDeferred.await(TwoGateB) + ] + }) +) + const ParentWorkflow = Workflow.make("ParentWorkflow", { payload: { id: Schema.String @@ -640,7 +1354,7 @@ const ShardedClockWorkflow = Workflow.make("ShardedClockWorkflow", { const ShardedClockWorkflowLayer = ShardedClockWorkflow.toLayer(Effect.fnUntraced(function*() { yield* DurableClock.sleep({ name: "ShardedClock", - duration: "10 seconds", + duration: 10000.5, inMemoryThreshold: Duration.zero }) })) @@ -732,9 +1446,24 @@ const makeBatchRequestError = () => { return error } -const TestWorkflowLayer = EmailWorkflowLayer.pipe( - Layer.merge(RaceWorkflowLayer), +const RaceWorkflowLayers = RaceWorkflowLayer.pipe( + Layer.merge(FailureRaceWorkflowLayer), Layer.merge(DurableRaceWorkflowLayer), + Layer.merge(MixedRaceWorkflowLayer), + Layer.merge(LosingDeferredWorkflowLayer), + Layer.merge(InPlaceWakeWorkflowLayer), + Layer.merge(IntoWrapWorkflowLayer), + Layer.merge(ClockCaptureWorkflowLayer), + Layer.merge(BareClockWorkflowLayer), + Layer.merge(MappedGateWorkflowLayer), + Layer.merge(TwoStepWorkflowLayer), + Layer.merge(PreGateWorkflowLayer), + Layer.merge(SlowUnwindWorkflowLayer), + Layer.merge(TwoGateWorkflowLayer) +) + +const TestWorkflowLayer = EmailWorkflowLayer.pipe( + Layer.merge(RaceWorkflowLayers), Layer.merge(ParentWorkflowLayer), Layer.merge(ChildWorkflowLayer), Layer.merge(ShardedClockWorkflowLayer), diff --git a/.context/effect/packages/effect/test/cluster/Entity.test.ts b/.context/effect/packages/effect/test/cluster/Entity.test.ts index 1414eff29..5722ed2ba 100644 --- a/.context/effect/packages/effect/test/cluster/Entity.test.ts +++ b/.context/effect/packages/effect/test/cluster/Entity.test.ts @@ -1,7 +1,7 @@ import { assert, describe, it } from "@effect/vitest" import { type Cause, Effect, Queue, Schema, Stream } from "effect" import { Entity, ShardingConfig } from "effect/unstable/cluster" -import { Rpc } from "effect/unstable/rpc/index" +import { Rpc } from "effect/unstable/rpc" import { CallerId, ContextBleedEntity, ContextBleedLayer, TestEntity, TestEntityLayer, User } from "./TestEntity.ts" const StreamEntity = Entity.make("StreamEntity", [ diff --git a/.context/effect/packages/effect/test/cluster/MessageStorage.test.ts b/.context/effect/packages/effect/test/cluster/MessageStorage.test.ts index f580519bc..b0fdb941e 100644 --- a/.context/effect/packages/effect/test/cluster/MessageStorage.test.ts +++ b/.context/effect/packages/effect/test/cluster/MessageStorage.test.ts @@ -23,6 +23,32 @@ const MemoryLive = MessageStorage.layerMemory.pipe( describe("MessageStorage", () => { describe("memory", () => { + it.effect("removes the primary-key index when clearing an address", () => + Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + const address = EntityAddress.make({ + shardId: ShardId.make("default", 1), + entityType: EntityType.make("Repro"), + entityId: EntityId.make("one") + }) + const envelope: Envelope.PartialRequestEncoded = { + _tag: "Request", + requestId: "1", + address: { shardId: { group: "default", id: 1 }, entityType: "Repro", entityId: "one" }, + tag: "Repro", + payload: {}, + headers: {} + } + yield* driver.encoded.saveEnvelope({ envelope, primaryKey: "dedup-key", deliverAt: null }) + yield* driver.encoded.clearAddress(address) + const result = yield* driver.encoded.saveEnvelope({ + envelope: { ...envelope, requestId: "2" }, + primaryKey: "dedup-key", + deliverAt: null + }) + expect(result._tag).toEqual("Success") + }).pipe(Effect.provide(MessageStorage.MemoryDriver.layer))) + it.effect("saves a request", () => Effect.gen(function*() { const storage = yield* MessageStorage.MessageStorage diff --git a/.context/effect/packages/effect/test/cluster/ResourceMap.test.ts b/.context/effect/packages/effect/test/cluster/ResourceMap.test.ts new file mode 100644 index 000000000..76508baab --- /dev/null +++ b/.context/effect/packages/effect/test/cluster/ResourceMap.test.ts @@ -0,0 +1,18 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" +import { ResourceMap } from "effect/unstable/cluster/internal/resourceMap" + +describe("ResourceMap", () => { + it.effect("closes a failed lookup scope", () => + Effect.scoped(Effect.gen(function*() { + let finalized = 0 + const map = yield* ResourceMap.make((_key: string) => + Effect.gen(function*() { + yield* Effect.addFinalizer(() => Effect.sync(() => finalized++)) + return yield* Effect.fail("failed") + }) + ) + yield* Effect.exit(map.get("key")) + assert.strictEqual(finalized, 1) + }))) +}) diff --git a/.context/effect/packages/effect/test/cluster/ResourceRef.test.ts b/.context/effect/packages/effect/test/cluster/ResourceRef.test.ts new file mode 100644 index 000000000..51288dde8 --- /dev/null +++ b/.context/effect/packages/effect/test/cluster/ResourceRef.test.ts @@ -0,0 +1,65 @@ +import { assert, it } from "@effect/vitest" +import { Deferred, Effect, Exit, Fiber, Option, Scope } from "effect" +import { ResourceRef } from "effect/unstable/cluster/internal/resourceRef" + +it.live("does not wedge await after a failed rebuild", () => + Effect.scoped(Effect.gen(function*() { + const parentScope = yield* Effect.scope + let fail = false + let releases = 0 + const ref = yield* ResourceRef.from(parentScope, (scope) => + Scope.addFinalizer( + scope, + Effect.sync(() => releases++) + ).pipe( + Effect.andThen(fail ? Effect.fail("failed") : Effect.succeed(1)) + )) + fail = true + assert.deepStrictEqual(yield* Effect.exit(ref.rebuildUnsafe()), Exit.fail("failed")) + assert.strictEqual(releases, 2) + const completed = yield* Effect.exit(ref.await).pipe(Effect.timeoutOption(10)) + assert.deepStrictEqual(completed, Option.some(Exit.fail("failed"))) + fail = false + yield* ref.rebuildUnsafe() + assert.strictEqual(yield* ref.await, 1) + }))) + +it.effect("does not let a stale rebuild overwrite a newer acquisition", () => + Effect.scoped(Effect.gen(function*() { + const parentScope = yield* Effect.scope + const releasing = yield* Deferred.make() + const release = yield* Deferred.make() + const newerAcquiring = yield* Deferred.make() + const newerAcquire = yield* Deferred.make() + let acquisitions = 0 + const ref = yield* ResourceRef.from(parentScope, (scope) => + Effect.gen(function*() { + const acquisition = ++acquisitions + if (acquisition === 1) { + yield* Scope.addFinalizer( + scope, + Deferred.succeed(releasing, void 0).pipe(Effect.andThen(Deferred.await(release))) + ) + } else if (acquisition === 2) { + yield* Deferred.succeed(newerAcquiring, void 0) + yield* Deferred.await(newerAcquire) + } else { + return yield* Effect.fail("stale") + } + return acquisition + })) + + const staleRebuild = yield* Effect.forkChild(ref.rebuildUnsafe()) + yield* Deferred.await(releasing) + const newerRebuild = yield* Effect.forkChild(ref.rebuildUnsafe()) + yield* Deferred.await(newerAcquiring) + + yield* Deferred.succeed(release, void 0) + assert.deepStrictEqual(yield* Fiber.await(staleRebuild), Exit.fail("stale")) + assert.strictEqual(ref.state.current._tag, "Acquiring") + assert.isFalse(ref.latch.isOpen()) + + yield* Deferred.succeed(newerAcquire, void 0) + yield* Fiber.join(newerRebuild) + assert.strictEqual(yield* ref.await, 2) + }))) diff --git a/.context/effect/packages/effect/test/cluster/RunnerServer.test.ts b/.context/effect/packages/effect/test/cluster/RunnerServer.test.ts new file mode 100644 index 000000000..b43f12274 --- /dev/null +++ b/.context/effect/packages/effect/test/cluster/RunnerServer.test.ts @@ -0,0 +1,95 @@ +import { assert, it } from "@effect/vitest" +import { Effect, Layer, Option, Queue, Schema, Stream } from "effect" +import { TestClock } from "effect/testing" +import { + ClusterSchema, + Entity, + EntityAddress, + EntityId, + EntityType, + Envelope, + MessageStorage, + RunnerHealth, + Runners, + RunnerServer, + RunnerStorage, + Sharding, + ShardingConfig, + Snowflake +} from "effect/unstable/cluster" +import { Headers } from "effect/unstable/http" +import { Rpc, RpcTest } from "effect/unstable/rpc" + +const ReproEntity = Entity.make("ReproRunnerServer", [ + Rpc.make("ReproStream", { success: Schema.Int, payload: { id: Schema.Number }, stream: true }) +]).annotateRpcs(ClusterSchema.Persisted, false) + +const handlers = RunnerServer.layerHandlers.pipe( + Layer.provideMerge(ReproEntity.toLayer({ ReproStream: () => Stream.make(1) })), + Layer.provideMerge(Sharding.layer), + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100, + refreshAssignmentsInterval: 0 + })) +) + +it.effect("completes a successful runner stream", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const sharding = yield* Sharding.Sharding + const snowflake = yield* Snowflake.Generator + const entityId = EntityId.make("one") + const request = { + _tag: "Request", + requestId: snowflake.nextUnsafe(), + address: EntityAddress.make({ + shardId: sharding.getShardId(entityId, ReproEntity.getShardGroup(entityId)), + entityType: EntityType.make("ReproRunnerServer"), + entityId + }), + tag: "ReproStream", + payload: { id: 1 }, + headers: Headers.empty + } as Envelope.PartialRequest + const client = yield* RpcTest.makeClient(Runners.Rpcs) + const queue = yield* client.Stream({ request, persisted: false }, { asQueue: true }) + const first = yield* Queue.take(queue).pipe( + Effect.timeout("1 second"), + TestClock.withLive + ) + if (first._tag !== "Chunk") { + return assert.fail("expected the stream value before the terminal reply") + } + assert.deepStrictEqual(first.values, [1]) + + yield* client.Envelope({ + envelope: new Envelope.AckChunk({ + id: snowflake.nextUnsafe(), + address: request.address, + requestId: request.requestId, + replyId: Snowflake.Snowflake(first.id) + }), + persisted: false + }) + + const completion = yield* Effect.gen(function*() { + const last = yield* Queue.take(queue) + yield* Queue.take(queue).pipe(Effect.catchTag("Done", () => Effect.void)) + return last + }).pipe( + Effect.timeoutOption("1 second"), + TestClock.withLive + ) + if (Option.isNone(completion)) { + return assert.fail("expected the runner stream to complete") + } + assert.strictEqual(completion.value._tag, "WithExit") + }).pipe(Effect.provide(handlers))) diff --git a/.context/effect/packages/effect/test/cluster/RunnerStorage.test.ts b/.context/effect/packages/effect/test/cluster/RunnerStorage.test.ts index b785512b4..4e9598362 100644 --- a/.context/effect/packages/effect/test/cluster/RunnerStorage.test.ts +++ b/.context/effect/packages/effect/test/cluster/RunnerStorage.test.ts @@ -1,8 +1,18 @@ import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" -import { RunnerAddress, RunnerStorage, ShardId } from "effect/unstable/cluster" +import { Runner, RunnerAddress, RunnerStorage, ShardId } from "effect/unstable/cluster" describe("RunnerStorage", () => { + it.effect("tracks runner health in memory", () => + Effect.gen(function*() { + const storage = yield* RunnerStorage.makeMemory + const address = RunnerAddress.make("localhost", 41001) + yield* storage.register(Runner.make({ address, groups: ["default"], weight: 1 }), false) + assert.strictEqual((yield* storage.getRunners)[0][1], false) + yield* storage.setRunnerHealth(address, true) + assert.strictEqual((yield* storage.getRunners)[0][1], true) + })) + it.effect("memory acquire accepts a one-shot iterable", () => Effect.gen(function*() { const storage = yield* RunnerStorage.makeMemory diff --git a/.context/effect/packages/effect/test/cluster/Runners.test.ts b/.context/effect/packages/effect/test/cluster/Runners.test.ts new file mode 100644 index 000000000..a75ed0264 --- /dev/null +++ b/.context/effect/packages/effect/test/cluster/Runners.test.ts @@ -0,0 +1,309 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Context, Effect, Exit, Layer, Option, Queue, Schema, Stream } from "effect" +import { TestClock } from "effect/testing" +import { + ClusterError, + ClusterSchema, + Entity, + EntityAddress, + EntityId, + EntityType, + Envelope, + Message, + MessageStorage, + type Reply, + RunnerAddress, + RunnerHealth, + Runners, + RunnerServer, + RunnerStorage, + ShardId, + Sharding, + ShardingConfig, + Snowflake +} from "effect/unstable/cluster" +import { Headers } from "effect/unstable/http" +import { Rpc, RpcClient, RpcTest } from "effect/unstable/rpc" +import { RpcClientError } from "effect/unstable/rpc/RpcClientError" +import type { FromClientEncoded, FromServerEncoded } from "effect/unstable/rpc/RpcMessage" +import { Socket } from "effect/unstable/socket" + +// An entity whose replies cannot be serialized: the handlers return +// non-integers for a `Schema.Int` success schema, so `Reply.serialize` fails +// on encode. +const BadReplyEntity = Entity.make("BadReplyEntity", [ + Rpc.make("BadReply", { + success: Schema.Int, + payload: { id: Schema.Number } + }), + Rpc.make("BadStream", { + success: Schema.Int, + payload: { id: Schema.Number }, + stream: true + }) +]).annotateRpcs(ClusterSchema.Persisted, false) + +const BadReplyEntityLayer = BadReplyEntity.toLayer({ + BadReply: () => Effect.succeed(1.5), + BadStream: () => Stream.make(2.5) +}) + +const TestShardingConfig = ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100, + refreshAssignmentsInterval: 0 +}) + +const RunnerServerHandlers = RunnerServer.layerHandlers.pipe( + Layer.provideMerge(BadReplyEntityLayer), + Layer.provideMerge(Sharding.layer), + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(TestShardingConfig) +) + +describe.concurrent("RunnerServer", () => { + const makeRequest = (options: { + readonly entityId: string + readonly tag: string + }) => + Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const snowflakeGen = yield* Snowflake.Generator + const entityId = EntityId.make(options.entityId) + const address = EntityAddress.make({ + shardId: sharding.getShardId(entityId, BadReplyEntity.getShardGroup(entityId)), + entityType: EntityType.make("BadReplyEntity"), + entityId + }) + return { + _tag: "Request", + requestId: snowflakeGen.nextUnsafe(), + address, + tag: options.tag, + payload: { id: 1 }, + headers: Headers.empty + } as Envelope.PartialRequest + }) + + it.effect("a reply that fails to serialize fails only its own request", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const client = yield* RpcTest.makeClient(Runners.Rpcs) + const request = yield* makeRequest({ entityId: "bad-1", tag: "BadReply" }) + + const exit = yield* Effect.exit(client.Effect({ request, persisted: false })) + if (!Exit.isSuccess(exit)) { + return assert.fail("Effect rpc must not defect on a reply serialization failure") + } + const reply = exit.value + if (reply._tag !== "WithExit" || reply.exit._tag !== "Failure") { + return assert.fail("expected a WithExit reply with a failure exit") + } + assert.strictEqual(reply.requestId, String(request.requestId)) + const die = reply.exit.cause.find((entry) => entry._tag === "Die") + assert.isDefined(die, "the reply exit must carry the encode failure as a defect") + assert.include( + JSON.stringify(die), + "MalformedMessage", + "the defect must identify the encode failure" + ) + }).pipe(Effect.provide(RunnerServerHandlers))) + + it.effect("a stream reply that fails to serialize ends the stream with the defect", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const client = yield* RpcTest.makeClient(Runners.Rpcs) + const request = yield* makeRequest({ entityId: "bad-2", tag: "BadStream" }) + + const queue = yield* client.Stream({ request, persisted: false }, { asQueue: true }) + const replies: Array = [] + yield* Queue.take(queue).pipe( + Effect.flatMap((reply) => + Effect.sync(() => { + replies.push(reply) + }) + ), + Effect.forever, + Effect.catchTag("Done", () => Effect.void) + ) + + assert.strictEqual(replies.length, 1) + const last = replies[0] + if (last._tag !== "WithExit" || last.exit._tag !== "Failure") { + return assert.fail("expected a terminal WithExit reply with a failure exit") + } + assert.strictEqual(last.requestId, String(request.requestId)) + assert.include( + JSON.stringify(last.exit.cause), + "MalformedMessage", + "the defect must identify the encode failure" + ) + }).pipe(Effect.provide(RunnerServerHandlers))) +}) + +describe.concurrent("Runners.makeRpc", () => { + const runnerAddress = RunnerAddress.make("localhost", 42_000) + + const TestRpc = Rpc.make("TestRpc", { + success: Schema.Number, + payload: { id: Schema.Number } + }).annotate(ClusterSchema.Persisted, false) + + const TestRpcPersisted = Rpc.make("TestRpcPersisted", { + success: Schema.Number, + payload: { id: Schema.Number } + }).annotate(ClusterSchema.Persisted, true) + + type SendRpc = typeof TestRpc | typeof TestRpcPersisted + + const makeOutgoingRequest = ( + rpc: SendRpc, + requestId: Snowflake.Snowflake, + respond: (reply: Reply.Reply) => Effect.Effect + ): Message.OutgoingRequest => + new Message.OutgoingRequest({ + envelope: Envelope.makeRequest({ + requestId, + address: EntityAddress.make({ + shardId: ShardId.make("default", 1), + entityType: EntityType.make("TestRpcEntity"), + entityId: EntityId.make("1") + }), + tag: rpc._tag, + payload: { id: 1 }, + headers: Headers.empty + }), + rpc, + context: Context.empty(), + lastReceivedReply: Option.none(), + respond, + annotations: Context.empty() + }) + + const layerFakeProtocol = ( + onRequest: ( + request: FromClientEncoded, + write: (data: FromServerEncoded) => Effect.Effect + ) => Effect.Effect + ) => + Layer.succeed(Runners.RpcClientProtocol)(() => + Effect.sync(() => { + let write!: (data: FromServerEncoded) => Effect.Effect + return RpcClient.Protocol.of({ + run(_clientId, f) { + write = f + return Effect.never + }, + send(_clientId, request) { + return onRequest(request, write) + }, + supportsAck: true, + supportsTransferables: false + }) + }) + ) + + const layerRunners = (protocol: Layer.Layer) => + Runners.layerRpc.pipe( + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide(protocol), + Layer.provideMerge(MessageStorage.layerNoop), + Layer.provide(TestShardingConfig) + ) + + const respondWithDefect = (request: FromClientEncoded, write: (data: FromServerEncoded) => Effect.Effect) => + request._tag === "Request" ? write({ _tag: "Defect", defect: "boom" }) : Effect.void + + const failTransport = () => + Effect.fail( + new RpcClientError({ + reason: new Socket.SocketCloseError({ code: 1006 }) + }) + ) + + it.effect("a server-delivered defect resolves the request instead of RunnerUnavailable", () => + Effect.gen(function*() { + const runners = yield* Runners.Runners + const snowflakeGen = yield* Snowflake.Generator + const replies: Array> = [] + const message = makeOutgoingRequest(TestRpc, snowflakeGen.nextUnsafe(), (reply) => + Effect.sync(() => { + replies.push(reply) + })) + + const exit = yield* Effect.exit(runners.send({ address: runnerAddress, message })) + assert.isTrue(Exit.isSuccess(exit), "send must not fail with RunnerUnavailable for a delivered defect") + assert.strictEqual(replies.length, 1) + const reply = replies[0] + if (reply._tag !== "WithExit" || !Exit.isFailure(reply.exit)) { + return assert.fail("expected a WithExit reply with a failure exit") + } + assert.include( + String(Cause.squash(reply.exit.cause)), + "boom", + "the reply must carry the server defect" + ) + }).pipe(Effect.provide(layerRunners(layerFakeProtocol(respondWithDefect))))) + + it.effect("transport failures still map to RunnerUnavailable", () => + Effect.gen(function*() { + const runners = yield* Runners.Runners + const snowflakeGen = yield* Snowflake.Generator + const message = makeOutgoingRequest(TestRpc, snowflakeGen.nextUnsafe(), () => Effect.void) + + const exit = yield* Effect.exit(runners.send({ address: runnerAddress, message })) + if (!Exit.isFailure(exit)) { + return assert.fail("send must fail for a transport failure") + } + assert.instanceOf(Cause.squash(exit.cause), ClusterError.RunnerUnavailable) + }).pipe(Effect.provide(layerRunners(layerFakeProtocol(failTransport))))) + + it.effect("volatile notification transport failures map to RunnerUnavailable", () => + Effect.gen(function*() { + const runners = yield* Runners.Runners + const snowflakeGen = yield* Snowflake.Generator + const message = makeOutgoingRequest(TestRpc, snowflakeGen.nextUnsafe(), () => Effect.void) + + const exit = yield* Effect.exit(runners.notify({ + address: Option.some(runnerAddress), + message, + discard: true + })) + if (!Exit.isFailure(exit)) { + return assert.fail("volatile notification must fail when delivery fails") + } + assert.instanceOf(Cause.squash(exit.cause), ClusterError.RunnerUnavailable) + }).pipe(Effect.provide(layerRunners(layerFakeProtocol(failTransport))))) + + it.effect("persisted notification transport failures are ignored", () => + Effect.gen(function*() { + const runners = yield* Runners.Runners + const snowflakeGen = yield* Snowflake.Generator + const message = makeOutgoingRequest(TestRpcPersisted, snowflakeGen.nextUnsafe(), () => Effect.void) + + yield* runners.notify({ + address: Option.some(runnerAddress), + message, + discard: true + }) + }).pipe(Effect.provide(layerRunners(layerFakeProtocol(failTransport))))) + + it.effect("a delivered defect for a persisted request maps to RunnerUnavailable for storage recovery", () => + Effect.gen(function*() { + const runners = yield* Runners.Runners + const snowflakeGen = yield* Snowflake.Generator + const message = makeOutgoingRequest(TestRpcPersisted, snowflakeGen.nextUnsafe(), () => Effect.void) + + const exit = yield* Effect.exit(runners.send({ address: runnerAddress, message })) + if (!Exit.isFailure(exit)) { + return assert.fail("send must fail for a delivered defect on a persisted request") + } + assert.instanceOf(Cause.squash(exit.cause), ClusterError.RunnerUnavailable) + }).pipe(Effect.provide(layerRunners(layerFakeProtocol(respondWithDefect))))) +}) diff --git a/.context/effect/packages/effect/test/cluster/Sharding.test.ts b/.context/effect/packages/effect/test/cluster/Sharding.test.ts index 2a526bbc6..651bee012 100644 --- a/.context/effect/packages/effect/test/cluster/Sharding.test.ts +++ b/.context/effect/packages/effect/test/cluster/Sharding.test.ts @@ -1,16 +1,42 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Array, Cause, Clock, Effect, Exit, Fiber, Layer, MutableRef, Option, Queue, Stream } from "effect" +import { + Array, + Cause, + Clock, + Context, + Deferred, + Effect, + Exit, + Fiber, + Latch, + Layer, + Logger, + MutableRef, + Option, + Queue, + Schema, + Stream +} from "effect" import { TestClock } from "effect/testing" import { + ClusterError, + ClusterMetrics, + ClusterSchema, + Entity, + EntityId, + MachineId, MessageStorage, + Runner, RunnerAddress, RunnerHealth, Runners, RunnerStorage, + ShardId, Sharding, ShardingConfig, Snowflake } from "effect/unstable/cluster" +import { Rpc } from "effect/unstable/rpc" import { CallerId, ContextBleedEntity, @@ -59,6 +85,22 @@ describe.concurrent("Sharding", () => { expect(driver.unprocessed.size).toEqual(0) }).pipe(Effect.provide(TestSharding))) + it.live("defects instead of hanging when persisted failures contain non-JSON Error values", () => + Effect.gen(function*() { + const driver = yield* MessageStorage.MemoryDriver + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const cause = yield* client.Fail().pipe( + Effect.timeout("2 seconds"), + Effect.sandbox, + Effect.flip + ) + assert(Cause.hasDies(cause)) + assert.include(Cause.pretty(cause), "MalformedMessage") + assert.strictEqual(driver.replyIds.size, 1) + assert.strictEqual(driver.unprocessed.size, 0) + }).pipe(Effect.provide(TestSharding))) + it.effect("routes durable interrupts through storage", () => Effect.gen(function*() { const driver = yield* MessageStorage.MemoryDriver @@ -95,6 +137,258 @@ describe.concurrent("Sharding", () => { expect(driver.replyIds.size).toEqual(0) })) + for (const persisted of [false, true] as const) { + for (const preemptiveShutdown of [true, false]) { + it.live( + `shutdown completes when a finalizing entity sends an outgoing message (persisted=${persisted}, preemptiveShutdown=${preemptiveShutdown})`, + () => + Effect.gen(function*() { + const discardExit = yield* Deferred.make>() + const Receiver = Entity.make("ShutdownDeadlockReceiver", [ + Rpc.make("Ping").annotate(ClusterSchema.Persisted, persisted) + ]) + const ReceiverLayer = Receiver.toLayer({ Ping: () => Effect.void }) + + const Sender = Entity.make("ShutdownDeadlockSender", [ + Rpc.make("Arm").annotate(ClusterSchema.Persisted, false) + ]) + const SenderLayer = Sender.toLayer(Effect.gen(function*() { + const receiver = yield* Receiver.client + // finalizer sends an outgoing message during entity teardown + yield* Effect.addFinalizer(() => + Effect.uninterruptible( + Effect.sleep(300).pipe( + Effect.andThen(receiver("peer").Ping(void 0, { discard: true })), + Effect.exit, + Effect.flatMap((exit) => Deferred.succeed(discardExit, exit)) + ) + ) + ) + return { Arm: () => Effect.void } + })) + + const env = Layer.mergeAll(SenderLayer, ReceiverLayer).pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 8, + entityTerminationTimeout: 0, + entityMessagePollInterval: 50, + refreshAssignmentsInterval: 20, + sendRetryInterval: 10, + preemptiveShutdown + })) + ) + + const shardAcquired = Latch.makeUnsafe() + const armed = Latch.makeUnsafe() + let driver!: MessageStorage.MemoryDriver["Service"] + const runFiber = yield* Effect.gen(function*() { + driver = yield* MessageStorage.MemoryDriver + const sharding = yield* Sharding.Sharding + const shardId = sharding.getShardId(EntityId.make("1"), "default") + while (!sharding.hasShardId(shardId)) { + yield* Effect.sleep(5) + } + yield* shardAcquired.open + yield* (yield* Sender.client)("1").Arm() + yield* armed.open + return yield* Effect.never + }).pipe(Effect.provide(env), Effect.scoped, Effect.forkDetach) + + const acquired = yield* shardAcquired.await.pipe(Effect.timeoutOption("8 seconds")) + if (Option.isNone(acquired)) { + runFiber.interruptUnsafe() + yield* Fiber.await(runFiber) + } + assert(Option.isSome(acquired), "Timed out waiting for sender shard acquisition") + yield* armed.await + + // Interrupting the fiber closes the Sharding scope, running the entity + // finalizer (and its outgoing send) as part of teardown + runFiber.interruptUnsafe() + const completed = yield* Fiber.await(runFiber).pipe(Effect.timeoutOption("4 seconds")) + assert(Option.isSome(completed), "Sharding scope-close hung during shutdown (deadlock)") + assert(Exit.isSuccess(yield* Deferred.await(discardExit)), "discard send failed during shutdown") + assert.strictEqual(driver.journal.length, persisted ? 1 : 0) + }), + 20_000 + ) + } + + it.live( + `shutdown completes when a finalizing entity awaits a reply from an unroutable entity (persisted=${persisted})`, + () => + Effect.gen(function*() { + const requestExit = yield* Deferred.make>() + const Receiver = Entity.make("StrandReceiver", [ + Rpc.make("Ping").annotate(ClusterSchema.Persisted, persisted) + ]) + const ReceiverLayer = Receiver.toLayer({ Ping: () => Effect.void }) + + const Sender = Entity.make("StrandSender", [ + Rpc.make("Arm").annotate(ClusterSchema.Persisted, false) + ]) + const SenderLayer = Sender.toLayer(Effect.gen(function*() { + const receiver = yield* Receiver.client + // uninterruptible finalizer that awaits a reply from another entity + yield* Effect.addFinalizer(() => + Effect.uninterruptible( + Effect.sleep(300).pipe( + Effect.andThen(receiver("peer").Ping()), + Effect.exit, + Effect.flatMap((exit) => Deferred.succeed(requestExit, exit)) + ) + ) + ) + return { Arm: () => Effect.void } + })) + + const env = Layer.mergeAll(SenderLayer, ReceiverLayer).pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 8, + entityTerminationTimeout: 0, + entityMessagePollInterval: 50, + refreshAssignmentsInterval: 20, + sendRetryInterval: 10 + })) + ) + + const shardAcquired = Latch.makeUnsafe() + const armed = Latch.makeUnsafe() + let driver!: MessageStorage.MemoryDriver["Service"] + const runFiber = yield* Effect.gen(function*() { + driver = yield* MessageStorage.MemoryDriver + const sharding = yield* Sharding.Sharding + const shardId = sharding.getShardId(EntityId.make("1"), "default") + while (!sharding.hasShardId(shardId)) { + yield* Effect.sleep(5) + } + yield* shardAcquired.open + yield* (yield* Sender.client)("1").Arm() + yield* armed.open + return yield* Effect.never + }).pipe(Effect.provide(env), Effect.scoped, Effect.forkDetach) + + const acquired = yield* shardAcquired.await.pipe(Effect.timeoutOption("8 seconds")) + if (Option.isNone(acquired)) { + runFiber.interruptUnsafe() + yield* Fiber.await(runFiber) + } + assert(Option.isSome(acquired), "Timed out waiting for sender shard acquisition") + yield* armed.await + + runFiber.interruptUnsafe() + const completed = yield* Fiber.await(runFiber).pipe(Effect.timeoutOption("4 seconds")) + assert(Option.isSome(completed), "Sharding scope-close hung during shutdown (stranded caller)") + assert(!Exit.hasDies(completed.value), "finalizer defected instead of failing cleanly") + const exit = yield* Deferred.await(requestExit) + assert(Exit.isFailure(exit), "request succeeded instead of failing during shutdown") + const failure = Cause.findErrorOption(exit.cause) + assert(Option.isSome(failure), "request did not fail with a typed error") + assert(failure.value instanceof ClusterError.EntityNotAssignedToRunner) + assert.strictEqual(driver.journal.length, persisted ? 1 : 0) + }), + 20_000 + ) + } + + it.live("fails a stream when its chunk acknowledgement is abandoned during shutdown", () => + Effect.gen(function*() { + const chunks = yield* Queue.unbounded() + const streamStarted = yield* Deferred.make() + const streamExit = yield* Deferred.make>() + + const Receiver = Entity.make("ShutdownStreamReceiver", [ + Rpc.make("Values", { success: Schema.Number, stream: true }).annotate(ClusterSchema.Persisted, true) + ]) + const ReceiverLayer = Receiver.toLayer({ + Values: () => Stream.fromQueue(chunks).pipe(Stream.onStart(Deferred.succeed(streamStarted, void 0))) + }) + + const Sender = Entity.make("ShutdownStreamSender", [ + Rpc.make("Arm").annotate(ClusterSchema.Persisted, false) + ]) + const SenderLayer = Sender.toLayer(Effect.gen(function*() { + const receiver = yield* Receiver.client + const streamFiber = yield* receiver("peer").Values().pipe( + Stream.runDrain, + Effect.uninterruptible, + Effect.forkScoped({ startImmediately: true }) + ) + yield* Deferred.await(streamStarted) + yield* Effect.addFinalizer(() => + Effect.uninterruptible( + Queue.offer(chunks, 1).pipe( + Effect.andThen(Fiber.await(streamFiber)), + Effect.flatMap((exit) => Deferred.succeed(streamExit, exit)) + ) + ) + ) + return { Arm: () => Effect.void } + })) + + const env = Layer.mergeAll(SenderLayer, ReceiverLayer).pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provideMerge(MessageStorage.layerMemory), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 8, + entityTerminationTimeout: 1000, + entityMessagePollInterval: 50, + refreshAssignmentsInterval: 20, + sendRetryInterval: 10 + })) + ) + + const shardAcquired = Latch.makeUnsafe() + const armed = Latch.makeUnsafe() + let driver!: MessageStorage.MemoryDriver["Service"] + const runFiber = yield* Effect.gen(function*() { + driver = yield* MessageStorage.MemoryDriver + const sharding = yield* Sharding.Sharding + const shardId = sharding.getShardId(EntityId.make("1"), "default") + while (!sharding.hasShardId(shardId)) { + yield* Effect.sleep(5) + } + yield* shardAcquired.open + yield* (yield* Sender.client)("1").Arm() + yield* armed.open + return yield* Effect.never + }).pipe(Effect.provide(env), Effect.scoped, Effect.forkDetach) + + const acquired = yield* shardAcquired.await.pipe(Effect.timeoutOption("8 seconds")) + if (Option.isNone(acquired)) { + runFiber.interruptUnsafe() + yield* Fiber.await(runFiber) + } + assert(Option.isSome(acquired), "Timed out waiting for sender shard acquisition") + yield* armed.await + + runFiber.interruptUnsafe() + const completed = yield* Fiber.await(runFiber).pipe(Effect.timeoutOption("4 seconds")) + assert(Option.isSome(completed), "Sharding scope-close hung after abandoning a stream chunk acknowledgement") + const exit = yield* Deferred.await(streamExit) + assert(Exit.isFailure(exit), "stream succeeded instead of failing during shutdown") + const failure = Cause.findErrorOption(exit.cause) + assert(Option.isSome(failure), "stream did not fail with a typed error") + assert(failure.value instanceof ClusterError.EntityNotAssignedToRunner) + assert.strictEqual(driver.journal.filter((envelope) => envelope._tag === "AckChunk").length, 1) + }), 20_000) + it.effect("interrupts are sent for volatile messages on shutdown", () => Effect.gen(function*() { let interrupted = false @@ -412,6 +706,132 @@ describe.concurrent("Sharding", () => { Layer.merge(TestEntityState.layer) )))) + it.effect("holds durable messages while entity layers are still building", () => + Effect.gen(function*() { + const config = ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityRegistrationTimeout: 6000, + entityTerminationTimeout: 0, + entityMessagePollInterval: 100, + sendRetryInterval: 100, + refreshAssignmentsInterval: 0 + }) + const env = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provide(config) + ) + const delayedEnv = TestEntityNoState.pipe( + Layer.provide(Layer.effectDiscard(Effect.sleep(10_000))), + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provide(config) + ) + 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* client.RequestWithKey({ key: "slow-registration" }).pipe(Effect.forkChild) + yield* TestClock.adjust(1) + }).pipe( + Effect.provide(env), + Effect.scoped + ) + + assert.strictEqual(driver.journal.length, 1) + assert.strictEqual(driver.replyIds.size, 0) + assert.strictEqual(driver.unprocessed.size, 1) + + const fiber = yield* Effect.never.pipe( + Effect.provide(delayedEnv), + Effect.scoped, + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(7500) + assert.strictEqual(driver.replyIds.size, 0) + assert.strictEqual(driver.unprocessed.size, 1) + + yield* TestClock.adjust(2500) + yield* Queue.offer(state.messages, void 0) + yield* TestClock.adjust(100) + + assert.strictEqual(driver.replyIds.size, 1) + assert.strictEqual(driver.unprocessed.size, 0) + yield* Fiber.interrupt(fiber) + }).pipe(Effect.provide(MessageStorage.layerMemory.pipe( + Layer.provide(ShardingConfig.layer({})), + Layer.merge(TestEntityState.layer) + )))) + + it.effect("defects durable messages when no entity ever registers", () => + Effect.gen(function*() { + const config = ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityRegistrationTimeout: 1000, + entityTerminationTimeout: 0, + entityMessagePollInterval: 100, + sendRetryInterval: 100, + refreshAssignmentsInterval: 0 + }) + const registeredEnv = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provide(config) + ) + const noEntitiesEnv = Sharding.layer.pipe( + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provide(config) + ) + const driver = yield* MessageStorage.MemoryDriver + const warnings: Array = [] + const logger = Logger.make((options) => { + if (options.logLevel === "Warn") { + warnings.push(options.message) + } + }) + + yield* Effect.gen(function*() { + yield* TestClock.adjust(1) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + yield* client.RequestWithKey({ key: "missing-registration" }).pipe(Effect.forkChild) + yield* TestClock.adjust(1) + }).pipe( + Effect.provide(registeredEnv), + Effect.scoped + ) + + const fiber = yield* Effect.never.pipe( + Effect.provide(noEntitiesEnv), + Effect.scoped, + Effect.withLogger(logger), + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(8000) + assert.isTrue(warnings.some((message) => + globalThis.Array.isArray(message) && message.includes("Could not find entity manager for address, retrying") + )) + assert.strictEqual(driver.replyIds.size, 1) + assert.strictEqual(driver.unprocessed.size, 0) + yield* Fiber.interrupt(fiber) + }).pipe(Effect.provide(MessageStorage.layerMemory.pipe( + Layer.provide(ShardingConfig.layer({})), + Layer.merge(TestEntityState.layer) + )))) + it.effect("durable streams are resumed on restart", () => Effect.gen(function*() { const EnvLayer = TestShardingWithoutState.pipe( @@ -499,6 +919,73 @@ describe.concurrent("Sharding", () => { expect(driver.unprocessed.size).toEqual(1) }).pipe(Effect.provide(TestSharding))) + it.effect("client discard returns while the volatile request keeps processing", () => + Effect.gen(function*() { + yield* TestClock.adjust(1) + const state = yield* TestEntityState + const makeClient = yield* TestEntity.client + const client = makeClient("1") + + const result = yield* client.NeverVolatile(void 0, { discard: true }) + + assert.isUndefined(result) + yield* TestClock.adjust(1) + assert.strictEqual(Queue.sizeUnsafe(state.envelopes), 1) + }).pipe(Effect.provide(TestSharding))) + + it.effect("client volatile discard retries a failed delivery", () => + Effect.gen(function*() { + let attempts = 0 + + yield* Effect.gen(function*() { + yield* TestClock.adjust(1) + const config = yield* ShardingConfig.ShardingConfig + ;(config as any).runnerAddress = Option.some(RunnerAddress.make("localhost", 1234)) + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const fiber = yield* client.NeverVolatile(void 0, { discard: true }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + assert.strictEqual(attempts, 1) + assert.isUndefined(fiber.pollUnsafe()) + yield* TestClock.adjust(100) + yield* Fiber.join(fiber) + assert.strictEqual(attempts, 2) + }).pipe( + Effect.provide(TestShardingWithoutRunners.pipe( + Layer.provide( + Layer.effect(Runners.Runners)( + Effect.gen(function*() { + const runners = yield* Runners.makeNoop + return { + ...runners, + notify(options) { + attempts++ + return attempts === 1 + ? Effect.fail( + new ClusterError.RunnerUnavailable({ + address: Option.getOrThrow(options.address) + }) + ) + : Effect.void + } + } + }) + ) + ), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provideMerge(ShardingConfig.layer({ + entityMailboxCapacity: 10, + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100, + refreshAssignmentsInterval: 0 + })) + )) + ) + })) + it.effect("defects when a durable request has no MessageStorage", () => Effect.gen(function*() { const makeClient = yield* TestEntity.client @@ -583,6 +1070,399 @@ describe.concurrent("Sharding", () => { })) }) +describe("Sharding shard lock failover", () => { + it.effect("interrupts entities and reacquires shards after lock storage recovers", () => + Effect.gen(function*() { + const storageState = makeFailoverStorageState() + const runnerStorage = Layer.effect( + RunnerStorage.RunnerStorage, + Effect.map(Clock.Clock, (clock) => makeFailoverStorage(storageState, clock)) + ) + const config = ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 1, + shardLockExpiration: 300, + shardLockRefreshInterval: 1000, + entityTerminationTimeout: 30_000, + entityMessagePollInterval: 10, + refreshAssignmentsInterval: 10, + sendRetryInterval: 10 + }) + const layer = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(runnerStorage), + Layer.provide(RunnerHealth.layerNoop), + Layer.provideMerge(TestEntityState.layer), + Layer.provide(Runners.layerNoop), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provide(config) + ) + + yield* Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const entityState = yield* TestEntityState + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const shardId = sharding.getShardId(EntityId.make("1"), "default") + + while (!sharding.hasShardId(shardId)) { + yield* TestClock.adjust(10) + } + while (!storageState.refreshCalls.some((call) => call.shards.length > 0)) { + yield* TestClock.adjust(100) + } + + const entityFiber = yield* client.NeverVolatile().pipe( + Effect.forkChild({ startImmediately: true }) + ) + yield* TestClock.adjust(1) + assert.strictEqual(Queue.sizeUnsafe(entityState.envelopes), 1) + + const acquireCount = storageState.acquireCalls.length + const partitionedAt = yield* Clock.currentTimeMillis + storageState.blackholed = true + + yield* TestClock.adjust(201) + + assert.isFalse(sharding.hasShardId(shardId)) + assert.isFalse(yield* sharding.isShutdown) + const entityExit = entityFiber.pollUnsafe() + assert(entityExit && Exit.hasInterrupts(entityExit)) + assert.strictEqual(ClusterMetrics.shards.valueUnsafe(Context.empty()).value, BigInt(0)) + + const failedRefreshes = storageState.refreshCalls.filter((call) => + call.at >= partitionedAt && call.shards.length > 0 + ) + assert.isAtMost(failedRefreshes.length, 2) + + yield* TestClock.adjust(1000) + assert.strictEqual(storageState.acquireCalls.length, acquireCount) + assert(storageState.refreshCalls.some((call) => call.at >= partitionedAt && call.shards.length === 0)) + assert( + storageState.refreshCalls + .filter((call) => call.at >= partitionedAt + 200) + .every((call) => call.shards.length === 0) + ) + + storageState.blackholed = false + yield* TestClock.adjust(101) + while (!sharding.hasShardId(shardId)) { + yield* TestClock.adjust(10) + } + + assert.isAbove(storageState.acquireCalls.length, acquireCount) + assert(storageState.acquireCalls.at(-1)!.shards.some((shard) => shard.id === shardId.id)) + assert.deepStrictEqual(yield* client.GetUserVolatile({ id: 2 }), new User({ id: 2, name: "User 2" })) + assert.strictEqual(Queue.sizeUnsafe(entityState.envelopes), 2) + }).pipe(Effect.provide(layer), Effect.scoped) + })) + + it.effect("keeps the graceful timeout for normal shard reassignment", () => + Effect.gen(function*() { + const storageState = makeFailoverStorageState() + const runnerStorage = Layer.effect( + RunnerStorage.RunnerStorage, + Effect.map(Clock.Clock, (clock) => makeFailoverStorage(storageState, clock)) + ) + const config = ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 1, + shardLockExpiration: 3000, + shardLockRefreshInterval: 100, + entityTerminationTimeout: 1000, + entityMessagePollInterval: 10, + refreshAssignmentsInterval: 10, + sendRetryInterval: 10 + }) + const layer = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(runnerStorage), + Layer.provide(RunnerHealth.layerNoop), + Layer.provideMerge(TestEntityState.layer), + Layer.provide(Runners.layerNoop), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provide(config) + ) + + yield* Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const shardId = sharding.getShardId(EntityId.make("1"), "default") + + while (!sharding.hasShardId(shardId)) { + yield* TestClock.adjust(10) + } + const entityFiber = yield* client.NeverVolatile().pipe( + Effect.forkChild({ startImmediately: true }) + ) + yield* TestClock.adjust(1) + + storageState.assignSelf = false + while (sharding.hasShardId(shardId)) { + yield* TestClock.adjust(10) + } + while ((yield* sharding.activeEntityCount) > 0) { + yield* TestClock.adjust(1) + } + + assert.isUndefined(entityFiber.pollUnsafe()) + assert.strictEqual(storageState.releaseCalls.length, 0) + yield* TestClock.adjust(900) + assert.isUndefined(entityFiber.pollUnsafe()) + assert.strictEqual(storageState.releaseCalls.length, 0) + + for (let i = 0; i < 20 && entityFiber.pollUnsafe() === undefined; i++) { + yield* TestClock.adjust(10) + } + const entityExit = entityFiber.pollUnsafe() + assert(entityExit && Exit.hasInterrupts(entityExit)) + assert.strictEqual(storageState.releaseCalls.length, 1) + }).pipe(Effect.provide(layer), Effect.scoped) + })) + + it.effect("does not wait for entity construction before a forced shard release", () => + Effect.gen(function*() { + const storageState = makeFailoverStorageState() + const runnerStorage = Layer.effect( + RunnerStorage.RunnerStorage, + Effect.map(Clock.Clock, (clock) => makeFailoverStorage(storageState, clock)) + ) + const config = ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup: 1, + shardLockExpiration: 300, + shardLockRefreshInterval: 1000, + entityTerminationTimeout: 0, + entityMessagePollInterval: 10, + refreshAssignmentsInterval: 10, + sendRetryInterval: 10 + }) + const layer = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(runnerStorage), + Layer.provide(RunnerHealth.layerNoop), + Layer.provideMerge(TestEntityState.layer), + Layer.provide(Runners.layerNoop), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provide(config) + ) + + yield* Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const entityState = yield* TestEntityState + const makeClient = yield* TestEntity.client + const client = makeClient("1") + const shardId = sharding.getShardId(EntityId.make("1"), "default") + + while (!sharding.hasShardId(shardId)) { + yield* TestClock.adjust(10) + } + while (!storageState.refreshCalls.some((call) => call.shards.length > 0)) { + yield* TestClock.adjust(100) + } + + yield* Effect.gen(function*() { + entityState.buildLatch.closeUnsafe() + const entityFiber = yield* client.GetUserVolatile({ id: 1 }).pipe( + Effect.forkChild({ startImmediately: true }) + ) + while (entityState.layerBuilds.current === 0) { + yield* TestClock.adjust(1) + } + + storageState.blackholed = true + yield* TestClock.adjust(201) + assert.isFalse(sharding.hasShardId(shardId)) + + storageState.blackholed = false + yield* TestClock.adjust(1000) + + assert.strictEqual(storageState.releaseAllCalls.length, 1) + entityState.buildLatch.openUnsafe() + yield* TestClock.adjust(1) + yield* Fiber.interrupt(entityFiber) + }).pipe(Effect.ensuring(entityState.buildLatch.open)) + }).pipe( + Effect.provide(layer), + Effect.scoped + ) + })) + + it.effect("does not acquire shards while a forced release is pending", () => + Effect.gen(function*() { + const shardsPerGroup = 4 + const storageState = makeFailoverStorageState({ + otherRunnerHealthy: true, + releaseAllDuration: 500 + }) + const runnerStorage = Layer.effect( + RunnerStorage.RunnerStorage, + Effect.map(Clock.Clock, (clock) => makeFailoverStorage(storageState, clock)) + ) + const config = ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("localhost", 1234)), + shardsPerGroup, + shardLockExpiration: 300, + shardLockRefreshInterval: 1000, + entityTerminationTimeout: 0, + entityMessagePollInterval: 10, + refreshAssignmentsInterval: 10, + sendRetryInterval: 10 + }) + const layer = TestEntityNoState.pipe( + Layer.provideMerge(Sharding.layer), + Layer.provide(runnerStorage), + Layer.provide(RunnerHealth.layerNoop), + Layer.provideMerge(TestEntityState.layer), + Layer.provide(Runners.layerNoop), + Layer.provide([MessageStorage.layerMemory, Snowflake.layerGenerator]), + Layer.provide(config) + ) + + yield* Effect.gen(function*() { + const sharding = yield* Sharding.Sharding + const allShards = Array.makeBy(shardsPerGroup, (i) => ShardId.make("default", i + 1)) + const ownedCount = () => allShards.filter((shardId) => sharding.hasShardId(shardId)).length + + // the other runner holds part of the ring, so this runner starts with a + // strict subset of the shards + while (ownedCount() === 0) { + yield* TestClock.adjust(10) + } + assert.isBelow(ownedCount(), shardsPerGroup) + while (!storageState.refreshCalls.some((call) => call.shards.length > 0)) { + yield* TestClock.adjust(100) + } + + const acquiresBeforeOutage = storageState.acquireCalls.length + storageState.blackholed = true + yield* TestClock.adjust(201) + assert.strictEqual(ownedCount(), 0) + + // the other runner's shards are reassigned to this runner during the + // outage, so they are not part of the forced release set + storageState.otherRunnerHealthy = false + yield* TestClock.adjust(100) + assert.strictEqual(storageState.releaseAllCalls.length, 0) + + // recovery runs the forced release, which stays in flight for + // `releaseAllDuration` + storageState.blackholed = false + while (storageState.releaseAllCalls.length === 0) { + yield* TestClock.adjust(10) + } + while (!storageState.releaseAllCalls[0].completed) { + yield* TestClock.adjust(10) + } + // keep shutdown from blocking on the finalizer release + storageState.releaseAllDuration = 0 + + while (ownedCount() < shardsPerGroup) { + yield* TestClock.adjust(10) + } + + // `releaseAll` drops every lock held by this runner, so nothing may be + // acquired before it has completed + assert.strictEqual(storageState.releaseAllCalls.length, 1) + assert( + storageState.acquireCalls + .slice(acquiresBeforeOutage) + .every((call) => call.completedReleaseAlls > 0) + ) + }).pipe(Effect.provide(layer), Effect.scoped) + })) +}) + +interface FailoverStorageState { + blackholed: boolean + assignSelf: boolean + otherRunnerHealthy: boolean + /** Test clock duration `releaseAll` stays in flight for. */ + releaseAllDuration: number + runner: Runner.Runner | undefined + readonly acquireCalls: Array<{ + readonly shards: Array + readonly completedReleaseAlls: number + }> + readonly refreshCalls: Array<{ + readonly at: number + readonly shards: Array + }> + readonly releaseCalls: Array + readonly releaseAllCalls: Array<{ completed: boolean }> +} + +const makeFailoverStorageState = ( + overrides?: Partial +): FailoverStorageState => ({ + blackholed: false, + assignSelf: true, + otherRunnerHealthy: false, + releaseAllDuration: 0, + runner: undefined, + acquireCalls: [], + refreshCalls: [], + releaseCalls: [], + releaseAllCalls: [], + ...overrides +}) + +const makeFailoverStorage = (state: FailoverStorageState, clock: Clock.Clock) => + RunnerStorage.RunnerStorage.of({ + getRunners: Effect.sync(() => { + if (!state.runner) return [] + if (!state.assignSelf) return [[state.runner, false], [otherRunner, true]] + return state.otherRunnerHealthy ? [[state.runner, true], [otherRunner, true]] : [[state.runner, true]] + }), + register: (runner) => + Effect.sync(() => { + state.runner = runner + return MachineId.make(1) + }), + unregister: () => Effect.void, + setRunnerHealth: () => Effect.void, + acquire: (_address, shardIds) => + Effect.sync(() => { + const shards = globalThis.Array.from(shardIds) + state.acquireCalls.push({ + shards, + completedReleaseAlls: state.releaseAllCalls.filter((call) => call.completed).length + }) + return shards + }), + refresh: (_address, shardIds) => + Effect.suspend(() => { + const shards = globalThis.Array.from(shardIds) + state.refreshCalls.push({ + at: clock.currentTimeMillisUnsafe(), + shards + }) + return state.blackholed ? Effect.never : Effect.succeed(shards) + }), + release: (_address, shardId) => + Effect.sync(() => { + state.releaseCalls.push(shardId) + }), + releaseAll: () => + Effect.suspend(() => { + const call = { completed: false } + state.releaseAllCalls.push(call) + return Effect.andThen( + Effect.sleep(state.releaseAllDuration), + Effect.sync(() => { + call.completed = true + }) + ) + }) + }) + +const otherRunner = Runner.make({ + address: RunnerAddress.make("localhost", 5678), + groups: ["default"], + weight: 1 +}) + const TestShardingConfig = ShardingConfig.layer({ entityMailboxCapacity: 10, entityTerminationTimeout: 0, diff --git a/.context/effect/packages/effect/test/cluster/ShardingConfig.test.ts b/.context/effect/packages/effect/test/cluster/ShardingConfig.test.ts new file mode 100644 index 000000000..44ef9a893 --- /dev/null +++ b/.context/effect/packages/effect/test/cluster/ShardingConfig.test.ts @@ -0,0 +1,37 @@ +import { assert, describe, it } from "@effect/vitest" +import { ConfigProvider, Effect, Option } from "effect" +import { RunnerAddress, ShardingConfig } from "effect/unstable/cluster" + +describe("ShardingConfig", () => { + it.effect("treats the optional listen address as an atomic group", () => + Effect.gen(function*() { + const defaults = yield* ShardingConfig.config.parse(ConfigProvider.fromUnknown({})) + assert.ok(Option.isNone(defaults.runnerListenAddress)) + + const withHost = yield* ShardingConfig.config.parse( + ConfigProvider.fromUnknown({ listenHost: "0.0.0.0" }) + ) + assert.deepStrictEqual( + Option.getOrThrow(withHost.runnerListenAddress), + RunnerAddress.make("0.0.0.0", 34431) + ) + + const missingHost = yield* ShardingConfig.config.parse( + ConfigProvider.fromUnknown({ listenPort: "8080" }) + ).pipe(Effect.flip) + assert.strictEqual( + missingHost.cause.message, + `Expected string + at ["listenHost"]` + ) + + const invalidPort = yield* ShardingConfig.config.parse( + ConfigProvider.fromUnknown({ listenHost: "0.0.0.0", listenPort: "invalid" }) + ).pipe(Effect.flip) + assert.strictEqual( + invalidPort.cause.message, + `Expected a string representing a finite number + at ["listenPort"]` + ) + })) +}) diff --git a/.context/effect/packages/effect/test/cluster/TestEntity.ts b/.context/effect/packages/effect/test/cluster/TestEntity.ts index 8341f60f3..d92e6da89 100644 --- a/.context/effect/packages/effect/test/cluster/TestEntity.ts +++ b/.context/effect/packages/effect/test/cluster/TestEntity.ts @@ -1,4 +1,4 @@ -import { type Cause, Context, Effect, Layer, MutableRef, Option, Queue, Schedule, Schema, Stream } from "effect" +import { type Cause, Context, Effect, Latch, Layer, MutableRef, Option, Queue, Schedule, Schema, Stream } from "effect" import type { Envelope } from "effect/unstable/cluster" import { ClusterSchema, Entity } from "effect/unstable/cluster" import { MemoryTransaction } from "effect/unstable/cluster/MessageStorage" @@ -10,6 +10,10 @@ export class User extends Schema.Class("User")({ name: Schema.String }) {} +export class BoomError extends Schema.TaggedError()("BoomError", { + cause: Schema.Unknown +}) {} + export class StreamWithKey extends Rpc.make("StreamWithKey", { success: RpcSchema.Stream(Schema.Number, Schema.Never), payload: { key: Schema.String }, @@ -26,6 +30,7 @@ export const TestEntity = Entity.make("TestEntity", [ payload: { id: Schema.Number } }).annotate(ClusterSchema.Persisted, false), Rpc.make("Never"), + Rpc.make("Fail", { error: BoomError }), Rpc.make("NeverFork"), Rpc.make("NeverVolatile").annotate(ClusterSchema.Persisted, false), Rpc.make("RequestWithKey", { @@ -58,6 +63,7 @@ export class TestEntityState extends Context.Service()("TestEnt >() const defectTrigger = MutableRef.make(false) const layerBuilds = MutableRef.make(0) + const buildLatch = Latch.makeUnsafe(true) return { messages, @@ -65,7 +71,8 @@ export class TestEntityState extends Context.Service()("TestEnt envelopes, interrupts, defectTrigger, - layerBuilds + layerBuilds, + buildLatch } as const }) }) { @@ -77,6 +84,7 @@ export const TestEntityNoState = TestEntity.toLayer( const state = yield* TestEntityState MutableRef.update(state.layerBuilds, (count) => count + 1) + yield* state.buildLatch.await const never = (envelope: any) => Effect.suspend(() => { @@ -102,6 +110,7 @@ export const TestEntityNoState = TestEntity.toLayer( return new User({ id: envelope.payload.id, name: `User ${envelope.payload.id}` }) }), Never: never, + Fail: () => Effect.fail(new BoomError({ cause: new Error("boom") })), NeverFork: (envelope) => Rpc.fork(never(envelope)), NeverVolatile: never, RequestWithKey: (envelope) => { diff --git a/.context/effect/packages/effect/test/fixtures/migrator/0001_first.js b/.context/effect/packages/effect/test/fixtures/migrator/0001_first.js new file mode 100644 index 000000000..7eac80b41 --- /dev/null +++ b/.context/effect/packages/effect/test/fixtures/migrator/0001_first.js @@ -0,0 +1 @@ +export const marker = "loaded" diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/text.txt b/.context/effect/packages/effect/test/fixtures/text.txt similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/text.txt rename to .context/effect/packages/effect/test/fixtures/text.txt diff --git a/.context/effect/packages/effect/test/reactivity/Atom.test.ts b/.context/effect/packages/effect/test/reactivity/Atom.test.ts index 3ded384d8..2f49bd3a4 100644 --- a/.context/effect/packages/effect/test/reactivity/Atom.test.ts +++ b/.context/effect/packages/effect/test/reactivity/Atom.test.ts @@ -15,7 +15,7 @@ import { } from "effect" import { TestClock } from "effect/testing" import { KeyValueStore } from "effect/unstable/persistence" -import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity" +import { AsyncResult, Atom, AtomRegistry, Hydration } from "effect/unstable/reactivity" declare const global: any @@ -108,6 +108,49 @@ describe.sequential("Atom", () => { expect(second).toEqual(1) }) + it("withEquality skips notifications for equivalent values", () => { + const point = Atom.make({ x: 0, y: 0 }).pipe( + Atom.withEquality<{ x: number; y: number }>((a, b) => a.x === b.x && a.y === b.y), + Atom.keepAlive + ) + const r = AtomRegistry.make() + const initial = r.get(point) + let count = 0 + r.subscribe(point, () => { + count++ + }) + + r.set(point, { x: 0, y: 0 }) + expect(count).toEqual(0) + expect(r.get(point)).toBe(initial) + + r.set(point, { x: 1, y: 0 }) + expect(count).toEqual(1) + expect(r.get(point)).toEqual({ x: 1, y: 0 }) + }) + + it("withEquality skips invalidation of derived atoms", () => { + const point = Atom.make({ x: 0, y: 0 }).pipe( + Atom.withEquality<{ x: number; y: number }>((a, b) => a.x === b.x && a.y === b.y), + Atom.keepAlive + ) + let builds = 0 + const x = Atom.map(point, (p) => { + builds++ + return p.x + }) + const r = AtomRegistry.make() + r.subscribe(x, () => {}) + + expect(r.get(x)).toEqual(0) + expect(builds).toEqual(1) + r.set(point, { x: 0, y: 0 }) + expect(builds).toEqual(1) + r.set(point, { x: 2, y: 0 }) + expect(r.get(x)).toEqual(2) + expect(builds).toEqual(2) + }) + it("searchParam with schema reads initial query value", () => { const previousWindow = (globalThis as any).window const r = AtomRegistry.make() @@ -157,6 +200,106 @@ describe.sequential("Atom", () => { expect(result.value).toEqual(1) }) + it("runtime layers are disposed with their registry", () => { + interface Service { + readonly id: number + readonly isAlive: () => boolean + readonly finalize: () => void + } + const Service = Context.Service("Atom.test/RegistryScopedService") + const finalized: Array = [] + let builds = 0 + const layer = Layer.effect( + Service, + Effect.acquireRelease( + Effect.sync(() => { + const id = ++builds + let alive = true + return Service.of({ id, isAlive: () => alive, finalize: () => alive = false }) + }), + (service) => + Effect.sync(() => { + service.finalize() + finalized.push(service.id) + }) + ) + ) + const runtime = Atom.runtime(layer) + const service = runtime.atom(Service) + const registryA = AtomRegistry.make() + const registryB = AtomRegistry.make() + + const resultA = registryA.get(service) + const resultB = registryB.get(service) + assert(AsyncResult.isSuccess(resultA)) + assert(AsyncResult.isSuccess(resultB)) + expect(resultA.value.id).not.toEqual(resultB.value.id) + + registryA.dispose() + + expect(finalized).toEqual([resultA.value.id]) + expect(resultA.value.isAlive()).toEqual(false) + expect(resultB.value.isAlive()).toEqual(true) + assert(AsyncResult.isSuccess(registryB.get(service))) + + registryB.dispose() + }) + + it("default runtime factories build layers once per registry", () => { + const Service = Context.Service("Atom.test/DefaultRegistryScopedService") + let builds = 0 + const runtime = Atom.runtime(Layer.sync(Service, () => ++builds)) + const service = runtime.atom(Service) + const registryA = AtomRegistry.make() + const registryB = AtomRegistry.make() + + expect(registryA.get(service)).toEqual(AsyncResult.success(1)) + expect(registryB.get(service)).toEqual(AsyncResult.success(2)) + expect(builds).toEqual(2) + + registryA.dispose() + registryB.dispose() + }) + + it("concrete runtime memo maps share layers across registries", () => { + const Service = Context.Service("Atom.test/SharedRuntimeService") + let builds = 0 + const factory = Atom.context({ memoMap: Layer.makeMemoMapUnsafe() }) + const runtime = factory(Layer.sync(Service, () => ++builds)) + const service = runtime.atom(Service) + const registryA = AtomRegistry.make() + const registryB = AtomRegistry.make() + + expect(registryA.get(service)).toEqual(AsyncResult.success(1)) + expect(registryB.get(service)).toEqual(AsyncResult.success(1)) + expect(builds).toEqual(1) + + registryA.dispose() + registryB.dispose() + }) + + it("shared memo map atoms share within a registry and isolate across registries", () => { + const Service = Context.Service("Atom.test/SharedRegistryScopedService") + let builds = 0 + const layer = Layer.sync(Service, () => ++builds) + const memoMap = Atom.make(() => Layer.makeMemoMapUnsafe()) + const factoryA = Atom.context({ memoMap }) + const factoryB = Atom.context({ memoMap }) + const serviceA = factoryA(layer).atom(Service) + const serviceB = factoryB(layer).atom(Service) + const registryA = AtomRegistry.make() + const registryB = AtomRegistry.make() + + expect(registryA.get(serviceA)).toEqual(AsyncResult.success(1)) + expect(registryA.get(serviceB)).toEqual(AsyncResult.success(1)) + expect(registryB.get(serviceA)).toEqual(AsyncResult.success(2)) + expect(registryB.get(serviceB)).toEqual(AsyncResult.success(2)) + expect(builds).toEqual(2) + + registryA.dispose() + registryB.dispose() + }) + it("runtime replacement", async () => { const count = counterRuntime.atom(Counter.use((_) => _.get)) const r = AtomRegistry.make({ @@ -818,6 +961,64 @@ describe.sequential("Atom", () => { expect(r.get(derived)).toEqual("2b") }) + it.effect("retains method-form dependencies added during a batch rebuild", () => + Effect.gen(function*() { + const registry = AtomRegistry.make() + const source = Atom.make(Option.none()) + const gate = yield* Latch.make() + const asyncAtom = Atom.make((get) => + Effect.gen(function*() { + const value = get(source) + if (Option.isNone(value)) { + return yield* Effect.fail("SourceIsNone" as const) + } + yield* gate.await + return `computed-${value.value}` + }) + ) + const derived = Atom.make((get): unknown => { + const value = get.get(source) + if (Option.isNone(value)) { + return "empty" + } + return get.get(asyncAtom) + }) + + registry.subscribe(derived, () => {}, { immediate: true }) + registry.subscribe(asyncAtom, () => {}, { immediate: true }) + + Atom.batch(() => registry.set(source, Option.some("a"))) + + yield* gate.open + yield* Effect.yieldNow + + const result = registry.get(derived) as AsyncResult.AsyncResult + assert(AsyncResult.isSuccess(result)) + assert.strictEqual(result.value, "computed-a") + })) + + it("rebuilds an atom invalidated during its own batch rebuild", () => { + const registry = AtomRegistry.make() + const source = Atom.make(0) + const enabled = Atom.make(false) + const updateSource = Atom.make((get) => { + get.set(source, 1) + }) + const derived = Atom.make((get) => { + const value = get(source) + if (get(enabled)) { + get(updateSource) + } + return value + }) + + registry.subscribe(derived, () => {}, { immediate: true }) + + Atom.batch(() => registry.set(enabled, true)) + + assert.strictEqual(registry.get(derived), 1) + }) + it("nested batch", async () => { const r = AtomRegistry.make() const state = Atom.make(1).pipe(Atom.keepAlive) @@ -2202,6 +2403,34 @@ describe.sequential("Atom", () => { }) describe("Reactivity", () => { + it("does not broadcast mutations across registries", () => { + let reads = 0 + const query = Atom.make(() => ++reads).pipe( + Atom.withReactivity(["counter"]), + Atom.keepAlive + ) + const runtime = Atom.runtime(Layer.empty) + const mutation = runtime.fn( + Effect.fn(function*() { + }), + { reactivityKeys: ["counter"] } + ) + const registryA = AtomRegistry.make() + const registryB = AtomRegistry.make() + + expect(registryA.get(query)).toEqual(1) + expect(registryB.get(query)).toEqual(2) + + registryA.set(mutation, void 0) + + expect(reads).toEqual(3) + expect(registryA.get(query)).toEqual(3) + expect(registryB.get(query)).toEqual(2) + + registryA.dispose() + registryB.dispose() + }) + it("rebuilds on mutation", async () => { const r = AtomRegistry.make() let rebuilds = 0 @@ -2249,6 +2478,42 @@ describe.sequential("Atom", () => { assert.strictEqual(r.get(atom), 11) assert.strictEqual(rebuilds, 2) }) + + it("rebuilds on mutation with a hydrated value", async () => { + let rebuilds = 0 + let value = 0 + const atom = Atom.make(() => { + rebuilds++ + return value + }).pipe( + Atom.withReactivity(["counter"]), + Atom.serializable({ key: "hydrated-counter", schema: Schema.Number }), + Atom.keepAlive + ) + const r = AtomRegistry.make() + const fn = counterRuntime.fn( + Effect.fn(function*() { + }), + { reactivityKeys: ["counter"] } + ) + const dehydratedState: Array = [{ + "~effect/reactivity/DehydratedAtom": true, + key: "hydrated-counter", + value: 10, + dehydratedAt: 0 + }] + Hydration.hydrate(r, dehydratedState) + r.mount(atom) + + assert.strictEqual(r.get(atom), 10) + assert.strictEqual(rebuilds, 1) + + value = 11 + r.set(fn, void 0) + + assert.strictEqual(r.get(atom), 11) + assert.strictEqual(rebuilds, 2) + }) }) it("Atom.Interrupt", async () => { diff --git a/.context/effect/packages/effect/test/reactivity/AtomHttpApi.test.ts b/.context/effect/packages/effect/test/reactivity/AtomHttpApi.test.ts index f0b23e4de..b300a6f61 100644 --- a/.context/effect/packages/effect/test/reactivity/AtomHttpApi.test.ts +++ b/.context/effect/packages/effect/test/reactivity/AtomHttpApi.test.ts @@ -19,7 +19,7 @@ const Api = HttpApi.make("api").add( ) describe("AtomHttpApi", () => { - it.effect("query creates a serializable atom that encodes the request and decodes the response", () => + it.effect("query creates a serializable atom with reactivity and retention that encodes the request", () => Effect.gen(function*() { const requestRef = yield* Ref.make< { @@ -48,9 +48,38 @@ describe("AtomHttpApi", () => { const atom = Client.query("group", "get", { params: { id: 1 }, query: { page: 2 }, + reactivityKeys: ["users"], + timeToLive: "1 minute", serializationKey: `1:2` }) + assert.deepStrictEqual( + { + idleTTL: atom.idleTTL, + serializable: Atom.isSerializable(atom) + }, + { + idleTTL: 60_000, + serializable: true + } + ) + const keepAliveAtom = Client.query("group", "get", { + params: { id: 2 }, + query: { page: 3 }, + reactivityKeys: ["users"], + timeToLive: "Infinity", + serializationKey: "keep-alive" + }) + assert.deepStrictEqual( + { + keepAlive: keepAliveAtom.keepAlive, + serializable: Atom.isSerializable(keepAliveAtom) + }, + { + keepAlive: true, + serializable: true + } + ) if (!Atom.isSerializable(atom)) { assert.fail("expected query atom to be serializable") } @@ -59,6 +88,8 @@ describe("AtomHttpApi", () => { const atomFromEncodedInput = Client.query("group", "get", { params: { id: 1 }, query: { page: 2 }, + reactivityKeys: ["users"], + timeToLive: "1 minute", serializationKey: `1:2` }) if (!Atom.isSerializable(atomFromEncodedInput)) { diff --git a/.context/effect/packages/effect/test/reactivity/AtomRpc.test.ts b/.context/effect/packages/effect/test/reactivity/AtomRpc.test.ts index 03da09901..f74d31358 100644 --- a/.context/effect/packages/effect/test/reactivity/AtomRpc.test.ts +++ b/.context/effect/packages/effect/test/reactivity/AtomRpc.test.ts @@ -16,7 +16,7 @@ const Group = RpcGroup.make( ) describe("AtomRpc", () => { - it.effect("query creates a serializable atom", () => + it.effect("query creates a serializable atom with reactivity and retention", () => Effect.gen(function*() { const Client = AtomRpc.Service()("Client", { group: Group, @@ -38,9 +38,36 @@ describe("AtomRpc", () => { headers: { "x-id": "abc" }, + reactivityKeys: ["users"], + timeToLive: "1 minute", serializationKey: "1" }) + assert.deepStrictEqual( + { + idleTTL: atom.idleTTL, + serializable: Atom.isSerializable(atom) + }, + { + idleTTL: 60_000, + serializable: true + } + ) + const keepAliveAtom = Client.query("getUser", { id: 2 }, { + reactivityKeys: ["users"], + timeToLive: "Infinity", + serializationKey: "keep-alive" + }) + assert.deepStrictEqual( + { + keepAlive: keepAliveAtom.keepAlive, + serializable: Atom.isSerializable(keepAliveAtom) + }, + { + keepAlive: true, + serializable: true + } + ) if (!Atom.isSerializable(atom)) { assert.fail("expected query atom to be serializable") } @@ -50,6 +77,8 @@ describe("AtomRpc", () => { headers: { "x-id": "abc" }, + reactivityKeys: ["users"], + timeToLive: "1 minute", serializationKey: "1" }) assert(Atom.isSerializable(atomFromEncodedPayload), "expected query atom from encoded payload to be serializable") diff --git a/.context/effect/packages/effect/test/rpc/RpcClient.test.ts b/.context/effect/packages/effect/test/rpc/RpcClient.test.ts index d2c03f7cf..6e62e7bd2 100644 --- a/.context/effect/packages/effect/test/rpc/RpcClient.test.ts +++ b/.context/effect/packages/effect/test/rpc/RpcClient.test.ts @@ -1,9 +1,15 @@ import { assert, describe, it } from "@effect/vitest" -import { Cause, Effect, Layer, Schema, Stream } from "effect" +import { Cause, Deferred, Effect, Fiber, Layer, Schedule, Schema, Stream } from "effect" +import { TestClock } from "effect/testing" import * as HttpClient from "effect/unstable/http/HttpClient" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" import { Rpc, RpcClient, RpcGroup, RpcMessage, RpcSchema, RpcSerialization } from "effect/unstable/rpc" import { RpcClientError } from "effect/unstable/rpc/RpcClientError" +import * as Socket from "effect/unstable/socket/Socket" +import * as Worker from "effect/unstable/workers/Worker" +import { WorkerError, WorkerReceiveError } from "effect/unstable/workers/WorkerError" +import { vi } from "vitest" +import type * as RpcClientErrorModule from "../../src/unstable/rpc/RpcClientError.ts" const TestGroup = RpcGroup.make( Rpc.make("Ping", { success: Schema.String }), @@ -20,15 +26,20 @@ const makeHttpClient = (body: string): HttpClient.HttpClient => ) ) -const makeProtocolLayer = ( +const makeProtocolLayerWithClient = ( serializationLayer: Layer.Layer, - body: string + client: HttpClient.HttpClient ) => RpcClient.layerProtocolHttp({ url: "http://localhost/rpc" }).pipe( Layer.provideMerge(serializationLayer), - Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, makeHttpClient(body))) + Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, client)) ) +const makeProtocolLayer = ( + serializationLayer: Layer.Layer, + body: string +) => makeProtocolLayerWithClient(serializationLayer, makeHttpClient(body)) + const assertEmptyResponseFailsRequest = ( serializationLayer: Layer.Layer, body: string @@ -51,6 +62,103 @@ const assertEmptyResponseFailsRequest = ( }) describe("RpcClient", () => { + it.effect("releases a worker pool slot when the worker run fails", () => + Effect.gen(function*() { + const runFailure = yield* Deferred.make() + const firstRequestSent = yield* Deferred.make() + const secondRequestSent = yield* Deferred.make() + const protocolErrorReceived = yield* Deferred.make() + const sentRequestIds: Array = [] + let runCount = 0 + const backing: Worker.Worker = { + send(message) { + return Effect.sync(() => { + if (message._tag !== "Request") return + sentRequestIds.push(message.id) + }).pipe( + Effect.andThen( + message._tag === "Request" && message.id === 1 + ? Deferred.succeed(firstRequestSent, void 0) + : message._tag === "Request" && message.id === 2 + ? Deferred.succeed(secondRequestSent, void 0) + : Effect.void + ) + ) + }, + run() { + return runCount++ === 0 ? Deferred.await(runFailure) : Effect.never + } + } + const workerPlatform = Worker.WorkerPlatform.of({ + spawn: () => Effect.succeed(backing) + }) + const protocol = yield* RpcClient.makeProtocolWorker({ size: 1, concurrency: 1 }).pipe( + Effect.provideService(Worker.WorkerPlatform, workerPlatform), + Effect.provideService(Worker.Spawner, (() => undefined) as Worker.SpawnerFn) + ) + yield* protocol.run(0, (response) => + response._tag === "ClientProtocolError" + ? Deferred.succeed(protocolErrorReceived, void 0) + : Effect.void).pipe(Effect.forkScoped) + + const request = (id: number) => ({ + _tag: "Request" as const, + id, + tag: "Test", + payload: null, + headers: [] + }) + const first = yield* protocol.send(0, request(1)).pipe(Effect.forkChild) + yield* Deferred.await(firstRequestSent) + yield* Deferred.fail( + runFailure, + new WorkerError({ reason: new WorkerReceiveError({ message: "worker exited" }) }) + ) + yield* Deferred.await(protocolErrorReceived) + + const firstCompleted = yield* Fiber.join(first).pipe( + Effect.timeout("1 second"), + Effect.forkChild + ) + yield* TestClock.adjust("1 second") + yield* Fiber.join(firstCompleted) + + const second = yield* protocol.send(0, request(2)).pipe(Effect.forkChild) + const secondSent = yield* Deferred.await(secondRequestSent).pipe( + Effect.timeout("1 second"), + Effect.forkChild + ) + yield* TestClock.adjust("1 second") + yield* Fiber.join(secondSent) + yield* Fiber.interrupt(second) + assert.deepStrictEqual(sentRequestIds, [1, 2]) + })) + + it("preserves RpcClientError failures from a reloaded module copy", async () => { + vi.resetModules() + const ForeignRpcClientError = await vi.importActual( + "../../src/unstable/rpc/RpcClientError.ts" + ) + const rpcClientError = new ForeignRpcClientError.RpcClientError({ + reason: new ForeignRpcClientError.RpcClientDefect({ message: "boom", cause: undefined }) + }) + assert.isFalse(rpcClientError instanceof RpcClientError) + + const httpClient = HttpClient.make((request) => { + const response = HttpClientResponse.fromWeb(request, new Response("", { status: 200 })) + Object.defineProperty(response, "stream", { value: Stream.fail(rpcClientError) }) + return Effect.succeed(response) + }) + const error = await Effect.gen(function*() { + const client = yield* RpcClient.make(TestGroup).pipe( + Effect.provide(makeProtocolLayerWithClient(RpcSerialization.layerNdjson, httpClient)) + ) + return yield* client.Ping().pipe(Effect.flip) + }).pipe(Effect.scoped, Effect.runPromise) + + assert.strictEqual(error, rpcClientError) + }) + it.effect("fails request on empty HTTP response for unframed serialization", () => assertEmptyResponseFailsRequest(RpcSerialization.layerJson, "[]")) @@ -80,4 +188,127 @@ describe("RpcClient", () => { assert.strictEqual(error.reason._tag, "RpcClientDefect") assert.strictEqual(error.reason.message, "HTTP response ended before RPC request completed") })) + + it.effect("reports transient socket open errors without failing in-flight streams", () => + Effect.gen(function*() { + const requestSent = yield* Deferred.make() + const threeErrors = yield* Deferred.make() + const errors: Array = [] + const socketError = new Socket.SocketError({ + reason: new Socket.SocketOpenError({ + kind: "Unknown", + cause: new Error("connection refused") + }) + }) + const socket = Socket.make({ + runRaw: () => Deferred.await(requestSent).pipe(Effect.andThen(Effect.fail(socketError))), + writer: Effect.succeed(() => Deferred.succeed(requestSent, void 0)) + }) + const protocol = yield* RpcClient.makeProtocolSocket({ + retryTransientErrors: true, + retryPolicy: Schedule.spaced("1 millis"), + onTransientError: (error) => + Effect.suspend(() => { + errors.push(error) + return errors.length === 3 ? Deferred.succeed(threeErrors, void 0) : Effect.void + }) + }).pipe( + Effect.provideService(Socket.Socket, socket), + Effect.provide(RpcSerialization.layerNdjson) + ) + const client = yield* RpcClient.make(TestGroup).pipe( + Effect.provideService(RpcClient.Protocol, protocol) + ) + const streamFiber = yield* client.Events().pipe(Stream.runDrain, Effect.forkChild) + + yield* TestClock.adjust("2 millis") + yield* Deferred.await(threeErrors).pipe(Effect.timeout("1 second")) + + assert.lengthOf(errors, 3) + for (const error of errors) { + assert.strictEqual(error.reason._tag, "SocketOpenError") + } + assert.isUndefined(streamFiber.pollUnsafe()) + })) + + it.effect("fails in-flight streams when transient retries are exhausted", () => + Effect.gen(function*() { + const requestSent = yield* Deferred.make() + const socketError = new Socket.SocketError({ + reason: new Socket.SocketOpenError({ + kind: "Unknown", + cause: new Error("connection refused") + }) + }) + const socket = Socket.make({ + runRaw: () => Deferred.await(requestSent).pipe(Effect.andThen(Effect.fail(socketError))), + writer: Effect.succeed(() => Deferred.succeed(requestSent, void 0)) + }) + const protocol = yield* RpcClient.makeProtocolSocket({ + retryTransientErrors: true, + retryPolicy: Schedule.recurs(2) + }).pipe( + Effect.provideService(Socket.Socket, socket), + Effect.provide(RpcSerialization.layerNdjson) + ) + const client = yield* RpcClient.make(TestGroup).pipe( + Effect.provideService(RpcClient.Protocol, protocol) + ) + const streamFiber = yield* client.Events().pipe( + Stream.runDrain, + Effect.timeout("1 second"), + Effect.flip, + Effect.forkChild + ) + + yield* TestClock.adjust("1 second") + const error = yield* Fiber.join(streamFiber) + + assert.instanceOf(error, RpcClientError) + assert.strictEqual(error.reason._tag, "SocketOpenError") + })) + + it.effect("continues retrying when the transient error hook defects", () => + Effect.gen(function*() { + const requestSent = yield* Deferred.make() + let attempts = 0 + const socketError = new Socket.SocketError({ + reason: new Socket.SocketOpenError({ + kind: "Unknown", + cause: new Error("connection refused") + }) + }) + const socket = Socket.make({ + runRaw: () => + Deferred.await(requestSent).pipe( + Effect.tap(() => Effect.sync(() => attempts++)), + Effect.andThen(Effect.fail(socketError)) + ), + writer: Effect.succeed(() => Deferred.succeed(requestSent, void 0)) + }) + const protocol = yield* RpcClient.makeProtocolSocket({ + retryTransientErrors: true, + retryPolicy: Schedule.recurs(2), + onTransientError: () => Effect.die("hook defect") + }).pipe( + Effect.provideService(Socket.Socket, socket), + Effect.provide(RpcSerialization.layerNdjson) + ) + const client = yield* RpcClient.make(TestGroup).pipe( + Effect.provideService(RpcClient.Protocol, protocol) + ) + const streamFiber = yield* client.Events().pipe( + Stream.runDrain, + Effect.timeout("1 second"), + Effect.flip, + Effect.forkChild + ) + + yield* TestClock.adjust("1 second") + const error = yield* Fiber.join(streamFiber) + + assert.strictEqual(attempts, 3) + assert.instanceOf(error, RpcClientError) + assert.strictEqual(error.reason._tag, "SocketOpenError") + })) }) diff --git a/.context/effect/packages/effect/test/rpc/RpcSerialization.test.ts b/.context/effect/packages/effect/test/rpc/RpcSerialization.test.ts index ecffd75dc..831d661a2 100644 --- a/.context/effect/packages/effect/test/rpc/RpcSerialization.test.ts +++ b/.context/effect/packages/effect/test/rpc/RpcSerialization.test.ts @@ -1,4 +1,5 @@ -import { assert, describe, it } from "@effect/vitest" +import { afterEach, assert, describe, it } from "@effect/vitest" +import { Effect } from "effect" import { RpcSerialization } from "effect/unstable/rpc" const responseExitSuccess = (requestId: string | number, value: unknown) => ({ @@ -10,7 +11,70 @@ const responseExitSuccess = (requestId: string | number, value: unknown) => ({ } }) +const objectPrototype = Object.prototype as Record + +const polluteObjectPrototype = (key: string, value: unknown) => { + Object.defineProperty(objectPrototype, key, { + configurable: true, + value + }) +} + +const decodeJsonRpcSuccess = () => + RpcSerialization.jsonRpc().makeUnsafe().decode("{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"ok\"}") + +const expectedJsonRpcSuccess = [{ + _tag: "Exit", + requestId: 1, + exit: { + _tag: "Success", + value: "ok" + } +}] + +const assertMaxBufferSizeExceeded = (f: () => unknown, maxBufferSize: number) => { + try { + f() + assert.fail("Expected MaxBufferSizeExceeded") + } catch (error) { + assert.instanceOf(error, RpcSerialization.MaxBufferSizeExceeded) + assert.strictEqual(error.maxBufferSize, maxBufferSize) + } +} + describe("RpcSerialization", () => { + describe.sequential("jsonRpc inherited properties", () => { + afterEach(() => { + delete objectPrototype["method"] + delete objectPrototype["error"] + delete objectPrototype["chunk"] + }) + + it("decodes a success response with a clean prototype", () => { + assert.deepStrictEqual(decodeJsonRpcSuccess(), expectedJsonRpcSuccess) + }) + + it("ignores an inherited method", () => { + polluteObjectPrototype("method", "attacker.evil") + assert.deepStrictEqual(decodeJsonRpcSuccess(), expectedJsonRpcSuccess) + }) + + it("ignores an inherited defect error", () => { + polluteObjectPrototype("error", { _tag: "Defect", data: "pwn" }) + assert.deepStrictEqual(decodeJsonRpcSuccess(), expectedJsonRpcSuccess) + }) + + it("ignores an inherited chunk marker", () => { + polluteObjectPrototype("chunk", true) + assert.deepStrictEqual(decodeJsonRpcSuccess(), expectedJsonRpcSuccess) + }) + + it("ignores an inherited exit error", () => { + polluteObjectPrototype("error", { _tag: "Cause", data: [] }) + assert.deepStrictEqual(decodeJsonRpcSuccess(), expectedJsonRpcSuccess) + }) + }) + it("json decode keeps array payloads flat", () => { const parser = RpcSerialization.json.makeUnsafe() const decoded = parser.decode("[1,2,3]") @@ -25,6 +89,52 @@ describe("RpcSerialization", () => { assert.deepStrictEqual(decoded, [{ a: 1 }]) }) + it("ndjson fails when an unterminated frame exceeds maxBufferSize", () => { + const parser = RpcSerialization.makeNdjson({ maxBufferSize: 4 }).makeUnsafe() + + assert.deepStrictEqual(parser.decode("12"), []) + assert.deepStrictEqual(parser.decode("34"), []) + assertMaxBufferSizeExceeded(() => parser.decode("5"), 4) + }) + + it("ndjson allows an unbounded incomplete frame", () => { + const parser = RpcSerialization.makeNdjson({ maxBufferSize: "unbounded" }).makeUnsafe() + + assert.deepStrictEqual(parser.decode("x".repeat(1024)), []) + }) + + it("ndjson decodes a multibyte character split across byte chunks", () => { + const parser = RpcSerialization.ndjson.makeUnsafe() + const message = { value: "\u20ac" } + const encoded = parser.encode(message) + assert(typeof encoded === "string") + const bytes = new TextEncoder().encode(encoded) + const split = bytes.indexOf(0xe2) + 1 + + assert.deepStrictEqual(parser.decode(bytes.slice(0, split)), []) + assert.deepStrictEqual(parser.decode(bytes.slice(split)), [message]) + }) + + it.effect("layerNdjsonWith forwards maxBufferSize to its decoder", () => + Effect.gen(function*() { + const serialization = yield* RpcSerialization.RpcSerialization + const parser = serialization.makeUnsafe() + + assert.deepStrictEqual(parser.decode("12"), []) + assert.deepStrictEqual(parser.decode("34"), []) + assertMaxBufferSizeExceeded(() => parser.decode("5"), 4) + }).pipe( + Effect.provide(RpcSerialization.layerNdjsonWith({ maxBufferSize: 4 })) + )) + + it("ndJsonRpc forwards maxBufferSize to its ndjson framing parser", () => { + const parser = RpcSerialization.ndJsonRpc({ maxBufferSize: 4 }).makeUnsafe() + + assert.deepStrictEqual(parser.decode("12"), []) + assert.deepStrictEqual(parser.decode("34"), []) + assert.throws(() => parser.decode("5"), RpcSerialization.MaxBufferSizeExceeded) + }) + it("jsonRpc encodes a non-batched single response array as an object", () => { const parser = RpcSerialization.jsonRpc().makeUnsafe() const decoded = parser.decode("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"users.get\"}") @@ -169,4 +279,35 @@ describe("RpcSerialization", () => { assert.strictEqual(decoded.length, 1) assert.deepStrictEqual(decoded[0], payload) }) + + it("makeMsgPack fails when incomplete frames exceed maxBufferSize", () => { + const parser = RpcSerialization.makeMsgPack({ maxBufferSize: 2 }).makeUnsafe() + const incompleteFrame = Uint8Array.of(0xd9) + + assert.deepStrictEqual(parser.decode(incompleteFrame), []) + assert.deepStrictEqual(parser.decode(incompleteFrame), []) + assertMaxBufferSizeExceeded(() => parser.decode(incompleteFrame), 2) + }) + + it("makeMsgPack allows an unbounded incomplete frame", () => { + const parser = RpcSerialization.makeMsgPack({ maxBufferSize: "unbounded" }).makeUnsafe() + const incompleteFrame = Uint8Array.of(0xd9) + + for (let i = 0; i < 20; i++) { + assert.deepStrictEqual(parser.decode(incompleteFrame), []) + } + }) + + it.effect("layerMsgPackWith forwards maxBufferSize to its decoder", () => + Effect.gen(function*() { + const serialization = yield* RpcSerialization.RpcSerialization + const parser = serialization.makeUnsafe() + const incompleteFrame = Uint8Array.of(0xd9) + + assert.deepStrictEqual(parser.decode(incompleteFrame), []) + assert.deepStrictEqual(parser.decode(incompleteFrame), []) + assertMaxBufferSizeExceeded(() => parser.decode(incompleteFrame), 2) + }).pipe( + Effect.provide(RpcSerialization.layerMsgPackWith({ maxBufferSize: 2 })) + )) }) diff --git a/.context/effect/packages/effect/test/rpc/RpcServer.test.ts b/.context/effect/packages/effect/test/rpc/RpcServer.test.ts new file mode 100644 index 000000000..e3a40b7b3 --- /dev/null +++ b/.context/effect/packages/effect/test/rpc/RpcServer.test.ts @@ -0,0 +1,60 @@ +import { assert, describe, it } from "@effect/vitest" +import { Deferred, Effect, Layer } from "effect" +import { RpcSerialization, RpcServer } from "effect/unstable/rpc" +import { Socket, SocketServer } from "effect/unstable/socket" + +describe("RpcServer", () => { + it.effect("closes a socket when the serialization buffer limit is exceeded", () => + Effect.gen(function*() { + const handledChunks: Array = [] + const writes: Array = [] + const completed = yield* Deferred.make() + let closed = false + + const socket = Socket.make({ + runRaw: (handler) => + Effect.gen(function*() { + for (const chunk of ["12", "34", "5", "{\"_tag\":\"Ping\"}\n"]) { + if (closed) break + handledChunks.push(chunk) + const result = handler(chunk) + if (Effect.isEffect(result)) { + yield* result + } + } + }).pipe(Effect.ensuring(Deferred.succeed(completed, void 0))), + writer: Effect.succeed((chunk) => + Effect.sync(() => { + writes.push(chunk) + if (Socket.isCloseEvent(chunk)) { + closed = true + } + }) + ) + }) + const socketServer = SocketServer.SocketServer.of({ + address: { + _tag: "TcpAddress", + hostname: "localhost", + port: 0 + }, + run: (handler) => handler(socket).pipe(Effect.orDie, Effect.andThen(Effect.never)) + }) + + const protocol = yield* RpcServer.makeProtocolSocketServer.pipe( + Effect.provide(Layer.succeed(SocketServer.SocketServer, socketServer)), + Effect.provide(Layer.succeed( + RpcSerialization.RpcSerialization, + RpcSerialization.makeNdjson({ maxBufferSize: 4 }) + )) + ) + yield* Effect.forkScoped(protocol.run(() => Effect.void)) + yield* Deferred.await(completed) + + assert.deepStrictEqual(handledChunks, ["12", "34", "5"]) + assert.strictEqual(writes.length, 1) + const closeEvent = writes[0] + assert(Socket.isCloseEvent(closeEvent)) + assert.strictEqual(closeEvent.code, 1009) + })) +}) diff --git a/.context/effect/packages/effect/test/schema/HMR.test.ts b/.context/effect/packages/effect/test/schema/HMR.test.ts index 88600120f..a06da4215 100644 --- a/.context/effect/packages/effect/test/schema/HMR.test.ts +++ b/.context/effect/packages/effect/test/schema/HMR.test.ts @@ -68,5 +68,8 @@ describe("HMR", () => { expect(b instanceof mod1.A).toBe(false) expect(String(schema.encodeUnknownExit(mod1.A)(b))).toBe(`Success({"a":"a"})`) + + const nested = schema.Struct({ rows: schema.Array(mod1.A) }) + expect(nested.make({ rows: [b] }).rows[0]).toBe(b) }) }) diff --git a/.context/effect/packages/effect/test/schema/Schema.test.ts b/.context/effect/packages/effect/test/schema/Schema.test.ts index 4829e50bc..1c3bd9abd 100644 --- a/.context/effect/packages/effect/test/schema/Schema.test.ts +++ b/.context/effect/packages/effect/test/schema/Schema.test.ts @@ -33,12 +33,13 @@ import { } from "effect" import { TestSchema } from "effect/testing" import { produce } from "immer" -import { deepStrictEqual, fail, ok, strictEqual } from "node:assert" -import { assertFalse, assertInclude, assertTrue, throws } from "../utils/assert.ts" +import { deepStrictEqual, fail, strictEqual } from "node:assert" +import { assertFalse, assertInclude, assertSchemaIssueError, assertTrue, throws } from "../utils/assert.ts" const verifyGeneration = true const equals = TestSchema.Asserts.ast.fields.equals +const formatIssue = SchemaIssue.makeFormatterDefault() const SnakeToCamel = Schema.String.pipe( Schema.decode( @@ -62,7 +63,7 @@ describe("Schema", () => { const schema = Schema.String const result = Schema.decodeUnknownExit(schema)(null) assertTrue(Exit.isFailure(result)) - strictEqual(String(result.cause.reasons[0]), "Fail(SchemaError(Expected string, got null))") + strictEqual(String(result.cause.reasons[0]), "Fail(SchemaError(Expected string))") }) describe("SchemaError", () => { @@ -73,11 +74,12 @@ describe("Schema", () => { assertTrue(error instanceof Error) assertTrue(Schema.isSchemaError(error)) + assertFalse(Schema.isSchemaError({ "~effect/SchemaError/SchemaError": false })) strictEqual(error._tag, "SchemaError") strictEqual(error.name, "SchemaError") strictEqual(error.issue, result.failure) - strictEqual(error.message, "Expected string, got null") - strictEqual(String(error), "SchemaError(Expected string, got null)") + strictEqual(error.message, "Expected string") + strictEqual(String(error), "SchemaError(Expected string)") }) }) @@ -91,8 +93,8 @@ describe("Schema", () => { const decoding = asserts.decoding() await decoding.fail( -1.2, - `Expected a value greater than 0, got -1.2 -Expected an integer, got -1.2` + `Expected a value greater than 0 +Expected an integer` ) }) @@ -198,15 +200,15 @@ Missing key const make = asserts.make() await make.succeed("a") - await make.fail(null, `Expected "a", got null`) + await make.fail(null, `Expected "a"`) const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(1, `Expected "a", got 1`) + await decoding.fail(1, `Expected "a"`) const encoding = asserts.encoding() await encoding.succeed("a") - await encoding.fail(1, `Expected "a", got 1`) + await encoding.fail(1, `Expected "a"`) }) it(`1`, async () => { @@ -215,15 +217,15 @@ Missing key const make = asserts.make() await make.succeed(1) - await make.fail(null, `Expected 1, got null`) + await make.fail(null, `Expected 1`) const decoding = asserts.decoding() await decoding.succeed(1) - await decoding.fail("1", `Expected 1, got "1"`) + await decoding.fail("1", `Expected 1`) const encoding = asserts.encoding() await encoding.succeed(1) - await encoding.fail("1", `Expected 1, got "1"`) + await encoding.fail("1", `Expected 1`) }) it("transform", async () => { @@ -232,11 +234,11 @@ Missing key const decoding = asserts.decoding() await decoding.succeed(0, "a") - await decoding.fail(1, `Expected 0, got 1`) + await decoding.fail(1, `Expected 0`) const encoding = asserts.encoding() await encoding.succeed("a", 0) - await encoding.fail("b", `Expected "a", got "b"`) + await encoding.fail("b", `Expected "a"`) }) }) @@ -251,7 +253,7 @@ Missing key await make.succeed("red") await make.succeed("green") await make.succeed("blue") - await make.fail("yellow", `Expected "red" | "green" | "blue", got "yellow"`) + await make.fail("yellow", `Expected "red" | "green" | "blue"`) }) it("transform", async () => { @@ -261,12 +263,12 @@ Missing key const decoding = asserts.decoding() await decoding.succeed(0, "a") await decoding.succeed(1, "b") - await decoding.fail(2, `Expected 0 | 1, got 2`) + await decoding.fail(2, `Expected 0 | 1`) const encoding = asserts.encoding() await encoding.succeed("a", 0) await encoding.succeed("b", 1) - await encoding.fail("c", `Expected "a" | "b", got "c"`) + await encoding.fail("c", `Expected "a" | "b"`) }) it("pick", () => { @@ -281,13 +283,13 @@ Missing key const asserts = new TestSchema.Asserts(schema) const make = asserts.make() - await make.fail(null as never, `Expected never, got null`) + await make.fail(null as never, `Expected never`) const decoding = asserts.decoding() - await decoding.fail("a", `Expected never, got "a"`) + await decoding.fail("a", `Expected never`) const encoding = asserts.encoding() - await encoding.fail("a", `Expected never, got "a"`) + await encoding.fail("a", `Expected never`) }) it("Any", async () => { @@ -318,7 +320,7 @@ Missing key const make = asserts.make() await make.succeed(null) - await make.fail(undefined, `Expected null, got undefined`) + await make.fail(undefined, `Expected null`) }) it("Undefined", async () => { @@ -327,7 +329,7 @@ Missing key const make = asserts.make() await make.succeed(undefined) - await make.fail(null, `Expected undefined, got null`) + await make.fail(null, `Expected undefined`) }) it("String", async () => { @@ -336,15 +338,15 @@ Missing key const make = asserts.make() await make.succeed("a") - await make.fail(null, `Expected string, got null`) + await make.fail(null, `Expected string`) const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(1, `Expected string, got 1`) + await decoding.fail(1, `Expected string`) const encoding = asserts.encoding() await encoding.succeed("a") - await encoding.fail(1, `Expected string, got 1`) + await encoding.fail(1, `Expected string`) }) it("Number", async () => { @@ -353,15 +355,15 @@ Missing key const make = asserts.make() await make.succeed(1) - await make.fail(null, `Expected number, got null`) + await make.fail(null, `Expected number`) const decoding = asserts.decoding() await decoding.succeed(1) - await decoding.fail("a", `Expected number, got "a"`) + await decoding.fail("a", `Expected number`) const encoding = asserts.encoding() await encoding.succeed(1) - await encoding.fail("a", `Expected number, got "a"`) + await encoding.fail("a", `Expected number`) }) it("Boolean", async () => { @@ -371,17 +373,17 @@ Missing key const make = asserts.make() await make.succeed(true) await make.succeed(false) - await make.fail(null, `Expected boolean, got null`) + await make.fail(null, `Expected boolean`) const decoding = asserts.decoding() await decoding.succeed(true) await decoding.succeed(false) - await decoding.fail("a", `Expected boolean, got "a"`) + await decoding.fail("a", `Expected boolean`) const encoding = asserts.encoding() await encoding.succeed(true) await encoding.succeed(false) - await encoding.fail("a", `Expected boolean, got "a"`) + await encoding.fail("a", `Expected boolean`) }) it("Symbol", async () => { @@ -390,15 +392,15 @@ Missing key const make = asserts.make() await make.succeed(Symbol("a")) - await make.fail(null, `Expected symbol, got null`) + await make.fail(null, `Expected symbol`) const decoding = asserts.decoding() await decoding.succeed(Symbol("a")) - await decoding.fail("a", `Expected symbol, got "a"`) + await decoding.fail("a", `Expected symbol`) const encoding = asserts.encoding() await encoding.succeed(Symbol("a")) - await encoding.fail("a", `Expected symbol, got "a"`) + await encoding.fail("a", `Expected symbol`) }) it("UniqueSymbol", async () => { @@ -408,11 +410,11 @@ Missing key const make = asserts.make() await make.succeed(a) - await make.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`) + await make.fail(Symbol("b"), `Expected Symbol(a)`) const decoding = asserts.decoding() await decoding.succeed(a) - await decoding.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`) + await decoding.fail(Symbol("b"), `Expected Symbol(a)`) }) it("BigInt", async () => { @@ -421,15 +423,15 @@ Missing key const make = asserts.make() await make.succeed(1n) - await make.fail(null, `Expected bigint, got null`) + await make.fail(null, `Expected bigint`) const decoding = asserts.decoding() await decoding.succeed(1n) - await decoding.fail("1", `Expected bigint, got "1"`) + await decoding.fail("1", `Expected bigint`) const encoding = asserts.encoding() await encoding.succeed(1n) - await encoding.fail("1", `Expected bigint, got "1"`) + await encoding.fail("1", `Expected bigint`) }) it("Void", async () => { @@ -467,22 +469,24 @@ Missing key const make = asserts.make() await make.succeed({}) await make.succeed([]) - await make.fail(null, `Expected object | array | function, got null`) + await make.fail(null, `Expected object | array | function`) const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed([]) - await decoding.fail("1", `Expected object | array | function, got "1"`) + await decoding.fail("1", `Expected object | array | function`) const encoding = asserts.encoding() await encoding.succeed({}) await encoding.succeed([]) - await encoding.fail("1", `Expected object | array | function, got "1"`) + await encoding.fail("1", `Expected object | array | function`) }) it("optionalKey", () => { const schema = Schema.optionalKey(Schema.String) strictEqual(schema.ast.context?.isOptional, true) + strictEqual(Schema.optionalKey(Schema.String).ast, schema.ast) + strictEqual(Schema.optionalKey(schema).ast, schema.ast) }) it("optionalKey & mutableKey", () => { @@ -492,13 +496,18 @@ Missing key }) it("optional", () => { - const schema = Schema.optionalKey(Schema.String) + const schema = Schema.optional(Schema.String) strictEqual(schema.ast.context?.isOptional, true) + strictEqual(Schema.optional(Schema.String).ast, schema.ast) + const nested = Schema.optional(schema) + strictEqual(Schema.required(nested), schema) }) it("mutableKey", () => { const schema = Schema.mutableKey(Schema.String) strictEqual(schema.ast.context?.isMutable, true) + strictEqual(Schema.mutableKey(Schema.String).ast, schema.ast) + strictEqual(Schema.mutableKey(schema).ast, schema.ast) }) it("mutableKey & optionalKey", () => { @@ -572,22 +581,22 @@ Missing key const decoding = asserts.decoding({ parseOptions: { onExcessProperty: "error" } }) await decoding.fail( { a: "a", b: "b" }, - `Unexpected key with value "b" + `Expected no excess property at ["b"]` ) const sym = Symbol("sym") await decoding.fail( { a: "a", [sym]: "sym" }, - `Unexpected key with value "sym" + `Expected no excess property at [Symbol(sym)]` ) const decodingAll = asserts.decoding({ parseOptions: { onExcessProperty: "error", errors: "all" } }) await decodingAll.fail( { a: "a", b: "b", c: "c" }, - `Unexpected key with value "b" + `Expected no excess property at ["b"] -Unexpected key with value "c" +Expected no excess property at ["c"]` ) }) @@ -632,7 +641,7 @@ Unexpected key with value "c" const make = asserts.make() await make.succeed({ a: "a" }) - await make.fail(null, `Expected object, got null`) + await make.fail(null, `Expected object`) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) @@ -643,7 +652,7 @@ Unexpected key with value "c" ) await decoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) @@ -656,7 +665,7 @@ Unexpected key with value "c" ) await encoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) }) @@ -671,7 +680,7 @@ Unexpected key with value "c" await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.fail( { a: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["a"]` ) @@ -679,7 +688,7 @@ Unexpected key with value "c" await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.fail( { a: "a" }, - `Expected number, got "a" + `Expected number at ["a"]` ) }) @@ -691,7 +700,7 @@ Unexpected key with value "c" const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() - await decoding.fail(null, `Expected ID, got null`) + await decoding.fail(null, `Expected ID`) }) it(`Schema.optionalKey: { readonly "a"?: string }`, async () => { @@ -709,7 +718,7 @@ Unexpected key with value "c" await decoding.succeed({}) await decoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) @@ -718,7 +727,7 @@ Unexpected key with value "c" await encoding.succeed({}) await encoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) }) @@ -740,7 +749,7 @@ Unexpected key with value "c" await decoding.succeed({}) await decoding.fail( { a: 1 }, - `Expected string | undefined, got 1 + `Expected string | undefined at ["a"]` ) @@ -750,7 +759,7 @@ Unexpected key with value "c" await encoding.succeed({}) await encoding.fail( { a: 1 }, - `Expected string | undefined, got 1 + `Expected string | undefined at ["a"]` ) }) @@ -766,7 +775,7 @@ Unexpected key with value "c" await decoding.succeed({}) await decoding.fail( { a: undefined }, - `Expected string, got undefined + `Expected string at ["a"]` ) @@ -846,7 +855,7 @@ Missing key await decoding.succeed({ a: "a", b: 1, c: 2 }) await decoding.fail( { a: "a", b: "b" }, - `Expected number, got "b" + `Expected number at ["b"]` ) }) @@ -865,7 +874,7 @@ Missing key await decoding.succeed({ a: "a", b: "a", c: "c" }) await decoding.fail( { a: "", b: "b", c: "c" }, - `Expected a === b, got {"a":"","b":"b","c":"c"}` + `Expected a === b` ) }) }) @@ -916,15 +925,15 @@ Missing key const decoding = asserts.decoding() await decoding.fail( ["a", "b"], - `Unexpected key with value "b" + `Expected no excess property at [1]` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( ["a", "b", "c"], - `Unexpected key with value "b" + `Expected no excess property at [1] -Unexpected key with value "c" +Expected no excess property at [2]` ) }) @@ -940,13 +949,13 @@ Unexpected key with value "c" await make.succeed(["a"]) await make.fail( [""], - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at [0]` ) const decoding = asserts.decoding() await decoding.succeed(["a"]) - await decoding.fail(null, `Expected array, got null`) + await decoding.fail(null, `Expected array`) await decoding.fail( [], `Missing key @@ -954,7 +963,7 @@ Unexpected key with value "c" ) await decoding.fail( [1], - `Expected string, got 1 + `Expected string at [0]` ) @@ -967,7 +976,7 @@ Unexpected key with value "c" ) await encoding.fail( [1], - `Expected string, got 1 + `Expected string at [0]` ) }) @@ -1008,7 +1017,7 @@ Unexpected key with value "c" await decoding.succeed(["a", "b"]) await decoding.fail( ["a", 1], - `Expected string, got 1 + `Expected string at [1]` ) @@ -1016,7 +1025,7 @@ Unexpected key with value "c" await encoding.succeed(["a", "b"]) await encoding.fail( ["a", 1], - `Expected string, got 1 + `Expected string at [1]` ) }) @@ -1030,11 +1039,11 @@ Unexpected key with value "c" await decoding.succeed("1", [1]) await decoding.succeed(["1", "2"], [1, 2]) await decoding.succeed([], []) - await decoding.fail(null, `Expected string | array, got null`) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail(null, `Expected string | array`) + await decoding.fail("a", `Expected a finite number`) await decoding.fail( ["a"], - `Expected a finite number, got NaN + `Expected a finite number at [0]` ) @@ -1068,7 +1077,7 @@ Unexpected key with value "c" ) await decoding.fail( ["a", 1], - `Expected string, got 1 + `Expected string at [1]` ) @@ -1082,7 +1091,7 @@ Unexpected key with value "c" ) await encoding.fail( ["a", 1], - `Expected string, got 1 + `Expected string at [1]` ) }) @@ -1100,7 +1109,7 @@ Unexpected key with value "c" await decoding.succeed("a") await decoding.fail( " a ", - `Expected a string with no leading or trailing whitespace, got " a "` + `Expected a string with no leading or trailing whitespace` ) }) @@ -1114,7 +1123,7 @@ Unexpected key with value "c" await decoding.succeed("abc") await decoding.fail( "ab", - `Expected a value with a length of at least 3, got "ab"` + `Expected a value with a length of at least 3` ) }) @@ -1129,13 +1138,13 @@ Unexpected key with value "c" await decoding.succeed("abc") await decoding.fail( "ab", - `Expected a value with a length of at least 3, got "ab"` + `Expected a value with a length of at least 3` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( "ab", - `Expected a value with a length of at least 3, got "ab" -Expected a string including "c", got "ab"` + `Expected a value with a length of at least 3 +Expected a string including "c"` ) }) @@ -1149,7 +1158,7 @@ Expected a string including "c", got "ab"` const decoding = asserts.decoding() await decoding.fail( "a", - `Expected a value with a length of at least 2, got "a"` + `Expected a value with a length of at least 2` ) }) @@ -1176,7 +1185,7 @@ Expected a string including "c", got "ab"` await decoding.succeed("abc") await decoding.fail( "", - `Expected a value with a length of at least 3, got ""` + `Expected a value with a length of at least 3` ) }) @@ -1218,11 +1227,11 @@ Expected a string including "c", got "ab"` await decoding.succeed(Option.some("a")) await decoding.fail( Option.some(""), - `Expected length > 0, got some("")` + `Expected length > 0` ) await decoding.fail( Option.none(), - `Expected isSome, got none()` + `Expected isSome` ) }) @@ -1235,17 +1244,28 @@ Expected a string including "c", got "ab"` await decoding.succeed("a") await decoding.fail( "b", - `Expected a string matching the RegExp ^a, got "b"` + `Expected a string matching the RegExp ^a` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", - `Expected a string matching the RegExp ^a, got "b"` + `Expected a string matching the RegExp ^a` ) }) + it("isPattern with stateful RegExp flags", async () => { + for (const regExp of [/^a/g, /^a/y]) { + const schema = Schema.String.check(Schema.isPattern(regExp)) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed("a") + await decoding.succeed("a") + strictEqual(regExp.lastIndex, 0) + } + }) + it("isStartsWith", async () => { const schema = Schema.String.check(Schema.isStartsWith("a")) const asserts = new TestSchema.Asserts(schema) @@ -1254,14 +1274,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("a") await decoding.fail( "b", - `Expected a string starting with "a", got "b"` + `Expected a string starting with "a"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", - `Expected a string starting with "a", got "b"` + `Expected a string starting with "a"` ) }) @@ -1273,14 +1293,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("a") await decoding.fail( "b", - `Expected a string ending with "a", got "b"` + `Expected a string ending with "a"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", - `Expected a string ending with "a", got "b"` + `Expected a string ending with "a"` ) }) @@ -1292,14 +1312,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("a") await decoding.fail( "A", - `Expected a string with all characters in lowercase, got "A"` + `Expected a string with all characters in lowercase` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "A", - `Expected a string with all characters in lowercase, got "A"` + `Expected a string with all characters in lowercase` ) }) @@ -1311,14 +1331,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("A") await decoding.fail( "a", - `Expected a string with all characters in uppercase, got "a"` + `Expected a string with all characters in uppercase` ) const encoding = asserts.encoding() await encoding.succeed("A") await encoding.fail( "a", - `Expected a string with all characters in uppercase, got "a"` + `Expected a string with all characters in uppercase` ) }) @@ -1330,14 +1350,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("Abc") await decoding.fail( "abc", - `Expected a string with the first character in uppercase, got "abc"` + `Expected a string with the first character in uppercase` ) const encoding = asserts.encoding() await encoding.succeed("Abc") await encoding.fail( "abc", - `Expected a string with the first character in uppercase, got "abc"` + `Expected a string with the first character in uppercase` ) }) @@ -1349,14 +1369,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("aBC") await decoding.fail( "ABC", - `Expected a string with the first character in lowercase, got "ABC"` + `Expected a string with the first character in lowercase` ) const encoding = asserts.encoding() await encoding.succeed("aBC") await encoding.fail( "ABC", - `Expected a string with the first character in lowercase, got "ABC"` + `Expected a string with the first character in lowercase` ) }) @@ -1368,14 +1388,14 @@ Expected a string including "c", got "ab"` await decoding.succeed("a") await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) }) }) @@ -1389,14 +1409,14 @@ Expected a string including "c", got "ab"` await decoding.succeed(2) await decoding.fail( 1, - `Expected a value greater than 1, got 1` + `Expected a value greater than 1` ) const encoding = asserts.encoding() await encoding.succeed(2) await encoding.fail( 1, - `Expected a value greater than 1, got 1` + `Expected a value greater than 1` ) }) @@ -1408,7 +1428,7 @@ Expected a string including "c", got "ab"` await decoding.succeed(1) await decoding.fail( 0, - `Expected a value greater than or equal to 1, got 0` + `Expected a value greater than or equal to 1` ) }) @@ -1420,7 +1440,7 @@ Expected a string including "c", got "ab"` await decoding.succeed(0) await decoding.fail( 1, - `Expected a value less than 1, got 1` + `Expected a value less than 1` ) }) @@ -1432,7 +1452,7 @@ Expected a string including "c", got "ab"` await decoding.succeed(1) await decoding.fail( 2, - `Expected a value less than or equal to 1, got 2` + `Expected a value less than or equal to 1` ) }) @@ -1444,7 +1464,7 @@ Expected a string including "c", got "ab"` await decoding.succeed(4) await decoding.fail( 3, - `Expected a value that is a multiple of 2, got 3` + `Expected a value that is a multiple of 2` ) }) @@ -1464,11 +1484,11 @@ Expected a string including "c", got "ab"` await decoding.succeed(3) await decoding.fail( 0, - `Expected a value between 1 and 3, got 0` + `Expected a value between 1 and 3` ) await decoding.fail( 4, - `Expected a value between 1 and 3, got 4` + `Expected a value between 1 and 3` ) const encoding = asserts.encoding() @@ -1476,7 +1496,7 @@ Expected a string including "c", got "ab"` await encoding.succeed(3) await encoding.fail( 0, - `Expected a value between 1 and 3, got 0` + `Expected a value between 1 and 3` ) }) @@ -1486,22 +1506,22 @@ Expected a string including "c", got "ab"` const decoding = asserts.decoding() await decoding.succeed(1) - await decoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`) + await decoding.fail(3, `Expected a value between 1 and 3 (excluded)`) await decoding.fail( 0, - `Expected a value between 1 and 3 (excluded), got 0` + `Expected a value between 1 and 3 (excluded)` ) await decoding.fail( 4, - `Expected a value between 1 and 3 (excluded), got 4` + `Expected a value between 1 and 3 (excluded)` ) const encoding = asserts.encoding() await encoding.succeed(1) - await encoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`) + await encoding.fail(3, `Expected a value between 1 and 3 (excluded)`) await encoding.fail( 0, - `Expected a value between 1 and 3 (excluded), got 0` + `Expected a value between 1 and 3 (excluded)` ) }) @@ -1510,23 +1530,23 @@ Expected a string including "c", got "ab"` const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() - await decoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`) + await decoding.fail(1, `Expected a value between 1 (excluded) and 3`) await decoding.succeed(3) await decoding.fail( 0, - `Expected a value between 1 (excluded) and 3, got 0` + `Expected a value between 1 (excluded) and 3` ) await decoding.fail( 4, - `Expected a value between 1 (excluded) and 3, got 4` + `Expected a value between 1 (excluded) and 3` ) const encoding = asserts.encoding() - await encoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`) + await encoding.fail(1, `Expected a value between 1 (excluded) and 3`) await encoding.succeed(3) await encoding.fail( 0, - `Expected a value between 1 (excluded) and 3, got 0` + `Expected a value between 1 (excluded) and 3` ) }) @@ -1538,24 +1558,24 @@ Expected a string including "c", got "ab"` const decoding = asserts.decoding() await decoding.succeed(2) - await decoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`) - await decoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`) + await decoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded)`) + await decoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded)`) await decoding.fail( 0, - `Expected a value between 1 (excluded) and 3 (excluded), got 0` + `Expected a value between 1 (excluded) and 3 (excluded)` ) await decoding.fail( 4, - `Expected a value between 1 (excluded) and 3 (excluded), got 4` + `Expected a value between 1 (excluded) and 3 (excluded)` ) const encoding = asserts.encoding() await encoding.succeed(2) - await encoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`) - await encoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`) + await encoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded)`) + await encoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded)`) await encoding.fail( 0, - `Expected a value between 1 (excluded) and 3 (excluded), got 0` + `Expected a value between 1 (excluded) and 3 (excluded)` ) }) }) @@ -1568,26 +1588,26 @@ Expected a string including "c", got "ab"` await decoding.succeed(1) await decoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) await decoding.fail( NaN, - `Expected an integer, got NaN` + `Expected an integer` ) await decoding.fail( Infinity, - `Expected an integer, got Infinity` + `Expected an integer` ) await decoding.fail( -Infinity, - `Expected an integer, got -Infinity` + `Expected an integer` ) }) @@ -1599,36 +1619,36 @@ Expected a string including "c", got "ab"` await decoding.succeed(1) await decoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) await decoding.fail( Number.MAX_SAFE_INTEGER + 1, - `Expected an integer, got 9007199254740992` + `Expected an integer` ) await decoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) await decoding.fail( Number.MIN_SAFE_INTEGER - 1, - `Expected an integer, got -9007199254740992` + `Expected an integer` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( Number.MAX_SAFE_INTEGER + 1, - `Expected an integer, got 9007199254740992 -Expected a value between -2147483648 and 2147483647, got 9007199254740992` + `Expected an integer +Expected a value between -2147483648 and 2147483647` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) await encoding.fail( Number.MAX_SAFE_INTEGER + 1, - `Expected an integer, got 9007199254740992` + `Expected an integer` ) }) }) @@ -1652,7 +1672,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed(10n) await decoding.fail( 4n, - `Expected a value between 5n and 10n, got 4n` + `Expected a value between 5n and 10n` ) }) @@ -1664,7 +1684,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed(6n) await decoding.fail( 5n, - `Expected a value greater than 5n, got 5n` + `Expected a value greater than 5n` ) }) @@ -1677,7 +1697,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed(6n) await decoding.fail( 4n, - `Expected a value greater than or equal to 5n, got 4n` + `Expected a value greater than or equal to 5n` ) }) @@ -1689,7 +1709,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed(4n) await decoding.fail( 5n, - `Expected a value less than 5n, got 5n` + `Expected a value less than 5n` ) }) @@ -1702,7 +1722,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed(4n) await decoding.fail( 6n, - `Expected a value less than or equal to 5n, got 6n` + `Expected a value less than or equal to 5n` ) }) }) @@ -1716,7 +1736,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( {}, - `Expected a value with at least 1 entry, got {}` + `Expected a value with at least 1 entry` ) }) @@ -1728,11 +1748,11 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 1, b: 2, c: 3 }, - `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}` + `Expected a value with at most 2 entries` ) await decoding.fail( { a: 1, b: 2, c: 3 }, - `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}` + `Expected a value with at most 2 entries` ) }) @@ -1747,7 +1767,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ a: 1, [sym]: 2 }) await decoding.fail( { [sym]: 1 }, - `Expected a value with at least 2 entries, got {Symbol(test):1}` + `Expected a value with at least 2 entries` ) }) @@ -1764,7 +1784,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ [sym1]: 1, [sym2]: 2 }) await decoding.fail( { [sym1]: 1, [sym2]: 2, [sym3]: 3 }, - `Expected a value with at most 2 entries, got {Symbol(test1):1,Symbol(test2):2,Symbol(test3):3}` + `Expected a value with at most 2 entries` ) }) @@ -1777,11 +1797,11 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ ["__proto__"]: 0, "": 0 }) await decoding.fail( { a: 1 }, - `Expected a value with exactly 2 entries, got {"a":1}` + `Expected a value with exactly 2 entries` ) await decoding.fail( { a: 1, b: 2, c: 3 }, - `Expected a value with exactly 2 entries, got {"a":1,"b":2,"c":3}` + `Expected a value with exactly 2 entries` ) }) @@ -1798,11 +1818,11 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ a: 1, [sym1]: 2 }) await decoding.fail( { [sym1]: 1 }, - `Expected a value with exactly 2 entries, got {Symbol(test1):1}` + `Expected a value with exactly 2 entries` ) await decoding.fail( { [sym1]: 1, [sym2]: 2, a: 3 }, - `Expected a value with exactly 2 entries, got {"a":3,Symbol(test1):1,Symbol(test2):2}` + `Expected a value with exactly 2 entries` ) }) @@ -1816,7 +1836,7 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({ Ab: 1 }) await decoding.fail( { ab: 1 }, - `Expected a string matching the RegExp ^[A-Z], got "ab" + `Expected a string matching the RegExp ^[A-Z] at ["ab"]` ) }) @@ -1829,72 +1849,12 @@ Expected a value between -2147483648 and 2147483647, got 9007199254740992` await decoding.succeed({}) await decoding.fail( { a: 1 }, - `Expected never, got "a" + `Expected never at ["a"]` ) }) }) - describe("Structural checks", () => { - it("Array + isMinLength", async () => { - const schema = Schema.Struct({ - tags: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(3)) - }) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding() - await decoding.fail( - {}, - `Missing key - at ["tags"]` - ) - const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) - await decodingAll.fail( - { tags: ["a", ""] }, - `Expected a value with a length of at least 1, got "" - at ["tags"][1] -Expected a value with a length of at least 3, got ["a",""] - at ["tags"]` - ) - }) - - it("Record + isMaxProperties", async () => { - const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2)) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding() - await decoding.fail( - null, - `Expected object, got null` - ) - const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) - await decodingAll.fail( - { a: 1, b: NaN, c: 3 }, - `Expected a finite number, got NaN - at ["b"] -Expected a value with at most 2 entries, got {"a":1,"b":NaN,"c":3}` - ) - }) - - it("ReadonlyMap + isMaxSize", async () => { - const schema = Schema.ReadonlyMap(Schema.String, Schema.Finite).check(Schema.isMaxSize(2)) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding() - await decoding.fail( - null, - `Expected ReadonlyMap, got null` - ) - const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) - await decodingAll.fail( - new Map([["a", 1], ["b", NaN], ["c", 3]]), - `Expected a finite number, got NaN - at ["entries"][1][1] -Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` - ) - }) - }) - describe("Array checks", () => { it("UniqueArray", async () => { const schema = Schema.UniqueArray(Schema.Struct({ @@ -1907,7 +1867,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed([{ a: "a", b: "b" }, { a: "c", b: "d" }]) await decoding.fail( [{ a: "a", b: "b" }, { a: "a", b: "b" }], - `Expected an array with unique items, got [{"a":"a","b":"b"},{"a":"a","b":"b"}]` + `Expected an array with unique items` ) }) }) @@ -1944,7 +1904,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(-Infinity, "-Infinity") await encoding.fail( "a", - `Expected number, got "a"` + `Expected number` ) }) @@ -1957,9 +1917,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("2021-01-01T00:00:00.000Z", new Date("2021-01-01T00:00:00.000Z")) + await decoding.fail("invalid", `Expected a valid Date`) const encoding = asserts.encoding() await encoding.succeed(new Date("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z") + await encoding.fail(new Date(NaN), `Expected a valid Date`) }) it("DateFromMillis", async () => { @@ -1971,17 +1933,18 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(0, new Date(0)) - assertTrue(Schema.decodeSync(schema)(NaN) instanceof Date) - assertTrue(Schema.decodeSync(schema)(Infinity) instanceof Date) - assertTrue(Schema.decodeSync(schema)(-Infinity) instanceof Date) - await decoding.fail(null, `Expected number, got null`) + await decoding.fail(NaN, `Expected an integer`) + await decoding.fail(Infinity, `Expected an integer`) + await decoding.fail(-Infinity, `Expected an integer`) + await decoding.fail(null, `Expected number`) + await decoding.fail(8640000000000001, `Expected a valid Date`) const encoding = asserts.encoding() await encoding.succeed(new Date(0), 0) - strictEqual(Schema.encodeSync(schema)(new Date("invalid")), NaN) - strictEqual(Schema.encodeSync(schema)(new Date(NaN)), NaN) - strictEqual(Schema.encodeSync(schema)(new Date(Infinity)), NaN) - strictEqual(Schema.encodeSync(schema)(new Date(-Infinity)), NaN) + await encoding.fail(new Date("invalid"), `Expected a valid Date`) + await encoding.fail(new Date(NaN), `Expected a valid Date`) + await encoding.fail(new Date(Infinity), `Expected a valid Date`) + await encoding.fail(new Date(-Infinity), `Expected a valid Date`) }) it("FiniteFromString", async () => { @@ -1996,30 +1959,30 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("1", 1) await decoding.fail( "a", - `Expected a finite number, got NaN` + `Expected a finite number` ) await decoding.fail( "NaN", - `Expected a finite number, got NaN` + `Expected a finite number` ) await decoding.fail( "Infintiy", - `Expected a finite number, got NaN` + `Expected a finite number` ) await decoding.fail( "+Infintiy", - `Expected a finite number, got NaN` + `Expected a finite number` ) await decoding.fail( "-Infintiy", - `Expected a finite number, got NaN` + `Expected a finite number` ) const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.fail( "a", - `Expected number, got "a"` + `Expected number` ) }) @@ -2034,7 +1997,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("0", 0n) await decoding.fail( "a", - `Expected a string representing a bigint, got "a"` + `Expected a string representing a bigint` ) const encoding = asserts.encoding() @@ -2058,7 +2021,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(BigDecimal.make(123456n, 3), "123.456") await encoding.fail( "a", - `Expected BigDecimal, got "a"` + `Expected BigDecimal` ) }) @@ -2077,7 +2040,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"), "Europe/London") await encoding.fail( "a", - `Expected DateTime.TimeZone.Named, got "a"` + `Expected DateTime.TimeZone.Named` ) }) @@ -2098,7 +2061,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000), "+03:00") await encoding.fail( "a", - `Expected DateTime.TimeZone, got "a"` + `Expected DateTime.TimeZone` ) }) @@ -2113,13 +2076,13 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const zoned = DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }) const decoding = asserts.decoding() - await decoding.fail("invalid", `Invalid Zoned DateTime string: invalid`) + await decoding.fail("invalid", "Expected a valid Zoned DateTime string") const encoding = asserts.encoding() await encoding.succeed(zoned, DateTime.formatIsoZoned(zoned)) await encoding.fail( "a", - `Expected DateTime.Zoned, got "a"` + `Expected DateTime.Zoned` ) }) @@ -2131,14 +2094,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("3", 3) await decoding.fail( "1", - `Expected a value greater than 2, got 1` + `Expected a value greater than 2` ) const encoding = asserts.encoding() await encoding.succeed(3, "3") await encoding.fail( 1, - `Expected a value greater than 2, got 1` + `Expected a value greater than 2` ) }) }) @@ -2239,7 +2202,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(" 2 ", 2) await decoding.fail( " a2 ", - `Expected a finite number, got NaN` + `Expected a finite number` ) const encoding = asserts.encoding() @@ -2265,7 +2228,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "aaa" }) await decoding.fail( { a: "aa" }, - `Expected a value with a length of at least 3, got "aa" + `Expected a value with a length of at least 3 at ["a"]` ) @@ -2273,7 +2236,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: "aaa" }) await encoding.fail( { a: "aa" }, - `Expected a value with a length of at least 3, got "aa" + `Expected a value with a length of at least 3 at ["a"]` ) }) @@ -2417,7 +2380,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(" 2 ", 2) await decoding.fail( " a2 ", - `Expected a finite number, got NaN` + `Expected a finite number` ) const encoding = asserts.encoding() @@ -2443,7 +2406,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "aaa" }) await decoding.fail( { a: "aa" }, - `Expected a value with a length of at least 3, got "aa" + `Expected a value with a length of at least 3 at ["a"]` ) @@ -2451,7 +2414,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: "aaa" }) await encoding.fail( { a: "aa" }, - `Expected a value with a length of at least 3, got "aa" + `Expected a value with a length of at least 3 at ["a"]` ) }) @@ -2496,11 +2459,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( 2, - `Expected a value greater than 2, got 2` + `Expected a value greater than 2` ) await decoding.fail( 3, - `Expected a value with a length of at least 3, got "3"` + `Expected a value with a length of at least 3` ) const encoding = asserts.encoding() @@ -2521,14 +2484,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( { a: "a" }, - `Expected a length > 1, got {"a":"a"}` + `Expected a length > 1` ) await decoding.succeed({ a: "aa" }) const encoding = asserts.encoding() await encoding.fail( { a: "a" }, - `Expected a length > 1, got {"a":"a"}` + `Expected a length > 1` ) await encoding.succeed({ a: "aa" }) }) @@ -2547,14 +2510,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( ["a"], - `Expected head length > 1, got ["a"]` + `Expected head length > 1` ) await decoding.succeed(["aa"]) const encoding = asserts.encoding() await encoding.fail( ["a"], - `Expected head length > 1, got ["a"]` + `Expected head length > 1` ) await encoding.succeed(["aa"]) }) @@ -2573,14 +2536,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( "a", - `Expected "aa", got "a"` + `Expected "aa"` ) await decoding.succeed("aa") const encoding = asserts.encoding() await encoding.fail( "a", - `Expected "aa", got "a"` + `Expected "aa"` ) await encoding.succeed("aa") }) @@ -2602,14 +2565,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( "a", - `Expected a length > 1, got "a"` + `Expected a length > 1` ) await decoding.succeed("aa") const encoding = asserts.encoding() await encoding.fail( "a", - `Expected a length > 1, got "a"` + `Expected a length > 1` ) await encoding.succeed("aa") }) @@ -2636,53 +2599,18 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( { b: "a" }, - `Expected a length > 1, got {"a":"a"}` + `Expected a length > 1` ) await decoding.succeed({ b: "aa" }, { a: "aa" }) const encoding = asserts.encoding() await encoding.fail( { a: "a" }, - `Expected a length > 1, got {"a":"a"}` + `Expected a length > 1` ) await encoding.succeed({ a: "aa" }, { b: "aa" }) }) - it(`Struct & encoding chain & structural checks should check the local value with errors: "all"`, async () => { - const local = Schema.Struct({ a: Schema.Finite }).check(Schema.isMaxProperties(1)) - const schema = Schema.Struct({ b: Schema.Number, c: Schema.String }).pipe( - Schema.decodeTo(local, { - decode: SchemaGetter.transform< - { readonly a: number }, - { readonly b: number; readonly c: string } - >((o) => ({ a: o.b })), - encode: SchemaGetter.transform< - { readonly b: number; readonly c: string }, - { readonly a: number } - >((o) => ({ b: o.a, c: "" })) - }) - ) - assertTrue(SchemaAST.isObjects(schema.ast)) - strictEqual(schema.ast.encoding?.length, 1) - strictEqual(schema.ast.checks?.length, 1) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) - await decoding.fail( - { b: NaN, c: "extra" }, - `Expected a finite number, got NaN - at ["a"]` - ) - - const encoding = asserts.encoding({ parseOptions: { errors: "all" } }) - await encoding.fail( - { a: NaN }, - `Expected a finite number, got NaN - at ["a"]` - ) - await encoding.succeed({ a: 1 }, { b: 1, c: "" }) - }) - it("should work with withConstructorDefault", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) @@ -2717,7 +2645,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new File([], "a.txt")) - await decoding.fail("a", `Expected File, got "a"`) + await decoding.fail("a", `Expected File`) }) describe("Redacted", () => { @@ -2727,6 +2655,17 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` strictEqual(schema.annotate({}).value, Schema.String) }) + it("does not expose unwrapped inputs", async () => { + const schema = Schema.Redacted(Schema.String) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail("secret", `Expected Redacted`) + + const encoding = asserts.encoding() + await encoding.fail("secret", `Expected Redacted`) + }) + it("Redacted(Finite)", async () => { const schema = Schema.Redacted(Schema.Int) const asserts = new TestSchema.Asserts(schema) @@ -2737,29 +2676,29 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Redacted.make(123)) - await decoding.fail(null, `Expected Redacted, got null`) + await decoding.fail(null, `Expected Redacted`) await decoding.fail( Redacted.make("a"), - `Invalid data + `Expected a valid value at ["value"]` ) await decoding.fail( Redacted.make(1.2), - `Invalid data + `Expected a valid value at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Redacted.make(123)) - await encoding.fail(null, `Expected Redacted, got null`) + await encoding.fail(null, `Expected Redacted`) await encoding.fail( Redacted.make("a"), - `Invalid data + `Expected a valid value at ["value"]` ) await encoding.fail( Redacted.make(1.2), - `Invalid data + `Expected a valid value at ["value"]` ) }) @@ -2774,39 +2713,39 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Redacted.make("123"), Redacted.make(123)) - await decoding.fail(null, `Expected Redacted, got null`) + await decoding.fail(null, `Expected Redacted`) await decoding.fail( Redacted.make(null), - `Invalid data + `Expected a valid value at ["value"]` ) await decoding.fail( Redacted.make("a"), - `Invalid data + `Expected a valid value at ["value"]` ) await decoding.fail( Redacted.make("1.2"), - `Invalid data + `Expected a valid value at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Redacted.make(123), Redacted.make("123")) - await encoding.fail(null, `Expected Redacted, got null`) + await encoding.fail(null, `Expected Redacted`) await encoding.fail( Redacted.make(null), - `Invalid data + `Expected a valid value at ["value"]` ) await encoding.fail( Redacted.make("a"), - `Invalid data + `Expected a valid value at ["value"]` ) await encoding.fail( Redacted.make(1.2), - `Invalid data + `Expected a valid value at ["value"]` ) }) @@ -2819,17 +2758,17 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(Redacted.make("a", { label: "password" })) await decoding.fail( Redacted.make("a", { label: "API key" }), - `Expected "password", got "API key" + `Expected "password" at ["label"]` ) await decoding.fail( Redacted.make(1, { label: "API key" }), - `Expected "password", got "API key" + `Expected "password" at ["label"]` ) await decoding.fail( Redacted.make(1, { label: "password" }), - `Invalid data + `Expected a valid value at ["value"]` ) @@ -2837,34 +2776,40 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(Redacted.make("a", { label: "password" })) await encoding.fail( Redacted.make("a", { label: "API key" }), - `Expected "password", got "API key" + `Expected "password" at ["label"]` ) await encoding.fail( Redacted.make("", { label: "password" }), - `Invalid data + `Expected a valid value at ["value"]` ) }) }) describe("RedactedFromValue", () => { - it("should not leak any information about the value", async () => { - const schema = Schema.RedactedFromValue(Schema.Literal("secret")) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding() - await decoding.fail(null, `Invalid data `) - }) - it("should decode a value", async () => { const schema = Schema.RedactedFromValue(Schema.FiniteFromString.check(Schema.isInt())) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("123", Redacted.make(123)) - await decoding.fail(null, `Invalid data `) - await decoding.fail("1.2", `Invalid data `) + await decoding.fail(null, `Expected string`) + await decoding.fail("1.2", `Expected an integer`) + + const encoding = asserts.encoding() + await encoding.succeed(Redacted.make(123), "123") + await encoding.fail("schema-secret", `Expected Redacted`) + await encoding.fail( + Redacted.make("schema-secret"), + `Expected a valid value + at ["value"]` + ) + await encoding.fail( + Redacted.make(1.2), + `Expected a valid value + at ["value"]` + ) }) }) @@ -2886,20 +2831,20 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Option.none()) await decoding.succeed(Option.some("123"), Option.some(123)) - await decoding.fail(null, `Expected Option, got null`) + await decoding.fail(null, `Expected Option`) await decoding.fail( Option.some(null), - `Expected string, got null + `Expected string at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Option.none()) await encoding.succeed(Option.some(123), Option.some("123")) - await encoding.fail(null, `Expected Option, got null`) + await encoding.fail(null, `Expected Option`) await encoding.fail( Option.some(null), - `Expected number, got null + `Expected number at ["value"]` ) }) @@ -2916,7 +2861,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(null, Option.none()) await decoding.succeed("1", Option.some(1)) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail("a", `Expected a finite number`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), null) @@ -2934,7 +2879,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail("a", `Expected a finite number`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), undefined) @@ -2954,7 +2899,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(null, Option.none()) await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail("a", `Expected a finite number`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), null) @@ -2973,7 +2918,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(null, Option.none()) await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail("a", `Expected a finite number`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), undefined) @@ -2996,12 +2941,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: undefined }, - `Expected string, got undefined + `Expected string at ["a"]` ) await decoding.fail( { a: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["a"]` ) @@ -3026,7 +2971,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["a"]` ) @@ -3053,7 +2998,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["a"]` ) @@ -3062,7 +3007,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: Option.some(1) }, { a: "1" }) await encoding.fail( { a: null }, - `Expected Option, got null + `Expected Option at ["a"]` ) }) @@ -3122,15 +3067,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Result.succeed("1"), Result.succeed(1)) await decoding.succeed(Result.fail("2"), Result.fail(2)) - await decoding.fail(null, `Expected Result, got null`) + await decoding.fail(null, `Expected Result`) await decoding.fail( Result.succeed("a"), - `Expected a finite number, got NaN + `Expected a finite number at ["success"]` ) await decoding.fail( Result.fail("b"), - `Expected a finite number, got NaN + `Expected a finite number at ["failure"]` ) @@ -3187,7 +3132,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(noPrototypeObject, { message: "a" }) }) - it("Error and Defect memoize equivalent options", () => { + it("ErrorInstance and Defect memoize equivalent options", () => { const assertMemoized = (schema: (options?: Schema.ErrorOptions) => S) => { strictEqual(schema(), schema({})) strictEqual(schema(), schema({ includeStack: false })) @@ -3207,7 +3152,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` assertFalse(schema({ excludeCause: true }) === schema({ includeStack: true, excludeCause: true })) } - assertMemoized(Schema.Error) + assertMemoized(Schema.ErrorInstance) assertMemoized(Schema.Defect) }) @@ -3254,12 +3199,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( Cause.fail("a"), - `Expected a finite number, got NaN + `Expected a finite number at ["failures"][0]["error"]` ) await decoding.fail( Cause.die("a"), - `Expected a finite number, got NaN + `Expected a finite number at ["failures"][0]["defect"]` ) @@ -3270,19 +3215,19 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.fail( Cause.fail("a"), - `Expected number, got "a" + `Expected number at ["failures"][0]["error"]` ) await encoding.fail( Cause.die("a"), - `Expected number, got "a" + `Expected number at ["failures"][0]["defect"]` ) }) }) it("Error", async () => { - const schema = Schema.Error() + const schema = Schema.ErrorInstance() const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { @@ -3299,11 +3244,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(customError) await decoding.fail( { message: "a" }, - `Expected Error, got {"message":"a"}` + `Expected Error` ) await decoding.fail( "a", - `Expected Error, got "a"` + `Expected Error` ) const encoding = asserts.encoding() @@ -3311,11 +3256,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(customError) await encoding.fail( { message: "a" }, - `Expected Error, got {"message":"a"}` + `Expected Error` ) await encoding.fail( "a", - `Expected Error, got "a"` + `Expected Error` ) }) @@ -3343,16 +3288,16 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(Exit.fail("boom")) await decoding.fail( null, - `Expected Exit, got null` + `Expected Exit` ) await decoding.fail( Exit.succeed(123), - `Expected string, got 123 + `Expected string at ["value"]` ) await decoding.fail( Exit.fail(null), - `Expected string, got null + `Expected string at ["cause"]["failures"][0]["error"]` ) }) @@ -3401,7 +3346,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` a: "1", categories: [{ a: "a", categories: [] }] }, - `Expected a finite number, got NaN + `Expected a finite number at ["categories"][0]["a"]` ) @@ -3413,16 +3358,42 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) await encoding.fail( { a: 1, categories: [{ a: -1, categories: [] }] }, - `Expected a value greater than 0, got -1 + `Expected a value greater than 0 at ["categories"][0]["a"]` ) }) }) describe("make", () => { + it("preserves __proto__ as an own option", () => { + const value = { polluted: true } + const schema = Schema.make(Schema.String.ast, { + ["__proto__"]: value + }) + + assertTrue(Schema.isSchema(schema)) + assertTrue(Object.hasOwn(schema, "__proto__")) + strictEqual((schema as any)["__proto__"], value) + }) + + it("preserves name and length as options", () => { + const schema = Schema.make(Schema.String.ast, { + name: "CustomSchema", + length: 2 + }) + + strictEqual((schema as any).name, "CustomSchema") + strictEqual((schema as any).length, 2) + + const rebuilt = schema.annotate({}) + + strictEqual((rebuilt as any).name, "CustomSchema") + strictEqual((rebuilt as any).length, 2) + }) + it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( - Cause.fail(new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ @@ -3478,7 +3449,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` deepStrictEqual(success, Result.succeed({ a: 1 })) const failure = yield* schema.makeEffect({ a: -1 }).pipe(Effect.flip) - assertTrue(Schema.isSchemaError(failure)) + assertTrue(SchemaIssue.isIssue(failure)) })) it.effect("Class", () => @@ -3489,15 +3460,13 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` deepStrictEqual(success, new A({ a: 1 })) const failure = yield* A.makeEffect({ a: -1 }).pipe(Effect.flip) - assertTrue(Schema.isSchemaError(failure)) + assertTrue(SchemaIssue.isIssue(failure)) })) - it.effect("should preserve mixed schema error and defect causes", () => + it.effect("should preserve mixed schema issue and defect causes", () => Effect.gen(function*() { const cause = Cause.combine( - Cause.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })) - ), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ @@ -3509,7 +3478,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` assertTrue(Exit.hasDies(exit)) const error = Cause.findError(exit.cause) assertTrue(Result.isSuccess(error)) - assertTrue(Schema.isSchemaError(error.success)) + assertTrue(SchemaIssue.isIssue(error.success)) })) }) @@ -3620,12 +3589,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await make.succeed({}, { a: -1 }) }) - it("Effect failing with SchemaError propagates as parse failure", async () => { + it("Effect failing with SchemaIssue propagates as parse failure", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault( - Effect.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "ctor default failed" })) - ) + Effect.fail(new SchemaIssue.InvalidValue({ message: "ctor default failed" })) )) }) const asserts = new TestSchema.Asserts(schema) @@ -3654,7 +3621,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await make.succeed({ a: 1 }) await make.fail( {}, - `Expected a value greater than 0, got -1 + `Expected a value greater than 0 at ["a"]["n"]` ) }) @@ -3757,14 +3724,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const make = asserts.make() await make.succeed({ a: 1 }) - await make.fail(null, `Expected object, got null`) + await make.fail(null, `Expected object`) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) - await decoding.fail(null, "Expected object, got null") + await decoding.fail(null, "Expected object") await decoding.fail( { a: "b" }, - `Expected number, got "b" + `Expected number at ["a"]` ) @@ -3772,12 +3739,46 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: 1 }) await encoding.fail( { a: "b" }, - `Expected number, got "b" + `Expected number at ["a"]` ) - await encoding.fail(null, "Expected object, got null") + await encoding.fail(null, "Expected object") + }) + + it("recursive values stay lazy", async () => { + interface Recursive { + readonly [key: string]: Recursive + } + const schema: Schema.Codec = Schema.Record( + Schema.String, + Schema.suspend((): Schema.Codec => schema) + ) + const asserts = new TestSchema.Asserts(schema) + + const input = { a: { b: {} } } + await asserts.decoding().succeed(input) + await asserts.encoding().succeed(input) }) + it.effect("sequential parsing resumes without replaying entries", () => + Effect.gen(function*() { + const calls: Array = [] + const value = Schema.String.pipe( + Schema.decode({ + decode: SchemaGetter.transformOrFail((value) => { + calls.push(value) + return value === "b" ? Effect.yieldNow.pipe(Effect.as(value)) : Effect.succeed(value) + }), + encode: SchemaGetter.passthrough() + }) + ) + const schema = Schema.Record(Schema.String, value) + const input = { a: "a", b: "b", c: "c" } + + deepStrictEqual(yield* Schema.decodeUnknownEffect(schema)(input), input) + deepStrictEqual(calls, ["a", "b", "c"]) + })) + it("Record(String, optionalKey(Number)) should throw", async () => { throws( () => Schema.Record(Schema.String, Schema.optionalKey(Schema.Number)), @@ -3792,7 +3793,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({ a: undefined }) - await make.fail(null, `Expected object, got null`) + await make.fail(null, `Expected object`) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) @@ -3811,7 +3812,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: 1, ab: 2, b: "ignored" }, { a: 1, ab: 2 }) await decoding.fail( { a: "bad", b: 1 }, - `Expected number, got "bad" + `Expected number at ["a"]` ) }) @@ -3822,14 +3823,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const make = asserts.make() await make.succeed({ [Symbol.for("a")]: 1 }) - await make.fail(null, `Expected object, got null`) + await make.fail(null, `Expected object`) const decoding = asserts.decoding() await decoding.succeed({ [Symbol.for("a")]: 1 }) - await decoding.fail(null, "Expected object, got null") + await decoding.fail(null, "Expected object") await decoding.fail( { [Symbol.for("a")]: "b" }, - `Expected number, got "b" + `Expected number at [Symbol(a)]` ) @@ -3837,10 +3838,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ [Symbol.for("a")]: 1 }) await encoding.fail( { [Symbol.for("a")]: "b" }, - `Expected number, got "b" + `Expected number at [Symbol(a)]` ) - await encoding.fail(null, "Expected object, got null") + await encoding.fail(null, "Expected object") }) it("Record(Symbol.check, Number) should use the key checks to select keys", async () => { @@ -3856,7 +3857,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ [a]: 1, [b]: "ignored" }, { [a]: 1 }) await decoding.fail( { [a]: "bad", [b]: 1 }, - `Expected number, got "bad" + `Expected number at [Symbol(a)]` ) }) @@ -3876,30 +3877,6 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "2" }) }) - it("Record(SnakeToCamel, Number, { keyValueCombiner: ... })", async () => { - const schema = Schema.Record(SnakeToCamel, Schema.NumberFromString, { - keyValueCombiner: { - decode: { - combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] - }, - encode: { - combine: ([_, v1], [k2, v2]) => [k2, v1 + "e" + v2] - } - } - }) - const asserts = new TestSchema.Asserts(schema) - - const decoding = asserts.decoding() - await decoding.succeed({ a: "1" }, { a: 1 }) - await decoding.succeed({ a_b: "1" }, { aB: 1 }) - await decoding.succeed({ a_b: "1", aB: "2" }, { aB: 3 }) - - const encoding = asserts.encoding() - await encoding.succeed({ a: 1 }, { a: "1" }) - await encoding.succeed({ aB: 1 }, { a_b: "1" }) - await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "1e2" }) - }) - it("UniqueSymbol", async () => { const a = Symbol.for("a") const schema = Schema.Record(Schema.UniqueSymbol(a), Schema.Number) @@ -3909,7 +3886,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ [a]: 1 }) await decoding.fail( { [a]: "b" }, - `Expected number, got "b" + `Expected number at [Symbol(a)]` ) }) @@ -3981,12 +3958,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) await decoding.fail( { 1: null }, - `Expected string, got null + `Expected string at ["1"]` ) await decoding.fail( { 1: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["1"]` ) }) @@ -4000,12 +3977,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ 1: "1", "1.1": "ignored", Infinity: "ignored", NaN: "ignored" }, { "1": 1 }) await decoding.fail( { 1: null }, - `Expected string, got null + `Expected string at ["1"]` ) await decoding.fail( { 1: "a" }, - `Expected a finite number, got NaN + `Expected a finite number at ["1"]` ) }) @@ -4018,7 +3995,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "ignored", ab: 1 }, { ab: 1 }) await decoding.fail( { a: 1, ab: "bad" }, - `Expected number, got "bad" + `Expected number at ["ab"]` ) }) @@ -4048,7 +4025,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() - await decoding.fail(null, `Expected never, got null`) + await decoding.fail(null, `Expected never`) }) it(`String`, async () => { @@ -4057,7 +4034,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(null, `Expected string, got null`) + await decoding.fail(null, `Expected string`) }) it(`Void`, async () => { @@ -4092,7 +4069,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(1) await decoding.fail( null, - `Expected string | number, got null` + `Expected string | number` ) }) @@ -4102,7 +4079,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(null, `Expected string | never, got null`) + await decoding.fail(null, `Expected string | never`) }) it(`String & isMinLength(1) | number & isGreaterThan(0)`, async () => { @@ -4117,11 +4094,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(1) await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) await decoding.fail( -1, - `Expected a value greater than 0, got -1` + `Expected a value greater than 0` ) }) @@ -4137,7 +4114,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ b: 1 }) await decoding.fail( { a: "a", b: 1 }, - `Expected exactly one member to match the input {"a":"a","b":1}` + "Expected exactly one member to match" ) }) @@ -4151,7 +4128,33 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( { kind: "a", status: "ready", value: "value" }, - `Expected exactly one member to match the input {"kind":"a","status":"ready","value":"value"}` + "Expected exactly one member to match" + ) + }) + + it(`mode: "oneOf" with nested and contradicted sentinels`, async () => { + const nested = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x") }), + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y") }) + ]) + const schema = Schema.Struct({ + block: Schema.Union([ + nested, + Schema.Struct({ kind: Schema.Literal("b") }) + ], { mode: "oneOf" }) + }) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed({ block: { kind: "a", variant: "x" } }) + await decoding.fail( + { block: { kind: "a", variant: "z" } }, + `Expected { readonly "kind": "a", readonly "variant": "x", ... } | { readonly "kind": "a", readonly "variant": "y", ... } + at ["block"]` + ) + await decoding.fail( + { block: { kind: "a", variant: undefined } }, + `Expected { readonly "kind": "a", readonly "variant": "x", ... } | { readonly "kind": "a", readonly "variant": "y", ... } + at ["block"]` ) }) @@ -4163,7 +4166,19 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( { kind: "a" }, - `Expected exactly one member to match the input {"kind":"a"}` + "Expected exactly one member to match" + ) + }) + + it(`mode: "oneOf" counts repeated literal occurrences`, async () => { + const member = Schema.Literal("a") + const schema = Schema.Union([member, member], { mode: "oneOf" }) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.fail( + "a", + "Expected exactly one member to match" ) }) @@ -4187,6 +4202,143 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ kind: "a", value: "value" }, "fallback") }) + it("preserves recovering members during runtime type dispatch", async () => { + const decodingFallback = Schema.String.pipe( + Schema.catchDecoding(() => Effect.succeed(Option.some("fallback"))) + ) + const encodingFallback = Schema.String.pipe( + Schema.catchEncoding(() => Effect.succeed(Option.some("fallback"))) + ) + const transformedFallback = Schema.NumberFromString.pipe( + Schema.catchDecoding(() => Effect.succeed(Option.some(0))) + ) + const nestedFallback = Schema.Union([decodingFallback, Schema.Number]) + const literalFallback = Schema.Literal("a").pipe( + Schema.catchDecoding(() => Effect.succeed(Option.some("a" as const))) + ) + const record = Schema.Record(decodingFallback, Schema.String) + + const decoding = new TestSchema.Asserts(Schema.Union([decodingFallback, Schema.Null])).decoding() + await decoding.succeed(null, "fallback") + + const transformedDecoding = new TestSchema.Asserts(Schema.Union([transformedFallback, Schema.Null])).decoding() + await transformedDecoding.succeed(null, 0) + + const nestedDecoding = new TestSchema.Asserts(Schema.Union([nestedFallback, Schema.Null])).decoding() + await nestedDecoding.succeed(null, "fallback") + + const recordDecoding = new TestSchema.Asserts(Schema.Union([record, Schema.Null])).decoding() + await recordDecoding.succeed(null, null) + + const literalDecoding = new TestSchema.Asserts( + Schema.Union([literalFallback, Schema.Literal("b")]) + ).decoding() + await literalDecoding.succeed("b", "a") + + const decodingOneOf = new TestSchema.Asserts( + Schema.Union([decodingFallback, Schema.Null], { mode: "oneOf" }) + ).decoding() + await decodingOneOf.fail(null, "Expected exactly one member to match") + + const encoding = new TestSchema.Asserts(Schema.Union([encodingFallback, Schema.Null])).encoding() + await encoding.succeed(null, "fallback") + + const encodingOneOf = new TestSchema.Asserts( + Schema.Union([encodingFallback, Schema.Null], { mode: "oneOf" }) + ).encoding() + await encodingOneOf.fail(null, "Expected exactly one member to match") + }) + + it("preserves recovering members during sentinel dispatch", async () => { + const decodingTag = Schema.Literal("a").pipe( + Schema.catchDecoding(() => Effect.succeed(Option.some("a" as const))) + ) + const encodingTag = Schema.Literal("a").pipe( + Schema.catchEncoding(() => Effect.succeed(Option.some("a" as const))) + ) + const decodingFirst = Schema.Struct({ kind: decodingTag }) + const encodingFirst = Schema.Struct({ kind: encodingTag }) + const decodingRoot = Schema.Struct({ kind: Schema.Literal("a") }).pipe( + Schema.catchDecoding(() => Effect.succeed(Option.some({ kind: "a" as const }))) + ) + const second = Schema.Struct({ kind: Schema.Literal("b") }) + const input = { kind: "b" as const } + + const decoding = new TestSchema.Asserts(Schema.Union([decodingFirst, second])).decoding() + await decoding.succeed(input, { kind: "a" }) + + const rootDecoding = new TestSchema.Asserts(Schema.Union([decodingRoot, second])).decoding() + await rootDecoding.succeed(input, { kind: "a" }) + + const decodingOneOf = new TestSchema.Asserts( + Schema.Union([decodingFirst, second], { mode: "oneOf" }) + ).decoding() + await decodingOneOf.fail(input, "Expected exactly one member to match") + + const encoding = new TestSchema.Asserts(Schema.Union([encodingFirst, second])).encoding() + await encoding.succeed(input, { kind: "a" }) + + const encodingOneOf = new TestSchema.Asserts( + Schema.Union([encodingFirst, second], { mode: "oneOf" }) + ).encoding() + await encodingOneOf.fail(input, "Expected exactly one member to match") + }) + + it("keeps suspended members lazy during candidate selection", async () => { + let decodingEvaluations = 0 + const decodingSuspended = Schema.suspend(() => { + decodingEvaluations++ + return Schema.String + }) + const decoding = new TestSchema.Asserts( + Schema.Union([Schema.Literal("a"), decodingSuspended]) + ).decoding() + + await decoding.succeed("a", "a") + strictEqual(decodingEvaluations, 0) + await decoding.succeed("b", "b") + await decoding.succeed("c", "c") + strictEqual(decodingEvaluations, 1) + + let encodingEvaluations = 0 + const encodingSuspended = Schema.suspend(() => { + encodingEvaluations++ + return Schema.String + }) + const encoding = new TestSchema.Asserts( + Schema.Union([Schema.Literal("a"), encodingSuspended]) + ).encoding() + + await encoding.succeed("a", "a") + strictEqual(encodingEvaluations, 0) + + let oneOfEvaluations = 0 + const oneOfSuspended = Schema.suspend(() => { + oneOfEvaluations++ + return Schema.String + }) + const oneOf = new TestSchema.Asserts( + Schema.Union([Schema.Literal("a"), oneOfSuspended], { mode: "oneOf" }) + ).decoding() + + await oneOf.fail("a", "Expected exactly one member to match") + strictEqual(oneOfEvaluations, 1) + }) + + it("does not force a recursive suspended member after an earlier success", async () => { + let evaluations = 0 + let recursive: Schema.Codec<"end"> + const suspended: Schema.Codec<"end"> = Schema.suspend(() => { + evaluations++ + return recursive + }) + recursive = Schema.Union([Schema.Literal("end"), suspended]) + + const decoding = new TestSchema.Asserts(recursive).decoding() + await decoding.succeed("end", "end") + strictEqual(evaluations, 0) + }) + it.effect("preserves member order with concurrent decoding", () => Effect.gen(function*() { const firstLatch = yield* Deferred.make() @@ -4221,9 +4373,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const secondCompleted = yield* Deferred.make() const first = Schema.String.pipe( Schema.decode({ - decode: SchemaGetter.transformOrFail((value) => + decode: SchemaGetter.transformOrFail(() => Deferred.await(firstLatch).pipe( - Effect.andThen(Effect.fail(new SchemaIssue.Forbidden(Option.some(value), { message: "first failed" }))) + Effect.andThen(Effect.fail(new SchemaIssue.Forbidden({ message: "first failed" }))) ) ), encode: SchemaGetter.passthrough() @@ -4334,18 +4486,29 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() - await decoding.fail("a", `Expected exactly one member to match the input "a"`) + await decoding.fail("a", "Expected exactly one member to match") }) - it("{} & Literal", async () => { + it("Struct({}) preserves its semantics in a union", async () => { const schema = Schema.Union([ Schema.Struct({}), - Schema.Literal("a") + Schema.Null ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() + const symbol = Symbol() + const fn = () => {} + await decoding.succeed("a") + await decoding.succeed(1) + await decoding.succeed(true) + await decoding.succeed(symbol) + await decoding.succeed(1n) + await decoding.succeed(fn) + await decoding.succeed({}) await decoding.succeed([]) + await decoding.succeed(null) + await decoding.fail(undefined, `Expected object | array | null`) }) describe("should exclude members based on failed sentinels", () => { @@ -4359,7 +4522,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( {}, - `Expected string | { readonly "_tag": "a", ... }, got {}` + `Expected string | { readonly "_tag": "a", ... }` ) }) @@ -4373,7 +4536,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( [], - `Expected string | readonly [ "a", ... ], got []` + `Expected string | readonly [ "a", ... ]` ) }) @@ -4397,7 +4560,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) await decoding.fail( { _tag: "c" }, - `Expected { readonly "_tag": "a", ... } | { readonly "_tag": "b", ... }, got {"_tag":"c"}` + `Expected { readonly "_tag": "a", ... } | { readonly "_tag": "b", ... }` ) }) @@ -4421,7 +4584,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) await decoding.fail( ["c"], - `Expected readonly [ "a", ... ] | readonly [ "b", ... ], got ["c"]` + `Expected readonly [ "a", ... ] | readonly [ "b", ... ]` ) }) }) @@ -4484,17 +4647,17 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) await decoding.fail( ["1", "a", true], - `Expected string, got true + `Expected string at [2]` ) await decoding.fail( ["1", "a", "b", "c"], - `Expected boolean, got "b" + `Expected boolean at [2]` ) await decoding.fail( ["1", "a", true, "b", "c"], - `Expected boolean, got "b" + `Expected boolean at [3]` ) @@ -4526,7 +4689,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) await decoding.fail( ["1", "a", "b", "c"], - `Expected a finite number, got NaN + `Expected a finite number at [3]` ) @@ -4545,7 +4708,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( ["a", true, "b", "1", "x"], - `Expected a finite number, got NaN + `Expected a finite number at [4]` ) await decoding.succeed(["a", true, "b", "1", "2"], ["a", true, "b", 1, 2]) @@ -4576,7 +4739,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 1, b: "" }, - `Expected number, got "" + `Expected number at ["b"]` ) }) @@ -4593,7 +4756,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: 1, [Symbol.for("b")]: 2 }) await decoding.fail( { a: 1, [Symbol.for("b")]: "c" }, - `Expected number, got "c" + `Expected number at [Symbol(b)]` ) }) @@ -4610,12 +4773,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: 1, "ab": 2 }) await decoding.fail( { a: NaN, "ab": 2 }, - `Expected a finite number, got NaN + `Expected a finite number at ["a"]` ) await decoding.fail( { a: 1, "ab": "c" }, - `Expected number, got "c" + `Expected number at ["ab"]` ) }) @@ -4638,11 +4801,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 0 }, - `Expected agt(0), got {"a":0}` + `Expected agt(0)` ) await decoding.fail( { a: 1, b: 1 }, - `Expected bgt(1), got {"a":1,"b":1}` + `Expected bgt(1)` ) }) @@ -4682,10 +4845,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(null) - await decoding.fail(undefined, `Expected string | null, got undefined`) + await decoding.fail(undefined, `Expected string | null`) await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) }) }) @@ -4698,10 +4861,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(undefined) - await decoding.fail(null, `Expected string | undefined, got null`) + await decoding.fail(null, `Expected string | undefined`) await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) }) }) @@ -4717,7 +4880,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(undefined) await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) }) }) @@ -4757,9 +4920,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", encoder.encode("foobar")) - await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5") - await decoding.fail("Zm9vYmF-", "Invalid character -") - await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end") + await decoding.fail("Zm9vY", "Expected a valid Base64 string") + await decoding.fail("Zm9vYmF-", "Expected a valid Base64 string") + await decoding.fail("=Zm9vYmF", "Expected a valid Base64 string") const encoding = asserts.encoding() await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy") @@ -4771,9 +4934,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", "foobar") - await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5") - await decoding.fail("Zm9vYmF-", "Invalid character -") - await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end") + await decoding.fail("Zm9vY", "Expected a valid Base64 string") + await decoding.fail("Zm9vYmF-", "Expected a valid Base64 string") + await decoding.fail("=Zm9vYmF", "Expected a valid Base64 string") const encoding = asserts.encoding() await encoding.succeed("foobar", "Zm9vYmFy") @@ -4785,9 +4948,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", "foobar") - await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5") + await decoding.fail("Zm9vY", "Expected a valid Base64Url string") await decoding.succeed("Pj8-ZD_Dnw", ">?>d?\u00DF") - await decoding.fail("Pj8/ZD+Dnw", "Invalid input") + await decoding.fail("Pj8/ZD+Dnw", "Expected a valid Base64Url string") const encoding = asserts.encoding() await encoding.succeed("foobar", "Zm9vYmFy") @@ -4800,9 +4963,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("67", "g") - await decoding.fail("0", "Length must be a multiple of 2, but is 1") - await decoding.fail("zd4aa", "Length must be a multiple of 2, but is 5") - await decoding.fail("0\x01", "Invalid input") + await decoding.fail("0", "Expected a valid hexadecimal string") + await decoding.fail("zd4aa", "Expected a valid hexadecimal string") + await decoding.fail("0\x01", "Expected a valid hexadecimal string") const encoding = asserts.encoding() await encoding.succeed("g", "67") @@ -4817,7 +4980,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("%D1%88%D0%B5%D0%BB%D0%BB%D1%8B", "шеллы") await decoding.succeed("hello%20world", "hello world") await decoding.succeed("hello", "hello") - await decoding.fail("%ZZ", `URI malformed`) + await decoding.fail("%ZZ", "Expected a valid URI component") const encoding = asserts.encoding() await encoding.succeed("{\"a\":1}", "%7B%22a%22%3A1%7D") @@ -4835,9 +4998,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", encoder.encode("foobar")) - await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5") + await decoding.fail("Zm9vY", "Expected a valid Base64Url string") await decoding.succeed("Pj8-ZD_Dnw", encoder.encode(">?>d?ß")) - await decoding.fail("Pj8/ZD+Dnw", "Invalid input") + await decoding.fail("Pj8/ZD+Dnw", "Expected a valid Base64Url string") const encoding = asserts.encoding() await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy") @@ -4863,9 +5026,9 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` Uint8Array.from([0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]) ) await decoding.succeed("67", encoder.encode("g")) - await decoding.fail("0", "Length must be a multiple of 2, but is 1") - await decoding.fail("2d4aa", "Length must be a multiple of 2, but is 5") - await decoding.fail("0\x01", "Invalid input") + await decoding.fail("0", "Expected a valid hexadecimal string") + await decoding.fail("2d4aa", "Expected a valid hexadecimal string") + await decoding.fail("0\x01", "Expected a valid hexadecimal string") const encoding = asserts.encoding() await encoding.succeed(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]), "0001020304050607") @@ -4881,17 +5044,13 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new Date("2021-01-01")) - await decoding.fail(null, `Expected Date, got null`) - await decoding.fail(0, `Expected Date, got 0`) - }) - - it("DateValid", async () => { - const schema = Schema.DateValid - const asserts = new TestSchema.Asserts(schema) + await decoding.fail(new Date(NaN), `Expected a valid Date`) + await decoding.fail(null, `Expected a valid Date`) + await decoding.fail(0, `Expected a valid Date`) - if (verifyGeneration) { - asserts.arbitrary().verifyGeneration() - } + const encoding = asserts.encoding() + await encoding.succeed(new Date("2021-01-01")) + await encoding.fail(new Date(NaN), `Expected a valid Date`) }) it("DateTimeUtc", async () => { @@ -4919,7 +5078,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new Date("2021-01-01T00:00:00.000Z"), DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) - await decoding.fail(new Date("invalid date"), `Expected a valid date, got Invalid Date`) + await decoding.fail(new Date("invalid date"), `Expected a valid Date`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), new Date("2021-01-01T00:00:00.000Z")) @@ -4935,8 +5094,8 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("2021-01-01T00:00:00.000Z", DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) - await decoding.fail("invalid", `Invalid UTC DateTime string: invalid`) - await decoding.fail(null, `Expected string, got null`) + await decoding.fail("invalid", "Expected a valid UTC DateTime string") + await decoding.fail(null, `Expected string`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z") @@ -4952,7 +5111,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(1609459200000, DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) - await decoding.fail(null, `Expected number, got null`) + await decoding.fail(null, `Expected number`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), 1609459200000) @@ -5033,10 +5192,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new Set(["1", "2", "3"]), new Set([1, 2, 3])) - await decoding.fail(null, `Expected ReadonlySet, got null`) + await decoding.fail(null, `Expected ReadonlySet`) await decoding.fail( new Set(["1", "2", null]), - `Expected string, got null + `Expected string at ["values"][2]` ) }) @@ -5054,10 +5213,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(HashSet.make("1", "2", "3"), HashSet.make(1, 2, 3)) - await decoding.fail(null, `Expected HashSet, got null`) + await decoding.fail(null, `Expected HashSet`) await decoding.fail( HashSet.make(null), - `Expected string, got null + `Expected string at ["values"][0]` ) @@ -5078,10 +5237,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Chunk.make("1", "2", "3"), Chunk.make(1, 2, 3)) - await decoding.fail(null, `Expected Chunk, got null`) + await decoding.fail(null, `Expected Chunk`) await decoding.fail( Chunk.make(null), - `Expected string, got null + `Expected string at ["values"][0]` ) @@ -5104,10 +5263,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new Map([["a", "1"]]), new Map([["a", 1]])) - await decoding.fail(null, `Expected ReadonlyMap, got null`) + await decoding.fail(null, `Expected ReadonlyMap`) await decoding.fail( new Map([["a", null]]), - `Expected string, got null + `Expected string at ["entries"][0][1]` ) @@ -5130,10 +5289,10 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(HashMap.make(["a", "1"]), HashMap.make(["a", 1])) - await decoding.fail(null, `Expected HashMap, got null`) + await decoding.fail(null, `Expected HashMap`) await decoding.fail( HashMap.make(["a", null]), - `Expected string, got null + `Expected string at ["entries"][0][1]` ) @@ -5169,6 +5328,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) describe("Opaque", () => { + it("returns the original schema", () => { + const schema = Schema.Struct({ a: Schema.String }) + + strictEqual(Schema.Opaque<{ readonly a: string }>()(schema), schema) + }) + it("Struct", () => { class A extends Schema.Opaque()(Schema.Struct({ a: Schema.String })) {} @@ -5198,11 +5363,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(new MyError("a")) - await decoding.fail(null, `Expected MyError, got null`) + await decoding.fail(null, `Expected MyError`) const encoding = asserts.encoding() await encoding.succeed(new MyError("a")) - await encoding.fail(null, `Expected MyError, got null`) + await encoding.fail(null, `Expected MyError`) }) }) @@ -5224,7 +5389,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("1 second", Duration.seconds(1)) await decoding.succeed("Infinity", Duration.infinity) await decoding.succeed("-Infinity", Duration.negativeInfinity) - await decoding.fail("value", "Invalid Duration string: value") + await decoding.fail("value", "Expected a valid Duration string") const encoding = asserts.encoding() await encoding.succeed(Duration.zero, "0 millis") @@ -5245,11 +5410,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(0n, Duration.zero) await decoding.succeed(1000n, Duration.nanos(1000n)) + await decoding.succeed(-1000n, Duration.nanos(-1000n)) const encoding = asserts.encoding() await encoding.succeed(Duration.millis(5), 5_000_000n) await encoding.succeed(Duration.nanos(5000n), 5000n) - await encoding.fail(Duration.infinity, "Unable to encode Infinity into a bigint") + await encoding.succeed(Duration.nanos(-5000n), -5000n) + await encoding.fail(Duration.infinity, "Expected a Duration representable as a bigint") + await encoding.fail(Duration.negativeInfinity, "Expected a Duration representable as a bigint") }) it("DurationFromMillis", async () => { @@ -5262,16 +5430,19 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(Infinity, Duration.infinity) + await decoding.succeed(-Infinity, Duration.negativeInfinity) await decoding.succeed(0, Duration.millis(0)) + await decoding.succeed(-1, Duration.millis(-1)) await decoding.succeed(1000, Duration.seconds(1)) await decoding.succeed(60 * 1000, Duration.minutes(1)) await decoding.succeed(0.1, Duration.millis(0.1)) - await decoding.fail(-1, "Expected a value greater than or equal to 0, got -1") - await decoding.fail(NaN, "Expected a value greater than or equal to 0, got NaN") + await decoding.succeed(NaN, Duration.zero) const encoding = asserts.encoding() await encoding.succeed(Duration.infinity, Infinity) + await encoding.succeed(Duration.negativeInfinity, -Infinity) await encoding.succeed(Duration.millis(NaN), 0) + await encoding.succeed(Duration.millis(-1), -1) await encoding.succeed(Duration.seconds(5), 5000) await encoding.succeed(Duration.millis(5000), 5000) await encoding.succeed(Duration.millis(0.1), 0.1) @@ -5288,7 +5459,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("123.45")) - await decoding.fail(null, `Expected BigDecimal, got null`) + await decoding.fail(null, `Expected BigDecimal`) const encoding = asserts.encoding() await encoding.succeed(BigDecimal.fromStringUnsafe("123.45")) @@ -5303,7 +5474,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(BigDecimal.fromStringUnsafe("2")) await decoding.fail( BigDecimal.fromStringUnsafe("1"), - `Expected a value greater than 1, got BigDecimal(1)` + `Expected a value greater than 1` ) }) @@ -5317,7 +5488,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(BigDecimal.fromStringUnsafe("1")) await decoding.fail( BigDecimal.fromStringUnsafe("0"), - `Expected a value greater than or equal to 1, got BigDecimal(0)` + `Expected a value greater than or equal to 1` ) }) @@ -5329,7 +5500,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(BigDecimal.fromStringUnsafe("0")) await decoding.fail( BigDecimal.fromStringUnsafe("1"), - `Expected a value less than 1, got BigDecimal(1)` + `Expected a value less than 1` ) }) @@ -5341,7 +5512,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(BigDecimal.fromStringUnsafe("1")) await decoding.fail( BigDecimal.fromStringUnsafe("2"), - `Expected a value less than or equal to 1, got BigDecimal(2)` + `Expected a value less than or equal to 1` ) }) @@ -5356,7 +5527,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(BigDecimal.fromStringUnsafe("3")) await decoding.fail( BigDecimal.fromStringUnsafe("0"), - `Expected a value between 1 and 5, got BigDecimal(0)` + `Expected a value between 1 and 5` ) }) }) @@ -5437,7 +5608,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await make.succeed({ a: 1 }, { _tag: "a", a: 1 }) await make.fail( { _tag: "c", a: 1 }, - `Expected "a", got "c" + `Expected "a" at ["_tag"]` ) @@ -5446,7 +5617,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "1" }, { _tag: "a", a: 1 }) await decoding.fail( { _tag: "c", a: 1 }, - `Expected "a", got "c" + `Expected "a" at ["_tag"]` ) @@ -5483,7 +5654,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("https://effect.website", new URL("https://effect.website")) await decoding.fail( "123", - `Invalid URL string: 123` + "Expected a valid URL string" ) const encoding = asserts.encoding() @@ -5503,7 +5674,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(`{"a":1}`, { a: 1 }) await decoding.fail( `{"a"`, - "SyntaxError: Expected ':' after property name in JSON at position 4 (line 1 column 5)" + "Expected a valid JSON string" ) const encoding = asserts.encoding() @@ -5527,6 +5698,30 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) }) + it("reviver option", async () => { + const schema = Schema.fromJsonString(Schema.Struct({ a: Schema.Number }), { + reviver: (key, value) => key === "a" ? Number(value) : value + }) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed(`{"a":"1"}`, { a: 1 }) + }) + + it("replacer and space options", async () => { + const schema = Schema.fromJsonString(Schema.Struct({ a: Schema.Number, b: Schema.Number }), { + replacer: (key, value) => key === "b" ? undefined : value, + space: 2 + }) + const encoding = new TestSchema.Asserts(schema).encoding() + + await encoding.succeed( + { a: 1, b: 2 }, + `{ + "a": 1 +}` + ) + }) + it("use case: parse / stringify a nested schema", async () => { const schema = Schema.Struct({ a: Schema.fromJsonString(Schema.Struct({ b: Schema.Number })) @@ -5587,7 +5782,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` formData.append("a", "") await decoding.fail( formData, - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["a"]` ) } @@ -5622,7 +5817,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const urlSearchParams = new URLSearchParams("a=") await decoding.fail( urlSearchParams, - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at ["a"]` ) } @@ -5647,7 +5842,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed("a") await encoding.fail( "a ", - `Expected a string with no leading or trailing whitespace, got "a "` + `Expected a string with no leading or trailing whitespace` ) }) @@ -5658,11 +5853,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` SchemaTransformation.transformOrFail({ decode: (s) => s === "a" - ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "a"` })) + ? Effect.fail(new SchemaIssue.Forbidden({ message: `input should not be "a"` })) : Effect.succeed(s), encode: (s) => s === "b" - ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "b"` })) + ? Effect.fail(new SchemaIssue.Forbidden({ message: `input should not be "b"` })) : Effect.succeed(s) }) ) @@ -5722,14 +5917,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(null, "Expected string, got null") + await decoding.fail(null, "Expected string") await decoding.fail( "ab", - `Expected a string matching template literal parts, got "ab"` + "Expected a string matching template literal parts" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) }) @@ -5742,7 +5937,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "a b", - `Expected a string matching template literal parts, got "a b"` + `Expected a string matching template literal parts` ) }) @@ -5755,7 +5950,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) }) @@ -5769,11 +5964,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( null, - "Expected string, got null" + "Expected string" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) }) @@ -5802,15 +5997,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( null, - "Expected string, got null" + "Expected string" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( "aa", - `Expected a string matching template literal parts, got "aa"` + `Expected a string matching template literal parts` ) }) @@ -5825,23 +6020,23 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( null, - "Expected string, got null" + "Expected string" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( "aa", - `Expected a string matching template literal parts, got "aa"` + `Expected a string matching template literal parts` ) await decoding.fail( "a1.2", - `Expected a string matching template literal parts, got "a1.2"` + `Expected a string matching template literal parts` ) await decoding.fail( "a+1", - `Expected a string matching template literal parts, got "a+1"` + `Expected a string matching template literal parts` ) }) @@ -5868,7 +6063,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("\na") await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) }) @@ -5891,15 +6086,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("abb") await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) await decoding.fail( "b", - `Expected a string matching template literal parts, got "b"` + `Expected a string matching template literal parts` ) const encoding = asserts.encoding() @@ -5916,11 +6111,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("acbd") await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) await decoding.fail( "b", - `Expected a string matching template literal parts, got "b"` + `Expected a string matching template literal parts` ) }) @@ -5938,7 +6133,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "_id", - `Expected a string matching template literal parts, got "_id"` + `Expected a string matching template literal parts` ) }) @@ -5950,7 +6145,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("a0") await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) }) @@ -5962,7 +6157,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("a1") await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) }) @@ -5975,7 +6170,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("aa") await decoding.fail( "b", - `Expected a string matching template literal parts, got "b"` + `Expected a string matching template literal parts` ) }) @@ -5992,7 +6187,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("10.1") await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) }) @@ -6009,7 +6204,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("ca bd") await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) }) @@ -6022,7 +6217,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("

") await decoding.fail( "

", - `Expected a string matching template literal parts, got "

"` + `Expected a string matching template literal parts` ) }) @@ -6034,15 +6229,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("ab") await decoding.fail( null, - "Expected string, got null" + "Expected string" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( "a", - `Expected a string matching template literal parts, got "a"` + `Expected a string matching template literal parts` ) }) @@ -6056,15 +6251,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( null, - "Expected string, got null" + "Expected string" ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( "ab", - `Expected a finite number, got NaN + `Expected a finite number at [1]` ) }) @@ -6077,6 +6272,27 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` deepStrictEqual(schema.parts, parts) }) + it("preserves greedy segmentation at repeated literal anchors", async () => { + const schema = Schema.TemplateLiteralParser([Schema.String, ":", Schema.String]) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed("a:b:c", ["a:b", ":", "c"]) + }) + + it("backtracks from an invalid literal anchor", async () => { + const schema = Schema.TemplateLiteralParser([Schema.String, ":", Schema.NonEmptyString, "x"]) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed("a:b:x", ["a", ":", "b:", "x"]) + }) + + it("backtracks across an empty literal anchor", async () => { + const schema = Schema.TemplateLiteralParser([Schema.String, "", Schema.NonEmptyString]) + const decoding = new TestSchema.Asserts(schema).decoding() + + await decoding.succeed("a", ["", "", "a"]) + }) + it(`NonEmptyString + String`, async () => { const schema = Schema.TemplateLiteralParser([Schema.NonEmptyString, Schema.String]) const asserts = new TestSchema.Asserts(schema) @@ -6093,15 +6309,15 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("a", ["a"]) await decoding.fail( "ab", - `Expected a string matching template literal parts, got "ab"` + `Expected a string matching template literal parts` ) await decoding.fail( "", - `Expected a string matching template literal parts, got ""` + `Expected a string matching template literal parts` ) await decoding.fail( null, - "Expected string, got null" + "Expected string" ) }) @@ -6114,7 +6330,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "a b", - `Expected a string matching template literal parts, got "a b"` + `Expected a string matching template literal parts` ) }) @@ -6126,14 +6342,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("1a", [1, "a"]) await decoding.fail( "1.1a", - `Expected a string matching template literal parts, got "1.1a"` + `Expected a string matching template literal parts` ) const encoding = asserts.encoding() await encoding.succeed([1, "a"], "1a") await encoding.fail( [1.1, "a"], - `Expected an integer, got 1.1 + `Expected an integer at [0]` ) }) @@ -6155,7 +6371,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("100ab23a", [100, "a", "b23a"]) await decoding.fail( "-ab", - `Expected a finite number, got NaN + `Expected a finite number at [0]` ) @@ -6163,7 +6379,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed([100, "a", "b"], "100ab") await encoding.fail( [100, "a", ""], - `Expected a value with a length of at least 1, got "" + `Expected a value with a length of at least 1 at [2]` ) }) @@ -6189,12 +6405,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("ced", ["c", "e", "d"]) await decoding.fail( "cabd", - `Expected a string matching template literal parts, got "ab" + `Expected a string matching template literal parts at [1]` ) await decoding.fail( "ed", - `Expected a string matching template literal parts, got "ed"` + `Expected a string matching template literal parts` ) }) @@ -6214,12 +6430,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("ca1bd", ["c", ["a", 1, "b"], "d"]) await decoding.fail( "ca1.1bd", - `Expected a string matching template literal parts, got "a1.1b" + `Expected a string matching template literal parts at [1]` ) await decoding.fail( "ca-bd", - `Expected a string matching template literal parts, got "a-b" + `Expected a string matching template literal parts at [1]` ) }) @@ -6233,7 +6449,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("

", ["<", "h2", ">"]) await decoding.fail( "

", - `Expected a string matching template literal parts, got "

"` + `Expected a string matching template literal parts` ) }) @@ -6250,7 +6466,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed("

", ["<", ["h", 2], ">"]) await decoding.fail( "

", - `Expected a string matching template literal parts, got "h3" + `Expected a string matching template literal parts at [1]` ) }) @@ -6264,6 +6480,85 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` deepStrictEqual(Effect.runSync(A.makeEffect()), new A()) }) + it("decoding validates the class struct once", () => { + let checks = 0 + const schema = Schema.Struct({ + a: Schema.String + }).check(Schema.makeFilter(() => { + checks++ + return true + })) + class A extends Schema.Class("A")(schema) {} + + const instance = Schema.decodeUnknownSync(A)({ a: "a" }) + + assertTrue(instance instanceof A) + strictEqual(checks, 1) + }) + + it("make validates an existing nested Class once", () => { + let checks = 0 + class A extends Schema.Class("A")({ a: Schema.String }) {} + const schema = Schema.Struct({ + a: A.check(Schema.makeFilter(() => { + checks++ + return true + })) + }) + const instance = A.make({ a: "a" }) + + strictEqual(schema.make({ a: instance }).a, instance) + strictEqual(checks, 1) + }) + + it("make validates a nested Class source and output once", () => { + let sourceChecks = 0 + let classChecks = 0 + class A extends Schema.Class("A")( + Schema.Struct({ a: Schema.String }).check(Schema.makeFilter(() => { + sourceChecks++ + return true + })) + ) {} + const schema = Schema.Struct({ + a: A.check(Schema.makeFilter(() => { + classChecks++ + return true + })) + }) + + assertTrue(schema.make({ a: { a: "a" } }).a instanceof A) + strictEqual(sourceChecks, 1) + strictEqual(classChecks, 1) + }) + + it("make allows an optional nested Class to be omitted", () => { + class A extends Schema.Class("A")({ a: Schema.String }) {} + const schema = Schema.Struct({ a: Schema.optionalKey(A) }) + + deepStrictEqual(schema.make({}), {}) + }) + + it("make applies constructor defaults only at a field occurrence", () => { + let defaults = 0 + const defaulted = Schema.String.pipe( + Schema.withConstructorDefault(Effect.sync(() => { + defaults++ + return "default" + })) + ) + const field = Schema.Struct({ value: defaulted }) + const unionMember = Schema.Struct({ value: Schema.Union([defaulted, Schema.Number]) }) + + deepStrictEqual(defaulted.makeOption(undefined as any), Option.none()) + strictEqual(defaults, 0) + deepStrictEqual(field.make({}), { value: "default" }) + strictEqual(defaults, 1) + deepStrictEqual(unionMember.makeOption({} as any), Option.none()) + deepStrictEqual(unionMember.makeOption({ value: undefined } as any), Option.none()) + strictEqual(defaults, 1) + }) + it("suspend before initialization", async () => { const schema = Schema.suspend(() => string) class A extends Schema.Class("A")(Schema.Struct({ a: schema })) {} @@ -6367,6 +6662,35 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await make.succeed({}, new B({ a: new A({ a: "default" }) })) }) + it("make preserves Class instances in Array(Class) and Array(Union(Class))", () => { + class Row extends Schema.Class("Row")({ value: Schema.String }) {} + class DirectTable extends Schema.Class("DirectTable")({ rows: Schema.Array(Row) }) {} + class UnionTable extends Schema.Class("UnionTable")({ rows: Schema.Array(Schema.Union([Row])) }) {} + const row = Row.make({ value: "a" }) + + strictEqual(DirectTable.make({ rows: [row] }).rows[0], row) + strictEqual(UnionTable.make({ rows: [row] }).rows[0], row) + deepStrictEqual(DirectTable.makeOption({ rows: [{ value: 1 } as any] }), Option.none()) + deepStrictEqual(UnionTable.makeOption({ rows: [{ value: 1 } as any] }), Option.none()) + }) + + it("make constructs nested Class instances with and without Union", () => { + class A extends Schema.Class("A")({ a: Schema.String }) {} + const direct = Schema.Struct({ a: A }) + const union = Schema.Struct({ a: Schema.Union([A]) }) + + assertTrue(direct.make({ a: { a: "a" } }).a instanceof A) + assertTrue(union.make({ a: { a: "a" } }).a instanceof A) + }) + + it("make selects the first TaggedClass Union member when the tag is defaulted", () => { + class A extends Schema.TaggedClass()("A", { a: Schema.String }) {} + class B extends Schema.TaggedClass()("B", { a: Schema.String }) {} + const schema = Schema.Union([A, B]) + + assertTrue(schema.make({ a: "a" } as any) instanceof A) + }) + it("should be possible to define a class with a mutable field", async () => { class A extends Schema.Class("A")({ a: Schema.mutableKey(Schema.String) @@ -6422,11 +6746,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "a" }, new A({ a: "a" })) await decoding.fail( null, - `Expected object, got null` + `Expected object` ) await decoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) @@ -6434,11 +6758,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(new A({ a: "a" }), { a: "a" }) await encoding.fail( null, - "Expected A, got null" + "Expected A" ) await encoding.fail( { a: "a" }, - `Expected A, got {"a":"a"}` + `Expected A` ) }) @@ -6482,7 +6806,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "a" }, new A({ a: "a" })) await decoding.fail( { a: 1 }, - `Expected string, got 1 + `Expected string at ["a"]` ) @@ -6490,11 +6814,11 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(new A({ a: "a" }), { a: "a" }) await encoding.fail( null, - "Expected A, got null" + "Expected A" ) await encoding.fail( { a: "a" }, - `Expected A, got {"a":"a"}` + `Expected A` ) }) @@ -6583,6 +6907,33 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) }) + it("decoding validates the extended struct once", () => { + let baseChecks = 0 + let extensionChecks = 0 + class A extends Schema.Class("A")( + Schema.Struct({ + a: Schema.String + }).check(Schema.makeFilter(() => { + baseChecks++ + return true + })) + ) {} + class B extends A.extend("B")( + Schema.Struct({ + b: Schema.Number + }).check(Schema.makeFilter(() => { + extensionChecks++ + return true + })) + ) {} + + const instance = Schema.decodeUnknownSync(B)({ a: "a", b: 1 }) + + assertTrue(instance instanceof B) + strictEqual(baseChecks, 1) + strictEqual(extensionChecks, 1) + }) + it("constructor preserves subclass fields while ignoring excess properties by default", () => { class A extends Schema.Class("A")({ a: Schema.String @@ -6652,8 +7003,8 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const make = asserts.make() await make.succeed({ a: 1, b: 1 }, new B({ a: 1, b: 1 })) - await make.fail({ a: 0, b: 1 }, `Expected positive a, got {"a":0,"b":1}`) - await make.fail({ a: 1, b: 0 }, `Expected positive b, got {"a":1,"b":0}`) + await make.fail({ a: 0, b: 1 }, `Expected positive a`) + await make.fail({ a: 1, b: 0 }, `Expected positive b`) }) it("static members", async () => { @@ -6735,7 +7086,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const make = asserts.make() await make.succeed({ a: "a" }, new A({ a: "a" })) - await make.fail({ a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`) + await make.fail({ a: "" }, `Expected "a" being longer than 0`) const decoding = asserts.decoding() await decoding.succeed({ _tag: "A", a: "a" }, new A({ a: "a" })) @@ -6744,7 +7095,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` `Missing key at ["_tag"]` ) - await decoding.fail({ _tag: "A", a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`) + await decoding.fail({ _tag: "A", a: "" }, `Expected "a" being longer than 0`) }) it("extended constructor does not treat subclass fields as excess properties", () => { @@ -6778,16 +7129,16 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) }) - describe("ErrorClass", () => { + describe("Error", () => { it("make with void input", () => { - class E extends Schema.ErrorClass("E")({}) {} + class E extends Schema.Error("E")({}) {} deepStrictEqual(E.make(), new E()) deepStrictEqual(E.makeOption(), Option.some(new E())) deepStrictEqual(Effect.runSync(E.makeEffect()), new E()) }) it("fields argument", async () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ id: Schema.Number }) {} const asserts = new TestSchema.Asserts(E) @@ -6804,7 +7155,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("constructor ignores excess properties by default", () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ message: Schema.String, cause: Schema.optionalKey(Schema.Unknown), code: Schema.Number @@ -6820,7 +7171,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("constructor preserves excess properties when requested", () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ message: Schema.String, code: Schema.Number }) {} @@ -6835,7 +7186,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("Struct argument", async () => { - class E extends Schema.ErrorClass("E")(Schema.Struct({ + class E extends Schema.Error("E")(Schema.Struct({ id: Schema.Number })) {} const asserts = new TestSchema.Asserts(E) @@ -6856,7 +7207,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extend", async () => { - class A extends Schema.ErrorClass("A")({ + class A extends Schema.Error("A")({ a: Schema.String }) { readonly _a = 1 @@ -6892,7 +7243,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extended constructor ignores excess properties by default", () => { - class A extends Schema.ErrorClass("A")({ + class A extends Schema.Error("A")({ message: Schema.String }) {} class B extends A.extend("B")({ @@ -6907,7 +7258,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extended constructor does not treat subclass fields as excess properties", () => { - class A extends Schema.ErrorClass("A")({ + class A extends Schema.Error("A")({ message: Schema.String }) {} class B extends A.extend("B")({ @@ -6923,7 +7274,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("`toString` to match native `Error` output format", async () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ message: Schema.String }) {} const err = new E({ message: "my message" }) @@ -6931,16 +7282,16 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) }) - describe("TaggedErrorClass", () => { + describe("TaggedError", () => { it("make with void input", () => { - class E extends Schema.TaggedErrorClass()("E", {}) {} + class E extends Schema.TaggedError()("E", {}) {} deepStrictEqual(E.make(), new E()) deepStrictEqual(E.makeOption(), Option.some(new E())) deepStrictEqual(Effect.runSync(E.makeEffect()), new E()) }) it("fields argument", async () => { - class E extends Schema.TaggedErrorClass()("E", { + class E extends Schema.TaggedError()("E", { id: Schema.Number }) {} const asserts = new TestSchema.Asserts(E) @@ -6962,7 +7313,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("constructor ignores excess properties by default", () => { - class E extends Schema.TaggedErrorClass()("E", { + class E extends Schema.TaggedError()("E", { id: Schema.Number }) {} @@ -6974,7 +7325,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("Struct argument", async () => { - class E extends Schema.TaggedErrorClass()( + class E extends Schema.TaggedError()( "E", Schema.Struct({ id: Schema.Number @@ -6988,7 +7339,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("name matches tag", () => { - class E extends Schema.TaggedErrorClass()("TaggedErrorName", { + class E extends Schema.TaggedError()("TaggedErrorName", { id: Schema.Number }) {} @@ -6997,7 +7348,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("name matches identifier", () => { - class E extends Schema.TaggedErrorClass("A")("B", { + class E extends Schema.TaggedError("A")("B", { a: Schema.Number }) {} @@ -7006,7 +7357,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("name matches identifier after extend", () => { - class E extends Schema.TaggedErrorClass("A")("B", { + class E extends Schema.TaggedError("A")("B", { a: Schema.Number }) {} class E2 extends E.extend("C")({ @@ -7017,8 +7368,8 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` strictEqual(err.name, "C") }) - it("zero-field TaggedErrorClass allows omitting props argument", () => { - class NotFoundError extends Schema.TaggedErrorClass()("NotFoundError", {}) {} + it("zero-field TaggedError allows omitting props argument", () => { + class NotFoundError extends Schema.TaggedError()("NotFoundError", {}) {} // new NotFoundError() should work without passing {} const a = new NotFoundError() @@ -7032,7 +7383,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extend", async () => { - class A extends Schema.TaggedErrorClass()("A", { + class A extends Schema.TaggedError()("A", { a: Schema.String }) {} class B extends A.extend("B")({ @@ -7045,7 +7396,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extended constructor ignores excess properties by default", () => { - class A extends Schema.TaggedErrorClass()("A", { + class A extends Schema.TaggedError()("A", { a: Schema.String }) {} class B extends A.extend("B")({ @@ -7061,7 +7412,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` }) it("extended constructor does not treat subclass fields as excess properties", () => { - class A extends Schema.TaggedErrorClass()("A", { + class A extends Schema.TaggedError()("A", { a: Schema.String }) {} class B extends A.extend("B")({ @@ -7105,7 +7456,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( 3, - `Expected 0 | 1, got 3` + `Expected 0 | 1` ) const encoding = asserts.encoding() @@ -7132,7 +7483,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "Cantaloupe", - `Expected "apple" | "banana" | 0, got "Cantaloupe"` + `Expected "apple" | "banana" | 0` ) const encoding = asserts.encoding() @@ -7157,7 +7508,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.fail( "Cantaloupe", - `Expected "apple" | "banana" | 3, got "Cantaloupe"` + `Expected "apple" | "banana" | 3` ) const encoding = asserts.encoding() @@ -7178,14 +7529,14 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(null, "b") await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( null, - "Expected string, got null" + "Expected string" ) }) @@ -7240,14 +7591,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.succeed("a", 0) - await decoding.fail(null, "Expected string, got null") + await decoding.fail(null, "Expected string") }) it("forced failure", async () => { const schema = Schema.String.pipe( - Schema.middlewareDecoding(() => - Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" })) - ) + Schema.middlewareDecoding(() => Effect.fail(new SchemaIssue.Forbidden({ message: "my message" }))) ) const asserts = new TestSchema.Asserts(schema) @@ -7269,7 +7618,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed(1) await decoding.fail( 1.2, - `Expected an integer, got 1.2` + `Expected an integer` ) const encoding = asserts.encoding() @@ -7277,7 +7626,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(null, 0) await encoding.fail( 1.2, - `Expected an integer, got 1.2` + `Expected an integer` ) }) @@ -7291,7 +7640,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(null, 0) await encoding.fail( 1.2, - `Expected an integer, got 1.2` + `Expected an integer` ) }) }) @@ -7318,7 +7667,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed(null, 0) await encoding.fail( 1.2, - `Expected an integer, got 1.2` + `Expected an integer` ) }) @@ -7343,14 +7692,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.succeed(NaN, "b") - await encoding.fail(null, "Expected number, got null") + await encoding.fail(null, "Expected number") }) it("forced failure", async () => { const schema = Schema.String.pipe( - Schema.middlewareEncoding(() => - Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" })) - ) + Schema.middlewareEncoding(() => Effect.fail(new SchemaIssue.Forbidden({ message: "my message" }))) ) const asserts = new TestSchema.Asserts(schema) @@ -7540,12 +7887,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" }) await encoding.fail( { a: 1, b: NaN }, - `Expected a finite number, got NaN + `Expected a finite number at ["b"]` ) await encoding.fail( { a: 1, b: undefined }, - `Expected number, got undefined + `Expected number at ["b"]` ) }) @@ -7568,7 +7915,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await decoding.succeed({ a: "1", b: "2" }, { a: 1, b: 2 }) await decoding.fail( { a: "1", b: null }, - `Expected string, got null + `Expected string at ["b"]` ) @@ -7576,12 +7923,12 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" }) await encoding.fail( { a: 1, b: NaN }, - `Expected a finite number, got NaN + `Expected a finite number at ["b"]` ) await encoding.fail( { a: 1, b: undefined }, - `Expected number, got undefined + `Expected number at ["b"]` ) }) @@ -7593,7 +7940,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` decode: SchemaGetter.checkEffect((s) => Effect.gen(function*() { if (s.length === 0) { - return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" }) + return new SchemaIssue.InvalidValue({ message: "input should not be empty string" }) } }).pipe(Effect.delay(100)) ), @@ -7619,7 +7966,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` Effect.gen(function*() { yield* Service if (s.length === 0) { - return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" }) + return new SchemaIssue.InvalidValue({ message: "input should not be empty string" }) } }) ), @@ -7661,8 +8008,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` Schema.asserts(schema, "a") fail("Expected asserts to throw an error") } catch (e) { - ok(e instanceof Error) - strictEqual(e.message, `Expected number, got "a"`) + assertSchemaIssueError(e, "Expected number") } }) }) @@ -7681,7 +8027,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const r2 = await decodeUnknownPromise(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r2)) assertTrue(Schema.isSchemaError(r2.failure)) - strictEqual(r2.failure.message, "Expected string, got null") + strictEqual(r2.failure.message, "Expected string") const r3 = await encodeUnknownPromise(1).then(Result.succeed, Result.fail) deepStrictEqual(r3, Result.succeed("1")) @@ -7689,22 +8035,20 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const r4 = await encodeUnknownPromise(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r4)) assertTrue(Schema.isSchemaError(r4.failure)) - strictEqual(r4.failure.message, "Expected number, got null") + strictEqual(r4.failure.message, "Expected number") const r5 = await decodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r5)) - assertTrue(r5.failure instanceof Error) - strictEqual(r5.failure.message, "Expected string, got null") + assertSchemaIssueError(r5.failure, "Expected string") const r6 = await encodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r6)) - assertTrue(r6.failure instanceof Error) - strictEqual(r6.failure.message, "Expected number, got null") + assertSchemaIssueError(r6.failure, "Expected number") }) it("should reject with an error when the cause contains both a schema issue and a defect", async () => { const cause = Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ @@ -7773,7 +8117,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ @@ -7811,7 +8155,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const r2 = decodeUnknownResult(null) assertTrue(Result.isFailure(r2)) assertTrue(Schema.isSchemaError(r2.failure)) - strictEqual(r2.failure.message, "Expected string, got null") + strictEqual(r2.failure.message, "Expected string") const r3 = encodeUnknownResult(1) assertTrue(Result.isSuccess(r3)) @@ -7820,22 +8164,22 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const r4 = encodeUnknownResult(null) assertTrue(Result.isFailure(r4)) assertTrue(Schema.isSchemaError(r4.failure)) - strictEqual(r4.failure.message, "Expected number, got null") + strictEqual(r4.failure.message, "Expected number") const r5 = SchemaParser.decodeUnknownResult(schema)(null) assertTrue(Result.isFailure(r5)) assertTrue(SchemaIssue.isIssue(r5.failure)) - strictEqual(r5.failure.toString(), "Expected string, got null") + strictEqual(formatIssue(r5.failure), "Expected string") const r6 = SchemaParser.encodeUnknownResult(schema)(null) assertTrue(Result.isFailure(r6)) assertTrue(SchemaIssue.isIssue(r6.failure)) - strictEqual(r6.failure.toString(), "Expected number, got null") + strictEqual(formatIssue(r6.failure), "Expected number") }) it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ @@ -7869,30 +8213,26 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` throws(() => Schema.decodeUnknownSync(schema)(null), (e) => { assertTrue(Schema.isSchemaError(e)) - strictEqual(e.message, "Expected string, got null") + strictEqual(e.message, "Expected string") }) throws(() => Schema.encodeUnknownSync(schema)(null), (e) => { assertTrue(Schema.isSchemaError(e)) - strictEqual(e.message, "Expected number, got null") + strictEqual(e.message, "Expected number") }) throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => { - assertTrue(e instanceof Error) - assertTrue(SchemaIssue.isIssue(e.cause)) - strictEqual(e.cause.toString(), "Expected string, got null") + assertSchemaIssueError(e, "Expected string") }) throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => { - assertTrue(e instanceof Error) - assertTrue(SchemaIssue.isIssue(e.cause)) - strictEqual(e.cause.toString(), "Expected number, got null") + assertSchemaIssueError(e, "Expected number") }) }) it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ @@ -7987,7 +8327,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` describe("decodeUnknownExit / encodeUnknownExit", () => { it("should preserve mixed schema issue and defect causes", () => { const cause = Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ @@ -8479,7 +8819,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` const decoding = asserts.decoding() await decoding.fail( "a", - `Expected , got "a"` + `Expected ` ) }) @@ -8496,7 +8836,7 @@ Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` describe("returns issue", () => { it("abort: false", async () => { const schema = Schema.String.check( - Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), { + Schema.makeFilter(() => new SchemaIssue.InvalidValue({ message: "error message 1" }), { title: "filter title 1" }), Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" }) @@ -8513,7 +8853,7 @@ error message 2` it("abort: true", async () => { const schema = Schema.String.check( - Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), { + Schema.makeFilter(() => new SchemaIssue.InvalidValue({ message: "error message 1" }), { title: "filter title 1" }, true), Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" }) @@ -8566,7 +8906,7 @@ error message 2` it("issue: Issue", async () => { const schema = Schema.String.check( Schema.makeFilter( - (s) => ({ path: ["a"], issue: new SchemaIssue.InvalidValue(Option.some(s), { message: "custom issue" }) }), + () => ({ path: ["a"], issue: new SchemaIssue.InvalidValue({ message: "custom issue" }) }), { title: "filter title" } ) ) @@ -8622,9 +8962,9 @@ error message 2 it("array mixing string, Issue, and { path, issue }", async () => { const schema = Schema.String.check( - Schema.makeFilter((s) => [ + Schema.makeFilter(() => [ "top-level message", - new SchemaIssue.InvalidValue(Option.some(s), { message: "direct issue" }), + new SchemaIssue.InvalidValue({ message: "direct issue" }), { path: ["a"], issue: "pointed message" } ], { title: "filter title" }) ) @@ -8910,7 +9250,7 @@ pointed message await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( { a: undefined }, - `Expected string, got undefined + `Expected string at ["a"]` ) }) @@ -8951,7 +9291,7 @@ pointed message await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) await decoding.fail( { a: { b: undefined } }, - `Expected string, got undefined + `Expected string at ["a"]["b"]` ) }) @@ -8960,7 +9300,7 @@ pointed message const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey( Effect.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "decoding default failed" })) ) )) }) @@ -8978,7 +9318,7 @@ pointed message it("Effect failing with SchemaError and a defect preserves the mixed cause", () => { const cause = Cause.combine( Cause.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "decoding default failed" })) ), Cause.die(new Error("defect")) ) @@ -9079,7 +9419,7 @@ pointed message it("Effect failing with SchemaError and a defect preserves the mixed cause", () => { const cause = Cause.combine( Cause.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "decoding default failed" })) ), Cause.die(new Error("defect")) ) @@ -9132,7 +9472,7 @@ pointed message await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( { a: undefined }, - `Expected string, got undefined + `Expected string at ["a"]` ) }) @@ -9173,7 +9513,7 @@ pointed message await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) await decoding.fail( { a: { b: undefined } }, - `Expected string, got undefined + `Expected string at ["a"]["b"]` ) }) @@ -9182,7 +9522,7 @@ pointed message const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey( Effect.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "decoding default failed" })) ) )) }) @@ -9280,7 +9620,7 @@ pointed message const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType( Effect.fail( - new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) + new Schema.SchemaError(new SchemaIssue.InvalidValue({ message: "decoding default failed" })) ) )) }) @@ -9323,14 +9663,14 @@ pointed message await decoding.succeed("a") await decoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "", - `Expected a value with a length of at least 1, got ""` + `Expected a value with a length of at least 1` ) }) @@ -9346,14 +9686,14 @@ pointed message await decoding.succeed("a") await decoding.fail( "ab", - `Expected a value with a length of 1, got "ab"` + `Expected a value with a length of 1` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "ab", - `Expected a value with a length of 1, got "ab"` + `Expected a value with a length of 1` ) }) @@ -9369,26 +9709,54 @@ pointed message await decoding.succeed(1) await decoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` ) await decoding.fail( NaN, - `Expected an integer, got NaN` + `Expected an integer` ) await decoding.fail( Infinity, - `Expected an integer, got Infinity` + `Expected an integer` ) await decoding.fail( -Infinity, - `Expected an integer, got -Infinity` + `Expected an integer` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, - `Expected an integer, got 1.1` + `Expected an integer` + ) + }) + + it("Natural", async () => { + const schema = Schema.Natural + const asserts = new TestSchema.Asserts(schema) + + if (verifyGeneration) { + asserts.arbitrary().verifyGeneration() + } + + const decoding = asserts.decoding() + await decoding.succeed(0) + await decoding.succeed(1) + await decoding.fail( + -1, + `Expected a value greater than or equal to 0` + ) + await decoding.fail( + 1.1, + `Expected an integer` + ) + + const encoding = asserts.encoding() + await encoding.succeed(0) + await encoding.fail( + -1, + `Expected a value greater than or equal to 0` ) }) @@ -9412,7 +9780,7 @@ pointed message await encoding.succeed("Abc") await encoding.fail( "abc", - `Expected a string with the first character in uppercase, got "abc"` + `Expected a string with the first character in uppercase` ) }) @@ -9436,7 +9804,7 @@ pointed message await encoding.succeed("abc") await encoding.fail( "Abc", - `Expected a string with the first character in lowercase, got "Abc"` + `Expected a string with the first character in lowercase` ) }) @@ -9460,7 +9828,7 @@ pointed message await encoding.succeed("abc") await encoding.fail( "ABC", - `Expected a string with all characters in lowercase, got "ABC"` + `Expected a string with all characters in lowercase` ) }) @@ -9484,7 +9852,7 @@ pointed message await encoding.succeed("ABC") await encoding.fail( "abc", - `Expected a string with all characters in uppercase, got "abc"` + `Expected a string with all characters in uppercase` ) }) }) @@ -9499,11 +9867,11 @@ describe("Getter", () => { const decoding = asserts.decoding() await decoding.succeed(0, "a") - await decoding.fail(1, `Expected 0, got 1`) + await decoding.fail(1, `Expected 0`) const encoding = asserts.encoding() await encoding.succeed("a", 0) - await encoding.fail("b", `Expected "a", got "b"`) + await encoding.fail("b", `Expected "a"`) }) }) @@ -9511,27 +9879,27 @@ describe("Check", () => { it("isStringFinite", async () => { const schema = Schema.String.check(Schema.isStringFinite()) - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isStringFinite", - regExp: /^[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isStringFinite", + payload: null }) }) it("isStringBigInt", async () => { const schema = Schema.String.check(Schema.isStringBigInt()) - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isStringBigInt", - regExp: /^-?\d+$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isStringBigInt", + payload: null }) }) it("isStringSymbol", async () => { const schema = Schema.String.check(Schema.isStringSymbol()) - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isStringSymbol", - regExp: /^Symbol\((.*)\)$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isStringSymbol", + payload: null }) }) @@ -9543,11 +9911,9 @@ describe("Check", () => { asserts.arbitrary().verifyGeneration() } - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isUUID", - regExp: - /^([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|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/, - version: undefined + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isUUID", + payload: { version: null } }) const decoding = asserts.decoding() @@ -9557,7 +9923,7 @@ describe("Check", () => { await decoding.succeed("00000000-0000-4000-8000-000000000001") await decoding.fail( "00000000-0000-0000-0000-000000000001", - `Expected a UUID, got "00000000-0000-0000-0000-000000000001"` + `Expected a UUID` ) }) @@ -9569,11 +9935,11 @@ describe("Check", () => { await decoding.succeed("00000000-0000-4000-8000-000000000001") await decoding.fail( "00000000-0000-0000-0000-000000000000", - `Expected a UUID v4, got "00000000-0000-0000-0000-000000000000"` + `Expected a UUID v4` ) await decoding.fail( "ffffffff-ffff-ffff-ffff-ffffffffffff", - `Expected a UUID v4, got "ffffffff-ffff-ffff-ffff-ffffffffffff"` + `Expected a UUID v4` ) }) @@ -9585,9 +9951,9 @@ describe("Check", () => { asserts.arbitrary().verifyGeneration() } - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isGUID", - regExp: /^([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})$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isGUID", + payload: null }) const decoding = asserts.decoding() @@ -9595,7 +9961,7 @@ describe("Check", () => { await decoding.succeed("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") await decoding.fail( "not-a-guid", - `Expected a GUID, got "not-a-guid"` + `Expected a GUID` ) }) @@ -9607,34 +9973,34 @@ describe("Check", () => { asserts.arbitrary().verifyGeneration() } - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isULID", - regExp: /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isULID", + payload: null }) const decoding = asserts.decoding() await decoding.succeed("01H4PGGGJVN2DKP2K1H7EH996V") await decoding.fail( "", - `Expected a string matching the RegExp ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$, got ""` + `Expected a string matching the RegExp ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$` ) }) it("isBase64", async () => { const schema = Schema.String.check(Schema.isBase64()) - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isBase64", - regExp: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isBase64", + payload: null }) }) it("isBase64Url", async () => { const schema = Schema.String.check(Schema.isBase64Url()) - deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { - _tag: "isBase64Url", - regExp: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/ + deepStrictEqual(Schema.resolveAnnotations(schema)?.representation, { + id: "effect/schema/isBase64Url", + payload: null }) }) @@ -9662,7 +10028,7 @@ describe("Check", () => { const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(1, `Expected string, got 1`) + await decoding.fail(1, `Expected string`) deepStrictEqual(schema.ast.annotations?.brands, ["a"]) }) @@ -9676,8 +10042,8 @@ describe("Check", () => { const decoding = asserts.decoding() await decoding.succeed(1) - await decoding.fail("a", `Expected number, got "a"`) - await decoding.fail(1.2, `Expected an integer, got 1.2`) + await decoding.fail("a", `Expected number`) + await decoding.fail(1.2, `Expected an integer`) deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["Int"]) }) @@ -9696,9 +10062,9 @@ describe("Check", () => { const decoding = asserts.decoding() await decoding.succeed(1) - await decoding.fail("a", `Expected number, got "a"`) - await decoding.fail(1.2, `Expected an integer, got 1.2`) - await decoding.fail(-1, `Expected a value greater than 0, got -1`) + await decoding.fail("a", `Expected number`) + await decoding.fail(1.2, `Expected an integer`) + await decoding.fail(-1, `Expected a value greater than 0`) deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["PositiveInt"]) }) @@ -9721,7 +10087,7 @@ describe("Check", () => { ) await decoding.fail( { a: "a", b: undefined }, - `Expected number, got undefined + `Expected number at ["b"]` ) await decoding.fail( @@ -9773,7 +10139,7 @@ describe("Check", () => { ) await decoding.fail( { a: "a", b: undefined }, - `Expected number, got undefined + `Expected number at ["b"]` ) await decoding.fail( @@ -9783,7 +10149,7 @@ describe("Check", () => { ) await decoding.fail( { a: undefined, b: 1 }, - `Expected string, got undefined + `Expected string at ["a"]` ) }) @@ -9819,15 +10185,33 @@ Missing key ) }) - describe("asClass", () => { + describe("class extension", () => { it("wrapping a primitive schema", () => { - class A extends Schema.asClass(Schema.String) {} + class A extends Schema.String {} strictEqual(Schema.decodeUnknownSync(A)("a"), "a") }) + it("inherits the schema protocol", () => { + class A extends Schema.String {} + + assertTrue(Schema.isSchema(A)) + strictEqual(A.make("a"), "a") + + const annotated = A.annotate({ title: "A" }) + assertTrue(Schema.isSchema(annotated)) + strictEqual(Schema.resolveAnnotations(annotated)?.title, "A") + }) + + it("extending a rebuilt schema", () => { + class A extends Schema.Struct({ name: Schema.String }).annotate({ title: "A" }) {} + + deepStrictEqual(Schema.decodeUnknownSync(A)({ name: "a" }), { name: "a" }) + strictEqual(Schema.resolveAnnotations(A)?.title, "A") + }) + it("static getter using this", () => { - class A extends Schema.asClass(Schema.String) { + class A extends Schema.String { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } @@ -9837,7 +10221,7 @@ Missing key }) it("static property", () => { - class A extends Schema.asClass(Schema.String) { + class A extends Schema.String { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) } @@ -9845,7 +10229,7 @@ Missing key }) it("static property using Schema.suspend", () => { - class A extends Schema.asClass(Schema.String) { + class A extends Schema.String { static readonly decodeUnknownSync = Schema.decodeUnknownSync(Schema.suspend(() => this)) } @@ -9856,7 +10240,7 @@ Missing key const struct = Schema.Struct({ name: Schema.String }) - class A extends Schema.asClass(struct) { + class A extends struct { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } @@ -9866,8 +10250,17 @@ Missing key strictEqual(A.fields, struct.fields) }) - it("subclassing (double wrap)", () => { - class A extends Schema.asClass(Schema.FiniteFromString) { + it("does not create class instances", () => { + class A extends Schema.Struct({ name: Schema.String }) {} + + const value = A.make({ name: "a" }) + + assertFalse(value instanceof A) + deepStrictEqual(value, { name: "a" }) + }) + + it("subclassing", () => { + class A extends Schema.FiniteFromString { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } diff --git a/.context/effect/packages/effect/test/schema/SchemaAST.test.ts b/.context/effect/packages/effect/test/schema/SchemaAST.test.ts index 0c6e88cbc..014944ba1 100644 --- a/.context/effect/packages/effect/test/schema/SchemaAST.test.ts +++ b/.context/effect/packages/effect/test/schema/SchemaAST.test.ts @@ -19,8 +19,20 @@ describe("SchemaAST", () => { strictEqual(SchemaAST.isJson(Symbol.for("symbol")), false) strictEqual(SchemaAST.isJson([]), true) strictEqual(SchemaAST.isJson([1]), true) + strictEqual(SchemaAST.isJson(new Array(1)), false) strictEqual(SchemaAST.isJson([1, undefined]), false) strictEqual(SchemaAST.isJson([1, 1n]), false) + let getterCalls = 0 + const arrayWithGetter = [0] + Object.defineProperty(arrayWithGetter, "0", { + enumerable: true, + get() { + getterCalls++ + return 1 + } + }) + strictEqual(SchemaAST.isJson(arrayWithGetter), true) + strictEqual(getterCalls, 1) strictEqual(SchemaAST.isJson({}), true) strictEqual(SchemaAST.isJson({ a: 1 }), true) strictEqual(SchemaAST.isJson({ a: undefined }), false) @@ -58,6 +70,17 @@ describe("SchemaAST", () => { strictEqual(SchemaAST.isJson(deeper), true) }) + it("isJson is stack safe", () => { + let valid: unknown = null + let invalid: unknown = undefined + for (let i = 0; i < 25_000; i++) { + valid = [valid] + invalid = [invalid] + } + strictEqual(SchemaAST.isJson(valid), true) + strictEqual(SchemaAST.isJson(invalid), false) + }) + it("Schema.toCodecJson rejects non-JSON objects", () => { const encode = Schema.encodeUnknownExit(Schema.toCodecJson(Schema.Unknown)) strictEqual(encode(new Map([["a", 1]]))._tag, "Failure") @@ -76,21 +99,61 @@ describe("SchemaAST", () => { strictEqual(SchemaAST.isStringTree(["a"]), true) strictEqual(SchemaAST.isStringTree(["a", undefined]), true) strictEqual(SchemaAST.isStringTree(["a", 1]), false) + strictEqual(SchemaAST.isStringTree(new Array(1)), true) + const sparseWithInheritedValue = new Array(1) + const arrayPrototype = Object.create(Array.prototype) + arrayPrototype[0] = 1 + Object.setPrototypeOf(sparseWithInheritedValue, arrayPrototype) + strictEqual(SchemaAST.isStringTree(sparseWithInheritedValue), false) strictEqual(SchemaAST.isStringTree({}), true) strictEqual(SchemaAST.isStringTree({ a: "b" }), true) strictEqual(SchemaAST.isStringTree({ a: undefined }), true) strictEqual(SchemaAST.isStringTree({ a: "b", c: 1 }), false) + strictEqual(SchemaAST.isStringTree(new Map([["a", "b"]])), false) + strictEqual(SchemaAST.isStringTree(new Date(0)), false) + class A { + readonly a = "a" + } + strictEqual(SchemaAST.isStringTree(new A()), false) // nested strictEqual(SchemaAST.isStringTree({ a: { b: "c" } }), true) strictEqual(SchemaAST.isStringTree({ a: ["b", { c: "d" }] }), true) strictEqual(SchemaAST.isStringTree({ a: { b: 1 } }), false) + // DAG + const shared = { value: "a" } + strictEqual(SchemaAST.isStringTree({ left: shared, right: shared }), true) // circular reference const circular: Record = {} circular.self = circular strictEqual(SchemaAST.isStringTree(circular), false) }) + it("isStringTree is stack safe", () => { + let valid: unknown = "value" + let invalid: unknown = 1 + for (let i = 0; i < 25_000; i++) { + valid = { value: valid } + invalid = { value: invalid } + } + strictEqual(SchemaAST.isStringTree(valid), true) + strictEqual(SchemaAST.isStringTree(invalid), false) + }) + describe("toType", () => { + it("is idempotent for suspended schemas", () => { + const schema = Schema.suspend(() => Schema.Struct({ a: Schema.NumberFromString })) + const ast = SchemaAST.toType(schema.ast) + + strictEqual(SchemaAST.toType(ast), ast) + }) + + it("toEncoded is idempotent for suspended schemas", () => { + const schema = Schema.suspend(() => Schema.Struct({ a: Schema.NumberFromString })) + const ast = SchemaAST.toEncoded(schema.ast) + + strictEqual(SchemaAST.toEncoded(ast), ast) + }) + it("promotes encodingChecks when contained type shape is preserved", () => { const schema = Schema.Struct({ a: Schema.String }).pipe( Schema.flip, @@ -118,6 +181,48 @@ describe("SchemaAST", () => { strictEqual(ast.checks, undefined) strictEqual(ast.encodingChecks, undefined) }) + + it("preserves structural checks when contained type shape changes", () => { + const check = Schema.isMinProperties(1) + const schema = Schema.Struct({ a: Schema.NumberFromString }).check(check) + + const ast = SchemaAST.toEncoded(schema.ast) + + strictEqual(SchemaAST.isObjects(ast), true) + strictEqual(ast.checks?.[0], check) + }) + + it("preserves structural checks when contained element shape changes", () => { + const check = Schema.isMinLength(1) + const schema = Schema.Array(Schema.NumberFromString).check(check) + + const ast = SchemaAST.toEncoded(schema.ast) + + strictEqual(SchemaAST.isArrays(ast), true) + strictEqual(ast.checks?.[0], check) + }) + + it("preserves structural checks when a declaration type parameter shape changes", () => { + const check = Schema.isMinSize(1) + const schema = Schema.ReadonlySet(Schema.NumberFromString).check(check) + + const ast = SchemaAST.toEncoded(schema.ast) + + strictEqual(SchemaAST.isDeclaration(ast), true) + strictEqual(ast.checks?.[0], check) + }) + + it("preserves only the structural members of a mixed filter group", () => { + const structural = Schema.isMinProperties(1) + const group = structural.and(Schema.makeFilter(() => true)) + const schema = Schema.Struct({ a: Schema.NumberFromString }).check(group) + + const ast = SchemaAST.toEncoded(schema.ast) + + strictEqual(SchemaAST.isObjects(ast), true) + strictEqual(ast.checks?.length, 1) + strictEqual(ast.checks?.[0], structural) + }) }) describe("collectSentinels", () => { @@ -195,8 +300,8 @@ describe("SchemaAST", () => { deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }]) }) - it("ErrorClass", () => { - class E extends Schema.ErrorClass("E")({ + it("Error", () => { + class E extends Schema.Error("E")({ type: Schema.Literal("E"), e: Schema.String }) {} @@ -204,13 +309,34 @@ describe("SchemaAST", () => { deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "type", literal: "E" }]) }) - it("TaggedErrorClass", () => { - class E extends Schema.TaggedErrorClass()("E", { + it("TaggedError", () => { + class E extends Schema.TaggedError()("E", { e: Schema.String }) {} const ast = E.ast deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "E" }]) }) + + it("Union: the sentinels common to every member", () => { + const shared = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x") }), + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y") }) + ]) + deepStrictEqual(SchemaAST.collectSentinels(shared.ast), [{ key: "kind", literal: "a" }]) + + const disjoint = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a") }), + Schema.Struct({ kind: Schema.Literal("b") }) + ]) + deepStrictEqual(SchemaAST.collectSentinels(disjoint.ast), []) + + // A suspended member stays opaque, so the intersection is conservative. + const withSuspend = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a") }), + Schema.suspend(() => Schema.Struct({ kind: Schema.Literal("a") })) + ]) + deepStrictEqual(SchemaAST.collectSentinels(withSuspend.ast), []) + }) }) describe("getCandidates", () => { @@ -254,6 +380,25 @@ describe("SchemaAST", () => { deepStrictEqual(SchemaAST.getCandidates(undefined, ast.types), []) }) + it("literal-only union with a unique symbol", () => { + const symbol = Symbol.for("a") + const schema = Schema.Union([Schema.UniqueSymbol(symbol), Schema.Literal("b")]) + const ast = schema.ast + deepStrictEqual(SchemaAST.getCandidates(symbol, ast.types), [ast.types[0]]) + deepStrictEqual(SchemaAST.getCandidates(Symbol("a"), ast.types), []) + }) + + it("should preserve duplicate literal candidates in member order", () => { + const schema = Schema.Union([ + Schema.Literal("a").transform("first"), + Schema.Literal("a").transform("second"), + Schema.Never + ]) + const ast = schema.ast + deepStrictEqual(SchemaAST.getCandidates("a", ast.types), [ast.types[0], ast.types[1]]) + deepStrictEqual(SchemaAST.getCandidates("b", ast.types), []) + }) + it("String | Literals", () => { const schema = Schema.Union([Schema.String, Schema.Literals(["a", "b", "c"])]) const ast = schema.ast @@ -275,7 +420,63 @@ describe("SchemaAST", () => { deepStrictEqual(SchemaAST.getCandidates(1, ast.types), []) }) - it("should collect matches from different sentinel keys without duplicates", () => { + it("constructor mode should keep tagged candidates only when an object discriminator is missing", () => { + const schema = Schema.Union([ + Schema.Struct({ _tag: Schema.tag("a"), a: Schema.String }), + Schema.Struct({ _tag: Schema.tag("b"), b: Schema.Number }) + ]) + const ast = schema.ast + + deepStrictEqual(SchemaAST.getCandidates({}, ast.types, true), ast.types) + deepStrictEqual(SchemaAST.getCandidates({ _tag: undefined }, ast.types, true), ast.types) + deepStrictEqual(SchemaAST.getCandidates({ _tag: "a" }, ast.types, true), [ast.types[0]]) + deepStrictEqual(SchemaAST.getCandidates("a", ast.types, true), []) + }) + + it("should handle function-valued declarations with sentinels", () => { + const a = Schema.declare( + (input): input is () => void => typeof input === "function", + { "~sentinels": [{ key: "kind", literal: "a" }] } + ) + const b = Schema.declare( + (input): input is () => void => typeof input === "function", + { "~sentinels": [{ key: "kind", literal: "b" }] } + ) + const schema = Schema.Union([a, b]) + const ast = schema.ast + const input = Object.assign(() => {}, { kind: "a" }) + deepStrictEqual(SchemaAST.getCandidates(input, ast.types), [ast.types[0]]) + }) + + it("should preserve duplicate candidates with a common discriminator", () => { + const member = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }) + const schema = Schema.Union([ + member, + Schema.Struct({ kind: Schema.Literal("b"), value: Schema.Number }), + member, + Schema.Never + ]) + const ast = schema.ast + deepStrictEqual(SchemaAST.getCandidates({ kind: "a" }, ast.types), [ast.types[0], ast.types[2]]) + deepStrictEqual(SchemaAST.getCandidates({ kind: "b" }, ast.types), [ast.types[1]]) + deepStrictEqual(SchemaAST.getCandidates({ kind: "c" }, ast.types), []) + deepStrictEqual(SchemaAST.getCandidates({}, ast.types), []) + deepStrictEqual(SchemaAST.getCandidates("a", ast.types), []) + }) + + it("should protect cached common sentinel candidates from external mutation", () => { + const schema = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a") }), + Schema.Struct({ kind: Schema.Literal("b") }) + ]) + const ast = schema.ast + const input = { kind: "a" } + const candidates = SchemaAST.getCandidates(input, ast.types) + Reflect.set(candidates, candidates.length, ast.types[1]) + deepStrictEqual(SchemaAST.getCandidates(input, ast.types), [ast.types[0]]) + }) + + it("should handle candidates with different sentinel keys", () => { const schema = Schema.Union([ Schema.Struct({ kind: Schema.Literal("a"), @@ -289,6 +490,14 @@ describe("SchemaAST", () => { SchemaAST.getCandidates({ kind: "a", status: "ready", value: "value" }, ast.types), [ast.types[0], ast.types[1]] ) + deepStrictEqual( + SchemaAST.getCandidates({ kind: "b", status: "ready", value: "value" }, ast.types), + [ast.types[1]] + ) + deepStrictEqual( + SchemaAST.getCandidates({ kind: undefined, status: "ready", value: "value" }, ast.types), + [ast.types[1]] + ) }) it("should handle tagged tuples", () => { @@ -305,6 +514,59 @@ describe("SchemaAST", () => { deepStrictEqual(SchemaAST.getCandidates("", ast.types), [ast.types[2]]) deepStrictEqual(SchemaAST.getCandidates(1, ast.types), []) }) + + it("should handle tagged tuples with unique symbol sentinels", () => { + const a = Symbol.for("a") + const b = Symbol.for("b") + const schema = Schema.Union([ + Schema.Tuple([Schema.UniqueSymbol(a), Schema.String]), + Schema.Tuple([Schema.UniqueSymbol(b), Schema.Number]) + ]) + const ast = schema.ast + deepStrictEqual(SchemaAST.getCandidates([a, "value"], ast.types), [ast.types[0]]) + deepStrictEqual(SchemaAST.getCandidates([b, 1], ast.types), [ast.types[1]]) + deepStrictEqual(SchemaAST.getCandidates([Symbol("a"), "value"], ast.types), []) + }) + + it(`should deduplicate repeated declaration sentinels in "oneOf" mode`, () => { + const member = Schema.declare( + (input): input is object => typeof input === "object" && input !== null, + { + "~sentinels": [ + { key: "kind", literal: "a" }, + { key: "kind", literal: "a" } + ] + } + ) + const schema = Schema.Union([member], { mode: "oneOf" }) + const input = { kind: "a" } + strictEqual(Schema.decodeUnknownSync(schema)(input), input) + }) + + it("should dispatch a nested union member by its common sentinel", () => { + const hosted = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x") }), + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y") }) + ]) + const flat = Schema.Struct({ kind: Schema.Literal("b") }) + const ast = Schema.Union([hosted, flat]).ast + deepStrictEqual(SchemaAST.getCandidates({ kind: "a" }, ast.types), [ast.types[0]]) + deepStrictEqual(SchemaAST.getCandidates({ kind: "b" }, ast.types), [ast.types[1]]) + }) + + it("should exclude members whose sentinel the input contradicts", () => { + const schema = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("x"), value: Schema.String }), + Schema.Struct({ kind: Schema.Literal("a"), variant: Schema.Literal("y"), value: Schema.Number }) + ]) + const ast = schema.ast + deepStrictEqual(SchemaAST.getCandidates({ kind: "a", variant: "x" }, ast.types), [ast.types[0]]) + deepStrictEqual(SchemaAST.getCandidates({ kind: "a", variant: "z" }, ast.types), []) + deepStrictEqual(SchemaAST.getCandidates({ kind: "a", variant: undefined }, ast.types), []) + // A missing sentinel key does not exclude: the member still owes the error. + deepStrictEqual(SchemaAST.getCandidates({ kind: "a" }, ast.types), [ast.types[0], ast.types[1]]) + deepStrictEqual(SchemaAST.getCandidates({ kind: "a", variant: undefined }, ast.types, true), ast.types) + }) }) describe("getIndexSignatureKeys", () => { @@ -360,7 +622,7 @@ describe("SchemaAST", () => { }) it("Number", () => { - const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5 } + const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5, x1: 6, "1x": 7 } deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Number.ast, SchemaAST.defaultParseOptions), [ "1", "1.5", @@ -387,13 +649,13 @@ describe("SchemaAST", () => { describe("record", () => { it("treats Never parameters as no keys", () => { - const ast = SchemaAST.record(Schema.Never.ast, Schema.Number.ast, undefined) + const ast = SchemaAST.record(Schema.Never.ast, Schema.Number.ast) deepStrictEqual(ast.propertySignatures, []) deepStrictEqual(ast.indexSignatures, []) }) it("ignores Never arms in union parameters", () => { - const ast = SchemaAST.record(Schema.Union([Schema.String, Schema.Never]).ast, Schema.Number.ast, undefined) + const ast = SchemaAST.record(Schema.Union([Schema.String, Schema.Never]).ast, Schema.Number.ast) const indexSignature = ast.indexSignatures[0]! deepStrictEqual(ast.propertySignatures, []) @@ -405,20 +667,19 @@ describe("SchemaAST", () => { describe("IndexSignature", () => { it("accepts valid parameters on both type and encoded side", () => { - doesNotThrow(() => new SchemaAST.IndexSignature(Schema.String.ast, Schema.Number.ast, undefined)) - doesNotThrow(() => new SchemaAST.IndexSignature(Schema.NumberFromString.ast, Schema.Number.ast, undefined)) + doesNotThrow(() => new SchemaAST.IndexSignature(Schema.String.ast, Schema.Number.ast)) + doesNotThrow(() => new SchemaAST.IndexSignature(Schema.NumberFromString.ast, Schema.Number.ast)) doesNotThrow(() => new SchemaAST.IndexSignature( Schema.Union([Schema.String, Schema.NumberFromString]).ast, - Schema.Number.ast, - undefined + Schema.Number.ast ) ) }) it("rejects invalid type side parameters", () => { throws( - () => new SchemaAST.IndexSignature(Schema.Literal("a").ast, Schema.Number.ast, undefined), + () => new SchemaAST.IndexSignature(Schema.Literal("a").ast, Schema.Number.ast), new Error("Invalid index signature parameter Literal") ) }) @@ -431,7 +692,7 @@ describe("SchemaAST", () => { }) ) throws( - () => new SchemaAST.IndexSignature(StringFromBoolean.ast, Schema.Number.ast, undefined), + () => new SchemaAST.IndexSignature(StringFromBoolean.ast, Schema.Number.ast), new Error("Invalid index signature parameter String") ) }) diff --git a/.context/effect/packages/effect/test/schema/SchemaGetter.test.ts b/.context/effect/packages/effect/test/schema/SchemaGetter.test.ts index 8b4633129..750c0e500 100644 --- a/.context/effect/packages/effect/test/schema/SchemaGetter.test.ts +++ b/.context/effect/packages/effect/test/schema/SchemaGetter.test.ts @@ -1,13 +1,14 @@ -import { assert } from "@effect/vitest" -import { DateTime, Effect, Option, Result, SchemaGetter } from "effect" -import { describe, it } from "vitest" +import { assert, describe, it } from "@effect/vitest" +import { DateTime, Effect, Option, Result, SchemaGetter, SchemaIssue } from "effect" import { assertSome, deepStrictEqual } from "../utils/assert.ts" +const formatIssue = SchemaIssue.makeFormatterDefault() + function makeAsserts(getter: SchemaGetter.Getter) { return async (input: E, expected: T) => { const r = await Effect.runPromise( getter.run(Option.some(input), {}).pipe( - Effect.mapError((issue) => issue.toString()), + Effect.mapError(formatIssue), Effect.result ) ) @@ -16,6 +17,12 @@ function makeAsserts(getter: SchemaGetter.Getter) { } describe("SchemaGetter", () => { + it.effect("stringifyJson fails when JSON.stringify returns undefined", () => + SchemaGetter.stringifyJson().run(Option.some(undefined), {}).pipe( + Effect.flip, + Effect.map((issue) => assert.strictEqual(issue._tag, "InvalidValue")) + )) + it("map", () => { const getter = SchemaGetter.succeed(1).map((t) => t + 1) const result = Effect.runSync(getter.run(Option.some(1), {})) @@ -31,6 +38,47 @@ describe("SchemaGetter", () => { }) describe("makeTreeRecord", () => { + it("replaces conflicting leaf values with containers", () => { + deepStrictEqual( + SchemaGetter.makeTreeRecord([ + ["a", "x"], + ["a[b]", "y"] + ]), + { a: { b: "y" } } + ) + deepStrictEqual( + SchemaGetter.makeTreeRecord([ + ["a", "x"], + ["a[0]", "y"] + ]), + { a: ["y"] } + ) + deepStrictEqual( + SchemaGetter.makeTreeRecord([ + ["a", Object.freeze({ value: "x" })], + ["a[b]", { value: "y" }] + ]), + { a: { b: { value: "y" } } } + ) + }) + + it("replaces conflicting object and array containers", () => { + deepStrictEqual( + SchemaGetter.makeTreeRecord([ + ["a[b]", "x"], + ["a[0]", "y"] + ]), + { a: ["y"] } + ) + deepStrictEqual( + SchemaGetter.makeTreeRecord([ + ["a[0]", "x"], + ["a[b]", "y"] + ]), + { a: { b: "y" } } + ) + }) + it("reinitializes own undefined values before descending", () => { deepStrictEqual( SchemaGetter.makeTreeRecord([ diff --git a/.context/effect/packages/effect/test/schema/SchemaIssue.test.ts b/.context/effect/packages/effect/test/schema/SchemaIssue.test.ts index 56cb2fd4c..8bb5331ad 100644 --- a/.context/effect/packages/effect/test/schema/SchemaIssue.test.ts +++ b/.context/effect/packages/effect/test/schema/SchemaIssue.test.ts @@ -1,9 +1,74 @@ -import { SchemaIssue } from "effect" -import { describe, it } from "vitest" -import { assertTrue } from "../utils/assert.ts" +import { assert, describe, it } from "@effect/vitest" +import { Result, Schema, SchemaIssue } from "effect" +import { assertFalse, assertTrue } from "../utils/assert.ts" describe("SchemaIssue", () => { + const formatIssue = SchemaIssue.makeFormatterDefault() + it("isIssue", () => { assertTrue(SchemaIssue.isIssue(new SchemaIssue.MissingKey(undefined))) + assertFalse(SchemaIssue.isIssue({ "~effect/SchemaIssue/Issue": false })) + }) + + it("does not expose an actual field", () => { + const union = Schema.Union([Schema.String, Schema.Number]).ast + const invalidValue = new SchemaIssue.InvalidValue() + const issues: ReadonlyArray = [ + new SchemaIssue.InvalidType(Schema.String.ast), + invalidValue, + new SchemaIssue.MissingKey(undefined), + new SchemaIssue.UnexpectedKey(Schema.String.ast), + new SchemaIssue.Forbidden(undefined), + new SchemaIssue.OneOf(union, [Schema.String.ast]), + new SchemaIssue.Filter(Schema.isMinLength(1), invalidValue), + new SchemaIssue.Encoding(Schema.String.ast, invalidValue), + new SchemaIssue.Pointer(["value"], invalidValue), + new SchemaIssue.Composite(Schema.String.ast, [invalidValue]), + new SchemaIssue.AnyOf(union, [invalidValue]) + ] + + for (const issue of issues) { + assertFalse("actual" in issue) + } + }) + + it("preserves structural metadata", () => { + const annotations = { message: "custom message" } + const invalidType = new SchemaIssue.InvalidType(Schema.String.ast) + const invalidValue = new SchemaIssue.InvalidValue(annotations) + const unexpectedKey = new SchemaIssue.UnexpectedKey(Schema.Number.ast) + const union = Schema.Union([Schema.String, Schema.Number]).ast + const successes = [Schema.String.ast] + const oneOf = new SchemaIssue.OneOf(union, successes) + + assert.strictEqual(invalidType.ast, Schema.String.ast) + assert.strictEqual(invalidValue.annotations, annotations) + assert.strictEqual(unexpectedKey.ast, Schema.Number.ast) + assert.strictEqual(oneOf.ast, union) + assert.strictEqual(oneOf.successes, successes) + }) + + it("formats leaf issues with static built-in messages", () => { + const union = Schema.Union([Schema.String, Schema.Number]).ast + const cases: ReadonlyArray = [ + [new SchemaIssue.InvalidType(Schema.String.ast), "Expected string"], + [new SchemaIssue.InvalidValue(), "Expected a valid value"], + [new SchemaIssue.MissingKey(undefined), "Missing key"], + [new SchemaIssue.UnexpectedKey(Schema.String.ast), "Expected no excess property"], + [new SchemaIssue.Forbidden(undefined), "Forbidden operation"], + [new SchemaIssue.OneOf(union, [Schema.String.ast]), "Expected exactly one member to match"] + ] + + for (const [issue, expected] of cases) { + assert.strictEqual(formatIssue(issue), expected) + } + }) + + it("uses an empty AnyOf when no union candidates apply", () => { + const result = Schema.decodeUnknownResult(Schema.Union([Schema.String, Schema.Number]))(null) + + assertTrue(Result.isFailure(result)) + assertTrue(result.failure.issue._tag === "AnyOf") + assert.deepStrictEqual(result.failure.issue.issues, []) }) }) diff --git a/.context/effect/packages/effect/test/schema/SchemaParser.test.ts b/.context/effect/packages/effect/test/schema/SchemaParser.test.ts index e5fe1af29..eabfed067 100644 --- a/.context/effect/packages/effect/test/schema/SchemaParser.test.ts +++ b/.context/effect/packages/effect/test/schema/SchemaParser.test.ts @@ -1,32 +1,24 @@ import { describe, it } from "@effect/vitest" import { Cause, Effect, Exit, Option, Result, Schema, SchemaGetter, SchemaIssue, SchemaParser } from "effect" -import { assertTrue, strictEqual, throws } from "../utils/assert.ts" +import { assertSchemaIssueError, assertTrue, strictEqual, throws } from "../utils/assert.ts" describe("SchemaParser", () => { const makeMixedCause = () => Cause.combine( - Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), + Cause.fail(new SchemaIssue.InvalidValue({ message: "schema issue" })), Cause.die(new Error("defect")) ) - const makeMixedSchemaErrorCause = () => - Cause.combine( - Cause.fail(new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))), - Cause.die(new Error("defect")) - ) - describe("make", () => { it("should throw an error when the input is invalid", () => { const schema = Schema.String throws(() => SchemaParser.make(schema)(null as any), (e) => { - assertTrue(e instanceof Error) - assertTrue(SchemaIssue.isIssue(e.cause)) - strictEqual(e.message, "Expected string, got null") + assertSchemaIssueError(e, "Expected string") }) }) it("should throw an error when the cause contains both an Issue and a defect", () => { const schema = Schema.Struct({ - a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedSchemaErrorCause()))) + a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedCause()))) }) throws(() => SchemaParser.make(schema)({}), (e) => { @@ -52,7 +44,7 @@ describe("SchemaParser", () => { it("should throw an error when the cause contains both an Issue and a defect", () => { const schema = Schema.Struct({ - a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedSchemaErrorCause()))) + a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedCause()))) }) throws(() => SchemaParser.makeOption(schema)({}), (e) => { @@ -67,14 +59,10 @@ describe("SchemaParser", () => { it("should throw an error when the input is invalid", () => { const schema = Schema.String throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => { - assertTrue(e instanceof Error) - assertTrue(SchemaIssue.isIssue(e.cause)) - strictEqual(e.message, "Expected string, got null") + assertSchemaIssueError(e, "Expected string") }) throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => { - assertTrue(e instanceof Error) - assertTrue(SchemaIssue.isIssue(e.cause)) - strictEqual(e.message, "Expected string, got null") + assertSchemaIssueError(e, "Expected string") }) }) @@ -106,14 +94,10 @@ describe("SchemaParser", () => { const schema = Schema.String const r1 = await SchemaParser.decodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r1)) - assertTrue(r1.failure instanceof Error) - assertTrue(SchemaIssue.isIssue(r1.failure.cause)) - strictEqual(r1.failure.message, "Expected string, got null") + assertSchemaIssueError(r1.failure, "Expected string") const r2 = await SchemaParser.encodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r2)) - assertTrue(r2.failure instanceof Error) - assertTrue(SchemaIssue.isIssue(r2.failure.cause)) - strictEqual(r2.failure.message, "Expected string, got null") + assertSchemaIssueError(r2.failure, "Expected string") }) it("should reject with an error when the cause contains both an Issue and a defect", async () => { @@ -303,21 +287,422 @@ describe("SchemaParser", () => { }) describe("decodeUnknownExit", () => { - it("should preserve mixed causes in union candidates instead of trying later candidates", () => { - const schema = Schema.Union([ + it("preserves values that use Effect data protocols", () => { + let executions = 0 + const suspended = Effect.sync(() => { + executions++ + return "a" + }) + const decode = SchemaParser.decodeUnknownExit(Schema.Unknown) + const values = [Option.none(), Option.some("a"), Exit.succeed("a"), Exit.fail("a"), suspended] + + for (const value of values) { + const result = decode(value) + assertTrue(Exit.isSuccess(result)) + strictEqual(result.value, value) + } + strictEqual(executions, 0) + + const failure = Exit.fail("a") + const unionResult = SchemaParser.decodeUnknownExit(Schema.Union([Schema.Unknown]))(failure) + assertTrue(Exit.isSuccess(unionResult)) + strictEqual(unionResult.value, failure) + }) + + it("distinguishes missing tuple elements from undefined", () => { + const decode = SchemaParser.decodeUnknownExit( + Schema.Tuple([Schema.optionalKey(Schema.Undefined)]) + ) + + const empty = decode([]) + assertTrue(Exit.isSuccess(empty)) + strictEqual(empty.value.length, 0) + + const present = decode([undefined]) + assertTrue(Exit.isSuccess(present)) + strictEqual(present.value.length, 1) + assertTrue(Object.hasOwn(present.value, 0)) + + const sparse = decode(new Array(1)) + assertTrue(Exit.isSuccess(sparse)) + strictEqual(sparse.value.length, 1) + assertTrue(Object.hasOwn(sparse.value, 0)) + }) + + it("preserves missing optional template literals", () => { + const template = Schema.optionalKey(Schema.TemplateLiteral(["a", Schema.String])) + const decodeTuple = SchemaParser.decodeUnknownExit(Schema.Tuple([template])) + const decodeStruct = SchemaParser.decodeUnknownExit(Schema.Struct({ value: template })) + + const tuple = decodeTuple([]) + assertTrue(Exit.isSuccess(tuple)) + strictEqual(tuple.value.length, 0) + + const struct = decodeStruct({}) + assertTrue(Exit.isSuccess(struct)) + assertTrue(!Object.hasOwn(struct.value, "value")) + }) + + it("reads property and index values once", () => { + let propertyReads = 0 + let indexReads = 0 + const propertyInput = { + get value(): unknown { + propertyReads++ + return propertyReads === 1 ? "a" : 1 + } + } + const indexInput = { + get value(): unknown { + indexReads++ + return indexReads === 1 ? "a" : 1 + } + } + + const property = SchemaParser.decodeUnknownExit(Schema.Struct({ value: Schema.String }))(propertyInput) + assertTrue(Exit.isSuccess(property)) + strictEqual(property.value.value, "a") + strictEqual(propertyReads, 1) + + const index = SchemaParser.decodeUnknownExit(Schema.Record(Schema.String, Schema.String))(indexInput) + assertTrue(Exit.isSuccess(index)) + strictEqual(index.value.value, "a") + strictEqual(indexReads, 1) + + let rejectedReads = 0 + const rejectedKey = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter(() => Effect.fail(new SchemaIssue.InvalidValue())), + encode: SchemaGetter.passthrough() + })) + const rejectedInput = { + get value(): unknown { + rejectedReads++ + return "a" + } + } + const rejected = SchemaParser.decodeUnknownExit(Schema.Record(rejectedKey, Schema.String))(rejectedInput) + assertTrue(Exit.isFailure(rejected)) + strictEqual(rejectedReads, 0) + }) + + it("keeps synchronous transformations eager", () => { + const effect = SchemaParser.decodeUnknownEffect(Schema.NumberFromString)("1") + assertTrue(Exit.isExit(effect)) + assertTrue(Exit.isSuccess(effect)) + strictEqual(effect.value, 1) + }) + + it("rejects a missing root output", () => { + const schema = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter(() => Effect.succeedNone), + encode: SchemaGetter.passthrough() + })) + const result = SchemaParser.decodeUnknownExit(schema)("value") + + assertTrue(Exit.isFailure(result)) + }) + + it("rejects a missing root output after parsing structural schemas", () => { + const missing = new SchemaGetter.Getter(() => Effect.succeedNone) + const array = Schema.Array(Schema.String).pipe(Schema.decode({ + decode: missing, + encode: missing + })) + const struct = Schema.Struct({ value: Schema.String }).pipe(Schema.decode({ + decode: missing, + encode: missing + })) + + assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(array)(["value"]))) + assertTrue(Exit.isFailure(SchemaParser.encodeUnknownExit(array)(["value"]))) + assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(struct)({ value: "value" }))) + assertTrue(Exit.isFailure(SchemaParser.encodeUnknownExit(struct)({ value: "value" }))) + }) + + it("rejects an invalid root without compiling deeply nested structural parsers", () => { + let array: Schema.Codec = Schema.String + let struct: Schema.Codec = Schema.String + for (let i = 0; i < 5_000; i++) { + array = Schema.Array(array) + struct = Schema.Struct({ value: struct }) + } + + assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(array)(null))) + assertTrue(Exit.isFailure(SchemaParser.decodeUnknownExit(struct)(null))) + }) + + it("stops outer checks after an aborting check in a FilterGroup", () => { + let innerRuns = 0 + let outerRuns = 0 + const group = Schema.makeFilter(() => false).abort().and( + Schema.makeFilter(() => { + innerRuns++ + return false + }) + ) + const schema = Schema.String.check( + group, + Schema.makeFilter(() => { + outerRuns++ + return false + }) + ) + + const result = SchemaParser.decodeUnknownExit(schema, { errors: "all" })("value") + + assertTrue(Exit.isFailure(result)) + strictEqual(innerRuns, 0) + strictEqual(outerRuns, 0) + }) + + it.effect("preserves unchanged values through asynchronous middleware", () => + Effect.gen(function*() { + const schema = Schema.String.pipe( + Schema.middlewareDecoding((effect) => Effect.yieldNow.pipe(Effect.andThen(effect))) + ) + + strictEqual(yield* SchemaParser.decodeUnknownEffect(schema)("value"), "value") + })) + + it.effect("wraps an asynchronous failure from a uniquely selected union member", () => + Effect.gen(function*() { + const failing = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter(() => + Effect.yieldNow.pipe( + Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue())) + ) + ), + encode: SchemaGetter.passthrough() + })) + const schema = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }), + Schema.Struct({ kind: Schema.Literal("b"), value: failing }) + ]) + + const exit = yield* Effect.exit( + SchemaParser.decodeUnknownEffect(schema)({ kind: "b", value: "value" }) + ) + assertTrue(Exit.isFailure(exit)) + const error = Cause.findError(exit.cause) + assertTrue(Result.isSuccess(error)) + strictEqual(error.success._tag, "AnyOf") + })) + + it.effect("resolves an unchanged concurrent union candidate", () => + Effect.gen(function*() { + const delayedFailure = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter(() => + Effect.yieldNow.pipe( + Effect.andThen(Effect.fail(new SchemaIssue.InvalidValue())) + ) + ), + encode: SchemaGetter.passthrough() + })) + const schema = Schema.Union([delayedFailure, Schema.String]) + + strictEqual( + yield* SchemaParser.decodeUnknownEffect(schema)("value", { concurrency: 2 }), + "value" + ) + })) + + it("does not replay eager fields after encountering a suspended transformation", () => { + const calls: Array = [] + const field = (name: string, suspended = false) => Schema.String.pipe(Schema.decode({ - decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())), + decode: new SchemaGetter.Getter((input) => { + calls.push(name) + return suspended ? Effect.suspend(() => Effect.succeed(input)) : Effect.succeed(input) + }), encode: SchemaGetter.passthrough() - })), + })) + const schema = Schema.Struct({ + a: field("a"), + b: field("b", true), + c: field("c") + }) + + const exit = SchemaParser.decodeUnknownExit(schema)({ a: "a", b: "b", c: "c" }) + + assertTrue(Exit.isSuccess(exit)) + strictEqual(calls.join(","), "a,b,c") + }) + + it("keeps encoding-link parser compilation lazy for Suspend", () => { + let evaluations = 0 + const schema = Schema.suspend(() => { + evaluations++ + return Schema.String + }).pipe(Schema.decode({ + decode: SchemaGetter.passthrough(), + encode: SchemaGetter.passthrough() + })) + + const decode = SchemaParser.decodeUnknownExit(schema) + strictEqual(evaluations, 0) + strictEqual(decode("a")._tag, "Success") + strictEqual(evaluations, 1) + strictEqual(decode("b")._tag, "Success") + strictEqual(evaluations, 1) + }) + + it("keeps direct Suspend parser compilation lazy", () => { + let evaluations = 0 + const schema = Schema.suspend(() => { + evaluations++ + return Schema.String + }) + + const decode = SchemaParser.decodeUnknownExit(schema) + strictEqual(evaluations, 0) + strictEqual(decode("a")._tag, "Success") + strictEqual(evaluations, 1) + strictEqual(decode("b")._tag, "Success") + strictEqual(evaluations, 1) + }) + + it("supports mutually recursive Suspend parsers", () => { + interface A { + readonly _tag: "A" + readonly next: string | B + } + interface B { + readonly _tag: "B" + readonly next: string | A + } + const A = Schema.Struct({ + _tag: Schema.Literal("A"), + next: Schema.Union([Schema.String, Schema.suspend((): Schema.Codec => B)]) + }) + const B = Schema.Struct({ + _tag: Schema.Literal("B"), + next: Schema.Union([Schema.String, Schema.suspend((): Schema.Codec => A)]) + }) + const decode = SchemaParser.decodeUnknownExit(A) + + strictEqual(decode({ _tag: "A", next: { _tag: "B", next: { _tag: "A", next: "end" } } })._tag, "Success") + }) + + it("keeps Declaration parser compilation lazy and shared by AST identity", () => { + let evaluations = 0 + const schema = Schema.declareConstructor()([], () => { + evaluations++ + return (input) => Effect.succeed(input) + }) + + const decode = SchemaParser.decodeUnknownExit(schema) + const decodeAgain = SchemaParser.decodeUnknownExit(schema) + strictEqual(evaluations, 0) + strictEqual(decode("a")._tag, "Success") + strictEqual(evaluations, 1) + strictEqual(decodeAgain("b")._tag, "Success") + strictEqual(evaluations, 1) + }) + + it("keeps untried Declaration union members lazy", () => { + let evaluations = 0 + const declaration = Schema.declareConstructor()([], () => { + evaluations++ + return (input) => Effect.succeed(input) + }) + const decode = SchemaParser.decodeUnknownExit(Schema.Union([Schema.Literal("a"), declaration])) + + strictEqual(decode("a")._tag, "Success") + strictEqual(evaluations, 0) + strictEqual(decode("b")._tag, "Success") + strictEqual(evaluations, 1) + }) + + it("keeps encode-side Declaration and Suspend parser compilation lazy", () => { + let declarationEvaluations = 0 + let suspendEvaluations = 0 + const declaration = Schema.declareConstructor()([], () => { + declarationEvaluations++ + return (input) => Effect.succeed(input) + }) + const suspend = Schema.suspend(() => { + suspendEvaluations++ + return Schema.String + }) + + const encodeDeclaration = SchemaParser.encodeUnknownExit(declaration) + const encodeSuspend = SchemaParser.encodeUnknownExit(suspend) + strictEqual(declarationEvaluations, 0) + strictEqual(suspendEvaluations, 0) + strictEqual(encodeDeclaration("a")._tag, "Success") + strictEqual(encodeSuspend("a")._tag, "Success") + strictEqual(declarationEvaluations, 1) + strictEqual(suspendEvaluations, 1) + strictEqual(encodeDeclaration("b")._tag, "Success") + strictEqual(encodeSuspend("b")._tag, "Success") + strictEqual(declarationEvaluations, 1) + strictEqual(suspendEvaluations, 1) + }) + + it("retries lazy parser compilation after a synchronous factory failure", () => { + let evaluations = 0 + const schema = Schema.declareConstructor()([], () => { + if (++evaluations === 1) { + throw new Error("factory failure") + } + return (input) => Effect.succeed(input) + }) + const decode = SchemaParser.decodeUnknownExit(schema) + + throws(() => decode("a"), (error) => { + assertTrue(error instanceof Error) + strictEqual(error.message, "factory failure") + }) + strictEqual(decode("b")._tag, "Success") + strictEqual(decode("c")._tag, "Success") + strictEqual(evaluations, 2) + }) + + it("retries lazy Suspend parser compilation after a synchronous thunk failure", () => { + let evaluations = 0 + const schema = Schema.suspend(() => { + if (++evaluations === 1) { + throw new Error("thunk failure") + } + return Schema.String + }) + const decode = SchemaParser.decodeUnknownExit(schema) + + throws(() => decode("a"), (error) => { + assertTrue(error instanceof Error) + strictEqual(error.message, "thunk failure") + }) + strictEqual(decode("b")._tag, "Success") + strictEqual(decode("c")._tag, "Success") + strictEqual(evaluations, 2) + }) + + it("should preserve mixed causes in union candidates instead of trying later candidates", () => { + const failure = Schema.String.pipe(Schema.decode({ + decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())), + encode: SchemaGetter.passthrough() + })) + const schema = Schema.Union([ + failure, Schema.Literal("a") ]) + const taggedSchema = Schema.Union([ + Schema.Struct({ kind: Schema.Literal("a"), value: failure }), + Schema.Struct({ kind: Schema.Literal("b"), value: Schema.String }) + ]) - const exit = SchemaParser.decodeUnknownExit(schema)("a") - assertTrue(Exit.isFailure(exit)) - assertTrue(Exit.hasDies(exit)) - const error = Cause.findError(exit.cause) - assertTrue(Result.isSuccess(error)) - assertTrue(SchemaIssue.isIssue(error.success)) + for ( + const exit of [ + SchemaParser.decodeUnknownExit(schema)("a"), + SchemaParser.decodeUnknownExit(taggedSchema)({ kind: "a", value: "a" }) + ] as ReadonlyArray> + ) { + assertTrue(Exit.isFailure(exit)) + assertTrue(Exit.hasDies(exit)) + const error = Cause.findError(exit.cause) + assertTrue(Result.isSuccess(error)) + assertTrue(SchemaIssue.isIssue(error.success)) + } }) }) }) diff --git a/.context/effect/packages/effect/test/schema/SchemaTransformation.test.ts b/.context/effect/packages/effect/test/schema/SchemaTransformation.test.ts new file mode 100644 index 000000000..33029305d --- /dev/null +++ b/.context/effect/packages/effect/test/schema/SchemaTransformation.test.ts @@ -0,0 +1,10 @@ +import { SchemaTransformation } from "effect" +import { describe, it } from "vitest" +import { assertFalse, assertTrue } from "../utils/assert.ts" + +describe("SchemaTransformation", () => { + it("isTransformation", () => { + assertTrue(SchemaTransformation.isTransformation(SchemaTransformation.passthrough())) + assertFalse(SchemaTransformation.isTransformation({ "~effect/SchemaTransformation/Transformation": false })) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/builtInRevivers.test.ts b/.context/effect/packages/effect/test/schema/representation/builtInRevivers.test.ts new file mode 100644 index 000000000..76f6e830e --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/builtInRevivers.test.ts @@ -0,0 +1,1064 @@ +import { assert, describe, it } from "@effect/vitest" +import { Formatter, Schema, type SchemaAST, SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +function assertFilterReviver(input: { + readonly schema: Schema.Codec + readonly id: string + readonly payload: Schema.Json + readonly schemas?: ReadonlyArray + readonly reviver: SchemaRepresentation.FilterReviver + readonly dependencies?: ReadonlyArray + readonly valid: unknown + readonly invalid: unknown + readonly hasToJsonSchema?: boolean +}): void { + const check = input.schema.ast.checks?.at(-1) + assert.isDefined(check) + assert.strictEqual(check._tag, "Filter") + if (check._tag !== "Filter") return + assert.deepStrictEqual(check.annotations?.representation, { + id: input.id, + payload: input.payload, + ...(input.schemas === undefined ? undefined : { schemas: input.schemas }) + }) + + const document = SchemaRepresentation.toRepresentation(input.schema.ast) + const json = SchemaRepresentation.toJson(document) + const revived = SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(json), { + revivers: [input.reviver, ...(input.dependencies ?? [])] + }) as Schema.Codec + + assert.strictEqual(Schema.decodeUnknownResult(revived)(input.valid)._tag, "Success") + assert.strictEqual(Schema.decodeUnknownResult(revived)(input.invalid)._tag, "Failure") + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(revived.ast)), + json + ) + const revivedCheck = revived.ast.checks?.at(-1) + assert.isDefined(revivedCheck) + assert.strictEqual(revivedCheck._tag, "Filter") + if (revivedCheck._tag !== "Filter") return + assert.strictEqual(revivedCheck.annotations?.representation?.id, input.id) + assert.strictEqual( + typeof revivedCheck.annotations?.toJsonSchema, + input.hasToJsonSchema === false ? "undefined" : "function" + ) + assert.strictEqual(typeof revivedCheck.annotations?.toCode, "function") +} + +function assertDeclarationReviver(input: { + readonly schema: Schema.Top + readonly id: string + readonly payload: Schema.Json + readonly reviver: SchemaRepresentation.DeclarationReviver + readonly dependencies?: ReadonlyArray +}): void { + const representation = input.schema.ast.annotations?.representation + assert.deepStrictEqual(representation, { + id: input.id, + payload: input.payload + }) + + const document = SchemaRepresentation.toRepresentation(input.schema.ast) + const json = SchemaRepresentation.toJson(document) + const revived = SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(json), { + revivers: [input.reviver, ...(input.dependencies ?? [])] + }) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(revived.ast)), + json + ) + assert.strictEqual( + (revived.ast.annotations?.representation as { readonly id?: string } | undefined)?.id, + input.id + ) + assert.strictEqual(typeof revived.ast.annotations?.toCode, "function") +} + +describe("SchemaRepresentation built-in string revivers", () => { + it("revives isStringFinite", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isStringFinite()), + id: "effect/schema/isStringFinite", + payload: null, + reviver: Schema.isStringFiniteReviver, + valid: "1.5", + invalid: "Infinity" + }) + }) + + it("revives isStringBigInt", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isStringBigInt()), + id: "effect/schema/isStringBigInt", + payload: null, + reviver: Schema.isStringBigIntReviver, + valid: "-10", + invalid: "1.5" + }) + }) + + it("revives isStringSymbol", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isStringSymbol()), + id: "effect/schema/isStringSymbol", + payload: null, + reviver: Schema.isStringSymbolReviver, + valid: "Symbol(shared)", + invalid: "shared" + }) + }) + + it("revives isMinLength", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isMinLength(1.8)), + id: "effect/schema/isMinLength", + payload: { minLength: 1 }, + reviver: Schema.isMinLengthReviver, + valid: "a", + invalid: "" + }) + }) + + it("revives isMaxLength", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isMaxLength(3.8)), + id: "effect/schema/isMaxLength", + payload: { maxLength: 3 }, + reviver: Schema.isMaxLengthReviver, + valid: "abc", + invalid: "abcd" + }) + }) + + it("revives isLengthBetween", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isLengthBetween(1.8, 3.8)), + id: "effect/schema/isLengthBetween", + payload: { minimum: 1, maximum: 3 }, + reviver: Schema.isLengthBetweenReviver, + valid: "ab", + invalid: "" + }) + }) + + it("revives isPattern", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isPattern(/^a+$/i)), + id: "effect/schema/isPattern", + payload: { source: "^a+$", flags: "i" }, + reviver: Schema.isPatternReviver, + valid: "AAA", + invalid: "bbb" + }) + }) + + it("revives isTrimmed", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isTrimmed()), + id: "effect/schema/isTrimmed", + payload: null, + reviver: Schema.isTrimmedReviver, + valid: "text", + invalid: " text " + }) + }) + + it("revives isUUID", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isUUID(4)), + id: "effect/schema/isUUID", + payload: { version: 4 }, + reviver: Schema.isUUIDReviver, + valid: "123e4567-e89b-42d3-a456-426614174000", + invalid: "123e4567-e89b-12d3-a456-426614174000" + }) + }) + + it("revives isGUID", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isGUID()), + id: "effect/schema/isGUID", + payload: null, + reviver: Schema.isGUIDReviver, + valid: "123e4567-e89b-12d3-a456-426614174000", + invalid: "not-a-guid" + }) + }) + + it("revives isULID", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isULID()), + id: "effect/schema/isULID", + payload: null, + reviver: Schema.isULIDReviver, + valid: "01ARZ3NDEKTSV4RRFFQ69G5FAV", + invalid: "not-a-ulid" + }) + }) + + it("revives isBase64", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isBase64()), + id: "effect/schema/isBase64", + payload: null, + reviver: Schema.isBase64Reviver, + valid: "YQ==", + invalid: "?" + }) + }) + + it("revives isBase64Url", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isBase64Url()), + id: "effect/schema/isBase64Url", + payload: null, + reviver: Schema.isBase64UrlReviver, + valid: "YQ", + invalid: "?" + }) + }) + + it("revives isStartsWith", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isStartsWith("pre")), + id: "effect/schema/isStartsWith", + payload: { startsWith: "pre" }, + reviver: Schema.isStartsWithReviver, + valid: "prefix", + invalid: "suffix" + }) + }) + + it("revives isEndsWith", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isEndsWith("end")), + id: "effect/schema/isEndsWith", + payload: { endsWith: "end" }, + reviver: Schema.isEndsWithReviver, + valid: "weekend", + invalid: "ending" + }) + }) + + it("revives isIncludes", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isIncludes("mid")), + id: "effect/schema/isIncludes", + payload: { includes: "mid" }, + reviver: Schema.isIncludesReviver, + valid: "middle", + invalid: "outside" + }) + }) + + it("revives isUppercased", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isUppercased()), + id: "effect/schema/isUppercased", + payload: null, + reviver: Schema.isUppercasedReviver, + valid: "ABC1", + invalid: "Abc" + }) + }) + + it("revives isLowercased", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isLowercased()), + id: "effect/schema/isLowercased", + payload: null, + reviver: Schema.isLowercasedReviver, + valid: "abc1", + invalid: "Abc" + }) + }) + + it("revives isCapitalized", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isCapitalized()), + id: "effect/schema/isCapitalized", + payload: null, + reviver: Schema.isCapitalizedReviver, + valid: "Hello", + invalid: "hello" + }) + }) + + it("revives isUncapitalized", () => { + assertFilterReviver({ + schema: Schema.String.check(Schema.isUncapitalized()), + id: "effect/schema/isUncapitalized", + payload: null, + reviver: Schema.isUncapitalizedReviver, + valid: "hello", + invalid: "Hello" + }) + }) +}) + +function expectInvalidPayload(json: Schema.Json, reviver: SchemaRepresentation.AnyReviver): void { + const path = Formatter.formatPath([ + "representation", + "checks", + 0, + "representation", + "payload" + ]) + throws( + () => SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(json), { revivers: [reviver] }), + `Invalid representation payload for ${reviver.id}\n at ${path}` + ) +} + +describe("SchemaRepresentation built-in number revivers", () => { + it("revives isFinite", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isFinite()), + id: "effect/schema/isFinite", + payload: null, + reviver: Schema.isFiniteReviver, + valid: 1, + invalid: Number.POSITIVE_INFINITY + }) + }) + + it("revives isInt", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isInt()), + id: "effect/schema/isInt", + payload: null, + reviver: Schema.isIntReviver, + valid: 1, + invalid: 1.5 + }) + }) + + it("revives isMultipleOf", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isMultipleOf(3)), + id: "effect/schema/isMultipleOf", + payload: { divisor: 3 }, + reviver: Schema.isMultipleOfReviver, + valid: 6, + invalid: 7 + }) + }) + + it("revives isGreaterThan", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isGreaterThan(1)), + id: "effect/schema/isGreaterThan", + payload: { exclusiveMinimum: 1 }, + reviver: Schema.isGreaterThanReviver, + valid: 2, + invalid: 1 + }) + }) + + it("revives isGreaterThanOrEqualTo", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isGreaterThanOrEqualTo(1)), + id: "effect/schema/isGreaterThanOrEqualTo", + payload: { minimum: 1 }, + reviver: Schema.isGreaterThanOrEqualToReviver, + valid: 1, + invalid: 0 + }) + }) + + it("revives isLessThan", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isLessThan(2)), + id: "effect/schema/isLessThan", + payload: { exclusiveMaximum: 2 }, + reviver: Schema.isLessThanReviver, + valid: 1, + invalid: 2 + }) + }) + + it("revives isLessThanOrEqualTo", () => { + assertFilterReviver({ + schema: Schema.Number.check(Schema.isLessThanOrEqualTo(2)), + id: "effect/schema/isLessThanOrEqualTo", + payload: { maximum: 2 }, + reviver: Schema.isLessThanOrEqualToReviver, + valid: 2, + invalid: 3 + }) + }) + + it("revives isBetween", () => { + assertFilterReviver({ + schema: Schema.Number.check( + Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMinimum: true }) + ), + id: "effect/schema/isBetween", + payload: { minimum: 1, maximum: 3, exclusiveMinimum: true }, + reviver: Schema.isBetweenReviver, + valid: 2, + invalid: 1 + }) + }) + + it("normalizes isBetween flags", () => { + const check = Schema.isBetween({ + minimum: 1, + maximum: 3, + exclusiveMinimum: false, + exclusiveMaximum: true + }) + + assert.deepStrictEqual(check.annotations?.representation, { + id: "effect/schema/isBetween", + payload: { minimum: 1, maximum: 3, exclusiveMaximum: true } + }) + }) + + it("rejects a non-numeric isMultipleOf payload", () => { + const json = SchemaRepresentation.toJson( + SchemaRepresentation.toRepresentation(Schema.Number.check(Schema.isMultipleOf(2)).ast) + ) as any + json.representation.checks[0].representation.payload.divisor = "2" + + expectInvalidPayload(json, Schema.isMultipleOfReviver) + }) + + it("rejects a non-canonical isBetween payload", () => { + const json = SchemaRepresentation.toJson( + SchemaRepresentation.toRepresentation( + Schema.Number.check(Schema.isBetween({ minimum: 1, maximum: 3 })).ast + ) + ) as any + json.representation.checks[0].representation.payload.exclusiveMinimum = false + + expectInvalidPayload(json, Schema.isBetweenReviver) + }) +}) + +describe("SchemaRepresentation built-in BigInt revivers", () => { + it("revives isGreaterThanBigInt", () => { + assertFilterReviver({ + schema: Schema.BigInt.check(Schema.isGreaterThanBigInt(10n)), + id: "effect/schema/isGreaterThanBigInt", + payload: { exclusiveMinimum: "10" }, + reviver: Schema.isGreaterThanBigIntReviver, + valid: 11n, + invalid: 10n + }) + }) + + it("revives isGreaterThanOrEqualToBigInt", () => { + assertFilterReviver({ + schema: Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(10n)), + id: "effect/schema/isGreaterThanOrEqualToBigInt", + payload: { minimum: "10" }, + reviver: Schema.isGreaterThanOrEqualToBigIntReviver, + valid: 10n, + invalid: 9n + }) + }) + + it("revives isLessThanBigInt", () => { + assertFilterReviver({ + schema: Schema.BigInt.check(Schema.isLessThanBigInt(10n)), + id: "effect/schema/isLessThanBigInt", + payload: { exclusiveMaximum: "10" }, + reviver: Schema.isLessThanBigIntReviver, + valid: 9n, + invalid: 10n + }) + }) + + it("revives isLessThanOrEqualToBigInt", () => { + assertFilterReviver({ + schema: Schema.BigInt.check(Schema.isLessThanOrEqualToBigInt(10n)), + id: "effect/schema/isLessThanOrEqualToBigInt", + payload: { maximum: "10" }, + reviver: Schema.isLessThanOrEqualToBigIntReviver, + valid: 10n, + invalid: 11n + }) + }) + + it("revives isBetweenBigInt", () => { + assertFilterReviver({ + schema: Schema.BigInt.check( + Schema.isBetweenBigInt({ minimum: -10n, maximum: 10n, exclusiveMaximum: true }) + ), + id: "effect/schema/isBetweenBigInt", + payload: { minimum: "-10", maximum: "10", exclusiveMaximum: true }, + reviver: Schema.isBetweenBigIntReviver, + valid: 0n, + invalid: 10n + }) + }) + + it("persists large bounds as canonical decimal strings", () => { + const value = 900719925474099312345678901234567890n + assert.deepStrictEqual(Schema.isGreaterThanBigInt(value).annotations?.representation, { + id: "effect/schema/isGreaterThanBigInt", + payload: { exclusiveMinimum: "900719925474099312345678901234567890" } + }) + }) + + it("normalizes isBetweenBigInt flags", () => { + assert.deepStrictEqual( + Schema.isBetweenBigInt({ + minimum: -1n, + maximum: 1n, + exclusiveMinimum: false, + exclusiveMaximum: true + }).annotations?.representation, + { + id: "effect/schema/isBetweenBigInt", + payload: { minimum: "-1", maximum: "1", exclusiveMaximum: true } + } + ) + }) +}) + +function date(millis: number): Date { + return new globalThis.Date(millis) +} + +const epoch = "1970-01-01T00:00:00.000Z" + +describe("SchemaRepresentation built-in Date revivers", () => { + it("revives isGreaterThanDate", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isGreaterThanDate(date(0))), + id: "effect/schema/isGreaterThanDate", + payload: { exclusiveMinimum: epoch }, + reviver: Schema.isGreaterThanDateReviver, + valid: date(1), + invalid: date(0) + }) + }) + + it("revives isGreaterThanOrEqualToDate", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isGreaterThanOrEqualToDate(date(0))), + id: "effect/schema/isGreaterThanOrEqualToDate", + payload: { minimum: epoch }, + reviver: Schema.isGreaterThanOrEqualToDateReviver, + valid: date(0), + invalid: date(-1) + }) + }) + + it("revives isLessThanDate", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isLessThanDate(date(0))), + id: "effect/schema/isLessThanDate", + payload: { exclusiveMaximum: epoch }, + reviver: Schema.isLessThanDateReviver, + valid: date(-1), + invalid: date(0) + }) + }) + + it("revives isLessThanOrEqualToDate", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isLessThanOrEqualToDate(date(0))), + id: "effect/schema/isLessThanOrEqualToDate", + payload: { maximum: epoch }, + reviver: Schema.isLessThanOrEqualToDateReviver, + valid: date(0), + invalid: date(1) + }) + }) + + it("revives isBetweenDate", () => { + assertFilterReviver({ + schema: Schema.Any.check( + Schema.isBetweenDate({ minimum: date(0), maximum: date(2), exclusiveMaximum: true }) + ), + id: "effect/schema/isBetweenDate", + payload: { + minimum: epoch, + maximum: "1970-01-01T00:00:00.002Z", + exclusiveMaximum: true + }, + reviver: Schema.isBetweenDateReviver, + valid: date(1), + invalid: date(2) + }) + }) + + it("persists millisecond precision", () => { + assert.deepStrictEqual(Schema.isGreaterThanDate(date(123)).annotations?.representation, { + id: "effect/schema/isGreaterThanDate", + payload: { exclusiveMinimum: "1970-01-01T00:00:00.123Z" } + }) + }) + + it("normalizes isBetweenDate flags", () => { + assert.deepStrictEqual( + Schema.isBetweenDate({ + minimum: date(-1), + maximum: date(1), + exclusiveMinimum: false, + exclusiveMaximum: true + }).annotations?.representation, + { + id: "effect/schema/isBetweenDate", + payload: { + minimum: "1969-12-31T23:59:59.999Z", + maximum: "1970-01-01T00:00:00.001Z", + exclusiveMaximum: true + } + } + ) + }) +}) + +describe("SchemaRepresentation built-in collection revivers", () => { + it("revives isMinSize", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isMinSize(2)), + id: "effect/schema/isMinSize", + payload: { minSize: 2 }, + reviver: Schema.isMinSizeReviver, + valid: new Set([1, 2]), + invalid: new Set([1]) + }) + }) + + it("revives isMaxSize", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isMaxSize(1)), + id: "effect/schema/isMaxSize", + payload: { maxSize: 1 }, + reviver: Schema.isMaxSizeReviver, + valid: new Set([1]), + invalid: new Set([1, 2]) + }) + }) + + it("revives isSizeBetween", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isSizeBetween(1, 2)), + id: "effect/schema/isSizeBetween", + payload: { minimum: 1, maximum: 2 }, + reviver: Schema.isSizeBetweenReviver, + valid: new Set([1]), + invalid: new Set() + }) + }) + + it("revives isUnique", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isUnique()), + id: "effect/schema/isUnique", + payload: null, + reviver: Schema.isUniqueReviver, + valid: [1, 2], + invalid: [1, 1] + }) + }) + + it("normalizes isMinSize", () => { + assert.deepStrictEqual(Schema.isMinSize(-1).annotations?.representation, { + id: "effect/schema/isMinSize", + payload: { minSize: 0 } + }) + }) + + it("normalizes isMaxSize", () => { + assert.deepStrictEqual(Schema.isMaxSize(2.9).annotations?.representation, { + id: "effect/schema/isMaxSize", + payload: { maxSize: 2 } + }) + }) + + it("normalizes isSizeBetween", () => { + assert.deepStrictEqual(Schema.isSizeBetween(1.9, 3.7).annotations?.representation, { + id: "effect/schema/isSizeBetween", + payload: { minimum: 1, maximum: 3 } + }) + }) +}) + +describe("SchemaRepresentation built-in object revivers", () => { + it("revives isMinProperties", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isMinProperties(2)), + id: "effect/schema/isMinProperties", + payload: { minProperties: 2 }, + reviver: Schema.isMinPropertiesReviver, + valid: { a: 1, b: 2 }, + invalid: { a: 1 } + }) + }) + + it("revives isMaxProperties", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isMaxProperties(1)), + id: "effect/schema/isMaxProperties", + payload: { maxProperties: 1 }, + reviver: Schema.isMaxPropertiesReviver, + valid: { a: 1 }, + invalid: { a: 1, b: 2 } + }) + }) + + it("revives isPropertiesLengthBetween", () => { + assertFilterReviver({ + schema: Schema.Any.check(Schema.isPropertiesLengthBetween(1, 2)), + id: "effect/schema/isPropertiesLengthBetween", + payload: { minimum: 1, maximum: 2 }, + reviver: Schema.isPropertiesLengthBetweenReviver, + valid: { a: 1 }, + invalid: {} + }) + }) + + it("revives isPropertyNames", () => { + const names = Schema.String.check(Schema.isPattern(/^[A-Z]/)) + assertFilterReviver({ + schema: Schema.Any.check(Schema.isPropertyNames(names)), + id: "effect/schema/isPropertyNames", + payload: null, + schemas: [names.ast], + reviver: Schema.isPropertyNamesReviver, + dependencies: [Schema.isPatternReviver], + valid: { Alpha: 1 }, + invalid: { alpha: 1 } + }) + }) + + it("persists the encoded key schema for isPropertyNames", () => { + const names = Schema.String.check(Schema.isPattern(/^[A-Z]/)) + const check = Schema.isPropertyNames(names) + + assert.deepStrictEqual(check.annotations?.representation, { + id: "effect/schema/isPropertyNames", + payload: null, + schemas: [names.ast] + }) + }) + + it("normalizes isMinProperties", () => { + assert.deepStrictEqual(Schema.isMinProperties(-1).annotations?.representation, { + id: "effect/schema/isMinProperties", + payload: { minProperties: 0 } + }) + }) + + it("normalizes isMaxProperties", () => { + assert.deepStrictEqual(Schema.isMaxProperties(2.9).annotations?.representation, { + id: "effect/schema/isMaxProperties", + payload: { maxProperties: 2 } + }) + }) + + it("normalizes isPropertiesLengthBetween", () => { + assert.deepStrictEqual(Schema.isPropertiesLengthBetween(1.9, 3.7).annotations?.representation, { + id: "effect/schema/isPropertiesLengthBetween", + payload: { minimum: 1, maximum: 3 } + }) + }) +}) + +describe("SchemaRepresentation built-in declaration revivers", () => { + it("revives Option", () => { + assertDeclarationReviver({ + schema: Schema.Option(Schema.String), + id: "effect/schema/Option", + payload: null, + reviver: Schema.OptionReviver + }) + }) + + it("revives Result", () => { + assertDeclarationReviver({ + schema: Schema.Result(Schema.String, Schema.Number), + id: "effect/schema/Result", + payload: null, + reviver: Schema.ResultReviver + }) + }) + + it("revives Redacted", () => { + assertDeclarationReviver({ + schema: Schema.Redacted(Schema.String), + id: "effect/schema/Redacted", + payload: null, + reviver: Schema.RedactedReviver + }) + }) + + it("revives CauseReason", () => { + assertDeclarationReviver({ + schema: Schema.CauseReason(Schema.String, Schema.Number), + id: "effect/schema/CauseReason", + payload: null, + reviver: Schema.CauseReasonReviver + }) + }) + + it("revives Cause", () => { + assertDeclarationReviver({ + schema: Schema.Cause(Schema.String, Schema.Number), + id: "effect/schema/Cause", + payload: null, + reviver: Schema.CauseReviver + }) + }) + + it("revives Error", () => { + assertDeclarationReviver({ + schema: Schema.ErrorInstance(), + id: "effect/schema/Error", + payload: null, + reviver: Schema.ErrorInstanceReviver + }) + }) + + it("revives Exit", () => { + assertDeclarationReviver({ + schema: Schema.Exit(Schema.String, Schema.Number, Schema.Boolean), + id: "effect/schema/Exit", + payload: null, + reviver: Schema.ExitReviver + }) + }) + + it("revives ReadonlyMap", () => { + assertDeclarationReviver({ + schema: Schema.ReadonlyMap(Schema.String, Schema.Number), + id: "effect/schema/ReadonlyMap", + payload: null, + reviver: Schema.ReadonlyMapReviver + }) + }) + + it("revives HashMap", () => { + assertDeclarationReviver({ + schema: Schema.HashMap(Schema.String, Schema.Number), + id: "effect/schema/HashMap", + payload: null, + reviver: Schema.HashMapReviver + }) + }) + + it("revives ReadonlySet", () => { + assertDeclarationReviver({ + schema: Schema.ReadonlySet(Schema.String), + id: "effect/schema/ReadonlySet", + payload: null, + reviver: Schema.ReadonlySetReviver + }) + }) + + it("revives HashSet", () => { + assertDeclarationReviver({ + schema: Schema.HashSet(Schema.String), + id: "effect/schema/HashSet", + payload: null, + reviver: Schema.HashSetReviver + }) + }) + + it("revives Chunk", () => { + assertDeclarationReviver({ + schema: Schema.Chunk(Schema.String), + id: "effect/schema/Chunk", + payload: null, + reviver: Schema.ChunkReviver + }) + }) + + it("revives RegExp", () => { + assertDeclarationReviver({ + schema: Schema.RegExp, + id: "effect/schema/RegExp", + payload: null, + reviver: Schema.RegExpReviver + }) + }) + + it("revives URL", () => { + assertDeclarationReviver({ + schema: Schema.URL, + id: "effect/schema/URL", + payload: null, + reviver: Schema.URLReviver + }) + }) + + it("revives Date", () => { + assertDeclarationReviver({ + schema: Schema.Date, + id: "effect/schema/Date", + payload: null, + reviver: Schema.DateReviver + }) + }) + + it("revives Duration", () => { + assertDeclarationReviver({ + schema: Schema.Duration, + id: "effect/schema/Duration", + payload: null, + reviver: Schema.DurationReviver + }) + }) + + it("revives BigDecimal", () => { + assertDeclarationReviver({ + schema: Schema.BigDecimal, + id: "effect/schema/BigDecimal", + payload: null, + reviver: Schema.BigDecimalReviver + }) + }) + + it("revives File", () => { + assertDeclarationReviver({ + schema: Schema.File, + id: "effect/schema/File", + payload: null, + reviver: Schema.FileReviver + }) + }) + + it("revives FormData", () => { + assertDeclarationReviver({ + schema: Schema.FormData, + id: "effect/schema/FormData", + payload: null, + reviver: Schema.FormDataReviver + }) + }) + + it("revives URLSearchParams", () => { + assertDeclarationReviver({ + schema: Schema.URLSearchParams, + id: "effect/schema/URLSearchParams", + payload: null, + reviver: Schema.URLSearchParamsReviver + }) + }) + + it("revives Uint8Array", () => { + assertDeclarationReviver({ + schema: Schema.Uint8Array, + id: "effect/schema/Uint8Array", + payload: null, + reviver: Schema.Uint8ArrayReviver + }) + }) + + it("revives DateTimeUtc", () => { + assertDeclarationReviver({ + schema: Schema.DateTimeUtc, + id: "effect/schema/DateTimeUtc", + payload: null, + reviver: Schema.DateTimeUtcReviver + }) + }) + + it("revives TimeZoneOffset", () => { + assertDeclarationReviver({ + schema: Schema.TimeZoneOffset, + id: "effect/schema/TimeZoneOffset", + payload: null, + reviver: Schema.TimeZoneOffsetReviver + }) + }) + + it("revives TimeZoneNamed", () => { + assertDeclarationReviver({ + schema: Schema.TimeZoneNamed, + id: "effect/schema/TimeZoneNamed", + payload: null, + reviver: Schema.TimeZoneNamedReviver + }) + }) + + it("revives TimeZone", () => { + assertDeclarationReviver({ + schema: Schema.TimeZone, + id: "effect/schema/TimeZone", + payload: null, + reviver: Schema.TimeZoneReviver + }) + }) + + it("revives DateTimeZoned", () => { + assertDeclarationReviver({ + schema: Schema.DateTimeZoned, + id: "effect/schema/DateTimeZoned", + payload: null, + reviver: Schema.DateTimeZonedReviver + }) + }) + + it("revives Json", () => { + assertDeclarationReviver({ + schema: Schema.Json, + id: "effect/schema/Json", + payload: null, + reviver: Schema.JsonReviver + }) + }) + + it("revives MutableJson", () => { + assertDeclarationReviver({ + schema: Schema.MutableJson, + id: "effect/schema/MutableJson", + payload: null, + reviver: Schema.MutableJsonReviver + }) + }) + + it("persists Error includeStack", () => { + assert.deepStrictEqual(Schema.ErrorInstance({ includeStack: true }).ast.annotations?.representation, { + id: "effect/schema/Error", + payload: { includeStack: true } + }) + }) + + it("persists Error excludeCause", () => { + assert.deepStrictEqual(Schema.ErrorInstance({ excludeCause: true }).ast.annotations?.representation, { + id: "effect/schema/Error", + payload: { excludeCause: true } + }) + }) + + it("omits disabled Error options", () => { + assert.deepStrictEqual( + Schema.ErrorInstance({ includeStack: false, excludeCause: false }).ast.annotations?.representation, + { id: "effect/schema/Error", payload: null } + ) + }) + + it("persists a Redacted label", () => { + assert.deepStrictEqual( + Schema.Redacted(Schema.String, { label: "password" }).ast.annotations?.representation, + { id: "effect/schema/Redacted", payload: { label: "password" } } + ) + }) + + it("persists Redacted disallowJsonEncode", () => { + assert.deepStrictEqual( + Schema.Redacted(Schema.String, { disallowJsonEncode: true }).ast.annotations?.representation, + { id: "effect/schema/Redacted", payload: { disallowJsonEncode: true } } + ) + }) + + it("omits disabled Redacted options", () => { + assert.deepStrictEqual( + Schema.Redacted(Schema.String, { + label: undefined, + disallowJsonEncode: false + }).ast.annotations?.representation, + { id: "effect/schema/Redacted", payload: null } + ) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts b/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts deleted file mode 100644 index 47ad4a860..000000000 --- a/.context/effect/packages/effect/test/schema/representation/fromASTs.test.ts +++ /dev/null @@ -1,1109 +0,0 @@ -import { Array as Arr, Option, Predicate, Schema, SchemaGetter, SchemaRepresentation } from "effect" -import { describe, it } from "vitest" -import { deepStrictEqual } from "../../utils/assert.ts" - -describe("fromASTs", () => { - function assertFromASTs(schemas: readonly [Schema.Constraint, ...Array], expected: { - readonly representations: readonly [ - SchemaRepresentation.Representation, - ...Array - ] - readonly references?: SchemaRepresentation.References - }) { - const document = SchemaRepresentation.fromASTs(Arr.map(schemas, (s) => s.ast)) - deepStrictEqual(document, { - representations: expected.representations, - references: expected.references ?? {} - }) - } - - it("should handle multiple schemas", () => { - const A = Schema.String.annotate({ identifier: "id", description: "a" }) - const B = Schema.String.annotate({ identifier: "id", description: "b" }) - const C = Schema.Tuple([A, B]) - assertFromASTs([A, B, C], { - representations: [ - { _tag: "Reference", $ref: "id" }, - { _tag: "Reference", $ref: "id1" }, - { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { _tag: "Reference", $ref: "id" } - }, - { - isOptional: false, - type: { _tag: "Reference", $ref: "id1" } - } - ], - rest: [], - checks: [] - } - ], - references: { - id: { - _tag: "String", - checks: [], - annotations: { identifier: "id", description: "a" } - }, - id1: { - _tag: "String", - checks: [], - annotations: { identifier: "id", description: "b" } - } - } - }) - }) -}) - -describe("fromAST", () => { - function assertFromAST(schema: Schema.Constraint, expected: { - readonly representation: SchemaRepresentation.Representation - readonly references?: SchemaRepresentation.References - }) { - const document = SchemaRepresentation.fromAST(schema.ast) - deepStrictEqual(document, { - representation: expected.representation, - references: expected.references ?? {} - }) - } - - describe("String", () => { - it("String", () => { - assertFromAST(Schema.String, { - representation: { - _tag: "String", - checks: [] - } - }) - }) - - it("String & brand", () => { - assertFromAST(Schema.String.pipe(Schema.brand("a")), { - representation: { - _tag: "String", - checks: [], - annotations: { brands: ["a"] } - } - }) - }) - - it("String & brand & brand", () => { - assertFromAST(Schema.String.pipe(Schema.brand("a"), Schema.brand("b")), { - representation: { - _tag: "String", - checks: [], - annotations: { brands: ["a", "b"] } - } - }) - }) - }) - - it("URL", () => { - assertFromAST(Schema.URL, { - representation: { - _tag: "Declaration", - annotations: { - expected: "URL", - typeConstructor: { _tag: "URL" }, - generation: { - runtime: "Schema.URL", - Type: "globalThis.URL" - } - }, - checks: [], - typeParameters: [], - encodedSchema: { - _tag: "String", - annotations: { - expected: "a string that will be decoded as a URL" - }, - checks: [] - } - } - }) - }) - - it("RegExp", () => { - assertFromAST(Schema.RegExp, { - representation: { - _tag: "Declaration", - annotations: { - expected: "RegExp", - typeConstructor: { _tag: "RegExp" }, - generation: { - runtime: "Schema.RegExp", - Type: "globalThis.RegExp" - } - }, - checks: [], - typeParameters: [], - encodedSchema: { - _tag: "Objects", - propertySignatures: [ - { - name: "source", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - }, - { - name: "flags", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("URLSearchParams", () => { - assertFromAST(Schema.URLSearchParams, { - representation: { - _tag: "Declaration", - annotations: { - expected: "URLSearchParams", - typeConstructor: { _tag: "URLSearchParams" }, - generation: { - runtime: "Schema.URLSearchParams", - Type: "globalThis.URLSearchParams" - } - }, - checks: [], - typeParameters: [], - encodedSchema: { - _tag: "String", - annotations: { - expected: "a query string that will be decoded as URLSearchParams" - }, - checks: [] - } - } - }) - }) - - it("Option(Number)", () => { - assertFromAST(Schema.Option(Schema.Number), { - representation: { - _tag: "Declaration", - annotations: { - expected: "Option", - typeConstructor: { _tag: "effect/Option" }, - generation: { - runtime: "Schema.Option(?)", - Type: "Option.Option", - importDeclaration: `import * as Option from "effect/Option"` - } - }, - checks: [], - typeParameters: [ - { _tag: "Number", checks: [] } - ], - encodedSchema: { - _tag: "Union", - types: [ - { - _tag: "Objects", - propertySignatures: [ - { - name: "_tag", - type: { _tag: "Literal", literal: "Some" }, - isOptional: false, - isMutable: false - }, - { - name: "value", - type: { _tag: "Number", checks: [] }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }, - { - _tag: "Objects", - propertySignatures: [ - { - name: "_tag", - type: { _tag: "Literal", literal: "None" }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - ], - mode: "anyOf" - } - } - }) - }) - - describe("node kinds", () => { - it("primitive nodes", () => { - assertFromAST( - Schema.Tuple([ - Schema.Null, - Schema.Undefined, - Schema.Void, - Schema.Never, - Schema.Unknown, - Schema.Any, - Schema.Boolean, - Schema.BigInt, - Schema.Symbol, - Schema.ObjectKeyword - ]), - { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "Null" } }, - { isOptional: false, type: { _tag: "Undefined" } }, - { isOptional: false, type: { _tag: "Void" } }, - { isOptional: false, type: { _tag: "Never" } }, - { isOptional: false, type: { _tag: "Unknown" } }, - { isOptional: false, type: { _tag: "Any" } }, - { isOptional: false, type: { _tag: "Boolean" } }, - { isOptional: false, type: { _tag: "BigInt", checks: [] } }, - { isOptional: false, type: { _tag: "Symbol" } }, - { isOptional: false, type: { _tag: "ObjectKeyword" } } - ], - rest: [], - checks: [] - } - } - ) - }) - - it("literal-like nodes", () => { - const symbol = Symbol.for("a") - assertFromAST( - Schema.Tuple([ - Schema.Literal("a"), - Schema.UniqueSymbol(symbol), - Schema.Enum({ A: "a", B: "b", One: 1 }), - Schema.TemplateLiteral(["a", Schema.String, Schema.Number]) - ]), - { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "Literal", literal: "a" } }, - { isOptional: false, type: { _tag: "UniqueSymbol", symbol } }, - { - isOptional: false, - type: { - _tag: "Enum", - enums: [["A", "a"], ["B", "b"], ["One", 1]] - } - }, - { - isOptional: false, - type: { - _tag: "TemplateLiteral", - parts: [ - { _tag: "Literal", literal: "a" }, - { _tag: "String", checks: [] }, - { _tag: "Number", checks: [] } - ] - } - } - ], - rest: [], - checks: [] - } - } - ) - }) - - it("string content schema", () => { - const document = SchemaRepresentation.fromAST( - Schema.fromJsonString(Schema.Struct({ a: Schema.String })).ast - ) - const representation = document.representation as SchemaRepresentation.String - deepStrictEqual(representation.contentSchema, { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }) - deepStrictEqual(representation.annotations?.expected, "a string that will be decoded as JSON") - deepStrictEqual(representation.annotations?.contentMediaType, "application/json") - }) - - it("tuple rest and mutable properties", () => { - assertFromAST( - Schema.Tuple([ - Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]), [Schema.Boolean]), - Schema.Struct({ a: Schema.mutableKey(Schema.String) }) - ]), - { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "String", checks: [] } }, - { isOptional: true, type: { _tag: "Number", checks: [] } } - ], - rest: [{ _tag: "Boolean" }], - checks: [] - } - }, - { - isOptional: false, - type: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: true - } - ], - indexSignatures: [], - checks: [] - } - } - ], - rest: [], - checks: [] - } - } - ) - }) - - it("declaration without an encoded schema", () => { - assertFromAST( - Schema.declare((u): u is string => typeof u === "string", { expected: "string declaration" }), - { - representation: { - _tag: "Declaration", - typeParameters: [], - encodedSchema: { _tag: "Null" }, - checks: [], - annotations: { expected: "string declaration" } - } - } - ) - }) - }) - - describe("checks", () => { - it("array and object checks", () => { - assertFromAST( - Schema.Tuple([ - Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isUnique()), - Schema.Record(Schema.String, Schema.Number).check( - Schema.isMinProperties(1), - Schema.isMaxProperties(2) - ) - ]), - { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { - _tag: "Arrays", - elements: [], - rest: [{ _tag: "String", checks: [] }], - checks: [ - { - _tag: "Filter", - meta: { _tag: "isMinLength", minLength: 1 }, - annotations: { expected: "a value with a length of at least 1" } - }, - { - _tag: "Filter", - meta: { _tag: "isUnique" }, - annotations: { expected: "an array with unique items" } - } - ] - } - }, - { - isOptional: false, - type: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { - parameter: { _tag: "String", checks: [] }, - type: { _tag: "Number", checks: [] } - } - ], - checks: [ - { - _tag: "Filter", - meta: { _tag: "isMinProperties", minProperties: 1 }, - annotations: { expected: "a value with at least 1 entry" } - }, - { - _tag: "Filter", - meta: { _tag: "isMaxProperties", maxProperties: 2 }, - annotations: { expected: "a value with at most 2 entries" } - } - ] - } - } - ], - rest: [], - checks: [] - } - } - ) - }) - - it("filter groups", () => { - assertFromAST( - Schema.String.check( - Schema.makeFilterGroup([ - Schema.isMinLength(1), - Schema.isMaxLength(2) - ], { description: "range" }) - ), - { - representation: { - _tag: "String", - checks: [ - { - _tag: "FilterGroup", - checks: [ - { - _tag: "Filter", - meta: { _tag: "isMinLength", minLength: 1 }, - annotations: { expected: "a value with a length of at least 1" } - }, - { - _tag: "Filter", - meta: { _tag: "isMaxLength", maxLength: 2 }, - annotations: { expected: "a value with a length of at most 2" } - } - ], - annotations: { description: "range" } - } - ] - } - } - ) - }) - - it("drops checks without representation metadata", () => { - assertFromAST( - Schema.String.check(Schema.makeFilter((s) => s.length > 0, { expected: "custom" })), - { - representation: { - _tag: "String", - checks: [] - } - } - ) - assertFromAST( - Schema.String.check( - Schema.makeFilterGroup([ - Schema.makeFilter((s) => s.length > 0, { expected: "custom" }) - ], { description: "group" }) - ), - { - representation: { - _tag: "String", - checks: [] - } - } - ) - }) - }) - - describe("Record", () => { - describe("checks", () => { - it("isPropertyNames", () => { - assertFromAST( - Schema.Record(Schema.String, Schema.Number) - .check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(/^[A-Z]/)))), - { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { - parameter: { _tag: "String", checks: [] }, - type: { _tag: "Number", checks: [] } - } - ], - checks: [ - { - _tag: "Filter", - meta: { - _tag: "isPropertyNames", - propertyNames: { - _tag: "String", - checks: [ - { - _tag: "Filter", - meta: { _tag: "isPattern", regExp: new RegExp("^[A-Z]") }, - annotations: { expected: "a string matching the RegExp ^[A-Z]" } - } - ] - } - }, - annotations: { expected: "an object with property names matching the schema" } - } - ] - }, - references: {} - } - ) - }) - }) - }) - - describe("Class", () => { - it("Class", () => { - class A extends Schema.Class("A")({ - a: Schema.String - }) {} - assertFromAST(A, { - representation: { _tag: "Reference", $ref: "A" }, - references: { - A: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { - _tag: "String", - checks: [] - }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("toType(Class)", () => { - class A extends Schema.Class("A")({ - a: Schema.String - }) {} - assertFromAST(Schema.toType(A), { - representation: { _tag: "Reference", $ref: "A" }, - references: { - A: { - _tag: "Declaration", - annotations: { - identifier: "A" - }, - checks: [], - typeParameters: [ - { _tag: "Reference", $ref: "A1" } - ], - encodedSchema: { _tag: "Reference", $ref: "A1" } - }, - A1: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { - _tag: "String", - checks: [] - }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("the type side and the class used together", () => { - class A extends Schema.Class("A")({ - a: Schema.String - }) {} - assertFromAST(Schema.Tuple([Schema.toType(A), A]), { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { _tag: "Reference", $ref: "A" } - }, - { - isOptional: false, - type: { _tag: "Reference", $ref: "A1" } - } - ], - rest: [], - checks: [] - }, - references: { - A: { - _tag: "Declaration", - annotations: { identifier: "A" }, - checks: [], - typeParameters: [ - { _tag: "Reference", $ref: "A1" } - ], - encodedSchema: { _tag: "Reference", $ref: "A1" } - }, - A1: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { - _tag: "String", - checks: [] - }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - }) - - describe("reference handling", () => { - it("using a schema with an identifier twice should point to the identifier as a reference", () => { - const S = Schema.String.annotate({ identifier: "id" }) - assertFromAST(Schema.Tuple([S, S]), { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { _tag: "Reference", $ref: "id" } - }, - { - isOptional: false, - type: { _tag: "Reference", $ref: "id" } - } - ], - rest: [], - checks: [] - }, - references: { - id: { - _tag: "String", - checks: [], - annotations: { identifier: "id" } - } - } - }) - }) - - it("should handle duplicate identifiers on different schemas with different representations", () => { - assertFromAST( - Schema.Union([ - Schema.String.annotate({ identifier: "id", description: "a" }), - Schema.String.annotate({ identifier: "id", description: "b" }) - ]), - { - representation: { - _tag: "Union", - mode: "anyOf", - types: [ - { _tag: "Reference", $ref: "id" }, - { _tag: "Reference", $ref: "id1" } - ] - }, - references: { - id: { - _tag: "String", - checks: [], - annotations: { identifier: "id", description: "a" } - }, - id1: { - _tag: "String", - checks: [], - annotations: { identifier: "id", description: "b" } - } - } - } - ) - }) - - it("should handle duplicate identifiers on different schemas with the same representation", () => { - const X = Schema.String.annotate({ title: "X", identifier: "X" }) - assertFromAST( - Schema.Struct({ - a: X, - b: Schema.NullOr(X), - c: Schema.optionalKey(X), - d: Schema.optionalKey(Schema.NullOr(X)), - e: Schema.NullOr(X).pipe( - Schema.encodeTo(Schema.optionalKey(X), { - decode: SchemaGetter.transformOptional(Option.orElseSome(() => null)), - encode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)) - }) - ) - }), - { - representation: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "Reference", $ref: "X" }, - isOptional: false, - isMutable: false - }, - { - name: "b", - type: { - _tag: "Union", - mode: "anyOf", - types: [ - { _tag: "Reference", $ref: "X" }, - { _tag: "Null" } - ] - }, - isOptional: false, - isMutable: false - }, - { - name: "c", - type: { _tag: "Reference", $ref: "X" }, - isOptional: true, - isMutable: false - }, - { - name: "d", - type: { - _tag: "Union", - mode: "anyOf", - types: [ - { _tag: "Reference", $ref: "X" }, - { _tag: "Null" } - ] - }, - isOptional: true, - isMutable: false - }, - { - name: "e", - type: { _tag: "Reference", $ref: "X" }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }, - references: { - X: { - _tag: "String", - checks: [], - annotations: { identifier: "X", title: "X" } - } - } - } - ) - }) - - describe("suspend", () => { - it("non-recursive", () => { - assertFromAST(Schema.suspend(() => Schema.String), { - representation: { - _tag: "Suspend", - checks: [], - thunk: { - _tag: "String", - checks: [] - } - } - }) - }) - - it("no identifier annotation", () => { - type A = { - readonly a?: A - } - const A = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) - }) - - assertFromAST(A, { - representation: { _tag: "Reference", $ref: "Objects_" }, - references: { - Objects_: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { - _tag: "Suspend", - checks: [], - thunk: { _tag: "Reference", $ref: "Objects_" } - }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("outer identifier annotation", () => { - type A = { - readonly a?: A - } - const A = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) - }).annotate({ identifier: "A" }) // outer identifier annotation - - assertFromAST(A, { - representation: { _tag: "Reference", $ref: "A" }, - references: { - A: { - _tag: "Objects", - annotations: { identifier: "A" }, - propertySignatures: [ - { - name: "a", - type: { - _tag: "Suspend", - checks: [], - thunk: { _tag: "Reference", $ref: "A" } - }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("inner identifier annotation", () => { - type A = { - readonly a?: A - } - const A = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A.annotate({ identifier: "A" }))) - }) - - assertFromAST(A, { - representation: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "Reference", $ref: "Suspend_" }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }, - references: { - A: { - _tag: "Objects", - annotations: { identifier: "A" }, - propertySignatures: [ - { - name: "a", - type: { _tag: "Reference", $ref: "Suspend_" }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }, - Suspend_: { - _tag: "Suspend", - checks: [], - thunk: { _tag: "Reference", $ref: "A" } - } - } - }) - }) - - it("suspend identifier annotation", () => { - type A = { - readonly a?: A - } - const A = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A).annotate({ identifier: "A" })) - }) - - assertFromAST(A, { - representation: { _tag: "Reference", $ref: "Objects_" }, - references: { - A: { - _tag: "Suspend", - annotations: { identifier: "A" }, - checks: [], - thunk: { _tag: "Reference", $ref: "Objects_" } - }, - Objects_: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "Reference", $ref: "A" }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - - it("duplicate identifiers", () => { - type A = { - readonly a?: A - } - const A = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) - }).annotate({ identifier: "A" }) - - type A1 = { - readonly a?: A1 - } - const A1 = Schema.Struct({ - a: Schema.optionalKey(Schema.suspend((): Schema.Codec => A1)) - }).annotate({ identifier: "A" }) - - const schema = Schema.Tuple([A, A1]) - assertFromAST(schema, { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { _tag: "Reference", $ref: "A" } - }, - { - isOptional: false, - type: { _tag: "Reference", $ref: "A1" } - } - ], - rest: [], - checks: [] - }, - references: { - A: { - _tag: "Objects", - annotations: { identifier: "A" }, - propertySignatures: [ - { - name: "a", - type: { - _tag: "Suspend", - checks: [], - thunk: { _tag: "Reference", $ref: "A" } - }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - }, - A1: { - _tag: "Objects", - annotations: { identifier: "A" }, - propertySignatures: [ - { - name: "a", - type: { - _tag: "Suspend", - checks: [], - thunk: { _tag: "Reference", $ref: "A1" } - }, - isOptional: true, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - }) - - describe("transformation schemas with identifiers", () => { - it("Class", () => { - class A extends Schema.Class("A")({ - a: Schema.String - }) {} - assertFromAST(Schema.Tuple([A, A]), { - representation: { - _tag: "Arrays", - elements: [ - { - isOptional: false, - type: { _tag: "Reference", $ref: "A" } - }, - { - isOptional: false, - type: { _tag: "Reference", $ref: "A" } - } - ], - rest: [], - checks: [] - }, - references: { - A: { - _tag: "Objects", - propertySignatures: [ - { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - } - ], - indexSignatures: [], - checks: [] - } - } - }) - }) - }) - }) -}) diff --git a/.context/effect/packages/effect/test/schema/representation/fromJson.test.ts b/.context/effect/packages/effect/test/schema/representation/fromJson.test.ts new file mode 100644 index 000000000..4cdfbbb8e --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/fromJson.test.ts @@ -0,0 +1,436 @@ +import { assert, describe, it } from "@effect/vitest" +import { SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +describe("SchemaRepresentation.fromJson", () => { + it("decodes a document", () => { + const input = { + representation: { + _tag: "String", + annotations: { description: "value" }, + checks: [] + }, + references: {} + } as const + + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Null", () => { + const input = { representation: { _tag: "Null", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Undefined", () => { + const input = { representation: { _tag: "Undefined", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Void", () => { + const input = { representation: { _tag: "Void", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Never", () => { + const input = { representation: { _tag: "Never", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Unknown", () => { + const input = { representation: { _tag: "Unknown", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Any", () => { + const input = { representation: { _tag: "Any", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Number", () => { + const input = { representation: { _tag: "Number", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Boolean", () => { + const input = { representation: { _tag: "Boolean", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes BigInt", () => { + const input = { representation: { _tag: "BigInt", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Symbol", () => { + const input = { representation: { _tag: "Symbol", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes ObjectKeyword", () => { + const input = { representation: { _tag: "ObjectKeyword", checks: [] }, references: {} } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Enum", () => { + const input = { + representation: { + _tag: "Enum", + enums: [ + ["A", { type: "string", value: "a" }], + ["One", { type: "number", value: 1 }] + ], + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), { + representation: { + _tag: "Enum", + enums: [["A", "a"], ["One", 1]], + checks: [] + }, + references: {} + }) + }) + + it("decodes TemplateLiteral", () => { + const input = { + representation: { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: { type: "string", value: "prefix-" }, checks: [] }, + { _tag: "String", checks: [] } + ], + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), { + representation: { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "prefix-", checks: [] }, + { _tag: "String", checks: [] } + ], + checks: [] + }, + references: {} + }) + }) + + it("decodes Arrays", () => { + const input = { + representation: { + _tag: "Arrays", + elements: [{ type: { _tag: "String", checks: [] }, isOptional: false }], + rest: [{ _tag: "Number", checks: [] }], + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Objects", () => { + const input = { + representation: { + _tag: "Objects", + propertySignatures: [{ + name: { type: "string", value: "value" }, + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: true + }], + indexSignatures: [{ + parameter: { _tag: "String", checks: [] }, + type: { _tag: "Number", checks: [] } + }], + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), { + representation: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: true + }], + indexSignatures: [{ + parameter: { _tag: "String", checks: [] }, + type: { _tag: "Number", checks: [] } + }], + checks: [] + }, + references: {} + }) + }) + + it("decodes Union", () => { + const input = { + representation: { + _tag: "Union", + types: [{ _tag: "String", checks: [] }, { _tag: "Number", checks: [] }], + mode: "oneOf", + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Reference", () => { + const input = { + representation: { _tag: "Reference", $ref: "Value" }, + references: { Value: { _tag: "String", checks: [] } } + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Suspend", () => { + const input = { + representation: { + _tag: "Suspend", + thunk: { _tag: "Reference", $ref: "Value" }, + checks: [] + }, + references: { Value: { _tag: "String", checks: [] } } + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Declaration", () => { + const input = { + representation: { + _tag: "Declaration", + representation: { id: "acme/schema/Value", payload: null }, + annotations: {}, + typeParameters: [{ _tag: "String", checks: [] }], + checks: [] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes Filter", () => { + const input = { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { id: "acme/schema/filter", payload: null }, + annotations: {}, + aborted: true + }] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes FilterGroup", () => { + const input = { + representation: { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + checks: [{ + _tag: "Filter", + representation: { id: "acme/schema/filter", payload: null }, + annotations: {}, + aborted: false + }] + }] + }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), input) + }) + + it("decodes bigint structural values", () => { + assert.deepStrictEqual( + SchemaRepresentation.fromJson({ + representation: { + _tag: "Literal", + literal: { type: "bigint", value: "1" }, + checks: [] + }, + references: {} + }), + { + representation: { _tag: "Literal", literal: 1n, checks: [] }, + references: {} + } + ) + }) + + it("rejects mismatched literal types", () => { + for ( + const representation of [ + { _tag: "Literal", literal: { type: "bigint", value: "not-an-integer" }, checks: [] }, + { _tag: "Literal", literal: { type: "boolean", value: "true" }, checks: [] }, + { _tag: "Literal", literal: { type: "string", value: 1 }, checks: [] } + ] as const + ) { + throws(() => SchemaRepresentation.fromJson({ representation, references: {} })) + } + }) + + it("rejects mismatched Enum value types", () => { + throws(() => + SchemaRepresentation.fromJson({ + representation: { + _tag: "Enum", + enums: [["One", { type: "number", value: "1" }]], + checks: [] + }, + references: {} + }) + ) + }) + + it("rejects mismatched property name types", () => { + throws(() => + SchemaRepresentation.fromJson({ + representation: { + _tag: "Objects", + propertySignatures: [{ + name: { type: "number", value: "1" }, + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + }, + references: {} + }) + ) + }) + + it("preserves string literals resembling non-finite numbers", () => { + for (const literal of ["NaN", "Infinity", "-Infinity"]) { + const input = { + representation: { _tag: "Literal", literal: { type: "string", value: literal }, checks: [] }, + references: {} + } as const + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), { + representation: { _tag: "Literal", literal, checks: [] }, + references: {} + }) + } + }) + + it("decodes global symbols", () => { + assert.deepStrictEqual( + SchemaRepresentation.fromJson({ + representation: { + _tag: "UniqueSymbol", + symbol: "Symbol(acme/schema/key)", + checks: [] + }, + references: {} + }), + { + representation: { + _tag: "UniqueSymbol", + symbol: Symbol.for("acme/schema/key"), + checks: [] + }, + references: {} + } + ) + }) + + it("does not coerce strings resembling symbols", () => { + const input = { + representation: { _tag: "Literal", literal: { type: "string", value: "Symbol(a)" }, checks: [] }, + references: {} + } as const + + assert.deepStrictEqual(SchemaRepresentation.fromJson(input), { + representation: { _tag: "Literal", literal: "Symbol(a)", checks: [] }, + references: {} + }) + }) + + it("requires representation on persisted Filters", () => { + throws( + () => + SchemaRepresentation.fromJson({ + representation: { _tag: "String", checks: [{ _tag: "Filter", aborted: false }] }, + references: {} + }), + `Missing key\n at ["representation"]["checks"][0]["representation"]` + ) + }) + + it("requires representation on persisted Declarations", () => { + throws( + () => + SchemaRepresentation.fromJson({ + representation: { _tag: "Declaration", typeParameters: [], checks: [] }, + references: {} + }), + `Missing key\n at ["representation"]["representation"]` + ) + }) + + it("rejects empty references", () => { + throws( + () => + SchemaRepresentation.fromJson({ + representation: { _tag: "Reference", $ref: "" }, + references: {} + }), + `Expected a value with a length of at least 1\n at ["representation"]["$ref"]` + ) + }) + + it("rejects non-JSON annotations", () => { + throws( + () => + SchemaRepresentation.fromJson({ + representation: { + _tag: "String", + annotations: { invalid: 1n }, + checks: [] + }, + references: {} + } as never), + `Expected JSON value\n at ["representation"]["annotations"]["invalid"]` + ) + }) + + it("rejects non-JSON roots", () => { + const input = () => undefined + throws( + () => SchemaRepresentation.fromJson(input as never), + `Expected object` + ) + }) + + it("does not invoke toJSON", () => { + let calls = 0 + const input = { + representation: { _tag: "String", checks: [], unexpected: true }, + references: {}, + toJSON() { + calls++ + return null + } + } + + assert.deepStrictEqual( + SchemaRepresentation.fromJson(input as never), + { + representation: { _tag: "String", checks: [] }, + references: {} + } + ) + assert.strictEqual(calls, 0) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/fromJsonMultiDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/fromJsonMultiDocument.test.ts new file mode 100644 index 000000000..ab6f39a2a --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/fromJsonMultiDocument.test.ts @@ -0,0 +1,48 @@ +import { assert, describe, it } from "@effect/vitest" +import { SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +describe("SchemaRepresentation.fromJsonMultiDocument", () => { + it("decodes every root in order", () => { + const input = { + representations: [ + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] }, + { _tag: "Literal", literal: { type: "string", value: "1" }, checks: [] }, + { _tag: "Literal", literal: { type: "bigint", value: "1" }, checks: [] } + ], + references: {} + } as const + + assert.deepStrictEqual(SchemaRepresentation.fromJsonMultiDocument(input), { + representations: [ + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] }, + { _tag: "Literal", literal: "1", checks: [] }, + { _tag: "Literal", literal: 1n, checks: [] } + ], + references: {} + }) + }) + + it("decodes shared references", () => { + const input = { + representations: [ + { _tag: "Reference", $ref: "Shared" }, + { _tag: "Reference", $ref: "Shared" } + ], + references: { + Shared: { _tag: "String", checks: [] } + } + } as const + + assert.deepStrictEqual(SchemaRepresentation.fromJsonMultiDocument(input), input) + }) + + it("rejects an empty roots array", () => { + throws( + () => SchemaRepresentation.fromJsonMultiDocument({ representations: [], references: {} }), + `Missing key\n at ["representations"][0]` + ) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaDocument.test.ts index 9c526a7b8..c1f6347ec 100644 --- a/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaDocument.test.ts @@ -1,55 +1,54 @@ import { JsonSchema, Schema, SchemaRepresentation } from "effect" import { describe, it } from "vitest" -import { assertFalse, assertTrue, deepStrictEqual, strictEqual } from "../../utils/assert.ts" - -const json = (annotations?: Schema.Annotations.Annotations) => { - const representation = SchemaRepresentation.fromAST(Schema.Json.ast).representation - return annotations === undefined - ? representation - : { - ...representation, - annotations: { - ...("annotations" in representation ? representation.annotations : undefined), - ...annotations - } - } +import { assertFalse, assertTrue, deepStrictEqual, strictEqual, throws } from "../../utils/assert.ts" + +function toSchemaFromJsonSchemaDocument( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions +): Schema.Top { + return SchemaRepresentation.fromJsonSchemaDocument(document, { patterns: "apply", ...options }) +} + +function fromJsonSchemaRepresentation( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions +): SchemaRepresentation.Document { + return SchemaRepresentation.toRepresentation(toSchemaFromJsonSchemaDocument(document, options).ast) } describe("fromJsonSchemaDocument", () => { function assertFromJsonSchema( input: { readonly schema: JsonSchema.JsonSchema - readonly options?: { - readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined - } + readonly options?: SchemaRepresentation.FromJsonSchemaOptions }, - expected: { - readonly representation: SchemaRepresentation.Representation - readonly references?: Record - }, - runtime?: string + expected: Schema.Json ) { - const expectedDocument: SchemaRepresentation.Document = { - representation: expected.representation, - references: expected.references ?? {} - } const jsonDocument = JsonSchema.fromSchemaDraft2020_12(input.schema) - const document = SchemaRepresentation.fromJsonSchemaDocument(jsonDocument, input.options) - deepStrictEqual(document, expectedDocument) - const multiDocument = SchemaRepresentation.toMultiDocument(document) - if (runtime !== undefined) { - strictEqual(SchemaRepresentation.toCodeDocument(multiDocument).codes[0].runtime, runtime) - } - return document + const schema = toSchemaFromJsonSchemaDocument(jsonDocument, input.options) + const document = SchemaRepresentation.toRepresentation(schema.ast) + deepStrictEqual(SchemaRepresentation.toJson(document), expected) + return schema } - it("{}", () => { + it("unconstrained schema", () => { assertFromJsonSchema( { schema: {} }, { - representation: json() - }, - "Schema.Json" + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) assertFromJsonSchema( { @@ -63,34 +62,63 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: json({ - title: "a", - description: "b", - default: "c", - examples: ["d"], - readOnly: true, - writeOnly: true - }) - }, - `Schema.Json.annotate({ "title": "a", "description": "b", "default": "c", "examples": ["d"], "readOnly": true, "writeOnly": true })` + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value", + "title": "a", + "description": "b", + "default": "c", + "examples": [ + "d" + ], + "readOnly": true, + "writeOnly": true + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) }) describe("const", () => { - it("const: literal (string)", () => { + it("string literal", () => { assertFromJsonSchema( { schema: { const: "a" } }, { - representation: { _tag: "Literal", literal: "a" } - }, - `Schema.Literal("a")` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) assertFromJsonSchema( { schema: { const: "a", description: "a" } }, { - representation: { _tag: "Literal", literal: "a", annotations: { description: "a" } } - }, - `Schema.Literal("a").annotate({ "description": "a" })` + "representation": { + "_tag": "Literal", + "annotations": { + "description": "a" + }, + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) }) @@ -98,9 +126,16 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { const: 1 } }, { - representation: { _tag: "Literal", literal: 1 } - }, - `Schema.Literal(1)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + "references": {} + } ) }) @@ -108,26 +143,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { const: true } }, { - representation: { _tag: "Literal", literal: true } - }, - `Schema.Literal(true)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } ) }) - it("const: null", () => { + it("null literal", () => { assertFromJsonSchema( { schema: { const: null } }, { - representation: { _tag: "Null" } - }, - `Schema.Null` + "representation": { + "_tag": "Null", + "checks": [] + }, + "references": {} + } ) assertFromJsonSchema( { schema: { const: null, description: "a" } }, { - representation: { _tag: "Null", annotations: { description: "a" } } - }, - `Schema.Null.annotate({ "description": "a" })` + "representation": { + "_tag": "Null", + "annotations": { + "description": "a" + }, + "checks": [] + }, + "references": {} + } ) }) @@ -135,28 +186,56 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { const: {} } }, { - representation: json() - }, - `Schema.Json` + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) }) }) describe("enum", () => { - it("single enum (string)", () => { + it("single string member", () => { assertFromJsonSchema( { schema: { enum: ["a"] } }, { - representation: { _tag: "Literal", literal: "a" } - }, - `Schema.Literal("a")` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) assertFromJsonSchema( { schema: { enum: ["a"], description: "a" } }, { - representation: { _tag: "Literal", literal: "a", annotations: { description: "a" } } - }, - `Schema.Literal("a").annotate({ "description": "a" })` + "representation": { + "_tag": "Literal", + "annotations": { + "description": "a" + }, + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) }) @@ -164,9 +243,16 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { enum: [1] } }, { - representation: { _tag: "Literal", literal: 1 } - }, - `Schema.Literal(1)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + "references": {} + } ) }) @@ -174,41 +260,80 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { enum: [true] } }, { - representation: { _tag: "Literal", literal: true } - }, - `Schema.Literal(true)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } ) }) - it("multiple enum (literals)", () => { + it("multiple literal members", () => { assertFromJsonSchema( { schema: { enum: ["a", 1] } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, - { _tag: "Literal", literal: 1 } + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + } ], - mode: "anyOf" - } - }, - `Schema.Literals(["a", 1])` + "mode": "anyOf" + }, + "references": {} + } ) assertFromJsonSchema( { schema: { enum: ["a", 1], description: "a" } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, - { _tag: "Literal", literal: 1 } + "representation": { + "_tag": "Union", + "annotations": { + "description": "a" + }, + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + } ], - mode: "anyOf", - annotations: { description: "a" } - } - }, - `Schema.Literals(["a", 1]).annotate({ "description": "a" })` + "mode": "anyOf" + }, + "references": {} + } ) }) @@ -216,16 +341,27 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { enum: ["a", null] } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, - { _tag: "Null" } + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + { + "_tag": "Null", + "checks": [] + } ], - mode: "anyOf" - } - }, - `Schema.Union([Schema.Literal("a"), Schema.Null])` + "mode": "anyOf" + }, + "references": {} + } ) }) }) @@ -234,23 +370,46 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { anyOf: [{ const: "a" }, { enum: [1, 2] }] } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, { - _tag: "Union", - types: [ - { _tag: "Literal", literal: 1 }, - { _tag: "Literal", literal: 2 } + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 2 + } + } ], - mode: "anyOf" + "mode": "anyOf" } ], - mode: "anyOf" - } - }, - `Schema.Union([Schema.Literal("a"), Schema.Literals([1, 2])])` + "mode": "anyOf" + }, + "references": {} + } ) }) @@ -268,35 +427,132 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [ + "representation": { + "_tag": "Union", + "checks": [], + "types": [ { - _tag: "Objects", - propertySignatures: [ - { name: "a", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } }, - { name: "id", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } } + "_tag": "Objects", + "checks": [], + "propertySignatures": [ + { + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + }, + { + "name": { + "type": "string", + "value": "id" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + } ], - indexSignatures: [{ parameter: { _tag: "String", checks: [] }, type: json() }], - checks: [] + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] }, { - _tag: "Objects", - propertySignatures: [ + "_tag": "Objects", + "checks": [], + "propertySignatures": [ { - name: "b", - isOptional: false, - isMutable: false, - type: { _tag: "Number", checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }] } + "name": { + "type": "string", + "value": "b" + }, + "type": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + }, + "isOptional": false, + "isMutable": false }, - { name: "id", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } } + { + "name": { + "type": "string", + "value": "id" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + } ], - indexSignatures: [{ parameter: { _tag: "String", checks: [] }, type: json() }], - checks: [] + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] } ], - mode: "anyOf" - } + "mode": "anyOf" + }, + "references": {} } ) }) @@ -305,23 +561,46 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { oneOf: [{ const: "a" }, { enum: [1, 2] }] } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, + "representation": { + "_tag": "Union", + "checks": [], + "types": [ { - _tag: "Union", - types: [ - { _tag: "Literal", literal: 1 }, - { _tag: "Literal", literal: 2 } + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 2 + } + } ], - mode: "anyOf" + "mode": "anyOf" } ], - mode: "oneOf" - } - }, - `Schema.Union([Schema.Literal("a"), Schema.Literals([1, 2])], { mode: "oneOf" })` + "mode": "oneOf" + }, + "references": {} + } ) }) @@ -339,35 +618,132 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [ + "representation": { + "_tag": "Union", + "checks": [], + "types": [ { - _tag: "Objects", - propertySignatures: [ - { name: "a", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } }, - { name: "id", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } } + "_tag": "Objects", + "checks": [], + "propertySignatures": [ + { + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + }, + { + "name": { + "type": "string", + "value": "id" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + } ], - indexSignatures: [{ parameter: { _tag: "String", checks: [] }, type: json() }], - checks: [] + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] }, { - _tag: "Objects", - propertySignatures: [ + "_tag": "Objects", + "checks": [], + "propertySignatures": [ { - name: "b", - isOptional: false, - isMutable: false, - type: { _tag: "Number", checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }] } + "name": { + "type": "string", + "value": "b" + }, + "type": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + }, + "isOptional": false, + "isMutable": false }, - { name: "id", isOptional: false, isMutable: false, type: { _tag: "String", checks: [] } } + { + "name": { + "type": "string", + "value": "id" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + } ], - indexSignatures: [{ parameter: { _tag: "String", checks: [] }, type: json() }], - checks: [] + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] } ], - mode: "oneOf" - } + "mode": "oneOf" + }, + "references": {} } ) }) @@ -377,9 +753,12 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "null" } }, { - representation: { _tag: "Null" } - }, - `Schema.Null` + "representation": { + "_tag": "Null", + "checks": [] + }, + "references": {} + } ) }) }) @@ -389,9 +768,12 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "string" } }, { - representation: { _tag: "String", checks: [] } - }, - `Schema.String` + "representation": { + "_tag": "String", + "checks": [] + }, + "references": {} + } ) }) @@ -400,12 +782,32 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "string", minLength: 1 } }, { - representation: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }] - } - }, - `Schema.String.check(Schema.isMinLength(1))` + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -413,39 +815,102 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "string", maxLength: 1 } }, { - representation: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 1 } }] - } - }, - `Schema.String.check(Schema.isMaxLength(1))` + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at most 1", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 1 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) - it("pattern", () => { + it("pattern with an explicit string type", () => { assertFromJsonSchema( { schema: { type: "string", pattern: "a*" } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("a*") } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "a*", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp a*", + "arbitrary": { + "constraint": { + "patterns": [ + "a*" + ] + } + } + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isPattern(new RegExp("a*")))` + }, + "references": {} + } ) + }) + + it("pattern infers the string type", () => { assertFromJsonSchema( { schema: { pattern: "a*" } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("a*") } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "a*", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp a*", + "arbitrary": { + "constraint": { + "patterns": [ + "a*" + ] + } + } + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isPattern(new RegExp("a*")))` + }, + "references": {} + } ) }) }) @@ -456,14 +921,30 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number" } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } } - ] - } - }, - `Schema.Number.check(Schema.isFinite())` + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -472,15 +953,43 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number", minimum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThanOrEqualTo", + "payload": { + "minimum": 1 + } + }, + "annotations": { + "expected": "a value greater than or equal to 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThanOrEqualTo(1))` + }, + "references": {} + } ) }) @@ -488,15 +997,43 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number", maximum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThanOrEqualTo", + "payload": { + "maximum": 1 + } + }, + "annotations": { + "expected": "a value less than or equal to 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isLessThanOrEqualTo(1))` + }, + "references": {} + } ) }) @@ -504,15 +1041,43 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number", exclusiveMinimum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isGreaterThan", exclusiveMinimum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThan", + "payload": { + "exclusiveMinimum": 1 + } + }, + "annotations": { + "expected": "a value greater than 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isGreaterThan(1))` + }, + "references": {} + } ) }) @@ -520,15 +1085,43 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number", exclusiveMaximum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isLessThan", exclusiveMaximum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThan", + "payload": { + "exclusiveMaximum": 1 + } + }, + "annotations": { + "expected": "a value less than 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isLessThan(1))` + }, + "references": {} + } ) }) @@ -536,15 +1129,43 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "number", multipleOf: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isMultipleOf", divisor: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMultipleOf", + "payload": { + "divisor": 1 + } + }, + "annotations": { + "expected": "a value that is a multiple of 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isMultipleOf(1))` + }, + "references": {} + } ) }) }) @@ -555,14 +1176,29 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer" } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt())` + }, + "references": {} + } ) }) @@ -571,15 +1207,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer", minimum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThanOrEqualTo", + "payload": { + "minimum": 1 + } + }, + "annotations": { + "expected": "a value greater than or equal to 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(1))` + }, + "references": {} + } ) }) @@ -587,15 +1250,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer", maximum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThanOrEqualTo", + "payload": { + "maximum": 1 + } + }, + "annotations": { + "expected": "a value less than or equal to 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isLessThanOrEqualTo(1))` + }, + "references": {} + } ) }) @@ -603,15 +1293,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer", exclusiveMinimum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isGreaterThan", exclusiveMinimum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThan", + "payload": { + "exclusiveMinimum": 1 + } + }, + "annotations": { + "expected": "a value greater than 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isGreaterThan(1))` + }, + "references": {} + } ) }) @@ -619,15 +1336,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer", exclusiveMaximum: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isLessThan", exclusiveMaximum: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThan", + "payload": { + "exclusiveMaximum": 1 + } + }, + "annotations": { + "expected": "a value less than 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isLessThan(1))` + }, + "references": {} + } ) }) @@ -635,15 +1379,42 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "integer", multipleOf: 1 } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isMultipleOf", divisor: 1 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMultipleOf", + "payload": { + "divisor": 1 + } + }, + "annotations": { + "expected": "a value that is a multiple of 1" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isMultipleOf(1))` + }, + "references": {} + } ) }) }) @@ -654,9 +1425,12 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "boolean" } }, { - representation: { _tag: "Boolean" } - }, - `Schema.Boolean` + "representation": { + "_tag": "Boolean", + "checks": [] + }, + "references": {} + } ) }) }) @@ -666,14 +1440,27 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "array" } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [] - } - }, - `Schema.Array(Schema.Json)` + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) }) @@ -686,54 +1473,257 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { _tag: "Arrays", elements: [], rest: [{ _tag: "String", checks: [] }], checks: [] } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [], + "rest": [ + { + "_tag": "String", + "checks": [] + } + ] + }, + "references": {} + } + ) + }) + + it("prefixItems preserves maxItems below the prefix length", () => { + assertFromJsonSchema( + { + schema: { + type: "array", + prefixItems: [{ type: "string" }, { type: "number" }], + maxItems: 1 + } }, - `Schema.Array(Schema.String)` + { + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at most 1", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 1 + } + } + }, + "aborted": false + } + ], + "elements": [ + { + "isOptional": true, + "type": { + "_tag": "String", + "checks": [] + } + }, + { + "isOptional": true, + "type": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + } + } + ], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) }) - it("prefixItems", () => { + it("prefixItems preserves maxItems above the prefix length", () => { assertFromJsonSchema( { schema: { type: "array", prefixItems: [{ type: "string" }], - maxItems: 1 + maxItems: 2 } }, { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: true, type: { _tag: "String", checks: [] } } + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + } ], - rest: [], - checks: [] + "elements": [ + { + "isOptional": true, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } + ) + }) + + it("prefixItems omits maxItems redundant with items: false", () => { + assertFromJsonSchema( + { + schema: { + type: "array", + prefixItems: [{ type: "string" }], + items: false, + maxItems: 2 } }, - `Schema.Tuple([Schema.optionalKey(Schema.String)])` + { + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": true, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [] + }, + "references": {} + } ) + }) + it("prefixItems closes when maxItems equals the prefix length", () => { assertFromJsonSchema( { schema: { type: "array", prefixItems: [{ type: "string" }], - minItems: 1, maxItems: 1 } }, { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "String", checks: [] } } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": true, + "type": { + "_tag": "String", + "checks": [] + } + } ], - rest: [], - checks: [] + "rest": [] + }, + "references": {} + } + ) + }) + + it("prefixItems marks elements required by minItems", () => { + assertFromJsonSchema( + { + schema: { + type: "array", + prefixItems: [{ type: "string" }], + minItems: 1, + maxItems: 1 } }, - `Schema.Tuple([Schema.String])` + { + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [] + }, + "references": {} + } ) }) @@ -748,18 +1738,45 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "String", checks: [] } } - ], - rest: [ - { _tag: "Number", checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }] } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + } ], - checks: [] - } - }, - `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number.check(Schema.isFinite())])` + "rest": [ + { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + } + ] + }, + "references": {} + } ) }) @@ -768,14 +1785,47 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "array", minItems: 1 } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [{ _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }] - } - }, - `Schema.Array(Schema.Json).check(Schema.isMinLength(1))` + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) }) @@ -783,14 +1833,47 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "array", maxItems: 1 } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [{ _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 1 } }] - } - }, - `Schema.Array(Schema.Json).check(Schema.isMaxLength(1))` + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at most 1", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 1 + } + } + }, + "aborted": false + } + ], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) }) @@ -798,35 +1881,93 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "array", uniqueItems: true } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [{ _tag: "Filter", meta: { _tag: "isUnique" } }] - } - }, - `Schema.Array(Schema.Json).check(Schema.isUnique())` + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isUnique", + "payload": null + }, + "annotations": { + "expected": "an array with unique items", + "arbitrary": { + "constraint": { + "unique": true + } + } + }, + "aborted": false + } + ], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } + ) + }) + + it("does not require uniqueness when uniqueItems is false", () => { + const schema = toSchemaFromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ type: "array", uniqueItems: false }) ) + + assertTrue(Schema.is(schema)(["a", "a"])) }) }) }) describe("type: object", () => { - it("type only", () => { + it("allows additional properties by default", () => { assertFromJsonSchema( { schema: { type: "object" } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: json() } - ], - checks: [] - } - }, - `Schema.Record(Schema.String, Schema.Json)` + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] + }, + "references": {} + } ) + }) + + it("closes an object when additionalProperties is false", () => { assertFromJsonSchema( { schema: { @@ -835,14 +1976,14 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [], - checks: [] - } - }, - `Schema.Struct({ })` + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [] + }, + "references": {} + } ) }) @@ -855,16 +1996,25 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: { _tag: "Boolean" } } - ], - checks: [] - } - }, - `Schema.Record(Schema.String, Schema.Boolean)` + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Boolean", + "checks": [] + } + } + ] + }, + "references": {} + } ) }) @@ -879,27 +2029,39 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [ + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [ { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false }, { - name: "b", - type: { _tag: "String", checks: [] }, - isOptional: true, - isMutable: false + "name": { + "type": "string", + "value": "b" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": true, + "isMutable": false } ], - indexSignatures: [], - checks: [] - } - }, - `Schema.Struct({ "a": Schema.String, "b": Schema.optionalKey(Schema.String) })` + "indexSignatures": [] + }, + "references": {} + } ) }) @@ -914,25 +2076,42 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [{ - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - }], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: { _tag: "Boolean" } } + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [ + { + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + } ], - checks: [] - } - }, - `Schema.StructWithRest(Schema.Struct({ "a": Schema.String }), [Schema.Record(Schema.String, Schema.Boolean)])` + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Boolean", + "checks": [] + } + } + ] + }, + "references": {} + } ) }) - it("patternProperties", () => { + it("imports a single pattern property", () => { assertFromJsonSchema( { schema: { @@ -944,23 +2123,51 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [ { - parameter: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("a*") } }] + "parameter": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "a*", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp a*", + "arbitrary": { + "constraint": { + "patterns": [ + "a*" + ] + } + } + }, + "aborted": false + } + ] }, - type: { _tag: "String", checks: [] } + "type": { + "_tag": "String", + "checks": [] + } } - ], - checks: [] - } - }, - `Schema.Record(Schema.String.check(Schema.isPattern(new RegExp("a*"))), Schema.String)` + ] + }, + "references": {} + } ) + }) + + it("imports multiple pattern properties", () => { assertFromJsonSchema( { schema: { @@ -973,29 +2180,97 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [ { - parameter: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("a*") } }] + "parameter": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "a*", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp a*", + "arbitrary": { + "constraint": { + "patterns": [ + "a*" + ] + } + } + }, + "aborted": false + } + ] }, - type: { _tag: "String", checks: [] } + "type": { + "_tag": "String", + "checks": [] + } }, { - parameter: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("b*") } }] + "parameter": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "b*", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp b*", + "arbitrary": { + "constraint": { + "patterns": [ + "b*" + ] + } + } + }, + "aborted": false + } + ] }, - type: { _tag: "Number", checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }] } - } - ], - checks: [] - } - }, - `Schema.StructWithRest(Schema.Struct({ }), [Schema.Record(Schema.String.check(Schema.isPattern(new RegExp("a*"))), Schema.String), Schema.Record(Schema.String.check(Schema.isPattern(new RegExp("b*"))), Schema.Number.check(Schema.isFinite()))])` + "type": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + } + } + ] + }, + "references": {} + } ) }) @@ -1004,16 +2279,53 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "object", minProperties: 1 } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: json() } + "representation": { + "_tag": "Objects", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinProperties", + "payload": { + "minProperties": 1 + } + }, + "annotations": { + "expected": "a value with at least 1 entry", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } ], - checks: [{ _tag: "Filter", meta: { _tag: "isMinProperties", minProperties: 1 } }] - } - }, - `Schema.Record(Schema.String, Schema.Json).check(Schema.isMinProperties(1))` + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] + }, + "references": {} + } ) }) @@ -1021,16 +2333,53 @@ describe("fromJsonSchemaDocument", () => { assertFromJsonSchema( { schema: { type: "object", maxProperties: 1 } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: json() } + "representation": { + "_tag": "Objects", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxProperties", + "payload": { + "maxProperties": 1 + } + }, + "annotations": { + "expected": "a value with at most 1 entry", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 1 + } + } + }, + "aborted": false + } ], - checks: [{ _tag: "Filter", meta: { _tag: "isMaxProperties", maxProperties: 1 } }] - } - }, - `Schema.Record(Schema.String, Schema.Json).check(Schema.isMaxProperties(1))` + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } + ] + }, + "references": {} + } ) }) }) @@ -1045,28 +2394,74 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ + "representation": { + "_tag": "Objects", + "checks": [ { - parameter: { _tag: "String", checks: [] }, - type: json() + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPropertyNames", + "payload": null, + "schemas": [ + { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "^[A-Z]", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp ^[A-Z]", + "arbitrary": { + "constraint": { + "patterns": [ + "^[A-Z]" + ] + } + } + }, + "aborted": false + } + ] + } + ] + }, + "annotations": { + "expected": "an object with property names matching the schema", + "~structural": true + }, + "aborted": false } ], - checks: [{ - _tag: "Filter", - meta: { - _tag: "isPropertyNames", - propertyNames: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("^[A-Z]") } }] + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] } } - }] - } - }, - `Schema.Record(Schema.String, Schema.Json).check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(new RegExp("^[A-Z]")))))` + ] + }, + "references": {} + } ) }) @@ -1079,18 +2474,52 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: json() } + "representation": { + "_tag": "Objects", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPropertyNames", + "payload": null, + "schemas": [ + { + "_tag": "Never", + "checks": [] + } + ] + }, + "annotations": { + "expected": "an object with property names matching the schema", + "~structural": true + }, + "aborted": false + } ], - checks: [ - { _tag: "Filter", meta: { _tag: "isPropertyNames", propertyNames: { _tag: "Never" } } } + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + } ] - } - }, - `Schema.Record(Schema.String, Schema.Json).check(Schema.isPropertyNames(Schema.Never))` + }, + "references": {} + } ) }) @@ -1105,55 +2534,140 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: json() } - ], - checks: [ + "representation": { + "_tag": "Objects", + "checks": [ { - _tag: "Filter", - meta: { - _tag: "isPropertyNames", - propertyNames: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isPattern", regExp: new RegExp("^[A-Z]") } }] - } - } + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPropertyNames", + "payload": null, + "schemas": [ + { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPattern", + "payload": { + "source": "^[A-Z]", + "flags": "" + } + }, + "annotations": { + "expected": "a string matching the RegExp ^[A-Z]", + "arbitrary": { + "constraint": { + "patterns": [ + "^[A-Z]" + ] + } + } + }, + "aborted": false + } + ] + } + ] + }, + "annotations": { + "expected": "an object with property names matching the schema", + "~structural": true + }, + "aborted": false }, { - _tag: "Filter", - meta: { - _tag: "isPropertyNames", - propertyNames: { - _tag: "String", - checks: [{ _tag: "Filter", meta: { _tag: "isMinLength", minLength: 2 } }] - } + "_tag": "Filter", + "representation": { + "id": "effect/schema/isPropertyNames", + "payload": null, + "schemas": [ + { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at least 2", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 2 + } + } + }, + "aborted": false + } + ] + } + ] + }, + "annotations": { + "expected": "an object with property names matching the schema", + "~structural": true + }, + "aborted": false + } + ], + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] } } ] - } - }, - `Schema.Record(Schema.String, Schema.Json).check(Schema.isPropertyNames(Schema.String.check(Schema.isPattern(new RegExp("^[A-Z]"))))).check(Schema.isPropertyNames(Schema.String.check(Schema.isMinLength(2))))` + }, + "references": {} + } ) }) }) }) - it("type: array of strings", () => { + it("array of types", () => { assertFromJsonSchema( { schema: { type: ["string", "null"] } }, { - representation: { - _tag: "Union", - types: [{ _tag: "String", checks: [] }, { _tag: "Null" }], - mode: "anyOf" - } - }, - `Schema.Union([Schema.String, Schema.Null])` + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "String", + "checks": [] + }, + { + "_tag": "Null", + "checks": [] + } + ], + "mode": "anyOf" + }, + "references": {} + } ) assertFromJsonSchema( { @@ -1163,18 +2677,131 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [{ _tag: "String", checks: [] }, { _tag: "Null" }], - mode: "anyOf", - annotations: { description: "a" } + "representation": { + "_tag": "Union", + "annotations": { + "description": "a" + }, + "checks": [], + "types": [ + { + "_tag": "String", + "checks": [] + }, + { + "_tag": "Null", + "checks": [] + } + ], + "mode": "anyOf" + }, + "references": {} + } + ) + }) + + it("ignores true schemas in allOf", () => { + assertFromJsonSchema( + { schema: { allOf: [true, { type: "string" }] } }, + { + "representation": { + "_tag": "String", + "checks": [] + }, + "references": {} + } + ) + }) + + it("imports structured enum members as JSON", () => { + assertFromJsonSchema( + { schema: { enum: [[], {}] } }, + { + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + }, + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ], + "mode": "anyOf" + }, + "references": {} + } + ) + }) + + it("imports built-in JSON Schema annotations", () => { + assertFromJsonSchema( + { + schema: { + type: "string", + format: "email", + contentEncoding: "base64", + contentMediaType: "application/json", + contentSchema: { type: "number" } } }, - `Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "a" })` + { + "representation": { + "_tag": "String", + "annotations": { + "format": "email", + "contentEncoding": "base64", + "contentMediaType": "application/json", + "contentSchema": { "type": "number" } + }, + "checks": [] + }, + "references": {} + } ) }) describe("$ref", () => { + it("treats a reference with an empty token as unconstrained", () => { + assertFromJsonSchema( + { schema: { $ref: "" } }, + { + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } + ) + }) + it("should create a Reference and a definition", () => { assertFromJsonSchema( { @@ -1188,18 +2815,24 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { _tag: "Reference", $ref: "A" }, - references: { - A: { - _tag: "String", - checks: [] + "representation": { + "_tag": "Reference", + "$ref": "A" + }, + "references": { + "A": { + "_tag": "String", + "annotations": { + "identifier": "A" + }, + "checks": [] } } } ) }) - it("should resolve the $ref if there are annotations", () => { + it("should preserve an annotated $ref as a stable suspend", () => { assertFromJsonSchema( { schema: { @@ -1213,22 +2846,53 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [], - annotations: { description: "a" } + "representation": { + "_tag": "Suspend", + "annotations": { + "description": "a" + }, + "checks": [], + "thunk": { + "_tag": "Reference", + "$ref": "A" + } }, - references: { - A: { - _tag: "String", - checks: [] + "references": { + "A": { + "_tag": "String", + "annotations": { + "identifier": "A" + }, + "checks": [] } } } ) }) - it("should resolve the $ref if there is an allOf", () => { + it("does not combine annotation siblings with a $ref", () => { + const schema = toSchemaFromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + $ref: "#/$defs/A", + format: "custom", + $defs: { + A: { type: "number" } + } + }) + ) + const is = Schema.is(schema) + assertTrue(is(1)) + assertFalse(is("a")) + + const document = SchemaRepresentation.toRepresentation(schema.ast) + strictEqual(document.representation._tag, "Suspend") + if (document.representation._tag === "Suspend") { + deepStrictEqual(document.representation.annotations, { format: "custom" }) + deepStrictEqual(document.representation.thunk, { _tag: "Reference", $ref: "A" }) + } + }) + + it("should preserve a $ref refined only by annotations as a stable suspend", () => { assertFromJsonSchema( { schema: { @@ -1244,15 +2908,24 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [], - annotations: { description: "a" } + "representation": { + "_tag": "Suspend", + "annotations": { + "description": "a" + }, + "checks": [], + "thunk": { + "_tag": "Reference", + "$ref": "A" + } }, - references: { - A: { - _tag: "String", - checks: [] + "references": { + "A": { + "_tag": "String", + "annotations": { + "identifier": "A" + }, + "checks": [] } } } @@ -1288,81 +2961,276 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { _tag: "Reference", $ref: "A" }, - references: { - A: { - _tag: "Objects", - propertySignatures: [ - { - name: "name", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false - }, - { - name: "children", - type: { - _tag: "Arrays", - elements: [], - rest: [{ - _tag: "Suspend", - checks: [], - thunk: { - _tag: "Reference", - $ref: "A" + "representation": { + "_tag": "Reference", + "$ref": "A" + }, + "references": { + "A": { + "_tag": "Objects", + "annotations": { + "identifier": "A" + }, + "checks": [], + "propertySignatures": [ + { + "name": { + "type": "string", + "value": "name" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false + }, + { + "name": { + "type": "string", + "value": "children" + }, + "type": { + "_tag": "Arrays", + "checks": [], + "elements": [], + "rest": [ + { + "_tag": "Suspend", + "checks": [], + "thunk": { + "_tag": "Reference", + "$ref": "A" + } } - }], - checks: [] + ] }, - isOptional: false, - isMutable: false + "isOptional": false, + "isMutable": false } ], - indexSignatures: [], - checks: [] + "indexSignatures": [] + } + } + } + ) + }) + + it("preserves annotations on a recursive $ref", () => { + assertFromJsonSchema( + { + schema: { + $ref: "#/$defs/Node", + $defs: { + Node: { + type: "object", + properties: { + child: { + $ref: "#/$defs/Node", + description: "recursive child" + } + }, + required: ["child"], + additionalProperties: false + } + } + } + }, + { + representation: { + _tag: "Reference", + $ref: "Node" + }, + references: { + Node: { + _tag: "Objects", + annotations: { + identifier: "Node" + }, + checks: [], + propertySignatures: [{ + name: { type: "string", value: "child" }, + type: { + _tag: "Suspend", + annotations: { + description: "recursive child" + }, + checks: [], + thunk: { + _tag: "Reference", + $ref: "Node" + } + }, + isOptional: false, + isMutable: false + }], + indexSignatures: [] } } } ) }) + + it("combines assertion siblings with a $ref", () => { + const schema = toSchemaFromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + $ref: "#/$defs/Name", + minLength: 2, + description: "name", + $defs: { + Name: { type: "string" } + } + }) + ) + const is = Schema.is(schema) + assertTrue(is("ab")) + assertFalse(is("a")) + + const document = SchemaRepresentation.toRepresentation(schema.ast) + strictEqual(document.representation._tag, "String") + if (document.representation._tag === "String") { + deepStrictEqual(document.representation.annotations, { description: "name" }) + } + deepStrictEqual(document.references, {}) + }) + + it("rejects assertion siblings on a recursive $ref", () => { + throws( + () => + SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + $ref: "#/$defs/Node", + $defs: { + Node: { + type: "object", + properties: { + child: { + $ref: "#/$defs/Node", + minProperties: 1 + } + } + } + } + }) + ), + `Unsupported assertion siblings on recursive reference Node\n at ["definitions"]["Node"]["properties"]["child"]["$ref"]` + ) + }) }) describe("allOf", () => { - it("resolves references on either side of an intersection", () => { + it("resolves a root reference before intersecting allOf", () => { + const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 } + assertFromJsonSchema({ + schema: { + $ref: "#/$defs/A", + allOf: [{ type: "string", maxLength: 2 }], + $defs: { A: definition } + } + }, { + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + }) + }) + + it("resolves a reference declared inside allOf", () => { const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 } - const expected = { - representation: { - _tag: "String" as const, - checks: [ - { _tag: "Filter" as const, meta: { _tag: "isMinLength" as const, minLength: 1 } }, - { _tag: "Filter" as const, meta: { _tag: "isMaxLength" as const, maxLength: 2 } } + assertFromJsonSchema({ + schema: { + allOf: [{ $ref: "#/$defs/A" }, { type: "string", maxLength: 2 }], + $defs: { A: definition } + } + }, { + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + } ] }, - references: { - A: { - _tag: "String" as const, - checks: [{ _tag: "Filter" as const, meta: { _tag: "isMinLength" as const, minLength: 1 } }] - } - } - } - for ( - const schema of [ - { - $ref: "#/$defs/A", - allOf: [{ type: "string", maxLength: 2 }], - $defs: { A: definition } - }, - { - allOf: [{ $ref: "#/$defs/A" }, { type: "string", maxLength: 2 }], - $defs: { A: definition } - } - ] - ) { - assertFromJsonSchema({ schema }, expected) - } + "references": {} + }) }) - it("preserves annotations on array and object intersections", () => { + it("preserves annotations on array intersections", () => { assertFromJsonSchema( { schema: { @@ -1371,16 +3239,34 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [], - annotations: { description: "a" } - } - }, - `Schema.Array(Schema.Json).annotate({ "description": "a" })` + "representation": { + "_tag": "Arrays", + "annotations": { + "description": "a" + }, + "checks": [], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) + }) + + it("preserves annotations on object intersections", () => { assertFromJsonSchema( { schema: { @@ -1390,15 +3276,17 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [], - checks: [], - annotations: { description: "a" } - } - }, - `Schema.Struct({ }).annotate({ "description": "a" })` + "representation": { + "_tag": "Objects", + "annotations": { + "description": "a" + }, + "checks": [], + "propertySignatures": [], + "indexSignatures": [] + }, + "references": {} + } ) }) @@ -1410,8 +3298,13 @@ describe("fromJsonSchemaDocument", () => { allOf: [{ anyOf: [{ type: "number" }, { type: "boolean" }] }] } }, - { representation: { _tag: "Never" } }, - `Schema.Never` + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } ) }) @@ -1422,10 +3315,10 @@ describe("fromJsonSchemaDocument", () => { ) { for (const literal of [valid, invalid]) { for (const allOf of [[refinement, { const: literal }], [{ const: literal }, refinement]]) { - const document = SchemaRepresentation.fromJsonSchemaDocument( + const schema = toSchemaFromJsonSchemaDocument( JsonSchema.fromSchemaDraft2020_12({ allOf }) ) - const is = Schema.is(SchemaRepresentation.toSchema(document)) + const is = Schema.is(schema) if (literal === valid) { assertTrue(is(literal)) } else { @@ -1436,70 +3329,108 @@ describe("fromJsonSchemaDocument", () => { } describe("literal refinements", () => { - const cases: ReadonlyArray< - readonly [ - name: string, - refinement: JsonSchema.JsonSchema, - valid: string | number, - invalid: string | number - ] - > = [ - ["minLength", { type: "string", minLength: 2 }, "ab", "a"], - ["maxLength", { type: "string", maxLength: 1 }, "a", "ab"], - ["pattern", { type: "string", pattern: "^a+$" }, "aa", "ab"], - ["integer", { type: "integer" }, 1, 1.5], - ["multipleOf", { type: "number", multipleOf: 0.1 }, 0.3, 0.31], - [ - "multipleOf beyond the toFixed precision limit", - { type: "number", multipleOf: Number("1e-101") }, - 0, - Number("5e-102") - ], - ["multipleOf with a large scientific operand", { type: "number", multipleOf: 2 }, Number("1e21"), 1], - [ - "multipleOf with a nonzero subnormal remainder", - { type: "number", multipleOf: Number("1e-323") }, - 0, - Number("1.042e-321") - ], - ["minimum", { type: "number", minimum: 1 }, 1, 0], - ["maximum", { type: "number", maximum: 1 }, 1, 2], - ["exclusiveMinimum", { type: "number", exclusiveMinimum: 1 }, 2, 1], - ["exclusiveMaximum", { type: "number", exclusiveMaximum: 1 }, 0, 1], - [ - "filter group", + it("minLength", () => { + assertLiteralRefinement({ type: "string", minLength: 2 }, "ab", "a") + }) + + it("maxLength", () => { + assertLiteralRefinement({ type: "string", maxLength: 1 }, "a", "ab") + }) + + it("pattern", () => { + assertLiteralRefinement({ type: "string", pattern: "^a+$" }, "aa", "ab") + }) + + it("integer", () => { + assertLiteralRefinement({ type: "integer" }, 1, 1.5) + }) + + it("multipleOf", () => { + assertLiteralRefinement({ type: "number", multipleOf: 0.1 }, 0.3, 0.31) + }) + + it("multipleOf beyond the toFixed precision limit", () => { + assertLiteralRefinement({ type: "number", multipleOf: Number("1e-101") }, 0, Number("5e-102")) + }) + + it("multipleOf with a large scientific operand", () => { + assertLiteralRefinement({ type: "number", multipleOf: 2 }, Number("1e21"), 1) + }) + + it("multipleOf with a nonzero subnormal remainder", () => { + assertLiteralRefinement({ type: "number", multipleOf: Number("1e-323") }, 0, Number("1.042e-321")) + }) + + it("minimum", () => { + assertLiteralRefinement({ type: "number", minimum: 1 }, 1, 0) + }) + + it("maximum", () => { + assertLiteralRefinement({ type: "number", maximum: 1 }, 1, 2) + }) + + it("exclusiveMinimum", () => { + assertLiteralRefinement({ type: "number", exclusiveMinimum: 1 }, 2, 1) + }) + + it("exclusiveMaximum", () => { + assertLiteralRefinement({ type: "number", exclusiveMaximum: 1 }, 0, 1) + }) + + it("filter group", () => { + assertLiteralRefinement( { type: "number", allOf: [{ minimum: 1, maximum: 2, description: "range" }] }, 2, 0 - ] - ] + ) + }) - for (const [name, refinement, valid, invalid] of cases) { - it(name, () => { - assertLiteralRefinement(refinement, valid, invalid) - }) - } + it("filters enum members when the refinement precedes the enum", () => { + assertFromJsonSchema( + { schema: { type: "string", minLength: 2, allOf: [{ enum: ["a", "ab"] }] } }, + { + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "ab" + } + } + ], + "mode": "anyOf" + }, + "references": {} + } + ) + }) - it("filters enum members", () => { - const expected = { - representation: { - _tag: "Union" as const, - types: [{ _tag: "Literal" as const, literal: "ab" }], - mode: "anyOf" as const + it("filters enum members when the enum precedes the refinement", () => { + assertFromJsonSchema( + { schema: { enum: ["a", "ab"], allOf: [{ type: "string", minLength: 2 }] } }, + { + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "ab" + } + } + ], + "mode": "anyOf" + }, + "references": {} } - } - for ( - const [schema, member] of [ - [{ type: "string", minLength: 2 }, { enum: ["a", "ab"] }], - [{ enum: ["a", "ab"] }, { type: "string", minLength: 2 }] - ] - ) { - assertFromJsonSchema( - { schema: { ...schema, allOf: [member] } }, - expected, - `Schema.Literal("ab")` - ) - } + ) }) }) @@ -1513,12 +3444,12 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [] - } - }, - `Schema.String` + "representation": { + "_tag": "String", + "checks": [] + }, + "references": {} + } ) }) @@ -1534,14 +3465,32 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isMinLength(1))` + }, + "references": {} + } ) }) @@ -1556,14 +3505,33 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 }, annotations: { description: "b" } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + }, + "description": "b" + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isMinLength(1, { "description": "b" }))` + }, + "references": {} + } ) }) @@ -1579,15 +3547,35 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } } - ], - annotations: { description: "a" } - } - }, - `Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1))` + "representation": { + "_tag": "String", + "annotations": { + "description": "a" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -1603,13 +3591,15 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [], - annotations: { description: "b" } - } - }, - `Schema.String.annotate({ "description": "b" })` + "representation": { + "_tag": "String", + "annotations": { + "description": "b" + }, + "checks": [] + }, + "references": {} + } ) }) @@ -1625,15 +3615,36 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 }, annotations: { description: "b" } } - ], - annotations: { description: "a" } - } - }, - `Schema.String.annotate({ "description": "a" }).check(Schema.isMinLength(1, { "description": "b" }))` + "representation": { + "_tag": "String", + "annotations": { + "description": "a" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + }, + "description": "b" + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -1649,15 +3660,51 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 } }, - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isMaxLength(2)).check(Schema.isMinLength(1))` + }, + "references": {} + } ) }) @@ -1674,16 +3721,54 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 } }, - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } } - ], - annotations: { description: "a" } - } - }, - `Schema.String.annotate({ "description": "a" }).check(Schema.isMaxLength(2)).check(Schema.isMinLength(1))` + "representation": { + "_tag": "String", + "annotations": { + "description": "a" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -1700,16 +3785,55 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 } }, - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 }, annotations: { description: "b" } } - ], - annotations: { description: "a" } - } - }, - `Schema.String.annotate({ "description": "a" }).check(Schema.isMaxLength(2)).check(Schema.isMinLength(1, { "description": "b" }))` + "representation": { + "_tag": "String", + "annotations": { + "description": "a" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + }, + "description": "b" + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) @@ -1724,15 +3848,51 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }, - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(2))` + }, + "references": {} + } ) }) @@ -1747,21 +3907,59 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ + "representation": { + "_tag": "String", + "checks": [ { - _tag: "FilterGroup", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }, - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 } } - ], - annotations: { description: "b" } + "_tag": "FilterGroup", + "annotations": { + "description": "b" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + } + }, + "aborted": false + } + ] } ] - } - }, - `Schema.String.check(Schema.makeFilterGroup([Schema.isMinLength(1), Schema.isMaxLength(2)], { "description": "b" }))` + }, + "references": {} + } ) }) @@ -1776,15 +3974,52 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }, - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 }, annotations: { description: "c" } } + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + }, + "description": "c" + }, + "aborted": false + } ] - } - }, - `Schema.String.check(Schema.isMinLength(1)).check(Schema.isMaxLength(2, { "description": "c" }))` + }, + "references": {} + } ) }) @@ -1799,25 +4034,64 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "String", - checks: [ + "representation": { + "_tag": "String", + "checks": [ { - _tag: "FilterGroup", - checks: [ - { _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }, - { _tag: "Filter", meta: { _tag: "isMaxLength", maxLength: 2 }, annotations: { description: "c" } } - ], - annotations: { description: "b" } + "_tag": "FilterGroup", + "annotations": { + "description": "b" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMaxLength", + "payload": { + "maxLength": 2 + } + }, + "annotations": { + "expected": "a value with a length of at most 2", + "~structural": true, + "arbitrary": { + "constraint": { + "maxLength": 2 + } + }, + "description": "c" + }, + "aborted": false + } + ] } ] - } - }, - `Schema.String.check(Schema.makeFilterGroup([Schema.isMinLength(1), Schema.isMaxLength(2, { "description": "c" })], { "description": "b" }))` + }, + "references": {} + } ) }) - it("& string enum", () => { + it("intersects with a single-member string enum", () => { assertFromJsonSchema( { schema: { @@ -1828,12 +4102,16 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: "a" - } - }, - `Schema.Literal("a")` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) assertFromJsonSchema( { @@ -1846,14 +4124,23 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: "a", - annotations: { description: "b" } - } - }, - `Schema.Literal("a").annotate({ "description": "b" })` + "representation": { + "_tag": "Literal", + "annotations": { + "description": "b" + }, + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + "references": {} + } ) + }) + + it("intersects with a multi-member string enum", () => { assertFromJsonSchema( { schema: { @@ -1864,16 +4151,31 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" }, - { _tag: "Literal", literal: "b" } + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "b" + } + } ], - mode: "anyOf" - } - }, - `Schema.Literals(["a", "b"])` + "mode": "anyOf" + }, + "references": {} + } ) }) @@ -1888,20 +4190,67 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: "a" } + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "a" + } + } ], - mode: "anyOf" - } - }, - `Schema.Literal("a")` + "mode": "anyOf" + }, + "references": {} + } ) }) }) describe("type: number", () => { + it("number & number preserves annotations after removing duplicate checks", () => { + assertFromJsonSchema( + { + schema: { + type: "number", + allOf: [{ type: "number", description: "b" }] + } + }, + { + "representation": { + "_tag": "Number", + "annotations": { + "description": "b" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } + ) + }) + it("number & integer", () => { assertFromJsonSchema( { @@ -1913,15 +4262,46 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isInt" } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isInt())` + }, + "references": {} + } ) }) @@ -1937,17 +4317,72 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: 2 } }, - { _tag: "Filter", meta: { _tag: "isLessThanOrEqualTo", maximum: 2 } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThanOrEqualTo", + "payload": { + "minimum": 2 + } + }, + "annotations": { + "expected": "a value greater than or equal to 2" + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThanOrEqualTo", + "payload": { + "maximum": 2 + } + }, + "annotations": { + "expected": "a value less than or equal to 2" + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isFinite()).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(2)).check(Schema.isLessThanOrEqualTo(2))` + }, + "references": {} + } ) }) @@ -1962,15 +4397,46 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isInt" } }, - { _tag: "Filter", meta: { _tag: "isFinite" } } + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + } ] - } - }, - `Schema.Number.check(Schema.isInt()).check(Schema.isFinite())` + }, + "references": {} + } ) }) @@ -1985,30 +4451,158 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Number", - checks: [ - { _tag: "Filter", meta: { _tag: "isFinite" } }, + "representation": { + "_tag": "Number", + "checks": [ { - _tag: "FilterGroup", - checks: [ - { _tag: "Filter", meta: { _tag: "isGreaterThanOrEqualTo", minimum: 1 } }, + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "FilterGroup", + "annotations": { + "description": "b" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThanOrEqualTo", + "payload": { + "minimum": 1 + } + }, + "annotations": { + "expected": "a value greater than or equal to 1" + }, + "aborted": false + }, { - _tag: "Filter", - meta: { _tag: "isLessThanOrEqualTo", maximum: 2 }, - annotations: { description: "c" } + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThanOrEqualTo", + "payload": { + "maximum": 2 + } + }, + "annotations": { + "expected": "a value less than or equal to 2", + "description": "c" + }, + "aborted": false } - ], - annotations: { description: "b" } + ] } ] + }, + "references": {} + } + ) + }) + + it("continues intersecting after an annotated filter group", () => { + assertFromJsonSchema( + { + schema: { + type: "number", + allOf: [ + { minimum: 1, maximum: 2, description: "range" }, + { type: "integer" } + ] } }, - `Schema.Number.check(Schema.isFinite()).check(Schema.makeFilterGroup([Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(2, { "description": "c" })], { "description": "b" }))` + { + "representation": { + "_tag": "Number", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isFinite", + "payload": null + }, + "annotations": { + "expected": "a finite number", + "arbitrary": { + "constraint": { + "noInfinity": true, + "noNaN": true + } + } + }, + "aborted": false + }, + { + "_tag": "FilterGroup", + "annotations": { + "description": "range" + }, + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isGreaterThanOrEqualTo", + "payload": { + "minimum": 1 + } + }, + "annotations": { + "expected": "a value greater than or equal to 1" + }, + "aborted": false + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isLessThanOrEqualTo", + "payload": { + "maximum": 2 + } + }, + "annotations": { + "expected": "a value less than or equal to 2" + }, + "aborted": false + } + ] + }, + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isInt", + "payload": null + }, + "annotations": { + "expected": "an integer", + "arbitrary": { + "constraint": { + "integer": true + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } ) }) - it("& number enum", () => { + it("intersects with a single-member number enum", () => { assertFromJsonSchema( { schema: { @@ -2019,12 +4613,16 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: 1 - } - }, - `Schema.Literal(1)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + "references": {} + } ) assertFromJsonSchema( { @@ -2037,14 +4635,23 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: 1, - annotations: { description: "b" } - } - }, - `Schema.Literal(1).annotate({ "description": "b" })` + "representation": { + "_tag": "Literal", + "annotations": { + "description": "b" + }, + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + "references": {} + } ) + }) + + it("intersects with a multi-member number enum", () => { assertFromJsonSchema( { schema: { @@ -2055,22 +4662,73 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Union", - types: [ - { _tag: "Literal", literal: 1 }, - { _tag: "Literal", literal: 2 } + "representation": { + "_tag": "Union", + "checks": [], + "types": [ + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 2 + } + } ], - mode: "anyOf" - } - }, - `Schema.Literals([1, 2])` + "mode": "anyOf" + }, + "references": {} + } ) }) }) describe("type: boolean", () => { - it("& boolean enum", () => { + it("boolean & boolean", () => { + assertFromJsonSchema( + { + schema: { + type: "boolean", + allOf: [{ type: "boolean" }] + } + }, + { + "representation": { + "_tag": "Boolean", + "checks": [] + }, + "references": {} + } + ) + }) + + it("boolean & non-boolean literal", () => { + assertFromJsonSchema( + { + schema: { + type: "boolean", + allOf: [{ const: 1 }] + } + }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("intersects with a single-member boolean enum", () => { assertFromJsonSchema( { schema: { @@ -2081,12 +4739,16 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: true - } - }, - `Schema.Literal(true)` + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } ) assertFromJsonSchema( { @@ -2099,13 +4761,19 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Literal", - literal: true, - annotations: { description: "b" } - } - }, - `Schema.Literal(true).annotate({ "description": "b" })` + "representation": { + "_tag": "Literal", + "annotations": { + "description": "b" + }, + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } ) }) }) @@ -2115,17 +4783,15 @@ describe("fromJsonSchemaDocument", () => { a: JsonSchema.JsonSchema, b: JsonSchema.JsonSchema, expected: Parameters[1], - runtime: string, valid: ReadonlyArray, invalid: ReadonlyArray ) { for (const [schema, member] of [[a, b], [b, a]]) { const document = assertFromJsonSchema( { schema: { ...schema, allOf: [member] } }, - expected, - runtime + expected ) - const is = Schema.is(SchemaRepresentation.toSchema(document)) + const is = Schema.is(document) for (const value of valid) { assertTrue(is(value)) } @@ -2147,14 +4813,44 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [json()], - checks: [{ _tag: "Filter", meta: { _tag: "isUnique" } }] - } - }, - `Schema.Array(Schema.Json).check(Schema.isUnique())` + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isUnique", + "payload": null + }, + "annotations": { + "expected": "an array with unique items", + "arbitrary": { + "constraint": { + "unique": true + } + } + }, + "aborted": false + } + ], + "elements": [], + "rest": [ + { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value" + }, + "typeParameters": [], + "checks": [] + } + ] + }, + "references": {} + } ) }) @@ -2173,17 +4869,38 @@ describe("fromJsonSchemaDocument", () => { items: { type: "string" } }, { - representation: { - _tag: "Arrays", - elements: [ - { isOptional: false, type: { _tag: "String", checks: [] } }, - { isOptional: false, type: { _tag: "Literal", literal: "tail" } } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + }, + { + "isOptional": false, + "type": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "string", + "value": "tail" + } + } + } ], - rest: [{ _tag: "String", checks: [] }], - checks: [] - } + "rest": [ + { + "_tag": "String", + "checks": [] + } + ] + }, + "references": {} }, - `Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.Literal("tail")]), [Schema.String])`, [["head", "tail"], ["head", "tail", "more"]], [["head"], ["head", "other"], ["head", "tail", 1]] ) @@ -2204,14 +4921,22 @@ describe("fromJsonSchemaDocument", () => { maxItems: 2 }, { - representation: { - _tag: "Arrays", - elements: [{ isOptional: false, type: { _tag: "String", checks: [] } }], - rest: [], - checks: [] - } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [] + }, + "references": {} }, - `Schema.Tuple([Schema.String])`, [["head"]], [[], ["head", 1]] ) @@ -2231,8 +4956,13 @@ describe("fromJsonSchemaDocument", () => { minItems: 2, maxItems: 2 }, - { representation: { _tag: "Never" } }, - `Schema.Never`, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + }, [], [[], ["head"], ["head", 1]] ) @@ -2253,14 +4983,22 @@ describe("fromJsonSchemaDocument", () => { maxItems: 1 }, { - representation: { - _tag: "Arrays", - elements: [{ isOptional: false, type: { _tag: "String", checks: [] } }], - rest: [], - checks: [] - } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [] + }, + "references": {} }, - `Schema.Tuple([Schema.String])`, [["head"]], [[], ["head", 1]] ) @@ -2281,14 +5019,22 @@ describe("fromJsonSchemaDocument", () => { items: { type: "number" } }, { - representation: { - _tag: "Arrays", - elements: [{ isOptional: false, type: { _tag: "String", checks: [] } }], - rest: [], - checks: [] - } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "String", + "checks": [] + } + } + ], + "rest": [] + }, + "references": {} }, - `Schema.Tuple([Schema.String])`, [["head"]], [[], ["head", "tail"], ["head", 1]] ) @@ -2307,8 +5053,13 @@ describe("fromJsonSchemaDocument", () => { minItems: 1, maxItems: 1 }, - { representation: { _tag: "Never" } }, - `Schema.Never`, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + }, [], [[], [0]] ) @@ -2326,14 +5077,14 @@ describe("fromJsonSchemaDocument", () => { maxItems: 1 }, { - representation: { - _tag: "Arrays", - elements: [], - rest: [], - checks: [] - } + "representation": { + "_tag": "Arrays", + "checks": [], + "elements": [], + "rest": [] + }, + "references": {} }, - `Schema.Tuple([])`, [[]], [[0]] ) @@ -2353,20 +5104,348 @@ describe("fromJsonSchemaDocument", () => { maxItems: 1 }, { - representation: { - _tag: "Arrays", - elements: [{ isOptional: false, type: { _tag: "Literal", literal: 2 } }], - rest: [], - checks: [{ _tag: "Filter", meta: { _tag: "isMinLength", minLength: 1 } }] - } + "representation": { + "_tag": "Arrays", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ], + "elements": [ + { + "isOptional": false, + "type": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 2 + } + } + } + ], + "rest": [] + }, + "references": {} }, - `Schema.Tuple([Schema.Literal(2)]).check(Schema.isMinLength(1))`, [[2]], [[], [0]] ) }) }) + it("short-circuits false intersections to Never", () => { + assertFromJsonSchema( + { schema: { allOf: [false, { type: "string" }] } }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("returns Never when intersecting array and string", () => { + assertFromJsonSchema( + { schema: { allOf: [{ type: "array" }, { type: "string" }] } }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("returns Never when intersecting object and string", () => { + assertFromJsonSchema( + { schema: { allOf: [{ type: "object" }, { type: "string" }] } }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("returns Never when intersecting null and string", () => { + assertFromJsonSchema( + { schema: { allOf: [{ type: "null" }, { type: "string" }] } }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("returns Never when intersecting distinct literals", () => { + assertFromJsonSchema( + { schema: { allOf: [{ const: 1 }, { const: 2 }] } }, + { + "representation": { + "_tag": "Never", + "checks": [] + }, + "references": {} + } + ) + }) + + it("preserves null when intersecting null types", () => { + assertFromJsonSchema( + { schema: { allOf: [{ type: "null" }, { type: "null" }] } }, + { + "representation": { + "_tag": "Null", + "checks": [] + }, + "references": {} + } + ) + }) + + it("preserves a literal when intersecting identical literals", () => { + assertFromJsonSchema( + { schema: { allOf: [{ const: 1 }, { const: 1 }] } }, + { + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "number", + "value": 1 + } + }, + "references": {} + } + ) + }) + + it("preserves a matching literal after a boolean type", () => { + assertFromJsonSchema( + { schema: { allOf: [{ type: "boolean" }, { const: true }] } }, + { + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } + ) + }) + + it("preserves a matching literal before a boolean type", () => { + assertFromJsonSchema( + { schema: { allOf: [{ const: true }, { type: "boolean" }] } }, + { + "representation": { + "_tag": "Literal", + "checks": [], + "literal": { + "type": "boolean", + "value": true + } + }, + "references": {} + } + ) + }) + + it("combines a string with a reference", () => { + const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 } + assertFromJsonSchema( + { + schema: { + type: "string", + allOf: [{ $ref: "#/$defs/A" }], + $defs: { A: definition } + } + }, + { + "representation": { + "_tag": "String", + "checks": [ + { + "_tag": "Filter", + "representation": { + "id": "effect/schema/isMinLength", + "payload": { + "minLength": 1 + } + }, + "annotations": { + "expected": "a value with a length of at least 1", + "~structural": true, + "arbitrary": { + "constraint": { + "minLength": 1 + } + } + }, + "aborted": false + } + ] + }, + "references": {} + } + ) + }) + + it("preserves annotations on a reference inside allOf", () => { + const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 } + const document = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + type: "string", + allOf: [{ $ref: "#/$defs/A", description: "annotated" }], + $defs: { A: definition } + }) + ) + strictEqual(document.representation._tag, "String") + if (document.representation._tag === "String") { + strictEqual(document.representation.annotations?.description, "annotated") + } + }) + + it("preserves annotations on a root reference intersected with allOf", () => { + const definition: JsonSchema.JsonSchema = { type: "string", minLength: 1 } + const document = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + $ref: "#/$defs/A", + description: "annotated", + allOf: [{ type: "string" }], + $defs: { A: definition } + }) + ) + strictEqual(document.representation._tag, "String") + if (document.representation._tag === "String") { + strictEqual(document.representation.annotations?.description, "annotated") + } + }) + + it("preserves annotations through reference aliases", () => { + const aliases = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + type: "string", + allOf: [{ $ref: "#/$defs/A" }], + $defs: { + A: { $ref: "#/$defs/B", description: "alias" }, + B: { type: "string" } + } + }) + ) + strictEqual(aliases.representation._tag, "String") + if (aliases.representation._tag === "String") { + strictEqual(aliases.representation.annotations?.description, "alias") + } + }) + + it("merges annotations on string intersections", () => { + const string = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + allOf: [ + { type: "string", contentMediaType: "application/json" }, + { type: "string", contentSchema: { type: "number" } } + ] + }) + ) + strictEqual(string.representation._tag, "String") + if (string.representation._tag === "String") { + deepStrictEqual(string.representation.annotations, { + contentMediaType: "application/json", + contentSchema: { type: "number" } + }) + } + }) + + it("merges constraints on overlapping required properties", () => { + const object = toSchemaFromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + type: "object", + additionalProperties: false, + properties: { a: { type: "string" } }, + required: ["a"], + allOf: [{ + type: "object", + additionalProperties: false, + properties: { a: { type: "string", minLength: 2 } }, + required: ["a"] + }] + }) + ) + const isObject = Schema.is(object) + assertTrue(isObject({ a: "ab" })) + assertFalse(isObject({ a: "a" })) + }) + + it("preserves optional properties when intersecting object fields", () => { + const optionalObject = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + type: "object", + additionalProperties: false, + properties: { a: { type: "string" } }, + allOf: [{ + type: "object", + additionalProperties: false, + properties: { a: { minLength: 1 } } + }] + }) + ) + strictEqual(optionalObject.representation._tag, "Objects") + if (optionalObject.representation._tag === "Objects") { + strictEqual(optionalObject.representation.propertySignatures[0].isOptional, true) + } + }) + + it("merges object index signatures", () => { + const indexes = fromJsonSchemaRepresentation( + JsonSchema.fromSchemaDraft2020_12({ + type: "object", + additionalProperties: false, + patternProperties: { "^a": { type: "string" } }, + allOf: [ + { type: "object", additionalProperties: true }, + { + type: "object", + additionalProperties: false, + patternProperties: { "^b": { type: "number" } } + } + ] + }) + ) + strictEqual(indexes.representation._tag, "Objects") + if (indexes.representation._tag === "Objects") { + strictEqual(indexes.representation.indexSignatures.length, 3) + } + }) + describe("type: object", () => { it("add properties", () => { assertFromJsonSchema( @@ -2380,21 +5459,27 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [ + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [ { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: true, - isMutable: false + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": true, + "isMutable": false } ], - indexSignatures: [], - checks: [] - } - }, - `Schema.Struct({ "a": Schema.optionalKey(Schema.String) })` + "indexSignatures": [] + }, + "references": {} + } ) }) @@ -2409,22 +5494,118 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [], - indexSignatures: [ - { parameter: { _tag: "String", checks: [] }, type: { _tag: "Boolean" } } - ], - checks: [] - } - }, - `Schema.Record(Schema.String, Schema.Boolean)` + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [], + "indexSignatures": [ + { + "parameter": { + "_tag": "String", + "checks": [] + }, + "type": { + "_tag": "Boolean", + "checks": [] + } + } + ] + }, + "references": {} + } ) }) }) }) describe("options", () => { + describe("patterns", () => { + it("rejects patterns by default", () => { + for ( + const [schema, path] of [ + [{ type: "string", pattern: "^a+$" }, `["schema"]["pattern"]`], + [ + { type: "object", patternProperties: { "^a+$": { type: "string" } } }, + `["schema"]["patternProperties"]["^a+$"]` + ], + [ + { type: "object", propertyNames: { pattern: "^a+$" } }, + `["schema"]["propertyNames"]["pattern"]` + ] + ] as const + ) { + throws( + () => SchemaRepresentation.fromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(schema)), + `Pattern encountered while patterns is set to "error"\n at ${path}` + ) + } + }) + + it("applies patterns explicitly", () => { + const schema = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ type: "string", pattern: "^a+$" }), + { patterns: "apply" } + ) + const is = Schema.is(schema) + assertTrue(is("aaa")) + assertFalse(is("bbb")) + }) + + it("ignores patterns explicitly", () => { + const schema = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ type: "string", pattern: "^a+$" }), + { patterns: "ignore" } + ) + const is = Schema.is(schema) + assertTrue(is("aaa")) + assertTrue(is("bbb")) + deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast).representation, { + _tag: "String", + checks: [] + }) + + const invalidPattern = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ type: "string", pattern: "[" }), + { patterns: "ignore" } + ) + assertTrue(Schema.is(invalidPattern)("anything")) + }) + + it("ignores pattern property value constraints explicitly", () => { + const schema = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + type: "object", + patternProperties: { + "^a+$": { type: "string" }, + "^b+$": { type: "number" } + }, + additionalProperties: false + }), + { patterns: "ignore" } + ) + const is = Schema.is(schema) + assertTrue(is({ aaa: 1, bbb: "b", ccc: true })) + const representation = SchemaRepresentation.toRepresentation(schema.ast).representation + strictEqual(representation._tag, "Objects") + if (representation._tag === "Objects") { + deepStrictEqual(representation.indexSignatures.map(({ parameter }) => parameter), [{ + _tag: "String", + checks: [] + }]) + } + + const withAdditionalProperties = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12({ + type: "object", + patternProperties: { "^a+$": { type: "string" } }, + additionalProperties: { type: "boolean" } + }), + { patterns: "ignore" } + ) + assertTrue(Schema.is(withAdditionalProperties)({ bbb: 1 })) + }) + }) + describe("onEnter", () => { it("additionalProperties false via onEnter", () => { assertFromJsonSchema( @@ -2446,21 +5627,27 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: { - _tag: "Objects", - propertySignatures: [ + "representation": { + "_tag": "Objects", + "checks": [], + "propertySignatures": [ { - name: "a", - type: { _tag: "String", checks: [] }, - isOptional: false, - isMutable: false + "name": { + "type": "string", + "value": "a" + }, + "type": { + "_tag": "String", + "checks": [] + }, + "isOptional": false, + "isMutable": false } ], - indexSignatures: [], - checks: [] - } - }, - `Schema.Struct({ "a": Schema.String })` + "indexSignatures": [] + }, + "references": {} + } ) }) @@ -2481,12 +5668,22 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: json({ - title: "a", - description: "b" - }) - }, - `Schema.Json.annotate({ "title": "a", "description": "b" })` + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value", + "title": "a", + "description": "b" + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) }) @@ -2510,12 +5707,22 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: json({ - title: "a", - default: "c" - }) - }, - `Schema.Json.annotate({ "title": "a", "default": "c" })` + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value", + "title": "a", + "default": "c" + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) }) @@ -2530,14 +5737,26 @@ describe("fromJsonSchemaDocument", () => { } }, { - representation: json({ - title: "a", - description: "b", - default: "c", - examples: ["d"] - }) - }, - `Schema.Json.annotate({ "title": "a", "description": "b", "default": "c", "examples": ["d"] })` + "representation": { + "_tag": "Declaration", + "representation": { + "id": "effect/schema/Json", + "payload": null + }, + "annotations": { + "expected": "JSON value", + "title": "a", + "description": "b", + "default": "c", + "examples": [ + "d" + ] + }, + "typeParameters": [], + "checks": [] + }, + "references": {} + } ) }) }) diff --git a/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaMultiDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaMultiDocument.test.ts index 9ce54f256..2dc625f3c 100644 --- a/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaMultiDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/fromJsonSchemaMultiDocument.test.ts @@ -1,10 +1,106 @@ -import { SchemaRepresentation } from "effect" +import { assert } from "@effect/vitest" +import { Schema, type SchemaAST, SchemaRepresentation } from "effect" import { describe, it } from "vitest" import { deepStrictEqual, throws } from "../../utils/assert.ts" -describe("fromJsonSchemaMultiDocument", () => { +function importAndLower( + document: Parameters[0] +): SchemaRepresentation.MultiDocument { + const schemas = SchemaRepresentation.fromJsonSchemaMultiDocument(document) + return SchemaRepresentation.toRepresentations( + schemas.map((schema) => schema.ast) as [SchemaAST.AST, ...Array] + ) +} + +describe("SchemaRepresentation.fromJsonSchemaMultiDocument", () => { + it("propagates the pattern policy through reachable definitions", () => { + const document: Parameters[0] = { + dialect: "draft-2020-12" as const, + schemas: [{ $ref: "#/$defs/A" }], + definitions: { + A: { type: "string", pattern: "^a+$" } + } + } + + throws( + () => SchemaRepresentation.fromJsonSchemaMultiDocument(document), + `Pattern encountered while patterns is set to "error"\n at ["definitions"]["A"]["pattern"]` + ) + + const [schema] = SchemaRepresentation.fromJsonSchemaMultiDocument(document, { patterns: "apply" }) + assert.isTrue(Schema.is(schema)("aaa")) + assert.isFalse(Schema.is(schema)("bbb")) + }) + + it("preserves an onEnter exception by identity", () => { + const cause = new Error("boom") + + throws( + () => + SchemaRepresentation.fromJsonSchemaMultiDocument({ + dialect: "draft-2020-12", + schemas: [{ type: "string" }], + definitions: {} + }, { + onEnter: () => { + throw cause + } + }), + (error: unknown) => { + assert.strictEqual(error, cause) + return undefined + } + ) + }) + + it("preserves contentSchema as an annotation without traversing it", () => { + const document = importAndLower({ + dialect: "draft-2020-12", + schemas: [{ + type: "string", + contentMediaType: "application/json", + contentSchema: { $ref: "#/$defs/Payload" } + }], + definitions: { + Payload: { + type: "object", + properties: { value: { type: "number" } }, + required: ["value"], + additionalProperties: false + } + } + }) + + const content = document.representations[0] + assert.strictEqual(content._tag, "String") + assert.deepStrictEqual(document.references, {}) + if (content._tag === "String") { + assert.deepStrictEqual(content.annotations, { + contentMediaType: "application/json", + contentSchema: { $ref: "#/$defs/Payload" } + }) + } + }) + + it("does not import unreachable definitions", () => { + const schemas = SchemaRepresentation.fromJsonSchemaMultiDocument({ + dialect: "draft-2020-12", + schemas: [{ type: "string" }], + definitions: { + Unused: { type: "number", description: "unused" } + } + }, { + onEnter: (schema) => { + if (schema.description === "unused") throw new Error("unreachable") + return schema + } + }) + + assert.strictEqual(schemas[0].ast._tag, "String") + }) + it("preserves root order and shares definitions", () => { - const document = SchemaRepresentation.fromJsonSchemaMultiDocument({ + const document = importAndLower({ dialect: "draft-2020-12", schemas: [ { $ref: "#/$defs/A" }, @@ -17,28 +113,52 @@ describe("fromJsonSchemaMultiDocument", () => { } }) - const definition = { - _tag: "String" as const, - checks: [{ _tag: "Filter" as const, meta: { _tag: "isMinLength" as const, minLength: 1 } }] - } - deepStrictEqual(document, { + deepStrictEqual(SchemaRepresentation.toJsonMultiDocument(document), { representations: [ { _tag: "Reference", $ref: "A" }, - { ...definition, annotations: { description: "second" } }, + { + _tag: "Suspend", + checks: [], + annotations: { description: "second" }, + thunk: { _tag: "Reference", $ref: "A" } + }, { _tag: "Arrays", elements: [], rest: [{ _tag: "Reference", $ref: "A" }], checks: [] }, - { ...definition, annotations: { description: "fourth" } } + { + _tag: "Suspend", + checks: [], + annotations: { description: "fourth" }, + thunk: { _tag: "Reference", $ref: "A" } + } ], - references: { A: definition } + references: { + A: { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "effect/schema/isMinLength", + payload: { minLength: 1 } + }, + annotations: { + identifier: "A", + expected: "a value with a length of at least 1", + "~structural": true, + arbitrary: { constraint: { minLength: 1 } } + }, + aborted: false + }] + } + } }) }) it("resolves alias chains when combining a reference", () => { - const document = SchemaRepresentation.fromJsonSchemaMultiDocument({ + const document = importAndLower({ dialect: "draft-2020-12", schemas: [{ $ref: "#/$defs/A", description: "root" }], definitions: { @@ -48,25 +168,33 @@ describe("fromJsonSchemaMultiDocument", () => { } }) - deepStrictEqual(document, { + deepStrictEqual(SchemaRepresentation.toJsonMultiDocument(document), { representations: [{ - _tag: "Number", - checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }], - annotations: { description: "root" } + _tag: "Suspend", + checks: [], + annotations: { description: "root" }, + thunk: { _tag: "Reference", $ref: "A" } }], references: { - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "C" }, - C: { + A: { _tag: "Number", - checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }] + checks: [{ + _tag: "Filter", + representation: { id: "effect/schema/isFinite", payload: null }, + annotations: { + identifier: "A", + expected: "a finite number", + arbitrary: { constraint: { noInfinity: true, noNaN: true } } + }, + aborted: false + }] } } }) }) it("tracks recursive definitions independently", () => { - const document = SchemaRepresentation.fromJsonSchemaMultiDocument({ + const document = importAndLower({ dialect: "draft-2020-12", schemas: [{ $ref: "#/$defs/A" }, { $ref: "#/$defs/B" }], definitions: { @@ -75,14 +203,24 @@ describe("fromJsonSchemaMultiDocument", () => { } }) - deepStrictEqual(document, { + deepStrictEqual(SchemaRepresentation.toJsonMultiDocument(document), { representations: [ { _tag: "Reference", $ref: "A" }, { _tag: "Reference", $ref: "B" } ], references: { - A: { _tag: "Suspend", thunk: { _tag: "Reference", $ref: "A" }, checks: [] }, - B: { _tag: "Suspend", thunk: { _tag: "Reference", $ref: "B" }, checks: [] } + A: { + _tag: "Suspend", + annotations: { identifier: "A" }, + checks: [], + thunk: { _tag: "Reference", $ref: "A" } + }, + B: { + _tag: "Suspend", + annotations: { identifier: "B" }, + checks: [], + thunk: { _tag: "Reference", $ref: "B" } + } } }) }) @@ -95,7 +233,7 @@ describe("fromJsonSchemaMultiDocument", () => { schemas: [{ $ref: "#/$defs/Missing", description: "resolve" }], definitions: {} }), - "Reference Missing not found" + "Invalid reference Missing\n at [\"schemas\"][0][\"$ref\"]" ) }) @@ -110,7 +248,7 @@ describe("fromJsonSchemaMultiDocument", () => { B: { $ref: "#/$defs/A" } } }), - "Circular reference detected: A" + "Invalid reference A\n at [\"schemas\"][0][\"$ref\"]" ) }) }) diff --git a/.context/effect/packages/effect/test/schema/representation/fromRepresentation.test.ts b/.context/effect/packages/effect/test/schema/representation/fromRepresentation.test.ts new file mode 100644 index 000000000..18f46c188 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/fromRepresentation.test.ts @@ -0,0 +1,488 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +const filterId = "acme/schema/minLength" + +const minLengthReviver: SchemaRepresentation.FilterReviver<{ readonly minimum: number }> = { + id: filterId, + payloadSchema: Schema.Struct({ minimum: Schema.Number }), + revive: ({ annotations, payload }) => minLengthCheck(payload.minimum, annotations) +} + +function minLengthCheck(minimum: number, annotations?: Schema.Annotations.Filter) { + return Schema.makeFilter((value) => value.length >= minimum, { + representation: { id: filterId, payload: { minimum } }, + ...annotations + }) +} + +function revive( + schema: Schema.Top, + revivers: ReadonlyArray = [] +): Schema.Top { + return SchemaRepresentation.fromRepresentation( + SchemaRepresentation.fromJson(SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast))), + { revivers } + ) +} + +function assertRepresentationRoundtrip( + schema: Schema.Top, + revivers: ReadonlyArray = [] +): Schema.Top { + const expected = SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)) + const revived = SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(expected), { revivers }) + assert.deepStrictEqual(SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(revived.ast)), expected) + return revived +} + +function errorFrom(run: () => unknown): Error { + let result: Error | undefined + throws(run, (error: unknown) => { + assert.instanceOf(error, Error) + result = error + return undefined + }) + assert.isDefined(result) + return result +} + +function filterJson(): Schema.Json { + return SchemaRepresentation.toJson( + SchemaRepresentation.toRepresentation(Schema.String.check(minLengthCheck(2)).ast) + ) +} + +describe("SchemaRepresentation.fromRepresentation", () => { + it("revives Null", () => { + assertRepresentationRoundtrip(Schema.Null) + }) + + it("revives Undefined", () => { + assertRepresentationRoundtrip(Schema.Undefined) + }) + + it("revives Void", () => { + assertRepresentationRoundtrip(Schema.Void) + }) + + it("revives Never", () => { + assertRepresentationRoundtrip(Schema.Never) + }) + + it("revives Unknown", () => { + assertRepresentationRoundtrip(Schema.Unknown) + }) + + it("revives Any", () => { + assertRepresentationRoundtrip(Schema.Any) + }) + + it("revives String", () => { + assertRepresentationRoundtrip(Schema.String) + }) + + it("revives Number", () => { + assertRepresentationRoundtrip(Schema.Number) + }) + + it("revives Boolean", () => { + assertRepresentationRoundtrip(Schema.Boolean) + }) + + it("revives BigInt", () => { + assertRepresentationRoundtrip(Schema.BigInt) + }) + + it("revives Symbol", () => { + assertRepresentationRoundtrip(Schema.Symbol) + }) + + it("revives ObjectKeyword", () => { + assertRepresentationRoundtrip(Schema.ObjectKeyword) + }) + + it("revives Literal without changing its type", () => { + for ( + const [literal, differentType] of [ + ["1", 1n], + ["true", true], + [1, "1"], + [1n, "1"], + [true, "true"] + ] as const + ) { + const schema = assertRepresentationRoundtrip(Schema.Literal(literal)) + assert.isTrue(Schema.is(schema)(literal)) + assert.isFalse(Schema.is(schema)(differentType)) + } + }) + + it("revives UniqueSymbol", () => { + const symbol = Symbol.for("acme/schema/symbol") + const schema = assertRepresentationRoundtrip(Schema.UniqueSymbol(symbol)) + assert.isTrue(Schema.is(schema)(symbol)) + assert.isFalse(Schema.is(schema)(Symbol.for("acme/schema/other"))) + }) + + it("revives Enum", () => { + const schema = assertRepresentationRoundtrip(Schema.Enum({ A: "a", One: 1 })) + assert.isTrue(Schema.is(schema)("a")) + assert.isTrue(Schema.is(schema)(1)) + assert.isFalse(Schema.is(schema)("other")) + }) + + it("revives ambiguous Enum values without changing their type", () => { + const schema = assertRepresentationRoundtrip(Schema.Enum({ + StringNaN: "NaN", + NumberNaN: Number.NaN, + StringInfinity: "Infinity", + NumberInfinity: Number.POSITIVE_INFINITY + })) + assert.isTrue(Schema.is(schema)("NaN")) + assert.isTrue(Schema.is(schema)(Number.NaN)) + assert.isTrue(Schema.is(schema)("Infinity")) + assert.isTrue(Schema.is(schema)(Number.POSITIVE_INFINITY)) + }) + + it("revives TemplateLiteral", () => { + const schema = assertRepresentationRoundtrip(Schema.TemplateLiteral(["prefix-", Schema.String])) + assert.isTrue(Schema.is(schema)("prefix-value")) + assert.isFalse(Schema.is(schema)("value")) + }) + + it("revives a reference in TemplateLiteral as a concrete part", () => { + const schema = assertRepresentationRoundtrip( + Schema.TemplateLiteral(["prefix-", Schema.String.annotate({ identifier: "Part" })]) + ) + assert.strictEqual(schema.ast._tag, "TemplateLiteral") + if (schema.ast._tag === "TemplateLiteral") { + assert.strictEqual(schema.ast.parts[1]._tag, "String") + } + assert.isTrue(Schema.is(schema)("prefix-value")) + }) + + it("revives nested references in a TemplateLiteral union", () => { + const schema = assertRepresentationRoundtrip( + Schema.TemplateLiteral([ + "prefix-", + Schema.Union([ + Schema.Literal("a").annotate({ identifier: "A" }), + Schema.Literal("b").annotate({ identifier: "B" }) + ]).annotate({ identifier: "Part" }) + ]) + ) + assert.isTrue(Schema.is(schema)("prefix-a")) + assert.isTrue(Schema.is(schema)("prefix-b")) + assert.isFalse(Schema.is(schema)("prefix-c")) + }) + + it("revives Tuple", () => { + assertRepresentationRoundtrip(Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])) + }) + + it("revives Array", () => { + assertRepresentationRoundtrip(Schema.Array(Schema.String)) + }) + + it("revives TupleWithRest", () => { + assertRepresentationRoundtrip( + Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) + ) + }) + + it("revives Struct", () => { + assertRepresentationRoundtrip(Schema.Struct({ + required: Schema.String, + optional: Schema.optionalKey(Schema.Number), + mutable: Schema.mutableKey(Schema.Boolean) + })) + }) + + it("revives Record", () => { + assertRepresentationRoundtrip(Schema.Record(Schema.String, Schema.Number)) + }) + + it("revives a reference used as a Record key", () => { + const schema = assertRepresentationRoundtrip( + Schema.Record(Schema.String.annotate({ identifier: "Key" }), Schema.Number) + ) + assert.strictEqual(schema.ast._tag, "Objects") + if (schema.ast._tag === "Objects") { + assert.strictEqual(schema.ast.indexSignatures[0].parameter._tag, "String") + } + assert.deepStrictEqual(Schema.decodeUnknownSync(schema as Schema.Codec)({ a: 1 }), { a: 1 }) + }) + + it("revives StructWithRest", () => { + assertRepresentationRoundtrip( + Schema.StructWithRest(Schema.Struct({ value: Schema.Number }), [Schema.Record(Schema.Symbol, Schema.String)]) + ) + }) + + it("revives Union", () => { + const schema = assertRepresentationRoundtrip(Schema.Union([Schema.String, Schema.Number])) + assert.isTrue(Schema.is(schema)("value")) + assert.isTrue(Schema.is(schema)(1)) + assert.isFalse(Schema.is(schema)(true)) + }) + + it("revives an empty Union as Never", () => { + const schema = SchemaRepresentation.fromRepresentation({ + representation: { _tag: "Union", types: [], mode: "anyOf", checks: [] }, + references: {} + }, { revivers: [] }) + assert.isFalse(Schema.is(schema)(undefined)) + assert.isFalse(Schema.is(schema)(null)) + }) + + it("revives Suspend", () => { + interface Category { + readonly name: string + readonly children: ReadonlyArray + } + const Category: Schema.Codec = Schema.Struct({ + name: Schema.String, + children: Schema.Array(Schema.suspend((): Schema.Codec => Category)) + }).annotate({ identifier: "Category" }) + const schema = revive(Category) as Schema.Codec + assert.strictEqual(schema.ast._tag, "Objects") + if (schema.ast._tag === "Objects") { + const children = schema.ast.propertySignatures.find((property) => property.name === "children") + assert.isDefined(children) + assert.strictEqual(children.type._tag, "Arrays") + if (children.type._tag === "Arrays") { + assert.strictEqual(children.type.rest[0]._tag, "Suspend") + } + } + assert.deepStrictEqual( + Schema.decodeUnknownSync(schema)({ + name: "root", + children: [{ name: "child", children: [] }] + }), + { + name: "root", + children: [{ name: "child", children: [] }] + } + ) + assert.strictEqual(SchemaRepresentation.toRepresentation(schema.ast).representation._tag, "Reference") + }) + + it("restores node annotations", () => { + const schema = revive(Schema.String.annotate({ title: "Name" })) + assert.strictEqual(schema.ast.annotations?.title, "Name") + }) + + it("restores node annotations before checks", () => { + const schema = assertRepresentationRoundtrip( + Schema.String + .annotate({ title: "node" }) + .check(minLengthCheck(2, { description: "check" })), + [minLengthReviver] + ) + + assert.strictEqual(schema.ast.annotations?.title, "node") + assert.strictEqual(schema.ast.checks?.[0].annotations?.description, "check") + assert.strictEqual(schema.ast.checks?.[0].annotations?.title, undefined) + }) + + it("restores tuple element annotations", () => { + const schema = revive(Schema.Tuple([Schema.String.annotateKey({ description: "element" })])) + const representation = SchemaRepresentation.toRepresentation(schema.ast).representation + assert.strictEqual(representation._tag, "Arrays") + if (representation._tag === "Arrays") { + assert.strictEqual(representation.elements[0].annotations?.description, "element") + } + }) + + it("restores property annotations", () => { + const schema = revive(Schema.Struct({ value: Schema.String.annotateKey({ description: "property" }) })) + const representation = SchemaRepresentation.toRepresentation(schema.ast).representation + assert.strictEqual(representation._tag, "Objects") + if (representation._tag === "Objects") { + assert.strictEqual(representation.propertySignatures[0].annotations?.description, "property") + } + }) + + it("restores brands", () => { + assertRepresentationRoundtrip(Schema.String.pipe(Schema.brand("A"), Schema.brand("B"))) + }) + + it("restores a node representation annotation without schema dependencies", () => { + assertRepresentationRoundtrip(Schema.String.annotate({ + representation: { id: "acme/schema/String", payload: null } + })) + }) + + it("restores a node representation annotation with schema dependencies", () => { + assertRepresentationRoundtrip(Schema.String.annotate({ + representation: { + id: "acme/schema/String", + payload: null, + schemas: [Schema.Number.ast] + } + })) + }) + + it("revives a Filter", () => { + const schema = assertRepresentationRoundtrip( + Schema.String.check(minLengthCheck(2, { description: "at least two" }).abort()), + [minLengthReviver] + ) + assert.strictEqual(Schema.decodeUnknownResult(schema as Schema.Codec)("a")._tag, "Failure") + assert.strictEqual(schema.ast.checks?.[0]._tag, "Filter") + assert.isTrue(schema.ast.checks?.[0]._tag === "Filter" && schema.ast.checks[0].aborted) + assert.strictEqual(schema.ast.checks?.[0].annotations?.description, "at least two") + }) + + it("revives a FilterGroup without an identity from its children", () => { + const group = Schema.makeFilterGroup([minLengthCheck(2), minLengthCheck(3)], { description: "both" }) + const schema = assertRepresentationRoundtrip(Schema.String.check(group), [minLengthReviver]) + assert.strictEqual(Schema.decodeUnknownResult(schema as Schema.Codec)("ab")._tag, "Failure") + assert.strictEqual(schema.ast.checks?.[0].annotations?.description, "both") + }) + + it("uses an identified FilterGroup reviver instead of its persisted children", () => { + const groupId = "acme/schema/group" + const document = SchemaRepresentation.fromJson({ + representation: { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + representation: { id: groupId, payload: null }, + checks: [{ + _tag: "Filter", + representation: { id: filterId, payload: { minimum: 1 } }, + aborted: false + }] + }] + }, + references: {} + }) + const reviver: SchemaRepresentation.FilterGroupReviver = { + id: groupId, + payloadSchema: Schema.Null, + revive: () => Schema.makeFilterGroup([Schema.makeFilter((value) => value !== "blocked")]) + } + const schema = SchemaRepresentation.fromRepresentation(document, { revivers: [reviver] }) as Schema.Codec + assert.strictEqual(Schema.decodeUnknownSync(schema)("allowed"), "allowed") + assert.strictEqual(Schema.decodeUnknownResult(schema)("blocked")._tag, "Failure") + }) + + it("revives a Declaration", () => { + const id = "acme/schema/Box" + const Box = Schema.declare<{ readonly value: string }>( + (input): input is { readonly value: string } => + typeof input === "object" && input !== null && typeof (input as any).value === "string", + { representation: { id, payload: { label: "Box" } } } + ) + const reviver: SchemaRepresentation.DeclarationReviver<{ readonly label: string }> = { + id, + payloadSchema: Schema.Struct({ label: Schema.String }), + revive: ({ annotations, payload }) => + Schema.declare<{ readonly value: string }>( + (input): input is { readonly value: string } => + typeof input === "object" && input !== null && typeof (input as any).value === "string", + { ...annotations, representation: { id, payload } } + ) + } + const schema = assertRepresentationRoundtrip(Box, [reviver]) as Schema.Codec + assert.deepStrictEqual(Schema.decodeUnknownSync(schema)({ value: "ok" }), { value: "ok" }) + }) + + it("reports a missing reviver", () => { + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(filterJson()), { revivers: [] }) + ) + .message, + `Missing reviver for ${filterId}\n at ["representation"]["checks"][0]["representation"]` + ) + }) + + it("rejects duplicate reviver IDs", () => { + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation({ + representation: { _tag: "String", checks: [] }, + references: {} + }, { revivers: [minLengthReviver, minLengthReviver] }) + ).message, + `Duplicate reviver for ${filterId}\n at ["revivers"][1]["id"]` + ) + }) + + it("rejects an invalid reviver payload", () => { + const json = filterJson() as any + json.representation.checks[0].representation.payload = { minimum: "two" } + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(json), { + revivers: [minLengthReviver] + }) + ).message, + `Invalid representation payload for ${filterId}\n at ["representation"]["checks"][0]["representation"]["payload"]` + ) + }) + + it("preserves a reviver exception by identity", () => { + const cause = new Error("boom") + const reviver = { + ...minLengthReviver, + revive: () => { + throw cause + } + } + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation(SchemaRepresentation.fromJson(filterJson()), { + revivers: [reviver] + }) + ), + cause + ) + }) + + it("requires a representation identity on a Filter", () => { + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation( + { + representation: { _tag: "String", checks: [{ _tag: "Filter", aborted: false }] }, + references: {} + }, + { revivers: [] } + ) + ).message, + `Missing representation annotation\n at ["representation"]["checks"][0]["representation"]` + ) + }) + + it("requires a representation identity on a Declaration", () => { + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation( + { + representation: { _tag: "Declaration", typeParameters: [], checks: [] }, + references: {} + }, + { revivers: [] } + ) + ).message, + `Missing representation annotation\n at ["representation"]["representation"]` + ) + }) + + it("reports an invalid reference", () => { + assert.strictEqual( + errorFrom(() => + SchemaRepresentation.fromRepresentation({ + representation: { _tag: "Reference", $ref: "Missing" }, + references: {} + }, { revivers: [] }) + ).message, + `Invalid reference Missing\n at ["representation"]["$ref"]` + ) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/fromRepresentations.test.ts b/.context/effect/packages/effect/test/schema/representation/fromRepresentations.test.ts new file mode 100644 index 000000000..832d378a7 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/fromRepresentations.test.ts @@ -0,0 +1,170 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaRepresentation } from "effect" + +function decode(schema: Schema.Top, input: unknown): unknown { + return Schema.decodeUnknownSync(schema as Schema.Codec)(input) +} + +describe("SchemaRepresentation.fromRepresentations", () => { + it("preserves root order", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [ + { _tag: "String", checks: [] }, + { _tag: "Boolean", checks: [] }, + { _tag: "Number", checks: [] } + ], + references: {} + }, { revivers: [] }) + + assert.strictEqual(decode(schemas[0], "value"), "value") + assert.strictEqual(decode(schemas[1], true), true) + assert.strictEqual(decode(schemas[2], 1), 1) + }) + + it("does not revive unreachable references", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [{ _tag: "String", checks: [] }], + references: { + Unused: { + _tag: "Declaration", + typeParameters: [], + representation: { id: "missing", payload: null }, + checks: [] + } + } + }, { revivers: [] }) + + assert.strictEqual(decode(schemas[0], "value"), "value") + }) + + it("normalizes aliases while preserving the outer reference", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [{ _tag: "Reference", $ref: "Alias" }], + references: { + Alias: { _tag: "Reference", $ref: "Value" }, + Value: { _tag: "String", checks: [] } + } + }, { revivers: [] }) + + assert.strictEqual(schemas[0].ast._tag, "String") + assert.strictEqual(decode(schemas[0], "value"), "value") + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schemas[0].ast).representation, { + _tag: "Reference", + $ref: "Alias" + }) + }) + + it("shares a resolved reference between roots", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [ + { _tag: "Reference", $ref: "Shared" }, + { _tag: "Reference", $ref: "Shared" } + ], + references: { + Shared: { _tag: "Number", checks: [] } + } + }, { revivers: [] }) + + assert.strictEqual(schemas[0], schemas[1]) + assert.strictEqual(schemas[0].ast._tag, "Number") + }) + + it("resolves a reachable __proto__ reference", () => { + const references: Record = {} + Object.defineProperty(references, "__proto__", { + value: { _tag: "String", checks: [] }, + enumerable: true + }) + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [{ _tag: "Reference", $ref: "__proto__" }], + references + }, { revivers: [] }) + + assert.strictEqual(decode(schemas[0], "value"), "value") + }) + + it("revives recursive definitions", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [{ _tag: "Reference", $ref: "Recursive" }], + references: { + Recursive: { + _tag: "Objects", + checks: [], + propertySignatures: [ + { + name: "value", + type: { _tag: "Number", checks: [] }, + isOptional: false, + isMutable: false + }, + { + name: "next", + type: { _tag: "Reference", $ref: "Recursive" }, + isOptional: true, + isMutable: false + } + ], + indexSignatures: [] + } + } + }, { revivers: [] }) + + assert.strictEqual(schemas[0].ast._tag, "Objects") + if (schemas[0].ast._tag === "Objects") { + assert.strictEqual(schemas[0].ast.propertySignatures[1].type._tag, "Suspend") + } + assert.deepStrictEqual( + decode(schemas[0], { + value: 1, + next: { value: 2 } + }), + { + value: 1, + next: { value: 2 } + } + ) + }) + + it("revives mutually recursive definitions with concrete roots", () => { + const schemas = SchemaRepresentation.fromRepresentations({ + representations: [ + { _tag: "Reference", $ref: "A" }, + { _tag: "Reference", $ref: "B" } + ], + references: { + A: { + _tag: "Objects", + checks: [], + propertySignatures: [{ + name: "b", + type: { _tag: "Reference", $ref: "B" }, + isOptional: true, + isMutable: false + }], + indexSignatures: [] + }, + B: { + _tag: "Objects", + checks: [], + propertySignatures: [{ + name: "a", + type: { _tag: "Reference", $ref: "A" }, + isOptional: true, + isMutable: false + }], + indexSignatures: [] + } + } + }, { revivers: [] }) + + assert.strictEqual(schemas[0].ast._tag, "Objects") + assert.strictEqual(schemas[1].ast._tag, "Objects") + if (schemas[0].ast._tag === "Objects" && schemas[1].ast._tag === "Objects") { + const b = schemas[0].ast.propertySignatures[0].type + const a = schemas[1].ast.propertySignatures[0].type + assert.isTrue(b._tag === "Suspend" || a._tag === "Suspend") + } + assert.deepStrictEqual(decode(schemas[0], { b: { a: {} } }), { b: { a: {} } }) + assert.deepStrictEqual(decode(schemas[1], { a: { b: {} } }), { a: { b: {} } }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/makeCode.test.ts b/.context/effect/packages/effect/test/schema/representation/makeCode.test.ts new file mode 100644 index 000000000..7e030a6b7 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/makeCode.test.ts @@ -0,0 +1,11 @@ +import { assert, describe, it } from "@effect/vitest" +import { SchemaRepresentation } from "effect" + +describe("SchemaRepresentation.makeCode", () => { + it("constructs runtime and type source", () => { + assert.deepStrictEqual(SchemaRepresentation.makeCode("Schema.String", "string"), { + runtime: "Schema.String", + Type: "string" + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/schemaToJsonSchemaDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/schemaToJsonSchemaDocument.test.ts new file mode 100644 index 000000000..c52e81882 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/schemaToJsonSchemaDocument.test.ts @@ -0,0 +1,140 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" + +describe("Schema.toJsonSchemaDocument", () => { + it("uses the encoded side for representations and JSON Schema", () => { + const representation = Schema.toRepresentation(Schema.FiniteFromString) + assert.strictEqual(representation.representation._tag, "String") + + const typeRepresentation = Schema.toRepresentation(Schema.toType(Schema.FiniteFromString)) + assert.strictEqual(typeRepresentation.representation._tag, "Number") + + assert.deepStrictEqual(Schema.toJsonSchemaDocument(Schema.FiniteFromString), { + dialect: "draft-2020-12", + schema: { type: "string" }, + definitions: {} + }) + }) + + it("projects encoded tuple elements for JSON Schema", () => { + assert.deepStrictEqual(Schema.toJsonSchemaDocument(Schema.Tuple([Schema.NumberFromString])).schema, { + type: "array", + prefixItems: [{ type: "string" }], + minItems: 1, + maxItems: 1 + }) + }) + + it("preserves Number checks on the finite encoded branch", () => { + assert.deepStrictEqual( + Schema.toJsonSchemaDocument(Schema.Number.check(Schema.isGreaterThan(0))), + { + dialect: "draft-2020-12", + schema: { + anyOf: [ + { + type: "number", + allOf: [{ exclusiveMinimum: 0 }] + }, + { + type: "string", + enum: ["Infinity", "-Infinity", "NaN"] + } + ] + }, + definitions: {} + } + ) + }) + + it("preserves output, references and generation options", () => { + const shared = Schema.String.check(Schema.isMinLength(2)).annotate({ + identifier: "Shared", + description: "shared text", + "x-consumer": "kept" + }) + const schema = Schema.Struct({ + first: shared, + second: shared, + count: Schema.FiniteFromString + }).annotate({ description: "root" }) + const options: Schema.ToJsonSchemaOptions = { + additionalProperties: true, + generateDescriptions: true, + includeAnnotationKey: (key) => key === "x-consumer" + } + + assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema, options), { + dialect: "draft-2020-12", + schema: { + type: "object", + properties: { + first: { $ref: "#/$defs/Shared" }, + second: { $ref: "#/$defs/Shared" }, + count: { + type: "string", + description: "a string that will be decoded as a finite number" + } + }, + required: ["first", "second", "count"], + additionalProperties: true, + description: "root" + }, + definitions: { + Shared: { + type: "string", + allOf: [{ + minLength: 2, + description: "shared text", + "x-consumer": "kept" + }] + } + } + }) + }) + + it("uses custom compiler annotations without a central built-in switch", () => { + const custom = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/schema/minTwoCharacters", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(Schema.toJsonSchemaDocument(custom), { + dialect: "draft-2020-12", + schema: { + type: "string", + allOf: [{ minLength: 2 }] + }, + definitions: {} + }) + }) + + it("emits JSON content media types after encoded projection", () => { + const schema = Schema.fromJsonString(Schema.Struct({ + value: Schema.FiniteFromString + })) + + assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema).schema, { + type: "string", + contentMediaType: "application/json" + }) + }) + + it("approximates declarations without a JSON codec", () => { + const schema = Schema.declare((input): input is string => typeof input === "string", { + representation: { + id: "test/schema/opaqueString", + payload: null + } + }) + + assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema), { + dialect: "draft-2020-12", + schema: {}, + definitions: {} + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/stringTreeRoundtrip.test.ts b/.context/effect/packages/effect/test/schema/representation/stringTreeRoundtrip.test.ts new file mode 100644 index 000000000..92d241f12 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/stringTreeRoundtrip.test.ts @@ -0,0 +1,72 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaGetter } from "effect" + +function makeValueCodec(type: Type, value: Schema.Codec) { + return value.pipe( + Schema.encodeTo(Schema.Struct({ type: Schema.tag(type), value }), { + decode: SchemaGetter.transform((encoded: { readonly type: Type; readonly value: Value }) => encoded.value), + encode: SchemaGetter.transform((value: Value) => ({ type, value })) + }) + ) +} + +const LiteralValueSchema = Schema.Union([ + makeValueCodec("string", Schema.String), + makeValueCodec("number", Schema.Finite), + makeValueCodec("bigint", Schema.BigInt), + makeValueCodec("boolean", Schema.Boolean) +]) + +const EnumValueSchema = Schema.Union([ + makeValueCodec("string", Schema.String), + makeValueCodec("number", Schema.Number) +]) + +const PropertyNameSchema = Schema.Union([ + makeValueCodec("string", Schema.String), + makeValueCodec("number", Schema.Number), + makeValueCodec("symbol", Schema.Symbol) +]) + +function assertRoundtrips( + schema: Schema.Codec, + cases: ReadonlyArray +): void { + const codec = Schema.toCodecStringTree(schema) + const encode = Schema.encodeSync(codec) + const decode = Schema.decodeSync(codec) + for (const [value, encoded] of cases) { + assert.deepStrictEqual(encode(value), encoded) + assert.deepStrictEqual(decode(encoded), value) + } +} + +describe("SchemaRepresentation encoded values through StringTree", () => { + it("preserves literal value types", () => { + assertRoundtrips(LiteralValueSchema, [ + ["1", { type: "string", value: "1" }], + [1, { type: "number", value: "1" }], + [1n, { type: "bigint", value: "1" }], + ["true", { type: "string", value: "true" }], + [true, { type: "boolean", value: "true" }] + ]) + }) + + it("preserves enum value types", () => { + assertRoundtrips(EnumValueSchema, [ + ["NaN", { type: "string", value: "NaN" }], + [Number.NaN, { type: "number", value: "NaN" }], + ["Infinity", { type: "string", value: "Infinity" }], + [Number.POSITIVE_INFINITY, { type: "number", value: "Infinity" }] + ]) + }) + + it("preserves property name types", () => { + assertRoundtrips(PropertyNameSchema, [ + ["1", { type: "string", value: "1" }], + [1, { type: "number", value: "1" }], + ["Symbol(key)", { type: "string", value: "Symbol(key)" }], + [Symbol.for("key"), { type: "symbol", value: "Symbol(key)" }] + ]) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toCodeDocument.annotations.test.ts b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.annotations.test.ts new file mode 100644 index 000000000..e1c2e96b1 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.annotations.test.ts @@ -0,0 +1,598 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaRepresentation } from "effect" +import { assertInclude, throws } from "../../utils/assert.ts" + +function expectError(thunk: () => void, expected: string | Error): void { + if (typeof expected === "string") { + throws(thunk, expected) + } else { + throws(thunk, (error: unknown) => { + assert.strictEqual(error, expected) + return undefined + }) + } +} + +const StringRepresentation: SchemaRepresentation.Representation = { + _tag: "String", + checks: [] +} + +const NumberRepresentation: SchemaRepresentation.Representation = { + _tag: "Number", + checks: [] +} + +const EmptyUnionRepresentation: SchemaRepresentation.Representation = { + _tag: "Union", + types: [], + mode: "anyOf", + checks: [] +} + +describe("SchemaRepresentation.toCodeDocument annotations", () => { + it("compiles an empty union as Never", () => { + assert.deepStrictEqual( + SchemaRepresentation.toCodeDocument({ + representations: [EmptyUnionRepresentation], + references: {} + }).codes, + [{ runtime: "Schema.Never", Type: "never" }] + ) + }) + + it("compiles the isPattern and Option vertical slice", () => { + const document = SchemaRepresentation.toRepresentations([ + Schema.String.check(Schema.isPattern(/^a+$/)).ast, + Schema.Option(Schema.String).ast + ]) + const output = SchemaRepresentation.toCodeDocument(document) + + assert.deepStrictEqual(output.codes, [ + { + runtime: + `Schema.String.check(Schema.isPattern(new RegExp("^a+$")).annotate({ "expected": "a string matching the RegExp ^a+$" }))`, + Type: "string" + }, + { + runtime: `Schema.Option(Schema.String).annotate({ "expected": "Option" })`, + Type: "Option.Option" + } + ]) + assert.deepStrictEqual(output.artifacts, [{ + _tag: "Import", + importDeclaration: `import * as Option from "effect/Option"` + }]) + }) + + it("passes compiled dependencies to checks and deduplicates imports", () => { + const check = (name: string): SchemaRepresentation.Filter => ({ + _tag: "Filter", + aborted: false, + representation: { + id: `acme/schema/${name}`, + payload: null, + schemas: [StringRepresentation] + }, + annotations: { + toCode: ({ schemas }: SchemaRepresentation.Generation.CheckInput) => ({ + runtime: `Custom.${name}(${schemas[0].runtime})`, + importDeclarations: [`import * as Custom from "acme/Custom"`] + }) + } + }) + const document: SchemaRepresentation.MultiDocument = { + representations: [{ _tag: "String", checks: [check("first"), check("second")] }], + references: {} + } + + const output = SchemaRepresentation.toCodeDocument(document) + assert.strictEqual( + output.codes[0].runtime, + "Schema.String.check(Custom.first(Schema.String)).check(Custom.second(Schema.String))" + ) + assert.deepStrictEqual(output.artifacts, [{ + _tag: "Import", + importDeclaration: `import * as Custom from "acme/Custom"` + }]) + }) + + it("emits supported annotation trees atomically", () => { + const document = SchemaRepresentation.toRepresentations([ + Schema.String.annotate({ + emitted: { + bigint: 1n, + symbol: Symbol.for("shared"), + nan: NaN, + positive: Infinity, + negative: -Infinity + }, + omitted: { value: 1, callback: () => 2 } + }).ast + ]) + + const runtime = SchemaRepresentation.toCodeDocument(document).codes[0].runtime + assertInclude(runtime, `"bigint": 1n`) + assertInclude(runtime, `"symbol": Symbol.for("shared")`) + assertInclude(runtime, `"nan": NaN`) + assertInclude(runtime, `"positive": Infinity`) + assertInclude(runtime, `"negative": -Infinity`) + assert.isFalse(runtime.includes("omitted")) + assert.isFalse(runtime.includes("callback")) + }) + + it("preserves fallback identifiers", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [{ + _tag: "String", + checks: [], + annotations: { "~identifier": "Person" } + }], + references: {} + }) + + assertInclude(output.codes[0].runtime, `.annotate({ "~identifier": "Person" })`) + }) + + it("emits tuple element and property annotations", () => { + const document: SchemaRepresentation.MultiDocument = { + representations: [ + { + _tag: "Arrays", + elements: [{ + isOptional: false, + type: StringRepresentation, + annotations: { + element: { value: 1 }, + omitted: { callback: () => 1 } + } + }], + rest: [], + checks: [] + }, + { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: NumberRepresentation, + isOptional: false, + isMutable: false, + annotations: { property: true } + }], + indexSignatures: [], + checks: [] + } + ], + references: {} + } + + assert.deepStrictEqual(SchemaRepresentation.toCodeDocument(document).codes, [ + { + runtime: `Schema.Tuple([Schema.String.annotateKey({ "element": { "value": 1 } })])`, + Type: "readonly [string]" + }, + { + runtime: `Schema.Struct({ "value": Schema.Number.annotateKey({ "property": true }) })`, + Type: `{ readonly "value": number }` + } + ]) + }) + + it("uses group overrides without visiting children and preserves abort", () => { + let visits = 0 + const child: SchemaRepresentation.Filter = { + _tag: "Filter", + aborted: true, + annotations: { + toCode: () => { + visits++ + return { runtime: "Custom.child()" } + } + } + } + const document: SchemaRepresentation.MultiDocument = { + representations: [ + { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + checks: [child], + annotations: { toCode: () => ({ runtime: "Custom.group()" }) } + }] + }, + { + _tag: "String", + checks: [{ _tag: "FilterGroup", checks: [child] }] + } + ], + references: {} + } + + const output = SchemaRepresentation.toCodeDocument(document) + assert.strictEqual(visits, 1) + assert.strictEqual(output.codes[0].runtime, "Schema.String.check(Custom.group())") + assert.strictEqual( + output.codes[1].runtime, + "Schema.String.check(Schema.makeFilterGroup([Custom.child().abort()]))" + ) + }) + + it("passes type parameters to declaration callbacks", () => { + const declaration: SchemaRepresentation.Representation = { + _tag: "Declaration", + typeParameters: [StringRepresentation], + checks: [], + representation: { + id: "acme/schema/Box", + payload: null + }, + annotations: { + toCode: ({ typeParameters }: SchemaRepresentation.Generation.DeclarationInput) => ({ + runtime: `Custom.box(${typeParameters[0].runtime})`, + Type: `Custom.Box<${typeParameters[0].Type}>`, + importDeclarations: [`import * as Custom from "acme/Custom"`] + }) + } + } + const output = SchemaRepresentation.toCodeDocument({ + representations: [declaration], + references: {} + }) + + assert.deepStrictEqual(output.codes, [{ + runtime: "Custom.box(Schema.String)", + Type: "Custom.Box" + }]) + }) + + it("reports missing toCode callbacks and preserves callback exceptions", () => { + const missing: SchemaRepresentation.MultiDocument = { + representations: [{ + _tag: "String", + checks: [{ _tag: "Filter", aborted: false }] + }], + references: {} + } + expectError( + () => SchemaRepresentation.toCodeDocument(missing), + `Missing toCode callback\n at ["representations"][0]["checks"][0]["annotations"]["toCode"]` + ) + + const cause = new Error("toCode callback") + const throwing: SchemaRepresentation.MultiDocument = { + representations: [{ + _tag: "String", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + toCode: () => { + throw cause + } + } + }] + }], + references: {} + } + expectError( + () => SchemaRepresentation.toCodeDocument(throwing), + cause + ) + }) + + it("generates content media types, optional pre-rest elements and numeric properties", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [ + { + _tag: "String", + annotations: { contentMediaType: "text/plain" }, + checks: [] + }, + { + _tag: "Arrays", + elements: [{ + type: StringRepresentation, + isOptional: true + }], + rest: [NumberRepresentation], + checks: [] + }, + { + _tag: "Objects", + propertySignatures: [{ + name: 1, + type: { _tag: "Boolean", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + } + ], + references: {} + }) + + assert.deepStrictEqual(output.codes, [ + { + runtime: `Schema.String.annotate({ "contentMediaType": "text/plain" })`, + Type: "string" + }, + { + runtime: `Schema.TupleWithRest(Schema.Tuple([Schema.optionalKey(Schema.String)]), [Schema.Number])`, + Type: `readonly [string?, ...Array]` + }, + { + runtime: `Schema.Struct({ 1: Schema.Boolean })`, + Type: `{ readonly 1: boolean }` + } + ]) + assert.deepStrictEqual(output.artifacts, []) + }) + + it("emits references that are not reachable from a root", () => { + const document: SchemaRepresentation.MultiDocument = { + representations: [StringRepresentation], + references: { + Unused: NumberRepresentation + } + } + assert.deepStrictEqual(SchemaRepresentation.toCodeDocument(document).references, { + nonRecursives: [{ + $ref: "Unused", + code: { runtime: "Schema.Number", Type: "number" } + }], + recursives: {} + }) + }) + + it("capitalizes a lowercase reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { abc: NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "Abc") + }) + + it("preserves a lowercase identifier annotation", () => { + const schema = Schema.Struct({ a: Schema.String }).annotate({ identifier: "hello" }) + const output = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toMultiDocument(Schema.toRepresentation(schema)) + ) + assert.deepStrictEqual(output.references.nonRecursives[0], { + $ref: "Hello", + code: { + runtime: `Schema.Struct({ "a": Schema.String }).annotate({ "identifier": "hello" })`, + Type: `{ readonly "a": string }` + } + }) + }) + + it("preserves an uppercase reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { Abc: NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "Abc") + }) + + it("prefixes a numeric reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { "1a": NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "_1a") + }) + + it("replaces punctuation in a reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { "a-b": NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "A_b") + }) + + it("replaces non-ASCII characters in a reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { café: NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "Caf_") + }) + + it("replaces an emoji reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { "🤖": NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "_") + }) + + it("uses an underscore for an empty reference identifier", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { "": NumberRepresentation } + }) + assert.strictEqual(output.references.nonRecursives[0].$ref, "_") + }) + + it("makes colliding sanitized reference identifiers unique", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { + "a-b": NumberRepresentation, + a_b: StringRepresentation + } + }) + assert.deepStrictEqual( + output.references.nonRecursives.map((reference) => reference.$ref), + ["A_b", "A_b1"] + ) + }) + + it("orders definitions after dependencies found in every representation position", () => { + const reference = ($ref: string): SchemaRepresentation.Reference => ({ _tag: "Reference", $ref }) + const filter: SchemaRepresentation.Filter = { + _tag: "Filter", + aborted: false, + representation: { id: "acme/schema/filter", payload: null, schemas: [reference("C")] }, + annotations: { + toCode: () => ({ runtime: "Schema.makeFilter(() => true)" }) + } + } + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { + A: StringRepresentation, + B: NumberRepresentation, + C: { _tag: "Boolean", checks: [] }, + D: { + _tag: "Declaration", + typeParameters: [reference("A")], + representation: { id: "acme/schema/declaration", payload: null }, + annotations: { + toCode: () => ({ runtime: "Schema.String", Type: "string" }) + }, + checks: [{ _tag: "FilterGroup", checks: [filter] }] + }, + E: { _tag: "TemplateLiteral", parts: [reference("D")], checks: [] }, + F: { _tag: "Union", types: [reference("E"), reference("A")], mode: "anyOf", checks: [] }, + G: { + _tag: "Arrays", + elements: [{ type: reference("F"), isOptional: false }], + rest: [reference("A")], + checks: [] + }, + H: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: reference("G"), + isOptional: false, + isMutable: false + }], + indexSignatures: [{ parameter: reference("A"), type: reference("B") }], + checks: [] + }, + I: { _tag: "Suspend", thunk: reference("H"), checks: [] } + } + }) + + assert.deepStrictEqual( + output.references.nonRecursives.map((entry) => entry.$ref), + ["A", "B", "C", "D", "E", "F", "G", "H", "I"] + ) + }) + + it("orders a shared dependency referenced from multiple representation positions once", () => { + const shared: SchemaRepresentation.Reference = { _tag: "Reference", $ref: "A" } + const output = SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { + A: StringRepresentation, + B: { + _tag: "Arrays", + elements: [{ type: shared, isOptional: false }], + rest: [shared], + checks: [] + }, + C: { + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ parameter: shared, type: shared }], + checks: [] + } + } + }) + + assert.deepStrictEqual( + output.references.nonRecursives.map((entry) => entry.$ref), + ["A", "B", "C"] + ) + }) + + it("generates a StructWithRest without fixed properties", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [{ + _tag: "Objects", + propertySignatures: [], + indexSignatures: [ + { parameter: StringRepresentation, type: NumberRepresentation }, + { parameter: { _tag: "Symbol", checks: [] }, type: { _tag: "Boolean", checks: [] } } + ], + checks: [] + }], + references: {} + }) + + assert.strictEqual(output.codes[0].Type, `{ readonly [x: string]: number, readonly [x: symbol]: boolean }`) + }) + + it("generates a single-literal Union as Literal", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [{ + _tag: "Union", + types: [{ _tag: "Literal", literal: "a", checks: [] }], + mode: "anyOf", + checks: [] + }], + references: {} + }) + + assert.deepStrictEqual(output.codes[0], { runtime: `Schema.Literal("a")`, Type: `"a"` }) + }) + + it("emits every member of a mutually recursive reference cycle", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [{ _tag: "Reference", $ref: "A" }], + references: { + A: { _tag: "Reference", $ref: "B" }, + B: { _tag: "Reference", $ref: "A" } + } + }) + + assert.deepStrictEqual(output.references, { + nonRecursives: [], + recursives: { + A: { runtime: "Schema.suspend((): Schema.Codec => B)", Type: "B" }, + B: { runtime: "Schema.suspend((): Schema.Codec => A)", Type: "A" } + } + }) + }) + + it("emits a non-recursive definition that depends on a recursive definition", () => { + const output = SchemaRepresentation.toCodeDocument({ + representations: [{ _tag: "Reference", $ref: "B" }], + references: { + A: { _tag: "Reference", $ref: "A" }, + B: { _tag: "Reference", $ref: "A" } + } + }) + + assert.deepStrictEqual(output.references, { + nonRecursives: [{ $ref: "B", code: { runtime: "A", Type: "A" } }], + recursives: { A: { runtime: "Schema.suspend((): Schema.Codec => A)", Type: "A" } } + }) + }) + + it("reports missing references with their document path", () => { + const document: SchemaRepresentation.MultiDocument = { + representations: [{ _tag: "Reference", $ref: "Missing" }], + references: {} + } + expectError( + () => SchemaRepresentation.toCodeDocument(document), + `Invalid reference Missing\n at ["representations"][0]["$ref"]` + ) + }) + + it("reports a missing reference from a definition", () => { + expectError( + () => + SchemaRepresentation.toCodeDocument({ + representations: [StringRepresentation], + references: { Value: { _tag: "Reference", $ref: "Missing" } } + }), + `Invalid reference Missing\n at ["references"]["Value"]["$ref"]` + ) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts index d10615207..77d4674ef 100644 --- a/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/toCodeDocument.test.ts @@ -1,6 +1,6 @@ import { JsonSchema, Schema, SchemaRepresentation } from "effect" import { describe, it } from "vitest" -import { deepStrictEqual, strictEqual } from "../../utils/assert.ts" +import { assertTrue, deepStrictEqual, strictEqual, throws } from "../../utils/assert.ts" type Category = { readonly name: string @@ -36,73 +36,84 @@ describe("toCodeDocument", () => { function assertSchema(input: { readonly schema: Schema.Constraint - readonly reviver?: SchemaRepresentation.Reviver | undefined }, expected: Expected) { - const multiDocument = SchemaRepresentation.fromASTs([input.schema.ast]) - assertMultiDocument({ multiDocument }, expected) + const multiDocument = SchemaRepresentation.toRepresentations([input.schema.ast]) + assertMultiDocument(multiDocument, expected) } function assertJsonSchema(input: { readonly schema: JsonSchema.JsonSchema - readonly reviver?: SchemaRepresentation.Reviver | undefined }, expected: Expected) { - const multiDocument = SchemaRepresentation.toMultiDocument( - SchemaRepresentation.fromJsonSchemaDocument(JsonSchema.fromSchemaDraft2020_12(input.schema), { - onEnter: (js) => { - if (js.type === "object" && js.additionalProperties === undefined) { - return { ...js, additionalProperties: false } - } - return js - } + const schema = SchemaRepresentation.fromJsonSchemaDocument( + JsonSchema.fromSchemaDraft2020_12(input.schema), + { + onEnter: (js) => + js.type === "object" && js.additionalProperties === undefined + ? { ...js, additionalProperties: false } + : js + } + ) + assertMultiDocument(SchemaRepresentation.toRepresentations([schema.ast]), expected) + } + + function assertMultiDocument( + multiDocument: SchemaRepresentation.MultiDocument, + expected: Expected + ) { + const codeDocument = SchemaRepresentation.toCodeDocument(multiDocument) + deepStrictEqual( + canonicalizeGeneratedCode(codeDocument), + canonicalizeGeneratedCode({ + codes: Array.isArray(expected.codes) ? expected.codes : [expected.codes], + references: { + nonRecursives: expected.references?.nonRecursives ?? [], + recursives: expected.references?.recursives ?? {} + }, + artifacts: expected.artifacts ?? [] }) ) - assertMultiDocument({ multiDocument }, expected) } - function assertMultiDocument(input: { - readonly multiDocument: SchemaRepresentation.MultiDocument - readonly reviver?: SchemaRepresentation.Reviver | undefined - }, expected: Expected) { - const codeDocument = SchemaRepresentation.toCodeDocument(input.multiDocument, { reviver: input.reviver }) - deepStrictEqual(codeDocument, { - codes: Array.isArray(expected.codes) ? expected.codes : [expected.codes], - references: { - nonRecursives: expected.references?.nonRecursives ?? [], - recursives: expected.references?.recursives ?? {} - }, - artifacts: expected.artifacts ?? [] - }) + function canonicalizeGeneratedCode(input: unknown): unknown { + if (typeof input === "string") { + return input + .replaceAll(/"expected": "(?:\\.|[^"\\])*"(?:, )?/g, "") + .replaceAll(", }", " }") + .replaceAll(".annotate({ })", "") + } + if (Array.isArray(input)) return input.map(canonicalizeGeneratedCode) + if (typeof input !== "object" || input === null) return input + return Object.fromEntries( + Object.entries(input).map(([key, value]) => [key, canonicalizeGeneratedCode(value)]) + ) } const makeCode = SchemaRepresentation.makeCode - - describe("options", () => { - it("reviver can override declaration code and recur into type parameters", () => { - }) - }) + const templateType = (...types: ReadonlyArray) => `\`${types.map((type) => `\${${type}}`).join("")}\`` describe("Declaration", () => { - it("declaration without typeConstructor annotation", () => { - assertSchema({ schema: Schema.instanceOf(URL) }, { - codes: makeCode("Schema.Null", "null") - }) + it("declaration without a toCode annotation", () => { + throws( + () => assertSchema({ schema: Schema.instanceOf(URL) }, { codes: makeCode("", "") }), + "Missing toCode callback\n at [\"representations\"][0][\"annotations\"][\"toCode\"]" + ) }) it("Error", () => { - assertSchema({ schema: Schema.Error() }, { - codes: makeCode(`Schema.Error()`, "globalThis.Error") + assertSchema({ schema: Schema.ErrorInstance() }, { + codes: makeCode(`Schema.ErrorInstance()`, "globalThis.Error") }) }) it("Error with stack", () => { - assertSchema({ schema: Schema.Error({ includeStack: true }) }, { - codes: makeCode(`Schema.Error({"includeStack":true})`, "globalThis.Error") + assertSchema({ schema: Schema.ErrorInstance({ includeStack: true }) }, { + codes: makeCode(`Schema.ErrorInstance({"includeStack":true})`, "globalThis.Error") }) }) it("Error with excluded cause", () => { - assertSchema({ schema: Schema.Error({ excludeCause: true }) }, { - codes: makeCode(`Schema.Error({"excludeCause":true})`, "globalThis.Error") + assertSchema({ schema: Schema.ErrorInstance({ excludeCause: true }) }, { + codes: makeCode(`Schema.ErrorInstance({"excludeCause":true})`, "globalThis.Error") }) }) @@ -367,7 +378,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.String.check(Schema.isMinLength(1, { description: "a" })) }, { - codes: makeCode(`Schema.String.check(Schema.isMinLength(1, { "description": "a" }))`, "string") + codes: makeCode(`Schema.String.check(Schema.isMinLength(1).annotate({ "description": "a" }))`, "string") } ) }) @@ -376,7 +387,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.String.check(Schema.isMinLength(1)).annotate({ "description": "a" }) }, { - codes: makeCode(`Schema.String.check(Schema.isMinLength(1, { "description": "a" }))`, "string") + codes: makeCode(`Schema.String.check(Schema.isMinLength(1).annotate({ "description": "a" }))`, "string") } ) }) @@ -413,7 +424,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.String.check(Schema.isGUID({ message: "message" })) }, { - codes: makeCode(`Schema.String.check(Schema.isGUID({ "message": "message" }))`, "string") + codes: makeCode(`Schema.String.check(Schema.isGUID().annotate({ "message": "message" }))`, "string") } ) }) @@ -561,7 +572,7 @@ describe("toCodeDocument", () => { artifacts: [{ _tag: "Symbol", identifier: "_symbol", - generation: makeCode(`Symbol("a")`, `typeof _symbol`) + code: makeCode(`Symbol("a")`, `typeof _symbol`) }] } ) @@ -572,7 +583,7 @@ describe("toCodeDocument", () => { artifacts: [{ _tag: "Symbol", identifier: "_symbol", - generation: makeCode(`Symbol()`, `typeof _symbol`) + code: makeCode(`Symbol()`, `typeof _symbol`) }] } ) @@ -586,7 +597,7 @@ describe("toCodeDocument", () => { artifacts: [{ _tag: "Symbol", identifier: "_symbol", - generation: makeCode(`Symbol.for("a")`, `typeof _symbol`) + code: makeCode(`Symbol.for("a")`, `typeof _symbol`) }] } ) @@ -600,7 +611,7 @@ describe("toCodeDocument", () => { artifacts: [{ _tag: "Symbol", identifier: "_symbol", - generation: makeCode(`Symbol.for("a")`, `typeof _symbol`) + code: makeCode(`Symbol.for("a")`, `typeof _symbol`) }] } ) @@ -617,11 +628,11 @@ describe("toCodeDocument", () => { }) }, { - codes: makeCode(`Schema.Enum(_Enum)`, `typeof _Enum`), + codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "A": "a", "B": "b" }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "A" = "a", "B" = "b" }`, `typeof _Enum`) }] } ) @@ -635,12 +646,12 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.Enum(_Enum).annotate({ "description": "a" })`, - `typeof _Enum` + `_Enum` ), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "A": "a", "B": "b" }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "A" = "a", "B" = "b" }`, `typeof _Enum`) }] } ) @@ -655,11 +666,11 @@ describe("toCodeDocument", () => { }) }, { - codes: makeCode(`Schema.Enum(_Enum)`, `typeof _Enum`), + codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "One": 1, "Two": 2 }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "One" = 1, "Two" = 2 }`, `typeof _Enum`) }] } ) @@ -673,12 +684,12 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.Enum(_Enum).annotate({ "description": "a" })`, - `typeof _Enum` + `_Enum` ), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "One": 1, "Two": 2 }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "One" = 1, "Two" = 2 }`, `typeof _Enum`) }] } ) @@ -693,11 +704,11 @@ describe("toCodeDocument", () => { }) }, { - codes: makeCode(`Schema.Enum(_Enum)`, `typeof _Enum`), + codes: makeCode(`Schema.Enum(_Enum)`, `_Enum`), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "A": "a", "One": 1 }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "A" = "a", "One" = 1 }`, `typeof _Enum`) }] } ) @@ -711,12 +722,12 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.Enum(_Enum).annotate({ "description": "a" })`, - `typeof _Enum` + `_Enum` ), artifacts: [{ _tag: "Enum", identifier: "_Enum", - generation: makeCode(`enum _Enum { "A": "a", "One": 1 }`, `typeof _Enum`) + code: makeCode(`enum _Enum { "A" = "a", "One" = 1 }`, `typeof _Enum`) }] } ) @@ -737,7 +748,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.TemplateLiteral([Schema.Literal("a")]) }, { - codes: makeCode(`Schema.TemplateLiteral([Schema.Literal("a")])`, "`a`") + codes: makeCode(`Schema.TemplateLiteral([Schema.Literal("a")])`, templateType(`"a"`)) } ) }) @@ -746,7 +757,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.TemplateLiteral([Schema.Literal(1)]) }, { - codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1)])`, "`1`") + codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1)])`, templateType("1")) } ) }) @@ -755,7 +766,7 @@ describe("toCodeDocument", () => { assertSchema( { schema: Schema.TemplateLiteral([Schema.Literal(1n)]) }, { - codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1n)])`, "`1`") + codes: makeCode(`Schema.TemplateLiteral([Schema.Literal(1n)])`, templateType("1n")) } ) }) @@ -766,7 +777,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Literal("b"), Schema.Literal("c")])`, - "`abc`" + templateType(`"a"`, `"b"`, `"c"`) ) } ) @@ -778,7 +789,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a b"), Schema.String])`, - "`a b${string}`" + templateType(`"a b"`, "string") ) } ) @@ -787,10 +798,19 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("\\n"), Schema.String])`, - "`\n${string}`" + templateType(`"\\n"`, "string") ) } ) + + for (const literal of ["`", "${number}", "\\"]) { + const code = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toRepresentations([ + Schema.TemplateLiteral([Schema.Literal(literal)]).ast + ]) + ).codes[0] + strictEqual(code.Type, templateType(JSON.stringify(literal))) + } }) it("only schemas", () => { @@ -829,7 +849,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.String, Schema.Literal("a")])`, - "`${string}a`" + templateType("string", `"a"`) ) } ) @@ -838,7 +858,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Number, Schema.Literal("a")])`, - "`${number}a`" + templateType("number", `"a"`) ) } ) @@ -847,7 +867,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.BigInt, Schema.Literal("a")])`, - "`${bigint}a`" + templateType("bigint", `"a"`) ) } ) @@ -859,7 +879,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.String])`, - "`a${string}`" + templateType(`"a"`, "string") ) } ) @@ -868,7 +888,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Number])`, - "`a${number}`" + templateType(`"a"`, "number") ) } ) @@ -877,7 +897,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.BigInt])`, - "`a${bigint}`" + templateType(`"a"`, "bigint") ) } ) @@ -889,7 +909,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number])`, - "`${string}-${number}`" + templateType("string", `"-"`, "number") ) } ) @@ -902,7 +922,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.String, Schema.Literal("-"), Schema.Number]).annotate({ "description": "ad" })`, - "`${string}-${number}`" + templateType("string", `"-"`, "number") ) } ) @@ -919,7 +939,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.TemplateLiteral([Schema.String, Schema.Literals(["-", "+"]), Schema.Number])])`, - "`a${string}-${number}` | `a${string}+${number}`" + templateType(`"a"`, templateType("string", `"-" | "+"`, "number")) ) } ) @@ -933,7 +953,7 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literal("a"), Schema.Union([Schema.String, Schema.Number])])`, - "`a${string}` | `a${number}`" + templateType(`"a"`, "string | number") ) } ) @@ -947,12 +967,41 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String])`, - "`a${string}` | `b${string}`" + templateType(`"a" | "b"`, "string") ) } ) }) + it("uses the encoded type of branded parts", () => { + const schema = Schema.TemplateLiteral([ + Schema.String.pipe(Schema.brand("StringPart")), + Schema.Union([ + Schema.Number.pipe(Schema.brand("NumberPart")), + Schema.String.pipe(Schema.brand("OtherStringPart")) + ]) + ]) + const code = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toRepresentations([schema.ast]) + ).codes[0] + + strictEqual( + code.runtime, + `Schema.TemplateLiteral([Schema.String.pipe(Schema.brand("StringPart")), Schema.Union([Schema.Number.pipe(Schema.brand("NumberPart")), Schema.String.pipe(Schema.brand("OtherStringPart"))])])` + ) + strictEqual(code.Type, templateType("string", "number | string")) + }) + + it("resolves the encoded type of branded references", () => { + const part = Schema.String.pipe(Schema.brand("Part")).annotate({ identifier: "Part" }) + const code = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toRepresentations([Schema.TemplateLiteral([part]).ast]) + ).codes[0] + + strictEqual(code.runtime, "Schema.TemplateLiteral([Part])") + strictEqual(code.Type, templateType("string")) + }) + it("multiple unions", () => { assertSchema( { @@ -964,12 +1013,23 @@ describe("toCodeDocument", () => { }, { codes: makeCode( - `Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String, Schema.Union([Schema.BigInt, Schema.Number])])`, - "`a${string}${bigint}` | `a${string}${number}` | `b${string}${bigint}` | `b${string}${number}`" + `Schema.TemplateLiteral([Schema.Literals(["a", "b"]), Schema.String, Schema.Union([Schema.Number, Schema.BigInt])])`, + templateType(`"a" | "b"`, "string", "number | bigint") ) } ) }) + + it("does not expand combinations of unions", () => { + const part = Schema.Literals(["a", "b"]) + const schema = Schema.TemplateLiteral(Array.from({ length: 20 }, () => part)) + const Type = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toRepresentations([schema.ast]) + ).codes[0].Type + + strictEqual(Type, templateType(...Array.from({ length: 20 }, () => `"a" | "b"`))) + assertTrue(Type.length < 500) + }) }) describe("Tuple", () => { @@ -1204,12 +1264,12 @@ describe("toCodeDocument", () => { { codes: makeCode( `Schema.Struct({ [_symbol]: Schema.String })`, - `{ readonly [typeof _symbol]: string }` + `{ readonly [_symbol]: string }` ), artifacts: [{ _tag: "Symbol", identifier: "_symbol", - generation: makeCode(`Symbol.for("a")`, `typeof _symbol`) + code: makeCode(`Symbol.for("a")`, `typeof _symbol`) }] } ) @@ -1342,6 +1402,70 @@ describe("toCodeDocument", () => { }) describe("suspend", () => { + it("implicit recursive reference", () => { + assertMultiDocument({ + representations: [{ _tag: "Reference", $ref: "Category" }], + references: { + Category: { + _tag: "Objects", + propertySignatures: [{ + name: "children", + type: { + _tag: "Arrays", + elements: [], + rest: [{ _tag: "Reference", $ref: "Category" }], + checks: [] + }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + } + } + }, { + codes: makeCode("Category", "Category"), + references: { + recursives: { + Category: makeCode( + `Schema.Struct({ "children": Schema.Array(Schema.suspend((): Schema.Codec => Category)) })`, + `{ readonly "children": ReadonlyArray }` + ) + } + } + }) + }) + + it("supports __proto__ as a recursive reference", () => { + const references = Object.fromEntries([["__proto__", { + _tag: "Objects", + propertySignatures: [{ + name: "next", + type: { _tag: "Reference", $ref: "__proto__" }, + isOptional: true, + isMutable: false + }], + indexSignatures: [], + checks: [] + }]]) as SchemaRepresentation.References + + assertMultiDocument({ + representations: [{ _tag: "Reference", $ref: "__proto__" }], + references + }, { + codes: makeCode("__proto__", "__proto__"), + references: { + recursives: Object.fromEntries([[ + "__proto__", + makeCode( + `Schema.Struct({ "next": Schema.optionalKey(Schema.suspend((): Schema.Codec<__proto__> => __proto__)) })`, + `{ readonly "next"?: __proto__ }` + ) + ]]) + } + }) + }) + it("non-recursive", () => { assertSchema( { @@ -1411,7 +1535,7 @@ describe("toCodeDocument", () => { references: { recursives: { A: makeCode( - `Schema.Struct({ "a": Schema.optionalKey(Suspend_) }).annotate({ "identifier": "A" })`, + `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec => Suspend_)) }).annotate({ "identifier": "A" })`, `{ readonly "a"?: Suspend_ }` ), Suspend_: makeCode( @@ -1440,7 +1564,7 @@ describe("toCodeDocument", () => { `Objects_` ), Objects_: makeCode( - `Schema.Struct({ "a": Schema.optionalKey(A) })`, + `Schema.Struct({ "a": Schema.optionalKey(Schema.suspend((): Schema.Codec => A)) })`, `{ readonly "a"?: A }` ) } @@ -1549,15 +1673,6 @@ describe("toCodeDocument", () => { }) describe("checks", () => { - it("isDateValid", () => { - assertSchema( - { schema: Schema.Date.check(Schema.isDateValid()) }, - { - codes: makeCode(`Schema.Date.check(Schema.isDateValid())`, "globalThis.Date") - } - ) - }) - it("isGreaterThanDate", () => { assertSchema( { schema: Schema.Date.check(Schema.isGreaterThanDate(new Date(0))) }, @@ -1599,7 +1714,7 @@ describe("toCodeDocument", () => { { schema: Schema.Date.check(Schema.isBetweenDate({ minimum: new Date(0), maximum: new Date(1) })) }, { codes: makeCode( - `Schema.Date.check(Schema.isBetweenDate({ minimum: new Date(0), maximum: new Date(1), exclusiveMinimum: undefined, exclusiveMaximum: undefined))`, + `Schema.Date.check(Schema.isBetweenDate({ minimum: new Date(0), maximum: new Date(1), exclusiveMinimum: undefined, exclusiveMaximum: undefined }))`, "globalThis.Date" ) } @@ -1683,7 +1798,16 @@ describe("toCodeDocument", () => { } } }, { - codes: makeCode(`Schema.String`, "string") + codes: makeCode(`A`, "A"), + references: { + nonRecursives: [{ + $ref: "A", + code: makeCode( + `Schema.String.annotate({ "identifier": "A" })`, + "string" + ) + }] + } }) }) @@ -1724,7 +1848,7 @@ describe("toCodeDocument", () => { { $ref: "A", code: makeCode( - `Schema.Struct({ "b": Schema.Number.check(Schema.isFinite()), "a": Schema.String })`, + `Schema.Struct({ "b": Schema.Number.check(Schema.isFinite()), "a": Schema.String }).annotate({ "identifier": "A" })`, `{ readonly "b": number, readonly "a": string }` ) } @@ -1734,356 +1858,3 @@ describe("toCodeDocument", () => { }) }) }) - -describe("sanitizeJavaScriptIdentifier", () => { - const sanitizeJavaScriptIdentifier = SchemaRepresentation.sanitizeJavaScriptIdentifier - - it("returns '_' for empty input", () => { - strictEqual(sanitizeJavaScriptIdentifier(""), "_") - }) - - it("returns input when already a valid uppercase-start identifier", () => { - strictEqual(sanitizeJavaScriptIdentifier("Abc"), "Abc") - strictEqual(sanitizeJavaScriptIdentifier("_"), "_") - strictEqual(sanitizeJavaScriptIdentifier("$"), "$") - strictEqual(sanitizeJavaScriptIdentifier("$a_b9"), "$a_b9") - strictEqual(sanitizeJavaScriptIdentifier("A1b2"), "A1b2") - }) - - it("uppercases a leading ASCII letter", () => { - strictEqual(sanitizeJavaScriptIdentifier("abc"), "Abc") - strictEqual(sanitizeJavaScriptIdentifier("a0"), "A0") - strictEqual(sanitizeJavaScriptIdentifier("a1b2c3"), "A1b2c3") - strictEqual(sanitizeJavaScriptIdentifier("class"), "Class") - }) - - it("prefixes '_' when starting with a digit", () => { - strictEqual(sanitizeJavaScriptIdentifier("1"), "_1") - strictEqual(sanitizeJavaScriptIdentifier("1a"), "_1a") - strictEqual(sanitizeJavaScriptIdentifier("9lives"), "_9lives") - }) - - it("replaces invalid leading characters with '_'", () => { - strictEqual(sanitizeJavaScriptIdentifier(" abc"), "_abc") - strictEqual(sanitizeJavaScriptIdentifier("-a"), "_a") - strictEqual(sanitizeJavaScriptIdentifier(".a"), "_a") - strictEqual(sanitizeJavaScriptIdentifier(" a"), "_a") - strictEqual(sanitizeJavaScriptIdentifier("\ta"), "_a") - }) - - it("replaces invalid characters with '_'", () => { - strictEqual(sanitizeJavaScriptIdentifier("a-b"), "A_b") - strictEqual(sanitizeJavaScriptIdentifier("a b"), "A_b") - strictEqual(sanitizeJavaScriptIdentifier("a.b"), "A_b") - strictEqual(sanitizeJavaScriptIdentifier("a/b"), "A_b") - }) - - it("replaces multiple invalid characters with '_'", () => { - strictEqual(sanitizeJavaScriptIdentifier("a-b c"), "A_b_c") - strictEqual(sanitizeJavaScriptIdentifier("a..b"), "A__b") - strictEqual(sanitizeJavaScriptIdentifier("a--b"), "A__b") - strictEqual(sanitizeJavaScriptIdentifier("a b\tc"), "A_b_c") - }) - - it("replaces non-ascii characters with '_' under ASCII rules", () => { - strictEqual(sanitizeJavaScriptIdentifier("café"), "Caf_") - strictEqual(sanitizeJavaScriptIdentifier("你好"), "__") - strictEqual(sanitizeJavaScriptIdentifier("🤖"), "_") - strictEqual(sanitizeJavaScriptIdentifier("a🤖b"), "A_b") - }) - - it("allows '$' and '_' anywhere", () => { - strictEqual(sanitizeJavaScriptIdentifier("a$b"), "A$b") - strictEqual(sanitizeJavaScriptIdentifier("a_b"), "A_b") - strictEqual(sanitizeJavaScriptIdentifier("$a_b9"), "$a_b9") - }) - - it("keeps already-sanitized results stable (idempotent)", () => { - const cases = [ - "", - "abc", - "_", - "$", - "a1b2", - "a-b", - "a b", - "1a", - "-a", - "class", - "café", - "a🤖b" - ] as const - - for (const input of cases) { - const once = sanitizeJavaScriptIdentifier(input) - const twice = sanitizeJavaScriptIdentifier(once) - strictEqual(twice, once) - } - }) - - it("preserves length when only replacements are needed", () => { - strictEqual(sanitizeJavaScriptIdentifier("a-b").length, "a-b".length) - strictEqual(sanitizeJavaScriptIdentifier("a b").length, "a b".length) - strictEqual(sanitizeJavaScriptIdentifier("..").length, "..".length) - }) - - it("increases length only when prefixing is required", () => { - strictEqual(sanitizeJavaScriptIdentifier("1a"), "_1a") - strictEqual(sanitizeJavaScriptIdentifier("1a").length, "1a".length + 1) - }) -}) - -describe("topologicalSort", () => { - function assertTopologicalSort( - definitions: Record, - expected: SchemaRepresentation.TopologicalSort - ) { - deepStrictEqual(SchemaRepresentation.topologicalSort(definitions), expected) - } - - it("empty definitions", () => { - assertTopologicalSort( - {}, - { nonRecursives: [], recursives: {} } - ) - }) - - it("single definition with no dependencies", () => { - assertTopologicalSort( - { - A: { _tag: "String", checks: [] } - }, - { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } } - ], - recursives: {} - } - ) - }) - - it("multiple independent definitions", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { _tag: "Number", checks: [] }, - C: { _tag: "Boolean" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { $ref: "B", representation: { _tag: "Number", checks: [] } }, - { $ref: "C", representation: { _tag: "Boolean" } } - ], - recursives: {} - }) - }) - - it("A -> B -> C", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "B" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } }, - { $ref: "C", representation: { _tag: "Reference", $ref: "B" } } - ], - recursives: {} - }) - }) - - it("A -> B, A -> C", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } }, - { $ref: "C", representation: { _tag: "Reference", $ref: "A" } } - ], - recursives: {} - }) - }) - - it("A -> B -> C, A -> D", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "B" }, - D: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } }, - { $ref: "D", representation: { _tag: "Reference", $ref: "A" } }, - { $ref: "C", representation: { _tag: "Reference", $ref: "B" } } - ], - recursives: {} - }) - }) - - it("self-referential definition (A -> A)", () => { - assertTopologicalSort({ - A: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [], - recursives: { - A: { _tag: "Reference", $ref: "A" } - } - }) - }) - - it("mutual recursion (A -> B -> A)", () => { - assertTopologicalSort({ - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [], - recursives: { - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "A" } - } - }) - }) - - it("complex cycle (A -> B -> C -> A)", () => { - assertTopologicalSort({ - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "C" }, - C: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [], - recursives: { - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "C" }, - C: { _tag: "Reference", $ref: "A" } - } - }) - }) - - it("mixed recursive and non-recursive definitions", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "C" }, - D: { _tag: "Reference", $ref: "E" }, - E: { _tag: "Reference", $ref: "D" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } } - ], - recursives: { - C: { _tag: "Reference", $ref: "C" }, - D: { _tag: "Reference", $ref: "E" }, - E: { _tag: "Reference", $ref: "D" } - } - }) - }) - - it("nested $ref in object properties", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { - _tag: "Objects", - propertySignatures: [{ - name: "value", - type: { _tag: "Reference", $ref: "A" }, - isOptional: false, - isMutable: false - }], - indexSignatures: [], - checks: [] - } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { - $ref: "B", - representation: { - _tag: "Objects", - propertySignatures: [{ - name: "value", - type: { _tag: "Reference", $ref: "A" }, - isOptional: false, - isMutable: false - }], - indexSignatures: [], - checks: [] - } - } - ], - recursives: {} - }) - }) - - it("nested $ref in array rest", () => { - assertTopologicalSort({ - A: { _tag: "String", checks: [] }, - B: { - _tag: "Arrays", - elements: [], - rest: [{ _tag: "Reference", $ref: "A" }], - checks: [] - } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "String", checks: [] } }, - { - $ref: "B", - representation: { _tag: "Arrays", elements: [], rest: [{ _tag: "Reference", $ref: "A" }], checks: [] } - } - ], - recursives: {} - }) - }) - - it("external $ref (not in definitions) should be ignored", () => { - assertTopologicalSort({ - A: { _tag: "Reference", $ref: "#/definitions/External" }, - B: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [ - { $ref: "A", representation: { _tag: "Reference", $ref: "#/definitions/External" } }, - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } } - ], - recursives: {} - }) - }) - - it("multiple cycles with independent definitions", () => { - assertTopologicalSort({ - Independent: { _tag: "String", checks: [] }, - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "D" }, - D: { _tag: "Reference", $ref: "C" } - }, { - nonRecursives: [ - { $ref: "Independent", representation: { _tag: "String", checks: [] } } - ], - recursives: { - A: { _tag: "Reference", $ref: "B" }, - B: { _tag: "Reference", $ref: "A" }, - C: { _tag: "Reference", $ref: "D" }, - D: { _tag: "Reference", $ref: "C" } - } - }) - }) - - it("definition depending on recursive definition", () => { - assertTopologicalSort({ - A: { _tag: "Reference", $ref: "A" }, - B: { _tag: "Reference", $ref: "A" } - }, { - nonRecursives: [ - { $ref: "B", representation: { _tag: "Reference", $ref: "A" } } - ], - recursives: { - A: { _tag: "Reference", $ref: "A" } - } - }) - }) -}) diff --git a/.context/effect/packages/effect/test/schema/representation/toJson.test.ts b/.context/effect/packages/effect/test/schema/representation/toJson.test.ts new file mode 100644 index 000000000..14233cbe2 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toJson.test.ts @@ -0,0 +1,605 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaAST, SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +function makeStringProperty(name: Name) { + return { + name, + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + } as const +} + +function assertToJson( + representation: SchemaRepresentation.Representation, + jsonRepresentation: Schema.Json +): void { + assert.deepStrictEqual( + SchemaRepresentation.toJson({ representation, references: {} }), + { representation: jsonRepresentation, references: {} } + ) +} + +function assertRoundtrip( + representation: SchemaRepresentation.Representation, + jsonRepresentation: Schema.Json +): void { + const document: SchemaRepresentation.Document = { representation, references: {} } + const json: Schema.Json = { representation: jsonRepresentation, references: {} } + assertToJson(representation, jsonRepresentation) + assert.deepStrictEqual(SchemaRepresentation.fromJson(json), document) +} + +describe("SchemaRepresentation.toJson", () => { + it("rejects invalid documents", () => { + throws( + () => + SchemaRepresentation.toJson({ + representation: { _tag: "Reference", $ref: "" }, + references: {} + }), + `Expected a value with a length of at least 1\n at ["representation"]["$ref"]` + ) + }) + + it("rejects checks on Suspend representations", () => { + throws( + () => + SchemaRepresentation.toJson({ + representation: { + _tag: "Suspend", + checks: [null], + thunk: { _tag: "String", checks: [] } + } as never, + references: {} + }), + `Expected no excess property\n at ["representation"]["checks"][0]` + ) + }) + + it("requires representation when persisting a Filter", () => { + throws( + () => + SchemaRepresentation.toJson({ + representation: { _tag: "String", checks: [{ _tag: "Filter", aborted: false }] }, + references: {} + }), + `Missing key\n at ["representation"]["checks"][0]["representation"]` + ) + }) + + it("removes live callbacks from a custom filter", () => { + const filter = Schema.makeFilter(() => true, { + description: "custom", + callback: () => "live", + representation: { + id: "acme/schema/custom", + payload: { minimum: 1 }, + schemas: [Schema.Number.ast] + }, + toCode: () => ({ runtime: "Custom" }), + toJsonSchema: () => ({ minLength: 1 }) + }).abort() + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(Schema.String.check(filter).ast)), + { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "acme/schema/custom", + payload: { minimum: 1 }, + schemas: [{ _tag: "Number", checks: [] }] + }, + annotations: { + description: "custom" + }, + aborted: true + }] + }, + references: {} + } + ) + }) + + it("removes live callbacks from a custom declaration", () => { + const schema = Schema.declare((input): input is string => typeof input === "string", { + description: "custom", + representation: { id: "acme/schema/custom", payload: null }, + toCode: () => ({ runtime: "Custom", Type: "string" }), + toJsonSchema: () => ({ type: "string" }) + }) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)), + { + representation: { + _tag: "Declaration", + representation: { id: "acme/schema/custom", payload: null }, + annotations: { + description: "custom" + }, + typeParameters: [], + checks: [] + }, + references: {} + } + ) + }) + + it("preserves JSON annotations", () => { + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ values: ["1", "Symbol(a)", "NaN"] }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { values: ["1", "Symbol(a)", "NaN"] }, + checks: [] + }, + references: {} + }) + }) + + it("omits annotations containing only non-JSON values", () => { + assertToJson( + { + _tag: "String", + annotations: { callback: () => "live" }, + checks: [] + }, + { _tag: "String", checks: [] } + ) + }) + + it("omits undefined annotations", () => { + assertToJson( + { + _tag: "String", + annotations: undefined, + checks: [] + }, + { _tag: "String", checks: [] } + ) + }) + + it("prunes nested representation annotations", () => { + assertToJson( + { + _tag: "Union", + types: [{ + _tag: "String", + annotations: { + title: "nested", + callback: () => "live" + }, + checks: [] + }], + mode: "anyOf", + checks: [] + }, + { + _tag: "Union", + types: [{ + _tag: "String", + annotations: { title: "nested" }, + checks: [] + }], + mode: "anyOf", + checks: [] + } + ) + }) + + it("prunes annotations in check schema dependencies", () => { + assertToJson( + { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "acme/schema/custom", + payload: null, + schemas: [{ + _tag: "Number", + annotations: { + title: "dependency", + callback: () => "live" + }, + checks: [] + }] + }, + aborted: false + }] + }, + { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "acme/schema/custom", + payload: null, + schemas: [{ + _tag: "Number", + annotations: { title: "dependency" }, + checks: [] + }] + }, + aborted: false + }] + } + ) + }) + + it("preserves __proto__ annotations as data properties", () => { + const annotations: Record = {} + Object.defineProperty(annotations, "__proto__", { + value: "safe", + enumerable: true + }) + + assertToJson( + { + _tag: "String", + annotations, + checks: [] + }, + { + _tag: "String", + annotations: JSON.parse(`{"__proto__":"safe"}`), + checks: [] + } + ) + }) + + it("preserves shared JSON annotation values", () => { + const shared = { value: "shared" } + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ value: { left: shared, right: shared } }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { + value: { + left: { value: "shared" }, + right: { value: "shared" } + } + }, + checks: [] + }, + references: {} + }) + }) + + it("omits a cyclic annotation atomically", () => { + const cyclic: { self?: unknown } = {} + cyclic.self = cyclic + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ cyclic, title: "kept" }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { title: "kept" }, + checks: [] + }, + references: {} + }) + }) + + it("omits a sparse-array annotation atomically", () => { + const sparse = new Array(1) + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ sparse, title: "kept" }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { title: "kept" }, + checks: [] + }, + references: {} + }) + }) + + it("omits an annotation containing bigint atomically", () => { + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ invalid: { value: 1n }, title: "kept" }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { title: "kept" }, + checks: [] + }, + references: {} + }) + }) + + it("omits an annotation containing undefined atomically", () => { + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ invalid: { value: undefined }, title: "kept" }).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { title: "kept" }, + checks: [] + }, + references: {} + }) + }) + + it("encodes annotation accessors", () => { + const accessor = {} + Object.defineProperty(accessor, "value", { + enumerable: true, + get() { + return "value" + } + }) + const document = SchemaRepresentation.toRepresentation(Schema.String.annotate({ accessor }).ast) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { accessor: { value: "value" } }, + checks: [] + }, + references: {} + }) + }) + + it("preserves filter groups without an identity", () => { + const first = Schema.makeFilter(() => true, { + representation: { id: "acme/schema/first", payload: null } + }) + const second = Schema.makeFilter(() => true, { + representation: { id: "acme/schema/second", payload: null } + }).abort() + const group = Schema.makeFilterGroup([first, second], { description: "both" }) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(Schema.String.check(group).ast)), + { + representation: { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + annotations: { description: "both" }, + checks: [ + { + _tag: "Filter", + representation: { id: "acme/schema/first", payload: null }, + aborted: false + }, + { + _tag: "Filter", + representation: { id: "acme/schema/second", payload: null }, + aborted: true + } + ] + }] + }, + references: {} + } + ) + }) + + it("preserves tuple element annotations independently", () => { + const schema = Schema.Tuple([ + Schema.String.annotateKey({ description: "element", callback: () => "live" }) + ]) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)), + { + representation: { + _tag: "Arrays", + elements: [{ + type: { _tag: "String", checks: [] }, + isOptional: false, + annotations: { description: "element" } + }], + rest: [], + checks: [] + }, + references: {} + } + ) + }) + + it("preserves property annotations independently", () => { + const schema = Schema.Struct({ + value: Schema.String.annotateKey({ description: "property", callback: () => "live" }) + }) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)), + { + representation: { + _tag: "Objects", + propertySignatures: [{ + name: { type: "string", value: "value" }, + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false, + annotations: { description: "property" } + }], + indexSignatures: [], + checks: [] + }, + references: {} + } + ) + }) + + it("encodes bigint structural values", () => { + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(Schema.Literal(1n).ast)), + { + representation: { + _tag: "Literal", + literal: { type: "bigint", value: "1" }, + checks: [] + }, + references: {} + } + ) + }) + + it("preserves ambiguous Enum values", () => { + assertRoundtrip( + { + _tag: "Enum", + enums: [ + ["StringNaN", "NaN"], + ["NumberNaN", Number.NaN], + ["StringInfinity", "Infinity"], + ["NumberInfinity", Number.POSITIVE_INFINITY] + ], + checks: [] + }, + { + _tag: "Enum", + enums: [ + ["StringNaN", { type: "string", value: "NaN" }], + ["NumberNaN", { type: "number", value: "NaN" }], + ["StringInfinity", { type: "string", value: "Infinity" }], + ["NumberInfinity", { type: "number", value: "Infinity" }] + ], + checks: [] + } + ) + }) + + it("preserves ambiguous property names", () => { + const globalSymbol = Symbol.for("acme/schema/key") + assertRoundtrip( + { + _tag: "Objects", + propertySignatures: [ + makeStringProperty("1"), + makeStringProperty(1), + makeStringProperty("Symbol(acme/schema/key)"), + makeStringProperty(globalSymbol) + ], + indexSignatures: [], + checks: [] + }, + { + _tag: "Objects", + propertySignatures: [ + makeStringProperty({ type: "string", value: "1" }), + makeStringProperty({ type: "number", value: 1 }), + makeStringProperty({ type: "string", value: "Symbol(acme/schema/key)" }), + makeStringProperty({ type: "symbol", value: "Symbol(acme/schema/key)" }) + ], + indexSignatures: [], + checks: [] + } + ) + }) + + it("preserves string literals resembling non-finite numbers", () => { + for (const literal of ["NaN", "Infinity", "-Infinity"]) { + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(Schema.Literal(literal).ast)), + { + representation: { _tag: "Literal", literal: { type: "string", value: literal }, checks: [] }, + references: {} + } + ) + } + }) + + it("encodes global symbols", () => { + assert.deepStrictEqual( + SchemaRepresentation.toJson( + SchemaRepresentation.toRepresentation(Schema.UniqueSymbol(Symbol.for("acme/schema/key")).ast) + ), + { + representation: { + _tag: "UniqueSymbol", + symbol: "Symbol(acme/schema/key)", + checks: [] + }, + references: {} + } + ) + }) + + it("rejects local symbols", () => { + throws( + () => + SchemaRepresentation.toJson( + SchemaRepresentation.toRepresentation(Schema.UniqueSymbol(Symbol("local")).ast) + ), + `cannot serialize to string, Symbol is not registered\n at ["representation"]["symbol"]` + ) + }) + + it("rejects local symbols used as property names", () => { + throws( + () => + SchemaRepresentation.toJson({ + representation: { + _tag: "Objects", + propertySignatures: [ + makeStringProperty(Symbol("local")) + ], + indexSignatures: [], + checks: [] + }, + references: {} + }), + `cannot serialize to string, Symbol is not registered\n at ["representation"]["propertySignatures"][0]["name"]["value"]` + ) + }) + + it("encodes recursive references", () => { + let schema: Schema.Codec + schema = Schema.suspend((): Schema.Codec => schema) + + assert.deepStrictEqual( + SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)), + { + representation: { _tag: "Reference", $ref: "Suspend_" }, + references: { + Suspend_: { + _tag: "Suspend", + thunk: { _tag: "Reference", $ref: "Suspend_" }, + checks: [] + } + } + } + ) + }) + + it("encodes fromJsonString annotations", () => { + const schema = SchemaAST.toEncoded(Schema.fromJsonString(Schema.Struct({ value: Schema.Number })).ast) + const document = SchemaRepresentation.toRepresentation(schema) + + assert.deepStrictEqual(SchemaRepresentation.toJson(document), { + representation: { + _tag: "String", + annotations: { + contentMediaType: "application/json", + expected: "a string that will be decoded as JSON" + }, + checks: [] + }, + references: {} + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toJsonMultiDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toJsonMultiDocument.test.ts new file mode 100644 index 000000000..3960c9afa --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toJsonMultiDocument.test.ts @@ -0,0 +1,40 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaRepresentation } from "effect" + +describe("SchemaRepresentation.toJsonMultiDocument", () => { + it("encodes every root in order", () => { + const document = SchemaRepresentation.toRepresentations([ + Schema.String.ast, + Schema.Number.ast, + Schema.Literal(1n).ast + ]) + + assert.deepStrictEqual(SchemaRepresentation.toJsonMultiDocument(document), { + representations: [ + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] }, + { _tag: "Literal", literal: { type: "bigint", value: "1" }, checks: [] } + ], + references: {} + }) + }) + + it("encodes shared references once", () => { + const shared = Schema.String.annotate({ identifier: "Shared", callback: () => "live" }) + const document = SchemaRepresentation.toRepresentations([shared.ast, shared.ast]) + + assert.deepStrictEqual(SchemaRepresentation.toJsonMultiDocument(document), { + representations: [ + { _tag: "Reference", $ref: "Shared" }, + { _tag: "Reference", $ref: "Shared" } + ], + references: { + Shared: { + _tag: "String", + annotations: { identifier: "Shared" }, + checks: [] + } + } + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts new file mode 100644 index 000000000..15d9906e5 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toJsonSchemaDocument.test.ts @@ -0,0 +1,862 @@ +import { assert, describe, it } from "@effect/vitest" +import { type JsonSchema, Schema, SchemaAST, SchemaRepresentation } from "effect" +import { throws } from "../../utils/assert.ts" + +function expectError(thunk: () => void, expected: string | Error): void { + if (typeof expected === "string") { + throws(thunk, expected) + } else { + throws(thunk, (error: unknown) => { + assert.strictEqual(error, expected) + return undefined + }) + } +} + +const StringRepresentation: SchemaRepresentation.Representation = { + _tag: "String", + checks: [] +} + +const NumberRepresentation: SchemaRepresentation.Representation = { + _tag: "Number", + checks: [] +} + +const EmptyUnionRepresentation: SchemaRepresentation.Representation = { + _tag: "Union", + types: [], + mode: "anyOf", + checks: [] +} + +describe("SchemaRepresentation.toJsonSchemaDocument", () => { + // Representation documents are public, persistable inputs. These tests construct them + // directly so the compiler is covered independently from Schema-to-Representation lowering. + function compile(representation: SchemaRepresentation.Representation) { + return SchemaRepresentation.toJsonSchemaDocument({ representation, references: {} }).schema + } + + describe("representation nodes", () => { + const keywords = [ + ["Any", { _tag: "Any", checks: [] }, {}], + ["Unknown", { _tag: "Unknown", checks: [] }, {}], + ["ObjectKeyword", { _tag: "ObjectKeyword", checks: [] }, { + anyOf: [{ type: "object" }, { type: "array" }] + }], + ["Void", { _tag: "Void", checks: [] }, { type: "null" }], + ["Undefined", { _tag: "Undefined", checks: [] }, { type: "null" }], + ["BigInt", { _tag: "BigInt", checks: [] }, { + type: "string", + allOf: [{ pattern: "^-?\\d+$" }] + }], + ["Symbol", { _tag: "Symbol", checks: [] }, { + type: "string", + allOf: [{ pattern: "^Symbol\\((.*)\\)$" }] + }], + ["UniqueSymbol", { _tag: "UniqueSymbol", symbol: Symbol.for("value"), checks: [] }, { + type: "string", + allOf: [{ pattern: "^Symbol\\((.*)\\)$" }] + }], + ["Null", { _tag: "Null", checks: [] }, { type: "null" }], + ["Never", { _tag: "Never", checks: [] }, { not: {} }] + ] satisfies ReadonlyArray< + readonly [string, SchemaRepresentation.Representation, JsonSchema.JsonSchema] + > + + for (const [tag, representation, expected] of keywords) { + it(tag, () => { + assert.deepStrictEqual(compile(representation), expected) + }) + } + + it("compiles Suspend", () => { + assert.deepStrictEqual( + compile({ + _tag: "Suspend", + thunk: StringRepresentation, + checks: [] + }), + { type: "string" } + ) + }) + + it("compiles a bigint Literal", () => { + assert.deepStrictEqual( + compile({ + _tag: "Literal", + literal: 1n, + checks: [] + }), + { + type: "string", + enum: ["1"] + } + ) + }) + + it("compiles a string Literal", () => { + assert.deepStrictEqual( + compile({ + _tag: "Literal", + literal: "a", + checks: [] + }), + { + type: "string", + enum: ["a"] + } + ) + }) + + it("compiles an empty Enum", () => { + assert.deepStrictEqual(compile({ _tag: "Enum", enums: [], checks: [] }), { not: {} }) + }) + + it("compiles an Enum", () => { + assert.deepStrictEqual( + compile({ + _tag: "Enum", + enums: [ + ["A", "a"], + ["One", 1] + ], + checks: [] + }), + { + anyOf: [ + { type: "string", enum: ["a"], title: "A" }, + { type: "number", enum: [1], title: "One" } + ] + } + ) + }) + + it("preserves ambiguous Enum values", () => { + assert.deepStrictEqual( + compile({ + _tag: "Enum", + enums: [ + ["StringNaN", "NaN"], + ["NumberNaN", Number.NaN], + ["StringInfinity", "Infinity"], + ["NumberInfinity", Number.POSITIVE_INFINITY] + ], + checks: [] + }), + { + anyOf: [ + { type: "string", enum: ["NaN"], title: "StringNaN" }, + { type: "string", enum: ["NaN"], title: "NumberNaN" }, + { type: "string", enum: ["Infinity"], title: "StringInfinity" }, + { type: "string", enum: ["Infinity"], title: "NumberInfinity" } + ] + } + ) + }) + + it("compiles a TemplateLiteral", () => { + assert.deepStrictEqual( + compile({ + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "prefix-", checks: [] }, + StringRepresentation + ], + checks: [] + }), + { type: "string", pattern: "^prefix-[\\s\\S]*?$" } + ) + }) + }) + + describe("arrays and objects", () => { + it("compiles optional tuple elements", () => { + assert.deepStrictEqual( + compile({ + _tag: "Arrays", + elements: [ + { type: StringRepresentation, isOptional: false }, + { type: NumberRepresentation, isOptional: true } + ], + rest: [], + checks: [] + }), + { + type: "array", + prefixItems: [{ type: "string" }, { + anyOf: [ + { type: "number" }, + { type: "string", enum: ["NaN"] }, + { type: "string", enum: ["Infinity"] }, + { type: "string", enum: ["-Infinity"] } + ] + }], + maxItems: 2, + minItems: 1 + } + ) + }) + + it("rejects multiple tuple rest elements", () => { + expectError( + () => + compile({ + _tag: "Arrays", + elements: [], + rest: [StringRepresentation, NumberRepresentation], + checks: [] + }), + `Invalid schema representation document\n at ["representation"]["rest"]` + ) + }) + + it("rejects a symbol object property", () => { + expectError( + () => + compile({ + _tag: "Objects", + propertySignatures: [{ + name: Symbol.for("value"), + type: StringRepresentation, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + }), + `Invalid schema representation document\n at ["representation"]["propertySignatures"][0]["name"]` + ) + }) + + it("compiles a Never index-signature value as false", () => { + assert.deepStrictEqual( + compile({ + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ parameter: StringRepresentation, type: { _tag: "Never", checks: [] } }], + checks: [] + }), + { type: "object", additionalProperties: false } + ) + }) + + it("removes an unconstrained index-signature value", () => { + assert.deepStrictEqual( + compile({ + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ parameter: StringRepresentation, type: { _tag: "Unknown", checks: [] } }], + checks: [] + }), + { type: "object" } + ) + }) + }) + + describe("unions", () => { + it("compacts a union of same-type literals", () => { + assert.deepStrictEqual( + compile({ + _tag: "Union", + types: [ + { _tag: "Literal", literal: "a", checks: [] }, + { _tag: "Literal", literal: "b", checks: [] } + ], + mode: "anyOf", + checks: [] + }), + { type: "string", enum: ["a", "b"] } + ) + }) + + it("does not compact a union of mixed-type literals", () => { + assert.deepStrictEqual( + compile({ + _tag: "Union", + types: [ + { _tag: "Literal", literal: "a", checks: [] }, + { _tag: "Literal", literal: 1, checks: [] } + ], + mode: "anyOf", + checks: [] + }), + { + anyOf: [{ type: "string", enum: ["a"] }, { type: "number", enum: [1] }] + } + ) + }) + }) + + describe("annotations and checks", () => { + it("emits every supported standard annotation", () => { + assert.deepStrictEqual( + compile({ + _tag: "String", + annotations: { + title: "Title", + description: "Description", + default: "default", + examples: ["a", "b"], + readOnly: true, + writeOnly: false + }, + checks: [] + }), + { + type: "string", + title: "Title", + description: "Description", + default: "default", + examples: ["a", "b"], + readOnly: true, + writeOnly: false + } + ) + }) + + it("uses a check fragment when the base JSON Schema is empty", () => { + assert.deepStrictEqual( + compile({ + _tag: "Unknown", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => ({ description: "checked" }) } + }] + }), + { description: "checked" } + ) + }) + + it("appends a check to an existing allOf", () => { + assert.deepStrictEqual( + compile({ + _tag: "BigInt", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => ({ description: "checked" }) } + }] + }), + { + type: "string", + allOf: [{ pattern: "^-?\\d+$" }, { description: "checked" }] + } + ) + }) + + it("lets a check refine number to integer", () => { + assert.deepStrictEqual( + compile({ + _tag: "Number", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => ({ type: "integer" }) } + }] + }), + { type: "integer" } + ) + }) + + it("compiles a callback-free FilterGroup without ordinary annotations", () => { + assert.deepStrictEqual( + compile({ + _tag: "String", + checks: [{ + _tag: "FilterGroup", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => ({ minLength: 1 }) } + }] + }] + }), + { type: "string", allOf: [{ minLength: 1 }] } + ) + }) + }) + + describe("collection and union edge cases", () => { + it("compiles tuple element annotations", () => { + assert.deepStrictEqual( + compile({ + _tag: "Arrays", + elements: [{ + type: StringRepresentation, + isOptional: false, + annotations: { description: "element" } + }], + rest: [], + checks: [] + }), + { + type: "array", + prefixItems: [{ type: "string", allOf: [{ description: "element" }] }], + maxItems: 1, + minItems: 1 + } + ) + }) + + it("omits minItems when every tuple element is optional", () => { + assert.deepStrictEqual( + compile({ + _tag: "Arrays", + elements: [{ type: StringRepresentation, isOptional: true }], + rest: [], + checks: [] + }), + { + type: "array", + prefixItems: [{ type: "string" }], + maxItems: 1 + } + ) + }) + + it("compiles a constrained tuple rest", () => { + assert.deepStrictEqual( + compile({ + _tag: "Arrays", + elements: [], + rest: [StringRepresentation], + checks: [] + }), + { type: "array", items: { type: "string" } } + ) + }) + + it("compiles property annotations", () => { + assert.deepStrictEqual( + compile({ + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: StringRepresentation, + isOptional: true, + isMutable: false, + annotations: { description: "property" } + }], + indexSignatures: [], + checks: [] + }), + { + type: "object", + properties: { value: { type: "string", allOf: [{ description: "property" }] } }, + additionalProperties: false + } + ) + }) + + it("compiles a single-member Union without compacting it", () => { + assert.deepStrictEqual( + compile({ + _tag: "Union", + types: [StringRepresentation], + mode: "anyOf", + checks: [] + }), + { anyOf: [{ type: "string" }] } + ) + }) + + it("rejects an unsupported index-signature parameter", () => { + expectError( + () => + compile({ + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ parameter: NumberRepresentation, type: StringRepresentation }], + checks: [] + }), + `Invalid schema representation document\n at ["representation"]["indexSignatures"][0]["parameter"]` + ) + }) + + it("compiles an empty union as Never", () => { + assert.deepStrictEqual( + SchemaRepresentation.toJsonSchemaDocument({ + representation: EmptyUnionRepresentation, + references: {} + }).schema, + { not: {} } + ) + }) + + it("removes items when an empty tuple has an open rest", () => { + assert.deepStrictEqual( + SchemaRepresentation.toJsonSchemaDocument({ + representation: { + _tag: "Arrays", + elements: [], + rest: [{ _tag: "Unknown", checks: [] }], + checks: [] + }, + references: {} + }).schema, + { type: "array" } + ) + }) + }) + + describe("checks and callbacks", () => { + it("passes the type produced by isInt to following check callbacks", () => { + let receivedType: unknown + const dependent = Schema.makeFilter(() => true, { + toJsonSchema: ({ type }) => { + receivedType = type + return { minimum: 0 } + } + }) + const document = SchemaRepresentation.toRepresentation( + Schema.Number.check(Schema.isInt(), dependent).ast + ) + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaDocument(document).schema, { + type: "integer", + allOf: [{ minimum: 0 }] + }) + assert.strictEqual(receivedType, "integer") + }) + + it("compiles the isPattern vertical slice", () => { + const pattern = SchemaRepresentation.toJsonSchemaDocument( + SchemaRepresentation.toRepresentation(Schema.String.check(Schema.isPattern(/^[a-z]+$/i)).ast) + ) + assert.deepStrictEqual(pattern, { + dialect: "draft-2020-12", + schema: { + type: "string", + allOf: [{ pattern: "^[a-z]+$" }] + }, + definitions: {} + }) + }) + + it("treats an empty override as authoritative and ignores a leaf without a callback", () => { + let visits = 0 + const document: SchemaRepresentation.Document = { + representation: { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + annotations: { toJsonSchema: () => ({}) }, + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + description: "ignored", + toJsonSchema: () => { + visits++ + return { minLength: 1 } + } + } + }] + }, { + _tag: "Filter", + aborted: false, + annotations: { description: "no callback" } + }] + }, + references: {} + } + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaDocument(document).schema, { type: "string" }) + assert.strictEqual(visits, 0) + }) + + it("compiles representation.schemas before invoking a callback", () => { + const document: SchemaRepresentation.Document = { + representation: { + _tag: "Objects", + propertySignatures: [], + indexSignatures: [], + checks: [{ + _tag: "Filter", + aborted: false, + representation: { + id: "acme/schema/propertyNames", + payload: null, + schemas: [StringRepresentation] + }, + annotations: { + toJsonSchema: ({ schemas }: SchemaRepresentation.ToJsonSchema.CheckInput) => ({ + propertyNames: schemas[0] + }) + } + }] + }, + references: {} + } + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaDocument(document).schema, { + anyOf: [{ type: "object" }, { type: "array" }], + allOf: [{ propertyNames: { type: "string" } }] + }) + }) + }) + + describe("declarations and pattern extraction", () => { + it("approximates declarations", () => { + const document: SchemaRepresentation.Document = { + representation: { + _tag: "Declaration", + typeParameters: [StringRepresentation], + checks: [], + representation: { + id: "acme/schema/Box", + payload: null + } + }, + references: {} + } + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaDocument(document).schema, {}) + }) + + it("compiles every member of a union index-signature parameter", () => { + const template = SchemaRepresentation.toRepresentation( + Schema.TemplateLiteral(["a", Schema.String]).ast + ).representation + const pattern = SchemaRepresentation.toRepresentation( + Schema.String.check(Schema.isPattern(/^b/)).ast + ).representation + const document: SchemaRepresentation.Document = { + representation: { + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ + parameter: { + _tag: "Union", + types: [template, pattern], + mode: "anyOf", + checks: [] + }, + type: StringRepresentation + }], + checks: [] + }, + references: {} + } + + const schema = SchemaRepresentation.toJsonSchemaDocument(document).schema + assert.deepStrictEqual(Object.keys(schema.patternProperties ?? {}), ["^a[\\s\\S]*?$", "^b"]) + }) + + it("ignores boolean members while collecting index-signature patterns", () => { + const document: SchemaRepresentation.Document = { + representation: { + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ + parameter: { + _tag: "String", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + toJsonSchema: () => ({ allOf: [false, { pattern: "^a" }] }) + } + }] + }, + type: StringRepresentation + }], + checks: [] + }, + references: {} + } + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaDocument(document).schema, { + type: "object", + patternProperties: { "^a": { type: "string" } } + }) + }) + + it("compiles all supported template-literal parts and rejects other nodes", () => { + const representation: SchemaRepresentation.Representation = { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "p", checks: [] }, + NumberRepresentation, + { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "x", checks: [] }, + StringRepresentation + ], + checks: [] + }, + { + _tag: "Union", + types: [ + { _tag: "Literal", literal: "a", checks: [] }, + { _tag: "Literal", literal: "b", checks: [] } + ], + mode: "anyOf", + checks: [] + } + ], + checks: [] + } + + assert.deepStrictEqual( + SchemaRepresentation.toJsonSchemaDocument({ representation, references: {} }).schema, + { + type: "string", + pattern: `^p${SchemaAST.FINITE_PATTERN}x${SchemaAST.STRING_PATTERN}a|b$` + } + ) + + expectError( + () => + SchemaRepresentation.toJsonSchemaDocument({ + representation: { + _tag: "TemplateLiteral", + parts: [{ _tag: "Boolean", checks: [] }], + checks: [] + }, + references: {} + }), + "Invalid schema representation document" + ) + }) + }) + + describe("normalization", () => { + it("extracts nested number types without losing other allOf members", () => { + const output = SchemaRepresentation.toJsonSchemaDocument({ + representation: { + _tag: "Number", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + toJsonSchema: () => ({ + allOf: [ + { + description: "nested", + allOf: [{ type: "number" }, { minimum: 1 }] + }, + { type: "integer", maximum: 10 }, + { title: "kept" } + ] + }) + } + }] + }, + references: {} + }) + + assert.deepStrictEqual(output.schema, { + type: "integer", + allOf: [ + { description: "nested", allOf: [{ minimum: 1 }] }, + { maximum: 10 }, + { title: "kept" } + ] + }) + + assert.deepStrictEqual( + SchemaRepresentation.toJsonSchemaDocument({ + representation: { + _tag: "Number", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + toJsonSchema: () => ({ allOf: [{ type: "number" }, { type: "number" }] }) + } + }] + }, + references: {} + }).schema, + { type: "number" } + ) + }) + }) + + describe("safety and errors", () => { + it("never exposes compiler capabilities as extension annotations", () => { + const document: SchemaRepresentation.Document = { + representation: { + _tag: "String", + annotations: { + description: "text", + identifier: "id", + representation: { id: "acme/schema/String", payload: null }, + toCode: () => ({ runtime: "ignored" }), + toJsonSchema: () => ({ title: "ignored" }), + "x-custom": { enabled: true }, + "x-invalid": () => "ignored" + }, + checks: [] + }, + references: {} + } + + assert.deepStrictEqual( + SchemaRepresentation.toJsonSchemaDocument(document, { + includeAnnotationKey: () => true + }).schema, + { + type: "string", + description: "text", + identifier: "id", + "x-custom": { enabled: true } + } + ) + }) + + it("captures exceptions from JSON Schema callbacks", () => { + const cause = new Error("json schema callback") + const document: SchemaRepresentation.Document = { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { + toJsonSchema: () => { + throw cause + } + } + }] + }, + references: {} + } + expectError( + () => SchemaRepresentation.toJsonSchemaDocument(document), + cause + ) + }) + + it("supports __proto__ as a reference", () => { + const document = SchemaRepresentation.toJsonSchemaDocument( + SchemaRepresentation.toRepresentation( + Schema.String.annotate({ identifier: "__proto__" }).ast + ) + ) + + assert.deepStrictEqual(document.schema, { + $ref: "#/$defs/__proto__" + }) + assert.deepStrictEqual(Object.keys(document.definitions), ["__proto__"]) + assert.deepStrictEqual(document.definitions["__proto__"], { + type: "string" + }) + }) + + it("reports missing references with their document path", () => { + const document: SchemaRepresentation.Document = { + representation: { _tag: "Reference", $ref: "Missing" }, + references: {} + } + expectError( + () => SchemaRepresentation.toJsonSchemaDocument(document), + `Invalid reference Missing\n at ["representation"]["$ref"]` + ) + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toJsonSchemaMultiDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toJsonSchemaMultiDocument.test.ts index 2e12cb328..cf279e58d 100644 --- a/.context/effect/packages/effect/test/schema/representation/toJsonSchemaMultiDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/representation/toJsonSchemaMultiDocument.test.ts @@ -1,39 +1,572 @@ -import { Schema, SchemaRepresentation } from "effect" -import { describe, it } from "vitest" -import { deepStrictEqual } from "../../utils/assert.ts" - -describe("toJsonSchemaMultiDocument", () => { - it("should handle multiple schemas", () => { - const A = Schema.String.annotate({ identifier: "id", description: "a" }) - const B = Schema.String.annotate({ identifier: "id", description: "b" }) - const C = Schema.Tuple([A, B]) - const multiDocument = SchemaRepresentation.fromASTs([A.ast, B.ast, C.ast]) - const jsonMultiDocument = SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument) - deepStrictEqual(jsonMultiDocument, { - dialect: "draft-2020-12", - schemas: [ - { "$ref": "#/$defs/id" }, - { "$ref": "#/$defs/id1" }, - { - "type": "array", - "prefixItems": [ - { "$ref": "#/$defs/id" }, - { "$ref": "#/$defs/id1" } - ], - "minItems": 2, - "maxItems": 2 +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaRepresentation, SchemaTransformation } from "effect" +import { throws } from "../../utils/assert.ts" + +function expectError(thunk: () => void, expected: string | Error): void { + if (typeof expected === "string") { + throws(thunk, expected) + } else { + throws(thunk, (error: unknown) => { + assert.strictEqual(error, expected) + return undefined + }) + } +} + +const StringRepresentation: SchemaRepresentation.Representation = { + _tag: "String", + checks: [] +} + +const stringIdentityTransformation = SchemaTransformation.transform({ + decode: (value: string) => value, + encode: (value: string) => value +}) + +describe("SchemaRepresentation.toJsonSchemaMultiDocument", () => { + describe("definition canonicalization", () => { + it("deduplicates equivalent fallback definitions", () => { + const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" }) + const first = Schema.toCodecJson(Schema.fromJsonString(Content)) + const second = Schema.toCodecJson(Schema.fromJsonString(Content)) + const document = SchemaRepresentation.toRepresentations([first.ast, second.ast]) + + assert.deepStrictEqual(SchemaRepresentation.toJsonSchemaMultiDocument(document), { + dialect: "draft-2020-12", + schemas: [ + { $ref: "#/$defs/Tool.ContentEncoded" }, + { $ref: "#/$defs/Tool.ContentEncoded" } + ], + definitions: { + "Tool.ContentEncoded": { + type: "string", + contentMediaType: "application/json" + } } - ], - definitions: { - id: { - "type": "string", - "description": "a" + }) + }) + + it("does not deduplicate different fallback definitions", () => { + const Content = Schema.String.annotate({ identifier: "Fallback" }) + const string = Schema.toCodecJson( + Schema.String.pipe(Schema.decodeTo(Content, stringIdentityTransformation)) + ) + const boolean = Schema.toCodecJson( + Schema.Boolean.pipe( + Schema.decodeTo( + Content, + SchemaTransformation.transform({ + decode: (value) => String(value), + encode: (value) => value === "true" + }) + ) + ) + ) + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([string.ast, boolean.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/FallbackEncoded" }, + { $ref: "#/$defs/FallbackEncoded_1" } + ]) + assert.deepStrictEqual(output.definitions, { + FallbackEncoded: { type: "string" }, + FallbackEncoded_1: { type: "boolean" } + }) + }) + + it("does not deduplicate explicit identifiers", () => { + const ExplicitA = Schema.String.annotate({ identifier: "ExplicitA" }) + const ExplicitB = Schema.String.annotate({ identifier: "ExplicitB" }) + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([ExplicitA.ast, ExplicitB.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/ExplicitA" }, + { $ref: "#/$defs/ExplicitB" } + ]) + assert.deepStrictEqual(output.definitions, { + ExplicitA: { type: "string" }, + ExplicitB: { type: "string" } + }) + }) + + it("deduplicates fallback definitions after linking their dependencies", () => { + const Child = Schema.String.annotate({ identifier: "Child" }) + const Parent = Schema.Struct({ child: Child }).annotate({ identifier: "Parent" }) + const make = () => { + const encoded = Schema.Struct({ + child: Schema.fromJsonString(Child) + }) + return Schema.toCodecJson( + encoded.pipe( + Schema.decodeTo( + Parent, + SchemaTransformation.transform({ + decode: ({ child }) => ({ child }), + encode: ({ child }) => ({ child }) + }) + ) + ) + ) + } + const first = make() + const second = make() + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([first.ast, second.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/ParentEncoded" }, + { $ref: "#/$defs/ParentEncoded" } + ]) + assert.deepStrictEqual(output.definitions, { + ChildEncoded: { + type: "string", + contentMediaType: "application/json" }, - id1: { - "type": "string", - "description": "b" + ParentEncoded: { + type: "object", + properties: { + child: { $ref: "#/$defs/ChildEncoded" } + }, + required: ["child"], + additionalProperties: false + } + }) + }) + + it("escapes canonical fallback references", () => { + const Content = Schema.String.annotate({ identifier: "Child/~" }) + const first = Schema.toCodecJson(Schema.fromJsonString(Content)) + const second = Schema.toCodecJson(Schema.fromJsonString(Content)) + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([first.ast, second.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/Child~1~0Encoded" }, + { $ref: "#/$defs/Child~1~0Encoded" } + ]) + assert.deepStrictEqual(output.definitions, { + "Child/~Encoded": { + type: "string", + contentMediaType: "application/json" } + }) + }) + + // JSON Schema callbacks are user-provided and may have effects, so invocation count is observable. + it("invokes each fallback callback once before deduplicating equivalent output", () => { + let visits = 0 + const Content = Schema.String.annotate({ identifier: "Callback" }) + const make = (reverse: boolean) => { + const encoded = Schema.String.check( + Schema.makeFilter(() => true, { + toJsonSchema: () => { + visits++ + return reverse ? { maxLength: 10, minLength: 1 } : { minLength: 1, maxLength: 10 } + } + }) + ) + return Schema.toCodecJson( + encoded.pipe(Schema.decodeTo(Content, stringIdentityTransformation)) + ) } + const first = make(false) + const second = make(true) + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([first.ast, second.ast]) + ) + + assert.strictEqual(visits, 2) + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/CallbackEncoded" }, + { $ref: "#/$defs/CallbackEncoded" } + ]) + assert.deepStrictEqual(output.definitions, { + CallbackEncoded: { + type: "string", + allOf: [{ minLength: 1, maxLength: 10 }] + } + }) + }) + + it("reuses a compiled definition when extracting an index signature pattern", () => { + let visits = 0 + const Key = Schema.String.check( + Schema.makeFilter(() => true, { + identifier: "Key", + toJsonSchema: () => { + visits++ + return { pattern: "^key$" } + } + }) + ) + const Root = Schema.Record(Key, Schema.String) + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([Root.ast]) + ) + + assert.strictEqual(visits, 1) + assert.deepStrictEqual(output.schemas, [{ + type: "object", + patternProperties: { + "^key$": { type: "string" } + } + }]) + }) + + it("deduplicates fallback definitions that share a recursive dependency", () => { + interface OptionalNode { + readonly next?: OptionalNode | undefined + } + const Encoded: Schema.Codec = Schema.suspend(() => + Schema.Struct({ + next: Schema.optional(Encoded) + }) + ) + const Content = Encoded.annotate({ identifier: "Node" }) + const make = () => + Schema.toCodecJson( + Encoded.pipe( + Schema.decodeTo( + Content, + SchemaTransformation.transform({ + decode: (node) => node, + encode: (node) => node + }) + ) + ) + ) + const first = make() + const second = make() + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([first.ast, second.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/NodeEncoded" }, + { $ref: "#/$defs/NodeEncoded" } + ]) + assert.deepStrictEqual(output.definitions, { + Objects_: { + type: "object", + properties: { + next: { + anyOf: [ + { $ref: "#/$defs/Objects_" }, + { type: "null" } + ] + } + }, + additionalProperties: false + }, + NodeEncoded: { $ref: "#/$defs/Objects_" } + }) + }) + + it("does not deduplicate fallback definitions with distinct recursive dependencies", () => { + interface RequiredNode { + readonly next: RequiredNode + } + const Content: Schema.Codec = Schema.Struct({ + next: Schema.suspend(() => Content) + }).annotate({ identifier: "Node" }) + const make = () => { + const Encoded: Schema.Codec = Schema.Struct({ + next: Schema.suspend(() => Encoded) + }) + return Schema.toCodecJson( + Encoded.pipe( + Schema.decodeTo( + Content, + SchemaTransformation.transform({ + decode: (node) => node, + encode: (node) => node + }) + ) + ) + ) + } + const first = make() + const second = make() + const output = SchemaRepresentation.toJsonSchemaMultiDocument( + SchemaRepresentation.toRepresentations([first.ast, second.ast]) + ) + + assert.deepStrictEqual(output.schemas, [ + { $ref: "#/$defs/NodeEncoded" }, + { $ref: "#/$defs/NodeEncoded_1" } + ]) + assert.deepStrictEqual(output.definitions, { + Suspend_: { + type: "object", + properties: { + next: { $ref: "#/$defs/Suspend_" } + }, + required: ["next"], + additionalProperties: false + }, + NodeEncoded: { + type: "object", + properties: { + next: { $ref: "#/$defs/Suspend_" } + }, + required: ["next"], + additionalProperties: false + }, + Suspend_1: { + type: "object", + properties: { + next: { $ref: "#/$defs/Suspend_1" } + }, + required: ["next"], + additionalProperties: false + }, + NodeEncoded_1: { + type: "object", + properties: { + next: { $ref: "#/$defs/Suspend_1" } + }, + required: ["next"], + additionalProperties: false + } + }) + }) + }) + + describe("multi-root compilation", () => { + it("preserves root order and shared named definitions", () => { + const A = Schema.String.annotate({ identifier: "A", description: "a" }) + const B = Schema.String.annotate({ identifier: "B", description: "b" }) + const C = Schema.Tuple([A, B]) + const multiDocument = SchemaRepresentation.toRepresentations([A.ast, B.ast, C.ast]) + const jsonMultiDocument = SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument) + assert.deepStrictEqual(jsonMultiDocument, { + dialect: "draft-2020-12", + schemas: [ + { "$ref": "#/$defs/A" }, + { "$ref": "#/$defs/B" }, + { + "type": "array", + "prefixItems": [ + { "$ref": "#/$defs/A" }, + { "$ref": "#/$defs/B" } + ], + "minItems": 2, + "maxItems": 2 + } + ], + definitions: { + A: { + "type": "string", + "description": "a" + }, + B: { + "type": "string", + "description": "b" + } + } + }) + }) + it("emits standard annotations and oneOf unions", () => { + const output = SchemaRepresentation.toJsonSchemaMultiDocument({ + representations: [ + { + _tag: "String", + annotations: { + format: "email", + contentEncoding: "base64", + contentMediaType: "application/json", + contentSchema: { type: "string" } + }, + checks: [] + }, + { + _tag: "Union", + types: [StringRepresentation, { _tag: "Boolean", checks: [] }], + mode: "oneOf", + checks: [] + } + ], + references: {} + }) + + assert.deepStrictEqual(output.schemas, [ + { + type: "string", + format: "email", + contentEncoding: "base64", + contentMediaType: "application/json", + contentSchema: { type: "string" } + }, + { oneOf: [{ type: "string" }, { type: "boolean" }] } + ]) + }) + + it("uses group overrides without visiting children and otherwise falls back to allOf", () => { + let visits = 0 + const child: SchemaRepresentation.Filter = { + _tag: "Filter", + aborted: false, + annotations: { + toJsonSchema: () => { + visits++ + return { minLength: 1 } + } + } + } + const override: SchemaRepresentation.FilterGroup = { + _tag: "FilterGroup", + checks: [child], + annotations: { + description: "override", + toJsonSchema: () => ({ format: "custom" }) + } + } + const fallback: SchemaRepresentation.FilterGroup = { + _tag: "FilterGroup", + checks: [child, { _tag: "Filter", aborted: false }], + annotations: { description: "fallback" } + } + const document: SchemaRepresentation.MultiDocument = { + representations: [ + { _tag: "String", checks: [override] }, + { _tag: "String", checks: [fallback] } + ], + references: {} + } + + const output = SchemaRepresentation.toJsonSchemaMultiDocument(document) + assert.strictEqual(visits, 1) + assert.deepStrictEqual(output.schemas, [ + { + type: "string", + allOf: [{ format: "custom", description: "override" }] + }, + { + type: "string", + allOf: [{ allOf: [{ minLength: 1 }], description: "fallback" }] + } + ]) + }) + + it("preserves unknown references returned by callbacks", () => { + const reference = { $ref: "#/$defs/Unknown" } + const output = SchemaRepresentation.toJsonSchemaMultiDocument({ + representations: [{ + _tag: "String", + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => reference } + }] + }], + references: {} + }) + + assert.deepStrictEqual(output.schemas, [{ + type: "string", + allOf: [reference] + }]) + }) + + it("rewrites callback references without mutating callback output", () => { + const defaultValue = Object.freeze({ $ref: "#/$defs//properties/value" }) + const reference = Object.freeze({ $ref: "#/$defs//properties/value", default: defaultValue }) + const definition = (): SchemaRepresentation.Representation => ({ + _tag: "String", + annotations: { "~identifier": "Value" }, + checks: [] + }) + const output = SchemaRepresentation.toJsonSchemaMultiDocument({ + representations: [{ + _tag: "String", + annotations: { default: -0 }, + checks: [{ + _tag: "Filter", + aborted: false, + annotations: { toJsonSchema: () => reference } + }] + }], + references: { A: definition(), "": definition() } + }) + + assert.deepStrictEqual(reference, { $ref: "#/$defs//properties/value", default: defaultValue }) + assert.strictEqual(Object.is(output.schemas[0].default, -0), true) + assert.deepStrictEqual(output, { + dialect: "draft-2020-12", + schemas: [{ + type: "string", + default: -0, + allOf: [{ $ref: "#/$defs/A/properties/value", default: { $ref: "#/$defs//properties/value" } }] + }], + definitions: { A: { type: "string" } } + }) + }) + + it("preserves unknown references in annotation values", () => { + const reference = { $ref: "#/$defs/Unknown" } + const output = SchemaRepresentation.toJsonSchemaMultiDocument({ + representations: [{ + _tag: "String", + annotations: { default: reference }, + checks: [] + }], + references: {} + }) + + assert.deepStrictEqual(output.schemas, [{ + type: "string", + default: reference + }]) + }) + + it("resolves referenced index-signature parameters and stops cycles", () => { + const record = ( + parameter: SchemaRepresentation.Representation + ): SchemaRepresentation.Representation => ({ + _tag: "Objects", + propertySignatures: [], + indexSignatures: [{ parameter, type: StringRepresentation }], + checks: [] + }) + const pattern = SchemaRepresentation.toRepresentation( + Schema.String.check(Schema.isPattern(/^a/)).ast + ).representation + const output = SchemaRepresentation.toJsonSchemaMultiDocument({ + representations: [ + record({ _tag: "Reference", $ref: "Pattern" }), + record({ _tag: "Reference", $ref: "Cycle" }) + ], + references: { + Pattern: pattern, + Cycle: { _tag: "Reference", $ref: "Cycle" } + } + }) + + assert.deepStrictEqual(output.schemas, [ + { + type: "object", + patternProperties: { "^a": { type: "string" } } + }, + { + type: "object", + additionalProperties: { type: "string" } + } + ]) + + expectError( + () => + SchemaRepresentation.toJsonSchemaDocument({ + representation: record({ _tag: "Reference", $ref: "Missing" }), + references: {} + }), + `Invalid reference Missing\n at ["representation"]["indexSignatures"][0]["parameter"]["$ref"]` + ) }) }) }) diff --git a/.context/effect/packages/effect/test/schema/representation/toMultiDocument.test.ts b/.context/effect/packages/effect/test/schema/representation/toMultiDocument.test.ts new file mode 100644 index 000000000..d3b1c388e --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toMultiDocument.test.ts @@ -0,0 +1,18 @@ +import { assert, describe, it } from "@effect/vitest" +import { SchemaRepresentation } from "effect" + +describe("SchemaRepresentation.toMultiDocument", () => { + it("wraps the root representation and preserves references", () => { + const reference = { _tag: "String" as const, checks: [] } + assert.deepStrictEqual( + SchemaRepresentation.toMultiDocument({ + representation: { _tag: "Reference", $ref: "Value" }, + references: { Value: reference } + }), + { + representations: [{ _tag: "Reference", $ref: "Value" }], + references: { Value: reference } + } + ) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toRepresentation.test.ts b/.context/effect/packages/effect/test/schema/representation/toRepresentation.test.ts new file mode 100644 index 000000000..4c8ae33a1 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toRepresentation.test.ts @@ -0,0 +1,936 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaAST, SchemaRepresentation } from "effect" + +describe("SchemaRepresentation.toRepresentation", () => { + describe("node conversion", () => { + const keywords = [ + ["Null", Schema.Null], + ["Undefined", Schema.Undefined], + ["Void", Schema.Void], + ["Never", Schema.Never], + ["Unknown", Schema.Unknown], + ["Any", Schema.Any], + ["String", Schema.String], + ["Number", Schema.Number], + ["Boolean", Schema.Boolean], + ["BigInt", Schema.BigInt], + ["Symbol", Schema.Symbol], + ["ObjectKeyword", Schema.ObjectKeyword] + ] as const + + for (const [tag, schema] of keywords) { + it(tag, () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: tag, checks: [] }, + references: {} + }) + }) + } + + it("literal", () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.Literal("value").ast), { + representation: { _tag: "Literal", literal: "value", checks: [] }, + references: {} + }) + }) + + it("global unique symbol", () => { + const symbol = Symbol.for("value") + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.UniqueSymbol(symbol).ast), { + representation: { _tag: "UniqueSymbol", symbol, checks: [] }, + references: {} + }) + }) + + it("enum", () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.Enum({ A: "a", One: 1 }).ast), { + representation: { + _tag: "Enum", + enums: [ + ["A", "a"], + ["One", 1] + ], + checks: [] + }, + references: {} + }) + }) + + it("template literal", () => { + assert.deepStrictEqual( + SchemaRepresentation.toRepresentation(Schema.TemplateLiteral(["prefix-", Schema.String, Schema.Number]).ast), + { + representation: { + _tag: "TemplateLiteral", + parts: [ + { _tag: "Literal", literal: "prefix-", checks: [] }, + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] } + ], + checks: [] + }, + references: {} + } + ) + }) + + it("tuple elements and rest", () => { + const schema = Schema.TupleWithRest( + Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]), + [Schema.Boolean] + ) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { + _tag: "Arrays", + elements: [ + { type: { _tag: "String", checks: [] }, isOptional: false }, + { type: { _tag: "Number", checks: [] }, isOptional: true } + ], + rest: [{ _tag: "Boolean", checks: [] }], + checks: [] + }, + references: {} + }) + }) + + it("object properties and index signatures", () => { + const schema = Schema.StructWithRest( + Schema.Struct({ + required: Schema.String, + optional: Schema.optionalKey(Schema.Number), + mutable: Schema.mutableKey(Schema.Boolean) + }), + [Schema.Record(Schema.Symbol, Schema.BigInt)] + ) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { + _tag: "Objects", + propertySignatures: [ + { + name: "required", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }, + { + name: "optional", + type: { _tag: "Number", checks: [] }, + isOptional: true, + isMutable: false + }, + { + name: "mutable", + type: { _tag: "Boolean", checks: [] }, + isOptional: false, + isMutable: true + } + ], + indexSignatures: [{ + parameter: { _tag: "Symbol", checks: [] }, + type: { _tag: "BigInt", checks: [] } + }], + checks: [] + }, + references: {} + }) + }) + + it("union member order", () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.Union([Schema.String, Schema.BigInt]).ast), { + representation: { + _tag: "Union", + types: [ + { _tag: "String", checks: [] }, + { _tag: "BigInt", checks: [] } + ], + mode: "anyOf", + checks: [] + }, + references: {} + }) + }) + }) + + describe("encoded and type projections", () => { + it("uses the encoded side of a transformation", () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.NumberFromString.ast), { + representation: { + _tag: "String", + annotations: { expected: "a string that will be decoded as a number" }, + checks: [] + }, + references: {} + }) + }) + + it("uses a type-side identifier as a fallback for the encoded representation", () => { + const schema = Schema.NumberFromString.annotate({ identifier: "Finite" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "FiniteEncoded" }, + references: { + FiniteEncoded: { + _tag: "String", + annotations: { + expected: "a string that will be decoded as a number", + "~identifier": "Finite" + }, + checks: [] + } + } + }) + }) + + it("prefers an explicit encoded-side identifier over a type-side identifier", () => { + const schema = Schema.NumberFromString.pipe( + Schema.annotateEncoded({ identifier: "EncodedFinite" }), + Schema.annotate({ identifier: "Finite" }) + ) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "EncodedFinite" }, + references: { + EncodedFinite: { + _tag: "String", + annotations: { + expected: "a string that will be decoded as a number", + identifier: "EncodedFinite" + }, + checks: [] + } + } + }) + }) + + it("overrides an encoded-side fallback with the type-side identifier", () => { + const schema = Schema.NumberFromString.pipe( + Schema.annotateEncoded({ "~identifier": "Previous" }), + Schema.annotate({ identifier: "Finite" }) + ) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "FiniteEncoded" }, + references: { + FiniteEncoded: { + _tag: "String", + annotations: { + expected: "a string that will be decoded as a number", + "~identifier": "Finite" + }, + checks: [] + } + } + }) + }) + + it("uses the type side when the caller projects it", () => { + assert.deepStrictEqual( + SchemaRepresentation.toRepresentation(SchemaAST.toType(Schema.NumberFromString.ast)), + { + representation: { _tag: "Number", checks: [] }, + references: {} + } + ) + }) + }) + + describe("schema annotations and declarations", () => { + it("preserves brands", () => { + assert.deepStrictEqual( + SchemaRepresentation.toRepresentation(Schema.String.pipe(Schema.brand("A"), Schema.brand("B")).ast), + { + representation: { + _tag: "String", + annotations: { brands: ["A", "B"] }, + checks: [] + }, + references: {} + } + ) + }) + + it("preserves declaration code callbacks", () => { + const toCode: SchemaRepresentation.Generation.Declaration = () => ({ runtime: "Custom", Type: "string" }) + const schema = Schema.declare((input): input is string => typeof input === "string", { + representation: { + id: "acme/schema/Custom", + payload: null + }, + toCode + }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { + _tag: "Declaration", + typeParameters: [], + checks: [], + representation: { + id: "acme/schema/Custom", + payload: null + }, + annotations: { + toCode + } + }, + references: {} + }) + }) + + it("converts declaration type parameters", () => { + const representation = SchemaRepresentation.toRepresentation(Schema.Option(Schema.Number).ast).representation + + assert.strictEqual(representation._tag, "Declaration") + if (representation._tag !== "Declaration") return + assert.deepStrictEqual(representation.typeParameters, [{ _tag: "Number", checks: [] }]) + }) + }) + + describe("checks", () => { + it("preserves custom filter callbacks, dependencies and aborted state", () => { + const toCode: SchemaRepresentation.Generation.Check = () => ({ runtime: "Custom" }) + const toJsonSchema: SchemaRepresentation.ToJsonSchema.Check = () => ({ minLength: 1 }) + const marker = () => "marker" + const filter = Schema.makeFilter(() => true, { + representation: { + id: "acme/schema/Custom", + payload: { minimum: 1 }, + schemas: [Schema.Number.ast] + }, + toCode, + toJsonSchema, + marker + }).abort() + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.String.check(filter).ast), { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "acme/schema/Custom", + payload: { minimum: 1 }, + schemas: [{ _tag: "Number", checks: [] }] + }, + annotations: { + toCode, + toJsonSchema, + marker + }, + aborted: true + }] + }, + references: {} + }) + }) + + it("preserves filters without persistence metadata", () => { + const filter = Schema.makeFilter(() => true, { expected: "custom" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.String.check(filter).ast), { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + annotations: { expected: "custom" }, + aborted: false + }] + }, + references: {} + }) + }) + + it("preserves a filter without annotations", () => { + const filter = Schema.makeFilter(() => true) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.String.check(filter).ast), { + representation: { + _tag: "String", + checks: [{ _tag: "Filter", aborted: false }] + }, + references: {} + }) + }) + + it("preserves filter groups", () => { + const first = Schema.makeFilter(() => true, { expected: "first" }) + const second = Schema.makeFilter(() => true, { expected: "second" }) + const group = Schema.makeFilterGroup([first, second], { description: "group" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.String.check(group).ast), { + representation: { + _tag: "String", + checks: [{ + _tag: "FilterGroup", + annotations: { description: "group" }, + checks: [ + { _tag: "Filter", annotations: { expected: "first" }, aborted: false }, + { _tag: "Filter", annotations: { expected: "second" }, aborted: false } + ] + }] + }, + references: {} + }) + }) + + it("converts representation dependencies of built-in filters", () => { + const schema = Schema.Record(Schema.String, Schema.Number).check( + Schema.isPropertyNames(Schema.String.check(Schema.isPattern(/^[A-Z]/))) + ) + const representation = SchemaRepresentation.toRepresentation(schema.ast).representation + + assert.strictEqual(representation._tag, "Objects") + if (representation._tag !== "Objects") return + const check = representation.checks[0] + assert.strictEqual(check._tag, "Filter") + if (check._tag !== "Filter") return + const dependency = check.representation?.schemas?.[0] + assert.isDefined(dependency) + assert.deepStrictEqual( + SchemaRepresentation.toJson({ representation: dependency, references: {} }), + { + representation: { + _tag: "String", + checks: [{ + _tag: "Filter", + representation: { + id: "effect/schema/isPattern", + payload: { source: "^[A-Z]", flags: "" } + }, + annotations: { + arbitrary: { constraint: { patterns: ["^[A-Z]"] } }, + expected: "a string matching the RegExp ^[A-Z]" + }, + aborted: false + }] + }, + references: {} + } + ) + }) + + it("extracts shared representation dependencies of filters", () => { + const shared = Schema.Struct({ value: Schema.String }) + const filter = Schema.makeFilter(() => true, { + representation: { + id: "acme/schema/Custom", + payload: null, + schemas: [shared.ast, shared.ast] + } + }) + const document = SchemaRepresentation.toRepresentation(Schema.String.check(filter).ast) + const representation = document.representation + + assert.strictEqual(representation._tag, "String") + if (representation._tag !== "String") return + assert.deepStrictEqual(representation.checks[0].representation?.schemas, [ + { _tag: "Reference", $ref: "Objects_" }, + { _tag: "Reference", $ref: "Objects_" } + ]) + assert.deepStrictEqual(Object.keys(document.references), ["Objects_"]) + }) + + it("converts checks on arrays", () => { + const representation = SchemaRepresentation.toRepresentation( + Schema.Array(Schema.String).check(Schema.isMinLength(1)).ast + ).representation + + assert.strictEqual(representation._tag, "Arrays") + if (representation._tag !== "Arrays") return + assert.strictEqual(representation.checks.length, 1) + assert.deepStrictEqual(representation.checks[0].representation, { + id: "effect/schema/isMinLength", + payload: { minLength: 1 } + }) + }) + + it("converts checks on objects", () => { + const representation = SchemaRepresentation.toRepresentation( + Schema.Record(Schema.String, Schema.Number).check(Schema.isMinProperties(1)).ast + ).representation + + assert.strictEqual(representation._tag, "Objects") + if (representation._tag !== "Objects") return + assert.strictEqual(representation.checks.length, 1) + assert.deepStrictEqual(representation.checks[0].representation, { + id: "effect/schema/isMinProperties", + payload: { minProperties: 1 } + }) + }) + }) + + describe("contextual annotations", () => { + it("preserves fromJsonString annotations", () => { + const schema = SchemaAST.toEncoded(Schema.fromJsonString(Schema.Struct({ value: Schema.Number })).ast) + const document = SchemaRepresentation.toRepresentation(schema) + + assert.deepStrictEqual(document, { + representation: { + _tag: "String", + annotations: { + contentMediaType: "application/json", + expected: "a string that will be decoded as JSON" + }, + checks: [] + }, + references: {} + }) + }) + + it("preserves tuple element annotations", () => { + const marker = () => "element" + const schema = Schema.Tuple([Schema.String.annotateKey({ description: "element", marker })]) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { + _tag: "Arrays", + elements: [{ + type: { _tag: "String", checks: [] }, + isOptional: false, + annotations: { description: "element", marker } + }], + rest: [], + checks: [] + }, + references: {} + }) + }) + + it("preserves property annotations", () => { + const marker = () => "property" + const schema = Schema.Struct({ + value: Schema.String.annotateKey({ description: "property", marker }) + }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false, + annotations: { description: "property", marker } + }], + indexSignatures: [], + checks: [] + }, + references: {} + }) + }) + }) + + describe("shared references", () => { + it("extracts shared Objects, Arrays, and Union schemas into references", () => { + const object = Schema.Struct({ value: Schema.String }) + const array = Schema.Array(Schema.Number) + const union = Schema.Union([Schema.Struct({ value: Schema.String }), Schema.Null]) + const document = SchemaRepresentation.toRepresentation( + Schema.Tuple([object, object, array, array, union, union]).ast + ) + + assert.deepStrictEqual(document, { + representation: { + _tag: "Arrays", + elements: [ + { type: { _tag: "Reference", $ref: "Objects_" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "Objects_" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "Arrays_" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "Arrays_" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "Union_" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "Union_" }, isOptional: false } + ], + rest: [], + checks: [] + }, + references: { + Objects_: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + }, + Arrays_: { + _tag: "Arrays", + elements: [], + rest: [{ _tag: "Number", checks: [] }], + checks: [] + }, + Union_: { + _tag: "Union", + types: [ + { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + }, + { _tag: "Null", checks: [] } + ], + mode: "anyOf", + checks: [] + } + } + }) + }) + + it("does not extract shared unions of leaf schemas", () => { + const union = Schema.Union([Schema.String, Schema.Number]) + const document = SchemaRepresentation.toRepresentation(Schema.Tuple([union, union]).ast) + + assert.deepStrictEqual(document.references, {}) + assert.strictEqual(document.representation._tag, "Arrays") + if (document.representation._tag === "Arrays") { + assert.deepStrictEqual(document.representation.elements.map((element) => element.type._tag), ["Union", "Union"]) + } + }) + + it("extracts leaf schemas only when references are estimated to be cheaper", () => { + const smallUnion = Schema.Union([Schema.String, Schema.Number]) + const smallEnum = Schema.Enum({ A: "a", B: "b" }) + const largeEnum = Schema.Enum({ A: "a", B: "b", C: "c" }) + const smallTemplateLiteral = Schema.TemplateLiteral(["a", Schema.String]) + const largeTemplateLiteral = Schema.TemplateLiteral(["a", Schema.String, "b"]) + const shortLiteral = Schema.Literal("a".repeat(64)) + const longLiteral = Schema.Literal("a".repeat(65)) + + assert.deepStrictEqual( + Object.keys(SchemaRepresentation.toRepresentations([smallUnion.ast, smallUnion.ast]).references), + [] + ) + assert.deepStrictEqual( + Object.keys( + SchemaRepresentation.toRepresentations([smallUnion.ast, smallUnion.ast, smallUnion.ast]).references + ), + ["Union_"] + ) + assert.deepStrictEqual( + Object.keys(SchemaRepresentation.toRepresentations([smallEnum.ast, smallEnum.ast]).references), + [] + ) + assert.deepStrictEqual( + Object.keys(SchemaRepresentation.toRepresentations([largeEnum.ast, largeEnum.ast]).references), + ["Enum_"] + ) + assert.deepStrictEqual( + Object.keys( + SchemaRepresentation.toRepresentations([smallTemplateLiteral.ast, smallTemplateLiteral.ast]).references + ), + [] + ) + assert.deepStrictEqual( + Object.keys( + SchemaRepresentation.toRepresentations([largeTemplateLiteral.ast, largeTemplateLiteral.ast]).references + ), + ["TemplateLiteral_"] + ) + assert.deepStrictEqual( + Object.keys(SchemaRepresentation.toRepresentations([shortLiteral.ast, shortLiteral.ast]).references), + [] + ) + assert.deepStrictEqual( + Object.keys(SchemaRepresentation.toRepresentations([longLiteral.ast, longLiteral.ast]).references), + ["Literal_"] + ) + }) + + it("does not extract structurally equivalent schemas with distinct ASTs", () => { + const first = Schema.Struct({ value: Schema.String }) + const second = Schema.Struct({ value: Schema.String }) + const document = SchemaRepresentation.toRepresentation(Schema.Tuple([first, second]).ast) + + assert.deepStrictEqual(document.references, {}) + }) + + it("does not extract a child solely because its shared parent is reused", () => { + const child = Schema.Struct({ value: Schema.String }) + const parent = Schema.Struct({ child }) + const document = SchemaRepresentation.toRepresentation(Schema.Tuple([parent, parent]).ast) + + assert.deepStrictEqual(document.references, { + Objects_: { + _tag: "Objects", + propertySignatures: [{ + name: "child", + type: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + } + }) + }) + + it("extracts shared Suspend schemas but not shared trivial schemas", () => { + const suspend = Schema.suspend(() => Schema.String) + const document = SchemaRepresentation.toRepresentation( + Schema.Tuple([Schema.String, Schema.String, suspend, suspend]).ast + ) + + assert.deepStrictEqual(document.references, { + Suspend_: { + _tag: "Suspend", + checks: [], + thunk: { _tag: "String", checks: [] } + } + }) + }) + + it("extracts shared Declaration schemas", () => { + const declaration = Schema.declare((input): input is string => typeof input === "string") + const document = SchemaRepresentation.toRepresentation(Schema.Tuple([declaration, declaration]).ast) + + assert.deepStrictEqual(document, { + representation: { + _tag: "Arrays", + elements: [ + { + isOptional: false, + type: { _tag: "Reference", $ref: "Declaration_" } + }, + { + isOptional: false, + type: { _tag: "Reference", $ref: "Declaration_" } + } + ], + rest: [], + checks: [] + }, + references: { + Declaration_: { + _tag: "Declaration", + typeParameters: [], + checks: [] + } + } + }) + }) + }) + + describe("reference allocation and recursion", () => { + it("extracts a named schema into references", () => { + const schema = Schema.String.annotate({ identifier: "Value", description: "value" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "Value" }, + references: { + Value: { + _tag: "String", + annotations: { identifier: "Value", description: "value" }, + checks: [] + } + } + }) + }) + + it("supports __proto__ as an identifier", () => { + const document = SchemaRepresentation.toRepresentation( + Schema.String.annotate({ identifier: "__proto__" }).ast + ) + + assert.deepStrictEqual(document.representation, { _tag: "Reference", $ref: "__proto__" }) + assert.deepStrictEqual(Object.keys(document.references), ["__proto__"]) + assert.strictEqual(Object.getPrototypeOf(document.references), Object.prototype) + assert.isTrue(Object.hasOwn(document.references, "__proto__")) + assert.strictEqual(document.references["__proto__"]._tag, "String") + }) + + it("converts a non-recursive suspend", () => { + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Schema.suspend(() => Schema.String).ast), { + representation: { + _tag: "Suspend", + thunk: { _tag: "String", checks: [] }, + checks: [] + }, + references: {} + }) + }) + + it("uses an outer identifier for a recursive schema", () => { + interface Node { + readonly next?: Node + } + const Node = Schema.Struct({ + next: Schema.optionalKey(Schema.suspend((): Schema.Codec => Node)) + }).annotate({ identifier: "Node" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(Node.ast), { + representation: { _tag: "Reference", $ref: "Node" }, + references: { + Node: { + _tag: "Objects", + annotations: { identifier: "Node" }, + propertySignatures: [{ + name: "next", + type: { + _tag: "Suspend", + thunk: { _tag: "Reference", $ref: "Node" }, + checks: [] + }, + isOptional: true, + isMutable: false + }], + indexSignatures: [], + checks: [] + } + } + }) + }) + + it("suffixes duplicate identifiers on recursive schemas", () => { + interface First { + readonly next?: First + } + const First = Schema.Struct({ + next: Schema.optionalKey(Schema.suspend((): Schema.Codec => First)) + }).annotate({ identifier: "Node" }) + interface Second { + readonly next?: Second + } + const Second = Schema.Struct({ + next: Schema.optionalKey(Schema.suspend((): Schema.Codec => Second)) + }).annotate({ identifier: "Node" }) + + assert.deepStrictEqual( + SchemaRepresentation.toRepresentation(Schema.Tuple([First, Second]).ast), + { + representation: { + _tag: "Arrays", + elements: [ + { isOptional: false, type: { _tag: "Reference", $ref: "Node" } }, + { isOptional: false, type: { _tag: "Reference", $ref: "Node_1" } } + ], + rest: [], + checks: [] + }, + references: { + Node: { + _tag: "Objects", + propertySignatures: [{ + name: "next", + type: { + _tag: "Suspend", + checks: [], + thunk: { _tag: "Reference", $ref: "Node" } + }, + isOptional: true, + isMutable: false + }], + indexSignatures: [], + checks: [], + annotations: { identifier: "Node" } + }, + Node_1: { + _tag: "Objects", + propertySignatures: [{ + name: "next", + type: { + _tag: "Suspend", + checks: [], + thunk: { _tag: "Reference", $ref: "Node_1" } + }, + isOptional: true, + isMutable: false + }], + indexSignatures: [], + checks: [], + annotations: { identifier: "Node_1" } + } + } + } + ) + }) + + it("does not resolve an identifier below a check", () => { + const schema = Schema.String + .annotate({ identifier: "Text" }) + .pipe(Schema.check(Schema.isMinLength(1))) + const document = SchemaRepresentation.toRepresentation(schema.ast) + + assert.strictEqual(document.representation._tag, "String") + assert.deepStrictEqual(document.references, {}) + if (document.representation._tag === "String") { + assert.deepStrictEqual(document.representation.annotations, { identifier: "Text" }) + assert.strictEqual(document.representation.checks.length, 1) + assert.strictEqual(document.representation.checks[0].representation?.id, "effect/schema/isMinLength") + } + }) + + it("uses a fallback identifier for encoded representations", () => { + const schema = Schema.String.annotate({ "~identifier": "Person" }) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "PersonEncoded" }, + references: { + PersonEncoded: { + _tag: "String", + checks: [], + annotations: { "~identifier": "Person" } + } + } + }) + }) + + it("prefers an identifier over a fallback identifier", () => { + const schema = Schema.String.annotate({ identifier: "EncodedPerson", "~identifier": "Person" }) + const document = SchemaRepresentation.toRepresentation(schema.ast) + + assert.deepStrictEqual(document.representation, { _tag: "Reference", $ref: "EncodedPerson" }) + assert.deepStrictEqual(Object.keys(document.references), ["EncodedPerson"]) + }) + + it("reuses a Class identifier across repeated type-side occurrences", () => { + class User extends Schema.Class("User")({ name: Schema.String }) {} + + const document = SchemaRepresentation.toRepresentation(SchemaAST.toType(Schema.Tuple([User, User]).ast)) + assert.deepStrictEqual(document.representation, { + _tag: "Arrays", + elements: [ + { type: { _tag: "Reference", $ref: "User" }, isOptional: false }, + { type: { _tag: "Reference", $ref: "User" }, isOptional: false } + ], + rest: [], + checks: [] + }) + assert.deepStrictEqual(Object.keys(document.references), ["User"]) + }) + + it("extracts anonymous recursion into references", () => { + let schema: Schema.Codec + schema = Schema.suspend((): Schema.Codec => schema) + + assert.deepStrictEqual(SchemaRepresentation.toRepresentation(schema.ast), { + representation: { _tag: "Reference", $ref: "Suspend_" }, + references: { + Suspend_: { + _tag: "Suspend", + thunk: { _tag: "Reference", $ref: "Suspend_" }, + checks: [] + } + } + }) + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toRepresentations.test.ts b/.context/effect/packages/effect/test/schema/representation/toRepresentations.test.ts new file mode 100644 index 000000000..7897184e1 --- /dev/null +++ b/.context/effect/packages/effect/test/schema/representation/toRepresentations.test.ts @@ -0,0 +1,254 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema, SchemaAST, SchemaRepresentation } from "effect" + +describe("SchemaRepresentation.toRepresentations", () => { + describe("root identity and sharing", () => { + it("preserves root order", () => { + const document = SchemaRepresentation.toRepresentations([ + Schema.String.ast, + Schema.Number.ast, + Schema.Boolean.ast + ]) + + assert.deepStrictEqual(document, { + representations: [ + { _tag: "String", checks: [] }, + { _tag: "Number", checks: [] }, + { _tag: "Boolean", checks: [] } + ], + references: {} + }) + }) + + it("shares a named reference between roots", () => { + const shared = Schema.String.annotate({ identifier: "Shared" }) + const document = SchemaRepresentation.toRepresentations([shared.ast, shared.ast]) + + assert.deepStrictEqual(document, { + representations: [ + { _tag: "Reference", $ref: "Shared" }, + { _tag: "Reference", $ref: "Shared" } + ], + references: { + Shared: { + _tag: "String", + annotations: { identifier: "Shared" }, + checks: [] + } + } + }) + }) + + it("shares a fallback reference across contextual copies of the same encoded AST", () => { + const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" }) + const first = Schema.toCodecJson(Schema.fromJsonString(Content)) + const second = Schema.toCodecJson(Schema.fromJsonString(Content)) + const document = SchemaRepresentation.toRepresentations([first.ast, second.ast]) + + assert.notStrictEqual(first.ast, second.ast) + assert.deepStrictEqual(document, { + representations: [ + { _tag: "Reference", $ref: "Tool.ContentEncoded" }, + { _tag: "Reference", $ref: "Tool.ContentEncoded" } + ], + references: { + "Tool.ContentEncoded": { + _tag: "String", + annotations: { + expected: "a string that will be decoded as JSON", + contentMediaType: "application/json", + "~identifier": "Tool.Content" + }, + checks: [] + } + } + }) + }) + + it("shares copies of the same AST with the same Context", () => { + const ast = Schema.String.annotate({ identifier: "Value" }).ast + const context = new SchemaAST.Context(false, false) + const first = SchemaAST.replaceContext(ast, context) + const second = SchemaAST.replaceContext(ast, context) + + assert.notStrictEqual(first, second) + assert.deepStrictEqual(SchemaRepresentation.toRepresentations([first, second]), { + representations: [ + { _tag: "Reference", $ref: "Value" }, + { _tag: "Reference", $ref: "Value" } + ], + references: { + Value: { + _tag: "String", + annotations: { identifier: "Value" }, + checks: [] + } + } + }) + }) + + it("preserves the original owner through repeated Context replacements", () => { + const ast = Schema.String.annotate({ identifier: "Value" }).ast + const first = SchemaAST.replaceContext(ast, new SchemaAST.Context(true, false)) + const chained = SchemaAST.replaceContext(first, new SchemaAST.Context(false, true)) + const direct = SchemaAST.replaceContext(ast, new SchemaAST.Context(false, true)) + + assert.strictEqual(SchemaAST.getContextOwner(first), ast) + assert.strictEqual(SchemaAST.getContextOwner(chained), ast) + assert.deepStrictEqual(SchemaRepresentation.toRepresentations([chained, direct]), { + representations: [ + { _tag: "Reference", $ref: "Value" }, + { _tag: "Reference", $ref: "Value" } + ], + references: { + Value: { + _tag: "String", + annotations: { identifier: "Value" }, + checks: [] + } + } + }) + }) + + it("keeps a checked derivative distinct from its identified source", () => { + const base = Schema.String.annotate({ identifier: "Text" }) + const refined = base.pipe(Schema.check(Schema.isMinLength(1))) + const forward = SchemaRepresentation.toRepresentations([base.ast, refined.ast]) + const reversed = SchemaRepresentation.toRepresentations([refined.ast, base.ast]) + + assert.deepStrictEqual(forward.representations[0], { _tag: "Reference", $ref: "Text" }) + assert.strictEqual(forward.representations[1]._tag, "String") + if (forward.representations[1]._tag === "String") { + assert.strictEqual(forward.representations[1].checks.length, 1) + } + assert.strictEqual(reversed.representations[0]._tag, "String") + if (reversed.representations[0]._tag === "String") { + assert.strictEqual(reversed.representations[0].checks.length, 1) + } + assert.deepStrictEqual(reversed.representations[1], { _tag: "Reference", $ref: "Text" }) + assert.deepStrictEqual(forward.references, { + Text: { + _tag: "String", + annotations: { identifier: "Text" }, + checks: [] + } + }) + assert.deepStrictEqual(reversed.references, forward.references) + }) + + it("shares an anonymous non-trivial schema between roots", () => { + const shared = Schema.Struct({ value: Schema.String }) + const document = SchemaRepresentation.toRepresentations([shared.ast, shared.ast]) + + assert.deepStrictEqual(document, { + representations: [ + { _tag: "Reference", $ref: "Objects_" }, + { _tag: "Reference", $ref: "Objects_" } + ], + references: { + Objects_: { + _tag: "Objects", + propertySignatures: [{ + name: "value", + type: { _tag: "String", checks: [] }, + isOptional: false, + isMutable: false + }], + indexSignatures: [], + checks: [] + } + } + }) + }) + }) + + describe("identifier collisions", () => { + it("suffixes different schemas with the same identifier", () => { + const first = Schema.String.annotate({ identifier: "Value", description: "first" }) + const second = Schema.Number.annotate({ identifier: "Value", description: "second" }) + + assert.deepStrictEqual( + SchemaRepresentation.toRepresentations([first.ast, second.ast]), + { + representations: [ + { _tag: "Reference", $ref: "Value" }, + { _tag: "Reference", $ref: "Value_1" } + ], + references: { + Value: { + _tag: "String", + annotations: { identifier: "Value", description: "first" }, + checks: [] + }, + Value_1: { + _tag: "Number", + annotations: { identifier: "Value_1", description: "second" }, + checks: [] + } + } + } + ) + }) + + it("suffixes referentially distinct ASTs with equal representations", () => { + const first = Schema.String.annotate({ identifier: "Value" }) + const second = Schema.String.annotate({ identifier: "Value" }) + + assert.deepStrictEqual( + SchemaRepresentation.toRepresentations([first.ast, second.ast]), + { + representations: [ + { _tag: "Reference", $ref: "Value" }, + { _tag: "Reference", $ref: "Value_1" } + ], + references: { + Value: { + _tag: "String", + annotations: { identifier: "Value" }, + checks: [] + }, + Value_1: { + _tag: "String", + annotations: { identifier: "Value_1" }, + checks: [] + } + } + } + ) + }) + + it("suffixes fallback and explicit identifier collisions in encounter order", () => { + const first = Schema.String.annotate({ "~identifier": "Person" }) + const second = Schema.Number.annotate({ "~identifier": "Person" }) + const explicit = Schema.Boolean.annotate({ identifier: "PersonEncoded" }) + + assert.deepStrictEqual( + SchemaRepresentation.toRepresentations([first.ast, second.ast, explicit.ast]), + { + representations: [ + { _tag: "Reference", $ref: "PersonEncoded" }, + { _tag: "Reference", $ref: "PersonEncoded_1" }, + { _tag: "Reference", $ref: "PersonEncoded_2" } + ], + references: { + PersonEncoded: { + _tag: "String", + annotations: { "~identifier": "Person" }, + checks: [] + }, + PersonEncoded_1: { + _tag: "Number", + annotations: { "~identifier": "Person" }, + checks: [] + }, + PersonEncoded_2: { + _tag: "Boolean", + annotations: { identifier: "PersonEncoded_2" }, + checks: [] + } + } + } + ) + }) + }) +}) diff --git a/.context/effect/packages/effect/test/schema/representation/toSchema.test.ts b/.context/effect/packages/effect/test/schema/representation/toSchema.test.ts deleted file mode 100644 index c935bcedf..000000000 --- a/.context/effect/packages/effect/test/schema/representation/toSchema.test.ts +++ /dev/null @@ -1,506 +0,0 @@ -import { Redacted, Schema, SchemaRepresentation } from "effect" -import { describe, it } from "vitest" -import { deepStrictEqual, strictEqual } from "../../utils/assert.ts" - -describe("toSchema", () => { - function assertToSchemaRoundtrip(input: { - schema: Schema.Top - readonly reviver?: SchemaRepresentation.Reviver | undefined - }, runtime: string) { - const document = SchemaRepresentation.fromAST(input.schema.ast) - const roundtrip = SchemaRepresentation.fromAST( - SchemaRepresentation.toSchema(document, { reviver: input.reviver }).ast - ) - deepStrictEqual(roundtrip, document) - const codeDocument = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(roundtrip)) - strictEqual(codeDocument.codes[0].runtime, runtime) - } - - describe("String", () => { - it("String", () => { - assertToSchemaRoundtrip( - { schema: Schema.String }, - `Schema.String` - ) - }) - - it("String & check", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isMinLength(1)) }, - `Schema.String.check(Schema.isMinLength(1))` - ) - }) - - describe("checks", () => { - it("isTrimmed", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isTrimmed()) }, - `Schema.String.check(Schema.isTrimmed())` - ) - }) - - it("isULID", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isULID()) }, - `Schema.String.check(Schema.isULID())` - ) - }) - - it("isGUID", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isGUID()) }, - `Schema.String.check(Schema.isGUID())` - ) - }) - }) - }) - - it("Struct", () => { - assertToSchemaRoundtrip( - { schema: Schema.Struct({}) }, - `Schema.Struct({ })` - ) - assertToSchemaRoundtrip( - { schema: Schema.Struct({ a: Schema.String }) }, - `Schema.Struct({ "a": Schema.String })` - ) - assertToSchemaRoundtrip( - { schema: Schema.Struct({ [Symbol.for("a")]: Schema.String }) }, - `Schema.Struct({ [_symbol]: Schema.String })` - ) - assertToSchemaRoundtrip( - { schema: Schema.Struct({ a: Schema.optionalKey(Schema.String) }) }, - `Schema.Struct({ "a": Schema.optionalKey(Schema.String) })` - ) - assertToSchemaRoundtrip( - { schema: Schema.Struct({ a: Schema.mutableKey(Schema.String) }) }, - `Schema.Struct({ "a": Schema.mutableKey(Schema.String) })` - ) - assertToSchemaRoundtrip( - { schema: Schema.Struct({ a: Schema.optionalKey(Schema.mutableKey(Schema.String)) }) }, - `Schema.Struct({ "a": Schema.optionalKey(Schema.mutableKey(Schema.String)) })` - ) - }) - - it("Record", () => { - assertToSchemaRoundtrip( - { schema: Schema.Record(Schema.String, Schema.Number) }, - `Schema.Record(Schema.String, Schema.Number)` - ) - assertToSchemaRoundtrip( - { schema: Schema.Record(Schema.Symbol, Schema.Number) }, - `Schema.Record(Schema.Symbol, Schema.Number)` - ) - }) - - it("StructWithRest", () => { - assertToSchemaRoundtrip( - { - schema: Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [ - Schema.Record(Schema.String, Schema.Number) - ]) - }, - `Schema.StructWithRest(Schema.Struct({ "a": Schema.Number }), [Schema.Record(Schema.String, Schema.Number)])` - ) - }) - - it("Tuple", () => { - assertToSchemaRoundtrip( - { schema: Schema.Tuple([]) }, - `Schema.Tuple([])` - ) - assertToSchemaRoundtrip( - { schema: Schema.Tuple([Schema.String, Schema.Number]) }, - `Schema.Tuple([Schema.String, Schema.Number])` - ) - assertToSchemaRoundtrip( - { schema: Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]) }, - `Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])` - ) - }) - - it("Array", () => { - assertToSchemaRoundtrip( - { schema: Schema.Array(Schema.String) }, - `Schema.Array(Schema.String)` - ) - }) - - it("TupleWithRest", () => { - assertToSchemaRoundtrip( - { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]) }, - `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number])` - ) - assertToSchemaRoundtrip( - { schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) }, - `Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])` - ) - }) - - it("Suspend", () => { - type Category = { - readonly name: string - readonly children: ReadonlyArray - } - - const OuterCategory = Schema.Struct({ - name: Schema.String, - children: Schema.Array(Schema.suspend((): Schema.Codec => OuterCategory)) - }).annotate({ identifier: "Category" }) - - assertToSchemaRoundtrip( - { schema: OuterCategory }, - `Category` - ) - }) - - describe("brand", () => { - it("brand", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.pipe(Schema.brand("a")) }, - `Schema.String.pipe(Schema.brand("a"))` - ) - }) - - it("brand & brand", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.pipe(Schema.brand("a"), Schema.brand("b")) }, - `Schema.String.pipe(Schema.brand("a"), Schema.brand("b"))` - ) - }) - - it("check & brand", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")) }, - `Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b"))` - ) - }) - - it("brand & check & brand", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b")) }, - `Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b"))` - ) - }) - - it("check & brand & check", () => { - assertToSchemaRoundtrip( - { schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2)) }, - `Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2))` - ) - }) - }) - - describe("toSchemaDefaultReviver", () => { - function assertToSchemaWithReviver(schema: Schema.Top, runtime: string) { - assertToSchemaRoundtrip({ schema, reviver: SchemaRepresentation.toSchemaDefaultReviver }, runtime) - } - - it("Option", () => { - assertToSchemaWithReviver( - Schema.Option(Schema.String), - `Schema.Option(Schema.String)` - ) - assertToSchemaWithReviver( - Schema.Option(Schema.URL), - `Schema.Option(Schema.URL)` - ) - }) - - it("Result", () => { - assertToSchemaWithReviver( - Schema.Result(Schema.String, Schema.Number), - `Schema.Result(Schema.String, Schema.Number)` - ) - }) - - it("Json", () => { - assertToSchemaWithReviver( - Schema.Json, - `Schema.Json` - ) - }) - - it("MutableJson", () => { - assertToSchemaWithReviver( - Schema.MutableJson, - `Schema.MutableJson` - ) - }) - - it("Redacted", () => { - assertToSchemaWithReviver( - Schema.Redacted(Schema.String), - `Schema.Redacted(Schema.String)` - ) - }) - - it("Redacted options", () => { - const schema = Schema.Redacted(Schema.String, { - label: "password", - disallowJsonEncode: true - }) - const document = SchemaRepresentation.fromAST(schema.ast) - const roundtrip = SchemaRepresentation.toSchema(document, { - reviver: SchemaRepresentation.toSchemaDefaultReviver - }) - const encode = Schema.encodeUnknownExit(Schema.toCodecJson(roundtrip)) - - strictEqual( - String(encode(Redacted.make("secret", { label: "password" }))), - `Failure(Cause([Fail(SchemaError(Cannot serialize Redacted with label: "password"))]))` - ) - strictEqual( - String(encode(Redacted.make("secret", { label: "other" }))), - `Failure(Cause([Fail(SchemaError(Expected "password", got "other" - at ["label"]))]))` - ) - }) - - it("CauseReason", () => { - assertToSchemaWithReviver( - Schema.CauseReason(Schema.String, Schema.Number), - `Schema.CauseReason(Schema.String, Schema.Number)` - ) - }) - - it("Cause", () => { - assertToSchemaWithReviver( - Schema.Cause(Schema.String, Schema.Number), - `Schema.Cause(Schema.String, Schema.Number)` - ) - }) - - it("Exit", () => { - assertToSchemaWithReviver( - Schema.Exit(Schema.String, Schema.Number, Schema.Boolean), - `Schema.Exit(Schema.String, Schema.Number, Schema.Boolean)` - ) - }) - - it("ReadonlyMap", () => { - assertToSchemaWithReviver( - Schema.ReadonlyMap(Schema.String, Schema.Number), - `Schema.ReadonlyMap(Schema.String, Schema.Number)` - ) - }) - - it("HashMap", () => { - assertToSchemaWithReviver( - Schema.HashMap(Schema.String, Schema.Number), - `Schema.HashMap(Schema.String, Schema.Number)` - ) - }) - - it("Chunk", () => { - assertToSchemaWithReviver( - Schema.Chunk(Schema.String), - `Schema.Chunk(Schema.String)` - ) - }) - - it("ReadonlySet", () => { - assertToSchemaWithReviver( - Schema.ReadonlySet(Schema.String), - `Schema.ReadonlySet(Schema.String)` - ) - }) - - it("RegExp", () => { - assertToSchemaWithReviver( - Schema.RegExp, - `Schema.RegExp` - ) - }) - - it("URL", () => { - assertToSchemaWithReviver( - Schema.URL, - `Schema.URL` - ) - }) - - describe("Date", () => { - it("Date", () => { - assertToSchemaWithReviver( - Schema.Date, - `Schema.Date` - ) - }) - - describe("checks", () => { - it("isDateValid", () => { - assertToSchemaWithReviver( - Schema.Date.check(Schema.isDateValid()), - `Schema.Date.check(Schema.isDateValid())` - ) - }) - - it("isGreaterThanDate", () => { - assertToSchemaWithReviver( - Schema.Date.check(Schema.isGreaterThanDate(new Date(0))), - `Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))` - ) - }) - - it("isGreaterThanOrEqualToDate", () => { - assertToSchemaWithReviver( - Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0))), - `Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0)))` - ) - }) - - it("isLessThanDate", () => { - assertToSchemaWithReviver( - Schema.Date.check(Schema.isLessThanDate(new Date(0))), - `Schema.Date.check(Schema.isLessThanDate(new Date(0)))` - ) - }) - - it("isLessThanOrEqualToDate", () => { - assertToSchemaWithReviver( - Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0))), - `Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0)))` - ) - }) - }) - }) - - it("Duration", () => { - assertToSchemaWithReviver( - Schema.Duration, - `Schema.Duration` - ) - }) - - it("FormData", () => { - assertToSchemaWithReviver( - Schema.FormData, - `Schema.FormData` - ) - }) - - it("URLSearchParams", () => { - assertToSchemaWithReviver( - Schema.URLSearchParams, - `Schema.URLSearchParams` - ) - }) - - it("Uint8Array", () => { - assertToSchemaWithReviver( - Schema.Uint8Array, - `Schema.Uint8Array` - ) - }) - - it("DateTime.Utc", () => { - assertToSchemaWithReviver( - Schema.DateTimeUtc, - `Schema.DateTimeUtc` - ) - }) - - it("Error", () => { - assertToSchemaWithReviver( - Schema.Error(), - `Schema.Error()` - ) - }) - - it("Error with stack", () => { - assertToSchemaWithReviver( - Schema.Error({ includeStack: true }), - `Schema.Error({"includeStack":true})` - ) - }) - - it("Error with excluded cause", () => { - assertToSchemaWithReviver( - Schema.Error({ excludeCause: true }), - `Schema.Error({"excludeCause":true})` - ) - }) - - it("Defect", () => { - assertToSchemaWithReviver( - Schema.Defect(), - `Schema.Json` - ) - }) - - it("HashSet", () => { - assertToSchemaWithReviver( - Schema.HashSet(Schema.String), - `Schema.HashSet(Schema.String)` - ) - }) - - it("BigDecimal", () => { - assertToSchemaWithReviver( - Schema.BigDecimal, - `Schema.BigDecimal` - ) - }) - - it("TimeZoneOffset", () => { - assertToSchemaWithReviver( - Schema.TimeZoneOffset, - `Schema.TimeZoneOffset` - ) - }) - - it("TimeZoneNamed", () => { - assertToSchemaWithReviver( - Schema.TimeZoneNamed, - `Schema.TimeZoneNamed` - ) - }) - - it("TimeZone", () => { - assertToSchemaWithReviver( - Schema.TimeZone, - `Schema.TimeZone` - ) - }) - - it("DateTimeZoned", () => { - assertToSchemaWithReviver( - Schema.DateTimeZoned, - `Schema.DateTimeZoned` - ) - }) - - describe("ReadonlySet", () => { - it("ReadonlySet(String)", () => { - assertToSchemaWithReviver( - Schema.ReadonlySet(Schema.String), - `Schema.ReadonlySet(Schema.String)` - ) - }) - - describe("checks", () => { - it("isMinSize", () => { - assertToSchemaWithReviver( - Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2)), - `Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2))` - ) - }) - - it("isMaxSize", () => { - assertToSchemaWithReviver( - Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2)), - `Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2))` - ) - }) - }) - - it("isSizeBetween", () => { - assertToSchemaWithReviver( - Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2)), - `Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2))` - ) - }) - }) - }) -}) diff --git a/.context/effect/packages/effect/test/schema/toArbitrary.test.ts b/.context/effect/packages/effect/test/schema/toArbitrary.test.ts index c55745aa9..522d0b9fd 100644 --- a/.context/effect/packages/effect/test/schema/toArbitrary.test.ts +++ b/.context/effect/packages/effect/test/schema/toArbitrary.test.ts @@ -1,10 +1,14 @@ -import { BigDecimal, Chunk, DateTime, Effect, HashMap, HashSet, Option, Order, Schema, SchemaIssue } from "effect" +import { BigDecimal, Chunk, DateTime, Effect, HashMap, HashSet, Order, Schema, SchemaIssue } from "effect" import { FastCheck, TestSchema } from "effect/testing" import { describe, it } from "vitest" import { assertInclude, assertInstanceOf, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" +function toArbitrary(schema: S) { + return Schema.toArbitrary(schema)(FastCheck) +} + function assertUnsupportedSchema(schema: Schema.Constraint, message: string) { - throws(() => Schema.toArbitrary(schema), message) + throws(() => toArbitrary(schema), message) } function verifyGeneration>(schema: S, numRuns?: number) { @@ -19,12 +23,12 @@ function verifyGeneration>(sc // Guard for "fast but wrong" regressions: samples the derived arbitrary and // asserts an output invariant (length/size/property-count bounds) over many runs. function assertInvariant(schema: Schema.Constraint, predicate: (value: any) => boolean, numRuns = 200) { - FastCheck.assert(FastCheck.property(Schema.toArbitrary(schema), predicate), { numRuns }) + FastCheck.assert(FastCheck.property(toArbitrary(schema), predicate), { numRuns }) } function assertRecursiveNoFiniteGenerationPath(schema: Schema.Constraint) { throws( - () => Schema.toArbitrary(schema), + () => toArbitrary(schema), (e) => { assertInstanceOf(e, Error) assertInclude( @@ -55,7 +59,7 @@ function CustomArray( () => (input, ast) => globalThis.Array.isArray(input) ? Effect.succeed(input as ReadonlyArray) - : Effect.fail(new SchemaIssue.InvalidType(ast, Option.some(input))), + : Effect.fail(new SchemaIssue.InvalidType(ast)), { toArbitrary } ) } @@ -174,9 +178,9 @@ describe("Arbitrary generation", () => { } ) - Schema.toArbitraryLazy(Schema.Number.check(noNaN))(fc) - Schema.toArbitraryLazy(Schema.Number.check(noInfinity))(fc) - Schema.toArbitraryLazy(Schema.Finite)(fc) + Schema.toArbitrary(Schema.Number.check(noNaN))(fc) + Schema.toArbitrary(Schema.Number.check(noInfinity))(fc) + Schema.toArbitrary(Schema.Finite)(fc) deepStrictEqual(constraints, [ { noNaN: true }, @@ -185,7 +189,16 @@ describe("Arbitrary generation", () => { ]) }) - describe("report and candidates", () => { + it("should enforce opaque filters", () => { + const schema = Schema.Struct({ + a: Schema.String.check(Schema.makeFilter((s: string) => s.length > 0, { expected: "a custom string" })) + }) + const arbitrary = toArbitrary(schema) + + FastCheck.assert(FastCheck.property(arbitrary, (a) => a.a.length > 0), { numRuns: 5 }) + }) + + describe("candidates", () => { it("should use filter candidates with the merged constraint context", () => { let constraint: Schema.Annotations.ToArbitrary.GenerationConstraint | undefined const schema = Schema.String.check( @@ -202,11 +215,31 @@ describe("Arbitrary generation", () => { } }) ) - const result = Schema.toArbitrary(schema, { report: true }) + const arbitrary = toArbitrary(schema) - deepStrictEqual(result.report.warnings, []) deepStrictEqual(constraint, { minLength: 9 }) - FastCheck.assert(FastCheck.property(result.value, (s) => s === "candidate"), { numRuns: 20 }) + FastCheck.assert(FastCheck.property(arbitrary, (s) => s === "candidate"), { numRuns: 20 }) + }) + + it("should use filter group candidates", () => { + const schema = Schema.String.check( + Schema.makeFilterGroup( + [ + Schema.makeFilter((s: string) => s.startsWith("a"), { expected: "starts with a" }), + Schema.makeFilter((s: string) => s.endsWith("a"), { expected: "ends with a" }) + ], + { + arbitrary: { + candidate: { + make: (fc) => fc.constant("a") + } + } + } + ) + ) + const arbitrary = toArbitrary(schema) + + FastCheck.assert(FastCheck.property(arbitrary, (s) => s.startsWith("a") && s.endsWith("a")), { numRuns: 20 }) }) it("should allow candidates to be disabled for a context", () => { @@ -223,10 +256,9 @@ describe("Arbitrary generation", () => { } }) ) - const result = Schema.toArbitrary(schema, { report: true }) + toArbitrary(schema) strictEqual(calls, 1) - deepStrictEqual(result.report.warnings, []) }) it("should fail fast for invalid candidate weights", () => { @@ -243,61 +275,14 @@ describe("Arbitrary generation", () => { ) throws( - () => Schema.toArbitrary(makeSchema(0)), + () => toArbitrary(makeSchema(0)), "Unable to derive an arbitrary for a candidate with an invalid weight" ) throws( - () => Schema.toArbitrary(makeSchema(0.5)), + () => toArbitrary(makeSchema(0.5)), "Unable to derive an arbitrary for a candidate with an invalid weight" ) }) - - it("should report opaque filters", () => { - const schema = Schema.Struct({ - a: Schema.String.check(Schema.makeFilter((s: string) => s.length > 0, { expected: "a custom string" })) - }) - const result = Schema.toArbitrary(schema, { report: true }) - - deepStrictEqual(result.report.warnings, [ - { _tag: "OpaqueFilter", path: ["a"], description: "a custom string" } - ]) - FastCheck.assert(FastCheck.property(result.value, (a) => a.a.length > 0), { numRuns: 5 }) - }) - - it("should not report child filters when a filter group provides arbitrary metadata", () => { - const schema = Schema.String.check( - Schema.makeFilterGroup( - [ - Schema.makeFilter((s: string) => s.startsWith("a"), { expected: "starts with a" }), - Schema.makeFilter((s: string) => s.endsWith("a"), { expected: "ends with a" }) - ], - { - arbitrary: { - candidate: { - make: (fc) => fc.constant("a") - } - } - } - ) - ) - const result = Schema.toArbitrary(schema, { report: true }) - - deepStrictEqual(result.report.warnings, []) - FastCheck.assert(FastCheck.property(result.value, (s) => s === "a"), { numRuns: 20 }) - }) - - it("should not report warnings for constructive built-in filters", () => { - const schema = Schema.Struct({ - string: Schema.String.check(Schema.isMinLength(1), Schema.isStartsWith("a")), - number: Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 10 })), - array: Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isUnique()), - object: Schema.Record(Schema.String, Schema.Number).check(Schema.isMinProperties(1), Schema.isMaxProperties(3)), - set: Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(1), Schema.isMaxSize(3)) - }) - const result = Schema.toArbitrary(schema, { report: true }) - - deepStrictEqual(result.report.warnings, []) - }) }) describe("object property counts", () => { @@ -308,7 +293,7 @@ describe("Arbitrary generation", () => { c: Schema.optionalKey(Schema.String) }).check(Schema.isMinProperties(2)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (o) => globalThis.Object.keys(o).length >= 2), + FastCheck.property(toArbitrary(schema), (o) => globalThis.Object.keys(o).length >= 2), { numRuns: 100 } ) verifyGeneration(schema) @@ -321,7 +306,7 @@ describe("Arbitrary generation", () => { c: Schema.optionalKey(Schema.String) }).check(Schema.isMaxProperties(1)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (o) => globalThis.Object.keys(o).length <= 1), + FastCheck.property(toArbitrary(schema), (o) => globalThis.Object.keys(o).length <= 1), { numRuns: 100 } ) verifyGeneration(schema) @@ -334,9 +319,9 @@ describe("Arbitrary generation", () => { b: Schema.optionalKey(Schema.String) }).check(Schema.isMinProperties(2)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (o) => + FastCheck.property(toArbitrary(schema), (o) => globalThis.Reflect.ownKeys(o).length >= 2 && - globalThis.Object.prototype.hasOwnProperty.call(o, key)), + globalThis.Object.hasOwn(o, key)), { numRuns: 100 } ) verifyGeneration(schema) @@ -350,7 +335,7 @@ describe("Arbitrary generation", () => { c: Schema.optionalKey(Schema.String) }).check(Schema.isPropertiesLengthBetween(2, 3)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (o) => { + FastCheck.property(toArbitrary(schema), (o) => { const n = globalThis.Object.keys(o).length return n >= 2 && n <= 3 }), @@ -384,7 +369,7 @@ describe("Arbitrary generation", () => { } const schema = Schema.Struct(fields).check(Schema.isMinProperties(64)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (o) => globalThis.Object.keys(o).length === 64), + FastCheck.property(toArbitrary(schema), (o) => globalThis.Object.keys(o).length === 64), { numRuns: 100 } ) }) @@ -591,6 +576,18 @@ describe("Arbitrary generation", () => { Schema.Tuple([Schema.String, Schema.optional(Schema.Number)]) ) }) + + it("generates values valid for optional tuple positions", () => { + const schema = Schema.Tuple([ + Schema.optionalKey(Schema.String), + Schema.optionalKey(Schema.Number) + ]) + + FastCheck.assert(FastCheck.property(toArbitrary(schema), Schema.is(schema)), { + numRuns: 100, + seed: 17 + }) + }) }) describe("Array", () => { @@ -741,7 +738,7 @@ describe("Arbitrary generation", () => { a: Rec }) throws( - () => Schema.toArbitrary(schema), + () => toArbitrary(schema), (e) => { assertInstanceOf(e, Error) assertInclude( @@ -757,7 +754,7 @@ describe("Arbitrary generation", () => { const Rec = Schema.suspend((): Schema.Codec => schema) const schema: any = Schema.Array(Rec).check(Schema.isMinLength(1)) throws( - () => Schema.toArbitrary(schema), + () => toArbitrary(schema), (e) => { assertInstanceOf(e, Error) assertInclude( @@ -799,7 +796,7 @@ describe("Arbitrary generation", () => { [Schema.Union([Schema.Number, Rec])] ).check(Schema.isMinLength(2)) FastCheck.assert( - FastCheck.property(Schema.toArbitrary(schema), (a) => (a as Array).length >= 2), + FastCheck.property(toArbitrary(schema), (a) => (a as Array).length >= 2), { numRuns: 100 } ) }) @@ -989,7 +986,7 @@ describe("Arbitrary generation", () => { }).check(Schema.isMinProperties(1)) assertInvariant( schema, - (o) => globalThis.Object.keys(o).length >= 1 && globalThis.Object.prototype.hasOwnProperty.call(o, "a") + (o) => globalThis.Object.keys(o).length >= 1 && globalThis.Object.hasOwn(o, "a") ) }) @@ -1154,10 +1151,6 @@ describe("Arbitrary generation", () => { }))) }) - it("DateValid", () => { - verifyGeneration(Schema.DateValid) - }) - it("isGreaterThanOrEqualToBigInt", () => { verifyGeneration(Schema.BigInt.check(Schema.isGreaterThanOrEqualToBigInt(BigInt(0)))) }) @@ -1221,7 +1214,7 @@ describe("Arbitrary generation", () => { it("non-natural Date order", () => { const order = Order.flip(Order.Date) - verifyGeneration(Schema.DateValid.check(Schema.makeIsGreaterThan({ order })(new Date(0)))) + verifyGeneration(Schema.Date.check(Schema.makeIsGreaterThan({ order })(new Date(0)))) }) it("non-natural BigInt order", () => { @@ -1418,6 +1411,12 @@ describe("Arbitrary generation", () => { verifyGeneration(Schema.String.check(Schema.isEndsWith("a"))) }) + it("literal string checks with regexp syntax", () => { + verifyGeneration(Schema.String.check(Schema.isStartsWith("a.b"))) + verifyGeneration(Schema.String.check(Schema.isEndsWith("a+b"))) + verifyGeneration(Schema.String.check(Schema.isIncludes("["))) + }) + it("Number", () => { verifyGeneration(Schema.Number) }) @@ -1530,17 +1529,9 @@ describe("Arbitrary generation", () => { }))) }) - it("isValidDate", () => { - verifyGeneration(Schema.Date.check(Schema.isDateValid())) - }) - - it("isValidDate & isGreaterThanOrEqualToDate", () => { - verifyGeneration(Schema.Date.check(Schema.isDateValid(), Schema.isGreaterThanOrEqualToDate(new Date(0)))) - }) - it("Date with non-natural order", () => { const order = Order.flip(Order.Date) - verifyGeneration(Schema.DateValid.check(Schema.makeIsGreaterThan({ order })(new Date(0)))) + verifyGeneration(Schema.Date.check(Schema.makeIsGreaterThan({ order })(new Date(0)))) }) it("isGreaterThanOrEqualToBigInt", () => { @@ -1655,7 +1646,7 @@ describe("Arbitrary generation", () => { it("isBetweenBigDecimal with impossible exclusive bounds", () => { throws(() => - Schema.toArbitrary(Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ + toArbitrary(Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("1.01"), maximum: BigDecimal.fromStringUnsafe("1.01"), exclusiveMinimum: true, @@ -1665,7 +1656,7 @@ describe("Arbitrary generation", () => { it("isGreaterThanBigDecimal + isLessThanBigDecimal with impossible bounds", () => { throws(() => - Schema.toArbitrary(Schema.BigDecimal.check( + toArbitrary(Schema.BigDecimal.check( Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("1.01")), Schema.isLessThanBigDecimal(BigDecimal.fromStringUnsafe("1.01")) )), "Unable to derive an arbitrary for the ordered BigDecimal constraints") diff --git a/.context/effect/packages/effect/test/schema/toCodec.test.ts b/.context/effect/packages/effect/test/schema/toCodec.test.ts index 8db97cdda..bdc67682e 100644 --- a/.context/effect/packages/effect/test/schema/toCodec.test.ts +++ b/.context/effect/packages/effect/test/schema/toCodec.test.ts @@ -8,6 +8,7 @@ import { Redacted, Result, Schema, + SchemaAST, SchemaGetter, SchemaIssue, SchemaParser, @@ -18,6 +19,7 @@ import { describe, it } from "vitest" import { assertTrue, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" const isDeno = "Deno" in globalThis +const formatIssue = SchemaIssue.makeFormatterDefault() const FiniteFromDate = Schema.Date.pipe(Schema.decodeTo( Schema.Number, @@ -35,6 +37,16 @@ describe("Serializers", () => { strictEqual(serializer.schema, schema) }) + it("treats Json as canonical", () => { + strictEqual(Schema.toCodecJson(Schema.Json).ast, Schema.Json.ast) + strictEqual(Schema.toCodecJson(Schema.MutableJson).ast, Schema.MutableJson.ast) + }) + + it("is idempotent", () => { + const once = Schema.toCodecJson(Schema.suspend(() => Schema.Struct({ value: Schema.Number }))) + strictEqual(Schema.toCodecJson(once).ast, once.ast) + }) + it("should reorder the types in the Union based on the encoded side", async () => { const schema = Schema.Union([ Schema.String, @@ -69,11 +81,11 @@ describe("Serializers", () => { const schema = Schema.instanceOf(URL) const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) - const encoding = asserts.encoding() - await encoding.succeed(new URL("https://effect.website"), null) - - const decoding = asserts.decoding() - await decoding.fail("https://effect.website/", `Expected null, got "https://effect.website/"`) + await asserts.encoding().fail( + new URL("https://example.com"), + "Expected JSON value" + ) + await asserts.decoding().fail({}, "Expected ") }) describe("instanceOf with annotation", () => { @@ -216,7 +228,7 @@ describe("Serializers", () => { const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) const encoding = asserts.encoding() - await encoding.fail({}, "Expected never, got {}") + await encoding.fail({}, "Expected never") }) it("Any", async () => { @@ -241,7 +253,7 @@ describe("Serializers", () => { await encoding.succeed(null) await encoding.succeed({ a: "a", b: 1, c: true }) await encoding.succeed(["a", 1, true]) - await encoding.fail({ a: 1n }, `Expected JSON value, got {"a":1n}`) + await encoding.fail({ a: 1n }, `Expected JSON value`) const decoding = asserts.decoding() await decoding.succeed("a") @@ -250,7 +262,7 @@ describe("Serializers", () => { await decoding.succeed(null) await decoding.succeed({ a: "a", b: 1, c: true }) await decoding.succeed(["a", 1, true]) - await decoding.fail({ a: 1n }, `Expected JSON value, got {"a":1n}`) + await decoding.fail({ a: 1n }, `Expected JSON value`) }) it("ObjectKeyword", async () => { @@ -260,14 +272,14 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.succeed({ a: "a", b: 1, c: true }) await encoding.succeed(["a", 1, true]) - await encoding.fail("a", `Expected object | array | function, got "a"`) - await encoding.fail({ a: 1n }, `Expected JSON value, got {"a":1n}`) + await encoding.fail("a", `Expected object | array | function`) + await encoding.fail({ a: 1n }, `Expected JSON value\n at ["a"]`) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 1, c: true }) await decoding.succeed(["a", 1, true]) - await decoding.fail("a", `Expected object | array | function, got "a"`) - await decoding.fail({ a: 1n }, `Expected JSON value, got {"a":1n}`) + await decoding.fail("a", `Expected array | object`) + await decoding.fail({ a: 1n }, `Expected JSON value\n at ["a"]`) }) it("Undefined", async () => { @@ -306,6 +318,37 @@ describe("Serializers", () => { }) describe("Number", () => { + it("reuses the Finite AST in the canonical encoding", () => { + const encoded = SchemaAST.getLastEncoding(Schema.toCodecJson(Schema.Number).ast) + strictEqual(encoded._tag, "Union") + if (encoded._tag === "Union") { + strictEqual(encoded.types[0], SchemaAST.finite) + strictEqual(encoded.types[0], Schema.Finite.ast) + } + }) + + it("does not propagate constructor defaults to the canonical encoding", () => { + const schema = Schema.Struct({ + a: Schema.Number.pipe( + Schema.optionalKey, + Schema.mutableKey, + Schema.annotateKey({ description: "a" }), + Schema.withConstructorDefault(Effect.succeed(0)) + ) + }) + const ast = Schema.toCodecJson(schema).ast + strictEqual(ast._tag, "Objects") + if (ast._tag === "Objects") { + const type = ast.propertySignatures[0].type + assertTrue(type.context?.constructorDefault !== undefined) + const encoded = SchemaAST.getLastEncoding(type) + strictEqual(encoded.context?.isOptional, true) + strictEqual(encoded.context?.isMutable, true) + strictEqual(encoded.context?.constructorDefault, undefined) + deepStrictEqual(encoded.context?.annotations, { description: "a" }) + } + }) + it("Number", async () => { const schema = Schema.Number const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) @@ -325,11 +368,11 @@ describe("Serializers", () => { await decoding.succeed("Infinity", Infinity) await decoding.succeed("-Infinity", -Infinity) await decoding.succeed("NaN", NaN) - await decoding.succeed(Infinity) - await decoding.succeed(-Infinity) - await decoding.succeed(NaN) - await decoding.fail(null, `Expected number | "Infinity" | "-Infinity" | "NaN", got null`) - await decoding.fail("a", `Expected "Infinity" | "-Infinity" | "NaN", got "a"`) + await decoding.fail(Infinity, "Expected a finite number") + await decoding.fail(-Infinity, "Expected a finite number") + await decoding.fail(NaN, "Expected a finite number") + await decoding.fail(null, `Expected number | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail("a", `Expected "Infinity" | "-Infinity" | "NaN"`) }) describe("checks", () => { @@ -341,22 +384,22 @@ describe("Serializers", () => { await encoding.succeed(1) await encoding.succeed(-1) await encoding.succeed(1.2) - await encoding.fail(Infinity, "Expected a finite number, got Infinity") - await encoding.fail(-Infinity, "Expected a finite number, got -Infinity") - await encoding.fail(NaN, "Expected a finite number, got NaN") + await encoding.fail(Infinity, "Expected a finite number") + await encoding.fail(-Infinity, "Expected a finite number") + await encoding.fail(NaN, "Expected a finite number") const decoding = asserts.decoding() await decoding.succeed(1) await decoding.succeed(-1) await decoding.succeed(1.2) - await decoding.fail("Infinity", `Expected number, got "Infinity"`) - await decoding.fail("-Infinity", `Expected number, got "-Infinity"`) - await decoding.fail("NaN", `Expected number, got "NaN"`) - await decoding.fail(Infinity, `Expected a finite number, got Infinity`) - await decoding.fail(-Infinity, `Expected a finite number, got -Infinity`) - await decoding.fail(NaN, `Expected a finite number, got NaN`) - await decoding.fail(null, `Expected number, got null`) - await decoding.fail("a", `Expected number, got "a"`) + await decoding.fail("Infinity", `Expected number`) + await decoding.fail("-Infinity", `Expected number`) + await decoding.fail("NaN", `Expected number`) + await decoding.fail(Infinity, `Expected a finite number`) + await decoding.fail(-Infinity, `Expected a finite number`) + await decoding.fail(NaN, `Expected a finite number`) + await decoding.fail(null, `Expected number`) + await decoding.fail("a", `Expected number`) }) it("Int", async () => { @@ -366,23 +409,23 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.succeed(1) await encoding.succeed(-1) - await encoding.fail(1.2, `Expected an integer, got 1.2`) - await encoding.fail(Infinity, "Expected an integer, got Infinity") - await encoding.fail(-Infinity, "Expected an integer, got -Infinity") - await encoding.fail(NaN, "Expected an integer, got NaN") + await encoding.fail(1.2, `Expected an integer`) + await encoding.fail(Infinity, "Expected an integer") + await encoding.fail(-Infinity, "Expected an integer") + await encoding.fail(NaN, "Expected an integer") const decoding = asserts.decoding() await decoding.succeed(1) await decoding.succeed(-1) - await decoding.fail(1.2, `Expected an integer, got 1.2`) - await decoding.fail("Infinity", `Expected number, got "Infinity"`) - await decoding.fail("-Infinity", `Expected number, got "-Infinity"`) - await decoding.fail("NaN", `Expected number, got "NaN"`) - await decoding.fail(Infinity, `Expected an integer, got Infinity`) - await decoding.fail(-Infinity, `Expected an integer, got -Infinity`) - await decoding.fail(NaN, `Expected an integer, got NaN`) - await decoding.fail(null, `Expected number, got null`) - await decoding.fail("a", `Expected number, got "a"`) + await decoding.fail(1.2, `Expected an integer`) + await decoding.fail("Infinity", `Expected number`) + await decoding.fail("-Infinity", `Expected number`) + await decoding.fail("NaN", `Expected number`) + await decoding.fail(Infinity, `Expected an integer`) + await decoding.fail(-Infinity, `Expected an integer`) + await decoding.fail(NaN, `Expected an integer`) + await decoding.fail(null, `Expected number`) + await decoding.fail("a", `Expected number`) }) it("isGreaterThanOrEqualTo", async () => { @@ -391,24 +434,24 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.succeed(1) - await encoding.fail(-1, `Expected a value greater than or equal to 1, got -1`) + await encoding.fail(-1, `Expected a value greater than or equal to 1`) await encoding.succeed(1.2) await encoding.succeed(Infinity, "Infinity") - await encoding.fail(-Infinity, "Expected a value greater than or equal to 1, got -Infinity") - await encoding.fail(NaN, "Expected a value greater than or equal to 1, got NaN") + await encoding.fail(-Infinity, "Expected a value greater than or equal to 1") + await encoding.fail(NaN, "Expected a value greater than or equal to 1") const decoding = asserts.decoding() await decoding.succeed(1) - await encoding.fail(-1, `Expected a value greater than or equal to 1, got -1`) + await encoding.fail(-1, `Expected a value greater than or equal to 1`) await decoding.succeed(1.2) await decoding.succeed("Infinity", Infinity) - await decoding.fail("-Infinity", `Expected a value greater than or equal to 1, got -Infinity`) - await decoding.fail("NaN", `Expected a value greater than or equal to 1, got NaN`) - await decoding.succeed(Infinity) - await decoding.fail(-Infinity, `Expected a value greater than or equal to 1, got -Infinity`) - await decoding.fail(NaN, `Expected a value greater than or equal to 1, got NaN`) - await decoding.fail(null, `Expected number | "Infinity" | "-Infinity" | "NaN", got null`) - await decoding.fail("a", `Expected "Infinity" | "-Infinity" | "NaN", got "a"`) + await decoding.fail("-Infinity", `Expected a value greater than or equal to 1`) + await decoding.fail("NaN", `Expected a value greater than or equal to 1`) + await decoding.fail(Infinity, "Expected a finite number") + await decoding.fail(-Infinity, "Expected a finite number") + await decoding.fail(NaN, "Expected a finite number") + await decoding.fail(null, `Expected number | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail("a", `Expected "Infinity" | "-Infinity" | "NaN"`) }) }) }) @@ -533,7 +576,7 @@ describe("Serializers", () => { const decoding = asserts.decoding() await decoding.fail( "-", - `Expected "a" | 1 | "2" | true, got "-"` + `Expected "a" | 1 | "2" | true` ) }) @@ -688,6 +731,17 @@ describe("Serializers", () => { ) }) + it("Struct with an explicitly encoded Symbol property name", async () => { + const field = Symbol.for("field") + const schema = Schema.Struct({ + [field]: Schema.String + }).pipe(Schema.encodeKeys({ [field]: "field" })) + const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) + + await asserts.encoding().succeed({ [field]: "a" }, { field: "a" }) + await asserts.decoding().succeed({ field: "a" }, { [field]: "a" }) + }) + describe("Tuple", () => { it("Date", async () => { const schema = Schema.Tuple([Schema.Date]) @@ -893,8 +947,8 @@ describe("Serializers", () => { await decoding.succeed({ a: 0 }, new A({ a: 0 })) }) - it("ErrorClass", async () => { - class E extends Schema.ErrorClass("E")({ + it("Error", async () => { + class E extends Schema.Error("E")({ a: Schema.Finite }) {} const asserts = new TestSchema.Asserts(Schema.toCodecJson(Schema.toType(E))) @@ -918,7 +972,7 @@ describe("Serializers", () => { }) it("Error", async () => { - const schema = Schema.Error() + const schema = Schema.ErrorInstance() const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) const encoding = asserts.encoding() @@ -990,7 +1044,7 @@ describe("Serializers", () => { }) it("Error with stack", async () => { - const schema = Schema.Error({ includeStack: true }) + const schema = Schema.ErrorInstance({ includeStack: true }) const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) const error = new Error("a") error.stack = "stack" @@ -1014,7 +1068,7 @@ describe("Serializers", () => { }) it("Error with excluded cause", async () => { - const schema = Schema.Error({ excludeCause: true }) + const schema = Schema.ErrorInstance({ excludeCause: true }) const asserts = new TestSchema.Asserts(Schema.toCodecJson(schema)) const encoding = asserts.encoding() @@ -1045,7 +1099,7 @@ describe("Serializers", () => { ) await decoding.fail( "not a url", - `Invalid URL string: not a url` + "Expected a valid URL string" ) }) @@ -1125,11 +1179,11 @@ describe("Serializers", () => { await decoding.succeed({ source: "a", flags: "i" }, new RegExp("a", "i")) await decoding.fail( { source: "(", flags: "" }, - `SyntaxError: Invalid regular expression: /(/: Unterminated group` + "Expected valid RegExp source and flags" ) await decoding.fail( { source: "a", flags: "x" }, - `SyntaxError: Invalid flags supplied to RegExp constructor 'x'` + "Expected valid RegExp source and flags" ) }) @@ -1144,7 +1198,7 @@ describe("Serializers", () => { await decoding.succeed("AQID", new Uint8Array([1, 2, 3])) await decoding.fail( "not a base64 string", - "Length must be a multiple of 4, but is 19" + "Expected a valid Base64 string" ) }) @@ -1259,7 +1313,7 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.fail( Redacted.make("a", { label: "API key" }), - `Expected "password", got "API key" + `Expected "password" at ["label"]` ) }) @@ -1382,7 +1436,7 @@ describe("Serializers", () => { }) it("Error", async () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ a: FiniteFromDate }) {} const asserts = new TestSchema.Asserts(Schema.toCodecJson(E)) @@ -1556,7 +1610,7 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.succeed(failureResult, { issues: [ - { path: ["a"], message: `Expected a value with a length of at least 1, got ""` }, + { path: ["a"], message: `Expected a value with a length of at least 1` }, { path: ["c", 0], message: "Missing key" }, { path: ["Symbol(b)"], message: "Missing key" } ] @@ -1565,7 +1619,7 @@ describe("Serializers", () => { const decoding = asserts.decoding() await decoding.succeed({ issues: [ - { path: ["a"], message: `Expected a value with a length of at least 1, got ""` }, + { path: ["a"], message: `Expected a value with a length of at least 1` }, { path: ["c", 0], message: "Missing key" }, { path: ["Symbol(b)"], message: "Missing key" } ] @@ -1573,6 +1627,31 @@ describe("Serializers", () => { }) }) + describe("toCodecIso", () => { + it("does not propagate constructor defaults to the canonical encoding", () => { + const schema = Schema.Struct({ + a: Schema.URL.pipe( + Schema.overrideToCodecIso(Schema.String, SchemaTransformation.urlFromString), + Schema.optionalKey, + Schema.mutableKey, + Schema.annotateKey({ description: "a" }), + Schema.withConstructorDefault(Effect.succeed(new URL("https://example.com"))) + ) + }) + const ast = Schema.toCodecIso(schema).ast + strictEqual(ast._tag, "Objects") + if (ast._tag === "Objects") { + const type = ast.propertySignatures[0].type + assertTrue(type.context?.constructorDefault !== undefined) + const encoded = SchemaAST.getLastEncoding(type) + strictEqual(encoded.context?.isOptional, true) + strictEqual(encoded.context?.isMutable, true) + strictEqual(encoded.context?.constructorDefault, undefined) + deepStrictEqual(encoded.context?.annotations, { description: "a" }) + } + }) + }) + describe("toCodecStringTree", () => { it("exposes the source schema", () => { const schema = Schema.FiniteFromString @@ -1580,6 +1659,28 @@ describe("Serializers", () => { strictEqual(serializer.schema, schema) }) + it("does not propagate constructor defaults to the canonical encoding", () => { + const schema = Schema.Struct({ + a: Schema.Number.pipe( + Schema.optionalKey, + Schema.mutableKey, + Schema.annotateKey({ description: "a" }), + Schema.withConstructorDefault(Effect.succeed(0)) + ) + }) + const ast = Schema.toCodecStringTree(schema).ast + strictEqual(ast._tag, "Objects") + if (ast._tag === "Objects") { + const type = ast.propertySignatures[0].type + assertTrue(type.context?.constructorDefault !== undefined) + const encoded = SchemaAST.getLastEncoding(type) + strictEqual(encoded.context?.isOptional, true) + strictEqual(encoded.context?.isMutable, true) + strictEqual(encoded.context?.constructorDefault, undefined) + deepStrictEqual(encoded.context?.annotations, { description: "a" }) + } + }) + it("should reorder the types in the Union based on the encoded side", async () => { const schema = Schema.Union([ Schema.String, @@ -1625,6 +1726,16 @@ describe("Serializers", () => { const serializer = Schema.toCodecStringTree(schema) strictEqual(serializer.ast, Schema.toCodecStringTree(serializer).ast) }) + + it("Unknown", () => { + const serializer = Schema.toCodecStringTree(Schema.Unknown) + strictEqual(serializer.ast, Schema.toCodecStringTree(serializer).ast) + }) + + it("Suspend", () => { + const serializer = Schema.toCodecStringTree(Schema.suspend(() => Schema.Array(Schema.Finite))) + strictEqual(serializer.ast, Schema.toCodecStringTree(serializer).ast) + }) }) describe("schemas without encoding", () => { @@ -1641,15 +1752,31 @@ describe("Serializers", () => { }) }) - it("Declaration", async () => { + it("Declaration", () => { const schema = Schema.instanceOf(URL) - const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema)) + throws( + () => Schema.toCodecStringTree(schema), + "Missing structural codec for StringTree" + ) + }) + + it("Json", async () => { + const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(Schema.Json)) const encoding = asserts.encoding() - await encoding.succeed(new URL("https://effect.website"), undefined) + await encoding.succeed("a") + await encoding.succeed(["a"]) + await encoding.succeed({ a: "a" }) + await encoding.fail(1, `Expected StringTree`) + await encoding.fail(true, `Expected StringTree`) + await encoding.fail(null, `Expected StringTree`) + await encoding.fail({ a: 1 }, `Expected StringTree`) const decoding = asserts.decoding() - await decoding.fail("https://effect.website/", `Expected undefined, got "https://effect.website/"`) + await decoding.succeed("a") + await decoding.succeed(["a"]) + await decoding.succeed({ a: "a" }) + await decoding.fail(undefined, `Expected JSON value`) }) it("Unknown", async () => { @@ -1658,17 +1785,17 @@ describe("Serializers", () => { const encoding = asserts.encoding() await encoding.succeed("a") - await encoding.fail(1, `Expected StringTree, got 1`) + await encoding.fail(1, `Expected StringTree`) await encoding.succeed({ a: "a" }) await encoding.succeed(["a"]) - await encoding.fail({ a: 1 }, `Expected StringTree, got {"a":1}`) + await encoding.fail({ a: 1 }, `Expected StringTree`) const decoding = asserts.decoding() await decoding.succeed("a") - await decoding.fail(1, `Expected StringTree, got 1`) + await decoding.fail(1, `Expected StringTree`) await decoding.succeed({ a: "a" }) await decoding.succeed(["a"]) - await decoding.fail({ a: 1 }, `Expected StringTree, got {"a":1}`) + await decoding.fail({ a: 1 }, `Expected StringTree`) }) it("ObjectKeyword", async () => { @@ -1676,18 +1803,18 @@ describe("Serializers", () => { const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema)) const encoding = asserts.encoding() - await encoding.fail("a", `Expected object | array | function, got "a"`) - await encoding.fail(1, `Expected object | array | function, got 1`) + await encoding.fail("a", `Expected object | array | function`) + await encoding.fail(1, `Expected object | array | function`) await encoding.succeed({ a: "a" }) await encoding.succeed(["a"]) - await encoding.fail({ a: 1 }, `Expected StringTree, got {"a":1}`) + await encoding.fail({ a: 1 }, `Expected StringTree`) const decoding = asserts.decoding() - await decoding.fail("a", `Expected object | array | function, got "a"`) - await decoding.fail(1, `Expected StringTree, got 1`) + await decoding.fail("a", `Expected object | array | function`) + await decoding.fail(1, `Expected StringTree`) await decoding.succeed({ a: "a" }) await decoding.succeed(["a"]) - await decoding.fail({ a: 1 }, `Expected StringTree, got {"a":1}`) + await decoding.fail({ a: 1 }, `Expected StringTree`) }) it("Never", async () => { @@ -1695,7 +1822,7 @@ describe("Serializers", () => { const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema)) const encoding = asserts.encoding() - await encoding.fail({}, "Expected never, got {}") + await encoding.fail({}, "Expected never") }) it("Any should be an escape hatch", async () => { @@ -1773,14 +1900,14 @@ describe("Serializers", () => { await decoding.succeed("Infinity", Infinity) await decoding.succeed("-Infinity", -Infinity) await decoding.succeed("NaN", NaN) - await decoding.fail(Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN", got Infinity`) - await decoding.fail(-Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN", got -Infinity`) - await decoding.fail(NaN, `Expected string | "Infinity" | "-Infinity" | "NaN", got NaN`) - await decoding.fail(null, `Expected string | "Infinity" | "-Infinity" | "NaN", got null`) + await decoding.fail(Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(-Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(NaN, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(null, `Expected string | "Infinity" | "-Infinity" | "NaN"`) await decoding.fail( "a", - `Expected a string representing a finite number, got "a" -Expected "Infinity" | "-Infinity" | "NaN", got "a"` + `Expected a string representing a finite number +Expected "Infinity" | "-Infinity" | "NaN"` ) }) @@ -1793,22 +1920,22 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` await encoding.succeed(1, "1") await encoding.succeed(-1, "-1") await encoding.succeed(1.2, "1.2") - await encoding.fail(Infinity, "Expected a finite number, got Infinity") - await encoding.fail(-Infinity, "Expected a finite number, got -Infinity") - await encoding.fail(NaN, "Expected a finite number, got NaN") + await encoding.fail(Infinity, "Expected a finite number") + await encoding.fail(-Infinity, "Expected a finite number") + await encoding.fail(NaN, "Expected a finite number") const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.succeed("-1", -1) await decoding.succeed("1.2", 1.2) - await decoding.fail("Infinity", `Expected a string representing a finite number, got "Infinity"`) - await decoding.fail("-Infinity", `Expected a string representing a finite number, got "-Infinity"`) - await decoding.fail("NaN", `Expected a string representing a finite number, got "NaN"`) - await decoding.fail(Infinity, `Expected string, got Infinity`) - await decoding.fail(-Infinity, `Expected string, got -Infinity`) - await decoding.fail(NaN, `Expected string, got NaN`) - await decoding.fail(null, `Expected string, got null`) - await decoding.fail("a", `Expected a string representing a finite number, got "a"`) + await decoding.fail("Infinity", `Expected a string representing a finite number`) + await decoding.fail("-Infinity", `Expected a string representing a finite number`) + await decoding.fail("NaN", `Expected a string representing a finite number`) + await decoding.fail(Infinity, `Expected string`) + await decoding.fail(-Infinity, `Expected string`) + await decoding.fail(NaN, `Expected string`) + await decoding.fail(null, `Expected string`) + await decoding.fail("a", `Expected a string representing a finite number`) }) it("Int", async () => { @@ -1818,23 +1945,23 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.succeed(-1, "-1") - await encoding.fail(1.2, `Expected an integer, got 1.2`) - await encoding.fail(Infinity, `Expected an integer, got Infinity`) - await encoding.fail(-Infinity, `Expected an integer, got -Infinity`) - await encoding.fail(NaN, `Expected an integer, got NaN`) + await encoding.fail(1.2, `Expected an integer`) + await encoding.fail(Infinity, `Expected an integer`) + await encoding.fail(-Infinity, `Expected an integer`) + await encoding.fail(NaN, `Expected an integer`) const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.succeed("-1", -1) - await decoding.fail("1.2", `Expected an integer, got 1.2`) - await decoding.fail("Infinity", `Expected a string representing a finite number, got "Infinity"`) - await decoding.fail("-Infinity", `Expected a string representing a finite number, got "-Infinity"`) - await decoding.fail("NaN", `Expected a string representing a finite number, got "NaN"`) - await decoding.fail(Infinity, `Expected string, got Infinity`) - await decoding.fail(-Infinity, `Expected string, got -Infinity`) - await decoding.fail(NaN, `Expected string, got NaN`) - await decoding.fail(null, `Expected string, got null`) - await decoding.fail("a", `Expected a string representing a finite number, got "a"`) + await decoding.fail("1.2", `Expected an integer`) + await decoding.fail("Infinity", `Expected a string representing a finite number`) + await decoding.fail("-Infinity", `Expected a string representing a finite number`) + await decoding.fail("NaN", `Expected a string representing a finite number`) + await decoding.fail(Infinity, `Expected string`) + await decoding.fail(-Infinity, `Expected string`) + await decoding.fail(NaN, `Expected string`) + await decoding.fail(null, `Expected string`) + await decoding.fail("a", `Expected a string representing a finite number`) }) it("isGreaterThanOrEqualTo", async () => { @@ -1843,27 +1970,27 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const encoding = asserts.encoding() await encoding.succeed(1, "1") - await encoding.fail(-1, `Expected a value greater than or equal to 1, got -1`) + await encoding.fail(-1, `Expected a value greater than or equal to 1`) await encoding.succeed(1.2, "1.2") await encoding.succeed(Infinity, "Infinity") - await encoding.fail(-Infinity, "Expected a value greater than or equal to 1, got -Infinity") - await encoding.fail(NaN, "Expected a value greater than or equal to 1, got NaN") + await encoding.fail(-Infinity, "Expected a value greater than or equal to 1") + await encoding.fail(NaN, "Expected a value greater than or equal to 1") const decoding = asserts.decoding() await decoding.succeed("1", 1) - await decoding.fail("-1", `Expected a value greater than or equal to 1, got -1`) + await decoding.fail("-1", `Expected a value greater than or equal to 1`) await decoding.succeed("1.2", 1.2) await decoding.succeed("Infinity", Infinity) - await decoding.fail("-Infinity", `Expected a value greater than or equal to 1, got -Infinity`) - await decoding.fail("NaN", `Expected a value greater than or equal to 1, got NaN`) - await decoding.fail(Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN", got Infinity`) - await decoding.fail(-Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN", got -Infinity`) - await decoding.fail(NaN, `Expected string | "Infinity" | "-Infinity" | "NaN", got NaN`) - await decoding.fail(null, `Expected string | "Infinity" | "-Infinity" | "NaN", got null`) + await decoding.fail("-Infinity", `Expected a value greater than or equal to 1`) + await decoding.fail("NaN", `Expected a value greater than or equal to 1`) + await decoding.fail(Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(-Infinity, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(NaN, `Expected string | "Infinity" | "-Infinity" | "NaN"`) + await decoding.fail(null, `Expected string | "Infinity" | "-Infinity" | "NaN"`) await decoding.fail( "a", - `Expected a string representing a finite number, got "a" -Expected "Infinity" | "-Infinity" | "NaN", got "a"` + `Expected a string representing a finite number +Expected "Infinity" | "-Infinity" | "NaN"` ) }) }) @@ -1899,7 +2026,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const decoding = asserts.decoding() await decoding.succeed("Symbol(a)", Symbol.for("a")) - await decoding.fail("a", `Expected a string representing a symbol, got "a"`) + await decoding.fail("a", `Expected a string representing a symbol`) }) it("UniqueSymbol", async () => { @@ -1911,7 +2038,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const decoding = asserts.decoding() await decoding.succeed("Symbol(a)", Symbol.for("a")) - await decoding.fail("a", `Expected a string representing a symbol, got "a"`) + await decoding.fail("a", `Expected a string representing a symbol`) }) it("BigInt", async () => { @@ -1923,7 +2050,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const decoding = asserts.decoding() await decoding.succeed("1", 1n) - await decoding.fail("a", `Expected a string representing a bigint, got "a"`) + await decoding.fail("a", `Expected a string representing a bigint`) }) it("PropertyKey", async () => { @@ -1994,7 +2121,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const decoding = asserts.decoding() await decoding.fail( "-", - `Expected "a" | "1" | "2" | "true", got "-"` + `Expected "a" | "1" | "2" | "true"` ) }) @@ -2153,6 +2280,17 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` ) }) + it("Struct with an explicitly encoded Symbol property name", async () => { + const field = Symbol.for("field") + const schema = Schema.Struct({ + [field]: Schema.String + }).pipe(Schema.encodeKeys({ [field]: "field" })) + const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema)) + + await asserts.encoding().succeed({ [field]: "a" }, { field: "a" }) + await asserts.decoding().succeed({ field: "a" }, { [field]: "a" }) + }) + describe("Tuple", () => { it("Date", async () => { const schema = Schema.Tuple([Schema.Date]) @@ -2264,7 +2402,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` await encoding.succeed([1, 2], ["1", "2"]) const decoding = asserts.decoding() - await decoding.fail("1,2", `Expected array, got "1,2"`) + await decoding.fail("1,2", `Expected array`) await decoding.succeed(["1", "2"], [1, 2]) }) @@ -2372,8 +2510,8 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` await decoding.succeed({ a: "0" }, new A({ a: 0 })) }) - it("ErrorClass", async () => { - class E extends Schema.ErrorClass("E")({ + it("Error", async () => { + class E extends Schema.Error("E")({ a: Schema.Finite }) {} const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(Schema.toType(E))) @@ -2397,7 +2535,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` }) it("Error", async () => { - const schema = Schema.Error() + const schema = Schema.ErrorInstance() const asserts = new TestSchema.Asserts(Schema.toCodecStringTree(schema)) const encoding = asserts.encoding() @@ -2407,12 +2545,10 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` ) const decoding = asserts.decoding() - // Error: message only await decoding.succeed( { message: "a" }, new Error("a") ) - // Error: message and name await decoding.succeed( { name: "b", message: "a" }, (() => { @@ -2421,7 +2557,6 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` return err })() ) - // Error: message, name, and stack await decoding.succeed( { name: "b", message: "a", stack: "c" }, (() => { @@ -2454,7 +2589,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` ) await decoding.fail( "not a url", - `Invalid URL string: not a url` + "Expected a valid URL string" ) }) @@ -2471,7 +2606,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` await decoding.succeed({ source: "a", flags: "i" }, new RegExp("a", "i")) await decoding.fail( { source: "a", flags: "x" }, - `SyntaxError: Invalid flags supplied to RegExp constructor 'x'` + "Expected valid RegExp source and flags" ) }) @@ -2607,11 +2742,23 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed({ a: ["a"] }) - await decoding.fail({ a: "a" }, `Expected array, got "a"\n at ["a"]`) + await decoding.fail({ a: "a" }, `Expected array\n at ["a"]`) }) }) describe("toCodecArrayFromSingle", () => { + it("preserves union fallback when a singleton fails array element checks", async () => { + const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Union([ + Schema.Array(Schema.String.check(Schema.isMinLength(2))), + Schema.String + ]))) + const asserts = new TestSchema.Asserts(schema) + + const decoding = asserts.decoding() + await decoding.succeed("a", "a") + await decoding.succeed("ab", ["ab"]) + }) + it("accepts string and array inputs for a top-level array", async () => { const serializer = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Array(Schema.Finite))) strictEqual(serializer.ast._tag, "Arrays") @@ -2620,11 +2767,11 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` const encoding = asserts.encoding() await encoding.succeed([1, 2], ["1", "2"]) - await encoding.fail(1 as any, "Expected array, got 1") + await encoding.fail(1 as any, "Expected array") const decoding = asserts.decoding() await decoding.succeed("1", [1]) - await decoding.fail("1,2", `Expected a string representing a finite number, got "1,2"\n at [0]`) + await decoding.fail("1,2", `Expected a string representing a finite number\n at [0]`) await decoding.succeed(["1", "2"], [1, 2]) }) @@ -2673,8 +2820,15 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` await decoding.succeed([["1", "2"]], [[1, 2]]) }) - it("is idempotent", () => { + it("preserves array-from-single encoding when converting to StringTree again", () => { const schema = Schema.toCodecArrayFromSingle(Schema.toCodecStringTree(Schema.Array(Schema.Finite))) + strictEqual(Schema.toCodecStringTree(schema).ast, schema.ast) + }) + + it("is idempotent", () => { + const schema = Schema.toCodecArrayFromSingle( + Schema.toCodecStringTree(Schema.suspend(() => Schema.Array(Schema.Finite))) + ) strictEqual(schema.ast, Schema.toCodecArrayFromSingle(schema).ast) }) }) @@ -2688,7 +2842,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` async function assertXmlFailure(schema: Schema.Codec, value: T, message: string) { const serializer = Schema.toEncoderXml(Schema.toCodecStringTree(schema)) const r = await serializer(value).pipe( - Effect.mapError((err) => err.issue.toString()), + Effect.mapError((err) => formatIssue(err.issue)), Effect.result, Effect.runPromise ) @@ -2696,16 +2850,19 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` } describe("Schemas without annotations", () => { - it("Declaration", async () => { - await assertXml(Schema.instanceOf(URL), new URL("https://effect.website"), "") + it("Declaration", () => { + throws( + () => Schema.toEncoderXml(Schema.instanceOf(URL)), + "Missing structural codec for StringTree" + ) }) it("Unknown", async () => { - await assertXml(Schema.Unknown, "value", "") + await assertXml(Schema.Unknown, "value", "value") }) it("ObjectKeyword", async () => { - await assertXml(Schema.ObjectKeyword, { a: "value" }, "") + await assertXml(Schema.ObjectKeyword, { a: "value" }, "\n value\n") }) }) @@ -2735,7 +2892,7 @@ Expected "Infinity" | "-Infinity" | "NaN", got "a"` }) it("Never", async () => { - await assertXmlFailure(Schema.Never, "test", `Expected never, got "test"`) + await assertXmlFailure(Schema.Never, "test", `Expected never`) }) it("Any", async () => { diff --git a/.context/effect/packages/effect/test/schema/toDifferJsonPatch.test.ts b/.context/effect/packages/effect/test/schema/toDifferJsonPatch.test.ts index f71a14a87..9e35f6444 100644 --- a/.context/effect/packages/effect/test/schema/toDifferJsonPatch.test.ts +++ b/.context/effect/packages/effect/test/schema/toDifferJsonPatch.test.ts @@ -2,7 +2,7 @@ import { Schema } from "effect" import * as DateTime from "effect/DateTime" import * as FastCheck from "effect/testing/FastCheck" import { describe, it } from "vitest" -import { deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" +import { assertSchemaIssueError, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" /** * This suite intentionally avoids re-testing generic JSON Patch behavior @@ -18,7 +18,7 @@ import { deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" function roundtrip(codec: Schema.Codec) { const differ = Schema.toDifferJsonPatch(codec) - const arbitrary = Schema.toArbitrary(codec) + const arbitrary = Schema.toArbitrary(codec)(FastCheck) const arb = arbitrary.filter((v) => { // avoid prototype-poisoning-ish values that aren't valid JSON-ish containers for patching if ( @@ -97,13 +97,12 @@ describe("Schema.toDifferJsonPatch", () => { deepStrictEqual(differ.patch(-0, [{ op: "replace", path: "", value: 0 }]), 0) }) - it("Date: encodes invalid Date as a string on diff", () => { + it("Date: rejects an invalid Date on diff", () => { const differ = Schema.toDifferJsonPatch(Schema.Date) - deepStrictEqual( - differ.diff(new Date("1970-01-01T00:00:00.000Z"), new Date(NaN)), - [{ op: "replace", path: "", value: "Invalid Date" }] - ) + throws(() => differ.diff(new Date("1970-01-01T00:00:00.000Z"), new Date(NaN)), (error) => { + assertSchemaIssueError(error, "Expected a valid Date") + }) }) it("Defect: diff encodes an Error to a plain object; patch decodes back to Error", () => { @@ -218,13 +217,12 @@ describe("Schema.toDifferJsonPatch", () => { roundtrip(Schema.RegExp) roundtrip(Schema.Duration) roundtrip(Schema.DateTimeUtc) - roundtrip(Schema.DateValid) roundtrip(Schema.Uint8Array) roundtrip(Schema.PropertyKey) roundtrip(Schema.Option(Schema.String)) roundtrip(Schema.Result(Schema.Number, Schema.String)) roundtrip(Schema.ReadonlyMap(Schema.String, Schema.Number)) - roundtrip(Schema.Error()) + roundtrip(Schema.ErrorInstance()) roundtrip(Schema.Json) roundtrip(Schema.Exit(Schema.Number, Schema.String, Schema.Json)) @@ -232,7 +230,7 @@ describe("Schema.toDifferJsonPatch", () => { class B extends Schema.Class("B")({ a: A }) {} roundtrip(B) - class E extends Schema.ErrorClass("E")({ message: Schema.String }) {} + class E extends Schema.Error("E")({ message: Schema.String }) {} roundtrip(E) }) }) diff --git a/.context/effect/packages/effect/test/schema/toEquivalence.test.ts b/.context/effect/packages/effect/test/schema/toEquivalence.test.ts index a11d05951..4f4ec812b 100644 --- a/.context/effect/packages/effect/test/schema/toEquivalence.test.ts +++ b/.context/effect/packages/effect/test/schema/toEquivalence.test.ts @@ -1,6 +1,17 @@ -import { BigDecimal, DateTime, Duration, Equivalence, HashMap, Option, Redacted, Result, Schema } from "effect" +import { + BigDecimal, + DateTime, + Duration, + Equivalence, + HashMap, + Option, + Redacted, + Result, + Schema, + SchemaGetter +} from "effect" import { describe, it } from "vitest" -import { assertFalse, assertTrue, throws } from "../utils/assert.ts" +import { assertFalse, assertTrue, strictEqual } from "../utils/assert.ts" const Modulo2 = Schema.Number.annotate({ toEquivalence: (): Equivalence.Equivalence => Equivalence.make((a, b) => a % 2 === b % 2) @@ -12,22 +23,11 @@ const Modulo3 = Schema.Number.annotate({ describe("toEquivalence", () => { it("Never", () => { - throws( - () => - Schema.toEquivalence(Schema.Struct({ - a: Schema.Never - })), - `Unsupported AST Never - at ["a"]` - ) - throws( - () => - Schema.toEquivalence(Schema.Tuple([ - Schema.Never - ])), - `Unsupported AST Never - at [0]` - ) + const equivalence = Schema.toEquivalence(Schema.Never) + const value = {} as never + + assertTrue(equivalence(value, value)) + assertFalse(equivalence({} as never, {} as never)) }) it("String", () => { @@ -343,6 +343,54 @@ describe("toEquivalence", () => { }) }) + it("precompiles Union members", () => { + let derivations = 0 + const member = Schema.Struct({ + tag: Schema.Literal("a"), + value: Schema.String + }).pipe( + Schema.overrideToEquivalence(() => { + derivations++ + return Equivalence.make((a, b) => a.value === b.value) + }) + ) + const equivalence = Schema.toEquivalence(Schema.Union([member, Schema.Never])) + + strictEqual(derivations, 1) + assertTrue(equivalence({ tag: "a", value: "a" }, { tag: "a", value: "a" })) + assertFalse(equivalence({ tag: "a", value: "a" }, { tag: "a", value: "b" })) + strictEqual(derivations, 1) + }) + + it("selects transformed Union members on the Type side", () => { + const Target = Schema.Struct({ value: Schema.String }).pipe( + Schema.overrideToEquivalence(() => Equivalence.make((a, b) => a.value === b.value)) + ) + const Transformed = Schema.String.pipe( + Schema.decodeTo(Target, { + decode: SchemaGetter.transform((s) => ({ value: s })), + encode: SchemaGetter.transform((a) => a.value) + }) + ) + const equivalence = Schema.toEquivalence(Schema.Union([Transformed, Schema.Boolean])) + + assertTrue(equivalence({ value: "a" }, { value: "a" })) + assertFalse(equivalence({ value: "a" }, { value: "b" })) + }) + + it("preserves Union member equivalence annotations", () => { + const member = Schema.String.pipe( + Schema.flip, + Schema.check(Schema.makeFilter(() => true)), + Schema.flip, + Schema.overrideToEquivalence(() => Equivalence.make((a, b) => a[0] === b[0])) + ) + const equivalence = Schema.toEquivalence(Schema.Union([member, Schema.Number])) + + assertTrue(equivalence("ab", "ac")) + assertFalse(equivalence("ab", "bc")) + }) + it("Date", () => { const schema = Schema.Date const equivalence = Schema.toEquivalence(schema) diff --git a/.context/effect/packages/effect/test/schema/toFormatter.test.ts b/.context/effect/packages/effect/test/schema/toFormatter.test.ts index 821e218c5..49c2bf031 100644 --- a/.context/effect/packages/effect/test/schema/toFormatter.test.ts +++ b/.context/effect/packages/effect/test/schema/toFormatter.test.ts @@ -1,4 +1,4 @@ -import { BigDecimal, DateTime, Duration, HashMap, Option, Redacted, Result, Schema } from "effect" +import { BigDecimal, DateTime, Duration, HashMap, Option, Redacted, Result, Schema, SchemaGetter } from "effect" import { describe, it } from "vitest" import { strictEqual } from "../utils/assert.ts" @@ -152,6 +152,63 @@ describe("toFormatter", () => { strictEqual(format(1), "1") }) + it("precompiles Union members", () => { + let derivations = 0 + const String = Schema.String.pipe( + Schema.overrideToFormatter(() => { + derivations++ + return (s) => s.toUpperCase() + }) + ) + const format = Schema.toFormatter(Schema.Union([String, Schema.Number, Schema.Never])) + + strictEqual(derivations, 1) + strictEqual(format("a"), "A") + strictEqual(format("b"), "B") + strictEqual(derivations, 1) + }) + + it("selects transformed Union members on the Type side", () => { + const Target = Schema.Struct({ value: Schema.String }).pipe( + Schema.overrideToFormatter(() => (a) => `formatted:${a.value}`) + ) + const Transformed = Schema.String.pipe( + Schema.decodeTo(Target, { + decode: SchemaGetter.transform((s) => ({ value: s })), + encode: SchemaGetter.transform((a) => a.value) + }) + ) + const format = Schema.toFormatter(Schema.Union([Transformed, Schema.Boolean])) + + strictEqual(format({ value: "a" }), "formatted:a") + }) + + it("preserves Union member formatter annotations", () => { + const member = Schema.String.pipe( + Schema.flip, + Schema.check(Schema.makeFilter(() => true)), + Schema.flip, + Schema.overrideToFormatter(() => (s) => s.toUpperCase()) + ) + const format = Schema.toFormatter(Schema.Union([member, Schema.Number])) + + strictEqual(format("a"), "A") + }) + + it("preserves Union member encoding metadata for onBefore", () => { + const member = Schema.String.pipe( + Schema.decode({ + decode: SchemaGetter.transform((s) => s), + encode: SchemaGetter.transform((s) => s) + }) + ) + const format = Schema.toFormatter(Schema.Union([member, Schema.Number]), { + onBefore: (ast) => ast.encoding ? () => "transformed" : undefined + }) + + strictEqual(format("a"), "transformed") + }) + describe("Tuple", () => { it("empty", () => { const format = Schema.toFormatter(Schema.Tuple([])) diff --git a/.context/effect/packages/effect/test/schema/toIso.test.ts b/.context/effect/packages/effect/test/schema/toIso.test.ts index caae1e745..d03bad3b3 100644 --- a/.context/effect/packages/effect/test/schema/toIso.test.ts +++ b/.context/effect/packages/effect/test/schema/toIso.test.ts @@ -1,21 +1,16 @@ -import { - Cause, - Data, - Exit, - HashMap, - Option, - Predicate, - Record, - Result, - Schema, - SchemaTransformation, - SchemaUtils -} from "effect" +import { Cause, Exit, HashMap, Option, Predicate, Record, Result, Schema, SchemaTransformation } from "effect" import { describe, it } from "vitest" -import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts" +import { + assertNone, + assertSchemaIssueError, + assertSome, + deepStrictEqual, + strictEqual, + throws +} from "../utils/assert.ts" class Value extends Schema.Class("Value")({ - a: Schema.DateValid + a: Schema.Date }) {} function addOne(date: Date): Date { @@ -79,7 +74,9 @@ describe("Optic generation", () => { const modify = optic.modify((n) => schema.make(n - 1)) strictEqual(modify(schema.make(2)), 1) - throws(() => modify(schema.make(1)), "Expected a value greater than 0, got 0") + throws(() => modify(schema.make(1)), (error) => { + assertSchemaIssueError(error, "Expected a value greater than 0") + }) }) }) @@ -321,7 +318,7 @@ describe("Optic generation", () => { }) it("Error", () => { - const schema = Schema.Error() + const schema = Schema.ErrorInstance() const optic = Schema.toIso(schema) const modify = optic.modify((e) => new Error(e.message + "!")) @@ -329,7 +326,7 @@ describe("Optic generation", () => { }) it("Exit", () => { - const schema = Schema.Exit(Value, Schema.Error(), Schema.Defect()) + const schema = Schema.Exit(Value, Schema.ErrorInstance(), Schema.Defect()) const optic = Schema.toIso(schema).tag("Success").key("value").key("a") const modify = optic.modify(addOne) @@ -374,21 +371,5 @@ describe("Optic generation", () => { HashMap.toEntries(HashMap.make(["a", Value.make({ a: new Date(1) })])) ) }) - - it("getNativeClassSchema", () => { - const Props = Schema.Struct({ - message: Schema.String - }) - class Err extends Data.Error { - constructor(props: typeof Props.Type) { - super(Props.make(props)) - } - } - const schema = SchemaUtils.getNativeClassSchema(Err, { encoding: Props }) - const optic = Schema.toIso(schema) - const modify = optic.modify((e) => new Err({ message: e.message + "!" })) - - deepStrictEqual(modify(new Err({ message: "a" })), new Err({ message: "a!" })) - }) }) }) diff --git a/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts b/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts index dc8e145d8..333e73b05 100644 --- a/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts +++ b/.context/effect/packages/effect/test/schema/toJsonSchemaDocument.test.ts @@ -43,7 +43,7 @@ function assertJsonSchemaDocument( const valid = ajvDraft2020_12.validateSchema(jsonSchema) assertTrue(valid) // const validate = ajvDraft2020_12.compile(jsonSchema) - // const arb = Schema.toArbitrary(schema) + // const arb = Schema.toArbitrary(schema)(FastCheck) // const codec = Schema.toCodecJson(schema) // const encode = Schema.encodeSync(codec) // FastCheck.assert(FastCheck.property(arb, (t) => { @@ -53,26 +53,139 @@ function assertJsonSchemaDocument( } describe("toJsonSchemaDocument", () => { - describe("Unsupported schemas", () => { - it("Tuple: unsupported post-rest elements", () => { + describe("unsupported schemas", () => { + it("rejects tuple post-rest elements", () => { assertUnsupportedSchema( Schema.TupleWithRest(Schema.Tuple([]), [Schema.Finite, Schema.String]), - "Generating a JSON Schema for post-rest elements is not supported" + `Invalid schema representation document\n at ["representation"]["rest"]` ) }) - it("Struct: unsupported property signature name", () => { + it("rejects symbol property names", () => { const a = Symbol.for("effect/Schema/test/a") assertUnsupportedSchema( Schema.Struct({ [a]: Schema.String }), - `Unsupported property signature name: Symbol(effect/Schema/test/a)` + "Objects property names must be strings" ) }) + }) - it("Record: unsupported index signature parameter", () => { - assertUnsupportedSchema( - Schema.Record(Schema.Symbol, Schema.Finite), - `Unsupported index signature parameter: Symbol` + it("Record(Symbol, Finite)", () => { + assertJsonSchemaDocument(Schema.Record(Schema.Symbol, Schema.Finite), { + schema: { + type: "object", + patternProperties: { + "^Symbol\\((.*)\\)$": { type: "number" } + } + } + }) + }) + + it("emits content annotations", () => { + assertJsonSchemaDocument( + Schema.String.annotate({ + description: "encoded payload", + contentMediaType: "application/json", + contentSchema: { type: "number" } + }), + { + schema: { + type: "string", + description: "encoded payload", + contentMediaType: "application/json", + contentSchema: { type: "number" } + } + } + ) + }) + + describe("reference extraction", () => { + it("preserves shared non-trivial schemas with references", () => { + const shared = Schema.Struct({ value: Schema.String }) + + assertJsonSchemaDocument( + Schema.Struct({ left: shared, right: shared }), + { + schema: { + type: "object", + properties: { + left: { $ref: "#/$defs/Objects_" }, + right: { $ref: "#/$defs/Objects_" } + }, + required: ["left", "right"], + additionalProperties: false + }, + definitions: { + Objects_: { + type: "object", + properties: { + value: { type: "string" } + }, + required: ["value"], + additionalProperties: false + } + } + } + ) + }) + + it("preserves repeated optional structural schemas with references", () => { + const shared = Schema.Struct({ value: Schema.String }) + + assertJsonSchemaDocument( + Schema.Struct({ left: Schema.optional(shared), right: Schema.optional(shared) }), + { + schema: { + type: "object", + properties: { + left: { $ref: "#/$defs/Union_" }, + right: { $ref: "#/$defs/Union_" } + }, + additionalProperties: false + }, + definitions: { + Union_: { + anyOf: [ + { + type: "object", + properties: { + value: { type: "string" } + }, + required: ["value"], + additionalProperties: false + }, + { type: "null" } + ] + } + } + } + ) + }) + + it("inlines shared canonical unions of leaf schemas", () => { + assertJsonSchemaDocument( + Schema.Struct({ left: Schema.Number, right: Schema.Number }), + { + schema: { + type: "object", + properties: { + left: { + anyOf: [ + { type: "number" }, + { type: "string", enum: ["Infinity", "-Infinity", "NaN"] } + ] + }, + right: { + anyOf: [ + { type: "number" }, + { type: "string", enum: ["Infinity", "-Infinity", "NaN"] } + ] + } + }, + required: ["left", "right"], + additionalProperties: false + } + } ) }) }) @@ -237,31 +350,6 @@ describe("toJsonSchemaDocument", () => { ) }) - it("does not overwrite generated contentSchema with the raw annotation", () => { - assertJsonSchemaDocument( - Schema.fromJsonString(Schema.Struct({ - a: Schema.String - })), - { - schema: { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "type": "object", - "properties": { - "a": { - "type": "string" - } - }, - "required": ["a"], - "additionalProperties": false - } - } - }, - { includeAnnotationKey: (key) => key === "contentSchema" } - ) - }) - it("passthroughs at property level in structs", () => { const schema = Schema.Struct({ name: Schema.String.annotate({ @@ -345,7 +433,7 @@ describe("toJsonSchemaDocument", () => { }) }) - it("should support JSON Schema annotations", () => { + it("emits standard annotations", () => { const schema = Schema.String.annotate({ title: "a", description: "b", @@ -367,8 +455,8 @@ describe("toJsonSchemaDocument", () => { }) }) - describe("identifier handling", () => { - it(`refs should escape "~" and "/"`, () => { + describe("identifiers", () => { + it(`escapes "~" and "/" in JSON Pointer references`, () => { const S = Schema.String.annotate({ identifier: "id~a/b" }) assertJsonSchemaDocument( S, @@ -381,7 +469,7 @@ describe("toJsonSchemaDocument", () => { ) }) - it("using the same identifier annotated schema twice", () => { + it("reuses a definition for repeated occurrences of the same identified AST", () => { const S = Schema.String.annotate({ identifier: "id" }) assertJsonSchemaDocument( Schema.Union([S, S]), @@ -399,26 +487,26 @@ describe("toJsonSchemaDocument", () => { ) }) - it("should handle duplicate identifiers on different schemas with different representations", () => { + it("suffixes duplicate identifiers on different schemas", () => { const S = Schema.Union([ Schema.String.annotate({ identifier: "id", description: "a" }), Schema.String.annotate({ identifier: "id", description: "b" }) ]) assertJsonSchemaDocument(S, { schema: { - "anyOf": [ - { "$ref": "#/$defs/id" }, - { "$ref": "#/$defs/id1" } + anyOf: [ + { $ref: "#/$defs/id" }, + { $ref: "#/$defs/id_1" } ] }, definitions: { - id: { "type": "string", "description": "a" }, - id1: { "type": "string", "description": "b" } + id: { type: "string", description: "a" }, + id_1: { type: "string", description: "b" } } }) }) - it("should handle duplicate identifiers on different schemas with the same representation", () => { + it("reuses one definition when the same identified AST appears in different schema shapes", () => { const X = Schema.String.annotate({ title: "X", identifier: "X" }) const S = Schema.Struct({ a: X, @@ -483,23 +571,17 @@ describe("toJsonSchemaDocument", () => { }) describe("Declaration", () => { - it("Date", () => { - const schema = Schema.Date - assertJsonSchemaDocument(schema, { - schema: { - "type": "string" - } + it("opaque Declaration", () => { + assertJsonSchemaDocument(Schema.instanceOf(URL), { + schema: {} }) }) - it("DateValid", () => { - const schema = Schema.DateValid + it("Date", () => { + const schema = Schema.Date assertJsonSchemaDocument(schema, { schema: { - "type": "string", - "allOf": [ - { "format": "date-time" } - ] + "type": "string" } }) }) @@ -514,7 +596,7 @@ describe("toJsonSchemaDocument", () => { }) it("Error", () => { - const schema = Schema.Error() + const schema = Schema.ErrorInstance() assertJsonSchemaDocument(schema, { schema: { "type": "object", @@ -704,9 +786,7 @@ describe("toJsonSchemaDocument", () => { assertJsonSchemaDocument( schema.annotate({ description: "a" }), { - schema: { - "description": "a" - } + schema: {} } ) }) @@ -725,8 +805,7 @@ describe("toJsonSchemaDocument", () => { schema.annotate({ description: "a" }), { schema: { - "type": "null", - "description": "a" + "type": "null" } } ) @@ -746,8 +825,7 @@ describe("toJsonSchemaDocument", () => { schema.annotate({ description: "a" }), { schema: { - "type": "null", - "description": "a" + "type": "null" } } ) @@ -864,7 +942,7 @@ describe("toJsonSchemaDocument", () => { ) }) - it("should ignore annotateKey annotations if the schema is not contextual", () => { + it("ignores annotateKey annotations when the schema is not contextual", () => { assertJsonSchemaDocument( Schema.String.annotateKey({ description: "a" @@ -1071,6 +1149,23 @@ describe("toJsonSchemaDocument", () => { }) }) + it("escapes regexp syntax in literal string checks", () => { + for ( + const [check, pattern] of [ + [Schema.isStartsWith("a.b"), "^a\\.b"], + [Schema.isEndsWith("a+b"), "a\\+b$"], + [Schema.isIncludes("["), "\\["] + ] as const + ) { + assertJsonSchemaDocument(Schema.String.check(check), { + schema: { + "type": "string", + "allOf": [{ pattern }] + } + }) + } + }) + it("isTrimmed", () => { const schema = Schema.Trimmed assertJsonSchemaDocument(schema, { @@ -1373,9 +1468,7 @@ describe("toJsonSchemaDocument", () => { schema: { "anyOf": [ { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] } ] } } @@ -1386,11 +1479,32 @@ describe("toJsonSchemaDocument", () => { schema: { "anyOf": [ { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } - ], - "description": "a" + { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] } + ] + } + } + ) + }) + + it("Number & annotateKey", () => { + assertJsonSchemaDocument( + Schema.Struct({ + value: Schema.Number.annotateKey({ description: "the field" }) + }), + { + schema: { + type: "object", + properties: { + value: { + anyOf: [ + { type: "number" }, + { type: "string", enum: ["Infinity", "-Infinity", "NaN"] } + ], + allOf: [{ description: "the field" }] + } + }, + required: ["value"], + additionalProperties: false } } ) @@ -1639,7 +1753,7 @@ describe("toJsonSchemaDocument", () => { assertJsonSchemaDocument( schema, { - schema: { anyOf: [{ type: "object" }, { type: "array" }] } + schema: { anyOf: [{ type: "array" }, { type: "object" }] } } ) assertJsonSchemaDocument( @@ -1647,10 +1761,9 @@ describe("toJsonSchemaDocument", () => { { schema: { "anyOf": [ - { "type": "object" }, - { "type": "array" } - ], - "description": "a" + { "type": "array" }, + { "type": "object" } + ] } } ) @@ -1742,8 +1855,7 @@ describe("toJsonSchemaDocument", () => { { schema: { "type": "string", - "enum": ["1"], - "description": "a" + "enum": ["1"] } } ) @@ -3543,16 +3655,13 @@ describe("toJsonSchemaDocument", () => { { schema: { "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "type": "string" - } + "contentMediaType": "application/json" } } ) }) - it("preserves the content schema identifier", () => { + it("preserves the content schema identifier as a canonical reference", () => { const MyEvent = Schema.Struct({ value: Schema.String }).annotate({ identifier: "MyEvent" }) @@ -3561,27 +3670,12 @@ describe("toJsonSchemaDocument", () => { Schema.fromJsonString(MyEvent), { schema: { - "$ref": "#/$defs/MyEventJsonString" + "$ref": "#/$defs/MyEventEncoded" }, definitions: { - "MyEvent": { - "type": "object", - "properties": { - "value": { - "type": "string" - } - }, - "required": [ - "value" - ], - "additionalProperties": false - }, - "MyEventJsonString": { + "MyEventEncoded": { "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "$ref": "#/$defs/MyEvent" - } + "contentMediaType": "application/json" } } } @@ -3603,54 +3697,9 @@ describe("toJsonSchemaDocument", () => { "$ref": "#/$defs/MyWireEvent" }, definitions: { - "MyEvent": { - "type": "object", - "properties": { - "value": { - "type": "string" - } - }, - "required": [ - "value" - ], - "additionalProperties": false - }, "MyWireEvent": { "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "$ref": "#/$defs/MyEvent" - } - } - } - } - ) - }) - - it("nested fromJsonString", () => { - assertJsonSchemaDocument( - Schema.fromJsonString(Schema.Struct({ - a: Schema.fromJsonString(Schema.FiniteFromString) - })), - { - schema: { - "type": "string", - "contentMediaType": "application/json", - "contentSchema": { - "additionalProperties": false, - "properties": { - "a": { - "contentMediaType": "application/json", - "contentSchema": { - "type": "string" - }, - "type": "string" - } - }, - "required": [ - "a" - ], - "type": "object" + "contentMediaType": "application/json" } } } @@ -3658,7 +3707,7 @@ describe("toJsonSchemaDocument", () => { }) }) - it("Class", () => { + it("Class preserves its identifier as a canonical reference", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} @@ -3666,10 +3715,10 @@ describe("toJsonSchemaDocument", () => { A, { schema: { - "$ref": "#/$defs/A" + "$ref": "#/$defs/AEncoded" }, definitions: { - A: { + "AEncoded": { "type": "object", "properties": { "a": { "type": "string" } @@ -3678,20 +3727,21 @@ describe("toJsonSchemaDocument", () => { "additionalProperties": false } } - } + }, + { includeAnnotationKey: () => true } ) }) - it("ErrorClass", () => { - class E extends Schema.ErrorClass("E")({ + it("Error preserves its identifier as a canonical reference", () => { + class E extends Schema.Error("E")({ a: Schema.String }) {} assertJsonSchemaDocument(E, { schema: { - "$ref": "#/$defs/E" + "$ref": "#/$defs/EEncoded" }, definitions: { - E: { + "EEncoded": { "type": "object", "properties": { "a": { "type": "string" } diff --git a/.context/effect/packages/effect/test/schema/toStandardSchemaV1.test.ts b/.context/effect/packages/effect/test/schema/toStandardSchemaV1.test.ts index 33a26062d..b4d7b949a 100644 --- a/.context/effect/packages/effect/test/schema/toStandardSchemaV1.test.ts +++ b/.context/effect/packages/effect/test/schema/toStandardSchemaV1.test.ts @@ -120,13 +120,13 @@ describe("toStandardSchemaV1", () => { expectSyncSuccess(standardSchema, "a", "a") expectSyncFailure(standardSchema, null, [ { - message: "Expected string, got null", + message: "Expected string", path: [] } ]) expectSyncFailure(standardSchema, "", [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: [] } ]) @@ -138,13 +138,13 @@ describe("toStandardSchemaV1", () => { await expectAsyncSuccess(standardSchema, "a", "a") expectSyncFailure(standardSchema, null, [ { - message: "Expected string, got null", + message: "Expected string", path: [] } ]) await expectAsyncFailure(standardSchema, "", [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: [] } ]) @@ -204,29 +204,29 @@ describe("toStandardSchemaV1", () => { expectSyncSuccess(standardSchema, { a: "a", b: "b" }, { a: "a", b: "b" }) expectSyncFailure(standardSchema, null, [ { - message: "Expected object, got null", + message: "Expected object", path: [] } ]) expectSyncFailure(standardSchema, { a: "a", b: "" }, [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: ["b"] } ]) expectSyncFailure(standardSchema, { a: "", b: "b" }, [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: ["a"] } ]) expectSyncFailure(standardSchema, { a: "", b: "" }, [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: ["a"] }, { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: ["b"] } ]) @@ -240,29 +240,12 @@ describe("toStandardSchemaV1", () => { const standardSchema = Schema.toStandardSchemaV1(schema, { parseOptions: { errors: "first" } }) expectSyncFailure(standardSchema, { a: "", b: "" }, [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: ["a"] } ]) }) - describe("Structural checks", () => { - it("Array + isMinLength", () => { - const schema = Schema.Struct({ - tags: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(3)) - }) - - const standardSchema = Schema.toStandardSchemaV1(schema) - expectSyncFailure(standardSchema, { tags: ["a", ""] }, [{ - message: `Expected a value with a length of at least 1, got ""`, - path: ["tags", 1] - }, { - message: `Expected a value with a length of at least 3, got ["a",""]`, - path: ["tags"] - }]) - }) - }) - describe("should respect the `message` annotation", () => { describe("String", () => { it("String & annotation", () => { @@ -292,7 +275,7 @@ describe("toStandardSchemaV1", () => { const standardSchema = Schema.toStandardSchemaV1(schema) expectSyncFailure(standardSchema, null, [ { - message: "Expected string, got null", + message: "Expected string", path: [] } ]) @@ -309,7 +292,7 @@ describe("toStandardSchemaV1", () => { const standardSchema = Schema.toStandardSchemaV1(schema) expectSyncFailure(standardSchema, null, [ { - message: "Expected string, got null", + message: "Expected string", path: [] } ]) @@ -473,7 +456,7 @@ describe("toStandardSchemaV1", () => { }) expectSyncFailure(standardSchema, null, [ { - message: "Expected string, got null", + message: "Expected string", path: [] } ]) @@ -486,7 +469,7 @@ describe("toStandardSchemaV1", () => { }) expectSyncFailure(standardSchema, "", [ { - message: `Expected a value with a length of at least 1, got ""`, + message: `Expected a value with a length of at least 1`, path: [] } ]) diff --git a/.context/effect/packages/effect/test/schema/v3-v4.test.ts b/.context/effect/packages/effect/test/schema/v3-v4.test.ts index 2e1a6085d..8bad7da1f 100644 --- a/.context/effect/packages/effect/test/schema/v3-v4.test.ts +++ b/.context/effect/packages/effect/test/schema/v3-v4.test.ts @@ -38,7 +38,7 @@ describe("v3 -> v4 migration tests", () => { ) await encoding.fail( { a: undefined }, - `Expected number, got undefined + `Expected number at ["a"]` ) }) @@ -68,7 +68,7 @@ describe("v3 -> v4 migration tests", () => { await decoding.succeed({}, { a: -1 }) await decoding.fail( { a: undefined }, - `Expected string, got undefined + `Expected string at ["a"]` ) @@ -81,7 +81,7 @@ describe("v3 -> v4 migration tests", () => { ) await encoding.fail( { a: undefined }, - `Expected number, got undefined + `Expected number at ["a"]` ) }) @@ -118,7 +118,7 @@ describe("v3 -> v4 migration tests", () => { await encoding.succeed({}) await encoding.fail( { a: null }, - `Expected number | undefined, got null + `Expected number | undefined at ["a"]` ) }) @@ -149,7 +149,7 @@ describe("v3 -> v4 migration tests", () => { await decoding.succeed({ a: null }, {}) await decoding.fail( { a: undefined }, - `Expected string | null, got undefined + `Expected string | null at ["a"]` ) @@ -158,7 +158,7 @@ describe("v3 -> v4 migration tests", () => { await encoding.succeed({}) await encoding.fail( { a: undefined }, - `Expected number, got undefined + `Expected number at ["a"]` ) }) @@ -229,7 +229,7 @@ describe("v3 -> v4 migration tests", () => { await decoding.succeed({ a: null }, { a: -1 }) await decoding.fail( { a: undefined }, - `Expected string | null, got undefined + `Expected string | null at ["a"]` ) @@ -237,7 +237,7 @@ describe("v3 -> v4 migration tests", () => { await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.fail( { a: undefined }, - `Expected number, got undefined + `Expected number at ["a"]` ) await encoding.fail( diff --git a/.context/effect/packages/effect/test/testing/TestSchema.test.ts b/.context/effect/packages/effect/test/testing/TestSchema.test.ts index 4e8006d87..fc78c7c5f 100644 --- a/.context/effect/packages/effect/test/testing/TestSchema.test.ts +++ b/.context/effect/packages/effect/test/testing/TestSchema.test.ts @@ -1,4 +1,4 @@ -import { Context, Effect, Option, Schema, SchemaGetter, SchemaIssue } from "effect" +import { Context, Effect, Schema, SchemaGetter, SchemaIssue } from "effect" import { TestSchema } from "effect/testing" import { describe, it } from "vitest" @@ -8,8 +8,8 @@ describe("TestSchema", () => { const assert = new TestSchema.Asserts(schema) const decoding = assert.decoding() await decoding.succeed("1", 1) - await decoding.fail("-1", `Expected a value greater than 0, got -1`) - await decoding.fail("a", `Expected a finite number, got NaN`) + await decoding.fail("-1", `Expected a value greater than 0`) + await decoding.fail("a", `Expected a finite number`) }) it("decoding.provide", async () => { @@ -21,7 +21,7 @@ describe("TestSchema", () => { Effect.gen(function*() { yield* Service if (s.length === 0) { - return new SchemaIssue.InvalidValue(Option.some(s), { + return new SchemaIssue.InvalidValue({ message: "input should not be empty string" }) } @@ -42,7 +42,7 @@ describe("TestSchema", () => { const assert = new TestSchema.Asserts(schema) const encoding = assert.encoding() await encoding.succeed(1, "1") - await encoding.fail(-1, `Expected a value greater than 0, got -1`) + await encoding.fail(-1, `Expected a value greater than 0`) }) it("encoding.provide", async () => { @@ -55,7 +55,7 @@ describe("TestSchema", () => { Effect.gen(function*() { yield* Service if (s.length === 0) { - return new SchemaIssue.InvalidValue(Option.some(s), { + return new SchemaIssue.InvalidValue({ message: "input should not be empty string" }) } diff --git a/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts index 85be5842f..3df2d58d3 100644 --- a/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutput.test.ts @@ -13,20 +13,30 @@ function assertError(schema: Schema.Constraint, message: string) { } describe("toCodecAnthropic", () => { - describe("Unsupported", () => { - it("Undefined", () => { - assertError(Schema.Undefined, "Unsupported AST Undefined") + describe("Canonical JSON and mechanical invariants", () => { + it("encodes Undefined as null", () => { + assertJsonSchema(Schema.Undefined, { type: "null" }) }) - it("Literal with unsupported type", () => { - assertError(Schema.Literal(1n), "Unsupported literal type bigint") + it("encodes a bigint Literal as a string", () => { + assertJsonSchema(Schema.Literal(1n), { type: "string", enum: ["1"] }) }) describe("Arrays", () => { - it("post-rest elements", () => { - assertError( + it("encodes post-rest elements as object properties", () => { + assertJsonSchema( Schema.TupleWithRest(Schema.Tuple([]), [Schema.String, Schema.String]), - "Post-rest elements are not supported for arrays" + { + type: "object", + properties: { + __rest__: { type: "array", items: { type: "string" } }, + __tail_0__: { type: "string" } + }, + required: ["__rest__", "__tail_0__"], + additionalProperties: false, + description: + "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements. Post-rest elements use '__tail_0__', '__tail_1__', and so on" + } ) }) }) @@ -35,22 +45,25 @@ describe("toCodecAnthropic", () => { it("non-string property signature name", () => { assertError( Schema.Struct({ [Symbol.for("effect/Schema/test/a")]: Schema.String }), - "Property names must be strings" + "Objects property names must be strings" ) }) }) - it("Suspend", () => { + it("non-recursive Suspend", () => { + assertJsonSchema(Schema.suspend(() => Schema.String), { type: "string" }) + }) + + it("rejects recursive Suspend", () => { interface A { readonly a: string readonly as: ReadonlyArray } const schema = Schema.Struct({ - a: Schema.String, + a: Schema.String.check(Schema.isStartsWith("a")), as: Schema.Array(Schema.suspend((): Schema.Codec => schema)) }) - assertError(Schema.suspend(() => Schema.String), "Unsupported AST Suspend") - assertError(schema, "Unsupported AST Suspend") + assertError(schema, "AnthropicStructuredOutput: Recursive schemas are not supported") }) }) @@ -146,9 +159,7 @@ describe("toCodecAnthropic", () => { assertJsonSchema(Schema.Number, { "anyOf": [ { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] } ] }) }) @@ -164,10 +175,10 @@ describe("toCodecAnthropic", () => { }) }) - it("Finite + supported format", () => { + it("Finite + string format", () => { assertJsonSchema(Schema.Finite.annotate({ format: "duration" }), { "type": "number", - "format": "duration" + "description": "a value with a format of duration" }) }) @@ -204,10 +215,10 @@ describe("toCodecAnthropic", () => { }) }) - it("Int + supported format", () => { + it("Int + string format", () => { assertJsonSchema(Schema.Int.annotate({ format: "duration" }), { "type": "integer", - "format": "duration" + "description": "a value with a format of duration" }) }) @@ -378,9 +389,11 @@ describe("toCodecAnthropic", () => { const encoding = asserts.encoding() await encoding.succeed(["a", 1], { "0": "a", "1": 1 }) + await encoding.succeed(["a"], { "0": "a", "1": null }) const decoding = asserts.decoding() await decoding.succeed({ "0": "a", "1": 1 }, ["a", 1]) + await decoding.succeed({ "0": "a", "1": null }, ["a"]) }) }) @@ -477,7 +490,7 @@ describe("toCodecAnthropic", () => { "required": ["name"], "additionalProperties": false, "$defs": { - "Person": { + "PersonEncoded": { "type": "object", "properties": { "name": { "type": "string" } @@ -528,6 +541,48 @@ describe("toCodecAnthropic", () => { await decoding.succeed([{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }], { "a": 1, "b": 2 }) }) + it("Record with properties and an index signature", async () => { + const schema = Schema.Record( + Schema.Union([Schema.Literal("fixed"), Schema.String]), + Schema.String + ) + const result = toCodecAnthropic(schema) + assert.deepStrictEqual(result.jsonSchema, { + type: "array", + description: "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object", + items: { + type: "object", + description: + "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements", + properties: { + "0": { + anyOf: [ + { type: "string", enum: ["fixed"] }, + { type: "string" } + ] + }, + "1": { type: "string" } + }, + required: ["0", "1"], + additionalProperties: false + } + }) + + const asserts = new TestSchema.Asserts(result.codec) + await asserts.encoding().succeed( + { fixed: "required", dynamic: "value" }, + [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }] + ) + await asserts.decoding().succeed( + [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }], + { fixed: "required", dynamic: "value" } + ) + assert.strictEqual( + Schema.decodeUnknownExit(result.codec)([{ 0: "dynamic", 1: "value" }])._tag, + "Failure" + ) + }) + it("Record(String, Finite) + description", () => { const schema = Schema.Record(Schema.String, Schema.Finite).annotate({ description: "description" }) assertJsonSchema(schema, { @@ -548,9 +603,10 @@ describe("toCodecAnthropic", () => { }) }) - it("Record(String, Finite) + isMinProperties", () => { + it("Record(String, Finite) + isMinProperties", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(2)) - assertJsonSchema(schema, { + const result = toCodecAnthropic(schema) + assert.deepStrictEqual(result.jsonSchema, { "type": "array", "description": "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object; a value with at least 2 entries", @@ -566,6 +622,10 @@ describe("toCodecAnthropic", () => { "additionalProperties": false } }) + await new TestSchema.Asserts(result.codec).decoding().fail( + [{ 0: "a", 1: 1 }, { 0: "a", 1: 2 }], + `Expected a value with at least 2 entries` + ) }) it("Record(String, Finite) + isMinProperties + description", () => { diff --git a/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutputRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutputRepresentation.test.ts new file mode 100644 index 000000000..aaf584009 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/AnthropicStructuredOutputRepresentation.test.ts @@ -0,0 +1,147 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import { TestSchema } from "effect/testing" +import { toCodecAnthropic } from "effect/unstable/ai/AnthropicStructuredOutput" + +describe("AnthropicStructuredOutput representation v2", () => { + it("projects the encoded side before JSON Schema generation", () => { + assert.strictEqual(toCodecAnthropic(Schema.FiniteFromString).jsonSchema.type, "string") + }) + + it("keeps supported custom JSON Schema filters with a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.startsWith("a"), { + description: "starts with a", + representation: { + id: "test/ai/anthropic/startsWithA", + payload: null + }, + toJsonSchema: () => ({ pattern: "^a" }) + })) + + assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { + type: "string", + description: "starts with a", + allOf: [{ pattern: "^a" }] + }) + }) + + it("drops unsupported custom JSON Schema filters with a description", async () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + description: "at least two characters", + representation: { + id: "test/ai/anthropic/minTwoCharactersWithDescription", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + const result = toCodecAnthropic(schema) + assert.deepStrictEqual(result.jsonSchema, { + type: "string", + description: "at least two characters" + }) + await new TestSchema.Asserts(result.codec).decoding().fail( + "a", + `Expected ` + ) + }) + + it("keeps supported custom JSON Schema filters without a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.startsWith("a"), { + representation: { + id: "test/ai/anthropic/startsWithAWithoutDescription", + payload: null + }, + toJsonSchema: () => ({ pattern: "^a" }) + })) + + assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { + type: "string", + allOf: [{ pattern: "^a" }] + }) + }) + + it("drops unsupported custom JSON Schema filters without a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/ai/anthropic/minTwoCharactersWithoutDescription", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { + type: "string" + }) + }) + + it("invokes custom JSON Schema callbacks only with compiler inputs", () => { + let invocations = 0 + const schema = Schema.String.check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/anthropic/compilerInputs", + payload: null, + schemas: [Schema.Finite.ast] + }, + toJsonSchema: ({ type, schemas }) => { + invocations++ + assert.strictEqual(type, "string") + assert.strictEqual(schemas.length, 1) + assert.strictEqual(schemas[0].type, "number") + return { pattern: "^a" } + } + })) + + assert.deepStrictEqual(toCodecAnthropic(schema).jsonSchema, { + type: "string", + allOf: [{ pattern: "^a" }] + }) + assert.strictEqual(invocations, 1) + }) + + it("compiles custom callbacks after structural transformations", () => { + let invocations = 0 + const schema = Schema.Record(Schema.String, Schema.Finite).check( + Schema.makeFilter(() => true, { + representation: { + id: "test/ai/anthropic/structuralCompilerInput", + payload: null + }, + toJsonSchema: ({ type }) => { + invocations++ + assert.strictEqual(type, "array") + return { description: "compiled from the provider shape" } + } + }) + ) + + const jsonSchema = toCodecAnthropic(schema).jsonSchema + assert.strictEqual(jsonSchema.type, "array") + assert.strictEqual( + jsonSchema.description, + "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object and compiled from the provider shape" + ) + assert.strictEqual(invocations, 1) + }) + + it("keeps property names that match unsupported keywords", () => { + const unsupported = Schema.Any.check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/anthropic/unsupportedPropertySchema", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(toCodecAnthropic(Schema.Struct({ minLength: unsupported })).jsonSchema, { + type: "object", + properties: { minLength: {} }, + required: ["minLength"], + additionalProperties: false + }) + }) + + it("keeps the original codec on the unchanged fast path", () => { + assert.strictEqual(toCodecAnthropic(Schema.String).codec, Schema.String) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/Chat.test.ts b/.context/effect/packages/effect/test/unstable/ai/Chat.test.ts index f8b205ae2..e3f89c046 100644 --- a/.context/effect/packages/effect/test/unstable/ai/Chat.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/Chat.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer, Option, Predicate, Ref, Schema } from "effect" +import { Effect, Layer, Predicate, Ref, Schema } from "effect" import { TestClock } from "effect/testing" import { Chat, IdGenerator, Prompt } from "effect/unstable/ai" import { Persistence } from "effect/unstable/persistence" @@ -12,11 +12,11 @@ const withConstantIdGenerator = (id: string) => const PersistenceLayer = Layer.provideMerge( Chat.layerPersisted({ storeId: "chat" }), - Persistence.layerMemory + Persistence.layerBackingMemory ) describe("Chat", () => { - it("should persist chat history to the backing persistence store", () => + it.effect("should persist chat history to the backing persistence store", () => Effect.gen(function*() { const storeId = "chat" const chatId = "1" @@ -52,7 +52,7 @@ describe("Chat", () => { assert.deepStrictEqual(chatHistory, storedHistory) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) - it("should respect the specified time to live", () => + it.effect("should respect the specified time to live", () => Effect.gen(function*() { const storeId = "chat" const chatId = "1" @@ -92,10 +92,10 @@ describe("Chat", () => { const afterExpiration = yield* store.get(chatId) - assert.deepStrictEqual(afterExpiration, Option.none()) + assert.isUndefined(afterExpiration) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) - it("should prefer the message identifier of the most recent assistant message", () => + it.effect("should prefer the message identifier of the most recent assistant message", () => Effect.gen(function*() { const storeId = "chat" const chatId = "2" @@ -135,7 +135,7 @@ describe("Chat", () => { assert.deepStrictEqual(storedHistory, expectedHistory) }).pipe(withConstantIdGenerator("msg_abc123"), Effect.provide(PersistenceLayer))) - it("should raise an error when retrieving a chat that does not exist", () => + it.effect("should raise an error when retrieving a chat that does not exist", () => Effect.gen(function*() { const persistence = yield* Chat.Persistence diff --git a/.context/effect/packages/effect/test/unstable/ai/EmbeddingModel.test.ts b/.context/effect/packages/effect/test/unstable/ai/EmbeddingModel.test.ts index f3c1772ab..5d089ce77 100644 --- a/.context/effect/packages/effect/test/unstable/ai/EmbeddingModel.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/EmbeddingModel.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Layer } from "effect" +import { Effect, Layer, Schema } from "effect" import { EmbeddingModel } from "effect/unstable/ai" import * as AiError from "effect/unstable/ai/AiError" @@ -16,6 +16,18 @@ const makeLayer = ( ) describe("EmbeddingModel", () => { + it.effect("round trips usage with undefined input tokens through JSON", () => + Effect.gen(function*() { + const usage = new EmbeddingModel.EmbeddingUsage({ inputTokens: undefined }) + + const encoded = yield* Schema.encodeEffect(EmbeddingModel.EmbeddingUsage)(usage) + const json = JSON.parse(JSON.stringify(encoded)) + const decoded = yield* Schema.decodeUnknownEffect(EmbeddingModel.EmbeddingUsage)(json) + + assert.deepStrictEqual(json, {}, "encoded JSON") + assert.deepStrictEqual(decoded, new EmbeddingModel.EmbeddingUsage({}), "decoded usage") + })) + it.effect("embed returns a vector", () => { const calls: Array> = [] diff --git a/.context/effect/packages/effect/test/unstable/ai/LanguageModel.test.ts b/.context/effect/packages/effect/test/unstable/ai/LanguageModel.test.ts index 7aaf55b9e..d43c68282 100644 --- a/.context/effect/packages/effect/test/unstable/ai/LanguageModel.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/LanguageModel.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effect/vitest" import { assertDefined, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Effect, Latch, Option, Schema, Stream } from "effect" +import { Effect, Fiber, Latch, Option, Ref, Schema, Stream } from "effect" import { TestClock } from "effect/testing" import { AiError, LanguageModel, Prompt, Response, ResponseIdTracker, Tool, Toolkit } from "effect/unstable/ai" import * as TestUtils from "./utils.ts" @@ -45,11 +45,12 @@ describe("LanguageModel", () => { usage: { inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, outputTokens: { total: 5, text: undefined, reasoning: undefined } - } + }, + response: undefined } describe("streamText", () => { - it("should emit tool calls before executing tool handlers", () => + it.effect("should emit tool calls before executing tool handlers", () => Effect.gen(function*() { const parts: Array>> = [] const latch = yield* Latch.make() @@ -59,7 +60,7 @@ describe("LanguageModel", () => { const toolParams = { testParam: "test-param" } const toolResult = { testSuccess: "test-success" } - yield* LanguageModel.streamText({ + const fiber = yield* LanguageModel.streamText({ prompt: [], toolkit: MyToolkit }).pipe( @@ -106,17 +107,21 @@ describe("LanguageModel", () => { deepStrictEqual(parts, [toolCallPart]) + // `TestClock.adjust` wakes the sleeping tool handler but returns before + // its result has propagated all the way downstream, so join the stream + // fiber to observe the completed sequence of parts. yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiber) deepStrictEqual(parts, [toolCallPart, toolResultPart]) })) - it("emits finish after resolved tool results", () => + it.effect("emits finish after resolved tool results", () => Effect.gen(function*() { const parts: Array>> = [] const latch = yield* Latch.make() - yield* LanguageModel.streamText({ + const fiber = yield* LanguageModel.streamText({ prompt: [], toolkit: MyToolkit }).pipe( @@ -149,16 +154,254 @@ describe("LanguageModel", () => { strictEqual(parts.some((part) => part.type === "finish"), false) yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiber) strictEqual(parts.length, 3) strictEqual(parts[0]?.type, "tool-call") strictEqual(parts[1]?.type, "tool-result") strictEqual(parts[2]?.type, "finish") })) + + it.effect("runs tool handlers sequentially with concurrency: 1", () => + Effect.gen(function*() { + const active = yield* Ref.make(0) + const maxActive = yield* Ref.make(0) + const started = yield* Latch.make() + const release = yield* Latch.make() + + const handlers = MyToolkit.toLayer({ + MyTool: () => + Effect.gen(function*() { + const current = yield* Ref.updateAndGet(active, (n) => n + 1) + yield* Ref.update(maxActive, (n) => Math.max(n, current)) + yield* started.open + yield* release.await + return { testSuccess: "test-success" } + }).pipe(Effect.ensuring(Ref.update(active, (n) => n - 1))) + }) + + const fiber = yield* LanguageModel.streamText({ + prompt: [], + toolkit: MyToolkit, + concurrency: 1 + }).pipe( + Stream.runDrain, + TestUtils.withLanguageModel({ + streamText: [ + { + type: "tool-call", + id: "tool-1", + name: "MyTool", + params: { testParam: "test-1" } + }, + { + type: "tool-call", + id: "tool-2", + name: "MyTool", + params: { testParam: "test-2" } + }, + { + type: "tool-call", + id: "tool-3", + name: "MyTool", + params: { testParam: "test-3" } + } + ] + }), + Effect.provide(handlers), + Effect.forkScoped + ) + + yield* started.await + strictEqual(yield* Ref.get(active), 1) + strictEqual(yield* Ref.get(maxActive), 1) + + yield* release.open + yield* Fiber.join(fiber) + + strictEqual(yield* Ref.get(active), 0) + strictEqual(yield* Ref.get(maxActive), 1) + })) + + it.effect("allows tool handler overlap up to a bounded concurrency", () => + Effect.gen(function*() { + const active = yield* Ref.make(0) + const maxActive = yield* Ref.make(0) + const twoStarted = yield* Latch.make() + const release = yield* Latch.make() + + const handlers = MyToolkit.toLayer({ + MyTool: () => + Effect.gen(function*() { + const current = yield* Ref.updateAndGet(active, (n) => n + 1) + yield* Ref.update(maxActive, (n) => Math.max(n, current)) + if (current === 2) { + yield* twoStarted.open + } + yield* release.await + return { testSuccess: "test-success" } + }).pipe(Effect.ensuring(Ref.update(active, (n) => n - 1))) + }) + + const fiber = yield* LanguageModel.streamText({ + prompt: [], + toolkit: MyToolkit, + concurrency: 2 + }).pipe( + Stream.runDrain, + TestUtils.withLanguageModel({ + streamText: [ + { + type: "tool-call", + id: "tool-1", + name: "MyTool", + params: { testParam: "test-1" } + }, + { + type: "tool-call", + id: "tool-2", + name: "MyTool", + params: { testParam: "test-2" } + }, + { + type: "tool-call", + id: "tool-3", + name: "MyTool", + params: { testParam: "test-3" } + } + ] + }), + Effect.provide(handlers), + Effect.forkScoped + ) + + yield* twoStarted.await + strictEqual(yield* Ref.get(active), 2) + strictEqual(yield* Ref.get(maxActive), 2) + + yield* release.open + yield* Fiber.join(fiber) + + strictEqual(yield* Ref.get(active), 0) + strictEqual(yield* Ref.get(maxActive), 2) + })) + + it.effect("provides tool call IDs to concurrent identical tool handlers", () => + Effect.gen(function*() { + const toolCallIds = yield* Ref.make>([]) + const twoStarted = yield* Latch.make() + const release = yield* Latch.make() + + const handlers = MyToolkit.toLayer({ + MyTool: (_, context) => + Effect.gen(function*() { + const toolCallId = context.toolCallId + assertDefined(toolCallId) + const ids = yield* Ref.updateAndGet(toolCallIds, (ids) => [...ids, toolCallId]) + if (ids.length === 2) { + yield* twoStarted.open + } + yield* release.await + return { testSuccess: "test-success" } + }) + }) + + const fiber = yield* LanguageModel.streamText({ + prompt: [], + toolkit: MyToolkit + }).pipe( + Stream.runDrain, + TestUtils.withLanguageModel({ + streamText: [ + { + type: "tool-call", + id: "tool-1", + name: "MyTool", + params: { testParam: "identical" } + }, + { + type: "tool-call", + id: "tool-2", + name: "MyTool", + params: { testParam: "identical" } + } + ] + }), + Effect.provide(handlers), + Effect.forkScoped + ) + + yield* twoStarted.await + deepStrictEqual((yield* Ref.get(toolCallIds)).sort(), ["tool-1", "tool-2"]) + + yield* release.open + yield* Fiber.join(fiber) + })) + + it.effect("bounds needsApproval evaluation with the tool handler concurrency", () => + Effect.gen(function*() { + const active = yield* Ref.make(0) + const maxActive = yield* Ref.make(0) + const started = yield* Latch.make() + const release = yield* Latch.make() + + const tool = Tool.make("ApprovalConcurrencyTool", { + parameters: Schema.Struct({ input: Schema.String }), + success: Schema.Struct({ output: Schema.String }), + needsApproval: () => + Effect.gen(function*() { + const current = yield* Ref.updateAndGet(active, (n) => n + 1) + yield* Ref.update(maxActive, (n) => Math.max(n, current)) + yield* started.open + yield* release.await + return false + }).pipe(Effect.ensuring(Ref.update(active, (n) => n - 1))) + }) + const toolkit = Toolkit.make(tool) + const handlers = toolkit.toLayer({ + ApprovalConcurrencyTool: () => Effect.succeed({ output: "done" }) + }) + + const fiber = yield* LanguageModel.streamText({ + prompt: [], + toolkit, + concurrency: 1 + }).pipe( + Stream.runDrain, + TestUtils.withLanguageModel({ + streamText: [ + { + type: "tool-call", + id: "tool-1", + name: "ApprovalConcurrencyTool", + params: { input: "test-1" } + }, + { + type: "tool-call", + id: "tool-2", + name: "ApprovalConcurrencyTool", + params: { input: "test-2" } + } + ] + }), + Effect.provide(handlers), + Effect.forkScoped + ) + + yield* started.await + strictEqual(yield* Ref.get(active), 1) + strictEqual(yield* Ref.get(maxActive), 1) + + yield* release.open + yield* Fiber.join(fiber) + + strictEqual(yield* Ref.get(active), 0) + strictEqual(yield* Ref.get(maxActive), 1) + })) }) describe("generateObject", () => { - it("includes full generated text in StructuredOutputError", () => + it.effect("includes full generated text in StructuredOutputError", () => Effect.gen(function*() { const error = yield* LanguageModel.generateObject({ prompt: [], @@ -179,7 +422,7 @@ describe("LanguageModel", () => { } })) - it("resolves top-level $ref for class schemas in defaultCodecTransformer", () => { + it("resolves the canonical top-level $ref for class schemas in defaultCodecTransformer", () => { class Person extends Schema.Class("Person")({ name: Schema.String }) {} @@ -196,7 +439,7 @@ describe("LanguageModel", () => { required: ["name"], additionalProperties: false, $defs: { - Person: { + "PersonEncoded": { type: "object", properties: { name: { @@ -212,7 +455,7 @@ describe("LanguageModel", () => { }) describe("provider options", () => { - it("initialize incremental fields as undefined in generateText", () => + it.effect("initialize incremental fields as undefined in generateText", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined @@ -232,7 +475,7 @@ describe("LanguageModel", () => { strictEqual(capturedOptions.incrementalPrompt, undefined) })) - it("initialize incremental fields as undefined in generateObject", () => + it.effect("initialize incremental fields as undefined in generateObject", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined @@ -259,7 +502,7 @@ describe("LanguageModel", () => { strictEqual(capturedOptions.incrementalPrompt, undefined) })) - it("initialize incremental fields as undefined in streamText", () => + it.effect("initialize incremental fields as undefined in streamText", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined @@ -280,7 +523,7 @@ describe("LanguageModel", () => { strictEqual(capturedOptions.incrementalPrompt, undefined) })) - it("falls back to full prompt in generateText when incremental request fails", () => + it.effect("falls back to full prompt in generateText when incremental request fails", () => Effect.gen(function*() { const fullPrompt = Prompt.make([ Prompt.systemMessage({ content: "system" }), @@ -337,7 +580,7 @@ describe("LanguageModel", () => { deepStrictEqual(calls[1]!.prompt, fullPrompt) })) - it("falls back to full prompt in streamText when incremental request fails", () => + it.effect("falls back to full prompt in streamText when incremental request fails", () => Effect.gen(function*() { const fullPrompt = Prompt.make([ Prompt.systemMessage({ content: "system" }), @@ -395,7 +638,7 @@ describe("LanguageModel", () => { deepStrictEqual(calls[1]!.prompt, fullPrompt) })) - it("uses tracker prepareUnsafe and markParts in generateText without toolkit", () => + it.effect("uses tracker prepareUnsafe and markParts in generateText without toolkit", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined let preparedPrompt: LanguageModel.ProviderOptions["prompt"] | undefined @@ -417,7 +660,10 @@ describe("LanguageModel", () => { return Effect.succeed([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -451,7 +697,7 @@ describe("LanguageModel", () => { strictEqual(markedResponseId, "resp_next") })) - it("uses tracker prepareUnsafe and markParts in generateText with empty toolkit", () => + it.effect("uses tracker prepareUnsafe and markParts in generateText with empty toolkit", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined let prepareCalls = 0 @@ -469,7 +715,10 @@ describe("LanguageModel", () => { return Effect.succeed([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -498,7 +747,7 @@ describe("LanguageModel", () => { strictEqual(markCalls, 1) })) - it("calls tracker.prepareUnsafe after stripping resolved approvals in toolkit flow", () => + it.effect("calls tracker.prepareUnsafe after stripping resolved approvals in toolkit flow", () => Effect.gen(function*() { const toolCallId = "call-tracker" const approvalId = "approval-tracker" @@ -531,7 +780,8 @@ describe("LanguageModel", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }), @@ -549,7 +799,10 @@ describe("LanguageModel", () => { Effect.succeed([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]), @@ -582,7 +835,7 @@ describe("LanguageModel", () => { strictEqual(markedParts, preparedPrompt.content) })) - it("uses tracker prepareUnsafe and markParts in streamText without toolkit", () => + it.effect("uses tracker prepareUnsafe and markParts in streamText without toolkit", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined let preparedPrompt: LanguageModel.ProviderOptions["prompt"] | undefined @@ -606,7 +859,10 @@ describe("LanguageModel", () => { return Stream.fromIterable([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -639,7 +895,7 @@ describe("LanguageModel", () => { strictEqual(markedResponseId, "resp_next") })) - it("uses tracker prepareUnsafe and markParts in streamText with empty toolkit", () => + it.effect("uses tracker prepareUnsafe and markParts in streamText with empty toolkit", () => Effect.gen(function*() { let capturedOptions: LanguageModel.ProviderOptions | undefined let preparedPrompt: LanguageModel.ProviderOptions["prompt"] | undefined @@ -664,7 +920,10 @@ describe("LanguageModel", () => { return Stream.fromIterable([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -697,7 +956,7 @@ describe("LanguageModel", () => { strictEqual(markedResponseId, "resp_next") })) - it("calls tracker.prepareUnsafe after stripping resolved approvals in streamText toolkit flow", () => + it.effect("calls tracker.prepareUnsafe after stripping resolved approvals in streamText toolkit flow", () => Effect.gen(function*() { const toolCallId = "call-tracker-stream" const approvalId = "approval-tracker-stream" @@ -730,7 +989,8 @@ describe("LanguageModel", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }), @@ -750,7 +1010,10 @@ describe("LanguageModel", () => { Stream.fromIterable([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -782,7 +1045,7 @@ describe("LanguageModel", () => { strictEqual(markedParts, preparedPrompt.content) })) - it("uses tracker prepareUnsafe and markParts when disableToolCallResolution is true", () => + it.effect("uses tracker prepareUnsafe and markParts when disableToolCallResolution is true", () => Effect.gen(function*() { const toolCallId = "call-tracker-stream-disable" const approvalId = "approval-tracker-stream-disable" @@ -821,7 +1084,8 @@ describe("LanguageModel", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }), @@ -843,7 +1107,10 @@ describe("LanguageModel", () => { return Stream.fromIterable([ { type: "response-metadata", - id: "resp_next" + id: "resp_next", + modelId: undefined, + timestamp: undefined, + request: undefined }, finishPart ]) @@ -887,7 +1154,7 @@ describe("LanguageModel", () => { }) describe("tool approval", () => { - it("emits tool-approval-request when tool has needsApproval: true", () => + it.effect("emits tool-approval-request when tool has needsApproval: true", () => Effect.gen(function*() { const parts: Array>> = [] @@ -936,7 +1203,7 @@ describe("LanguageModel", () => { } })) - it("pre-resolves approved tool calls before calling LLM", () => + it.effect("pre-resolves approved tool calls before calling LLM", () => Effect.gen(function*() { const toolCallId = "call-456" const approvalId = "approval-456" @@ -975,14 +1242,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1003,7 +1263,7 @@ describe("LanguageModel", () => { strictEqual(toolResults[0].isFailure, false) })) - it("pre-resolves denied tool calls with execution-denied before calling LLM", () => + it.effect("pre-resolves denied tool calls with execution-denied before calling LLM", () => Effect.gen(function*() { const toolCallId = "call-789" const approvalId = "approval-789" @@ -1043,14 +1303,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1073,7 +1326,7 @@ describe("LanguageModel", () => { strictEqual(toolResults[0].isFailure, true) })) - it("strips approved approval artifacts from prompt sent to provider (streamText)", () => + it.effect("strips approved approval artifacts from prompt sent to provider (streamText)", () => Effect.gen(function*() { const toolCallId = "call-strip" const approvalId = "approval-strip" @@ -1112,14 +1365,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1144,7 +1390,7 @@ describe("LanguageModel", () => { } })) - it("strips denied approval artifacts from prompt sent to provider", () => + it.effect("strips denied approval artifacts from prompt sent to provider", () => Effect.gen(function*() { const toolCallId = "call-strip-deny" const approvalId = "approval-strip-deny" @@ -1184,14 +1430,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1222,7 +1461,7 @@ describe("LanguageModel", () => { } })) - it("strips only resolved approvals, preserves unrelated parts", () => + it.effect("strips only resolved approvals, preserves unrelated parts", () => Effect.gen(function*() { const resolvedCallId = "call-resolved" const resolvedApprovalId = "approval-resolved" @@ -1274,14 +1513,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1307,7 +1539,7 @@ describe("LanguageModel", () => { } })) - it("strips approval artifacts via generateText path", () => + it.effect("strips approval artifacts via generateText path", () => Effect.gen(function*() { const toolCallId = "call-gen" const approvalId = "approval-gen" @@ -1345,14 +1577,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ generateText: (opts) => { capturedPrompt = opts.prompt - return Effect.succeed([{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }]) + return Effect.succeed([finishPart]) } }), Effect.provide(ApprovalToolkitLayer) @@ -1372,7 +1597,7 @@ describe("LanguageModel", () => { } })) - it("dynamic needsApproval returns true when condition met", () => + it.effect("dynamic needsApproval returns true when condition met", () => Effect.gen(function*() { const parts: Array>> = [] @@ -1408,7 +1633,7 @@ describe("LanguageModel", () => { } })) - it("dynamic needsApproval returns false when condition not met", () => + it.effect("dynamic needsApproval returns false when condition not met", () => Effect.gen(function*() { const parts: Array>> = [] @@ -1444,14 +1669,14 @@ describe("LanguageModel", () => { } })) - it("tool without needsApproval executes normally", () => + it.effect("tool without needsApproval executes normally", () => Effect.gen(function*() { const parts: Array>> = [] const toolCallId = "call-normal" const latch = yield* Latch.make() - yield* LanguageModel.streamText({ + const fiber = yield* LanguageModel.streamText({ prompt: [], toolkit: MyToolkit }).pipe( @@ -1479,6 +1704,7 @@ describe("LanguageModel", () => { yield* latch.await yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiber) strictEqual(parts.length, 2) strictEqual(parts[0].type, "tool-call") @@ -1488,7 +1714,7 @@ describe("LanguageModel", () => { } })) - it("strips previous-round approval artifacts even when no new pending approvals (streamText)", () => + it.effect("strips previous-round approval artifacts even when no new pending approvals (streamText)", () => Effect.gen(function*() { const toolCallId = "call-prev" const approvalId = "approval-prev" @@ -1521,7 +1747,8 @@ describe("LanguageModel", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }), @@ -1537,14 +1764,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ streamText: (opts) => { capturedPrompt = opts.prompt - return [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + return [finishPart] } }), Effect.provide(ApprovalToolkitLayer) @@ -1576,7 +1796,7 @@ describe("LanguageModel", () => { } })) - it("strips previous-round approval artifacts even when no new pending approvals (generateText)", () => + it.effect("strips previous-round approval artifacts even when no new pending approvals (generateText)", () => Effect.gen(function*() { const toolCallId = "call-prev-gen" const approvalId = "approval-prev-gen" @@ -1607,7 +1827,8 @@ describe("LanguageModel", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }), @@ -1621,14 +1842,7 @@ describe("LanguageModel", () => { TestUtils.withLanguageModel({ generateText: (opts) => { capturedPrompt = opts.prompt - return Effect.succeed([{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }]) + return Effect.succeed([finishPart]) } }), Effect.provide(ApprovalToolkitLayer) @@ -1647,7 +1861,7 @@ describe("LanguageModel", () => { } })) - it("streamText emits pre-resolved tool results as stream parts", () => + it.effect("streamText emits pre-resolved tool results as stream parts", () => Effect.gen(function*() { const toolCallId = "call-emit" const approvalId = "approval-emit" @@ -1688,14 +1902,7 @@ describe("LanguageModel", () => { }) ), TestUtils.withLanguageModel({ - streamText: [{ - type: "finish", - reason: "stop", - usage: { - inputTokens: { uncached: 5, total: 5, cacheRead: undefined, cacheWrite: undefined }, - outputTokens: { total: 5, text: undefined, reasoning: undefined } - } - }] + streamText: [finishPart] }), Effect.provide(ApprovalToolkitLayer) ) diff --git a/.context/effect/packages/effect/test/unstable/ai/LanguageModelRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/ai/LanguageModelRepresentation.test.ts new file mode 100644 index 000000000..69e5936f8 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/LanguageModelRepresentation.test.ts @@ -0,0 +1,24 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import { LanguageModel } from "effect/unstable/ai" + +describe("LanguageModel representation v2", () => { + it("projects the encoded side before JSON Schema generation", () => { + assert.strictEqual(LanguageModel.defaultCodecTransformer(Schema.FiniteFromString).jsonSchema.type, "string") + }) + + it("uses custom JSON Schema compiler annotations in the default transformer", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/ai/language-model/minTwoCharacters", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(LanguageModel.defaultCodecTransformer(schema).jsonSchema, { + type: "string", + allOf: [{ minLength: 2 }] + }) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/LanguageModelTrackerLifecycle.test.ts b/.context/effect/packages/effect/test/unstable/ai/LanguageModelTrackerLifecycle.test.ts index 2eff726c1..8b645cbec 100644 --- a/.context/effect/packages/effect/test/unstable/ai/LanguageModelTrackerLifecycle.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/LanguageModelTrackerLifecycle.test.ts @@ -393,7 +393,8 @@ describe("LanguageModel tracker lifecycle integration", () => { id: toolCallId, name: "ApprovalTool", result: { result: "approved-result" }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpProtocol.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpProtocol.test.ts new file mode 100644 index 000000000..be950fc52 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpProtocol.test.ts @@ -0,0 +1,140 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, Schema } from "effect" +import * as McpProtocol from "effect/unstable/ai/internal/mcpProtocol" +import * as McpProtocolRegistry from "effect/unstable/ai/internal/mcpProtocolRegistry" +import * as Rpc from "effect/unstable/rpc/Rpc" +import * as RpcGroup from "effect/unstable/rpc/RpcGroup" + +const makeTestProtocol = < + const Version extends string, + const Discriminator extends string +>( + protocolVersion: Version, + discriminator: Discriminator +) => { + const Payload = Schema.Struct({ + protocol: Schema.Literal(discriminator), + value: Schema.String + }) + const TestRequest = Rpc.make("test/shape", { + payload: Payload, + success: Schema.Struct({ + protocol: Schema.Literal(protocolVersion), + value: Schema.String + }) + }) + + return McpProtocol.make({ + protocolVersion, + transport: { + acceptsJsonRpcBatches: true, + requiresVersionHeader: false + }, + clientRpcs: RpcGroup.make(TestRequest), + clientNotificationRpcs: RpcGroup.make(), + serverRequestRpcs: RpcGroup.make(), + serverNotificationRpcs: RpcGroup.make() + }) +} + +const request = (payload: unknown) => ({ + _tag: "Request" as const, + id: 1, + tag: "test/shape", + payload, + headers: [] +}) + +describe("McpProtocolRegistry", () => { + it.effect("should reject an empty declaration when runtime input bypasses the non-empty type", () => + Effect.gen(function*() { + const protocols = [] as unknown as Parameters[0] + + const error = yield* Effect.flip(McpProtocolRegistry.make(protocols)) + + assert.strictEqual(error._tag, "IllegalArgumentError") + assert.match(error.message, /at least one MCP protocol/) + })) + + it.effect("should reject duplicate versions when distinct adapters declare the same version", () => + Effect.gen(function*() { + const first = makeTestProtocol("test-a", "a") + const second = makeTestProtocol("test-a", "other-a") + + const error = yield* Effect.flip(McpProtocolRegistry.make([first, second])) + + assert.strictEqual(error._tag, "IllegalArgumentError") + assert.match(error.message, /Duplicate MCP protocol version: test-a/) + })) + + it.effect("should select the matching adapter when the offered version is declared", () => + Effect.gen(function*() { + const first = makeTestProtocol("test-a", "a") + const second = makeTestProtocol("test-b", "b") + const registry = yield* McpProtocolRegistry.make([first, second]) + const selected: typeof first | typeof second = registry.select("test-b") + + assert.strictEqual(selected, second) + })) + + it.effect("should select the first declared adapter when the offered version is unsupported", () => + Effect.gen(function*() { + const first = makeTestProtocol("test-a", "a") + const second = makeTestProtocol("test-b", "b") + const registry = yield* McpProtocolRegistry.make([first, second]) + + assert.strictEqual(registry.select("unsupported"), first) + })) + + it.effect("should preserve declaration order when the source array mutates after registry creation", () => + Effect.gen(function*() { + const first = makeTestProtocol("test-a", "a") + const second = makeTestProtocol("test-b", "b") + const protocols: [McpProtocol.AnyProtocolAdapter, McpProtocol.AnyProtocolAdapter] = [ + first, + second + ] + const registry = yield* McpProtocolRegistry.make(protocols) + + protocols.reverse() + + assert.strictEqual(registry.select("unsupported"), first) + assert.strictEqual(registry.select("test-b"), second) + })) + + it.effect("should route to only the selected namespace when protocol schemas are incompatible", () => + Effect.gen(function*() { + const first = makeTestProtocol("test-a", "a") + const second = makeTestProtocol("test-b", "b") + const registry = yield* McpProtocolRegistry.make([first, second]) + const selected = registry.select("test-a") + + const selectedRequest = registry.routeClientRequest( + selected, + request({ protocol: "b", value: "wrong adapter" }) + ) + const unselectedRequest = registry.routeClientRequest( + second, + request({ protocol: "b", value: "other adapter" }) + ) + + assert.notStrictEqual(selectedRequest.tag, unselectedRequest.tag) + assert.notStrictEqual(selectedRequest.tag, "test/shape") + + const selectedRpc = registry.clientRpcs.requests.get(selectedRequest.tag) + const unselectedRpc = registry.clientRpcs.requests.get(unselectedRequest.tag) + assert.isDefined(selectedRpc) + assert.isDefined(unselectedRpc) + + const invalid = yield* Effect.exit( + selected.payloadCodecs(selectedRpc).decode(selectedRequest.payload) + ) + + assert.strictEqual(invalid._tag, "Failure") + const valid = yield* second.payloadCodecs(unselectedRpc).decode(unselectedRequest.payload) + assert.deepStrictEqual(valid, { + protocol: "b", + value: "other adapter" + }) + })) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpSchema.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpSchema.test.ts new file mode 100644 index 000000000..14bd3fd27 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpSchema.test.ts @@ -0,0 +1,15 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import * as McpSchema from "effect/unstable/ai/McpSchema" + +describe("McpSchema", () => { + const decodeCreateMessage = Schema.decodeUnknownSync(McpSchema.CreateMessage.payloadSchema) + + it("allows create-message metadata to be omitted", () => { + assert.doesNotThrow(() => decodeCreateMessage({ messages: [], maxTokens: 1 })) + }) + + it("requires create-message metadata to be an object", () => { + assert.throws(() => decodeCreateMessage({ messages: [], maxTokens: 1, metadata: "invalid" })) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer.test.ts deleted file mode 100644 index 3f3edf20a..000000000 --- a/.context/effect/packages/effect/test/unstable/ai/McpServer.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { describe, it } from "@effect/vitest" -import { strictEqual } from "@effect/vitest/utils" -import { Effect, Layer } from "effect" -import * as McpSchema from "effect/unstable/ai/McpSchema" -import * as McpServer from "effect/unstable/ai/McpServer" -import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" -import * as HttpClient from "effect/unstable/http/HttpClient" -import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" -import * as HttpRouter from "effect/unstable/http/HttpRouter" -import { RpcSerialization } from "effect/unstable/rpc" -import * as RpcClient from "effect/unstable/rpc/RpcClient" - -const makeTestClient = Effect.gen(function*() { - const responses: Array = [] - - const serverLayer = McpServer.layerHttp({ - name: "TestServer", - version: "1.0.0", - path: "/mcp" - }) - const { handler, dispose } = HttpRouter.toWebHandler(serverLayer, { disableLogger: true }) - yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) - - let sessionId: string | null = null - const customFetch: typeof fetch = async (input, init) => { - const request = input instanceof Request ? input : new Request(input, init) - if (sessionId) { - request.headers.set("Mcp-Session-Id", sessionId) - } - const response = await handler(request) - sessionId = response.headers.get("Mcp-Session-Id") - responses.push(response.clone()) - return response - } - - const clientLayer = RpcClient.layerProtocolHttp({ url: "http://localhost/mcp" }).pipe( - Layer.provideMerge([FetchHttpClient.layer, RpcSerialization.layerJsonRpc()]), - Layer.provide(Layer.succeed(FetchHttpClient.Fetch, customFetch)) - ) - const client = yield* RpcClient.make(McpSchema.ClientRpcs).pipe( - Effect.provide(clientLayer) - ) - - const httpClient = yield* HttpClient.HttpClient.pipe( - Effect.provide(clientLayer) - ) - - return { client, responses, httpClient } -}) - -describe("McpServer", () => { - it.effect("replays MCP session and negotiated protocol headers after initialize", () => - Effect.gen(function*() { - const { client, responses } = yield* makeTestClient - - yield* client.initialize({ - protocolVersion: "9999-01-01", - capabilities: {}, - clientInfo: { - name: "TestClient", - version: "1.0.0" - } - }) - - yield* client.ping({}) - - strictEqual(responses.length, 2) - strictEqual(responses[0].headers.get("Mcp-Protocol-Version"), "2025-06-18") - })) - - it.effect("returns 404 when a non-initialize request omits the MCP session id", () => - Effect.gen(function*() { - const { httpClient } = yield* makeTestClient - - const response = yield* HttpClientRequest.post("http://locahost/mcp").pipe( - HttpClientRequest.bodyJsonUnsafe({ jsonrpc: "2.0", method: "ping", params: {}, id: 0 }), - httpClient.execute - ) - - strictEqual(response.status, 404) - })) -}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/Lifecycle.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/Lifecycle.test.ts new file mode 100644 index 000000000..67e4f5857 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/Lifecycle.test.ts @@ -0,0 +1,254 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as McpServer from "effect/unstable/ai/McpServer" +import * as Tool from "effect/unstable/ai/Tool" +import * as Toolkit from "effect/unstable/ai/Toolkit" +import { makeRawHttpHarness, makeServerLayer } from "./utils.ts" + +const ServerLayer = makeServerLayer({ name: "LifecycleServer" }) +const makeHarness = makeRawHttpHarness(ServerLayer) + +const initializeRequest = (protocolVersion: string, id = 1) => ({ + jsonrpc: "2.0", + id, + method: "initialize", + params: { + protocolVersion, + capabilities: {}, + clientInfo: { + name: "LifecycleClient", + version: "1.0.0" + } + } +}) + +const initializedNotification = { + jsonrpc: "2.0", + method: "notifications/initialized" +} + +const pingRequest = { + jsonrpc: "2.0", + id: 2, + method: "ping", + params: {} +} + +const InitializeResponse = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Number, + result: McpSchema.InitializeResult +}) + +const ErrorResponse = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.NullOr(Schema.Number), + error: McpSchema.McpError +}) + +const decodeInitializeResponse = Schema.decodeUnknownEffect(InitializeResponse) +const decodeErrorResponse = Schema.decodeUnknownEffect(ErrorResponse) + +type Post = (body: unknown, headers?: HeadersInit) => Effect.Effect + +const initialize = Effect.fnUntraced(function*( + post: Post, + protocolVersion: string, + id = 1 +) { + const response = yield* post(initializeRequest(protocolVersion, id)) + const body = yield* Effect.promise(() => response.json()) + return { + response, + message: yield* decodeInitializeResponse(body) + } as const +}) + +const TestTool = Tool.make("TestTool", { + success: Schema.String +}) +const TestToolkit = Toolkit.make(TestTool) +const TestToolkitLayer = McpServer.toolkit(TestToolkit).pipe( + Layer.provide(TestToolkit.toLayer({ + TestTool: () => Effect.succeed("ok") + })) +) +const FeaturesServerLayer = Layer.mergeAll( + TestToolkitLayer, + McpServer.resource({ + uri: "file:///test", + name: "TestResource", + content: Effect.succeed("test") + }), + McpServer.prompt({ + name: "TestPrompt", + content: () => Effect.succeed("test") + }) +).pipe( + Layer.provide(makeServerLayer({ + name: "LifecycleServer", + extensions: { "example/lifecycle": { enabled: true } } + })) +) + +describe("McpServer initialization", () => { + describe("2025-11-25", () => { + describe("Lifecycle", () => { + describe("1. Lifecycle Phases", () => { + describe("1.1 Initialization", () => { + it.effect("requires initialize to be the first request", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const response = yield* post(pingRequest) + + assert.isAtLeast(response.status, 400) + })) + + it.effect("rejects initialized notifications before initialize", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const response = yield* post(initializedNotification) + + assert.isAtLeast(response.status, 400) + })) + + it.effect("requires protocolVersion, capabilities, and clientInfo", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const invalidParams = [ + { + capabilities: {}, + clientInfo: { name: "LifecycleClient", version: "1.0.0" } + }, + { + protocolVersion: "2025-11-25", + clientInfo: { name: "LifecycleClient", version: "1.0.0" } + }, + { + protocolVersion: "2025-11-25", + capabilities: {} + } + ] + + for (let i = 0; i < invalidParams.length; i++) { + const response = yield* post({ + jsonrpc: "2.0", + id: i + 1, + method: "initialize", + params: invalidParams[i] + }) + const body = yield* Effect.promise(() => response.json()) + const error = yield* decodeErrorResponse(body) + + assert.strictEqual(error.id, i + 1) + assert.isNumber(error.error.code) + assert.isNull(response.headers.get("Mcp-Session-Id")) + } + })) + + it.effect("returns server capabilities and implementation information", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const { message, response } = yield* initialize(post, "2025-11-25") + + assert.strictEqual(response.status, 200) + assert.strictEqual(message.id, 1) + assert.deepStrictEqual(message.result.capabilities, { + completions: {}, + logging: {} + }) + assert.deepStrictEqual(message.result.serverInfo, { + name: "LifecycleServer", + version: "1.0.0" + }) + const sessionId = response.headers.get("Mcp-Session-Id") + assert.isNotNull(sessionId) + assert.match(sessionId, /^[\x21-\x7e]+$/) + })) + + it.effect("accepts initialized after a successful initialize response", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const initialized = yield* initialize(post, "2025-11-25") + const sessionId = initialized.response.headers.get("Mcp-Session-Id") + assert.isNotNull(sessionId) + + const response = yield* post(initializedNotification, { + "Mcp-Session-Id": sessionId, + "Mcp-Protocol-Version": initialized.message.result.protocolVersion + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + + describe("1.1.1 Version Negotiation", () => { + it.effect("echoes a requested version supported by the server", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const { message } = yield* initialize(post, "2025-06-18") + + assert.strictEqual(message.result.protocolVersion, "2025-06-18") + })) + + it.effect("negotiates an unsupported requested version to the latest supported version", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const { message } = yield* initialize(post, "2025-11-25") + + assert.strictEqual(message.result.protocolVersion, "2025-06-18") + })) + }) + + describe("1.1.2 Capability Negotiation", () => { + it.effect("advertises the capabilities provided by the server", () => + Effect.gen(function*() { + const { post } = yield* makeRawHttpHarness(FeaturesServerLayer) + const { message } = yield* initialize(post, "2025-11-25") + + assert.deepStrictEqual(message.result.capabilities, { + completions: {}, + extensions: { "example/lifecycle": { enabled: true } }, + logging: {}, + prompts: { listChanged: true }, + resources: { listChanged: true, subscribe: false }, + tools: { listChanged: true } + }) + })) + }) + + describe("1.2 Operation", () => { + it.effect("continues to use the version negotiated during initialization", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const initialized = yield* initialize(post, "2025-06-18") + const sessionId = initialized.response.headers.get("Mcp-Session-Id") + assert.isNotNull(sessionId) + + const response = yield* post(pingRequest, { + "Mcp-Session-Id": sessionId, + "Mcp-Protocol-Version": initialized.message.result.protocolVersion + }) + + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers.get("Mcp-Protocol-Version"), "2025-06-18") + })) + }) + }) + + describe("3. Error Handling", () => { + it.effect("handles protocol version mismatch through version negotiation", () => + Effect.gen(function*() { + const { post } = yield* makeHarness + const { message } = yield* initialize(post, "invalid-version") + + assert.strictEqual(message.result.protocolVersion, "2025-06-18") + })) + }) + }) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/BaseProtocolTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/BaseProtocolTest.ts new file mode 100644 index 000000000..b2e9f5b10 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/BaseProtocolTest.ts @@ -0,0 +1,299 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import { TestClock } from "effect/testing" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Base Protocol", () => { + // https://modelcontextprotocol.io/specification/2025-06-18/basic + describe("Messages", () => { + describe("Requests", () => { + it.effect("SCHEMA accepts JSON-RPC 2.0 requests with string identifiers", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: "ping-1", + method: "ping" + }) + const message = yield* test.decodeResult(response) + + assert.strictEqual(message.id, "ping-1") + })) + + it.effect("SCHEMA accepts JSON-RPC 2.0 requests with numeric identifiers", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.ping(initialized, { id: 42 }) + const message = yield* test.decodeResult(response) + + assert.strictEqual(message.id, 42) + })) + + it.effect("MUST reject requests with an invalid JSON-RPC version", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "1.0", + id: 2, + method: "ping" + }) + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, 2) + assert.strictEqual(message.error.code, McpSchema.INVALID_REQUEST_ERROR_CODE) + })) + + it.effect("MUST return method not found for unknown request methods", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 3, + method: "unknown/method" + }) + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, 3) + assert.strictEqual(message.error.code, McpSchema.METHOD_NOT_FOUND_ERROR_CODE) + })) + it.effect("MUST return invalid params for request payloads that do not match the method schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 7, + method: "ping", + params: "invalid" + }) + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, 7) + assert.strictEqual(message.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("MUST not reply to unknown notifications", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "unknown/method" + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + + it.effect("MUST not reply to notifications with invalid params", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "ping", + params: "invalid" + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + + it.effect("MUST reject requests with invalid identifiers", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: true, + method: "ping" + }) + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, null) + assert.strictEqual(message.error.code, McpSchema.INVALID_REQUEST_ERROR_CODE) + })) + }) + + describe("Responses", () => { + it.effect("MUST return exactly one result response for a successful request", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + yield* fixture.sendRaw({ jsonrpc: "2.0", id: 4, method: "ping" }) + const message = yield* fixture.takeMessage + assert.strictEqual(message.id, 4) + assert.deepStrictEqual(message.result, {}) + + const duplicate = yield* fixture.takeMessage.pipe( + Effect.timeoutOption("1 millis"), + Effect.forkChild + ) + yield* TestClock.adjust("1 millis") + + assert.isTrue(Option.isNone(yield* Fiber.join(duplicate))) + })) + + it.effect("MUST return exactly one error response for a failed request", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: 8, + method: "unknown/method" + }) + const message = yield* fixture.takeMessage + assert.strictEqual(message.id, 8) + assert.property(message, "error") + assert.notProperty(message, "result") + + const duplicate = yield* fixture.takeMessage.pipe( + Effect.timeoutOption("1 millis"), + Effect.forkChild + ) + yield* TestClock.adjust("1 millis") + + assert.isTrue(Option.isNone(yield* Fiber.join(duplicate))) + })) + it.effect("SCHEMA preserves the request identifier in result responses", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const message = yield* test.ping(initialized, { id: 5 }).pipe( + Effect.flatMap(test.decodeResult) + ) + + assert.strictEqual(message.id, 5) + })) + + it.effect("SCHEMA preserves the request identifier in error responses", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const message = yield* test.send(initialized, { + jsonrpc: "2.0", + id: "unknown-1", + method: "unknown/method" + }).pipe(Effect.flatMap(test.decodeError)) + + assert.strictEqual(message.id, "unknown-1") + })) + it.effect("MUST not include both result and error in a response", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 9, + method: "unknown/method" + }) + const raw = yield* Effect.promise(() => response.json()).pipe( + Effect.map(Schema.decodeUnknownSync(Schema.Record(Schema.String, Schema.Unknown))) + ) + + assert.property(raw, "error") + assert.notProperty(raw, "result") + })) + }) + + describe("Notifications", () => { + it.effect("MUST accept notifications without an identifier and send no response", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + + const response = yield* test.send(initialized, test.initializedNotification) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + + it.effect("MUST return a parse error for malformed JSON", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.sendText(initialized, "{") + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, null) + assert.strictEqual(message.error.code, McpSchema.PARSE_ERROR_CODE) + })) + + it.effect("MUST return an invalid request error for malformed JSON-RPC messages", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 10 + }) + const message = yield* test.decodeError(response) + + assert.strictEqual(message.id, 10) + assert.strictEqual(message.error.code, McpSchema.INVALID_REQUEST_ERROR_CODE) + })) + }) + + describe("General fields", () => { + it.effect("SCHEMA preserves additional result metadata fields", () => + Effect.gen(function*() { + const result = yield* Schema.decodeUnknownEffect(McpSchema.ReadResourceResult)({ + contents: [], + _meta: { + "example/conformance": { + enabled: true, + labels: ["one", "two"] + } + } + }) + + assert.deepStrictEqual(result._meta, { + "example/conformance": { + enabled: true, + labels: ["one", "two"] + } + }) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/CompletionTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/CompletionTest.ts new file mode 100644 index 000000000..c2eaa2059 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/CompletionTest.ts @@ -0,0 +1,164 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const decodeCompletion = Schema.decodeUnknownEffect(McpSchema.CompleteResult) + +const complete = ( + ref: { readonly type: "ref/prompt"; readonly name: string } | { + readonly type: "ref/resource" + readonly uri: string + }, + argument: { readonly name: string; readonly value: string }, + context?: { readonly arguments?: Readonly> | undefined } +) => + Effect.gen(function*() { + const response = yield* completeRaw(ref, argument, context) + const test = yield* McpConformance + return yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeCompletion(message.result)) + ) + }) + +const completeRaw = ( + ref: { readonly type: "ref/prompt"; readonly name: string } | { + readonly type: "ref/resource" + readonly uri: string + }, + argument: { readonly name: string; readonly value: string }, + context?: { readonly arguments?: Readonly> | undefined } +) => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "completion/complete", + params: { ref, argument, ...(context === undefined ? {} : { context }) } + }) + return response + }) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Completion", () => { + // Shared by the 2024-11-05, 2025-03-26, and 2025-06-18 specifications, + // except completion context, which was added in 2025-06-18. + describe("Capabilities", () => { + it.effect("MUST advertise completions when argument completion is supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.property(initialized.message.result.capabilities, "completions") + })) + }) + + describe("Requesting Completions", () => { + it.effect("MUST complete a prompt argument", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/prompt", name: "TestPrompt" }, + { name: "required", value: "f" } + ) + + assert.deepStrictEqual(result.completion.values, ["first", "second"]) + })) + + it.effect("MUST complete a resource template argument", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/resource", uri: "file:///template/{path}" }, + { name: "path", value: "a" } + ) + + assert.deepStrictEqual(result.completion.values, ["alpha", "beta"]) + })) + it.effect("MUST pass previously resolved argument context to the completion handler", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/prompt", name: "ContextCompletionPrompt" }, + { name: "value", value: "c" }, + { arguments: { locale: "en" } } + ) + + assert.deepStrictEqual(result.completion.values, ["context received"]) + })) + it.effect("SHOULD reject an unknown prompt reference with Invalid Params", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "completion/complete", + params: { + ref: { type: "ref/prompt", name: "UnknownPrompt" }, + argument: { name: "value", value: "" } + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("MUST reject an unknown argument name", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "completion/complete", + params: { + ref: { type: "ref/prompt", name: "TestPrompt" }, + argument: { name: "unknown", value: "" } + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("MUST return completion values in order", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/resource", uri: "file:///template/{path}" }, + { name: "path", value: "" } + ) + + assert.deepStrictEqual(result.completion.values, ["beta", "alpha"]) + })) + + it.effect("SCHEMA returns the total and additional-results indicator", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/resource", uri: "file:///template/{path}" }, + { name: "path", value: "" } + ) + + assert.strictEqual(result.completion.total, 2) + assert.strictEqual(result.completion.hasMore, false) + })) + + it.effect("MUST return at most one hundred completion values", () => + Effect.gen(function*() { + const result = yield* complete( + { type: "ref/prompt", name: "TestPrompt" }, + { name: "required", value: "limit" } + ) + + assert.strictEqual(result.completion.values.length, 100) + assert.strictEqual(result.completion.total, 101) + assert.strictEqual(result.completion.hasMore, true) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ElicitationTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ElicitationTest.ts new file mode 100644 index 000000000..4addf9796 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ElicitationTest.ts @@ -0,0 +1,218 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as McpServer from "effect/unstable/ai/McpServer" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" +import type { McpTestPeer } from "./McpTestPeer.ts" + +const ElicitationRequest = Schema.Struct({ + message: Schema.String, + requestedSchema: Schema.Record(Schema.String, Schema.Unknown) +}) + +const decodeElicitationRequest = Schema.decodeUnknownEffect(ElicitationRequest) + +const request = { + message: "Please provide your profile", + requestedSchema: { + type: "object", + properties: { + name: { + type: "string", + title: "Name" + }, + age: { + type: "integer", + minimum: 0 + }, + subscribed: { + type: "boolean", + default: false + } + }, + required: ["name"] + } +} as const + +const runElicitation = , unknown>>( + client: McpTestPeer["client"], + protocolVersion: McpProtocol.ProtocolVersion, + schema: S +) => + McpServer.elicit({ + message: request.message, + schema + }).pipe( + Effect.provideService( + McpSchema.McpServerClient, + McpSchema.McpServerClient.of({ + clientId: 1, + protocolVersion, + initializePayload: { + protocolVersion, + capabilities: { elicitation: {} }, + clientInfo: { + name: "McpConformancePeer", + version: "1.0.0" + } + }, + getClient: Effect.succeed(client) + }) + ) + ) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Elicitation", () => { + // https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation + describe("Capabilities", () => { + it.effect("MUST send elicitation requests when the client advertises elicitation", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => + Effect.succeed({ + action: "accept", + content: { name: "Ada" } + }) + } + }) + + yield* peer.client["elicitation/create"](request) + + assert.strictEqual((yield* peer.takeRequest).method, "elicitation/create") + })) + }) + + describe("Form Mode", () => { + it.effect("MUST send the message and requested primitive form schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => + Effect.succeed({ + action: "accept", + content: { name: "Ada", age: 37, subscribed: true } + }) + } + }) + + const result = yield* peer.client["elicitation/create"](request) + const recorded = yield* peer.takeRequest + const payload = yield* decodeElicitationRequest(recorded.payload) + + assert.strictEqual(payload.message, request.message) + assert.deepStrictEqual(payload.requestedSchema, request.requestedSchema) + assert.strictEqual(result.action, "accept") + if (result.action === "accept") { + assert.deepStrictEqual(result.content, { + name: "Ada", + age: 37, + subscribed: true + }) + } + })) + + it.effect("MUST decode accepted content against the requested schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => + Effect.succeed({ + action: "accept", + content: { name: "Ada", age: "37" } + }) + } + }) + + const result = yield* runElicitation( + peer.client, + protocol.protocolVersion, + Schema.Struct({ + name: Schema.String, + age: Schema.NumberFromString + }) + ) + + assert.deepStrictEqual(result, { name: "Ada", age: 37 }) + })) + + it.effect("SCENARIO returns a typed failure when the user declines", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => Effect.succeed({ action: "decline" }) + } + }) + + const error = yield* runElicitation( + peer.client, + protocol.protocolVersion, + Schema.Struct({ name: Schema.String }) + ).pipe(Effect.flip) + + assert.instanceOf(error, McpSchema.ElicitationDeclined) + })) + + it.effect("SCENARIO interrupts the operation when the user cancels", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => Effect.succeed({ action: "cancel" }) + } + }) + + const exit = yield* Effect.exit(runElicitation( + peer.client, + protocol.protocolVersion, + Schema.Struct({ name: Schema.String }) + )) + + assert.isTrue(Exit.isFailure(exit)) + if (Exit.isFailure(exit)) { + assert.isTrue(Cause.hasInterrupts(exit.cause)) + } + })) + + it.effect("MUST reject accepted content that does not match the requested schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { elicitation: {} }, + handlers: { + "elicitation/create": () => + Effect.succeed({ + action: "accept", + content: { name: 123 } + }) + } + }) + + const exit = yield* Effect.exit(runElicitation( + peer.client, + protocol.protocolVersion, + Schema.Struct({ name: Schema.String }) + )) + + assert.isTrue(Exit.isFailure(exit)) + if (Exit.isFailure(exit)) { + assert.isTrue(Cause.hasDies(exit.cause)) + } + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LifecycleTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LifecycleTest.ts new file mode 100644 index 000000000..d069855c7 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LifecycleTest.ts @@ -0,0 +1,147 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +export const suite = ( + protocol: McpProtocol.ProtocolAdapter, + layer: McpConformanceLayer +) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Lifecycle", () => { + // Shared lifecycle behavior. Dated transport requirements stay in the + // version entrypoints that compose this suite. + describe("Lifecycle Phases", () => { + describe("Initialization", () => { + it.effect("MUST reject non-ping requests before initialize", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.post({ + jsonrpc: "2.0", + id: 1, + method: "tools/list", + params: {} + }) + + assert.strictEqual(response.status, 400) + })) + + it.effect("MUST reject initialized notifications before initialize", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.post(test.initializedNotification) + + assert.strictEqual(response.status, 400) + })) + + it.effect("SCHEMA requires protocolVersion, capabilities, and clientInfo", () => + Effect.gen(function*() { + const test = yield* McpConformance + const invalidParams = [ + { + capabilities: {}, + clientInfo: { name: "McpConformanceClient", version: "1.0.0" } + }, + { + protocolVersion: protocol.protocolVersion, + clientInfo: { name: "McpConformanceClient", version: "1.0.0" } + }, + { + protocolVersion: protocol.protocolVersion, + capabilities: {} + } + ] + + for (const [index, params] of invalidParams.entries()) { + const response = yield* test.post({ + jsonrpc: "2.0", + id: index + 1, + method: "initialize", + params + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.id, index + 1) + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + assert.isNull(response.headers.get("Mcp-Session-Id")) + } + })) + + it.effect("SCHEMA returns server capabilities and implementation information", () => + Effect.gen(function*() { + const test = yield* McpConformance + const { message, response, sessionId } = yield* test.initialize() + + assert.strictEqual(response.status, 200) + assert.strictEqual(message.id, 1) + assert.isObject(message.result.capabilities) + assert.deepStrictEqual(message.result.serverInfo, test.serverInfo) + assert.isNotNull(sessionId) + assert.match(sessionId, /^[\x21-\x7e]+$/) + })) + + it.effect("MUST accept initialized after a successful initialize response", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + assert.isNotNull(initialized.sessionId) + + const response = yield* test.notifyInitialized(initialized) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + + describe("Version Negotiation", () => { + it.effect("MUST echo a requested version supported by the server", () => + Effect.gen(function*() { + const test = yield* McpConformance + const { message } = yield* test.initialize() + + assert.strictEqual(message.result.protocolVersion, protocol.protocolVersion) + })) + + it.effect("SHOULD negotiate an unsupported requested version to a supported version", () => + Effect.gen(function*() { + const test = yield* McpConformance + const { message } = yield* test.initialize({ + protocolVersion: "unsupported-version" + }) + + assert.strictEqual(message.result.protocolVersion, protocol.protocolVersion) + })) + }) + + describe("Capability Negotiation", () => { + it.effect("SCHEMA advertises the registered prompt, resource, and tool capabilities", () => + Effect.gen(function*() { + const test = yield* McpConformance + const { message } = yield* test.initialize({ server: "features" }) + + assert.deepStrictEqual(message.result.capabilities.prompts, { listChanged: true }) + assert.deepStrictEqual(message.result.capabilities.resources, { + listChanged: true, + subscribe: false + }) + assert.deepStrictEqual(message.result.capabilities.tools, { listChanged: true }) + })) + }) + + describe("Operation", () => { + it.effect("MUST continue to use the version negotiated during initialization", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + assert.isNotNull(initialized.sessionId) + + const response = yield* test.ping(initialized) + + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers.get("Mcp-Protocol-Version"), protocol.protocolVersion) + })) + }) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LoggingTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LoggingTest.ts new file mode 100644 index 000000000..9aead410d --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/LoggingTest.ts @@ -0,0 +1,203 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const levels = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"] as const + +const setLevel = (level: string) => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "logging/setLevel", + params: { level } + }) + return { initialized, response, test } + }) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Logging", () => { + // Logging has the same protocol surface in all three dated specifications. + describe("Capabilities", () => { + it.effect("MUST advertise logging when log notifications are supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + assert.property(initialized.message.result.capabilities, "logging") + })) + }) + + describe("Setting Log Level", () => { + it.effect("MUST accept every specified log level", () => + Effect.forEach(levels, (level) => + Effect.gen(function*() { + const { response, test } = yield* setLevel(level) + const result = yield* test.decodeResult(response) + assert.deepStrictEqual(result.result, {}) + }), { concurrency: 1 })) + it.effect("MUST reject an unknown log level", () => + Effect.gen(function*() { + const { response, test } = yield* setLevel("verbose") + const error = yield* test.decodeError(response) + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + it.effect("SHOULD update the minimum level for subsequent operations", () => + Effect.gen(function*() { + const { initialized, test } = yield* setLevel("debug") + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 3, + method: "tools/call", + params: { name: "LogLevelTool", arguments: {} } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => Schema.decodeUnknownEffect(McpSchema.CallToolResult)(message.result)) + ) + assert.deepStrictEqual(result.content, [{ type: "text", text: JSON.stringify("Debug") }]) + })) + it.effect("SHOULD send notifications at the selected level and higher", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + const response = yield* fixture.sendRequest("logging/setLevel", { level: "warning" }, 2) + assert.deepStrictEqual(response.result, {}) + + yield* fixture.server.notifications["notifications/message"]({ + level: "warning", + logger: "conformance", + data: "at-threshold" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/message") + const payload = yield* Schema.decodeUnknownEffect( + McpSchema.LoggingMessageNotification.payloadSchema + )(notification.params) + assert.deepStrictEqual(payload, { + level: "warning", + logger: "conformance", + data: "at-threshold" + }) + })) + it.effect("MUST not send notifications below the selected level", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + const response = yield* fixture.sendRequest("logging/setLevel", { level: "warning" }, 2) + assert.deepStrictEqual(response.result, {}) + + yield* fixture.server.notifications["notifications/message"]({ + level: "debug", + logger: "conformance", + data: "below-threshold" + }) + yield* fixture.server.notifications["notifications/message"]({ + level: "warning", + logger: "conformance", + data: "allowed-sentinel" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/message") + assert.deepNestedInclude(notification, { + params: { + level: "warning", + logger: "conformance", + data: "allowed-sentinel" + } + }) + })) + }) + + describe("Log Message Notifications", () => { + it.effect("SCHEMA preserves the log level, logger name, and data", () => + Effect.gen(function*() { + const payload = yield* Schema.decodeUnknownEffect( + McpSchema.LoggingMessageNotification.payloadSchema + )({ + level: "warning", + logger: "database", + data: { + message: "slow query", + durationMs: 120 + } + }) + + assert.deepStrictEqual(payload, { + level: "warning", + logger: "database", + data: { + message: "slow query", + durationMs: 120 + } + }) + })) + it.effect("MUST allow arbitrary JSON-compatible log data", () => + Effect.forEach([ + "message", + 42, + true, + null, + ["one", { nested: "two" }], + { nested: { value: 1 } } + ], (data) => + Schema.decodeUnknownEffect( + McpSchema.LoggingMessageNotification.payloadSchema + )({ + level: "info", + data + }).pipe( + Effect.map((payload) => assert.deepStrictEqual(payload.data, data)) + ))) + it.effect("MUST emit log messages as notifications without an identifier", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + yield* fixture.server.notifications["notifications/message"]({ + level: "warning", + logger: "database", + data: { message: "slow query" } + }) + + const notification = yield* fixture.awaitOutboundMethod("notifications/message") + assert.strictEqual(notification.jsonrpc, "2.0") + assert.strictEqual(notification.method, "notifications/message") + yield* Schema.decodeUnknownEffect( + McpSchema.LoggingMessageNotification.payloadSchema + )(notification.params) + assert.notProperty(notification, "id") + assert.notProperty(notification, "result") + })) + it.effect("SCENARIO does not corrupt the stdio protocol stream with log output", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + yield* fixture.takeFrame + yield* fixture.server.notifications["notifications/message"]({ + level: "info", + logger: "conformance", + data: "stdio-integrity-diagnostic" + }) + const ping = yield* fixture.sendRequest("ping", {}, 2).pipe(Effect.forkChild) + + const notificationFrame = yield* fixture.takeFrame + const responseFrame = yield* fixture.takeFrame + assert.isObject(notificationFrame) + assert.isObject(responseFrame) + assert.deepInclude(notificationFrame, { + method: "notifications/message" + }) + assert.deepNestedInclude(notificationFrame, { + "params.data": "stdio-integrity-diagnostic" + }) + assert.deepInclude(responseFrame, { jsonrpc: "2.0", id: 2, result: {} }) + yield* Fiber.join(ping) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformance.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformance.ts new file mode 100644 index 000000000..098b4e545 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformance.ts @@ -0,0 +1,247 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Ref from "effect/Ref" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeHttpHarness } from "../TestUtils/McpHttpHarness.ts" +import { makeServerLayer } from "../TestUtils/McpServerLayer.ts" +import { makeFeaturesServerLayer, type Observations } from "./McpConformanceFixtures.ts" +import { makeMcpTestPeer, type McpTestPeerOptions } from "./McpTestPeer.ts" + +const SERVER_NAME = "McpConformance" +const SERVER_VERSION = "1.0.0" + +const InitializeResponse = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Number, + result: McpSchema.InitializeResult +}) + +const ErrorResponse = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.NullOr(Schema.Union([Schema.String, Schema.Number])), + error: McpSchema.McpError +}) + +const ResultResponse = Schema.Struct({ + jsonrpc: Schema.Literal("2.0"), + id: Schema.Union([Schema.String, Schema.Number]), + result: Schema.Record(Schema.String, Schema.Unknown) +}) + +const BatchResponse = Schema.Array(Schema.Struct({ + id: Schema.Number, + result: Schema.Struct({}) +})) + +const decodeInitializeResponse = Schema.decodeUnknownEffect(InitializeResponse) +const decodeErrorResponse = Schema.decodeUnknownEffect(ErrorResponse) +const decodeResultResponse = Schema.decodeUnknownEffect(ResultResponse) +const decodeBatchResponse = Schema.decodeUnknownEffect(BatchResponse) + +export interface InitializedSession { + readonly response: Response + readonly message: typeof InitializeResponse.Type + readonly sessionId: string | null + readonly server: "default" | "features" +} + +export interface InitializeOptions { + readonly id?: number | undefined + readonly protocolVersion?: string | undefined + readonly server?: "default" | "features" | undefined +} + +export interface SendOptions { + readonly includeProtocolVersion?: boolean | undefined + readonly protocolVersion?: string | undefined +} + +export interface McpConformanceShape { + readonly protocol: McpProtocol.ProtocolAdapter + readonly serverInfo: { + readonly name: string + readonly version: string + } + readonly initializeRequest: (options?: Omit) => { + readonly jsonrpc: "2.0" + readonly id: number + readonly method: "initialize" + readonly params: { + readonly protocolVersion: string + readonly capabilities: {} + readonly clientInfo: { + readonly name: string + readonly version: string + } + } + } + readonly initializedNotification: { + readonly jsonrpc: "2.0" + readonly method: "notifications/initialized" + } + readonly pingRequest: (id?: number) => { + readonly jsonrpc: "2.0" + readonly id: number + readonly method: "ping" + readonly params: {} + } + readonly post: (body: unknown, headers?: HeadersInit) => Effect.Effect + readonly request: (request: Request) => Effect.Effect + readonly initialize: ( + options?: InitializeOptions + ) => Effect.Effect + readonly send: ( + session: InitializedSession, + body: unknown, + options?: SendOptions + ) => Effect.Effect + readonly sendText: ( + session: InitializedSession, + body: string, + options?: SendOptions + ) => Effect.Effect + readonly notifyInitialized: ( + session: InitializedSession, + options?: SendOptions + ) => Effect.Effect + readonly ping: ( + session: InitializedSession, + options?: SendOptions & { readonly id?: number | undefined } + ) => Effect.Effect + readonly makePeer: ( + options?: McpTestPeerOptions + ) => ReturnType + readonly observations: Effect.Effect + readonly resetObservations: Effect.Effect + readonly decodeError: (response: Response) => Effect.Effect + readonly decodeResult: (response: Response) => Effect.Effect + readonly decodeBatchResponseIds: ( + response: Response + ) => Effect.Effect, Schema.SchemaError> +} + +export class McpConformance extends Context.Service()( + "effect/test/unstable/ai/McpConformance" +) {} + +export type McpConformanceLayer = Layer.Layer + +export const layer = (protocol: McpProtocol.ProtocolAdapter) => + Layer.effect( + McpConformance, + Effect.gen(function*() { + const defaultHarness = yield* makeHttpHarness(makeServerLayer({ + name: SERVER_NAME, + protocols: [protocol] + })) + const observations = yield* Ref.make({ + toolInvocations: 0, + promptInvocations: 0, + resourceTemplateInvocations: 0 + }) + const featuresHarness = yield* makeHttpHarness(makeFeaturesServerLayer(protocol, observations)) + + const initializeRequest: McpConformanceShape["initializeRequest"] = (options) => ({ + jsonrpc: "2.0", + id: options?.id ?? 1, + method: "initialize", + params: { + protocolVersion: options?.protocolVersion ?? protocol.protocolVersion, + capabilities: {}, + clientInfo: { + name: "McpConformanceClient", + version: "1.0.0" + } + } + }) + + const initializedNotification = { + jsonrpc: "2.0", + method: "notifications/initialized" + } as const + + const pingRequest: McpConformanceShape["pingRequest"] = (id = 2) => ({ + jsonrpc: "2.0", + id, + method: "ping", + params: {} + }) + + const initialize: McpConformanceShape["initialize"] = Effect.fnUntraced(function*(options) { + const server = options?.server ?? "default" + const harness = server === "features" ? featuresHarness : defaultHarness + const response = yield* harness.post(initializeRequest(options)) + const body = yield* Effect.promise(() => response.json()) + return { + response, + message: yield* decodeInitializeResponse(body), + sessionId: response.headers.get("Mcp-Session-Id"), + server + } + }) + + const sessionHeaders = (session: InitializedSession, options?: SendOptions): HeadersInit => ({ + ...(session.sessionId === null ? {} : { "Mcp-Session-Id": session.sessionId }), + ...(options?.includeProtocolVersion ?? true + ? { + "Mcp-Protocol-Version": options?.protocolVersion ?? session.message.result.protocolVersion + } + : {}) + }) + + const harnessFor = (session: InitializedSession) => + session.server === "features" ? featuresHarness : defaultHarness + + const sendText: McpConformanceShape["sendText"] = (session, body, options) => + harnessFor(session).postText(body, sessionHeaders(session, options)) + + const send: McpConformanceShape["send"] = (session, body, options) => + harnessFor(session).post(body, sessionHeaders(session, options)) + + const notifyInitialized: McpConformanceShape["notifyInitialized"] = (session, options) => + send(session, initializedNotification, options) + + const ping: McpConformanceShape["ping"] = (session, options) => send(session, pingRequest(options?.id), options) + + return McpConformance.of({ + protocol, + serverInfo: { + name: SERVER_NAME, + version: SERVER_VERSION + }, + initializeRequest, + initializedNotification, + pingRequest, + post: defaultHarness.post, + request: (request) => Effect.promise(() => defaultHarness.handler(request)), + initialize, + send, + sendText, + notifyInitialized, + ping, + makePeer: (options) => makeMcpTestPeer(protocol, options), + observations: Ref.get(observations), + resetObservations: Ref.set(observations, { + toolInvocations: 0, + promptInvocations: 0, + resourceTemplateInvocations: 0 + }), + decodeError: (response) => + Effect.promise(() => response.json()).pipe( + Effect.flatMap(decodeErrorResponse) + ), + decodeResult: (response) => + Effect.promise(() => response.json()).pipe( + Effect.flatMap(decodeResultResponse) + ), + decodeBatchResponseIds: (response) => + Effect.promise(() => response.json()).pipe( + Effect.flatMap(decodeBatchResponse), + Effect.map((responses) => responses.map((message) => message.id)) + ) + }) + }) + ) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformanceFixtures.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformanceFixtures.ts new file mode 100644 index 000000000..e5353e574 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpConformanceFixtures.ts @@ -0,0 +1,308 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Ref from "effect/Ref" +import { CurrentLogLevel } from "effect/References" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as McpServer from "effect/unstable/ai/McpServer" +import * as Tool from "effect/unstable/ai/Tool" +import * as Toolkit from "effect/unstable/ai/Toolkit" +import { makeServerLayer } from "../TestUtils/McpServerLayer.ts" + +export interface Observations { + readonly toolInvocations: number + readonly promptInvocations: number + readonly resourceTemplateInvocations: number +} + +const TestTool = Tool.make("TestTool", { + description: "A test tool", + parameters: Schema.Struct({ + value: Schema.String + }), + success: Schema.String +}) + +const makeStructuredTool = (protocolVersion: string) => + Tool.make("StructuredTool", { + parameters: Tool.EmptyParams, + success: Schema.Struct({ + value: Schema.String + }) + }).annotate( + McpSchema.EnabledWhen, + (client) => client.protocolVersion === protocolVersion + ) + +const LogLevelTool = Tool.make("LogLevelTool", { + parameters: Tool.EmptyParams, + success: Schema.String, + dependencies: [CurrentLogLevel] +}) + +const makeTestToolkitLayer = (observations: Ref.Ref, protocolVersion: string) => { + const TestToolkit = Toolkit.make(TestTool, makeStructuredTool(protocolVersion), LogLevelTool) + return McpServer.toolkit(TestToolkit).pipe( + Layer.provide(TestToolkit.toLayer({ + TestTool: ({ value }) => + Ref.update(observations, (current) => ({ + ...current, + toolInvocations: current.toolInvocations + 1 + })).pipe(Effect.as(value)), + StructuredTool: () => Effect.succeed({ value: "structured" }), + LogLevelTool: () => CurrentLogLevel + })) + ) +} + +const makeContentToolsLayer = Layer.effectDiscard( + Effect.gen(function*() { + const server = yield* McpServer.McpServer + const add = ( + name: string, + result: McpSchema.CallToolResult + ) => + server.addTool({ + tool: new McpSchema.Tool({ + name, + inputSchema: { type: "object", properties: {} } + }), + annotations: Context.empty(), + handle: () => Effect.succeed(result) + }) + + yield* add( + "ImageTool", + new McpSchema.CallToolResult({ + content: [{ + type: "image", + data: new Uint8Array([1, 2, 3]), + mimeType: "image/png" + }] + }) + ) + yield* add( + "EmbeddedResourceTool", + new McpSchema.CallToolResult({ + content: [{ + type: "resource", + resource: { + uri: "file:///embedded", + mimeType: "text/plain", + text: "embedded" + } + }] + }) + ) + yield* add( + "MultipleContentTool", + new McpSchema.CallToolResult({ + content: [ + { type: "text", text: "first" }, + { type: "text", text: "second" } + ] + }) + ) + yield* add( + "ErrorTool", + new McpSchema.CallToolResult({ + content: [{ type: "text", text: "expected failure" }], + isError: true + }) + ) + yield* server.addTool({ + tool: new McpSchema.Tool({ + name: "DefectTool", + inputSchema: { type: "object", properties: {} } + }), + annotations: Context.empty(), + handle: () => Effect.die("private defect details") + }) + + yield* add( + "AudioTool", + new McpSchema.CallToolResult({ + content: [{ + type: "audio", + data: new Uint8Array([4, 5, 6]), + mimeType: "audio/wav" + }] + }) + ) + yield* add( + "ResourceLinkTool", + new McpSchema.CallToolResult({ + content: [{ + type: "resource_link", + uri: "file:///test", + name: "TestResource", + mimeType: "text/plain" + }] + }) + ) + }) +) + +const templatePath = McpSchema.param("path", Schema.String) +const TestResourceTemplate = McpServer.resource`file:///template/${templatePath}`({ + name: "TestResourceTemplate", + description: "A test resource template", + mimeType: "text/plain", + completion: { + path: (value) => Effect.succeed(value === "" ? ["beta", "alpha"] : ["alpha", "beta"]) + }, + content: (uri, path) => Effect.succeed(`${uri}:${path}`) +}) + +const numericId = McpSchema.param("id", Schema.FiniteFromString) +const makeNumericResourceTemplate = (observations: Ref.Ref) => + McpServer.resource`file:///numeric/${numericId}`({ + name: "NumericResourceTemplate", + content: (uri) => + Ref.update(observations, (current) => ({ + ...current, + resourceTemplateInvocations: current.resourceTemplateInvocations + 1 + })).pipe(Effect.as(uri)) + }) + +const ImagePrompt = McpServer.prompt({ + name: "ImagePrompt", + content: () => + Effect.succeed([{ + role: "user", + content: McpSchema.ImageContent.make({ + data: new Uint8Array([1, 2, 3]), + mimeType: "image/png" + }) + }]) +}) + +const AudioPrompt = McpServer.prompt({ + name: "AudioPrompt", + content: () => + Effect.succeed([{ + role: "user", + content: McpSchema.AudioContent.make({ + data: new Uint8Array([4, 5, 6]), + mimeType: "audio/wav" + }) + }]) +}) + +const EmbeddedResourcePrompt = McpServer.prompt({ + name: "EmbeddedResourcePrompt", + content: () => + Effect.succeed([{ + role: "user", + content: McpSchema.EmbeddedResource.make({ + resource: { + uri: "file:///embedded", + mimeType: "text/plain", + text: "embedded" + } + }) + }]) +}) + +const ContextCompletionPrompt = McpServer.prompt({ + name: "ContextCompletionPrompt", + parameters: { + value: Schema.String + }, + completion: { + value: (_input, context) => + Effect.succeed(context?.arguments?.locale === "en" ? ["context received"] : ["context missing"]) + }, + content: ({ value }) => Effect.succeed(value) +}) + +export const makeFeaturesServerLayer = ( + protocol: McpProtocol.ProtocolAdapter, + observations: Ref.Ref +) => + Layer.mergeAll( + makeTestToolkitLayer(observations, protocol.protocolVersion), + makeContentToolsLayer, + McpServer.resource({ + uri: "file:///test", + name: "TestResource", + description: "A test resource", + mimeType: "text/plain", + content: Effect.succeed(McpSchema.ReadResourceResult.make({ + contents: [{ + uri: "file:///test", + mimeType: "text/plain", + text: "test" + }] + })) + }), + McpServer.resource({ + uri: "file:///binary", + name: "BinaryResource", + mimeType: "application/octet-stream", + content: Effect.succeed(McpSchema.ReadResourceResult.make({ + contents: [{ + uri: "file:///binary", + mimeType: "application/octet-stream", + blob: new Uint8Array([1, 2, 3]) + }] + })) + }), + McpServer.resource({ + uri: "file:///multiple", + name: "MultipleResource", + content: Effect.succeed(McpSchema.ReadResourceResult.make({ + contents: [ + { + uri: "file:///multiple#first", + mimeType: "text/plain", + text: "first" + }, + { + uri: "file:///multiple#second", + mimeType: "text/plain", + text: "second" + } + ] + })) + }), + TestResourceTemplate, + makeNumericResourceTemplate(observations), + McpServer.prompt({ + name: "TestPrompt", + description: "A test prompt", + parameters: { + required: Schema.String, + optional: Schema.optional(Schema.String) + }, + completion: { + required: (value) => + Effect.succeed( + value === "limit" + ? Array.from({ length: 101 }, (_, index) => `value-${index}`) + : ["first", "second"] + ) + }, + content: ({ optional, required }) => + Ref.update(observations, (current) => ({ + ...current, + promptInvocations: current.promptInvocations + 1 + })).pipe(Effect.as(`${required}:${optional ?? "omitted"}`)) + }), + McpServer.prompt({ + name: "NoArgumentPrompt", + content: () => Effect.succeed("no arguments") + }), + ImagePrompt, + EmbeddedResourcePrompt, + AudioPrompt, + ContextCompletionPrompt + ).pipe( + Layer.provide(makeServerLayer({ + name: "McpConformance", + protocols: [protocol], + extensions: { "example/lifecycle": { enabled: true } } + })) + ) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpTestPeer.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpTestPeer.ts new file mode 100644 index 000000000..1b13ae3ee --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/McpTestPeer.ts @@ -0,0 +1,120 @@ +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Ref from "effect/Ref" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as RpcClient from "effect/unstable/rpc/RpcClient" +import type * as RpcClientError from "effect/unstable/rpc/RpcClientError" +import type * as RpcGroup from "effect/unstable/rpc/RpcGroup" + +export type ReverseMethod = "roots/list" | "sampling/createMessage" | "elicitation/create" + +export interface RecordedRequest { + readonly id: string | number + readonly method: ReverseMethod + readonly payload: unknown +} + +export type Handler = ( + request: RecordedRequest +) => Effect.Effect + +export interface McpTestPeerOptions { + readonly capabilities?: typeof McpSchema.ClientCapabilities.Type | undefined + readonly clientInfo?: typeof McpSchema.Implementation.Type | undefined + readonly handlers?: Partial> | undefined +} + +export interface McpTestPeer { + readonly client: RpcClient.RpcClient< + RpcGroup.Rpcs, + RpcClientError.RpcClientError + > + readonly requests: Effect.Effect> + readonly takeRequest: Effect.Effect +} + +const isReverseMethod = (method: string): method is ReverseMethod => + ["roots/list", "sampling/createMessage", "elicitation/create"].includes(method) + +export const makeMcpTestPeer = Effect.fn("McpTestPeer.make")(function*( + _protocol: McpProtocol.ProtocolAdapter, + options: McpTestPeerOptions = {} +) { + const requests = yield* Ref.make>([]) + const inbox = yield* Queue.unbounded() + const handlers = options.handlers ?? {} + + const rpcProtocol = yield* RpcClient.Protocol.make((writeResponse) => + Effect.succeed({ + send: (clientId, message) => { + if (message._tag !== "Request" || !isReverseMethod(message.tag)) { + return Effect.void + } + const request: RecordedRequest = { + id: message.id, + method: message.tag, + payload: message.payload + } + return Effect.gen(function*() { + yield* Ref.update(requests, (current) => [...current, request]) + yield* Queue.offer(inbox, request) + + const handler = handlers[request.method] + const result = yield* Effect.exit( + handler === undefined + ? Effect.fail({ + code: -32601, + message: `No test peer handler for ${request.method}` + }) + : handler(request) + ) + + if (Exit.isSuccess(result)) { + return yield* writeResponse(clientId, { + _tag: "Exit", + requestId: request.id, + exit: { + _tag: "Success", + value: result.value + } + }) + } + + const failure = Cause.findErrorOption(result.cause) + if (Option.isSome(failure)) { + return yield* writeResponse(clientId, { + _tag: "Exit", + requestId: request.id, + exit: { + _tag: "Failure", + cause: [{ + _tag: "Fail", + error: failure.value + }] + } + }) + } + + return yield* Effect.die(Cause.squash(result.cause)) + }) + }, + supportsAck: true, + supportsTransferables: false, + supportsStructuredClone: false + }) + ) + + const client = yield* RpcClient.make(McpSchema.ServerRequestRpcs).pipe( + Effect.provideService(RpcClient.Protocol, rpcProtocol) + ) + + return { + client, + requests: Ref.get(requests), + takeRequest: Queue.take(inbox) + } satisfies McpTestPeer +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/PromptsTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/PromptsTest.ts new file mode 100644 index 000000000..ead7f4324 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/PromptsTest.ts @@ -0,0 +1,385 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const decodePrompts = Schema.decodeUnknownEffect(McpSchema.ListPromptsResult) +const decodeGetPrompt = Schema.decodeUnknownEffect(McpSchema.GetPromptResult) + +const getPrompt = (name: string) => + Effect.gen(function*() { + const message = yield* getPromptWire(name) + return yield* decodeGetPrompt(message.result) + }) + +const getPromptWire = (name: string) => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { name } + }) + return yield* test.decodeResult(response) + }) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Prompts", () => { + // Identical requirements in the 2024-11-05, 2025-03-26, and 2025-06-18 specifications. + describe("Capabilities", () => { + it.effect("MUST advertise prompts when prompts are registered", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.property(initialized.message.result.capabilities, "prompts") + })) + + it.effect("MUST NOT advertise prompts when prompts are not supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + + assert.notProperty(initialized.message.result.capabilities, "prompts") + })) + + it.effect("MUST advertise listChanged when prompt list change notifications are supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.strictEqual(initialized.message.result.capabilities.prompts?.listChanged, true) + })) + }) + + describe("Listing Prompts", () => { + it.effect("MUST list every prompt visible to the initialized client", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodePrompts(message.result)) + ) + + const expected = [ + "AudioPrompt", + "ContextCompletionPrompt", + "EmbeddedResourcePrompt", + "ImagePrompt", + "NoArgumentPrompt", + "TestPrompt" + ].sort() + assert.deepStrictEqual(result.prompts.map((prompt) => prompt.name).sort(), expected) + })) + + it.effect("SCHEMA preserves prompt names, descriptions, and arguments", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodePrompts(message.result)) + ) + + const prompt = result.prompts.find((prompt) => prompt.name === "TestPrompt") + assert.isDefined(prompt) + assert.strictEqual(prompt.description, "A test prompt") + assert.deepStrictEqual(prompt.arguments?.map((argument) => argument.name), [ + "required", + "optional" + ]) + })) + + it.effect("MUST mark required and optional prompt arguments correctly", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodePrompts(message.result)) + ) + + const prompt = result.prompts.find((prompt) => prompt.name === "TestPrompt") + assert.isDefined(prompt) + assert.deepStrictEqual(prompt.arguments, [ + { name: "required", required: true }, + { name: "optional", required: false } + ]) + })) + }) + + describe("Getting Prompts", () => { + it.effect("MUST get a registered prompt without arguments", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { name: "NoArgumentPrompt" } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeGetPrompt(message.result)) + ) + + assert.deepStrictEqual(result.messages, [{ + role: "user", + content: { type: "text", text: "no arguments" } + }]) + })) + it.effect("MUST get a registered prompt with valid arguments", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: { + required: "required", + optional: "optional" + } + } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeGetPrompt(message.result)) + ) + + assert.deepStrictEqual(result.messages, [{ + role: "user", + content: { type: "text", text: "required:optional" } + }]) + })) + + it.effect("SHOULD reject an unknown prompt name with Invalid Params", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "UnknownPrompt", + arguments: {} + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("SHOULD reject missing required prompt arguments with Invalid Params", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: {} + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("SHOULD reject prompt arguments with invalid values", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: { + required: 123 + } + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + it.effect("MUST not invoke the prompt handler when argument validation fails", () => + Effect.gen(function*() { + const test = yield* McpConformance + yield* test.resetObservations + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: { required: 123 } + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + assert.strictEqual((yield* test.observations).promptInvocations, 0) + })) + it.effect("SCHEMA preserves the prompt description and message order", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: { required: "test" } + } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeGetPrompt(message.result)) + ) + + assert.strictEqual(result.description, "A test prompt") + assert.deepStrictEqual(result.messages.map((message) => message.role), ["user"]) + })) + + it.effect("MUST return text message content", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "prompts/get", + params: { + name: "TestPrompt", + arguments: { required: "text" } + } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeGetPrompt(message.result)) + ) + + assert.deepStrictEqual(result.messages[0]?.content, { + type: "text", + text: "text:omitted" + }) + })) + it.effect("MUST return image message content", () => + Effect.gen(function*() { + const result = yield* getPromptWire("ImagePrompt") + assert.deepStrictEqual(result.result.messages, [{ + role: "user", + content: { + type: "image", + data: "AQID", + mimeType: "image/png" + } + }]) + })) + it.effect("MUST return audio message content", () => + Effect.gen(function*() { + const result = yield* getPromptWire("AudioPrompt") + assert.deepStrictEqual(result.result.messages, [{ + role: "user", + content: { + type: "audio", + data: "BAUG", + mimeType: "audio/wav" + } + }]) + })) + it.effect("MUST return embedded resource message content", () => + Effect.gen(function*() { + const result = yield* getPrompt("EmbeddedResourcePrompt") + assert.deepStrictEqual(result.messages, [{ + role: "user", + content: { + type: "resource", + resource: { + uri: "file:///embedded", + mimeType: "text/plain", + text: "embedded" + } + } + }]) + })) + }) + + describe("List Changed Notification", () => { + it.effect("SHOULD send a prompt list changed notification when the advertised list changes", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + const makePrompt = (name: string) => ({ + prompt: new McpSchema.Prompt({ name }), + annotations: Context.empty(), + completions: {}, + handle: () => + Effect.succeed( + new McpSchema.GetPromptResult({ + messages: [{ role: "user", content: { type: "text", text: name } }] + }) + ) + }) + yield* fixture.server.addPrompt(makePrompt("baseline-list-changed-prompt")) + const initialized = yield* fixture.initialize() + const initializeResult = yield* Schema.decodeUnknownEffect(McpSchema.InitializeResult)(initialized.result) + assert.strictEqual( + initializeResult.capabilities.prompts?.listChanged, + true + ) + + yield* fixture.server.addPrompt(makePrompt("dynamic-list-changed-prompt")) + const notification = yield* fixture.awaitOutboundMethod("notifications/prompts/list_changed") + assert.strictEqual(notification.jsonrpc, "2.0") + assert.strictEqual(notification.method, "notifications/prompts/list_changed") + assert.notProperty(notification, "id") + + const response = yield* fixture.sendRequest("prompts/list", {}) + const result = yield* decodePrompts(response.result) + assert.isTrue(result.prompts.some((prompt) => prompt.name === "dynamic-list-changed-prompt")) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ResourcesTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ResourcesTest.ts new file mode 100644 index 000000000..bdbf71c03 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ResourcesTest.ts @@ -0,0 +1,439 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const decodeResources = Schema.decodeUnknownEffect(McpSchema.ListResourcesResult) +const decodeResourceTemplates = Schema.decodeUnknownEffect(McpSchema.ListResourceTemplatesResult) +const decodeReadResource = Schema.decodeUnknownEffect(McpSchema.ReadResourceResult) +const decodeResourceUpdated = Schema.decodeUnknownEffect(McpSchema.ResourceUpdatedNotification.payloadSchema) +const makeResource = (uri: string, name: string) => ({ + resource: new McpSchema.Resource({ uri, name }), + annotations: Context.empty(), + handle: Effect.succeed( + McpSchema.ReadResourceResult.make({ + contents: [{ uri, text: name }] + }) + ) +}) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Resources", () => { + // Identical requirements in the 2024-11-05, 2025-03-26, and 2025-06-18 specifications. + describe("Capabilities", () => { + it.effect("MUST advertise resources when resources are registered", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.property(initialized.message.result.capabilities, "resources") + })) + + it.effect("MUST NOT advertise resources when resources are not supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + + assert.notProperty(initialized.message.result.capabilities, "resources") + })) + + it.effect("MUST NOT advertise resource subscriptions when they are unsupported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.strictEqual(initialized.message.result.capabilities.resources?.subscribe, false) + })) + + it.effect("MUST advertise listChanged when resource list change notifications are supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.strictEqual(initialized.message.result.capabilities.resources?.listChanged, true) + })) + }) + + describe("Listing Resources", () => { + it.effect("MUST list every resource visible to the initialized client", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeResources(message.result)) + ) + + assert.deepStrictEqual(result.resources.map((resource) => resource.uri).sort(), [ + "file:///binary", + "file:///multiple", + "file:///test" + ]) + })) + + it.effect("SCHEMA preserves resource URI, name, description, and MIME type", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeResources(message.result)) + ) + + const resource = result.resources.find((resource) => resource.uri === "file:///test") + assert.isDefined(resource) + assert.deepInclude(resource, { + uri: "file:///test", + name: "TestResource", + description: "A test resource", + mimeType: "text/plain" + }) + })) + }) + + describe("Reading Resources", () => { + it.effect("MUST read text resource contents", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///test" } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeReadResource(message.result)) + ) + + assert.deepStrictEqual(result.contents, [{ + uri: "file:///test", + mimeType: "text/plain", + text: "test" + }]) + })) + it.effect("MUST read binary resource contents as base64", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///binary" } + }) + const body = yield* Effect.promise(() => response.json()) + const message = yield* Schema.decodeUnknownEffect(Schema.Struct({ + result: Schema.Struct({ + contents: Schema.Array(Schema.Struct({ + uri: Schema.String, + mimeType: Schema.String, + blob: Schema.String + })) + }) + }))(body) + + assert.deepStrictEqual(message.result.contents, [{ + uri: "file:///binary", + mimeType: "application/octet-stream", + blob: "AQID" + }]) + })) + it.effect("SCHEMA preserves the resource URI and MIME type in returned contents", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///test" } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeReadResource(message.result)) + ) + + assert.strictEqual(result.contents[0]?.uri, "file:///test") + assert.strictEqual(result.contents[0]?.mimeType, "text/plain") + })) + it.effect("MUST return multiple resource contents in order", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///multiple" } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeReadResource(message.result)) + ) + + assert.deepStrictEqual(result.contents.map((content) => content.uri), [ + "file:///multiple#first", + "file:///multiple#second" + ]) + })) + it.effect("SHOULD return resource not found for an unknown resource URI", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///missing" } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, -32002) + })) + }) + + describe("Resource Templates", () => { + it.effect("MUST list every registered resource template", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/templates/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeResourceTemplates(message.result)) + ) + + const template = result.resourceTemplates.find( + (template) => template.name === "TestResourceTemplate" + ) + assert.deepStrictEqual( + template && { + uriTemplate: template.uriTemplate, + name: template.name, + description: template.description, + mimeType: template.mimeType + }, + { + uriTemplate: "file:///template/{path}", + name: "TestResourceTemplate", + description: "A test resource template", + mimeType: "text/plain" + } + ) + })) + + it.effect("MUST match and decode a concrete resource-template URI", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///template/encoded%20path" } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeReadResource(message.result)) + ) + + assert.strictEqual( + result.contents[0] && "text" in result.contents[0] ? result.contents[0].text : undefined, + "file:///template/encoded%20path:encoded path" + ) + })) + it.effect("MUST not invoke the handler when template parameter decoding fails", () => + Effect.gen(function*() { + const test = yield* McpConformance + yield* test.resetObservations + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "resources/read", + params: { uri: "file:///numeric/not-a-number" } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + assert.strictEqual((yield* test.observations).resourceTemplateInvocations, 0) + })) + }) + + describe("List Changed Notification", () => { + it.effect("SHOULD send a resource list changed notification when the advertised list changes", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource( + makeResource("file:///baseline-list-changed", "baseline-list-changed-resource") + ) + const initialized = yield* fixture.initialize() + const initializeResult = yield* Schema.decodeUnknownEffect(McpSchema.InitializeResult)(initialized.result) + assert.strictEqual( + initializeResult.capabilities.resources?.listChanged, + true + ) + + yield* fixture.server.addResource( + makeResource("file:///dynamic-list-changed", "dynamic-list-changed-resource") + ) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/list_changed") + assert.strictEqual(notification.jsonrpc, "2.0") + assert.strictEqual(notification.method, "notifications/resources/list_changed") + assert.notProperty(notification, "id") + + const response = yield* fixture.sendRequest("resources/list", {}) + const result = yield* decodeResources(response.result) + assert.isTrue(result.resources.some((resource) => resource.uri === "file:///dynamic-list-changed")) + })) + }) + + describe("Subscriptions", () => { + it.effect("MUST subscribe to a resource when subscriptions are advertised", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource(makeResource("file:///subscription-target", "subscription-target")) + const initialized = yield* fixture.initialize() + const initializeResult = yield* Schema.decodeUnknownEffect(McpSchema.InitializeResult)(initialized.result) + assert.strictEqual(initializeResult.capabilities.resources?.subscribe, true) + + const response = yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-target" + }) + assert.notProperty(response, "error") + assert.deepStrictEqual(response.result, {}) + + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-target" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/updated") + const payload = yield* decodeResourceUpdated(notification.params) + assert.strictEqual(payload.uri, "file:///subscription-target") + })) + + it.effect("MUST send update notifications only for subscribed resources", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource(makeResource("file:///subscription-target", "subscription-target")) + yield* fixture.initialize() + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-target" + }) + + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///not-subscribed" + }) + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-target" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/updated") + const payload = yield* decodeResourceUpdated(notification.params) + assert.strictEqual(payload.uri, "file:///subscription-target") + })) + + it.effect("MUST include the updated resource URI in each notification", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource(makeResource("file:///subscription-target", "subscription-target")) + yield* fixture.initialize() + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-target" + }) + + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-target" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/updated") + assert.strictEqual(notification.jsonrpc, "2.0") + assert.strictEqual(notification.method, "notifications/resources/updated") + assert.notProperty(notification, "id") + const payload = yield* decodeResourceUpdated(notification.params) + assert.strictEqual(payload.uri, "file:///subscription-target") + })) + + it.effect("MUST unsubscribe from resource updates", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource(makeResource("file:///subscription-target", "subscription-target")) + yield* fixture.server.addResource(makeResource("file:///subscription-sentinel", "subscription-sentinel")) + yield* fixture.initialize() + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-target" + }) + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-sentinel" + }) + + const response = yield* fixture.sendRequest("resources/unsubscribe", { + uri: "file:///subscription-target" + }) + assert.notProperty(response, "error") + assert.deepStrictEqual(response.result, {}) + + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-target" + }) + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-sentinel" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/updated") + const payload = yield* decodeResourceUpdated(notification.params) + assert.strictEqual(payload.uri, "file:///subscription-sentinel") + })) + + it.effect("MUST not send updates after a resource is unsubscribed", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.server.addResource(makeResource("file:///subscription-target", "subscription-target")) + yield* fixture.server.addResource(makeResource("file:///subscription-sentinel", "subscription-sentinel")) + yield* fixture.initialize() + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-target" + }) + yield* fixture.sendRequest("resources/subscribe", { + uri: "file:///subscription-sentinel" + }) + yield* fixture.sendRequest("resources/unsubscribe", { + uri: "file:///subscription-target" + }) + + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-target" + }) + yield* fixture.server.notifications["notifications/resources/updated"]({ + uri: "file:///subscription-sentinel" + }) + const notification = yield* fixture.awaitOutboundMethod("notifications/resources/updated") + const payload = yield* decodeResourceUpdated(notification.params) + assert.strictEqual(payload.uri, "file:///subscription-sentinel") + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/RootsTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/RootsTest.ts new file mode 100644 index 000000000..a5deec1ca --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/RootsTest.ts @@ -0,0 +1,133 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const rootsHandler = (roots: ReadonlyArray<{ readonly uri: string; readonly name?: string }>) => () => + Effect.succeed({ roots }) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Roots", () => { + // Identical requirements in the 2024-11-05, 2025-03-26, and 2025-06-18 specifications. + describe("Capabilities", () => { + it.effect("MUST send roots requests when the client advertises roots", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { roots: {} }, + handlers: { + "roots/list": rootsHandler([]) + } + }) + + yield* peer.client["roots/list"](undefined) + + assert.strictEqual((yield* peer.takeRequest).method, "roots/list") + })) + + it.effect("MUST accept roots requests when the client advertises list changes", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { roots: { listChanged: true } }, + handlers: { + "roots/list": rootsHandler([]) + } + }) + + yield* peer.client["roots/list"](undefined) + + assert.strictEqual((yield* peer.takeRequest).method, "roots/list") + })) + }) + + describe("Listing Roots", () => { + it.effect("MUST accept roots with file URIs and preserve optional names", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { roots: {} }, + handlers: { + "roots/list": rootsHandler([ + { uri: "file:///workspace", name: "Workspace" }, + { uri: "file:///unnamed" } + ]) + } + }) + + const result = yield* peer.client["roots/list"](undefined) + + assert.deepStrictEqual( + result.roots.map((root) => ({ + uri: root.uri, + name: root.name + })), + [ + { uri: "file:///workspace", name: "Workspace" }, + { uri: "file:///unnamed", name: undefined } + ] + ) + })) + + it.effect("MAY accept an empty roots list", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { roots: {} }, + handlers: { + "roots/list": rootsHandler([]) + } + }) + + const result = yield* peer.client["roots/list"](undefined) + + assert.deepStrictEqual(result.roots, []) + })) + + it.effect("MUST surface client errors returned by roots/list", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { roots: {} }, + handlers: { + "roots/list": () => + Effect.fail( + new McpSchema.InternalError({ + message: "Roots unavailable" + }) + ) + } + }) + + const error = yield* peer.client["roots/list"](undefined).pipe(Effect.flip) + + assert.instanceOf(error, McpSchema.InternalError) + assert.strictEqual(error.message, "Roots unavailable") + })) + }) + + describe("Root List Changes", () => { + it.effect("SHOULD refresh roots after a capable client reports a list change", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize({ roots: { listChanged: true } }) + + yield* fixture.sendNotification("notifications/roots/list_changed") + + const request = yield* fixture.awaitOutboundMethod("roots/list") + assert.strictEqual(request.jsonrpc, "2.0") + assert.strictEqual(request.method, "roots/list") + if (typeof request.id !== "string" && typeof request.id !== "number") { + return assert.fail("roots/list request must include an identifier") + } + + yield* fixture.respond(request.id, { + roots: [{ uri: "file:///updated", name: "Updated" }] + }) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/SamplingTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/SamplingTest.ts new file mode 100644 index 000000000..8323da893 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/SamplingTest.ts @@ -0,0 +1,236 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const SamplingRequest = Schema.Struct({ + messages: Schema.Array(Schema.Struct({ + role: Schema.String, + content: Schema.Record(Schema.String, Schema.Unknown) + })), + modelPreferences: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)), + systemPrompt: Schema.optionalKey(Schema.String), + includeContext: Schema.optionalKey(Schema.String), + maxTokens: Schema.Number, + stopSequences: Schema.optionalKey(Schema.Array(Schema.String)), + metadata: Schema.Unknown +}) + +const decodeSamplingRequest = Schema.decodeUnknownEffect(SamplingRequest) +const decodeSamplingResult = Schema.decodeUnknownEffect(Schema.Struct({ + role: Schema.String, + content: Schema.Record(Schema.String, Schema.Unknown), + model: Schema.String, + stopReason: Schema.optionalKey(Schema.String) +})) + +const textResponse = { + role: "assistant", + content: { type: "text", text: "sampled" }, + model: "test-model", + stopReason: "endTurn" +} as const + +const samplingRequest = McpSchema.CreateMessage.payloadSchema.make({ + messages: [ + McpSchema.SamplingMessage.make({ + role: "user", + content: McpSchema.TextContent.make({ text: "sample" }) + }) + ], + maxTokens: 64, + metadata: {} +}) + +const samplingRequestWithOptions = McpSchema.CreateMessage.payloadSchema.make({ + messages: [ + McpSchema.SamplingMessage.make({ + role: "user", + content: McpSchema.TextContent.make({ text: "first" }) + }), + McpSchema.SamplingMessage.make({ + role: "assistant", + content: McpSchema.TextContent.make({ text: "second" }) + }) + ], + modelPreferences: new McpSchema.ModelPreferences({ + hints: [McpSchema.ModelHint.make({ name: "test-model" })], + costPriority: 0.2, + speedPriority: 0.4, + intelligencePriority: 0.8 + }), + systemPrompt: "System", + includeContext: "thisServer", + maxTokens: 64, + stopSequences: ["STOP"], + metadata: { request: "metadata" } +}) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Sampling", () => { + // Text and image sampling are shared by all three dated specifications. + // Audio sampling was added in 2025-03-26. + describe("Capabilities", () => { + it.effect("MUST send sampling requests when the client advertises sampling", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => Effect.succeed(textResponse) + } + }) + + yield* peer.client["sampling/createMessage"](samplingRequest) + + assert.strictEqual((yield* peer.takeRequest).method, "sampling/createMessage") + })) + }) + + describe("Creating Messages", () => { + it.effect("MUST preserve message order and sampling request options", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => Effect.succeed(textResponse) + } + }) + + yield* peer.client["sampling/createMessage"](samplingRequestWithOptions) + const recorded = yield* peer.takeRequest + const payload = yield* decodeSamplingRequest(recorded.payload) + + assert.deepStrictEqual( + payload.messages.map((message) => ({ + role: message.role, + text: message.content.text + })), + [ + { role: "user", text: "first" }, + { role: "assistant", text: "second" } + ] + ) + assert.strictEqual(payload.systemPrompt, "System") + assert.deepStrictEqual(payload.modelPreferences, { + hints: [{ name: "test-model" }], + costPriority: 0.2, + speedPriority: 0.4, + intelligencePriority: 0.8 + }) + assert.strictEqual(payload.maxTokens, 64) + assert.deepStrictEqual(payload.stopSequences, ["STOP"]) + assert.deepStrictEqual(payload.metadata, { request: "metadata" }) + assert.strictEqual(payload.includeContext, "thisServer") + })) + + it.effect("MUST accept and decode text sampling content", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => Effect.succeed(textResponse) + } + }) + + const result = yield* peer.client["sampling/createMessage"](samplingRequest).pipe( + Effect.flatMap(decodeSamplingResult) + ) + + assert.strictEqual(result.role, "assistant") + assert.strictEqual(result.model, "test-model") + assert.strictEqual(result.stopReason, "endTurn") + assert.deepStrictEqual(result.content, { + type: "text", + text: "sampled" + }) + })) + + it.effect("MUST accept image sampling content", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => + Effect.succeed({ + role: "assistant", + content: { + type: "image", + data: "AQID", + mimeType: "image/png" + }, + model: "vision-model" + }) + } + }) + + const result = yield* peer.client["sampling/createMessage"](samplingRequest).pipe( + Effect.flatMap(decodeSamplingResult) + ) + + assert.deepStrictEqual(result.content, { + type: "image", + data: new Uint8Array([1, 2, 3]), + mimeType: "image/png" + }) + })) + + it.effect("MUST accept audio sampling content", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => + Effect.succeed({ + role: "assistant", + content: { + type: "audio", + data: "BAUG", + mimeType: "audio/wav" + }, + model: "audio-model" + }) + } + }) + + const result = yield* peer.client["sampling/createMessage"](samplingRequest).pipe( + Effect.flatMap(decodeSamplingResult) + ) + + assert.deepStrictEqual(result.content, { + type: "audio", + data: new Uint8Array([4, 5, 6]), + mimeType: "audio/wav" + }) + })) + + it.effect("MUST surface sampling errors returned by the client", () => + Effect.gen(function*() { + const test = yield* McpConformance + const peer = yield* test.makePeer({ + capabilities: { sampling: {} }, + handlers: { + "sampling/createMessage": () => + Effect.fail( + new McpSchema.InternalError({ + message: "Sampling failed" + }) + ) + } + }) + + const error = yield* peer.client["sampling/createMessage"](samplingRequest).pipe(Effect.flip) + + assert.instanceOf(error, McpSchema.InternalError) + assert.strictEqual(error.message, "Sampling failed") + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ToolsTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ToolsTest.ts new file mode 100644 index 000000000..f50e794b7 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/ToolsTest.ts @@ -0,0 +1,394 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const decodeTools = Schema.decodeUnknownEffect(McpSchema.ListToolsResult) +const decodeCallTool = Schema.decodeUnknownEffect(McpSchema.CallToolResult) + +const callTool = (name: string, arguments_: Record = {}) => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name, arguments: arguments_ } + }) + return yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeCallTool(message.result)) + ) + }) + +const callToolWire = (name: string) => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name, arguments: {} } + }) + return yield* test.decodeResult(response) + }) + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Tools", () => { + // Identical requirements in the 2024-11-05, 2025-03-26, and 2025-06-18 specifications. + describe("Capabilities", () => { + it.effect("MUST advertise the tools capability when tools are registered", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.property(initialized.message.result.capabilities, "tools") + })) + + it.effect("MUST NOT advertise the tools capability when tools are not supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + + assert.notProperty(initialized.message.result.capabilities, "tools") + })) + + it.effect("MUST advertise listChanged when tool list change notifications are supported", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + + assert.strictEqual(initialized.message.result.capabilities.tools?.listChanged, true) + })) + }) + + describe("Listing Tools", () => { + it.effect("MUST list every tool visible to the initialized client", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeTools(message.result)) + ) + + const expected = [ + "AudioTool", + "DefectTool", + "EmbeddedResourceTool", + "ErrorTool", + "ImageTool", + "LogLevelTool", + "MultipleContentTool", + "ResourceLinkTool", + "StructuredTool", + "TestTool" + ].sort() + assert.deepStrictEqual(result.tools.map((tool) => tool.name).sort(), expected) + })) + + it.effect("SCHEMA preserves tool names and descriptions", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeTools(message.result)) + ) + + const tool = result.tools.find((tool) => tool.name === "TestTool") + assert.isDefined(tool) + assert.strictEqual(tool.description, "A test tool") + })) + + it.effect("MUST return each tool input schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeTools(message.result)) + ) + + assert.isTrue(result.tools.every((tool) => tool.inputSchema.type === "object")) + })) + it.effect("MUST return each declared tool output schema", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/list", + params: {} + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeTools(message.result)) + ) + + assert.strictEqual( + result.tools.find((tool) => tool.name === "StructuredTool")?.outputSchema?.type, + "object" + ) + const scalarTool = result.tools.find((tool) => tool.name === "TestTool") + assert.isDefined(scalarTool) + assert.notProperty(scalarTool, "outputSchema") + })) + }) + + describe("Calling Tools", () => { + it.effect("MUST call a registered tool with valid arguments", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "TestTool", + arguments: { value: "called" } + } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeCallTool(message.result)) + ) + + assert.strictEqual(result.isError, false) + assert.deepStrictEqual(result.content, [{ type: "text", text: JSON.stringify("called") }]) + })) + + it.effect("MUST reject an unknown tool name with a protocol error", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "UnknownTool", + arguments: {} + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("MUST reject arguments that do not match the input schema with a protocol error", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "TestTool", + arguments: { value: 123 } + } + }) + const error = yield* test.decodeError(response) + + assert.strictEqual(error.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + it.effect("MUST not invoke a tool handler when argument validation fails", () => + Effect.gen(function*() { + const test = yield* McpConformance + const before = (yield* test.observations).toolInvocations + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "TestTool", + arguments: { value: 123 } + } + }) + + assert.strictEqual((yield* test.observations).toolInvocations, before) + })) + it.effect("SCHEMA returns text content", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { + name: "TestTool", + arguments: { value: "text" } + } + }) + const result = yield* test.decodeResult(response).pipe( + Effect.flatMap((message) => decodeCallTool(message.result)) + ) + + assert.deepStrictEqual(result.content, [{ type: "text", text: JSON.stringify("text") }]) + })) + it.effect("SCHEMA returns image content", () => + Effect.gen(function*() { + const result = yield* callToolWire("ImageTool") + assert.deepStrictEqual(result.result.content, [{ + type: "image", + data: "AQID", + mimeType: "image/png" + }]) + })) + it.effect("SCHEMA returns audio content", () => + Effect.gen(function*() { + const result = yield* callToolWire("AudioTool") + assert.deepStrictEqual(result.result.content, [{ + type: "audio", + data: "BAUG", + mimeType: "audio/wav" + }]) + })) + it.effect("SCHEMA returns resource links", () => + Effect.gen(function*() { + const result = yield* callTool("ResourceLinkTool") + assert.deepStrictEqual(result.content, [{ + type: "resource_link", + uri: "file:///test", + name: "TestResource", + mimeType: "text/plain" + }]) + })) + it.effect("SCHEMA returns embedded resources", () => + Effect.gen(function*() { + const result = yield* callTool("EmbeddedResourceTool") + assert.deepStrictEqual(result.content, [{ + type: "resource", + resource: { + uri: "file:///embedded", + mimeType: "text/plain", + text: "embedded" + } + }]) + })) + it.effect("MUST return multiple content items in order", () => + Effect.gen(function*() { + const result = yield* callTool("MultipleContentTool") + assert.deepStrictEqual(result.content, [ + { type: "text", text: "first" }, + { type: "text", text: "second" } + ]) + })) + it.effect("SCHEMA returns structured content", () => + Effect.gen(function*() { + const result = yield* callTool("StructuredTool") + assert.deepStrictEqual(result.structuredContent, { value: "structured" }) + })) + it.effect("MUST return tool execution failures with isError", () => + Effect.gen(function*() { + const result = yield* callTool("ErrorTool") + assert.strictEqual(result.isError, true) + assert.deepStrictEqual(result.content, [{ type: "text", text: "expected failure" }]) + })) + it.effect("MUST keep tool execution errors distinct from protocol errors", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + + const execution = yield* callTool("ErrorTool") + const protocolResponse = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 3, + method: "tools/call", + params: { name: "UnknownTool", arguments: {} } + }) + const protocol = yield* test.decodeError(protocolResponse) + + assert.strictEqual(execution.isError, true) + assert.strictEqual(protocol.error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + })) + + it.effect("SHOULD not expose defects or internal error details", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize({ server: "features" }) + yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 2, + method: "tools/call", + params: { name: "DefectTool", arguments: {} } + }) + const result = yield* test.decodeError(response) + + assert.strictEqual(result.error.code, McpSchema.INTERNAL_ERROR_CODE) + assert.strictEqual(result.error.message, "Internal error") + assert.notMatch(JSON.stringify(result), /private defect details/) + })) + }) + + describe("List Changed Notification", () => { + it.effect("SHOULD send a tool list changed notification when the advertised list changes", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + const makeTool = (name: string) => ({ + tool: new McpSchema.Tool({ + name, + inputSchema: { type: "object", properties: {} } + }), + annotations: Context.empty(), + handle: () => Effect.succeed(new McpSchema.CallToolResult({ content: [] })) + }) + yield* fixture.server.addTool(makeTool("baseline-list-changed-tool")) + const initialized = yield* fixture.initialize() + const initializeResult = yield* Schema.decodeUnknownEffect(McpSchema.InitializeResult)(initialized.result) + assert.strictEqual( + initializeResult.capabilities.tools?.listChanged, + true + ) + + yield* fixture.server.addTool(makeTool("dynamic-list-changed-tool")) + const notification = yield* fixture.awaitOutboundMethod("notifications/tools/list_changed") + assert.strictEqual(notification.jsonrpc, "2.0") + assert.strictEqual(notification.method, "notifications/tools/list_changed") + assert.notProperty(notification, "id") + + const response = yield* fixture.sendRequest("tools/list", {}) + const result = yield* decodeTools(response.result) + assert.isTrue(result.tools.some((tool) => tool.name === "dynamic-list-changed-tool")) + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/TransportsTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/TransportsTest.ts new file mode 100644 index 000000000..686df560a --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/TransportsTest.ts @@ -0,0 +1,410 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as Schema from "effect/Schema" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { MCP_ENDPOINT } from "../TestUtils/McpHttpHarness.ts" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +const decodeErrorFrame = Schema.decodeUnknownEffect(Schema.Struct({ + id: Schema.Null, + error: McpSchema.McpError +})) + +const jsonRequest = (method: string, body?: unknown, headers?: HeadersInit) => { + const requestHeaders = new Headers({ + accept: "application/json, text/event-stream", + "content-type": "application/json" + }) + new Headers(headers).forEach((value, name) => requestHeaders.set(name, value)) + return new Request(MCP_ENDPOINT, { + method, + headers: requestHeaders, + ...(body === undefined ? {} : { body: JSON.stringify(body) }) + }) +} + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Transports", () => { + // https://modelcontextprotocol.io/specification/2024-11-05/basic/transports + // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + // https://modelcontextprotocol.io/specification/2025-06-18/basic/transports + describe("stdio", () => { + it.effect("MUST exchange compact UTF-8 newline-delimited JSON-RPC records", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: protocol.protocolVersion, + capabilities: {}, + clientInfo: { name: "stdio-client", version: "1.0.0" } + } + }) + const frame = yield* fixture.takeRawStdout + assert.strictEqual(frame.endsWith("\n"), true) + assert.strictEqual(frame.slice(0, -1).includes("\n"), false) + const message = JSON.parse(frame) + assert.deepInclude(message, { jsonrpc: "2.0", id: 1 }) + assert.property(message, "result") + })) + + it.effect("SCENARIO parses UTF-8 JSON-RPC records split across input chunks", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + const bytes = new TextEncoder().encode(`${ + JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: protocol.protocolVersion, + capabilities: {}, + clientInfo: { name: "stdio-🧪", version: "1.0.0" } + } + }) + }\n`) + const splitAt = bytes.indexOf(0xf0) + 2 + + yield* fixture.sendChunk(bytes.slice(0, splitAt)) + yield* fixture.sendChunk(bytes.slice(splitAt)) + + assert.deepInclude(yield* fixture.takeFrame, { + jsonrpc: "2.0", + id: 1 + }) + })) + + it.effect("SCENARIO processes consecutive stdio messages independently", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: protocol.protocolVersion, + capabilities: {}, + clientInfo: { name: "stdio-client", version: "1.0.0" } + } + }) + yield* fixture.takeFrame + yield* fixture.sendRaw({ jsonrpc: "2.0", id: 2, method: "ping", params: {} }) + yield* fixture.sendRaw({ jsonrpc: "2.0", id: 3, method: "ping", params: {} }) + assert.deepStrictEqual(yield* fixture.takeFrame, { + jsonrpc: "2.0", + id: 2, + result: {} + }) + assert.deepStrictEqual(yield* fixture.takeFrame, { + jsonrpc: "2.0", + id: 3, + result: {} + }) + })) + + it.effect("SCENARIO applies the revision-specific stdio batch policy", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: protocol.protocolVersion, + capabilities: {}, + clientInfo: { name: "stdio-client", version: "1.0.0" } + } + }) + yield* fixture.takeFrame + yield* fixture.sendRaw([ + { jsonrpc: "2.0", id: 2, method: "ping", params: {} }, + { jsonrpc: "2.0", id: 3, method: "ping", params: {} } + ]) + const response = yield* fixture.takeFrame.pipe(Effect.flatMap(decodeErrorFrame)) + assert.strictEqual(response.id, null) + assert.strictEqual(response.error.code, McpSchema.INVALID_REQUEST_ERROR_CODE) + })) + + it.effect("MUST shut down when the client closes stdin", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.close + const exit = yield* Fiber.await(fixture.serverFiber) + assert.isTrue( + Exit.isSuccess(exit) || + (Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)) + ) + })) + }) + + describe("Streamable HTTP", () => { + describe("Sending Messages to the Server", () => { + it.effect("MUST accept JSON-RPC requests through POST on the MCP endpoint", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.post(test.initializeRequest()) + + assert.strictEqual(response.status, 200) + })) + + it.effect("MUST accept JSON-RPC notifications through POST on the MCP endpoint", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const response = yield* test.notifyInitialized(initialized) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + + it.effect("MUST accept JSON-RPC responses through POST on the MCP endpoint", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 99, + result: {} + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + + it.effect("MUST require the application/json content type for POST requests", () => + Effect.gen(function*() { + const test = yield* McpConformance + const accepted = yield* test.request(jsonRequest("POST", test.initializeRequest(), { + "content-type": "Application/JSON; charset=utf-8" + })) + assert.strictEqual(accepted.status, 200) + + for (const contentType of ["text/plain", "application/json-malicious", ""]) { + const response = yield* test.request(jsonRequest("POST", test.initializeRequest(), { + "content-type": contentType + })) + assert.strictEqual(response.status, 415) + } + })) + + it.effect("MUST require clients to accept application/json and text/event-stream", () => + Effect.gen(function*() { + const test = yield* McpConformance + const accepted = yield* test.request(jsonRequest("POST", test.initializeRequest(), { + accept: " Text/Event-Stream; q=0.9, Application/JSON " + })) + assert.strictEqual(accepted.status, 200) + + for ( + const accept of [ + "application/json", + "text/event-stream", + "*/*", + "application/json, text/event-stream; q=0", + "application/json, text/event-stream; q=bogus", + "application/json, text/event-stream; q=2", + "application/json, text/event-stream; q=-1", + "application/json-malicious, text/event-stream", + "" + ] + ) { + const response = yield* test.request(jsonRequest("POST", test.initializeRequest(), { accept })) + assert.strictEqual(response.status, 406) + } + })) + + it.effect("MUST return application/json for a single JSON-RPC response", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.post(test.initializeRequest()) + + assert.match(response.headers.get("content-type") ?? "", /^application\/json\b/) + })) + it.effect("MUST return an empty 202 response for accepted notifications and responses", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const notification = yield* test.notifyInitialized(initialized) + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + id: 99, + result: {} + }) + + assert.strictEqual(notification.status, 202) + assert.strictEqual(yield* Effect.promise(() => notification.text()), "") + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + + it.effect("MUST reject unsupported HTTP methods with method not allowed", () => + Effect.gen(function*() { + const test = yield* McpConformance + for (const method of ["PUT", "PATCH", "HEAD"] as const) { + const response = yield* test.request(jsonRequest(method)) + assert.strictEqual(response.status, 405) + } + })) + }) + + describe("Listening for Messages from the Server", () => { + it.effect("MUST return method not allowed when GET SSE is not offered", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.request(jsonRequest("GET")) + assert.strictEqual(response.status, 405) + assert.strictEqual(response.headers.get("allow"), "POST") + })) + }) + + describe("Session Management", () => { + it.effect("SCENARIO returns an MCP session identifier during initialization", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + + assert.isNotNull(initialized.sessionId) + })) + it.effect("SCENARIO uses distinct UUIDv4 session identifiers", () => + Effect.gen(function*() { + const test = yield* McpConformance + const first = yield* test.initialize() + const second = yield* test.initialize({ id: 2 }) + assert.match( + first.sessionId ?? "", + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + ) + assert.match( + second.sessionId ?? "", + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + ) + assert.notStrictEqual(first.sessionId, second.sessionId) + })) + it.effect("MUST require the returned session identifier on subsequent HTTP requests", () => + Effect.gen(function*() { + const test = yield* McpConformance + yield* test.initialize() + const response = yield* test.request(jsonRequest("POST", test.pingRequest(), { + "Mcp-Protocol-Version": protocol.protocolVersion + })) + assert.strictEqual(response.status, 400) + })) + it.effect("MUST reject an unknown session identifier with not found", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.request(jsonRequest("POST", test.pingRequest(), { + "Mcp-Protocol-Version": protocol.protocolVersion, + "Mcp-Session-Id": "unknown-session" + })) + + assert.strictEqual(response.status, 404) + })) + it.effect("SCENARIO declines client session termination without invalidating the session", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + assert.isNotNull(initialized.sessionId) + const response = yield* test.request(jsonRequest("DELETE", undefined, { + "Mcp-Protocol-Version": protocol.protocolVersion, + "Mcp-Session-Id": initialized.sessionId + })) + assert.strictEqual(response.status, 405) + assert.strictEqual(response.headers.get("allow"), "POST") + assert.strictEqual((yield* test.ping(initialized)).status, 200) + })) + it.effect("MUST reject initialize requests carrying a session identifier", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const repeated = yield* test.send(initialized, test.initializeRequest({ id: 2 })) + assert.strictEqual(repeated.status, 400) + const unknown = yield* test.request(jsonRequest("POST", test.initializeRequest({ id: 3 }), { + "Mcp-Session-Id": "unknown-session" + })) + assert.strictEqual(unknown.status, 404) + })) + it.effect("SCENARIO keeps two distinct POST sessions live", () => + Effect.gen(function*() { + const test = yield* McpConformance + const first = yield* test.initialize() + const second = yield* test.initialize({ id: 2 }) + assert.strictEqual((yield* test.ping(first, { id: 3 })).status, 200) + assert.strictEqual((yield* test.ping(second, { id: 4 })).status, 200) + })) + }) + + describe("Protocol Version Header", () => { + it.effect("MUST apply the revision-specific protocol header requirement", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const response = yield* test.ping(initialized, { + includeProtocolVersion: false + }) + assert.strictEqual(response.status, 400) + })) + it.effect("MUST accept the negotiated protocol version", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const response = yield* test.ping(initialized, { + includeProtocolVersion: true, + protocolVersion: protocol.protocolVersion + }) + assert.strictEqual(response.status, 200) + })) + it.effect("MUST reject an unsupported protocol version with bad request", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + const response = yield* test.ping(initialized, { + includeProtocolVersion: true, + protocolVersion: "2099-01-01" + }) + assert.strictEqual(response.status, 400) + })) + it.effect("SCENARIO replays the selected protocol version on HTTP responses", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + assert.strictEqual( + initialized.response.headers.get("Mcp-Protocol-Version"), + protocol.protocolVersion + ) + const response = yield* test.ping(initialized, { includeProtocolVersion: true }) + assert.strictEqual(response.headers.get("Mcp-Protocol-Version"), protocol.protocolVersion) + })) + }) + + describe("Security", () => { + it.effect("MUST validate the Origin header before every MCP route", () => + Effect.gen(function*() { + const test = yield* McpConformance + assert.strictEqual((yield* test.post(test.initializeRequest())).status, 200) + assert.strictEqual( + (yield* test.request(jsonRequest("POST", test.initializeRequest({ id: 2 }), { + origin: "https://allowed.example" + }))).status, + 200 + ) + for (const method of ["POST", "GET", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const) { + const response = yield* test.request(jsonRequest( + method, + method === "POST" ? test.initializeRequest({ id: 3 }) : undefined, + { origin: "https://attacker.example" } + )) + assert.strictEqual(response.status, 403) + } + })) + }) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/UtilitiesTest.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/UtilitiesTest.ts new file mode 100644 index 000000000..08194faeb --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpConformance/UtilitiesTest.ts @@ -0,0 +1,223 @@ +import { assert, describe, it } from "@effect/vitest" +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 type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import { makeMcpStdioHarness } from "../TestUtils/McpStdioHarness.ts" +import { McpConformance, type McpConformanceLayer } from "./McpConformance.ts" + +export const suite = (protocol: McpProtocol.ProtocolAdapter, layer: McpConformanceLayer) => + it.layer(layer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Utilities", () => { + describe("Ping", () => { + // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping + it.effect("MUST respond to a client ping with an empty result", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.ping(initialized, { id: 42 }) + const message = yield* test.decodeResult(response) + + assert.strictEqual(response.status, 200) + assert.strictEqual(message.id, 42) + assert.deepStrictEqual(message.result, {}) + })) + }) + + describe("Cancellation", () => { + // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation + it.effect("MUST not send a response to a cancellation notification", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { + requestId: "unknown-request", + reason: "No longer needed" + } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + it.effect("SHOULD stop work and suppress the response after cancellation", () => + Effect.gen(function*() { + const entered = yield* Deferred.make() + const release = yield* Deferred.make() + const interrupted = yield* Deferred.make() + yield* Effect.addFinalizer(() => Deferred.succeed(release, void 0)) + const fixture = yield* makeMcpStdioHarness(protocol) + const cancelledRequestId = "cancelled-tool-call" + const pingRequestId = "post-cancellation-ping" + + yield* fixture.server.addTool({ + tool: new McpSchema.Tool({ + name: "GatedTool", + inputSchema: { type: "object", properties: {} } + }), + annotations: Context.empty(), + handle: () => + Deferred.succeed(entered, void 0).pipe( + Effect.andThen(Deferred.await(release)), + Effect.onInterrupt(() => Deferred.succeed(interrupted, void 0)), + Effect.as( + new McpSchema.CallToolResult({ + content: [{ type: "text", text: "released" }] + }) + ) + ) + }) + + yield* fixture.initialize() + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: cancelledRequestId, + method: "tools/call", + params: { name: "GatedTool", arguments: {} } + }) + yield* Deferred.await(entered) + yield* fixture.sendNotification("notifications/cancelled", { + requestId: cancelledRequestId, + reason: "No longer needed" + }) + yield* Deferred.await(interrupted) + yield* fixture.sendRaw({ + jsonrpc: "2.0", + id: pingRequestId, + method: "ping" + }) + + while (true) { + const message = yield* fixture.takeMessage + assert.notStrictEqual(message.id, cancelledRequestId) + if (message.id === pingRequestId) { + assert.deepStrictEqual(message.result, {}) + break + } + } + })) + it.effect("SHOULD ignore cancellation for an unknown request identifier", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { requestId: 999 } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + it.effect("SHOULD allow a later request to reuse an unknown cancelled identifier", () => + Effect.gen(function*() { + const fixture = yield* makeMcpStdioHarness(protocol) + yield* fixture.initialize() + yield* fixture.sendNotification("notifications/cancelled", { + requestId: "reused-id" + }) + + const reused = yield* fixture.sendRequest("ping", undefined, "reused-id").pipe(Effect.forkChild) + yield* Effect.yieldNow + const control = yield* fixture.sendRequest("ping", undefined, "control-id") + assert.deepStrictEqual(control.result, {}) + yield* Effect.yieldNow + + const reusedExit = reused.pollUnsafe() + assert(reusedExit !== undefined && Exit.isSuccess(reusedExit)) + assert.deepStrictEqual(reusedExit.value.result, {}) + })) + it.effect("SHOULD ignore cancellation for an already completed request identifier", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + yield* test.ping(initialized, { id: 11 }) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/cancelled", + params: { requestId: 11 } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + + describe("Progress", () => { + // https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress + // NOTE: Smoke test only. The client capability accepts this one-way notification, + // but McpServer does not expose an observer for its decoded payload. + it.effect("MUST accept string progress tokens", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: "task-1", + progress: 1 + } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + // NOTE: Smoke test only. The client capability accepts this one-way notification, + // but McpServer does not expose an observer for its decoded payload. + it.effect("MUST accept numeric progress tokens", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: 12, + progress: 1 + } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + // NOTE: Smoke test only. The client capability accepts this one-way notification, + // but McpServer does not expose an observer for its decoded payload. + it.effect("SCHEMA accepts the optional total", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: "task-with-total", + progress: 1, + total: 2 + } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + }) + }) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/McpServer.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpServer.test.ts new file mode 100644 index 000000000..7a0ad15a5 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/McpServer.test.ts @@ -0,0 +1,675 @@ +import { assert, describe, it } from "@effect/vitest" +import { assertTrue, strictEqual } from "@effect/vitest/utils" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Queue from "effect/Queue" +import * as Schema from "effect/Schema" +import * as Sink from "effect/Sink" +import * as Stdio from "effect/Stdio" +import * as Stream from "effect/Stream" +import * as AiError from "effect/unstable/ai/AiError" +import * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as McpServer from "effect/unstable/ai/McpServer" +import * as Tool from "effect/unstable/ai/Tool" +import * as Toolkit from "effect/unstable/ai/Toolkit" +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpRouter from "effect/unstable/http/HttpRouter" +import { RpcSerialization } from "effect/unstable/rpc" +import * as RpcClient from "effect/unstable/rpc/RpcClient" +import type * as RpcMessage from "effect/unstable/rpc/RpcMessage" +import * as RpcServer from "effect/unstable/rpc/RpcServer" +import { makeHttpHarness } from "./TestUtils/McpHttpHarness.ts" +import { makeServerLayer } from "./TestUtils/McpServerLayer.ts" + +const OptionalStringTool = Tool.make("OptionalStringTool", { + parameters: Schema.Struct({ signature: Schema.optional(Schema.String) }), + success: Schema.String +}) + +const PublicFailureTool = Tool.make("PublicFailureTool", { + success: Schema.String, + failure: Schema.ErrorInstance() +}) + +const InternalAiErrorTool = Tool.make("InternalAiErrorTool", { + success: Schema.String +}) + +const DefectTool = Tool.make("DefectTool", { + success: Schema.String +}) + +const UntypedTool = Tool.make("UntypedTool") + +const StructuredResultTool = Tool.make("StructuredResultTool", { + success: Schema.Struct({ answer: Schema.String }) +}) + +const AnnotatedVoidTool = Tool.make("AnnotatedVoidTool", { + success: Schema.Void.annotate({ description: "No output" }) +}) + +const TestToolkit = Toolkit.make( + OptionalStringTool, + PublicFailureTool, + InternalAiErrorTool, + DefectTool, + UntypedTool, + StructuredResultTool, + AnnotatedVoidTool +) +type TestToolkitHandlers = Toolkit.HandlersFrom> + +const testToolkitHandlers = TestToolkit.of({ + OptionalStringTool: ({ signature }) => Effect.succeed(signature ?? "omitted"), + PublicFailureTool: () => Effect.fail(new Error("Public failure")), + InternalAiErrorTool: () => Effect.fail(new AiError.RateLimitError({})), + DefectTool: () => Effect.die("private defect details"), + UntypedTool: () => Effect.void, + StructuredResultTool: () => Effect.succeed({ answer: "result" }), + AnnotatedVoidTool: () => Effect.void +}) + +const INTERNAL_TOOL_ERROR_MESSAGE = "Tool execution failed due to an internal server error." + +const TestServerLayer = makeServerLayer({ name: "TestServer" }) + +const initializePayload = { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { + name: "TestClient", + version: "1.0.0" + } +} + +const pingBody = { + jsonrpc: "2.0", + method: "ping", + params: {}, + id: 0 +} + +const makeTestClientWith = Effect.fnUntraced(function*( + serverLayer: Layer.Layer, + options?: { + readonly routerLayer?: Layer.Layer | undefined + } | undefined +) { + const harness = yield* makeHttpHarness(serverLayer, options) + + const clientLayer = RpcClient.layerProtocolHttp({ + url: "http://localhost/mcp", + transformClient: HttpClient.mapRequest( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream") + ) + }).pipe( + Layer.provideMerge([FetchHttpClient.layer, RpcSerialization.layerJsonRpc()]), + Layer.provide(Layer.succeed(FetchHttpClient.Fetch, harness.fetch)) + ) + const client = yield* RpcClient.make(McpSchema.ClientRpcs).pipe( + Effect.provide(clientLayer) + ) + + const httpClient = yield* HttpClient.HttpClient.pipe( + Effect.provide(clientLayer) + ) + + return { client, responses: harness.responses, httpClient } +}) + +const makeTestClient = makeTestClientWith(TestServerLayer) + +const makeRouterTestClient = ( + router: Layer.Layer +) => makeTestClientWith(TestServerLayer, { routerLayer: router }) + +const makeToolkitTestClient = Effect.fnUntraced(function*(handlers: TestToolkitHandlers = testToolkitHandlers) { + const serverLayer = McpServer.toolkit(TestToolkit).pipe( + Layer.provideMerge(TestToolkit.toLayer(handlers)), + Layer.provide(TestServerLayer) + ) + const { client } = yield* makeTestClientWith(serverLayer) + yield* client.initialize({ + protocolVersion: "9999-01-01", + capabilities: {}, + clientInfo: { + name: "TestClient", + version: "1.0.0" + } + }) + return client +}) + +const toolResultText = (result: McpSchema.CallToolResult): string => { + const content = result.content[0] + assertTrue(content?.type === "text", "Expected text tool-result content") + return content.text +} + +describe("McpServer", () => { + it.effect("should reject browser Origins by default while accepting Origin-less clients", () => + Effect.gen(function*() { + const harness = yield* makeHttpHarness(TestServerLayer) + assert.strictEqual( + (yield* harness.post({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: initializePayload + })).status, + 200 + ) + assert.strictEqual( + (yield* harness.post({ + jsonrpc: "2.0", + id: 2, + method: "initialize", + params: initializePayload + }, { origin: "https://browser.example" })).status, + 403 + ) + })) + + it.effect("should replay the selected protocol header when a session is initialized", () => + Effect.gen(function*() { + const { client, responses } = yield* makeTestClient + + yield* client.initialize({ + protocolVersion: "2025-03-26", + capabilities: {}, + clientInfo: { + name: "TestClient", + version: "1.0.0" + } + }) + + yield* client.ping({}) + + strictEqual(responses.length, 2) + strictEqual(responses[0].headers.get("Mcp-Protocol-Version"), "2025-06-18") + strictEqual(responses[1].headers.get("Mcp-Protocol-Version"), "2025-06-18") + })) + + it.effect("should return 400 when a non-initialize request omits the MCP session id", () => + Effect.gen(function*() { + const { httpClient } = yield* makeTestClient + + const response = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe({ jsonrpc: "2.0", method: "ping", params: {}, id: 0 }), + httpClient.execute + ) + + strictEqual(response.status, 400) + })) + describe("registerToolkit", () => { + it.effect("lists output schemas only for structured tool results", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const result = yield* client["tools/list"]({}) + const structuredTool = result.tools.find((tool) => tool.name === "StructuredResultTool") + const scalarTool = result.tools.find((tool) => tool.name === "OptionalStringTool") + const untypedTool = result.tools.find((tool) => tool.name === "UntypedTool") + const annotatedVoidTool = result.tools.find((tool) => tool.name === "AnnotatedVoidTool") + + assert.deepStrictEqual(structuredTool?.outputSchema, { + type: "object", + properties: { answer: { type: "string" } }, + required: ["answer"], + additionalProperties: false + }) + assertTrue(scalarTool !== undefined) + assert.isFalse("outputSchema" in scalarTool) + assertTrue(untypedTool !== undefined) + assert.isFalse("outputSchema" in untypedTool) + assertTrue(annotatedVoidTool !== undefined) + assert.isFalse("outputSchema" in annotatedVoidTool) + })) + + it.effect("returns concise parameter-validation errors without invoking the handler", () => + Effect.gen(function*() { + let handlerInvoked = false + const client = yield* makeToolkitTestClient(TestToolkit.of({ + ...testToolkitHandlers, + OptionalStringTool: ({ signature }) => { + handlerInvoked = true + return Effect.succeed(signature ?? "omitted") + } + })) + + const error = yield* client["tools/call"]({ + name: "OptionalStringTool", + arguments: { signature: null } + }).pipe(Effect.flip) + + assert.isFalse(handlerInvoked) + assert.instanceOf(error, McpSchema.InvalidParams) + assert.match(error.message, /Invalid parameters for tool 'OptionalStringTool'/) + assert.match(error.message, /Expected string \| undefined/) + assert.match(error.message, /at \["signature"\]/) + })) + + it.effect("preserves successful results when optional parameters are omitted", () => + Effect.gen(function*() { + let handlerInvoked = false + const client = yield* makeToolkitTestClient(TestToolkit.of({ + ...testToolkitHandlers, + OptionalStringTool: ({ signature }) => { + handlerInvoked = true + return Effect.succeed(signature ?? "omitted") + } + })) + + const result = yield* client["tools/call"]({ + name: "OptionalStringTool", + arguments: {} + }) + + assert.isTrue(handlerInvoked) + assert.deepStrictEqual( + result, + new McpSchema.CallToolResult({ + isError: false, + content: [{ type: "text", text: JSON.stringify("omitted") }] + }) + ) + })) + + it.effect("keeps void tool results successful", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const result = yield* client["tools/call"]({ + name: "UntypedTool", + arguments: {} + }) + + assert.deepStrictEqual( + result, + new McpSchema.CallToolResult({ + isError: false, + content: [] + }) + ) + })) + + it.effect("returns schema-validated messages for declared handler failures", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const result = yield* client["tools/call"]({ + name: "PublicFailureTool", + arguments: {} + }) + + assert.strictEqual(result.isError, true) + const text = toolResultText(result) + assert.strictEqual(text, "Public failure") + })) + + it.effect("returns a generic message for non-validation AiError failures", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const result = yield* client["tools/call"]({ + name: "InternalAiErrorTool", + arguments: {} + }) + + assert.strictEqual(result.isError, true) + const text = toolResultText(result) + assert.strictEqual(text, INTERNAL_TOOL_ERROR_MESSAGE) + })) + + it.effect("returns a generic message for handler defects", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const result = yield* client["tools/call"]({ + name: "DefectTool", + arguments: {} + }) + + assert.strictEqual(result.isError, true) + const text = toolResultText(result) + assert.strictEqual(text, INTERNAL_TOOL_ERROR_MESSAGE) + })) + + it.effect("keeps unknown tools as protocol errors", () => + Effect.gen(function*() { + const client = yield* makeToolkitTestClient() + + const error = yield* client["tools/call"]({ + name: "UnknownTool", + arguments: {} + }).pipe(Effect.flip) + + assert.instanceOf(error, McpSchema.InvalidParams) + assert.strictEqual(error.code, McpSchema.INVALID_PARAMS_ERROR_CODE) + assert.strictEqual(error.message, "Tool 'UnknownTool' not found") + })) + }) + + it.effect("rejects unsupported HTTP methods without disturbing an initialized session", () => + Effect.gen(function*() { + const { client, httpClient } = yield* makeTestClient + + yield* client.initialize(initializePayload) + + for (const method of ["GET", "PUT", "PATCH", "DELETE", "HEAD"] as const) { + const response = yield* HttpClientRequest.make(method)("http://localhost/mcp").pipe( + httpClient.execute + ) + strictEqual(response.status, 405) + strictEqual(response.headers["allow"], "POST") + } + + yield* client.ping({}) + })) + + it.effect("returns an empty 202 for notifications and responses and remains successful for request POSTs", () => + Effect.gen(function*() { + const { client, httpClient } = yield* makeRouterTestClient(HttpRouter.cors()) + + yield* client.initialize(initializePayload) + + const notificationResponse = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe({ + jsonrpc: "2.0", + method: "notifications/initialized", + params: {} + }), + httpClient.execute + ) + strictEqual(notificationResponse.status, 202) + strictEqual(yield* notificationResponse.text, "") + strictEqual(notificationResponse.headers["content-type"], undefined) + strictEqual(notificationResponse.headers["access-control-allow-origin"], "*") + strictEqual(notificationResponse.headers["mcp-protocol-version"], "2025-06-18") + + const responseOnly = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe({ jsonrpc: "2.0", id: 1, result: {} }), + httpClient.execute + ) + strictEqual(responseOnly.status, 202) + strictEqual(yield* responseOnly.text, "") + + const pingResponse = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe(pingBody), + httpClient.execute + ) + strictEqual(pingResponse.status, 200) + const pingResponseBody = yield* pingResponse.text + strictEqual(pingResponseBody.length > 0, true) + })) + + it.effect("validates supplied protocol versions on POST", () => + Effect.gen(function*() { + const { client, httpClient } = yield* makeRouterTestClient(HttpRouter.cors()) + + yield* client.initialize(initializePayload) + + const unsupportedResponse = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe(pingBody), + HttpClientRequest.setHeader("Mcp-Protocol-Version", "9999-01-01"), + httpClient.execute + ) + strictEqual(unsupportedResponse.status, 400) + strictEqual(yield* unsupportedResponse.text, "") + strictEqual(unsupportedResponse.headers["access-control-allow-origin"], "*") + + const responseOnly = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe({ jsonrpc: "2.0", id: 1, result: {} }), + HttpClientRequest.setHeader("Mcp-Protocol-Version", "9999-01-01"), + httpClient.execute + ) + strictEqual(responseOnly.status, 400) + strictEqual(yield* responseOnly.text, "") + + const absentVersionResponse = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe(pingBody), + httpClient.execute + ) + strictEqual(absentVersionResponse.status, 200) + + for (const protocolVersion of ["2025-03-26", "2024-11-05", "2024-10-07"]) { + const response = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe(pingBody), + HttpClientRequest.setHeader("Mcp-Protocol-Version", protocolVersion), + httpClient.execute + ) + strictEqual(response.status, 400) + } + + const declaredVersionResponse = yield* HttpClientRequest.post("http://localhost/mcp").pipe( + HttpClientRequest.setHeader("accept", "application/json, text/event-stream"), + HttpClientRequest.bodyJsonUnsafe(pingBody), + HttpClientRequest.setHeader("Mcp-Protocol-Version", "2025-06-18"), + httpClient.execute + ) + strictEqual(declaredVersionResponse.status, 200) + })) + describe("protocol selection", () => { + it.effect("should select June when an unsupported version is offered", () => + Effect.gen(function*() { + const { client, responses } = yield* makeTestClient + + const result = yield* client.initialize({ + protocolVersion: "2024-10-07", + capabilities: {}, + clientInfo: { + name: "TestClient", + version: "1.0.0" + } + }) + + strictEqual(result.protocolVersion, "2025-06-18") + strictEqual(responses[0].headers.get("Mcp-Protocol-Version"), "2025-06-18") + })) + }) + + describe("resource subscriptions", () => { + it.effect("should isolate resource update subscriptions between sessions", () => + Effect.gen(function*() { + const clientIds = new Set([1, 2]) + const client1Outbound = yield* Queue.unbounded< + RpcMessage.FromServerEncoded | RpcMessage.RequestEncoded + >() + const client2Outbound = yield* Queue.unbounded< + RpcMessage.FromServerEncoded | RpcMessage.RequestEncoded + >() + const disconnects = yield* Queue.unbounded() + const writeRequest = yield* Deferred.make< + (clientId: number, message: RpcMessage.FromClientEncoded) => Effect.Effect + >() + const protocol = yield* RpcServer.Protocol.make((write) => + Deferred.succeed(writeRequest, write).pipe( + Effect.as({ + disconnects, + send: (clientId, message) => + Queue.offer(clientId === 1 ? client1Outbound : client2Outbound, message).pipe(Effect.asVoid), + end: (_clientId) => Effect.void, + clientIds: Effect.succeed(clientIds), + initialMessage: Effect.succeedNone, + supportsAck: false, + supportsTransferables: false, + supportsSpanPropagation: false + }) + ) + ) + const ready = yield* Deferred.make() + yield* Effect.gen(function*() { + const context = yield* Layer.build( + McpServer.resource({ + uri: "file:///target", + name: "Target", + content: Effect.succeed("target") + }).pipe( + Layer.provideMerge( + McpServer.layer({ + name: "TestServer", + version: "1.0.0", + protocols: [McpProtocol.v2025_06_18] + }).pipe(Layer.provide(Layer.succeed(RpcServer.Protocol, protocol))) + ) + ) + ) + yield* Deferred.succeed(ready, Context.get(context, McpServer.McpServer)) + return yield* Effect.never + }).pipe(Effect.scoped, Effect.forkScoped) + const server = yield* Deferred.await(ready) + const send = yield* Deferred.await(writeRequest) + const nextResponse = Effect.fnUntraced(function*(clientId: number, requestId: number) { + while (true) { + const message = yield* Queue.take(clientId === 1 ? client1Outbound : client2Outbound) + if (message._tag === "Exit" && message.requestId === requestId) { + return message + } + } + }) + const nextResourceUpdate = Effect.fnUntraced(function*(clientId: number) { + while (true) { + const message = yield* Queue.take(clientId === 1 ? client1Outbound : client2Outbound) + if (message._tag === "Request" && message.tag === "notifications/resources/updated") { + return yield* Schema.decodeUnknownEffect( + McpSchema.ResourceUpdatedNotification.payloadSchema + )(message.payload) + } + } + }) + const request = Effect.fnUntraced(function*( + clientId: number, + id: number, + method: string, + payload: unknown, + isNotification = false + ) { + yield* send(clientId, { + _tag: "Request", + id, + tag: method, + payload, + headers: [], + ...(isNotification ? { isNotification: true as const } : {}) + }) + if (!isNotification) { + yield* nextResponse(clientId, id) + } + }) + const initialize = (clientId: number) => + request(clientId, clientId, "initialize", initializePayload).pipe( + Effect.andThen(request(clientId, clientId + 10, "notifications/initialized", {}, true)) + ) + + yield* initialize(1) + yield* initialize(2) + yield* request(1, 21, "resources/subscribe", { uri: "file:///target" }) + yield* request(2, 22, "resources/subscribe", { uri: "file:///sentinel" }) + + yield* server.notifications["notifications/resources/updated"]({ uri: "file:///target" }) + yield* server.notifications["notifications/resources/updated"]({ uri: "file:///sentinel" }) + + assert.strictEqual((yield* nextResourceUpdate(1)).uri, "file:///target") + assert.strictEqual((yield* nextResourceUpdate(2)).uri, "file:///sentinel") + assert.isTrue(Option.isNone(yield* Queue.poll(client1Outbound))) + assert.isTrue(Option.isNone(yield* Queue.poll(client2Outbound))) + })) + }) + + describe("stdio", () => { + it.effect("should preserve the June wire transcript when requests use stdio", () => + Effect.gen(function*() { + const stdin = yield* Queue.unbounded() + const stdout = yield* Queue.unbounded() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const stdioLayer = Stdio.layerTest({ + stdin: Stream.fromQueue(stdin), + stdout: () => Sink.forEach((chunk) => Queue.offer(stdout, chunk)) + }) + + const ready = yield* Deferred.make() + yield* Effect.gen(function*() { + yield* Layer.build( + McpServer.layerStdio({ + name: "TestServer", + version: "1.0.0", + protocols: [McpProtocol.v2025_06_18] + }).pipe(Layer.provide(stdioLayer)) + ) + yield* Deferred.succeed(ready, undefined) + return yield* Effect.never + }).pipe( + Effect.scoped, + Effect.forkScoped + ) + yield* Deferred.await(ready) + + const write = (message: unknown) => Queue.offer(stdin, encoder.encode(`${JSON.stringify(message)}\n`)) + const read = Effect.fnUntraced(function*() { + const chunk = yield* Queue.take(stdout) + const frame = typeof chunk === "string" ? chunk : decoder.decode(chunk) + assert.strictEqual(frame.endsWith("\n"), true) + return JSON.parse(frame) + }) + + yield* write({ + jsonrpc: "2.0", + id: 0, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { + name: "TestClient", + version: "1.0.0" + } + } + }) + + assert.deepStrictEqual(yield* read(), { + jsonrpc: "2.0", + id: 0, + result: { + protocolVersion: "2025-06-18", + capabilities: { + completions: {}, + logging: {} + }, + serverInfo: { + name: "TestServer", + version: "1.0.0" + } + } + }) + + yield* write({ + jsonrpc: "2.0", + id: 1, + method: "ping", + params: {} + }) + + assert.deepStrictEqual(yield* read(), { + jsonrpc: "2.0", + id: 1, + result: {} + }) + })) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpHttpHarness.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpHttpHarness.ts new file mode 100644 index 000000000..d2f41aab6 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpHttpHarness.ts @@ -0,0 +1,60 @@ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as HttpRouter from "effect/unstable/http/HttpRouter" + +export const MCP_ENDPOINT = "http://localhost/mcp" + +export const makeHttpHarness = Effect.fnUntraced(function*( + serverLayer: Layer.Layer, + options?: { + readonly routerLayer?: Layer.Layer | undefined + } +) { + const appLayer = options?.routerLayer ? Layer.merge(serverLayer, options.routerLayer) : serverLayer + const { dispose, handler } = HttpRouter.toWebHandler(appLayer, { disableLogger: true }) + yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) + const responses: Array = [] + let sessionId: string | null = null + let protocolVersion: string | null = null + + const fetchImpl = async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = input instanceof Request ? input : new Request(input, init) + if (sessionId !== null) { + request.headers.set("Mcp-Session-Id", sessionId) + } + if (protocolVersion !== null && !request.headers.has("Mcp-Protocol-Version")) { + request.headers.set("Mcp-Protocol-Version", protocolVersion) + } + const response = await handler(request) + sessionId = response.headers.get("Mcp-Session-Id") ?? sessionId + protocolVersion = response.headers.get("Mcp-Protocol-Version") ?? protocolVersion + responses.push(response.clone()) + return response + } + const fetch: typeof globalThis.fetch = Object.assign(fetchImpl, { preconnect() {} }) + + const postText = (body: string, headers?: HeadersInit) => + Effect.promise(() => + handler( + new Request(MCP_ENDPOINT, { + method: "POST", + headers: { + accept: "application/json, text/event-stream", + "content-type": "application/json", + ...headers + }, + body + }) + ) + ) + + const post = (body: unknown, headers?: HeadersInit) => postText(JSON.stringify(body), headers) + + return { + handler, + fetch, + post, + postText, + responses + } as const +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpServerLayer.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpServerLayer.ts new file mode 100644 index 000000000..6d6e49f40 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpServerLayer.ts @@ -0,0 +1,33 @@ +import { constVoid } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" +import * as References from "effect/References" +import * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpServer from "effect/unstable/ai/McpServer" + +const noopLogger = Logger.make(constVoid) + +export const makeServerLayer = (options: { + readonly name: string + readonly version?: string | undefined + readonly protocols?: + | readonly [ + McpProtocol.ProtocolAdapter, + ...Array + ] + | undefined + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined +}) => + McpServer.layerHttp({ + name: options.name, + version: options.version ?? "1.0.0", + path: "/mcp", + protocols: options.protocols ?? [McpProtocol.v2025_06_18], + allowedOrigins: ["https://allowed.example"], + extensions: options.extensions + }).pipe( + Layer.provideMerge(Layer.succeed( + References.CurrentLoggers, + new Set([noopLogger]) + )) + ) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpStdioHarness.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpStdioHarness.ts new file mode 100644 index 000000000..40bf916ec --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/TestUtils/McpStdioHarness.ts @@ -0,0 +1,213 @@ +import type * as Cause from "effect/Cause" +import * as Context from "effect/Context" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import type * as Fiber from "effect/Fiber" +import * as Layer from "effect/Layer" +import * as Queue from "effect/Queue" +import * as Sink from "effect/Sink" +import * as Stdio from "effect/Stdio" +import * as Stream from "effect/Stream" +import type * as McpProtocol from "effect/unstable/ai/McpProtocol" +import type * as McpSchema from "effect/unstable/ai/McpSchema" +import * as McpServer from "effect/unstable/ai/McpServer" + +export interface JsonRpcMessage { + readonly jsonrpc: "2.0" + readonly id?: string | number | null | undefined + readonly method?: string | undefined + readonly params?: unknown + readonly result?: unknown + readonly error?: unknown +} + +export interface McpStdioHarness { + readonly server: McpServer.McpServer["Service"] + readonly serverFiber: Fiber.Fiber + readonly close: Effect.Effect + readonly sendRaw: (message: unknown) => Effect.Effect + readonly sendChunk: (chunk: string | Uint8Array) => Effect.Effect + readonly sendRequest: ( + method: string, + params?: unknown, + id?: string | number + ) => Effect.Effect + readonly sendNotification: (method: string, params?: unknown) => Effect.Effect + readonly initialize: ( + capabilities?: typeof McpSchema.ClientCapabilities.Type + ) => Effect.Effect + readonly takeMessage: Effect.Effect + readonly takeFrame: Effect.Effect + readonly takeRawStdout: Effect.Effect + readonly takeStderr: Effect.Effect + readonly awaitOutboundMethod: (method: string) => Effect.Effect + readonly respond: (id: string | number, result: unknown) => Effect.Effect +} + +const isJsonRpcMessage = (value: unknown): value is JsonRpcMessage => + typeof value === "object" && value !== null && (value as JsonRpcMessage).jsonrpc === "2.0" + +const isResponse = ( + message: JsonRpcMessage +): message is JsonRpcMessage & { readonly id: string | number } => + (typeof message.id === "string" || typeof message.id === "number") && + message.method === undefined + +const requestKey = (id: string | number) => `${typeof id}:${id}` + +export const makeMcpStdioHarness = Effect.fnUntraced(function*( + protocol: McpProtocol.ProtocolAdapter, + protocols: ReadonlyArray = [protocol] +) { + const stdin = yield* Queue.unbounded() + const stdout = yield* Queue.unbounded() + const stderr = yield* Queue.unbounded() + const frames = yield* Queue.unbounded() + const messages = yield* Queue.unbounded() + const rawStdout = yield* Queue.unbounded() + const rawStderr = yield* Queue.unbounded() + const responseQueues = new Map>() + const retainedMessages: Array = [] + const encoder = new TextEncoder() + const stdoutDecoder = new TextDecoder() + const stderrDecoder = new TextDecoder() + let nextRequestId = 1 + + const stdioLayer = Stdio.layerTest({ + stdin: Stream.fromQueue(stdin), + stdout: () => Sink.forEach((chunk) => Queue.offer(stdout, chunk)), + stderr: () => Sink.forEach((chunk) => Queue.offer(stderr, chunk)) + }) + const ready = yield* Deferred.make() + const serverFiber = yield* Effect.gen(function*() { + const context = yield* Layer.build( + McpServer.layerStdio({ + name: "McpConformance", + version: "1.0.0", + protocols: protocols as [ + McpProtocol.ProtocolAdapter, + ...Array + ] + }).pipe(Layer.provide(stdioLayer)) + ) + yield* Deferred.succeed(ready, Context.get(context, McpServer.McpServer)) + return yield* Effect.never + }).pipe(Effect.scoped, Effect.forkScoped) + const server = yield* Deferred.await(ready) + + const routeFrame = Effect.fnUntraced(function*(frame: unknown) { + yield* Queue.offer(frames, frame) + if (!isJsonRpcMessage(frame)) { + return + } + if (isResponse(frame)) { + const responseQueue = responseQueues.get(requestKey(frame.id)) + if (responseQueue !== undefined) { + yield* Queue.offer(responseQueue, frame) + return + } + } + yield* Queue.offer(messages, frame) + }) + + yield* Effect.gen(function*() { + let pending = "" + while (true) { + const chunk = yield* Queue.take(stdout) + const text = typeof chunk === "string" + ? chunk + : stdoutDecoder.decode(chunk, { stream: true }) + yield* Queue.offer(rawStdout, text) + pending += text + let newline = pending.indexOf("\n") + while (newline !== -1) { + const line = pending.slice(0, newline) + pending = pending.slice(newline + 1) + if (line.length > 0) { + yield* routeFrame(JSON.parse(line)) + } + newline = pending.indexOf("\n") + } + } + }).pipe(Effect.forkScoped) + + yield* Effect.gen(function*() { + while (true) { + const chunk = yield* Queue.take(stderr) + yield* Queue.offer( + rawStderr, + typeof chunk === "string" ? chunk : stderrDecoder.decode(chunk, { stream: true }) + ) + } + }).pipe(Effect.forkScoped) + + const sendChunk = (chunk: string | Uint8Array) => + Queue.offer(stdin, typeof chunk === "string" ? encoder.encode(chunk) : chunk) + const sendRaw = (message: unknown) => sendChunk(`${JSON.stringify(message)}\n`) + const sendNotification = (method: string, params?: unknown) => + sendRaw({ + jsonrpc: "2.0", + method, + ...(params === undefined ? {} : { params }) + }) + const sendRequest = Effect.fnUntraced(function*( + method: string, + params?: unknown, + id: string | number = nextRequestId++ + ) { + const responseQueue = yield* Queue.unbounded() + const key = requestKey(id) + responseQueues.set(key, responseQueue) + yield* sendRaw({ + jsonrpc: "2.0", + id, + method, + ...(params === undefined ? {} : { params }) + }) + return yield* Queue.take(responseQueue).pipe( + Effect.ensuring(Effect.sync(() => responseQueues.delete(key))) + ) + }) + const takeMessage = Effect.suspend(() => { + const retained = retainedMessages.shift() + return retained === undefined ? Queue.take(messages) : Effect.succeed(retained) + }) + const awaitOutboundMethod = Effect.fnUntraced(function*(method: string) { + const retainedIndex = retainedMessages.findIndex((message) => message.method === method) + if (retainedIndex !== -1) { + return retainedMessages.splice(retainedIndex, 1)[0]! + } + while (true) { + const message = yield* Queue.take(messages) + if (message.method === method) { + return message + } + retainedMessages.push(message) + } + }) + + return { + server, + serverFiber, + close: Queue.end(stdin), + sendRaw, + sendChunk, + sendRequest, + sendNotification, + initialize: Effect.fnUntraced(function*(capabilities = {}) { + const response = yield* sendRequest("initialize", { + protocolVersion: protocol.protocolVersion, + capabilities, + clientInfo: { name: "stdio-client", version: "1.0.0" } + }) + yield* sendNotification("notifications/initialized") + return response + }), + takeMessage, + takeFrame: Queue.take(frames), + takeRawStdout: Queue.take(rawStdout), + takeStderr: Queue.take(rawStderr), + awaitOutboundMethod, + respond: (id, result) => sendRaw({ jsonrpc: "2.0", id, result }) + } satisfies McpStdioHarness +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/utils.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/utils.ts new file mode 100644 index 000000000..fc1aea5b4 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/utils.ts @@ -0,0 +1,64 @@ +import * as Effect from "effect/Effect" +import { constVoid } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Logger from "effect/Logger" +import * as References from "effect/References" +import * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpServer from "effect/unstable/ai/McpServer" +import * as HttpRouter from "effect/unstable/http/HttpRouter" + +export const MCP_ENDPOINT = "http://localhost/mcp" + +const noopLogger = Logger.make(constVoid) + +export const makeServerLayer = (options: { + readonly name: string + readonly version?: string | undefined + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined +}) => + McpServer.layerHttp({ + name: options.name, + version: options.version ?? "1.0.0", + path: "/mcp", + protocols: [McpProtocol.v2025_06_18], + extensions: options.extensions + }).pipe( + Layer.provideMerge(Layer.succeed( + References.CurrentLoggers, + new Set([noopLogger]) + )), + Layer.orDie + ) + +export const makeWebHandler = Effect.fnUntraced(function*( + serverLayer: Layer.Layer, + options?: { + readonly routerLayer?: Layer.Layer | undefined + } +) { + const appLayer = options?.routerLayer ? Layer.merge(serverLayer, options.routerLayer) : serverLayer + const { dispose, handler } = HttpRouter.toWebHandler(appLayer, { disableLogger: true }) + yield* Effect.addFinalizer(() => Effect.promise(() => dispose())) + return handler +}) + +export const makeRawHttpHarness = Effect.fnUntraced(function*( + serverLayer: Layer.Layer +) { + const handler = yield* makeWebHandler(serverLayer) + const post = (body: unknown, headers?: HeadersInit) => + Effect.promise(() => + handler( + new Request(MCP_ENDPOINT, { + method: "POST", + headers: { + accept: "application/json, text/event-stream", + "content-type": "application/json", + ...headers + }, + body: JSON.stringify(body) + }) + ) + ) + return { post } as const +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/McpServer/v2025_06_18.test.ts b/.context/effect/packages/effect/test/unstable/ai/McpServer/v2025_06_18.test.ts new file mode 100644 index 000000000..4f3bbf507 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/McpServer/v2025_06_18.test.ts @@ -0,0 +1,99 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Schema from "effect/Schema" +import * as McpProtocol from "effect/unstable/ai/McpProtocol" +import * as McpSchema from "effect/unstable/ai/McpSchema" +import * as BaseProtocolTest from "./McpConformance/BaseProtocolTest.ts" +import * as CompletionTest from "./McpConformance/CompletionTest.ts" +import * as ElicitationTest from "./McpConformance/ElicitationTest.ts" +import * as LifecycleTest from "./McpConformance/LifecycleTest.ts" +import * as LoggingTest from "./McpConformance/LoggingTest.ts" +import { layer as makeMcpConformanceLayer, McpConformance } from "./McpConformance/McpConformance.ts" +import * as PromptsTest from "./McpConformance/PromptsTest.ts" +import * as ResourcesTest from "./McpConformance/ResourcesTest.ts" +import * as RootsTest from "./McpConformance/RootsTest.ts" +import * as SamplingTest from "./McpConformance/SamplingTest.ts" +import * as ToolsTest from "./McpConformance/ToolsTest.ts" +import * as TransportsTest from "./McpConformance/TransportsTest.ts" +import * as UtilitiesTest from "./McpConformance/UtilitiesTest.ts" + +it("accepts tools/call without optional arguments", () => { + const decoded = Schema.decodeUnknownExit(McpSchema.CallTool.payloadSchema)({ name: "ping" }) + assert.strictEqual(decoded._tag, "Success") + if (decoded._tag === "Success") { + assert.deepStrictEqual(decoded.value.arguments, {}) + } +}) + +const protocol = McpProtocol.v2025_06_18 +const testLayer = makeMcpConformanceLayer(protocol) + +LifecycleTest.suite(protocol, testLayer) +BaseProtocolTest.suite(protocol, testLayer) +TransportsTest.suite(protocol, testLayer) +UtilitiesTest.suite(protocol, testLayer) +ToolsTest.suite(protocol, testLayer) +ResourcesTest.suite(protocol, testLayer) +PromptsTest.suite(protocol, testLayer) +CompletionTest.suite(protocol, testLayer) +LoggingTest.suite(protocol, testLayer) +RootsTest.suite(protocol, testLayer) +SamplingTest.suite(protocol, testLayer) +ElicitationTest.suite(protocol, testLayer) + +it.layer(testLayer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => { + describe("Utilities", () => { + describe("Progress", () => { + // NOTE: Smoke test only. The client capability accepts this one-way notification, + // but McpServer does not expose an observer for its decoded payload. + it.effect("SCHEMA accepts the optional progress message", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + yield* test.notifyInitialized(initialized) + + const response = yield* test.send(initialized, { + jsonrpc: "2.0", + method: "notifications/progress", + params: { + progressToken: "task-with-message", + progress: 1, + message: "Working" + } + }) + + assert.strictEqual(response.status, 202) + assert.strictEqual(yield* Effect.promise(() => response.text()), "") + })) + }) + }) + + describe("Transport-specific behavior", () => { + it.effect("MUST reject JSON-RPC batches", () => + Effect.gen(function*() { + const test = yield* McpConformance + const response = yield* test.post([test.initializeRequest()]) + + assert.isAtLeast(response.status, 400) + })) + + it.effect("MUST require the negotiated protocol-version header after initialization", () => + Effect.gen(function*() { + const test = yield* McpConformance + const initialized = yield* test.initialize() + assert.isNotNull(initialized.sessionId) + + const missing = yield* test.ping(initialized, { + includeProtocolVersion: false + }) + const mismatched = yield* test.ping(initialized, { + id: 3, + protocolVersion: "2025-03-26" + }) + + assert.strictEqual(initialized.message.result.protocolVersion, protocol.protocolVersion) + assert.isAtLeast(missing.status, 400) + assert.isAtLeast(mismatched.status, 400) + })) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts index 6e0ab6e11..03d88757b 100644 --- a/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutput.test.ts @@ -5,7 +5,16 @@ import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" import * as Tool from "effect/unstable/ai/Tool" function assertJsonSchema(schema: Schema.Constraint, expected: JsonSchema.JsonSchema) { - assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected) + if (expected.type === "object" && expected.anyOf === undefined) { + assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, expected) + return + } + assert.deepStrictEqual(toCodecOpenAI(Schema.Struct({ value: schema })).jsonSchema, { + type: "object", + properties: { value: expected }, + required: ["value"], + additionalProperties: false + }) } function assertError(schema: Schema.Constraint, message: string) { @@ -13,21 +22,61 @@ function assertError(schema: Schema.Constraint, message: string) { } describe("toCodecOpenAI", () => { - describe("Unsupported", () => { - it("Undefined", () => { - assertError(Schema.Undefined, "Unsupported AST Undefined") + describe("Canonical JSON and mechanical invariants", () => { + describe("Root", () => { + const message = `OpenAiStructuredOutput: Root JSON Schema must have type "object" and must not use "anyOf"` + + it("rejects primitive roots", () => { + assertError(Schema.String, message) + }) + + it("rejects array roots", () => { + assertError(Schema.Array(Schema.String), message) + }) + + it("rejects anyOf roots", () => { + assertError( + Schema.Union([ + Schema.Struct({ a: Schema.String }), + Schema.Struct({ b: Schema.String }) + ]), + message + ) + }) + + it("rejects unconstrained roots", () => { + assertError(Schema.Any, message) + assertError(Schema.Unknown, message) + assertError(Schema.Never, message) + }) + }) + + it("encodes Undefined as null", () => { + assertJsonSchema(Schema.Undefined, { type: "null" }) }) - it("Literal with unsupported type", () => { - assertError(Schema.Literal(1n), "Unsupported literal type bigint") + it("encodes a bigint Literal as a string", () => { + assertJsonSchema(Schema.Literal(1n), { type: "string", enum: ["1"] }) }) describe("Arrays", () => { - it("post-rest elements", () => { - assertError( - Schema.TupleWithRest(Schema.Tuple([]), [Schema.String, Schema.String]), - "Post-rest elements are not supported for arrays" - ) + it("encodes post-rest elements as object properties", async () => { + const schema = Schema.TupleWithRest(Schema.Tuple([]), [Schema.String, Schema.String]) + const result = toCodecOpenAI(schema) + assert.deepStrictEqual(result.jsonSchema, { + type: "object", + properties: { + __rest__: { type: "array", items: { type: "string" } }, + __tail_0__: { type: "string" } + }, + required: ["__rest__", "__tail_0__"], + additionalProperties: false, + description: + "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements. Post-rest elements use '__tail_0__', '__tail_1__', and so on" + }) + const asserts = new TestSchema.Asserts(result.codec) + await asserts.encoding().succeed(["a", "b"], { __rest__: ["a"], __tail_0__: "b" }) + await asserts.decoding().succeed({ __rest__: ["a"], __tail_0__: "b" }, ["a", "b"]) }) }) @@ -35,7 +84,7 @@ describe("toCodecOpenAI", () => { it("non-string property signature name", () => { assertError( Schema.Struct({ [Symbol.for("effect/Schema/test/a")]: Schema.String }), - "Property names must be strings" + "Objects property names must be strings" ) }) }) @@ -224,9 +273,7 @@ describe("toCodecOpenAI", () => { assertJsonSchema(Schema.Number, { "anyOf": [ { "type": "number" }, - { "type": "string", "enum": ["NaN"] }, - { "type": "string", "enum": ["Infinity"] }, - { "type": "string", "enum": ["-Infinity"] } + { "type": "string", "enum": ["Infinity", "-Infinity", "NaN"] } ] }) }) @@ -242,10 +289,10 @@ describe("toCodecOpenAI", () => { }) }) - it("Finite + supported format", () => { + it("Finite + string format", () => { assertJsonSchema(Schema.Finite.annotate({ format: "duration" }), { "type": "number", - "format": "duration" + "description": "a value with a format of duration" }) }) @@ -326,10 +373,10 @@ describe("toCodecOpenAI", () => { }) }) - it("Int + supported format", () => { + it("Int + string format", () => { assertJsonSchema(Schema.Int.annotate({ format: "duration" }), { "type": "integer", - "format": "duration" + "description": "a value with a format of duration" }) }) @@ -506,9 +553,11 @@ describe("toCodecOpenAI", () => { const encoding = asserts.encoding() await encoding.succeed(["a", 1], { "0": "a", "1": 1 }) + await encoding.succeed(["a"], { "0": "a", "1": null }) const decoding = asserts.decoding() await decoding.succeed({ "0": "a", "1": 1 }, ["a", 1]) + await decoding.succeed({ "0": "a", "1": null }, ["a"]) }) }) @@ -605,7 +654,7 @@ describe("toCodecOpenAI", () => { "required": ["name"], "additionalProperties": false, "$defs": { - "Person": { + "PersonEncoded": { "type": "object", "properties": { "name": { "type": "string" } @@ -648,14 +697,64 @@ describe("toCodecOpenAI", () => { "additionalProperties": false } }) - const codec = toCodecOpenAI(schema).codec + const codec = toCodecOpenAI(Schema.Struct({ value: schema })).codec const asserts = new TestSchema.Asserts(codec) const encoding = asserts.encoding() - await encoding.succeed({ "a": 1, "b": 2 }, [{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }]) + await encoding.succeed( + { value: { "a": 1, "b": 2 } }, + { value: [{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }] } + ) const decoding = asserts.decoding() - await decoding.succeed([{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }], { "a": 1, "b": 2 }) + await decoding.succeed( + { value: [{ 0: "a", 1: 1 }, { 0: "b", 1: 2 }] }, + { value: { "a": 1, "b": 2 } } + ) + }) + + it("Record with properties and an index signature", async () => { + const schema = Schema.Record( + Schema.Union([Schema.Literal("fixed"), Schema.String]), + Schema.String + ) + const expected = { + type: "array", + description: "Object encoded as array of [key, value] pairs. Apply object constraints to the decoded object", + items: { + type: "object", + description: + "Tuple encoded as an object with numeric string keys ('0', '1', ...). If present, '__rest__' contains remaining elements", + properties: { + "0": { + anyOf: [ + { type: "string", enum: ["fixed"] }, + { type: "string" } + ] + }, + "1": { type: "string" } + }, + required: ["0", "1"], + additionalProperties: false + } + } as const + assertJsonSchema(schema, expected) + + const result = toCodecOpenAI(Schema.Struct({ value: schema })) + + const asserts = new TestSchema.Asserts(result.codec) + await asserts.encoding().succeed( + { value: { fixed: "required", dynamic: "value" } }, + { value: [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }] } + ) + await asserts.decoding().succeed( + { value: [{ 0: "fixed", 1: "required" }, { 0: "dynamic", 1: "value" }] }, + { value: { fixed: "required", dynamic: "value" } } + ) + assert.strictEqual( + Schema.decodeUnknownExit(result.codec)({ value: [{ 0: "dynamic", 1: "value" }] })._tag, + "Failure" + ) }) it("Record(String, Finite) + description", () => { @@ -678,7 +777,7 @@ describe("toCodecOpenAI", () => { }) }) - it("Record(String, Finite) + isMinProperties", () => { + it("Record(String, Finite) + isMinProperties", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(2)) assertJsonSchema(schema, { "type": "array", @@ -694,8 +793,15 @@ describe("toCodecOpenAI", () => { }, "required": ["0", "1"], "additionalProperties": false - } + }, + "minItems": 2 }) + const result = toCodecOpenAI(Schema.Struct({ value: schema })) + await new TestSchema.Asserts(result.codec).decoding().fail( + { value: [{ 0: "a", 1: 1 }, { 0: "a", 1: 2 }] }, + `Expected a value with at least 2 entries + at ["value"]` + ) }) it("Record(String, Finite) + isMinProperties + description", () => { @@ -716,7 +822,8 @@ describe("toCodecOpenAI", () => { }, "required": ["0", "1"], "additionalProperties": false - } + }, + "minItems": 2 }) }) }) diff --git a/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutputRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutputRepresentation.test.ts new file mode 100644 index 000000000..075df27f6 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/OpenAiStructuredOutputRepresentation.test.ts @@ -0,0 +1,250 @@ +import { assert, describe, it } from "@effect/vitest" +import { type JsonSchema, Schema } from "effect" +import { TestSchema } from "effect/testing" +import { toCodecOpenAI } from "effect/unstable/ai/OpenAiStructuredOutput" + +function getValueSchema(jsonSchema: JsonSchema.JsonSchema): JsonSchema.JsonSchema { + return (jsonSchema.properties as Record).value +} + +function toValueSchema(schema: Schema.Constraint): JsonSchema.JsonSchema { + return getValueSchema(toCodecOpenAI(Schema.Struct({ value: schema })).jsonSchema) +} + +describe("OpenAiStructuredOutput representation v2", () => { + it("projects the encoded side before JSON Schema generation", () => { + assert.strictEqual(toValueSchema(Schema.FiniteFromString).type, "string") + }) + + it("keeps supported custom JSON Schema filters with a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.startsWith("a"), { + description: "starts with a", + representation: { + id: "test/ai/openai/startsWithA", + payload: null + }, + toJsonSchema: () => ({ pattern: "^a" }) + })) + + assert.deepStrictEqual(toValueSchema(schema), { + type: "string", + description: "starts with a", + pattern: "^a" + }) + }) + + it("drops unsupported custom JSON Schema filters with a description", async () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + description: "at least two characters", + representation: { + id: "test/ai/openai/minTwoCharactersWithDescription", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + const result = toCodecOpenAI(Schema.Struct({ value: schema })) + assert.deepStrictEqual(getValueSchema(result.jsonSchema), { + type: "string", + description: "at least two characters" + }) + await new TestSchema.Asserts(result.codec).decoding().fail( + { value: "a" }, + `Expected + at ["value"]` + ) + }) + + it("keeps supported custom JSON Schema filters without a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.startsWith("a"), { + representation: { + id: "test/ai/openai/startsWithAWithoutDescription", + payload: null + }, + toJsonSchema: () => ({ pattern: "^a" }) + })) + + assert.deepStrictEqual(toValueSchema(schema), { + type: "string", + pattern: "^a" + }) + }) + + it("drops unsupported custom JSON Schema filters without a description", () => { + const schema = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/ai/openai/minTwoCharactersWithoutDescription", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(toValueSchema(schema), { + type: "string" + }) + }) + + it("invokes custom JSON Schema callbacks only with compiler inputs", () => { + let invocations = 0 + const schema = Schema.String.check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/openai/compilerInputs", + payload: null, + schemas: [Schema.Finite.ast] + }, + toJsonSchema: ({ type, schemas }) => { + invocations++ + assert.strictEqual(type, "string") + assert.strictEqual(schemas.length, 1) + assert.strictEqual(schemas[0].type, "number") + return { pattern: "^a" } + } + })) + + assert.deepStrictEqual(toValueSchema(schema), { + type: "string", + pattern: "^a" + }) + assert.strictEqual(invocations, 1) + }) + + it("compiles custom callbacks after structural transformations", () => { + let invocations = 0 + const schema = Schema.Record(Schema.String, Schema.Finite).check( + Schema.makeFilter(() => true, { + representation: { + id: "test/ai/openai/structuralCompilerInput", + payload: null + }, + toJsonSchema: ({ type }) => { + invocations++ + assert.strictEqual(type, "array") + return { minItems: 1 } + } + }) + ) + + const jsonSchema = toValueSchema(schema) + assert.strictEqual(jsonSchema.type, "array") + assert.strictEqual(jsonSchema.minItems, 1) + assert.strictEqual(invocations, 1) + }) + + it("keeps property names that match unsupported keywords", () => { + const unsupported = Schema.Any.check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/openai/unsupportedPropertySchema", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + + assert.deepStrictEqual(toCodecOpenAI(Schema.Struct({ minLength: unsupported })).jsonSchema, { + type: "object", + properties: { minLength: {} }, + required: ["minLength"], + additionalProperties: false + }) + }) + + it("drops patternProperties returned by custom callbacks", () => { + const schema = Schema.Struct({ fixed: Schema.String }).check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/openai/patternProperties", + payload: null + }, + toJsonSchema: () => ({ + patternProperties: { + "^x": { type: "string" } + } + }) + })) + + assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, { + type: "object", + properties: { fixed: { type: "string" } }, + required: ["fixed"], + additionalProperties: false + }) + }) + + it("does not merge structural constraints from allOf", () => { + const schema = Schema.Struct({ fixed: Schema.String }).check(Schema.makeFilter(() => true, { + representation: { + id: "test/ai/openai/structuralAllOf", + payload: null + }, + toJsonSchema: () => ({ + properties: { + extra: { type: "string" } + }, + required: ["extra"] + }) + })) + + assert.deepStrictEqual(toCodecOpenAI(schema).jsonSchema, { + type: "object", + properties: { fixed: { type: "string" } }, + required: ["fixed"], + additionalProperties: false + }) + }) + + it("keeps the original codec on the unchanged fast path", () => { + const schema = Schema.Struct({ value: Schema.String }) + assert.strictEqual(toCodecOpenAI(schema).codec, schema) + }) + + it("keeps declaration fallback validation in the codec", () => { + class Opaque { + readonly _tag = "Opaque" + } + const schema = Schema.declare((input): input is Opaque => input instanceof Opaque, { + expected: "Opaque" + }) + const result = toCodecOpenAI(Schema.Struct({ value: schema })) + assert.ok(Object.keys(getValueSchema(result.jsonSchema)).length > 0) + assert.strictEqual(Schema.decodeUnknownExit(result.codec)({ value: {} })._tag, "Failure") + }) + + it("merges repeated lower bounds conservatively", () => { + assert.deepStrictEqual( + toValueSchema(Schema.Finite.check( + Schema.isGreaterThan(1), + Schema.isGreaterThanOrEqualTo(2), + Schema.isGreaterThan(2) + )), + { + type: "number", + description: "a value greater than 1 and a value greater than or equal to 2 and a value greater than 2", + exclusiveMinimum: 2 + } + ) + }) + + it("merges repeated array bounds conservatively", () => { + const jsonSchema = toValueSchema( + Schema.Array(Schema.String).check( + Schema.isMinLength(1), + Schema.isMinLength(2), + Schema.isMaxLength(5), + Schema.isMaxLength(4) + ) + ) + assert.strictEqual(jsonSchema.minItems, 2) + assert.strictEqual(jsonSchema.maxItems, 4) + }) + + it("transports only sound record size bounds to entry arrays", () => { + const maximum = toValueSchema( + Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(3)) + ) + assert.isUndefined(maximum.maxItems) + + const between = toValueSchema( + Schema.Record(Schema.String, Schema.Finite).check(Schema.isPropertiesLengthBetween(1, 3)) + ) + assert.strictEqual(between.minItems, 1) + assert.isUndefined(between.maxItems) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/Prompt.test.ts b/.context/effect/packages/effect/test/unstable/ai/Prompt.test.ts index 81f599529..a246f86e5 100644 --- a/.context/effect/packages/effect/test/unstable/ai/Prompt.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/Prompt.test.ts @@ -1,7 +1,59 @@ import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" import { Prompt, Response } from "effect/unstable/ai" describe("Prompt", () => { + describe("part schemas", () => { + it("exports schemas for all parts and message-specific parts", () => { + const parts = { + text: { type: "text", text: "hello" }, + reasoning: { type: "reasoning", text: "thinking" }, + file: { type: "file", mediaType: "text/plain", data: "hello" }, + toolCall: { type: "tool-call", id: "call-1", name: "tool", params: {} }, + toolResult: { type: "tool-result", id: "call-1", name: "tool", isFailure: false, result: "done" }, + toolApprovalResponse: { + type: "tool-approval-response", + approvalId: "approval-1", + approved: true + }, + toolApprovalRequest: { + type: "tool-approval-request", + approvalId: "approval-1", + toolCallId: "call-1" + } + } as const + + const decodePart = Schema.decodeUnknownExit(Prompt.Part) + for (const part of Object.values(parts)) { + assert.strictEqual(decodePart(part)._tag, "Success") + } + + const decodeUserMessagePart = Schema.decodeUnknownExit(Prompt.UserMessagePart) + for (const part of [parts.text, parts.file]) { + assert.strictEqual(decodeUserMessagePart(part)._tag, "Success") + } + + const decodeAssistantMessagePart = Schema.decodeUnknownExit(Prompt.AssistantMessagePart) + for ( + const part of [ + parts.text, + parts.file, + parts.reasoning, + parts.toolCall, + parts.toolResult, + parts.toolApprovalRequest + ] + ) { + assert.strictEqual(decodeAssistantMessagePart(part)._tag, "Success") + } + + const decodeToolMessagePart = Schema.decodeUnknownExit(Prompt.ToolMessagePart) + for (const part of [parts.toolResult, parts.toolApprovalResponse]) { + assert.strictEqual(decodeToolMessagePart(part)._tag, "Success") + } + }) + }) + describe("fromResponseParts", () => { it("folds streamed text and reasoning deltas into an assistant message", () => { const parts = [ @@ -29,6 +81,145 @@ describe("Prompt", () => { assert.deepStrictEqual(prompt, expected) }) + it("preserves metadata on non-streaming response parts", () => { + const parts = [ + Response.makePart("text", { + text: "Hello", + metadata: { test: { value: "text" } } + }), + Response.makePart("reasoning", { + text: "Thinking", + metadata: { openai: { encryptedContent: "encrypted-reasoning" } } + }), + Response.makePart("tool-call", { + id: "call-1", + name: "get_weather", + params: { city: "London" }, + providerExecuted: false, + metadata: { google: { thoughtSignature: "signed-call" } } + }), + Response.makePart("tool-result", { + id: "call-1", + name: "get_weather", + isFailure: false, + result: { temp: 20 }, + encodedResult: { temp: 20 }, + preliminary: false, + providerExecuted: false, + metadata: { test: { value: "tool-result" } } + }) + ] + + const prompt = Prompt.fromResponseParts(parts) + + assert.deepStrictEqual( + prompt, + Prompt.make([ + { + role: "assistant", + content: [ + { type: "text", text: "Hello", options: { test: { value: "text" } } }, + { + type: "reasoning", + text: "Thinking", + options: { openai: { encryptedContent: "encrypted-reasoning" } } + }, + { + type: "tool-call", + id: "call-1", + name: "get_weather", + params: { city: "London" }, + providerExecuted: false, + options: { google: { thoughtSignature: "signed-call" } } + } + ] + }, + { + role: "tool", + content: [{ + type: "tool-result", + id: "call-1", + name: "get_weather", + isFailure: false, + result: { temp: 20 }, + providerExecuted: false, + options: { test: { value: "tool-result" } } + }] + } + ]) + ) + }) + + it("accumulates metadata across streamed text and reasoning parts", () => { + const parts = [ + Response.makePart("text-start", { + id: "text-1", + metadata: { testStart: { value: "text-start" } } + }), + Response.makePart("text-delta", { + id: "text-1", + delta: "Hello", + metadata: { testDelta: { value: "text-delta" } } + }), + Response.makePart("text-end", { + id: "text-1", + metadata: { testEnd: { value: "text-end" } } + }), + Response.makePart("reasoning-start", { + id: "reasoning-1", + metadata: { openai: { itemId: "reasoning-item" } } + }), + Response.makePart("reasoning-delta", { + id: "reasoning-1", + delta: "Thinking" + }), + Response.makePart("reasoning-delta", { + id: "reasoning-1", + delta: "", + metadata: { testDelta: { value: "reasoning-delta" } } + }), + Response.makePart("reasoning-end", { + id: "reasoning-1", + metadata: { + openai: { encryptedContent: "encrypted-reasoning" }, + testEnd: { value: "reasoning-end" } + } + }) + ] + + const prompt = Prompt.fromResponseParts(parts) + + assert.deepStrictEqual( + prompt, + Prompt.make([{ + role: "assistant", + content: [ + { + type: "text", + text: "Hello", + options: { + testStart: { value: "text-start" }, + testDelta: { value: "text-delta" }, + testEnd: { value: "text-end" } + } + }, + { + type: "reasoning", + text: "Thinking", + options: { + openai: { + itemId: "reasoning-item", + encryptedContent: "encrypted-reasoning" + }, + testDelta: { value: "reasoning-delta" }, + testEnd: { value: "reasoning-end" } + } + } + ] + }]) + ) + }) + it("places tool calls in assistant messages and tool results in tool messages", () => { const parts = [ Response.makePart("tool-call", { @@ -65,6 +256,59 @@ describe("Prompt", () => { assert.strictEqual(typeof toolContent[0] === "object" && toolContent[0].type, "tool-result") }) + it("places provider-executed tool results in the assistant message", () => { + const parts = [ + Response.makePart("tool-call", { + id: "ws-1", + name: "web_search", + params: {}, + providerExecuted: true + }), + Response.makePart("tool-result", { + id: "ws-1", + name: "web_search", + isFailure: false, + result: { sources: 3 }, + encodedResult: { sources: 3 }, + preliminary: false, + providerExecuted: true + }), + Response.makePart("tool-call", { + id: "call-1", + name: "get_weather", + params: { city: "London" }, + providerExecuted: false + }), + Response.makePart("tool-result", { + id: "call-1", + name: "get_weather", + isFailure: false, + result: { temp: 20 }, + encodedResult: { temp: 20 }, + preliminary: false, + providerExecuted: false + }) + ] + const prompt = Prompt.fromResponseParts(parts) + + // Provider-executed pair stays in the assistant message; only the + // framework-executed result forms a tool message + assert.strictEqual(prompt.content.length, 2) + assert.strictEqual(prompt.content[0].role, "assistant") + assert.strictEqual(prompt.content[1].role, "tool") + + const assistantContent = prompt.content[0].content + assert.strictEqual(assistantContent.length, 3) + assert.strictEqual(typeof assistantContent[1] === "object" && assistantContent[1].type, "tool-result") + assert.deepStrictEqual((assistantContent[1] as any).id, "ws-1") + assert.strictEqual((assistantContent[1] as any).providerExecuted, true) + + const toolContent = prompt.content[1].content + assert.strictEqual(toolContent.length, 1) + assert.deepStrictEqual((toolContent[0] as any).id, "call-1") + assert.strictEqual((toolContent[0] as any).providerExecuted, false) + }) + it("should handle out-of-order tool results (result before call in stream)", () => { // This simulates concurrent tool execution where results may arrive // in different order than their corresponding calls diff --git a/.context/effect/packages/effect/test/unstable/ai/Response.test.ts b/.context/effect/packages/effect/test/unstable/ai/Response.test.ts new file mode 100644 index 000000000..466413496 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/Response.test.ts @@ -0,0 +1,111 @@ +import { describe, it } from "@effect/vitest" +import { deepStrictEqual } from "@effect/vitest/utils" +import { Effect, Schema } from "effect" +import { Response } from "effect/unstable/ai" + +describe("Response", () => { + it.effect("decodes response metadata with omitted optional fields", () => + Effect.gen(function*() { + const encoded: Response.ResponseMetadataPartEncoded = { + type: "response-metadata" + } + + const decoded = yield* Schema.decodeUnknownEffect(Response.ResponseMetadataPart)(encoded) + + deepStrictEqual(decoded, Response.makePart("response-metadata", {})) + })) + + it.effect("round trips response metadata with undefined optional fields through JSON", () => + Effect.gen(function*() { + const part = Response.makePart("response-metadata", { + id: undefined, + modelId: undefined, + timestamp: undefined, + request: undefined + }) + + const encoded = yield* Schema.encodeEffect(Response.ResponseMetadataPart)(part) + const json = JSON.parse(JSON.stringify(encoded)) + const decoded = yield* Schema.decodeUnknownEffect(Response.ResponseMetadataPart)(json) + + deepStrictEqual(json, { + metadata: {}, + type: "response-metadata" + }, "encoded JSON") + deepStrictEqual(decoded, Response.makePart("response-metadata", {}), "decoded part") + })) + + it.effect("round trips HTTP request details with an undefined hash through JSON", () => + Effect.gen(function*() { + const request: typeof Response.HttpRequestDetails.Type = { + method: "POST", + url: "https://example.com/v1/responses", + urlParams: [], + hash: undefined, + headers: {} + } + + const encoded = yield* Schema.encodeEffect(Response.HttpRequestDetails)(request) + const json = JSON.parse(JSON.stringify(encoded)) + const decoded = yield* Schema.decodeUnknownEffect(Response.HttpRequestDetails)(json) + + deepStrictEqual(json, { + method: "POST", + url: "https://example.com/v1/responses", + urlParams: [], + headers: {} + }, "encoded JSON") + deepStrictEqual(decoded, { + method: "POST", + url: "https://example.com/v1/responses", + urlParams: [], + headers: {} + }, "decoded request") + })) + + it.effect("round trips a finish part with undefined optional fields through JSON", () => + Effect.gen(function*() { + const part = Response.makePart("finish", { + reason: "stop", + usage: new Response.Usage({ + inputTokens: { + uncached: undefined, + total: undefined, + cacheRead: undefined, + cacheWrite: undefined + }, + outputTokens: { + total: undefined, + text: undefined, + reasoning: undefined + } + }), + response: undefined + }) + + const encoded = yield* Schema.encodeEffect(Response.FinishPart)(part) + const json = JSON.parse(JSON.stringify(encoded)) + const decoded = yield* Schema.decodeUnknownEffect(Response.FinishPart)(json) + + deepStrictEqual(json, { + metadata: {}, + type: "finish", + reason: "stop", + usage: { + inputTokens: {}, + outputTokens: {} + } + }, "encoded JSON") + deepStrictEqual( + decoded, + Response.makePart("finish", { + reason: "stop", + usage: new Response.Usage({ + inputTokens: {}, + outputTokens: {} + }) + }), + "decoded part" + ) + })) +}) diff --git a/.context/effect/packages/effect/test/unstable/ai/ResponseIdTracker.test.ts b/.context/effect/packages/effect/test/unstable/ai/ResponseIdTracker.test.ts index c19a81dee..42d2ab971 100644 --- a/.context/effect/packages/effect/test/unstable/ai/ResponseIdTracker.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/ResponseIdTracker.test.ts @@ -27,7 +27,8 @@ const toolResultMessage = (id: string) => id, name: "test_tool", result: { ok: true }, - isFailure: false + isFailure: false, + providerExecuted: false }) ] }) diff --git a/.context/effect/packages/effect/test/unstable/ai/Tool.test.ts b/.context/effect/packages/effect/test/unstable/ai/Tool.test.ts index 974cbcc29..b92d2d5c3 100644 --- a/.context/effect/packages/effect/test/unstable/ai/Tool.test.ts +++ b/.context/effect/packages/effect/test/unstable/ai/Tool.test.ts @@ -1048,6 +1048,34 @@ const HandlerRequired = Tool.providerDefined({ }) }) +describe("setNeedsApproval", () => { + it("sets a static approval requirement without mutating the original tool", () => { + const tool = Tool.make("TestTool", { needsApproval: true }) + const updated = tool.setNeedsApproval(false) + + strictEqual(tool.needsApproval, true) + strictEqual(updated.needsApproval, false) + }) + + it("sets a dynamic approval requirement", () => { + const needsApproval = (params: { readonly dangerous: boolean }) => params.dangerous + const tool = Tool.make("TestTool", { + parameters: Schema.Struct({ dangerous: Schema.Boolean }) + }).setNeedsApproval(needsApproval) + + strictEqual(tool.needsApproval, needsApproval) + }) + + it("preserves dynamic tool identity", () => { + const tool = Tool.dynamic("TestTool", { + parameters: { type: "object", properties: {} } + }).setNeedsApproval(true) + + assertTrue(Tool.isDynamic(tool)) + strictEqual(tool.needsApproval, true) + }) +}) + describe("Dynamic", () => { describe("isDynamic", () => { it.effect("returns true for dynamic tools with Effect Schema", () => diff --git a/.context/effect/packages/effect/test/unstable/ai/ToolRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/ai/ToolRepresentation.test.ts new file mode 100644 index 000000000..2f81f7d0b --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/ai/ToolRepresentation.test.ts @@ -0,0 +1,35 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import { Tool } from "effect/unstable/ai" + +describe("Tool representation v2", () => { + it("projects the encoded side before JSON Schema generation", () => { + assert.strictEqual(Tool.getJsonSchemaFromSchema(Schema.FiniteFromString).type, "string") + }) + + it("uses custom JSON Schema compiler annotations for schema parameters", () => { + const schema = Schema.Struct({ + value: Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/ai/tool/minTwoCharacters", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + }) + const tool = Tool.make("CustomCheck", { parameters: schema }) + + assert.deepStrictEqual(Tool.getJsonSchema(tool), Tool.getJsonSchemaFromSchema(schema)) + assert.deepStrictEqual(Tool.getJsonSchema(tool), { + type: "object", + properties: { + value: { + type: "string", + allOf: [{ minLength: 2 }] + } + }, + required: ["value"], + additionalProperties: false + }) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/cli/Ansi.test.ts b/.context/effect/packages/effect/test/unstable/cli/Ansi.test.ts new file mode 100644 index 000000000..88aa46120 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/cli/Ansi.test.ts @@ -0,0 +1,10 @@ +import { assert, it } from "@effect/vitest" +import * as Ansi from "effect/unstable/cli/internal/ansi" + +it("emits a standard CSI horizontal absolute sequence", () => { + assert.strictEqual(Ansi.cursorTo(0), "\x1b[1G") +}) + +it("emits a standard CSI cursor-position sequence", () => { + assert.strictEqual(Ansi.cursorTo(2, 3), "\x1b[4;3H") +}) diff --git a/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts b/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts index 5c0f37788..02c20473f 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Arguments.test.ts @@ -15,7 +15,7 @@ const FileSystemLayer = FileSystem.layerNoop({ if (path.includes("/non/existent/file.txt")) { return Effect.fail(PlatformError.badArgument({ module: "", method: "" })) } - if (path.includes("workspace")) { + if (path.endsWith("workspace")) { return Effect.succeed({ type: "Directory" } as any) } return Effect.succeed({ type: "File" } as any) @@ -49,7 +49,7 @@ const TestLayer = Layer.mergeAll( ) describe("Command arguments", () => { - it("should parse all argument types correctly", () => + it.effect("should parse all argument types correctly", () => Effect.gen(function*() { // Create a Ref to store the result const resultRef = yield* Ref.make(null) @@ -89,7 +89,7 @@ describe("Command arguments", () => { assert.strictEqual(result.verbose, true) }).pipe(Effect.provide(TestLayer))) - it("should handle file mustExist validation", () => + it.effect("should handle file mustExist validation", () => Effect.gen(function*() { // Test 1: mustExist: true with existing file - should pass const result1Ref = yield* Ref.make(null) @@ -103,7 +103,9 @@ describe("Command arguments", () => { // Test 2: mustExist: true with non-existing file - should display error and help const runCommand = Command.runWith(existingFileCommand, { version: "1.0.0" }) - yield* runCommand(["/non/existent/file.txt"]) + yield* runCommand(["/non/existent/file.txt"]).pipe( + Effect.catchTag("ShowHelp", () => Effect.void) + ) // Check that help was shown const stdout = yield* TestConsole.logLines @@ -127,7 +129,7 @@ describe("Command arguments", () => { assert.isTrue(result3!.includes("/non/existent/file.txt")) }).pipe(Effect.provide(TestLayer))) - it("should fail with invalid arguments", () => + it.effect("should fail with invalid arguments", () => Effect.gen(function*() { const testCommand = Command.make("test", { count: Argument.integer("count"), @@ -136,19 +138,17 @@ describe("Command arguments", () => { // Test invalid integer - should display help and error const runCommand = Command.runWith(testCommand, { version: "1.0.0" }) - yield* runCommand(["not-a-number", "dev"]) + yield* runCommand(["not-a-number", "dev"]).pipe( + Effect.catchTag("ShowHelp", () => Effect.void) + ) // Check help was shown const stdout = yield* TestConsole.logLines const helpText = stdout.join("\n") - expect(helpText).toMatchInlineSnapshot(` - "USAGE - test [flags] - - ARGUMENTS - count integer - env choice " - `) + expect(helpText).toContain("USAGE") + expect(helpText).toContain("test [flags] ") + expect(helpText).toContain("count integer") + expect(helpText).toContain("env choice") // Check error was shown const stderr = yield* TestConsole.errorLines @@ -156,11 +156,11 @@ describe("Command arguments", () => { expect(errorText).toMatchInlineSnapshot(` " ERROR - Invalid value for argument : "not-a-number". Expected a string representing a finite number, got "not-a-number"" + Invalid value for argument : "not-a-number". Expected a string representing a finite number" `) }).pipe(Effect.provide(TestLayer))) - it("should handle variadic arguments", () => + it.effect("should handle variadic arguments", () => Effect.gen(function*() { let result: { readonly files: ReadonlyArray } | undefined @@ -181,7 +181,7 @@ describe("Command arguments", () => { assert.deepStrictEqual(result.files, ["file1.txt", "file2.txt", "file3.txt"]) }).pipe(Effect.provide(TestLayer))) - it("should handle choiceWithValue", () => + it.effect("should handle choiceWithValue", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) @@ -201,7 +201,7 @@ describe("Command arguments", () => { assert.strictEqual(result.level, 1) }).pipe(Effect.provide(TestLayer))) - it("should handle filter combinator - valid", () => + it.effect("should handle filter combinator - valid", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) @@ -219,7 +219,7 @@ describe("Command arguments", () => { assert.strictEqual(result.port, 8080) }).pipe(Effect.provide(TestLayer))) - it("should handle filter combinator - invalid", () => + it.effect("should handle filter combinator - invalid", () => Effect.gen(function*() { const testCommand = Command.make("test", { port: Argument.integer("port").pipe( @@ -230,12 +230,14 @@ describe("Command arguments", () => { ) }, () => Effect.void) - yield* Command.runWith(testCommand, { version: "1.0.0" })(["99999"]) + yield* Command.runWith(testCommand, { version: "1.0.0" })(["99999"]).pipe( + Effect.catchTag("ShowHelp", () => Effect.void) + ) const stderr = yield* TestConsole.errorLines assert.isTrue(stderr.some((line) => String(line).includes("out of range"))) }).pipe(Effect.provide(TestLayer))) - it("should handle filterMap combinator - valid", () => + it.effect("should handle filterMap combinator - valid", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) @@ -253,7 +255,7 @@ describe("Command arguments", () => { assert.strictEqual(result.positiveInt, 42) }).pipe(Effect.provide(TestLayer))) - it("should handle filterMap combinator - invalid", () => + it.effect("should handle filterMap combinator - invalid", () => Effect.gen(function*() { const testCommand = Command.make("test", { positiveInt: Argument.integer("num").pipe( @@ -264,12 +266,14 @@ describe("Command arguments", () => { ) }, () => Effect.void) - yield* Command.runWith(testCommand, { version: "1.0.0" })(["0"]) + yield* Command.runWith(testCommand, { version: "1.0.0" })(["0"]).pipe( + Effect.catchTag("ShowHelp", () => Effect.void) + ) const stderr = yield* TestConsole.errorLines assert.isTrue(stderr.some((line) => String(line).includes("Expected positive integer"))) }).pipe(Effect.provide(TestLayer))) - it("should handle orElse combinator", () => + it.effect("should handle orElse combinator", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) @@ -292,7 +296,7 @@ describe("Command arguments", () => { assert.strictEqual(result.value, -1) }).pipe(Effect.provide(TestLayer))) - it("should handle orElseResult combinator", () => + it.effect("should handle orElseResult combinator", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) @@ -306,17 +310,17 @@ describe("Command arguments", () => { yield* Command.runWith(testCommand, { version: "1.0.0" })(["42"]) let result = yield* Ref.get(resultRef) assert.isTrue(Result.isSuccess(result.value)) - assert.strictEqual(result.value.value, 42) + assert.strictEqual(result.value.success, 42) // Invalid integer - returns Failure with string yield* Ref.set(resultRef, null) yield* Command.runWith(testCommand, { version: "1.0.0" })(["abc"]) result = yield* Ref.get(resultRef) assert.isTrue(Result.isFailure(result.value)) - assert.strictEqual(result.value.value, "abc") + assert.strictEqual(result.value.failure, "abc") }).pipe(Effect.provide(TestLayer))) - it("should handle withMetavar combinator", () => + it.effect("should handle withMetavar combinator", () => Effect.gen(function*() { const testCommand = Command.make("test", { file: Argument.string("file").pipe( @@ -331,7 +335,7 @@ describe("Command arguments", () => { assert.isTrue(helpText.includes("FILE_PATH")) }).pipe(Effect.provide(TestLayer))) - it("should handle optional arguments - when provided", () => + it.effect("should handle optional arguments - when provided", () => Effect.gen(function*() { const resultRef = yield* Ref.make(null) diff --git a/.context/effect/packages/effect/test/unstable/cli/Command.test.ts b/.context/effect/packages/effect/test/unstable/cli/Command.test.ts index 5dac07ac1..d6d1994a5 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Command.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Command.test.ts @@ -1,7 +1,7 @@ import { assert, describe, expect, it } from "@effect/vitest" -import { Context, Effect, Fiber, FileSystem, Layer, Option, Path, Stdio } from "effect" +import { Context, Effect, Fiber, FileSystem, Layer, Option, Path, Runtime, Stdio } from "effect" import { TestConsole } from "effect/testing" -import { Argument, CliConfig, CliOutput, Command, Flag, GlobalFlag } from "effect/unstable/cli" +import { Argument, CliConfig, CliError, CliOutput, Command, Flag, GlobalFlag } from "effect/unstable/cli" import { toImpl } from "effect/unstable/cli/internal/command" import { ChildProcessSpawner } from "effect/unstable/process" import * as Cli from "./fixtures/ComprehensiveCli.ts" @@ -722,9 +722,131 @@ describe("Command", () => { const result = yield* Effect.flip(Cli.run(["test-failing", "--input", "test"])) assert.strictEqual(result, "Handler error") }).pipe(Effect.provide(TestLayer))) + + it.effect("should render and rethrow UserError handler failures without help", () => + Effect.gen(function*() { + const failure = new CliError.UserError({ + cause: new Error("internal details"), + userMessage: "Deployment failed" + }) + const command = Command.make("deploy", {}, () => failure) + + const error = yield* Effect.flip(Command.runWith(command, { version: "1.0.0" })([])) + + assert.strictEqual(error, failure) + assert.isFalse(Runtime.getErrorReported(error)) + const stderr = yield* TestConsole.errorLines + assert.lengthOf(stderr, 1) + assert.strictEqual(String(stderr[0]), "\nERROR\n Deployment failed") + assert.isEmpty(yield* TestConsole.logLines) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should render UserError handler failures with the installed formatter", () => + Effect.gen(function*() { + const formatter: CliOutput.Formatter = { + ...CliOutput.defaultFormatter({ colors: false }), + formatError: (error) => `CUSTOM ERROR: ${error.message}` + } + const failure = new CliError.UserError({ cause: "Deployment failed" }) + const command = Command.make("deploy", {}, () => failure) + + yield* Command.runWith(command, { version: "1.0.0" })([]).pipe( + Effect.flip, + Effect.provide(TestLayerWithoutFormatter), + Effect.provideService(CliOutput.Formatter, formatter) + ) + + assert.deepStrictEqual(yield* TestConsole.errorLines, ["CUSTOM ERROR: Deployment failed"]) + })) + + it.effect("should render UserError once when running wizard-generated arguments", () => + Effect.gen(function*() { + const failure = new CliError.UserError({ cause: "Deployment failed" }) + const command = Command.make("deploy", {}, () => failure) + + const fiber = yield* Command.runWith(command, { version: "1.0.0" })(["--wizard"]).pipe( + Effect.flip, + Effect.forkChild + ) + yield* MockTerminal.inputKey("enter") + const error = yield* Fiber.join(fiber) + + assert.strictEqual(error, failure) + assert.deepStrictEqual(yield* TestConsole.errorLines, ["\nERROR\n Deployment failed"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should render UserError argument failures with command help", () => + Effect.gen(function*() { + const command = Command.make("deploy", { + target: Argument.string("target").pipe( + Argument.mapEffect(() => + Effect.fail( + new CliError.UserError({ + cause: "Invalid deployment target" + }) + ) + ) + ) + }) + + const error = yield* Effect.flip(Command.runWith(command, { version: "1.0.0" })(["invalid"])) + + assert.instanceOf(error, CliError.ShowHelp) + assert.include((yield* TestConsole.errorLines).join("\n"), "Invalid deployment target") + assert.include((yield* TestConsole.logLines).join("\n"), "USAGE") + }).pipe(Effect.provide(TestLayer))) + + it.effect("should suppress automatic error rendering", () => + Effect.gen(function*() { + const userError = new CliError.UserError({ cause: "Deployment failed" }) + const command = Command.make("deploy", {}, () => userError) + const config = { version: "1.0.0", renderErrors: false } as const + + const handlerFailure = yield* Effect.flip(Command.runWith(command, config)([])) + const parseFailure = yield* Effect.flip(Command.runWith(command, config)(["--unknown"])) + + assert.strictEqual(handlerFailure, userError) + assert.isTrue(Runtime.getErrorReported(handlerFailure)) + assert.instanceOf(parseFailure, CliError.ShowHelp) + assert.isEmpty(yield* TestConsole.errorLines) + assert.include((yield* TestConsole.logLines).join("\n"), "USAGE") + }).pipe(Effect.provide(TestLayer))) + + it.effect("should still render help when automatic error rendering is disabled", () => + Effect.gen(function*() { + const child = Command.make("child") + const command = Command.make("app").pipe(Command.withSubcommands([child])) + + const error = yield* Effect.flip( + Command.runWith(command, { version: "1.0.0", renderErrors: false })([]) + ) + + assert.instanceOf(error, CliError.ShowHelp) + assert.isEmpty(error.errors) + assert.include((yield* TestConsole.logLines).join("\n"), "USAGE") + assert.isEmpty(yield* TestConsole.errorLines) + }).pipe(Effect.provide(TestLayer))) }) describe("withSubcommands", () => { + it("preserves unlisted metadata when adding subcommands", () => { + const withChild = Command.make("internal").pipe( + Command.unlisted, + Command.withSubcommands([Command.make("child")]) + ) + + assert.isTrue(withChild.unlisted) + }) + + it("preserves unlisted metadata when adding shared flags", () => { + const withShared = Command.make("internal").pipe( + Command.unlisted, + Command.withSharedFlags({ verbose: Flag.boolean("verbose") }) + ) + + assert.isTrue(withShared.unlisted) + }) + it.effect("should execute parent handler when no subcommand provided", () => Effect.gen(function*() { const command = "git" @@ -1289,6 +1411,100 @@ describe("Command", () => { assert.deepStrictEqual(captured, [["child", "--value", "x"]]) }).pipe(Effect.provide(TestLayer))) + it.effect("should pass trailing operands to the selected subcommand", () => + Effect.gen(function*() { + const captured: Array> = [] + + const child = Command.make("child", { + values: Argument.string("value").pipe(Argument.variadic()) + }, ({ values }) => Effect.sync(() => captured.push(values))) + + const cli = Command.make("tool").pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })([ + "child", + "--", + "value", + "--literal", + "-x" + ]) + + assert.deepStrictEqual(captured, [["value", "--literal", "-x"]]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should pass trailing operands through nested subcommands", () => + Effect.gen(function*() { + const captured: Array = [] + + const child = Command.make("child", { + value: Argument.string("value") + }, ({ value }) => Effect.sync(() => captured.push(value))) + const group = Command.make("group").pipe(Command.withSubcommands([child])) + const cli = Command.make("tool").pipe(Command.withSubcommands([group])) + + yield* Command.runWith(cli, { version: "1.0.0" })(["group", "child", "--", "-literal"]) + + assert.deepStrictEqual(captured, ["-literal"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should allow no trailing operands after -- for a subcommand", () => + Effect.gen(function*() { + let invoked = false + + const child = Command.make("child", {}, () => + Effect.sync(() => { + invoked = true + })) + const cli = Command.make("tool").pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })(["child", "--"]) + + assert.isTrue(invoked) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should preserve trailing operands for a leaf command", () => + Effect.gen(function*() { + const captured: Array> = [] + + const command = Command.make("tool", { + values: Argument.string("value").pipe(Argument.variadic()) + }, ({ values }) => Effect.sync(() => captured.push(values))) + + yield* Command.runWith(command, { version: "1.0.0" })(["--", "--literal", "-x"]) + + assert.deepStrictEqual(captured, [["--literal", "-x"]]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("should preserve inherited flags around a subcommand with trailing operands", () => + Effect.gen(function*() { + const captured: Array<{ before: boolean; after: boolean; value: string }> = [] + + const root = Command.make("tool").pipe( + Command.withSharedFlags({ + before: Flag.boolean("before"), + after: Flag.boolean("after") + }) + ) + const child = Command.make("child", { + value: Argument.string("value") + }, ({ value }) => + Effect.gen(function*() { + const parent = yield* root + captured.push({ before: parent.before, after: parent.after, value }) + })) + const cli = root.pipe(Command.withSubcommands([child])) + + yield* Command.runWith(cli, { version: "1.0.0" })([ + "--before", + "child", + "--after", + "--", + "-literal" + ]) + + assert.deepStrictEqual(captured, [{ before: true, after: true, value: "-literal" }]) + }).pipe(Effect.provide(TestLayer))) + it.effect("should coerce boolean flags to false when given falsey literals", () => Effect.gen(function*() { const captured: Array = [] diff --git a/.context/effect/packages/effect/test/unstable/cli/Errors.test.ts b/.context/effect/packages/effect/test/unstable/cli/Errors.test.ts index 2fcb127de..8c6cd3ea3 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Errors.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Errors.test.ts @@ -1,7 +1,7 @@ // @effect-diagnostics floatingEffect:skip-file import { assert, describe, it } from "@effect/vitest" -import { Effect, FileSystem, Layer, Path, Stdio } from "effect" -import { CliError, CliOutput, Command, Flag } from "effect/unstable/cli" +import { Effect, FileSystem, Layer, Path, Runtime, Stdio } from "effect" +import { Argument, CliError, CliOutput, Command, Flag } from "effect/unstable/cli" import { toImpl } from "effect/unstable/cli/internal/command" import * as Lexer from "effect/unstable/cli/internal/lexer" import * as Parser from "effect/unstable/cli/internal/parser" @@ -25,6 +25,15 @@ const TestLayer = Layer.mergeAll( ) describe("Command errors", () => { + it("uses the UnknownSubcommand class name as its runtime tag", () => { + const error = new CliError.UnknownSubcommand({ + subcommand: "deplyo", + suggestions: ["deploy"] + }) + + assert.strictEqual(error._tag as string, "UnknownSubcommand") + }) + describe("parse", () => { it.effect("fails with MissingOption when a required flag is absent", () => Effect.gen(function*() { @@ -114,9 +123,147 @@ describe("Command errors", () => { assert.strictEqual(error.subcommand, "deplyo") assert.isTrue(error.suggestions.includes("deploy")) }).pipe(Effect.provide(TestLayer))) + + it.effect("fails with UnexpectedArgument when a bounded variadic leaves operands", () => + Effect.gen(function*() { + const command = Command.make("test", { + values: Argument.string("value").pipe(Argument.variadic({ max: 2 })) + }) + + const parsedInput = yield* Parser.parseArgs( + Lexer.lex(["one", "two", "three"]), + command + ) + const error = yield* Effect.flip(toImpl(command).parse(parsedInput)) + + assert.instanceOf(error, CliError.UnexpectedArgument) + assert.deepStrictEqual(error.arguments, ["three"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("allows a bounded variadic to leave an operand for a following argument", () => + Effect.gen(function*() { + const command = Command.make("test", { + values: Argument.string("value").pipe(Argument.variadic({ max: 2 })), + destination: Argument.string("destination") + }) + + const parsedInput = yield* Parser.parseArgs( + Lexer.lex(["one", "two", "destination"]), + command + ) + const result = yield* toImpl(command).parse(parsedInput) + + assert.deepStrictEqual(result, { + values: ["one", "two"], + destination: "destination" + }) + }).pipe(Effect.provide(TestLayer))) + + it.effect("fails with UnexpectedArgument when a fixed argument leaves operands", () => + Effect.gen(function*() { + const command = Command.make("test", { + value: Argument.string("value") + }) + + const parsedInput = yield* Parser.parseArgs( + Lexer.lex(["one", "two"]), + command + ) + const error = yield* Effect.flip(toImpl(command).parse(parsedInput)) + + assert.instanceOf(error, CliError.UnexpectedArgument) + assert.deepStrictEqual(error.arguments, ["two"]) + }).pipe(Effect.provide(TestLayer))) }) - describe("formatErrors", () => { + describe("error formatting", () => { + it("escapes control characters in an unrecognized flag", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const error = new CliError.UnrecognizedOption({ + option: "--foo\x1b]52;c;bWFsaWNpb3Vz\x07", + suggestions: [] + }) + + assert.strictEqual( + formatter.formatCliError(error), + "Unrecognized flag: --foo\\x1b]52;c;bWFsaWNpb3Vz\\x07" + ) + }) + + it("escapes control characters in an unknown subcommand with colors enabled", () => { + const formatter = CliOutput.defaultFormatter({ colors: true }) + const error = new CliError.UnknownSubcommand({ + subcommand: "deplyo\x1b]8;;https://example.com\x07", + suggestions: [] + }) + + assert.strictEqual( + formatter.formatError(error), + `\n\x1b[1m\x1b[31mERROR\x1b[0m\n Unknown subcommand "deplyo\\x1b]8;;https://example.com\\x07"\x1b[0m` + ) + }) + + it("escapes control characters in an invalid argument value without colors", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const error = new CliError.InvalidValue({ + option: "count", + value: "12\x1b]52;c;bWFsaWNpb3Vz\x07\x7f", + expected: "an integer", + kind: "argument" + }) + + assert.strictEqual( + formatter.formatErrors([error]), + `\nERROR\n Invalid value for argument : "12\\x1b]52;c;bWFsaWNpb3Vz\\x07\\x7f". Expected: an integer` + ) + }) + + it("preserves multi-line suggestion blocks", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const errors = [ + new CliError.UnrecognizedOption({ + option: "--deplyo", + suggestions: ["--deploy"] + }), + new CliError.UnknownSubcommand({ + subcommand: "usrs", + parent: ["app"], + suggestions: ["users"] + }) + ] + + assert.strictEqual( + formatter.formatErrors(errors), + [ + "", + "ERRORS", + " Unrecognized flag: --deplyo", + "", + " Did you mean this?", + " --deploy", + " Unknown subcommand \"usrs\" for \"app\"", + "", + " Did you mean this?", + " users" + ].join("\n") + ) + }) + + it("preserves line feeds and tabs in error messages", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const error = new CliError.InvalidValue({ + option: "count", + value: "twelve", + expected: "one line\n\tcontinuation", + kind: "argument" + }) + + assert.strictEqual( + formatter.formatCliError(error), + "Invalid value for argument : \"twelve\". Expected: one line\n\tcontinuation" + ) + }) + it("formats single error with ERROR header", () => { const formatter = CliOutput.defaultFormatter({ colors: false }) const error = new CliError.MissingOption({ option: "value" }) @@ -148,6 +295,74 @@ describe("Command errors", () => { }) }) + describe("UserError", () => { + it("prefers the user-facing message over the cause", () => { + const error = new CliError.UserError({ + cause: new Error("internal details"), + userMessage: "Could not deploy the application" + }) + + assert.strictEqual(error.message, "Could not deploy the application") + }) + + it("uses an Error cause message as the fallback", () => { + const error = new CliError.UserError({ + cause: new Error("Connection refused") + }) + + assert.strictEqual(error.message, "Connection refused") + }) + + it("uses a string cause as the fallback", () => { + const error = new CliError.UserError({ cause: "Connection refused" }) + + assert.strictEqual(error.message, "Connection refused") + }) + + it("uses a generic fallback for causes without a message", () => { + const error = new CliError.UserError({ cause: { status: 503 } }) + + assert.strictEqual(error.message, "An error occurred") + }) + + it("falls back past empty user-facing and cause messages", () => { + const emptyUserMessage = new CliError.UserError({ + cause: new Error("Connection refused"), + userMessage: "" + }) + const emptyCause = new CliError.UserError({ cause: "" }) + + assert.strictEqual(emptyUserMessage.message, "Connection refused") + assert.strictEqual(emptyCause.message, "An error occurred") + }) + + it("allows runtime reporting before the CLI runner renders it", () => { + const error = new CliError.UserError({ cause: "failed" }) + + assert.isTrue(Runtime.getErrorReported(error)) + }) + + it("escapes control characters in the user-facing message", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const error = new CliError.UserError({ + cause: "internal details", + userMessage: "Deployment failed\x1b]52;c;bWFsaWNpb3Vz\x07" + }) + + assert.strictEqual( + formatter.formatError(error), + "\nERROR\n Deployment failed\\x1b]52;c;bWFsaWNpb3Vz\\x07" + ) + }) + + it("formats the resolved fallback message with other CLI errors", () => { + const formatter = CliOutput.defaultFormatter({ colors: false }) + const error = new CliError.UserError({ cause: new Error("Connection refused") }) + + assert.strictEqual(formatter.formatErrors([error]), "\nERROR\n Connection refused") + }) + }) + describe("InvalidValue", () => { it("labels a bare expected description", () => { const error = new CliError.InvalidValue({ @@ -203,4 +418,18 @@ describe("Command errors", () => { ) }) }) + + describe("UnexpectedArgument", () => { + it("formats one or more unexpected positional arguments", () => { + const single = new CliError.UnexpectedArgument({ + arguments: ["extra"] + }) + const multiple = new CliError.UnexpectedArgument({ + arguments: ["first", "second"] + }) + + assert.strictEqual(single.message, `Unexpected positional argument: "extra"`) + assert.strictEqual(multiple.message, `Unexpected positional arguments: "first", "second"`) + }) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/cli/Help.test.ts b/.context/effect/packages/effect/test/unstable/cli/Help.test.ts index 2e2d82dd5..fed72fdd8 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Help.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Help.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it } from "@effect/vitest" +import { assert, describe, expect, it } from "@effect/vitest" import { Effect, FileSystem, Layer, Path, Stdio } from "effect" import { TestConsole } from "effect/testing" -import { CliOutput, Command, Flag } from "effect/unstable/cli" +import { Argument, CliOutput, Command, Flag } from "effect/unstable/cli" +import { toImpl } from "effect/unstable/cli/internal/command" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" import * as Cli from "./fixtures/ComprehensiveCli.ts" import * as MockTerminal from "./services/MockTerminal.ts" @@ -38,6 +39,38 @@ const runCommand = Effect.fnUntraced( ) describe("Command help output", () => { + it("marks omittable flags as not required in structured help", () => { + const command = Command.make("app", { + required: Flag.string("required"), + optional: Flag.string("optional").pipe(Flag.optional), + defaulted: Flag.string("defaulted").pipe(Flag.withDefault("output.txt")) + }) + const help = toImpl(command).buildHelpDoc(["app"]) + + assert.deepStrictEqual(help.flags.map((flag) => flag.required), [true, false, false]) + }) + + it("marks omittable arguments as not required in structured help", () => { + const requiredVariadic = Command.make("app", { + files: Argument.string("files").pipe(Argument.variadic({ min: 1 })) + }) + const optionalVariadic = Command.make("app", { + files: Argument.string("files").pipe(Argument.variadic()) + }) + const defaulted = Command.make("app", { + output: Argument.string("output").pipe(Argument.withDefault("output.txt")) + }) + + assert.deepStrictEqual( + [ + toImpl(requiredVariadic).buildHelpDoc(["app"]).args![0].required, + toImpl(optionalVariadic).buildHelpDoc(["app"]).args![0].required, + toImpl(defaulted).buildHelpDoc(["app"]).args![0].required + ], + [true, false, false] + ) + }) + it.effect("renders root command help", () => Effect.gen(function*() { const helpText = yield* runCommand(["--help"]) @@ -148,14 +181,14 @@ describe("Command help output", () => { expect(errorText + helpText).not.toContain("experimental-foo") }).pipe(Effect.provide(TestLayer))) - it.effect("hides subcommands marked with withHidden from help output", () => + it.effect("hides unlisted subcommands from help output", () => Effect.gen(function*() { const visible = Command.make("visible").pipe( Command.withDescription("A visible subcommand") ) const secret = Command.make("experimental-foo").pipe( Command.withDescription("Should not appear"), - Command.withHidden + Command.unlisted ) const root = Command.make("tool").pipe( Command.withSubcommands([visible, secret]) @@ -170,11 +203,11 @@ describe("Command help output", () => { expect(helpText).not.toContain("Should not appear") }).pipe(Effect.provide(TestLayer))) - it.effect("hidden subcommand still parses on the command line", () => + it.effect("unlisted subcommand still parses on the command line", () => Effect.gen(function*() { let invoked = false const secret = Command.make("experimental-foo").pipe( - Command.withHidden, + Command.unlisted, Command.withHandler(() => Effect.sync(() => { invoked = true @@ -191,9 +224,9 @@ describe("Command help output", () => { expect(invoked).toBe(true) }).pipe(Effect.provide(TestLayer))) - it.effect("hidden subcommand name does not leak through unknown-subcommand suggestions", () => + it.effect("unlisted subcommand name does not leak through unknown-subcommand suggestions", () => Effect.gen(function*() { - const secret = Command.make("experimental-foo").pipe(Command.withHidden) + const secret = Command.make("experimental-foo").pipe(Command.unlisted) const root = Command.make("tool").pipe( Command.withSubcommands([secret]) ) @@ -208,9 +241,9 @@ describe("Command help output", () => { expect(errorText + helpText).not.toContain("experimental-foo") }).pipe(Effect.provide(TestLayer))) - it.effect("subcommand group with only hidden commands disappears entirely", () => + it.effect("subcommand group with only unlisted commands disappears entirely", () => Effect.gen(function*() { - const secret = Command.make("experimental-foo").pipe(Command.withHidden) + const secret = Command.make("experimental-foo").pipe(Command.unlisted) const root = Command.make("tool").pipe( Command.withSubcommands([secret]) ) diff --git a/.context/effect/packages/effect/test/unstable/cli/Lexer.test.ts b/.context/effect/packages/effect/test/unstable/cli/Lexer.test.ts new file mode 100644 index 000000000..7d55f2681 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/cli/Lexer.test.ts @@ -0,0 +1,15 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Lexer from "effect/unstable/cli/internal/lexer" + +describe("Lexer", () => { + it("preserves every equals sign in a long option's inline value", () => { + const result = Lexer.lex(["--query=left=right"]) + + assert.deepStrictEqual(result.tokens, [{ + _tag: "LongOption", + name: "query", + raw: "--query=left=right", + value: "left=right" + }]) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/cli/Param.test.ts b/.context/effect/packages/effect/test/unstable/cli/Param.test.ts index 9e6990074..ef3642fb2 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Param.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Param.test.ts @@ -1,7 +1,9 @@ import { assert, describe, it } from "@effect/vitest" -import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Stdio } from "effect" +import { Config, ConfigProvider, Effect, FileSystem, Layer, Option, Path, Ref, Result, Stdio } from "effect" import { TestConsole } from "effect/testing" -import { Argument, CliError, Flag, Prompt } from "effect/unstable/cli" +import { Argument, CliError, Command, Flag, Param, Primitive, Prompt } from "effect/unstable/cli" +import * as Lexer from "effect/unstable/cli/internal/lexer" +import * as Parser from "effect/unstable/cli/internal/parser" import { ChildProcessSpawner } from "effect/unstable/process" import * as MockTerminal from "./services/MockTerminal.ts" @@ -25,6 +27,50 @@ const TestLayer = Layer.mergeAll( ) describe("Param", () => { + it.effect("recognizes the alternate flag declared by orElse", () => + Effect.gen(function*() { + const command = Command.make("app", { + config: Flag.string("config").pipe( + Flag.orElse(() => Flag.string("config-url")) + ) + }) + + const parsed = yield* Parser.parseArgs(Lexer.lex(["--config-url", "https://example.com"]), command) + + assert.isUndefined(parsed.errors) + assert.deepStrictEqual(parsed.flags["config-url"], ["https://example.com"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("registers and parses the alternate flag declared by orElseResult", () => + Effect.gen(function*() { + const flag = Flag.string("config").pipe( + Flag.orElseResult(() => Flag.string("config-url")) + ) + + assert.deepStrictEqual(Param.extractSingleParams(flag).map((param) => param.name), ["config", "config-url"]) + + const [, result] = yield* flag.parse({ + flags: { "config-url": ["https://example.com"] }, + arguments: [] + }) + + assert.deepStrictEqual(result, Result.fail("https://example.com")) + }).pipe(Effect.provide(TestLayer))) + + it("preserves __proto__ as an own makeSingle option", () => { + const value = { polluted: true } + const param = Param.makeSingle({ + ["__proto__"]: value, + kind: Param.flagKind, + name: "name", + primitiveType: Primitive.string + } as any) + + assert.isTrue(Param.isParam(param)) + assert.isTrue(Object.hasOwn(param, "__proto__")) + assert.strictEqual((param as any)["__proto__"], value) + }) + describe("optional", () => { it.effect("returns none when an optional boolean flag is omitted", () => Effect.gen(function*() { diff --git a/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts b/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts index f3e3706c5..a3d2503a4 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Primitive.test.ts @@ -1,6 +1,6 @@ import { assert, describe, it } from "@effect/vitest" import { Effect, FileSystem, Layer, Path, PlatformError, Redacted, Stdio } from "effect" -import { TestConsole } from "effect/testing/index" +import { TestConsole } from "effect/testing" import { Primitive } from "effect/unstable/cli" import { ChildProcessSpawner } from "effect/unstable/process" import * as MockTerminal from "./services/MockTerminal.ts" @@ -85,7 +85,7 @@ describe("Primitive", () => { expectInvalidValues( Primitive.boolean, ["invalid"], - [`Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n", got "invalid"`] + [`Expected "true" | "yes" | "on" | "1" | "y" | "false" | "no" | "off" | "0" | "n"`] )) it("should have correct _tag", () => { @@ -107,7 +107,7 @@ describe("Primitive", () => { it.effect("should fail for invalid values", () => expectInvalidValues(Primitive.float, ["not-a-number"], [ - `Expected a string representing a finite number, got "not-a-number"` + `Expected a string representing a finite number` ])) it("should have correct _tag", () => { @@ -145,7 +145,7 @@ describe("Primitive", () => { ])) it.effect("should fail for invalid values", () => - expectInvalidValues(Primitive.date, ["not-a-date"], [`Expected a valid date, got Invalid Date`])) + expectInvalidValues(Primitive.date, ["not-a-date"], [`Expected a valid Date`])) it("should have correct _tag", () => { assert.strictEqual(Primitive.date._tag, "Date") @@ -168,7 +168,7 @@ describe("Primitive", () => { expectInvalidValues( Primitive.integer, ["3.14", "not-a-number"], - [`Expected an integer, got 3.14`, `Expected a string representing a finite number, got "not-a-number"`] + [`Expected an integer`, `Expected a string representing a finite number`] )) it("should have correct _tag", () => { diff --git a/.context/effect/packages/effect/test/unstable/cli/Prompt.test.ts b/.context/effect/packages/effect/test/unstable/cli/Prompt.test.ts index 11d7adc0a..5d1e6d82b 100644 --- a/.context/effect/packages/effect/test/unstable/cli/Prompt.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/Prompt.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "@effect/vitest" -import { Data, Effect, Fiber, FileSystem, Layer, Match, Path, Queue, Redacted } from "effect" +import { Data, DateTime, Effect, Fiber, FileSystem, Layer, Match, Path, Queue, Redacted } from "effect" import { Prompt } from "effect/unstable/cli" import * as MockTerminal from "./services/MockTerminal.ts" @@ -49,6 +49,33 @@ const toRawFrames = (lines: ReadonlyArray) => const findFrame = (frames: ReadonlyArray, text: string) => frames.find((frame) => frame.includes(text)) +describe("Prompt.date", () => { + it.effect("renders two-digit years, teen ordinals, and noon meridiem correctly", () => + Effect.gen(function*() { + const initial = DateTime.toDateUtc(DateTime.makeUnsafe({ year: 2024, month: 1, day: 11, hour: 12 })) + yield* MockTerminal.inputKey("enter") + + yield* Prompt.run(Prompt.date({ message: "When", initial, dateMask: "YY Do A" })) + const output = (yield* MockTerminal.displayLines).map(String).join("\n") + + assert.include(output, "24 11th PM") + }).pipe(Effect.provide(TestLayer))) +}) + +describe("Prompt.all", () => { + it.effect("supports an empty record", () => + Effect.gen(function*() { + const result = yield* Prompt.all({}) + assert.deepStrictEqual(result, {}) + }).pipe(Effect.provide(TestLayer))) + + it.effect("supports a non-array iterable", () => + Effect.gen(function*() { + const result = yield* Prompt.all(new Set([Prompt.succeed(1)])) + assert.deepStrictEqual(result, [1]) + }).pipe(Effect.provide(TestLayer))) +}) + describe("Prompt.integer", () => { it.effect("submits the default value", () => Effect.gen(function*() { @@ -85,6 +112,16 @@ describe("Prompt.integer", () => { }) describe("Prompt.float", () => { + it.effect("preserves a leading zero in the fractional part", () => + Effect.gen(function*() { + yield* MockTerminal.inputText("0.05") + yield* MockTerminal.inputKey("enter") + + const value = yield* Prompt.run(Prompt.float({ message: "Rate" })) + + assert.strictEqual(value, 0.05) + }).pipe(Effect.provide(TestLayer))) + it.effect("renders appended input without literal parsed", () => Effect.gen(function*() { const prompt = Prompt.float({ message: "Rate" }) @@ -483,6 +520,83 @@ describe("Prompt.file", () => { }) describe("Prompt.multiSelect", () => { + it.effect("does not allow a disabled multi-select choice to be selected", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Pick items", + choices: [{ title: "Unavailable", value: "unavailable", disabled: true }] + }) + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("enter") + + const value = yield* Prompt.run(prompt) + + assert.deepStrictEqual(value, []) + const output = yield* MockTerminal.displayLines + assert.isTrue(output.some((line) => String(line).includes("\x07"))) + }).pipe(Effect.provide(TestLayer))) + + it.effect("does not select disabled choices when selecting all", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Pick items", + choices: [ + { title: "Available", value: "available" }, + { title: "Unavailable", value: "unavailable", disabled: true } + ] + }) + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("enter") + + const value = yield* Prompt.run(prompt) + + assert.deepStrictEqual(value, ["available"]) + const output = yield* MockTerminal.displayLines + assert.isTrue(findFrame(toFrames(output), "Select None") !== undefined) + }).pipe(Effect.provide(TestLayer))) + + it.effect("does not select disabled choices when inverting the selection", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Pick items", + choices: [ + { title: "Available", value: "available" }, + { title: "Unavailable", value: "unavailable", disabled: true } + ] + }) + yield* MockTerminal.inputKey("down") + yield* MockTerminal.inputKey("space") + yield* MockTerminal.inputKey("enter") + + const value = yield* Prompt.run(prompt) + + assert.deepStrictEqual(value, ["available"]) + }).pipe(Effect.provide(TestLayer))) + + it.effect("ignores disabled preselected choices when validating and submitting", () => + Effect.gen(function*() { + const prompt = Prompt.multiSelect({ + message: "Pick items", + choices: [ + { title: "Available", value: "available", selected: true }, + { title: "Unavailable", value: "unavailable", disabled: true, selected: true } + ], + max: 1 + }) + yield* MockTerminal.inputKey("enter") + + const value = yield* Prompt.run(prompt) + + assert.deepStrictEqual(value, ["available"]) + const output = yield* MockTerminal.displayLines + const initialFrame = findFrame(toFrames(output), "Unavailable") + assert.isTrue(initialFrame?.includes("☐ Unavailable")) + const rawInitialFrame = findFrame(toRawFrames(output), "Unavailable") + assert.isTrue(rawInitialFrame?.includes(`${escape}[9m${escape}[90mUnavailable`)) + }).pipe(Effect.provide(TestLayer))) + it.effect("underlines the active label", () => Effect.gen(function*() { const prompt = Prompt.multiSelect({ diff --git a/.context/effect/packages/effect/test/unstable/cli/completions/completions.test.ts b/.context/effect/packages/effect/test/unstable/cli/completions/completions.test.ts index f5e921f01..474bd89a6 100644 --- a/.context/effect/packages/effect/test/unstable/cli/completions/completions.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/completions/completions.test.ts @@ -44,7 +44,8 @@ const withSubcommands = (() => { }).pipe(Command.withDescription("Stop the server")) return Command.make("server", { - verbose: Flag.boolean("verbose").pipe(Flag.withAlias("v")) + verbose: Flag.boolean("verbose").pipe(Flag.withAlias("v")), + config: Flag.string("config") }).pipe( Command.withDescription("Server management"), Command.withSubcommands([start, stop]) @@ -91,6 +92,52 @@ const emptyCmd = Command.make("noop").pipe( // --------------------------------------------------------------------------- describe("Bash completions", () => { + it("completes the active positional argument instead of always using the first", () => { + const descriptor: Completions.CommandDescriptor = { + name: "tool", + description: undefined, + flags: [ + { + name: "verbose", + aliases: ["v"], + description: undefined, + type: { _tag: "Boolean" } + }, + { + name: "format", + aliases: ["f"], + description: undefined, + type: { _tag: "Choice", values: ["json", "text"] } + } + ], + arguments: [ + { + name: "source", + description: undefined, + required: true, + variadic: false, + type: { _tag: "Choice", values: ["one"] } + }, + { + name: "target", + description: undefined, + required: true, + variadic: false, + type: { _tag: "Choice", values: ["two"] } + } + ], + subcommands: [] + } + const script = Bash.generate("tool", descriptor) + + assert.include(script, `for ((i = _command_index + 1; i < cword; i++)); do`) + assert.include(script, `--verbose|-v|--no-verbose) ;;`) + assert.include(script, `--format|-f) _skip_next=1 ;;`) + assert.include(script, `--format=*|-f=*) ;;`) + assert.include(script, `0)\n COMPREPLY=( $(compgen -W 'one' -- "$cur") )`) + assert.include(script, `1)\n COMPREPLY=( $(compgen -W 'two' -- "$cur") )`) + }) + it("generates completion function for root command", () => { const desc = fromCommand(simpleCmd) const script = Bash.generate("greet", desc) @@ -106,6 +153,23 @@ describe("Bash completions", () => { assert.include(script, "stop)") }) + it("does not dispatch subcommands from flag values", () => { + const desc = fromCommand(withSubcommands) + const script = Bash.generate("server", desc) + assert.include( + script, + `for ((i = _command_index + 1; i < cword; i++)); do + if (( _skip_next )); then + _skip_next=0 + continue + fi + case "\${words[i]}" in + --config) _skip_next=1 ;; + --config=*) ;; + start)` + ) + }) + it("includes long flag names with -- prefix", () => { const desc = fromCommand(simpleCmd) const script = Bash.generate("greet", desc) @@ -149,6 +213,7 @@ describe("Bash completions", () => { assert.include(script, "_server()") assert.include(script, "_server_start()") assert.include(script, "_server_stop()") + assert.include(script, `_server_start "$i"`) }) it("handles commands with no subcommands", () => { @@ -339,6 +404,38 @@ describe("Zsh completions", () => { // --------------------------------------------------------------------------- describe("Fish completions", () => { + it("scopes nested completions by the full command path", () => { + const leaf = (name: string, flag: string): Completions.CommandDescriptor => ({ + name, + description: undefined, + flags: [{ name: flag, aliases: [], description: undefined, type: { _tag: "Boolean" } }], + arguments: [], + subcommands: [] + }) + const descriptor: Completions.CommandDescriptor = { + name: "tool", + description: undefined, + flags: [], + arguments: [], + subcommands: [ + { + name: "alpha", + description: undefined, + flags: [], + arguments: [], + subcommands: [leaf("common", "alpha-only")] + }, + { name: "beta", description: undefined, flags: [], arguments: [], subcommands: [leaf("common", "beta-only")] } + ] + } + const lines = Fish.generate("tool", descriptor).split("\n") + const alphaOnly = lines.find((line) => line.includes("-l alpha-only"))! + const betaOnly = lines.find((line) => line.includes("-l beta-only"))! + + assert.include(alphaOnly, "__fish_seen_subcommand_from alpha; and __fish_seen_subcommand_from common") + assert.include(betaOnly, "__fish_seen_subcommand_from beta; and __fish_seen_subcommand_from common") + }) + it("generates complete commands for root subcommands", () => { const desc = fromCommand(withSubcommands) const script = Fish.generate("server", desc) diff --git a/.context/effect/packages/effect/test/unstable/cli/completions/descriptor.test.ts b/.context/effect/packages/effect/test/unstable/cli/completions/descriptor.test.ts index 4744f1a8e..f7b3ceeee 100644 --- a/.context/effect/packages/effect/test/unstable/cli/completions/descriptor.test.ts +++ b/.context/effect/packages/effect/test/unstable/cli/completions/descriptor.test.ts @@ -1,4 +1,5 @@ import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" import { Argument, Command, Flag } from "effect/unstable/cli" import { fromCommand } from "effect/unstable/cli/internal/completions/descriptor" @@ -68,6 +69,33 @@ describe("CommandDescriptor", () => { assert.deepStrictEqual(configFlag.type, { _tag: "Path", pathType: "either" }) }) + it("retains path semantics when a path has a custom metavar", () => { + const command = Command.make("app", { + directory: Flag.path("directory", { pathType: "directory", typeName: "DIR" }), + file: Argument.path("file", { pathType: "file" }).pipe(Argument.withMetavar("INPUT")) + }) + const descriptor = fromCommand(command) + + assert.deepStrictEqual(descriptor.flags[0].type, { _tag: "Path", pathType: "directory" }) + assert.deepStrictEqual(descriptor.arguments[0].type, { _tag: "Path", pathType: "file" }) + }) + + it("classifies file-backed flags and arguments as file paths", () => { + const command = Command.make("app", { + flagText: Flag.fileText("flag-text"), + flagParse: Flag.fileParse("flag-parse"), + flagSchema: Flag.fileSchema("flag-schema", Schema.Unknown), + argumentText: Argument.fileText("argument-text"), + argumentParse: Argument.fileParse("argument-parse"), + argumentSchema: Argument.fileSchema("argument-schema", Schema.Unknown) + }) + const descriptor = fromCommand(command) + const fileType = { _tag: "Path", pathType: "file" } as const + + assert.deepStrictEqual(descriptor.flags.map((flag) => flag.type), [fileType, fileType, fileType]) + assert.deepStrictEqual(descriptor.arguments.map((argument) => argument.type), [fileType, fileType, fileType]) + }) + it("extracts choice flags with values", () => { const cmd = Command.make("test", { color: Flag.choice("color", ["red", "green", "blue"]) diff --git a/.context/effect/packages/effect/test/unstable/devtools/DevToolsClient.test.ts b/.context/effect/packages/effect/test/unstable/devtools/DevToolsClient.test.ts new file mode 100644 index 000000000..93441b175 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/devtools/DevToolsClient.test.ts @@ -0,0 +1,47 @@ +import { assert, describe, it } from "@effect/vitest" +import { Deferred, Effect, Layer } from "effect" +import { DevToolsClient } from "effect/unstable/devtools" +import { Socket } from "effect/unstable/socket" + +describe("DevToolsClient", () => { + it.effect("sends a short-lived span once in each state", () => + Effect.gen(function*() { + const spans: Array = [] + const received = yield* Deferred.make() + const socket = Socket.make({ + runRaw: (handler) => + Effect.suspend(() => { + const result = handler("{\"_tag\":\"Pong\"}\n") + return Effect.isEffect(result) ? result : Effect.void + }).pipe(Effect.andThen(Effect.never)), + writer: Effect.succeed((chunk) => + Effect.sync(() => { + if (Socket.isCloseEvent(chunk)) return false + const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk) + for (const line of text.trim().split("\n")) { + if (line.length === 0) continue + const message = JSON.parse(line) + if (message._tag === "Span") spans.push(message) + } + return spans.length >= 2 + }).pipe( + Effect.flatMap((done) => done ? Deferred.succeed(received, void 0) : Effect.void), + Effect.asVoid + ) + ) + }) + + yield* Effect.gen(function*() { + yield* Effect.void.pipe(Effect.withSpan("child")) + yield* Deferred.await(received) + }).pipe( + Effect.provide( + DevToolsClient.layerTracer.pipe( + Layer.provide(Layer.succeed(Socket.Socket, socket)) + ) + ) + ) + + assert.deepStrictEqual(spans.map((span) => span.status._tag), ["Started", "Ended"]) + })) +}) diff --git a/.context/effect/packages/effect/test/unstable/encoding/Ini.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Ini.test.ts new file mode 100644 index 000000000..2a67c4b10 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/encoding/Ini.test.ts @@ -0,0 +1,48 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Ini from "effect/unstable/encoding/Ini" + +describe("Ini", () => { + it("parses sections, arrays, and scalar values", () => { + assert.deepStrictEqual( + Ini.parse(` +enabled=true +missing=null +name=effect +tag[]=one +tag[]=two + +[database.pool] +size=10 +`), + { + enabled: true, + missing: null, + name: "effect", + tag: ["one", "two"], + database: { + pool: { + size: "10" + } + } + } + ) + }) + + it("supports quoted values, comments, and escaped section dots", () => { + assert.deepStrictEqual( + Ini.parse(` +plain=value ; comment +quoted="value # retained" +[a\\.b] +key=yes +`), + { + plain: "value", + quoted: "value # retained", + "a.b": { + key: "yes" + } + } + ) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/encoding/Msgpack.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Msgpack.test.ts new file mode 100644 index 000000000..00d382d44 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/encoding/Msgpack.test.ts @@ -0,0 +1,16 @@ +import { assert, it } from "@effect/vitest" +import { Effect, Stream } from "effect" +import * as Msgpack from "effect/unstable/encoding/Msgpack" +import { encode } from "msgpackr" + +it.effect("fails when the stream ends with an incomplete MessagePack frame", () => + Effect.gen(function*() { + const frame = Uint8Array.from(encode("hello")) + const error = yield* Stream.make(frame.subarray(0, frame.length - 1)).pipe( + Stream.pipeThroughChannel(Msgpack.decode()), + Stream.runCollect, + Effect.flip + ) + + assert.instanceOf(error, Msgpack.MsgPackError) + })) diff --git a/.context/effect/packages/effect/test/unstable/encoding/Ndjson.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Ndjson.test.ts index 4e1f06d55..fbc0b10ce 100644 --- a/.context/effect/packages/effect/test/unstable/encoding/Ndjson.test.ts +++ b/.context/effect/packages/effect/test/unstable/encoding/Ndjson.test.ts @@ -4,6 +4,22 @@ import * as Schema from "effect/Schema" import * as Ndjson from "effect/unstable/encoding/Ndjson" describe("Ndjson", () => { + it.effect("fails for values without a JSON representation", () => + Effect.gen(function*() { + const inputs = [undefined, () => {}, Symbol("x")] + + for (const input of inputs) { + const error = yield* Stream.make(input).pipe( + Stream.pipeThroughChannel(Ndjson.encodeString()), + Stream.runCollect, + Effect.flip + ) + + assert.instanceOf(error, Ndjson.NdjsonError) + assert.strictEqual(error.kind, "Pack") + } + })) + it.effect("decodeSchema decodes records split across Uint8Array chunks", () => Effect.gen(function*() { const messages = yield* Stream.make( diff --git a/.context/effect/packages/effect/test/unstable/encoding/Sse.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Sse.test.ts index 4f07895b9..f3de53102 100644 --- a/.context/effect/packages/effect/test/unstable/encoding/Sse.test.ts +++ b/.context/effect/packages/effect/test/unstable/encoding/Sse.test.ts @@ -4,6 +4,86 @@ import * as Schema from "effect/Schema" import * as Sse from "effect/unstable/encoding/Sse" describe("Sse", () => { + it("treats CRLF split across chunks as one line ending", () => { + const events: Array = [] + const parser = Sse.makeParser((event) => events.push(event)) + + parser.feed("data: first\r") + parser.feed("\ndata: second\n\n") + + assert.deepStrictEqual(events, [{ + _tag: "Event", + event: "message", + id: undefined, + data: "first\nsecond" + }]) + }) + + it("retains the last event ID for later events", () => { + const events: Array = [] + const parser = Sse.makeParser((event) => events.push(event)) + + parser.feed("id: 1\ndata: first\n\ndata: second\n\n") + + assert.deepStrictEqual(events, [ + { + _tag: "Event", + id: "1", + event: "message", + data: "first" + }, + { + _tag: "Event", + id: "1", + event: "message", + data: "second" + } + ]) + }) + + it("roundtrips an SSE event with empty data", () => { + const input: Sse.Event = { _tag: "Event", event: "message", id: undefined, data: "" } + const events: Array = [] + const parser = Sse.makeParser((event) => events.push(event)) + + parser.feed(Sse.encoder.write(input)) + + assert.deepStrictEqual(events, [input]) + }) + + it("strips a UTF-8 BOM from the start of an SSE stream", () => { + const events: Array = [] + const parser = Sse.makeParser((event) => events.push(event)) + + parser.feed("\uFEFFdata: ok\n\n") + + assert.deepStrictEqual(events, [{ + _tag: "Event", + event: "message", + id: undefined, + data: "ok" + }]) + }) + + it("uses message for an empty SSE event type", () => { + const events: Array = [] + const parser = Sse.makeParser((event) => events.push(event)) + + parser.feed("event:\ndata: ok\n\n") + + assert.strictEqual(events.length, 1) + assert.strictEqual((events[0] as Sse.Event).event, "message") + }) + + it("ignores retry values that are not ASCII digits", () => { + const events: Array = [] + for (const value of ["123x", "+123", "-123", "1.5"]) { + Sse.makeParser((event) => events.push(event)).feed(`retry: ${value}\n`) + } + + assert.deepStrictEqual(events, []) + }) + it("Event preserves string payloads", () => { const decode = Schema.decodeUnknownSync(Sse.Event) const encode = Schema.encodeSync(Sse.Event) @@ -59,4 +139,65 @@ describe("Sse", () => { } }]) })) + + it.effect("fails when an unterminated line exceeds maxEventSize", () => + Effect.gen(function*() { + const error = yield* Stream.make("12345").pipe( + Stream.pipeThroughChannel(Sse.decode({ maxEventSize: 4 })), + Stream.runCollect, + Effect.flip + ) + + assert.instanceOf(error, Sse.SseError) + assert.instanceOf(error.reason, Sse.EventTooLarge) + assert.strictEqual(error.reason.maxEventSize, 4) + })) + + it.effect("fails when pending data exceeds maxEventSize", () => + Effect.gen(function*() { + const error = yield* Stream.make("data: a\n", "data: b\n").pipe( + Stream.pipeThroughChannel(Sse.decode({ maxEventSize: 3 })), + Stream.runCollect, + Effect.flip + ) + + assert.instanceOf(error, Sse.SseError) + assert.instanceOf(error.reason, Sse.EventTooLarge) + assert.strictEqual(error.reason.maxEventSize, 3) + })) + + it.effect("parses pending state just under maxEventSize", () => + Effect.gen(function*() { + const events = yield* Stream.make("data: a\ndata: b", "\n\n").pipe( + Stream.pipeThroughChannel(Sse.decode({ maxEventSize: 10 })), + Stream.runCollect + ) + + assert.deepStrictEqual([...events], [{ + _tag: "Event", + event: "message", + id: undefined, + data: "a\nb" + }]) + })) + + it.effect("parses well-formed events split across chunks", () => + Effect.gen(function*() { + const events = yield* Stream.make( + "id: 1\nevent: up", + "date\ndata: hel", + "lo\n", + "\n" + ).pipe( + Stream.pipeThroughChannel(Sse.decode()), + Stream.runCollect + ) + + assert.deepStrictEqual([...events], [{ + _tag: "Event", + event: "update", + id: "1", + data: "hello" + }]) + })) }) diff --git a/.context/effect/packages/effect/test/unstable/encoding/Toml.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Toml.test.ts new file mode 100644 index 000000000..dde14d5f9 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/encoding/Toml.test.ts @@ -0,0 +1,71 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Toml from "effect/unstable/encoding/Toml" + +describe("Toml", () => { + it("parses tables, dotted keys, arrays, and inline tables", () => { + assert.deepStrictEqual( + Toml.parse(` +title = "Effect" +ports = [8000, 8001] +database.connection.timeout = 30 + +[database] +enabled = true +credentials = { user = "root", roles = ["admin", "writer"] } +`), + { + title: "Effect", + ports: [8000, 8001], + database: { + connection: { timeout: 30 }, + enabled: true, + credentials: { user: "root", roles: ["admin", "writer"] } + } + } + ) + }) + + it("parses arrays of tables and date-time values", () => { + assert.deepStrictEqual( + Toml.parse(` +[[servers]] +name = "alpha" +started = 2026-08-05T01:02:03Z + +[[servers]] +name = "beta" +started = 2026-08-05 +`), + { + servers: [ + { name: "alpha", started: new Date("2026-08-05T01:02:03Z") }, + { name: "beta", started: "2026-08-05" } + ] + } + ) + }) + + it("parses multiline strings and numeric formats", () => { + assert.deepStrictEqual( + Toml.parse(` +message = """ +hello \\ + world""" +hex = 0xDEAD_BEEF +fraction = 1_000.5 +local = 2026-08-05 01:02:03 +`), + { + message: "hello world", + hex: 0xdeadbeef, + fraction: 1000.5, + local: "2026-08-05T01:02:03" + } + ) + }) + + it("rejects duplicate keys", () => { + assert.throws(() => Toml.parse("key = 1\nkey = 2\n")) + assert.throws(() => Toml.parse("key = 1__000\n")) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/encoding/Yaml.test.ts b/.context/effect/packages/effect/test/unstable/encoding/Yaml.test.ts new file mode 100644 index 000000000..4ecf73a65 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/encoding/Yaml.test.ts @@ -0,0 +1,72 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Yaml from "effect/unstable/encoding/Yaml" + +describe("Yaml", () => { + it("parses nested block and flow collections", () => { + assert.deepStrictEqual( + Yaml.parse(` +name: effect +enabled: true +ports: [3000, 3001] +database: + host: localhost + credentials: + - user: root + roles: [admin, writer] + - user: guest + roles: [] +`), + { + name: "effect", + enabled: true, + ports: [3000, 3001], + database: { + host: "localhost", + credentials: [ + { user: "root", roles: ["admin", "writer"] }, + { user: "guest", roles: [] } + ] + } + } + ) + }) + + it("parses quoted and block scalars", () => { + assert.deepStrictEqual( + Yaml.parse(` +quoted: "line\\nvalue" +literal: | + first + second +folded: >- + first + second +`), + { + quoted: "line\nvalue", + literal: "first\nsecond\n", + folded: "first second" + } + ) + }) + + it("resolves aliases", () => { + assert.deepStrictEqual( + Yaml.parse(` +defaults: &defaults + host: localhost + port: 5432 +development: + settings: *defaults +`), + { + defaults: { host: "localhost", port: 5432 }, + development: { settings: { host: "localhost", port: 5432 } } + } + ) + }) + + it("rejects invalid indentation", () => { + assert.throws(() => Yaml.parse("root:\n child: true\n sibling: false\n")) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/eventlog/EventJournal.test.ts b/.context/effect/packages/effect/test/unstable/eventlog/EventJournal.test.ts index 411eca348..29729d794 100644 --- a/.context/effect/packages/effect/test/unstable/eventlog/EventJournal.test.ts +++ b/.context/effect/packages/effect/test/unstable/eventlog/EventJournal.test.ts @@ -2,7 +2,82 @@ import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" import * as EventJournal from "effect/unstable/eventlog/EventJournal" +const entry = (msecs: number) => + new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe({ msecs }), + event: "Repro", + primaryKey: "key", + payload: new Uint8Array() + }, { disableChecks: true }) + describe("EventJournal", () => { + it.effect("relays an imported entry to another remote", () => + Effect.gen(function*() { + const journal = yield* EventJournal.makeMemory + const source = EventJournal.makeRemoteIdUnsafe() + const target = EventJournal.makeRemoteIdUnsafe() + yield* journal.nextRemoteSequence(target) + const entry = new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe(), + event: "Repro", + primaryKey: "key", + payload: new Uint8Array() + }, { disableChecks: true }) + yield* journal.writeFromRemote({ + remoteId: source, + entries: [new EventJournal.RemoteEntry({ remoteSequence: 0, entry })], + effect: () => Effect.void + }) + const missing = yield* journal.withRemoteUncommited(target, Effect.succeed) + assert.deepStrictEqual(missing.map((item) => item.idString), [entry.idString]) + const sourceMissing = yield* journal.withRemoteUncommited(source, Effect.succeed) + assert.deepStrictEqual(sourceMissing, []) + })) + + it.effect("returns the next unused remote sequence", () => + Effect.gen(function*() { + const journal = yield* EventJournal.makeMemory + const remoteId = EventJournal.makeRemoteIdUnsafe() + const entry = new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe(), + event: "Repro", + primaryKey: "key", + payload: new Uint8Array() + }, { disableChecks: true }) + + assert.strictEqual(yield* journal.nextRemoteSequence(remoteId), 0) + yield* journal.writeFromRemote({ + remoteId, + entries: [new EventJournal.RemoteEntry({ remoteSequence: 0, entry })], + effect: () => Effect.void + }) + assert.strictEqual(yield* journal.nextRemoteSequence(remoteId), 1) + yield* journal.writeFromRemote({ + remoteId, + entries: [new EventJournal.RemoteEntry({ remoteSequence: 5, entry })], + effect: () => Effect.void + }) + assert.strictEqual(yield* journal.nextRemoteSequence(remoteId), 6) + })) + + it.effect("reports the first newer conflicting entry", () => + Effect.gen(function*() { + const journal = yield* EventJournal.makeMemory + const newer = entry(2_000) + yield* journal.writeFromRemote({ + remoteId: EventJournal.makeRemoteIdUnsafe(), + entries: [new EventJournal.RemoteEntry({ remoteSequence: 0, entry: newer })], + effect: () => Effect.void + }) + let conflicts: ReadonlyArray = [] + yield* journal.writeFromRemote({ + remoteId: EventJournal.makeRemoteIdUnsafe(), + entries: [new EventJournal.RemoteEntry({ remoteSequence: 0, entry: entry(1_000) })], + effect: (options) => Effect.sync(() => conflicts = options.conflicts) + }) + assert.deepStrictEqual(conflicts.map((item) => item.idString), [newer.idString]) + })) + it.effect("records entries in memory and publishes local changes", () => Effect.gen(function*() { const journal = yield* EventJournal.EventJournal diff --git a/.context/effect/packages/effect/test/unstable/eventlog/EventLog.test.ts b/.context/effect/packages/effect/test/unstable/eventlog/EventLog.test.ts index 3a3e6f362..c8fd59fb9 100644 --- a/.context/effect/packages/effect/test/unstable/eventlog/EventLog.test.ts +++ b/.context/effect/packages/effect/test/unstable/eventlog/EventLog.test.ts @@ -55,25 +55,28 @@ describe("EventLog", () => { }).pipe(Effect.provide(logLayer(handled))) })) - it.effect("encrypts and decrypts entries", () => + it.effect("encrypts and decrypts entries with a distinct IV per entry", () => Effect.gen(function*() { const encryption = yield* EventLogEncryption.EventLogEncryption const identity = yield* encryption.generateIdentity - const entry = new EventJournal.Entry({ - id: EventJournal.makeEntryIdUnsafe(), - event: "UserCreated", - primaryKey: "user-1", - payload: new Uint8Array([1, 2, 3]) - }, { disableChecks: true }) - const encrypted = yield* encryption.encrypt(identity, [entry]) - const decrypted = yield* encryption.decrypt(identity, [{ - sequence: 0, - iv: encrypted.iv, - entryId: entry.id, - encryptedEntry: encrypted.encryptedEntries[0] - }]) - assert.strictEqual(decrypted.length, 1) - assert.strictEqual(decrypted[0].entry.idString, entry.idString) + const entries = ["user-1", "user-2"].map((primaryKey, index) => + new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe(), + event: "UserCreated", + primaryKey, + payload: new Uint8Array([index]) + }, { disableChecks: true }) + ) + const encrypted = yield* encryption.encrypt(identity, entries) + assert.notDeepEqual(encrypted[0].iv, encrypted[1].iv) + const decrypted = yield* encryption.decrypt( + identity, + encrypted.map((entry, index) => ({ ...entry, sequence: index, entryId: entries[index].id })) + ) + assert.deepStrictEqual( + decrypted.map((remote) => remote.entry.idString), + entries.map((entry) => entry.idString) + ) }).pipe(Effect.provide(EventLogEncryption.layerSubtle))) it.effect("publishes local journal changes through a scoped subscription", () => diff --git a/.context/effect/packages/effect/test/unstable/eventlog/EventLogMessage.test.ts b/.context/effect/packages/effect/test/unstable/eventlog/EventLogMessage.test.ts new file mode 100644 index 000000000..7f5ee4f45 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/eventlog/EventLogMessage.test.ts @@ -0,0 +1,27 @@ +import { assert, describe, it } from "@effect/vitest" +import { ChunkedMessage } from "effect/unstable/eventlog/EventLogMessage" + +describe("EventLogMessage", () => { + it("ignores duplicate chunks when joining", () => { + const state = ChunkedMessage.initialJoinState() + const chunk = new ChunkedMessage({ + id: 1, + part: [0, 2], + data: new Uint8Array([1]) + }) + + assert.isUndefined(ChunkedMessage.join(state, chunk)) + assert.isUndefined(ChunkedMessage.join(state, chunk)) + assert.deepStrictEqual( + ChunkedMessage.join( + state, + new ChunkedMessage({ + id: 1, + part: [1, 2], + data: new Uint8Array([2]) + }) + ), + new Uint8Array([1, 2]) + ) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts b/.context/effect/packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts new file mode 100644 index 000000000..188d636cb --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/eventlog/EventLogServerUnencrypted.test.ts @@ -0,0 +1,117 @@ +import { assert, it } from "@effect/vitest" +import { Context, Effect, Layer, Redacted, Ref, Schema } from "effect" +import * as EventGroup from "effect/unstable/eventlog/EventGroup" +import * as EventJournal from "effect/unstable/eventlog/EventJournal" +import * as EventLog from "effect/unstable/eventlog/EventLog" +import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption" +import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage" +import * as EventLogServerUnencrypted from "effect/unstable/eventlog/EventLogServerUnencrypted" +import * as EventLogSessionAuth from "effect/unstable/eventlog/EventLogSessionAuth" +import { makeGetIdentityRootSecretMaterial } from "effect/unstable/eventlog/internal/identityRootSecretDerivation" +import * as RpcTest from "effect/unstable/rpc/RpcTest" + +const ReproGroup = EventGroup.empty.add({ + tag: "ReproEvent", + primaryKey: (payload) => payload.key, + payload: Schema.Struct({ key: Schema.String, value: Schema.Number }) +}) +const event = ReproGroup.events.ReproEvent +const storeId = EventLogMessage.StoreId.make("repro-store") +const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto) + +const authenticate = Effect.fnUntraced(function*(options: { + readonly identity: EventLog.Identity["Service"] + readonly challenge: Uint8Array + readonly remoteId: EventJournal.RemoteId +}) { + const material = yield* getIdentityRootSecretMaterial(options.identity) + const signature = yield* EventLogSessionAuth.signSessionAuthPayload({ + remoteId: options.remoteId, + challenge: options.challenge, + publicKey: options.identity.publicKey, + signingPublicKey: material.signingPublicKey, + signingPrivateKey: Redacted.value(material.signingPrivateKey) + }) + return new EventLogMessage.Authenticate({ + publicKey: options.identity.publicKey, + signingPublicKey: material.signingPublicKey, + signature, + algorithm: "Ed25519" + }) +}) + +it.effect("indexes conflicts from the sliced history", () => + Effect.gen(function*() { + const encode = Schema.encodeUnknownEffect(event.payloadMsgPack) + const makeEntry = Effect.fnUntraced(function*(msecs: number, key: string, value: number) { + return new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe({ msecs }), + event: "ReproEvent", + primaryKey: key, + payload: yield* encode({ key, value }) + }, { disableChecks: true }) + }) + const originA = yield* makeEntry(1_000, "other-origin", 10) + const oldSameKey = yield* makeEntry(2_000, "key", 20) + const originB = yield* makeEntry(3_000, "key", 30) + const newerOtherKey = yield* makeEntry(4_000, "other", 40) + const newerSameKey = yield* makeEntry(5_000, "key", 50) + + const storage = yield* EventLogServerUnencrypted.makeStorageMemory + yield* storage.write(storeId, [oldSameKey, newerOtherKey, newerSameKey]) + const registry = yield* EventLog.Registry.pipe(Effect.provide(EventLog.layerRegistry)) + const seenOriginA = yield* Ref.make | undefined>(undefined) + const seenOriginB = yield* Ref.make | undefined>(undefined) + registry.registerHandlerUnsafe({ + event: event.tag, + handler: { + event, + context: Context.empty() as Context.Context, + handler: ({ payload, conflicts }) => { + const value = (payload as { value: number }).value + return value === 10 + ? Ref.set(seenOriginA, conflicts.map((conflict) => conflict.entry)) + : value === 30 + ? Ref.set(seenOriginB, conflicts.map((conflict) => conflict.entry)) + : Effect.void + } + } + }) + + const client = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe( + Effect.provide(EventLogServerUnencrypted.layerRpcHandlers.pipe( + Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)), + Layer.provide(Layer.succeed(EventLog.Registry, registry)), + Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, { + resolve: ({ storeId }) => Effect.succeed(storeId), + hasStore: () => Effect.succeed(true) + })), + Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, { + authorizeWrite: () => Effect.void, + authorizeRead: () => Effect.void, + authorizeIdentity: () => Effect.void + })) + )) + ) + const identity = yield* EventLog.makeIdentity + const hello = yield* client["EventLog.Hello"]() + yield* client["EventLog.Authenticate"]( + yield* authenticate({ + identity, + challenge: hello.challenge, + remoteId: hello.remoteId + }) + ) + const data = yield* new EventLogMessage.WriteEntriesUnencrypted({ + publicKey: identity.publicKey, + storeId, + entries: [originA, originB] + }).encoded + yield* client["EventLog.WriteSingle"]({ data }) + const originAConflicts = yield* Ref.get(seenOriginA) + assert.isDefined(originAConflicts) + assert.deepStrictEqual(originAConflicts.map((entry) => entry.idString), []) + const originBConflicts = yield* Ref.get(seenOriginB) + assert.isDefined(originBConflicts) + assert.deepStrictEqual(originBConflicts.map((entry) => entry.idString), [newerSameKey.idString]) + }).pipe(Effect.provide(EventLogEncryption.layerSubtle))) diff --git a/.context/effect/packages/effect/test/unstable/http/Cookies.test.ts b/.context/effect/packages/effect/test/unstable/http/Cookies.test.ts index 0095ccda7..96a82fd1a 100644 --- a/.context/effect/packages/effect/test/unstable/http/Cookies.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/Cookies.test.ts @@ -1,12 +1,66 @@ import { assert, describe, it } from "@effect/vitest" import { assertNone, assertSome, deepStrictEqual } from "@effect/vitest/utils" -import { Schema } from "effect" +import { Result, Schema } from "effect" import * as Option from "effect/Option" import { TestSchema } from "effect/testing" import { Cookies } from "effect/unstable/http" -import { assertSuccess } from "../../utils/assert.ts" +import { assertFailure, assertSuccess } from "../../utils/assert.ts" describe("Cookies", () => { + describe("makeCookie", () => { + it("rejects cookie attribute delimiters in names, domains, and paths", () => { + assertFailure( + Cookies.makeCookie("a; Domain=evil.com; b", "token"), + Cookies.CookiesError.fromReason("InvalidCookieName") + ) + assertFailure( + Cookies.makeCookie("session", "token", { domain: "legit.com; Domain=.parent.tld" }), + Cookies.CookiesError.fromReason("InvalidCookieDomain") + ) + assertFailure( + Cookies.makeCookie("session", "token", { path: "/; HttpOnly" }), + Cookies.CookiesError.fromReason("InvalidCookiePath") + ) + }) + + it("accepts RFC 6265 token names and legitimate domains and paths", () => { + for (const domain of ["sub.example.com", ".sub.example.com"]) { + const cookie = Result.getOrThrow( + Cookies.makeCookie("!#$%&'*+-.^_`|~", "token", { + domain, + path: "/some-path_with~chars/%20" + }) + ) + + assert.strictEqual( + Cookies.serializeCookie(cookie), + `!#$%&'*+-.^_\`|~=token; Domain=${domain}; Path=/some-path_with~chars/%20` + ) + } + }) + }) + + describe("toSetCookieHeaders", () => { + const invalidCookie = { + name: "session", + value: "token", + valueEncoded: "token", + options: { domain: "legit.com; Domain=.evil.com" } + } as unknown as Cookies.Cookie + + it("rejects invalid cookies supplied through fromIterable", () => { + const cookies = Cookies.fromIterable([invalidCookie]) + + assert.throws(() => Cookies.toSetCookieHeaders(cookies), /InvalidCookieDomain/) + }) + + it("rejects invalid cookies supplied through setCookie", () => { + const cookies = Cookies.setCookie(Cookies.empty, invalidCookie) + + assert.throws(() => Cookies.toSetCookieHeaders(cookies), /InvalidCookieDomain/) + }) + }) + it("expireCookie returns a Result with an expired Set-Cookie value", () => { assertSuccess( Cookies.expireCookie(Cookies.empty, "session", { path: "/", secure: true }), @@ -76,5 +130,6 @@ describe("Cookies", () => { assertSome(Cookies.getValue(cookies, "session"), "abc") assertNone(Cookies.get(cookies, "missing")) assertNone(Cookies.getValue(cookies, "missing")) + assertNone(Cookies.get(Cookies.empty, "constructor")) }) }) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/case-insensitive.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/case-insensitive.test.ts new file mode 100644 index 000000000..18e6c06c0 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/case-insensitive.test.ts @@ -0,0 +1,153 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +const make = () => + Router.make({ + caseSensitive: false + }) + +it("case insensitive static routes of level 1", () => { + const router = make() + router.on("GET", "/woo", true) + assert(router.find("GET", "/woo")?.handler) +}) + +it("case insensitive static routes of level 2", () => { + const router = make() + router.on("GET", "/foo/woo", true) + assert(router.find("GET", "/FoO/WOO")?.handler) +}) + +it("case insensitive static routes of level 3", () => { + const router = make() + router.on("GET", "/foo/bar/woo", true) + assert(router.find("GET", "/Foo/bAR/WoO")?.handler) +}) + +it("parametric case insensitive", () => { + const router = make() + router.on("GET", "/foo/:param", true) + const result = router.find("GET", "/Foo/bAR") + assert(result) + assert(result.handler) + assert(result.params.param === "bAR") +}) + +it("parametric case insensitive with a static part", () => { + const router = make() + router.on("GET", "/foo/my-:param", true) + const result = router.find("GET", "/Foo/MY-bAR") + assert(result) + assert(result.handler) + assert(result.params.param === "bAR") +}) + +it("parametric case insensitive with capital letter", () => { + const router = make() + router.on("GET", "/foo/:Param", true) + const result = router.find("GET", "/Foo/bAR") + assert(result) + assert(result.handler) + assert(result.params.Param === "bAR") +}) + +it("case insensitive with capital letter in static path with param", () => { + const router = make() + router.on("GET", "/Foo/bar/:param", true) + const result = router.find("GET", "/foo/bar/baZ") + assert(result) + assert(result.handler) + assert(result.params.param === "baZ") +}) + +it("case insensitive with multiple paths containing capital letter in static path with param", () => { + const router = make() + router.on("GET", "/Foo/bar/:param", true) + router.on("GET", "/Foo/baz/:param", true) + + let result = router.find("GET", "/foo/bar/baZ") + assert(result) + assert(result.handler) + assert(result.params.param === "baZ") + + result = router.find("GET", "/foo/bar/baR") + assert(result) + assert(result.handler) + assert(result.params.param === "baR") +}) + +it("case insensitive with multiple mixed-case params within same slash couple", () => { + const router = make() + router.on("GET", "/foo/:param1-:param2", true) + + const result = router.find("GET", "/FOO/My-bAR") + assert(result) + assert(result.handler) + assert(result.params.param1 === "My") + assert(result.params.param2 === "bAR") +}) + +it("case insensitive with multiple mixed-case params", () => { + const router = make() + router.on("GET", "/foo/:param1/:param2", true) + + const result = router.find("GET", "/FOO/My/bAR") + assert(result) + assert(result.handler) + assert(result.params.param1 === "My") + assert(result.params.param2 === "bAR") +}) + +it("case insensitive with wildcard", () => { + const router = make() + router.on("GET", "/foo/*", true) + + const result = router.find("GET", "/FOO/bAR") + assert(result) + assert(result.handler) + assert(result.params["*"] === "bAR") +}) + +it("parametric case insensitive with multiple routes", () => { + const router = make() + const tests = [ + [ + "POST", + "/foo/:param/Static/:userId/Save", + "/foo/bAR/static/one/SAVE", + { + param: "bAR", + userId: "one" + } + ], + [ + "POST", + "/foo/:param/Static/:userId/Update", + "/fOO/Bar/Static/two/update", + { + param: "Bar", + userId: "two" + } + ], + [ + "POST", + "/foo/:param/Static/:userId/CANCEL", + "/Foo/bAR/STATIC/THREE/cAnCeL", + { + param: "bAR", + userId: "THREE" + } + ] + ] as const + + tests.forEach(([method, path]) => { + router.on(method, path, true) + }) + + tests.forEach(([method, , url, params]) => { + const result = router.find(method, url) + assert(result) + assert(result.handler) + assert.deepStrictEqual(result.params, params) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/matching-order.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/matching-order.test.ts new file mode 100644 index 000000000..02a6de927 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/matching-order.test.ts @@ -0,0 +1,16 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("Matching order", () => { + const router = Router.make() + + router.on("GET", "/foo/bar/*", true) + router.on("GET", "/foo/:param/static", true) + + assert.deepStrictEqual(router.find("GET", "/foo/bar/static")?.params, { + "*": "static" + }) + assert.deepStrictEqual(router.find("GET", "/foo/value/static")?.params, { + param: "value" + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/methods.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/methods.test.ts new file mode 100644 index 000000000..70e11ccc1 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/methods.test.ts @@ -0,0 +1,16 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("returns undefined for method names inherited from Object.prototype", () => { + const router = Router.make() + router.on("GET", "/", true) + + assert.isUndefined(router.find("constructor", "/")) +}) + +it("registers QUERY with all", () => { + const router = Router.make() + router.all("/all", true) + + assert.strictEqual(router.find("QUERY", "/all")?.handler, true) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/optional-params.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/optional-params.test.ts new file mode 100644 index 000000000..7fff29a27 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/optional-params.test.ts @@ -0,0 +1,119 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("Test route with optional parameter", () => { + const router = Router.make() + + router.on("GET", "/a/:param/b/:optional?", true) + + assert.deepStrictEqual(router.find("GET", "/a/foo-bar/b")?.params, { + param: "foo-bar" + }) + assert.deepStrictEqual(router.find("GET", "/a/foo-bar/b/foo")?.params, { + param: "foo-bar", + optional: "foo" + }) +}) + +it("Test for duplicate route with optional param", () => { + const router = Router.make() + router.on("GET", "/foo/:bar?", true) + assert.throws(() => router.on("GET", "/foo", true)) +}) + +it("Test for param with ? not at the end", () => { + const router = Router.make() + + assert.throws(() => router.on("GET", "/foo/:bar?/baz", true)) +}) + +it("Multi parametric route with optional param", () => { + const router = Router.make() + + router.on("GET", "/a/:p1-:p2?", true) + + assert.deepStrictEqual(router.find("GET", "/a/foo-bar-baz")?.params, { + p1: "foo-bar", + p2: "baz" + }) + assert.deepStrictEqual(router.find("GET", "/a")?.params, {}) +}) + +it("Optional parameter at root", () => { + const router = Router.make() + + router.on("GET", "/:optional?", true) + + assert.deepStrictEqual(router.find("GET", "/")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/foo")?.params, { + optional: "foo" + }) +}) + +it("Optional Parameter with ignoreTrailingSlash = true", () => { + const router = Router.make({ + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: false + }) + + router.on("GET", "/test/hello/:optional?", true) + + assert.deepStrictEqual(router.find("GET", "/test/hello/")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello/foo")?.params, { + optional: "foo" + }) + assert.deepStrictEqual(router.find("GET", "/test/hello/foo/")?.params, { + optional: "foo" + }) +}) + +it("Optional Parameter with ignoreTrailingSlash = false", () => { + const router = Router.make({ + ignoreTrailingSlash: false, + ignoreDuplicateSlashes: false + }) + + router.on("GET", "/test/hello/:optional?", true) + + assert.deepStrictEqual(router.find("GET", "/test/hello/")?.params, { + optional: "" + }) + assert.deepStrictEqual(router.find("GET", "/test/hello")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello/foo")?.params, { + optional: "foo" + }) + assert.isUndefined(router.find("GET", "/test/hello/foo/")) +}) + +it("Optional Parameter with ignoreDuplicateSlashes = true", () => { + const router = Router.make({ + ignoreDuplicateSlashes: true + }) + + router.on("GET", "/test/hello/:optional?", true) + + assert.deepStrictEqual(router.find("GET", "/test//hello")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello/foo")?.params, { + optional: "foo" + }) + assert.deepStrictEqual(router.find("GET", "/test//hello//foo")?.params, { + optional: "foo" + }) +}) + +it("Optional Parameter with ignoreDuplicateSlashes = false", () => { + const router = Router.make({ + ignoreDuplicateSlashes: false + }) + + router.on("GET", "/test/hello/:optional?", true) + + assert.isUndefined(router.find("GET", "/test//hello")) + assert.deepStrictEqual(router.find("GET", "/test/hello")?.params, {}) + assert.deepStrictEqual(router.find("GET", "/test/hello/foo")?.params, { + optional: "foo" + }) + assert.isUndefined(router.find("GET", "/test//hello//foo")) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/params-collisions.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/params-collisions.test.ts new file mode 100644 index 000000000..0a4826a5b --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/params-collisions.test.ts @@ -0,0 +1,101 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("should setup parametric and regexp node", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar", 1) + router.on("GET", "/foo/:bar(123)", 2) + + assert.strictEqual(router.find("GET", "/foo/value")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/123")?.handler, 2) +}) + +it("should setup parametric and multi-parametric node", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar", 1) + router.on("GET", "/foo/:bar.png", 2) + + assert.strictEqual(router.find("GET", "/foo/value")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/value.png")?.handler, 2) +}) + +it("should throw when set upping two parametric nodes", () => { + const router = Router.make() + router.on("GET", "/foo/:bar", 1) + assert.throws(() => router.on("GET", "/foo/:baz", 2)) +}) + +it("should throw when set upping two regexp nodes", () => { + const router = Router.make() + router.on("GET", "/foo/:bar(123)", 1) + assert.throws(() => router.on("GET", "/foo/:bar(456)", 2)) +}) + +it("should set up two parametric nodes with static ending", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar.png", 1) + router.on("GET", "/foo/:bar.jpeg", 2) + + assert.strictEqual(router.find("GET", "/foo/value.png")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/value.jpeg")?.handler, 2) +}) + +it("should set up two regexp nodes with static ending", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar(123).png", 1) + router.on("GET", "/foo/:bar(456).jpeg", 2) + + assert.strictEqual(router.find("GET", "/foo/123.png")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/456.jpeg")?.handler, 2) +}) + +it("node with longer static suffix should have higher priority", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar.png", 1) + router.on("GET", "/foo/:bar.png.png", 2) + + assert.strictEqual(router.find("GET", "/foo/value.png")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/value.png.png")?.handler, 2) +}) + +it("node with longer static suffix should have higher priority", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar.png.png", 2) + router.on("GET", "/foo/:bar.png", 1) + + assert.strictEqual(router.find("GET", "/foo/value.png")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/value.png.png")?.handler, 2) +}) + +it("should set up regexp node and node with static ending", () => { + const router = Router.make() + + router.on("GET", "/foo/:bar(123)", 1) + router.on("GET", "/foo/:bar(123).jpeg", 2) + + assert.strictEqual(router.find("GET", "/foo/123")?.handler, 1) + assert.strictEqual(router.find("GET", "/foo/123.jpeg")?.handler, 2) +}) + +it("distinguishes routes with different static parts between parameters", () => { + const router = Router.make() + + router.on("GET", "/foo/:a-:b", "dash") + router.on("GET", "/foo/:a.:b", "dot") + + assert.strictEqual(router.find("GET", "/foo/x-y")?.handler, "dash") + assert.strictEqual(router.find("GET", "/foo/x.y")?.handler, "dot") +}) + +it("preserves parameters named __proto__", () => { + const router = Router.make() + router.on("GET", "/foo/:__proto__", true) + + assert.strictEqual(router.find("GET", "/foo/value")?.params.__proto__, "value") +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/path-params-match.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/path-params-match.test.ts new file mode 100644 index 000000000..6f157275b --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/path-params-match.test.ts @@ -0,0 +1,61 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("path params match", () => { + const router = Router.make<1 | 2 | "c" | "param">() + + router.on("GET", "/ab1", 1) + router.on("GET", "/ab2", 2) + router.on("GET", "/ac", "c") + router.on("GET", "/:pam", "param") + + assert.strictEqual(router.find("GET", "/ab1")?.handler, 1) + assert.strictEqual(router.find("GET", "/ab1/")?.handler, 1) + assert.strictEqual(router.find("GET", "//ab1")?.handler, 1) + assert.strictEqual(router.find("GET", "//ab1//")?.handler, 1) + assert.strictEqual(router.find("GET", "/ab2")?.handler, 2) + assert.strictEqual(router.find("GET", "/ab2/")?.handler, 2) + assert.strictEqual(router.find("GET", "//ab2")?.handler, 2) + assert.strictEqual(router.find("GET", "//ab2//")?.handler, 2) + assert.strictEqual(router.find("GET", "/ac")?.handler, "c") + assert.strictEqual(router.find("GET", "/ac/")?.handler, "c") + assert.strictEqual(router.find("GET", "//ac")?.handler, "c") + assert.strictEqual(router.find("GET", "//ac//")?.handler, "c") + assert.strictEqual(router.find("GET", "/foo")?.handler, "param") + assert.strictEqual(router.find("GET", "/foo/")?.handler, "param") + assert.strictEqual(router.find("GET", "//foo")?.handler, "param") + assert.strictEqual(router.find("GET", "//foo//")?.handler, "param") + assert.deepStrictEqual(router.find("GET", "/abcdef"), { + handler: "param", + params: { pam: "abcdef" }, + searchParams: {} + }) + assert.deepStrictEqual(router.find("GET", "/abcdef/"), { + handler: "param", + params: { pam: "abcdef" }, + searchParams: {} + }) + assert.deepStrictEqual(router.find("GET", "//abcdef"), { + handler: "param", + params: { pam: "abcdef" }, + searchParams: {} + }) +}) + +it("does not read inherited static child entries", () => { + const label = "\uE000" + // oxlint-disable-next-line no-extend-native -- Reproduce an inherited entry without reaching into router internals. + Object.defineProperty(Object.prototype, label, { + configurable: true, + value: {} + }) + + try { + const router = Router.make() + router.on("GET", `/${label}`, true) + + assert.strictEqual(router.find("GET", `/${label}`)?.handler, true) + } finally { + Reflect.deleteProperty(Object.prototype, label) + } +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/querystring.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/querystring.test.ts new file mode 100644 index 000000000..e170dc654 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/querystring.test.ts @@ -0,0 +1,38 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("should sanitize the url - query", () => { + const router = Router.make() + router.on("GET", "/test", true) + assert.deepStrictEqual( + router.find("GET", "/test?hello=world")?.searchParams, + { hello: "world" } + ) +}) + +it("should sanitize the url - hash", () => { + const router = Router.make() + + router.on("GET", "/test", true) + + assert.deepStrictEqual(router.find("GET", "/test#hello")?.searchParams, { + hello: "" + }) +}) + +it("handles path and query separated by ;", () => { + const router = Router.make() + router.on("GET", "/test", true) + assert.deepStrictEqual( + router.find("GET", "/test;jsessionid=123456")?.searchParams, + { jsessionid: "123456" } + ) +}) + +it("handles %", () => { + const router = Router.make() + router.on("GET", "/test", true) + assert.deepStrictEqual(router.find("GET", "/test?%")?.searchParams, { + "%": "" + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/FindMyWay/regex.test.ts b/.context/effect/packages/effect/test/unstable/http/FindMyWay/regex.test.ts new file mode 100644 index 000000000..0cc599126 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/FindMyWay/regex.test.ts @@ -0,0 +1,124 @@ +import { assert, it } from "@effect/vitest" +import { FindMyWay as Router } from "effect/unstable/http" + +it("route with matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)", true) + assert.deepStrictEqual(router.find("GET", "/test/12")?.handler, true) +}) + +it("route without matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)", true) + assert.isUndefined(router.find("GET", "/test/test")) +}) + +it("route with an extension regex 2", () => { + const router = Router.make() + + router.on("GET", "/test/S/:file(^\\S+).png", 1) + router.on("GET", "/test/D/:file(^\\D+).png", 2) + + assert.strictEqual(router.find("GET", "/test/S/foo.png")?.handler, 1) + assert.strictEqual(router.find("GET", "/test/D/foo.png")?.handler, 2) +}) + +it("nested route with matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)/hello", true) + assert.deepStrictEqual(router.find("GET", "/test/12/hello")?.handler, true) +}) + +it("mixed nested route with matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)/hello/:world", true) + assert.deepStrictEqual(router.find("GET", "/test/12/hello/world")?.params, { + id: "12", + world: "world" + }) +}) + +it("mixed nested route with double matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)/hello/:world(^\\d+$)", true) + assert.deepStrictEqual(router.find("GET", "/test/12/hello/15")?.params, { + id: "12", + world: "15" + }) +}) + +it("mixed nested route without double matching regex", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)/hello/:world(^\\d+$)", true) + assert.isUndefined(router.find("GET", "/test/12/hello/test")) +}) + +it("route with an extension regex", () => { + const router = Router.make() + router.on("GET", "/test/:file(^\\d+).png", true) + assert.deepStrictEqual(router.find("GET", "/test/12.png")?.handler, true) +}) + +it("route with an extension regex - no match", () => { + const router = Router.make() + router.on("GET", "/test/:file(^\\d+).png", true) + assert.isUndefined(router.find("GET", "/test/aa.png")) +}) + +it("safe decodeURIComponent", () => { + const router = Router.make() + router.on("GET", "/test/:id(^\\d+$)", true) + assert.isUndefined(router.find("GET", "/test/hel%\"Flo")) +}) + +it("rejects truncated percent encodings", () => { + const router = Router.make() + router.on("GET", "/test/:id", true) + + assert.isUndefined(router.find("GET", "/test/a%")) + assert.isUndefined(router.find("GET", "/test/a%2")) +}) + +it("does not match an empty segment against a non-empty regex", () => { + const router = Router.make({ + ignoreTrailingSlash: false + }) + router.on("GET", "/users/:userId(^\\d+)", true) + + assert.isUndefined(router.find("GET", "/users/")) +}) + +it("matches undefined regex captures as empty strings", () => { + const router = Router.make({ + ignoreTrailingSlash: false + }) + router.on("GET", "/test/:id(^((?!abc).)*$)", true) + + assert.deepStrictEqual(router.find("GET", "/test/")?.params, { + id: "" + }) +}) + +it("falls back when a parameter exceeds maxParamLength", () => { + const router = Router.make({ + maxParamLength: 3 + }) + router.on("GET", "/users/:userId", "parameter") + router.on("GET", "/users/*", "wildcard") + + assert.strictEqual(router.find("GET", "/users/long")?.handler, "wildcard") +}) + +it("avoids backtracking across static parameter separators", { timeout: 1_000 }, () => { + const router = Router.make() + router.on("GET", "/:foo-:bar-", true) + + router.find("GET", "/" + "-".repeat(16_000) + "a") +}) + +it("avoids backtracking across mixed static parameter separators", { timeout: 1_000 }, () => { + const router = Router.make() + router.on("GET", "/:foo-:bar-", true) + + router.find("GET", "/" + "a-".repeat(8_000) + "b") +}) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpBody.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpBody.test.ts new file mode 100644 index 000000000..8429ebc35 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/HttpBody.test.ts @@ -0,0 +1,14 @@ +import { assert, it } from "@effect/vitest" +import { Effect, FileSystem, Stream } from "effect" +import { HttpBody } from "effect/unstable/http" + +it.effect("uses the selected byte count as partial file content length", () => + Effect.gen(function*() { + const body = yield* HttpBody.fileFromInfo("x", { size: 6n } as any, { offset: 2, bytesToRead: 2 }).pipe( + Effect.provideService(FileSystem.FileSystem, { + stream: () => Stream.succeed(new Uint8Array([3, 4])) + } as any) + ) + const bytes = yield* Stream.mkUint8Array(body.stream) + assert.strictEqual(body.contentLength, bytes.length) + })) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts index 4660a42cf..1f0618130 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpClient.test.ts @@ -2,7 +2,8 @@ import { assert, describe, it } from "@effect/vitest" import { strictEqual } from "@effect/vitest/utils" import { Clock, Duration, Effect, Fiber, Layer, Ref, Stream } from "effect" import { TestClock } from "effect/testing" -import { HttpClient, HttpClientResponse } from "effect/unstable/http" +import * as Tracer from "effect/Tracer" +import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { RateLimiter } from "effect/unstable/persistence" const makeStatusClient = Effect.fnUntraced(function*(status: number) { @@ -16,9 +17,247 @@ const makeStatusClient = Effect.fnUntraced(function*(status: number) { return { attempts, client } as const }) +const makeRedirectClient = Effect.fnUntraced(function*(status: number, location: string | ReadonlyArray) { + const locations = typeof location === "string" ? [location] : location + const requests = yield* Ref.make>([]) + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(requests, (requests) => [...requests, request]), + (requests) => + HttpClientResponse.fromWeb( + request, + requests.length <= locations.length + ? new Response(null, { status, headers: { location: locations[requests.length - 1] } }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe(HttpClient.followRedirects()) + return { client, requests } as const +}) + const RateLimiterTestLayer = RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory)) describe("HttpClient", () => { + it.effect("preserves source bytes after reading response text", () => + Effect.gen(function*() { + const response = HttpClientResponse.fromWeb( + HttpClientRequest.get("https://example.com"), + new Response(new Uint8Array([0xef, 0xbb, 0xbf, 0x61])) + ) + yield* response.text + const bytes = new Uint8Array(yield* response.arrayBuffer) + assert.deepStrictEqual(Array.from(bytes), [0xef, 0xbb, 0xbf, 0x61]) + })) + + describe("tracer", () => { + it.effect("includes request and response headers by default", () => + Effect.gen(function*() { + let clientSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + clientSpan = new Tracer.NativeSpan(options) + return clientSpan + } + }) + const client = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { + headers: { "x-response-default": "response" } + }) + ) + ) + ) + + yield* client.get("http://test/", { + headers: { "x-request-default": "request" } + }).pipe(Effect.provideService(Tracer.Tracer, tracer)) + + assert(clientSpan !== undefined) + assert.strictEqual(clientSpan.attributes.get("http.request.header.x-request-default"), "request") + assert.strictEqual(clientSpan.attributes.get("http.response.header.x-response-default"), "response") + })) + + it.effect("filters request and response header span attributes", () => + Effect.gen(function*() { + let clientSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + clientSpan = new Tracer.NativeSpan(options) + return clientSpan + } + }) + const client = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { + headers: { + "x-response-drop": "drop", + "x-response-keep": "keep" + } + }) + ) + ) + ) + + yield* client.get("http://test/", { + headers: { + "x-request-drop": "drop", + "x-request-keep": "keep" + } + }).pipe( + Effect.provideService(HttpClient.TracerHeaderFilter, (name) => name.endsWith("-keep")), + Effect.provideService(Tracer.Tracer, tracer) + ) + + assert(clientSpan !== undefined) + assert.strictEqual(clientSpan.attributes.get("http.request.header.x-request-drop"), undefined) + assert.strictEqual(clientSpan.attributes.get("http.request.header.x-request-keep"), "keep") + assert.strictEqual(clientSpan.attributes.get("http.response.header.x-response-drop"), undefined) + assert.strictEqual(clientSpan.attributes.get("http.response.header.x-response-keep"), "keep") + })) + + it.effect("filters the same header name independently by phase", () => + Effect.gen(function*() { + let clientSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + clientSpan = new Tracer.NativeSpan(options) + return clientSpan + } + }) + const client = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { + headers: { "x-phase-filter": "response" } + }) + ) + ) + ) + + yield* client.get("http://test/", { + headers: { "x-phase-filter": "request" } + }).pipe( + Effect.provideService(HttpClient.TracerHeaderFilter, (_name, phase) => phase === "response"), + Effect.provideService(Tracer.Tracer, tracer) + ) + + assert(clientSpan !== undefined) + assert.strictEqual(clientSpan.attributes.get("http.request.header.x-phase-filter"), undefined) + assert.strictEqual(clientSpan.attributes.get("http.response.header.x-phase-filter"), "response") + })) + }) + + describe("followRedirects", () => { + it.effect("preserves credential headers on same-origin redirects", () => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(302, "https://origin.test/destination") + yield* client.get("https://origin.test/start", { + headers: { + Authorization: "Bearer secret", + Cookie: "session=secret" + } + }) + + const redirected = (yield* Ref.get(requests))[1] + assert.strictEqual(redirected.headers.authorization, "Bearer secret") + assert.strictEqual(redirected.headers.cookie, "session=secret") + })) + + it.effect("strips credential headers on cross-origin redirects", () => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(302, "https://redirect.test/destination") + yield* client.get("https://origin.test/start", { + headers: { + Authorization: "Bearer secret", + Cookie: "session=secret", + "Proxy-Authorization": "Basic secret", + "X-Test": "retained" + } + }) + + const redirected = (yield* Ref.get(requests))[1] + assert.isUndefined(redirected.headers.authorization) + assert.isUndefined(redirected.headers.cookie) + assert.isUndefined(redirected.headers["proxy-authorization"]) + assert.strictEqual(redirected.headers["x-test"], "retained") + })) + + it.effect("strips credential headers on scheme downgrade redirects", () => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(302, "http://origin.test/destination") + yield* client.get("https://origin.test/start", { + headers: { + Authorization: "Bearer secret", + Cookie: "session=secret", + "Proxy-Authorization": "Basic secret" + } + }) + + const redirected = (yield* Ref.get(requests))[1] + assert.isUndefined(redirected.headers.authorization) + assert.isUndefined(redirected.headers.cookie) + assert.isUndefined(redirected.headers["proxy-authorization"]) + })) + + it.effect("resolves relative locations against the current hop", () => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(302, ["../next/step", "destination"]) + yield* client.get("https://origin.test/path/start", { + headers: { Authorization: "Bearer secret" } + }) + + const redirected = (yield* Ref.get(requests))[2] + assert.strictEqual(redirected.url, "https://origin.test/next/destination") + assert.strictEqual(redirected.headers.authorization, "Bearer secret") + })) + + it.effect("switches 303 requests to GET and drops the body", () => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(303, "/destination") + yield* HttpClientRequest.post("https://origin.test/start").pipe( + HttpClientRequest.bodyText("payload"), + client.execute + ) + + const redirected = (yield* Ref.get(requests))[1] + assert.strictEqual(redirected.method, "GET") + assert.strictEqual(redirected.body._tag, "Empty") + assert.isUndefined(redirected.headers["content-type"]) + assert.isUndefined(redirected.headers["content-length"]) + })) + + it.effect.each([301, 302])("switches POST to GET on %s redirects", (status) => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(status, "/destination") + yield* HttpClientRequest.post("https://origin.test/start").pipe( + HttpClientRequest.bodyText("payload"), + client.execute + ) + + const redirected = (yield* Ref.get(requests))[1] + assert.strictEqual(redirected.method, "GET") + assert.strictEqual(redirected.body._tag, "Empty") + })) + + it.effect.each([301, 302])("preserves non-POST methods on %s redirects", (status) => + Effect.gen(function*() { + const { client, requests } = yield* makeRedirectClient(status, "/destination") + yield* HttpClientRequest.put("https://origin.test/start").pipe( + HttpClientRequest.bodyText("payload"), + client.execute + ) + + const redirected = (yield* Ref.get(requests))[1] + assert.strictEqual(redirected.method, "PUT") + assert.strictEqual(redirected.body._tag, "Uint8Array") + })) + }) + describe("retryTransient", () => { it.effect("retries transient responses with retryOn errors-and-responses", () => Effect.gen(function*() { @@ -145,6 +384,52 @@ describe("HttpClient", () => { strictEqual(yield* Ref.get(attempts), 2) }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("updates limits from custom response headers", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 200, + headers: { + "x-vendor-limit": "1", + "x-vendor-reset": "60" + } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: yield* RateLimiter.RateLimiter, + key: "custom-limit", + limit: 10, + window: "1 minute", + responseHeaders: { + limit: "X-Vendor-Limit", + reset: "X-Vendor-Reset" + } + }) + ) + + const fiber = yield* client.get("http://test/").pipe( + Effect.andThen(client.get("http://test/")), + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust("5 seconds") + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("1 minute") + yield* Fiber.join(fiber) + strictEqual(yield* Ref.get(attempts), 2) + }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("inspects remaining headers to infer updated limits", () => Effect.gen(function*() { const attempts = yield* Ref.make(0) @@ -187,6 +472,90 @@ describe("HttpClient", () => { strictEqual(yield* Ref.get(attempts), 2) }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("inspects custom remaining and reset-after headers", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const limiter = yield* RateLimiter.RateLimiter + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 200, + headers: { + "x-vendor-remaining": "0", + "x-vendor-reset-after": "60" + } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter, + key: "custom-remaining", + limit: 10, + window: "1 minute", + responseHeaders: { + remaining: "X-Vendor-Remaining", + resetAfter: "X-Vendor-Reset-After" + } + }) + ) + + const fiber = yield* client.get("http://test/").pipe( + Effect.andThen(client.get("http://test/")), + Effect.forkChild({ startImmediately: true }) + ) + + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("10 seconds") + yield* Fiber.join(fiber) + strictEqual(yield* Ref.get(attempts), 2) + }).pipe(Effect.provide(RateLimiterTestLayer))) + + it.effect("uses custom header overrides without falling back to built-in names", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 200, + headers: { + "x-ratelimit-limit": "1", + "x-ratelimit-reset": "60" + } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: yield* RateLimiter.RateLimiter, + key: "custom-override", + limit: 10, + window: "1 minute", + responseHeaders: { + limit: "X-Vendor-Limit" + } + }) + ) + + yield* client.get("http://test/") + yield* client.get("http://test/") + + strictEqual(yield* Ref.get(attempts), 2) + }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("can disable response header inspection", () => Effect.gen(function*() { const attempts = yield* Ref.make(0) @@ -303,6 +672,95 @@ describe("HttpClient", () => { strictEqual(yield* Ref.get(attempts), 2) }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("bounds automatic 429 response retries", () => + Effect.gen(function*() { + const { attempts, client } = yield* makeStatusClient(429) + const limitedClient = client.pipe( + HttpClient.withRateLimiter({ + limiter: yield* RateLimiter.RateLimiter, + key: "bounded-response", + limit: 100, + window: "1 minute", + times: 2, + disableResponseInspection: true + }) + ) + + const response = yield* limitedClient.get("http://test/") + + strictEqual(response.status, 429) + strictEqual(yield* Ref.get(attempts), 3) + }).pipe(Effect.provide(RateLimiterTestLayer))) + + it.effect("bounds automatic HttpClientError 429 retries", () => + Effect.gen(function*() { + const { attempts, client } = yield* makeStatusClient(429) + const limitedClient = client.pipe( + HttpClient.filterStatusOk, + HttpClient.withRateLimiter({ + limiter: yield* RateLimiter.RateLimiter, + key: "bounded-error", + limit: 100, + window: "1 minute", + times: 2, + disableResponseInspection: true + }) + ) + + const error = yield* limitedClient.get("http://test/").pipe(Effect.flip) + + strictEqual(error._tag, "HttpClientError") + strictEqual(error.reason._tag, "StatusCodeError") + if (error.reason._tag === "StatusCodeError") { + strictEqual(error.reason.response.status, 429) + } + strictEqual(yield* Ref.get(attempts), 3) + }).pipe(Effect.provide(RateLimiterTestLayer))) + + it.effect("uses a custom Retry-After header", () => + Effect.gen(function*() { + const attempts = yield* Ref.make(0) + const client = HttpClient.make((request) => + Effect.map( + Ref.updateAndGet(attempts, (n) => n + 1), + (attempt) => + HttpClientResponse.fromWeb( + request, + attempt === 1 + ? new Response(null, { + status: 429, + headers: { "x-vendor-retry-after": "10" } + }) + : new Response(null, { status: 200 }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: yield* RateLimiter.RateLimiter, + key: "custom-retry-after", + limit: 100, + window: "1 minute", + times: 1, + responseHeaders: { + retryAfter: "X-Vendor-Retry-After" + }, + disableAdaptiveLearning: true + }) + ) + + const fiber = yield* client.get("http://test/").pipe(Effect.forkChild({ startImmediately: true })) + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("1 second") + const response = yield* Fiber.join(fiber) + + strictEqual(response.status, 200) + strictEqual(yield* Ref.get(attempts), 2) + }).pipe(Effect.provide(RateLimiterTestLayer))) + it.effect("applies adaptive cooldown to requests delayed by the configured limiter", () => Effect.gen(function*() { const attempts = yield* Ref.make>([]) @@ -413,6 +871,56 @@ describe("HttpClient", () => { strictEqual(yield* Ref.get(attemptsB), 1) }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + it.effect("applies Retry-After feedback when automatic retries are disabled", () => + Effect.gen(function*() { + const attemptsB = yield* Ref.make(0) + const limiterA = yield* RateLimiter.make + const limiterB = yield* RateLimiter.make + const clientA = HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { + status: 429, + headers: { "retry-after": "10" } + }) + ) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: limiterA, + key: "terminal-feedback", + limit: 100, + window: "1 minute", + times: 0 + }) + ) + const clientB = HttpClient.make((request) => + Effect.as( + Ref.update(attemptsB, (n) => n + 1), + HttpClientResponse.fromWeb(request, new Response(null, { status: 200 })) + ) + ).pipe( + HttpClient.withRateLimiter({ + limiter: limiterB, + key: "terminal-feedback", + limit: 100, + window: "1 minute" + }) + ) + + const response = yield* clientA.get("http://test/a") + strictEqual(response.status, 429) + + const fiberB = yield* clientB.get("http://test/b").pipe(Effect.forkChild({ startImmediately: true })) + yield* TestClock.adjust("9 seconds") + strictEqual(yield* Ref.get(attemptsB), 0) + + yield* TestClock.adjust("1 second") + yield* Fiber.join(fiberB) + strictEqual(yield* Ref.get(attemptsB), 1) + }).pipe(Effect.provide(RateLimiter.layerStoreMemory))) + it.effect("learns adaptive pacing from Retry-After feedback", () => Effect.gen(function*() { const attempts = yield* Ref.make(0) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpClientRequest.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpClientRequest.test.ts index 5eb757907..ba09c404a 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpClientRequest.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpClientRequest.test.ts @@ -1,8 +1,8 @@ import { describe, it } from "@effect/vitest" -import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" +import { assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Effect, Stream } from "effect" import * as Option from "effect/Option" -import { HttpClientRequest } from "effect/unstable/http" +import { Headers, HttpBody, HttpClientRequest } from "effect/unstable/http" describe("HttpClientRequest", () => { describe("appendUrl", () => { @@ -82,6 +82,71 @@ describe("HttpClientRequest", () => { }) }) + describe("setBody", () => { + it("removes stale content length when the replacement body has no known length", () => { + const request = HttpClientRequest.bodyText(HttpClientRequest.post("https://example.com"), "abc").pipe( + HttpClientRequest.setBody(HttpBody.stream(Stream.empty)) + ) + + strictEqual(request.headers["content-length"], undefined) + }) + }) + + describe("removeHeader", () => { + it("removes an existing header", () => { + const request = HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("X-Test", "ok"), + HttpClientRequest.removeHeader("X-Test") + ) + + strictEqual(request.headers["x-test"], undefined) + }) + + it("no-ops on a missing header", () => { + const request = HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeader("X-Test", "ok") + ) + const removed = HttpClientRequest.removeHeader(request, "X-Missing") + + deepStrictEqual(removed.headers, request.headers) + }) + + it("preserves the request prototype", () => { + const request = HttpClientRequest.get("/").pipe( + HttpClientRequest.removeHeader("X-Test") + ) + + assertTrue(HttpClientRequest.isHttpClientRequest(request)) + }) + }) + + describe("updateHeaders", () => { + it("transforms the header collection", () => { + const request = HttpClientRequest.get("/").pipe( + HttpClientRequest.setHeaders({ "X-A": "a", "X-B": "b" }), + HttpClientRequest.updateHeaders((headers) => Headers.set(Headers.remove(headers, "X-A"), "X-C", "c")) + ) + + deepStrictEqual(request.headers, Headers.fromInput({ "x-b": "b", "x-c": "c" })) + }) + + it("preserves the request prototype", () => { + const request = HttpClientRequest.get("/").pipe( + HttpClientRequest.updateHeaders(Headers.set("X-Test", "ok")) + ) + + assertTrue(HttpClientRequest.isHttpClientRequest(request)) + strictEqual(request.headers["x-test"], "ok") + }) + + it("supports the data-first overload", () => { + const request = HttpClientRequest.get("/") + const updated = HttpClientRequest.updateHeaders(request, Headers.set("X-Test", "ok")) + + strictEqual(updated.headers["x-test"], "ok") + }) + }) + describe("hash", () => { it("stores hash as Option", () => { const request = HttpClientRequest.get(new URL("http://example.com/path?x=1#section")) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpCompression.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpCompression.test.ts new file mode 100644 index 000000000..7c6d9a130 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/HttpCompression.test.ts @@ -0,0 +1,416 @@ +import { afterAll, assert, describe, it } from "@effect/vitest" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Latch from "effect/Latch" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Cookies from "effect/unstable/http/Cookies" +import * as HttpEffect from "effect/unstable/http/HttpEffect" +import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" +import * as HttpServer from "effect/unstable/http/HttpServer" +import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" + +const bigText = "All work and no play makes Jack a dull boy. ".repeat(100) +const bigJson = JSON.stringify({ text: bigText }) + +const bigJsonApp = Effect.succeed(HttpServerResponse.text(bigJson, { contentType: "application/json" })) + +type App = Effect.Effect +type CompressionOptions = Parameters[0] + +const get = ( + handler: (request: Request) => Promise, + headers?: Record, + method?: string +) => + handler( + new Request("http://localhost/", { + method: method ?? "GET", + ...(headers === undefined ? {} : { headers }) + }) + ) + +const decompress = (data: ArrayBuffer, format: "gzip" | "deflate"): Promise => + new Response(new Blob([data]).stream().pipeThrough(new DecompressionStream(format))).text() + +const platformContext = (compression: HttpPlatform.Compression): Context.Context => + Context.make( + HttpPlatform.HttpPlatform, + HttpPlatform.HttpPlatform.of({ + platform: "web", + compression, + fileResponse: () => Effect.die("not implemented"), + fileWebResponse: () => Effect.die("not implemented") + }) + ) + +// A platform stub used to exercise negotiation against algorithms the Web +// implementation does not support. It only marks the chosen algorithm. +const negotiationPlatformContext = (algorithms: ReadonlyArray) => + platformContext({ + algorithms: new Set(algorithms), + compressResponse: (response, algorithm) => + Effect.succeed(HttpServerResponse.setHeader(response, "content-encoding", algorithm)) + }) + +const makeHandler = ( + app: App, + options?: CompressionOptions, + context?: Context.Context +) => { + const self = app as Effect.Effect + const middleware = HttpMiddleware.compression(options) + if (context !== undefined) { + return HttpEffect.toWebHandlerWith(context)( + self, + middleware + ) + } + const { dispose, handler } = HttpEffect.toWebHandlerLayer(self, HttpServer.layerServices, { middleware }) + disposers.push(dispose) + return handler +} + +const disposers: Array<() => Promise> = [] + +afterAll(() => Promise.all(disposers.map((dispose) => dispose()))) + +const randomBytes = (length: number): Uint8Array => { + const data = new Uint8Array(length) + let state = 0x9e3779b9 + for (let i = 0; i < length; i++) { + state ^= state << 13 + state ^= state >>> 17 + state ^= state << 5 + data[i] = state & 0xff + } + return data +} + +describe("HttpCompression", () => { + it("compresses a compressible response with gzip", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip" }) + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(response.headers.get("content-length"), null) + const text = await decompress(await response.arrayBuffer(), "gzip") + assert.strictEqual(text, bigJson) + }) + + it("compresses with deflate when requested", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "deflate" }) + assert.strictEqual(response.headers.get("content-encoding"), "deflate") + const text = await decompress(await response.arrayBuffer(), "deflate") + assert.strictEqual(text, bigJson) + }) + + it("server preference order picks br over gzip when supported", async () => { + const response = await get( + makeHandler(bigJsonApp, undefined, negotiationPlatformContext(["gzip", "deflate", "br", "zstd"])), + { "accept-encoding": "gzip, br" } + ) + assert.strictEqual(response.headers.get("content-encoding"), "br") + }) + + it("client q-values decide acceptability only, server order decides ranking", async () => { + const response = await get( + makeHandler(bigJsonApp, undefined, negotiationPlatformContext(["gzip", "deflate", "br", "zstd"])), + { "accept-encoding": "gzip;q=1, br;q=0.5" } + ) + assert.strictEqual(response.headers.get("content-encoding"), "br") + }) + + it("q=0 makes a coding unacceptable", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip;q=0" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(await response.text(), bigJson) + }) + + it("falls through server order past q=0 codings", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip;q=0, deflate" }) + assert.strictEqual(response.headers.get("content-encoding"), "deflate") + }) + + it("wildcard matches unlisted codings", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "*" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + }) + + it("wildcard with q=0 disables unlisted codings", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "*;q=0" }) + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("sends identity with 200 when nothing is acceptable, never 406", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "br, identity;q=0" }) + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(await response.text(), bigJson) + }) + + it("treats a malformed Accept-Encoding as absent", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip;q=oops" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("treats an out-of-range q-value as absent", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip;q=1.5" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("does not compress without an Accept-Encoding header", async () => { + const response = await get(makeHandler(bigJsonApp)) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(await response.text(), bigJson) + }) + + it("skips bodies below minSize but still sets Vary", async () => { + const app = Effect.succeed(HttpServerResponse.text("{}", { contentType: "application/json" })) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(response.headers.get("content-length"), "2") + assert.strictEqual(await response.text(), "{}") + }) + + it("respects a custom minSize", async () => { + const app = Effect.succeed(HttpServerResponse.text("{}", { contentType: "application/json" })) + const response = await get(makeHandler(app, { minSize: 1 }), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + }) + + it("skips non-compressible content types without Vary", async () => { + const app = Effect.succeed( + HttpServerResponse.uint8Array(randomBytes(2048), { contentType: "image/png" }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), null) + }) + + it("supports a custom compressible predicate", async () => { + const app = Effect.succeed( + HttpServerResponse.uint8Array(randomBytes(2048), { contentType: "image/png" }) + ) + const response = await get( + makeHandler(app, { compressible: (contentType) => contentType === "image/png" }), + { "accept-encoding": "gzip" } + ) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + }) + + it("leaves already-encoded responses untouched", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { "content-encoding": "br" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "br") + assert.strictEqual(response.headers.get("vary"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("does not stamp headers when a Raw body cannot be transformed", async () => { + const app = Effect.succeed( + HttpServerResponse.raw(new Response(null), { + contentType: "application/json", + headers: { etag: "\"abc\"" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), null) + assert.strictEqual(response.headers.get("etag"), "\"abc\"") + assert.strictEqual(await response.text(), "") + }) + + it("strips a Content-Encoding: identity opt-out before sending", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { "content-encoding": "identity" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("honors Cache-Control: no-transform", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { "cache-control": "public, no-transform" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(await response.text(), bigJson) + }) + + it("leaves 204, 304, and 206 responses untouched", async () => { + for (const status of [204, 304]) { + const response = await get( + makeHandler(Effect.succeed(HttpServerResponse.empty({ status }))), + { "accept-encoding": "gzip" } + ) + assert.strictEqual(response.status, status) + assert.strictEqual(response.headers.get("content-encoding"), null) + assert.strictEqual(response.headers.get("vary"), null) + } + const partial = await get( + makeHandler(Effect.succeed(HttpServerResponse.text(bigJson, { + status: 206, + contentType: "application/json" + }))), + { "accept-encoding": "gzip" } + ) + assert.strictEqual(partial.status, 206) + assert.strictEqual(partial.headers.get("content-encoding"), null) + assert.strictEqual(await partial.text(), bigJson) + }) + + it("mirrors GET headers for HEAD requests without a body", async () => { + const response = await get(makeHandler(bigJsonApp), { "accept-encoding": "gzip" }, "HEAD") + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + assert.strictEqual(await response.text(), "") + }) + + it("compresses unknown-length streams incrementally", async () => { + const latch = Latch.makeUnsafe(false) + const first = randomBytes(1 << 17) + const second = randomBytes(1 << 10) + const app = Effect.succeed(HttpServerResponse.stream( + Stream.concat( + Stream.succeed(first), + Stream.concat(Stream.drain(Stream.fromEffect(latch.await)), Stream.succeed(second)) + ), + { contentType: "text/event-stream" } + )) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + const reader = response.body!.getReader() + const chunks: Array = [] + // the first compressed chunk must arrive while the source stream is + // still open, i.e. before the latch is released + const initial = await reader.read() + assert.isFalse(initial.done) + chunks.push(initial.value!) + latch.openUnsafe() + let result = await reader.read() + while (!result.done) { + chunks.push(result.value!) + result = await reader.read() + } + const decompressed = new Uint8Array( + await new Response( + new Blob(chunks as Array).stream().pipeThrough(new DecompressionStream("gzip")) + ).arrayBuffer() + ) + assert.strictEqual(decompressed.length, first.length + second.length) + assert.deepStrictEqual(decompressed.slice(0, first.length), first) + assert.deepStrictEqual(decompressed.slice(first.length), second) + }) + + it("weakens a strong ETag on compressed responses", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { etag: "\"abc\"" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("etag"), "W/\"abc\"") + }) + + it("leaves a weak ETag unchanged on compressed responses", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { etag: "W/\"abc\"" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("etag"), "W/\"abc\"") + }) + + it("leaves the ETag of skipped responses unchanged", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { etag: "\"abc\"" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "identity" }) + assert.strictEqual(response.headers.get("etag"), "\"abc\"") + }) + + it("falls through to a supported algorithm when the preferred one is unavailable", async () => { + const response = await get( + makeHandler(bigJsonApp, { algorithms: ["zstd", "br", "gzip"] }), + { "accept-encoding": "zstd, br, gzip" } + ) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + }) + + it("appends Accept-Encoding to an existing Vary header", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { vary: "Origin" } + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("vary"), "Origin, Accept-Encoding") + }) + + it("does not duplicate Accept-Encoding in Vary and leaves Vary: * alone", async () => { + const existing = await get( + makeHandler(Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { vary: "accept-encoding" } + }) + )), + { "accept-encoding": "gzip" } + ) + assert.strictEqual(existing.headers.get("vary"), "accept-encoding") + + const star = await get( + makeHandler(Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + headers: { vary: "*" } + }) + )), + { "accept-encoding": "gzip" } + ) + assert.strictEqual(star.headers.get("vary"), "*") + }) + + it("preserves cookies on compressed responses", async () => { + const app = Effect.succeed( + HttpServerResponse.text(bigJson, { + contentType: "application/json", + cookies: Cookies.fromSetCookie(["session=abc"]) + }) + ) + const response = await get(makeHandler(app), { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.deepStrictEqual(response.headers.getSetCookie(), ["session=abc"]) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpEffect.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpEffect.test.ts index 1b4ddee1c..544826a95 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpEffect.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpEffect.test.ts @@ -1,6 +1,6 @@ -import { describe, test } from "@effect/vitest" +import { describe, it, test } from "@effect/vitest" import { deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import { Context, Effect, References, Stream } from "effect" +import { Context, Effect, References, Scope, Stream } from "effect" import * as Layer from "effect/Layer" import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { @@ -8,7 +8,33 @@ import { requestPreResponseHandlers } from "effect/unstable/http/internal/preResponseHandler" +const TestValue = Context.Reference("test/TestValue", { defaultValue: () => 0 }) + describe("HttpEffect", () => { + it.effect("restores the request Scope context identity", () => { + const request = HttpServerRequest.fromWeb(new Request("http://localhost:3000/")) + return Effect.gen(function*() { + const before = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context)) + let during: Context.Context | undefined + + yield* HttpEffect.toHandled( + Effect.withFiber((fiber) => { + during = fiber.context + strictEqual(Context.getOrUndefined(fiber.context, Scope.Scope) !== undefined, true) + return Effect.succeed(HttpServerResponse.empty()) + }), + () => Effect.void + ) + + const after = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context)) + strictEqual(during === before, false) + strictEqual(after, before) + }).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(References.TracerEnabled, false) + ) + }) + describe("toWebHandler", () => { test("json", async () => { const handler = HttpEffect.toWebHandler(HttpServerResponse.json({ foo: "bar" })) @@ -84,9 +110,9 @@ describe("HttpEffect", () => { test("stream runtime", async () => { const handler = Effect.succeed(HttpServerResponse.stream( - Stream.fromEffect(References.CurrentConcurrency).pipe(Stream.map(String), Stream.encodeText) + Stream.fromEffect(TestValue).pipe(Stream.map(String), Stream.encodeText) )).pipe( - HttpEffect.toWebHandlerWith(References.CurrentConcurrency.context(420)) + HttpEffect.toWebHandlerWith(TestValue.context(420)) ) const response = await handler(new Request("http://localhost:3000/")) strictEqual(await response.text(), "420") @@ -95,13 +121,13 @@ describe("HttpEffect", () => { test("stream layer", async () => { const { handler } = HttpEffect.toWebHandlerLayer( Effect.succeed(HttpServerResponse.stream( - References.CurrentConcurrency.pipe( + TestValue.pipe( Stream.fromEffect, Stream.map(String), Stream.encodeText ) )), - Layer.succeed(References.CurrentConcurrency, 420) + Layer.succeed(TestValue, 420) ) const response = await handler(new Request("http://localhost:3000/")) strictEqual(await response.text(), "420") diff --git a/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts index 8de2eeaf0..25abe87bf 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpMiddleware.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest" 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 Logger from "effect/Logger" @@ -59,6 +60,90 @@ describe("HttpMiddleware", () => { }) describe("tracer", () => { + it.effect("restores the ParentSpan context identity", () => { + const request = HttpServerRequest.fromWeb(new Request("http://localhost:3000/")) + return Effect.gen(function*() { + const before = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context)) + let during: typeof before | undefined + + yield* HttpMiddleware.tracer( + Effect.withFiber((fiber) => { + during = fiber.context + assert.strictEqual(Context.getOrUndefined(fiber.context, Tracer.ParentSpan) !== undefined, true) + return Effect.succeed(HttpServerResponse.empty()) + }) + ) + + const after = yield* Effect.withFiber((fiber) => Effect.succeed(fiber.context)) + assert.notStrictEqual(during, before) + assert.strictEqual(after, before) + }).pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, request)) + }) + + it.effect("records attributes for sampled spans", () => + Effect.gen(function*() { + let serverSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + serverSpan = new Tracer.NativeSpan(options) + return serverSpan + } + }) + const request = HttpServerRequest.fromWeb( + new Request("https://localhost:3000/todos/1?foo=bar", { + method: "POST", + headers: { + "user-agent": "test-agent", + "x-request": "request" + } + }) + ) + const response = HttpServerResponse.empty({ + status: 201, + headers: { "x-response": "response" } + }) + + yield* HttpMiddleware.tracer(Effect.succeed(response)).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Tracer.Tracer, tracer) + ) + yield* Effect.yieldNow + + assert(serverSpan !== undefined) + assert.strictEqual(serverSpan.sampled, true) + assert.strictEqual(serverSpan.attributes.get("http.request.method"), "POST") + assert.strictEqual(serverSpan.attributes.get("url.path"), "/todos/1") + assert.strictEqual(serverSpan.attributes.get("url.query"), "foo=bar") + assert.strictEqual(serverSpan.attributes.get("user_agent.original"), "test-agent") + assert.strictEqual(serverSpan.attributes.get("http.request.header.x-request"), "request") + assert.strictEqual(serverSpan.attributes.get("http.response.status_code"), 201) + assert.strictEqual(serverSpan.attributes.get("http.response.header.x-response"), "response") + })) + + it.effect("skips attributes for unsampled spans", () => + Effect.gen(function*() { + let serverSpan: Tracer.NativeSpan | undefined + const tracer = Tracer.make({ + span(options) { + serverSpan = new Tracer.NativeSpan(options) + return serverSpan + } + }) + const request = HttpServerRequest.fromWeb(new Request("http://localhost:3000/unsampled")) + + yield* HttpMiddleware.tracer(Effect.succeed(HttpServerResponse.empty({ status: 204 }))).pipe( + Effect.provideService(HttpServerRequest.HttpServerRequest, request), + Effect.provideService(Tracer.MinimumTraceLevel, "Fatal"), + Effect.provideService(Tracer.Tracer, tracer) + ) + yield* Effect.yieldNow + + assert(serverSpan !== undefined) + assert.strictEqual(serverSpan.sampled, false) + assert.strictEqual(serverSpan.attributes.size, 0) + assert.strictEqual(serverSpan.status._tag, "Ended") + })) + it.effect("excludes the sent response from a failed stream span", () => Effect.gen(function*() { let serverSpan: Tracer.NativeSpan | undefined diff --git a/.context/effect/packages/effect/test/unstable/http/HttpPlatform.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpPlatform.test.ts new file mode 100644 index 000000000..f5ae0cab4 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/HttpPlatform.test.ts @@ -0,0 +1,43 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, FileSystem, Stream } from "effect" +import { HttpPlatform } from "effect/unstable/http" + +describe("HttpPlatform", () => { + const file = { + name: "file.bin", + lastModified: 0, + size: 4, + type: "application/octet-stream", + stream: () => new Blob([new Uint8Array([1, 2, 3, 4])]).stream() + } + + it.effect("honors Web file offset and byte count", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) + assert.strictEqual(response.body._tag, "Stream") + if (response.body._tag === "Stream") { + assert.strictEqual(response.body.contentLength, 2) + const bytes = yield* Stream.mkUint8Array(response.body.stream) + assert.deepStrictEqual(Array.from(bytes), [2, 3]) + } + }).pipe( + Effect.provide(HttpPlatform.layer), + Effect.provideService(FileSystem.FileSystem, {} as any) + )) + + it.effect("honors Web file chunk size", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileWebResponse(file, { offset: 0, bytesToRead: 4, chunkSize: 2 }) + assert.strictEqual(response.body._tag, "Stream") + if (response.body._tag === "Stream") { + assert.strictEqual(response.body.contentLength, 4) + const chunks = yield* Stream.runCollect(response.body.stream) + assert.deepStrictEqual(chunks.map((chunk) => Array.from(chunk)), [[1, 2], [3, 4]]) + } + }).pipe( + Effect.provide(HttpPlatform.layer), + Effect.provideService(FileSystem.FileSystem, {} as any) + )) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/HttpServerRequest.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpServerRequest.test.ts index e6d4f5e59..63aeeacf6 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpServerRequest.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpServerRequest.test.ts @@ -2,9 +2,33 @@ import { assert, describe, it } from "@effect/vitest" import { assertNone, assertSome, deepStrictEqual, strictEqual } from "@effect/vitest/utils" import { Effect, Schema, Stream } from "effect" import * as Option from "effect/Option" -import { HttpClientRequest, HttpServerRequest } from "effect/unstable/http" +import { HttpBody, HttpClientRequest, HttpServerRequest } from "effect/unstable/http" describe("HttpServerRequest", () => { + it.effect("preserves FormData through client-server-client conversion", () => + Effect.gen(function*() { + const formData = new FormData() + formData.set("name", "alice") + const clientRequest = HttpClientRequest.post("https://example.com/upload").pipe( + HttpClientRequest.bodyFormData(formData) + ) + + const roundTrip = HttpServerRequest.toClientRequest(HttpServerRequest.fromClientRequest(clientRequest)) + const webRequest = yield* HttpClientRequest.toWeb(roundTrip) + const parsed = yield* Effect.tryPromise({ + try: () => webRequest.formData(), + catch: () => undefined + }).pipe(Effect.option) + + deepStrictEqual( + { + multipartMime: webRequest.headers.get("content-type")?.startsWith("multipart/form-data; boundary=") ?? false, + name: Option.isSome(parsed) ? parsed.value.get("name") : undefined + }, + { multipartMime: true, name: "alice" } + ) + })) + it("toClientRequest", async () => { const serverRequest = HttpServerRequest.fromWeb( new Request("http://localhost:3000/todos/1?a=1&a=2#top", { @@ -109,6 +133,30 @@ describe("HttpServerRequest", () => { } })) + it.effect("reads a raw BodyInit after conversion from a client request", () => + Effect.gen(function*() { + const client = HttpClientRequest.setBody(HttpClientRequest.post("https://example.com"), HttpBody.raw("abc")) + const server = HttpServerRequest.fromClientRequest(client) + assert.strictEqual(yield* server.text, "abc") + })) + + it.effect("reads raw BodyInit bytes after conversion from a client request", () => + Effect.gen(function*() { + const client = HttpClientRequest.setBody(HttpClientRequest.post("https://example.com"), HttpBody.raw("abc")) + const server = HttpServerRequest.fromClientRequest(client) + assert.deepStrictEqual(new Uint8Array(yield* server.arrayBuffer), new Uint8Array([97, 98, 99])) + })) + + it.effect("streams a raw URLSearchParams after conversion from a client request", () => + Effect.gen(function*() { + const client = HttpClientRequest.setBody( + HttpClientRequest.post("https://example.com"), + HttpBody.raw(new URLSearchParams({ a: "1", b: "two" })) + ) + const server = HttpServerRequest.fromClientRequest(client) + assert.strictEqual(yield* server.stream.pipe(Stream.decodeText(), Stream.mkString), "a=1&b=two") + })) + it.effect("schemaBodyJson applies parse options", () => Effect.gen(function*() { const request = HttpServerRequest.fromWeb( diff --git a/.context/effect/packages/effect/test/unstable/http/HttpServerResponse.test.ts b/.context/effect/packages/effect/test/unstable/http/HttpServerResponse.test.ts index f5ae2c692..122b086f3 100644 --- a/.context/effect/packages/effect/test/unstable/http/HttpServerResponse.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/HttpServerResponse.test.ts @@ -1,8 +1,20 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, References, Stream } from "effect" -import { HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/unstable/http" +import { Context, Effect, Stream } from "effect" +import { HttpBody, HttpClientRequest, HttpClientResponse, HttpServerResponse } from "effect/unstable/http" + +const TestValue = Context.Reference("test/TestValue", { defaultValue: () => 0 }) describe("HttpServerResponse", () => { + it("setHeader overrides body-derived content headers", () => { + const response = HttpServerResponse.text("body").pipe( + HttpServerResponse.setHeader("content-type", "text/custom"), + HttpServerResponse.setHeader("content-length", "1") + ) + + assert.strictEqual(response.headers["content-type"], "text/custom") + assert.strictEqual(response.headers["content-length"], "1") + }) + it.effect("fromClientResponse preserves status, headers, cookies, and json", () => Effect.gen(function*() { const request = HttpClientRequest.get("http://localhost:3000/todos/1") @@ -29,7 +41,7 @@ describe("HttpServerResponse", () => { Effect.gen(function*() { const clientResponse = HttpServerResponse.toClientResponse( HttpServerResponse.stream( - Stream.fromEffect(References.CurrentConcurrency).pipe( + Stream.fromEffect(TestValue).pipe( Stream.map(String), Stream.encodeText ) @@ -39,7 +51,7 @@ describe("HttpServerResponse", () => { const response = HttpServerResponse.fromClientResponse(clientResponse) const roundTrip = HttpServerResponse.toClientResponse(response) const text = yield* roundTrip.text.pipe( - Effect.provideService(References.CurrentConcurrency, 420) + Effect.provideService(TestValue, 420) ) assert.strictEqual(text, "420") @@ -73,4 +85,20 @@ describe("HttpServerResponse", () => { assert.strictEqual(response.status, 200) assert.strictEqual(yield* roundTrip.text, "") })) + + it("synchronizes body metadata headers for empty and replaced bodies", () => { + const emptyBytes = HttpServerResponse.uint8Array(new Uint8Array()) + assert.strictEqual(emptyBytes.headers["content-length"], "0") + + const replaced = HttpServerResponse.setBody(HttpServerResponse.text("abc"), HttpBody.empty) + assert.notProperty(replaced.headers, "content-type") + assert.notProperty(replaced.headers, "content-length") + + const streamed = HttpServerResponse.setBody( + HttpServerResponse.text("abc"), + HttpBody.stream(Stream.empty, "application/octet-stream") + ) + assert.strictEqual(streamed.headers["content-type"], "application/octet-stream") + assert.notProperty(streamed.headers, "content-length") + }) }) diff --git a/.context/effect/packages/effect/test/unstable/http/Multipart.test.ts b/.context/effect/packages/effect/test/unstable/http/Multipart.test.ts index 1dafd5a56..948bacdf1 100644 --- a/.context/effect/packages/effect/test/unstable/http/Multipart.test.ts +++ b/.context/effect/packages/effect/test/unstable/http/Multipart.test.ts @@ -1,8 +1,14 @@ import { describe, it } from "@effect/vitest" -import { Effect, ErrorReporter, identity, Schema, Stream, Unify } from "effect" -import { Multipart } from "effect/unstable/http" +import { Effect, ErrorReporter, FileSystem, identity, Path, Schema, Stream, Unify } from "effect" +import { + HttpClientRequest, + HttpIncomingMessage, + HttpServerRequest, + Multipart, + MultipartParser +} from "effect/unstable/http" import * as HttpServerRespondable from "effect/unstable/http/HttpServerRespondable" -import { deepStrictEqual, strictEqual } from "node:assert" +import { deepStrictEqual, notStrictEqual, strictEqual } from "node:assert" describe("Multipart", () => { it.effect("parses fields and streams file content", () => @@ -38,6 +44,58 @@ describe("Multipart", () => { ]) })) + it.effect("collects file content across pulls and a split trailing boundary", () => + Effect.gen(function*() { + const boundary = "----testboundary" + const encoder = new TextEncoder() + const boundarySplit = 8 + const chunks = [ + encoder.encode( + `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="file.txt"\r\n` + + `Content-Type: text/plain\r\n\r\n` + + "abc" + ), + encoder.encode(`def\r\n--${boundary.slice(0, boundarySplit)}`), + encoder.encode(`${boundary.slice(boundarySplit)}--\r\n`) + ] + + const contents = yield* Stream.fromArray(chunks).pipe( + Stream.rechunk(1), + Stream.pipeThroughChannel( + Multipart.makeChannel({ "content-type": `multipart/form-data; boundary=${boundary}` }) + ), + Stream.mapEffect((part) => part._tag === "File" ? part.contentEffect : Effect.die("expected file")), + Stream.runCollect + ) + + deepStrictEqual(contents, [encoder.encode("abcdef")]) + })) + + it.effect("parses non-Latin-1 filenames", () => + Effect.gen(function*() { + const data = new globalThis.FormData() + data.append("file", new globalThis.File(["content"], "日本語.txt", { type: "text/plain" })) + const response = new Response(data) + + const parts = yield* Stream.fromReadableStream({ + evaluate: () => response.body!, + onError: identity + }).pipe( + Stream.pipeThroughChannel(Multipart.makeChannel(Object.fromEntries(response.headers))), + Stream.mapEffect((part) => + Unify.unify( + part._tag === "File" + ? Stream.runDrain(part.content).pipe(Effect.as([part.key, part.name] as const)) + : Effect.succeed([part.key, part.value] as const) + ) + ), + Stream.runCollect + ) + + deepStrictEqual(parts, [["file", "日本語.txt"]]) + })) + it.effect("fails when a limit is exceeded even if the whole body arrives in one chunk", () => Effect.gen(function*() { const boundary = "----testboundary" @@ -60,6 +118,224 @@ describe("Multipart", () => { strictEqual(error.reason._tag, "TooManyParts") })) + it.effect("propagates FileTooLarge after a file exceeds the limit mid-stream", () => + Effect.gen(function*() { + const boundary = "----testboundary" + const encoder = new TextEncoder() + let fileParts = 0 + const fileStart = `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="file.txt"\r\n` + + `Content-Type: text/plain\r\n\r\n` + + "a" + const fileEnd = `\r\n--${boundary}--\r\n` + + const error = yield* Stream.make( + encoder.encode(fileStart), + encoder.encode("a".repeat(1024)), + encoder.encode(fileEnd) + ).pipe( + Stream.pipeThroughChannel( + Multipart.makeChannel({ "content-type": `multipart/form-data; boundary=${boundary}` }) + ), + Stream.mapEffect((part) => { + if (part._tag !== "File") { + return Effect.void + } + fileParts++ + return Stream.runDrain(part.content) + }), + Stream.runDrain, + Effect.provideService(Multipart.MaxFileSize, 256), + Effect.flip + ) + + strictEqual(fileParts, 1) + strictEqual(error._tag, "MultipartError") + strictEqual(error.reason._tag, "FileTooLarge") + })) + + it.effect("propagates BodyTooLarge after the total size limit is exceeded mid-file", () => + Effect.gen(function*() { + const boundary = "----testboundary" + const encoder = new TextEncoder() + let fileParts = 0 + const fileStart = `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="file.txt"\r\n` + + `Content-Type: text/plain\r\n\r\n` + + "a" + const fileEnd = `\r\n--${boundary}--\r\n` + + const error = yield* Stream.make( + encoder.encode(fileStart), + encoder.encode("a".repeat(1024)), + encoder.encode(fileEnd) + ).pipe( + Stream.pipeThroughChannel( + Multipart.makeChannel({ "content-type": `multipart/form-data; boundary=${boundary}` }) + ), + Stream.mapEffect((part) => { + if (part._tag !== "File") { + return Effect.void + } + fileParts++ + return Stream.runDrain(part.content) + }), + Stream.runDrain, + Effect.provideService(HttpIncomingMessage.MaxBodySize, FileSystem.Size(256)), + Effect.flip + ) + + strictEqual(fileParts, 1) + strictEqual(error._tag, "MultipartError") + strictEqual(error.reason._tag, "BodyTooLarge") + })) + + it.effect("propagates Parse when the body ends mid-file", () => + Effect.gen(function*() { + const boundary = "----testboundary" + const encoder = new TextEncoder() + let fileParts = 0 + const fileStart = `--${boundary}\r\n` + + `Content-Disposition: form-data; name="file"; filename="file.txt"\r\n` + + `Content-Type: text/plain\r\n\r\n` + + "a" + + const error = yield* Stream.make( + encoder.encode(fileStart), + encoder.encode("bbbb") + ).pipe( + Stream.pipeThroughChannel( + Multipart.makeChannel({ "content-type": `multipart/form-data; boundary=${boundary}` }) + ), + Stream.mapEffect((part) => { + if (part._tag !== "File") { + return Effect.void + } + fileParts++ + return Stream.runDrain(part.content) + }), + Stream.runDrain, + Effect.flip + ) + + strictEqual(fileParts, 1) + strictEqual(error._tag, "MultipartError") + strictEqual(error.reason._tag, "Parse") + })) + + it.each<{ + description: string + options: { + readonly maxParts?: number + readonly maxFieldSize?: number + readonly maxPartSize?: number + } + limit: "MaxParts" | "MaxFieldSize" | "MaxPartSize" + expectedFields: Array + }>([ + { + description: "maxParts", + options: { maxParts: 2 }, + limit: "MaxParts", + expectedFields: ["a", "b"] + }, + { + description: "maxFieldSize", + options: { maxFieldSize: 1 }, + limit: "MaxFieldSize", + expectedFields: [] + }, + { + description: "maxPartSize", + options: { maxPartSize: 1 }, + limit: "MaxPartSize", + expectedFields: [] + } + ])("stops delivering fields when $description is exceeded", ({ expectedFields, limit, options }) => { + const boundary = "----testboundary" + const encoder = new TextEncoder() + const fields: Array = [] + const errors: Array = [] + const parser = MultipartParser.make({ + headers: { "content-type": `multipart/form-data; boundary=${boundary}` }, + ...options, + onField(info) { + fields.push(info.name) + }, + onFile: () => () => {}, + onError(error) { + errors.push(error) + }, + onDone() {} + }) + const part = (name: string) => `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\nvalue\r\n` + + parser.write(encoder.encode(part("a") + part("b") + part("c") + part("d") + `--${boundary}--\r\n`)) + parser.end() + + deepStrictEqual(errors, [{ _tag: "ReachedLimit", limit }]) + deepStrictEqual(fields, expectedFields) + }) + + it("handles the final boundary delimiter split between the trailing hyphens", () => { + const boundary = "----testboundary" + const encoder = new TextEncoder() + const decoder = new TextDecoder() + const errors: Array = [] + const fields: Array = [] + let done = false + const parser = MultipartParser.make({ + headers: { "content-type": `multipart/form-data; boundary=${boundary}` }, + onField(info, value) { + fields.push([info.name, decoder.decode(value)]) + }, + onFile: () => () => {}, + onError(error) { + errors.push(error) + }, + onDone() { + done = true + } + }) + const body = `--${boundary}\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue` + + parser.write(encoder.encode(`${body}\r\n--${boundary}-`)) + parser.write(encoder.encode("-\r\n")) + strictEqual(done, true) + parser.end() + + deepStrictEqual(fields, [["field", "value"]]) + deepStrictEqual(errors, []) + }) + + it.effect("returns distinct persisted file paths for files with the same client filename", () => + Effect.gen(function*() { + const formData = new FormData() + formData.append("first", new File(["one"], "same.txt")) + formData.append("second", new File(["two"], "same.txt")) + const request = HttpServerRequest.fromClientRequest( + HttpClientRequest.bodyFormData(HttpClientRequest.post("https://example.com"), formData) + ) + const writes: Array = [] + const persisted = yield* Multipart.toPersisted( + request.multipartStream, + (path) => Effect.sync(() => writes.push(path)) + ).pipe( + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + makeTempDirectoryScoped: () => Effect.succeed("/tmp/audit") + }) + ), + Effect.provide(Path.layer) + ) + const first = (persisted.first as Array)[0] + const second = (persisted.second as Array)[0] + strictEqual(first.path, "/tmp/audit/same.txt") + notStrictEqual(first.path, second.path) + deepStrictEqual(writes, [first.path, second.path]) + })) + it.effect("responds based on the reason and is ignored by the ErrorReporter", () => Effect.gen(function*() { const cases = [ diff --git a/.context/effect/packages/effect/test/unstable/http/SchemaRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/http/SchemaRepresentation.test.ts new file mode 100644 index 000000000..089fcf8e8 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/SchemaRepresentation.test.ts @@ -0,0 +1,49 @@ +import { assert, describe, it } from "@effect/vitest" +import { SchemaRepresentation } from "effect" +import { Cookies, Headers, UrlParams } from "effect/unstable/http" + +describe("HTTP schema representations", () => { + it("generates code for declaration schemas", () => { + const document = SchemaRepresentation.toRepresentations([ + Headers.HeadersSchema.ast, + Cookies.CookiesSchema.ast, + Cookies.CookieSchema.ast, + UrlParams.UrlParamsSchema.ast + ]) + + assert.deepStrictEqual( + document.representations.map((representation) => + representation._tag === "Declaration" ? representation.representation : undefined + ), + [ + { id: "effect/http/Headers", payload: null }, + { id: "effect/http/Cookies", payload: null }, + { id: "effect/http/Cookie", payload: null }, + { id: "effect/http/UrlParams", payload: null } + ] + ) + + const output = SchemaRepresentation.toCodeDocument(document) + + assert.deepStrictEqual(output.codes, [ + { runtime: `Headers.HeadersSchema.annotate({ "expected": "Headers" })`, Type: "Headers.Headers" }, + { runtime: `Cookies.CookiesSchema.annotate({ "expected": "Cookies" })`, Type: "Cookies.Cookies" }, + { runtime: `Cookies.CookieSchema.annotate({ "expected": "Cookie" })`, Type: "Cookies.Cookie" }, + { runtime: `UrlParams.UrlParamsSchema.annotate({ "expected": "UrlParams" })`, Type: "UrlParams.UrlParams" } + ]) + assert.deepStrictEqual(output.artifacts, [ + { + _tag: "Import", + importDeclaration: `import * as Headers from "effect/unstable/http/Headers"` + }, + { + _tag: "Import", + importDeclaration: `import * as Cookies from "effect/unstable/http/Cookies"` + }, + { + _tag: "Import", + importDeclaration: `import * as UrlParams from "effect/unstable/http/UrlParams"` + } + ]) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/http/Template.test.ts b/.context/effect/packages/effect/test/unstable/http/Template.test.ts new file mode 100644 index 000000000..bbe76e7bb --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/http/Template.test.ts @@ -0,0 +1,38 @@ +import { assert, describe, it } from "@effect/vitest" +import { Deferred, Effect, Fiber, Stream } from "effect" +import { TestClock } from "effect/testing" +import { Template } from "effect/unstable/http" + +describe("Template", () => { + it.effect("preserves template segment order", () => + Effect.gen(function*() { + const fiber = yield* Stream.runCollect( + Template.stream`a${Effect.delay(Effect.succeed("slow"), "1 second")}b${"fast"}c` + ).pipe(Effect.forkChild) + yield* Effect.yieldNow + yield* TestClock.adjust("1 second") + const chunks = yield* Fiber.join(fiber) + assert.strictEqual(chunks.join(""), "aslowbfastc") + })) + + it.effect("evaluates effect interpolations concurrently", () => + Effect.gen(function*() { + const firstStarted = yield* Deferred.make() + const secondStarted = yield* Deferred.make() + const releaseFirst = yield* Deferred.make() + const fiber = yield* Template.stream`${ + Deferred.succeed(firstStarted, void 0).pipe( + Effect.andThen(Deferred.await(releaseFirst)), + Effect.as("first") + ) + }${Deferred.succeed(secondStarted, void 0).pipe(Effect.as("second"))}`.pipe( + Stream.runCollect, + Effect.forkChild + ) + yield* Deferred.await(firstStarted) + yield* Effect.yieldNow + assert.isTrue(yield* Deferred.isDone(secondStarted)) + yield* Deferred.succeed(releaseFirst, void 0) + assert.deepStrictEqual(yield* Fiber.join(fiber), ["first", "second"]) + })) +}) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApi.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApi.test.ts index 815e9bb3c..d5a77468f 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApi.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApi.test.ts @@ -51,6 +51,17 @@ describe("HttpApi", () => { assert.strictEqual(Context.getUnsafe(parent.groups.users.annotations, OpenApi.Description), "Child API") }) + it("does not add inherited groups", () => { + const inherited = HttpApiGroup.make("inherited") + const child = HttpApi.make("Child").add(HttpApiGroup.make("own")) + Object.setPrototypeOf(child.groups, { inherited }) + + const parent = HttpApi.make("Parent").addHttpApi(child) + + assert.isTrue(Object.hasOwn(parent.groups, "own")) + assert.isFalse(Object.hasOwn(parent.groups, "inherited")) + }) + it("keeps annotations from API variants isolated", () => { const group = HttpApiGroup.make("users").annotate(OpenApi.Title, "Users") const child = HttpApi.make("Child").add(group) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts index dd25322ed..5dedefe87 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiBuilder.test.ts @@ -1,5 +1,16 @@ import { assert, it, vi } from "@effect/vitest" -import { Cause, Effect, FileSystem, Layer, Path, Redacted, Schema, Stream } from "effect" +import { + Cause, + DateTime, + Effect, + FileSystem, + Layer, + Path, + Redacted, + Schema, + SchemaTransformation, + Stream +} from "effect" import { Etag, HttpPlatform } from "effect/unstable/http" import { HttpApi, @@ -22,6 +33,32 @@ const TestServices = Layer.mergeAll( HttpPlatform.layer ).pipe(Layer.provideMerge(FileSystem.layerNoop({}))) +it.layer(TestServices)("HttpApiBuilder query parameters", (it) => { + it.effect("round trips array query parameters with one or more values", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("list", "/list", { + query: { ids: Schema.Array(Schema.String) }, + success: Schema.Struct({ ids: Schema.Array(Schema.String) }) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("list", ({ query }) => Effect.succeed(query)) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const multiple = yield* client.test.list({ query: { ids: ["a", "b"] } }) + const single = yield* client.test.list({ query: { ids: ["a"] } }) + + assert.deepStrictEqual(multiple, { ids: ["a", "b"] }) + assert.deepStrictEqual(single, { ids: ["a"] }) + })) +}) + it.effect("reuses response schema transformations by source AST", () => { const SharedSuccess = Schema.String.pipe(HttpApiSchema.asText()) const DistinctSuccess = Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/custom" })) @@ -122,6 +159,636 @@ it.layer(TestServices)("HttpApiBuilder payload content types", (it) => { })) }) +it.layer(TestServices)("HttpApiBuilder WithHeaders responses", (it) => { + it.effect("encodes WithHeaders using the schema for the response status", () => + Effect.gen(function*() { + const encodedHeader = (prefix: string) => + Schema.String.pipe( + Schema.decodeTo( + Schema.String, + SchemaTransformation.transform({ + decode: (value) => value, + encode: (value) => `${prefix}:${value}` + }) + ) + ) + const Ok = HttpApiSchema.WithHeaders( + Schema.TaggedStruct("Ok", {}), + { "x-source": encodedHeader("ok") } + ) + const Created = HttpApiSchema.WithHeaders( + Schema.TaggedStruct("Created", {}).pipe(HttpApiSchema.status(201)), + { "x-source": encodedHeader("created") } + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { success: [Ok, Created] }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("result", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { _tag: "Created" as const }, + headers: { "x-source": "value" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.result({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 201) + assert.strictEqual(response.headers["x-source"], "created:value") + })) + + it.effect("encodes a branded response against the header-carrying member when body shapes overlap", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { + success: [ + Schema.Struct({ value: Schema.String }).pipe(HttpApiSchema.status(201)), + HttpApiSchema.WithHeaders( + Schema.Struct({ value: Schema.String }), + { "x-source": Schema.String } + ) + ] + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("result", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { value: "wrapped" }, + headers: { "x-source": "value" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.result({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers["x-source"], "value") + })) + + it.effect("fails with a Body schema error when a branded response body matches no header-carrying member", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { + success: [ + HttpApiSchema.WithHeaders( + Schema.TaggedStruct("A", {}), + { "x-source": Schema.String } + ), + Schema.TaggedStruct("B", {}).pipe(HttpApiSchema.status(201)) + ] + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("result", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { _tag: "B" as const }, + headers: { "x-source": "value" } + }) as any)) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const exit = yield* Effect.exit(client.test.result({ responseMode: "response-only" })) + + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause) as any + assert.strictEqual(error._tag, "HttpApiSchemaError") + assert.strictEqual(error.kind, "Body") + } + })) + + it.effect("round trips user-managed header codecs through HttpApiTest", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { + disableCodecs: true, + success: HttpApiSchema.WithHeaders( + Schema.String.pipe(HttpApiSchema.asText()), + { "x-count": Schema.FiniteFromString } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("result", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: "ok", + headers: { "x-count": 2 } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const result = yield* client.test.result({}) + + assert.deepStrictEqual( + result, + HttpApiSchema.withHeaders({ body: "ok", headers: { "x-count": 2 } }) + ) + })) + + it.effect("passes through string-shaped WithHeaders when endpoint codecs are disabled", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test") + .add( + HttpApiEndpoint.get("success", "/success", { + disableCodecs: true, + success: HttpApiSchema.WithHeaders( + Schema.String.pipe(HttpApiSchema.asText()), + { "x-count": Schema.String } + ) + }) + ) + .add( + HttpApiEndpoint.get("error", "/error", { + disableCodecs: true, + error: HttpApiSchema.WithHeaders( + Schema.String.pipe(HttpApiSchema.status(429), HttpApiSchema.asText()), + { "retry-after": Schema.String } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers + .handle("success", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: "ok", + headers: { "x-count": "2" } + }))) + .handle("error", () => + Effect.fail(HttpApiSchema.withHeaders({ + body: "slow down", + headers: { "retry-after": "30" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const [success, successResponse] = yield* client.test.success({ responseMode: "decoded-and-response" }) + const error = yield* Effect.flip(client.test.error({})) + const errorResponse = yield* client.test.error({ responseMode: "response-only" }) + if (!HttpApiSchema.isWithHeadersValue(error)) { + throw new Error("Expected WithHeaders error") + } + + assert.strictEqual(successResponse.headers["x-count"], "2") + assert.deepStrictEqual(success.headers, { "x-count": "2" }) + assert.strictEqual(errorResponse.headers["retry-after"], "30") + assert.deepStrictEqual(error.headers, { "retry-after": "30" }) + })) + + it.effect("encodes and decodes response headers with codecs enabled", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("created", "/test", { + success: HttpApiSchema.WithHeaders( + Schema.Struct({ id: Schema.Int }).pipe(HttpApiSchema.status(201)), + { "x-count": Schema.Int } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("created", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { id: 1 }, + headers: { "x-count": 2 } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const [value, response] = yield* client.test.created({ responseMode: "decoded-and-response" }) + + assert.strictEqual(response.headers["x-count"], "2") + assert.deepStrictEqual( + value, + HttpApiSchema.withHeaders({ + body: { id: 1 }, + headers: { "x-count": 2 } + }) + ) + })) + + it.effect("decodes a transforming buffered success body and its declared headers", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("created", "/test", { + success: HttpApiSchema.WithHeaders(Schema.Date, { "x-source": Schema.String }) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("created", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: DateTime.makeUnsafe("2024-01-02T03:04:05.000Z").pipe(DateTime.toDate), + headers: { "x-source": "schema" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const value = yield* client.test.created({}) + + assert.strictEqual(value.body.toISOString(), "2024-01-02T03:04:05.000Z") + assert.deepStrictEqual(value.headers, { "x-source": "schema" }) + })) + + it.effect("round trips a client-received branded value through another handler", () => + Effect.gen(function*() { + const Success = HttpApiSchema.WithHeaders( + Schema.Struct({ id: Schema.Int }).pipe(HttpApiSchema.status(201)), + { "x-count": Schema.Int } + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test") + .add(HttpApiEndpoint.get("created", "/created", { success: Success })) + .add(HttpApiEndpoint.get("forwarded", "/forwarded", { success: Success })) + ) + let forwarded: typeof Success.Type | undefined + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers + .handle("created", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { id: 1 }, + headers: { "x-count": 2 } + }))) + .handle("forwarded", () => Effect.succeed(forwarded!)) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const [value, response] = yield* client.test.created({ responseMode: "decoded-and-response" }) + forwarded = value + const result = yield* client.test.forwarded({}) + + assert.strictEqual(response.status, 201) + assert.deepStrictEqual(result, value) + })) + + it.effect("encodes a buffered success body and its declared headers", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("created", "/test", { + success: HttpApiSchema.WithHeaders( + Schema.Struct({ id: Schema.Int }).pipe(HttpApiSchema.status(201)), + { + "x-count": Schema.Int, + "x-optional": Schema.optional(Schema.String) + } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("created", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { id: 1 }, + headers: { "x-count": 2, "x-optional": undefined } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.created({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 201) + assert.strictEqual(response.headers["x-count"], "2") + assert.isFalse("x-optional" in response.headers) + assert.deepStrictEqual(yield* response.json, { id: 1 }) + })) + + it.effect("does not unwrap an unbranded value in a mixed success union", () => + Effect.gen(function*() { + const Plain = Schema.Struct({ + body: Schema.String, + headers: Schema.Struct({ "x-source": Schema.String }) + }).pipe(HttpApiSchema.asJson({ contentType: "application/vnd.plain+json" })) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("mixed", "/test", { + success: [ + HttpApiSchema.WithHeaders(Schema.String, { "x-source": Schema.String }), + Plain + ] + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("mixed", () => Effect.succeed({ body: "plain", headers: { "x-source": "body" } })) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.mixed({ responseMode: "response-only" }) + + assert.strictEqual(response.headers["content-type"], "application/vnd.plain+json") + assert.isFalse("x-source" in response.headers) + assert.deepStrictEqual(yield* response.json, { body: "plain", headers: { "x-source": "body" } }) + })) + + it.effect("decodes a plain value in a mixed WithHeaders success union", () => + Effect.gen(function*() { + const Wrapped = HttpApiSchema.WithHeaders( + Schema.TaggedStruct("Wrapped", { value: Schema.String }), + { "x-source": Schema.String } + ) + const Plain = Schema.TaggedStruct("Plain", { value: Schema.String }).pipe( + HttpApiSchema.asJson({ contentType: "application/vnd.plain+json" }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("mixed", "/test", { success: [Wrapped, Plain] }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("mixed", () => Effect.succeed({ _tag: "Plain" as const, value: "plain" })) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const value = yield* client.test.mixed({}) + + assert.deepStrictEqual(value, { _tag: "Plain", value: "plain" }) + })) + + it.effect("applies declared headers after body encoding", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("override", "/test", { + success: HttpApiSchema.WithHeaders( + Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/plain" })), + { "content-type": Schema.String, "x-source": Schema.String } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("override", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: "ok", + headers: { "content-type": "application/custom", "x-source": "schema" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.override({ responseMode: "response-only" }) + + assert.strictEqual(response.headers["content-type"], "application/custom") + assert.strictEqual(response.headers["x-source"], "schema") + assert.strictEqual(yield* response.text, "ok") + })) + + it.effect("encodes error headers through encodeToWithHeaders", () => + Effect.gen(function*() { + class RateLimited extends Schema.TaggedError()("RateLimited", { + retryAfter: Schema.Int + }) {} + const RateLimitedResponse = RateLimited.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.status(429), HttpApiSchema.asText()), + headers: { "retry-after": Schema.Int } + }, { + decode: ({ headers }) => new RateLimited({ retryAfter: headers["retry-after"] }), + encode: (error) => ({ body: "slow down", headers: { "retry-after": error.retryAfter } }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("limited", "/test", { error: RateLimitedResponse }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("limited", () => Effect.fail(new RateLimited({ retryAfter: 30 }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.limited({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 429) + assert.strictEqual(response.headers["content-type"], "text/plain") + assert.strictEqual(response.headers["retry-after"], "30") + assert.strictEqual(yield* response.text, "slow down") + })) + + it.effect("encodes annotation headers from their declared encoded shape", () => + Effect.gen(function*() { + class RateLimited extends Schema.TaggedError()("RateLimited", { + expiresAt: Schema.Date + }) {} + const RateLimitedResponse = RateLimited.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.status(429)), + headers: { "expires-at": Schema.Date } + }, { + decode: ({ headers }) => new RateLimited({ expiresAt: headers["expires-at"] }), + encode: (error) => ({ body: "slow down", headers: { "expires-at": error.expiresAt } }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("limited", "/test", { error: RateLimitedResponse }) + ) + ) + const expiresAt = DateTime.makeUnsafe("2026-08-05T02:00:00.000Z").pipe(DateTime.toDate) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("limited", () => Effect.fail(new RateLimited({ expiresAt }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.limited({ responseMode: "response-only" }) + const error = yield* Effect.flip(client.test.limited({})) + + assert.strictEqual(response.headers["expires-at"], "2026-08-05T02:00:00.000Z") + assert.deepStrictEqual(error, new RateLimited({ expiresAt })) + })) + + it.effect("encodes and decodes an error wrapped in WithHeaders", () => + Effect.gen(function*() { + const RateLimited = HttpApiSchema.WithHeaders( + Schema.TaggedStruct("RateLimited", { message: Schema.String }).pipe(HttpApiSchema.status(429)), + { "retry-after": Schema.Int } + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("limited", "/test", { error: RateLimited }) + ) + ) + const expected = HttpApiSchema.withHeaders({ + body: { _tag: "RateLimited" as const, message: "slow down" }, + headers: { "retry-after": 30 } + }) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("limited", () => Effect.fail(expected)) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.limited({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 429) + assert.strictEqual(response.headers["retry-after"], "30") + assert.deepStrictEqual(yield* response.json, expected.body) + assert.deepStrictEqual(yield* Effect.flip(client.test.limited({})), expected) + })) + + it.effect("passes through string-shaped encodeToWithHeaders values when endpoint codecs are disabled", () => + Effect.gen(function*() { + class RateLimited extends Schema.TaggedError()("RateLimited", { + retryAfter: Schema.String + }) {} + const RateLimitedResponse = RateLimited.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.status(429), HttpApiSchema.asText()), + headers: { "retry-after": Schema.String } + }, { + decode: ({ headers }) => new RateLimited({ retryAfter: headers["retry-after"] }), + encode: (error) => ({ body: "slow down", headers: { "retry-after": error.retryAfter } }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("limited", "/test", { + disableCodecs: true, + error: RateLimitedResponse + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("limited", () => Effect.fail(new RateLimited({ retryAfter: "30" }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.limited({ responseMode: "response-only" }) + + assert.strictEqual(response.status, 429) + assert.strictEqual(response.headers["retry-after"], "30") + assert.strictEqual(yield* response.text, "slow down") + assert.deepStrictEqual( + yield* Effect.flip(client.test.limited({})), + new RateLimited({ retryAfter: "30" }) + ) + })) + + it.effect("decodes an error body and headers through encodeToWithHeaders", () => + Effect.gen(function*() { + class RateLimited extends Schema.TaggedError()("RateLimited", { + message: Schema.String, + retryAfter: Schema.Int + }) {} + const RateLimitedResponse = RateLimited.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.status(429), HttpApiSchema.asText()), + headers: { "retry-after": Schema.Int } + }, { + decode: ({ body, headers }) => + new RateLimited({ + message: body, + retryAfter: headers["retry-after"] + }), + encode: (error) => ({ + body: error.message, + headers: { "retry-after": error.retryAfter } + }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("limited", "/test", { error: RateLimitedResponse }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("limited", () => Effect.fail(new RateLimited({ message: "slow down", retryAfter: 30 }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const error = yield* Effect.flip(client.test.limited({})) + + assert.deepStrictEqual(error, new RateLimited({ message: "slow down", retryAfter: 30 })) + })) + + it.effect("defects with ResponseHeaders when success header encoding fails", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("invalid", "/test", { + success: HttpApiSchema.WithHeaders(Schema.String, { "x-count": Schema.Int }) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("invalid", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: "ok", + headers: { "x-count": "invalid" as any } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const exit = yield* Effect.exit(client.test.invalid({ responseMode: "response-only" })) + + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause) as any + assert.strictEqual(error._tag, "HttpApiSchemaError") + assert.strictEqual(error.kind, "ResponseHeaders") + } + })) +}) + it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { it.effect("emits StreamUint8Array handler responses as streamed bytes with the declared content type", () => Effect.gen(function*() { @@ -154,6 +821,41 @@ it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { assert.deepStrictEqual(Array.from(chunks, (chunk) => Array.from(chunk)), [[1, 2], [3]]) })) + it.effect("unwraps WithHeaders StreamUint8Array responses before stream dispatch", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("download", "/test", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.status(206)( + HttpApiSchema.StreamUint8Array({ contentType: "application/custom-bytes" }) + ), + { "content-type": Schema.String, "x-count": Schema.Int } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("download", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: Stream.make(new Uint8Array([1, 2]), new Uint8Array([3])), + headers: { "content-type": "application/overridden", "x-count": 2 } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.download({ responseMode: "response-only" }) + const chunks = yield* response.stream.pipe(Stream.runCollect) + + assert.strictEqual(response.status, 206) + assert.strictEqual(response.headers["content-type"], "application/overridden") + assert.strictEqual(response.headers["x-count"], "2") + assert.deepStrictEqual(Array.from(chunks, (chunk) => Array.from(chunk)), [[1, 2], [3]]) + })) + it.effect("renders successful StreamSse events incrementally with the declared content type", () => Effect.gen(function*() { const Events = Schema.Struct({ @@ -197,6 +899,85 @@ it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { ) })) + it.effect("unwraps WithHeaders StreamSse responses before SSE encoding", () => + Effect.gen(function*() { + const Events = Schema.Struct({ + event: Schema.String, + data: Schema.String + }) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("events", "/test", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.status(202)( + HttpApiSchema.StreamSse({ + contentType: "text/event-stream; charset=utf-8", + events: Events, + error: StreamError + }) + ), + { "x-count": Schema.Int } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("events", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: Stream.make({ event: "first", data: "one" }), + headers: { "x-count": 1 } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.events({ responseMode: "response-only" }) + const chunks = yield* response.stream.pipe(Stream.runCollect) + + assert.strictEqual(response.status, 202) + assert.strictEqual(response.headers["content-type"], "text/event-stream; charset=utf-8") + assert.strictEqual(response.headers["x-count"], "1") + assert.deepStrictEqual(Array.from(chunks, (chunk) => textDecoder.decode(chunk)), [ + "event: first\ndata: one\n\n" + ]) + })) + + it.effect("fails WithHeaders stream header encoding before returning a response", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("download", "/test", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamUint8Array(), + { "x-count": Schema.Int } + ) + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("download", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: Stream.make(new Uint8Array([1])), + headers: { "x-count": "invalid" as any } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const exit = yield* Effect.exit(client.test.download({ responseMode: "response-only" })) + + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + const error = Cause.squash(exit.cause) as any + assert.strictEqual(error._tag, "HttpApiSchemaError") + assert.strictEqual(error.kind, "ResponseHeaders") + } + })) + it.effect("renders StreamSse failures as one reserved event containing an encoded full cause", () => Effect.gen(function*() { const Events = Schema.Struct({ @@ -286,6 +1067,67 @@ it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { ]) })) + it.effect("keeps a plain buffered alternative when the stream is wrapped", () => + Effect.gen(function*() { + const Buffered = Schema.Struct({ message: Schema.String }) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { + success: [ + HttpApiSchema.WithHeaders( + HttpApiSchema.StreamUint8Array(), + { "x-source": Schema.String } + ), + Buffered + ] + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => handlers.handle("result", () => Effect.succeed({ message: "done" })) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.result({ responseMode: "response-only" }) + + assert.strictEqual(response.headers["content-type"], "application/json") + assert.deepStrictEqual(yield* response.json, { message: "done" }) + })) + + it.effect("detects a wrapped buffered alternative alongside a plain stream", () => + Effect.gen(function*() { + const Buffered = Schema.Struct({ message: Schema.String }) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("result", "/test", { + success: [ + HttpApiSchema.StreamUint8Array(), + HttpApiSchema.WithHeaders(Buffered, { "x-source": Schema.String }) + ] + }) + ) + ) + const GroupLive = HttpApiBuilder.group( + Api, + "test", + (handlers) => + handlers.handle("result", () => + Effect.succeed(HttpApiSchema.withHeaders({ + body: { message: "done" }, + headers: { "x-source": "buffered" } + }))) + ) + + const client = yield* HttpApiTest.groups(Api, ["test"]).pipe(Effect.provide(GroupLive)) + const response = yield* client.test.result({ responseMode: "response-only" }) + + assert.strictEqual(response.headers["content-type"], "application/json") + assert.strictEqual(response.headers["x-source"], "buffered") + assert.deepStrictEqual(yield* response.json, { message: "done" }) + })) + it.effect("registers handleAll handlers at runtime", () => Effect.gen(function*() { const User = Schema.Struct({ @@ -475,10 +1317,11 @@ it.layer(TestServices)("HttpApiBuilder streaming success responses", (it) => { it.effect("does not try another security scheme after the handler fails", () => Effect.gen(function*() { - class HandlerFailure extends Schema.TaggedErrorClass()("HandlerFailure", { + class HandlerFailure extends Schema.TaggedError()("HandlerFailure", { message: Schema.String }, { httpApiStatus: 418 }) {} + // @effect-diagnostics-next-line leakingRequirements:off class M extends HttpApiMiddleware.Service()("Security/HandlerFailure", { error: Schema.String.pipe( HttpApiSchema.status(401), diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiClient.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiClient.test.ts index fe5bcb8b7..dbbf7085a 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiClient.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiClient.test.ts @@ -110,6 +110,138 @@ describe("HttpApiClient", () => { assert.deepStrictEqual(first.map((chunk) => Array.from(chunk)), [[1, 2]]) })) + it.effect("decodes WithHeaders StreamUint8Array bodies and headers", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("download", "/download", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamUint8Array(), + { "x-count": Schema.Int } + ) + }) + ) + ) + const client = yield* HttpApiClient.makeWith(Api, { + baseUrl: "http://test", + httpClient: clientFromResponse(() => + new Response(byteStream([new Uint8Array([1, 2]), new Uint8Array([3])]), { + status: 200, + headers: { "x-count": "2" } + }) + ) + }) + + const value = yield* client.test.download({}) + const first = yield* value.body.pipe(Stream.take(1), Stream.runCollect) + + assert.deepStrictEqual(value.headers, { "x-count": 2 }) + assert.deepStrictEqual(first.map((chunk) => Array.from(chunk)), [[1, 2]]) + })) + + it.effect("decodes WithHeaders StreamSse bodies and headers", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("events", "/events", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamSse({ + data: Schema.Struct({ text: Schema.String }), + error: StreamError + }), + { "x-count": Schema.Int } + ) + }) + ) + ) + const client = yield* HttpApiClient.makeWith(Api, { + baseUrl: "http://test", + httpClient: clientFromResponse(() => + new Response(textStream([`data: {"text":"hello"}\n\n`]), { + status: 200, + headers: { + "content-type": "text/event-stream", + "x-count": "1" + } + }) + ) + }) + + const value = yield* client.test.events({}) + const events = yield* Stream.runCollect(value.body) + + assert.deepStrictEqual(value.headers, { "x-count": 1 }) + assert.deepStrictEqual(events, [{ text: "hello" }]) + })) + + it.effect("fails invalid WithHeaders stream headers before returning the body", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("download", "/download", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamUint8Array(), + { "x-count": Schema.Int } + ) + }) + ) + ) + const client = yield* HttpApiClient.makeWith(Api, { + baseUrl: "http://test", + httpClient: clientFromResponse(() => + new Response(byteStream([new Uint8Array([1])]), { + status: 200, + headers: { "x-count": "invalid" } + }) + ) + }) + + const exit = yield* Effect.exit(client.test.download({})) + + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assert.strictEqual((Cause.squash(exit.cause) as { readonly _tag?: string })._tag, "SchemaError") + } + })) + + it.effect("selects a WithHeaders stream from a mixed buffered success by content type", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("chat", "/chat", { + success: [ + Schema.Struct({ message: Schema.String }), + HttpApiSchema.WithHeaders( + HttpApiSchema.StreamSse({ data: Schema.Struct({ text: Schema.String }) }), + { "x-count": Schema.Int } + ) + ] + }) + ) + ) + const client = yield* HttpApiClient.makeWith(Api, { + baseUrl: "http://test", + httpClient: clientFromResponse(() => + new Response(textStream([`data: {"text":"hello"}\n\n`]), { + status: 200, + headers: { + "content-type": "text/event-stream; charset=utf-8", + "x-count": "1" + } + }) + ) + }) + + const value = yield* client.test.chat({}) + if (!(HttpApiSchema.WithHeadersValueTypeId in value)) { + throw new Error("Expected WithHeaders response") + } + const events = yield* Stream.runCollect(value.body) + + assert.deepStrictEqual(value.headers, { "x-count": 1 }) + assert.deepStrictEqual(events, [{ text: "hello" }]) + })) + it.effect("decodes StreamSse successes at the annotated status", () => Effect.gen(function*() { const client = yield* HttpApiClient.makeWith(AnnotatedStreamingApi, { @@ -291,6 +423,48 @@ describe("HttpApiClient", () => { })) }) + describe("response headers", () => { + it.effect("fails response decoding when a declared header is invalid", () => + Effect.gen(function*() { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("created", "/created", { + success: HttpApiSchema.WithHeaders( + Schema.Struct({ id: Schema.Int }), + { "x-count": Schema.Int } + ) + }) + ) + ) + const decodeFailure = Effect.fnUntraced(function*(body: unknown, count: string) { + const client = yield* HttpApiClient.makeWith(Api, { + baseUrl: "http://test", + httpClient: clientFromResponse(() => + new Response(JSON.stringify(body), { + status: 200, + headers: { + "content-type": "application/json", + "x-count": count + } + }) + ) + }) + const exit = yield* Effect.exit(client.test.created({})) + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Success") { + throw new Error("Expected response decoding to fail") + } + return Cause.squash(exit.cause) as { readonly _tag?: string } + }) + + const bodyError = yield* decodeFailure({ id: "invalid" }, "1") + const headerError = yield* decodeFailure({ id: 1 }, "invalid") + + assert.strictEqual(bodyError._tag, "SchemaError") + assert.strictEqual(headerError._tag, bodyError._tag) + })) + }) + describe("urlBuilder", () => { const Api = HttpApi.make("Api") .add( @@ -552,7 +726,7 @@ const Events = Schema.Struct({ data: Schema.String }) -class EndpointError extends Schema.TaggedErrorClass()("EndpointError", { +class EndpointError extends Schema.TaggedError()("EndpointError", { message: Schema.String }, { httpApiStatus: 400 }) {} diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiEndpoint.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiEndpoint.test.ts index 73e09ca4e..eeedb9d18 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiEndpoint.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiEndpoint.test.ts @@ -178,17 +178,16 @@ describe("HttpApiEndpoint streaming success schemas", () => { ) }) - it("two streaming successes for distinct statuses are allowed", () => { + it("two streaming successes for distinct statuses throw", () => { const stream = HttpApiSchema.status(206)(sse()) const bytes = HttpApiSchema.status(200)( HttpApiSchema.StreamUint8Array({ contentType: "application/custom-stream" }) ) - const endpoint = HttpApiEndpoint.get("events", "/events", { - success: [stream, bytes] - }) - assert.isTrue(endpoint.success.has(stream)) - assert.isTrue(endpoint.success.has(bytes)) + assert.throws( + () => HttpApiEndpoint.get("events", "/events", { success: [stream, bytes] }), + "Multiple streaming success responses are not supported" + ) }) it("statically detectable SSE reserved failure event name throws", () => { @@ -207,3 +206,265 @@ describe("HttpApiEndpoint streaming success schemas", () => { ) }) }) + +describe("HttpApiEndpoint WithHeaders schemas", () => { + it("rejects a WithHeaders success sharing its status and content type", () => { + assert.throws( + () => + HttpApiEndpoint.get("get", "/", { + success: [ + Schema.Struct({ plain: Schema.String }), + HttpApiSchema.WithHeaders( + Schema.Struct({ wrapped: Schema.String }), + { "x-trace-id": Schema.String } + ) + ] + }), + "Cannot combine a response with headers with another response for status 200 and content-type: application/json" + ) + }) + + it("rejects an encodeToWithHeaders error sharing its status and content type", () => { + const ErrorWithHeaders = Schema.String.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.Struct({ wrapped: Schema.String }), + headers: { "x-trace-id": Schema.String } + }, { + decode: ({ body }) => body.wrapped, + encode: (message) => ({ body: { wrapped: message }, headers: { "x-trace-id": "trace" } }) + }) + ) + + assert.throws( + () => + HttpApiEndpoint.get("get", "/", { + error: [ + Schema.Struct({ plain: Schema.String }), + ErrorWithHeaders + ] + }), + "Cannot combine a response with headers with another response for status 500 and content-type: application/json" + ) + }) + + it("rejects two WithHeaders successes sharing a status across content types", () => { + assert.throws( + () => + HttpApiEndpoint.get("get", "/", { + success: [ + HttpApiSchema.WithHeaders( + Schema.Struct({ value: Schema.String }), + { "x-json": Schema.String } + ), + HttpApiSchema.WithHeaders( + Schema.String.pipe(HttpApiSchema.asText()), + { "x-text": Schema.String } + ) + ] + }), + "Cannot declare multiple responses with headers for status 200" + ) + }) + + it("rejects two encodeToWithHeaders errors sharing a status", () => { + const JsonError = Schema.String.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.Struct({ message: Schema.String }), + headers: { "x-json": Schema.String } + }, { + decode: ({ body }) => body.message, + encode: (message) => ({ body: { message }, headers: { "x-json": "json" } }) + }) + ) + const TextError = Schema.String.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.asText()), + headers: { "x-text": Schema.String } + }, { + decode: ({ body }) => body, + encode: (message) => ({ body: message, headers: { "x-text": "text" } }) + }) + ) + + assert.throws( + () => HttpApiEndpoint.get("get", "/", { error: [JsonError, TextError] }), + "Cannot declare multiple responses with headers for status 500" + ) + }) + + it("rejects mixed WithHeaders and encodeToWithHeaders responses sharing a status", () => { + const AnnotatedError = Schema.String.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.status(429), HttpApiSchema.asText()), + headers: { "retry-after": Schema.String } + }, { + decode: ({ body }) => body, + encode: (message) => ({ body: message, headers: { "retry-after": "30" } }) + }) + ) + + assert.throws( + () => + HttpApiEndpoint.get("get", "/", { + error: [ + HttpApiSchema.WithHeaders( + Schema.Struct({ message: Schema.String }).pipe(HttpApiSchema.status(429)), + { "x-trace-id": Schema.String } + ), + AnnotatedError + ] + }), + "Cannot declare multiple responses with headers for status 429" + ) + }) + + it("allows one response with headers and a plain response for the same status across content types", () => { + const endpoint = HttpApiEndpoint.get("get", "/", { + success: [ + HttpApiSchema.WithHeaders( + Schema.Struct({ value: Schema.String }), + { "x-trace-id": Schema.String } + ), + Schema.String.pipe(HttpApiSchema.asText()) + ] + }) + + assert.strictEqual(endpoint.success.size, 2) + }) + + it("allows responses with headers on distinct statuses", () => { + const endpoint = HttpApiEndpoint.get("get", "/", { + success: [ + HttpApiSchema.WithHeaders( + Schema.Struct({ value: Schema.String }), + { "x-trace-id": Schema.String } + ), + HttpApiSchema.WithHeaders( + Schema.Struct({ created: Schema.Boolean }).pipe(HttpApiSchema.status(201)), + { location: Schema.String } + ) + ] + }) + + assert.strictEqual(endpoint.success.size, 2) + }) + + it("keeps the wrapper in the success set with codec-transformed parts", () => { + const endpoint = HttpApiEndpoint.get("list", "/users", { + success: HttpApiSchema.WithHeaders(Schema.Struct({ a: Schema.String }), { + "x-count": Schema.Int + }) + }) + + const [schema] = Array.from(endpoint.success) + assert.isTrue(HttpApiSchema.isWithHeaders(schema)) + if (HttpApiSchema.isWithHeaders(schema)) { + assert.deepStrictEqual( + Schema.encodeSync(schema.headers as any)({ "x-count": 3 }), + { "x-count": "3" } + ) + } + }) + + it("leaves the wrapper untouched when codecs are disabled", () => { + const wrapped = HttpApiSchema.WithHeaders(Schema.Struct({ a: Schema.String }), { + "x-count": Schema.Int + }) + const endpoint = HttpApiEndpoint.get("list", "/users", { + disableCodecs: true, + success: wrapped + }) + + assert.isTrue(endpoint.success.has(wrapped)) + }) + + it("keeps a wrapped stream schema as the inner schema", () => { + const stream = sse() + const endpoint = HttpApiEndpoint.get("events", "/events", { + success: HttpApiSchema.WithHeaders(stream, { "x-count": Schema.Int }) + }) + + const [schema] = Array.from(endpoint.success) + assert.isTrue(HttpApiSchema.isWithHeaders(schema)) + if (HttpApiSchema.isWithHeaders(schema)) { + assert.strictEqual(schema.schema, stream) + } + }) + + it("preserves wrapper annotations through endpoint construction", () => { + const endpoint = HttpApiEndpoint.get("list", "/users", { + success: HttpApiSchema.WithHeaders(Schema.Struct({ a: Schema.String }), { + "x-count": Schema.Int + }).pipe(HttpApiSchema.status(201)) + }) + + const [schema] = Array.from(endpoint.success) + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(schema), 201) + }) + + it("validates a wrapped stream like a bare stream", () => { + assert.throws(() => + HttpApiEndpoint.get("events", "/events", { + success: [ + HttpApiSchema.WithHeaders(sse(), { "x-count": Schema.Int }), + HttpApiSchema.StreamUint8Array({ contentType: "application/custom-stream" }) + ] + }) + ) + assert.throws(() => + HttpApiEndpoint.head("events", "/events", { + success: HttpApiSchema.WithHeaders(sse(), { "x-count": Schema.Int }) as any + }) + ) + }) + + it("rejects wrapped and bare streams at distinct statuses", () => { + assert.throws( + () => + HttpApiEndpoint.get("events", "/events", { + success: [ + HttpApiSchema.status(206)( + HttpApiSchema.StreamUint8Array({ contentType: "application/custom-bytes" }) + ), + HttpApiSchema.WithHeaders( + HttpApiSchema.status(201)( + HttpApiSchema.StreamUint8Array({ contentType: "application/other-bytes" }) + ), + { "x-source": Schema.String } + ) + ] + }), + "Multiple streaming success responses are not supported" + ) + + assert.throws( + () => + HttpApiEndpoint.get("events", "/events", { + success: [ + HttpApiSchema.WithHeaders(sse(), { "x-count": Schema.Int }), + sse() + ] + }), + "Multiple streaming success responses are not supported" + ) + }) + + it("keeps WithHeaders in the error set with codec-transformed parts", () => { + const endpoint = HttpApiEndpoint.get("list", "/users", { + error: HttpApiSchema.WithHeaders( + Schema.Struct({ message: Schema.String }).pipe(HttpApiSchema.status(429)), + { "retry-after": Schema.Int } + ) + }) + + const [schema] = Array.from(endpoint.error) + assert.isTrue(HttpApiSchema.isWithHeaders(schema)) + assert.strictEqual(HttpApiSchema.getStatusErrorSchema(schema), 429) + if (HttpApiSchema.isWithHeaders(schema)) { + assert.deepStrictEqual( + Schema.encodeSync(schema.headers as any)({ "retry-after": 30 }), + { "retry-after": "30" } + ) + } + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts index 9e60a9e5e..8facf9e47 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/HttpApiSchema.test.ts @@ -115,6 +115,161 @@ describe("HttpApiSchema", () => { }) }) + describe("WithHeaders", () => { + it("stores the inner schema and headers schema", () => { + const headers = Schema.Struct({ "x-total-count": Schema.FiniteFromString }) + const schema = HttpApiSchema.WithHeaders(Schema.String, headers) + + assert.isTrue(Schema.isSchema(schema)) + assert.isTrue(HttpApiSchema.isWithHeaders(schema)) + assert.strictEqual(schema.schema, Schema.String) + assert.strictEqual(schema.headers, headers) + }) + + it("does not identify other schemas as WithHeaders", () => { + assert.isFalse(HttpApiSchema.isWithHeaders(Schema.String)) + assert.isFalse(HttpApiSchema.isWithHeaders(HttpApiSchema.StreamUint8Array())) + }) + + it("supports a fields shorthand for headers", () => { + const schema = HttpApiSchema.WithHeaders(Schema.String, { + "x-total-count": Schema.FiniteFromString + }) + + assert.isTrue(Schema.isSchema(schema.headers)) + assert.deepStrictEqual(Object.keys(schema.headers.fields), ["x-total-count"]) + }) + + it("rejects nested WithHeaders schemas", () => { + const inner = HttpApiSchema.WithHeaders(Schema.String, { "x-a": Schema.String }) + + assert.throws( + () => HttpApiSchema.WithHeaders(inner, { "x-b": Schema.String }), + "WithHeaders schemas cannot be nested" + ) + }) + + it("accepts only branded values when decoding", () => { + const schema = HttpApiSchema.WithHeaders(Schema.String, { "x-a": Schema.String }) + const value = HttpApiSchema.withHeaders({ body: "a", headers: { "x-a": "1" } }) + + assert.strictEqual(Schema.decodeUnknownSync(schema)(value), value) + assert.throws(() => Schema.decodeUnknownSync(schema)({ body: "a", headers: { "x-a": "1" } })) + }) + + it("keeps the wrapper intact through annotate", () => { + const headers = Schema.Struct({ "x-a": Schema.String }) + const schema = HttpApiSchema.WithHeaders(Schema.String, headers).annotate({ description: "described" }) + + assert.isTrue(HttpApiSchema.isWithHeaders(schema)) + assert.strictEqual(schema.schema, Schema.String) + assert.strictEqual(schema.headers, headers) + }) + + it("resolves the status from the wrapper first, falling through to the inner schema", () => { + const headers = Schema.Struct({ "x-a": Schema.String }) + + const onInner = HttpApiSchema.WithHeaders(Schema.String.pipe(HttpApiSchema.status(201)), headers) + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(onInner), 201) + + const onWrapper = HttpApiSchema.WithHeaders(Schema.String, headers).pipe(HttpApiSchema.status(202)) + assert.isTrue(HttpApiSchema.isWithHeaders(onWrapper)) + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(onWrapper), 202) + + const onBoth = HttpApiSchema.WithHeaders(Schema.String.pipe(HttpApiSchema.status(201)), headers) + .pipe(HttpApiSchema.status(202)) + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(onBoth), 202) + + const onNeither = HttpApiSchema.WithHeaders(Schema.String, headers) + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(onNeither), 200) + + assert.strictEqual(HttpApiSchema.getStatusSuccessSchema(Schema.String.pipe(HttpApiSchema.status(201))), 201) + }) + + it("resolves the response encoding from the wrapper first, falling through to the inner schema", () => { + const headers = Schema.Struct({ "x-a": Schema.String }) + + const onInner = HttpApiSchema.WithHeaders(Schema.String.pipe(HttpApiSchema.asText()), headers) + assert.strictEqual(HttpApiSchema.getResponseEncodingSchema(onInner)._tag, "Text") + + const onWrapper = HttpApiSchema.WithHeaders(Schema.String, headers).pipe( + HttpApiSchema.asJson({ contentType: "application/vnd.custom+json" }) + ) + assert.isTrue(HttpApiSchema.isWithHeaders(onWrapper)) + assert.strictEqual( + HttpApiSchema.getResponseEncodingSchema(onWrapper).contentType, + "application/vnd.custom+json" + ) + + const onNeither = HttpApiSchema.WithHeaders(Schema.String, headers) + assert.strictEqual(HttpApiSchema.getResponseEncodingSchema(onNeither)._tag, "Json") + }) + }) + + describe("withHeaders", () => { + it("constructs a branded response value", () => { + const value = HttpApiSchema.withHeaders({ body: "a", headers: { "x-a": "1" } }) + + assert.strictEqual(value.body, "a") + assert.deepStrictEqual(value.headers, { "x-a": "1" }) + }) + }) + + describe("encodeToWithHeaders", () => { + class UserNotFound extends Schema.TaggedError()("UserNotFound", { + userId: Schema.Int + }) {} + + const WrappedUserNotFound = UserNotFound.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: HttpApiSchema.Empty(404), + headers: { "x-user-id": Schema.Int } + }, { + decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }), + encode: (error) => ({ + headers: { "x-user-id": error.userId }, + body: undefined + }) + }) + ) + + it("encodes to a body and headers pair", () => { + assert.deepStrictEqual( + Schema.encodeSync(WrappedUserNotFound)(new UserNotFound({ userId: 1 })), + { body: undefined, headers: { "x-user-id": 1 } } + ) + }) + + it("decodes a pair back to the source type", () => { + assert.deepStrictEqual( + Schema.decodeUnknownSync(WrappedUserNotFound)({ body: undefined, headers: { "x-user-id": 1 } }), + new UserNotFound({ userId: 1 }) + ) + }) + + it("resolves the status from the body schema", () => { + assert.strictEqual(HttpApiSchema.getStatusError(WrappedUserNotFound.ast), 404) + }) + + it("resolves the response encoding from the body schema", () => { + const schema = Schema.String.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: Schema.String.pipe(HttpApiSchema.asText()), + headers: { "x-a": Schema.String } + }, { + decode: ({ body }) => body, + encode: (value) => ({ body: value, headers: { "x-a": "1" } }) + }) + ) + + assert.strictEqual(HttpApiSchema.getResponseEncoding(schema.ast)._tag, "Text") + }) + + it("defaults the response encoding to Json", () => { + assert.strictEqual(HttpApiSchema.getResponseEncoding(WrappedUserNotFound.ast)._tag, "Json") + }) + }) + it("does not identify buffered schemas as stream schemas", () => { assert.isFalse(HttpApiSchema.isStreamSchema(Schema.String)) assert.isFalse(HttpApiSchema.isStreamSchema(Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()))) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts index 36a932d5e..c0de5e47d 100644 --- a/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts +++ b/.context/effect/packages/effect/test/unstable/httpapi/OpenApi.test.ts @@ -70,6 +70,45 @@ const makeSecurityApi = ( ) describe("OpenApi", () => { + it("returns fresh spec instances when using the cache", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("get", "/resource") + ) + ) + + const first = OpenApi.fromApi(Api) + first.info.title = "mutated" + first.paths["/resource"]!.get!.summary = "mutated" + + const second = OpenApi.fromApi(Api) + + assert.notStrictEqual(first, second) + assert.notStrictEqual(first.info, second.info) + assert.notStrictEqual(first.paths["/resource"]!.get, second.paths["/resource"]!.get) + assert.strictEqual(second.info.title, "Api") + assert.isUndefined(second.paths["/resource"]!.get!.summary) + + second.info.title = "mutated again" + second.paths["/resource"]!.get!.summary = "mutated again" + const third = OpenApi.fromApi(Api) + + assert.strictEqual(third.info.title, "Api") + assert.isUndefined(third.paths["/resource"]!.get!.summary) + }) + + it("isolates the cached spec from external override mutations", () => { + const info = { title: "Api", version: "1.0.0" } + const Api = HttpApi.make("Api").annotate(OpenApi.Override, { info }) + + OpenApi.fromApi(Api) + info.title = "mutated" + + const cached = OpenApi.fromApi(Api) + + assert.strictEqual(cached.info.title, "Api") + }) + it("preserves every declared payload content type for normalized equivalents", () => { const profileA = "Application/Vnd.Effect+JSON; Profile=A" const profileB = "application/vnd.effect+json; profile=b" @@ -138,32 +177,150 @@ describe("OpenApi", () => { assert.property(streamExtension, "errorSchema") }) - it("preserves the data schema identifier for SSE streams", () => { - const Event = Schema.Struct({ - kind: Schema.String, - payload: Schema.String - }).annotate({ identifier: "MyEvent" }) + it("emits encoded success response headers", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("success", "/success", { + success: HttpApiSchema.WithHeaders(Schema.String, { + "X-Count": Schema.FiniteFromString, + "X-Optional": Schema.optionalKey(Schema.FiniteFromString), + "Content-Type": Schema.String + }) + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual(spec.paths["/success"]?.get?.responses[200]?.headers, { + "x-count": { + schema: { type: "string" }, + required: true + }, + "x-optional": { + schema: { type: "string" }, + required: false + } + }) + }) + it("emits encoded error response headers", () => { + class NotFound extends Schema.TaggedError()("NotFound", { + id: Schema.Number + }) {} + const NotFoundWithHeaders = NotFound.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: HttpApiSchema.Empty(404), + headers: { "X-Error-Id": Schema.FiniteFromString } + }, { + decode: ({ headers }) => new NotFound({ id: headers["X-Error-Id"] }), + encode: (error) => ({ body: undefined, headers: { "X-Error-Id": error.id } }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("error", "/error", { + error: NotFoundWithHeaders + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual(spec.paths["/error"]?.get?.responses[404]?.headers, { + "x-error-id": { + schema: { type: "string" }, + required: true + } + }) + }) + + it("emits encodeToWithHeaders wire schemas according to codec mode", () => { + const ErrorWithHeaders = Schema.Number.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: HttpApiSchema.Empty(404), + headers: { "X-Error-Id": Schema.Int } + }, { + decode: ({ headers }) => headers["X-Error-Id"], + encode: (id) => ({ body: undefined, headers: { "X-Error-Id": id } }) + }) + ) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test") + .add( + HttpApiEndpoint.get("encoded", "/encoded", { + error: ErrorWithHeaders + }) + ) + .add( + HttpApiEndpoint.get("unencoded", "/unencoded", { + disableCodecs: true, + error: ErrorWithHeaders + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual( + spec.paths["/encoded"]?.get?.responses[404]?.headers?.["x-error-id"]?.schema, + { + type: "string", + allOf: [{ pattern: "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" }] + } + ) + assert.deepStrictEqual( + spec.paths["/unencoded"]?.get?.responses[404]?.headers?.["x-error-id"]?.schema, + { type: "integer" } + ) + }) + + it("emits headers for an error wrapped in WithHeaders", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("error", "/error", { + error: HttpApiSchema.WithHeaders( + Schema.Struct({ message: Schema.String }).pipe(HttpApiSchema.status(429)), + { "X-Retry-After": Schema.Int } + ) + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual(spec.paths["/error"]?.get?.responses[429]?.headers, { + "x-retry-after": { + schema: { + type: "string", + allOf: [{ pattern: "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" }] + }, + required: true + } + }) + }) + + it("emits stream response headers", () => { const Api = HttpApi.make("Api").add( HttpApiGroup.make("test").add( HttpApiEndpoint.get("stream", "/stream", { - success: [HttpApiSchema.StreamSse({ data: Event })] + success: HttpApiSchema.WithHeaders(HttpApiSchema.StreamUint8Array(), { + "X-Stream-Id": Schema.Int + }) }) ) ) const spec = OpenApi.fromApi(Api) - const schemas = spec.components?.schemas - // The decoded data schema keeps its identifier. - assert.deepStrictEqual(schemas?.MyEvent, { - type: "object", - properties: { - kind: { type: "string" }, - payload: { type: "string" } - }, - required: ["kind", "payload"], - additionalProperties: false + assert.deepStrictEqual(spec.paths["/stream"]?.get?.responses[200]?.headers, { + "x-stream-id": { + schema: { + type: "string", + allOf: [{ pattern: "^[+-]?\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?$" }] + }, + required: true + } }) }) diff --git a/.context/effect/packages/effect/test/unstable/httpapi/OpenApiRepresentation.test.ts b/.context/effect/packages/effect/test/unstable/httpapi/OpenApiRepresentation.test.ts new file mode 100644 index 000000000..b7bfe3856 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/httpapi/OpenApiRepresentation.test.ts @@ -0,0 +1,136 @@ +import { assert, describe, it } from "@effect/vitest" +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" + +describe("OpenApi representation v2 consumer", () => { + it("uses canonical JSON codecs for additional declaration schemas", () => { + const AdditionalDate = Schema.Date.annotate({ identifier: "AdditionalDate" }) + const Api = HttpApi.make("Api").annotate(HttpApi.AdditionalSchemas, [AdditionalDate]) + + assert.deepStrictEqual(OpenApi.fromApi(Api).components.schemas, { + AdditionalDate: { $ref: "#/components/schemas/AdditionalDateEncoded" }, + AdditionalDateEncoded: { type: "string" } + }) + }) + + it("uses canonical JSON codecs for response declaration schemas", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("date", "/date", { success: Schema.Date }) + ) + ) + + assert.deepStrictEqual( + OpenApi.fromApi(Api).paths["/date"]?.get?.responses[200]?.content?.["application/json"]?.schema, + { type: "string" } + ) + }) + + it("deduplicates JSON encoding definitions across regular and SSE responses", () => { + const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" }) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.get("content", "/content", { + success: Schema.fromJsonString(Content) + }), + HttpApiEndpoint.get("stream", "/stream", { + success: HttpApiSchema.StreamSse({ data: Content }) + }) + ) + ) + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual(spec.components.schemas, { + "Tool.ContentEncoded": { + type: "string", + contentMediaType: "application/json" + } + }) + assert.deepStrictEqual( + spec.paths["/content"]?.get?.responses[200]?.content?.["application/json"]?.schema, + { $ref: "#/components/schemas/Tool.ContentEncoded" } + ) + assert.deepStrictEqual( + spec.paths["/stream"]?.get?.responses[200]?.content?.["text/event-stream"]?.schema, + { + type: "object", + properties: { + id: { anyOf: [{ type: "string" }, { type: "null" }] }, + event: { type: "string" }, + data: { $ref: "#/components/schemas/Tool.ContentEncoded" } + }, + required: ["id", "event", "data"], + additionalProperties: false + } + ) + }) + + it("projects request and response schemas to the encoded side", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.post("create", "/create", { + payload: Schema.FiniteFromString, + success: Schema.FiniteFromString + }) + ) + ) + + const spec = OpenApi.fromApi(Api) + + assert.deepStrictEqual( + spec.paths["/create"]?.post?.requestBody?.content["application/json"]?.schema, + { type: "string" } + ) + assert.deepStrictEqual( + spec.paths["/create"]?.post?.responses[200]?.content?.["application/json"]?.schema, + { type: "string" } + ) + }) + + it("uses custom JSON Schema compiler annotations", () => { + const CustomString = Schema.String.check(Schema.makeFilter((value) => value.length >= 2, { + representation: { + id: "test/openapi/minTwoCharacters", + payload: null + }, + toJsonSchema: () => ({ minLength: 2 }) + })) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.post("create", "/create", { payload: CustomString }) + ) + ) + + assert.deepStrictEqual( + OpenApi.fromApi(Api).paths["/create"]?.post?.requestBody?.content["application/json"]?.schema, + { + type: "string", + allOf: [{ minLength: 2 }] + } + ) + }) + + it("shares definitions and returns cached copies by API identity", () => { + const Shared = Schema.Struct({ value: Schema.FiniteFromString }).annotate({ identifier: "Shared" }) + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add( + HttpApiEndpoint.post("shared", "/shared", { + payload: Schema.Struct({ first: Shared, second: Shared }), + success: Shared + }) + ) + ) + + const first = OpenApi.fromApi(Api) + const second = OpenApi.fromApi(Api) + + assert.notStrictEqual(second, first) + assert.deepStrictEqual(second, first) + assert.deepStrictEqual(first.components.schemas.Shared, { + type: "object", + properties: { value: { type: "string" } }, + required: ["value"], + additionalProperties: false + }) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/observability/OtlpEnvHeaders.test.ts b/.context/effect/packages/effect/test/unstable/observability/OtlpEnvHeaders.test.ts new file mode 100644 index 000000000..bffa981bc --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/observability/OtlpEnvHeaders.test.ts @@ -0,0 +1,19 @@ +import { assert, it } from "@effect/vitest" +import { ConfigProvider, Effect } from "effect" +import * as OtlpEnv from "effect/unstable/observability/internal/otlpEnv" + +it.effect("decodes percent-encoded OTLP header values", () => + Effect.gen(function*() { + const headers = yield* OtlpEnv.headers("TRACES").parse( + ConfigProvider.fromEnv({ + env: { + OTEL_EXPORTER_OTLP_TRACES_HEADERS: "authorization=Bearer%20token,x-comma=comma%2Cvalue" + } + }) + ) + + assert.deepStrictEqual(headers, { + authorization: "Bearer token", + "x-comma": "comma,value" + }) + })) diff --git a/.context/effect/packages/effect/test/unstable/observability/OtlpExporter.test.ts b/.context/effect/packages/effect/test/unstable/observability/OtlpExporter.test.ts index 73d84c76d..072472de9 100644 --- a/.context/effect/packages/effect/test/unstable/observability/OtlpExporter.test.ts +++ b/.context/effect/packages/effect/test/unstable/observability/OtlpExporter.test.ts @@ -1,9 +1,9 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Ref } from "effect" +import { Clock, ConfigProvider, Deferred, type Duration, Effect, Exit, Fiber, Layer, Metric, Ref, Scope } from "effect" import { TestClock } from "effect/testing" import { HttpBody, HttpClient, HttpClientResponse } from "effect/unstable/http" import type { HttpClientError } from "effect/unstable/http" -import { OtlpExporter } from "effect/unstable/observability" +import { OtlpExporter, OtlpLogger, OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability" const makeHttpClient = Effect.fnUntraced(function*(retryAfter: string | undefined) { const attempts = yield* Ref.make(0) @@ -28,81 +28,458 @@ const makeHttpClient = Effect.fnUntraced(function*(retryAfter: string | undefine return { attempts, httpClient } as const }) -const makeExporter = (httpClient: HttpClient.HttpClient) => +const makeExporter = ( + httpClient: HttpClient.HttpClient, + options?: { + readonly exportInterval?: Duration.Input + readonly maxBatchSize?: number | "disabled" + readonly shutdownTimeout?: Duration.Input + readonly body?: (data: Array) => HttpBody.HttpBody + } +) => + OtlpExporter.make({ + label: "OtlpExporterTest", + url: "http://localhost:4318/v1/logs", + headers: undefined, + exportInterval: options?.exportInterval ?? "1 hour", + maxBatchSize: options?.maxBatchSize ?? 1, + body: (data) => [options?.body?.(data) ?? HttpBody.empty, Effect.void], + shutdownTimeout: options?.shutdownTimeout ?? "1 second" + }).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provide(OtlpExporter.layerFlusher) + ) + +const makeExporterRaw = (maxBatchSize: number | "disabled" = 1) => OtlpExporter.make({ label: "OtlpExporterTest", url: "http://localhost:4318/v1/logs", headers: undefined, exportInterval: "1 hour", - maxBatchSize: 1, - body: () => HttpBody.empty, + maxBatchSize, + body: () => [HttpBody.empty, Effect.void], shutdownTimeout: "1 second" - }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)) + }) + +const makeStatusHttpClient = Effect.fnUntraced(function*(status: number) { + const attempts = yield* Ref.make(0) + const urls = yield* Ref.make>([]) + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + yield* Ref.update(attempts, (attempts) => attempts + 1) + yield* Ref.update(urls, (urls) => [...urls, request.url]) + return HttpClientResponse.fromWeb(request, new Response(null, { status })) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + return { attempts, httpClient, urls } as const +}) const yieldNowN = (times: number) => Effect.forEach(Array.from({ length: times }), () => Effect.yieldNow, { discard: true }) +const makeControlledHttpClient = Effect.fnUntraced(function*(requestCount: number) { + const started = yield* Effect.forEach(Array.from({ length: requestCount }), () => Deferred.make()) + const releases = yield* Effect.forEach(Array.from({ length: requestCount }), () => Deferred.make()) + const interrupted = yield* Ref.make(0) + let requestIndex = 0 + + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + const index = requestIndex++ + yield* Deferred.succeed(started[index], undefined) + yield* Deferred.await(releases[index]).pipe( + Effect.onInterrupt(() => Ref.update(interrupted, (count) => count + 1)) + ) + return HttpClientResponse.fromWeb(request, new Response()) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + + return { httpClient, interrupted, releases, started } as const +}) + describe("OtlpExporter", () => { + it.effect("allows an in-flight timer export to finish during shutdown", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const { httpClient, releases, started } = yield* makeControlledHttpClient(1) + const exporter = yield* makeExporter(httpClient, { + exportInterval: "1 second", + maxBatchSize: 10 + }).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + yield* TestClock.adjust("1 second") + yield* Deferred.await(started[0]) + + const closeFiber = yield* Effect.forkChild(Scope.close(scope, Exit.void)) + yield* Effect.yieldNow + assert.isUndefined(closeFiber.pollUnsafe()) + + yield* Deferred.succeed(releases[0], undefined) + yield* Fiber.join(closeFiber) + })) + + it.effect("waits for each periodic export before starting the next interval", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const { httpClient, releases, started } = yield* makeControlledHttpClient(3) + const exporter = yield* makeExporter(httpClient, { + exportInterval: "1 second", + maxBatchSize: "disabled" + }).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + yield* TestClock.adjust("1 second") + yield* Deferred.await(started[0]) + + yield* TestClock.adjust("1 second") + assert.isFalse(yield* Deferred.isDone(started[1])) + + yield* Deferred.succeed(releases[0], undefined) + yield* TestClock.adjust("1 second") + yield* Deferred.await(started[1]) + + const closeFiber = yield* Effect.forkChild(Scope.close(scope, Exit.void)) + yield* Deferred.await(started[2]) + yield* Deferred.succeed(releases[1], undefined) + yield* Deferred.succeed(releases[2], undefined) + yield* Fiber.join(closeFiber) + })) + + it.effect("allows an in-flight batch export to finish during shutdown", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const { httpClient, releases, started } = yield* makeControlledHttpClient(1) + const exporter = yield* makeExporter(httpClient).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + yield* Deferred.await(started[0]) + + const closeFiber = yield* Effect.forkChild(Scope.close(scope, Exit.void)) + yield* Effect.yieldNow + assert.isUndefined(closeFiber.pollUnsafe()) + + yield* Deferred.succeed(releases[0], undefined) + yield* Fiber.join(closeFiber) + })) + + it.effect("exports remaining telemetry concurrently and waits for all requests", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const { httpClient, releases, started } = yield* makeControlledHttpClient(2) + const batches: Array> = [] + const exporter = yield* makeExporter(httpClient, { + maxBatchSize: 2, + body(data) { + batches.push(data.map((item) => item.value)) + return HttpBody.empty + } + }).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + exporter.push({ value: 2 }) + yield* Deferred.await(started[0]) + exporter.push({ value: 3 }) + + const closeFiber = yield* Effect.forkChild(Scope.close(scope, Exit.void)) + yield* Deferred.await(started[1]) + assert.deepStrictEqual(batches, [[1, 2], [3]]) + + yield* Deferred.succeed(releases[0], undefined) + yield* Effect.yieldNow + assert.isUndefined(closeFiber.pollUnsafe()) + yield* Deferred.succeed(releases[1], undefined) + yield* Fiber.join(closeFiber) + })) + + it.effect("bounds shutdown waiting and interrupts remaining requests", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const { httpClient, interrupted, started } = yield* makeControlledHttpClient(1) + const exporter = yield* makeExporter(httpClient).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + yield* Deferred.await(started[0]) + + const closeFiber = yield* Effect.forkChild(Scope.close(scope, Exit.void)) + yield* TestClock.adjust("1 second") + yield* Fiber.join(closeFiber) + assert.strictEqual(yield* Ref.get(interrupted), 1) + })) + + it.effect("does not initiate or wait for delivery when disabled", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const blocked = yield* Deferred.make() + const blockedInterrupted = yield* Deferred.make() + const failed = yield* Deferred.make() + const attempts = yield* Ref.make(0) + const httpClient = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + const attempt = yield* Ref.updateAndGet(attempts, (count) => count + 1) + if (attempt === 1) { + yield* Deferred.succeed(blocked, undefined) + return yield* Effect.never.pipe( + Effect.onInterrupt(() => Deferred.succeed(blockedInterrupted, undefined)) + ) + } + yield* Deferred.succeed(failed, undefined) + return HttpClientResponse.fromWeb(request, new Response(null, { status: 400 })) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + const exporter = yield* makeExporter(httpClient).pipe(Scope.provide(scope)) + + exporter.push({ value: 1 }) + yield* Deferred.await(blocked) + exporter.push({ value: 2 }) + yield* Deferred.await(failed) + yield* yieldNowN(3) + exporter.push({ value: 3 }) + + yield* Scope.close(scope, Exit.void) + yield* Deferred.await(blockedInterrupted) + assert.strictEqual(yield* Ref.get(attempts), 2) + })) + it.effect("retries status 429 with numeric retry-after delay", () => - Effect.scoped( - Effect.gen(function*() { - const { attempts, httpClient } = yield* makeHttpClient("2") - const exporter = yield* makeExporter(httpClient) + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeHttpClient("2") + const exporter = yield* makeExporter(httpClient) - exporter.push({ value: 1 }) - yield* yieldNowN(3) + exporter.push({ value: 1 }) + yield* yieldNowN(3) - assert.strictEqual(yield* Ref.get(attempts), 1) + assert.strictEqual(yield* Ref.get(attempts), 1) - yield* TestClock.adjust("1 second") - yield* yieldNowN(2) - assert.strictEqual(yield* Ref.get(attempts), 1) + yield* TestClock.adjust("1 second") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 1) - yield* TestClock.adjust("1 second") - yield* yieldNowN(2) - assert.strictEqual(yield* Ref.get(attempts), 2) - }) - )) + yield* TestClock.adjust("1 second") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 2) + })) + + it.effect("retries status 429 with HTTP-date retry-after delay", () => + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeHttpClient("Thu, 01 Jan 1970 00:01:00 GMT") + const exporter = yield* makeExporter(httpClient) + + exporter.push({ value: 1 }) + yield* yieldNowN(3) + + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("5 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("55 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 2) + })) it.effect("uses fallback retry-after delay when header is non-numeric", () => - Effect.scoped( - Effect.gen(function*() { - const { attempts, httpClient } = yield* makeHttpClient("soon") - const exporter = yield* makeExporter(httpClient) + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeHttpClient("soon") + const exporter = yield* makeExporter(httpClient) - exporter.push({ value: 1 }) - yield* yieldNowN(3) + exporter.push({ value: 1 }) + yield* yieldNowN(3) - assert.strictEqual(yield* Ref.get(attempts), 1) + assert.strictEqual(yield* Ref.get(attempts), 1) - yield* TestClock.adjust("4 seconds") - yield* yieldNowN(2) - assert.strictEqual(yield* Ref.get(attempts), 1) + yield* TestClock.adjust("4 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 1) - yield* TestClock.adjust("1 second") - yield* yieldNowN(2) - assert.strictEqual(yield* Ref.get(attempts), 2) - }) - )) + yield* TestClock.adjust("1 second") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 2) + })) it.effect("uses fallback retry-after delay when header is missing", () => - Effect.scoped( + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeHttpClient(undefined) + const exporter = yield* makeExporter(httpClient) + + exporter.push({ value: 1 }) + yield* yieldNowN(3) + + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("4 seconds") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 1) + + yield* TestClock.adjust("1 second") + yield* yieldNowN(2) + assert.strictEqual(yield* Ref.get(attempts), 2) + })) + + describe("flush", () => { + it.effect("exports buffered items without advancing to the export interval", () => Effect.gen(function*() { - const { attempts, httpClient } = yield* makeHttpClient(undefined) - const exporter = yield* makeExporter(httpClient) + const { attempts, httpClient } = yield* makeStatusHttpClient(200) + yield* Effect.scoped( + Effect.gen(function*() { + const exporter = yield* makeExporterRaw(10) + const flusher = yield* OtlpExporter.Flusher + const before = yield* Clock.currentTimeMillis + + exporter.push({ value: 1 }) + yield* flusher.flush + + assert.strictEqual(yield* Clock.currentTimeMillis, before) + assert.strictEqual(yield* Ref.get(attempts), 1) + }).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provide(OtlpExporter.layerFlusher) + ) + ) + })) - exporter.push({ value: 1 }) - yield* yieldNowN(3) + it.effect("shares one registry across traces, logs and metrics", () => + Effect.gen(function*() { + const { httpClient, urls } = yield* makeStatusHttpClient(200) + const signals = Layer.mergeAll( + OtlpTracer.layer({ + url: "http://localhost:4318/v1/traces", + resource: { serviceName: "test" }, + exportInterval: "1 hour", + maxBatchSize: 100 + }), + OtlpLogger.layer({ + url: "http://localhost:4318/v1/logs", + resource: { serviceName: "test" }, + exportInterval: "1 hour", + maxBatchSize: 100, + mergeWithExisting: false + }), + OtlpMetrics.layer({ + url: "http://localhost:4318/v1/metrics", + resource: { serviceName: "test" }, + exportInterval: "1 hour" + }) + ).pipe( + Layer.provide(OtlpSerialization.layerJson), + Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, httpClient)) + ) + yield* Effect.gen(function*() { + yield* Effect.log("flush test") + yield* Metric.update(Metric.counter("otlp_flush_test"), 1) + yield* Effect.void.pipe(Effect.withSpan("flush test")) + + const flusher = yield* OtlpExporter.Flusher + yield* flusher.flush + + assert.deepStrictEqual( + [...yield* Ref.get(urls)].sort(), + [ + "http://localhost:4318/v1/logs", + "http://localhost:4318/v1/metrics", + "http://localhost:4318/v1/traces" + ] + ) + }).pipe(Effect.provide(signals)) + })) + + it.effect("is a no-op while the exporter is disabled", () => + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeStatusHttpClient(400) + yield* Effect.scoped( + Effect.gen(function*() { + const exporter = yield* makeExporterRaw(10) + const flusher = yield* OtlpExporter.Flusher + + exporter.push({ value: 1 }) + yield* flusher.flush + assert.strictEqual(yield* Ref.get(attempts), 1) + + exporter.push({ value: 2 }) + yield* flusher.flush + assert.strictEqual(yield* Ref.get(attempts), 1) + }).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provide(OtlpExporter.layerFlusher) + ) + ) + })) + + it.effect("deregisters an exporter when its scope closes", () => + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeStatusHttpClient(200) + const flusher = yield* OtlpExporter.Flusher + const scope = yield* Scope.make() + + yield* makeExporterRaw("disabled").pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provideService(Scope.Scope, scope) + ) + + yield* Scope.close(scope, Exit.void) assert.strictEqual(yield* Ref.get(attempts), 1) - yield* TestClock.adjust("4 seconds") - yield* yieldNowN(2) + yield* flusher.flush assert.strictEqual(yield* Ref.get(attempts), 1) + }).pipe(Effect.provide(OtlpExporter.layerFlusher))) - yield* TestClock.adjust("1 second") - yield* yieldNowN(2) - assert.strictEqual(yield* Ref.get(attempts), 2) - }) - )) + it.effect("succeeds with no registered exporters", () => + Effect.gen(function*() { + const flusher = yield* OtlpExporter.Flusher + yield* flusher.flush + }).pipe(Effect.provide(OtlpExporter.layerFlusher))) + + it.effect("is available when the SDK is disabled", () => + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeStatusHttpClient(200) + const layer = OtlpTracer.layerFromConfig().pipe( + Layer.provide(OtlpSerialization.layerJson), + Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, httpClient)), + Layer.provideMerge(ConfigProvider.layer(ConfigProvider.fromEnv({ + env: { OTEL_SDK_DISABLED: "true" } + }))) + ) + + yield* Effect.gen(function*() { + const flusher = yield* OtlpExporter.Flusher + yield* flusher.flush + }).pipe(Effect.provide(layer)) + + assert.strictEqual(yield* Ref.get(attempts), 0) + })) + + it.effect("does not fail when the collector returns 500", () => + Effect.gen(function*() { + const { attempts, httpClient } = yield* makeStatusHttpClient(500) + yield* Effect.scoped( + Effect.gen(function*() { + const exporter = yield* makeExporterRaw(10) + const flusher = yield* OtlpExporter.Flusher + + exporter.push({ value: 1 }) + const fiber = yield* Effect.forkChild(flusher.flush) + yield* yieldNowN(3) + yield* TestClock.adjust("3 seconds") + yield* Fiber.join(fiber) + assert.strictEqual(yield* Ref.get(attempts), 4) + + yield* flusher.flush + assert.strictEqual(yield* Ref.get(attempts), 4) + }).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + Effect.provide(OtlpExporter.layerFlusher) + ) + ) + })) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/observability/OtlpMetrics.test.ts b/.context/effect/packages/effect/test/unstable/observability/OtlpMetrics.test.ts index 328db5922..510a0a0fc 100644 --- a/.context/effect/packages/effect/test/unstable/observability/OtlpMetrics.test.ts +++ b/.context/effect/packages/effect/test/unstable/observability/OtlpMetrics.test.ts @@ -1,10 +1,134 @@ import { assert, describe, it } from "@effect/vitest" -import { Array, Context, Effect, Layer, Metric, Predicate, Ref } from "effect" +import { Array, Context, Deferred, Effect, Fiber, Layer, Metric, Predicate, Ref } from "effect" import { TestClock } from "effect/testing" import { HttpClient, type HttpClientError, HttpClientResponse } from "effect/unstable/http" -import { OtlpMetrics, OtlpSerialization } from "effect/unstable/observability" +import { OtlpExporter, OtlpMetrics, OtlpSerialization } from "effect/unstable/observability" describe("OtlpMetrics", () => { + it.effect("retains delta checkpoints after a failed export", () => + Effect.gen(function*() { + const bodies = yield* Ref.make>([]) + const attempts = yield* Ref.make(0) + const client = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + if (request.body._tag === "Uint8Array") { + const body = JSON.parse(new TextDecoder().decode(request.body.body)) as OtlpExportRequest + yield* Ref.update(bodies, Array.append(body)) + } + const attempt = yield* Ref.updateAndGet(attempts, (n) => n + 1) + return HttpClientResponse.fromWeb(request, new Response(null, { status: attempt === 1 ? 400 : 200 })) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + const layer = OtlpMetrics.layer({ + url: "http://localhost:4318/v1/metrics", + resource: { serviceName: "repro" }, + temporality: "delta", + exportInterval: "1 hour" + }).pipe( + Layer.provide(OtlpSerialization.layerJson), + Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, client)) + ) + yield* Effect.gen(function*() { + yield* Metric.update(Metric.counter("repro_counter"), 5) + const histogram = Metric.histogram("repro_histogram", { boundaries: [10, 50, 100] }) + yield* Metric.update(histogram, 25) + yield* Metric.update(histogram, 75) + const frequency = Metric.frequency("repro_frequency") + yield* Metric.update(frequency, "a") + yield* Metric.update(frequency, "a") + yield* Metric.update(frequency, "b") + const summary = Metric.summary("repro_summary", { + maxAge: "1 minute", + maxSize: 100, + quantiles: [0.5] + }) + yield* Metric.update(summary, 10) + yield* Metric.update(summary, 20) + yield* Metric.update(summary, 30) + const flusher = yield* OtlpExporter.Flusher + yield* flusher.flush + yield* TestClock.adjust("60 seconds") + yield* flusher.flush + }).pipe(Effect.provide(layer), Effect.provideService(Metric.MetricRegistry, new Map())) + const [first, second] = yield* Ref.get(bodies) + assert.strictEqual(findMetric(first, "repro_counter")?.sum?.dataPoints[0].asDouble, 5) + assert.strictEqual(findMetric(second, "repro_counter")?.sum?.dataPoints[0].asDouble, 5) + assert.strictEqual(findMetric(first, "repro_histogram")?.histogram?.dataPoints[0].count, 2) + assert.strictEqual(findMetric(second, "repro_histogram")?.histogram?.dataPoints[0].count, 2) + assert.strictEqual(findMetric(first, "repro_histogram")?.histogram?.dataPoints[0].sum, 100) + assert.strictEqual(findMetric(second, "repro_histogram")?.histogram?.dataPoints[0].sum, 100) + assert.strictEqual(findFrequencyValue(first, "repro_frequency", "a"), 2) + assert.strictEqual(findFrequencyValue(second, "repro_frequency", "a"), 2) + assert.strictEqual(findFrequencyValue(first, "repro_frequency", "b"), 1) + assert.strictEqual(findFrequencyValue(second, "repro_frequency", "b"), 1) + assert.strictEqual(findMetric(first, "repro_summary_count")?.sum?.dataPoints[0].asInt, 3) + assert.strictEqual(findMetric(second, "repro_summary_count")?.sum?.dataPoints[0].asInt, 3) + assert.strictEqual(findMetric(first, "repro_summary_sum")?.sum?.dataPoints[0].asDouble, 60) + assert.strictEqual(findMetric(second, "repro_summary_sum")?.sum?.dataPoints[0].asDouble, 60) + assert.strictEqual( + findMetric(second, "repro_counter")?.sum?.dataPoints[0].startTimeUnixNano, + findMetric(first, "repro_counter")?.sum?.dataPoints[0].startTimeUnixNano + ) + })) + + it.effect("does not regress delta checkpoints when exports complete out of order", () => + Effect.scoped(Effect.gen(function*() { + const bodies = yield* Ref.make>([]) + const started = yield* Effect.forEach([0, 1], () => Deferred.make()) + const releases = yield* Effect.forEach([0, 1], () => Deferred.make()) + let requestIndex = 0 + const client = HttpClient.makeWith( + Effect.fnUntraced(function*(requestEffect) { + const request = yield* requestEffect + if (request.body._tag === "Uint8Array") { + const body = JSON.parse(new TextDecoder().decode(request.body.body)) as OtlpExportRequest + yield* Ref.update(bodies, Array.append(body)) + } + const index = requestIndex++ + if (index < 2) { + yield* Deferred.succeed(started[index], undefined) + yield* Deferred.await(releases[index]) + } + return HttpClientResponse.fromWeb(request, new Response()) + }), + Effect.succeed as HttpClient.HttpClient.Preprocess + ) + const layer = OtlpMetrics.layer({ + url: "http://localhost:4318/v1/metrics", + resource: { serviceName: "repro" }, + temporality: "delta", + exportInterval: "1 hour" + }).pipe( + Layer.provide(OtlpSerialization.layerJson), + Layer.provideMerge(Layer.succeed(HttpClient.HttpClient, client)) + ) + yield* Effect.gen(function*() { + const counter = Metric.counter("concurrent_delta") + const flusher = yield* OtlpExporter.Flusher + yield* Metric.update(counter, 1) + const first = yield* Effect.forkChild(flusher.flush) + yield* Deferred.await(started[0]) + + yield* TestClock.adjust("1 second") + yield* Metric.update(counter, 1) + const second = yield* Effect.forkChild(flusher.flush) + yield* Deferred.await(started[1]) + + yield* Deferred.succeed(releases[1], undefined) + yield* Fiber.join(second) + yield* Deferred.succeed(releases[0], undefined) + yield* Fiber.join(first) + + yield* TestClock.adjust("1 second") + yield* flusher.flush + }).pipe(Effect.provide(layer), Effect.provideService(Metric.MetricRegistry, new Map())) + + const requests = yield* Ref.get(bodies) + assert.strictEqual(findMetric(requests[2], "concurrent_delta")?.sum?.dataPoints[0].asDouble, 0) + }))) + describe("cumulative temporality", () => { it.effect("reports counter totals across export intervals", () => Effect.gen(function*() { @@ -516,3 +640,12 @@ const findMetric = (request: OtlpExportRequest, name: string): OtlpMetric | unde } return undefined } + +const findFrequencyValue = (request: OtlpExportRequest, name: string, key: string): number | undefined => + findMetric(request, name)?.sum?.dataPoints.find((dataPoint) => + dataPoint.attributes.some((attribute) => + attribute.key === "key" && + Predicate.hasProperty(attribute.value, "stringValue") && + attribute.value.stringValue === key + ) + )?.asInt diff --git a/.context/effect/packages/effect/test/unstable/observability/OtlpResource.test.ts b/.context/effect/packages/effect/test/unstable/observability/OtlpResource.test.ts index 5ca896756..fec532a9f 100644 --- a/.context/effect/packages/effect/test/unstable/observability/OtlpResource.test.ts +++ b/.context/effect/packages/effect/test/unstable/observability/OtlpResource.test.ts @@ -7,20 +7,43 @@ const attributesRecord = (resource: OtlpResource.Resource): Record { describe("fromConfig", () => { - it.effect("uses OTEL service variables before explicit options", () => + it.effect("decodes percent-encoded OTEL_RESOURCE_ATTRIBUTES", () => + Effect.gen(function*() { + const resource = yield* OtlpResource.fromConfig() + const attributes = Object.fromEntries( + resource.attributes.map((attribute) => [attribute.key, attribute.value.stringValue]) + ) + + assert.strictEqual(attributes.message, "hello world") + assert.strictEqual(attributes.comma, "comma,value") + }).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + OTEL_SERVICE_NAME: "repro", + OTEL_RESOURCE_ATTRIBUTES: "message=hello%20world,comma=comma%2Cvalue" + } + }) + ) + )) + + it.effect("uses explicit service options before attributes and environment variables", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({ serviceName: "explicit-service", serviceVersion: "explicit-version", attributes: { - "custom.attribute": "explicit" + "custom.attribute": "explicit", + "service.name": "explicit-attribute-service", + "service.version": "explicit-attribute-version" } }) assert.deepStrictEqual(attributesRecord(resource), { "custom.attribute": "explicit", - "service.name": "env-service", - "service.version": "env-version" + "service.name": "explicit-service", + "service.version": "explicit-version" }) }).pipe( Effect.provideService( @@ -28,17 +51,16 @@ describe("OtlpResource", () => { ConfigProvider.fromEnv({ env: { OTEL_SERVICE_NAME: "env-service", - OTEL_SERVICE_VERSION: "env-version" + OTEL_SERVICE_VERSION: "env-version", + OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version" } }) ) )) - it.effect("uses OTEL resource attributes before explicit options", () => + it.effect("uses explicit attributes before environment variables", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({ - serviceName: "explicit-service", - serviceVersion: "explicit-version", attributes: { "custom.attribute": "explicit", "service.name": "explicit-attribute-service", @@ -47,15 +69,17 @@ describe("OtlpResource", () => { }) assert.deepStrictEqual(attributesRecord(resource), { - "custom.attribute": "env", - "service.name": "env-attribute-service", - "service.version": "env-attribute-version" + "custom.attribute": "explicit", + "service.name": "explicit-attribute-service", + "service.version": "explicit-attribute-version" }) }).pipe( Effect.provideService( ConfigProvider.ConfigProvider, ConfigProvider.fromEnv({ env: { + OTEL_SERVICE_NAME: "env-service", + OTEL_SERVICE_VERSION: "env-version", OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version,custom.attribute=env" } @@ -63,6 +87,27 @@ describe("OtlpResource", () => { ) )) + it.effect("uses dedicated service variables before OTEL resource attributes", () => + Effect.gen(function*() { + const resource = yield* OtlpResource.fromConfig() + + assert.deepStrictEqual(attributesRecord(resource), { + "service.name": "env-service", + "service.version": "env-version" + }) + }).pipe( + Effect.provideService( + ConfigProvider.ConfigProvider, + ConfigProvider.fromEnv({ + env: { + OTEL_SERVICE_NAME: "env-service", + OTEL_SERVICE_VERSION: "env-version", + OTEL_RESOURCE_ATTRIBUTES: "service.name=env-attribute-service,service.version=env-attribute-version" + } + }) + ) + )) + it.effect("omits service.version when it is not configured", () => Effect.gen(function*() { const resource = yield* OtlpResource.fromConfig({ @@ -81,4 +126,13 @@ describe("OtlpResource", () => { ) )) }) + + describe("unknownToAttributeValue", () => { + it("preserves bigint attribute precision", () => { + const input = 9_007_199_254_740_993n + const output = OtlpResource.unknownToAttributeValue(input) + + assert.strictEqual(String(output.intValue), input.toString()) + }) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/observability/OtlpSerialization.test.ts b/.context/effect/packages/effect/test/unstable/observability/OtlpSerialization.test.ts new file mode 100644 index 000000000..44b04d804 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/observability/OtlpSerialization.test.ts @@ -0,0 +1,10 @@ +import { assert, describe, it } from "@effect/vitest" +import { encodeAnyValue } from "effect/unstable/observability/internal/otlpProtobuf" + +describe("OtlpSerialization", () => { + it("encodes a negative protobuf int64 as a ten-byte varint", () => { + const encoded = encodeAnyValue({ intValue: -1 }) + assert.strictEqual(encoded.length, 11) + assert.deepStrictEqual(Array.from(encoded), [0x18, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/persistence/KeyValueStore.test.ts b/.context/effect/packages/effect/test/unstable/persistence/KeyValueStore.test.ts index 2c78422ef..d0def431f 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/KeyValueStore.test.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/KeyValueStore.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, it } from "@effect/vitest" import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils" -import type { Layer } from "effect" -import { Effect, Option, Schema } from "effect" +import { Effect, type Layer, Option, Schema } from "effect" import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" +import * as Persistence from "effect/unstable/persistence/Persistence" export const testLayer = (layer: Layer.Layer) => { const run = (effect: Effect.Effect) => @@ -84,12 +84,27 @@ export const testLayer = (layer: Layer.Layer) strictEqual(value, undefined) strictEqual(length, 0) }))) + + it("setMany stores entries without a TTL", () => + run( + Effect.gen(function*() { + const backing = yield* Persistence.BackingPersistence + const store = yield* backing.make("store") + + yield* store.setMany([["key", { value: 1 }, undefined]]) + + deepStrictEqual(yield* store.get("key"), { value: 1 }) + }).pipe( + Effect.scoped, + Effect.provide(Persistence.layerBackingKvs) + ) + )) } describe("KeyValueStore / layerMemory", () => testLayer(KeyValueStore.layerMemory)) describe("KeyValueStore / prefix", () => { - it("prefixes the keys", () => + it.effect("prefixes the keys", () => Effect.gen(function*() { const store = yield* (KeyValueStore.KeyValueStore) const prefixed = KeyValueStore.prefix(store, "prefix/") @@ -103,8 +118,7 @@ describe("KeyValueStore / prefix", () => { strictEqual(yield* (store.get("prefix/foo")), "barbar") assertTrue(yield* (store.has("prefix/foo"))) }).pipe( - Effect.provide(KeyValueStore.layerMemory), - Effect.runPromise + Effect.provide(KeyValueStore.layerMemory) )) }) @@ -114,7 +128,7 @@ describe("toSchemaStore", () => { age: Schema.Number }) {} - it("encodes & decodes", () => + it.effect("encodes & decodes", () => Effect.gen(function*() { const store = yield* KeyValueStore.KeyValueStore const schemaStore = KeyValueStore.toSchemaStore(store, User) @@ -126,11 +140,10 @@ describe("toSchemaStore", () => { strictEqual(value.value.name, "foo") strictEqual(value.value.age, 43) }).pipe( - Effect.provide(KeyValueStore.layerMemory), - Effect.runPromise + Effect.provide(KeyValueStore.layerMemory) )) - it("prefix", () => + it.effect("prefix", () => Effect.gen(function*() { const store = yield* KeyValueStore.KeyValueStore const schemaStore = KeyValueStore.toSchemaStore(store, User) @@ -142,11 +155,10 @@ describe("toSchemaStore", () => { strictEqual(value.value.age, 42) } }).pipe( - Effect.provide(KeyValueStore.layerMemory), - Effect.runPromise + Effect.provide(KeyValueStore.layerMemory) )) - it("json compliant", () => + it.effect("json compliant", () => Effect.gen(function*() { const store = yield* KeyValueStore.KeyValueStore const schema = Schema.Struct({ @@ -158,7 +170,6 @@ describe("toSchemaStore", () => { assertTrue(Option.isSome(value)) deepStrictEqual(value.value.a, new Date(0)) }).pipe( - Effect.provide(KeyValueStore.layerMemory), - Effect.runPromise + Effect.provide(KeyValueStore.layerMemory) )) }) diff --git a/.context/effect/packages/effect/test/unstable/persistence/PersistedCacheTest.ts b/.context/effect/packages/effect/test/unstable/persistence/PersistedCacheTest.ts index e5a996f18..318c4444a 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/PersistedCacheTest.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/PersistedCacheTest.ts @@ -1,6 +1,7 @@ -import { assert, describe, it } from "@effect/vitest" -import type { Layer } from "effect" -import { Context, Data, Effect, Exit, Result, Schema } from "effect" +import { assert, it } from "@effect/vitest" +import type { Vitest } from "@effect/vitest" +import type { Duration, Layer } from "effect" +import { Context, Data, Effect, Exit, Schema } from "effect" import { Persistable, PersistedCache, Persistence } from "effect/unstable/persistence" class User extends Schema.Class("User")({ @@ -20,8 +21,13 @@ export class TransientError extends Data.TaggedError("TransientError") {} class LookupService extends Context.Service()("LookupService") {} -export const suite = (storeId: string, layer: Layer.Layer) => - describe(`PersistedCache (${storeId})`, { timeout: 30_000 }, () => { +export const suiteWith = ( + storeId: string, + layer: Layer.Layer, + testApi: Vitest.MethodsNonLive, + timeout: Duration.Input = "60 seconds" +) => + testApi.layer(layer, { timeout })(`PersistedCache (${storeId})`, (it) => { it.effect("smoke test", () => Effect.gen(function*() { const persistence = yield* Persistence.Persistence @@ -65,11 +71,7 @@ export const suite = (storeId: string, layer: Layer.Layer e instanceof TransientError ? Result.succeed(e) : Result.fail(e), () => Effect.void), - flakyTest - )) + }), 30_000) it.effect("requireServicesAt: 'lookup' requires lookup services at get-time", () => Effect.gen(function*() { @@ -95,17 +97,8 @@ export const suite = (storeId: string, layer: Layer.Layer e instanceof TransientError ? Result.succeed(e) : Result.fail(e), () => Effect.void), - flakyTest - )) + }), 30_000) }) -const flakyTest = (effect: Effect.Effect) => - effect.pipe( - Effect.timeoutOrElse({ - duration: "10 seconds", - orElse: () => Effect.void - }) - ) +export const suite = (storeId: string, layer: Layer.Layer) => + suiteWith(storeId, layer, it) diff --git a/.context/effect/packages/effect/test/unstable/persistence/PersistedQueueTest.ts b/.context/effect/packages/effect/test/unstable/persistence/PersistedQueueTest.ts index dedca134d..c897e7f5f 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/PersistedQueueTest.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/PersistedQueueTest.ts @@ -1,14 +1,21 @@ import { assert, it } from "@effect/vitest" +import type { Vitest } from "@effect/vitest" import { Effect, Fiber, Latch, Layer, Schema } from "effect" +import type { Duration } from "effect" import { TestClock } from "effect/testing" import { PersistedQueue } from "effect/unstable/persistence" -export const suite = (name: string, layer: Layer.Layer) => - it.layer( +export const suiteWith = ( + name: string, + layer: Layer.Layer, + testApi: Vitest.MethodsNonLive, + timeout: Duration.Input = "30 seconds" +) => + testApi.layer( PersistedQueue.layer.pipe( Layer.provideMerge(layer) ), - { timeout: "30 seconds" } + { timeout } )(`PersistedQueue (${name})`, (it) => { it.effect("offer + take", () => Effect.gen(function*() { @@ -100,6 +107,22 @@ export const suite = (name: string, layer: Layer.Layer + Effect.gen(function*() { + const first = yield* PersistedQueue.make({ name: "custom-id-first", schema: Item }) + const second = yield* PersistedQueue.make({ name: "custom-id-second", schema: Item }) + + yield* first.offer({ n: 1n }, { id: "shared-custom-id" }) + yield* second.offer({ n: 2n }, { id: "shared-custom-id" }) + + const fiber = yield* second.take(Effect.succeed).pipe(Effect.forkScoped) + yield* TestClock.adjust(1000) + yield* Effect.sleep(1000).pipe(TestClock.withLive) + + assert.isDefined(fiber.pollUnsafe()) + assert.deepStrictEqual(yield* Fiber.join(fiber), { n: 2n }) + })) + it.effect("does not redeliver in-flight elements", () => Effect.gen(function*() { const queue = yield* PersistedQueue.make({ @@ -193,3 +216,6 @@ export const suite = (name: string, layer: Layer.Layer) => + suiteWith(name, layer, it) diff --git a/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts b/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts index 3f82770f7..7117d58ff 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/RateLimiter.test.ts @@ -4,6 +4,37 @@ import { TestClock } from "effect/testing" import { RateLimiter } from "effect/unstable/persistence" describe(`RateLimiter`, () => { + it.effect("supports partially applied sleep", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const sleep = RateLimiter.sleep(limiter) + const result = yield* sleep({ + algorithm: "fixed-window", + window: "1 minute", + limit: 5, + key: "partial" + }) + + assert.strictEqual(result.remaining, 4) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + + it.effect("supports uncurried sleep", () => + Effect.gen(function*() { + const limiter = yield* RateLimiter.make + const result = yield* RateLimiter.sleep(limiter, { + algorithm: "fixed-window", + window: "1 minute", + limit: 5, + key: "direct" + }) + + assert.strictEqual(result.remaining, 4) + }).pipe( + Effect.provide(RateLimiter.layerStoreMemory) + )) + describe("fixed-window", () => { it.effect("returns accumulated delays after the fixed window is exceeded", () => Effect.gen(function*() { diff --git a/.context/effect/packages/effect/test/unstable/persistence/Redis.test.ts b/.context/effect/packages/effect/test/unstable/persistence/Redis.test.ts index 2716757d1..176462cfb 100644 --- a/.context/effect/packages/effect/test/unstable/persistence/Redis.test.ts +++ b/.context/effect/packages/effect/test/unstable/persistence/Redis.test.ts @@ -1,8 +1,76 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect } from "effect" -import { Redis } from "effect/unstable/persistence" +import { Duration, Effect, Layer } from "effect" +import { Persistence, Redis } from "effect/unstable/persistence" describe("Redis", () => { + it("preserves __proto__ as an own script option", () => { + const value = { polluted: true } + const script = Redis.script(() => [], { + ["__proto__"]: value, + lua: "return nil", + numberOfKeys: 0 + } as any) + + assert.isTrue(Object.hasOwn(script, "__proto__")) + assert.strictEqual((script as any)["__proto__"], value) + }) + + it.effect("clearing an empty persistence store succeeds", () => { + const redis = Redis.Redis.of({ + send: (command: string, ...args: ReadonlyArray) => { + if (command.toUpperCase() === "KEYS") return Effect.succeed([] as unknown as A) + if (command.toUpperCase() === "DEL" && args.length === 0) { + return Effect.fail(new Redis.RedisError({ cause: "ERR wrong number of arguments for 'del' command" })) + } + return Effect.succeed(undefined as unknown as A) + }, + eval: () => () => Effect.die("unused") + }) + return Effect.gen(function*() { + const backing = yield* Persistence.BackingPersistence + const store = yield* backing.make("empty") + yield* store.clear + }).pipe( + Effect.provide(Persistence.layerBackingRedis.pipe(Layer.provide(Layer.succeed(Redis.Redis, redis)))) + ) + }) + + it.effect("rounds fractional persistence TTLs up to whole milliseconds", () => { + const commands: Array]> = [] + const scripts: Array = [] + const redis = Redis.Redis.of({ + send: (command: string, ...args: ReadonlyArray) => { + commands.push([command, args]) + return Effect.succeed(undefined as unknown as A) + }, + eval: + ; readonly result: unknown }>() => + (...params: Config["params"]) => { + scripts.push(params[0]) + return Effect.succeed(undefined as Config["result"]) + } + }) + return Effect.gen(function*() { + const backing = yield* Persistence.BackingPersistence + const store = yield* backing.make("ttl") + const ttl = Duration.nanos(1_500_000n) + + yield* store.set("single", {}, ttl) + yield* store.setMany([["batch", {}, ttl]]) + + assert.deepStrictEqual( + commands.filter(([command]) => command === "SET"), + [["SET", ["ttl:single", "{}", "PX", "2"]]] + ) + assert.deepStrictEqual(scripts, [{ + sets: new Map([["ttl:batch", "{}"]]), + expires: new Map([["ttl:batch", 2]]) + }]) + }).pipe( + Effect.provide(Persistence.layerBackingRedis.pipe(Layer.provide(Layer.succeed(Redis.Redis, redis)))) + ) + }) + it.effect("retries script loading when SCRIPT LOAD fails", () => Effect.gen(function*() { const commands: Array]> = [] diff --git a/.context/effect/packages/effect/test/unstable/persistence/SqlCleanupTest.ts b/.context/effect/packages/effect/test/unstable/persistence/SqlCleanupTest.ts new file mode 100644 index 000000000..f23152a60 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/persistence/SqlCleanupTest.ts @@ -0,0 +1,22 @@ +import { Duration, Effect, Schedule } from "effect" +import { sqlCleanupBatchSize } from "effect/internal/persistence" +import { TestClock } from "effect/testing" + +export const expiredEntryCount = sqlCleanupBatchSize + 1 +export const expiredAtEpoch = 0 +export const futureExpiresAt = Number.MAX_SAFE_INTEGER +export const cleanupBatchDelay = Duration.millis(10) +export const testTimeout = 30_000 + +export const waitForCount = ( + effect: Effect.Effect, + predicate: (count: number) => boolean +) => + effect.pipe( + Effect.repeat({ + until: predicate, + schedule: Schedule.spaced(cleanupBatchDelay) + }), + Effect.timeout(Duration.millis(testTimeout)), + TestClock.withLive + ) diff --git a/.context/effect/packages/effect/test/unstable/process/ChildProcess.test.ts b/.context/effect/packages/effect/test/unstable/process/ChildProcess.test.ts index e48c7f095..31ed43eb8 100644 --- a/.context/effect/packages/effect/test/unstable/process/ChildProcess.test.ts +++ b/.context/effect/packages/effect/test/unstable/process/ChildProcess.test.ts @@ -83,21 +83,21 @@ describe("ChildProcess", () => { const cmd = ChildProcess.make`echo hello` const handle = yield* cmd assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345)) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("should spawn a standard command", () => Effect.gen(function*() { const cmd = ChildProcess.make("node", ["--version"]) const handle = yield* cmd assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345)) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("should return a process handle", () => Effect.gen(function*() { const cmd = ChildProcess.make`long-running-process` const handle = yield* cmd assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345)) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("collects stdout through Stream APIs", () => Effect.gen(function*() { @@ -105,7 +105,7 @@ describe("ChildProcess", () => { const handle = yield* cmd const chunks = yield* Stream.runCollect(handle.stdout) assert.isTrue(chunks.length > 0) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("should allow waiting for exit code", () => Effect.gen(function*() { @@ -113,7 +113,7 @@ describe("ChildProcess", () => { const handle = yield* cmd const exitCode = yield* handle.exitCode assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("should unref a process and return a reref effect", () => Effect.gen(function*() { @@ -122,7 +122,7 @@ describe("ChildProcess", () => { const reref = yield* handle.unref assert.isDefined(reref) yield* reref - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) it.effect("should allow restoring the reference within acquireRelease", () => Effect.gen(function*() { @@ -148,7 +148,7 @@ describe("ChildProcess", () => { ) const handle = yield* pipeline assert.strictEqual(handle.pid, ChildProcessSpawner.ProcessId(12345)) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) }) describe("setCwd", () => { @@ -346,7 +346,7 @@ describe("ChildProcess", () => { const handle = yield* cmd assert.isDefined(handle.getInputFd) assert.isDefined(handle.getOutputFd) - }).pipe(Effect.scoped, Effect.provide(MockExecutorLayer))) + }).pipe(Effect.provide(MockExecutorLayer))) }) }) }) diff --git a/.context/effect/packages/effect/test/unstable/process/ChildProcessSpawnerTest.ts b/.context/effect/packages/effect/test/unstable/process/ChildProcessSpawnerTest.ts new file mode 100644 index 000000000..9c24aa48e --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/process/ChildProcessSpawnerTest.ts @@ -0,0 +1,1170 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import type * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import type * as PlatformError from "effect/PlatformError" +import * as Schedule from "effect/Schedule" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/testing/TestClock" +import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" + +export interface Options { + readonly processGroups: boolean + readonly additionalFds?: boolean | undefined +} + +// Helper to collect stream output into a string +const decodeByteStream = Effect.fnUntraced( + function*( + stream: Stream.Stream + ) { + const chunks = yield* Stream.runCollect(stream) + const totalLength = chunks.reduce((acc, c) => acc + c.length, 0) + const result = new Uint8Array(totalLength) + let offset = 0 + for (const chunk of chunks) { + result.set(chunk, offset) + offset += chunk.length + } + return new TextDecoder().decode(result).trim() + } +) + +export const suite = ( + name: string, + layer: Layer.Layer, + options: Options +) => + describe(name, () => { + it.layer(layer)((it) => { + describe("spawn", () => { + describe("basic spawning", () => { + it.effect("should spawn a simple command and collect output", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "printf portable"]) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "portable") + })) + + it.effect("should spawn echo command", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["hello", "world"]) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "hello world") + })) + + it.effect("should spawn with template literal", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo spawned` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "spawned") + })) + }) + + describe("cwd option", () => { + it.effect("should handle command with working directory", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("pwd", [], { cwd: "/tmp" }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + // On macOS, /tmp is a symlink to /private/tmp + assert.isTrue(output.includes("tmp")) + })) + + it.effect("should use cwd with template literal form", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make({ cwd: "/tmp" })`pwd` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.isTrue(output.includes("tmp")) + })) + }) + + describe("env option", () => { + it.effect("should handle environment variables", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo $TEST_VAR"], { + env: { TEST_VAR: "test_value" }, + extendEnv: true + }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "test_value") + })) + + it.effect("should handle multiple environment variables", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo $VAR1-$VAR2-$VAR3"], { + env: { VAR1: "one", VAR2: "two", VAR3: "three" }, + extendEnv: true + }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "one-two-three") + })) + + it.effect("should merge environment variables with setEnv", () => + Effect.gen(function*() { + const command = ChildProcess.make("sh", ["-c", "echo $VAR1-$VAR2-$VAR3"], { + env: { VAR1: "one", VAR2: "two" }, + extendEnv: true + }).pipe(ChildProcess.setEnv({ VAR2: "override", VAR3: "three" })) + const handle = yield* command + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "one-override-three") + })) + }) + + describe("shell option", () => { + it.effect("should execute with shell when using sh -c", () => + Effect.gen(function*() { + // Use sh -c to test shell expansion without triggering deprecation warning + const handle = yield* ChildProcess.make("sh", ["-c", "echo $SHELL_EXPANSION_VAR"], { + env: { SHELL_EXPANSION_VAR: "expanded" }, + extendEnv: true + }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "expanded") + })) + + it.effect("should not expand variables without shell", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["$HOME"], { shell: false }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + // Without shell, $HOME should not be expanded + assert.strictEqual(output, "$HOME") + })) + + it.effect("should allow piping with shell", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo hello | tr a-z A-Z"]) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO") + })) + }) + + describe("template literal forms", () => { + it.effect("should work with template literal form", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "hello") + })) + + it.effect("should handle string interpolation", () => + Effect.gen(function*() { + const name = "world" + const handle = yield* ChildProcess.make`echo hello ${name}` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "hello world") + })) + + it.effect("should handle number interpolation", () => + Effect.gen(function*() { + const count = 42 + const handle = yield* ChildProcess.make`echo count is ${count}` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "count is 42") + })) + + it.effect("should handle array interpolation", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const dir = yield* fs.makeTempDirectoryScoped() + const file = path.join(dir, "array-interpolation.txt") + const args = ["-l", "-a"] + yield* fs.writeFile(file, new TextEncoder().encode("test")) + + const handle = yield* ChildProcess.make`ls ${args} ${dir}` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.isTrue(output.includes("array-interpolation.txt")) + })) + + it.effect("should handle multiple interpolations", () => + Effect.gen(function*() { + const greeting = "hello" + const target = "world" + const handle = yield* ChildProcess.make`echo ${greeting} ${target}` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "hello world") + })) + + it.effect("should handle options with template literal", () => + Effect.gen(function*() { + const filename = "test.txt" + const handle = yield* ChildProcess.make({ cwd: "/tmp" })`echo ${filename}` + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "test.txt") + })) + }) + + describe("stderr streaming", () => { + it.effect("should capture stderr output", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo error message >&2"]) + const stderr = yield* decodeByteStream(handle.stderr) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stderr, "error message") + })) + + it.effect("should capture both stdout and stderr", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo stdout; echo stderr >&2"]) + const [stdout, stderr] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, "stdout") + assert.strictEqual(stderr, "stderr") + })) + + it.effect("should handle more stdout than stderr", () => + Effect.gen(function*() { + // Process outputs many lines to stdout but only one to stderr + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo line1; echo line2; echo line3; echo line4; echo line5; echo error >&2"] + ) + const [stdout, stderr] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, ["line1", "line2", "line3", "line4", "line5"].join("\n")) + assert.strictEqual(stderr, "error") + })) + + it.effect("should handle more stderr than stdout", () => + Effect.gen(function*() { + // Process outputs many lines to stderr but only one to stdout + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo output; echo err1 >&2; echo err2 >&2; echo err3 >&2; echo err4 >&2; echo err5 >&2"] + ) + const [stdout, stderr] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, "output") + assert.strictEqual(stderr, ["err1", "err2", "err3", "err4", "err5"].join("\n")) + })) + + it.effect("should allow reading only stdout when stderr is empty", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["only stdout"]) + // Read streams in parallel to avoid deadlock when one stream is empty + const [stdout, stderr] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, "only stdout") + assert.strictEqual(stderr, "") + })) + + it.effect("should allow reading only stderr when stdout is empty", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo only stderr >&2"]) + // Read streams in parallel to avoid deadlock when one stream is empty + const [stdout, stderr] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, "") + assert.strictEqual(stderr, "only stderr") + })) + }) + + describe("combined output (all)", () => { + it.effect("should read interspersed stdout and stderr via .all", () => + Effect.gen(function*() { + // Use sleep to force buffer flushes between writes, ensuring + // stdout and stderr chunks arrive separately for proper interleaving + const handle = yield* ChildProcess.make( + "sh", + [ + "-c", + [ + "echo stdout1; sleep 0.01;", + "echo stderr1 >&2; sleep 0.01;", + "echo stdout2; sleep 0.01;", + "echo stderr2 >&2" + ].join(" ") + ] + ) + const all = yield* decodeByteStream(handle.all) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + const lines = all.split("\n") + assert.strictEqual(lines.length, 4) + assert.deepStrictEqual(lines.filter((line) => line.startsWith("stdout")), ["stdout1", "stdout2"]) + assert.deepStrictEqual(lines.filter((line) => line.startsWith("stderr")), ["stderr1", "stderr2"]) + })) + + it.effect("should capture only stdout via .all when no stderr", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["hello from stdout"]) + const all = yield* decodeByteStream(handle.all) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(all, "hello from stdout") + })) + + it.effect("should capture only stderr via .all when no stdout", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo hello from stderr >&2"]) + const all = yield* decodeByteStream(handle.all) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(all, "hello from stderr") + })) + + it.effect("should handle many lines of interspersed output via .all", () => + Effect.gen(function*() { + // Use sleep to force buffer flushes, ensuring interleaved arrival + const handle = yield* ChildProcess.make( + "sh", + ["-c", "for i in 1 2 3 4 5; do echo stdout$i; sleep 0.01; echo stderr$i >&2; sleep 0.01; done"] + ) + const all = yield* decodeByteStream(handle.all) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + const lines = all.split("\n") + assert.strictEqual(lines.length, 10) + assert.deepStrictEqual( + lines.filter((line) => line.startsWith("stdout")), + ["stdout1", "stdout2", "stdout3", "stdout4", "stdout5"] + ) + assert.deepStrictEqual( + lines.filter((line) => line.startsWith("stderr")), + ["stderr1", "stderr2", "stderr3", "stderr4", "stderr5"] + ) + })) + + it.effect("should allow reading .all independently", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo out; sleep 0.01; echo err >&2"] + ) + const all = yield* decodeByteStream(handle.all) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(all, ["out", "err"].join("\n")) + })) + }) + + describe("stdout streaming", () => { + it.effect("should stream stdout", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["streaming output"]) + const output = yield* decodeByteStream(handle.stdout) + + assert.strictEqual(output, "streaming output") + })) + + it.effect("should stream multiple lines", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "echo line1; echo line2; echo line3"]) + const output = yield* decodeByteStream(handle.stdout) + + assert.strictEqual(output, ["line1", "line2", "line3"].join("\n")) + })) + }) + + describe("process control", () => { + it.effect("should kill a process", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sleep", ["10"]) + + yield* handle.kill() + + // After killing, exitCode should eventually resolve (with signal error) + const exit = yield* Effect.exit(handle.exitCode) + assert.isTrue(exit._tag === "Failure") + })) + + it.effect("should kill with specific signal", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sleep", ["10"]) + + yield* handle.kill({ killSignal: "SIGKILL" }) + + const exit = yield* Effect.exit(handle.exitCode) + assert.isTrue(exit._tag === "Failure") + })) + + it.effect("should force kill a process after the initial signal times out", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const directory = yield* fs.makeTempDirectoryScoped() + const ready = path.join(directory, "ready") + const handle = yield* ChildProcess.make("sh", [ + "-c", + "trap '' TERM; : > \"$1\"; while :; do sleep 1; done", + "force-kill", + ready + ], { + killSignal: "SIGKILL", + stdout: "ignore", + stderr: "ignore" + }) + + yield* fs.exists(ready).pipe( + Effect.repeat({ + while: (exists) => !exists, + schedule: Schedule.spaced("10 millis") + }), + Effect.timeout("1 second"), + TestClock.withLive + ) + + const completed = yield* handle.kill({ + killSignal: "SIGTERM", + forceKillAfter: "50 millis" + }).pipe( + Effect.as(true), + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.succeed(false) + }), + TestClock.withLive + ) + + assert.isTrue(completed) + })) + + it.effect("should force kill a process when its scope closes", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const directory = yield* fs.makeTempDirectoryScoped() + const ready = path.join(directory, "ready") + const completed = yield* Effect.scoped(Effect.gen(function*() { + yield* ChildProcess.make("sh", [ + "-c", + "trap '' TERM; : > \"$1\"; sleep 2", + "force-kill", + ready + ], { + killSignal: "SIGTERM", + forceKillAfter: "50 millis", + stdout: "ignore", + stderr: "ignore" + }) + + yield* fs.exists(ready).pipe( + Effect.repeat({ + while: (exists) => !exists, + schedule: Schedule.spaced("10 millis") + }), + Effect.timeout("1 second") + ) + })).pipe( + Effect.as(true), + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.succeed(false) + }), + TestClock.withLive + ) + + assert.isTrue(completed) + })) + }) + }) + + describe("pipeline spawning", () => { + it.effect("should spawn a simple pipeline", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello world`.pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO WORLD") + })) + + it.effect("should spawn a three-stage pipeline", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello world`.pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`), + ChildProcess.pipeTo(ChildProcess.make("tr", [" ", "-"])) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO-WORLD") + })) + + it.effect("should pipe grep output", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["line1\nline2\nline3"]).pipe( + ChildProcess.pipeTo(ChildProcess.make`grep line2`) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "line2") + })) + + it.effect("should handle mixed command forms in pipeline", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["hello"]).pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO") + })) + }) + + describe("pipeline pipe options", () => { + it.effect("should pipe stderr to stdin with { from: 'stderr' }", () => + Effect.gen(function*() { + // Command that writes "error" to stderr + const handle = yield* ChildProcess.make("sh", ["-c", "echo error >&2"]).pipe( + ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "stderr" }) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "error") + })) + + it.effect("should pipe combined output with { from: 'all' }", () => + Effect.gen(function*() { + // Command that writes to both stdout and stderr with small delays + const handle = yield* ChildProcess.make("sh", [ + "-c", + "echo out1; sleep 0.01; echo err1 >&2; sleep 0.01; echo out2" + ]).pipe( + ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "all" }) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + const lines = output.split("\n") + assert.strictEqual(lines.length, 3) + assert.deepStrictEqual(lines.filter((line) => line.startsWith("out")), ["out1", "out2"]) + assert.deepStrictEqual(lines.filter((line) => line.startsWith("err")), ["err1"]) + })) + + it.effect("should default to stdout when no options provided", () => + Effect.gen(function*() { + // Command that writes to both stdout and stderr + const handle = yield* ChildProcess.make("sh", ["-c", "echo stdout; echo stderr >&2"]).pipe( + ChildProcess.pipeTo(ChildProcess.make`cat`) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + // Only stdout should be piped (default behavior) + assert.strictEqual(output, "stdout") + })) + + it.effect("should work with empty options object", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello`.pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, {}) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO") + })) + + it.effect("should work with explicit { from: 'stdout' }", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello`.pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, { from: "stdout" }) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO") + })) + + it.effect("should work with explicit { to: 'stdin' }", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make`echo hello`.pipe( + ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, { to: "stdin" }) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "HELLO") + })) + + it.effect("should support chained pipes with different options", () => + Effect.gen(function*() { + // First pipe: stdout to stdin (default) + // Second pipe: from stderr + const handle = yield* ChildProcess.make`echo hello`.pipe( + ChildProcess.pipeTo(ChildProcess.make("sh", ["-c", "cat; echo error >&2"])), + ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "stderr" }) + ) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, "error") + })) + }) + + describe("error handling", () => { + it.effect("should return non-zero exit code", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("sh", ["-c", "exit 1"]) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) + })) + + it.effect("should fail for invalid command", () => + Effect.gen(function*() { + const exit = yield* Effect.exit( + ChildProcess.make("nonexistent-command-12345") + ) + + assert.isTrue(exit._tag === "Failure") + })) + + it.effect("should handle spawn error with invalid cwd", () => + Effect.gen(function*() { + const exit = yield* Effect.exit( + ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" }) + ) + + assert.isTrue(exit._tag === "Failure") + })) + + it.effect("should throw permission denied as a typed error", () => + Effect.gen(function*() { + const path = yield* Path.Path + const cwd = yield* path.fromFileUrl(new URL("./fixtures/bash/", import.meta.url)) + + const command = ChildProcess.make({ cwd })`./no-permissions.sh` + const result = yield* Effect.flip(command) + + assert.strictEqual(result.reason._tag, "PermissionDenied") + assert.strictEqual(result.reason.module, "ChildProcess") + assert.strictEqual(result.reason.method, "spawn") + })) + }) + + describe("stdin", () => { + it.effect("allows providing standard input to a command", () => + Effect.gen(function*() { + const input = "a b c" + const stdin = Stream.make(new TextEncoder().encode(input)) + const handle = yield* ChildProcess.make("cat", { stdin }) + const output = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.deepStrictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(output, input) + })) + }) + + describe.skipIf(options.additionalFds === false)("additionalFds", () => { + it.effect("should read data from an output fd (fd3)", () => + Effect.gen(function*() { + // Use a shell script that writes to fd3 + // The script echoes "hello from fd3" to file descriptor 3 + const handle = yield* ChildProcess.make("sh", ["-c", "echo 'hello from fd3' >&3"], { + additionalFds: { fd3: { type: "output" } } + }) + + const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(fd3Output, "hello from fd3") + })) + + it.effect("should write data to an input fd (fd3)", () => + Effect.gen(function*() { + // Use a shell script that reads from fd3 and echoes it to stdout + // The script reads from file descriptor 3 and outputs to stdout + const inputData = "data from parent" + const inputStream = Stream.make(new TextEncoder().encode(inputData)) + + const handle = yield* ChildProcess.make("sh", ["-c", "cat <&3"], { + additionalFds: { + fd3: { type: "input", stream: inputStream } + } + }) + + const stdout = yield* decodeByteStream(handle.stdout) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, inputData) + })) + + it.effect("should handle multiple additional fds", () => + Effect.gen(function*() { + // Script that writes different messages to fd3 and fd4 + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo 'output on fd3' >&3; echo 'output on fd4' >&4"], + { + additionalFds: { + fd3: { type: "output" }, + fd4: { type: "output" } + } + } + ) + + const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) + const fd4Output = yield* decodeByteStream(handle.getOutputFd(4)) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(fd3Output, "output on fd3") + assert.strictEqual(fd4Output, "output on fd4") + })) + + it.effect("should handle fd gaps (e.g., fd3 and fd5 without fd4)", () => + Effect.gen(function*() { + // Script that writes to fd3 and fd5, skipping fd4 + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo 'on fd3' >&3; echo 'on fd5' >&5"], + { + additionalFds: { + fd3: { type: "output" }, + fd5: { type: "output" } + } + } + ) + + const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) + const fd5Output = yield* decodeByteStream(handle.getOutputFd(5)) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(fd3Output, "on fd3") + assert.strictEqual(fd5Output, "on fd5") + })) + + it.effect("should return empty stream for unconfigured output fd", () => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("echo", ["test"]) + + // fd3 was not configured, should return empty stream + const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(fd3Output, "") + })) + + it.effect("should handle bidirectional communication via separate fds", () => + Effect.gen(function*() { + // Script that reads from fd3, transforms it, and writes to fd4 + const inputData = "hello" + const inputStream = Stream.make(new TextEncoder().encode(inputData)) + + const handle = yield* ChildProcess.make( + "sh", + ["-c", "cat <&3 | tr a-z A-Z >&4"], + { + additionalFds: { + fd3: { type: "input", stream: inputStream }, + fd4: { type: "output" } + } + } + ) + + const fd4Output = yield* decodeByteStream(handle.getOutputFd(4)) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(fd4Output, "HELLO") + })) + + it.effect("should work alongside normal stdin/stdout/stderr", () => + Effect.gen(function*() { + // Script that uses all standard streams plus fd3 + const handle = yield* ChildProcess.make( + "sh", + ["-c", "echo 'stdout'; echo 'stderr' >&2; echo 'fd3' >&3"], + { additionalFds: { fd3: { type: "output" } } } + ) + + const [stdout, stderr, fd3Output] = yield* Effect.all([ + decodeByteStream(handle.stdout), + decodeByteStream(handle.stderr), + decodeByteStream(handle.getOutputFd(3)) + ], { concurrency: "unbounded" }) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + assert.strictEqual(stdout, "stdout") + assert.strictEqual(stderr, "stderr") + assert.strictEqual(fd3Output, "fd3") + })) + }) + + describe.sequential("process supervision", () => { + const countMatchingProcesses = (pattern: string) => + Effect.gen(function*() { + const handle = yield* ChildProcess.make("bash", [ + "-c", + `ps aux | grep '${pattern}' | grep -v grep | wc -l` + ]) + const output = yield* decodeByteStream(handle.stdout) + return Number.parseInt(output.trim()) + }).pipe(Effect.orElseSucceed(() => 0)) + + const killMatchingProcesses = (pattern: string) => { + if (!/^[A-Za-z0-9_-]+$/.test(pattern)) { + return Effect.die(new Error(`Invalid process pattern: ${pattern}`)) + } + return Effect.gen(function*() { + const escaped = `[${pattern[0]}]${pattern.slice(1)}` + const handle = yield* ChildProcess.make("bash", ["-c", `pkill -f '${escaped}' || true`]) + yield* Effect.ignore(handle.exitCode) + }).pipe(Effect.asVoid) + } + + const longRunningCommand = () => + ChildProcess.make("sh", ["-c", "sleep 30", "long-running-command"], { + stdin: "ignore", + stdout: "ignore", + stderr: "ignore" + }) + + it.effect.skipIf(!options.processGroups)("should kill all child processes in process group", () => + Effect.gen(function*() { + const path = yield* Path.Path + const cwd = yield* path.fromFileUrl(new URL("./fixtures/bash/", import.meta.url)) + + // Start the process that spawns children and grandchildren + const handle = yield* ChildProcess.make("./spawn-children.sh", { cwd }) + + // Give it time to spawn all processes + yield* TestClock.withLive(Effect.sleep("100 millis")) + + // Verify the main process is running + const isRunningBeforeKill = yield* handle.isRunning + assert.isTrue(isRunningBeforeKill) + + // Count processes before killing - should be at least 7 (1 parent + 3 children + 3 grandchildren) + const beforeKillHandle = yield* ChildProcess.make("bash", [ + "-c", + "ps aux | grep spawn-children.sh | grep -v grep | wc -l" + ]) + const beforeKill = yield* decodeByteStream(beforeKillHandle.stdout).pipe( + Effect.map((s) => + Number.parseInt(s.trim()) + ), + Effect.orElseSucceed(() => 0) + ) + assert.isAtLeast(beforeKill, 7) + + // Kill the main process + yield* handle.kill() + + // Verify the main process is no longer running + const isRunningAfterKill = yield* handle.isRunning + assert.isFalse(isRunningAfterKill) + + // Give a moment for cleanup to complete + yield* TestClock.withLive(Effect.sleep("100 millis")) + + // Check that no processes from the script are still running + const afterKillHandle = yield* ChildProcess.make("bash", [ + "-c", + "ps aux | grep spawn-children.sh | grep -v grep | wc -l" + ]) + const afterKill = yield* decodeByteStream(afterKillHandle.stdout).pipe( + Effect.map((s) => Number.parseInt(s.trim())), + Effect.orElseSucceed(() => 0) + ) + assert.strictEqual(afterKill, 0) + })) + + it.effect.skipIf(!options.processGroups)( + "should cleanup child processes when parent exits with non-zero code", + () => + Effect.gen(function*() { + const path = yield* Path.Path + const cwd = yield* path.fromFileUrl(new URL("./fixtures/bash/", import.meta.url)) + + // Count processes before running the command + const beforeRunHandle = yield* ChildProcess.make("bash", [ + "-c", + "ps aux | grep parent-exits-early.sh | grep -v grep | wc -l" + ]) + const beforeRun = yield* decodeByteStream(beforeRunHandle.stdout).pipe( + Effect.map((s) => Number.parseInt(s.trim())), + Effect.orElseSucceed(() => 0) + ) + assert.strictEqual(beforeRun, 0) + + // Run command in a separate scope so cleanup happens before we check + const exitCode = yield* Effect.scoped(Effect.gen(function*() { + const handle = yield* ChildProcess.make({ cwd })`./parent-exits-early.sh` + return yield* handle.exitCode + })) + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) + + // Allow cleanup to occur + yield* TestClock.withLive(Effect.sleep("100 millis")) + + const afterExitHandle = yield* ChildProcess.make("bash", [ + "-c", + "ps aux | grep 'parent-exits-early-' | grep -v grep | wc -l" + ]) + const afterExit = yield* decodeByteStream(afterExitHandle.stdout).pipe( + Effect.map((s) => Number.parseInt(s.trim())), + Effect.orElseSucceed(() => 0) + ) + // Child processes should be cleaned up after non-zero exit + assert.strictEqual(afterExit, 0) + }) + ) + + it.effect("should not kill an unrefed process when scope closes", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const handle = yield* Scope.provide(scope)(Effect.gen(function*() { + return yield* longRunningCommand() + })).pipe( + Effect.provide(layer) + ) + + yield* Effect.gen(function*() { + // @effect-diagnostics-next-line floatingEffect:off + yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(layer)) + yield* Scope.close(scope, Exit.void) + yield* TestClock.withLive(Effect.sleep("100 millis")) + + const isRunning = yield* handle.isRunning + assert.isTrue(isRunning) + }).pipe(Effect.ensuring(Effect.ignore(handle.kill({ killSignal: "SIGKILL" })))) + }).pipe(Effect.provide(layer))) + + it.effect("should kill a restored process when scope closes", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const handle = yield* Scope.provide(scope)(Effect.gen(function*() { + return yield* longRunningCommand() + })).pipe( + Effect.provide(layer) + ) + + const reref = yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(layer)) + yield* reref + yield* Scope.close(scope, Exit.void) + yield* TestClock.withLive(Effect.sleep("100 millis")) + + const isRunning = yield* handle.isRunning + assert.isFalse(isRunning) + }).pipe(Effect.provide(layer))) + + it.effect("should resolve exitCode after closing the original scope of an unrefed process", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const handle = yield* Scope.provide(scope)(Effect.gen(function*() { + return yield* ChildProcess.make("sh", ["-c", "sleep 0.05"], { + stdin: "ignore", + stdout: "ignore", + stderr: "ignore" + }) + })).pipe(Effect.provide(layer)) + + // @effect-diagnostics-next-line floatingEffect:off + yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(layer)) + yield* Scope.close(scope, Exit.void) + + const exitCode = yield* handle.exitCode + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + }).pipe(Effect.provide(layer))) + + it.effect.skipIf(!options.processGroups)( + "should cleanup descendants after an unrefed parent exits non-zero", + () => + Effect.gen(function*() { + const path = yield* Path.Path + const cwd = yield* path.fromFileUrl(new URL("./fixtures/bash/", import.meta.url)) + const scope = yield* Scope.make() + + const handle = yield* Scope.provide(scope)(Effect.gen(function*() { + return yield* ChildProcess.make({ cwd })`./parent-exits-early.sh` + })).pipe(Effect.provide(layer)) + + // @effect-diagnostics-next-line floatingEffect:off + yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(layer)) + yield* Scope.close(scope, Exit.void) + + const exitCode = yield* handle.exitCode + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) + + yield* TestClock.withLive(Effect.sleep("100 millis")) + + const remaining = yield* countMatchingProcesses("parent-exits-early-") + assert.strictEqual(remaining, 0) + }).pipe(Effect.provide(layer)) + ) + + it.effect("should unref every process in a pipeline", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const rootMarker = "pipeline-unref-root" + const tailMarker = "pipeline-unref-tail" + + const handle = yield* Scope.provide(scope)(Effect.gen(function*() { + return yield* ChildProcess.make("sh", ["-c", "sleep 30; :", rootMarker], { + stdin: "ignore", + stdout: "pipe", + stderr: "ignore" + }).pipe( + ChildProcess.pipeTo( + ChildProcess.make("sh", ["-c", "sleep 30; :", tailMarker], { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore" + }) + ) + ) + })).pipe(Effect.provide(layer)) + + yield* Effect.gen(function*() { + // @effect-diagnostics-next-line floatingEffect:off + yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(layer)) + yield* Scope.close(scope, Exit.void) + yield* TestClock.withLive(Effect.sleep("100 millis")) + + const rootCount = yield* countMatchingProcesses(rootMarker) + const tailCount = yield* countMatchingProcesses(tailMarker) + assert.strictEqual(rootCount, 1) + assert.strictEqual(tailCount, 1) + }).pipe( + Effect.ensuring(Effect.ignore(Effect.all([ + killMatchingProcesses(rootMarker), + killMatchingProcesses(tailMarker) + ], { discard: true }))) + ) + }).pipe(Effect.provide(layer))) + }) + + it.effect("should not deadlock on large stdout output", () => + Effect.gen(function*() { + // Generate ~5MB of output — enough to exceed the default PassThrough + // highWaterMark (16KB) many times over. Without the fix, the unread + // combinedPassThrough (.all) would exert backpressure on the source + // stream, blocking stdout too. + const handle = yield* ChildProcess.make("sh", ["-c", "seq 1 100000"]) + const output = yield* handle.stdout.pipe( + Stream.decodeText(), + Stream.runFold(() => "", (acc, chunk) => acc + chunk) + ) + const exitCode = yield* handle.exitCode + + assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) + const lines = output.trim().split("\n") + assert.strictEqual(lines.length, 100000) + assert.strictEqual(lines[0], "1") + assert.strictEqual(lines[99999], "100000") + }), { timeout: 10_000 }) + + it.effect("ChildProcess.string should not deadlock on large output", () => + Effect.gen(function*() { + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const output = yield* spawner.string(ChildProcess.make("sh", ["-c", "seq 1 100000"])) + const lines = output.trim().split("\n") + assert.strictEqual(lines.length, 100000) + assert.strictEqual(lines[0], "1") + assert.strictEqual(lines[99999], "100000") + }), { timeout: 10_000 }) + }) + }) diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/bash/no-permissions.sh b/.context/effect/packages/effect/test/unstable/process/fixtures/bash/no-permissions.sh similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/bash/no-permissions.sh rename to .context/effect/packages/effect/test/unstable/process/fixtures/bash/no-permissions.sh diff --git a/.context/effect/packages/effect/test/unstable/process/fixtures/bash/parent-exits-early.sh b/.context/effect/packages/effect/test/unstable/process/fixtures/bash/parent-exits-early.sh new file mode 100755 index 000000000..121c181a9 --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/process/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 with a marker for process assertions + sh -c 'sleep 30; :' parent-exits-early-grandchild + ) & + + # Keep the child running with a marker for process assertions + sh -c 'sleep 30; :' parent-exits-early-child + ) & +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/.context/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh b/.context/effect/packages/effect/test/unstable/process/fixtures/bash/spawn-children.sh similarity index 92% rename from .context/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh rename to .context/effect/packages/effect/test/unstable/process/fixtures/bash/spawn-children.sh index b192a58f7..dcc667581 100755 --- a/.context/effect/packages/platform-node-shared/test/fixtures/bash/spawn-children.sh +++ b/.context/effect/packages/effect/test/unstable/process/fixtures/bash/spawn-children.sh @@ -15,13 +15,13 @@ for i in {1..3}; do grandchild_pid=$BASHPID echo "Grandchild of child $i started with PID $grandchild_pid" # Keep running for 60 seconds - for j in {1..60}; do + for _ in {1..60}; do sleep 1 done ) & # Keep the child running - for j in {1..60}; do + for _ in {1..60}; do sleep 1 done ) & diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/config/SHOUTING b/.context/effect/packages/effect/test/unstable/process/fixtures/config/SHOUTING similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/config/SHOUTING rename to .context/effect/packages/effect/test/unstable/process/fixtures/config/SHOUTING diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/config/integer b/.context/effect/packages/effect/test/unstable/process/fixtures/config/integer similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/config/integer rename to .context/effect/packages/effect/test/unstable/process/fixtures/config/integer diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/config/nested/config b/.context/effect/packages/effect/test/unstable/process/fixtures/config/nested/config similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/config/nested/config rename to .context/effect/packages/effect/test/unstable/process/fixtures/config/nested/config diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/config/secret b/.context/effect/packages/effect/test/unstable/process/fixtures/config/secret similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/config/secret rename to .context/effect/packages/effect/test/unstable/process/fixtures/config/secret diff --git a/.context/effect/packages/effect/test/unstable/schema/VariantSchema.test.ts b/.context/effect/packages/effect/test/unstable/schema/VariantSchema.test.ts index e897b1b28..9d6bc6e3f 100644 --- a/.context/effect/packages/effect/test/unstable/schema/VariantSchema.test.ts +++ b/.context/effect/packages/effect/test/unstable/schema/VariantSchema.test.ts @@ -18,6 +18,69 @@ describe("VariantSchema", () => { assert.deepStrictEqual(Object.keys(Test.extract(struct, "b").fields), ["common", "onlyB", "exceptC"]) assert.deepStrictEqual(Object.keys(Test.extract(struct, "c").fields), ["common"]) }) + + it("Class preserves class and variant schema behavior", () => { + const Test = VariantSchema.make({ + variants: ["a", "b"], + defaultVariant: "a" + }) + class User extends Test.Class("User")({ + id: Test.FieldOnly(["a"])(Schema.Number), + name: Schema.String + }) {} + + const user = User.make({ id: 1, name: "Alice" }) + + assert.isTrue(user instanceof User) + assert.deepStrictEqual(user, new User({ id: 1, name: "Alice" })) + assert.deepStrictEqual(Schema.decodeSync(User)({ id: 1, name: "Alice" }), user) + assert.deepStrictEqual(Schema.decodeSync(User.b)({ name: "Alice" }), { name: "Alice" }) + assert.deepStrictEqual(Object.keys(User.fields), ["id", "name"]) + }) + + it("includes plain variant structs in the default union", () => { + const Test = VariantSchema.make({ variants: ["a", "b"], defaultVariant: "a" }) + const first = Test.Struct({ value: Schema.String }) + const second = Test.Struct({ value: Schema.Number }) + const union = Test.Union([first, second]) + + assert.strictEqual(union.members.length, 2) + assert.deepStrictEqual(Schema.decodeUnknownSync(union)({ value: "foo" }), { value: "foo" }) + assert.deepStrictEqual(Schema.decodeUnknownSync(union)({ value: 42 }), { value: 42 }) + }) + + it("omits undefined fields accepted by VariantSchema.Struct", () => { + const Test = VariantSchema.make({ variants: ["a"], defaultVariant: "a" }) + const struct = Test.Struct({ value: Schema.String, skipped: undefined }) + + assert.deepStrictEqual(Object.keys(Test.extract(struct, "a").fields), ["value"]) + }) + + it("omits undefined fields selected by VariantSchema.Field", () => { + const Test = VariantSchema.make({ variants: ["a"], defaultVariant: "a" }) + const struct = Test.Struct({ value: Schema.String, skipped: Test.Field({ a: undefined }) }) + + assert.deepStrictEqual(Object.keys(Test.extract(struct, "a").fields), ["value"]) + }) + + it("does not collide the __default variant with the default-schema cache entry", () => { + const Test = VariantSchema.make({ variants: ["a", "__default"], defaultVariant: "a" }) + const defaultFirst = Test.Struct({ + value: Test.Field({ a: Schema.String, __default: Schema.Number }) + }) + + Test.extract(defaultFirst, "a") + + assert.strictEqual(Test.extract(defaultFirst, "__default").fields.value, Schema.Number) + + const namedFirst = Test.Struct({ + value: Test.Field({ a: Schema.String, __default: Schema.Number }) + }) + + Test.extract(namedFirst, "__default") + + assert.strictEqual(Test.extract(namedFirst, "a").fields.value, Schema.String) + }) }) describe("Model", () => { diff --git a/.context/effect/packages/effect/test/unstable/sql/SqlResolver.test.ts b/.context/effect/packages/effect/test/unstable/sql/SqlResolver.test.ts index 87743e061..8dad85424 100644 --- a/.context/effect/packages/effect/test/unstable/sql/SqlResolver.test.ts +++ b/.context/effect/packages/effect/test/unstable/sql/SqlResolver.test.ts @@ -1,10 +1,69 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect } from "effect" +import { Effect, Exit } from "effect" import * as Schema from "effect/Schema" import { SqlResolver } from "effect/unstable/sql" describe("SqlResolver", () => { + describe("grouped", () => { + it.effect("does not execute a batch when every request fails encoding", () => + Effect.gen(function*() { + let executions = 0 + const resolver = SqlResolver.grouped({ + Request: Schema.Number.check(Schema.isGreaterThan(0)), + RequestGroupKey: (request) => request, + Result: Schema.Number, + ResultGroupKey: (result) => result, + execute: (inputs) => { + executions++ + return Effect.succeed(inputs) + } + }) + + const error = yield* Effect.flip(SqlResolver.request(-1, resolver)) + + assert.strictEqual(error._tag, "SchemaError") + assert.strictEqual(executions, 0) + })) + }) + describe("findById", () => { + it.effect("does not execute a batch when every request fails encoding", () => + Effect.gen(function*() { + let executions = 0 + const resolver = SqlResolver.findById({ + Id: Schema.Number.check(Schema.isGreaterThan(0)), + Result: Schema.Struct({ id: Schema.Number }), + ResultId: (result) => result.id, + execute: (inputs) => { + executions++ + return Effect.succeed(inputs.map((id) => ({ id }))) + } + }) + + const error = yield* Effect.flip(SqlResolver.request(-1, resolver)) + + assert.strictEqual(error._tag, "SchemaError") + assert.strictEqual(executions, 0) + })) + + it.effect("completes duplicate requests when id encoding fails", () => + Effect.gen(function*() { + const resolver = SqlResolver.findById({ + Id: Schema.Number.check(Schema.isGreaterThan(0)), + Result: Schema.Struct({ id: Schema.Number }), + ResultId: (result) => result.id, + execute: (inputs) => Effect.succeed(inputs.map((id) => ({ id }))) + }) + const execute = SqlResolver.request(resolver) + + const errors = yield* Effect.all([ + Effect.flip(execute(-1)), + Effect.flip(execute(-1)) + ], { concurrency: "unbounded" }) + + assert.deepStrictEqual(errors.map((error) => error._tag), ["SchemaError", "SchemaError"]) + })) + it.effect("deduplicates requests by id", () => Effect.gen(function*() { const batches: Array> = [] @@ -34,4 +93,61 @@ describe("SqlResolver", () => { assert.deepStrictEqual(batches, [[1, 2]]) })) }) + + describe("ordered", () => { + it.effect("does not execute a batch when every request fails encoding", () => + Effect.gen(function*() { + let executions = 0 + const resolver = SqlResolver.ordered({ + Request: Schema.Number.check(Schema.isGreaterThan(0)), + Result: Schema.String, + execute: (inputs) => { + executions++ + return Effect.succeed(inputs.map(String)) + } + }) + + const error = yield* Effect.flip(SqlResolver.request(-1, resolver)) + + assert.strictEqual(error._tag, "SchemaError") + assert.strictEqual(executions, 0) + })) + + it.effect("keeps valid results aligned when another request fails encoding", () => + Effect.gen(function*() { + const resolver = SqlResolver.ordered({ + Request: Schema.Number.check(Schema.isGreaterThan(0)), + Result: Schema.String, + execute: (inputs) => Effect.succeed(inputs.map((input) => `value-${input}`)) + }) + const execute = SqlResolver.request(resolver) + const [invalid, valid] = yield* Effect.all([ + Effect.exit(execute(-1)), + Effect.exit(execute(2)) + ], { concurrency: "unbounded" }) + + assert(Exit.isFailure(invalid)) + assert(Exit.isSuccess(valid)) + assert.strictEqual(valid.value, "value-2") + })) + }) + + describe("void", () => { + it.effect("does not execute a batch when every request fails encoding", () => + Effect.gen(function*() { + let executions = 0 + const resolver = SqlResolver.void({ + Request: Schema.Number.check(Schema.isGreaterThan(0)), + execute: (inputs) => { + executions++ + return Effect.succeed(inputs) + } + }) + + const error = yield* Effect.flip(SqlResolver.request(-1, resolver)) + + assert.strictEqual(error._tag, "SchemaError") + assert.strictEqual(executions, 0) + })) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/sql/Statement.test.ts b/.context/effect/packages/effect/test/unstable/sql/Statement.test.ts new file mode 100644 index 000000000..0f15afc2e --- /dev/null +++ b/.context/effect/packages/effect/test/unstable/sql/Statement.test.ts @@ -0,0 +1,35 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Statement from "effect/unstable/sql/Statement" + +describe("Statement", () => { + it("defaultTransforms ignores inherited properties", () => { + const row = Object.create({ inherited: 1 }) + row.own = 2 + + const nested = Statement.defaultTransforms((key) => key.toUpperCase()) + const flat = Statement.defaultTransforms((key) => key.toUpperCase(), false) + + assert.deepStrictEqual(nested.object(row), { OWN: 2 }) + assert.deepStrictEqual(nested.array([row]), [{ OWN: 2 }]) + assert.deepStrictEqual(nested.array([[row]]), [[{ OWN: 2 }]]) + assert.deepStrictEqual(flat.array([row]), [{ OWN: 2 }]) + }) + + it("compiles one fragment independently for each compiler", () => { + const postgres = Statement.makeCompiler({ + dialect: "pg", + placeholder: (index) => `$${index}`, + onIdentifier: Statement.defaultEscape("\""), + onRecordUpdate: () => ["", []], + onCustom: () => ["", []] + }) + const sqlite = Statement.makeCompilerSqlite() + const fragment = Statement.fragment([ + Statement.identifier("value"), + Statement.parameter(1) + ]) + + assert.deepStrictEqual(postgres.compile(fragment, false), ["\"value\"$1", [1]]) + assert.deepStrictEqual(sqlite.compile(fragment, false), ["\"value\"?", [1]]) + }) +}) diff --git a/.context/effect/packages/effect/test/unstable/workers/WorkerError.test.ts b/.context/effect/packages/effect/test/unstable/workers/WorkerError.test.ts index 7088cf930..9ef0b3682 100644 --- a/.context/effect/packages/effect/test/unstable/workers/WorkerError.test.ts +++ b/.context/effect/packages/effect/test/unstable/workers/WorkerError.test.ts @@ -1,5 +1,6 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Schema } from "effect" +import { Cause, Deferred, Effect, Exit, Fiber, Schema } from "effect" +import * as Worker from "effect/unstable/workers/Worker" import { isWorkerError, WorkerError, @@ -87,4 +88,43 @@ describe("WorkerError", () => { assert.strictEqual(decoded.message, "Failed to send message") })) }) + + describe("buffered sends", () => { + it.effect("reports postMessage failures as WorkerSendError", () => + Effect.gen(function*() { + const listening = yield* Deferred.make() + let emit!: (message: Worker.PlatformMessage) => void + const platform = Worker.makePlatform()({ + setup: () => + Effect.succeed({ + postMessage() { + throw new Error("post failed") + } + }), + listen: (options) => + Effect.sync(() => { + emit = options.emit + }).pipe(Effect.andThen(Deferred.succeed(listening, void 0))) + }) + const worker = yield* platform.spawn(0).pipe( + Effect.provideService(Worker.Spawner, () => ({})) + ) + + yield* worker.send("buffered") + const run = yield* Effect.forkChild(worker.run(() => Effect.void)) + yield* Deferred.await(listening) + yield* Effect.sync(() => emit([0])) + const exit = yield* Fiber.await(run) + + assert(Exit.isFailure(exit)) + if (Exit.isFailure(exit)) { + assert.isFalse(Cause.hasDies(exit.cause)) + const error = Cause.squash(exit.cause) + assert.isTrue(isWorkerError(error)) + if (isWorkerError(error)) { + assert.strictEqual(error.reason._tag, "WorkerSendError") + } + } + })) + }) }) diff --git a/.context/effect/packages/effect/test/unstable/workflow/WorkflowEngine.test.ts b/.context/effect/packages/effect/test/unstable/workflow/WorkflowEngine.test.ts index fe14b492c..964103c01 100644 --- a/.context/effect/packages/effect/test/unstable/workflow/WorkflowEngine.test.ts +++ b/.context/effect/packages/effect/test/unstable/workflow/WorkflowEngine.test.ts @@ -1,6 +1,7 @@ import { assert, describe, it } from "@effect/vitest" -import { Effect, Exit, Layer, Option, Schema } from "effect" -import { Workflow, WorkflowEngine } from "effect/unstable/workflow" +import { Effect, Exit, Fiber, Layer, Option, Schema, Scope } from "effect" +import { TestClock } from "effect/testing" +import { DurableDeferred, Workflow, WorkflowEngine } from "effect/unstable/workflow" describe("WorkflowEngine", () => { const IncrementWorkflow = Workflow.make("WorkflowEngine/IncrementWorkflow", { @@ -19,6 +20,32 @@ describe("WorkflowEngine", () => { const ClassWorkflowLayer = ClassWorkflow.toLayer(({ value }) => Effect.succeed(value + 1)) + const DeferredRaceWorkflow = Workflow.make("WorkflowEngine/DeferredRaceWorkflow", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id + }) + + const DeferredRaceGate = DurableDeferred.make("WorkflowEngine/DeferredRaceGate", { + success: Schema.String + }) + + let deferredRaceRuns = 0 + const DeferredRaceWorkflowLayer = DeferredRaceWorkflow.toLayer(() => + Effect.suspend(() => { + deferredRaceRuns++ + return DurableDeferred.raceAll({ + name: "memory-deferred-race", + success: Schema.String, + error: Schema.Never, + effects: [ + DurableDeferred.await(DeferredRaceGate), + Effect.sleep("10 seconds").pipe(Effect.as("activity")) + ] + }) + }) + ) + it.effect("layer executes and polls workflows", () => Effect.gen(function*() { const executionId = yield* IncrementWorkflow.execute({ value: 1 }, { discard: true }) @@ -57,4 +84,97 @@ describe("WorkflowEngine", () => { Layer.provideMerge(WorkflowEngine.layerMemory) )) )) + + it.effect("layerMemory wakes an active workflow when a durable deferred completes", () => + Effect.gen(function*() { + const payload = { id: "memory-deferred-race" } + const executionId = yield* DeferredRaceWorkflow.executionId(payload) + const fiber = yield* DeferredRaceWorkflow.execute(payload).pipe( + Effect.forkChild({ startImmediately: true }) + ) + + // Park the deferred branch before completing it. + yield* TestClock.adjust(1) + yield* TestClock.adjust(1) + const token = DurableDeferred.tokenFromExecutionId(DeferredRaceGate, { + workflow: DeferredRaceWorkflow, + executionId + }) + yield* DurableDeferred.succeed(DeferredRaceGate, { token, value: "signal" }) + // Require the wake to settle before the sleeper. + let polled = yield* DeferredRaceWorkflow.poll(executionId) + while (Option.isNone(polled) || polled.value._tag !== "Complete") { + yield* Effect.yieldNow + polled = yield* DeferredRaceWorkflow.poll(executionId) + } + + // Let the caller's suspended-retry loop pick up a replayed result. + yield* TestClock.adjust("1 second") + assert.strictEqual(yield* Fiber.join(fiber), "signal") + // Usually the completion preempts the parked run (2 runs); under load + // it can land before the branch parks and is read directly (1 run). + assert(deferredRaceRuns === 1 || deferredRaceRuns === 2) + }).pipe( + Effect.provide(DeferredRaceWorkflowLayer.pipe( + Layer.provideMerge(WorkflowEngine.layerMemory) + )) + )) + + it.effect("layerMemory propagates interruption when the engine is shut down", () => + Effect.gen(function*() { + const Stuck = Workflow.make("WorkflowEngine/ShutdownWorkflow", { + payload: { id: Schema.String }, + idempotencyKey: ({ id }) => id + }) + const layer = Stuck.toLayer(() => Effect.never).pipe( + Layer.provideMerge(WorkflowEngine.layerMemory) + ) + const scope = yield* Scope.make() + const context = yield* Scope.provide(Layer.build(layer), scope) + const fiber = yield* Stuck.execute({ id: "one" }).pipe( + Effect.provideContext(context), + Effect.forkChild({ startImmediately: true }) + ) + yield* Effect.yieldNow + yield* Effect.yieldNow + + // Shutting down must interrupt the caller, not report a suspension. + yield* Scope.close(scope, Exit.void) + const exit = yield* Fiber.await(fiber) + assert(Exit.hasInterrupts(exit)) + })) + + it.effect("layerMemory closes finalizers registered before suspension", () => + Effect.gen(function*() { + const gate = DurableDeferred.make("WorkflowEngine/SuspendedScope/Gate") + const finalized: Array = [] + let runs = 0 + const Suspends = Workflow.make("WorkflowEngine/SuspendedScope", { + payload: { id: Schema.String }, + idempotencyKey: ({ id }) => id + }) + const layer = Suspends.toLayer(() => + Effect.gen(function*() { + const run = ++runs + yield* Workflow.addFinalizer(() => Effect.sync(() => finalized.push(run))) + yield* DurableDeferred.await(gate) + }) + ).pipe(Layer.provideMerge(WorkflowEngine.layerMemory)) + + yield* Effect.gen(function*() { + const payload = { id: "one" } + const executionId = yield* Suspends.execute(payload, { discard: true }) + let result = yield* Suspends.poll(executionId) + while (Option.isNone(result) || result.value._tag !== "Suspended") { + yield* Effect.yieldNow + result = yield* Suspends.poll(executionId) + } + + const token = DurableDeferred.tokenFromExecutionId(gate, { workflow: Suspends, executionId }) + yield* DurableDeferred.succeed(gate, { token, value: void 0 }) + yield* Suspends.execute(payload) + + assert.deepStrictEqual(finalized, [2, 1]) + }).pipe(Effect.provide(layer)) + })) }) diff --git a/.context/effect/packages/effect/test/utils/assert.ts b/.context/effect/packages/effect/test/utils/assert.ts index e845d09d0..acb52784c 100644 --- a/.context/effect/packages/effect/test/utils/assert.ts +++ b/.context/effect/packages/effect/test/utils/assert.ts @@ -1,4 +1,4 @@ -import { Cause, Equal, Option, Predicate, Result } from "effect" +import { Cause, Equal, Option, Predicate, Result, SchemaIssue } from "effect" import * as Exit from "effect/Exit" import * as assert from "node:assert" import { assert as vassert } from "vitest" @@ -76,6 +76,17 @@ export function assertFalse(self: boolean, message?: string, ..._: Array) strictEqual(self, false, message) } +export function assertSchemaIssueError( + self: unknown, + expectedIssueMessage: string, + ..._: Array +): asserts self is Error & { readonly cause: SchemaIssue.Issue } { + assertInstanceOf(self, Error) + strictEqual(self.message, "Schema validation failed") + assertTrue(SchemaIssue.isIssue(self.cause)) + strictEqual(SchemaIssue.defaultFormatter(self.cause), expectedIssueMessage) +} + export function assertInclude(actual: string | undefined, expected: string, ..._: Array) { if (Predicate.isString(expected)) { if (!actual?.includes(expected)) { diff --git a/.context/effect/packages/effect/tsconfig.json b/.context/effect/packages/effect/tsconfig.json index b8984d44d..1d6699a5a 100644 --- a/.context/effect/packages/effect/tsconfig.json +++ b/.context/effect/packages/effect/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.base.json", "include": ["src"], "compilerOptions": { diff --git a/.context/effect/packages/effect/typetest/Channel.tst.ts b/.context/effect/packages/effect/typetest/Channel.tst.ts index fec89edf8..7124e9972 100644 --- a/.context/effect/packages/effect/typetest/Channel.tst.ts +++ b/.context/effect/packages/effect/typetest/Channel.tst.ts @@ -1,4 +1,4 @@ -import { Channel, Data, pipe, Result } from "effect" +import { Channel, Data, type Effect, pipe, Result } from "effect" import { describe, expect, it } from "tstyche" class ErrorA extends Data.TaggedError("ErrorA")<{ readonly message: string }> {} @@ -110,3 +110,9 @@ describe("Channel.catchReasons", () => { expect(result).type.toBe>() }) }) + +describe("Channel.runCount", () => { + it("returns the output count", () => { + expect(Channel.runCount(Channel.fromIterable([1, 2, 3]))).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/ChannelSchema.tst.ts b/.context/effect/packages/effect/typetest/ChannelSchema.tst.ts new file mode 100644 index 000000000..8946e1ab1 --- /dev/null +++ b/.context/effect/packages/effect/typetest/ChannelSchema.tst.ts @@ -0,0 +1,32 @@ +import { type Array, type Channel, ChannelSchema, Schema } from "effect" +import { expect, it } from "tstyche" + +it("decodeUnknown accepts unknown input chunks", () => { + const channel = ChannelSchema.decodeUnknown(Schema.NumberFromString)() + + expect(channel).type.toBe< + Channel.Channel< + Array.NonEmptyReadonlyArray, + Schema.SchemaError, + unknown, + Array.NonEmptyReadonlyArray, + never, + unknown + > + >() +}) + +it("decode preserves the schema encoded input type", () => { + const channel = ChannelSchema.decode(Schema.NumberFromString)() + + expect(channel).type.toBe< + Channel.Channel< + Array.NonEmptyReadonlyArray, + Schema.SchemaError, + unknown, + Array.NonEmptyReadonlyArray, + never, + unknown + > + >() +}) diff --git a/.context/effect/packages/effect/typetest/Clock.tst.ts b/.context/effect/packages/effect/typetest/Clock.tst.ts new file mode 100644 index 000000000..beb01dc80 --- /dev/null +++ b/.context/effect/packages/effect/typetest/Clock.tst.ts @@ -0,0 +1,13 @@ +import type { Effect } from "effect" +import { Clock } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Clock", () => { + it("exposes monotonic nanosecond time", () => { + expect(Clock.monotonicTimeNanos).type.toBe>() + + const clock = null as unknown as Clock.Clock + expect(clock.monotonicTimeNanosUnsafe()).type.toBe() + expect(clock.monotonicTimeNanos).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/Config.tst.ts b/.context/effect/packages/effect/typetest/Config.tst.ts index aa760eafa..b3ccab9ab 100644 --- a/.context/effect/packages/effect/typetest/Config.tst.ts +++ b/.context/effect/packages/effect/typetest/Config.tst.ts @@ -1,4 +1,4 @@ -import { Config, Schema } from "effect" +import { Config, ConfigProvider, Schema } from "effect" import { describe, expect, it } from "tstyche" describe("Config", () => { @@ -40,4 +40,13 @@ describe("Config", () => { expect(c).type.toBe>>() }) + + it("parse", () => { + const config = Config.string("a") + const provider = ConfigProvider.fromUnknown({ a: "value" }) + + config.parse(provider) + // @ts-expect-error Expected 1 arguments, but got 2. + config.parse(provider, ["prefix"]) + }) }) diff --git a/.context/effect/packages/effect/typetest/ConfigProvider.tst.ts b/.context/effect/packages/effect/typetest/ConfigProvider.tst.ts new file mode 100644 index 000000000..5a0175441 --- /dev/null +++ b/.context/effect/packages/effect/typetest/ConfigProvider.tst.ts @@ -0,0 +1,20 @@ +import { ConfigProvider, Effect } from "effect" +import { describe, expect, it } from "tstyche" + +declare const process: { readonly env: Record } + +describe("ConfigProvider", () => { + it("exposes lookup absence and input transformation", () => { + const provider = ConfigProvider.make((_path) => Effect.succeed(ConfigProvider.makeValue("value"))) + + expect(provider.load([])) + .type.toBe>() + expect(provider.mapInput((path) => path)) + .type.toBe() + }) + + it("accepts process.env as an explicit environment record", () => { + expect(ConfigProvider.fromEnvRecord(process.env)) + .type.toBe() + }) +}) diff --git a/.context/effect/packages/effect/typetest/Context.tst.ts b/.context/effect/packages/effect/typetest/Context.tst.ts new file mode 100644 index 000000000..0c1c89cc3 --- /dev/null +++ b/.context/effect/packages/effect/typetest/Context.tst.ts @@ -0,0 +1,11 @@ +import { Context, Option } from "effect" +import { expect, it } from "tstyche" + +it("does not type a service removed with addOrOmit as present", () => { + const Service = Context.Service<{ readonly value: number }>("TestService") + const context = Context.make(Service, { value: 1 }).pipe(Context.addOrOmit(Service, Option.none())) + const dataFirst = Context.addOrOmit(Context.make(Service, { value: 1 }), Service, Option.none()) + + expect(Context.get).type.not.toBeCallableWith(context, Service) + expect(Context.get).type.not.toBeCallableWith(dataFirst, Service) +}) diff --git a/.context/effect/packages/effect/typetest/Effect.tst.ts b/.context/effect/packages/effect/typetest/Effect.tst.ts index 295949125..86de6e1aa 100644 --- a/.context/effect/packages/effect/typetest/Effect.tst.ts +++ b/.context/effect/packages/effect/typetest/Effect.tst.ts @@ -5,11 +5,13 @@ import { Context, Data, Effect, + type ExecutionPlan, Fiber, type Layer, type Option, pipe, Result, + type Schedule, type Scope, type Sink, type Stream, @@ -74,6 +76,18 @@ class AcquireReleaseDependency extends Context.Service()( + "UpdateServiceScopedService" +) {} + +const UpdateServiceScopedReference = Context.Reference("UpdateServiceScopedReference", { + defaultValue: () => 0 +}) + +class ProvideServiceEffectServiceLiteral extends Context.Service()( + "ProvideServiceEffectServiceLiteral" +) {} + describe("Types", () => { describe("ReasonOf", () => { it("extracts reason type", () => { @@ -600,6 +614,34 @@ describe("Effect.annotateLogsScoped", () => { }) }) +describe("Effect.updateServiceScoped", () => { + it("adds a Context.Service to the requirements", () => { + const result = Effect.updateServiceScoped(UpdateServiceScopedService, (value) => value + 1) + expect(result).type.toBe>() + }) + + it("does not add a Context.Reference to the requirements", () => { + const result = Effect.updateServiceScoped(UpdateServiceScopedReference, (value) => value + 1) + expect(result).type.toBe>() + }) + + it("types the reset values from the service", () => { + const result = Effect.updateServiceScoped( + UpdateServiceScopedService, + (value) => value + 1, + { + reset: (original, updated, current) => { + expect(original).type.toBe() + expect(updated).type.toBe() + expect(current).type.toBe() + return current + } + } + ) + expect(result).type.toBe>() + }) +}) + describe("Effect.forkScoped", () => { it("adds Scope to requirements in data-first usage", () => { const result = pipe( @@ -1063,3 +1105,121 @@ describe("Effect.retry", () => { expect(result).type.toBe>() }) }) + +describe("Effect.schedule", () => { + it("includes schedule errors in data-first usage", () => { + const schedule = null as unknown as Schedule.Schedule + expect(Effect.schedule(Effect.fail("effect-error" as const), schedule)) + .type.toBe>() + }) + + it("includes schedule errors in data-last usage", () => { + const schedule = null as unknown as Schedule.Schedule + expect(Effect.fail("effect-error" as const).pipe(Effect.schedule(schedule))) + .type.toBe>() + }) +}) + +describe("Effect.scheduleFrom", () => { + it("includes schedule errors in data-first usage", () => { + const schedule = null as unknown as Schedule.Schedule + expect(Effect.scheduleFrom(Effect.fail("effect-error" as const), "initial", schedule)) + .type.toBe>() + }) + + it("includes schedule errors in data-last usage", () => { + const schedule = null as unknown as Schedule.Schedule + expect(Effect.fail("effect-error" as const).pipe(Effect.scheduleFrom("initial", schedule))) + .type.toBe>() + }) +}) + +describe("Effect.provideServiceEffect", () => { + it("data-first disallows supertype return", () => { + Effect.provideServiceEffect( + Effect.void, + ProvideServiceEffectServiceLiteral, + // @ts-expect-error Argument of type 'Effect' is not assignable to parameter of type 'Effect<"LITERAL", never, never>' + Effect.gen(function*() { + return "test" + }) + ) + }) + + it("data-last disallows supertype return", () => { + Effect.provideServiceEffect( + ProvideServiceEffectServiceLiteral, + // @ts-expect-error Argument of type 'Effect' is not assignable to parameter of type 'Effect<"LITERAL", never, never>' + Effect.gen(function*() { + return "test" + }) + ) + }) +}) + +describe("Effect.updateService", () => { + it("data-first disallows supertype return", () => { + Effect.updateService( + Effect.void, + ProvideServiceEffectServiceLiteral, + // @ts-expect-error Type 'string' is not assignable to type '"LITERAL"' + () => "test" + ) + }) + + it("data-last disallows supertype return", () => { + Effect.updateService( + ProvideServiceEffectServiceLiteral, + // @ts-expect-error Type 'string' is not assignable to type '"LITERAL"' + () => "test" + ) + }) +}) + +describe("Effect.updateServiceScoped", () => { + it("disallows supertype return", () => { + Effect.updateServiceScoped( + ProvideServiceEffectServiceLiteral, + // @ts-expect-error Type 'string' is not assignable to type '"LITERAL"' + () => "test" + ) + }) +}) + +describe("Effect.withExecutionPlan", () => { + const plan = null as unknown as ExecutionPlan.ExecutionPlan<{ + provides: "provided" + input: string + error: "plan-error" + requirements: "plan-dep" + }> + const self = null as unknown as Effect.Effect + + it("data-first adds handler requirements to R", () => { + const result = Effect.withExecutionPlan(self, plan, { + onEvent: (event) => { + expect(event).type.toBe>() + return null as unknown as Effect.Effect + } + }) + expect(result).type.toBe>() + }) + + it("data-last adds handler requirements to R", () => { + const result = pipe( + self, + Effect.withExecutionPlan(plan, { + onEvent: (event) => { + expect(event).type.toBe>() + return null as unknown as Effect.Effect + } + }) + ) + expect(result).type.toBe>() + }) + + it("without options the requirements are unchanged", () => { + const result = Effect.withExecutionPlan(self, plan) + expect(result).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/Fiber.tst.ts b/.context/effect/packages/effect/typetest/Fiber.tst.ts new file mode 100644 index 000000000..ffbbe1784 --- /dev/null +++ b/.context/effect/packages/effect/typetest/Fiber.tst.ts @@ -0,0 +1,13 @@ +import type { Effect } from "effect" +import { Fiber } from "effect" +import { expect, it } from "tstyche" + +it("joinAll preserves input fiber errors", () => { + const fibers = null as unknown as readonly [ + Fiber.Fiber, + Fiber.Fiber + ] + const result = Fiber.joinAll(fibers) + + expect>().type.toBe<"err-1" | "err-2">() +}) diff --git a/.context/effect/packages/effect/typetest/Function.tst.ts b/.context/effect/packages/effect/typetest/Function.tst.ts new file mode 100644 index 000000000..a7aa42230 --- /dev/null +++ b/.context/effect/packages/effect/typetest/Function.tst.ts @@ -0,0 +1,26 @@ +import { Function } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Function", () => { + it("memoize", () => { + const memoized = Function.memoize((input: { readonly n: number }) => input.n) + expect(memoized).type.toBe<(input: { readonly n: number }) => number>() + + const nullable = Function.memoize((_input: object) => null) + expect(nullable).type.toBe<(input: object) => null>() + + expect(Function.memoize).type.not.toBeCallableWith((_input: object): undefined => undefined) + expect(Function.memoize).type.not.toBeCallableWith((_input: object): number | undefined => undefined) + }) + + it("memoizeIdempotent", () => { + const memoized = Function.memoizeIdempotent((input: { readonly n: number }) => input) + expect(memoized).type.toBe<(input: { readonly n: number }) => { readonly n: number }>() + + const generic = Function.memoizeIdempotent((input: A): A => input) + type Generic = (input: A) => A + expect(generic).type.toBe() + + expect(Function.memoizeIdempotent).type.not.toBeCallableWith((_input: object): number => 1) + }) +}) diff --git a/.context/effect/packages/effect/typetest/Optic.tst.ts b/.context/effect/packages/effect/typetest/Optic.tst.ts index af61413df..10752fe77 100644 --- a/.context/effect/packages/effect/typetest/Optic.tst.ts +++ b/.context/effect/packages/effect/typetest/Optic.tst.ts @@ -1,8 +1,59 @@ -import { Optic, Schema } from "effect" -import type { Option, Result } from "effect" +import { Optic, type Option, Result, Schema, type SchemaIssue } from "effect" import { describe, expect, it } from "tstyche" describe("Optic", () => { + describe("compose", () => { + const iso = Optic.makeIso((n) => n, (n) => n) + const lens = Optic.makeLens((n) => n, (n) => n) + const prism = Optic.makePrism(Result.succeed, (n) => n) + const optional = Optic.makeOptional(Result.succeed, (n) => Result.succeed(n)) + + it("preserves the optic kind matrix", () => { + expect(iso.compose(iso)).type.toBe>() + expect(iso.compose(lens)).type.toBe>() + expect(iso.compose(prism)).type.toBe>() + expect(iso.compose(optional)).type.toBe>() + + expect(lens.compose(iso)).type.toBe>() + expect(lens.compose(lens)).type.toBe>() + expect(lens.compose(prism)).type.toBe>() + expect(lens.compose(optional)).type.toBe>() + + expect(prism.compose(iso)).type.toBe>() + expect(prism.compose(lens)).type.toBe>() + expect(prism.compose(prism)).type.toBe>() + expect(prism.compose(optional)).type.toBe>() + + expect(optional.compose(iso)).type.toBe>() + expect(optional.compose(lens)).type.toBe>() + expect(optional.compose(prism)).type.toBe>() + expect(optional.compose(optional)).type.toBe>() + }) + + it("does not expose internal nodes", () => { + expect(iso).type.not.toHaveProperty("node") + expect(lens).type.not.toHaveProperty("node") + expect(prism).type.not.toHaveProperty("node") + expect(optional).type.not.toHaveProperty("node") + }) + + it("uses structured issues for failures", () => { + expect(prism.getResult).type.toBe<(source: number) => Result.Result>() + expect(optional.getResult).type.toBe<(source: number) => Result.Result>() + expect(optional.replaceResult) + .type.toBe<(value: number, source: number) => Result.Result>() + + expect(Optic.makePrism).type.not.toBeCallableWith( + (_: number) => Result.fail("failure"), + (n: number) => n + ) + expect(Optic.makeOptional).type.not.toBeCallableWith( + (_: number) => Result.fail("failure"), + (n: number) => Result.succeed(n) + ) + }) + }) + describe("key", () => { it("should not be allowed on union types", () => { type S = { readonly _tag: "A"; readonly a?: string } | { readonly _tag: "B"; readonly a?: number } @@ -94,9 +145,17 @@ describe("Optic", () => { }) }) - it("notUndefined", () => { - const optic = Optic.id().notUndefined() - expect(optic).type.toBe>() + describe("notUndefined", () => { + it("Prism", () => { + const optic = Optic.id().notUndefined() + expect(optic).type.toBe>() + }) + + it("Optional", () => { + type S = Record + const optic = Optic.id().at("a").notUndefined() + expect(optic).type.toBe>() + }) }) it("fromChecks", () => { diff --git a/.context/effect/packages/effect/typetest/Sink.tst.ts b/.context/effect/packages/effect/typetest/Sink.tst.ts new file mode 100644 index 000000000..4ed4dd6b3 --- /dev/null +++ b/.context/effect/packages/effect/typetest/Sink.tst.ts @@ -0,0 +1,13 @@ +import { Effect, Sink } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Sink", () => { + it("curried catch replaces the handled error type", () => { + const sink = null as unknown as Sink.Sink + const recovered = Sink.catch<"old-error", never, "new-error", never>( + (_) => Effect.fail("new-error" as const) + )(sink) + + expect(recovered).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/Stream.tst.ts b/.context/effect/packages/effect/typetest/Stream.tst.ts index 13454bb51..ebabd421a 100644 --- a/.context/effect/packages/effect/typetest/Stream.tst.ts +++ b/.context/effect/packages/effect/typetest/Stream.tst.ts @@ -1,4 +1,4 @@ -import { type Cause, Data, type Effect, pipe, type Queue, Result, type Scope, Stream } from "effect" +import { type Cause, Data, type Effect, type ExecutionPlan, pipe, type Queue, Result, type Scope, Stream } from "effect" import { describe, expect, it } from "tstyche" class ErrorA extends Data.TaggedError("ErrorA")<{ @@ -195,3 +195,41 @@ describe("Stream.toQueue", () => { >() }) }) + +describe("Stream.withExecutionPlan", () => { + const plan = null as unknown as ExecutionPlan.ExecutionPlan<{ + provides: "provided" + input: string + error: "plan-error" + requirements: "plan-dep" + }> + const self = null as unknown as Stream.Stream + + it("data-first adds handler requirements to R", () => { + const result = Stream.withExecutionPlan(self, plan, { + onEvent: (event) => { + expect(event).type.toBe>() + return null as unknown as Effect.Effect + } + }) + expect(result).type.toBe>() + }) + + it("data-last adds handler requirements to R", () => { + const result = pipe( + self, + Stream.withExecutionPlan(plan, { + onEvent: (event) => { + expect(event).type.toBe>() + return null as unknown as Effect.Effect + } + }) + ) + expect(result).type.toBe>() + }) + + it("without options the requirements are unchanged", () => { + const result = Stream.withExecutionPlan(self, plan) + expect(result).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/TestSchema.tst.ts b/.context/effect/packages/effect/typetest/TestSchema.tst.ts new file mode 100644 index 000000000..4ed7ccc1d --- /dev/null +++ b/.context/effect/packages/effect/typetest/TestSchema.tst.ts @@ -0,0 +1,12 @@ +import type { Effect, SchemaIssue } from "effect" +import { Schema } from "effect" +import { TestSchema } from "effect/testing" +import { describe, expect, it } from "tstyche" + +describe("TestSchema", () => { + it("types Encoding.encodeUnknownEffect with the encoded output", () => { + const encoding = new TestSchema.Asserts(Schema.NumberFromString).encoding() + + expect(encoding.encodeUnknownEffect(1)).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/Tuple.tst.ts b/.context/effect/packages/effect/typetest/Tuple.tst.ts index 75bd47260..4c3922760 100644 --- a/.context/effect/packages/effect/typetest/Tuple.tst.ts +++ b/.context/effect/packages/effect/typetest/Tuple.tst.ts @@ -7,6 +7,10 @@ const tuple = ["a", 2, true] as [string, number, boolean] const optionalTuple = ["a", 2, true] as [string?, number?, boolean?] describe("Tuple", () => { + it("make preserves argument literals", () => { + expect(Tuple.make("a", 1, true)).type.toBe<["a", 1, true]>() + }) + describe("get", () => { it("errors", () => { pipe( @@ -54,6 +58,10 @@ describe("Tuple", () => { it("data-last", () => { expect(pipe(tuple, Tuple.pick([0, 2]))).type.toBe<[string, boolean]>() }) + + it("preserves index order and duplicates", () => { + expect(Tuple.pick(tuple, [2, 0, 2])).type.toBe<[boolean, string, boolean]>() + }) }) describe("omit", () => { diff --git a/.context/effect/packages/effect/typetest/VariantSchema.tst.ts b/.context/effect/packages/effect/typetest/VariantSchema.tst.ts index 6f3d8db35..5d0233c25 100644 --- a/.context/effect/packages/effect/typetest/VariantSchema.tst.ts +++ b/.context/effect/packages/effect/typetest/VariantSchema.tst.ts @@ -26,9 +26,36 @@ describe("VariantSchema", () => { const second = Test.Struct({ value: Test.FieldOnly(["a", "b"])(Schema.Number) }) + const union = Test.Union([first, second]) expect(Test.Union).type.toBeCallableWith([first, second]) expect(Test.Union).type.not.toBeCallableWith(first, second) + expect>().type.toBe< + { readonly value: string } | { readonly value: number } + >() + }) + + it("Class preserves constructor and variant schema types", () => { + const Test = VariantSchema.make({ + variants: ["a", "b"], + defaultVariant: "a" + }) + class User extends Test.Class("User")({ + id: Test.FieldOnly(["a"])(Schema.Number), + name: Schema.String + }) {} + + expect(User).type.toBeConstructableWith({ id: 1, name: "Alice" }) + expect(User).type.not.toBeConstructableWith({ name: "Alice" }) + expect(User.make({ id: 1, name: "Alice" })).type.toBe() + expect>().type.toBe() + expect>().type.toBe< + { readonly id: number; readonly name: string } + >() + expect>().type.toBe< + { readonly id: number; readonly name: string } + >() + expect>().type.toBe<{ readonly name: string }>() }) }) diff --git a/.context/effect/packages/effect/typetest/schema/FromJsonSchema.tst.ts b/.context/effect/packages/effect/typetest/schema/FromJsonSchema.tst.ts new file mode 100644 index 000000000..ccb7c5d2a --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/FromJsonSchema.tst.ts @@ -0,0 +1,49 @@ +import { type JsonSchema, type Schema, SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("JSON Schema importer", () => { + it("exposes exact synchronous signatures", () => { + const fromDocument: ( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions + ) => Schema.Top = SchemaRepresentation.fromJsonSchemaDocument + const fromMultiDocument: ( + document: JsonSchema.MultiDocument<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions + ) => readonly [Schema.Top, ...Array] = SchemaRepresentation.fromJsonSchemaMultiDocument + expect(fromDocument).type.toBe< + ( + document: JsonSchema.Document<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions + ) => Schema.Top + >() + expect(fromMultiDocument).type.toBe< + ( + document: JsonSchema.MultiDocument<"draft-2020-12">, + options?: SchemaRepresentation.FromJsonSchemaOptions + ) => readonly [Schema.Top, ...Array] + >() + }) + + it("keeps onEnter limited to JSON Schema nodes", () => { + const options: SchemaRepresentation.FromJsonSchemaOptions = { + onEnter: (schema) => ({ ...schema, description: "entered" }) + } + + expect(options.onEnter).type.toBe< + ((schema: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined + >() + }) + + it("limits the pattern policy to the supported modes", () => { + const options: SchemaRepresentation.FromJsonSchemaOptions = { patterns: "error" } + + expect(options.patterns).type.toBe<"error" | "ignore" | "apply" | undefined>() + + const invalidOptions: SchemaRepresentation.FromJsonSchemaOptions = { + // @ts-expect-error Type '"safe"' is not assignable to type + patterns: "safe" + } + void invalidOptions + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/Schema.tst.ts b/.context/effect/packages/effect/typetest/schema/Schema.tst.ts index ff42b15ed..5afd42204 100644 --- a/.context/effect/packages/effect/typetest/schema/Schema.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Schema.tst.ts @@ -1,4 +1,3 @@ -import type { SchemaAST } from "effect" import { Brand, Context, @@ -7,7 +6,9 @@ import { Option, Predicate, Schema, + type SchemaAST, SchemaGetter, + type SchemaIssue, SchemaTransformation, Struct, Tuple @@ -19,13 +20,19 @@ type Make = (input: In, options?: Schema.MakeOptions | undefined) => Ou type MakeEffect = ( input: In, options?: Schema.MakeOptions | undefined -) => Effect.Effect +) => Effect.Effect const revealClass = , Inherited>( klass: Schema.Class ): Schema.Class => klass describe("Schema", () => { + it("RedactedFromValue", () => { + const schema = Schema.RedactedFromValue(Schema.String) + expect(schema).type.toBe>() + expect(schema.from).type.toBe() + }) + describe("variance", () => { it("Type", () => { const f1 = hole< @@ -332,16 +339,16 @@ describe("Schema", () => { }) }) - describe("ErrorClass", () => { + describe("Error", () => { it("make with void input", () => { - class E extends Schema.ErrorClass("E")({}) {} + class E extends Schema.Error("E")({}) {} expect(E.make).type.toBe>() }) }) - describe("TaggedErrorClass", () => { + describe("TaggedError", () => { it("make with void input", () => { - class E extends Schema.TaggedErrorClass()("E", {}) {} + class E extends Schema.TaggedError()("E", {}) {} expect(E.make).type.toBe>() }) }) @@ -1435,7 +1442,7 @@ describe("Schema", () => { describe("Error", () => { it("extend Fields", () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ a: Schema.String }) {} @@ -1449,7 +1456,7 @@ describe("Schema", () => { }) it("extend Struct", () => { - class E extends Schema.ErrorClass("E")(Schema.Struct({ + class E extends Schema.Error("E")(Schema.Struct({ a: Schema.String })) {} @@ -1463,7 +1470,7 @@ describe("Schema", () => { }) it("should reject non existing props", () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ a: Schema.String }) {} @@ -1472,7 +1479,7 @@ describe("Schema", () => { }) it("mutable field", () => { - class E extends Schema.ErrorClass("E")({ + class E extends Schema.Error("E")({ a: Schema.String.pipe(Schema.mutableKey) }) {} @@ -1500,15 +1507,15 @@ describe("Schema", () => { ) }) - it("ErrorClass", () => { - expect(Schema.ErrorClass("A")({})).type.toBe( - "Missing `Self` generic - use `class Self extends Schema.ErrorClass(...)`" + it("Error", () => { + expect(Schema.Error("A")({})).type.toBe( + "Missing `Self` generic - use `class Self extends Schema.Error(...)`" ) }) - it("TaggedErrorClass", () => { - expect(Schema.TaggedErrorClass("A")("A", {})).type.toBe( - "Missing `Self` generic - use `class Self extends Schema.TaggedErrorClass(...)`" + it("TaggedError", () => { + expect(Schema.TaggedError("A")("A", {})).type.toBe( + "Missing `Self` generic - use `class Self extends Schema.TaggedError(...)`" ) }) }) @@ -1846,26 +1853,115 @@ describe("Schema", () => { }) }) - describe("asClass", () => { - it("preserves schema Type", () => { - class A extends Schema.asClass(Schema.String) {} - expect(Schema.revealCodec(A)).type.toBe>() + describe("class extension", () => { + it("keeps protocol bases constructor-free", () => { + const bottomWithoutNew: Schema.BottomWithoutNew< + string, + string, + never, + never, + (typeof Schema.String)["ast"], + Schema.String + > = Schema.String + expect(bottomWithoutNew).type.not.toBeAssignableTo< + abstract new(...args: Array) => unknown + >() + + const struct = Schema.Struct({ name: Schema.String }) + const bottomLazyWithoutNew: Schema.BottomLazyWithoutNew< + (typeof struct)["ast"], + (typeof struct)["Rebuild"] + > = struct + expect(bottomLazyWithoutNew).type.not.toBeAssignableTo< + abstract new(...args: Array) => unknown + >() + }) + + it("keeps class-compatible bases extendable", () => { + const bottom: Schema.Bottom< + string, + string, + never, + never, + (typeof Schema.String)["ast"], + Schema.String + > = Schema.String + class A extends bottom {} + + const struct = Schema.Struct({ name: Schema.String }) + const bottomLazy: Schema.BottomLazy< + (typeof struct)["ast"], + (typeof struct)["Rebuild"] + > = struct + class B extends bottomLazy {} + + expect(Schema.revealCodec(A)).type.toBe>() + expect(B).type.toBeAssignableTo() + }) + + it("keeps Opaque assignable to Top", () => { + class A extends Schema.Opaque()(Schema.Struct({ name: Schema.String })) {} + + expect(A).type.toBeAssignableTo() + }) + + it("keeps Schema.Class assignable to Top", () => { + class A extends Schema.Class("A")({ name: Schema.String }) {} - class B extends Schema.asClass(Schema.Struct({ name: Schema.String })) {} - expect(Schema.revealCodec(B)).type.toBe< - Schema.Codec<{ readonly name: string }, { readonly name: string }, never, never> + expect(A).type.toBeAssignableTo() + }) + + it("preserves codec parameters", () => { + interface DecodingService { + readonly DecodingService: unique symbol + } + interface EncodingService { + readonly EncodingService: unique symbol + } + + const schema = Schema.FiniteFromString.pipe( + Schema.middlewareDecoding((effect) => + Effect.andThen( + Effect.context(), + effect + ) + ), + Schema.middlewareEncoding((effect) => + Effect.andThen( + Effect.context(), + effect + ) + ) + ) + + class A extends schema {} + + expect(Schema.revealCodec(A)).type.toBe< + Schema.Codec >() + }) + + it("preserves Struct fields", () => { + class B extends Schema.Struct({ name: Schema.String }) {} + expect(B.fields).type.toBe<{ readonly name: Schema.String }>() }) + it("cannot be constructed", () => { + class A extends Schema.String {} + + expect(A).type.not.toBeConstructableWith() + expect(A).type.not.toBeConstructableWith("a") + }) + it("annotate returns the original schema type", () => { - class A extends Schema.asClass(Schema.String) {} + class A extends Schema.String {} expect(A.annotate({})).type.toBe() }) it("should support static methods", () => { - class A extends Schema.asClass(Schema.FiniteFromString) { + class A extends Schema.FiniteFromString { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) static get encodeSync() { return Schema.encodeSync(this) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInAtomicDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInAtomicDeclarationRevivers.tst.ts new file mode 100644 index 000000000..6eb183e48 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInAtomicDeclarationRevivers.tst.ts @@ -0,0 +1,21 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in atomic declaration revivers", () => { + it("composes every atomic declaration reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.DateReviver, + Schema.FileReviver, + Schema.FormDataReviver, + Schema.RegExpReviver, + Schema.Uint8ArrayReviver, + Schema.URLReviver, + Schema.URLSearchParamsReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.DateReviver).type.toBe>() + expect(Schema.FileReviver).type.toBe>() + expect(Schema.FormDataReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigDecimalDurationChunkDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigDecimalDurationChunkDeclarationRevivers.tst.ts new file mode 100644 index 000000000..3d3972be6 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigDecimalDurationChunkDeclarationRevivers.tst.ts @@ -0,0 +1,17 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in BigDecimal, Duration and Chunk declaration revivers", () => { + it("composes every declaration reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.BigDecimalReviver, + Schema.DurationReviver, + Schema.ChunkReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.BigDecimalReviver).type.toBe>() + expect(Schema.DurationReviver).type.toBe>() + expect(Schema.ChunkReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigIntRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigIntRevivers.tst.ts new file mode 100644 index 000000000..dd3c0af3f --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInBigIntRevivers.tst.ts @@ -0,0 +1,27 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in BigInt revivers", () => { + it("composes every BigInt check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isGreaterThanBigIntReviver, + Schema.isGreaterThanOrEqualToBigIntReviver, + Schema.isLessThanBigIntReviver, + Schema.isLessThanOrEqualToBigIntReviver, + Schema.isBetweenBigIntReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isGreaterThanBigIntReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly exclusiveMinimum: bigint }> + >() + expect(Schema.isBetweenBigIntReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly minimum: bigint + readonly maximum: bigint + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined + }> + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCauseAndExitDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCauseAndExitDeclarationRevivers.tst.ts new file mode 100644 index 000000000..f9ee5e1c5 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCauseAndExitDeclarationRevivers.tst.ts @@ -0,0 +1,17 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in Cause and Exit declaration revivers", () => { + it("exposes exact null payload reviver types", () => { + const revivers: ReadonlyArray = [ + Schema.CauseReasonReviver, + Schema.CauseReviver, + Schema.ExitReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.CauseReasonReviver).type.toBe>() + expect(Schema.CauseReviver).type.toBe>() + expect(Schema.ExitReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCollectionRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCollectionRevivers.tst.ts new file mode 100644 index 000000000..00e5e3563 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInCollectionRevivers.tst.ts @@ -0,0 +1,25 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in collection revivers", () => { + it("composes every collection check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isMinSizeReviver, + Schema.isMaxSizeReviver, + Schema.isSizeBetweenReviver, + Schema.isUniqueReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isMinSizeReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly minSize: number }> + >() + expect(Schema.isSizeBetweenReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number + }> + >() + expect(Schema.isUniqueReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateRevivers.tst.ts new file mode 100644 index 000000000..7fc656de9 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateRevivers.tst.ts @@ -0,0 +1,27 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in Date revivers", () => { + it("composes every Date check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isGreaterThanDateReviver, + Schema.isGreaterThanOrEqualToDateReviver, + Schema.isLessThanDateReviver, + Schema.isLessThanOrEqualToDateReviver, + Schema.isBetweenDateReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isGreaterThanDateReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly exclusiveMinimum: globalThis.Date }> + >() + expect(Schema.isBetweenDateReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly minimum: globalThis.Date + readonly maximum: globalThis.Date + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined + }> + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateTimeDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateTimeDeclarationRevivers.tst.ts new file mode 100644 index 000000000..a9d877f9e --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInDateTimeDeclarationRevivers.tst.ts @@ -0,0 +1,21 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in DateTime declaration revivers", () => { + it("composes every DateTime declaration reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.TimeZoneReviver, + Schema.TimeZoneNamedReviver, + Schema.TimeZoneOffsetReviver, + Schema.DateTimeUtcReviver, + Schema.DateTimeZonedReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.TimeZoneReviver).type.toBe>() + expect(Schema.TimeZoneNamedReviver).type.toBe>() + expect(Schema.TimeZoneOffsetReviver).type.toBe>() + expect(Schema.DateTimeUtcReviver).type.toBe>() + expect(Schema.DateTimeZonedReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInErrorAndCollectionDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInErrorAndCollectionDeclarationRevivers.tst.ts new file mode 100644 index 000000000..aea602808 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInErrorAndCollectionDeclarationRevivers.tst.ts @@ -0,0 +1,25 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in Error and collection declaration revivers", () => { + it("exposes exact payload and declaration reviver types", () => { + const revivers: ReadonlyArray = [ + Schema.ErrorInstanceReviver, + Schema.ReadonlyMapReviver, + Schema.ReadonlySetReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.ErrorInstanceReviver).type.toBe< + SchemaRepresentation.DeclarationReviver< + | null + | { + readonly includeStack?: true | undefined + readonly excludeCause?: true | undefined + } + > + >() + expect(Schema.ReadonlyMapReviver).type.toBe>() + expect(Schema.ReadonlySetReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInJsonAndHashDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInJsonAndHashDeclarationRevivers.tst.ts new file mode 100644 index 000000000..4afae11c8 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInJsonAndHashDeclarationRevivers.tst.ts @@ -0,0 +1,19 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in JSON and hash collection declaration revivers", () => { + it("exposes exact null payload reviver types", () => { + const revivers: ReadonlyArray = [ + Schema.JsonReviver, + Schema.MutableJsonReviver, + Schema.HashMapReviver, + Schema.HashSetReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.JsonReviver).type.toBe>() + expect(Schema.MutableJsonReviver).type.toBe>() + expect(Schema.HashMapReviver).type.toBe>() + expect(Schema.HashSetReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInNumberRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInNumberRevivers.tst.ts new file mode 100644 index 000000000..4b8c6ae2d --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInNumberRevivers.tst.ts @@ -0,0 +1,33 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in number revivers", () => { + it("composes every number check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isFiniteReviver, + Schema.isIntReviver, + Schema.isMultipleOfReviver, + Schema.isGreaterThanReviver, + Schema.isGreaterThanOrEqualToReviver, + Schema.isLessThanReviver, + Schema.isLessThanOrEqualToReviver, + Schema.isBetweenReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isMultipleOfReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly divisor: number }> + >() + expect(Schema.isGreaterThanReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly exclusiveMinimum: number }> + >() + expect(Schema.isBetweenReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number + readonly exclusiveMinimum?: true | undefined + readonly exclusiveMaximum?: true | undefined + }> + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInObjectRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInObjectRevivers.tst.ts new file mode 100644 index 000000000..917ff9f33 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInObjectRevivers.tst.ts @@ -0,0 +1,25 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in object revivers", () => { + it("composes every object check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isMinPropertiesReviver, + Schema.isMaxPropertiesReviver, + Schema.isPropertiesLengthBetweenReviver, + Schema.isPropertyNamesReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isMinPropertiesReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly minProperties: number }> + >() + expect(Schema.isPropertiesLengthBetweenReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly minimum: number + readonly maximum: number + }> + >() + expect(Schema.isPropertyNamesReviver).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInResultAndRedactedDeclarationRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInResultAndRedactedDeclarationRevivers.tst.ts new file mode 100644 index 000000000..02662ca51 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInResultAndRedactedDeclarationRevivers.tst.ts @@ -0,0 +1,23 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in Result and Redacted declaration revivers", () => { + it("exposes exact payload and declaration reviver types", () => { + const revivers: ReadonlyArray = [ + Schema.ResultReviver, + Schema.RedactedReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.ResultReviver).type.toBe>() + expect(Schema.RedactedReviver).type.toBe< + SchemaRepresentation.DeclarationReviver< + | null + | { + readonly label?: string | undefined + readonly disallowJsonEncode?: true | undefined + } + > + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInRevivers.tst.ts new file mode 100644 index 000000000..cd10d73a5 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInRevivers.tst.ts @@ -0,0 +1,17 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in revivers", () => { + it("exports the isPattern and Option revivers with concrete payload types", () => { + expect(Schema.isPatternReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly source: string; readonly flags: string }> + >() + expect(Schema.OptionReviver).type.toBe>() + + const revivers: ReadonlyArray = [ + Schema.isPatternReviver, + Schema.OptionReviver + ] + expect(revivers).type.toBe>() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaBuiltInStringRevivers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInStringRevivers.tst.ts new file mode 100644 index 000000000..768db4cd7 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaBuiltInStringRevivers.tst.ts @@ -0,0 +1,42 @@ +import { Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema built-in string revivers", () => { + it("composes every string check reviver without casts", () => { + const revivers: ReadonlyArray = [ + Schema.isStringFiniteReviver, + Schema.isStringBigIntReviver, + Schema.isStringSymbolReviver, + Schema.isMinLengthReviver, + Schema.isMaxLengthReviver, + Schema.isLengthBetweenReviver, + Schema.isPatternReviver, + Schema.isTrimmedReviver, + Schema.isUUIDReviver, + Schema.isGUIDReviver, + Schema.isULIDReviver, + Schema.isBase64Reviver, + Schema.isBase64UrlReviver, + Schema.isStartsWithReviver, + Schema.isEndsWithReviver, + Schema.isIncludesReviver, + Schema.isUppercasedReviver, + Schema.isLowercasedReviver, + Schema.isCapitalizedReviver, + Schema.isUncapitalizedReviver + ] + + expect(revivers).type.toBe>() + expect(Schema.isMinLengthReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly minLength: number }> + >() + expect(Schema.isLengthBetweenReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ readonly minimum: number; readonly maximum: number }> + >() + expect(Schema.isUUIDReviver).type.toBe< + SchemaRepresentation.FilterReviver<{ + readonly version: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | null + }> + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaJsonSchemaConsumer.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaJsonSchemaConsumer.tst.ts new file mode 100644 index 000000000..fbf869cd1 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaJsonSchemaConsumer.tst.ts @@ -0,0 +1,22 @@ +import { type JsonSchema, Schema, type SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("Schema JSON Schema consumer", () => { + it("exposes exact synchronous high-level signatures", () => { + const toJsonSchema: ( + schema: Schema.Constraint, + options?: Schema.ToJsonSchemaOptions + ) => JsonSchema.Document<"draft-2020-12"> = Schema.toJsonSchemaDocument + + expect(Schema.toRepresentation(Schema.String)).type.toBe< + SchemaRepresentation.Document + >() + expect(Schema.toJsonSchemaDocument(Schema.String)).type.toBe>() + expect(toJsonSchema).type.toBe< + ( + schema: Schema.Constraint, + options?: Schema.ToJsonSchemaOptions + ) => JsonSchema.Document<"draft-2020-12"> + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaRepresentation.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaRepresentation.tst.ts new file mode 100644 index 000000000..6040e7abc --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaRepresentation.tst.ts @@ -0,0 +1,91 @@ +import { type Schema, type SchemaAST, SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("SchemaRepresentation persisted wire", () => { + it("exposes exact construction signatures", () => { + expect(SchemaRepresentation.toRepresentation).type.toBe< + (ast: SchemaAST.AST) => SchemaRepresentation.Document + >() + expect(SchemaRepresentation.toRepresentations).type.toBe< + ( + asts: readonly [SchemaAST.AST, ...Array] + ) => SchemaRepresentation.MultiDocument + >() + }) + + it("keeps projection explicit for single and multi documents", () => { + expect(SchemaRepresentation.toJson).type.toBe< + (document: SchemaRepresentation.Document) => Schema.Json + >() + expect(SchemaRepresentation.toJsonMultiDocument).type.toBe< + (document: SchemaRepresentation.MultiDocument) => Schema.Json + >() + }) + + it("wraps a document as a multi-document", () => { + expect(SchemaRepresentation.toMultiDocument).type.toBe< + (document: SchemaRepresentation.Document) => SchemaRepresentation.MultiDocument + >() + }) + + it("constructs generated code", () => { + expect(SchemaRepresentation.makeCode).type.toBe< + (runtime: string, Type: string) => SchemaRepresentation.Code + >() + }) + + it("keeps representation metadata separate from annotations", () => { + const declaration = null as unknown as SchemaRepresentation.Declaration + const filter = null as unknown as SchemaRepresentation.Filter + const group = null as unknown as SchemaRepresentation.FilterGroup + + expect(declaration.representation).type.toBe< + SchemaRepresentation.RepresentationAnnotation | undefined + >() + expect(filter.representation).type.toBe< + SchemaRepresentation.CheckRepresentationAnnotation | undefined + >() + expect(group.representation).type.toBe< + SchemaRepresentation.CheckRepresentationAnnotation | undefined + >() + }) + + it("uses native literal values", () => { + const literal = null as unknown as SchemaRepresentation.Literal + expect(literal.literal).type.toBe() + + expect( + { + _tag: "Literal", + literal: "value", + checks: [] + } as const + ).type.toBeAssignableTo() + + expect( + { + _tag: "Literal", + literal: { type: "string", value: "value" }, + checks: [] + } as const + ).type.not.toBeAssignableTo() + }) + + it("uses native Enum values", () => { + const representation = null as unknown as SchemaRepresentation.Enum + expect(representation.enums).type.toBe>() + expect([["A", 1]] as const).type.toBeAssignableTo() + expect([["A", { type: "number", value: 1 }]] as const).type.not.toBeAssignableTo< + SchemaRepresentation.Enum["enums"] + >() + }) + + it("uses native property keys", () => { + const property = null as unknown as SchemaRepresentation.PropertySignature + expect(property.name).type.toBe() + expect(Symbol.for("key")).type.toBeAssignableTo() + expect({ type: "symbol", value: Symbol.for("key") } as const).type.not.toBeAssignableTo< + SchemaRepresentation.PropertySignature["name"] + >() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaRepresentationCompilers.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaRepresentationCompilers.tst.ts new file mode 100644 index 000000000..a30c66a24 --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaRepresentationCompilers.tst.ts @@ -0,0 +1,49 @@ +import { type JsonSchema, Schema, SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("SchemaRepresentation compilers", () => { + it("exposes exact compiler signatures", () => { + expect(SchemaRepresentation.toJsonSchemaDocument).type.toBe< + ( + document: SchemaRepresentation.Document, + options?: Schema.ToJsonSchemaOptions + ) => JsonSchema.Document<"draft-2020-12"> + >() + expect(SchemaRepresentation.toJsonSchemaMultiDocument).type.toBe< + ( + document: SchemaRepresentation.MultiDocument, + options?: Schema.ToJsonSchemaOptions + ) => JsonSchema.MultiDocument<"draft-2020-12"> + >() + expect(SchemaRepresentation.toCodeDocument).type.toBe< + (document: SchemaRepresentation.MultiDocument) => SchemaRepresentation.CodeDocument + >() + }) + + it("distinguishes live compiler inputs and their outputs", () => { + const document = SchemaRepresentation.toRepresentation(Schema.String.ast) + const multiDocument = SchemaRepresentation.toRepresentations([Schema.String.ast]) + + expect(SchemaRepresentation.toJsonSchemaDocument(document)).type.toBe< + JsonSchema.Document<"draft-2020-12"> + >() + expect(SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument)).type.toBe< + JsonSchema.MultiDocument<"draft-2020-12"> + >() + expect(SchemaRepresentation.toCodeDocument(multiDocument)).type.toBe< + SchemaRepresentation.CodeDocument + >() + }) + + it("exposes node code generation through toCode annotations", () => { + const declaration: Schema.Annotations.Declaration = { + toCode: () => ({ runtime: "Custom", Type: "unknown" }) + } + const filter: Schema.Annotations.Filter = { + toCode: () => ({ runtime: "Custom.check()" }) + } + + expect(declaration.toCode).type.toBe() + expect(filter.toCode).type.toBe() + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/SchemaRepresentationReviver.tst.ts b/.context/effect/packages/effect/typetest/schema/SchemaRepresentationReviver.tst.ts new file mode 100644 index 000000000..39273f44e --- /dev/null +++ b/.context/effect/packages/effect/typetest/schema/SchemaRepresentationReviver.tst.ts @@ -0,0 +1,77 @@ +import { Schema, SchemaRepresentation } from "effect" +import { describe, expect, it } from "tstyche" + +describe("SchemaRepresentation revivers", () => { + it("infers payload types from reviver constructors", () => { + const declaration = SchemaRepresentation.makeDeclarationReviver( + "acme/schema/Box", + Schema.Struct({ label: Schema.String }), + ({ payload }) => { + expect(payload).type.toBe<{ readonly label: string }>() + return Schema.String + } + ) + expect(declaration).type.toBe>() + + const filter = SchemaRepresentation.makeFilterReviver( + "acme/schema/minLength", + Schema.Struct({ minimum: Schema.Number }), + ({ annotations, payload }) => { + expect(payload).type.toBe<{ readonly minimum: number }>() + return Schema.isMinLength(payload.minimum, annotations) + } + ) + expect(filter).type.toBe>() + + const filterGroup = SchemaRepresentation.makeFilterGroupReviver( + "acme/schema/nonEmpty", + Schema.Null, + ({ annotations, payload }) => { + expect(payload).type.toBe() + return Schema.makeFilterGroup([Schema.isMinLength(1)], annotations) + } + ) + expect(filterGroup).type.toBe>() + }) + + it("accepts concrete payload revivers at the erased collection boundary", () => { + const reviver: SchemaRepresentation.FilterReviver<{ readonly source: string }> = { + id: "acme/schema/isPattern", + payloadSchema: Schema.Struct({ source: Schema.String }), + revive: ({ payload, annotations }) => Schema.isPattern(new RegExp(payload.source), annotations) + } + const revivers: ReadonlyArray = [reviver] + + expect(revivers).type.toBe>() + }) + + it("separates JSON decoding from schema revival", () => { + expect(SchemaRepresentation.fromJson).type.toBe< + (input: Schema.Json) => SchemaRepresentation.Document + >() + expect(SchemaRepresentation.fromJsonMultiDocument).type.toBe< + (input: Schema.Json) => SchemaRepresentation.MultiDocument + >() + expect(SchemaRepresentation.fromRepresentation).type.toBe< + ( + document: SchemaRepresentation.Document, + options: { readonly revivers: ReadonlyArray } + ) => Schema.Top + >() + expect(SchemaRepresentation.fromRepresentations).type.toBe< + ( + document: SchemaRepresentation.MultiDocument, + options: { readonly revivers: ReadonlyArray } + ) => readonly [Schema.Top, ...Array] + >() + + const document = SchemaRepresentation.fromJson({ representation: { _tag: "String", checks: [] }, references: {} }) + // @ts-expect-error Expected 2 arguments, but got 1. + SchemaRepresentation.fromRepresentation(document) + // @ts-expect-error Expected 2 arguments, but got 1. + SchemaRepresentation.fromRepresentations({ + representations: [{ _tag: "String", checks: [] }], + references: {} + }) + }) +}) diff --git a/.context/effect/packages/effect/typetest/schema/Struct.tst.ts b/.context/effect/packages/effect/typetest/schema/Struct.tst.ts index 8264b65d8..ea6d95390 100644 --- a/.context/effect/packages/effect/typetest/schema/Struct.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/Struct.tst.ts @@ -62,6 +62,15 @@ describe("Struct", () => { void [type, encoded, iso] }) + it("simplifies readonly & required make input", () => { + const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }) + + // @ts-expect-error Type '{ readonly a: string; readonly b: number; }' + const makeIn: never = null as unknown as Schema.Struct.MakeIn + + void makeIn + }) + it("readonly & optionalKey field", () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String) diff --git a/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts b/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts index afa0d0175..6a5c0e7eb 100644 --- a/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/toArbitrary.tst.ts @@ -11,30 +11,13 @@ describe("toArbitrary", () => { const arbitrary = Schema.toArbitrary(schema) expect(arbitrary).type.toBe< - FastCheck.Arbitrary<{ + Schema.Arbitrary<{ readonly name: string readonly age: number }> >() }) - it("returns a report when requested", () => { - const schema = Schema.Struct({ - name: Schema.String, - age: Schema.Number - }) - const result = Schema.toArbitrary(schema, { report: true }) - - expect(result).type.toBe< - Schema.Annotations.ToArbitrary.WithReport< - FastCheck.Arbitrary<{ - readonly name: string - readonly age: number - }> - > - >() - }) - it("passes recursion metadata in the arbitrary context", () => { Schema.String.annotate({ toArbitrary: () => (fc, context) => { diff --git a/.context/effect/packages/effect/typetest/schema/toIso.tst.ts b/.context/effect/packages/effect/typetest/schema/toIso.tst.ts index 6dec45c8e..86516741c 100644 --- a/.context/effect/packages/effect/typetest/schema/toIso.tst.ts +++ b/.context/effect/packages/effect/typetest/schema/toIso.tst.ts @@ -1,9 +1,9 @@ import type { Brand, Cause, Exit, Optic, Option } from "effect" -import { Data, Schema, SchemaUtils } from "effect" +import { Schema } from "effect" import { describe, expect, it } from "tstyche" class Value extends Schema.Class("Value")({ - a: Schema.DateValid + a: Schema.Date }) {} describe("toIso", () => { @@ -239,19 +239,19 @@ it("Cause", () => { >() }) -it("Error", () => { - const schema = Schema.Error() +it("ErrorInstance", () => { + const schema = Schema.ErrorInstance() const optic = Schema.toIso(schema) expect(optic).type.toBe>() }) it("Exit", () => { - const schema = Schema.Exit(Value, Schema.Error(), Schema.Defect()) + const schema = Schema.Exit(Value, Schema.ErrorInstance(), Schema.Defect()) const optic = Schema.toIso(schema) expect(optic).type.toBe< - Optic.Iso, Schema.ExitIso> + Optic.Iso, Schema.ExitIso> >() }) @@ -263,20 +263,3 @@ it("ReadonlyMap", () => { Optic.Iso, ReadonlyArray> >() }) - -it("getNativeClassSchema", () => { - const Props = Schema.Struct({ - message: Schema.String - }) - class Err extends Data.Error { - constructor(props: typeof Props.Type) { - super(Props.make(props)) - } - } - const schema = SchemaUtils.getNativeClassSchema(Err, { encoding: Props }) - const optic = Schema.toIso(schema) - - expect(optic).type.toBe< - Optic.Iso - >() -}) diff --git a/.context/effect/packages/effect/typetest/unstable/ai/McpServer.tst.ts b/.context/effect/packages/effect/typetest/unstable/ai/McpServer.tst.ts new file mode 100644 index 000000000..14a563094 --- /dev/null +++ b/.context/effect/packages/effect/typetest/unstable/ai/McpServer.tst.ts @@ -0,0 +1,106 @@ +import type * as Cause from "effect/Cause" +import type * as Effect from "effect/Effect" +import type * as Layer from "effect/Layer" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import { McpProtocol, McpSchema, McpServer } from "effect/unstable/ai" +import * as McpProtocolInternal from "effect/unstable/ai/internal/mcpProtocol" +import * as Rpc from "effect/unstable/rpc/Rpc" +import * as RpcGroup from "effect/unstable/rpc/RpcGroup" +import { describe, expect, it } from "tstyche" + +const serverOptions = { + name: "TestServer", + version: "1.0.0", + protocols: [McpProtocol.v2025_06_18] +} as const + +describe("McpServer", () => { + describe("protocol configuration", () => { + it("should accept a non-empty protocol declaration when constructing any server", () => { + expect(McpServer.run).type.toBeCallableWith(serverOptions) + expect(McpServer.layer).type.toBeCallableWith(serverOptions) + expect(McpServer.layerStdio).type.toBeCallableWith(serverOptions) + expect(McpServer.layerHttp).type.toBeCallableWith({ + ...serverOptions, + path: "/mcp", + allowedOrigins: ["https://mcp.example"] + }) + }) + + it("should require a protocol declaration when constructing a server", () => { + expect(McpServer.layer).type.not.toBeCallableWith({ + name: "TestServer", + version: "1.0.0" + }) + }) + + it("should reject an empty protocol declaration when constructing a server", () => { + expect(McpServer.layer).type.not.toBeCallableWith({ + name: "TestServer", + version: "1.0.0", + protocols: [] as const + }) + }) + + it("should reject version strings when protocol adapter values are required", () => { + expect(McpServer.layer).type.not.toBeCallableWith({ + name: "TestServer", + version: "1.0.0", + protocols: ["2025-06-18"] as const + }) + }) + + it("should require client notification RPCs to be included in the complete client RPC group", () => { + const Request = Rpc.make("request", { + payload: Schema.Struct({}), + success: Schema.Struct({}) + }) + const Notification = Rpc.make("notification", { + payload: Schema.Struct({}), + success: Schema.Struct({}) + }) + + expect(McpProtocolInternal.make).type.not.toBeCallableWith({ + protocolVersion: "test", + clientRpcs: RpcGroup.make(Request), + clientNotificationRpcs: RpcGroup.make(Notification), + serverRequestRpcs: RpcGroup.make(), + serverNotificationRpcs: RpcGroup.make() + }) + }) + + it("should expose the supported protocol adapter", () => { + expect<"v2025_06_18">().type.toBeAssignableTo() + expect().type.toBe<"2025-06-18">() + }) + + it("should expose invalid protocol declarations as typed constructor failures", () => { + const run = McpServer.run(serverOptions) + const layer = McpServer.layer(serverOptions) + + expect>().type.toBe() + expect>().type.toBe() + }) + }) + + describe("request context", () => { + it("should expose the selected protocol version when a handler reads its client", () => { + expect(McpSchema.McpServerClient.useSync((client) => client.protocolVersion)).type.toBe< + Effect.Effect + >() + }) + + it("should expose initialization data and the generated reverse RPC client", () => { + expect(McpSchema.McpServerClient.useSync((client) => client.initializePayload.capabilities)).type.toBe< + Effect.Effect + >() + expect(McpSchema.McpServerClient.useSync((client) => client.initializePayload.clientInfo)).type.toBe< + Effect.Effect + >() + expect(McpSchema.McpServerClient.use((client) => client.getClient)).type.toBeAssignableTo< + Effect.Effect + >() + }) + }) +}) diff --git a/.context/effect/packages/effect/typetest/unstable/http/HttpClient.tst.ts b/.context/effect/packages/effect/typetest/unstable/http/HttpClient.tst.ts index db872a269..f35b66270 100644 --- a/.context/effect/packages/effect/typetest/unstable/http/HttpClient.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/http/HttpClient.tst.ts @@ -122,7 +122,12 @@ describe("HttpClient", () => { limiter, key: "test", limit: 1, - window: "1 minute" + window: "1 minute", + times: 2, + responseHeaders: { + limit: "x-vendor-limit", + retryAfter: "x-vendor-retry-after" + } } as const const dataLast = client.pipe(HttpClient.withRateLimiter(options)) diff --git a/.context/effect/packages/effect/typetest/unstable/http/HttpRouter.tst.ts b/.context/effect/packages/effect/typetest/unstable/http/HttpRouter.tst.ts index d04489ab3..1ed480af0 100644 --- a/.context/effect/packages/effect/typetest/unstable/http/HttpRouter.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/http/HttpRouter.tst.ts @@ -1,8 +1,40 @@ -import { Context, Effect } from "effect" -import { HttpRouter, HttpServerResponse } from "effect/unstable/http" +import { Context, Effect, Layer } from "effect" +import { HttpRouter, type HttpServerError, HttpServerResponse } from "effect/unstable/http" import { describe, expect, it } from "tstyche" describe("HttpRouter", () => { + describe("middleware", () => { + it("provides handled request errors", () => { + class MyError { + readonly _tag = "MyError" + } + + const middleware = HttpRouter.middleware<{ handles: MyError }>()((effect) => + effect.pipe(Effect.catchTag("MyError", Effect.die)) + ) + + expect>().type + .toBeAssignableFrom>() + }) + }) + + describe("toHttpEffect", () => { + it("includes errors from global middleware", () => { + class MyError { + readonly _tag = "MyError" + } + + const globalMiddleware = HttpRouter.middleware( + (effect) => Effect.andThen(effect, Effect.fail(new MyError())), + { global: true } + ) + const result = HttpRouter.toHttpEffect(globalMiddleware) + + expect>>().type + .toBe() + }) + }) + describe("toWebHandler", () => { it("excludes adapter services required by middleware from the request context", () => { class CurrentUser extends Context.Service()("CurrentUser") {} @@ -28,5 +60,27 @@ describe("HttpRouter", () => { Context.make(CurrentUser, { id: "user-1" }) ) }) + + it("excludes services provided by the application layer from the request context", () => { + class CurrentUser extends Context.Service()("CurrentUser") {} + + const app = Layer.merge( + HttpRouter.add( + "GET", + "/", + Effect.map(CurrentUser, (user) => HttpServerResponse.text(user.id)) + ), + Layer.succeed(CurrentUser, { id: "user-1" }) + ) + const { handler } = HttpRouter.toWebHandler(app, { + disableLogger: true, + middleware: (effect) => effect + }) + + expect(handler).type.toBe< + (request: Request, context?: Context.Context | undefined) => Promise + >() + expect(handler).type.toBeCallableWith(new Request("http://localhost/")) + }) }) }) diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiBuilder.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiBuilder.tst.ts index e80e30ec5..76a2c98c5 100644 --- a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiBuilder.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiBuilder.tst.ts @@ -21,7 +21,7 @@ import { describe, expect, it } from "tstyche" describe("HttpApiBuilder", () => { describe("group", () => { it("does not require unknown services for status annotations piped onto errors", () => { - class NotFound extends Schema.TaggedErrorClass()("NotFound", {}) {} + class NotFound extends Schema.TaggedError()("NotFound", {}) {} const Api = HttpApi.make("api").add( HttpApiGroup.make("group").add( HttpApiEndpoint.get("get", "/", { diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiClient.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiClient.tst.ts index e5203a950..63a3d2b50 100644 --- a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiClient.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiClient.tst.ts @@ -621,7 +621,7 @@ describe("HttpApiClient", () => { type StreamError = { readonly reason: string } type ClientStream = Stream.Stream< Event, - StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry + StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError > expect(f()).type.toBe< @@ -641,6 +641,76 @@ describe("HttpApiClient", () => { >() }) + it("widens only the body stream errors for WithHeaders StreamSse successes", () => { + const Api = HttpApi.make("Api") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("a", "/a", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamSse({ + data: Schema.Struct({ id: Schema.String }), + error: Schema.Struct({ reason: Schema.String }) + }), + { "x-count": Schema.Int } + ) + }) + ) + ) + const client = Effect.runSync( + HttpApiClient.make(Api).pipe(Effect.provide(FetchHttpClient.layer)) + ) + const f = client.group.a + + type ClientStream = Stream.Stream< + { readonly id: string }, + | { readonly reason: string } + | HttpClientError.HttpClientError + | Schema.SchemaError + | Sse.Retry + | Sse.SseError + > + type Success = HttpApiSchema.withHeaders + + expect(f()).type.toBe< + Effect.Effect + >() + expect(f({ responseMode: "decoded-and-response" })).type.toBe< + Effect.Effect< + [Success, HttpClientResponse.HttpClientResponse], + HttpClientError.HttpClientError | Schema.SchemaError + > + >() + }) + + it("widens the body transport error for WithHeaders StreamUint8Array successes", () => { + const Api = HttpApi.make("Api") + .add( + HttpApiGroup.make("group") + .add( + HttpApiEndpoint.get("a", "/a", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamUint8Array(), + { "x-count": Schema.Int } + ) + }) + ) + ) + const client = Effect.runSync( + HttpApiClient.make(Api).pipe(Effect.provide(FetchHttpClient.layer)) + ) + const f = client.group.a + + type Success = HttpApiSchema.withHeaders< + Stream.Stream, + { readonly "x-count": number } + > + + expect(f()).type.toBe< + Effect.Effect + >() + }) + it("returns decoded data streams for StreamSse data successes", () => { const Api = HttpApi.make("Api") .add( @@ -663,7 +733,7 @@ describe("HttpApiClient", () => { type StreamError = { readonly reason: string } type ClientStream = Stream.Stream< Data, - StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry + StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError > expect(f()).type.toBe< @@ -697,7 +767,7 @@ describe("HttpApiClient", () => { type Data = { readonly id: string } type ClientStream = Stream.Stream< Data, - HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry + HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError > expect(f()).type.toBe< @@ -764,7 +834,7 @@ describe("HttpApiClient", () => { type ClientStream = Stream.Stream< Event, - StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry + StreamError | HttpClientError.HttpClientError | Schema.SchemaError | Sse.Retry | Sse.SseError > expect(f({ responseMode: "decoded-only" })).type.toBe< @@ -843,7 +913,7 @@ describe("HttpApiClient", () => { }) it("preserves custom client errors when normalizing HttpClientError", () => { - class CustomClientError extends Schema.ErrorClass("CustomClientError")({ + class CustomClientError extends Schema.Error("CustomClientError")({ _tag: Schema.tag("CustomClientError") }) {} @@ -957,10 +1027,10 @@ describe("HttpApiClient", () => { }) it("selects within the requested group when endpoint identifiers overlap", () => { - class UsersEndpointError extends Schema.TaggedErrorClass()("UsersEndpointError", {}) {} - class AdminsEndpointError extends Schema.TaggedErrorClass()("AdminsEndpointError", {}) {} - class UsersClientError extends Schema.TaggedErrorClass()("UsersClientError", {}) {} - class AdminsClientError extends Schema.TaggedErrorClass()("AdminsClientError", {}) {} + class UsersEndpointError extends Schema.TaggedError()("UsersEndpointError", {}) {} + class AdminsEndpointError extends Schema.TaggedError()("AdminsEndpointError", {}) {} + class UsersClientError extends Schema.TaggedError()("UsersClientError", {}) {} + class AdminsClientError extends Schema.TaggedError()("AdminsClientError", {}) {} class UsersMiddleware extends HttpApiMiddleware.Service { readonly customService: "customService" } - class CustomClientError extends Schema.ErrorClass("CustomClientError")({ + class CustomClientError extends Schema.Error("CustomClientError")({ _tag: Schema.tag("CustomClientError") }) {} @@ -1159,11 +1229,11 @@ describe("HttpApiClient", () => { describe("client middleware", () => { it("requires layers and includes errors for required client middleware", () => { - class RequiredClientError extends Schema.ErrorClass("RequiredClientError")({ + class RequiredClientError extends Schema.Error("RequiredClientError")({ _tag: Schema.tag("RequiredClientError") }) {} - class OptionalClientError extends Schema.ErrorClass("OptionalClientError")({ + class OptionalClientError extends Schema.Error("OptionalClientError")({ _tag: Schema.tag("OptionalClientError") }) {} @@ -1212,7 +1282,7 @@ describe("HttpApiClient", () => { }) it("enforces required middleware for makeWith, group, and endpoint", () => { - class RequiredClientError extends Schema.ErrorClass("RequiredClientError")({ + class RequiredClientError extends Schema.Error("RequiredClientError")({ _tag: Schema.tag("RequiredClientError") }) {} diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiEndpoint.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiEndpoint.tst.ts index 78b7ba773..3dff19a20 100644 --- a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiEndpoint.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiEndpoint.tst.ts @@ -468,6 +468,75 @@ describe("HttpApiEndpoint", () => { expect(endpoint["~Success"]).type.toBe() }) + it("applies the JSON and string-tree codecs to WithHeaders parts", () => { + const Body = Schema.Struct({ a: Schema.String }) + const Headers = Schema.Struct({ "x-count": Schema.Int }) + const endpoint = HttpApiEndpoint.get("a", "/a", { + success: HttpApiSchema.WithHeaders(Body, Headers) + }) + + expect(endpoint["~Success"]).type.toBe< + HttpApiSchema.WithHeaders, Schema.toCodecStringTree> + >() + }) + + it("maps WithHeaders to branded value success and handler types", () => { + const endpoint = HttpApiEndpoint.get("a", "/a", { + success: HttpApiSchema.WithHeaders(Schema.Struct({ a: Schema.String }), { + "x-count": Schema.Int + }) + }) + + type Success = HttpApiSchema.withHeaders< + { readonly a: string }, + { readonly "x-count": number } + > + + expect>().type.toBe() + expect>>().type.toBe< + Effect.Effect + >() + }) + + it("keeps the stream handler type for a wrapped stream schema", () => { + const endpoint = HttpApiEndpoint.get("a", "/a", { + success: HttpApiSchema.WithHeaders( + HttpApiSchema.StreamSse({ + data: Schema.Struct({ id: Schema.String }), + error: Schema.Struct({ reason: Schema.String }) + }), + { "x-count": Schema.Int } + ) + }) + + type Success = HttpApiSchema.withHeaders< + Stream.Stream<{ readonly id: string }, { readonly reason: string }>, + { readonly "x-count": number } + > + + expect>().type.toBe() + expect>>().type.toBe< + Effect.Effect + >() + }) + + it("preserves mixed WithHeaders and plain schemas", () => { + const endpoint = HttpApiEndpoint.get("a", "/a", { + success: [ + HttpApiSchema.WithHeaders(Schema.Struct({ a: Schema.String }), { + "x-count": Schema.Int + }), + Schema.String.pipe(HttpApiSchema.status(201), HttpApiSchema.asText()) + ] + }) + + type Success = + | HttpApiSchema.withHeaders<{ readonly a: string }, { readonly "x-count": number }> + | string + + expect>().type.toBe() + }) + it("maps StreamUint8Array to stream success and handler types", () => { const endpoint = HttpApiEndpoint.get("a", "/a", { success: HttpApiSchema.StreamUint8Array() diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiMiddleware.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiMiddleware.tst.ts index 2ef8c29ba..179f4bdbc 100644 --- a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiMiddleware.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiMiddleware.tst.ts @@ -30,7 +30,7 @@ describe("HttpApiMiddleware", () => { }) it("preserves error services for status annotations used with pipe", () => { - class NotFound extends Schema.TaggedErrorClass()("NotFound", {}) {} + class NotFound extends Schema.TaggedError()("NotFound", {}) {} class M extends HttpApiMiddleware.Service()("Http/Logger", { error: NotFound.pipe(HttpApiSchema.status(404)) }) {} diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiSchema.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiSchema.tst.ts index e23e02dcf..c60f81eb3 100644 --- a/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiSchema.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/HttpApiSchema.tst.ts @@ -6,7 +6,7 @@ import { describe, expect, it } from "tstyche" describe("HttpApiSchema", () => { describe("status", () => { it("preserves schema services when used with pipe", () => { - class NotFound extends Schema.TaggedErrorClass()("NotFound", {}) {} + class NotFound extends Schema.TaggedError()("NotFound", {}) {} const schema = NotFound.pipe(HttpApiSchema.status(404)) @@ -117,6 +117,81 @@ describe("HttpApiSchema", () => { }) }) + describe("WithHeaders", () => { + it("preserves the inner schema and headers schema types", () => { + const Headers = Schema.Struct({ "x-total-count": Schema.FiniteFromString }) + const schema = HttpApiSchema.WithHeaders(Schema.String, Headers) + + expect(schema).type.toBe>() + expect(schema.schema).type.toBe() + expect(schema.headers).type.toBe() + expect().type.toBe< + HttpApiSchema.withHeaders + >() + }) + + it("supports a fields shorthand for headers", () => { + const schema = HttpApiSchema.WithHeaders(Schema.String, { + "x-total-count": Schema.FiniteFromString + }) + + expect(schema).type.toBe< + HttpApiSchema.WithHeaders> + >() + }) + + it("preserves the wrapper type when annotated with status", () => { + const Headers = Schema.Struct({ "x-total-count": Schema.FiniteFromString }) + const schema = HttpApiSchema.WithHeaders(Schema.String, Headers).pipe(HttpApiSchema.status(201)) + + expect(schema).type.toBe>() + }) + + it("preserves schema services", () => { + const Headers = Schema.Struct({ "x-total-count": Schema.FiniteFromString }) + const schema = HttpApiSchema.WithHeaders(Schema.String, Headers) + + expect().type.toBe() + expect().type.toBe() + }) + }) + + describe("withHeaders", () => { + it("constructs a branded value", () => { + const value = HttpApiSchema.withHeaders({ body: "a", headers: { "x-a": "1" } }) + + expect(value).type.toBe>() + expect(value.body).type.toBe() + }) + }) + + describe("encodeToWithHeaders", () => { + it("keeps the source Type and encodes to a body and headers pair", () => { + class UserNotFound extends Schema.TaggedError()("UserNotFound", { + userId: Schema.Int + }) {} + + const schema = UserNotFound.pipe( + HttpApiSchema.encodeToWithHeaders({ + body: HttpApiSchema.Empty(404), + headers: { "x-user-id": Schema.Int } + }, { + decode: ({ headers }) => new UserNotFound({ userId: headers["x-user-id"] }), + encode: (error) => ({ + headers: { "x-user-id": error.userId }, + body: undefined + }) + }) + ) + + expect().type.toBe() + expect().type.toBeAssignableTo<{ + readonly body: void + readonly headers: { readonly "x-user-id": number } + }>() + }) + }) + describe("StreamUint8Array", () => { it("constructs the stream schema", () => { const stream = HttpApiSchema.StreamUint8Array() diff --git a/.context/effect/packages/effect/typetest/unstable/httpapi/OpenApiRepresentation.tst.ts b/.context/effect/packages/effect/typetest/unstable/httpapi/OpenApiRepresentation.tst.ts new file mode 100644 index 000000000..8e7d92656 --- /dev/null +++ b/.context/effect/packages/effect/typetest/unstable/httpapi/OpenApiRepresentation.tst.ts @@ -0,0 +1,12 @@ +import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { describe, expect, it } from "tstyche" + +describe("OpenApi representation consumer", () => { + it("keeps the synchronous fromApi signature", () => { + const Api = HttpApi.make("Api").add( + HttpApiGroup.make("test").add(HttpApiEndpoint.get("health", "/health")) + ) + + expect(OpenApi.fromApi(Api)).type.toBe() + }) +}) diff --git a/.context/effect/packages/effect/typetest/unstable/reactivity/Atom.tst.ts b/.context/effect/packages/effect/typetest/unstable/reactivity/Atom.tst.ts new file mode 100644 index 000000000..db8f043c9 --- /dev/null +++ b/.context/effect/packages/effect/typetest/unstable/reactivity/Atom.tst.ts @@ -0,0 +1,23 @@ +import { Layer } from "effect" +import { Atom } from "effect/unstable/reactivity" +import { describe, expect, it } from "tstyche" + +describe("Atom", () => { + describe("context", () => { + it("returns a registry runtime factory by default", () => { + expect(Atom.context()).type.toBe() + }) + + it("returns a registry runtime factory for an atom-backed memo map", () => { + const memoMap = Atom.make(() => Layer.makeMemoMapUnsafe()) + + expect(Atom.context({ memoMap })).type.toBe() + }) + + it("returns a shared runtime factory for a concrete memo map", () => { + const memoMap = Layer.makeMemoMapUnsafe() + + expect(Atom.context({ memoMap })).type.toBe() + }) + }) +}) diff --git a/.context/effect/packages/effect/typetest/unstable/reactivity/AtomHttpApi.tst.ts b/.context/effect/packages/effect/typetest/unstable/reactivity/AtomHttpApi.tst.ts index a6e0506a4..510b0086d 100644 --- a/.context/effect/packages/effect/typetest/unstable/reactivity/AtomHttpApi.tst.ts +++ b/.context/effect/packages/effect/typetest/unstable/reactivity/AtomHttpApi.tst.ts @@ -4,15 +4,15 @@ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effec import { type Atom, AtomHttpApi } from "effect/unstable/reactivity" import { describe, expect, it } from "tstyche" -class EndpointError extends Schema.ErrorClass("EndpointError")({ +class EndpointError extends Schema.Error("EndpointError")({ _tag: Schema.tag("EndpointError") }) {} -class MiddlewareError extends Schema.ErrorClass("MiddlewareError")({ +class MiddlewareError extends Schema.Error("MiddlewareError")({ _tag: Schema.tag("MiddlewareError") }) {} -class MiddlewareClientError extends Schema.ErrorClass("MiddlewareClientError")({ +class MiddlewareClientError extends Schema.Error("MiddlewareClientError")({ _tag: Schema.tag("MiddlewareClientError") }) {} diff --git a/.context/effect/packages/effect/vitest.config.ts b/.context/effect/packages/effect/vitest.config.ts deleted file mode 100644 index 07afe8d38..000000000 --- a/.context/effect/packages/effect/vitest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { mergeConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const isDeno = process.versions.deno !== undefined - -export default mergeConfig(shared, { - test: { - // @see https://github.com/denoland/deno/issues/23882 - exclude: (isDeno ? ["test/cluster/**"] : []) - } -}) diff --git a/.context/effect/packages/opentelemetry/CHANGELOG.md b/.context/effect/packages/opentelemetry/CHANGELOG.md index c858e0e19..c2a566d12 100644 --- a/.context/effect/packages/opentelemetry/CHANGELOG.md +++ b/.context/effect/packages/opentelemetry/CHANGELOG.md @@ -1,5 +1,71 @@ # @effect/opentelemetry +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7112](https://github.com/Effect-TS/effect/pull/7112) [`14278e7`](https://github.com/Effect-TS/effect/commit/14278e7d6213d5752111ee4b2ffdf514e600b370) Thanks @fubhy! - Bound Node tracer provider shutdown by the configured `shutdownTimeout`. + +- [#7118](https://github.com/Effect-TS/effect/pull/7118) [`ac71ede`](https://github.com/Effect-TS/effect/commit/ac71ede1682a5548c6144647556bad95b351320c) Thanks @fubhy! - Prevent log annotations from overwriting active span correlation identifiers. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#7064](https://github.com/Effect-TS/effect/pull/7064) [`abc7355`](https://github.com/Effect-TS/effect/commit/abc73555eaa345c11e55100cdacf7fdf5fbf8f7e) Thanks @fubhy! - Ensure Web and Node tracer providers shut down when flushing fails during layer release. + +- [#7046](https://github.com/Effect-TS/effect/pull/7046) [`3159d15`](https://github.com/Effect-TS/effect/commit/3159d15040ac8700560f0f4791bbd95beb30d5d3) Thanks @fubhy! - Ensure logger providers shut down when flushing fails. + +- [#7069](https://github.com/Effect-TS/effect/pull/7069) [`79512bb`](https://github.com/Effect-TS/effect/commit/79512bb207df346310264f046bc4315e9543568d) Thanks @fubhy! - Fix wrapped spans treating non-error OpenTelemetry statuses as errors. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6904](https://github.com/Effect-TS/effect/pull/6904) [`0d83916`](https://github.com/Effect-TS/effect/commit/0d83916b6604c818efd645601cfb69bd48a9879f) Thanks @fubhy! - Isolate delta metric baselines for each registered metric reader + +- [#6902](https://github.com/Effect-TS/effect/pull/6902) [`ab0859b`](https://github.com/Effect-TS/effect/commit/ab0859b97420aaaed5c00b04f12d7bdbba004837) Thanks @fubhy! - Preserve trace state and locality when adapting active OpenTelemetry parent contexts. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/opentelemetry/README.md b/.context/effect/packages/opentelemetry/README.md index 8c7f71497..5486d3207 100644 --- a/.context/effect/packages/opentelemetry/README.md +++ b/.context/effect/packages/opentelemetry/README.md @@ -1,5 +1,16 @@ -# `@effect/opentelemetry` +# @effect/opentelemetry + +An [OpenTelemetry](https://opentelemetry.io) integration for Effect. Exports Effect tracing, metrics, and logs through the OpenTelemetry SDK, with `NodeSdk` and `WebSdk` layers for easy setup. + +## Installation + +```sh +npm install effect@beta @effect/opentelemetry@beta +``` + +The relevant `@opentelemetry/*` SDK packages are required as peer dependencies, depending on which features you use. ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/opentelemetry). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/opentelemetry) diff --git a/.context/effect/packages/opentelemetry/docgen.json b/.context/effect/packages/opentelemetry/docgen.json deleted file mode 100644 index 9e6205d2b..000000000 --- a/.context/effect/packages/opentelemetry/docgen.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/opentelemetry/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { - "name": "@effect/language-service", - "includeSuggestionsInTsc": false, - "diagnosticSeverity": { - "globalErrorInEffectFailure": "off" - } - } - ] - } -} diff --git a/.context/effect/packages/opentelemetry/package.json b/.context/effect/packages/opentelemetry/package.json index c8731ce19..bbfd27ba3 100644 --- a/.context/effect/packages/opentelemetry/package.json +++ b/.context/effect/packages/opentelemetry/package.json @@ -1,7 +1,7 @@ { "name": "@effect/opentelemetry", "type": "module", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "license": "MIT", "description": "OpenTelemetry integration for Effect", "homepage": "https://effect.website", @@ -40,6 +40,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -47,7 +48,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -57,6 +61,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -64,20 +69,18 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "peerDependencies": { - "@opentelemetry/api": "^1.9", + "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", - "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/resources": ">=2.0.0 <3.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", + "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", + "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", + "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "workspace:^" }, "peerDependenciesMeta": { diff --git a/.context/effect/packages/opentelemetry/src/NodeSdk.ts b/.context/effect/packages/opentelemetry/src/NodeSdk.ts index df91ec140..cd48d7199 100644 --- a/.context/effect/packages/opentelemetry/src/NodeSdk.ts +++ b/.context/effect/packages/opentelemetry/src/NodeSdk.ts @@ -74,7 +74,7 @@ export const layerTracerProvider = ( return provider }), (provider) => - Effect.promise(() => provider.forceFlush().then(() => provider.shutdown())).pipe( + Effect.promise(() => provider.forceFlush().finally(() => provider.shutdown())).pipe( Effect.ignore, Effect.interruptible, Effect.timeoutOption(config?.shutdownTimeout ?? 3000) diff --git a/.context/effect/packages/opentelemetry/src/OtelLogger.ts b/.context/effect/packages/opentelemetry/src/OtelLogger.ts index fac7038ec..8ccd6ee82 100644 --- a/.context/effect/packages/opentelemetry/src/OtelLogger.ts +++ b/.context/effect/packages/opentelemetry/src/OtelLogger.ts @@ -21,6 +21,7 @@ import * as Layer from "effect/Layer" import * as Logger from "effect/Logger" import type * as LogLevel from "effect/LogLevel" import * as Predicate from "effect/Predicate" +import * as Rec from "effect/Record" import * as References from "effect/References" import * as Tracer from "effect/Tracer" import { nanosToHrTime, unknownToAttributeValue } from "./internal/attributes.ts" @@ -90,6 +91,10 @@ export const make: Effect.Effect< fiberId: options.fiber.id } + for (const [key, value] of Object.entries(options.fiber.getRef(References.CurrentLogAnnotations))) { + Rec.assignProperty(attributes, key, unknownToAttributeValue(value)) + } + const span = Context.getOrUndefined(options.fiber.context, Tracer.ParentSpan) if (Predicate.isNotUndefined(span)) { @@ -97,9 +102,6 @@ export const make: Effect.Effect< attributes.traceId = span.traceId } - for (const [key, value] of Object.entries(options.fiber.getRef(References.CurrentLogAnnotations))) { - attributes[key] = unknownToAttributeValue(value) - } const now = options.date.getTime() for (const [label, startTime] of options.fiber.getRef(References.CurrentLogSpans)) { attributes[`logSpan.${label}`] = `${now - startTime}ms` @@ -183,7 +185,7 @@ export const layerLoggerProvider = ( }) ), (provider) => - Effect.promise(() => provider.forceFlush().then(() => provider.shutdown())).pipe( + Effect.promise(() => provider.forceFlush().finally(() => provider.shutdown())).pipe( Effect.ignore, Effect.interruptible, Effect.timeoutOption(config?.shutdownTimeout ?? 3000) diff --git a/.context/effect/packages/opentelemetry/src/OtelMetrics.ts b/.context/effect/packages/opentelemetry/src/OtelMetrics.ts index 8674844e0..da76f6b7c 100644 --- a/.context/effect/packages/opentelemetry/src/OtelMetrics.ts +++ b/.context/effect/packages/opentelemetry/src/OtelMetrics.ts @@ -65,7 +65,7 @@ export const makeProducer = (temporality?: TemporalityPreference): Effect.Effect /** * Registers a metric producer with one or more metric readers. * - * @category constructors + * @category resource management * @since 4.0.0 */ export const registerProducer = ( @@ -79,7 +79,7 @@ export const registerProducer = ( Effect.sync(() => { const reader = metricReader() const readers: Array = Array.isArray(reader) ? reader : [reader] as any - readers.forEach((reader) => reader.setMetricProducer(self)) + readers.forEach((reader) => reader.setMetricProducer(self instanceof MetricProducerImpl ? self.fork() : self)) return readers }), (readers) => @@ -97,29 +97,40 @@ export const registerProducer = ( /** * Creates a Layer that registers a metric producer with metric readers. * - * **Example** (Creating a metrics layer with temporality) - * - * ```ts - * import { OtelMetrics } from "@effect/opentelemetry" - * import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics" - * import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" - * - * const metricExporter = new OTLPMetricExporter({ url: "" }) - * - * // Use delta temporality for backends like Datadog or Dynatrace - * const metricsLayer = OtelMetrics.layer( - * () => new PeriodicExportingMetricReader({ - * exporter: metricExporter, - * exportIntervalMillis: 10000 - * }), - * { temporality: "delta" } + * **Example** (Exporting delta metrics) + * + * ```ts import.meta.vitest + * import { OtelMetrics, Resource } from "@effect/opentelemetry" + * import { + * AggregationTemporality, + * InMemoryMetricExporter, + * PeriodicExportingMetricReader + * } from "@opentelemetry/sdk-metrics" + * import { Effect, Layer, Metric } from "effect" + * + * const exporter = new InMemoryMetricExporter(AggregationTemporality.DELTA) + * const reader = new PeriodicExportingMetricReader({ + * exporter, + * exportIntervalMillis: 60_000 + * }) + * const metricsLayer = OtelMetrics.layer(() => reader, { temporality: "delta" }).pipe( + * Layer.provide(Resource.layerEmpty) * ) * - * // Use cumulative temporality for backends like Prometheus (default) - * const cumulativeLayer = OtelMetrics.layer( - * () => new PeriodicExportingMetricReader({ exporter: metricExporter }), - * { temporality: "cumulative" } + * const program = Effect.gen(function*() { + * yield* Metric.update(Metric.counter("docs.requests", { incremental: true }), 2) + * yield* Effect.promise(() => reader.forceFlush()) + * + * const metric = exporter.getMetrics()[0]?.scopeMetrics[0]?.metrics.find( + * (metric) => metric.descriptor.name === "docs.requests" + * ) + * return [metric?.descriptor.name, metric?.aggregationTemporality, metric?.dataPoints[0]?.value] as const + * }).pipe( + * Effect.provide(metricsLayer), + * Effect.provideService(Metric.MetricRegistry, new Map()) * ) + * + * await Effect.runPromise(program) // => ["docs.requests", AggregationTemporality.DELTA, 2] * ``` * * @category layers diff --git a/.context/effect/packages/opentelemetry/src/OtelTracer.ts b/.context/effect/packages/opentelemetry/src/OtelTracer.ts index 8e34a0abb..074916ecf 100644 --- a/.context/effect/packages/opentelemetry/src/OtelTracer.ts +++ b/.context/effect/packages/opentelemetry/src/OtelTracer.ts @@ -130,24 +130,20 @@ export const makeExternalSpan = (options: { readonly traceFlags?: number | undefined readonly traceState?: string | Otel.TraceState | undefined }): Tracer.ExternalSpan => { - const annotations = Context.mutate(Context.empty(), (annotations) => { - let next = annotations - if (options.traceFlags !== undefined) { - next = Context.add(next, OtelTraceFlags, options.traceFlags) - } + let annotations = Context.empty() + if (options.traceFlags !== undefined) { + annotations = Context.add(annotations, OtelTraceFlags, options.traceFlags) + } - if (typeof options.traceState === "string") { - try { - next = Context.add(next, OtelTraceState, Otel.createTraceState(options.traceState)) - } catch { - // - } - } else if (options.traceState) { - next = Context.add(next, OtelTraceState, options.traceState) + if (typeof options.traceState === "string") { + try { + annotations = Context.add(annotations, OtelTraceState, Otel.createTraceState(options.traceState)) + } catch { + // } - - return next - }) + } else if (options.traceState) { + annotations = Context.add(annotations, OtelTraceState, options.traceState) + } return { _tag: "ExternalSpan", @@ -314,7 +310,7 @@ const makeOtelSpan = (span: Tracer.Span, clock: Clock.Clock): Otel.Span => { return self }, setStatus(status) { - exit = Otel.SpanStatusCode.ERROR + exit = status.code === Otel.SpanStatusCode.ERROR ? Exit.die(status.message ?? "Unknown error") : Exit.void return self @@ -518,18 +514,23 @@ export class OtelSpan implements Tracer.Span { const isSampled = (traceFlags: Otel.TraceFlags): boolean => (traceFlags & Otel.TraceFlags.SAMPLED) === Otel.TraceFlags.SAMPLED +class OtelParentSpanContext extends Context.Service< + OtelParentSpanContext, + Otel.SpanContext +>()("@effect/opentelemetry/Tracer/OtelParentSpanContext") {} + const getOtelParent = ( tracer: Otel.TraceAPI, context: Otel.Context, annotations: Context.Context ): Option.Option => { - const otelParent = tracer.getSpan(context)?.spanContext() + const otelParent = tracer.getSpanContext(context) if (!otelParent) return Option.none() return Option.some(Tracer.externalSpan({ spanId: otelParent.spanId, traceId: otelParent.traceId, - sampled: (otelParent.traceFlags & 1) === 1, - annotations + sampled: isSampled(otelParent.traceFlags), + annotations: Context.add(annotations, OtelParentSpanContext, otelParent) })) } @@ -537,6 +538,17 @@ const makeSpanContext = ( span: Tracer.AnySpan, annotations?: Context.Context ): Otel.SpanContext => { + const otelParent = Context.getOrUndefined(span.annotations, OtelParentSpanContext) + if (otelParent !== undefined) { + if (annotations === undefined) return otelParent + const traceFlags = extractTraceService(span, annotations, OtelTraceFlags) + const traceState = extractTraceService(span, annotations, OtelTraceState) + return { + ...otelParent, + traceFlags: traceFlags ?? otelParent.traceFlags, + traceState: traceState ?? otelParent.traceState! + } + } const traceFlags = makeTraceFlags(span, annotations) const traceState = makeTraceState(span, annotations)! return ({ diff --git a/.context/effect/packages/opentelemetry/src/Resource.ts b/.context/effect/packages/opentelemetry/src/Resource.ts index 9a4c758df..0aa5e5089 100644 --- a/.context/effect/packages/opentelemetry/src/Resource.ts +++ b/.context/effect/packages/opentelemetry/src/Resource.ts @@ -18,6 +18,7 @@ import * as Config from "effect/Config" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" +import * as Rec from "effect/Record" /** * Service tag for OpenTelemetry metadata attached to emitted telemetry. @@ -74,7 +75,7 @@ export const layer = (config: { * @see {@link layer} for creating a `Resource` layer from explicit metadata * @see {@link layerFromEnv} for merging attributes with OpenTelemetry environment variables * - * @category configuration + * @category converting * @since 4.0.0 */ export const configToAttributes = (options: { @@ -120,7 +121,7 @@ export const layerFromEnv = ( if (parts.length !== 2) { return acc } - acc[parts[0].trim()] = parts[1].trim() + Rec.assignProperty(acc, parts[0].trim(), parts[1].trim()) return acc }) }) @@ -128,10 +129,10 @@ export const layerFromEnv = ( if (serviceName._tag === "Some") { attributes[OtelSemConv.ATTR_SERVICE_NAME] = serviceName.value } - if (additionalAttributes) { - Object.assign(attributes, additionalAttributes) - } - return Resources.resourceFromAttributes(attributes) + return Resources.resourceFromAttributes({ + ...attributes, + ...additionalAttributes + }) }).pipe(Effect.orDie) ) diff --git a/.context/effect/packages/opentelemetry/src/WebSdk.ts b/.context/effect/packages/opentelemetry/src/WebSdk.ts index 0fae7d60b..ea44ab7e7 100644 --- a/.context/effect/packages/opentelemetry/src/WebSdk.ts +++ b/.context/effect/packages/opentelemetry/src/WebSdk.ts @@ -69,8 +69,9 @@ export const layerTracerProvider = ( return provider }), (provider) => - Effect.ignore( - Effect.promise(() => provider.forceFlush().then(() => provider.shutdown())) + Effect.promise(() => provider.forceFlush()).pipe( + Effect.ensuring(Effect.promise(() => provider.shutdown())), + Effect.ignore ) ) }) diff --git a/.context/effect/packages/opentelemetry/src/internal/attributes.ts b/.context/effect/packages/opentelemetry/src/internal/attributes.ts index 2d01dc224..d8fa20915 100644 --- a/.context/effect/packages/opentelemetry/src/internal/attributes.ts +++ b/.context/effect/packages/opentelemetry/src/internal/attributes.ts @@ -1,5 +1,6 @@ import type * as Otel from "@opentelemetry/api" import * as Inspectable from "effect/Inspectable" +import * as Rec from "effect/Record" const bigint1e9 = BigInt(1_000_000_000) @@ -12,7 +13,7 @@ export const nanosToHrTime = (timestamp: bigint): Otel.HrTime => { export const recordToAttributes = (record: Record): Otel.Attributes => { const attributes: Otel.Attributes = {} for (const [key, value] of Object.entries(record)) { - attributes[key] = unknownToAttributeValue(value) + Rec.assignProperty(attributes, key, unknownToAttributeValue(value)) } return attributes } diff --git a/.context/effect/packages/opentelemetry/src/internal/metrics.ts b/.context/effect/packages/opentelemetry/src/internal/metrics.ts index 24692997d..3fd560a7a 100644 --- a/.context/effect/packages/opentelemetry/src/internal/metrics.ts +++ b/.context/effect/packages/opentelemetry/src/internal/metrics.ts @@ -13,6 +13,7 @@ import type { InstrumentDescriptor } from "@opentelemetry/sdk-metrics/build/src/ import * as Arr from "effect/Array" import type * as Context from "effect/Context" import * as Metric from "effect/Metric" +import * as Rec from "effect/Record" import type * as Metrics from "../OtelMetrics.ts" const sdkName = "@effect/opentelemetry/Metrics" @@ -64,6 +65,10 @@ export class MetricProducerImpl implements MetricProducer { this.previousSummaryState = new Map() } + fork(): MetricProducerImpl { + return new MetricProducerImpl(this.resource, this.context, this.temporality) + } + startTimeFor(name: string, hrTime: HrTime) { if (this.startTimes.has(name)) { return this.startTimes.get(name)! @@ -94,7 +99,7 @@ export class MetricProducerImpl implements MetricProducer { const state = snapshot[i] const attributes = state.attributes ? Arr.reduce(Object.entries(state.attributes), {} as Record, (acc, [key, value]) => { - acc[key] = String(value) + Rec.assignProperty(acc, key, String(value)) return acc }) : {} diff --git a/.context/effect/packages/opentelemetry/test/OtelLogger.test.ts b/.context/effect/packages/opentelemetry/test/OtelLogger.test.ts index cc486f141..fa09d2f5f 100644 --- a/.context/effect/packages/opentelemetry/test/OtelLogger.test.ts +++ b/.context/effect/packages/opentelemetry/test/OtelLogger.test.ts @@ -1,7 +1,9 @@ import * as NodeSdk from "@effect/opentelemetry/NodeSdk" +import * as OtelLogger from "@effect/opentelemetry/OtelLogger" +import * as Resource from "@effect/opentelemetry/Resource" import { assert, describe, it } from "@effect/vitest" import { SeverityNumber } from "@opentelemetry/api-logs" -import { InMemoryLogRecordExporter, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs" +import { InMemoryLogRecordExporter, type LogRecordProcessor, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Clock from "effect/Clock" import * as Effect from "effect/Effect" @@ -9,6 +11,23 @@ import * as Layer from "effect/Layer" import * as References from "effect/References" describe("Logger", () => { + it.effect("shuts down the logger provider after forceFlush rejects", () => + Effect.gen(function*() { + let shutdowns = 0 + const processor: LogRecordProcessor = { + onEmit() {}, + forceFlush: () => Promise.reject(new Error("flush failed")), + shutdown: () => { + shutdowns++ + return Promise.resolve() + } + } + yield* Effect.exit( + Effect.scoped(Layer.build(OtelLogger.layerLoggerProvider(processor)).pipe(Effect.provide(Resource.layerEmpty))) + ) + assert.strictEqual(shutdowns, 1) + })) + describe("provided", () => { const exporter = new InMemoryLogRecordExporter() @@ -56,19 +75,22 @@ describe("Logger", () => { ) }) - it.effect("uses monotonic clock timestamps and keeps them aligned with spans", () => { + it.effect("uses wall-clock timestamps and keeps them aligned with spans", () => { const logExporter = new InMemoryLogRecordExporter() const spanExporter = new InMemorySpanExporter() - const timeNanos = 1_735_689_600_123_456_789n + const wallTimeNanos = 1_735_689_600_123_456_789n + const monotonicTimeNanos = 123_456_789n const expectedTime: readonly [number, number] = [ - Number(timeNanos / BigInt(1_000_000_000)), - Number(timeNanos % BigInt(1_000_000_000)) + Number(wallTimeNanos / BigInt(1_000_000_000)), + Number(wallTimeNanos % BigInt(1_000_000_000)) ] const skewedClock: Clock.Clock = { currentTimeMillisUnsafe: () => 1, currentTimeMillis: Effect.succeed(1), - currentTimeNanosUnsafe: () => timeNanos, - currentTimeNanos: Effect.succeed(timeNanos), + currentTimeNanosUnsafe: () => wallTimeNanos, + currentTimeNanos: Effect.succeed(wallTimeNanos), + monotonicTimeNanosUnsafe: () => monotonicTimeNanos, + monotonicTimeNanos: Effect.succeed(monotonicTimeNanos), sleep: () => Effect.void } @@ -102,6 +124,28 @@ describe("Logger", () => { Effect.provideService(Clock.Clock, skewedClock) ) }) + + it.effect("does not let annotations overwrite active span correlation", () => { + const logExporter = new InMemoryLogRecordExporter() + const spanExporter = new InMemorySpanExporter() + const TracingLive = NodeSdk.layer(Effect.sync(() => ({ + resource: { serviceName: "test" }, + spanProcessor: [new SimpleSpanProcessor(spanExporter)], + logRecordProcessor: [new SimpleLogRecordProcessor({ exporter: logExporter })] + }))) + + return Effect.gen(function*() { + yield* Effect.log("test").pipe( + Effect.annotateLogs({ traceId: "spoof-trace", spanId: "spoof-span" }), + Effect.withSpan("parent") + ) + + const log = logExporter.getFinishedLogRecords()[0]! + const span = spanExporter.getFinishedSpans()[0]! + assert.strictEqual(log.attributes.traceId, span.spanContext().traceId) + assert.strictEqual(log.attributes.spanId, span.spanContext().spanId) + }).pipe(Effect.provide(TracingLive)) + }) }) describe("not provided", () => { diff --git a/.context/effect/packages/opentelemetry/test/OtelMetrics.test.ts b/.context/effect/packages/opentelemetry/test/OtelMetrics.test.ts index c57d00376..df8947529 100644 --- a/.context/effect/packages/opentelemetry/test/OtelMetrics.test.ts +++ b/.context/effect/packages/opentelemetry/test/OtelMetrics.test.ts @@ -1,7 +1,9 @@ import * as internal from "@effect/opentelemetry/internal/metrics" +import * as OtelMetrics from "@effect/opentelemetry/OtelMetrics" import { assert, describe, it } from "@effect/vitest" import { ValueType } from "@opentelemetry/api" import { resourceFromAttributes } from "@opentelemetry/resources" +import { MetricReader } from "@opentelemetry/sdk-metrics" import * as Effect from "effect/Effect" import * as Metric from "effect/Metric" @@ -336,4 +338,29 @@ describe("Metrics", () => { ] }) })) + + it.effect("reports the same delta to every registered reader", () => + Effect.gen(function*() { + class Reader extends MetricReader { + protected onShutdown(): Promise { + return Promise.resolve() + } + protected onForceFlush(): Promise { + return Promise.resolve() + } + } + + const services = yield* Effect.context() + const producer = new internal.MetricProducerImpl(resourceFromAttributes({}), services, "delta") + const first = new Reader() + const second = new Reader() + yield* OtelMetrics.registerProducer(producer, () => [first, second]) + yield* Metric.update(Metric.counter("requests", { incremental: true }), 1) + + const firstResult = yield* Effect.promise(() => first.collect()) + const secondResult = yield* Effect.promise(() => second.collect()) + const firstValue = (firstResult.resourceMetrics.scopeMetrics[0]!.metrics[0] as any).dataPoints[0].value + const secondValue = (secondResult.resourceMetrics.scopeMetrics[0]!.metrics[0] as any).dataPoints[0].value + assert.deepStrictEqual([firstValue, secondValue], [1, 1]) + }).pipe(Effect.provideService(Metric.MetricRegistry, new Map()))) }) diff --git a/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts b/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts index d4f8df6f8..a4647691b 100644 --- a/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts +++ b/.context/effect/packages/opentelemetry/test/OtelTracer.test.ts @@ -5,7 +5,9 @@ import * as OtelApi from "@opentelemetry/api" import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks" import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" import * as Cause from "effect/Cause" +import * as EffectContext from "effect/Context" import * as Effect from "effect/Effect" +import * as Option from "effect/Option" import * as EffectTracer from "effect/Tracer" const TracingLive = NodeSdk.layer(Effect.sync(() => ({ @@ -40,7 +42,6 @@ describe("Tracer", () => { assert.instanceOf(span, OtelTracer.OtelSpan) assert.lengthOf(span.links, 1) }).pipe( - Effect.scoped, Effect.provide(TracingLive) )) @@ -87,6 +88,33 @@ describe("Tracer", () => { Effect.provide(TracingLive) )) + it.effect.each([OtelApi.SpanStatusCode.UNSET, OtelApi.SpanStatusCode.OK])( + "honors non-error wrapper status %s", + (status) => + Effect.gen(function*() { + const span = yield* Effect.currentSpan + const wrapper = yield* OtelTracer.currentOtelSpan + wrapper.setStatus({ code: status }) + wrapper.end() + assert.strictEqual(span.status._tag, "Ended") + if (span.status._tag === "Ended") { + assert.strictEqual(span.status.exit._tag, "Success") + } + }).pipe(Effect.withSpan("repro")) + ) + + it.effect("honors an error wrapper status", () => + Effect.gen(function*() { + const span = yield* Effect.currentSpan + const wrapper = yield* OtelTracer.currentOtelSpan + wrapper.setStatus({ code: OtelApi.SpanStatusCode.ERROR }) + wrapper.end() + assert.strictEqual(span.status._tag, "Ended") + if (span.status._tag === "Ended") { + assert.strictEqual(span.status.exit._tag, "Failure") + } + }).pipe(Effect.withSpan("repro"))) + it.effect("preserves the sampling decision of generic external spans", () => Effect.gen(function*() { const span = yield* Effect.currentSpan @@ -102,6 +130,52 @@ describe("Tracer", () => { Effect.provide(TracingLive) )) + it("preserves trace state and locality on an active OpenTelemetry parent", () => { + const parent: OtelApi.SpanContext = { + traceId: "1".repeat(32), + spanId: "2".repeat(16), + traceFlags: OtelApi.TraceFlags.SAMPLED, + traceState: OtelApi.createTraceState("vendor=value"), + isRemote: false + } + const active = OtelApi.trace.setSpanContext(OtelApi.ROOT_CONTEXT, parent) + let receivedParent: OtelApi.SpanContext | undefined + const tracer = { + startSpan(_name: string, _options: unknown, context: OtelApi.Context) { + receivedParent = OtelApi.trace.getSpanContext(context) + return { + spanContext: () => ({ + traceId: "3".repeat(32), + spanId: "4".repeat(16), + traceFlags: OtelApi.TraceFlags.SAMPLED + }) + } as OtelApi.Span + } + } as OtelApi.Tracer + + const child = new OtelTracer.OtelSpan( + { active: () => active } as OtelApi.ContextAPI, + OtelApi.trace, + tracer, + { + name: "child", + parent: Option.none(), + annotations: EffectContext.empty(), + links: [], + startTime: 0n, + kind: "internal", + root: false, + sampled: true + } + ) + + assert.instanceOf(child, OtelTracer.OtelSpan) + assert.deepStrictEqual( + [receivedParent?.traceState?.serialize(), receivedParent?.isRemote], + ["vendor=value", false] + ) + }) + it.effect("records every pretty error", () => Effect.gen(function*() { const exporter = new InMemorySpanExporter() @@ -195,7 +269,6 @@ describe("Tracer", () => { }) }) }).pipe( - Effect.scoped, Effect.provide(TracingLive) )) }) diff --git a/.context/effect/packages/opentelemetry/tsconfig.json b/.context/effect/packages/opentelemetry/tsconfig.json index ae1d58ea9..d27a5f7d0 100644 --- a/.context/effect/packages/opentelemetry/tsconfig.json +++ b/.context/effect/packages/opentelemetry/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/opentelemetry/vitest.config.ts b/.context/effect/packages/opentelemetry/vitest.config.ts deleted file mode 100644 index fb966ae87..000000000 --- a/.context/effect/packages/opentelemetry/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/platform-browser/CHANGELOG.md b/.context/effect/packages/platform-browser/CHANGELOG.md deleted file mode 100644 index 38e6a56b0..000000000 --- a/.context/effect/packages/platform-browser/CHANGELOG.md +++ /dev/null @@ -1,763 +0,0 @@ -# @effect/platform-browser - -## 4.0.0-beta.101 - -### Patch Changes - -- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: - - effect@4.0.0-beta.101 - -## 4.0.0-beta.100 - -### Patch Changes - -- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: - - effect@4.0.0-beta.100 - -## 4.0.0-beta.99 - -### Patch Changes - -- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: - - effect@4.0.0-beta.99 - -## 4.0.0-beta.98 - -### Patch Changes - -- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: - - effect@4.0.0-beta.98 - -## 4.0.0-beta.97 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.97 - -## 4.0.0-beta.96 - -### Patch Changes - -- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: - - effect@4.0.0-beta.96 - -## 4.0.0-beta.95 - -### Patch Changes - -- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: - - effect@4.0.0-beta.95 - -## 4.0.0-beta.94 - -### Patch Changes - -- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: - - effect@4.0.0-beta.94 - -## 4.0.0-beta.93 - -### Patch Changes - -- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: - - effect@4.0.0-beta.93 - -## 4.0.0-beta.92 - -### Patch Changes - -- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: - - effect@4.0.0-beta.92 - -## 4.0.0-beta.91 - -### Patch Changes - -- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: - - effect@4.0.0-beta.91 - -## 4.0.0-beta.90 - -### Patch Changes - -- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: - - effect@4.0.0-beta.90 - -## 4.0.0-beta.89 - -### Patch Changes - -- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: - - effect@4.0.0-beta.89 - -## 4.0.0-beta.88 - -### Patch Changes - -- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: - - effect@4.0.0-beta.88 - -## 4.0.0-beta.87 - -### Patch Changes - -- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: - - effect@4.0.0-beta.87 - -## 4.0.0-beta.86 - -### Patch Changes - -- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: - - effect@4.0.0-beta.86 - -## 4.0.0-beta.85 - -### Patch Changes - -- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: - - effect@4.0.0-beta.85 - -## 4.0.0-beta.84 - -### Patch Changes - -- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: - - effect@4.0.0-beta.84 - -## 4.0.0-beta.83 - -### Patch Changes - -- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: - - effect@4.0.0-beta.83 - -## 4.0.0-beta.82 - -### Patch Changes - -- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: - - effect@4.0.0-beta.82 - -## 4.0.0-beta.81 - -### Patch Changes - -- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: - - effect@4.0.0-beta.81 - -## 4.0.0-beta.80 - -### Patch Changes - -- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: - - effect@4.0.0-beta.80 - -## 4.0.0-beta.79 - -### Patch Changes - -- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: - - effect@4.0.0-beta.79 - -## 4.0.0-beta.78 - -### Patch Changes - -- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: - - effect@4.0.0-beta.78 - -## 4.0.0-beta.77 - -### Patch Changes - -- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: - - effect@4.0.0-beta.77 - -## 4.0.0-beta.76 - -### Patch Changes - -- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: - - effect@4.0.0-beta.76 - -## 4.0.0-beta.75 - -### Patch Changes - -- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: - - effect@4.0.0-beta.75 - -## 4.0.0-beta.74 - -### Patch Changes - -- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: - - effect@4.0.0-beta.74 - -## 4.0.0-beta.73 - -### Patch Changes - -- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: - - effect@4.0.0-beta.73 - -## 4.0.0-beta.72 - -### Patch Changes - -- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: - - effect@4.0.0-beta.72 - -## 4.0.0-beta.71 - -### Patch Changes - -- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: - - effect@4.0.0-beta.71 - -## 4.0.0-beta.70 - -### Patch Changes - -- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: - - effect@4.0.0-beta.70 - -## 4.0.0-beta.69 - -### Patch Changes - -- [#2241](https://github.com/Effect-TS/effect-smol/pull/2241) [`c5e54d8`](https://github.com/Effect-TS/effect-smol/commit/c5e54d8e4d4ea0f64d9023793b54dfa83d85eac4) Thanks @tim-smart! - Fix IndexedDB bulk writes so `insertAll` and `upsertAll` resume when used inside `withTransaction`. - -- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: - - effect@4.0.0-beta.69 - -## 4.0.0-beta.68 - -### Patch Changes - -- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. - -- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: - - effect@4.0.0-beta.68 - -## 4.0.0-beta.67 - -### Patch Changes - -- [#2196](https://github.com/Effect-TS/effect-smol/pull/2196) [`b7abd0e`](https://github.com/Effect-TS/effect-smol/commit/b7abd0e5c236500a00de3a871c151cc50c3ec40b) Thanks @juemrami! - Adds an IndexedDB backed implementation of `KeyValueStore` as `BrowserKeyValueStore.layerIndexedDb`. This backend allows for non-blocking `KeyValueStore` operations, unlike the existing `Storage` api backed implementations. - -- [#2183](https://github.com/Effect-TS/effect-smol/pull/2183) [`e32343a`](https://github.com/Effect-TS/effect-smol/commit/e32343adf3e449cb9452908655c83757843d1907) Thanks @tim-smart! - use Cause.NoSuchElementError for idb .first queries - -- [#2182](https://github.com/Effect-TS/effect-smol/pull/2182) [`ae40463`](https://github.com/Effect-TS/effect-smol/commit/ae404636ab75fc47d0167c22a20564777c12cf59) Thanks @tim-smart! - cache base idb query builders - -- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: - - effect@4.0.0-beta.67 - -## 4.0.0-beta.66 - -### Patch Changes - -- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: - - effect@4.0.0-beta.66 - -## 4.0.0-beta.65 - -### Patch Changes - -- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: - - effect@4.0.0-beta.65 - -## 4.0.0-beta.64 - -### Patch Changes - -- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: - - effect@4.0.0-beta.64 - -## 4.0.0-beta.63 - -### Patch Changes - -- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: - - effect@4.0.0-beta.63 - -## 4.0.0-beta.62 - -### Patch Changes - -- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: - - effect@4.0.0-beta.62 - -## 4.0.0-beta.61 - -### Patch Changes - -- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: - - effect@4.0.0-beta.61 - -## 4.0.0-beta.60 - -### Patch Changes - -- [#2110](https://github.com/Effect-TS/effect-smol/pull/2110) [`f862e40`](https://github.com/Effect-TS/effect-smol/commit/f862e40573b6d1c04942799be5ff6f7dbea22ae9) Thanks @tim-smart! - cleanup IndexedDb prototypes - -- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: - - effect@4.0.0-beta.60 - -## 4.0.0-beta.59 - -### Patch Changes - -- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: - - effect@4.0.0-beta.59 - -## 4.0.0-beta.58 - -### Patch Changes - -- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: - - effect@4.0.0-beta.58 - -## 4.0.0-beta.57 - -### Patch Changes - -- [#2086](https://github.com/Effect-TS/effect-smol/pull/2086) [`979b56b`](https://github.com/Effect-TS/effect-smol/commit/979b56b5c45facc488cc920a83339b87fc51334d) Thanks @tim-smart! - fix idb entries transaction - -- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: - - effect@4.0.0-beta.57 - -## 4.0.0-beta.56 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.56 - -## 4.0.0-beta.55 - -### Patch Changes - -- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: - - effect@4.0.0-beta.55 - -## 4.0.0-beta.54 - -### Patch Changes - -- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: - - effect@4.0.0-beta.54 - -## 4.0.0-beta.53 - -### Patch Changes - -- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: - - effect@4.0.0-beta.53 - -## 4.0.0-beta.52 - -### Patch Changes - -- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: - - effect@4.0.0-beta.52 - -## 4.0.0-beta.51 - -### Patch Changes - -- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: - - effect@4.0.0-beta.51 - -## 4.0.0-beta.50 - -### Patch Changes - -- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: - - effect@4.0.0-beta.50 - -## 4.0.0-beta.49 - -### Patch Changes - -- [#2030](https://github.com/Effect-TS/effect-smol/pull/2030) [`253efe6`](https://github.com/Effect-TS/effect-smol/commit/253efe6f52ecef187d286e6eaba270e0f4d939ed) Thanks @tim-smart! - Add BrowserPersistence.layerIndexedDb for composing Persistence.layer with the IndexedDB backing layer, and export BrowserPersistence from the package barrel. - -- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: - - effect@4.0.0-beta.49 - -## 4.0.0-beta.48 - -### Patch Changes - -- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: - - effect@4.0.0-beta.48 - -## 4.0.0-beta.47 - -### Patch Changes - -- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: - - effect@4.0.0-beta.47 - -## 4.0.0-beta.46 - -### Patch Changes - -- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: - - effect@4.0.0-beta.46 - -## 4.0.0-beta.45 - -### Patch Changes - -- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: - - effect@4.0.0-beta.45 - -## 4.0.0-beta.44 - -### Patch Changes - -- [#1976](https://github.com/Effect-TS/effect-smol/pull/1976) [`7a35a83`](https://github.com/Effect-TS/effect-smol/commit/7a35a83382f9956b1cb92b6a47399adabd79cbf9) Thanks @tim-smart! - improve idb support for compound indexes - -- [#1993](https://github.com/Effect-TS/effect-smol/pull/1993) [`acc7fff`](https://github.com/Effect-TS/effect-smol/commit/acc7fffcaa82b34225f54d324090c2853f9b3547) Thanks @tim-smart! - improve idb transaction api - -- [#1964](https://github.com/Effect-TS/effect-smol/pull/1964) [`1ee4543`](https://github.com/Effect-TS/effect-smol/commit/1ee4543450895f58d2f3ba986dadea6962de4818) Thanks @tim-smart! - allow customizing idb durability - -- [#1937](https://github.com/Effect-TS/effect-smol/pull/1937) [`96cb778`](https://github.com/Effect-TS/effect-smol/commit/96cb77829d6677de788c4227cbee06fc4c707c9c) Thanks @tim-smart! - add .reactive to indexeddb .first queries - -- [#1953](https://github.com/Effect-TS/effect-smol/pull/1953) [`fbbdaec`](https://github.com/Effect-TS/effect-smol/commit/fbbdaec5ed020f7f94a030ede02c121011313037) Thanks @tim-smart! - add defaults to indexeddb reactivity keys - -- [#1977](https://github.com/Effect-TS/effect-smol/pull/1977) [`4f58e30`](https://github.com/Effect-TS/effect-smol/commit/4f58e309d16ded3cac3fb95185a61c06972e2e26) Thanks @tim-smart! - add idb stream and offset - -- [#1989](https://github.com/Effect-TS/effect-smol/pull/1989) [`3cc091d`](https://github.com/Effect-TS/effect-smol/commit/3cc091de7ecfa657b519b0ced316754bcbf53099) Thanks @tim-smart! - use encoded types for idb queries - -- [#1985](https://github.com/Effect-TS/effect-smol/pull/1985) [`f244e71`](https://github.com/Effect-TS/effect-smol/commit/f244e7141770bab8a00f34e48e6923d97ebbe405) Thanks @tim-smart! - add .reverse() to idb select - -- [#1992](https://github.com/Effect-TS/effect-smol/pull/1992) [`8c74d03`](https://github.com/Effect-TS/effect-smol/commit/8c74d0353fff9099075ba28bb50166a23f4062dc) Thanks @tim-smart! - Add rebuild api to idb databases - -- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. - -- [#1924](https://github.com/Effect-TS/effect-smol/pull/1924) [`716fe24`](https://github.com/Effect-TS/effect-smol/commit/716fe24886292aa6af2ad1f3fd7aa1b2f0a10c7f) Thanks @tim-smart! - allow Model.Class for indexeddb schemas - -- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: - - effect@4.0.0-beta.44 - -## 4.0.0-beta.43 - -### Patch Changes - -- [#1240](https://github.com/Effect-TS/effect-smol/pull/1240) [`583ea00`](https://github.com/Effect-TS/effect-smol/commit/583ea002fdccc58fd2110a36c6d103e63152dcb3) Thanks @SandroMaglione! - add IndexedDb modules - -- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: - - effect@4.0.0-beta.43 - -## 4.0.0-beta.42 - -### Patch Changes - -- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: - - effect@4.0.0-beta.42 - -## 4.0.0-beta.41 - -### Patch Changes - -- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: - - effect@4.0.0-beta.41 - -## 4.0.0-beta.40 - -### Patch Changes - -- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: - - effect@4.0.0-beta.40 - -## 4.0.0-beta.39 - -### Patch Changes - -- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: - - effect@4.0.0-beta.39 - -## 4.0.0-beta.38 - -### Patch Changes - -- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: - - effect@4.0.0-beta.38 - -## 4.0.0-beta.37 - -### Patch Changes - -- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: - - effect@4.0.0-beta.37 - -## 4.0.0-beta.36 - -### Patch Changes - -- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: - - effect@4.0.0-beta.36 - -## 4.0.0-beta.35 - -### Patch Changes - -- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: - - effect@4.0.0-beta.35 - -## 4.0.0-beta.34 - -### Patch Changes - -- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: - - effect@4.0.0-beta.34 - -## 4.0.0-beta.33 - -### Patch Changes - -- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: - - effect@4.0.0-beta.33 - -## 4.0.0-beta.32 - -### Patch Changes - -- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: - - effect@4.0.0-beta.32 - -## 4.0.0-beta.31 - -### Patch Changes - -- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. - - Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. - -- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: - - effect@4.0.0-beta.31 - -## 4.0.0-beta.30 - -### Patch Changes - -- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: - - effect@4.0.0-beta.30 - -## 4.0.0-beta.29 - -### Patch Changes - -- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: - - effect@4.0.0-beta.29 - -## 4.0.0-beta.28 - -### Patch Changes - -- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: - - effect@4.0.0-beta.28 - -## 4.0.0-beta.27 - -### Patch Changes - -- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: - - effect@4.0.0-beta.27 - -## 4.0.0-beta.26 - -### Patch Changes - -- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: - - effect@4.0.0-beta.26 - -## 4.0.0-beta.25 - -### Patch Changes - -- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: - - effect@4.0.0-beta.25 - -## 4.0.0-beta.24 - -### Patch Changes - -- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: - - effect@4.0.0-beta.24 - -## 4.0.0-beta.23 - -### Patch Changes - -- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: - - effect@4.0.0-beta.23 - -## 4.0.0-beta.22 - -### Patch Changes - -- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: - - effect@4.0.0-beta.22 - -## 4.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: - - effect@4.0.0-beta.21 - -## 4.0.0-beta.20 - -### Patch Changes - -- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: - - effect@4.0.0-beta.20 - -## 4.0.0-beta.19 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.19 - -## 4.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: - - effect@4.0.0-beta.18 - -## 4.0.0-beta.17 - -### Patch Changes - -- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: - - effect@4.0.0-beta.17 - -## 4.0.0-beta.16 - -### Patch Changes - -- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: - - effect@4.0.0-beta.16 - -## 4.0.0-beta.15 - -### Patch Changes - -- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: - - effect@4.0.0-beta.15 - -## 4.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: - - effect@4.0.0-beta.14 - -## 4.0.0-beta.13 - -### Patch Changes - -- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: - - effect@4.0.0-beta.13 - -## 4.0.0-beta.12 - -### Patch Changes - -- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: - - effect@4.0.0-beta.12 - -## 4.0.0-beta.11 - -### Patch Changes - -- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: - - effect@4.0.0-beta.11 - -## 4.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: - - effect@4.0.0-beta.10 - -## 4.0.0-beta.9 - -### Patch Changes - -- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: - - effect@4.0.0-beta.9 - -## 4.0.0-beta.8 - -### Patch Changes - -- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: - - effect@4.0.0-beta.8 - -## 4.0.0-beta.7 - -### Patch Changes - -- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: - - effect@4.0.0-beta.7 - -## 4.0.0-beta.6 - -### Patch Changes - -- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: - - effect@4.0.0-beta.6 - -## 4.0.0-beta.5 - -### Patch Changes - -- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: - - effect@4.0.0-beta.5 - -## 4.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: - - effect@4.0.0-beta.4 - -## 4.0.0-beta.3 - -### Patch Changes - -- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: - - effect@4.0.0-beta.3 - -## 4.0.0-beta.2 - -### Patch Changes - -- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: - - effect@4.0.0-beta.2 - -## 4.0.0-beta.1 - -### Patch Changes - -- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: - - effect@4.0.0-beta.1 - -## 4.0.0-beta.0 - -### Major Changes - -- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta - -### Patch Changes - -- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: - - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-browser/README.md b/.context/effect/packages/platform-browser/README.md deleted file mode 100644 index b8be094e2..000000000 --- a/.context/effect/packages/platform-browser/README.md +++ /dev/null @@ -1 +0,0 @@ -# `@effect/platform-browser` diff --git a/.context/effect/packages/platform-browser/docgen.json b/.context/effect/packages/platform-browser/docgen.json deleted file mode 100644 index 7d417cd6d..000000000 --- a/.context/effect/packages/platform-browser/docgen.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-browser/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["node"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/platform-browser/package.json b/.context/effect/packages/platform-browser/package.json deleted file mode 100644 index 96ca01545..000000000 --- a/.context/effect/packages/platform-browser/package.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "@effect/platform-browser", - "type": "module", - "version": "4.0.0-beta.101", - "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" - ], - "sideEffects": [], - "exports": { - "./package.json": "./package.json", - ".": "./src/index.ts", - "./*": "./src/*.ts", - "./internal/*": null, - "./*/index": null - }, - "files": [ - "src/**/*.ts", - "dist/**/*.js", - "dist/**/*.js.map", - "dist/**/*.d.ts", - "dist/**/*.d.ts.map" - ], - "publishConfig": { - "access": "public", - "provenance": true, - "exports": { - "./package.json": "./package.json", - ".": "./dist/index.js", - "./*": "./dist/*.js", - "./internal/*": null, - "./*/index": null - } - }, - "scripts": { - "codegen": "effect-utils codegen", - "build": "tsc -b tsconfig.json && pnpm babel", - "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" - }, - "peerDependencies": { - "effect": "workspace:^" - }, - "devDependencies": { - "effect": "workspace:^", - "fake-indexeddb": "^6.2.5", - "mock-xmlhttprequest": "^8.4.1" - }, - "dependencies": { - "multipasta": "^0.2.8" - } -} diff --git a/.context/effect/packages/platform-browser/test/BrowserCrypto.test.ts b/.context/effect/packages/platform-browser/test/BrowserCrypto.test.ts deleted file mode 100644 index 45650777c..000000000 --- a/.context/effect/packages/platform-browser/test/BrowserCrypto.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto" -import { assert, describe, it } from "@effect/vitest" -import { Layer } from "effect" -import * as Crypto from "effect/Crypto" -import * as Effect from "effect/Effect" -import * as TestClock from "effect/testing/TestClock" - -const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ -const uuidV7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ - -const getRandomValues = (array: T): T => { - if (array instanceof Uint8Array) { - for (let i = 0; i < array.length; i++) { - array[i] = i & 0xff - } - } - return array -} - -describe("BrowserCrypto", () => { - it.effect("generates UUIDv4 values from getRandomValues", () => - Effect.gen(function*() { - const crypto = yield* Crypto.Crypto - const uuid = yield* crypto.randomUUIDv4 - assert.strictEqual(uuid, "00010203-0405-4607-8809-0a0b0c0d0e0f") - assert.match(uuid, uuidV4Regex) - }).pipe(Effect.provide(BrowserCrypto.layer.pipe( - Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { - ...crypto, - getRandomValues(array) { - return getRandomValues(array) - } - })) - )))) - - it.effect("generates UUIDv7 values from getRandomValues and the Clock", () => - Effect.gen(function*() { - yield* TestClock.setTime(0x0123456789ab) - const crypto = yield* Crypto.Crypto - const uuid = yield* crypto.randomUUIDv7 - assert.strictEqual(uuid, "01234567-89ab-7607-8809-0a0b0c0d0e0f") - assert.match(uuid, uuidV7Regex) - }).pipe(Effect.provide(BrowserCrypto.layer.pipe( - Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { - ...crypto, - getRandomValues(array) { - return getRandomValues(array) - } - })) - )))) - - it.effect("computes digests with subtle crypto", () => { - const buffer = new ArrayBuffer(3) - new Uint8Array(buffer).set([1, 2, 3]) - - return Effect.gen(function*() { - const crypto = yield* Crypto.Crypto - const digest = yield* crypto.digest("SHA-256", new Uint8Array(buffer)) - assert.deepStrictEqual(digest, new Uint8Array([1, 2, 3])) - }).pipe( - Effect.provide(BrowserCrypto.layer.pipe( - Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { - ...crypto, - subtle: { - ...crypto.subtle, - digest() { - return Promise.resolve(buffer) - } - } - })) - )) - ) - }) -}) diff --git a/.context/effect/packages/platform-browser/test/BrowserKeyValueStore.test.ts b/.context/effect/packages/platform-browser/test/BrowserKeyValueStore.test.ts deleted file mode 100644 index eabe56367..000000000 --- a/.context/effect/packages/platform-browser/test/BrowserKeyValueStore.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore" -import * as IndexedDb from "@effect/platform-browser/IndexedDb" -import { describe } from "@effect/vitest" -import { Layer } from "effect" -import { testLayer } from "effect-test/unstable/persistence/KeyValueStore.test" -import { IDBKeyRange, indexedDB } from "fake-indexeddb" - -describe("KeyValueStore / layerLocalStorage", () => testLayer(BrowserKeyValueStore.layerLocalStorage)) - -describe("KeyValueStore / layerSessionStorage", () => testLayer(BrowserKeyValueStore.layerSessionStorage)) - -describe("KeyValueStore / layerIndexedDb", () => { - const layerFakeIndexedDb = Layer.succeed( - IndexedDb.IndexedDb, - IndexedDb.make({ indexedDB, IDBKeyRange }) - ) - - testLayer( - BrowserKeyValueStore.layerIndexedDb({ database: "kvs_test_db" }).pipe( - Layer.provide(layerFakeIndexedDb) - ) - ) -}) diff --git a/.context/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts b/.context/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts deleted file mode 100644 index 1bbe326fa..000000000 --- a/.context/effect/packages/platform-browser/test/fixtures/rpc-schemas.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Context, Effect, Layer, Metric, Option, Queue, Schema } from "effect" -import { Headers } from "effect/unstable/http" -import * as Rpc from "effect/unstable/rpc/Rpc" -import * as RpcGroup from "effect/unstable/rpc/RpcGroup" -import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware" -import * as RpcServer from "effect/unstable/rpc/RpcServer" - -export class User extends Schema.Class("User")({ - id: Schema.String, - name: Schema.String -}) {} - -class StreamUsers extends Rpc.make("StreamUsers", { - success: User, - payload: { - id: Schema.String - }, - stream: true -}) {} - -class CurrentUser extends Context.Service()("CurrentUser") {} - -class Unauthorized extends Schema.ErrorClass("Unauthorized")({ - _tag: Schema.tag("Unauthorized") -}) {} - -class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { - error: Unauthorized, - requiredForClient: true -}) {} - -class TimingMiddleware extends RpcMiddleware.Service()("TimingMiddleware") {} - -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 } - }), - StreamUsers, - Rpc.make("GetInterrupts", { - success: Schema.Number - }), - Rpc.make("GetEmits", { - success: Schema.Number - }), - Rpc.make("ProduceDefect"), - 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) - -export const AuthLive = Layer.succeed(AuthMiddleware)( - AuthMiddleware.of((effect, options) => - Effect.provideService( - effect, - CurrentUser, - 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") -export const TimingLive = Layer.succeed(TimingMiddleware)( - TimingMiddleware.of((effect) => - effect.pipe( - Effect.tap(Metric.update(rpcSuccesses, 1)), - Effect.tapDefect(() => Metric.update(rpcDefects, 1)), - Effect.ensuring(Metric.update(rpcCount, 1)) - ) - ) -) - -export 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* Queue.bounded(0) - - yield* Effect.addFinalizer(() => - Effect.sync(() => { - interrupts++ - }) - ) - - yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe( - Effect.tap(() => - Effect.sync(() => { - emits++ - }) - ), - Effect.delay(100), - Effect.forever, - Effect.forkScoped - ) - - return mailbox - }), - GetInterrupts: () => Effect.sync(() => interrupts), - GetEmits: () => Effect.sync(() => emits), - ProduceDefect: () => Effect.die("boom"), - 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, { - disableFatalDefects: true -}).pipe( - Layer.provide([ - UsersLive, - AuthLive, - TimingLive - ]) -) - -export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) => - next({ - ...request, - headers: Headers.set(request.headers, "name", "Logged in user") - })) diff --git a/.context/effect/packages/platform-browser/tsconfig.json b/.context/effect/packages/platform-browser/tsconfig.json deleted file mode 100644 index ae1d58ea9..000000000 --- a/.context/effect/packages/platform-browser/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "include": ["src"], - "references": [ - { "path": "../effect" } - ] -} diff --git a/.context/effect/packages/platform-browser/vitest.config.ts b/.context/effect/packages/platform-browser/vitest.config.ts deleted file mode 100644 index 2281aacb0..000000000 --- a/.context/effect/packages/platform-browser/vitest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as os from "node:os" -import * as path from "node:path" -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const config: ViteUserConfig = { - test: { - environment: "happy-dom", - execArgv: [ - "--localstorage-file", - path.resolve(os.tmpdir(), `vitest-${process.pid}.localstorage`) - ], - setupFiles: "./vitest.setup.ts" - } -} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/platform-bun/CHANGELOG.md b/.context/effect/packages/platform-bun/CHANGELOG.md deleted file mode 100644 index 3f4b8c177..000000000 --- a/.context/effect/packages/platform-bun/CHANGELOG.md +++ /dev/null @@ -1,837 +0,0 @@ -# @effect/platform-bun - -## 4.0.0-beta.101 - -### Patch Changes - -- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: - - effect@4.0.0-beta.101 - - @effect/platform-node-shared@4.0.0-beta.101 - -## 4.0.0-beta.100 - -### Patch Changes - -- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: - - effect@4.0.0-beta.100 - - @effect/platform-node-shared@4.0.0-beta.100 - -## 4.0.0-beta.99 - -### Patch Changes - -- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: - - effect@4.0.0-beta.99 - - @effect/platform-node-shared@4.0.0-beta.99 - -## 4.0.0-beta.98 - -### Patch Changes - -- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: - - effect@4.0.0-beta.98 - - @effect/platform-node-shared@4.0.0-beta.98 - -## 4.0.0-beta.97 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.97 - - @effect/platform-node-shared@4.0.0-beta.97 - -## 4.0.0-beta.96 - -### Patch Changes - -- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: - - effect@4.0.0-beta.96 - - @effect/platform-node-shared@4.0.0-beta.96 - -## 4.0.0-beta.95 - -### Patch Changes - -- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: - - effect@4.0.0-beta.95 - - @effect/platform-node-shared@4.0.0-beta.95 - -## 4.0.0-beta.94 - -### Patch Changes - -- [#2537](https://github.com/Effect-TS/effect-smol/pull/2537) [`6d2c614`](https://github.com/Effect-TS/effect-smol/commit/6d2c614fab3da932afbb52e849c2663cf32d7d57) Thanks @tim-smart! - optimize bun stream reading - -- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: - - effect@4.0.0-beta.94 - - @effect/platform-node-shared@4.0.0-beta.94 - -## 4.0.0-beta.93 - -### Patch Changes - -- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: - - effect@4.0.0-beta.93 - - @effect/platform-node-shared@4.0.0-beta.93 - -## 4.0.0-beta.92 - -### Patch Changes - -- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: - - effect@4.0.0-beta.92 - - @effect/platform-node-shared@4.0.0-beta.92 - -## 4.0.0-beta.91 - -### Patch Changes - -- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: - - effect@4.0.0-beta.91 - - @effect/platform-node-shared@4.0.0-beta.91 - -## 4.0.0-beta.90 - -### Patch Changes - -- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: - - effect@4.0.0-beta.90 - - @effect/platform-node-shared@4.0.0-beta.90 - -## 4.0.0-beta.89 - -### Patch Changes - -- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: - - effect@4.0.0-beta.89 - - @effect/platform-node-shared@4.0.0-beta.89 - -## 4.0.0-beta.88 - -### Patch Changes - -- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: - - effect@4.0.0-beta.88 - - @effect/platform-node-shared@4.0.0-beta.88 - -## 4.0.0-beta.87 - -### Patch Changes - -- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: - - effect@4.0.0-beta.87 - - @effect/platform-node-shared@4.0.0-beta.87 - -## 4.0.0-beta.86 - -### Patch Changes - -- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: - - effect@4.0.0-beta.86 - - @effect/platform-node-shared@4.0.0-beta.86 - -## 4.0.0-beta.85 - -### Patch Changes - -- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: - - effect@4.0.0-beta.85 - - @effect/platform-node-shared@4.0.0-beta.85 - -## 4.0.0-beta.84 - -### Patch Changes - -- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: - - effect@4.0.0-beta.84 - - @effect/platform-node-shared@4.0.0-beta.84 - -## 4.0.0-beta.83 - -### Patch Changes - -- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: - - effect@4.0.0-beta.83 - - @effect/platform-node-shared@4.0.0-beta.83 - -## 4.0.0-beta.82 - -### Patch Changes - -- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: - - effect@4.0.0-beta.82 - - @effect/platform-node-shared@4.0.0-beta.82 - -## 4.0.0-beta.81 - -### Patch Changes - -- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: - - effect@4.0.0-beta.81 - - @effect/platform-node-shared@4.0.0-beta.81 - -## 4.0.0-beta.80 - -### Patch Changes - -- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: - - effect@4.0.0-beta.80 - - @effect/platform-node-shared@4.0.0-beta.80 - -## 4.0.0-beta.79 - -### Patch Changes - -- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: - - effect@4.0.0-beta.79 - - @effect/platform-node-shared@4.0.0-beta.79 - -## 4.0.0-beta.78 - -### Patch Changes - -- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: - - effect@4.0.0-beta.78 - - @effect/platform-node-shared@4.0.0-beta.78 - -## 4.0.0-beta.77 - -### Patch Changes - -- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: - - effect@4.0.0-beta.77 - - @effect/platform-node-shared@4.0.0-beta.77 - -## 4.0.0-beta.76 - -### Patch Changes - -- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: - - effect@4.0.0-beta.76 - - @effect/platform-node-shared@4.0.0-beta.76 - -## 4.0.0-beta.75 - -### Patch Changes - -- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: - - effect@4.0.0-beta.75 - - @effect/platform-node-shared@4.0.0-beta.75 - -## 4.0.0-beta.74 - -### Patch Changes - -- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: - - effect@4.0.0-beta.74 - - @effect/platform-node-shared@4.0.0-beta.74 - -## 4.0.0-beta.73 - -### Patch Changes - -- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: - - effect@4.0.0-beta.73 - - @effect/platform-node-shared@4.0.0-beta.73 - -## 4.0.0-beta.72 - -### Patch Changes - -- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: - - effect@4.0.0-beta.72 - - @effect/platform-node-shared@4.0.0-beta.72 - -## 4.0.0-beta.71 - -### Patch Changes - -- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: - - effect@4.0.0-beta.71 - - @effect/platform-node-shared@4.0.0-beta.71 - -## 4.0.0-beta.70 - -### Patch Changes - -- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: - - effect@4.0.0-beta.70 - - @effect/platform-node-shared@4.0.0-beta.70 - -## 4.0.0-beta.69 - -### Patch Changes - -- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: - - effect@4.0.0-beta.69 - - @effect/platform-node-shared@4.0.0-beta.69 - -## 4.0.0-beta.68 - -### Patch Changes - -- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. - -- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: - - effect@4.0.0-beta.68 - - @effect/platform-node-shared@4.0.0-beta.68 - -## 4.0.0-beta.67 - -### Patch Changes - -- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal - -- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: - - effect@4.0.0-beta.67 - - @effect/platform-node-shared@4.0.0-beta.67 - -## 4.0.0-beta.66 - -### Patch Changes - -- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: - - effect@4.0.0-beta.66 - - @effect/platform-node-shared@4.0.0-beta.66 - -## 4.0.0-beta.65 - -### Patch Changes - -- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: - - effect@4.0.0-beta.65 - - @effect/platform-node-shared@4.0.0-beta.65 - -## 4.0.0-beta.64 - -### Patch Changes - -- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: - - effect@4.0.0-beta.64 - - @effect/platform-node-shared@4.0.0-beta.64 - -## 4.0.0-beta.63 - -### Patch Changes - -- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: - - effect@4.0.0-beta.63 - - @effect/platform-node-shared@4.0.0-beta.63 - -## 4.0.0-beta.62 - -### Patch Changes - -- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: - - effect@4.0.0-beta.62 - - @effect/platform-node-shared@4.0.0-beta.62 - -## 4.0.0-beta.61 - -### Patch Changes - -- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: - - effect@4.0.0-beta.61 - - @effect/platform-node-shared@4.0.0-beta.61 - -## 4.0.0-beta.60 - -### Patch Changes - -- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: - - effect@4.0.0-beta.60 - - @effect/platform-node-shared@4.0.0-beta.60 - -## 4.0.0-beta.59 - -### Patch Changes - -- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: - - effect@4.0.0-beta.59 - - @effect/platform-node-shared@4.0.0-beta.59 - -## 4.0.0-beta.58 - -### Patch Changes - -- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption - -- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: - - effect@4.0.0-beta.58 - - @effect/platform-node-shared@4.0.0-beta.58 - -## 4.0.0-beta.57 - -### Patch Changes - -- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: - - effect@4.0.0-beta.57 - - @effect/platform-node-shared@4.0.0-beta.57 - -## 4.0.0-beta.56 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.56 - - @effect/platform-node-shared@4.0.0-beta.56 - -## 4.0.0-beta.55 - -### Patch Changes - -- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: - - effect@4.0.0-beta.55 - - @effect/platform-node-shared@4.0.0-beta.55 - -## 4.0.0-beta.54 - -### Patch Changes - -- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: - - effect@4.0.0-beta.54 - - @effect/platform-node-shared@4.0.0-beta.54 - -## 4.0.0-beta.53 - -### Patch Changes - -- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: - - effect@4.0.0-beta.53 - - @effect/platform-node-shared@4.0.0-beta.53 - -## 4.0.0-beta.52 - -### Patch Changes - -- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: - - effect@4.0.0-beta.52 - - @effect/platform-node-shared@4.0.0-beta.52 - -## 4.0.0-beta.51 - -### Patch Changes - -- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: - - effect@4.0.0-beta.51 - - @effect/platform-node-shared@4.0.0-beta.51 - -## 4.0.0-beta.50 - -### Patch Changes - -- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: - - effect@4.0.0-beta.50 - - @effect/platform-node-shared@4.0.0-beta.50 - -## 4.0.0-beta.49 - -### Patch Changes - -- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: - - effect@4.0.0-beta.49 - - @effect/platform-node-shared@4.0.0-beta.49 - -## 4.0.0-beta.48 - -### Patch Changes - -- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: - - effect@4.0.0-beta.48 - - @effect/platform-node-shared@4.0.0-beta.48 - -## 4.0.0-beta.47 - -### Patch Changes - -- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: - - effect@4.0.0-beta.47 - - @effect/platform-node-shared@4.0.0-beta.47 - -## 4.0.0-beta.46 - -### Patch Changes - -- Updated dependencies [[`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505), [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: - - @effect/platform-node-shared@4.0.0-beta.46 - - effect@4.0.0-beta.46 - -## 4.0.0-beta.45 - -### Patch Changes - -- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: - - effect@4.0.0-beta.45 - - @effect/platform-node-shared@4.0.0-beta.45 - -## 4.0.0-beta.44 - -### Patch Changes - -- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. - -- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: - - effect@4.0.0-beta.44 - - @effect/platform-node-shared@4.0.0-beta.44 - -## 4.0.0-beta.43 - -### Patch Changes - -- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: - - effect@4.0.0-beta.43 - - @effect/platform-node-shared@4.0.0-beta.43 - -## 4.0.0-beta.42 - -### Patch Changes - -- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: - - effect@4.0.0-beta.42 - - @effect/platform-node-shared@4.0.0-beta.42 - -## 4.0.0-beta.41 - -### Patch Changes - -- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: - - effect@4.0.0-beta.41 - - @effect/platform-node-shared@4.0.0-beta.41 - -## 4.0.0-beta.40 - -### Patch Changes - -- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: - - effect@4.0.0-beta.40 - - @effect/platform-node-shared@4.0.0-beta.40 - -## 4.0.0-beta.39 - -### Patch Changes - -- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: - - effect@4.0.0-beta.39 - - @effect/platform-node-shared@4.0.0-beta.39 - -## 4.0.0-beta.38 - -### Patch Changes - -- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: - - effect@4.0.0-beta.38 - - @effect/platform-node-shared@4.0.0-beta.38 - -## 4.0.0-beta.37 - -### Patch Changes - -- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: - - effect@4.0.0-beta.37 - - @effect/platform-node-shared@4.0.0-beta.37 - -## 4.0.0-beta.36 - -### Patch Changes - -- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: - - effect@4.0.0-beta.36 - - @effect/platform-node-shared@4.0.0-beta.36 - -## 4.0.0-beta.35 - -### Patch Changes - -- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: - - effect@4.0.0-beta.35 - - @effect/platform-node-shared@4.0.0-beta.35 - -## 4.0.0-beta.34 - -### Patch Changes - -- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: - - effect@4.0.0-beta.34 - - @effect/platform-node-shared@4.0.0-beta.34 - -## 4.0.0-beta.33 - -### Patch Changes - -- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: - - effect@4.0.0-beta.33 - - @effect/platform-node-shared@4.0.0-beta.33 - -## 4.0.0-beta.32 - -### Patch Changes - -- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: - - effect@4.0.0-beta.32 - - @effect/platform-node-shared@4.0.0-beta.32 - -## 4.0.0-beta.31 - -### Patch Changes - -- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. - - Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. - -- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: - - effect@4.0.0-beta.31 - - @effect/platform-node-shared@4.0.0-beta.31 - -## 4.0.0-beta.30 - -### Patch Changes - -- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: - - effect@4.0.0-beta.30 - - @effect/platform-node-shared@4.0.0-beta.30 - -## 4.0.0-beta.29 - -### Patch Changes - -- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: - - effect@4.0.0-beta.29 - - @effect/platform-node-shared@4.0.0-beta.29 - -## 4.0.0-beta.28 - -### Patch Changes - -- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: - - effect@4.0.0-beta.28 - - @effect/platform-node-shared@4.0.0-beta.28 - -## 4.0.0-beta.27 - -### Patch Changes - -- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: - - effect@4.0.0-beta.27 - - @effect/platform-node-shared@4.0.0-beta.27 - -## 4.0.0-beta.26 - -### Patch Changes - -- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: - - effect@4.0.0-beta.26 - - @effect/platform-node-shared@4.0.0-beta.26 - -## 4.0.0-beta.25 - -### Patch Changes - -- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: - - effect@4.0.0-beta.25 - - @effect/platform-node-shared@4.0.0-beta.25 - -## 4.0.0-beta.24 - -### Patch Changes - -- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: - - effect@4.0.0-beta.24 - - @effect/platform-node-shared@4.0.0-beta.24 - -## 4.0.0-beta.23 - -### Patch Changes - -- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: - - effect@4.0.0-beta.23 - - @effect/platform-node-shared@4.0.0-beta.23 - -## 4.0.0-beta.22 - -### Patch Changes - -- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: - - effect@4.0.0-beta.22 - - @effect/platform-node-shared@4.0.0-beta.22 - -## 4.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: - - effect@4.0.0-beta.21 - - @effect/platform-node-shared@4.0.0-beta.21 - -## 4.0.0-beta.20 - -### Patch Changes - -- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: - - effect@4.0.0-beta.20 - - @effect/platform-node-shared@4.0.0-beta.20 - -## 4.0.0-beta.19 - -### Patch Changes - -- Updated dependencies [[`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f)]: - - @effect/platform-node-shared@4.0.0-beta.19 - - effect@4.0.0-beta.19 - -## 4.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: - - effect@4.0.0-beta.18 - - @effect/platform-node-shared@4.0.0-beta.18 - -## 4.0.0-beta.17 - -### Patch Changes - -- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: - - effect@4.0.0-beta.17 - - @effect/platform-node-shared@4.0.0-beta.17 - -## 4.0.0-beta.16 - -### Patch Changes - -- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: - - effect@4.0.0-beta.16 - - @effect/platform-node-shared@4.0.0-beta.16 - -## 4.0.0-beta.15 - -### Patch Changes - -- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: - - effect@4.0.0-beta.15 - - @effect/platform-node-shared@4.0.0-beta.15 - -## 4.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: - - effect@4.0.0-beta.14 - - @effect/platform-node-shared@4.0.0-beta.14 - -## 4.0.0-beta.13 - -### Patch Changes - -- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: - - effect@4.0.0-beta.13 - - @effect/platform-node-shared@4.0.0-beta.13 - -## 4.0.0-beta.12 - -### Patch Changes - -- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts - -- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: - - effect@4.0.0-beta.12 - - @effect/platform-node-shared@4.0.0-beta.12 - -## 4.0.0-beta.11 - -### Patch Changes - -- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: - - effect@4.0.0-beta.11 - - @effect/platform-node-shared@4.0.0-beta.11 - -## 4.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: - - effect@4.0.0-beta.10 - - @effect/platform-node-shared@4.0.0-beta.10 - -## 4.0.0-beta.9 - -### Patch Changes - -- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: - - effect@4.0.0-beta.9 - - @effect/platform-node-shared@4.0.0-beta.9 - -## 4.0.0-beta.8 - -### Patch Changes - -- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: - - effect@4.0.0-beta.8 - - @effect/platform-node-shared@4.0.0-beta.8 - -## 4.0.0-beta.7 - -### Patch Changes - -- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: - - effect@4.0.0-beta.7 - - @effect/platform-node-shared@4.0.0-beta.7 - -## 4.0.0-beta.6 - -### Patch Changes - -- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: - - effect@4.0.0-beta.6 - - @effect/platform-node-shared@4.0.0-beta.6 - -## 4.0.0-beta.5 - -### Patch Changes - -- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: - - effect@4.0.0-beta.5 - - @effect/platform-node-shared@4.0.0-beta.5 - -## 4.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: - - effect@4.0.0-beta.4 - - @effect/platform-node-shared@4.0.0-beta.4 - -## 4.0.0-beta.3 - -### Patch Changes - -- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: - - effect@4.0.0-beta.3 - - @effect/platform-node-shared@4.0.0-beta.3 - -## 4.0.0-beta.2 - -### Patch Changes - -- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: - - effect@4.0.0-beta.2 - - @effect/platform-node-shared@4.0.0-beta.2 - -## 4.0.0-beta.1 - -### Patch Changes - -- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: - - effect@4.0.0-beta.1 - - @effect/platform-node-shared@4.0.0-beta.1 - -## 4.0.0-beta.0 - -### Major Changes - -- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta - -### Patch Changes - -- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: - - @effect/platform-node-shared@4.0.0-beta.0 - - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-bun/README.md b/.context/effect/packages/platform-bun/README.md deleted file mode 100644 index 7b953f9a0..000000000 --- a/.context/effect/packages/platform-bun/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `@effect/platform-bun` - -Provides Bun-specific implementations for Effect's platform abstractions, 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/.context/effect/packages/platform-bun/docgen.json b/.context/effect/packages/platform-bun/docgen.json deleted file mode 100644 index f909ae1cc..000000000 --- a/.context/effect/packages/platform-bun/docgen.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/platform-bun/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/platform-bun/package.json b/.context/effect/packages/platform-bun/package.json deleted file mode 100644 index 6092549f5..000000000 --- a/.context/effect/packages/platform-bun/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "@effect/platform-bun", - "type": "module", - "version": "4.0.0-beta.101", - "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" - ], - "sideEffects": [], - "exports": { - "./package.json": "./package.json", - ".": "./src/index.ts", - "./*": "./src/*.ts", - "./internal/*": null, - "./*/index": null - }, - "files": [ - "src/**/*.ts", - "dist/**/*.js", - "dist/**/*.js.map", - "dist/**/*.d.ts", - "dist/**/*.d.ts.map" - ], - "publishConfig": { - "access": "public", - "provenance": true, - "exports": { - "./package.json": "./package.json", - ".": "./dist/index.js", - "./*": "./dist/*.js", - "./internal/*": null, - "./*/index": null - } - }, - "scripts": { - "codegen": "effect-utils codegen", - "build": "tsc -b tsconfig.json && pnpm babel", - "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" - }, - "peerDependencies": { - "effect": "workspace:^" - }, - "dependencies": { - "@effect/platform-node-shared": "workspace:^" - }, - "devDependencies": { - "@types/bun": "^1.3.14", - "effect": "workspace:^" - } -} diff --git a/.context/effect/packages/platform-bun/src/BunHttpPlatform.ts b/.context/effect/packages/platform-bun/src/BunHttpPlatform.ts deleted file mode 100644 index 4fe1e85ce..000000000 --- a/.context/effect/packages/platform-bun/src/BunHttpPlatform.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Bun implementation of the Effect HTTP platform service. - * - * This module provides one `layer` for `HttpPlatform`. It implements file - * responses with `Bun.file`, supports sliced file responses for byte ranges, - * and returns Web `File` values as raw HTTP server responses. The layer also - * provides the Bun file-system layer and ETag generator required by - * `HttpPlatform`. - * - * @since 4.0.0 - */ -import type { Effect } from "effect" -import type { FileSystem } from "effect/FileSystem" -import * as Layer from "effect/Layer" -import * as Etag from "effect/unstable/http/Etag" -import * as Platform from "effect/unstable/http/HttpPlatform" -import * as Response from "effect/unstable/http/HttpServerResponse" -import * as BunFileSystem from "./BunFileSystem.ts" - -/** - * @category constructors - * @since 4.0.0 - */ -const make: Effect.Effect< - Platform.HttpPlatform["Service"], - never, - FileSystem | Etag.Generator -> = 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 Response.raw(file, { headers, status, statusText }) - }, - fileWebResponse(file, status, statusText, headers, _options) { - return Response.raw(file, { headers, status, statusText }) - } -}) - -/** - * Layer that provides the Bun `HttpPlatform`, including file responses backed by `Bun.file`. - * - * @category layers - * @since 4.0.0 - */ -export const layer = Layer.effect(Platform.HttpPlatform)(make).pipe( - Layer.provide(BunFileSystem.layer), - Layer.provide(Etag.layer) -) diff --git a/.context/effect/packages/platform-bun/tsconfig.json b/.context/effect/packages/platform-bun/tsconfig.json deleted file mode 100644 index 7ed30dd4a..000000000 --- a/.context/effect/packages/platform-bun/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "include": ["src"], - "references": [ - { "path": "../effect" }, - { "path": "../platform-node-shared" } - ], - "compilerOptions": { - "types": ["bun"] - } -} diff --git a/.context/effect/packages/platform-node-shared/CHANGELOG.md b/.context/effect/packages/platform-node-shared/CHANGELOG.md deleted file mode 100644 index a2c5696f3..000000000 --- a/.context/effect/packages/platform-node-shared/CHANGELOG.md +++ /dev/null @@ -1,747 +0,0 @@ -# @effect/platform-node-shared - -## 4.0.0-beta.101 - -### Patch Changes - -- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: - - effect@4.0.0-beta.101 - -## 4.0.0-beta.100 - -### Patch Changes - -- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: - - effect@4.0.0-beta.100 - -## 4.0.0-beta.99 - -### Patch Changes - -- [#6411](https://github.com/Effect-TS/effect/pull/6411) [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd) Thanks @sbking! - Fix child process termination to escalate to `SIGKILL` when the initial signal does not stop the process within `forceKillAfter`. - -- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: - - effect@4.0.0-beta.99 - -## 4.0.0-beta.98 - -### Patch Changes - -- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: - - effect@4.0.0-beta.98 - -## 4.0.0-beta.97 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.97 - -## 4.0.0-beta.96 - -### Patch Changes - -- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: - - effect@4.0.0-beta.96 - -## 4.0.0-beta.95 - -### Patch Changes - -- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: - - effect@4.0.0-beta.95 - -## 4.0.0-beta.94 - -### Patch Changes - -- [#2523](https://github.com/Effect-TS/effect-smol/pull/2523) [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c) Thanks @rajzik! - Add glob to filesystem - -- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: - - effect@4.0.0-beta.94 - -## 4.0.0-beta.93 - -### Patch Changes - -- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: - - effect@4.0.0-beta.93 - -## 4.0.0-beta.92 - -### Patch Changes - -- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: - - effect@4.0.0-beta.92 - -## 4.0.0-beta.91 - -### Patch Changes - -- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: - - effect@4.0.0-beta.91 - -## 4.0.0-beta.90 - -### Patch Changes - -- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: - - effect@4.0.0-beta.90 - -## 4.0.0-beta.89 - -### Patch Changes - -- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: - - effect@4.0.0-beta.89 - -## 4.0.0-beta.88 - -### Patch Changes - -- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: - - effect@4.0.0-beta.88 - -## 4.0.0-beta.87 - -### Patch Changes - -- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: - - effect@4.0.0-beta.87 - -## 4.0.0-beta.86 - -### Patch Changes - -- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: - - effect@4.0.0-beta.86 - -## 4.0.0-beta.85 - -### Patch Changes - -- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: - - effect@4.0.0-beta.85 - -## 4.0.0-beta.84 - -### Patch Changes - -- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: - - effect@4.0.0-beta.84 - -## 4.0.0-beta.83 - -### Patch Changes - -- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: - - effect@4.0.0-beta.83 - -## 4.0.0-beta.82 - -### Patch Changes - -- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: - - effect@4.0.0-beta.82 - -## 4.0.0-beta.81 - -### Patch Changes - -- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: - - effect@4.0.0-beta.81 - -## 4.0.0-beta.80 - -### Patch Changes - -- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: - - effect@4.0.0-beta.80 - -## 4.0.0-beta.79 - -### Patch Changes - -- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: - - effect@4.0.0-beta.79 - -## 4.0.0-beta.78 - -### Patch Changes - -- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: - - effect@4.0.0-beta.78 - -## 4.0.0-beta.77 - -### Patch Changes - -- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: - - effect@4.0.0-beta.77 - -## 4.0.0-beta.76 - -### Patch Changes - -- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: - - effect@4.0.0-beta.76 - -## 4.0.0-beta.75 - -### Patch Changes - -- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: - - effect@4.0.0-beta.75 - -## 4.0.0-beta.74 - -### Patch Changes - -- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: - - effect@4.0.0-beta.74 - -## 4.0.0-beta.73 - -### Patch Changes - -- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: - - effect@4.0.0-beta.73 - -## 4.0.0-beta.72 - -### Patch Changes - -- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: - - effect@4.0.0-beta.72 - -## 4.0.0-beta.71 - -### Patch Changes - -- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: - - effect@4.0.0-beta.71 - -## 4.0.0-beta.70 - -### Patch Changes - -- [#2235](https://github.com/Effect-TS/effect-smol/pull/2235) [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3) Thanks @gcanti! - Add the package root barrel export. - -- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: - - effect@4.0.0-beta.70 - -## 4.0.0-beta.69 - -### Patch Changes - -- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: - - effect@4.0.0-beta.69 - -## 4.0.0-beta.68 - -### Patch Changes - -- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. - -- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: - - effect@4.0.0-beta.68 - -## 4.0.0-beta.67 - -### Patch Changes - -- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: - - effect@4.0.0-beta.67 - -## 4.0.0-beta.66 - -### Patch Changes - -- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: - - effect@4.0.0-beta.66 - -## 4.0.0-beta.65 - -### Patch Changes - -- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: - - effect@4.0.0-beta.65 - -## 4.0.0-beta.64 - -### Patch Changes - -- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: - - effect@4.0.0-beta.64 - -## 4.0.0-beta.63 - -### Patch Changes - -- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: - - effect@4.0.0-beta.63 - -## 4.0.0-beta.62 - -### Patch Changes - -- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: - - effect@4.0.0-beta.62 - -## 4.0.0-beta.61 - -### Patch Changes - -- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: - - effect@4.0.0-beta.61 - -## 4.0.0-beta.60 - -### Patch Changes - -- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: - - effect@4.0.0-beta.60 - -## 4.0.0-beta.59 - -### Patch Changes - -- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: - - effect@4.0.0-beta.59 - -## 4.0.0-beta.58 - -### Patch Changes - -- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - generate binary arrays from streams with less copying - -- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption - -- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: - - effect@4.0.0-beta.58 - -## 4.0.0-beta.57 - -### Patch Changes - -- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: - - effect@4.0.0-beta.57 - -## 4.0.0-beta.56 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.56 - -## 4.0.0-beta.55 - -### Patch Changes - -- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: - - effect@4.0.0-beta.55 - -## 4.0.0-beta.54 - -### Patch Changes - -- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: - - effect@4.0.0-beta.54 - -## 4.0.0-beta.53 - -### Patch Changes - -- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: - - effect@4.0.0-beta.53 - -## 4.0.0-beta.52 - -### Patch Changes - -- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: - - effect@4.0.0-beta.52 - -## 4.0.0-beta.51 - -### Patch Changes - -- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: - - effect@4.0.0-beta.51 - -## 4.0.0-beta.50 - -### Patch Changes - -- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: - - effect@4.0.0-beta.50 - -## 4.0.0-beta.49 - -### Patch Changes - -- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: - - effect@4.0.0-beta.49 - -## 4.0.0-beta.48 - -### Patch Changes - -- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: - - effect@4.0.0-beta.48 - -## 4.0.0-beta.47 - -### Patch Changes - -- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: - - effect@4.0.0-beta.47 - -## 4.0.0-beta.46 - -### Patch Changes - -- [`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505) Thanks @tim-smart! - don't remove SIGINT listener until fiber exit - -- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: - - effect@4.0.0-beta.46 - -## 4.0.0-beta.45 - -### Patch Changes - -- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: - - effect@4.0.0-beta.45 - -## 4.0.0-beta.44 - -### Patch Changes - -- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. - -- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. - -- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: - - effect@4.0.0-beta.44 - -## 4.0.0-beta.43 - -### Patch Changes - -- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: - - effect@4.0.0-beta.43 - -## 4.0.0-beta.42 - -### Patch Changes - -- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: - - effect@4.0.0-beta.42 - -## 4.0.0-beta.41 - -### Patch Changes - -- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: - - effect@4.0.0-beta.41 - -## 4.0.0-beta.40 - -### Patch Changes - -- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: - - effect@4.0.0-beta.40 - -## 4.0.0-beta.39 - -### Patch Changes - -- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: - - effect@4.0.0-beta.39 - -## 4.0.0-beta.38 - -### Patch Changes - -- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: - - effect@4.0.0-beta.38 - -## 4.0.0-beta.37 - -### Patch Changes - -- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: - - effect@4.0.0-beta.37 - -## 4.0.0-beta.36 - -### Patch Changes - -- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: - - effect@4.0.0-beta.36 - -## 4.0.0-beta.35 - -### Patch Changes - -- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: - - effect@4.0.0-beta.35 - -## 4.0.0-beta.34 - -### Patch Changes - -- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: - - effect@4.0.0-beta.34 - -## 4.0.0-beta.33 - -### Patch Changes - -- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: - - effect@4.0.0-beta.33 - -## 4.0.0-beta.32 - -### Patch Changes - -- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: - - effect@4.0.0-beta.32 - -## 4.0.0-beta.31 - -### Patch Changes - -- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: - - effect@4.0.0-beta.31 - -## 4.0.0-beta.30 - -### Patch Changes - -- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: - - effect@4.0.0-beta.30 - -## 4.0.0-beta.29 - -### Patch Changes - -- [#1671](https://github.com/Effect-TS/effect-smol/pull/1671) [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0) Thanks @tim-smart! - default to endOnDone: false in NodeStdio - -- [#1671](https://github.com/Effect-TS/effect-smol/pull/1671) [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0) Thanks @tim-smart! - catch errors in pullIntoWritable - -- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: - - effect@4.0.0-beta.29 - -## 4.0.0-beta.28 - -### Patch Changes - -- [#1658](https://github.com/Effect-TS/effect-smol/pull/1658) [`0fc977b`](https://github.com/Effect-TS/effect-smol/commit/0fc977b20d08caee7f64b2065e68f84afe124316) Thanks @nikelborm! - Add `endOnDone` option to Stdio stdout / stderr - -- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: - - effect@4.0.0-beta.28 - -## 4.0.0-beta.27 - -### Patch Changes - -- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: - - effect@4.0.0-beta.27 - -## 4.0.0-beta.26 - -### Patch Changes - -- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: - - effect@4.0.0-beta.26 - -## 4.0.0-beta.25 - -### Patch Changes - -- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: - - effect@4.0.0-beta.25 - -## 4.0.0-beta.24 - -### Patch Changes - -- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: - - effect@4.0.0-beta.24 - -## 4.0.0-beta.23 - -### Patch Changes - -- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: - - effect@4.0.0-beta.23 - -## 4.0.0-beta.22 - -### Patch Changes - -- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: - - effect@4.0.0-beta.22 - -## 4.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: - - effect@4.0.0-beta.21 - -## 4.0.0-beta.20 - -### Patch Changes - -- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: - - effect@4.0.0-beta.20 - -## 4.0.0-beta.19 - -### Patch Changes - -- [#1526](https://github.com/Effect-TS/effect-smol/pull/1526) [`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f) Thanks @tim-smart! - fix fs.stat when blksize is undefined - -- Updated dependencies []: - - effect@4.0.0-beta.19 - -## 4.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: - - effect@4.0.0-beta.18 - -## 4.0.0-beta.17 - -### Patch Changes - -- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: - - effect@4.0.0-beta.17 - -## 4.0.0-beta.16 - -### Patch Changes - -- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: - - effect@4.0.0-beta.16 - -## 4.0.0-beta.15 - -### Patch Changes - -- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: - - effect@4.0.0-beta.15 - -## 4.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: - - effect@4.0.0-beta.14 - -## 4.0.0-beta.13 - -### Patch Changes - -- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: - - effect@4.0.0-beta.13 - -## 4.0.0-beta.12 - -### Patch Changes - -- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: - - effect@4.0.0-beta.12 - -## 4.0.0-beta.11 - -### Patch Changes - -- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: - - effect@4.0.0-beta.11 - -## 4.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: - - effect@4.0.0-beta.10 - -## 4.0.0-beta.9 - -### Patch Changes - -- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: - - effect@4.0.0-beta.9 - -## 4.0.0-beta.8 - -### Patch Changes - -- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: - - effect@4.0.0-beta.8 - -## 4.0.0-beta.7 - -### Patch Changes - -- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: - - effect@4.0.0-beta.7 - -## 4.0.0-beta.6 - -### Patch Changes - -- [#1349](https://github.com/Effect-TS/effect-smol/pull/1349) [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878) Thanks @tim-smart! - simplify NodeChildSpawner stdout streams - -- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: - - effect@4.0.0-beta.6 - -## 4.0.0-beta.5 - -### Patch Changes - -- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: - - effect@4.0.0-beta.5 - -## 4.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: - - effect@4.0.0-beta.4 - -## 4.0.0-beta.3 - -### Patch Changes - -- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: - - effect@4.0.0-beta.3 - -## 4.0.0-beta.2 - -### Patch Changes - -- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: - - effect@4.0.0-beta.2 - -## 4.0.0-beta.1 - -### Patch Changes - -- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: - - effect@4.0.0-beta.1 - -## 4.0.0-beta.0 - -### Major Changes - -- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta - -### Patch Changes - -- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: - - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-node-shared/README.md b/.context/effect/packages/platform-node-shared/README.md deleted file mode 100644 index 52425e592..000000000 --- a/.context/effect/packages/platform-node-shared/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `@effect/platform-node-shared` - -Provides shared Node.js-compatible implementations used by the Effect Node.js and Bun platform packages. - -## Documentation - -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/platform-node-shared). diff --git a/.context/effect/packages/platform-node-shared/docgen.json b/.context/effect/packages/platform-node-shared/docgen.json deleted file mode 100644 index cb127e0a0..000000000 --- a/.context/effect/packages/platform-node-shared/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$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"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/platform-node-shared/package.json b/.context/effect/packages/platform-node-shared/package.json deleted file mode 100644 index 1557e3f4a..000000000 --- a/.context/effect/packages/platform-node-shared/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@effect/platform-node-shared", - "type": "module", - "version": "4.0.0-beta.101", - "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" - ], - "engines": { - "node": ">=18.0.0" - }, - "sideEffects": [], - "exports": { - "./package.json": "./package.json", - ".": "./src/index.ts", - "./*": "./src/*.ts", - "./internal/*": null, - "./*/index": null - }, - "files": [ - "src/**/*.ts", - "dist/**/*.js", - "dist/**/*.js.map", - "dist/**/*.d.ts", - "dist/**/*.d.ts.map" - ], - "publishConfig": { - "access": "public", - "provenance": true, - "exports": { - "./package.json": "./package.json", - ".": "./dist/index.js", - "./*": "./dist/*.js", - "./internal/*": null, - "./*/index": null - } - }, - "scripts": { - "codegen": "effect-utils codegen", - "build": "tsc -b tsconfig.json && pnpm babel", - "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" - }, - "peerDependencies": { - "effect": "workspace:^" - }, - "devDependencies": { - "@types/node": "^26.1.1", - "effect": "workspace:^", - "tar": "^7.5.19" - }, - "dependencies": { - "@types/ws": "^8.18.1", - "ws": "^8.21.0" - } -} diff --git a/.context/effect/packages/platform-node-shared/src/NodePath.ts b/.context/effect/packages/platform-node-shared/src/NodePath.ts deleted file mode 100644 index c0dc5a43b..000000000 --- a/.context/effect/packages/platform-node-shared/src/NodePath.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Node-backed provider for Effect's `Path` service. - * - * This module turns Node's `node:path` and `node:url` APIs into `Path` layers. - * `layer` uses the host platform path implementation, while `layerPosix` and - * `layerWin32` provide fixed POSIX and Windows variants. All three layers also - * include helpers for converting between file paths and file URLs. - * - * @since 4.0.0 - */ -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import { Path, TypeId } from "effect/Path" -import { BadArgument } from "effect/PlatformError" -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: (cause) => - new BadArgument({ - module: "Path", - method: "fromFileUrl", - cause - }) - }) - -const toFileUrl = (path: string): Effect.Effect => - Effect.try({ - try: () => NodeUrl.pathToFileURL(path), - catch: (cause) => - new BadArgument({ - module: "Path", - method: "toFileUrl", - cause - }) - }) - -/** - * Provides the `Path` service using Node's POSIX path implementation plus - * file URL conversion helpers. - * - * @category layers - * @since 4.0.0 - */ -export const layerPosix: Layer.Layer = Layer.succeed(Path)({ - [TypeId]: TypeId, - ...NodePath.posix, - fromFileUrl, - toFileUrl -}) - -/** - * Provides the `Path` service using Node's Windows path implementation plus - * file URL conversion helpers. - * - * @category layers - * @since 4.0.0 - */ -export const layerWin32: Layer.Layer = Layer.succeed(Path)({ - [TypeId]: TypeId, - ...NodePath.win32, - fromFileUrl, - toFileUrl -}) - -/** - * Provides the default `Path` service using the host platform's Node path - * implementation plus file URL conversion helpers. - * - * @category layers - * @since 4.0.0 - */ -export const layer: Layer.Layer = Layer.succeed(Path)({ - [TypeId]: TypeId, - ...NodePath, - fromFileUrl, - toFileUrl -}) diff --git a/.context/effect/packages/platform-node-shared/src/NodeTerminal.ts b/.context/effect/packages/platform-node-shared/src/NodeTerminal.ts deleted file mode 100644 index 1a5e962b4..000000000 --- a/.context/effect/packages/platform-node-shared/src/NodeTerminal.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Shared Node.js implementation of Effect's `Terminal` service. - * - * `NodeTerminal` adapts Node's `readline` APIs plus the current process - * `stdin` and `stdout` streams into {@link Terminal.Terminal}. The service can - * display output, read a line, stream key input, and read terminal dimensions. - * `make` manages readline and TTY raw mode in a scope, while `layer` provides - * the default service that ends key input on Ctrl+C or Ctrl+D. - * - * @since 4.0.0 - */ -import type * as Cause from "effect/Cause" -import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" -import * as Option from "effect/Option" -import { badArgument, type PlatformError } from "effect/PlatformError" -import * as Predicate from "effect/Predicate" -import * as Queue from "effect/Queue" -import * as RcRef from "effect/RcRef" -import type * as Scope from "effect/Scope" -import * as Terminal from "effect/Terminal" -import * as readline from "node:readline" - -/** - * Creates a scoped process-backed `Terminal` using Node `readline`, enabling - * TTY raw mode while in scope and using the supplied predicate to decide when - * key input should end. - * - * @category constructors - * @since 4.0.0 - */ -export const make: ( - shouldQuit?: (input: Terminal.UserInput) => boolean -) => Effect.Effect = 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 readInput = Effect.gen(function*() { - yield* RcRef.get(rlRef) - const queue = yield* Queue.make() - const handleKeypress = (s: string | undefined, k: readline.Key) => { - const userInput = { - input: Option.fromUndefinedOr(s), - key: { name: k.name ?? "", ctrl: !!k.ctrl, meta: !!k.meta, shift: !!k.shift } - } - Queue.offerUnsafe(queue, userInput) - if (shouldQuit(userInput)) { - Queue.endUnsafe(queue) - } - } - yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("keypress", handleKeypress))) - stdin.on("keypress", handleKeypress) - return queue as Queue.Dequeue - }) - - const readLine = Effect.scoped( - Effect.flatMap(RcRef.get(rlRef), (readlineInterface) => - Effect.callback((resume) => { - const onLine = (line: string) => resume(Effect.succeed(line)) - readlineInterface.once("line", onLine) - return Effect.sync(() => readlineInterface.off("line", onLine)) - })) - ) - - const display = (prompt: string) => - Effect.uninterruptible( - Effect.callback((resume) => { - stdout.write(prompt, (err) => - Predicate.isNullish(err) - ? resume(Effect.void) - : resume(Effect.fail( - badArgument({ - module: "Terminal", - method: "display", - description: "Failed to write prompt to stdout", - cause: err - }) - ))) - }) - ) - - return Terminal.make({ - columns, - rows, - readInput, - readLine, - display - }) - } -) - -/** - * Provides the default process-backed `Terminal` service, ending key input on - * Ctrl+C or Ctrl+D. - * - * @category layers - * @since 4.0.0 - */ -export const layer: Layer.Layer = Layer.effect(Terminal.Terminal, make(defaultShouldQuit)) - -function defaultShouldQuit(input: Terminal.UserInput) { - return input.key.ctrl && (input.key.name === "c" || input.key.name === "d") -} diff --git a/.context/effect/packages/platform-node-shared/src/index.ts b/.context/effect/packages/platform-node-shared/src/index.ts deleted file mode 100644 index e3c162e9e..000000000 --- a/.context/effect/packages/platform-node-shared/src/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @since 4.0.0 - */ - -// @barrel: Auto-generated exports. Do not edit manually. - -/** - * @since 4.0.0 - */ -export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts" - -/** - * @since 4.0.0 - */ -export * as NodeClusterSocket from "./NodeClusterSocket.ts" - -/** - * @since 1.0.0 - */ -export * as NodeCrypto from "./NodeCrypto.ts" - -/** - * @since 4.0.0 - */ -export * as NodeFileSystem from "./NodeFileSystem.ts" - -/** - * @since 4.0.0 - */ -export * as NodePath from "./NodePath.ts" - -/** - * @since 4.0.0 - */ -export * as NodeRuntime from "./NodeRuntime.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSink from "./NodeSink.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSocket from "./NodeSocket.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSocketServer from "./NodeSocketServer.ts" - -/** - * @since 4.0.0 - */ -export * as NodeStdio from "./NodeStdio.ts" - -/** - * @since 4.0.0 - */ -export * as NodeStream from "./NodeStream.ts" - -/** - * @since 4.0.0 - */ -export * as NodeTerminal from "./NodeTerminal.ts" diff --git a/.context/effect/packages/platform-node-shared/src/internal/utils.ts b/.context/effect/packages/platform-node-shared/src/internal/utils.ts deleted file mode 100644 index c07d5f20c..000000000 --- a/.context/effect/packages/platform-node-shared/src/internal/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { SystemError, SystemErrorTag } from "effect/PlatformError" -import * as PlatformError from "effect/PlatformError" -import type { PathLike } from "node:fs" - -/** @internal */ -export const handleErrnoException = (module: SystemError["module"], method: string) => -( - err: NodeJS.ErrnoException, - [path]: [path: PathLike | number | string | readonly string[], ...args: Array] -): PlatformError.PlatformError => { - let reason: SystemErrorTag = "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 PlatformError.systemError({ - _tag: reason, - module, - method, - pathOrDescriptor: path as string | number, - syscall: err.syscall, - cause: err - }) -} diff --git a/.context/effect/packages/platform-node-shared/test/NodeChildProcessSpawner.test.ts b/.context/effect/packages/platform-node-shared/test/NodeChildProcessSpawner.test.ts deleted file mode 100644 index c43886074..000000000 --- a/.context/effect/packages/platform-node-shared/test/NodeChildProcessSpawner.test.ts +++ /dev/null @@ -1,1149 +0,0 @@ -import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner" -import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" -import * as NodePath from "@effect/platform-node-shared/NodePath" -import { assert, describe, it } from "@effect/vitest" -import * as Effect from "effect/Effect" -import * as Exit from "effect/Exit" -import * as FileSystem from "effect/FileSystem" -import * as Layer from "effect/Layer" -import * as Path from "effect/Path" -import * as PlatformError from "effect/PlatformError" -import * as Schedule from "effect/Schedule" -import * as Scope from "effect/Scope" -import * as Stream from "effect/Stream" -import * as TestClock from "effect/testing/TestClock" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" - -const TEST_BASH_SCRIPTS_PATH = [__dirname, "fixtures", "bash"] - -const NodeServices = NodeChildProcessSpawner.layer.pipe( - Layer.provideMerge(Layer.mergeAll( - NodeFileSystem.layer, - NodePath.layer - )) -) - -// Helper to collect stream output into a string -const decodeByteStream = Effect.fnUntraced( - function*( - stream: Stream.Stream, - encoding: ChildProcess.Encoding = "utf-8" - ) { - const chunks = yield* Stream.runCollect(stream) - const totalLength = chunks.reduce((acc, c) => acc + c.length, 0) - const result = new Uint8Array(totalLength) - let offset = 0 - for (const chunk of chunks) { - result.set(chunk, offset) - offset += chunk.length - } - return new TextDecoder(encoding).decode(result).trim() - } -) - -describe("NodeChildProcessSpawner", () => { - it.layer(NodeServices)((it) => { - describe("spawn", () => { - describe("basic spawning", () => { - it.effect("should spawn a simple command and collect output", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("node", ["--version"]) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // Verify it contains "v" (version string starts with v) - assert.isTrue(output.includes("v")) - }).pipe(Effect.scoped)) - - it.effect("should spawn echo command", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["hello", "world"]) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "hello world") - }).pipe(Effect.scoped)) - - it.effect("should spawn with template literal", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo spawned` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "spawned") - }).pipe(Effect.scoped)) - }) - - describe("cwd option", () => { - it.effect("should handle command with working directory", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("pwd", [], { cwd: "/tmp" }) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // On macOS, /tmp is a symlink to /private/tmp - assert.isTrue(output.includes("tmp")) - }).pipe(Effect.scoped)) - - it.effect("should use cwd with template literal form", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make({ cwd: "/tmp" })`pwd` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.isTrue(output.includes("tmp")) - }).pipe(Effect.scoped)) - }) - - describe("env option", () => { - it.effect("should handle environment variables", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo $TEST_VAR"], { - env: { TEST_VAR: "test_value" }, - extendEnv: true - }) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "test_value") - }).pipe(Effect.scoped)) - - it.effect("should handle multiple environment variables", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo $VAR1-$VAR2-$VAR3"], { - env: { VAR1: "one", VAR2: "two", VAR3: "three" }, - extendEnv: true - }) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "one-two-three") - }).pipe(Effect.scoped)) - - it.effect("should merge environment variables with setEnv", () => - Effect.gen(function*() { - const command = ChildProcess.make("sh", ["-c", "echo $VAR1-$VAR2-$VAR3"], { - env: { VAR1: "one", VAR2: "two" }, - extendEnv: true - }).pipe(ChildProcess.setEnv({ VAR2: "override", VAR3: "three" })) - const handle = yield* command - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "one-override-three") - }).pipe(Effect.scoped)) - }) - - describe("shell option", () => { - it.effect("should execute with shell when using sh -c", () => - Effect.gen(function*() { - // Use sh -c to test shell expansion without triggering deprecation warning - const handle = yield* ChildProcess.make("sh", ["-c", "echo $HOME"]) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // With shell, $HOME should be expanded - assert.isTrue(output.length > 0) - assert.isFalse(output.includes("$HOME")) - }).pipe(Effect.scoped)) - - it.effect("should not expand variables without shell", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["$HOME"], { shell: false }) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // Without shell, $HOME should not be expanded - assert.strictEqual(output, "$HOME") - }).pipe(Effect.scoped)) - - it.effect("should allow piping with shell", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo hello | tr a-z A-Z"]) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO") - }).pipe(Effect.scoped)) - }) - - describe("template literal forms", () => { - it.effect("should work with template literal form", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "hello") - }).pipe(Effect.scoped)) - - it.effect("should handle string interpolation", () => - Effect.gen(function*() { - const name = "world" - const handle = yield* ChildProcess.make`echo hello ${name}` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "hello world") - }).pipe(Effect.scoped)) - - it.effect("should handle number interpolation", () => - Effect.gen(function*() { - const count = 42 - const handle = yield* ChildProcess.make`echo count is ${count}` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "count is 42") - }).pipe(Effect.scoped)) - - it.effect("should handle array interpolation", () => - Effect.gen(function*() { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const dir = yield* fs.makeTempDirectoryScoped() - const file = path.join(dir, "array-interpolation.txt") - const args = ["-l", "-a"] - yield* fs.writeFile(file, new TextEncoder().encode("test")) - - const handle = yield* ChildProcess.make`ls ${args} ${dir}` - const exitCode = yield* handle.exitCode - const output = yield* decodeByteStream(handle.stdout) - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.isTrue(output.includes("array-interpolation.txt")) - }).pipe(Effect.scoped)) - - it.effect("should handle multiple interpolations", () => - Effect.gen(function*() { - const greeting = "hello" - const target = "world" - const handle = yield* ChildProcess.make`echo ${greeting} ${target}` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "hello world") - }).pipe(Effect.scoped)) - - it.effect("should handle options with template literal", () => - Effect.gen(function*() { - const filename = "test.txt" - const handle = yield* ChildProcess.make({ cwd: "/tmp" })`echo ${filename}` - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "test.txt") - }).pipe(Effect.scoped)) - }) - - describe("stderr streaming", () => { - it.effect("should capture stderr output", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo error message >&2"]) - const stderr = yield* decodeByteStream(handle.stderr) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stderr, "error message") - }).pipe(Effect.scoped)) - - it.effect("should capture both stdout and stderr", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo stdout; echo stderr >&2"]) - const stdout = yield* decodeByteStream(handle.stdout) - const stderr = yield* decodeByteStream(handle.stderr) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, "stdout") - assert.strictEqual(stderr, "stderr") - }).pipe(Effect.scoped)) - - it.effect("should handle more stdout than stderr", () => - Effect.gen(function*() { - // Process outputs many lines to stdout but only one to stderr - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo line1; echo line2; echo line3; echo line4; echo line5; echo error >&2"] - ) - const [stdout, stderr] = yield* Effect.all([ - decodeByteStream(handle.stdout), - decodeByteStream(handle.stderr) - ], { concurrency: "unbounded" }) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, ["line1", "line2", "line3", "line4", "line5"].join("\n")) - assert.strictEqual(stderr, "error") - }).pipe(Effect.scoped)) - - it.effect("should handle more stderr than stdout", () => - Effect.gen(function*() { - // Process outputs many lines to stderr but only one to stdout - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo output; echo err1 >&2; echo err2 >&2; echo err3 >&2; echo err4 >&2; echo err5 >&2"] - ) - const stdout = yield* decodeByteStream(handle.stdout) - const stderr = yield* decodeByteStream(handle.stderr) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, "output") - assert.strictEqual(stderr, ["err1", "err2", "err3", "err4", "err5"].join("\n")) - }).pipe(Effect.scoped)) - - it.effect("should allow reading only stdout when stderr is empty", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["only stdout"]) - // Read streams in parallel to avoid deadlock when one stream is empty - const [stdout, stderr] = yield* Effect.all([ - decodeByteStream(handle.stdout), - decodeByteStream(handle.stderr) - ], { concurrency: "unbounded" }) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, "only stdout") - assert.strictEqual(stderr, "") - }).pipe(Effect.scoped)) - - it.effect("should allow reading only stderr when stdout is empty", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo only stderr >&2"]) - // Read streams in parallel to avoid deadlock when one stream is empty - const [stdout, stderr] = yield* Effect.all([ - decodeByteStream(handle.stdout), - decodeByteStream(handle.stderr) - ], { concurrency: "unbounded" }) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, "") - assert.strictEqual(stderr, "only stderr") - }).pipe(Effect.scoped)) - }) - - describe("combined output (all)", () => { - it.effect("should read interspersed stdout and stderr via .all", () => - Effect.gen(function*() { - // Use sleep to force buffer flushes between writes, ensuring - // stdout and stderr chunks arrive separately for proper interleaving - const handle = yield* ChildProcess.make( - "sh", - [ - "-c", - [ - "echo stdout1; sleep 0.01;", - "echo stderr1 >&2; sleep 0.01;", - "echo stdout2; sleep 0.01;", - "echo stderr2 >&2" - ].join(" ") - ] - ) - const all = yield* decodeByteStream(handle.all) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // With delays forcing buffer flushes, we should see proper interleaving - assert.strictEqual(all, ["stdout1", "stderr1", "stdout2", "stderr2"].join("\n")) - }).pipe(Effect.scoped)) - - it.effect("should capture only stdout via .all when no stderr", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["hello from stdout"]) - const all = yield* decodeByteStream(handle.all) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(all, "hello from stdout") - }).pipe(Effect.scoped)) - - it.effect("should capture only stderr via .all when no stdout", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo hello from stderr >&2"]) - const all = yield* decodeByteStream(handle.all) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(all, "hello from stderr") - }).pipe(Effect.scoped)) - - it.effect("should handle many lines of interspersed output via .all", () => - Effect.gen(function*() { - // Use sleep to force buffer flushes, ensuring interleaved arrival - const handle = yield* ChildProcess.make( - "sh", - ["-c", "for i in 1 2 3 4 5; do echo stdout$i; sleep 0.01; echo stderr$i >&2; sleep 0.01; done"] - ) - const all = yield* decodeByteStream(handle.all) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // Verify all lines are present in interleaved order - const expected = [] - for (let i = 1; i <= 5; i++) { - expected.push(`stdout${i}`, `stderr${i}`) - } - assert.strictEqual(all, expected.join("\n")) - }).pipe(Effect.scoped)) - - it.effect("should allow reading .all independently", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo out; sleep 0.01; echo err >&2"] - ) - const all = yield* decodeByteStream(handle.all) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(all, ["out", "err"].join("\n")) - }).pipe(Effect.scoped)) - }) - - describe("stdout streaming", () => { - it.effect("should stream stdout", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["streaming output"]) - const output = yield* decodeByteStream(handle.stdout) - - assert.strictEqual(output, "streaming output") - }).pipe(Effect.scoped)) - - it.effect("should stream multiple lines", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "echo line1; echo line2; echo line3"]) - const output = yield* decodeByteStream(handle.stdout) - - assert.strictEqual(output, ["line1", "line2", "line3"].join("\n")) - }).pipe(Effect.scoped)) - }) - - describe("process control", () => { - it.effect("should kill a process", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sleep", ["10"]) - - yield* handle.kill() - - // After killing, exitCode should eventually resolve (with signal error) - const exit = yield* Effect.exit(handle.exitCode) - assert.isTrue(exit._tag === "Failure") - }).pipe(Effect.scoped)) - - it.effect("should kill with specific signal", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sleep", ["10"]) - - yield* handle.kill({ killSignal: "SIGKILL" }) - - const exit = yield* Effect.exit(handle.exitCode) - assert.isTrue(exit._tag === "Failure") - }).pipe(Effect.scoped)) - - it.effect("should force kill a process after the initial signal times out", () => - Effect.gen(function*() { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const directory = yield* fs.makeTempDirectoryScoped() - const ready = path.join(directory, "ready") - const handle = yield* ChildProcess.make("node", [ - "-e", - // Installing a listener suppresses Node's default SIGTERM exit so the test exercises force-kill escalation. - "process.on('SIGTERM', () => {}); require('node:fs').writeFileSync(process.argv[1], ''); setInterval(() => {}, 1000)", - ready - ], { - killSignal: "SIGKILL", - stdout: "ignore", - stderr: "ignore" - }) - - yield* fs.exists(ready).pipe( - Effect.repeat({ - while: (exists) => !exists, - schedule: Schedule.spaced("10 millis") - }), - Effect.timeout("1 second"), - TestClock.withLive - ) - - const completed = yield* handle.kill({ - killSignal: "SIGTERM", - forceKillAfter: "50 millis" - }).pipe( - Effect.as(true), - Effect.timeoutOrElse({ - duration: "1 second", - orElse: () => Effect.succeed(false) - }), - TestClock.withLive - ) - - assert.isTrue(completed) - }).pipe(Effect.scoped)) - - it.effect("should force kill a process when its scope closes", () => - Effect.gen(function*() { - const fs = yield* FileSystem.FileSystem - const path = yield* Path.Path - const directory = yield* fs.makeTempDirectoryScoped() - const ready = path.join(directory, "ready") - const completed = yield* Effect.scoped(Effect.gen(function*() { - yield* ChildProcess.make("node", [ - "-e", - // Installing a listener suppresses Node's default SIGTERM exit so the test exercises force-kill escalation. - "process.on('SIGTERM', () => {}); require('node:fs').writeFileSync(process.argv[1], ''); setTimeout(() => process.exit(0), 2000)", - ready - ], { - killSignal: "SIGTERM", - forceKillAfter: "50 millis", - stdout: "ignore", - stderr: "ignore" - }) - - yield* fs.exists(ready).pipe( - Effect.repeat({ - while: (exists) => !exists, - schedule: Schedule.spaced("10 millis") - }), - Effect.timeout("1 second") - ) - })).pipe( - Effect.as(true), - Effect.timeoutOrElse({ - duration: "1 second", - orElse: () => Effect.succeed(false) - }), - TestClock.withLive - ) - - assert.isTrue(completed) - }).pipe(Effect.scoped)) - }) - }) - - describe("pipeline spawning", () => { - it.effect("should spawn a simple pipeline", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello world`.pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO WORLD") - }).pipe(Effect.scoped)) - - it.effect("should spawn a three-stage pipeline", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello world`.pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`), - ChildProcess.pipeTo(ChildProcess.make("tr", [" ", "-"])) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO-WORLD") - }).pipe(Effect.scoped)) - - it.effect("should pipe grep output", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["line1\nline2\nline3"]).pipe( - ChildProcess.pipeTo(ChildProcess.make`grep line2`) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "line2") - }).pipe(Effect.scoped)) - - it.effect("should handle mixed command forms in pipeline", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["hello"]).pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO") - }).pipe(Effect.scoped)) - }) - - describe("pipeline pipe options", () => { - it.effect("should pipe stderr to stdin with { from: 'stderr' }", () => - Effect.gen(function*() { - // Command that writes "error" to stderr - const handle = yield* ChildProcess.make("sh", ["-c", "echo error >&2"]).pipe( - ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "stderr" }) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "error") - }).pipe(Effect.scoped)) - - it.effect("should pipe combined output with { from: 'all' }", () => - Effect.gen(function*() { - // Command that writes to both stdout and stderr with small delays - const handle = yield* ChildProcess.make("sh", [ - "-c", - "echo out1; sleep 0.01; echo err1 >&2; sleep 0.01; echo out2" - ]).pipe( - ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "all" }) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, ["out1", "err1", "out2"].join("\n")) - }).pipe(Effect.scoped)) - - it.effect("should default to stdout when no options provided", () => - Effect.gen(function*() { - // Command that writes to both stdout and stderr - const handle = yield* ChildProcess.make("sh", ["-c", "echo stdout; echo stderr >&2"]).pipe( - ChildProcess.pipeTo(ChildProcess.make`cat`) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - // Only stdout should be piped (default behavior) - assert.strictEqual(output, "stdout") - }).pipe(Effect.scoped)) - - it.effect("should work with empty options object", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello`.pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, {}) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO") - }).pipe(Effect.scoped)) - - it.effect("should work with explicit { from: 'stdout' }", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello`.pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, { from: "stdout" }) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO") - }).pipe(Effect.scoped)) - - it.effect("should work with explicit { to: 'stdin' }", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make`echo hello`.pipe( - ChildProcess.pipeTo(ChildProcess.make`tr a-z A-Z`, { to: "stdin" }) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "HELLO") - }).pipe(Effect.scoped)) - - it.effect("should support chained pipes with different options", () => - Effect.gen(function*() { - // First pipe: stdout to stdin (default) - // Second pipe: from stderr - const handle = yield* ChildProcess.make`echo hello`.pipe( - ChildProcess.pipeTo(ChildProcess.make("sh", ["-c", "cat; echo error >&2"])), - ChildProcess.pipeTo(ChildProcess.make`cat`, { from: "stderr" }) - ) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, "error") - }).pipe(Effect.scoped)) - }) - - describe("error handling", () => { - it.effect("should return non-zero exit code", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("sh", ["-c", "exit 1"]) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) - }).pipe(Effect.scoped)) - - it.effect("should fail for invalid command", () => - Effect.gen(function*() { - const exit = yield* Effect.exit( - ChildProcess.make("nonexistent-command-12345") - ) - - assert.isTrue(exit._tag === "Failure") - }).pipe(Effect.scoped)) - - it.effect("should handle spawn error with invalid cwd", () => - Effect.gen(function*() { - const exit = yield* Effect.exit( - ChildProcess.make("echo", ["test"], { cwd: "/nonexistent/directory/path" }) - ) - - assert.isTrue(exit._tag === "Failure") - }).pipe(Effect.scoped)) - - it.effect("should throw permission denied as a typed error", () => - Effect.gen(function*() { - const path = yield* Path.Path - const cwd = path.join(...TEST_BASH_SCRIPTS_PATH) - - const command = ChildProcess.make({ cwd })`./no-permissions.sh` - const result = yield* Effect.flip(command) - - assert.deepStrictEqual( - result, - PlatformError.systemError({ - _tag: "PermissionDenied", - module: "ChildProcess", - method: "spawn", - pathOrDescriptor: "./no-permissions.sh ", - syscall: "spawn ./no-permissions.sh" - }) - ) - }).pipe(Effect.scoped)) - }) - - describe("stdin", () => { - it.effect("allows providing standard input to a command", () => - Effect.gen(function*() { - const input = "a b c" - const stdin = Stream.make(Buffer.from(input, "utf-8")) - const handle = yield* ChildProcess.make("cat", { stdin }) - const output = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.deepStrictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(output, input) - }).pipe(Effect.scoped)) - }) - - describe("additionalFds", () => { - it.effect("should read data from an output fd (fd3)", () => - Effect.gen(function*() { - // Use a shell script that writes to fd3 - // The script echoes "hello from fd3" to file descriptor 3 - const handle = yield* ChildProcess.make("sh", ["-c", "echo 'hello from fd3' >&3"], { - additionalFds: { fd3: { type: "output" } } - }) - - const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(fd3Output, "hello from fd3") - }).pipe(Effect.scoped)) - - it.effect("should write data to an input fd (fd3)", () => - Effect.gen(function*() { - // Use a shell script that reads from fd3 and echoes it to stdout - // The script reads from file descriptor 3 and outputs to stdout - const inputData = "data from parent" - const inputStream = Stream.make(new TextEncoder().encode(inputData)) - - const handle = yield* ChildProcess.make("sh", ["-c", "cat <&3"], { - additionalFds: { - fd3: { type: "input", stream: inputStream } - } - }) - - const stdout = yield* decodeByteStream(handle.stdout) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, inputData) - }).pipe(Effect.scoped)) - - it.effect("should handle multiple additional fds", () => - Effect.gen(function*() { - // Script that writes different messages to fd3 and fd4 - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo 'output on fd3' >&3; echo 'output on fd4' >&4"], - { - additionalFds: { - fd3: { type: "output" }, - fd4: { type: "output" } - } - } - ) - - const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) - const fd4Output = yield* decodeByteStream(handle.getOutputFd(4)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(fd3Output, "output on fd3") - assert.strictEqual(fd4Output, "output on fd4") - }).pipe(Effect.scoped)) - - it.effect("should handle fd gaps (e.g., fd3 and fd5 without fd4)", () => - Effect.gen(function*() { - // Script that writes to fd3 and fd5, skipping fd4 - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo 'on fd3' >&3; echo 'on fd5' >&5"], - { - additionalFds: { - fd3: { type: "output" }, - fd5: { type: "output" } - } - } - ) - - const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) - const fd5Output = yield* decodeByteStream(handle.getOutputFd(5)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(fd3Output, "on fd3") - assert.strictEqual(fd5Output, "on fd5") - }).pipe(Effect.scoped)) - - it.effect("should return empty stream for unconfigured output fd", () => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("echo", ["test"]) - - // fd3 was not configured, should return empty stream - const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(fd3Output, "") - }).pipe(Effect.scoped)) - - it.effect("should handle bidirectional communication via separate fds", () => - Effect.gen(function*() { - // Script that reads from fd3, transforms it, and writes to fd4 - const inputData = "hello" - const inputStream = Stream.make(new TextEncoder().encode(inputData)) - - const handle = yield* ChildProcess.make( - "sh", - ["-c", "cat <&3 | tr a-z A-Z >&4"], - { - additionalFds: { - fd3: { type: "input", stream: inputStream }, - fd4: { type: "output" } - } - } - ) - - const fd4Output = yield* decodeByteStream(handle.getOutputFd(4)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(fd4Output, "HELLO") - }).pipe(Effect.scoped)) - - it.effect("should work alongside normal stdin/stdout/stderr", () => - Effect.gen(function*() { - // Script that uses all standard streams plus fd3 - const handle = yield* ChildProcess.make( - "sh", - ["-c", "echo 'stdout'; echo 'stderr' >&2; echo 'fd3' >&3"], - { additionalFds: { fd3: { type: "output" } } } - ) - - const stdout = yield* decodeByteStream(handle.stdout) - const stderr = yield* decodeByteStream(handle.stderr) - const fd3Output = yield* decodeByteStream(handle.getOutputFd(3)) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - assert.strictEqual(stdout, "stdout") - assert.strictEqual(stderr, "stderr") - assert.strictEqual(fd3Output, "fd3") - }).pipe(Effect.scoped)) - }) - - describe("process supervision", () => { - const countMatchingProcesses = (pattern: string) => - Effect.gen(function*() { - const handle = yield* ChildProcess.make("bash", [ - "-c", - `ps aux | grep '${pattern}' | grep -v grep | wc -l` - ]) - const output = yield* decodeByteStream(handle.stdout) - return Number.parseInt(output.trim()) - }).pipe(Effect.orElseSucceed(() => 0)) - - const killMatchingProcesses = (pattern: string) => - Effect.gen(function*() { - const escaped = `[${pattern[0]}]${pattern.slice(1)}` - const handle = yield* ChildProcess.make("bash", ["-c", `pkill -f '${escaped}' || true`]) - yield* Effect.ignore(handle.exitCode) - }).pipe(Effect.asVoid) - - const longRunningCommand = () => - ChildProcess.make("node", ["-e", "setTimeout(() => {}, 30000)"], { - stdin: "ignore", - stdout: "ignore", - stderr: "ignore" - }) - - it.effect("should kill all child processes in process group", () => - Effect.gen(function*() { - const path = yield* Path.Path - const cwd = path.join(...TEST_BASH_SCRIPTS_PATH) - - // Start the process that spawns children and grandchildren - const handle = yield* ChildProcess.make("./spawn-children.sh", { cwd }) - - // Give it time to spawn all processes - yield* TestClock.withLive(Effect.sleep("100 millis")) - - // Verify the main process is running - const isRunningBeforeKill = yield* handle.isRunning - assert.isTrue(isRunningBeforeKill) - - // Count processes before killing - should be at least 7 (1 parent + 3 children + 3 grandchildren) - const beforeKillHandle = yield* ChildProcess.make("bash", [ - "-c", - "ps aux | grep spawn-children.sh | grep -v grep | wc -l" - ]) - const beforeKill = yield* decodeByteStream(beforeKillHandle.stdout).pipe( - Effect.map((s) => Number.parseInt(s.trim())), - Effect.orElseSucceed(() => 0) - ) - assert.isAtLeast(beforeKill, 7) - - // Kill the main process - yield* handle.kill() - - // Verify the main process is no longer running - const isRunningAfterKill = yield* handle.isRunning - assert.isFalse(isRunningAfterKill) - - // Give a moment for cleanup to complete - yield* TestClock.withLive(Effect.sleep("100 millis")) - - // Check that no processes from the script are still running - const afterKillHandle = yield* ChildProcess.make("bash", [ - "-c", - "ps aux | grep spawn-children.sh | grep -v grep | wc -l" - ]) - const afterKill = yield* decodeByteStream(afterKillHandle.stdout).pipe( - Effect.map((s) => Number.parseInt(s.trim())), - Effect.orElseSucceed(() => 0) - ) - assert.strictEqual(afterKill, 0) - }).pipe(Effect.scoped)) - - it.effect("should cleanup child processes when parent exits with non-zero code", () => - Effect.gen(function*() { - const path = yield* Path.Path - const cwd = path.join(...TEST_BASH_SCRIPTS_PATH) - - // Count processes before running the command - const beforeRunHandle = yield* ChildProcess.make("bash", [ - "-c", - "ps aux | grep parent-exits-early.sh | grep -v grep | wc -l" - ]) - const beforeRun = yield* decodeByteStream(beforeRunHandle.stdout).pipe( - Effect.map((s) => Number.parseInt(s.trim())), - Effect.orElseSucceed(() => 0) - ) - assert.strictEqual(beforeRun, 0) - - // Run command in a separate scope so cleanup happens before we check - const exitCode = yield* Effect.scoped(Effect.gen(function*() { - const handle = yield* ChildProcess.make({ cwd })`./parent-exits-early.sh` - return yield* handle.exitCode - })) - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) - - // Allow cleanup to occur - yield* TestClock.withLive(Effect.sleep("100 millis")) - - const afterExitHandle = yield* ChildProcess.make("bash", [ - "-c", - "ps aux | grep 'sleep 30' | grep -v grep | wc -l" - ]) - const afterExit = yield* decodeByteStream(afterExitHandle.stdout).pipe( - Effect.map((s) => Number.parseInt(s.trim())), - Effect.orElseSucceed(() => 0) - ) - // Child processes should be cleaned up after non-zero exit - assert.strictEqual(afterExit, 0) - }).pipe(Effect.scoped)) - - it.effect("should not kill an unrefed process when scope closes", () => - Effect.gen(function*() { - const scope = yield* Scope.make() - const handle = yield* Scope.provide(scope)(Effect.gen(function*() { - return yield* longRunningCommand() - })).pipe( - Effect.provide(NodeServices) - ) - - // @effect-diagnostics-next-line floatingEffect:off - yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(NodeServices)) - yield* Scope.close(scope, Exit.void) - yield* TestClock.withLive(Effect.sleep("100 millis")) - - const isRunning = yield* handle.isRunning - assert.isTrue(isRunning) - - yield* handle.kill({ killSignal: "SIGKILL" }) - }).pipe(Effect.provide(NodeServices))) - - it.effect("should kill a restored process when scope closes", () => - Effect.gen(function*() { - const scope = yield* Scope.make() - const handle = yield* Scope.provide(scope)(Effect.gen(function*() { - return yield* longRunningCommand() - })).pipe( - Effect.provide(NodeServices) - ) - - const reref = yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(NodeServices)) - yield* reref - yield* Scope.close(scope, Exit.void) - yield* TestClock.withLive(Effect.sleep("100 millis")) - - const isRunning = yield* handle.isRunning - assert.isFalse(isRunning) - }).pipe(Effect.provide(NodeServices))) - - it.effect("should resolve exitCode after closing the original scope of an unrefed process", () => - Effect.gen(function*() { - const scope = yield* Scope.make() - const handle = yield* Scope.provide(scope)(Effect.gen(function*() { - return yield* ChildProcess.make("node", ["-e", "setTimeout(() => process.exit(0), 50)"], { - stdin: "ignore", - stdout: "ignore", - stderr: "ignore" - }) - })).pipe(Effect.provide(NodeServices)) - - // @effect-diagnostics-next-line floatingEffect:off - yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(NodeServices)) - yield* Scope.close(scope, Exit.void) - - const exitCode = yield* handle.exitCode - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - }).pipe(Effect.provide(NodeServices))) - - it.effect("should cleanup descendants after an unrefed parent exits non-zero", () => - Effect.gen(function*() { - const path = yield* Path.Path - const cwd = path.join(...TEST_BASH_SCRIPTS_PATH) - const scope = yield* Scope.make() - - const handle = yield* Scope.provide(scope)(Effect.gen(function*() { - return yield* ChildProcess.make({ cwd })`./parent-exits-early.sh` - })).pipe(Effect.provide(NodeServices)) - - // @effect-diagnostics-next-line floatingEffect:off - yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(NodeServices)) - yield* Scope.close(scope, Exit.void) - - const exitCode = yield* handle.exitCode - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(1)) - - yield* TestClock.withLive(Effect.sleep("100 millis")) - - const remaining = yield* countMatchingProcesses("sleep 30") - assert.strictEqual(remaining, 0) - }).pipe(Effect.provide(NodeServices))) - - it.effect("should unref every process in a pipeline", () => - Effect.gen(function*() { - const scope = yield* Scope.make() - const rootMarker = "pipeline-unref-root" - const tailMarker = "pipeline-unref-tail" - - const handle = yield* Scope.provide(scope)(Effect.gen(function*() { - return yield* ChildProcess.make("node", ["-e", "setTimeout(() => {}, 30000)", rootMarker], { - stdin: "ignore", - stdout: "pipe", - stderr: "ignore" - }).pipe( - ChildProcess.pipeTo( - ChildProcess.make("node", ["-e", "setTimeout(() => {}, 30000)", tailMarker], { - stdin: "pipe", - stdout: "ignore", - stderr: "ignore" - }) - ) - ) - })).pipe(Effect.provide(NodeServices)) - - // @effect-diagnostics-next-line floatingEffect:off - yield* Scope.provide(scope)(handle.unref).pipe(Effect.provide(NodeServices)) - yield* Scope.close(scope, Exit.void) - yield* TestClock.withLive(Effect.sleep("100 millis")) - - const rootCount = yield* countMatchingProcesses(rootMarker) - const tailCount = yield* countMatchingProcesses(tailMarker) - assert.strictEqual(rootCount, 1) - assert.strictEqual(tailCount, 1) - - yield* killMatchingProcesses(rootMarker) - yield* killMatchingProcesses(tailMarker) - }).pipe(Effect.provide(NodeServices))) - }) - - it.effect("should not deadlock on large stdout output", () => - Effect.gen(function*() { - // Generate ~5MB of output — enough to exceed the default PassThrough - // highWaterMark (16KB) many times over. Without the fix, the unread - // combinedPassThrough (.all) would exert backpressure on the source - // stream, blocking stdout too. - const handle = yield* ChildProcess.make("sh", ["-c", "seq 1 100000"]) - const output = yield* handle.stdout.pipe( - Stream.decodeText(), - Stream.runFold(() => "", (acc, chunk) => acc + chunk) - ) - const exitCode = yield* handle.exitCode - - assert.strictEqual(exitCode, ChildProcessSpawner.ExitCode(0)) - const lines = output.trim().split("\n") - assert.strictEqual(lines.length, 100000) - assert.strictEqual(lines[0], "1") - assert.strictEqual(lines[99999], "100000") - }).pipe(Effect.scoped), { timeout: 10_000 }) - - it.effect("ChildProcess.string should not deadlock on large output", () => - Effect.gen(function*() { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const output = yield* spawner.string(ChildProcess.make("sh", ["-c", "seq 1 100000"])) - const lines = output.trim().split("\n") - assert.strictEqual(lines.length, 100000) - assert.strictEqual(lines[0], "1") - assert.strictEqual(lines[99999], "100000") - }), { timeout: 10_000 }) - }) -}) diff --git a/.context/effect/packages/platform-node-shared/test/NodeFileSystem.test.ts b/.context/effect/packages/platform-node-shared/test/NodeFileSystem.test.ts deleted file mode 100644 index 017f2eee9..000000000 --- a/.context/effect/packages/platform-node-shared/test/NodeFileSystem.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" -import { assert, describe, expect, it } from "@effect/vitest" -import { Array } from "effect" -import * as Effect from "effect/Effect" -import * as Fs from "effect/FileSystem" -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* Effect.scoped(Effect.gen(function*() { - dir = yield* fs.makeTempDirectory() - const stat = yield* fs.stat(dir) - expect(stat.type).toEqual("Directory") - })) - 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* Effect.scoped( - Effect.gen(function*() { - dir = yield* fs.makeTempDirectoryScoped() - const stat = yield* fs.stat(dir) - expect(stat.type).toEqual("Directory") - }) - ) - const error = yield* Effect.flip(fs.stat(dir)) - assert(error.reason._tag === "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* Effect.map(fs.readFile(file), (_) => new TextDecoder().decode(_)) - expect(before).toEqual(text) - - yield* fs.truncate(file) - - const after = yield* Effect.map(fs.readFile(file), (_) => new TextDecoder().decode(_)) - expect(after).toEqual("") - }))) - - it("should track the cursor position when reading", () => - runPromise(Effect.gen(function*() { - const fs = yield* Fs.FileSystem - - yield* Effect.gen(function*() { - let text: string - const file = yield* fs.open(`${__dirname}/fixtures/text.txt`) - - text = yield* file.readAlloc(Fs.Size(5)).pipe( - Effect.flatMap(Effect.fromOption), - Effect.map((_) => new TextDecoder().decode(_)) - ) - expect(text).toBe("lorem") - - yield* file.seek(Fs.Size(7), "current") - text = yield* file.readAlloc(Fs.Size(5)).pipe( - Effect.flatMap(Effect.fromOption), - Effect.map((_) => new TextDecoder().decode(_)) - ) - expect(text).toBe("dolar") - - yield* file.seek(Fs.Size(1), "current") - text = yield* file.readAlloc(Fs.Size(8)).pipe( - Effect.flatMap(Effect.fromOption), - Effect.map((_) => new TextDecoder().decode(_)) - ) - expect(text).toBe("sit amet") - - yield* file.seek(Fs.Size(0), "start") - text = yield* file.readAlloc(Fs.Size(11)).pipe( - Effect.flatMap(Effect.fromOption), - Effect.map((_) => new TextDecoder().decode(_)) - ) - expect(text).toBe("lorem ipsum") - - text = yield* fs.stream(`${__dirname}/fixtures/text.txt`, { offset: Fs.Size(6), bytesToRead: Fs.Size(5) }).pipe( - Stream.map((_) => new TextDecoder().decode(_)), - Stream.runCollect, - Effect.map(Array.join("")) - ) - expect(text).toBe("ipsum") - }).pipe( - Effect.scoped - ) - }))) - - it("should track the cursor position when writing", () => - runPromise(Effect.gen(function*() { - const fs = yield* Fs.FileSystem - - yield* 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") - }).pipe( - Effect.scoped - ) - }))) - - it("should maintain a read cursor in append mode", () => - runPromise(Effect.gen(function*() { - const fs = yield* Fs.FileSystem - - yield* 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* file.readAlloc(Fs.Size(3)).pipe( - Effect.flatMap(Effect.fromOption), - 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* file.readAlloc(Fs.Size(6)).pipe( - Effect.flatMap(Effect.fromOption), - Effect.map((_) => new TextDecoder().decode(_)) - ) - expect(text).toBe("barbaz") - }).pipe( - Effect.scoped - ) - }))) - - it("should keep the current cursor if truncating doesn't affect it", () => - runPromise(Effect.gen(function*() { - const fs = yield* Fs.FileSystem - - yield* 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)) - }).pipe( - Effect.scoped - ) - }))) - - it("should update the current cursor if truncating affects it", () => - runPromise(Effect.gen(function*() { - const fs = yield* Fs.FileSystem - - yield* 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)) - }).pipe( - Effect.scoped - ) - }))) -}) diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh b/.context/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh deleted file mode 100755 index 7f2eabb0c..000000000 --- a/.context/effect/packages/platform-node-shared/test/fixtures/bash/parent-exits-early.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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/.context/effect/packages/platform-node-shared/tsconfig.json b/.context/effect/packages/platform-node-shared/tsconfig.json deleted file mode 100644 index be73d74c7..000000000 --- a/.context/effect/packages/platform-node-shared/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "include": ["src"], - "references": [ - { "path": "../effect" } - ], - "compilerOptions": { - "types": ["node"] - } -} diff --git a/.context/effect/packages/platform-node-shared/vitest.config.ts b/.context/effect/packages/platform-node-shared/vitest.config.ts deleted file mode 100644 index fb966ae87..000000000 --- a/.context/effect/packages/platform-node-shared/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/platform-node/CHANGELOG.md b/.context/effect/packages/platform-node/CHANGELOG.md deleted file mode 100644 index 59c15d759..000000000 --- a/.context/effect/packages/platform-node/CHANGELOG.md +++ /dev/null @@ -1,843 +0,0 @@ -# @effect/platform-node - -## 4.0.0-beta.101 - -### Patch Changes - -- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: - - effect@4.0.0-beta.101 - - @effect/platform-node-shared@4.0.0-beta.101 - -## 4.0.0-beta.100 - -### Patch Changes - -- [#6506](https://github.com/Effect-TS/effect/pull/6506) [`b0f1a50`](https://github.com/Effect-TS/effect/commit/b0f1a50dacece60aa2393a15da853d82891a7a34) Thanks @tim-smart! - Ensure aborted `HEAD` responses do not block `NodeHttpServer` disposal. - -- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: - - effect@4.0.0-beta.100 - - @effect/platform-node-shared@4.0.0-beta.100 - -## 4.0.0-beta.99 - -### Patch Changes - -- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: - - effect@4.0.0-beta.99 - - @effect/platform-node-shared@4.0.0-beta.99 - -## 4.0.0-beta.98 - -### Patch Changes - -- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: - - effect@4.0.0-beta.98 - - @effect/platform-node-shared@4.0.0-beta.98 - -## 4.0.0-beta.97 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.97 - - @effect/platform-node-shared@4.0.0-beta.97 - -## 4.0.0-beta.96 - -### Patch Changes - -- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: - - effect@4.0.0-beta.96 - - @effect/platform-node-shared@4.0.0-beta.96 - -## 4.0.0-beta.95 - -### Patch Changes - -- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: - - effect@4.0.0-beta.95 - - @effect/platform-node-shared@4.0.0-beta.95 - -## 4.0.0-beta.94 - -### Patch Changes - -- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: - - effect@4.0.0-beta.94 - - @effect/platform-node-shared@4.0.0-beta.94 - -## 4.0.0-beta.93 - -### Patch Changes - -- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: - - effect@4.0.0-beta.93 - - @effect/platform-node-shared@4.0.0-beta.93 - -## 4.0.0-beta.92 - -### Patch Changes - -- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: - - effect@4.0.0-beta.92 - - @effect/platform-node-shared@4.0.0-beta.92 - -## 4.0.0-beta.91 - -### Patch Changes - -- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: - - effect@4.0.0-beta.91 - - @effect/platform-node-shared@4.0.0-beta.91 - -## 4.0.0-beta.90 - -### Patch Changes - -- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: - - effect@4.0.0-beta.90 - - @effect/platform-node-shared@4.0.0-beta.90 - -## 4.0.0-beta.89 - -### Patch Changes - -- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: - - effect@4.0.0-beta.89 - - @effect/platform-node-shared@4.0.0-beta.89 - -## 4.0.0-beta.88 - -### Patch Changes - -- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: - - effect@4.0.0-beta.88 - - @effect/platform-node-shared@4.0.0-beta.88 - -## 4.0.0-beta.87 - -### Patch Changes - -- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: - - effect@4.0.0-beta.87 - - @effect/platform-node-shared@4.0.0-beta.87 - -## 4.0.0-beta.86 - -### Patch Changes - -- [#2463](https://github.com/Effect-TS/effect-smol/pull/2463) [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3) Thanks @tim-smart! - Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`. - -- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: - - effect@4.0.0-beta.86 - - @effect/platform-node-shared@4.0.0-beta.86 - -## 4.0.0-beta.85 - -### Patch Changes - -- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: - - effect@4.0.0-beta.85 - - @effect/platform-node-shared@4.0.0-beta.85 - -## 4.0.0-beta.84 - -### Patch Changes - -- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: - - effect@4.0.0-beta.84 - - @effect/platform-node-shared@4.0.0-beta.84 - -## 4.0.0-beta.83 - -### Patch Changes - -- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: - - effect@4.0.0-beta.83 - - @effect/platform-node-shared@4.0.0-beta.83 - -## 4.0.0-beta.82 - -### Patch Changes - -- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: - - effect@4.0.0-beta.82 - - @effect/platform-node-shared@4.0.0-beta.82 - -## 4.0.0-beta.81 - -### Patch Changes - -- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: - - effect@4.0.0-beta.81 - - @effect/platform-node-shared@4.0.0-beta.81 - -## 4.0.0-beta.80 - -### Patch Changes - -- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: - - effect@4.0.0-beta.80 - - @effect/platform-node-shared@4.0.0-beta.80 - -## 4.0.0-beta.79 - -### Patch Changes - -- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: - - effect@4.0.0-beta.79 - - @effect/platform-node-shared@4.0.0-beta.79 - -## 4.0.0-beta.78 - -### Patch Changes - -- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: - - effect@4.0.0-beta.78 - - @effect/platform-node-shared@4.0.0-beta.78 - -## 4.0.0-beta.77 - -### Patch Changes - -- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: - - effect@4.0.0-beta.77 - - @effect/platform-node-shared@4.0.0-beta.77 - -## 4.0.0-beta.76 - -### Patch Changes - -- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: - - effect@4.0.0-beta.76 - - @effect/platform-node-shared@4.0.0-beta.76 - -## 4.0.0-beta.75 - -### Patch Changes - -- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: - - effect@4.0.0-beta.75 - - @effect/platform-node-shared@4.0.0-beta.75 - -## 4.0.0-beta.74 - -### Patch Changes - -- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: - - effect@4.0.0-beta.74 - - @effect/platform-node-shared@4.0.0-beta.74 - -## 4.0.0-beta.73 - -### Patch Changes - -- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: - - effect@4.0.0-beta.73 - - @effect/platform-node-shared@4.0.0-beta.73 - -## 4.0.0-beta.72 - -### Patch Changes - -- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: - - effect@4.0.0-beta.72 - - @effect/platform-node-shared@4.0.0-beta.72 - -## 4.0.0-beta.71 - -### Patch Changes - -- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: - - effect@4.0.0-beta.71 - - @effect/platform-node-shared@4.0.0-beta.71 - -## 4.0.0-beta.70 - -### Patch Changes - -- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: - - effect@4.0.0-beta.70 - - @effect/platform-node-shared@4.0.0-beta.70 - -## 4.0.0-beta.69 - -### Patch Changes - -- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: - - effect@4.0.0-beta.69 - - @effect/platform-node-shared@4.0.0-beta.69 - -## 4.0.0-beta.68 - -### Patch Changes - -- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. - -- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: - - effect@4.0.0-beta.68 - - @effect/platform-node-shared@4.0.0-beta.68 - -## 4.0.0-beta.67 - -### Patch Changes - -- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal - -- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: - - effect@4.0.0-beta.67 - - @effect/platform-node-shared@4.0.0-beta.67 - -## 4.0.0-beta.66 - -### Patch Changes - -- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: - - effect@4.0.0-beta.66 - - @effect/platform-node-shared@4.0.0-beta.66 - -## 4.0.0-beta.65 - -### Patch Changes - -- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: - - effect@4.0.0-beta.65 - - @effect/platform-node-shared@4.0.0-beta.65 - -## 4.0.0-beta.64 - -### Patch Changes - -- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: - - effect@4.0.0-beta.64 - - @effect/platform-node-shared@4.0.0-beta.64 - -## 4.0.0-beta.63 - -### Patch Changes - -- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: - - effect@4.0.0-beta.63 - - @effect/platform-node-shared@4.0.0-beta.63 - -## 4.0.0-beta.62 - -### Patch Changes - -- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: - - effect@4.0.0-beta.62 - - @effect/platform-node-shared@4.0.0-beta.62 - -## 4.0.0-beta.61 - -### Patch Changes - -- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: - - effect@4.0.0-beta.61 - - @effect/platform-node-shared@4.0.0-beta.61 - -## 4.0.0-beta.60 - -### Patch Changes - -- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: - - effect@4.0.0-beta.60 - - @effect/platform-node-shared@4.0.0-beta.60 - -## 4.0.0-beta.59 - -### Patch Changes - -- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: - - effect@4.0.0-beta.59 - - @effect/platform-node-shared@4.0.0-beta.59 - -## 4.0.0-beta.58 - -### Patch Changes - -- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption - -- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: - - effect@4.0.0-beta.58 - - @effect/platform-node-shared@4.0.0-beta.58 - -## 4.0.0-beta.57 - -### Patch Changes - -- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: - - effect@4.0.0-beta.57 - - @effect/platform-node-shared@4.0.0-beta.57 - -## 4.0.0-beta.56 - -### Patch Changes - -- Updated dependencies []: - - effect@4.0.0-beta.56 - - @effect/platform-node-shared@4.0.0-beta.56 - -## 4.0.0-beta.55 - -### Patch Changes - -- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: - - effect@4.0.0-beta.55 - - @effect/platform-node-shared@4.0.0-beta.55 - -## 4.0.0-beta.54 - -### Patch Changes - -- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: - - effect@4.0.0-beta.54 - - @effect/platform-node-shared@4.0.0-beta.54 - -## 4.0.0-beta.53 - -### Patch Changes - -- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: - - effect@4.0.0-beta.53 - - @effect/platform-node-shared@4.0.0-beta.53 - -## 4.0.0-beta.52 - -### Patch Changes - -- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: - - effect@4.0.0-beta.52 - - @effect/platform-node-shared@4.0.0-beta.52 - -## 4.0.0-beta.51 - -### Patch Changes - -- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: - - effect@4.0.0-beta.51 - - @effect/platform-node-shared@4.0.0-beta.51 - -## 4.0.0-beta.50 - -### Patch Changes - -- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: - - effect@4.0.0-beta.50 - - @effect/platform-node-shared@4.0.0-beta.50 - -## 4.0.0-beta.49 - -### Patch Changes - -- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: - - effect@4.0.0-beta.49 - - @effect/platform-node-shared@4.0.0-beta.49 - -## 4.0.0-beta.48 - -### Patch Changes - -- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: - - effect@4.0.0-beta.48 - - @effect/platform-node-shared@4.0.0-beta.48 - -## 4.0.0-beta.47 - -### Patch Changes - -- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: - - effect@4.0.0-beta.47 - - @effect/platform-node-shared@4.0.0-beta.47 - -## 4.0.0-beta.46 - -### Patch Changes - -- Updated dependencies [[`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505), [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: - - @effect/platform-node-shared@4.0.0-beta.46 - - effect@4.0.0-beta.46 - -## 4.0.0-beta.45 - -### Patch Changes - -- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: - - effect@4.0.0-beta.45 - - @effect/platform-node-shared@4.0.0-beta.45 - -## 4.0.0-beta.44 - -### Patch Changes - -- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. - -- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. - -- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: - - effect@4.0.0-beta.44 - - @effect/platform-node-shared@4.0.0-beta.44 - -## 4.0.0-beta.43 - -### Patch Changes - -- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: - - effect@4.0.0-beta.43 - - @effect/platform-node-shared@4.0.0-beta.43 - -## 4.0.0-beta.42 - -### Patch Changes - -- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: - - effect@4.0.0-beta.42 - - @effect/platform-node-shared@4.0.0-beta.42 - -## 4.0.0-beta.41 - -### Patch Changes - -- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: - - effect@4.0.0-beta.41 - - @effect/platform-node-shared@4.0.0-beta.41 - -## 4.0.0-beta.40 - -### Patch Changes - -- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: - - effect@4.0.0-beta.40 - - @effect/platform-node-shared@4.0.0-beta.40 - -## 4.0.0-beta.39 - -### Patch Changes - -- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: - - effect@4.0.0-beta.39 - - @effect/platform-node-shared@4.0.0-beta.39 - -## 4.0.0-beta.38 - -### Patch Changes - -- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: - - effect@4.0.0-beta.38 - - @effect/platform-node-shared@4.0.0-beta.38 - -## 4.0.0-beta.37 - -### Patch Changes - -- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: - - effect@4.0.0-beta.37 - - @effect/platform-node-shared@4.0.0-beta.37 - -## 4.0.0-beta.36 - -### Patch Changes - -- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: - - effect@4.0.0-beta.36 - - @effect/platform-node-shared@4.0.0-beta.36 - -## 4.0.0-beta.35 - -### Patch Changes - -- [#1779](https://github.com/Effect-TS/effect-smol/pull/1779) [`3015c2d`](https://github.com/Effect-TS/effect-smol/commit/3015c2dc25fb44694978b4ff921af9b24178fcc0) Thanks @aeterno-caspian! - bump undici versions - -- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: - - effect@4.0.0-beta.35 - - @effect/platform-node-shared@4.0.0-beta.35 - -## 4.0.0-beta.34 - -### Patch Changes - -- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: - - effect@4.0.0-beta.34 - - @effect/platform-node-shared@4.0.0-beta.34 - -## 4.0.0-beta.33 - -### Patch Changes - -- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: - - effect@4.0.0-beta.33 - - @effect/platform-node-shared@4.0.0-beta.33 - -## 4.0.0-beta.32 - -### Patch Changes - -- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: - - effect@4.0.0-beta.32 - - @effect/platform-node-shared@4.0.0-beta.32 - -## 4.0.0-beta.31 - -### Patch Changes - -- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. - - Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. - -- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: - - effect@4.0.0-beta.31 - - @effect/platform-node-shared@4.0.0-beta.31 - -## 4.0.0-beta.30 - -### Patch Changes - -- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: - - effect@4.0.0-beta.30 - - @effect/platform-node-shared@4.0.0-beta.30 - -## 4.0.0-beta.29 - -### Patch Changes - -- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: - - effect@4.0.0-beta.29 - - @effect/platform-node-shared@4.0.0-beta.29 - -## 4.0.0-beta.28 - -### Patch Changes - -- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: - - effect@4.0.0-beta.28 - - @effect/platform-node-shared@4.0.0-beta.28 - -## 4.0.0-beta.27 - -### Patch Changes - -- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: - - effect@4.0.0-beta.27 - - @effect/platform-node-shared@4.0.0-beta.27 - -## 4.0.0-beta.26 - -### Patch Changes - -- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: - - effect@4.0.0-beta.26 - - @effect/platform-node-shared@4.0.0-beta.26 - -## 4.0.0-beta.25 - -### Patch Changes - -- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: - - effect@4.0.0-beta.25 - - @effect/platform-node-shared@4.0.0-beta.25 - -## 4.0.0-beta.24 - -### Patch Changes - -- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: - - effect@4.0.0-beta.24 - - @effect/platform-node-shared@4.0.0-beta.24 - -## 4.0.0-beta.23 - -### Patch Changes - -- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: - - effect@4.0.0-beta.23 - - @effect/platform-node-shared@4.0.0-beta.23 - -## 4.0.0-beta.22 - -### Patch Changes - -- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: - - effect@4.0.0-beta.22 - - @effect/platform-node-shared@4.0.0-beta.22 - -## 4.0.0-beta.21 - -### Patch Changes - -- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: - - effect@4.0.0-beta.21 - - @effect/platform-node-shared@4.0.0-beta.21 - -## 4.0.0-beta.20 - -### Patch Changes - -- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: - - effect@4.0.0-beta.20 - - @effect/platform-node-shared@4.0.0-beta.20 - -## 4.0.0-beta.19 - -### Patch Changes - -- Updated dependencies [[`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f)]: - - @effect/platform-node-shared@4.0.0-beta.19 - - effect@4.0.0-beta.19 - -## 4.0.0-beta.18 - -### Patch Changes - -- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: - - effect@4.0.0-beta.18 - - @effect/platform-node-shared@4.0.0-beta.18 - -## 4.0.0-beta.17 - -### Patch Changes - -- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: - - effect@4.0.0-beta.17 - - @effect/platform-node-shared@4.0.0-beta.17 - -## 4.0.0-beta.16 - -### Patch Changes - -- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: - - effect@4.0.0-beta.16 - - @effect/platform-node-shared@4.0.0-beta.16 - -## 4.0.0-beta.15 - -### Patch Changes - -- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: - - effect@4.0.0-beta.15 - - @effect/platform-node-shared@4.0.0-beta.15 - -## 4.0.0-beta.14 - -### Patch Changes - -- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: - - effect@4.0.0-beta.14 - - @effect/platform-node-shared@4.0.0-beta.14 - -## 4.0.0-beta.13 - -### Patch Changes - -- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: - - effect@4.0.0-beta.13 - - @effect/platform-node-shared@4.0.0-beta.13 - -## 4.0.0-beta.12 - -### Patch Changes - -- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts - -- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: - - effect@4.0.0-beta.12 - - @effect/platform-node-shared@4.0.0-beta.12 - -## 4.0.0-beta.11 - -### Patch Changes - -- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: - - effect@4.0.0-beta.11 - - @effect/platform-node-shared@4.0.0-beta.11 - -## 4.0.0-beta.10 - -### Patch Changes - -- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: - - effect@4.0.0-beta.10 - - @effect/platform-node-shared@4.0.0-beta.10 - -## 4.0.0-beta.9 - -### Patch Changes - -- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: - - effect@4.0.0-beta.9 - - @effect/platform-node-shared@4.0.0-beta.9 - -## 4.0.0-beta.8 - -### Patch Changes - -- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: - - effect@4.0.0-beta.8 - - @effect/platform-node-shared@4.0.0-beta.8 - -## 4.0.0-beta.7 - -### Patch Changes - -- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: - - effect@4.0.0-beta.7 - - @effect/platform-node-shared@4.0.0-beta.7 - -## 4.0.0-beta.6 - -### Patch Changes - -- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: - - effect@4.0.0-beta.6 - - @effect/platform-node-shared@4.0.0-beta.6 - -## 4.0.0-beta.5 - -### Patch Changes - -- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: - - effect@4.0.0-beta.5 - - @effect/platform-node-shared@4.0.0-beta.5 - -## 4.0.0-beta.4 - -### Patch Changes - -- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: - - effect@4.0.0-beta.4 - - @effect/platform-node-shared@4.0.0-beta.4 - -## 4.0.0-beta.3 - -### Patch Changes - -- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: - - effect@4.0.0-beta.3 - - @effect/platform-node-shared@4.0.0-beta.3 - -## 4.0.0-beta.2 - -### Patch Changes - -- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: - - effect@4.0.0-beta.2 - - @effect/platform-node-shared@4.0.0-beta.2 - -## 4.0.0-beta.1 - -### Patch Changes - -- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: - - effect@4.0.0-beta.1 - - @effect/platform-node-shared@4.0.0-beta.1 - -## 4.0.0-beta.0 - -### Major Changes - -- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta - -### Patch Changes - -- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: - - @effect/platform-node-shared@4.0.0-beta.0 - - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-node/README.md b/.context/effect/packages/platform-node/README.md deleted file mode 100644 index d97ee42b8..000000000 --- a/.context/effect/packages/platform-node/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `@effect/platform-node` - -Provides Node.js-specific implementations for Effect's platform abstractions, 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/.context/effect/packages/platform-node/docgen.json b/.context/effect/packages/platform-node/docgen.json deleted file mode 100644 index 1c73cb523..000000000 --- a/.context/effect/packages/platform-node/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$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"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/platform-node/package.json b/.context/effect/packages/platform-node/package.json deleted file mode 100644 index 110a1545c..000000000 --- a/.context/effect/packages/platform-node/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "@effect/platform-node", - "type": "module", - "version": "4.0.0-beta.101", - "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" - ], - "engines": { - "node": ">=18.0.0" - }, - "sideEffects": [], - "exports": { - "./package.json": "./package.json", - ".": "./src/index.ts", - "./*": "./src/*.ts", - "./internal/*": null, - "./*/index": null - }, - "files": [ - "src/**/*.ts", - "dist/**/*.js", - "dist/**/*.js.map", - "dist/**/*.d.ts", - "dist/**/*.d.ts.map" - ], - "publishConfig": { - "access": "public", - "provenance": true, - "exports": { - "./package.json": "./package.json", - ".": "./dist/index.js", - "./*": "./dist/*.js", - "./internal/*": null, - "./*/index": null - } - }, - "scripts": { - "codegen": "effect-utils codegen", - "build": "tsc -b tsconfig.json && pnpm babel", - "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" - }, - "dependencies": { - "@effect/platform-node-shared": "workspace:^", - "mime": "^4.1.0", - "undici": "^8.7.0" - }, - "peerDependencies": { - "effect": "workspace:^", - "ioredis": "^5.7.0" - }, - "devDependencies": { - "@testcontainers/mysql": "^11.14.0", - "@testcontainers/postgresql": "^11.14.0", - "@testcontainers/redis": "^11.14.0", - "@types/node": "^26.1.1", - "effect": "workspace:^" - } -} diff --git a/.context/effect/packages/platform-node/src/NodeHttpPlatform.ts b/.context/effect/packages/platform-node/src/NodeHttpPlatform.ts deleted file mode 100644 index 2901851b6..000000000 --- a/.context/effect/packages/platform-node/src/NodeHttpPlatform.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Node.js implementation of the Effect HTTP platform service. - * - * This module connects the portable `HttpPlatform` file response helpers to - * Node runtime primitives. It serves local files through Node readable streams, - * supports byte ranges, converts Web `File` values to readable streams, and - * fills in content type and content length headers when needed. - * - * @since 4.0.0 - */ -import { pipe } from "effect/Function" -import * as Layer from "effect/Layer" -import * as EtagImpl from "effect/unstable/http/Etag" -import * as Headers from "effect/unstable/http/Headers" -import * as Platform from "effect/unstable/http/HttpPlatform" -import * as ServerResponse from "effect/unstable/http/HttpServerResponse" -import * as Fs from "node:fs" -import { Readable } from "node:stream" -import Mime from "./Mime.ts" -import * as NodeFileSystem from "./NodeFileSystem.ts" - -/** - * Creates the Node `HttpPlatform`, serving file responses from Node readable - * streams and adding MIME type and content-length headers when needed. - * - * @category constructors - * @since 4.0.0 - */ -export const make = Platform.make({ - fileResponse(path, status, statusText, headers, start, end, contentLength) { - const stream = contentLength === 0 - ? Readable.from([]) - : Fs.createReadStream(path, { start, end: end === undefined ? undefined : end - 1 }) - 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.fromRecordUnsafe({ - "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", - "content-length": file.size.toString() - }) - ), - status, - statusText - }) - } -}) - -/** - * Provides the Node `HttpPlatform` together with the filesystem and ETag - * services it needs for file responses. - * - * @category layers - * @since 4.0.0 - */ -export const layer: Layer.Layer = pipe( - Layer.effect(Platform.HttpPlatform)(make), - Layer.provide(NodeFileSystem.layer), - Layer.provide(EtagImpl.layer) -) diff --git a/.context/effect/packages/platform-node/src/NodeWorkerRunner.ts b/.context/effect/packages/platform-node/src/NodeWorkerRunner.ts deleted file mode 100644 index b6ff66e50..000000000 --- a/.context/effect/packages/platform-node/src/NodeWorkerRunner.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Node.js runtime support for workers that serve Effect worker requests. - * - * `NodeWorkerRunner` supplies the Node implementation of the Effect worker - * runner platform. The exported `layer` runs inside a `node:worker_threads` - * worker through `parentPort`, or inside a child process through - * `process.send`. It listens for parent messages, runs handlers registered with - * `WorkerRunner`, sends replies over the same channel, and closes when the - * parent sends the close message. - * - * @since 4.0.0 - */ -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 * as Layer from "effect/Layer" -import { WorkerError, WorkerReceiveError, WorkerSpawnError } from "effect/unstable/workers/WorkerError" -import * as WorkerRunner from "effect/unstable/workers/WorkerRunner" -import * as WorkerThreads from "node:worker_threads" - -/** - * Provides the `WorkerRunnerPlatform` for code running inside a Node worker - * thread or child process, routing parent messages to the registered handler - * and sending responses back through the parent channel. - * - * @category layers - * @since 4.0.0 - */ -export const layer: Layer.Layer = Layer.succeed(WorkerRunner.WorkerRunnerPlatform)({ - start() { - return Effect.gen(function*() { - if (!WorkerThreads.parentPort && !process.send) { - return yield* new WorkerError({ - reason: new WorkerSpawnError({ message: "not in a worker" }) - }) - } - - const sendUnsafe = WorkerThreads.parentPort - ? (_portId: number, message: any, transfers?: any) => WorkerThreads.parentPort!.postMessage(message, transfers) - : (_portId: number, message: any, _transfers?: any) => process.send!(message) - const send = (_portId: number, message: O, transfers?: ReadonlyArray) => - Effect.sync(() => sendUnsafe(_portId, [1, message], transfers as any)) - - const run = ( - handler: (portId: number, message: I) => Effect.Effect | void - ): Effect.Effect => - Effect.scopedWith(Effect.fnUntraced(function*(scope) { - const closeLatch = Deferred.makeUnsafe() - const trackFiber = Fiber.runIn(scope) - const services = yield* Effect.context() - const runFork = Effect.runForkWith(services) - const onExit = (exit: Exit.Exit) => { - if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) { - runFork(Effect.logError("unhandled error in worker", exit.cause)) - } - } - ;(WorkerThreads.parentPort ?? process).on("message", (message: WorkerRunner.PlatformMessage) => { - if (message[0] === 0) { - const result = handler(0, message[1]) - if (Effect.isEffect(result)) { - const fiber = runFork(result) - fiber.addObserver(onExit) - trackFiber(fiber) - } - } else { - if (WorkerThreads.parentPort) { - WorkerThreads.parentPort.close() - } else { - process.channel?.unref() - } - Deferred.doneUnsafe(closeLatch, Exit.void) - } - }) - - if (WorkerThreads.parentPort) { - WorkerThreads.parentPort.on("messageerror", (cause) => { - Deferred.doneUnsafe( - closeLatch, - new WorkerError({ - reason: new WorkerReceiveError({ - message: "received messageerror event", - cause - }) - }) - ) - }) - WorkerThreads.parentPort.on("error", (cause) => { - Deferred.doneUnsafe( - closeLatch, - new WorkerError({ - reason: new WorkerReceiveError({ - message: "received messageerror event", - cause - }) - }) - ) - }) - } - - sendUnsafe(0, [0]) - - return yield* Deferred.await(closeLatch) - })) - - return { run, send, sendUnsafe } - }) - } -}) diff --git a/.context/effect/packages/platform-node/src/index.ts b/.context/effect/packages/platform-node/src/index.ts deleted file mode 100644 index c626aaed1..000000000 --- a/.context/effect/packages/platform-node/src/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @since 4.0.0 - */ - -// @barrel: Auto-generated exports. Do not edit manually. - -/** - * @since 4.0.0 - */ -export * as Mime from "./Mime.ts" - -/** - * @since 4.0.0 - */ -export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts" - -/** - * @since 4.0.0 - */ -export * as NodeClusterHttp from "./NodeClusterHttp.ts" - -/** - * @since 4.0.0 - */ -export * as NodeClusterSocket from "./NodeClusterSocket.ts" - -/** - * @since 1.0.0 - */ -export * as NodeCrypto from "./NodeCrypto.ts" - -/** - * @since 4.0.0 - */ -export * as NodeFileSystem from "./NodeFileSystem.ts" - -/** - * @since 4.0.0 - */ -export * as NodeHttpClient from "./NodeHttpClient.ts" - -/** - * @since 4.0.0 - */ -export * as NodeHttpIncomingMessage from "./NodeHttpIncomingMessage.ts" - -/** - * @since 4.0.0 - */ -export * as NodeHttpPlatform from "./NodeHttpPlatform.ts" - -/** - * @since 4.0.0 - */ -export * as NodeHttpServer from "./NodeHttpServer.ts" - -/** - * @since 4.0.0 - */ -export * as NodeHttpServerRequest from "./NodeHttpServerRequest.ts" - -/** - * @since 4.0.0 - */ -export * as NodeMultipart from "./NodeMultipart.ts" - -/** - * @since 4.0.0 - */ -export * as NodePath from "./NodePath.ts" - -/** - * @since 4.0.0 - */ -export * as NodeRedis from "./NodeRedis.ts" - -/** - * @since 4.0.0 - */ -export * as NodeRuntime from "./NodeRuntime.ts" - -/** - * @since 4.0.0 - */ -export * as NodeServices from "./NodeServices.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSink from "./NodeSink.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSocket from "./NodeSocket.ts" - -/** - * @since 4.0.0 - */ -export * as NodeSocketServer from "./NodeSocketServer.ts" - -/** - * @since 4.0.0 - */ -export * as NodeStdio from "./NodeStdio.ts" - -/** - * @since 4.0.0 - */ -export * as NodeStream from "./NodeStream.ts" - -/** - * @since 4.0.0 - */ -export * as NodeTerminal from "./NodeTerminal.ts" - -/** - * @since 4.0.0 - */ -export * as NodeWorker from "./NodeWorker.ts" - -/** - * @since 4.0.0 - */ -export * as NodeWorkerRunner from "./NodeWorkerRunner.ts" - -/** - * @since 4.0.0 - */ -export * as Undici from "./Undici.ts" diff --git a/.context/effect/packages/platform-node/test/NodeHttpClient.test.ts b/.context/effect/packages/platform-node/test/NodeHttpClient.test.ts deleted file mode 100644 index ab559f248..000000000 --- a/.context/effect/packages/platform-node/test/NodeHttpClient.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -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" -import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" - -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.Success {} -const JsonPlaceholder = Context.Service("test/JsonPlaceholder") -const JsonPlaceholderLive = Layer.effect(JsonPlaceholder)(makeJsonPlaceholder) -;[ - { - name: "fetch", - layer: NodeClient.layerFetch - }, - { - name: "node:http", - layer: NodeClient.layerNodeHttp - }, - { - 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(Effect.provide(layer), flaky)) - - it.effect("google followRedirects", () => - flaky( - 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(Effect.provide(layer)) - )) - - it.effect("google stream", () => - flaky( - Effect.gen(function*() { - const client = yield* HttpClient.HttpClient - const response = yield* client.get("https://www.google.com/").pipe( - Effect.map((_) => _.stream), - Stream.unwrap, - Stream.decodeText(), - Stream.mkString - ) - expect(response).toContain("Google") - }).pipe(Effect.provide(layer)) - )) - - 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( - Effect.provide(JsonPlaceholderLive.pipe( - Layer.provide(layer) - )), - flaky - )) - - 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( - Effect.provide(JsonPlaceholderLive.pipe( - Layer.provide(layer) - )), - flaky - )) - - 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(Effect.provide(layer), flaky)) - - 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("TimeoutError", () => Effect.succeedNone) - ) - expect(response._tag).toEqual("None") - }).pipe(Effect.provide(layer), flaky)) - - it.effect("close early", () => - Effect.gen(function*() { - const response = yield* HttpClient.get("https://www.google.com/") - expect(response.status).toBe(200) - }).pipe(Effect.provide(layer), flaky)) - }) -}) - -const flaky = (effect: Effect.Effect) => - effect.pipe( - Effect.timeoutOrElse({ - duration: "10 seconds", - orElse: () => Effect.void - }) - ) diff --git a/.context/effect/packages/platform-node/test/NodeSocket.test.ts b/.context/effect/packages/platform-node/test/NodeSocket.test.ts deleted file mode 100644 index 5b2e06235..000000000 --- a/.context/effect/packages/platform-node/test/NodeSocket.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { NodeSocket, NodeSocketServer } from "@effect/platform-node" -import { assert, describe, expect, it } from "@effect/vitest" -import { Effect, Queue } from "effect" -import * as Fiber from "effect/Fiber" -import * as Stream from "effect/Stream" -import { Socket, type SocketServer } from "effect/unstable/socket" -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.effect("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 - ) - - const output = yield* outputEffect - assert.strictEqual(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.effect("messages", () => - Effect.gen(function*() { - const server = yield* makeServer - const socket = yield* Socket.makeWebSocket(Effect.succeed(url), { - closeCodeIsError: () => false - }) - const messages = yield* Queue.unbounded() - const fiber = yield* Effect.forkChild(socket.run((_) => Queue.offer(messages, _))) - 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* Queue.take(messages) - assert.deepStrictEqual(message, new TextEncoder().encode("Right back at you!")) - - server.send(new Blob(["A Blob message"])) - message = yield* Queue.take(messages) - assert.deepStrictEqual(message, new TextEncoder().encode("A Blob message")) - - server.close() - const exit = yield* Fiber.await(fiber) - assert.strictEqual(exit._tag, "Success") - }).pipe( - Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)) - )) - - it.effect("close codes are errors by default", () => - Effect.gen(function*() { - const server = yield* makeServer - const socket = yield* Socket.makeWebSocket(Effect.succeed(url)) - const fiber = yield* Effect.forkChild(socket.run(() => {})) - - yield* Effect.promise(() => server.connected) - server.close({ code: 1000, reason: "done", wasClean: true }) - - const exit = yield* Effect.exit(Fiber.join(fiber)) - assert.isTrue(exit._tag === "Failure") - if (exit._tag === "Failure") { - const failure = exit.cause.reasons[0] - if (failure._tag === "Fail") { - assert.isTrue(failure.error instanceof Socket.SocketError) - assert.strictEqual(failure.error.reason._tag, "SocketCloseError") - if (failure.error.reason._tag === "SocketCloseError") { - assert.strictEqual(failure.error.reason.code, 1000) - assert.strictEqual(failure.error.reason.closeReason, "done") - } - } - } - }).pipe( - Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)) - )) - }) - - describe("TransformStream", () => { - it.effect("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 - }), - { - closeCodeIsError: () => false - } - ) - yield* socket.writer.pipe( - Effect.tap((write) => - write("Hello").pipe( - Effect.andThen(write("World")) - ) - ), - Effect.scoped, - Effect.forkChild - ) - 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/.context/effect/packages/platform-node/test/RpcServer.test.ts b/.context/effect/packages/platform-node/test/RpcServer.test.ts deleted file mode 100644 index b4104c316..000000000 --- a/.context/effect/packages/platform-node/test/RpcServer.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { NodeHttpServer, NodeSocket, NodeSocketServer } from "@effect/platform-node" -import { assert, describe, it } from "@effect/vitest" -import { Cause, Deferred, Effect, Fiber, Layer, Ref, Schedule, Schema, Stream } from "effect" -import { Entity, EntityProxy, EntityProxyServer, Sharding } from "effect/unstable/cluster" -import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" -import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" -import { SocketServer } from "effect/unstable/socket" -import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts" -import { RpcLive, User } from "./fixtures/rpc-schemas.ts" - -describe("RpcServer", () => { - // http ndjson - const HttpProtocol = RpcServer.layerProtocolHttp({ path: "/rpc" }).pipe( - Layer.provide(HttpRouter.layer) - ) - const HttpNdjsonServer = RpcLive.pipe( - Layer.provideMerge(HttpProtocol), - Layer.provide(HttpRouter.serve(HttpProtocol, { disableListenLog: true, disableLogger: true })) - ) - const HttpNdjsonClient = UsersClient.layer.pipe( - Layer.provide( - RpcClient.layerProtocolHttp({ - url: "", - transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc")) - }) - ) - ) - const CustomDefectLayer = HttpNdjsonClient.pipe( - Layer.provideMerge(HttpNdjsonServer), - Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) - ) - 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 WsProtocol = RpcServer.layerProtocolWebsocket({ path: "/rpc" }).pipe( - Layer.provide(HttpRouter.layer) - ) - const HttpWsServer = RpcLive.pipe( - Layer.provideMerge(WsProtocol), - Layer.provide(HttpRouter.serve(WsProtocol, { disableListenLog: true, disableLogger: true })) - ) - 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.unwrap) - ) - ) - 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.unwrap) - ) - ) - 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: "node" - // }) - // ) - // ), - // 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", () => { - it.effect("preserves full defect with custom schema", () => - Effect.gen(function*() { - const client = yield* UsersClient - const cause = yield* client.ProduceDefectCustom().pipe( - Effect.sandbox, - Effect.flip - ) - const defect = Cause.squash(cause) - assert.instanceOf(defect, Error) - assert.strictEqual(defect.name, "CustomDefect") - assert.strictEqual(defect.message, "detailed error") - assert.strictEqual(defect.stack, "Error: detailed error\n at handler.ts:1") - }).pipe(Effect.provide(CustomDefectLayer))) - }) - - describe("entity proxy", () => { - it.effect("provides handler context for generated rpc handlers", () => - Effect.gen(function*() { - const TestEntity = Entity.make("TestEntity", [Rpc.make("NoPayload")]) - const TestEntityRpcs = EntityProxy.toRpcGroup(TestEntity) - const called = yield* Deferred.make() - const testClient = (entityId: string) => ({ - NoPayload: (payload: void, options?: { readonly discard?: boolean }) => - Effect.gen(function*() { - assert.strictEqual(entityId, "id") - assert.strictEqual(payload, undefined) - assert.strictEqual(options?.discard, true) - yield* Deferred.succeed(called, undefined) - }) - }) - const sharding = Sharding.Sharding.of({ - ...({} as Sharding.Sharding["Service"]), - isShutdown: Effect.succeed(false), - makeClient: () => Effect.succeed(testClient) as never, - pollStorage: Effect.void - }) - - const client = yield* RpcTest.makeClient(TestEntityRpcs).pipe( - Effect.provide(EntityProxyServer.layerRpcHandlers(TestEntity)), - Effect.provideService(Sharding.Sharding, sharding) - ) - - yield* client["TestEntity.NoPayloadDiscard"]({ - entityId: "id", - payload: undefined - }) - yield* Deferred.await(called) - })) - }) - - describe("unknown-tag isolation", () => { - const Ticker = Rpc.make("Ticker", { - success: Schema.Number, - stream: true - }) - const Ghost = Rpc.make("Ghost", { - payload: { value: Schema.String }, - success: Schema.String - }) - - const serverGroup = RpcGroup.make(Ticker) - const clientGroup = RpcGroup.make(Ticker, Ghost) - - const TickerHandlers = serverGroup.toLayer({ - Ticker: () => Stream.fromSchedule(Schedule.spaced("60 millis")) - }) - - const IsolationServer = RpcServer.layer(serverGroup).pipe( - Layer.provide(TickerHandlers), - Layer.provideMerge(RpcServer.layerProtocolSocketServer), - Layer.provideMerge(NodeSocketServer.layer({ port: 0 })), - Layer.provide(RpcSerialization.layerNdjson) - ) - const IsolationClient = RpcClient.layerProtocolSocket().pipe( - 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.unwrap) - ), - Layer.provide(RpcSerialization.layerNdjson) - ) - - it.live( - "an unknown request tag fails only its own request, not other in-flight streams on the same connection", - () => - Effect.gen(function*() { - const client = yield* RpcClient.make(clientGroup) - - const received = yield* Ref.make>([]) - - const tickerFiber = yield* client.Ticker().pipe( - Stream.runForEach((value) => Ref.update(received, (xs) => [...xs, value])), - Effect.forkChild - ) - - yield* Effect.retry( - Effect.flatMap( - Ref.get(received), - (xs) => xs.length >= 2 ? Effect.void : Effect.fail("not enough ticks yet") - ), - { schedule: Schedule.spaced("50 millis"), times: 200 } - ) - - const ticksBeforeGhost = (yield* Ref.get(received)).length - assert.isAtLeast(ticksBeforeGhost, 2) - - const ghostExit = yield* client.Ghost({ value: "boo" }).pipe(Effect.exit) - assert.isTrue(ghostExit._tag === "Failure", "Ghost call should fail with the routing miss") - - yield* Effect.sleep("300 millis") - - const ticksAfterGhost = (yield* Ref.get(received)).length - const tickerStatus = tickerFiber.pollUnsafe() - - yield* Fiber.interrupt(tickerFiber) - - assert.isUndefined(tickerStatus, "Ticker stream must still be running after the unknown-tag failure") - assert.isAbove( - ticksAfterGhost, - ticksBeforeGhost, - "Ticker stream must keep emitting after the unknown-tag failure" - ) - }).pipe(Effect.provide(IsolationClient.pipe(Layer.provideMerge(IsolationServer)))), - { timeout: 30_000 } - ) - }) -}) diff --git a/.context/effect/packages/platform-node/test/cluster/SocketRunner.test.ts b/.context/effect/packages/platform-node/test/cluster/SocketRunner.test.ts deleted file mode 100644 index 721cbf443..000000000 --- a/.context/effect/packages/platform-node/test/cluster/SocketRunner.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NodeClusterSocket } from "@effect/platform-node" -import { describe, it } from "@effect/vitest" -import { BigDecimal, Effect, Layer, Option, PrimaryKey, Schema } from "effect" -import { - ClusterSchema, - Entity, - MessageStorage, - RunnerAddress, - RunnerHealth, - RunnerStorage, - ShardingConfig, - SocketRunner -} from "effect/unstable/cluster" -import { Rpc, RpcSerialization } from "effect/unstable/rpc" - -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.live( - "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.fromStringUnsafe("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 - )), - 30_000 - ) -}) diff --git a/.context/effect/packages/platform-node/test/cluster/SqlMessageStorage.test.ts b/.context/effect/packages/platform-node/test/cluster/SqlMessageStorage.test.ts deleted file mode 100644 index 16deaaa86..000000000 --- a/.context/effect/packages/platform-node/test/cluster/SqlMessageStorage.test.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { SqliteClient } from "@effect/sql-sqlite-node" -import { assert, describe, expect, it } from "@effect/vitest" -import { Effect, Fiber, FileSystem, Latch, Layer, Option } from "effect" -import { TestClock } from "effect/testing" -import { Message, MessageStorage, ShardingConfig, Snowflake, SqlMessageStorage } from "effect/unstable/cluster" -import { SqlClient } from "effect/unstable/sql" -import { MysqlContainer } from "../fixtures/mysql2-utils.ts" -import { PgContainer } from "../fixtures/pg-utils.ts" -import { - makeAckChunk, - makeChunkReply, - makeReply, - makeRequest, - PrimaryKeyTest, - StreamRpc -} from "./MessageStorageTest.ts" - -const StorageLive = SqlMessageStorage.layer.pipe( - Layer.provideMerge(Snowflake.layerGenerator), - Layer.provide(ShardingConfig.layerDefaults) -) - -const truncate = Effect.gen(function*() { - const sql = yield* SqlClient.SqlClient - yield* sql`DELETE FROM cluster_replies` - yield* sql`DELETE FROM cluster_messages` -}) - -describe("SqlMessageStorage", () => { - ;([ - ["pg", Layer.orDie(PgContainer.layerClient)], - ["mysql", Layer.orDie(MysqlContainer.layerClient)], - ["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.SqlClient - const storage = yield* MessageStorage.MessageStorage - const request = yield* makeRequest({ - rpc: StreamRpc, - payload: StreamRpc.payloadSchema.make({ 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: StreamRpc.payloadSchema.make({ id: 123 }) - }) - ) - assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply)) - 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: StreamRpc.payloadSchema.make({ id: 123 }) - }) - ) - assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply)) - expect(result.lastReceivedReply.value._tag).toEqual("WithExit") - - // duplicate WithExit - const fiber = yield* storage.saveReply(yield* makeReply(request)).pipe(Effect.forkChild) - yield* TestClock.adjust(1) - while (!fiber.pollUnsafe()) { - 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: PrimaryKeyTest, - payload: PrimaryKeyTest.payloadSchema.make({ id: 123 }) - }) - ) - const result = yield* storage.saveRequest( - yield* makeRequest({ - rpc: PrimaryKeyTest, - payload: PrimaryKeyTest.payloadSchema.make({ 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* Latch.make() - const request = yield* makeRequest() - yield* storage.saveRequest(request) - const fiber = yield* storage.registerReplyHandler( - new Message.OutgoingRequest({ - ...request, - respond: () => latch.open - }) - ).pipe(Effect.forkChild) - yield* TestClock.adjust(1) - yield* storage.saveReply(yield* makeReply(request)) - yield* latch.await - yield* Fiber.await(fiber) - })) - - 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.unwrap, Layer.provide(NodeFileSystem.layer)) diff --git a/.context/effect/packages/platform-node/test/cluster/SqlRunnerStorage.test.ts b/.context/effect/packages/platform-node/test/cluster/SqlRunnerStorage.test.ts deleted file mode 100644 index 913899875..000000000 --- a/.context/effect/packages/platform-node/test/cluster/SqlRunnerStorage.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { NodeFileSystem } from "@effect/platform-node" -import { SqliteClient } from "@effect/sql-sqlite-node" -import { describe, expect, it } from "@effect/vitest" -import { Effect, FileSystem, Layer } from "effect" -import { - Runner, - RunnerAddress, - RunnerStorage, - ShardId, - ShardingConfig, - SqlRunnerStorage -} from "effect/unstable/cluster" -import { MysqlContainer } from "../fixtures/mysql2-utils.ts" -import { PgContainer } from "../fixtures/pg-utils.ts" - -const StorageLive = SqlRunnerStorage.layer - -describe("SqlRunnerStorage", () => { - ;([ - ["pg", Layer.orDie(PgContainer.layerClient)], - ["mysql", Layer.orDie(MysqlContainer.layerClient)], - ["vitess", Layer.orDie(MysqlContainer.layerClientVitess)], - ["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.unwrap, Layer.provide(NodeFileSystem.layer)) diff --git a/.context/effect/packages/platform-node/test/fixtures/rpc-schemas.ts b/.context/effect/packages/platform-node/test/fixtures/rpc-schemas.ts deleted file mode 100644 index 4becda4d6..000000000 --- a/.context/effect/packages/platform-node/test/fixtures/rpc-schemas.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Context, Deferred, Effect, Layer, Metric, Option, Queue, Schema } from "effect" -import { Headers } from "effect/unstable/http" -import * as Rpc from "effect/unstable/rpc/Rpc" -import * as RpcGroup from "effect/unstable/rpc/RpcGroup" -import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware" -import * as RpcServer from "effect/unstable/rpc/RpcServer" - -export class User extends Schema.Class("User")({ - id: Schema.String, - name: Schema.String -}) {} - -class StreamUsers extends Rpc.make("StreamUsers", { - success: User, - payload: { - id: Schema.String - }, - stream: true -}) {} - -class CurrentUser extends Context.Service()("CurrentUser") {} - -class Unauthorized extends Schema.ErrorClass("Unauthorized")({ - _tag: Schema.tag("Unauthorized") -}) {} - -class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { - error: Unauthorized, - requiredForClient: true -}) {} - -class TimingMiddleware extends RpcMiddleware.Service()("TimingMiddleware") {} - -class GetUser extends Rpc.make("GetUser", { - success: User, - payload: { id: Schema.String } -}) {} - -export const UserRpcs = RpcGroup.make( - GetUser, - Rpc.make("GetUserDeferred", { - success: User, - payload: { id: Schema.String } - }), - Rpc.make("GetUserOption", { - success: Schema.Option(User), - payload: { id: Schema.String } - }), - StreamUsers, - Rpc.make("GetInterrupts", { - success: Schema.Number - }), - Rpc.make("GetEmits", { - success: Schema.Number - }), - Rpc.make("ProduceDefect"), - Rpc.make("ProduceDefectCustom", { - defect: Schema.Defect({ includeStack: true }) - }), - 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) - -export const AuthLive = Layer.succeed(AuthMiddleware)( - AuthMiddleware.of((effect, options) => - Effect.provideService( - effect, - CurrentUser, - 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") -export const TimingLive = Layer.succeed(TimingMiddleware)( - TimingMiddleware.of((effect) => - effect.pipe( - Effect.tap(Metric.update(rpcSuccesses, 1)), - Effect.tapDefect(() => Metric.update(rpcDefects, 1)), - Effect.ensuring(Metric.update(rpcCount, 1)) - ) - ) -) - -export const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { - let interrupts = 0 - let emits = 0 - return UserRpcs.of({ - GetUser: (_) => - CurrentUser.pipe( - Rpc.fork - ), - GetUserDeferred(_) { - const deferred = Deferred.makeUnsafe() - Deferred.doneUnsafe(deferred, Effect.succeed(new User({ id: "1", name: "John" }))) - return Effect.succeed(deferred) - }, - GetUserOption: Effect.fnUntraced(function*(req) { - return Option.some(new User({ id: req.id, name: "John" })) - }), - StreamUsers: Effect.fnUntraced(function*(req, _) { - const mailbox = yield* Queue.bounded(0) - - yield* Effect.addFinalizer(() => - Effect.sync(() => { - interrupts++ - }) - ) - - yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe( - Effect.tap(() => - Effect.sync(() => { - emits++ - }) - ), - Effect.delay(100), - Effect.forever, - Effect.forkScoped - ) - - return mailbox - }), - GetInterrupts: () => Effect.sync(() => interrupts), - GetEmits: () => Effect.sync(() => emits), - ProduceDefect: () => Effect.die("boom"), - ProduceDefectCustom: () => - Effect.die({ - message: "detailed error", - stack: "Error: detailed error\n at handler.ts:1", - name: "CustomDefect" - }), - 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, { - disableFatalDefects: true -}).pipe( - Layer.provide([ - UsersLive, - AuthLive, - TimingLive - ]) -) - -export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) => - next({ - ...request, - headers: Headers.set(request.headers, "name", "Logged in user") - })) diff --git a/.context/effect/packages/platform-node/tsconfig.json b/.context/effect/packages/platform-node/tsconfig.json deleted file mode 100644 index 2b4745a05..000000000 --- a/.context/effect/packages/platform-node/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "../../tsconfig.base.json", - "include": ["src"], - "references": [ - { "path": "../effect" }, - { "path": "../platform-node-shared" } - ], - "compilerOptions": { - "types": ["node"] - } -} diff --git a/.context/effect/packages/platform-node/vitest.config.ts b/.context/effect/packages/platform-node/vitest.config.ts deleted file mode 100644 index fb966ae87..000000000 --- a/.context/effect/packages/platform-node/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/platform/browser/CHANGELOG.md b/.context/effect/packages/platform/browser/CHANGELOG.md new file mode 100644 index 000000000..5ea0238ae --- /dev/null +++ b/.context/effect/packages/platform/browser/CHANGELOG.md @@ -0,0 +1,829 @@ +# @effect/platform-browser + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#7075](https://github.com/Effect-TS/effect/pull/7075) [`2f9016d`](https://github.com/Effect-TS/effect/commit/2f9016d23cdbde159153fd8b03507d03a066966f) Thanks @fubhy! - Fix form data decoding for XMLHttpRequest client responses. + +- [#7012](https://github.com/Effect-TS/effect/pull/7012) [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217) Thanks @tim-smart! - Vendor the multipart parser as `effect/unstable/http/MultipartParser`, add the Node.js adapter at `@effect/platform-node/NodeMultipartParser`, and remove the external `multipasta` dependency. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6857](https://github.com/Effect-TS/effect/pull/6857) [`79db8e5`](https://github.com/Effect-TS/effect/commit/79db8e5e42b300888401b6487e7628e0a05180ab) Thanks @fubhy! - Fix IndexedDB-backed key-value writes to wait for transaction commit before reporting success. + +- [#6852](https://github.com/Effect-TS/effect/pull/6852) [`ef6eca7`](https://github.com/Effect-TS/effect/commit/ef6eca71dcb9abef7c2685d127a89e25fb6f0e62) Thanks @fubhy! - Fix IndexedDB query range, ordering, streaming, and transaction semantics. + +- [#6683](https://github.com/Effect-TS/effect/pull/6683) [`5f87bc7`](https://github.com/Effect-TS/effect/commit/5f87bc7a6608f129e2ee4eaa6745c61d920bac53) Thanks @tim-smart! - Fix `BrowserCrypto.randomBytes` for requests larger than the Web Crypto per-call limit. + +- [#6680](https://github.com/Effect-TS/effect/pull/6680) [`23e176a`](https://github.com/Effect-TS/effect/commit/23e176a4f05ed3e81cc13a5d70111099692ea9a5) Thanks @tim-smart! - Fix worker runner disconnect notifications and event listener cleanup. + +- [#6854](https://github.com/Effect-TS/effect/pull/6854) [`39ed94a`](https://github.com/Effect-TS/effect/commit/39ed94a0751ae060ae8db24eb74865a636acf1cd) Thanks @fubhy! - Abort IndexedDB versionchange transactions when schema migrations fail. + +- [#6659](https://github.com/Effect-TS/effect/pull/6659) [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff) Thanks @tim-smart! - Preserve prototype accessors when code is compiled with loose object spread transforms. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + +## 4.0.0-beta.101 + +### Patch Changes + +- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: + - effect@4.0.0-beta.101 + +## 4.0.0-beta.100 + +### Patch Changes + +- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: + - effect@4.0.0-beta.100 + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- [#2241](https://github.com/Effect-TS/effect-smol/pull/2241) [`c5e54d8`](https://github.com/Effect-TS/effect-smol/commit/c5e54d8e4d4ea0f64d9023793b54dfa83d85eac4) Thanks @tim-smart! - Fix IndexedDB bulk writes so `insertAll` and `upsertAll` resume when used inside `withTransaction`. + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- [#2196](https://github.com/Effect-TS/effect-smol/pull/2196) [`b7abd0e`](https://github.com/Effect-TS/effect-smol/commit/b7abd0e5c236500a00de3a871c151cc50c3ec40b) Thanks @juemrami! - Adds an IndexedDB backed implementation of `KeyValueStore` as `BrowserKeyValueStore.layerIndexedDb`. This backend allows for non-blocking `KeyValueStore` operations, unlike the existing `Storage` api backed implementations. + +- [#2183](https://github.com/Effect-TS/effect-smol/pull/2183) [`e32343a`](https://github.com/Effect-TS/effect-smol/commit/e32343adf3e449cb9452908655c83757843d1907) Thanks @tim-smart! - use Cause.NoSuchElementError for idb .first queries + +- [#2182](https://github.com/Effect-TS/effect-smol/pull/2182) [`ae40463`](https://github.com/Effect-TS/effect-smol/commit/ae404636ab75fc47d0167c22a20564777c12cf59) Thanks @tim-smart! - cache base idb query builders + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- [#2110](https://github.com/Effect-TS/effect-smol/pull/2110) [`f862e40`](https://github.com/Effect-TS/effect-smol/commit/f862e40573b6d1c04942799be5ff6f7dbea22ae9) Thanks @tim-smart! - cleanup IndexedDb prototypes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- [#2086](https://github.com/Effect-TS/effect-smol/pull/2086) [`979b56b`](https://github.com/Effect-TS/effect-smol/commit/979b56b5c45facc488cc920a83339b87fc51334d) Thanks @tim-smart! - fix idb entries transaction + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- [#2030](https://github.com/Effect-TS/effect-smol/pull/2030) [`253efe6`](https://github.com/Effect-TS/effect-smol/commit/253efe6f52ecef187d286e6eaba270e0f4d939ed) Thanks @tim-smart! - Add BrowserPersistence.layerIndexedDb for composing Persistence.layer with the IndexedDB backing layer, and export BrowserPersistence from the package barrel. + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1976](https://github.com/Effect-TS/effect-smol/pull/1976) [`7a35a83`](https://github.com/Effect-TS/effect-smol/commit/7a35a83382f9956b1cb92b6a47399adabd79cbf9) Thanks @tim-smart! - improve idb support for compound indexes + +- [#1993](https://github.com/Effect-TS/effect-smol/pull/1993) [`acc7fff`](https://github.com/Effect-TS/effect-smol/commit/acc7fffcaa82b34225f54d324090c2853f9b3547) Thanks @tim-smart! - improve idb transaction api + +- [#1964](https://github.com/Effect-TS/effect-smol/pull/1964) [`1ee4543`](https://github.com/Effect-TS/effect-smol/commit/1ee4543450895f58d2f3ba986dadea6962de4818) Thanks @tim-smart! - allow customizing idb durability + +- [#1937](https://github.com/Effect-TS/effect-smol/pull/1937) [`96cb778`](https://github.com/Effect-TS/effect-smol/commit/96cb77829d6677de788c4227cbee06fc4c707c9c) Thanks @tim-smart! - add .reactive to indexeddb .first queries + +- [#1953](https://github.com/Effect-TS/effect-smol/pull/1953) [`fbbdaec`](https://github.com/Effect-TS/effect-smol/commit/fbbdaec5ed020f7f94a030ede02c121011313037) Thanks @tim-smart! - add defaults to indexeddb reactivity keys + +- [#1977](https://github.com/Effect-TS/effect-smol/pull/1977) [`4f58e30`](https://github.com/Effect-TS/effect-smol/commit/4f58e309d16ded3cac3fb95185a61c06972e2e26) Thanks @tim-smart! - add idb stream and offset + +- [#1989](https://github.com/Effect-TS/effect-smol/pull/1989) [`3cc091d`](https://github.com/Effect-TS/effect-smol/commit/3cc091de7ecfa657b519b0ced316754bcbf53099) Thanks @tim-smart! - use encoded types for idb queries + +- [#1985](https://github.com/Effect-TS/effect-smol/pull/1985) [`f244e71`](https://github.com/Effect-TS/effect-smol/commit/f244e7141770bab8a00f34e48e6923d97ebbe405) Thanks @tim-smart! - add .reverse() to idb select + +- [#1992](https://github.com/Effect-TS/effect-smol/pull/1992) [`8c74d03`](https://github.com/Effect-TS/effect-smol/commit/8c74d0353fff9099075ba28bb50166a23f4062dc) Thanks @tim-smart! - Add rebuild api to idb databases + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- [#1924](https://github.com/Effect-TS/effect-smol/pull/1924) [`716fe24`](https://github.com/Effect-TS/effect-smol/commit/716fe24886292aa6af2ad1f3fd7aa1b2f0a10c7f) Thanks @tim-smart! - allow Model.Class for indexeddb schemas + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- [#1240](https://github.com/Effect-TS/effect-smol/pull/1240) [`583ea00`](https://github.com/Effect-TS/effect-smol/commit/583ea002fdccc58fd2110a36c6d103e63152dcb3) Thanks @SandroMaglione! - add IndexedDb modules + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. + + Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-browser/LICENSE b/.context/effect/packages/platform/browser/LICENSE similarity index 100% rename from .context/effect/packages/platform-browser/LICENSE rename to .context/effect/packages/platform/browser/LICENSE diff --git a/.context/effect/packages/platform/browser/README.md b/.context/effect/packages/platform/browser/README.md new file mode 100644 index 000000000..470b09f6c --- /dev/null +++ b/.context/effect/packages/platform/browser/README.md @@ -0,0 +1,14 @@ +# @effect/platform-browser + +Browser implementations of the Effect platform services, including the HTTP client, workers, key-value storage, IndexedDB, clipboard, and geolocation. + +## Installation + +```sh +npm install effect@beta @effect/platform-browser@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/platform-browser) diff --git a/.context/effect/packages/platform/browser/package.json b/.context/effect/packages/platform/browser/package.json new file mode 100644 index 000000000..2810b0df6 --- /dev/null +++ b/.context/effect/packages/platform/browser/package.json @@ -0,0 +1,73 @@ +{ + "name": "@effect/platform-browser", + "type": "module", + "version": "4.0.0-rc.108", + "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" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./index": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./index": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "devDependencies": { + "effect": "workspace:^", + "fake-indexeddb": "^6.2.5", + "mock-xmlhttprequest": "^8.4.1" + } +} diff --git a/.context/effect/packages/platform-browser/src/BrowserCrypto.ts b/.context/effect/packages/platform/browser/src/BrowserCrypto.ts similarity index 95% rename from .context/effect/packages/platform-browser/src/BrowserCrypto.ts rename to .context/effect/packages/platform/browser/src/BrowserCrypto.ts index 2734cada3..bf00b051a 100644 --- a/.context/effect/packages/platform-browser/src/BrowserCrypto.ts +++ b/.context/effect/packages/platform/browser/src/BrowserCrypto.ts @@ -22,7 +22,7 @@ import * as PlatformError from "effect/PlatformError" * Use to override the browser `Crypto` object used by the platform crypto * layer. * - * @category references + * @category services * @since 1.0.0 */ export const WebCrypto = Context.Reference("@effect/platform-browser/Crypto/WebCrypto", { @@ -60,7 +60,9 @@ export const layer: Layer.Layer = Layer.effect( } const randomBytes = (size: number): Uint8Array => { const bytes = new Uint8Array(size) - crypto.getRandomValues(bytes) + for (let i = 0; i < bytes.length; i += 65_536) { + crypto.getRandomValues(bytes.subarray(i, i + 65_536)) + } return bytes } diff --git a/.context/effect/packages/platform-browser/src/BrowserHttpClient.ts b/.context/effect/packages/platform/browser/src/BrowserHttpClient.ts similarity index 97% rename from .context/effect/packages/platform-browser/src/BrowserHttpClient.ts rename to .context/effect/packages/platform/browser/src/BrowserHttpClient.ts index 29e68516b..2d506a078 100644 --- a/.context/effect/packages/platform-browser/src/BrowserHttpClient.ts +++ b/.context/effect/packages/platform/browser/src/BrowserHttpClient.ts @@ -28,8 +28,8 @@ import * as HttpClientError from "effect/unstable/http/HttpClientError" import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" import * as HttpIncomingMessage from "effect/unstable/http/HttpIncomingMessage" +import * as HeaderParser from "effect/unstable/http/MultipartParser/HeadersParser" import * as UrlParams from "effect/unstable/http/UrlParams" -import * as HeaderParser from "multipasta/HeadersParser" // ============================================================================= // Fetch @@ -39,14 +39,14 @@ export { /** * Context reference for the `fetch` implementation used by the fetch-based HTTP client. * - * @category fetch + * @category services * @since 4.0.0 */ Fetch, /** * Layer that provides an `HttpClient` implementation backed by the configured `Fetch` function. * - * @category fetch + * @category layers * @since 4.0.0 */ layer as layerFetch, @@ -58,7 +58,7 @@ export { * Use to provide default credentials, cache, redirect, integrity, or other * fetch options for browser HTTP requests. * - * @category fetch + * @category services * @since 4.0.0 */ RequestInit @@ -87,7 +87,7 @@ export type XHRResponseType = "arraybuffer" | "text" * @see {@link XHRResponseType} for the allowed response body modes * @see {@link withXHRArrayBuffer} for scoping XHR response handling to `ArrayBuffer` * - * @category references + * @category services * @since 4.0.0 */ export const CurrentXHRResponseType: Context.Reference = Context.Reference( @@ -98,7 +98,7 @@ export const CurrentXHRResponseType: Context.Reference = Contex /** * Runs an effect with `CurrentXHRResponseType` set to `"arraybuffer"` so the XHR HTTP client receives response bodies as `ArrayBuffer` values. * - * @category references + * @category providing services * @since 4.0.0 */ export const withXHRArrayBuffer = ( @@ -399,7 +399,11 @@ class ClientResponseImpl extends IncomingMessageImpl { - return Effect.die("Not implemented") + return Effect.flatMap(this.arrayBuffer, (body) => + Effect.tryPromise({ + try: () => new globalThis.Response(body, { headers: this.headers }).formData(), + catch: this.onError + })) } override toString(): string { diff --git a/.context/effect/packages/platform-browser/src/BrowserKeyValueStore.ts b/.context/effect/packages/platform/browser/src/BrowserKeyValueStore.ts similarity index 79% rename from .context/effect/packages/platform-browser/src/BrowserKeyValueStore.ts rename to .context/effect/packages/platform/browser/src/BrowserKeyValueStore.ts index 72840e396..dd63cbffe 100644 --- a/.context/effect/packages/platform-browser/src/BrowserKeyValueStore.ts +++ b/.context/effect/packages/platform/browser/src/BrowserKeyValueStore.ts @@ -74,8 +74,11 @@ export const layerIndexedDb = (options?: { return KeyValueStore.make({ clear: Effect.suspend(() => { - const store = getKvsEntriesStore(db, "readwrite") - return idbRequest({ method: "clear", message: "Failed to clear backing store" }, () => store.clear()) + return idbWriteRequest( + db, + { method: "clear", message: "Failed to clear backing store" }, + (store) => store.clear() + ) }), get: (key: string) => Effect.map( @@ -103,10 +106,10 @@ export const layerIndexedDb = (options?: { ), set: (key: string, value: string | Uint8Array) => Effect.asVoid(Effect.suspend(() => { - const store = getKvsEntriesStore(db, "readwrite") - return idbRequest( + return idbWriteRequest( + db, { method: "set", message: "Failed to set value in backing store", key }, - () => store.put({ key, value }) + (store) => store.put({ key, value }) ) })), size: Effect.suspend(() => { @@ -118,10 +121,10 @@ export const layerIndexedDb = (options?: { }), remove: (key: string) => Effect.asVoid(Effect.suspend(() => { - const store = getKvsEntriesStore(db, "readwrite") - return idbRequest( + return idbWriteRequest( + db, { method: "remove", message: "Failed to remove value from backing store", key }, - () => store.delete(key) + (store) => store.delete(key) ) })) }) @@ -171,6 +174,45 @@ const idbRequest = ( )) }) +const idbWriteRequest = ( + db: IDBDatabase, + failArgs: { method: string; message: string; key?: string }, + evaluate: (store: IDBObjectStore) => IDBRequest +): Effect.Effect => + Effect.callback((resume) => { + const transaction = db.transaction(entriesStoreName, "readwrite") + const request = evaluate(transaction.objectStore(entriesStoreName)) + let result: A + let done = false + + const fail = (cause: unknown) => { + if (done) return + done = true + resume(Effect.fail(new KeyValueStore.KeyValueStoreError({ ...failArgs, cause }))) + } + + if (request.readyState === "done") { + result = request.result + } else { + request.onsuccess = () => { + result = request.result + } + request.onerror = () => fail(request.error) + } + + transaction.oncomplete = () => { + if (done) return + done = true + resume(Effect.succeed(result!)) + } + transaction.onerror = () => fail(transaction.error) + transaction.onabort = () => fail(transaction.error) + + return Effect.sync(() => { + if (!done) transaction.abort() + }) + }) + const getKvsEntriesStore = (db: IDBDatabase, mode: IDBTransactionMode) => { const transaction = db.transaction(entriesStoreName, mode) return transaction.objectStore(entriesStoreName) diff --git a/.context/effect/packages/platform-browser/src/BrowserPersistence.ts b/.context/effect/packages/platform/browser/src/BrowserPersistence.ts similarity index 100% rename from .context/effect/packages/platform-browser/src/BrowserPersistence.ts rename to .context/effect/packages/platform/browser/src/BrowserPersistence.ts diff --git a/.context/effect/packages/platform-browser/src/BrowserRuntime.ts b/.context/effect/packages/platform/browser/src/BrowserRuntime.ts similarity index 98% rename from .context/effect/packages/platform-browser/src/BrowserRuntime.ts rename to .context/effect/packages/platform/browser/src/BrowserRuntime.ts index 0cc99d7f8..ef1474e66 100644 --- a/.context/effect/packages/platform-browser/src/BrowserRuntime.ts +++ b/.context/effect/packages/platform/browser/src/BrowserRuntime.ts @@ -29,7 +29,7 @@ import { makeRunMain, type Teardown } from "effect/Runtime" * The `beforeunload` interruption is best-effort. Browser teardown may prevent * asynchronous finalizers, network work, timers, or prompts from completing. * - * @category Runtime + * @category running * @since 4.0.0 */ export const runMain: { diff --git a/.context/effect/packages/platform-browser/src/BrowserSocket.ts b/.context/effect/packages/platform/browser/src/BrowserSocket.ts similarity index 100% rename from .context/effect/packages/platform-browser/src/BrowserSocket.ts rename to .context/effect/packages/platform/browser/src/BrowserSocket.ts diff --git a/.context/effect/packages/platform-browser/src/BrowserStream.ts b/.context/effect/packages/platform/browser/src/BrowserStream.ts similarity index 96% rename from .context/effect/packages/platform-browser/src/BrowserStream.ts rename to .context/effect/packages/platform/browser/src/BrowserStream.ts index 5b2fa79dd..56d7333c8 100644 --- a/.context/effect/packages/platform-browser/src/BrowserStream.ts +++ b/.context/effect/packages/platform/browser/src/BrowserStream.ts @@ -19,7 +19,7 @@ import * as Stream from "effect/Stream" * buffer size by passing an object as the second argument with the `bufferSize` * field. * - * @category streams + * @category constructors * @since 4.0.0 */ export const fromEventListenerWindow = ( @@ -41,7 +41,7 @@ export const fromEventListenerWindow = ( * buffer size by passing an object as the second argument with the `bufferSize` * field. * - * @category streams + * @category constructors * @since 4.0.0 */ export const fromEventListenerDocument = ( diff --git a/.context/effect/packages/platform-browser/src/BrowserWorker.ts b/.context/effect/packages/platform/browser/src/BrowserWorker.ts similarity index 100% rename from .context/effect/packages/platform-browser/src/BrowserWorker.ts rename to .context/effect/packages/platform/browser/src/BrowserWorker.ts diff --git a/.context/effect/packages/platform-browser/src/BrowserWorkerRunner.ts b/.context/effect/packages/platform/browser/src/BrowserWorkerRunner.ts similarity index 98% rename from .context/effect/packages/platform-browser/src/BrowserWorkerRunner.ts rename to .context/effect/packages/platform/browser/src/BrowserWorkerRunner.ts index e565fb9c8..0c51c8b03 100644 --- a/.context/effect/packages/platform-browser/src/BrowserWorkerRunner.ts +++ b/.context/effect/packages/platform/browser/src/BrowserWorkerRunner.ts @@ -81,6 +81,7 @@ export const make = (self: MessagePort | Window): WorkerRunner.WorkerRunnerPlatf return Deferred.doneUnsafe(closeLatch, Exit.void) } ports.delete(portId) + Queue.offerUnsafe(disconnects, portId) Effect.runFork(Scope.close(port[1], Exit.void)) } } @@ -122,7 +123,7 @@ export const make = (self: MessagePort | Window): WorkerRunner.WorkerRunnerPlatf portScope, Effect.sync(() => { port.removeEventListener("message", onMsg) - port.removeEventListener("messageerror", onError) + port.removeEventListener("messageerror", onMessageError) port.close() }) )) diff --git a/.context/effect/packages/platform-browser/src/Clipboard.ts b/.context/effect/packages/platform/browser/src/Clipboard.ts similarity index 99% rename from .context/effect/packages/platform-browser/src/Clipboard.ts rename to .context/effect/packages/platform/browser/src/Clipboard.ts index 3a24d8733..a68c41937 100644 --- a/.context/effect/packages/platform-browser/src/Clipboard.ts +++ b/.context/effect/packages/platform/browser/src/Clipboard.ts @@ -38,7 +38,7 @@ const ErrorTypeId = "~@effect/platform-browser/Clipboard/ClipboardError" * MIME type support varies by browser. Failed browser operations are surfaced * as `ClipboardError`. * - * @category models + * @category services * @since 4.0.0 */ export interface Clipboard { diff --git a/.context/effect/packages/platform-browser/src/Geolocation.ts b/.context/effect/packages/platform/browser/src/Geolocation.ts similarity index 99% rename from .context/effect/packages/platform-browser/src/Geolocation.ts rename to .context/effect/packages/platform/browser/src/Geolocation.ts index 309af083e..dc687bee6 100644 --- a/.context/effect/packages/platform-browser/src/Geolocation.ts +++ b/.context/effect/packages/platform/browser/src/Geolocation.ts @@ -44,7 +44,7 @@ const ErrorTypeId = "~@effect/platform-browser/Geolocation/GeolocationError" * @see {@link GeolocationError} for represented browser geolocation failures * @see {@link layer} for the browser-backed service implementation * - * @category models + * @category services * @since 4.0.0 */ export interface Geolocation { diff --git a/.context/effect/packages/platform-browser/src/IndexedDb.ts b/.context/effect/packages/platform/browser/src/IndexedDb.ts similarity index 98% rename from .context/effect/packages/platform-browser/src/IndexedDb.ts rename to .context/effect/packages/platform/browser/src/IndexedDb.ts index 6fdf18d23..aaa96d079 100644 --- a/.context/effect/packages/platform-browser/src/IndexedDb.ts +++ b/.context/effect/packages/platform/browser/src/IndexedDb.ts @@ -19,7 +19,7 @@ const TypeId = "~@effect/platform-browser/IndexedDb" /** * Service interface that provides the browser `indexedDB` factory and `IDBKeyRange` constructor. * - * @category models + * @category services * @since 4.0.0 */ export interface IndexedDb { @@ -40,7 +40,7 @@ export const IndexedDb: Context.Service = Context.Service< const IDBFlatKey = Schema.Union([ Schema.String, Schema.Number.check(Schema.makeFilter((input) => !Number.isNaN(input))), - Schema.DateValid, + Schema.Date, Schema.declare( (input): input is BufferSource => input instanceof ArrayBuffer || @@ -93,7 +93,7 @@ export const make = (impl: Omit): IndexedDb => Indexed /** * Layer that provides `IndexedDb` from `window.indexedDB` and `window.IDBKeyRange`, failing with a config error when they are unavailable. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layerWindow: Layer.Layer = Layer.effect( diff --git a/.context/effect/packages/platform-browser/src/IndexedDbDatabase.ts b/.context/effect/packages/platform/browser/src/IndexedDbDatabase.ts similarity index 98% rename from .context/effect/packages/platform-browser/src/IndexedDbDatabase.ts rename to .context/effect/packages/platform/browser/src/IndexedDbDatabase.ts index 0e98f997d..77f07cc76 100644 --- a/.context/effect/packages/platform-browser/src/IndexedDbDatabase.ts +++ b/.context/effect/packages/platform/browser/src/IndexedDbDatabase.ts @@ -31,12 +31,6 @@ const SchemaProto = { [TypeId]: { _A: (_: never) => _ }, - ...Effectable.Prototype>({ - label: "IndexedDbSchema", - evaluate() { - return this.getQueryBuilder - } - }), get getQueryBuilder() { const self = this as unknown as IndexedDbSchema return IndexedDbDatabase.useSync(({ database, IDBKeyRange, reactivity }) => @@ -48,6 +42,12 @@ const SchemaProto = { }) ) }, + ...Effectable.Prototype>({ + label: "IndexedDbSchema", + evaluate() { + return this.getQueryBuilder + } + }), add( this: IndexedDbSchema, version: Version, @@ -126,7 +126,7 @@ export class IndexedDbDatabaseError extends Data.TaggedError( * @see {@link IndexedDb.IndexedDb} for the lower-level browser IndexedDB primitives * @see {@link make} for creating a schema that provides this service as a layer * - * @category models + * @category services * @since 4.0.0 */ export class IndexedDbDatabase extends Context.Service< @@ -234,7 +234,7 @@ export interface Transaction< /** * Extracts the string-literal index names defined by an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type IndexFromTable = IsStringLiteral< @@ -245,7 +245,7 @@ export type IndexFromTable
= IsString /** * Extracts the valid index names for a table name within an IndexedDB version. * - * @category models + * @category utility types * @since 4.0.0 */ export type IndexFromTableName< @@ -478,6 +478,12 @@ const layer = ( Effect.provideService(IndexedDbQueryBuilder.IndexedDbTransaction, transaction) ) fiber = runForkWith(effect) + fiber.addObserver((exit) => { + if (exit._tag === "Failure") { + transaction.abort() + resume(exit) + } + }) fiber.currentDispatcher.flush() } diff --git a/.context/effect/packages/platform-browser/src/IndexedDbQueryBuilder.ts b/.context/effect/packages/platform/browser/src/IndexedDbQueryBuilder.ts similarity index 96% rename from .context/effect/packages/platform-browser/src/IndexedDbQueryBuilder.ts rename to .context/effect/packages/platform/browser/src/IndexedDbQueryBuilder.ts index e5682d9a2..a294a6aaf 100644 --- a/.context/effect/packages/platform-browser/src/IndexedDbQueryBuilder.ts +++ b/.context/effect/packages/platform/browser/src/IndexedDbQueryBuilder.ts @@ -136,13 +136,17 @@ export interface IndexedDbQueryBuilder< readonly tables: Tables readonly mode: Mode readonly durability?: IDBTransactionDurability - }) => (effect: Effect.Effect) => Effect.Effect> + }) => (effect: Effect.Effect) => Effect.Effect< + A, + E | IndexedDbQueryError, + Exclude + > } /** * Valid key-path type for a table schema, using encoded fields whose values are IndexedDB-valid keys. * - * @category models + * @category utility types * @since 4.0.0 */ export type KeyPath = @@ -152,7 +156,7 @@ export type KeyPath = /** * Valid numeric key-path type for a table schema, used for auto-increment key paths. * - * @category models + * @category utility types * @since 4.0.0 */ export type KeyPathNumber = @@ -168,7 +172,7 @@ export declare namespace IndexedDbQuery { /** * Decoded row type returned by select queries, adding a `key` field when the table does not define a key path. * - * @category models + * @category utility types * @since 4.0.0 */ export type SelectType< @@ -181,7 +185,7 @@ export declare namespace IndexedDbQuery { /** * Input type for insert and upsert operations, adjusted for auto-increment keys and out-of-line keys. * - * @category models + * @category utility types * @since 4.0.0 */ export type ModifyType< @@ -215,7 +219,7 @@ export declare namespace IndexedDbQuery { /** * Value type accepted by `equals` comparisons for a table key path or index. * - * @category models + * @category utility types * @since 4.0.0 */ export type EqualsType< @@ -229,7 +233,7 @@ export declare namespace IndexedDbQuery { /** * Value type accepted by range comparisons for a table key path or index, including partial tuples for compound indexes. * - * @category models + * @category utility types * @since 4.0.0 */ export type ExtractIndexType< @@ -248,7 +252,7 @@ export declare namespace IndexedDbQuery { /** * Mutation input type for insert and upsert operations, including any required key fields. * - * @category models + * @category utility types * @since 4.0.0 */ export type ModifyWithKey
= ModifyType
@@ -719,7 +723,7 @@ export declare namespace IndexedDbQuery { /** * Service tag for the active `IDBTransaction` used to share a transaction across IndexedDB query effects. * - * @category models + * @category services * @since 4.0.0 */ export class IndexedDbTransaction extends Context.Service()( @@ -752,6 +756,7 @@ const applyDelete = (query: IndexedDbQuery.Delete) => durability: query.delete.from.table.durability }) const objectStore = transaction.objectStore(query.delete.from.table.tableName) + const store = query.delete.index === undefined ? objectStore : objectStore.index(query.delete.index) const predicate = query.predicate let keyRange: globalThis.IDBKeyRange | undefined = undefined @@ -782,8 +787,8 @@ const applyDelete = (query: IndexedDbQuery.Delete) => let request: globalThis.IDBRequest - if (query.limitValue !== undefined || predicate) { - const cursorRequest = objectStore.openCursor() + if (query.delete.index !== undefined || query.limitValue !== undefined || predicate) { + const cursorRequest = store.openCursor(keyRange) let count = 0 cursorRequest.onerror = () => { @@ -897,7 +902,7 @@ const applySelect = Effect.fnUntraced(function*( const keyPath = query.from.table.keyPath const predicate = query.predicate - const data = predicate || keyPath === undefined || query.offsetValue !== undefined ? + const data = predicate || keyPath === undefined || query.offsetValue !== undefined || query.reverseValue ? yield* Effect.callback, IndexedDbQueryError>((resume) => { const { keyRange, store } = getReadonlyObjectStore(query) @@ -1001,7 +1006,13 @@ const applyFirst = Effect.fnUntraced(function*( } request.onsuccess = () => { - resume(Effect.succeed(request.result)) + if (request.result === undefined) { + resume( + Effect.fail(new Cause.NoSuchElementError(`No such element in table ${query.select.from.table.tableName}`)) + ) + } else { + resume(Effect.succeed(request.result)) + } } } else { const request = store.openCursor() @@ -1331,7 +1342,6 @@ const FromProto: Omit< | "countCache" | "deleteCache" > = { - ...CommonProto, select>( this: IndexedDbQuery.From, index?: Index @@ -1392,7 +1402,8 @@ const FromProto: Omit< database: self.database.current, table: self.table }) - } + }, + ...CommonProto } const makeFrom = < @@ -1516,7 +1527,7 @@ const DeleteProto: Omit< filter(this: IndexedDbQuery.Delete, filter: (value: IndexedDbTable.Encoded) => boolean) { const prev = this.predicate return makeDelete({ - delete: this.delete, + ...this, predicate: prev ? (item) => prev(item) && filter(item) : filter }) }, @@ -1760,18 +1771,22 @@ const SelectProto: Omit< }) { const limit = this.limitValue const chunkSize = Math.min(options?.chunkSize ?? 100, limit ?? Number.MAX_SAFE_INTEGER) - const initial = this.limit(chunkSize) + const initialOffset = this.offsetValue ?? 0 return Stream.suspend(() => { let total = 0 + const initial = this.limit(chunkSize) return Stream.paginate(initial, (select) => Effect.map( applySelect(select as any), (data) => { total += data.length - ;(select as any).offsetValue = total const reachedLimit = limit && total >= limit const isPartial = data.length < chunkSize - return [data, isPartial || reachedLimit ? Option.none() : Option.some(select)] as const + const next = makeSelect({ + ...select, + offsetValue: initialOffset + total + }) + return [data, isPartial || reachedLimit ? Option.none() : Option.some(next)] as const } )) }) @@ -1951,7 +1966,6 @@ const QueryBuilderProto: Omit< | "IDBTransaction" | "reactivity" > = { - ...CommonProto, use(this: IndexedDbQueryBuilder, f: (database: globalThis.IDBDatabase) => any) { return Effect.try({ try: () => f(this.database.current), @@ -1979,6 +1993,7 @@ const QueryBuilderProto: Omit< const self = this as IndexedDbQueryBuilder return applyClearAll({ database: self.database.current }) }, + ...CommonProto, withTransaction(this: IndexedDbQueryBuilder, options: { readonly tables: NonEmptyReadonlyArray readonly mode: globalThis.IDBTransactionMode @@ -1987,7 +2002,11 @@ const QueryBuilderProto: Omit< return (effect) => Effect.suspend(() => { const transaction = this.database.current.transaction(options.tables, options.mode, options) - return Effect.provideService(effect, IndexedDbTransaction, transaction) + return Effect.provideService(effect, IndexedDbTransaction, transaction).pipe( + Effect.onExit((exit) => + exit._tag === "Success" ? awaitTransaction(transaction) : abortTransaction(transaction) + ) + ) }).pipe( // To prevent async gaps between transaction queries Effect.provideService(References.PreventSchedulerYield, true) @@ -1995,6 +2014,30 @@ const QueryBuilderProto: Omit< } } +const abortTransaction = (transaction: globalThis.IDBTransaction) => + Effect.try({ + try: () => transaction.abort(), + catch: () => undefined + }).pipe(Effect.ignore) + +const awaitTransaction = (transaction: globalThis.IDBTransaction) => + Effect.callback((resume) => { + transaction.oncomplete = () => { + resume(Effect.void) + } + transaction.onabort = () => { + resume( + Effect.fail( + new IndexedDbQueryError({ + reason: "TransactionError", + cause: transaction.error + }) + ) + ) + } + return abortTransaction(transaction) + }) + /** * Creates an `IndexedDbQueryBuilder` from an open database reference, key-range constructor, table map, and reactivity service. * diff --git a/.context/effect/packages/platform-browser/src/IndexedDbTable.ts b/.context/effect/packages/platform/browser/src/IndexedDbTable.ts similarity index 97% rename from .context/effect/packages/platform-browser/src/IndexedDbTable.ts rename to .context/effect/packages/platform/browser/src/IndexedDbTable.ts index 125d881b5..12be0e987 100644 --- a/.context/effect/packages/platform-browser/src/IndexedDbTable.ts +++ b/.context/effect/packages/platform/browser/src/IndexedDbTable.ts @@ -23,7 +23,7 @@ const TypeId = "~@effect/platform-browser/IndexedDbTable" /** * Typed IndexedDB table definition containing its name, schema, key path, indexes, auto-increment setting, and transaction durability. * - * @category interface + * @category models * @since 4.0.0 */ export interface IndexedDbTable< @@ -94,14 +94,14 @@ export type AnyWithProps = IndexedDbTable< /** * Extracts the table name type from an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type TableName
= Table["tableName"] /** * Extracts the key-path type from an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type KeyPath
= Table["keyPath"] @@ -109,7 +109,7 @@ export type KeyPath
= Table["keyPath"] /** * Extracts the auto-increment flag type from an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type AutoIncrement
= Table["autoIncrement"] @@ -117,14 +117,14 @@ export type AutoIncrement
= Table["autoIncrement"] /** * Extracts the schema type from an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type TableSchema
= Table["tableSchema"] /** * Extracts the decoding or encoding service requirements needed by an `IndexedDbTable` schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type Context
= @@ -134,7 +134,7 @@ export type Context
= /** * Extracts the encoded row type from an `IndexedDbTable` schema. * - * @category models + * @category utility types * @since 4.0.0 */ export type Encoded
= Table["tableSchema"]["Encoded"] @@ -142,7 +142,7 @@ export type Encoded
= Table["tableSchema"]["Encoded"] /** * Extracts the index definition map from an `IndexedDbTable`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Indexes
= Table["indexes"] @@ -150,7 +150,7 @@ export type Indexes
= Table["indexes"] /** * Selects the table with the given name from a union of `IndexedDbTable` types. * - * @category models + * @category utility types * @since 4.0.0 */ export type WithName
= Extract< diff --git a/.context/effect/packages/platform-browser/src/IndexedDbVersion.ts b/.context/effect/packages/platform/browser/src/IndexedDbVersion.ts similarity index 98% rename from .context/effect/packages/platform-browser/src/IndexedDbVersion.ts rename to .context/effect/packages/platform/browser/src/IndexedDbVersion.ts index 1e471d7f6..ebc3b2358 100644 --- a/.context/effect/packages/platform-browser/src/IndexedDbVersion.ts +++ b/.context/effect/packages/platform/browser/src/IndexedDbVersion.ts @@ -27,7 +27,7 @@ const TypeId = "~@effect/platform-browser/IndexedDbVersion" /** * Typed IndexedDB version definition containing the tables available in that schema version. * - * @category interface + * @category models * @since 4.0.0 */ export interface IndexedDbVersion< @@ -59,7 +59,7 @@ export type AnyWithProps = IndexedDbVersion /** * Extracts the table union from an `IndexedDbVersion`. * - * @category models + * @category utility types * @since 4.0.0 */ export type Tables = Db extends IndexedDbVersion ? _Tables : never @@ -67,7 +67,7 @@ export type Tables = Db extends IndexedDbVersion /** * Selects a table by name from an `IndexedDbVersion`. * - * @category models + * @category utility types * @since 4.0.0 */ export type TableWithName< @@ -78,7 +78,7 @@ export type TableWithName< /** * Extracts the schema for a named table within an `IndexedDbVersion`. * - * @category models + * @category utility types * @since 4.0.0 */ export type SchemaWithName< diff --git a/.context/effect/packages/platform-browser/src/Permissions.ts b/.context/effect/packages/platform/browser/src/Permissions.ts similarity index 99% rename from .context/effect/packages/platform-browser/src/Permissions.ts rename to .context/effect/packages/platform/browser/src/Permissions.ts index 20449a788..b58b7e46f 100644 --- a/.context/effect/packages/platform-browser/src/Permissions.ts +++ b/.context/effect/packages/platform/browser/src/Permissions.ts @@ -21,7 +21,7 @@ const ErrorTypeId = "~@effect/platform-browser/Permissions/PermissionsError" * Wrapper on the Permission API (`navigator.permissions`) with methods for * querying status of permissions. * - * @category models + * @category services * @since 4.0.0 */ export interface Permissions { diff --git a/.context/effect/packages/platform-browser/src/index.ts b/.context/effect/packages/platform/browser/src/index.ts similarity index 100% rename from .context/effect/packages/platform-browser/src/index.ts rename to .context/effect/packages/platform/browser/src/index.ts diff --git a/.context/effect/packages/platform/browser/test/BrowserCrypto.test.ts b/.context/effect/packages/platform/browser/test/BrowserCrypto.test.ts new file mode 100644 index 000000000..3c9b78d52 --- /dev/null +++ b/.context/effect/packages/platform/browser/test/BrowserCrypto.test.ts @@ -0,0 +1,96 @@ +import * as BrowserCrypto from "@effect/platform-browser/BrowserCrypto" +import { assert, describe, it } from "@effect/vitest" +import { Layer } from "effect" +import * as Crypto from "effect/Crypto" +import * as Effect from "effect/Effect" +import * as TestClock from "effect/testing/TestClock" + +const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ +const uuidV7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + +const getRandomValues = (array: T): T => { + if (array instanceof Uint8Array) { + for (let i = 0; i < array.length; i++) { + array[i] = i & 0xff + } + } + return array +} + +describe("BrowserCrypto", () => { + it.effect("generates random bytes at and above the getRandomValues limit", () => { + const chunks: Array = [] + + return Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + for (const size of [65_536, 65_537, 70_000]) { + const bytes = yield* crypto.randomBytes(size) + assert.strictEqual(bytes.length, size) + assert.strictEqual(bytes[0], 0) + assert.strictEqual(bytes[size - 1], (size - 1) & 0xff) + } + assert.deepStrictEqual(chunks, [65_536, 65_536, 1, 65_536, 4_464]) + }).pipe(Effect.provide(BrowserCrypto.layer.pipe( + Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { + ...crypto, + getRandomValues(array: T): T { + if (array !== null) { + assert.ok(array.byteLength <= 65_536) + chunks.push(array.byteLength) + } + return getRandomValues(array) + } + })) + ))) + }) + + it.effect("generates UUIDv4 values from getRandomValues", () => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + const uuid = yield* crypto.randomUUIDv4 + assert.strictEqual(uuid, "00010203-0405-4607-8809-0a0b0c0d0e0f") + assert.match(uuid, uuidV4Regex) + }).pipe(Effect.provide(BrowserCrypto.layer.pipe( + Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { + ...crypto, + getRandomValues + })) + )))) + + it.effect("generates UUIDv7 values from getRandomValues and the Clock", () => + Effect.gen(function*() { + yield* TestClock.setTime(0x0123456789ab) + const crypto = yield* Crypto.Crypto + const uuid = yield* crypto.randomUUIDv7 + assert.strictEqual(uuid, "01234567-89ab-7607-8809-0a0b0c0d0e0f") + assert.match(uuid, uuidV7Regex) + }).pipe(Effect.provide(BrowserCrypto.layer.pipe( + Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { + ...crypto, + getRandomValues + })) + )))) + + it.effect("computes digests with subtle crypto", () => { + const buffer = new ArrayBuffer(3) + new Uint8Array(buffer).set([1, 2, 3]) + + return Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + const digest = yield* crypto.digest("SHA-256", new Uint8Array(buffer)) + assert.deepStrictEqual(digest, new Uint8Array([1, 2, 3])) + }).pipe( + Effect.provide(BrowserCrypto.layer.pipe( + Layer.provide(Layer.succeed(BrowserCrypto.WebCrypto, { + ...crypto, + subtle: { + ...crypto.subtle, + digest() { + return Promise.resolve(buffer) + } + } + })) + )) + ) + }) +}) diff --git a/.context/effect/packages/platform-browser/test/BrowserHttpClient.test.ts b/.context/effect/packages/platform/browser/test/BrowserHttpClient.test.ts similarity index 86% rename from .context/effect/packages/platform-browser/test/BrowserHttpClient.test.ts rename to .context/effect/packages/platform/browser/test/BrowserHttpClient.test.ts index 74fb34991..4ae16f07b 100644 --- a/.context/effect/packages/platform-browser/test/BrowserHttpClient.test.ts +++ b/.context/effect/packages/platform/browser/test/BrowserHttpClient.test.ts @@ -88,4 +88,17 @@ describe("BrowserHttpClient", () => { body: "{ \"message\": \"Success!\" }" }] })))) + + it.effect("decodes an XHR response as FormData", () => + Effect.gen(function*() { + const formData = yield* HttpClient.get("http://localhost/form").pipe( + Effect.flatMap((response) => response.formData) + ) + assert.strictEqual(formData.get("value"), "test") + }).pipe(Effect.provide(layer({ + get: ["http://localhost/form", { + headers: { "content-type": "multipart/form-data; boundary=x" }, + body: "--x\r\nContent-Disposition: form-data; name=\"value\"\r\n\r\ntest\r\n--x--\r\n" + }] + })))) }) diff --git a/.context/effect/packages/platform/browser/test/BrowserKeyValueStore.test.ts b/.context/effect/packages/platform/browser/test/BrowserKeyValueStore.test.ts new file mode 100644 index 000000000..f4ca6281f --- /dev/null +++ b/.context/effect/packages/platform/browser/test/BrowserKeyValueStore.test.ts @@ -0,0 +1,91 @@ +import * as BrowserKeyValueStore from "@effect/platform-browser/BrowserKeyValueStore" +import * as IndexedDb from "@effect/platform-browser/IndexedDb" +import { assert, describe, it } from "@effect/vitest" +import { Layer } from "effect" +import { testLayer } from "effect-test/unstable/persistence/KeyValueStore.test" +import * as Effect from "effect/Effect" +import * as Result from "effect/Result" +import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" +import { IDBKeyRange, indexedDB } from "fake-indexeddb" + +describe("KeyValueStore / layerLocalStorage", () => testLayer(BrowserKeyValueStore.layerLocalStorage)) + +describe("KeyValueStore / layerSessionStorage", () => testLayer(BrowserKeyValueStore.layerSessionStorage)) + +describe("KeyValueStore / layerIndexedDb", () => { + const layerFakeIndexedDb = Layer.succeed( + IndexedDb.IndexedDb, + IndexedDb.make({ indexedDB, IDBKeyRange }) + ) + + testLayer( + BrowserKeyValueStore.layerIndexedDb({ database: "kvs_test_db" }).pipe( + Layer.provide(layerFakeIndexedDb) + ) + ) + + it.effect("does not report a write before its transaction commits", () => { + const db = { + objectStoreNames: { contains: () => true }, + close() {}, + transaction() { + const transaction = { + error: null as unknown, + onabort: null as null | (() => void), + objectStore() { + return { + put() { + const request = { + readyState: "pending", + result: undefined, + error: null, + onsuccess: null as null | (() => void), + onerror: null as null | (() => void) + } + queueMicrotask(() => { + request.readyState = "done" + request.onsuccess?.() + transaction.error = new DOMException("Commit failed", "AbortError") + transaction.onabort?.() + }) + return request + } + } + } + } + return transaction + } + } + const failingIndexedDb = { + open() { + const request = { + readyState: "pending", + result: undefined as unknown, + error: null, + onsuccess: null as null | (() => void), + onerror: null as null | (() => void), + onupgradeneeded: null as null | (() => void) + } + queueMicrotask(() => { + request.readyState = "done" + request.result = db + request.onsuccess?.() + }) + return request + } + } + const layer = BrowserKeyValueStore.layerIndexedDb({ database: "transaction_repro" }).pipe( + Layer.provide(Layer.succeed( + IndexedDb.IndexedDb, + IndexedDb.make({ indexedDB: failingIndexedDb as unknown as IDBFactory, IDBKeyRange }) + )) + ) + + return Effect.gen(function*() { + const store = yield* KeyValueStore.KeyValueStore + const result = yield* Effect.result(store.set("key", "value")) + yield* Effect.yieldNow + assert.isTrue(Result.isFailure(result), "the aborted transaction was reported as successful") + }).pipe(Effect.provide(layer)) + }) +}) diff --git a/.context/effect/packages/platform-browser/test/BrowserPersistence.test.ts b/.context/effect/packages/platform/browser/test/BrowserPersistence.test.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/BrowserPersistence.test.ts rename to .context/effect/packages/platform/browser/test/BrowserPersistence.test.ts diff --git a/.context/effect/packages/platform-browser/test/BrowserPersistencePersistedCache.test.ts b/.context/effect/packages/platform/browser/test/BrowserPersistencePersistedCache.test.ts similarity index 88% rename from .context/effect/packages/platform-browser/test/BrowserPersistencePersistedCache.test.ts rename to .context/effect/packages/platform/browser/test/BrowserPersistencePersistedCache.test.ts index 7306781a0..01324b67f 100644 --- a/.context/effect/packages/platform-browser/test/BrowserPersistencePersistedCache.test.ts +++ b/.context/effect/packages/platform/browser/test/BrowserPersistencePersistedCache.test.ts @@ -1,5 +1,5 @@ import * as BrowserPersistence from "@effect/platform-browser/BrowserPersistence" -import { afterEach, beforeEach, describe } from "@effect/vitest" +import { afterAll, beforeAll, describe } from "@effect/vitest" import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" import { indexedDB as fakeIndexedDb } from "fake-indexeddb" @@ -7,12 +7,12 @@ const database = "effect_persistence_integration" let previousIndexedDb: unknown -beforeEach(() => { +beforeAll(() => { previousIndexedDb = Reflect.get(globalThis, "indexedDB") Reflect.set(globalThis, "indexedDB", fakeIndexedDb) }) -afterEach(() => { +afterAll(() => { fakeIndexedDb.deleteDatabase(database) if (previousIndexedDb === undefined) { Reflect.deleteProperty(globalThis, "indexedDB") diff --git a/.context/effect/packages/platform/browser/test/BrowserWorkerRunner.test.ts b/.context/effect/packages/platform/browser/test/BrowserWorkerRunner.test.ts new file mode 100644 index 000000000..3707e7ec2 --- /dev/null +++ b/.context/effect/packages/platform/browser/test/BrowserWorkerRunner.test.ts @@ -0,0 +1,101 @@ +import * as BrowserWorkerRunner from "@effect/platform-browser/BrowserWorkerRunner" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Queue from "effect/Queue" + +type Listener = EventListenerOrEventListenerObject + +const makePort = () => { + const listeners = new Map>() + const added: Array = [] + const removed: Array = [] + const port = { + addEventListener(type: string, listener: Listener | null) { + if (listener === null) return + added.push([type, listener]) + const eventListeners = listeners.get(type) ?? new Set() + eventListeners.add(listener) + listeners.set(type, eventListeners) + }, + removeEventListener(type: string, listener: Listener | null) { + if (listener === null) return + removed.push([type, listener]) + listeners.get(type)?.delete(listener) + }, + postMessage() {}, + start() {}, + close() {} + } as unknown as MessagePort + + return { + added, + removed, + port, + emit(type: string, data: unknown) { + const event = { data } as MessageEvent + for (const listener of listeners.get(type) ?? []) { + if (typeof listener === "function") { + listener(event) + } else { + listener.handleEvent(event) + } + } + } + } +} + +const makeSharedWorker = () => { + const worker = { + onconnect: null as ((event: MessageEvent) => void) | null, + addEventListener() {}, + removeEventListener() {}, + close() {} + } + return { + worker: worker as unknown as Window, + connect(port: MessagePort) { + worker.onconnect?.({ ports: [port] } as unknown as MessageEvent) + } + } +} + +describe("BrowserWorkerRunner", () => { + it.effect("removes the registered messageerror listener", () => + Effect.gen(function*() { + const fake = makePort() + const runner = yield* BrowserWorkerRunner.make(fake.port).start() + const fiber = yield* Effect.forkChild(runner.run(() => {})) + yield* Effect.yieldNow + + fake.emit("message", [1]) + yield* Fiber.join(fiber) + + const added = fake.added.find(([type]) => type === "messageerror") + const removed = fake.removed.find(([type]) => type === "messageerror") + assert.isDefined(added) + assert.isDefined(removed) + assert.strictEqual(removed[1], added[1]) + })) + + it.effect("emits disconnects for closed SharedWorker ports", () => + Effect.gen(function*() { + const sharedWorker = makeSharedWorker() + const first = makePort() + const second = makePort() + const runner = yield* BrowserWorkerRunner.make(sharedWorker.worker).start() + const fiber = yield* Effect.forkChild(runner.run(() => {})) + yield* Effect.yieldNow + + sharedWorker.connect(first.port) + sharedWorker.connect(second.port) + first.emit("message", [1]) + + const disconnects = runner.disconnects + assert.isDefined(disconnects) + assert.strictEqual(yield* Queue.take(disconnects), 0) + + second.emit("message", [1]) + yield* Fiber.join(fiber) + })) +}) diff --git a/.context/effect/packages/platform-browser/test/IndexedDbDatabase.test.ts b/.context/effect/packages/platform/browser/test/IndexedDbDatabase.test.ts similarity index 85% rename from .context/effect/packages/platform-browser/test/IndexedDbDatabase.test.ts rename to .context/effect/packages/platform/browser/test/IndexedDbDatabase.test.ts index 25d85ca47..2b6464d69 100644 --- a/.context/effect/packages/platform-browser/test/IndexedDbDatabase.test.ts +++ b/.context/effect/packages/platform/browser/test/IndexedDbDatabase.test.ts @@ -1,6 +1,6 @@ import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from "@effect/platform-browser" import { afterEach, assert, describe, it } from "@effect/vitest" -import { Effect, Layer, Schema } from "effect" +import { Effect, Layer, Result, Schema } from "effect" import { IDBKeyRange, indexedDB } from "fake-indexeddb" const databaseName = "db" @@ -250,4 +250,36 @@ describe.sequential("IndexedDbDatabase", () => { assert.deepStrictEqual(Array.from(objectStoreNames), ["user"]) }).pipe(provideMigration(Migration)) }) + + it.effect("aborts the versionchange transaction when a migration fails", () => { + const Table = IndexedDbTable.make({ + name: "entries", + schema: Schema.Struct({ id: Schema.Number }), + keyPath: "id" + }) + const Version = IndexedDbVersion.make(Table) + class Database extends IndexedDbDatabase.make(Version, (transaction) => + Effect.andThen( + transaction.createObjectStore("entries"), + Effect.fail("migration failed") + )) + {} + const open = Effect.callback((resume) => { + const request = indexedDB.open(databaseName) + request.onerror = () => resume(Effect.fail(request.error ?? new Error("IndexedDB open request failed"))) + request.onblocked = () => resume(Effect.fail(new Error("IndexedDB open request blocked"))) + request.onsuccess = () => resume(Effect.succeed(request.result)) + }) + + return Effect.gen(function*() { + const migration = yield* Effect.result( + Effect.service(IndexedDbDatabase.IndexedDbDatabase).pipe(provideMigration(Database)) + ) + assert.isTrue(Result.isFailure(migration)) + + const database = yield* open + assert.deepStrictEqual(Array.from(database.objectStoreNames), []) + database.close() + }) + }) }) diff --git a/.context/effect/packages/platform-browser/test/IndexedDbQueryBuilder.test.ts b/.context/effect/packages/platform/browser/test/IndexedDbQueryBuilder.test.ts similarity index 93% rename from .context/effect/packages/platform-browser/test/IndexedDbQueryBuilder.test.ts rename to .context/effect/packages/platform/browser/test/IndexedDbQueryBuilder.test.ts index 45f8aea79..9760eeb54 100644 --- a/.context/effect/packages/platform-browser/test/IndexedDbQueryBuilder.test.ts +++ b/.context/effect/packages/platform/browser/test/IndexedDbQueryBuilder.test.ts @@ -2,12 +2,12 @@ import { IndexedDb, IndexedDbDatabase, IndexedDbTable, IndexedDbVersion } from " import { afterEach, assert, describe, it } from "@effect/vitest" import { Array, + Cause, Context, DateTime, Effect, Fiber, Layer, - Option, Schema, SchemaGetter, SchemaIssue, @@ -68,7 +68,7 @@ const VerifyId = Schema.String.pipe( const { maxLength } = yield* VerifyContext if (s.length > maxLength) { return yield* Effect.fail( - new SchemaIssue.InvalidValue(Option.some(s), { + new SchemaIssue.InvalidValue({ message: "Max length exceeded" }) ) @@ -1456,4 +1456,95 @@ describe.sequential("IndexedDbQueryBuilder", () => { assert.deepStrictEqual(data2, { id: 3, title: "test2", count: 3, completed: false }) }).pipe(provideDb(Db)) }) + + it.effect("rolls back withTransaction writes when the effect fails", () => { + class Db extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")) {} + + return Effect.gen(function*() { + const api = yield* Db + yield* Effect.result( + api.withTransaction({ tables: ["todo"], mode: "readwrite" })( + Effect.andThen( + api.from("todo").insert({ id: 1, title: "committed", count: 1, completed: false }), + Effect.fail("rollback") + ) + ) + ) + + assert.deepStrictEqual(yield* api.from("todo").select(), []) + }).pipe(provideDb(Db)) + }) + + it.effect("applies reverse before a select limit", () => { + class Db extends IndexedDbDatabase.make( + V1, + Effect.fn(function*(api) { + yield* api.createObjectStore("todo") + yield* api.from("todo").insertAll([ + { id: 1, title: "one", count: 1, completed: false }, + { id: 2, title: "two", count: 2, completed: false }, + { id: 3, title: "three", count: 3, completed: false } + ]) + }) + ) {} + + return Effect.gen(function*() { + const api = yield* Db + const rows = yield* api.from("todo").select().reverse().limit(2) + assert.deepStrictEqual(rows.map((row) => row.id), [3, 2]) + }).pipe(provideDb(Db)) + }) + + it.effect("honors an indexed range when delete has a limit", () => { + class Db extends IndexedDbDatabase.make( + V1, + Effect.fn(function*(api) { + yield* api.createObjectStore("todo") + yield* api.createIndex("todo", "titleIndex") + yield* api.from("todo").insertAll([ + { id: 1, title: "keep", count: 1, completed: false }, + { id: 2, title: "delete", count: 2, completed: false } + ]) + }) + ) {} + + return Effect.gen(function*() { + const api = yield* Db + yield* api.from("todo").delete("titleIndex").equals("delete").limit(1) + const rows = yield* api.from("todo").select() + assert.deepStrictEqual(rows.map((row) => row.id), [1]) + }).pipe(provideDb(Db)) + }) + + it.effect("can consume the same paged select stream twice", () => { + class Db extends IndexedDbDatabase.make( + V1, + Effect.fn(function*(api) { + yield* api.createObjectStore("todo") + yield* api.from("todo").insertAll([ + { id: 1, title: "one", count: 1, completed: false }, + { id: 2, title: "two", count: 2, completed: false }, + { id: 3, title: "three", count: 3, completed: false } + ]) + }) + ) {} + + return Effect.gen(function*() { + const api = yield* Db + const stream = api.from("todo").select().stream({ chunkSize: 2 }) + const first = yield* Stream.runCollect(stream) + const second = yield* Stream.runCollect(stream) + assert.deepStrictEqual(second.map((row) => row.id), first.map((row) => row.id)) + }).pipe(provideDb(Db)) + }) + + it.effect("reports NoSuchElementError for an empty ranged first query", () => { + class Db extends IndexedDbDatabase.make(V1, (api) => api.createObjectStore("todo")) {} + + return Effect.gen(function*() { + const api = yield* Db + const error = yield* Effect.flip(api.from("todo").select().equals(1).first()) + assert.instanceOf(error, Cause.NoSuchElementError) + }).pipe(provideDb(Db)) + }) }) diff --git a/.context/effect/packages/platform-browser/test/IndexedDbTable.test.ts b/.context/effect/packages/platform/browser/test/IndexedDbTable.test.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/IndexedDbTable.test.ts rename to .context/effect/packages/platform/browser/test/IndexedDbTable.test.ts diff --git a/.context/effect/packages/platform-browser/test/IndexedDbVersion.test.ts b/.context/effect/packages/platform/browser/test/IndexedDbVersion.test.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/IndexedDbVersion.test.ts rename to .context/effect/packages/platform/browser/test/IndexedDbVersion.test.ts diff --git a/.context/effect/packages/platform-browser/test/Permissions.test.ts b/.context/effect/packages/platform/browser/test/Permissions.test.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/Permissions.test.ts rename to .context/effect/packages/platform/browser/test/Permissions.test.ts diff --git a/.context/effect/packages/platform-browser/test/RpcWorker.test.ts b/.context/effect/packages/platform/browser/test/RpcWorker.test.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/RpcWorker.test.ts rename to .context/effect/packages/platform/browser/test/RpcWorker.test.ts diff --git a/.context/effect/packages/platform-browser/test/fixtures/rpc-e2e.ts b/.context/effect/packages/platform/browser/test/fixtures/rpc-e2e.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/fixtures/rpc-e2e.ts rename to .context/effect/packages/platform/browser/test/fixtures/rpc-e2e.ts diff --git a/.context/effect/packages/platform/browser/test/fixtures/rpc-schemas.ts b/.context/effect/packages/platform/browser/test/fixtures/rpc-schemas.ts new file mode 100644 index 000000000..9f69d2015 --- /dev/null +++ b/.context/effect/packages/platform/browser/test/fixtures/rpc-schemas.ts @@ -0,0 +1,157 @@ +import { Context, Effect, Layer, Metric, Option, Queue, Schema } from "effect" +import { Headers } from "effect/unstable/http" +import * as Rpc from "effect/unstable/rpc/Rpc" +import * as RpcGroup from "effect/unstable/rpc/RpcGroup" +import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware" +import * as RpcServer from "effect/unstable/rpc/RpcServer" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +class StreamUsers extends Rpc.make("StreamUsers", { + success: User, + payload: { + id: Schema.String + }, + stream: true +}) {} + +class CurrentUser extends Context.Service()("CurrentUser") {} + +class Unauthorized extends Schema.Error("Unauthorized")({ + _tag: Schema.tag("Unauthorized") +}) {} + +class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { + error: Unauthorized, + requiredForClient: true +}) {} + +class TimingMiddleware extends RpcMiddleware.Service()("TimingMiddleware") {} + +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 } + }), + StreamUsers, + Rpc.make("GetInterrupts", { + success: Schema.Number + }), + Rpc.make("GetEmits", { + success: Schema.Number + }), + Rpc.make("ProduceDefect"), + 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) + +export const AuthLive = Layer.succeed(AuthMiddleware)( + AuthMiddleware.of((effect, options) => + Effect.provideService( + effect, + CurrentUser, + 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") +export const TimingLive = Layer.succeed(TimingMiddleware)( + TimingMiddleware.of((effect) => + effect.pipe( + Effect.tap(Metric.update(rpcSuccesses, 1)), + Effect.tapDefect(() => Metric.update(rpcDefects, 1)), + Effect.ensuring(Metric.update(rpcCount, 1)) + ) + ) +) + +export 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* Queue.bounded(0) + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + interrupts++ + }) + ) + + yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe( + Effect.tap(() => + Effect.sync(() => { + emits++ + }) + ), + Effect.delay(100), + Effect.forever, + Effect.forkScoped + ) + + return mailbox + }), + GetInterrupts: () => Effect.sync(() => interrupts), + GetEmits: () => Effect.sync(() => emits), + ProduceDefect: () => Effect.die("boom"), + 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, { + disableFatalDefects: true +}).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + +export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) => + next({ + ...request, + headers: Headers.set(request.headers, "name", "Logged in user") + })) diff --git a/.context/effect/packages/platform-browser/test/fixtures/rpc-worker.ts b/.context/effect/packages/platform/browser/test/fixtures/rpc-worker.ts similarity index 100% rename from .context/effect/packages/platform-browser/test/fixtures/rpc-worker.ts rename to .context/effect/packages/platform/browser/test/fixtures/rpc-worker.ts diff --git a/.context/effect/packages/platform/browser/tsconfig.json b/.context/effect/packages/platform/browser/tsconfig.json new file mode 100644 index 000000000..e2a8ca19a --- /dev/null +++ b/.context/effect/packages/platform/browser/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ] +} diff --git a/.context/effect/packages/platform-browser/vitest.setup.ts b/.context/effect/packages/platform/browser/vitest.setup.ts similarity index 100% rename from .context/effect/packages/platform-browser/vitest.setup.ts rename to .context/effect/packages/platform/browser/vitest.setup.ts diff --git a/.context/effect/packages/platform/bun/CHANGELOG.md b/.context/effect/packages/platform/bun/CHANGELOG.md new file mode 100644 index 000000000..a14c48acc --- /dev/null +++ b/.context/effect/packages/platform/bun/CHANGELOG.md @@ -0,0 +1,936 @@ +# @effect/platform-bun + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node-shared@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9), [`e74c302`](https://github.com/Effect-TS/effect/commit/e74c302afe0368e5d3f15d18c10fc54cf33f9003)]: + - effect@4.0.0-beta.107 + - @effect/platform-node-shared@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7113](https://github.com/Effect-TS/effect/pull/7113) [`b9875ea`](https://github.com/Effect-TS/effect/commit/b9875ea4538087227149db56e6e7e3a05d72b94a) Thanks @fubhy! - Fix Bun HTTP server handler restoration and defer shutdown while serve scopes remain active. + +- [#7122](https://github.com/Effect-TS/effect/pull/7122) [`c1a13f3`](https://github.com/Effect-TS/effect/commit/c1a13f3157463b2bdadca41cefb505bd04733636) Thanks @fubhy! - Honor offset and byte-count options in Bun Web File responses. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`5f1775c`](https://github.com/Effect-TS/effect/commit/5f1775cb060cb3dbf96adb067fd97967da7eca2f), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`089313e`](https://github.com/Effect-TS/effect/commit/089313ec2a4c307393c7c5e00c725ec23840c9c1), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node-shared@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node-shared@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`e2ec131`](https://github.com/Effect-TS/effect/commit/e2ec1311bed9bb8709c26396e71b15b4241a9185), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`e508589`](https://github.com/Effect-TS/effect/commit/e50858905fed68f29ec202ecdc9c902e44bfedd8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node-shared@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6723](https://github.com/Effect-TS/effect/pull/6723) [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e) Thanks @tim-smart! - add platform literal to HttpPlatform + +- [#6802](https://github.com/Effect-TS/effect/pull/6802) [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92) Thanks @tim-smart! - Allow configuring cluster RPC serialization buffer limits. + +- [#6726](https://github.com/Effect-TS/effect/pull/6726) [`955eb69`](https://github.com/Effect-TS/effect/commit/955eb69b795734b13965e0cb83f6396441b4e413) Thanks @tim-smart! - Construct an empty multipart stream for each bodiless Bun request. + +- [#6898](https://github.com/Effect-TS/effect/pull/6898) [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b) Thanks @tim-smart! - Add HTTP response compression support. Node.js, Bun, and Deno use asynchronous + `node:zlib` one-shot compression for byte-array bodies, preserving an exact + `Content-Length`; stream and raw bodies remain streaming transforms. + +- [#6680](https://github.com/Effect-TS/effect/pull/6680) [`23e176a`](https://github.com/Effect-TS/effect/commit/23e176a4f05ed3e81cc13a5d70111099692ea9a5) Thanks @tim-smart! - Fix worker runner disconnect notifications and event listener cleanup. + +- [#6691](https://github.com/Effect-TS/effect/pull/6691) [`6a5e86f`](https://github.com/Effect-TS/effect/commit/6a5e86f896c573c391e0fc7888f13e9ae0f07531) Thanks @t3dotgg! - Allow configuring the WebSocket server in `NodeHttpServer` and `BunHttpServer`. + + Both servers now accept a `websocket` option that is forwarded to the underlying implementation, with the wiring/lifecycle options the server manages excluded from the type: + + ```ts + // Node: forwarded to the `ws` WebSocketServer + NodeHttpServer.layer(() => createServer(), { + port: 3000, + websocket: { perMessageDeflate: true }, + }); + + // Bun: merged into Bun.serve's websocket handler + BunHttpServer.layer({ + port: 3000, + websocket: { perMessageDeflate: true }, + }); + ``` + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node-shared@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6618](https://github.com/Effect-TS/effect/pull/6618) [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e) Thanks @tim-smart! - unstable/cluster: hash over-length SQL message deduplication keys to prevent `message_id` overflow, closes [#6317](https://github.com/Effect-TS/effect/issues/6317). + + The composed request deduplication key (`entityType/entityId/tag/primaryKey`) can legally exceed the 255-character `message_id` column — the address columns alone allow 458 characters before the RPC primary key is appended. `SqlMessageStorage` now stores a SHA-256 digest (64 hex characters) of the composed key in the unique `message_id` column when the key exceeds 255 characters, so keys of any length work on PostgreSQL, MySQL, MSSQL, and SQLite. Keys that fit are stored as plaintext, byte-compatible with rows written by previous versions, so existing deployments keep deduplicating with no migration or schema change. + + `SqlMessageStorage.layer`/`layerWith` (and consequently `SingleRunner.layer`) now require `Crypto.Crypto`. The Node and Bun cluster convenience layers provide the platform Crypto implementation internally, so their requirements are unchanged. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + - @effect/platform-node-shared@4.0.0-beta.102 + +## 4.0.0-beta.101 + +### Patch Changes + +- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: + - effect@4.0.0-beta.101 + - @effect/platform-node-shared@4.0.0-beta.101 + +## 4.0.0-beta.100 + +### Patch Changes + +- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: + - effect@4.0.0-beta.100 + - @effect/platform-node-shared@4.0.0-beta.100 + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + - @effect/platform-node-shared@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + - @effect/platform-node-shared@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + - @effect/platform-node-shared@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + - @effect/platform-node-shared@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + - @effect/platform-node-shared@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- [#2537](https://github.com/Effect-TS/effect-smol/pull/2537) [`6d2c614`](https://github.com/Effect-TS/effect-smol/commit/6d2c614fab3da932afbb52e849c2663cf32d7d57) Thanks @tim-smart! - optimize bun stream reading + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + - @effect/platform-node-shared@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node-shared@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node-shared@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node-shared@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node-shared@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node-shared@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node-shared@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node-shared@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node-shared@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + - @effect/platform-node-shared@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + - @effect/platform-node-shared@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + - @effect/platform-node-shared@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + - @effect/platform-node-shared@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + - @effect/platform-node-shared@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + - @effect/platform-node-shared@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + - @effect/platform-node-shared@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + - @effect/platform-node-shared@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + - @effect/platform-node-shared@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + - @effect/platform-node-shared@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + - @effect/platform-node-shared@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + - @effect/platform-node-shared@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + - @effect/platform-node-shared@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + - @effect/platform-node-shared@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + - @effect/platform-node-shared@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + - @effect/platform-node-shared@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + - @effect/platform-node-shared@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + - @effect/platform-node-shared@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + - @effect/platform-node-shared@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + - @effect/platform-node-shared@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + - @effect/platform-node-shared@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + - @effect/platform-node-shared@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + - @effect/platform-node-shared@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + - @effect/platform-node-shared@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + - @effect/platform-node-shared@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + - @effect/platform-node-shared@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + - @effect/platform-node-shared@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + - @effect/platform-node-shared@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + - @effect/platform-node-shared@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + - @effect/platform-node-shared@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + - @effect/platform-node-shared@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + - @effect/platform-node-shared@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + - @effect/platform-node-shared@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + - @effect/platform-node-shared@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + - @effect/platform-node-shared@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + - @effect/platform-node-shared@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + - @effect/platform-node-shared@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + - @effect/platform-node-shared@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + - @effect/platform-node-shared@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505), [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - @effect/platform-node-shared@4.0.0-beta.46 + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + - @effect/platform-node-shared@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + - @effect/platform-node-shared@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + - @effect/platform-node-shared@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + - @effect/platform-node-shared@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + - @effect/platform-node-shared@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + - @effect/platform-node-shared@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + - @effect/platform-node-shared@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + - @effect/platform-node-shared@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + - @effect/platform-node-shared@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + - @effect/platform-node-shared@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + - @effect/platform-node-shared@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + - @effect/platform-node-shared@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + - @effect/platform-node-shared@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + - @effect/platform-node-shared@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. + + Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + - @effect/platform-node-shared@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + - @effect/platform-node-shared@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + - @effect/platform-node-shared@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + - @effect/platform-node-shared@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + - @effect/platform-node-shared@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + - @effect/platform-node-shared@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + - @effect/platform-node-shared@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + - @effect/platform-node-shared@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + - @effect/platform-node-shared@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + - @effect/platform-node-shared@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + - @effect/platform-node-shared@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + - @effect/platform-node-shared@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies [[`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f)]: + - @effect/platform-node-shared@4.0.0-beta.19 + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + - @effect/platform-node-shared@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + - @effect/platform-node-shared@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + - @effect/platform-node-shared@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + - @effect/platform-node-shared@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + - @effect/platform-node-shared@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + - @effect/platform-node-shared@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + - @effect/platform-node-shared@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + - @effect/platform-node-shared@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + - @effect/platform-node-shared@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + - @effect/platform-node-shared@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + - @effect/platform-node-shared@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + - @effect/platform-node-shared@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + - @effect/platform-node-shared@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + - @effect/platform-node-shared@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + - @effect/platform-node-shared@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + - @effect/platform-node-shared@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + - @effect/platform-node-shared@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + - @effect/platform-node-shared@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - @effect/platform-node-shared@4.0.0-beta.0 + - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-bun/LICENSE b/.context/effect/packages/platform/bun/LICENSE similarity index 100% rename from .context/effect/packages/platform-bun/LICENSE rename to .context/effect/packages/platform/bun/LICENSE diff --git a/.context/effect/packages/platform/bun/README.md b/.context/effect/packages/platform/bun/README.md new file mode 100644 index 000000000..3be54cdbd --- /dev/null +++ b/.context/effect/packages/platform/bun/README.md @@ -0,0 +1,14 @@ +# @effect/platform-bun + +[Bun](https://bun.sh) implementations of the Effect platform services, including the file system, HTTP client and server, sockets, workers, and terminal. + +## Installation + +```sh +npm install effect@beta @effect/platform-bun@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/platform-bun) diff --git a/.context/effect/packages/platform/bun/package.json b/.context/effect/packages/platform/bun/package.json new file mode 100644 index 000000000..97d3bd716 --- /dev/null +++ b/.context/effect/packages/platform/bun/package.json @@ -0,0 +1,75 @@ +{ + "name": "@effect/platform-bun", + "type": "module", + "version": "4.0.0-rc.108", + "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" + ], + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./index": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./index": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "dependencies": { + "@effect/platform-node-shared": "workspace:^" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "effect": "workspace:^" + } +} diff --git a/.context/effect/packages/platform-bun/src/BunChildProcessSpawner.ts b/.context/effect/packages/platform/bun/src/BunChildProcessSpawner.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunChildProcessSpawner.ts rename to .context/effect/packages/platform/bun/src/BunChildProcessSpawner.ts diff --git a/.context/effect/packages/platform-bun/src/BunClusterHttp.ts b/.context/effect/packages/platform/bun/src/BunClusterHttp.ts similarity index 93% rename from .context/effect/packages/platform-bun/src/BunClusterHttp.ts rename to .context/effect/packages/platform/bun/src/BunClusterHttp.ts index ca727c8e8..e89e0a136 100644 --- a/.context/effect/packages/platform-bun/src/BunClusterHttp.ts +++ b/.context/effect/packages/platform/bun/src/BunClusterHttp.ts @@ -29,6 +29,7 @@ import type { ServeError } from "effect/unstable/http/HttpServerError" import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" import type { SqlClient } from "effect/unstable/sql/SqlClient" import { layerK8sHttpClient } from "./BunClusterSocket.ts" +import * as BunCrypto from "./BunCrypto.ts" import * as BunFileSystem from "./BunFileSystem.ts" import * as BunHttpServer from "./BunHttpServer.ts" import type { BunServices } from "./BunServices.ts" @@ -102,6 +103,7 @@ export const layer = < >(options: { readonly transport: "http" | "websocket" readonly serialization?: "msgpack" | "ndjson" | undefined + readonly serializationMaxBufferSize?: number | "unbounded" | undefined readonly clientOnly?: ClientOnly | undefined readonly storage?: Storage | undefined readonly runnerHealth?: "ping" | "k8s" | undefined @@ -157,7 +159,7 @@ export const layer = < ? MessageStorage.layerNoop : options?.storage === "byo" ? Layer.empty - : Layer.orDie(SqlMessageStorage.layer) + : Layer.orDie(SqlMessageStorage.layer).pipe(Layer.provide(BunCrypto.layer)) ), Layer.provide( options?.storage === "local" @@ -168,7 +170,9 @@ export const layer = < ), Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), Layer.provide( - options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + options?.serialization === "ndjson" + ? RpcSerialization.layerNdjsonWith({ maxBufferSize: options.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options.serializationMaxBufferSize }) ) ) as any } diff --git a/.context/effect/packages/platform-bun/src/BunClusterSocket.ts b/.context/effect/packages/platform/bun/src/BunClusterSocket.ts similarity index 92% rename from .context/effect/packages/platform-bun/src/BunClusterSocket.ts rename to .context/effect/packages/platform/bun/src/BunClusterSocket.ts index 22f3bfa30..44a562790 100644 --- a/.context/effect/packages/platform-bun/src/BunClusterSocket.ts +++ b/.context/effect/packages/platform/bun/src/BunClusterSocket.ts @@ -28,6 +28,7 @@ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" import type * as SocketServer from "effect/unstable/socket/SocketServer" import type { SqlClient } from "effect/unstable/sql/SqlClient" +import * as BunCrypto from "./BunCrypto.ts" import * as BunFileSystem from "./BunFileSystem.ts" export { @@ -61,6 +62,7 @@ export const layer = < >( options?: { readonly serialization?: "msgpack" | "ndjson" | undefined + readonly serializationMaxBufferSize?: number | "unbounded" | undefined readonly clientOnly?: ClientOnly | undefined readonly storage?: Storage | undefined readonly runnerHealth?: "ping" | "k8s" | undefined @@ -109,7 +111,7 @@ export const layer = < ? MessageStorage.layerNoop : options?.storage === "byo" ? Layer.empty - : Layer.orDie(SqlMessageStorage.layer) + : Layer.orDie(SqlMessageStorage.layer).pipe(Layer.provide(BunCrypto.layer)) ), Layer.provide( options?.storage === "local" @@ -120,7 +122,9 @@ export const layer = < ), Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), Layer.provide( - options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + options?.serialization === "ndjson" + ? RpcSerialization.layerNdjsonWith({ maxBufferSize: options?.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options?.serializationMaxBufferSize }) ) ) as any } diff --git a/.context/effect/packages/platform-bun/src/BunCrypto.ts b/.context/effect/packages/platform/bun/src/BunCrypto.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunCrypto.ts rename to .context/effect/packages/platform/bun/src/BunCrypto.ts diff --git a/.context/effect/packages/platform-bun/src/BunFileSystem.ts b/.context/effect/packages/platform/bun/src/BunFileSystem.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunFileSystem.ts rename to .context/effect/packages/platform/bun/src/BunFileSystem.ts diff --git a/.context/effect/packages/platform-bun/src/BunHttpClient.ts b/.context/effect/packages/platform/bun/src/BunHttpClient.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunHttpClient.ts rename to .context/effect/packages/platform/bun/src/BunHttpClient.ts diff --git a/.context/effect/packages/platform/bun/src/BunHttpPlatform.ts b/.context/effect/packages/platform/bun/src/BunHttpPlatform.ts new file mode 100644 index 000000000..905d2c407 --- /dev/null +++ b/.context/effect/packages/platform/bun/src/BunHttpPlatform.ts @@ -0,0 +1,65 @@ +/** + * Bun implementation of the Effect HTTP platform service. + * + * This module provides one `layer` for `HttpPlatform`. It implements file + * responses with `Bun.file`, supports sliced file responses for byte ranges, + * and returns Web `File` values as raw HTTP server responses. The layer also + * provides the Bun file-system layer and ETag generator required by + * `HttpPlatform`. + * + * @since 4.0.0 + */ +import * as NodeHttpCompression from "@effect/platform-node-shared/NodeHttpCompression" +import type * as Effect from "effect/Effect" +import type { FileSystem } from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Etag from "effect/unstable/http/Etag" +import * as Platform from "effect/unstable/http/HttpPlatform" +import * as Response from "effect/unstable/http/HttpServerResponse" +import * as BunFileSystem from "./BunFileSystem.ts" + +// Bun's CompressionStream supports an extended format set covering brotli and +// zstd +const compression = NodeHttpCompression.make(Platform.makeCompressionWeb({ + algorithms: ["gzip", "deflate", "br", "zstd"], + transform: (algorithm) => Platform.compressionTransformWeb(algorithm === "br" ? "brotli" : algorithm) +})) + +/** + * @category constructors + * @since 4.0.0 + */ +const make: Effect.Effect< + Platform.HttpPlatform["Service"], + never, + FileSystem | Etag.Generator +> = Platform.make({ + platform: "bun", + compression, + fileResponse(path, status, statusText, headers, start, end, _contentLength) { + let file = Bun.file(path) + if (start > 0 || end !== undefined) { + file = file.slice(start, end) + } + return Response.raw(file, { headers, status, statusText }) + }, + fileWebResponse(file, status, statusText, headers, options) { + const start = Number(options?.offset ?? 0) + const end = options?.bytesToRead !== undefined ? start + Number(options.bytesToRead) : undefined + const body = start > 0 || end !== undefined + ? (file as File).slice(start, end, file.type) + : file + return Response.raw(body, { headers, status, statusText }) + } +}) + +/** + * Layer that provides the Bun `HttpPlatform`, including file responses backed by `Bun.file`. + * + * @category layers + * @since 4.0.0 + */ +export const layer = Layer.effect(Platform.HttpPlatform)(make).pipe( + Layer.provide(BunFileSystem.layer), + Layer.provide(Etag.layer) +) diff --git a/.context/effect/packages/platform-bun/src/BunHttpServer.ts b/.context/effect/packages/platform/bun/src/BunHttpServer.ts similarity index 93% rename from .context/effect/packages/platform-bun/src/BunHttpServer.ts rename to .context/effect/packages/platform/bun/src/BunHttpServer.ts index 9f2ac3dfc..4ccfcf16b 100644 --- a/.context/effect/packages/platform-bun/src/BunHttpServer.ts +++ b/.context/effect/packages/platform/bun/src/BunHttpServer.ts @@ -66,6 +66,25 @@ export type ServeOptions = ) & { readonly routes?: Bun.Serve.Routes } +/** + * WebSocket tuning options forwarded to `Bun.serve`'s `websocket` handler. + * + * **Details** + * + * The lifecycle handlers (`open`, `message`, `close`, ...) are managed by the + * server and cannot be overridden; everything else — such as + * `perMessageDeflate` compression, payload limits, and idle timeouts — passes + * through, e.g. + * `BunHttpServer.layer({ port: 3000, websocket: { perMessageDeflate: true } })`. + * + * @category options + * @since 4.0.0 + */ +export type WebSocketOptions = Omit< + Bun.WebSocketHandler, + "open" | "message" | "close" | "drain" | "ping" | "pong" | "data" | "binaryType" +> + /** * Creates a scoped Bun `HttpServer` from `Bun.serve` options, stopping the server on scope finalization with optional graceful shutdown settings. * @@ -77,6 +96,7 @@ export const make = Effect.fnUntraced( options: ServeOptions & { readonly disablePreemptiveShutdown?: boolean | undefined readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: WebSocketOptions | undefined } ) { const scope = yield* Effect.scope @@ -90,6 +110,7 @@ export const make = Effect.fnUntraced( ...options as ServeOptions, fetch: handlerStack[0], websocket: { + ...options.websocket, open(ws) { Deferred.doneUnsafe(ws.data.deferred, Exit.succeed(ws)) }, @@ -137,12 +158,12 @@ export const make = Effect.fnUntraced( function handler(request: Request, server: BunServer) { return new Promise((resolve, _reject) => { - const map = new Map(services.mapUnsafe) - map.set( - ServerRequest.HttpServerRequest.key, + const context = Context.add( + services, + ServerRequest.HttpServerRequest, new BunServerRequest(request, resolve, removeHost(request.url), server) ) - const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe(map))(httpEffect), scope) + const fiber = Fiber.runIn(Effect.runForkWith(context)(httpEffect), scope) request.signal.addEventListener("abort", () => { fiber.interruptUnsafe(parent.id, Error.ClientAbort.annotation) }, { once: true }) @@ -150,9 +171,10 @@ export const make = Effect.fnUntraced( } yield* Scope.addFinalizerExit(serveScope, () => { - handlerStack.pop() + const index = handlerStack.indexOf(handler) + if (index !== -1) handlerStack.splice(index, 1) server.reload({ fetch: handlerStack[handlerStack.length - 1] }) - return preemptiveShutdown + return handlerStack.length === 1 ? preemptiveShutdown : Effect.void }) handlerStack.push(handler) server.reload({ fetch: handler }) @@ -233,6 +255,7 @@ export const layerServer: ( options: ServeOptions & { readonly disablePreemptiveShutdown?: boolean | undefined readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: WebSocketOptions | undefined } ) => Layer.Layer = flow(make, Layer.effect(Server.HttpServer)) as any @@ -262,6 +285,7 @@ export const layer = ( options: ServeOptions & { readonly disablePreemptiveShutdown?: boolean | undefined readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: WebSocketOptions | undefined } ): Layer.Layer< | Server.HttpServer @@ -273,7 +297,7 @@ export const layer = ( /** * Layer that starts a Bun HTTP server on an ephemeral port for tests. * - * @category layers + * @category testing * @since 4.0.0 */ export const layerTest: Layer.Layer< @@ -296,6 +320,7 @@ export const layerConfig = ( ServeOptions & { readonly disablePreemptiveShutdown?: boolean | undefined readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: WebSocketOptions | undefined } > ): Layer.Layer< diff --git a/.context/effect/packages/platform-bun/src/BunHttpServerRequest.ts b/.context/effect/packages/platform/bun/src/BunHttpServerRequest.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunHttpServerRequest.ts rename to .context/effect/packages/platform/bun/src/BunHttpServerRequest.ts diff --git a/.context/effect/packages/platform-bun/src/BunMultipart.ts b/.context/effect/packages/platform/bun/src/BunMultipart.ts similarity index 88% rename from .context/effect/packages/platform-bun/src/BunMultipart.ts rename to .context/effect/packages/platform/bun/src/BunMultipart.ts index 080eea6b9..37162cc6b 100644 --- a/.context/effect/packages/platform-bun/src/BunMultipart.ts +++ b/.context/effect/packages/platform/bun/src/BunMultipart.ts @@ -24,19 +24,18 @@ import * as BunStream from "./BunStream.ts" */ export const stream = (source: Request): Stream.Stream => BunStream.fromReadableStream({ - evaluate: () => source.body ?? emptyReadbleStream, + evaluate: () => + source.body ?? new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array()) + controller.close() + } + }), onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause) }).pipe( Stream.pipeThroughChannel(Multipart.makeChannel(Object.fromEntries(source.headers))) ) -const emptyReadbleStream = new ReadableStream({ - start(controller) { - controller.enqueue(new Uint8Array()) - controller.close() - } -}) - /** * Parses and persists multipart data from a Bun `Request`, requiring file-system, path, and scope services. * diff --git a/.context/effect/packages/platform-bun/src/BunPath.ts b/.context/effect/packages/platform/bun/src/BunPath.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunPath.ts rename to .context/effect/packages/platform/bun/src/BunPath.ts diff --git a/.context/effect/packages/platform-bun/src/BunRedis.ts b/.context/effect/packages/platform/bun/src/BunRedis.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunRedis.ts rename to .context/effect/packages/platform/bun/src/BunRedis.ts diff --git a/.context/effect/packages/platform-bun/src/BunRuntime.ts b/.context/effect/packages/platform/bun/src/BunRuntime.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunRuntime.ts rename to .context/effect/packages/platform/bun/src/BunRuntime.ts diff --git a/.context/effect/packages/platform-bun/src/BunServices.ts b/.context/effect/packages/platform/bun/src/BunServices.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunServices.ts rename to .context/effect/packages/platform/bun/src/BunServices.ts diff --git a/.context/effect/packages/platform-bun/src/BunSink.ts b/.context/effect/packages/platform/bun/src/BunSink.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunSink.ts rename to .context/effect/packages/platform/bun/src/BunSink.ts diff --git a/.context/effect/packages/platform-bun/src/BunSocket.ts b/.context/effect/packages/platform/bun/src/BunSocket.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunSocket.ts rename to .context/effect/packages/platform/bun/src/BunSocket.ts diff --git a/.context/effect/packages/platform-bun/src/BunSocketServer.ts b/.context/effect/packages/platform/bun/src/BunSocketServer.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunSocketServer.ts rename to .context/effect/packages/platform/bun/src/BunSocketServer.ts diff --git a/.context/effect/packages/platform-bun/src/BunStdio.ts b/.context/effect/packages/platform/bun/src/BunStdio.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunStdio.ts rename to .context/effect/packages/platform/bun/src/BunStdio.ts diff --git a/.context/effect/packages/platform-bun/src/BunStream.ts b/.context/effect/packages/platform/bun/src/BunStream.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunStream.ts rename to .context/effect/packages/platform/bun/src/BunStream.ts diff --git a/.context/effect/packages/platform-bun/src/BunTerminal.ts b/.context/effect/packages/platform/bun/src/BunTerminal.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunTerminal.ts rename to .context/effect/packages/platform/bun/src/BunTerminal.ts diff --git a/.context/effect/packages/platform-bun/src/BunWorker.ts b/.context/effect/packages/platform/bun/src/BunWorker.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/BunWorker.ts rename to .context/effect/packages/platform/bun/src/BunWorker.ts diff --git a/.context/effect/packages/platform-bun/src/BunWorkerRunner.ts b/.context/effect/packages/platform/bun/src/BunWorkerRunner.ts similarity index 90% rename from .context/effect/packages/platform-bun/src/BunWorkerRunner.ts rename to .context/effect/packages/platform/bun/src/BunWorkerRunner.ts index 255b54a9f..f6473cfeb 100644 --- a/.context/effect/packages/platform-bun/src/BunWorkerRunner.ts +++ b/.context/effect/packages/platform/bun/src/BunWorkerRunner.ts @@ -77,22 +77,11 @@ export const layer: Layer.Layer = Layer.succe }) ) } - function onError(error: MessageEvent) { - Deferred.doneUnsafe( - closeLatch, - new WorkerError({ - reason: new WorkerReceiveError({ - message: "received error event", - cause: error.data - }) - }) - ) - } yield* Scope.addFinalizer( scope, Effect.sync(() => { port.removeEventListener("message", onMessage) - port.removeEventListener("messageerror", onError) + port.removeEventListener("messageerror", onMessageError) }) ) port.addEventListener("message", onMessage) diff --git a/.context/effect/packages/platform-bun/src/index.ts b/.context/effect/packages/platform/bun/src/index.ts similarity index 100% rename from .context/effect/packages/platform-bun/src/index.ts rename to .context/effect/packages/platform/bun/src/index.ts diff --git a/.context/effect/packages/platform/bun/test/BunHttpCompression.test.ts b/.context/effect/packages/platform/bun/test/BunHttpCompression.test.ts new file mode 100644 index 000000000..d1a6310a9 --- /dev/null +++ b/.context/effect/packages/platform/bun/test/BunHttpCompression.test.ts @@ -0,0 +1,110 @@ +import * as BunHttpPlatform from "@effect/platform-bun/BunHttpPlatform" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Stream from "effect/Stream" +import * as HttpEffect from "effect/unstable/http/HttpEffect" +import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" +import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" +import * as Fs from "node:fs" +import { fileURLToPath } from "node:url" +import * as Zlib from "node:zlib" + +const bigJson = JSON.stringify({ text: "All work and no play makes Jack a dull boy. ".repeat(100) }) +const bigJsonApp = Effect.succeed(HttpServerResponse.text(bigJson, { contentType: "application/json" })) + +type App = Effect.Effect +type CompressionOptions = Parameters[0] +type Algorithm = HttpPlatform.CompressionAlgorithm + +const algorithms: ReadonlyArray = ["gzip", "deflate", "br", "zstd"] + +const decompress = (algorithm: Algorithm, data: Uint8Array): string => { + switch (algorithm) { + case "gzip": + return Zlib.gunzipSync(data).toString() + case "deflate": + return Zlib.inflateSync(data).toString() + case "br": + return Zlib.brotliDecompressSync(data).toString() + case "zstd": + return Zlib.zstdDecompressSync(data).toString() + } +} + +const withHandler = async ( + app: App, + options: CompressionOptions, + run: (handler: (request: Request) => Promise) => Promise +) => { + const { dispose, handler } = HttpEffect.toWebHandlerLayer( + app as Effect.Effect, + BunHttpPlatform.layer, + { middleware: HttpMiddleware.compression(options) } + ) + try { + await run(handler) + } finally { + await dispose() + } +} + +const get = ( + handler: (request: Request) => Promise, + algorithm: Algorithm +) => handler(new Request("http://localhost/", { headers: { "accept-encoding": algorithm } })) + +// Bun's CompressionStream does not expose an explicit flush operation, so +// incremental SSE-style delivery is runtime-defined and intentionally is not +// guaranteed by this suite. +describe("BunHttpCompression", () => { + it.effect("advertises Bun's supported algorithms", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + assert.deepStrictEqual([...platform.compression.algorithms], algorithms) + }).pipe(Effect.provide(BunHttpPlatform.layer))) + + for (const algorithm of algorithms) { + it(`compresses one-shot bodies with ${algorithm}`, () => + withHandler(bigJsonApp, { algorithms }, async (handler) => { + const response = await get(handler, algorithm) + assert.strictEqual(response.headers.get("content-encoding"), algorithm) + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(decompress(algorithm, compressed), bigJson) + })) + + it(`compresses stream bodies with ${algorithm}`, () => + withHandler( + Effect.succeed(HttpServerResponse.stream( + Stream.fromArray([new TextEncoder().encode(bigJson)]), + { contentType: "application/json" } + )), + { algorithms }, + async (handler) => { + const response = await get(handler, algorithm) + assert.strictEqual(response.headers.get("content-encoding"), algorithm) + assert.strictEqual(response.headers.get("content-length"), null) + assert.strictEqual(decompress(algorithm, new Uint8Array(await response.arrayBuffer())), bigJson) + } + )) + } + + it("compresses Bun.file responses", async () => { + const path = fileURLToPath(new URL("./BunHttpCompression.test.ts", import.meta.url)) + const contents = Fs.readFileSync(path, "utf8") + await withHandler( + HttpServerResponse.file(path, { headers: { "content-type": "text/plain" } }), + { algorithms, minSize: 0 }, + async (handler) => { + const response = await get(handler, "gzip") + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + assert.isTrue(response.headers.get("etag")!.startsWith("W/")) + assert.strictEqual(decompress("gzip", new Uint8Array(await response.arrayBuffer())), contents) + } + ) + }) +}) diff --git a/.context/effect/packages/platform/bun/test/BunHttpPlatform.test.ts b/.context/effect/packages/platform/bun/test/BunHttpPlatform.test.ts new file mode 100644 index 000000000..9f9656fa8 --- /dev/null +++ b/.context/effect/packages/platform/bun/test/BunHttpPlatform.test.ts @@ -0,0 +1,25 @@ +import * as BunHttpPlatform from "@effect/platform-bun/BunHttpPlatform" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import type * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" + +const readBody = (body: HttpBody.HttpBody) => { + assert.strictEqual(body._tag, "Raw") + return Effect.promise(() => new Response((body as HttpBody.Raw).body as BodyInit).text()) +} + +describe("BunHttpPlatform", () => { + it.effect("fileWebResponse honors offset and bytesToRead including zero", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const file = new File(["abcd"], "file.txt", { type: "text/plain", lastModified: 0 }) + const sliced = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) + const empty = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 0 }) + + assert.deepStrictEqual( + { sliced: yield* readBody(sliced.body), empty: yield* readBody(empty.body) }, + { sliced: "bc", empty: "" } + ) + }).pipe(Effect.provide(BunHttpPlatform.layer))) +}) diff --git a/.context/effect/packages/platform/bun/test/BunHttpServer.test.ts b/.context/effect/packages/platform/bun/test/BunHttpServer.test.ts new file mode 100644 index 000000000..ca1e8d112 --- /dev/null +++ b/.context/effect/packages/platform/bun/test/BunHttpServer.test.ts @@ -0,0 +1,50 @@ +import * as BunHttpServer from "@effect/platform-bun/BunHttpServer" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as Scope from "effect/Scope" +import * as HttpServer from "effect/unstable/http/HttpServer" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" + +const fetchText = (url: string) => + Effect.promise(() => fetch(url, { headers: { connection: "close" } }).then((response) => response.text())) + +describe("BunHttpServer", () => { + it.effect("closing an older serve scope keeps the newer handler active", () => + Effect.gen(function*() { + const ownerScope = yield* Effect.scope + const server = yield* BunHttpServer.make({ + hostname: "127.0.0.1", + port: 0 + }) + const firstScope = yield* Scope.fork(ownerScope) + const secondScope = yield* Scope.fork(ownerScope) + + yield* server.serve(Effect.succeed(HttpServerResponse.text("first"))).pipe(Scope.provide(firstScope)) + yield* server.serve(Effect.succeed(HttpServerResponse.text("second"))).pipe(Scope.provide(secondScope)) + const url = HttpServer.formatAddress(server.address) + + assert.strictEqual(yield* fetchText(url), "second") + yield* Scope.close(firstScope, Exit.void) + assert.strictEqual(yield* fetchText(url), "second") + })) + + it.effect("closing the newer serve scope restores the older handler", () => + Effect.gen(function*() { + const ownerScope = yield* Effect.scope + const server = yield* BunHttpServer.make({ + hostname: "127.0.0.1", + port: 0 + }) + const firstScope = yield* Scope.fork(ownerScope) + const secondScope = yield* Scope.fork(ownerScope) + + yield* server.serve(Effect.succeed(HttpServerResponse.text("first"))).pipe(Scope.provide(firstScope)) + yield* server.serve(Effect.succeed(HttpServerResponse.text("second"))).pipe(Scope.provide(secondScope)) + const url = HttpServer.formatAddress(server.address) + + assert.strictEqual(yield* fetchText(url), "second") + yield* Scope.close(secondScope, Exit.void) + assert.strictEqual(yield* fetchText(url), "first") + })) +}) diff --git a/.context/effect/packages/platform/bun/tsconfig.json b/.context/effect/packages/platform/bun/tsconfig.json new file mode 100644 index 000000000..e15e22c6b --- /dev/null +++ b/.context/effect/packages/platform/bun/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" }, + { "path": "../node-shared" } + ], + "compilerOptions": { + "types": ["bun"] + } +} diff --git a/.context/effect/packages/platform/deno/CHANGELOG.md b/.context/effect/packages/platform/deno/CHANGELOG.md new file mode 100644 index 000000000..173b81e03 --- /dev/null +++ b/.context/effect/packages/platform/deno/CHANGELOG.md @@ -0,0 +1,106 @@ +# @effect/platform-deno + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node-shared@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9), [`e74c302`](https://github.com/Effect-TS/effect/commit/e74c302afe0368e5d3f15d18c10fc54cf33f9003)]: + - effect@4.0.0-beta.107 + - @effect/platform-node-shared@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7123](https://github.com/Effect-TS/effect/pull/7123) [`4bca71d`](https://github.com/Effect-TS/effect/commit/4bca71d4910ef3afff647076ee9c507e5c060827) Thanks @fubhy! - Honor `offset` and `bytesToRead` when creating Deno Web file responses. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`5f1775c`](https://github.com/Effect-TS/effect/commit/5f1775cb060cb3dbf96adb067fd97967da7eca2f), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`089313e`](https://github.com/Effect-TS/effect/commit/089313ec2a4c307393c7c5e00c725ec23840c9c1), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node-shared@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7086](https://github.com/Effect-TS/effect/pull/7086) [`c91f401`](https://github.com/Effect-TS/effect/commit/c91f4015f871b66cdc24cceeca46be26cbf23a76) Thanks @tim-smart! - Preserve high-level filesystem error context for `writeFile` and normalize Deno `AlreadyExists` errors from `copy`. + +- [#7090](https://github.com/Effect-TS/effect/pull/7090) [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2) Thanks @tim-smart! - Expose `stdinIsTerminal` and `stdoutIsTerminal` effects through the `Stdio` service. +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node-shared@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Minor Changes + +- [#7076](https://github.com/Effect-TS/effect/pull/7076) [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d) Thanks @tim-smart! - Return the new file offset as a `Size` from `File.seek`. + +### Patch Changes + +- [#7080](https://github.com/Effect-TS/effect/pull/7080) [`c2c966d`](https://github.com/Effect-TS/effect/commit/c2c966d70b157651bedd0b280c196823ac6b6432) Thanks @tim-smart! - Honor `FileSystem.writeFile` open flags in the Deno implementation. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`e2ec131`](https://github.com/Effect-TS/effect/commit/e2ec1311bed9bb8709c26396e71b15b4241a9185), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`e508589`](https://github.com/Effect-TS/effect/commit/e50858905fed68f29ec202ecdc9c902e44bfedd8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node-shared@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6682](https://github.com/Effect-TS/effect/pull/6682) [`cd98622`](https://github.com/Effect-TS/effect/commit/cd9862247f3faee43ed6bf7bc44c3d5385613916) Thanks @tim-smart! - Add a Deno-backed FileSystem layer. + +- [#6685](https://github.com/Effect-TS/effect/pull/6685) [`45e7810`](https://github.com/Effect-TS/effect/commit/45e78108823d24abe075cfb69a75733223960573) Thanks @tim-smart! - Add `DenoHttpClient`, re-exporting `effect/unstable/http/FetchHttpClient` + + Deno's `fetch` is spec-compliant, so the core fetch-based `HttpClient` works on Deno unmodified. This module mirrors `BunHttpClient` so the platform packages expose a consistent surface. + +- [#6725](https://github.com/Effect-TS/effect/pull/6725) [`d435467`](https://github.com/Effect-TS/effect/commit/d435467e634f47b383fc4e8ece1b37fe3eecc7ac) Thanks @tim-smart! - Add web-standard multipart request parsing helpers for Deno. + +- [#6724](https://github.com/Effect-TS/effect/pull/6724) [`7870557`](https://github.com/Effect-TS/effect/commit/7870557987ec0801b0ea2f8c4e15fd2ab6b11aac) Thanks @tim-smart! - Add native Deno TCP, Unix, and TLS socket server adapters. + +- [#6721](https://github.com/Effect-TS/effect/pull/6721) [`35141f1`](https://github.com/Effect-TS/effect/commit/35141f1a02d367daba069b321f5a6b09b379a580) Thanks @tim-smart! - Add native Deno TCP, Unix, and WebSocket integrations for Effect sockets. + +- [#6412](https://github.com/Effect-TS/effect/pull/6412) [`ec656dc`](https://github.com/Effect-TS/effect/commit/ec656dccf17cfdf0be4152e042c56f7a21f169ac) Thanks @lishaduck! - Add Deno platform integrations for paths, runtime execution, workers, and Web Storage. + +- [#6723](https://github.com/Effect-TS/effect/pull/6723) [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e) Thanks @tim-smart! - add platform literal to HttpPlatform + +- [#6802](https://github.com/Effect-TS/effect/pull/6802) [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92) Thanks @tim-smart! - Allow configuring cluster RPC serialization buffer limits. + +- [#6684](https://github.com/Effect-TS/effect/pull/6684) [`dbd6ea4`](https://github.com/Effect-TS/effect/commit/dbd6ea4ef6f78f41070d57bd9a5bfa2699df268d) Thanks @tim-smart! - Add a Deno Web Crypto implementation of the `Crypto` service. + +- [#6688](https://github.com/Effect-TS/effect/pull/6688) [`504343b`](https://github.com/Effect-TS/effect/commit/504343b0cdf9a0306191c069c31b7d569eba0ed7) Thanks @tim-smart! - Add a native Deno `ChildProcessSpawner` implementation and shared process conformance coverage. + +- [#6715](https://github.com/Effect-TS/effect/pull/6715) [`75f340e`](https://github.com/Effect-TS/effect/commit/75f340ef3d6dc350337ff6ff930ae533046a969f) Thanks @tim-smart! - Add a Deno `Terminal` implementation and keep `NodeTerminal` input readers alive until stdin ends under Deno. + +- [#6700](https://github.com/Effect-TS/effect/pull/6700) [`d51329b`](https://github.com/Effect-TS/effect/commit/d51329b4339b4072944682e2950149acaa152971) Thanks @tim-smart! - Add a native Deno implementation of the `Stdio` service. + +- [#6716](https://github.com/Effect-TS/effect/pull/6716) [`897b35c`](https://github.com/Effect-TS/effect/commit/897b35ca373bfed7d3f3da60ee90a43c5a8d48ca) Thanks @tim-smart! - Add the aggregate Deno platform services layer. + +- [#6720](https://github.com/Effect-TS/effect/pull/6720) [`87ca79e`](https://github.com/Effect-TS/effect/commit/87ca79ec8fac0546f865f82c97734c53c8bed068) Thanks @tim-smart! - Add a native Deno `HttpPlatform` layer with resource-backed file responses. + +- [#6686](https://github.com/Effect-TS/effect/pull/6686) [`a149f89`](https://github.com/Effect-TS/effect/commit/a149f89295910003dff3d7bb16c73ee3750d9ab2) Thanks @tim-smart! - Add a native Deno Redis integration backed by `@db/redis`. + +- [#6730](https://github.com/Effect-TS/effect/pull/6730) [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30) Thanks @tim-smart! - Add a native Deno HTTP server with multipart requests, file responses, and WebSocket upgrades. + +- [#6731](https://github.com/Effect-TS/effect/pull/6731) [`a3fabe2`](https://github.com/Effect-TS/effect/commit/a3fabe25add28461ef624c37bf52129a9b00435f) Thanks @tim-smart! - Add native Deno HTTP and WebSocket layers for Effect Cluster runners. + +- [#6728](https://github.com/Effect-TS/effect/pull/6728) [`e0426da`](https://github.com/Effect-TS/effect/commit/e0426da103e931d43ef8c85023c9d3439bd8533d) Thanks @tim-smart! - Add native Deno socket layers for Effect Cluster runners. + +- [#6898](https://github.com/Effect-TS/effect/pull/6898) [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b) Thanks @tim-smart! - Add HTTP response compression support. Node.js, Bun, and Deno use asynchronous + `node:zlib` one-shot compression for byte-array bodies, preserving an exact + `Content-Length`; stream and raw bodies remain streaming transforms. + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6705](https://github.com/Effect-TS/effect/pull/6705) [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146) Thanks @tylergibbs1! - Restore the `recursive` option for `FileSystem.watch`, with non-recursive watching as the default. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node-shared@4.0.0-beta.103 diff --git a/.context/effect/packages/platform/deno/LICENSE b/.context/effect/packages/platform/deno/LICENSE new file mode 100644 index 000000000..2d1e0c682 --- /dev/null +++ b/.context/effect/packages/platform/deno/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 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/.context/effect/packages/platform/deno/README.md b/.context/effect/packages/platform/deno/README.md new file mode 100644 index 000000000..d67874c0d --- /dev/null +++ b/.context/effect/packages/platform/deno/README.md @@ -0,0 +1,14 @@ +# @effect/platform-deno + +[Deno](https://deno.com) implementations of the Effect platform services, including the file system, HTTP client and server, sockets, workers, and terminal. + +## Installation + +```sh +npm install effect@beta @effect/platform-deno@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/platform-deno) diff --git a/.context/effect/packages/platform/deno/package.json b/.context/effect/packages/platform/deno/package.json new file mode 100644 index 000000000..ee3a37a96 --- /dev/null +++ b/.context/effect/packages/platform/deno/package.json @@ -0,0 +1,83 @@ +{ + "name": "@effect/platform-deno", + "type": "module", + "version": "4.0.0-rc.108", + "license": "MIT", + "description": "Platform specific implementations for the Deno runtime", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/platform/deno" + }, + "bugs": { + "url": "https://github.com/Effect-TS/effect/issues" + }, + "tags": [ + "deno", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "keywords": [ + "deno", + "typescript", + "algebraic-data-types", + "functional-programming" + ], + "engines": { + "deno": ">=2.5.0" + }, + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./index": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./index": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "check": "deno check" + }, + "dependencies": { + "@db/redis": "jsr:^0.41.2", + "@effect/platform-node-shared": "workspace:^", + "@std/fs": "jsr:^1.0.24", + "@std/media-types": "jsr:^1.1.0", + "@std/path": "jsr:^1.1.6", + "@std/streams": "jsr:^1.1.1" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "devDependencies": { + "@std/assert": "jsr:^1.0.16", + "@testcontainers/redis": "^12.0.4", + "@types/deno": "^2.7.0", + "effect": "workspace:^" + } +} diff --git a/.context/effect/packages/platform/deno/src/DenoChildProcessSpawner.ts b/.context/effect/packages/platform/deno/src/DenoChildProcessSpawner.ts new file mode 100644 index 000000000..3941f8acb --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoChildProcessSpawner.ts @@ -0,0 +1,439 @@ +/** + * Deno implementation of the child process spawner service. + * + * This module uses `Deno.Command` and Web Streams directly. Deno cannot create + * detached process groups, so killing a handle terminates only its direct child; + * descendants spawned by that child are left running. + * + * @since 4.0.0 + */ +import type * as Arr from "effect/Array" +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 Path from "effect/Path" +import * as PlatformError from "effect/PlatformError" +import * as Predicate from "effect/Predicate" +import type * as Scope from "effect/Scope" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as ChildProcess from "effect/unstable/process/ChildProcess" +import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner" +import { + ChildProcessSpawner, + ExitCode, + make as makeSpawner, + makeHandle, + ProcessId +} from "effect/unstable/process/ChildProcessSpawner" + +const commandString = (command: ChildProcess.Command): string => { + const { commands } = flattenCommand(command) + return commands.map((command) => `${command.command} ${command.args.join(" ")}`).join(" | ") +} + +const toPlatformError = ( + method: string, + cause: unknown, + command: ChildProcess.Command +): PlatformError.PlatformError => { + const errorName = Predicate.hasProperty(cause, "name") ? cause.name : undefined + const tag = errorName === "NotFound" ? + "NotFound" : + errorName === "PermissionDenied" ? + "PermissionDenied" : + errorName === "TimedOut" + ? "TimedOut" + : "Unknown" + return PlatformError.systemError({ + _tag: tag, + module: "ChildProcess", + method, + pathOrDescriptor: commandString(command), + syscall: `${method} ${commandString(command).trim()}`, + cause + }) +} + +const unsupported = (option: "additionalFds" | "detached") => + PlatformError.badArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: `The ${option} option is unsupported because Deno has no equivalent` + }) + +const make = Effect.gen(function*() { + const path = yield* Path.Path + + const resolveWorkingDirectory = (options: ChildProcess.CommandOptions) => + options.cwd === undefined ? undefined : path.resolve(options.cwd) + + const resolveEnvironment = (options: ChildProcess.CommandOptions) => { + if (options.env === undefined) return undefined + const env: Record = {} + for (const [key, value] of Object.entries(options.env)) { + if (value !== undefined) env[key] = value + } + return env + } + + const resolveStdinOption = (options: ChildProcess.CommandOptions): ChildProcess.StdinConfig => { + const defaultConfig: ChildProcess.StdinConfig = { stream: "pipe", encoding: "utf-8", endOnDone: true } + if (options.stdin === undefined) { + return defaultConfig + } + if (typeof options.stdin === "string") { + return { ...defaultConfig, stream: options.stdin } + } + if (Stream.isStream(options.stdin)) { + return { ...defaultConfig, stream: options.stdin } + } + return { + stream: options.stdin.stream, + encoding: options.stdin.encoding ?? defaultConfig.encoding, + endOnDone: options.stdin.endOnDone ?? defaultConfig.endOnDone + } + } + + const resolveOutputOption = ( + options: ChildProcess.CommandOptions, + streamName: "stdout" | "stderr" + ): ChildProcess.StdoutConfig => { + const option = options[streamName] + if (option === undefined) return { stream: "pipe" } + if (typeof option === "string" || Sink.isSink(option)) return { stream: option } + return { stream: option.stream } + } + + const inputToStdioOption = ( + input: ChildProcess.CommandInput + ): "piped" | "inherit" | "null" => + Stream.isStream(input) || input === "pipe" || input === "overlapped" ? + "piped" : + input === "ignore" + ? "null" + : "inherit" + + const outputToStdioOption = ( + output: ChildProcess.CommandOutput | undefined + ): "piped" | "inherit" | "null" => + Sink.isSink(output) || output === "pipe" || output === "overlapped" || output === undefined ? + "piped" : + output === "ignore" + ? "null" + : "inherit" + + const setupChildStdin = ( + command: ChildProcess.StandardCommand, + childProcess: Deno.ChildProcess, + config: ChildProcess.StdinConfig + ) => + Effect.suspend(() => { + if (inputToStdioOption(config.stream) !== "piped") return Effect.succeed(Sink.drain) + const sink = Sink.fromWritableStream({ + evaluate: () => childProcess.stdin, + onError: (cause) => toPlatformError("fromWritable(stdin)", cause, command), + closeOnDone: config.endOnDone + }) + return Stream.isStream(config.stream) + ? Effect.as(Effect.forkScoped(Stream.run(config.stream, sink)), sink) + : Effect.succeed(sink) + }) + + const setupChildOutputStreams = ( + command: ChildProcess.StandardCommand, + childProcess: Deno.ChildProcess, + stdoutConfig: ChildProcess.StdoutConfig, + stderrConfig: ChildProcess.StderrConfig + ) => { + let stdout: Stream.Stream = + outputToStdioOption(stdoutConfig.stream) === "piped" + ? Stream.fromReadableStream({ + evaluate: () => childProcess.stdout, + onError: (cause) => toPlatformError("fromReadable(stdout)", cause, command) + }) + : Stream.empty + let stderr: Stream.Stream = + outputToStdioOption(stderrConfig.stream) === "piped" + ? Stream.fromReadableStream({ + evaluate: () => childProcess.stderr, + onError: (cause) => toPlatformError("fromReadable(stderr)", cause, command) + }) + : Stream.empty + + if (Sink.isSink(stdoutConfig.stream)) stdout = Stream.transduce(stdout, stdoutConfig.stream) + if (Sink.isSink(stderrConfig.stream)) stderr = Stream.transduce(stderr, stderrConfig.stream) + + return { stdout, stderr, all: Stream.merge(stdout, stderr) } + } + + const spawn = ( + command: ChildProcess.StandardCommand, + executable: string, + args: ReadonlyArray, + options: Deno.CommandOptions + ) => + Effect.try({ + try: () => { + const childProcess = new Deno.Command(executable, { ...options, args: [...args] }).spawn() + const exitSignal = Deferred.makeUnsafe() + childProcess.status.then( + (status) => Deferred.doneUnsafe(exitSignal, Exit.succeed(status)), + (cause) => Deferred.doneUnsafe(exitSignal, Exit.fail(toPlatformError("status", cause, command))) + ) + return [childProcess, exitSignal] as const + }, + catch: (cause) => toPlatformError("spawn", cause, command) + }) + + const killProcess = ( + command: ChildProcess.StandardCommand, + childProcess: Deno.ChildProcess, + signal: ChildProcess.Signal + ) => + Effect.try({ + try: () => childProcess.kill(signal as Deno.Signal), + catch: (cause) => toPlatformError("kill", cause, command) + }) + + const withTimeout = ( + childProcess: Deno.ChildProcess, + command: ChildProcess.StandardCommand, + options: ChildProcess.KillOptions | undefined + ) => + ( + kill: ( + command: ChildProcess.StandardCommand, + childProcess: Deno.ChildProcess, + signal: ChildProcess.Signal + ) => Effect.Effect + ) => { + const killSignal = options?.killSignal ?? "SIGTERM" + return options?.forceKillAfter === undefined + ? kill(command, childProcess, killSignal) + : Effect.timeoutOrElse(kill(command, childProcess, killSignal), { + duration: options.forceKillAfter, + orElse: () => kill(command, childProcess, "SIGKILL") + }) + } + + const getSourceStream = ( + handle: ChildProcessHandle, + from: "stdout" | "stderr" | "all" + ): Stream.Stream => { + switch (from) { + case "stdout": + return handle.stdout + case "stderr": + return handle.stderr + case "all": + return handle.all + } + } + + const spawnCommand: ( + command: ChildProcess.Command + ) => Effect.Effect = Effect.fnUntraced(function*(cmd) { + switch (cmd._tag) { + case "StandardCommand": { + if (cmd.options.additionalFds !== undefined) { + return yield* Effect.fail(unsupported("additionalFds")) + } + if (cmd.options.detached !== undefined) { + return yield* Effect.fail(unsupported("detached")) + } + + const stdinConfig = resolveStdinOption(cmd.options) + const stdoutConfig = resolveOutputOption(cmd.options, "stdout") + const stderrConfig = resolveOutputOption(cmd.options, "stderr") + let isReferenced = true + + let executable = cmd.command + let args = cmd.args + let windowsRawArguments = false + if (cmd.options.shell) { + const shellCommand = `${cmd.command} ${cmd.args.join(" ")}` + if (Deno.build.os === "windows") { + executable = typeof cmd.options.shell === "string" ? cmd.options.shell : "cmd.exe" + args = ["/d", "/s", "/c", shellCommand] + windowsRawArguments = true + } else { + executable = typeof cmd.options.shell === "string" ? cmd.options.shell : "/bin/sh" + args = ["-c", shellCommand] + } + } + + const cwd = resolveWorkingDirectory(cmd.options) + const env = resolveEnvironment(cmd.options) + const [childProcess, exitSignal] = yield* Effect.acquireRelease( + spawn(cmd, executable, args, { + ...(cwd === undefined ? undefined : { cwd }), + ...(env === undefined ? undefined : { env }), + clearEnv: cmd.options.env !== undefined && cmd.options.extendEnv !== true, + stdin: inputToStdioOption(stdinConfig.stream), + stdout: outputToStdioOption(stdoutConfig.stream), + stderr: outputToStdioOption(stderrConfig.stream), + windowsRawArguments + }), + Effect.fnUntraced(function*([childProcess, exitSignal]) { + const exited = yield* Deferred.isDone(exitSignal) + if (exited || !isReferenced) return + const killWithTimeout = withTimeout(childProcess, cmd, cmd.options) + yield* killWithTimeout((command, childProcess, signal) => + killProcess(command, childProcess, signal).pipe( + Effect.andThen(Deferred.await(exitSignal)) + ) + ).pipe(Effect.ignore) + }) + ) + + const reref = Effect.sync(() => { + if (!isReferenced) { + childProcess.ref() + isReferenced = true + } + }) + const unref = Effect.sync(() => { + if (isReferenced) { + childProcess.unref() + isReferenced = false + } + return reref + }) + const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig) + const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig) + const isRunning = Effect.map(Deferred.isDone(exitSignal), (done) => !done) + const exitCode = Effect.flatMap(Deferred.await(exitSignal), (status) => + Predicate.isNull(status.signal) + ? Effect.succeed(ExitCode(status.code)) + : Effect.fail(toPlatformError( + "exitCode", + new globalThis.Error(`Process interrupted due to receipt of signal: '${status.signal}'`), + cmd + ))) + const kill = (options?: ChildProcess.KillOptions | undefined) => { + const killWithTimeout = withTimeout(childProcess, cmd, options) + return killWithTimeout((command, childProcess, signal) => + killProcess(command, childProcess, signal).pipe( + Effect.andThen(Deferred.await(exitSignal)) + ) + ).pipe(Effect.asVoid) + } + + return makeHandle({ + pid: ProcessId(childProcess.pid), + exitCode, + isRunning, + kill, + stdin, + stdout, + stderr, + all, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + unref + }) + } + case "PipedCommand": { + const { commands, pipeOptions } = flattenCommand(cmd) + const [root, ...pipeline] = commands + const handles = [yield* spawnCommand(root)] + + for (let i = 0; i < pipeline.length; i++) { + const command = pipeline[i] + const options = pipeOptions[i] ?? {} + const stdinConfig = resolveStdinOption(command.options) + const from = options.from ?? "stdout" + + if ( + (from !== "stdout" && from !== "stderr" && from !== "all") || + (options.to ?? "stdin") !== "stdin" + ) { + return yield* Effect.fail(unsupported("additionalFds")) + } + const sourceStream = getSourceStream(handles[handles.length - 1], from) + handles.push( + yield* spawnCommand(ChildProcess.make(command.command, command.args, { + ...command.options, + stdin: { ...stdinConfig, stream: sourceStream } + })) + ) + } + + const handle = handles[handles.length - 1] + const kill = (options?: ChildProcess.KillOptions | undefined) => + Effect.forEach([...handles].reverse(), (handle) => Effect.ignore(handle.kill(options)), { discard: true }) + const unref = Effect.gen(function*() { + const rerefs: Array> = [] + for (const handle of handles) rerefs.push(yield* handle.unref) + return Effect.forEach([...rerefs].reverse(), (reref) => reref, { discard: true }) + }) + + return makeHandle({ + pid: handle.pid, + exitCode: handle.exitCode, + isRunning: handle.isRunning, + kill, + stdin: handle.stdin, + stdout: handle.stdout, + stderr: handle.stderr, + all: handle.all, + getInputFd: handle.getInputFd, + getOutputFd: handle.getOutputFd, + unref + }) + } + } + }) + + return makeSpawner(spawnCommand) +}) + +/** + * Layer that provides the Deno child process spawner. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect(ChildProcessSpawner, make) + +/** + * Result of flattening a pipeline of commands. + * + * @category models + * @since 4.0.0 + */ +export interface FlattenedPipeline { + readonly commands: Arr.NonEmptyReadonlyArray + readonly pipeOptions: ReadonlyArray +} + +/** + * Flattens a command into standard commands and their pipe options. + * + * @category transforming + * @since 4.0.0 + */ +export const flattenCommand = (command: ChildProcess.Command): FlattenedPipeline => { + const commands: Array = [] + const pipeOptions: Array = [] + + const flatten = (command: ChildProcess.Command): void => { + switch (command._tag) { + case "StandardCommand": + commands.push(command) + break + case "PipedCommand": + flatten(command.left) + pipeOptions.push(command.options) + flatten(command.right) + break + } + } + flatten(command) + + const [first, ...rest] = commands + if (first === undefined) throw new Error("flattenCommand produced empty commands array") + return { commands: [first, ...rest], pipeOptions } +} diff --git a/.context/effect/packages/platform/deno/src/DenoClusterHttp.ts b/.context/effect/packages/platform/deno/src/DenoClusterHttp.ts new file mode 100644 index 000000000..0114b9ff8 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoClusterHttp.ts @@ -0,0 +1,156 @@ +/** + * Native Deno HTTP and WebSocket layers for Effect Cluster runners. + * + * `layerHttpServer` provides the Deno HTTP server used by cluster runners. The + * main `layer` builds a sharding layer for HTTP or WebSocket transport, + * choosing serialization, runner health checks, runner storage, message + * storage, and optional client-only mode from the supplied options. + * + * @since 4.0.0 + */ +import type * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as HttpRunner from "effect/unstable/cluster/HttpRunner" +import * as MessageStorage from "effect/unstable/cluster/MessageStorage" +import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth" +import * as Runners from "effect/unstable/cluster/Runners" +import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage" +import type { Sharding } from "effect/unstable/cluster/Sharding" +import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig" +import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage" +import type * as Etag from "effect/unstable/http/Etag" +import type { HttpPlatform } from "effect/unstable/http/HttpPlatform" +import type { HttpServer } from "effect/unstable/http/HttpServer" +import type { ServeError } from "effect/unstable/http/HttpServerError" +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" +import type { SqlClient } from "effect/unstable/sql/SqlClient" +import { layerK8sHttpClient } from "./DenoClusterSocket.ts" +import * as DenoCrypto from "./DenoCrypto.ts" +import * as DenoHttpClient from "./DenoHttpClient.ts" +import * as DenoHttpServer from "./DenoHttpServer.ts" +import type { DenoServices } from "./DenoServices.ts" +import * as DenoSocket from "./DenoSocket.ts" + +export { + /** + * Layer that provides a Kubernetes HTTP client for runner health checks. + * + * @category re-exports + * @since 4.0.0 + */ + layerK8sHttpClient +} + +/** + * Layer that provides a native Deno HTTP server for cluster runners. + * + * @category layers + * @since 4.0.0 + */ +export const layerHttpServer: Layer.Layer< + | HttpPlatform + | Etag.Generator + | DenoServices + | HttpServer, + ServeError, + ShardingConfig.ShardingConfig +> = Effect.gen(function*() { + const config = yield* ShardingConfig.ShardingConfig + const listenAddress = Option.orElse(config.runnerListenAddress, () => config.runnerAddress) + if (Option.isNone(listenAddress)) { + return yield* Effect.die("DenoClusterHttp.layerHttpServer: ShardingConfig.runnerAddress is None") + } + return DenoHttpServer.layer({ + hostname: listenAddress.value.host, + port: listenAddress.value.port, + onListen: () => {} + }) +}).pipe(Layer.unwrap) + +/** + * Creates Deno cluster layers for HTTP or WebSocket transport, configuring + * serialization, storage, runner health, and optional client-only mode. + * + * @category layers + * @since 4.0.0 + */ +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 serializationMaxBufferSize?: number | "unbounded" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly runnerHealth?: "ping" | "k8s" | 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), + Config.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 | Config.ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options.clientOnly + ? options.transport === "http" + ? Layer.provide(HttpRunner.layerHttpClientOnly, DenoHttpClient.layer) + : Layer.provide(HttpRunner.layerWebsocketClientOnly, DenoSocket.layerWebSocketConstructor) + : options.transport === "http" + ? Layer.provide(HttpRunner.layerHttp, [layerHttpServer, DenoHttpClient.layer]) + : Layer.provide(HttpRunner.layerWebsocket, [layerHttpServer, DenoSocket.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(DenoHttpClient.layer)) + : HttpRunner.layerClientProtocolWebsocketDefault.pipe(Layer.provide(DenoSocket.layerWebSocketConstructor)) + ) + ) + + return layer.pipe( + Layer.provide(runnerHealth), + Layer.provideMerge( + options.storage === "local" + ? MessageStorage.layerNoop + : options.storage === "byo" + ? Layer.empty + : Layer.orDie(SqlMessageStorage.layer).pipe(Layer.provide(DenoCrypto.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.layerNdjsonWith({ maxBufferSize: options.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options.serializationMaxBufferSize }) + ) + ) as any +} diff --git a/.context/effect/packages/platform/deno/src/DenoClusterSocket.ts b/.context/effect/packages/platform/deno/src/DenoClusterSocket.ts new file mode 100644 index 000000000..fbb86a2db --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoClusterSocket.ts @@ -0,0 +1,197 @@ +/** + * Native Deno socket layers for Effect Cluster runners. + * + * The main `layer` builds a sharding layer for socket transport, choosing + * serialization, runner health checks, runner storage, message storage, and + * optional client-only mode from the supplied options. Unlike Node sockets, + * Deno connections have no native idle-timeout option, so peer connections use + * only the one-second open timeout. + * + * @since 4.0.0 + */ +import type * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as K8sHttpClient from "effect/unstable/cluster/K8sHttpClient" +import * as MessageStorage from "effect/unstable/cluster/MessageStorage" +import * as RunnerHealth from "effect/unstable/cluster/RunnerHealth" +import * as Runners from "effect/unstable/cluster/Runners" +import * as RunnerStorage from "effect/unstable/cluster/RunnerStorage" +import type { Sharding } from "effect/unstable/cluster/Sharding" +import * as ShardingConfig from "effect/unstable/cluster/ShardingConfig" +import * as SocketRunner from "effect/unstable/cluster/SocketRunner" +import * as SqlMessageStorage from "effect/unstable/cluster/SqlMessageStorage" +import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage" +import * as RpcClient from "effect/unstable/rpc/RpcClient" +import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" +import { Socket } from "effect/unstable/socket/Socket" +import type * as SocketServer from "effect/unstable/socket/SocketServer" +import type { SqlClient } from "effect/unstable/sql/SqlClient" +import * as DenoCrypto from "./DenoCrypto.ts" +import * as DenoFileSystem from "./DenoFileSystem.ts" +import * as DenoHttpClient from "./DenoHttpClient.ts" +import * as DenoSocket from "./DenoSocket.ts" +import * as DenoSocketServer from "./DenoSocketServer.ts" + +/** + * Provides the cluster `RpcClientProtocol` using native Deno TCP sockets. + * + * @category layers + * @since 4.0.0 + */ +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* DenoSocket.makeTcp({ + openTimeout: 1000, + hostname: address.host, + port: address.port + }) + return yield* RpcClient.makeProtocolSocket().pipe( + Effect.provideService(Socket, socket), + Effect.provideService(RpcSerialization.RpcSerialization, serialization) + ) + }, Effect.orDie) + }) +) + +/** + * Provides the native Deno socket server used by cluster runners, listening on + * `ShardingConfig.runnerListenAddress` or `runnerAddress`. + * + * @category layers + * @since 4.0.0 + */ +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 (Option.isNone(listenAddress)) { + return yield* Effect.die("layerSocketServer: ShardingConfig.runnerListenAddress is None") + } + return DenoSocketServer.layer({ + hostname: listenAddress.value.host, + port: listenAddress.value.port + }) +}).pipe(Layer.unwrap) + +/** + * Creates Deno socket cluster layers, configuring serialization, storage, + * runner health, and optional client-only mode. + * + * @category layers + * @since 4.0.0 + */ +export const layer = < + const ClientOnly extends boolean = false, + const Storage extends "local" | "sql" | "byo" = never +>( + options?: { + readonly serialization?: "msgpack" | "ndjson" | undefined + readonly serializationMaxBufferSize?: number | "unbounded" | undefined + readonly clientOnly?: ClientOnly | undefined + readonly storage?: Storage | undefined + readonly runnerHealth?: "ping" | "k8s" | 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), + Config.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 | Config.ConfigError, + "local" extends Storage ? never + : "byo" extends Storage ? (MessageStorage.MessageStorage | RunnerStorage.RunnerStorage) + : SqlClient + > => +{ + const layer: Layer.Layer = options?.clientOnly + ? Layer.provide(SocketRunner.layerClientOnly, layerClientProtocol) + : 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).pipe(Layer.provide(DenoCrypto.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.layerNdjsonWith({ maxBufferSize: options?.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options?.serializationMaxBufferSize }) + ) + ) as any +} + +/** + * Layer that provides `K8sHttpClient`, using a scoped native Deno HTTP client + * with the Kubernetes service-account CA certificate when it is available. + * + * @category layers + * @since 4.0.0 + */ +export const layerK8sHttpClient: Layer.Layer = K8sHttpClient.layer.pipe( + Layer.provide( + Layer.fresh(DenoHttpClient.layer).pipe( + Layer.provide(Layer.effect( + DenoHttpClient.Fetch, + 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 (Option.isNone(caCertOption)) { + return globalThis.fetch + } + + const client = yield* Effect.acquireRelease( + Effect.sync(() => Deno.createHttpClient({ caCerts: [caCertOption.value] })), + (client) => Effect.sync(() => client.close()) + ) + return ((input, init) => globalThis.fetch(input, { ...init, client } as any)) as typeof globalThis.fetch + }) + )) + ) + ), + Layer.provide(DenoFileSystem.layer) +) diff --git a/.context/effect/packages/platform/deno/src/DenoCrypto.ts b/.context/effect/packages/platform/deno/src/DenoCrypto.ts new file mode 100644 index 000000000..1a0d0dafd --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoCrypto.ts @@ -0,0 +1,63 @@ +/** + * Deno-backed implementation of Effect's Crypto service. + * + * This module uses Deno's global Web Crypto API. + * + * @since 4.0.0 + */ +import * as Context from "effect/Context" +import * as EffectCrypto from "effect/Crypto" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as PlatformError from "effect/PlatformError" + +/** + * Provides the Web Crypto API used by the Crypto service implementation. + * + * @category services + * @since 4.0.0 + */ +export const WebCrypto = Context.Reference("@effect/platform-deno/Crypto/WebCrypto", { + defaultValue: () => globalThis.crypto +}) + +/** + * A layer that provides Effect's Crypto service using Deno's Web Crypto API. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect( + EffectCrypto.Crypto, + Effect.gen(function*() { + const crypto = yield* WebCrypto + const randomBytes = (size: number): Uint8Array => { + const bytes = new Uint8Array(size) + for (let offset = 0; offset < bytes.length; offset += 65_536) { + crypto.getRandomValues(bytes.subarray(offset, offset + 65_536)) + } + return bytes + } + + const digest: EffectCrypto.Crypto["digest"] = (algorithm, data) => + Effect.map( + Effect.tryPromise({ + try: () => crypto.subtle.digest(algorithm, new Uint8Array(data)), + catch: (cause) => + PlatformError.systemError({ + module: "Crypto", + method: "digest", + _tag: "Unknown", + description: "Could not compute digest", + cause + }) + }), + (buffer) => new Uint8Array(buffer) + ) + + return EffectCrypto.make({ + randomBytes, + digest + }) + }) +) diff --git a/.context/effect/packages/platform/deno/src/DenoFileSystem.ts b/.context/effect/packages/platform/deno/src/DenoFileSystem.ts new file mode 100644 index 000000000..9e263a7c3 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoFileSystem.ts @@ -0,0 +1,504 @@ +/** + * Deno implementation of Effect's `FileSystem` service. + * + * @since 4.0.0 + */ +import { copy as denoCopy, expandGlob, walk } from "@std/fs" +import { relative } from "@std/path" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as PlatformError from "effect/PlatformError" +import * as Stream from "effect/Stream" +import { handleError } from "./internal/error.ts" + +const tryPromise = ( + method: string, + pathOrDescriptor: string | number | undefined, + evaluate: (signal: AbortSignal) => PromiseLike +) => + Effect.tryPromise({ + try: evaluate, + catch: handleError("FileSystem", method, pathOrDescriptor) + }) + +const close = (file: Deno.FsFile, method: string, pathOrDescriptor: string | number) => + Effect.orDie(Effect.try({ + try: () => file.close(), + catch: handleError("FileSystem", method, pathOrDescriptor) + })) + +const collectAsyncIterable = ( + method: string, + pathOrDescriptor: string | number | undefined, + evaluate: () => AsyncIterable +) => + Effect.tryPromise({ + try: async () => { + const values = new Array() + for await (const value of evaluate()) { + values.push(value) + } + return values + }, + catch: handleError("FileSystem", method, pathOrDescriptor) + }) + +const access: FileSystem.FileSystem["access"] = (path, options) => { + if (!options?.readable && !options?.writable) { + return Effect.asVoid(tryPromise("access", path, () => Deno.stat(path))) + } + return Effect.acquireUseRelease( + tryPromise("access", path, () => + Deno.open(path, { + ...(options.readable ? { read: true } : {}), + ...(options.writable ? { write: true } : {}) + })), + () => Effect.void, + (file) => close(file, "access", path) + ) +} + +const copy: FileSystem.FileSystem["copy"] = (fromPath, toPath, options) => + tryPromise("copy", fromPath, () => + denoCopy(fromPath, toPath, { + overwrite: options?.overwrite ?? false, + preserveTimestamps: options?.preserveTimestamps ?? false + })) + +const copyFile: FileSystem.FileSystem["copyFile"] = (fromPath, toPath) => + tryPromise("copyFile", fromPath, () => Deno.copyFile(fromPath, toPath)) + +const chmod: FileSystem.FileSystem["chmod"] = (path, mode) => tryPromise("chmod", path, () => Deno.chmod(path, mode)) + +const chown: FileSystem.FileSystem["chown"] = (path, uid, gid) => + tryPromise("chown", path, () => Deno.chown(path, uid, gid)) + +const glob: FileSystem.FileSystem["glob"] = (pattern, options) => + Effect.map( + collectAsyncIterable("glob", pattern, () => + expandGlob(pattern, { + root: options?.root ?? Deno.cwd(), + exclude: options?.exclude ? [...options.exclude] : [] + })), + (entries) => entries.map((entry) => entry.path) + ) + +const link: FileSystem.FileSystem["link"] = (existingPath, newPath) => + tryPromise("link", existingPath, () => Deno.link(existingPath, newPath)) + +const makeDirectory: FileSystem.FileSystem["makeDirectory"] = (path, options) => + tryPromise("makeDirectory", path, () => + Deno.mkdir(path, { + recursive: options?.recursive ?? false, + ...(options?.mode === undefined ? {} : { mode: options.mode }) + })) + +const makeTempDirectoryFactory = (method: string): FileSystem.FileSystem["makeTempDirectory"] => (options) => + tryPromise(method, options?.directory, () => + Deno.makeTempDir({ + ...(options?.directory === undefined ? {} : { dir: options.directory }), + ...(options?.prefix === undefined ? {} : { prefix: options.prefix }) + })) + +const makeTempDirectory = makeTempDirectoryFactory("makeTempDirectory") + +const removeFactory = (method: string): FileSystem.FileSystem["remove"] => (path, options) => { + const effect = tryPromise(method, path, () => Deno.remove(path, { recursive: options?.recursive ?? false })) + return options?.force + ? Effect.catchTag( + effect, + "PlatformError", + (error) => error.reason._tag === "NotFound" ? Effect.void : Effect.fail(error) + ) + : effect +} + +const remove = removeFactory("remove") + +const makeTempDirectoryScoped: FileSystem.FileSystem["makeTempDirectoryScoped"] = (options) => + Effect.acquireRelease( + makeTempDirectoryFactory("makeTempDirectoryScoped")(options), + (directory) => Effect.orDie(removeFactory("makeTempDirectoryScoped")(directory, { recursive: true })) + ) + +const openOptions = (flag: FileSystem.OpenFlag = "r", mode?: number): Deno.OpenOptions => { + const modeOption = mode === undefined ? {} : { mode } + switch (flag) { + case "r": + return { read: true, ...modeOption } + case "r+": + return { read: true, write: true, ...modeOption } + case "w": + return { write: true, create: true, truncate: true, ...modeOption } + case "wx": + return { write: true, createNew: true, ...modeOption } + case "w+": + return { read: true, write: true, create: true, truncate: true, ...modeOption } + case "wx+": + return { read: true, write: true, createNew: true, ...modeOption } + case "a": + return { append: true, create: true, ...modeOption } + case "ax": + return { append: true, createNew: true, ...modeOption } + case "a+": + return { read: true, append: true, create: true, ...modeOption } + case "ax+": + return { read: true, append: true, createNew: true, ...modeOption } + } +} + +const makeFileInfo = (info: Deno.FileInfo): FileSystem.File.Info => ({ + type: info.isFile ? + "File" : + info.isDirectory ? + "Directory" : + info.isSymlink ? + "SymbolicLink" : + info.isBlockDevice ? + "BlockDevice" : + info.isCharDevice ? + "CharacterDevice" : + info.isFifo ? + "FIFO" : + info.isSocket ? + "Socket" : + "Unknown", + mtime: Option.fromNullishOr(info.mtime), + atime: Option.fromNullishOr(info.atime), + birthtime: Option.fromNullishOr(info.birthtime), + dev: info.dev, + rdev: Option.fromNullishOr(info.rdev), + ino: Option.fromNullishOr(info.ino), + mode: info.mode ?? 0, + nlink: Option.fromNullishOr(info.nlink), + uid: Option.fromNullishOr(info.uid), + gid: Option.fromNullishOr(info.gid), + size: FileSystem.Size(info.size), + blksize: Option.map(Option.fromNullishOr(info.blksize), FileSystem.Size), + blocks: Option.fromNullishOr(info.blocks) +}) + +/** A file handle is stateful and must not be used concurrently. */ +class FileImpl implements FileSystem.File { + readonly [FileSystem.FileTypeId]: typeof FileSystem.FileTypeId = FileSystem.FileTypeId + private readonly file: Deno.FsFile + private readonly append: boolean + private position = BigInt(0) + private nativePosition: bigint | undefined = undefined + + constructor( + file: Deno.FsFile, + append: boolean + ) { + this.file = file + this.append = append + } + + get stat() { + return Effect.map( + tryPromise("stat", undefined, () => this.file.stat()), + makeFileInfo + ) + } + + get sync() { + return tryPromise("sync", undefined, () => this.file.sync()) + } + + seek(offset: FileSystem.SizeInput, from: FileSystem.SeekMode) { + const size = FileSystem.Size(offset) + return Effect.sync(() => { + if (from === "start") { + this.position = size + } else { + this.position += size + } + return FileSystem.Size(this.position) + }) + } + + private readChunk(method: string, buffer: Uint8Array) { + return Effect.suspend(() => { + const position = this.position + return Effect.map( + tryPromise( + method, + undefined, + async () => { + if (this.nativePosition !== position) { + this.file.seekSync(position, Deno.SeekMode.Start) + } + this.nativePosition = undefined + return await this.file.read(buffer) + } + ), + (bytesRead) => { + const sizeRead = FileSystem.Size(bytesRead ?? 0) + this.position = this.nativePosition = position + sizeRead + return sizeRead + } + ) + }) + } + + read(buffer: Uint8Array) { + return this.readChunk("read", buffer) + } + + readAlloc(size: FileSystem.SizeInput) { + const sizeNumber = Number(size) + return Effect.suspend(() => { + const buffer = new Uint8Array(sizeNumber) + return Effect.map(this.readChunk("readAlloc", buffer), (bytesRead) => { + if (bytesRead === BigInt(0)) { + return Option.none() + } + return Option.some(bytesRead === BigInt(sizeNumber) ? buffer : buffer.subarray(0, Number(bytesRead))) + }) + }) + } + + truncate(length?: FileSystem.SizeInput) { + const size = FileSystem.Size(length ?? 0) + return Effect.map( + tryPromise("truncate", undefined, () => this.file.truncate(Number(size))), + () => { + if (!this.append && this.position > size) { + this.position = size + } + } + ) + } + + private writeChunk(method: string, buffer: Uint8Array) { + return Effect.suspend(() => { + const position = this.position + return Effect.map( + tryPromise( + method, + undefined, + async () => { + if (!this.append && this.nativePosition !== position) { + this.file.seekSync(position, Deno.SeekMode.Start) + } + this.nativePosition = undefined + return await this.file.write(buffer) + } + ), + (bytesWritten) => { + const sizeWritten = FileSystem.Size(bytesWritten) + if (this.append) { + this.nativePosition = undefined + } else { + this.position = this.nativePosition = position + sizeWritten + } + return sizeWritten + } + ) + }) + } + + write(buffer: Uint8Array) { + return this.writeChunk("write", buffer) + } + + private writeAllChunk(buffer: Uint8Array): Effect.Effect { + return Effect.flatMap(this.writeChunk("writeAll", buffer), (bytesWritten) => { + if (bytesWritten === BigInt(0)) { + return Effect.fail(PlatformError.systemError({ + module: "FileSystem", + method: "writeAll", + _tag: "WriteZero", + description: "write returned 0 bytes written" + })) + } + return bytesWritten < buffer.length + ? this.writeAllChunk(buffer.subarray(Number(bytesWritten))) + : Effect.void + }) + } + + writeAll(buffer: Uint8Array) { + return buffer.length === 0 ? Effect.void : this.writeAllChunk(buffer) + } +} + +const open: FileSystem.FileSystem["open"] = (path, options) => { + const append = options?.flag?.startsWith("a") ?? false + return Effect.map( + Effect.acquireRelease( + tryPromise("open", path, () => Deno.open(path, openOptions(options?.flag, options?.mode))), + (file) => close(file, "open", path) + ), + (file) => new FileImpl(file, append) + ) +} + +const makeTempFileFactory = (method: string): FileSystem.FileSystem["makeTempFile"] => (options) => + tryPromise(method, options?.directory, () => + Deno.makeTempFile({ + ...(options?.directory === undefined ? {} : { dir: options.directory }), + ...(options?.prefix === undefined ? {} : { prefix: options.prefix }), + ...(options?.suffix === undefined ? {} : { suffix: options.suffix }) + })) + +const makeTempFile = makeTempFileFactory("makeTempFile") + +const makeTempFileScoped: FileSystem.FileSystem["makeTempFileScoped"] = (options) => + Effect.acquireRelease( + makeTempFileFactory("makeTempFileScoped")(options), + (file) => Effect.orDie(removeFactory("makeTempFileScoped")(file, { force: true })) + ) + +const readDirectory: FileSystem.FileSystem["readDirectory"] = (path, options) => { + if (options?.recursive) { + return Effect.map( + collectAsyncIterable("readDirectory", path, () => walk(path)), + (entries) => entries.map((entry) => relative(path, entry.path)).filter(Boolean) + ) + } + return Effect.map( + collectAsyncIterable("readDirectory", path, () => Deno.readDir(path)), + (entries) => entries.map((entry) => entry.name) + ) +} + +const readFile: FileSystem.FileSystem["readFile"] = (path) => + tryPromise("readFile", path, (signal) => Deno.readFile(path, { signal })) + +const readLink: FileSystem.FileSystem["readLink"] = (path) => tryPromise("readLink", path, () => Deno.readLink(path)) + +const realPath: FileSystem.FileSystem["realPath"] = (path) => tryPromise("realPath", path, () => Deno.realPath(path)) + +const rename: FileSystem.FileSystem["rename"] = (oldPath, newPath) => + tryPromise("rename", oldPath, () => Deno.rename(oldPath, newPath)) + +const stat: FileSystem.FileSystem["stat"] = (path) => + Effect.map( + tryPromise("stat", path, () => Deno.stat(path)), + makeFileInfo + ) + +const symlink: FileSystem.FileSystem["symlink"] = (target, path) => + tryPromise("symlink", target, () => Deno.symlink(target, path)) + +const truncate: FileSystem.FileSystem["truncate"] = (path, length) => + tryPromise("truncate", path, () => Deno.truncate(path, length === undefined ? undefined : Number(length))) + +const utimes: FileSystem.FileSystem["utimes"] = (path, atime, mtime) => + tryPromise("utimes", path, () => Deno.utime(path, atime, mtime)) + +const watchNative = ( + path: string, + options?: FileSystem.WatchOptions +): Stream.Stream => + Stream.unwrap( + Effect.map( + Effect.try({ + try: () => Deno.watchFs(path, { recursive: options?.recursive ?? false }), + catch: handleError("FileSystem", "watch", path) + }), + (watcher) => + Stream.fromAsyncIterable(watcher, handleError("FileSystem", "watch", path)).pipe( + Stream.flatMap((event): Stream.Stream => { + switch (event.kind) { + case "create": + return Stream.map(Stream.fromIterable(event.paths), (path) => ({ _tag: "Create" as const, path })) + case "modify": + return Stream.map(Stream.fromIterable(event.paths), (path) => ({ _tag: "Update" as const, path })) + case "remove": + return Stream.map(Stream.fromIterable(event.paths), (path) => ({ _tag: "Remove" as const, path })) + case "rename": + return Stream.mapEffect(Stream.fromIterable(event.paths), (path) => + Effect.match(stat(path), { + onFailure: () => ({ _tag: "Remove" as const, path }), + onSuccess: () => ({ _tag: "Create" as const, path }) + })) + default: + return Stream.empty + } + }) + ) + ) + ) + +const watch = ( + backend: Option.Option, + path: string, + options?: FileSystem.WatchOptions +) => + stat(path).pipe( + Effect.map((info) => + backend.pipe( + Option.flatMap((backend) => backend.register(path, info, options)), + Option.getOrElse(() => watchNative(path, options)) + ) + ), + Stream.unwrap + ) + +const writeFile: FileSystem.FileSystem["writeFile"] = (path, data, options) => { + const flag = options?.flag ?? "w" + if (flag === "w" || flag === "wx" || flag === "a" || flag === "ax") { + return tryPromise("writeFile", path, (signal) => + Deno.writeFile(path, data, { + append: flag.startsWith("a"), + createNew: flag.includes("x"), + ...(options?.mode === undefined ? {} : { mode: options.mode }), + signal + })) + } + return Effect.acquireUseRelease( + tryPromise("writeFile", path, () => Deno.open(path, openOptions(flag, options?.mode))), + (file) => + new FileImpl(file, flag.startsWith("a")).writeAll(data).pipe( + Effect.mapError((error) => + error.reason._tag !== "BadArgument" + ? PlatformError.systemError({ ...error.reason, method: "writeFile", pathOrDescriptor: path }) + : error + ) + ), + (file) => close(file, "writeFile", path) + ) +} + +const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend), (backend) => + FileSystem.make({ + access, + chmod, + chown, + copy, + copyFile, + glob, + 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 + })) + +/** + * Provides the `FileSystem` service backed by Deno filesystem APIs. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect(FileSystem.FileSystem)(makeFileSystem) diff --git a/.context/effect/packages/platform/deno/src/DenoHttpClient.ts b/.context/effect/packages/platform/deno/src/DenoHttpClient.ts new file mode 100644 index 000000000..34232a7de --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoHttpClient.ts @@ -0,0 +1,9 @@ +/** + * @since 4.0.0 + */ + +/** + * @category re-exports + * @since 4.0.0 + */ +export * from "effect/unstable/http/FetchHttpClient" diff --git a/.context/effect/packages/platform/deno/src/DenoHttpPlatform.ts b/.context/effect/packages/platform/deno/src/DenoHttpPlatform.ts new file mode 100644 index 000000000..f0dd88f78 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoHttpPlatform.ts @@ -0,0 +1,110 @@ +/** + * Deno implementation of the Effect HTTP platform service. + * + * File responses use `Deno.FsFile.readable` directly so full-file responses + * retain Deno's resource-backed fast path. Deno closes the file after the body + * is sent, but server adapters must cancel a raw body when they do not send it, + * including when handling `HEAD` requests. + * + * The provided layer uses strong ETags, unlike the portable `HttpPlatform` + * layer, which uses weak ETags. + * + * @since 4.0.0 + */ +import * as NodeHttpCompression from "@effect/platform-node-shared/NodeHttpCompression" +import { contentType } from "@std/media-types" +import { extname } from "@std/path" +import { ByteSliceStream } from "@std/streams" +import * as Layer from "effect/Layer" +import * as Etag from "effect/unstable/http/Etag" +import * as Platform from "effect/unstable/http/HttpPlatform" +import * as Response from "effect/unstable/http/HttpServerResponse" +import * as DenoFileSystem from "./DenoFileSystem.ts" + +// gzip and deflate use the native CompressionStream, which does not expose a +// per-chunk flush control. br and zstd go through node:zlib compatibility +// streams and flush each input chunk. +const compression = NodeHttpCompression.make(Platform.makeCompressionWeb({ + algorithms: NodeHttpCompression.algorithms, + transform: (algorithm, options) => + algorithm === "gzip" || algorithm === "deflate" + ? Platform.compressionTransformWeb(algorithm) + : NodeHttpCompression.compressTransformWeb(algorithm, options) +})) + +/** + * Creates the Deno `HttpPlatform`, serving file responses from resource-backed + * readable streams and adding content type and content length headers. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Platform.make({ + platform: "deno", + compression, + fileResponse(path, status, statusText, headers, start, end, contentLength) { + let body: ReadableStream + if (contentLength === 0) { + body = new ReadableStream({ + start(controller) { + controller.close() + } + }) + } else { + const file = Deno.openSync(path) + file.seekSync(start, Deno.SeekMode.Start) + body = end === undefined + ? file.readable + : file.readable.pipeThrough(new ByteSliceStream(0, contentLength - 1)) + } + return Response.raw(body, { + headers: { + ...headers, + "content-type": headers["content-type"] ?? contentType(extname(path)) ?? "application/octet-stream", + "content-length": contentLength.toString() + }, + status, + statusText + }) + }, + fileWebResponse(file, status, statusText, headers, options) { + const offset = Number(options?.offset ?? 0) + const available = Math.max(0, file.size - offset) + const contentLength = options?.bytesToRead === undefined + ? available + : Math.min(available, Math.max(0, Number(options.bytesToRead))) + let body: typeof file | ReadableStream = file + if (contentLength === 0) { + body = new ReadableStream({ + start(controller) { + controller.close() + } + }) + } else if (offset > 0 || options?.bytesToRead !== undefined) { + body = (file.stream() as ReadableStream).pipeThrough( + new ByteSliceStream(offset, offset + contentLength - 1) + ) + } + return Response.raw(body, { + headers: { + ...headers, + "content-type": file.type, + "content-length": contentLength.toString() + }, + status, + statusText + }) + } +}) + +/** + * Provides the Deno `HttpPlatform` together with its filesystem and strong ETag + * services. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect(Platform.HttpPlatform)(make).pipe( + Layer.provide(DenoFileSystem.layer), + Layer.provide(Etag.layer) +) diff --git a/.context/effect/packages/platform/deno/src/DenoHttpServer.ts b/.context/effect/packages/platform/deno/src/DenoHttpServer.ts new file mode 100644 index 000000000..a21262711 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoHttpServer.ts @@ -0,0 +1,535 @@ +/** + * Native Deno implementation of the Effect `HttpServer`. + * + * @since 4.0.0 + */ +import * as Config from "effect/Config" +import type { ConfigError } from "effect/Config" +import * as Context from "effect/Context" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import type * as FileSystem from "effect/FileSystem" +import { flow } from "effect/Function" +import * as Inspectable from "effect/Inspectable" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import type * as Path from "effect/Path" +import type * as Record from "effect/Record" +import type * as Schema from "effect/Schema" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Cookies from "effect/unstable/http/Cookies" +import * as Etag from "effect/unstable/http/Etag" +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" +import * as Headers from "effect/unstable/http/Headers" +import type * as HttpBody from "effect/unstable/http/HttpBody" +import type { HttpClient } from "effect/unstable/http/HttpClient" +import * as HttpEffect from "effect/unstable/http/HttpEffect" +import * as IncomingMessage from "effect/unstable/http/HttpIncomingMessage" +import type { HttpMethod } from "effect/unstable/http/HttpMethod" +import type { HttpPlatform } from "effect/unstable/http/HttpPlatform" +import * as Server from "effect/unstable/http/HttpServer" +import * as Error from "effect/unstable/http/HttpServerError" +import * as ServerRequest from "effect/unstable/http/HttpServerRequest" +import type * as ServerResponse from "effect/unstable/http/HttpServerResponse" +import type * as Multipart from "effect/unstable/http/Multipart" +import * as UrlParams from "effect/unstable/http/UrlParams" +import * as Socket from "effect/unstable/socket/Socket" +import * as Platform from "./DenoHttpPlatform.ts" +import * as DenoMultipart from "./DenoMultipart.ts" +import * as DenoServices from "./DenoServices.ts" + +/** + * Native Deno TCP, TLS, or Unix serve options managed by the scoped server. + * + * **Details** + * + * `signal` and `onError` are omitted because the scope and Effect HTTP handler + * own server and failure lifecycles. Server constructors additionally accept + * WebSocket settings that apply to every upgrade; `protocol` therefore cannot + * vary per request. + * + * @category options + * @since 4.0.0 + */ +export type ServeOptions = + | (Omit & Partial) + | Omit + +/** + * Creates a scoped native Deno HTTP server. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Effect.fnUntraced(function*( + options: ServeOptions & { + readonly disablePreemptiveShutdown?: boolean | undefined + readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: Deno.UpgradeWebSocketOptions | undefined + } +) { + const scope = yield* Effect.scope + type Handler = (request: Request, info: Deno.ServeHandlerInfo) => Promise + const notFound: Handler = (_request, _info) => Promise.resolve(new Response("not found", { status: 404 })) + const handlerStack: Array = [notFound] + let currentHandler = notFound + const { + disablePreemptiveShutdown, + gracefulShutdownTimeout, + websocket, + ...serveOptions + } = options + const server = Deno.serve( + serveOptions as Deno.ServeTcpOptions, + (request, info) => currentHandler(request, info) + ) as Deno.HttpServer + + const shutdown = yield* Effect.promise(() => server.shutdown()).pipe(Effect.cached) + const preemptiveShutdown = disablePreemptiveShutdown ? Effect.void : Effect.timeoutOrElse(shutdown, { + duration: gracefulShutdownTimeout ?? Duration.seconds(20), + orElse: () => Effect.void + }) + + yield* Scope.addFinalizer(scope, shutdown) + + const serverAddress = server.addr + const address: Server.Address = serverAddress.transport === "unix" + ? { _tag: "UnixAddress", path: serverAddress.path } + : { + _tag: "TcpAddress", + port: (serverAddress as Deno.NetAddr).port, + hostname: (serverAddress as Deno.NetAddr).hostname + } + + return Server.make({ + address, + serve: Effect.fnUntraced(function*(httpApp, middleware) { + const parent = yield* Effect.fiber + const services = parent.context + const serveScope = Context.getUnsafe(services, Scope.Scope) + const scope = Scope.forkUnsafe(serveScope, "parallel") + + const httpEffect = HttpEffect.toHandled(httpApp, (request, response) => { + const denoRequest = request as DenoServerRequest + if (denoRequest.upgraded) return cancelResponseBody(response.body) + return Effect.flatMap( + makeResponse(request, response, services, scope), + (response) => Effect.sync(() => denoRequest.resolve(response)) + ) + }, middleware) + + function handler( + request: Request, + info: Deno.ServeHandlerInfo + ): Promise { + return new Promise((resolve) => { + const context = Context.add( + services, + ServerRequest.HttpServerRequest, + new DenoServerRequest(request, info.remoteAddr, resolve, removeHost(request.url), websocket) + ) + const fiber = Fiber.runIn(Effect.runForkWith(context)(httpEffect), scope) + request.signal.addEventListener("abort", () => { + fiber.interruptUnsafe(parent.id, Error.ClientAbort.annotation) + }, { once: true }) + }) + } + + yield* Scope.addFinalizerExit(serveScope, () => { + const index = handlerStack.lastIndexOf(handler) + if (index !== -1) handlerStack.splice(index, 1) + currentHandler = handlerStack[handlerStack.length - 1] ?? notFound + return preemptiveShutdown + }) + handlerStack.push(handler) + currentHandler = handler + }) + }) +}) + +const makeResponse = Effect.fnUntraced(function*( + request: ServerRequest.HttpServerRequest, + response: ServerResponse.HttpServerResponse, + context: Context.Context, + scope: Scope.Scope +) { + 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") { + yield* cancelResponseBody(response.body) + return new Response(undefined, fields) + } + response = HttpEffect.scopeTransferToStream(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.toReadableStreamWith( + Stream.unwrap(Effect.withFiber((fiber) => { + Fiber.runIn(fiber, scope) + return Effect.succeed(body.stream) + })), + context + ), + fields + ) + } +}) + +/** + * Provides only the native Deno HTTP server. + * + * @category layers + * @since 4.0.0 + */ +export const layerServer: ( + options: ServeOptions & { + readonly disablePreemptiveShutdown?: boolean | undefined + readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: Deno.UpgradeWebSocketOptions | undefined + } +) => Layer.Layer = flow( + make, + Layer.effect(Server.HttpServer) +) + +/** + * Provides Deno HTTP platform services and the standard Deno service set. + * + * @category layers + * @since 4.0.0 + */ +export const layerHttpServices: Layer.Layer = Layer.mergeAll( + Platform.layer, + Etag.layerWeak, + DenoServices.layer +) + +/** + * Provides a native Deno HTTP server together with Deno HTTP services. + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + options: ServeOptions & { + readonly disablePreemptiveShutdown?: boolean | undefined + readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: Deno.UpgradeWebSocketOptions | undefined + } +): Layer.Layer => + Layer.mergeAll(layerServer(options), layerHttpServices) + +/** + * Starts a Deno HTTP server on an ephemeral loopback port for tests. + * + * @category testing + * @since 4.0.0 + */ +export const layerTest: Layer.Layer< + Server.HttpServer | HttpPlatform | FileSystem.FileSystem | Etag.Generator | Path.Path | HttpClient +> = Server.layerTestClient.pipe( + Layer.provide(FetchHttpClient.layer.pipe( + Layer.provide(Layer.succeed(FetchHttpClient.RequestInit)({ keepalive: false })) + )), + Layer.provideMerge(layer({ hostname: "127.0.0.1", port: 0, onListen: () => {} })) +) + +/** + * Creates the Deno HTTP server and support-services layer from configurable options. + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = ( + options: Config.Wrap< + ServeOptions & { + readonly disablePreemptiveShutdown?: boolean | undefined + readonly gracefulShutdownTimeout?: Duration.Input | undefined + readonly websocket?: Deno.UpgradeWebSocketOptions | undefined + } + > +): Layer.Layer< + Server.HttpServer | HttpPlatform | FileSystem.FileSystem | Etag.Generator | Path.Path, + ConfigError +> => + Layer.mergeAll( + Layer.effect(Server.HttpServer)(Effect.flatMap(Config.unwrap(options), make)), + layerHttpServices + ) + +class DenoServerRequest extends Inspectable.Class implements ServerRequest.HttpServerRequest { + readonly [ServerRequest.TypeId]: typeof ServerRequest.TypeId + readonly [IncomingMessage.TypeId]: typeof IncomingMessage.TypeId + readonly source: Request + readonly remoteAddr: Deno.NetAddr | Deno.UnixAddr + readonly url: string + readonly websocketOptions: Deno.UpgradeWebSocketOptions | undefined + public resolve: (response: Response) => void + public upgraded = false + public headersOverride?: Headers.Headers | undefined + private remoteAddressOverride?: Option.Option | undefined + + constructor( + source: Request, + remoteAddr: Deno.NetAddr | Deno.UnixAddr, + resolve: (response: Response) => void, + url: string, + websocketOptions: Deno.UpgradeWebSocketOptions | undefined, + headersOverride?: Headers.Headers, + remoteAddressOverride?: Option.Option + ) { + super() + this[ServerRequest.TypeId] = ServerRequest.TypeId + this[IncomingMessage.TypeId] = IncomingMessage.TypeId + this.source = source + this.remoteAddr = remoteAddr + this.resolve = resolve + this.url = url + this.websocketOptions = websocketOptions + this.headersOverride = headersOverride + this.remoteAddressOverride = remoteAddressOverride + } + toJSON(): unknown { + return IncomingMessage.inspect(this, { + _id: "HttpServerRequest", + method: this.method, + url: this.originalUrl + }) + } + modify(options: { + readonly url?: string | undefined + readonly headers?: Headers.Headers | undefined + readonly remoteAddress?: Option.Option | undefined + }) { + return new DenoServerRequest( + this.source, + this.remoteAddr, + this.resolve, + options.url ?? this.url, + this.websocketOptions, + options.headers ?? this.headersOverride, + "remoteAddress" in options ? 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 ?? (this.remoteAddr.transport === "tcp" + ? Option.some(this.remoteAddr.hostname) + : Option.none()) + } + get headers(): Headers.Headers { + this.headersOverride ??= Headers.fromInput(this.source.headers) + return this.headersOverride + } + + private cachedCookies: Record.ReadonlyRecord | undefined + get cookies() { + return this.cachedCookies ??= Cookies.parseHeader(this.headers.cookie ?? "") + } + + get stream(): Stream.Stream { + return this.source.body + ? Stream.fromReadableStream({ + evaluate: () => this.source.body ?? emptyReadableStream, + onError: (cause) => + new Error.HttpServerError({ + reason: new Error.RequestParseError({ request: this, cause }) + }) + }) + : Stream.fail( + new Error.HttpServerError({ + reason: new Error.RequestParseError({ + request: this, + description: "can not create stream from empty body" + }) + }) + ) + } + + private textEffect: Effect.Effect | undefined + get text(): Effect.Effect { + return this.textEffect ??= Effect.runSync(Effect.cached( + Effect.tryPromise({ + try: () => this.source.text(), + catch: (cause) => + new Error.HttpServerError({ + reason: new Error.RequestParseError({ request: this, cause }) + }) + }) + )) + } + get json(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => JSON.parse(_) as Schema.Json, + catch: (cause) => + new Error.HttpServerError({ + reason: new Error.RequestParseError({ request: this, cause }) + }) + })) + } + get urlParamsBody(): Effect.Effect { + return Effect.flatMap(this.text, (_) => + Effect.try({ + try: () => UrlParams.fromInput(new URLSearchParams(_)), + catch: (cause) => + new Error.HttpServerError({ + reason: new Error.RequestParseError({ request: this, cause }) + }) + })) + } + + private multipartEffect: + | Effect.Effect + | undefined + get multipart(): Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + Scope.Scope | FileSystem.FileSystem | Path.Path + > { + return this.multipartEffect ??= Effect.runSync(Effect.cached(DenoMultipart.persisted(this.source))) + } + get multipartStream(): Stream.Stream { + return DenoMultipart.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.HttpServerError({ + reason: new Error.RequestParseError({ request: this, cause }) + }) + }) + )) + this.textEffect = Effect.map(this.arrayBufferEffect, (_) => new TextDecoder().decode(_)) + return this.arrayBufferEffect + } + + get upgrade(): Effect.Effect { + return Effect.flatMap( + Effect.try({ + try: () => Deno.upgradeWebSocket(this.source, this.websocketOptions), + catch: (cause) => + new Error.HttpServerError({ + reason: new Error.RequestParseError({ + request: this, + cause, + description: "Not an upgradeable ServerRequest" + }) + }) + }), + (upgrade) => { + const buffered: Array = [] + const buffer = (event: MessageEvent) => buffered.push(event) + upgrade.socket.addEventListener("message", buffer) + this.upgraded = true + this.resolve(upgrade.response) + + return Effect.callback((resume) => { + const cleanup = () => { + upgrade.socket.removeEventListener("open", onOpen) + upgrade.socket.removeEventListener("error", onFailure) + upgrade.socket.removeEventListener("close", onFailure) + } + const onFailure = (cause: Event) => { + cleanup() + upgrade.socket.removeEventListener("message", buffer) + buffered.length = 0 + resume(Effect.fail( + new Error.HttpServerError({ + reason: new Error.RequestParseError({ + request: this, + cause, + description: "WebSocket upgrade failed before open" + }) + }) + )) + } + const onOpen = () => { + cleanup() + resume(Socket.fromWebSocket( + Effect.acquireRelease( + Effect.succeed(upgrade.socket), + (socket) => Effect.sync(() => socket.close(1000)) + ), + { + onInitialRun: (socket) => { + socket.removeEventListener("message", buffer) + return buffered.splice(0) + } + } + )) + } + upgrade.socket.addEventListener("open", onOpen, { once: true }) + upgrade.socket.addEventListener("error", onFailure, { once: true }) + upgrade.socket.addEventListener("close", onFailure, { once: true }) + return Effect.sync(() => { + cleanup() + upgrade.socket.removeEventListener("message", buffer) + buffered.length = 0 + upgrade.socket.close() + }) + }) + } + ) + } +} + +const cancelResponseBody = (body: HttpBody.HttpBody): Effect.Effect => { + const stream = (body as any).body + if ((body._tag === "Raw" || body._tag === "Uint8Array") && stream instanceof ReadableStream) { + return Effect.ignoreCause(Effect.promise(() => stream.cancel())) + } + return Effect.void +} + +const emptyReadableStream = new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array()) + controller.close() + } +}) + +const removeHost = (url: string) => { + if (url[0] === "/") return url + const index = url.indexOf("/", url.indexOf("//") + 2) + return index === -1 ? "/" : url.slice(index) +} diff --git a/.context/effect/packages/platform/deno/src/DenoHttpServerRequest.ts b/.context/effect/packages/platform/deno/src/DenoHttpServerRequest.ts new file mode 100644 index 000000000..d9fc05918 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoHttpServerRequest.ts @@ -0,0 +1,18 @@ +/** + * Accessor for the web-standard request behind a Deno HTTP server request. + * + * Unlike Bun's accessor, this function has no route-pattern generic because + * `Deno.serve` always receives a standard `Request`. Connection information is + * intentionally kept internal to `DenoHttpServer`. + * + * @since 4.0.0 + */ +import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" + +/** + * Returns the underlying web-standard `Request` from an Effect `HttpServerRequest`. + * + * @category accessors + * @since 4.0.0 + */ +export const toDenoServerRequest = (self: HttpServerRequest): Request => (self as any).source diff --git a/.context/effect/packages/platform/deno/src/DenoKeyValueStore.ts b/.context/effect/packages/platform/deno/src/DenoKeyValueStore.ts new file mode 100644 index 000000000..cc221c48c --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoKeyValueStore.ts @@ -0,0 +1,28 @@ +/** + * Deno-backed `KeyValueStore` layers using Web Storage APIs. + * + * @since 4.0.0 + */ + +import type * as Layer from "effect/Layer" +import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" + +/** + * Creates a `KeyValueStore` layer backed by `localStorage`, with values stored between sessions. + * + * @category layers + * @since 4.0.0 + */ +export const layerLocalStorage: Layer.Layer = KeyValueStore.layerStorage(() => + localStorage +) + +/** + * Creates a `KeyValueStore` layer backed by `sessionStorage`, with values stored only for the current session. + * + * @category layers + * @since 4.0.0 + */ +export const layerSessionStorage: Layer.Layer = KeyValueStore.layerStorage(() => + sessionStorage +) diff --git a/.context/effect/packages/platform/deno/src/DenoMultipart.ts b/.context/effect/packages/platform/deno/src/DenoMultipart.ts new file mode 100644 index 000000000..8189c61a1 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoMultipart.ts @@ -0,0 +1,53 @@ +/** + * Web-standard helpers for parsing HTTP `multipart/form-data` request bodies. + * + * This module mirrors `BunMultipart`, adapting a web `Request` body and headers + * into the shared `Multipart` model. `stream` returns multipart parts as a + * `Stream`, while `persisted` collects the form and writes file parts to scoped + * temporary files through the current `FileSystem`, `Path`, and `Scope` + * services. + * + * @since 4.0.0 + */ +import type * as Effect from "effect/Effect" +import type { FileSystem } from "effect/FileSystem" +import type { Path } from "effect/Path" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Multipart from "effect/unstable/http/Multipart" + +/** + * Parses a web `Request` body as multipart data and returns a stream of multipart parts. + * + * @category constructors + * @since 4.0.0 + */ +export const stream = (source: Request): Stream.Stream => + Stream.fromReadableStream({ + evaluate: () => + source.body ?? new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array()) + controller.close() + } + }), + onError: (cause) => Multipart.MultipartError.fromReason("InternalError", cause) + }).pipe( + Stream.pipeThroughChannel(Multipart.makeChannel(Object.fromEntries(source.headers))) + ) + +/** + * Parses and persists multipart data from a web `Request`, requiring file-system, path, and scope services. + * + * @category constructors + * @since 4.0.0 + */ +export const persisted = ( + source: Request +): Effect.Effect< + Multipart.Persisted, + Multipart.MultipartError, + | FileSystem + | Path + | Scope.Scope +> => Multipart.toPersisted(stream(source)) diff --git a/.context/effect/packages/platform/deno/src/DenoPath.ts b/.context/effect/packages/platform/deno/src/DenoPath.ts new file mode 100644 index 000000000..fcf4e4d92 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoPath.ts @@ -0,0 +1,102 @@ +/** + * Path service layers backed by the Deno Standard Library. + * + * **Example** (Using Deno path operations) + * + * ```ts import.meta.vitest + * import { Effect, Path } from "effect" + * import { DenoPath } from "@effect/platform-deno" + * + * const program = Effect.gen(function*() { + * const path = yield* Path.Path + * return path.extname("file.txt") + * }).pipe(Effect.provide(DenoPath.layer)) + * + * Effect.runSync(program) // => ".txt" + * ``` + * + * @since 4.0.0 + */ + +import * as DenoPath from "@std/path" +import * as DenoPathPosix from "@std/path/posix" +import * as DenoPathWin from "@std/path/windows" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as PlatformError from "effect/PlatformError" + +const fileUrlOps = (impl: { + readonly fromFileUrl: typeof DenoPath.fromFileUrl + readonly toFileUrl: typeof DenoPath.toFileUrl +}) => ({ + fromFileUrl: (url: URL): Effect.Effect => + Effect.try({ + try: () => impl.fromFileUrl(url), + catch: (cause) => + new PlatformError.BadArgument({ + module: "Path", + method: "fromFileUrl", + cause + }) + }), + toFileUrl: (path: string): Effect.Effect => + Effect.try({ + try: (): URL => impl.toFileUrl(path), + catch: (cause): PlatformError.BadArgument => + new PlatformError.BadArgument({ + module: "Path", + method: "toFileUrl", + cause + }) + }) +}) + +/** + * A {@linkplain Layer.Layer | layer} that provides POSIX path operations. + * + * @category layers + * @since 4.0.0 + */ +export const layerPosix: Layer.Layer = Layer.succeed(Path.Path)( + Path.Path.of({ + [Path.TypeId]: Path.TypeId, + ...DenoPathPosix, + sep: DenoPathPosix.SEPARATOR, + ...fileUrlOps(DenoPathPosix) + }) +) + +/** + * A {@linkplain Layer.Layer | layer} that provides Windows path operations. + * + * @category layers + * @since 4.0.0 + */ +export const layerWin32: Layer.Layer = Layer.succeed( + Path.Path +)( + Path.Path.of({ + [Path.TypeId]: Path.TypeId, + ...DenoPathWin, + sep: DenoPathWin.SEPARATOR, + ...fileUrlOps(DenoPathWin) + }) +) + +/** + * A {@linkplain Layer.Layer | layer} that provides OS-agnostic path operations. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.succeed( + Path.Path +)( + Path.Path.of({ + [Path.TypeId]: Path.TypeId, + ...DenoPath, + sep: DenoPath.SEPARATOR, + ...fileUrlOps(DenoPath) + }) +) diff --git a/.context/effect/packages/platform/deno/src/DenoRedis.ts b/.context/effect/packages/platform/deno/src/DenoRedis.ts new file mode 100644 index 000000000..5c41ba9f5 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoRedis.ts @@ -0,0 +1,113 @@ +/** + * Deno Redis integration backed by `@db/redis`. + * + * This module creates a scoped, native Deno Redis client and exposes it as + * both the portable `Redis` service and the Deno-specific {@link DenoRedis} + * service for direct access to the raw client. Unlike Bun's built-in client, + * `@db/redis` connects eagerly using RESP2, so layer construction can fail + * with a `RedisError`. + * + * @since 4.0.0 + */ +import { connect, parseURL, type Redis as RedisClient, type RedisConnectOptions } from "@db/redis" +import * as Config from "effect/Config" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Fn from "effect/Function" +import * as Layer from "effect/Layer" +import * as Predicate from "effect/Predicate" +import * as Record from "effect/Record" +import * as Redis from "effect/unstable/persistence/Redis" + +/** + * Options for connecting to Redis, including a Redis URL or individual + * connection settings. Explicit settings override values from the URL. + * + * @category models + * @since 4.0.0 + */ +export type RedisOptions = Omit & { + readonly hostname?: string + readonly url?: string +} + +/** + * Service tag for Deno Redis integration, exposing the raw `@db/redis` client + * and a `use` helper that maps client promise failures to `RedisError`. + * + * @category services + * @since 4.0.0 + */ +export class DenoRedis extends Context.Service(f: (client: RedisClient) => Promise) => Effect.Effect +}>()("@effect/platform-deno/DenoRedis") {} + +const make = Effect.fnUntraced(function*(options: RedisOptions = {}) { + const client = yield* Effect.acquireRelease( + Effect.tryPromise({ + try: () => { + const { url, ...connectOptions } = options + const { name, ...parsed } = url === undefined ? { hostname: "localhost" } : parseURL(url) + return connect({ + ...parsed, + ...(name === undefined ? {} : { username: name }), + ...Record.filter(connectOptions, Predicate.isNotUndefined) + }) + }, + catch: (cause) => new Redis.RedisError({ cause }) + }), + (client) => Effect.sync(() => client.close()) + ) + + const use = (f: (client: RedisClient) => Promise) => + Effect.tryPromise({ + try: () => f(client), + catch: (cause) => new Redis.RedisError({ cause }) + }) + + const redis = yield* Redis.make({ + send: (command: string, ...args: ReadonlyArray) => + Effect.tryPromise({ + try: () => client.sendCommand(command, args as Array) as Promise, + catch: (cause) => new Redis.RedisError({ cause }) + }) + }) + + const denoRedis = Fn.identity({ + client, + use + }) + + return Context.make(DenoRedis, denoRedis).pipe( + Context.add(Redis.Redis, redis) + ) +}) + +/** + * Provides `Redis` and `DenoRedis` services backed by an `@db/redis` client, + * closing the client when the layer scope ends. URL-derived options can be + * overridden by other supplied options. + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + options?: RedisOptions | undefined +): Layer.Layer => Layer.effectContext(make(options)) + +/** + * Provides `Redis` and `DenoRedis` services from `Config`-backed options, + * closing the client when the layer scope ends. + * + * @category layers + * @since 4.0.0 + */ +export const layerConfig = ( + options: Config.Wrap +): Layer.Layer => + Layer.effectContext( + Config.unwrap(options).pipe( + Effect.flatMap(make) + ) + ) diff --git a/.context/effect/packages/platform/deno/src/DenoRuntime.ts b/.context/effect/packages/platform/deno/src/DenoRuntime.ts new file mode 100644 index 000000000..2b4355026 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoRuntime.ts @@ -0,0 +1,59 @@ +/** + * Deno helper for running a root Effect program. + * + * @since 4.0.0 + */ + +import type { Effect } from "effect/Effect" +import * as Runtime from "effect/Runtime" + +/** + * Run an Effect as the entrypoint to a Deno application. + * + * @category running + * @since 4.0.0 + */ + +export const runMain: { + ( + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly teardown?: Runtime.Teardown | undefined + } + ): (effect: Effect) => void + ( + effect: Effect, + options?: { + readonly disableErrorReporting?: boolean | undefined + readonly teardown?: Runtime.Teardown | undefined + } + ): void +} = Runtime.makeRunMain(({ + fiber, + teardown +}) => { + let receivedSignal = false + + fiber.addObserver((exit) => { + if (!receivedSignal) { + Deno.removeSignalListener("SIGINT", onSigint) + Deno.removeSignalListener("SIGTERM", onSigint) + } + + teardown(exit, (code) => { + if (receivedSignal || code !== 0) { + Deno.exit(code) + } + }) + }) + + function onSigint(): void { + receivedSignal = true + Deno.removeSignalListener("SIGINT", onSigint) + Deno.removeSignalListener("SIGTERM", onSigint) + fiber.interruptUnsafe(fiber.id) + } + + Deno.addSignalListener("SIGINT", onSigint) + Deno.addSignalListener("SIGTERM", onSigint) +}) diff --git a/.context/effect/packages/platform/deno/src/DenoServices.ts b/.context/effect/packages/platform/deno/src/DenoServices.ts new file mode 100644 index 000000000..17cd7dbef --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoServices.ts @@ -0,0 +1,49 @@ +/** + * Aggregate Deno platform services layer. + * + * This module defines the `DenoServices` union and a single `layer` that + * provides Deno-backed child process spawning, crypto, filesystem, path, stdio, + * and terminal services. Use the layer when a Deno program wants the standard + * platform services from one place. + * + * @since 4.0.0 + */ +import type { Crypto } from "effect/Crypto" +import type { FileSystem } from "effect/FileSystem" +import * as Layer from "effect/Layer" +import type { Path } from "effect/Path" +import type { Stdio } from "effect/Stdio" +import type { Terminal } from "effect/Terminal" +import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import * as DenoChildProcessSpawner from "./DenoChildProcessSpawner.ts" +import * as DenoCrypto from "./DenoCrypto.ts" +import * as DenoFileSystem from "./DenoFileSystem.ts" +import * as DenoPath from "./DenoPath.ts" +import * as DenoStdio from "./DenoStdio.ts" +import * as DenoTerminal from "./DenoTerminal.ts" + +/** + * The union of core services provided by the Deno platform layer, including + * child process spawning, crypto, filesystem, path, stdio, and terminal services. + * + * @category models + * @since 4.0.0 + */ +export type DenoServices = ChildProcessSpawner | Crypto | FileSystem | Path | Terminal | Stdio + +/** + * Provides the default Deno implementations for child process spawning, + * crypto, filesystem, path, stdio, and terminal services. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = DenoChildProcessSpawner.layer.pipe( + Layer.provideMerge(Layer.mergeAll( + DenoFileSystem.layer, + DenoCrypto.layer, + DenoPath.layer, + DenoStdio.layer, + DenoTerminal.layer + )) +) diff --git a/.context/effect/packages/platform/deno/src/DenoSocket.ts b/.context/effect/packages/platform/deno/src/DenoSocket.ts new file mode 100644 index 000000000..fa2e94e9a --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoSocket.ts @@ -0,0 +1,318 @@ +/** + * Native Deno socket adapters for Effect sockets. + * + * This module uses Deno-specific names: `makeTcp`, `makeTcpChannel`, + * `layerTcp`, and `fromConn` correspond to the Node platform's net and duplex + * APIs. Deno does not support `keepAliveInitialDelay`, and the `noDelay` and + * `keepAlive` options have no effect on Unix connections. A `CloseEvent` always + * closes gracefully because Deno has no equivalent of Node's reset-on-close. + * + * @since 4.0.0 + */ +import type { Array } from "effect" +import * as Channel from "effect/Channel" +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 * as Function from "effect/Function" +import * as Latch from "effect/Latch" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Socket from "effect/unstable/socket/Socket" + +/** + * Options for opening a TCP or Unix connection. + * + * @category options + * @since 4.0.0 + */ +export type ConnectOptions = (Deno.ConnectOptions | Deno.UnixConnectOptions) & { + readonly noDelay?: boolean | undefined + readonly keepAlive?: boolean | undefined +} + +/** + * Options for opening a TCP or Unix connection. + * + * @category options + * @since 4.0.0 + */ +export type TcpOptions = ConnectOptions & { + readonly openTimeout?: Duration.Input | undefined +} + +/** + * Service tag for the underlying Deno connection. + * + * @category services + * @since 4.0.0 + */ +export class Conn extends Context.Service()( + "@effect/platform-deno/DenoSocket/Conn" +) {} + +/** + * Adapts a Deno connection into an Effect socket. + * + * @category constructors + * @since 4.0.0 + */ +export const fromConn = ( + open: Effect.Effect +): Effect.Effect> => + Effect.withFiber>((fiber) => { + let current: { + readonly conn: Deno.Conn + readonly writer: WritableStreamDefaultWriter + } | undefined + let tearingDown = false + let writeClosed = false + const latch = Latch.makeUnsafe(false) + const openServices = fiber.context as Context.Context + + const run = (handler: (_: Uint8Array) => Effect.Effect<_, E, R> | void, options?: { + readonly onOpen?: Effect.Effect | undefined + }) => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const fiberSet = yield* FiberSet.make().pipe( + Scope.provide(scope) + ) + let conn: Deno.Conn | undefined + yield* Scope.addFinalizer( + scope, + Effect.suspend(() => { + tearingDown = true + return conn === undefined ? Effect.void : close(conn) + }) + ) + conn = yield* Scope.provide(open, scope) + const reader = conn.readable.getReader() + const writer = conn.writable.getWriter() + const runFork = yield* Effect.provideService(FiberSet.runtime(fiberSet)(), Conn, conn) + + current = { conn, writer } + tearingDown = false + if (writeClosed) { + writeClosed = false + writer.releaseLock() + yield* closeWrite(conn) + } + latch.openUnsafe() + + const read = Effect.tryPromise( + () => reader.read() + ).pipe( + Effect.catchIf( + (error) => tearingDown && isTeardownError(error.cause), + () => Effect.succeed({ done: true, value: undefined } as ReadableStreamReadDoneResult) + ), + Effect.mapError((error) => + new Socket.SocketError({ + reason: new Socket.SocketReadError({ cause: error.cause }) + }) + ) + ) + const readLoop: Effect.Effect = Effect.suspend(() => + Effect.flatMap(read, ({ done, value }) => { + if (done) { + Deferred.doneUnsafe(fiberSet.deferred, Effect.void) + return Effect.void + } + const result = handler(value) + if (Effect.isEffect(result)) { + runFork(result) + } + return readLoop + }) + ) + yield* FiberSet.run(fiberSet, readLoop) + + if (options?.onOpen) { + yield* options.onOpen + } + return yield* FiberSet.join(fiberSet) + })).pipe( + Effect.updateContext((input: Context.Context) => Context.merge(openServices, input)), + Effect.onExit(() => + Effect.sync(() => { + tearingDown = true + latch.closeUnsafe() + current = undefined + }) + ) + ) + + const write = (chunk: Uint8Array | string | Socket.CloseEvent) => + latch.whenOpen(Effect.suspend(() => { + const { conn, writer } = current! + if (Socket.isCloseEvent(chunk)) { + tearingDown = true + return close(conn) + } + const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk + return Effect.tryPromise({ + try: () => writer.ready.then(() => writer.write(bytes)), + catch: (cause) => + new Socket.SocketError({ + reason: new Socket.SocketWriteError({ cause }) + }) + }) + })) + + const writer = Effect.acquireRelease( + Effect.sync(() => { + writeClosed = false + return write + }), + () => + Effect.suspend(() => { + if (current === undefined) { + writeClosed = true + return Effect.void + } + const { conn, writer } = current + writer.releaseLock() + return closeWrite(conn) + }) + ) + + return Effect.succeed(Socket.make({ + run, + runRaw: run, + writer + })) + }) + +/** + * Opens a native Deno TCP or Unix connection as an Effect socket. + * + * **Details** + * + * An `openTimeout` interrupts acquisition but cannot cancel the in-flight + * `Deno.connect` promise. The scope finalizer closes a connection that arrives + * after the timeout. + * + * @category constructors + * @since 4.0.0 + */ +export const makeTcp = (options: TcpOptions): Effect.Effect => { + const { keepAlive, noDelay, openTimeout, ...connectOptions } = options + const acquire = Effect.contextWith((context: Context.Context) => { + let conn: Deno.Conn | undefined + let finalized = false + return Scope.addFinalizer( + Context.get(context, Scope.Scope), + Effect.suspend(() => { + finalized = true + return conn === undefined ? Effect.void : close(conn) + }) + ).pipe( + Effect.andThen(Effect.tryPromise({ + try: () => { + const connecting = connectOptions.transport === "unix" + ? Deno.connect(connectOptions) + : Deno.connect(connectOptions) + return connecting.then((connection) => { + conn = connection + if (finalized) connection.close() + return connection + }) + }, + catch: (cause) => + new Socket.SocketError({ + reason: new Socket.SocketOpenError({ kind: "Unknown", cause }) + }) + })), + Effect.tap((connection) => + Effect.sync(() => { + if ("setNoDelay" in connection && noDelay !== undefined) { + connection.setNoDelay(noDelay) + } + if ("setKeepAlive" in connection && keepAlive !== undefined) { + connection.setKeepAlive(keepAlive) + } + }) + ) + ) + }).pipe( + openTimeout === undefined + ? Function.identity + : Effect.timeoutOrElse({ + duration: openTimeout, + orElse: () => + Effect.fail( + new Socket.SocketError({ + reason: new Socket.SocketOpenError({ kind: "Timeout", cause: new Error("Connection timed out") }) + }) + ) + }) + ) + return fromConn(acquire) +} + +/** + * Creates a channel over a native Deno TCP or Unix connection. + * + * @category constructors + * @since 4.0.0 + */ +export const makeTcpChannel = ( + options: ConnectOptions +): Channel.Channel< + Array.NonEmptyReadonlyArray, + Socket.SocketError | IE, + void, + Array.NonEmptyReadonlyArray, + IE +> => Channel.unwrap(Effect.map(makeTcp(options), Socket.toChannelWith())) + +/** + * Provides a socket by opening a native Deno TCP or Unix connection. + * + * @category layers + * @since 4.0.0 + */ +export const layerTcp: (options: ConnectOptions) => Layer.Layer< + Socket.Socket, + Socket.SocketError +> = Function.flow(makeTcp, Layer.effect(Socket.Socket)) + +/** + * Creates a socket layer connected to a URL with Deno's global WebSocket. + * + * @category layers + * @since 4.0.0 + */ +export const layerWebSocket = (url: string, options?: { + readonly closeCodeIsError?: (code: number) => boolean +}): Layer.Layer => + Layer.effect(Socket.Socket, Socket.makeWebSocket(url, options)).pipe( + Layer.provide(layerWebSocketConstructor) + ) + +/** + * Provides the WebSocket constructor backed by `globalThis.WebSocket`. + * + * @category layers + * @since 4.0.0 + */ +export const layerWebSocketConstructor: Layer.Layer = + Socket.layerWebSocketConstructorGlobal + +const encoder = new TextEncoder() + +const isBadResource = (cause: unknown): cause is Deno.errors.BadResource => cause instanceof Deno.errors.BadResource + +const isTeardownError = (cause: unknown): boolean => cause instanceof Deno.errors.Interrupted || isBadResource(cause) + +const close = (conn: Deno.Conn): Effect.Effect => + Effect.try(() => conn.close()).pipe( + Effect.catch(({ cause }) => isBadResource(cause) ? Effect.void : Effect.die(cause)) + ) + +const closeWrite = (conn: Deno.Conn): Effect.Effect => + Effect.tryPromise(() => conn.closeWrite()).pipe( + Effect.catch((error) => isBadResource(error) ? Effect.void : Effect.die(error)) + ) diff --git a/.context/effect/packages/platform/deno/src/DenoSocketServer.ts b/.context/effect/packages/platform/deno/src/DenoSocketServer.ts new file mode 100644 index 000000000..f944ec0f1 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoSocketServer.ts @@ -0,0 +1,120 @@ +/** + * Native Deno TCP and Unix socket servers for Effect's unstable socket server + * API. + * + * The plain `make` and `layer` names intentionally differ from + * `DenoSocket.makeTcp` and `DenoSocket.layerTcp` because this module has only + * one server kind. Deno has no standalone WebSocket server, so WebSocket + * constructors are omitted, and there is no `IncomingMessage` analogue. + * Connections made before `run` starts remain in the kernel backlog instead of + * being accepted and buffered as they are by the Node implementation. A second + * concurrent `run` waits for the first because listener access is guarded by a + * semaphore. + * + * Deno-native TLS server constructors are included even though the Node socket + * server has no corresponding dedicated constructors. TLS is deliberately not + * tested here because the repository has no certificate fixture. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as Function from "effect/Function" +import * as Layer from "effect/Layer" +import type * as Scope from "effect/Scope" +import * as SocketServer from "effect/unstable/socket/SocketServer" +import { closeListener, fromListener } from "./internal/denoSocketServer.ts" + +/** + * Native Deno options for listening on a TCP or Unix socket. + * + * @category models + * @since 4.0.0 + */ +export type ListenOptions = + | (Deno.TcpListenOptions & { transport?: "tcp" }) + | (Deno.UnixListenOptions & { transport: "unix" }) + +/** + * Native Deno options and certified key material for listening with TLS. + * + * @category models + * @since 4.0.0 + */ +export type TlsListenOptions = Deno.ListenTlsOptions & Deno.TlsCertifiedKeyPem + +/** + * Creates a scoped socket server using a native Deno TCP or Unix listener. + * + * @category constructors + * @since 4.0.0 + */ +export const make: ( + options: ListenOptions +) => Effect.Effect< + SocketServer.SocketServer["Service"], + SocketServer.SocketServerError, + Scope.Scope +> = Effect.fnUntraced(function*(options) { + const listener = yield* Effect.acquireRelease( + Effect.try({ + try: () => options.transport === "unix" ? Deno.listen(options) : Deno.listen(options), + catch: openError + }), + closeListener + ) + return fromListener(listener) +}) + +/** + * Provides a socket server using a scoped native Deno TCP or Unix listener. + * + * @category layers + * @since 4.0.0 + */ +export const layer: ( + options: ListenOptions +) => Layer.Layer = Function.flow( + make, + Layer.effect(SocketServer.SocketServer) +) + +/** + * Creates a scoped TLS socket server using a native Deno TLS listener. + * + * @category constructors + * @since 4.0.0 + */ +export const makeTls: ( + options: TlsListenOptions +) => Effect.Effect< + SocketServer.SocketServer["Service"], + SocketServer.SocketServerError, + Scope.Scope +> = Effect.fnUntraced(function*(options) { + const listener = yield* Effect.acquireRelease( + Effect.try({ + try: () => Deno.listenTls(options), + catch: openError + }), + closeListener + ) + return fromListener(listener) +}) + +/** + * Provides a TLS socket server using a scoped native Deno TLS listener. + * + * @category layers + * @since 4.0.0 + */ +export const layerTls: ( + options: TlsListenOptions +) => Layer.Layer = Function.flow( + makeTls, + Layer.effect(SocketServer.SocketServer) +) + +const openError = (cause: unknown) => + new SocketServer.SocketServerError({ + reason: new SocketServer.SocketServerOpenError({ cause }) + }) diff --git a/.context/effect/packages/platform/deno/src/DenoStdio.ts b/.context/effect/packages/platform/deno/src/DenoStdio.ts new file mode 100644 index 000000000..8a79a6050 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoStdio.ts @@ -0,0 +1,53 @@ +/** + * Process stdio for Deno applications. + * + * This module provides Effect's `Stdio` service using Deno's native process + * arguments and Web Streams. Standard input remains open, and standard output + * and error output are not closed unless requested through `endOnDone`. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Sink from "effect/Sink" +import * as Stdio from "effect/Stdio" +import * as Stream from "effect/Stream" +import { handleError } from "./internal/error.ts" + +const encoder = new TextEncoder() + +const output = ( + evaluate: () => WritableStream, + method: "stdout" | "stderr", + options?: { readonly endOnDone?: boolean | undefined } +) => + Sink.fromWritableStream({ + evaluate, + onError: handleError("Stdio", method), + closeOnDone: options?.endOnDone ?? false + }).pipe( + Sink.mapInput((input: string | Uint8Array) => typeof input === "string" ? encoder.encode(input) : input) + ) + +/** + * Provides the `Stdio` service backed by `Deno.args`, `Deno.stdin`, + * `Deno.stdout`, and `Deno.stderr`. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.succeed( + Stdio.Stdio, + Stdio.make({ + args: Effect.sync(() => Deno.args), + stdinIsTerminal: Effect.sync(() => Deno.stdin.isTerminal()), + stdoutIsTerminal: Effect.sync(() => Deno.stdout.isTerminal()), + stdout: (options) => output(() => Deno.stdout.writable, "stdout", options), + stderr: (options) => output(() => Deno.stderr.writable, "stderr", options), + stdin: Stream.fromReadableStream({ + evaluate: () => Deno.stdin.readable, + onError: handleError("Stdio", "stdin"), + releaseLockOnEnd: true + }) + }) +) diff --git a/.context/effect/packages/platform/deno/src/DenoTerminal.ts b/.context/effect/packages/platform/deno/src/DenoTerminal.ts new file mode 100644 index 000000000..cb02cf119 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoTerminal.ts @@ -0,0 +1,32 @@ +/** + * Deno-backed implementation of Effect's `Terminal` service. + * + * This module reuses the shared Node terminal implementation for Deno. `make` + * creates a scoped process-backed `Terminal` service, and `layer` provides the + * default terminal service with the standard quit behavior for key input. + * + * @since 4.0.0 + */ +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import type { Effect } from "effect/Effect" +import type { Layer } from "effect/Layer" +import type { Scope } from "effect/Scope" +import type { Terminal, UserInput } from "effect/Terminal" + +/** + * Creates a scoped `Terminal` service backed by process stdin/stdout, using the + * optional predicate to decide when key input should end the input stream. + * + * @category constructors + * @since 4.0.0 + */ +export const make: (shouldQuit?: (input: UserInput) => boolean) => Effect = NodeTerminal.make + +/** + * Provides the default process-backed `Terminal` service, ending key input on + * the default quit keys. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer = NodeTerminal.layer diff --git a/.context/effect/packages/platform/deno/src/DenoWorker.ts b/.context/effect/packages/platform/deno/src/DenoWorker.ts new file mode 100644 index 000000000..7f4d40c6f --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoWorker.ts @@ -0,0 +1,75 @@ +/** + * Parent-side Deno platform for Effect workers. + * + * @since 4.0.0 + */ +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import * as Worker from "effect/unstable/workers/Worker" +import { WorkerError, WorkerReceiveError } from "effect/unstable/workers/WorkerError" + +/** + * Creates Deno worker layers by combining the default worker platform with a spawner. + * + * @category layers + * @since 4.0.0 + */ +export const layer = ( + spawn: (id: number) => globalThis.Worker | MessagePort +): Layer.Layer => + Layer.merge( + layerPlatform, + Worker.layerSpawner(spawn) + ) + +/** + * Layer that provides the Deno worker platform. + * + * @category layers + * @since 4.0.0 + */ +export const layerPlatform: Layer.Layer = Layer.succeed(Worker.WorkerPlatform)( + Worker.makePlatform()({ + setup({ scope, worker }) { + return Effect.as( + Scope.addFinalizer( + scope, + Effect.sync(() => { + worker.postMessage([1]) + }) + ), + worker + ) + }, + listen({ deferred, emit, port, scope }) { + function onMessage(event: MessageEvent) { + emit(event.data) + } + function onError(event: ErrorEvent) { + Deferred.doneUnsafe( + deferred, + new WorkerError({ + reason: new WorkerReceiveError({ + message: "An error event was emitted", + 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) + }) + ) + } + }) +) diff --git a/.context/effect/packages/platform/deno/src/DenoWorkerRunner.ts b/.context/effect/packages/platform/deno/src/DenoWorkerRunner.ts new file mode 100644 index 000000000..6e7547541 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/DenoWorkerRunner.ts @@ -0,0 +1,175 @@ +/** + * Runner-side Deno platform for Effect worker handlers using the standard `MessagePort` API. + * + * @since 4.0.0 + */ +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 { identity } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Queue from "effect/Queue" +import * as Scope from "effect/Scope" +import { WorkerError, WorkerReceiveError } from "effect/unstable/workers/WorkerError" +import * as WorkerRunner from "effect/unstable/workers/WorkerRunner" + +const cachedPorts = new Set() +function globalHandleConnect(event: MessageEvent) { + cachedPorts.add(event.ports[0]) +} +if (typeof self !== "undefined" && "onconnect" in self) { + self.onconnect = globalHandleConnect +} + +/** + * Creates a worker runner platform over a `MessagePort`. + * + * @category constructors + * @since 4.0.0 + */ +export const make = (self: MessagePort): WorkerRunner.WorkerRunnerPlatform["Service"] => ({ + start: Effect.fnUntraced(function*() { + const disconnects = yield* Queue.make() + let currentPortId = 0 + + const ports = new Map() + const sendUnsafe = (portId: number, message: O, transfer?: ReadonlyArray) => + (ports.get(portId)?.[0] ?? self).postMessage([1, message], { + transfer: transfer as any + }) + const send = (portId: number, message: O, transfer?: ReadonlyArray) => + Effect.sync(() => sendUnsafe(portId, message, transfer)) + + const run = ( + handler: (portId: number, message: I) => Effect.Effect | void + ) => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const closeLatch = Deferred.makeUnsafe() + const trackFiber = Fiber.runIn(scope) + const services = yield* Effect.context() + const runFork = Effect.runForkWith(services) + const onExit = (exit: Exit.Exit) => { + if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) { + runFork(Effect.logError("unhandled error in worker", exit.cause)) + } + } + + function onMessage(portId: number) { + return function(event: MessageEvent) { + const message = event.data as WorkerRunner.PlatformMessage + if (message[0] === 0) { + const result = handler(portId, message[1]) + if (Effect.isEffect(result)) { + const fiber = runFork(result) + fiber.addObserver(onExit) + trackFiber(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.doneUnsafe(closeLatch, Exit.void) + } + ports.delete(portId) + Queue.offerUnsafe(disconnects, portId) + Effect.runFork(Scope.close(port[1], Exit.void)) + } + } + } + function onMessageError(error: MessageEvent) { + Deferred.doneUnsafe( + closeLatch, + new WorkerError({ + reason: new WorkerReceiveError({ + message: "An messageerror event was emitted", + cause: error.data + }) + }) + ) + } + function onError(error: any) { + Deferred.doneUnsafe( + closeLatch, + new WorkerError({ + reason: new WorkerReceiveError({ + message: "An error event was emitted", + cause: error.data + }) + }) + ) + } + function handlePort(port: MessagePort) { + const portScope = Scope.forkUnsafe(scope) + const portId = currentPortId++ + ports.set(portId, [port, portScope]) + const onMsg = onMessage(portId) + port.addEventListener("message", onMsg) + port.addEventListener("messageerror", onMessageError) + if ("start" in port) { + port.start() + } + port.postMessage([0]) + Effect.runSync(Scope.addFinalizer( + portScope, + Effect.sync(() => { + port.removeEventListener("message", onMsg) + port.removeEventListener("messageerror", onMessageError) + port.close() + }) + )) + } + 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() + } else { + handlePort(self as any) + } + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + self.removeEventListener("error", onError) + if ("onconnect" in self) { + self.close() + self.onconnect = prevOnConnect + } + }) + ) + + yield* Deferred.await(closeLatch) + })) + + return identity>({ run, send, sendUnsafe, disconnects }) + }) as any +}) + +/** + * Layer that provides the worker runner platform using the global worker scope. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.sync(WorkerRunner.WorkerRunnerPlatform)(() => + make(self as unknown as MessagePort) +) + +/** + * Layer that provides the worker runner platform using the supplied `MessagePort`. + * + * @category layers + * @since 4.0.0 + */ +export const layerMessagePort = (port: MessagePort): Layer.Layer => + Layer.succeed(WorkerRunner.WorkerRunnerPlatform)(make(port)) diff --git a/.context/effect/packages/platform/deno/src/index.ts b/.context/effect/packages/platform/deno/src/index.ts new file mode 100644 index 000000000..111341276 --- /dev/null +++ b/.context/effect/packages/platform/deno/src/index.ts @@ -0,0 +1,110 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as DenoChildProcessSpawner from "./DenoChildProcessSpawner.ts" + +/** + * @since 4.0.0 + */ +export * as DenoClusterHttp from "./DenoClusterHttp.ts" + +/** + * @since 4.0.0 + */ +export * as DenoClusterSocket from "./DenoClusterSocket.ts" + +/** + * @since 4.0.0 + */ +export * as DenoCrypto from "./DenoCrypto.ts" + +/** + * @since 4.0.0 + */ +export * as DenoFileSystem from "./DenoFileSystem.ts" + +/** + * @since 4.0.0 + */ +export * as DenoHttpClient from "./DenoHttpClient.ts" + +/** + * @since 4.0.0 + */ +export * as DenoHttpPlatform from "./DenoHttpPlatform.ts" + +/** + * @since 4.0.0 + */ +export * as DenoHttpServer from "./DenoHttpServer.ts" + +/** + * @since 4.0.0 + */ +export * as DenoHttpServerRequest from "./DenoHttpServerRequest.ts" + +/** + * @since 4.0.0 + */ +export * as DenoKeyValueStore from "./DenoKeyValueStore.ts" + +/** + * @since 4.0.0 + */ +export * as DenoMultipart from "./DenoMultipart.ts" + +/** + * @since 4.0.0 + */ +export * as DenoPath from "./DenoPath.ts" + +/** + * @since 4.0.0 + */ +export * as DenoRedis from "./DenoRedis.ts" + +/** + * @since 4.0.0 + */ +export * as DenoRuntime from "./DenoRuntime.ts" + +/** + * @since 4.0.0 + */ +export * as DenoServices from "./DenoServices.ts" + +/** + * @since 4.0.0 + */ +export * as DenoSocket from "./DenoSocket.ts" + +/** + * @since 4.0.0 + */ +export * as DenoSocketServer from "./DenoSocketServer.ts" + +/** + * @since 4.0.0 + */ +export * as DenoStdio from "./DenoStdio.ts" + +/** + * @since 4.0.0 + */ +export * as DenoTerminal from "./DenoTerminal.ts" + +/** + * @since 4.0.0 + */ +export * as DenoWorker from "./DenoWorker.ts" + +/** + * @since 4.0.0 + */ +export * as DenoWorkerRunner from "./DenoWorkerRunner.ts" diff --git a/.context/effect/packages/platform/deno/src/internal/denoSocketServer.ts b/.context/effect/packages/platform/deno/src/internal/denoSocketServer.ts new file mode 100644 index 000000000..0d4a13c3b --- /dev/null +++ b/.context/effect/packages/platform/deno/src/internal/denoSocketServer.ts @@ -0,0 +1,89 @@ +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 { pipe } from "effect/Function" +import * as References from "effect/References" +import * as Scope from "effect/Scope" +import * as Semaphore from "effect/Semaphore" +import type * as Socket from "effect/unstable/socket/Socket" +import * as SocketServer from "effect/unstable/socket/SocketServer" +import * as DenoSocket from "../DenoSocket.ts" + +const initialBackoffMillis = 10 +const maximumBackoffMillis = 1_000 + +type Listener = Deno.Listener + +export const fromListener = (listener: Listener): SocketServer.SocketServer["Service"] => { + const semaphore = Semaphore.makeUnsafe(1) + + const run = (handler: (socket: Socket.Socket) => Effect.Effect<_, E, R>) => + semaphore.withPermit(Effect.gen(function*() { + const scope = yield* Scope.make() + const services = Context.omit(Scope.Scope)(yield* Effect.context()) as Context.Context + const trackFiber = Fiber.runIn(scope) + let failures = 0 + + const loop: Effect.Effect = Effect.suspend(() => + Effect.tryPromise(() => listener.accept()).pipe( + Effect.matchEffect({ + onFailure: (error) => { + const cause = error.cause + if (isTeardownError(cause)) return Effect.never + const backoff = Math.min(initialBackoffMillis * 2 ** failures++, maximumBackoffMillis) + return reportUnhandledError(Cause.fail(cause)).pipe( + Effect.andThen(Effect.sleep(backoff)), + Effect.andThen(loop) + ) + }, + onSuccess: (conn) => { + failures = 0 + pipe( + DenoSocket.fromConn(Effect.acquireRelease(Effect.succeed(conn), closeConn)), + Effect.flatMap(handler), + Effect.ensuring(closeConn(conn)), + Effect.catchCause(reportUnhandledError), + Effect.runForkWith(Context.add(services, DenoSocket.Conn, conn)), + trackFiber + ) + return loop + } + }) + ) + ) + + return yield* loop.pipe(Effect.ensuring(Scope.close(scope, Exit.void))) + })) + + const address = listener.addr + return SocketServer.SocketServer.of({ + address: "path" in address + ? { _tag: "UnixAddress", path: address.path } + : { _tag: "TcpAddress", hostname: address.hostname, port: address.port }, + run + }) +} + +export const closeListener = (listener: Listener): Effect.Effect => + Effect.try(() => listener.close()).pipe( + Effect.catch(({ cause }) => isTeardownError(cause) ? Effect.void : Effect.die(cause)) + ) + +const closeConn = (conn: Deno.Conn): Effect.Effect => + Effect.try(() => conn.close()).pipe( + Effect.catch(({ cause }) => isTeardownError(cause) ? Effect.void : Effect.die(cause)) + ) + +const isTeardownError = (cause: unknown): boolean => + cause instanceof Deno.errors.BadResource || cause instanceof Deno.errors.Interrupted + +const reportUnhandledError = (cause: Cause.Cause) => + Effect.withFiber((fiber) => { + const unhandledLogLevel = fiber.getRef(References.UnhandledLogLevel) + if (unhandledLogLevel) { + return Effect.logWithLevel(unhandledLogLevel)(cause, "Unhandled error in SocketServer") + } + return Effect.void + }) diff --git a/.context/effect/packages/platform/deno/src/internal/error.ts b/.context/effect/packages/platform/deno/src/internal/error.ts new file mode 100644 index 000000000..11a0f5a0a --- /dev/null +++ b/.context/effect/packages/platform/deno/src/internal/error.ts @@ -0,0 +1,66 @@ +import type { SystemError, SystemErrorTag } from "effect/PlatformError" +import * as PlatformError from "effect/PlatformError" + +interface DenoError { + readonly code?: unknown + readonly name?: unknown +} + +export const handleError = ( + module: SystemError["module"], + method: string, + pathOrDescriptor?: string | number +) => +(error: unknown): PlatformError.PlatformError => { + const denoError = error as DenoError + let tag: SystemErrorTag = "Unknown" + + switch (denoError?.name) { + case "AlreadyExists": + tag = "AlreadyExists" + break + case "NotCapable": + tag = "PermissionDenied" + break + case "BadResource": + case "InvalidData": + case "TimedOut": + case "UnexpectedEof": + case "WouldBlock": + case "WriteZero": + tag = denoError.name + break + default: + switch (denoError?.code) { + case "ENOENT": + tag = "NotFound" + break + + case "EACCES": + tag = "PermissionDenied" + break + + case "EEXIST": + tag = "AlreadyExists" + break + + case "EISDIR": + case "ENOTDIR": + case "ELOOP": + tag = "BadResource" + break + + case "EBUSY": + tag = "Busy" + break + } + } + + return PlatformError.systemError({ + _tag: tag, + module, + method, + pathOrDescriptor, + cause: error + }) +} diff --git a/.context/effect/packages/platform/deno/test/DenoChildProcessSpawner.test.ts b/.context/effect/packages/platform/deno/test/DenoChildProcessSpawner.test.ts new file mode 100644 index 000000000..3609d67a0 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoChildProcessSpawner.test.ts @@ -0,0 +1,153 @@ +import * as DenoChildProcessSpawner from "@effect/platform-deno/DenoChildProcessSpawner" +import * as DenoPath from "@effect/platform-deno/DenoPath" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as PlatformError from "effect/PlatformError" +import * as Predicate from "effect/Predicate" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/testing/TestClock" +import { ChildProcess } from "effect/unstable/process" +import * as ChildProcessSpawnerTest from "../../../effect/test/unstable/process/ChildProcessSpawnerTest.ts" + +const platformError = (method: string, path: string, cause: unknown) => + PlatformError.systemError({ + _tag: Predicate.hasProperty(cause, "name") && cause.name === "NotFound" ? "NotFound" : "Unknown", + module: "FileSystem", + method, + pathOrDescriptor: path, + cause + }) + +const fileSystem = FileSystem.layerNoop({ + access: (path) => + Effect.tryPromise({ + try: () => Deno.stat(path), + catch: (cause) => platformError("access", path, cause) + }).pipe(Effect.asVoid), + exists: (path) => + Effect.tryPromise({ + try: () => Deno.stat(path).then(() => true), + catch: (cause) => platformError("exists", path, cause) + }).pipe(Effect.catchTag("PlatformError", (error) => + error.reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(error))), + makeTempDirectoryScoped: (options) => + Effect.acquireRelease( + Effect.tryPromise({ + try: () => Deno.makeTempDir({ dir: options?.directory, prefix: options?.prefix }), + catch: (cause) => platformError("makeTempDirectoryScoped", options?.directory ?? "", cause) + }), + (path) => Effect.promise(() => Deno.remove(path, { recursive: true })).pipe(Effect.ignore) + ), + writeFile: (path, data) => + Effect.tryPromise({ + try: () => Deno.writeFile(path, data), + catch: (cause) => platformError("writeFile", path, cause) + }) +}) + +const layer = DenoChildProcessSpawner.layer.pipe( + Layer.provideMerge(Layer.mergeAll(DenoPath.layer, fileSystem)) +) + +const output = (command: ChildProcess.Command) => + Effect.gen(function*() { + const handle = yield* command + return yield* handle.stdout.pipe(Stream.decodeText(), Stream.mkString) + }).pipe(Effect.scoped, Effect.provide(layer)) + +ChildProcessSpawnerTest.suite("DenoChildProcessSpawner", layer, { + processGroups: false, + additionalFds: false +}) + +describe("DenoChildProcessSpawner options", () => { + it.effect("rejects detached", () => + Effect.gen(function*() { + const error = yield* ChildProcess.make("echo", ["test"], { detached: false }).pipe(Effect.flip) + assert.deepStrictEqual( + error.reason, + new PlatformError.BadArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The detached option is unsupported because Deno has no equivalent" + }) + ) + }).pipe(Effect.provide(layer))) + + it.effect("rejects additionalFds", () => + Effect.gen(function*() { + const error = yield* ChildProcess.make("echo", ["test"], { + additionalFds: { fd3: { type: "output" } } + }).pipe(Effect.flip) + assert.deepStrictEqual( + error.reason, + new PlatformError.BadArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The additionalFds option is unsupported because Deno has no equivalent" + }) + ) + }).pipe(Effect.provide(layer))) + + it.effect("rejects additional fd pipe sources", () => + Effect.gen(function*() { + const error = yield* ChildProcess.make("echo", ["test"]).pipe( + ChildProcess.pipeTo(ChildProcess.make("cat"), { from: "fd3" }), + Effect.flip + ) + assert.deepStrictEqual( + error.reason, + new PlatformError.BadArgument({ + module: "ChildProcessSpawner", + method: "spawn", + description: "The additionalFds option is unsupported because Deno has no equivalent" + }) + ) + }).pipe(Effect.provide(layer))) + + it.effect("kills every process in a pipeline", () => + Effect.gen(function*() { + const directory = yield* Effect.acquireRelease( + Effect.promise(() => Deno.makeTempDir()), + (path) => Effect.promise(() => Deno.remove(path, { recursive: true })).pipe(Effect.ignore) + ) + const rootHeartbeat = `${directory}/root-heartbeat` + const childHeartbeat = `${directory}/child-heartbeat` + const handle = yield* ChildProcess.make( + "sh", + ["-c", "while :; do printf x >> \"$1\"; sleep 0.01; done", "pipeline-root", rootHeartbeat] + ).pipe( + ChildProcess.pipeTo( + ChildProcess.make( + "sh", + ["-c", "while :; do printf x >> \"$1\"; sleep 0.01; done", "pipeline-child", childHeartbeat] + ) + ) + ) + + yield* TestClock.withLive(Effect.sleep("100 millis")) + yield* handle.kill({ killSignal: "SIGKILL" }) + const rootSizeAfterKill = (yield* Effect.promise(() => Deno.stat(rootHeartbeat))).size + const childSizeAfterKill = (yield* Effect.promise(() => Deno.stat(childHeartbeat))).size + yield* TestClock.withLive(Effect.sleep("100 millis")) + const rootFinalSize = (yield* Effect.promise(() => Deno.stat(rootHeartbeat))).size + const childFinalSize = (yield* Effect.promise(() => Deno.stat(childHeartbeat))).size + + assert.strictEqual(rootFinalSize, rootSizeAfterKill) + assert.strictEqual(childFinalSize, childSizeAfterKill) + }).pipe(Effect.provide(layer))) + + it.effect("emulates shell true without escaping joined arguments", () => + Effect.gen(function*() { + const result = yield* output(ChildProcess.make("printf", ["[%s]", "a b"], { shell: true })) + assert.strictEqual(result, "[a][b]") + })) + + it.effect("uses a custom shell", () => + Effect.gen(function*() { + const result = yield* output(ChildProcess.make("printf", ["custom-shell"], { shell: "/bin/sh" })) + assert.strictEqual(result, "custom-shell") + })) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoCrypto.test.ts b/.context/effect/packages/platform/deno/test/DenoCrypto.test.ts new file mode 100644 index 000000000..4e8a0c37f --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoCrypto.test.ts @@ -0,0 +1,37 @@ +import * as DenoCrypto from "@effect/platform-deno/DenoCrypto" +import { assert, describe, it } from "@effect/vitest" +import * as Crypto from "effect/Crypto" +import * as Effect from "effect/Effect" + +const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ +const uuidV7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ + +const hex = (bytes: Uint8Array): string => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("") + +describe("DenoCrypto", () => { + it.effect("computes SHA-256 digests", () => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + const digest = yield* crypto.digest("SHA-256", new TextEncoder().encode("hello")) + assert.strictEqual(hex(digest), "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824") + }).pipe(Effect.provide(DenoCrypto.layer))) + + it.effect("generates random bytes larger than the Web Crypto quota", () => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + const bytes = yield* crypto.randomBytes(70_000) + assert.strictEqual(bytes.length, 70_000) + }).pipe(Effect.provide(DenoCrypto.layer))) + + it.effect("generates UUIDv4 values", () => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + assert.match(yield* crypto.randomUUIDv4, uuidV4Regex) + }).pipe(Effect.provide(DenoCrypto.layer))) + + it.effect("generates UUIDv7 values", () => + Effect.gen(function*() { + const crypto = yield* Crypto.Crypto + assert.match(yield* crypto.randomUUIDv7, uuidV7Regex) + }).pipe(Effect.provide(DenoCrypto.layer))) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoFileSystem.test.ts b/.context/effect/packages/platform/deno/test/DenoFileSystem.test.ts new file mode 100644 index 000000000..b842099e4 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoFileSystem.test.ts @@ -0,0 +1,9 @@ +import * as DenoFileSystem from "@effect/platform-deno/DenoFileSystem" +import { describe } from "@effect/vitest" +import { testLayer } from "../../../effect/test/FileSystem.test-utils.ts" + +describe("FileSystem", () => + testLayer(DenoFileSystem.layer, { + accessOnDirectory: false, + tempFileScopedRemovesDirectory: false + })) diff --git a/.context/effect/packages/platform/deno/test/DenoHttpCompression.test.ts b/.context/effect/packages/platform/deno/test/DenoHttpCompression.test.ts new file mode 100644 index 000000000..6092b902f --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoHttpCompression.test.ts @@ -0,0 +1,158 @@ +import * as DenoHttpPlatform from "@effect/platform-deno/DenoHttpPlatform" +import * as DenoHttpServer from "@effect/platform-deno/DenoHttpServer" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Stream from "effect/Stream" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpEffect from "effect/unstable/http/HttpEffect" +import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" +import * as HttpRouter from "effect/unstable/http/HttpRouter" +import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" +import { fileURLToPath } from "node:url" +import * as Zlib from "node:zlib" + +const bigJson = JSON.stringify({ text: "All work and no play makes Jack a dull boy. ".repeat(100) }) +const bigJsonApp = Effect.succeed(HttpServerResponse.text(bigJson, { contentType: "application/json" })) + +const zstdSupported = typeof Zlib.zstdCompress === "function" + +type App = Effect.Effect +type CompressionOptions = Parameters[0] + +const withHandler = async ( + app: App, + options: CompressionOptions, + run: (handler: (request: Request) => Promise) => Promise +) => { + const { dispose, handler } = HttpEffect.toWebHandlerLayer( + app as Effect.Effect, + DenoHttpPlatform.layer, + { middleware: HttpMiddleware.compression(options) } + ) + try { + await run(handler) + } finally { + await dispose() + } +} + +const get = ( + handler: (request: Request) => Promise, + headers?: Record +) => handler(new Request("http://localhost/", headers === undefined ? {} : { headers })) + +describe("DenoHttpCompression", () => { + it.effect("advertises supported algorithms", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + assert.isTrue(platform.compression.algorithms.has("gzip")) + assert.isTrue(platform.compression.algorithms.has("deflate")) + assert.isTrue(platform.compression.algorithms.has("br")) + assert.strictEqual(platform.compression.algorithms.has("zstd"), zstdSupported) + }).pipe(Effect.provide(DenoHttpPlatform.layer))) + + it("compresses one-shot bodies asynchronously with gzip and an exact Content-Length", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.gunzipSync(compressed).toString(), bigJson) + })) + + it("prefers br over gzip in server order", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip, br" }) + assert.strictEqual(response.headers.get("content-encoding"), "br") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.brotliDecompressSync(compressed).toString(), bigJson) + })) + + it.skipIf(!zstdSupported)( + "compresses with zstd when opted in", + () => + withHandler(bigJsonApp, { algorithms: ["zstd", "gzip"] }, async (handler) => { + const response = await get(handler, { "accept-encoding": "zstd" }) + assert.strictEqual(response.headers.get("content-encoding"), "zstd") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.zstdDecompressSync(compressed).toString(), bigJson) + }) + ) + + it("compresses stream bodies with gzip via CompressionStream", () => + withHandler( + Effect.succeed(HttpServerResponse.stream( + Stream.fromArray([new TextEncoder().encode(bigJson)]), + { contentType: "application/json" } + )), + undefined, + async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + assert.strictEqual(Zlib.gunzipSync(new Uint8Array(await response.arrayBuffer())).toString(), bigJson) + } + )) + + it("compresses stream bodies with br via node:zlib streams", () => + withHandler( + Effect.succeed(HttpServerResponse.stream( + Stream.fromArray([new TextEncoder().encode(bigJson)]), + { contentType: "application/json" } + )), + undefined, + async (handler) => { + const response = await get(handler, { "accept-encoding": "br" }) + assert.strictEqual(response.headers.get("content-encoding"), "br") + assert.strictEqual(response.headers.get("content-length"), null) + assert.strictEqual(Zlib.brotliDecompressSync(new Uint8Array(await response.arrayBuffer())).toString(), bigJson) + } + )) + + it("compresses file responses through the streaming path and weakens the ETag", async () => { + const path = fileURLToPath(new URL("./DenoHttpCompression.test.ts", import.meta.url)) + const contents = await Deno.readTextFile(path) + await withHandler( + HttpServerResponse.file(path, { headers: { "content-type": "text/plain" } }), + undefined, + async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + assert.isTrue(response.headers.get("etag")!.startsWith("W/")) + assert.strictEqual(Zlib.gunzipSync(new Uint8Array(await response.arrayBuffer())).toString(), contents) + } + ) + }) + + it.effect("compresses end-to-end through the Deno HTTP server", () => + Effect.gen(function*() { + yield* HttpRouter.add("GET", "/json", bigJsonApp).pipe( + (self) => + HttpRouter.serve(self, { + middleware: HttpMiddleware.compression({ algorithms: ["zstd", "br", "gzip", "deflate"] }) + }), + Layer.build + ) + const client = yield* HttpClient.HttpClient + // Deno's fetch transparently decompresses gzip and strips the + // Content-Encoding header, so assert on the round-tripped body + const gzip = yield* client.get("/json", { headers: { "accept-encoding": "gzip" } }) + assert.strictEqual(gzip.headers["vary"], "Accept-Encoding") + assert.strictEqual(yield* gzip.text, bigJson) + if (zstdSupported) { + // Deno's fetch does not decode zstd, so the raw compressed payload and + // its headers are observable + const zstd = yield* client.get("/json", { headers: { "accept-encoding": "zstd" } }) + assert.strictEqual(zstd.headers["content-encoding"], "zstd") + const compressed = new Uint8Array(yield* zstd.arrayBuffer) + assert.strictEqual(Zlib.zstdDecompressSync(compressed).toString(), bigJson) + } + }).pipe(Effect.provide(DenoHttpServer.layerTest))) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoHttpPlatform.test.ts b/.context/effect/packages/platform/deno/test/DenoHttpPlatform.test.ts new file mode 100644 index 000000000..76a421f1f --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoHttpPlatform.test.ts @@ -0,0 +1,68 @@ +import * as DenoHttpPlatform from "@effect/platform-deno/DenoHttpPlatform" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import type * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" + +const fixture = `${import.meta.dirname}/fixtures/text.txt` + +const readStream = (stream: ReadableStream) => Effect.promise(() => new globalThis.Response(stream).text()) + +const readBody = (body: HttpBody.HttpBody) => { + assert.strictEqual(body._tag, "Raw") + return Effect.promise(() => new Response((body as HttpBody.Raw).body as BodyInit).text()) +} + +describe("DenoHttpPlatform", () => { + it.effect("fileWebResponse honors offset and bytesToRead including zero", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const file = new File(["abcd"], "file.txt", { type: "text/plain", lastModified: 0 }) + const sliced = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 2 }) + const empty = yield* platform.fileWebResponse(file, { offset: 1, bytesToRead: 0 }) + + assert.deepStrictEqual( + { + slicedLength: sliced.headers["content-length"], + slicedBody: yield* readBody(sliced.body), + emptyLength: empty.headers["content-length"], + emptyBody: yield* readBody(empty.body) + }, + { slicedLength: "2", slicedBody: "bc", emptyLength: "0", emptyBody: "" } + ) + }).pipe(Effect.provide(DenoHttpPlatform.layer))) + + it.effect("fileResponse reads exact bytesToRead", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileResponse(fixture, { + offset: 6, + bytesToRead: 5 + }) + + assert.strictEqual(response.headers["content-length"], "5") + assert.strictEqual(response.body._tag, "Raw") + const body = (response.body as HttpBody.Raw).body + assert(body instanceof ReadableStream) + + const text = yield* readStream(body) + assert.strictEqual(text, "ipsum") + }).pipe(Effect.provide(DenoHttpPlatform.layer))) + + it.effect("fileResponse supports zero bytesToRead", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + const response = yield* platform.fileResponse(fixture, { + offset: 6, + bytesToRead: 0 + }) + + assert.strictEqual(response.headers["content-length"], "0") + assert.strictEqual(response.body._tag, "Raw") + const body = (response.body as HttpBody.Raw).body + assert(body instanceof ReadableStream) + + const text = yield* readStream(body) + assert.strictEqual(text, "") + }).pipe(Effect.provide(DenoHttpPlatform.layer))) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoHttpServer.test.ts b/.context/effect/packages/platform/deno/test/DenoHttpServer.test.ts new file mode 100644 index 000000000..6af9c53b5 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoHttpServer.test.ts @@ -0,0 +1,735 @@ +import * as DenoHttpServer from "@effect/platform-deno/DenoHttpServer" +import { assert, describe, it } from "@effect/vitest" +import * as Duration from "effect/Duration" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Latch from "effect/Latch" +import * as Layer from "effect/Layer" +import * as ManagedRuntime from "effect/ManagedRuntime" +import * as Queue from "effect/Queue" +import * as Schema from "effect/Schema" +import type * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import * as Tracer from "effect/Tracer" +import * as Cookies from "effect/unstable/http/Cookies" +import * as Etag from "effect/unstable/http/Etag" +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" +import * as HttpRouter from "effect/unstable/http/HttpRouter" +import * as HttpServer from "effect/unstable/http/HttpServer" +import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest" +import * as HttpServerRespondable from "effect/unstable/http/HttpServerRespondable" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" +import * as Multipart from "effect/unstable/http/Multipart" +import * as UrlParams from "effect/unstable/http/UrlParams" +import * as HttpApiError from "effect/unstable/httpapi/HttpApiError" +import type * as Socket from "effect/unstable/socket/Socket" + +const Todo = Schema.Struct({ + id: Schema.Number, + title: Schema.String +}) +const IdParams = Schema.Struct({ + id: Schema.FiniteFromString +}) +const todoResponse = HttpServerResponse.schemaJson(Todo) +const fixture = `${import.meta.dirname}/fixtures/text.txt` + +describe("DenoHttpServer", () => { + it.effect("schema", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/todos/:id", + Effect.flatMap(HttpRouter.schemaParams(IdParams), ({ id }) => todoResponse({ id, title: "test" })) + ).pipe(HttpRouter.serve, Layer.build) + const todo = yield* HttpClient.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + assert.deepStrictEqual(todo, { id: 1, title: "test" }) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("exports a weak ETag generator", () => + Effect.gen(function*() { + const generator = yield* Etag.Generator + const etag = yield* generator.fromFileWeb(new File(["test"], "test.txt", { lastModified: 0 })) + assert.strictEqual(etag._tag, "Weak") + }).pipe(Effect.provide(DenoHttpServer.layerHttpServices))) + + it.effect("formData", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "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") + assert(file.path.endsWith("/test.txt")) + assert.strictEqual(file.contentType, "text/plain") + assert.strictEqual(yield* Effect.promise(() => Deno.readTextFile(file.path)), "test") + return yield* HttpServerResponse.json({ ok: "file" in formData }) + }) + ).pipe(HttpRouter.serve, Layer.build) + const formData = new FormData() + formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") + const response = yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) }) + assert.deepStrictEqual(yield* response.json, { ok: true }) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("multipartStream", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const parts = yield* Stream.runCollect(request.multipartStream) + assert.strictEqual(parts.length, 2) + const field = parts[0] + const file = parts[1] + assert(Multipart.isField(field)) + assert.deepStrictEqual({ key: field.key, value: field.value }, { key: "name", value: "value" }) + assert(Multipart.isFile(file)) + assert.deepStrictEqual( + { key: file.key, name: file.name, contentType: file.contentType }, + { key: "file", name: "test.txt", contentType: "text/plain" } + ) + assert.strictEqual(new TextDecoder().decode(yield* file.contentEffect), "test") + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + const formData = new FormData() + formData.append("name", "value") + formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") + const response = yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) }) + assert.strictEqual(response.status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyForm", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const files = yield* HttpServerRequest.schemaBodyForm(Schema.Struct({ + file: Multipart.FilesSchema, + test: Schema.String + })) + assert("file" in files) + assert.strictEqual(files.test, "test") + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + const formData = new FormData() + formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") + formData.append("test", "test") + const response = yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) }) + assert.strictEqual(response.status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("formData withMaxFileSize", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart + return HttpServerResponse.empty() + }).pipe(Effect.catchTag("MultipartError", (error) => + error.reason._tag === "FileTooLarge" + ? Effect.succeed(HttpServerResponse.empty({ status: 413 })) + : Effect.fail(error))) + ).pipe( + HttpRouter.serve, + Layer.build, + Effect.provideService(Multipart.MaxFileSize, 100) + ) + const formData = new FormData() + formData.append("file", new Blob([new Uint8Array(1000)], { type: "text/plain" }), "test.txt") + const response = yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) }) + assert.strictEqual(response.status, 413) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("formData withMaxFieldSize", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.multipart + return HttpServerResponse.empty() + }).pipe(Effect.catchTag("MultipartError", (error) => + error.reason._tag === "FieldTooLarge" + ? Effect.succeed(HttpServerResponse.empty({ status: 413 })) + : Effect.fail(error))) + ).pipe( + HttpRouter.serve, + Layer.build, + Effect.provideService(Multipart.MaxFieldSize, 100) + ) + const formData = new FormData() + formData.append("file", "x".repeat(1000)) + const response = yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) }) + assert.strictEqual(response.status, 413) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("mountApp", () => + Effect.gen(function*() { + const child = Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url)) + yield* HttpRouter.use((router) => router.prefixed("/child").add("*", "*", child)).pipe( + HttpRouter.serve, + Layer.build + ) + assert.strictEqual(yield* HttpClient.get("/child/1").pipe(Effect.flatMap((_) => _.text)), "/1") + assert.strictEqual(yield* HttpClient.get("/child").pipe(Effect.flatMap((_) => _.text)), "/") + assert.strictEqual(yield* HttpClient.get("/child?foo=bar").pipe(Effect.flatMap((_) => _.text)), "?foo=bar") + assert.strictEqual(yield* HttpClient.get("/child/").pipe(Effect.flatMap((_) => _.text)), "/") + assert.strictEqual( + yield* HttpClient.get("/child1/", { urlParams: { foo: "bar" } }).pipe(Effect.map((_) => _.status)), + 404 + ) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("file", () => + Effect.gen(function*() { + yield* (yield* HttpServerResponse.file(fixture).pipe( + Effect.updateService(HttpPlatform.HttpPlatform, (_) => ({ + ..._, + fileResponse: (path, options) => + Effect.map(_.fileResponse(path, options), (response) => { + ;(response as any).headers.etag = "\"etag\"" + return response + }) + })) + )).pipe(Effect.succeed, HttpServer.serveEffect()) + const response = yield* HttpClient.get("/", { + headers: { "accept-encoding": "identity" } + }) + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers["content-type"], "text/plain; charset=UTF-8") + assert.strictEqual(response.headers.etag, "\"etag\"") + assert.strictEqual((yield* response.text).trim(), "lorem ipsum dolar sit amet") + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("fileWeb", () => + Effect.gen(function*() { + const now = new Date() + const file = new File([new TextEncoder().encode("test")], "test.txt", { + type: "text/plain", + lastModified: now.getTime() + }) + yield* HttpServerResponse.fileWeb(file).pipe( + Effect.updateService(HttpPlatform.HttpPlatform, (_) => ({ + ..._, + fileWebResponse: (file, options) => + Effect.map(_.fileWebResponse(file, options), (response) => ({ + ...response, + headers: { ...response.headers, etag: "W/\"etag\"" } + })) + })), + HttpServer.serveEffect() + ) + const response = yield* HttpClient.get("/") + assert.strictEqual(response.status, 200) + assert.strictEqual(response.headers["content-type"], "text/plain") + assert.strictEqual(response.headers["last-modified"], now.toUTCString()) + assert.strictEqual(response.headers.etag, "W/\"etag\"") + assert.strictEqual(yield* response.text, "test") + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyUrlParams", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/todos", + Effect.flatMap( + HttpServerRequest.schemaBodyUrlParams(Schema.Struct({ + id: Schema.FiniteFromString, + title: Schema.String + })), + ({ id, title }) => todoResponse({ id, title }) + ) + ).pipe(HttpRouter.serve, Layer.build) + const todo = yield* HttpClientRequest.post("/todos").pipe( + HttpClientRequest.bodyUrlParams({ id: "1", title: "test" }), + HttpClient.execute, + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + assert.deepStrictEqual(todo, { id: 1, title: "test" }) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyUrlParams error", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/todos", + Effect.flatMap( + HttpServerRequest.schemaBodyUrlParams(Schema.Struct({ + id: Schema.FiniteFromString, + title: Schema.String + })), + ({ id, title }) => todoResponse({ id, title }) + ).pipe(Effect.catchTag("SchemaError", (error) => + Effect.succeed(HttpServerResponse.jsonUnsafe({ error }, { status: 400 })))) + ).pipe(HttpRouter.serve, Layer.build) + assert.strictEqual((yield* HttpClient.get("/todos")).status, 400) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyFormJson", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ test: Schema.String }))("json") + assert.strictEqual(result.test, "content") + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + const formData = new FormData() + formData.append("json", JSON.stringify({ test: "content" })) + assert.strictEqual((yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) })).status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyFormJson file", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ test: Schema.String }))("json") + assert.strictEqual(result.test, "content") + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + const formData = new FormData() + formData.append( + "json", + new Blob([JSON.stringify({ test: "content" })], { type: "application/json" }), + "test.json" + ) + assert.strictEqual((yield* HttpClient.post("/upload", { body: HttpBody.formData(formData) })).status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("schemaBodyFormJson url encoded", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "POST", + "/upload", + Effect.gen(function*() { + const result = yield* HttpServerRequest.schemaBodyFormJson(Schema.Struct({ test: Schema.String }))("json") + assert.strictEqual(result.test, "content") + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + const response = yield* HttpClient.post("/upload", { + body: HttpBody.urlParams(UrlParams.fromInput({ json: JSON.stringify({ test: "content" }) })) + }) + assert.strictEqual(response.status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("tracing", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/", + Effect.flatMap(Effect.currentSpan, (_) => HttpServerResponse.json({ spanId: _.spanId, parent: _.parent })) + ).pipe(HttpRouter.serve, Layer.build) + const requestSpan = yield* Effect.makeSpan("client request") + const body = yield* HttpClient.get("/").pipe( + Effect.flatMap((response) => response.json), + Effect.provideService( + Tracer.Tracer, + Tracer.make({ + span(options) { + assert.strictEqual(options.name, "http.client GET") + assert.strictEqual(options.kind, "client") + assert(options.parent._tag === "Some") + if (options.parent.value._tag !== "Span") throw new Error("Expected span parent") + assert.strictEqual(options.parent.value.name, "request parent") + return requestSpan + } + }) + ), + Effect.withSpan("request parent"), + Effect.repeat({ times: 2 }) + ) + assert.strictEqual((body as any).parent._tag, "Some") + assert.strictEqual((body as any).parent.value.spanId, requestSpan.spanId) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("html", () => + Effect.gen(function*() { + yield* HttpRouter.addAll([ + HttpRouter.route("GET", "/home", HttpServerResponse.html("")), + HttpRouter.route( + "GET", + "/about", + HttpServerResponse.html`${Effect.succeed("")}` + ), + HttpRouter.route( + "GET", + "/stream", + HttpServerResponse.htmlStream`${Stream.make("", 123, "hello")}` + ) + ]).pipe(HttpRouter.serve, Layer.build) + assert.strictEqual(yield* HttpClient.get("/home").pipe(Effect.flatMap((_) => _.text)), "") + assert.strictEqual(yield* HttpClient.get("/about").pipe(Effect.flatMap((_) => _.text)), "") + assert.strictEqual( + yield* HttpClient.get("/stream").pipe(Effect.flatMap((_) => _.text)), + "123hello" + ) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("setCookie", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/home", + HttpServerResponse.empty().pipe( + HttpServerResponse.setCookieUnsafe("test", "value"), + HttpServerResponse.setCookieUnsafe("test2", "value2", { + httpOnly: true, + secure: true, + sameSite: "lax", + partitioned: true, + path: "/", + domain: "example.com", + expires: new Date(2022, 1, 1), + maxAge: "5 minutes" + }) + ) + ).pipe(HttpRouter.serve, Layer.build) + const response = yield* HttpClient.get("/home") + assert.deepStrictEqual( + response.cookies.toJSON(), + Cookies.fromReadonlyRecord({ + test: Cookies.makeCookieUnsafe("test", "value"), + test2: Cookies.makeCookieUnsafe("test2", "value2", { + httpOnly: true, + secure: true, + sameSite: "lax", + partitioned: true, + path: "/", + domain: "example.com", + expires: new Date(2022, 1, 1), + maxAge: Duration.minutes(5) + }) + }).toJSON() + ) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.live("uninterruptible routes", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/home", + Effect.gen(function*() { + const fiber = Fiber.getCurrent()! + setTimeout(() => fiber.interruptUnsafe(fiber.id), 10) + yield* Effect.sleep(50) + return HttpServerResponse.empty() + }), + { uninterruptible: true } + ).pipe(HttpRouter.serve, Layer.build) + assert.strictEqual((yield* HttpClient.get("/home")).status, 204) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.live("disposes after a client aborts a handler awaiting an upstream request", () => + Effect.gen(function*() { + const upstreamStarted = Latch.makeUnsafe() + const upstream = yield* Effect.acquireRelease( + Effect.sync(() => { + const controller = new AbortController() + const server = Deno.serve( + { hostname: "127.0.0.1", port: 0, onListen: () => {}, signal: controller.signal }, + (request) => { + upstreamStarted.openUnsafe() + return new Promise((resolve) => { + request.signal.addEventListener("abort", () => resolve(new Response()), { once: true }) + controller.signal.addEventListener("abort", () => resolve(new Response()), { once: true }) + }) + } + ) + return { controller, server } + }), + ({ controller, server }) => + Effect.sync(() => controller.abort()).pipe(Effect.andThen(Effect.promise(() => server.finished))) + ) + const upstreamPort = (upstream.server.addr as Deno.NetAddr).port + const router = HttpRouter.use((router) => + router.add( + "GET", + "/", + Effect.gen(function*() { + yield* HttpClient.head(`http://127.0.0.1:${upstreamPort}`) + return HttpServerResponse.empty() + }) + ) + ) + const serverLayer = DenoHttpServer.layer({ + hostname: "127.0.0.1", + port: 0, + onListen: () => {}, + gracefulShutdownTimeout: "100 millis" + }) + const services = Layer.merge(serverLayer, FetchHttpClient.layer) + const runtime = yield* Effect.acquireRelease( + Effect.sync(() => + ManagedRuntime.make(Layer.merge( + services, + HttpRouter.serve(router).pipe(Layer.provide(services)) + )) + ), + (runtime) => Effect.promise(() => runtime.dispose()) + ) + yield* Effect.promise(() => runtime.context()) + const downstreamServer = yield* Effect.promise(() => runtime.runPromise(HttpServer.HttpServer)) + const downstreamPort = (downstreamServer.address as HttpServer.TcpAddress).port + + const controller = new AbortController() + const downstream = fetch(`http://127.0.0.1:${downstreamPort}`, { + signal: controller.signal + }).catch(() => undefined) + yield* upstreamStarted.await + controller.abort() + yield* Effect.promise(() => downstream) + + const disposed = yield* Effect.promise(() => runtime.dispose()).pipe(Effect.timeoutOption("2 seconds")) + assert.strictEqual(disposed._tag, "Some") + })) + + describe("HttpServerRespondable", () => { + it.effect("error/schema", () => + Effect.gen(function*() { + class CustomError extends Schema.Error("CustomError")({ + _tag: Schema.tag("CustomError"), + name: Schema.String + }) { + [HttpServerRespondable.symbol]() { + return HttpServerResponse.schemaJson(CustomError)(this, { status: 599 }) + } + } + yield* HttpRouter.add("GET", "/home", new CustomError({ name: "test" })).pipe( + HttpRouter.serve, + Layer.build + ) + const response = yield* HttpClient.get("/home") + assert.strictEqual(response.status, 599) + assert.deepStrictEqual( + yield* HttpClientResponse.schemaBodyJson(CustomError)(response), + new CustomError({ name: "test" }) + ) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("httpapi error", () => + Effect.gen(function*() { + yield* HttpRouter.add("GET", "/home", new HttpApiError.BadRequest({})).pipe( + HttpRouter.serve, + Layer.build + ) + assert.strictEqual((yield* HttpClient.get("/home")).status, 400) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + }) + + it.effect("RouterConfig", () => + Effect.gen(function*() { + yield* HttpRouter.add("GET", "/:param", Effect.succeed(HttpServerResponse.empty())).pipe( + HttpRouter.serve, + Layer.build + ) + assert.strictEqual((yield* HttpClient.get("/123456")).status, 404) + assert.strictEqual((yield* HttpClient.get("/12345")).status, 204) + }).pipe( + Effect.provide([ + DenoHttpServer.layerTest, + Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 5 }) + ]) + )) + + it.effect("HttpRouter prefixed", () => + Effect.gen(function*() { + const handler = HttpRouter.serve(HttpRouter.use(Effect.fnUntraced(function*(router_) { + const router = router_.prefixed("/todos") + yield* router.add( + "GET", + "/:id", + Effect.flatMap(HttpRouter.schemaParams(IdParams), ({ id }) => todoResponse({ id, title: "test" })) + ) + yield* router.addAll([ + HttpRouter.route("GET", "/", Effect.succeed(HttpServerResponse.text("root"))) + ]) + }))) + yield* Layer.build(handler) + assert.deepStrictEqual( + yield* HttpClient.get("/todos/1").pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo))), + { id: 1, title: "test" } + ) + assert.strictEqual(yield* HttpClient.get("/todos").pipe(Effect.flatMap((_) => _.text)), "root") + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("cancels a file body for HEAD requests", () => + Effect.gen(function*() { + let cancelled = false + const fileResponse = yield* HttpServerResponse.file(fixture).pipe( + Effect.updateService(HttpPlatform.HttpPlatform, (platform) => ({ + ...platform, + fileResponse: (path, options) => + Effect.map(platform.fileResponse(path, options), (response) => { + assert.strictEqual(response.body._tag, "Raw") + const source = (response.body as HttpBody.Raw).body + assert(source instanceof ReadableStream) + const reader = source.getReader() + const body = new ReadableStream({ + pull(controller) { + return reader.read().then(({ done, value }) => { + if (done) controller.close() + else controller.enqueue(value) + }) + }, + cancel(reason) { + cancelled = true + return reader.cancel(reason) + } + }) + return HttpServerResponse.raw(body, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + cookies: response.cookies + }) + }) + })) + ) + yield* fileResponse.pipe( + Effect.succeed, + HttpServer.serveEffect() + ) + const response = yield* HttpClient.head("/") + assert.strictEqual(response.status, 200) + assert(cancelled) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("ignores cancellation errors for HEAD response bodies", () => { + const body = new ReadableStream() + const reader = body.getReader() + return Effect.gen(function*() { + yield* HttpServerResponse.raw(body).pipe( + Effect.succeed, + HttpServer.serveEffect() + ) + const response = yield* HttpClient.head("/") + assert.strictEqual(response.status, 200) + }).pipe( + Effect.ensuring(Effect.sync(() => reader.releaseLock())), + Effect.provide(DenoHttpServer.layerTest) + ) + }) + + it.effect("round trips WebSocket frames and closes cleanly", () => + Effect.gen(function*() { + yield* serveWebSocket(Effect.fnUntraced(function*(socket) { + const write = yield* socket.writer + yield* socket.runRaw((message) => write(message)) + })) + const server = yield* HttpServer.HttpServer + const port = (server.address as HttpServer.TcpAddress).port + const messages = yield* connectWebSocket(`ws://127.0.0.1:${port}/`, (socket) => socket.send("hello"), 1) + assert.deepStrictEqual(messages, ["hello"]) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("preserves eager WebSocket frames across an async boundary", () => + Effect.gen(function*() { + yield* serveWebSocket(Effect.fnUntraced(function*(socket) { + yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 0))) + const write = yield* socket.writer + yield* socket.runRaw((message) => write(message)) + })) + + const server = yield* HttpServer.HttpServer + const port = (server.address as HttpServer.TcpAddress).port + const messages = yield* connectWebSocket(`ws://127.0.0.1:${port}/`, (socket) => { + socket.send("first") + socket.send("second") + }, 2) + + assert.deepStrictEqual(messages, ["first", "second"]) + }).pipe(Effect.provide(DenoHttpServer.layerTest))) + + it.effect("delivers binary WebSocket frames as Uint8Array", () => + Effect.gen(function*() { + const received = yield* Queue.unbounded() + yield* serveWebSocket(Effect.fnUntraced(function*(socket) { + yield* socket.runRaw((message) => { + assert(message instanceof Uint8Array) + return Queue.offer(received, message) + }) + })) + + const server = yield* HttpServer.HttpServer + const port = (server.address as HttpServer.TcpAddress).port + const socket = yield* openWebSocket(`ws://127.0.0.1:${port}/`) + socket.send(new Uint8Array([1, 2, 3])) + + assert.deepStrictEqual(yield* Queue.take(received), new Uint8Array([1, 2, 3])) + socket.close() + }).pipe(Effect.provide(DenoHttpServer.layerTest))) +}) + +const serveWebSocket = ( + run: (socket: Socket.Socket) => Effect.Effect +) => + HttpRouter.add( + "GET", + "/", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const socket = yield* request.upgrade + yield* run(socket) + return HttpServerResponse.empty() + }) + ).pipe(HttpRouter.serve, Layer.build) + +const openWebSocket = (url: string) => + Effect.acquireRelease( + Effect.callback((resume) => { + const socket = new WebSocket(url) + socket.addEventListener("open", () => resume(Effect.succeed(socket)), { once: true }) + socket.addEventListener("error", () => resume(Effect.fail(new Error("WebSocket connection failed"))), { + once: true + }) + }), + (socket) => Effect.sync(() => socket.close()) + ) + +const connectWebSocket = (url: string, onOpen: (socket: WebSocket) => void, messageCount: number) => + Effect.acquireUseRelease( + openWebSocket(url), + (socket) => + Effect.gen(function*() { + const messages: Array = [] + const fiber = yield* Effect.callback((resume) => { + socket.addEventListener("message", (event) => { + messages.push(event.data) + if (messages.length === messageCount) resume(Effect.void) + }) + socket.addEventListener("error", () => resume(Effect.fail(new Error("WebSocket connection failed"))), { + once: true + }) + onOpen(socket) + }).pipe(Effect.forkChild) + yield* Fiber.join(fiber) + return messages + }), + (socket) => Effect.sync(() => socket.close()) + ) diff --git a/.context/effect/packages/platform/deno/test/DenoKeyValueStore.test.ts b/.context/effect/packages/platform/deno/test/DenoKeyValueStore.test.ts new file mode 100644 index 000000000..324894789 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoKeyValueStore.test.ts @@ -0,0 +1,12 @@ +import * as DenoKeyValueStore from "@effect/platform-deno/DenoKeyValueStore" +import { beforeAll, describe } from "@effect/vitest" +import { testLayer } from "../../../effect/test/unstable/persistence/KeyValueStore.test.ts" + +beforeAll(() => { + localStorage.clear() + sessionStorage.clear() +}) + +describe("KeyValueStore / layerLocalStorage", () => testLayer(DenoKeyValueStore.layerLocalStorage)) + +describe("KeyValueStore / layerSessionStorage", () => testLayer(DenoKeyValueStore.layerSessionStorage)) diff --git a/.context/effect/packages/platform/deno/test/DenoPath.test.ts b/.context/effect/packages/platform/deno/test/DenoPath.test.ts new file mode 100644 index 000000000..986a7cb15 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoPath.test.ts @@ -0,0 +1,39 @@ +import * as DenoPath from "@effect/platform-deno/DenoPath" +import { assert, it } from "@effect/vitest" +import { Effect, Path } from "effect" + +it.layer(DenoPath.layer)("Integration", (it) => { + it.effect("runs the demo program", () => + Effect.gen(function*() { + // Access the Path service + const path = yield* Path.Path + + // Join parts of a path to create a complete file path + const tmpPath = path.join("tmp", "file.txt") + + assert.strictEqual(tmpPath, "tmp/file.txt") + })) +}) + +it.layer(DenoPath.layerPosix)("POSIX file URLs", (it) => { + it.effect("uses POSIX conversions", () => + Effect.gen(function*() { + const path = yield* Path.Path + + assert.strictEqual(yield* path.fromFileUrl(new URL("file:///tmp/file.txt")), "/tmp/file.txt") + assert.strictEqual((yield* path.toFileUrl("/tmp/file.txt")).href, "file:///tmp/file.txt") + })) +}) + +it.layer(DenoPath.layerWin32)("Windows file URLs", (it) => { + it.effect("uses Windows conversions", () => + Effect.gen(function*() { + const path = yield* Path.Path + + assert.strictEqual(yield* path.fromFileUrl(new URL("file:///C:/Users/me/file.txt")), "C:\\Users\\me\\file.txt") + assert.strictEqual( + (yield* path.toFileUrl("C:\\Users\\me\\file.txt")).href, + "file:///C:/Users/me/file.txt" + ) + })) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoRedis.integration.test.ts b/.context/effect/packages/platform/deno/test/DenoRedis.integration.test.ts new file mode 100644 index 000000000..75555981e --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoRedis.integration.test.ts @@ -0,0 +1,174 @@ +import * as DenoRedis from "@effect/platform-deno/DenoRedis" +import { assert, it } from "@effect/vitest" +import { RedisContainer } from "@testcontainers/redis" +import { Deferred, Effect, Fiber, Layer, Schema } from "effect" +import { PersistedQueue, Persistence } from "effect/unstable/persistence" +import * as PersistedCacheTest from "../../../effect/test/unstable/persistence/PersistedCacheTest.ts" +import * as PersistedQueueTest from "../../../effect/test/unstable/persistence/PersistedQueueTest.ts" + +const RedisLayer = Layer.unwrap( + Effect.gen(function*() { + const container = yield* Effect.acquireRelease( + Effect.promise(() => new RedisContainer("redis:alpine").start()), + (container) => Effect.promise(() => container.stop()) + ) + return DenoRedis.layer({ + url: `redis://${container.getHost()}:${container.getMappedPort(6379)}` + }) + }).pipe( + Effect.catchCause(() => Effect.fail(new PersistedCacheTest.TransientError())) + ) +) + +PersistedCacheTest.suite( + "DenoRedis", + Persistence.layerRedis.pipe(Layer.provide(RedisLayer)) +) + +PersistedQueueTest.suite( + "DenoRedis", + // short intervals so the periodic reset runs while the suite's takes are + // in flight + PersistedQueue.layerStoreRedis({ + pollInterval: "50 millis", + lockRefreshInterval: "100 millis" + }).pipe(Layer.provide(RedisLayer)) +) + +const PersistedQueueRedisLayer = Layer.mergeAll( + RedisLayer, + PersistedQueue.layer.pipe( + Layer.provideMerge( + PersistedQueue.layerStoreRedis().pipe(Layer.provide(RedisLayer)) + ) + ) +) + +it.layer(PersistedQueueRedisLayer, { timeout: "30 seconds" })( + "PersistedQueue (DenoRedis)", + (it) => { + it.effect("moves exhausted elements to the failed list", () => + Effect.gen(function*() { + const redis = yield* DenoRedis.DenoRedis + const queueName = "test-redis-failed" + + const queue = yield* PersistedQueue.make({ + name: queueName, + schema: RedisItem + }) + const id = yield* queue.offer({ n: 42 }) + const error = yield* queue.take(() => Effect.fail("boom"), { maxAttempts: 1 }).pipe(Effect.flip) + assert.strictEqual(error, "boom") + + const failed = yield* redis.use((client) => client.lrange(`effectq:${queueName}:failed`, 0, -1)) + assert.strictEqual(failed.length, 1) + const failedItem = JSON.parse(failed[0]) + assert.strictEqual(failedItem.id, id) + assert.deepStrictEqual(failedItem.element, { n: 42 }) + assert.strictEqual(failedItem.attempts, 1) + + const pending = yield* redis.use((client) => client.hlen(`effectq:${queueName}:pending`)) + assert.strictEqual(pending, 0) + })) + } +) + +it.effect("closes the connection when interrupted during acquisition", () => + Effect.gen(function*() { + const listener = yield* makeListener + const authReceived = yield* Deferred.make() + const sendAuthReply = yield* Deferred.make() + const server = yield* Effect.gen(function*() { + const connection = yield* accept(listener) + yield* read(connection) + yield* Deferred.succeed(authReceived, void 0) + yield* Deferred.await(sendAuthReply) + yield* write(connection, "+OK\r\n") + return yield* read(connection) + }).pipe(Effect.forkChild) + + const port = (listener.addr as Deno.NetAddr).port + const layerFiber = yield* Layer.build(DenoRedis.layer({ + url: `redis://:secret@127.0.0.1:${port}` + })).pipe(Effect.forkChild({ startImmediately: true })) + + yield* Deferred.await(authReceived) + const interruptFiber = yield* Fiber.interrupt(layerFiber).pipe( + Effect.forkChild({ startImmediately: true }) + ) + yield* Deferred.succeed(sendAuthReply, void 0) + yield* Fiber.join(interruptFiber) + + assert.isNull(yield* Fiber.join(server)) + })) + +it.effect("uses the URL username for two-argument AUTH", () => + Effect.gen(function*() { + const listener = yield* makeListener + const server = yield* Effect.gen(function*() { + const connection = yield* accept(listener) + const request = yield* read(connection) + yield* write(connection, "+OK\r\n") + return request + }).pipe(Effect.forkChild) + + const port = (listener.addr as Deno.NetAddr).port + yield* Layer.build(DenoRedis.layer({ + url: `redis://alice:secret@127.0.0.1:${port}`, + password: undefined + })) + + assert.strictEqual( + yield* Fiber.join(server), + "*3\r\n$4\r\nAUTH\r\n$5\r\nalice\r\n$6\r\nsecret\r\n" + ) + })) + +it.effect("prefers explicit credentials over URL credentials", () => + Effect.gen(function*() { + const listener = yield* makeListener + const server = yield* Effect.gen(function*() { + const connection = yield* accept(listener) + const request = yield* read(connection) + yield* write(connection, "+OK\r\n") + return request + }).pipe(Effect.forkChild) + + const port = (listener.addr as Deno.NetAddr).port + yield* Layer.build(DenoRedis.layer({ + url: `redis://alice:secret@127.0.0.1:${port}`, + username: "bob", + password: "other" + })) + + assert.strictEqual( + yield* Fiber.join(server), + "*3\r\n$4\r\nAUTH\r\n$3\r\nbob\r\n$5\r\nother\r\n" + ) + })) + +const RedisItem = Schema.Struct({ + n: Schema.Number +}) + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +const makeListener = Effect.acquireRelease( + Effect.sync(() => Deno.listen({ hostname: "127.0.0.1", port: 0 })), + (listener) => Effect.sync(() => listener.close()) +) + +const accept = (listener: Deno.TcpListener) => + Effect.acquireRelease( + Effect.promise(() => listener.accept()), + (connection) => Effect.sync(() => connection.close()) + ) + +const read = (connection: Deno.TcpConn) => + Effect.promise(() => { + const buffer = new Uint8Array(256) + return connection.read(buffer).then((size) => size === null ? null : decoder.decode(buffer.subarray(0, size))) + }) + +const write = (connection: Deno.TcpConn, value: string) => Effect.promise(() => connection.write(encoder.encode(value))) diff --git a/.context/effect/packages/platform/deno/test/DenoSocket.test.ts b/.context/effect/packages/platform/deno/test/DenoSocket.test.ts new file mode 100644 index 000000000..a47fac063 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoSocket.test.ts @@ -0,0 +1,332 @@ +import * as DenoSocket from "@effect/platform-deno/DenoSocket" +import { assert, describe, it } from "@effect/vitest" +import { Deferred, Effect, Fiber, Queue } from "effect" +import * as Stream from "effect/Stream" +import * as Socket from "effect/unstable/socket/Socket" + +const makeTcpServer = Effect.acquireRelease( + Effect.sync(() => Deno.listen({ hostname: "127.0.0.1", port: 0 })), + (listener) => Effect.sync(() => listener.close()) +) + +const makeTempDir = Effect.acquireRelease( + Effect.promise(() => Deno.makeTempDir()), + (path) => Effect.promise(() => Deno.remove(path, { recursive: true })) +) + +const runEchoServer = (listener: Deno.Listener) => + Effect.forever( + Effect.promise(() => listener.accept().then((conn) => conn.readable.pipeTo(conn.writable))) + ) + +const makeTestConn = (options?: { + readonly data?: Uint8Array | undefined + readonly pendingRead?: boolean | undefined + readonly closeReadableOnCloseWrite?: boolean | undefined + readonly setNoDelay?: ((value?: boolean) => void) | undefined + readonly setKeepAlive?: ((value?: boolean) => void) | undefined +}) => { + let controller!: ReadableStreamDefaultController + let readableClosed = false + let closeWrites = 0 + const readable = new ReadableStream({ + start(value) { + controller = value + if (options?.data) value.enqueue(options.data) + if (options?.pendingRead !== true) { + readableClosed = true + value.close() + } + } + }) + const conn = { + readable, + writable: new WritableStream(), + read: () => Promise.resolve(null), + write: (data: Uint8Array) => Promise.resolve(data.length), + close() { + if (readableClosed) return + readableClosed = true + controller.close() + }, + closeWrite() { + closeWrites++ + if (options?.closeReadableOnCloseWrite && !readableClosed) { + readableClosed = true + controller.close() + } + return Promise.resolve() + }, + ref() {}, + unref() {}, + localAddr: { transport: "tcp", hostname: "127.0.0.1", port: 0 }, + remoteAddr: { transport: "tcp", hostname: "127.0.0.1", port: 0 }, + [Symbol.dispose]() { + this.close() + }, + ...(options?.setNoDelay ? { setNoDelay: options.setNoDelay } : {}), + ...(options?.setKeepAlive ? { setKeepAlive: options.setKeepAlive } : {}) + } as Deno.Conn + return { conn, closeWrites: () => closeWrites } +} + +const replaceDenoConnect = ( + connect: (options: Deno.ConnectOptions | Deno.UnixConnectOptions) => Promise +) => + Effect.acquireRelease( + Effect.sync(() => { + const descriptor = Object.getOwnPropertyDescriptor(Deno, "connect")! + Object.defineProperty(Deno, "connect", { ...descriptor, value: connect }) + return descriptor + }), + (descriptor) => Effect.sync(() => Object.defineProperty(Deno, "connect", descriptor)) + ) + +describe("DenoSocket", () => { + it.effect("echoes over TCP", () => + Effect.gen(function*() { + const listener = yield* makeTcpServer + yield* runEchoServer(listener).pipe(Effect.forkScoped) + const address = listener.addr as Deno.NetAddr + + const output = yield* Stream.make("Hello", "World").pipe( + Stream.encodeText, + Stream.pipeThroughChannel(DenoSocket.makeTcpChannel({ hostname: address.hostname, port: address.port })), + Stream.decodeText(), + Stream.mkString + ) + + assert.strictEqual(output, "HelloWorld") + })) + + it.effect("keeps reading after the write side closes", () => + Effect.gen(function*() { + const listener = yield* makeTcpServer + const address = listener.addr as Deno.NetAddr + const writeClosed = yield* Deferred.make() + const encoder = new TextEncoder() + const decoder = new TextDecoder() + + yield* Effect.gen(function*() { + const conn = yield* Effect.promise(() => listener.accept()) + const chunks: Array = [] + const read: Effect.Effect = Effect.suspend(() => { + const buffer = new Uint8Array(5) + return Effect.promise(() => conn.read(buffer)).pipe( + Effect.flatMap((size) => { + if (size === null) return Effect.void + chunks.push(decoder.decode(buffer.subarray(0, size))) + return read + }) + ) + }) + yield* read + assert.strictEqual(chunks.join(""), "Hello") + yield* Deferred.succeed(writeClosed, undefined) + const writer = conn.writable.getWriter() + yield* Effect.promise(() => writer.ready.then(() => writer.write(encoder.encode("Hello")))) + writer.releaseLock() + yield* Effect.promise(() => conn.closeWrite()) + }).pipe(Effect.forkScoped) + + const socket = yield* DenoSocket.makeTcp({ hostname: address.hostname, port: address.port }) + const messages = yield* Queue.unbounded() + const runFiber = yield* socket.run((chunk) => Queue.offer(messages, chunk)).pipe(Effect.forkChild) + yield* Effect.scoped( + socket.writer.pipe(Effect.flatMap((write) => write(encoder.encode("Hello")))) + ) + yield* Deferred.await(writeClosed) + + assert.deepStrictEqual(yield* Queue.take(messages), encoder.encode("Hello")) + yield* Fiber.join(runFiber) + })) + + it.effect("half-closes an empty channel input", () => + Effect.gen(function*() { + const listener = yield* makeTcpServer + const address = listener.addr as Deno.NetAddr + const encoder = new TextEncoder() + + yield* Effect.gen(function*() { + const conn = yield* Effect.promise(() => listener.accept()) + assert.strictEqual(yield* Effect.promise(() => conn.read(new Uint8Array(1))), null) + const writer = conn.writable.getWriter() + yield* Effect.promise(() => writer.write(encoder.encode("Closed"))) + writer.releaseLock() + yield* Effect.promise(() => conn.closeWrite()) + }).pipe(Effect.forkScoped) + + const output = yield* Stream.empty.pipe( + Stream.pipeThroughChannel(DenoSocket.makeTcpChannel({ hostname: address.hostname, port: address.port })), + Stream.decodeText(), + Stream.mkString + ) + + assert.strictEqual(output, "Closed") + })) + + it.effect("does not carry a consumed half-close into a second run", () => + Effect.gen(function*() { + const encoder = new TextEncoder() + const first = makeTestConn({ pendingRead: true, closeReadableOnCloseWrite: true }) + const second = makeTestConn({ data: encoder.encode("Second") }) + const connections = [first.conn, second.conn] + let index = 0 + const socket = yield* DenoSocket.fromConn(Effect.sync(() => connections[index++]!)) + const opened = yield* Deferred.make() + + const firstRun = yield* socket.run(() => {}, { + onOpen: Deferred.succeed(opened, undefined) + }).pipe(Effect.forkChild) + yield* Deferred.await(opened) + yield* Effect.scoped(socket.writer) + yield* Fiber.join(firstRun) + + const received: Array = [] + yield* socket.run((chunk) => Effect.sync(() => received.push(chunk))) + + assert.deepStrictEqual(received, [encoder.encode("Second")]) + assert.strictEqual(first.closeWrites(), 1) + assert.strictEqual(second.closeWrites(), 0) + })) + + it.effect("maps connection refusal to SocketOpenError", () => + Effect.gen(function*() { + const listener = Deno.listen({ hostname: "127.0.0.1", port: 0 }) + const address = listener.addr as Deno.NetAddr + listener.close() + const socket = yield* DenoSocket.makeTcp({ hostname: address.hostname, port: address.port }) + + const error = yield* socket.run(() => {}).pipe(Effect.flip) + + assert.instanceOf(error, Socket.SocketError) + assert.strictEqual(error.reason._tag, "SocketOpenError") + if (error.reason._tag === "SocketOpenError") { + assert.strictEqual(error.reason.kind, "Unknown") + assert.instanceOf(error.reason.cause, Deno.errors.ConnectionRefused) + } + })) + + it.effect("applies TCP tuning options and ignores them for Unix", () => + Effect.gen(function*() { + const noDelay: Array = [] + const keepAlive: Array = [] + const tcp = makeTestConn({ + setNoDelay: (value) => noDelay.push(value), + setKeepAlive: (value) => keepAlive.push(value) + }) + const unix = makeTestConn() + const connections = [tcp.conn, unix.conn] + const connectOptions: Array = [] + let index = 0 + yield* replaceDenoConnect((options) => { + connectOptions.push(options) + return Promise.resolve(connections[index++]!) + }) + + const tcpSocket = yield* DenoSocket.makeTcp({ port: 1, noDelay: false, keepAlive: true }) + yield* tcpSocket.run(() => {}) + const unixSocket = yield* DenoSocket.makeTcp({ + transport: "unix", + path: "/unused.sock", + noDelay: true, + keepAlive: false + }) + yield* unixSocket.run(() => {}) + + assert.deepStrictEqual(noDelay, [false]) + assert.deepStrictEqual(keepAlive, [true]) + assert.deepStrictEqual(connectOptions, [ + { port: 1 }, + { transport: "unix", path: "/unused.sock" } + ]) + })) + + it.effect("echoes over Unix sockets", () => + Effect.gen(function*() { + const directory = yield* makeTempDir + const path = `${directory}/echo.sock` + const listener = yield* Effect.acquireRelease( + Effect.sync(() => Deno.listen({ transport: "unix", path })), + (listener) => Effect.sync(() => listener.close()) + ) + yield* runEchoServer(listener).pipe(Effect.forkScoped) + + const output = yield* Stream.make("Hello", "Unix").pipe( + Stream.encodeText, + Stream.pipeThroughChannel(DenoSocket.makeTcpChannel({ transport: "unix", path })), + Stream.decodeText(), + Stream.mkString + ) + + assert.strictEqual(output, "HelloUnix") + })) + + it.effect("uses Deno's native WebSocket", () => + Effect.gen(function*() { + const server = yield* Effect.acquireRelease( + Effect.sync(() => + Deno.serve( + { hostname: "127.0.0.1", port: 0, onListen: () => {} }, + (request) => { + const { response, socket } = Deno.upgradeWebSocket(request) + socket.onmessage = (event) => socket.send(event.data) + return response + } + ) + ), + (server) => Effect.promise(() => server.shutdown()) + ) + const address = server.addr as Deno.NetAddr + const messages = yield* Queue.unbounded() + + yield* Effect.gen(function*() { + const socket = yield* Socket.Socket + const runFiber = yield* socket.run((chunk) => Queue.offer(messages, chunk)).pipe(Effect.forkChild) + const write = yield* socket.writer + yield* write("Hello WebSocket") + + assert.deepStrictEqual(yield* Queue.take(messages), new TextEncoder().encode("Hello WebSocket")) + yield* Fiber.interrupt(runFiber) + }).pipe( + Effect.scoped, + Effect.provide(DenoSocket.layerWebSocket(`ws://${address.hostname}:${address.port}`, { + closeCodeIsError: () => false + })) + ) + })) + + it.effect("adapts a TransformStream", () => + 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 }), + { closeCodeIsError: () => false } + ) + yield* socket.writer.pipe( + Effect.tap((write) => write("Hello").pipe(Effect.andThen(write("World")))), + Effect.scoped, + Effect.forkChild + ) + 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/.context/effect/packages/platform/deno/test/DenoSocketServer.test.ts b/.context/effect/packages/platform/deno/test/DenoSocketServer.test.ts new file mode 100644 index 000000000..84482d2e0 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoSocketServer.test.ts @@ -0,0 +1,266 @@ +import * as DenoSocket from "@effect/platform-deno/DenoSocket" +import * as DenoSocketServer from "@effect/platform-deno/DenoSocketServer" +import { assert, describe, it } from "@effect/vitest" +import { Cause, Deferred, Effect, Exit, Fiber, Logger, Option, References, Scope } from "effect" +import * as Stream from "effect/Stream" +import { TestClock } from "effect/testing" +import type * as Socket from "effect/unstable/socket/Socket" +import { fromListener } from "../src/internal/denoSocketServer.ts" + +const makeTempDir = Effect.acquireRelease( + Effect.promise(() => Deno.makeTempDir()), + (path) => Effect.promise(() => Deno.remove(path, { recursive: true })) +) + +const echo = (socket: Socket.Socket) => + Effect.scoped( + Effect.gen(function*() { + const write = yield* socket.writer + yield* socket.run(write) + }) + ) + +const makeTestConn = (onClose?: () => void): Deno.Conn => { + const transform = new TransformStream() + return { + readable: transform.readable, + writable: transform.writable, + read: () => Promise.resolve(null), + write: (data) => Promise.resolve(data.length), + close() { + onClose?.() + }, + closeWrite: () => Promise.resolve(), + ref() {}, + unref() {}, + localAddr: { transport: "tcp", hostname: "127.0.0.1", port: 1 }, + remoteAddr: { transport: "tcp", hostname: "127.0.0.1", port: 2 }, + [Symbol.dispose]() {} + } +} + +const makeTestListener = ( + accepts: ReadonlyArray<() => Promise>, + called: ReadonlyArray> +): Deno.Listener => { + let index = 0 + return { + addr: { transport: "tcp", hostname: "127.0.0.1", port: 1 }, + accept() { + Deferred.doneUnsafe(called[index], Exit.void) + return accepts[index++]!() + }, + close() {}, + ref() {}, + unref() {}, + [Symbol.dispose]() {} + } as unknown as Deno.Listener +} + +const neverAccept = () => new Promise(() => {}) +const settlePromises = Effect.promise(() => Promise.resolve()) + +describe("DenoSocketServer", () => { + it.effect("echoes over TCP and reports its address", () => + Effect.gen(function*() { + const server = yield* DenoSocketServer.make({ hostname: "127.0.0.1", port: 0 }) + const address = server.address + assert.strictEqual(address._tag, "TcpAddress") + if (address._tag !== "TcpAddress") return + assert.strictEqual(address.hostname, "127.0.0.1") + assert.notStrictEqual(address.port, 0) + yield* server.run(echo).pipe(Effect.forkScoped) + + const output = yield* Stream.make("Hello", "Deno").pipe( + Stream.encodeText, + Stream.pipeThroughChannel(DenoSocket.makeTcpChannel({ + hostname: address.hostname, + port: address.port + })), + Stream.decodeText(), + Stream.mkString + ) + + assert.strictEqual(output, "HelloDeno") + })) + + it.effect("echoes over Unix sockets and reports its address", () => + Effect.gen(function*() { + const directory = yield* makeTempDir + const path = `${directory}/echo.sock` + const server = yield* DenoSocketServer.make({ transport: "unix", path }) + assert.deepStrictEqual(server.address, { _tag: "UnixAddress", path }) + yield* server.run(echo).pipe(Effect.forkScoped) + + const output = yield* Stream.make("Hello", "Unix").pipe( + Stream.encodeText, + Stream.pipeThroughChannel(DenoSocket.makeTcpChannel({ transport: "unix", path })), + Stream.decodeText(), + Stream.mkString + ) + + assert.strictEqual(output, "HelloUnix") + })) + + it.effect("interrupts handler fibers when run is interrupted", () => + Effect.gen(function*() { + const server = yield* DenoSocketServer.make({ hostname: "127.0.0.1", port: 0 }) + const address = server.address + assert.strictEqual(address._tag, "TcpAddress") + if (address._tag !== "TcpAddress") return + const started = yield* Deferred.make() + const interrupted = yield* Deferred.make() + const runFiber = yield* server.run(() => + Deferred.succeed(started, undefined).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => Deferred.succeed(interrupted, undefined)) + ) + ).pipe(Effect.forkChild) + const conn = yield* Effect.acquireRelease( + Effect.promise(() => Deno.connect({ hostname: address.hostname, port: address.port })), + (conn) => Effect.sync(() => conn.close()) + ) + void conn + yield* Deferred.await(started) + + yield* Fiber.interrupt(runFiber) + + assert.isTrue(yield* Deferred.isDone(interrupted)) + })) + + it.effect("does not surface BadResource when the listener is closed", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const server = yield* DenoSocketServer.make({ hostname: "127.0.0.1", port: 0 }).pipe(Scope.provide(scope)) + const runFiber = yield* server.run(() => Effect.void).pipe(Effect.forkChild) + + yield* Scope.close(scope, Exit.void) + yield* Effect.yieldNow + + assert.isUndefined(runFiber.pollUnsafe()) + yield* Fiber.interrupt(runFiber) + })) + + it.effect("closes with a pending pre-run connection", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const server = yield* DenoSocketServer.make({ hostname: "127.0.0.1", port: 0 }).pipe(Scope.provide(scope)) + const address = server.address + assert.strictEqual(address._tag, "TcpAddress") + if (address._tag !== "TcpAddress") return + const conn = yield* Effect.acquireRelease( + Effect.promise(() => Deno.connect({ hostname: address.hostname, port: address.port })), + (conn) => Effect.sync(() => conn.close()) + ) + void conn + + yield* Scope.close(scope, Exit.void).pipe(Effect.timeout("1 second")) + })) + + it.effect("closes the connection after its handler completes", () => + Effect.gen(function*() { + const called = [Deferred.makeUnsafe(), Deferred.makeUnsafe()] + const handled = yield* Deferred.make() + const closed = yield* Deferred.make() + const listener = makeTestListener([ + () => Promise.resolve(makeTestConn(() => Deferred.doneUnsafe(closed, Exit.void))), + neverAccept + ], called) + const server = fromListener(listener) + yield* server.run(() => Deferred.succeed(handled, undefined)).pipe(Effect.forkChild) + + yield* Deferred.await(handled) + yield* Effect.yieldNow + + assert.isTrue(yield* Deferred.isDone(closed)) + })) + + it.effect("logs accept failures and continues accepting", () => + Effect.gen(function*() { + const called = [Deferred.makeUnsafe(), Deferred.makeUnsafe(), Deferred.makeUnsafe()] + const failure = new Error("accept failed") + const handled = yield* Deferred.make() + const logs: Array<{ readonly cause: Cause.Cause; readonly message: unknown }> = [] + const logger = Logger.make((options) => + logs.push({ cause: options.cause, message: options.message }) + ) + const listener = makeTestListener([ + () => Promise.reject(failure), + () => Promise.resolve(makeTestConn()), + neverAccept + ], called) + const server = fromListener(listener) + yield* server.run(() => Deferred.succeed(handled, undefined)).pipe( + Effect.provide(Logger.layer([logger])), + Effect.provideService(References.UnhandledLogLevel, "Error"), + Effect.forkChild + ) + + yield* Deferred.await(called[0]) + yield* settlePromises + yield* TestClock.adjust(10) + assert.isTrue(yield* Deferred.isDone(handled)) + + assert.strictEqual(logs.length, 1) + assert.deepStrictEqual(logs[0]!.message, ["Unhandled error in SocketServer"]) + assert.deepStrictEqual(Cause.findErrorOption(logs[0]!.cause), Option.some(failure)) + })) + + it.effect("backs off consecutive accept failures", () => + Effect.gen(function*() { + const called = [Deferred.makeUnsafe(), Deferred.makeUnsafe(), Deferred.makeUnsafe()] + const listener = makeTestListener([ + () => Promise.reject(new Error("first")), + () => Promise.reject(new Error("second")), + neverAccept + ], called) + const server = fromListener(listener) + yield* server.run(() => Effect.void).pipe( + Effect.provideService(References.UnhandledLogLevel, undefined), + Effect.forkChild + ) + + yield* Deferred.await(called[0]) + yield* settlePromises + yield* TestClock.adjust(9) + assert.isFalse(yield* Deferred.isDone(called[1])) + yield* TestClock.adjust(1) + assert.isTrue(yield* Deferred.isDone(called[1])) + yield* settlePromises + yield* TestClock.adjust(19) + assert.isFalse(yield* Deferred.isDone(called[2])) + yield* TestClock.adjust(1) + assert.isTrue(yield* Deferred.isDone(called[2])) + })) + + it.effect("resets accept backoff after a successful connection", () => + Effect.gen(function*() { + const called = [ + Deferred.makeUnsafe(), + Deferred.makeUnsafe(), + Deferred.makeUnsafe(), + Deferred.makeUnsafe() + ] + const listener = makeTestListener([ + () => Promise.reject(new Error("first")), + () => Promise.resolve(makeTestConn()), + () => Promise.reject(new Error("after success")), + neverAccept + ], called) + const server = fromListener(listener) + yield* server.run(() => Effect.void).pipe( + Effect.provideService(References.UnhandledLogLevel, undefined), + Effect.forkChild + ) + + yield* Deferred.await(called[0]) + yield* settlePromises + yield* TestClock.adjust(10) + assert.isTrue(yield* Deferred.isDone(called[2])) + yield* settlePromises + yield* TestClock.adjust(9) + assert.isFalse(yield* Deferred.isDone(called[3])) + yield* TestClock.adjust(1) + assert.isTrue(yield* Deferred.isDone(called[3])) + })) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoStdio.test.ts b/.context/effect/packages/platform/deno/test/DenoStdio.test.ts new file mode 100644 index 000000000..936c1dded --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoStdio.test.ts @@ -0,0 +1,50 @@ +import * as DenoStdio from "@effect/platform-deno/DenoStdio" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Stdio from "effect/Stdio" + +const streams = [Deno.stdin, Deno.stdout] as const + +const setIsTerminal = (stdin: boolean, stdout: boolean) => + Effect.sync(() => { + Object.defineProperty(streams[0], "isTerminal", { configurable: true, value: () => stdin }) + Object.defineProperty(streams[1], "isTerminal", { configurable: true, value: () => stdout }) + }) + +const withRestoredIsTerminal = (effect: Effect.Effect) => + Effect.acquireUseRelease( + Effect.sync(() => streams.map((stream) => Object.getOwnPropertyDescriptor(stream, "isTerminal"))), + () => effect, + (descriptors) => + Effect.sync(() => { + streams.forEach((stream, index) => { + const descriptor = descriptors[index] + if (descriptor === undefined) { + Reflect.deleteProperty(stream, "isTerminal") + } else { + Object.defineProperty(stream, "isTerminal", descriptor) + } + }) + }) + ) + +describe("DenoStdio", () => { + it.effect("reads terminal state when the effects run", () => + withRestoredIsTerminal( + Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + + yield* setIsTerminal(true, false) + assert.deepStrictEqual( + yield* Effect.all([stdio.stdinIsTerminal, stdio.stdoutIsTerminal]), + [true, false] + ) + + yield* setIsTerminal(false, true) + assert.deepStrictEqual( + yield* Effect.all([stdio.stdinIsTerminal, stdio.stdoutIsTerminal]), + [false, true] + ) + }).pipe(Effect.provide(DenoStdio.layer)) + )) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoTerminal.test.ts b/.context/effect/packages/platform/deno/test/DenoTerminal.test.ts new file mode 100644 index 000000000..58653d17b --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoTerminal.test.ts @@ -0,0 +1,100 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import { spawn, spawnSync } from "node:child_process" +import { fileURLToPath } from "node:url" + +const fixture = fileURLToPath(new URL("fixtures/deno-terminal.ts", import.meta.url)) + +const runFixture = (mode: string, input: string) => + spawnSync(Deno.execPath(), [fixture, mode], { + encoding: "utf8", + input, + timeout: 2_000 + }) + +const assertResult = (mode: string, input: string, expected: string) => { + const result = runFixture(mode, input) + assert.isUndefined(result.error) + assert.strictEqual(result.status, 0, result.stderr) + assert.isTrue(result.stderr.includes(`RESULT ${expected}`), result.stderr) +} + +const assertOpenResult = (mode: string, input: string, expected: string) => + Effect.callback((resume) => { + const child = spawn(Deno.execPath(), [fixture, mode]) + let stderr = "" + child.stderr.setEncoding("utf8") + child.stderr.on("data", (data) => { + stderr += data + if (stderr.includes("RESULT ")) { + child.stdin.end() + } + }) + child.on("exit", (code) => { + resume( + code === 0 && stderr.includes(`RESULT ${expected}`) + ? Effect.void + : Effect.die(new Error(stderr)) + ) + }) + child.stdin.write(input) + return Effect.sync(() => child.kill()) + }) + +const assertInteropResult = Effect.callback((resume) => { + const child = spawn(Deno.execPath(), [fixture, "interop"]) + let stderr = "" + let sentTerminalInput = false + child.stderr.setEncoding("utf8") + child.stderr.on("data", (data) => { + stderr += data + if (!sentTerminalInput && stderr.includes("READY")) { + sentTerminalInput = true + child.stdin.end("terminal\n") + } + }) + child.on("exit", (code) => { + resume( + code === 0 && stderr.includes("RESULT {\"stdio\":\"stdio\",\"terminal\":\"terminal\"}") + ? Effect.void + : Effect.die(new Error(stderr)) + ) + }) + child.stdin.write("stdio\n") + return Effect.sync(() => child.kill()) +}) + +describe("DenoTerminal", () => { + it("does not install a readline interface until the terminal is used", () => { + assertResult("unused", "", "{\"dataListeners\":0}") + }) + + it("fails a prompt with QuitError after piped input is exhausted", () => { + assertResult("prompts", "y\n", "{\"first\":true,\"second\":\"QuitError\"}") + }) + + it("delivers buffered keypresses before ending the input queue", () => { + assertResult("read-input", "yn", "{\"first\":\"y\",\"second\":\"n\",\"ended\":true}") + }) + + it("flushes an unterminated line before failing readLine with QuitError at EOF", () => { + assertResult("read-line", "last line", "{\"first\":\"last line\",\"second\":\"QuitError\"}") + }) + + it("preserves lines buffered between sequential readLine calls", () => { + assertResult("read-lines", "first\nsecond\n", "{\"first\":\"first\",\"second\":\"second\"}") + }) + + it("fails readLine with QuitError when stdin ended before initialization", () => { + assertResult("read-line-after-end", "", "\"QuitError\"") + }) + + it.effect("disposes readline after its idle TTL", () => + assertOpenResult( + "read-line-disposed", + "line\n", + "{\"line\":\"line\",\"duringTtl\":1,\"dataListeners\":0}" + )) + + it.effect("interoperates with DenoStdio on stdin", () => assertInteropResult) +}) diff --git a/.context/effect/packages/platform/deno/test/DenoWorkerRunner.test.ts b/.context/effect/packages/platform/deno/test/DenoWorkerRunner.test.ts new file mode 100644 index 000000000..78eb72e39 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/DenoWorkerRunner.test.ts @@ -0,0 +1,101 @@ +import * as DenoWorkerRunner from "@effect/platform-deno/DenoWorkerRunner" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as Queue from "effect/Queue" + +type Listener = EventListenerOrEventListenerObject + +const makePort = () => { + const listeners = new Map>() + const added: Array = [] + const removed: Array = [] + const port = { + addEventListener(type: string, listener: Listener | null) { + if (listener === null) return + added.push([type, listener]) + const eventListeners = listeners.get(type) ?? new Set() + eventListeners.add(listener) + listeners.set(type, eventListeners) + }, + removeEventListener(type: string, listener: Listener | null) { + if (listener === null) return + removed.push([type, listener]) + listeners.get(type)?.delete(listener) + }, + postMessage() {}, + start() {}, + close() {} + } as unknown as MessagePort + + return { + added, + removed, + port, + emit(type: string, data: unknown) { + const event = { data } as MessageEvent + for (const listener of listeners.get(type) ?? []) { + if (typeof listener === "function") { + listener(event) + } else { + listener.handleEvent(event) + } + } + } + } +} + +const makeSharedWorker = () => { + const worker = { + onconnect: null as ((event: MessageEvent) => void) | null, + addEventListener() {}, + removeEventListener() {}, + close() {} + } + return { + worker: worker as unknown as MessagePort, + connect(port: MessagePort) { + worker.onconnect?.({ ports: [port] } as unknown as MessageEvent) + } + } +} + +describe("DenoWorkerRunner", () => { + it.effect("removes the registered messageerror listener", () => + Effect.gen(function*() { + const fake = makePort() + const runner = yield* DenoWorkerRunner.make(fake.port).start() + const fiber = yield* Effect.forkChild(runner.run(() => {})) + yield* Effect.yieldNow + + fake.emit("message", [1]) + yield* Fiber.join(fiber) + + const added = fake.added.find(([type]) => type === "messageerror") + const removed = fake.removed.find(([type]) => type === "messageerror") + assert.isDefined(added) + assert.isDefined(removed) + assert.strictEqual(removed[1], added[1]) + })) + + it.effect("emits disconnects for closed SharedWorker ports", () => + Effect.gen(function*() { + const sharedWorker = makeSharedWorker() + const first = makePort() + const second = makePort() + const runner = yield* DenoWorkerRunner.make(sharedWorker.worker).start() + const fiber = yield* Effect.forkChild(runner.run(() => {})) + yield* Effect.yieldNow + + sharedWorker.connect(first.port) + sharedWorker.connect(second.port) + first.emit("message", [1]) + + const disconnects = runner.disconnects + assert.isDefined(disconnects) + assert.strictEqual(yield* Queue.take(disconnects), 0) + + second.emit("message", [1]) + yield* Fiber.join(fiber) + })) +}) diff --git a/.context/effect/packages/platform/deno/test/RpcWorker.test.ts b/.context/effect/packages/platform/deno/test/RpcWorker.test.ts new file mode 100644 index 000000000..23e5c749c --- /dev/null +++ b/.context/effect/packages/platform/deno/test/RpcWorker.test.ts @@ -0,0 +1,21 @@ +import * as DenoWorker from "@effect/platform-deno/DenoWorker" +import { describe } from "@effect/vitest" +import * as Layer from "effect/Layer" +import * as RpcClient from "effect/unstable/rpc/RpcClient" +import * as RpcServer from "effect/unstable/rpc/RpcServer" +import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts" + +describe("RpcWorker", () => { + const WorkerClient = UsersClient.layer.pipe( + Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })), + Layer.provide( + DenoWorker.layer(() => new Worker(new URL("./fixtures/rpc-worker.ts", import.meta.url), { type: "module" })) + ), + Layer.merge( + Layer.succeed(RpcServer.Protocol)({ + supportsAck: true + } as any) + ) + ) + e2eSuite("e2e worker", WorkerClient, false) +}) diff --git a/.context/effect/packages/platform/deno/test/cluster/SocketRunner.test.ts b/.context/effect/packages/platform/deno/test/cluster/SocketRunner.test.ts new file mode 100644 index 000000000..80e70e1fc --- /dev/null +++ b/.context/effect/packages/platform/deno/test/cluster/SocketRunner.test.ts @@ -0,0 +1,195 @@ +import { DenoClusterSocket } from "@effect/platform-deno" +import { assert, describe, it } from "@effect/vitest" +import { BigDecimal, Cause, Deferred, Effect, Exit, Fiber, Layer, Option, PrimaryKey, Schema } from "effect" +import type { Sharding } from "effect/unstable/cluster" +import { + ClusterSchema, + Entity, + MessageStorage, + RunnerAddress, + RunnerHealth, + RunnerStorage, + ShardingConfig, + SocketRunner +} from "effect/unstable/cluster" +import { Rpc, RpcSerialization } from "effect/unstable/rpc" + +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 + }).annotate(ClusterSchema.Persisted, true), + Rpc.make("ProcessVolatile", { + payload: TestPayload, + success: Schema.Void + }).annotate(ClusterSchema.Persisted, false) + ]) + .annotateRpcs(ClusterSchema.Uninterruptible, true) + +const RUNNER_PORT = 50_125 +const SharedStorage = Layer.mergeAll( + RunnerStorage.layerMemory, + MessageStorage.layerMemory +).pipe( + Layer.provide(ShardingConfig.layerDefaults) +) + +const makeRunnerLayer = (port: number, entities: Layer.Layer) => + entities.pipe( + Layer.provideMerge(SocketRunner.layer), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(DenoClusterSocket.layerSocketServer), + Layer.provide(DenoClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("127.0.0.1", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +const makeClientLayer = (port: number) => + SocketRunner.layerClientOnly.pipe( + Layer.provide(DenoClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + runnerAddress: Option.some(RunnerAddress.make("127.0.0.1", port)), + runnerListenAddress: Option.some(RunnerAddress.make("127.0.0.1", port)), + entityTerminationTimeout: 0, + entityMessagePollInterval: 5000, + sendRetryInterval: 100 + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +const IsolationEntity = Entity + .make("IsolationEntity", [ + Rpc.make("BadReply", { + payload: { id: Schema.Number }, + success: Schema.Int + }), + Rpc.make("Slow", { + success: Schema.String + }) + ]) + .annotateRpcs(ClusterSchema.Persisted, false) + +const IsolationEntityLayer = IsolationEntity.toLayer( + Effect.succeed({ + BadReply: () => Effect.succeed(1.5), + Slow: () => Effect.as(Effect.sleep("2 seconds"), "done") + }) +) + +const ISOLATION_PORT = 50_126 + +// 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. +// +// Volatile discard should complete after the request is sent, without waiting for the +// host runner to finish handling it. +describe("SocketRunner", () => { + it.live( + "discarded persisted requests serialize circular values and volatile requests do not wait for replies", + () => + Effect.gen(function*() { + const volatileStarted = yield* Deferred.make() + const releaseVolatile = yield* Deferred.make() + const TestEntityLayer = TestEntity.toLayer( + Effect.succeed({ + Process: () => Effect.void, + ProcessVolatile: () => + Deferred.succeed(volatileStarted, void 0).pipe( + Effect.andThen(Deferred.await(releaseVolatile)) + ) + }) + ) + + yield* Layer.launch(makeRunnerLayer(RUNNER_PORT, TestEntityLayer)).pipe(Effect.forkScoped) + + yield* Effect.sleep("2 seconds") + yield* Effect.gen(function*() { + yield* Effect.sleep("2 seconds") + const makeClient = yield* TestEntity.client + yield* Effect.sleep("3 seconds") + const client = makeClient("entity-1") + + const amount = BigDecimal.fromStringUnsafe("123.45") + yield* client.Process( + TestPayload.make({ id: "req-1", amount }), + { discard: true } + ) + + const volatileFiber = yield* client.ProcessVolatile( + TestPayload.make({ id: "req-2", amount }), + { discard: true } + ).pipe(Effect.forkChild) + + yield* Deferred.await(volatileStarted) + yield* Fiber.join(volatileFiber).pipe( + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.die("volatile discard waited for the entity reply") + }) + ) + yield* Deferred.succeed(releaseVolatile, void 0) + }).pipe( + Effect.provide(makeClientLayer(RUNNER_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide(SharedStorage)), + 30_000 + ) + + it.live( + "a reply serialization failure fails only its own request", + () => + Effect.gen(function*() { + yield* Layer.launch(makeRunnerLayer(ISOLATION_PORT, IsolationEntityLayer)).pipe(Effect.forkScoped) + yield* Effect.sleep("2 seconds") + + yield* Effect.gen(function*() { + const makeClient = yield* IsolationEntity.client + yield* Effect.sleep("3 seconds") + + const slowFiber = yield* makeClient("slow-entity").Slow().pipe(Effect.forkChild) + yield* Effect.sleep("300 millis") + + const badExit = yield* makeClient("bad-entity").BadReply({ id: 1 }).pipe(Effect.exit) + assert.isTrue(Exit.isFailure(badExit), "the unencodable reply must fail the request") + const failure = Exit.isFailure(badExit) ? String(Cause.squash(badExit.cause)) : "" + assert.include(failure, "MalformedMessage", "the caller must receive the real encode error") + assert.notInclude( + failure, + "AlreadyProcessingMessage", + "the request must not be re-sent into the entity's dedup guard" + ) + + const slowExit = yield* Fiber.await(slowFiber) + assert.isTrue( + Exit.isSuccess(slowExit), + "a sibling in-flight request on the same connection must be unaffected" + ) + if (Exit.isSuccess(slowExit)) { + assert.strictEqual(slowExit.value, "done") + } + }).pipe( + Effect.provide(makeClientLayer(ISOLATION_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide(SharedStorage)), + 30_000 + ) +}) diff --git a/.context/effect/packages/platform/deno/test/fixtures/deno-terminal.ts b/.context/effect/packages/platform/deno/test/fixtures/deno-terminal.ts new file mode 100644 index 000000000..3264499d1 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/fixtures/deno-terminal.ts @@ -0,0 +1,127 @@ +import * as DenoStdio from "@effect/platform-deno/DenoStdio" +import * as DenoTerminal from "@effect/platform-deno/DenoTerminal" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Path from "effect/Path" +import * as Queue from "effect/Queue" +import * as Stdio from "effect/Stdio" +import * as Stream from "effect/Stream" +import * as Terminal from "effect/Terminal" +import { Prompt } from "effect/unstable/cli" + +const TerminalLayer = Layer.mergeAll( + DenoStdio.layer, + DenoTerminal.layer, + FileSystem.layerNoop({}), + Path.layer +) + +const prompts = Effect.gen(function*() { + const first = yield* Prompt.run(Prompt.confirm({ message: "First" })) + const second = yield* Prompt.run(Prompt.confirm({ message: "Second" })).pipe(Effect.flip) + return { first, second: second._tag } +}) + +const readInput = Effect.scoped( + Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const input = yield* terminal.readInput + const first = yield* Queue.take(input) + const second = yield* Queue.take(input) + const end = yield* Effect.exit(Queue.take(input)) + return { + first: Option.getOrNull(first.input), + second: Option.getOrNull(second.input), + ended: Exit.isFailure(end) + } + }) +) + +const readLine = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const first = yield* terminal.readLine + const second = yield* terminal.readLine.pipe(Effect.flip) + return { first, second: second._tag } +}) + +const readLines = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const first = yield* terminal.readLine + const second = yield* terminal.readLine + return { first, second } +}) + +const unused = Effect.gen(function*() { + yield* Terminal.Terminal + return { dataListeners: process.stdin.listenerCount("data") } +}) + +const readLineAfterEnd = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + yield* Effect.callback((resume) => { + if (process.stdin.readableEnded) { + resume(Effect.void) + return + } + const onEnd = () => resume(Effect.void) + process.stdin.once("end", onEnd) + process.stdin.resume() + return Effect.sync(() => process.stdin.off("end", onEnd)) + }) + const error = yield* terminal.readLine.pipe(Effect.flip) + return error._tag +}) + +const readLineDisposed = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const line = yield* terminal.readLine + const duringTtl = process.stdin.listenerCount("data") + yield* Effect.sleep("20 millis") + return { line, duringTtl, dataListeners: process.stdin.listenerCount("data") } +}) + +const interop = Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + const terminal = yield* Terminal.Terminal + const line = yield* stdio.stdin.pipe( + Stream.decodeText(), + Stream.splitLines, + Stream.runHead + ) + yield* Effect.sync(() => process.stderr.write("READY\n")) + const terminalLine = yield* terminal.readLine + return { stdio: Option.getOrNull(line), terminal: terminalLine } +}) + +const mode = process.argv[2] +const program = Effect.gen(function*() { + if (mode === "prompts") { + return yield* prompts + } else if (mode === "read-input") { + return yield* readInput + } else if (mode === "read-line") { + return yield* readLine + } else if (mode === "read-lines") { + return yield* readLines + } else if (mode === "unused") { + return yield* unused + } else if (mode === "read-line-after-end") { + return yield* readLineAfterEnd + } else if (mode === "read-line-disposed") { + return yield* readLineDisposed + } else if (mode === "interop") { + return yield* interop + } + return yield* Effect.die(`Unknown mode: ${mode}`) +}) + +Effect.runPromise(program.pipe(Effect.provide(TerminalLayer))).then( + (result) => process.stderr.write(`RESULT ${JSON.stringify(result)}\n`), + (cause) => { + process.stderr.write(`ERROR ${String(cause)}\n`) + process.exitCode = 1 + } +) diff --git a/.context/effect/packages/platform/deno/test/fixtures/rpc-e2e.ts b/.context/effect/packages/platform/deno/test/fixtures/rpc-e2e.ts new file mode 100644 index 000000000..78af7570a --- /dev/null +++ b/.context/effect/packages/platform/deno/test/fixtures/rpc-e2e.ts @@ -0,0 +1,142 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Context, Effect, Fiber, Option, Schedule, Stream } from "effect" +import * as Layer from "effect/Layer" +import * as RpcClient from "effect/unstable/rpc/RpcClient" +import type { RpcClientError } from "effect/unstable/rpc/RpcClientError" +import type * as RpcGroup from "effect/unstable/rpc/RpcGroup" +import * as RpcServer from "effect/unstable/rpc/RpcServer" +import * as RpcTest from "effect/unstable/rpc/RpcTest" +import { AuthClient, AuthLive, TimingLive, User, UserRpcs, UsersLive } from "./rpc-schemas.ts" + +export class UsersClient extends Context.Service< + UsersClient, + RpcClient.RpcClient, RpcClientError> +>()("UsersClient") { + static readonly layer = Layer.effect(UsersClient)(RpcClient.make(UserRpcs)).pipe( + Layer.provide(AuthClient) + ) + static layerTest = Layer.effect(UsersClient)(RpcTest.makeClient(UserRpcs)).pipe( + Layer.provide([UsersLive, AuthLive, TimingLive, AuthClient]) + ) +} + +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 = [] + const fiber = yield* client.StreamUsers({ id: "1" }).pipe( + Stream.take(5), + Stream.runForEach((user) => + Effect.sync(() => { + users.push(user) + }) + ), + Effect.forkChild + ) + yield* Fiber.join(fiber) + 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.live("never", () => + Effect.gen(function*() { + const client = yield* UsersClient + const fiber = yield* client.Never().pipe( + Effect.forkChild + ) + yield* Effect.sleep(500) + assert.isUndefined(fiber.pollUnsafe()) + + yield* Fiber.interrupt(fiber) + + const { supportsAck } = yield* RpcServer.Protocol + if (supportsAck) { + const interrupts = yield* Effect.retry( + Effect.flatMap( + client.GetInterrupts(), + (interrupts) => interrupts === 1 ? Effect.succeed(interrupts) : Effect.fail(interrupts) + ), + { schedule: Schedule.spaced("10 millis"), times: 100 } + ) + assert.equal(interrupts, 1) + } + }).pipe( + RpcClient.withHeaders({ userId: "123" }), + Effect.provide(layer) + )) + + it.effect("timing 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/.context/effect/packages/platform/deno/test/fixtures/rpc-schemas.ts b/.context/effect/packages/platform/deno/test/fixtures/rpc-schemas.ts new file mode 100644 index 000000000..35e43a2af --- /dev/null +++ b/.context/effect/packages/platform/deno/test/fixtures/rpc-schemas.ts @@ -0,0 +1,157 @@ +import { Context, Effect, Layer, Metric, Option, Queue, Schema } from "effect" +import * as Headers from "effect/unstable/http/Headers" +import * as Rpc from "effect/unstable/rpc/Rpc" +import * as RpcGroup from "effect/unstable/rpc/RpcGroup" +import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware" +import * as RpcServer from "effect/unstable/rpc/RpcServer" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +class StreamUsers extends Rpc.make("StreamUsers", { + success: User, + payload: { + id: Schema.String + }, + stream: true +}) {} + +class CurrentUser extends Context.Service()("CurrentUser") {} + +class Unauthorized extends Schema.Error("Unauthorized")({ + _tag: Schema.tag("Unauthorized") +}) {} + +class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { + error: Unauthorized, + requiredForClient: true +}) {} + +class TimingMiddleware extends RpcMiddleware.Service()("TimingMiddleware") {} + +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 } + }), + StreamUsers, + Rpc.make("GetInterrupts", { + success: Schema.Number + }), + Rpc.make("GetEmits", { + success: Schema.Number + }), + Rpc.make("ProduceDefect"), + 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) + +export const AuthLive = Layer.succeed(AuthMiddleware)( + AuthMiddleware.of((effect, options) => + Effect.provideService( + effect, + CurrentUser, + 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") +export const TimingLive = Layer.succeed(TimingMiddleware)( + TimingMiddleware.of((effect) => + effect.pipe( + Effect.tap(Metric.update(rpcSuccesses, 1)), + Effect.tapDefect(() => Metric.update(rpcDefects, 1)), + Effect.ensuring(Metric.update(rpcCount, 1)) + ) + ) +) + +export 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* Queue.bounded(0) + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + interrupts++ + }) + ) + + yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe( + Effect.tap(() => + Effect.sync(() => { + emits++ + }) + ), + Effect.delay(100), + Effect.forever, + Effect.forkScoped + ) + + return mailbox + }), + GetInterrupts: () => Effect.sync(() => interrupts), + GetEmits: () => Effect.sync(() => emits), + ProduceDefect: () => Effect.die("boom"), + 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, { + disableFatalDefects: true +}).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + +export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) => + next({ + ...request, + headers: Headers.set(request.headers, "name", "Logged in user") + })) diff --git a/.context/effect/packages/platform/deno/test/fixtures/rpc-worker.ts b/.context/effect/packages/platform/deno/test/fixtures/rpc-worker.ts new file mode 100644 index 000000000..2282c4bed --- /dev/null +++ b/.context/effect/packages/platform/deno/test/fixtures/rpc-worker.ts @@ -0,0 +1,12 @@ +import * as DenoWorkerRunner from "@effect/platform-deno/DenoWorkerRunner" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as RpcServer from "effect/unstable/rpc/RpcServer" +import { RpcLive } from "./rpc-schemas.ts" + +const MainLive = RpcLive.pipe( + Layer.provide(RpcServer.layerProtocolWorkerRunner), + Layer.provide(DenoWorkerRunner.layer) +) + +Effect.runFork(Layer.launch(MainLive)) diff --git a/.context/effect/packages/platform-node/test/fixtures/text.txt b/.context/effect/packages/platform/deno/test/fixtures/text.txt similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/text.txt rename to .context/effect/packages/platform/deno/test/fixtures/text.txt diff --git a/.context/effect/packages/platform/deno/test/internal/error.test.ts b/.context/effect/packages/platform/deno/test/internal/error.test.ts new file mode 100644 index 000000000..991974ed9 --- /dev/null +++ b/.context/effect/packages/platform/deno/test/internal/error.test.ts @@ -0,0 +1,39 @@ +import { assert, describe, it } from "@effect/vitest" +import { SystemError, type SystemErrorTag } from "effect/PlatformError" +import { handleError } from "../../src/internal/error.ts" + +const withCode = (error: Error, code: string): Error & { readonly code: string } => Object.assign(error, { code }) + +describe("handleError", () => { + const mapError = handleError("FileSystem", "test", "/tmp/test") + const cases: ReadonlyArray = [ + [withCode(new Deno.errors.NotFound(), "ENOENT"), "NotFound"], + [withCode(new Deno.errors.NotADirectory(), "ENOTDIR"), "BadResource"], + [new Deno.errors.AlreadyExists(), "AlreadyExists"], + [withCode(new Deno.errors.AlreadyExists(), "EEXIST"), "AlreadyExists"], + [withCode(new Deno.errors.IsADirectory(), "EISDIR"), "BadResource"], + [withCode(new Deno.errors.PermissionDenied(), "EACCES"), "PermissionDenied"], + [new Deno.errors.NotCapable(), "PermissionDenied"], + [new Deno.errors.BadResource(), "BadResource"], + [new Deno.errors.InvalidData(), "InvalidData"], + [new Deno.errors.TimedOut(), "TimedOut"], + [new Deno.errors.UnexpectedEof(), "UnexpectedEof"], + [new Deno.errors.WouldBlock(), "WouldBlock"], + [new Deno.errors.WriteZero(), "WriteZero"], + [new Error("unrecognised"), "Unknown"] + ] + + for (const [error, tag] of cases) { + it(`maps ${error.name} to ${tag}`, () => { + const platformError = mapError(error) + const reason = platformError.reason + + assert(reason instanceof SystemError) + assert.strictEqual(reason._tag, tag) + assert.strictEqual(reason.module, "FileSystem") + assert.strictEqual(reason.method, "test") + assert.strictEqual(reason.pathOrDescriptor, "/tmp/test") + assert.strictEqual(reason.cause, error) + }) + } +}) diff --git a/.context/effect/packages/platform/deno/tsconfig.json b/.context/effect/packages/platform/deno/tsconfig.json new file mode 100644 index 000000000..58884ac0b --- /dev/null +++ b/.context/effect/packages/platform/deno/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" }, + { "path": "../node-shared" } + ], + "compilerOptions": { + "types": ["deno"] + } +} diff --git a/.context/effect/packages/platform/node-shared/CHANGELOG.md b/.context/effect/packages/platform/node-shared/CHANGELOG.md new file mode 100644 index 000000000..9d53ac5f7 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/CHANGELOG.md @@ -0,0 +1,824 @@ +# @effect/platform-node-shared + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- [#7154](https://github.com/Effect-TS/effect/pull/7154) [`e74c302`](https://github.com/Effect-TS/effect/commit/e74c302afe0368e5d3f15d18c10fc54cf33f9003) Thanks @CDVolvik! - Pass Node's `windowsHide` flag for spawned Windows children by default (except detached processes), with an independent + `windowsHide` option for callers that need visible GUI windows. Process-group cleanup now invokes `taskkill` without a + `cmd.exe` wrapper and hides its window. +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7106](https://github.com/Effect-TS/effect/pull/7106) [`5f1775c`](https://github.com/Effect-TS/effect/commit/5f1775cb060cb3dbf96adb067fd97967da7eca2f) Thanks @tim-smart! - Keep NodeTerminal's readline interface alive briefly between adjacent prompts to avoid a Windows TTY raw-mode hang. + +- [#7143](https://github.com/Effect-TS/effect/pull/7143) [`089313e`](https://github.com/Effect-TS/effect/commit/089313ec2a4c307393c7c5e00c725ec23840c9c1) Thanks @fubhy! - Fix `NodeStream.toString` registering a duplicate `error` event listener. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7090](https://github.com/Effect-TS/effect/pull/7090) [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2) Thanks @tim-smart! - Expose `stdinIsTerminal` and `stdoutIsTerminal` effects through the `Stdio` service. +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Minor Changes + +- [#7076](https://github.com/Effect-TS/effect/pull/7076) [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d) Thanks @tim-smart! - Return the new file offset as a `Size` from `File.seek`. + +### Patch Changes + +- [#7073](https://github.com/Effect-TS/effect/pull/7073) [`e2ec131`](https://github.com/Effect-TS/effect/commit/e2ec1311bed9bb8709c26396e71b15b4241a9185) Thanks @fubhy! - Kill every process in a Node child process pipeline when killing its aggregate handle. + +- [#7067](https://github.com/Effect-TS/effect/pull/7067) [`e508589`](https://github.com/Effect-TS/effect/commit/e50858905fed68f29ec202ecdc9c902e44bfedd8) Thanks @fubhy! - Close pending TCP and WebSocket connections when a scoped socket server shuts down +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6715](https://github.com/Effect-TS/effect/pull/6715) [`75f340e`](https://github.com/Effect-TS/effect/commit/75f340ef3d6dc350337ff6ff930ae533046a969f) Thanks @tim-smart! - Add a Deno `Terminal` implementation and keep `NodeTerminal` input readers alive until stdin ends under Deno. + +- [#6914](https://github.com/Effect-TS/effect/pull/6914) [`0b4a3c4`](https://github.com/Effect-TS/effect/commit/0b4a3c4b1b1f1ad010fcc15cd9bb2fb39261c380) Thanks @spencerbeggs! - NodePath: `layerPosix` and `layerWin32` now convert between paths and `file:` URLs using their own platform flavor instead of the host's. + +- [#6679](https://github.com/Effect-TS/effect/pull/6679) [`9f02491`](https://github.com/Effect-TS/effect/commit/9f02491ae33eb930071dd7b81ee3f9683b2d6900) Thanks @tim-smart! - NodeTerminal: preserve buffered input across sequential `readLine` calls + +- [#6676](https://github.com/Effect-TS/effect/pull/6676) [`381c141`](https://github.com/Effect-TS/effect/commit/381c14141bc9bad437d77a98f6beb4d547cbaa75) Thanks @chenxin-yan! - NodeTerminal: end key input and fail `readLine` with `QuitError` at stdin EOF instead of hanging + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6696](https://github.com/Effect-TS/effect/pull/6696) [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9) Thanks @tim-smart! - remove file descriptor type + +- [#6705](https://github.com/Effect-TS/effect/pull/6705) [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146) Thanks @tylergibbs1! - Restore the `recursive` option for `FileSystem.watch`, with non-recursive watching as the default. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + +## 4.0.0-beta.101 + +### Patch Changes + +- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: + - effect@4.0.0-beta.101 + +## 4.0.0-beta.100 + +### Patch Changes + +- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: + - effect@4.0.0-beta.100 + +## 4.0.0-beta.99 + +### Patch Changes + +- [#6411](https://github.com/Effect-TS/effect/pull/6411) [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd) Thanks @sbking! - Fix child process termination to escalate to `SIGKILL` when the initial signal does not stop the process within `forceKillAfter`. + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- [#2523](https://github.com/Effect-TS/effect-smol/pull/2523) [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c) Thanks @rajzik! - Add glob to filesystem + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- [#2235](https://github.com/Effect-TS/effect-smol/pull/2235) [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3) Thanks @gcanti! - Add the package root barrel export. + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - generate binary arrays from streams with less copying + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- [`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505) Thanks @tim-smart! - don't remove SIGINT listener until fiber exit + +- Updated dependencies [[`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- [#1671](https://github.com/Effect-TS/effect-smol/pull/1671) [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0) Thanks @tim-smart! - default to endOnDone: false in NodeStdio + +- [#1671](https://github.com/Effect-TS/effect-smol/pull/1671) [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0) Thanks @tim-smart! - catch errors in pullIntoWritable + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- [#1658](https://github.com/Effect-TS/effect-smol/pull/1658) [`0fc977b`](https://github.com/Effect-TS/effect-smol/commit/0fc977b20d08caee7f64b2065e68f84afe124316) Thanks @nikelborm! - Add `endOnDone` option to Stdio stdout / stderr + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- [#1526](https://github.com/Effect-TS/effect-smol/pull/1526) [`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f) Thanks @tim-smart! - fix fs.stat when blksize is undefined + +- Updated dependencies []: + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- [#1349](https://github.com/Effect-TS/effect-smol/pull/1349) [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878) Thanks @tim-smart! - simplify NodeChildSpawner stdout streams + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-node-shared/LICENSE b/.context/effect/packages/platform/node-shared/LICENSE similarity index 100% rename from .context/effect/packages/platform-node-shared/LICENSE rename to .context/effect/packages/platform/node-shared/LICENSE diff --git a/.context/effect/packages/platform/node-shared/README.md b/.context/effect/packages/platform/node-shared/README.md new file mode 100644 index 000000000..5d89715a8 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/README.md @@ -0,0 +1,14 @@ +# @effect/platform-node-shared + +Effect platform services shared between Node.js-compatible runtimes. Used internally by `@effect/platform-node`, `@effect/platform-bun`, and `@effect/platform-deno`. + +## Installation + +```sh +npm install effect@beta @effect/platform-node-shared@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/platform-node-shared) diff --git a/.context/effect/packages/platform/node-shared/package.json b/.context/effect/packages/platform/node-shared/package.json new file mode 100644 index 000000000..75c1c4d67 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/package.json @@ -0,0 +1,78 @@ +{ + "name": "@effect/platform-node-shared", + "type": "module", + "version": "4.0.0-rc.108", + "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" + ], + "engines": { + "node": ">=18.0.0" + }, + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./index": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./index": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "peerDependencies": { + "effect": "workspace:^" + }, + "devDependencies": { + "@types/node": "^26.1.2", + "effect": "workspace:^", + "tar": "^7.5.19" + }, + "dependencies": { + "@types/ws": "^8.18.1", + "ws": "^8.21.0" + } +} diff --git a/.context/effect/packages/platform-node-shared/src/NodeChildProcessSpawner.ts b/.context/effect/packages/platform/node-shared/src/NodeChildProcessSpawner.ts similarity index 96% rename from .context/effect/packages/platform-node-shared/src/NodeChildProcessSpawner.ts rename to .context/effect/packages/platform/node-shared/src/NodeChildProcessSpawner.ts index bf6f007c4..38299e217 100644 --- a/.context/effect/packages/platform-node-shared/src/NodeChildProcessSpawner.ts +++ b/.context/effect/packages/platform/node-shared/src/NodeChildProcessSpawner.ts @@ -40,6 +40,7 @@ import { } from "effect/unstable/process/ChildProcessSpawner" import * as NodeChildProcess from "node:child_process" import { PassThrough } from "node:stream" +import { buildSpawnOptions } from "./internal/nodeChildProcessSpawner.ts" import { handleErrnoException } from "./internal/utils.ts" import * as NodeSink from "./NodeSink.ts" import * as NodeStream from "./NodeStream.ts" @@ -65,6 +66,17 @@ const toPlatformError = ( type ExitCodeWithSignal = readonly [code: number | null, signal: NodeJS.Signals | null] type ExitSignal = Deferred.Deferred +const taskkill = ( + childProcess: NodeChildProcess.ChildProcess, + onExit: (error: NodeChildProcess.ExecException | null) => void = () => {} +) => + NodeChildProcess.execFile( + "taskkill", + ["/pid", String(childProcess.pid!), "/T", "/F"], + { windowsHide: true }, + onExit + ) + const make = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const path = yield* Path.Path @@ -354,7 +366,7 @@ const make = Effect.gen(function*() { ) => { if (globalThis.process.platform === "win32") { return Effect.callback((resume) => { - NodeChildProcess.exec(`taskkill /pid ${childProcess.pid} /T /F`, (error) => { + taskkill(childProcess, (error) => { if (error) { resume(Effect.fail(toPlatformError("kill", toError(error), command))) } else { @@ -376,9 +388,8 @@ const make = Effect.gen(function*() { signal: NodeJS.Signals ): void => { if (globalThis.process.platform === "win32") { - NodeChildProcess.exec(`taskkill /pid ${childProcess.pid} /T /F`, () => { - // ignore errors during best-effort cleanup - }) + // ignore errors during best-effort cleanup + taskkill(childProcess) return } try { @@ -471,13 +482,7 @@ const make = Effect.gen(function*() { const stdio = buildStdioArray(stdinConfig, stdoutConfig, stderrConfig, resolvedAdditionalFds) const [childProcess, exitSignal] = yield* Effect.acquireRelease( - spawn(cmd, { - cwd, - env, - stdio, - detached: cmd.options.detached ?? process.platform !== "win32", - shell: cmd.options.shell - }), + spawn(cmd, buildSpawnOptions(cmd.options, { cwd, env, stdio }, process.platform)), Effect.fnUntraced(function*([childProcess, exitSignal]) { const exited = yield* Deferred.isDone(exitSignal) const killWithTimeout = withTimeout(childProcess, cmd, cmd.options) @@ -619,6 +624,8 @@ const make = Effect.gen(function*() { } const handle = handles[handles.length - 1] + const kill = (options?: ChildProcess.KillOptions | undefined) => + Effect.forEach([...handles].reverse(), (handle) => Effect.ignore(handle.kill(options)), { discard: true }) const unref = Effect.gen(function*() { const rerefs: Array> = [] for (const handle of handles) { @@ -631,7 +638,7 @@ const make = Effect.gen(function*() { pid: handle.pid, exitCode: handle.exitCode, isRunning: handle.isRunning, - kill: handle.kill, + kill, stdin: handle.stdin, stdout: handle.stdout, stderr: handle.stderr, diff --git a/.context/effect/packages/platform-node-shared/src/NodeClusterSocket.ts b/.context/effect/packages/platform/node-shared/src/NodeClusterSocket.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/src/NodeClusterSocket.ts rename to .context/effect/packages/platform/node-shared/src/NodeClusterSocket.ts diff --git a/.context/effect/packages/platform-node-shared/src/NodeCrypto.ts b/.context/effect/packages/platform/node-shared/src/NodeCrypto.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/src/NodeCrypto.ts rename to .context/effect/packages/platform/node-shared/src/NodeCrypto.ts diff --git a/.context/effect/packages/platform-node-shared/src/NodeFileSystem.ts b/.context/effect/packages/platform/node-shared/src/NodeFileSystem.ts similarity index 96% rename from .context/effect/packages/platform-node-shared/src/NodeFileSystem.ts rename to .context/effect/packages/platform/node-shared/src/NodeFileSystem.ts index fe7dd6314..8be146df3 100644 --- a/.context/effect/packages/platform-node-shared/src/NodeFileSystem.ts +++ b/.context/effect/packages/platform/node-shared/src/NodeFileSystem.ts @@ -210,7 +210,7 @@ const openFactory = (method: string): FileSystem.FileSystem["open"] => { nodeOpen(path, options?.flag ?? "r", options?.mode), (fd) => Effect.orDie(nodeClose(fd)) ), - Effect.map((fd) => makeFile(FileSystem.FileDescriptor(fd), options?.flag?.startsWith("a") ?? false)) + Effect.map((fd) => makeFile(fd, options?.flag?.startsWith("a") ?? false)) ) } const open = openFactory("open") @@ -252,13 +252,13 @@ const makeFile = (() => { class FileImpl implements FileSystem.File { readonly [FileSystem.FileTypeId]: typeof FileSystem.FileTypeId - readonly fd: FileSystem.File.Descriptor + readonly fd: number private readonly append: boolean private position: bigint = BigInt(0) constructor( - fd: FileSystem.File.Descriptor, + fd: number, append: boolean ) { this[FileSystem.FileTypeId] = FileSystem.FileTypeId @@ -283,7 +283,7 @@ const makeFile = (() => { this.position = this.position + offsetSize } - return this.position + return FileSystem.Size(this.position) }) } @@ -386,7 +386,7 @@ const makeFile = (() => { } } - return (fd: FileSystem.File.Descriptor, append: boolean): FileSystem.File => new FileImpl(fd, append) + return (fd: number, append: boolean): FileSystem.File => new FileImpl(fd, append) })() // == makeTempFile @@ -550,12 +550,12 @@ const utimes = (() => { // == watch -const watchNode = (path: string) => +const watchNode = (path: string, options?: FileSystem.WatchOptions) => Stream.callback((queue) => Effect.acquireRelease( Effect.sync(() => { const watcher = NFS.watch(path, { - recursive: true + recursive: options?.recursive ?? false }, (event, path) => { if (!path) return switch (event) { @@ -595,12 +595,16 @@ const watchNode = (path: string) => ) ) -const watch = (backend: Option.Option, path: string) => +const watch = ( + backend: Option.Option, + path: string, + options?: FileSystem.WatchOptions +) => stat(path).pipe( Effect.map((stat) => backend.pipe( - Option.flatMap((_) => _.register(path, stat)), - Option.getOrElse(() => watchNode(path)) + Option.flatMap((_) => _.register(path, stat, options)), + Option.getOrElse(() => watchNode(path, options)) ) ), Stream.unwrap @@ -652,8 +656,8 @@ const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend), symlink, truncate, utimes, - watch(path) { - return watch(backend, path) + watch(path, options) { + return watch(backend, path, options) }, writeFile })) diff --git a/.context/effect/packages/platform/node-shared/src/NodeHttpCompression.ts b/.context/effect/packages/platform/node-shared/src/NodeHttpCompression.ts new file mode 100644 index 000000000..009fa8f61 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/NodeHttpCompression.ts @@ -0,0 +1,153 @@ +/** + * HTTP response compression backed by `node:zlib`, shared by the Node.js, Bun, + * and Deno platforms. + * + * Byte-array bodies are compressed in one shot with the asynchronous + * `node:zlib` APIs, preserving an exact `Content-Length`. Streaming bodies go + * through `node:zlib` transform streams that flush each input chunk. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as HttpBody from "effect/unstable/http/HttpBody" +import type * as Platform from "effect/unstable/http/HttpPlatform" +import * as Response from "effect/unstable/http/HttpServerResponse" +import type { Duplex } from "node:stream" +import { Readable } from "node:stream" +import * as Zlib from "node:zlib" + +/** + * The compression algorithms supported by the runtime's `node:zlib`. `zstd` + * requires Node.js 22.15 or newer. + * + * @category constants + * @since 4.0.0 + */ +export const algorithms: ReadonlySet = new Set( + typeof Zlib.zstdCompress === "function" + ? ["gzip", "deflate", "br", "zstd"] + : ["gzip", "deflate", "br"] +) + +const brotliParams = (level: number | undefined, sizeHint?: number): Zlib.BrotliOptions => { + const params: Record = {} + if (level !== undefined) { + params[Zlib.constants.BROTLI_PARAM_QUALITY] = level + } + if (sizeHint !== undefined) { + params[Zlib.constants.BROTLI_PARAM_SIZE_HINT] = sizeHint + } + return { params } +} + +const zstdParams = (level: number | undefined): Zlib.ZstdOptions | undefined => + level === undefined || level === 3 ? undefined : { params: { [Zlib.constants.ZSTD_c_compressionLevel]: level } } + +const compress = ( + data: Uint8Array, + algorithm: Platform.CompressionAlgorithm, + options?: Platform.CompressionOptions | undefined +): Effect.Effect => + Effect.callback((resume) => { + const complete = (error: Error | null, result: Uint8Array) => + resume(error === null ? Effect.succeed(result) : Effect.die(error)) + switch (algorithm) { + case "gzip": { + Zlib.gzip(data, { level: options?.level }, complete) + break + } + case "deflate": { + Zlib.deflate(data, { level: options?.level }, complete) + break + } + case "br": { + Zlib.brotliCompress(data, brotliParams(options?.level, data.byteLength), complete) + break + } + case "zstd": { + const params = zstdParams(options?.level) + if (params === undefined) { + Zlib.zstdCompress(data, complete) + } else { + Zlib.zstdCompress(data, params, complete) + } + break + } + } + }) + +/** + * Creates a `Compression` that compresses byte-array bodies in one shot with + * the asynchronous `node:zlib` APIs, setting the exact `Content-Length` of the + * compressed body. All other bodies are delegated to `fallback`. + * + * @category constructors + * @since 4.0.0 + */ +export const make = (fallback: Platform.Compression): Platform.Compression => ({ + algorithms: fallback.algorithms, + compressResponse(response, algorithm, options) { + const body = response.body + if (body._tag !== "Uint8Array") { + return fallback.compressResponse(response, algorithm, options) + } + return Effect.map(compress(body.body, algorithm, options), (result) => + Response.setHeader( + Response.setBody(response, HttpBody.uint8Array(result, body.contentType)), + "content-length", + result.byteLength.toString() + )) + } +}) + +/** + * Creates a `node:zlib` compression transform stream that flushes each input + * chunk, for streaming response bodies. + * + * @category constructors + * @since 4.0.0 + */ +export const compressTransform = ( + algorithm: Platform.CompressionAlgorithm, + options?: Platform.CompressionOptions | undefined +): Duplex => { + switch (algorithm) { + case "gzip": { + return Zlib.createGzip({ level: options?.level, flush: Zlib.constants.Z_SYNC_FLUSH }) + } + case "deflate": { + return Zlib.createDeflate({ level: options?.level, flush: Zlib.constants.Z_SYNC_FLUSH }) + } + case "br": { + return Zlib.createBrotliCompress({ + ...brotliParams(options?.level), + flush: Zlib.constants.BROTLI_OPERATION_FLUSH + }) + } + case "zstd": { + return Zlib.createZstdCompress({ + ...zstdParams(options?.level), + flush: Zlib.constants.ZSTD_e_flush + }) + } + } +} + +/** + * A Web `ReadableStream` version of `compressTransform`, for platforms that + * stream response bodies as Web streams. + * + * @category constructors + * @since 4.0.0 + */ +export const compressTransformWeb = ( + algorithm: Platform.CompressionAlgorithm, + options?: Platform.CompressionOptions | undefined +) => +(stream: ReadableStream): ReadableStream => { + const transform = compressTransform(algorithm, options) + const source = Readable.fromWeb(stream as any) + source.on("error", (cause) => transform.destroy(cause)) + transform.on("close", () => source.destroy()) + return Readable.toWeb(source.pipe(transform)) as unknown as ReadableStream +} diff --git a/.context/effect/packages/platform/node-shared/src/NodePath.ts b/.context/effect/packages/platform/node-shared/src/NodePath.ts new file mode 100644 index 000000000..2a29c60f4 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/NodePath.ts @@ -0,0 +1,78 @@ +/** + * Node-backed provider for Effect's `Path` service. + * + * This module turns Node's `node:path` and `node:url` APIs into `Path` layers. + * `layer` uses the host platform path implementation, while `layerPosix` and + * `layerWin32` provide fixed POSIX and Windows variants. All three layers also + * include helpers for converting between file paths and file URLs. + * + * @since 4.0.0 + */ +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { Path, TypeId } from "effect/Path" +import { BadArgument } from "effect/PlatformError" +import * as NodePath from "node:path" +import * as NodeUrl from "node:url" + +const fileUrlOps = (windows: boolean | undefined) => ({ + fromFileUrl: (url: URL): Effect.Effect => + Effect.try({ + try: () => NodeUrl.fileURLToPath(url, { windows }), + catch: (cause) => + new BadArgument({ + module: "Path", + method: "fromFileUrl", + cause + }) + }), + toFileUrl: (path: string): Effect.Effect => + Effect.try({ + try: () => NodeUrl.pathToFileURL(path, { windows }), + catch: (cause) => + new BadArgument({ + module: "Path", + method: "toFileUrl", + cause + }) + }) +}) + +/** + * Provides the `Path` service using Node's POSIX path implementation plus + * file URL conversion helpers. + * + * @category layers + * @since 4.0.0 + */ +export const layerPosix: Layer.Layer = Layer.succeed(Path)({ + [TypeId]: TypeId, + ...NodePath.posix, + ...fileUrlOps(false) +}) + +/** + * Provides the `Path` service using Node's Windows path implementation plus + * file URL conversion helpers. + * + * @category layers + * @since 4.0.0 + */ +export const layerWin32: Layer.Layer = Layer.succeed(Path)({ + [TypeId]: TypeId, + ...NodePath.win32, + ...fileUrlOps(true) +}) + +/** + * Provides the default `Path` service using the host platform's Node path + * implementation plus file URL conversion helpers. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.succeed(Path)({ + [TypeId]: TypeId, + ...NodePath, + ...fileUrlOps(undefined) +}) diff --git a/.context/effect/packages/platform-node-shared/src/NodeRuntime.ts b/.context/effect/packages/platform/node-shared/src/NodeRuntime.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/src/NodeRuntime.ts rename to .context/effect/packages/platform/node-shared/src/NodeRuntime.ts diff --git a/.context/effect/packages/platform-node-shared/src/NodeSink.ts b/.context/effect/packages/platform/node-shared/src/NodeSink.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/src/NodeSink.ts rename to .context/effect/packages/platform/node-shared/src/NodeSink.ts diff --git a/.context/effect/packages/platform-node-shared/src/NodeSocket.ts b/.context/effect/packages/platform/node-shared/src/NodeSocket.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/src/NodeSocket.ts rename to .context/effect/packages/platform/node-shared/src/NodeSocket.ts diff --git a/.context/effect/packages/platform-node-shared/src/NodeSocketServer.ts b/.context/effect/packages/platform/node-shared/src/NodeSocketServer.ts similarity index 91% rename from .context/effect/packages/platform-node-shared/src/NodeSocketServer.ts rename to .context/effect/packages/platform/node-shared/src/NodeSocketServer.ts index 2d908e316..854455d61 100644 --- a/.context/effect/packages/platform-node-shared/src/NodeSocketServer.ts +++ b/.context/effect/packages/platform/node-shared/src/NodeSocketServer.ts @@ -52,12 +52,14 @@ export const make = Effect.fnUntraced(function*( options: Net.ServerOpts & Net.ListenOptions ) { const errorDeferred = Deferred.makeUnsafe() - const pending = new Set() + const pending = new Map void>() function defaultOnConnection(conn: Net.Socket) { - pending.add(conn) const remove = () => { pending.delete(conn) + conn.off("close", remove) + conn.off("error", remove) } + pending.set(conn, remove) conn.on("close", remove) conn.on("error", remove) } @@ -66,6 +68,10 @@ export const make = Effect.fnUntraced(function*( let server: Net.Server | undefined yield* Effect.addFinalizer(() => Effect.callback((resume) => { + pending.forEach((remove, conn) => { + remove() + conn.destroy() + }) server?.close(() => resume(Effect.void)) }) ) @@ -129,12 +135,10 @@ export const make = Effect.fnUntraced(function*( trackFiber ) } - pending.forEach((conn) => { - conn.removeAllListeners("error") - conn.removeAllListeners("close") + pending.forEach((remove, conn) => { + remove() onConnection(conn) }) - pending.clear() return yield* Effect.callback((_resume) => { return Effect.suspend(() => { onConnection = prevOnConnection @@ -190,20 +194,29 @@ export const makeWebSocket: ( > = Effect.fnUntraced(function*( options: NodeWS.ServerOptions ) { + const pendingConnections = new Map< + globalThis.WebSocket, + readonly [request: Http.IncomingMessage, remove: () => void] + >() const server = yield* Effect.acquireRelease( Effect.sync(() => new NodeWS.WebSocketServer(options)), (server) => Effect.callback((resume) => { + pendingConnections.forEach(([, remove], conn) => { + remove() + const socket = conn as unknown as NodeWS.WebSocket + socket.terminate() + }) server.close(() => resume(Effect.void)) }) ) - const pendingConnections = new Set() function defaultHandler(conn: globalThis.WebSocket, req: Http.IncomingMessage) { - const entry = [conn, req] as const - pendingConnections.add(entry) - conn.addEventListener("close", () => { - pendingConnections.delete(entry) - }) + const remove = () => { + pendingConnections.delete(conn) + conn.removeEventListener("close", remove) + } + pendingConnections.set(conn, [req, remove]) + conn.addEventListener("close", remove) } let onConnection = defaultHandler server.on("connection", (conn, req) => onConnection(conn as any, req)) @@ -248,10 +261,10 @@ export const makeWebSocket: ( trackFiber ) } - for (const [conn, req] of pendingConnections) { + pendingConnections.forEach(([req, remove], conn) => { + remove() onConnection(conn, req) - } - pendingConnections.clear() + }) return yield* Effect.callback((_resume) => { return Effect.sync(() => { onConnection = prevOnConnection diff --git a/.context/effect/packages/platform-node-shared/src/NodeStdio.ts b/.context/effect/packages/platform/node-shared/src/NodeStdio.ts similarity index 93% rename from .context/effect/packages/platform-node-shared/src/NodeStdio.ts rename to .context/effect/packages/platform/node-shared/src/NodeStdio.ts index fcd083a43..cd05c6e3b 100644 --- a/.context/effect/packages/platform-node-shared/src/NodeStdio.ts +++ b/.context/effect/packages/platform/node-shared/src/NodeStdio.ts @@ -28,6 +28,8 @@ export const layer: Layer.Layer = Layer.succeed( Stdio.Stdio, Stdio.make({ args: Effect.sync(() => process.argv.slice(2)), + stdinIsTerminal: Effect.sync(() => process.stdin.isTTY === true), + stdoutIsTerminal: Effect.sync(() => process.stdout.isTTY === true), stdout: (options) => fromWritable({ evaluate: () => process.stdout, diff --git a/.context/effect/packages/platform-node-shared/src/NodeStream.ts b/.context/effect/packages/platform/node-shared/src/NodeStream.ts similarity index 99% rename from .context/effect/packages/platform-node-shared/src/NodeStream.ts rename to .context/effect/packages/platform/node-shared/src/NodeStream.ts index 7ea0a40d0..8aa14ba1e 100644 --- a/.context/effect/packages/platform-node-shared/src/NodeStream.ts +++ b/.context/effect/packages/platform/node-shared/src/NodeStream.ts @@ -235,9 +235,6 @@ export const toString = ( } resume(Effect.fail(onError(err) as E)) }) - stream.once("error", (err) => { - resume(Effect.fail(onError(err) as E)) - }) let string = "" let bytes = 0 diff --git a/.context/effect/packages/platform/node-shared/src/NodeTerminal.ts b/.context/effect/packages/platform/node-shared/src/NodeTerminal.ts new file mode 100644 index 000000000..3cd23e66e --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/NodeTerminal.ts @@ -0,0 +1,178 @@ +/** + * Shared Node.js implementation of Effect's `Terminal` service. + * + * `NodeTerminal` adapts Node's `readline` APIs plus the current process + * `stdin` and `stdout` streams into {@link Terminal.Terminal}. The service can + * display output, read a line, stream key input, and read terminal dimensions. + * `make` manages readline and TTY raw mode in a scope, while `layer` provides + * the default service that ends key input on Ctrl+C or Ctrl+D. + * + * @since 4.0.0 + */ +import type * as Cause from "effect/Cause" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import { badArgument, type PlatformError } from "effect/PlatformError" +import * as Predicate from "effect/Predicate" +import * as Queue from "effect/Queue" +import * as RcRef from "effect/RcRef" +import type * as Scope from "effect/Scope" +import * as Terminal from "effect/Terminal" +import * as readline from "node:readline" + +/** + * Creates a scoped process-backed `Terminal` using Node `readline`, enabling + * TTY raw mode while in scope and using the supplied predicate to decide when + * key input should end. + * + * @category constructors + * @since 4.0.0 + */ +export const make: ( + shouldQuit?: (input: Terminal.UserInput) => boolean +) => Effect.Effect = Effect.fnUntraced( + function*(shouldQuit: (input: Terminal.UserInput) => boolean = defaultShouldQuit) { + const stdin = process.stdin + const stdout = process.stdout + const lines = yield* Queue.make() + + // stdin "end" fires once per process, so remember end-of-input for readers + // created after the event (Bun never sets `readableEnded`). + let inputEnded = stdin.readableEnded + let readlineActive = false + const onStdinEnd = () => { + inputEnded = true + if (!readlineActive) { + Queue.endUnsafe(lines) + } + } + stdin.once("end", onStdinEnd) + yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("end", onStdinEnd))) + + const rlRef = yield* RcRef.make({ + acquire: Effect.acquireRelease( + Effect.sync(() => { + const rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 }) + const onLine = (line: string) => Queue.offerUnsafe(lines, line) + const onClose = () => { + readlineActive = false + Queue.endUnsafe(lines) + } + readlineActive = true + readline.emitKeypressEvents(stdin, rl) + rl.on("line", onLine) + rl.once("close", onClose) + + if (stdin.isTTY) { + stdin.setRawMode(true) + } + return { rl, onClose, onLine } + }), + ({ rl, onClose, onLine }) => + Effect.sync(() => { + readlineActive = false + rl.off("line", onLine) + rl.off("close", onClose) + if (stdin.isTTY) { + stdin.setRawMode(false) + } + rl.close() + if (inputEnded) { + Queue.endUnsafe(lines) + } + }) + ), + idleTimeToLive: "10 millis" + }) + + const columns = Effect.sync(() => stdout.columns ?? 0) + const rows = Effect.sync(() => stdout.rows ?? 0) + + const readInput = Effect.gen(function*() { + const queue = yield* Queue.make() + const handleKeypress = (s: string | undefined, k: readline.Key) => { + const userInput = { + input: Option.fromUndefinedOr(s), + key: { name: k.name ?? "", ctrl: !!k.ctrl, meta: !!k.meta, shift: !!k.shift } + } + Queue.offerUnsafe(queue, userInput) + if (shouldQuit(userInput)) { + Queue.endUnsafe(queue) + } + } + // Deno's `process.stdin` shim does not keep the event loop alive, so a + // program blocked on input can exit before `end` is ever delivered. A + // timer holds the loop open for as long as this reader is active. + const keepAlive = setInterval(() => {}, 2147483647) + // Without this, consumers (e.g. `Prompt.run`) hang forever on closed stdin. + const handleEnd = () => { + clearInterval(keepAlive) + Queue.endUnsafe(queue) + } + yield* Effect.addFinalizer(() => + Effect.sync(() => { + clearInterval(keepAlive) + stdin.off("keypress", handleKeypress) + stdin.off("end", handleEnd) + }) + ) + stdin.on("keypress", handleKeypress) + if (inputEnded) { + handleEnd() + } else { + yield* RcRef.get(rlRef) + stdin.once("end", handleEnd) + } + return queue as Queue.Dequeue + }) + + const readLine = Effect.suspend(() => + Queue.poll(lines).pipe( + Effect.flatMap(Option.match({ + onNone: () => Effect.scoped(Effect.andThen(RcRef.get(rlRef), Queue.take(lines))), + onSome: Effect.succeed + })), + Effect.mapError(() => new Terminal.QuitError({})) + ) + ) + + const display = (prompt: string) => + Effect.uninterruptible( + Effect.callback((resume) => { + stdout.write(prompt, (err) => + Predicate.isNullish(err) + ? resume(Effect.void) + : resume(Effect.fail( + badArgument({ + module: "Terminal", + method: "display", + description: "Failed to write prompt to stdout", + cause: err + }) + ))) + }) + ) + + return Terminal.make({ + columns, + rows, + readInput, + readLine, + display + }) + } +) + +/** + * Provides the default process-backed `Terminal` service, ending key input on + * Ctrl+C or Ctrl+D. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.effect(Terminal.Terminal, make(defaultShouldQuit)) + +function defaultShouldQuit(input: Terminal.UserInput) { + return input.key.ctrl && (input.key.name === "c" || input.key.name === "d") +} diff --git a/.context/effect/packages/platform/node-shared/src/index.ts b/.context/effect/packages/platform/node-shared/src/index.ts new file mode 100644 index 000000000..759e93b65 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/index.ts @@ -0,0 +1,70 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts" + +/** + * @since 4.0.0 + */ +export * as NodeClusterSocket from "./NodeClusterSocket.ts" + +/** + * @since 1.0.0 + */ +export * as NodeCrypto from "./NodeCrypto.ts" + +/** + * @since 4.0.0 + */ +export * as NodeFileSystem from "./NodeFileSystem.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpCompression from "./NodeHttpCompression.ts" + +/** + * @since 4.0.0 + */ +export * as NodePath from "./NodePath.ts" + +/** + * @since 4.0.0 + */ +export * as NodeRuntime from "./NodeRuntime.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSink from "./NodeSink.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSocket from "./NodeSocket.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSocketServer from "./NodeSocketServer.ts" + +/** + * @since 4.0.0 + */ +export * as NodeStdio from "./NodeStdio.ts" + +/** + * @since 4.0.0 + */ +export * as NodeStream from "./NodeStream.ts" + +/** + * @since 4.0.0 + */ +export * as NodeTerminal from "./NodeTerminal.ts" diff --git a/.context/effect/packages/platform/node-shared/src/internal/nodeChildProcessSpawner.ts b/.context/effect/packages/platform/node-shared/src/internal/nodeChildProcessSpawner.ts new file mode 100644 index 000000000..7cd1e9e95 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/internal/nodeChildProcessSpawner.ts @@ -0,0 +1,16 @@ +import type * as ChildProcess from "effect/unstable/process/ChildProcess" +import type * as NodeChildProcess from "node:child_process" + +export const buildSpawnOptions = ( + options: ChildProcess.CommandOptions, + base: Pick, + platform: NodeJS.Platform +): NodeChildProcess.SpawnOptions => { + const detached = options.detached ?? platform !== "win32" + return { + ...base, + detached, + shell: options.shell, + windowsHide: options.windowsHide ?? !detached + } +} diff --git a/.context/effect/packages/platform/node-shared/src/internal/utils.ts b/.context/effect/packages/platform/node-shared/src/internal/utils.ts new file mode 100644 index 000000000..f62bb8bfe --- /dev/null +++ b/.context/effect/packages/platform/node-shared/src/internal/utils.ts @@ -0,0 +1,51 @@ +import type { SystemError, SystemErrorTag } from "effect/PlatformError" +import * as PlatformError from "effect/PlatformError" +import type { PathLike } from "node:fs" + +/** @internal */ +export const handleErrnoException = (module: SystemError["module"], method: string) => +( + err: NodeJS.ErrnoException, + [path]: [path: PathLike | number | string | ReadonlyArray, ...args: Array] +): PlatformError.PlatformError => { + let reason: SystemErrorTag = "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 PlatformError.systemError({ + _tag: reason, + module, + method, + pathOrDescriptor: path as string | number, + syscall: err.syscall, + cause: err + }) +} diff --git a/.context/effect/packages/platform/node-shared/test/NodeChildProcessSpawner.test.ts b/.context/effect/packages/platform/node-shared/test/NodeChildProcessSpawner.test.ts new file mode 100644 index 000000000..7c646284d --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/NodeChildProcessSpawner.test.ts @@ -0,0 +1,92 @@ +import { buildSpawnOptions } from "@effect/platform-node-shared/internal/nodeChildProcessSpawner" +import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner" +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import * as NodePath from "@effect/platform-node-shared/NodePath" +import { assert, describe, it } from "@effect/vitest" +import * as ChildProcessSpawnerTest from "effect-test/unstable/process/ChildProcessSpawnerTest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as ChildProcess from "effect/unstable/process/ChildProcess" + +const NodeServices = NodeChildProcessSpawner.layer.pipe( + Layer.provideMerge(Layer.mergeAll( + NodeFileSystem.layer, + NodePath.layer + )) +) + +ChildProcessSpawnerTest.suite("NodeChildProcessSpawner", NodeServices, { + processGroups: true +}) + +describe("buildSpawnOptions", () => { + const base = { stdio: "pipe" } as const + + it("defaults to hiding non-detached Windows children", () => { + assert.deepStrictEqual(buildSpawnOptions({}, base, "win32"), { + stdio: "pipe", + detached: false, + shell: undefined, + windowsHide: true + }) + assert.deepStrictEqual(buildSpawnOptions({ detached: true }, base, "win32"), { + stdio: "pipe", + detached: true, + shell: undefined, + windowsHide: false + }) + assert.deepStrictEqual(buildSpawnOptions({ detached: false }, base, "win32"), { + stdio: "pipe", + detached: false, + shell: undefined, + windowsHide: true + }) + }) + + it("allows windowsHide to be configured independently of detached", () => { + assert.deepStrictEqual( + buildSpawnOptions({ detached: false, windowsHide: false }, base, "win32"), + { + stdio: "pipe", + detached: false, + shell: undefined, + windowsHide: false + } + ) + assert.deepStrictEqual( + buildSpawnOptions({ detached: true, windowsHide: true }, base, "win32"), + { + stdio: "pipe", + detached: true, + shell: undefined, + windowsHide: true + } + ) + }) +}) + +it.live("kills every process in a pipeline", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const directory = yield* fs.makeTempDirectoryScoped() + const rootHeartbeat = `${directory}/root-heartbeat` + const childHeartbeat = `${directory}/child-heartbeat` + const handle = yield* ChildProcess.make( + "sh", + ["-c", "while :; do printf x >> \"$1\"; sleep 0.01; done", "pipeline-root", rootHeartbeat] + ).pipe(ChildProcess.pipeTo(ChildProcess.make( + "sh", + ["-c", "while :; do printf x >> \"$1\"; sleep 0.01; done", "pipeline-child", childHeartbeat] + ))) + yield* Effect.sleep("100 millis") + yield* handle.kill({ killSignal: "SIGKILL" }) + const rootSizeAfterKill = (yield* fs.stat(rootHeartbeat)).size + const childSizeAfterKill = (yield* fs.stat(childHeartbeat)).size + yield* Effect.sleep("100 millis") + const rootFinalSize = (yield* fs.stat(rootHeartbeat)).size + const childFinalSize = (yield* fs.stat(childHeartbeat)).size + + assert.strictEqual(rootFinalSize, rootSizeAfterKill) + assert.strictEqual(childFinalSize, childSizeAfterKill) + }).pipe(Effect.scoped, Effect.provide(NodeServices))) diff --git a/.context/effect/packages/platform/node-shared/test/NodeFileSystem.test.ts b/.context/effect/packages/platform/node-shared/test/NodeFileSystem.test.ts new file mode 100644 index 000000000..c618ead16 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/NodeFileSystem.test.ts @@ -0,0 +1,99 @@ +import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem" +import { assert, describe, it } from "@effect/vitest" +import * as Deferred from "effect/Deferred" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as FileSystem from "effect/FileSystem" +import * as Stream from "effect/Stream" +import * as TestClock from "effect/testing/TestClock" +import { testLayer } from "../../../effect/test/FileSystem.test-utils.ts" + +const startWatch = ( + fs: FileSystem.FileSystem, + root: string, + watch: () => Stream.Stream +) => + Effect.gen(function*() { + const ready = yield* Deferred.make() + const readyName = ".watch-ready" + const fiber = yield* watch().pipe( + Stream.tap((event) => + event.path === readyName + ? Deferred.succeed(ready, undefined) + : Effect.void + ), + Stream.dropUntil((event) => event.path === readyName), + Stream.filter((event) => event.path !== readyName), + Stream.runHead, + Effect.flatMap(Effect.fromOption), + Effect.forkChild + ) + const signalFiber = yield* Effect.sleep("10 millis").pipe( + TestClock.withLive, + Effect.andThen(fs.writeFileString(`${root}/${readyName}`, "")), + Effect.forever, + Effect.forkChild + ) + yield* Deferred.await(ready).pipe( + Effect.raceFirst(Fiber.join(fiber).pipe(Effect.asVoid)), + Effect.ensuring(Fiber.interrupt(signalFiber)) + ) + return fiber + }) + +describe("FileSystem", () => { + testLayer(NodeFileSystem.layer) + + it.effect("watch does not report nested changes when recursive is false", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const root = yield* fs.makeTempDirectoryScoped() + const nested = `${root}/nested` + yield* fs.makeDirectory(nested) + + const fiber = yield* startWatch(fs, root, () => fs.watch(root, { recursive: false })) + + yield* fs.writeFileString(`${nested}/nested.txt`, "") + yield* fs.writeFileString(`${root}/direct.txt`, "") + + const event = yield* Fiber.join(fiber) + assert.strictEqual(event.path, "direct.txt") + }).pipe( + Effect.provide(NodeFileSystem.layer) + )) + + it.effect("watch is non-recursive when options are omitted", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const root = yield* fs.makeTempDirectoryScoped() + const nested = `${root}/nested` + yield* fs.makeDirectory(nested) + + const fiber = yield* startWatch(fs, root, () => fs.watch(root)) + + yield* fs.writeFileString(`${nested}/nested.txt`, "") + yield* fs.writeFileString(`${root}/direct.txt`, "") + + const event = yield* Fiber.join(fiber) + assert.strictEqual(event.path, "direct.txt") + }).pipe( + Effect.provide(NodeFileSystem.layer) + )) + + it.effect("watch reports nested changes when recursive is true", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const root = yield* fs.makeTempDirectoryScoped() + const nested = `${root}/nested` + yield* fs.makeDirectory(nested) + + const fiber = yield* startWatch(fs, root, () => fs.watch(root, { recursive: true })) + + yield* fs.writeFileString(`${nested}/nested.txt`, "") + + const event = yield* Fiber.join(fiber) + assert(event.path.endsWith("nested.txt")) + }).pipe( + Effect.provide(NodeFileSystem.layer) + )) +}) diff --git a/.context/effect/packages/platform/node-shared/test/NodePath.test.ts b/.context/effect/packages/platform/node-shared/test/NodePath.test.ts new file mode 100644 index 000000000..d8b9cadf6 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/NodePath.test.ts @@ -0,0 +1,27 @@ +import * as NodePath from "@effect/platform-node-shared/NodePath" +import { assert, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Path from "effect/Path" + +it.layer(NodePath.layerPosix)("POSIX file URLs", (it) => { + it.effect("uses POSIX conversions", () => + Effect.gen(function*() { + const path = yield* Path.Path + + assert.strictEqual(yield* path.fromFileUrl(new URL("file:///tmp/file.txt")), "/tmp/file.txt") + assert.strictEqual((yield* path.toFileUrl("/tmp/file.txt")).href, "file:///tmp/file.txt") + })) +}) + +it.layer(NodePath.layerWin32)("Windows file URLs", (it) => { + it.effect("uses Windows conversions", () => + Effect.gen(function*() { + const path = yield* Path.Path + + assert.strictEqual(yield* path.fromFileUrl(new URL("file:///C:/Users/me/file.txt")), "C:\\Users\\me\\file.txt") + assert.strictEqual( + (yield* path.toFileUrl("C:\\Users\\me\\file.txt")).href, + "file:///C:/Users/me/file.txt" + ) + })) +}) diff --git a/.context/effect/packages/platform-node-shared/test/NodeSink.test.ts b/.context/effect/packages/platform/node-shared/test/NodeSink.test.ts similarity index 100% rename from .context/effect/packages/platform-node-shared/test/NodeSink.test.ts rename to .context/effect/packages/platform/node-shared/test/NodeSink.test.ts diff --git a/.context/effect/packages/platform/node-shared/test/NodeStdio.test.ts b/.context/effect/packages/platform/node-shared/test/NodeStdio.test.ts new file mode 100644 index 000000000..5d822b8ac --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/NodeStdio.test.ts @@ -0,0 +1,50 @@ +import * as NodeStdio from "@effect/platform-node-shared/NodeStdio" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Stdio from "effect/Stdio" + +const streams = [process.stdin, process.stdout] as const + +const setIsTTY = (stdin: boolean, stdout: boolean) => + Effect.sync(() => { + Object.defineProperty(streams[0], "isTTY", { configurable: true, value: stdin }) + Object.defineProperty(streams[1], "isTTY", { configurable: true, value: stdout }) + }) + +const withRestoredIsTTY = (effect: Effect.Effect) => + Effect.acquireUseRelease( + Effect.sync(() => streams.map((stream) => Object.getOwnPropertyDescriptor(stream, "isTTY"))), + () => effect, + (descriptors) => + Effect.sync(() => { + streams.forEach((stream, index) => { + const descriptor = descriptors[index] + if (descriptor === undefined) { + Reflect.deleteProperty(stream, "isTTY") + } else { + Object.defineProperty(stream, "isTTY", descriptor) + } + }) + }) + ) + +describe("NodeStdio", () => { + it.effect("reads terminal state when the effects run", () => + withRestoredIsTTY( + Effect.gen(function*() { + const stdio = yield* Stdio.Stdio + + yield* setIsTTY(true, false) + assert.deepStrictEqual( + yield* Effect.all([stdio.stdinIsTerminal, stdio.stdoutIsTerminal]), + [true, false] + ) + + yield* setIsTTY(false, true) + assert.deepStrictEqual( + yield* Effect.all([stdio.stdinIsTerminal, stdio.stdoutIsTerminal]), + [false, true] + ) + }).pipe(Effect.provide(NodeStdio.layer)) + )) +}) diff --git a/.context/effect/packages/platform-node-shared/test/NodeStream.test.ts b/.context/effect/packages/platform/node-shared/test/NodeStream.test.ts similarity index 94% rename from .context/effect/packages/platform-node-shared/test/NodeStream.test.ts rename to .context/effect/packages/platform/node-shared/test/NodeStream.test.ts index edd098bcb..84fefb0f0 100644 --- a/.context/effect/packages/platform-node-shared/test/NodeStream.test.ts +++ b/.context/effect/packages/platform/node-shared/test/NodeStream.test.ts @@ -170,4 +170,14 @@ describe("Stream", () => { ) assert.deepEqual(error.cause, "error") })) + + it.effect("toString registers one error listener", () => + Effect.gen(function*() { + const stream = new Readable({ + read() {} + }) + yield* NodeStream.toString(() => stream).pipe(Effect.forkChild) + yield* Effect.yieldNow + assert.strictEqual(stream.listenerCount("error"), 1) + })) }) diff --git a/.context/effect/packages/platform/node-shared/test/NodeTerminal.test.ts b/.context/effect/packages/platform/node-shared/test/NodeTerminal.test.ts new file mode 100644 index 000000000..4e82a977f --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/NodeTerminal.test.ts @@ -0,0 +1,75 @@ +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import { spawn, spawnSync } from "node:child_process" +import { join } from "node:path" + +const fixture = join(__dirname, "fixtures", "node-terminal.ts") + +const runFixture = (mode: string, input: string) => + spawnSync(process.execPath, [fixture, mode], { + encoding: "utf8", + input, + timeout: 2_000 + }) + +const assertResult = (mode: string, input: string, expected: string) => { + const result = runFixture(mode, input) + assert.isUndefined(result.error) + assert.strictEqual(result.status, 0, result.stderr) + assert.isTrue(result.stderr.includes(`RESULT ${expected}`), result.stderr) +} + +const assertOpenResult = (mode: string, input: string, expected: string) => + Effect.callback((resume) => { + const child = spawn(process.execPath, [fixture, mode]) + let stderr = "" + child.stderr.setEncoding("utf8") + child.stderr.on("data", (data) => { + stderr += data + if (stderr.includes("RESULT ")) { + child.stdin.end() + } + }) + child.on("exit", (code) => { + resume( + code === 0 && stderr.includes(`RESULT ${expected}`) + ? Effect.void + : Effect.die(new Error(stderr)) + ) + }) + child.stdin.write(input) + return Effect.sync(() => child.kill()) + }) + +describe("NodeTerminal", () => { + it("does not install a readline interface until the terminal is used", () => { + assertResult("unused", "", "{\"dataListeners\":0}") + }) + + it("fails a prompt with QuitError after piped input is exhausted", () => { + assertResult("prompts", "y\n", "{\"first\":true,\"second\":\"QuitError\"}") + }) + + it("delivers buffered keypresses before ending the input queue", () => { + assertResult("read-input", "yn", "{\"first\":\"y\",\"second\":\"n\",\"ended\":true}") + }) + + it("flushes an unterminated line before failing readLine with QuitError at EOF", () => { + assertResult("read-line", "last line", "{\"first\":\"last line\",\"second\":\"QuitError\"}") + }) + + it("preserves lines buffered between sequential readLine calls", () => { + assertResult("read-lines", "first\nsecond\n", "{\"first\":\"first\",\"second\":\"second\"}") + }) + + it("fails readLine with QuitError when stdin ended before initialization", () => { + assertResult("read-line-after-end", "", "\"QuitError\"") + }) + + it.effect("disposes readline after its idle TTL", () => + assertOpenResult( + "read-line-disposed", + "line\n", + "{\"line\":\"line\",\"duringTtl\":1,\"dataListeners\":0}" + )) +}) diff --git a/.context/effect/packages/platform-node-shared/test/fixtures/helloworld.tar.gz b/.context/effect/packages/platform/node-shared/test/fixtures/helloworld.tar.gz similarity index 100% rename from .context/effect/packages/platform-node-shared/test/fixtures/helloworld.tar.gz rename to .context/effect/packages/platform/node-shared/test/fixtures/helloworld.tar.gz diff --git a/.context/effect/packages/platform/node-shared/test/fixtures/node-terminal.ts b/.context/effect/packages/platform/node-shared/test/fixtures/node-terminal.ts new file mode 100644 index 000000000..63f6ea590 --- /dev/null +++ b/.context/effect/packages/platform/node-shared/test/fixtures/node-terminal.ts @@ -0,0 +1,108 @@ +import * as NodeTerminal from "@effect/platform-node-shared/NodeTerminal" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Path from "effect/Path" +import * as Queue from "effect/Queue" +import * as Terminal from "effect/Terminal" +import { Prompt } from "effect/unstable/cli" + +const TerminalLayer = Layer.mergeAll( + NodeTerminal.layer, + FileSystem.layerNoop({}), + Path.layer +) + +const prompts = Effect.gen(function*() { + const first = yield* Prompt.run(Prompt.confirm({ message: "First" })) + const second = yield* Prompt.run(Prompt.confirm({ message: "Second" })).pipe(Effect.flip) + return { first, second: second._tag } +}) + +const readInput = Effect.scoped( + Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const input = yield* terminal.readInput + const first = yield* Queue.take(input) + const second = yield* Queue.take(input) + const end = yield* Effect.exit(Queue.take(input)) + return { + first: Option.getOrNull(first.input), + second: Option.getOrNull(second.input), + ended: Exit.isFailure(end) + } + }) +) + +const readLine = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const first = yield* terminal.readLine + const second = yield* terminal.readLine.pipe(Effect.flip) + return { first, second: second._tag } +}) + +const readLines = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const first = yield* terminal.readLine + const second = yield* terminal.readLine + return { first, second } +}) + +const unused = Effect.gen(function*() { + yield* Terminal.Terminal + return { dataListeners: process.stdin.listenerCount("data") } +}) + +const readLineAfterEnd = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + yield* Effect.callback((resume) => { + if (process.stdin.readableEnded) { + resume(Effect.void) + return + } + const onEnd = () => resume(Effect.void) + process.stdin.once("end", onEnd) + process.stdin.resume() + return Effect.sync(() => process.stdin.off("end", onEnd)) + }) + const error = yield* terminal.readLine.pipe(Effect.flip) + return error._tag +}) + +const readLineDisposed = Effect.gen(function*() { + const terminal = yield* Terminal.Terminal + const line = yield* terminal.readLine + const duringTtl = process.stdin.listenerCount("data") + yield* Effect.sleep("20 millis") + return { line, duringTtl, dataListeners: process.stdin.listenerCount("data") } +}) + +const mode = process.argv[2] +const program = Effect.gen(function*() { + if (mode === "prompts") { + return yield* prompts + } else if (mode === "read-input") { + return yield* readInput + } else if (mode === "read-line") { + return yield* readLine + } else if (mode === "read-lines") { + return yield* readLines + } else if (mode === "unused") { + return yield* unused + } else if (mode === "read-line-after-end") { + return yield* readLineAfterEnd + } else if (mode === "read-line-disposed") { + return yield* readLineDisposed + } + return yield* Effect.die(`Unknown mode: ${mode}`) +}) + +Effect.runPromise(program.pipe(Effect.provide(TerminalLayer))).then( + (result) => process.stderr.write(`RESULT ${JSON.stringify(result)}\n`), + (cause) => { + process.stderr.write(`ERROR ${String(cause)}\n`) + process.exitCode = 1 + } +) diff --git a/.context/effect/packages/platform/node-shared/tsconfig.json b/.context/effect/packages/platform/node-shared/tsconfig.json new file mode 100644 index 000000000..0ec4ad2bc --- /dev/null +++ b/.context/effect/packages/platform/node-shared/tsconfig.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" } + ], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/.context/effect/packages/platform/node/CHANGELOG.md b/.context/effect/packages/platform/node/CHANGELOG.md new file mode 100644 index 000000000..d0bd4b0c3 --- /dev/null +++ b/.context/effect/packages/platform/node/CHANGELOG.md @@ -0,0 +1,945 @@ +# @effect/platform-node + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node-shared@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9), [`e74c302`](https://github.com/Effect-TS/effect/commit/e74c302afe0368e5d3f15d18c10fc54cf33f9003)]: + - effect@4.0.0-beta.107 + - @effect/platform-node-shared@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`5f1775c`](https://github.com/Effect-TS/effect/commit/5f1775cb060cb3dbf96adb067fd97967da7eca2f), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`089313e`](https://github.com/Effect-TS/effect/commit/089313ec2a4c307393c7c5e00c725ec23840c9c1), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node-shared@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node-shared@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#7049](https://github.com/Effect-TS/effect/pull/7049) [`545d876`](https://github.com/Effect-TS/effect/commit/545d8767648cbbbf1820361662c0c1e10768db6a) Thanks @fubhy! - Fix Node HTTP client requests hanging when a streamed request body fails. + +- [#7012](https://github.com/Effect-TS/effect/pull/7012) [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217) Thanks @tim-smart! - Vendor the multipart parser as `effect/unstable/http/MultipartParser`, add the Node.js adapter at `@effect/platform-node/NodeMultipartParser`, and remove the external `multipasta` dependency. + +- [#6927](https://github.com/Effect-TS/effect/pull/6927) [`721b9f0`](https://github.com/Effect-TS/effect/commit/721b9f0d320e50f3e2324c2cd1f43c6643af3ca0) Thanks @longtngo! - Stop a reset upgrade connection from crashing the process in `NodeHttpServer` +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`e2ec131`](https://github.com/Effect-TS/effect/commit/e2ec1311bed9bb8709c26396e71b15b4241a9185), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`e508589`](https://github.com/Effect-TS/effect/commit/e50858905fed68f29ec202ecdc9c902e44bfedd8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node-shared@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6723](https://github.com/Effect-TS/effect/pull/6723) [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e) Thanks @tim-smart! - add platform literal to HttpPlatform + +- [#6802](https://github.com/Effect-TS/effect/pull/6802) [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92) Thanks @tim-smart! - Allow configuring cluster RPC serialization buffer limits. + +- [#6898](https://github.com/Effect-TS/effect/pull/6898) [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b) Thanks @tim-smart! - Add HTTP response compression support. Node.js, Bun, and Deno use asynchronous + `node:zlib` one-shot compression for byte-array bodies, preserving an exact + `Content-Length`; stream and raw bodies remain streaming transforms. + +- [#6680](https://github.com/Effect-TS/effect/pull/6680) [`23e176a`](https://github.com/Effect-TS/effect/commit/23e176a4f05ed3e81cc13a5d70111099692ea9a5) Thanks @tim-smart! - Fix worker runner disconnect notifications and event listener cleanup. + +- [#6691](https://github.com/Effect-TS/effect/pull/6691) [`6a5e86f`](https://github.com/Effect-TS/effect/commit/6a5e86f896c573c391e0fc7888f13e9ae0f07531) Thanks @t3dotgg! - Allow configuring the WebSocket server in `NodeHttpServer` and `BunHttpServer`. + + Both servers now accept a `websocket` option that is forwarded to the underlying implementation, with the wiring/lifecycle options the server manages excluded from the type: + + ```ts + // Node: forwarded to the `ws` WebSocketServer + NodeHttpServer.layer(() => createServer(), { + port: 3000, + websocket: { perMessageDeflate: true }, + }); + + // Bun: merged into Bun.serve's websocket handler + BunHttpServer.layer({ + port: 3000, + websocket: { perMessageDeflate: true }, + }); + ``` + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. + +- [#6813](https://github.com/Effect-TS/effect/pull/6813) [`705d1f1`](https://github.com/Effect-TS/effect/commit/705d1f13a100defe694d7e5c04d4b59cf4e2113b) Thanks @tim-smart! - Optimize Node HTTP streaming responses and ensure HEAD completion and stream backpressure are handled once. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node-shared@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6618](https://github.com/Effect-TS/effect/pull/6618) [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e) Thanks @tim-smart! - unstable/cluster: hash over-length SQL message deduplication keys to prevent `message_id` overflow, closes [#6317](https://github.com/Effect-TS/effect/issues/6317). + + The composed request deduplication key (`entityType/entityId/tag/primaryKey`) can legally exceed the 255-character `message_id` column — the address columns alone allow 458 characters before the RPC primary key is appended. `SqlMessageStorage` now stores a SHA-256 digest (64 hex characters) of the composed key in the unique `message_id` column when the key exceeds 255 characters, so keys of any length work on PostgreSQL, MySQL, MSSQL, and SQLite. Keys that fit are stored as plaintext, byte-compatible with rows written by previous versions, so existing deployments keep deduplicating with no migration or schema change. + + `SqlMessageStorage.layer`/`layerWith` (and consequently `SingleRunner.layer`) now require `Crypto.Crypto`. The Node and Bun cluster convenience layers provide the platform Crypto implementation internally, so their requirements are unchanged. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + - @effect/platform-node-shared@4.0.0-beta.102 + +## 4.0.0-beta.101 + +### Patch Changes + +- Updated dependencies [[`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`b35ed29`](https://github.com/Effect-TS/effect/commit/b35ed2904f01536d303b21f288daf343cf740462), [`dd44624`](https://github.com/Effect-TS/effect/commit/dd446245736a0e88c807a02f03c21450bb9340fa), [`731bea1`](https://github.com/Effect-TS/effect/commit/731bea19416755a904ff5e809413e5922785d0a4), [`2bae1ac`](https://github.com/Effect-TS/effect/commit/2bae1accce9d3b72cf6d5aefc9b2161af6d88436)]: + - effect@4.0.0-beta.101 + - @effect/platform-node-shared@4.0.0-beta.101 + +## 4.0.0-beta.100 + +### Patch Changes + +- [#6506](https://github.com/Effect-TS/effect/pull/6506) [`b0f1a50`](https://github.com/Effect-TS/effect/commit/b0f1a50dacece60aa2393a15da853d82891a7a34) Thanks @tim-smart! - Ensure aborted `HEAD` responses do not block `NodeHttpServer` disposal. + +- Updated dependencies [[`c1288dd`](https://github.com/Effect-TS/effect/commit/c1288dd1a52a2811ab7df57fc4ce236c6be4c745), [`2b58a3d`](https://github.com/Effect-TS/effect/commit/2b58a3dab6bc99776dddaf76e27d811e0f47f3d8), [`6dc83f2`](https://github.com/Effect-TS/effect/commit/6dc83f26ddf20d48db28cf761dd8f3716e5273fb), [`c1e2fe0`](https://github.com/Effect-TS/effect/commit/c1e2fe0cf93564f4d919e3998874c3e70b0cf30f), [`f3fbae8`](https://github.com/Effect-TS/effect/commit/f3fbae8d7bae0d77cb4f35a1598b26c58e3bf94d), [`e000f80`](https://github.com/Effect-TS/effect/commit/e000f80fd55bcd8edc699fdbf4cd109004f4f754), [`f4ee765`](https://github.com/Effect-TS/effect/commit/f4ee7655ee052cf9ba726fd602bb87c89c7c62a9), [`510b55f`](https://github.com/Effect-TS/effect/commit/510b55f3e21750685dbfd5f476a130c1c5af9dbd), [`31d3fc4`](https://github.com/Effect-TS/effect/commit/31d3fc4327c50867bb8d881fa7353aeb03ea2826), [`875e618`](https://github.com/Effect-TS/effect/commit/875e618c3764a7b817ac863d0af86924449528f2), [`688d46a`](https://github.com/Effect-TS/effect/commit/688d46afd0ef923d983ad3d7385f52f217b28d70), [`6ff5023`](https://github.com/Effect-TS/effect/commit/6ff502363b9840a5a5ee0a24bc6cae734ac3a3eb), [`c0333e7`](https://github.com/Effect-TS/effect/commit/c0333e7f755f42ddcca7051e029da8b4eed527bf), [`06e7e8c`](https://github.com/Effect-TS/effect/commit/06e7e8c66015ee318f871b9d2218dee82df2b108), [`eb9b102`](https://github.com/Effect-TS/effect/commit/eb9b10256c8558881b441c2fef833b7037174400), [`8b155da`](https://github.com/Effect-TS/effect/commit/8b155da06e0740c354ec562957a45ab65eb4573b), [`3a87335`](https://github.com/Effect-TS/effect/commit/3a8733564c5db35271aa20564ed0d344daa2a79f)]: + - effect@4.0.0-beta.100 + - @effect/platform-node-shared@4.0.0-beta.100 + +## 4.0.0-beta.99 + +### Patch Changes + +- Updated dependencies [[`8ce4795`](https://github.com/Effect-TS/effect/commit/8ce4795ccbaebca4292757db568c005a992546a4), [`80b539f`](https://github.com/Effect-TS/effect/commit/80b539f8aba68f478c75c35c2b4140c4ffc4fada), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`e6e6dba`](https://github.com/Effect-TS/effect/commit/e6e6dba6e9d86e7c2ad27dcedf289db76a19697f), [`bfb203e`](https://github.com/Effect-TS/effect/commit/bfb203e95aa439f731acad37fc3a9a831a190f1c), [`2e9a34a`](https://github.com/Effect-TS/effect/commit/2e9a34ac2bece4f3a206160480c991e3841dc67a), [`55d4eb3`](https://github.com/Effect-TS/effect/commit/55d4eb34f2c64d54f6a25a305b5c5438ebd7934e), [`bddb010`](https://github.com/Effect-TS/effect/commit/bddb010eac3d4436cb094edbbee7460c5440c162), [`a328835`](https://github.com/Effect-TS/effect/commit/a328835e50d76bc96648a1c1550456e8c9f81210), [`5560d05`](https://github.com/Effect-TS/effect/commit/5560d05aa6abdd29466d9c3412cc5e648b0adbde), [`8f6e3ad`](https://github.com/Effect-TS/effect/commit/8f6e3adb185b16e8820b98c509b308086f7ff1af), [`46997fa`](https://github.com/Effect-TS/effect/commit/46997fa60401f5e3c93daa4b61f7df8e31caaab4), [`9e6e12d`](https://github.com/Effect-TS/effect/commit/9e6e12d75c118cd265496f2880490d1f33a5c8bf), [`3394b93`](https://github.com/Effect-TS/effect/commit/3394b93d97d6f24fc38670641d1490289ffca7f1), [`febeabc`](https://github.com/Effect-TS/effect/commit/febeabc3f7c31094da000a23edeaabfe2ab00a38), [`54161c9`](https://github.com/Effect-TS/effect/commit/54161c98f6f3569e0c31842f54e6a257f9421c4c), [`385f7a4`](https://github.com/Effect-TS/effect/commit/385f7a4ee4a7359928597ea56d151dbaf5eb5802), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`7543afe`](https://github.com/Effect-TS/effect/commit/7543afea6f4d97d1f1ad876224323838a48daadd), [`44b9cf3`](https://github.com/Effect-TS/effect/commit/44b9cf3d240d726997b4bbcd0ede48e074d3c456), [`b330264`](https://github.com/Effect-TS/effect/commit/b3302645fc1c23f7d03a3693dabdcb88763d5cbd), [`7eea4d0`](https://github.com/Effect-TS/effect/commit/7eea4d0b73ec554915d7066a71f46326ce2ba45f), [`0a8aa6a`](https://github.com/Effect-TS/effect/commit/0a8aa6acb90a72b91c24d17133c950e4cacd8abd), [`c8d9fcf`](https://github.com/Effect-TS/effect/commit/c8d9fcf7b030f7c474effbab2764ce7aee1c7209), [`9ca7f9a`](https://github.com/Effect-TS/effect/commit/9ca7f9a69363e4485645966d5a93b8f9597c5206), [`e7aca89`](https://github.com/Effect-TS/effect/commit/e7aca894bb32fbb785b5830837e6061c415a6015), [`55d7560`](https://github.com/Effect-TS/effect/commit/55d75609b8acf8a1b54c1b1c7fbbb65ec741aa3e), [`f809189`](https://github.com/Effect-TS/effect/commit/f809189ddf6b6011ba43a9901baaa734e315da2a), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`0ebdbe7`](https://github.com/Effect-TS/effect/commit/0ebdbe74463dc84385956d0b1e8c2b79ebab5400), [`7517d09`](https://github.com/Effect-TS/effect/commit/7517d09f12a0b183a81bd425962c4e280a68b05d), [`212493b`](https://github.com/Effect-TS/effect/commit/212493b9a1eb98cd1ef6959c707a2e5784a5ae91), [`88a54cc`](https://github.com/Effect-TS/effect/commit/88a54cc341006e3ebcb13482c618f62a680ce199), [`80ea8cb`](https://github.com/Effect-TS/effect/commit/80ea8cb9222ca73f564c8267ab2f82966fea027a), [`8df19f4`](https://github.com/Effect-TS/effect/commit/8df19f4fe81d90cc33ace88b9a77e5534f82d604)]: + - effect@4.0.0-beta.99 + - @effect/platform-node-shared@4.0.0-beta.99 + +## 4.0.0-beta.98 + +### Patch Changes + +- Updated dependencies [[`989603b`](https://github.com/Effect-TS/effect-smol/commit/989603b60ab1197b64acf214208e0d370cd1f842), [`214c458`](https://github.com/Effect-TS/effect-smol/commit/214c458084bb6995d543cd37d1055f24be3d454e), [`a037273`](https://github.com/Effect-TS/effect-smol/commit/a0372736ac34796969b051bbba4717d7983f1ebe), [`97fdaa9`](https://github.com/Effect-TS/effect-smol/commit/97fdaa9c1f522c65e579365d314a07878e2b904f), [`b24d248`](https://github.com/Effect-TS/effect-smol/commit/b24d248c8df44222ce642087cde2bd859a2dc709), [`19c222c`](https://github.com/Effect-TS/effect-smol/commit/19c222cac2353a3d7b7733caecb00556fffe9a5c), [`eec85dd`](https://github.com/Effect-TS/effect-smol/commit/eec85ddba09ea326fd268ee33eeffd47e50d4671), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`8849052`](https://github.com/Effect-TS/effect-smol/commit/884905232d1e9a365e046d8dde27bf9c5707f57f), [`c15e16a`](https://github.com/Effect-TS/effect-smol/commit/c15e16ad130d1fbde25d912b7ac55995066cb35b), [`01d00a3`](https://github.com/Effect-TS/effect-smol/commit/01d00a3abfbf1f37996cdbe738ea5137c646cdd7), [`8bd4589`](https://github.com/Effect-TS/effect-smol/commit/8bd458975a1b3a8ed042eccf317b93d28ded91e7), [`0082f4f`](https://github.com/Effect-TS/effect-smol/commit/0082f4f74fb139fd578f87f0a790e845133983dc), [`6e08428`](https://github.com/Effect-TS/effect-smol/commit/6e08428d980501b856f846ad3f3f0e4ea46e7786), [`388dcf9`](https://github.com/Effect-TS/effect-smol/commit/388dcf953f65d317547f34d40e6443c5f264205f), [`2b7ce2b`](https://github.com/Effect-TS/effect-smol/commit/2b7ce2b513e7ec2a77822f1116dc6ffb6ba93f4e), [`87bea7e`](https://github.com/Effect-TS/effect-smol/commit/87bea7e16259246f3bcdf565446394751abca953), [`ce38dc3`](https://github.com/Effect-TS/effect-smol/commit/ce38dc33bda805a684432cca071f4dc3c6b9a1ba), [`a807cd1`](https://github.com/Effect-TS/effect-smol/commit/a807cd170341deca8a1cfb52c4222585f2431bb9), [`fd8a356`](https://github.com/Effect-TS/effect-smol/commit/fd8a356f06a8c9ce4e7e0a13fc4021c178ed31de), [`c2a5edc`](https://github.com/Effect-TS/effect-smol/commit/c2a5edc3abd31ad5bc123362bc1213e03e4095c3), [`5946da3`](https://github.com/Effect-TS/effect-smol/commit/5946da3804a1be5e752b05b96bd058cdba50a1bf), [`4ae0c5f`](https://github.com/Effect-TS/effect-smol/commit/4ae0c5ffcbe6c56ddfcb05c639112a079483539e), [`5b2a0bc`](https://github.com/Effect-TS/effect-smol/commit/5b2a0bceea3a28a33a58555210c90a415dc74a76), [`72ac585`](https://github.com/Effect-TS/effect-smol/commit/72ac585884befde6af9208da738699a93f1bae79), [`5e8c1b8`](https://github.com/Effect-TS/effect-smol/commit/5e8c1b82bfafa121311f987a49ab75395e3647a7), [`0f9c078`](https://github.com/Effect-TS/effect-smol/commit/0f9c07841b04183f485ee6e6458de73b290b09f5)]: + - effect@4.0.0-beta.98 + - @effect/platform-node-shared@4.0.0-beta.98 + +## 4.0.0-beta.97 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.97 + - @effect/platform-node-shared@4.0.0-beta.97 + +## 4.0.0-beta.96 + +### Patch Changes + +- Updated dependencies [[`1503f45`](https://github.com/Effect-TS/effect-smol/commit/1503f45cb5bb2a74f4705252ec505a1f0ade7e62), [`57fe793`](https://github.com/Effect-TS/effect-smol/commit/57fe79316ffbc380b30626a168981fb26ae97459), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`0c2f78f`](https://github.com/Effect-TS/effect-smol/commit/0c2f78f695ec474e1ff5474da183577975e418f5), [`97f29df`](https://github.com/Effect-TS/effect-smol/commit/97f29df457f7ffd07cfb4b379315c12c086af805)]: + - effect@4.0.0-beta.96 + - @effect/platform-node-shared@4.0.0-beta.96 + +## 4.0.0-beta.95 + +### Patch Changes + +- Updated dependencies [[`a482442`](https://github.com/Effect-TS/effect-smol/commit/a482442abdeb490e9652b854ec3495e4aa7273e7), [`fbefa85`](https://github.com/Effect-TS/effect-smol/commit/fbefa850fab2f0a302c20614496aeaaa2a8b5590), [`0b4a32f`](https://github.com/Effect-TS/effect-smol/commit/0b4a32f4260f0d8500942a133001b0d349328102), [`18a49e1`](https://github.com/Effect-TS/effect-smol/commit/18a49e1786679456258002ff9397faf02f678c2d), [`266cb90`](https://github.com/Effect-TS/effect-smol/commit/266cb90bb2c17aabc40563c32db334f09ba3d74b), [`912f095`](https://github.com/Effect-TS/effect-smol/commit/912f095a34572bbd3cedf6edb27878443e3e4a95), [`a6718f9`](https://github.com/Effect-TS/effect-smol/commit/a6718f9e00a15ca903b0732da46116cbf3d6aca7), [`bef5154`](https://github.com/Effect-TS/effect-smol/commit/bef51540a243aa2f872a00c01d0cd58b7a769baa), [`18e0564`](https://github.com/Effect-TS/effect-smol/commit/18e0564bd0f8ebbdfcaf1e2c21529948e9e4a81d), [`fb50f14`](https://github.com/Effect-TS/effect-smol/commit/fb50f14fc3657c1973785aa5b72ecf0b0d28e0b2)]: + - effect@4.0.0-beta.95 + - @effect/platform-node-shared@4.0.0-beta.95 + +## 4.0.0-beta.94 + +### Patch Changes + +- Updated dependencies [[`95a0e9b`](https://github.com/Effect-TS/effect-smol/commit/95a0e9bb62797af0e81c9998773405f248f218c5), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63), [`f11ce73`](https://github.com/Effect-TS/effect-smol/commit/f11ce73af60823754dc24194f4ffc561b9ea1c2d), [`ff30b6e`](https://github.com/Effect-TS/effect-smol/commit/ff30b6e7c2c63ffc56a4c5818d6d86b01b5ad528), [`1caab3c`](https://github.com/Effect-TS/effect-smol/commit/1caab3cc30f626efbf15e59d74f539a487e5c85c), [`aa80c47`](https://github.com/Effect-TS/effect-smol/commit/aa80c4775a04db87553e5568764cab7e32a72814), [`c2ae4fc`](https://github.com/Effect-TS/effect-smol/commit/c2ae4fce2f03a4cd1861c2b1179da7df656e662d), [`a0a3490`](https://github.com/Effect-TS/effect-smol/commit/a0a3490bbce765f199d8e077aceac504f0462e63)]: + - effect@4.0.0-beta.94 + - @effect/platform-node-shared@4.0.0-beta.94 + +## 4.0.0-beta.93 + +### Patch Changes + +- Updated dependencies [[`00652fe`](https://github.com/Effect-TS/effect-smol/commit/00652fe95c18f87208e91343eb8bf218faa2f677), [`6c58167`](https://github.com/Effect-TS/effect-smol/commit/6c5816746eaf91d2a3c7c899c5720809fa230ae3), [`2bc5415`](https://github.com/Effect-TS/effect-smol/commit/2bc541501a7ef89e542d7cb98e96beb53cd205cc), [`e11cccc`](https://github.com/Effect-TS/effect-smol/commit/e11cccc7d5fe631abccc7d6e3bd296938de0fa2e), [`ba7e77e`](https://github.com/Effect-TS/effect-smol/commit/ba7e77e046b8641a3a4e9750bb88ca4a1d063d3f), [`5713ee7`](https://github.com/Effect-TS/effect-smol/commit/5713ee7edbc3054efde407b2286bbfd45bbc6e1c)]: + - effect@4.0.0-beta.93 + - @effect/platform-node-shared@4.0.0-beta.93 + +## 4.0.0-beta.92 + +### Patch Changes + +- Updated dependencies [[`affdc13`](https://github.com/Effect-TS/effect-smol/commit/affdc139045cc325dce321a84a580fdc1b2da7b9)]: + - effect@4.0.0-beta.92 + - @effect/platform-node-shared@4.0.0-beta.92 + +## 4.0.0-beta.91 + +### Patch Changes + +- Updated dependencies [[`b135b25`](https://github.com/Effect-TS/effect-smol/commit/b135b2517fca9e7839734ace3699a7dfa75b9075), [`aaa21a3`](https://github.com/Effect-TS/effect-smol/commit/aaa21a369a171c600db294f2a4f640583043e150), [`3475ee6`](https://github.com/Effect-TS/effect-smol/commit/3475ee6c2bda6b05c6d7a12ce30c8bb840b5b1a6)]: + - effect@4.0.0-beta.91 + - @effect/platform-node-shared@4.0.0-beta.91 + +## 4.0.0-beta.90 + +### Patch Changes + +- Updated dependencies [[`d237fdf`](https://github.com/Effect-TS/effect-smol/commit/d237fdf726481f76eb52a6196e111b24122bc3d5)]: + - effect@4.0.0-beta.90 + - @effect/platform-node-shared@4.0.0-beta.90 + +## 4.0.0-beta.89 + +### Patch Changes + +- Updated dependencies [[`b7d46ab`](https://github.com/Effect-TS/effect-smol/commit/b7d46ab7e1a29d8711817bab583c9febf48a0dad), [`7777e15`](https://github.com/Effect-TS/effect-smol/commit/7777e1540fd3680dd8346723cffec812b9384669), [`5376197`](https://github.com/Effect-TS/effect-smol/commit/5376197ca8e50358a41b1fd3cec27bd1ec680ec6)]: + - effect@4.0.0-beta.89 + - @effect/platform-node-shared@4.0.0-beta.89 + +## 4.0.0-beta.88 + +### Patch Changes + +- Updated dependencies [[`911f1b8`](https://github.com/Effect-TS/effect-smol/commit/911f1b84790ce42b3a70c95b33e6f6fd9e74de8b), [`8beeeea`](https://github.com/Effect-TS/effect-smol/commit/8beeeea52879d8613a39468848f01c3092bd54d4), [`c306fcf`](https://github.com/Effect-TS/effect-smol/commit/c306fcfeb1ef38455156932a1faf49292b1318da)]: + - effect@4.0.0-beta.88 + - @effect/platform-node-shared@4.0.0-beta.88 + +## 4.0.0-beta.87 + +### Patch Changes + +- Updated dependencies [[`5a0c1a4`](https://github.com/Effect-TS/effect-smol/commit/5a0c1a4faee5707b5cc35e646ff1ffdad70f1956), [`1eea2ea`](https://github.com/Effect-TS/effect-smol/commit/1eea2ea3795ba47316b82b1ac8d4612c0ba389ed)]: + - effect@4.0.0-beta.87 + - @effect/platform-node-shared@4.0.0-beta.87 + +## 4.0.0-beta.86 + +### Patch Changes + +- [#2463](https://github.com/Effect-TS/effect-smol/pull/2463) [`28b4196`](https://github.com/Effect-TS/effect-smol/commit/28b4196390d3ab83be1567b65440919a9061fcc3) Thanks @tim-smart! - Update `NodeHttpServer.layerConfig`'s type to report the same provided Node services as `NodeHttpServer.layer`. + +- Updated dependencies [[`0b5795a`](https://github.com/Effect-TS/effect-smol/commit/0b5795a0ab4395e8f15955d8d96f2303084bfc64), [`3e3a859`](https://github.com/Effect-TS/effect-smol/commit/3e3a859ec6351a9e0d31674aabbd48fcefabb12e), [`7dbec24`](https://github.com/Effect-TS/effect-smol/commit/7dbec240dbf3bca599a20c486632abce694ef5ab), [`d8c00a1`](https://github.com/Effect-TS/effect-smol/commit/d8c00a171ac7141e8adc08c332d1162d9a9d56fc), [`85b6317`](https://github.com/Effect-TS/effect-smol/commit/85b631701e935866f2762bd595237aa718370cd9), [`6d0fda0`](https://github.com/Effect-TS/effect-smol/commit/6d0fda0d0cbdfffc523c89c57dfdb1608f84fb12), [`108a933`](https://github.com/Effect-TS/effect-smol/commit/108a9335ff8571928197e5847a09c28ac83d6f46), [`7e1f455`](https://github.com/Effect-TS/effect-smol/commit/7e1f455fab5005d769b939c91e519d450f802cf9), [`46b3e79`](https://github.com/Effect-TS/effect-smol/commit/46b3e79944cfdae7901eb148135c85b7eb39834e)]: + - effect@4.0.0-beta.86 + - @effect/platform-node-shared@4.0.0-beta.86 + +## 4.0.0-beta.85 + +### Patch Changes + +- Updated dependencies [[`328d97c`](https://github.com/Effect-TS/effect-smol/commit/328d97cc53c0dcb89077a5623e35b095eaa59a8c), [`8441836`](https://github.com/Effect-TS/effect-smol/commit/8441836e6dde70e8ae2126be9cefe9b45798b134), [`074e436`](https://github.com/Effect-TS/effect-smol/commit/074e4361091289104cb0ab6959dc3b0ea7794a6a), [`c1dfd60`](https://github.com/Effect-TS/effect-smol/commit/c1dfd60663eb13a58916f3712d877499943b628a), [`2ba316b`](https://github.com/Effect-TS/effect-smol/commit/2ba316bd15fcbf1c50626500d44a2c9b3bec19f5), [`7ce7344`](https://github.com/Effect-TS/effect-smol/commit/7ce7344c41056c79e2ee19ee6a9346c0f1d227c1)]: + - effect@4.0.0-beta.85 + - @effect/platform-node-shared@4.0.0-beta.85 + +## 4.0.0-beta.84 + +### Patch Changes + +- Updated dependencies [[`87f52ba`](https://github.com/Effect-TS/effect-smol/commit/87f52ba16c4370ffa3f84bf8e53038e1419c284e), [`b8ee07f`](https://github.com/Effect-TS/effect-smol/commit/b8ee07ffda8903b5ec2e45a786ddcba59f128fda), [`867c0d7`](https://github.com/Effect-TS/effect-smol/commit/867c0d70a09079b040260d45a1e92ff04dbfbf2f), [`b93bc6c`](https://github.com/Effect-TS/effect-smol/commit/b93bc6c9cb27b909a41d094c97c4f9d25bbc6d6b), [`57d387f`](https://github.com/Effect-TS/effect-smol/commit/57d387f92c30ab63e15e3e641f0a903b65886610), [`bacca41`](https://github.com/Effect-TS/effect-smol/commit/bacca4141c2400effae1eabfdb36c89a459cf246), [`0f8ac79`](https://github.com/Effect-TS/effect-smol/commit/0f8ac7959d29ed68c68ce25aabd6bf0cb7e63ecc), [`25b4482`](https://github.com/Effect-TS/effect-smol/commit/25b448270c01317703f25107e1480d4cd0246d9a), [`9cf3a25`](https://github.com/Effect-TS/effect-smol/commit/9cf3a25c66b0c44a52be9829870c44517ea52db2), [`8def767`](https://github.com/Effect-TS/effect-smol/commit/8def7674b1787f91035298cda4d122937e87ef72)]: + - effect@4.0.0-beta.84 + - @effect/platform-node-shared@4.0.0-beta.84 + +## 4.0.0-beta.83 + +### Patch Changes + +- Updated dependencies [[`1f2e8ce`](https://github.com/Effect-TS/effect-smol/commit/1f2e8ceef09e0a791c850ed2ade01f97089596f9)]: + - effect@4.0.0-beta.83 + - @effect/platform-node-shared@4.0.0-beta.83 + +## 4.0.0-beta.82 + +### Patch Changes + +- Updated dependencies [[`193690b`](https://github.com/Effect-TS/effect-smol/commit/193690b642ea802bbed40d663bd677251bbe9dc3)]: + - effect@4.0.0-beta.82 + - @effect/platform-node-shared@4.0.0-beta.82 + +## 4.0.0-beta.81 + +### Patch Changes + +- Updated dependencies [[`93cb4f8`](https://github.com/Effect-TS/effect-smol/commit/93cb4f8fbfb9e07cb9dc86ce6b155fd1f8167914), [`60341d9`](https://github.com/Effect-TS/effect-smol/commit/60341d9ca744d0473ce3fab621ca9bd225af3a39), [`1105ab5`](https://github.com/Effect-TS/effect-smol/commit/1105ab56cb724212f7ea7b431396ce82e8fd0484), [`4500fbf`](https://github.com/Effect-TS/effect-smol/commit/4500fbfe00763d8a72af6e5d6c5988e8bd4ade36)]: + - effect@4.0.0-beta.81 + - @effect/platform-node-shared@4.0.0-beta.81 + +## 4.0.0-beta.80 + +### Patch Changes + +- Updated dependencies [[`d944330`](https://github.com/Effect-TS/effect-smol/commit/d94433090ee03f426d43e13b883abae4494e55e6), [`f48659f`](https://github.com/Effect-TS/effect-smol/commit/f48659fdcc84930ebc1e5b45b540c0f973389182), [`7652aaa`](https://github.com/Effect-TS/effect-smol/commit/7652aaa3bdbc39f241fe58b54b9a43b713e22e12), [`98630b7`](https://github.com/Effect-TS/effect-smol/commit/98630b7c8f679c352ba6796636c85688fa009d8d), [`90ae23c`](https://github.com/Effect-TS/effect-smol/commit/90ae23cf07284da5e1bcd9dffa882e85df7e617b)]: + - effect@4.0.0-beta.80 + - @effect/platform-node-shared@4.0.0-beta.80 + +## 4.0.0-beta.79 + +### Patch Changes + +- Updated dependencies [[`b9704dc`](https://github.com/Effect-TS/effect-smol/commit/b9704dc9de9f1649ad502371014fe869b69a49a3), [`a207113`](https://github.com/Effect-TS/effect-smol/commit/a207113f66837bb54416926718a9a7d66774d079), [`5e9b9e2`](https://github.com/Effect-TS/effect-smol/commit/5e9b9e217b164ebfd4a002dd4380b3b1563200c3), [`7c128ae`](https://github.com/Effect-TS/effect-smol/commit/7c128aef458a1e2d224712e51c483c9badad1d44), [`0ada457`](https://github.com/Effect-TS/effect-smol/commit/0ada457c0513d8d908254ab77ebb7d29d2b523d6), [`d7cc5a2`](https://github.com/Effect-TS/effect-smol/commit/d7cc5a2bede3de10943aa0c6bdb4f26836a91efd), [`aad63be`](https://github.com/Effect-TS/effect-smol/commit/aad63becf65e0a6b076e94f8973be7bbe7fbd46f), [`09809f6`](https://github.com/Effect-TS/effect-smol/commit/09809f60f19ec98232f98b33e33e02ecb7e4fbd6), [`2fddda5`](https://github.com/Effect-TS/effect-smol/commit/2fddda5311929f46b61e503f0ade4fc749e8c77d), [`5f21768`](https://github.com/Effect-TS/effect-smol/commit/5f2176833399757c4500d8875b7f2fba0393de75), [`f27003e`](https://github.com/Effect-TS/effect-smol/commit/f27003e00524ff83f20dd9909f62b2f8795efe03)]: + - effect@4.0.0-beta.79 + - @effect/platform-node-shared@4.0.0-beta.79 + +## 4.0.0-beta.78 + +### Patch Changes + +- Updated dependencies [[`7836b8e`](https://github.com/Effect-TS/effect-smol/commit/7836b8eb8bb0f3e04cdf554ee070caccf74f00c1), [`35d49a3`](https://github.com/Effect-TS/effect-smol/commit/35d49a3a09bdba6b513de87ddcead9e61a1042ba), [`4093258`](https://github.com/Effect-TS/effect-smol/commit/40932580e65bafab5f23c5f14b520cb411d0b2cd)]: + - effect@4.0.0-beta.78 + - @effect/platform-node-shared@4.0.0-beta.78 + +## 4.0.0-beta.77 + +### Patch Changes + +- Updated dependencies [[`6e9a5ca`](https://github.com/Effect-TS/effect-smol/commit/6e9a5ca62a61156fd67b2518ad3ab14ac0d25f23), [`302f398`](https://github.com/Effect-TS/effect-smol/commit/302f3984ce206e35d86ddd99d3b72be144850a51)]: + - effect@4.0.0-beta.77 + - @effect/platform-node-shared@4.0.0-beta.77 + +## 4.0.0-beta.76 + +### Patch Changes + +- Updated dependencies [[`016108a`](https://github.com/Effect-TS/effect-smol/commit/016108a472af7048ddbbfd05f233e67529fafe12), [`95c03d2`](https://github.com/Effect-TS/effect-smol/commit/95c03d2c55930668c215b5a41c23cf7742fead84), [`07299a3`](https://github.com/Effect-TS/effect-smol/commit/07299a33c09fd52faa9810d30835a2622c752386)]: + - effect@4.0.0-beta.76 + - @effect/platform-node-shared@4.0.0-beta.76 + +## 4.0.0-beta.75 + +### Patch Changes + +- Updated dependencies [[`81b187c`](https://github.com/Effect-TS/effect-smol/commit/81b187c17a0d8817b58232826939154010ae49d7), [`ad4b535`](https://github.com/Effect-TS/effect-smol/commit/ad4b535e17f94ce35261829d5a3675f0a7808b4e), [`a29c2e7`](https://github.com/Effect-TS/effect-smol/commit/a29c2e7e3570920156702671d6f3367cd0195f6c), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`1fdd9ae`](https://github.com/Effect-TS/effect-smol/commit/1fdd9aeed92b6bb70987c862e7f6f66ead0339b3), [`ffea4ec`](https://github.com/Effect-TS/effect-smol/commit/ffea4ecf2925f6a4c9fd13079d47584cbf2bed00), [`4255c9b`](https://github.com/Effect-TS/effect-smol/commit/4255c9ba78bb98c7838fbe9dccdd8465e9da5427)]: + - effect@4.0.0-beta.75 + - @effect/platform-node-shared@4.0.0-beta.75 + +## 4.0.0-beta.74 + +### Patch Changes + +- Updated dependencies [[`b1fc6a4`](https://github.com/Effect-TS/effect-smol/commit/b1fc6a4b4d0ca7fa9fd162799ae17c86f2f7ee8e)]: + - effect@4.0.0-beta.74 + - @effect/platform-node-shared@4.0.0-beta.74 + +## 4.0.0-beta.73 + +### Patch Changes + +- Updated dependencies [[`361ca30`](https://github.com/Effect-TS/effect-smol/commit/361ca30eb6e134feece547d6e00f82be4cb23f75), [`b9598c6`](https://github.com/Effect-TS/effect-smol/commit/b9598c6a209e75bfdb87ee3b024ecd1e3923ff6e)]: + - effect@4.0.0-beta.73 + - @effect/platform-node-shared@4.0.0-beta.73 + +## 4.0.0-beta.72 + +### Patch Changes + +- Updated dependencies [[`73e67d1`](https://github.com/Effect-TS/effect-smol/commit/73e67d119a84d697773eaecb4865c6a71eb1a9cb), [`01d71ec`](https://github.com/Effect-TS/effect-smol/commit/01d71ec5a75f3c2747a8d3b1ad9701d1e27b7ce5), [`fcd707e`](https://github.com/Effect-TS/effect-smol/commit/fcd707e091a16e1b35343c901cc4052274e32239)]: + - effect@4.0.0-beta.72 + - @effect/platform-node-shared@4.0.0-beta.72 + +## 4.0.0-beta.71 + +### Patch Changes + +- Updated dependencies [[`d8ac76b`](https://github.com/Effect-TS/effect-smol/commit/d8ac76b5bad458c42cebe8a0c1b3843f955ac293), [`2c3c00a`](https://github.com/Effect-TS/effect-smol/commit/2c3c00af6faba7b7d422af26a7a2bbc35636d230), [`3751e7c`](https://github.com/Effect-TS/effect-smol/commit/3751e7cf353e7a54cd692c37401207d9afba1e63), [`fc5f25b`](https://github.com/Effect-TS/effect-smol/commit/fc5f25b03ada5fc2431987768a74d3d3e75ca485), [`7ccced4`](https://github.com/Effect-TS/effect-smol/commit/7ccced42867c14c013b01160b3d292f14c05bd04), [`a2e1fe5`](https://github.com/Effect-TS/effect-smol/commit/a2e1fe5835c98c8ee4393a091b1d11b75126e349), [`4a4a36b`](https://github.com/Effect-TS/effect-smol/commit/4a4a36b10e6e616cad07584a43908f6a7e07e618), [`d350292`](https://github.com/Effect-TS/effect-smol/commit/d3502922b4740fa9d745797cbc3775cb67839b6d), [`730afb6`](https://github.com/Effect-TS/effect-smol/commit/730afb66696adf9bd5a328cbca29df9c05968771), [`df1b008`](https://github.com/Effect-TS/effect-smol/commit/df1b008f370f414c2a67a7b8139ef747af8e5fba), [`6d469d5`](https://github.com/Effect-TS/effect-smol/commit/6d469d567a7c41d7e5343bdee21d45b07b0e8190)]: + - effect@4.0.0-beta.71 + - @effect/platform-node-shared@4.0.0-beta.71 + +## 4.0.0-beta.70 + +### Patch Changes + +- Updated dependencies [[`af7782d`](https://github.com/Effect-TS/effect-smol/commit/af7782d3008d08b043f3a3f261516001514b2b4e), [`5805569`](https://github.com/Effect-TS/effect-smol/commit/5805569ef5ac47db49924fa34abf03fbdc1675f3), [`7212d70`](https://github.com/Effect-TS/effect-smol/commit/7212d701a3eee7b3553ff502e2c066126e52e839)]: + - effect@4.0.0-beta.70 + - @effect/platform-node-shared@4.0.0-beta.70 + +## 4.0.0-beta.69 + +### Patch Changes + +- Updated dependencies [[`70ea04a`](https://github.com/Effect-TS/effect-smol/commit/70ea04aa96a2a7859d738d414e1f0e3ed081a27a), [`d0ea8b0`](https://github.com/Effect-TS/effect-smol/commit/d0ea8b03f7d73ae076c1db12666141e480d11178), [`a57674b`](https://github.com/Effect-TS/effect-smol/commit/a57674b64845e9e75a456cf907bfdcb858859118), [`59aa334`](https://github.com/Effect-TS/effect-smol/commit/59aa334fbd0a504dda3c36f6d2ef1be7449b4b8b), [`8f4208e`](https://github.com/Effect-TS/effect-smol/commit/8f4208ee83bc7bdaa6793b5429847b45aab72470)]: + - effect@4.0.0-beta.69 + - @effect/platform-node-shared@4.0.0-beta.69 + +## 4.0.0-beta.68 + +### Patch Changes + +- [#2180](https://github.com/Effect-TS/effect-smol/pull/2180) [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5) Thanks @IMax153! - Add a platform-agnostic `Crypto` service for cryptographic random bytes, secure random generators, UUIDv4 / UUIDv7 generation, and digest operations. UUID generation should now use the `Crypto` service's `randomUUIDv4` or `randomUUIDv7`, which format bytes from the platform `Crypto` service; UUIDv7 also uses the `Clock` service timestamp. `Random.nextUUIDv4` has been removed because the base `Random` service is not cryptographically secure. + +- Updated dependencies [[`af8267f`](https://github.com/Effect-TS/effect-smol/commit/af8267f2f3588c3fb611e9286f6f933f29ce1217), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`0176eaf`](https://github.com/Effect-TS/effect-smol/commit/0176eaf3ecd7c1b99a10268f2af02d7e8ce161e5), [`f136bb7`](https://github.com/Effect-TS/effect-smol/commit/f136bb763048cbc6b17edd26496dba3e2415b9fa), [`6f38f07`](https://github.com/Effect-TS/effect-smol/commit/6f38f07d5941a211b251383aaab0f4f55e8a6557), [`aec9c40`](https://github.com/Effect-TS/effect-smol/commit/aec9c401a53db227f18bf5e0c84db7130ad862d6)]: + - effect@4.0.0-beta.68 + - @effect/platform-node-shared@4.0.0-beta.68 + +## 4.0.0-beta.67 + +### Patch Changes + +- [#2185](https://github.com/Effect-TS/effect-smol/pull/2185) [`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f) Thanks @lloydrichards! - add rows to Terminal + +- Updated dependencies [[`a42ef66`](https://github.com/Effect-TS/effect-smol/commit/a42ef6632abbddfa820995ae310ccc84ae8d9b6f), [`35594f8`](https://github.com/Effect-TS/effect-smol/commit/35594f811cafe471acd490114b103a1f8392c8d8), [`8bddd62`](https://github.com/Effect-TS/effect-smol/commit/8bddd628cb623f9533d345082583ff51cead6836), [`4be4c8d`](https://github.com/Effect-TS/effect-smol/commit/4be4c8d60862aa963869ee2ed9ffa048ffac0527), [`0c9d3ab`](https://github.com/Effect-TS/effect-smol/commit/0c9d3ab43eb721a370ed8306260cbac218c27e87), [`b156acc`](https://github.com/Effect-TS/effect-smol/commit/b156accd2691b4a051f823affdece7c39923ce85), [`d16c034`](https://github.com/Effect-TS/effect-smol/commit/d16c03434ee3e6dcd3bfc82b65d99e881d89025b), [`b559d68`](https://github.com/Effect-TS/effect-smol/commit/b559d68845f848a10153395778f035682d399075), [`a3de5d9`](https://github.com/Effect-TS/effect-smol/commit/a3de5d9215e5cc4a62e2666efbd7c1bf595eb84f), [`7e6c12e`](https://github.com/Effect-TS/effect-smol/commit/7e6c12ec9b3a5945f6c26e272cc8f6390541ad3e), [`098167a`](https://github.com/Effect-TS/effect-smol/commit/098167a220fe07da6f14455818733ab1b269c9dd)]: + - effect@4.0.0-beta.67 + - @effect/platform-node-shared@4.0.0-beta.67 + +## 4.0.0-beta.66 + +### Patch Changes + +- Updated dependencies [[`ca2498e`](https://github.com/Effect-TS/effect-smol/commit/ca2498e702ac2d83fb7187707b7eb069bdb261a2), [`cd7d1fb`](https://github.com/Effect-TS/effect-smol/commit/cd7d1fba7e2e2c5ac3ad64e1be433440a5bda436), [`19a7033`](https://github.com/Effect-TS/effect-smol/commit/19a703367ec817cffc41d152da9b594827408e2b), [`33d26b4`](https://github.com/Effect-TS/effect-smol/commit/33d26b4210b2e974f146a71e7eed962f8ce00900), [`856766b`](https://github.com/Effect-TS/effect-smol/commit/856766b2c506aaed6d2df1d63bf3a5b1b062e1d4), [`079c7df`](https://github.com/Effect-TS/effect-smol/commit/079c7df82559bb9ce10a86dffb85d25e6ce07dc3)]: + - effect@4.0.0-beta.66 + - @effect/platform-node-shared@4.0.0-beta.66 + +## 4.0.0-beta.65 + +### Patch Changes + +- Updated dependencies [[`6f11454`](https://github.com/Effect-TS/effect-smol/commit/6f11454a9b6c3bd00f6b35fd7af14a2f2d63a0a2)]: + - effect@4.0.0-beta.65 + - @effect/platform-node-shared@4.0.0-beta.65 + +## 4.0.0-beta.64 + +### Patch Changes + +- Updated dependencies [[`7d4877a`](https://github.com/Effect-TS/effect-smol/commit/7d4877a1929cdb690280ea254326c04f2ec97ea5)]: + - effect@4.0.0-beta.64 + - @effect/platform-node-shared@4.0.0-beta.64 + +## 4.0.0-beta.63 + +### Patch Changes + +- Updated dependencies [[`7f927ff`](https://github.com/Effect-TS/effect-smol/commit/7f927ffb7a9801dcfc4096c29e369d13d65cd0ac), [`a696b3e`](https://github.com/Effect-TS/effect-smol/commit/a696b3e83a8504cdbe261a18c10a1cc0619ae102)]: + - effect@4.0.0-beta.63 + - @effect/platform-node-shared@4.0.0-beta.63 + +## 4.0.0-beta.62 + +### Patch Changes + +- Updated dependencies [[`4ab4b90`](https://github.com/Effect-TS/effect-smol/commit/4ab4b9007dc27a52ffabc6fcb37c96eeec795bf7)]: + - effect@4.0.0-beta.62 + - @effect/platform-node-shared@4.0.0-beta.62 + +## 4.0.0-beta.61 + +### Patch Changes + +- Updated dependencies [[`50790af`](https://github.com/Effect-TS/effect-smol/commit/50790af9b190c38d10fb0723837d49b66432638f), [`71f7c3d`](https://github.com/Effect-TS/effect-smol/commit/71f7c3df997deda92c84146d569696dab3bd645c), [`aae8797`](https://github.com/Effect-TS/effect-smol/commit/aae8797b9cb383be0c182dd58d03d787c354238b)]: + - effect@4.0.0-beta.61 + - @effect/platform-node-shared@4.0.0-beta.61 + +## 4.0.0-beta.60 + +### Patch Changes + +- Updated dependencies [[`f69d567`](https://github.com/Effect-TS/effect-smol/commit/f69d5675dcff9f4137295752baf066b7153fdc09), [`7909c95`](https://github.com/Effect-TS/effect-smol/commit/7909c954b8f6244a35a4b429f8dd0dff45dad620), [`bbb4dcc`](https://github.com/Effect-TS/effect-smol/commit/bbb4dcc6c406b83a416b4ad3541cc02037c420e4), [`7af2207`](https://github.com/Effect-TS/effect-smol/commit/7af2207901eabf3132c1b7010a69b3899c06fbbe), [`848b40a`](https://github.com/Effect-TS/effect-smol/commit/848b40a4bd4bf54a5098617d50c33c88eee8270a)]: + - effect@4.0.0-beta.60 + - @effect/platform-node-shared@4.0.0-beta.60 + +## 4.0.0-beta.59 + +### Patch Changes + +- Updated dependencies [[`56837ea`](https://github.com/Effect-TS/effect-smol/commit/56837ea2a338395b35550641374e9e589bd8b71d)]: + - effect@4.0.0-beta.59 + - @effect/platform-node-shared@4.0.0-beta.59 + +## 4.0.0-beta.58 + +### Patch Changes + +- [#2098](https://github.com/Effect-TS/effect-smol/pull/2098) [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec) Thanks @tim-smart! - improve http body consumption + +- Updated dependencies [[`11993d4`](https://github.com/Effect-TS/effect-smol/commit/11993d4934c66f5dc611b8bbf553f01d501ef8f7), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec), [`96c8b22`](https://github.com/Effect-TS/effect-smol/commit/96c8b22c2057ccddbf10ed269d7697f22119b3ec)]: + - effect@4.0.0-beta.58 + - @effect/platform-node-shared@4.0.0-beta.58 + +## 4.0.0-beta.57 + +### Patch Changes + +- Updated dependencies [[`a971f5c`](https://github.com/Effect-TS/effect-smol/commit/a971f5cbd92dfe4274420bf0966595eb35531060), [`8e110c5`](https://github.com/Effect-TS/effect-smol/commit/8e110c5f02a429ccc43a91df8678e402138c0851)]: + - effect@4.0.0-beta.57 + - @effect/platform-node-shared@4.0.0-beta.57 + +## 4.0.0-beta.56 + +### Patch Changes + +- Updated dependencies []: + - effect@4.0.0-beta.56 + - @effect/platform-node-shared@4.0.0-beta.56 + +## 4.0.0-beta.55 + +### Patch Changes + +- Updated dependencies [[`42cc744`](https://github.com/Effect-TS/effect-smol/commit/42cc744570968deb365fb46d47b53d3277050c93), [`04855ce`](https://github.com/Effect-TS/effect-smol/commit/04855ceeca4d40c55a5750dd9893b691f8ea741a)]: + - effect@4.0.0-beta.55 + - @effect/platform-node-shared@4.0.0-beta.55 + +## 4.0.0-beta.54 + +### Patch Changes + +- Updated dependencies [[`e4b74f9`](https://github.com/Effect-TS/effect-smol/commit/e4b74f9c01a0e9b6cd58416de4af3a26d51da7c8), [`4c72808`](https://github.com/Effect-TS/effect-smol/commit/4c728081851c66dacf889a816535671bc841ae96)]: + - effect@4.0.0-beta.54 + - @effect/platform-node-shared@4.0.0-beta.54 + +## 4.0.0-beta.53 + +### Patch Changes + +- Updated dependencies [[`0768509`](https://github.com/Effect-TS/effect-smol/commit/07685094e931af07d104165195826a535b55fa7e), [`476aede`](https://github.com/Effect-TS/effect-smol/commit/476aede69c6efa06b5781ca5eb3e3b128ca29141), [`4f79c54`](https://github.com/Effect-TS/effect-smol/commit/4f79c542e7b508c235ff485d862cc8b29a8260c5), [`4be6a7c`](https://github.com/Effect-TS/effect-smol/commit/4be6a7cf35dab2a01d652f56dd35f0358c5a7e88), [`88927eb`](https://github.com/Effect-TS/effect-smol/commit/88927ebb896162cdba103b36553280b58e0facac)]: + - effect@4.0.0-beta.53 + - @effect/platform-node-shared@4.0.0-beta.53 + +## 4.0.0-beta.52 + +### Patch Changes + +- Updated dependencies [[`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`cf3a311`](https://github.com/Effect-TS/effect-smol/commit/cf3a311d863a8abb818840c3b80f847e621c43c1), [`8e04bfc`](https://github.com/Effect-TS/effect-smol/commit/8e04bfc95554b74eac205d67a20388e056b21499), [`131fdd5`](https://github.com/Effect-TS/effect-smol/commit/131fdd5b1f26531e265fe1a08f002002f47c276e)]: + - effect@4.0.0-beta.52 + - @effect/platform-node-shared@4.0.0-beta.52 + +## 4.0.0-beta.51 + +### Patch Changes + +- Updated dependencies [[`778d2af`](https://github.com/Effect-TS/effect-smol/commit/778d2afe9b5154bc1f9abae46d93ea7e54c87344), [`4e24dcf`](https://github.com/Effect-TS/effect-smol/commit/4e24dcf75037f65eebc1eb68623bc7cbf9d5512a), [`4b1c015`](https://github.com/Effect-TS/effect-smol/commit/4b1c0150e9bdb5559ed32d250deb66e17b4240c7), [`454f8ad`](https://github.com/Effect-TS/effect-smol/commit/454f8adad822929c3ef60f8280d0987226b049fd), [`6754a0c`](https://github.com/Effect-TS/effect-smol/commit/6754a0cd18626b06805a079cc5265525a5eb7d27), [`90f7fd5`](https://github.com/Effect-TS/effect-smol/commit/90f7fd5243871b30980964135db4512b8119fa82), [`d7e1519`](https://github.com/Effect-TS/effect-smol/commit/d7e151974934201fd93fa4c8a1192ee9a5d965a0), [`72a8122`](https://github.com/Effect-TS/effect-smol/commit/72a81228e09782bae512f7d041bbfbc78bc668d0)]: + - effect@4.0.0-beta.51 + - @effect/platform-node-shared@4.0.0-beta.51 + +## 4.0.0-beta.50 + +### Patch Changes + +- Updated dependencies [[`07be594`](https://github.com/Effect-TS/effect-smol/commit/07be594825de60f8e1b2102d21dbb9b8fc63b414), [`ae02433`](https://github.com/Effect-TS/effect-smol/commit/ae02433103ce28f53a0c9bfb4a44e75773289b7b)]: + - effect@4.0.0-beta.50 + - @effect/platform-node-shared@4.0.0-beta.50 + +## 4.0.0-beta.49 + +### Patch Changes + +- Updated dependencies [[`7d87873`](https://github.com/Effect-TS/effect-smol/commit/7d8787340ff549370f6f2a88b612e9ebbfd6ba45), [`c2f6f90`](https://github.com/Effect-TS/effect-smol/commit/c2f6f901b200a6e515b4f02c93ce8005b7bbf1c5), [`216f13c`](https://github.com/Effect-TS/effect-smol/commit/216f13c1fce454a21b489bb915714a17e791a1ac)]: + - effect@4.0.0-beta.49 + - @effect/platform-node-shared@4.0.0-beta.49 + +## 4.0.0-beta.48 + +### Patch Changes + +- Updated dependencies [[`4da56ec`](https://github.com/Effect-TS/effect-smol/commit/4da56ecff129b2da40137ffede23a73cc4e532d8), [`a5e6f77`](https://github.com/Effect-TS/effect-smol/commit/a5e6f774bab195cf50ecdc818240765f69a3bf4a), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070), [`f1ba5b8`](https://github.com/Effect-TS/effect-smol/commit/f1ba5b8584d325a541156928cecf041b37fd5070)]: + - effect@4.0.0-beta.48 + - @effect/platform-node-shared@4.0.0-beta.48 + +## 4.0.0-beta.47 + +### Patch Changes + +- Updated dependencies [[`c584726`](https://github.com/Effect-TS/effect-smol/commit/c58472674e750e6938df955044eab88feda95e45), [`86a91a4`](https://github.com/Effect-TS/effect-smol/commit/86a91a4f0c59286dfa9393232d8020dea70ed4db), [`131caf9`](https://github.com/Effect-TS/effect-smol/commit/131caf9525151a0cb29803a8f1dffa0f4f479d12), [`c3615c8`](https://github.com/Effect-TS/effect-smol/commit/c3615c88379b9daf252df0db72c6ac5a20326406)]: + - effect@4.0.0-beta.47 + - @effect/platform-node-shared@4.0.0-beta.47 + +## 4.0.0-beta.46 + +### Patch Changes + +- Updated dependencies [[`a48d439`](https://github.com/Effect-TS/effect-smol/commit/a48d439efad075728bbaee45b77a8ab41f552505), [`3a30b9e`](https://github.com/Effect-TS/effect-smol/commit/3a30b9e2ec2bd8b8193e1aa139f6878a07e3f5ee)]: + - @effect/platform-node-shared@4.0.0-beta.46 + - effect@4.0.0-beta.46 + +## 4.0.0-beta.45 + +### Patch Changes + +- Updated dependencies [[`5c3af6d`](https://github.com/Effect-TS/effect-smol/commit/5c3af6d554f60be34f8fc21d598d9a298ae11beb)]: + - effect@4.0.0-beta.45 + - @effect/platform-node-shared@4.0.0-beta.45 + +## 4.0.0-beta.44 + +### Patch Changes + +- [#1960](https://github.com/Effect-TS/effect-smol/pull/1960) [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf) Thanks @IMax153! - Add `ChildProcessHandle.unref`, returning an `Effect` that restores the child process reference when run. + +- [#1961](https://github.com/Effect-TS/effect-smol/pull/1961) [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970) Thanks @IMax153! - Rename the `ServiceMap` module to `Context` across exports, docs, and tests. + +- Updated dependencies [[`e3f0621`](https://github.com/Effect-TS/effect-smol/commit/e3f0621454c3f5d11070d30619da27c9232cadc1), [`5b476ab`](https://github.com/Effect-TS/effect-smol/commit/5b476abc0bd7e9bb59135ea1bcad2e4936227ced), [`6b40e5a`](https://github.com/Effect-TS/effect-smol/commit/6b40e5a4a6bd2087c15a3d7374d25057fdedfa16), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`3b09fb3`](https://github.com/Effect-TS/effect-smol/commit/3b09fb31c40c2802b01f21c23bcdd1fe7fb0aa82), [`2370410`](https://github.com/Effect-TS/effect-smol/commit/237041062e5af4594d32db91597e34e70a632877), [`dabc272`](https://github.com/Effect-TS/effect-smol/commit/dabc272444a700eb629c07ba3e77671a841ca86e), [`08b63c3`](https://github.com/Effect-TS/effect-smol/commit/08b63c3df11bd35c9fd6090dbd166287fdc40664), [`dfff04c`](https://github.com/Effect-TS/effect-smol/commit/dfff04c4c2b1d352dfad83992a6dce1280c85cf9), [`9baed9e`](https://github.com/Effect-TS/effect-smol/commit/9baed9e17e84702e6e480fcef6f86404f9e24be9), [`7846792`](https://github.com/Effect-TS/effect-smol/commit/7846792adc7e1631d62d26d657bd7ba6139f369b), [`1556a24`](https://github.com/Effect-TS/effect-smol/commit/1556a247623636b7ebe438fb56d77f1a7bf957bb), [`7c11bc2`](https://github.com/Effect-TS/effect-smol/commit/7c11bc292ab8e46252fe8f7576fb685917bfb8b5), [`b5ea591`](https://github.com/Effect-TS/effect-smol/commit/b5ea5913ec1d45d0dd12a327b9dd966bda2f6d02), [`0853afa`](https://github.com/Effect-TS/effect-smol/commit/0853afaeb1633b2d7f8b66893bd01c3aa1ef2c22), [`ac845f3`](https://github.com/Effect-TS/effect-smol/commit/ac845f3ab40e0b8719576e7f9bc16ea2e0e02cd4), [`b80c462`](https://github.com/Effect-TS/effect-smol/commit/b80c46247480f47bb64fc480fab48a3f37bc8888), [`b3f535d`](https://github.com/Effect-TS/effect-smol/commit/b3f535d9a7ac13b5fb984c29f93561c57a081ff0), [`6fe2e93`](https://github.com/Effect-TS/effect-smol/commit/6fe2e93cc2f1b173ef89651d74b6a5d2626b3226), [`cda8004`](https://github.com/Effect-TS/effect-smol/commit/cda800451c1ffbdddfc08415aed7b2d91e0412ee), [`8335477`](https://github.com/Effect-TS/effect-smol/commit/8335477a8a936a24b5f3ee6203c1b268bd1bfc3c), [`8c836f9`](https://github.com/Effect-TS/effect-smol/commit/8c836f99ab1e896b9580a71d67773625baff2eaf), [`718ff6f`](https://github.com/Effect-TS/effect-smol/commit/718ff6fe3e3d3820cefd67d2bff1b2224fe08060), [`7eed84f`](https://github.com/Effect-TS/effect-smol/commit/7eed84fc33c5781a6fb11bf4fd189d424902ebd4), [`5df46fe`](https://github.com/Effect-TS/effect-smol/commit/5df46fe2f654d59ab5fc1578f4fc27fa40368ef9), [`82dd0f2`](https://github.com/Effect-TS/effect-smol/commit/82dd0f26c6442b07143762ef7bc33742d3978dd6), [`03ae41e`](https://github.com/Effect-TS/effect-smol/commit/03ae41e7304cffac9f18feea22b73468feafc43a), [`4677a0a`](https://github.com/Effect-TS/effect-smol/commit/4677a0a58f95eea38a211efcd3f345f237a9e44a), [`87e1fc8`](https://github.com/Effect-TS/effect-smol/commit/87e1fc8b67e4901d75f567b2fecc3841ab762cc4), [`c1af1b7`](https://github.com/Effect-TS/effect-smol/commit/c1af1b756f63291e9c0298cf95c98a6920a0c2a0), [`7bb5dce`](https://github.com/Effect-TS/effect-smol/commit/7bb5dce60e1d904ef049a0287dec2b2e6113c970), [`c8a877b`](https://github.com/Effect-TS/effect-smol/commit/c8a877b53e8f29616335719e5dd1c3992dddf780), [`7da961a`](https://github.com/Effect-TS/effect-smol/commit/7da961ae4916229d2246699a5d3b20e5b2dd2020)]: + - effect@4.0.0-beta.44 + - @effect/platform-node-shared@4.0.0-beta.44 + +## 4.0.0-beta.43 + +### Patch Changes + +- Updated dependencies [[`2ae33d0`](https://github.com/Effect-TS/effect-smol/commit/2ae33d050914915f7cb9c25ab0a020901e08d596), [`979811a`](https://github.com/Effect-TS/effect-smol/commit/979811a4c3f7ed21ed18ef560c49fb7f5569e80e), [`eb7dbef`](https://github.com/Effect-TS/effect-smol/commit/eb7dbeffa883386ad912815e62c0820cac1fdf8e), [`cf50eb4`](https://github.com/Effect-TS/effect-smol/commit/cf50eb49cb04706dae5185f624708117c413dee8), [`1d046fe`](https://github.com/Effect-TS/effect-smol/commit/1d046fe484560e23f3e22cb23eec6433f8f1fa02)]: + - effect@4.0.0-beta.43 + - @effect/platform-node-shared@4.0.0-beta.43 + +## 4.0.0-beta.42 + +### Patch Changes + +- Updated dependencies [[`924e216`](https://github.com/Effect-TS/effect-smol/commit/924e216caa7e0bbf22e994a0cd2ce8b1f0f0b3ee), [`80e7f0c`](https://github.com/Effect-TS/effect-smol/commit/80e7f0cd9116e811e97b0ce30a77a8d1ecd072aa), [`f8328bf`](https://github.com/Effect-TS/effect-smol/commit/f8328bf0314da3dc7f31d314f94a5840e8d5217f), [`66d1c06`](https://github.com/Effect-TS/effect-smol/commit/66d1c06039079129707a230f7ad8c676439d7133), [`bee800b`](https://github.com/Effect-TS/effect-smol/commit/bee800bf285192a01bec72a7b7b51bc1159434e6), [`8930441`](https://github.com/Effect-TS/effect-smol/commit/8930441dee6f94c59c583d18d3ebd677cf1f2623)]: + - effect@4.0.0-beta.42 + - @effect/platform-node-shared@4.0.0-beta.42 + +## 4.0.0-beta.41 + +### Patch Changes + +- Updated dependencies [[`36f5c21`](https://github.com/Effect-TS/effect-smol/commit/36f5c2174d31ab42c4598bf81f178f40d0802283), [`d8ce758`](https://github.com/Effect-TS/effect-smol/commit/d8ce758669d6297ae932ac3251d83e7b49b22f30), [`11aab4c`](https://github.com/Effect-TS/effect-smol/commit/11aab4c6d37d5691adafc2d33da1a631b28ce814), [`3bc1efb`](https://github.com/Effect-TS/effect-smol/commit/3bc1efb53dd75b4a40de46f1f80c7f8a7d50af86), [`70e724e`](https://github.com/Effect-TS/effect-smol/commit/70e724e604604d4be1061cd8da0d360494998c84), [`738dee7`](https://github.com/Effect-TS/effect-smol/commit/738dee7edfd70af82dc4d2376db3a8ebe603eb48), [`2111963`](https://github.com/Effect-TS/effect-smol/commit/2111963f19b4c28c800664a8fac9590c1321885f), [`198a553`](https://github.com/Effect-TS/effect-smol/commit/198a553d9ce45f6a00bfc4d65ed0640669602d95)]: + - effect@4.0.0-beta.41 + - @effect/platform-node-shared@4.0.0-beta.41 + +## 4.0.0-beta.40 + +### Patch Changes + +- Updated dependencies [[`f62860f`](https://github.com/Effect-TS/effect-smol/commit/f62860f0e5e45978fabf7256ae620a13152a772a), [`973f281`](https://github.com/Effect-TS/effect-smol/commit/973f2812529aadc1cc54598b2039799fa72b80f8)]: + - effect@4.0.0-beta.40 + - @effect/platform-node-shared@4.0.0-beta.40 + +## 4.0.0-beta.39 + +### Patch Changes + +- Updated dependencies [[`f91fd3d`](https://github.com/Effect-TS/effect-smol/commit/f91fd3db39fe5628439fd175fba201a65a1aa9d0), [`edaae9d`](https://github.com/Effect-TS/effect-smol/commit/edaae9d65f464f941d7eddd723cd33d324f4b071), [`b47db0b`](https://github.com/Effect-TS/effect-smol/commit/b47db0bd5802064b6a24b3ea27c6ff2e0520d513), [`82d3c8e`](https://github.com/Effect-TS/effect-smol/commit/82d3c8e4f3f49b00df611b25aa6f8f74ec21b59b), [`7c22b31`](https://github.com/Effect-TS/effect-smol/commit/7c22b315d198dcbf44ae8cdb8b37879e1c9e3996)]: + - effect@4.0.0-beta.39 + - @effect/platform-node-shared@4.0.0-beta.39 + +## 4.0.0-beta.38 + +### Patch Changes + +- Updated dependencies [[`f4dbe5b`](https://github.com/Effect-TS/effect-smol/commit/f4dbe5b26b9c2d33fae024bf44afbdf8541792cd), [`a71a607`](https://github.com/Effect-TS/effect-smol/commit/a71a607c89fb6669a12a562c2c23be81dfbe1adb), [`66a0494`](https://github.com/Effect-TS/effect-smol/commit/66a0494ed75cd12f2721dcbb1d8a072e3d9e14b6), [`5ef7218`](https://github.com/Effect-TS/effect-smol/commit/5ef7218fc559d57301fe929b8a0cab4033f4f1fd), [`472d260`](https://github.com/Effect-TS/effect-smol/commit/472d260655bc311fba5c2c6e23bb77d8f7e36ba0)]: + - effect@4.0.0-beta.38 + - @effect/platform-node-shared@4.0.0-beta.38 + +## 4.0.0-beta.37 + +### Patch Changes + +- Updated dependencies [[`f7a0b71`](https://github.com/Effect-TS/effect-smol/commit/f7a0b711da8fdd645597dee29cacc5619c6afcf2), [`1e223c3`](https://github.com/Effect-TS/effect-smol/commit/1e223c30ccf835dfbb21284535d78549efaeca80), [`53740f4`](https://github.com/Effect-TS/effect-smol/commit/53740f47aa76d114b7d535649fb50efc54a09608), [`8c7cf89`](https://github.com/Effect-TS/effect-smol/commit/8c7cf89f719e580cbce1bf6c24e6996f1992a0a6), [`b6b81a9`](https://github.com/Effect-TS/effect-smol/commit/b6b81a940eaafcbc792d25413d6c02c707de31b2), [`8f4c1f9`](https://github.com/Effect-TS/effect-smol/commit/8f4c1f97ed60f8810b0b327b50117ffb2d8260d4), [`f2479f9`](https://github.com/Effect-TS/effect-smol/commit/f2479f9d3113b1f012db17a3852b4e28f478cf9c), [`c919921`](https://github.com/Effect-TS/effect-smol/commit/c9199217fad65529421d2cf95ecfff41257090fd), [`7af90c2`](https://github.com/Effect-TS/effect-smol/commit/7af90c2e3c99038eafa39650433839523790e2fe), [`f3be185`](https://github.com/Effect-TS/effect-smol/commit/f3be18569e5ca57c25eabf00df3ca601ebab43c7)]: + - effect@4.0.0-beta.37 + - @effect/platform-node-shared@4.0.0-beta.37 + +## 4.0.0-beta.36 + +### Patch Changes + +- Updated dependencies [[`60fcbcc`](https://github.com/Effect-TS/effect-smol/commit/60fcbcc43d09471e8f7e0969955d99dcefc5be81), [`0a60837`](https://github.com/Effect-TS/effect-smol/commit/0a6083713124440e630030375bab367e8d7df24e), [`49164d2`](https://github.com/Effect-TS/effect-smol/commit/49164d2c20a8d21b66514992c4a15d8521f6b36e), [`334b6e4`](https://github.com/Effect-TS/effect-smol/commit/334b6e4f76fe11941b516d61f57e268bc31f0ca6), [`5700695`](https://github.com/Effect-TS/effect-smol/commit/5700695f76ae6da6b94c9c87d4dd2b8054fb829b), [`f8f4456`](https://github.com/Effect-TS/effect-smol/commit/f8f445644f3aa7ec093cab7445198a62ba18a480), [`969d24f`](https://github.com/Effect-TS/effect-smol/commit/969d24fdfa48c4838e811983848d9cb4e9b3b12c), [`851eda0`](https://github.com/Effect-TS/effect-smol/commit/851eda0533946e39bacaaf581896320d7a4f3e8c), [`8059c1c`](https://github.com/Effect-TS/effect-smol/commit/8059c1c3eba9a90af7cd889ea261bcb8fff0c185), [`6f83295`](https://github.com/Effect-TS/effect-smol/commit/6f8329546a73eaddc7cb5e85ea8e37e73fbfb611), [`65f7f57`](https://github.com/Effect-TS/effect-smol/commit/65f7f5737575fed668987462c96d29a446707c32), [`e7fabd2`](https://github.com/Effect-TS/effect-smol/commit/e7fabd2265db690eae5cfc9b83730c84699aef61), [`89c3e98`](https://github.com/Effect-TS/effect-smol/commit/89c3e985401eb38f33a3ae21a94ad27de3c1d28b), [`53794ab`](https://github.com/Effect-TS/effect-smol/commit/53794ab7af30aa5c5004ecf53659fafbe4b10542)]: + - effect@4.0.0-beta.36 + - @effect/platform-node-shared@4.0.0-beta.36 + +## 4.0.0-beta.35 + +### Patch Changes + +- [#1779](https://github.com/Effect-TS/effect-smol/pull/1779) [`3015c2d`](https://github.com/Effect-TS/effect-smol/commit/3015c2dc25fb44694978b4ff921af9b24178fcc0) Thanks @aeterno-caspian! - bump undici versions + +- Updated dependencies [[`9252b43`](https://github.com/Effect-TS/effect-smol/commit/9252b43560f507709c2985abcf52a7837b23ddf8), [`7daf387`](https://github.com/Effect-TS/effect-smol/commit/7daf3870a656882a488a60f67881e6808c8f4d04), [`e1664a3`](https://github.com/Effect-TS/effect-smol/commit/e1664a38bc31ef4ceb4e9324c7226e1e99bf9c07), [`fdaa6e0`](https://github.com/Effect-TS/effect-smol/commit/fdaa6e0a41b6b6605438fa8557441792135380a2), [`19aa47e`](https://github.com/Effect-TS/effect-smol/commit/19aa47ef7b470e427620edca8970dd9cdd551216), [`c667dad`](https://github.com/Effect-TS/effect-smol/commit/c667dad07777b860e4764a3ba9a6cc41c236cd98), [`764d150`](https://github.com/Effect-TS/effect-smol/commit/764d1501bc5026b60fc8aef6cb02a5a87c762801), [`3c27098`](https://github.com/Effect-TS/effect-smol/commit/3c27098b5685a63db2c2eff654a250c94d3fcfa7)]: + - effect@4.0.0-beta.35 + - @effect/platform-node-shared@4.0.0-beta.35 + +## 4.0.0-beta.34 + +### Patch Changes + +- Updated dependencies [[`f2f75ee`](https://github.com/Effect-TS/effect-smol/commit/f2f75ee564bce1cd95f5189c7bdeeed4f92dacb1), [`342fc4b`](https://github.com/Effect-TS/effect-smol/commit/342fc4b051739e32e7977159f26ff9541eda664f), [`5d704ee`](https://github.com/Effect-TS/effect-smol/commit/5d704ee10d20e8eb107e34bb8a21feb5aa4a7685), [`00add69`](https://github.com/Effect-TS/effect-smol/commit/00add69b59551e9df34772eb927638b093f6d71e), [`58217d3`](https://github.com/Effect-TS/effect-smol/commit/58217d318a7d716ccd707cce0f41573946939c28), [`f4e2aba`](https://github.com/Effect-TS/effect-smol/commit/f4e2aba01b76d1e3059b297e3cc942284dfeafb2), [`e3b44b6`](https://github.com/Effect-TS/effect-smol/commit/e3b44b6a2af9ee21dc5c1e928f0c20af857fa7a9), [`e1472b7`](https://github.com/Effect-TS/effect-smol/commit/e1472b7525c5d57a48bdec2353c3b742f7f916c0), [`7686320`](https://github.com/Effect-TS/effect-smol/commit/7686320cd123fa352b5c3d076fb18a3cac0a9bba)]: + - effect@4.0.0-beta.34 + - @effect/platform-node-shared@4.0.0-beta.34 + +## 4.0.0-beta.33 + +### Patch Changes + +- Updated dependencies [[`571447d`](https://github.com/Effect-TS/effect-smol/commit/571447da67334449f8ae3d6ecb3d77ea4e0c4295)]: + - effect@4.0.0-beta.33 + - @effect/platform-node-shared@4.0.0-beta.33 + +## 4.0.0-beta.32 + +### Patch Changes + +- Updated dependencies [[`bf8fff8`](https://github.com/Effect-TS/effect-smol/commit/bf8fff8a5f54b6df74cb7bbb42346fe9ba52435a), [`1af3ef3`](https://github.com/Effect-TS/effect-smol/commit/1af3ef3e3ca7fd417d0fc15f8ca8fe207eba4f74), [`27fea0f`](https://github.com/Effect-TS/effect-smol/commit/27fea0f66910de5905f40fd63f8ddbb6f7ac5aba), [`2ad6c1b`](https://github.com/Effect-TS/effect-smol/commit/2ad6c1b2c85a3a0fe351e3d56636a75eb76b4b4e), [`398ac3e`](https://github.com/Effect-TS/effect-smol/commit/398ac3e01cb75efce0e4e2913d1450cf65866732), [`51fe22f`](https://github.com/Effect-TS/effect-smol/commit/51fe22f3266e417b6c541aaed4b75d246fac91e7), [`4605db6`](https://github.com/Effect-TS/effect-smol/commit/4605db69cfacddbdbf1525865ddfde135158090c), [`f4de1b0`](https://github.com/Effect-TS/effect-smol/commit/f4de1b087c998d0bad1d9468f70b7d16c13b9f6f), [`60214f2`](https://github.com/Effect-TS/effect-smol/commit/60214f2080b2aeb091f691140eb20acb741691c3), [`c4b8b0f`](https://github.com/Effect-TS/effect-smol/commit/c4b8b0ffa8efb47c4cd7578a8943d6868509373f), [`6d9393a`](https://github.com/Effect-TS/effect-smol/commit/6d9393a0770a18722d23340e77f15455de341245), [`6de4efe`](https://github.com/Effect-TS/effect-smol/commit/6de4efe463c783614ceb0c094d77a336a899cbe0), [`4f969d1`](https://github.com/Effect-TS/effect-smol/commit/4f969d1563ba755ffa116c8ae409bb3436bd881d), [`6cc67c8`](https://github.com/Effect-TS/effect-smol/commit/6cc67c855e054ee3f3ac3485dca5f7805e79e8fb), [`8531a22`](https://github.com/Effect-TS/effect-smol/commit/8531a22ffbb52e11a030b09f358cafbfdf5edff7), [`b226760`](https://github.com/Effect-TS/effect-smol/commit/b22676067617f15c00722a3a63fd7c2c172c3d45), [`47a51ab`](https://github.com/Effect-TS/effect-smol/commit/47a51aba0ecdf3ef478bfa28a498bca188399bd4), [`1521d02`](https://github.com/Effect-TS/effect-smol/commit/1521d02e1f19f1d795edaaf862c1a1031d9c755e)]: + - effect@4.0.0-beta.32 + - @effect/platform-node-shared@4.0.0-beta.32 + +## 4.0.0-beta.31 + +### Patch Changes + +- [#1710](https://github.com/Effect-TS/effect-smol/pull/1710) [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423) Thanks @gcanti! - Schema: `toCodecJson` now returns `Codec` instead of `Codec`. + + Http: the `json` property on `HttpIncomingMessage`, `HttpClientResponse`, `HttpServerRequest`, and `HttpServerResponse` now returns `Effect` instead of `Effect`. + +- Updated dependencies [[`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6f23f0e`](https://github.com/Effect-TS/effect-smol/commit/6f23f0ed4cba573cd9395c2e582f582fe7271544), [`654aaec`](https://github.com/Effect-TS/effect-smol/commit/654aaec593305521b65dd042c204d761cc6e8c28), [`2958a42`](https://github.com/Effect-TS/effect-smol/commit/2958a42078966a8713a98f00485ab36484d5eccf), [`95d27a2`](https://github.com/Effect-TS/effect-smol/commit/95d27a239ed5147302605ab0b3147a056541b0c7), [`0fbaea8`](https://github.com/Effect-TS/effect-smol/commit/0fbaea8f9555a8044cec31a770394db613fc78e2), [`21d5d5e`](https://github.com/Effect-TS/effect-smol/commit/21d5d5e0439fd4d9bb6e508377215b1087555d45), [`5a84853`](https://github.com/Effect-TS/effect-smol/commit/5a8485397b7f321ae021640c1999821143659462), [`6e49959`](https://github.com/Effect-TS/effect-smol/commit/6e499590357a104c81779b3176cd3f84e4f91064), [`8f5805d`](https://github.com/Effect-TS/effect-smol/commit/8f5805dbdd0d1bc0ff0727cc398c8d80e544edee), [`990df2c`](https://github.com/Effect-TS/effect-smol/commit/990df2c3ceeb32e659acc10cc9485617f7b3c423)]: + - effect@4.0.0-beta.31 + - @effect/platform-node-shared@4.0.0-beta.31 + +## 4.0.0-beta.30 + +### Patch Changes + +- Updated dependencies [[`c88e5b7`](https://github.com/Effect-TS/effect-smol/commit/c88e5b723ff09da4edaef6ce14d927ca01104a32), [`947d0e4`](https://github.com/Effect-TS/effect-smol/commit/947d0e4268ba5c4020ead380aa80812c7342408f), [`7517908`](https://github.com/Effect-TS/effect-smol/commit/75179085d159b88a1ab0bce70669d76dcf0d79a4), [`a49ecd5`](https://github.com/Effect-TS/effect-smol/commit/a49ecd5a183d7e7d33f47ff95e9d2dea5a12ead5), [`6993e33`](https://github.com/Effect-TS/effect-smol/commit/6993e3329122c834c20bacea72d8678232f4f103), [`514f2a2`](https://github.com/Effect-TS/effect-smol/commit/514f2a2ae54580fcacdbe2ea2196a83a852d0748), [`3214b47`](https://github.com/Effect-TS/effect-smol/commit/3214b47676de2d33fddc5fecfc2d226e6e83cc7b), [`95ec5ed`](https://github.com/Effect-TS/effect-smol/commit/95ec5ed345de77c893049e182d37a37cf164a268)]: + - effect@4.0.0-beta.30 + - @effect/platform-node-shared@4.0.0-beta.30 + +## 4.0.0-beta.29 + +### Patch Changes + +- Updated dependencies [[`9d93adb`](https://github.com/Effect-TS/effect-smol/commit/9d93adb1c1795d1978391b30d7d2972c88052662), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`b52721c`](https://github.com/Effect-TS/effect-smol/commit/b52721cf0d11a567722b060c8536e3bdd4161f07), [`a891c7b`](https://github.com/Effect-TS/effect-smol/commit/a891c7b12f415b2287613dd4b91a09dfd38ef30d), [`ef26cdf`](https://github.com/Effect-TS/effect-smol/commit/ef26cdfb65d9955fc7e161629191930c2cc2c63f), [`174a88d`](https://github.com/Effect-TS/effect-smol/commit/174a88d37b2c1fd52af2340c3b19c8272bccc8d0), [`82fd3ed`](https://github.com/Effect-TS/effect-smol/commit/82fd3ed922063ee5a34f96f3993c15c7515e4f67)]: + - effect@4.0.0-beta.29 + - @effect/platform-node-shared@4.0.0-beta.29 + +## 4.0.0-beta.28 + +### Patch Changes + +- Updated dependencies [[`ff533f2`](https://github.com/Effect-TS/effect-smol/commit/ff533f203cd06302ad08032a27e01269b4a2d4c6), [`dc803ee`](https://github.com/Effect-TS/effect-smol/commit/dc803ee52ebd3e9f931118f0dfcb804542847556), [`d660b1c`](https://github.com/Effect-TS/effect-smol/commit/d660b1c99cb93d4f79715e91c7a4486801c0eefa), [`93a05e3`](https://github.com/Effect-TS/effect-smol/commit/93a05e3eaa624058b162aedd66aad70102837270), [`2a65cf6`](https://github.com/Effect-TS/effect-smol/commit/2a65cf6fd81ef63d944e6fb51f058d439bf4a834), [`a561a40`](https://github.com/Effect-TS/effect-smol/commit/a561a40cc41c548c2cf3153aca065ee92ee8aa57), [`29cd24d`](https://github.com/Effect-TS/effect-smol/commit/29cd24d1fe78480a72eeb38a90281ffddc0530bc), [`662a8e6`](https://github.com/Effect-TS/effect-smol/commit/662a8e6857dac64a7cd13bd8df4b0674654622f8), [`d2b52ba`](https://github.com/Effect-TS/effect-smol/commit/d2b52bae5b9336cf59729fbdcc4d7f09512b0cbf), [`407c3b4`](https://github.com/Effect-TS/effect-smol/commit/407c3b43a5d1414558e0e33b6f1fc0e6a6d489cc), [`42bc7ce`](https://github.com/Effect-TS/effect-smol/commit/42bc7ce5480f6f2953c39f8cb5c850d61df6f5a2), [`e741322`](https://github.com/Effect-TS/effect-smol/commit/e74132226cbfee24234311c7c1c13e6b7391384e), [`5c75fa8`](https://github.com/Effect-TS/effect-smol/commit/5c75fa8fb71163bc4c035ba1a215574dfd4badfc), [`747177b`](https://github.com/Effect-TS/effect-smol/commit/747177b0602f12d4461a843e953dfdffbeb0a429), [`326cd48`](https://github.com/Effect-TS/effect-smol/commit/326cd4828bce573fe985f35152155464bf4c5a70), [`627e922`](https://github.com/Effect-TS/effect-smol/commit/627e922b8d1e9521eae5e1caa5d667ad00b1619a), [`662287e`](https://github.com/Effect-TS/effect-smol/commit/662287e9abc76c941ccc2ee330aa07904d571341)]: + - effect@4.0.0-beta.28 + - @effect/platform-node-shared@4.0.0-beta.28 + +## 4.0.0-beta.27 + +### Patch Changes + +- Updated dependencies [[`903a839`](https://github.com/Effect-TS/effect-smol/commit/903a839e94239e6ec4568315af28e405bcad95f4), [`91a0168`](https://github.com/Effect-TS/effect-smol/commit/91a016836680a6669308ecf464d3584bcc4ae1b7), [`c890f9a`](https://github.com/Effect-TS/effect-smol/commit/c890f9a1b3a989ed22528bd5a43326342e05b142), [`1e985f2`](https://github.com/Effect-TS/effect-smol/commit/1e985f237d250b51b91de22dde77160c1e778ce7)]: + - effect@4.0.0-beta.27 + - @effect/platform-node-shared@4.0.0-beta.27 + +## 4.0.0-beta.26 + +### Patch Changes + +- Updated dependencies [[`fb21462`](https://github.com/Effect-TS/effect-smol/commit/fb21462642cdd5b1bada92f3eba18ae20445be42), [`2ed26b1`](https://github.com/Effect-TS/effect-smol/commit/2ed26b139805700e3df39efaa768ff01565e5c86), [`e832a57`](https://github.com/Effect-TS/effect-smol/commit/e832a57b570fe38f010c1fd99bceac5a325a9e07), [`7f01be7`](https://github.com/Effect-TS/effect-smol/commit/7f01be7f8db363d4b2e88e6b5571e96bb815786f), [`e965143`](https://github.com/Effect-TS/effect-smol/commit/e9651431e114479e6becf8ca7b1ed99ac7e91ccc), [`b9b80f1`](https://github.com/Effect-TS/effect-smol/commit/b9b80f1f15e152ceef0a727d150b7dc230abae99), [`98252aa`](https://github.com/Effect-TS/effect-smol/commit/98252aa0c0b17fc73fbdad65d0a1104965f9fc0f), [`56fbd94`](https://github.com/Effect-TS/effect-smol/commit/56fbd94311ad19a05001ad649d9e34ab00c74541), [`3faa109`](https://github.com/Effect-TS/effect-smol/commit/3faa109b7d093fbf14ad410d3e11d663f16e28f1), [`692ecfe`](https://github.com/Effect-TS/effect-smol/commit/692ecfed99fe58056b7a5afe001f4fcd1a61c446), [`1e70b72`](https://github.com/Effect-TS/effect-smol/commit/1e70b72d0b210474d0e96a15a5cfc279eae37e0c), [`ecf0782`](https://github.com/Effect-TS/effect-smol/commit/ecf07829ef2dfc01d8943c96c4fe9c1b44b97926)]: + - effect@4.0.0-beta.26 + - @effect/platform-node-shared@4.0.0-beta.26 + +## 4.0.0-beta.25 + +### Patch Changes + +- Updated dependencies [[`fa17bb5`](https://github.com/Effect-TS/effect-smol/commit/fa17bb5be9f2533d01e11322b14804c7dec43714), [`f46e5b5`](https://github.com/Effect-TS/effect-smol/commit/f46e5b5ca2a918ee4d9270167e79db223077c96f), [`ce4767c`](https://github.com/Effect-TS/effect-smol/commit/ce4767cadcacc6ce8ff4c3a0d0fbc82ede655f63), [`c830a8b`](https://github.com/Effect-TS/effect-smol/commit/c830a8b6c292a6528d7f9318759d34800b00372d)]: + - effect@4.0.0-beta.25 + - @effect/platform-node-shared@4.0.0-beta.25 + +## 4.0.0-beta.24 + +### Patch Changes + +- Updated dependencies [[`a909e1c`](https://github.com/Effect-TS/effect-smol/commit/a909e1c1ac2bc707527f5073776e3e7d239688d9), [`8814a4e`](https://github.com/Effect-TS/effect-smol/commit/8814a4ef78d67144d27689370af10099ea210399), [`3f942c5`](https://github.com/Effect-TS/effect-smol/commit/3f942c51cefa7b2ffa7c49e8c8a2c887570ba4c0), [`774ed59`](https://github.com/Effect-TS/effect-smol/commit/774ed59c52b2ab578bbb897c4f551f812231e1d2), [`f54b8d3`](https://github.com/Effect-TS/effect-smol/commit/f54b8d398fedad1815fd1f4c49814ab938cfc385)]: + - effect@4.0.0-beta.24 + - @effect/platform-node-shared@4.0.0-beta.24 + +## 4.0.0-beta.23 + +### Patch Changes + +- Updated dependencies [[`5c73c41`](https://github.com/Effect-TS/effect-smol/commit/5c73c41b69eaeab80fcd62c9bfda490b446d1966)]: + - effect@4.0.0-beta.23 + - @effect/platform-node-shared@4.0.0-beta.23 + +## 4.0.0-beta.22 + +### Patch Changes + +- Updated dependencies [[`0874332`](https://github.com/Effect-TS/effect-smol/commit/0874332f7c81118b06ac2eb105e0710211631479), [`c592dcd`](https://github.com/Effect-TS/effect-smol/commit/c592dcde0697e322065c8f418c0480ef910cb183), [`1dbe28d`](https://github.com/Effect-TS/effect-smol/commit/1dbe28dac8299cd3e218c9768450cfd173b5e294), [`564d730`](https://github.com/Effect-TS/effect-smol/commit/564d730b6bbf38dd8548a3b046e7a693b28699a4), [`3cfadc4`](https://github.com/Effect-TS/effect-smol/commit/3cfadc458b070c6cba6c5674b72a059f1e49118b), [`6634fd0`](https://github.com/Effect-TS/effect-smol/commit/6634fd07da067d80b8261fb2959d1a952b9e412e), [`d10dabe`](https://github.com/Effect-TS/effect-smol/commit/d10dabeb7af9a368f995829cd36ad08167cd8f95), [`f82f549`](https://github.com/Effect-TS/effect-smol/commit/f82f549a09e950e9d4987f279a800f4d953f0939), [`78a3382`](https://github.com/Effect-TS/effect-smol/commit/78a3382ddfbe034408f7480fa794733d9e82147b)]: + - effect@4.0.0-beta.22 + - @effect/platform-node-shared@4.0.0-beta.22 + +## 4.0.0-beta.21 + +### Patch Changes + +- Updated dependencies [[`e691909`](https://github.com/Effect-TS/effect-smol/commit/e691909495ccb162ea7bfa351dd74632b99997cb), [`d5f413f`](https://github.com/Effect-TS/effect-smol/commit/d5f413f3c8fc57f2413cc5649c2003d6d4e5a6d7), [`139d152`](https://github.com/Effect-TS/effect-smol/commit/139d152941e562a073b5be12e8d66c8a4d4a8a57), [`947e3d4`](https://github.com/Effect-TS/effect-smol/commit/947e3d436ab8a017efda9b29be523efd1ca8df28), [`84b2cce`](https://github.com/Effect-TS/effect-smol/commit/84b2ccefe2aa3a7413b86738a4dc33cdb311ca55), [`7f5305e`](https://github.com/Effect-TS/effect-smol/commit/7f5305e69f5a33309e77b08a576edb25d7daaee2), [`9e6fd84`](https://github.com/Effect-TS/effect-smol/commit/9e6fd8471c93a3c643929151a3bdb62cb9c0ca0e), [`fdb8a4b`](https://github.com/Effect-TS/effect-smol/commit/fdb8a4b172721fbefe98bd5aa6fe4f0efd1da3eb), [`0f986ef`](https://github.com/Effect-TS/effect-smol/commit/0f986ef22f196fe091a7afdbd179485a7d888882), [`9355fc0`](https://github.com/Effect-TS/effect-smol/commit/9355fc0ffb5b7382146a5aed9eea83974b10d007)]: + - effect@4.0.0-beta.21 + - @effect/platform-node-shared@4.0.0-beta.21 + +## 4.0.0-beta.20 + +### Patch Changes + +- Updated dependencies [[`842a624`](https://github.com/Effect-TS/effect-smol/commit/842a624f79d5e1407460b0ef3ab27d14d48ccf74), [`4785eef`](https://github.com/Effect-TS/effect-smol/commit/4785eef5d7cf1edb96ef2509aed2ba4d1edf3862), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`12ee8e2`](https://github.com/Effect-TS/effect-smol/commit/12ee8e27df7eb393d83a5e403390d0cfc82ca732), [`e542c94`](https://github.com/Effect-TS/effect-smol/commit/e542c942bee4729138b02222f4421220a90a57d8), [`8fac95b`](https://github.com/Effect-TS/effect-smol/commit/8fac95bd9e0338b7a82da8da579c1ac22afa045c), [`6f4ebd1`](https://github.com/Effect-TS/effect-smol/commit/6f4ebd193c2595983394127dd808601b75430d34), [`989d1cc`](https://github.com/Effect-TS/effect-smol/commit/989d1cca936fce0cc459057825ba40e3f5ef3827)]: + - effect@4.0.0-beta.20 + - @effect/platform-node-shared@4.0.0-beta.20 + +## 4.0.0-beta.19 + +### Patch Changes + +- Updated dependencies [[`b9951d6`](https://github.com/Effect-TS/effect-smol/commit/b9951d6b178b4c2173112fa7454b6abd8030585f)]: + - @effect/platform-node-shared@4.0.0-beta.19 + - effect@4.0.0-beta.19 + +## 4.0.0-beta.18 + +### Patch Changes + +- Updated dependencies [[`01e31fd`](https://github.com/Effect-TS/effect-smol/commit/01e31fdf8e5206849d23cbafd23a346f2f177ab8), [`0890aab`](https://github.com/Effect-TS/effect-smol/commit/0890aab15ed9c5ba52c383a72fdc6a444d7504d5), [`725260b`](https://github.com/Effect-TS/effect-smol/commit/725260b53f5142d6af7a93a2f9f464f974eda92d)]: + - effect@4.0.0-beta.18 + - @effect/platform-node-shared@4.0.0-beta.18 + +## 4.0.0-beta.17 + +### Patch Changes + +- Updated dependencies [[`8f59c32`](https://github.com/Effect-TS/effect-smol/commit/8f59c32922597a48392744f7203e284866747781)]: + - effect@4.0.0-beta.17 + - @effect/platform-node-shared@4.0.0-beta.17 + +## 4.0.0-beta.16 + +### Patch Changes + +- Updated dependencies [[`bf9096c`](https://github.com/Effect-TS/effect-smol/commit/bf9096c52a7d8791d93d232739e523eb84f6625a), [`29f81ca`](https://github.com/Effect-TS/effect-smol/commit/29f81ca07c67dba265804b140a7487fb15a5fc6b), [`68eb28c`](https://github.com/Effect-TS/effect-smol/commit/68eb28c2b0fc67a9f6204ade9bd16c5b37803bfb)]: + - effect@4.0.0-beta.16 + - @effect/platform-node-shared@4.0.0-beta.16 + +## 4.0.0-beta.15 + +### Patch Changes + +- Updated dependencies [[`24ae609`](https://github.com/Effect-TS/effect-smol/commit/24ae60995d2fd7d621be356cdfdfd328c79639ba), [`0e3c059`](https://github.com/Effect-TS/effect-smol/commit/0e3c059987caa55ebd0c134f7c7b147c639c328e), [`e843b0a`](https://github.com/Effect-TS/effect-smol/commit/e843b0a7d7e7b600a0b3bd477f24e2e4cd26bc8b), [`f4389a2`](https://github.com/Effect-TS/effect-smol/commit/f4389a2cca3c5bbf00d69779f52ce41255f15a28), [`5b73de0`](https://github.com/Effect-TS/effect-smol/commit/5b73de095b3402d0c5c74092ace6ce18ebfad566), [`595d2d6`](https://github.com/Effect-TS/effect-smol/commit/595d2d6e7d50419f3532bd39266191532ace38f2)]: + - effect@4.0.0-beta.15 + - @effect/platform-node-shared@4.0.0-beta.15 + +## 4.0.0-beta.14 + +### Patch Changes + +- Updated dependencies [[`c414700`](https://github.com/Effect-TS/effect-smol/commit/c414700ef1932e4b67d0102856de417336912350), [`a30c969`](https://github.com/Effect-TS/effect-smol/commit/a30c9699c0d736cf3952041e45d508b7d58907a9)]: + - effect@4.0.0-beta.14 + - @effect/platform-node-shared@4.0.0-beta.14 + +## 4.0.0-beta.13 + +### Patch Changes + +- Updated dependencies [[`368f4c3`](https://github.com/Effect-TS/effect-smol/commit/368f4c363dd117e6f5a19ad77b161176cfd29fdd), [`db8a579`](https://github.com/Effect-TS/effect-smol/commit/db8a579e93e93ff73b1e60712732e03b597b916b), [`668b703`](https://github.com/Effect-TS/effect-smol/commit/668b70337e9ddbb0d1ae2282a95c282ce404e562), [`d40e76b`](https://github.com/Effect-TS/effect-smol/commit/d40e76b973543979e60e04a6baca04a8c65bdfc2), [`6e18cf8`](https://github.com/Effect-TS/effect-smol/commit/6e18cf883e9905ca718a6697b6a2a4bbd42739aa), [`86062e8`](https://github.com/Effect-TS/effect-smol/commit/86062e8a0c61bca5412fc40d2cf151d676901f08), [`c27ce75`](https://github.com/Effect-TS/effect-smol/commit/c27ce75d34c74dcfc6dba1bf77f1ce88f410a0de), [`e2d4fbf`](https://github.com/Effect-TS/effect-smol/commit/e2d4fbfeeda6a5d2a4c5aeb0501d8240c248b9eb), [`114ab42`](https://github.com/Effect-TS/effect-smol/commit/114ab42ad0edc590d29169675a493e0e915aa58f), [`484caec`](https://github.com/Effect-TS/effect-smol/commit/484caec47cccac8b86db2910742e406dfc7173ab)]: + - effect@4.0.0-beta.13 + - @effect/platform-node-shared@4.0.0-beta.13 + +## 4.0.0-beta.12 + +### Patch Changes + +- [#1450](https://github.com/Effect-TS/effect-smol/pull/1450) [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668) Thanks @tim-smart! - use cause annotations for detecting client aborts + +- Updated dependencies [[`70a74e8`](https://github.com/Effect-TS/effect-smol/commit/70a74e88a8767c9d4acdb9e5f25aec9a33588d07), [`b5b6e10`](https://github.com/Effect-TS/effect-smol/commit/b5b6e10621d54bf8c9857fec0d647ced78ecd857), [`f5ce5a9`](https://github.com/Effect-TS/effect-smol/commit/f5ce5a915359c6ebf254079e1da23cab6cde34fb), [`a29eb70`](https://github.com/Effect-TS/effect-smol/commit/a29eb702ffe3fc58bd28c4d7857298cd65d73668), [`c7b36e5`](https://github.com/Effect-TS/effect-smol/commit/c7b36e541a23e9a00f64e25b23851e51a37dfce5), [`9381d6d`](https://github.com/Effect-TS/effect-smol/commit/9381d6d4d9d819a81a46e56d0364c76e92a4fbca), [`88439f1`](https://github.com/Effect-TS/effect-smol/commit/88439f13ca13549f3e4822c48c4f019c14fc2bcc), [`e35307d`](https://github.com/Effect-TS/effect-smol/commit/e35307dbeb8eb26a9923f958b894a8eaaf259bf2), [`c7df4bc`](https://github.com/Effect-TS/effect-smol/commit/c7df4bce34009474c63d62a807abfdafb76971eb), [`accaf3b`](https://github.com/Effect-TS/effect-smol/commit/accaf3be7ac8da36e2334c509c23b8c9e88ea160), [`3e1c270`](https://github.com/Effect-TS/effect-smol/commit/3e1c2707bbdf67720af1509642b8ced195790882), [`6cd81f7`](https://github.com/Effect-TS/effect-smol/commit/6cd81f73baad86f5bbfa455a55d75cde71e9611a), [`f222da3`](https://github.com/Effect-TS/effect-smol/commit/f222da3cdb44554f3324c2c52d0d005ee575053e), [`61f901d`](https://github.com/Effect-TS/effect-smol/commit/61f901d830005b66e22d1de889fda132aeea97cd)]: + - effect@4.0.0-beta.12 + - @effect/platform-node-shared@4.0.0-beta.12 + +## 4.0.0-beta.11 + +### Patch Changes + +- Updated dependencies [[`88659ed`](https://github.com/Effect-TS/effect-smol/commit/88659edb26e3623d557dccfe914c2c949672da16), [`f2915e8`](https://github.com/Effect-TS/effect-smol/commit/f2915e8e2efe80d50c281e53f297b9701d6dc199), [`eb71ace`](https://github.com/Effect-TS/effect-smol/commit/eb71acebbe0f228e4920278013beee3b67d62310), [`2a16999`](https://github.com/Effect-TS/effect-smol/commit/2a169996c7513d377ac47adbfd68e1490457135c), [`d42dd52`](https://github.com/Effect-TS/effect-smol/commit/d42dd52f11203f8e749fb5d3ecf7153e4a5a6814), [`339adaf`](https://github.com/Effect-TS/effect-smol/commit/339adaf850a62a892adebcb208c2d9dddf3b97b3), [`de19645`](https://github.com/Effect-TS/effect-smol/commit/de1964526d01102dd1cb99c8cfdd3e8df1f49ef1), [`9b1dc3b`](https://github.com/Effect-TS/effect-smol/commit/9b1dc3bcf2a1b68d0a67e3465db5ad01a1a56997), [`e4cb2f5`](https://github.com/Effect-TS/effect-smol/commit/e4cb2f55b30f4771ec1bf613ced36d6d96464dd5), [`8bced95`](https://github.com/Effect-TS/effect-smol/commit/8bced954ecb35d4489197a57b0efe927e7d75f49), [`9431420`](https://github.com/Effect-TS/effect-smol/commit/94314207c8019918200fbcb97aec992219f801f0), [`948dca2`](https://github.com/Effect-TS/effect-smol/commit/948dca22e4f672ba7a6db57f9899272bec7c08b8), [`d18e327`](https://github.com/Effect-TS/effect-smol/commit/d18e32765a2665e31ffb31e746bf983fcfac34c5), [`ab512f7`](https://github.com/Effect-TS/effect-smol/commit/ab512f7be1c0e6b359da921e22cd4944e4c57d3e)]: + - effect@4.0.0-beta.11 + - @effect/platform-node-shared@4.0.0-beta.11 + +## 4.0.0-beta.10 + +### Patch Changes + +- Updated dependencies [[`371acab`](https://github.com/Effect-TS/effect-smol/commit/371acabb58d56f3a7a5e3e33d3d5fdc9f5573c74), [`856d774`](https://github.com/Effect-TS/effect-smol/commit/856d7741f1e296dd5048c6ff2b44b95d023e6ae4), [`b9e9202`](https://github.com/Effect-TS/effect-smol/commit/b9e92023c38caa322975d77cfe83e2d34ac9305a), [`1d1a974`](https://github.com/Effect-TS/effect-smol/commit/1d1a974bd280c81bff5d4505491cda03ba7a3f36), [`6bfe2a6`](https://github.com/Effect-TS/effect-smol/commit/6bfe2a659bc6335db75709931f405da45301cba2), [`b12c811`](https://github.com/Effect-TS/effect-smol/commit/b12c81157be287b1649c210616a244b50ec094d2), [`d17d98a`](https://github.com/Effect-TS/effect-smol/commit/d17d98ad78e2b44d95ef434adab79ac3c35e75ab), [`68c3c7c`](https://github.com/Effect-TS/effect-smol/commit/68c3c7cb1e06ed94fa5c4c123a234b4ccbfdecd8)]: + - effect@4.0.0-beta.10 + - @effect/platform-node-shared@4.0.0-beta.10 + +## 4.0.0-beta.9 + +### Patch Changes + +- Updated dependencies [[`3386557`](https://github.com/Effect-TS/effect-smol/commit/338655731564a7be9f8859dedbf4d5bcac6eb350), [`b6666e3`](https://github.com/Effect-TS/effect-smol/commit/b6666e3cf6bd44ba1a8704e65c256c30359cb422)]: + - effect@4.0.0-beta.9 + - @effect/platform-node-shared@4.0.0-beta.9 + +## 4.0.0-beta.8 + +### Patch Changes + +- Updated dependencies [[`246e672`](https://github.com/Effect-TS/effect-smol/commit/246e672dbbd7848d60e0c78fd66671b2f10b3752), [`807dec0`](https://github.com/Effect-TS/effect-smol/commit/807dec03801b4c58a6d00c237b6d98d6386911df)]: + - effect@4.0.0-beta.8 + - @effect/platform-node-shared@4.0.0-beta.8 + +## 4.0.0-beta.7 + +### Patch Changes + +- Updated dependencies [[`a2bda6d`](https://github.com/Effect-TS/effect-smol/commit/a2bda6d4ef6de9d9b0c53ae2df5434f778d6161a), [`1f95a2b`](https://github.com/Effect-TS/effect-smol/commit/1f95a2b5aa9524bb38f4437f4691a664bf463ca1), [`a8d5e79`](https://github.com/Effect-TS/effect-smol/commit/a8d5e792fec201a83af0eb92fc79928d055125fd), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`a5386ba`](https://github.com/Effect-TS/effect-smol/commit/a5386ba67005dff697d45a45398f398773f58dcf), [`06d8a03`](https://github.com/Effect-TS/effect-smol/commit/06d8a0391631e6130e3ab25227e59817852e227f), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430), [`f9e883e`](https://github.com/Effect-TS/effect-smol/commit/f9e883e266fbda870336ee62f46b7ac85ba3de6e), [`8caac76`](https://github.com/Effect-TS/effect-smol/commit/8caac76a35821edfe03c75dab5eb056e8fc05430)]: + - effect@4.0.0-beta.7 + - @effect/platform-node-shared@4.0.0-beta.7 + +## 4.0.0-beta.6 + +### Patch Changes + +- Updated dependencies [[`3247da2`](https://github.com/Effect-TS/effect-smol/commit/3247da28331f345f68be5dbd2974a7e03d300fe1), [`f205705`](https://github.com/Effect-TS/effect-smol/commit/f2057050dbd034b8c186be2d40c3d03ee63a5a3b), [`f35022c`](https://github.com/Effect-TS/effect-smol/commit/f35022c212e4111527e1bb43f360a67b2b49fa85), [`8622721`](https://github.com/Effect-TS/effect-smol/commit/86227217b02d43680a3c6f3c21731b1d852c91f5), [`fc660ab`](https://github.com/Effect-TS/effect-smol/commit/fc660ab8b5ebae38b8d6b96cbf2f9b880cc09253), [`f37dc33`](https://github.com/Effect-TS/effect-smol/commit/f37dc335f64622fa9ce8d6d1d5dd8fc3f260257b), [`3662f32`](https://github.com/Effect-TS/effect-smol/commit/3662f328fcfa3b2fa01ffa79da40e12e93fcede8), [`a7d436f`](https://github.com/Effect-TS/effect-smol/commit/a7d436f438dcd7f49b9485e4e95a4511f31fad7d), [`6856a41`](https://github.com/Effect-TS/effect-smol/commit/6856a415d7eddd9d73d60919e976f1d071421be4), [`8c417d0`](https://github.com/Effect-TS/effect-smol/commit/8c417d03475e5e12d00dca0c4781d0af7e66b86c), [`5419570`](https://github.com/Effect-TS/effect-smol/commit/5419570ba47ce882a3a10882707b46f66e464906), [`449c5ed`](https://github.com/Effect-TS/effect-smol/commit/449c5ed5318e8a874e730420bcf52918fa2ec80f), [`4b5ec12`](https://github.com/Effect-TS/effect-smol/commit/4b5ec12f87f95f2a3cd8fe4d5b26c6eb0529381a), [`df87937`](https://github.com/Effect-TS/effect-smol/commit/df879375fc3b169c43f9c434b3775e12b80dffe4), [`5dbfca8`](https://github.com/Effect-TS/effect-smol/commit/5dbfca8d1dbb6d18d1605d4f8562e99c86e2ff11), [`e629497`](https://github.com/Effect-TS/effect-smol/commit/e6294973d55597ab6b6deca6babbe1e946b2c91d), [`981c991`](https://github.com/Effect-TS/effect-smol/commit/981c991cd78db34def815d5754379d737157f005), [`1ca2ed6`](https://github.com/Effect-TS/effect-smol/commit/1ca2ed67301a5dc40ae0ed94346b99f26fd22bbe), [`45722bd`](https://github.com/Effect-TS/effect-smol/commit/45722bde974458311f11ad237711363a10ec6894), [`5207c20`](https://github.com/Effect-TS/effect-smol/commit/5207c2054a3f60c8fd795faa6cf3f179926b8878), [`eb2a85e`](https://github.com/Effect-TS/effect-smol/commit/eb2a85ed4dc162b2535d304799333a5a20477fd0)]: + - effect@4.0.0-beta.6 + - @effect/platform-node-shared@4.0.0-beta.6 + +## 4.0.0-beta.5 + +### Patch Changes + +- Updated dependencies [[`f6e133e`](https://github.com/Effect-TS/effect-smol/commit/f6e133e9a16b32317bd09ff08c12b97a0ae44600), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8), [`a88e206`](https://github.com/Effect-TS/effect-smol/commit/a88e206e44dc66ca5a2b45bedc797877c5dbb083), [`e3893cc`](https://github.com/Effect-TS/effect-smol/commit/e3893ccf2632338c7d8e745f639dcd825a9d42f8)]: + - effect@4.0.0-beta.5 + - @effect/platform-node-shared@4.0.0-beta.5 + +## 4.0.0-beta.4 + +### Patch Changes + +- Updated dependencies [[`c5a18ef`](https://github.com/Effect-TS/effect-smol/commit/c5a18ef44171e3880bf983faee74529908974b32), [`bc6b885`](https://github.com/Effect-TS/effect-smol/commit/bc6b885b94d887a200657c0775dfa874dc15bc0c)]: + - effect@4.0.0-beta.4 + - @effect/platform-node-shared@4.0.0-beta.4 + +## 4.0.0-beta.3 + +### Patch Changes + +- Updated dependencies [[`3a0cf36`](https://github.com/Effect-TS/effect-smol/commit/3a0cf36eff106ba48d74e133c1598cd40613e530), [`c4da328`](https://github.com/Effect-TS/effect-smol/commit/c4da328d32fad1d61e0e538f5d371edf61521d7e)]: + - effect@4.0.0-beta.3 + - @effect/platform-node-shared@4.0.0-beta.3 + +## 4.0.0-beta.2 + +### Patch Changes + +- Updated dependencies [[`a22ce73`](https://github.com/Effect-TS/effect-smol/commit/a22ce73b2bd9305b7ba665694d2255c0e6d5a8d0), [`ebdabf7`](https://github.com/Effect-TS/effect-smol/commit/ebdabf79ff4e62c8384aa8cf9a8d2787d536ee78), [`8f663bb`](https://github.com/Effect-TS/effect-smol/commit/8f663bb121021bf12bd264e8ae385187cb7a5dae)]: + - effect@4.0.0-beta.2 + - @effect/platform-node-shared@4.0.0-beta.2 + +## 4.0.0-beta.1 + +### Patch Changes + +- Updated dependencies [[`0fecf70`](https://github.com/Effect-TS/effect-smol/commit/0fecf70048057623eed7c584a06671773a2b1743), [`709569e`](https://github.com/Effect-TS/effect-smol/commit/709569ed76bead9ebb0670599e4d890a07ca5a43)]: + - effect@4.0.0-beta.1 + - @effect/platform-node-shared@4.0.0-beta.1 + +## 4.0.0-beta.0 + +### Major Changes + +- [#1183](https://github.com/Effect-TS/effect-smol/pull/1183) [`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66) Thanks @tim-smart! - v4 beta + +### Patch Changes + +- Updated dependencies [[`be642ab`](https://github.com/Effect-TS/effect-smol/commit/be642ab1b3b4cd49e53c9732d7aba1b367fddd66)]: + - @effect/platform-node-shared@4.0.0-beta.0 + - effect@4.0.0-beta.0 diff --git a/.context/effect/packages/platform-node/LICENSE b/.context/effect/packages/platform/node/LICENSE similarity index 100% rename from .context/effect/packages/platform-node/LICENSE rename to .context/effect/packages/platform/node/LICENSE diff --git a/.context/effect/packages/platform/node/README.md b/.context/effect/packages/platform/node/README.md new file mode 100644 index 000000000..4f2d152ef --- /dev/null +++ b/.context/effect/packages/platform/node/README.md @@ -0,0 +1,14 @@ +# @effect/platform-node + +[Node.js](https://nodejs.org) implementations of the Effect platform services, including the file system, HTTP client and server, sockets, workers, and terminal. + +## Installation + +```sh +npm install effect@beta @effect/platform-node@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/platform-node) diff --git a/.context/effect/packages/platform/node/package.json b/.context/effect/packages/platform/node/package.json new file mode 100644 index 000000000..86a645415 --- /dev/null +++ b/.context/effect/packages/platform/node/package.json @@ -0,0 +1,84 @@ +{ + "name": "@effect/platform-node", + "type": "module", + "version": "4.0.0-rc.108", + "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" + ], + "engines": { + "node": ">=18.0.0" + }, + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./internal/*": null, + "./index": null, + "./*/index": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./internal/*": null, + "./index": null, + "./*/index": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "dependencies": { + "@effect/platform-node-shared": "workspace:^", + "mime": "^4.1.0", + "undici": "^8.7.0" + }, + "peerDependencies": { + "effect": "workspace:^", + "ioredis": ">=5.7.0 <6.0.0" + }, + "devDependencies": { + "@testcontainers/mysql": "^12.0.4", + "@testcontainers/postgresql": "^12.0.4", + "@testcontainers/redis": "^12.0.4", + "@types/node": "^26.1.2", + "effect": "workspace:^" + } +} diff --git a/.context/effect/packages/platform-node/src/Mime.ts b/.context/effect/packages/platform/node/src/Mime.ts similarity index 100% rename from .context/effect/packages/platform-node/src/Mime.ts rename to .context/effect/packages/platform/node/src/Mime.ts diff --git a/.context/effect/packages/platform-node/src/NodeChildProcessSpawner.ts b/.context/effect/packages/platform/node/src/NodeChildProcessSpawner.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeChildProcessSpawner.ts rename to .context/effect/packages/platform/node/src/NodeChildProcessSpawner.ts diff --git a/.context/effect/packages/platform-node/src/NodeClusterHttp.ts b/.context/effect/packages/platform/node/src/NodeClusterHttp.ts similarity index 93% rename from .context/effect/packages/platform-node/src/NodeClusterHttp.ts rename to .context/effect/packages/platform/node/src/NodeClusterHttp.ts index 82543c806..8f31ccf87 100644 --- a/.context/effect/packages/platform-node/src/NodeClusterHttp.ts +++ b/.context/effect/packages/platform/node/src/NodeClusterHttp.ts @@ -31,6 +31,7 @@ import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" import type { SqlClient } from "effect/unstable/sql/SqlClient" import { createServer } from "node:http" import { layerK8sHttpClient } from "./NodeClusterSocket.ts" +import * as NodeCrypto from "./NodeCrypto.ts" import * as NodeHttpClient from "./NodeHttpClient.ts" import * as NodeHttpServer from "./NodeHttpServer.ts" import type { NodeServices } from "./NodeServices.ts" @@ -60,6 +61,7 @@ export const layer = < >(options: { readonly transport: "http" | "websocket" readonly serialization?: "msgpack" | "ndjson" | undefined + readonly serializationMaxBufferSize?: number | "unbounded" | undefined readonly clientOnly?: ClientOnly | undefined readonly storage?: Storage | undefined readonly runnerHealth?: "ping" | "k8s" | undefined @@ -115,7 +117,7 @@ export const layer = < ? MessageStorage.layerNoop : options?.storage === "byo" ? Layer.empty - : Layer.orDie(SqlMessageStorage.layer) + : Layer.orDie(SqlMessageStorage.layer).pipe(Layer.provide(NodeCrypto.layer)) ), Layer.provide( options?.storage === "local" @@ -126,7 +128,9 @@ export const layer = < ), Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), Layer.provide( - options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + options?.serialization === "ndjson" + ? RpcSerialization.layerNdjsonWith({ maxBufferSize: options.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options.serializationMaxBufferSize }) ) ) as any } diff --git a/.context/effect/packages/platform-node/src/NodeClusterSocket.ts b/.context/effect/packages/platform/node/src/NodeClusterSocket.ts similarity index 91% rename from .context/effect/packages/platform-node/src/NodeClusterSocket.ts rename to .context/effect/packages/platform/node/src/NodeClusterSocket.ts index cd8f1d976..5faaf5a43 100644 --- a/.context/effect/packages/platform-node/src/NodeClusterSocket.ts +++ b/.context/effect/packages/platform/node/src/NodeClusterSocket.ts @@ -28,6 +28,7 @@ import * as SqlRunnerStorage from "effect/unstable/cluster/SqlRunnerStorage" import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization" import type * as SocketServer from "effect/unstable/socket/SocketServer" import type { SqlClient } from "effect/unstable/sql/SqlClient" +import * as NodeCrypto from "./NodeCrypto.ts" import * as NodeFileSystem from "./NodeFileSystem.ts" import * as NodeHttpClient from "./NodeHttpClient.ts" import * as Undici from "./Undici.ts" @@ -65,6 +66,7 @@ export const layer = < >( options?: { readonly serialization?: "msgpack" | "ndjson" | undefined + readonly serializationMaxBufferSize?: number | "unbounded" | undefined readonly clientOnly?: ClientOnly | undefined readonly storage?: Storage | undefined readonly runnerHealth?: "ping" | "k8s" | undefined @@ -113,7 +115,7 @@ export const layer = < ? MessageStorage.layerNoop : options?.storage === "byo" ? Layer.empty - : Layer.orDie(SqlMessageStorage.layer) + : Layer.orDie(SqlMessageStorage.layer).pipe(Layer.provide(NodeCrypto.layer)) ), Layer.provide( options?.storage === "local" @@ -124,7 +126,9 @@ export const layer = < ), Layer.provide(ShardingConfig.layerFromEnv(options?.shardingConfig)), Layer.provide( - options?.serialization === "ndjson" ? RpcSerialization.layerNdjson : RpcSerialization.layerMsgPack + options?.serialization === "ndjson" + ? RpcSerialization.layerNdjsonWith({ maxBufferSize: options?.serializationMaxBufferSize }) + : RpcSerialization.layerMsgPackWith({ maxBufferSize: options?.serializationMaxBufferSize }) ) ) as any } @@ -146,6 +150,8 @@ export const layerDispatcherK8s: Layer.Layer = Layer. if (caCertOption._tag === "Some") { return yield* Effect.acquireRelease( Effect.sync(() => + // oxlint cannot resolve values re-exported through the local Undici facade. + // oxlint-disable-next-line import/namespace new Undici.Agent({ connect: { ca: caCertOption.value diff --git a/.context/effect/packages/platform-node/src/NodeCrypto.ts b/.context/effect/packages/platform/node/src/NodeCrypto.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeCrypto.ts rename to .context/effect/packages/platform/node/src/NodeCrypto.ts diff --git a/.context/effect/packages/platform-node/src/NodeFileSystem.ts b/.context/effect/packages/platform/node/src/NodeFileSystem.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeFileSystem.ts rename to .context/effect/packages/platform/node/src/NodeFileSystem.ts diff --git a/.context/effect/packages/platform-node/src/NodeHttpClient.ts b/.context/effect/packages/platform/node/src/NodeHttpClient.ts similarity index 95% rename from .context/effect/packages/platform-node/src/NodeHttpClient.ts rename to .context/effect/packages/platform/node/src/NodeHttpClient.ts index 420e41dac..27fb8e097 100644 --- a/.context/effect/packages/platform-node/src/NodeHttpClient.ts +++ b/.context/effect/packages/platform/node/src/NodeHttpClient.ts @@ -51,14 +51,14 @@ export { * Use to access or override the fetch implementation used by the Node * fetch-based HTTP client. * - * @category fetch + * @category services * @since 4.0.0 */ Fetch, /** * Layer that provides the fetch-based HTTP client implementation. * - * @category fetch + * @category layers * @since 4.0.0 */ layer as layerFetch, @@ -69,7 +69,7 @@ export { * * Use to provide default fetch request options for Node HTTP requests. * - * @category fetch + * @category services * @since 4.0.0 */ RequestInit @@ -83,7 +83,7 @@ export { * Service tag for the Undici `Dispatcher` used by the Undici-backed HTTP * client. * - * @category Dispatcher + * @category services * @since 4.0.0 */ export class Dispatcher extends Context.Service()( @@ -94,10 +94,12 @@ export class Dispatcher extends Context.Service() * Acquires a new Undici `Agent` dispatcher and destroys it when the enclosing * scope is finalized. * - * @category Dispatcher + * @category resource management * @since 4.0.0 */ export const makeDispatcher: Effect.Effect = Effect.acquireRelease( + // oxlint cannot resolve values re-exported through the local Undici facade. + // oxlint-disable-next-line import/namespace Effect.sync(() => new Undici.Agent()), (dispatcher) => Effect.promise(() => dispatcher.destroy()) ) @@ -105,7 +107,7 @@ export const makeDispatcher: Effect.Effect = Layer.effect(Dispatcher)(makeDispatcher) @@ -114,16 +116,18 @@ export const layerDispatcher: Layer.Layer = Layer.effect(Dispatcher) * Provides the `Dispatcher` service from Undici's process-global dispatcher, * without creating or owning a new agent. * - * @category Dispatcher + * @category layers * @since 4.0.0 */ +// oxlint cannot resolve values re-exported through the local Undici facade. +// oxlint-disable-next-line import/namespace export const dispatcherLayerGlobal: Layer.Layer = Layer.sync(Dispatcher)(() => Undici.getGlobalDispatcher()) /** * Fiber reference containing default Undici request options applied to requests * sent by `makeUndici`. * - * @category Undici + * @category services * @since 4.0.0 */ export const UndiciOptions = Context.Reference>( @@ -136,7 +140,7 @@ export const UndiciOptions = Context.Reference = Layer.provide(layerUndiciNoDispatcher, layerDispatcher) @@ -379,7 +383,7 @@ export const layerUndici: Layer.Layer = Layer.provide(layerUn * Service tag for the paired Node `http` and `https` agents used by the * node:http-backed HTTP client. * - * @category HttpAgent + * @category services * @since 4.0.0 */ export class HttpAgent extends Context.Service => @@ -411,7 +415,7 @@ export const makeAgent = (options?: Https.AgentOptions): Effect.Effect Layer.Layer< @@ -422,7 +426,7 @@ export const layerAgentOptions: (options?: Https.AgentOptions | undefined) => La * Provides the `HttpAgent` service using default scoped Node `http` and * `https` agents. * - * @category HttpAgent + * @category layers * @since 4.0.0 */ export const layerAgent: Layer.Layer = layerAgentOptions() @@ -432,7 +436,7 @@ export const layerAgent: Layer.Layer = layerAgentOptions() * current `HttpAgent`, streaming request bodies, and wrapping Node responses * as `HttpClientResponse` values. * - * @category node:http + * @category constructors * @since 4.0.0 */ export const makeNodeHttp = Effect.gen(function*() { @@ -451,8 +455,11 @@ export const makeNodeHttp = Effect.gen(function*() { headers: request.headers, signal }) - return Effect.forkChild(sendBody(nodeRequest, request, request.body)).pipe( - Effect.flatMap(() => waitForResponse(nodeRequest, request)), + return Effect.raceFirst( + waitForResponse(nodeRequest, request), + sendBody(nodeRequest, request, request.body).pipe(Effect.andThen(Effect.never)) + ).pipe( + Effect.onError(() => Effect.sync(() => nodeRequest.destroy())), Effect.map((_) => new NodeHttpResponse(request, _)) ) }) @@ -645,7 +652,7 @@ class NodeHttpResponse extends NodeHttpIncomingMessage im * Provides a node:http-backed `HttpClient` using the current `HttpAgent` * service. * - * @category node:http + * @category layers * @since 4.0.0 */ export const layerNodeHttpNoAgent: Layer.Layer< @@ -658,7 +665,7 @@ export const layerNodeHttpNoAgent: Layer.Layer< * Provides a node:http-backed `HttpClient` together with default scoped Node * `http` and `https` agents. * - * @category node:http + * @category layers * @since 4.0.0 */ export const layerNodeHttp: Layer.Layer = Layer.provide(layerNodeHttpNoAgent, layerAgent) diff --git a/.context/effect/packages/platform-node/src/NodeHttpIncomingMessage.ts b/.context/effect/packages/platform/node/src/NodeHttpIncomingMessage.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeHttpIncomingMessage.ts rename to .context/effect/packages/platform/node/src/NodeHttpIncomingMessage.ts diff --git a/.context/effect/packages/platform/node/src/NodeHttpPlatform.ts b/.context/effect/packages/platform/node/src/NodeHttpPlatform.ts new file mode 100644 index 000000000..8421b7c32 --- /dev/null +++ b/.context/effect/packages/platform/node/src/NodeHttpPlatform.ts @@ -0,0 +1,119 @@ +/** + * Node.js implementation of the Effect HTTP platform service. + * + * This module connects the portable `HttpPlatform` file response helpers to + * Node runtime primitives. It serves local files through Node readable streams, + * supports byte ranges, converts Web `File` values to readable streams, and + * fills in content type and content length headers when needed. + * + * @since 4.0.0 + */ +import * as NodeHttpCompression from "@effect/platform-node-shared/NodeHttpCompression" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as EtagImpl from "effect/unstable/http/Etag" +import * as Headers from "effect/unstable/http/Headers" +import * as HttpBody from "effect/unstable/http/HttpBody" +import * as Platform from "effect/unstable/http/HttpPlatform" +import * as ServerResponse from "effect/unstable/http/HttpServerResponse" +import * as Fs from "node:fs" +import { Readable } from "node:stream" +import Mime from "./Mime.ts" +import * as NodeFileSystem from "./NodeFileSystem.ts" +import * as NodeStream from "./NodeStream.ts" + +// replaces the response body while keeping every other field, dropping the +// now-stale Content-Length header +const compressedBody = ( + response: ServerResponse.HttpServerResponse, + body: HttpBody.HttpBody +): ServerResponse.HttpServerResponse => + ServerResponse.removeHeader(ServerResponse.setBody(response, body), "content-length") + +const compression = NodeHttpCompression.make({ + algorithms: NodeHttpCompression.algorithms, + compressResponse(response, algorithm, options) { + const body = response.body + switch (body._tag) { + case "Stream": { + return Effect.succeed(compressedBody( + response, + HttpBody.stream( + NodeStream.pipeThroughDuplex(body.stream, { + evaluate: () => NodeHttpCompression.compressTransform(algorithm, options) + }), + body.contentType + ) + )) + } + case "Raw": { + const readable = body.body instanceof Readable + ? body.body + : Readable.fromWeb(new Response(body.body as BodyInit).body as any) + const transform = NodeHttpCompression.compressTransform(algorithm, options) + readable.on("error", (cause) => transform.destroy(cause)) + transform.on("error", (cause) => readable.destroy(cause)) + transform.on("close", () => readable.destroy()) + return Effect.succeed( + compressedBody(response, HttpBody.raw(readable.pipe(transform), { contentType: body.contentType })) + ) + } + default: { + return Effect.succeed(response) + } + } + } +}) + +/** + * Creates the Node `HttpPlatform`, serving file responses from Node readable + * streams and adding MIME type and content-length headers when needed. + * + * @category constructors + * @since 4.0.0 + */ +export const make = Platform.make({ + platform: "node", + compression, + fileResponse(path, status, statusText, headers, start, end, contentLength) { + const stream = contentLength === 0 + ? Readable.from([]) + : Fs.createReadStream(path, { start, end: end === undefined ? undefined : end - 1 }) + 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.fromRecordUnsafe({ + "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", + "content-length": file.size.toString() + }) + ), + status, + statusText + }) + } +}) + +/** + * Provides the Node `HttpPlatform` together with the filesystem and ETag + * services it needs for file responses. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = pipe( + Layer.effect(Platform.HttpPlatform)(make), + Layer.provide(NodeFileSystem.layer), + Layer.provide(EtagImpl.layer) +) diff --git a/.context/effect/packages/platform-node/src/NodeHttpServer.ts b/.context/effect/packages/platform/node/src/NodeHttpServer.ts similarity index 89% rename from .context/effect/packages/platform-node/src/NodeHttpServer.ts rename to .context/effect/packages/platform/node/src/NodeHttpServer.ts index e18cd931f..ed106d3b8 100644 --- a/.context/effect/packages/platform-node/src/NodeHttpServer.ts +++ b/.context/effect/packages/platform/node/src/NodeHttpServer.ts @@ -62,6 +62,26 @@ import * as NodeMultipart from "./NodeMultipart.ts" import * as NodeServices from "./NodeServices.ts" import { NodeWS } from "./NodeSocket.ts" +/** + * Options accepted by the Node `HttpServer` constructors and layers. + * + * @category options + * @since 4.0.0 + */ +export interface Options extends Net.ListenOptions { + readonly disablePreemptiveShutdown?: boolean | undefined + readonly gracefulShutdownTimeout?: Duration.Input | undefined + /** + * Options forwarded to the underlying `ws` `WebSocketServer`, minus the + * wiring options the server manages itself. Use this to enable + * `permessage-deflate` compression or tune payload limits, e.g. + * `websocket: { perMessageDeflate: true }`. + */ + readonly websocket?: + | Omit + | undefined +} + /** * Creates a scoped `HttpServer` from a Node `http.Server`, starts listening * with the supplied options, registers request and upgrade handling, and closes @@ -72,10 +92,7 @@ import { NodeWS } from "./NodeSocket.ts" */ export const make = Effect.fnUntraced(function*( evaluate: LazyArg, - options: Net.ListenOptions & { - readonly disablePreemptiveShutdown?: boolean | undefined - readonly gracefulShutdownTimeout?: Duration.Input | undefined - } + options: Options ) { const scope = yield* Effect.scope const server = evaluate() @@ -116,7 +133,7 @@ export const make = Effect.fnUntraced(function*( const address = server.address()! const wss = yield* Effect.acquireRelease( - Effect.sync(() => new NodeWS.WebSocketServer({ noServer: true })), + Effect.sync(() => new NodeWS.WebSocketServer({ ...options.websocket, noServer: true })), (wss) => Effect.callback((resume) => { wss.close(() => resume(Effect.void)) @@ -189,9 +206,8 @@ export const makeHandler = < nodeRequest: Http.IncomingMessage, nodeResponse: Http.ServerResponse ) { - const map = new Map(services.mapUnsafe) - map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse)) - const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe(map))(handled), options.scope) + const context = Context.add(services, HttpServerRequest, new ServerRequestImpl(nodeRequest, nodeResponse)) + const fiber = Fiber.runIn(Effect.runForkWith(context as Context.Context)(handled), options.scope) nodeResponse.on("close", () => { if (!nodeResponse.writableEnded) { fiber.interruptUnsafe(parent.id, ClientAbort.annotation) @@ -256,9 +272,13 @@ export const makeUpgradeHandler = < (ws) => Effect.sync(() => ws.close()) ) )) - const map = new Map(services.mapUnsafe) - map.set(HttpServerRequest.key, new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect)) - const fiber = Fiber.runIn(Effect.runForkWith(Context.makeUnsafe(map))(handledApp), options.scope) + const context = Context.add( + services, + HttpServerRequest, + new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect) + ) + const fiber = Fiber.runIn(Effect.runForkWith(context as Context.Context)(handledApp), options.scope) + socket.on("error", () => {}) socket.on("close", () => { if (!socket.writableEnded) { fiber.interruptUnsafe(parent.id, ClientAbort.annotation) @@ -330,8 +350,9 @@ class ServerRequestImpl extends NodeHttpIncomingMessage impleme return this.source.url! } + private cachedMethod: HttpMethod | undefined get method(): HttpMethod { - return this.source.method!.toUpperCase() as HttpMethod + return this.cachedMethod ??= this.source.method!.toUpperCase() as HttpMethod } override get headers(): Headers.Headers { @@ -397,10 +418,7 @@ class ServerRequestImpl extends NodeHttpIncomingMessage impleme */ export const layerServer: ( evaluate: LazyArg>, - options: Net.ListenOptions & { - readonly disablePreemptiveShutdown?: boolean | undefined - readonly gracefulShutdownTimeout?: Duration.Input | undefined - } + options: Options ) => Layer.Layer = flow(make, Layer.effect(HttpServer.HttpServer)) /** @@ -427,10 +445,7 @@ export const layerHttpServices: Layer.Layer< */ export const layer = ( evaluate: LazyArg, - options: Net.ListenOptions & { - readonly disablePreemptiveShutdown?: boolean | undefined - readonly gracefulShutdownTimeout?: Duration.Input | undefined - } + options: Options ): Layer.Layer< HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator, ServeError @@ -450,12 +465,7 @@ export const layer = ( */ export const layerConfig = ( evaluate: LazyArg, - options: Config.Wrap< - Net.ListenOptions & { - readonly disablePreemptiveShutdown?: boolean | undefined - readonly gracefulShutdownTimeout?: Duration.Input | undefined - } - > + options: Config.Wrap ): Layer.Layer< HttpServer.HttpServer | NodeServices.NodeServices | HttpPlatform.HttpPlatform | Etag.Generator, ServeError | Config.ConfigError @@ -517,14 +527,20 @@ const handleResponse = ( if (request.method === "HEAD") { nodeResponse.writeHead(response.status, headers) - return Effect.callback((resume) => { - const done = () => { - nodeResponse.off("close", done) - resume(Effect.void) - } - nodeResponse.once("close", done) - nodeResponse.end(done) - }) + return Effect.andThen( + cancelResponseBody(response.body), + Effect.callback((resume) => { + let completed = false + const done = () => { + if (completed) return + completed = true + nodeResponse.off("close", done) + resume(Effect.void) + } + nodeResponse.once("close", done) + nodeResponse.end(done) + }) + ) } const body = response.body switch (body._tag) { @@ -608,17 +624,9 @@ const handleResponse = ( return body.stream.pipe( Stream.orDie, Stream.runForEachArray((array) => { - let needDrain = false - for (let i = 0; i < array.length; i++) { - const written = nodeResponse.write(array[i]) - if (!written && !needDrain) { - needDrain = true - drainLatch.closeUnsafe() - } else if (written && needDrain) { - needDrain = false - } - } - if (!needDrain) return Effect.void + const chunk = array.length > 1 ? Buffer.concat(array) : array[0] + if (nodeResponse.write(chunk)) return Effect.void + drainLatch.closeUnsafe() return drainLatch.await }), Effect.interruptible, @@ -631,6 +639,14 @@ const handleResponse = ( } } +const cancelResponseBody = (body: HttpServerResponse["body"]): Effect.Effect => { + const stream = body._tag === "Raw" ? body.body : undefined + if (stream instanceof Readable) { + return Effect.sync(() => stream.destroy()) + } + return Effect.void +} + const handleCause = ( nodeResponse: Http.ServerResponse, originalResponse: HttpServerResponse diff --git a/.context/effect/packages/platform-node/src/NodeHttpServerRequest.ts b/.context/effect/packages/platform/node/src/NodeHttpServerRequest.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeHttpServerRequest.ts rename to .context/effect/packages/platform/node/src/NodeHttpServerRequest.ts diff --git a/.context/effect/packages/platform-node/src/NodeMultipart.ts b/.context/effect/packages/platform/node/src/NodeMultipart.ts similarity index 94% rename from .context/effect/packages/platform-node/src/NodeMultipart.ts rename to .context/effect/packages/platform/node/src/NodeMultipart.ts index c0f411420..e7a43e109 100644 --- a/.context/effect/packages/platform-node/src/NodeMultipart.ts +++ b/.context/effect/packages/platform/node/src/NodeMultipart.ts @@ -17,11 +17,12 @@ import type * as Path from "effect/Path" import type * as Scope from "effect/Scope" import * as Stream from "effect/Stream" import * as Multipart from "effect/unstable/http/Multipart" -import * as MP from "effect/unstable/http/Multipasta/Node" +import * as MultipartParser from "effect/unstable/http/MultipartParser" 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 MP from "./NodeMultipartParser.ts" import * as NodeStream from "./NodeStream.ts" /** @@ -103,13 +104,13 @@ class FieldImpl extends PartBase implements Multipart.Field { readonly value: string constructor( - info: MP.PartInfo, + info: MultipartParser.PartInfo, value: Uint8Array ) { super() this.key = info.name this.contentType = info.contentType - this.value = MP.decodeField(info, value) + this.value = MultipartParser.decodeField(info, value) } toJSON(): unknown { @@ -158,7 +159,7 @@ class FileImpl extends PartBase implements Multipart.File { } } -function convertError(cause: MP.MultipartError): Multipart.MultipartError { +function convertError(cause: MultipartParser.MultipartError): Multipart.MultipartError { switch (cause._tag) { case "ReachedLimit": { switch (cause.limit) { diff --git a/.context/effect/packages/platform/node/src/NodeMultipartParser.ts b/.context/effect/packages/platform/node/src/NodeMultipartParser.ts new file mode 100644 index 000000000..b1dfbd5cb --- /dev/null +++ b/.context/effect/packages/platform/node/src/NodeMultipartParser.ts @@ -0,0 +1,184 @@ +/** + * Node.js streams adapter for the low-level multipart parser. + * + * @since 4.0.0 + */ +// oxlint-disable typescript/no-unsafe-declaration-merging +/// +import type { BaseConfig, MultipartError, Parser, PartInfo } from "effect/unstable/http/MultipartParser" +import { make as makeParser } from "effect/unstable/http/MultipartParser" +import type { IncomingHttpHeaders } from "node:http" +import { Duplex, Readable } from "node:stream" + +/** + * A part emitted by the Node.js multipart parser. + * + * @category models + * @since 4.0.0 + */ +export type Part = Field | FileStream + +/** + * A parsed multipart field. + * + * @category models + * @since 4.0.0 + */ +export interface Field { + readonly _tag: "Field" + readonly info: PartInfo + readonly value: Uint8Array +} + +/** + * A Node.js duplex stream that emits parsed multipart parts. + * + * @category models + * @since 4.0.0 + */ +export interface MultipartStream extends Duplex { + [Symbol.asyncIterator](): NodeJS.AsyncIterator + + on(event: "field", listener: (field: Field) => void): this + on(event: "file", listener: (file: FileStream) => void): this + on(event: "close", listener: () => void): this + on(event: "data", listener: (part: Part) => void): this + on(event: "drain", listener: () => void): this + on(event: "end", listener: () => void): this + on(event: "error", listener: (err: MultipartError) => void): this + on(event: "finish", listener: () => void): this + on(event: "pause", listener: () => void): this + on(event: "pipe", listener: (src: Readable) => void): this + on(event: "readable", listener: () => void): this + on(event: "resume", listener: () => void): this + on(event: "unpipe", listener: (src: Readable) => void): this + on(event: string | symbol, listener: (...args: Array) => void): this + + read(size?: number): Part | null +} + +/** + * Configuration for the Node.js multipart parser. + * + * @category models + * @since 4.0.0 + */ +export type NodeConfig = Omit & { + readonly headers: IncomingHttpHeaders +} + +/** + * A Node.js duplex stream that parses multipart input. + * + * @category models + * @since 4.0.0 + */ +export class MultipartStream extends Duplex { + private _parser: Parser + _canWrite = true + private _writeCallback: (() => void) | undefined + + constructor(config: NodeConfig) { + super({ readableObjectMode: true }) + let currentError: MultipartError | undefined + let currentFile: FileStream | undefined + this._parser = makeParser({ + ...(config as any), + onField: (info, value) => { + if (currentError !== undefined) return + const field: Field = { _tag: "Field", info, value } + this.push(field) + this.emit("field", field) + }, + onFile: (info) => { + if (currentError !== undefined) return (_) => {} + const file = new FileStream(info, this) + currentFile = file + this.push(file) + this.emit("file", file) + return (chunk) => { + if (currentError !== undefined) return + this._canWrite = file.push(chunk) + if (chunk === null && !this._canWrite) { + currentFile = undefined + this._resume() + } + } + }, + onError: (error) => { + this.emit("error", error) + currentFile?.emit("error", error) + currentError = error + }, + onDone: () => { + this.push(null) + } + }) + } + + _resume() { + this._canWrite = true + if (this._writeCallback !== undefined) { + const callback = this._writeCallback + this._writeCallback = undefined + callback() + } + } + + override _read(_size: number) {} + + override _write( + chunk: any, + encoding: BufferEncoding, + callback: (error?: Error | null | undefined) => void + ): void { + this._parser.write( + chunk instanceof Uint8Array ? chunk : Buffer.from(chunk, encoding) + ) + if (this._canWrite) { + callback() + } else { + this._writeCallback = callback + } + } + + override _final(callback: (error?: Error | null | undefined) => void): void { + this._parser.end() + callback() + } +} + +/** + * Creates a Node.js multipart parser stream. + * + * @category constructors + * @since 4.0.0 + */ +export const make = (config: NodeConfig): MultipartStream => new MultipartStream(config) + +/** + * A readable stream containing a parsed multipart file. + * + * @category models + * @since 4.0.0 + */ +export class FileStream extends Readable { + readonly _tag = "File" + readonly filename: string | undefined + readonly info: PartInfo + private _parent: MultipartStream + constructor( + info: PartInfo, + parent: MultipartStream + ) { + super() + this.info = info + this._parent = parent + this.filename = info.filename + } + override _read(_size: number) { + if (this._parent._canWrite === false) { + this._parent._resume() + } + } +} diff --git a/.context/effect/packages/platform-node/src/NodePath.ts b/.context/effect/packages/platform/node/src/NodePath.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodePath.ts rename to .context/effect/packages/platform/node/src/NodePath.ts diff --git a/.context/effect/packages/platform-node/src/NodeRedis.ts b/.context/effect/packages/platform/node/src/NodeRedis.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeRedis.ts rename to .context/effect/packages/platform/node/src/NodeRedis.ts diff --git a/.context/effect/packages/platform-node/src/NodeRuntime.ts b/.context/effect/packages/platform/node/src/NodeRuntime.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeRuntime.ts rename to .context/effect/packages/platform/node/src/NodeRuntime.ts diff --git a/.context/effect/packages/platform-node/src/NodeServices.ts b/.context/effect/packages/platform/node/src/NodeServices.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeServices.ts rename to .context/effect/packages/platform/node/src/NodeServices.ts diff --git a/.context/effect/packages/platform-node/src/NodeSink.ts b/.context/effect/packages/platform/node/src/NodeSink.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeSink.ts rename to .context/effect/packages/platform/node/src/NodeSink.ts diff --git a/.context/effect/packages/platform-node/src/NodeSocket.ts b/.context/effect/packages/platform/node/src/NodeSocket.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeSocket.ts rename to .context/effect/packages/platform/node/src/NodeSocket.ts diff --git a/.context/effect/packages/platform-node/src/NodeSocketServer.ts b/.context/effect/packages/platform/node/src/NodeSocketServer.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeSocketServer.ts rename to .context/effect/packages/platform/node/src/NodeSocketServer.ts diff --git a/.context/effect/packages/platform-node/src/NodeStdio.ts b/.context/effect/packages/platform/node/src/NodeStdio.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeStdio.ts rename to .context/effect/packages/platform/node/src/NodeStdio.ts diff --git a/.context/effect/packages/platform-node/src/NodeStream.ts b/.context/effect/packages/platform/node/src/NodeStream.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeStream.ts rename to .context/effect/packages/platform/node/src/NodeStream.ts diff --git a/.context/effect/packages/platform-node/src/NodeTerminal.ts b/.context/effect/packages/platform/node/src/NodeTerminal.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeTerminal.ts rename to .context/effect/packages/platform/node/src/NodeTerminal.ts diff --git a/.context/effect/packages/platform-node/src/NodeWorker.ts b/.context/effect/packages/platform/node/src/NodeWorker.ts similarity index 100% rename from .context/effect/packages/platform-node/src/NodeWorker.ts rename to .context/effect/packages/platform/node/src/NodeWorker.ts diff --git a/.context/effect/packages/platform/node/src/NodeWorkerRunner.ts b/.context/effect/packages/platform/node/src/NodeWorkerRunner.ts new file mode 100644 index 000000000..f41ef5e5b --- /dev/null +++ b/.context/effect/packages/platform/node/src/NodeWorkerRunner.ts @@ -0,0 +1,126 @@ +/** + * Node.js runtime support for workers that serve Effect worker requests. + * + * `NodeWorkerRunner` supplies the Node implementation of the Effect worker + * runner platform. The exported `layer` runs inside a `node:worker_threads` + * worker through `parentPort`, or inside a child process through + * `process.send`. It listens for parent messages, runs handlers registered with + * `WorkerRunner`, sends replies over the same channel, and closes when the + * parent sends the close message. + * + * @since 4.0.0 + */ +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 * as Layer from "effect/Layer" +import * as Scope from "effect/Scope" +import { WorkerError, WorkerReceiveError, WorkerSpawnError } from "effect/unstable/workers/WorkerError" +import * as WorkerRunner from "effect/unstable/workers/WorkerRunner" +import * as WorkerThreads from "node:worker_threads" + +/** + * Provides the `WorkerRunnerPlatform` for code running inside a Node worker + * thread or child process, routing parent messages to the registered handler + * and sending responses back through the parent channel. + * + * @category layers + * @since 4.0.0 + */ +export const layer: Layer.Layer = Layer.succeed(WorkerRunner.WorkerRunnerPlatform)({ + start() { + return Effect.gen(function*() { + if (!WorkerThreads.parentPort && !process.send) { + return yield* new WorkerError({ + reason: new WorkerSpawnError({ message: "not in a worker" }) + }) + } + + const sendUnsafe = WorkerThreads.parentPort + ? (_portId: number, message: any, transfers?: any) => WorkerThreads.parentPort!.postMessage(message, transfers) + : (_portId: number, message: any, _transfers?: any) => process.send!(message) + const send = (_portId: number, message: O, transfers?: ReadonlyArray) => + Effect.sync(() => sendUnsafe(_portId, [1, message], transfers as any)) + + const run = ( + handler: (portId: number, message: I) => Effect.Effect | void + ): Effect.Effect => + Effect.scopedWith(Effect.fnUntraced(function*(scope) { + const closeLatch = Deferred.makeUnsafe() + const trackFiber = Fiber.runIn(scope) + const services = yield* Effect.context() + const runFork = Effect.runForkWith(services) + const onExit = (exit: Exit.Exit) => { + if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) { + runFork(Effect.logError("unhandled error in worker", exit.cause)) + } + } + const port = WorkerThreads.parentPort ?? process + function onMessage(message: WorkerRunner.PlatformMessage) { + if (message[0] === 0) { + const result = handler(0, message[1]) + if (Effect.isEffect(result)) { + const fiber = runFork(result) + fiber.addObserver(onExit) + trackFiber(fiber) + } + } else { + if (WorkerThreads.parentPort) { + WorkerThreads.parentPort.close() + } else { + process.channel?.unref() + } + Deferred.doneUnsafe(closeLatch, Exit.void) + } + } + port.on("message", onMessage) + + function onMessageError(cause: unknown) { + Deferred.doneUnsafe( + closeLatch, + new WorkerError({ + reason: new WorkerReceiveError({ + message: "received messageerror event", + cause + }) + }) + ) + } + function onError(cause: unknown) { + Deferred.doneUnsafe( + closeLatch, + new WorkerError({ + reason: new WorkerReceiveError({ + message: "received error event", + cause + }) + }) + ) + } + if (WorkerThreads.parentPort) { + WorkerThreads.parentPort.on("messageerror", onMessageError) + WorkerThreads.parentPort.on("error", onError) + } + + yield* Scope.addFinalizer( + scope, + Effect.sync(() => { + port.off("message", onMessage) + if (WorkerThreads.parentPort) { + WorkerThreads.parentPort.off("messageerror", onMessageError) + WorkerThreads.parentPort.off("error", onError) + } + }) + ) + + sendUnsafe(0, [0]) + + return yield* Deferred.await(closeLatch) + })) + + return { run, send, sendUnsafe } + }) + } +}) diff --git a/.context/effect/packages/platform-node/src/Undici.ts b/.context/effect/packages/platform/node/src/Undici.ts similarity index 94% rename from .context/effect/packages/platform-node/src/Undici.ts rename to .context/effect/packages/platform/node/src/Undici.ts index 90a756059..0caaab409 100644 --- a/.context/effect/packages/platform-node/src/Undici.ts +++ b/.context/effect/packages/platform/node/src/Undici.ts @@ -17,13 +17,13 @@ import Undici from "undici" /** - * @category Undici + * @category re-exports * @since 4.0.0 */ export * from "undici" /** - * @category Undici + * @category re-exports * @since 4.0.0 */ export default Undici diff --git a/.context/effect/packages/platform/node/src/index.ts b/.context/effect/packages/platform/node/src/index.ts new file mode 100644 index 000000000..c006c3d54 --- /dev/null +++ b/.context/effect/packages/platform/node/src/index.ts @@ -0,0 +1,135 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as Mime from "./Mime.ts" + +/** + * @since 4.0.0 + */ +export * as NodeChildProcessSpawner from "./NodeChildProcessSpawner.ts" + +/** + * @since 4.0.0 + */ +export * as NodeClusterHttp from "./NodeClusterHttp.ts" + +/** + * @since 4.0.0 + */ +export * as NodeClusterSocket from "./NodeClusterSocket.ts" + +/** + * @since 1.0.0 + */ +export * as NodeCrypto from "./NodeCrypto.ts" + +/** + * @since 4.0.0 + */ +export * as NodeFileSystem from "./NodeFileSystem.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpClient from "./NodeHttpClient.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpIncomingMessage from "./NodeHttpIncomingMessage.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpPlatform from "./NodeHttpPlatform.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpServer from "./NodeHttpServer.ts" + +/** + * @since 4.0.0 + */ +export * as NodeHttpServerRequest from "./NodeHttpServerRequest.ts" + +/** + * @since 4.0.0 + */ +export * as NodeMultipart from "./NodeMultipart.ts" + +/** + * @since 4.0.0 + */ +export * as NodeMultipartParser from "./NodeMultipartParser.ts" + +/** + * @since 4.0.0 + */ +export * as NodePath from "./NodePath.ts" + +/** + * @since 4.0.0 + */ +export * as NodeRedis from "./NodeRedis.ts" + +/** + * @since 4.0.0 + */ +export * as NodeRuntime from "./NodeRuntime.ts" + +/** + * @since 4.0.0 + */ +export * as NodeServices from "./NodeServices.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSink from "./NodeSink.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSocket from "./NodeSocket.ts" + +/** + * @since 4.0.0 + */ +export * as NodeSocketServer from "./NodeSocketServer.ts" + +/** + * @since 4.0.0 + */ +export * as NodeStdio from "./NodeStdio.ts" + +/** + * @since 4.0.0 + */ +export * as NodeStream from "./NodeStream.ts" + +/** + * @since 4.0.0 + */ +export * as NodeTerminal from "./NodeTerminal.ts" + +/** + * @since 4.0.0 + */ +export * as NodeWorker from "./NodeWorker.ts" + +/** + * @since 4.0.0 + */ +export * as NodeWorkerRunner from "./NodeWorkerRunner.ts" + +/** + * @since 4.0.0 + */ +export * as Undici from "./Undici.ts" diff --git a/.context/effect/packages/platform-node/test/HttpApi.test.ts b/.context/effect/packages/platform/node/test/HttpApi.test.ts similarity index 99% rename from .context/effect/packages/platform-node/test/HttpApi.test.ts rename to .context/effect/packages/platform/node/test/HttpApi.test.ts index af168589c..f00a4f505 100644 --- a/.context/effect/packages/platform-node/test/HttpApi.test.ts +++ b/.context/effect/packages/platform/node/test/HttpApi.test.ts @@ -307,7 +307,7 @@ describe("HttpApi", () => { }) it.effect("required client middleware can fail with typed clientError", () => { - class ClientFailure extends Schema.ErrorClass("ClientFailure")({ + class ClientFailure extends Schema.Error("ClientFailure")({ _tag: Schema.tag("ClientFailure") }) {} @@ -1421,7 +1421,7 @@ describe("HttpApi", () => { }) it.effect("error from plain text", () => { - class RateLimitError extends Schema.ErrorClass("RateLimitError")({ + class RateLimitError extends Schema.Error("RateLimitError")({ _tag: Schema.tag("RateLimitError"), message: Schema.String }) {} @@ -1488,17 +1488,17 @@ describe("HttpApi", () => { }) }) -class UserError extends Schema.ErrorClass("UserError")({ +class UserError extends Schema.Error("UserError")({ _tag: Schema.tag("UserError") }, { httpApiStatus: 400 }) {} -class GroupError extends Schema.ErrorClass("GroupError")({ +class GroupError extends Schema.Error("GroupError")({ _tag: Schema.tag("GroupError") }, { httpApiStatus: 418 }) {} -class NoStatusError extends Schema.ErrorClass("NoStatusError")({ +class NoStatusError extends Schema.Error("NoStatusError")({ _tag: Schema.tag("NoStatusError") }) {} diff --git a/.context/effect/packages/platform-node/test/HttpStaticServer.test.ts b/.context/effect/packages/platform/node/test/HttpStaticServer.test.ts similarity index 100% rename from .context/effect/packages/platform-node/test/HttpStaticServer.test.ts rename to .context/effect/packages/platform/node/test/HttpStaticServer.test.ts diff --git a/.context/effect/packages/platform-node/test/HttpStaticServerConditional.test.ts b/.context/effect/packages/platform/node/test/HttpStaticServerConditional.test.ts similarity index 97% rename from .context/effect/packages/platform-node/test/HttpStaticServerConditional.test.ts rename to .context/effect/packages/platform/node/test/HttpStaticServerConditional.test.ts index b1c8449f4..695ba7467 100644 --- a/.context/effect/packages/platform-node/test/HttpStaticServerConditional.test.ts +++ b/.context/effect/packages/platform/node/test/HttpStaticServerConditional.test.ts @@ -55,12 +55,19 @@ const fileInfo: FileSystem.File.Info = { blocks: Option.none() } +const stubCompression: HttpPlatform.Compression = { + algorithms: new Set(), + compressResponse: Effect.succeed +} + const makeHandler = async () => { const fileSystem = FileSystem.makeNoop({ stat: (path) => path === filePath ? Effect.succeed(fileInfo) : Effect.fail(notFoundError(path)) }) const httpPlatform = HttpPlatform.HttpPlatform.of({ + platform: "node", + compression: stubCompression, fileResponse: (_path, options) => Effect.succeed(HttpServerResponse.text(fileBody, { status: options?.status, @@ -100,6 +107,8 @@ const makeFailingApp = async (options: { }) const httpPlatform = HttpPlatform.HttpPlatform.of({ + platform: "node", + compression: stubCompression, fileResponse: (_path, fileOptions) => { if (options.fileResponseError !== undefined) { return Effect.fail(options.fileResponseError) @@ -137,6 +146,8 @@ const makeLayerHandler = (options: { } }) const httpPlatform = HttpPlatform.HttpPlatform.of({ + platform: "node", + compression: stubCompression, fileResponse: () => options.fileResponseError !== undefined ? Effect.fail(options.fileResponseError) diff --git a/.context/effect/packages/platform/node/test/KeyValueStore.test.ts b/.context/effect/packages/platform/node/test/KeyValueStore.test.ts new file mode 100644 index 000000000..8f6c7c6b2 --- /dev/null +++ b/.context/effect/packages/platform/node/test/KeyValueStore.test.ts @@ -0,0 +1,50 @@ +import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem" +import * as NodePath from "@effect/platform-node/NodePath" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore" + +const platformLayer = Layer.merge(NodeFileSystem.layer, NodePath.layer) + +describe("KeyValueStore / layerFileSystem", () => { + it.effect("rejects invalid keys without modifying the file system", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const root = yield* fs.makeTempDirectoryScoped() + const directory = `${root}/store` + const sibling = `${root}/sibling.txt` + yield* fs.makeDirectory(directory) + yield* fs.writeFileString(sibling, "sibling") + + yield* Effect.gen(function*() { + const store = yield* KeyValueStore.KeyValueStore + + for (const key of ["", ".", ".."]) { + const operations = [ + ["get", Effect.asVoid(store.get(key))], + ["getUint8Array", Effect.asVoid(store.getUint8Array(key))], + ["set", Effect.asVoid(store.set(key, "value"))], + ["remove", Effect.asVoid(store.remove(key))], + ["has", Effect.asVoid(store.has(key))] + ] as const + + for (const [method, operation] of operations) { + const error = yield* Effect.flip(operation) + assert.instanceOf(error, KeyValueStore.KeyValueStoreError) + assert.strictEqual(error.method, method) + assert.strictEqual(error.key, key) + } + } + }).pipe( + Effect.provide(KeyValueStore.layerFileSystem(directory).pipe(Layer.provide(platformLayer))) + ) + + assert.isTrue(yield* fs.exists(directory)) + assert.deepStrictEqual(yield* fs.readDirectory(directory), []) + assert.strictEqual(yield* fs.readFileString(sibling), "sibling") + }).pipe( + Effect.provide(NodeFileSystem.layer) + )) +}) diff --git a/.context/effect/packages/platform/node/test/MultipartParser.test.ts b/.context/effect/packages/platform/node/test/MultipartParser.test.ts new file mode 100644 index 000000000..3a5a38a1e --- /dev/null +++ b/.context/effect/packages/platform/node/test/MultipartParser.test.ts @@ -0,0 +1,885 @@ +import * as Node from "@effect/platform-node/NodeMultipartParser" +import * as Multipart from "effect/unstable/http/MultipartParser" +import { assert, describe, expectTypeOf, test } from "vitest" + +type Expected = Array< + | [type: "field", name: string, value: string, contentType: string] + | [ + type: "file", + name: string, + bytesReceived: number, + filename: string, + contentType: string + ] +> + +interface MultipartCase { + readonly config?: Partial + readonly name: string + readonly source: ReadonlyArray + readonly boundary: string + readonly expected: Expected + readonly errors?: ReadonlyArray +} + +const cases: ReadonlyArray = [ + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_0\"", + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_1\"", + "", + "super beta file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_1\"; filename=\"1k_b.dat\"", + "Content-Type: application/octet-stream", + "", + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + ["field", "file_name_0", "super alpha file", "text/plain"], + ["field", "file_name_1", "super beta file", "text/plain"], + ["file", "upload_file_0", 1023, "1k_a.dat", "application/octet-stream"], + ["file", "upload_file_1", 1023, "1k_b.dat", "application/octet-stream"] + ], + name: "Fields and files" + }, + { + source: [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k\r\n", + `Content-Disposition: form-data; name="file_name_0"\r\n`, + "\r\n", + "super alpha file\r\n", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k\r\n", + "Content-Disposition: form-data; name=\"file_name_1\"\r\n", + "\r\n", + "super beta file\r\n", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k\r\n", + `Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"\r\n`, + "Content-Type: application/octet-stream\r\n", + "\r\n", + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "A".repeat(1024 * 1024), + "\r\n-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + ["field", "file_name_0", "super alpha file", "text/plain"], + ["field", "file_name_1", "super beta file", "text/plain"], + [ + "file", + "upload_file_0", + 1024 * 1024 * 10, + "1k_a.dat", + "application/octet-stream" + ] + ], + name: "Fields and large file" + }, + { + source: [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k\r\n", + "Content-Disposition: form-data; name=\"file_name_0\"\r\n", + "\r\n", + "super alpha file\r\n", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [["field", "file_name_0", "super alpha file", "text/plain"]], + name: "Headers over multiple chunks", + errors: [] + }, + { + source: [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_0\"", + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ] + .join("\r\n") + .split(""), + + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [["field", "file_name_0", "super alpha file", "text/plain"]], + name: "Headers over single byte chunks", + errors: [] + }, + { + source: [ + [ + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"cont\"", + "", + "some random content", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"pass\"", + "", + "some random pass", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"bit\"", + "", + "2", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [ + ["field", "cont", "some random content", "text/plain"], + ["field", "pass", "some random pass", "text/plain"], + ["field", "bit", "2", "text/plain"] + ], + name: "Fields only" + }, + { + source: [ + [ + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"first\"", + "", + "A".repeat(32), + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"second\"", + "", + "B".repeat(32), + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + config: { + maxPartSize: 100 + }, + expected: [ + ["field", "first", "A".repeat(32), "text/plain"], + ["field", "second", "B".repeat(32), "text/plain"] + ], + errors: [], + name: "Resets maxPartSize between field parts" + }, + { + source: [], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [], + errors: ["EndNotReached"], + name: "No fields and no files" + }, + { + source: [ + "--boundary\r\ncontent-disposition: form-data; nam" + ], + boundary: "boundary", + expected: [], + errors: ["EndNotReached"], + name: "Truncated mid-header" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_0\"", + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + config: { + maxFieldSize: 5 + }, + expected: [], + errors: ["ReachedLimit"], + name: "stops after maxFieldSize is exceeded" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + config: { + maxPartSize: 13 + }, + expected: [], + errors: ["ReachedLimit"], + name: "stops after maxPartSize is exceeded" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_0\"", + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + config: { + maxParts: 0 + }, + expected: [], + errors: ["ReachedLimit"], + name: "stops before the first part when maxParts is 0" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_0\"", + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file_name_1\"", + "", + "super beta file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + config: { + maxParts: 1 + }, + errors: ["ReachedLimit"], + expected: [["field", "file_name_0", "super alpha file", "text/plain"]], + name: "stops after maxParts is exceeded" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"/absolute/1k_a.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_1\"; filename=\"C:\\absolute\\1k_b.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_2\"; filename=\"relative/1k_c.dat\"", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + [ + "file", + "upload_file_0", + 26, + "/absolute/1k_a.dat", + "application/octet-stream" + ], + [ + "file", + "upload_file_1", + 26, + "C:\\absolute\\1k_b.dat", + "application/octet-stream" + ], + [ + "file", + "upload_file_2", + 26, + "relative/1k_c.dat", + "application/octet-stream" + ] + ], + name: "Paths to be preserved" + }, + { + source: [ + [ + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"cont\"", + "Content-Type: ", + "", + "some random content", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: ", + "", + "some random pass", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [ + ["field", "cont", "some random content", "text/plain"], + ["field", "", "some random pass", "text/plain"] + ], + name: "Empty content-type and empty content-disposition" + }, + { + config: { + isFile: (_) => _.name !== "upload_file_0" + }, + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"blob\"", + "Content-Type: application/json", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + [ + "field", + "upload_file_0", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "application/json" + ] + ], + name: "Blob uploads should be handled as fields if isFile is provided." + }, + { + config: { + isFile: (_) => _.name !== "upload_file_0" + }, + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"blob\"", + "Content-Type: application/json", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file\"; filename*=utf-8''n%C3%A4me.txt", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + [ + "field", + "upload_file_0", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "application/json" + ], + ["file", "file", 26, "näme.txt", "application/octet-stream"] + ], + name: "Blob uploads should be handled as fields if isFile is provided. Other parts should be files." + }, + { + config: { + isFile: (_) => _.name === "upload_file_0" + }, + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"upload_file_0\"; filename=\"blob\"", + "Content-Type: application/json", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file\"; filename*=utf-8''n%C3%A4me.txt", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + ["file", "upload_file_0", 26, "blob", "application/json"], + [ + "field", + "file", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "application/octet-stream" + ] + ], + name: "Blob uploads sould be handled as files if corresponding isFile is provided. Other parts should be fields." + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file\"; filename*=utf-8''n%C3%A4me.txt", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [["file", "file", 26, "näme.txt", "application/octet-stream"]], + name: "Unicode filenames" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"file\"; filename*=utf-8''%ZZ", + "Content-Type: application/octet-stream", + "", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [["file", "file", 26, "%ZZ", "application/octet-stream"]], + name: "Malformed encoded filenames" + }, + { + source: [ + [ + "--asdasdasdasd\r\n", + "Content-Type: text/plain\r\n", + "Content-Disposition: form-data; name=\"foo\"\r\n", + "\r\n", + "asd\r\n", + "--asdasdasdasd--" + ].join(":)") + ], + boundary: "asdasdasdasd", + expected: [], + errors: ["BadHeaders", "EndNotReached"], + name: "Stopped mid-header" + }, + { + source: [ + [ + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY", + "Content-Disposition: form-data; name=\"cont\"", + "Content-Type: application/json", + "", + "{}", + "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [["field", "cont", "{}", "application/json"]], + name: "content-type for fields" + }, + { + source: ["------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n"], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [], + name: "empty form" + }, + { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"field1\"", + "content-type: text/plain; charset=utf-8", + "", + "Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + "Content-Disposition: form-data; name=\"field2\"", + "content-type: text/plain; charset=iso-8859-1", + "", + "sapere aude!", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + [ + "field", + "field1", + "Aufklärung ist der Ausgang des Menschen aus seiner selbstverschuldeten Unmündigkeit.", + "text/plain" + ], + ["field", "field2", "sapere aude!", "text/plain"] + ], + name: "Fields and files" + }, + { + source: [ + [ + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"regsubmit\"", + "", + "yes", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"referer\"", + "", + "http://domainExample/./", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"activationauth\"", + "", + "", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"seccodemodid\"", + "", + "member::register", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryzca7IDMnT6QwqBp7", + expected: [ + ["field", "regsubmit", "yes", "text/plain"], + ["field", "referer", "http://domainExample/./", "text/plain"], + ["field", "activationauth", "", "text/plain"], + ["field", "seccodemodid", "member::register", "text/plain"] + ], + name: "one empty part should get ignored" + }, + { + source: [" ------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n"], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhY", + expected: [], + errors: ["EndNotReached"], + name: "empty form with preceding whitespace" + }, + { + source: ["------WebKitFormBoundaryTB2MiQ36fnSJlrhY--\r\n"], + boundary: "----WebKitFormBoundaryTB2MiQ36fnSJlrhYY", + expected: [], + errors: ["EndNotReached"], + name: "empty form with wrong boundary (extra Y)" + }, + { + source: [ + [ + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"regsubmit\"", + "", + "yes", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"referer\"", + "", + "http://domainExample/./", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"activationauth\"", + "", + "", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7", + "Content-Disposition: form-data; name=\"seccodemodid\"", + "", + "member::register", + "------WebKitFormBoundaryzca7IDMnT6QwqBp7--" + ].join("\r\n") + ], + boundary: "----WebKitFormBoundaryzca7IDMnT6QwqBp7", + expected: [ + ["field", "regsubmit", "yes", "text/plain"], + ["field", "referer", "http://domainExample/./", "text/plain"], + ["field", "activationauth", "", "text/plain"], + ["field", "seccodemodid", "member::register", "text/plain"] + ], + name: "multiple empty parts should get ignored" + } +] + +describe("multipart", () => { + test.each(cases)("$name", (opts) => { + const parts: Expected = [] + const errors: Array = [] + + const parser = Multipart.make({ + ...opts.config, + headers: { + "content-type": "multipart/form-data; boundary=" + opts.boundary + }, + onFile: (info) => { + let size = 0 + return (chunk) => { + if (chunk) { + size += chunk.length + } else { + parts.push([ + "file", + info.name, + size, + info.filename!, + info.contentType + ]) + } + } + }, + onField: (info, value) => { + parts.push([ + "field", + info.name, + Multipart.decodeField(info, value), + info.contentType + ]) + }, + onError: (error) => { + errors.push(error._tag) + }, + onDone: () => {} + }) + + opts.source.forEach((chunk) => { + parser.write(new TextEncoder().encode(chunk)) + }) + parser.end() + + assert.deepStrictEqual(opts.expected, parts) + if (opts.errors) { + assert.deepEqual(opts.errors, errors) + } + }) +}) + +describe("node api", () => { + test("exposes missing filenames as undefined", () => { + const parser = Node.make({ + headers: { + "content-type": "multipart/form-data; boundary=boundary" + } + }) + let emitted = false + parser.on("file", (file) => { + emitted = true + expectTypeOf(file.filename).toEqualTypeOf() + assert.strictEqual(file.filename, undefined) + file.resume() + }) + + parser.write( + new TextEncoder().encode( + "--boundary\r\nContent-Disposition: form-data; name=\"file\"\r\nContent-Type: application/octet-stream\r\n\r\ncontent\r\n--boundary--" + ) + ) + parser.end() + + assert.isTrue(emitted) + parser.destroy() + }) + + test("stops sending file chunks after an error", () => { + const parser = Node.make({ + headers: { + "content-type": "multipart/form-data; boundary=boundary" + }, + maxPartSize: 100 + }) + let file: Node.FileStream | undefined + parser.on("file", (part) => { + file = part + part.on("error", () => {}) + }) + parser.on("error", () => {}) + + parser.write( + new TextEncoder().encode( + "--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n\r\nA" + ) + ) + parser.write(new TextEncoder().encode("B".repeat(128))) + + assert.strictEqual(file?.read().toString(), "A") + file?.destroy() + parser.destroy() + }) + + test.each(cases)("$name", (opts) => { + const parts: Expected = [] + const errors: Array = [] + + const parser = Node.make({ + ...opts.config, + headers: { + "content-type": "multipart/form-data; boundary=" + opts.boundary + } + }) + parser.on("field", (field) => { + parts.push([ + "field", + field.info.name, + Multipart.decodeField(field.info, field.value), + field.info.contentType + ]) + }) + parser.on("file", (file) => { + let size = 0 + file.on("data", (chunk) => { + size += chunk.length + }) + file.on("end", () => { + parts.push([ + "file", + file.info.name, + size, + file.info.filename!, + file.info.contentType + ]) + }) + }) + parser.on("error", (error) => { + errors.push(error._tag) + }) + + parser.on("end", () => { + assert.deepStrictEqual(opts.expected, parts) + if (opts.errors) { + assert.deepEqual(opts.errors, errors) + } + }) + + opts.source.forEach((chunk) => { + parser.write(new TextEncoder().encode(chunk)) + }) + parser.end() + }) +}) + +describe("node async-iterable api", () => { + test.each(cases)("$name", async (opts) => { + const parts: Expected = [] + + const parser = Node.make({ + ...opts.config, + headers: { + "content-type": "multipart/form-data; boundary=" + opts.boundary + } + }) + async function read() { + for await (const part of parser) { + if (part._tag === "Field") { + parts.push([ + "field", + part.info.name, + Multipart.decodeField(part.info, part.value), + part.info.contentType + ]) + } else { + let size = 0 + for await (const chunk of part) { + size += chunk.length + } + parts.push([ + "file", + part.info.name, + size, + part.info.filename!, + part.info.contentType + ]) + } + } + } + + const readPromise = read() + + opts.source.forEach((chunk) => { + parser.write(new TextEncoder().encode(chunk)) + }) + parser.end() + + try { + await readPromise + assert.deepStrictEqual(opts.expected, parts) + } catch (err) { + if (opts.errors) { + assert.strictEqual((err as any)._tag, opts.errors[0]) + } + } + }) +}) + +describe("random data", () => { + test("smoke test", () => { + const boundary = "------WebKitFormBoundaryTB2MiQ36fnSJlrhY--" + let seed = 0x9e3779b9 + const random = () => { + seed ^= seed << 13 + seed ^= seed >>> 17 + seed ^= seed << 5 + return (seed >>> 0) / 0x1_0000_0000 + } + for (let i = 0; i < 100; i++) { + const size = Math.round(random() * 1024 * 1024 * 100) + const data = Buffer.alloc(size, i) + let success = false + + const parser = Multipart.make({ + headers: { + "content-type": `multipart/form-data; boundary=${boundary}` + }, + onDone() { + success = true + }, + onFile() { + return () => {} + }, + onField() {}, + onError() {} + }) + + const buffer = Buffer.concat([ + Buffer.from(`--${boundary}\r\n`), + Buffer.from( + "Content-Disposition: form-data; name=\"file\"; filename=\"blob\"\r\n" + ), + Buffer.from("Content-Type: application/octet-stream\r\n"), + Buffer.from("\r\n"), + data, + Buffer.from(`\r\n--${boundary}--`) + ]) + + let cursor = 0 + while (cursor < buffer.length) { + const maxChunkSize = buffer.length - cursor + const chunkSize = Math.max( + 1, + Math.min( + Math.round(random() * 128 * 1024 * 1024), + maxChunkSize + ) + ) + parser.write(buffer.subarray(cursor, cursor + chunkSize)) + cursor += chunkSize + } + parser.end() + assert.isTrue(success) + } + }, 30_000) +}) diff --git a/.context/effect/packages/platform-node/test/NodeCrypto.test.ts b/.context/effect/packages/platform/node/test/NodeCrypto.test.ts similarity index 100% rename from .context/effect/packages/platform-node/test/NodeCrypto.test.ts rename to .context/effect/packages/platform/node/test/NodeCrypto.test.ts diff --git a/.context/effect/packages/platform/node/test/NodeHttpClient.test.ts b/.context/effect/packages/platform/node/test/NodeHttpClient.test.ts new file mode 100644 index 000000000..345204bdb --- /dev/null +++ b/.context/effect/packages/platform/node/test/NodeHttpClient.test.ts @@ -0,0 +1,200 @@ +import { NodeHttpServer } from "@effect/platform-node" +import * as NodeClient from "@effect/platform-node/NodeHttpClient" +import { assert, 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" +import { + HttpBody, + HttpClient, + HttpClientRequest, + HttpClientResponse, + HttpRouter, + HttpServer, + HttpServerRequest, + HttpServerResponse +} from "effect/unstable/http" +import * as Http from "node:http" + +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 largeResponseBody = "a".repeat(10 * 1024 * 1024) + +const makeLocalServerClient = Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + 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 LocalServerClient extends Effect.Success {} +const LocalServerClient = Context.Service("test/LocalServerClient") +const LocalServerClientLive = Layer.effect(LocalServerClient)(makeLocalServerClient) +const LocalServerRoutes = HttpRouter.serve(HttpRouter.addAll([ + HttpRouter.route( + "GET", + "/todos/1", + Effect.succeed(HttpServerResponse.jsonUnsafe({ + userId: 1, + id: 1, + title: "test", + completed: false + })) + ), + HttpRouter.route( + "POST", + "/todos", + Effect.gen(function*() { + const todo = yield* HttpServerRequest.schemaBodyJson(TodoWithoutId) + return HttpServerResponse.jsonUnsafe({ ...todo, id: 201 }) + }) + ), + HttpRouter.route("GET", "/text", Effect.succeed(HttpServerResponse.text("test"))), + HttpRouter.route("GET", "/large", Effect.succeed(HttpServerResponse.text(largeResponseBody))), + HttpRouter.route("GET", "/hang", Effect.never), + HttpRouter.route("GET", "/redirect", Effect.succeed(HttpServerResponse.redirect("/redirected"))), + HttpRouter.route("GET", "/redirected", Effect.succeed(HttpServerResponse.text("redirected"))), + HttpRouter.route("HEAD", "/todos", Effect.succeed(HttpServerResponse.empty({ status: 200 }))) +])) +;[ + { + name: "fetch", + layer: NodeClient.layerFetch + }, + { + name: "node:http", + layer: NodeClient.layerNodeHttp + }, + { + name: "undici", + layer: NodeClient.layerUndici + } +].forEach(({ layer, name }) => { + const layerTest = HttpServer.layerTestClient.pipe( + Layer.provide(layer), + Layer.provideMerge(NodeHttpServer.layer(Http.createServer, { port: 0 })) + ) + const localServerTestLayer = Layer.merge(LocalServerClientLive, LocalServerRoutes).pipe( + Layer.provideMerge(layerTest) + ) + + describe(`NodeHttpClient - ${name}`, () => { + it.effect("text", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("/text").pipe( + Effect.flatMap((_) => _.text) + ) + expect(response).toBe("test") + }).pipe(Effect.provide(localServerTestLayer))) + + it.effect("local server followRedirects", () => + Effect.gen(function*() { + const client = (yield* HttpClient.HttpClient).pipe( + HttpClient.followRedirects() + ) + const response = yield* client.get("/redirect").pipe( + Effect.flatMap((_) => _.text) + ) + expect(response).toBe("redirected") + }).pipe(Effect.provide(localServerTestLayer))) + + it.effect("text stream", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("/text").pipe( + Effect.map((_) => _.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.mkString + ) + expect(response).toBe("test") + }).pipe(Effect.provide(localServerTestLayer))) + + it.effect("local server", () => + Effect.gen(function*() { + const local = yield* LocalServerClient + const response = yield* local.client.get("/todos/1").pipe( + Effect.flatMap(HttpClientResponse.schemaBodyJson(Todo)) + ) + expect(response.id).toBe(1) + }).pipe( + Effect.provide(localServerTestLayer) + )) + + it.effect("local server schemaBodyJson", () => + Effect.gen(function*() { + const local = yield* LocalServerClient + const response = yield* local.createTodo({ + userId: 1, + title: "test", + completed: false + }) + expect(response.title).toBe("test") + }).pipe( + Effect.provide(localServerTestLayer) + )) + + it.effect("head request with schemaJson", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.head("/todos").pipe( + Effect.flatMap( + HttpClientResponse.schemaJson(Schema.Struct({ status: Schema.Literal(200) })) + ) + ) + expect(response).toEqual({ status: 200 }) + }).pipe(Effect.provide(localServerTestLayer))) + + it.live("interrupt", () => + Effect.gen(function*() { + const client = yield* HttpClient.HttpClient + const response = yield* client.get("/hang").pipe( + Effect.flatMap((_) => _.text), + Effect.timeout(1), + Effect.asSome, + Effect.catchTag("TimeoutError", () => Effect.succeedNone) + ) + expect(response._tag).toEqual("None") + }).pipe(Effect.provide(localServerTestLayer))) + + it.effect("close early", () => + Effect.gen(function*() { + const response = yield* HttpClient.get("/large") + expect(response.status).toBe(200) + }).pipe(Effect.provide(localServerTestLayer))) + }) +}) + +it.live("returns a stream body encoding failure", () => + Effect.gen(function*() { + yield* Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + yield* request.text + return HttpServerResponse.empty() + }).pipe(HttpServer.serveEffect()) + const error = yield* HttpClient.post("/", { + body: HttpBody.stream(Stream.fail("encode failure")) + }).pipe(Effect.timeout("1 second"), Effect.flip) + assert(error._tag === "HttpClientError") + assert.strictEqual(error.reason._tag, "EncodeError") + assert.strictEqual(error.reason.cause, "encode failure") + }).pipe(Effect.provide(HttpServer.layerTestClient.pipe( + Layer.provide(NodeClient.layerNodeHttp), + Layer.provideMerge(NodeHttpServer.layer(Http.createServer, { port: 0 })) + )))) diff --git a/.context/effect/packages/platform/node/test/NodeHttpCompression.test.ts b/.context/effect/packages/platform/node/test/NodeHttpCompression.test.ts new file mode 100644 index 000000000..6a70c0996 --- /dev/null +++ b/.context/effect/packages/platform/node/test/NodeHttpCompression.test.ts @@ -0,0 +1,208 @@ +import { NodeHttpServer } from "@effect/platform-node" +import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Latch from "effect/Latch" +import * as Layer from "effect/Layer" +import * as Stream from "effect/Stream" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpEffect from "effect/unstable/http/HttpEffect" +import * as HttpMiddleware from "effect/unstable/http/HttpMiddleware" +import * as HttpPlatform from "effect/unstable/http/HttpPlatform" +import * as HttpRouter from "effect/unstable/http/HttpRouter" +import type { HttpServerRequest } from "effect/unstable/http/HttpServerRequest" +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse" +import * as Crypto from "node:crypto" +import * as Fs from "node:fs" +import * as Os from "node:os" +import * as Path from "node:path" +import { fileURLToPath } from "node:url" +import * as Zlib from "node:zlib" + +const bigJson = JSON.stringify({ text: "All work and no play makes Jack a dull boy. ".repeat(100) }) +const bigJsonApp = Effect.succeed(HttpServerResponse.text(bigJson, { contentType: "application/json" })) + +const zstdSupported = typeof Zlib.zstdCompress === "function" + +type App = Effect.Effect +type CompressionOptions = Parameters[0] + +const withHandler = async ( + app: App, + options: CompressionOptions, + run: (handler: (request: Request) => Promise) => Promise +) => { + const { dispose, handler } = HttpEffect.toWebHandlerLayer( + app as Effect.Effect, + NodeHttpPlatform.layer, + { middleware: HttpMiddleware.compression(options) } + ) + try { + await run(handler) + } finally { + await dispose() + } +} + +const get = ( + handler: (request: Request) => Promise, + headers?: Record +) => handler(new Request("http://localhost/", headers === undefined ? {} : { headers })) + +describe("NodeHttpCompression", () => { + it.effect("advertises supported algorithms", () => + Effect.gen(function*() { + const platform = yield* HttpPlatform.HttpPlatform + assert.isTrue(platform.compression.algorithms.has("gzip")) + assert.isTrue(platform.compression.algorithms.has("deflate")) + assert.isTrue(platform.compression.algorithms.has("br")) + assert.strictEqual(platform.compression.algorithms.has("zstd"), zstdSupported) + }).pipe(Effect.provide(NodeHttpPlatform.layer))) + + it("compresses one-shot bodies asynchronously with gzip and an exact Content-Length", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("vary"), "Accept-Encoding") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.gunzipSync(compressed).toString(), bigJson) + })) + + it("prefers br over gzip in server order", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip, br" }) + assert.strictEqual(response.headers.get("content-encoding"), "br") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.brotliDecompressSync(compressed).toString(), bigJson) + })) + + it("compresses with deflate", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "deflate" }) + assert.strictEqual(response.headers.get("content-encoding"), "deflate") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.inflateSync(compressed).toString(), bigJson) + })) + + it.skipIf(!zstdSupported)( + "compresses with zstd when opted in", + () => + withHandler(bigJsonApp, { algorithms: ["zstd", "gzip"] }, async (handler) => { + const response = await get(handler, { "accept-encoding": "zstd" }) + assert.strictEqual(response.headers.get("content-encoding"), "zstd") + const compressed = new Uint8Array(await response.arrayBuffer()) + assert.strictEqual(response.headers.get("content-length"), compressed.byteLength.toString()) + assert.strictEqual(Zlib.zstdDecompressSync(compressed).toString(), bigJson) + }) + ) + + it("excludes zstd unless opted in", () => + withHandler(bigJsonApp, undefined, async (handler) => { + const response = await get(handler, { "accept-encoding": "zstd, gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + })) + + it("compresses stream bodies without a Content-Length", () => + withHandler( + Effect.succeed(HttpServerResponse.stream( + Stream.fromArray([new TextEncoder().encode(bigJson)]), + { contentType: "application/json" } + )), + undefined, + async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + assert.strictEqual(Zlib.gunzipSync(new Uint8Array(await response.arrayBuffer())).toString(), bigJson) + } + )) + + it("compresses file responses through the streaming path and weakens the ETag", async () => { + const path = fileURLToPath(new URL("./NodeHttpCompression.test.ts", import.meta.url)) + const contents = Fs.readFileSync(path).toString() + await withHandler( + HttpServerResponse.file(path, { headers: { "content-type": "text/plain" } }), + undefined, + async (handler) => { + const response = await get(handler, { "accept-encoding": "gzip" }) + assert.strictEqual(response.headers.get("content-encoding"), "gzip") + assert.strictEqual(response.headers.get("content-length"), null) + assert.isTrue(response.headers.get("etag")!.startsWith("W/")) + assert.strictEqual(Zlib.gunzipSync(new Uint8Array(await response.arrayBuffer())).toString(), contents) + } + ) + }) + + it.effect("closes compressed file bodies for HEAD requests", () => + Effect.gen(function*() { + const directory = Fs.mkdtempSync(Path.join(Os.tmpdir(), "effect-http-compression-")) + yield* Effect.addFinalizer(() => Effect.sync(() => Fs.rmSync(directory, { recursive: true }))) + const path = Path.join(directory, "random.bin") + Fs.writeFileSync(path, Crypto.randomBytes(1024 * 1024)) + + const closed = yield* Latch.make(false) + const response = yield* HttpServerResponse.file(path, { headers: { "content-type": "text/plain" } }) + .pipe( + Effect.tap((response) => + Effect.sync(() => { + if (response.body._tag !== "Raw") { + throw new Error(`Expected a Raw body, received ${response.body._tag}`) + } + const readable = response.body.body as Fs.ReadStream + readable.once("close", () => closed.openUnsafe()) + }) + ) + ) + + yield* HttpRouter.add("GET", "/file", Effect.succeed(response)).pipe( + (self) => HttpRouter.serve(self, { middleware: HttpMiddleware.compression({ minSize: 0 }) }), + Layer.build + ) + const head = yield* HttpClient.head("/file", { headers: { "accept-encoding": "gzip" } }) + assert.strictEqual(head.status, 200) + const result = yield* closed.await.pipe(Effect.timeoutOption("1 second")) + assert.strictEqual(result._tag, "Some") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.effect("flushes compressed chunks incrementally over the wire", () => + Effect.gen(function*() { + const latch = yield* Latch.make(false) + const encoder = new TextEncoder() + yield* HttpRouter.add( + "GET", + "/sse", + Effect.succeed(HttpServerResponse.stream( + Stream.concat( + Stream.succeed(encoder.encode("data: first\n\n")), + Stream.concat( + Stream.drain(Stream.fromEffect(latch.await)), + Stream.succeed(encoder.encode("data: second\n\n")) + ) + ), + { contentType: "text/event-stream" } + )) + ).pipe( + (self) => HttpRouter.serve(self, { middleware: HttpMiddleware.compression() }), + Layer.build + ) + const client = yield* HttpClient.HttpClient + const response = yield* client.get("/sse", { headers: { "accept-encoding": "gzip" } }) + assert.strictEqual(response.headers["content-encoding"], "gzip") + // the source stream only ends after the latch opens, and the latch only + // opens once the first decoded event arrives, so this deadlocks unless + // compressed chunks flush incrementally + const received: Array = [] + yield* response.stream.pipe( + Stream.runForEach((chunk) => + Effect.suspend(() => { + received.push(new TextDecoder().decode(chunk)) + return latch.open + }) + ) + ) + assert.strictEqual(received.join(""), "data: first\n\ndata: second\n\n") + }).pipe(Effect.provide(NodeHttpServer.layerTest))) +}) diff --git a/.context/effect/packages/platform-node/test/NodeHttpPlatform.test.ts b/.context/effect/packages/platform/node/test/NodeHttpPlatform.test.ts similarity index 100% rename from .context/effect/packages/platform-node/test/NodeHttpPlatform.test.ts rename to .context/effect/packages/platform/node/test/NodeHttpPlatform.test.ts diff --git a/.context/effect/packages/platform-node/test/NodeHttpServer.test.ts b/.context/effect/packages/platform/node/test/NodeHttpServer.test.ts similarity index 75% rename from .context/effect/packages/platform-node/test/NodeHttpServer.test.ts rename to .context/effect/packages/platform/node/test/NodeHttpServer.test.ts index 031025b68..e41d6afdb 100644 --- a/.context/effect/packages/platform-node/test/NodeHttpServer.test.ts +++ b/.context/effect/packages/platform/node/test/NodeHttpServer.test.ts @@ -1,5 +1,6 @@ /** @effect-diagnostics preferSchemaOverJson:skip-file */ import { NodeHttpServer } from "@effect/platform-node" +import { NodeWS } from "@effect/platform-node/NodeSocket" import { assert, describe, expect, it } from "@effect/vitest" import { Effect } from "effect" import * as Duration from "effect/Duration" @@ -28,7 +29,9 @@ import { } from "effect/unstable/http" import * as HttpApiError from "effect/unstable/httpapi/HttpApiError" import * as Buffer from "node:buffer" +import { EventEmitter } from "node:events" import * as Http from "node:http" +import * as Net from "node:net" const Todo = Schema.Struct({ id: Schema.Number, @@ -508,6 +511,120 @@ describe("HttpServer", () => { assert.strictEqual(res.status, 204) }).pipe(Effect.provide(NodeHttpServer.layerTest))) + it.effect("completes a HEAD response once when close precedes the end callback", () => + Effect.gen(function*() { + const scope = yield* Effect.scope + const handler = yield* NodeHttpServer.makeHandler( + Effect.succeed(HttpServerResponse.empty()), + { scope } + ) + const completed = Latch.makeUnsafe() + let writableEnded = false + const nodeResponse = Object.defineProperty(new EventEmitter(), "writableEnded", { + get: () => writableEnded + }) as Http.ServerResponse + let closeListenerRemovals = 0 + nodeResponse.writeHead = () => nodeResponse + nodeResponse.off = ((event: string | symbol, listener: (...args: Array) => void) => { + if (event === "close") { + closeListenerRemovals++ + } + return EventEmitter.prototype.off.call(nodeResponse, event, listener) as Http.ServerResponse + }) as Http.ServerResponse["off"] + nodeResponse.end = ((callback: () => void) => { + writableEnded = true + nodeResponse.emit("close") + callback() + completed.openUnsafe() + return nodeResponse + }) as Http.ServerResponse["end"] + + handler( + { method: "HEAD", url: "/", headers: {}, socket: {} } as Http.IncomingMessage, + nodeResponse + ) + yield* completed.await + + assert.strictEqual(closeListenerRemovals, 1) + })) + + it.effect("coalesces streaming chunks from the same pull", () => + Effect.gen(function*() { + const scope = yield* Effect.scope + const handler = yield* NodeHttpServer.makeHandler( + Effect.succeed(HttpServerResponse.stream(Stream.make( + Buffer.Buffer.from("a"), + Buffer.Buffer.from("b") + ))), + { scope } + ) + const completed = Latch.makeUnsafe() + const writes: Array = [] + let writableEnded = false + const nodeResponse = Object.defineProperty(new EventEmitter(), "writableEnded", { + get: () => writableEnded + }) as Http.ServerResponse + nodeResponse.writeHead = () => nodeResponse + nodeResponse.write = ((chunk: Uint8Array) => { + writes.push(chunk) + return true + }) as Http.ServerResponse["write"] + nodeResponse.end = (() => { + writableEnded = true + completed.openUnsafe() + return nodeResponse + }) as Http.ServerResponse["end"] + + handler( + { method: "GET", url: "/", headers: {}, socket: {} } as Http.IncomingMessage, + nodeResponse + ) + yield* completed.await + + assert.deepStrictEqual(writes.map((chunk) => Buffer.Buffer.from(chunk).toString()), ["ab"]) + })) + + it.effect("waits for drain after a streaming write applies backpressure", () => + Effect.gen(function*() { + const scope = yield* Effect.scope + const handler = yield* NodeHttpServer.makeHandler( + Effect.succeed(HttpServerResponse.stream(Stream.make( + Buffer.Buffer.from("a"), + Buffer.Buffer.from("b") + ))), + { scope } + ) + const writeObserved = Latch.makeUnsafe() + const completed = Latch.makeUnsafe() + let writableEnded = false + const nodeResponse = Object.defineProperty(new EventEmitter(), "writableEnded", { + get: () => writableEnded + }) as Http.ServerResponse + let writeCount = 0 + nodeResponse.writeHead = () => nodeResponse + nodeResponse.write = (() => { + writeCount++ + queueMicrotask(() => writeObserved.openUnsafe()) + return writeCount > 1 + }) as Http.ServerResponse["write"] + nodeResponse.end = (() => { + writableEnded = true + completed.openUnsafe() + return nodeResponse + }) as Http.ServerResponse["end"] + + handler( + { method: "GET", url: "/", headers: {}, socket: {} } as Http.IncomingMessage, + nodeResponse + ) + yield* writeObserved.await + assert.strictEqual(nodeResponse.writableEnded, false) + + nodeResponse.emit("drain") + yield* completed.await + assert.strictEqual(writeCount, 1) + })) + it.live("disposes after a client aborts a handler awaiting an upstream request", () => { const upstreamStarted = Latch.makeUnsafe() const upstream = Http.createServer(() => { @@ -585,7 +702,7 @@ describe("HttpServer", () => { describe("HttpServerRespondable", () => { it.effect("error/schema", () => Effect.gen(function*() { - class CustomError extends Schema.ErrorClass("CustomError")({ + class CustomError extends Schema.Error("CustomError")({ _tag: Schema.tag("CustomError"), name: Schema.String }) { @@ -673,8 +790,95 @@ describe("HttpServer", () => { ) expect(root).toEqual("root") }).pipe(Effect.provide(NodeHttpServer.layerTest))) + + it.effect("websocket options are forwarded to the WebSocketServer", () => + Effect.gen(function*() { + yield* HttpRouter.add( + "GET", + "/ws", + Effect.gen(function*() { + const request = yield* HttpServerRequest.HttpServerRequest + const socket = yield* Effect.orDie(request.upgrade) + yield* Effect.orDie(socket.run(() => Effect.void)) + return HttpServerResponse.empty() + }) + ).pipe( + HttpRouter.serve, + Layer.build + ) + const server = yield* HttpServer.HttpServer + const port = (server.address as HttpServer.TcpAddress).port + + const connect = (perMessageDeflate: boolean) => + Effect.acquireRelease( + Effect.callback((resume) => { + const ws = new NodeWS.WebSocket(`ws://127.0.0.1:${port}/ws`, { perMessageDeflate }) + ws.on("open", () => resume(Effect.succeed(ws))) + ws.on("error", (error) => resume(Effect.fail(error))) + }), + (ws) => Effect.sync(() => ws.close()) + ) + + // layerTest configures websocket: { perMessageDeflate: true }, so the + // server accepts the extension when the client offers it... + const compressed = yield* connect(true) + expect(compressed.extensions).toContain("permessage-deflate") + + // ...and clients that do not offer it still connect uncompressed. + const plain = yield* connect(false) + expect(plain.extensions).not.toContain("permessage-deflate") + }).pipe(Effect.scoped, Effect.provide(layerTestWebsocket))) + + it.effect("an upgrade connection reset by the peer does not crash the process", () => + Effect.gen(function*() { + yield* HttpRouter.add("GET", "/", HttpServerResponse.text("ok")).pipe( + HttpRouter.serve, + Layer.build + ) + const server = yield* HttpServer.HttpServer + const port = (server.address as HttpServer.TcpAddress).port + + const uncaught: Array = [] + const onUncaught = (error: unknown) => uncaught.push(error) + process.on("uncaughtException", onUncaught) + yield* Effect.addFinalizer(() => Effect.sync(() => process.off("uncaughtException", onUncaught))) + + yield* Effect.callback((resume) => { + const socket = Net.connect({ port, host: "127.0.0.1" }, () => { + socket.write( + "GET /ws HTTP/1.1\r\n" + + "Host: 127.0.0.1\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + + "Sec-WebSocket-Version: 13\r\n\r\n" + ) + setTimeout(() => { + socket.resetAndDestroy() + setTimeout(() => resume(Effect.void), 50) + }, 50) + }) + socket.on("error", () => {}) + }) + + expect(uncaught).toEqual([]) + const response = yield* HttpClient.get("/") + expect(response.status).toEqual(200) + }).pipe(Effect.provide(layerTestWebsocket))) }) +const layerTestWebsocket = HttpServer.layerTestClient.pipe( + Layer.provide( + Layer.fresh(FetchHttpClient.layer).pipe( + Layer.provide(Layer.succeed(FetchHttpClient.RequestInit)({ keepalive: false })) + ) + ), + Layer.provideMerge(NodeHttpServer.layer(Http.createServer, { + port: 0, + websocket: { perMessageDeflate: true } + })) +) + const tcpPort = (server: Http.Server): number => { const address = server.address() assert(address !== null && typeof address !== "string") diff --git a/.context/effect/packages/platform-node/test/NodeRedis.test.ts b/.context/effect/packages/platform/node/test/NodeRedis.integration.test.ts similarity index 100% rename from .context/effect/packages/platform-node/test/NodeRedis.test.ts rename to .context/effect/packages/platform/node/test/NodeRedis.integration.test.ts diff --git a/.context/effect/packages/platform/node/test/NodeSocket.test.ts b/.context/effect/packages/platform/node/test/NodeSocket.test.ts new file mode 100644 index 000000000..943aa758e --- /dev/null +++ b/.context/effect/packages/platform/node/test/NodeSocket.test.ts @@ -0,0 +1,259 @@ +import { NodeSocket, NodeSocketServer } from "@effect/platform-node" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Queue } from "effect" +import * as Exit from "effect/Exit" +import * as Fiber from "effect/Fiber" +import * as Scope from "effect/Scope" +import * as Stream from "effect/Stream" +import { Socket, type SocketServer } from "effect/unstable/socket" +import * as Net from "node:net" +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.live("closes with a pending pre-run socket", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const server = yield* NodeSocketServer.make({ host: "127.0.0.1", port: 0 }).pipe(Scope.provide(scope)) + assert.strictEqual(server.address._tag, "TcpAddress") + if (server.address._tag !== "TcpAddress") return + const socket = Net.createConnection({ host: "127.0.0.1", port: server.address.port }) + yield* Effect.promise(() => + new Promise((resolve, reject) => { + socket.once("connect", resolve) + socket.once("error", reject) + }) + ) + const closing = yield* Scope.close(scope, Exit.void).pipe(Effect.forkChild) + const exit = yield* Fiber.await(closing).pipe( + Effect.timeout("1 second"), + Effect.ensuring(Effect.sync(() => socket.destroy())) + ) + assert.isTrue(Exit.isSuccess(exit)) + })) + + it.live("closes with a pending pre-run WebSocket", () => + Effect.gen(function*() { + const scope = yield* Scope.make() + const server = yield* NodeSocketServer.makeWebSocket({ host: "127.0.0.1", port: 0 }).pipe(Scope.provide(scope)) + assert.strictEqual(server.address._tag, "TcpAddress") + if (server.address._tag !== "TcpAddress") return + const socket = new NodeSocket.NodeWS.WebSocket(`ws://127.0.0.1:${server.address.port}`) + yield* Effect.promise(() => + new Promise((resolve, reject) => { + socket.once("open", resolve) + socket.once("error", reject) + }) + ) + const closing = yield* Scope.close(scope, Exit.void).pipe(Effect.forkChild) + const exit = yield* Fiber.await(closing).pipe( + Effect.timeout("1 second"), + Effect.ensuring(Effect.sync(() => socket.terminate())) + ) + assert.isTrue(Exit.isSuccess(exit)) + })) + + it.effect("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 + ) + + const output = yield* outputEffect + assert.strictEqual(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.effect("messages", () => + Effect.gen(function*() { + const server = yield* makeServer + const socket = yield* Socket.makeWebSocket(Effect.succeed(url), { + closeCodeIsError: () => false + }) + const messages = yield* Queue.unbounded() + const fiber = yield* Effect.forkChild(socket.run((_) => Queue.offer(messages, _))) + yield* Effect.gen(function*() { + const write = yield* socket.writer + yield* write(new TextEncoder().encode("Hello")) + yield* write(new TextEncoder().encode("World")) + }).pipe(Effect.scoped) + assert.deepStrictEqual(yield* Effect.promise(() => server.nextMessage), new TextEncoder().encode("Hello")) + assert.deepStrictEqual(yield* Effect.promise(() => server.nextMessage), new TextEncoder().encode("World")) + + server.send("Right back at you!") + let message = yield* Queue.take(messages) + assert.deepStrictEqual(message, new TextEncoder().encode("Right back at you!")) + + server.send(new Blob(["A Blob message"])) + message = yield* Queue.take(messages) + assert.deepStrictEqual(message, new TextEncoder().encode("A Blob message")) + + server.close() + const exit = yield* Fiber.await(fiber) + assert.strictEqual(exit._tag, "Success") + }).pipe( + Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)) + )) + + it.effect("close codes are errors by default", () => + Effect.gen(function*() { + const server = yield* makeServer + const socket = yield* Socket.makeWebSocket(Effect.succeed(url)) + const fiber = yield* Effect.forkChild(socket.run(() => {})) + + yield* Effect.promise(() => server.connected) + server.close({ code: 1000, reason: "done", wasClean: true }) + + const exit = yield* Effect.exit(Fiber.join(fiber)) + assert.isTrue(exit._tag === "Failure") + if (exit._tag === "Failure") { + const failure = exit.cause.reasons[0] + if (failure._tag === "Fail") { + assert.isTrue(failure.error instanceof Socket.SocketError) + assert.strictEqual(failure.error.reason._tag, "SocketCloseError") + if (failure.error.reason._tag === "SocketCloseError") { + assert.strictEqual(failure.error.reason.code, 1000) + assert.strictEqual(failure.error.reason.closeReason, "done") + } + } + } + }).pipe( + Effect.provideService(Socket.WebSocketConstructor, (url) => new globalThis.WebSocket(url)) + )) + + it.effect("reports send errors as SocketError", () => + Effect.gen(function*() { + class ThrowingWebSocket extends EventTarget { + readonly readyState = globalThis.WebSocket.OPEN + + close(): void {} + + send(): void { + throw new Error("send failed") + } + } + const webSocket = new ThrowingWebSocket() + const socket = yield* Socket.makeWebSocket(Effect.succeed(url), { closeCodeIsError: () => false }).pipe( + Effect.provideService( + Socket.WebSocketConstructor, + () => webSocket as unknown as globalThis.WebSocket + ) + ) + const exit = yield* Effect.scoped(Effect.gen(function*() { + const run = yield* Effect.forkChild(socket.runRaw(() => {})) + const write = yield* socket.writer + const exit = yield* Effect.exit(write(new Uint8Array([1]))) + webSocket.dispatchEvent(new CloseEvent("close", { code: 1000 })) + yield* Fiber.join(run) + return exit + })) + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assert.strictEqual(exit.cause.reasons[0]?._tag, "Fail") + const reason = exit.cause.reasons[0] + if (reason?._tag === "Fail") { + assert.isTrue(Socket.SocketError.is(reason.error)) + assert.strictEqual(reason.error.reason._tag, "SocketWriteError") + } + } + })) + }) + + describe("TransformStream", () => { + it.effect("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 + }), + { + closeCodeIsError: () => false + } + ) + yield* socket.writer.pipe( + Effect.tap((write) => + write("Hello").pipe( + Effect.andThen(write("World")) + ) + ), + Effect.scoped, + Effect.forkChild + ) + 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"]) + })) + + it.effect("reports writable stream rejection as SocketError", () => + Effect.gen(function*() { + const socket = yield* Socket.fromTransformStream(Effect.succeed({ + readable: new ReadableStream({}), + writable: new WritableStream({ + write: () => Promise.reject(new Error("write failed")) + }) + })) + const exit = yield* Effect.scoped(Effect.gen(function*() { + const run = yield* Effect.forkChild(socket.runRaw(() => {})) + const write = yield* socket.writer + const exit = yield* Effect.exit(write(new Uint8Array([1]))) + yield* Fiber.interrupt(run) + return exit + })) + assert.strictEqual(exit._tag, "Failure") + if (exit._tag === "Failure") { + assert.strictEqual(exit.cause.reasons[0]?._tag, "Fail") + const reason = exit.cause.reasons[0] + if (reason?._tag === "Fail") { + assert.isTrue(Socket.SocketError.is(reason.error)) + assert.strictEqual(reason.error.reason._tag, "SocketWriteError") + } + } + })) + }) +}) diff --git a/.context/effect/packages/platform-node/test/OpenApi.test.ts b/.context/effect/packages/platform/node/test/OpenApi.test.ts similarity index 100% rename from .context/effect/packages/platform-node/test/OpenApi.test.ts rename to .context/effect/packages/platform/node/test/OpenApi.test.ts diff --git a/.context/effect/packages/platform/node/test/RpcServer.test.ts b/.context/effect/packages/platform/node/test/RpcServer.test.ts new file mode 100644 index 000000000..1390216e2 --- /dev/null +++ b/.context/effect/packages/platform/node/test/RpcServer.test.ts @@ -0,0 +1,355 @@ +import { NodeHttpServer, NodeSocket, NodeSocketServer } from "@effect/platform-node" +import { assert, describe, it } from "@effect/vitest" +import { Cause, Deferred, Effect, Exit, Fiber, Layer, Ref, Schedule, Schema, Stream } from "effect" +import { Entity, EntityProxy, EntityProxyServer, Sharding } from "effect/unstable/cluster" +import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http" +import { Rpc, RpcClient, RpcGroup, RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc" +import { SocketServer } from "effect/unstable/socket" +import { e2eSuite, UsersClient } from "./fixtures/rpc-e2e.ts" +import { RpcLive, User } from "./fixtures/rpc-schemas.ts" + +describe("RpcServer", () => { + // http ndjson + const HttpProtocol = RpcServer.layerProtocolHttp({ path: "/rpc" }).pipe( + Layer.provide(HttpRouter.layer) + ) + const HttpNdjsonServer = RpcLive.pipe( + Layer.provideMerge(HttpProtocol), + Layer.provide(HttpRouter.serve(HttpProtocol, { disableListenLog: true, disableLogger: true })) + ) + const HttpNdjsonClient = UsersClient.layer.pipe( + Layer.provide( + RpcClient.layerProtocolHttp({ + url: "", + transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl("/rpc")) + }) + ) + ) + const CustomDefectLayer = HttpNdjsonClient.pipe( + Layer.provideMerge(HttpNdjsonServer), + Layer.provide([NodeHttpServer.layerTest, RpcSerialization.layerNdjson]) + ) + 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 WsProtocol = RpcServer.layerProtocolWebsocket({ path: "/rpc" }).pipe( + Layer.provide(HttpRouter.layer) + ) + const HttpWsServer = RpcLive.pipe( + Layer.provideMerge(WsProtocol), + Layer.provide(HttpRouter.serve(WsProtocol, { disableListenLog: true, disableLogger: true })) + ) + 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.unwrap) + ) + ) + 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.unwrap) + ) + ) + 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: "node" + // }) + // ) + // ), + // 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", () => { + it.effect("preserves full defect with custom schema", () => + Effect.gen(function*() { + const client = yield* UsersClient + const cause = yield* client.ProduceDefectCustom().pipe( + Effect.sandbox, + Effect.flip + ) + const defect = Cause.squash(cause) + assert.instanceOf(defect, Error) + assert.strictEqual(defect.name, "CustomDefect") + assert.strictEqual(defect.message, "detailed error") + assert.strictEqual(defect.stack, "Error: detailed error\n at handler.ts:1") + }).pipe(Effect.provide(CustomDefectLayer))) + }) + + describe("entity proxy", () => { + it.effect("provides handler context for generated rpc handlers", () => + Effect.gen(function*() { + const TestEntity = Entity.make("TestEntity", [Rpc.make("NoPayload")]) + const TestEntityRpcs = EntityProxy.toRpcGroup(TestEntity) + const called = yield* Deferred.make() + const testClient = (entityId: string) => ({ + NoPayload: (payload: void, options?: { readonly discard?: boolean }) => + Effect.gen(function*() { + assert.strictEqual(entityId, "id") + assert.strictEqual(payload, undefined) + assert.strictEqual(options?.discard, true) + yield* Deferred.succeed(called, undefined) + }) + }) + const sharding = Sharding.Sharding.of({ + ...({} as Sharding.Sharding["Service"]), + isShutdown: Effect.succeed(false), + makeClient: () => Effect.succeed(testClient) as never, + pollStorage: Effect.void + }) + + const client = yield* RpcTest.makeClient(TestEntityRpcs).pipe( + Effect.provide(EntityProxyServer.layerRpcHandlers(TestEntity)), + Effect.provideService(Sharding.Sharding, sharding) + ) + + yield* client["TestEntity.NoPayloadDiscard"]({ + entityId: "id", + payload: undefined + }) + yield* Deferred.await(called) + })) + }) + + // Asserts a failing sibling call does not affect an in-flight Ticker stream on the same connection + const Ticker = Rpc.make("Ticker", { + success: Schema.Number, + stream: true + }) + + const IsolationClient = RpcClient.layerProtocolSocket().pipe( + 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.unwrap) + ), + Layer.provide(RpcSerialization.layerNdjson) + ) + + const assertTickerSurvives = ( + setup: Effect.Effect< + { + readonly ticker: Stream.Stream + readonly failingCall: Effect.Effect + }, + never, + R + >, + label: string + ) => + Effect.gen(function*() { + const { failingCall, ticker } = yield* setup + + const received = yield* Ref.make>([]) + + const tickerFiber = yield* ticker.pipe( + Stream.runForEach((value) => Ref.update(received, (xs) => [...xs, value])), + Effect.forkChild + ) + + yield* Effect.retry( + Effect.flatMap( + Ref.get(received), + (xs) => xs.length >= 2 ? Effect.void : Effect.fail("not enough ticks yet") + ), + { schedule: Schedule.spaced("50 millis"), times: 200 } + ) + + const ticksBefore = (yield* Ref.get(received)).length + assert.isAtLeast(ticksBefore, 2) + + yield* failingCall + + yield* Effect.sleep("300 millis") + + const ticksAfter = (yield* Ref.get(received)).length + const tickerStatus = tickerFiber.pollUnsafe() + + yield* Fiber.interrupt(tickerFiber) + + assert.isUndefined(tickerStatus, `Ticker stream must still be running after ${label}`) + assert.isAbove(ticksAfter, ticksBefore, `Ticker stream must keep emitting after ${label}`) + }) + + describe("unknown-tag isolation", () => { + const Ghost = Rpc.make("Ghost", { + payload: { value: Schema.String }, + success: Schema.String + }) + + const serverGroup = RpcGroup.make(Ticker) + const clientGroup = RpcGroup.make(Ticker, Ghost) + + const TickerHandlers = serverGroup.toLayer({ + Ticker: () => Stream.fromSchedule(Schedule.spaced("60 millis")) + }) + + const IsolationServer = RpcServer.layer(serverGroup).pipe( + Layer.provide(TickerHandlers), + Layer.provideMerge(RpcServer.layerProtocolSocketServer), + Layer.provideMerge(NodeSocketServer.layer({ port: 0 })), + Layer.provide(RpcSerialization.layerNdjson) + ) + + it.live( + "an unknown request tag fails only its own request, not other in-flight streams on the same connection", + () => + assertTickerSurvives( + Effect.map(RpcClient.make(clientGroup), (client) => ({ + ticker: client.Ticker(), + failingCall: Effect.gen(function*() { + const ghostExit = yield* client.Ghost({ value: "boo" }).pipe(Effect.exit) + assert.isTrue(Exit.isFailure(ghostExit), "Ghost call should fail with the routing miss") + }) + })), + "the unknown-tag failure" + ).pipe(Effect.provide(IsolationClient.pipe(Layer.provideMerge(IsolationServer)))), + { timeout: 30_000 } + ) + }) + + describe("fatal-defect isolation", () => { + const Boom = Rpc.make("Boom", { + success: Schema.String + }) + + const group = RpcGroup.make(Ticker, Boom) + + const Handlers = group.toLayer({ + Ticker: () => Stream.fromSchedule(Schedule.spaced("60 millis")), + Boom: () => Effect.die("boom") + }) + + const DefectServer = RpcServer.layer(group, { disableFatalDefects: true }).pipe( + Layer.provide(Handlers), + Layer.provideMerge(RpcServer.layerProtocolSocketServer), + Layer.provideMerge(NodeSocketServer.layer({ port: 0 })), + Layer.provide(RpcSerialization.layerNdjson) + ) + + it.live( + "with disableFatalDefects a handler defect fails only its own request, not other in-flight streams", + () => + assertTickerSurvives( + Effect.map(RpcClient.make(group), (client) => ({ + ticker: client.Ticker(), + failingCall: Effect.gen(function*() { + const boomExit = yield* client.Boom().pipe(Effect.exit) + if (!Exit.isFailure(boomExit)) { + return assert.fail("Boom call must fail with the handler defect") + } + assert.include( + String(Cause.squash(boomExit.cause)), + "boom", + "the caller must receive the handler defect" + ) + }) + })), + "the handler defect" + ).pipe(Effect.provide(IsolationClient.pipe(Layer.provideMerge(DefectServer)))), + { timeout: 30_000 } + ) + }) +}) diff --git a/.context/effect/packages/platform-node/test/__snapshots__/HttpApi.test.ts.snap b/.context/effect/packages/platform/node/test/__snapshots__/HttpApi.test.ts.snap similarity index 92% rename from .context/effect/packages/platform-node/test/__snapshots__/HttpApi.test.ts.snap rename to .context/effect/packages/platform/node/test/__snapshots__/HttpApi.test.ts.snap index 9765aadd0..4afc5ec7d 100644 --- a/.context/effect/packages/platform-node/test/__snapshots__/HttpApi.test.ts.snap +++ b/.context/effect/packages/platform/node/test/__snapshots__/HttpApi.test.ts.snap @@ -20,7 +20,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` ], "type": "object", }, - "Group": { + "GroupEncoded": { "additionalProperties": false, "properties": { "id": { @@ -36,7 +36,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` ], "type": "object", }, - "NoStatusError": { + "NoStatusErrorEncoded": { "additionalProperties": false, "properties": { "_tag": { @@ -51,7 +51,17 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` ], "type": "object", }, - "User": { + "Union_": { + "anyOf": [ + { + "type": "string", + }, + { + "type": "null", + }, + ], + }, + "UserEncoded": { "additionalProperties": false, "properties": { "createdAt": { @@ -64,14 +74,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "type": "string", }, "uuid": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "null", - }, - ], + "$ref": "#/components/schemas/Union_", }, }, "required": [ @@ -81,7 +84,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` ], "type": "object", }, - "UserError": { + "UserErrorEncoded": { "additionalProperties": false, "properties": { "_tag": { @@ -168,7 +171,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Group", + "$ref": "#/components/schemas/GroupEncoded", }, }, }, @@ -325,7 +328,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Group", + "$ref": "#/components/schemas/GroupEncoded", }, }, }, @@ -397,7 +400,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/User", + "$ref": "#/components/schemas/UserEncoded", }, "type": "array", }, @@ -409,7 +412,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NoStatusError", + "$ref": "#/components/schemas/NoStatusErrorEncoded", }, }, }, @@ -453,14 +456,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "type": "string", }, "uuid": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "null", - }, - ], + "$ref": "#/components/schemas/Union_", }, }, "required": [ @@ -477,7 +473,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User", + "$ref": "#/components/schemas/UserEncoded", }, }, }, @@ -489,10 +485,10 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "schema": { "anyOf": [ { - "$ref": "#/components/schemas/UserError", + "$ref": "#/components/schemas/UserErrorEncoded", }, { - "$ref": "#/components/schemas/UserError", + "$ref": "#/components/schemas/UserErrorEncoded", }, ], }, @@ -520,14 +516,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "name": "0", "required": true, "schema": { - "anyOf": [ - { - "type": "string", - }, - { - "type": "null", - }, - ], + "$ref": "#/components/schemas/Union_", }, }, ], @@ -668,7 +657,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/User", + "$ref": "#/components/schemas/UserEncoded", }, }, }, @@ -678,7 +667,7 @@ exports[`HttpApi > original tests > OpenAPI spec > fixture 1`] = ` "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserError", + "$ref": "#/components/schemas/UserErrorEncoded", }, }, }, diff --git a/.context/effect/packages/platform/node/test/cluster-integration/ClusterCron.test.ts b/.context/effect/packages/platform/node/test/cluster-integration/ClusterCron.test.ts new file mode 100644 index 000000000..8ecb051d4 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/ClusterCron.test.ts @@ -0,0 +1,301 @@ +import { assert, describe, it } from "@effect/vitest" +import { Clock, Context, Cron, DateTime, Duration, Effect, Latch, Layer } from "effect" +import { ClusterCron, ClusterSchema, Entity } from "effect/unstable/cluster" +import { type Backend, make } from "./harness.ts" + +interface Tick { + readonly at: number + readonly runner: string + readonly scheduled: string +} + +const everySecond = Cron.parseUnsafe("* * * * * *", "UTC") +const testConfig = { shardsPerGroup: 12 } as const + +const addressString = (address: { readonly host: string; readonly port: number }) => `${address.host}:${address.port}` + +const recordTick = (ticks: Array) => + Effect.contextWith((context: Context.Context) => + Effect.gen(function*() { + const address = Context.getUnsafe(context, Entity.CurrentAddress) + const runner = Context.getUnsafe(context, Entity.CurrentRunnerAddress) + const at = yield* Clock.currentTimeMillis + const tick = { + at, + runner: addressString(runner), + scheduled: String(address.entityId) + } + const isFirst = ticks.length === 0 + ticks.push(tick) + return isFirst + }) + ) + +const cronProbe = (name: string, shardGroup = "default") => + Entity.make(`ClusterCron/${name}`, []).annotate(ClusterSchema.ShardGroup, () => shardGroup) + +const nextScheduled = (cron: Cron.Cron, after: DateTime.DateTime.Input) => + DateTime.formatIso(DateTime.fromDateUnsafe(Cron.next(cron, after))) + +const assertScheduledFromExecutionTime = ( + cron: Cron.Cron, + ticks: ReadonlyArray, + startIndex = 1 +) => { + for (let index = startIndex; index < ticks.length; index++) { + assert.strictEqual(ticks[index].scheduled, nextScheduled(cron, ticks[index - 1].at)) + } +} + +const assertScheduledFromPrevious = (cron: Cron.Cron, ticks: ReadonlyArray) => { + assert.strictEqual(ticks[0].scheduled, "initial") + for (let index = 2; index < ticks.length; index++) { + assert.strictEqual(ticks[index].scheduled, nextScheduled(cron, ticks[index - 1].scheduled)) + } +} + +describe("cluster cron integration", () => { + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: runs once per scheduled instant and continues after failure`, () => + Effect.gen(function*() { + const ticks: Array = [] + let failingAttempts = 0 + let successfulAttempts = 0 + const cron = ClusterCron.make({ + name: `basic-${backend}`, + cron: everySecond, + execute: recordTick(ticks) + }) + const failingCron = ClusterCron.make({ + name: `failure-${backend}`, + cron: everySecond, + execute: Effect.suspend(() => { + failingAttempts++ + if (failingAttempts === 1) return Effect.fail("expected cron failure") + successfulAttempts++ + return Effect.void + }) + }) + const cluster = yield* make({ + backend, + config: testConfig, + entities: Layer.merge(cron, failingCron) + }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The cron jobs did not continue through four scheduled instants", + Effect.sync(() => ticks.length >= 4 && failingAttempts >= 4) + ) + + const firstFour = ticks.slice(0, 4) + assert.strictEqual(new Set(firstFour.map((tick) => tick.scheduled)).size, firstFour.length) + assertScheduledFromExecutionTime(everySecond, firstFour) + assert.strictEqual(successfulAttempts, failingAttempts - 1) + })) + + it.live(`${backend}: calculates the next run from the previous instant or the current time`, () => + Effect.gen(function*() { + const previousTicks: Array = [] + const currentTicks: Array = [] + const gate = Latch.makeUnsafe() + let entered = 0 + const blockedExecution = Effect.fnUntraced( + function*(ticks: Array) { + if (yield* recordTick(ticks)) { + entered++ + yield* gate.await + } + }, + Effect.uninterruptible + ) + const previousCron = ClusterCron.make({ + name: `previous-${backend}`, + cron: everySecond, + calculateNextRunFromPrevious: true, + execute: blockedExecution(previousTicks) + }) + const currentCron = ClusterCron.make({ + name: `current-${backend}`, + cron: everySecond, + execute: blockedExecution(currentTicks) + }) + const cluster = yield* make({ + backend, + config: testConfig, + entities: Layer.merge(previousCron, currentCron) + }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "Both cron executions did not enter their first run", + Effect.sync(() => entered === 2) + ) + const blockedAt = yield* Clock.currentTimeMillis + yield* cluster.waitUntil( + "The cron executions were not held across several scheduled instants", + Effect.map(Clock.currentTimeMillis, (now) => now >= blockedAt + 3_200), + "5 seconds" + ) + gate.openUnsafe() + yield* cluster.waitUntil( + "The cron jobs did not resume after their first execution", + Effect.sync(() => previousTicks.length >= 4 && currentTicks.length >= 4), + "12 seconds" + ) + + const previousSecond = DateTime.toEpochMillis(DateTime.makeUnsafe(previousTicks[1].scheduled)) + const currentSecond = DateTime.toEpochMillis(DateTime.makeUnsafe(currentTicks[1].scheduled)) + assertScheduledFromPrevious(everySecond, previousTicks) + assertScheduledFromExecutionTime(everySecond, currentTicks, 2) + assert.isAtLeast(previousTicks[1].at - previousSecond, 2_000) + assert.isAtMost(currentTicks[1].at - currentSecond, 1_000) + })) + + it.live(`${backend}: catches up or skips stale runs and preserves the schedule across restart`, () => + Effect.gen(function*() { + const catchUpTicks: Array = [] + const skipTicks: Array = [] + const catchUpCron = ClusterCron.make({ + name: `catch-up-${backend}`, + cron: everySecond, + calculateNextRunFromPrevious: true, + execute: recordTick(catchUpTicks) + }) + const skipCron = ClusterCron.make({ + name: `skip-stale-${backend}`, + cron: everySecond, + calculateNextRunFromPrevious: true, + skipIfOlderThan: Duration.millis(500), + execute: recordTick(skipTicks) + }) + const cluster = yield* make({ + backend, + config: testConfig, + entities: Layer.merge(catchUpCron, skipCron) + }) + const runners = yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The cron schedules did not start", + Effect.sync(() => catchUpTicks.length >= 2 && skipTicks.length >= 2) + ) + for (const runner of runners) { + yield* cluster.kill(runner) + } + const catchUpBefore = catchUpTicks.length + const skipBefore = skipTicks.length + const lastScheduledBeforeRestart = catchUpTicks[catchUpBefore - 1].scheduled + const stoppedAt = yield* Clock.currentTimeMillis + yield* cluster.waitUntil( + "The cluster downtime window did not elapse", + Effect.map(Clock.currentTimeMillis, (now) => now >= stoppedAt + 3_200), + "5 seconds" + ) + assert.strictEqual(catchUpTicks.length, catchUpBefore) + assert.strictEqual(skipTicks.length, skipBefore) + + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The restarted cluster did not expose the catch-up and stale-skip difference", + Effect.sync(() => { + const caughtUp = catchUpTicks.length - catchUpBefore + const skipped = skipTicks.length - skipBefore + return caughtUp >= 3 && caughtUp >= skipped + 2 + }), + "15 seconds" + ) + yield* cluster.waitUntil( + "The stale-skipping cron did not resume", + Effect.sync(() => skipTicks.length > skipBefore) + ) + + const firstCatchUp = catchUpTicks[catchUpBefore] + const firstAfterSkip = skipTicks[skipBefore] + const firstAfterSkipScheduledAt = DateTime.toEpochMillis(DateTime.makeUnsafe(firstAfterSkip.scheduled)) + assert.strictEqual( + firstCatchUp.scheduled, + nextScheduled(everySecond, lastScheduledBeforeRestart) + ) + assert.isAtLeast(firstCatchUp.at - DateTime.toEpochMillis(DateTime.makeUnsafe(firstCatchUp.scheduled)), 2_000) + assert.isAtMost(firstAfterSkip.at - firstAfterSkipScheduledAt, 750) + assertScheduledFromPrevious(everySecond, catchUpTicks) + })) + + it.live(`${backend}: resumes without duplicate or missing ticks after the singleton owner dies`, () => + Effect.gen(function*() { + const name = `owner-failover-${backend}` + const ticks: Array = [] + const cron = ClusterCron.make({ + name, + cron: everySecond, + calculateNextRunFromPrevious: true, + execute: recordTick(ticks) + }) + const cluster = yield* make({ backend, config: testConfig, entities: cron }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The cron did not execute before failover", + Effect.sync(() => ticks.length >= 2) + ) + const owner = yield* cluster.ownerOfEntity(cronProbe(name), name) + assert.isDefined(owner) + yield* cluster.kill(owner!) + const afterKill = ticks.length + yield* cluster.waitUntil( + "The cron did not resume after its singleton owner died", + Effect.sync(() => ticks.length >= afterKill + 3), + "12 seconds" + ) + yield* cluster.waitForStableAssignments() + + assertScheduledFromPrevious(everySecond, ticks) + assert.strictEqual(new Set(ticks.map((tick) => tick.scheduled)).size, ticks.length) + assert.isTrue(ticks.slice(afterKill).every((tick) => tick.runner !== addressString(owner!.address))) + })) + + it.live(`${backend}: assigns cron singletons and executions to their shard groups`, () => + Effect.gen(function*() { + const defaultName = `default-group-${backend}` + const specialName = `special-group-${backend}` + const defaultTicks: Array = [] + const specialTicks: Array = [] + const defaultCron = ClusterCron.make({ + name: defaultName, + cron: everySecond, + execute: recordTick(defaultTicks) + }) + const specialCron = ClusterCron.make({ + name: specialName, + cron: everySecond, + shardGroup: "special", + execute: recordTick(specialTicks) + }) + const cluster = yield* make({ + backend, + config: { + availableShardGroups: ["default", "special"], + shardsPerGroup: 12 + }, + entities: Layer.merge(defaultCron, specialCron) + }) + const [defaultRunner] = yield* cluster.start(1, { assignedShardGroups: ["default"] }) + const [specialRunner] = yield* cluster.start(1, { assignedShardGroups: ["special"] }) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The shard-group cron jobs did not execute", + Effect.sync(() => defaultTicks.length >= 2 && specialTicks.length >= 2) + ) + + assert.strictEqual(yield* cluster.ownerOfEntity(cronProbe(defaultName), defaultName), defaultRunner) + assert.strictEqual( + yield* cluster.ownerOfEntity(cronProbe(specialName, "special"), specialName), + specialRunner + ) + assert.isTrue(defaultTicks.every((tick) => tick.runner === addressString(defaultRunner.address))) + assert.isTrue(specialTicks.every((tick) => tick.runner === addressString(specialRunner.address))) + })) + } +}) diff --git a/.context/effect/packages/platform/node/test/cluster-integration/Entity.test.ts b/.context/effect/packages/platform/node/test/cluster-integration/Entity.test.ts new file mode 100644 index 000000000..a0f89435f --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/Entity.test.ts @@ -0,0 +1,426 @@ +import { assert, describe, it } from "@effect/vitest" +import { Clock, Effect, Fiber, Latch, Layer, PrimaryKey, Schema, Scope } from "effect" +import { ClusterSchema, Entity, EntityResource, Singleton } from "effect/unstable/cluster" +import { Rpc } from "effect/unstable/rpc" +import { type Backend, type ClusterRunner, make } from "./harness.ts" + +class Request extends Schema.Class("ClusterEntityRequest")({ + id: Schema.String, + sequence: Schema.Number +}) { + [PrimaryKey.symbol]() { + return this.id + } +} + +const StateReply = Schema.Struct({ + generation: Schema.Number, + runner: Schema.String, + value: Schema.Number +}) + +const StateEntity = Entity.make("ClusterIntegrationState", [ + Rpc.make("Increment", { + payload: Request, + success: StateReply + }), + Rpc.make("Ordered", { + payload: Request, + success: Schema.Number + }) +]).annotateRpcs(ClusterSchema.Persisted, true) + +let orderGate = Latch.makeUnsafe(true) +let orderEntered = Latch.makeUnsafe() +let order: Array = [] +const generations = new Map() + +const addressString = (address: { readonly host: string; readonly port: number }) => `${address.host}:${address.port}` + +const StateEntityLayer = StateEntity.toLayer( + Effect.gen(function*() { + const address = yield* Entity.CurrentAddress + const runner = yield* Entity.CurrentRunnerAddress + const entityId = String(address.entityId) + const generation = (generations.get(entityId) ?? 0) + 1 + generations.set(entityId, generation) + let value = 0 + return { + Increment: () => + Effect.sync(() => ({ + generation, + runner: addressString(runner), + value: ++value + })), + Ordered: Effect.fnUntraced(function*({ payload }) { + order.push(payload.sequence) + if (payload.sequence === 1) { + orderEntered.openUnsafe() + yield* orderGate.await + } + return payload.sequence + }) + } + }), + { maxIdleTime: "1 second" } +) + +const MailboxEntity = Entity.make("ClusterIntegrationMailbox", [ + Rpc.make("Hold", { + payload: Request, + success: Schema.Number + }) +]) + +let mailboxGate = Latch.makeUnsafe(true) +let mailboxEntered = Latch.makeUnsafe() + +const MailboxEntityLayer = MailboxEntity.toLayer({ + Hold: Effect.fnUntraced(function*({ payload }) { + mailboxEntered.openUnsafe() + yield* mailboxGate.await + return payload.sequence + }) +}, { mailboxCapacity: 1 }) + +const GroupEntity = Entity.make("ClusterIntegrationSpecialGroup", [ + Rpc.make("Runner", { success: Schema.String }) +]).annotate(ClusterSchema.ShardGroup, () => "special") + +const GroupEntityLayer = GroupEntity.toLayer(Effect.gen(function*() { + const runner = yield* Entity.CurrentRunnerAddress + return { Runner: () => Effect.succeed(addressString(runner)) } +})) + +const ResourceEntity = Entity.make("ClusterIntegrationResource", [ + Rpc.make("Get", { success: Schema.Number }), + Rpc.make("Close", { success: Schema.Void }) +]) + +const resourceState = { acquired: 0, released: 0 } + +const ResourceEntityLayer = ResourceEntity.toLayer(Effect.gen(function*() { + const resource = yield* EntityResource.make({ + acquire: Effect.gen(function*() { + const closeScope = yield* EntityResource.CloseScope + return yield* Effect.acquireRelease( + Effect.sync(() => ++resourceState.acquired), + () => Effect.sync(() => resourceState.released++) + ).pipe(Scope.provide(closeScope)) + }) + }) + return { + Close: () => resource.close, + Get: () => Effect.scoped(resource.get) + } +})) + +const StandardEntities = Layer.mergeAll(StateEntityLayer, MailboxEntityLayer, ResourceEntityLayer) + +const resetOrder = () => { + orderGate = Latch.makeUnsafe() + orderEntered = Latch.makeUnsafe() + order = [] +} + +const resetMailbox = () => { + mailboxGate = Latch.makeUnsafe() + mailboxEntered = Latch.makeUnsafe() +} + +const findIdsByRunner = Effect.fnUntraced(function*( + cluster: Effect.Success>, + runners: ReadonlyArray +) { + const found = new Map() + for (let index = 0; index < 2_000 && found.size < runners.length; index++) { + const id = `entity-${index}` + const owner = yield* cluster.ownerOfEntity(StateEntity, id) + if (owner && runners.includes(owner) && !found.has(owner)) found.set(owner, id) + } + assert.strictEqual(found.size, runners.length) + return found +}) + +describe("cluster entity integration", () => { + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: routes by entity id, isolates state, and preserves mailbox order`, () => + Effect.gen(function*() { + generations.clear() + resetOrder() + const cluster = yield* make({ backend, entities: StandardEntities }) + const runners = yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const ids = yield* findIdsByRunner(cluster, runners) + const client = yield* cluster.getClient(StateEntity) + + for (const [runner, id] of ids) { + const first = yield* client(id).Increment(new Request({ id: `${id}-1`, sequence: 0 })) + const second = yield* client(id).Increment(new Request({ id: `${id}-2`, sequence: 0 })) + assert.strictEqual(first.runner, addressString(runner.address)) + assert.strictEqual(second.runner, first.runner) + assert.strictEqual(first.value, 1) + assert.strictEqual(second.value, 2) + assert.isFalse(cluster.clientSharding.hasShardId(yield* cluster.shardOfEntity(StateEntity, id))) + } + + const orderedId = ids.values().next().value! + const first = yield* client(orderedId).Ordered( + new Request({ id: `${backend}-ordered-1`, sequence: 1 }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil("The first ordered request did not start", Effect.as(orderEntered.await, true)) + const second = yield* client(orderedId).Ordered( + new Request({ id: `${backend}-ordered-2`, sequence: 2 }) + ).pipe(Effect.forkChild({ startImmediately: true })) + orderGate.openUnsafe() + assert.strictEqual(yield* Fiber.join(first), 1) + assert.strictEqual(yield* Fiber.join(second), 2) + assert.deepStrictEqual(order, [1, 2]) + + const registrations = (yield* cluster.diagnostics()).registrations + assert.strictEqual(registrations.length, runners.length) + })) + + it.live(`${backend}: reports mailbox saturation and revives idle entities with fresh state`, () => + Effect.gen(function*() { + generations.clear() + resetMailbox() + const cluster = yield* make({ backend, entities: StandardEntities }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + + const mailbox = yield* cluster.getClient(MailboxEntity) + const held = yield* mailbox("full").Hold( + new Request({ id: `${backend}-held`, sequence: 1 }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil("The mailbox request did not start", Effect.as(mailboxEntered.await, true)) + const error = yield* mailbox("full").Hold( + new Request({ id: `${backend}-rejected`, sequence: 2 }) + ).pipe(Effect.flip) + assert.strictEqual(error._tag, "MailboxFull") + mailboxGate.openUnsafe() + assert.strictEqual(yield* Fiber.join(held), 1) + + const state = yield* cluster.getClient(StateEntity) + const first = yield* state("idle").Increment( + new Request({ id: `${backend}-idle-1`, sequence: 0 }) + ) + const owner = yield* cluster.ownerOfEntity(StateEntity, "idle") + yield* cluster.waitUntil( + "The idle entity was not reaped", + Effect.map(owner!.sharding.activeEntityCount, (count) => count === 0), + "12 seconds" + ) + const revived = yield* state("idle").Increment( + new Request({ id: `${backend}-idle-2`, sequence: 0 }) + ) + assert.strictEqual(first.generation, 1) + assert.strictEqual(revived.generation, 2) + assert.strictEqual(revived.value, 1) + })) + + it.live(`${backend}: rebalances on runner addition, graceful stop, and abrupt death`, () => + Effect.gen(function*() { + resetOrder() + const cluster = yield* make({ backend, entities: StandardEntities }) + const initial = yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const before = new Map() + for (let index = 0; index < 2_000; index++) { + const id = `moving-${index}` + before.set(id, (yield* cluster.ownerOfEntity(StateEntity, id))!) + } + + const [added] = yield* cluster.start(1) + assert.strictEqual(added.index, 2) + yield* cluster.waitForStableAssignments() + let movedId: string | undefined + for (const [id, old] of before) { + if (old !== added && (yield* cluster.ownerOfEntity(StateEntity, id)) === added) { + movedId = id + break + } + } + assert.isDefined(movedId) + const client = yield* cluster.getClient(StateEntity) + const moved = yield* client(movedId!).Increment( + new Request({ id: `${backend}-moved`, sequence: 0 }) + ) + assert.strictEqual(moved.runner, addressString(added.address)) + + const stopId = (yield* findIdsByRunner(cluster, initial)).get(initial[0])! + const request = yield* client(stopId).Ordered( + new Request({ id: `${backend}-stop`, sequence: 1 }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil("The handover request did not start", Effect.as(orderEntered.await, true)) + const stopping = yield* cluster.stop(initial[0]).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil( + "The stopped runner did not hand over its entity", + Effect.map(cluster.ownerOfEntity(StateEntity, stopId), (owner) => owner !== undefined && owner !== initial[0]) + ) + orderGate.openUnsafe() + assert.strictEqual(yield* Fiber.join(request), 1) + yield* Fiber.join(stopping) + + const killId = `kill-${backend}` + const killed = yield* cluster.ownerOfEntity(StateEntity, killId) + yield* cluster.kill(killed!) + const reply = yield* client(killId).Increment( + new Request({ id: `${backend}-kill`, sequence: 0 }) + ) + assert.notStrictEqual(reply.runner, addressString(killed!.address)) + yield* cluster.waitForStableAssignments() + assert.strictEqual((yield* cluster.messageCounts()).unprocessed, 0) + })) + + it.live(`${backend}: transfers frozen row locks after expiry`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities: StandardEntities, lockMode: "row" }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const id = `frozen-${backend}` + const oldOwner = yield* cluster.ownerOfEntity(StateEntity, id) + const shard = yield* cluster.shardOfEntity(StateEntity, id) + yield* cluster.freeze(oldOwner!) + yield* cluster.waitUntil( + "The frozen runner's row lock did not expire", + Effect.map(cluster.ownerOfEntity(StateEntity, id), (owner) => owner !== undefined && owner !== oldOwner), + "12 seconds" + ) + const nextOwner = yield* cluster.ownerOfEntity(StateEntity, id) + assert.strictEqual(cluster.ownersOfShard(shard).length, 1) + assert.strictEqual(cluster.ownersOfShard(shard)[0], nextOwner) + const client = yield* cluster.getClient(StateEntity) + const reply = yield* client(id).Increment(new Request({ id: `${backend}-freeze`, sequence: 0 })) + assert.strictEqual(reply.runner, addressString(nextOwner!.address)) + yield* cluster.kill(oldOwner!) + })) + + it.live(`${backend}: retains frozen advisory locks until the session closes`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities: StandardEntities }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const id = `frozen-advisory-${backend}` + const oldOwner = yield* cluster.ownerOfEntity(StateEntity, id) + const shard = yield* cluster.shardOfEntity(StateEntity, id) + yield* cluster.freeze(oldOwner!) + const deadline = (yield* Clock.currentTimeMillis) + 3_000 + yield* cluster.waitUntil( + "The advisory-lock observation window did not elapse", + Effect.map(Clock.currentTimeMillis, (now) => now >= deadline), + "5 seconds" + ) + assert.strictEqual(cluster.ownersOfShard(shard).length, 0) + assert.deepStrictEqual(cluster.ownersOfShard(shard, true), [oldOwner]) + yield* cluster.kill(oldOwner!) + yield* cluster.waitUntil( + "The advisory lock was not handed over after its session closed", + Effect.map(cluster.ownerOfEntity(StateEntity, id), (owner) => owner !== undefined && owner !== oldOwner) + ) + })) + } + + it.live("assigns annotated entities only to runners in their shard group", () => + Effect.gen(function*() { + const entities = Layer.mergeAll(StateEntityLayer, GroupEntityLayer) + const cluster = yield* make({ + backend: "pg", + config: { availableShardGroups: ["default", "special"], shardsPerGroup: 30 }, + entities + }) + const [defaultRunner] = yield* cluster.start(1, { assignedShardGroups: ["default"] }) + const [specialRunner] = yield* cluster.start(1, { assignedShardGroups: ["special"] }) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(GroupEntity) + assert.strictEqual(yield* client("grouped").Runner(), addressString(specialRunner.address)) + assert.strictEqual(yield* cluster.ownerOfEntity(GroupEntity, "grouped"), specialRunner) + assert.strictEqual(yield* cluster.ownerOfEntity(StateEntity, "default"), defaultRunner) + })) + + it.live("runs one singleton and migrates it after owner death", () => + Effect.gen(function*() { + const singleton = { active: 0, maxActive: 0, starts: 0 } + const singletonLayer = Singleton.make( + "cluster-integration-singleton", + Effect.acquireRelease( + Effect.sync(() => { + singleton.active++ + singleton.starts++ + singleton.maxActive = Math.max(singleton.maxActive, singleton.active) + }), + () => Effect.sync(() => singleton.active--) + ) + ) + const cluster = yield* make({ + backend: "pg", + entities: Layer.merge(StateEntityLayer, singletonLayer) + }) + const runners = yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil("The singleton did not start", Effect.sync(() => singleton.active === 1)) + const firstOwner = yield* cluster.ownerOfEntity(StateEntity, "cluster-integration-singleton") + assert.isTrue(runners.includes(firstOwner!)) + yield* cluster.kill(firstOwner!) + yield* cluster.waitUntil( + "The singleton did not migrate", + Effect.sync(() => singleton.starts >= 2 && singleton.active === 1) + ) + assert.notStrictEqual( + yield* cluster.ownerOfEntity(StateEntity, "cluster-integration-singleton"), + firstOwner + ) + assert.strictEqual(singleton.maxActive, 1) + })) + + it.live("keeps EntityResource alive during movement and releases it explicitly", () => + Effect.gen(function*() { + resourceState.acquired = 0 + resourceState.released = 0 + const cluster = yield* make({ backend: "pg", entities: ResourceEntityLayer }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(ResourceEntity) + assert.strictEqual(yield* client("resource").Get(), 1) + const owner = yield* cluster.ownerOfEntity(ResourceEntity, "resource") + yield* cluster.stop(owner!) + yield* cluster.waitUntil( + "The resource entity did not move", + Effect.map(cluster.ownerOfEntity(ResourceEntity, "resource"), (next) => next !== undefined && next !== owner) + ) + assert.strictEqual(resourceState.released, 0) + assert.strictEqual(yield* client("resource").Get(), 2) + yield* client("resource").Close() + yield* cluster.waitUntil( + "The entity resource was not released", + Effect.sync(() => resourceState.released === 1) + ) + })) + + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: isolates clusters with different table prefixes`, () => + Effect.gen(function*() { + const first = yield* make({ backend, entities: StateEntityLayer, config: { shardsPerGroup: 30 } }) + const second = yield* make({ backend, entities: StateEntityLayer, config: { shardsPerGroup: 30 } }) + const [firstRunner] = yield* first.start(1) + const [secondRunner] = yield* second.start(1) + yield* first.waitForStableAssignments() + yield* second.waitForStableAssignments() + assert.notStrictEqual(first.prefix, second.prefix) + const firstRegistrations = (yield* first.diagnostics()).registrations + const secondRegistrations = (yield* second.diagnostics()).registrations + assert.deepStrictEqual(firstRegistrations.map((row) => row.address), [addressString(firstRunner.address)]) + assert.deepStrictEqual(secondRegistrations.map((row) => row.address), [addressString(secondRunner.address)]) + const firstClient = yield* first.getClient(StateEntity) + const secondClient = yield* second.getClient(StateEntity) + const firstReply = yield* firstClient("same-id").Increment( + new Request({ id: `${backend}-prefix-first`, sequence: 0 }) + ) + const secondReply = yield* secondClient("same-id").Increment( + new Request({ id: `${backend}-prefix-second`, sequence: 0 }) + ) + assert.strictEqual(firstReply.runner, addressString(firstRunner.address)) + assert.strictEqual(secondReply.runner, addressString(secondRunner.address)) + })) + } +}) diff --git a/.context/effect/packages/platform/node/test/cluster-integration/Persistence.test.ts b/.context/effect/packages/platform/node/test/cluster-integration/Persistence.test.ts new file mode 100644 index 000000000..850de48f0 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/Persistence.test.ts @@ -0,0 +1,466 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Clock, DateTime, Effect, Exit, Fiber, Latch, Option, PrimaryKey, Schema, Stream } from "effect" +import { ClusterSchema, DeliverAt, Entity } from "effect/unstable/cluster" +import { Rpc, RpcSchema } from "effect/unstable/rpc" +import { type Backend, make } from "./harness.ts" + +class KeyedPayload extends Schema.Class("ClusterPersistenceKeyedPayload")({ + id: Schema.String +}) { + [PrimaryKey.symbol]() { + return this.id + } +} + +class ScheduledPayload extends Schema.Class("ClusterPersistenceScheduledPayload")({ + deliverAt: Schema.Number, + id: Schema.String +}) { + [PrimaryKey.symbol]() { + return this.id + } + + [DeliverAt.symbol]() { + return DateTime.makeUnsafe(this.deliverAt) + } +} + +const PersistedRpc = Rpc.make("Persisted", { + payload: KeyedPayload, + success: Schema.String +}) + +const StoredReplyRpc = Rpc.make("StoredReply", { + payload: KeyedPayload, + success: Schema.String +}) + +const UninterruptibleRpc = Rpc.make("Uninterruptible", { + payload: KeyedPayload, + success: Schema.String +}).annotate(ClusterSchema.Uninterruptible, true) + +const VolatileRpc = Rpc.make("Volatile", { + payload: KeyedPayload, + success: Schema.String +}).annotate(ClusterSchema.Persisted, false) + +const TypedFailureRpc = Rpc.make("TypedFailure", { + error: Schema.String, + payload: KeyedPayload, + success: Schema.Never +}) + +const DefectRpc = Rpc.make("Defect", { + payload: KeyedPayload, + success: Schema.Never +}) + +const HealthyRpc = Rpc.make("Healthy", { + payload: KeyedPayload, + success: Schema.String +}) + +const StreamedRpc = Rpc.make("Streamed", { + payload: KeyedPayload, + success: RpcSchema.Stream(Schema.Number, Schema.Never) +}) + +const ScheduledRpc = Rpc.make("Scheduled", { + payload: ScheduledPayload, + success: Schema.Number +}) + +const PersistenceEntity = Entity.make("ClusterIntegrationPersistence", [ + PersistedRpc, + StoredReplyRpc, + UninterruptibleRpc, + VolatileRpc, + TypedFailureRpc, + DefectRpc, + HealthyRpc, + StreamedRpc, + ScheduledRpc +]).annotateRpcs(ClusterSchema.Persisted, true) + +const freshState = () => ({ + completedUninterruptible: 0, + completedVolatile: 0, + counts: new Map(), + scheduledDeliveries: [] as Array, + streamThirdEntered: Latch.makeUnsafe(), + streamThirdGate: Latch.makeUnsafe(), + uninterruptibleEntered: Latch.makeUnsafe(), + uninterruptibleGate: Latch.makeUnsafe(), + volatileEntered: Latch.makeUnsafe(), + volatileGate: Latch.makeUnsafe() +}) + +let state = freshState() + +const resetState = () => { + state = freshState() +} + +const increment = (tag: string, id: string) => { + const key = `${tag}:${id}` + const next = (state.counts.get(key) ?? 0) + 1 + state.counts.set(key, next) + return next +} + +const count = (tag: string, id: string) => state.counts.get(`${tag}:${id}`) ?? 0 + +const PersistenceEntityLayer = PersistenceEntity.toLayer({ + Defect: ({ payload }) => + Effect.sync(() => increment("Defect", payload.id)).pipe( + Effect.andThen(Effect.die(`defect:${payload.id}`)) + ), + Healthy: ({ payload }) => + Effect.sync(() => { + increment("Healthy", payload.id) + return `healthy:${payload.id}` + }), + Persisted: ({ payload }) => + Effect.sync(() => { + increment("Persisted", payload.id) + return `persisted:${payload.id}` + }), + Scheduled: Effect.fnUntraced(function*({ payload }) { + increment("Scheduled", payload.id) + const deliveredAt = yield* Clock.currentTimeMillis + state.scheduledDeliveries.push(deliveredAt) + return deliveredAt + }), + StoredReply: ({ payload }) => + Effect.sync(() => { + increment("StoredReply", payload.id) + return `stored:${payload.id}` + }), + Streamed: (request) => { + increment("Streamed", request.payload.id) + const start = Option.match(request.lastSentChunkValue, { + onNone: () => 0, + onSome: (value) => value + 1 + }) + return Stream.fromIterable([0, 1, 2, 3, 4].slice(start)).pipe( + Stream.mapEffect((value) => { + if (request.payload.id.endsWith("-restart") && value === 2) { + state.streamThirdEntered.openUnsafe() + return Effect.as(state.streamThirdGate.await, value) + } + return Effect.succeed(value) + }), + Stream.rechunk(1) + ) + }, + TypedFailure: ({ payload }) => + Effect.sync(() => increment("TypedFailure", payload.id)).pipe( + Effect.andThen(Effect.fail(`typed:${payload.id}`)) + ), + Uninterruptible: Effect.fnUntraced(function*({ payload }) { + increment("Uninterruptible", payload.id) + state.uninterruptibleEntered.openUnsafe() + yield* state.uninterruptibleGate.await + state.completedUninterruptible++ + return `uninterruptible:${payload.id}` + }), + Volatile: Effect.fnUntraced(function*({ payload }) { + increment("Volatile", payload.id) + state.volatileEntered.openUnsafe() + yield* state.volatileGate.await + state.completedVolatile++ + return `volatile:${payload.id}` + }) +}, { disableFatalDefects: true }) + +describe("cluster message persistence integration", () => { + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: delivers a persisted request sent while its runner is down exactly once`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + const owner = yield* cluster.ownerOfEntity(PersistenceEntity, "restart") + yield* cluster.kill(owner!) + + const client = yield* cluster.getClient(PersistenceEntity) + const replyFiber = yield* client("restart").Persisted( + new KeyedPayload({ id: `${backend}-restart` }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil( + "The request was not persisted while the runner was down", + Effect.map(cluster.unprocessedMessageCount, (value) => value === 1) + ) + + yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + assert.strictEqual(yield* Fiber.join(replyFiber), `persisted:${backend}-restart`) + yield* cluster.waitUntil( + "The persisted reply was not recorded", + Effect.map(cluster.repliedMessageCount, (value) => value === 1) + ) + assert.strictEqual(count("Persisted", `${backend}-restart`), 1) + assert.deepStrictEqual(yield* cluster.messageCounts(), { + failed: 0, + replied: 1, + unprocessed: 0 + }) + })) + + it.live(`${backend}: serves primary-key duplicates from the stored reply`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-stored` + const payload = new KeyedPayload({ id }) + + assert.strictEqual(yield* client("stored").StoredReply(payload), `stored:${id}`) + const firstOwner = yield* cluster.ownerOfEntity(PersistenceEntity, "stored") + yield* cluster.kill(firstOwner!) + yield* cluster.waitForStableAssignments() + + assert.strictEqual(yield* client("stored").StoredReply(payload), `stored:${id}`) + assert.strictEqual(count("StoredReply", id), 1) + assert.deepStrictEqual(yield* cluster.messageCounts(), { + failed: 0, + replied: 1, + unprocessed: 0 + }) + })) + + it.live(`${backend}: does not lose an uninterruptible request during runner shutdown`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ + backend, + config: { entityTerminationTimeout: 100 }, + entities: PersistenceEntityLayer + }) + const [owner] = yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-uninterruptible` + const replyFiber = yield* client("uninterruptible").Uninterruptible( + new KeyedPayload({ id }) + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil( + "The uninterruptible handler did not start", + Effect.as(state.uninterruptibleEntered.await, true) + ) + + const stopping = yield* cluster.stop(owner).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The uninterruptible request was not resumed by the replacement runner", + Effect.sync(() => count("Uninterruptible", id) >= 2) + ) + state.uninterruptibleGate.openUnsafe() + yield* cluster.waitUntil( + "The resumed uninterruptible request did not complete", + Effect.sync(() => state.completedUninterruptible === 1) + ) + + assert.strictEqual(yield* Fiber.join(replyFiber), `uninterruptible:${id}`) + yield* Fiber.join(stopping) + assert.strictEqual(yield* cluster.repliedMessageCount, 1) + })) + + it.live(`${backend}: does not store or redeliver a volatile request after runner failure`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ + backend, + config: { entityTerminationTimeout: 100 }, + entities: PersistenceEntityLayer + }) + const [owner] = yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-volatile` + const requestFiber = yield* client("volatile").Volatile( + new KeyedPayload({ id }), + { discard: true } + ).pipe(Effect.forkChild({ startImmediately: true })) + yield* cluster.waitUntil( + "The volatile handler did not start", + Effect.as(state.volatileEntered.await, true) + ) + + yield* cluster.kill(owner) + yield* Fiber.interrupt(requestFiber) + state.volatileGate.openUnsafe() + yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + const observationDeadline = (yield* Clock.currentTimeMillis) + 1_000 + yield* cluster.waitUntil( + "The volatile redelivery observation window did not elapse", + Effect.map(Clock.currentTimeMillis, (now) => now >= observationDeadline), + "2 seconds" + ) + + assert.strictEqual(count("Volatile", id), 1) + assert.strictEqual(state.completedVolatile, 0) + assert.deepStrictEqual(yield* cluster.messageCounts(), { + failed: 0, + replied: 0, + unprocessed: 0 + }) + })) + + it.live(`${backend}: persists typed failures and defects without wedging the mailbox`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const typedId = `${backend}-typed` + const defectId = `${backend}-defect` + + assert.strictEqual( + yield* client("failures").TypedFailure(new KeyedPayload({ id: typedId })).pipe(Effect.flip), + `typed:${typedId}` + ) + assert.strictEqual( + yield* client("failures").TypedFailure(new KeyedPayload({ id: typedId })).pipe(Effect.flip), + `typed:${typedId}` + ) + assert.strictEqual(count("TypedFailure", typedId), 1) + + const firstDefect = yield* client("failures").Defect( + new KeyedPayload({ id: defectId }) + ).pipe(Effect.exit) + assert.isTrue(Exit.isFailure(firstDefect)) + if (Exit.isFailure(firstDefect)) { + assert.include(Cause.pretty(firstDefect.cause), `defect:${defectId}`) + } + const storedDefect = yield* client("failures").Defect( + new KeyedPayload({ id: defectId }) + ).pipe(Effect.exit) + assert.isTrue(Exit.isFailure(storedDefect)) + if (Exit.isFailure(storedDefect)) { + assert.include(Cause.pretty(storedDefect.cause), `defect:${defectId}`) + } + assert.strictEqual(count("Defect", defectId), 1) + + assert.strictEqual( + yield* client("failures").Healthy(new KeyedPayload({ id: `${backend}-healthy` })), + `healthy:${backend}-healthy` + ) + yield* cluster.waitUntil( + "The failure replies were not persisted", + Effect.map(cluster.messageCounts(), (counts) => counts.failed === 2 && counts.replied === 1) + ) + assert.deepStrictEqual(yield* cluster.messageCounts(), { + failed: 2, + replied: 1, + unprocessed: 0 + }) + })) + + it.live(`${backend}: round-trips a chunked reply through storage`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-stream` + const values = yield* client("stream").Streamed(new KeyedPayload({ id })).pipe(Stream.runCollect) + assert.deepStrictEqual(Array.from(values), [0, 1, 2, 3, 4]) + yield* cluster.waitUntil( + "The terminal stream reply was not persisted", + Effect.map(cluster.repliedMessageCount, (value) => value === 1) + ) + assert.strictEqual(count("Streamed", id), 1) + })) + + it.live(`${backend}: resumes a persisted stream after its runner is killed`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + const [owner] = yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-stream-restart` + const received: Array = [] + const valuesFiber = yield* client("stream-restart").Streamed(new KeyedPayload({ id })).pipe( + Stream.tap((value) => Effect.sync(() => received.push(value))), + Stream.runCollect, + Effect.forkChild({ startImmediately: true }) + ) + yield* cluster.waitUntil( + "The stream did not deliver two chunks before blocking the third", + Effect.sync(() => received.length === 2 && received[0] === 0 && received[1] === 1) + ) + yield* cluster.waitUntil( + "The stream handler did not block before delivering its third chunk", + Effect.as(state.streamThirdEntered.await, true) + ) + + yield* cluster.kill(owner) + yield* cluster.start(1) + yield* cluster.waitForStableAssignments() + yield* cluster.waitUntil( + "The replacement runner did not resume the persisted stream", + Effect.sync(() => count("Streamed", id) === 2) + ) + state.streamThirdGate.openUnsafe() + + assert.deepStrictEqual(Array.from(yield* Fiber.join(valuesFiber)), [0, 1, 2, 3, 4]) + yield* cluster.waitUntil( + "The terminal stream reply was not persisted after recovery", + Effect.map( + cluster.messageCounts(), + (counts) => counts.replied === 1 && counts.unprocessed === 0 + ) + ) + assert.deepStrictEqual(yield* cluster.messageCounts(), { + failed: 0, + replied: 1, + unprocessed: 0 + }) + })) + + it.live(`${backend}: delivers scheduled messages only after their deadline`, () => + Effect.gen(function*() { + resetState() + const cluster = yield* make({ backend, entities: PersistenceEntityLayer }) + yield* cluster.start(2) + yield* cluster.waitForStableAssignments() + const client = yield* cluster.getClient(PersistenceEntity) + const id = `${backend}-scheduled` + const deliverAt = (yield* Clock.currentTimeMillis) + 1_500 + const replyFiber = yield* client("scheduled").Scheduled( + new ScheduledPayload({ deliverAt, id }) + ).pipe(Effect.forkChild({ startImmediately: true })) + + yield* cluster.waitUntil( + "The early-delivery observation point was not reached", + Effect.map(Clock.currentTimeMillis, (now) => now >= deliverAt - 500), + "2 seconds" + ) + assert.strictEqual(count("Scheduled", id), 0) + assert.deepStrictEqual(state.scheduledDeliveries, []) + + yield* cluster.waitUntil( + "The scheduled message was not delivered after its deadline", + Effect.sync(() => state.scheduledDeliveries.length === 1), + "5 seconds" + ) + yield* cluster.waitUntil( + "The scheduled reply was not persisted", + Effect.map(cluster.repliedMessageCount, (value) => value === 1) + ) + const deliveredAt = yield* Fiber.join(replyFiber) + assert.isAtLeast(deliveredAt, deliverAt) + assert.strictEqual(state.scheduledDeliveries[0], deliveredAt) + })) + } +}) diff --git a/.context/effect/packages/platform/node/test/cluster-integration/README.md b/.context/effect/packages/platform/node/test/cluster-integration/README.md new file mode 100644 index 000000000..590f6da63 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/README.md @@ -0,0 +1,36 @@ +# Cluster integration tests + +This suite runs multi-runner clusters against shared PostgreSQL and MySQL +containers. It is excluded from the default test projects and only registered +when `EFFECT_CLUSTER_TESTS=1`. + +```sh +EFFECT_CLUSTER_TESTS=1 pnpm test-cluster +``` + +Docker must be running. Vitest global setup starts one `postgres:alpine` and one +`mysql:lts` container for the entire project. Every cluster uses a unique table +prefix, and every runner listens on an operating-system-assigned port. + +## Harness + +`harness.ts` exposes: + +- `make({ backend, entities, lockMode, config })` to create a scoped cluster harness. +- `start(count, { assignedShardGroups, runnerShardWeight })` to start in-process socket runners and a client over msgpack. +- `stop(runner)` for graceful deregistration and shard handoff. +- `kill(runner)` for abrupt teardown without deregistration or explicit lock cleanup. +- `freeze(runner)` to suspend SQL heartbeats and lock refresh while leaving the runner's sockets and reserved SQL connection open. +- `waitUntil`, `waitForStableAssignments`, and `waitForEntityOwner` for deadline-based polling with cluster diagnostics on failure. +- `clientSharding` and `ownersOfShard` for direct shard ownership assertions. +- `messageCounts`, `unprocessedMessageCount`, `repliedMessageCount`, and `failedMessageCount` for storage assertions scoped to the cluster prefix. + +Advisory locks are owned by the reserved database session. A frozen advisory-lock +runner therefore keeps its locks until it is stopped or killed. Row-lock mode +uses expiry-driven takeover while frozen. + +## Adding a test + +Add `*.test.ts` under this directory. Test files define entities and assertions; +all cluster startup, lifecycle, waiting, and storage inspection belongs in the +harness. Use the harness polling helpers instead of calling `Effect.sleep`. diff --git a/.context/effect/packages/platform/node/test/cluster-integration/Smoke.test.ts b/.context/effect/packages/platform/node/test/cluster-integration/Smoke.test.ts new file mode 100644 index 000000000..58cc5215e --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/Smoke.test.ts @@ -0,0 +1,55 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, PrimaryKey, Schema } from "effect" +import { ClusterSchema, Entity } from "effect/unstable/cluster" +import { Rpc } from "effect/unstable/rpc" +import { type Backend, make } from "./harness.ts" + +class Ping extends Schema.Class("Ping")({ + id: Schema.String +}) { + [PrimaryKey.symbol]() { + return this.id + } +} + +const TestEntity = Entity.make("ClusterIntegrationSmoke", [ + Rpc.make("Ping", { + payload: Ping, + success: Schema.String + }) +]).annotateRpcs(ClusterSchema.Persisted, true) + +const TestEntityLayer = TestEntity.toLayer({ + Ping: ({ payload }) => Effect.succeed(`pong:${payload.id}`) +}) + +describe("cluster integration smoke", () => { + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: persists messages and rebalances after an abrupt runner death`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities: TestEntityLayer }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + + const client = yield* cluster.getClient(TestEntity) + assert.strictEqual( + yield* client("entity-1").Ping(new Ping({ id: "request-1" })), + "pong:request-1" + ) + + yield* cluster.waitUntil( + "The smoke entity had no active owner", + Effect.map(cluster.ownerOfEntity(TestEntity, "entity-1"), (owner) => owner !== undefined) + ) + const owner = yield* cluster.ownerOfEntity(TestEntity, "entity-1") + yield* cluster.kill(owner!) + yield* cluster.waitForStableAssignments() + + assert.strictEqual( + yield* client("entity-1").Ping(new Ping({ id: "request-2" })), + "pong:request-2" + ) + assert.strictEqual(yield* cluster.repliedMessageCount, 2) + })) + } +}) diff --git a/.context/effect/packages/platform/node/test/cluster-integration/Workflow.test.ts b/.context/effect/packages/platform/node/test/cluster-integration/Workflow.test.ts new file mode 100644 index 000000000..c83137759 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/Workflow.test.ts @@ -0,0 +1,426 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Clock, Duration, Effect, Exit, Fiber, Latch, Layer, Option, Schema } from "effect" +import { ClusterWorkflowEngine, Entity, EntityId } from "effect/unstable/cluster" +import { PersistedQueue } from "effect/unstable/persistence" +import { Rpc } from "effect/unstable/rpc" +import { + Activity, + DurableClock, + DurableDeferred, + DurableQueue, + Workflow, + WorkflowEngine +} from "effect/unstable/workflow" +import { type Backend, type ClusterRunner, make } from "./harness.ts" + +const EndToEndWorkflow = Workflow.make("ClusterIntegrationEndToEnd", { + payload: { + id: Schema.String, + value: Schema.Number + }, + success: Schema.Number, + idempotencyKey: ({ id }) => id +}) + +let endToEndGate = Latch.makeUnsafe(true) +let endToEndEntered = Latch.makeUnsafe() +const endToEndRuns = new Map() + +const EndToEndWorkflowLayer = EndToEndWorkflow.toLayer(({ id, value }) => + Activity.make({ + name: "EndToEnd", + success: Schema.Number, + execute: Effect.gen(function*() { + endToEndRuns.set(id, (endToEndRuns.get(id) ?? 0) + 1) + endToEndEntered.openUnsafe() + yield* endToEndGate.await + return value + 1 + }) + }) +) + +const ReplayGate = DurableDeferred.make("ClusterIntegrationReplayGate", { + success: Schema.String +}) + +const ReplayWorkflow = Workflow.make("ClusterIntegrationReplay", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const replayRuns = new Map() + +const ReplayWorkflowLayer = ReplayWorkflow.toLayer(Effect.fnUntraced(function*({ id }) { + yield* Activity.make({ + name: "BeforeSuspension", + execute: Effect.sync(() => replayRuns.set(id, (replayRuns.get(id) ?? 0) + 1)) + }) + return yield* DurableDeferred.await(ReplayGate) +})) + +const RestartGate = DurableDeferred.make("ClusterIntegrationRestartGate", { + success: Schema.String +}) + +const RestartWorkflow = Workflow.make("ClusterIntegrationRestart", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const RestartWorkflowLayer = RestartWorkflow.toLayer(() => DurableDeferred.await(RestartGate)) + +class RetryError extends Schema.Error("ClusterIntegrationRetryError")({ + _tag: Schema.tag("ClusterIntegrationRetryError"), + attempt: Schema.Number +}) {} + +const RetryWorkflow = Workflow.make("ClusterIntegrationRetry", { + payload: { + id: Schema.String, + succeed: Schema.Boolean + }, + success: Schema.Number, + error: RetryError, + idempotencyKey: ({ id }) => id +}) + +const retryAttempts = new Map>() + +const RetryWorkflowLayer = RetryWorkflow.toLayer(({ id, succeed }) => + Activity.make({ + name: "Retry", + success: Schema.Number, + error: RetryError, + execute: Effect.gen(function*() { + const attempt = yield* Activity.CurrentAttempt + const attempts = retryAttempts.get(id) ?? [] + attempts.push(attempt) + retryAttempts.set(id, attempts) + if (succeed && attempt === 3) return attempt + return yield* new RetryError({ attempt }) + }) + }).pipe(Activity.retry({ times: 2 })) +) + +const ClockWorkflow = Workflow.make("ClusterIntegrationClock", { + payload: { id: Schema.String }, + success: Schema.Number, + idempotencyKey: ({ id }) => id +}) + +const ClockWorkflowLayer = ClockWorkflow.toLayer(() => + Effect.gen(function*() { + yield* DurableClock.sleep({ + name: "RestartSleep", + duration: "1 second", + inMemoryThreshold: Duration.zero + }) + return yield* Clock.currentTimeMillis + }) +) + +const Queue = DurableQueue.make({ + name: "ClusterIntegrationQueue", + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const QueueWorkflow = Workflow.make("ClusterIntegrationQueue", { + payload: { id: Schema.String }, + success: Schema.String, + idempotencyKey: ({ id }) => id +}) + +const QueueWorkflowLayer = QueueWorkflow.toLayer(({ id }) => DurableQueue.process(Queue, { id })) +const queueRuns = new Map() +let queueWorkerGate = Latch.makeUnsafe() + +const QueueWorkerLayer = Layer.effectDiscard( + Effect.forkScoped( + Effect.suspend(() => queueWorkerGate.await).pipe( + Effect.andThen(DurableQueue.makeWorker(Queue, ({ id }) => + Effect.sync(() => { + queueRuns.set(id, (queueRuns.get(id) ?? 0) + 1) + return `processed:${id}` + }))) + ) + ) +) + +const InterruptGate = DurableDeferred.make("ClusterIntegrationInterruptGate") + +const InterruptWorkflow = Workflow.make("ClusterIntegrationInterrupt", { + payload: { id: Schema.String }, + idempotencyKey: ({ id }) => id +}) + +const InterruptWorkflowLayer = InterruptWorkflow.toLayer(() => DurableDeferred.await(InterruptGate)) + +const CompleteDeferred = Rpc.make("CompleteDeferred", { + payload: { + token: DurableDeferred.Token, + value: Schema.String + }, + success: Schema.String +}) + +const DeferredControl = Entity.make("ClusterIntegrationDeferredControl", [CompleteDeferred]) + +const DeferredControlLayer = DeferredControl.toLayer(Effect.gen(function*() { + const runner = yield* Entity.CurrentRunnerAddress + return { + CompleteDeferred: ({ payload }) => + DurableDeferred.succeed(RestartGate, payload).pipe( + Effect.as(`${runner.host}:${runner.port}`) + ) + } +})) + +const Workflows = Layer.mergeAll( + EndToEndWorkflowLayer, + ReplayWorkflowLayer, + RestartWorkflowLayer, + RetryWorkflowLayer, + ClockWorkflowLayer, + QueueWorkflowLayer, + QueueWorkerLayer, + InterruptWorkflowLayer, + DeferredControlLayer +) + +const entities = ({ prefix }: { readonly prefix: string }) => { + const queue = PersistedQueue.layer.pipe( + Layer.provideMerge(PersistedQueue.layerStoreSql({ + tableName: `${prefix}_workflow_queue`, + pollInterval: 100, + lockRefreshInterval: 500, + lockExpiration: 1_750 + })) + ) + return Workflows.pipe( + Layer.provide(queue), + Layer.provide(ClusterWorkflowEngine.layer), + Layer.orDie + ) +} + +type Cluster = Effect.Success> + +const withWorkflow = ( + cluster: Cluster, + effect: Effect.Effect +) => Effect.provideService(effect, WorkflowEngine.WorkflowEngine, cluster.workflowEngine) + +const waitForSuspended = Effect.fnUntraced(function*< + Name extends string, + Payload extends Workflow.AnyStructSchema, + Success extends Schema.Top, + Error extends Schema.Top +>( + cluster: Cluster, + workflow: Workflow.Workflow, + executionId: string +) { + yield* cluster.waitUntil( + `${workflow._tag}/${executionId} did not suspend`, + Effect.map(workflow.poll(executionId), (result) => Option.isSome(result) && result.value._tag === "Suspended") + ) +}) + +const waitForComplete = Effect.fnUntraced(function*< + Name extends string, + Payload extends Workflow.AnyStructSchema, + Success extends Schema.Top, + Error extends Schema.Top +>( + cluster: Cluster, + workflow: Workflow.Workflow, + executionId: string +) { + let complete: Workflow.Complete | undefined + yield* cluster.waitUntil( + `${workflow._tag}/${executionId} did not complete`, + Effect.map(workflow.poll(executionId), (result) => { + if (Option.isNone(result) || result.value._tag !== "Complete") return false + complete = result.value + return true + }) + ) + return complete! +}) + +const restart = Effect.fnUntraced(function*(cluster: Cluster) { + const running = cluster.runners.filter((runner) => runner.state() === "running") + yield* Effect.forEach(running, cluster.kill, { concurrency: "unbounded", discard: true }) + const replacements = yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + return replacements +}) + +const workflowOwner = (cluster: Cluster, executionId: string) => { + const shard = cluster.clientSharding.getShardId(EntityId.make(executionId), "default") + return cluster.ownersOfShard(shard)[0] +} + +const findControlOnAnotherRunner = Effect.fnUntraced(function*( + cluster: Cluster, + workflowRunner: ClusterRunner +) { + for (let index = 0; index < 2_000; index++) { + const id = `control-${index}` + const owner = yield* cluster.ownerOfEntity(DeferredControl, id) + if (owner !== undefined && owner !== workflowRunner) return [id, owner] as const + } + return yield* Effect.die("Could not route deferred control to another runner") +}) + +describe("cluster workflow integration", () => { + for (const backend of ["pg", "mysql"] satisfies ReadonlyArray) { + it.live(`${backend}: executes end to end and deduplicates concurrent callers`, () => + Effect.gen(function*() { + const id = `${backend}-end-to-end` + endToEndGate = Latch.makeUnsafe() + endToEndEntered = Latch.makeUnsafe() + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + + const first = yield* withWorkflow(cluster, EndToEndWorkflow.execute({ id, value: 41 })).pipe( + Effect.forkChild({ startImmediately: true }) + ) + yield* cluster.waitUntil("The end-to-end activity did not start", Effect.as(endToEndEntered.await, true)) + const second = yield* withWorkflow(cluster, EndToEndWorkflow.execute({ id, value: 41 })).pipe( + Effect.forkChild({ startImmediately: true }) + ) + endToEndGate.openUnsafe() + + assert.strictEqual(yield* Fiber.join(first), 42) + assert.strictEqual(yield* Fiber.join(second), 42) + assert.strictEqual(endToEndRuns.get(id), 1) + })) + + it.live(`${backend}: replays completed activities after the owner dies`, () => + Effect.gen(function*() { + const id = `${backend}-replay` + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const executionId = yield* withWorkflow(cluster, ReplayWorkflow.execute({ id }, { discard: true })) + yield* waitForSuspended(cluster, ReplayWorkflow, executionId) + assert.strictEqual(replayRuns.get(id), 1) + + const owner = workflowOwner(cluster, executionId) + assert.isDefined(owner) + yield* cluster.kill(owner!) + yield* cluster.waitForStableAssignments() + const token = DurableDeferred.tokenFromExecutionId(ReplayGate, { workflow: ReplayWorkflow, executionId }) + yield* withWorkflow(cluster, DurableDeferred.succeed(ReplayGate, { token, value: "resumed" })) + const result = yield* waitForComplete(cluster, ReplayWorkflow, executionId) + + assert.deepStrictEqual(result.exit, Exit.succeed("resumed")) + assert.strictEqual(replayRuns.get(id), 1) + })) + + it.live(`${backend}: resumes deferred workflows across a whole-cluster restart from another runner`, () => + Effect.gen(function*() { + const id = `${backend}-restart` + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const executionId = yield* withWorkflow(cluster, RestartWorkflow.execute({ id }, { discard: true })) + yield* waitForSuspended(cluster, RestartWorkflow, executionId) + yield* restart(cluster) + + const owner = workflowOwner(cluster, executionId) + assert.isDefined(owner) + const [controlId, controlOwner] = yield* findControlOnAnotherRunner(cluster, owner!) + const control = yield* cluster.getClient(DeferredControl) + const token = DurableDeferred.tokenFromExecutionId(RestartGate, { workflow: RestartWorkflow, executionId }) + const completedBy = yield* control(controlId).CompleteDeferred({ token, value: "after-restart" }) + assert.strictEqual(completedBy, `${controlOwner.address.host}:${controlOwner.address.port}`) + + const result = yield* waitForComplete(cluster, RestartWorkflow, executionId) + assert.deepStrictEqual(result.exit, Exit.succeed("after-restart")) + })) + + it.live(`${backend}: applies activity retry policy and preserves the exhausted error`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const successId = `${backend}-retry-success` + const failureId = `${backend}-retry-failure` + + assert.strictEqual( + yield* withWorkflow(cluster, RetryWorkflow.execute({ id: successId, succeed: true })), + 3 + ) + const error = yield* withWorkflow( + cluster, + RetryWorkflow.execute({ id: failureId, succeed: false }) + ).pipe(Effect.flip) + + assert.deepStrictEqual(retryAttempts.get(successId), [1, 2, 3]) + assert.deepStrictEqual(retryAttempts.get(failureId), [1, 2, 3]) + assert.strictEqual(error._tag, "ClusterIntegrationRetryError") + assert.strictEqual(error.attempt, 3) + })) + + it.live(`${backend}: wakes a durable clock after a whole-cluster restart`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const started = yield* Clock.currentTimeMillis + const executionId = yield* withWorkflow( + cluster, + ClockWorkflow.execute({ id: `${backend}-clock` }, { discard: true }) + ) + yield* waitForSuspended(cluster, ClockWorkflow, executionId) + yield* restart(cluster) + + const result = yield* waitForComplete(cluster, ClockWorkflow, executionId) + assert(Exit.isSuccess(result.exit)) + assert.isAtLeast(result.exit.value - started, 900) + })) + + it.live(`${backend}: persists queued work across restart and consumes it once`, () => + Effect.gen(function*() { + const id = `${backend}-queue` + queueWorkerGate = Latch.makeUnsafe() + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const executionId = yield* withWorkflow(cluster, QueueWorkflow.execute({ id }, { discard: true })) + yield* waitForSuspended(cluster, QueueWorkflow, executionId) + yield* restart(cluster) + queueWorkerGate.openUnsafe() + + const result = yield* waitForComplete(cluster, QueueWorkflow, executionId) + assert.deepStrictEqual(result.exit, Exit.succeed(`processed:${id}`)) + assert.strictEqual(queueRuns.get(id), 1) + })) + + it.live(`${backend}: persists interruption across a whole-cluster restart`, () => + Effect.gen(function*() { + const cluster = yield* make({ backend, entities }) + yield* cluster.start(3) + yield* cluster.waitForStableAssignments() + const executionId = yield* withWorkflow( + cluster, + InterruptWorkflow.execute({ id: `${backend}-interrupt` }, { discard: true }) + ) + yield* waitForSuspended(cluster, InterruptWorkflow, executionId) + yield* withWorkflow(cluster, InterruptWorkflow.interrupt(executionId)) + yield* restart(cluster) + + const result = yield* waitForComplete(cluster, InterruptWorkflow, executionId) + assert(Exit.isFailure(result.exit)) + assert.isTrue(Exit.hasInterrupts(result.exit)) + assert.isTrue(Cause.hasInterrupts(result.exit.cause)) + })) + } +}) diff --git a/.context/effect/packages/platform/node/test/cluster-integration/globalSetup.ts b/.context/effect/packages/platform/node/test/cluster-integration/globalSetup.ts new file mode 100644 index 000000000..52dcf8b86 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/globalSetup.ts @@ -0,0 +1,38 @@ +import { MySqlContainer } from "@testcontainers/mysql" +import { PostgreSqlContainer } from "@testcontainers/postgresql" +import type { TestProject } from "vitest/node" + +export interface ClusterDatabases { + readonly mysql: string + readonly pg: string +} + +declare module "vitest" { + export interface ProvidedContext { + readonly clusterDatabases: ClusterDatabases + } +} + +const makeMysqlContainer = () => + new MySqlContainer("mysql:lts").withHealthCheck({ + test: [ + "CMD-SHELL", + "MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysqladmin ping --protocol TCP --host 127.0.0.1 --user root --silent" + ], + interval: 250, + timeout: 1000, + retries: 1000 + }) + +export default function setup(project: TestProject) { + return Promise.all([ + new PostgreSqlContainer("postgres:alpine").start(), + makeMysqlContainer().start() + ]).then(([pg, mysql]) => { + project.provide("clusterDatabases", { + mysql: mysql.getConnectionUri(), + pg: pg.getConnectionUri() + }) + return () => Promise.all([pg.stop(), mysql.stop()]).then(() => undefined) + }) +} diff --git a/.context/effect/packages/platform/node/test/cluster-integration/harness.ts b/.context/effect/packages/platform/node/test/cluster-integration/harness.ts new file mode 100644 index 000000000..11992f7ea --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster-integration/harness.ts @@ -0,0 +1,469 @@ +import { NodeClusterSocket, NodeCrypto, NodeSocketServer } from "@effect/platform-node" +import { MysqlClient } from "@effect/sql-mysql2" +import { PgClient } from "@effect/sql-pg" +import { Clock, Context, Duration, Effect, Exit, Latch, Layer, Option, Redacted, Scope } from "effect" +import { + ClusterWorkflowEngine, + type Entity, + EntityId, + type MessageStorage, + type Runner as RunnerModel, + RunnerAddress, + RunnerHealth, + Runners, + RunnerStorage, + ShardId, + Sharding, + ShardingConfig, + SocketRunner, + SqlMessageStorage, + SqlRunnerStorage +} from "effect/unstable/cluster" +import type { Rpc } from "effect/unstable/rpc" +import { RpcSerialization } from "effect/unstable/rpc" +import * as SocketServer from "effect/unstable/socket/SocketServer" +import { SqlClient } from "effect/unstable/sql" +import { WorkflowEngine } from "effect/unstable/workflow" +import { inject } from "vitest" + +export type Backend = "mysql" | "pg" +export type LockMode = "advisory" | "row" + +export interface ClusterRunner { + readonly address: RunnerAddress.RunnerAddress + readonly index: number + readonly shardGroups: ReadonlyArray + readonly sharding: Sharding.Sharding["Service"] + readonly state: () => "frozen" | "killed" | "running" | "stopped" +} + +export interface MessageCounts { + readonly failed: number + readonly replied: number + readonly unprocessed: number +} + +export interface MakeOptions { + readonly backend: Backend + readonly config?: Partial | undefined + readonly entities: + | RunnerEntities + | ((options: { readonly prefix: string }) => RunnerEntities) + readonly lockMode?: LockMode | undefined + readonly runnerLayer?: RunnerLayer | undefined +} + +export interface StartOptions { + readonly assignedShardGroups?: ReadonlyArray | undefined + readonly runnerShardWeight?: number | undefined +} + +type HarnessConfig = Partial & { + readonly shardLockDisableAdvisory: boolean +} + +type RunnerEntities = Layer.Layer< + never, + never, + Sharding.Sharding | MessageStorage.MessageStorage | SqlClient.SqlClient +> + +interface RegistrationRow { + readonly address: string + readonly healthy: boolean | number + readonly last_heartbeat: Date | string + readonly runner: unknown +} + +interface MessageRow { + readonly processed: boolean | number + readonly reply_payload: string | Record | null +} + +interface RunnerEntry extends ClusterRunner { + readonly controller: ReturnType + readonly scope: Scope.Closeable + setState(state: ReturnType): void +} + +const clusterConfig = { + entityMaxIdleTime: 3_000, + entityMessagePollInterval: 500, + refreshAssignmentsInterval: 150, + shardLockExpiration: 1_750, + shardLockRefreshInterval: 500 +} as const + +let nextCluster = 0 + +const makeRunnerStorageController = (storage: RunnerStorage.RunnerStorage["Service"]) => { + const gate = Latch.makeUnsafe(true) + const refreshPaused = Latch.makeUnsafe() + const syncPaused = Latch.makeUnsafe() + let mode: "frozen" | "killed" | "running" = "running" + let lastRunners: Array = [] + + const waitWhileFrozen = ( + paused: Latch.Latch, + effect: Effect.Effect, + onKilled: () => A + ): Effect.Effect => + Effect.suspend(() => { + if (mode === "running") return effect + if (mode === "killed") return Effect.succeed(onKilled()) + paused.openUnsafe() + return gate.await.pipe( + Effect.uninterruptible, + Effect.andThen(Effect.suspend(() => mode === "running" ? effect : Effect.succeed(onKilled()))) + ) + }) + + const controlled = RunnerStorage.RunnerStorage.of({ + ...storage, + getRunners: waitWhileFrozen( + syncPaused, + storage.getRunners.pipe(Effect.tap((runners) => Effect.sync(() => lastRunners = runners))), + () => lastRunners + ), + refresh: (address, shardIds) => { + const shards = Array.from(shardIds) + return waitWhileFrozen(refreshPaused, storage.refresh(address, shards), () => shards) + }, + release: (address, shardId) => mode === "killed" ? Effect.void : storage.release(address, shardId), + releaseAll: (address) => mode === "killed" ? Effect.void : storage.releaseAll(address), + unregister: (address) => mode === "killed" ? Effect.void : storage.unregister(address) + }) + + return { + controlled, + freeze: Effect.sync(() => { + mode = "frozen" + gate.closeUnsafe() + }).pipe(Effect.andThen(Effect.all([refreshPaused.await, syncPaused.await], { discard: true }))), + kill: Effect.sync(() => { + mode = "killed" + gate.openUnsafe() + }), + resume: Effect.sync(() => { + mode = "running" + gate.openUnsafe() + }) + } +} + +const RunnerHealthLive = RunnerHealth.layerPing.pipe( + Layer.provide(Runners.layerRpc), + Layer.provide(NodeClusterSocket.layerClientProtocol) +) + +export const socketRunnerLayer = ( + address: RunnerAddress.RunnerAddress, + entities: RunnerEntities, + socketServer: SocketServer.SocketServer["Service"], + config: HarnessConfig +) => + entities.pipe( + Layer.provideMerge(SocketRunner.layer), + Layer.provide(RunnerHealthLive), + Layer.provide(Layer.succeed(SocketServer.SocketServer, socketServer)), + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer({ + ...config, + runnerAddress: Option.some(address) + })), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +export type RunnerLayer = typeof socketRunnerLayer + +const clientLayer = (config: HarnessConfig) => + SocketRunner.layerClientOnly.pipe( + Layer.provide(NodeClusterSocket.layerClientProtocol), + Layer.provide(ShardingConfig.layer(config)), + Layer.provide(RpcSerialization.layerMsgPack) + ) + +const parseReply = (payload: MessageRow["reply_payload"]): Record | undefined => { + if (payload === null) return undefined + return typeof payload === "string" ? JSON.parse(payload) : payload +} + +export const make = Effect.fnUntraced(function*(options: MakeOptions) { + const parentScope = yield* Effect.scope + const prefix = `cluster_${process.pid}_${nextCluster++}` + const config = { + ...clusterConfig, + ...options.config, + shardLockDisableAdvisory: options.lockMode === "row" + } + const databases = inject("clusterDatabases") + const databaseLayer = options.backend === "pg" + ? PgClient.layer({ url: Redacted.make(databases.pg), maxConnections: 32 }) + : MysqlClient.layer({ url: Redacted.make(databases.mysql), maxConnections: 32 }) + const database = yield* Layer.buildWithScope(databaseLayer, parentScope) + const sql = Context.get(database, SqlClient.SqlClient).withoutTransforms() + const messageStorage = yield* SqlMessageStorage.layerWith({ prefix }).pipe( + Layer.provide(NodeCrypto.layer), + Layer.provide(ShardingConfig.layer(config)), + Layer.buildWithScope(parentScope), + Effect.provide(database) + ) + const shared = Context.merge(database, messageStorage) + const entities = typeof options.entities === "function" ? options.entities({ prefix }) : options.entities + const runners: Array = [] + + const makeRunnerStorage = Effect.fnUntraced(function*( + scope: Scope.Closeable, + storageConfig: HarnessConfig = config + ) { + return yield* SqlRunnerStorage.layerWith({ prefix }).pipe( + Layer.provide(ShardingConfig.layer(storageConfig)), + Layer.orDie, + Layer.buildWithScope(scope), + Effect.provide(database) + ) + }) + + const clientScope = yield* Scope.fork(parentScope) + const clientStorage = yield* makeRunnerStorage(clientScope) + const clientBase = yield* clientLayer(config).pipe( + Layer.buildWithScope(clientScope), + Effect.provide(Context.merge(shared, clientStorage)) + ) + const workflowEngine = yield* ClusterWorkflowEngine.make.pipe( + Effect.provide(Context.merge(shared, clientBase)) + ) + const client = Context.add(clientBase, WorkflowEngine.WorkflowEngine, workflowEngine) + const clientSharding = Context.get(client, Sharding.Sharding) + let nextRunnerIndex = 0 + const startRunner = Effect.fnUntraced(function*(index: number, startOptions?: StartOptions) { + const scope = yield* Scope.fork(parentScope) + const runnerConfig: HarnessConfig = { + ...config, + ...(startOptions?.assignedShardGroups === undefined + ? undefined + : { assignedShardGroups: startOptions.assignedShardGroups }), + ...(startOptions?.runnerShardWeight === undefined + ? undefined + : { runnerShardWeight: startOptions.runnerShardWeight }) + } + const serverContext = yield* NodeSocketServer.layer({ host: "127.0.0.1", port: 0 }).pipe( + Layer.buildWithScope(scope) + ) + const socketServer = Context.get(serverContext, SocketServer.SocketServer) + if (socketServer.address._tag !== "TcpAddress") { + return yield* Effect.die("Expected a TCP socket server") + } + const address = RunnerAddress.make("127.0.0.1", socketServer.address.port) + const rawStorageContext = yield* makeRunnerStorage(scope, runnerConfig) + const controller = makeRunnerStorageController(Context.get(rawStorageContext, RunnerStorage.RunnerStorage)) + const storage = Context.make(RunnerStorage.RunnerStorage, controller.controlled) + const context = yield* (options.runnerLayer ?? socketRunnerLayer)( + address, + entities, + socketServer, + runnerConfig + ).pipe( + Layer.buildWithScope(scope), + Effect.provide(Context.mergeAll(shared, storage)) + ) + const sharding = Context.get(context, Sharding.Sharding) + let state: ReturnType = "running" + const runner: RunnerEntry = { + address, + controller, + index, + shardGroups: runnerConfig.assignedShardGroups ?? ShardingConfig.defaults.assignedShardGroups, + scope, + setState(next) { + state = next + }, + sharding, + state: () => state + } + runners.push(runner) + return runner as ClusterRunner + }) + + const start = Effect.fnUntraced(function*(runnerCount: number, startOptions?: StartOptions) { + return yield* Effect.forEach( + Array.from({ length: runnerCount }, () => nextRunnerIndex++), + (index) => startRunner(index, startOptions) + ) + }) + + const entryFor = (runner: ClusterRunner) => runners.find((entry) => entry === runner)! + + const stop = Effect.fnUntraced(function*(runner: ClusterRunner) { + const entry = entryFor(runner) + if (entry.state() === "stopped" || entry.state() === "killed") return + if (entry.state() === "frozen") yield* entry.controller.resume + entry.setState("stopped") + yield* Scope.close(entry.scope, Exit.void) + }) + + const kill = Effect.fnUntraced(function*(runner: ClusterRunner) { + const entry = entryFor(runner) + if (entry.state() === "stopped" || entry.state() === "killed") return + entry.setState("killed") + yield* entry.controller.kill + yield* Scope.close(entry.scope, Exit.void) + }) + + const freeze = Effect.fnUntraced(function*(runner: ClusterRunner) { + const entry = entryFor(runner) + if (entry.state() !== "running") return + entry.setState("frozen") + yield* entry.controller.freeze + }) + + const assignmentMap = () => { + const assignments: Record> = {} + const groups = config.availableShardGroups ?? ShardingConfig.defaults.availableShardGroups + const shardsPerGroup = config.shardsPerGroup ?? ShardingConfig.defaults.shardsPerGroup + for (const group of groups) { + for (let id = 1; id <= shardsPerGroup; id++) { + const shard = ShardId.make(group, id) + assignments[shard.toString()] = runners + .filter((runner) => runner.state() === "running" && runner.sharding.hasShardId(shard)) + .map((runner) => `${runner.address.host}:${runner.address.port}`) + } + } + return assignments + } + + const ownersOfShard = (shard: ShardId.ShardId, includeInactive = false) => + runners.filter((runner) => (includeInactive || runner.state() === "running") && runner.sharding.hasShardId(shard)) + + const messageCounts = Effect.fnUntraced(function*() { + const messages = sql(`${prefix}_messages`) + const replies = sql(`${prefix}_replies`) + const rows = yield* sql` + SELECT m.processed, r.payload AS reply_payload + FROM ${messages} m + LEFT JOIN ${replies} r ON r.id = m.last_reply_id + WHERE m.kind = 0 + ` + let failed = 0 + let replied = 0 + let unprocessed = 0 + for (const row of rows) { + if (!row.processed) { + unprocessed++ + continue + } + const reply = parseReply(row.reply_payload) + if (reply?._tag === "Success") replied++ + if (reply?._tag === "Failure") failed++ + } + return { failed, replied, unprocessed } satisfies MessageCounts + }) + + const diagnostics = Effect.fnUntraced(function*() { + const table = sql(`${prefix}_runners`) + const registrations = yield* sql` + SELECT address, runner, healthy, last_heartbeat + FROM ${table} + ORDER BY address + ` + return { + assignments: assignmentMap(), + messageCounts: yield* messageCounts(), + registrations + } + }) + + const waitUntil = Effect.fnUntraced(function*( + description: string, + condition: Effect.Effect, + timeout: Duration.Input = "15 seconds" + ) { + const started = yield* Clock.currentTimeMillis + const deadline = started + Duration.toMillis(Duration.fromInputUnsafe(timeout)) + if (yield* Effect.provide(condition, client)) return + while ((yield* Clock.currentTimeMillis) < deadline) { + yield* Effect.sleep(100) + if (yield* Effect.provide(condition, client)) return + } + const state = yield* diagnostics() + return yield* Effect.fail(new Error(`${description}\n${JSON.stringify(state, null, 2)}`)) + }) + + const waitForStableAssignments = Effect.fnUntraced(function*(timeout?: Duration.Input) { + let previous = "" + let stablePolls = 0 + yield* waitUntil( + "Shard assignments did not stabilize before the deadline", + Effect.sync(() => { + const current = assignmentMap() + const owners = new Set(Object.values(current).flat()) + const complete = Object.values(current).every((owners) => owners.length === 1) && + runners.every((runner) => + runner.state() !== "running" || owners.has(`${runner.address.host}:${runner.address.port}`) + ) + const encoded = complete ? JSON.stringify(current) : "" + stablePolls = encoded !== "" && encoded === previous ? stablePolls + 1 : 0 + previous = encoded + return stablePolls >= 3 + }), + timeout + ) + return assignmentMap() + }) + + const shardOfEntity = ( + entity: Entity.Entity, + entityId: string + ) => entity.getShardId(EntityId.make(entityId)).pipe(Effect.provide(client)) + + const ownerOfEntity = Effect.fnUntraced(function*( + entity: Entity.Entity, + entityId: string + ) { + const shardId = yield* shardOfEntity(entity, entityId) + return runners.find((runner) => runner.state() === "running" && runner.sharding.hasShardId(shardId)) as + | ClusterRunner + | undefined + }) + + const waitForEntityOwner = ( + entity: Entity.Entity, + entityId: string, + runner: ClusterRunner, + timeout?: Duration.Input + ) => + waitUntil( + `Entity ${entity.type}/${entityId} was not owned by runner ${runner.index} before the deadline`, + Effect.map(ownerOfEntity(entity, entityId), (owner) => owner === runner), + timeout + ) + + const getClient = (entity: Entity.Entity) => + entity.client.pipe(Effect.provide(client)) + + return { + assignmentMap, + backend: options.backend, + clientSharding, + diagnostics, + failedMessageCount: Effect.map(messageCounts(), (counts) => counts.failed), + freeze, + getClient, + kill, + lockMode: options.lockMode ?? "advisory", + messageCounts, + ownerOfEntity, + ownersOfShard, + prefix, + repliedMessageCount: Effect.map(messageCounts(), (counts) => counts.replied), + runners: runners as ReadonlyArray, + shardOfEntity, + start, + stop, + unprocessedMessageCount: Effect.map(messageCounts(), (counts) => counts.unprocessed), + waitForEntityOwner, + waitForStableAssignments, + waitUntil, + workflowEngine + } as const +}) diff --git a/.context/effect/packages/platform-node/test/cluster/MessageStorageTest.ts b/.context/effect/packages/platform/node/test/cluster/MessageStorageTest.ts similarity index 97% rename from .context/effect/packages/platform-node/test/cluster/MessageStorageTest.ts rename to .context/effect/packages/platform/node/test/cluster/MessageStorageTest.ts index f580519bc..972719fda 100644 --- a/.context/effect/packages/platform-node/test/cluster/MessageStorageTest.ts +++ b/.context/effect/packages/platform/node/test/cluster/MessageStorageTest.ts @@ -136,6 +136,13 @@ export class PrimaryKeyTest extends Rpc.make("PrimaryKeyTest", { primaryKey: (value) => value.id.toString() }) {} +export class LongKeyRpc extends Rpc.make("LongKeyRpc", { + payload: { + id: Schema.String + }, + primaryKey: (value) => value.id +}) {} + export class StreamRpc extends Rpc.make("StreamTest", { success: RpcSchema.Stream(Schema.Void, Schema.Never), payload: { diff --git a/.context/effect/packages/platform/node/test/cluster/SocketRunner.test.ts b/.context/effect/packages/platform/node/test/cluster/SocketRunner.test.ts new file mode 100644 index 000000000..9a8154ce9 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster/SocketRunner.test.ts @@ -0,0 +1,216 @@ +import { NodeClusterSocket } from "@effect/platform-node" +import { assert, describe, it } from "@effect/vitest" +import { BigDecimal, Cause, Deferred, Effect, Exit, Fiber, Layer, Option, PrimaryKey, Schema } from "effect" +import type { Sharding } from "effect/unstable/cluster" +import { + ClusterSchema, + Entity, + MessageStorage, + RunnerAddress, + RunnerHealth, + RunnerStorage, + ShardingConfig, + SocketRunner +} from "effect/unstable/cluster" +import { Rpc, RpcSerialization } from "effect/unstable/rpc" + +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 + }).annotate(ClusterSchema.Persisted, true), + Rpc.make("ProcessVolatile", { + payload: TestPayload, + success: Schema.Void + }).annotate(ClusterSchema.Persisted, false) + ]) + .annotateRpcs(ClusterSchema.Uninterruptible, true) + +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, entities: Layer.Layer) => + entities.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) + ) + +// An entity whose reply cannot be serialized: the handler returns a +// non-integer for a `Schema.Int` success schema, so `Reply.serialize` fails +// on the host runner when encoding the reply for the wire. +const IsolationEntity = Entity + .make("IsolationEntity", [ + Rpc.make("BadReply", { + payload: { id: Schema.Number }, + success: Schema.Int + }), + Rpc.make("Slow", { + success: Schema.String + }) + ]) + .annotateRpcs(ClusterSchema.Persisted, false) + +const IsolationEntityLayer = IsolationEntity.toLayer( + Effect.succeed({ + BadReply: () => Effect.succeed(1.5), + Slow: () => Effect.as(Effect.sleep("2 seconds"), "done") + }) +) + +const ISOLATION_PORT = 50_124 + +// 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. +// +// Volatile discard should complete after the request is sent, without waiting for the +// host runner to finish handling it. +describe("SocketRunner", () => { + it.live( + "discarded persisted requests serialize circular values and volatile requests do not wait for replies", + () => + Effect.gen(function*() { + const volatileStarted = yield* Deferred.make() + const releaseVolatile = yield* Deferred.make() + const TestEntityLayer = TestEntity.toLayer( + Effect.succeed({ + Process: () => Effect.void, + ProcessVolatile: () => + Deferred.succeed(volatileStarted, void 0).pipe( + Effect.andThen(Deferred.await(releaseVolatile)) + ) + }) + ) + + // Start the runner (with socket server and entity handler) + yield* Layer.launch(makeRunnerLayer(RUNNER_PORT, TestEntityLayer)).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.fromStringUnsafe("123.45") + + yield* client.Process( + TestPayload.make({ id: "req-1", amount }), + { discard: true } + ) + + const volatileFiber = yield* client.ProcessVolatile( + TestPayload.make({ id: "req-2", amount }), + { discard: true } + ).pipe(Effect.forkChild) + + yield* Deferred.await(volatileStarted) + yield* Fiber.join(volatileFiber).pipe( + Effect.timeoutOrElse({ + duration: "1 second", + orElse: () => Effect.die("volatile discard waited for the entity reply") + }) + ) + yield* Deferred.succeed(releaseVolatile, void 0) + }).pipe( + Effect.provide(makeClientLayer(RUNNER_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide( + SharedStorage + )), + 30_000 + ) + + it.live( + "a reply serialization failure fails only its own request", + () => + Effect.gen(function*() { + // Start the runner hosting the entities + yield* Layer.launch(makeRunnerLayer(ISOLATION_PORT, IsolationEntityLayer)).pipe(Effect.forkScoped) + yield* Effect.sleep("2 seconds") + + yield* Effect.gen(function*() { + const makeClient = yield* IsolationEntity.client + // Give the client time to discover the runner + yield* Effect.sleep("3 seconds") + + // a sibling request in flight on the same runner-to-runner connection + const slowFiber = yield* makeClient("slow-entity").Slow().pipe(Effect.forkChild) + yield* Effect.sleep("300 millis") + + const badExit = yield* makeClient("bad-entity").BadReply({ id: 1 }).pipe(Effect.exit) + assert.isTrue(Exit.isFailure(badExit), "the unencodable reply must fail the request") + const failure = Exit.isFailure(badExit) ? String(Cause.squash(badExit.cause)) : "" + assert.include(failure, "MalformedMessage", "the caller must receive the real encode error") + assert.notInclude( + failure, + "AlreadyProcessingMessage", + "the request must not be re-sent into the entity's dedup guard" + ) + + const slowExit = yield* Fiber.await(slowFiber) + assert.isTrue( + Exit.isSuccess(slowExit), + "a sibling in-flight request on the same connection must be unaffected" + ) + if (Exit.isSuccess(slowExit)) { + assert.strictEqual(slowExit.value, "done") + } + }).pipe( + Effect.provide(makeClientLayer(ISOLATION_PORT)), + Effect.scoped + ) + }).pipe(Effect.provide( + SharedStorage + )), + 30_000 + ) +}) diff --git a/.context/effect/packages/platform/node/test/cluster/SqlMessageStorage.integration.test.ts b/.context/effect/packages/platform/node/test/cluster/SqlMessageStorage.integration.test.ts new file mode 100644 index 000000000..6c88a13aa --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster/SqlMessageStorage.integration.test.ts @@ -0,0 +1,390 @@ +import { NodeCrypto, NodeFileSystem } from "@effect/platform-node" +import { SqliteClient } from "@effect/sql-sqlite-node" +import { assert, describe, expect, it } from "@effect/vitest" +import { Effect, Fiber, FileSystem, Latch, Layer, Option } from "effect" +import { TestClock } from "effect/testing" +import { + Entity, + Envelope, + Message, + MessageStorage, + RunnerHealth, + Runners, + RunnerStorage, + Sharding, + ShardingConfig, + Snowflake, + SqlMessageStorage +} from "effect/unstable/cluster" +import { SqlClient } from "effect/unstable/sql" +import { MysqlContainer } from "../fixtures/mysql2-utils.ts" +import { PgContainer } from "../fixtures/pg-utils.ts" +import { + GetUserRpc, + LongKeyRpc, + makeAckChunk, + makeChunkReply, + makeReply, + makeRequest, + PrimaryKeyTest, + StreamRpc +} from "./MessageStorageTest.ts" + +const TestEntity = Entity.make("test", [GetUserRpc]) +const TestEntityLayer = TestEntity.toLayer({ + GetUser: () => Effect.void +}) + +const StorageLive = SqlMessageStorage.layer.pipe( + Layer.provideMerge(Snowflake.layerGenerator), + Layer.provide([ShardingConfig.layerDefaults, NodeCrypto.layer]) +) + +const truncate = Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + yield* sql`DELETE FROM cluster_replies` + yield* sql`DELETE FROM cluster_messages` +}) + +describe("SqlMessageStorage", () => { + ;([ + ["pg", Layer.orDie(PgContainer.layerClient)], + ["mysql", Layer.orDie(MysqlContainer.layerClient)], + ["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.SqlClient + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest({ + rpc: StreamRpc, + payload: StreamRpc.payloadSchema.make({ 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: StreamRpc.payloadSchema.make({ id: 123 }) + }) + ) + assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply)) + 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: StreamRpc.payloadSchema.make({ id: 123 }) + }) + ) + assert(result._tag === "Duplicate" && Option.isSome(result.lastReceivedReply)) + expect(result.lastReceivedReply.value._tag).toEqual("WithExit") + + // duplicate WithExit + const fiber = yield* storage.saveReply(yield* makeReply(request)).pipe(Effect.forkChild) + yield* TestClock.adjust(1) + while (!fiber.pollUnsafe()) { + 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: PrimaryKeyTest, + payload: PrimaryKeyTest.payloadSchema.make({ id: 123 }) + }) + ) + const result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: PrimaryKeyTest, + payload: PrimaryKeyTest.payloadSchema.make({ id: 123 }) + }) + ) + expect(result._tag).toEqual("Duplicate") + })) + + it.effect("hashes primary keys longer than the message_id column", () => + Effect.gen(function*() { + yield* truncate + + const storage = yield* MessageStorage.MessageStorage + const longId = "long-key-".repeat(50) + const request = yield* makeRequest({ + rpc: LongKeyRpc, + payload: LongKeyRpc.payloadSchema.make({ id: longId }) + }) + const result = yield* storage.saveRequest(request) + expect(result._tag).toEqual("Success") + + const duplicate = yield* storage.saveRequest( + yield* makeRequest({ + rpc: LongKeyRpc, + payload: LongKeyRpc.payloadSchema.make({ id: longId }) + }) + ) + expect(duplicate._tag).toEqual("Duplicate") + + const requestId = yield* storage.requestIdForPrimaryKey({ + address: request.envelope.address, + tag: request.envelope.tag, + id: longId + }) + expect(requestId).toEqual(Option.some(request.envelope.requestId)) + + const sql = yield* SqlClient.SqlClient + const rows = yield* sql<{ message_id: string }>`SELECT message_id FROM cluster_messages` + expect(rows).toHaveLength(1) + expect(rows[0].message_id).toMatch(/^[0-9a-f]{64}$/) + })) + + it.effect("keeps primary keys within the column width as plaintext", () => + Effect.gen(function*() { + yield* truncate + + const sql = yield* SqlClient.SqlClient + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest({ + rpc: PrimaryKeyTest, + payload: PrimaryKeyTest.payloadSchema.make({ id: 456 }) + }) + yield* storage.saveRequest(request) + + // rows written by previous versions store the plaintext composed + // key, so short keys must stay byte-identical to keep deduplicating + const plaintext = Envelope.primaryKey(request.envelope)! + const rows = yield* sql<{ message_id: string }>`SELECT message_id FROM cluster_messages` + expect(rows).toHaveLength(1) + expect(rows[0].message_id).toEqual(plaintext) + + const result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: PrimaryKeyTest, + payload: PrimaryKeyTest.payloadSchema.make({ id: 456 }) + }) + ) + assert(result._tag === "Duplicate") + expect(result.originalId).toEqual(request.envelope.requestId) + + const requestId = yield* storage.requestIdForPrimaryKey({ + address: request.envelope.address, + tag: request.envelope.tag, + id: "456" + }) + expect(requestId).toEqual(Option.some(request.envelope.requestId)) + })) + + if (label === "sqlite") { + // sqlite's TEXT message_id column stored over-long plaintext keys + // before hashing, so the legacy fallback must also cover keys longer + // than the 255-character limit of the width-enforcing dialects + it.effect("detects duplicates for legacy long-key plaintext rows", () => + Effect.gen(function*() { + yield* truncate + + const sql = yield* SqlClient.SqlClient + const storage = yield* MessageStorage.MessageStorage + const longId = "legacy-long-key-".repeat(30) + const request = yield* makeRequest({ + rpc: LongKeyRpc, + payload: LongKeyRpc.payloadSchema.make({ id: longId }) + }) + yield* storage.saveRequest(request) + + // simulate a row written before message_id values were hashed + const plaintext = Envelope.primaryKey(request.envelope)! + expect(plaintext.length).toBeGreaterThan(255) + yield* sql`UPDATE cluster_messages SET message_id = ${plaintext} WHERE id = ${ + String(request.envelope.requestId) + }` + + const result = yield* storage.saveRequest( + yield* makeRequest({ + rpc: LongKeyRpc, + payload: LongKeyRpc.payloadSchema.make({ id: longId }) + }) + ) + assert(result._tag === "Duplicate") + expect(result.originalId).toEqual(request.envelope.requestId) + + const requestId = yield* storage.requestIdForPrimaryKey({ + address: request.envelope.address, + tag: request.envelope.tag, + id: longId + }) + expect(requestId).toEqual(Option.some(request.envelope.requestId)) + })) + } + + 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* Latch.make() + const request = yield* makeRequest() + yield* storage.saveRequest(request) + const fiber = yield* storage.registerReplyHandler( + new Message.OutgoingRequest({ + ...request, + respond: () => latch.open + }) + ).pipe(Effect.forkChild) + yield* TestClock.adjust(1) + yield* storage.saveReply(yield* makeReply(request)) + yield* latch.await + yield* Fiber.await(fiber) + })) + + 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) + })) + }) + }) + + it.effect("keeps held messages readable while entity layers build", () => + Effect.gen(function*() { + const config = ShardingConfig.layer({ + entityRegistrationTimeout: 6000, + entityTerminationTimeout: 0, + entityMessagePollInterval: 100, + refreshAssignmentsInterval: 0 + }) + const delayedEnv = TestEntityLayer.pipe( + Layer.provide(Layer.effectDiscard(Effect.sleep(10_000))), + Layer.provideMerge(Sharding.layer), + Layer.provide(RunnerStorage.layerMemory), + Layer.provide(RunnerHealth.layerNoop), + Layer.provide(Runners.layerNoop), + Layer.provide(config) + ) + const storage = yield* MessageStorage.MessageStorage + const request = yield* makeRequest() + yield* storage.saveRequest(request) + + const fiber = yield* Effect.never.pipe( + Effect.provide(delayedEnv), + Effect.scoped, + Effect.forkChild({ startImmediately: true }) + ) + + yield* TestClock.adjust(7500) + expect(yield* storage.repliesFor([request])).toHaveLength(0) + + yield* TestClock.adjust(2500) + yield* TestClock.adjust(100) + expect(yield* storage.repliesFor([request])).toHaveLength(1) + yield* Fiber.interrupt(fiber) + }).pipe(Effect.provide(StorageLive.pipe( + Layer.provideMerge(SqliteLayer) + )))) +}) + +const SqliteLayer = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + return SqliteClient.layer({ + filename: dir + "/test.db" + }) +}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer)) diff --git a/.context/effect/packages/platform/node/test/cluster/SqlRunnerStorage.integration.test.ts b/.context/effect/packages/platform/node/test/cluster/SqlRunnerStorage.integration.test.ts new file mode 100644 index 000000000..d95f99363 --- /dev/null +++ b/.context/effect/packages/platform/node/test/cluster/SqlRunnerStorage.integration.test.ts @@ -0,0 +1,459 @@ +import { NodeFileSystem } from "@effect/platform-node" +import { SqliteClient } from "@effect/sql-sqlite-node" +import { assert, describe, expect, it } from "@effect/vitest" +import { Cause, Duration, Effect, Exit, FileSystem, Layer, Schedule } from "effect" +import { TestClock } from "effect/testing" +import { + ClusterError, + Runner, + RunnerAddress, + RunnerStorage, + ShardId, + ShardingConfig, + SqlRunnerStorage +} from "effect/unstable/cluster" +import { SqlClient, type SqlConnection, SqlError } from "effect/unstable/sql" +import { MysqlContainer } from "../fixtures/mysql2-utils.ts" +import { PgContainer } from "../fixtures/pg-utils.ts" + +const StorageLive = SqlRunnerStorage.layer + +describe("SqlRunnerStorage", () => { + it.effect("bounds shard lock operations and rebuilds an unresponsive reserved connection", () => { + const partitioned = makePartitionState() + const layer = StorageLive.pipe( + Layer.provideMerge(blackholeReservedConnection(partitioned, true)), + Layer.provide(ShardingConfig.layer({ + shardLockExpiration: 1000, + shardLockRefreshInterval: 100 + })) + ) + + return Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + const runner = Runner.make({ + address: runnerAddress1, + groups: ["default"], + weight: 1 + }) + const shards = [ShardId.make("default", 1)] + + yield* storage.register(runner, true) + yield* storage.acquire(runnerAddress1, shards) + partitioned.current = true + + const expectDeadline = Effect.fnUntraced(function*(operation: Effect.Effect) { + const [elapsed, exit] = yield* operation.pipe( + Effect.exit, + Effect.timed, + TestClock.withLive + ) + assert(Exit.isFailure(exit)) + const error = Cause.squash(exit.cause) + assert(error instanceof ClusterError.PersistenceError) + assert.isBelow(Duration.toMillis(elapsed), 1000) + yield* Effect.sleep(20).pipe(TestClock.withLive) + }) + + yield* expectDeadline(storage.refresh(runnerAddress1, shards)) + yield* expectDeadline(storage.refresh(runnerAddress1, shards)) + yield* expectDeadline(storage.refresh(runnerAddress1, shards)) + + assert.isAtLeast(partitioned.interruptedQueries, 1) + assert.isAtMost(partitioned.maxActiveQueries, 1) + + // Rebuilding is asynchronous, so wait until the replacement connection + // is ready before checking that lock operations recover. + const usableConnections = partitioned.usableConnections + partitioned.current = false + yield* waitUntil(() => partitioned.usableConnections > usableConnections) + expect(yield* storage.refresh(runnerAddress1, shards).pipe(TestClock.withLive)).toEqual(shards) + + partitioned.current = true + yield* expectDeadline(storage.acquire(runnerAddress1, [ShardId.make("default", 2)])) + const usableConnectionsAfterAcquire = partitioned.usableConnections + partitioned.current = false + yield* waitUntil(() => partitioned.usableConnections > usableConnectionsAfterAcquire) + yield* storage.refresh(runnerAddress1, shards).pipe(TestClock.withLive) + + partitioned.current = true + yield* expectDeadline(storage.release(runnerAddress1, shards[0])) + const usableConnectionsAfterRelease = partitioned.usableConnections + partitioned.current = false + yield* waitUntil(() => partitioned.usableConnections > usableConnectionsAfterRelease) + yield* storage.refresh(runnerAddress1, shards).pipe(TestClock.withLive) + yield* storage.release(runnerAddress1, shards[0]).pipe(TestClock.withLive) + + assert.strictEqual(partitioned.activeQueries, 0) + }).pipe( + // Ensure layer teardown cannot mask a body failure with Vitest's timeout. + Effect.ensuring(Effect.sync(() => { + partitioned.current = false + })), + Effect.provide(layer) + ) + }, 60_000) + + it.effect("recovers when a blackholed query cannot resume after the partition clears", () => { + const partitioned = makePartitionState() + const layer = StorageLive.pipe( + Layer.provideMerge(blackholeReservedConnection(partitioned, false)), + Layer.provide(ShardingConfig.layer({ + shardLockDisableAdvisory: true, + shardLockExpiration: 1000, + shardLockRefreshInterval: 100 + })) + ) + + return Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + const runner = Runner.make({ + address: runnerAddress1, + groups: ["default"], + weight: 1 + }) + const shards = [ShardId.make("default", 1)] + + yield* storage.register(runner, true) + yield* storage.acquire(runnerAddress1, shards) + partitioned.current = true + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + yield* waitUntil(() => partitioned.activeQueries === 0) + + partitioned.current = false + expect( + yield* storage.refresh(runnerAddress1, shards).pipe( + Effect.retry({ times: 5, schedule: Schedule.spaced(20) }), + TestClock.withLive + ) + ).toEqual(shards) + assert.isAtLeast(partitioned.interruptedQueries, 1) + assert.isAtMost(partitioned.maxActiveQueries, 1) + }).pipe(Effect.provide(layer)) + }, 60_000) + + it.effect("rebuilds the reserved connection again when a rebuilt connection stops responding", () => { + const partitioned = makePartitionState() + const layer = StorageLive.pipe( + Layer.provideMerge(blackholeReservedConnection(partitioned, false)), + Layer.provide(ShardingConfig.layer({ + shardLockExpiration: 1000, + shardLockRefreshInterval: 100 + })) + ) + + return Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + const runner = Runner.make({ + address: runnerAddress1, + groups: ["default"], + weight: 1 + }) + const shards = [ShardId.make("default", 1)] + + yield* storage.register(runner, true) + yield* storage.acquire(runnerAddress1, shards) + + // a failing lock operation rebuilds the reserved connection + partitioned.failNextQueries = 1 + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + yield* waitUntil(() => partitioned.usableConnections === 2) + + // the rebuilt connection then wedges, without any lock operation + // succeeding in between - a further rebuild still has to be attempted + const reserved = partitioned.reservedConnections + partitioned.current = true + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + partitioned.current = false + yield* waitUntil(() => partitioned.reservedConnections > reserved) + + expect( + yield* storage.refresh(runnerAddress1, shards).pipe( + Effect.retry({ times: 5, schedule: Schedule.spaced(20) }), + TestClock.withLive + ) + ).toEqual(shards) + }).pipe(Effect.provide(layer)) + }, 60_000) + + it.effect("rebuilds the reserved connection when releasing the previous one hangs", () => { + const partitioned = makePartitionState() + const layer = StorageLive.pipe( + Layer.provideMerge(blackholeReservedConnection(partitioned, false)), + Layer.provide(ShardingConfig.layer({ + shardLockDisableAdvisory: true, + shardLockExpiration: 1000, + shardLockRefreshInterval: 100 + })) + ) + + return Effect.gen(function*() { + const storage = yield* RunnerStorage.RunnerStorage + const runner = Runner.make({ + address: runnerAddress1, + groups: ["default"], + weight: 1 + }) + const shards = [ShardId.make("default", 1)] + + yield* storage.register(runner, true) + yield* storage.acquire(runnerAddress1, shards) + + // the connection wedges and the driver never releases it back to the + // pool, so the rebuild stalls closing the previous scope + partitioned.current = true + partitioned.blockRelease = true + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + yield* Effect.sleep(150).pipe(TestClock.withLive) + + // the stalled release must not disable further rebuilds + partitioned.current = false + const reserved = partitioned.reservedConnections + yield* storage.refresh(runnerAddress1, shards).pipe(Effect.exit, TestClock.withLive) + yield* waitUntil(() => partitioned.reservedConnections > reserved) + + expect( + yield* storage.refresh(runnerAddress1, shards).pipe( + Effect.retry({ times: 5, schedule: Schedule.spaced(20) }), + TestClock.withLive + ) + ).toEqual(shards) + }).pipe( + // let the stalled release finish so the layer can be torn down + Effect.ensuring(Effect.sync(() => { + partitioned.blockRelease = false + })), + Effect.provide(layer) + ) + }, 60_000) + + it.effect("isolates advisory shard locks by prefix", () => + Effect.gen(function*() { + const storageA = yield* SqlRunnerStorage.make({ prefix: "cluster" }) + const storageB = yield* SqlRunnerStorage.make({ prefix: "other" }) + const shard = ShardId.make("default", 1) + + expect(yield* storageA.acquire(runnerAddress1, [shard])).toEqual([shard]) + expect(yield* storageB.acquire(runnerAddress2, [shard])).toEqual([shard]) + }).pipe( + // Release the advisory-lock connections before the PostgreSQL layer. + Effect.scoped, + Effect.provide(PgContainer.layerClient), + Effect.provide(ShardingConfig.layer()) + ), 60_000) + + it.effect("excludes other storages using the same prefix", () => + Effect.gen(function*() { + const storageA = yield* SqlRunnerStorage.make({ prefix: "cluster" }) + const storageB = yield* SqlRunnerStorage.make({ prefix: "cluster" }) + const shard = ShardId.make("default", 1) + + expect(yield* storageA.acquire(runnerAddress1, [shard])).toEqual([shard]) + expect(yield* storageB.acquire(runnerAddress2, [shard])).toEqual([]) + }).pipe( + // Release the advisory-lock connections before the PostgreSQL layer. + Effect.scoped, + Effect.provide(PgContainer.layerClient), + Effect.provide(ShardingConfig.layer()) + ), 60_000) + ;([ + ["pg", Layer.orDie(PgContainer.layerClient)], + ["mysql", Layer.orDie(MysqlContainer.layerClient)], + ["vitess", Layer.orDie(MysqlContainer.layerClientVitess)], + ["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 runnerAddress2 = RunnerAddress.make("localhost", 5678) + +interface PartitionState { + current: boolean + blockRelease: boolean + activeQueries: number + maxActiveQueries: number + interruptedQueries: number + failNextQueries: number + reservedConnections: number + usableConnections: number +} + +const makePartitionState = (): PartitionState => ({ + current: false, + blockRelease: false, + activeQueries: 0, + maxActiveQueries: 0, + interruptedQueries: 0, + failNextQueries: 0, + reservedConnections: 0, + usableConnections: 0 +}) + +const waitUntil = Effect.fnUntraced( + function*(predicate: () => boolean) { + while (!predicate()) { + yield* Effect.sleep(20) + } + }, + Effect.timeoutOrElse({ + duration: 10_000, + orElse: () => Effect.die("timed out waiting for condition") + }), + TestClock.withLive +) + +const blackholeReservedConnection = (partitioned: PartitionState, resumePending: boolean) => + Layer.effect( + SqlClient.SqlClient, + Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + const wrapConnection = (connection: SqlConnection.Connection): SqlConnection.Connection => { + let usable = false + const execute = (effect: Effect.Effect) => + Effect.suspend((): Effect.Effect => { + if (partitioned.failNextQueries > 0) { + partitioned.failNextQueries-- + return Effect.fail( + new SqlError.SqlError({ + reason: new SqlError.ConnectionError({ cause: new Error("connection lost") }) + }) + ) + } + partitioned.activeQueries++ + partitioned.maxActiveQueries = Math.max(partitioned.maxActiveQueries, partitioned.activeQueries) + return Effect.suspend(function waitForConnection(): Effect.Effect { + if (!partitioned.current) return effect + return resumePending + ? Effect.andThen(Effect.sleep(5), waitForConnection) + : Effect.never + }).pipe( + Effect.onExit((exit) => + Effect.sync(() => { + partitioned.activeQueries-- + if (Exit.hasInterrupts(exit)) { + partitioned.interruptedQueries++ + } + if (Exit.isSuccess(exit) && !usable) { + usable = true + partitioned.usableConnections++ + } + }) + ) + ) + }) + return { + ...connection, + execute: (...args) => execute(connection.execute(...args)), + executeRaw: (...args) => execute(connection.executeRaw(...args)), + executeValues: (...args) => execute(connection.executeValues(...args)), + executeValuesUnprepared: (...args) => execute(connection.executeValuesUnprepared(...args)), + executeUnprepared: (...args) => execute(connection.executeUnprepared(...args)) + } + } + let client: SqlClient.SqlClient + client = new Proxy(sql, { + get(target, property, receiver) { + if (property === "reserve") { + return Effect.andThen( + // simulates a driver that stalls uninterruptibly while tearing + // down a reserved connection, so closing its scope cannot be + // interrupted + Effect.addFinalizer(() => + Effect.uninterruptible(Effect.suspend(function waitForRelease(): Effect.Effect { + if (!partitioned.blockRelease) return Effect.void + return Effect.andThen( + Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 5))), + waitForRelease + ) + })) + ), + Effect.map(target.reserve, (connection) => { + partitioned.reservedConnections++ + return wrapConnection(connection) + }) + ) + } + if (property === "withoutTransforms") { + return () => client + } + return Reflect.get(target, property, receiver) + } + }) + return client + }) + ).pipe(Layer.provide(PgContainer.layerClient)) + +const SqliteLayer = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + return SqliteClient.layer({ + filename: dir + "/test.db" + }) +}).pipe(Layer.unwrap, Layer.provide(NodeFileSystem.layer)) diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server-outside.txt b/.context/effect/packages/platform/node/test/fixtures/http-static-server-outside.txt similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server-outside.txt rename to .context/effect/packages/platform/node/test/fixtures/http-static-server-outside.txt diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/conditional.txt b/.context/effect/packages/platform/node/test/fixtures/http-static-server/conditional.txt similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/conditional.txt rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/conditional.txt diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/custom/home.html b/.context/effect/packages/platform/node/test/fixtures/http-static-server/custom/home.html similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/custom/home.html rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/custom/home.html diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/file.binx b/.context/effect/packages/platform/node/test/fixtures/http-static-server/file.binx similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/file.binx rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/file.binx diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/guide/index.html b/.context/effect/packages/platform/node/test/fixtures/http-static-server/guide/index.html similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/guide/index.html rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/guide/index.html diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/hello.txt b/.context/effect/packages/platform/node/test/fixtures/http-static-server/hello.txt similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/hello.txt rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/hello.txt diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/index.html b/.context/effect/packages/platform/node/test/fixtures/http-static-server/index.html similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/index.html rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/index.html diff --git a/.context/effect/packages/platform-node/test/fixtures/http-static-server/range.txt b/.context/effect/packages/platform/node/test/fixtures/http-static-server/range.txt similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/http-static-server/range.txt rename to .context/effect/packages/platform/node/test/fixtures/http-static-server/range.txt diff --git a/.context/effect/packages/platform-node/test/fixtures/mysql2-utils.ts b/.context/effect/packages/platform/node/test/fixtures/mysql2-utils.ts similarity index 84% rename from .context/effect/packages/platform-node/test/fixtures/mysql2-utils.ts rename to .context/effect/packages/platform/node/test/fixtures/mysql2-utils.ts index 344145a5d..ef749fcce 100644 --- a/.context/effect/packages/platform-node/test/fixtures/mysql2-utils.ts +++ b/.context/effect/packages/platform/node/test/fixtures/mysql2-utils.ts @@ -7,6 +7,17 @@ export class ContainerError extends Data.TaggedError("ContainerError")<{ cause: unknown }> {} +const makeMysqlContainer = () => + new MySqlContainer("mysql:lts").withHealthCheck({ + test: [ + "CMD-SHELL", + "MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysqladmin ping --protocol TCP --host 127.0.0.1 --user root --silent" + ], + interval: 250, + timeout: 1000, + retries: 1000 + }) + export class MysqlContainer extends Context.Service< MysqlContainer, StartedMySqlContainer @@ -14,7 +25,7 @@ export class MysqlContainer extends Context.Service< static readonly layer = Layer.effect(this)( Effect.acquireRelease( Effect.tryPromise({ - try: () => new MySqlContainer("mysql:lts").start(), + try: () => makeMysqlContainer().start(), catch: (cause) => new ContainerError({ cause }) }), (container) => Effect.promise(() => container.stop()) diff --git a/.context/effect/packages/platform-node/test/fixtures/pg-utils.ts b/.context/effect/packages/platform/node/test/fixtures/pg-utils.ts similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/pg-utils.ts rename to .context/effect/packages/platform/node/test/fixtures/pg-utils.ts diff --git a/.context/effect/packages/platform-node/test/fixtures/rpc-e2e.ts b/.context/effect/packages/platform/node/test/fixtures/rpc-e2e.ts similarity index 100% rename from .context/effect/packages/platform-node/test/fixtures/rpc-e2e.ts rename to .context/effect/packages/platform/node/test/fixtures/rpc-e2e.ts diff --git a/.context/effect/packages/platform/node/test/fixtures/rpc-schemas.ts b/.context/effect/packages/platform/node/test/fixtures/rpc-schemas.ts new file mode 100644 index 000000000..668b0eae2 --- /dev/null +++ b/.context/effect/packages/platform/node/test/fixtures/rpc-schemas.ts @@ -0,0 +1,175 @@ +import { Context, Deferred, Effect, Layer, Metric, Option, Queue, Schema } from "effect" +import { Headers } from "effect/unstable/http" +import * as Rpc from "effect/unstable/rpc/Rpc" +import * as RpcGroup from "effect/unstable/rpc/RpcGroup" +import * as RpcMiddleware from "effect/unstable/rpc/RpcMiddleware" +import * as RpcServer from "effect/unstable/rpc/RpcServer" + +export class User extends Schema.Class("User")({ + id: Schema.String, + name: Schema.String +}) {} + +class StreamUsers extends Rpc.make("StreamUsers", { + success: User, + payload: { + id: Schema.String + }, + stream: true +}) {} + +class CurrentUser extends Context.Service()("CurrentUser") {} + +class Unauthorized extends Schema.Error("Unauthorized")({ + _tag: Schema.tag("Unauthorized") +}) {} + +class AuthMiddleware extends RpcMiddleware.Service()("AuthMiddleware", { + error: Unauthorized, + requiredForClient: true +}) {} + +class TimingMiddleware extends RpcMiddleware.Service()("TimingMiddleware") {} + +class GetUser extends Rpc.make("GetUser", { + success: User, + payload: { id: Schema.String } +}) {} + +export const UserRpcs = RpcGroup.make( + GetUser, + Rpc.make("GetUserDeferred", { + success: User, + payload: { id: Schema.String } + }), + Rpc.make("GetUserOption", { + success: Schema.Option(User), + payload: { id: Schema.String } + }), + StreamUsers, + Rpc.make("GetInterrupts", { + success: Schema.Number + }), + Rpc.make("GetEmits", { + success: Schema.Number + }), + Rpc.make("ProduceDefect"), + Rpc.make("ProduceDefectCustom", { + defect: Schema.Defect({ includeStack: true }) + }), + 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) + +export const AuthLive = Layer.succeed(AuthMiddleware)( + AuthMiddleware.of((effect, options) => + Effect.provideService( + effect, + CurrentUser, + 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") +export const TimingLive = Layer.succeed(TimingMiddleware)( + TimingMiddleware.of((effect) => + effect.pipe( + Effect.tap(Metric.update(rpcSuccesses, 1)), + Effect.tapDefect(() => Metric.update(rpcDefects, 1)), + Effect.ensuring(Metric.update(rpcCount, 1)) + ) + ) +) + +export const UsersLive = UserRpcs.toLayer(Effect.gen(function*() { + let interrupts = 0 + let emits = 0 + return UserRpcs.of({ + GetUser: (_) => + CurrentUser.pipe( + Rpc.fork + ), + GetUserDeferred(_) { + const deferred = Deferred.makeUnsafe() + Deferred.doneUnsafe(deferred, Effect.succeed(new User({ id: "1", name: "John" }))) + return Effect.succeed(deferred) + }, + GetUserOption: Effect.fnUntraced(function*(req) { + return Option.some(new User({ id: req.id, name: "John" })) + }), + StreamUsers: Effect.fnUntraced(function*(req, _) { + const mailbox = yield* Queue.bounded(0) + + yield* Effect.addFinalizer(() => + Effect.sync(() => { + interrupts++ + }) + ) + + yield* Queue.offer(mailbox, new User({ id: req.id, name: "John" })).pipe( + Effect.tap(() => + Effect.sync(() => { + emits++ + }) + ), + Effect.delay(100), + Effect.forever, + Effect.forkScoped + ) + + return mailbox + }), + GetInterrupts: () => Effect.sync(() => interrupts), + GetEmits: () => Effect.sync(() => emits), + ProduceDefect: () => Effect.die("boom"), + ProduceDefectCustom: () => + Effect.die({ + message: "detailed error", + stack: "Error: detailed error\n at handler.ts:1", + name: "CustomDefect" + }), + 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, { + disableFatalDefects: true +}).pipe( + Layer.provide([ + UsersLive, + AuthLive, + TimingLive + ]) +) + +export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) => + next({ + ...request, + headers: Headers.set(request.headers, "name", "Logged in user") + })) diff --git a/.context/effect/packages/platform/node/test/fixtures/text.txt b/.context/effect/packages/platform/node/test/fixtures/text.txt new file mode 100644 index 000000000..72b190ee6 --- /dev/null +++ b/.context/effect/packages/platform/node/test/fixtures/text.txt @@ -0,0 +1 @@ +lorem ipsum dolar sit amet diff --git a/.context/effect/packages/platform/node/tsconfig.json b/.context/effect/packages/platform/node/tsconfig.json new file mode 100644 index 000000000..4805d1322 --- /dev/null +++ b/.context/effect/packages/platform/node/tsconfig.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "references": [ + { "path": "../../effect" }, + { "path": "../node-shared" } + ], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/.context/effect/packages/sql/clickhouse/CHANGELOG.md b/.context/effect/packages/sql/clickhouse/CHANGELOG.md index eeef51a4e..e4fab9a9a 100644 --- a/.context/effect/packages/sql/clickhouse/CHANGELOG.md +++ b/.context/effect/packages/sql/clickhouse/CHANGELOG.md @@ -1,5 +1,70 @@ # @effect/sql-clickhouse +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + - @effect/platform-node@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7138](https://github.com/Effect-TS/effect/pull/7138) [`9f15190`](https://github.com/Effect-TS/effect/commit/9f151906ebfbce4c7981af23be5e1e00b31212fd) Thanks @fubhy! - Parameterize ClickHouse query IDs when cancelling queries and inserts. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`545d876`](https://github.com/Effect-TS/effect/commit/545d8767648cbbbf1820361662c0c1e10768db6a), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`721b9f0`](https://github.com/Effect-TS/effect/commit/721b9f0d320e50f3e2324c2cd1f43c6643af3ca0), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6908](https://github.com/Effect-TS/effect/pull/6908) [`8a45ec3`](https://github.com/Effect-TS/effect/commit/8a45ec3117bab83552f77ed49f61009129e4cefb) Thanks @fubhy! - Preserve fractional JavaScript numbers in inferred ClickHouse parameters. + +- [#6907](https://github.com/Effect-TS/effect/pull/6907) [`c8fd57e`](https://github.com/Effect-TS/effect/commit/c8fd57e07ef86d741482d76c41cfce5374305af0) Thanks @fubhy! - Close the ClickHouse client when the startup connection check times out. + +- [#6906](https://github.com/Effect-TS/effect/pull/6906) [`5ed5692`](https://github.com/Effect-TS/effect/commit/5ed5692f1a4361759cb19b83ee86d4bbbb78f074) Thanks @fubhy! - Propagate ClickHouse result decoding failures as `SqlError` values. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + - @effect/platform-node@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/clickhouse/README.md b/.context/effect/packages/sql/clickhouse/README.md index 7166ccc93..99e696fb8 100644 --- a/.context/effect/packages/sql/clickhouse/README.md +++ b/.context/effect/packages/sql/clickhouse/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-clickhouse` +# @effect/sql-clickhouse -An Effect SQL implementation for [ClickHouse](https://clickhouse.com/). +An Effect SQL client for [ClickHouse](https://clickhouse.com), built on the [`@clickhouse/client`](https://clickhouse.com/docs/integrations/javascript) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-clickhouse@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-clickhouse). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-clickhouse) diff --git a/.context/effect/packages/sql/clickhouse/docgen.json b/.context/effect/packages/sql/clickhouse/docgen.json deleted file mode 100644 index 6cba08119..000000000 --- a/.context/effect/packages/sql/clickhouse/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/clickhouse/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/clickhouse/package.json b/.context/effect/packages/sql/clickhouse/package.json index 1907879c9..af308267e 100644 --- a/.context/effect/packages/sql/clickhouse/package.json +++ b/.context/effect/packages/sql/clickhouse/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-clickhouse", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A Clickhouse toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "@effect/platform-node": "workspace:^", diff --git a/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts b/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts index fb5883eb1..a84ee2763 100644 --- a/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts +++ b/.context/effect/packages/sql/clickhouse/src/ClickhouseClient.ts @@ -104,7 +104,7 @@ export type TypeId = "~@effect/sql-clickhouse/ClickhouseClient" * typed parameter fragments, command-mode execution, insert queries, and * per-effect query ID and ClickHouse settings. * - * @category models + * @category services * @since 4.0.0 */ export interface ClickhouseClient extends Client.SqlClient { @@ -149,7 +149,7 @@ export const ClickhouseClient = Context.Service("@effect/sql-c * `@clickhouse/client` options with optional span attributes and query/result * name transforms. * - * @category constructors + * @category models * @since 4.0.0 */ export interface ClickhouseClientConfig extends Clickhouse.ClickHouseClientConfigOptions { @@ -175,16 +175,16 @@ export const make = ( ? Statement.defaultTransforms(options.transformResultNames).array : undefined - const client = Clickhouse.createClient(options) + const client = yield* Effect.acquireRelease( + Effect.sync(() => Clickhouse.createClient(options)), + (client) => Effect.promise(() => client.close()) + ) - yield* Effect.acquireRelease( - Effect.tryPromise({ - try: () => client.exec({ query: "SELECT 1" }), - catch: (cause) => - new SqlError({ reason: classifyError(cause, "ClickhouseClient: Failed to connect", "connect", "connection") }) - }), - () => Effect.promise(() => client.close()) - ).pipe( + yield* Effect.tryPromise({ + try: () => client.exec({ query: "SELECT 1" }), + catch: (cause) => + new SqlError({ reason: classifyError(cause, "ClickhouseClient: Failed to connect", "connect", "connection") }) + }).pipe( Effect.timeoutOrElse({ duration: Duration.seconds(5), orElse: () => @@ -253,7 +253,12 @@ export const make = ( } return Effect.suspend(() => { controller.abort() - return Effect.promise(() => this.conn.command({ query: `KILL QUERY WHERE query_id = '${queryId}'` })) + return Effect.promise(() => + this.conn.command({ + query: "KILL QUERY WHERE query_id = {queryId:String}", + query_params: { queryId } + }) + ) }) }) }) @@ -263,12 +268,11 @@ export const make = ( return this.runRaw(sql, params, format).pipe( Effect.flatMap((result) => { if ("json" in result) { - return Effect.promise(() => - result.json().then( - (result) => "data" in result ? result.data : result as any, - () => [] - ) - ) + return Effect.tryPromise({ + try: () => result.json().then((result) => "data" in result ? result.data : result as any), + catch: (cause) => + new SqlError({ reason: classifyError(cause, "Failed to parse result", "parseResult") }) + }) } return Effect.succeed([]) }) @@ -376,7 +380,12 @@ export const make = ( ) return Effect.suspend(() => { controller.abort() - return Effect.promise(() => client.command({ query: `KILL QUERY WHERE query_id = '${queryId}'` })) + return Effect.promise(() => + client.command({ + query: "KILL QUERY WHERE query_id = {queryId:String}", + query_params: { queryId } + }) + ) }) }) }, @@ -399,7 +408,7 @@ export const make = ( * Fiber reference read by the low-level ClickHouse connection to choose query * or command execution for statements; defaults to `query`. * - * @category references + * @category services * @since 4.0.0 */ export const ClientMethod = Context.Reference<"query" | "command" | "insert">( @@ -413,7 +422,7 @@ export const ClientMethod = Context.Reference<"query" | "command" | "insert">( * Fiber reference for the ClickHouse `query_id` applied to queries and * inserts; a random UUID is generated when no query ID is set. * - * @category references + * @category services * @since 4.0.0 */ export const QueryId = Context.Reference( @@ -425,7 +434,7 @@ export const QueryId = Context.Reference( * Fiber reference containing ClickHouse settings to attach to queries, * commands, and inserts. * - * @category references + * @category services * @since 4.0.0 */ export const ClickhouseSettings: Context.Reference< @@ -484,7 +493,7 @@ const typeFromUnknown = (value: unknown): string => { } switch (typeof value) { case "number": - return "Decimal" + return "Float64" case "bigint": return "Int64" case "boolean": @@ -504,7 +513,7 @@ const typeFromUnknown = (value: unknown): string => { * `{pN: Type}` placeholders and escaping identifiers with an optional query * name transform. * - * @category compiler + * @category constructors * @since 4.0.0 */ export const makeCompiler = (transform?: (_: string) => string) => @@ -534,13 +543,13 @@ const escape = Statement.defaultEscape("\"") * Custom SQL fragment type used for ClickHouse typed parameters created by * `ClickhouseClient.param`. * - * @category custom types + * @category models * @since 4.0.0 */ export type ClickhouseCustom = ClickhouseParam /** - * @category custom types + * @category models * @since 4.0.0 */ interface ClickhouseParam extends Statement.Custom<"ClickhouseParam", string, unknown> {} diff --git a/.context/effect/packages/sql/clickhouse/src/ClickhouseMigrator.ts b/.context/effect/packages/sql/clickhouse/src/ClickhouseMigrator.ts index 687ae5007..c57a484fe 100644 --- a/.context/effect/packages/sql/clickhouse/src/ClickhouseMigrator.ts +++ b/.context/effect/packages/sql/clickhouse/src/ClickhouseMigrator.ts @@ -24,7 +24,7 @@ export * from "effect/unstable/sql/Migrator" * Runs SQL migrations for ClickHouse using the supplied migrator options and * returns the applied migration IDs and names. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( diff --git a/.context/effect/packages/sql/clickhouse/test/Client.test.ts b/.context/effect/packages/sql/clickhouse/test/Client.test.ts index 097065a23..c9c6f28c4 100644 --- a/.context/effect/packages/sql/clickhouse/test/Client.test.ts +++ b/.context/effect/packages/sql/clickhouse/test/Client.test.ts @@ -1,6 +1,94 @@ -import { describe, it } from "@effect/vitest" +import { ClickhouseClient } from "@effect/sql-clickhouse" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Fiber } from "effect" +import { TestClock } from "effect/testing" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import * as Statement from "effect/unstable/sql/Statement" +import { vi } from "vitest" + +let closeCalls = 0 +let connectImmediately = false +const commandCalls: Array> = [] + +vi.mock("@clickhouse/client", () => ({ + createClient: () => ({ + exec: () => connectImmediately ? Promise.resolve({}) : new Promise(() => {}), + query: () => new Promise(() => {}), + insert: () => new Promise(() => {}), + command: (options: Record) => { + commandCalls.push(options) + return Promise.resolve({}) + }, + close: () => { + closeCalls++ + return Promise.resolve() + } + }) +})) describe("ClickhouseClient", () => { - it("should work", () => { + it("preserves fractional JavaScript numbers in inferred parameters", () => { + const sql = Statement.make(Effect.void as any, ClickhouseClient.makeCompiler(), [], undefined) + const [query] = sql`SELECT ${1.5}`.compile() + + assert.strictEqual(query, "SELECT {p1: Float64}") }) + + it.effect("closes the client when the connection check times out", () => + Effect.gen(function*() { + connectImmediately = false + closeCalls = 0 + const fiber = yield* Effect.forkDetach( + ClickhouseClient.make({ url: "http://localhost:8123" }).pipe(Effect.scoped) + ) + yield* Effect.yieldNow + yield* TestClock.adjust("5 seconds") + const result = fiber.pollUnsafe() + + assert.isDefined(result) + assert.strictEqual(closeCalls, 1) + }).pipe(Effect.provide(Reactivity.layer))) + + it.effect("parameterizes the query id when cancelling a query", () => + Effect.gen(function*() { + connectImmediately = true + commandCalls.length = 0 + const queryId = "id' OR 1 = 1 --" + const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" }) + const fiber = yield* client.withQueryId(client.unsafe("SELECT 1"), queryId).pipe(Effect.forkScoped) + yield* Effect.yieldNow + + yield* Fiber.interrupt(fiber) + + assert.deepStrictEqual(commandCalls, [{ + query: "KILL QUERY WHERE query_id = {queryId:String}", + query_params: { queryId } + }]) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) + + it.effect("parameterizes the query id when cancelling an insert", () => + Effect.gen(function*() { + connectImmediately = true + commandCalls.length = 0 + const queryId = "id' OR 1 = 1 --" + const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" }) + const fiber = yield* client.withQueryId( + client.insertQuery({ table: "test", values: [] }), + queryId + ).pipe(Effect.forkScoped) + yield* Effect.yieldNow + + yield* Fiber.interrupt(fiber) + + assert.deepStrictEqual(commandCalls, [{ + query: "KILL QUERY WHERE query_id = {queryId:String}", + query_params: { queryId } + }]) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) }) diff --git a/.context/effect/packages/sql/clickhouse/test/SqlErrorClassification.test.ts b/.context/effect/packages/sql/clickhouse/test/SqlErrorClassification.test.ts index f2e3f0484..d0461345c 100644 --- a/.context/effect/packages/sql/clickhouse/test/SqlErrorClassification.test.ts +++ b/.context/effect/packages/sql/clickhouse/test/SqlErrorClassification.test.ts @@ -1,14 +1,17 @@ import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import { SqlError } from "effect/unstable/sql/SqlError" import { vi } from "vitest" const state: { connectCause: unknown queryCause: unknown + resultJsonCause: unknown } = { connectCause: null, - queryCause: null + queryCause: null, + resultJsonCause: null } vi.mock("@clickhouse/client", () => ({ @@ -19,7 +22,7 @@ vi.mock("@clickhouse/client", () => ({ state.queryCause ? Promise.reject(state.queryCause) : Promise.resolve({ - json: () => Promise.resolve({ data: [] }) + json: () => state.resultJsonCause ? Promise.reject(state.resultJsonCause) : Promise.resolve({ data: [] }) }), command: () => Promise.resolve({}), insert: () => Promise.resolve({}) @@ -30,6 +33,7 @@ const connectFailureReasonTag = (code: number) => Effect.gen(function*() { state.connectCause = { code } state.queryCause = null + state.resultJsonCause = null const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse")) const error = yield* Effect.flip(ClickhouseClient.make({ url: "http://localhost:8123" })) return error.reason._tag @@ -42,6 +46,7 @@ const queryFailureReasonTag = (code: number) => Effect.gen(function*() { state.connectCause = null state.queryCause = { code } + state.resultJsonCause = null const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse")) const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" }) const error = yield* Effect.flip(client`SELECT 1`) @@ -72,4 +77,21 @@ describe("ClickhouseClient SqlError classification", () => { const tag = yield* queryFailureReasonTag(999) assert.strictEqual(tag, "UnknownError") })) + + it.effect("maps result decoding failures to SqlError", () => + Effect.gen(function*() { + state.connectCause = null + state.queryCause = null + state.resultJsonCause = new SyntaxError("invalid JSON response") + const { ClickhouseClient } = yield* Effect.promise(() => import("@effect/sql-clickhouse")) + const client = yield* ClickhouseClient.make({ url: "http://localhost:8123" }) + const error = yield* Effect.flip(client`SELECT 1`) + + assert(error instanceof SqlError) + assert.strictEqual(error.reason._tag, "UnknownError") + assert.strictEqual(error.reason.cause, state.resultJsonCause) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) }) diff --git a/.context/effect/packages/sql/clickhouse/tsconfig.json b/.context/effect/packages/sql/clickhouse/tsconfig.json index 0040406ea..91fad270a 100644 --- a/.context/effect/packages/sql/clickhouse/tsconfig.json +++ b/.context/effect/packages/sql/clickhouse/tsconfig.json @@ -1,10 +1,10 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ], "compilerOptions": { "types": ["node"] diff --git a/.context/effect/packages/sql/clickhouse/vitest.config.ts b/.context/effect/packages/sql/clickhouse/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/clickhouse/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/d1/CHANGELOG.md b/.context/effect/packages/sql/d1/CHANGELOG.md index 82ec2fc6f..80465b076 100644 --- a/.context/effect/packages/sql/d1/CHANGELOG.md +++ b/.context/effect/packages/sql/d1/CHANGELOG.md @@ -1,5 +1,60 @@ # @effect/sql-d1 +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Minor Changes + +- [#6524](https://github.com/Effect-TS/effect/pull/6524) [`c5bf174`](https://github.com/Effect-TS/effect/commit/c5bf174cb3f2c02278d0daf6294ee1d0747a4ed8) Thanks @nr1brolyfan! - Add `D1Client.batch` for executing a collection of SQL statements as a single atomic D1 batch. + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/d1/README.md b/.context/effect/packages/sql/d1/README.md index 9981ef433..7f97c7372 100644 --- a/.context/effect/packages/sql/d1/README.md +++ b/.context/effect/packages/sql/d1/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-d1` +# @effect/sql-d1 -An Effect SQL implementation for [Cloudflare D1](https://developers.cloudflare.com/d1/). +An Effect SQL client for [Cloudflare D1](https://developers.cloudflare.com/d1/), for use in Cloudflare Workers. + +## Installation + +```sh +npm install effect@beta @effect/sql-d1@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-d1). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-d1) diff --git a/.context/effect/packages/sql/d1/docgen.json b/.context/effect/packages/sql/d1/docgen.json deleted file mode 100644 index 140bbc1b3..000000000 --- a/.context/effect/packages/sql/d1/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/d1/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/d1/package.json b/.context/effect/packages/sql/d1/package.json index 9c0b86c5f..41b5baf27 100644 --- a/.context/effect/packages/sql/d1/package.json +++ b/.context/effect/packages/sql/d1/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-d1", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A Cloudflare D1 integration for Effect", @@ -33,6 +33,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -40,7 +41,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -50,6 +54,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -57,9 +62,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^", diff --git a/.context/effect/packages/sql/d1/src/D1Client.ts b/.context/effect/packages/sql/d1/src/D1Client.ts index 50078f5bc..9ffffa467 100644 --- a/.context/effect/packages/sql/d1/src/D1Client.ts +++ b/.context/effect/packages/sql/d1/src/D1Client.ts @@ -27,6 +27,8 @@ import { SqlError, UnknownError } from "effect/unstable/sql/SqlError" import * as Statement from "effect/unstable/sql/Statement" const ATTR_DB_SYSTEM_NAME = "db.system.name" +const ATTR_DB_OPERATION_NAME = "db.operation.name" +const ATTR_DB_QUERY_TEXT = "db.query.text" const classifyError = (cause: unknown, message: string, operation: string) => new UnknownError({ cause, message, operation }) @@ -50,13 +52,38 @@ export type TypeId = "~@effect/sql-d1/D1Client" /** * Cloudflare D1 SQL client service, extending `SqlClient` with its D1 configuration and no `updateValues` support. * - * @category models + * @category services * @since 4.0.0 */ export interface D1Client extends Client.SqlClient { readonly [TypeId]: TypeId readonly config: D1ClientConfig + /** + * Executes SQL statements as a single atomic D1 batch and returns their row results in order. + * + * **When to use** + * + * Use when you have a fixed collection of statements that should run in one + * request and roll back together if any statement fails. + * + * **Gotchas** + * + * Each statement uses the query and result name transformations from the + * client that created it. Mixing clients can produce differently shaped row + * results within the same batch. + * + * @since 4.0.0 + */ + readonly batch: >>( + statements: Statements + ) => Effect.Effect< + { + readonly [K in keyof Statements]: Effect.Success + }, + SqlError + > + /** Not supported in d1 */ readonly updateValues: never } @@ -90,6 +117,78 @@ export interface D1ClientConfig { readonly transformQueryNames?: ((str: string) => string) | undefined } +type TransformRows = (rows: ReadonlyArray) => ReadonlyArray + +type BatchResults>> = { + readonly [K in keyof Statements]: Effect.Success +} + +interface StatementWithTransformRows extends Statement.Statement { + readonly transformRows: TransformRows | undefined +} + +const makeBatch = (options: { + readonly db: D1Database + readonly prepareCache: Cache.Cache + readonly spanAttributes: ReadonlyArray + readonly getClient: () => D1Client +}): D1Client["batch"] => +>>( + statements: Statements +) => { + if (statements.length === 0) { + return Effect.succeed([] as unknown as BatchResults) + } + return Effect.useSpan( + "sql.execute", + { kind: "client" }, + (span) => + Effect.withFiber(Effect.fnUntraced(function*(fiber) { + const transformer = fiber.getRef(Statement.CurrentTransformer) + const prepared: Array = [] + const transforms: Array = [] + const queryTexts: Array = [] + + for (const original of statements) { + const statement = transformer === undefined + ? original + : yield* transformer(original, options.getClient(), fiber, span) + const [sql, params] = statement.compile() + queryTexts.push(sql) + transforms.push((statement as StatementWithTransformRows).transformRows) + prepared.push((yield* Cache.get(options.prepareCache, sql)).bind(...params)) + } + + for (const [key, value] of options.spanAttributes) { + span.attribute(key, value) + } + span.attribute(ATTR_DB_OPERATION_NAME, "batch") + span.attribute(ATTR_DB_QUERY_TEXT, queryTexts.join("; ")) + + // D1 batches execute on the binding directly and intentionally cannot participate in SqlClient transactions. + const responses = yield* Effect.tryPromise({ + try: () => + options.db.batch>(prepared).then((responses) => { + for (const response of responses) { + if (response.error) { + throw response.error + } + } + return responses + }), + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute batch", "execute") }) + }) + + const results = responses.map((response, index) => { + const rows = response.results || [] + const transformRows = transforms[index] + return transformRows ? transformRows(rows) : rows + }) + return results as BatchResults + })) + ) +} + /** * Creates a scoped Cloudflare D1 SQL client. Prepared statements are cached, while transactions and streaming queries are not supported by this driver. * @@ -104,6 +203,10 @@ export const make = ( const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined + const spanAttributes: Array = [ + ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), + [ATTR_DB_SYSTEM_NAME, "sqlite"] + ] const makeConnection = Effect.gen(function*() { const db = options.db @@ -182,7 +285,7 @@ export const make = ( catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) }) - return identity({ + const connection = identity({ execute(sql, params, transformRows) { return transformRows ? Effect.map(runCached(sql, params), transformRows) @@ -206,28 +309,60 @@ export const make = ( return Stream.die("executeStream not implemented") } }) + return { connection, prepareCache } as const }) - const connection = yield* makeConnection + const { connection, prepareCache } = yield* makeConnection const acquirer = Effect.succeed(connection) const transactionAcquirer = Effect.die("transactions are not supported in D1") - return Object.assign( + let client!: D1Client + client = Object.assign( (yield* Client.make({ acquirer, compiler, transactionAcquirer, - spanAttributes: [ - ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), - [ATTR_DB_SYSTEM_NAME, "sqlite"] - ], + spanAttributes, transformRows })) as D1Client, { [TypeId]: TypeId as TypeId, - config: options + config: options, + batch: makeBatch({ + db: options.db, + prepareCache, + spanAttributes, + getClient: () => client + }) } ) + + if (options.transformQueryNames !== undefined || transformRows !== undefined) { + const clientWithoutTransformsBase = yield* Client.make({ + acquirer: Effect.succeed(connection), + compiler: compiler.withoutTransform, + transactionAcquirer, + spanAttributes, + transformRows: undefined + }) + let clientWithoutTransforms!: D1Client + clientWithoutTransforms = Object.assign(clientWithoutTransformsBase as D1Client, { + [TypeId]: TypeId as TypeId, + config: options, + batch: makeBatch({ + db: options.db, + prepareCache, + spanAttributes, + getClient: () => clientWithoutTransforms + }), + withoutTransforms: () => clientWithoutTransforms + }) + Object.assign(client, { + withoutTransforms: () => clientWithoutTransforms + }) + } + + return client }) /** diff --git a/.context/effect/packages/sql/d1/test/Client.test.ts b/.context/effect/packages/sql/d1/test/Client.test.ts index a8678b24c..7c658d227 100644 --- a/.context/effect/packages/sql/d1/test/Client.test.ts +++ b/.context/effect/packages/sql/d1/test/Client.test.ts @@ -2,6 +2,7 @@ import { D1Client } from "@effect/sql-d1" import { assert, describe, it } from "@effect/vitest" import { Cause, Effect } from "effect" import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import { Statement } from "effect/unstable/sql" import { D1Miniflare } from "./utils.ts" describe("Client", () => { @@ -49,6 +50,137 @@ describe("Client", () => { assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) }).pipe(Effect.provide(D1Miniflare.layerClient))) + it.effect("should execute statements in a batch", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + + const results: readonly [ + ReadonlyArray<{ id: number; name: string }>, + ReadonlyArray<{ id: number; name: string }>, + ReadonlyArray<{ count: number }> + ] = yield* sql.batch( + [ + sql<{ id: number; name: string }>`INSERT INTO test (name) VALUES (${"hello"}) RETURNING *`, + sql<{ id: number; name: string }>`INSERT INTO test (name) VALUES (${"world"}) RETURNING *`, + sql<{ count: number }>`SELECT COUNT(*) AS count FROM test` + ] as const + ) + + assert.deepStrictEqual(results, [ + [{ id: 1, name: "hello" }], + [{ id: 2, name: "world" }], + [{ count: 2 }] + ]) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should apply result transforms to batch results", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (first_name TEXT, "firstName" TEXT)` + yield* sql`INSERT INTO test (first_name, "firstName") VALUES ('John', 'Jane')` + + yield* Effect.gen(function*() { + const transformed = yield* D1Client.make({ + db: sql.config.db, + transformQueryNames: (name) => name === "firstName" ? "first_name" : name, + transformResultNames: (name) => name === "first_name" ? "firstName" : name + }) + const [rows] = yield* transformed.batch([ + transformed<{ firstName: string }>`SELECT ${transformed("firstName")} FROM test` + ]) + assert.deepStrictEqual(rows, [{ firstName: "John" }]) + + const withoutTransforms = transformed.withoutTransforms() + const [rawRows] = yield* withoutTransforms.batch([ + withoutTransforms<{ firstName: string }>`SELECT ${withoutTransforms("firstName")} FROM test` + ]) + assert.deepStrictEqual(rawRows, [{ firstName: "Jane" }]) + + const [rawRowsFromTransformedBatch] = yield* transformed.batch([ + withoutTransforms<{ first_name: string }>`SELECT ${withoutTransforms("first_name")} FROM test` + ]) + assert.deepStrictEqual(rawRowsFromTransformedBatch, [{ first_name: "John" }]) + + const [transformedRowsFromRawBatch] = yield* withoutTransforms.batch([ + transformed<{ firstName: string }>`SELECT ${transformed("firstName")} FROM test` + ]) + assert.deepStrictEqual(transformedRowsFromRawBatch, [{ firstName: "John" }]) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should disable query-only transforms", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (first_name TEXT, "firstName" TEXT)` + yield* sql`INSERT INTO test (first_name, "firstName") VALUES ('John', 'Jane')` + + yield* Effect.gen(function*() { + const transformed = yield* D1Client.make({ + db: sql.config.db, + transformQueryNames: (name) => name === "firstName" ? "first_name" : name + }) + const [transformedRows] = yield* transformed.batch([ + transformed<{ first_name: string }>`SELECT ${transformed("firstName")} FROM test` + ]) + assert.deepStrictEqual(transformedRows, [{ first_name: "John" }]) + + const withoutTransforms = transformed.withoutTransforms() + const [rawRows] = yield* withoutTransforms.batch([ + withoutTransforms<{ firstName: string }>`SELECT ${withoutTransforms("firstName")} FROM test` + ]) + assert.deepStrictEqual(rawRows, [{ firstName: "Jane" }]) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should roll back a failed batch", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT UNIQUE)` + + const error = yield* sql.batch([ + sql`INSERT INTO test (name) VALUES (${"duplicate"})`, + sql`INSERT INTO test (name) VALUES (${"duplicate"})` + ]).pipe(Effect.flip) + + assert.strictEqual(error.reason._tag, "UnknownError") + assert.strictEqual(error.reason.operation, "execute") + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, []) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should support an empty batch", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + const results: readonly [] = yield* sql.batch([]) + assert.deepStrictEqual(results, []) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should apply statement transformers in a batch", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + const queries: Array = [] + + const results = yield* sql.batch([ + sql`SELECT ${1} AS value`, + sql`SELECT ${2} AS value` + ]).pipe( + Effect.provideService(Statement.CurrentTransformer, (statement, sql) => + Effect.sync(() => { + queries.push(statement.compile()[0]) + }).pipe(Effect.as(sql`SELECT ${3} AS value`))) + ) + + assert.deepStrictEqual(queries, ["SELECT ? AS value", "SELECT ? AS value"]) + assert.deepStrictEqual(results, [[{ value: 3 }], [{ value: 3 }]]) + }).pipe(Effect.provide(D1Miniflare.layerClient))) + it.effect("should defect on transactions", () => Effect.gen(function*() { const sql = yield* D1Client.D1Client @@ -62,4 +194,20 @@ describe("Client", () => { assert.deepStrictEqual(rows, []) assert.equal(Cause.hasDies(res), true) }).pipe(Effect.provide(D1Miniflare.layerClient))) + + it.effect("should defect when batching in a transaction", () => + Effect.gen(function*() { + const sql = yield* D1Client.D1Client + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + const res = yield* sql.batch([ + sql`INSERT INTO test ${sql.insert({ name: "hello" })}` + ]).pipe( + sql.withTransaction, + Effect.sandbox, + Effect.flip + ) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, []) + assert.equal(Cause.hasDies(res), true) + }).pipe(Effect.provide(D1Miniflare.layerClient))) }) diff --git a/.context/effect/packages/sql/d1/test/Resolver.test.ts b/.context/effect/packages/sql/d1/test/Resolver.test.ts index 047f538ee..0f203f285 100644 --- a/.context/effect/packages/sql/d1/test/Resolver.test.ts +++ b/.context/effect/packages/sql/d1/test/Resolver.test.ts @@ -1,6 +1,6 @@ import { D1Client } from "@effect/sql-d1" import { assert, describe, it } from "@effect/vitest" -import { Cause, Effect, Iterable } from "effect" +import { Cause, Effect } from "effect" import * as Schema from "effect/Schema" import { SqlError, SqlResolver } from "effect/unstable/sql" import { D1Miniflare } from "./utils.ts" @@ -8,9 +8,14 @@ import { D1Miniflare } from "./utils.ts" const seededClient = Effect.gen(function*() { const sql = yield* D1Client.D1Client yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` - for (const id of Iterable.range(1, 100)) { - yield* sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}` - } + yield* sql`INSERT INTO test ${ + sql.insert([ + { id: 1, name: "name1" }, + { id: 2, name: "name2" }, + { id: 3, name: "name3" }, + { id: 100, name: "name100" } + ]) + }` return sql }) diff --git a/.context/effect/packages/sql/d1/tsconfig.json b/.context/effect/packages/sql/d1/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/d1/tsconfig.json +++ b/.context/effect/packages/sql/d1/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/d1/vitest.config.ts b/.context/effect/packages/sql/d1/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/d1/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/libsql/CHANGELOG.md b/.context/effect/packages/sql/libsql/CHANGELOG.md index b6b2497e3..b457a1173 100644 --- a/.context/effect/packages/sql/libsql/CHANGELOG.md +++ b/.context/effect/packages/sql/libsql/CHANGELOG.md @@ -1,5 +1,58 @@ # @effect/sql-libsql +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6858](https://github.com/Effect-TS/effect/pull/6858) [`9fb59a5`](https://github.com/Effect-TS/effect/commit/9fb59a5bab412182320239c86068da23df5b2172) Thanks @fubhy! - Release transaction serialization when beginning a libSQL transaction fails, allowing later operations to retry. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/libsql/README.md b/.context/effect/packages/sql/libsql/README.md index 02a2379bf..896e87043 100644 --- a/.context/effect/packages/sql/libsql/README.md +++ b/.context/effect/packages/sql/libsql/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-libsql` +# @effect/sql-libsql -An Effect SQL implementation using the `@libsql/client` library. +An Effect SQL client for [libSQL](https://turso.tech/libsql), built on the [`@libsql/client`](https://docs.turso.tech/sdk/ts) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-libsql@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-libsql). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-libsql) diff --git a/.context/effect/packages/sql/libsql/docgen.json b/.context/effect/packages/sql/libsql/docgen.json deleted file mode 100644 index 76309a9f3..000000000 --- a/.context/effect/packages/sql/libsql/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/libsql/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/libsql/package.json b/.context/effect/packages/sql/libsql/package.json index 2da93517b..cd2cb53d7 100644 --- a/.context/effect/packages/sql/libsql/package.json +++ b/.context/effect/packages/sql/libsql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-libsql", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A libSQL toolkit for Effect", @@ -31,6 +31,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -38,7 +39,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -48,6 +52,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -55,13 +60,11 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^", - "testcontainers": "^11.14.0" + "testcontainers": "^12.0.4" }, "peerDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/sql/libsql/src/LibsqlClient.ts b/.context/effect/packages/sql/libsql/src/LibsqlClient.ts index 4b574e039..55c12997e 100644 --- a/.context/effect/packages/sql/libsql/src/LibsqlClient.ts +++ b/.context/effect/packages/sql/libsql/src/LibsqlClient.ts @@ -13,6 +13,7 @@ import * as Libsql from "@libsql/client" import * as Config from "effect/Config" import * as Context from "effect/Context" 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 Redacted from "effect/Redacted" @@ -49,7 +50,7 @@ export type TypeId = "~@effect/sql-libsql/LibsqlClient" /** * libSQL-backed SQL client service, extending `SqlClient` with its runtime type marker and client configuration. * - * @category models + * @category services * @since 4.0.0 */ export interface LibsqlClient extends Client.SqlClient { @@ -311,7 +312,9 @@ export const make = ( const scope = Scope.makeUnsafe() yield* restore(semaphore.take(1)) yield* Scope.addFinalizer(scope, semaphore.release(1)) - const conn = yield* connection.beginTransaction + const conn = yield* connection.beginTransaction.pipe( + Effect.tapCause((cause) => Scope.close(scope, Exit.failCause(cause))) + ) return [scope, conn] as const })), begin: () => Effect.void, // already begun in acquireConnection diff --git a/.context/effect/packages/sql/libsql/src/LibsqlMigrator.ts b/.context/effect/packages/sql/libsql/src/LibsqlMigrator.ts index 9d7082436..210ca70fe 100644 --- a/.context/effect/packages/sql/libsql/src/LibsqlMigrator.ts +++ b/.context/effect/packages/sql/libsql/src/LibsqlMigrator.ts @@ -24,7 +24,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -38,7 +38,7 @@ export const run: ( /** * Creates a layer that runs the configured SQL migrations during layer construction. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/libsql/test/Client.integration.test.ts b/.context/effect/packages/sql/libsql/test/Client.integration.test.ts new file mode 100644 index 000000000..352f647d5 --- /dev/null +++ b/.context/effect/packages/sql/libsql/test/Client.integration.test.ts @@ -0,0 +1,139 @@ +import { LibsqlClient } from "@effect/sql-libsql" +import { assert, describe, it, layer } from "@effect/vitest" +import { Effect, Exit, Layer } from "effect" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import { LibsqlContainer } from "./util.ts" + +const Migrations = Layer.effectDiscard( + LibsqlClient.LibsqlClient.pipe( + Effect.andThen((sql) => + Effect.acquireRelease( + sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`, + () => sql`DROP TABLE test;`.pipe(Effect.ignore) + ) + ) + ) +) + +describe("Client", () => { + it.effect("releases transaction serialization after begin fails", () => { + let transactionCalls = 0 + const transaction = { + execute: () => Promise.resolve({ rows: [] }), + commit: () => Promise.resolve(), + rollback: () => Promise.resolve() + } + const liveClient = { + execute: () => Promise.resolve({ rows: [] }), + transaction: () => { + transactionCalls++ + return transactionCalls === 1 + ? Promise.reject(new Error("transient begin failure")) + : Promise.resolve(transaction) + } + } + + return Effect.gen(function*() { + const client = yield* LibsqlClient.make({ liveClient: liveClient as any }) + const first = yield* Effect.exit(client.withTransaction(Effect.void)) + assert.isTrue(Exit.isFailure(first)) + + yield* Effect.forkChild(client.withTransaction(Effect.void)) + yield* Effect.yieldNow + + assert.strictEqual(transactionCalls, 2) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ) + }) + + layer(LibsqlContainer.layerClient, { timeout: "30 seconds" })((it) => { + it.effect("should work", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + let response = yield* sql`INSERT INTO test (name) VALUES ('hello')` + assert.deepStrictEqual(response, []) + response = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(response, [{ id: 1, name: "hello" }]) + response = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(yield* sql`select * from test`.values, [ + [1, "hello"] + ]) + }).pipe(Effect.provide(Migrations))) + + it.effect("should work with raw", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + let response: any + response = yield* sql`CREATE TABLE test2 (id INTEGER PRIMARY KEY, name TEXT)`.raw + yield* Effect.addFinalizer(() => sql`DROP TABLE test2;`.pipe(Effect.ignore)) + assert.deepStrictEqual(response.toJSON(), { + columnTypes: [], + columns: [], + lastInsertRowid: null, + rows: [], + rowsAffected: 0 + }) + response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw + assert.deepStrictEqual(response.toJSON(), { + columnTypes: [], + columns: [], + lastInsertRowid: "1", + rows: [], + rowsAffected: 1 + }) + response = yield* sql`SELECT * FROM test`.raw + assert.deepStrictEqual(response.toJSON(), { + columnTypes: ["INTEGER", "TEXT"], + columns: ["id", "name"], + lastInsertRowid: null, + rows: [[1, "hello"]], + rowsAffected: 0 + }) + }).pipe(Effect.provide(Migrations))) + + it.effect("withTransaction", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) + }).pipe(Effect.provide(Migrations))) + + it.effect("withTransaction rollback", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe( + Effect.andThen(Effect.fail("boom")), + sql.withTransaction, + Effect.ignore + ) + const rows = yield* sql`SELECT * FROM test` + assert.deepStrictEqual(rows, []) + }).pipe(Effect.provide(Migrations))) + + it.effect("withTransaction nested", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + const stmt = sql`INSERT INTO test (name) VALUES ('hello')` + + yield* stmt.pipe(Effect.andThen(() => stmt.pipe(sql.withTransaction)), sql.withTransaction) + const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` + assert.deepStrictEqual(rows.at(0)?.total_rows, 2) + }).pipe(Effect.provide(Migrations))) + + it.effect("withTransaction nested rollback", () => + Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + const stmt = sql`INSERT INTO test (name) VALUES ('hello')` + + yield* stmt.pipe( + Effect.andThen(() => stmt.pipe(Effect.andThen(Effect.fail("boom")), sql.withTransaction, Effect.ignore)), + sql.withTransaction + ) + const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` + assert.deepStrictEqual(rows.at(0)?.total_rows, 1) + }).pipe(Effect.provide(Migrations))) + }) +}) diff --git a/.context/effect/packages/sql/libsql/test/Client.test.ts b/.context/effect/packages/sql/libsql/test/Client.test.ts deleted file mode 100644 index c2f925f7b..000000000 --- a/.context/effect/packages/sql/libsql/test/Client.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { LibsqlClient } from "@effect/sql-libsql" -import { assert, describe, layer } from "@effect/vitest" -import { Effect, Layer } from "effect" -import { LibsqlContainer } from "./util.ts" - -const Migrations = Layer.effectDiscard( - LibsqlClient.LibsqlClient.pipe( - Effect.andThen((sql) => - Effect.acquireRelease( - sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)`, - () => sql`DROP TABLE test;`.pipe(Effect.ignore) - ) - ) - ) -) - -describe("Client", () => { - layer(LibsqlContainer.layerClient, { timeout: "30 seconds" })((it) => { - it.effect("should work", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - let response = yield* sql`INSERT INTO test (name) VALUES ('hello')` - assert.deepStrictEqual(response, []) - response = yield* sql`SELECT * FROM test` - assert.deepStrictEqual(response, [{ id: 1, name: "hello" }]) - response = yield* sql`SELECT * FROM test` - assert.deepStrictEqual(yield* sql`select * from test`.values, [ - [1, "hello"] - ]) - }).pipe(Effect.provide(Migrations))) - - it.effect("should work with raw", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - let response: any - response = yield* sql`CREATE TABLE test2 (id INTEGER PRIMARY KEY, name TEXT)`.raw - yield* Effect.addFinalizer(() => sql`DROP TABLE test2;`.pipe(Effect.ignore)) - assert.deepStrictEqual(response.toJSON(), { - columnTypes: [], - columns: [], - lastInsertRowid: null, - rows: [], - rowsAffected: 0 - }) - response = yield* sql`INSERT INTO test (name) VALUES ('hello')`.raw - assert.deepStrictEqual(response.toJSON(), { - columnTypes: [], - columns: [], - lastInsertRowid: "1", - rows: [], - rowsAffected: 1 - }) - response = yield* sql`SELECT * FROM test`.raw - assert.deepStrictEqual(response.toJSON(), { - columnTypes: ["INTEGER", "TEXT"], - columns: ["id", "name"], - lastInsertRowid: null, - rows: [[1, "hello"]], - rowsAffected: 0 - }) - }).pipe(Effect.provide(Migrations))) - - it.effect("withTransaction", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - yield* sql.withTransaction(sql`INSERT INTO test (name) VALUES ('hello')`) - const rows = yield* sql`SELECT * FROM test` - assert.deepStrictEqual(rows, [{ id: 1, name: "hello" }]) - }).pipe(Effect.provide(Migrations))) - - it.effect("withTransaction rollback", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - yield* sql`INSERT INTO test (name) VALUES ('hello')`.pipe( - Effect.andThen(Effect.fail("boom")), - sql.withTransaction, - Effect.ignore - ) - const rows = yield* sql`SELECT * FROM test` - assert.deepStrictEqual(rows, []) - }).pipe(Effect.provide(Migrations))) - - it.effect("withTransaction nested", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - const stmt = sql`INSERT INTO test (name) VALUES ('hello')` - - yield* stmt.pipe(Effect.andThen(() => stmt.pipe(sql.withTransaction)), sql.withTransaction) - const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` - assert.deepStrictEqual(rows.at(0)?.total_rows, 2) - }).pipe(Effect.provide(Migrations))) - - it.effect("withTransaction nested rollback", () => - Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - const stmt = sql`INSERT INTO test (name) VALUES ('hello')` - - yield* stmt.pipe( - Effect.andThen(() => stmt.pipe(Effect.andThen(Effect.fail("boom")), sql.withTransaction, Effect.ignore)), - sql.withTransaction - ) - const rows = yield* sql<{ total_rows: number }>`select count(*) as total_rows FROM test` - assert.deepStrictEqual(rows.at(0)?.total_rows, 1) - }).pipe(Effect.provide(Migrations))) - }) -}) diff --git a/.context/effect/packages/sql/libsql/test/Resolver.integration.test.ts b/.context/effect/packages/sql/libsql/test/Resolver.integration.test.ts new file mode 100644 index 000000000..6e98a9eac --- /dev/null +++ b/.context/effect/packages/sql/libsql/test/Resolver.integration.test.ts @@ -0,0 +1,182 @@ +import { LibsqlClient } from "@effect/sql-libsql" +import { assert, describe, layer } from "@effect/vitest" +import { Cause, Effect } from "effect" +import * as Schema from "effect/Schema" +import { SqlError, SqlResolver } from "effect/unstable/sql" +import { LibsqlContainer } from "./util.ts" + +const seededClient = Effect.gen(function*() { + const sql = yield* LibsqlClient.LibsqlClient + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` + yield* sql`INSERT INTO test ${ + sql.insert([ + { id: 1, name: "name1" }, + { id: 2, name: "name2" }, + { id: 3, name: "name3" }, + { id: 100, name: "name100" } + ]) + }` + yield* Effect.addFinalizer(() => sql`DROP TABLE test;`.pipe(Effect.orDie)) + return sql +}) + +layer(LibsqlContainer.layerClient, { timeout: "30 seconds" })("Resolver", (it) => { + describe.sequential("ordered", () => { + it.effect("insert", () => + Effect.gen(function*() { + const batches: Array> = [] + const sql = yield* seededClient + const Insert = SqlResolver.ordered({ + Request: Schema.String, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + execute: (names) => { + batches.push(names) + return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *` + } + }) + const execute = SqlResolver.request(Insert) + assert.deepStrictEqual( + yield* Effect.all({ + one: execute("one"), + two: execute("two") + }, { concurrency: "unbounded" }), + { + one: { id: 101, name: "one" }, + two: { id: 102, name: "two" } + } + ) + assert.deepStrictEqual(batches, [["one", "two"]]) + })) + + it.effect("result length mismatch", () => + Effect.gen(function*() { + const batches: Array> = [] + const sql = yield* seededClient + const Select = SqlResolver.ordered({ + Request: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + execute: (ids) => { + batches.push(ids) + return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + } + }) + const execute = SqlResolver.request(Select) + const error = yield* Effect.all([ + execute(1), + execute(2), + execute(3), + execute(101) + ], { concurrency: "unbounded" }).pipe( + Effect.flip + ) + assert(error instanceof SqlError.ResultLengthMismatch) + assert.strictEqual(error.actual, 3) + assert.strictEqual(error.expected, 4) + assert.deepStrictEqual(batches, [[1, 2, 3, 101]]) + })) + }) + + describe.sequential("grouped", () => { + it.effect("find by name", () => + Effect.gen(function*() { + const sql = yield* seededClient + const FindByName = SqlResolver.grouped({ + Request: Schema.String, + RequestGroupKey: (name) => name, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultGroupKey: (result) => result.name, + execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` + }) + yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}` + const execute = SqlResolver.request(FindByName) + assert.deepStrictEqual( + yield* Effect.all({ + one: execute("name1"), + two: execute("name2"), + three: Effect.flip(execute("name0")) + }, { concurrency: "unbounded" }), + { + one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], + two: [{ id: 2, name: "name2" }], + three: new Cause.NoSuchElementError() + } + ) + })) + + it.effect("using raw rows", () => + Effect.gen(function*() { + const sql = yield* seededClient + const FindByName = SqlResolver.grouped({ + Request: Schema.String, + RequestGroupKey: (name) => name, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultGroupKey: (_, result: any) => result.name, + execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` + }) + yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}` + const execute = SqlResolver.request(FindByName) + assert.deepStrictEqual( + yield* Effect.all({ + one: execute("name1"), + two: execute("name2"), + three: Effect.flip(execute("name0")) + }, { concurrency: "unbounded" }), + { + one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], + two: [{ id: 2, name: "name2" }], + three: new Cause.NoSuchElementError() + } + ) + })) + }) + + describe.sequential("findById", () => { + it.effect("find by id", () => + Effect.gen(function*() { + const sql = yield* seededClient + const FindById = SqlResolver.findById({ + Id: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultId: (result) => result.id, + execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + }) + const execute = SqlResolver.request(FindById) + assert.deepStrictEqual( + yield* Effect.all({ + one: execute(1), + two: execute(2), + three: Effect.flip(execute(101)) + }, { concurrency: "unbounded" }), + { + one: { id: 1, name: "name1" }, + two: { id: 2, name: "name2" }, + three: new Cause.NoSuchElementError() + } + ) + })) + + it.effect("using raw rows", () => + Effect.gen(function*() { + const sql = yield* seededClient + const FindById = SqlResolver.findById({ + Id: Schema.Number, + Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), + ResultId: (_, result: any) => result.id, + execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` + }) + const execute = SqlResolver.request(FindById) + assert.deepStrictEqual( + yield* Effect.all({ + one: execute(1), + two: execute(2), + three: Effect.flip(execute(101)) + }, { concurrency: "unbounded" }), + { + one: { id: 1, name: "name1" }, + two: { id: 2, name: "name2" }, + three: new Cause.NoSuchElementError() + } + ) + })) + }) +}) diff --git a/.context/effect/packages/sql/libsql/test/Resolver.test.ts b/.context/effect/packages/sql/libsql/test/Resolver.test.ts deleted file mode 100644 index af8b19bc2..000000000 --- a/.context/effect/packages/sql/libsql/test/Resolver.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { LibsqlClient } from "@effect/sql-libsql" -import { assert, describe, layer } from "@effect/vitest" -import { Cause, Effect, Iterable } from "effect" -import * as Schema from "effect/Schema" -import { SqlError, SqlResolver } from "effect/unstable/sql" -import { LibsqlContainer } from "./util.ts" - -const seededClient = Effect.gen(function*() { - const sql = yield* LibsqlClient.LibsqlClient - yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)` - for (const id of Iterable.range(1, 100)) { - yield* sql`INSERT INTO test ${sql.insert({ id, name: `name${id}` })}` - } - yield* Effect.addFinalizer(() => sql`DROP TABLE test;`.pipe(Effect.orDie)) - return sql -}) - -layer(LibsqlContainer.layerClient, { timeout: "30 seconds" })("Resolver", (it) => { - describe.sequential("ordered", () => { - it.effect("insert", () => - Effect.gen(function*() { - const batches: Array> = [] - const sql = yield* seededClient - const Insert = SqlResolver.ordered({ - Request: Schema.String, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - execute: (names) => { - batches.push(names) - return sql`INSERT INTO test ${sql.insert(names.map((name) => ({ name })))} RETURNING *` - } - }) - const execute = SqlResolver.request(Insert) - assert.deepStrictEqual( - yield* Effect.all({ - one: execute("one"), - two: execute("two") - }, { concurrency: "unbounded" }), - { - one: { id: 101, name: "one" }, - two: { id: 102, name: "two" } - } - ) - assert.deepStrictEqual(batches, [["one", "two"]]) - })) - - it.effect("result length mismatch", () => - Effect.gen(function*() { - const batches: Array> = [] - const sql = yield* seededClient - const Select = SqlResolver.ordered({ - Request: Schema.Number, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - execute: (ids) => { - batches.push(ids) - return sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` - } - }) - const execute = SqlResolver.request(Select) - const error = yield* Effect.all([ - execute(1), - execute(2), - execute(3), - execute(101) - ], { concurrency: "unbounded" }).pipe( - Effect.flip - ) - assert(error instanceof SqlError.ResultLengthMismatch) - assert.strictEqual(error.actual, 3) - assert.strictEqual(error.expected, 4) - assert.deepStrictEqual(batches, [[1, 2, 3, 101]]) - })) - }) - - describe.sequential("grouped", () => { - it.effect("find by name", () => - Effect.gen(function*() { - const sql = yield* seededClient - const FindByName = SqlResolver.grouped({ - Request: Schema.String, - RequestGroupKey: (name) => name, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - ResultGroupKey: (result) => result.name, - execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` - }) - yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}` - const execute = SqlResolver.request(FindByName) - assert.deepStrictEqual( - yield* Effect.all({ - one: execute("name1"), - two: execute("name2"), - three: Effect.flip(execute("name0")) - }, { concurrency: "unbounded" }), - { - one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], - two: [{ id: 2, name: "name2" }], - three: new Cause.NoSuchElementError() - } - ) - })) - - it.effect("using raw rows", () => - Effect.gen(function*() { - const sql = yield* seededClient - const FindByName = SqlResolver.grouped({ - Request: Schema.String, - RequestGroupKey: (name) => name, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - ResultGroupKey: (_, result: any) => result.name, - execute: (names) => sql`SELECT * FROM test WHERE name IN ${sql.in(names)}` - }) - yield* sql`INSERT INTO test ${sql.insert({ name: "name1" })}` - const execute = SqlResolver.request(FindByName) - assert.deepStrictEqual( - yield* Effect.all({ - one: execute("name1"), - two: execute("name2"), - three: Effect.flip(execute("name0")) - }, { concurrency: "unbounded" }), - { - one: [{ id: 1, name: "name1" }, { id: 101, name: "name1" }], - two: [{ id: 2, name: "name2" }], - three: new Cause.NoSuchElementError() - } - ) - })) - }) - - describe.sequential("findById", () => { - it.effect("find by id", () => - Effect.gen(function*() { - const sql = yield* seededClient - const FindById = SqlResolver.findById({ - Id: Schema.Number, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - ResultId: (result) => result.id, - execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` - }) - const execute = SqlResolver.request(FindById) - assert.deepStrictEqual( - yield* Effect.all({ - one: execute(1), - two: execute(2), - three: Effect.flip(execute(101)) - }, { concurrency: "unbounded" }), - { - one: { id: 1, name: "name1" }, - two: { id: 2, name: "name2" }, - three: new Cause.NoSuchElementError() - } - ) - })) - - it.effect("using raw rows", () => - Effect.gen(function*() { - const sql = yield* seededClient - const FindById = SqlResolver.findById({ - Id: Schema.Number, - Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), - ResultId: (_, result: any) => result.id, - execute: (ids) => sql`SELECT * FROM test WHERE id IN ${sql.in(ids)}` - }) - const execute = SqlResolver.request(FindById) - assert.deepStrictEqual( - yield* Effect.all({ - one: execute(1), - two: execute(2), - three: Effect.flip(execute(101)) - }, { concurrency: "unbounded" }), - { - one: { id: 1, name: "name1" }, - two: { id: 2, name: "name2" }, - three: new Cause.NoSuchElementError() - } - ) - })) - }) -}) diff --git a/.context/effect/packages/sql/libsql/tsconfig.json b/.context/effect/packages/sql/libsql/tsconfig.json index 739eb6b55..fce3e3d43 100644 --- a/.context/effect/packages/sql/libsql/tsconfig.json +++ b/.context/effect/packages/sql/libsql/tsconfig.json @@ -1,9 +1,9 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/sql/libsql/vitest.config.ts b/.context/effect/packages/sql/libsql/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/libsql/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/mssql/CHANGELOG.md b/.context/effect/packages/sql/mssql/CHANGELOG.md index 54f3ff223..e8a3ee71d 100644 --- a/.context/effect/packages/sql/mssql/CHANGELOG.md +++ b/.context/effect/packages/sql/mssql/CHANGELOG.md @@ -1,5 +1,67 @@ # @effect/sql-mssql +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#6999](https://github.com/Effect-TS/effect/pull/6999) [`e184f73`](https://github.com/Effect-TS/effect/commit/e184f7367e837623d1d3704224e6d27cec99faa1) Thanks @fubhy! - Cancel in-flight Tedious requests when their Effects are interrupted. + +- [#6995](https://github.com/Effect-TS/effect/pull/6995) [`6e7f2e2`](https://github.com/Effect-TS/effect/commit/6e7f2e2d1dacde69cbbbe1ba572aa5f52491dc33) Thanks @fubhy! - Return MSSQL procedure values through the output property. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6766](https://github.com/Effect-TS/effect/pull/6766) [`f9ba412`](https://github.com/Effect-TS/effect/commit/f9ba41282e9ea207ddcf97fb0bcf369dd3c6a551) Thanks @tim-smart! - **Breaking:** Secure Microsoft SQL Server connections by default by enabling encryption and validating server certificates. + + Users connecting to SQL Server instances without TLS must now explicitly set `encrypt: false`. Users connecting with untrusted or self-signed certificates must explicitly set `trustServer: true`. + +- [#6922](https://github.com/Effect-TS/effect/pull/6922) [`c53591f`](https://github.com/Effect-TS/effect/commit/c53591f2b02cfa2c6ff294b040ea45a8deb2df9c) Thanks @fubhy! - Preserve fractional numbers and Unicode strings in default Microsoft SQL Server parameters. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/mssql/README.md b/.context/effect/packages/sql/mssql/README.md index ef8f1f269..9bb80923c 100644 --- a/.context/effect/packages/sql/mssql/README.md +++ b/.context/effect/packages/sql/mssql/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-mssql` +# @effect/sql-mssql -An Effect SQL implementation using the mssql `tedious` library. +An Effect SQL client for Microsoft SQL Server, built on the [`tedious`](https://tediousjs.github.io/tedious/) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-mssql@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-mssql). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-mssql) diff --git a/.context/effect/packages/sql/mssql/docgen.json b/.context/effect/packages/sql/mssql/docgen.json deleted file mode 100644 index 7c8e131a5..000000000 --- a/.context/effect/packages/sql/mssql/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/mssql/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/mssql/package.json b/.context/effect/packages/sql/mssql/package.json index 59e4d09b8..bbe640737 100644 --- a/.context/effect/packages/sql/mssql/package.json +++ b/.context/effect/packages/sql/mssql/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mssql", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A Microsoft SQL Server toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,11 +58,10 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { + "@testcontainers/mssqlserver": "^12.0.4", "effect": "workspace:^" }, "peerDependencies": { diff --git a/.context/effect/packages/sql/mssql/src/MssqlClient.ts b/.context/effect/packages/sql/mssql/src/MssqlClient.ts index d58c0f734..481e0e7b4 100644 --- a/.context/effect/packages/sql/mssql/src/MssqlClient.ts +++ b/.context/effect/packages/sql/mssql/src/MssqlClient.ts @@ -19,6 +19,7 @@ import * as Effect from "effect/Effect" import { identity } from "effect/Function" import * as Layer from "effect/Layer" import * as Pool from "effect/Pool" +import * as Rec from "effect/Record" import * as Redacted from "effect/Redacted" import * as Scope from "effect/Scope" import * as Stream from "effect/Stream" @@ -159,7 +160,7 @@ export type TypeId = typeof TypeId /** * Microsoft SQL Server client service, extending `SqlClient` with typed parameter fragments and stored procedure calls. * - * @category models + * @category services * @since 4.0.0 */ export interface MssqlClient extends Client.SqlClient { @@ -205,7 +206,13 @@ export interface MssqlClientConfig { readonly domain?: string | undefined readonly server: string readonly instanceName?: string | undefined + /** + * Whether to encrypt traffic between the client and server. Defaults to `true`. Setting this to `false` disables transport encryption and transmits credentials in cleartext. + */ readonly encrypt?: boolean | undefined + /** + * Whether to trust the server certificate without validating it. Defaults to `false`. Setting this to `true` disables TLS certificate validation. + */ readonly trustServer?: boolean | undefined readonly port?: number | undefined readonly authType?: string | undefined @@ -283,7 +290,7 @@ export const make = ( options: { port: options.port, database: options.database, - trustServerCertificate: options.trustServer ?? true, + trustServerCertificate: options.trustServer ?? false, multiSubnetFailover: options.multiSubnetFailover, connectTimeout: options.connectTimeout ? Duration.toMillis(Duration.fromInputUnsafe(options.connectTimeout)) @@ -291,7 +298,7 @@ export const make = ( rowCollectionOnRequestCompletion: true, useColumnNames: false, instanceName: options.instanceName, - encrypt: options.encrypt ?? false, + encrypt: options.encrypt ?? true, cancelTimeout: options.cancelTimeout ? Duration.toMillis(Duration.fromInputUnsafe(options.cancelTimeout)) : undefined, @@ -366,6 +373,7 @@ export const make = ( conn.cancel() conn.execSql(req) + return Effect.sync(() => conn.cancel()) }) const runProcedure = ( @@ -389,7 +397,7 @@ export const make = ( } resume( Effect.succeed({ - params: result, + output: result, rows }) ) @@ -409,11 +417,12 @@ export const make = ( } req.on("returnValue", (name, value) => { - result[name] = value + Rec.assignProperty(result, name, value) }) conn.cancel() conn.callProcedure(req) + return Effect.sync(() => conn.cancel()) }) const connection = identity({ @@ -648,7 +657,7 @@ export const layer = ( /** * Creates the SQL Server statement compiler, using `@1`-style placeholders, bracket-escaped identifiers, and SQL Server `OUTPUT INSERTED` returning clauses. * - * @category compiler + * @category constructors * @since 4.0.0 */ export const makeCompiler = (transform?: (_: string) => string) => @@ -700,12 +709,12 @@ function numberToParamName(n: number) { /** * Default mapping from Effect SQL primitive value kinds to Tedious SQL Server parameter data types. * - * @category configuration + * @category constants * @since 4.0.0 */ export const defaultParameterTypes: Record = { - string: Tedious.TYPES.VarChar, - number: Tedious.TYPES.Int, + string: Tedious.TYPES.NVarChar, + number: Tedious.TYPES.Float, bigint: Tedious.TYPES.BigInt, boolean: Tedious.TYPES.Bit, Date: Tedious.TYPES.DateTime, @@ -738,7 +747,7 @@ function rowsToObjects(rows: ReadonlyArray) { const newRow: any = {} for (let j = 0, columnLen = row.length; j < columnLen; j++) { const column = row[j] - newRow[column.metadata.colName] = column.value + Rec.assignProperty(newRow, column.metadata.colName, column.value) } newRows[i] = newRow } diff --git a/.context/effect/packages/sql/mssql/src/MssqlMigrator.ts b/.context/effect/packages/sql/mssql/src/MssqlMigrator.ts index 2d947b900..9b0e8a1ab 100644 --- a/.context/effect/packages/sql/mssql/src/MssqlMigrator.ts +++ b/.context/effect/packages/sql/mssql/src/MssqlMigrator.ts @@ -23,7 +23,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( diff --git a/.context/effect/packages/sql/mssql/test/Client.test.ts b/.context/effect/packages/sql/mssql/test/Client.test.ts index 41a6d37b7..6618bb135 100644 --- a/.context/effect/packages/sql/mssql/test/Client.test.ts +++ b/.context/effect/packages/sql/mssql/test/Client.test.ts @@ -1,11 +1,85 @@ -import { MssqlClient } from "@effect/sql-mssql" -import { describe, expect, it } from "@effect/vitest" -import { Effect } from "effect" +import { MssqlClient, Procedure } from "@effect/sql-mssql" +import { assert, describe, expect, it } from "@effect/vitest" +import { Effect, Fiber } from "effect" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" import * as Statement from "effect/unstable/sql/Statement" +import type * as Tedious from "tedious" +import { vi } from "vitest" + +const state = vi.hoisted(() => ({ cancelCalls: 0, completeRequests: true, type: {} })) + +vi.mock("tedious", async (importOriginal) => { + const original = await importOriginal() + + class MockRequest { + readonly listeners: Record) => void> = {} + + constructor( + readonly sql: string, + readonly callback: (cause: unknown, rowCount: number, rows: ReadonlyArray) => void + ) {} + + addParameter() {} + addOutputParameter() {} + on(event: string, listener: (...args: Array) => void) { + this.listeners[event] = listener + } + } + + class MockConnection { + connect(callback: (cause: unknown) => void) { + callback(null) + } + close() {} + on() {} + cancel() { + state.cancelCalls++ + } + execSql(request: MockRequest) { + if (state.completeRequests) { + request.callback(null, 0, []) + } + } + callProcedure(request: MockRequest) { + request.listeners.returnValue("answer", 42) + request.callback(null, 0, []) + } + beginTransaction(callback: (cause: unknown) => void) { + callback(null) + } + commitTransaction(callback: (cause: unknown) => void) { + callback(null) + } + saveTransaction(callback: (cause: unknown) => void) { + callback(null) + } + rollbackTransaction(callback: (cause: unknown) => void) { + callback(null) + } + } + + return { + ...original, + Connection: MockConnection, + Request: MockRequest + } +}) const sql = Statement.make(Effect.void as any, MssqlClient.makeCompiler(), [], undefined) describe("mssql", () => { + it("preserves fractional JavaScript numbers with the default parameter mapping", () => { + const value = MssqlClient.defaultParameterTypes.number.validate(1.5, undefined) + + expect(value).toBe(1.5) + }) + + it("preserves Unicode JavaScript strings with the default parameter mapping", () => { + const value = MssqlClient.defaultParameterTypes.string.validate("lambda: \u03bb", undefined) + + expect(value).toBe("lambda: \u03bb") + }) + it("insert helper", () => { const [query, params] = sql`INSERT INTO ${sql("people")} ${sql.insert({ name: "Tim", age: 10 })}`.compile() expect(query).toEqual( @@ -88,4 +162,33 @@ describe("mssql", () => { const [query] = sql`SELECT * FROM ${sql("peo[]ple.te[st]ing")}`.compile() expect(query).toEqual(`SELECT * FROM [peo[]]ple].[te[st]]ing]`) }) + + it.effect("returns stored procedure output parameters under output", () => + Effect.gen(function*() { + const client = yield* MssqlClient.make({ server: "localhost" }) + const definition = Procedure.outputParam()("answer", state.type as any)(Procedure.make("get_answer")) + const result = yield* client.call(Procedure.compile(definition)({})) + + assert.deepStrictEqual(result, { output: { answer: 42 }, rows: [] }) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) + + it.effect("cancels an in-flight Tedious request when interrupted", () => + Effect.gen(function*() { + state.cancelCalls = 0 + state.completeRequests = true + const client = yield* MssqlClient.make({ server: "localhost" }) + state.completeRequests = false + const fiber = yield* Effect.forkChild(client`WAITFOR DELAY '00:01:00'`) + yield* Effect.yieldNow + const callsBeforeInterrupt = state.cancelCalls + yield* Fiber.interrupt(fiber) + + assert.strictEqual(state.cancelCalls, callsBeforeInterrupt + 1) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) }) diff --git a/.context/effect/packages/sql/mssql/test/Persistence.integration.test.ts b/.context/effect/packages/sql/mssql/test/Persistence.integration.test.ts new file mode 100644 index 000000000..7a39ac8fa --- /dev/null +++ b/.context/effect/packages/sql/mssql/test/Persistence.integration.test.ts @@ -0,0 +1,14 @@ +import { Layer } from "effect" +import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" +import { Persistence } from "effect/unstable/persistence" +import { MssqlContainer } from "./utils.ts" + +PersistedCacheTest.suite( + "sql-mssql-multi", + Persistence.layerSqlMultiTable.pipe(Layer.provide(MssqlContainer.layerClient)) +) + +PersistedCacheTest.suite( + "sql-mssql-single", + Persistence.layerSql.pipe(Layer.provide(MssqlContainer.layerClient)) +) diff --git a/.context/effect/packages/sql/mssql/test/utils.ts b/.context/effect/packages/sql/mssql/test/utils.ts new file mode 100644 index 000000000..2941f0bd3 --- /dev/null +++ b/.context/effect/packages/sql/mssql/test/utils.ts @@ -0,0 +1,36 @@ +import { MssqlClient } from "@effect/sql-mssql" +import { MSSQLServerContainer } from "@testcontainers/mssqlserver" +import { Context, Data, Effect, Layer, Redacted } from "effect" + +export class ContainerError extends Data.TaggedError("ContainerError")<{ + cause: unknown +}> {} + +export class MssqlContainer extends Context.Service()("test/MssqlContainer", { + make: Effect.acquireRelease( + Effect.tryPromise({ + try: () => + new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-latest") + .acceptLicense() + .start(), + catch: (cause) => new ContainerError({ cause }) + }), + (container) => Effect.promise(() => container.stop()) + ) +}) { + static readonly layer = Layer.effect(this)(this.make) + + static layerClient = Layer.unwrap( + Effect.gen(function*() { + const container = yield* MssqlContainer + return MssqlClient.layer({ + server: container.getHost(), + port: container.getPort(), + database: container.getDatabase(), + username: container.getUsername(), + password: Redacted.make(container.getPassword()), + trustServer: true + }) + }) + ).pipe(Layer.provide(this.layer)) +} diff --git a/.context/effect/packages/sql/mssql/tsconfig.json b/.context/effect/packages/sql/mssql/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/mssql/tsconfig.json +++ b/.context/effect/packages/sql/mssql/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/mssql/vitest.config.ts b/.context/effect/packages/sql/mssql/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/mssql/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/mysql2/CHANGELOG.md b/.context/effect/packages/sql/mysql2/CHANGELOG.md index 6cb9f57b1..792988495 100644 --- a/.context/effect/packages/sql/mysql2/CHANGELOG.md +++ b/.context/effect/packages/sql/mysql2/CHANGELOG.md @@ -1,5 +1,56 @@ # @effect/sql-mysql2 +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/mysql2/README.md b/.context/effect/packages/sql/mysql2/README.md index 074afd269..90d24e3d9 100644 --- a/.context/effect/packages/sql/mysql2/README.md +++ b/.context/effect/packages/sql/mysql2/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-mysql2` +# @effect/sql-mysql2 -An Effect SQL implementation using the `mysql2` library. +An Effect SQL client for MySQL, built on the [`mysql2`](https://sidorares.github.io/node-mysql2/docs) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-mysql2@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-mysql2). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-mysql2) diff --git a/.context/effect/packages/sql/mysql2/docgen.json b/.context/effect/packages/sql/mysql2/docgen.json deleted file mode 100644 index 5adbae538..000000000 --- a/.context/effect/packages/sql/mysql2/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/mysql2/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/mysql2/package.json b/.context/effect/packages/sql/mysql2/package.json index 5e7a0dfc0..b3e0142ed 100644 --- a/.context/effect/packages/sql/mysql2/package.json +++ b/.context/effect/packages/sql/mysql2/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-mysql2", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A MySQL toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,12 +58,10 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@testcontainers/mysql": "^11.14.0", + "@testcontainers/mysql": "^12.0.4", "effect": "workspace:^" }, "peerDependencies": { diff --git a/.context/effect/packages/sql/mysql2/src/MysqlClient.ts b/.context/effect/packages/sql/mysql2/src/MysqlClient.ts index 12bddee66..afd682f66 100644 --- a/.context/effect/packages/sql/mysql2/src/MysqlClient.ts +++ b/.context/effect/packages/sql/mysql2/src/MysqlClient.ts @@ -153,7 +153,7 @@ export type TypeId = "~@effect/sql-mysql2/MysqlClient" /** * mysql2-backed SQL client service, extending `SqlClient` with its runtime type marker and client configuration. * - * @category models + * @category services * @since 4.0.0 */ export interface MysqlClient extends Client.SqlClient { @@ -455,7 +455,7 @@ export const layer = ( /** * Creates the MySQL statement compiler, using `?` placeholders and backtick-escaped identifiers. * - * @category compiler + * @category constructors * @since 4.0.0 */ export const makeCompiler = (transform?: (_: string) => string) => diff --git a/.context/effect/packages/sql/mysql2/src/MysqlMigrator.ts b/.context/effect/packages/sql/mysql2/src/MysqlMigrator.ts index ca2fe7494..bea60d622 100644 --- a/.context/effect/packages/sql/mysql2/src/MysqlMigrator.ts +++ b/.context/effect/packages/sql/mysql2/src/MysqlMigrator.ts @@ -23,7 +23,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( diff --git a/.context/effect/packages/sql/mysql2/test/Client.test.ts b/.context/effect/packages/sql/mysql2/test/Client.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/mysql2/test/Client.test.ts rename to .context/effect/packages/sql/mysql2/test/Client.integration.test.ts diff --git a/.context/effect/packages/sql/mysql2/test/KeyValueStore.test.ts b/.context/effect/packages/sql/mysql2/test/KeyValueStore.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/mysql2/test/KeyValueStore.test.ts rename to .context/effect/packages/sql/mysql2/test/KeyValueStore.integration.test.ts diff --git a/.context/effect/packages/sql/mysql2/test/Model.test.ts b/.context/effect/packages/sql/mysql2/test/Model.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/mysql2/test/Model.test.ts rename to .context/effect/packages/sql/mysql2/test/Model.integration.test.ts diff --git a/.context/effect/packages/sql/mysql2/test/MysqlClient.test.ts b/.context/effect/packages/sql/mysql2/test/MysqlClient.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/mysql2/test/MysqlClient.test.ts rename to .context/effect/packages/sql/mysql2/test/MysqlClient.integration.test.ts diff --git a/.context/effect/packages/sql/mysql2/test/Persistence.integration.test.ts b/.context/effect/packages/sql/mysql2/test/Persistence.integration.test.ts new file mode 100644 index 000000000..61ac5457c --- /dev/null +++ b/.context/effect/packages/sql/mysql2/test/Persistence.integration.test.ts @@ -0,0 +1,66 @@ +import { assert, it } from "@effect/vitest" +import { Effect, Layer } from "effect" +import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" +import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest" +import * as SqlCleanupTest from "effect-test/unstable/persistence/SqlCleanupTest" +import { TestClock } from "effect/testing" +import { PersistedQueue, Persistence } from "effect/unstable/persistence" +import { SqlClient } from "effect/unstable/sql" +import { MysqlContainer } from "./utils.ts" + +it.layer(MysqlContainer.layerClient, { timeout: "90 seconds" })("Persistence", (it) => { + PersistedCacheTest.suiteWith("sql-mysql2-multi", Persistence.layerSqlMultiTable, it) + + PersistedCacheTest.suiteWith("sql-mysql2-single", Persistence.layerSql, it) + + PersistedQueueTest.suiteWith("sql-mysql2", PersistedQueue.layerStoreSql(), it) + + it.effect("deletes expired entries in batches", () => + Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const table = sql("effect_persistence") + const expiredCount = sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM ${table} WHERE store_id = 'expired' + `.pipe(Effect.map((rows) => Number(rows[0].count))) + // Reset the table left by the single-table cache suite so cleanup builds its own schema and index. + yield* sql`DROP TABLE IF EXISTS ${table}` + yield* sql` + CREATE TABLE ${table} ( + store_id VARCHAR(191) NOT NULL, + id VARCHAR(191) NOT NULL, + value TEXT NOT NULL, + expires BIGINT, + PRIMARY KEY (store_id, id) + ) + ` + + const entries = Array.from({ length: SqlCleanupTest.expiredEntryCount }, (_, i) => ({ + store_id: "expired", + id: String(i), + value: "{}", + expires: SqlCleanupTest.expiredAtEpoch + })) + yield* sql`INSERT INTO ${table} ${sql.insert(entries)}`.unprepared + yield* sql` + INSERT INTO ${table} (store_id, id, value, expires) + VALUES ('live', 'live', '{}', NULL), ('live', 'future', '{}', ${SqlCleanupTest.futureExpiresAt}) + ` + + yield* Layer.build(Persistence.layerBackingSql).pipe(TestClock.withLive) + + const expired = yield* SqlCleanupTest.waitForCount(expiredCount, (count) => count === 0) + assert.strictEqual(expired, 0) + const live = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM ${table} WHERE store_id = 'live' + ` + assert.strictEqual(Number(live[0].count), 2) + + const indexes = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'effect_persistence' + AND index_name = 'effect_persistence_expires_idx' + ` + assert.strictEqual(Number(indexes[0].count), 1) + }), { timeout: SqlCleanupTest.testTimeout }) +}) diff --git a/.context/effect/packages/sql/mysql2/test/Persistence.test.ts b/.context/effect/packages/sql/mysql2/test/Persistence.test.ts deleted file mode 100644 index b0e9b7625..000000000 --- a/.context/effect/packages/sql/mysql2/test/Persistence.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Layer } from "effect" -import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" -import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest" -import { PersistedQueue, Persistence } from "effect/unstable/persistence" -import { MysqlContainer } from "./utils.ts" - -PersistedCacheTest.suite( - "sql-mysql2-multi", - Persistence.layerSqlMultiTable.pipe(Layer.provide(MysqlContainer.layerClient)) -) - -PersistedCacheTest.suite( - "sql-mysql2-single", - Persistence.layerSql.pipe(Layer.provide(MysqlContainer.layerClient)) -) - -PersistedQueueTest.suite( - "sql-mysql2", - PersistedQueue.layerStoreSql().pipe( - Layer.provide(MysqlContainer.layerClient) - ) -) diff --git a/.context/effect/packages/sql/mysql2/test/SqlEventLogServerUnencrypted.test.ts b/.context/effect/packages/sql/mysql2/test/SqlEventLogServerUnencrypted.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/mysql2/test/SqlEventLogServerUnencrypted.test.ts rename to .context/effect/packages/sql/mysql2/test/SqlEventLogServerUnencrypted.integration.test.ts diff --git a/.context/effect/packages/sql/mysql2/test/utils.ts b/.context/effect/packages/sql/mysql2/test/utils.ts index 344145a5d..ef749fcce 100644 --- a/.context/effect/packages/sql/mysql2/test/utils.ts +++ b/.context/effect/packages/sql/mysql2/test/utils.ts @@ -7,6 +7,17 @@ export class ContainerError extends Data.TaggedError("ContainerError")<{ cause: unknown }> {} +const makeMysqlContainer = () => + new MySqlContainer("mysql:lts").withHealthCheck({ + test: [ + "CMD-SHELL", + "MYSQL_PWD=\"$MYSQL_ROOT_PASSWORD\" mysqladmin ping --protocol TCP --host 127.0.0.1 --user root --silent" + ], + interval: 250, + timeout: 1000, + retries: 1000 + }) + export class MysqlContainer extends Context.Service< MysqlContainer, StartedMySqlContainer @@ -14,7 +25,7 @@ export class MysqlContainer extends Context.Service< static readonly layer = Layer.effect(this)( Effect.acquireRelease( Effect.tryPromise({ - try: () => new MySqlContainer("mysql:lts").start(), + try: () => makeMysqlContainer().start(), catch: (cause) => new ContainerError({ cause }) }), (container) => Effect.promise(() => container.stop()) diff --git a/.context/effect/packages/sql/mysql2/tsconfig.json b/.context/effect/packages/sql/mysql2/tsconfig.json index 739eb6b55..fce3e3d43 100644 --- a/.context/effect/packages/sql/mysql2/tsconfig.json +++ b/.context/effect/packages/sql/mysql2/tsconfig.json @@ -1,9 +1,9 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/sql/mysql2/vitest.config.ts b/.context/effect/packages/sql/mysql2/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/mysql2/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/pg/CHANGELOG.md b/.context/effect/packages/sql/pg/CHANGELOG.md index a1ab3ba21..05c34b50a 100644 --- a/.context/effect/packages/sql/pg/CHANGELOG.md +++ b/.context/effect/packages/sql/pg/CHANGELOG.md @@ -1,5 +1,59 @@ # @effect/sql-pg +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- [#7142](https://github.com/Effect-TS/effect/pull/7142) [`c25b84c`](https://github.com/Effect-TS/effect/commit/c25b84c09bbbb68343e157bda9cd4c42e1e5e515) Thanks @fubhy! - Prevent unhandled `pg` client error events while `PgClient.makeClient` is connecting. +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#6991](https://github.com/Effect-TS/effect/pull/6991) [`4a17049`](https://github.com/Effect-TS/effect/commit/4a17049157d83f55d44a6e8b1c58c28df2ffd65a) Thanks @fubhy! - Hold the shared PostgreSQL client permit for the full transaction lifetime. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/pg/README.md b/.context/effect/packages/sql/pg/README.md index 9017bb4b8..24dfc8892 100644 --- a/.context/effect/packages/sql/pg/README.md +++ b/.context/effect/packages/sql/pg/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-pg` +# @effect/sql-pg -An Effect SQL implementation using the `pg` library. +An Effect SQL client for PostgreSQL, built on the [`pg`](https://node-postgres.com) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-pg@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-pg). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-pg) diff --git a/.context/effect/packages/sql/pg/docgen.json b/.context/effect/packages/sql/pg/docgen.json deleted file mode 100644 index 90c1f6298..000000000 --- a/.context/effect/packages/sql/pg/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/pg/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/pg/package.json b/.context/effect/packages/sql/pg/package.json index a4d056e50..687b7f2c2 100644 --- a/.context/effect/packages/sql/pg/package.json +++ b/.context/effect/packages/sql/pg/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pg", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A PostgreSQL toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,12 +58,10 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@testcontainers/postgresql": "^11.14.0", + "@testcontainers/postgresql": "^12.0.4", "@types/pg": "^8.20.0", "@types/pg-cursor": "^2.7.2", "effect": "workspace:^" @@ -68,7 +71,7 @@ }, "dependencies": { "pg": "^8.22.0", - "pg-connection-string": "2.14.0", + "pg-connection-string": "^2.14.0", "pg-cursor": "^2.21.0", "pg-pool": "^3.14.0", "pg-types": "^4.1.0" diff --git a/.context/effect/packages/sql/pg/src/PgClient.ts b/.context/effect/packages/sql/pg/src/PgClient.ts index 1f3213468..8fae62d0c 100644 --- a/.context/effect/packages/sql/pg/src/PgClient.ts +++ b/.context/effect/packages/sql/pg/src/PgClient.ts @@ -73,7 +73,7 @@ export type TypeId = "~@effect/sql-pg/PgClient" /** * PostgreSQL client service, extending `SqlClient` with JSON parameter fragments and LISTEN/NOTIFY helpers. * - * @category models + * @category services * @since 4.0.0 */ export interface PgClient extends Client.SqlClient { @@ -99,7 +99,7 @@ export const PgClient = Context.Service("@effect/sql-pg/PgClient") /** * Configuration for a PostgreSQL client, including connection, TLS, custom stream, application name, type parser, JSON transform, and query/result name transform options. * - * @category constructors + * @category models * @since 4.0.0 */ export interface PgClientConfig { @@ -129,7 +129,7 @@ export interface PgClientConfig { /** * PostgreSQL pool configuration, extending `PgClientConfig` with idle timeout, pool size, and connection lifetime settings. * - * @category constructors + * @category models * @since 4.0.0 */ export interface PgPoolConfig extends PgClientConfig { @@ -219,8 +219,9 @@ export const makeClient = ( */ readonly acquireForStream?: boolean | undefined } -): Effect.Effect => - fromClient({ +): Effect.Effect => { + function onError() {} + return fromClient({ ...options, acquire: Effect.acquireRelease( Effect.tryPromise({ @@ -237,13 +238,17 @@ export const makeClient = ( application_name: options.applicationName ?? "@effect/sql-pg", types: options.types }) + client.on("error", onError) await client.connect() return client }, catch: (cause) => new SqlError({ reason: classifyError(cause, "PgClient: Failed to connect", "connect") }) }), (client) => - Effect.promise(() => client.end()).pipe( + Effect.promise(() => { + client.off("error", onError) + return client.end() + }).pipe( Effect.timeoutOption(1000) ), { interruptible: true } @@ -264,6 +269,7 @@ export const makeClient = ( ), acquireForStream: options.acquireForStream ?? false }) +} /** * Builds a PostgreSQL client from a scoped `pg` pool acquisition effect, deriving transaction, streaming, and LISTEN/NOTIFY support from that pool. @@ -522,6 +528,17 @@ export const fromClient = Effect.fnUntraced(function*( ) const connection = makeConection(client) const acquirer = semaphore.withPermit(Effect.succeed(connection)) + const transactionAcquirer = Effect.uninterruptibleMask((restore) => { + const fiber = Fiber.getCurrent()! + const scope = Context.getUnsafe(fiber.context, Scope.Scope) + return Effect.as( + Effect.tap( + restore(semaphore.take(1)), + () => Scope.addFinalizer(scope, semaphore.release(1)) + ), + connection + ) + }) const config: PgClientConfig = { ...options, @@ -535,7 +552,7 @@ export const fromClient = Effect.fnUntraced(function*( return yield* makeWith({ acquirer, - transactionAcquirer: acquirer, + transactionAcquirer, listenAcquirer: streamClient, config, spanAttributes: options.spanAttributes, @@ -865,18 +882,18 @@ const escape = Statement.defaultEscape("\"") /** * PostgreSQL-specific custom statement fragments supported by the compiler, currently JSON parameter fragments. * - * @category custom types + * @category models * @since 4.0.0 */ export type PgCustom = PgJson /** - * @category custom types + * @category models * @since 4.0.0 */ interface PgJson extends Custom<"PgJson", unknown> {} /** - * @category custom types + * @category constructors * @since 4.0.0 */ const PgJson = Statement.custom("PgJson") diff --git a/.context/effect/packages/sql/pg/src/PgMigrator.ts b/.context/effect/packages/sql/pg/src/PgMigrator.ts index 00b38a7f4..3751edeaa 100644 --- a/.context/effect/packages/sql/pg/src/PgMigrator.ts +++ b/.context/effect/packages/sql/pg/src/PgMigrator.ts @@ -29,7 +29,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs PostgreSQL SQL migrations using the configured clients. Schema dumps use `pg_dump` and require child process, filesystem, and path services. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( diff --git a/.context/effect/packages/sql/pg/test/Client.integration.test.ts b/.context/effect/packages/sql/pg/test/Client.integration.test.ts new file mode 100644 index 000000000..de27f8300 --- /dev/null +++ b/.context/effect/packages/sql/pg/test/Client.integration.test.ts @@ -0,0 +1,495 @@ +import { PgClient } from "@effect/sql-pg" +import { assert, expect, it } from "@effect/vitest" +import { Deferred, Effect, Fiber, Option, Redacted, Stream, String } from "effect" +import { TestClock } from "effect/testing" +import * as Reactivity from "effect/unstable/reactivity/Reactivity" +import { SqlClient } from "effect/unstable/sql" +import * as Statement from "effect/unstable/sql/Statement" +import * as Pg from "pg" +import { parse as parsePgConnectionString } from "pg-connection-string" +import { vi } from "vitest" +import { PgContainer } from "./utils.ts" + +const compilerTransform = PgClient.makeCompiler(String.camelToSnake) +const transformsNested = Statement.defaultTransforms(String.snakeToCamel) +const transforms = Statement.defaultTransforms(String.snakeToCamel, false) + +it.layer(PgContainer.layerClient, { timeout: "30 seconds" })("PgClient", (it) => { + it.effect("insert helper", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`INSERT INTO people ${sql.insert({ name: "Tim", age: 10 })}`.compile() + expect(query).toEqual(`INSERT INTO people ("name","age") VALUES ($1,$2)`) + expect(params).toEqual(["Tim", 10]) + })) + + it.effect("updateValues helper", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`UPDATE people SET name = data.name FROM ${ + sql.updateValues( + [{ name: "Tim" }, { name: "John" }], + "data" + ) + }`.compile() + expect(query).toEqual( + `UPDATE people SET name = data.name FROM (values ($1),($2)) AS data("name")` + ) + expect(params).toEqual(["Tim", "John"]) + })) + + it.effect("updateValues helper returning", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`UPDATE people SET name = data.name FROM ${ + sql.updateValues( + [{ name: "Tim" }, { name: "John" }], + "data" + ).returning("*") + }`.compile() + expect(query).toEqual( + `UPDATE people SET name = data.name FROM (values ($1),($2)) AS data("name") RETURNING *` + ) + expect(params).toEqual(["Tim", "John"]) + })) + + it.effect("update helper", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + let result = sql`UPDATE people SET ${sql.update({ name: "Tim" })}`.compile() + expect(result[0]).toEqual(`UPDATE people SET "name" = $1`) + expect(result[1]).toEqual(["Tim"]) + + result = sql`UPDATE people SET ${sql.update({ name: "Tim", age: 10 }, ["age"])}`.compile() + expect(result[0]).toEqual(`UPDATE people SET "name" = $1`) + expect(result[1]).toEqual(["Tim"]) + })) + + it.effect("update helper returning", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const result = sql`UPDATE people SET ${sql.update({ name: "Tim" }).returning("*")}`.compile() + expect(result[0]).toEqual(`UPDATE people SET "name" = $1 RETURNING *`) + expect(result[1]).toEqual(["Tim"]) + })) + + it.effect("array helper", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`SELECT * FROM ${sql("people")} WHERE id IN ${sql.in([1, 2, "string"])}`.compile() + expect(query).toEqual(`SELECT * FROM "people" WHERE id IN ($1,$2,$3)`) + expect(params).toEqual([1, 2, "string"]) + })) + + it.effect("array helper with column", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + let result = sql`SELECT * FROM ${sql("people")} WHERE ${sql.in("id", [1, 2, "string"])}`.compile() + expect(result[0]).toEqual(`SELECT * FROM "people" WHERE "id" IN ($1,$2,$3)`) + expect(result[1]).toEqual([1, 2, "string"]) + + result = sql`SELECT * FROM ${sql("people")} WHERE ${sql.in("id", [])}`.compile() + expect(result[0]).toEqual(`SELECT * FROM "people" WHERE 1=0`) + expect(result[1]).toEqual([]) + })) + + it.effect("and", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const now = new Date() + const result = sql`SELECT * FROM ${sql("people")} WHERE ${ + sql.and([ + sql.in("name", ["Tim", "John"]), + sql`created_at < ${now}` + ]) + }`.compile() + expect(result[0]).toEqual(`SELECT * FROM "people" WHERE ("name" IN ($1,$2) AND created_at < $3)`) + expect(result[1]).toEqual(["Tim", "John", now]) + })) + + it("transform nested", () => { + assert.deepEqual( + transformsNested.array([ + { + a_key: 1, + nested: [{ b_key: 2 }], + arr_primitive: [1, "2", true] + } + ]) as any, + [ + { + aKey: 1, + nested: [{ bKey: 2 }], + arrPrimitive: [1, "2", true] + } + ] + ) + }) + + it("transform non nested", () => { + assert.deepEqual( + transforms.array([ + { + a_key: 1, + nested: [{ b_key: 2 }], + arr_primitive: [1, "2", true] + } + ]) as any, + [ + { + aKey: 1, + nested: [{ b_key: 2 }], + arrPrimitive: [1, "2", true] + } + ] + ) + + assert.deepEqual( + transforms.array([ + { + json_field: { + test_value: [1, true, null, "text"], + test_nested: { + test_value: [1, true, null, "text"] + } + } + } + ]) as any, + [ + { + jsonField: { + test_value: [1, true, null, "text"], + test_nested: { + test_value: [1, true, null, "text"] + } + } + } + ] + ) + }) + + it.effect("insert fragments", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`INSERT INTO people ${ + sql.insert({ + name: "Tim", + age: 10, + json: sql.json({ a: 1 }) + }) + }`.compile() + assert.strictEqual( + query, + "INSERT INTO people (\"name\",\"age\",\"json\") VALUES ($1,$2,$3)" + ) + assert.lengthOf(params, 3) + })) + + it.effect("update fragments", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const now = new Date() + const [query, params] = sql`UPDATE people SET json = data.json FROM ${ + sql.updateValues( + [{ json: sql.json({ a: 1 }) }, { json: sql.json({ b: 1 }) }], + "data" + ) + } WHERE created_at > ${now}`.compile() + assert.strictEqual( + query, + `UPDATE people SET json = data.json FROM (values ($1),($2)) AS data("json") WHERE created_at > $3` + ) + assert.lengthOf(params, 3) + })) + + it.effect("onDialect", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + assert.strictEqual( + sql.onDialect({ + sqlite: () => "A", + pg: () => "B", + mysql: () => "C", + mssql: () => "D", + clickhouse: () => "E" + }), + "B" + ) + assert.strictEqual( + sql.onDialectOrElse({ + orElse: () => "A", + pg: () => "B" + }), + "B" + ) + })) + + it.effect("identifier transform", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query] = compilerTransform.compile( + sql`SELECT * from ${sql("peopleTest")}`, + false + ) + expect(query).toEqual(`SELECT * from "people_test"`) + })) + + it.effect("jsonb", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const rows = yield* sql<{ json: unknown }>`select ${{ testValue: 123 }}::jsonb as json` + expect(rows[0].json).toEqual({ testValue: 123 }) + })) + + it.effect("stream", () => + Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + const rows = yield* sql`SELECT generate_series(1, 3)`.stream.pipe( + Stream.runCollect + ) + expect(rows).toEqual([ + { "generate_series": 1 }, + { "generate_series": 2 }, + { "generate_series": 3 } + ]) + })) + + it.effect("preserves successful concurrent nested transactions", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const firstStarted = yield* Deferred.make() + const firstInserted = yield* Deferred.make() + + const rows = yield* sql.withTransaction( + Effect.gen(function*() { + yield* sql`CREATE TEMP TABLE nested_transactions (value TEXT) ON COMMIT DROP` + yield* Effect.all([ + sql.withTransaction( + Effect.gen(function*() { + yield* Deferred.succeed(firstStarted, undefined) + yield* Effect.sleep("100 millis") + yield* sql`INSERT INTO nested_transactions VALUES ('first')` + yield* Deferred.succeed(firstInserted, undefined) + }) + ), + Deferred.await(firstStarted).pipe( + Effect.andThen(sql.withTransaction( + Deferred.await(firstInserted).pipe( + Effect.andThen(Effect.fail("rollback")) + ) + )) + ) + ], { concurrency: "unbounded" }).pipe(Effect.catch(() => Effect.void)) + return yield* sql<{ value: string }>`SELECT value FROM nested_transactions` + }) + ) + + assert.deepStrictEqual(rows, [{ value: "first" }]) + }).pipe(TestClock.withLive)) +}) + +it.layer(PgContainer.layerMakeClient, { timeout: "30 seconds" })("PgClient.makeClient", (it) => { + it.effect("connects before executing queries", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const rows = yield* sql<{ value: number }>`SELECT 1 AS value` + assert.deepStrictEqual(rows, [{ value: 1 }]) + })) +}) + +it.effect("PgClient.makeClient handles errors emitted while connecting", () => + Effect.acquireUseRelease( + Effect.sync(() => ({ + connect: vi.spyOn(Pg.Client.prototype, "connect").mockImplementation(function(this: Pg.Client) { + return new Promise((resolve, reject) => { + queueMicrotask(() => { + try { + this.emit("error", new Error("connection failed")) + resolve() + } catch (cause) { + reject(cause) + } + }) + }) + }), + end: vi.spyOn(Pg.Client.prototype, "end").mockResolvedValue(undefined) + })), + () => + PgClient.makeClient({ host: "localhost" }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + ), + ({ connect, end }) => + Effect.sync(() => { + connect.mockRestore() + end.mockRestore() + }) + )) + +it.layer(PgContainer.layerClientWithTransforms, { timeout: "30 seconds" })("PgClient transforms", (it) => { + it.effect("insert helper", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const [query, params] = sql`INSERT INTO people ${sql.insert({ firstName: "Tim", age: 10 })}`.compile() + expect(query).toEqual(`INSERT INTO people ("first_name","age") VALUES ($1,$2)`) + expect(params).toEqual(["Tim", 10]) + })) + + it.effect("insert helper withoutTransforms", () => + Effect.gen(function*() { + const sql = (yield* PgClient.PgClient).withoutTransforms() + const [query, params] = sql`INSERT INTO people ${sql.insert({ first_name: "Tim", age: 10 })}`.compile() + expect(query).toEqual(`INSERT INTO people ("first_name","age") VALUES ($1,$2)`) + expect(params).toEqual(["Tim", 10]) + })) + + it.effect("multi-statement queries", () => + Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + + const result = yield* sql<{ id: string; name: string }>` + CREATE TABLE test_multi (id TEXT PRIMARY KEY, name TEXT); + INSERT INTO test_multi (id, name) VALUES ('id1', 'test1') RETURNING *; + INSERT INTO test_multi (id, name) VALUES ('id2', 'test2') RETURNING *; + ` + + expect(result).toHaveLength(3) + expect(result[0]).toEqual([]) + expect(result[1]).toEqual([{ id: "id1", name: "test1" }]) + expect(result[2]).toEqual([{ id: "id2", name: "test2" }]) + })) + + it.effect("interruption", () => + Effect.gen(function*() { + const sql = yield* SqlClient.SqlClient + const conn = yield* sql.reserve + yield* conn.executeRaw("select pg_sleep(1000)", []).pipe( + Effect.timeoutOption("50 millis"), + TestClock.withLive + ) + const value = yield* conn.executeValues("select 1", []) + expect(value).toEqual([[1]]) + })) + + it.effect("Should populate config", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + + assert.isDefined(sql.config.url) + + const parsedConfig = parsePgConnectionString(Redacted.value(sql.config.url)) + + expect(sql.config.host).toEqual(parsedConfig.host) + assert.isNotNull(parsedConfig.port) + assert.isDefined(parsedConfig.port) + expect(sql.config.port).toEqual(parseInt(parsedConfig.port)) + expect(sql.config.username).toEqual(parsedConfig.user) + assert.isDefined(sql.config.password) + expect(Redacted.value(sql.config.password)).toEqual(parsedConfig.password) + expect(sql.config.database).toEqual(parsedConfig.database) + })) +}) + +it.layer(PgContainer.layerClientSingleConnection, { timeout: "30 seconds" })("PgClient listen", (it) => { + it.effect("listen does not reserve a pool connection", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const channel = "pool_connection_listen" + + const listenFiber = yield* sql.listen(channel).pipe( + Stream.take(1), + Stream.runCollect, + Effect.forkScoped + ) + + yield* Effect.sleep("250 millis") + + const rows = yield* sql<{ value: number }>`SELECT 1 as value`.pipe( + Effect.timeoutOrElse({ + duration: "3 seconds", + orElse: () => Effect.fail(new Error("query timed out while listener was active")) + }) + ) + expect(rows).toEqual([{ value: 1 }]) + + yield* sql.notify(channel, "payload") + const payloads = yield* Fiber.join(listenFiber).pipe( + Effect.timeoutOrElse({ + duration: "3 seconds", + orElse: () => Effect.fail(new Error("listener did not receive notification in time")) + }) + ) + expect(Array.from(payloads)).toEqual(["payload"]) + }).pipe(TestClock.withLive), 20_000) + + it.effect("notify sends payload", () => + Effect.gen(function*() { + const sql = yield* PgClient.PgClient + const channel = "pool_connection_notify" + + const listenFiber = yield* sql.listen(channel).pipe( + Stream.take(1), + Stream.runCollect, + Effect.forkScoped + ) + + yield* Effect.sleep("250 millis") + yield* sql.notify(channel, "payload") + + const payloads = yield* Fiber.join(listenFiber).pipe( + Effect.timeoutOrElse({ + duration: "3 seconds", + orElse: () => Effect.fail(new Error("listener did not receive notification in time")) + }) + ) + expect(Array.from(payloads)).toEqual(["payload"]) + }).pipe(TestClock.withLive), 20_000) +}) + +it.effect("serializes transactions that share one pg.Client", () => + Effect.gen(function*() { + const secondBegin = yield* Deferred.make() + const firstBodyStarted = yield* Deferred.make() + const releaseFirstBody = yield* Deferred.make() + let beginCalls = 0 + const pg = { + host: "localhost", + port: 5432, + database: "postgres", + user: "postgres", + password: undefined, + ssl: false, + on() {}, + off() {}, + query(sql: string, _params: ReadonlyArray, callback: (error: null, result: unknown) => void) { + if (sql === "BEGIN" && ++beginCalls === 2) { + Deferred.doneUnsafe(secondBegin, Effect.void) + } + callback(null, { rows: [] }) + } + } + + const sql = yield* PgClient.fromClient({ + acquire: Effect.succeed(pg as any), + acquireForStream: false + }) + const first = yield* sql.withTransaction(Effect.gen(function*() { + yield* Deferred.succeed(firstBodyStarted, undefined) + yield* Deferred.await(releaseFirstBody) + })).pipe(Effect.forkScoped) + yield* Deferred.await(firstBodyStarted) + const second = yield* sql.withTransaction(Effect.void).pipe(Effect.forkScoped) + + const overlap = yield* Deferred.await(secondBegin).pipe( + Effect.timeoutOption("100 millis"), + TestClock.withLive + ) + yield* Deferred.succeed(releaseFirstBody, undefined) + yield* Fiber.join(first) + yield* Fiber.join(second) + + assert.isTrue(Option.isNone(overlap)) + }).pipe( + Effect.scoped, + Effect.provide(Reactivity.layer) + )) diff --git a/.context/effect/packages/sql/pg/test/Client.test.ts b/.context/effect/packages/sql/pg/test/Client.test.ts deleted file mode 100644 index 43bba2896..000000000 --- a/.context/effect/packages/sql/pg/test/Client.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { PgClient } from "@effect/sql-pg" -import { assert, expect, it } from "@effect/vitest" -import { Effect, Fiber, Redacted, Stream, String } from "effect" -import { TestClock } from "effect/testing" -import { SqlClient } from "effect/unstable/sql" -import * as Statement from "effect/unstable/sql/Statement" -import { parse as parsePgConnectionString } from "pg-connection-string" -import { PgContainer } from "./utils.ts" - -const compilerTransform = PgClient.makeCompiler(String.camelToSnake) -const transformsNested = Statement.defaultTransforms(String.snakeToCamel) -const transforms = Statement.defaultTransforms(String.snakeToCamel, false) - -it.layer(PgContainer.layerClient, { timeout: "30 seconds" })("PgClient", (it) => { - it.effect("insert helper", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`INSERT INTO people ${sql.insert({ name: "Tim", age: 10 })}`.compile() - expect(query).toEqual(`INSERT INTO people ("name","age") VALUES ($1,$2)`) - expect(params).toEqual(["Tim", 10]) - })) - - it.effect("updateValues helper", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`UPDATE people SET name = data.name FROM ${ - sql.updateValues( - [{ name: "Tim" }, { name: "John" }], - "data" - ) - }`.compile() - expect(query).toEqual( - `UPDATE people SET name = data.name FROM (values ($1),($2)) AS data("name")` - ) - expect(params).toEqual(["Tim", "John"]) - })) - - it.effect("updateValues helper returning", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`UPDATE people SET name = data.name FROM ${ - sql.updateValues( - [{ name: "Tim" }, { name: "John" }], - "data" - ).returning("*") - }`.compile() - expect(query).toEqual( - `UPDATE people SET name = data.name FROM (values ($1),($2)) AS data("name") RETURNING *` - ) - expect(params).toEqual(["Tim", "John"]) - })) - - it.effect("update helper", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - let result = sql`UPDATE people SET ${sql.update({ name: "Tim" })}`.compile() - expect(result[0]).toEqual(`UPDATE people SET "name" = $1`) - expect(result[1]).toEqual(["Tim"]) - - result = sql`UPDATE people SET ${sql.update({ name: "Tim", age: 10 }, ["age"])}`.compile() - expect(result[0]).toEqual(`UPDATE people SET "name" = $1`) - expect(result[1]).toEqual(["Tim"]) - })) - - it.effect("update helper returning", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const result = sql`UPDATE people SET ${sql.update({ name: "Tim" }).returning("*")}`.compile() - expect(result[0]).toEqual(`UPDATE people SET "name" = $1 RETURNING *`) - expect(result[1]).toEqual(["Tim"]) - })) - - it.effect("array helper", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`SELECT * FROM ${sql("people")} WHERE id IN ${sql.in([1, 2, "string"])}`.compile() - expect(query).toEqual(`SELECT * FROM "people" WHERE id IN ($1,$2,$3)`) - expect(params).toEqual([1, 2, "string"]) - })) - - it.effect("array helper with column", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - let result = sql`SELECT * FROM ${sql("people")} WHERE ${sql.in("id", [1, 2, "string"])}`.compile() - expect(result[0]).toEqual(`SELECT * FROM "people" WHERE "id" IN ($1,$2,$3)`) - expect(result[1]).toEqual([1, 2, "string"]) - - result = sql`SELECT * FROM ${sql("people")} WHERE ${sql.in("id", [])}`.compile() - expect(result[0]).toEqual(`SELECT * FROM "people" WHERE 1=0`) - expect(result[1]).toEqual([]) - })) - - it.effect("and", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const now = new Date() - const result = sql`SELECT * FROM ${sql("people")} WHERE ${ - sql.and([ - sql.in("name", ["Tim", "John"]), - sql`created_at < ${now}` - ]) - }`.compile() - expect(result[0]).toEqual(`SELECT * FROM "people" WHERE ("name" IN ($1,$2) AND created_at < $3)`) - expect(result[1]).toEqual(["Tim", "John", now]) - })) - - it("transform nested", () => { - assert.deepEqual( - transformsNested.array([ - { - a_key: 1, - nested: [{ b_key: 2 }], - arr_primitive: [1, "2", true] - } - ]) as any, - [ - { - aKey: 1, - nested: [{ bKey: 2 }], - arrPrimitive: [1, "2", true] - } - ] - ) - }) - - it("transform non nested", () => { - assert.deepEqual( - transforms.array([ - { - a_key: 1, - nested: [{ b_key: 2 }], - arr_primitive: [1, "2", true] - } - ]) as any, - [ - { - aKey: 1, - nested: [{ b_key: 2 }], - arrPrimitive: [1, "2", true] - } - ] - ) - - assert.deepEqual( - transforms.array([ - { - json_field: { - test_value: [1, true, null, "text"], - test_nested: { - test_value: [1, true, null, "text"] - } - } - } - ]) as any, - [ - { - jsonField: { - test_value: [1, true, null, "text"], - test_nested: { - test_value: [1, true, null, "text"] - } - } - } - ] - ) - }) - - it.effect("insert fragments", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`INSERT INTO people ${ - sql.insert({ - name: "Tim", - age: 10, - json: sql.json({ a: 1 }) - }) - }`.compile() - assert.strictEqual( - query, - "INSERT INTO people (\"name\",\"age\",\"json\") VALUES ($1,$2,$3)" - ) - assert.lengthOf(params, 3) - })) - - it.effect("update fragments", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const now = new Date() - const [query, params] = sql`UPDATE people SET json = data.json FROM ${ - sql.updateValues( - [{ json: sql.json({ a: 1 }) }, { json: sql.json({ b: 1 }) }], - "data" - ) - } WHERE created_at > ${now}`.compile() - assert.strictEqual( - query, - `UPDATE people SET json = data.json FROM (values ($1),($2)) AS data("json") WHERE created_at > $3` - ) - assert.lengthOf(params, 3) - })) - - it.effect("onDialect", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - assert.strictEqual( - sql.onDialect({ - sqlite: () => "A", - pg: () => "B", - mysql: () => "C", - mssql: () => "D", - clickhouse: () => "E" - }), - "B" - ) - assert.strictEqual( - sql.onDialectOrElse({ - orElse: () => "A", - pg: () => "B" - }), - "B" - ) - })) - - it.effect("identifier transform", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query] = compilerTransform.compile( - sql`SELECT * from ${sql("peopleTest")}`, - false - ) - expect(query).toEqual(`SELECT * from "people_test"`) - })) - - it.effect("jsonb", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const rows = yield* sql<{ json: unknown }>`select ${{ testValue: 123 }}::jsonb as json` - expect(rows[0].json).toEqual({ testValue: 123 }) - })) - - it.effect("stream", () => - Effect.gen(function*() { - const sql = yield* SqlClient.SqlClient - const rows = yield* sql`SELECT generate_series(1, 3)`.stream.pipe( - Stream.runCollect - ) - expect(rows).toEqual([ - { "generate_series": 1 }, - { "generate_series": 2 }, - { "generate_series": 3 } - ]) - })) -}) - -it.layer(PgContainer.layerMakeClient, { timeout: "30 seconds" })("PgClient.makeClient", (it) => { - it.effect("connects before executing queries", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const rows = yield* sql<{ value: number }>`SELECT 1 AS value` - assert.deepStrictEqual(rows, [{ value: 1 }]) - })) -}) - -it.layer(PgContainer.layerClientWithTransforms, { timeout: "30 seconds" })("PgClient transforms", (it) => { - it.effect("insert helper", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const [query, params] = sql`INSERT INTO people ${sql.insert({ firstName: "Tim", age: 10 })}`.compile() - expect(query).toEqual(`INSERT INTO people ("first_name","age") VALUES ($1,$2)`) - expect(params).toEqual(["Tim", 10]) - })) - - it.effect("insert helper withoutTransforms", () => - Effect.gen(function*() { - const sql = (yield* PgClient.PgClient).withoutTransforms() - const [query, params] = sql`INSERT INTO people ${sql.insert({ first_name: "Tim", age: 10 })}`.compile() - expect(query).toEqual(`INSERT INTO people ("first_name","age") VALUES ($1,$2)`) - expect(params).toEqual(["Tim", 10]) - })) - - it.effect("multi-statement queries", () => - Effect.gen(function*() { - const sql = yield* SqlClient.SqlClient - - const result = yield* sql<{ id: string; name: string }>` - CREATE TABLE test_multi (id TEXT PRIMARY KEY, name TEXT); - INSERT INTO test_multi (id, name) VALUES ('id1', 'test1') RETURNING *; - INSERT INTO test_multi (id, name) VALUES ('id2', 'test2') RETURNING *; - ` - - expect(result).toHaveLength(3) - expect(result[0]).toEqual([]) - expect(result[1]).toEqual([{ id: "id1", name: "test1" }]) - expect(result[2]).toEqual([{ id: "id2", name: "test2" }]) - })) - - it.effect("interruption", () => - Effect.gen(function*() { - const sql = yield* SqlClient.SqlClient - const conn = yield* sql.reserve - yield* conn.executeRaw("select pg_sleep(1000)", []).pipe( - Effect.timeoutOption("50 millis"), - TestClock.withLive - ) - const value = yield* conn.executeValues("select 1", []) - expect(value).toEqual([[1]]) - })) - - it.effect("Should populate config", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - - assert.isDefined(sql.config.url) - - const parsedConfig = parsePgConnectionString(Redacted.value(sql.config.url)) - - expect(sql.config.host).toEqual(parsedConfig.host) - assert.isNotNull(parsedConfig.port) - assert.isDefined(parsedConfig.port) - expect(sql.config.port).toEqual(parseInt(parsedConfig.port)) - expect(sql.config.username).toEqual(parsedConfig.user) - assert.isDefined(sql.config.password) - expect(Redacted.value(sql.config.password)).toEqual(parsedConfig.password) - expect(sql.config.database).toEqual(parsedConfig.database) - })) -}) - -it.layer(PgContainer.layerClientSingleConnection, { timeout: "30 seconds" })("PgClient listen", (it) => { - it.effect("listen does not reserve a pool connection", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const channel = "pool_connection_listen" - - const listenFiber = yield* sql.listen(channel).pipe( - Stream.take(1), - Stream.runCollect, - Effect.forkScoped - ) - - yield* Effect.sleep("250 millis") - - const rows = yield* sql<{ value: number }>`SELECT 1 as value`.pipe( - Effect.timeoutOrElse({ - duration: "3 seconds", - orElse: () => Effect.fail(new Error("query timed out while listener was active")) - }) - ) - expect(rows).toEqual([{ value: 1 }]) - - yield* sql.notify(channel, "payload") - const payloads = yield* Fiber.join(listenFiber).pipe( - Effect.timeoutOrElse({ - duration: "3 seconds", - orElse: () => Effect.fail(new Error("listener did not receive notification in time")) - }) - ) - expect(Array.from(payloads)).toEqual(["payload"]) - }).pipe(TestClock.withLive), 20_000) - - it.effect("notify sends payload", () => - Effect.gen(function*() { - const sql = yield* PgClient.PgClient - const channel = "pool_connection_notify" - - const listenFiber = yield* sql.listen(channel).pipe( - Stream.take(1), - Stream.runCollect, - Effect.forkScoped - ) - - yield* Effect.sleep("250 millis") - yield* sql.notify(channel, "payload") - - const payloads = yield* Fiber.join(listenFiber).pipe( - Effect.timeoutOrElse({ - duration: "3 seconds", - orElse: () => Effect.fail(new Error("listener did not receive notification in time")) - }) - ) - expect(Array.from(payloads)).toEqual(["payload"]) - }).pipe(TestClock.withLive), 20_000) -}) diff --git a/.context/effect/packages/sql/pg/test/KeyValueStore.test.ts b/.context/effect/packages/sql/pg/test/KeyValueStore.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/pg/test/KeyValueStore.test.ts rename to .context/effect/packages/sql/pg/test/KeyValueStore.integration.test.ts diff --git a/.context/effect/packages/sql/pg/test/Persistence.integration.test.ts b/.context/effect/packages/sql/pg/test/Persistence.integration.test.ts new file mode 100644 index 000000000..5ffada648 --- /dev/null +++ b/.context/effect/packages/sql/pg/test/Persistence.integration.test.ts @@ -0,0 +1,155 @@ +import { assert, it } from "@effect/vitest" +import { Effect, Exit, Fiber, Latch, Layer, Schema } from "effect" +import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" +import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest" +import * as SqlCleanupTest from "effect-test/unstable/persistence/SqlCleanupTest" +import { TestClock } from "effect/testing" +import { PersistedQueue, Persistence } from "effect/unstable/persistence" +import { SqlClient } from "effect/unstable/sql" +import { PgContainer } from "./utils.ts" + +PersistedCacheTest.suite( + "sql-pg-multi", + Persistence.layerSqlMultiTable.pipe(Layer.provide(PgContainer.layerClient)) +) + +PersistedCacheTest.suite( + "sql-pg-single", + Persistence.layerSql.pipe(Layer.provide(PgContainer.layerClient)) +) + +PersistedQueueTest.suite( + "sql-pg", + PersistedQueue.layerStoreSql().pipe(Layer.provide(PgContainer.layerClient)) +) + +it.layer(PgContainer.layerClient, { timeout: "30 seconds" })("PersistedQueue SQL locks", (it) => { + it.effect("refreshes locks for acquired elements", () => + Effect.gen(function*() { + const options = { + tableName: "effect_queue_lock_refresh", + pollInterval: "10 millis", + lockRefreshInterval: "100 millis", + lockExpiration: "1 second" + } as const + const store1 = yield* PersistedQueue.makeStoreSql(options) + const store2 = yield* PersistedQueue.makeStoreSql(options) + const element = { message: "hello" } + + yield* store1.offer({ + name: "lock-refresh", + id: crypto.randomUUID(), + element, + isCustomId: false + }) + + const acquired = Latch.makeUnsafe() + const first = yield* Effect.scoped(Effect.gen(function*() { + yield* store1.take({ name: "lock-refresh", maxAttempts: 10 }) + yield* acquired.open + return yield* Effect.never + })).pipe(Effect.forkScoped) + + yield* acquired.await + + const second = yield* Effect.scoped( + store2.take({ name: "lock-refresh", maxAttempts: 10 }) + ).pipe(Effect.forkScoped) + + yield* Effect.sleep("1500 millis") + assert.isUndefined(second.pollUnsafe()) + + yield* Fiber.interrupt(first) + const received = yield* Fiber.join(second) + assert.deepStrictEqual(received.element, element) + }).pipe(TestClock.withLive)) + + it.effect("counts malformed JSON as an attempt and continues", () => + Effect.gen(function*() { + const tableName = "effect_queue_invalid_json" + const store = yield* PersistedQueue.makeStoreSql({ + tableName, + pollInterval: "10 millis" + }) + const factory = yield* PersistedQueue.makeFactory.pipe( + Effect.provideService(PersistedQueue.PersistedQueueStore, store) + ) + const queue = yield* factory.make({ + name: "invalid-json", + schema: Schema.String + }) + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const table = sql(tableName) + const poisonId = crypto.randomUUID() + + yield* store.offer({ + name: "invalid-json", + id: poisonId, + element: "poison", + isCustomId: false + }) + yield* sql`UPDATE ${table} SET element = ${"{"} WHERE id = ${poisonId}` + yield* queue.offer("valid") + + const malformed = yield* Effect.exit(queue.take(Effect.succeed, { maxAttempts: 1 })) + assert.isTrue(Exit.isFailure(malformed)) + + const rows = yield* sql<{ + readonly attempts: number + readonly last_failure: string | null + }>`SELECT attempts, last_failure FROM ${table} WHERE id = ${poisonId}` + assert.strictEqual(rows[0].attempts, 1) + assert.isNotNull(rows[0].last_failure) + + const value = yield* queue.take(Effect.succeed, { maxAttempts: 1 }) + assert.strictEqual(value, "valid") + }).pipe(TestClock.withLive)) +}) + +it.layer(PgContainer.layerClient, { timeout: "30 seconds" })("Persistence SQL cleanup", (it) => { + it.effect("deletes expired entries in batches", () => + Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const table = sql("effect_persistence") + const expiredCount = sql<{ readonly count: number }>` + SELECT COUNT(*)::INT AS count FROM ${table} WHERE store_id = 'expired' + `.pipe(Effect.map((rows) => rows[0].count)) + yield* sql` + CREATE TABLE ${table} ( + store_id TEXT NOT NULL, + id TEXT NOT NULL, + value TEXT NOT NULL, + expires BIGINT, + PRIMARY KEY (store_id, id) + ) + ` + + const entries = Array.from({ length: SqlCleanupTest.expiredEntryCount }, (_, i) => ({ + store_id: "expired", + id: String(i), + value: "{}", + expires: SqlCleanupTest.expiredAtEpoch + })) + yield* sql`INSERT INTO ${table} ${sql.insert(entries)}`.unprepared + yield* sql` + INSERT INTO ${table} (store_id, id, value, expires) + VALUES ('live', 'live', '{}', NULL), ('live', 'future', '{}', ${SqlCleanupTest.futureExpiresAt}) + ` + + yield* Layer.build(Persistence.layerBackingSql).pipe(TestClock.withLive) + + const expired = yield* SqlCleanupTest.waitForCount(expiredCount, (count) => count === 0) + assert.strictEqual(expired, 0) + const live = yield* sql<{ readonly count: number }>` + SELECT COUNT(*)::INT AS count FROM ${table} WHERE store_id = 'live' + ` + assert.strictEqual(live[0].count, 2) + + const indexes = yield* sql<{ readonly count: number }>` + SELECT COUNT(*)::INT AS count FROM pg_indexes + WHERE tablename = 'effect_persistence' + AND indexname = 'effect_persistence_expires_idx' + ` + assert.strictEqual(indexes[0].count, 1) + }), { timeout: SqlCleanupTest.testTimeout }) +}) diff --git a/.context/effect/packages/sql/pg/test/Persistence.test.ts b/.context/effect/packages/sql/pg/test/Persistence.test.ts deleted file mode 100644 index 4ee287631..000000000 --- a/.context/effect/packages/sql/pg/test/Persistence.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { assert, it } from "@effect/vitest" -import { Effect, Exit, Fiber, Latch, Layer, Schema } from "effect" -import * as PersistedCacheTest from "effect-test/unstable/persistence/PersistedCacheTest" -import * as PersistedQueueTest from "effect-test/unstable/persistence/PersistedQueueTest" -import { TestClock } from "effect/testing" -import { PersistedQueue, Persistence } from "effect/unstable/persistence" -import { SqlClient } from "effect/unstable/sql" -import { PgContainer } from "./utils.ts" - -PersistedCacheTest.suite( - "sql-pg-multi", - Persistence.layerSqlMultiTable.pipe(Layer.provide(PgContainer.layerClient)) -) - -PersistedCacheTest.suite( - "sql-pg-single", - Persistence.layerSql.pipe(Layer.provide(PgContainer.layerClient)) -) - -PersistedQueueTest.suite( - "sql-pg", - PersistedQueue.layerStoreSql().pipe(Layer.provide(PgContainer.layerClient)) -) - -it.layer(PgContainer.layerClient, { timeout: "30 seconds" })("PersistedQueue SQL locks", (it) => { - it.effect("refreshes locks for acquired elements", () => - Effect.gen(function*() { - const options = { - tableName: "effect_queue_lock_refresh", - pollInterval: "10 millis", - lockRefreshInterval: "100 millis", - lockExpiration: "1 second" - } as const - const store1 = yield* PersistedQueue.makeStoreSql(options) - const store2 = yield* PersistedQueue.makeStoreSql(options) - const element = { message: "hello" } - - yield* store1.offer({ - name: "lock-refresh", - id: crypto.randomUUID(), - element, - isCustomId: false - }) - - const acquired = Latch.makeUnsafe() - const first = yield* Effect.scoped(Effect.gen(function*() { - yield* store1.take({ name: "lock-refresh", maxAttempts: 10 }) - yield* acquired.open - return yield* Effect.never - })).pipe(Effect.forkScoped) - - yield* acquired.await - - const second = yield* Effect.scoped( - store2.take({ name: "lock-refresh", maxAttempts: 10 }) - ).pipe(Effect.forkScoped) - - yield* Effect.sleep("1500 millis") - assert.isUndefined(second.pollUnsafe()) - - yield* Fiber.interrupt(first) - const received = yield* Fiber.join(second) - assert.deepStrictEqual(received.element, element) - }).pipe(TestClock.withLive)) - - it.effect("counts malformed JSON as an attempt and continues", () => - Effect.gen(function*() { - const tableName = "effect_queue_invalid_json" - const store = yield* PersistedQueue.makeStoreSql({ - tableName, - pollInterval: "10 millis" - }) - const factory = yield* PersistedQueue.makeFactory.pipe( - Effect.provideService(PersistedQueue.PersistedQueueStore, store) - ) - const queue = yield* factory.make({ - name: "invalid-json", - schema: Schema.String - }) - const sql = (yield* SqlClient.SqlClient).withoutTransforms() - const table = sql(tableName) - const poisonId = crypto.randomUUID() - - yield* store.offer({ - name: "invalid-json", - id: poisonId, - element: "poison", - isCustomId: false - }) - yield* sql`UPDATE ${table} SET element = ${"{"} WHERE id = ${poisonId}` - yield* queue.offer("valid") - - const malformed = yield* Effect.exit(queue.take(Effect.succeed, { maxAttempts: 1 })) - assert.isTrue(Exit.isFailure(malformed)) - - const rows = yield* sql<{ - readonly attempts: number - readonly last_failure: string | null - }>`SELECT attempts, last_failure FROM ${table} WHERE id = ${poisonId}` - assert.strictEqual(rows[0].attempts, 1) - assert.isNotNull(rows[0].last_failure) - - const value = yield* queue.take(Effect.succeed, { maxAttempts: 1 }) - assert.strictEqual(value, "valid") - }).pipe(TestClock.withLive)) -}) diff --git a/.context/effect/packages/sql/pg/test/SqlEventLogServerUnencrypted.test.ts b/.context/effect/packages/sql/pg/test/SqlEventLogServerUnencrypted.integration.test.ts similarity index 100% rename from .context/effect/packages/sql/pg/test/SqlEventLogServerUnencrypted.test.ts rename to .context/effect/packages/sql/pg/test/SqlEventLogServerUnencrypted.integration.test.ts diff --git a/.context/effect/packages/sql/pg/tsconfig.json b/.context/effect/packages/sql/pg/tsconfig.json index 8380d9577..6c8560e50 100644 --- a/.context/effect/packages/sql/pg/tsconfig.json +++ b/.context/effect/packages/sql/pg/tsconfig.json @@ -1,9 +1,9 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node-shared" } + { "path": "../../platform/node-shared" } ] } diff --git a/.context/effect/packages/sql/pg/vitest.config.ts b/.context/effect/packages/sql/pg/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/pg/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/pglite/CHANGELOG.md b/.context/effect/packages/sql/pglite/CHANGELOG.md index d232844f7..7a9bc42d6 100644 --- a/.context/effect/packages/sql/pglite/CHANGELOG.md +++ b/.context/effect/packages/sql/pglite/CHANGELOG.md @@ -1,5 +1,56 @@ # @effect/sql-pglite +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/pglite/README.md b/.context/effect/packages/sql/pglite/README.md index cb6b3d5ee..f872efe25 100644 --- a/.context/effect/packages/sql/pglite/README.md +++ b/.context/effect/packages/sql/pglite/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-pglite` +# @effect/sql-pglite -An Effect SQL implementation using the `@electric-sql/pglite` library. +An Effect SQL client for [PGlite](https://pglite.dev), a WASM build of PostgreSQL that runs in the browser, Node.js, and Bun. + +## Installation + +```sh +npm install effect@beta @effect/sql-pglite@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-pglite). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-pglite) diff --git a/.context/effect/packages/sql/pglite/docgen.json b/.context/effect/packages/sql/pglite/docgen.json deleted file mode 100644 index 2810b4417..000000000 --- a/.context/effect/packages/sql/pglite/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/pglite/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/pglite/package.json b/.context/effect/packages/sql/pglite/package.json index 09a3d0aec..89089b187 100644 --- a/.context/effect/packages/sql/pglite/package.json +++ b/.context/effect/packages/sql/pglite/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-pglite", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A PGlite toolkit for Effect", @@ -33,6 +33,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -40,7 +41,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -50,6 +54,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -57,9 +62,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "effect": "workspace:^" diff --git a/.context/effect/packages/sql/pglite/src/PgliteClient.ts b/.context/effect/packages/sql/pglite/src/PgliteClient.ts index 95aa38a98..84dcc063f 100644 --- a/.context/effect/packages/sql/pglite/src/PgliteClient.ts +++ b/.context/effect/packages/sql/pglite/src/PgliteClient.ts @@ -60,7 +60,7 @@ export type TypeId = "~@effect/sql-pglite/PgliteClient" /** * PGlite-backed PostgreSQL client service, extending `SqlClient` with access to the PGlite instance, JSON fragments, LISTEN/NOTIFY, data directory dumps, and array type refresh. * - * @category models + * @category services * @since 4.0.0 */ export interface PgliteClient extends Client.SqlClient { @@ -436,13 +436,13 @@ const escapeLiteral = (value: string) => `'${value.replace(/'/g, "''")}'` /** * PGlite-specific custom statement fragments supported by the compiler, currently JSON parameter fragments. * - * @category custom types + * @category models * @since 4.0.0 */ export type PgCustom = PgJson /** - * @category custom types + * @category models * @since 4.0.0 */ interface PgJson extends Custom<"PgJson", unknown> {} diff --git a/.context/effect/packages/sql/pglite/src/PgliteMigrator.ts b/.context/effect/packages/sql/pglite/src/PgliteMigrator.ts index c6f9f15f6..c4da7bb24 100644 --- a/.context/effect/packages/sql/pglite/src/PgliteMigrator.ts +++ b/.context/effect/packages/sql/pglite/src/PgliteMigrator.ts @@ -22,7 +22,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -36,7 +36,7 @@ export const run: ( /** * Creates a layer that runs the configured SQL migrations during layer construction. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/pglite/test/Transaction.test.ts b/.context/effect/packages/sql/pglite/test/Transaction.test.ts index 9a9cc4514..0e2023b2a 100644 --- a/.context/effect/packages/sql/pglite/test/Transaction.test.ts +++ b/.context/effect/packages/sql/pglite/test/Transaction.test.ts @@ -1,6 +1,7 @@ import { PgliteClient } from "@effect/sql-pglite" import { assert, describe, layer } from "@effect/vitest" -import { Effect } from "effect" +import { Deferred, Effect } from "effect" +import { TestClock } from "effect/testing" const ClientLayer = PgliteClient.layer({}) @@ -58,5 +59,35 @@ describe("PgliteClient transactions", () => { ) assert.strictEqual(rows.at(0)?.total, 1) })) + + it.effect("preserves successful concurrent nested transactions", () => + Effect.gen(function*() { + const sql = yield* setup("tx_nested_concurrent") + const firstStarted = yield* Deferred.make() + const firstInserted = yield* Deferred.make() + + yield* sql.withTransaction( + Effect.all([ + sql.withTransaction( + Effect.gen(function*() { + yield* Deferred.succeed(firstStarted, undefined) + yield* Effect.sleep("100 millis") + yield* sql.unsafe(`INSERT INTO tx_nested_concurrent (name) VALUES ('first')`) + yield* Deferred.succeed(firstInserted, undefined) + }) + ), + Deferred.await(firstStarted).pipe( + Effect.andThen(sql.withTransaction( + Deferred.await(firstInserted).pipe( + Effect.andThen(Effect.fail("rollback")) + ) + )) + ) + ], { concurrency: "unbounded" }).pipe(Effect.catch(() => Effect.void)) + ) + + const rows = yield* sql.unsafe<{ name: string }>(`SELECT name FROM tx_nested_concurrent`) + assert.deepStrictEqual(rows, [{ name: "first" }]) + }).pipe(TestClock.withLive)) }) }) diff --git a/.context/effect/packages/sql/pglite/tsconfig.json b/.context/effect/packages/sql/pglite/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/pglite/tsconfig.json +++ b/.context/effect/packages/sql/pglite/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/pglite/vitest.config.ts b/.context/effect/packages/sql/pglite/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/pglite/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md b/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md index 9cc854726..52c17e2ae 100644 --- a/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-bun/CHANGELOG.md @@ -1,5 +1,58 @@ # @effect/sql-sqlite-bun +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- [#7162](https://github.com/Effect-TS/effect/pull/7162) [`c30386d`](https://github.com/Effect-TS/effect/commit/c30386df4be367350ee480fa357ddeae7a8f0d08) Thanks @tim-smart! - Use a configurable five-second busy timeout and immediate transactions by default to avoid SQLite lock failures under concurrent access. Busy waits can block the event loop, while immediate transactions serialize behind other writers. +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#6993](https://github.com/Effect-TS/effect/pull/6993) [`5d6fe77`](https://github.com/Effect-TS/effect/commit/5d6fe775b77528936ec3cb1e19bf817f65ecadb3) Thanks @fubhy! - Enforce read-only mode when opening Bun SQLite databases. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-bun/README.md b/.context/effect/packages/sql/sqlite-bun/README.md index 303b5e35b..bd4d525e7 100644 --- a/.context/effect/packages/sql/sqlite-bun/README.md +++ b/.context/effect/packages/sql/sqlite-bun/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-sqlite-bun` +# @effect/sql-sqlite-bun -An Effect SQL implementation using the `bun:sqlite` library. +An Effect SQL client for SQLite on the [Bun](https://bun.sh) runtime, built on `bun:sqlite`. + +## Installation + +```sh +npm install effect@beta @effect/sql-sqlite-bun@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-bun). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-sqlite-bun) diff --git a/.context/effect/packages/sql/sqlite-bun/docgen.json b/.context/effect/packages/sql/sqlite-bun/docgen.json deleted file mode 100644 index 187844a7d..000000000 --- a/.context/effect/packages/sql/sqlite-bun/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-bun/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/sqlite-bun/package.json b/.context/effect/packages/sql/sqlite-bun/package.json index 59c068223..68a77f3e4 100644 --- a/.context/effect/packages/sql/sqlite-bun/package.json +++ b/.context/effect/packages/sql/sqlite-bun/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-bun", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "@effect/platform-bun": "workspace:^", diff --git a/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts index 1e3493e41..e54047a11 100644 --- a/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-bun/src/SqliteClient.ts @@ -3,14 +3,20 @@ * * This module opens a SQLite database and exposes it as both `SqliteClient` and * the generic Effect SQL client. It serializes access to the database, enables - * WAL mode unless disabled, and supports database export and extension loading. - * Streaming queries and `updateValues` are not supported by this driver. + * WAL mode unless disabled, and waits up to five seconds for busy databases by + * default. Explicit transactions on writable connections use `BEGIN IMMEDIATE` + * to avoid read-to-write lock upgrades, which serializes them behind other + * writers even when they only read. Clients opened with `readonly: true` are + * unaffected. Busy waits block the event loop because `bun:sqlite` is + * synchronous. Database export and extension loading are supported; streaming + * queries and `updateValues` are not. * * @since 4.0.0 */ import { Database } from "bun:sqlite" import * as Config from "effect/Config" import * as Context from "effect/Context" +import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" @@ -25,6 +31,7 @@ import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" import * as Statement from "effect/unstable/sql/Statement" const ATTR_DB_SYSTEM_NAME = "db.system.name" +const MAX_BUSY_TIMEOUT = 2_147_483_647 const classifyError = (cause: unknown, message: string, operation: string) => classifySqliteError(cause, { message, operation }) @@ -48,7 +55,7 @@ export type TypeId = "~@effect/sql-sqlite-bun/SqliteClient" /** * Bun SQLite client service, extending `SqlClient` with database export and extension loading helpers. `updateValues` is not supported. * - * @category models + * @category services * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { @@ -74,7 +81,7 @@ export interface SqliteClient extends Client.SqlClient { export const SqliteClient = Context.Service("@effect/sql-sqlite-bun/Client") /** - * Configuration for a Bun SQLite client, including filename, open mode flags, WAL behavior, span attributes, and query/result name transforms. + * Configuration for a Bun SQLite client, including filename, open mode flags, WAL and busy timeout behavior, span attributes, and query/result name transforms. * * @category models * @since 4.0.0 @@ -85,6 +92,12 @@ export interface SqliteClientConfig { readonly create?: boolean | undefined readonly readwrite?: boolean | undefined readonly disableWAL?: boolean | undefined + /** + * How long SQLite waits when the database is busy. Defaults to 5 seconds. + * `Duration.infinity` is clamped to SQLite's maximum timeout. + * Waiting blocks the event loop because `bun:sqlite` is synchronous. + */ + readonly busyTimeout?: Duration.Input | undefined readonly spanAttributes?: Record | undefined @@ -98,7 +111,7 @@ interface SqliteConnection extends Connection { } /** - * Creates a scoped Bun SQLite client for a database file, enabling WAL by default and serializing access. Streaming queries are not implemented. + * Creates a scoped Bun SQLite client for a database file, enabling WAL and a 5-second busy timeout by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected. Streaming queries are not implemented. * * @category constructors * @since 4.0.0 @@ -115,14 +128,20 @@ export const make = ( undefined const makeConnection = Effect.gen(function*() { + const readonly = options.readonly === true const db = new Database(options.filename, { - readonly: options.readonly, - readwrite: options.readwrite ?? true, - create: options.create ?? true + readonly, + readwrite: readonly ? false : options.readwrite ?? true, + create: readonly ? false : options.create ?? true } as any) yield* Effect.addFinalizer(() => Effect.sync(() => db.close())) + const busyTimeout = Math.min( + MAX_BUSY_TIMEOUT, + Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))) + ) + db.run(`PRAGMA busy_timeout = ${busyTimeout};`) - if (options.disableWAL !== true) { + if (options.disableWAL !== true && !readonly) { db.run("PRAGMA journal_mode = WAL;") } @@ -216,6 +235,7 @@ export const make = ( acquirer, compiler, transactionAcquirer, + beginTransaction: "BEGIN IMMEDIATE", spanAttributes: [ ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "sqlite"] diff --git a/.context/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts index a07cb54d6..7d90ca4c6 100644 --- a/.context/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-bun/src/SqliteMigrator.ts @@ -22,7 +22,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -78,7 +78,7 @@ export const run: ( /** * Creates a layer that runs the configured SQL migrations during layer construction. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/sqlite-bun/test/Client.test.ts b/.context/effect/packages/sql/sqlite-bun/test/Client.test.ts index fc3285a36..905a4a5e6 100644 --- a/.context/effect/packages/sql/sqlite-bun/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-bun/test/Client.test.ts @@ -1,6 +1,72 @@ -import { describe, it } from "@effect/vitest" -import { Effect } from "effect" +import { assert, describe, it } from "@effect/vitest" +import { Duration, Effect } from "effect" +import { Reactivity } from "effect/unstable/reactivity" +import { rm } from "node:fs/promises" + +const isBun = "bun" in process.versions describe("Client", () => { it.effect("should work", () => Effect.void) + + it.effect.skipIf(!isBun)("uses a 5 second busy timeout", () => + Effect.gen(function*() { + const { SqliteClient } = yield* Effect.promise(() => import("@effect/sql-sqlite-bun")) + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + assert.deepStrictEqual(yield* sql`PRAGMA busy_timeout`, [{ timeout: 5000 }]) + + const custom = yield* SqliteClient.make({ filename: ":memory:", busyTimeout: "1 second" }) + assert.deepStrictEqual(yield* custom`PRAGMA busy_timeout`, [{ timeout: 1000 }]) + + const infinite = yield* SqliteClient.make({ filename: ":memory:", busyTimeout: Duration.infinity }) + assert.deepStrictEqual(yield* infinite`PRAGMA busy_timeout`, [{ timeout: 2_147_483_647 }]) + }).pipe(Effect.provide(Reactivity.layer))) + + it.effect.skipIf(!isBun)("starts transactions immediately", () => + Effect.gen(function*() { + const { SqliteClient } = yield* Effect.promise(() => import("@effect/sql-sqlite-bun")) + const filename = `/tmp/effect-sqlite-bun-transaction-${crypto.randomUUID()}.db` + yield* Effect.acquireRelease( + Effect.void, + () => Effect.promise(() => rm(filename, { force: true })) + ) + + const client = yield* SqliteClient.make({ filename }) + const contender = yield* SqliteClient.make({ filename }) + yield* contender`PRAGMA busy_timeout = 1` + + yield* client.withTransaction( + Effect.gen(function*() { + const error = yield* Effect.flip(contender`BEGIN IMMEDIATE`) + assert.strictEqual(error._tag, "SqlError") + assert(error.reason.cause instanceof Error) + assert.match(error.reason.cause.message, /database is locked/i) + }) + ) + }).pipe(Effect.provide(Reactivity.layer))) + + it.effect.skipIf(!isBun)("readonly clients reject writes", () => + Effect.gen(function*() { + const { SqliteClient } = yield* Effect.promise(() => import("@effect/sql-sqlite-bun")) + const filename = `/tmp/effect-sqlite-bun-readonly-${crypto.randomUUID()}.db` + yield* Effect.acquireRelease( + Effect.void, + () => Effect.promise(() => rm(filename, { force: true })) + ) + + yield* Effect.scoped( + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename }) + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY)` + }) + ) + + const sql = yield* SqliteClient.make({ filename, readonly: true }) + assert.deepStrictEqual(yield* sql`SELECT * FROM test`, []) + assert.deepStrictEqual(yield* sql.withTransaction(sql`SELECT * FROM test`), []) + + const error = yield* Effect.flip(sql`INSERT INTO test DEFAULT VALUES`) + assert.strictEqual(error._tag, "SqlError") + assert(error.reason.cause instanceof Error) + assert.match(error.reason.cause.message, /attempt to write a readonly database/i) + }).pipe(Effect.provide(Reactivity.layer))) }) diff --git a/.context/effect/packages/sql/sqlite-bun/tsconfig.json b/.context/effect/packages/sql/sqlite-bun/tsconfig.json index cca75f8a0..aee873cc4 100644 --- a/.context/effect/packages/sql/sqlite-bun/tsconfig.json +++ b/.context/effect/packages/sql/sqlite-bun/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/sqlite-bun/vitest.config.ts b/.context/effect/packages/sql/sqlite-bun/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/sqlite-bun/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/sqlite-do/CHANGELOG.md b/.context/effect/packages/sql/sqlite-do/CHANGELOG.md index c70f492ef..10b78b694 100644 --- a/.context/effect/packages/sql/sqlite-do/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-do/CHANGELOG.md @@ -1,5 +1,58 @@ # @effect/sql-sqlite-do +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-do/README.md b/.context/effect/packages/sql/sqlite-do/README.md index 33f3cef87..4fa150374 100644 --- a/.context/effect/packages/sql/sqlite-do/README.md +++ b/.context/effect/packages/sql/sqlite-do/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-sqlite-do` +# @effect/sql-sqlite-do -An Effect SQL implementation for Cloudflare Durable Objects SQLite storage. +An Effect SQL client for the SQLite storage in [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/). + +## Installation + +```sh +npm install effect@beta @effect/sql-sqlite-do@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-do). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-sqlite-do) diff --git a/.context/effect/packages/sql/sqlite-do/docgen.json b/.context/effect/packages/sql/sqlite-do/docgen.json deleted file mode 100644 index 7b6a5f3a4..000000000 --- a/.context/effect/packages/sql/sqlite-do/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-do/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/sqlite-do/package.json b/.context/effect/packages/sql/sqlite-do/package.json index 9cad3395f..fdd4ce786 100644 --- a/.context/effect/packages/sql/sqlite-do/package.json +++ b/.context/effect/packages/sql/sqlite-do/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-do", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,9 +58,7 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "@cloudflare/workers-types": "^5.20260708.1", diff --git a/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts index 9df97d269..7a4c1a253 100644 --- a/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-do/src/SqliteClient.ts @@ -28,6 +28,7 @@ import * as Exit from "effect/Exit" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" import * as Layer from "effect/Layer" +import * as Rec from "effect/Record" import * as Scope from "effect/Scope" import * as Semaphore from "effect/Semaphore" import * as Stream from "effect/Stream" @@ -61,7 +62,7 @@ export type TypeId = "~@effect/sql-sqlite-do/SqliteClient" /** * Cloudflare Durable Object SQLite client service, extending `SqlClient` with its configuration. `updateValues` is not supported. * - * @category models + * @category services * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { @@ -199,7 +200,7 @@ export const make = ( const obj: any = {} for (let i = 0; i < columns.length; i++) { const value = result[i] - obj[columns[i]] = value instanceof ArrayBuffer ? new Uint8Array(value) : value + Rec.assignProperty(obj, columns[i], value instanceof ArrayBuffer ? new Uint8Array(value) : value) } yield obj } diff --git a/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts index c02c16727..234e276a3 100644 --- a/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-do/src/SqliteMigrator.ts @@ -38,7 +38,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations using the configured `SqlClient`, returning the migrations that were applied. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -52,7 +52,7 @@ export const run: ( /** * Creates a layer that runs the configured SQL migrations during layer construction. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/sqlite-do/tsconfig.json b/.context/effect/packages/sql/sqlite-do/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/sqlite-do/tsconfig.json +++ b/.context/effect/packages/sql/sqlite-do/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/sqlite-do/vitest.config.ts b/.context/effect/packages/sql/sqlite-do/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/sqlite-do/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/sqlite-node/CHANGELOG.md b/.context/effect/packages/sql/sqlite-node/CHANGELOG.md index 03ff3fbfb..bcc72d04c 100644 --- a/.context/effect/packages/sql/sqlite-node/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-node/CHANGELOG.md @@ -1,5 +1,57 @@ # @effect/sql-sqlite-node +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- [#7162](https://github.com/Effect-TS/effect/pull/7162) [`c30386d`](https://github.com/Effect-TS/effect/commit/c30386df4be367350ee480fa357ddeae7a8f0d08) Thanks @tim-smart! - Use a configurable five-second busy timeout and immediate transactions by default to avoid SQLite lock failures under concurrent access. Busy waits can block the event loop, while immediate transactions serialize behind other writers. +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-node/README.md b/.context/effect/packages/sql/sqlite-node/README.md index 8c4dd1432..57a8e16c4 100644 --- a/.context/effect/packages/sql/sqlite-node/README.md +++ b/.context/effect/packages/sql/sqlite-node/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-sqlite-node` +# @effect/sql-sqlite-node -An Effect SQL implementation using Node.js' built-in `node:sqlite` module. +An Effect SQL client for SQLite on Node.js, built on the built-in `node:sqlite` module. + +## Installation + +```sh +npm install effect@beta @effect/sql-sqlite-node@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-node). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-sqlite-node) diff --git a/.context/effect/packages/sql/sqlite-node/docgen.json b/.context/effect/packages/sql/sqlite-node/docgen.json deleted file mode 100644 index fc71e4926..000000000 --- a/.context/effect/packages/sql/sqlite-node/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-node/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/sqlite-node/package.json b/.context/effect/packages/sql/sqlite-node/package.json index e44f36597..98a111876 100644 --- a/.context/effect/packages/sql/sqlite-node/package.json +++ b/.context/effect/packages/sql/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-node", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,13 +58,11 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "@effect/platform-node": "workspace:^", - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "effect": "workspace:^" }, "peerDependencies": { diff --git a/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts index a1f3c1a32..9124443ab 100644 --- a/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-node/src/SqliteClient.ts @@ -3,9 +3,14 @@ * * This module opens a SQLite database and exposes it as both `SqliteClient` and * the generic Effect SQL client. It serializes access through one connection, - * caches prepared statements, enables WAL mode unless disabled, and supports - * database backup, and extension loading. Streaming queries and - * `updateValues` are not supported by this driver. + * caches prepared statements, enables WAL mode unless disabled, and waits up + * to five seconds for busy databases by default. Explicit transactions on + * writable connections use `BEGIN IMMEDIATE` to avoid read-to-write lock + * upgrades, which serializes them behind other writers even when they only + * read. Clients opened with `readonly: true` are unaffected. Busy waits block + * the Node.js event loop because `node:sqlite` is synchronous. Database backup + * and extension loading are supported; streaming queries and `updateValues` + * are not. * * @since 4.0.0 */ @@ -29,6 +34,7 @@ import { backup as backupDatabase, DatabaseSync } from "node:sqlite" import type { StatementSync } from "node:sqlite" const ATTR_DB_SYSTEM_NAME = "db.system.name" +const MAX_BUSY_TIMEOUT = 2_147_483_647 /** * Runtime type identifier used to mark Node `SqliteClient` values. @@ -49,7 +55,7 @@ export type TypeId = "~@effect/sql-sqlite-node/SqliteClient" /** * Node SQLite client service, extending `SqlClient` with database export, backup, and extension loading helpers. `updateValues` is not supported. * - * @category models + * @category services * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { @@ -82,7 +88,7 @@ export interface BackupMetadata { export const SqliteClient = Context.Service("@effect/sql-sqlite-node/SqliteClient") /** - * Configuration for a node SQLite client backed by `node:sqlite`, including the database filename, read-only mode, statement cache settings, WAL behavior, span attributes, and query/result name transforms. + * Configuration for a node SQLite client backed by `node:sqlite`, including the database filename, read-only mode, statement cache settings, WAL and busy timeout behavior, span attributes, and query/result name transforms. * * @category models * @since 4.0.0 @@ -93,6 +99,12 @@ export interface SqliteClientConfig { readonly prepareCacheSize?: number | undefined readonly prepareCacheTTL?: Duration.Input | undefined readonly disableWAL?: boolean | undefined + /** + * How long SQLite waits when the database is busy. Defaults to 5 seconds. + * `Duration.infinity` is clamped to SQLite's maximum timeout. + * Waiting blocks the Node.js event loop because `node:sqlite` is synchronous. + */ + readonly busyTimeout?: Duration.Input | undefined readonly spanAttributes?: Record | undefined readonly transformResultNames?: ((str: string) => string) | undefined @@ -105,7 +117,7 @@ interface SqliteConnection extends Connection { } /** - * Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL enabled by default and exposing SQLite-specific `export`, `backup`, and `loadExtension` operations. + * Creates a scoped node SQLite client from the supplied configuration, using a single serialized connection with WAL and a 5-second busy timeout enabled by default. Explicit transactions on writable connections take the write lock for their duration, even when they only read; clients opened with `readonly: true` are unaffected. * * @category constructors * @since 4.0.0 @@ -129,6 +141,11 @@ export const make = ( }) yield* Scope.addFinalizer(scope, Effect.sync(() => db.close())) db.enableLoadExtension(false) + const busyTimeout = Math.min( + MAX_BUSY_TIMEOUT, + Math.max(0, Math.round(Duration.toMillis(options.busyTimeout ?? Duration.seconds(5)))) + ) + db.exec(`PRAGMA busy_timeout = ${busyTimeout}`) if (options.disableWAL !== true) { db.exec("PRAGMA journal_mode = WAL") @@ -303,6 +320,7 @@ export const make = ( acquirer, compiler, transactionAcquirer, + beginTransaction: "BEGIN IMMEDIATE", spanAttributes: [ ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "sqlite"] diff --git a/.context/effect/packages/sql/sqlite-node/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-node/src/SqliteMigrator.ts index 8ea3e7801..ba8441a2a 100644 --- a/.context/effect/packages/sql/sqlite-node/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-node/src/SqliteMigrator.ts @@ -22,7 +22,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations for a SQLite database using the shared `Migrator` implementation and the current `SqlClient`. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -78,7 +78,7 @@ export const run: ( /** * Creates a layer that runs the configured SQLite migrations during layer construction and provides no services. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/sqlite-node/test/Client.test.ts b/.context/effect/packages/sql/sqlite-node/test/Client.test.ts index 8c35e0f11..3a20cec55 100644 --- a/.context/effect/packages/sql/sqlite-node/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/Client.test.ts @@ -1,7 +1,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { SqliteClient } from "@effect/sql-sqlite-node" import { assert, describe, it } from "@effect/vitest" -import { Effect, FileSystem } from "effect" +import { Duration, Effect, FileSystem } from "effect" import { Reactivity } from "effect/unstable/reactivity" const makeClient = Effect.gen(function*() { @@ -12,6 +12,16 @@ const makeClient = Effect.gen(function*() { }) }).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer])) +const makeClients = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + const filename = dir + "/test.db" + return { + client: yield* SqliteClient.make({ filename }), + contender: yield* SqliteClient.make({ filename }) + } +}).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer])) + describe("Client", () => { it.effect("should work", () => Effect.gen(function*() { @@ -75,6 +85,54 @@ describe("Client", () => { assert.deepStrictEqual(rows, []) })) + it.effect("uses a 5 second busy timeout", () => + Effect.gen(function*() { + const sql = yield* makeClient + assert.deepStrictEqual(yield* sql`PRAGMA busy_timeout`, [{ timeout: 5000 }]) + + const custom = yield* SqliteClient.make({ filename: ":memory:", busyTimeout: "1 second" }).pipe( + Effect.provide(Reactivity.layer) + ) + assert.deepStrictEqual(yield* custom`PRAGMA busy_timeout`, [{ timeout: 1000 }]) + + const infinite = yield* SqliteClient.make({ filename: ":memory:", busyTimeout: Duration.infinity }).pipe( + Effect.provide(Reactivity.layer) + ) + assert.deepStrictEqual(yield* infinite`PRAGMA busy_timeout`, [{ timeout: 2_147_483_647 }]) + })) + + it.effect("starts transactions immediately", () => + Effect.gen(function*() { + const { client, contender } = yield* makeClients + yield* contender`PRAGMA busy_timeout = 1` + + yield* client.withTransaction( + Effect.gen(function*() { + const error = yield* Effect.flip(contender`BEGIN IMMEDIATE`) + assert.strictEqual(error._tag, "SqlError") + assert(error.reason.cause instanceof Error) + assert.match(error.reason.cause.message, /database is locked/i) + }) + ) + })) + + it.effect("supports transactions on readonly clients", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const dir = yield* fs.makeTempDirectoryScoped() + const filename = dir + "/test.db" + + yield* Effect.scoped( + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename }) + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY)` + }) + ) + + const sql = yield* SqliteClient.make({ filename, readonly: true }) + assert.deepStrictEqual(yield* sql.withTransaction(sql`SELECT * FROM test`), []) + }).pipe(Effect.provide([NodeFileSystem.layer, Reactivity.layer]))) + it.effect("supports backup and export", () => Effect.gen(function*() { const sql = yield* makeClient diff --git a/.context/effect/packages/sql/sqlite-node/test/Persistence.test.ts b/.context/effect/packages/sql/sqlite-node/test/Persistence.test.ts index 78313b98a..0acc117ac 100644 --- a/.context/effect/packages/sql/sqlite-node/test/Persistence.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/Persistence.test.ts @@ -1,11 +1,12 @@ import { NodeFileSystem } from "@effect/platform-node" import { SqliteClient } from "@effect/sql-sqlite-node" -import { expect, it } from "@effect/vitest" +import { assert, expect, it } from "@effect/vitest" import { Duration, Effect, FileSystem, Layer } from "effect" +import * as SqlCleanupTest from "effect-test/unstable/persistence/SqlCleanupTest" import { TestClock } from "effect/testing" import { Persistence } from "effect/unstable/persistence" import { Reactivity } from "effect/unstable/reactivity" -import type * as SqlClient from "effect/unstable/sql/SqlClient" +import * as SqlClient from "effect/unstable/sql/SqlClient" const ClientLayer = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem @@ -53,6 +54,15 @@ const suite = (name: string, layer: Layer.Layer + Effect.gen(function*() { + const persistence = yield* Persistence.BackingPersistence + const store = yield* persistence.make("test_store_duplicate_keys") + yield* store.set("key", { value: 1 }, undefined) + + expect(yield* store.getMany(["key", "key"])).toEqual([{ value: 1 }, { value: 1 }]) + })) + it.effect("remove", () => Effect.gen(function*() { const persistence = yield* Persistence.BackingPersistence @@ -107,3 +117,51 @@ const suite = (name: string, layer: Layer.Layer { + it.effect("deletes expired entries in batches", () => + Effect.gen(function*() { + const sql = (yield* SqlClient.SqlClient).withoutTransforms() + const table = sql("effect_persistence") + const expiredCount = sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM ${table} WHERE store_id = 'expired' + `.pipe(Effect.map((rows) => rows[0].count)) + yield* sql` + CREATE TABLE ${table} ( + store_id TEXT NOT NULL, + id TEXT NOT NULL, + value TEXT NOT NULL, + expires INTEGER, + PRIMARY KEY (store_id, id) + ) + ` + + const entries = Array.from({ length: SqlCleanupTest.expiredEntryCount }, (_, i) => ({ + store_id: "expired", + id: String(i), + value: "{}", + expires: SqlCleanupTest.expiredAtEpoch + })) + yield* sql`INSERT INTO ${table} ${sql.insert(entries)}`.unprepared + yield* sql` + INSERT INTO ${table} (store_id, id, value, expires) + VALUES ('live', 'live', '{}', NULL), ('live', 'future', '{}', ${SqlCleanupTest.futureExpiresAt}) + ` + + yield* Layer.build(Persistence.layerBackingSql).pipe(TestClock.withLive) + + const expired = yield* SqlCleanupTest.waitForCount(expiredCount, (count) => count === 0) + assert.strictEqual(expired, 0) + const live = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM ${table} WHERE store_id = 'live' + ` + assert.strictEqual(live[0].count, 2) + + const indexes = yield* sql<{ readonly count: number }>` + SELECT COUNT(*) AS count FROM sqlite_master + WHERE type = 'index' + AND name = 'effect_persistence_expires_idx' + ` + assert.strictEqual(indexes[0].count, 1) + }), { timeout: SqlCleanupTest.testTimeout }) +}) diff --git a/.context/effect/packages/sql/sqlite-node/test/SqlEventJournal.test.ts b/.context/effect/packages/sql/sqlite-node/test/SqlEventJournal.test.ts index 5eeb0a294..0b4327780 100644 --- a/.context/effect/packages/sql/sqlite-node/test/SqlEventJournal.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/SqlEventJournal.test.ts @@ -14,6 +14,19 @@ const makeJournal = Effect.gen(function*() { }).pipe(Effect.provide(Reactivity.layer)) describe("SqlEventJournal", () => { + it.effect("commits only after the write callback succeeds", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const journal = yield* SqlEventJournal.make().pipe(Effect.provideService(SqlClient.SqlClient, sql)) + yield* Effect.exit(journal.write({ + event: "Repro", + primaryKey: "key", + payload: new Uint8Array([1]), + effect: () => Effect.fail("callback failed") + })) + assert.deepStrictEqual(yield* journal.entries, []) + }).pipe(Effect.provide(Reactivity.layer))) + it.effect("writes and reads entries", () => Effect.gen(function*() { const journal = yield* makeJournal diff --git a/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerEncrypted.test.ts b/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerEncrypted.test.ts index 57bc47d89..0db9b95c6 100644 --- a/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerEncrypted.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerEncrypted.test.ts @@ -32,15 +32,32 @@ const persistEntries = ( ) => Effect.gen(function*() { const encrypted = yield* encryption.encrypt(identity, entries) - return encrypted.encryptedEntries.map((encryptedEntry, index) => + return encrypted.map(({ encryptedEntry, iv }, index) => new EventLogServer.PersistedEntry({ entryId: entries[index].id, - iv: encrypted.iv, + iv, encryptedEntry }) ) }) +const encodeWrite = Effect.fnUntraced(function*( + encryption: EventLogEncryption.EventLogEncryption["Service"], + identity: EventLog.Identity["Service"], + entry: EventJournal.Entry +) { + const encrypted = yield* encryption.encrypt(identity, [entry]) + return yield* new EventLogMessage.WriteEntries({ + publicKey: identity.publicKey, + storeId: storeIdA, + encryptedEntries: [{ + entryId: entry.id, + iv: encrypted[0].iv, + encryptedEntry: encrypted[0].encryptedEntry + }] + }).encoded +}) + const makePersistedEntry = (index: number, entryId = EventJournal.makeEntryIdUnsafe()) => new EventLogServer.PersistedEntry({ entryId, @@ -71,7 +88,201 @@ const makeAuthenticateRequest = Effect.fnUntraced(function*(options: { }) }) +const makeAuthenticatedRpcClient = Effect.fnUntraced(function*( + storage: EventLogServer.Storage["Service"], + identities: ReadonlyArray +) { + const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe( + Effect.provide( + EventLogServer.layerRpcHandlers.pipe( + Layer.provide(Layer.succeed(EventLogServer.Storage, storage)) + ) + ) + ) + for (const identity of identities) { + const hello = yield* rpcClient["EventLog.Hello"]() + yield* rpcClient["EventLog.Authenticate"]( + yield* makeAuthenticateRequest({ + identity, + challenge: hello.challenge, + remoteId: hello.remoteId + }) + ) + } + return rpcClient +}) + +const assertForbidden = Effect.fnUntraced(function*( + effect: Effect.Effect +) { + const error = yield* Effect.flip(effect) + assert.instanceOf(error, EventLogMessage.EventLogProtocolError) + assert.strictEqual(error.code, "Forbidden") +}) + describe("SqlEventLogServer", () => { + it.effect("forbids reading another identity's changes", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServer.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql) + ) + const encryption = yield* EventLogEncryption.EventLogEncryption + const identityA = yield* encryption.generateIdentity + const identityB = yield* encryption.generateIdentity + const rpcClient = yield* makeAuthenticatedRpcClient(storage, [identityA]) + + yield* storage.write(identityB.publicKey, storeIdA, [makePersistedEntry(1)]) + const error = yield* rpcClient["EventLog.Changes"]({ + publicKey: identityB.publicKey, + storeId: storeIdA, + startSequence: 0 + }).pipe( + Stream.take(1), + Stream.runCollect, + Effect.flip + ) + + assert.instanceOf(error, EventLogMessage.EventLogProtocolError) + assert.strictEqual(error.code, "Forbidden") + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + + it.effect("forbids writing entries for another identity", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServer.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql) + ) + const encryption = yield* EventLogEncryption.EventLogEncryption + const identityA = yield* encryption.generateIdentity + const identityB = yield* encryption.generateIdentity + const rpcClient = yield* makeAuthenticatedRpcClient(storage, [identityA]) + const entry = makeEntry(1) + const data = yield* encodeWrite(encryption, identityB, entry) + + const error = yield* rpcClient["EventLog.WriteSingle"]({ data }).pipe(Effect.flip) + + assert.instanceOf(error, EventLogMessage.EventLogProtocolError) + assert.strictEqual(error.code, "Forbidden") + const written = yield* storage.write(identityB.publicKey, storeIdA, [makePersistedEntry(2)]) + assert.deepStrictEqual(written.map((entry) => entry.sequence), [1]) + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + + it.effect("forbids writing chunked entries for another identity", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServer.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql) + ) + const encryption = yield* EventLogEncryption.EventLogEncryption + const identityA = yield* encryption.generateIdentity + const identityB = yield* encryption.generateIdentity + const rpcClient = yield* makeAuthenticatedRpcClient(storage, [identityA]) + const entry = makeEntry(1) + const data = yield* encodeWrite(encryption, identityB, entry) + const midpoint = Math.ceil(data.byteLength / 2) + const parts = [ + new EventLogMessage.ChunkedMessage({ id: 1, part: [0, 2], data: data.subarray(0, midpoint) }), + new EventLogMessage.ChunkedMessage({ id: 1, part: [1, 2], data: data.subarray(midpoint) }) + ] as const + + yield* rpcClient["EventLog.WriteChunked"](parts[0]) + const error = yield* rpcClient["EventLog.WriteChunked"](parts[1]).pipe(Effect.flip) + + assert.instanceOf(error, EventLogMessage.EventLogProtocolError) + assert.strictEqual(error.code, "Forbidden") + const written = yield* storage.write(identityB.publicKey, storeIdA, [makePersistedEntry(2)]) + assert.deepStrictEqual(written.map((entry) => entry.sequence), [1]) + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + + it.effect("isolates authenticated identities between connections", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServer.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql) + ) + const encryption = yield* EventLogEncryption.EventLogEncryption + const identityA = yield* encryption.generateIdentity + const identityB = yield* encryption.generateIdentity + const handlers = yield* Layer.build( + EventLogServer.layerRpcHandlers.pipe( + Layer.provide(Layer.succeed(EventLogServer.Storage, storage)) + ) + ) + const rpcClientA = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe( + Effect.provide(handlers) + ) + const rpcClientB = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe( + Effect.provide(handlers) + ) + const helloA = yield* rpcClientA["EventLog.Hello"]() + yield* rpcClientA["EventLog.Authenticate"]( + yield* makeAuthenticateRequest({ + identity: identityA, + challenge: helloA.challenge, + remoteId: helloA.remoteId + }) + ) + const helloB = yield* rpcClientB["EventLog.Hello"]() + yield* rpcClientB["EventLog.Authenticate"]( + yield* makeAuthenticateRequest({ + identity: identityB, + challenge: helloB.challenge, + remoteId: helloB.remoteId + }) + ) + const data = yield* encodeWrite(encryption, identityB, makeEntry(1)) + const midpoint = Math.ceil(data.byteLength / 2) + const parts = [ + new EventLogMessage.ChunkedMessage({ id: 1, part: [0, 2], data: data.subarray(0, midpoint) }), + new EventLogMessage.ChunkedMessage({ id: 1, part: [1, 2], data: data.subarray(midpoint) }) + ] as const + + yield* storage.write(identityB.publicKey, storeIdA, [makePersistedEntry(1)]) + yield* assertForbidden( + rpcClientA["EventLog.Changes"]({ + publicKey: identityB.publicKey, + storeId: storeIdA, + startSequence: 0 + }).pipe(Stream.take(1), Stream.runCollect) + ) + yield* assertForbidden(rpcClientA["EventLog.WriteSingle"]({ data })) + yield* rpcClientA["EventLog.WriteChunked"](parts[0]) + yield* assertForbidden(rpcClientA["EventLog.WriteChunked"](parts[1])) + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + + it.effect("supports multiple authenticated identities on one connection", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServer.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql) + ) + const encryption = yield* EventLogEncryption.EventLogEncryption + const identityA = yield* encryption.generateIdentity + const identityB = yield* encryption.generateIdentity + const rpcClient = yield* makeAuthenticatedRpcClient(storage, [identityA, identityB]) + const entryA = makeEntry(1) + const entryB = makeEntry(2) + const dataA = yield* encodeWrite(encryption, identityA, entryA) + const dataB = yield* encodeWrite(encryption, identityB, entryB) + + yield* rpcClient["EventLog.WriteSingle"]({ data: dataA }) + yield* rpcClient["EventLog.WriteSingle"]({ data: dataB }) + const changesA = yield* rpcClient["EventLog.Changes"]({ + publicKey: identityA.publicKey, + storeId: storeIdA, + startSequence: 0 + }).pipe(Stream.take(1), Stream.runCollect) + const changesB = yield* rpcClient["EventLog.Changes"]({ + publicKey: identityB.publicKey, + storeId: storeIdA, + startSequence: 0 + }).pipe(Stream.take(1), Stream.runCollect) + + assert.strictEqual(changesA.length, 1) + assert.strictEqual(changesB.length, 1) + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + it.effect("persists remote id across storage instances", () => Effect.gen(function*() { const sql = yield* SqliteClient.make({ filename: ":memory:" }) diff --git a/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts b/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts index 9b9a69ddc..68b36de34 100644 --- a/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts +++ b/.context/effect/packages/sql/sqlite-node/test/SqlEventLogServerUnencrypted.test.ts @@ -2,7 +2,7 @@ import { SqliteClient } from "@effect/sql-sqlite-node" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Redacted } from "effect" import * as SqlEventLogServerUnencryptedStorageTest from "effect-test/unstable/eventlog/SqlEventLogServerUnencryptedStorageTest" -import type * as EventJournal from "effect/unstable/eventlog/EventJournal" +import * as EventJournal from "effect/unstable/eventlog/EventJournal" import * as EventLog from "effect/unstable/eventlog/EventLog" import * as EventLogEncryption from "effect/unstable/eventlog/EventLogEncryption" import * as EventLogMessage from "effect/unstable/eventlog/EventLogMessage" @@ -22,6 +22,15 @@ SqlEventLogServerUnencryptedStorageTest.suite( ) const getIdentityRootSecretMaterial = makeGetIdentityRootSecretMaterial(globalThis.crypto) +const storeId = EventLogMessage.StoreId.make("store-a") + +const makeEntry = (value: number) => + new EventJournal.Entry({ + id: EventJournal.makeEntryIdUnsafe(), + event: "UserCreated", + primaryKey: `user-${value}`, + payload: new Uint8Array([value]) + }, { disableChecks: true }) const makeAuthenticateRequest = Effect.fnUntraced(function*(options: { readonly identity: EventLog.Identity["Service"] @@ -45,6 +54,54 @@ const makeAuthenticateRequest = Effect.fnUntraced(function*(options: { }) describe("SqlEventLogServerUnencrypted (sql-sqlite-node)", () => { + it.effect("forbids writing entries for another identity", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: ":memory:" }) + const storage = yield* SqlEventLogServerUnencrypted.makeStorage().pipe( + Effect.provideService(SqlClient.SqlClient, sql), + Effect.orDie + ) + const rpcClient = yield* RpcTest.makeClient(EventLogMessage.EventLogRemoteRpcs).pipe( + Effect.provide( + EventLogServerUnencrypted.layerRpcHandlers.pipe( + Layer.provideMerge(EventLog.layerRegistry), + Layer.provide(Layer.succeed(EventLogServerUnencrypted.Storage, storage)), + Layer.provide(Layer.succeed(EventLogServerUnencrypted.StoreMapping, { + resolve: ({ storeId }) => Effect.succeed(storeId), + hasStore: () => Effect.succeed(true) + })), + Layer.provide(Layer.succeed(EventLogServerUnencrypted.EventLogServerAuthorization, { + authorizeWrite: () => Effect.void, + authorizeRead: () => Effect.void, + authorizeIdentity: () => Effect.void + })) + ) + ) + ) + const identityA = yield* EventLog.makeIdentity + const identityB = yield* EventLog.makeIdentity + const hello = yield* rpcClient["EventLog.Hello"]() + yield* rpcClient["EventLog.Authenticate"]( + yield* makeAuthenticateRequest({ + identity: identityA, + challenge: hello.challenge, + remoteId: hello.remoteId + }) + ) + const data = yield* new EventLogMessage.WriteEntriesUnencrypted({ + publicKey: identityB.publicKey, + storeId, + entries: [makeEntry(1)] + }).encoded + + const error = yield* rpcClient["EventLog.WriteSingle"]({ data }).pipe(Effect.flip) + + assert.instanceOf(error, EventLogMessage.EventLogProtocolError) + assert.strictEqual(error.code, "Forbidden") + const written = yield* storage.write(storeId, [makeEntry(2)]) + assert.deepStrictEqual(written.map((entry) => entry.remoteSequence), [1]) + }).pipe(Effect.provide([Reactivity.layer, EventLogEncryption.layerSubtle]))) + it.effect("rejects session-auth rebinding for an existing publicKey", () => Effect.gen(function*() { const sql = yield* SqliteClient.make({ filename: ":memory:" }) diff --git a/.context/effect/packages/sql/sqlite-node/tsconfig.json b/.context/effect/packages/sql/sqlite-node/tsconfig.json index 0f059a6bf..0ec4ad2bc 100644 --- a/.context/effect/packages/sql/sqlite-node/tsconfig.json +++ b/.context/effect/packages/sql/sqlite-node/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/sqlite-node/vitest.config.ts b/.context/effect/packages/sql/sqlite-node/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/sqlite-node/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md b/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md index 166b4d999..17b4c0d4f 100644 --- a/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-react-native/CHANGELOG.md @@ -1,5 +1,58 @@ # @effect/sql-sqlite-react-native +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#6992](https://github.com/Effect-TS/effect/pull/6992) [`fd652ce`](https://github.com/Effect-TS/effect/commit/fd652ce2bd9a6ce89dc753061fe4b18ce29d2abf) Thanks @fubhy! - Return selected rows from synchronous and asynchronous value queries. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-react-native/README.md b/.context/effect/packages/sql/sqlite-react-native/README.md index 8a948d766..432b95811 100644 --- a/.context/effect/packages/sql/sqlite-react-native/README.md +++ b/.context/effect/packages/sql/sqlite-react-native/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-sqlite-react-native` +# @effect/sql-sqlite-react-native -An Effect SQL implementation using the `@op-engineering/op-sqlite` library. +An Effect SQL client for SQLite in React Native applications, built on the [`@op-engineering/op-sqlite`](https://op-engineering.github.io/op-sqlite/) library. + +## Installation + +```sh +npm install effect@beta @effect/sql-sqlite-react-native@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-react-native). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-sqlite-react-native) diff --git a/.context/effect/packages/sql/sqlite-react-native/docgen.json b/.context/effect/packages/sql/sqlite-react-native/docgen.json deleted file mode 100644 index 11d3daebb..000000000 --- a/.context/effect/packages/sql/sqlite-react-native/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-react-native/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/sqlite-react-native/package.json b/.context/effect/packages/sql/sqlite-react-native/package.json index 603b43d4a..692930fcd 100644 --- a/.context/effect/packages/sql/sqlite-react-native/package.json +++ b/.context/effect/packages/sql/sqlite-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-react-native", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,16 +58,14 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { - "@op-engineering/op-sqlite": "17.1.2", + "@op-engineering/op-sqlite": "^17.1.2", "effect": "workspace:^" }, "peerDependencies": { - "@op-engineering/op-sqlite": "15.0.4", + "@op-engineering/op-sqlite": ">=17.1.2 <18.0.0", "effect": "workspace:^" } } diff --git a/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts index 9ea80d2a9..f3aacd31d 100644 --- a/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-react-native/src/SqliteClient.ts @@ -51,7 +51,7 @@ export type TypeId = "~@effect/sql-sqlite-react-native/SqliteClient" /** * React Native SQLite client service interface, extending `SqlClient` with its configuration and marking `updateValues` as unsupported for SQLite. * - * @category models + * @category services * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { @@ -93,7 +93,7 @@ export interface SqliteClientConfig { * Use to switch React Native SQLite query execution to the asynchronous driver * API for a scoped effect. * - * @category fiber refs + * @category services * @since 4.0.0 */ export const AsyncQuery = Context.Reference( @@ -104,7 +104,7 @@ export const AsyncQuery = Context.Reference( /** * Runs an effect with `AsyncQuery` enabled, causing React Native SQLite queries in that effect to use the asynchronous driver API. * - * @category fiber refs + * @category providing services * @since 4.0.0 */ export const withAsyncQuery = (effect: Effect.Effect) => @@ -143,8 +143,7 @@ export const make = ( const run = ( sql: string, - params: ReadonlyArray = [], - values = false + params: ReadonlyArray = [] ) => Effect.withFiber, SqlError>((fiber) => { if (fiber.getRef(AsyncQuery)) { @@ -154,14 +153,29 @@ export const make = ( catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement (async)", "execute") }) }), - (result) => values ? result.rawRows ?? [] : result.rows + (result) => result.rows ) } return Effect.try({ - try: () => { - const result = db.executeSync(sql, params as Array) - return values ? result.rawRows ?? [] : result.rows - }, + try: () => db.executeSync(sql, params as Array).rows, + catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) + }) + }) + + const runValues = ( + sql: string, + params: ReadonlyArray = [] + ) => + Effect.withFiber, SqlError>((fiber) => { + if (fiber.getRef(AsyncQuery)) { + return Effect.tryPromise({ + try: () => db.executeRaw(sql, params as Array), + catch: (cause) => + new SqlError({ reason: classifyError(cause, "Failed to execute statement (async)", "execute") }) + }) + } + return Effect.try({ + try: () => db.executeRawSync(sql, params as Array), catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") }) }) }) @@ -176,10 +190,10 @@ export const make = ( return run(sql, params) }, executeValues(sql, params) { - return run(sql, params, true) + return runValues(sql, params) }, executeValuesUnprepared(sql, params) { - return run(sql, params, true) + return runValues(sql, params) }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) diff --git a/.context/effect/packages/sql/sqlite-react-native/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-react-native/src/SqliteMigrator.ts index 23c4dcadc..2df737653 100644 --- a/.context/effect/packages/sql/sqlite-react-native/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-react-native/src/SqliteMigrator.ts @@ -33,7 +33,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations for a React Native SQLite database using the shared `Migrator` implementation and the current `SqlClient`. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -47,7 +47,7 @@ export const run: ( /** * Creates a layer that runs the configured React Native SQLite migrations during layer construction and provides no services. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/sqlite-react-native/test/Client.test.ts b/.context/effect/packages/sql/sqlite-react-native/test/Client.test.ts index fc3285a36..82db4961f 100644 --- a/.context/effect/packages/sql/sqlite-react-native/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-react-native/test/Client.test.ts @@ -1,6 +1,42 @@ -import { describe, it } from "@effect/vitest" +import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" +import { Reactivity } from "effect/unstable/reactivity" +import { vi } from "vitest" + +const state = vi.hoisted(() => ({ + database: { + close() {}, + execute: async () => ({ rowsAffected: 0, rows: [{ value: 1 }] }), + executeRaw: async () => [[1]], + executeRawSync: () => [[1]], + executeSync: () => ({ rowsAffected: 0, rows: [{ value: 1 }] }) + } +})) + +vi.mock("@op-engineering/op-sqlite", () => ({ + open: () => state.database +})) + +import { SqliteClient } from "@effect/sql-sqlite-react-native" describe("Client", () => { it.effect("should work", () => Effect.void) + + it.effect("returns array rows from synchronous values queries", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: "test.db" }) + const rows = yield* sql`SELECT 1 AS value`.values + const unpreparedRows = yield* sql`SELECT 1 AS value`.valuesUnprepared + assert.deepStrictEqual(rows, [[1]]) + assert.deepStrictEqual(unpreparedRows, [[1]]) + }).pipe(Effect.provide(Reactivity.layer))) + + it.effect("returns array rows from asynchronous values queries", () => + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename: "test.db" }) + const rows = yield* SqliteClient.withAsyncQuery(sql`SELECT 1 AS value`.values) + const unpreparedRows = yield* SqliteClient.withAsyncQuery(sql`SELECT 1 AS value`.valuesUnprepared) + assert.deepStrictEqual(rows, [[1]]) + assert.deepStrictEqual(unpreparedRows, [[1]]) + }).pipe(Effect.provide(Reactivity.layer))) }) diff --git a/.context/effect/packages/sql/sqlite-react-native/tsconfig.json b/.context/effect/packages/sql/sqlite-react-native/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/sqlite-react-native/tsconfig.json +++ b/.context/effect/packages/sql/sqlite-react-native/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/sqlite-react-native/vitest.config.ts b/.context/effect/packages/sql/sqlite-react-native/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/sqlite-react-native/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md b/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md index 74de9b663..d53188bb4 100644 --- a/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md +++ b/.context/effect/packages/sql/sqlite-wasm/CHANGELOG.md @@ -1,5 +1,62 @@ # @effect/sql-sqlite-wasm +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- [#6994](https://github.com/Effect-TS/effect/pull/6994) [`3dd1ddc`](https://github.com/Effect-TS/effect/commit/3dd1ddce893aa04abf6b13f31b43c5fc2d3ae7cd) Thanks @fubhy! - Settle pending SQLite WASM requests before replacing failed workers. +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6923](https://github.com/Effect-TS/effect/pull/6923) [`34ced69`](https://github.com/Effect-TS/effect/commit/34ced6990ed7cc3bbd4d9fc7a5517608cb18b5ef) Thanks @fubhy! - Close OPFS access handles when the SQLite worker shuts down. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/sql/sqlite-wasm/README.md b/.context/effect/packages/sql/sqlite-wasm/README.md index fc2073bfc..d341e1b91 100644 --- a/.context/effect/packages/sql/sqlite-wasm/README.md +++ b/.context/effect/packages/sql/sqlite-wasm/README.md @@ -1,7 +1,14 @@ -# `@effect/sql-sqlite-wasm` +# @effect/sql-sqlite-wasm -An Effect SQL implementation using the `@sqlite.org/sqlite-wasm` library. +An Effect SQL client for SQLite compiled to WebAssembly, built on the [`@effect/wa-sqlite`](https://www.npmjs.com/package/@effect/wa-sqlite) library. Works in the browser and other WASM-capable environments. + +## Installation + +```sh +npm install effect@beta @effect/sql-sqlite-wasm@beta +``` ## Documentation -- **API Reference**: [View the full documentation](https://effect-ts.github.io/effect/docs/sql-sqlite-wasm). +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/sql-sqlite-wasm) diff --git a/.context/effect/packages/sql/sqlite-wasm/docgen.json b/.context/effect/packages/sql/sqlite-wasm/docgen.json deleted file mode 100644 index fcbd29618..000000000 --- a/.context/effect/packages/sql/sqlite-wasm/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/sql/sqlite-wasm/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/sql/sqlite-wasm/package.json b/.context/effect/packages/sql/sqlite-wasm/package.json index 9ab83b78c..27c400bd5 100644 --- a/.context/effect/packages/sql/sqlite-wasm/package.json +++ b/.context/effect/packages/sql/sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@effect/sql-sqlite-wasm", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A SQLite toolkit for Effect", @@ -29,6 +29,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -36,7 +37,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -46,6 +50,7 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, @@ -53,16 +58,14 @@ "codegen": "effect-utils codegen", "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "devDependencies": { "@effect/wa-sqlite": "^0.2.1", "effect": "workspace:^" }, "peerDependencies": { - "@effect/wa-sqlite": "^0.1.2", + "@effect/wa-sqlite": ">=0.1.2 <0.2.0", "effect": "workspace:^" } } diff --git a/.context/effect/packages/sql/sqlite-wasm/src/OpfsWorker.ts b/.context/effect/packages/sql/sqlite-wasm/src/OpfsWorker.ts index 59fc97afe..0e078ab6b 100644 --- a/.context/effect/packages/sql/sqlite-wasm/src/OpfsWorker.ts +++ b/.context/effect/packages/sql/sqlite-wasm/src/OpfsWorker.ts @@ -36,7 +36,7 @@ export interface OpfsWorkerConfig { /** * Runs the SQLite OPFS worker loop, opening the configured database, posting a ready message, handling query/import/export/update-hook messages, and closing when a close message is received. * - * @category constructors + * @category running * @since 4.0.0 */ export const run = ( @@ -45,7 +45,10 @@ export const run = ( Effect.gen(function*() { const factory = yield* Effect.promise(() => SQLiteESMFactory()) const sqlite3 = WaSqlite.Factory(factory) - const vfs = yield* Effect.promise(() => AccessHandlePoolVFS.create("opfs", factory)) + const vfs = yield* Effect.acquireRelease( + Effect.promise(() => AccessHandlePoolVFS.create("opfs", factory)), + (vfs) => Effect.promise(() => vfs.close()) + ) sqlite3.vfs_register(vfs, false) const db = yield* Effect.acquireRelease( Effect.try({ diff --git a/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts b/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts index d68f540b8..55fa88d06 100644 --- a/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts +++ b/.context/effect/packages/sql/sqlite-wasm/src/SqliteClient.ts @@ -24,6 +24,7 @@ import * as Exit from "effect/Exit" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" import * as Layer from "effect/Layer" +import * as Rec from "effect/Record" import * as Scope from "effect/Scope" import * as ScopedRef from "effect/ScopedRef" import * as Semaphore from "effect/Semaphore" @@ -59,7 +60,7 @@ export type TypeId = "~@effect/sql-sqlite-wasm/SqliteClient" /** * SQLite WASM client service interface, extending `SqlClient` with database `export` and `import` operations and marking `updateValues` as unsupported for SQLite. * - * @category models + * @category services * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { @@ -183,7 +184,7 @@ export const makeMemory = ( if (rowMode === "object") { const obj: Record = {} for (let i = 0; i < columns.length; i++) { - obj[columns[i]] = row[i] + Rec.assignProperty(obj, columns[i], row[i]) } results.push(obj) } else { @@ -224,7 +225,7 @@ export const makeMemory = ( const row = sqlite3.row(stmt) const obj: Record = {} for (let i = 0; i < columns.length; i++) { - obj[columns[i]] = row[i] + Rec.assignProperty(obj, columns[i], row[i]) } yield obj } @@ -305,10 +306,9 @@ export const make = ( const transformRows = options.transformResultNames ? Statement.defaultTransforms(options.transformResultNames).array : undefined - const pending = new Map) => void>() - const makeConnection = Effect.gen(function*() { let currentId = 0 + const pending = new Map) => void>() const scope = yield* Effect.scope const readyDeferred = yield* Deferred.make() @@ -344,7 +344,15 @@ export const make = ( } port.addEventListener("message", onMessage) - function onError() { + function onError(cause: Event) { + const exit = Exit.fail( + new SqlError({ reason: classifyError(cause, "SQLite WASM worker failed", "worker") }) + ) + const requests = Array.from(pending.values()) + pending.clear() + for (const resume of requests) { + resume(exit) + } Effect.runFork(ScopedRef.set(connectionRef, makeConnection)) } if ("onerror" in worker) { @@ -456,7 +464,7 @@ export const make = ( function rowToObject(columns: Array, row: Array) { const obj: Record = {} for (let i = 0; i < columns.length; i++) { - obj[columns[i]] = row[i] + Rec.assignProperty(obj, columns[i], row[i]) } return obj } @@ -466,7 +474,7 @@ const extractRows = (rows: [Array, Array]) => rows[1] /** * Fiber reference that stores transferables to include with worker-backed SQLite WASM query messages. * - * @category transferables + * @category services * @since 4.0.0 */ export const Transferables = Context.Reference>( diff --git a/.context/effect/packages/sql/sqlite-wasm/src/SqliteMigrator.ts b/.context/effect/packages/sql/sqlite-wasm/src/SqliteMigrator.ts index 5074d5a92..361c662b7 100644 --- a/.context/effect/packages/sql/sqlite-wasm/src/SqliteMigrator.ts +++ b/.context/effect/packages/sql/sqlite-wasm/src/SqliteMigrator.ts @@ -33,7 +33,7 @@ export * from "effect/unstable/sql/Migrator" /** * Runs SQL migrations for a SQLite WASM database using the shared `Migrator` implementation and the current `SqlClient`. * - * @category constructors + * @category running * @since 4.0.0 */ export const run: ( @@ -47,7 +47,7 @@ export const run: ( /** * Creates a layer that runs the configured SQLite WASM migrations during layer construction and provides no services. * - * @category constructors + * @category layers * @since 4.0.0 */ export const layer = ( diff --git a/.context/effect/packages/sql/sqlite-wasm/test/Client.test.ts b/.context/effect/packages/sql/sqlite-wasm/test/Client.test.ts index fc3285a36..d126c9748 100644 --- a/.context/effect/packages/sql/sqlite-wasm/test/Client.test.ts +++ b/.context/effect/packages/sql/sqlite-wasm/test/Client.test.ts @@ -1,6 +1,89 @@ -import { describe, it } from "@effect/vitest" -import { Effect } from "effect" +import { assert, describe, it } from "@effect/vitest" +import { Deferred, Effect, Exit, Fiber, Option } from "effect" +import { TestClock } from "effect/testing" +import { Reactivity } from "effect/unstable/reactivity" +import { vi } from "vitest" + +const state = vi.hoisted(() => ({ vfsCloseCalls: 0 })) + +vi.mock("@effect/wa-sqlite/dist/wa-sqlite.mjs", () => ({ default: async () => ({}) })) +vi.mock("@effect/wa-sqlite/src/examples/AccessHandlePoolVFS.js", () => ({ + AccessHandlePoolVFS: { + create: async () => ({ + close: async () => { + state.vfsCloseCalls++ + } + }) + } +})) +vi.mock("@effect/wa-sqlite", () => ({ + Factory: () => ({ + close() {}, + open_v2: () => 1, + vfs_register() {} + }) +})) + +import { OpfsWorker, SqliteClient } from "@effect/sql-sqlite-wasm" + +class FakePort extends EventTarget { + close() {} + + postMessage(message: ReadonlyArray): void { + if (message[0] === "ready") { + queueMicrotask(() => this.dispatchEvent(new MessageEvent("message", { data: ["close"] }))) + } + } +} + +class FakeWorker extends EventTarget { + onerror: unknown = null + + constructor(readonly queryPosted: Deferred.Deferred) { + super() + } + + override addEventListener( + type: string, + listener: EventListenerOrEventListenerObject | null, + options?: unknown + ): void { + super.addEventListener(type, listener, options as boolean) + if (type === "message") { + queueMicrotask(() => this.dispatchEvent(new MessageEvent("message", { data: ["ready"] }))) + } + } + + postMessage(message: ReadonlyArray): void { + if (typeof message[0] === "number") { + Effect.runFork(Deferred.succeed(this.queryPosted, undefined)) + } + } +} describe("Client", () => { it.effect("should work", () => Effect.void) + + it.effect("closes the OPFS VFS when the worker loop closes", () => + Effect.gen(function*() { + yield* OpfsWorker.run({ port: new FakePort(), dbName: "test.db" }) + assert.strictEqual(state.vfsCloseCalls, 1) + })) + + it.effect("settles an in-flight query when the worker errors and reconnects", () => + Effect.gen(function*() { + const queryPosted = yield* Deferred.make() + const worker = new FakeWorker(queryPosted) + const sql = yield* SqliteClient.make({ worker: Effect.succeed(worker as unknown as Worker) }) + const fiber = yield* Effect.forkChild(sql`SELECT 1`) + + yield* Deferred.await(queryPosted) + worker.dispatchEvent(new Event("error")) + + const joinFiber = yield* Fiber.join(fiber).pipe(Effect.exit, Effect.timeoutOption("100 millis"), Effect.forkChild) + yield* TestClock.adjust("100 millis") + const result = yield* Fiber.join(joinFiber) + assert(Option.isSome(result), "the request remained pending after worker replacement") + assert(Exit.isFailure(result.value)) + }).pipe(Effect.provide(Reactivity.layer))) }) diff --git a/.context/effect/packages/sql/sqlite-wasm/tsconfig.json b/.context/effect/packages/sql/sqlite-wasm/tsconfig.json index 19a2f5dbc..e2a8ca19a 100644 --- a/.context/effect/packages/sql/sqlite-wasm/tsconfig.json +++ b/.context/effect/packages/sql/sqlite-wasm/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/sql/sqlite-wasm/vitest.config.ts b/.context/effect/packages/sql/sqlite-wasm/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/sql/sqlite-wasm/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/ai-codegen/docgen.json b/.context/effect/packages/tools/ai-codegen/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/ai-codegen/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/ai-codegen/package.json b/.context/effect/packages/tools/ai-codegen/package.json index 97ed250b0..689fd72e9 100644 --- a/.context/effect/packages/tools/ai-codegen/package.json +++ b/.context/effect/packages/tools/ai-codegen/package.json @@ -43,9 +43,7 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "dependencies": { "@effect/openapi-generator": "workspace:^", @@ -55,6 +53,6 @@ "yaml": "^2.9.0" }, "devDependencies": { - "@types/node": "^26.1.1" + "@types/node": "^26.1.2" } } diff --git a/.context/effect/packages/tools/ai-codegen/src/Config.ts b/.context/effect/packages/tools/ai-codegen/src/Config.ts index 5cb417aa9..166187550 100644 --- a/.context/effect/packages/tools/ai-codegen/src/Config.ts +++ b/.context/effect/packages/tools/ai-codegen/src/Config.ts @@ -34,7 +34,7 @@ export const SpecSourceConfig = Schema.Struct({ * * **Example** (Decoding a codegen configuration) * - * ```ts + * ```ts import.meta.vitest * import * as Config from "@effect/ai-codegen/Config" * import { Schema } from "effect" * @@ -44,8 +44,7 @@ export const SpecSourceConfig = Schema.Struct({ * name: "MyClient" * }) * - * console.log(config.spec) - * // "https://example.com/openapi.json" + * config.spec // => "https://example.com/openapi.json" * ``` * * @category models @@ -179,7 +178,7 @@ export declare namespace SpecSource { * * **Example** (Creating spec sources) * - * ```ts + * ```ts import.meta.vitest * import * as Config from "@effect/ai-codegen/Config" * * // Create a URL-based source @@ -187,6 +186,9 @@ export declare namespace SpecSource { * * // Create a file-based source * const fileSource = Config.SpecSource.File("/path/to/spec.json") + * + * urlSource._tag // => "Url" + * fileSource._tag // => "File" * ``` * * @category constructors @@ -252,13 +254,16 @@ export const SpecSource = { * * **Example** (Creating a config parse error) * - * ```ts + * ```ts import.meta.vitest * import * as Config from "@effect/ai-codegen/Config" * * const error = new Config.ConfigParseError({ * path: "/path/to/codegen.json", * cause: new Error("Invalid JSON") * }) + * + * error._tag // => "ConfigParseError" + * error.path // => "/path/to/codegen.json" * ``` * * @category errors @@ -274,13 +279,16 @@ export class ConfigParseError extends Data.TaggedError("ConfigParseError")<{ * * **Example** (Creating a config not found error) * - * ```ts + * ```ts import.meta.vitest * import * as Config from "@effect/ai-codegen/Config" * * const error = new Config.ConfigNotFoundError({ * provider: "openai", * expectedPath: "/path/to/packages/ai/openai/codegen.json" * }) + * + * error._tag // => "ConfigNotFoundError" + * error.provider // => "openai" * ``` * * @category errors diff --git a/.context/effect/packages/tools/ai-codegen/src/Discovery.ts b/.context/effect/packages/tools/ai-codegen/src/Discovery.ts index 74270e536..09c19365c 100644 --- a/.context/effect/packages/tools/ai-codegen/src/Discovery.ts +++ b/.context/effect/packages/tools/ai-codegen/src/Discovery.ts @@ -19,13 +19,23 @@ import * as Glob from "./Glob.ts" * * **Example** (Inspecting a discovered provider) * - * ```ts + * ```ts import.meta.vitest + * import * as Config from "@effect/ai-codegen/Config" * import type * as Discovery from "@effect/ai-codegen/Discovery" * - * declare const provider: Discovery.DiscoveredProvider - * - * console.log(provider.name) // "openai" - * console.log(provider.specSource._tag) // "Url" | "File" + * const provider: Discovery.DiscoveredProvider = { + * name: "openai", + * packagePath: "packages/ai/openai", + * config: new Config.CodegenConfig({ + * spec: "https://example.com/openapi.json", + * output: "Generated.ts" + * }), + * specSource: Config.SpecSource.Url("https://example.com/openapi.json"), + * outputPath: "packages/ai/openai/src/Generated.ts" + * } + * + * provider.name // => "openai" + * provider.specSource._tag // => "Url" * ``` * * @category models @@ -42,7 +52,7 @@ export interface DiscoveredProvider { /** * Service for discovering AI provider configurations. * - * @category models + * @category services * @since 4.0.0 */ export interface ProviderDiscovery { @@ -73,13 +83,16 @@ export const ProviderDiscovery: Context.Service "DiscoveryError" + * error.message // => "Failed to parse config" * ``` * * @category errors @@ -95,13 +108,16 @@ export class DiscoveryError extends Data.TaggedError("DiscoveryError")<{ * * **Example** (Creating a provider not found error) * - * ```ts + * ```ts import.meta.vitest * import * as Discovery from "@effect/ai-codegen/Discovery" * * const error = new Discovery.ProviderNotFoundError({ * provider: "openai", * available: ["anthropic", "google"] * }) + * + * error.provider // => "openai" + * error.available // => ["anthropic", "google"] * ``` * * @category errors diff --git a/.context/effect/packages/tools/ai-codegen/src/Generator.ts b/.context/effect/packages/tools/ai-codegen/src/Generator.ts index 1bdd898aa..ed705d2e8 100644 --- a/.context/effect/packages/tools/ai-codegen/src/Generator.ts +++ b/.context/effect/packages/tools/ai-codegen/src/Generator.ts @@ -21,13 +21,16 @@ import type { DiscoveredProvider } from "./Discovery.ts" * * **Example** (Creating a generation error) * - * ```ts + * ```ts import.meta.vitest * import * as Generator from "@effect/ai-codegen/Generator" * * const error = new Generator.GenerationError({ * provider: "openai", * cause: new Error("Invalid spec") * }) + * + * error._tag // => "GenerationError" + * error.provider // => "openai" * ``` * * @category errors @@ -43,13 +46,16 @@ export class GenerationError extends Data.TaggedError("GenerationError")<{ * * **Example** (Creating a patch error) * - * ```ts + * ```ts import.meta.vitest * import * as Generator from "@effect/ai-codegen/Generator" * * const error = new Generator.PatchError({ * provider: "openai", * cause: new Error("Invalid patch") * }) + * + * error._tag // => "PatchError" + * error.provider // => "openai" * ``` * * @category errors @@ -63,7 +69,7 @@ export class PatchError extends Data.TaggedError("PatchError")<{ /** * Service for generating Effect code from OpenAPI specs. * - * @category models + * @category services * @since 4.0.0 */ export interface CodeGenerator { diff --git a/.context/effect/packages/tools/ai-codegen/src/Glob.ts b/.context/effect/packages/tools/ai-codegen/src/Glob.ts index 08d0e4a67..822d075c8 100644 --- a/.context/effect/packages/tools/ai-codegen/src/Glob.ts +++ b/.context/effect/packages/tools/ai-codegen/src/Glob.ts @@ -23,7 +23,7 @@ export class GlobError extends Data.TaggedError("GlobError")<{ /** * Service for glob pattern matching. * - * @category models + * @category services * @since 4.0.0 */ export interface Glob { diff --git a/.context/effect/packages/tools/ai-codegen/src/PostProcess.ts b/.context/effect/packages/tools/ai-codegen/src/PostProcess.ts index 9ddd4524b..f88792013 100644 --- a/.context/effect/packages/tools/ai-codegen/src/PostProcess.ts +++ b/.context/effect/packages/tools/ai-codegen/src/PostProcess.ts @@ -16,7 +16,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne * * **Example** (Creating a post-process error) * - * ```ts + * ```ts import.meta.vitest * import * as PostProcess from "@effect/ai-codegen/PostProcess" * * const error = new PostProcess.PostProcessError({ @@ -28,6 +28,9 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne * stderr: "error: some lint error", * cause: new Error("Lint failed") * }) + * + * error.step // => "lint" + * error.exitCode // => 1 * ``` * * @category errors @@ -63,7 +66,7 @@ export class PostProcessError extends Data.TaggedError("PostProcessError")<{ /** * Service for post-processing generated code. * - * @category models + * @category services * @since 4.0.0 */ export interface PostProcessor { diff --git a/.context/effect/packages/tools/ai-codegen/src/SpecFetcher.ts b/.context/effect/packages/tools/ai-codegen/src/SpecFetcher.ts index 0cc808970..a7207ed76 100644 --- a/.context/effect/packages/tools/ai-codegen/src/SpecFetcher.ts +++ b/.context/effect/packages/tools/ai-codegen/src/SpecFetcher.ts @@ -18,7 +18,7 @@ import type { SpecSource } from "./Config.ts" * * **Example** (Creating a spec fetch error) * - * ```ts + * ```ts import.meta.vitest * import * as SpecFetcher from "@effect/ai-codegen/SpecFetcher" * * const error = new SpecFetcher.SpecFetchError({ @@ -26,6 +26,9 @@ import type { SpecSource } from "./Config.ts" * source: "https://example.com/openapi.json", * cause: new Error("Network error") * }) + * + * error._tag // => "SpecFetchError" + * error.provider // => "openai" * ``` * * @category errors @@ -40,7 +43,7 @@ export class SpecFetchError extends Data.TaggedError("SpecFetchError")<{ /** * Service for fetching OpenAPI specifications. * - * @category models + * @category services * @since 4.0.0 */ export interface SpecFetcher { diff --git a/.context/effect/packages/tools/ai-codegen/src/main.ts b/.context/effect/packages/tools/ai-codegen/src/main.ts index 714578c9d..9157e7629 100644 --- a/.context/effect/packages/tools/ai-codegen/src/main.ts +++ b/.context/effect/packages/tools/ai-codegen/src/main.ts @@ -229,7 +229,7 @@ const ServicesLayer = Layer.mergeAll( /** * Run the CLI. * - * @category execution + * @category running * @since 4.0.0 */ export const run = Command.run(root, { version: "0.0.0" }).pipe( diff --git a/.context/effect/packages/tools/ai-codegen/tsconfig.json b/.context/effect/packages/tools/ai-codegen/tsconfig.json index 6a3a16461..cdb818391 100644 --- a/.context/effect/packages/tools/ai-codegen/tsconfig.json +++ b/.context/effect/packages/tools/ai-codegen/tsconfig.json @@ -1,10 +1,10 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" }, + { "path": "../../platform/node" }, { "path": "../openapi-generator" } ], "compilerOptions": { diff --git a/.context/effect/packages/tools/ai-docgen/docgen.json b/.context/effect/packages/tools/ai-docgen/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/ai-docgen/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/ai-docgen/package.json b/.context/effect/packages/tools/ai-docgen/package.json index 8edfae410..c3c544adf 100644 --- a/.context/effect/packages/tools/ai-docgen/package.json +++ b/.context/effect/packages/tools/ai-docgen/package.json @@ -43,9 +43,7 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "dependencies": { "@effect/platform-node": "workspace:^", @@ -54,6 +52,6 @@ "yaml": "^2.9.0" }, "devDependencies": { - "@types/node": "^26.1.1" + "@types/node": "^26.1.2" } } diff --git a/.context/effect/packages/tools/ai-docgen/tsconfig.json b/.context/effect/packages/tools/ai-docgen/tsconfig.json index aa14dc8ca..ce21b4357 100644 --- a/.context/effect/packages/tools/ai-docgen/tsconfig.json +++ b/.context/effect/packages/tools/ai-docgen/tsconfig.json @@ -1,10 +1,10 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ], "compilerOptions": { "outDir": "dist", diff --git a/.context/effect/packages/tools/api-diff/README.md b/.context/effect/packages/tools/api-diff/README.md new file mode 100644 index 000000000..b4841ec8c --- /dev/null +++ b/.context/effect/packages/tools/api-diff/README.md @@ -0,0 +1,51 @@ +# API Diff + +`@effect/api-diff` compares the consumer-visible TypeScript declarations emitted +by two repository revisions. Its JSON output is canonical; the Markdown report +is intended for migration review and does not make semantic-version +compatibility claims. + +Run a complete comparison from the repository root: + +```sh +pnpm api-diff \ + --base-ref v3 \ + --head-ref origin/main \ + --output tmp/api-diff/run +``` + +Both refs are required and are resolved to commit SHAs before work starts. The +tool builds detached disposable worktrees with each branch's native build, +discovers every public package entrypoint independently, extracts both snapshots +with one pinned TypeScript compiler API, and caches successful snapshots by +commit and compiler. + +The command writes: + +- `base.snapshot.json` +- `head.snapshot.json` +- `diff.json` +- `report.md` + +Generate the agent-facing v3-to-v4 migration reference from a fresh diff and +the YAML files in `migration/annotations`: + +```sh +pnpm api-diff --write-doc migration/v3-to-v4.md +``` + +The document command defaults to refs `v3` and `main`, records their resolved +SHAs, preserves the existing import map sections, and replaces the API +reference in place. It does not write `diff.json` unless `--output` is also +provided. + +List missing annotations, grouped by v3 module, and exit non-zero when any are +missing: + +```sh +pnpm api-diff --check +``` + +Unmatched APIs are reported as removals from the base and additions in the +head. Likely replacements are reported separately for review, including +cross-module moves and the paired type/value facets of class-style APIs. diff --git a/.context/effect/packages/tools/api-diff/package.json b/.context/effect/packages/tools/api-diff/package.json new file mode 100644 index 000000000..3062e861c --- /dev/null +++ b/.context/effect/packages/tools/api-diff/package.json @@ -0,0 +1,30 @@ +{ + "name": "@effect/api-diff", + "version": "0.0.0", + "private": true, + "homepage": "https://effect.website", + "type": "module", + "exports": { + ".": "./src/Cli.ts", + "./*": "./src/*.ts", + "./bin": null + }, + "bin": { + "effect-api-diff": "./src/bin.ts" + }, + "scripts": { + "check": "tsc -b tsconfig.json" + }, + "dependencies": { + "@effect/platform-node": "workspace:^", + "effect": "workspace:^", + "yaml": "^2.9.0" + }, + "devDependencies": { + "@effect/vitest": "workspace:^", + "@types/node": "^26.1.2", + "typescript": "^6.0.3", + "typescript-compiler": "npm:typescript@6.0.3", + "vitest": "4.1.10" + } +} diff --git a/.context/effect/packages/tools/api-diff/src/Annotations.ts b/.context/effect/packages/tools/api-diff/src/Annotations.ts new file mode 100644 index 000000000..86bcbc096 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Annotations.ts @@ -0,0 +1,72 @@ +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" +import * as Yaml from "yaml" +import { ApiDiffError, isApiDiffError } from "./Error.ts" + +export interface MigrationAnnotation { + readonly replacement: string + readonly note: string + readonly example?: string | undefined +} + +const compareStrings = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0 + +const parseAnnotation = (id: string, value: unknown, file: string): MigrationAnnotation => { + if ( + typeof value !== "object" || value === null || + typeof Reflect.get(value, "replacement") !== "string" || + typeof Reflect.get(value, "note") !== "string" || + (Reflect.get(value, "example") !== undefined && typeof Reflect.get(value, "example") !== "string") + ) { + throw new Error(`Invalid annotation for ${id} in ${file}`) + } + return { + replacement: Reflect.get(value, "replacement"), + note: Reflect.get(value, "note"), + ...(Reflect.get(value, "example") === undefined ? {} : { example: Reflect.get(value, "example") }) + } +} + +const loadAnnotationsInternal = Effect.fnUntraced(function*(directory: string) { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + if (!(yield* fs.exists(directory))) { + return new Map() + } + + const annotations = new Map() + const files = (yield* fs.readDirectory(directory)) + .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml")) + .sort(compareStrings) + for (const file of files) { + const source = yield* fs.readFileString(path.join(directory, file)) + const document = yield* Effect.try({ + try: () => Yaml.parse(source) as unknown, + catch: (cause) => new ApiDiffError({ message: `Could not parse annotation file ${file}`, cause }) + }) + if (typeof document !== "object" || document === null || Array.isArray(document)) { + return yield* new ApiDiffError({ message: `Annotation file ${file} must contain an object` }) + } + for (const [id, value] of Object.entries(document).sort(([left], [right]) => compareStrings(left, right))) { + if (annotations.has(id)) { + return yield* new ApiDiffError({ message: `Duplicate annotation id ${id}` }) + } + const annotation = yield* Effect.try({ + try: () => parseAnnotation(id, value, file), + catch: (cause) => new ApiDiffError({ message: String(cause), cause }) + }) + annotations.set(id, annotation) + } + } + return annotations +}) + +export const loadAnnotations = (directory: string) => + loadAnnotationsInternal(directory).pipe( + Effect.mapError((cause) => + isApiDiffError(cause) + ? cause + : new ApiDiffError({ message: `Could not load annotations from ${directory}`, cause }) + ) + ) diff --git a/.context/effect/packages/tools/api-diff/src/ApiDiff.ts b/.context/effect/packages/tools/api-diff/src/ApiDiff.ts new file mode 100644 index 000000000..3a05ebb30 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/ApiDiff.ts @@ -0,0 +1,170 @@ +import * as Console from "effect/Console" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import { loadAnnotations } from "./Annotations.ts" +import { diffSnapshots } from "./Diff.ts" +import { ApiDiffError, isApiDiffError } from "./Error.ts" +import { prettyJson } from "./Json.ts" +import { + extractImportMapSections, + markdownSafetyIssues, + renderMigrationDocument, + renderMissingAnnotations, + unannotatedApiIds, + unannotatedModuleIds +} from "./MigrationDoc.ts" +import { renderMarkdownReport } from "./Report.ts" +import { Worktrees } from "./Worktrees.ts" + +export interface ApiDiffOptions { + readonly baseRef: string + readonly headRef: string + readonly output?: string | undefined + readonly writeDoc?: string | undefined + readonly check: boolean +} + +export class ApiDiff extends Context.Service Effect.Effect +}>()("@effect/api-diff/ApiDiff") { + static readonly layerNoDependencies = Layer.effect( + ApiDiff, + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const worktrees = yield* Worktrees + + const absolute = (repoRoot: string, location: string): string => + path.isAbsolute(location) ? location : path.resolve(repoRoot, location) + + const findRepoRoot = Effect.fnUntraced(function*() { + let current = path.resolve() + while (true) { + if ( + (yield* fs.exists(path.join(current, ".git"))) || + (yield* fs.exists(path.join(current, "pnpm-workspace.yaml"))) + ) { + return current + } + const parent = path.dirname(current) + if (parent === current) { + return yield* new ApiDiffError({ + message: `Could not locate repository root from ${path.resolve()}` + }) + } + current = parent + } + }) + + const runInternal = Effect.fnUntraced(function*(options: ApiDiffOptions) { + const repoRoot = yield* findRepoRoot() + if (options.output === undefined && options.writeDoc === undefined && !options.check) { + return yield* new ApiDiffError({ message: "Specify --output, --write-doc, or --check" }) + } + + const baseSha = yield* worktrees.resolveRef(repoRoot, options.baseRef) + const headSha = yield* (options.headRef === "main" + ? worktrees.resolveRef(repoRoot, "origin/main").pipe( + Effect.catch(() => worktrees.resolveRef(repoRoot, options.headRef)) + ) + : worktrees.resolveRef(repoRoot, options.headRef)) + const toolRoot = path.join(repoRoot, "tmp", "api-diff") + const cacheRoot = path.join(toolRoot, "cache") + const worktreesRoot = path.join(toolRoot, "worktrees") + yield* Console.log( + `Base ${options.baseRef}: ${baseSha}\nHead ${options.headRef}: ${headSha}` + ) + const base = yield* worktrees.prepareSnapshot({ + repoRoot, + cacheRoot, + worktreesRoot, + name: "base", + ref: options.baseRef, + sha: baseSha + }) + const head = yield* worktrees.prepareSnapshot({ + repoRoot, + cacheRoot, + worktreesRoot, + name: "head", + ref: options.headRef, + sha: headSha + }) + const diff = diffSnapshots(base, head) + + if (options.output !== undefined) { + const output = absolute(repoRoot, options.output) + yield* fs.makeDirectory(output, { recursive: true }) + yield* fs.writeFileString(path.join(output, "base.snapshot.json"), prettyJson(base)) + yield* fs.writeFileString(path.join(output, "head.snapshot.json"), prettyJson(head)) + yield* fs.writeFileString(path.join(output, "diff.json"), prettyJson(diff)) + yield* fs.writeFileString(path.join(output, "report.md"), renderMarkdownReport(diff)) + yield* Console.log(`Wrote ${path.relative(repoRoot, output)} (${diff.changes.length} changes)`) + } + + if (options.writeDoc !== undefined || options.check) { + const annotations = yield* loadAnnotations(path.join(repoRoot, "migration", "annotations")).pipe( + Effect.provideService(FileSystem.FileSystem, fs), + Effect.provideService(Path.Path, path) + ) + const documentPath = options.writeDoc === undefined + ? path.join(repoRoot, "migration", "v3-to-v4.md") + : absolute(repoRoot, options.writeDoc) + const existing = (yield* fs.exists(documentPath)) ? yield* fs.readFileString(documentPath) : "" + const importMapSections = extractImportMapSections(existing) + let documentForCheck = existing + if (options.writeDoc !== undefined) { + const document = renderMigrationDocument(diff, annotations, importMapSections) + const unsafeMarkdown = markdownSafetyIssues(document) + if (unsafeMarkdown.length > 0) { + return yield* new ApiDiffError({ + message: `Generated migration document has unsafe Markdown:\n${unsafeMarkdown.join("\n")}` + }) + } + documentForCheck = document + yield* fs.makeDirectory(path.dirname(documentPath), { recursive: true }) + yield* fs.writeFileString(documentPath, document) + yield* Console.log(`Wrote ${path.relative(repoRoot, documentPath)} (${diff.changes.length} changes)`) + } + if (options.check) { + const unsafeMarkdown = markdownSafetyIssues(documentForCheck) + if (unsafeMarkdown.length > 0) { + return yield* new ApiDiffError({ + message: `Migration document has unsafe Markdown:\n${unsafeMarkdown.join("\n")}` + }) + } + const missingApis = unannotatedApiIds(diff, annotations, importMapSections) + const missingModules = unannotatedModuleIds(diff, annotations, importMapSections) + yield* Console.log(renderMissingAnnotations(diff, annotations, importMapSections).trimEnd()) + if (missingApis.size > 0 || missingModules.length > 0) { + return yield* new ApiDiffError({ + message: `${missingApis.size} API groups and ${missingModules.length} modules need migration guidance` + }) + } + } + } + }) + + const run = (options: ApiDiffOptions): Effect.Effect => + runInternal(options).pipe( + Effect.mapError((cause) => + isApiDiffError(cause) + ? cause + : new ApiDiffError({ + message: "API diff failed", + cause + }) + ) + ) + + return ApiDiff.of({ run }) + }) + ) + + static readonly layer = this.layerNoDependencies.pipe( + Layer.provide(Worktrees.layer) + ) +} diff --git a/.context/effect/packages/tools/api-diff/src/Cli.ts b/.context/effect/packages/tools/api-diff/src/Cli.ts new file mode 100644 index 000000000..a220035fd --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Cli.ts @@ -0,0 +1,61 @@ +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Command from "effect/unstable/cli/Command" +import * as Flag from "effect/unstable/cli/Flag" +import { ApiDiff, type ApiDiffOptions } from "./ApiDiff.ts" + +const baseRef = Flag.string("base-ref").pipe( + Flag.withDescription("Explicit base Git ref"), + Flag.withDefault("v3") +) + +const headRef = Flag.string("head-ref").pipe( + Flag.withDescription("Explicit head Git ref"), + Flag.withDefault("main") +) + +const output = Flag.string("output").pipe( + Flag.withMetavar("DIRECTORY"), + Flag.withDescription("Report output directory"), + Flag.optional +) + +const writeDoc = Flag.string("write-doc").pipe( + Flag.withMetavar("FILE"), + Flag.withDescription("Write the annotation-driven migration reference"), + Flag.optional +) + +const check = Flag.boolean("check").pipe( + Flag.withDescription("List APIs without migration annotations and fail if any remain") +) + +const runApiDiff = Effect.fnUntraced(function*(options: { + readonly baseRef: string + readonly headRef: string + readonly output: Option.Option + readonly writeDoc: Option.Option + readonly check: boolean +}) { + const apiDiff = yield* ApiDiff + yield* apiDiff.run( + { + baseRef: options.baseRef, + headRef: options.headRef, + output: Option.getOrUndefined(options.output), + writeDoc: Option.getOrUndefined(options.writeDoc), + check: options.check + } satisfies ApiDiffOptions + ) +}) + +export const cli = Command.make("api-diff", { + baseRef, + headRef, + output, + writeDoc, + check +}).pipe( + Command.withDescription("Compare the consumer-visible TypeScript API of two repository revisions"), + Command.withHandler(runApiDiff) +) diff --git a/.context/effect/packages/tools/api-diff/src/Diff.ts b/.context/effect/packages/tools/api-diff/src/Diff.ts new file mode 100644 index 000000000..acabe93d0 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Diff.ts @@ -0,0 +1,504 @@ +import { fingerprint, stableJson } from "./Json.ts" +import type { ApiChange, ApiDiff, ApiEntity, ApiSnapshot, ChangeClassification, DeclarationModel } from "./Model.ts" + +interface Match { + readonly base: ApiEntity + readonly head: ApiEntity + readonly confidence: number + readonly authoritative: boolean + readonly note?: string +} + +interface ApiGroup { + readonly module: string + readonly path: ReadonlyArray + readonly entities: ReadonlyArray +} + +const pathName = (entity: ApiEntity): string => entity.path.at(-1) ?? "" + +const levenshtein = (left: string, right: string): number => { + const row = Array.from({ length: right.length + 1 }, (_, index) => index) + for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) { + let previous = row[0]! + row[0] = leftIndex + for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) { + const above = row[rightIndex]! + const diagonal = previous + previous = above + row[rightIndex] = left[leftIndex - 1] === right[rightIndex - 1] + ? diagonal + : Math.min(diagonal, above, row[rightIndex - 1]!) + 1 + } + } + return row[right.length]! +} + +const nameSimilarity = (left: string, right: string): number => { + const length = Math.max(left.length, right.length) + return length === 0 ? 1 : 1 - levenshtein(left.toLowerCase(), right.toLowerCase()) / length +} + +const entityFeatureCache = new WeakMap>() + +const entityFeatures = (entity: ApiEntity): ReadonlySet => { + const cached = entityFeatureCache.get(entity) + if (cached !== undefined) { + return cached + } + const features = new Set() + for (const declaration of entity.declarations) { + features.add(`declaration:${declaration.kind}`) + for (const member of declaration.members ?? []) { + features.add(`member:${member.kind}:${member.name}`) + } + } + entityFeatureCache.set(entity, features) + return features +} + +const setSimilarity = (left: ReadonlySet, right: ReadonlySet): number => { + if (left.size === 0 && right.size === 0) { + return 1 + } + let shared = 0 + for (const value of left) { + if (right.has(value)) { + shared++ + } + } + return shared / (left.size + right.size - shared) +} + +const documentationTokenCache = new WeakMap>() + +const documentationTokens = (entity: ApiEntity): ReadonlySet => { + const cached = documentationTokenCache.get(entity) + if (cached !== undefined) { + return cached + } + const tokens = new Set( + (entity.documentation.summary ?? "") + .toLowerCase() + .match(/[a-z][a-z0-9]+/g) + ?.filter((token) => token.length >= 4) ?? [] + ) + documentationTokenCache.set(entity, tokens) + return tokens +} + +const groupEntities = (entities: Iterable): ReadonlyArray => { + const groups = new Map>() + for (const entity of entities) { + const key = `${entity.module}#${entity.path.join(".")}` + const group = groups.get(key) + if (group === undefined) { + groups.set(key, [entity]) + } else { + group.push(entity) + } + } + return [...groups.values()].map((group) => ({ + module: group[0]!.module, + path: group[0]!.path, + entities: group.sort((left, right) => left.bucket.localeCompare(right.bucket)) + })) +} + +const groupName = (group: ApiGroup): string => group.path.at(-1) ?? "" + +const groupSimilarity = ( + base: ApiGroup, + head: ApiGroup +): number => { + const facets = base.entities.flatMap((baseEntity) => { + const headEntity = head.entities.find((candidate) => candidate.bucket === baseEntity.bucket) + return headEntity === undefined ? [] : [{ base: baseEntity, head: headEntity }] + }) + if (facets.length === 0) { + return 0 + } + const sameKind = facets.filter(({ base, head }) => base.declarationKind === head.declarationKind).length / + facets.length + const sameFingerprint = facets.filter(({ base, head }) => base.fingerprint === head.fingerprint).length / + facets.length + const structure = facets.reduce( + (total, { base, head }) => total + setSimilarity(entityFeatures(base), entityFeatures(head)), + 0 + ) / facets.length + const documentation = facets.reduce( + (total, { base, head }) => total + setSimilarity(documentationTokens(base), documentationTokens(head)), + 0 + ) / facets.length + const sameCategory = facets.some(({ base, head }) => + base.documentation.category !== undefined && + base.documentation.category === head.documentation.category + ) + const headModuleName = head.module.split("/").at(-1)?.toLowerCase() + const mentionsHeadModule = headModuleName !== undefined && + facets.some(({ base }) => documentationTokens(base).has(headModuleName)) + const moduleScore = base.module === head.module ? 0.25 : 0 + const packageScore = base.entities[0]!.packageName === head.entities[0]!.packageName ? 0.05 : 0 + return Math.min( + 0.99, + 0.2 * nameSimilarity(groupName(base), groupName(head)) + + (groupName(base) === groupName(head) ? 0.15 : 0) + + moduleScore + + packageScore + + 0.4 * sameFingerprint + + 0.15 * sameKind + + 0.15 * structure + + 0.025 * documentation + + (sameCategory ? 0.05 : 0) + + (mentionsHeadModule ? 0.075 : 0) + ) +} + +const changeId = ( + classification: ChangeClassification, + base?: ApiEntity, + head?: ApiEntity, + suffix = "" +): string => `change-${fingerprint([classification, base?.id ?? null, head?.id ?? null, suffix]).slice(0, 16)}` + +const makeChange = ( + classification: ChangeClassification, + match: Partial, + delta?: unknown, + suffix = "" +): ApiChange => ({ + id: changeId(classification, match.base, match.head, suffix), + classification, + confidence: match.confidence ?? 1, + baseApiId: match.base?.id, + headApiId: match.head?.id, + before: match.base?.displaySignature, + after: match.head?.displaySignature, + delta, + baseSource: match.base?.source, + headSource: match.head?.source, + reviewNotes: match.note, + authoritative: match.authoritative ?? true +}) + +const declarationHash = (value: unknown): string => fingerprint(value) + +const memberMap = (declarations: ReadonlyArray): Map => + new Map( + declarations.flatMap((declaration) => declaration.members ?? []) + .map((member) => [`${member.kind}:${member.name}`, member]) + ) + +const classifyStructure = (match: Match): ReadonlyArray => { + const base = match.base + const head = match.head + const changes: Array = [] + if (base.bucket !== head.bucket) { + changes.push(makeChange("bucket-changed", match, { before: base.bucket, after: head.bucket })) + } + if (base.declarationKind !== head.declarationKind) { + changes.push(makeChange("declaration-kind-changed", match, { + before: base.declarationKind, + after: head.declarationKind + })) + } + + const baseDeclarations = base.declarations + const headDeclarations = head.declarations + const baseSignatures = baseDeclarations.filter((declaration) => + declaration.kind === "function" || declaration.kind === "method" + ) + const headSignatures = headDeclarations.filter((declaration) => + declaration.kind === "function" || declaration.kind === "method" + ) + if (headSignatures.length > baseSignatures.length) { + changes.push(makeChange("overload-added", match, { + before: baseSignatures.length, + after: headSignatures.length + })) + } else if (headSignatures.length < baseSignatures.length) { + changes.push(makeChange("overload-removed", match, { + before: baseSignatures.length, + after: headSignatures.length + })) + } else if ( + baseSignatures.length > 1 && + stableJson(baseSignatures.map(declarationHash).sort()) === stableJson(headSignatures.map(declarationHash).sort()) && + stableJson(baseSignatures.map(declarationHash)) !== stableJson(headSignatures.map(declarationHash)) + ) { + changes.push(makeChange("overload-reordered", match)) + } + + const comparedSignatures = Math.min(baseSignatures.length, headSignatures.length) + for (let index = 0; index < comparedSignatures; index++) { + const before = baseSignatures[index]! + const after = headSignatures[index]! + const beforeParameters = before.parameters ?? [] + const afterParameters = after.parameters ?? [] + if (afterParameters.length > beforeParameters.length) { + changes.push(makeChange("parameter-added", match, { + overload: index, + before: beforeParameters, + after: afterParameters + }, String(index))) + } else if (afterParameters.length < beforeParameters.length) { + changes.push(makeChange("parameter-removed", match, { + overload: index, + before: beforeParameters, + after: afterParameters + }, String(index))) + } else if ( + stableJson(beforeParameters.map((parameter) => parameter.name).sort()) === + stableJson(afterParameters.map((parameter) => parameter.name).sort()) && + stableJson(beforeParameters.map((parameter) => parameter.name)) !== + stableJson(afterParameters.map((parameter) => parameter.name)) + ) { + changes.push(makeChange("parameter-reordered", match, { + overload: index, + before: beforeParameters, + after: afterParameters + }, String(index))) + } else if (stableJson(beforeParameters) !== stableJson(afterParameters)) { + changes.push(makeChange("parameter-changed", match, { + overload: index, + before: beforeParameters, + after: afterParameters + }, String(index))) + } + if (stableJson(before.returnType) !== stableJson(after.returnType)) { + changes.push(makeChange("return-type-changed", match, { + overload: index, + before: before.returnType, + after: after.returnType + }, String(index))) + } + if (stableJson(before.typeParameters) !== stableJson(after.typeParameters)) { + changes.push(makeChange("generic-parameter-changed", match, { + overload: index, + before: before.typeParameters, + after: after.typeParameters + }, String(index))) + } + } + + const baseMembers = memberMap(baseDeclarations) + const headMembers = memberMap(headDeclarations) + for (const [key, member] of baseMembers) { + const next = headMembers.get(key) + if (next === undefined) { + changes.push(makeChange("member-removed", match, { member }, key)) + } else if (stableJson(member) !== stableJson(next)) { + changes.push(makeChange("member-changed", match, { before: member, after: next }, key)) + } + } + for (const [key, member] of headMembers) { + if (!baseMembers.has(key)) { + changes.push(makeChange("member-added", match, { member }, key)) + } + } + + const baseHeritage = baseDeclarations.flatMap((declaration) => declaration.heritage ?? []) + const headHeritage = headDeclarations.flatMap((declaration) => declaration.heritage ?? []) + if (stableJson(baseHeritage) !== stableJson(headHeritage)) { + changes.push(makeChange("heritage-changed", match, { before: baseHeritage, after: headHeritage })) + } + + const baseTypes = baseDeclarations.map((declaration) => declaration.type) + const headTypes = headDeclarations.map((declaration) => declaration.type) + const baseKinds = new Set(baseTypes.flatMap((type) => type?.kind === undefined ? [] : [type.kind])) + const headKinds = new Set(headTypes.flatMap((type) => type?.kind === undefined ? [] : [type.kind])) + if ((baseKinds.has("union") || headKinds.has("union")) && stableJson(baseTypes) !== stableJson(headTypes)) { + changes.push(makeChange("union-member-changed", match, { before: baseTypes, after: headTypes })) + } + if ( + (baseKinds.has("intersection") || headKinds.has("intersection")) && + stableJson(baseTypes) !== stableJson(headTypes) + ) { + changes.push(makeChange("intersection-member-changed", match, { before: baseTypes, after: headTypes })) + } + if (stableJson(base.documentation) !== stableJson(head.documentation)) { + changes.push(makeChange("documentation-changed", match, { + before: base.documentation, + after: head.documentation + })) + } + if (base.fingerprint !== head.fingerprint && changes.length === 0) { + changes.push(makeChange("structural-change", match, { + before: base.declarations, + after: head.declarations + })) + } + return changes +} + +const movementChange = (match: Match): ApiChange | undefined => { + const beforeName = pathName(match.base) + const afterName = pathName(match.head) + if (beforeName !== afterName) { + return makeChange("api-renamed", match) + } + if (match.base.module !== match.head.module || match.base.path.join(".") !== match.head.path.join(".")) { + return makeChange("api-moved", match) + } + return undefined +} + +const moduleChanges = (base: ApiSnapshot, head: ApiSnapshot): ReadonlyArray => { + const changes: Array = [] + const basePackages = new Set(base.packages) + const headPackages = new Set(head.packages) + for (const packageName of basePackages) { + if (!headPackages.has(packageName)) { + changes.push({ + id: `change-${fingerprint(["package-removed", packageName]).slice(0, 16)}`, + classification: "package-removed", + confidence: 1, + delta: { packageName }, + authoritative: true + }) + } + } + for (const packageName of headPackages) { + if (!basePackages.has(packageName)) { + changes.push({ + id: `change-${fingerprint(["package-added", packageName]).slice(0, 16)}`, + classification: "package-added", + confidence: 1, + delta: { packageName }, + authoritative: true + }) + } + } + const baseModules = new Set(base.entrypoints.map((entrypoint) => entrypoint.module)) + const headModules = new Set(head.entrypoints.map((entrypoint) => entrypoint.module)) + for (const module of baseModules) { + if (!headModules.has(module)) { + changes.push({ + id: `change-${fingerprint(["module-removed", module]).slice(0, 16)}`, + classification: "module-removed", + confidence: 1, + delta: { from: module, to: [] }, + authoritative: true + }) + } + } + for (const module of headModules) { + if (!baseModules.has(module)) { + changes.push({ + id: `change-${fingerprint(["module-added", module]).slice(0, 16)}`, + classification: "module-added", + confidence: 1, + delta: { to: [module] }, + authoritative: true + }) + } + } + return changes +} + +export const diffSnapshots = (base: ApiSnapshot, head: ApiSnapshot): ApiDiff => { + const unmatchedBase = new Map(base.entities.map((entity) => [entity.id, entity])) + const unmatchedHead = new Map(head.entities.map((entity) => [entity.id, entity])) + const matches: Array = [] + const suggestedMatches: Array = [] + + const addMatch = ( + baseEntity: ApiEntity | undefined, + headEntity: ApiEntity | undefined, + details: Omit + ): boolean => { + if ( + baseEntity === undefined || headEntity === undefined || + !unmatchedBase.has(baseEntity.id) || !unmatchedHead.has(headEntity.id) + ) { + return false + } + unmatchedBase.delete(baseEntity.id) + unmatchedHead.delete(headEntity.id) + matches.push({ base: baseEntity, head: headEntity, ...details }) + return true + } + + for (const baseEntity of unmatchedBase.values()) { + addMatch(baseEntity, unmatchedHead.get(baseEntity.id), { + confidence: 1, + authoritative: true + }) + } + + const headGroups = groupEntities(unmatchedHead.values()) + for (const baseGroup of groupEntities(unmatchedBase.values())) { + const ranked = headGroups + .filter((group) => + groupName(group) === groupName(baseGroup) || + group.module === baseGroup.module || + baseGroup.entities.some((baseEntity) => + group.entities.some((headEntity) => + baseEntity.bucket === headEntity.bucket && + baseEntity.fingerprint === headEntity.fingerprint + ) + ) + ) + .map((group) => ({ group, score: groupSimilarity(baseGroup, group) })) + .filter(({ score }) => score > 0) + .sort((left, right) => + right.score - left.score || + left.group.module.localeCompare(right.group.module) || + left.group.path.join(".").localeCompare(right.group.path.join(".")) + ) + const best = ranked[0] + const next = ranked[1] + if (best === undefined || best.score < 0.5 || (next !== undefined && best.score - next.score < 0.05)) { + continue + } + for (const baseEntity of baseGroup.entities) { + const headEntity = best.group.entities.find((candidate) => candidate.bucket === baseEntity.bucket) + if (headEntity !== undefined) { + suggestedMatches.push({ + base: baseEntity, + head: headEntity, + confidence: Number(best.score.toFixed(3)), + authoritative: false, + note: "Suggested replacement for removed API; requires review" + }) + } + } + } + + const changes = [...moduleChanges(base, head)] + for (const match of matches) { + changes.push(...classifyStructure(match)) + } + for (const match of suggestedMatches) { + const movement = movementChange(match) + if (movement !== undefined) { + changes.push(movement) + } + } + for (const entity of unmatchedBase.values()) { + changes.push(makeChange("api-removed", { + base: entity, + confidence: 1, + authoritative: true + })) + } + for (const entity of unmatchedHead.values()) { + changes.push(makeChange("api-added", { + head: entity, + confidence: 1, + authoritative: true + })) + } + + return { + version: 1, + base: { ref: base.ref, sha: base.sha }, + head: { ref: head.ref, sha: head.sha }, + changes: changes.sort((left, right) => + left.classification.localeCompare(right.classification) || + (left.baseApiId ?? "").localeCompare(right.baseApiId ?? "") || + (left.headApiId ?? "").localeCompare(right.headApiId ?? "") || + left.id.localeCompare(right.id) + ) + } +} diff --git a/.context/effect/packages/tools/api-diff/src/Discovery.ts b/.context/effect/packages/tools/api-diff/src/Discovery.ts new file mode 100644 index 000000000..15cba603d --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Discovery.ts @@ -0,0 +1,242 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import type * as PlatformError from "effect/PlatformError" +import * as Schema from "effect/Schema" +import { ApiDiffError } from "./Error.ts" +import type { Entrypoint } from "./Model.ts" + +const PackageManifest = Schema.Struct({ + name: Schema.optional(Schema.String), + private: Schema.optional(Schema.Boolean), + exports: Schema.optional(Schema.Unknown), + publishConfig: Schema.optional(Schema.Struct({ + exports: Schema.optional(Schema.Unknown) + })) +}) + +type PackageManifest = typeof PackageManifest.Type + +interface PackageInfo { + readonly name: string + readonly root: string + readonly manifest: PackageManifest + readonly exports: unknown +} + +export interface DiscoveryResult { + readonly entrypoints: ReadonlyArray + readonly missing: ReadonlyArray +} + +const decodePackageManifest = Schema.decodeUnknownEffect(Schema.fromJsonString(PackageManifest)) + +const exportedTarget = (value: unknown): string | null | undefined => { + if (value === null || typeof value === "string") { + return value + } + if (typeof value !== "object" || value === null) { + return undefined + } + for (const condition of ["types", "import", "default", "node", "browser"]) { + const target = exportedTarget(Reflect.get(value, condition)) + if (target !== undefined) { + return target + } + } + return undefined +} + +const matchPattern = (pattern: string, value: string): string | undefined => { + const star = pattern.indexOf("*") + if (star === -1) { + return pattern === value ? "" : undefined + } + const prefix = pattern.slice(0, star) + const suffix = pattern.slice(star + 1) + return value.startsWith(prefix) && value.endsWith(suffix) + ? value.slice(prefix.length, value.length - suffix.length) + : undefined +} + +export class Discovery extends Context.Service + ) => Effect.Effect +}>()("@effect/api-diff/Discovery") { + static readonly layer = Layer.effect( + Discovery, + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + + const readManifest = Effect.fnUntraced(function*(location: string) { + const source = yield* fs.readFileString(location) + return yield* decodePackageManifest(source) + }) + + const walk = Effect.fnUntraced(function*( + root: string, + predicate: (location: string) => boolean + ) { + const output: Array = [] + const visit: (directory: string) => Effect.Effect = Effect.fnUntraced( + function*(directory: string) { + const entries = yield* fs.readDirectory(directory) + for (const entry of entries) { + if (entry === "node_modules" || entry === ".git") { + continue + } + const location = path.join(directory, entry) + const info = yield* fs.stat(location) + if (info.type === "Directory") { + yield* visit(location) + } else if (predicate(location)) { + output.push(location) + } + } + } + ) + if (yield* fs.exists(root)) { + yield* visit(root) + } + return output.sort() + }) + + const packagesIn = Effect.fnUntraced(function*(repoRoot: string) { + const manifestPaths = yield* walk( + path.join(repoRoot, "packages"), + (location) => location.endsWith(`${path.sep}package.json`) + ) + const packages: Array = [] + for (const manifestPath of manifestPaths) { + const sourceRoot = path.dirname(manifestPath) + if ( + path.basename(sourceRoot) === "dist" && + (yield* fs.exists(path.join(path.dirname(sourceRoot), "package.json"))) + ) { + continue + } + const packedPath = path.join(sourceRoot, "dist", "package.json") + const packed = yield* fs.exists(packedPath) + const manifest = yield* readManifest(packed ? packedPath : manifestPath) + if (manifest.name === undefined || manifest.private === true) { + continue + } + packages.push({ + name: manifest.name, + root: packed ? path.join(sourceRoot, "dist") : sourceRoot, + exports: packed ? manifest.exports : (manifest.publishConfig?.exports ?? manifest.exports), + manifest + }) + } + return packages.sort((left, right) => right.name.length - left.name.length) + }) + + const targetToDeclaration = (packageInfo: PackageInfo, target: string): string | undefined => { + const relativeTarget = target.replace(/^\.\//, "") + const declaration = relativeTarget.endsWith(".d.ts") + ? relativeTarget + : relativeTarget.replace(/\.(?:mjs|cjs|js|mts|cts|ts)$/, ".d.ts") + return declaration.endsWith(".d.ts") ? path.resolve(packageInfo.root, declaration) : undefined + } + + const expandPattern = Effect.fnUntraced(function*( + packageInfo: PackageInfo, + keyPattern: string, + targetPattern: string, + requested: ReadonlySet | undefined, + exportsMap: Readonly> + ) { + const candidates = yield* walk(packageInfo.root, (location) => location.endsWith(".d.ts")) + const targetNormalized = targetPattern.endsWith(".d.ts") + ? targetPattern + : targetPattern.replace(/\.(?:mjs|cjs|js|mts|cts|ts)$/, ".d.ts") + const entries: Array = [] + for (const declarationFile of candidates) { + const relativeFile = `./${path.relative(packageInfo.root, declarationFile).split(path.sep).join("/")}` + const capture = matchPattern(targetNormalized, relativeFile) + if (capture === undefined) { + continue + } + const key = keyPattern.replace("*", capture) + const module = key === "." ? packageInfo.name : `${packageInfo.name}${key.slice(1)}` + const excluded = Object.entries(exportsMap).some(([excludedKey, excludedTarget]) => + excludedTarget === null && matchPattern(excludedKey, key) !== undefined + ) + if (!excluded && (requested === undefined || requested.has(module))) { + entries.push({ packageName: packageInfo.name, module, declarationFile }) + } + } + return entries + }) + + const discoverEntrypointsInternal = Effect.fnUntraced(function*( + repoRoot: string, + requestedModules?: ReadonlyArray + ) { + const packages = yield* packagesIn(repoRoot) + const requested = requestedModules === undefined ? undefined : new Set(requestedModules) + const output = new Map() + + for (const packageInfo of packages) { + if (typeof packageInfo.exports !== "object" || packageInfo.exports === null) { + continue + } + const exportsMap = packageInfo.exports as Record + for (const [keyPattern, rawTarget] of Object.entries(exportsMap)) { + const target = exportedTarget(rawTarget) + if ( + !keyPattern.includes("*") && + typeof target === "string" && + (requested === undefined || requested.has( + keyPattern === "." ? packageInfo.name : `${packageInfo.name}${keyPattern.slice(1)}` + )) + ) { + const module = keyPattern === "." ? packageInfo.name : `${packageInfo.name}${keyPattern.slice(1)}` + const declarationFile = targetToDeclaration(packageInfo, target) + if ( + declarationFile !== undefined && + !output.has(module) && + (yield* fs.exists(declarationFile)) && + (yield* fs.stat(declarationFile)).type === "File" + ) { + output.set(module, { packageName: packageInfo.name, module, declarationFile }) + } + } + if (keyPattern.includes("*") && typeof target === "string" && target.includes("*")) { + for (const entry of yield* expandPattern(packageInfo, keyPattern, target, requested, exportsMap)) { + if (!output.has(entry.module)) { + output.set(entry.module, entry) + } + } + } + } + } + + return { + entrypoints: [...output.values()].sort((left, right) => left.module.localeCompare(right.module)), + missing: requestedModules?.filter((module) => !output.has(module)).sort() ?? [] + } + }) + + const discoverEntrypoints = ( + repoRoot: string, + requestedModules?: ReadonlyArray + ): Effect.Effect => + discoverEntrypointsInternal(repoRoot, requestedModules).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not discover API entrypoints in ${repoRoot}`, + cause + }) + ) + ) + + return Discovery.of({ discoverEntrypoints }) + }) + ) +} diff --git a/.context/effect/packages/tools/api-diff/src/Error.ts b/.context/effect/packages/tools/api-diff/src/Error.ts new file mode 100644 index 000000000..d2d4444c4 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Error.ts @@ -0,0 +1,9 @@ +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" + +export class ApiDiffError extends Schema.TaggedError()("ApiDiffError", { + message: Schema.String, + cause: Schema.optional(Schema.Defect()) +}) {} + +export const isApiDiffError = (u: unknown): u is ApiDiffError => Predicate.isTagged(u, "ApiDiffError") diff --git a/.context/effect/packages/tools/api-diff/src/Json.ts b/.context/effect/packages/tools/api-diff/src/Json.ts new file mode 100644 index 000000000..2d151929a --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Json.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema" +import { createHash } from "node:crypto" + +export const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown)) + +export const stableJson = (value: unknown): string => { + const visit = (input: unknown): unknown => { + if (Array.isArray(input)) { + return input.map(visit) + } + if (input !== null && typeof input === "object") { + return Object.fromEntries( + Object.entries(input) + .filter(([, entry]) => entry !== undefined) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => [key, visit(entry)]) + ) + } + return input + } + return JSON.stringify(visit(value)) +} + +export const fingerprint = (value: unknown): string => createHash("sha256").update(stableJson(value)).digest("hex") + +export const prettyJson = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n` diff --git a/.context/effect/packages/tools/api-diff/src/MigrationDoc.ts b/.context/effect/packages/tools/api-diff/src/MigrationDoc.ts new file mode 100644 index 000000000..eacb0038c --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/MigrationDoc.ts @@ -0,0 +1,451 @@ +import type { MigrationAnnotation } from "./Annotations.ts" +import type { ApiChange, ApiDiff, ChangeClassification } from "./Model.ts" + +const breakingClassifications = new Set([ + "bucket-changed", + "declaration-kind-changed", + "overload-removed", + "overload-reordered", + "parameter-removed", + "parameter-reordered", + "parameter-changed", + "return-type-changed", + "generic-parameter-changed", + "member-removed", + "member-changed", + "heritage-changed", + "union-member-changed", + "intersection-member-changed", + "structural-change" +]) + +const stableApiId = (id: string): string => id.replace(/#(?:type|value)$/, "") + +const displayApiId = (id: string): string => { + const separator = id.indexOf("#") + if (separator === -1) { + return id + } + const module = id.slice(0, separator).split("/").at(-1)! + return `${module}.${id.slice(separator + 1)}` +} + +const codeSpan = (value: string): string => { + const delimiter = "`".repeat(Math.max(1, ...(value.match(/`+/g)?.map((run) => run.length + 1) ?? []))) + const content = value.startsWith("`") || value.endsWith("`") ? ` ${value} ` : value + return `${delimiter}${content}${delimiter}` +} + +const escapeMarkdownText = (value: string): string => value.replace(/\\/g, "\\\\").replace(/[~<>*_]/g, "\\$&") + +const escapeAnnotationText = (value: string): string => { + const output: Array = [] + let index = 0 + while (index < value.length) { + const start = value.indexOf("`", index) + if (start === -1) { + output.push(escapeMarkdownText(value.slice(index))) + break + } + output.push(escapeMarkdownText(value.slice(index, start))) + let end = start + 1 + while (value[end] === "`") { + end++ + } + const delimiter = value.slice(start, end) + const close = value.indexOf(delimiter, end) + if (close === -1) { + output.push("\\`".repeat(delimiter.length)) + index = end + } else { + output.push(value.slice(start, close + delimiter.length)) + index = close + delimiter.length + } + } + return output.join("") +} + +const compareStrings = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0 + +const isBreakingChange = (change: ApiChange): boolean => { + if (change.classification !== "parameter-added") { + return breakingClassifications.has(change.classification) + } + const delta = change.delta as { + readonly before?: ReadonlyArray + readonly after?: ReadonlyArray<{ readonly optional?: boolean; readonly rest?: boolean }> + } | undefined + const beforeLength = delta?.before?.length ?? 0 + return delta?.after?.slice(beforeLength).some((parameter) => !parameter.optional && !parameter.rest) ?? true +} + +interface MigrationEntry { + readonly id: string + readonly module: string + readonly rename: ApiChange | undefined +} + +const importMapReplacements = (importMapSections: string): ReadonlyMap> => { + const replacements = new Map>() + for (const line of importMapSections.split("\n")) { + const match = /^(\S+) -> (\S+)(?: \(barrel: [^)]+\))?$/.exec(line) + if (match === null) { + continue + } + const targets = replacements.get(match[1]!) + if (targets === undefined) { + replacements.set(match[1]!, [match[2]!]) + } else if (!targets.includes(match[2]!)) { + targets.push(match[2]!) + } + } + return replacements +} + +const removedModules = (diff: ApiDiff): ReadonlyArray => + [ + ...new Set(diff.changes.flatMap((change) => { + if (change.classification !== "module-removed") { + return [] + } + const from = (change.delta as { readonly from?: unknown } | undefined)?.from + return typeof from === "string" ? [from] : [] + })) + ].sort(compareStrings) + +const sameStrings = (left: ReadonlyArray, right: ReadonlyArray): boolean => + left.length === right.length && left.every((value, index) => value === right[index]) + +const addedApiSignatures = (diff: ApiDiff): ReadonlyMap> => { + const signatures = new Map>() + for (const change of diff.changes) { + if (change.classification !== "api-added" || change.headApiId === undefined || change.after === undefined) { + continue + } + const id = stableApiId(change.headApiId) + const group = signatures.get(id) + if (group === undefined) { + signatures.set(id, [change.after]) + } else { + group.push(change.after) + } + } + for (const group of signatures.values()) { + group.sort(compareStrings) + } + return signatures +} + +const isUnchangedModuleMove = ( + id: string, + changes: ReadonlyArray, + removed: ReadonlySet, + replacements: ReadonlyMap>, + addedSignatures: ReadonlyMap> +): boolean => { + const separator = id.indexOf("#") + const module = id.slice(0, separator) + const targets = replacements.get(module) + if (separator === -1 || !removed.has(module) || targets === undefined) { + return false + } + const before = changes + .filter((change) => change.classification === "api-removed" && change.before !== undefined) + .map((change) => change.before!) + .sort(compareStrings) + if (before.length === 0) { + return false + } + const path = id.slice(separator + 1) + return targets.some((target) => { + const after = addedSignatures.get(`${target}#${path}`) + return after !== undefined && sameStrings(before, after) + }) +} + +const isRemovalCoveredByModule = ( + id: string, + removed: ReadonlySet, + annotations: ReadonlyMap +): boolean => { + const separator = id.indexOf("#") + const module = id.slice(0, separator) + if (separator === -1 || !removed.has(module) || annotations.get(module)?.replacement !== "none") { + return false + } + const annotation = annotations.get(id) + return annotation === undefined || annotation.replacement === "none" +} + +const migrationEntries = ( + diff: ApiDiff, + annotations: ReadonlyMap, + importMapSections: string +): ReadonlyArray => { + const grouped = new Map>() + for (const change of diff.changes) { + if (change.baseApiId === undefined) { + continue + } + const id = stableApiId(change.baseApiId) + const changes = grouped.get(id) + if (changes === undefined) { + grouped.set(id, [change]) + } else { + changes.push(change) + } + } + + const entries: Array = [] + const removedModuleSet = new Set(removedModules(diff)) + const replacements = importMapReplacements(importMapSections) + const addedSignatures = addedApiSignatures(diff) + for (const [id, changes] of grouped) { + const rename = changes.find((change) => change.classification === "api-renamed") + const breaking = changes.some(isBreakingChange) + const removed = changes.some((change) => change.classification === "api-removed") + const importMove = changes.some((change) => change.classification === "api-moved") + if ( + isUnchangedModuleMove(id, changes, removedModuleSet, replacements, addedSignatures) || + (removed && isRemovalCoveredByModule(id, removedModuleSet, annotations)) || + (!breaking && rename === undefined && !removed) || + (importMove && rename === undefined && !breaking && !annotations.has(id)) + ) { + continue + } + entries.push({ + id, + module: id.split("#")[0]!, + rename + }) + } + return entries.sort((left, right) => compareStrings(left.module, right.module) || compareStrings(left.id, right.id)) +} + +const renderDetailedEntry = ( + entry: MigrationEntry, + annotation: MigrationAnnotation, + example: string +): ReadonlyArray => [ + `#### ${codeSpan(displayApiId(entry.id))}`, + "", + `**Replacement:** ${codeSpan(annotation.replacement)}`, + "", + escapeAnnotationText(annotation.note), + "", + "**Example**", + "", + "```ts", + example, + "```", + "" +] + +const renderCompactEntry = ( + entry: MigrationEntry, + annotation: MigrationAnnotation | undefined +): string => { + if (annotation !== undefined) { + return `- ${codeSpan(displayApiId(entry.id))} -> ${codeSpan(annotation.replacement)}: ${ + escapeAnnotationText(annotation.note) + }` + } + const target = entry.rename?.headApiId + return target === undefined + ? `- ${codeSpan(displayApiId(entry.id))}: TODO: needs guidance` + : `- ${codeSpan(displayApiId(entry.id))} -> ${codeSpan(displayApiId(stableApiId(target)))}: TODO: needs guidance` +} + +export const extractImportMapSections = (document: string): string => { + const start = document.indexOf("## Import Map") + if (start === -1) { + return "## Import Map\n\nNo import map is available.\n" + } + const apiRenames = document.indexOf("\n## API Renames", start) + const removedModules = document.indexOf("\n## Removed Modules", start) + const apiReference = document.indexOf("\n## API Reference", start) + const ends = [apiRenames, removedModules, apiReference].filter((index) => index !== -1) + const end = ends.length === 0 ? document.length : Math.min(...ends) + return `${document.slice(start, end).trim()}\n` +} + +export const renderMigrationDocument = ( + diff: ApiDiff, + annotations: ReadonlyMap, + importMapSections: string +): string => { + const entries = migrationEntries(diff, annotations, importMapSections) + const replacements = importMapReplacements(importMapSections) + const modulesRemoved = removedModules(diff) + const modules = new Map>() + for (const entry of entries) { + const group = modules.get(entry.module) + if (group === undefined) { + modules.set(entry.module, [entry]) + } else { + group.push(entry) + } + } + const lines = [ + "", + "", + "# v3 to v4 Migration Reference", + "", + `Base: \`${diff.base.ref}\` (\`${diff.base.sha}\`)`, + "", + `Head: \`${diff.head.ref}\` (\`${diff.head.sha}\`)`, + "", + "This file is generated from the API diff and `migration/annotations/*.yaml`.", + "", + importMapSections.trim(), + "", + "## Removed Modules", + "" + ] + for (const module of modulesRemoved) { + const annotation = annotations.get(module) + const targets = replacements.get(module) + if (annotation !== undefined) { + lines.push( + `- ${codeSpan(module)} -> ${codeSpan(annotation.replacement)}: ${escapeAnnotationText(annotation.note)}` + ) + } else if (targets !== undefined) { + lines.push(`- ${codeSpan(module)} -> ${targets.map(codeSpan).join(", ")}`) + } else if ([...annotations.keys()].some((id) => id.startsWith(`${module}#`))) { + lines.push(`- ${codeSpan(module)}: No single module replacement; follow the curated per-API guidance below.`) + } else { + lines.push(`- ${codeSpan(module)}: TODO: needs module guidance`) + } + } + if (modulesRemoved.length === 0) { + lines.push("No modules were removed.") + } + lines.push( + "", + "## API Reference", + "" + ) + for (const [module, moduleEntries] of modules) { + lines.push(`### ${codeSpan(module)}`, "") + for (const entry of moduleEntries) { + const annotation = annotations.get(entry.id) + if (annotation?.example !== undefined) { + lines.push(...renderDetailedEntry(entry, annotation, annotation.example)) + } else { + lines.push(renderCompactEntry(entry, annotation), "") + } + } + } + return `${lines.join("\n").trim()}\n` +} + +export const markdownSafetyIssues = (document: string): ReadonlyArray => { + const issues: Array = [] + let fence: string | undefined + const lines = document.split("\n") + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]! + const fenceMatch = /^\s*(`{3,})(?:[^`]*)$/.exec(line) + if (fence !== undefined) { + if (line.trim() === fence) { + fence = undefined + } + continue + } + if (fenceMatch !== null) { + fence = fenceMatch[1]! + continue + } + if ( + line === "" || + line.startsWith("**Replacement:** ") || + line === "**Example**" + ) { + continue + } + let index = 0 + while (index < line.length) { + const character = line[index]! + if (character === "\\") { + index += 2 + continue + } + if (character === "`") { + let end = index + 1 + while (line[end] === "`") { + end++ + } + const delimiter = line.slice(index, end) + const close = line.indexOf(delimiter, end) + if (close === -1) { + issues.push(`line ${lineIndex + 1}: unclosed inline code span`) + break + } + index = close + delimiter.length + continue + } + if (character === "~" || character === "<" || character === "*" || character === "_") { + issues.push(`line ${lineIndex + 1}: unescaped ${JSON.stringify(character)}`) + } + index++ + } + } + if (fence !== undefined) { + issues.push(`unclosed ${fence} code fence`) + } + return issues +} + +export const unannotatedApiIds = ( + diff: ApiDiff, + annotations: ReadonlyMap, + importMapSections = "" +): ReadonlyMap> => { + const modules = new Map>() + for (const entry of migrationEntries(diff, annotations, importMapSections)) { + if (annotations.has(entry.id)) { + continue + } + const ids = modules.get(entry.module) + if (ids === undefined) { + modules.set(entry.module, [entry.id]) + } else { + ids.push(entry.id) + } + } + return modules +} + +export const unannotatedModuleIds = ( + diff: ApiDiff, + annotations: ReadonlyMap, + importMapSections: string +): ReadonlyArray => { + const replacements = importMapReplacements(importMapSections) + return removedModules(diff).filter((module) => + !annotations.has(module) && + !replacements.has(module) && + ![...annotations.keys()].some((id) => id.startsWith(`${module}#`)) + ) +} + +export const renderMissingAnnotations = ( + diff: ApiDiff, + annotations: ReadonlyMap, + importMapSections = "" +): string => { + const missingApis = unannotatedApiIds(diff, annotations, importMapSections) + const missingModules = unannotatedModuleIds(diff, annotations, importMapSections) + if (missingApis.size === 0 && missingModules.length === 0) { + return "All migration APIs and removed modules have guidance.\n" + } + return `${ + [ + ...(missingModules.length === 0 ? [] : ["Removed modules:", ...missingModules.map((module) => ` - ${module}`)]), + ...[...missingApis].flatMap(([module, ids]) => [ + `${module}:`, + ...ids.map((id) => ` - ${id}`) + ]) + ].join("\n") + }\n` +} diff --git a/.context/effect/packages/tools/api-diff/src/Model.ts b/.context/effect/packages/tools/api-diff/src/Model.ts new file mode 100644 index 000000000..e2b88b527 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Model.ts @@ -0,0 +1,156 @@ +import * as Schema from "effect/Schema" + +export const Bucket = Schema.Literals(["type", "value"]) + +export type Bucket = typeof Bucket.Type + +export const SourceLocation = Schema.Struct({ + file: Schema.String, + line: Schema.Number, + column: Schema.Number +}) + +export type SourceLocation = typeof SourceLocation.Type + +export interface Documentation { + readonly summary?: string | undefined + readonly deprecated?: string | undefined + readonly since?: string | undefined + readonly category?: string | undefined + readonly stability?: "stable" | "unstable" | undefined +} + +export interface TypeModel { + readonly kind: string + readonly [key: string]: unknown +} + +export interface DeclarationModel { + readonly kind: string + readonly name: string + readonly typeParameters?: ReadonlyArray | undefined + readonly parameters?: ReadonlyArray | undefined + readonly returnType?: TypeModel | undefined + readonly type?: TypeModel | undefined + readonly members?: ReadonlyArray | undefined + readonly overloads?: ReadonlyArray | undefined + readonly heritage?: ReadonlyArray | undefined + readonly modifiers?: ReadonlyArray | undefined + readonly value?: string | number | undefined +} + +export interface TypeParameterModel { + readonly id: string + readonly displayName: string + readonly constraint?: TypeModel | undefined + readonly default?: TypeModel | undefined +} + +export interface ParameterModel { + readonly name: string + readonly type: TypeModel + readonly optional: boolean + readonly rest: boolean + readonly modifiers?: ReadonlyArray | undefined +} + +export interface ImportRoute { + readonly module: string + readonly path: ReadonlyArray +} + +export interface ApiEntity { + readonly id: string + readonly packageName: string + readonly module: string + readonly path: ReadonlyArray + readonly bucket: Bucket + readonly declarationKind: string + readonly importRoutes: ReadonlyArray + readonly declarations: ReadonlyArray + readonly displaySignature: string + readonly fingerprint: string + readonly documentation: Documentation + readonly source: SourceLocation +} + +export interface Entrypoint { + readonly packageName: string + readonly module: string + readonly declarationFile: string +} + +export const SnapshotDiagnostic = Schema.Struct({ + code: Schema.String, + message: Schema.String, + module: Schema.optional(Schema.String), + path: Schema.optional(Schema.Array(Schema.String)), + source: Schema.optional(SourceLocation) +}) + +export type SnapshotDiagnostic = typeof SnapshotDiagnostic.Type + +export interface ApiSnapshot { + readonly version: 1 + readonly compiler: { + readonly name: "typescript" + readonly version: string + } + readonly ref: string + readonly sha: string + readonly packages: ReadonlyArray + readonly entrypoints: ReadonlyArray + readonly entities: ReadonlyArray + readonly diagnostics: ReadonlyArray +} + +export type ChangeClassification = + | "package-added" + | "package-removed" + | "module-added" + | "module-removed" + | "api-added" + | "api-removed" + | "api-moved" + | "api-renamed" + | "bucket-changed" + | "declaration-kind-changed" + | "overload-added" + | "overload-removed" + | "overload-reordered" + | "parameter-added" + | "parameter-removed" + | "parameter-reordered" + | "parameter-changed" + | "return-type-changed" + | "generic-parameter-changed" + | "member-added" + | "member-removed" + | "member-changed" + | "heritage-changed" + | "union-member-changed" + | "intersection-member-changed" + | "documentation-changed" + | "structural-change" + +export interface ApiChange { + readonly id: string + readonly classification: ChangeClassification + readonly confidence: number + readonly baseApiId?: string | undefined + readonly headApiId?: string | undefined + readonly before?: string | undefined + readonly after?: string | undefined + readonly delta?: unknown + readonly baseSource?: SourceLocation | undefined + readonly headSource?: SourceLocation | undefined + readonly reviewNotes?: string | undefined + readonly authoritative: boolean +} + +export interface ApiDiff { + readonly version: 1 + readonly base: { readonly ref: string; readonly sha: string } + readonly head: { readonly ref: string; readonly sha: string } + readonly changes: ReadonlyArray +} diff --git a/.context/effect/packages/tools/api-diff/src/Report.ts b/.context/effect/packages/tools/api-diff/src/Report.ts new file mode 100644 index 000000000..146a121e0 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Report.ts @@ -0,0 +1,141 @@ +import type { ApiChange, ApiDiff } from "./Model.ts" + +const escapeCell = (value: string): string => value.replaceAll("|", "\\|").replaceAll("\n", " ") + +const changeLabel = (change: ApiChange): string => + change.baseApiId === undefined + ? change.headApiId ?? JSON.stringify(change.delta) + : change.headApiId === undefined + ? change.baseApiId + : `${change.baseApiId} → ${change.headApiId}` + +const signature = (label: string, source: string | undefined): ReadonlyArray => + source === undefined ? [] : ["", `${label}:`, "", "```ts", source, "```"] + +const renderChange = (change: ApiChange, level = 4): ReadonlyArray => [ + `${"#".repeat(level)} ${change.classification}: \`${changeLabel(change)}\``, + "", + `Confidence: ${change.confidence.toFixed(3)} · ${change.authoritative ? "authoritative" : "review required"}`, + ...(change.reviewNotes === undefined ? [] : ["", change.reviewNotes]), + ...signature("Before", change.before), + ...signature("After", change.after), + "" +] + +const orderedGroups = [ + ["Renames and moves", new Set(["api-moved", "api-renamed"])], + ["Removals", new Set(["package-removed", "module-removed", "api-removed"])], + ["Additions", new Set(["package-added", "module-added", "api-added"])], + [ + "Signature and structural changes", + new Set([ + "bucket-changed", + "declaration-kind-changed", + "overload-added", + "overload-removed", + "overload-reordered", + "parameter-added", + "parameter-removed", + "parameter-reordered", + "parameter-changed", + "return-type-changed", + "generic-parameter-changed", + "member-added", + "member-removed", + "member-changed", + "heritage-changed", + "union-member-changed", + "intersection-member-changed", + "structural-change" + ]) + ], + ["Documentation changes", new Set(["documentation-changed"])] +] as const + +export const renderMarkdownReport = (diff: ApiDiff): string => { + const counts = new Map() + for (const change of diff.changes) { + counts.set(change.classification, (counts.get(change.classification) ?? 0) + 1) + } + const authoritative = diff.changes.filter((change) => change.authoritative) + const suggested = diff.changes.filter((change) => !change.authoritative) + const moduleCounts = new Map() + for (const change of diff.changes) { + const id = change.headApiId ?? change.baseApiId + const delta = change.delta as { + readonly packageName?: string + readonly from?: string + readonly to?: ReadonlyArray + } | undefined + const module = id?.split("#")[0] ?? delta?.packageName ?? delta?.from ?? delta?.to?.[0] ?? "" + moduleCounts.set(module, (moduleCounts.get(module) ?? 0) + 1) + } + const lines: Array = [ + "# TypeScript API Diff", + "", + `Base: \`${diff.base.ref}\` (\`${diff.base.sha}\`)`, + "", + `Head: \`${diff.head.ref}\` (\`${diff.head.sha}\`)`, + "", + "This report describes structural API changes and review confidence; it is not a semantic-version compatibility claim.", + "", + "## Summary", + "", + "| Classification | Count |", + "| --- | ---: |", + ...[...counts].sort(([left], [right]) => left.localeCompare(right)) + .map(([classification, count]) => `| ${escapeCell(classification)} | ${count} |`), + "", + "## Changes by module", + "", + "| Domain | Module | Count |", + "| --- | --- | ---: |", + ...[...moduleCounts].sort(([left], [right]) => left.localeCompare(right)).map(([module, count]) => { + const unstable = module.match(/^effect\/unstable\/([^/]+)/) + const domain = unstable?.[1] === undefined + ? module.startsWith("@effect/") + ? module.split("/").slice(0, 2).join("/") + : "stable" + : `unstable/${unstable[1]}` + return `| ${escapeCell(domain)} | ${escapeCell(module)} | ${count} |` + }), + "" + ] + const sections = [ + [ + "Stable API changes", + authoritative.filter((change) => + !(change.baseApiId ?? change.headApiId ?? JSON.stringify(change.delta)).includes("/unstable/") + ) + ], + [ + "Unstable API changes", + authoritative.filter((change) => + (change.baseApiId ?? change.headApiId ?? JSON.stringify(change.delta)).includes("/unstable/") + ) + ] + ] as const + for (const [section, sectionChanges] of sections) { + if (sectionChanges.length === 0) { + continue + } + lines.push(`## ${section}`, "") + for (const [title, classifications] of orderedGroups) { + const changes = sectionChanges.filter((change) => classifications.has(change.classification)) + if (changes.length === 0) { + continue + } + lines.push(`### ${title}`, "") + for (const change of changes) { + lines.push(...renderChange(change)) + } + } + } + if (suggested.length > 0) { + lines.push("## Suggested replacements for removed APIs", "") + for (const change of suggested) { + lines.push(...renderChange(change, 3)) + } + } + return `${lines.join("\n").trim()}\n` +} diff --git a/.context/effect/packages/tools/api-diff/src/Snapshot.ts b/.context/effect/packages/tools/api-diff/src/Snapshot.ts new file mode 100644 index 000000000..445d58097 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Snapshot.ts @@ -0,0 +1,884 @@ +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as Predicate from "effect/Predicate" +import * as Schema from "effect/Schema" +import ts from "typescript-compiler" +import { Discovery, type DiscoveryResult } from "./Discovery.ts" +import { ApiDiffError } from "./Error.ts" +import { fingerprint, stableJson } from "./Json.ts" +import { SnapshotDiagnostic as SnapshotDiagnosticSchema } from "./Model.ts" +import type { + ApiEntity, + ApiSnapshot, + Bucket, + DeclarationModel, + Documentation, + Entrypoint, + ImportRoute, + ParameterModel, + SnapshotDiagnostic, + SourceLocation, + TypeModel, + TypeParameterModel +} from "./Model.ts" + +interface SerializationContext { + readonly checker: ts.TypeChecker + readonly path: Path.Path + readonly repoRoot: string + readonly typeParameters: ReadonlyMap + readonly publicSymbols: ReadonlyMap +} + +const normalizedPath = (path: Path.Path, root: string, location: string): string => + path.relative(root, location).split(path.sep).join("/") + +const sourceLocation = (path: Path.Path, root: string, node: ts.Node): SourceLocation => { + const source = node.getSourceFile() + const position = source.getLineAndCharacterOfPosition(node.getStart(source, false)) + return { + file: normalizedPath(path, root, source.fileName), + line: position.line + 1, + column: position.character + 1 + } +} + +const modifiers = (node: ts.Node): ReadonlyArray | undefined => { + if (!ts.canHaveModifiers(node)) { + return undefined + } + const values = ts.getModifiers(node)?.map((modifier) => + ts.tokenToString(modifier.kind) ?? ts.SyntaxKind[modifier.kind] + ) + .filter((modifier) => modifier !== "export" && modifier !== "declare") + .sort() + return values === undefined || values.length === 0 ? undefined : values +} + +const nameText = (name: ts.DeclarationName | ts.BindingName | undefined): string => { + if (name === undefined) { + return "" + } + if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) { + return name.text + } + if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { + return name.text + } + return name.getText() +} + +const sortModels = (values: ReadonlyArray): ReadonlyArray => + [...values].sort((left, right) => stableJson(left).localeCompare(stableJson(right))) + +const referenceResolution = (name: ts.EntityName, context: SerializationContext): unknown => { + let symbol = context.checker.getSymbolAtLocation(name) + if (symbol === undefined) { + return { unresolved: true } + } + if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) { + symbol = context.checker.getAliasedSymbol(symbol) + } + const declaration = symbol?.declarations?.[0] + if (declaration === undefined) { + return { unresolved: true } + } + const publicApiId = context.publicSymbols.get(symbolIdentity(symbol, "type")) ?? + context.publicSymbols.get(symbolIdentity(symbol, "value")) + if (publicApiId !== undefined) { + return { apiId: publicApiId } + } + const file = declaration.getSourceFile().fileName + const separator = context.path.sep + const nodeModules = file.lastIndexOf(`${separator}node_modules${separator}`) + if (nodeModules !== -1) { + const remainder = file.slice(nodeModules + `${separator}node_modules${separator}`.length).split(separator) + return { + externalPackage: remainder[0]?.startsWith("@") ? remainder.slice(0, 2).join("/") : remainder[0] + } + } + return { declaration: normalizedPath(context.path, context.repoRoot, file) } +} + +const primitiveKinds = new Map([ + [ts.SyntaxKind.AnyKeyword, "any"], + [ts.SyntaxKind.BigIntKeyword, "bigint"], + [ts.SyntaxKind.BooleanKeyword, "boolean"], + [ts.SyntaxKind.IntrinsicKeyword, "intrinsic"], + [ts.SyntaxKind.NeverKeyword, "never"], + [ts.SyntaxKind.NumberKeyword, "number"], + [ts.SyntaxKind.ObjectKeyword, "object"], + [ts.SyntaxKind.StringKeyword, "string"], + [ts.SyntaxKind.SymbolKeyword, "symbol"], + [ts.SyntaxKind.UndefinedKeyword, "undefined"], + [ts.SyntaxKind.UnknownKeyword, "unknown"], + [ts.SyntaxKind.VoidKeyword, "void"] +]) + +const serializeTypeParameters = ( + nodes: ts.NodeArray | undefined, + context: SerializationContext +): { readonly parameters?: ReadonlyArray; readonly context: SerializationContext } => { + if (nodes === undefined || nodes.length === 0) { + return { context } + } + const names = new Map(context.typeParameters) + nodes.forEach((node, index) => names.set(node.name.text, `T${index}`)) + const next = { ...context, typeParameters: names } + return { + context: next, + parameters: nodes.map((node, index) => ({ + id: `T${index}`, + displayName: node.name.text, + constraint: node.constraint === undefined ? undefined : serializeType(node.constraint, next), + default: node.default === undefined ? undefined : serializeType(node.default, next) + })) + } +} + +const serializeParameters = ( + nodes: ts.NodeArray, + context: SerializationContext +): ReadonlyArray => + nodes.map((node) => ({ + name: nameText(node.name), + type: node.type === undefined ? { kind: "unknown" } : serializeType(node.type, context), + optional: node.questionToken !== undefined || node.initializer !== undefined, + rest: node.dotDotDotToken !== undefined, + modifiers: modifiers(node) + })) + +const serializeSignature = ( + node: ts.SignatureDeclarationBase, + context: SerializationContext, + kind: string, + name = "" +): DeclarationModel => { + const generic = serializeTypeParameters(node.typeParameters, context) + return { + kind, + name, + typeParameters: generic.parameters, + parameters: serializeParameters(node.parameters, generic.context), + returnType: node.type === undefined ? { kind: "unknown" } : serializeType(node.type, generic.context), + modifiers: modifiers(node) + } +} + +const serializeTypeMember = (node: ts.TypeElement, context: SerializationContext): DeclarationModel => { + if (ts.isPropertySignature(node)) { + return { + kind: "property", + name: nameText(node.name), + type: node.type === undefined ? { kind: "unknown" } : serializeType(node.type, context), + modifiers: [ + ...(modifiers(node) ?? []), + ...(node.questionToken === undefined ? [] : ["optional"]) + ].sort() + } + } + if (ts.isMethodSignature(node)) { + return { + ...serializeSignature(node, context, "method", nameText(node.name)), + modifiers: [ + ...(modifiers(node) ?? []), + ...(node.questionToken === undefined ? [] : ["optional"]) + ].sort() + } + } + if (ts.isCallSignatureDeclaration(node)) { + return serializeSignature(node, context, "call-signature") + } + if (ts.isConstructSignatureDeclaration(node)) { + return serializeSignature(node, context, "construct-signature") + } + if (ts.isIndexSignatureDeclaration(node)) { + return serializeSignature(node, context, "index-signature") + } + if (ts.isGetAccessorDeclaration(node)) { + return serializeSignature(node, context, "getter", nameText(node.name)) + } + if (ts.isSetAccessorDeclaration(node)) { + return serializeSignature(node, context, "setter", nameText(node.name)) + } + throw new Error(`Unsupported public type member: ${ts.SyntaxKind[node.kind]}`) +} + +const serializeObjectMembers = ( + members: ts.NodeArray, + context: SerializationContext +): ReadonlyArray => + members.map((member) => serializeTypeMember(member, context)) + .sort((left, right) => + `${left.kind}:${left.name}:${stableJson(left)}`.localeCompare( + `${right.kind}:${right.name}:${stableJson(right)}` + ) + ) + +export const serializeType = (node: ts.TypeNode, context: SerializationContext): TypeModel => { + const primitive = primitiveKinds.get(node.kind) + if (primitive !== undefined) { + return { kind: "primitive", name: primitive } + } + if (ts.isParenthesizedTypeNode(node)) { + return serializeType(node.type, context) + } + if (ts.isLiteralTypeNode(node)) { + const literal = node.literal + return { + kind: "literal", + value: literal.kind === ts.SyntaxKind.TrueKeyword + ? true + : literal.kind === ts.SyntaxKind.FalseKeyword + ? false + : ts.isPrefixUnaryExpression(literal) + ? literal.getText() + : Reflect.get(literal, "text") ?? literal.getText() + } + } + if (ts.isTypeReferenceNode(node)) { + const rawName = node.typeName.getText() + const genericId = context.typeParameters.get(rawName) + if (genericId !== undefined) { + return { kind: "type-parameter", id: genericId, displayName: rawName } + } + return { + kind: "reference", + name: rawName, + arguments: node.typeArguments?.map((argument) => serializeType(argument, context)) ?? [], + resolution: referenceResolution(node.typeName, context) + } + } + if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) { + return { + kind: ts.isUnionTypeNode(node) ? "union" : "intersection", + members: sortModels(node.types.map((type) => serializeType(type, context))) + } + } + if (ts.isArrayTypeNode(node)) { + return { kind: "array", element: serializeType(node.elementType, context) } + } + if (ts.isTupleTypeNode(node)) { + return { kind: "tuple", elements: node.elements.map((element) => serializeType(element, context)) } + } + if (ts.isNamedTupleMember(node)) { + return { + kind: "named-tuple-member", + name: node.name.text, + optional: node.questionToken !== undefined, + rest: node.dotDotDotToken !== undefined, + type: serializeType(node.type, context) + } + } + if (ts.isOptionalTypeNode(node)) { + return { kind: "optional", type: serializeType(node.type, context) } + } + if (ts.isRestTypeNode(node)) { + return { kind: "rest", type: serializeType(node.type, context) } + } + if (ts.isFunctionTypeNode(node) || ts.isConstructorTypeNode(node)) { + return { + kind: ts.isFunctionTypeNode(node) ? "function" : "constructor", + signature: serializeSignature(node, context, "signature") + } + } + if (ts.isTypeLiteralNode(node)) { + return { kind: "object", members: serializeObjectMembers(node.members, context) } + } + if (ts.isMappedTypeNode(node)) { + const generic = serializeTypeParameters(ts.factory.createNodeArray([node.typeParameter]), context) + return { + kind: "mapped", + typeParameter: generic.parameters?.[0], + readonly: node.readonlyToken?.kind === ts.SyntaxKind.MinusToken + ? "remove" + : node.readonlyToken === undefined + ? "none" + : "add", + optional: node.questionToken?.kind === ts.SyntaxKind.MinusToken + ? "remove" + : node.questionToken === undefined + ? "none" + : "add", + nameType: node.nameType === undefined ? undefined : serializeType(node.nameType, generic.context), + type: node.type === undefined ? undefined : serializeType(node.type, generic.context) + } + } + if (ts.isConditionalTypeNode(node)) { + return { + kind: "conditional", + check: serializeType(node.checkType, context), + extends: serializeType(node.extendsType, context), + trueType: serializeType(node.trueType, context), + falseType: serializeType(node.falseType, context) + } + } + if (ts.isIndexedAccessTypeNode(node)) { + return { + kind: "indexed-access", + object: serializeType(node.objectType, context), + index: serializeType(node.indexType, context) + } + } + if (ts.isTypeOperatorNode(node)) { + return { + kind: "operator", + operator: ts.tokenToString(node.operator) ?? ts.SyntaxKind[node.operator], + type: serializeType(node.type, context) + } + } + if (ts.isTypeQueryNode(node)) { + return { kind: "type-query", expression: node.exprName.getText() } + } + if (ts.isImportTypeNode(node)) { + const argument = node.argument.getText().replace(/^["']|["']$/g, "") + const parts = argument.split("/") + return { + kind: "import", + argument, + externalPackage: argument.startsWith(".") + ? undefined + : parts[0]?.startsWith("@") + ? parts.slice(0, 2).join("/") + : parts[0], + qualifier: node.qualifier?.getText(), + attributes: node.attributes?.getText(), + arguments: node.typeArguments?.map((argument) => serializeType(argument, context)) ?? [], + typeof: node.isTypeOf + } + } + if (ts.isTemplateLiteralTypeNode(node)) { + return { + kind: "template-literal", + head: node.head.text, + spans: node.templateSpans.map((span) => ({ + type: serializeType(span.type, context), + literal: span.literal.text + })) + } + } + if (ts.isInferTypeNode(node)) { + const generic = serializeTypeParameters(ts.factory.createNodeArray([node.typeParameter]), context) + return { kind: "infer", typeParameter: generic.parameters?.[0] } + } + if (ts.isTypePredicateNode(node)) { + return { + kind: "predicate", + asserts: node.assertsModifier !== undefined, + parameter: node.parameterName.getText(), + type: node.type === undefined ? undefined : serializeType(node.type, context) + } + } + if (ts.isExpressionWithTypeArguments(node)) { + return { + kind: "heritage-reference", + name: node.expression.getText(), + arguments: node.typeArguments?.map((argument) => serializeType(argument, context)) ?? [] + } + } + if (node.kind === ts.SyntaxKind.ThisType) { + return { kind: "this" } + } + throw new Error(`Unsupported public type syntax: ${ts.SyntaxKind[node.kind]}`) +} + +const serializeClassMember = ( + node: ts.ClassElement, + context: SerializationContext +): DeclarationModel | undefined => { + const visibility = modifiers(node) + if (visibility?.includes("private")) { + return undefined + } + if (ts.isPropertyDeclaration(node)) { + return { + kind: "property", + name: nameText(node.name), + type: node.type === undefined ? { kind: "unknown" } : serializeType(node.type, context), + modifiers: visibility + } + } + if (ts.isMethodDeclaration(node)) { + return serializeSignature(node, context, "method", nameText(node.name)) + } + if (ts.isConstructorDeclaration(node)) { + return serializeSignature(node, context, "constructor", "constructor") + } + if (ts.isGetAccessorDeclaration(node)) { + return serializeSignature(node, context, "getter", nameText(node.name)) + } + if (ts.isSetAccessorDeclaration(node)) { + return serializeSignature(node, context, "setter", nameText(node.name)) + } + if (ts.isIndexSignatureDeclaration(node)) { + return serializeSignature(node, context, "index-signature") + } + if (ts.isClassStaticBlockDeclaration(node) || ts.isSemicolonClassElement(node)) { + return undefined + } + throw new Error(`Unsupported public class member: ${ts.SyntaxKind[node.kind]}`) +} + +const serializeHeritage = ( + clauses: ts.NodeArray | undefined, + context: SerializationContext +): ReadonlyArray | undefined => { + if (clauses === undefined) { + return undefined + } + return clauses.flatMap((clause) => + clause.types.map((type) => ({ + ...serializeType(type, context), + clause: ts.tokenToString(clause.token) ?? ts.SyntaxKind[clause.token] + })) + ).sort((left, right) => stableJson(left).localeCompare(stableJson(right))) +} + +const moduleMembers = ( + node: ts.ModuleDeclaration, + context: SerializationContext +): ReadonlyArray => { + let body = node.body + while (body !== undefined && ts.isModuleDeclaration(body)) { + body = body.body + } + if (body === undefined || !ts.isModuleBlock(body)) { + return [] + } + return body.statements.flatMap((statement) => { + if ( + ts.isFunctionDeclaration(statement) || ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) || + ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement) || + ts.isVariableStatement(statement) + ) { + if (ts.isVariableStatement(statement)) { + return statement.declarationList.declarations.map((declaration) => serializeDeclaration(declaration, context)) + } + return [serializeDeclaration(statement, context)] + } + if (ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) { + return [] + } + throw new Error(`Unsupported public namespace statement: ${ts.SyntaxKind[statement.kind]}`) + }).sort((left, right) => `${left.kind}:${left.name}`.localeCompare(`${right.kind}:${right.name}`)) +} + +const serializeDeclaration = (node: ts.Declaration, context: SerializationContext): DeclarationModel => { + if (ts.isFunctionDeclaration(node)) { + return serializeSignature(node, context, "function", nameText(node.name)) + } + if (ts.isVariableDeclaration(node)) { + return { + kind: "variable", + name: nameText(node.name), + type: node.type === undefined ? { kind: "unknown" } : serializeType(node.type, context) + } + } + if (ts.isTypeAliasDeclaration(node)) { + const generic = serializeTypeParameters(node.typeParameters, context) + return { + kind: "type-alias", + name: node.name.text, + typeParameters: generic.parameters, + type: serializeType(node.type, generic.context), + modifiers: modifiers(node) + } + } + if (ts.isInterfaceDeclaration(node)) { + const generic = serializeTypeParameters(node.typeParameters, context) + return { + kind: "interface", + name: node.name.text, + typeParameters: generic.parameters, + members: serializeObjectMembers(node.members, generic.context), + heritage: serializeHeritage(node.heritageClauses, generic.context), + modifiers: modifiers(node) + } + } + if (ts.isClassDeclaration(node)) { + const generic = serializeTypeParameters(node.typeParameters, context) + return { + kind: "class", + name: nameText(node.name), + typeParameters: generic.parameters, + members: node.members.flatMap((member) => { + const serialized = serializeClassMember(member, generic.context) + return serialized === undefined ? [] : [serialized] + }).sort((left, right) => + `${left.kind}:${left.name}:${stableJson(left)}`.localeCompare( + `${right.kind}:${right.name}:${stableJson(right)}` + ) + ), + heritage: serializeHeritage(node.heritageClauses, generic.context), + modifiers: modifiers(node) + } + } + if (ts.isEnumDeclaration(node)) { + return { + kind: "enum", + name: node.name.text, + members: node.members.map((member) => ({ + kind: "enum-member", + name: nameText(member.name), + value: member.initializer === undefined + ? undefined + : ts.isStringLiteral(member.initializer) || ts.isNumericLiteral(member.initializer) + ? member.initializer.text + : member.initializer.getText() + })) + } + } + if (ts.isModuleDeclaration(node)) { + return { kind: "namespace", name: nameText(node.name), members: moduleMembers(node, context) } + } + if (ts.isExportAssignment(node)) { + return { kind: "export-assignment", name: "default", value: node.expression.getText() } + } + throw new Error(`Unsupported public declaration: ${ts.SyntaxKind[node.kind]}`) +} + +const declarationKind = (declarations: ReadonlyArray): string => + [...new Set(declarations.map((declaration) => declaration.kind))].sort().join("+") + +const documentation = (symbol: ts.Symbol, checker: ts.TypeChecker, module: string): Documentation => { + const tags = new Map( + symbol.getJsDocTags(checker).map((tag) => [ + tag.name, + tag.text?.map((part) => part.text).join("").trim() ?? "" + ]) + ) + const summary = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim() + return { + summary: summary === "" ? undefined : summary, + deprecated: tags.get("deprecated"), + since: tags.get("since"), + category: tags.get("category"), + stability: module.includes("/unstable/") ? "unstable" : "stable" + } +} + +const bucketDeclarations = (symbol: ts.Symbol, bucket: Bucket): ReadonlyArray => { + const declarations = symbol.declarations ?? [] + return declarations.filter((declaration) => { + if (bucket === "type") { + return ts.isInterfaceDeclaration(declaration) || ts.isTypeAliasDeclaration(declaration) || + ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration) || + ts.isModuleDeclaration(declaration) + } + return ts.isFunctionDeclaration(declaration) || ts.isVariableDeclaration(declaration) || + ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration) || + ts.isModuleDeclaration(declaration) || ts.isExportAssignment(declaration) + }) +} + +const symbolBuckets = (symbol: ts.Symbol): ReadonlyArray => { + const output: Array = [] + if ((symbol.flags & ts.SymbolFlags.Type) !== 0) { + output.push("type") + } + if ((symbol.flags & ts.SymbolFlags.Value) !== 0 || (symbol.flags & ts.SymbolFlags.Namespace) !== 0) { + output.push("value") + } + return output +} + +const displayDeclarations = (nodes: ReadonlyArray): string => + nodes.map((node) => node.getText().replace(/\/\*\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").trim()).join("\n") + +const fingerprintDeclarations = (declarations: ReadonlyArray): string => { + const normalize = (value: unknown, parent?: string): unknown => { + if (Array.isArray(value)) { + return value.map((entry) => normalize(entry, parent)) + } + if (value !== null && typeof value === "object") { + const input = value as Record + return Object.fromEntries( + Object.entries(input).flatMap(([key, entry]) => { + if ((parent === "parameters" && key === "name") || key === "displayName") { + return [] + } + return [[key, normalize(entry, key)]] + }) + ) + } + return value + } + return fingerprint( + declarations.map((declaration) => ({ + ...declaration, + name: "", + parameters: declaration.parameters?.map((parameter) => ({ ...parameter, name: "" })) + })).map((declaration) => normalize(declaration)) + ) +} + +interface PendingEntity { + readonly symbol: ts.Symbol + readonly bucket: Bucket + readonly packageName: string + readonly routes: Array + readonly declarations: ReadonlyArray +} + +const symbolIdentity = (symbol: ts.Symbol, bucket: Bucket): string => { + const declarations = symbol.declarations ?? [] + return `${bucket}:${ + declarations.map((node) => `${node.getSourceFile().fileName}:${node.pos}:${node.end}`).sort().join("|") + }` +} + +const collectExports = ( + checker: ts.TypeChecker, + moduleSymbol: ts.Symbol, + entrypoint: Entrypoint, + pending: Map, + path: ReadonlyArray = [], + seenNamespaces: ReadonlySet = new Set() +): void => { + for ( + const exported of checker.getExportsOfModule(moduleSymbol).sort((left, right) => + left.name.localeCompare(right.name) + ) + ) { + let symbol = exported + if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) { + symbol = checker.getAliasedSymbol(symbol) + } + const exportPath = [...path, exported.name] + const declarations = symbol.declarations ?? [] + for (const bucket of symbolBuckets(symbol)) { + const selected = bucketDeclarations(symbol, bucket) + if (selected.length === 0) { + continue + } + const key = symbolIdentity(symbol, bucket) + const existing = pending.get(key) + const route = { module: entrypoint.module, path: exportPath } + if (existing === undefined) { + pending.set(key, { + symbol, + bucket, + packageName: entrypoint.packageName, + routes: [route], + declarations: selected + }) + } else if ( + !existing.routes.some((candidate) => + candidate.module === route.module && candidate.path.join(".") === route.path.join(".") + ) + ) { + existing.routes.push(route) + } + } + const isNamespace = (symbol.flags & ts.SymbolFlags.Module) !== 0 + if (isNamespace && !seenNamespaces.has(symbol) && declarations.length > 0) { + collectExports( + checker, + symbol, + entrypoint, + pending, + exportPath, + new Set([...seenNamespaces, symbol]) + ) + } + } +} + +const canonicalRoute = (routes: ReadonlyArray): ImportRoute => + [...routes].sort((left, right) => + left.path.length - right.path.length || + right.module.split("/").length - left.module.split("/").length || + left.module.localeCompare(right.module) || + left.path.join(".").localeCompare(right.path.join(".")) + )[0]! + +export interface ExtractSnapshotOptions { + readonly repoRoot: string + readonly ref: string + readonly sha: string + readonly modules?: ReadonlyArray +} + +export class SnapshotExtractionError extends Schema.TaggedError()( + "SnapshotExtractionError", + { + message: Schema.String, + diagnostics: Schema.Array(SnapshotDiagnosticSchema) + } +) {} + +export const isSnapshotExtractionError = (u: unknown): u is SnapshotExtractionError => + Predicate.isTagged(u, "SnapshotExtractionError") + +const snapshotExtractionError = (diagnostics: ReadonlyArray): SnapshotExtractionError => + new SnapshotExtractionError({ + message: diagnostics.map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("\n"), + diagnostics + }) + +const extractSnapshot = ( + options: ExtractSnapshotOptions, + discovered: DiscoveryResult, + path: Path.Path +): ApiSnapshot => { + const diagnostics: Array = discovered.missing.map((module) => ({ + code: "missing-entrypoint", + message: `Consumer-visible declaration entrypoint not found: ${module}`, + module + })) + if (diagnostics.length > 0) { + throw snapshotExtractionError(diagnostics) + } + const program = ts.createProgram({ + rootNames: discovered.entrypoints.map((entrypoint) => entrypoint.declarationFile), + options: { + module: ts.ModuleKind.NodeNext, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + target: ts.ScriptTarget.ESNext, + skipLibCheck: true, + types: [], + noEmit: true + } + }) + // The declarations were already checked by the branch-native build. Re-checking + // them with the pinned extractor compiler would introduce lib-version drift. + const compilerDiagnostics = program.getSyntacticDiagnostics() + .filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error) + if (compilerDiagnostics.length > 0) { + throw snapshotExtractionError(compilerDiagnostics.map((diagnostic) => ({ + code: `typescript-${diagnostic.code}`, + message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), + source: diagnostic.file === undefined + ? undefined + : sourceLocation(path, options.repoRoot, diagnostic.file) + }))) + } + const checker = program.getTypeChecker() + const pending = new Map() + for (const entrypoint of discovered.entrypoints) { + const source = program.getSourceFile(entrypoint.declarationFile) + const symbol = source === undefined ? undefined : checker.getSymbolAtLocation(source) + if (source === undefined || symbol === undefined) { + diagnostics.push({ + code: "missing-module-symbol", + message: `Could not load module symbol for ${entrypoint.module}`, + module: entrypoint.module + }) + continue + } + collectExports(checker, symbol, entrypoint, pending) + } + + const entities: Array = [] + const publicSymbols = new Map() + for (const [key, pendingEntity] of pending) { + const route = canonicalRoute(pendingEntity.routes) + publicSymbols.set(key, `${route.module}#${route.path.join(".")}#${pendingEntity.bucket}`) + } + for (const pendingEntity of pending.values()) { + const route = canonicalRoute(pendingEntity.routes) + const declaration = pendingEntity.declarations[0] + if (declaration === undefined) { + continue + } + let serialized: ReadonlyArray + try { + serialized = pendingEntity.declarations.map((node) => + serializeDeclaration(node, { + checker, + path, + repoRoot: options.repoRoot, + typeParameters: new Map(), + publicSymbols + }) + ) + } catch (error) { + diagnostics.push({ + code: "unsupported-public-declaration", + message: error instanceof Error ? error.message : String(error), + module: route.module, + path: route.path, + source: sourceLocation(path, options.repoRoot, declaration) + }) + continue + } + const id = `${route.module}#${route.path.join(".")}#${pendingEntity.bucket}` + entities.push({ + id, + packageName: pendingEntity.packageName, + module: route.module, + path: route.path, + bucket: pendingEntity.bucket, + declarationKind: declarationKind(serialized), + importRoutes: [...pendingEntity.routes].sort((left, right) => + left.module.localeCompare(right.module) || left.path.join(".").localeCompare(right.path.join(".")) + ), + declarations: serialized, + displaySignature: displayDeclarations(pendingEntity.declarations), + fingerprint: fingerprintDeclarations(serialized), + documentation: documentation(pendingEntity.symbol, checker, route.module), + source: sourceLocation(path, options.repoRoot, declaration) + }) + } + if (diagnostics.length > 0) { + throw snapshotExtractionError(diagnostics) + } + + const packages = [...new Set(discovered.entrypoints.map((entrypoint) => entrypoint.packageName))].sort() + return { + version: 1, + compiler: { name: "typescript", version: ts.version }, + ref: options.ref, + sha: options.sha, + packages, + entrypoints: discovered.entrypoints.map((entrypoint) => ({ + ...entrypoint, + declarationFile: normalizedPath(path, options.repoRoot, entrypoint.declarationFile) + })), + entities: entities.sort((left, right) => left.id.localeCompare(right.id)), + diagnostics: [] + } +} + +export const snapshotCacheKey = ( + sha: string, + modules?: ReadonlyArray +): string => fingerprint(["snapshot-v4", sha, ts.version, modules === undefined ? "all" : [...modules].sort()]) + +export class Snapshotter extends Context.Service Effect.Effect< + ApiSnapshot, + ApiDiffError | SnapshotExtractionError + > +}>()("@effect/api-diff/Snapshotter") { + static readonly layerNoDependencies = Layer.effect( + Snapshotter, + Effect.gen(function*() { + const discovery = yield* Discovery + const path = yield* Path.Path + + const extract = Effect.fnUntraced(function*(options: ExtractSnapshotOptions) { + const discovered = yield* discovery.discoverEntrypoints(options.repoRoot, options.modules) + return yield* Effect.try({ + try: () => extractSnapshot(options, discovered, path), + catch: (cause) => + isSnapshotExtractionError(cause) + ? cause + : new ApiDiffError({ + message: `Could not extract the API snapshot for ${options.ref}`, + cause + }) + }) + }) + + return Snapshotter.of({ extract }) + }) + ) + + static readonly layer = this.layerNoDependencies.pipe( + Layer.provide(Discovery.layer) + ) +} diff --git a/.context/effect/packages/tools/api-diff/src/Worktrees.ts b/.context/effect/packages/tools/api-diff/src/Worktrees.ts new file mode 100644 index 000000000..837cc9853 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/Worktrees.ts @@ -0,0 +1,243 @@ +import * as Console from "effect/Console" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as Stream from "effect/Stream" +import * as ChildProcess from "effect/unstable/process/ChildProcess" +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" +import { ApiDiffError, isApiDiffError } from "./Error.ts" +import { decodeJson } from "./Json.ts" +import type { ApiSnapshot } from "./Model.ts" +import { snapshotCacheKey, Snapshotter } from "./Snapshot.ts" + +export interface PrepareSnapshotOptions { + readonly repoRoot: string + readonly cacheRoot: string + readonly worktreesRoot: string + readonly name: "base" | "head" + readonly ref: string + readonly sha: string + readonly modules?: ReadonlyArray +} + +export class Worktrees extends Context.Service Effect.Effect + readonly prepareSnapshot: (options: PrepareSnapshotOptions) => Effect.Effect +}>()("@effect/api-diff/Worktrees") { + static readonly layerNoDependencies = Layer.effect( + Worktrees, + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const snapshotter = yield* Snapshotter + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + + const runCommandScoped = Effect.fnUntraced(function*( + command: string, + args: ReadonlyArray, + cwd: string + ) { + const display = `${command} ${args.join(" ")}` + const handle = yield* spawner.spawn( + ChildProcess.make(command, args, { cwd }) + ).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not start command (${display})`, + cause + }) + ) + ) + const [output, exitCode] = yield* Effect.all([ + Stream.mkString(Stream.decodeText(handle.all)), + handle.exitCode + ], { concurrency: "unbounded" }).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not run command (${display})`, + cause + }) + ) + ) + return { display, exitCode, output } + }) + + const runCommand = ( + command: string, + args: ReadonlyArray, + cwd: string + ): Effect.Effect<{ + readonly display: string + readonly exitCode: ChildProcessSpawner.ExitCode + readonly output: string + }, ApiDiffError> => Effect.scoped(runCommandScoped(command, args, cwd)) + + const runChecked = Effect.fnUntraced(function*( + command: string, + args: ReadonlyArray, + cwd: string + ) { + const result = yield* runCommand(command, args, cwd) + if (result.exitCode !== ChildProcessSpawner.ExitCode(0)) { + return yield* new ApiDiffError({ + message: `Command failed (${result.display}):\n${result.output}`.trim() + }) + } + return result.output.trim() + }) + + const enableStripInternal = Effect.fnUntraced(function*(worktree: string) { + const baseConfig = path.join(worktree, "tsconfig.base.json") + if (!(yield* fs.exists(baseConfig))) { + return + } + const source = yield* fs.readFileString(baseConfig) + if (/"stripInternal"\s*:\s*true/.test(source)) { + return + } + const updated = source.replace(/("stripInternal"\s*:\s*)false/, "$1true") + if (updated !== source) { + yield* fs.writeFileString(baseConfig, updated) + } + }) + + const hasProductionStripInternal = Effect.fnUntraced(function*(worktree: string) { + const candidates = [ + path.join(worktree, "tsconfig.base.json"), + path.join(worktree, "tsconfig.build.json"), + path.join(worktree, "packages", "effect", "tsconfig.build.json") + ] + for (const candidate of candidates) { + if ((yield* fs.exists(candidate)) && /"stripInternal"\s*:\s*true/.test(yield* fs.readFileString(candidate))) { + return true + } + } + return false + }) + + const buildWorktree = Effect.fnUntraced(function*(worktree: string) { + yield* enableStripInternal(worktree) + if (!(yield* hasProductionStripInternal(worktree))) { + return yield* new ApiDiffError({ + message: `No production TypeScript configuration enables stripInternal in ${worktree}` + }) + } + // Native test-only dependencies in old branches may not build on the + // current Node runtime. Declaration emission does not require dependency lifecycle scripts. + yield* runChecked("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts"], worktree) + yield* runChecked("pnpm", ["build"], worktree) + }) + + const removeWorktree = (repoRoot: string, worktree: string): Effect.Effect => + runCommand("git", ["worktree", "remove", "--force", worktree], repoRoot).pipe( + Effect.flatMap((result) => + result.exitCode === ChildProcessSpawner.ExitCode(0) + ? Effect.void + : Console.error(result.output) + ), + Effect.catch((error) => Console.error(error.message)) + ) + + const resolveRef = Effect.fnUntraced(function*(repoRoot: string, ref: string) { + return yield* runChecked("git", ["rev-parse", "--verify", `${ref}^{commit}`], repoRoot) + }) + + const prepareSnapshotInternal = Effect.fnUntraced(function*(options: PrepareSnapshotOptions) { + const key = snapshotCacheKey(options.sha, options.modules) + const cacheLocation = path.join(options.cacheRoot, key, "snapshot.json") + if (yield* fs.exists(cacheLocation)) { + const source = yield* fs.readFileString(cacheLocation).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not read cached snapshot ${cacheLocation}`, + cause + }) + ) + ) + const cached = (yield* decodeJson(source).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not parse cached snapshot ${cacheLocation}`, + cause + }) + ) + )) as ApiSnapshot + return { ...cached, ref: options.ref, sha: options.sha } + } + + yield* fs.makeDirectory(options.worktreesRoot, { recursive: true }) + return yield* Effect.scoped(Effect.gen(function*() { + const runRoot = yield* fs.makeTempDirectoryScoped({ + directory: options.worktreesRoot, + prefix: `${options.name}-` + }) + const worktree = path.join(runRoot, "repo") + return yield* Effect.acquireUseRelease( + runChecked("git", ["worktree", "add", "--detach", worktree, options.sha], options.repoRoot), + () => + Effect.gen(function*() { + yield* buildWorktree(worktree) + const snapshot = yield* snapshotter.extract({ + repoRoot: worktree, + ref: options.ref, + sha: options.sha, + ...(options.modules === undefined ? {} : { modules: options.modules }) + }).pipe( + Effect.mapError((cause) => + isApiDiffError(cause) + ? cause + : new ApiDiffError({ + message: `Could not extract the ${options.name} API snapshot`, + cause + }) + ) + ) + let encoded: string | undefined + try { + encoded = JSON.stringify(snapshot) + } catch { + encoded = undefined + } + if (encoded !== undefined) { + yield* fs.makeDirectory(path.dirname(cacheLocation), { recursive: true }) + yield* fs.writeFileString(cacheLocation, encoded).pipe( + Effect.mapError((cause) => + new ApiDiffError({ + message: `Could not cache the ${options.name} API snapshot`, + cause + }) + ) + ) + } + return snapshot + }), + () => removeWorktree(options.repoRoot, worktree) + ) + })) + }) + + const prepareSnapshot = (options: PrepareSnapshotOptions): Effect.Effect => + prepareSnapshotInternal(options).pipe( + Effect.mapError((cause) => + isApiDiffError(cause) + ? cause + : new ApiDiffError({ + message: `Could not prepare the ${options.name} API snapshot`, + cause + }) + ) + ) + + return Worktrees.of({ + prepareSnapshot, + resolveRef + }) + }) + ) + + static readonly layer = this.layerNoDependencies.pipe( + Layer.provide(Snapshotter.layer) + ) +} diff --git a/.context/effect/packages/tools/api-diff/src/bin.ts b/.context/effect/packages/tools/api-diff/src/bin.ts new file mode 100644 index 000000000..4455c3f21 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/src/bin.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env node +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as NodeServices from "@effect/platform-node/NodeServices" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Command from "effect/unstable/cli/Command" +import PackageJson from "../package.json" with { type: "json" } +import { ApiDiff } from "./ApiDiff.ts" +import { cli } from "./Cli.ts" + +const MainLayer = ApiDiff.layer.pipe( + Layer.provideMerge(NodeServices.layer) +) + +Command.run(cli, { version: PackageJson.version }).pipe( + Effect.provide(MainLayer), + NodeRuntime.runMain +) diff --git a/.context/effect/packages/tools/api-diff/test/Annotations.test.ts b/.context/effect/packages/tools/api-diff/test/Annotations.test.ts new file mode 100644 index 000000000..97cf90f65 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Annotations.test.ts @@ -0,0 +1,59 @@ +import { loadAnnotations } from "@effect/api-diff/Annotations" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" + +describe("migration annotations", () => { + it.effect("loads and merges YAML files", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-annotations-" }) + yield* fs.writeFileString( + path.join(root, "Effect.yaml"), + [ + "effect/Effect#async:", + " replacement: Effect.callback", + " note: Rename the callback constructor.", + " example: Effect.callback((resume) => resume(Effect.void))", + "" + ].join("\n") + ) + yield* fs.writeFileString( + path.join(root, "Stream.yaml"), + [ + "effect/Stream#async:", + " replacement: Stream.callback", + " note: Rename the stream constructor.", + "" + ].join("\n") + ) + + const annotations = yield* loadAnnotations(root) + + assert.deepStrictEqual([...annotations], [ + ["effect/Effect#async", { + replacement: "Effect.callback", + note: "Rename the callback constructor.", + example: "Effect.callback((resume) => resume(Effect.void))" + }], + ["effect/Stream#async", { + replacement: "Stream.callback", + note: "Rename the stream constructor." + }] + ]) + }).pipe(Effect.provide(NodeServices.layer))) + + it.effect("treats a missing directory as no annotations", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-annotations-" }) + + const annotations = yield* loadAnnotations(path.join(root, "missing")) + + assert.strictEqual(annotations.size, 0) + }).pipe(Effect.provide(NodeServices.layer))) +}) diff --git a/.context/effect/packages/tools/api-diff/test/Cli.test.ts b/.context/effect/packages/tools/api-diff/test/Cli.test.ts new file mode 100644 index 000000000..568ba25b8 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Cli.test.ts @@ -0,0 +1,111 @@ +import { cli } from "@effect/api-diff" +import { ApiDiff } from "@effect/api-diff/ApiDiff" +import type { ApiEntity, ApiSnapshot } from "@effect/api-diff/Model" +import { Worktrees } from "@effect/api-diff/Worktrees" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as Command from "effect/unstable/cli/Command" + +const snapshot = (ref: string, sha: string, entities: ReadonlyArray = []): ApiSnapshot => ({ + version: 1, + compiler: { name: "typescript", version: "fixture" }, + ref, + sha, + packages: [], + entrypoints: [], + entities, + diagnostics: [] +}) + +const WorktreesTest = Layer.succeed( + Worktrees, + Worktrees.of({ + resolveRef: (_repoRoot, ref) => Effect.succeed(ref.repeat(40).slice(0, 40)), + prepareSnapshot: (options) => Effect.succeed(snapshot(options.ref, options.sha)) + }) +) + +const MainLayer = ApiDiff.layerNoDependencies.pipe( + Layer.provide(WorktreesTest), + Layer.provideMerge(NodeServices.layer) +) + +const removed: ApiEntity = { + id: "effect/Effect#removed#value", + packageName: "effect", + module: "effect/Effect", + path: ["removed"], + bucket: "value", + declarationKind: "variable", + importRoutes: [{ module: "effect/Effect", path: ["removed"] }], + declarations: [{ kind: "variable", name: "removed", type: { kind: "primitive", name: "string" } }], + displaySignature: "declare const removed: string", + fingerprint: "removed", + documentation: { stability: "stable" }, + source: { file: "Effect.d.ts", line: 1, column: 1 } +} + +const CheckLayer = ApiDiff.layerNoDependencies.pipe( + Layer.provide(Layer.succeed( + Worktrees, + Worktrees.of({ + resolveRef: (_repoRoot, ref) => Effect.succeed(ref.repeat(40).slice(0, 40)), + prepareSnapshot: (options) => + Effect.succeed(snapshot(options.ref, options.sha, options.name === "base" ? [removed] : [])) + }) + )), + Layer.provideMerge(NodeServices.layer) +) + +it.effect("compares refs without a migration map", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-cli-" }) + const output = path.join(root, "output") + + yield* Command.runWith(cli, { version: "0.0.0" })([ + "--base-ref", + "a", + "--head-ref", + "b", + "--output", + output + ]) + + const diff = JSON.parse(yield* fs.readFileString(path.join(output, "diff.json"))) + assert.deepStrictEqual(diff.base, { ref: "a", sha: "a".repeat(40) }) + assert.deepStrictEqual(diff.head, { ref: "b", sha: "b".repeat(40) }) + }).pipe(Effect.provide(MainLayer))) + +it.effect("writes the migration document with default refs", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-cli-" }) + const document = path.join(root, "v3-to-v4.md") + yield* fs.writeFileString(document, "# Existing\n\n## Import Map\n\nKeep this map.\n\n## API Renames\n") + + yield* Command.runWith(cli, { version: "0.0.0" })([ + "--write-doc", + document + ]) + + const source = yield* fs.readFileString(document) + assert(source.includes(`Base: \`v3\` (\`${"v3".repeat(40).slice(0, 40)}\`)`)) + assert(source.includes(`Head: \`main\` (\`${"origin/main".repeat(40).slice(0, 40)}\`)`)) + assert(source.includes("## Import Map\n\nKeep this map.")) + assert(source.includes("## API Reference")) + }).pipe(Effect.provide(MainLayer))) + +it.effect("fails check mode when guidance is missing", () => + Effect.gen(function*() { + const result = yield* Effect.exit(Command.runWith(cli, { version: "0.0.0" })(["--check"])) + + assert(Exit.isFailure(result)) + }).pipe(Effect.provide(CheckLayer))) diff --git a/.context/effect/packages/tools/api-diff/test/Diff.test.ts b/.context/effect/packages/tools/api-diff/test/Diff.test.ts new file mode 100644 index 000000000..c06f644b2 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Diff.test.ts @@ -0,0 +1,219 @@ +import { diffSnapshots } from "@effect/api-diff/Diff" +import type { ApiDiff, ApiEntity, ApiSnapshot, DeclarationModel } from "@effect/api-diff/Model" +import { renderMarkdownReport } from "@effect/api-diff/Report" +import { assert, describe, it } from "@effect/vitest" + +const entity = ( + module: string, + name: string, + declaration: DeclarationModel, + hash: string +): ApiEntity => ({ + id: `${module}#${name}#value`, + packageName: module.split("/")[0]!, + module, + path: [name], + bucket: "value", + declarationKind: declaration.kind, + importRoutes: [{ module, path: [name] }], + declarations: [declaration], + displaySignature: `declare const ${name}: unknown`, + fingerprint: hash, + documentation: { stability: "stable" }, + source: { file: `${name}.d.ts`, line: 1, column: 1 } +}) + +const snapshot = (ref: string, entities: ReadonlyArray): ApiSnapshot => ({ + version: 1, + compiler: { name: "typescript", version: "fixture" }, + ref, + sha: ref.repeat(40).slice(0, 40), + packages: ["old", "new"], + entrypoints: [], + entities, + diagnostics: [] +}) + +describe("snapshot diff", () => { + it("matches renames, classifies signature changes, and separates suggestions", () => { + const base = snapshot("a", [ + entity("old/A", "changed", { + kind: "function", + name: "changed", + parameters: [{ name: "value", type: { kind: "primitive", name: "string" }, optional: false, rest: false }], + returnType: { kind: "primitive", name: "string" } + }, "before"), + entity("old/A", "similarName", { + kind: "variable", + name: "similarName", + type: { kind: "primitive", name: "string" } + }, "x"), + entity( + "old/A", + "removed", + { kind: "variable", name: "removed", type: { kind: "primitive", name: "string" } }, + "r" + ) + ]) + const head = snapshot("b", [ + entity("old/A", "changed", { + kind: "function", + name: "changed", + parameters: [ + { name: "value", type: { kind: "primitive", name: "string" }, optional: false, rest: false }, + { name: "count", type: { kind: "primitive", name: "number" }, optional: true, rest: false } + ], + returnType: { kind: "primitive", name: "number" } + }, "different"), + entity("old/A", "similarNames", { + kind: "variable", + name: "similarNames", + type: { kind: "primitive", name: "number" } + }, "y"), + entity("old/A", "added", { kind: "variable", name: "added", type: { kind: "primitive", name: "string" } }, "a") + ]) + const diff = diffSnapshots(base, head) + assert(diff.changes.some((change) => change.classification === "parameter-added")) + assert(diff.changes.some((change) => change.classification === "return-type-changed")) + assert(diff.changes.some((change) => change.baseApiId?.includes("similarName") && !change.authoritative)) + assert( + diff.changes.some((change) => + change.classification === "api-removed" && change.baseApiId?.includes("similarName") + ) + ) + assert( + diff.changes.some((change) => change.classification === "api-added" && change.headApiId?.includes("similarNames")) + ) + const report = renderMarkdownReport(diff) + assert(report.includes("Suggested replacements for removed APIs")) + assert(report.includes(base.sha)) + assert.deepStrictEqual(diff, diffSnapshots(base, head)) + }) + + it("suggests replacements across modules and preserves class facets", () => { + const variable = (name: string): DeclarationModel => ({ + kind: "variable", + name, + type: { kind: "primitive", name: "unknown" } + }) + const serviceInterface = (name: string, extraMember?: string): DeclarationModel => ({ + kind: "interface", + name, + members: [ + { + kind: "method", + name: "context", + parameters: [], + returnType: { kind: "primitive", name: "unknown" } + }, + { + kind: "method", + name: "of", + parameters: [], + returnType: { kind: "primitive", name: "unknown" } + }, + ...(extraMember === undefined + ? [] + : [{ + kind: "method", + name: extraMember, + parameters: [], + returnType: { kind: "primitive", name: "unknown" } + }]) + ] + }) + const withBucket = (api: ApiEntity, bucket: ApiEntity["bucket"]): ApiEntity => ({ + ...api, + id: `${api.module}#${api.path.join(".")}#${bucket}`, + bucket + }) + const effectService = { + ...entity("effect/Effect", "Service", variable("Service"), "effect-service"), + documentation: { + summary: "Creates a Context Tag and Layer for a service.", + stability: "stable" as const + } + } + const contextTagValue = entity("effect/Context", "Tag", variable("Tag"), "tag-value") + const contextTagType = withBucket( + entity("effect/Context", "Tag", serviceInterface("Tag"), "tag-type"), + "type" + ) + const contextServiceValue = entity("effect/Context", "Service", variable("Service"), "service-value") + const contextServiceType = withBucket( + entity("effect/Context", "Service", serviceInterface("Service", "use"), "service-type"), + "type" + ) + const layerMapService = entity("effect/LayerMap", "Service", variable("Service"), "layer-map-service") + const diff = diffSnapshots( + snapshot("a", [effectService, contextTagType, contextTagValue]), + snapshot("b", [contextServiceType, contextServiceValue, layerMapService]) + ) + const suggestions = diff.changes.filter((change) => !change.authoritative) + assert(suggestions.some((change) => + change.baseApiId === "effect/Effect#Service#value" && + change.headApiId === "effect/Context#Service#value" + )) + assert(suggestions.some((change) => + change.baseApiId === "effect/Context#Tag#type" && + change.headApiId === "effect/Context#Service#type" + )) + assert(suggestions.some((change) => + change.baseApiId === "effect/Context#Tag#value" && + change.headApiId === "effect/Context#Service#value" + )) + assert(!suggestions.some((change) => change.headApiId === "effect/LayerMap#Service#value")) + assert.strictEqual(diff.changes.filter((change) => change.classification === "api-removed").length, 3) + assert.strictEqual(diff.changes.filter((change) => change.classification === "api-added").length, 3) + }) + + it("classifies overload and parameter reordering", () => { + const signature = (name: string, parameters: ReadonlyArray<"left" | "right">): DeclarationModel => ({ + kind: "function", + name, + parameters: parameters.map((parameter) => ({ + name: parameter, + type: { kind: "primitive", name: "string" }, + optional: false, + rest: false + })), + returnType: { kind: "primitive", name: "string" } + }) + const before = entity("old/A", "ordered", signature("ordered", ["left", "right"]), "before") + const after = entity("old/A", "ordered", signature("ordered", ["right", "left"]), "after") + const diff = diffSnapshots( + snapshot("a", [before]), + snapshot("b", [after]) + ) + assert(diff.changes.some((change) => change.classification === "parameter-reordered")) + }) + + it("groups package and module changes by their delta names", () => { + const report = renderMarkdownReport( + { + version: 1, + base: { ref: "a", sha: "a".repeat(40) }, + head: { ref: "b", sha: "b".repeat(40) }, + changes: [ + { + id: "package-removed", + classification: "package-removed", + confidence: 1, + delta: { packageName: "@effect/old" }, + authoritative: true + }, + { + id: "module-added", + classification: "module-added", + confidence: 1, + delta: { to: ["effect/New"] }, + authoritative: true + } + ] + } satisfies ApiDiff + ) + assert(report.includes("| @effect/old | @effect/old | 1 |")) + assert(report.includes("| stable | effect/New | 1 |")) + assert(!report.includes("")) + }) +}) diff --git a/.context/effect/packages/tools/api-diff/test/Discovery.test.ts b/.context/effect/packages/tools/api-diff/test/Discovery.test.ts new file mode 100644 index 000000000..0586fa66b --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Discovery.test.ts @@ -0,0 +1,62 @@ +import { Discovery } from "@effect/api-diff/Discovery" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import { writeFixturePackage } from "./utils.ts" + +const MainLayer = Discovery.layer.pipe( + Layer.provideMerge(NodeServices.layer) +) + +describe("entrypoint discovery", () => { + it.effect("expands wildcards and respects null exclusions", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const discovery = yield* Discovery + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-discovery-" }) + yield* writeFixturePackage(root, { + "index.d.ts": `export * as Foo from "./Foo.js"\n`, + "Foo.d.ts": `export declare const value: string\n`, + "internal/Secret.d.ts": `export declare const secret: string\n` + }) + + const result = yield* discovery.discoverEntrypoints(root, [ + "@fixture/sample", + "@fixture/sample/Foo", + "@fixture/sample/internal/Secret" + ]) + assert.deepStrictEqual(result.entrypoints.map((entrypoint) => entrypoint.module), [ + "@fixture/sample", + "@fixture/sample/Foo" + ]) + assert.deepStrictEqual(result.missing, ["@fixture/sample/internal/Secret"]) + + const all = yield* discovery.discoverEntrypoints(root) + assert.deepStrictEqual(all.entrypoints.map((entrypoint) => entrypoint.module), [ + "@fixture/sample", + "@fixture/sample/Foo", + "@fixture/sample/index" + ]) + assert.deepStrictEqual(all.missing, []) + }).pipe(Effect.provide(MainLayer))) + + it.effect("supports conditional export targets", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const discovery = yield* Discovery + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-discovery-" }) + yield* writeFixturePackage(root, { + "Foo.d.ts": `export declare const value: string\n` + }, { + "./Foo": { + types: "./Foo.d.ts", + import: "./Foo.js" + } + }) + const result = yield* discovery.discoverEntrypoints(root, ["@fixture/sample/Foo"]) + assert.strictEqual(result.missing.length, 0) + assert.strictEqual(result.entrypoints[0]?.module, "@fixture/sample/Foo") + }).pipe(Effect.provide(MainLayer))) +}) diff --git a/.context/effect/packages/tools/api-diff/test/Error.test.ts b/.context/effect/packages/tools/api-diff/test/Error.test.ts new file mode 100644 index 000000000..d9b5e6433 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Error.test.ts @@ -0,0 +1,25 @@ +import { ApiDiffError, isApiDiffError } from "@effect/api-diff/Error" +import { isSnapshotExtractionError, SnapshotExtractionError } from "@effect/api-diff/Snapshot" +import { assert, describe, it } from "@effect/vitest" +import { vi } from "vitest" +import type * as ApiDiffErrorModule from "../src/Error.ts" +import type * as SnapshotModule from "../src/Snapshot.ts" + +describe("tagged errors", () => { + it("recognizes errors from a reloaded module copy", async () => { + vi.resetModules() + const ForeignApiDiffError = await vi.importActual("../src/Error.ts") + const ForeignSnapshot = await vi.importActual("../src/Snapshot.ts") + + const apiDiffError = new ForeignApiDiffError.ApiDiffError({ message: "boom" }) + assert.isFalse(apiDiffError instanceof ApiDiffError) + assert.isTrue(isApiDiffError(apiDiffError)) + + const snapshotError = new ForeignSnapshot.SnapshotExtractionError({ + message: "boom", + diagnostics: [] + }) + assert.isFalse(snapshotError instanceof SnapshotExtractionError) + assert.isTrue(isSnapshotExtractionError(snapshotError)) + }) +}) diff --git a/.context/effect/packages/tools/api-diff/test/MigrationDoc.test.ts b/.context/effect/packages/tools/api-diff/test/MigrationDoc.test.ts new file mode 100644 index 000000000..89f1e53e9 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/MigrationDoc.test.ts @@ -0,0 +1,364 @@ +import type { MigrationAnnotation } from "@effect/api-diff/Annotations" +import { + extractImportMapSections, + markdownSafetyIssues, + renderMigrationDocument, + renderMissingAnnotations +} from "@effect/api-diff/MigrationDoc" +import type { ApiChange, ApiDiff } from "@effect/api-diff/Model" +import { assert, describe, it } from "@effect/vitest" + +const change = (change: Partial & Pick): ApiChange => ({ + id: `change-${change.classification}-${change.baseApiId ?? change.headApiId}`, + confidence: 1, + authoritative: true, + ...change +}) + +const diff: ApiDiff = { + version: 1, + base: { ref: "v3", sha: "a".repeat(40) }, + head: { ref: "main", sha: "b".repeat(40) }, + changes: [ + change({ + classification: "parameter-removed", + baseApiId: "effect/Zeta#changed#value", + headApiId: "effect/Zeta#changed#value", + before: "declare const changed: (value: string) => string", + after: "declare const changed: () => string" + }), + change({ + classification: "api-renamed", + baseApiId: "effect/Alpha#oldName#value", + headApiId: "effect/Alpha#newName#value" + }), + change({ classification: "api-removed", baseApiId: "effect/Alpha#oldName#value" }), + change({ + classification: "api-removed", + baseApiId: "effect/Beta#removed#value", + before: "declare const removed: string" + }), + change({ + classification: "api-moved", + baseApiId: "effect/Legacy#keptName#value", + headApiId: "effect/Current#keptName#value" + }), + change({ classification: "api-removed", baseApiId: "effect/Legacy#keptName#value" }), + change({ classification: "documentation-changed", baseApiId: "effect/Zeta#documented#value" }), + change({ + classification: "parameter-added", + baseApiId: "effect/Zeta#optional#value", + headApiId: "effect/Zeta#optional#value", + delta: { + before: [], + after: [{ name: "options", type: { kind: "primitive", name: "string" }, optional: true, rest: false }] + } + }), + change({ classification: "api-added", headApiId: "effect/Zeta#added#value" }) + ] +} + +const annotations = new Map([ + ["effect/Alpha#oldName", { + replacement: "Alpha.newName", + note: "Use the renamed function." + }] +]) + +describe("migration document", () => { + it("renders the selected API changes deterministically", () => { + const document = renderMigrationDocument( + diff, + annotations, + "## Import Map\n\n```text\neffect/Alpha -> effect/Alpha\n```\n" + ) + + assert.strictEqual( + document, + [ + "", + "", + "# v3 to v4 Migration Reference", + "", + `Base: \`v3\` (\`${"a".repeat(40)}\`)`, + "", + `Head: \`main\` (\`${"b".repeat(40)}\`)`, + "", + "This file is generated from the API diff and `migration/annotations/*.yaml`.", + "", + "## Import Map", + "", + "```text", + "effect/Alpha -> effect/Alpha", + "```", + "", + "## Removed Modules", + "", + "No modules were removed.", + "", + "## API Reference", + "", + "### `effect/Alpha`", + "", + "- `Alpha.oldName` -> `Alpha.newName`: Use the renamed function.", + "", + "### `effect/Beta`", + "", + "- `Beta.removed`: TODO: needs guidance", + "", + "### `effect/Zeta`", + "", + "- `Zeta.changed`: TODO: needs guidance", + "" + ].join("\n") + ) + assert(!document.includes("keptName")) + assert(!document.includes("documented")) + assert(!document.includes("optional")) + assert(!document.includes("added")) + }) + + it("lists unannotated ids grouped by module", () => { + assert.strictEqual( + renderMissingAnnotations(diff, annotations), + "effect/Beta:\n - effect/Beta#removed\neffect/Zeta:\n - effect/Zeta#changed\n" + ) + }) + + it("renders guidance and examples for detailed changes", () => { + const document = renderMigrationDocument( + diff, + new Map([...annotations, ["effect/Zeta#changed", { + replacement: "Zeta.changed()", + note: "Call the zero-argument form.", + example: "Zeta.changed()" + }]]), + "## Import Map\n" + ) + + assert(document.includes("**Replacement:** `Zeta.changed()`")) + assert(document.includes("Call the zero-argument form.")) + assert(document.includes("**Example**\n\n```ts\nZeta.changed()\n```")) + assert(!document.includes("**Before**")) + assert(!document.includes("**After**")) + }) + + it("escapes annotation prose while preserving inline code and fenced examples", () => { + const document = renderMigrationDocument( + diff, + new Map([...annotations, ["effect/Zeta#changed", { + replacement: "Zeta.changed()", + note: "Use ~effect/Zeta with Effect, *literal*, snake_case, `Context.Context`, and a bare ` marker.", + example: "const marker = `~effect/Zeta<*>`" + }]]), + "## Import Map\n" + ) + + assert(document.includes( + "Use \\~effect/Zeta with Effect\\, \\*literal\\*, snake\\_case, `Context.Context`, and a bare \\` marker." + )) + assert(document.includes("```ts\nconst marker = `~effect/Zeta<*>`\n```")) + assert.deepStrictEqual(markdownSafetyIssues(document), []) + assert.deepStrictEqual(markdownSafetyIssues("Unsafe ~~note and Effect"), [ + "line 1: unescaped \"~\"", + "line 1: unescaped \"~\"", + "line 1: unescaped \"<\"" + ]) + }) + + it("retains annotated removals that also have move suggestions", () => { + const serviceDiff: ApiDiff = { + ...diff, + changes: [ + change({ + classification: "api-moved", + baseApiId: "effect/Effect#Service#value", + headApiId: "effect/Context#Service#value", + authoritative: false + }), + change({ + classification: "api-removed", + baseApiId: "effect/Effect#Service#value", + before: "declare const Service: unknown" + }) + ] + } + const document = renderMigrationDocument( + serviceDiff, + new Map([["effect/Effect#Service", { + replacement: "Context.Service", + note: "Use the v4 service constructor." + }]]), + "## Import Map\n" + ) + + assert(document.includes("- `Effect.Service` -> `Context.Service`: Use the v4 service constructor.")) + }) + + it("renders removed modules and omits unchanged APIs moved with them", () => { + const moduleDiff: ApiDiff = { + ...diff, + changes: [ + change({ classification: "module-removed", delta: { from: "effect/Legacy", to: [] } }), + change({ + classification: "api-removed", + baseApiId: "effect/Legacy#unchanged#value", + before: "declare const unchanged: string" + }), + change({ + classification: "api-added", + headApiId: "effect/Current#unchanged#value", + after: "declare const unchanged: string" + }), + change({ + classification: "api-removed", + baseApiId: "effect/Legacy#changed#value", + before: "declare const changed: string" + }), + change({ + classification: "api-added", + headApiId: "effect/Current#changed#value", + after: "declare const changed: number" + }) + ] + } + const document = renderMigrationDocument( + moduleDiff, + new Map([["effect/Legacy#changed", { + replacement: "Current.changed", + note: "Use the changed API." + }]]), + "## Import Map\n\neffect/Legacy -> effect/Current\n" + ) + + assert(document.includes("## Removed Modules\n\n- `effect/Legacy` -> `effect/Current`")) + assert(!document.includes("Legacy.unchanged")) + assert(document.includes("- `Legacy.changed` -> `Current.changed`: Use the changed API.")) + assert.strictEqual( + extractImportMapSections(document), + "## Import Map\n\neffect/Legacy -> effect/Current\n" + ) + }) + + it("uses exact removed-module guidance for APIs without replacements", () => { + const moduleDiff: ApiDiff = { + ...diff, + changes: [ + change({ classification: "module-removed", delta: { from: "effect/Legacy", to: [] } }), + change({ + classification: "api-removed", + baseApiId: "effect/Legacy#removed#value", + before: "declare const removed: string" + }), + change({ + classification: "api-removed", + baseApiId: "effect/Legacy#replaced#value", + before: "declare const replaced: string" + }), + change({ + classification: "api-removed", + baseApiId: "effect/LegacyExtra#removed#value", + before: "declare const removed: string" + }), + change({ + classification: "api-removed", + baseApiId: "effect/Current#removed#value", + before: "declare const removed: string" + }) + ] + } + const moduleAnnotations = new Map([ + ["effect/Legacy", { + replacement: "none", + note: "The module was removed." + }], + ["effect/Legacy#replaced", { + replacement: "Current.replaced", + note: "Use the replacement." + }], + ["effect/LegacyExtra#removed", { + replacement: "none", + note: "This similarly prefixed module still exists." + }], + ["effect/Current#removed", { + replacement: "none", + note: "This module still exists." + }] + ]) + const document = renderMigrationDocument(moduleDiff, moduleAnnotations, "## Import Map\n") + + assert(document.includes("- `effect/Legacy` -> `none`: The module was removed.")) + assert(!document.includes("Legacy.removed")) + assert(document.includes("- `Legacy.replaced` -> `Current.replaced`: Use the replacement.")) + assert(document.includes("- `LegacyExtra.removed` -> `none`: This similarly prefixed module still exists.")) + assert(document.includes("- `Current.removed` -> `none`: This module still exists.")) + assert.strictEqual( + renderMissingAnnotations(moduleDiff, moduleAnnotations), + "All migration APIs and removed modules have guidance.\n" + ) + }) + + it("keeps removals from relocated modules and modules without module guidance", () => { + const moduleDiff: ApiDiff = { + ...diff, + changes: [ + change({ classification: "module-removed", delta: { from: "effect/Relocated", to: [] } }), + change({ classification: "module-removed", delta: { from: "effect/Unguided", to: [] } }), + change({ classification: "api-removed", baseApiId: "effect/Relocated#removed#value" }), + change({ classification: "api-removed", baseApiId: "effect/Unguided#removed#value" }) + ] + } + const moduleAnnotations = new Map([ + ["effect/Relocated", { + replacement: "effect/Current", + note: "The module moved." + }], + ["effect/Relocated#removed", { + replacement: "none", + note: "This API was not moved." + }], + ["effect/Unguided#removed", { + replacement: "none", + note: "This API was removed." + }] + ]) + const document = renderMigrationDocument(moduleDiff, moduleAnnotations, "## Import Map\n") + + assert(document.includes("- `effect/Relocated` -> `effect/Current`: The module moved.")) + assert(document.includes("- `Relocated.removed` -> `none`: This API was not moved.")) + assert(document.includes("- `Unguided.removed` -> `none`: This API was removed.")) + assert.strictEqual( + renderMissingAnnotations(moduleDiff, moduleAnnotations), + "All migration APIs and removed modules have guidance.\n" + ) + }) + + it("uses per-API annotations for split modules and reports missing module guidance", () => { + const moduleDiff: ApiDiff = { + ...diff, + changes: [ + change({ classification: "module-removed", delta: { from: "effect/Split", to: [] } }), + change({ classification: "module-removed", delta: { from: "effect/Unknown", to: [] } }), + change({ + classification: "api-removed", + baseApiId: "effect/Split#removed#value", + before: "declare const removed: string" + }) + ] + } + const splitAnnotations = new Map([["effect/Split#removed", { + replacement: "Current.removed", + note: "Use the split replacement." + }]]) + const document = renderMigrationDocument(moduleDiff, splitAnnotations, "## Import Map\n") + + assert( + document.includes("- `effect/Split`: No single module replacement; follow the curated per-API guidance below.") + ) + assert(document.includes("- `effect/Unknown`: TODO: needs module guidance")) + assert.strictEqual( + renderMissingAnnotations(moduleDiff, splitAnnotations), + "Removed modules:\n - effect/Unknown\n" + ) + }) +}) diff --git a/.context/effect/packages/tools/api-diff/test/Snapshot.test.ts b/.context/effect/packages/tools/api-diff/test/Snapshot.test.ts new file mode 100644 index 000000000..4ba612941 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Snapshot.test.ts @@ -0,0 +1,130 @@ +import { SnapshotExtractionError, Snapshotter } from "@effect/api-diff/Snapshot" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import { writeFixturePackage } from "./utils.ts" + +const MainLayer = Snapshotter.layer.pipe( + Layer.provideMerge(NodeServices.layer) +) + +const source = ` +export declare function overloaded(value: A): A +export declare function overloaded(value: number, radix?: number): string + +export interface Parent { + readonly parent: string +} + +export interface Service extends Parent { + readonly value?: A + run(input: A): Promise + [key: \`item-\${string}\`]: unknown +} + +export declare class Client implements Service { + static readonly version: string + readonly parent: string + readonly value?: A + constructor(value?: A) + run(input: A): Promise +} + +export type Recursive = A | ReadonlyArray> +export type Conditional = A extends readonly [infer Head, ...infer Tail] ? Head | Tail[number] : never +export type Mapped = { readonly [K in keyof A as \`get\${Capitalize}\`]?: A[K] } +export type Imported = import("node:fs").PathLike +export declare const token: unique symbol + +export declare namespace merged { + const value: string + interface Options { + readonly enabled: boolean + } +} +export declare function merged(value: string): string +` + +describe("canonical snapshot", () => { + it.effect("extracts declarations, overloads, namespaces, re-exports, and canonical types deterministically", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const snapshotter = yield* Snapshotter + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-snapshot-" }) + yield* writeFixturePackage(root, { + "index.d.ts": `export * as Foo from "./Foo.js"\n`, + "Foo.d.ts": source + }) + + const options = { + repoRoot: root, + ref: "fixture", + sha: "0000000000000000000000000000000000000000", + modules: ["@fixture/sample", "@fixture/sample/Foo"] + } + const first = yield* snapshotter.extract(options) + const second = yield* snapshotter.extract(options) + assert.deepStrictEqual(first, second) + assert(first.entities.some((entity) => + entity.module === "@fixture/sample/Foo" && + entity.path.join(".") === "overloaded" && + entity.declarations.length === 2 + )) + const overloaded = first.entities.find((entity) => entity.id === "@fixture/sample/Foo#overloaded#value") + assert.deepStrictEqual(overloaded?.importRoutes, [ + { module: "@fixture/sample", path: ["Foo", "overloaded"] }, + { module: "@fixture/sample/Foo", path: ["overloaded"] } + ]) + const client = first.entities.filter((entity) => entity.path.join(".") === "Client") + assert.deepStrictEqual(client.map((entity) => entity.bucket), ["type", "value"]) + assert(first.entities.some((entity) => entity.path.join(".") === "merged.Options" && entity.bucket === "type")) + assert(first.entities.some((entity) => JSON.stringify(entity.declarations).includes("\"kind\":\"conditional\""))) + assert(first.entities.some((entity) => JSON.stringify(entity.declarations).includes("\"kind\":\"mapped\""))) + const imported = first.entities.find((entity) => entity.path.join(".") === "Imported") + assert(imported !== undefined, JSON.stringify(first.entities.map((entity) => entity.id))) + assert(JSON.stringify(imported.declarations).includes("\"externalPackage\":\"node:fs\"")) + }).pipe(Effect.provide(MainLayer))) + + it.effect("fails rather than silently omitting unsupported public declarations", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const snapshotter = yield* Snapshotter + const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-snapshot-" }) + yield* writeFixturePackage(root, { + "Bad.d.ts": ` +declare namespace Other {} +export declare namespace Bad { + export import Alias = Other +} +` + }) + const failure = yield* Effect.flip(snapshotter.extract({ + repoRoot: root, + ref: "fixture", + sha: "0000000000000000000000000000000000000000", + modules: ["@fixture/sample/Bad"] + })) + assert(failure instanceof SnapshotExtractionError) + assert(failure.diagnostics.some((diagnostic) => diagnostic.code === "unsupported-public-declaration")) + }).pipe(Effect.provide(MainLayer))) + + it.effect("normalizes union order in structural fingerprints", () => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const snapshotter = yield* Snapshotter + const leftRoot = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-snapshot-" }) + const rightRoot = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-snapshot-" }) + yield* writeFixturePackage(leftRoot, { "Choice.d.ts": "export type Choice = string | number\n" }) + yield* writeFixturePackage(rightRoot, { "Choice.d.ts": "export type Choice = number | string\n" }) + const extract = (repoRoot: string) => + snapshotter.extract({ + repoRoot, + ref: "fixture", + sha: "0000000000000000000000000000000000000000", + modules: ["@fixture/sample/Choice"] + }).pipe(Effect.map((snapshot) => snapshot.entities[0]?.fingerprint)) + assert.strictEqual(yield* extract(leftRoot), yield* extract(rightRoot)) + }).pipe(Effect.provide(MainLayer))) +}) diff --git a/.context/effect/packages/tools/api-diff/test/Worktrees.test.ts b/.context/effect/packages/tools/api-diff/test/Worktrees.test.ts new file mode 100644 index 000000000..87d0c8f12 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/Worktrees.test.ts @@ -0,0 +1,86 @@ +import type { ApiSnapshot } from "@effect/api-diff/Model" +import { Snapshotter } from "@effect/api-diff/Snapshot" +import { Worktrees } from "@effect/api-diff/Worktrees" +import { assert, it } from "@effect/vitest" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Path from "effect/Path" +import * as Sink from "effect/Sink" +import * as Stream from "effect/Stream" +import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" + +const snapshot = { + version: 1, + compiler: { name: "typescript", version: "fixture" }, + ref: "main", + sha: "a".repeat(40), + packages: [], + entrypoints: [], + entities: [], + diagnostics: [], + toJSON() { + throw new RangeError("Invalid string length") + } +} as ApiSnapshot + +const SnapshotterTest = Layer.succeed( + Snapshotter, + Snapshotter.of({ extract: () => Effect.succeed(snapshot) }) +) + +const ChildProcessSpawnerTest = Layer.succeed( + ChildProcessSpawner.ChildProcessSpawner, + ChildProcessSpawner.make(() => + Effect.succeed(ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(1), + stdin: Sink.drain, + stdout: Stream.empty, + stderr: Stream.empty, + all: Stream.empty, + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(0)), + isRunning: Effect.succeed(false), + kill: () => Effect.void, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + unref: Effect.succeed(Effect.void) + })) + ) +) + +it.effect("skips the cache write when a snapshot is too large to serialize", () => { + let writes = 0 + const FileSystemTest = FileSystem.layerNoop({ + exists: (location) => Effect.succeed(location.endsWith("tsconfig.base.json")), + makeDirectory: () => Effect.void, + makeTempDirectoryScoped: () => Effect.succeed("/worktrees/head-1"), + readFileString: () => Effect.succeed(`{"compilerOptions":{"stripInternal":true}}`), + writeFileString: () => + Effect.sync(() => { + writes++ + }) + }) + const MainLayer = Worktrees.layerNoDependencies.pipe( + Layer.provide(Layer.mergeAll( + FileSystemTest, + Path.layer, + SnapshotterTest, + ChildProcessSpawnerTest + )) + ) + + return Effect.gen(function*() { + const worktrees = yield* Worktrees + const result = yield* worktrees.prepareSnapshot({ + repoRoot: "/repo", + cacheRoot: "/cache", + worktreesRoot: "/worktrees", + name: "head", + ref: snapshot.ref, + sha: snapshot.sha + }) + + assert.strictEqual(result, snapshot) + assert.strictEqual(writes, 0) + }).pipe(Effect.provide(MainLayer)) +}) diff --git a/.context/effect/packages/tools/api-diff/test/utils.ts b/.context/effect/packages/tools/api-diff/test/utils.ts new file mode 100644 index 000000000..c296e4d89 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/test/utils.ts @@ -0,0 +1,37 @@ +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import * as Path from "effect/Path" + +export const writeFixturePackage = Effect.fnUntraced(function*( + repoRoot: string, + files: Readonly>, + exports: Readonly> = { + ".": "./index.js", + "./*": "./*.js", + "./internal/*": null + } +) { + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const root = path.join(repoRoot, "packages", "sample", "dist") + yield* fs.makeDirectory(root, { recursive: true }) + yield* fs.writeFileString( + path.join(root, "package.json"), + `${ + JSON.stringify( + { + name: "@fixture/sample", + version: "1.0.0", + exports + }, + null, + 2 + ) + }\n` + ) + for (const [name, source] of Object.entries(files)) { + const location = path.join(root, name) + yield* fs.makeDirectory(path.dirname(location), { recursive: true }) + yield* fs.writeFileString(location, source) + } +}) diff --git a/.context/effect/packages/tools/api-diff/tsconfig.json b/.context/effect/packages/tools/api-diff/tsconfig.json new file mode 100644 index 000000000..b512ffd91 --- /dev/null +++ b/.context/effect/packages/tools/api-diff/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/.context/effect/packages/tools/bundle/docgen.json b/.context/effect/packages/tools/bundle/docgen.json deleted file mode 100644 index 0685d6d3b..000000000 --- a/.context/effect/packages/tools/bundle/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["src/bin.ts"] -} diff --git a/.context/effect/packages/tools/bundle/fixtures/brand.ts b/.context/effect/packages/tools/bundle/fixtures/brand.ts index 97f6fda73..2f15b21d4 100644 --- a/.context/effect/packages/tools/bundle/fixtures/brand.ts +++ b/.context/effect/packages/tools/bundle/fixtures/brand.ts @@ -2,4 +2,4 @@ import * as Brand from "effect/Brand" import * as Schema from "effect/Schema" type Positive = number & Brand.Brand<"Positive"> -const Positive = Brand.check(Schema.isGreaterThan(0)) +export const Positive = Brand.check(Schema.isGreaterThan(0)) diff --git a/.context/effect/packages/tools/bundle/fixtures/differ.ts b/.context/effect/packages/tools/bundle/fixtures/differ.ts index 30166be43..fae164af8 100644 --- a/.context/effect/packages/tools/bundle/fixtures/differ.ts +++ b/.context/effect/packages/tools/bundle/fixtures/differ.ts @@ -1,9 +1,9 @@ import * as Schema from "effect/Schema" const schema = Schema.Struct({ - id: Schema.Number, - name: Schema.String, - price: Schema.Number + a: Schema.String, + b: Schema.optional(Schema.FiniteFromString), + c: Schema.Array(Schema.String) }) -Schema.toDifferJsonPatch(schema) +export const differ = Schema.toDifferJsonPatch(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/optic.ts b/.context/effect/packages/tools/bundle/fixtures/optic.ts index de04dc36d..2f0c786de 100644 --- a/.context/effect/packages/tools/bundle/fixtures/optic.ts +++ b/.context/effect/packages/tools/bundle/fixtures/optic.ts @@ -3,4 +3,4 @@ import * as Optic from "effect/Optic" type S = { readonly a: number } const optic = Optic.id().key("a") -optic.getResult({ a: 1 }) +export const result = optic.getResult({ a: 1 }) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-fromJsonSchemaDocument.ts b/.context/effect/packages/tools/bundle/fixtures/schema-fromJsonSchemaDocument.ts index fbde73041..78005b971 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-fromJsonSchemaDocument.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-fromJsonSchemaDocument.ts @@ -1,6 +1,6 @@ import * as SchemaRepresentation from "effect/SchemaRepresentation" -const doc = SchemaRepresentation.fromJsonSchemaDocument({ +export const schema = SchemaRepresentation.fromJsonSchemaDocument({ "dialect": "draft-2020-12", "schema": { "type": "object", @@ -12,5 +12,3 @@ const doc = SchemaRepresentation.fromJsonSchemaDocument({ }, "definitions": {} }) - -console.dir(doc, { depth: null }) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-representation-roundtrip.ts b/.context/effect/packages/tools/bundle/fixtures/schema-representation-roundtrip.ts index 4a66b9c46..a22278cce 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-representation-roundtrip.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-representation-roundtrip.ts @@ -7,10 +7,9 @@ const schema = Schema.toCodecJson(Schema.Struct({ c: Schema.Array(Schema.String) })) -const json = Schema.encodeSync(SchemaRepresentation.DocumentFromJson)( - SchemaRepresentation.fromAST(schema.ast) -) +const json = SchemaRepresentation.toJson(SchemaRepresentation.toRepresentation(schema.ast)) -SchemaRepresentation.toSchema( - Schema.decodeSync(SchemaRepresentation.DocumentFromJson)(JSON.parse(JSON.stringify(json))) +export const roundtrip = SchemaRepresentation.fromRepresentation( + SchemaRepresentation.fromJson(JSON.parse(JSON.stringify(json))), + { revivers: [Schema.isFiniteReviver] } ) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-string-transformation.ts b/.context/effect/packages/tools/bundle/fixtures/schema-string-transformation.ts index fcab3b277..305148efa 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-string-transformation.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-string-transformation.ts @@ -15,6 +15,6 @@ const schema = Schema.String.pipe(Schema.decodeTo( }) )) -Schema.decodeUnknownEffect(schema)({ a: "a", b: 1, c: ["c"] }).pipe( +Schema.decodeUnknownEffect(schema)("a").pipe( Effect.runFork ) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toArbitrary.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toArbitrary.ts new file mode 100644 index 000000000..191ad957d --- /dev/null +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toArbitrary.ts @@ -0,0 +1,9 @@ +import * as Schema from "effect/Schema" + +const schema = Schema.Struct({ + a: Schema.String, + b: Schema.optional(Schema.FiniteFromString), + c: Schema.Array(Schema.String) +}) + +export const arbitrary = Schema.toArbitrary(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toArbitraryLazy.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toArbitraryLazy.ts deleted file mode 100644 index 9c016cd66..000000000 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toArbitraryLazy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as Schema from "effect/Schema" - -const schema = Schema.Struct({ - a: Schema.String, - b: Schema.optional(Schema.FiniteFromString), - c: Schema.Array(Schema.String) -}) - -Schema.toArbitraryLazy(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toCodeDocument.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toCodeDocument.ts index a664245eb..f00de9532 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toCodeDocument.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toCodeDocument.ts @@ -7,6 +7,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -const representation = Schema.toRepresentation(schema) - -SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(representation)) +export const document = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toRepresentations([schema.ast])) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toCodecJson.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toCodecJson.ts index 1babb48bd..394831df7 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toCodecJson.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toCodecJson.ts @@ -6,4 +6,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -Schema.toCodecJson(schema) +export const codec = Schema.toCodecJson(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toEquivalence.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toEquivalence.ts index a6f470c3c..58ea7753f 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toEquivalence.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toEquivalence.ts @@ -6,4 +6,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -Schema.toEquivalence(schema) +export const equivalence = Schema.toEquivalence(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toFormatter.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toFormatter.ts index ee619b520..258b7a6aa 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toFormatter.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toFormatter.ts @@ -6,4 +6,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -Schema.toFormatter(schema) +export const formatter = Schema.toFormatter(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toJsonSchemaDocument.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toJsonSchemaDocument.ts index 53590fb95..33c7fd90c 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toJsonSchemaDocument.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toJsonSchemaDocument.ts @@ -6,4 +6,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -Schema.toJsonSchemaDocument(schema) +export const document = Schema.toJsonSchemaDocument(schema) diff --git a/.context/effect/packages/tools/bundle/fixtures/schema-toRepresentation.ts b/.context/effect/packages/tools/bundle/fixtures/schema-toRepresentation.ts index cf54a1012..63f72369b 100644 --- a/.context/effect/packages/tools/bundle/fixtures/schema-toRepresentation.ts +++ b/.context/effect/packages/tools/bundle/fixtures/schema-toRepresentation.ts @@ -6,4 +6,4 @@ const schema = Schema.Struct({ c: Schema.Array(Schema.String) }) -Schema.toRepresentation(schema) +export const representation = Schema.toRepresentation(schema) diff --git a/.context/effect/packages/tools/bundle/package.json b/.context/effect/packages/tools/bundle/package.json index 040c78d15..8ad9b7684 100644 --- a/.context/effect/packages/tools/bundle/package.json +++ b/.context/effect/packages/tools/bundle/package.json @@ -41,11 +41,11 @@ "@rollup/plugin-terser": "^1.0.0", "effect": "workspace:^", "glob": "^13.0.6", - "rollup": "^4.62.2", + "rollup": "^4.62.3", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-visualizer": "^7.0.1" }, "devDependencies": { - "@types/node": "^26.1.1" + "@types/node": "^26.1.2" } } diff --git a/.context/effect/packages/tools/bundle/tsconfig.fixtures.json b/.context/effect/packages/tools/bundle/tsconfig.fixtures.json index d96a29d40..e137b36da 100644 --- a/.context/effect/packages/tools/bundle/tsconfig.fixtures.json +++ b/.context/effect/packages/tools/bundle/tsconfig.fixtures.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["fixtures"], "compilerOptions": { @@ -8,6 +8,6 @@ }, "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/tools/bundle/tsconfig.json b/.context/effect/packages/tools/bundle/tsconfig.json index 885758118..eef9e2be3 100644 --- a/.context/effect/packages/tools/bundle/tsconfig.json +++ b/.context/effect/packages/tools/bundle/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": [], "references": [ diff --git a/.context/effect/packages/tools/bundle/tsconfig.src.json b/.context/effect/packages/tools/bundle/tsconfig.src.json index fd2e4d017..2499141ca 100644 --- a/.context/effect/packages/tools/bundle/tsconfig.src.json +++ b/.context/effect/packages/tools/bundle/tsconfig.src.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "compilerOptions": { @@ -8,6 +8,6 @@ }, "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/tools/bundle/vitest.config.ts b/.context/effect/packages/tools/bundle/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/tools/bundle/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/docgen/CHANGELOG.md b/.context/effect/packages/tools/docgen/CHANGELOG.md new file mode 100644 index 000000000..467ea3f76 --- /dev/null +++ b/.context/effect/packages/tools/docgen/CHANGELOG.md @@ -0,0 +1,285 @@ +# @effect/docgen + +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + - @effect/platform-node@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`545d876`](https://github.com/Effect-TS/effect/commit/545d8767648cbbbf1820361662c0c1e10768db6a), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`721b9f0`](https://github.com/Effect-TS/effect/commit/721b9f0d320e50f3e2324c2cd1f43c6643af3ca0), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6828](https://github.com/Effect-TS/effect/pull/6828) [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a) Thanks @tim-smart! - Use layered storage for Context, making `Context.add` O(1) and eliminating per-request service map clones in the HTTP servers. Docgen now omits `@internal` option properties from generated signatures. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Major Changes + +- [#6556](https://github.com/Effect-TS/effect/pull/6556) [`19eeda6`](https://github.com/Effect-TS/effect/commit/19eeda68bac6d65bd24b9a9ca30565f35dbae565) Thanks @fubhy! - Migrate `@effect/docgen` into the Effect monorepo and update it to Effect 4 while retaining existing behavior. + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + - @effect/platform-node@4.0.0-beta.102 + +## 0.5.2 + +### Patch Changes + +- 3b595aa: Remove duplicate logger + +## 0.5.1 + +### Patch Changes + +- 0b3b34e: Typecheck examples deeply nested within namespaces + +## 0.5.0 + +### Minor Changes + +- 8a0eb55: Support custom code fences when rendering examples + +## 0.4.7 + +### Patch Changes + +- e3ae139: Typecheck namespace examples + +## 0.4.6 + +### Patch Changes + +- fcd5649: Support examples enclosed in Extended Markdown [code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) +- 95f136e: Support deeply nested namespaces. + + Previously, the docgen would fail with a `[Markdown] Unsupported namespace nesting: 4` error. With this change all namespace headers at depth level 3 and above would be rendered using H4 elements. + +## 0.4.5 + +### Patch Changes + +- 8959440: Fixes the type checking and execution of examples on Windows + +## 0.4.4 + +### Patch Changes + +- 00ce7a0: upgrade ts-morph to 23.0.0 +- 5b888e5: srcDir and outDir fields in docgen.json are currently ignored. With this patch, they are taken into account + +## 0.4.3 + +### Patch Changes + +- 7add2b9: update dependencies + +## 0.4.2 + +### Patch Changes + +- 619a0e3: use @effect/markdown-toc instead of github dependency + +## 0.4.1 + +### Patch Changes + +- b9bfab0: add reporting of `tsc` and `tsx` errors, closes #66 + +## 0.4.0 + +### Minor Changes + +- 5fbec18: update effect + +### Patch Changes + +- 08e8347: use ConfigProvider to load configuration for docgen + +## 0.3.8 + +### Patch Changes + +- 8abf24f: Core: do not swallow examples errors + +## 0.3.7 + +### Patch Changes + +- 2573662: update effect + +## 0.3.6 + +### Patch Changes + +- d58b355: chore: add defaults to `schema.json` + +## 0.3.5 + +### Patch Changes + +- bcaf971: fix glob pattern on windows + +## 0.3.4 + +### Patch Changes + +- 4e72aee: Re-added schema.json + +## 0.3.3 + +### Patch Changes + +- 3ee6dd1: Improve error output when spawning child process fails +- 73a1d93: build with tsup + +## 0.3.2 + +### Patch Changes + +- 16fc976: Updated dependencies + +## 0.3.1 + +### Patch Changes + +- b799243: add `--no-examples` option + +## 0.3.0 + +### Minor Changes + +- e08edb1: Modernized and switched to a `tsc` and `tsx` based setup with support for `NodeNext` module resolution. + +## 0.2.1 + +### Patch Changes + +- 2677b9d: updated effect +- 743ce06: change theme default + +## 0.2.0 + +### Minor Changes + +- ecd00a5: update effect and add effect/platform-node dependency + +## 0.1.8 + +### Patch Changes + +- 5411c71: Support for parsing "export \* as namespace" + +## 0.1.7 + +### Patch Changes + +- b94de9f: add support for `export * from ...` + +## 0.1.6 + +### Patch Changes + +- 172ac81: Fix parsing regression caused by compilerOptions parsing + +## 0.1.5 + +### Patch Changes + +- 85301ea: Add support for resolving compilerOptions from tsconfig files + +## 0.1.4 + +### Patch Changes + +- 8be0092: add support for namespaces +- 8be0092: BugFix: remove stale modules from /docs folder + +## 0.1.3 + +### Patch Changes + +- d8006f3: patch markdown-toc to prevent duplicate links +- 514f73f: update to effect framework package + +## 0.1.2 + +### Patch Changes + +- 115b996: fix formatting of the \_config.yml output by docgen +- 115b996: upgrade dependencies + +## 0.1.1 + +### Patch Changes + +- 2a909a1: fix config handling + +## 0.1.0 + +### Minor Changes + +- eb5ef08: rename docs-ts.json to docgen.json + +## 0.0.3 + +### Patch Changes + +- d139f78: ignore internal classes + +## 0.0.2 + +### Patch Changes + +- 4e88501: fix shebang line + +## 0.0.1 + +### Patch Changes + +- 4faa066: add initial code diff --git a/.context/effect/packages/tools/docgen/LICENSE b/.context/effect/packages/tools/docgen/LICENSE new file mode 100644 index 000000000..f8f4392a0 --- /dev/null +++ b/.context/effect/packages/tools/docgen/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/.context/effect/packages/tools/docgen/README.md b/.context/effect/packages/tools/docgen/README.md new file mode 100644 index 000000000..a32c268b3 --- /dev/null +++ b/.context/effect/packages/tools/docgen/README.md @@ -0,0 +1,146 @@ +# @effect/docgen + +An opinionated documentation generator for Effect projects. + +## Installation + +```sh +npm install -D @effect/docgen@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/docgen) + +## Credits + +This library was inspired by the following projects: + +- [docs-ts](https://github.com/gcanti/docs-ts) + +## Setup + +1. (Optional) Add a `docgen.json` configuration file. + +```json +{ + "$schema": "node_modules/@effect/docgen/schema.json" +} +``` + +2. Add the following script to your `package.json` file: + +```json +{ + "scripts": { + "docgen": "docgen" + } +} +``` + +> [!WARNING] +> To use "@effect/docgen", Node.js v18 or above is required. + +### Example Configuration + +The `docgen.json` configuration file allows you to customize `docgen`'s behavior. Here's an example configuration: + +```json +{ + "exclude": ["src/internal/**/*.ts"], + "parseCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "paths": { + "@effect/": ["./src/index.js"], + "@effect//test/*": ["./test/*.js"], + "@effect//examples/*": ["./examples/*.js"], + "@effect//*": ["./src/*.js"] + } + }, + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "paths": { + "@effect/": ["../../src/index.js"], + "@effect//test/*": ["../../test/*.js"], + "@effect//examples/*": ["../../examples/*.js"], + "@effect//*": ["../../src/*.js"] + } + } +} +``` + +## Supported JSDoc Tags + +| Tag | Description | Default | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| `@category` | Groups associated module exports together in the generated documentation. | `'utils'` | +| `@example` | Allows usage examples to be provided for your source code. All examples are type checked using `tsc`. Examples are also run using `tsx` and the NodeJS [assert](https://nodejs.org/api/assert.html) module can be used for on-the-fly testing. | | +| `@since` | Allows for documenting most recent library version in which a given piece of source code was updated. | | +| `@deprecated` | Marks source code as deprecated, which will ~~strikethrough~~ the name of the annotated module or function in the generated documentation. | `false` | +| `@internal` | Prevents `docgen` from generating documentation for the annotated block of code. Additionally, if the `stripInternal` flag is set to `true` in `tsconfig.json`, TypeScript will not emit declarations for the annotated code. | | +| `@ignore` | Prevents `docgen` from generating documentation for the annotated block of code. | | + +By default, `docgen` will search for files in the `src` directory and will output generated files into a `docs` directory. For information on how to configure `docgen`, see the [Configuration](#configuration) section below. + +## Configuration + +`docgen` is meant to be a zero-configuration command-line tool by default. However, there are several configuration settings that can be specified for `docgen`. To customize the configuration of `docgen`, create a `docgen.json` file in the root directory of your project and indicate the custom configuration parameters that the tool should use when generating documentation. + +The `docgen.json` configuration file adheres to the following interface: + +```ts +interface Config { + readonly projectHomepage?: string + readonly srcLink?: string + readonly srcDir?: string + readonly outDir?: string + readonly theme?: string + readonly enableSearch?: boolean + readonly enforceDescriptions?: boolean + readonly enforceExamples?: boolean + readonly enforceVersion?: boolean + readonly tscExecutable?: string + readonly exclude?: ReadonlyArray + readonly parseCompilerOptions?: string | Record + readonly examplesCompilerOptions?: string | Record +} +``` + +The following table describes each configuration parameter, its purpose, and its default value. + +| Parameter | Description | Default Value | +| :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------- | +| projectHomepage | Will link to the project homepage from the [Auxiliary Links](https://pmarsceill.github.io/just-the-docs/docs/navigation-structure/#auxiliary-links) of the generated documentation. | `homepage` in `package.json` | +| srcLink | Will link to the project source code. | `{projectHomepage}/blob/main/src/` | +| srcDir | The directory in which `docgen` will search for TypeScript files to parse. | `'src'` | +| outDir | The directory to which `docgen` will generate its output markdown documents. | `'docs'` | +| theme | The theme that `docgen` will specify should be used for GitHub Docs in the generated `_config.yml` file. | `'mikearnaldi/just-the-docs'` | +| enableSearch | Whether or not search should be enabled for GitHub Docs in the generated `_config.yml` file. | `true` | +| enforceDescriptions | Whether or not descriptions for each module export should be required. | `false` | +| enforceExamples | Whether or not `@example` tags for each module export should be required. (**Note**: examples will not be enforced in module documentation) | `false` | +| enforceVersion | Whether or not `@since` tags for each module export should be required. | `true` | +| tscExecutable | The path to the TypeScript compiler executable that docgen should use when invoking the compiler programmatically. | `'tsc'` | +| exclude | An array of glob strings specifying files that should be excluded from the documentation. | `[]` | +| parseCompilerOptions | tsconfig for parsing options (or path to a tsconfig) | {} | +| examplesCompilerOptions | tsconfig for the examples options (or path to a tsconfig) | {} | + +## FAQ + +**Q:** For functions that have overloaded definitions, is it possible to document each overload separately? + +**A:** No, `docgen` will use the documentation provided for the first overload of a function in its generated output. + +## License + +The MIT License (MIT) diff --git a/.context/effect/packages/tools/docgen/package.json b/.context/effect/packages/tools/docgen/package.json new file mode 100644 index 000000000..5665c52d3 --- /dev/null +++ b/.context/effect/packages/tools/docgen/package.json @@ -0,0 +1,84 @@ +{ + "name": "@effect/docgen", + "version": "4.0.0-rc.108", + "type": "module", + "license": "MIT", + "description": "An opinionated documentation generator for Effect projects", + "engines": { + "node": ">=18.0.0" + }, + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/tools/docgen" + }, + "sideEffects": [], + "bin": { + "docgen": "./src/bin.ts" + }, + "exports": { + "./package.json": "./package.json", + "./schema.json": "./schema.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./index": null, + "./bin": null + }, + "files": [ + "src/**/*.ts", + "schema.json", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "bin": { + "docgen": "./dist/bin.js" + }, + "exports": { + "./package.json": "./package.json", + "./schema.json": "./schema.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./index": null, + "./bin": null + } + }, + "scripts": { + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@effect/markdown-toc": "^0.1.0", + "@effect/platform-node": "workspace:^", + "chalk": "^5.6.2", + "doctrine": "^3.0.0", + "effect": "workspace:^", + "glob": "^13.0.6", + "prettier": "^3.6.2", + "ts-morph": "^27.0.2", + "tsconfck": "^3.1.6" + }, + "peerDependencies": { + "tsx": ">=4.19.3 <5.0.0", + "typescript": ">=5.8.2 <7.0.0" + }, + "devDependencies": { + "@effect/vitest": "workspace:^", + "@types/babel__code-frame": "^7.0.6", + "@types/doctrine": "^0.0.9", + "@types/node": "^26.1.2", + "tsx": "^4.23.1", + "typescript": "^6.0.3", + "vitest": "^4.1.10" + } +} diff --git a/.context/effect/packages/tools/docgen/schema.json b/.context/effect/packages/tools/docgen/schema.json new file mode 100644 index 000000000..ac151cdec --- /dev/null +++ b/.context/effect/packages/tools/docgen/schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$defs": { + "ConfigurationSchema": { + "type": "object", + "required": [], + "properties": { + "$schema": { + "type": "string" + }, + "projectHomepage": { + "type": "string", + "description": "Will link to the project homepage from the Auxiliary Links of the generated documentation." + }, + "srcLink": { + "type": "string", + "description": "Will link to the project source code." + }, + "srcDir": { + "type": "string", + "description": "The directory in which docgen will search for TypeScript files to parse.", + "default": "src" + }, + "outDir": { + "type": "string", + "description": "The directory to which docgen will generate its output markdown documents.", + "default": "docs" + }, + "theme": { + "type": "string", + "description": "The theme that docgen will specify should be used for GitHub Docs in the generated _config.yml file.", + "default": "mikearnaldi/just-the-docs" + }, + "enableSearch": { + "type": "boolean", + "description": "Whether or not search should be enabled for GitHub Docs in the generated _config.yml file.", + "default": true + }, + "enforceDescriptions": { + "type": "boolean", + "description": "Whether or not descriptions for each module export should be required.", + "default": false + }, + "enforceExamples": { + "type": "boolean", + "description": "Whether or not @example tags for each module export should be required. (Note: examples will not be enforced in module documentation)", + "default": false + }, + "enforceVersion": { + "type": "boolean", + "description": "Whether or not @since tags for each module export should be required.", + "default": true + }, + "tscExecutable": { + "type": "string", + "description": "The path to the TypeScript compiler executable that docgen should use when invoking the compiler programmatically.", + "default": "tsc" + }, + "runExamples": { + "type": "boolean", + "description": "Whether or not docgen should attempt to run example code snippets and include the output in the generated documentation.", + "default": false + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "An array of glob strings specifying files that should be excluded from the documentation.", + "default": [] + }, + "parseCompilerOptions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { + "$id": "/schemas/unknown", + "title": "unknown" + } + } + ], + "description": "tsconfig for parsing options (or path to a tsconfig)", + "default": {} + }, + "examplesCompilerOptions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [], + "properties": {}, + "additionalProperties": { + "$id": "/schemas/unknown", + "title": "unknown" + } + } + ], + "description": "tsconfig for the examples options (or path to a tsconfig)", + "default": {} + } + }, + "additionalProperties": false + } + }, + "$ref": "#/$defs/ConfigurationSchema" +} diff --git a/.context/effect/packages/tools/docgen/src/CLI.ts b/.context/effect/packages/tools/docgen/src/CLI.ts new file mode 100644 index 000000000..7b2caa858 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/CLI.ts @@ -0,0 +1,244 @@ +#!/usr/bin/env node + +/** + * Command-line interface for generating Effect API documentation. + * + * @since 0.6.0 + */ +import * as Array from "effect/Array" +import * as Config from "effect/Config" +import * as Effect from "effect/Effect" +import * as Option from "effect/Option" +import * as Schema from "effect/Schema" +import * as CliError from "effect/unstable/cli/CliError" +import * as Command from "effect/unstable/cli/Command" +import * as Flag from "effect/unstable/cli/Flag" +import PackageJson from "../package.json" with { type: "json" } +import * as Configuration from "./Configuration.ts" +import * as Core from "./Core.ts" +import * as Domain from "./Domain.ts" + +const projectHomepage = Flag.string("homepage").pipe( + Flag.withFallbackConfig(Config.string("projectHomepage")), + Flag.withDescription( + "The link to the project homepage (will be shown in the Auxiliary Links of the generated documentation)" + ), + Flag.optional +) + +const srcLink = Flag.string("srcLink").pipe( + Flag.withFallbackConfig(Config.string("srcLink")), + Flag.withDescription("The link to the project source code"), + Flag.optional +) + +const srcDir = Flag.directory("src", { mustExist: true }).pipe( + Flag.withFallbackConfig(Config.string("src").pipe(Config.withDefault("src"))), + Flag.withDescription("The directory in which docgen will search for TypeScript files to parse") +) + +const outDir = Flag.directory("out").pipe( + Flag.withFallbackConfig(Config.string("out").pipe(Config.withDefault("docs"))), + Flag.withDescription("The directory to which docgen will generate its output markdown documents") +) + +const theme = Flag.string("theme").pipe( + Flag.withFallbackConfig(Config.string("theme").pipe(Config.withDefault(Configuration.DEFAULT_THEME))), + Flag.withDescription("The Jekyll theme that should be used for the generated documentation") +) + +const disableSearch = Flag.boolean("disable-search").pipe( + Flag.withDescription("Whether or not search should be enabled in the generated documentation"), + Flag.optional +) + +const enableSearchAlias = Flag.boolean("enable-search").pipe( + Flag.withDescription("Whether or not search should be enabled in the generated documentation"), + Flag.optional +) + +const enforceDescriptions = Flag.boolean("enforce-descriptions").pipe( + Flag.withDescription("Whether or not a description for each module export should be required"), + Flag.optional +) + +const enforceExamples = Flag.boolean("enforce-examples").pipe( + Flag.withDescription( + "Whether or not @example tags for each module export should be required " + + "(Note: examples will not be enforced in module documentation)" + ), + Flag.optional +) + +const noEnforceVersion = Flag.boolean("no-enforce-version").pipe( + Flag.withDescription("Whether or not @since tags for each module export should be required"), + Flag.optional +) + +const enforceVersionAlias = Flag.boolean("enforce-version").pipe( + Flag.withDescription("Whether or not @since tags for each module export should be required"), + Flag.optional +) + +const runExamples = Flag.boolean("run-examples").pipe( + Flag.withDescription("Whether or not to execute examples discovered in the TypeScript source files"), + Flag.optional +) + +const exclude = Flag.string("exclude").pipe( + Flag.between(0, Infinity), + Flag.withFallbackConfig( + Config.schema(Config.Array(Schema.String), "exclude").pipe( + Config.withDefault(Array.empty()) + ) + ), + Flag.withDescription( + "An array of glob patterns specifying files that should be excluded from the generated documentation" + ) +) + +const compilerOptionsSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)) + +const parseCompilerOptionsFlag = (name: string, description: string) => + Flag.string(name).pipe( + Flag.withDescription(description), + Flag.mapEffect((value) => + Schema.decodeUnknownEffect(compilerOptionsSchema)(value).pipe( + Effect.mapError((error) => + new CliError.InvalidValue({ + option: name, + value, + expected: `a JSON record (${globalThis.String(error)})`, + kind: "flag" + }) + ) + ) + ) + ) + +const parseCompilerOptionsFile = Flag.file("parse-tsconfig-file", { mustExist: true }).pipe( + Flag.withDescription("The TypeScript TSConfig file to use for parsing source files"), + Flag.optional +) + +const parseCompilerOptionsInline = parseCompilerOptionsFlag( + "parse-compiler-options", + "The TypeScript compiler options to use for parsing source files" +).pipe(Flag.optional) + +const examplesCompilerOptionsFile = Flag.file("examples-tsconfig-file", { mustExist: true }).pipe( + Flag.withDescription("The TypeScript TSConfig file to use for examples"), + Flag.optional +) + +const examplesCompilerOptionsInline = parseCompilerOptionsFlag( + "examples-compiler-options", + "The TypeScript compiler options to use for examples" +).pipe(Flag.optional) + +const options = { + projectHomepage, + srcLink, + srcDir, + outDir, + theme, + disableSearch, + enableSearchAlias, + enforceDescriptions, + enforceExamples, + noEnforceVersion, + enforceVersionAlias, + runExamples, + exclude, + parseCompilerOptionsFile, + parseCompilerOptionsInline, + examplesCompilerOptionsFile, + examplesCompilerOptionsInline +} + +/** @internal */ +export const docgenCommand = Command.make("docgen", options) + +/** @internal */ +export const loadConfiguration = Effect.fnUntraced(function*( + args: Command.Command.Config.Infer +) { + const { + enableSearchAlias, + disableSearch, + enforceDescriptions, + enforceExamples, + enforceVersionAlias, + noEnforceVersion, + runExamples, + examplesCompilerOptionsFile, + examplesCompilerOptionsInline, + parseCompilerOptionsFile, + parseCompilerOptionsInline, + ...config + } = args + if (Option.isSome(parseCompilerOptionsFile) && Option.isSome(parseCompilerOptionsInline)) { + return yield* new CliError.InvalidValue({ + option: "parse-compiler-options", + value: JSON.stringify(parseCompilerOptionsInline.value), + expected: "only one of --parse-tsconfig-file or --parse-compiler-options", + kind: "flag" + }) + } + if (Option.isSome(examplesCompilerOptionsFile) && Option.isSome(examplesCompilerOptionsInline)) { + return yield* new CliError.InvalidValue({ + option: "examples-compiler-options", + value: JSON.stringify(examplesCompilerOptionsInline.value), + expected: "only one of --examples-tsconfig-file or --examples-compiler-options", + kind: "flag" + }) + } + const configuredEnableSearch = yield* Config.boolean("enableSearch").pipe(Effect.orElseSucceed(() => true)) + const configuredEnforceDescriptions = yield* Config.boolean("enforceDescriptions").pipe( + Effect.orElseSucceed(() => false) + ) + const configuredEnforceExamples = yield* Config.boolean("enforceExamples").pipe( + Effect.orElseSucceed(() => false) + ) + const configuredEnforceVersion = yield* Config.boolean("enforceVersion").pipe(Effect.orElseSucceed(() => true)) + const configuredRunExamples = yield* Config.boolean("runExamples").pipe(Effect.orElseSucceed(() => false)) + return yield* Configuration.load({ + ...config, + enableSearch: Option.match(disableSearch, { + onNone: () => Option.getOrElse(enableSearchAlias, () => configuredEnableSearch), + onSome: (disabled) => !disabled + }), + enforceDescriptions: Option.getOrElse(enforceDescriptions, () => configuredEnforceDescriptions), + enforceExamples: Option.getOrElse(enforceExamples, () => configuredEnforceExamples), + enforceVersion: Option.match(noEnforceVersion, { + onNone: () => Option.getOrElse(enforceVersionAlias, () => configuredEnforceVersion), + onSome: (disabled) => !disabled + }), + runExamples: Option.getOrElse(runExamples, () => configuredRunExamples), + parseCompilerOptions: Option.orElse(parseCompilerOptionsFile, () => parseCompilerOptionsInline), + examplesCompilerOptions: Option.orElse(examplesCompilerOptionsFile, () => examplesCompilerOptionsInline) + }) +}) + +const command = docgenCommand.pipe( + Command.withHandler(() => + Effect.scoped(Core.program).pipe( + Effect.catchTag("DocgenError", (error) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + return yield* new Domain.DocgenError({ message: `[${config.projectName}] ${error.message}` }) + })) + ) + ), + Command.provideEffect(Configuration.Configuration, loadConfiguration) +) + +/** + * Runs the docgen command-line program. + * + * @category running + * @since 0.6.0 + */ +export const cli = Command.runWith(command, { + version: PackageJson["version"] +}) diff --git a/.context/effect/packages/tools/docgen/src/Checker.ts b/.context/effect/packages/tools/docgen/src/Checker.ts new file mode 100644 index 000000000..5c1455a34 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Checker.ts @@ -0,0 +1,248 @@ +/** + * Validates parsed documentation against the configured requirements. + * + * @since 0.6.0 + */ +import { codeFrameColumns } from "@babel/code-frame" +import * as Array from "effect/Array" +import * as Effect from "effect/Effect" +import * as Configuration from "./Configuration.ts" +import type * as Domain from "./Domain.ts" +import * as Parser from "./Parser.ts" + +const makeError = ( + source: Parser.SourceShape, + position: Domain.Position, + message: (filePath: string, frame: string) => string +) => { + const location = { start: position } + const frame = codeFrameColumns(source.sourceFile.getFullText(), location) + return [message(source.sourceFile.getFilePath(), frame)] +} + +type Entry = { + readonly doc: Domain.Doc + readonly position: Domain.Position +} + +function checkEntry(model: Entry, options: { + readonly enforceVersion: boolean +}) { + return Effect.gen(function*() { + const source = yield* Parser.Source + const config = yield* Configuration.Configuration + + let errors: Array = [] + + // description + if (config.enforceDescriptions) { + if (model.doc.description === undefined) { + errors = errors.concat(makeError( + source, + model.position, + (filePath, frame) => `Missing description in file ${filePath}:\n\n${frame}` + )) + } + } + + // @example tags + if (config.enforceExamples) { + if (model.doc.examples.length === 0) { + errors = errors.concat(makeError( + source, + model.position, + (filePath, frame) => `Missing examples in file ${filePath}:\n\n${frame}` + )) + } + } + + // @since tags + if (config.enforceVersion && options.enforceVersion !== false) { + const since = model.doc.since + if (since.length === 0) { + errors = errors.concat(makeError( + source, + model.position, + (filePath, frame) => `Missing \`@since\` tag in file ${filePath}:\n\n${frame}` + )) + } + } + + return errors + }) +} + +function checkEntries(models: ReadonlyArray, options: { + readonly enforceVersion: boolean +}) { + return Effect.forEach(models, (model) => checkEntry(model, options)).pipe(Effect.map(Array.flatten)) +} + +function checkFunction(model: Domain.Function) { + return checkEntry(model, { + enforceVersion: true + }) +} + +/** + * Validates documentation for function declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkFunctions(models: ReadonlyArray) { + return Effect.forEach(models, checkFunction).pipe(Effect.map(Array.flatten)) +} + +function checkClass(model: Domain.Class) { + return Effect.gen(function*() { + const docErrors = yield* checkEntry(model, { + enforceVersion: true + }) + const staticMethodsErrors = yield* checkEntries(model.staticMethods, { + enforceVersion: false + }) + const methodsErrors = yield* checkEntries(model.methods, { + enforceVersion: false + }) + const propertiesErrors = yield* checkEntries(model.properties, { + enforceVersion: false + }) + return Array.flatten([docErrors, staticMethodsErrors, methodsErrors, propertiesErrors]) + }) +} + +/** + * Validates documentation for class declarations and their documented members. + * + * @category validation + * @since 0.6.0 + */ +export function checkClasses(models: ReadonlyArray) { + return Effect.forEach(models, checkClass).pipe(Effect.map(Array.flatten)) +} + +function checkConstant(model: Domain.Constant) { + return checkEntry(model, { + enforceVersion: true + }) +} + +/** + * Validates documentation for constant declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkConstants(models: ReadonlyArray) { + return Effect.forEach(models, checkConstant).pipe(Effect.map(Array.flatten)) +} + +function checkInterface(model: Domain.Interface) { + return checkEntry(model, { + enforceVersion: true + }) +} + +/** + * Validates documentation for interface declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkInterfaces(models: ReadonlyArray) { + return Effect.forEach(models, checkInterface).pipe(Effect.map(Array.flatten)) +} + +function checkTypeAlias(model: Domain.TypeAlias) { + return checkEntry(model, { + enforceVersion: true + }) +} + +/** + * Validates documentation for type alias declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkTypeAliases(models: ReadonlyArray) { + return Effect.forEach(models, checkTypeAlias).pipe(Effect.map(Array.flatten)) +} + +function checkNamespace( + model: Domain.Namespace +): Effect.Effect, never, Parser.Source | Configuration.Configuration> { + return Effect.gen(function*() { + const docErrors = yield* checkEntry(model, { + enforceVersion: true + }) + const interfacesErrors = yield* checkInterfaces(model.interfaces) + const typeAliasesErrors = yield* checkTypeAliases(model.typeAliases) + const namespacesErrors = yield* checkNamespaces(model.namespaces) + return Array.flatten([docErrors, interfacesErrors, typeAliasesErrors, namespacesErrors]) + }) +} + +/** + * Validates documentation for namespaces and their nested declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkNamespaces(models: ReadonlyArray) { + return Effect.forEach(models, checkNamespace).pipe(Effect.map(Array.flatten)) +} + +function checkExport(model: Domain.Export) { + return checkEntry(model, { + enforceVersion: true + }) +} + +/** + * Validates documentation for explicit export declarations. + * + * @category validation + * @since 0.6.0 + */ +export function checkExports(models: ReadonlyArray) { + return Effect.forEach(models, checkExport).pipe(Effect.map(Array.flatten)) +} + +/** + * Validates every documented declaration in a parsed module. + * + * @category validation + * @since 0.6.0 + */ +export function checkModule(module: Domain.Module) { + return Effect.gen(function*() { + const functionsErrors = yield* checkFunctions(module.functions) + const classesErrors = yield* checkClasses(module.classes) + const constantsErrors = yield* checkConstants(module.constants) + const interfacesErrors = yield* checkInterfaces(module.interfaces) + const typeAliasesErrors = yield* checkTypeAliases(module.typeAliases) + const namespacesErrors = yield* checkNamespaces(module.namespaces) + const exportsErrors = yield* checkExports(module.exports) + return Array.flatten([ + functionsErrors, + classesErrors, + constantsErrors, + interfacesErrors, + typeAliasesErrors, + namespacesErrors, + exportsErrors + ]) + }).pipe(Effect.provideService(Parser.Source, module.source)) +} + +/** + * Validates every documented declaration in a collection of parsed modules. + * + * @category validation + * @since 0.6.0 + */ +export function checkModules(modules: ReadonlyArray) { + return Effect.forEach(modules, checkModule).pipe(Effect.map(Array.flatten)) +} diff --git a/.context/effect/packages/tools/docgen/src/Configuration.ts b/.context/effect/packages/tools/docgen/src/Configuration.ts new file mode 100644 index 000000000..19c45035d --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Configuration.ts @@ -0,0 +1,380 @@ +/** + * Loads and provides configuration for documentation generation. + * + * @since 0.6.0 + */ + +import * as Array from "effect/Array" +import * as Config from "effect/Config" +import * as ConfigProvider from "effect/ConfigProvider" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as FileSystem from "effect/FileSystem" +import { pipe } from "effect/Function" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Path from "effect/Path" +import * as Result from "effect/Result" +import * as Schema from "effect/Schema" +import * as tsconfck from "tsconfck" +import * as Domain from "./Domain.ts" +import { DocgenError } from "./Domain.ts" + +/** + * Default GitHub Pages theme written to generated configuration. + * + * @category constants + * @since 0.6.0 + */ +export const DEFAULT_THEME = "mikearnaldi/just-the-docs" + +const PACKAGE_JSON_FILE_NAME = "package.json" +const CONFIG_FILE_NAME = "docgen.json" + +const compilerOptionsSchema = Schema.Union([ + Schema.String, + Schema.Record(Schema.String, Schema.Unknown) +]) + +/** + * Schema for docgen configuration files. + * + * @category schemas + * @since 0.6.0 + */ +export const ConfigurationSchema = Schema.Struct({ + "$schema": Schema.optional(Schema.String), + projectHomepage: Schema.optional(Schema.String.annotate({ + description: "Will link to the project homepage from the Auxiliary Links of the generated documentation." + })), + srcLink: Schema.optional(Schema.String.annotate({ + description: "Will link to the project source code." + })), + srcDir: Schema.optional(Schema.String.annotate({ + description: "The directory in which docgen will search for TypeScript files to parse.", + default: "src" + })), + outDir: Schema.optional(Schema.String.annotate({ + description: "The directory to which docgen will generate its output markdown documents.", + default: "docs" + })), + theme: Schema.optional(Schema.String.annotate({ + description: "The theme that docgen will specify should be used for GitHub Docs in the generated _config.yml file.", + default: DEFAULT_THEME + })), + enableSearch: Schema.optional(Schema.Boolean.annotate({ + description: "Whether or not search should be enabled for GitHub Docs in the generated _config.yml file.", + default: true + })), + enforceDescriptions: Schema.optional(Schema.Boolean.annotate({ + description: "Whether or not descriptions for each module export should be required.", + default: false + })), + enforceExamples: Schema.optional(Schema.Boolean.annotate({ + description: + "Whether or not @example tags for each module export should be required. (Note: examples will not be enforced in module documentation)", + default: false + })), + enforceVersion: Schema.optional(Schema.Boolean.annotate({ + description: "Whether or not @since tags for each module export should be required.", + default: true + })), + tscExecutable: Schema.optional(Schema.String.annotate({ + description: + "The path to the TypeScript compiler executable that docgen should use when invoking the compiler programmatically.", + default: "tsc" + })), + runExamples: Schema.optional(Schema.Boolean.annotate({ + description: + "Whether or not docgen should attempt to run example code snippets and include the output in the generated documentation.", + default: false + })), + exclude: Schema.optional( + Schema.Array(Schema.String).annotate({ + description: "An array of glob strings specifying files that should be excluded from the documentation.", + default: [] + }) + ), + parseCompilerOptions: Schema.optional(compilerOptionsSchema.annotate({ + description: "tsconfig for parsing options (or path to a tsconfig)", + default: {} + })), + examplesCompilerOptions: Schema.optional(compilerOptionsSchema.annotate({ + description: "tsconfig for the examples options (or path to a tsconfig)", + default: {} + })) +}).annotate({ identifier: "ConfigurationSchema" }) + +/** + * Resolved configuration used by the docgen services. + * + * @category services + * @since 0.6.0 + */ +export interface ConfigurationShape { + readonly projectName: string + readonly projectHomepage: string + readonly srcLink: string + readonly srcDir: string + readonly outDir: string + readonly theme: string + readonly enableSearch: boolean + readonly enforceDescriptions: boolean + readonly enforceExamples: boolean + readonly enforceVersion: boolean + readonly tscExecutable: string + readonly runExamples: boolean + readonly exclude: ReadonlyArray + readonly parseCompilerOptions: Record + readonly examplesCompilerOptions: Record +} + +/** + * Service that provides resolved docgen configuration. + * + * @category services + * @since 0.6.0 + */ +export class Configuration + extends Context.Service()("@effect/docgen/Configuration") +{} + +/** @internal */ +export const defaultCompilerOptions = { + noEmit: true, + strict: true, + skipLibCheck: true, + moduleResolution: "Bundler", + target: "ES2022", + lib: [ + "ES2022", + "DOM" + ] +} + +const readJsonFile = ( + path: string +): Effect.Effect => + Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const content = yield* Effect.orDie(fs.readFileString(path)) + return yield* pipe( + Effect.try({ + try: () => JSON.parse(content), + catch: (error) => `[FileSystem] Unable to read and parse JSON file from '${path}': ${String(error)}` + }), + Effect.orDie + ) + }) + +const validateJsonFile = ( + schema: Schema.Codec, + path: string +): Effect.Effect => + Effect.gen(function*() { + const content = yield* readJsonFile(path) + return yield* pipe( + Schema.decodeUnknownEffect(schema)(content), + Effect.mapError((error) => + new DocgenError({ + message: `[Configuration.validateJsonFile]\n${String(error)}` + }) + ), + Effect.orDie + ) + }) + +// TODO: this is invoked twice +const readDocgenConfig = ( + path: string +): Effect.Effect>, never, FileSystem.FileSystem> => { + return Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const exists = yield* Effect.orDie(fs.exists(path)) + if (exists) { + const config = yield* validateJsonFile(ConfigurationSchema, path) + return Option.some(config) + } else { + return Option.none() + } + }) +} + +const readTSConfig = (fileName: string): Effect.Effect< + { readonly [x: string]: unknown }, + never, + Path.Path | Domain.Process +> => + Effect.gen(function*() { + const path = yield* Path.Path + const process = yield* Domain.Process + const cwd = yield* process.cwd + return yield* pipe( + Effect.tryPromise(() => tsconfck.parse(path.resolve(cwd, fileName))).pipe( + Effect.map(({ tsconfig }) => tsconfig.compilerOptions ?? defaultCompilerOptions), + Effect.mapError((error) => + new DocgenError({ + message: `[Configuration.readTSConfig] Failed to read TSConfig file\n${String(error)}` + }) + ), + Effect.orDie + ) + ) + }) + +const loadCompilerOptions = (configKey: string) => + Config.string(configKey).pipe( + Effect.flatMap((config) => + Schema.decodeUnknownEffect(JsonRecordSchema)(config).pipe(Effect.orElseSucceed(() => config)) + ) + ) + +const resolveCompilerOptions = ( + configKey: string, + fromCLI: Option.Option>, + fromDocgenJson: Option.Option> +): Effect.Effect<{ readonly [x: string]: unknown }, never, Path.Path | Domain.Process> => { + const fromConfigProvider = loadCompilerOptions(configKey) + return Effect.gen(function*() { + let config: string | Record + if (Option.isSome(fromCLI)) { + config = fromCLI.value + } else { + const provided = yield* Effect.result(fromConfigProvider) + if (Result.isSuccess(provided)) { + config = provided.success + } else if (Option.isSome(fromDocgenJson)) { + config = fromDocgenJson.value + } else { + config = defaultCompilerOptions + } + } + return typeof config === "string" ? yield* readTSConfig(config) : config + }) +} + +const JsonRecordSchema = Schema.fromJsonString(Schema.Record(Schema.String, Schema.Unknown)) + +const PackageJsonSchema = Schema.Struct({ + name: Schema.String, + homepage: Schema.String +}) + +/** @internal */ +export const load = (args: { + readonly projectHomepage: Option.Option + readonly srcLink: Option.Option + readonly srcDir: string + readonly outDir: string + readonly theme: string + readonly enableSearch: boolean + readonly enforceDescriptions: boolean + readonly enforceExamples: boolean + readonly enforceVersion: boolean + readonly runExamples: boolean + readonly exclude: ReadonlyArray + readonly parseCompilerOptions: Option.Option> + readonly examplesCompilerOptions: Option.Option> +}) => + Effect.gen(function*() { + // Extract the requisite services + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + + // Read and parse the required fields from the `package.json` + const packageJsonPath = path.join(cwd, PACKAGE_JSON_FILE_NAME) + const packageJson = yield* validateJsonFile(PackageJsonSchema, packageJsonPath) + const projectName = packageJson.name + const projectHomepage = Option.getOrElse(args.projectHomepage, () => packageJson.homepage) + const srcLink = Option.getOrElse(args.srcLink, () => `${projectHomepage}/blob/main/src/`) + + // Read the `docgen.json` configuration file to gain access to the TypeScript + // configuration options + const configPath = path.join(cwd, CONFIG_FILE_NAME) + const config = yield* readDocgenConfig(configPath) + + // Resolve the excluded files + const exclude = yield* Array.match(args.exclude, { + onEmpty: () => + Effect.result(Config.schema(Config.Array(Schema.String), "exclude")).pipe( + Effect.map((configured) => + Result.isSuccess(configured) + ? configured.success + : Option.match(config, { + onNone: () => Array.empty(), + onSome: ({ exclude }) => exclude || Array.empty() + }) + ) + ), + onNonEmpty: (exclude) => Effect.succeed(exclude) + }) + + // Resolve the TypeScript configuration options + const examplesCompilerOptions = yield* resolveCompilerOptions( + "examplesCompilerOptions", + args.examplesCompilerOptions, + Option.flatMap(config, (config) => Option.fromNullishOr(config.examplesCompilerOptions)) + ) + const parseCompilerOptions = yield* resolveCompilerOptions( + "parseCompilerOptions", + args.parseCompilerOptions, + Option.flatMap(config, (config) => Option.fromNullishOr(config.parseCompilerOptions)) + ) + + const srcDir = config.pipe( + Option.flatMapNullishOr((config) => config.srcDir), + Option.getOrElse(() => args.srcDir) + ) + + const outDir = config.pipe( + Option.flatMapNullishOr((config) => config.outDir), + Option.getOrElse(() => args.outDir) + ) + + const runExamples = config.pipe( + Option.flatMapNullishOr((config) => config.runExamples), + Option.getOrElse(() => args.runExamples) + ) + + const tscExecutable = config.pipe( + Option.flatMapNullishOr((config) => config.tscExecutable), + Option.getOrElse(() => "tsc") + ) + + return Configuration.of({ + ...args, + srcDir, + outDir, + projectName, + projectHomepage, + srcLink, + exclude, + examplesCompilerOptions, + parseCompilerOptions, + runExamples, + tscExecutable + }) + }) + +/** @internal */ +export const configProviderLayer = Layer.effect(ConfigProvider.ConfigProvider)(Effect.gen(function*() { + // Extract the requisite services + const process = yield* Domain.Process + const cwd = yield* process.cwd + const env = yield* process.env + const path = yield* Path.Path + // Attempt to load the `docgen.json` configuration file + const configPath = path.join(cwd, CONFIG_FILE_NAME) + const maybeConfig = yield* readDocgenConfig(configPath) + // Construct a config provider for the environment + const fromEnv = ConfigProvider.fromEnv({ env }).pipe( + ConfigProvider.nested("DOCGEN"), + ConfigProvider.constantCase + ) + // Construct a config provider for the `docgen.json` file + const fromDocgenJson = ConfigProvider.fromUnknown(Option.getOrElse(maybeConfig, () => ({}))) + // Prefer the environment over the `docgen.json` file + const provider = fromEnv.pipe(ConfigProvider.orElse(fromDocgenJson)) + return provider +})) diff --git a/.context/effect/packages/tools/docgen/src/Core.ts b/.context/effect/packages/tools/docgen/src/Core.ts new file mode 100644 index 000000000..ad36ea344 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Core.ts @@ -0,0 +1,623 @@ +/** + * Coordinates source parsing, validation, example checking, and Markdown generation. + * + * @since 0.6.0 + */ + +import * as NodePath from "@effect/platform-node/NodePath" +import chalk from "chalk" +import * as Array from "effect/Array" +import * as Effect from "effect/Effect" +import * as Fiber from "effect/Fiber" +import * as FileSystem from "effect/FileSystem" +import { pipe } from "effect/Function" +import * as Path from "effect/Path" +import * as Stream from "effect/Stream" +import * as String from "effect/String" +import * as ChildProcess from "effect/unstable/process/ChildProcess" +import * as Glob from "glob" +import * as Checker from "./Checker.ts" +import * as Configuration from "./Configuration.ts" +import * as Domain from "./Domain.ts" +import * as Parser from "./Parser.ts" +import * as Printer from "./Printer.ts" +/** + * Find all files matching the specified `glob` pattern, optionally excluding + * files matching the provided `exclude` patterns. + */ +const glob = (pattern: string, exclude: ReadonlyArray = []) => + Effect.tryPromise(() => + Glob.glob(pattern, { + ignore: exclude.slice(), + withFileTypes: false + }) + ).pipe( + Effect.mapError(() => + new Domain.DocgenError({ + message: `[Core.glob] Unable to execute glob pattern '${pattern}' ` + + `excluding files matching '${exclude}'` + }) + ), + Effect.orDie + ) + +/** @internal */ +export const runCommand = Effect.fnUntraced(function*( + executable: string, + args: ReadonlyArray, + shell: boolean +) { + const handle = yield* ChildProcess.make(executable, args, { shell }) + const [stdout, stderr, exitCode] = yield* Effect.all([ + Stream.mkString(Stream.decodeText(handle.stdout)), + Stream.mkString(Stream.decodeText(handle.stderr)), + handle.exitCode + ], { concurrency: "unbounded" }) + return { stdout, stderr, exitCode } as const +}) + +/** + * Reads all TypeScript files in the source directory and returns an array of file objects. + * Each file object contains the file path and its content. + */ +const readSourceFiles = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)) + const pattern = path.normalize(path.join(config.srcDir, "**", "*.ts")) + const paths = yield* glob(pattern, config.exclude) + yield* Effect.logInfo(chalk.bold(`${paths.length} module(s) found`)) + return yield* Effect.forEach(paths, (path) => + Effect.map( + fs.readFileString(path), + (content) => new Domain.File(path, content, false) + ), { concurrency: "unbounded" }) +}) + +/** + * Writes a file to the `config.outDir` directory, taking into account the configuration and existing files. + */ +const writeFileToOutDir = (file: Domain.File) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const fs = yield* FileSystem.FileSystem + const path = yield* Path.Path + const process = yield* Domain.Process + const cwd = yield* process.cwd + const fileName = path.relative(path.join(cwd, config.outDir), file.path) + + const exists = yield* fs.exists(file.path) + if (exists) { + if (file.isOverwriteable) { + yield* Effect.logDebug(`Overwriting file ${chalk.black(fileName)}...`) + yield* fs.makeDirectory(path.dirname(file.path), { recursive: true }) + yield* fs.writeFileString(file.path, file.content) + } else { + yield* Effect.logDebug( + `File ${chalk.black(fileName)} already exists, skipping creation.` + ) + } + } else { + yield* fs.makeDirectory(path.dirname(file.path), { recursive: true }) + yield* fs.writeFileString(file.path, file.content) + } + }) + +const writeFilesToOutDir = ( + files: ReadonlyArray +) => Effect.forEach(files, writeFileToOutDir, { discard: true }) + +const parseModules = (files: ReadonlyArray) => + Parser.parseFiles(files).pipe( + Effect.mapError((errors) => + new Domain.DocgenError({ + message: "[Core.parseModules] The following error(s) occurred while " + + `parsing the TypeScript source files:\n${errors.map((errors) => errors.join("\n")).join("\n")}` + }) + ) + ) + +/** + * Runs the example files for the given modules, type-checking them before execution. + */ +const typeCheckAndRunExamples = (modules: ReadonlyArray) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + yield* cleanupExamples + const files = yield* getExampleFiles(modules) + const len = files.length + if (len > 0) { + yield* Effect.logInfo(`${len} example(s) found`) + yield* writeExamplesToOutDir(files) + yield* createExamplesTsConfigJson + yield* Effect.logInfo("Typechecking examples...") + yield* runTscOnExamples + if (config.runExamples) { + yield* Effect.logInfo("Running examples...") + yield* runTsxOnExamples + } else { + yield* Effect.logInfo(chalk.gray("Skipping running examples")) + } + } else { + yield* Effect.logInfo("No examples found.") + } + yield* cleanupExamples + }) + +/** + * Joins an array of strings with a "-" after dropping all empty strings. + */ +const filterJoin = (self: Array) => + pipe( + self, + Array.filter(String.isNonEmpty), + Array.join("-") + ) + +/** + * Extracts deeply nested namespaces with their corresponding namespace prefix + * from a given namespace. + */ +const extractPrefixedNestedNamespaces = ( + doc: Domain.Namespace, + prefix: string +): ReadonlyArray<[string, Domain.Namespace]> => { + const newPrefix = String.isEmpty(prefix) ? doc.name : `${prefix}-${doc.name}` + const namespaces = Array.flatMap( + doc.namespaces, + (namespace) => extractPrefixedNestedNamespaces(namespace, newPrefix) + ) + return Array.prepend(namespaces, [prefix, doc]) +} + +/** + * Fence metadata that excludes an example from docgen type checking. + * + * @category constants + * @since 0.6.0 + */ +export const SKIP_TYPE_CHECKING_FENCE_METADATA = "skip-type-checking" + +/** + * Extracts all fenced code blocks from markdown content. + * Handles both ``` and ~~~ fences, including any metadata like language, title, and other attributes. + * + * @internal + */ +export const extractFencedCode = (content: string): [examples: Array, warnings: Array] => { + // The regex now captures the closing fence (group 3) if present. + // If there's no closing fence, group 3 will be undefined. + const fenceRegex = /(?:```|~~~)(.*?)\n([\s\S]*?)(?:(```|~~~)|$)/g + const matches = Array.fromIterable(content.matchAll(fenceRegex)) + + const warnings: Array = [] + + // Log a warning if a code fence is not properly closed. + for (const match of matches) { + if (match[3] === undefined) { + warnings.push(`Code block does not have a matching closing fence:\n${content}`) + } + } + + return [ + matches + .filter((match) => { + const meta = match[1].toLocaleLowerCase() + const isTypeScript = meta.startsWith("ts") || meta.startsWith("typescript") + const isSkipTypeChecking = meta.includes(SKIP_TYPE_CHECKING_FENCE_METADATA) + return isTypeScript && !isSkipTypeChecking + }) + .map((match) => match[2].trim()), + warnings + ] +} + +/** + * Generates example files for the given modules. + */ +const getExampleFiles = (modules: ReadonlyArray) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + let warnings: Array = [] + const files = Array.flatMap(modules, (module) => { + const prefix = module.path.join("-") + + const getFiles = + (exampleId: string) => + (namedDoc: { readonly name: string; readonly doc: Domain.Doc }): ReadonlyArray => { + let descriptionExamples: Array = [] + if (namedDoc.doc.description !== undefined) { + const [es, ws] = extractFencedCode(namedDoc.doc.description) + warnings = warnings.concat(ws) + descriptionExamples = es + } + let exampleTagExamples: Array = [] + for (const example of namedDoc.doc.examples) { + const [es, ws] = extractFencedCode(example) + warnings = warnings.concat(ws) + exampleTagExamples = exampleTagExamples.concat(es) + } + const examples = descriptionExamples.concat(exampleTagExamples) + return Array.map( + examples, + (example, i) => { + return new Domain.File( + path.join( + config.outDir, + "examples", + `${prefix}-${exampleId}-${namedDoc.name}-${i}.ts` + ), + example, + true // make the file overwritable + ) + } + ) + } + + const allPrefixedNamespaces = Array.flatMap(module.namespaces, (namespace) => + extractPrefixedNestedNamespaces(namespace, "")) + + const moduleExamples = getFiles("module")(module) + const classExamples = Array.flatMap(module.classes, (c) => + Array.flatten([ + getFiles("class")(c), + Array.flatMap( + c.methods, + getFiles(`${c.name}-method`) + ), + Array.flatMap( + c.staticMethods, + getFiles(`${c.name}-staticmethod`) + ) + ])) + const allPrefixedInterfaces = [ + ...module.interfaces.map((iface) => + ["" as string, iface] as const + ), + ...Array.flatMap(allPrefixedNamespaces, ([prefix, namespace]) => + namespace.interfaces.map((iface) => + [filterJoin([prefix, namespace.name]), iface] as const + )) + ] + const interfacesExamples = Array.flatMap( + allPrefixedInterfaces, + ([ns, doc]) => getFiles(filterJoin(["interface", ns]))(doc) + ) + const allPrefixedTypeAliases = [ + ...module.typeAliases.map((typeAlias) => ["" as string, typeAlias] as const), + ...Array.flatMap(allPrefixedNamespaces, ([prefix, namespace]) => + namespace.typeAliases.map((typeAlias) => + [filterJoin([prefix, namespace.name]), typeAlias] as const + )) + ] + const typeAliasesExamples = Array.flatMap( + allPrefixedTypeAliases, + ([ns, doc]) => + getFiles(filterJoin(["typealias", ns]))(doc) + ) + const constantsExamples = Array.flatMap( + module.constants, + getFiles("constant") + ) + const functionsExamples = Array.flatMap( + module.functions, + getFiles("function") + ) + const exportsExamples = Array.flatMap( + module.exports, + getFiles("export") + ) + const namespacesExamples = Array.flatMap( + allPrefixedNamespaces, + ([ns, doc]) => getFiles(filterJoin(["namespace", ns]))(doc) + ) + + return Array.flatten([ + moduleExamples, + classExamples, + interfacesExamples, + typeAliasesExamples, + constantsExamples, + functionsExamples, + namespacesExamples, + exportsExamples + ]) + }) + + if (warnings.length > 0) { + yield* Effect.logWarning(warnings.join("\n")) + } + + return files + }) + +/** + * Generates an entry point file for the given examples. + */ +const getExamplesEntryPoint = (examples: ReadonlyArray) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + const content = examples.map((example) => `import './${path.basename(example.path, ".ts")}'`) + .join("\n") + return new Domain.File( + path.normalize(path.join(config.outDir, "examples", "index.ts")), + `${content}\n`, + true // make the file overwritable + ) + }) + +/** + * Removes the "examples" directory from the output directory specified in the configuration. + */ +const cleanupExamples = Effect.gen(function*() { + const fs = yield* FileSystem.FileSystem + const config = yield* Configuration.Configuration + const path = yield* Path.Path + const examplesDir = path.join(config.outDir, "examples") + const exists = yield* Effect.orDie(fs.exists(examplesDir)) + if (exists) { + yield* fs.remove(examplesDir, { recursive: true }) + } +}) + +/** + * Runs tsc on the examples directory. + */ +const runTscOnExamples = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + const platform = yield* process.platform + + const tsconfig = path.normalize(path.join(cwd, config.outDir, "examples", "tsconfig.json")) + const options = ["--noEmit", "--project", tsconfig] + yield* Effect.logDebug("Running tsc on examples...") + const result = yield* runCommand( + platform === "win32" ? `${config.tscExecutable}.cmd` : config.tscExecutable, + options, + platform === "win32" + ).pipe(Effect.mapError((error) => + new Domain.DocgenError({ + message: `Something went wrong while running tsc on examples:\n\n${globalThis.String(error)}` + }) + )) + if (result.exitCode !== 0) { + return yield* new Domain.DocgenError({ + message: `Something went wrong while running tsc on examples:\n\n${result.stdout}` + }) + } +}) + +/** + * Runs tsc on the examples directory. + */ +const runTsxOnExamples = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + const process = yield* Domain.Process + const cwd = yield* process.cwd + const platform = yield* process.platform + + const examples = path.normalize(path.join(cwd, config.outDir, "examples")) + const tsconfig = path.join(examples, "tsconfig.json") + const index = path.join(examples, "index.ts") + const options = ["--tsconfig", tsconfig, index] + yield* Effect.logDebug("Running tsx on examples...") + const result = yield* runCommand( + platform === "win32" ? "tsx.cmd" : "tsx", + options, + platform === "win32" + ).pipe(Effect.mapError((error) => + new Domain.DocgenError({ + message: `Something went wrong while running tsx on examples:\n\n${globalThis.String(error)}` + }) + )) + if (result.exitCode !== 0) { + return yield* new Domain.DocgenError({ + message: `Something went wrong while running tsx on examples:\n\n${result.stderr}` + }) + } +}) + +const writeExamplesToOutDir = (examples: ReadonlyArray) => + Effect.gen(function*() { + yield* Effect.logDebug("Writing examples...") + const entryPoint = yield* getExamplesEntryPoint(examples) + const files = [entryPoint, ...examples] + yield* writeFilesToOutDir(files) + }) + +const createExamplesTsConfigJson = Effect.gen(function*() { + yield* Effect.logDebug("Writing examples tsconfig...") + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + yield* writeFileToOutDir( + new Domain.File( + path.join(cwd, config.outDir, "examples", "tsconfig.json"), + JSON.stringify({ compilerOptions: config.examplesCompilerOptions }, null, 2), + true // make the file overwritable + ) + ) +}) + +const getMarkdown = (modules: ReadonlyArray) => + Effect.gen(function*() { + const homepage = yield* getMarkdownHomepage + const index = yield* getMarkdownIndex + const yml = yield* getMarkdownConfigYML + const moduleFiles = yield* getModuleMarkdownFiles(modules) + return [homepage, index, yml, ...moduleFiles] + }) + +const getMarkdownHomepage = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + return new Domain.File( + path.join(cwd, config.outDir, "index.md"), + String.stripMargin( + `|--- + |title: Home + |nav_order: 1 + |--- + |` + ), + false + ) +}) + +const getMarkdownIndex = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + const path = yield* Path.Path + return new Domain.File( + path.join(cwd, config.outDir, "modules", "index.md"), + String.stripMargin( + `|--- + |title: Modules + |has_children: true + |permalink: /docs/modules + |nav_order: 2 + |--- + |` + ), + false + ) +}) + +const resolveConfigYML = (content: string) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + return content + .replace(/^remote_theme:.*$/m, `remote_theme: ${config.theme}`) + .replace( + /^search_enabled:.*$/m, + `search_enabled: ${config.enableSearch}` + ).replace( + /^ {2}'\S* on GitHub':\n {4}- '.*'/m, + ` '${config.projectName} on GitHub':\n - '${config.projectHomepage}'` + ) + }) + +const getHomepageNavigationHeader = (config: Configuration.ConfigurationShape): string => { + const isGitHub = config.projectHomepage.toLowerCase().includes("github") + return isGitHub ? config.projectName + " on GitHub" : "Homepage" +} + +const getMarkdownConfigYML = Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const fs = yield* FileSystem.FileSystem + const cwd = yield* process.cwd + const path = yield* Path.Path + const configPath = path.join(cwd, config.outDir, "_config.yml") + const exists = yield* fs.exists(configPath) + if (exists) { + const content = yield* fs.readFileString(configPath) + const resolved = yield* resolveConfigYML(content) + return new Domain.File(configPath, resolved, true) + } else { + return new Domain.File( + configPath, + String.stripMargin( + `|remote_theme: ${config.theme} + | + |# Enable or disable the site search + |search_enabled: ${config.enableSearch} + | + |# Aux links for the upper right navigation + |aux_links: + |'${getHomepageNavigationHeader(config)}': + | - '${config.projectHomepage}'` + ), + false + ) + } +}) + +const getModuleMarkdownOutputPath = (module: Domain.Module) => { + return Effect.gen(function*() { + const config = yield* Configuration.Configuration + const path = yield* Path.Path + return path.normalize(path.join( + config.outDir, + "modules", + `${module.path.slice(1).join(path.sep)}.md` + )) + }) +} + +const getModuleMarkdownFiles = (modules: ReadonlyArray) => + Effect.forEach(modules, (module, i) => + Effect.gen(function*() { + const outputPath = yield* getModuleMarkdownOutputPath(module) + const moduleContent = yield* Printer.printModule(module) + const tocgen = yield* Effect.promise(() => import("@effect/markdown-toc").then((module) => module.default)).pipe( + Effect.orDie + ) + const toc = tocgen(moduleContent, { bullets: "-" }).content + const frontMatter = Printer.printFrontMatter(module, i + 1) + const content = (frontMatter + "\n\n" + moduleContent).replace( + "", + `--- +## Exports Grouped by Category +${toc} +---` + ) + + const prettified = yield* Printer.prettify(content) + return new Domain.File(outputPath, prettified, true) + })) + +const writeMarkdown = (files: ReadonlyArray) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const fileSystem = yield* FileSystem.FileSystem + const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)) + const pattern = path.normalize(path.join(config.outDir, "**/*.ts.md")) + yield* Effect.logDebug(`Deleting ${chalk.black(pattern)}...`) + const paths = yield* glob(pattern) + yield* Effect.forEach(paths, (path) => fileSystem.remove(path, { recursive: true }), { + concurrency: "unbounded" + }) + return yield* writeFilesToOutDir(files) + }) + +/** @internal */ +export const program = Effect.gen(function*() { + yield* Effect.logInfo("Reading modules...") + const sourceFiles = yield* readSourceFiles + yield* Effect.logInfo("Parsing modules...") + const modules = yield* parseModules(sourceFiles) + + const checkFiber = yield* Effect.gen(function*() { + yield* Effect.logInfo("Checking modules...") + const errors = yield* Checker.checkModules(modules) + if (errors.length > 0) { + return yield* Effect.fail( + new Domain.DocgenError({ + message: `The following errors occurred while checking the modules:\n\n${errors.join("\n\n")}` + }) + ) + } + yield* typeCheckAndRunExamples(modules) + }).pipe(Effect.forkChild) + + const markdownFiber = yield* Effect.gen(function*() { + yield* Effect.logInfo("Creating markdown files...") + const outputFiles = yield* getMarkdown(modules) + yield* Effect.logInfo("Writing markdown files...") + yield* writeMarkdown(outputFiles) + }).pipe(Effect.forkChild) + + yield* Fiber.joinAll([checkFiber, markdownFiber]) + + yield* Effect.logInfo(chalk.bold.green("✓ Docs generation succeeded!")) +}) diff --git a/.context/effect/packages/tools/docgen/src/Domain.ts b/.context/effect/packages/tools/docgen/src/Domain.ts new file mode 100644 index 000000000..9657d1b33 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Domain.ts @@ -0,0 +1,348 @@ +/** + * Data models shared by the docgen parser, checker, and printer. + * + * @since 0.6.0 + */ + +import type * as Array from "effect/Array" +import * as Context from "effect/Context" +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import * as Order from "effect/Order" +import * as Rec from "effect/Record" +import * as String from "effect/String" +import type * as Parser from "./Parser.ts" + +/** + * Base model for a named, documented declaration. + * + * @category models + * @since 0.6.0 + */ +export class DocEntry { + readonly name: string + readonly doc: Doc + readonly signature: string + readonly position: Position + constructor( + name: string, + doc: Doc, + signature: string, + position: Position + ) { + this.name = name + this.doc = doc + this.signature = signature + this.position = position + } +} + +/** + * Parsed JSDoc content attached to a declaration or module. + * + * @category models + * @since 0.6.0 + */ +export class Doc { + readonly description: string | undefined + readonly since: ReadonlyArray + readonly deprecated: ReadonlyArray + readonly examples: ReadonlyArray + readonly category: ReadonlyArray + readonly throws: ReadonlyArray + readonly sees: ReadonlyArray + readonly tags: Record | undefined> + constructor( + description: string | undefined, + since: ReadonlyArray, + deprecated: ReadonlyArray, + examples: ReadonlyArray, + category: ReadonlyArray, + throws: ReadonlyArray, + sees: ReadonlyArray, + tags: Record | undefined> + ) { + this.description = description + this.since = since + this.deprecated = deprecated + this.examples = examples + this.category = category + this.throws = throws + this.sees = sees + this.tags = tags + } + + modifyDescription(description: string | undefined): Doc { + return new Doc( + description, + this.since, + this.deprecated, + this.examples, + this.category, + this.throws, + this.sees, + this.tags + ) + } +} + +/** + * Parsed documentation model for one source module. + * + * @category models + * @since 0.6.0 + */ +export class Module { + readonly source: Parser.SourceShape + readonly name: string + readonly doc: Doc + readonly path: Array.NonEmptyReadonlyArray + readonly classes: ReadonlyArray + readonly interfaces: ReadonlyArray + readonly functions: ReadonlyArray + readonly typeAliases: ReadonlyArray + readonly constants: ReadonlyArray + readonly exports: ReadonlyArray + readonly namespaces: ReadonlyArray + constructor( + source: Parser.SourceShape, + name: string, + doc: Doc, + path: Array.NonEmptyReadonlyArray, + classes: ReadonlyArray, + interfaces: ReadonlyArray, + functions: ReadonlyArray, + typeAliases: ReadonlyArray, + constants: ReadonlyArray, + exports: ReadonlyArray, + namespaces: ReadonlyArray + ) { + this.source = source + this.name = name + this.doc = doc + this.path = path + this.classes = classes + this.interfaces = interfaces + this.functions = functions + this.typeAliases = typeAliases + this.constants = constants + this.exports = exports + this.namespaces = namespaces + } +} + +/** + * Parsed documentation model for a class and its documented members. + * + * @category models + * @since 0.6.0 + */ +export class Class extends DocEntry { + readonly _tag = "Class" + readonly methods: ReadonlyArray + readonly staticMethods: ReadonlyArray + readonly properties: ReadonlyArray + constructor( + name: string, + doc: Doc, + signature: string, + position: Position, + methods: ReadonlyArray, + staticMethods: ReadonlyArray, + properties: ReadonlyArray + ) { + super(name, doc, signature, position) + this.methods = methods + this.staticMethods = staticMethods + this.properties = properties + } +} + +/** + * Parsed documentation model for an interface. + * + * @category models + * @since 0.6.0 + */ +export class Interface extends DocEntry { + readonly _tag = "Interface" +} + +/** + * One-based source position used in diagnostics. + * + * @category models + * @since 0.6.0 + */ +export interface Position { + readonly line: number + readonly column: number +} + +/** + * Parsed documentation model for a function. + * + * @category models + * @since 0.6.0 + */ +export class Function extends DocEntry { + readonly _tag = "Function" +} + +/** + * Parsed documentation model for a type alias. + * + * @category models + * @since 0.6.0 + */ +export class TypeAlias extends DocEntry { + readonly _tag = "TypeAlias" +} + +/** + * Parsed documentation model for a constant. + * + * @category models + * @since 0.6.0 + */ +export class Constant extends DocEntry { + readonly _tag = "Constant" +} + +/** + * Parsed documentation model for an explicit named or namespace export. + * + * @category models + * @since 0.6.0 + */ +export class Export extends DocEntry { + readonly _tag = "Export" + readonly isNamespaceExport: boolean + constructor( + name: string, + doc: Doc, + signature: string, + position: Position, + isNamespaceExport: boolean + ) { + super(name, doc, signature, position) + this.isNamespaceExport = isNamespaceExport + } +} + +/** + * Parsed documentation model for a namespace and its nested declarations. + * + * @category models + * @since 0.6.0 + */ +export class Namespace { + readonly _tag = "Namespace" + readonly name: string + readonly doc: Doc + readonly position: Position + readonly interfaces: ReadonlyArray + readonly typeAliases: ReadonlyArray + readonly namespaces: ReadonlyArray + constructor( + name: string, + doc: Doc, + position: Position, + interfaces: ReadonlyArray, + typeAliases: ReadonlyArray, + namespaces: ReadonlyArray + ) { + this.name = name + this.doc = doc + this.position = position + this.interfaces = interfaces + this.typeAliases = typeAliases + this.namespaces = namespaces + } +} + +/** + * A comparator function for sorting `Module` objects by their file path, represented as a string. + * The file path is converted to lowercase before comparison. + * + * @category sorting + * @since 0.6.0 + */ +export const ByPath: Order.Order = Order.mapInput( + String.Order, + (module: Module) => module.path.join("/").toLowerCase() +) + +/** + * Represents a file which can be optionally overwriteable. + * + * @category models + * @since 0.6.0 + */ +export class File { + readonly path: string + readonly content: string + readonly isOverwriteable: boolean + constructor( + path: string, + content: string, + isOverwriteable: boolean = false + ) { + this.path = path + this.content = content + this.isOverwriteable = isOverwriteable + } +} + +/** + * Type ID for `DocgenError`. + * + * @category symbols + * @since 0.6.0 + */ +export const DocgenErrorTypeId = Symbol.for("@effect/docgen/DocgenError") + +/** + * Type-level representation of `DocgenErrorTypeId`. + * + * @category symbols + * @since 0.6.0 + */ +export type DocgenErrorTypeId = typeof DocgenErrorTypeId + +/** + * Error reported when documentation generation cannot continue. + * + * @category errors + * @since 0.6.0 + */ +export class DocgenError extends Data.TaggedError("DocgenError")<{ + readonly message: string +}> {} + +/** + * Represents a handle to the currently executing process. + * + * @category services + * @since 0.6.0 + */ +export class Process extends Context.Service + readonly platform: Effect.Effect + readonly argv: Effect.Effect> + readonly env: Effect.Effect> +}>()("@effect/docgen/Process") { + static readonly layer = Layer.succeed(Process, { + cwd: Effect.sync(() => process.cwd()), + platform: Effect.sync(() => process.platform), + argv: Effect.sync(() => process.argv), + env: Effect.sync(() => { + const env: Record = {} + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) Rec.assignProperty(env, key, value) + } + return env + }) + }) +} diff --git a/.context/effect/packages/tools/docgen/src/Parser.ts b/.context/effect/packages/tools/docgen/src/Parser.ts new file mode 100644 index 000000000..33f6116c4 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Parser.ts @@ -0,0 +1,746 @@ +/** + * Parses TypeScript declarations and JSDoc into docgen models. + * + * @since 0.6.0 + */ +import * as doctrine from "doctrine" +import * as Array from "effect/Array" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Option from "effect/Option" +import * as Path from "effect/Path" +import * as Record from "effect/Record" +import * as String from "effect/String" +import * as ast from "ts-morph" +import * as Configuration from "./Configuration.ts" +import * as Domain from "./Domain.ts" + +/** + * Source file and path currently being parsed. + * + * @category services + * @since 0.6.0 + */ +export interface SourceShape { + readonly path: Array.NonEmptyReadonlyArray + readonly sourceFile: ast.SourceFile +} + +/** @internal */ +export class Source extends Context.Service()("@effect/docgen/Source") {} + +const sortModulesByPath: (self: Iterable) => Array = Array + .sort(Domain.ByPath) + +const getJSDocText: (jsdocs: ReadonlyArray) => string = Array.matchRight({ + onEmpty: () => "", + onNonEmpty: (_, last) => last.getText() +}) + +const getDocComment = (ranges: ReadonlyArray): Option.Option => + pipe( + ranges, + Array.filter((range) => range.getText().startsWith("/**")), + Array.last + ) + +class Comment { + readonly description: string | undefined + readonly tags: Record | undefined> + constructor( + description: string | undefined, + tags: Record | undefined> + ) { + this.description = description + this.tags = tags + } +} + +/** + * @internal + */ +export const parseComment = (text: string): Comment => { + const annotation: doctrine.Annotation = doctrine.parse(text, { + unwrap: true + }) + + const description = pipe( + Option.fromNullishOr(annotation.description), + Option.map((s) => s.trim()), + Option.filter(String.isNonEmpty), + Option.getOrUndefined + ) + + const tags = pipe( + annotation.tags, + Array.groupBy((tag) => tag.title), + Record.map((values) => + Array.map(values, (tag) => + pipe( + Option.fromNullishOr(tag.description), + Option.map(String.trim), + Option.getOrElse(() => "") + )) + ) + ) + + return { description, tags } +} + +const isVariableDeclarationList = ( + u: ast.VariableDeclarationList | ast.CatchClause +): u is ast.VariableDeclarationList => u.getKind() === ast.ts.SyntaxKind.VariableDeclarationList + +const isVariableStatement = ( + u: + | ast.VariableStatement + | ast.ForStatement + | ast.ForOfStatement + | ast.ForInStatement +): u is ast.VariableStatement => u.getKind() === ast.ts.SyntaxKind.VariableStatement + +const parseDoc = (text: string) => { + const comment = parseComment(text) + return new Domain.Doc( + comment.description, + comment.tags["since"] ?? [], + comment.tags["deprecated"] ?? [], + comment.tags["example"] ?? [], + comment.tags["category"] ?? [], + comment.tags["throws"] ?? [], + comment.tags["see"] ?? [], + comment.tags + ) +} + +const shouldIgnore = (doc: Domain.Doc): boolean => { + return Record.has(doc.tags, "internal") || Record.has(doc.tags, "ignore") +} + +const parseInterfaceDeclaration = (id: ast.InterfaceDeclaration) => + Effect.gen(function*() { + const doc = parseDoc(getJSDocText(id.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = id.getName() + const signature = id.getText() + const position = yield* parsePosition(id) + return [ + new Domain.Interface( + name, + doc, + signature, + position + ) + ] + }) + +const parseInterfaceDeclarations = (interfaces: ReadonlyArray) => { + const exportedInterfaces = Array.filter( + interfaces, + (id) => id.isExported() + ) + return Effect.forEach(exportedInterfaces, parseInterfaceDeclaration).pipe(Effect.map(Array.flatten)) +} + +/** + * Parses exported interfaces from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseInterfaces = Effect.flatMap( + Source, + (source) => parseInterfaceDeclarations(source.sourceFile.getInterfaces()) +) + +const getTypeText = (node: ast.Node) => + node.getType().getText( + node, + ast.ts.TypeFormatFlags.NoTruncation + | ast.ts.TypeFormatFlags.WriteArrayAsGenericType + | ast.ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope + | ast.ts.TypeFormatFlags.NoTypeReduction + | ast.ts.TypeFormatFlags.AllowUniqueESSymbolType + | ast.ts.TypeFormatFlags.WriteArrowStyleSignature + ) + +const parseType = (node: ast.Node) => { + let text = getTypeText(node) + for (const property of node.getDescendantsOfKind(ast.ts.SyntaxKind.PropertySignature)) { + if (!shouldIgnore(parseDoc(getJSDocText(property.getJsDocs())))) continue + const readonly = property.getFirstModifierByKind(ast.ts.SyntaxKind.ReadonlyKeyword) ? "readonly " : "" + const optional = property.hasQuestionToken() ? "?" : "" + const type = property.getTypeNode()?.getText() ?? getTypeText(property) + const signature = `${readonly}${property.getName()}${optional}: ${type}` + text = text + .replaceAll(`; ${signature}`, "") + .replaceAll(`${signature}; `, "") + .replaceAll(signature, "") + } + return text +} + +const getFunctionDeclarationJSDocs = (fd: ast.FunctionDeclaration): Array => + Array.matchLeft(fd.getOverloads(), { + onEmpty: () => fd.getJsDocs(), + onNonEmpty: (firstOverload) => firstOverload.getJsDocs() + }) + +const parsePosition = (node: ast.Node): Effect.Effect => { + return Effect.gen(function*() { + const source = yield* Source + const startPos = node.getStart() + const position = source.sourceFile.getLineAndColumnAtPos(startPos) + return position + }) +} + +const parseFunctionDeclaration = (fd: ast.FunctionDeclaration) => + Effect.gen(function*() { + const doc = parseDoc(getJSDocText(getFunctionDeclarationJSDocs(fd))) + if (shouldIgnore(doc)) { + return [] + } + const name = fd.getName() + const type = parseType(fd) + const signature = `declare const ${name}: ${type}` + const position = yield* parsePosition(fd) + return [ + new Domain.Function( + name ?? "", + doc, + signature, + position + ) + ] + }) + +const parseFunctionVariableDeclaration = (vd: ast.VariableDeclaration) => + Effect.gen(function*() { + const vs: any = vd.getParent().getParent() + const doc = parseDoc(getJSDocText(vs.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = vd.getName() + const type = parseType(vd) + const signature = `declare const ${name}: ${type}` + const startPos = vd.getStart() + const source = yield* Source + const position = source.sourceFile.getLineAndColumnAtPos(startPos) + return [ + new Domain.Function( + name ?? "", + doc, + signature, + position + ) + ] + }) + +const getFunctionDeclarations = Effect.gen(function*() { + const source = yield* Source + const functions = Array.filter( + source.sourceFile.getFunctions(), + (fd) => fd.isExported() + ) + const arrows = pipe( + Array.filter( + source.sourceFile.getVariableDeclarations(), + (vd) => { + if (isVariableDeclarationList(vd.getParent())) { + const vs: any = vd.getParent().getParent() + if (isVariableStatement(vs)) { + return vs.isExported() && + Option.fromNullishOr(vd.getInitializer()).pipe( + Option.filter((expr) => ast.Node.isFunctionLikeDeclaration(expr)), + Option.isSome + ) + } + } + return false + } + ) + ) + return { functions, arrows } +}) + +/** + * Parses exported function declarations and function-valued variables. + * + * @category parsing + * @since 0.6.0 + */ +export const parseFunctions = Effect.gen(function*() { + const { arrows, functions } = yield* getFunctionDeclarations + const functionDeclarations = yield* Effect.forEach(functions, parseFunctionDeclaration).pipe( + Effect.map(Array.flatten) + ) + const functionVariableDeclarations = yield* Effect.forEach(arrows, parseFunctionVariableDeclaration).pipe( + Effect.map(Array.flatten) + ) + return [...functionDeclarations, ...functionVariableDeclarations] +}) + +const parseTypeAliasDeclaration = (ta: ast.TypeAliasDeclaration) => + Effect.gen(function*() { + const doc = parseDoc(getJSDocText(ta.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = ta.getName() + const len = ta.getTypeParameters().length + const type = parseType(ta) + const definition = ta.getTypeNode()?.getText() + const signature = `type ${len > 0 ? type : name} = ${definition}` + const position = yield* parsePosition(ta) + return [ + new Domain.TypeAlias( + name, + doc, + signature, + position + ) + ] + }) + +const parseTypeAliasDeclarations = (typeAliases: ReadonlyArray) => { + const exportedTypeAliases = Array.filter( + typeAliases, + (tad) => tad.isExported() + ) + return Effect.forEach(exportedTypeAliases, parseTypeAliasDeclaration).pipe(Effect.map(Array.flatten)) +} + +/** + * Parses exported type aliases from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseTypeAliases = Effect.flatMap( + Source, + (source) => parseTypeAliasDeclarations(source.sourceFile.getTypeAliases()) +) + +const parseConstantVariableDeclaration = (vd: ast.VariableDeclaration) => + Effect.gen(function*() { + const vs: any = vd.getParent().getParent() + const doc = parseDoc(getJSDocText(vs.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = vd.getName() + const type = parseType(vd) + const signature = `declare const ${name}: ${type}` + const position = yield* parsePosition(vd) + return [ + new Domain.Constant( + name, + doc, + signature, + position + ) + ] + }) + +/** + * Parses exported non-function constants from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseConstants = Effect.gen(function*() { + const source = yield* Source + const variableDeclarations = pipe( + Array.filter( + source.sourceFile.getVariableDeclarations(), + (vd) => { + if (isVariableDeclarationList(vd.getParent())) { + const vs: any = vd.getParent().getParent() + if (isVariableStatement(vs)) { + return vs.isExported() && + Option.fromNullishOr(vd.getInitializer()).pipe( + Option.filter((expr) => !ast.Node.isFunctionLikeDeclaration(expr)), + Option.isSome + ) + } + } + return false + } + ) + ) + return yield* Effect.forEach(variableDeclarations, parseConstantVariableDeclaration).pipe( + Effect.map(Array.flatten) + ) +}) + +const parseExportSpecifier = (es: ast.ExportSpecifier) => + Effect.gen(function*() { + const name = es.compilerNode.name.text + const type = parseType(es) + const oDocComment = getDocComment(es.getLeadingCommentRanges()) + const doc = Option.isSome(oDocComment) ? parseDoc(oDocComment.value.getText()) : parseDoc("") + const signature = `declare const ${name}: ${type}` + const position = yield* parsePosition(es) + return new Domain.Export( + name, + doc, + signature, + position, + false + ) + }) + +const parseExportStar = (ed: ast.ExportDeclaration) => + Effect.gen(function*() { + const es = ed.getModuleSpecifier()! + const name = es.getText() + const namespace = ed.getNamespaceExport()?.getName() + const signature = `export *${namespace === undefined ? "" : ` as ${namespace}`} from ${name}` + const oDocComment = getDocComment(ed.getLeadingCommentRanges()) + const doc = Option.isSome(oDocComment) ? parseDoc(oDocComment.value.getText()) : parseDoc("") + const position = yield* parsePosition(ed) + return new Domain.Export( + namespace ?? name, + doc.modifyDescription( + `Re-exports all named exports from the ${name} module${namespace === undefined ? "" : ` as \`${namespace}\``}.` + ), + signature, + position, + true + ) + }) + +const parseNamedExports = (ed: ast.ExportDeclaration) => { + const namedExports = ed.getNamedExports() + if (namedExports.length === 0) { + if (ed.getModuleSpecifier() !== undefined) { + return parseExportStar(ed).pipe(Effect.map(Array.of)) + } + return Effect.succeed([]) + } + return Effect.forEach(namedExports, parseExportSpecifier) +} + +/** + * Parses explicit export declarations from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseExports = pipe( + Effect.map(Source, (source) => source.sourceFile.getExportDeclarations()), + Effect.flatMap((exportDeclarations) => Effect.forEach(exportDeclarations, parseNamedExports)), + Effect.map(Array.flatten) +) + +const parseModuleDeclaration = ( + ed: ast.ModuleDeclaration +): Effect.Effect, never, Source | Configuration.Configuration> => { + const doc = parseDoc(getJSDocText(ed.getJsDocs())) + if (shouldIgnore(doc)) { + return Effect.succeed([]) + } + const name = ed.getName() + const getInterfaces = parseInterfaceDeclarations(ed.getInterfaces()) + const getTypeAliases = parseTypeAliasDeclarations(ed.getTypeAliases()) + const getNamespaces = parseModuleDeclarations(ed.getModules()) + return Effect.gen(function*() { + const interfaces = yield* getInterfaces + const typeAliases = yield* getTypeAliases + const namespaces = yield* getNamespaces + const position = yield* parsePosition(ed) + return [ + new Domain.Namespace( + name, + doc, + position, + interfaces, + typeAliases, + namespaces + ) + ] + }) +} + +const parseModuleDeclarations = (namespaces: ReadonlyArray) => { + return Effect.forEach( + namespaces.filter((md) => md.isExported()), + parseModuleDeclaration + ).pipe(Effect.map(Array.flatten)) +} + +/** + * Parses exported namespaces from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseNamespaces = Effect.gen(function*() { + const source = yield* Source + return yield* parseModuleDeclarations(source.sourceFile.getModules()) +}) + +const getTypeParameters = (tps: ReadonlyArray): string => + tps.length === 0 ? "" : `<${tps.map((p) => p.getName()).join(", ")}>` + +const parseMethod = (md: ast.MethodDeclaration) => + Effect.gen(function*() { + const name = md.getName() + const jsdocs = Array.matchLeft(md.getOverloads(), { + onEmpty: () => md.getJsDocs(), + onNonEmpty: (head) => head.getJsDocs() + }) + const doc = parseDoc(getJSDocText(jsdocs)) + if (shouldIgnore(doc)) { + return Option.none() + } + const type = parseType(md) + const signature = `declare const ${name}: ${type}` + const position = yield* parsePosition(md) + return Option.some( + new Domain.DocEntry( + name, + doc, + signature, + position + ) + ) + }) + +const parseProperty = (pd: ast.PropertyDeclaration) => + Effect.gen(function*() { + const doc = parseDoc(getJSDocText(pd.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = pd.getName() + const type = parseType(pd) + const readonly = pipe( + Option.fromNullishOr(pd.getFirstModifierByKind(ast.ts.SyntaxKind.ReadonlyKeyword)), + Option.match({ + onNone: () => "", + onSome: () => "readonly " + }) + ) + const signature = `${readonly}${name}: ${type}` + const position = yield* parsePosition(pd) + return [ + new Domain.DocEntry( + name, + doc, + signature, + position + ) + ] + }) + +const parseProperties = (c: ast.ClassDeclaration) => { + const properties = Array.filter( + c.getProperties(), + (pd) => + !pd.isStatic() && pipe( + pd.getFirstModifierByKind(ast.ts.SyntaxKind.PrivateKeyword), + Option.fromNullishOr, + Option.isNone + ) + ) + return Effect.forEach(properties, parseProperty).pipe(Effect.map(Array.flatten)) +} + +/** + * @internal + */ +export const getConstructorDeclarationSignature = ( + c: ast.ConstructorDeclaration +): string => + pipe( + Option.fromNullishOr(c.compilerNode.body), + Option.match({ + onNone: () => c.getText(), + onSome: (body) => { + const end = body.getStart() - c.getStart() - 1 + return c.getText().substring(0, end) + } + }) + ) + +const getClassDeclarationSignature = (c: ast.ClassDeclaration) => { + const name = c.getName() ?? "" + return pipe( + Effect.succeed(getTypeParameters(c.getTypeParameters())), + Effect.map((typeParameters) => + pipe( + c.getConstructors(), + Array.matchLeft({ + onEmpty: () => `declare class ${name}${typeParameters}`, + onNonEmpty: (head) => + `declare class ${name}${typeParameters} { ${ + getConstructorDeclarationSignature( + head + ) + } }` + }) + ) + ) + ) +} + +const parseClass = (c: ast.ClassDeclaration) => + Effect.gen(function*() { + const doc = parseDoc(getJSDocText(c.getJsDocs())) + if (shouldIgnore(doc)) { + return [] + } + const name = c.getName() ?? "" + const signature = yield* getClassDeclarationSignature(c) + const methods = yield* pipe( + c.getInstanceMethods(), + Effect.forEach(parseMethod), + Effect.map(Array.getSomes) + ) + const staticMethods = yield* pipe( + c.getStaticMethods(), + Effect.forEach(parseMethod), + Effect.map(Array.getSomes) + ) + const properties = yield* parseProperties(c) + const position = yield* parsePosition(c) + return [ + new Domain.Class( + name, + doc, + signature, + position, + methods, + staticMethods, + properties + ) + ] + }) + +/** + * Parses exported classes and their documented members from the current source file. + * + * @category parsing + * @since 0.6.0 + */ +export const parseClasses = Effect.gen(function*() { + const source = yield* Source + const exportedClasses = source.sourceFile.getClasses().filter((cd) => cd.isExported()) + return yield* Effect.forEach(exportedClasses, parseClass).pipe(Effect.map(Array.flatten)) +}) + +/** + * @internal + */ +export const parseModuleDocumentation = Effect.gen(function*() { + const source = yield* Source + const statements = source.sourceFile.getStatements() + const ofirstStatement = Array.head(statements) + if (Option.isSome(ofirstStatement)) { + const oDocComment = getDocComment(ofirstStatement.value.getLeadingCommentRanges()) + if (Option.isSome(oDocComment)) { + return parseDoc(oDocComment.value.getText()) + } + } + return parseDoc("") +}) + +/** + * Parses the current source file into a module documentation model. + * + * @category parsing + * @since 0.6.0 + */ +export const parseModule = Effect.gen(function*() { + const source = yield* Source + const doc = yield* parseModuleDocumentation + const interfaces = yield* parseInterfaces + const functions = yield* parseFunctions + const typeAliases = yield* parseTypeAliases + const classes = yield* parseClasses + const constants = yield* parseConstants + const exports = yield* parseExports + const namespaces = yield* parseNamespaces + const name = source.sourceFile.getBaseName() + return new Domain.Module( + source, + name, + doc, + source.path, + classes, + interfaces, + functions, + typeAliases, + constants, + exports, + namespaces + ) +}) + +/** + * @internal + */ +export const parseFile = + (project: ast.Project) => + (file: Domain.File): Effect.Effect, Configuration.Configuration | Path.Path> => { + return Effect.gen(function*() { + const path = yield* Path.Path + const sourceFile = project.getSourceFile(file.path) + const filePath = file.path.split(path.sep) + if (sourceFile !== undefined && Array.isArrayNonEmpty(filePath)) { + return yield* Effect.provideService(parseModule, Source, { sourceFile, path: filePath }) + } + return yield* Effect.fail([`Unable to locate file: ${file.path}`]) + }) + } + +const createProject = (files: ReadonlyArray) => + Effect.gen(function*() { + const config = yield* Configuration.Configuration + const process = yield* Domain.Process + const cwd = yield* process.cwd + // Convert the raw config into a format that TS/TS-Morph expects + const parsed = ast.ts.parseJsonConfigFileContent( + { + compilerOptions: { + strict: true, + moduleResolution: "node", + ...config.parseCompilerOptions + } + }, + ast.ts.sys, + cwd + ) + + const options: ast.ProjectOptions = { + compilerOptions: parsed.options + } + const project = new ast.Project(options) + for (const file of files) { + project.addSourceFileAtPath(file.path) + } + return project + }) + +/** + * Parses source files into module documentation models sorted by path. + * + * @category parsing + * @since 0.6.0 + */ +export const parseFiles = (files: ReadonlyArray) => + createProject(files).pipe( + Effect.flatMap((project) => + pipe( + files, + Effect.validate(parseFile(project)), + Effect.map(sortModulesByPath) + ) + ) + ) diff --git a/.context/effect/packages/tools/docgen/src/Printer.ts b/.context/effect/packages/tools/docgen/src/Printer.ts new file mode 100644 index 000000000..97f743ffb --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/Printer.ts @@ -0,0 +1,383 @@ +/** + * Renders parsed documentation models as Markdown. + * + * @since 0.6.0 + */ +import * as Array from "effect/Array" +import * as Effect from "effect/Effect" +import { pipe } from "effect/Function" +import * as Order from "effect/Order" +import * as Record from "effect/Record" +import * as String from "effect/String" +import * as Prettier from "prettier" +import * as Configuration from "./Configuration.ts" +import type * as Domain from "./Domain.ts" +import * as Parser from "./Parser.ts" + +/** @internal */ +export type Printable = + | Domain.Class + | Domain.Constant + | Domain.Export + | Domain.Function + | Domain.Interface + | Domain.TypeAlias + | Domain.Namespace + +const Markdown = { + bold: (content: string) => `**${content}**`, + fence: (content: string) => `\`\`\`ts\n${content}\n\`\`\`\n\n`, + strikethrough: (content: string) => `~~${content}~~` +} + +/** + * Replaces the link from a JSDoc link tag with a simple text. + * + * Given "This is a description containing two links to {@link foo} and {@link bar baz}." + * returns "This is a description containing two links to `foo` and `baz`." + */ +function replaceJSDocLinks(text: string): string { + return text.replace(/\{@link\s+([^\s}]+)(?:\s+([^}]+))?\}/g, (_, link, label) => { + // Use the label if provided; otherwise, use the link target + return `\`${(label || link).trim()}\`` + }) +} + +/** + * Removes all extra metadata from fenced code blocks in a Markdown string. + * For each code fence, only the first token (the language identifier) is preserved. + */ +function removeFenceMetadata(markdown: string): string { + return markdown.replace(/^(`{3,})([^\n]*)/gm, (_match, fence, info) => { + // Trim the info string and split by whitespace into tokens. + // The first token (if present) is typically the language identifier. + const tokens = info.trim().split(/\s+/) + // Rebuild the fence line with just the language (if it exists) + return fence + (tokens[0] || "") + }) +} + +const printOptionalDescription = (description: string | undefined) => { + return Effect.gen(function*() { + if (description === undefined) { + return "" + } + const config = yield* Configuration.Configuration + const descriptionWithoutLinks = replaceJSDocLinks(description) + const out = config.theme === Configuration.DEFAULT_THEME + ? removeFenceMetadata(descriptionWithoutLinks) + : descriptionWithoutLinks + return `\n\n${out}` + }) +} + +const printArray = (title: string, ss?: ReadonlyArray): string => { + if (ss === undefined || ss.length === 0) { + return "" + } + return `\n\n${Markdown.bold(title)}\n\n${ss.join("\n")}` +} + +const printFence = (code: string): string => { + if (code.startsWith("```ts") || code.startsWith("~~~ts")) { + return code + } + return "```ts\n" + code + "\n```" +} + +const printOptionalSignature = (signature?: string): string => { + if (signature === undefined) { + return "" + } + return `\n\n${Markdown.bold("Signature")}\n\n${printFence(signature)}` +} + +const printThrowsArray = (throws?: ReadonlyArray): string => printArray("Throws", throws) + +const printExamplesArray = (examples: ReadonlyArray): string => { + if (examples.length === 0) { + return "" + } + return examples.map((ex) => "\n\n**Example**\n\n" + printFence(ex)).join("") +} + +const printOptionalSince = (since: ReadonlyArray): string => { + if (since.length === 0) { + return "" + } + return `\n\nSince v${since.join(", ")}` +} + +const printHeaderByIndentation = (indentation: number) => { + switch (indentation) { + case 0: + return "## " + case 1: + return "### " + default: + return "#### " + } +} + +const printTitle = (s: string, deprecated: ReadonlyArray, postfix?: string): string => { + const name = s.trim() === "hasOwnProperty" ? `${s} (function)` : s + const title = deprecated.length > 0 ? Markdown.strikethrough(name) : name + return postfix === undefined ? title : title + ` ${postfix}` +} + +const printSeesArray = (sees?: ReadonlyArray): string => { + if (sees === undefined || sees.length === 0) { + return "" + } + return `\n\n${Markdown.bold("See")}\n\n${sees.map((see) => `- ${replaceJSDocLinks(see)}`).join("\n")}` +} + +const printOptionalSourceLink = (position?: Domain.Position) => { + return Effect.gen(function*() { + if (position === undefined) { + return "" + } + const config = yield* Configuration.Configuration + const source = yield* Parser.Source + const name = source.sourceFile.getBaseName() + return `\n\n[Source](${config.srcLink}${name}#L${position.line})` + }) +} + +const printModel = (name: string, doc: Domain.Doc, options: { + readonly signature?: string | undefined + readonly position?: Domain.Position | undefined + readonly indentation?: number | undefined + readonly postfix?: string | undefined +}) => { + return Effect.gen(function*() { + const sourceLink = yield* printOptionalSourceLink(options.position) + const description = yield* printOptionalDescription(doc.description) + return printHeaderByIndentation(options.indentation ?? 0) + printTitle(name, doc.deprecated, options.postfix) + + description + + printThrowsArray(doc.throws) + + printExamplesArray(doc.examples) + + printSeesArray(doc.sees) + + printOptionalSignature(options.signature) + + sourceLink + + printOptionalSince(doc.since) + }) +} + +const printEntry = (model: Domain.DocEntry, options: { + readonly indentation?: number | undefined + readonly postfix?: string | undefined +}) => { + return printModel(model.name, model.doc, { + signature: model.signature, + position: model.position, + indentation: options.indentation, + postfix: options.postfix + }) +} + +const printStaticMethod = (model: Domain.DocEntry) => { + return printEntry(model, { + indentation: 1, + postfix: "(static method)" + }) +} + +const printMethod = (model: Domain.DocEntry) => { + return printEntry(model, { + indentation: 1, + postfix: "(method)" + }) +} + +const printProperty = (model: Domain.DocEntry) => { + return printEntry(model, { + indentation: 1, + postfix: "(property)" + }) +} + +const printClass = (model: Domain.Class) => { + return Effect.gen(function*() { + const header = yield* printEntry(model, { + postfix: "(class)" + }) + const staticMethods = yield* Effect.forEach(model.staticMethods, (method) => printStaticMethod(method)) + const methods = yield* Effect.forEach(model.methods, (method) => printMethod(method)) + const properties = yield* Effect.forEach(model.properties, (property) => printProperty(property)) + return header + + staticMethods.map((s) => "\n\n" + s).join("") + + methods.map((s) => "\n\n" + s).join("") + + properties.map((s) => "\n\n" + s).join("") + }) +} + +const printConstant = (model: Domain.Constant) => { + return printEntry(model, {}) +} + +const printExport = (model: Domain.Export) => { + return printEntry(model, { + postfix: model.isNamespaceExport ? "(namespace export)" : undefined + }) +} + +const printFunction = (model: Domain.Function) => { + return printEntry(model, {}) +} + +const printInterface = (model: Domain.Interface, indentation: number) => { + return printEntry(model, { + indentation, + postfix: "(interface)" + }) +} + +const printTypeAlias = (model: Domain.TypeAlias, indentation: number) => { + return printEntry(model, { + indentation, + postfix: "(type alias)" + }) +} + +const printNamespace = ( + model: Domain.Namespace, + indentation: number +): Effect.Effect => { + return Effect.gen(function*() { + const header = yield* printModel(model.name, model.doc, { + position: model.position, + indentation, + postfix: "(namespace)" + }) + const interfaces = yield* Effect.forEach(model.interfaces, (inter) => printInterface(inter, indentation + 1)) + const typeAliases = yield* Effect.forEach( + model.typeAliases, + (typeAlias) => printTypeAlias(typeAlias, indentation + 1) + ) + const namespaces = yield* Effect.forEach( + model.namespaces, + (namespace) => printNamespace(namespace, indentation + 1) + ) + return header + + interfaces.map((s) => "\n\n" + s).join("") + + typeAliases.map((s) => "\n\n" + s).join("") + + namespaces.map((s) => "\n\n" + s).join("") + }) +} + +/** @internal */ +export const print = (p: Printable) => { + switch (p._tag) { + case "Class": + return printClass(p) + case "Constant": + return printConstant(p) + case "Export": + return printExport(p) + case "Function": + return printFunction(p) + case "Interface": + return printInterface(p, 0) + case "TypeAlias": + return printTypeAlias(p, 0) + case "Namespace": + return printNamespace(p, 0) + } +} + +const DEFAULT_CATEGORY = "utils" + +const byCategory = Order.mapInput( + String.Order, + ([category]: [string, ...Array]) => category +) + +const getPrintables = (module: Domain.Module): ReadonlyArray => + Array.flatten([ + module.classes, + module.constants, + module.exports, + module.functions, + module.interfaces, + module.typeAliases, + module.namespaces + ]) + +const sortByName: (self: Iterable) => Array = Array.sort( + pipe( + String.Order, + Order.mapInput(({ name }: { name: string }) => name) + ) +) + +/** + * Renders a parsed module as a Markdown documentation page. + * + * @category printers + * @since 0.6.0 + */ +export const printModule = (module: Domain.Module) => { + return Effect.gen(function*() { + const description = yield* printModel(module.name, module.doc, { + postfix: "overview" + }) + + const printables = pipe( + sortByName(getPrintables(module)), + Array.groupBy((printable) => + printable.doc.category.length === 0 ? DEFAULT_CATEGORY : printable.doc.category.join(", ") + ), + Record.toEntries, + Array.sort(byCategory) + ) + + const strings = yield* Effect.forEach(printables, ([category, printables]) => + Effect.gen(function*() { + const out = `\n\n# ${category}` + const strings = yield* Effect.forEach(sortByName(printables), (printable) => print(printable)) + return out + strings.map((s) => "\n\n" + s).join("") + })) + + const content = strings.join("") + + return `${description} + +${content}` + }).pipe(Effect.provideService(Parser.Source, module.source)) +} + +const defaultPrettierOptions: Prettier.Options = { + parser: "markdown", + semi: false, + singleQuote: false, + printWidth: 120, + trailingComma: "none" +} + +/** + * Renders the front matter for a generated module page. + * + * @category printers + * @since 0.6.0 + */ +export const printFrontMatter = (module: Domain.Module, nav_order: number): string => { + return `--- +title: ${module.name} +nav_order: ${nav_order} +parent: Modules +---` +} + +/** + * Formats generated Markdown with the docgen Prettier settings. + * + * @category printers + * @since 0.6.0 + */ +export function prettify(s: string) { + return Effect.tryPromise({ + try: () => Prettier.format(s, defaultPrettierOptions), + catch: globalThis.String + }).pipe(Effect.orDie) +} diff --git a/.context/effect/packages/tools/docgen/src/bin.ts b/.context/effect/packages/tools/docgen/src/bin.ts new file mode 100755 index 000000000..91d8e9d98 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/bin.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +/** + * @since 0.6.0 + */ +import * as NodeRuntime from "@effect/platform-node/NodeRuntime" +import * as NodeServices from "@effect/platform-node/NodeServices" +import * as Effect from "effect/Effect" +import * as Layer from "effect/Layer" +import { cli } from "./CLI.ts" +import * as Configuration from "./Configuration.ts" +import * as Domain from "./Domain.ts" + +const MainLive = Configuration.configProviderLayer.pipe( + Layer.provideMerge(Layer.mergeAll(Domain.Process.layer, NodeServices.layer)) +) + +Effect.sync(() => process.argv.slice(2)).pipe( + Effect.flatMap(cli), + Effect.provide(MainLive), + NodeRuntime.runMain +) diff --git a/.context/effect/packages/tools/docgen/src/index.ts b/.context/effect/packages/tools/docgen/src/index.ts new file mode 100644 index 000000000..7679c3b12 --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/index.ts @@ -0,0 +1,28 @@ +/** + * @since 0.6.0 + */ + +/** + * @since 0.6.0 + */ +export * as Configuration from "./Configuration.ts" + +/** + * @since 0.6.0 + */ +export * as Core from "./Core.ts" + +/** + * @since 0.6.0 + */ +export * as Domain from "./Domain.ts" + +/** + * @since 0.6.0 + */ +export * as Printer from "./Printer.ts" + +/** + * @since 0.6.0 + */ +export * as Parser from "./Parser.ts" diff --git a/.context/effect/packages/tools/docgen/src/internal/markdown-toc.d.ts b/.context/effect/packages/tools/docgen/src/internal/markdown-toc.d.ts new file mode 100644 index 000000000..4b50ab81d --- /dev/null +++ b/.context/effect/packages/tools/docgen/src/internal/markdown-toc.d.ts @@ -0,0 +1,4 @@ +declare module "@effect/markdown-toc" { + const markdownToc: (content: string, options: { readonly bullets: string }) => { readonly content: string } + export default markdownToc +} diff --git a/.context/effect/packages/tools/docgen/test/Checker.test.ts b/.context/effect/packages/tools/docgen/test/Checker.test.ts new file mode 100644 index 000000000..5ee3d60ad --- /dev/null +++ b/.context/effect/packages/tools/docgen/test/Checker.test.ts @@ -0,0 +1,220 @@ +import * as Checker from "@effect/docgen/Checker" +import * as Configuration from "@effect/docgen/Configuration" +import * as Parser from "@effect/docgen/Parser" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Predicate } from "effect" +import * as Path from "effect/Path" +import * as ast from "ts-morph" + +const project = new ast.Project({ + compilerOptions: { strict: true }, + useInMemoryFileSystem: true +}) + +const defaultConfig: Configuration.ConfigurationShape = { + projectName: "docgen", + projectHomepage: "https://github.com/effect-ts/docgen", + srcLink: "https://github.com/effect-ts/docgen/blob/main/src/", + srcDir: "src", + outDir: "docs", + theme: "mikearnaldi/just-the-docs", + enableSearch: true, + enforceDescriptions: false, + enforceExamples: false, + enforceVersion: true, + runExamples: false, + tscExecutable: "tsc", + exclude: [], + parseCompilerOptions: {}, + examplesCompilerOptions: {} +} + +const makeSourcefile = (source: string | ast.SourceFile) => { + if (Predicate.isString(source)) { + const filename = `test.ts` + const existing = project.getSourceFile(filename) + if (existing) { + project.removeSourceFile(existing) + } + return project.createSourceFile(filename, source) + } + return source +} + +const makeSource = (source: string | ast.SourceFile) => { + const sourceFile = makeSourcefile(source) + const filename = sourceFile.getBaseName() + return Parser.Source.of({ + path: [filename], + sourceFile + }) +} + +const expectFailure = ( + config: Partial, + sourceText: string, + parser: Effect.Effect, + checker: (a: A) => Effect.Effect, never, Configuration.Configuration | Parser.Source>, + failure: ReadonlyArray +) => { + return Effect.gen(function*() { + const actual = yield* Effect.exit(parser.pipe( + Effect.flatMap(checker), + Effect.provideService(Parser.Source, makeSource(sourceText)), + Effect.provideService(Configuration.Configuration, { ...defaultConfig, ...config }), + Effect.provide(Path.layer) + )) + assert.ok(actual._tag === "Success") + assert.deepStrictEqual(actual.value, failure) + }) +} + +describe("Checker", () => { + describe("checkFunctions", () => { + it.effect("should raise an error if `@since` tag is missing", () => + expectFailure( + {}, + ` +/** @since 1.0.0 */ +export function a() {} + +/** description */ +export function b() {} + `, + Parser.parseFunctions, + Checker.checkFunctions, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + " 4 |\n" + + " 5 | /** description */\n" + + "> 6 | export function b() {}\n" + + " | ^\n" + + " 7 | " + ] + )) + }) + + describe("checkExports", () => { + it.effect("should raise an error if `@since` tag is missing", () => + expectFailure( + {}, + "export { a }", + Parser.parseExports, + Checker.checkExports, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + "> 1 | export { a }\n" + + " | ^" + ] + )) + }) + + describe("checkNamespaces", () => { + it.effect("should raise an error if `@since` tag is missing", () => + expectFailure( + {}, + "export namespace A {}", + Parser.parseNamespaces, + Checker.checkNamespaces, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + "> 1 | export namespace A {}\n" + + " | ^" + ] + )) + + it.effect("should raise an error if `@since` tag is missing on a nested interface", () => + expectFailure( + {}, + ` + /** + * @since 1.0.0 + */ + export namespace A { + export interface B {} + } + `, + Parser.parseNamespaces, + Checker.checkNamespaces, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + " 4 | */\n" + + " 5 | export namespace A {\n" + + "> 6 | export interface B {}\n" + + " | ^\n" + + " 7 | }\n" + + " 8 | " + ] + )) + + it.effect("should raise an error if `@since` tag is missing on a nested type alias", () => + expectFailure( + {}, + ` + /** + * @since 1.0.0 + */ + export namespace A { + export type B = string + } + `, + Parser.parseNamespaces, + Checker.checkNamespaces, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + " 4 | */\n" + + " 5 | export namespace A {\n" + + "> 6 | export type B = string\n" + + " | ^\n" + + " 7 | }\n" + + " 8 | " + ] + )) + + it.effect("should raise an error if `@since` tag is missing on a nested namespace", () => + expectFailure( + {}, + ` + /** + * @since 1.0.0 + */ + export namespace A { + export namespace B {} + } + `, + Parser.parseNamespaces, + Checker.checkNamespaces, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + " 4 | */\n" + + " 5 | export namespace A {\n" + + "> 6 | export namespace B {}\n" + + " | ^\n" + + " 7 | }\n" + + " 8 | " + ] + )) + }) + + describe("checkClasses", () => { + it.effect("should raise an error if `@since` tag is missing", () => + expectFailure( + {}, + `export class MyClass {}`, + Parser.parseClasses, + Checker.checkClasses, + [ + "Missing `@since` tag in file /test.ts:\n" + + "\n" + + "> 1 | export class MyClass {}\n" + + " | ^" + ] + )) + }) +}) diff --git a/.context/effect/packages/tools/docgen/test/Configuration.test.ts b/.context/effect/packages/tools/docgen/test/Configuration.test.ts new file mode 100644 index 000000000..653c05ce2 --- /dev/null +++ b/.context/effect/packages/tools/docgen/test/Configuration.test.ts @@ -0,0 +1,333 @@ +import * as CLI from "@effect/docgen/CLI" +import * as Configuration from "@effect/docgen/Configuration" +import * as Domain from "@effect/docgen/Domain" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, describe, it } from "@effect/vitest" +import * as Context from "effect/Context" +import * as Effect from "effect/Effect" +import * as Exit from "effect/Exit" +import * as FileSystem from "effect/FileSystem" +import * as Layer from "effect/Layer" +import * as Option from "effect/Option" +import * as Path from "effect/Path" +import * as Result from "effect/Result" +import * as Stdio from "effect/Stdio" +import * as CliOutput from "effect/unstable/cli/CliOutput" +import * as Command from "effect/unstable/cli/Command" + +type DocgenJson = typeof Configuration.ConfigurationSchema.Type + +const existingFile = `${import.meta.dirname}/fixtures/invalid-json.txt` + +const fileInfo: FileSystem.File.Info = { + type: "File", + mtime: Option.none(), + atime: Option.none(), + birthtime: Option.none(), + dev: 0, + ino: Option.none(), + mode: 0, + nlink: Option.none(), + uid: Option.none(), + gid: Option.none(), + rdev: Option.none(), + size: FileSystem.Size(0), + blksize: Option.none(), + blocks: Option.none() +} + +class DocgenJsonTag extends Context.Service()("DocgenJsonTag") {} + +const makeDocgenJson = (config: DocgenJson) => Layer.succeed(DocgenJsonTag, config) + +const TestFileSystem = Layer.effect( + FileSystem.FileSystem, + Effect.gen(function*() { + const path = yield* Path.Path + const context = yield* Effect.context() + const docgenJson = Option.getOrElse(Context.getOption(context, DocgenJsonTag), () => ({} as DocgenJson)) + const readFileString: FileSystem.FileSystem["readFileString"] = (filePath) => { + const fileName = path.basename(filePath) + if (fileName === "package.json") { + return Effect.succeed(JSON.stringify({ name: "name", homepage: "homepage" })) + } else if (fileName === "docgen.json") { + return Effect.succeed(JSON.stringify(docgenJson)) + } + return Effect.die(`file not found: ${filePath}`) + } + const exists: FileSystem.FileSystem["exists"] = (filePath) => { + const fileName = path.basename(filePath) + if (fileName === "invalid-json.txt") { + return Effect.succeed(true) + } + if (fileName === "docgen.json") { + return Effect.succeed(Context.getOption(context, DocgenJsonTag).pipe(Option.isSome)) + } + return Effect.succeed(false) + } + const stat: FileSystem.FileSystem["stat"] = (filePath) => + path.basename(filePath) === "invalid-json.txt" ? Effect.succeed(fileInfo) : Effect.die("file not found") + return FileSystem.makeNoop({ exists, readFileString, stat }) + }) +).pipe(Layer.provide(Path.layer)) + +const makeProcess = (env: Record = {}) => + Layer.succeed(Domain.Process, { + cwd: Effect.sync(() => process.cwd()), + platform: Effect.sync(() => process.platform), + argv: Effect.sync(() => process.argv), + env: Effect.succeed(env) + }) + +const makeTestLive = (env: Record = {}) => + Configuration.configProviderLayer.pipe( + Layer.fresh, + Layer.provideMerge(Layer.mergeAll( + CliOutput.layer(CliOutput.defaultFormatter({ colors: false })), + NodeServices.layer, + Stdio.layerTest({}), + makeProcess(env), + Path.layer, + TestFileSystem + )) + ) + +const testCliFor = ( + program: Effect.Effect +) => { + const command = CLI.docgenCommand.pipe( + Command.withHandler(() => program), + Command.provideEffect(Configuration.Configuration, CLI.loadConfiguration) + ) + return Command.runWith(command, { version: "v1.0.0" }) +} + +describe("Configuration", () => { + it.effect("should use the default configuration if no configuration is provided", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.deepStrictEqual(config, { + projectName: "name", + projectHomepage: "homepage", + srcLink: "homepage/blob/main/src/", + srcDir: "src", + outDir: "docs", + theme: "mikearnaldi/just-the-docs", + enableSearch: true, + enforceDescriptions: false, + enforceExamples: false, + enforceVersion: true, + runExamples: false, + tscExecutable: "tsc", + exclude: [], + parseCompilerOptions: Configuration.defaultCompilerOptions, + examplesCompilerOptions: Configuration.defaultCompilerOptions + }) + }) + return testCliFor(program)([]).pipe(Effect.provide(makeTestLive())) + }) + + it.effect("should use the configuration contained in docgen.json if it exists", () => { + const parseCompilerOptions = { + noEmit: true, + strict: true, + skipLibCheck: true, + exactOptionalPropertyTypes: true, + moduleResolution: "Bundler", + target: "ES2022", + lib: ["ES2022", "DOM"] + } + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.deepStrictEqual(config, { + projectName: "name", + projectHomepage: "myproject", + srcLink: "mygithub", + srcDir: "src", + outDir: "docs", + theme: "mikearnaldi/just-the-docs", + enableSearch: true, + enforceDescriptions: false, + enforceExamples: false, + enforceVersion: true, + runExamples: false, + tscExecutable: "tsc", + exclude: [], + parseCompilerOptions, + examplesCompilerOptions: Configuration.defaultCompilerOptions + }) + }) + return testCliFor(program)([]).pipe( + Effect.provide( + makeTestLive().pipe(Layer.provide(makeDocgenJson({ + projectHomepage: "myproject", + srcLink: "mygithub", + parseCompilerOptions + }))) + ) + ) + }) + + it.effect("should raise a validation error if docgen.json is not valid", () => + Effect.gen(function*() { + const result = yield* Effect.exit( + testCliFor(Effect.void)([]).pipe( + Effect.provide(makeTestLive().pipe(Layer.provide(makeDocgenJson({ projectHomepage: 1 } as any)))) + ) + ) + if (Exit.isSuccess(result)) { + return assert.fail("expected configuration validation to fail") + } + assert.include(globalThis.String(result.cause), "Configuration.validateJsonFile") + assert.include(globalThis.String(result.cause), "projectHomepage") + })) + + it.effect("accepts inverse search and version flags", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.isTrue(config.enableSearch) + assert.isTrue(config.enforceVersion) + }) + return testCliFor(program)(["--enable-search", "--enforce-version"]).pipe( + Effect.provide( + makeTestLive().pipe(Layer.provide(makeDocgenJson({ + enableSearch: false, + enforceVersion: false + }))) + ) + ) + }) + + it.effect("retains boolean values from docgen.json", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.isFalse(config.enableSearch) + assert.isFalse(config.enforceVersion) + }) + return testCliFor(program)([]).pipe( + Effect.provide( + makeTestLive().pipe(Layer.provide(makeDocgenJson({ + enableSearch: false, + enforceVersion: false + }))) + ) + ) + }) + + it.effect("retains primary search and version flags", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.isFalse(config.enableSearch) + assert.isFalse(config.enforceVersion) + }) + return testCliFor(program)(["--disable-search", "--no-enforce-version"]).pipe( + Effect.provide(makeTestLive()) + ) + }) + + it.effect("automatic negative flags override true environment configuration", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.isFalse(config.enforceDescriptions) + assert.isFalse(config.enforceExamples) + assert.isFalse(config.runExamples) + }) + return testCliFor(program)([ + "--no-enforce-descriptions", + "--no-enforce-examples", + "--no-run-examples" + ]).pipe(Effect.provide(makeTestLive({ + DOCGEN_ENFORCE_DESCRIPTIONS: "true", + DOCGEN_ENFORCE_EXAMPLES: "true", + DOCGEN_RUN_EXAMPLES: "true" + }))) + }) + + it.effect("docgen.json retains its later runExamples precedence", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.isTrue(config.runExamples) + }) + return testCliFor(program)(["--no-run-examples"]).pipe( + Effect.provide( + makeTestLive({ DOCGEN_RUN_EXAMPLES: "true" }).pipe( + Layer.provide(makeDocgenJson({ runExamples: true })) + ) + ) + ) + }) + + it.effect("loads comma-delimited environment arrays before docgen.json", () => { + const program = Effect.gen(function*() { + const process = yield* Domain.Process + assert.deepStrictEqual(yield* process.env, { DOCGEN_EXCLUDE: "a,b" }) + const config = yield* Configuration.Configuration + assert.deepStrictEqual(config.exclude, ["a", "b"]) + }) + return testCliFor(program)([]).pipe( + Effect.provide( + makeTestLive({ DOCGEN_EXCLUDE: "a,b" }).pipe( + Layer.provide(makeDocgenJson({ exclude: ["from-docgen"] })) + ) + ) + ) + }) + + it.effect("parses inline compiler options as JSON records", () => { + const program = Effect.gen(function*() { + const config = yield* Configuration.Configuration + assert.deepStrictEqual(config.parseCompilerOptions, { strict: false }) + assert.deepStrictEqual(config.examplesCompilerOptions, { module: "ESNext" }) + }) + return testCliFor(program)([ + "--parse-compiler-options", + "{\"strict\":false}", + "--examples-compiler-options", + "{\"module\":\"ESNext\"}" + ]).pipe(Effect.provide(makeTestLive())) + }) + + it.effect("rejects both compiler option forms for the same category", () => + Effect.gen(function*() { + const cases = [ + ["--parse-tsconfig-file", "--parse-compiler-options"], + ["--examples-tsconfig-file", "--examples-compiler-options"] + ] as const + for (const [fileFlag, inlineFlag] of cases) { + const result = yield* Effect.result( + testCliFor(Effect.void)([fileFlag, existingFile, inlineFlag, "{}"]).pipe( + Effect.provide(makeTestLive()) + ) + ) + if (Result.isSuccess(result)) { + return assert.fail(`${fileFlag} and ${inlineFlag} should be mutually exclusive`) + } + assert.strictEqual(result.failure._tag, "InvalidValue") + if (result.failure._tag === "InvalidValue") { + assert.include(result.failure.expected, `only one of ${fileFlag} or ${inlineFlag}`) + } + } + })) + + it.effect("rejects non-record inline compiler options", () => + Effect.gen(function*() { + for (const flag of ["--parse-compiler-options", "--examples-compiler-options"]) { + for (const value of ["null", "[]", "1", "\"text\""]) { + const result = yield* Effect.result( + testCliFor(Effect.void)([flag, value]).pipe(Effect.provide(makeTestLive())) + ) + if (Result.isSuccess(result)) { + return assert.fail(`${flag} should reject ${value}`) + } + assert.strictEqual(result.failure._tag, "ShowHelp") + if (result.failure._tag === "ShowHelp") { + assert.isTrue( + result.failure.errors.some((error) => + error._tag === "InvalidValue" && error.expected.includes("JSON record") + ) + ) + } + } + } + })) +}) diff --git a/.context/effect/packages/tools/docgen/test/Core.test.ts b/.context/effect/packages/tools/docgen/test/Core.test.ts new file mode 100644 index 000000000..fd16ccf2c --- /dev/null +++ b/.context/effect/packages/tools/docgen/test/Core.test.ts @@ -0,0 +1,66 @@ +import * as Core from "@effect/docgen/Core" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { assert, describe, it } from "@effect/vitest" +import * as Effect from "effect/Effect" + +const assertFencedCode = ( + markdown: string, + expectedExamples: ReadonlyArray, + expectedWarnings: ReadonlyArray +) => { + assert.deepStrictEqual(Core.extractFencedCode(markdown), [expectedExamples, expectedWarnings]) +} + +describe("Core", () => { + describe("[internal] extractFencedCode", () => { + it("should extract fenced code blocks from markdown (backticks)", () => { + assertFencedCode("a\n\n```ts\nconst a = 1\n```\n\nb", ["const a = 1"], []) + }) + + it("should extract fenced code blocks from markdown (tildes)", () => { + assertFencedCode("a\n\n~~~ts\nconst a = 1\n~~~~\n\nb", ["const a = 1"], []) + }) + + it("should skip-type-checking (backticks)", () => { + assertFencedCode("a\n\n```ts skip-type-checking a=1\nconst a = 1\n```\n\nb", [], []) + }) + + it("should skip-type-checking (tildes)", () => { + assertFencedCode("a\n\n~~~ts skip-type-checking a=1\nconst a = 1\n~~~~\n\nb", [], []) + }) + + it("should handle metadata (backticks)", () => { + assertFencedCode("a\n\n```ts a=1\nconst a = 1\n```\n\nb", ["const a = 1"], []) + }) + + it("should handle metadata (tildes)", () => { + assertFencedCode("a\n\n~~~ts a=1\nconst a = 1\n~~~~\n\nb", ["const a = 1"], []) + }) + + it("should handle non closing fences (backticks)", () => { + assertFencedCode("a\n\n```ts\nconst a = 1", ["const a = 1"], [ + "Code block does not have a matching closing fence:\na\n\n```ts\nconst a = 1" + ]) + }) + + it("should handle non closing fences (tildes)", () => { + assertFencedCode("a\n\n~~~ts\nconst a = 1", ["const a = 1"], [ + "Code block does not have a matching closing fence:\na\n\n~~~ts\nconst a = 1" + ]) + }) + }) + + describe("[internal] runCommand", () => { + it.effect("streams output without a maxBuffer limit", () => + Effect.gen(function*() { + const size = 1024 * 1024 + 1 + const result = yield* Core.runCommand("node", [ + "-e", + `process.stdout.write("x".repeat(${size})); process.stderr.write("problem"); process.exitCode = 2` + ], false) + assert.strictEqual(result.stdout.length, size) + assert.strictEqual(result.stderr, "problem") + assert.strictEqual(result.exitCode, 2) + }).pipe(Effect.scoped, Effect.provide(NodeServices.layer))) + }) +}) diff --git a/.context/effect/packages/tools/docgen/test/Parser.test.ts b/.context/effect/packages/tools/docgen/test/Parser.test.ts new file mode 100644 index 000000000..7b831bbd6 --- /dev/null +++ b/.context/effect/packages/tools/docgen/test/Parser.test.ts @@ -0,0 +1,1829 @@ +import * as Configuration from "@effect/docgen/Configuration" +import * as Domain from "@effect/docgen/Domain" +import * as Parser from "@effect/docgen/Parser" +import * as Printer from "@effect/docgen/Printer" +import { assert, describe, it } from "@effect/vitest" +import { Effect, Exit, Predicate } from "effect" +import * as Path from "effect/Path" +import * as ast from "ts-morph" + +let testCounter = 0 + +const project = new ast.Project({ + compilerOptions: { strict: true }, + useInMemoryFileSystem: true +}) + +const defaultConfig: Configuration.ConfigurationShape = { + projectName: "docgen", + projectHomepage: "https://github.com/effect-ts/docgen", + srcLink: "https://github.com/effect-ts/docgen/blob/main/src/", + srcDir: "src", + outDir: "docs", + theme: "mikearnaldi/just-the-docs", + enableSearch: true, + enforceDescriptions: false, + enforceExamples: false, + enforceVersion: true, + runExamples: false, + tscExecutable: "tsc", + exclude: [], + parseCompilerOptions: {}, + examplesCompilerOptions: {} +} + +const makeSourcefile = (source: string | ast.SourceFile) => { + if (Predicate.isString(source)) { + const filename = `test.ts` + const existing = project.getSourceFile(filename) + if (existing) { + project.removeSourceFile(existing) + } + return project.createSourceFile(filename, source) + } + return source +} + +const makeSource = (source: string | ast.SourceFile) => { + const sourceFile = makeSourcefile(source) + const filename = sourceFile.getBaseName() + return Parser.Source.of({ + path: [filename], + sourceFile + }) +} + +const print = (printables: ReadonlyArray) => { + return Effect.gen(function*() { + const strings = yield* Effect.forEach(printables, (printable) => Printer.print(printable)) + return strings.join("\n") + }) +} + +const isModule = (printableOr: ReadonlyArray | Domain.Module): printableOr is Domain.Module => { + return !Array.isArray(printableOr) +} + +const expectMarkdown = Effect.fnUntraced(function*( + eff: Effect.Effect< + ReadonlyArray | Domain.Module, + E, + Parser.Source | Configuration.Configuration | Path.Path + >, + sourceText: string, + expected: string, + config?: Partial +) { + const exit = yield* Effect.exit(eff.pipe( + Effect.flatMap((printableOr) => { + if (isModule(printableOr)) { + return Printer.printModule(printableOr) + } + return print(printableOr) + }), + Effect.provideService(Parser.Source, makeSource(sourceText)), + Effect.provideService(Configuration.Configuration, { ...defaultConfig, ...config }), + Effect.provide(Path.layer) + )) + assert.ok(exit._tag === "Success") + if (exit.value !== expected) { + console.log(exit.value) + } + assert.strictEqual(exit.value, expected) +}) + +describe("Parser", () => { + describe("parseModule", () => { + it.effect("should not require an example for modules when `enforceExamples` is set to true", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseModule, + `/** +* This is the assert module. +* +* @since 1.0.0 +*/ +import * as assert from 'assert' + +/** + * This is the foo export. + * + * @example + * import { foo } from 'test' + * + * console.log(foo) + * + * @category category + * @since 1.0.0 + */ +export const foo = 'foo'`, + `## test.ts overview + +This is the assert module. + +Since v1.0.0 + + + +# category + +## foo + +This is the foo export. + +**Example** + +\`\`\`ts +import { foo } from 'test' + +console.log(foo) +\`\`\` + +**Signature** + +\`\`\`ts +declare const foo: "foo" +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L19) + +Since v1.0.0` + ) + })) + + it.effect("should ignore non-JSDoc comments above JSDoc comments", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseModule, + `/** +* This is the assert module. +* +* @since 1.0.0 +*/ +import * as assert from 'assert' + +// This comment should be ignored + +/** + * This is the foo export. + * + * @example + * import { foo } from 'test' + * + * console.log(foo) + * + * @category category + * @since 1.0.0 + */ +export const foo = 'foo'`, + `## test.ts overview + +This is the assert module. + +Since v1.0.0 + + + +# category + +## foo + +This is the foo export. + +**Example** + +\`\`\`ts +import { foo } from 'test' + +console.log(foo) +\`\`\` + +**Signature** + +\`\`\`ts +declare const foo: "foo" +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L21) + +Since v1.0.0` + ) + })) + }) + + describe("parseFunctions", () => { + it.effect("omits internal properties from signatures", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @since 1.0.0 + */ + export const myfunc: (options: { + readonly visible?: string + /** @internal */ + readonly internal?: boolean + }) => void = () => {}`, + `## myfunc + +**Signature** + +\`\`\`ts +declare const myfunc: (options: { readonly visible?: string; }) => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L4) + +Since v1.0.0` + ) + })) + + it.effect(`should remove all metadata from typedcript code blocks when the theme is ${Configuration.DEFAULT_THEME}`, () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * \`\`\`ts skip-type-checking a=1 showLineNumbers=true + * const a: string = 1 + * \`\`\` + * + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +\`\`\`ts +const a: string = 1 +\`\`\` + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L8) + +Since v1.0.0` + ) + })) + it.effect("generics", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * This is a description containing two links to {@link foo} and {@link bar}. + * + * @since 1.2.0 + */ + export function myfunc() {}`, + `## myfunc + +This is a description containing two links to \`foo\` and \`bar\`. + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.2.0` + ) + })) + + it.effect("description", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * This is a description containing two links to {@link foo} and {@link bar}. + * + * @since 1.2.0 + */ + export function myfunc() {}`, + `## myfunc + +This is a description containing two links to \`foo\` and \`bar\`. + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.2.0` + ) + })) + + it.effect("throws", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @throws \`Error1\` - Description 1 + * @throws \`Error2\` - Description 2 + * @since 1.2.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Throws** + +\`Error1\` - Description 1 +\`Error2\` - Description 2 + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L7) + +Since v1.2.0` + ) + })) + + it.effect("sees", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @see \`foo\` Description 1 + * @see {@link bar} Description 2 + * @see {@link baz quux} Description 2 + * @since 1.2.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**See** + +- \`foo\` Description 1 +- \`bar\` Description 2 +- \`quux\` Description 2 + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L8) + +Since v1.2.0` + ) + })) + + it.effect("example without fence", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @example + * const x = 1 + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Example** + +\`\`\`ts +const x = 1 +\`\`\` + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L7) + +Since v1.0.0` + ) + })) + + it.effect("example with backtick fence", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @example + * \`\`\`ts + * const x = 1 + * \`\`\` + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Example** + +\`\`\`ts +const x = 1 +\`\`\` + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.0` + ) + })) + + it.effect("2 examples", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @example + * \`\`\`ts + * const x = 1 + * \`\`\` + * @example + * \`\`\`ts + * const x = 2 + * \`\`\` + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Example** + +\`\`\`ts +const x = 1 +\`\`\` + +**Example** + +\`\`\`ts +const x = 2 +\`\`\` + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L13) + +Since v1.0.0` + ) + })) + + it.effect("example with metas", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @example + * \`\`\`ts a=1 + * const x = 1 + * \`\`\` + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Example** + +\`\`\`ts a=1 +const x = 1 +\`\`\` + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.0` + ) + })) + + it.effect("example with titde fence", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + * @example + * ~~~ts + * const x = 1 + * ~~~ + * @since 1.0.0 + */ + export function myfunc() {}`, + `## myfunc + +description... + +**Example** + +~~~ts +const x = 1 +~~~ + +**Signature** + +\`\`\`ts +declare const myfunc: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.0` + ) + })) + + it.effect("should not return private function declarations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * description... + */ + function myfunc() {}`, + "" + ) + })) + + it.effect("should not return ignored function declarations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @ignore + */ + export function myfunc() {}`, + "" + ) + })) + + it.effect("should not return ignored function declarations with overloads", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @ignore + */ + export function sum(a: number, b: number) + export function sum(a: number, b: number): number { return a + b }`, + "" + ) + })) + + it.effect("should not return internal function declarations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @internal + */ + export function sum(a: number, b: number): number { return a + b }`, + "" + ) + })) + + it.effect("should not return internal function declarations even with overloads", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @internal + */ + export function sum(a: number, b: number) + export function sum(a: number, b: number): number { return a + b }`, + "" + ) + })) + + it.effect("should not return private const function declarations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `const sum = (a: number, b: number): number => a + b `, + "" + ) + })) + + it.effect("should not return internal const function declarations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @internal + */ + export const sum = (a: number, b: number): number => a + b `, + "" + ) + })) + + it.effect("should account for nullable polymorphic return types", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @since 1.0.0 + */ + export const toNullable = (ma: A | null): A | null => ma`, + `## toNullable + +**Signature** + +\`\`\`ts +declare const toNullable: (ma: A | null) => A | null +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L4) + +Since v1.0.0` + ) + })) + + it.effect("should handle a const function declaration", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * a description... + * @since 1.0.0 + * @example + * assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) + * @example + * assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 }) + * @deprecated + */ + export const f = (a: number, b: number): { [key: string]: number } => ({ a, b })`, + `## ~~f~~ + +a description... + +**Example** + +\`\`\`ts +assert.deepStrictEqual(f(1, 2), { a: 1, b: 2 }) +\`\`\` + +**Example** + +\`\`\`ts +assert.deepStrictEqual(f(3, 4), { a: 3, b: 4 }) +\`\`\` + +**Signature** + +\`\`\`ts +declare const f: (a: number, b: number) => { [key: string]: number; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L10) + +Since v1.0.0` + ) + })) + + it.effect("should handle a function declaration", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * @since 1.0.0 + */ + export function f(a: number, b: number): { [key: string]: number } { return { a, b } }`, + `## f + +**Signature** + +\`\`\`ts +declare const f: (a: number, b: number) => { [key: string]: number; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L4) + +Since v1.0.0` + ) + })) + + it.effect("should handle overloadings", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseFunctions, + `/** + * a description... + * @since 1.0.0 + * @deprecated + */ + export function f(a: Int, b: Int): { [key: string]: number } + export function f(a: number, b: number): { [key: string]: number } + export function f(a: any, b: any): { [key: string]: number } { return { a, b } }`, + `## ~~f~~ + +a description... + +**Signature** + +\`\`\`ts +declare const f: { (a: Int, b: Int): { [key: string]: number; }; (a: number, b: number): { [key: string]: number; }; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L8) + +Since v1.0.0` + ) + })) + }) + + describe("parseConstants", () => { + it.effect("should handle a constant value", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseConstants, + `/** + * a description... + * @since 1.0.0 + * @deprecated + */ + export const s: string = ''`, + `## ~~s~~ + +a description... + +**Signature** + +\`\`\`ts +declare const s: string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0` + ) + })) + + it.effect("should support constants with default type parameters", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseConstants, + `/** + * @since 1.0.0 + */ + export const left: (l: E) => string = T.left`, + `## left + +**Signature** + +\`\`\`ts +declare const left: (l: E) => string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L4) + +Since v1.0.0` + ) + })) + + it.effect("should support untyped constants", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseConstants, + ` + class A {} + /** + * @since 1.0.0 + */ + export const empty = new A()`, + `## empty + +**Signature** + +\`\`\`ts +declare const empty: A +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0` + ) + })) + + it.effect("should handle constants with typeof annotations", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseConstants, + ` const task: { a: number } = { + a: 1 + } + /** + * @since 1.0.0 + */ + export const taskSeq: typeof task = { + ...task, + ap: (mab, ma) => () => mab().then(f => ma().then(a => f(a))) + }`, + `## taskSeq + +**Signature** + +\`\`\`ts +declare const taskSeq: { a: number; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L7) + +Since v1.0.0` + ) + })) + + it.effect("should not include variables declared in for loops", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseConstants, + ` const object = { a: 1, b: 2, c: 3 }; + + for (const property in object) { + console.log(property); + }`, + "" + ) + })) + }) + + describe("parseTypeAliases", () => { + it.effect("should return a type alias", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseTypeAliases, + ` + type None = { readonly _tag: "None" } + type Some = { readonly _tag: "Some"; readonly value: A } + /** + * a description... + * @since 1.0.0 + * @deprecated + */ + export type Option = None | Some`, + `## ~~Option~~ (type alias) + +a description... + +**Signature** + +\`\`\`ts +type Option = None | Some +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.0` + ) + })) + }) + + describe("parseExports", () => { + it.effect("should return no exports if the file is empty", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseExports, + "", + "" + ) + })) + + it.effect("should return an `Export`", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseExports, + ` + const a = 1; + const b = 2; + export { + /** + * description_of_a + * \`\`\`ts + * const a: string = 1 + * \`\`\` + * + * @since 1.0.0 + */ + a, + /** + * description_of_b + * @since 2.0.0 + */ + b + }`, + `## a + +description_of_a +\`\`\`ts +const a: string = 1 +\`\`\` + +**Signature** + +\`\`\`ts +declare const a: 1 +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L13) + +Since v1.0.0 +## b + +description_of_b + +**Signature** + +\`\`\`ts +declare const b: 2 +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L18) + +Since v2.0.0` + ) + })) + + it.effect("should handle renamimg", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseExports, + `const a = 1; + export { + /** + * @since 1.0.0 + */ + a as b + }`, + `## b + +**Signature** + +\`\`\`ts +declare const b: 1 +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0` + ) + })) + + it.effect("should handle a single re-export", () => + Effect.gen(function*() { + project.createSourceFile("a.ts", `export const a = 1`) + const sourceFile = project.createSourceFile( + "b.ts", + `import { a } from './a' + const b = a + export { + /** + * @since 1.0.0 + */ + b + }` + ) + const actual = yield* Effect.exit(Parser.parseExports.pipe( + Effect.provideService(Parser.Source, makeSource(sourceFile)), + Effect.provideService(Configuration.Configuration, defaultConfig) + )) + assert.deepStrictEqual( + actual, + Exit.succeed([ + new Domain.Export( + "b", + new Domain.Doc( + undefined, + ["1.0.0"], + [], + [], + [], + [], + [], + { + "since": ["1.0.0"] + } + ), + "declare const b: 1", + { + "column": 11, + "line": 7 + }, + false + ) + ]) + ) + })) + + it.effect("should handle `export * from ...`", () => + Effect.gen(function*() { + project.createSourceFile("example.ts", `export const a = 1`, { overwrite: true }) + + const sourceFile = project.createSourceFile( + "export-all.ts", + ` + /** + * @since 1.0.0 + */ + export * from './example' + ` + ) + + const actual = yield* Effect.exit(Parser.parseExports.pipe( + Effect.provideService(Parser.Source, makeSource(sourceFile)), + Effect.provideService(Configuration.Configuration, defaultConfig) + )) + + assert.deepStrictEqual( + actual, + Exit.succeed([ + new Domain.Export( + "'./example'", + new Domain.Doc( + "Re-exports all named exports from the './example' module.", + ["1.0.0"], + [], + [], + [], + [], + [], + { + "since": ["1.0.0"] + } + ), + "export * from './example'", + { + "column": 10, + "line": 5 + }, + true + ) + ]) + ) + })) + + it.effect("should handle `export * as ... from ...`", () => + Effect.gen(function*() { + project.createSourceFile("example.ts", `export const a = 1`, { overwrite: true }) + + const sourceFile = project.createSourceFile( + "export-all-namespace.ts", + ` + /** + * @since 1.0.0 + */ + export * as example from './example' + ` + ) + + const actual = yield* Effect.exit(Parser.parseExports.pipe( + Effect.provideService(Parser.Source, makeSource(sourceFile)), + Effect.provideService(Configuration.Configuration, defaultConfig) + )) + + assert.deepStrictEqual( + actual, + Exit.succeed([ + new Domain.Export( + "example", + new Domain.Doc( + "Re-exports all named exports from the './example' module as `example`.", + ["1.0.0"], + [], + [], + [], + [], + [], + { + "since": ["1.0.0"] + } + ), + "export * as example from './example'", + { + "column": 11, + "line": 5 + }, + true + ) + ]) + ) + })) + }) + + describe("parseInterfaces", () => { + it.effect("should return no interfaces if the file is empty", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseInterfaces, + "", + "" + ) + })) + + it.effect("should return no interfaces if there are no exported interfaces", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseInterfaces, + "interface A {}", + "" + ) + })) + + it.effect("should return an interface", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseInterfaces, + `/** + * a description... + * @since 1.0.0 + * @deprecated + */ + export interface A {}`, + `## ~~A~~ (interface) + +a description... + +**Signature** + +\`\`\`ts +export interface A {} +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0` + ) + })) + }) + + describe("parseNamespaces", () => { + it.effect("should return no namespaces if the file is empty", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + "", + "" + ) + })) + + it.effect("should return no namespaces if there are no exported namespaces", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + "namespace A {}", + "" + ) + })) + + it.effect("should parse an empty Namespace", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A {} + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + + describe("namespace > interfaces", () => { + it.effect("should ignore not exported interfaces", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A { + interface C {} + } + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + + it.effect("should parse an interface", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` +/** + * @since 1.0.0 + */ +export namespace A { + /** + * @since 1.0.1 + */ + export interface B { + readonly d: boolean + } +} + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0 + +### B (interface) + +**Signature** + +\`\`\`ts +export interface B { + readonly d: boolean + } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.1` + ) + })) + }) + + describe("namespace > type aliases", () => { + it.effect("should ignore not exported type aliases", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A { + type C = number + } + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + + it.effect("should parse a type alias", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A { + /** + * @since 1.0.1 + */ + export type B = string + } + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0 + +### B (type alias) + +**Signature** + +\`\`\`ts +type B = string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.1` + ) + })) + }) + + describe("namespace > nested namespaces", () => { + it.effect("should ignore not exported namespaces", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A { + namespace B {} + } + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + + it.effect("should parse a namespace", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseNamespaces, + ` + /** + * @since 1.0.0 + */ + export namespace A { + /** + * @since 1.0.1 + */ + export namespace B { + /** + * @since 1.0.2 + */ + export type C = string + } + } + `, + `## A (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0 + +### B (namespace) + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.1 + +#### C (type alias) + +**Signature** + +\`\`\`ts +type C = string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L13) + +Since v1.0.2` + ) + })) + }) + }) + + describe("parseClasses", () => { + it.effect("should ignore `@internal` classes", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** @internal */export class MyClass {}`, + "" + ) + })) + + it.effect("should ignore `@ignore` classes", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + ` + /** @ignore */ + export class MyClass {} + `, + "" + ) + })) + + it.effect("should ignore not exported classes", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + ` + class MyClass {} + `, + "" + ) + })) + + it.effect("should skip ignored properties", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * @since 1.0.0 + */ + export class MyClass { + /** + * @ignore + */ + readonly _A!: A + }`, + `## MyClass (class) + +**Signature** + +\`\`\`ts +declare class MyClass +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L4) + +Since v1.0.0` + ) + })) + + it.effect("should skip the constructor body", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * description + * @since 1.0.0 + */ + export class C { constructor() {} }`, + `## C (class) + +description + +**Signature** + +\`\`\`ts +declare class C { constructor() } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + + it("should get a constructor declaration signature", () => { + const sourceFile = project.createSourceFile( + `test-${testCounter++}.ts`, + ` + /** + * @since 1.0.0 + */ + declare class A { + constructor() + } + ` + ) + + const constructorDeclaration = sourceFile + .getClass("A")! + .getConstructors()[0] + + assert.deepStrictEqual( + Parser.getConstructorDeclarationSignature(constructorDeclaration), + "constructor()" + ) + }) + + it.effect("should handle non-readonly properties", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * description + * @since 1.0.0 + */ + export class C { + /** + * @since 1.0.0 + */ + a: string + }`, + `## C (class) + +description + +**Signature** + +\`\`\`ts +declare class C +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0 + +### a (property) + +**Signature** + +\`\`\`ts +a: string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L9) + +Since v1.0.0` + ) + })) + + it.effect("should return a `Class`", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * a class description... + * @since 1.0.0 + * @deprecated + */ + export class Test { + /** + * a property... + * @since 1.1.0 + * @deprecated + */ + readonly a: string + private readonly b: number + /** + * a static method description... + * @since 1.1.0 + * @deprecated + */ + static f(): void {} + constructor(readonly value: string) { } + /** + * a method description... + * @since 1.1.0 + * @deprecated + */ + g(a: number, b: number): { [key: string]: number } { + return { a, b } + } + }`, + `## ~~Test~~ (class) + +a class description... + +**Signature** + +\`\`\`ts +declare class Test { constructor(readonly value: string) } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0 + +### ~~f~~ (static method) + +a static method description... + +**Signature** + +\`\`\`ts +declare const f: () => void +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L19) + +Since v1.1.0 + +### ~~g~~ (method) + +a method description... + +**Signature** + +\`\`\`ts +declare const g: (a: number, b: number) => { [key: string]: number; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L26) + +Since v1.1.0 + +### ~~a~~ (property) + +a property... + +**Signature** + +\`\`\`ts +readonly a: string +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L12) + +Since v1.1.0` + ) + })) + + it.effect("should handle method overloadings", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * a class description... + * @since 1.0.0 + * @deprecated + */ + export class Test { + /** + * a static method description... + * @since 1.1.0 + * @deprecated + */ + static f(x: number): number + static f(x: string): string + static f(x: any): any {} + constructor(readonly value: A) { } + /** + * a method description... + * @since 1.1.0 + * @deprecated + */ + map(f: (a: number) => number): Test + map(f: (a: string) => string): Test + map(f: (a: any) => any): any { + return new Test(f(this.value)) + } + }`, + `## ~~Test~~ (class) + +a class description... + +**Signature** + +\`\`\`ts +declare class Test { constructor(readonly value: A) } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L6) + +Since v1.0.0 + +### ~~f~~ (static method) + +a static method description... + +**Signature** + +\`\`\`ts +declare const f: { (x: number): number; (x: string): string; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L14) + +Since v1.1.0 + +### ~~map~~ (method) + +a method description... + +**Signature** + +\`\`\`ts +declare const map: { (f: (a: number) => number): Test; (f: (a: string) => string): Test; } +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L23) + +Since v1.1.0` + ) + })) + + it.effect("should ignore internal/ignored methods (#42)", () => + Effect.gen(function*() { + yield* expectMarkdown( + Parser.parseClasses, + `/** + * a class description... + * @since 1.0.0 + */ + export class Test { + /** + * @since 0.0.1 + * @internal + **/ + private foo(): void {} + /** + * @since 0.0.1 + * @ignore + **/ + private bar(): void {} + }`, + `## Test (class) + +a class description... + +**Signature** + +\`\`\`ts +declare class Test +\`\`\` + +[Source](https://github.com/effect-ts/docgen/blob/main/src/test.ts#L5) + +Since v1.0.0` + ) + })) + }) + + describe("parseFile", () => { + it.effect("should not parse a non-existent file", () => + Effect.gen(function*() { + const file = new Domain.File("non-existent.ts", "") + const project = new ast.Project({ useInMemoryFileSystem: true }) + + assert.deepStrictEqual( + yield* Effect.exit( + Parser.parseFile(project)(file).pipe( + Effect.provideService(Configuration.Configuration, defaultConfig), + Effect.provide(Path.layer) + ) + ), + Exit.fail(["Unable to locate file: non-existent.ts"]) + ) + })) + }) + + describe("utils", () => { + it("parseComment", () => { + assert.deepStrictEqual(Parser.parseComment(""), { + description: undefined, + tags: {} + }) + + assert.deepStrictEqual(Parser.parseComment("/** description */"), { + description: "description", + tags: {} + }) + + assert.deepStrictEqual( + Parser.parseComment("/** description\n * @since 1.0.0\n */"), + { + description: "description", + tags: { + since: ["1.0.0"] + } + } + ) + + assert.deepStrictEqual( + Parser.parseComment("/** description\n * @deprecated\n */"), + { + description: "description", + tags: { + deprecated: [""] + } + } + ) + + assert.deepStrictEqual( + Parser.parseComment("/** description\n * @category instance\n */"), + { + description: "description", + tags: { + category: ["instance"] + } + } + ) + }) + }) +}) diff --git a/.context/effect/packages/tools/docgen/test/fixtures/invalid-json.txt b/.context/effect/packages/tools/docgen/test/fixtures/invalid-json.txt new file mode 100644 index 000000000..8e9352830 --- /dev/null +++ b/.context/effect/packages/tools/docgen/test/fixtures/invalid-json.txt @@ -0,0 +1 @@ +{] diff --git a/.context/effect/packages/tools/docgen/tsconfig.json b/.context/effect/packages/tools/docgen/tsconfig.json new file mode 100644 index 000000000..b512ffd91 --- /dev/null +++ b/.context/effect/packages/tools/docgen/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/.context/effect/packages/tools/doctest/CHANGELOG.md b/.context/effect/packages/tools/doctest/CHANGELOG.md new file mode 100644 index 000000000..a05241fce --- /dev/null +++ b/.context/effect/packages/tools/doctest/CHANGELOG.md @@ -0,0 +1,47 @@ +# @effect/doctest + +## 4.0.0-rc.108 + +### Patch Changes + +- [#7151](https://github.com/Effect-TS/effect/pull/7151) [`e15fa96`](https://github.com/Effect-TS/effect/commit/e15fa96239e4a4dfbf99d975cd8700d392d42c1a) Thanks @f15u! - Support `.mdx` files +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6794](https://github.com/Effect-TS/effect/pull/6794) [`8e89cc6`](https://github.com/Effect-TS/effect/commit/8e89cc60a64e93a599af265dd75db3c7c511d7f8) Thanks @fubhy! - Add convention-based `// =>` assertions that compare documentation example values using Effect equality. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 diff --git a/.context/effect/packages/tools/doctest/README.md b/.context/effect/packages/tools/doctest/README.md new file mode 100644 index 000000000..883f2b9d3 --- /dev/null +++ b/.context/effect/packages/tools/doctest/README.md @@ -0,0 +1,84 @@ +# @effect/doctest + +`@effect/doctest` extracts marked TypeScript examples from JSDoc comments, Markdown, and MDX files, then runs each example as an isolated Vitest module. + +## Installation + +```sh +npm install -D @effect/doctest@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/doctest) + +## Usage + +Mark runnable fences with `import.meta.vitest`: + +````ts +/** + * ```ts import.meta.vitest name="adds two numbers" + * 1 + 1 // => 2 + * ``` + */ +export const value = 1 +```` + +The optional `name="..."` metadata labels the test without appearing in the example body. Unnamed examples use the opening fence line, such as `line 12`; Vitest displays the containing file alongside it. + +## Inline assertions + +Add a trailing `// =>` comment to assert the value of an expression: + +````ts +/** + * ```ts import.meta.vitest + * import { Array, Option } from "effect" + * + * Array.get([1, 2, 3], 1) // => Option.some(2) + * Array.get([1, 2, 3], 10) // => Option.none() + * ``` + */ +export const value = 1 +```` + +The expected value is a TypeScript expression evaluated in the same lexical scope. Values are compared with Effect's `Equal.equals` semantics, so the convention supports primitives, arrays, plain objects, and Effect data types such as `Option`, `Result`, `Exit`, and `HashMap` without converting them to console output. + +Prefer asserting the API call directly instead of introducing a binding used only by the assertion. Keep bindings for reuse or meaningful multi-step setup, with a blank line before a later assertion block. Keep the call and assertion on one line when it fits within 120 characters, and format expected arrays densely, for example `[1, 2]`, `[[1], [2]]`, and `Option.some([1, 2])`. Preserve runnable markers on type-level examples without adding tautological runtime assertions. + +An assertion may also trail a single initialized `const` declaration with an identifier binding. The initializer is evaluated once and the binding remains available to subsequent code: + +````ts +/** + * ```ts import.meta.vitest + * import { Effect, Option } from "effect" + * + * const result = await Effect.runPromise(Effect.succeed(Option.some(1))) // => Option.some(1) + * Option.isSome(result) // => true + * ``` + */ +export const value = 1 +```` + +Markers must trail a complete expression statement or supported `const` declaration on the same line. Standalone markers, destructuring declarations, multiple declarations, and `let` or `var` declarations are not supported. The transform does not implicitly await promises, run Effects, or consume iterators; write those operations explicitly. Ordinary comments are ignored. Await asynchronous work so all assertions and cleanup occur before the snippet module finishes evaluating. + +Regular tests can use `include` in the same project. Documentation sources use `includeSource`, which lets Vitest discard files without the marker before collection. The plugin resolves imports relative to each example's original TypeScript, Markdown, or MDX file: + +```ts +import * as Doctest from "@effect/doctest/Plugin" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [Doctest.plugin()], + test: { + include: ["test/**/*.test.ts"], + includeSource: ["src/**/*.ts", "docs/**/*.{md,mdx}"] + } +}) +``` + +Source files selected by `includeSource` are collected through generated doctest collectors and are not executed. Native in-source tests using `import.meta.vitest` are therefore not supported by this plugin. Regular test files included through `test.include` continue to run normally. + +The plugin configures `@effect/doctest/Runner` when no test runner is specified. If `test.runner` is already configured, the plugin leaves it unchanged; that runner is then responsible for integrating doctest collection when required. diff --git a/.context/effect/packages/tools/doctest/package.json b/.context/effect/packages/tools/doctest/package.json new file mode 100644 index 000000000..0123e16e6 --- /dev/null +++ b/.context/effect/packages/tools/doctest/package.json @@ -0,0 +1,63 @@ +{ + "name": "@effect/doctest", + "version": "4.0.0-rc.108", + "type": "module", + "license": "MIT", + "description": "Runs TypeScript documentation examples as Vitest tests", + "homepage": "https://effect.website", + "repository": { + "type": "git", + "url": "https://github.com/Effect-TS/effect.git", + "directory": "packages/tools/doctest" + }, + "sideEffects": [], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts", + "./*": "./src/*.ts", + "./index": null, + "./internal/*": null + }, + "files": [ + "src/**/*.ts", + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" + ], + "publishConfig": { + "access": "public", + "provenance": true, + "exports": { + "./package.json": "./package.json", + ".": "./dist/index.js", + "./*": "./dist/*.js", + "./index": null, + "./internal/*": null + } + }, + "scripts": { + "codegen": "effect-utils codegen", + "build": "tsc -b tsconfig.json && pnpm babel", + "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", + "check": "tsc -b tsconfig.json" + }, + "dependencies": { + "rolldown": "^1.1.5" + }, + "peerDependencies": { + "effect": "workspace:^", + "vite": ">=8.1.5 <9.0.0", + "vitest": ">=4.1.10 <5.0.0" + }, + "devDependencies": { + "@effect/vitest": "workspace:^", + "@types/node": "^26.1.2", + "effect": "workspace:^", + "vite": "^8.1.5", + "vitest": "^4.1.10" + } +} diff --git a/.context/effect/packages/tools/doctest/src/Plugin.ts b/.context/effect/packages/tools/doctest/src/Plugin.ts new file mode 100644 index 000000000..d0735859e --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Plugin.ts @@ -0,0 +1,120 @@ +/** + * @since 4.0.0 + */ + +import { normalizePath, type Plugin } from "vite" +import type { TestUserConfig } from "vitest/config" +import * as Protocol from "./Protocol.ts" +import * as Source from "./Source.ts" +import { transform } from "./Transform.ts" + +const runner = "@effect/doctest/Runner" + +const collectorModule = ( + file: string, + snippets: ReadonlyArray, + version?: string | undefined +): string => { + const tests = snippets.map((snippet, index) => { + const id = JSON.stringify(Protocol.snippetId(file, index, version)) + const label = JSON.stringify(snippet.name ?? `line ${snippet.line}`) + return `test(${label}, () => import(${id}))` + }) + + return `import { test } from "@effect/doctest/Runtime"\n\n${tests.join("\n\n")}\n` +} + +/** + * Creates a Vite plugin that transforms marked documentation snippets into Vitest tests. + * + * **Details** + * + * Use Vitest's `includeSource` option to discover files containing `import.meta.vitest`. The plugin collects marked files through a collector module without executing the source module. Every snippet executes in its own module, with imports resolved relative to the original source file. Native in-source tests are not supported. + * + * @category testing + * @since 4.0.0 + */ +export const plugin = (): Plugin => { + const store = new Map>() + const cache = new Map> + }>() + + const loadExamples = ( + file: string, + version?: string | undefined + ): Promise> => { + const cached = cache.get(file) + if (cached !== undefined && cached.version === version) { + return cached.value + } + + const loaded = Source.extractFile(file).then((snippets) => { + store.set(file, snippets) + return snippets + }) + + cache.set(file, { version, value: loaded }) + return loaded + } + + return { + name: "effect-doctest", + enforce: "pre", + perEnvironmentWatchChangeDuringDev: true, + config(config) { + if (config.test?.runner !== undefined) { + return undefined + } + + return { test: { runner } satisfies TestUserConfig } + }, + resolveId(source, importer, options) { + const collector = Protocol.request(Protocol.collectorPrefix, source) + if (collector !== undefined) { + return Protocol.resolvedId("collector", collector) + } + + const snippet = Protocol.request(Protocol.snippetPrefix, source) + if (snippet !== undefined && store.has(snippet.file)) { + return Protocol.resolvedId("snippet", snippet) + } + + const parent = importer === undefined ? undefined : Protocol.resolvedRequest(importer) + if (parent?.kind !== "snippet") { + return null + } + + return this.resolve(source, parent.file, { ...options, skipSelf: true }) + }, + load(id) { + const loaded = Protocol.resolvedRequest(id) + if (loaded === undefined) { + return null + } + + this.addWatchFile(loaded.file) + return loadExamples(loaded.file, loaded.version).then((snippets) => { + if (loaded.kind === "collector") { + return collectorModule(loaded.file, snippets, loaded.version) + } + + const snippet = loaded.index === undefined ? undefined : snippets[loaded.index] + if (snippet === undefined) { + throw new Error(`Unknown documentation snippet module '${id}'`) + } + + return transform(snippet.source, loaded.file, snippet.line) + }) + }, + watchChange(id) { + const normalized = normalizePath(id) + for (const file of store.keys()) { + if (normalizePath(file) === normalized) { + cache.delete(file) + } + } + } + } +} diff --git a/.context/effect/packages/tools/doctest/src/Protocol.ts b/.context/effect/packages/tools/doctest/src/Protocol.ts new file mode 100644 index 000000000..6adb1b95c --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Protocol.ts @@ -0,0 +1,145 @@ +/** + * @since 4.0.0 + */ + +/** + * Prefix for virtual doctest collector module requests. + * + * @category constants + * @since 4.0.0 + */ +export const collectorPrefix = "virtual:effect-doctest/collector?" + +/** + * Prefix for virtual doctest snippet module requests. + * + * @category constants + * @since 4.0.0 + */ +export const snippetPrefix = "virtual:effect-doctest/snippet?" + +/** + * Query parameter used to identify resolved doctest modules. + * + * @category constants + * @since 4.0.0 + */ +export const resolvedMarker = "effect-doctest" + +/** + * Describes a virtual doctest module request. + * + * @category models + * @since 4.0.0 + */ +export interface Request { + readonly file: string + readonly index?: number | undefined + readonly version?: string | undefined +} + +/** + * Parses a virtual doctest request with the specified prefix. + * + * @category protocols + * @since 4.0.0 + */ +export const request = (prefix: string, id: string): Request | undefined => { + if (!id.startsWith(prefix)) return undefined + const parameters = new URLSearchParams(id.slice(prefix.length)) + const file = parameters.get("file") + const index = parameters.get("index") + const version = parameters.get("version") + if (file === null || (index !== null && !/^\d+$/.test(index))) { + return undefined + } + + return { + file, + index: index === null ? undefined : Number(index), + version: version === null ? undefined : version + } +} + +/** + * Parses the identifier of a resolved doctest module. + * + * @category protocols + * @since 4.0.0 + */ +export const resolvedRequest = ( + id: string +): (Request & { readonly kind: "collector" | "snippet" }) | undefined => { + const query = id.indexOf("?") + if (query === -1) { + return undefined + } + + const parameters = new URLSearchParams(id.slice(query + 1)) + const kind = parameters.get(resolvedMarker) + if (kind !== "collector" && kind !== "snippet") { + return undefined + } + + const index = parameters.get("index") + const version = parameters.get("version") + if (kind === "snippet" && (index === null || !/^\d+$/.test(index))) { + return undefined + } + + return { + file: id.slice(0, query), + index: index === null ? undefined : Number(index), + version: version === null ? undefined : version, + kind + } +} + +/** + * Creates the identifier of a resolved doctest module. + * + * @category protocols + * @since 4.0.0 + */ +export const resolvedId = (kind: "collector" | "snippet", value: Request): string => { + const parameters = new URLSearchParams({ [resolvedMarker]: kind }) + if (value.index !== undefined) { + parameters.set("index", String(value.index)) + } + + if (value.version !== undefined) { + parameters.set("version", value.version) + } + + return `${value.file}?${parameters}` +} + +/** + * Creates a virtual doctest collector module identifier. + * + * @category protocols + * @since 4.0.0 + */ +export const collectorId = (file: string, version?: string | undefined): string => { + const parameters = new URLSearchParams({ file }) + if (version !== undefined) { + parameters.set("version", version) + } + + return `${collectorPrefix}${parameters}` +} + +/** + * Creates a virtual doctest snippet module identifier. + * + * @category protocols + * @since 4.0.0 + */ +export const snippetId = (file: string, index: number, version?: string | undefined): string => { + const parameters = new URLSearchParams({ file, index: String(index) }) + if (version !== undefined) { + parameters.set("version", version) + } + + return `${snippetPrefix}${parameters}` +} diff --git a/.context/effect/packages/tools/doctest/src/Runner.ts b/.context/effect/packages/tools/doctest/src/Runner.ts new file mode 100644 index 000000000..499dac99f --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Runner.ts @@ -0,0 +1,41 @@ +/** + * @since 4.0.0 + */ + +import { createHash } from "node:crypto" +import { readFile } from "node:fs/promises" +import { TestRunner } from "vitest" +import * as Protocol from "./Protocol.ts" + +/** + * Wraps a Vitest runner so marked documentation files use doctest collectors. + * + * @category testing + * @since 4.0.0 + */ +export const wrap = (Base: typeof TestRunner): typeof TestRunner => { + return class DoctestRunner extends Base { + override importFile(filepath: string, source: "collect" | "setup"): unknown { + if (source !== "collect") { + return super.importFile(filepath, source) + } + + return readFile(filepath).then((contents) => { + if (!contents.includes("import.meta.vitest")) { + return super.importFile(filepath, source) + } + + const version = createHash("sha256").update(contents).digest("hex").slice(0, 16) + return super.importFile(Protocol.collectorId(filepath, version), source) + }) + } + } +} + +/** + * Vitest runner that routes marked documentation files to doctest collectors. + * + * @category testing + * @since 4.0.0 + */ +export default wrap(TestRunner) diff --git a/.context/effect/packages/tools/doctest/src/Runtime.ts b/.context/effect/packages/tools/doctest/src/Runtime.ts new file mode 100644 index 000000000..5ad8c890a --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Runtime.ts @@ -0,0 +1,33 @@ +/** + * @since 4.0.0 + */ + +import * as Equal from "effect/Equal" +import * as assert from "node:assert" +import { test as vitest } from "vitest" + +/** + * Asserts that two values are equal using Effect's equality semantics. + * + * @category testing + * @since 4.0.0 + */ +export const assertEquals = (actual: A, expected: A): void => { + if (!Equal.equals(actual, expected)) { + assert.deepStrictEqual(actual, expected) + assert.fail("Expected values to be Equal.equals") + } +} + +/** + * Registers a documentation snippet as a test. + * + * @category testing + * @since 4.0.0 + */ +export const test = ( + name: string, + run: () => unknown | PromiseLike +): void => { + vitest(name, run) +} diff --git a/.context/effect/packages/tools/doctest/src/Source.ts b/.context/effect/packages/tools/doctest/src/Source.ts new file mode 100644 index 000000000..9185b2ed5 --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Source.ts @@ -0,0 +1,103 @@ +/** + * @since 4.0.0 + */ + +import { readFile } from "node:fs/promises" + +/** + * Represents an executable TypeScript code snippet. + * + * @category models + * @since 4.0.0 + */ +export interface Snippet { + readonly source: string + readonly line: number + readonly name: string | undefined +} + +/** + * Identifies how documentation is embedded in the source text. + * + * @category models + * @since 4.0.0 + */ +export type SourceFormat = "jsdoc" | "markdown" + +const jsdocPattern = /\/\*\*[\s\S]*?\*\//g +const fencePattern = /(?:```|~~~)(.*?)\n([\s\S]*?)(?:(?:```|~~~)|$)/g +const runnableMarker = "import.meta.vitest" +const namePattern = /(?:^|\s)name=(?:"([^"]+)"|'([^']+)'|([^\s]+))/ + +const lineNumberAt = (source: string): (offset: number) => number => { + const starts = [0] + for (let index = 0; index < source.length; index++) { + if (source.charCodeAt(index) === 10) starts.push(index + 1) + } + + return (offset) => { + let low = 0 + let high = starts.length + while (low < high) { + const middle = (low + high) >>> 1 + if (starts[middle] <= offset) low = middle + 1 + else high = middle + } + return low + } +} + +const snippets = ( + text: string, + offset: number, + lineAt: (offset: number) => number, + jsdoc = false +): ReadonlyArray => { + return Array.from(text.matchAll(fencePattern)).flatMap((match) => { + const metadata = match[1].toLowerCase() + if ((!metadata.startsWith("ts") && !metadata.startsWith("typescript")) || !metadata.includes(runnableMarker)) { + return [] + } + + const name = namePattern.exec(match[1]) + const source = jsdoc ? match[2].replace(/^[ \t]*\* ?/gm, "").trim() : match[2].trim() + + return [{ + source, + line: lineAt(offset + (match.index ?? 0)), + name: name?.[1] ?? name?.[2] ?? name?.[3] + }] + }) +} + +/** + * Extracts marked TypeScript code snippets from documentation text. + * + * **Details** + * + * Extraction uses an ordered text scan and does not parse the containing source file. + * + * @category extraction + * @since 4.0.0 + */ +export const extract = (source: string, format: SourceFormat = "jsdoc"): ReadonlyArray => { + if (!source.includes(runnableMarker)) { + return [] + } + + const lineAt = lineNumberAt(source) + return format === "markdown" + ? snippets(source, 0, lineAt) + : Array.from(source.matchAll(jsdocPattern)).flatMap((match) => snippets(match[0], match.index ?? 0, lineAt, true)) +} + +/** + * Reads a file and extracts its marked code snippets. + * + * @category extraction + * @since 4.0.0 + */ +export const extractFile = (file: string): Promise> => + readFile(file, "utf8").then((source) => + extract(source, file.endsWith(".md") || file.endsWith(".mdx") ? "markdown" : "jsdoc") + ) diff --git a/.context/effect/packages/tools/doctest/src/Transform.ts b/.context/effect/packages/tools/doctest/src/Transform.ts new file mode 100644 index 000000000..49c78a0c8 --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/Transform.ts @@ -0,0 +1,208 @@ +/** + * @since 4.0.0 + */ + +import { RolldownMagicString } from "rolldown" +import { type ESTree, parseSync } from "rolldown/utils" + +const markerPattern = /^ => (.+)$/ +const helper = "__effect_doctest_assert_" +const statementTypes = new Set([ + "ClassDeclaration", + "ExportAllDeclaration", + "ExportDefaultDeclaration", + "ExportNamedDeclaration", + "FunctionDeclaration", + "ImportDeclaration", + "TSDeclareFunction", + "TSEnumDeclaration", + "TSExportAssignment", + "TSImportEqualsDeclaration", + "TSInterfaceDeclaration", + "TSModuleDeclaration", + "TSNamespaceExportDeclaration", + "TSTypeAliasDeclaration", + "VariableDeclaration" +]) + +interface Node { + readonly type: string + readonly start: number + readonly end: number + readonly [key: string]: unknown +} + +interface Candidate { + readonly node: Node + readonly parent: Node | undefined + readonly depth: number +} + +const isNode = (value: unknown): value is Node => + typeof value === "object" && value !== null && + "type" in value && typeof value.type === "string" && + "start" in value && typeof value.start === "number" && + "end" in value && typeof value.end === "number" + +const isStatement = (node: Node): boolean => node.type.endsWith("Statement") || statementTypes.has(node.type) + +const collect = (program: ESTree.Program): { readonly candidates: Array; readonly names: Set } => { + const candidates: Array = [] + const names = new Set() + + const visit = (node: Node, parent: Node | undefined, depth: number): void => { + if (isStatement(node)) candidates.push({ node, parent, depth }) + if (node.type === "Identifier" && typeof node.name === "string") names.add(node.name) + + for (const value of Object.values(node)) { + if (isNode(value)) { + visit(value, node, depth + 1) + } else if (Array.isArray(value)) { + for (const child of value) { + if (isNode(child)) visit(child, node, depth + 1) + } + } + } + } + + visit(program as unknown as Node, undefined, 0) + return { candidates, names } +} + +const location = (source: string, offset: number, file: string, line: number): string => { + let localLine = 1 + let column = 1 + for (let index = 0; index < offset; index++) { + if (source.charCodeAt(index) === 10) { + localLine++ + column = 1 + } else { + column++ + } + } + return `${file}:${line + localLine}:${column}` +} + +const fail = (source: string, offset: number, file: string, line: number, message: string): never => { + throw new Error(`${location(source, offset, file, line)} ${message}`) +} + +const isUnbracedControlFlow = (parent: Node | undefined): boolean => + parent !== undefined && new Set([ + "DoWhileStatement", + "ForInStatement", + "ForOfStatement", + "ForStatement", + "IfStatement", + "LabeledStatement", + "WhileStatement", + "WithStatement" + ]).has(parent.type) + +const assertionTarget = ( + source: string, + candidates: ReadonlyArray, + comment: { readonly start: number }, + file: string, + line: number +): Candidate => { + const target = candidates + .filter(({ node }) => node.end <= comment.start && /^[ \t]*$/.test(source.slice(node.end, comment.start))) + .sort((left, right) => right.node.end - left.node.end || right.depth - left.depth)[0] + + if (target === undefined) { + return fail(source, comment.start, file, line, "doctest assertion must trail a complete statement on the same line") + } + if (isUnbracedControlFlow(target.parent)) { + return fail(source, comment.start, file, line, "doctest assertions in control flow require an explicit block") + } + return target +} + +const parseExpected = (expected: string, source: string, offset: number, file: string, line: number): void => { + const parsed = parseSync(`${file}?doctest-expected`, `const __expected = (${expected})`, { lang: "ts" }) + const error = parsed.errors[0] + if (error !== undefined) { + fail(source, offset, file, line, `invalid doctest expected expression: ${error.message}`) + } +} + +/** + * Transforms trailing doctest assertion comments into executable assertions. + * + * **Details** + * + * An assertion may trail an expression statement or a single initialized + * `const` identifier. Expected values are TypeScript expressions evaluated in + * the same lexical scope. + * + * @category testing + * @since 4.0.0 + */ +export const transform = (source: string, file: string, line: number): string => { + if (!source.includes("// =>")) return source + + const parsed = parseSync(file, source, { lang: "ts" }) + const parseError = parsed.errors[0] + if (parseError !== undefined) { + fail(source, parseError.labels[0]?.start ?? 0, file, line, `invalid doctest source: ${parseError.message}`) + } + + const markers = parsed.comments.flatMap((comment) => { + if (comment.type !== "Line") return [] + const match = markerPattern.exec(comment.value) + return match === null ? [] : [{ comment, expected: match[1].trim() }] + }) + if (markers.length === 0) return source + + const { candidates, names } = collect(parsed.program) + let index = 0 + while (names.has(`${helper}${index}`)) index++ + const alias = `${helper}${index}` + const output = new RolldownMagicString(source) + + for (const { comment, expected } of markers) { + parseExpected(expected, source, comment.start, file, line) + const { node, parent } = assertionTarget(source, candidates, comment, file, line) + + if (node.type === "ExpressionStatement") { + if (node.directive !== null) { + fail(source, comment.start, file, line, "doctest assertions cannot target directives") + } + const expression = isNode(node.expression) + ? node.expression + : fail(source, comment.start, file, line, "doctest assertion has no expression") + output.overwrite( + expression.start, + comment.end, + `${alias}(${source.slice(expression.start, expression.end)}, ${expected})` + ) + continue + } + + if (node.type === "VariableDeclaration" && parent?.type !== "ExportNamedDeclaration") { + const declarations = node.declarations + const declaration = Array.isArray(declarations) && declarations.length === 1 ? declarations[0] : undefined + const id = isNode(declaration) && isNode(declaration.id) ? declaration.id : undefined + if ( + node.kind === "const" && node.declare === false && isNode(declaration?.init) && + id?.type === "Identifier" && typeof id.name === "string" + ) { + const indentation = source.slice(source.lastIndexOf("\n", node.start - 1) + 1, node.start) + output.overwrite(node.end, comment.end, `\n${indentation}${alias}(${id.name}, ${expected})`) + continue + } + } + + fail( + source, + comment.start, + file, + line, + "doctest assertions can only target expression statements or a single initialized const identifier" + ) + } + + output.append(`\n\nimport { assertEquals as ${alias} } from "@effect/doctest/Runtime"`) + return output.toString() +} diff --git a/.context/effect/packages/tools/doctest/src/index.ts b/.context/effect/packages/tools/doctest/src/index.ts new file mode 100644 index 000000000..b29fe396d --- /dev/null +++ b/.context/effect/packages/tools/doctest/src/index.ts @@ -0,0 +1,35 @@ +/** + * @since 4.0.0 + */ + +// @barrel: Auto-generated exports. Do not edit manually. + +/** + * @since 4.0.0 + */ +export * as Plugin from "./Plugin.ts" + +/** + * @since 4.0.0 + */ +export * as Protocol from "./Protocol.ts" + +/** + * @since 4.0.0 + */ +export * as Runner from "./Runner.ts" + +/** + * @since 4.0.0 + */ +export * as Runtime from "./Runtime.ts" + +/** + * @since 4.0.0 + */ +export * as Source from "./Source.ts" + +/** + * @since 4.0.0 + */ +export * as Transform from "./Transform.ts" diff --git a/.context/effect/packages/tools/doctest/test/Plugin.test.ts b/.context/effect/packages/tools/doctest/test/Plugin.test.ts new file mode 100644 index 000000000..f4fe70e46 --- /dev/null +++ b/.context/effect/packages/tools/doctest/test/Plugin.test.ts @@ -0,0 +1,67 @@ +import * as Doctest from "@effect/doctest/Plugin" +import * as Protocol from "@effect/doctest/Protocol" +import { assert, describe, it } from "@effect/vitest" +import { mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +describe("Plugin", () => { + it("configures the doctest runner by default", () => { + const config = Doctest.plugin().config + if (typeof config !== "function") return assert.fail("expected config hook") + + assert.deepStrictEqual(config.call({} as never, {}, {} as never), { + test: { runner: "@effect/doctest/Runner" } + }) + }) + + it("preserves a configured runner", () => { + const config = Doctest.plugin().config + if (typeof config !== "function") return assert.fail("expected config hook") + + assert.isUndefined(config.call({} as never, { test: { runner: "./custom-runner.ts" } }, {} as never)) + }) + + it("transforms inline assertions in snippet modules", async () => { + const root = mkdtempSync(join(tmpdir(), "effect-doctest-plugin-")) + const file = join(root, "example.ts") + writeFileSync( + file, + [ + "/**", + " * ```ts import.meta.vitest name=asserted", + " * const result = 1 // => 1", + " * ```", + " */" + ].join("\n") + ) + + try { + const plugin = Doctest.plugin() + const resolveId = plugin.resolveId + const load = plugin.load + if (typeof resolveId !== "function" || typeof load !== "function") { + return assert.fail("expected function plugin hooks") + } + const context = { addWatchFile() {} } as never + const id = Protocol.collectorId(file, "test") + const resolved = await resolveId.call(context, id, undefined, {} as never) + if (typeof resolved !== "string") return assert.fail("expected resolved collector ID") + const source = await load.call(context, resolved, {} as never) + if (typeof source !== "string") return assert.fail("expected collector source") + + assert.match(source, /import \{ test \} from "@effect\/doctest\/Runtime"/) + assert.match(source, /test\("asserted", \(\) => import\([^)]*\)\)/) + + const snippetId = Protocol.snippetId(file, 0, "test") + const resolvedSnippet = await resolveId.call(context, snippetId, undefined, {} as never) + if (typeof resolvedSnippet !== "string") return assert.fail("expected resolved snippet ID") + const snippet = await load.call(context, resolvedSnippet, {} as never) + if (typeof snippet !== "string") return assert.fail("expected snippet source") + assert.match(snippet, /const result = 1\n__effect_doctest_assert_0\(result, 1\)/) + assert.match(snippet, /assertEquals as __effect_doctest_assert_0/) + } finally { + rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/.context/effect/packages/tools/doctest/test/Protocol.test.ts b/.context/effect/packages/tools/doctest/test/Protocol.test.ts new file mode 100644 index 000000000..0bd5c8ad8 --- /dev/null +++ b/.context/effect/packages/tools/doctest/test/Protocol.test.ts @@ -0,0 +1,78 @@ +import * as Protocol from "@effect/doctest/Protocol" +import { assert, describe, it } from "@effect/vitest" + +describe("Protocol", () => { + it("round-trips collector requests", () => { + const id = Protocol.collectorId("/src/a file.ts", "version-1") + + assert.deepStrictEqual(Protocol.request(Protocol.collectorPrefix, id), { + file: "/src/a file.ts", + index: undefined, + version: "version-1" + }) + }) + + it("round-trips snippet requests", () => { + const id = Protocol.snippetId("/src/example.ts", 3, "version-1") + + assert.deepStrictEqual(Protocol.request(Protocol.snippetPrefix, id), { + file: "/src/example.ts", + index: 3, + version: "version-1" + }) + }) + + it("rejects malformed virtual requests", () => { + for ( + const id of [ + "other:file", + Protocol.snippetPrefix, + `${Protocol.snippetPrefix}file=example.ts&index=-1`, + `${Protocol.snippetPrefix}file=example.ts&index=1.5` + ] + ) { + assert.isUndefined(Protocol.request(Protocol.snippetPrefix, id)) + } + }) + + it("round-trips resolved collector identifiers", () => { + const id = Protocol.resolvedId("collector", { + file: "/src/example.ts", + version: "version-1" + }) + + assert.deepStrictEqual(Protocol.resolvedRequest(id), { + file: "/src/example.ts", + index: undefined, + version: "version-1", + kind: "collector" + }) + }) + + it("round-trips resolved snippet identifiers", () => { + const id = Protocol.resolvedId("snippet", { + file: "/src/example.ts", + index: 2 + }) + + assert.deepStrictEqual(Protocol.resolvedRequest(id), { + file: "/src/example.ts", + index: 2, + version: undefined, + kind: "snippet" + }) + }) + + it("rejects malformed resolved identifiers", () => { + for ( + const id of [ + "/src/example.ts", + "/src/example.ts?effect-doctest=unknown", + "/src/example.ts?effect-doctest=snippet", + "/src/example.ts?effect-doctest=snippet&index=one" + ] + ) { + assert.isUndefined(Protocol.resolvedRequest(id)) + } + }) +}) diff --git a/.context/effect/packages/tools/doctest/test/Runtime.test.ts b/.context/effect/packages/tools/doctest/test/Runtime.test.ts new file mode 100644 index 000000000..c2f15c16c --- /dev/null +++ b/.context/effect/packages/tools/doctest/test/Runtime.test.ts @@ -0,0 +1,18 @@ +import * as Runtime from "@effect/doctest/Runtime" +import { assert, describe, it } from "@effect/vitest" +import { HashMap, Option } from "effect" + +describe("Runtime", () => { + it("compares Effect values with Equal.equals", () => { + Runtime.assertEquals(Option.some(1), Option.some(1)) + Runtime.assertEquals(HashMap.make(["a", 1]), HashMap.make(["a", 1])) + }) + + it("reports unequal values", () => { + assert.throws(() => Runtime.assertEquals(Option.some(1), Option.some(2))) + }) +}) + +Runtime.test("registers documentation tests", () => { + Runtime.assertEquals(Option.none(), Option.none()) +}) diff --git a/.context/effect/packages/tools/doctest/test/Source.test.ts b/.context/effect/packages/tools/doctest/test/Source.test.ts new file mode 100644 index 000000000..57508e6ec --- /dev/null +++ b/.context/effect/packages/tools/doctest/test/Source.test.ts @@ -0,0 +1,139 @@ +import * as Source from "@effect/doctest/Source" +import { assert, describe, it } from "@effect/vitest" + +describe("Source", () => { + describe("JSDoc", () => { + it("extracts marked TypeScript fences in source order", () => { + const source = [ + "/**", + " * First example.", + " * ```ts import.meta.vitest name=first", + " * const first = 1", + " * ```", + " */", + "const first = 1", + "/**", + " * ~~~typescript import.meta.vitest name='second example'", + " * const second = 2", + " * ~~~", + " */", + "const second = 2" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source), [ + { source: "const first = 1", line: 3, name: "first" }, + { source: "const second = 2", line: 9, name: "second example" } + ]) + }) + + it("ignores fences outside JSDoc comments", () => { + const source = [ + "```ts import.meta.vitest", + "const outside = true", + "```", + "/**", + " * ```ts import.meta.vitest", + " * const inside = true", + " * ```", + " */" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source), [ + { source: "const inside = true", line: 5, name: undefined } + ]) + }) + + it("ignores unmarked and non-TypeScript fences", () => { + const source = [ + "/**", + " * ```ts", + " * const unmarked = true", + " * ```", + " * ```js import.meta.vitest", + " * const javascript = true", + " * ```", + " */" + ].join("\n") + + assert.isEmpty(Source.extract(source)) + }) + + it("preserves assertion comments in snippet source", () => { + const source = [ + "/**", + " * ```ts import.meta.vitest", + " * const value = 1 // => 1", + " * ```", + " */" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source), [{ + source: "const value = 1 // => 1", + line: 2, + name: undefined + }]) + }) + + it("extracts inline output and ignores explanatory comments", () => { + const source = [ + "/**", + " * ```ts import.meta.vitest", + " * // Explain the output to the reader.", + " * const value = 0 // ordinary inline comment", + " * console.log(1) // > 1", + " * ```", + " */" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source), [{ + source: [ + "// Explain the output to the reader.", + "const value = 0 // ordinary inline comment", + "console.log(1) // > 1" + ].join("\n"), + line: 2, + name: undefined + }]) + }) + }) + + describe("Markdown", () => { + it("extracts only marked TypeScript fences", () => { + const source = [ + "# Examples", + "", + "```ts", + "const ignored = true", + "```", + "", + "~~~typescript import.meta.vitest name=example", + "const value = 1", + "~~~" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source, "markdown"), [ + { source: "const value = 1", line: 7, name: "example" } + ]) + }) + }) + + describe("MDX", () => { + it("extracts marked TypeScript fences from mdx content", () => { + const source = [ + "import { SomeComponent } from './component'", + "", + "## Example", + "", + "", + "", + "```ts import.meta.vitest", + "const result = 42", + "```" + ].join("\n") + + assert.deepStrictEqual(Source.extract(source, "markdown"), [ + { source: "const result = 42", line: 7, name: undefined } + ]) + }) + }) +}) diff --git a/.context/effect/packages/tools/doctest/test/Transform.test.ts b/.context/effect/packages/tools/doctest/test/Transform.test.ts new file mode 100644 index 000000000..563014090 --- /dev/null +++ b/.context/effect/packages/tools/doctest/test/Transform.test.ts @@ -0,0 +1,65 @@ +import { transform } from "@effect/doctest/Transform" +import { assert, describe, it } from "@effect/vitest" + +const imported = `import { assertEquals as __effect_doctest_assert_0 } from "@effect/doctest/Runtime"` + +describe("Transform", () => { + it("transforms expression assertions", () => { + assert.strictEqual( + transform("Option.some(1) // => Option.some(1)", "example.ts", 10), + `__effect_doctest_assert_0(Option.some(1), Option.some(1))\n\n${imported}` + ) + }) + + it("transforms const declaration assertions without evaluating the initializer twice", () => { + assert.strictEqual( + transform("const result = makeValue() // => Option.some(1)\nuse(result)", "example.ts", 10), + `const result = makeValue()\n__effect_doctest_assert_0(result, Option.some(1))\nuse(result)\n\n${imported}` + ) + }) + + it("preserves indentation in nested blocks", () => { + assert.strictEqual( + transform("if (enabled) {\n const result = makeValue() // => 1\n}", "example.ts", 10), + `if (enabled) {\n const result = makeValue()\n __effect_doctest_assert_0(result, 1)\n}\n\n${imported}` + ) + }) + + it("chooses an assertion binding that is not used by the snippet", () => { + const source = "const __effect_doctest_assert_0 = 1\nvalue // => 1" + assert.match(transform(source, "example.ts", 10), /assertEquals as __effect_doctest_assert_1/) + }) + + it("ignores ordinary and legacy output comments", () => { + const source = "console.log(value) // > value\nconst text = \"// => not an assertion\"" + assert.strictEqual(transform(source, "example.ts", 10), source) + }) + + it("rejects malformed expected expressions", () => { + assert.throws( + () => transform("value // => Option.some(", "example.ts", 10), + /example\.ts:11:7 invalid doctest expected expression/ + ) + }) + + it("rejects standalone markers", () => { + assert.throws( + () => transform("value\n// => 1", "example.ts", 10), + /doctest assertion must trail a complete statement on the same line/ + ) + }) + + it("rejects unsupported declarations", () => { + assert.throws( + () => transform("let value = 1 // => 1", "example.ts", 10), + /doctest assertions can only target expression statements or a single initialized const identifier/ + ) + }) + + it("rejects assertions in unbraced control flow", () => { + assert.throws( + () => transform("if (enabled) value // => 1", "example.ts", 10), + /doctest assertions in control flow require an explicit block/ + ) + }) +}) diff --git a/.context/effect/packages/tools/doctest/tsconfig.json b/.context/effect/packages/tools/doctest/tsconfig.json new file mode 100644 index 000000000..b512ffd91 --- /dev/null +++ b/.context/effect/packages/tools/doctest/tsconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "../../../tsconfig.base.json", + "include": ["src"], + "compilerOptions": { + "types": ["node"] + } +} diff --git a/.context/effect/packages/tools/jsdocs/docgen.json b/.context/effect/packages/tools/jsdocs/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/jsdocs/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/jsdocs/package.json b/.context/effect/packages/tools/jsdocs/package.json index 6940a8f92..556a84838 100644 --- a/.context/effect/packages/tools/jsdocs/package.json +++ b/.context/effect/packages/tools/jsdocs/package.json @@ -43,9 +43,7 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "dependencies": { "effect": "workspace:^", @@ -56,7 +54,7 @@ }, "devDependencies": { "@effect/vitest": "workspace:^", - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "typescript": "^6.0.3", "vitest": "^4.1.10" } diff --git a/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts b/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts index ce2686b14..a5b9d0d0a 100644 --- a/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts +++ b/.context/effect/packages/tools/jsdocs/src/Jsdocs.ts @@ -275,8 +275,20 @@ export interface ParsedJSDocImports { readonly flatNames: ReadonlyArray } +/** + * Kinds of public API records represented in a JSDoc model. + * + * @category models + * @since 4.0.0 + */ export type JSDocApiKind = "root-declaration" | "namespace" | "namespace-declaration" | "member" +/** + * Recommended import declaration and usage for an importable API. + * + * @category models + * @since 4.0.0 + */ export type JSDocApiImportGuidance = | { readonly style: "namespace-barrel" @@ -294,6 +306,12 @@ export type JSDocApiImportGuidance = readonly usage: string } +/** + * Resolution result for a link in an API's `@see` tags. + * + * @category models + * @since 4.0.0 + */ export type JSDocApiSeeLinkResolution = | { readonly _tag: "Resolved" @@ -306,15 +324,33 @@ export type JSDocApiSeeLinkResolution = readonly candidates: ReadonlyArray } +/** + * Parsed `@see` link paired with its public API resolution. + * + * @category models + * @since 4.0.0 + */ export interface JSDocApiSeeLink extends ParsedInlineLink { readonly resolution: JSDocApiSeeLinkResolution } +/** + * Parsed `@see` tag and its resolved links. + * + * @category models + * @since 4.0.0 + */ export interface JSDocApiSeeTag { readonly text: string readonly links: ReadonlyArray } +/** + * Standard tags attached to a public API record. + * + * @category models + * @since 4.0.0 + */ export interface JSDocApiTags { readonly category: string | null readonly since: string | null @@ -322,6 +358,12 @@ export interface JSDocApiTags { readonly default: string | null } +/** + * One public API record in an extracted JSDoc model. + * + * @category models + * @since 4.0.0 + */ export interface JSDocApi { readonly id: string readonly kind: JSDocApiKind @@ -791,12 +833,24 @@ function resolveJSDocImports( } } +/** + * Reads source text from an Oxlint-compatible rule context. + * + * @category getters + * @since 4.0.0 + */ export function getSourceText(context: { readonly sourceCode: { readonly text?: string; getText(node?: unknown): string } }): string { return context.sourceCode.text ?? context.sourceCode.getText() } +/** + * Resolves the working directory from an Oxlint-compatible rule context. + * + * @category getters + * @since 4.0.0 + */ export function getCwd(context: { readonly cwd?: string; getCwd?: () => string }): string { return context.cwd ?? context.getCwd?.() ?? process.cwd() } @@ -828,6 +882,12 @@ function skipDirectiveComments(source: string, end: number): number { return end } +/** + * Finds the JSDoc block immediately preceding an AST node. + * + * @category parsing + * @since 4.0.0 + */ export function findLeadingJSDoc( source: string, node: AstNode, @@ -858,7 +918,9 @@ export function findLeadingJSDoc( * * **Example** (Parsing a block) * - * ```ts + * ```ts import.meta.vitest + * import { parseJSDoc } from "@effect/jsdocs" + * * const rawBlock = [ * "/" + "**", * " * A value.", @@ -868,6 +930,8 @@ export function findLeadingJSDoc( * " *" + "/" * ].join("\n") * const result = parseJSDoc(rawBlock) + * + * result._tag // => "Success" * ``` * * @category parsing @@ -1183,7 +1247,7 @@ function parseSection(lines: Array, headingIndex: number): { while (index < lines.length) { const trimmed = lines[index].trim() if (trimmed.startsWith("```")) { - if (trimmed === "```ts") { + if (isTypeScriptFence(trimmed)) { diagnostics.push(diagnostic("loose-ts-fence", "TypeScript examples must use **Example** (Title) sections")) } inFence = !inFence @@ -1212,6 +1276,8 @@ function parseSection(lines: Array, headingIndex: number): { return { body: joinBody(bodyLines), nextIndex: index, diagnostics } } +const isTypeScriptFence = (line: string): boolean => /^```ts(?:\s.*)?$/.test(line) + function parseExample(lines: Array, headingIndex: number): { readonly example?: ParsedExample readonly nextIndex: number @@ -1232,7 +1298,7 @@ function parseExample(lines: Array, headingIndex: number): { let fenceIndex = -1 while (index < lines.length) { const trimmed = lines[index].trim() - if (trimmed === "```ts") { + if (isTypeScriptFence(trimmed)) { fenceIndex = index break } @@ -1265,7 +1331,7 @@ function parseExample(lines: Array, headingIndex: number): { index = fenceIndex + 1 const codeStart = index while (index < lines.length && lines[index].trim() !== "```") { - if (lines[index].trim() === "```ts") { + if (isTypeScriptFence(lines[index].trim())) { diagnostics.push(diagnostic("malformed-example", "Examples must contain exactly one TypeScript code fence")) } index++ @@ -1521,6 +1587,12 @@ function collectTsConfigFiles(tsconfigPath: string, seen: Set, fileNames return result } +/** + * Loads and caches the TypeScript program for a project configuration. + * + * @category constructors + * @since 4.0.0 + */ export function getProgram(tsconfigPath: string): ProgramCacheEntry { const cached = programCache.get(tsconfigPath) if (cached !== undefined) return cached @@ -1733,10 +1805,22 @@ function attachSeeLinkSymbols } +/** + * File selection and output configuration for JSDoc extraction. + * + * @category configuration + * @since 4.0.0 + */ export interface JSDocConfig { readonly tsconfig: string readonly include: ReadonlyArray @@ -1760,6 +1856,12 @@ export interface JSDocConfig { readonly output: string } +/** + * JSDoc extraction configuration with an optional working directory. + * + * @category configuration + * @since 4.0.0 + */ export interface ExtractJSDocsOptions extends JSDocConfig { readonly cwd?: string } @@ -1771,6 +1873,12 @@ function addInputFile(files: Set, filename: string) { } } +/** + * Computes the cache key for the configured JSDoc extraction inputs. + * + * @category hashing + * @since 4.0.0 + */ export function computeJSDocInputHash(options: ExtractJSDocsOptions): string { const cwd = path.resolve(options.cwd ?? process.cwd()) const hash = crypto.createHash("sha256") @@ -3250,12 +3358,24 @@ function parseSourceFileDocs( } } +/** + * Loads a JSDoc extraction configuration from JSON. + * + * @category configuration + * @since 4.0.0 + */ export function loadJSDocConfig(cwd = process.cwd(), configPath = "jsdocs.config.json"): JSDocConfig { const absolute = path.resolve(cwd, configPath) const parsed = JSON.parse(fs.readFileSync(absolute, "utf8")) as JSDocConfig return parsed } +/** + * Extracts a complete JSDoc model synchronously. + * + * @category extraction + * @since 4.0.0 + */ export function extractJSDocsSync(options: ExtractJSDocsOptions): JSDocModel { const cwd = path.resolve(options.cwd ?? process.cwd()) const tsconfigPath = path.resolve(cwd, options.tsconfig) @@ -3337,15 +3457,33 @@ export function extractJSDocsSync(options: ExtractJSDocsOptions): JSDocModel { } } +/** + * Extracts a complete JSDoc model in `Effect`. + * + * @category extraction + * @since 4.0.0 + */ export const extractJSDocs = (options: ExtractJSDocsOptions): Effect.Effect => Effect.sync(() => extractJSDocsSync(options)) +/** + * Writes a JSDoc model as formatted JSON. + * + * @category persistence + * @since 4.0.0 + */ export function writeJSDocModel(cwd: string, output: string, model: JSDocModel) { const filename = path.resolve(cwd, output) fs.mkdirSync(path.dirname(filename), { recursive: true }) fs.writeFileSync(filename, `${JSON.stringify(model, null, 2)}\n`) } +/** + * Reads and validates the outer structure of a persisted JSDoc model. + * + * @category persistence + * @since 4.0.0 + */ export function readJSDocModel(filename: string): Result { if (!fs.existsSync(filename)) return { _tag: "Failure", error: "missing" } try { @@ -3359,6 +3497,12 @@ export function readJSDocModel(filename: string): Result { } } +/** + * Computes the content hash stored for a source file in a JSDoc model. + * + * @category hashing + * @since 4.0.0 + */ export function sourceHash(source: string): string { return hashSource(source) } diff --git a/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts b/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts index dc4b0008f..a9f86a7a1 100644 --- a/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts +++ b/.context/effect/packages/tools/jsdocs/test/jsdocs.test.ts @@ -33,6 +33,22 @@ describe("jsdocs", () => { } }) + it("accepts doctest metadata on TypeScript fences", () => { + const result = parseJSDoc(`/** + * Creates a value. + * + * **Example** (Creating a value) + * + * \`\`\`ts import.meta.vitest name="creates a value" + * const value = 1 + * \`\`\` + * + * @category constructors + * @since 1.0.0 + */`) + assert.strictEqual(result._tag, "Success") + }) + it("accepts practical When to use forms", () => { const result = parseJSDoc(`/** * Creates a value. diff --git a/.context/effect/packages/tools/jsdocs/tsconfig.json b/.context/effect/packages/tools/jsdocs/tsconfig.json index 997ff419c..b512ffd91 100644 --- a/.context/effect/packages/tools/jsdocs/tsconfig.json +++ b/.context/effect/packages/tools/jsdocs/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "compilerOptions": { diff --git a/.context/effect/packages/tools/jsdocs/vitest.config.ts b/.context/effect/packages/tools/jsdocs/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/tools/jsdocs/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/openapi-generator/CHANGELOG.md b/.context/effect/packages/tools/openapi-generator/CHANGELOG.md index ef1609d84..fa9bc4b87 100644 --- a/.context/effect/packages/tools/openapi-generator/CHANGELOG.md +++ b/.context/effect/packages/tools/openapi-generator/CHANGELOG.md @@ -1,5 +1,79 @@ # @effect/openapi-generator +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + - @effect/platform-node@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + - @effect/platform-node@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + - @effect/platform-node@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + - @effect/platform-node@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`545d876`](https://github.com/Effect-TS/effect/commit/545d8767648cbbbf1820361662c0c1e10768db6a), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`721b9f0`](https://github.com/Effect-TS/effect/commit/721b9f0d320e50f3e2324c2cd1f43c6643af3ca0), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + - @effect/platform-node@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Patch Changes + +- [#6781](https://github.com/Effect-TS/effect/pull/6781) [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9) Thanks @gcanti! - Deduplicate equivalent fallback definitions when compiling JSON Schema, and reconstruct only definitions reachable from multi-document roots. + + Remove `SchemaMultiDocument` and `fromSchemaMultiDocument`; multi-document import and revival now return the ordered root schemas directly. + + Stop the OpenAPI generator from emitting component schemas that are not reachable from a generated root. + +- [#6777](https://github.com/Effect-TS/effect/pull/6777) [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7) Thanks @tim-smart! - Bound pending SSE decoder state with a configurable maximum event size. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- [#6770](https://github.com/Effect-TS/effect/pull/6770) [`f0c8151`](https://github.com/Effect-TS/effect/commit/f0c8151688c9ae50a6325757e3c11fcabb43fab5) Thanks @tim-smart! - Decode Effect SSE event schemas as complete events, including reserved failure events, in generated HTTP clients. + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + - @effect/platform-node@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6608](https://github.com/Effect-TS/effect/pull/6608) [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246) Thanks @gcanti! - Add `Schema.Natural` for non-negative safe integers and use canonical `Schema.Int`, `Schema.Finite`, and `Schema.Natural` schemas for numeric domain values across Effect, AI protocols, and OpenAPI patches. + + Update the date, date-time, file, time-zone, cluster, event-log, persistence, socket, SQL, and DevTools schemas to reject invalid non-finite or non-integer values where appropriate. Correct the decoded schema of `Schema.NumberFromString`, and allow `Schema.DurationFromMillis` and `Schema.DurationFromNanos` to represent negative durations. + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + - @effect/platform-node@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/tools/openapi-generator/README.md b/.context/effect/packages/tools/openapi-generator/README.md new file mode 100644 index 000000000..3050fb749 --- /dev/null +++ b/.context/effect/packages/tools/openapi-generator/README.md @@ -0,0 +1,14 @@ +# @effect/openapi-generator + +Generates Effect `Schema` types, HTTP clients, and `HttpApi` modules from OpenAPI specifications. + +## Installation + +```sh +npm install effect@beta @effect/openapi-generator@beta +``` + +## Documentation + +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/openapi-generator) diff --git a/.context/effect/packages/tools/openapi-generator/docgen.json b/.context/effect/packages/tools/openapi-generator/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/openapi-generator/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/openapi-generator/package.json b/.context/effect/packages/tools/openapi-generator/package.json index 95f8951d2..6b28b0eb0 100644 --- a/.context/effect/packages/tools/openapi-generator/package.json +++ b/.context/effect/packages/tools/openapi-generator/package.json @@ -1,7 +1,7 @@ { "name": "@effect/openapi-generator", "type": "module", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "license": "MIT", "description": "Generate Effect Schema types, HTTP clients, and HttpApi modules from OpenAPI specifications", "homepage": "https://effect.website", @@ -38,7 +38,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -56,9 +59,7 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "peerDependencies": { "@effect/platform-node": "workspace:^", diff --git a/.context/effect/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts b/.context/effect/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts index 2dcb40908..df325baf2 100644 --- a/.context/effect/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts +++ b/.context/effect/packages/tools/openapi-generator/src/JsonSchemaGenerator.ts @@ -24,16 +24,17 @@ import * as Rec from "effect/Record" import * as SchemaRepresentation from "effect/SchemaRepresentation" type Source = "openapi-3.0" | "openapi-3.1" - interface GenerateOptions { readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined } +interface MultipartSchemaRefs { + readonly singleFile: string + readonly files: string +} + interface GenerateHttpApiOptions extends GenerateOptions { - readonly multipartSchemaRefs?: { - readonly singleFile: string - readonly files: string - } | undefined + readonly multipartSchemaRefs?: MultipartSchemaRefs | undefined } /** @@ -49,10 +50,14 @@ interface GenerateHttpApiOptions extends GenerateOptions { * @since 4.0.0 */ export function make() { - const store: Record = {} + return makeWithRepresentation() +} + +function makeWithRepresentation() { + const store = Object.create(null) as Record function addSchema(name: string, schema: JsonSchema.JsonSchema): string { - if (name in store) { + if (Object.hasOwn(store, name)) { throw new Error(`Schema ${name} already exists`) } store[name] = schema @@ -114,7 +119,8 @@ export function make() { renderSchemaTypeAndRuntime(generated.nameMap[i], code, typeOnly) ) - return render("recursive declarations", recursiveDeclarations) + + return renderImportArtifacts(generated.codeDocument, !typeOnly) + + render("recursive declarations", recursiveDeclarations) + render("non-recursive definitions", nonRecursives) + render("recursive definitions", recursives) + render("schemas", codes) @@ -165,7 +171,8 @@ export function make() { renderSchemaTypeAndRuntime(generated.nameMap[i], code, false, options?.multipartSchemaRefs) ) - return render("recursive declarations", recursiveDeclarations) + + return renderImportArtifacts(generated.codeDocument, true) + + render("recursive declarations", recursiveDeclarations) + render("non-recursive definitions", nonRecursives) + render("recursive definitions", recursives) + render("schemas", codes) @@ -174,7 +181,7 @@ export function make() { function makeCodeDocument( source: Source, components: JsonSchema.Definitions, - options?: GenerateOptions + options?: GenerateHttpApiOptions ): { readonly nameMap: Array readonly codeDocument: SchemaRepresentation.CodeDocument @@ -182,7 +189,7 @@ export function make() { const nameMap: Array = [] const schemas: Array = [] - const definitions: JsonSchema.Definitions = Rec.map( + let definitions: JsonSchema.Definitions = Rec.map( components, (js) => fromSchemaOpenApi(source, js).schema ) @@ -195,24 +202,33 @@ export function make() { if (!Arr.isArrayNonEmpty(schemas)) { return } + if (options?.multipartSchemaRefs !== undefined) { + definitions = omitSupersededMultipartDefinitions(definitions, schemas, options.multipartSchemaRefs) + } - const multiDocument: SchemaRepresentation.MultiDocument = SchemaRepresentation.fromJsonSchemaMultiDocument({ - dialect: "draft-2020-12", + const document = { + dialect: "draft-2020-12" as const, schemas, definitions - }, { - onEnter(js) { + } + const importerOptions: SchemaRepresentation.FromJsonSchemaOptions = { + patterns: "apply", + onEnter(js: JsonSchema.JsonSchema) { const out = { ...js } if (out.type === "object" && out.additionalProperties === undefined) { out.additionalProperties = false } - return options?.onEnter?.(out) ?? out + return options?.onEnter === undefined ? out : options.onEnter(out) } - }) + } + const rootSchemas = SchemaRepresentation.fromJsonSchemaMultiDocument(document, importerOptions) + const codeDocument = SchemaRepresentation.toCodeDocument( + SchemaRepresentation.toRepresentations(Arr.map(rootSchemas, (schema) => schema.ast)) + ) return { nameMap, - codeDocument: SchemaRepresentation.toCodeDocument(multiDocument) + codeDocument } } @@ -232,10 +248,7 @@ function renderSchemaTypeAndRuntime( $ref: string, code: SchemaRepresentation.Code, typeOnly: boolean, - multipartSchemaRefs?: { - readonly singleFile: string - readonly files: string - } + multipartSchemaRefs?: MultipartSchemaRefs ) { if (!typeOnly && multipartSchemaRefs !== undefined) { if ($ref === multipartSchemaRefs.singleFile) { @@ -275,6 +288,72 @@ function render(title: string, as: ReadonlyArray) { return "// " + title + "\n" + as.join("\n") + "\n" } +function renderImportArtifacts(codeDocument: SchemaRepresentation.CodeDocument, enabled: boolean): string { + if (!enabled) return "" + const imports = codeDocument.artifacts.flatMap((artifact) => + artifact._tag === "Import" ? [artifact.importDeclaration] : [] + ) + return imports.length === 0 ? "" : imports.join("\n") + "\n" +} + +function omitSupersededMultipartDefinitions( + definitions: JsonSchema.Definitions, + schemas: ReadonlyArray, + multipartSchemaRefs: MultipartSchemaRefs +): JsonSchema.Definitions { + const rootReferences = collectReferenceKeys(schemas) + const multipartReferences = new Set([multipartSchemaRefs.singleFile, multipartSchemaRefs.files]) + const output: JsonSchema.Definitions = {} + + for (const [key, schema] of Object.entries(definitions)) { + const superseded = !multipartReferences.has(key) && !rootReferences.has(key) && + referencesAny(schema, multipartReferences) + if (!superseded) { + Object.defineProperty(output, key, { + value: schema, + enumerable: true, + configurable: true, + writable: true + }) + } + } + return output +} + +function collectReferenceKeys(input: unknown): Set { + const references = new Set() + visitReferences(input, ($ref) => { + const token = $ref.split("/").at(-1) + if (token !== undefined && token.length > 0) { + references.add(token.replaceAll("~1", "/").replaceAll("~0", "~")) + } + }) + return references +} + +function referencesAny(input: unknown, keys: ReadonlySet): boolean { + const references = collectReferenceKeys(input) + for (const key of references) { + if (keys.has(key)) return true + } + return false +} + +function visitReferences(input: unknown, onReference: ($ref: string) => void): void { + if (Array.isArray(input)) { + for (const value of input) visitReferences(value, onReference) + return + } + if (typeof input !== "object" || input === null) return + for (const [key, value] of Object.entries(input)) { + if (key === "$ref" && typeof value === "string") { + onReference(value) + } else { + visitReferences(value, onReference) + } + } +} + const tokenPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g function collectForwardReferencedRecursives( diff --git a/.context/effect/packages/tools/openapi-generator/src/OpenApiGenerator.ts b/.context/effect/packages/tools/openapi-generator/src/OpenApiGenerator.ts index d447a0452..a82813ece 100644 --- a/.context/effect/packages/tools/openapi-generator/src/OpenApiGenerator.ts +++ b/.context/effect/packages/tools/openapi-generator/src/OpenApiGenerator.ts @@ -15,6 +15,7 @@ import * as Effect from "effect/Effect" import type * as JsonSchema from "effect/JsonSchema" import * as Layer from "effect/Layer" import * as Predicate from "effect/Predicate" +import * as Rec from "effect/Record" import * as String from "effect/String" import type { OpenAPISecurityScheme, OpenAPISpec, OpenAPISpecMethodName } from "effect/unstable/httpapi/OpenApi" import SwaggerToOpenApi from "swagger2openapi" @@ -156,7 +157,7 @@ export const make = Effect.gen(function*() { const generation = options.format === "httpapi" ? generator.generateHttpApi( source, - withHttpApiMultipartSchemas(spec.components?.schemas ?? {}, multipartSchemaRefs), + withHttpApiMultipartSchemas(spec.components?.schemas ?? {}, multipartSchemaRefs, resolveRef), { onEnter: options.onEnter, multipartSchemaRefs @@ -604,11 +605,23 @@ const parseOpenApi = ( } } - const sseResponseSchema = content?.["text/event-stream"]?.schema + const sseMediaType = content?.["text/event-stream"] + const sseResponseSchema = sseMediaType?.schema if (!isHttpApi && Predicate.isUndefined(op.sseSchema) && Predicate.isNotUndefined(sseResponseSchema)) { const statusMajorNumber = Number(parsedStatus[0]) if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) { - op.sseSchema = addSchema(`${schemaId}${status}Sse`, sseResponseSchema, op) + const effectStream = sseMediaType["x-effect-stream"] + op.sseSchemaMode = getEffectStreamEncoding(sseMediaType) === "sse" ? "event" : "data" + op.sseSchema = addSchema( + `${schemaId}${status}Sse`, + op.sseSchemaMode === "event" + ? makeSseEventSchema( + resolveReference(sseResponseSchema, resolveRef), + effectStream?.encoding === "sse" ? effectStream.failureEvent : undefined + ) + : sseResponseSchema, + op + ) } } @@ -744,14 +757,14 @@ const buildParameterSchema = < for (const [name, propertySchema] of Object.entries(paramSchema.properties)) { const adjustedName = `${parameter.name}[${name}]` - schema.properties[adjustedName] = propertySchema as JsonSchema.JsonSchema + Rec.assignProperty(schema.properties, adjustedName, propertySchema as JsonSchema.JsonSchema) if (required.includes(name)) { schema.required.push(adjustedName) } added.push(adjustedName) } } else { - schema.properties[parameter.name] = parameter.schema as JsonSchema.JsonSchema + Rec.assignProperty(schema.properties, parameter.name, parameter.schema as JsonSchema.JsonSchema) if (parameter.required) { schema.required.push(parameter.name) } @@ -807,13 +820,14 @@ const toDefinitionRef = (name: string): string => `#/$defs/${name.replaceAll("~" const withHttpApiMultipartSchemas = ( definitions: JsonSchema.Definitions, - multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined + multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined, + resolveRef: (ref: string) => unknown ): JsonSchema.Definitions => { if (multipartSchemaRefs === undefined) { return definitions } return { - ...definitions, + ...Rec.map(definitions, (schema) => transformMultipartSchema(schema, multipartSchemaRefs, resolveRef)), [multipartSchemaRefs.singleFile]: { type: "string", format: "binary" @@ -850,18 +864,21 @@ const transformMultipartSchema = ( } if (typeof value.$ref === "string" && value.$ref.startsWith("#/components/schemas/")) { - const cached = cache.get(value.$ref) + const { $ref, ...siblings } = value + const withSiblings = (schema: unknown): unknown => + Object.keys(siblings).length === 0 ? schema : { allOf: [schema, visit(siblings)] } + const cached = cache.get($ref) if (cached !== undefined) { - return cached + return withSiblings(cached) } - if (stack.has(value.$ref)) { + if (stack.has($ref)) { return value } - stack.add(value.$ref) - const transformed = visit(resolveSchemaReference(value.$ref, resolveRef)) - stack.delete(value.$ref) - cache.set(value.$ref, transformed) - return transformed + stack.add($ref) + const transformed = visit(resolveRef($ref)) + stack.delete($ref) + cache.set($ref, transformed) + return withSiblings(transformed) } if (isMultipartBinaryFile(value)) { @@ -870,7 +887,7 @@ const transformMultipartSchema = ( const out: Record = {} for (const [key, current] of Object.entries(value)) { - out[key] = visit(current) + Rec.assignProperty(out, key, visit(current)) } if (isMultipartBinaryFiles(out, singleFileRef)) { @@ -883,19 +900,6 @@ const transformMultipartSchema = ( return visit(schema) as JsonSchema.JsonSchema } -const resolveSchemaReference = (ref: string, resolveRef: (ref: string) => unknown): unknown => { - let current: unknown = { $ref: ref } - const seen = new Set() - while (Predicate.isObject(current) && typeof current.$ref === "string") { - if (seen.has(current.$ref)) { - return current - } - seen.add(current.$ref) - current = resolveRef(current.$ref) - } - return current -} - const isMultipartBinaryFile = (value: unknown): value is JsonSchema.JsonSchema => Predicate.isObject(value) && value.type === "string" && @@ -979,6 +983,59 @@ const getEffectStreamErrorSchema = (mediaType: object): JsonSchema.JsonSchema | return stream.errorSchema as JsonSchema.JsonSchema } +const makeSseEventSchema = ( + input: JsonSchema.JsonSchema, + failureEvent: string | undefined +): JsonSchema.JsonSchema => { + const eventSchema = normalizeSseEventSchema(input) + if (failureEvent === undefined) { + return eventSchema + } + return { + anyOf: [ + eventSchema, + { + type: "object", + properties: { + id: { type: "string" }, + event: { const: failureEvent }, + data: { type: "string" } + }, + required: ["event", "data"], + additionalProperties: false + } + ] + } +} + +const normalizeSseEventSchema = (input: JsonSchema.JsonSchema): JsonSchema.JsonSchema => { + const schema = { ...input } as Record + for (const key of ["allOf", "anyOf", "oneOf"] as const) { + if (Array.isArray(schema[key])) { + schema[key] = schema[key].map(normalizeSseEventSchema) + } + } + if ( + schema.type !== "object" || + !Predicate.isObject(schema.properties) || + !Object.hasOwn(schema.properties, "id") || + !Object.hasOwn(schema.properties, "event") || + !Object.hasOwn(schema.properties, "data") + ) { + return schema as JsonSchema.JsonSchema + } + + // OpenAPI represents `undefined` as required nullable; restore the SSE parser's optional wire field. + schema.properties = { + ...schema.properties, + id: { type: "string" } + } + if (Array.isArray(schema.required)) { + schema.required = schema.required.filter((name: unknown) => name !== "id") + } + return schema as JsonSchema.JsonSchema +} + const resolveReference = (input: unknown, resolveRef: (ref: string) => unknown): any => { let current = input while (Predicate.isObject(current) && typeof current.$ref === "string") { diff --git a/.context/effect/packages/tools/openapi-generator/src/OpenApiPatch.ts b/.context/effect/packages/tools/openapi-generator/src/OpenApiPatch.ts index 2ea7a3fb9..ecc63c7d0 100644 --- a/.context/effect/packages/tools/openapi-generator/src/OpenApiPatch.ts +++ b/.context/effect/packages/tools/openapi-generator/src/OpenApiPatch.ts @@ -35,22 +35,21 @@ import * as Yaml from "yaml" * * **Example** (Creating a parse error) * - * ```ts - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * ```ts import.meta.vitest + * import { JsonPatchParseError } from "@effect/openapi-generator/OpenApiPatch" * - * const error = new OpenApiPatch.JsonPatchParseError({ + * const error = new JsonPatchParseError({ * source: "./patches/fix.json", * reason: "Unexpected token at position 42" * }) * - * console.log(error.message) - * // "Failed to parse patch from ./patches/fix.json: Unexpected token at position 42" + * error.message // => "Failed to parse patch from ./patches/fix.json: Unexpected token at position 42" * ``` * * @category errors * @since 4.0.0 */ -export class JsonPatchParseError extends Schema.ErrorClass("JsonPatchParseError")({ +export class JsonPatchParseError extends Schema.Error("JsonPatchParseError")({ _tag: Schema.tag("JsonPatchParseError"), source: Schema.String, reason: Schema.String @@ -73,22 +72,21 @@ export class JsonPatchParseError extends Schema.ErrorClass( * * **Example** (Creating a validation error) * - * ```ts - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * ```ts import.meta.vitest + * import { JsonPatchValidationError } from "@effect/openapi-generator/OpenApiPatch" * - * const error = new OpenApiPatch.JsonPatchValidationError({ + * const error = new JsonPatchValidationError({ * source: "inline", * reason: "Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'" * }) * - * console.log(error.message) - * // "Invalid JSON Patch from inline: Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'" + * error.message // => "Invalid JSON Patch from inline: Expected 'add' | 'remove' | 'replace' at [0].op, got 'copy'" * ``` * * @category errors * @since 4.0.0 */ -export class JsonPatchValidationError extends Schema.ErrorClass("JsonPatchValidationError")({ +export class JsonPatchValidationError extends Schema.Error("JsonPatchValidationError")({ _tag: Schema.tag("JsonPatchValidationError"), source: Schema.String, reason: Schema.String @@ -110,10 +108,10 @@ export class JsonPatchValidationError extends Schema.ErrorClass 'Failed to apply patch from ./patches/fix.json: operation 2 (remove at /paths/~1users): Property "users" does not exist' * ``` * * @category errors * @since 4.0.0 */ -export class JsonPatchApplicationError - extends Schema.ErrorClass("JsonPatchApplicationError")({ - _tag: Schema.tag("JsonPatchApplicationError"), - source: Schema.String, - operationIndex: Schema.Number, - operation: Schema.String, - path: Schema.String, - reason: Schema.String - }) -{ +export class JsonPatchApplicationError extends Schema.Error("JsonPatchApplicationError")({ + _tag: Schema.tag("JsonPatchApplicationError"), + source: Schema.String, + operationIndex: Schema.Natural, + operation: Schema.String, + path: Schema.String, + reason: Schema.String +}) { override get message() { return `Failed to apply patch from ${this.source}: operation ${this.operationIndex} ` + `(${this.operation} at ${this.path}): ${this.reason}` @@ -154,19 +149,19 @@ export class JsonPatchApplicationError * * **Example** (Creating an aggregate error) * - * ```ts - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * ```ts import.meta.vitest + * import { JsonPatchAggregateError, JsonPatchApplicationError } from "@effect/openapi-generator/OpenApiPatch" * - * const error = new OpenApiPatch.JsonPatchAggregateError({ + * const error = new JsonPatchAggregateError({ * errors: [ - * new OpenApiPatch.JsonPatchApplicationError({ + * new JsonPatchApplicationError({ * source: "./fix.json", * operationIndex: 0, * operation: "replace", * path: "/info/x", * reason: "Property does not exist" * }), - * new OpenApiPatch.JsonPatchApplicationError({ + * new JsonPatchApplicationError({ * source: "./fix.json", * operationIndex: 2, * operation: "remove", @@ -176,14 +171,13 @@ export class JsonPatchApplicationError * ] * }) * - * console.log(error.message) - * // "2 patch operations failed:\n 1. ..." + * error.message.split("\n")[0] // => "2 patch operations failed:" * ``` * * @category errors * @since 4.0.0 */ -export class JsonPatchAggregateError extends Schema.ErrorClass("JsonPatchAggregateError")({ +export class JsonPatchAggregateError extends Schema.Error("JsonPatchAggregateError")({ _tag: Schema.tag("JsonPatchAggregateError"), errors: Schema.Array(Schema.Unknown) }) { @@ -283,15 +277,17 @@ export const JsonPatchOperation: Schema.Codec = Sc * * **Example** (Decoding a patch document) * - * ```ts + * ```ts import.meta.vitest * import { Schema } from "effect" - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * import { JsonPatchDocument } from "@effect/openapi-generator/OpenApiPatch" * - * const patch = Schema.decodeUnknownSync(OpenApiPatch.JsonPatchDocument)([ + * const patch = Schema.decodeUnknownSync(JsonPatchDocument)([ * { op: "add", path: "/foo", value: "bar" }, * { op: "remove", path: "/baz" }, * { op: "replace", path: "/qux", value: 42 } * ]) + * + * patch.map((operation) => operation.op) // => ["add", "remove", "replace"] * ``` * * @category schemas @@ -302,7 +298,7 @@ export const JsonPatchDocument = Schema.Array(JsonPatchOperation) /** * Type for a JSON Patch document. * - * @category types + * @category models * @since 4.0.0 */ export type JsonPatchDocument = typeof JsonPatchDocument.Type @@ -434,23 +430,21 @@ const parseInlinePatch = Effect.fn("parseInlinePatch")(function*(input: string) * * **Example** (Parsing patch input) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * import { parsePatchInput } from "@effect/openapi-generator/OpenApiPatch" * * // From inline JSON - * const fromInline = OpenApiPatch.parsePatchInput( + * const fromInline = parsePatchInput( * '[{"op":"replace","path":"/info/title","value":"My API"}]' * ) * - * // From file path - * const fromFile = OpenApiPatch.parsePatchInput("./patches/fix-api.json") - * * const program = Effect.gen(function*() { * const patch = yield* fromInline - * console.log(patch) - * // [{ op: "replace", path: "/info/title", value: "My API" }] + * return [patch[0].op, patch[0].path] * }) + * + * Effect.runSync(program) // => ["replace", "/info/title"] * ``` * * @category parsing @@ -481,9 +475,9 @@ export const parsePatchInput = Effect.fn("parsePatchInput")(function*(input: str * * **Example** (Applying patches) * - * ```ts + * ```ts import.meta.vitest * import { Effect } from "effect" - * import * as OpenApiPatch from "@effect/openapi-generator/OpenApiPatch" + * import { applyPatches } from "@effect/openapi-generator/OpenApiPatch" * * const document = { info: { title: "Old Title" }, paths: {} } * const patches = [ @@ -494,13 +488,14 @@ export const parsePatchInput = Effect.fn("parsePatchInput")(function*(input: str * ] * * const program = Effect.gen(function*() { - * const result = yield* OpenApiPatch.applyPatches(patches, document) - * console.log(result) - * // { info: { title: "New Title" }, paths: {} } + * const result = yield* applyPatches(patches, document) + * return (result as typeof document).info.title * }) + * + * Effect.runSync(program) // => "New Title" * ``` * - * @category application + * @category transforming * @since 4.0.0 */ export const applyPatches = Effect.fn("applyPatches")(function*( diff --git a/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts b/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts index e511a6407..e56c65725 100644 --- a/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts +++ b/.context/effect/packages/tools/openapi-generator/src/OpenApiTransformer.ts @@ -26,7 +26,7 @@ import * as Utils from "./Utils.ts" * types, and the implementation body. The generator swaps implementations to * choose between schema-backed clients and type-only clients. * - * @category code generation + * @category services * @since 4.0.0 */ export class OpenApiTransformer extends Context.Service< @@ -40,21 +40,30 @@ export class OpenApiTransformer extends Context.Service< interface ImportRequirements { readonly eventStream: boolean + readonly eventStreamData: boolean + readonly eventStreamSchema: boolean readonly octetStream: boolean } const computeImportRequirements = (operations: ReadonlyArray): ImportRequirements => { let eventStream = false + let eventStreamData = false + let eventStreamSchema = false let octetStream = false for (const op of operations) { if (op.sseSchema) { eventStream = true + if (op.sseSchemaMode === "event") { + eventStreamSchema = true + } else { + eventStreamData = true + } } if (op.binaryResponse) { octetStream = true } } - return { eventStream, octetStream } + return { eventStream, eventStreamData, eventStreamSchema, octetStream } } const requiresStreaming = (requirements: ImportRequirements): boolean => @@ -173,8 +182,11 @@ ${clientErrorSource(name)}` const jsdoc = Utils.toComment(operation.description) const methodKey = `readonly "${operation.id}Sse"` const parameters = args.join(", ") + const value = operation.sseSchemaMode === "event" + ? `typeof ${operation.sseSchema}.Type` + : `{ readonly event: string; readonly id: string | undefined; readonly data: typeof ${operation.sseSchema}.Type }` const returnType = - `Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof ${operation.sseSchema}.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof ${operation.sseSchema}.DecodingServices>` + `Stream.Stream<${value}, HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof ${operation.sseSchema}.DecodingServices>` return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}` } @@ -226,9 +238,12 @@ ${clientErrorSource(name)}` } const helpers: Array = [commonSource] - if (requirements.eventStream) { + if (requirements.eventStreamData) { helpers.push(sseRequestSource(importName)) } + if (requirements.eventStreamSchema) { + helpers.push(sseEventRequestSource) + } if (requirements.octetStream) { helpers.push(binaryRequestSource) } @@ -370,7 +385,7 @@ export const make = ( pipeline.push(`HttpClientRequest.bodyJsonUnsafe(options.payload)`) } - pipeline.push(`sseRequest(${operation.sseSchema})`) + pipeline.push(`${operation.sseSchemaMode === "event" ? "sseEventRequest" : "sseRequest"}(${operation.sseSchema})`) return ( `"${operation.id}Sse": (${params}) => ` + @@ -462,7 +477,7 @@ export const make = ( * Use when you use this layer when generated HttpClient code should perform runtime response * decoding with generated Effect Schema values. * - * @category code generation + * @category layers * @since 4.0.0 */ export const layerTransformerSchema = Layer.sync( @@ -873,7 +888,7 @@ export const make = ( * generated client relies on TypeScript types instead of runtime Schema * decoding. * - * @category code generation + * @category layers * @since 4.0.0 */ export const layerTransformerTs = Layer.sync( @@ -923,7 +938,7 @@ const sseRequestSource = (_importName: string) => request: HttpClientRequest.HttpClientRequest ): Stream.Stream< { readonly event: string; readonly id: string | undefined; readonly data: Type }, - HttpClientError.HttpClientError | SchemaError | Sse.Retry, + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, DecodingServices > => HttpClient.filterStatusOk(httpClient).execute(request).pipe( @@ -933,6 +948,21 @@ const sseRequestSource = (_importName: string) => Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) )` +const sseEventRequestSource = `const sseEventRequest = (schema: S) => + ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream< + S["Type"], + HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, + S["DecodingServices"] + > => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeSchema(schema)) + )` + const binaryRequestSource = `const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream => HttpClient.filterStatusOk(httpClient).execute(request).pipe( diff --git a/.context/effect/packages/tools/openapi-generator/src/ParsedOperation.ts b/.context/effect/packages/tools/openapi-generator/src/ParsedOperation.ts index 725133d36..b6812e59f 100644 --- a/.context/effect/packages/tools/openapi-generator/src/ParsedOperation.ts +++ b/.context/effect/packages/tools/openapi-generator/src/ParsedOperation.ts @@ -221,6 +221,7 @@ export interface ParsedOperation { readonly voidSchemas: ReadonlySet // SSE streaming response schema (text/event-stream) readonly sseSchema?: string + readonly sseSchemaMode: "data" | "event" // Binary stream response (application/octet-stream) readonly binaryResponse: boolean } @@ -273,5 +274,6 @@ export const makeDeepMutable = (options: { errorSchemas: new Map(), voidSchemas: new Set(), paramsOptional: true, + sseSchemaMode: "data", binaryResponse: false }) diff --git a/.context/effect/packages/tools/openapi-generator/src/Utils.ts b/.context/effect/packages/tools/openapi-generator/src/Utils.ts index 958f0faf8..aef310c3b 100644 --- a/.context/effect/packages/tools/openapi-generator/src/Utils.ts +++ b/.context/effect/packages/tools/openapi-generator/src/Utils.ts @@ -101,7 +101,7 @@ export const toComment = UndefinedOr.match({ * This mutates `destination` directly, which avoids allocating an intermediate * array when generator code needs to merge collections. * - * @category concatenating + * @category mutations * @since 4.0.0 */ export const spreadElementsInto = (source: Array, destination: Array): void => { diff --git a/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGenerator.test.ts b/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGenerator.test.ts index 58923aed1..1b4a6aab2 100644 --- a/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGenerator.test.ts +++ b/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGenerator.test.ts @@ -22,7 +22,7 @@ export const A = Schema.String const result = generator.generate("openapi-3.1", definitions, false) expect(result).toBe(`// non-recursive definitions export type B = string -export const B = Schema.String +export const B = Schema.String.annotate({ "identifier": "B" }) // schemas export type A = B export const A = B @@ -108,7 +108,7 @@ export const B = Schema.Struct({ "id": Schema.String })`) const result = generator.generate("openapi-3.1", definitions, false) expect(result).toBe(`// recursive definitions export type B = { readonly "name": string, readonly "children": ReadonlyArray } -export const B = Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec => B)) }) +export const B = Schema.Struct({ "name": Schema.String, "children": Schema.Array(Schema.suspend((): Schema.Codec => B)) }).annotate({ "identifier": "B" }) // schemas export type A = B export const A = B @@ -144,15 +144,15 @@ export const A = B const result = generator.generate("openapi-3.1", definitions, false) const recursiveDeclaration = - "export const ResourcesNetworkCardSRIOV = Schema.suspend((): Schema.Codec => __recursive_ResourcesNetworkCardSRIOV)" + "export const ResourcesNetworkCard = Schema.suspend((): Schema.Codec => __recursive_ResourcesNetworkCard)" expect(result).toContain(recursiveDeclaration) - expect(result).toContain("const __recursive_ResourcesNetworkCardSRIOV =") + expect(result).toContain("const __recursive_ResourcesNetworkCard =") expect(result.indexOf(recursiveDeclaration)).toBeLessThan( - result.indexOf("export const ResourcesNetworkCard =") + result.indexOf("export const ResourcesNetworkCardSRIOV =") ) - expect(result.indexOf("export const ResourcesNetworkCard =")).toBeLessThan( - result.indexOf("const __recursive_ResourcesNetworkCardSRIOV =") + expect(result.indexOf("export const ResourcesNetworkCardSRIOV =")).toBeLessThan( + result.indexOf("const __recursive_ResourcesNetworkCard =") ) }) diff --git a/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGeneratorRepresentation.test.ts b/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGeneratorRepresentation.test.ts new file mode 100644 index 000000000..3c9dc9af4 --- /dev/null +++ b/.context/effect/packages/tools/openapi-generator/test/JsonSchemaGeneratorRepresentation.test.ts @@ -0,0 +1,34 @@ +import * as JsonSchemaGenerator from "@effect/openapi-generator/JsonSchemaGenerator" +import { assert, describe, it } from "@effect/vitest" + +describe("JsonSchemaGenerator representation", () => { + it("preserves patterns from code generation inputs", () => { + const generator = JsonSchemaGenerator.make() + generator.addSchema("Root", { type: "string", pattern: "^a+$" }) + + const output = generator.generate("openapi-3.1", {}, false) + + assert.include(output, `Schema.isPattern(new RegExp("^a+$"))`) + }) + + it("emits only reachable definitions", () => { + const generator = JsonSchemaGenerator.make() + generator.addSchema("Root", { $ref: "#/components/schemas/Shared" }) + + const output = generator.generate("openapi-3.1", { + Shared: { type: "string" }, + Unused: { type: "boolean" } + }, false) + + assert.strictEqual( + output, + `// non-recursive definitions +export type Shared = string +export const Shared = Schema.String.annotate({ "identifier": "Shared" }) +// schemas +export type Root = Shared +export const Root = Shared +` + ) + }) +}) diff --git a/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts b/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts index bf93c46b4..46e437b92 100644 --- a/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts +++ b/.context/effect/packages/tools/openapi-generator/test/OpenApiGenerator.test.ts @@ -518,13 +518,74 @@ export const TestClientError = ( }, [ `import * as Sse from "effect/unstable/encoding/Sse"`, - `readonly "streamEventsSse": () => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof StreamEvents200Sse.DecodingServices>`, + `readonly "streamEventsSse": () => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry | Sse.SseError, typeof StreamEvents200Sse.DecodingServices>`, `"streamEventsSse": () => HttpClientRequest.get(\`/events\`).pipe(`, `sseRequest(StreamEvents200Sse)`, `schema: Schema.ConstraintDecoder` ] )) + it.effect("annotated sse operation decodes the full event schema", () => + assertRuntimeIncludes( + { + openapi: "3.1.0", + info: { + title: "Test API", + version: "1.0.0" + }, + paths: { + "/events": { + get: { + operationId: "streamEvents", + parameters: [], + responses: { + 200: { + description: "Events streamed successfully", + content: { + "text/event-stream": { + schema: { + type: "object", + properties: { + id: { + anyOf: [{ type: "string" }, { type: "null" }] + }, + event: { const: "message" }, + data: { type: "string" } + }, + required: ["id", "event", "data"], + additionalProperties: false + }, + "x-effect-stream": { + encoding: "sse", + errorSchema: {}, + causeSchema: {}, + failureEvent: "effect/httpapi/stream/failure" + } + } + } + } + }, + tags: ["Events"], + security: [] + } + } + }, + components: { + schemas: {}, + securitySchemes: {} + }, + security: [], + tags: [] + }, + [ + `"id": Schema.optionalKey(Schema.String), "event": Schema.Literal("message"), "data": Schema.String`, + `"id": Schema.optionalKey(Schema.String), "event": Schema.Literal("effect/httpapi/stream/failure"), "data": Schema.String`, + `readonly "streamEventsSse": () => Stream.Stream`, + `sseEventRequest(StreamEvents200Sse)`, + `Stream.pipeThroughChannel(Sse.decodeSchema(schema))` + ] + )) + it.effect("form-urlencoded request body generates bodyUrlParams", () => assertRuntimeIncludes( { @@ -1542,6 +1603,144 @@ export const __HttpApiMultipartFiles = Multipart.FilesSchema`, ] )) + it.effect("preserves multipart component reference siblings", () => + assertHttpApiIncludes( + { + openapi: "3.1.0", + info: { + title: "Test API", + version: "1.0.0" + }, + paths: { + "/upload": { + post: { + operationId: "upload", + parameters: [], + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + first: { + $ref: "#/components/schemas/UploadBody", + minProperties: 1, + description: "First upload" + }, + second: { + $ref: "#/components/schemas/UploadBody", + minProperties: 2, + description: "Second upload" + } + }, + required: ["first", "second"], + additionalProperties: false + } + } + } + } as any, + responses: { + 200: { + description: "Uploaded" + } + }, + tags: ["Payload"], + security: [] + } + } + }, + components: { + schemas: { + UploadBody: { + type: "object", + properties: { + file: { type: "string", format: "binary" } + }, + required: ["file"], + additionalProperties: false + } + }, + securitySchemes: {} + }, + security: [], + tags: [{ name: "Payload" }] + }, + [ + `"first": Schema.Struct({ "file": __HttpApiMultipartSingleFile })`, + `Schema.isMinProperties(1)`, + `"description": "First upload"`, + `"second": Schema.Struct({ "file": __HttpApiMultipartSingleFile })`, + `Schema.isMinProperties(2)`, + `"description": "Second upload"` + ] + )) + + it.effect("preserves multipart component alias siblings", () => + assertHttpApiIncludes( + { + openapi: "3.1.0", + info: { + title: "Test API", + version: "1.0.0" + }, + paths: { + "/upload": { + post: { + operationId: "upload", + parameters: [], + requestBody: { + required: true, + content: { + "multipart/form-data": { + schema: { + $ref: "#/components/schemas/UploadAlias" + } + } + } + } as any, + responses: { + 200: { + description: "Uploaded" + } + }, + tags: ["Payload"], + security: [] + } + } + }, + components: { + schemas: { + UploadAlias: { + $ref: "#/components/schemas/UploadBody", + minProperties: 1, + description: "Aliased upload" + }, + UploadBody: { + type: "object", + properties: { + file: { type: "string", format: "binary" } + }, + required: ["file"], + additionalProperties: false + } + }, + securitySchemes: {} + }, + security: [], + tags: [{ name: "Payload" }] + }, + [ + `export type UploadRequestFormData = { readonly "file": __HttpApiMultipartSingleFile }`, + `Schema.isMinProperties(1)`, + `"description": "Aliased upload"` + ], + [ + `export type UploadAlias =`, + `export type UploadBody =` + ] + )) + it.effect("maps multipart contentEncoding binary schemas (case-insensitive) to Multipart file schemas", () => assertHttpApiIncludes( { diff --git a/.context/effect/packages/tools/openapi-generator/tsconfig.json b/.context/effect/packages/tools/openapi-generator/tsconfig.json index 739eb6b55..fce3e3d43 100644 --- a/.context/effect/packages/tools/openapi-generator/tsconfig.json +++ b/.context/effect/packages/tools/openapi-generator/tsconfig.json @@ -1,9 +1,9 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/tools/openapi-generator/vitest.config.ts b/.context/effect/packages/tools/openapi-generator/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/tools/openapi-generator/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/oxc/docgen.json b/.context/effect/packages/tools/oxc/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/oxc/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/oxc/oxlintrc.json b/.context/effect/packages/tools/oxc/oxlintrc.json index 10cddc50e..a517e19c5 100644 --- a/.context/effect/packages/tools/oxc/oxlintrc.json +++ b/.context/effect/packages/tools/oxc/oxlintrc.json @@ -21,7 +21,6 @@ "effect/no-js-extension-imports": "error", "effect/no-opaque-instance-fields": "error", "effect/no-unused-internal": "error", - "effect/jsdocs": "error", // Tune native rules // Import rules "typescript/consistent-type-imports": ["error", { @@ -60,7 +59,7 @@ "unicorn/require-post-message-target-origin": "off", "unicorn/prefer-add-event-listener": "off", "unicorn/prefer-set-has": "off", - "no-dangling-underscore": "off", + "no-underscore-dangle": "off", "typescript/no-explicit-any": "off", "typescript/no-empty-interface": "off", "typescript/ban-ts-comment": "off", diff --git a/.context/effect/packages/tools/oxc/package.json b/.context/effect/packages/tools/oxc/package.json index fde4970a5..531874153 100644 --- a/.context/effect/packages/tools/oxc/package.json +++ b/.context/effect/packages/tools/oxc/package.json @@ -53,18 +53,16 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "dependencies": { - "@effect/jsdocs": "workspace:^" + "@oxlint/plugins": "^1.76.0" }, "peerDependencies": { "typescript": ">=5.0.0 <7.0.0" }, "devDependencies": { - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "typescript": "^6.0.3", "vitest": "^4.1.10" } diff --git a/.context/effect/packages/tools/oxc/src/oxlint/index.ts b/.context/effect/packages/tools/oxc/src/oxlint/index.ts index 5f8d63651..205eddce0 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/index.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/index.ts @@ -1,4 +1,3 @@ -import jsdocs from "./rules/jsdocs.ts" import noBigIntLiterals from "./rules/no-bigint-literals.ts" import noImportFromBarrelPackage from "./rules/no-import-from-barrel-package.ts" import noJsExtensionImports from "./rules/no-js-extension-imports.ts" @@ -14,7 +13,6 @@ export default { "no-import-from-barrel-package": noImportFromBarrelPackage, "no-js-extension-imports": noJsExtensionImports, "no-opaque-instance-fields": noOpaqueInstanceFields, - "no-unused-internal": noUnusedInternal, - "jsdocs": jsdocs + "no-unused-internal": noUnusedInternal } } diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/jsdocs.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/jsdocs.ts deleted file mode 100644 index 05f000bac..000000000 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/jsdocs.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as Jsdocs from "@effect/jsdocs/Jsdocs" -import * as fs from "node:fs" -import * as path from "node:path" -import type { CreateRule, ESTree, Visitor } from "oxlint" - -interface RuleOptions { - readonly model?: string -} - -interface CachedModel { - readonly result: ReturnType - readonly filesByPath: ReadonlyMap[number]> - readonly mtimeMs: number - readonly size: number -} - -const modelCache = new Map() - -function getSourceText(context: { - readonly sourceCode: { readonly text?: string; getText(node?: unknown): string } -}): string { - return context.sourceCode.text ?? context.sourceCode.getText() -} - -function getCwd(context: { readonly cwd?: string; getCwd?: () => string }): string { - return context.cwd ?? context.getCwd?.() ?? process.cwd() -} - -function normalizePathName(filename: string): string { - return filename.split(path.sep).join("/") -} - -function readCachedModel(modelPath: string): CachedModel["result"] { - if (!fs.existsSync(modelPath)) return { _tag: "Failure", error: "missing" } - const stats = fs.statSync(modelPath) - const cached = modelCache.get(modelPath) - if (cached !== undefined && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) return cached.result - const result = Jsdocs.readJSDocModel(modelPath) - modelCache.set(modelPath, { - result, - filesByPath: result._tag === "Success" ? new Map(result.value.files.map((file) => [file.file, file])) : new Map(), - mtimeMs: stats.mtimeMs, - size: stats.size - }) - return result -} - -function getCachedFile( - modelPath: string, - file: string -): ReturnType extends infer R - ? R extends { readonly _tag: "Success"; readonly value: { readonly files: ReadonlyArray } } ? A | undefined - : never - : never -{ - return modelCache.get(modelPath)?.filesByPath.get(file) as never -} - -function rangeNode(range: readonly [number, number]): { readonly range: [number, number] } { - return { range: [range[0], range[1]] } -} - -const rule: CreateRule = { - meta: { - type: "problem", - docs: { description: "Enforce Effect's public API JSDoc structure" }, - schema: [ - { - type: "object", - properties: { - model: { type: "string" } - }, - additionalProperties: false - } - ] - }, - create(context) { - const options = (context.options[0] as RuleOptions | undefined) ?? {} - const source = getSourceText(context) - const cwd = getCwd(context) - const modelPath = path.resolve(cwd, options.model ?? ".data/jsdocs.json") - const result = readCachedModel(modelPath) - if (result._tag === "Failure") { - if (result.error === "missing") return {} as Visitor - return { - Program(node: ESTree.Node) { - context.report({ node, message: result.error }) - } - } as Visitor - } - const relative = normalizePathName(path.relative(cwd, context.filename)) - const file = getCachedFile(modelPath, relative) - if (file === undefined) return {} as Visitor - return { - Program(node: ESTree.Node) { - if (file.hash !== Jsdocs.sourceHash(source)) { - context.report({ node, message: "JSDoc model is stale for this file; run `pnpm jsdocs`" }) - return - } - for (const diagnostic of file.diagnostics) { - context.report({ node: rangeNode(diagnostic.range), message: diagnostic.message }) - } - } - } as Visitor - } -} - -export default rule diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-bigint-literals.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-bigint-literals.ts index cc274b7d6..2139717de 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-bigint-literals.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-bigint-literals.ts @@ -1,4 +1,4 @@ -import type { CreateRule, Visitor } from "oxlint" +import type { CreateRule, Visitor } from "@oxlint/plugins" const rule: CreateRule = { meta: { diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-import-from-barrel-package.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-import-from-barrel-package.ts index ab337ad40..1921966e2 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-import-from-barrel-package.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-import-from-barrel-package.ts @@ -1,6 +1,6 @@ +import type { CreateRule, ESTree, Visitor } from "@oxlint/plugins" import * as fs from "node:fs" import * as path from "node:path" -import type { CreateRule, ESTree, Visitor } from "oxlint" interface RuleOptions { checkPatterns?: Array diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-js-extension-imports.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-js-extension-imports.ts index f09551308..4dcf2def0 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-js-extension-imports.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-js-extension-imports.ts @@ -1,4 +1,4 @@ -import type { CreateRule, ESTree, Fixer, Visitor } from "oxlint" +import type { CreateRule, ESTree, Fixer, Visitor } from "@oxlint/plugins" const jsExtensions = [".js", ".jsx", ".mjs", ".cjs"] const extensionMap: Record = { diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-opaque-instance-fields.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-opaque-instance-fields.ts index d4985d16f..4dbd4c1f7 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-opaque-instance-fields.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-opaque-instance-fields.ts @@ -1,4 +1,4 @@ -import type { CreateRule, ESTree, Visitor } from "oxlint" +import type { CreateRule, ESTree, Visitor } from "@oxlint/plugins" const SCHEMA_SOURCES = new Set(["effect", "effect/Schema"]) const SCHEMA_NAMESPACE_SOURCES = new Set(["effect/Schema"]) diff --git a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts index 981f673d7..3fdbd6c82 100644 --- a/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts +++ b/.context/effect/packages/tools/oxc/src/oxlint/rules/no-unused-internal.ts @@ -1,6 +1,6 @@ +import type { CreateRule, ESTree, Visitor } from "@oxlint/plugins" import * as fs from "node:fs" import * as path from "node:path" -import type { CreateRule, ESTree, Visitor } from "oxlint" import ts from "typescript" interface InternalExport { diff --git a/.context/effect/packages/tools/oxc/test/jsdocs.test.ts b/.context/effect/packages/tools/oxc/test/jsdocs.test.ts deleted file mode 100644 index 9f0cea50e..000000000 --- a/.context/effect/packages/tools/oxc/test/jsdocs.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import rule from "@effect/oxc/oxlint/rules/jsdocs" -import * as crypto from "node:crypto" -import * as fs from "node:fs" -import * as os from "node:os" -import * as path from "node:path" -import { describe, expect, it } from "vitest" -import { createTestContext } from "./utils.ts" - -function hash(source: string): string { - return crypto.createHash("sha256").update(source).digest("hex") -} - -function run(source: string, cwd: string, filename: string, model = ".data/jsdocs.json") { - const { context, errors } = createTestContext({ sourceCode: source, cwd, filename, ruleOptions: [{ model }] }) - const visitors = rule.create(context as never) - visitors.Program?.({ type: "Program", range: [0, source.length] } as never) - return errors -} - -describe("jsdocs", () => { - it("skips when the model is missing", () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-")) - const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts")) - expect(errors).toEqual([]) - }) - - it("reports invalid model files", () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-")) - fs.mkdirSync(path.join(cwd, ".data"), { recursive: true }) - fs.writeFileSync(path.join(cwd, ".data/jsdocs.json"), "{") - const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts")) - expect(errors.map((error) => error.message)).toEqual([expect.stringContaining("Invalid jsdocs model")]) - }) - - it("reports stale model files", () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-")) - fs.mkdirSync(path.join(cwd, ".data"), { recursive: true }) - fs.writeFileSync( - path.join(cwd, ".data/jsdocs.json"), - JSON.stringify({ - version: 2, - generatedBy: "@effect/jsdocs", - generatedAt: "now", - files: [{ file: "src/Foo.ts", hash: "old", diagnostics: [], declarations: [], namespaces: [] }], - apis: [] - }) - ) - const errors = run("export const a = 1\n", cwd, path.join(cwd, "src/Foo.ts")) - expect(errors.map((error) => error.message)).toEqual(["JSDoc model is stale for this file; run `pnpm jsdocs`"]) - }) - - it("reports model diagnostics", () => { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "jsdocs-rule-")) - const source = "export const a = 1\n" - fs.mkdirSync(path.join(cwd, ".data"), { recursive: true }) - fs.writeFileSync( - path.join(cwd, ".data/jsdocs.json"), - JSON.stringify({ - version: 2, - generatedBy: "@effect/jsdocs", - generatedAt: "now", - apis: [], - files: [{ - file: "src/Foo.ts", - hash: hash(source), - diagnostics: [{ code: "missing-jsdoc", message: "Public JSDoc is required", range: [0, 6] }], - declarations: [], - namespaces: [] - }] - }) - ) - const errors = run(source, cwd, path.join(cwd, "src/Foo.ts")) - expect(errors.map((error) => error.message)).toEqual(["Public JSDoc is required"]) - }) -}) diff --git a/.context/effect/packages/tools/oxc/test/utils.ts b/.context/effect/packages/tools/oxc/test/utils.ts index c26f6c112..002c8c956 100644 --- a/.context/effect/packages/tools/oxc/test/utils.ts +++ b/.context/effect/packages/tools/oxc/test/utils.ts @@ -1,4 +1,4 @@ -import type { CreateRule, Visitor } from "oxlint" +import type { CreateRule, Visitor } from "@oxlint/plugins" export interface ReportedError { node: unknown diff --git a/.context/effect/packages/tools/oxc/tsconfig.json b/.context/effect/packages/tools/oxc/tsconfig.json index 997ff419c..b512ffd91 100644 --- a/.context/effect/packages/tools/oxc/tsconfig.json +++ b/.context/effect/packages/tools/oxc/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "compilerOptions": { diff --git a/.context/effect/packages/tools/oxc/vitest.config.ts b/.context/effect/packages/tools/oxc/vitest.config.ts deleted file mode 100644 index c8a52c182..000000000 --- a/.context/effect/packages/tools/oxc/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/packages/tools/utils/docgen.json b/.context/effect/packages/tools/utils/docgen.json deleted file mode 100644 index 4dd36175a..000000000 --- a/.context/effect/packages/tools/utils/docgen.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@effect/docgen/schema.json", - "exclude": ["**/*.ts"] -} diff --git a/.context/effect/packages/tools/utils/package.json b/.context/effect/packages/tools/utils/package.json index 4302f8f6e..d5eef440d 100644 --- a/.context/effect/packages/tools/utils/package.json +++ b/.context/effect/packages/tools/utils/package.json @@ -43,9 +43,7 @@ "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "dependencies": { "@effect/platform-node": "workspace:^", @@ -53,6 +51,6 @@ "glob": "^13.0.6" }, "devDependencies": { - "@types/node": "^26.1.1" + "@types/node": "^26.1.2" } } diff --git a/.context/effect/packages/tools/utils/src/Codegen.ts b/.context/effect/packages/tools/utils/src/Codegen.ts index 688ffbd37..5c948a3e1 100644 --- a/.context/effect/packages/tools/utils/src/Codegen.ts +++ b/.context/effect/packages/tools/utils/src/Codegen.ts @@ -118,7 +118,7 @@ export interface BarrelFile { /** * Service interface for discovering annotated barrel files and regenerating their export contents. * - * @category models + * @category services * @since 4.0.0 */ export interface BarrelGenerator { diff --git a/.context/effect/packages/tools/utils/src/Glob.ts b/.context/effect/packages/tools/utils/src/Glob.ts index d43caa29d..0fb6a11d8 100644 --- a/.context/effect/packages/tools/utils/src/Glob.ts +++ b/.context/effect/packages/tools/utils/src/Glob.ts @@ -28,7 +28,7 @@ export class GlobError extends Data.TaggedError("GlobError")<{ /** * Service interface for matching filesystem paths with glob patterns. * - * @category models + * @category services * @since 4.0.0 */ export interface Glob { diff --git a/.context/effect/packages/tools/utils/src/commands/codegen.ts b/.context/effect/packages/tools/utils/src/commands/codegen.ts index f225f5f15..0d30fc57a 100644 --- a/.context/effect/packages/tools/utils/src/commands/codegen.ts +++ b/.context/effect/packages/tools/utils/src/commands/codegen.ts @@ -38,7 +38,7 @@ export const codegen = Command.make("codegen", { const files = yield* generator.discoverFiles(config.pattern, path.resolve(config.cwd)) yield* Effect.forEach(files, (file) => generator.processFile(file), { - concurrency: "inherit", + concurrency: "unbounded", discard: true }) })).pipe(Command.provide(CodegenLayer)) diff --git a/.context/effect/packages/tools/utils/tsconfig.json b/.context/effect/packages/tools/utils/tsconfig.json index 739eb6b55..fce3e3d43 100644 --- a/.context/effect/packages/tools/utils/tsconfig.json +++ b/.context/effect/packages/tools/utils/tsconfig.json @@ -1,9 +1,9 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../../tsconfig.base.json", "include": ["src"], "references": [ { "path": "../../effect" }, - { "path": "../../platform-node" } + { "path": "../../platform/node" } ] } diff --git a/.context/effect/packages/vitest/CHANGELOG.md b/.context/effect/packages/vitest/CHANGELOG.md index b4681df28..5bf8ccd84 100644 --- a/.context/effect/packages/vitest/CHANGELOG.md +++ b/.context/effect/packages/vitest/CHANGELOG.md @@ -1,5 +1,65 @@ # @effect/vitest +## 4.0.0-rc.108 + +### Patch Changes + +- Updated dependencies [[`dfb173e`](https://github.com/Effect-TS/effect/commit/dfb173efffd20c4feded4efe409018dd55acdca8), [`005e090`](https://github.com/Effect-TS/effect/commit/005e0902cace9f8960a4f43573665a3a9b53b6fa), [`c82c532`](https://github.com/Effect-TS/effect/commit/c82c53228dc1c50cc99654ce6de7766b4de09e75), [`22b579f`](https://github.com/Effect-TS/effect/commit/22b579f6c582e6e2d951784791fea6f1802517ed), [`3e19539`](https://github.com/Effect-TS/effect/commit/3e19539205082b1006d84553045d1b03db9cc8a1), [`08a3c74`](https://github.com/Effect-TS/effect/commit/08a3c74133206fc1cc728e0aa96d02e672fd80bd), [`eb0bae0`](https://github.com/Effect-TS/effect/commit/eb0bae08d543d58754c9bb7a57e67c1e2bb3f55a), [`97b544d`](https://github.com/Effect-TS/effect/commit/97b544d8b636587647b90691d669305c0eb4fc66), [`4f6d131`](https://github.com/Effect-TS/effect/commit/4f6d131e85d74ab0ec0300e52e503a5f943fc576), [`fad4b7c`](https://github.com/Effect-TS/effect/commit/fad4b7c5138b3f38c2427436da2e0685c1ca4e9b), [`accf447`](https://github.com/Effect-TS/effect/commit/accf4474513064e2a21d14b1937503261b4f34dc), [`31b27e4`](https://github.com/Effect-TS/effect/commit/31b27e49903c351588435f666c953aaac28f6120), [`8458951`](https://github.com/Effect-TS/effect/commit/84589518c3966c63d7f3679a5296d380eb1ba887)]: + - effect@4.0.0-rc.108 + +## 4.0.0-beta.107 + +### Patch Changes + +- Updated dependencies [[`596f3f9`](https://github.com/Effect-TS/effect/commit/596f3f92d7fe355811b815cb212332b082268ce8), [`9611ed4`](https://github.com/Effect-TS/effect/commit/9611ed42d11300546b339ab13492a0f7bdb1ebfb), [`8b91605`](https://github.com/Effect-TS/effect/commit/8b9160548556e4b0ec7ee2f2707716776be49018), [`d901928`](https://github.com/Effect-TS/effect/commit/d901928efa44f573ed1247f53fdb203a8e4fcede), [`b32bdef`](https://github.com/Effect-TS/effect/commit/b32bdef0d119a1ad1463dc01a46763ffee1f9bd9)]: + - effect@4.0.0-beta.107 + +## 4.0.0-beta.106 + +### Patch Changes + +- Updated dependencies [[`2695168`](https://github.com/Effect-TS/effect/commit/269516851b24916d72771f8a554b88722e3732e7), [`6310a8c`](https://github.com/Effect-TS/effect/commit/6310a8c68c74dcf1d23948ec9243ac5f407a1651), [`c2071b1`](https://github.com/Effect-TS/effect/commit/c2071b1647e2326568c1d0689274ef62b8a7183f), [`7aff81a`](https://github.com/Effect-TS/effect/commit/7aff81a9cefe681483ef8abf717d786fd10e7e8d), [`a1d4057`](https://github.com/Effect-TS/effect/commit/a1d4057711935a544ef441bc2d0ac3565dfa9266), [`abf77b0`](https://github.com/Effect-TS/effect/commit/abf77b04009dcb4d67a258f9d8ada778e9f4ffae), [`6c60375`](https://github.com/Effect-TS/effect/commit/6c60375e68683a32d54554150cc493e16550a06d), [`22f4897`](https://github.com/Effect-TS/effect/commit/22f4897bbae24783d4516f6bef353f1db4ec6d03), [`615d1d5`](https://github.com/Effect-TS/effect/commit/615d1d5d0256ec8160f2e08d0dcf5dc83acb7bf1), [`3a86757`](https://github.com/Effect-TS/effect/commit/3a867573ddeed5888dabdeb3225a9ebbf00491e7), [`f4a9762`](https://github.com/Effect-TS/effect/commit/f4a9762bb9dfad59c215f2e099dcc829d74f4ed1), [`0bcf6ed`](https://github.com/Effect-TS/effect/commit/0bcf6ed57c22e8a36964726b15464101d90f5997), [`ba9cb63`](https://github.com/Effect-TS/effect/commit/ba9cb63b87d45ce2df872dd8ef0905da147cc675), [`42c810d`](https://github.com/Effect-TS/effect/commit/42c810dd372275b822dd99c7d7e774e153f0a752), [`1416ccd`](https://github.com/Effect-TS/effect/commit/1416ccd474bc9da8979f51b72b5e53fb3ac56edf), [`08d0d39`](https://github.com/Effect-TS/effect/commit/08d0d39a225deccb9db213ab5fcf55edb9f9ba5d), [`548908a`](https://github.com/Effect-TS/effect/commit/548908a71d9337cb7defe7fc93b2fba8f6a04b6f), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`d170596`](https://github.com/Effect-TS/effect/commit/d17059615cca37ca2776654078fe0501ac5202e6), [`aea89d0`](https://github.com/Effect-TS/effect/commit/aea89d0c42ee0ac707a4962cd348fd3158cb469b), [`deed5fb`](https://github.com/Effect-TS/effect/commit/deed5fbdc91cf8bf8c5fce7dfa5d6527ac944726)]: + - effect@4.0.0-beta.106 + +## 4.0.0-beta.105 + +### Patch Changes + +- [#7094](https://github.com/Effect-TS/effect/pull/7094) [`31efc5c`](https://github.com/Effect-TS/effect/commit/31efc5c4eabbe37f9ddab030ef760926e6ff3d24) Thanks @fubhy! - Update peer dependencies +- Updated dependencies [[`0418564`](https://github.com/Effect-TS/effect/commit/04185644dabb8e4169f1ef6cbbc0b36c4db2f7f4), [`d334a85`](https://github.com/Effect-TS/effect/commit/d334a8593aafcd60753995a9449b654c67bfdcc1), [`f0be855`](https://github.com/Effect-TS/effect/commit/f0be8554da6ee00293a6b23869ac46a0b0d97dc8), [`b206fa5`](https://github.com/Effect-TS/effect/commit/b206fa5d7655c1634c9993410a9203f6616a5ca2), [`b938c8a`](https://github.com/Effect-TS/effect/commit/b938c8ad2823bd88493187922f7d9090eff037b6), [`8525f05`](https://github.com/Effect-TS/effect/commit/8525f05d1e14ea12298e9e1a0df497bfaac2ce9a)]: + - effect@4.0.0-beta.105 + +## 4.0.0-beta.104 + +### Patch Changes + +- Updated dependencies [[`1001bcc`](https://github.com/Effect-TS/effect/commit/1001bccb9e874918d59dbb36860f1c5d4499ac20), [`993ba60`](https://github.com/Effect-TS/effect/commit/993ba60ee6c7ca6eb84522040f8b0d268b6ba7d4), [`67faacd`](https://github.com/Effect-TS/effect/commit/67faacd4679242559bee31717c05a5b10b990322), [`b78acdf`](https://github.com/Effect-TS/effect/commit/b78acdf422568f10ae8684fd3f10d52b065f0b56), [`fbb9ce5`](https://github.com/Effect-TS/effect/commit/fbb9ce5e625d1a7d6b7005bda42cbb6cd31476c5), [`722ea48`](https://github.com/Effect-TS/effect/commit/722ea484c9d01364c9242d929c0a564f7831a57c), [`3058fd5`](https://github.com/Effect-TS/effect/commit/3058fd594f5a683034212d71d957017fcc084006), [`62d0575`](https://github.com/Effect-TS/effect/commit/62d057566c241405c23ecf0bf4156186bd2be924), [`99dd6b5`](https://github.com/Effect-TS/effect/commit/99dd6b580434f97c5b40adc919f429e4abc3dfe7), [`7963ce1`](https://github.com/Effect-TS/effect/commit/7963ce1cd95f037fbefea67a29ead49cce4d16cb), [`af14e75`](https://github.com/Effect-TS/effect/commit/af14e752edd65e2b652e960411afafc88975a8d8), [`24e22d2`](https://github.com/Effect-TS/effect/commit/24e22d23a73a2e93ebf6d8edd2246a4a406942c8), [`647d14e`](https://github.com/Effect-TS/effect/commit/647d14e572c8004fa92fba256e00552b42bf34b7), [`1434eec`](https://github.com/Effect-TS/effect/commit/1434eecbd368e00839c24b3950f0b7a69218669a), [`a5278b1`](https://github.com/Effect-TS/effect/commit/a5278b18242011d1b2b08304c7c128151f9a4370), [`6af04a5`](https://github.com/Effect-TS/effect/commit/6af04a50bd019238f6acdb9cbda40439a3c09210), [`cb6c837`](https://github.com/Effect-TS/effect/commit/cb6c8376b2f322d4e7cbfc0973fc3b4f2951ee6e), [`d44cead`](https://github.com/Effect-TS/effect/commit/d44cead7e0e0ce61f0d980906e494f49a07e7899), [`88c7632`](https://github.com/Effect-TS/effect/commit/88c7632c2b59a49fcc40d250865bd8d0dccf31b0), [`abcbb2a`](https://github.com/Effect-TS/effect/commit/abcbb2abe16f1b6c587c15007df14371e1e70e93), [`8f63cce`](https://github.com/Effect-TS/effect/commit/8f63cce636700fde26b140b82e350ef916989d86), [`d56dfcf`](https://github.com/Effect-TS/effect/commit/d56dfcf54c2b9c53c3d098ce4b0ffcc84496c5f7), [`a98cda9`](https://github.com/Effect-TS/effect/commit/a98cda9422e1352f22e81696f759f326ffcfb667), [`6704bb8`](https://github.com/Effect-TS/effect/commit/6704bb84c320547f83cf50e8586ffc4c5e4c3cc5), [`6143de2`](https://github.com/Effect-TS/effect/commit/6143de21ee22038b45a8d4eba86f5aade6238eba), [`936b135`](https://github.com/Effect-TS/effect/commit/936b1358396eb0a1a7c8e0878ba63297e2106812), [`1bbae84`](https://github.com/Effect-TS/effect/commit/1bbae84f88b577a26d04ceb2e76d3143d09c4a20), [`d795ee7`](https://github.com/Effect-TS/effect/commit/d795ee771701ea62bd187ef7c0307d9737f68c1a), [`0a82d88`](https://github.com/Effect-TS/effect/commit/0a82d88b7da73278b6f270118e396d5ed4a64747), [`9215bc5`](https://github.com/Effect-TS/effect/commit/9215bc5da7dd10aa45f07fe44b98f06b6e433d62), [`a1b5df2`](https://github.com/Effect-TS/effect/commit/a1b5df2064d92431cfc6e638af613cc3114313d7), [`92a9ac5`](https://github.com/Effect-TS/effect/commit/92a9ac5ac0aa63d8975b9ba7a094d6a8f59a98f2), [`6bde7f2`](https://github.com/Effect-TS/effect/commit/6bde7f27f3243427203e53fe74472990e5c2a349), [`a712131`](https://github.com/Effect-TS/effect/commit/a7121310dbb60cbd819bbd702f97663098ec7bb8), [`2e6f760`](https://github.com/Effect-TS/effect/commit/2e6f760dcb44e2b984f3311a8af03a1d68a2ec7e), [`aa05804`](https://github.com/Effect-TS/effect/commit/aa0580497e027ed30b756058db0067c3fe07664f), [`badd3bf`](https://github.com/Effect-TS/effect/commit/badd3bf65fac4dd1e66e1f602db43659722dfced), [`02b0265`](https://github.com/Effect-TS/effect/commit/02b02651ede46a5a2dd3ef8081d0ad89648d0cbf), [`3437e21`](https://github.com/Effect-TS/effect/commit/3437e21a56d805781c5e5946a6189795a1dfd411), [`41a550d`](https://github.com/Effect-TS/effect/commit/41a550d1fed31e829929a8f5362b5340303164ac), [`17b5d50`](https://github.com/Effect-TS/effect/commit/17b5d50219ad49533cf9e33d01924a3e16af5eb3), [`96e5e95`](https://github.com/Effect-TS/effect/commit/96e5e9576b0315c747462761a61940ff9fe32dd1), [`e4d589e`](https://github.com/Effect-TS/effect/commit/e4d589e0ea08dc57c4793053b395dc0fcc499f34), [`ae4cf7b`](https://github.com/Effect-TS/effect/commit/ae4cf7b5e2cb5f8c55657e31a61789ad21c38c18), [`6ef5f1a`](https://github.com/Effect-TS/effect/commit/6ef5f1a041f3a40bf03fadd0b1feb275c277c635), [`2235a29`](https://github.com/Effect-TS/effect/commit/2235a29502c3f33cf6468511ad931089013a7916), [`b32f4cb`](https://github.com/Effect-TS/effect/commit/b32f4cb7b2d8ebe817075322622498e3beb05336), [`7f4c095`](https://github.com/Effect-TS/effect/commit/7f4c095b62da43780dd7fc2a5d1785ddfce60edf), [`5f3fb81`](https://github.com/Effect-TS/effect/commit/5f3fb814d18d8a54946c1c1cd0b41459cdb24006), [`17f0b91`](https://github.com/Effect-TS/effect/commit/17f0b91a243ccfe4a38d27debdc983adf434e738), [`0cdadd7`](https://github.com/Effect-TS/effect/commit/0cdadd75bc8abbbcad7956a4bc71f4e7a9b13250), [`39b57d7`](https://github.com/Effect-TS/effect/commit/39b57d7857358040558b67dd33eafc7bb5457830), [`5a6a573`](https://github.com/Effect-TS/effect/commit/5a6a5738e5bfc39e3a37ae7ba99081601fa19ac3), [`59f5e99`](https://github.com/Effect-TS/effect/commit/59f5e9981913b92d7a9beb2214a21d658b999d3a), [`45379d6`](https://github.com/Effect-TS/effect/commit/45379d6179ee4df2cbd3f848bd39ff7149c24a38), [`1949439`](https://github.com/Effect-TS/effect/commit/1949439175809ef81ab9c6411ed5559109edb4c9), [`e443403`](https://github.com/Effect-TS/effect/commit/e443403cf0e4effea14bb6cd950c5ac1c86cc748), [`03af7e8`](https://github.com/Effect-TS/effect/commit/03af7e85551204c605ea2fa2c43c10a4538ac8fb), [`0f721d4`](https://github.com/Effect-TS/effect/commit/0f721d406df8703ea92ca28777b3f09599e2056d), [`130b28d`](https://github.com/Effect-TS/effect/commit/130b28df552d7053407b041a96ff09dae82575e5), [`c987a12`](https://github.com/Effect-TS/effect/commit/c987a12a01b6a52ad53d29edf02613b03574dbcc), [`4158562`](https://github.com/Effect-TS/effect/commit/41585620977de9b84171f76619b72e29cc2284e5), [`306014a`](https://github.com/Effect-TS/effect/commit/306014a1ce4d5cb956c76bdc20e4e28ab3e61a6a), [`729a663`](https://github.com/Effect-TS/effect/commit/729a663275dd31f2357c446fe69664429220a83d), [`caf84b6`](https://github.com/Effect-TS/effect/commit/caf84b660044089e8d7f4067b279b27b8b50e8fd), [`ce067f7`](https://github.com/Effect-TS/effect/commit/ce067f799ea27735d4194345298a216aaf429f01), [`7a41f5a`](https://github.com/Effect-TS/effect/commit/7a41f5aa72d540ecf2746992ecc3fa3e6b40d31f), [`781022a`](https://github.com/Effect-TS/effect/commit/781022acdd3537ca18c88e2fa3681bafa6ef1b21), [`39f1297`](https://github.com/Effect-TS/effect/commit/39f1297acc08864feb12de6b8cf2bf73434f6cf5), [`2db266b`](https://github.com/Effect-TS/effect/commit/2db266b1bfbc81868bc1778c37c76032a267c79f), [`2141e28`](https://github.com/Effect-TS/effect/commit/2141e28903754d72604acf81673ceb2c62a56646), [`3c5e429`](https://github.com/Effect-TS/effect/commit/3c5e429878669ffcf5e0da4ddfbf50bde5bbcaad), [`20ddc63`](https://github.com/Effect-TS/effect/commit/20ddc630584f8fe488162ba384adcae53fc6810a), [`841b3ea`](https://github.com/Effect-TS/effect/commit/841b3ea6ae19a784bc1c20497b02f632af0c91e9), [`82a3fbf`](https://github.com/Effect-TS/effect/commit/82a3fbfce8b9df33e587076b7d7168ecd6799e17), [`eb9ee83`](https://github.com/Effect-TS/effect/commit/eb9ee83b38844a71d1cd5653a229309cfcb04a36), [`64dc7c7`](https://github.com/Effect-TS/effect/commit/64dc7c76dc5c89887b9e7c181d1873dcbb7820d1), [`84dc8ab`](https://github.com/Effect-TS/effect/commit/84dc8ab7accc682bc668c78a97e4a1776b633be8), [`b4463f4`](https://github.com/Effect-TS/effect/commit/b4463f46fc33d3b01ea5eadd7d012a5abda347a3), [`592dd36`](https://github.com/Effect-TS/effect/commit/592dd361645739ac0cd8e6babb084cd27403c172), [`85d2b44`](https://github.com/Effect-TS/effect/commit/85d2b446e3059de4919be730105868f79728308d), [`32e4a69`](https://github.com/Effect-TS/effect/commit/32e4a69b3151b7ec4058af2213b96a41d11e9e06), [`13c5872`](https://github.com/Effect-TS/effect/commit/13c5872ed30830360367ad89af2dab68a003c351), [`3454cdb`](https://github.com/Effect-TS/effect/commit/3454cdb528fdb5d3ed0c5c5c8169bc47de41fbd8), [`e930804`](https://github.com/Effect-TS/effect/commit/e9308045be1d8a00c0b4046f1e8ff22cf68c93da), [`7f12d4b`](https://github.com/Effect-TS/effect/commit/7f12d4b4e731dc3a213ae5c3f60db9edc50292d2), [`181c9ef`](https://github.com/Effect-TS/effect/commit/181c9ef5e5d4ab247bf4aec06424f15b0a1e802e), [`dd9f891`](https://github.com/Effect-TS/effect/commit/dd9f891e23f316abb6192893008f0e33ece9d97d), [`433fb81`](https://github.com/Effect-TS/effect/commit/433fb81ca4c15c681a8ae097ce3ff9bd3a9c9aa5), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`6124ab3`](https://github.com/Effect-TS/effect/commit/6124ab39eb64688fbd5d688d24766542f9cb5a2c), [`01bd954`](https://github.com/Effect-TS/effect/commit/01bd9546f142706fca1628f7261e6d1cb9638948), [`ba2c3aa`](https://github.com/Effect-TS/effect/commit/ba2c3aa05eb87ec05d263b960017ecf29746f66a), [`0a45ef3`](https://github.com/Effect-TS/effect/commit/0a45ef3bb4a1ae9b345c43c548db4336a31b3191), [`8459cdb`](https://github.com/Effect-TS/effect/commit/8459cdbae8a476dc04b6247fffe6a1668dcb1217), [`eaa7e71`](https://github.com/Effect-TS/effect/commit/eaa7e71b88bf59b24610128c6115a2a126432731), [`db4c2cc`](https://github.com/Effect-TS/effect/commit/db4c2ccdec77d813b6f4cc72a302ee7c4fe6e39d), [`22f150a`](https://github.com/Effect-TS/effect/commit/22f150a0936cef30517e87eaca73bff1c5e4873a), [`90ffb08`](https://github.com/Effect-TS/effect/commit/90ffb083b3091c211300f50a42ba7bf56536c0ee), [`d517692`](https://github.com/Effect-TS/effect/commit/d517692ef75f45d5f6d9d68b32d41fa0ccc56c99), [`01af079`](https://github.com/Effect-TS/effect/commit/01af079c189d1fc5067d3b1933b2870c4baf2693), [`32a59e8`](https://github.com/Effect-TS/effect/commit/32a59e8058b1ec9738cb083cf1cb116b393ca114)]: + - effect@4.0.0-beta.104 + +## 4.0.0-beta.103 + +### Minor Changes + +- [#6668](https://github.com/Effect-TS/effect/pull/6668) [`83c7497`](https://github.com/Effect-TS/effect/commit/83c74972f67cd05513897e43892b63ef48fb7cc1) Thanks @tim-smart! - Require Vitest 4.1 or later and read suite state from `TestRunner`, removing the direct `@vitest/runner` import and support for Vitest 3 and 4.0. + +### Patch Changes + +- [#6843](https://github.com/Effect-TS/effect/pull/6843) [`4a394bd`](https://github.com/Effect-TS/effect/commit/4a394bd2980c357bbadbb1684172189511c33381) Thanks @fubhy! - Ensure `throws` and `throwsAsync` fail when the supplied operation returns or resolves without throwing. + +- [#6701](https://github.com/Effect-TS/effect/pull/6701) [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c) Thanks @fubhy! - Removed explicit ./index entrypoints + +- Updated dependencies [[`e56cd8f`](https://github.com/Effect-TS/effect/commit/e56cd8f90c3559baccf8fcf2852ea911235d5944), [`f77c120`](https://github.com/Effect-TS/effect/commit/f77c120d8e04779ddeb8bce8e9cde932f268e4b6), [`b2f95a9`](https://github.com/Effect-TS/effect/commit/b2f95a9c2f2581deb89dc3bae9e89cf819e82923), [`04fd44a`](https://github.com/Effect-TS/effect/commit/04fd44a42abfa8dc2642300dcf49ee48c8ef4539), [`b74333d`](https://github.com/Effect-TS/effect/commit/b74333d83e15b9d042e4698ad23040de60454afe), [`1c40b28`](https://github.com/Effect-TS/effect/commit/1c40b2809503d6aa1358777196fc66317906e657), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b3901d2`](https://github.com/Effect-TS/effect/commit/b3901d29c543fd5bd05ceec669a17896c8e19006), [`4a0984a`](https://github.com/Effect-TS/effect/commit/4a0984af62738fedf4bd3e87adb4d4d641ce9147), [`fffd88b`](https://github.com/Effect-TS/effect/commit/fffd88b3135abdf928ca7c4b0e00e610985091c7), [`f3f6c1e`](https://github.com/Effect-TS/effect/commit/f3f6c1e02cb543423fcffef5dc2db03fac503588), [`ef07642`](https://github.com/Effect-TS/effect/commit/ef07642dfe671d5258b65d1c1480c4d05c495f15), [`f1bc827`](https://github.com/Effect-TS/effect/commit/f1bc8274a608813d7b09d28dcca04adbf62f8c92), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`081f4d8`](https://github.com/Effect-TS/effect/commit/081f4d8cd06a2ac222d2810b46e61efcee26939e), [`5287b24`](https://github.com/Effect-TS/effect/commit/5287b24f5f8fa094ba20e117bfb1a80fba6d2cf5), [`13d31cf`](https://github.com/Effect-TS/effect/commit/13d31cfc2dde46210e94391b5b6767ae9aeaf2c9), [`acee269`](https://github.com/Effect-TS/effect/commit/acee26944bc89ee554d7b9fadab7443f9edc28a9), [`31170c1`](https://github.com/Effect-TS/effect/commit/31170c19b236c37abb5476c821bc6f5bfa2735ab), [`205ebc7`](https://github.com/Effect-TS/effect/commit/205ebc776062012581e98fced7ced19adfc44ee7), [`ed0ebf8`](https://github.com/Effect-TS/effect/commit/ed0ebf8e5c864d46fed1f232e99c0e680f10a58f), [`a3fd084`](https://github.com/Effect-TS/effect/commit/a3fd08482157bd78b089f77c7b173d54ef68b5cd), [`ee29ddf`](https://github.com/Effect-TS/effect/commit/ee29ddf862c3723ad466abc93ab6f6fe723b2319), [`6086309`](https://github.com/Effect-TS/effect/commit/60863090af8e5af0bfa1435f08dc5390f9993e30), [`4a57af2`](https://github.com/Effect-TS/effect/commit/4a57af24011db1d66e947289d2f7ffc2074696d2), [`660875b`](https://github.com/Effect-TS/effect/commit/660875b4325e6eebb3f04513998301cd2a0847ec), [`8e7c706`](https://github.com/Effect-TS/effect/commit/8e7c706b0aca855489b53d987404566d3e9cb5e7), [`5f63adb`](https://github.com/Effect-TS/effect/commit/5f63adbe75fc9d50d23706a52b3e483ad2a1a01c), [`053bc42`](https://github.com/Effect-TS/effect/commit/053bc42e2a964755611a216e78ed214322efee37), [`c0a1534`](https://github.com/Effect-TS/effect/commit/c0a153494484ecf9f0d0f20895a7a648b4be363b), [`f1e3a37`](https://github.com/Effect-TS/effect/commit/f1e3a378c144f974a6122b299f421b75595af20f), [`cedb01a`](https://github.com/Effect-TS/effect/commit/cedb01a025492a1faf9e59eb23eb96bc3b5e2fff), [`1747440`](https://github.com/Effect-TS/effect/commit/1747440de9a51a56ed3660da748cc01b256adce7), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`b4f1ee2`](https://github.com/Effect-TS/effect/commit/b4f1ee238d96aa78c5f040158cb78671d75b381e), [`a4757f1`](https://github.com/Effect-TS/effect/commit/a4757f1c47067d8d016a6c4a2c541bb8ae520f9b), [`cd122b9`](https://github.com/Effect-TS/effect/commit/cd122b90300d995a237993a2edb7a049785ab6a4), [`5de588b`](https://github.com/Effect-TS/effect/commit/5de588b2472fb0f4eb919766eb8472583a044772), [`3895b9c`](https://github.com/Effect-TS/effect/commit/3895b9cf179262cd277a9c6daafe9050dcf8265e), [`89ce5f3`](https://github.com/Effect-TS/effect/commit/89ce5f3e16e23a193daa475dc72ea8133ae1dacd), [`985de09`](https://github.com/Effect-TS/effect/commit/985de097d75906db2aed784841f81e23cc978b43), [`9800e3a`](https://github.com/Effect-TS/effect/commit/9800e3acc8f36530f671bc8b91558cb112f449a7), [`4dc35f6`](https://github.com/Effect-TS/effect/commit/4dc35f64641746366f867ea3dbfedb9cd4685ada), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`ecd9993`](https://github.com/Effect-TS/effect/commit/ecd99936112cb69efdb02de3a2fd57f47baefdf3), [`5ab9c08`](https://github.com/Effect-TS/effect/commit/5ab9c08463ce049c45f3502676954a7b72c6b024), [`f5cf965`](https://github.com/Effect-TS/effect/commit/f5cf96548afd51f4b3cf1aea11b04d7f8549ce90), [`a94cbed`](https://github.com/Effect-TS/effect/commit/a94cbed84e9e49bea4bff925599c0f19c4e3deab), [`9160ad7`](https://github.com/Effect-TS/effect/commit/9160ad7d146d4376dd12f7510c025e5b2f638a70), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`52494be`](https://github.com/Effect-TS/effect/commit/52494be9e8eb3bb542d06a3dfefc6bca4e168984), [`5441c8e`](https://github.com/Effect-TS/effect/commit/5441c8e656a6418c0d27feb2df67565a3e1155f4), [`c9b56ab`](https://github.com/Effect-TS/effect/commit/c9b56ab507f224426ee8388dc450da447ec4715f), [`8ef7257`](https://github.com/Effect-TS/effect/commit/8ef72577d1f43212cab87951d659e54e3c8d7d91), [`1519406`](https://github.com/Effect-TS/effect/commit/1519406fed6e8b017ae178dc20bcaa2cf318b570), [`9716990`](https://github.com/Effect-TS/effect/commit/97169902eec3c99baa7f0b2c7b45a0a5eae75819), [`733f75b`](https://github.com/Effect-TS/effect/commit/733f75b7125e3016a975fdd251c0179ae5393786), [`48155c8`](https://github.com/Effect-TS/effect/commit/48155c8ccfc12dcca8a00fa358d50b20c30874e4), [`951d06b`](https://github.com/Effect-TS/effect/commit/951d06b83d459d3e8fa9024e727a5db1662d3322), [`d767b65`](https://github.com/Effect-TS/effect/commit/d767b65a7687e38be23f0b0ee3d52ab5f2360cbe), [`5d52d9d`](https://github.com/Effect-TS/effect/commit/5d52d9d148aaa7f736ed8c310fc8bfa9dc81badf), [`f4151e1`](https://github.com/Effect-TS/effect/commit/f4151e1937c26de14f1d64566f8126173f1b5014), [`e02fbb6`](https://github.com/Effect-TS/effect/commit/e02fbb66f5a0f13dba6c33ef63528a37a17a0676), [`724ce09`](https://github.com/Effect-TS/effect/commit/724ce09650a458d4565e5c7331ea92ca04f08e68), [`dbe91f6`](https://github.com/Effect-TS/effect/commit/dbe91f6961ef9f7e8da910ee5758d9c0d385fca8), [`4c008d2`](https://github.com/Effect-TS/effect/commit/4c008d28b370d817f7ae4579db09836fe084c8d2), [`b650832`](https://github.com/Effect-TS/effect/commit/b6508328708a842f3163467b72486bd228f1a289), [`b46c92f`](https://github.com/Effect-TS/effect/commit/b46c92f3b314f4ffd612b831efa55dd856c587a3), [`5335797`](https://github.com/Effect-TS/effect/commit/5335797003076d9c6fd170da98d779696d555596), [`4b3460d`](https://github.com/Effect-TS/effect/commit/4b3460daa434ec465a95a50704fe1103a9275999), [`6301fd7`](https://github.com/Effect-TS/effect/commit/6301fd710b4325718de2c42997dac28a9e9aa250), [`aebc5c6`](https://github.com/Effect-TS/effect/commit/aebc5c61664b89a840465ec65b79ce635a5ceee8), [`52b2d7b`](https://github.com/Effect-TS/effect/commit/52b2d7b5bd3c7cce3bd5b69c6ab3941004da70f3), [`eec5744`](https://github.com/Effect-TS/effect/commit/eec57445dfa0ef3c5977195ad69415b7e7d42bb6), [`24e0e93`](https://github.com/Effect-TS/effect/commit/24e0e93dc307dc2c2ae86caacb7289e1dab3c103), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1a7ce81`](https://github.com/Effect-TS/effect/commit/1a7ce8150e3977586c44d8ccb9a8384389bb4d49), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`c96b7f6`](https://github.com/Effect-TS/effect/commit/c96b7f6359662053c3e09344f61dddc7a6caf4ac), [`6d2a942`](https://github.com/Effect-TS/effect/commit/6d2a942ed7cd33b8fd79d549edba33bc9e2a7e3e), [`cc27b19`](https://github.com/Effect-TS/effect/commit/cc27b194b9d13fa3a66ab037e853fca9d41700ff), [`8f9499f`](https://github.com/Effect-TS/effect/commit/8f9499f562729f5f7b08d8bcc4db86b4aeff8a21), [`3eeea73`](https://github.com/Effect-TS/effect/commit/3eeea73cfc3e9b126975c2ddbdb7f7c8c92026e2), [`0a532e5`](https://github.com/Effect-TS/effect/commit/0a532e503f165fdea485a5343fc2f420917e8376), [`f398149`](https://github.com/Effect-TS/effect/commit/f398149c134fd9b67b6cdc52eae3f3248d5c7bbe), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`ace903e`](https://github.com/Effect-TS/effect/commit/ace903e09c2549ceebdec380797beb027cd29f3d), [`e8eb62b`](https://github.com/Effect-TS/effect/commit/e8eb62b3d0ef27e9761cdc2eb93bdec52d6ee204), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`48f22a7`](https://github.com/Effect-TS/effect/commit/48f22a7d16ae57ee2175d450dafbdeb69e187d2a), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`d48506d`](https://github.com/Effect-TS/effect/commit/d48506d97525040aa714305e928126df799795b4), [`52262be`](https://github.com/Effect-TS/effect/commit/52262be2edce0e350c6ac10f8f725678606399c5), [`1284aa1`](https://github.com/Effect-TS/effect/commit/1284aa183451955ad7921bbe01fd0e095695d444), [`9867b9f`](https://github.com/Effect-TS/effect/commit/9867b9fc69f9cc6c443594fc7eccc7be0c674d9c), [`d0f1a22`](https://github.com/Effect-TS/effect/commit/d0f1a2295155c350b04efb46852cb40032805273), [`979ce39`](https://github.com/Effect-TS/effect/commit/979ce3985d7d62ce2bf240681ca19feda3027452), [`b6d3e67`](https://github.com/Effect-TS/effect/commit/b6d3e67c7cc143cd8470cdf704324e79d23954a9), [`adf6c6c`](https://github.com/Effect-TS/effect/commit/adf6c6cd388af8a3c0c546492e71555368556f6a), [`7314d60`](https://github.com/Effect-TS/effect/commit/7314d605284717aaafe7fc34b88c3c93397e865c), [`aeba0c8`](https://github.com/Effect-TS/effect/commit/aeba0c8c9ffc5f125d961ae21e4ac15491e51046), [`1acbd8b`](https://github.com/Effect-TS/effect/commit/1acbd8b44c68ebb23735e9810476b870dbe58aea), [`7bde6cc`](https://github.com/Effect-TS/effect/commit/7bde6ccb2b144fe953ff30a7ef5e1ecc97697146), [`a959a8b`](https://github.com/Effect-TS/effect/commit/a959a8bf21cdb976369f494dc949fa00a050d3e0)]: + - effect@4.0.0-beta.103 + +## 4.0.0-beta.102 + +### Patch Changes + +- [#6567](https://github.com/Effect-TS/effect/pull/6567) [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06) Thanks @gcanti! - Add `Record.assignProperty` and safely handle dynamic record keys such as `__proto__` and inherited property names. + +- Updated dependencies [[`b6392e1`](https://github.com/Effect-TS/effect/commit/b6392e119704553edec1b4fd2869ac0dbec621ef), [`7ed9450`](https://github.com/Effect-TS/effect/commit/7ed945044eb56aa9aeaf62d4746a011c96c58628), [`45762bd`](https://github.com/Effect-TS/effect/commit/45762bd78df9ecd87c98b8d3738cdeeac7d81128), [`a6e8391`](https://github.com/Effect-TS/effect/commit/a6e8391cd31acd898fae18b3f8e7ca4c6f14f065), [`4ac7e8b`](https://github.com/Effect-TS/effect/commit/4ac7e8b136c61a26c3e438c013dfd7349b38e999), [`4cd40f5`](https://github.com/Effect-TS/effect/commit/4cd40f5692477783bef84fed3c5ef1c0cf5602e6), [`6956bc0`](https://github.com/Effect-TS/effect/commit/6956bc0e6cb27f53fbec39d9b18545940f9f598f), [`0e50ec7`](https://github.com/Effect-TS/effect/commit/0e50ec7dbb94390666f292cf9120719bf30a7246), [`9fcdade`](https://github.com/Effect-TS/effect/commit/9fcdade4a8af772b9ccd8b8a24fe8cee0e5d8470), [`57367d5`](https://github.com/Effect-TS/effect/commit/57367d54de55047ff0c5fce9685475e236bf354c), [`35c445f`](https://github.com/Effect-TS/effect/commit/35c445ff18029d192900ea0914c993f58d5cf1a5), [`c917bb9`](https://github.com/Effect-TS/effect/commit/c917bb94a4c1c4e0a24372a8ebb8a5ca232e36b5), [`bc1f358`](https://github.com/Effect-TS/effect/commit/bc1f3583e63344cb2c398d9040d9c975488ed123), [`0e0c9d7`](https://github.com/Effect-TS/effect/commit/0e0c9d7922ff463c1093d9e0576fae12cb0698d5), [`73d40aa`](https://github.com/Effect-TS/effect/commit/73d40aacd8fcae1b48c23f5b0a5c542127401d1d), [`4f1e318`](https://github.com/Effect-TS/effect/commit/4f1e3183f7123591c46224e9c587df7594562a5f), [`9d8d85c`](https://github.com/Effect-TS/effect/commit/9d8d85c1bb7da51970845b8ea830e386e777514a), [`6079fda`](https://github.com/Effect-TS/effect/commit/6079fda7b02f2f01ad91c15ab8c307336f3ba252), [`5101e92`](https://github.com/Effect-TS/effect/commit/5101e92c9c149c153423f43dd7a94f6194653c06), [`d0b3265`](https://github.com/Effect-TS/effect/commit/d0b3265c3262670761471ab3518cf933b1b3b20a), [`7a03c89`](https://github.com/Effect-TS/effect/commit/7a03c893ce6492bf94c0ebfb00b63bf25dcbf83e), [`cea1d9c`](https://github.com/Effect-TS/effect/commit/cea1d9c92601e69ebda040af8a1d860d604d885c), [`078e1f5`](https://github.com/Effect-TS/effect/commit/078e1f5636e31b76a86722a636afc37a8cc25580), [`97bafea`](https://github.com/Effect-TS/effect/commit/97bafeab460833b9781527b437d1cb9cbee63260), [`fab0ab8`](https://github.com/Effect-TS/effect/commit/fab0ab8f7ab15ae596faa4ccf75615a494d11b0b), [`c323d8b`](https://github.com/Effect-TS/effect/commit/c323d8b30dbbe85f9df25b67288b93d5332de333), [`6966353`](https://github.com/Effect-TS/effect/commit/69663534d626003eb10a5e55ab1f13e0379fead1), [`0444004`](https://github.com/Effect-TS/effect/commit/04440041989c1785fe4db286379f2be2c15baa85), [`028bbb3`](https://github.com/Effect-TS/effect/commit/028bbb391e161185da10d974ab33381f769940d7), [`ff5d6e2`](https://github.com/Effect-TS/effect/commit/ff5d6e278a1fdff714315dc1a17075012f05c1f0), [`1bfce93`](https://github.com/Effect-TS/effect/commit/1bfce93e6d2bf0794c11733daf51c2390e7de375), [`7ce815c`](https://github.com/Effect-TS/effect/commit/7ce815cd5af6af991dfc13b890fd22345fc77c20), [`7271a7f`](https://github.com/Effect-TS/effect/commit/7271a7faf1080aa75f2f53ca6a0b5ec9334c1d38), [`475fe5c`](https://github.com/Effect-TS/effect/commit/475fe5c12c2d6504c475797c0634f90da01e1797)]: + - effect@4.0.0-beta.102 + ## 4.0.0-beta.101 ### Patch Changes diff --git a/.context/effect/packages/vitest/README.md b/.context/effect/packages/vitest/README.md index 42bd2755c..e0e0892c2 100644 --- a/.context/effect/packages/vitest/README.md +++ b/.context/effect/packages/vitest/README.md @@ -1,24 +1,21 @@ -# Introduction +# @effect/vitest -Welcome to your guide on testing Effect-based applications using `vitest` and the `@effect/vitest` package. This package simplifies running tests for Effect-based code with Vitest. +Helpers for testing Effect-based code with [Vitest](https://vitest.dev). Provides an enhanced `it` function with support for scoped tests, test services such as `TestClock`, shared layers, and property testing. -In this guide, we'll walk you through setting up the necessary dependencies and provide examples of how to write Effect-based tests using `@effect/vitest`. +## Installation -# Requirements - -First, ensure you have a supported [`vitest`](https://vitest.dev/guide/) version installed (`^3.0.0` or `^4.0.0`). +Ensure a supported `vitest` version is installed (`^4.1.0`), then add the package as a dev dependency: ```sh -pnpm add -D vitest +npm install -D vitest @effect/vitest@beta ``` -Next, install the `@effect/vitest` package, which integrates Effect with Vitest. +## Documentation -```sh -pnpm add -D @effect/vitest -``` +- [Effect website](https://effect.website) +- [API reference](https://effect.website/docs/v4/api/vitest) -# Overview +## Overview The main entry point is the following import: @@ -28,15 +25,15 @@ import { it } from "@effect/vitest" This import enhances the standard `it` function from `vitest` with several powerful features, including: -| Feature | Description | -| --------------- | ------------------------------------------------------------------------------------------------------ | -| `it.effect` | Automatically injects a `TestContext` (e.g., `TestClock`) when running a test. | -| `it.live` | Runs the test with the live Effect environment. | -| `it.scoped` | Allows running an Effect program that requires a `Scope`. | -| `it.scopedLive` | Combines the features of `scoped` and `live`, using a live Effect environment that requires a `Scope`. | -| `it.flakyTest` | Facilitates the execution of tests that might occasionally fail. | +| Feature | Description | +| -------------- | --------------------------------------------------------------------------------------------------- | +| `it.effect` | Runs a scoped test with test services such as `TestClock` and `TestConsole`. | +| `it.live` | Runs a scoped test with the live Effect environment. | +| `it.layer` | Shares a `Layer` between multiple tests. | +| `it.prop` | Runs property tests using Effect `Schema` values or FastCheck arbitraries. | +| `it.flakyTest` | Retries an Effect that might occasionally fail until it succeeds or reaches the configured timeout. | -# Writing Tests with `it.effect` +## Writing Tests with `it.effect` Here's how to use `it.effect` to write your tests: @@ -48,9 +45,9 @@ import { it } from "@effect/vitest" it.effect("test name", () => EffectContainingAssertions, timeout: number | TestOptions = 5_000) ``` -`it.effect` automatically provides a `TestContext`, allowing access to services like [`TestClock`](#using-the-testclock). +`it.effect` automatically provides the Effect test services, including [`TestClock`](#using-the-testclock), and a fresh `Scope` for each test. The scope is closed when the test finishes. -## Testing Successful Operations +### Testing Successful Operations To write a test, place your assertions directly within the main effect. This ensures that your assertions are evaluated as part of the test's execution. @@ -76,7 +73,7 @@ it.effect("test success", () => })) ``` -## Testing Successes and Failures as `Exit` +### Testing Successes and Failures as `Exit` When you need to handle both success and failure cases in a test, you can use `Effect.exit` to capture the outcome as an `Exit` object. This allows you to verify both successful and failed results within the same test structure. @@ -108,9 +105,9 @@ it.effect("test failure as Exit", () => })) ``` -## Using the TestClock +### Using the TestClock -When writing tests with `it.effect`, a `TestContext` is automatically provided. This context gives access to various testing services, including the [`TestClock`](https://effect.website/docs/guides/testing/testclock), which allows you to simulate the passage of time in your tests. +When writing tests with `it.effect`, Effect test services are automatically provided. These include the [`TestClock`](https://effect.website/docs/guides/testing/testclock), which allows you to simulate the passage of time in your tests. **Note**: If you want to use the real-time clock (instead of the simulated one), you can switch to `it.live`. @@ -126,7 +123,8 @@ Here are examples that demonstrate how you can work with time in your tests usin ```ts import { it } from "@effect/vitest" -import { Clock, Effect, TestClock } from "effect" +import { Clock, Effect } from "effect" +import { TestClock } from "effect/testing" // Effect to log the current time const logNow = Effect.gen(function*() { @@ -154,7 +152,7 @@ it.effect("run the test with the test environment and the time adjusted", () => })) ``` -## Skipping Tests +### Skipping Tests If you need to temporarily disable a test but don't want to delete or comment out the code, you can use `it.effect.skip`. This is helpful when you're working on other parts of your test suite but want to keep the test for future execution. @@ -178,7 +176,7 @@ it.effect.skip("test failure as Exit", () => })) ``` -## Running a Single Test +### Running a Single Test When you're developing or debugging, it's often useful to run a specific test without executing the entire test suite. You can achieve this by using `it.effect.only`, which will run just the selected test and ignore the others. @@ -202,7 +200,7 @@ it.effect.only("test failure as Exit", () => })) ``` -## Expecting Tests to Fail +### Expecting Tests to Fail When adding new failing tests, you might not be able to fix them right away. Instead of skipping them, you may want to assert it fails, so that when you fix them, you'll know and can re-enable them before it regresses. @@ -212,7 +210,7 @@ When adding new failing tests, you might not be able to fix them right away. Ins import { it } from "@effect/vitest" import { Effect, Exit } from "effect" -function divide(a: number, b: number): number { +function divide(a: number, b: number) { if (b === 0) return Effect.fail("Cannot divide by zero") return Effect.succeed(a / b) } @@ -225,7 +223,7 @@ it.effect.fails("dividing by zero special cases", ({ expect }) => })) ``` -## Logging +### Logging By default, `it.effect` suppresses log output, which can be useful for keeping test results clean. However, if you want to enable logging during tests, you can use `it.live` or provide a custom logger to control the output. @@ -246,7 +244,7 @@ it.effect("providing a logger displays a log", () => Effect.gen(function*() { yield* Effect.log("it.effect with custom logger") // Log will be displayed }).pipe( - Effect.provide(Logger.pretty) // Providing a pretty logger for log output + Effect.provide(Logger.layer([Logger.consolePretty()])) // Providing a pretty logger for log output )) // This test runs using `it.live`, which enables logging by default @@ -256,11 +254,11 @@ it.live("it.live displays a log", () => })) ``` -# Writing Tests with `it.scoped` +## Resource Safety and Scope -The `it.scoped` method is used for tests that involve `Effect` programs needing a `Scope`. A `Scope` ensures that any resources your test acquires are managed properly, meaning they will be released when the test completes. This helps prevent resource leaks and guarantees test isolation. +Both `it.effect` and `it.live` provide a fresh `Scope` and close it after each test. Test bodies can therefore use scoped resources directly. Do not wrap the test body in `Effect.scoped`, because the test runner already manages its scope. -**Example** (Using `it.scoped` to Manage Resource Lifecycle) +**Example** (Managing a Resource Lifecycle) ```ts import { it } from "@effect/vitest" @@ -273,20 +271,13 @@ const release = Console.log("release resource") // Defining a resource that requires proper management const resource = Effect.acquireRelease(acquire, () => release) -// Incorrect usage: This will result in a type error because it lacks a scope it.effect("run with scope", () => Effect.gen(function*() { yield* resource })) - -// Correct usage: Using 'it.scoped' to manage the scope correctly -it.scoped("run with scope", () => - Effect.gen(function*() { - yield* resource - })) ``` -# Writing Tests with `it.flakyTest` +## Writing Tests with `it.flakyTest` `it.flakyTest` is a utility designed to manage tests that may not succeed consistently on the first attempt. These tests, often referred to as "flaky," can fail due to factors like timing issues, external dependencies, or randomness. `it.flakyTest` allows for retrying these tests until they pass or a specified timeout is reached. diff --git a/.context/effect/packages/vitest/docgen.json b/.context/effect/packages/vitest/docgen.json deleted file mode 100644 index fbb3f7389..000000000 --- a/.context/effect/packages/vitest/docgen.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "../../node_modules/@effect/docgen/schema.json", - "srcLink": "https://github.com/Effect-TS/effect/tree/main/packages/vitest/src/", - "exclude": ["src/internal/**/*.ts"], - "examplesCompilerOptions": { - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "moduleResolution": "Bundler", - "module": "ES2022", - "target": "ES2022", - "lib": ["ES2022", "DOM"], - "rewriteRelativeImportExtensions": true, - "allowImportingTsExtensions": true, - "paths": { - "effect": ["../../../effect/src/index.js"], - "effect/*": ["../../../effect/src/*.js"] - }, - "plugins": [ - { "name": "@effect/language-service", "includeSuggestionsInTsc": false } - ] - } -} diff --git a/.context/effect/packages/vitest/package.json b/.context/effect/packages/vitest/package.json index 2152f31c4..5ae0df96b 100644 --- a/.context/effect/packages/vitest/package.json +++ b/.context/effect/packages/vitest/package.json @@ -1,6 +1,6 @@ { "name": "@effect/vitest", - "version": "4.0.0-beta.101", + "version": "4.0.0-rc.108", "type": "module", "license": "MIT", "description": "A set of helpers for testing Effects with vitest", @@ -19,6 +19,7 @@ ".": "./src/index.ts", "./*": "./src/*.ts", "./internal/*": null, + "./index": null, "./*/index": null }, "files": [ @@ -26,7 +27,10 @@ "dist/**/*.js", "dist/**/*.js.map", "dist/**/*.d.ts", - "dist/**/*.d.ts.map" + "dist/**/*.d.ts.map", + "AGENTS.md", + "CLAUDE.md", + "ai-docs/**/*" ], "publishConfig": { "access": "public", @@ -36,23 +40,21 @@ ".": "./dist/index.js", "./*": "./dist/*.js", "./internal/*": null, + "./index": null, "./*/index": null } }, "scripts": { "build": "tsc -b tsconfig.json && pnpm babel", "babel": "babel dist --plugins annotate-pure-calls --out-dir dist --source-maps", - "check": "tsc -b tsconfig.json", - "test": "vitest", - "coverage": "vitest --coverage" + "check": "tsc -b tsconfig.json" }, "peerDependencies": { "effect": "workspace:^", - "vitest": "^3.0.0 || ^4.0.0" + "vitest": ">=4.1.0 <5.0.0" }, "devDependencies": { - "@types/node": "^26.1.1", - "@vitest/runner": "4.1.10", + "@types/node": "^26.1.2", "effect": "workspace:^", "vitest": "4.1.10" } diff --git a/.context/effect/packages/vitest/src/index.ts b/.context/effect/packages/vitest/src/index.ts index 37886fb6c..b52478662 100644 --- a/.context/effect/packages/vitest/src/index.ts +++ b/.context/effect/packages/vitest/src/index.ts @@ -180,14 +180,14 @@ export const live: Vitest.Tester = internal.live * @since 4.0.0 * * ```ts - * import { expect, layer } from "@effect/vitest" + * import { assert, layer } from "@effect/vitest" * import { Effect, Layer, Context } from "effect" * - * class Foo extends Context.Service("Foo")() { + * class Foo extends Context.Service()("Foo") { * static Live = Layer.succeed(Foo, "foo") * } * - * class Bar extends Context.Service("Bar")() { + * class Bar extends Context.Service()("Bar") { * static Live = Layer.effect( * Bar, * Effect.map(Foo, () => "bar" as const) @@ -198,7 +198,7 @@ export const live: Vitest.Tester = internal.live * it.effect("adds context", () => * Effect.gen(function*() { * const foo = yield* Foo - * expect(foo).toEqual("foo") + * assert.strictEqual(foo, "foo") * })) * * it.layer(Bar.Live)("nested", (it) => { @@ -206,8 +206,8 @@ export const live: Vitest.Tester = internal.live * Effect.gen(function*() { * const foo = yield* Foo * const bar = yield* Bar - * expect(foo).toEqual("foo") - * expect(bar).toEqual("bar") + * assert.strictEqual(foo, "foo") + * assert.strictEqual(bar, "bar") * })) * }) * }) diff --git a/.context/effect/packages/vitest/src/internal/internal.ts b/.context/effect/packages/vitest/src/internal/internal.ts index f4ad9bcda..f7717b0a6 100644 --- a/.context/effect/packages/vitest/src/internal/internal.ts +++ b/.context/effect/packages/vitest/src/internal/internal.ts @@ -2,7 +2,6 @@ * @since 4.0.0 */ -import { getCurrentSuite } from "@vitest/runner" import * as Cause from "effect/Cause" import * as Duration from "effect/Duration" import * as Effect from "effect/Effect" @@ -10,6 +9,7 @@ import * as Exit from "effect/Exit" import { flow, pipe } from "effect/Function" import * as Layer from "effect/Layer" import { isObject } from "effect/Predicate" +import * as Rec from "effect/Record" import * as Schedule from "effect/Schedule" import * as Schema from "effect/Schema" import * as Scope from "effect/Scope" @@ -19,6 +19,8 @@ import * as TestConsole from "effect/testing/TestConsole" import * as V from "vitest" import type * as Vitest from "../index.ts" +const getCurrentSuite = V.TestRunner.getCurrentSuite + const runPromise: ( _: Effect.Effect, ctx?: V.TestContext | undefined @@ -61,7 +63,7 @@ const makeItProxy = ( return Reflect.apply(target, thisArg, argArray) }, get(target, property, receiver) { - if (property in overrides) { + if (Object.hasOwn(overrides, property)) { return Reflect.get(overrides, property) } // do not bind: binding would strip vitest's static helpers (e.g. `describe.each`) @@ -127,7 +129,7 @@ const makeTester = ( if (Array.isArray(arbitraries)) { const arbs = arbitraries.map((arbitrary) => { if (Schema.isSchema(arbitrary)) { - return Schema.toArbitrary(arbitrary) + return Schema.toArbitrary(arbitrary)(fc) } return arbitrary as fc.Arbitrary }) @@ -148,7 +150,7 @@ const makeTester = ( const arbs = fc.record( Object.keys(arbitraries).reduce(function(result, key) { const arb: any = arbitraries[key] - result[key] = Schema.isSchema(arb) ? Schema.toArbitrary(arb) : arb + Rec.assignProperty(result, key, Schema.isSchema(arb) ? Schema.toArbitrary(arb)(fc) : arb) return result }, {} as Record>) ) @@ -194,7 +196,7 @@ export const prop: Vitest.Vitest.Methods["prop"] = (name, arbitraries, self, tim if (Schema.isSchema(arb)) { throw new Error("Schemas are not supported yet") } - result[key] = arb + Rec.assignProperty(result, key, arb) return result }, {} as Record>) ) diff --git a/.context/effect/packages/vitest/src/utils.ts b/.context/effect/packages/vitest/src/utils.ts index 09c824139..4a4612cd2 100644 --- a/.context/effect/packages/vitest/src/utils.ts +++ b/.context/effect/packages/vitest/src/utils.ts @@ -59,7 +59,11 @@ export function notDeepStrictEqual(actual: A, expected: A, message?: string, * @since 4.0.0 */ export function strictEqual(actual: A, expected: A, message?: string, ..._: Array) { - assert.strictEqual(actual, expected, message as string) + if (message !== undefined) { + assert.strictEqual(actual, expected, message) + } else { + assert.strictEqual(actual, expected) + } } /** @@ -159,7 +163,6 @@ export function assertMatch(actual: string, regExp: RegExp, ..._: Array) 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)) { @@ -170,7 +173,9 @@ export function throws(thunk: () => void, error?: Error | ((u: unknown) => undef throw e } } + return } + fail("Expected to throw an error") } /** @@ -186,7 +191,6 @@ export async function throwsAsync( ) { try { await thunk() - fail("Expected to throw an error") } catch (e) { if (error !== undefined) { if (Predicate.isFunction(error)) { @@ -195,7 +199,9 @@ export async function throwsAsync( deepStrictEqual(e, error) } } + return } + fail("Expected to throw an error") } // ---------------------------- diff --git a/.context/effect/packages/vitest/test/index.test.ts b/.context/effect/packages/vitest/test/index.test.ts index 93ac28b9a..49446dcca 100644 --- a/.context/effect/packages/vitest/test/index.test.ts +++ b/.context/effect/packages/vitest/test/index.test.ts @@ -1,4 +1,5 @@ import { afterAll, assert, describe, expect, it, layer } from "@effect/vitest" +import * as testAssert from "@effect/vitest/utils" import { Clock, Context, Duration, Effect, Fiber, Layer, Schema } from "effect" import { FastCheck, TestClock } from "effect/testing" @@ -11,6 +12,14 @@ it.live( () => Effect.acquireRelease(Effect.sync(() => expect(1).toEqual(1)), () => Effect.void) ) +it("throws fails when the thunk does not throw", () => { + expect(() => testAssert.throws(() => {})).toThrow() +}) + +it("throwsAsync fails when the promise resolves", async () => { + await expect(testAssert.throwsAsync(() => Promise.resolve())).rejects.toThrow() +}) + // each it.effect.each([1, 2, 3])( diff --git a/.context/effect/packages/vitest/tsconfig.json b/.context/effect/packages/vitest/tsconfig.json index be73d74c7..f1ef563eb 100644 --- a/.context/effect/packages/vitest/tsconfig.json +++ b/.context/effect/packages/vitest/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../../tsconfig.base.json", "include": ["src"], "references": [ diff --git a/.context/effect/packages/vitest/vitest.config.ts b/.context/effect/packages/vitest/vitest.config.ts deleted file mode 100644 index fb966ae87..000000000 --- a/.context/effect/packages/vitest/vitest.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { mergeConfig, type ViteUserConfig } from "vitest/config" -import shared from "../../vitest.shared.ts" - -const config: ViteUserConfig = {} - -export default mergeConfig(shared, config) diff --git a/.context/effect/patches/@changesets__assemble-release-plan.patch b/.context/effect/patches/@changesets__assemble-release-plan.patch deleted file mode 100644 index 4967232db..000000000 --- a/.context/effect/patches/@changesets__assemble-release-plan.patch +++ /dev/null @@ -1,26 +0,0 @@ -diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js -index e07ba6e793021b6cfdec898afca517e293386ddb..88d80a95fbe739996918ef4883601b4388926123 100644 ---- a/dist/changesets-assemble-release-plan.cjs.js -+++ b/dist/changesets-assemble-release-plan.cjs.js -@@ -215,7 +215,7 @@ function determineDependents({ - preInfo, - onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange - })) { -- type = "major"; -+ type = "minor"; - } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange))) { - switch (depType) { - case "dependencies": -diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js -index ea2be567403c4ef94a65f3218ccb683cf5cb4bc1..b62b66628d8887618b02ee35359faf70cbe685ad 100644 ---- a/dist/changesets-assemble-release-plan.esm.js -+++ b/dist/changesets-assemble-release-plan.esm.js -@@ -204,7 +204,7 @@ function determineDependents({ - preInfo, - onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange - })) { -- type = "major"; -+ type = "minor"; - } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange))) { - switch (depType) { - case "dependencies": diff --git a/.context/effect/patches/@changesets__get-github-info.patch b/.context/effect/patches/@changesets__get-github-info.patch deleted file mode 100644 index 52e437f59..000000000 --- a/.context/effect/patches/@changesets__get-github-info.patch +++ /dev/null @@ -1,44 +0,0 @@ -diff --git a/dist/changesets-get-github-info.cjs.js b/dist/changesets-get-github-info.cjs.js -index 4187b5c54532a1c1c179b60cb314026f67e48323..740a894a5f8ac240a5ce1d3f12d2d77cabc0c2aa 100644 ---- a/dist/changesets-get-github-info.cjs.js -+++ b/dist/changesets-get-github-info.cjs.js -@@ -237,16 +237,13 @@ async function getInfo(request) { - b = new Date(b.mergedAt); - return a > b ? 1 : a < b ? -1 : 0; - })[0] : null; -- if (associatedPullRequest) { -- user = associatedPullRequest.author; -- } - return { - user: user ? user.login : null, - pull: associatedPullRequest ? associatedPullRequest.number : null, - links: { - commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, - pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, -- user: user ? `[@${user.login}](${user.url})` : null -+ user: user ? `@${user.login}` : null - } - }; - } -diff --git a/dist/changesets-get-github-info.esm.js b/dist/changesets-get-github-info.esm.js -index 071ec75bb2b5894f09c06d4ead395e56ddbfd1c8..9a6134e78ace53ff341ef21f0352f0bd734b6b2c 100644 ---- a/dist/changesets-get-github-info.esm.js -+++ b/dist/changesets-get-github-info.esm.js -@@ -228,16 +228,13 @@ async function getInfo(request) { - b = new Date(b.mergedAt); - return a > b ? 1 : a < b ? -1 : 0; - })[0] : null; -- if (associatedPullRequest) { -- user = associatedPullRequest.author; -- } - return { - user: user ? user.login : null, - pull: associatedPullRequest ? associatedPullRequest.number : null, - links: { - commit: `[\`${request.commit.slice(0, 7)}\`](${data.commitUrl})`, - pull: associatedPullRequest ? `[#${associatedPullRequest.number}](${associatedPullRequest.url})` : null, -- user: user ? `[@${user.login}](${user.url})` : null -+ user: user ? `@${user.login}` : null - } - }; - } diff --git a/.context/effect/patches/@changesets__get-github-info@1.0.0-next.4.patch b/.context/effect/patches/@changesets__get-github-info@1.0.0-next.4.patch new file mode 100644 index 000000000..c7a3e4498 --- /dev/null +++ b/.context/effect/patches/@changesets__get-github-info@1.0.0-next.4.patch @@ -0,0 +1,38 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index 0b8a76abaac754e0a9a5d69ec26e62fc5788aa98..6b8ffafb842aba9b15dde5cc50dc276b6edc3900 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -27,7 +27,14 @@ async function readEnv() { + } + //#endregion + //#region src/dataloader.ts +-const GHDataLoader = new DataLoader(batchLoad, { maxBatchSize: 50 }); ++const GHDataLoader = new DataLoader(batchLoad, { ++ maxBatchSize: 50, ++ cacheKeyFn: (request) => JSON.stringify([ ++ request.repo, ++ request.kind, ++ request.kind === "pull" ? request.pull : request.commit ++ ]) ++}); + async function loadCommitData(options) { + return await GHDataLoader.load({ + ...options, +@@ -166,7 +173,7 @@ async function getCommitInfo(options) { + const bDate = new Date(b.mergedAt); + return aDate.getTime() - bDate.getTime(); + })[0]; +- const author = pr?.author ?? data.author?.user; ++ const author = data.author?.user; + return { + commit: { + sha: options.commit, +@@ -176,7 +183,7 @@ async function getCommitInfo(options) { + author: author ? { + login: author.login, + url: author.url, +- markdownLink: `[@${author.login}](${author.url})` ++ markdownLink: `@${author.login}` + } : void 0, + pull: pr ? { + number: pr.number, diff --git a/.context/effect/patches/@changesets__read@1.0.0-next.10.patch b/.context/effect/patches/@changesets__read@1.0.0-next.10.patch new file mode 100644 index 000000000..60a6ce56a --- /dev/null +++ b/.context/effect/patches/@changesets__read@1.0.0-next.10.patch @@ -0,0 +1,12 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index 012be78..2c23f84 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -30,7 +30,6 @@ async function readChangesets(rootDir, sinceRef) { + } catch (err) { + if (err.code !== "ENOENT") throw err; + } +- console.log(changesets); + if (sinceRef != null) changesets = await filterChangesetsSinceRef(changesets, changesetBase, sinceRef); + changesets = changesets.filter((file) => { + file = path.basename(file); diff --git a/.context/effect/pnpm-lock.yaml b/.context/effect/pnpm-lock.yaml index a42129c20..c7c194d5b 100644 --- a/.context/effect/pnpm-lock.yaml +++ b/.context/effect/pnpm-lock.yaml @@ -5,35 +5,35 @@ settings: excludeLinksFromLockfile: false patchedDependencies: - '@changesets/assemble-release-plan': - hash: 29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e - path: patches/@changesets__assemble-release-plan.patch - '@changesets/get-github-info': - hash: 314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9 - path: patches/@changesets__get-github-info.patch + '@changesets/get-github-info@1.0.0-next.4': + hash: a5b1907668397f36aab989954aa78993792f578b83e41a164ab7ecd573ac9166 + path: patches/@changesets__get-github-info@1.0.0-next.4.patch + '@changesets/read@1.0.0-next.10': + hash: 00298a7e3295e97aab2f89a36af496eef21b576642bd2eac88ae13ceca78cccb + path: patches/@changesets__read@1.0.0-next.10.patch importers: .: devDependencies: '@babel/cli': - specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + specifier: ^8.0.4 + version: 8.0.4(@babel/core@8.0.1) '@babel/core': - specifier: ^7.29.7 - version: 7.29.7 + specifier: ^8.0.1 + version: 8.0.1 '@babel/plugin-transform-export-namespace-from': - specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + specifier: ^8.0.1 + version: 8.0.1(@babel/core@8.0.1) '@babel/plugin-transform-modules-commonjs': - specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + specifier: ^8.0.1 + version: 8.0.1(@babel/core@8.0.1) '@changesets/changelog-github': - specifier: ^0.7.0 - version: 0.7.0 + specifier: 1.0.0-next.9 + version: 1.0.0-next.9 '@changesets/cli': - specifier: ^2.31.1 - version: 2.31.1(@types/node@25.9.5) + specifier: 3.0.0-next.11 + version: 3.0.0-next.11 '@effect/ai-docgen': specifier: workspace:^ version: link:packages/tools/ai-docgen @@ -41,8 +41,11 @@ importers: specifier: workspace:^ version: link:packages/tools/bundle '@effect/docgen': - specifier: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5 - version: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5(tsx@4.21.0)(typescript@7.0.2) + specifier: workspace:^ + version: link:packages/tools/docgen + '@effect/doctest': + specifier: workspace:^ + version: link:packages/tools/doctest '@effect/jsdocs': specifier: workspace:^ version: link:packages/tools/jsdocs @@ -63,37 +66,37 @@ importers: version: 10.5.0 '@rollup/plugin-node-resolve': specifier: ^16.0.3 - version: 16.0.3(rollup@4.62.2) + version: 16.0.3(rollup@4.62.3) '@rollup/plugin-replace': specifier: ^6.0.3 - version: 6.0.3(rollup@4.62.2) + version: 6.0.3(rollup@4.62.3) '@rollup/plugin-terser': specifier: ^1.0.0 - version: 1.0.0(rollup@4.62.2) + version: 1.0.0(rollup@4.62.3) '@types/jscodeshift': specifier: ^17.3.0 version: 17.3.0 '@types/node': - specifier: ^25.9.5 - version: 25.9.5 + specifier: ^26.1.2 + version: 26.1.2 '@vitest/browser': - specifier: 4.1.10 - version: 4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + specifier: ^4.1.10 + version: 4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': - specifier: 4.1.10 + specifier: ^4.1.10 version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) '@vitest/expect': - specifier: 4.1.10 + specifier: ^4.1.10 version: 4.1.10 '@vitest/web-worker': - specifier: 4.1.10 + specifier: ^4.1.10 version: 4.1.10(vitest@4.1.10) ast-types: specifier: ^0.14.2 version: 0.14.2 babel-plugin-annotate-pure-calls: specifier: ^0.5.0 - version: 0.5.0(@babel/core@7.29.7) + version: 0.5.0(@babel/core@8.0.1) dprint: specifier: ^0.55.2 version: 0.55.2 @@ -113,23 +116,26 @@ importers: specifier: ^8.0.0 version: 8.0.0(typescript@7.0.2) oxlint: - specifier: 1.42.0 - version: 1.42.0 + specifier: ^1.76.0 + version: 1.76.0 + pkg-pr-new: + specifier: 0.0.78 + version: 0.0.78 playwright: - specifier: ^1.61.1 - version: 1.61.1 + specifier: ^1.62.0 + version: 1.62.0 rollup: - specifier: ^4.62.2 - version: 4.62.2 + specifier: ^4.62.3 + version: 4.62.3 rollup-plugin-bundle-stats: specifier: ^4.22.2 - version: 4.22.2(core-js@3.47.0)(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.22.2(core-js@3.47.0)(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) rollup-plugin-esbuild: specifier: ^6.2.1 - version: 6.2.1(esbuild@0.28.1)(rollup@4.62.2) + version: 6.2.1(esbuild@0.28.1)(rollup@4.62.3) rollup-plugin-visualizer: specifier: ^7.0.1 - version: 7.0.1(rolldown@1.1.5)(rollup@4.62.2) + version: 7.0.1(rolldown@1.1.5)(rollup@4.62.3) terser: specifier: ^5.49.0 version: 5.49.0 @@ -140,17 +146,14 @@ importers: specifier: ^7.0.2 version: 7.0.2 vite: - specifier: ^7.3.6 - version: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - vite-tsconfig-paths: - specifier: ^6.1.1 - version: 6.1.1(typescript@7.0.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + specifier: ^8.1.5 + version: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vitest: - specifier: 4.1.10 - version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + specifier: ^4.1.10 + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) vitest-websocket-mock: - specifier: ^0.5.0 - version: 0.5.0(vitest@4.1.10) + specifier: ^0.7.0 + version: 0.7.0(vitest@4.1.10) zod: specifier: ^4.4.3 version: 4.4.3 @@ -183,10 +186,10 @@ importers: version: link:../packages/opentelemetry '@effect/platform-bun': specifier: workspace:* - version: link:../packages/platform-bun + version: link:../packages/platform/bun '@effect/platform-node': specifier: workspace:* - version: link:../packages/platform-node + version: link:../packages/platform/node '@effect/sql-clickhouse': specifier: workspace:* version: link:../packages/sql/clickhouse @@ -267,8 +270,8 @@ importers: specifier: ^10.4.1 version: 10.4.1 '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -285,13 +288,13 @@ importers: specifier: workspace:^ version: link:../../effect jsdom: - specifier: ^29.1.1 - version: 29.1.1 + specifier: ^30.0.0 + version: 30.0.0 react: - specifier: 19.2.7 + specifier: ^19.2.7 version: 19.2.7 react-dom: - specifier: 19.2.7 + specifier: ^19.2.7 version: 19.2.7(react@19.2.7) react-error-boundary: specifier: ^6.1.2 @@ -309,14 +312,14 @@ importers: specifier: ^10.4.1 version: 10.4.1 '@testing-library/jest-dom': - specifier: ^6.9.1 - version: 6.9.1 + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) effect: specifier: workspace:^ version: link:../../effect jsdom: - specifier: ^29.1.1 - version: 29.1.1 + specifier: ^30.0.0 + version: 30.0.0 solid-js: specifier: ^1.9.14 version: 1.9.14 @@ -338,43 +341,25 @@ importers: fast-check: specifier: ^4.9.0 version: 4.9.0 - find-my-way-ts: - specifier: ^0.1.6 - version: 0.1.6 - ini: - specifier: ^7.0.0 - version: 7.0.0 kubernetes-types: specifier: ^1.30.0 version: 1.30.0 msgpackr: specifier: ^2.0.4 version: 2.0.4 - multipasta: - specifier: ^0.2.8 - version: 0.2.8 - toml: - specifier: ^4.1.2 - version: 4.1.2 uuid: specifier: ^14.0.1 version: 14.0.1 - yaml: - specifier: ^2.9.0 - version: 2.9.0 devDependencies: - '@types/ini': - specifier: ^4.1.1 - version: 4.1.1 '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 ajv: specifier: ^8.20.0 version: 8.20.0 - arktype: - specifier: ^2.2.3 - version: 2.2.3 + ajv-draft-04: + specifier: ^1.0.0 + version: 1.0.0(ajv@8.20.0) ast-types: specifier: ^0.14.2 version: 0.14.2 @@ -437,15 +422,11 @@ importers: specifier: ^1.42.0 version: 1.42.0 - packages/platform-browser: - dependencies: - multipasta: - specifier: ^0.2.8 - version: 0.2.8 + packages/platform/browser: devDependencies: effect: specifier: workspace:^ - version: link:../effect + version: link:../../effect fake-indexeddb: specifier: ^6.2.5 version: 6.2.5 @@ -453,26 +434,60 @@ importers: specifier: ^8.4.1 version: 8.4.1 - packages/platform-bun: + packages/platform/bun: dependencies: '@effect/platform-node-shared': specifier: workspace:^ - version: link:../platform-node-shared + version: link:../node-shared devDependencies: '@types/bun': specifier: ^1.3.14 version: 1.3.14 effect: specifier: workspace:^ - version: link:../effect + version: link:../../effect + + packages/platform/deno: + dependencies: + '@db/redis': + specifier: jsr:^0.41.2 + version: '@jsr/db__redis@0.41.2' + '@effect/platform-node-shared': + specifier: workspace:^ + version: link:../node-shared + '@std/fs': + specifier: jsr:^1.0.24 + version: '@jsr/std__fs@1.0.24' + '@std/media-types': + specifier: jsr:^1.1.0 + version: '@jsr/std__media-types@1.1.0' + '@std/path': + specifier: jsr:^1.1.6 + version: '@jsr/std__path@1.1.6' + '@std/streams': + specifier: jsr:^1.1.1 + version: '@jsr/std__streams@1.1.1' + devDependencies: + '@std/assert': + specifier: jsr:^1.0.16 + version: '@jsr/std__assert@1.0.19' + '@testcontainers/redis': + specifier: ^12.0.4 + version: 12.0.4 + '@types/deno': + specifier: ^2.7.0 + version: 2.7.0 + effect: + specifier: workspace:^ + version: link:../../effect - packages/platform-node: + packages/platform/node: dependencies: '@effect/platform-node-shared': specifier: workspace:^ - version: link:../platform-node-shared + version: link:../node-shared ioredis: - specifier: ^5.7.0 + specifier: '>=5.7.0 <6.0.0' version: 5.8.2 mime: specifier: ^4.1.0 @@ -482,22 +497,22 @@ importers: version: 8.7.0 devDependencies: '@testcontainers/mysql': - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 '@testcontainers/postgresql': - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 '@testcontainers/redis': - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 effect: specifier: workspace:^ - version: link:../effect + version: link:../../effect - packages/platform-node-shared: + packages/platform/node-shared: dependencies: '@types/ws': specifier: ^8.18.1 @@ -507,11 +522,11 @@ importers: version: 8.21.0 devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 effect: specifier: workspace:^ - version: link:../effect + version: link:../../effect tar: specifier: ^7.5.19 version: 7.5.19 @@ -524,7 +539,7 @@ importers: devDependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node effect: specifier: workspace:^ version: link:../../effect @@ -552,8 +567,8 @@ importers: specifier: workspace:^ version: link:../../effect testcontainers: - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 packages/sql/mssql: dependencies: @@ -561,6 +576,9 @@ importers: specifier: ^19.2.1 version: 19.2.1(@azure/core-client@1.10.1) devDependencies: + '@testcontainers/mssqlserver': + specifier: ^12.0.4 + version: 12.0.4 effect: specifier: workspace:^ version: link:../../effect @@ -569,11 +587,11 @@ importers: dependencies: mysql2: specifier: ^3.22.6 - version: 3.22.6(@types/node@25.9.5) + version: 3.22.6(@types/node@26.1.2) devDependencies: '@testcontainers/mysql': - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 effect: specifier: workspace:^ version: link:../../effect @@ -584,7 +602,7 @@ importers: specifier: ^8.22.0 version: 8.22.0 pg-connection-string: - specifier: 2.14.0 + specifier: ^2.14.0 version: 2.14.0 pg-cursor: specifier: ^2.21.0 @@ -597,8 +615,8 @@ importers: version: 4.1.0 devDependencies: '@testcontainers/postgresql': - specifier: ^11.14.0 - version: 11.14.0 + specifier: ^12.0.4 + version: 12.0.4 '@types/pg': specifier: ^8.20.0 version: 8.20.0 @@ -623,7 +641,7 @@ importers: devDependencies: '@effect/platform-bun': specifier: workspace:^ - version: link:../../platform-bun + version: link:../../platform/bun '@types/bun': specifier: ^1.3.14 version: 1.3.14 @@ -644,10 +662,10 @@ importers: devDependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 effect: specifier: workspace:^ version: link:../../effect @@ -655,8 +673,8 @@ importers: packages/sql/sqlite-react-native: devDependencies: '@op-engineering/op-sqlite': - specifier: 17.1.2 - version: 17.1.2(react-native@0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) + specifier: ^17.1.2 + version: 17.1.2(react-native@0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) effect: specifier: workspace:^ version: link:../../effect @@ -677,7 +695,7 @@ importers: version: link:../openapi-generator '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node effect: specifier: workspace:^ version: link:../../effect @@ -689,14 +707,14 @@ importers: version: 2.9.0 devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 packages/tools/ai-docgen: dependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node effect: specifier: workspace:^ version: link:../../effect @@ -708,23 +726,51 @@ importers: version: 2.9.0 devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 + + packages/tools/api-diff: + dependencies: + '@effect/platform-node': + specifier: workspace:^ + version: link:../../platform/node + effect: + specifier: workspace:^ + version: link:../../effect + yaml: + specifier: ^2.9.0 + version: 2.9.0 + devDependencies: + '@effect/vitest': + specifier: workspace:^ + version: link:../../vitest + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + typescript-compiler: + specifier: npm:typescript@6.0.3 + version: typescript@6.0.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/tools/bundle: dependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node '@rollup/plugin-node-resolve': specifier: ^16.0.3 - version: 16.0.3(rollup@4.62.2) + version: 16.0.3(rollup@4.62.3) '@rollup/plugin-replace': specifier: ^6.0.3 - version: 6.0.3(rollup@4.62.2) + version: 6.0.3(rollup@4.62.3) '@rollup/plugin-terser': specifier: ^1.0.0 - version: 1.0.0(rollup@4.62.2) + version: 1.0.0(rollup@4.62.3) effect: specifier: workspace:^ version: link:../../effect @@ -732,18 +778,95 @@ importers: specifier: ^13.0.6 version: 13.0.6 rollup: - specifier: ^4.62.2 - version: 4.62.2 + specifier: ^4.62.3 + version: 4.62.3 rollup-plugin-esbuild: specifier: ^6.2.1 - version: 6.2.1(esbuild@0.28.1)(rollup@4.62.2) + version: 6.2.1(esbuild@0.28.1)(rollup@4.62.3) rollup-plugin-visualizer: specifier: ^7.0.1 - version: 7.0.1(rolldown@1.1.5)(rollup@4.62.2) + version: 7.0.1(rolldown@1.1.5)(rollup@4.62.3) + devDependencies: + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 + + packages/tools/docgen: + dependencies: + '@babel/code-frame': + specifier: ^8.0.0 + version: 8.0.0 + '@effect/markdown-toc': + specifier: ^0.1.0 + version: 0.1.0 + '@effect/platform-node': + specifier: workspace:^ + version: link:../../platform/node + chalk: + specifier: ^5.6.2 + version: 5.6.2 + doctrine: + specifier: ^3.0.0 + version: 3.0.0 + effect: + specifier: workspace:^ + version: link:../../effect + glob: + specifier: ^13.0.6 + version: 13.0.6 + prettier: + specifier: ^3.6.2 + version: 3.9.6 + ts-morph: + specifier: ^27.0.2 + version: 27.0.2 + tsconfck: + specifier: ^3.1.6 + version: 3.1.6(typescript@6.0.3) + devDependencies: + '@effect/vitest': + specifier: workspace:^ + version: link:../../vitest + '@types/babel__code-frame': + specifier: ^7.0.6 + version: 7.27.0 + '@types/doctrine': + specifier: ^0.0.9 + version: 0.0.9 + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 + tsx: + specifier: ^4.23.1 + version: 4.23.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + + packages/tools/doctest: + dependencies: + rolldown: + specifier: ^1.1.5 + version: 1.1.5 devDependencies: + '@effect/vitest': + specifier: workspace:^ + version: link:../../vitest '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 + effect: + specifier: workspace:^ + version: link:../../effect + vite: + specifier: ^8.1.5 + version: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/tools/jsdocs: dependencies: @@ -758,20 +881,20 @@ importers: specifier: workspace:^ version: link:../../vitest '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/tools/openapi-generator: dependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node swagger2openapi: specifier: ^7.0.8 version: 7.0.8 @@ -794,25 +917,25 @@ importers: packages/tools/oxc: dependencies: - '@effect/jsdocs': - specifier: workspace:^ - version: link:../jsdocs + '@oxlint/plugins': + specifier: ^1.76.0 + version: 1.76.0 devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 typescript: specifier: ^6.0.3 version: 6.0.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/tools/utils: dependencies: '@effect/platform-node': specifier: workspace:^ - version: link:../../platform-node + version: link:../../platform/node effect: specifier: workspace:^ version: link:../../effect @@ -821,23 +944,20 @@ importers: version: 13.0.6 devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 + specifier: ^26.1.2 + version: 26.1.2 packages/vitest: devDependencies: '@types/node': - specifier: ^26.1.1 - version: 26.1.1 - '@vitest/runner': - specifier: 4.1.10 - version: 4.1.10 + specifier: ^26.1.2 + version: 26.1.2 effect: specifier: workspace:^ version: link:../effect vitest: specifier: 4.1.10 - version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) scratchpad: dependencies: @@ -858,10 +978,10 @@ importers: version: link:../packages/opentelemetry '@effect/platform-bun': specifier: workspace:* - version: link:../packages/platform-bun + version: link:../packages/platform/bun '@effect/platform-node': specifier: workspace:* - version: link:../packages/platform-node + version: link:../packages/platform/node '@effect/sql-clickhouse': specifier: workspace:* version: link:../packages/sql/clickhouse @@ -906,10 +1026,10 @@ importers: dependencies: '@effect/platform-bun': specifier: workspace:* - version: link:../packages/platform-bun + version: link:../packages/platform/bun '@effect/platform-node': specifier: workspace:* - version: link:../packages/platform-node + version: link:../packages/platform/node '@effect/sql-clickhouse': specifier: workspace:* version: link:../packages/sql/clickhouse @@ -949,29 +1069,16 @@ importers: packages: - '@adobe/css-tools@4.4.4': - resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} - - '@ark/schema@0.56.2': - resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} - '@ark/util@0.56.2': - resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} + '@asamuzakjp/css-color@6.0.5': + resolution: {integrity: sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==} + engines: {node: ^22.13.0 || >=24.0.0} - '@asamuzakjp/css-color@5.1.11': - resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@7.1.1': - resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/generational-cache@1.0.1': - resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@asamuzakjp/dom-selector@8.3.0': + resolution: {integrity: sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==} + engines: {node: ^22.13.0 || >=24.0.0} '@azure-rest/core-client@2.6.0': resolution: {integrity: sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==} @@ -1044,12 +1151,12 @@ packages: resolution: {integrity: sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A==} engines: {node: '>=20'} - '@babel/cli@7.29.7': - resolution: {integrity: sha512-/75HwRbAYPqXv/Ax1h7Fg3IZfXgdU98jnA8H93/m/QBaPV3Hp5ICoLqzGYye1yHBCgpmXvtqgSUN8oOKX5tojQ==} - engines: {node: '>=6.9.0'} + '@babel/cli@8.0.4': + resolution: {integrity: sha512-mgg9G7dJw7xzx/0Sn8eWQkDpEcQrlDdEV4Y4Ii+8Oay88+lK45vNuSavRUj4g+e5Yfw4tkH4U3ObBFOMJhy4oQ==} + engines: {node: ^22.18.0 || >=24.11.0} hasBin: true peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^8.0.0 '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -1059,18 +1166,34 @@ packages: resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} + '@babel/code-frame@8.0.0': + resolution: {integrity: sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/compat-data@7.29.7': resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@8.0.0': + resolution: {integrity: sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/core@7.29.7': resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/generator@7.29.7': resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0': + resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-annotate-as-pure@7.29.7': resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} engines: {node: '>=6.9.0'} @@ -1079,6 +1202,10 @@ packages: resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@8.0.0': + resolution: {integrity: sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-create-class-features-plugin@7.29.7': resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} @@ -1089,6 +1216,10 @@ packages: resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@8.0.0': + resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-member-expression-to-functions@7.29.7': resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} @@ -1097,12 +1228,22 @@ packages: resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@8.0.0': + resolution: {integrity: sha512-NZ7mSS93o4ndX4KrbD7W8Sf3QT8Qe24PrnFyUcuOPDzK6faqDFKjY9RG7he7+I7FdiQ4llpnosFqzrXa+Vy3Ew==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-module-transforms@7.29.7': resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@8.0.1': + resolution: {integrity: sha512-UgAhl1kqiW5ciE0yCXqqvnb4H2n3IELJ7lIIQRezwDPilPEZX5i+Rvbja9MFTkwUn2biEiSMeV31aUzR4Lwakw==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/helper-optimise-call-expression@7.29.7': resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} engines: {node: '>=6.9.0'} @@ -1111,6 +1252,12 @@ packages: resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@8.0.1': + resolution: {integrity: sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/helper-replace-supers@7.29.7': resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} @@ -1125,27 +1272,44 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@8.0.4': + resolution: {integrity: sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-option@7.29.7': resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helpers@7.29.7': resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} + '@babel/helpers@8.0.0': + resolution: {integrity: sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.4': + resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} peerDependencies: @@ -1249,11 +1413,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-export-namespace-from@7.29.7': - resolution: {integrity: sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==} - engines: {node: '>=6.9.0'} + '@babel/plugin-transform-export-namespace-from@8.0.1': + resolution: {integrity: sha512-bFzznm46bvWGaTYKle3iolbBJ+oPBfUjwCPesxlFE3SQ7DaY9EHf/8Y5ZzrodKJi8JDdcAyaVWaDUSVyhULh0g==} + engines: {node: ^22.18.0 || >=24.11.0} peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/core': ^8.0.0 '@babel/plugin-transform-flow-strip-types@7.29.7': resolution: {integrity: sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==} @@ -1267,6 +1431,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@8.0.1': + resolution: {integrity: sha512-PMuzulWrrzFNmY3lXSk/tV9NRb7y0eZZLJY4UEo2TKszroxvUZHAPPi+T9FDyrQhod+TQA+t+8/QYaaMpiEuhA==} + engines: {node: ^22.18.0 || >=24.11.0} + peerDependencies: + '@babel/core': ^8.0.0 + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7': resolution: {integrity: sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==} engines: {node: '>=6.9.0'} @@ -1321,14 +1491,26 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/traverse@7.29.7': resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} + '@babel/traverse@8.0.4': + resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/types@7.29.7': resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@babel/types@8.0.4': + resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} @@ -1367,66 +1549,82 @@ packages: core-js: ^3.0.0 lodash: ^4.0.0 - '@changesets/apply-release-plan@7.1.1': - resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + '@changesets/apply-release-plan@8.0.0-next.10': + resolution: {integrity: sha512-Yps335/MoZe8nKMJ8Jt4CCZ4N9zFF+5q0INfmcCuDJebrB/cvCfJMJLMVQ/Pz4lFY7fWgCVFSsvRFHiT23G08g==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/assemble-release-plan@6.0.10': - resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} + '@changesets/assemble-release-plan@7.0.0-next.10': + resolution: {integrity: sha512-zuC3jl9KQi2dc0WeT1mpwPDfk6Y6kswx7sER65YN53AtWY0PJCSa0t9zDMLKfTO0yYeqrk40Z/AEOwum9jA/vg==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/changelog-git@0.2.1': - resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + '@changesets/changelog-git@1.0.0-next.9': + resolution: {integrity: sha512-v9YatKMVoljRPRPzI9PoxDHxBsLXZs+ndKBts6nOJlRpGHoGyzScOyC2YYce0krYhFxhn+lOiIto/7AOlTFuwA==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/changelog-github@0.7.0': - resolution: {integrity: sha512-rBsbRvc4TVn+FvFnOVM3LxlFJfTXXCp8gfVJ+0BubxWNSVnLuAzowi5j+IEraLLP52w8AAs9QfKbPS3MMiXQJA==} + '@changesets/changelog-github@1.0.0-next.9': + resolution: {integrity: sha512-xGtpDLiJeQdyoOGNfKIKp09+o41J7KTNJ+QXWq8OqrzFbOCuI+KZC1AlyIDAsjZce3tBYLhu9sQtAMkmRnGOew==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/cli@2.31.1': - resolution: {integrity: sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w==} + '@changesets/cli@3.0.0-next.11': + resolution: {integrity: sha512-yH79OtoLPruqx0BTO0eKE1rxupgk7Qf1SzmVhwdDqikkOoFlPD5/QEAN3zvRXLoGcT0VN0Wuq2zCFTi8CyJgLA==} + engines: {node: ^22.11 || ^24 || >=26, npm: '>=10.9.0', pnpm: '>=10.0.0', yarn: '>=4.5.2'} hasBin: true - '@changesets/config@3.1.4': - resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} - - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + '@changesets/config@4.0.0-next.9': + resolution: {integrity: sha512-V/dN3IQ2gnYguztEQuT6z+IQ6vqcA0KUue0CdjWOjLRHA8dQaMrcKBKZlbNecvm31fGc1qIWu9IyXML1Xf3NUw==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/get-dependents-graph@2.1.4': - resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} + '@changesets/errors@1.0.0-next.4': + resolution: {integrity: sha512-ZUoabMz+a4t0/Eoz5uUHzufcWfXIlkpCuCUWUMkWErqcCjgB0pwuPWSU2CamhufsG7ZKDUlUoIGVSQWyPc77CA==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/get-github-info@0.8.0': - resolution: {integrity: sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ==} + '@changesets/format@0.1.1': + resolution: {integrity: sha512-OBN/xfe+lcNWCv8P4Ogop9EOTi5QRKd1c2JJfZRBT9AT3AybzDYDfHeX4Y7j20Q4e16BPu3XvcdOe9sPZmTEig==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/get-release-plan@4.0.16': - resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} + '@changesets/get-dependents-graph@3.0.0-next.9': + resolution: {integrity: sha512-zyukhblBz0TO7Ze4rJHS/LN8dLvNpKPGEm3YhYRpZnlRCT+r4X9YtrHJjUFHbnjn2wNRRzzy6eHzsFn43YI3jQ==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/get-version-range-type@0.4.0': - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + '@changesets/get-github-info@1.0.0-next.4': + resolution: {integrity: sha512-Bosh+XOoFvLMzAj301tg6phbrEggBtvEccrnceEvYsKSM7PjcIa32Fy22SU5g+501HZywkdwcAlybdWF+e/zzw==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/git@3.0.4': - resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + '@changesets/git@4.0.0-next.9': + resolution: {integrity: sha512-7WqrwEVpsKZKbBGR7hPRuHTbkIMiDdoyxSsQDOWrmuBh13wDTGaIV6yYQoOs0I3l56khxydm/68v01c/+4g9jQ==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/logger@0.1.1': - resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + '@changesets/parse@1.0.0-next.10': + resolution: {integrity: sha512-ENFmw8Ytq6fNY3O+uC99KDdChZ3dM8PFgth3qi1Ms78GCCHsSgik0ojQwGht5VPsP6WhfTkNqW3fCakFt+euHg==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/parse@0.4.3': - resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + '@changesets/pre@3.0.0-next.9': + resolution: {integrity: sha512-NznGjw/PCDy6d74DusPriYbLZQl6O8cvRwuv/qsSKUTF/9O9+EvJnXTx+wtHS9SLJnJqRaGprQ1EMSkjRRyDGQ==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/pre@2.0.2': - resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + '@changesets/read@1.0.0-next.10': + resolution: {integrity: sha512-wmhNbv8gnN6Gv6dpFftmqkyy5OTQfFWFlyfwLa7s+b0vMihA5ZYwU/Zd8VN53AsSlYp6k8s4sc43ZJk0zUIkRA==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/read@0.6.7': - resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + '@changesets/should-skip-package@1.0.0-next.9': + resolution: {integrity: sha512-KaDRCnNy+J4Q8vTZIyKGV3wtDyW79bJN7+XBg919ZKneAj7jJpJLIPkVK+MlyGqyWoxhWEcDBRZ3aTjVkx/tnw==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/should-skip-package@0.1.2': - resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + '@changesets/types@7.0.0-next.9': + resolution: {integrity: sha512-okzQIKr+HZXZne37+QI0o0GrKNNTinP8jqsSdr2jUXEt8qhiwdG/X2o7aukp4WBtG4R395BOGyJVfaCy+xg7IA==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/types@4.1.0': - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + '@changesets/write@1.0.0-next.9': + resolution: {integrity: sha512-vZ7iTrI8FhJydoWla4G9LSk1kJf4yOtA9m3h9Wffvn+0v5DXUe3LrP/7ZBxHlFCu+WY5ypKo+ClLaDvB9I7Zpg==} + engines: {node: ^22.11 || ^24 || >=26} - '@changesets/types@6.1.0': - resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} - '@changesets/write@0.4.0': - resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} '@clickhouse/client@1.23.1': resolution: {integrity: sha512-vs3/Zc1dHvT171btW5nMoPsPCJ6QVJ5pp7obxzO5sjqwFx/jjz9wwCAqcFOdc2DhprugDBaVn+4dVY8hG3A9nw==} @@ -1469,19 +1667,19 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} engines: {node: '>=20.19.0'} - '@csstools/css-calc@3.2.0': - resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.1.0': - resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -1493,8 +1691,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3': - resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -1596,15 +1794,6 @@ packages: resolution: {integrity: sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w==} engines: {node: '>=18'} - '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5': - resolution: {tarball: https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5} - version: 0.5.2 - engines: {node: '>=18.0.0'} - hasBin: true - peerDependencies: - tsx: ^4.19.3 - typescript: ^5.8.2 - '@effect/markdown-toc@0.1.0': resolution: {integrity: sha512-IRfvvwqQLabVTIw9hhIj4scOGIYPfa13QuEFv+dBWE6p47R+RR0J8jQvfDINFf0Vn80XXVjNRtZxkZpkKXLx2A==} engines: {node: '>=0.10.0'} @@ -1661,326 +1850,170 @@ packages: '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@1.11.2': - resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.27.7': - resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.7': - resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.28.1': resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.7': - resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.28.1': resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.7': - resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.28.1': resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.7': - resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.28.1': resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.7': - resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.28.1': resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.7': - resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.28.1': resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.7': - resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.28.1': resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.7': - resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.28.1': resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.7': - resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.28.1': resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.7': - resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.28.1': resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.7': - resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.28.1': resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.7': - resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.28.1': resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.7': - resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.28.1': resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.7': - resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.28.1': resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.7': - resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.28.1': resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.7': - resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.28.1': resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.7': - resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - '@esbuild/netbsd-arm64@0.28.1': resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.7': - resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.28.1': resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.7': - resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - '@esbuild/openbsd-arm64@0.28.1': resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.7': - resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.28.1': resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.7': - resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - '@esbuild/openharmony-arm64@0.28.1': resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.7': - resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.28.1': resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.7': - resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.28.1': resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.7': - resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.28.1': resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.7': - resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.28.1': resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@exodus/bytes@1.15.0': - resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: '@noble/hashes': ^1.8.0 || ^2.0.0 @@ -2146,15 +2179,6 @@ packages: cpu: [x64] os: [win32] - '@inquirer/external-editor@1.0.3': - resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} - engines: {node: '>=18'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@ioredis/commands@1.4.0': resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==} @@ -2162,10 +2186,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/cliui@9.0.0': - resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} - engines: {node: '>=18'} - '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -2238,6 +2258,45 @@ packages: resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==} engines: {node: '>=12'} + '@jsr/db__redis@0.41.2': + resolution: {integrity: sha512-pDHlF3sQaU5+VDwDH3jRkETBSD9gAIqdoaC3j0B4nrffr+H/Kh0Pbw7i/p5eD+9W5j38l8yRqZGbo4xgohYttg==, tarball: https://npm.jsr.io/~/11/@jsr/db__redis/0.41.2.tgz} + + '@jsr/std__assert@1.0.19': + resolution: {integrity: sha512-pEj6RPkGbqlgRmyKwATp4cUs6+ijxtdrv3bq8v1d2I2CEcMEyPaO8cVKro61wGRDH4cNg8Zx6haztvK/9m7gkA==, tarball: https://npm.jsr.io/~/11/@jsr/std__assert/1.0.19.tgz} + + '@jsr/std__async@1.5.0': + resolution: {integrity: sha512-I2Qekl1oQYM+dpI2RQUCKi1TBuaA7Od3LNzxcqls9s1e2ytiZyREL4403qHJ6UbLNAwbqO2ZhdWAWDUQAajyiA==, tarball: https://npm.jsr.io/~/11/@jsr/std__async/1.5.0.tgz} + + '@jsr/std__bytes@1.0.6': + resolution: {integrity: sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==, tarball: https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz} + + '@jsr/std__collections@1.3.0': + resolution: {integrity: sha512-oJ5V5ZvsWyO5VLdGk3TaRHaVibfVKHvDC8KJdgmxIucHN2QzAXlMt36ApyTUBncfrhXdN/5Q3yMNvloAKym8RA==, tarball: https://npm.jsr.io/~/11/@jsr/std__collections/1.3.0.tgz} + + '@jsr/std__data-structures@1.1.1': + resolution: {integrity: sha512-ceWGRdAPq3oD2If4k/JvgL/bskN5VohnM6wINlJhYLz4yCW9cmHFcGsrhRgh9HbD9xhXi9sEQoiHQG5B9caHEg==, tarball: https://npm.jsr.io/~/11/@jsr/std__data-structures/1.1.1.tgz} + + '@jsr/std__fs@1.0.24': + resolution: {integrity: sha512-UunW2Rg++9sA74NJnYMGTsMVQgoFRAwPP02wRRohL0PQBNorbyxHiyCMRaxYQCtLepTLkC4nUyMtcwrGKqTR6g==, tarball: https://npm.jsr.io/~/11/@jsr/std__fs/1.0.24.tgz} + + '@jsr/std__internal@1.0.14': + resolution: {integrity: sha512-JT8b/t40WcR9q0GDwRZUooY7aXeXnFal8iidE/5TckdAFtvpVAsaJ71/Xgf1SfoChs41s6CZkuCYM8toaNgdvA==, tarball: https://npm.jsr.io/~/11/@jsr/std__internal/1.0.14.tgz} + + '@jsr/std__io@0.224.5': + resolution: {integrity: sha512-1Y8ZWIjFiQKkaSJwbt7NM/9esDtO0ieKS4vcT929dFArG2ADk7ZKVAV4KluYYh4+kF5VgkOcvJkEm84KeBpqFA==, tarball: https://npm.jsr.io/~/11/@jsr/std__io/0.224.5.tgz} + + '@jsr/std__media-types@1.1.0': + resolution: {integrity: sha512-dHvaxHL7ENWnltgL653uo3KnKFse3ZbopZop2gqsT7yrscx7irZEClu5Cba7gMPPRk4Lg1FbriNcaBViM2RSBw==, tarball: https://npm.jsr.io/~/11/@jsr/std__media-types/1.1.0.tgz} + + '@jsr/std__path@1.1.6': + resolution: {integrity: sha512-H1Cmg6z8jFyIsQbVV+vZB4eOx4k6Qg8lyUM6l7nZysj5MZumg89kb0M1bsPBTGQis3fw6mJFkJELo5+AK6oCFA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.6.tgz} + + '@jsr/std__random@0.1.0': + resolution: {integrity: sha512-gt+pI83ha04zLv2+5kSf/i6uPT4c8QEwscpATrYe2IKfOkCVpL++NrFcH7pWHc0vgm2fDGC8aY6OI2Mz7tEHhQ==, tarball: https://npm.jsr.io/~/11/@jsr/std__random/0.1.0.tgz} + + '@jsr/std__streams@1.1.1': + resolution: {integrity: sha512-V9auR/i6gJz6SR1+h5wc4EN42IC5+ObBjXu/h7Buef7UOE0GtX4vRSx+3MJywEiBcJqQC+lCnf+wBAodvBL1SA==, tarball: https://npm.jsr.io/~/11/@jsr/std__streams/1.1.1.tgz} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2298,11 +2357,17 @@ packages: cpu: [x64] os: [win32] - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + '@manypkg/find-root@3.1.0': + resolution: {integrity: sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==} + engines: {node: '>=20.0.0'} + + '@manypkg/get-packages@3.1.0': + resolution: {integrity: sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==} + engines: {node: '>=20.0.0'} - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@manypkg/tools@2.1.2': + resolution: {integrity: sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==} + engines: {node: '>=20.0.0'} '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} @@ -2334,30 +2399,16 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + '@napi-rs/wasm-runtime@1.2.0': + resolution: {integrity: sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 + '@emnapi/core': ^2.0.0-alpha.3 + '@emnapi/runtime': ^2.0.0-alpha.3 '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': - resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@op-engineering/op-sqlite@17.1.2': resolution: {integrity: sha512-R1E0MO9dGuOSr5K+Wrwi2k2HrEDbG6iJGwboYl+2hJXpuo01k556GcAj1l7nu4RDPH0wdpM7NL7pieIL5+k3Mw==} peerDependencies: @@ -2467,50 +2518,132 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - '@oxlint/darwin-arm64@1.42.0': - resolution: {integrity: sha512-ui5CdAcDsXPQwZQEXOOSWsilJWhgj9jqHCvYBm2tDE8zfwZZuF9q58+hGKH1x5y0SV4sRlyobB2Quq6uU6EgeA==} + '@oxlint/binding-android-arm-eabi@1.76.0': + resolution: {integrity: sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.76.0': + resolution: {integrity: sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.76.0': + resolution: {integrity: sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/darwin-x64@1.42.0': - resolution: {integrity: sha512-wo0M/hcpHRv7vFje99zHHqheOhVEwUOKjOgBKyi0M99xcLizv04kcSm1rTd6HSCeZgOtiJYZRVAlKhQOQw2byQ==} + '@oxlint/binding-darwin-x64@1.76.0': + resolution: {integrity: sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/linux-arm64-gnu@1.42.0': - resolution: {integrity: sha512-j4QzfCM8ks+OyM+KKYWDiBEQsm5RCW50H1Wz16wUyoFsobJ+X5qqcJxq6HvkE07m8euYmZelyB0WqsiDoz1v8g==} + '@oxlint/binding-freebsd-x64@1.76.0': + resolution: {integrity: sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.76.0': + resolution: {integrity: sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.76.0': + resolution: {integrity: sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.76.0': + resolution: {integrity: sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/linux-arm64-musl@1.42.0': - resolution: {integrity: sha512-g5b1Uw7zo6yw4Ymzyd1etKzAY7xAaGA3scwB8tAp3QzuY7CYdfTwlhiLKSAKbd7T/JBgxOXAGNcLDorJyVTXcg==} + '@oxlint/binding-linux-arm64-musl@1.76.0': + resolution: {integrity: sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/linux-x64-gnu@1.42.0': - resolution: {integrity: sha512-HnD99GD9qAbpV4q9iQil7mXZUJFpoBdDavfcC2CgGLPlawfcV5COzQPNwOgvPVkr7C0cBx6uNCq3S6r9IIiEIg==} + '@oxlint/binding-linux-ppc64-gnu@1.76.0': + resolution: {integrity: sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@oxlint/binding-linux-riscv64-gnu@1.76.0': + resolution: {integrity: sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-riscv64-musl@1.76.0': + resolution: {integrity: sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + + '@oxlint/binding-linux-s390x-gnu@1.76.0': + resolution: {integrity: sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@oxlint/binding-linux-x64-gnu@1.76.0': + resolution: {integrity: sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/linux-x64-musl@1.42.0': - resolution: {integrity: sha512-8NTe8A78HHFn+nBi+8qMwIjgv9oIBh+9zqCPNLH56ah4vKOPvbePLI6NIv9qSkmzrBuu8SB+FJ2TH/G05UzbNA==} + '@oxlint/binding-linux-x64-musl@1.76.0': + resolution: {integrity: sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/win32-arm64@1.42.0': - resolution: {integrity: sha512-lAPS2YAuu+qFqoTNPFcNsxXjwSV0M+dOgAzzVTAN7Yo2ifj+oLOx0GsntWoM78PvQWI7Q827ZxqtU2ImBmDapA==} + '@oxlint/binding-openharmony-arm64@1.76.0': + resolution: {integrity: sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.76.0': + resolution: {integrity: sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/win32-x64@1.42.0': - resolution: {integrity: sha512-3/KmyUOHNriL6rLpaFfm9RJxdhpXY2/Ehx9UuorJr2pUA+lrZL15FAEx/DOszYm5r10hfzj40+efAHcCilNvSQ==} + '@oxlint/binding-win32-ia32-msvc@1.76.0': + resolution: {integrity: sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.76.0': + resolution: {integrity: sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxlint/plugins@1.76.0': + resolution: {integrity: sha512-twmbsVrYAjkaOw6I2pDiYSAxVLNOV8kcqMzajJ599SpqXzea+GjDCZVad1VUqNR/4thWjM4PER5n+3/TQtTkZQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@pnpm/deps.graph-sequencer@1100.0.1': + resolution: {integrity: sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==} + engines: {node: '>=22.13'} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -2744,128 +2877,128 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.62.2': - resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + '@rollup/rollup-android-arm-eabi@4.62.3': + resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.62.2': - resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + '@rollup/rollup-android-arm64@4.62.3': + resolution: {integrity: sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.2': - resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + '@rollup/rollup-darwin-arm64@4.62.3': + resolution: {integrity: sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.2': - resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + '@rollup/rollup-darwin-x64@4.62.3': + resolution: {integrity: sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.2': - resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + '@rollup/rollup-freebsd-arm64@4.62.3': + resolution: {integrity: sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.2': - resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + '@rollup/rollup-freebsd-x64@4.62.3': + resolution: {integrity: sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': - resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': + resolution: {integrity: sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.62.2': - resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + '@rollup/rollup-linux-arm-musleabihf@4.62.3': + resolution: {integrity: sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.62.2': - resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + '@rollup/rollup-linux-arm64-gnu@4.62.3': + resolution: {integrity: sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.62.2': - resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + '@rollup/rollup-linux-arm64-musl@4.62.3': + resolution: {integrity: sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.62.2': - resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + '@rollup/rollup-linux-loong64-gnu@4.62.3': + resolution: {integrity: sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loong64-musl@4.62.2': - resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + '@rollup/rollup-linux-loong64-musl@4.62.3': + resolution: {integrity: sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.62.2': - resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + '@rollup/rollup-linux-ppc64-gnu@4.62.3': + resolution: {integrity: sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-ppc64-musl@4.62.2': - resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + '@rollup/rollup-linux-ppc64-musl@4.62.3': + resolution: {integrity: sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.62.2': - resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + '@rollup/rollup-linux-riscv64-gnu@4.62.3': + resolution: {integrity: sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.62.2': - resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + '@rollup/rollup-linux-riscv64-musl@4.62.3': + resolution: {integrity: sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.62.2': - resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + '@rollup/rollup-linux-s390x-gnu@4.62.3': + resolution: {integrity: sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.62.2': - resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + '@rollup/rollup-linux-x64-gnu@4.62.3': + resolution: {integrity: sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.62.2': - resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + '@rollup/rollup-linux-x64-musl@4.62.3': + resolution: {integrity: sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.62.2': - resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + '@rollup/rollup-openbsd-x64@4.62.3': + resolution: {integrity: sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.62.2': - resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + '@rollup/rollup-openharmony-arm64@4.62.3': + resolution: {integrity: sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.2': - resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + '@rollup/rollup-win32-arm64-msvc@4.62.3': + resolution: {integrity: sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.2': - resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + '@rollup/rollup-win32-ia32-msvc@4.62.3': + resolution: {integrity: sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.2': - resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + '@rollup/rollup-win32-x64-gnu@4.62.3': + resolution: {integrity: sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.2': - resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + '@rollup/rollup-win32-x64-msvc@4.62.3': + resolution: {integrity: sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==} cpu: [x64] os: [win32] @@ -2940,22 +3073,27 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@testcontainers/mysql@11.14.0': - resolution: {integrity: sha512-0/OKd1gOvnl0qS+RIpmT6J0XKWpjkVyIbOtS3cFTVR8t6Ps7W9TV0U+zp7FPxCmitWuWYPXwH/+RQXF+1jZuZQ==} + '@testcontainers/mssqlserver@12.0.4': + resolution: {integrity: sha512-UkxfpOavn4V6yoLQ/uxBPCgc13J8PcWemqTJSe0JCj54ZcYSh/24qjN5eTHzcLsIYSpx/ZXXBP31E2vWKLtIfA==} - '@testcontainers/postgresql@11.14.0': - resolution: {integrity: sha512-wYbJn8GRTj8qfqzfVubxioYWlHJU/ImIjuzPwyy9C5Qfo6g3GLduPZAj+BifvqTZjgT3gd4gFVLCPhBji7dc1w==} + '@testcontainers/mysql@12.0.4': + resolution: {integrity: sha512-XZWh/L6EN8XQa5lsBdESDolDmyUA56bDgYh5SicLzzC2b4jho4lc0yA0GOOVX/bd0zK6VQK6hmDDJE+T3rFrdA==} - '@testcontainers/redis@11.14.0': - resolution: {integrity: sha512-WX005slz2JMQPw2avbSjf5awVjpmFhOs5xCxeGSYLcV5ia4W1edv/P6MdOw4dZnvDQDuN5LfqNoV/ut3XGb2pA==} + '@testcontainers/postgresql@12.0.4': + resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==} + + '@testcontainers/redis@12.0.4': + resolution: {integrity: sha512-44Ov3Lcptb3KqljcCy6xIJ2VxNsN+3Kw/YpBiotZpOcI4iLgLNmcOgvCFPVo+vDoh4rCfgtNUEbIpL5Mu7oRuw==} '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' '@testing-library/react@16.3.2': resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} @@ -2988,12 +3126,18 @@ packages: resolution: {integrity: sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==} engines: {node: '>=18'} + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__code-frame@7.27.0': + resolution: {integrity: sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3015,21 +3159,27 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/deno@2.7.0': + resolution: {integrity: sha512-Y6fWcV8KpYeO3Lik/RiYnUxtr/LWqoeACciU0CA+dr1U/45tGgaX3HWQQAPCNN53raN4Rr4Fht4y6qsUH57fDA==} + '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} '@types/dockerode@4.0.1': resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/ini@4.1.1': - resolution: {integrity: sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==} - '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -3042,17 +3192,14 @@ packages: '@types/jscodeshift@17.3.0': resolution: {integrity: sha512-ogvGG8VQQqAQQ096uRh+d6tBHrYuZjsumHirKtvBa5qEyTMN3IQJ7apo+sw9lxaB/iKWIhbbLlF3zmAWk9XQIg==} - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@25.9.5': - resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==} - - '@types/node@26.1.1': - resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} '@types/nodemailer@8.0.1': resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} @@ -3285,9 +3432,6 @@ packages: vite: optional: true - '@vitest/pretty-format@3.2.7': - resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} - '@vitest/pretty-format@4.1.10': resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} @@ -3300,9 +3444,6 @@ packages: '@vitest/spy@4.1.10': resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@3.2.7': - resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} - '@vitest/utils@4.1.10': resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} @@ -3372,6 +3513,14 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -3433,16 +3582,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arkregex@0.0.8: - resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} - - arktype@2.2.3: - resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -3564,31 +3703,23 @@ packages: bare-events: optional: true - bare-url@2.4.5: - resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + bare-url@2.4.6: + resolution: {integrity: sha512-iQxPClE07hETVpbRoX7JXX3v/ZQViCxe/SYCxylRLzdEx1xJAufPptfiOqR8tqiCtmbtMDANKWszzjLu1PMAZQ==} base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.11.1: - resolution: {integrity: sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==} + baseline-browser-mapping@2.11.5: + resolution: {integrity: sha512-xJo6a6YZnwZfnyGmQKWMbVOcii7XRibjOskRh+WJ9UHQoX16xrQrcIgAMQOzfvs8XiLMx6ih/fsLPF73iY2D1A==} engines: {node: '>=6.0.0'} hasBin: true bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -3598,8 +3729,8 @@ packages: brace-expansion@1.1.16: resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} - brace-expansion@2.1.2: - resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==} + brace-expansion@2.1.3: + resolution: {integrity: sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==} brace-expansion@5.0.7: resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} @@ -3652,6 +3783,10 @@ packages: resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} engines: {node: '>=0.10.0'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} + call-me-maybe@1.0.2: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} @@ -3674,15 +3809,16 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - chardet@2.2.0: - resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3730,10 +3866,17 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.0: + resolution: {integrity: sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==} + engines: {node: '>=0.10.0'} + cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -3748,13 +3891,13 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@6.2.1: - resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} - engines: {node: '>= 6'} - commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -3824,8 +3967,8 @@ packages: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - dataloader@1.4.0: - resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} @@ -3891,10 +4034,6 @@ packages: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} @@ -3950,10 +4089,6 @@ packages: resolution: {integrity: sha512-3omnDTYrGigU0i4cJjvaKwD52B8aoqyX/NEIkukFFkogBemsIbhSa1O414fpTp5nuszJG6lvQ5vBvDVNCbSsaQ==} engines: {node: '>=0.8.0'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - docker-compose@1.4.2: resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} engines: {node: '>= 6.0.0'} @@ -3962,9 +4097,9 @@ packages: resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} engines: {node: '>= 8.0'} - dockerode@4.0.12: - resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} - engines: {node: '>= 8.0'} + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} @@ -3976,10 +4111,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dotenv@8.6.0: - resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} - engines: {node: '>=10'} - dprint@0.55.2: resolution: {integrity: sha512-1d4D4SB9KiD2qFnBWbl3aoYjLvPVpZoth28yxTT00xJv2yXugdz0Xoyv7sGG0w0hzE6hQZMgd6B333GnySDpGQ==} hasBin: true @@ -3993,8 +4124,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.395: - resolution: {integrity: sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==} + electron-to-chromium@1.5.397: + resolution: {integrity: sha512-khGTy9U9x02KEtsKM8vx5A62BsRmcOsIgDpWr1ImE32Ax8GxHGPHZf+Eu9H8zOOyHJnB0jTbseyTHbq2XCT8yw==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -4005,6 +4136,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -4020,10 +4155,6 @@ packages: resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} - entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -4051,11 +4182,6 @@ packages: es6-promise@3.3.1: resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} - esbuild@0.27.7: - resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} - engines: {node: '>=18'} - hasBin: true - esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -4134,9 +4260,6 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} - extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - fake-indexeddb@6.2.5: resolution: {integrity: sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==} engines: {node: '>=18'} @@ -4151,21 +4274,23 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.2: resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} fb-dotslash@0.5.8: resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} @@ -4209,9 +4334,6 @@ packages: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} - find-my-way-ts@0.1.6: - resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} - find-up@3.0.0: resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} engines: {node: '>=6'} @@ -4246,17 +4368,6 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fs-readdir-recursive@1.1.0: - resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -4299,26 +4410,15 @@ packages: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} - get-port@7.2.0: - resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} - engines: {node: '>=16'} + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - glob@11.1.0: - resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} - engines: {node: 20 || >=22} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.6: @@ -4329,13 +4429,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - gonzales-pe@4.3.0: resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} engines: {node: '>=0.6.0'} @@ -4350,7 +4443,6 @@ packages: gulp-header@1.8.12: resolution: {integrity: sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==} - deprecated: Removed event-stream from gulp-header happy-dom@20.11.1: resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} @@ -4420,10 +4512,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - image-size@1.2.1: resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} engines: {node: '>=16.x'} @@ -4432,6 +4520,9 @@ packages: immer@11.1.11: resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4454,10 +4545,6 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -4465,10 +4552,6 @@ packages: resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==} engines: {node: '>=12.22.0'} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} @@ -4494,18 +4577,10 @@ packages: resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} engines: {node: '>=0.10.0'} - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -4556,10 +4631,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -4568,10 +4639,6 @@ packages: resolution: {integrity: sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==} engines: {node: '>=10'} - is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} @@ -4613,10 +4680,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.2.3: - resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} - engines: {node: 20 || >=22} - jest-environment-node@29.7.0: resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4653,6 +4716,9 @@ packages: resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-base64@3.8.0: resolution: {integrity: sha512-65kvbemyZhj+ExQt1PEFyBEjL5vAHysu1lJdW1AwhhChkO8ZBPizYk/m9GVrpbS2Je1hF+UYZ+6KywqtZV8mHw==} @@ -4677,10 +4743,6 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} - hasBin: true - jsbi@4.3.2: resolution: {integrity: sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==} @@ -4697,11 +4759,11 @@ packages: '@babel/preset-env': optional: true - jsdom@29.1.1: - resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + jsdom@30.0.0: + resolution: {integrity: sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: - canvas: ^3.0.0 + canvas: ^3.2.3 peerDependenciesMeta: canvas: optional: true @@ -4722,8 +4784,8 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} @@ -4758,6 +4820,9 @@ packages: resolution: {integrity: sha512-1Ut5QslLbAYZfrZqnEcG7InUeZgjmDPhODdWNt7DtulTRLVqQtqzSDJXylLa90L0ajZzeGZk36fjSsAB5m/sdQ==} hasBin: true + launch-editor@2.14.1: + resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} + lazy-cache@2.0.2: resolution: {integrity: sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==} engines: {node: '>=0.10.0'} @@ -4778,74 +4843,74 @@ packages: lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} list-item@1.1.1: @@ -4893,12 +4958,8 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lodash.template@4.18.1: resolution: {integrity: sha512-5urZrLnV/VD6zHK5KsVtZgt7H19v51mIzoS0aBNH8yp3I8tbswrEjOABOPY8m8uB7NuibubLrMX+Y0PXsU9X+w==} - deprecated: This package is deprecated. Use https://socket.dev/npm/package/eta instead. lodash.templatesettings@4.2.0: resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} @@ -4920,16 +4981,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.6: - resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} - engines: {node: 20 || >=22} - lru-cache@11.5.2: resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} @@ -4995,10 +5049,6 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - metro-babel-transformer@0.83.7: resolution: {integrity: sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==} engines: {node: '>=20.19.4'} @@ -5153,10 +5203,6 @@ packages: engines: {node: '>=18'} hasBin: true - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -5174,9 +5220,6 @@ packages: msgpackr@2.0.4: resolution: {integrity: sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==} - multipasta@0.2.8: - resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} - mysql2@3.22.6: resolution: {integrity: sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==} engines: {node: '>= 8.0'} @@ -5326,22 +5369,18 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - - oxlint@1.42.0: - resolution: {integrity: sha512-qnspC/lrp8FgKNaONLLn14dm+W5t0SSlus6V5NJpgI2YNT1tkFYZt4fBf14ESxf9AAh98WBASnW5f0gtw462Lg==} + oxlint@1.76.0: + resolution: {integrity: sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - oxlint-tsgolint: '>=0.11.2' + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' peerDependenciesMeta: oxlint-tsgolint: optional: true - - p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} + vite-plus: + optional: true p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} @@ -5355,10 +5394,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -5366,8 +5401,8 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@0.2.11: - resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} parse-json@8.3.0: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} @@ -5384,6 +5419,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -5411,10 +5449,6 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -5495,14 +5529,18 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - playwright-core@1.61.1: - resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} - engines: {node: '>=18'} + pkg-pr-new@0.0.78: + resolution: {integrity: sha512-sBfGGCiCLmJArx99Z/QmNN3jmkqRYhp1TV8Y1IOpSpXfOljqFcEjDyyc4HCVy+nbrmlkw9qXRq/9/L4ahF/d4w==} hasBin: true - playwright@1.61.1: - resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} - engines: {node: '>=18'} + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} hasBin: true pluralize@8.0.0: @@ -5519,8 +5557,8 @@ packages: peerDependencies: postcss: ^8.2.9 - postcss@8.5.22: - resolution: {integrity: sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==} + postcss@8.5.24: + resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -5567,11 +5605,6 @@ packages: engines: {node: '>=18'} hasBin: true - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - prettier@3.9.6: resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} @@ -5623,12 +5656,6 @@ packages: pure-rand@8.4.1: resolution: {integrity: sha512-c58R2+SPFcSIPXoU834QN/KPDDOSd8sXcSrqf6e83Me6Rrp1EYkxukkjXMVrKvKaADs1SOyNkWdfvLf6zY8qLQ==} - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} @@ -5685,10 +5712,6 @@ packages: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} - read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -5703,9 +5726,9 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} recast@0.23.12: resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} @@ -5783,10 +5806,6 @@ packages: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -5862,8 +5881,8 @@ packages: vite: optional: true - rollup@4.62.2: - resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + rollup@4.62.3: + resolution: {integrity: sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5871,9 +5890,6 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -5996,14 +6012,17 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} - slash@2.0.0: - resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} - engines: {node: '>=6'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + smob@1.6.2: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} @@ -6030,9 +6049,6 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} - spawndamnit@3.0.1: - resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} - split-ca@1.0.1: resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} @@ -6210,10 +6226,6 @@ packages: teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} - terser@5.49.0: resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} engines: {node: '>=10'} @@ -6223,8 +6235,12 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - testcontainers@11.14.0: - resolution: {integrity: sha512-r9pniwv/iwzyHaI7gwAvAm4Y+IvjJg3vBWdjrUCaDMc2AXIr4jKbq7jJO18Mw2ybs73pZy1Aj7p/4RVBGMRWjg==} + testcontainers@12.0.4: + resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} + + testcontainers@12.1.0: + resolution: {integrity: sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA==} + engines: {node: '>= 22.22'} text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -6253,19 +6269,15 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@7.0.30: - resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==} + tldts-core@7.4.9: + resolution: {integrity: sha512-DxKfPBI52p2msTEu7MPhdpdDTBhhVQg1a/8PjQckeyAvO13eMYElX545grIp6nnTGIMZlRvFZPvFhvI/WIz2Vg==} - tldts@7.0.30: - resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==} + tldts@7.4.9: + resolution: {integrity: sha512-3kZ8wQQ/k5DrChD4X4FVvr2D7E5uoRgAqkPyLpSCGUvqOvqu+JEdr3mwMUaVWb+vMHZaKhF5fp2PBigKsui7hA==} hasBin: true tmp@0.2.7: @@ -6287,16 +6299,12 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toml@4.1.2: - resolution: {integrity: sha512-m0vXfHODcw3gk+KONAOlVQ5yNHc3yS3B1ybM3HS1vqDoS0RWTDDVBVVTYi8hH0k+2OM1vmo9fb1WX9EVqjqfHA==} - engines: {node: '>=20'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} tr46@0.0.3: @@ -6335,10 +6343,12 @@ packages: resolution: {integrity: sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==} engines: {node: '>=18'} + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} - deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -6363,8 +6373,8 @@ packages: typescript: optional: true - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -6404,16 +6414,9 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@7.24.6: - resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} - undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@7.25.0: - resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} - engines: {node: '>=20.18.1'} - undici@7.28.0: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} @@ -6422,9 +6425,9 @@ packages: resolution: {integrity: sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==} engines: {node: '>=22.19.0'} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} @@ -6450,18 +6453,12 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true valibot@1.4.2: @@ -6472,20 +6469,16 @@ packages: typescript: optional: true - vite-tsconfig-paths@6.1.1: - resolution: {integrity: sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg==} - peerDependencies: - vite: '*' - - vite@7.3.6: - resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + vite@8.1.5: + resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -6496,12 +6489,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -6517,10 +6512,10 @@ packages: yaml: optional: true - vitest-websocket-mock@0.5.0: - resolution: {integrity: sha512-vzBWeuF/kD/OCOFzB7WAclb7PxfI105qPkZtdOkPMwZdilBskQjJL4l319JtPtmeovDU7ZVhO3hTfGPjM4txQQ==} + vitest-websocket-mock@0.7.0: + resolution: {integrity: sha512-OrDWs/FiZl8MfcCjvxLri1Uq2NpFcqbQdFHuotmU5FgA9Z5PHRxCyHdmu/xSPgBzbxO9pz3ZdHtokFELCT7AOg==} peerDependencies: - vitest: '>=3' + vitest: '>=4' vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} @@ -6610,6 +6605,10 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -6764,33 +6763,22 @@ packages: snapshots: - '@adobe/css-tools@4.4.4': {} - - '@ark/schema@0.56.2': - dependencies: - '@ark/util': 0.56.2 - - '@ark/util@0.56.2': {} + '@adobe/css-tools@4.5.0': {} - '@asamuzakjp/css-color@5.1.11': + '@asamuzakjp/css-color@6.0.5': dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 - '@asamuzakjp/dom-selector@7.1.1': + '@asamuzakjp/dom-selector@8.3.0': dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - - '@asamuzakjp/generational-cache@1.0.1': {} - - '@asamuzakjp/nwsapi@2.3.9': {} + lru-cache: 11.5.2 '@azure-rest/core-client@2.6.0': dependencies: @@ -6936,23 +6924,19 @@ snapshots: jsonwebtoken: 9.0.3 uuid: 8.3.2 - '@babel/cli@7.29.7(@babel/core@7.29.7)': + '@babel/cli@8.0.4(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@jridgewell/trace-mapping': 0.3.31 - commander: 6.2.1 + chokidar: 5.0.0 + commander: 14.0.3 convert-source-map: 2.0.0 - fs-readdir-recursive: 1.1.0 - glob: 7.2.3 - make-dir: 2.1.0 - slash: 2.0.0 - optionalDependencies: - '@nicolo-ribaudo/chokidar-2': 2.1.8-no-fsevents.3 - chokidar: 3.6.0 + glob: 13.0.6 + slash: 5.1.0 '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -6962,8 +6946,15 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@8.0.0': + dependencies: + '@babel/helper-validator-identifier': 8.0.4 + js-tokens: 10.0.0 + '@babel/compat-data@7.29.7': {} + '@babel/compat-data@8.0.0': {} + '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -6984,6 +6975,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/core@8.0.1': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-compilation-targets': 8.0.0 + '@babel/helpers': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.0.1 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 2.1.4 + semver: 7.8.5 + '@babel/generator@7.29.7': dependencies: '@babel/parser': 7.29.7 @@ -6992,6 +7002,15 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@8.0.0': + dependencies: + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.29.7': dependencies: '@babel/types': 7.29.7 @@ -7004,6 +7023,14 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 + '@babel/helper-compilation-targets@8.0.0': + dependencies: + '@babel/compat-data': 8.0.0 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.28.7 + lru-cache: 11.5.2 + semver: 7.8.5 + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -7019,6 +7046,8 @@ snapshots: '@babel/helper-globals@7.29.7': {} + '@babel/helper-globals@8.0.0': {} + '@babel/helper-member-expression-to-functions@7.29.7': dependencies: '@babel/traverse': 7.29.7 @@ -7033,6 +7062,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@8.0.0': + dependencies: + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -7042,12 +7076,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-module-imports': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + '@babel/traverse': 8.0.4 + '@babel/helper-optimise-call-expression@7.29.7': dependencies: '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.29.7': {} + '@babel/helper-plugin-utils@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -7066,39 +7111,52 @@ snapshots: '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-string-parser@8.0.0': {} '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-identifier@8.0.4': {} + '@babel/helper-validator-option@7.29.7': {} + '@babel/helper-validator-option@8.0.0': {} + '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 '@babel/types': 7.29.7 + '@babel/helpers@8.0.0': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + '@babel/parser@7.29.7': dependencies: '@babel/types': 7.29.7 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + '@babel/parser@8.0.4': dependencies: - '@babel/core': 7.29.7 + '@babel/types': 8.0.4 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7)': @@ -7106,19 +7164,19 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': @@ -7126,44 +7184,44 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': @@ -7179,10 +7237,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-export-namespace-from@8.0.1(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-plugin-utils': 7.29.7 + '@babel/core': 8.0.1 + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7)': dependencies: @@ -7198,6 +7256,12 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-modules-commonjs@8.0.1(@babel/core@8.0.1)': + dependencies: + '@babel/core': 8.0.1 + '@babel/helper-module-transforms': 8.0.1(@babel/core@8.0.1) + '@babel/helper-plugin-utils': 8.0.1(@babel/core@8.0.1) + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -7267,6 +7331,12 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 + '@babel/traverse@7.29.7': dependencies: '@babel/code-frame': 7.29.7 @@ -7279,11 +7349,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@8.0.4': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-globals': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + obug: 2.1.4 + '@babel/types@7.29.7': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.4': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + '@balena/dockerignore@1.0.2': {} '@bcoe/v8-coverage@1.0.2': {} @@ -7327,163 +7412,130 @@ snapshots: lodash: 4.18.1 serialize-query-params: 2.0.4 - '@changesets/apply-release-plan@7.1.1': - dependencies: - '@changesets/config': 3.1.4 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.4 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - detect-indent: 6.1.0 - fs-extra: 7.0.1 - lodash.startcase: 4.4.0 - outdent: 0.5.0 - prettier: 2.8.8 - resolve-from: 5.0.0 - semver: 7.8.5 - - '@changesets/assemble-release-plan@6.0.10(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e)': + '@changesets/apply-release-plan@8.0.0-next.10': dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.4 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + '@changesets/config': 4.0.0-next.9 + '@changesets/format': 0.1.1 + '@changesets/git': 4.0.0-next.9 + '@changesets/should-skip-package': 1.0.0-next.9 + '@changesets/types': 7.0.0-next.9 + import-meta-resolve: 4.2.0 + jsonc-parser: 3.3.1 semver: 7.8.5 - '@changesets/changelog-git@0.2.1': + '@changesets/assemble-release-plan@7.0.0-next.10': dependencies: - '@changesets/types': 6.1.0 - - '@changesets/changelog-github@0.7.0': - dependencies: - '@changesets/get-github-info': 0.8.0(patch_hash=314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9) - '@changesets/types': 6.1.0 - dotenv: 8.6.0 - transitivePeerDependencies: - - encoding + '@changesets/errors': 1.0.0-next.4 + '@changesets/get-dependents-graph': 3.0.0-next.9 + '@changesets/should-skip-package': 1.0.0-next.9 + '@changesets/types': 7.0.0-next.9 + semver: 7.8.5 - '@changesets/cli@2.31.1(@types/node@25.9.5)': - dependencies: - '@changesets/apply-release-plan': 7.1.1 - '@changesets/assemble-release-plan': 6.0.10(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e) - '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.4 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.4 - '@changesets/get-release-plan': 4.0.16 - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.7 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.9.5) - '@manypkg/get-packages': 1.1.3 - ansi-colors: 4.1.3 - enquirer: 2.4.1 - fs-extra: 7.0.1 - mri: 1.2.0 - package-manager-detector: 0.2.11 - picocolors: 1.1.1 - resolve-from: 5.0.0 + '@changesets/changelog-git@1.0.0-next.9': + dependencies: + '@changesets/types': 7.0.0-next.9 + + '@changesets/changelog-github@1.0.0-next.9': + dependencies: + '@changesets/get-github-info': 1.0.0-next.4(patch_hash=a5b1907668397f36aab989954aa78993792f578b83e41a164ab7ecd573ac9166) + '@changesets/types': 7.0.0-next.9 + + '@changesets/cli@3.0.0-next.11': + dependencies: + '@changesets/apply-release-plan': 8.0.0-next.10 + '@changesets/assemble-release-plan': 7.0.0-next.10 + '@changesets/changelog-git': 1.0.0-next.9 + '@changesets/config': 4.0.0-next.9 + '@changesets/errors': 1.0.0-next.4 + '@changesets/get-dependents-graph': 3.0.0-next.9 + '@changesets/git': 4.0.0-next.9 + '@changesets/pre': 3.0.0-next.9 + '@changesets/read': 1.0.0-next.10(patch_hash=00298a7e3295e97aab2f89a36af496eef21b576642bd2eac88ae13ceca78cccb) + '@changesets/should-skip-package': 1.0.0-next.9 + '@changesets/types': 7.0.0-next.9 + '@changesets/write': 1.0.0-next.9 + '@clack/prompts': 1.7.0 + '@manypkg/get-packages': 3.1.0 + '@pnpm/deps.graph-sequencer': 1100.0.1 + cac: 7.0.0 + import-meta-resolve: 4.2.0 + launch-editor: 2.14.1 + package-manager-detector: 1.8.0 semver: 7.8.5 - spawndamnit: 3.0.1 - term-size: 2.2.1 - transitivePeerDependencies: - - '@types/node' + tinyexec: 1.2.4 - '@changesets/config@3.1.4': + '@changesets/config@4.0.0-next.9': dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.4 - '@changesets/logger': 0.1.1 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - micromatch: 4.0.8 + '@changesets/get-dependents-graph': 3.0.0-next.9 + '@changesets/should-skip-package': 1.0.0-next.9 + '@changesets/types': 7.0.0-next.9 + '@manypkg/get-packages': 3.1.0 + picomatch: 4.0.5 - '@changesets/errors@0.2.0': + '@changesets/errors@1.0.0-next.4': {} + + '@changesets/format@0.1.1': dependencies: - extendable-error: 0.1.7 + package-manager-detector: 1.8.0 + tinyexec: 1.2.4 - '@changesets/get-dependents-graph@2.1.4': + '@changesets/get-dependents-graph@3.0.0-next.9': dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.1 + '@changesets/types': 7.0.0-next.9 semver: 7.8.5 - '@changesets/get-github-info@0.8.0(patch_hash=314478eeae7ab2776d847d3b2b7ea1cd4231c65907e85597dacd0bea56355bc9)': + '@changesets/get-github-info@1.0.0-next.4(patch_hash=a5b1907668397f36aab989954aa78993792f578b83e41a164ab7ecd573ac9166)': dependencies: - dataloader: 1.4.0 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + dataloader: 2.2.3 - '@changesets/get-release-plan@4.0.16': + '@changesets/git@4.0.0-next.9': dependencies: - '@changesets/assemble-release-plan': 6.0.10(patch_hash=29cf7a343aefd3c59ad238a4cb067f0a1a0adbeed07b6d57d6f31d6c80a5e25e) - '@changesets/config': 3.1.4 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.7 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/get-version-range-type@0.4.0': {} + '@changesets/errors': 1.0.0-next.4 + '@changesets/types': 7.0.0-next.9 + '@manypkg/get-packages': 3.1.0 + picomatch: 4.0.5 + tinyexec: 1.2.4 - '@changesets/git@3.0.4': + '@changesets/parse@1.0.0-next.10': dependencies: - '@changesets/errors': 0.2.0 - '@manypkg/get-packages': 1.1.3 - is-subdir: 1.2.0 - micromatch: 4.0.8 - spawndamnit: 3.0.1 + '@changesets/types': 7.0.0-next.9 + yaml: 2.9.0 - '@changesets/logger@0.1.1': + '@changesets/pre@3.0.0-next.9': dependencies: - picocolors: 1.1.1 + '@changesets/errors': 1.0.0-next.4 + '@changesets/types': 7.0.0-next.9 + '@manypkg/get-packages': 3.1.0 - '@changesets/parse@0.4.3': + '@changesets/read@1.0.0-next.10(patch_hash=00298a7e3295e97aab2f89a36af496eef21b576642bd2eac88ae13ceca78cccb)': dependencies: - '@changesets/types': 6.1.0 - js-yaml: 4.3.0 + '@changesets/git': 4.0.0-next.9 + '@changesets/parse': 1.0.0-next.10 + '@changesets/types': 7.0.0-next.9 - '@changesets/pre@2.0.2': + '@changesets/should-skip-package@1.0.0-next.9': dependencies: - '@changesets/errors': 0.2.0 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 + '@changesets/types': 7.0.0-next.9 - '@changesets/read@0.6.7': - dependencies: - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.3 - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - p-filter: 2.1.0 - picocolors: 1.1.1 + '@changesets/types@7.0.0-next.9': {} - '@changesets/should-skip-package@0.1.2': + '@changesets/write@1.0.0-next.9': dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/types@4.1.0': {} + '@changesets/format': 0.1.1 + '@changesets/types': 7.0.0-next.9 + human-id: 4.2.0 - '@changesets/types@6.1.0': {} + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 - '@changesets/write@0.4.0': + '@clack/prompts@1.7.0': dependencies: - '@changesets/types': 6.1.0 - fs-extra: 7.0.1 - human-id: 4.2.0 - prettier: 2.8.8 + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 '@clickhouse/client@1.23.1': {} @@ -7508,17 +7560,17 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@6.0.2': {} + '@csstools/color-helpers@6.1.0': {} - '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -7526,7 +7578,7 @@ snapshots: dependencies: '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 @@ -7592,16 +7644,6 @@ snapshots: '@edge-runtime/primitives': 6.0.0 optional: true - '@effect/docgen@https://pkg.pr.new/Effect-TS/docgen/@effect/docgen@e57e5f5(tsx@4.21.0)(typescript@7.0.2)': - dependencies: - '@babel/code-frame': 7.29.7 - '@effect/markdown-toc': 0.1.0 - doctrine: 3.0.0 - glob: 11.1.0 - prettier: 3.9.6 - tsx: 4.21.0 - typescript: 7.0.2 - '@effect/markdown-toc@0.1.0': dependencies: concat-stream: 1.6.2 @@ -7663,7 +7705,7 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.11.2': + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true @@ -7673,163 +7715,85 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.7': - optional: true - '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.27.7': - optional: true - '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.27.7': - optional: true - '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.27.7': - optional: true - '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.27.7': - optional: true - '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.27.7': - optional: true - '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.27.7': - optional: true - '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.27.7': - optional: true - '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.27.7': - optional: true - '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.27.7': - optional: true - '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.27.7': - optional: true - '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.27.7': - optional: true - '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.27.7': - optional: true - '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.27.7': - optional: true - '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.27.7': - optional: true - '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.27.7': - optional: true - '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.27.7': - optional: true - '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.27.7': - optional: true - '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.27.7': - optional: true - '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.27.7': - optional: true - '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.27.7': - optional: true - '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.27.7': - optional: true - '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.27.7': - optional: true - '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.27.7': - optional: true - '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.27.7': - optional: true - '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.27.7': - optional: true - '@esbuild/win32-x64@0.28.1': optional: true - '@exodus/bytes@1.15.0': {} + '@exodus/bytes@1.15.1': {} '@exodus/schemasafe@1.3.0': {} @@ -7938,7 +7902,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.11.2 + '@emnapi/runtime': 1.11.3 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -7950,13 +7914,6 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.9.5)': - dependencies: - chardet: 2.2.0 - iconv-lite: 0.7.3 - optionalDependencies: - '@types/node': 25.9.5 - '@ioredis/commands@1.4.0': {} '@isaacs/cliui@8.0.2': @@ -7968,8 +7925,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/cliui@9.0.0': {} - '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 @@ -7994,14 +7949,14 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 26.1.1 + '@types/node': 26.1.2 jest-mock: 29.7.0 '@jest/fake-timers@29.7.0': dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 26.1.1 + '@types/node': 26.1.2 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -8035,7 +7990,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -8076,6 +8031,54 @@ snapshots: dependencies: jsbi: 4.3.2 + '@jsr/db__redis@0.41.2': + dependencies: + '@jsr/std__async': 1.5.0 + '@jsr/std__bytes': 1.0.6 + '@jsr/std__collections': 1.3.0 + '@jsr/std__io': 0.224.5 + '@jsr/std__random': 0.1.0 + cluster-key-slot: 1.1.0 + + '@jsr/std__assert@1.0.19': + dependencies: + '@jsr/std__internal': 1.0.14 + + '@jsr/std__async@1.5.0': + dependencies: + '@jsr/std__data-structures': 1.1.1 + + '@jsr/std__bytes@1.0.6': {} + + '@jsr/std__collections@1.3.0': {} + + '@jsr/std__data-structures@1.1.1': + dependencies: + '@jsr/std__assert': 1.0.19 + + '@jsr/std__fs@1.0.24': + dependencies: + '@jsr/std__internal': 1.0.14 + '@jsr/std__path': 1.1.6 + + '@jsr/std__internal@1.0.14': {} + + '@jsr/std__io@0.224.5': + dependencies: + '@jsr/std__bytes': 1.0.6 + + '@jsr/std__media-types@1.1.0': {} + + '@jsr/std__path@1.1.6': + dependencies: + '@jsr/std__internal': 1.0.14 + + '@jsr/std__random@0.1.0': {} + + '@jsr/std__streams@1.1.1': + dependencies: + '@jsr/std__bytes': 1.0.6 + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -8140,21 +8143,20 @@ snapshots: '@libsql/win32-x64-msvc@0.5.29': optional: true - '@manypkg/find-root@1.1.0': + '@manypkg/find-root@3.1.0': dependencies: - '@babel/runtime': 7.29.7 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 + '@manypkg/tools': 2.1.2 - '@manypkg/get-packages@1.1.3': + '@manypkg/get-packages@3.1.0': dependencies: - '@babel/runtime': 7.29.7 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 + '@manypkg/find-root': 3.1.0 + '@manypkg/tools': 2.1.2 + + '@manypkg/tools@2.1.2': + dependencies: + jju: 1.4.0 + tinyglobby: 0.2.17 + yaml: 2.9.0 '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': optional: true @@ -8174,7 +8176,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': optional: true - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@napi-rs/wasm-runtime@1.2.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 @@ -8183,25 +8185,10 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': - optional: true - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@op-engineering/op-sqlite@17.1.2(react-native@0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)': + '@op-engineering/op-sqlite@17.1.2(react-native@0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)': dependencies: react: 19.2.7 - react-native: 0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7) + react-native: 0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7) '@opentelemetry/api-logs@0.220.0': dependencies: @@ -8301,45 +8288,81 @@ snapshots: '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.42.0 + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.42.0 + + '@opentelemetry/semantic-conventions@1.42.0': {} + + '@oxc-project/types@0.139.0': {} + + '@oxlint/binding-android-arm-eabi@1.76.0': + optional: true + + '@oxlint/binding-android-arm64@1.76.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.76.0': + optional: true + + '@oxlint/binding-darwin-x64@1.76.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.76.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.76.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.76.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.76.0': + optional: true - '@opentelemetry/semantic-conventions@1.42.0': {} + '@oxlint/binding-linux-ppc64-gnu@1.76.0': + optional: true - '@oxc-project/types@0.139.0': + '@oxlint/binding-linux-riscv64-gnu@1.76.0': optional: true - '@oxlint/darwin-arm64@1.42.0': + '@oxlint/binding-linux-riscv64-musl@1.76.0': optional: true - '@oxlint/darwin-x64@1.42.0': + '@oxlint/binding-linux-s390x-gnu@1.76.0': optional: true - '@oxlint/linux-arm64-gnu@1.42.0': + '@oxlint/binding-linux-x64-gnu@1.76.0': optional: true - '@oxlint/linux-arm64-musl@1.42.0': + '@oxlint/binding-linux-x64-musl@1.76.0': optional: true - '@oxlint/linux-x64-gnu@1.42.0': + '@oxlint/binding-openharmony-arm64@1.76.0': optional: true - '@oxlint/linux-x64-musl@1.42.0': + '@oxlint/binding-win32-arm64-msvc@1.76.0': optional: true - '@oxlint/win32-arm64@1.42.0': + '@oxlint/binding-win32-ia32-msvc@1.76.0': optional: true - '@oxlint/win32-x64@1.42.0': + '@oxlint/binding-win32-x64-msvc@1.76.0': optional: true + '@oxlint/plugins@1.76.0': {} + '@pkgjs/parseargs@0.11.0': optional: true + '@pnpm/deps.graph-sequencer@1100.0.1': {} + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.6': @@ -8376,9 +8399,9 @@ snapshots: '@react-native/assets-registry@0.83.0': {} - '@react-native/codegen@0.83.0(@babel/core@7.29.7)': + '@react-native/codegen@0.83.0(@babel/core@8.0.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@babel/parser': 7.29.7 glob: 7.2.3 hermes-parser: 0.32.0 @@ -8432,12 +8455,12 @@ snapshots: '@react-native/normalize-colors@0.83.0': {} - '@react-native/virtualized-lists@0.83.0(@types/react@19.2.17)(react-native@0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)': + '@react-native/virtualized-lists@0.83.0(@types/react@19.2.17)(react-native@0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.7 - react-native: 0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7) + react-native: 0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7) optionalDependencies: '@types/react': 19.2.17 @@ -8504,7 +8527,7 @@ snapshots: dependencies: '@emnapi/core': 1.11.1 '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@napi-rs/wasm-runtime': 1.2.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true '@rolldown/binding-win32-arm64-msvc@1.1.5': @@ -8513,115 +8536,114 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true - '@rolldown/pluginutils@1.0.1': - optional: true + '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.2)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.3)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@rollup/pluginutils': 5.4.0(rollup@4.62.3) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.12 optionalDependencies: - rollup: 4.62.2 + rollup: 4.62.3 - '@rollup/plugin-replace@6.0.3(rollup@4.62.2)': + '@rollup/plugin-replace@6.0.3(rollup@4.62.3)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@rollup/pluginutils': 5.4.0(rollup@4.62.3) magic-string: 0.30.21 optionalDependencies: - rollup: 4.62.2 + rollup: 4.62.3 - '@rollup/plugin-terser@1.0.0(rollup@4.62.2)': + '@rollup/plugin-terser@1.0.0(rollup@4.62.3)': dependencies: serialize-javascript: 7.0.7 smob: 1.6.2 terser: 5.49.0 optionalDependencies: - rollup: 4.62.2 + rollup: 4.62.3 - '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + '@rollup/pluginutils@5.4.0(rollup@4.62.3)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 picomatch: 4.0.5 optionalDependencies: - rollup: 4.62.2 + rollup: 4.62.3 - '@rollup/rollup-android-arm-eabi@4.62.2': + '@rollup/rollup-android-arm-eabi@4.62.3': optional: true - '@rollup/rollup-android-arm64@4.62.2': + '@rollup/rollup-android-arm64@4.62.3': optional: true - '@rollup/rollup-darwin-arm64@4.62.2': + '@rollup/rollup-darwin-arm64@4.62.3': optional: true - '@rollup/rollup-darwin-x64@4.62.2': + '@rollup/rollup-darwin-x64@4.62.3': optional: true - '@rollup/rollup-freebsd-arm64@4.62.2': + '@rollup/rollup-freebsd-arm64@4.62.3': optional: true - '@rollup/rollup-freebsd-x64@4.62.2': + '@rollup/rollup-freebsd-x64@4.62.3': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + '@rollup/rollup-linux-arm-gnueabihf@4.62.3': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.2': + '@rollup/rollup-linux-arm-musleabihf@4.62.3': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.2': + '@rollup/rollup-linux-arm64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.2': + '@rollup/rollup-linux-arm64-musl@4.62.3': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.2': + '@rollup/rollup-linux-loong64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.2': + '@rollup/rollup-linux-loong64-musl@4.62.3': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.2': + '@rollup/rollup-linux-ppc64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.2': + '@rollup/rollup-linux-ppc64-musl@4.62.3': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.2': + '@rollup/rollup-linux-riscv64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.2': + '@rollup/rollup-linux-riscv64-musl@4.62.3': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.2': + '@rollup/rollup-linux-s390x-gnu@4.62.3': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.2': + '@rollup/rollup-linux-x64-gnu@4.62.3': optional: true - '@rollup/rollup-linux-x64-musl@4.62.2': + '@rollup/rollup-linux-x64-musl@4.62.3': optional: true - '@rollup/rollup-openbsd-x64@4.62.2': + '@rollup/rollup-openbsd-x64@4.62.3': optional: true - '@rollup/rollup-openharmony-arm64@4.62.2': + '@rollup/rollup-openharmony-arm64@4.62.3': optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.2': + '@rollup/rollup-win32-arm64-msvc@4.62.3': optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.2': + '@rollup/rollup-win32-ia32-msvc@4.62.3': optional: true - '@rollup/rollup-win32-x64-gnu@4.62.2': + '@rollup/rollup-win32-x64-gnu@4.62.3': optional: true - '@rollup/rollup-win32-x64-msvc@4.62.2': + '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true '@sinclair/typebox@0.27.12': {} @@ -8666,27 +8688,36 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@testcontainers/mysql@11.14.0': + '@testcontainers/mssqlserver@12.0.4': + dependencies: + testcontainers: 12.1.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + '@testcontainers/mysql@12.0.4': dependencies: - testcontainers: 11.14.0 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-abort-controller - bare-buffer - react-native-b4a - supports-color - '@testcontainers/postgresql@11.14.0': + '@testcontainers/postgresql@12.0.4': dependencies: - testcontainers: 11.14.0 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-abort-controller - bare-buffer - react-native-b4a - supports-color - '@testcontainers/redis@11.14.0': + '@testcontainers/redis@12.0.4': dependencies: - testcontainers: 11.14.0 + testcontainers: 12.0.4 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -8704,9 +8735,10 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': dependencies: - '@adobe/css-tools': 4.4.4 + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 aria-query: 5.3.2 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 @@ -8738,6 +8770,12 @@ snapshots: '@ts-graphviz/ast': 2.0.7 '@ts-graphviz/common': 2.1.5 + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.17 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 @@ -8745,6 +8783,8 @@ snapshots: '@types/aria-query@5.0.4': {} + '@types/babel__code-frame@7.27.0': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.7 @@ -8777,24 +8817,28 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/deno@2.7.0': {} + '@types/docker-modem@3.0.6': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/ssh2': 1.15.5 '@types/dockerode@4.0.1': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/ssh2': 1.15.5 + '@types/doctrine@0.0.9': {} + '@types/estree@1.0.9': {} + '@types/gensync@1.0.5': {} + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 26.1.1 - - '@types/ini@4.1.1': {} + '@types/node': 26.1.2 '@types/istanbul-lib-coverage@2.0.6': {} @@ -8811,32 +8855,28 @@ snapshots: ast-types: 0.16.1 recast: 0.23.12 - '@types/node@12.20.55': {} + '@types/jsesc@2.5.1': {} '@types/node@18.19.130': dependencies: undici-types: 5.26.5 - '@types/node@25.9.5': - dependencies: - undici-types: 7.24.6 - - '@types/node@26.1.1': + '@types/node@26.1.2': dependencies: undici-types: 8.3.0 '@types/nodemailer@8.0.1': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/pg-cursor@2.7.2': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/pg': 8.20.0 '@types/pg@8.20.0': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -8850,7 +8890,7 @@ snapshots: '@types/readable-stream@4.0.23': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/resolve@1.20.2': {} @@ -8858,11 +8898,11 @@ snapshots: '@types/ssh2-streams@0.1.13': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/ssh2@0.5.52': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/ssh2-streams': 0.1.13 '@types/ssh2@1.15.5': @@ -8873,14 +8913,14 @@ snapshots: '@types/swagger2openapi@7.0.4': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 openapi-types: 12.1.3 '@types/whatwg-mimetype@3.0.2': {} '@types/ws@8.18.1': dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@types/yargs-parser@21.0.3': {} @@ -8991,16 +9031,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser@4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10)': + '@vitest/browser@4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) ws: 8.21.1 transitivePeerDependencies: - bufferutil @@ -9020,9 +9060,9 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/expect@4.1.10': dependencies: @@ -9033,25 +9073,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - - '@vitest/mocker@4.1.10(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - - '@vitest/pretty-format@3.2.7': - dependencies: - tinyrainbow: 2.0.0 + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -9071,12 +9099,6 @@ snapshots: '@vitest/spy@4.1.10': {} - '@vitest/utils@3.2.7': - dependencies: - '@vitest/pretty-format': 3.2.7 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - '@vitest/utils@4.1.10': dependencies: '@vitest/pretty-format': 4.1.10 @@ -9086,7 +9108,7 @@ snapshots: '@vitest/web-worker@4.1.10(vitest@4.1.10)': dependencies: obug: 2.1.4 - vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vue/compiler-core@3.5.39': dependencies: @@ -9123,7 +9145,7 @@ snapshots: '@vue/shared': 3.5.39 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.22 + postcss: 8.5.24 source-map-js: 1.2.1 '@vue/compiler-sfc@3.5.40': @@ -9135,7 +9157,7 @@ snapshots: '@vue/shared': 3.5.40 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.22 + postcss: 8.5.24 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.39': @@ -9187,6 +9209,10 @@ snapshots: agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -9255,18 +9281,6 @@ snapshots: aria-query@5.3.2: {} - arkregex@0.0.8: - dependencies: - '@ark/util': 0.56.2 - - arktype@2.2.3: - dependencies: - '@ark/schema': 0.56.2 - '@ark/util': 0.56.2 - arkregex: 0.0.8 - - array-union@2.1.0: {} - asap@2.0.6: {} asn1@0.2.6: @@ -9303,22 +9317,22 @@ snapshots: b4a@1.8.1: {} - babel-jest@29.7.0(@babel/core@7.29.7): + babel-jest@29.7.0(@babel/core@8.0.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.7) + babel-preset-jest: 29.6.3(@babel/core@8.0.1) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-plugin-annotate-pure-calls@0.5.0(@babel/core@7.29.7): + babel-plugin-annotate-pure-calls@0.5.0(@babel/core@8.0.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 8.0.1 babel-plugin-istanbul@6.1.1: dependencies: @@ -9341,30 +9355,30 @@ snapshots: dependencies: hermes-parser: 0.32.0 - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) - - babel-preset-jest@29.6.3(@babel/core@7.29.7): - dependencies: - '@babel/core': 7.29.7 + babel-preset-current-node-syntax@1.2.0(@babel/core@8.0.1): + dependencies: + '@babel/core': 8.0.1 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@8.0.1) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@8.0.1) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@8.0.1) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@8.0.1) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@8.0.1) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@8.0.1) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@8.0.1) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@8.0.1) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@8.0.1) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@8.0.1) + + babel-preset-jest@29.6.3(@babel/core@8.0.1): + dependencies: + '@babel/core': 8.0.1 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + babel-preset-current-node-syntax: 1.2.0(@babel/core@8.0.1) balanced-match@1.0.2: {} @@ -9377,7 +9391,7 @@ snapshots: bare-events: 2.9.1 bare-path: 3.1.1 bare-stream: 2.13.3(bare-events@2.9.1) - bare-url: 2.4.5 + bare-url: 2.4.6 fast-fifo: 1.3.2 transitivePeerDependencies: - bare-abort-controller @@ -9395,29 +9409,22 @@ snapshots: transitivePeerDependencies: - react-native-b4a - bare-url@2.4.5: + bare-url@2.4.6: dependencies: bare-path: 3.1.1 base64-js@1.5.1: {} - baseline-browser-mapping@2.11.1: {} + baseline-browser-mapping@2.11.5: {} bcrypt-pbkdf@1.0.2: dependencies: tweetnacl: 0.14.5 - better-path-resolve@1.0.0: - dependencies: - is-windows: 1.0.2 - bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 - binary-extensions@2.3.0: - optional: true - bl@4.1.0: dependencies: buffer: 5.7.1 @@ -9436,7 +9443,7 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.1.2: + brace-expansion@2.1.3: dependencies: balanced-match: 1.0.2 @@ -9450,9 +9457,9 @@ snapshots: browserslist@4.28.7: dependencies: - baseline-browser-mapping: 2.11.1 + baseline-browser-mapping: 2.11.5 caniuse-lite: 1.0.30001806 - electron-to-chromium: 1.5.395 + electron-to-chromium: 1.5.397 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.7) @@ -9468,7 +9475,7 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 25.9.5 + '@types/node': 26.1.2 buffer@5.7.1: dependencies: @@ -9485,7 +9492,7 @@ snapshots: bun-types@1.3.14: dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 bundle-name@4.1.0: dependencies: @@ -9493,6 +9500,8 @@ snapshots: byline@5.0.0: {} + cac@7.0.0: {} + call-me-maybe@1.0.2: {} camelcase@5.3.1: {} @@ -9508,22 +9517,13 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - change-case@5.4.4: {} + chalk@5.6.2: {} - chardet@2.2.0: {} + change-case@5.4.4: {} - chokidar@3.6.0: + chokidar@5.0.0: dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - optional: true + readdirp: 5.0.0 chownr@1.1.4: {} @@ -9531,7 +9531,7 @@ snapshots: chrome-launcher@0.15.2: dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -9540,7 +9540,7 @@ snapshots: chromium-edge-launcher@0.2.0: dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 1.4.2 @@ -9579,8 +9579,12 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.0: {} + cluster-key-slot@1.1.2: {} + code-block-writer@13.0.3: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -9591,9 +9595,9 @@ snapshots: commander@12.1.0: {} - commander@2.20.3: {} + commander@14.0.3: {} - commander@6.2.1: {} + commander@2.20.3: {} commander@7.2.0: {} @@ -9672,7 +9676,7 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' - dataloader@1.4.0: {} + dataloader@2.2.3: {} debug@2.6.9: dependencies: @@ -9721,8 +9725,6 @@ snapshots: destroy@1.2.0: {} - detect-indent@6.1.0: {} - detect-libc@2.0.2: {} detect-libc@2.1.2: {} @@ -9743,11 +9745,11 @@ snapshots: dependencies: node-source-walk: 7.0.2 - detective-postcss@8.0.4(postcss@8.5.22): + detective-postcss@8.0.4(postcss@8.5.24): dependencies: is-url-superb: 4.0.0 - postcss: 8.5.22 - postcss-values-parser: 6.0.2(postcss@8.5.22) + postcss: 8.5.24 + postcss-values-parser: 6.0.2(postcss@8.5.24) detective-sass@6.0.2: dependencies: @@ -9785,10 +9787,6 @@ snapshots: diacritics-map@0.1.0: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - docker-compose@1.4.2: dependencies: yaml: 2.9.0 @@ -9802,7 +9800,7 @@ snapshots: transitivePeerDependencies: - supports-color - dockerode@4.0.12: + dockerode@5.0.1: dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.4 @@ -9810,7 +9808,6 @@ snapshots: docker-modem: 5.0.7 protobufjs: 7.6.5 tar-fs: 2.1.5 - uuid: 10.0.0 transitivePeerDependencies: - supports-color @@ -9822,8 +9819,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dotenv@8.6.0: {} - dprint@0.55.2: optionalDependencies: '@dprint/android-arm64': 0.55.2 @@ -9850,7 +9845,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.395: {} + electron-to-chromium@1.5.397: {} emoji-regex@10.6.0: {} @@ -9858,6 +9853,8 @@ snapshots: emoji-regex@9.2.2: {} + empathic@2.0.1: {} + encodeurl@1.0.2: {} encodeurl@2.0.0: {} @@ -9871,11 +9868,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 - entities@7.0.1: {} entities@8.0.0: {} @@ -9894,35 +9886,6 @@ snapshots: es6-promise@3.3.1: {} - esbuild@0.27.7: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.7 - '@esbuild/android-arm': 0.27.7 - '@esbuild/android-arm64': 0.27.7 - '@esbuild/android-x64': 0.27.7 - '@esbuild/darwin-arm64': 0.27.7 - '@esbuild/darwin-x64': 0.27.7 - '@esbuild/freebsd-arm64': 0.27.7 - '@esbuild/freebsd-x64': 0.27.7 - '@esbuild/linux-arm': 0.27.7 - '@esbuild/linux-arm64': 0.27.7 - '@esbuild/linux-ia32': 0.27.7 - '@esbuild/linux-loong64': 0.27.7 - '@esbuild/linux-mips64el': 0.27.7 - '@esbuild/linux-ppc64': 0.27.7 - '@esbuild/linux-riscv64': 0.27.7 - '@esbuild/linux-s390x': 0.27.7 - '@esbuild/linux-x64': 0.27.7 - '@esbuild/netbsd-arm64': 0.27.7 - '@esbuild/netbsd-x64': 0.27.7 - '@esbuild/openbsd-arm64': 0.27.7 - '@esbuild/openbsd-x64': 0.27.7 - '@esbuild/openharmony-arm64': 0.27.7 - '@esbuild/sunos-x64': 0.27.7 - '@esbuild/win32-arm64': 0.27.7 - '@esbuild/win32-ia32': 0.27.7 - '@esbuild/win32-x64': 0.27.7 - esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -10006,8 +9969,6 @@ snapshots: dependencies: is-extendable: 0.1.1 - extendable-error@0.1.7: {} - fake-indexeddb@6.2.5: {} fast-check@4.9.0: @@ -10018,23 +9979,21 @@ snapshots: fast-fifo@1.3.2: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-safe-stringify@2.1.1: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.2: {} - fastq@1.20.1: + fast-wrap-ansi@0.2.2: dependencies: - reusify: 1.1.0 + fast-string-width: 3.0.2 fb-dotslash@0.5.8: {} @@ -10096,8 +10055,6 @@ snapshots: make-dir: 3.1.0 pkg-dir: 4.2.0 - find-my-way-ts@0.1.6: {} - find-up@3.0.0: dependencies: locate-path: 3.0.0 @@ -10126,20 +10083,6 @@ snapshots: fs-constants@1.0.0: {} - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-readdir-recursive@1.1.0: {} - fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -10169,16 +10112,12 @@ snapshots: get-package-type@0.1.0: {} - get-port@7.2.0: {} + get-port@5.1.1: {} get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -10188,15 +10127,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.1.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 4.2.3 - minimatch: 10.2.5 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.2 - glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -10212,17 +10142,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - - globrex@0.1.2: {} - gonzales-pe@4.3.0: dependencies: minimist: 1.2.8 @@ -10244,7 +10163,7 @@ snapshots: happy-dom@20.11.1: dependencies: - '@types/node': 25.9.5 + '@types/node': 26.1.2 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -10279,7 +10198,7 @@ snapshots: html-encoding-sniffer@6.0.0: dependencies: - '@exodus/bytes': 1.15.0 + '@exodus/bytes': 1.15.1 transitivePeerDependencies: - '@noble/hashes' @@ -10321,14 +10240,14 @@ snapshots: ieee754@1.2.1: {} - ignore@5.3.2: {} - image-size@1.2.1: dependencies: queue: 6.0.2 immer@11.1.11: {} + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -10344,8 +10263,6 @@ snapshots: ini@1.3.8: {} - ini@7.0.0: {} - invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -10364,11 +10281,6 @@ snapshots: transitivePeerDependencies: - supports-color - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - optional: true - is-buffer@1.1.6: {} is-core-module@2.16.2: @@ -10385,14 +10297,8 @@ snapshots: dependencies: is-plain-object: 2.0.4 - is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - is-in-ssh@1.0.0: {} is-inside-container@1.0.0: @@ -10425,16 +10331,10 @@ snapshots: is-stream@2.0.1: {} - is-subdir@1.2.0: - dependencies: - better-path-resolve: 1.0.0 - is-unicode-supported@0.1.0: {} is-url-superb@4.0.0: {} - is-windows@1.0.2: {} - is-wsl@2.2.0: dependencies: is-docker: 2.2.1 @@ -10482,16 +10382,12 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.2.3: - dependencies: - '@isaacs/cliui': 9.0.0 - jest-environment-node@29.7.0: dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 26.1.1 + '@types/node': 26.1.2 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -10501,7 +10397,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 26.1.1 + '@types/node': 26.1.2 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -10528,7 +10424,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 26.1.1 + '@types/node': 26.1.2 jest-util: 29.7.0 jest-regex-util@29.6.3: {} @@ -10536,7 +10432,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 26.1.1 + '@types/node': 26.1.2 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10553,11 +10449,13 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 26.1.1 + '@types/node': 26.1.2 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 + jju@1.4.0: {} + js-base64@3.8.0: {} js-levenshtein@1.1.6: {} @@ -10577,10 +10475,6 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@4.3.0: - dependencies: - argparse: 2.0.1 - jsbi@4.3.2: {} jsc-safe-url@0.2.4: {} @@ -10608,28 +10502,28 @@ snapshots: transitivePeerDependencies: - supports-color - jsdom@29.1.1: + jsdom@30.0.0: dependencies: - '@asamuzakjp/css-color': 5.1.11 - '@asamuzakjp/dom-selector': 7.1.1 + '@asamuzakjp/css-color': 6.0.5 + '@asamuzakjp/dom-selector': 8.3.0 '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) - '@exodus/bytes': 1.15.0 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 css-tree: 3.2.1 data-urls: 7.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.3.6 + lru-cache: 11.5.2 parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.25.0 + tough-cookie: 6.0.2 + undici: 8.9.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 17.1.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -10642,9 +10536,7 @@ snapshots: json5@2.2.3: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 + jsonc-parser@3.3.1: {} jsonwebtoken@9.0.3: dependencies: @@ -10696,6 +10588,11 @@ snapshots: '@sqliteai/sqlite-vector-linux-x86_64-musl': 1.0.0 '@sqliteai/sqlite-vector-win32-x86_64': 1.0.0 + launch-editor@2.14.1: + dependencies: + picocolors: 1.1.1 + shell-quote: 1.10.0 + lazy-cache@2.0.2: dependencies: set-getter: 0.1.1 @@ -10728,55 +10625,54 @@ snapshots: transitivePeerDependencies: - supports-color - lightningcss-android-arm64@1.32.0: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-darwin-arm64@1.32.0: + lightningcss-darwin-arm64@1.33.0: optional: true - lightningcss-darwin-x64@1.32.0: + lightningcss-darwin-x64@1.33.0: optional: true - lightningcss-freebsd-x64@1.32.0: + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.32.0: + lightningcss-linux-arm64-gnu@1.33.0: optional: true - lightningcss-linux-arm64-musl@1.32.0: + lightningcss-linux-arm64-musl@1.33.0: optional: true - lightningcss-linux-x64-gnu@1.32.0: + lightningcss-linux-x64-gnu@1.33.0: optional: true - lightningcss-linux-x64-musl@1.32.0: + lightningcss-linux-x64-musl@1.33.0: optional: true - lightningcss-win32-arm64-msvc@1.32.0: + lightningcss-win32-arm64-msvc@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.32.0: + lightningcss-win32-x64-msvc@1.33.0: optional: true - lightningcss@1.32.0: + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 - optional: true + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 list-item@1.1.1: dependencies: @@ -10816,8 +10712,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.startcase@4.4.0: {} - lodash.template@4.18.1: dependencies: lodash._reinterpolate: 3.0.0 @@ -10842,12 +10736,8 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.2.1: {} - lru-cache@10.4.3: {} - lru-cache@11.3.6: {} - lru-cache@11.5.2: {} lru-cache@5.1.1: @@ -10916,8 +10806,6 @@ snapshots: merge-stream@2.0.0: {} - merge2@1.4.1: {} - metro-babel-transformer@0.83.7: dependencies: '@babel/core': 7.29.7 @@ -11133,11 +11021,11 @@ snapshots: minimatch@5.1.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.3 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.2 + brace-expansion: 2.1.3 minimist@1.2.8: {} @@ -11173,8 +11061,6 @@ snapshots: requirejs: 2.3.8 requirejs-config-file: 4.0.0 - mri@1.2.0: {} - mrmime@2.0.1: {} ms@2.0.0: {} @@ -11197,11 +11083,9 @@ snapshots: optionalDependencies: msgpackr-extract: 3.0.4 - multipasta@0.2.8: {} - - mysql2@3.22.6(@types/node@25.9.5): + mysql2@3.22.6(@types/node@26.1.2): dependencies: - '@types/node': 25.9.5 + '@types/node': 26.1.2 aws-ssl-profiles: 1.1.2 denque: 2.1.0 generate-function: 2.3.1 @@ -11365,22 +11249,27 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - outdent@0.5.0: {} - - oxlint@1.42.0: + oxlint@1.76.0: optionalDependencies: - '@oxlint/darwin-arm64': 1.42.0 - '@oxlint/darwin-x64': 1.42.0 - '@oxlint/linux-arm64-gnu': 1.42.0 - '@oxlint/linux-arm64-musl': 1.42.0 - '@oxlint/linux-x64-gnu': 1.42.0 - '@oxlint/linux-x64-musl': 1.42.0 - '@oxlint/win32-arm64': 1.42.0 - '@oxlint/win32-x64': 1.42.0 - - p-filter@2.1.0: - dependencies: - p-map: 2.1.0 + '@oxlint/binding-android-arm-eabi': 1.76.0 + '@oxlint/binding-android-arm64': 1.76.0 + '@oxlint/binding-darwin-arm64': 1.76.0 + '@oxlint/binding-darwin-x64': 1.76.0 + '@oxlint/binding-freebsd-x64': 1.76.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.76.0 + '@oxlint/binding-linux-arm-musleabihf': 1.76.0 + '@oxlint/binding-linux-arm64-gnu': 1.76.0 + '@oxlint/binding-linux-arm64-musl': 1.76.0 + '@oxlint/binding-linux-ppc64-gnu': 1.76.0 + '@oxlint/binding-linux-riscv64-gnu': 1.76.0 + '@oxlint/binding-linux-riscv64-musl': 1.76.0 + '@oxlint/binding-linux-s390x-gnu': 1.76.0 + '@oxlint/binding-linux-x64-gnu': 1.76.0 + '@oxlint/binding-linux-x64-musl': 1.76.0 + '@oxlint/binding-openharmony-arm64': 1.76.0 + '@oxlint/binding-win32-arm64-msvc': 1.76.0 + '@oxlint/binding-win32-ia32-msvc': 1.76.0 + '@oxlint/binding-win32-x64-msvc': 1.76.0 p-limit@2.3.0: dependencies: @@ -11394,15 +11283,11 @@ snapshots: dependencies: p-limit: 2.3.0 - p-map@2.1.0: {} - p-try@2.2.0: {} package-json-from-dist@1.0.1: {} - package-manager-detector@0.2.11: - dependencies: - quansync: 0.2.11 + package-manager-detector@1.8.0: {} parse-json@8.3.0: dependencies: @@ -11418,6 +11303,8 @@ snapshots: parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -11438,8 +11325,6 @@ snapshots: lru-cache: 11.5.2 minipass: 7.1.3 - path-type@4.0.0: {} - pathe@2.0.3: {} pg-cloudflare@1.4.0: @@ -11513,11 +11398,13 @@ snapshots: dependencies: find-up: 4.1.0 - playwright-core@1.61.1: {} + pkg-pr-new@0.0.78: {} + + playwright-core@1.62.0: {} - playwright@1.61.1: + playwright@1.62.0: dependencies: - playwright-core: 1.61.1 + playwright-core: 1.62.0 optionalDependencies: fsevents: 2.3.2 @@ -11525,14 +11412,14 @@ snapshots: pngjs@7.0.0: {} - postcss-values-parser@6.0.2(postcss@8.5.22): + postcss-values-parser@6.0.2(postcss@8.5.24): dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.5.22 + postcss: 8.5.24 quote-unquote: 1.0.0 - postcss@8.5.22: + postcss@8.5.24: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -11569,7 +11456,7 @@ snapshots: detective-amd: 6.1.0 detective-cjs: 6.1.1 detective-es6: 5.0.2 - detective-postcss: 8.0.4(postcss@8.5.22) + detective-postcss: 8.0.4(postcss@8.5.24) detective-sass: 6.0.2 detective-scss: 5.0.2 detective-stylus: 5.0.1 @@ -11577,13 +11464,11 @@ snapshots: detective-vue2: 2.3.0(typescript@5.9.3) module-definition: 6.0.2 node-source-walk: 7.0.2 - postcss: 8.5.22 + postcss: 8.5.24 typescript: 5.9.3 transitivePeerDependencies: - supports-color - prettier@2.8.8: {} - prettier@3.9.6: {} pretty-format@27.5.1: @@ -11636,7 +11521,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.2 - '@types/node': 26.1.1 + '@types/node': 26.1.2 long: 5.3.2 pump@3.0.4: @@ -11648,10 +11533,6 @@ snapshots: pure-rand@8.4.1: {} - quansync@0.2.11: {} - - queue-microtask@1.2.3: {} - queue@6.0.2: dependencies: inherits: 2.0.4 @@ -11694,20 +11575,20 @@ snapshots: react-is@18.3.1: {} - react-native@0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7): + react-native@0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.83.0 - '@react-native/codegen': 0.83.0(@babel/core@7.29.7) + '@react-native/codegen': 0.83.0(@babel/core@8.0.1) '@react-native/community-cli-plugin': 0.83.0 '@react-native/gradle-plugin': 0.83.0 '@react-native/js-polyfills': 0.83.0 '@react-native/normalize-colors': 0.83.0 - '@react-native/virtualized-lists': 0.83.0(@types/react@19.2.17)(react-native@0.83.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) + '@react-native/virtualized-lists': 0.83.0(@types/react@19.2.17)(react-native@0.83.0(@babel/core@8.0.1)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.29.7) + babel-jest: 29.7.0(@babel/core@8.0.1) babel-plugin-syntax-hermes-parser: 0.32.0 base64-js: 1.5.1 commander: 12.1.0 @@ -11746,13 +11627,6 @@ snapshots: react@19.2.7: {} - read-yaml-file@1.1.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.15.0 - pify: 4.0.1 - strip-bom: 3.0.0 - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -11781,10 +11655,7 @@ snapshots: dependencies: minimatch: 5.1.9 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - optional: true + readdirp@5.0.0: {} recast@0.23.12: dependencies: @@ -11849,8 +11720,6 @@ snapshots: retry@0.12.0: {} - reusify@1.1.0: {} - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -11875,39 +11744,38 @@ snapshots: '@rolldown/binding-wasm32-wasi': 1.1.5 '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 - optional: true - rollup-plugin-bundle-stats@4.22.2(core-js@3.47.0)(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + rollup-plugin-bundle-stats@4.22.2(core-js@3.47.0)(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): dependencies: '@bundle-stats/cli-utils': 4.22.2(core-js@3.47.0) - rollup-plugin-stats: 2.1.2(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) - rollup-plugin-webpack-stats: 3.1.2(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + rollup-plugin-stats: 2.1.2(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + rollup-plugin-webpack-stats: 3.1.2(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) tslib: 2.8.1 optionalDependencies: rolldown: 1.1.5 - rollup: 4.62.2 - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) + rollup: 4.62.3 + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - core-js - rollup-plugin-esbuild@6.2.1(esbuild@0.28.1)(rollup@4.62.2): + rollup-plugin-esbuild@6.2.1(esbuild@0.28.1)(rollup@4.62.3): dependencies: debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 esbuild: 0.28.1 get-tsconfig: 4.14.0 - rollup: 4.62.2 + rollup: 4.62.3 unplugin-utils: 0.2.5 transitivePeerDependencies: - supports-color - rollup-plugin-stats@2.1.2(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + rollup-plugin-stats@2.1.2(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): optionalDependencies: rolldown: 1.1.5 - rollup: 4.62.2 - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) + rollup: 4.62.3 + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.2): + rollup-plugin-visualizer@7.0.1(rolldown@1.1.5)(rollup@4.62.3): dependencies: open: 11.0.0 picomatch: 4.0.5 @@ -11915,53 +11783,49 @@ snapshots: yargs: 18.0.0 optionalDependencies: rolldown: 1.1.5 - rollup: 4.62.2 + rollup: 4.62.3 - rollup-plugin-webpack-stats@3.1.2(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + rollup-plugin-webpack-stats@3.1.2(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): dependencies: - rollup-plugin-stats: 2.1.2(rolldown@1.1.5)(rollup@4.62.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + rollup-plugin-stats: 2.1.2(rolldown@1.1.5)(rollup@4.62.3)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) optionalDependencies: rolldown: 1.1.5 - rollup: 4.62.2 - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) + rollup: 4.62.3 + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - rollup@4.62.2: + rollup@4.62.3: dependencies: '@types/estree': 1.0.9 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.62.2 - '@rollup/rollup-android-arm64': 4.62.2 - '@rollup/rollup-darwin-arm64': 4.62.2 - '@rollup/rollup-darwin-x64': 4.62.2 - '@rollup/rollup-freebsd-arm64': 4.62.2 - '@rollup/rollup-freebsd-x64': 4.62.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 - '@rollup/rollup-linux-arm-musleabihf': 4.62.2 - '@rollup/rollup-linux-arm64-gnu': 4.62.2 - '@rollup/rollup-linux-arm64-musl': 4.62.2 - '@rollup/rollup-linux-loong64-gnu': 4.62.2 - '@rollup/rollup-linux-loong64-musl': 4.62.2 - '@rollup/rollup-linux-ppc64-gnu': 4.62.2 - '@rollup/rollup-linux-ppc64-musl': 4.62.2 - '@rollup/rollup-linux-riscv64-gnu': 4.62.2 - '@rollup/rollup-linux-riscv64-musl': 4.62.2 - '@rollup/rollup-linux-s390x-gnu': 4.62.2 - '@rollup/rollup-linux-x64-gnu': 4.62.2 - '@rollup/rollup-linux-x64-musl': 4.62.2 - '@rollup/rollup-openbsd-x64': 4.62.2 - '@rollup/rollup-openharmony-arm64': 4.62.2 - '@rollup/rollup-win32-arm64-msvc': 4.62.2 - '@rollup/rollup-win32-ia32-msvc': 4.62.2 - '@rollup/rollup-win32-x64-gnu': 4.62.2 - '@rollup/rollup-win32-x64-msvc': 4.62.2 + '@rollup/rollup-android-arm-eabi': 4.62.3 + '@rollup/rollup-android-arm64': 4.62.3 + '@rollup/rollup-darwin-arm64': 4.62.3 + '@rollup/rollup-darwin-x64': 4.62.3 + '@rollup/rollup-freebsd-arm64': 4.62.3 + '@rollup/rollup-freebsd-x64': 4.62.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.3 + '@rollup/rollup-linux-arm-musleabihf': 4.62.3 + '@rollup/rollup-linux-arm64-gnu': 4.62.3 + '@rollup/rollup-linux-arm64-musl': 4.62.3 + '@rollup/rollup-linux-loong64-gnu': 4.62.3 + '@rollup/rollup-linux-loong64-musl': 4.62.3 + '@rollup/rollup-linux-ppc64-gnu': 4.62.3 + '@rollup/rollup-linux-ppc64-musl': 4.62.3 + '@rollup/rollup-linux-riscv64-gnu': 4.62.3 + '@rollup/rollup-linux-riscv64-musl': 4.62.3 + '@rollup/rollup-linux-s390x-gnu': 4.62.3 + '@rollup/rollup-linux-x64-gnu': 4.62.3 + '@rollup/rollup-linux-x64-musl': 4.62.3 + '@rollup/rollup-openbsd-x64': 4.62.3 + '@rollup/rollup-openharmony-arm64': 4.62.3 + '@rollup/rollup-win32-arm64-msvc': 4.62.3 + '@rollup/rollup-win32-ia32-msvc': 4.62.3 + '@rollup/rollup-win32-x64-gnu': 4.62.3 + '@rollup/rollup-win32-x64-msvc': 4.62.3 fsevents: 2.3.3 run-applescript@7.1.0: {} - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -12111,10 +11975,12 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 - slash@2.0.0: {} + sisteransi@1.0.5: {} slash@3.0.0: {} + slash@5.1.0: {} + smob@1.6.2: {} solid-js@1.9.14: @@ -12136,11 +12002,6 @@ snapshots: source-map@0.7.6: {} - spawndamnit@3.0.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - split-ca@1.0.1: {} split2@4.2.0: {} @@ -12347,7 +12208,7 @@ snapshots: '@azure/identity': 4.13.1 '@azure/keyvault-keys': 4.10.0(@azure/core-client@1.10.1) '@js-joda/core': 5.7.0 - '@types/node': 26.1.1 + '@types/node': 26.1.2 bl: 6.1.6 iconv-lite: 0.7.2 js-md4: 0.3.2 @@ -12364,8 +12225,6 @@ snapshots: - bare-abort-controller - react-native-b4a - term-size@2.2.1: {} - terser@5.49.0: dependencies: '@jridgewell/source-map': 0.3.11 @@ -12379,7 +12238,7 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 - testcontainers@11.14.0: + testcontainers@12.0.4: dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 @@ -12388,14 +12247,37 @@ snapshots: byline: 5.0.0 debug: 4.4.3(supports-color@10.2.2) docker-compose: 1.4.2 - dockerode: 4.0.12 - get-port: 7.2.0 + dockerode: 5.0.1 + get-port: 5.1.1 proper-lockfile: 4.1.2 properties-reader: 3.0.1 ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.3 tmp: 0.2.7 - undici: 7.28.0 + undici: 8.9.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + testcontainers@12.1.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3(supports-color@10.2.2) + docker-compose: 1.4.2 + dockerode: 5.0.1 + get-port: 5.1.1 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.3 + tmp: 0.2.7 + undici: 8.9.0 transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -12428,15 +12310,13 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - tinyrainbow@2.0.0: {} - tinyrainbow@3.1.0: {} - tldts-core@7.0.30: {} + tldts-core@7.4.9: {} - tldts@7.0.30: + tldts@7.4.9: dependencies: - tldts-core: 7.0.30 + tldts-core: 7.4.9 tmp@0.2.7: {} @@ -12452,13 +12332,11 @@ snapshots: toidentifier@1.0.1: {} - toml@4.1.2: {} - totalist@3.0.1: {} - tough-cookie@6.0.1: + tough-cookie@6.0.2: dependencies: - tldts: 7.0.30 + tldts: 7.4.9 tr46@0.0.3: {} @@ -12497,9 +12375,14 @@ snapshots: '@ts-graphviz/common': 2.1.5 '@ts-graphviz/core': 2.0.7 - tsconfck@3.1.6(typescript@7.0.2): + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + tsconfck@3.1.6(typescript@6.0.3): optionalDependencies: - typescript: 7.0.2 + typescript: 6.0.3 tsconfig-paths@4.2.0: dependencies: @@ -12513,10 +12396,9 @@ snapshots: optionalDependencies: typescript: 7.0.2 - tsx@4.21.0: + tsx@4.23.1: dependencies: - esbuild: 0.27.7 - get-tsconfig: 4.14.0 + esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 @@ -12559,17 +12441,13 @@ snapshots: undici-types@5.26.5: {} - undici-types@7.24.6: {} - undici-types@8.3.0: {} - undici@7.25.0: {} - undici@7.28.0: {} undici@8.7.0: {} - universalify@0.1.2: {} + undici@8.9.0: {} unpipe@1.0.0: {} @@ -12590,8 +12468,6 @@ snapshots: utils-merge@1.0.1: {} - uuid@10.0.0: {} - uuid@14.0.1: {} uuid@8.3.2: {} @@ -12600,90 +12476,30 @@ snapshots: optionalDependencies: typescript: 7.0.2 - vite-tsconfig-paths@6.1.1(typescript@7.0.2)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): - dependencies: - debug: 4.4.3(supports-color@10.2.2) - globrex: 0.1.2 - tsconfck: 3.1.6(typescript@7.0.2) - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - transitivePeerDependencies: - - supports-color - - typescript - - vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0): + vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: - esbuild: 0.28.1 - fdir: 6.5.0(picomatch@4.0.5) + lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.22 - rollup: 4.62.2 + postcss: 8.5.24 + rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.9.5 - fsevents: 2.3.3 - lightningcss: 1.32.0 - terser: 5.49.0 - tsx: 4.21.0 - yaml: 2.9.0 - - vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: + '@types/node': 26.1.2 esbuild: 0.28.1 - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - postcss: 8.5.22 - rollup: 4.62.2 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 26.1.1 fsevents: 2.3.3 - lightningcss: 1.32.0 terser: 5.49.0 - tsx: 4.21.0 + tsx: 4.23.1 yaml: 2.9.0 - vitest-websocket-mock@0.5.0(vitest@4.1.10): + vitest-websocket-mock@0.7.0(vitest@4.1.10): dependencies: - '@vitest/utils': 3.2.7 mock-socket: 9.3.1 - vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) - - vitest@4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 7.3.6(@types/node@25.9.5)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@edge-runtime/vm': 5.0.0 - '@opentelemetry/api': 1.9.1 - '@types/node': 25.9.5 - '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) - happy-dom: 20.11.1 - jsdom: 29.1.1 - transitivePeerDependencies: - - msw + vitest: 4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - vitest@4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@29.1.1)(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)): + vitest@4.1.10(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.1)(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@30.0.0)(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -12700,15 +12516,15 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 7.3.6(@types/node@26.1.1)(lightningcss@1.32.0)(terser@5.49.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.5(@types/node@26.1.2)(esbuild@0.28.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@edge-runtime/vm': 5.0.0 '@opentelemetry/api': 1.9.1 - '@types/node': 26.1.1 + '@types/node': 26.1.2 '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) happy-dom: 20.11.1 - jsdom: 29.1.1 + jsdom: 30.0.0 transitivePeerDependencies: - msw @@ -12750,7 +12566,15 @@ snapshots: whatwg-url@16.0.1: dependencies: - '@exodus/bytes': 1.15.0 + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.0: + dependencies: + '@exodus/bytes': 1.15.1 tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: diff --git a/.context/effect/pnpm-workspace.yaml b/.context/effect/pnpm-workspace.yaml index 5aa895687..6680214cd 100644 --- a/.context/effect/pnpm-workspace.yaml +++ b/.context/effect/pnpm-workspace.yaml @@ -5,6 +5,7 @@ packages: - packages/* - packages/ai/* - packages/atom/* + - packages/platform/* - packages/sql/* - packages/tools/* - examples/* @@ -30,5 +31,5 @@ onlyBuiltDependencies: - tree-sitter-typescript patchedDependencies: - '@changesets/assemble-release-plan': patches/@changesets__assemble-release-plan.patch - '@changesets/get-github-info': patches/@changesets__get-github-info.patch + '@changesets/get-github-info@1.0.0-next.4': patches/@changesets__get-github-info@1.0.0-next.4.patch + '@changesets/read@1.0.0-next.10': patches/@changesets__read@1.0.0-next.10.patch diff --git a/.context/effect/scratchpad/tsconfig.json b/.context/effect/scratchpad/tsconfig.json index 96cfc9e67..cf5be2e5f 100644 --- a/.context/effect/scratchpad/tsconfig.json +++ b/.context/effect/scratchpad/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../tsconfig.base.json", "include": ["**/*.ts"], "compilerOptions": { diff --git a/.context/effect/scripts/circular.mjs b/.context/effect/scripts/circular.mjs index bd75fe5b7..39fead0e6 100644 --- a/.context/effect/scripts/circular.mjs +++ b/.context/effect/scripts/circular.mjs @@ -3,7 +3,7 @@ import * as glob from "glob" import madge from "madge" madge( - glob.globSync(["packages/*/src/**/*.ts", "packages/ai/*/src/**/*.ts"], { + glob.globSync(["packages/*/src/**/*.ts", "packages/ai/*/src/**/*.ts", "packages/platform/*/src/**/*.ts"], { ignore: [ "packages/sql-sqlite-bun/**", "packages/experimental/src/EventLogServer/Cloudflare.ts" diff --git a/.context/effect/scripts/clean.mjs b/.context/effect/scripts/clean.mjs index b49e844ed..aad0b637e 100644 --- a/.context/effect/scripts/clean.mjs +++ b/.context/effect/scripts/clean.mjs @@ -1,9 +1,12 @@ import * as Glob from "glob" import * as Fs from "node:fs" -const dirs = [".", ...Glob.sync("packages/*/"), ...Glob.sync("packages/sql/*/"), ...Glob.sync("packages/tools/*/"), ...Glob.sync("packages/ai/*/"), ...Glob.sync("packages/atom/*/")] +const dirs = [".", ...Glob.sync("packages/*/"), ...Glob.sync("packages/sql/*/"), ...Glob.sync("packages/tools/*/"), ...Glob.sync("packages/ai/*/"), ...Glob.sync("packages/atom/*/"), ...Glob.sync("packages/platform/*/")] dirs.forEach((pkg) => { const files = [".tsbuildinfo", "tsconfig.tsbuildinfo", "tsconfig.fixtures.tsbuildinfo", "tsconfig.src.tsbuildinfo", "docs", "build", "dist", "coverage"] + if (pkg !== ".") { + files.push("AGENTS.md", "CLAUDE.md", "ai-docs") + } files.forEach((file) => { if (pkg === "." && file === "docs") { diff --git a/.context/effect/scripts/copy-ai-docs.mjs b/.context/effect/scripts/copy-ai-docs.mjs new file mode 100644 index 000000000..ada86e039 --- /dev/null +++ b/.context/effect/scripts/copy-ai-docs.mjs @@ -0,0 +1,33 @@ +import * as Fs from "node:fs" +import * as Glob from "glob" +import * as Path from "node:path" + +const source = "LLMS.md" +const aiDocsSource = "ai-docs" +const packageFiles = ["AGENTS.md", "CLAUDE.md", "ai-docs/**/*"] + +const includeAiDocs = (source) => { + const [root] = Path.relative(aiDocsSource, source).split(Path.sep) + return root !== "dist" && root !== "node_modules" +} + +for (const packageJsonPath of Glob.globSync("packages/{*,*/*}/package.json")) { + const packageJson = JSON.parse(Fs.readFileSync(packageJsonPath, "utf8")) + if (packageJson.private) { + continue + } + + const packageDirectory = Path.dirname(packageJsonPath) + for (const file of packageFiles) { + if (!packageJson.files?.includes(file)) { + throw new Error(`${packageJsonPath} must include ${file} in its files list`) + } + } + + Fs.copyFileSync(source, Path.join(packageDirectory, "AGENTS.md")) + Fs.copyFileSync(source, Path.join(packageDirectory, "CLAUDE.md")) + + const aiDocsTarget = Path.join(packageDirectory, aiDocsSource) + Fs.rmSync(aiDocsTarget, { recursive: true, force: true }) + Fs.cpSync(aiDocsSource, aiDocsTarget, { recursive: true, filter: includeAiDocs }) +} diff --git a/.context/effect/scripts/docs.mjs b/.context/effect/scripts/docs.mjs deleted file mode 100644 index b9fe97698..000000000 --- a/.context/effect/scripts/docs.mjs +++ /dev/null @@ -1,64 +0,0 @@ -import * as Fs from "node:fs" -import * as Path from "node:path" - -function packages() { - return Fs.readdirSync("packages") - .filter((_) => Fs.existsSync(Path.join("packages", _, "docs/modules"))) -} - -function pkgName(pkg) { - const packageJson = Fs.readFileSync( - Path.join("packages", pkg, "package.json") - ) - return JSON.parse(packageJson).name -} - -function copyFiles(pkg) { - const name = pkgName(pkg) - const docs = Path.join("packages", pkg, "docs/modules") - const dest = Path.join("docs", pkg) - const files = Fs.readdirSync(docs, { withFileTypes: true }) - - function handleFiles(root, files) { - for (const file of files) { - const path = Path.join(docs, root, file.name) - const destPath = Path.join(dest, root, file.name) - - if (file.isDirectory()) { - Fs.mkdirSync(destPath, { recursive: true }) - handleFiles(Path.join(root, file.name), Fs.readdirSync(path, { withFileTypes: true })) - continue - } - - const content = Fs.readFileSync(path, "utf8").replace( - /^parent: Modules$/m, - `parent: "${name}"` - ) - Fs.writeFileSync(destPath, content) - } - } - - Fs.rmSync(dest, { recursive: true, force: true }) - Fs.mkdirSync(dest, { recursive: true }) - handleFiles("", files) -} - -function generateIndex(pkg, order) { - const name = pkgName(pkg) - const content = `--- -title: "${name}" -has_children: true -permalink: /docs/${pkg} -nav_order: ${order} ---- -` - - Fs.writeFileSync(Path.join("docs", pkg, "index.md"), content) -} - -packages().forEach((pkg, i) => { - Fs.rmSync(Path.join("docs", pkg), { recursive: true, force: true }) - Fs.mkdirSync(Path.join("docs", pkg), { recursive: true }) - copyFiles(pkg) - generateIndex(pkg, i + 2) -}) diff --git a/.context/effect/scripts/set-strip-internal.mjs b/.context/effect/scripts/set-strip-internal.mjs new file mode 100644 index 000000000..a7533f75d --- /dev/null +++ b/.context/effect/scripts/set-strip-internal.mjs @@ -0,0 +1,11 @@ +import * as Fs from "node:fs" + +const path = new URL("../tsconfig.base.json", import.meta.url) +const contents = Fs.readFileSync(path, "utf8") +const updated = contents.replace('"stripInternal": false', '"stripInternal": true') + +if (contents === updated) { + throw new Error("Could not enable stripInternal in tsconfig.base.json") +} + +Fs.writeFileSync(path, updated) diff --git a/.context/effect/scripts/setup-agents.mjs b/.context/effect/scripts/setup-agents.mjs new file mode 100644 index 000000000..b6537a05a --- /dev/null +++ b/.context/effect/scripts/setup-agents.mjs @@ -0,0 +1,13 @@ +import * as Fs from "node:fs" + +const source = ".agents/AGENTS.md" +const target = "AGENTS.md" + +try { + Fs.lstatSync(target) +} catch (error) { + if (error?.code !== "ENOENT") { + throw error + } + Fs.symlinkSync(source, target) +} diff --git a/.context/effect/scripts/tsconfig.json b/.context/effect/scripts/tsconfig.json index f3d7810ab..fe5cecfba 100644 --- a/.context/effect/scripts/tsconfig.json +++ b/.context/effect/scripts/tsconfig.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "../tsconfig.base.json", "include": ["**/*.ts"], "compilerOptions": { diff --git a/.context/effect/scripts/version.mjs b/.context/effect/scripts/version.mjs deleted file mode 100644 index 6b95f8d88..000000000 --- a/.context/effect/scripts/version.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import * as Fs from "node:fs" -import Package from "../packages/effect/package.json" with { type: "json" } - -const tpl = Fs.readFileSync("./scripts/version.template.txt").toString("utf8") - -Fs.writeFileSync( - "packages/effect/src/internal/version.ts", - tpl.replace(/VERSION/g, Package.version) -) diff --git a/.context/effect/scripts/version.template.txt b/.context/effect/scripts/version.template.txt deleted file mode 100644 index 7e41a1e92..000000000 --- a/.context/effect/scripts/version.template.txt +++ /dev/null @@ -1,2 +0,0 @@ -export const version: version = "VERSION" -export type version = "VERSION" diff --git a/.context/effect/scripts/worktree-setup.sh b/.context/effect/scripts/worktree-setup.sh index 081f99d1f..26a8924a1 100755 --- a/.context/effect/scripts/worktree-setup.sh +++ b/.context/effect/scripts/worktree-setup.sh @@ -8,7 +8,9 @@ pnpm install # setup repositories git clone --depth 1 https://github.com/tstyche/tstyche.org.git .repos/tstyche.org -cat << EOF >> AGENTS.md +cp .agents/AGENTS.md AGENTS.md.tmp + +cat << EOF >> AGENTS.md.tmp ## Learning about "effect" v3 @@ -22,3 +24,5 @@ the website repository here: \`.repos/tstyche.org\` EOF + +mv AGENTS.md.tmp AGENTS.md diff --git a/.context/effect/tsconfig.base.json b/.context/effect/tsconfig.base.json index 4fc19a51b..0a0a036e2 100644 --- a/.context/effect/tsconfig.base.json +++ b/.context/effect/tsconfig.base.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "include": [], "compilerOptions": { "outDir": "${configDir}/dist", diff --git a/.context/effect/tsconfig.json b/.context/effect/tsconfig.json index b2a9c688e..8d474c4cb 100644 --- a/.context/effect/tsconfig.json +++ b/.context/effect/tsconfig.json @@ -1,131 +1,8 @@ { - "$schema": "http://json.schemastore.org/tsconfig", - "extends": "./tsconfig.base.json", - "references": [{ "path": "tsconfig.packages.json" }], - "include": [ - "**/vitest.*.ts", // All vitest config files across all packages - "./bundle/**/*.ts", // Bundle size test files - "./packages/*/test/**/*.ts", - "./packages/*/test/**/*.json", - "./packages/*/typetest/**/*.ts", - "./packages/*/benchmark/**/*.ts", - "./packages/ai/*/test/**/*.ts", - "./packages/ai/*/test/**/*.json", - "./packages/ai/*/typetest/**/*.ts", - "./packages/ai/*/benchmark/**/*.ts", - "./packages/atom/*/test/**/*.ts", - "./packages/atom/*/test/**/*.json", - "./packages/atom/*/typetest/**/*.ts", - "./packages/atom/*/benchmark/**/*.ts", - "./packages/sql/*/test/**/*.ts", - "./packages/sql/*/test/**/*.json", - "./packages/sql/*/typetest/**/*.ts", - "./packages/sql/*/benchmark/**/*.ts", - "./packages/tools/*/test/**/*.ts", - "./packages/tools/*/test/**/*.json", - "./packages/tools/*/typetest/**/*.ts", - "./packages/tools/*/benchmark/**/*.ts" - ], - "compilerOptions": { - "rootDir": ".", - "noEmit": true, - "erasableSyntaxOnly": false, - "resolveJsonModule": true, - "types": ["node"], - "paths": { - // These path aliases allow us to load internal modules in tests and to use other workspace packages in tests - // that would otherwise be considered cyclic dependencies (e.g. loading `@effect/sql-sqlite-node` in the test - // suite of `@effect/platform-node` which itself depends on `@effect/platform-node`). - "effect/*": ["./packages/effect/src/*.ts"], - "@effect/ai-anthropic": ["./packages/ai/anthropic/src/index.ts"], - "@effect/ai-anthropic/*": ["./packages/ai/anthropic/src/*.ts"], - "@effect/ai-openai-compat": ["./packages/ai/openai-compat/src/index.ts"], - "@effect/ai-openai-compat/*": ["./packages/ai/openai-compat/src/*.ts"], - "@effect/ai-openai": ["./packages/ai/openai/src/index.ts"], - "@effect/ai-openai/*": ["./packages/ai/openai/src/*.ts"], - "@effect/ai-openrouter": ["./packages/ai/openrouter/src/index.ts"], - "@effect/ai-openrouter/*": ["./packages/ai/openrouter/src/*.ts"], - "@effect/atom-react": ["./packages/atom/react/src/index.ts"], - "@effect/atom-react/*": ["./packages/atom/react/src/*.ts"], - "@effect/atom-vue": ["./packages/atom/vue/src/index.ts"], - "@effect/atom-vue/*": ["./packages/atom/vue/src/*.ts"], - "@effect/atom-solid": ["./packages/atom/solid/src/index.ts"], - "@effect/atom-solid/*": ["./packages/atom/solid/src/*.ts"], - "@effect/opentelemetry": ["./packages/opentelemetry/src/index.ts"], - "@effect/opentelemetry/*": ["./packages/opentelemetry/src/*.ts"], - "@effect/platform-browser": ["./packages/platform-browser/src/index.ts"], - "@effect/platform-browser/*": ["./packages/platform-browser/src/*.ts"], - "@effect/platform-bun": ["./packages/platform-bun/src/index.ts"], - "@effect/platform-bun/*": ["./packages/platform-bun/src/*.ts"], - "@effect/platform-node": ["./packages/platform-node/src/index.ts"], - "@effect/platform-node/*": ["./packages/platform-node/src/*.ts"], - "@effect/platform-node-shared": ["./packages/platform-node-shared/src/index.ts"], - "@effect/platform-node-shared/*": ["./packages/platform-node-shared/src/*.ts"], - "@effect/sql-clickhouse": ["./packages/sql/clickhouse/src/index.ts"], - "@effect/sql-clickhouse/*": ["./packages/sql/clickhouse/src/*.ts"], - "@effect/sql-d1": ["./packages/sql/d1/src/index.ts"], - "@effect/sql-d1/*": ["./packages/sql/d1/src/*.ts"], - "@effect/sql-libsql": ["./packages/sql/libsql/src/index.ts"], - "@effect/sql-libsql/*": ["./packages/sql/libsql/src/*.ts"], - "@effect/sql-mssql": ["./packages/sql/mssql/src/index.ts"], - "@effect/sql-mssql/*": ["./packages/sql/mssql/src/*.ts"], - "@effect/sql-mysql2": ["./packages/sql/mysql2/src/index.ts"], - "@effect/sql-mysql2/*": ["./packages/sql/mysql2/src/*.ts"], - "@effect/sql-pg": ["./packages/sql/pg/src/index.ts"], - "@effect/sql-pg/*": ["./packages/sql/pg/src/*.ts"], - "@effect/sql-pglite": ["./packages/sql/pglite/src/index.ts"], - "@effect/sql-pglite/*": ["./packages/sql/pglite/src/*.ts"], - "@effect/sql-sqlite-bun": ["./packages/sql/sqlite-bun/src/index.ts"], - "@effect/sql-sqlite-bun/*": ["./packages/sql/sqlite-bun/src/*.ts"], - "@effect/sql-sqlite-do": ["./packages/sql/sqlite-do/src/index.ts"], - "@effect/sql-sqlite-do/*": ["./packages/sql/sqlite-do/src/*.ts"], - "@effect/sql-sqlite-node": ["./packages/sql/sqlite-node/src/index.ts"], - "@effect/sql-sqlite-node/*": ["./packages/sql/sqlite-node/src/*.ts"], - "@effect/sql-sqlite-react-native": ["./packages/sql/sqlite-react-native/src/index.ts"], - "@effect/sql-sqlite-react-native/*": ["./packages/sql/sqlite-react-native/src/*.ts"], - "@effect/sql-sqlite-wasm": ["./packages/sql/sqlite-wasm/src/index.ts"], - "@effect/sql-sqlite-wasm/*": ["./packages/sql/sqlite-wasm/src/*.ts"], - "@effect/ai-codegen": ["./packages/tools/ai-codegen/src/index.ts"], - "@effect/ai-codegen/*": ["./packages/tools/ai-codegen/src/*.ts"], - "@effect/ai-docgen": ["./packages/tools/ai-docgen/src/index.ts"], - "@effect/ai-docgen/*": ["./packages/tools/ai-docgen/src/*.ts"], - "@effect/bundle/*": ["./packages/tools/bundle/src/*.ts"], - "@effect/openapi-generator": ["./packages/tools/openapi-generator/src/index.ts"], - "@effect/openapi-generator/*": ["./packages/tools/openapi-generator/src/*.ts"], - "@effect/oxc": ["./packages/tools/oxc/src/index.ts"], - "@effect/oxc/*": ["./packages/tools/oxc/src/*.ts"], - "@effect/tools-utils": ["./packages/tools/utils/src/index.ts"], - "@effect/tools-utils/*": ["./packages/tools/utils/src/*.ts"], - "@effect/vitest": ["./packages/vitest/src/index.ts"], - "@effect/vitest/*": ["./packages/vitest/src/*.ts"], - // TODO: This is a special alias used by some tests. We should try to get rid of it. - "effect-test/*": ["./packages/effect/test/*.ts"] - }, - "plugins": [{ - "name": "@effect/language-service", - "namespaceImportPackages": [], - "includeSuggestionsInTsc": false, - "ignoreEffectWarningsInTscExitCode": true, - "ignoreEffectErrorsInTscExitCode": true, - "overrides": [ - { - "include": [ - "./packages/*/typetest/**/*.ts", - "./packages/ai/*/typetest/**/*.ts", - "./packages/tools/*/typetest/**/*.ts", - "./packages/atom/*/typetest/**/*.ts" - ], - "options": { - "diagnosticSeverity": { - "floatingEffect": "off" - } - } - } - ], - "diagnosticSeverity": { - "unknownInEffectCatch": "off", - "multipleEffectProvide": "off" - } - }] - } + "$schema": "https://json.schemastore.org/tsconfig", + "include": [], + "references": [ + { "path": "tsconfig.packages.json" }, + { "path": "tsconfig.tests.json" } + ] } diff --git a/.context/effect/tsconfig.packages.json b/.context/effect/tsconfig.packages.json index 229e31251..fb54b67c7 100644 --- a/.context/effect/tsconfig.packages.json +++ b/.context/effect/tsconfig.packages.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/tsconfig", + "$schema": "https://json.schemastore.org/tsconfig", "extends": "./tsconfig.base.json", "include": [], "references": [ @@ -13,10 +13,11 @@ { "path": "packages/atom/vue" }, { "path": "packages/atom/solid" }, { "path": "packages/opentelemetry" }, - { "path": "packages/platform-browser" }, - { "path": "packages/platform-bun" }, - { "path": "packages/platform-node" }, - { "path": "packages/platform-node-shared" }, + { "path": "packages/platform/browser" }, + { "path": "packages/platform/bun" }, + { "path": "packages/platform/deno" }, + { "path": "packages/platform/node" }, + { "path": "packages/platform/node-shared" }, { "path": "packages/sql/clickhouse" }, { "path": "packages/sql/d1" }, { "path": "packages/sql/libsql" }, @@ -30,7 +31,10 @@ { "path": "packages/sql/sqlite-react-native" }, { "path": "packages/sql/sqlite-wasm" }, { "path": "packages/tools/ai-codegen" }, + { "path": "packages/tools/api-diff" }, { "path": "packages/tools/bundle" }, + { "path": "packages/tools/docgen" }, + { "path": "packages/tools/doctest" }, { "path": "packages/tools/openapi-generator" }, { "path": "packages/tools/jsdocs" }, { "path": "packages/tools/oxc" }, diff --git a/.context/effect/tsconfig.tests.json b/.context/effect/tsconfig.tests.json new file mode 100644 index 000000000..943e0af02 --- /dev/null +++ b/.context/effect/tsconfig.tests.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.base.json", + "references": [{ "path": "tsconfig.packages.json" }], + "include": [ + "**/vitest.*.ts", // All vitest config files across all packages + "./bundle/**/*.ts", // Bundle size test files + "./packages/*/test/**/*.ts", + "./packages/*/test/**/*.json", + "./packages/*/typetest/**/*.ts", + "./packages/*/benchmark/**/*.ts", + "./packages/**/test/**/*.ts", + "./packages/**/test/**/*.json", + "./packages/**/typetest/**/*.ts", + "./packages/**/benchmark/**/*.ts" + ], + "exclude": ["**/node_modules/**", "**/dist/**", "./packages/platform/deno/**"], + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "erasableSyntaxOnly": false, + "resolveJsonModule": true, + "types": ["node"], + "paths": { + // These path aliases allow us to load internal modules in tests and to use other workspace packages in tests + // that would otherwise be considered cyclic dependencies (e.g. loading `@effect/sql-sqlite-node` in the test + // suite of `@effect/platform-node` which itself depends on `@effect/platform-node`). + "effect": ["./packages/effect/src/index.ts"], + "effect/*": ["./packages/effect/src/*.ts"], + "@effect/ai-anthropic": ["./packages/ai/anthropic/src/index.ts"], + "@effect/ai-anthropic/*": ["./packages/ai/anthropic/src/*.ts"], + "@effect/ai-openai-compat": ["./packages/ai/openai-compat/src/index.ts"], + "@effect/ai-openai-compat/*": ["./packages/ai/openai-compat/src/*.ts"], + "@effect/ai-openai": ["./packages/ai/openai/src/index.ts"], + "@effect/ai-openai/*": ["./packages/ai/openai/src/*.ts"], + "@effect/ai-openrouter": ["./packages/ai/openrouter/src/index.ts"], + "@effect/ai-openrouter/*": ["./packages/ai/openrouter/src/*.ts"], + "@effect/atom-react": ["./packages/atom/react/src/index.ts"], + "@effect/atom-react/*": ["./packages/atom/react/src/*.ts"], + "@effect/atom-vue": ["./packages/atom/vue/src/index.ts"], + "@effect/atom-vue/*": ["./packages/atom/vue/src/*.ts"], + "@effect/atom-solid": ["./packages/atom/solid/src/index.ts"], + "@effect/atom-solid/*": ["./packages/atom/solid/src/*.ts"], + "@effect/opentelemetry": ["./packages/opentelemetry/src/index.ts"], + "@effect/opentelemetry/*": ["./packages/opentelemetry/src/*.ts"], + "@effect/platform-browser": ["./packages/platform/browser/src/index.ts"], + "@effect/platform-browser/*": ["./packages/platform/browser/src/*.ts"], + "@effect/platform-bun": ["./packages/platform/bun/src/index.ts"], + "@effect/platform-bun/*": ["./packages/platform/bun/src/*.ts"], + "@effect/platform-node": ["./packages/platform/node/src/index.ts"], + "@effect/platform-node/*": ["./packages/platform/node/src/*.ts"], + "@effect/platform-node-shared": ["./packages/platform/node-shared/src/index.ts"], + "@effect/platform-node-shared/*": ["./packages/platform/node-shared/src/*.ts"], + "@effect/sql-clickhouse": ["./packages/sql/clickhouse/src/index.ts"], + "@effect/sql-clickhouse/*": ["./packages/sql/clickhouse/src/*.ts"], + "@effect/sql-d1": ["./packages/sql/d1/src/index.ts"], + "@effect/sql-d1/*": ["./packages/sql/d1/src/*.ts"], + "@effect/sql-libsql": ["./packages/sql/libsql/src/index.ts"], + "@effect/sql-libsql/*": ["./packages/sql/libsql/src/*.ts"], + "@effect/sql-mssql": ["./packages/sql/mssql/src/index.ts"], + "@effect/sql-mssql/*": ["./packages/sql/mssql/src/*.ts"], + "@effect/sql-mysql2": ["./packages/sql/mysql2/src/index.ts"], + "@effect/sql-mysql2/*": ["./packages/sql/mysql2/src/*.ts"], + "@effect/sql-pg": ["./packages/sql/pg/src/index.ts"], + "@effect/sql-pg/*": ["./packages/sql/pg/src/*.ts"], + "@effect/sql-pglite": ["./packages/sql/pglite/src/index.ts"], + "@effect/sql-pglite/*": ["./packages/sql/pglite/src/*.ts"], + "@effect/sql-sqlite-bun": ["./packages/sql/sqlite-bun/src/index.ts"], + "@effect/sql-sqlite-bun/*": ["./packages/sql/sqlite-bun/src/*.ts"], + "@effect/sql-sqlite-do": ["./packages/sql/sqlite-do/src/index.ts"], + "@effect/sql-sqlite-do/*": ["./packages/sql/sqlite-do/src/*.ts"], + "@effect/sql-sqlite-node": ["./packages/sql/sqlite-node/src/index.ts"], + "@effect/sql-sqlite-node/*": ["./packages/sql/sqlite-node/src/*.ts"], + "@effect/sql-sqlite-react-native": ["./packages/sql/sqlite-react-native/src/index.ts"], + "@effect/sql-sqlite-react-native/*": ["./packages/sql/sqlite-react-native/src/*.ts"], + "@effect/sql-sqlite-wasm": ["./packages/sql/sqlite-wasm/src/index.ts"], + "@effect/sql-sqlite-wasm/*": ["./packages/sql/sqlite-wasm/src/*.ts"], + "@effect/ai-codegen": ["./packages/tools/ai-codegen/src/index.ts"], + "@effect/ai-codegen/*": ["./packages/tools/ai-codegen/src/*.ts"], + "@effect/api-diff": ["./packages/tools/api-diff/src/Cli.ts"], + "@effect/api-diff/*": ["./packages/tools/api-diff/src/*.ts"], + "@effect/ai-docgen": ["./packages/tools/ai-docgen/src/index.ts"], + "@effect/ai-docgen/*": ["./packages/tools/ai-docgen/src/*.ts"], + "@effect/bundle/*": ["./packages/tools/bundle/src/*.ts"], + "@effect/docgen": ["./packages/tools/docgen/src/index.ts"], + "@effect/docgen/*": ["./packages/tools/docgen/src/*.ts"], + "@effect/openapi-generator": ["./packages/tools/openapi-generator/src/index.ts"], + "@effect/openapi-generator/*": ["./packages/tools/openapi-generator/src/*.ts"], + "@effect/oxc": ["./packages/tools/oxc/src/index.ts"], + "@effect/oxc/*": ["./packages/tools/oxc/src/*.ts"], + "@effect/tools-utils": ["./packages/tools/utils/src/index.ts"], + "@effect/tools-utils/*": ["./packages/tools/utils/src/*.ts"], + "@effect/vitest": ["./packages/vitest/src/index.ts"], + "@effect/vitest/*": ["./packages/vitest/src/*.ts"], + // TODO: This is a special alias used by some tests. We should try to get rid of it. + "effect-test/*": ["./packages/effect/test/*.ts"] + }, + "plugins": [{ + "name": "@effect/language-service", + "namespaceImportPackages": [], + "includeSuggestionsInTsc": false, + "ignoreEffectWarningsInTscExitCode": true, + "ignoreEffectErrorsInTscExitCode": true, + "overrides": [ + { + "include": ["**/typetest/**/*.ts"], + "options": { + "diagnosticSeverity": { + "floatingEffect": "off" + } + } + } + ], + "diagnosticSeverity": { + "unknownInEffectCatch": "off", + "multipleEffectProvide": "off" + } + }] + } +} diff --git a/.context/effect/tstyche.json b/.context/effect/tstyche.json index 937599d2d..10164cb9b 100644 --- a/.context/effect/tstyche.json +++ b/.context/effect/tstyche.json @@ -2,7 +2,7 @@ "$schema": "./node_modules/tstyche/schemas/config.json", "testFileMatch": [ "packages/*/typetest/**/*.tst.*", - "packages/ai/*/typetest/**/*.tst.*" + "packages/*/*/typetest/**/*.tst.*" ], "tsconfig": "baseline" } diff --git a/.context/effect/vitest.config.ts b/.context/effect/vitest.config.ts index b5f7ff95a..66aa54298 100644 --- a/.context/effect/vitest.config.ts +++ b/.context/effect/vitest.config.ts @@ -1,31 +1,196 @@ -import { defineConfig } from "vitest/config" +import * as os from "node:os" +import * as path from "node:path" +import { defineConfig, mergeConfig, type ViteUserConfig } from "vitest/config" const isDeno = process.versions.deno !== undefined const isBun = process.versions.bun !== undefined +const isNode = typeof process !== "undefined" && + process.release.name === "node" && + !isDeno && + !isBun +const integrationTestsEnabled = process.env.EFFECT_INTEGRATION_TESTS === "1" +const clusterTestsEnabled = process.env.EFFECT_CLUSTER_TESTS === "1" + +const project = ( + name: string, + directory: string, + include: boolean = true, + config: ViteUserConfig = {}, + projectExclude?: ReadonlyArray, + projectInclude?: ReadonlyArray +) => { + if (!include) { + return [] + } + + const cfg = mergeConfig({ + root: directory, + test: { name } + }, config) + + const merged = mergeConfig(shared, cfg) + if (projectExclude !== undefined) { + merged.test!.exclude = [...projectExclude] + } + if (projectInclude !== undefined) { + merged.test!.include = [...projectInclude] + } + return [merged] +} + +export const exclude = [ + "**/.*/**", + "**/node_modules/**", + "**/dist/**", + "**/benchmark/**", + "**/bundle/**", + "**/typetest/**", + "**/coverage/**", + "**/test/utils/**", + "**/test/cluster-integration/**", + ...(!integrationTestsEnabled ? ["**/*.integration.test.{ts,tsx}"] : []), + "**/*.d.ts", + "**/*.config.*", + "**/vitest.*" +] + +const shared: ViteUserConfig = { + optimizeDeps: { + exclude: ["bun:sqlite"] + }, + server: { + watch: { + ignored: exclude + } + }, + resolve: { + tsconfigPaths: true + }, + test: { + exclude, + passWithNoTests: true, + setupFiles: [path.join(__dirname, "vitest.setup.ts")], + fakeTimers: { + toFake: undefined + }, + sequence: { + concurrent: true + }, + include: ["test/**/*.test.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["html"], + reportsDirectory: "coverage", + exclude + } + } +} export default defineConfig({ test: { + passWithNoTests: true, projects: [ - "packages/*/vitest.config.ts", - "packages/ai/*/vitest.config.ts", - "packages/atom/*/vitest.config.ts", - "packages/tools/*/vitest.config.ts", - "packages/sql/*/vitest.config.ts", - ...(isDeno ? - [ - "!packages/atom", - "!packages/platform-bun", - "!packages/platform-node", - "!packages/platform-node-shared", - "!packages/sql/d1", - "!packages/sql/sqlite-node" - ] : - []), - ...(isBun ? + ...project("effect", "packages/effect", true, { + test: { + // @see https://github.com/denoland/deno/issues/23882 + exclude: isDeno ? ["test/cluster/**"] : [] + } + }), + ...project("@effect/ai-anthropic", "packages/ai/anthropic"), + ...project("@effect/ai-openai", "packages/ai/openai"), + ...project("@effect/ai-openai-compat", "packages/ai/openai-compat"), + ...project("@effect/ai-openrouter", "packages/ai/openrouter"), + ...project("@effect/atom-react", "packages/atom/react", true, { + test: { + environment: "jsdom", + setupFiles: [path.join(__dirname, "packages/atom/react/vitest.setup.ts")] + } + }), + ...project("@effect/atom-solid", "packages/atom/solid", true, { + resolve: { + conditions: ["browser"] + }, + test: { + environment: "jsdom", + setupFiles: [path.join(__dirname, "packages/atom/solid/vitest.setup.ts")] + } + }), + ...project("@effect/atom-vue", "packages/atom/vue", true, { + test: { + environment: "happy-dom" + } + }), + ...project("@effect/opentelemetry", "packages/opentelemetry"), + ...project("@effect/platform-browser", "packages/platform/browser", true, { + test: { + environment: "happy-dom", + execArgv: [ + "--localstorage-file", + path.resolve(os.tmpdir(), `vitest-${process.pid}.localstorage`) + ], + setupFiles: [path.join(__dirname, "packages/platform/browser/vitest.setup.ts")] + } + }), + ...project("@effect/platform-bun", "packages/platform/bun", isBun), + ...project("@effect/platform-deno", "packages/platform/deno", isDeno), + ...project("@effect/platform-node", "packages/platform/node", isNode), + ...project( + "cluster-integration", + "packages/platform/node", + isNode && clusterTestsEnabled, + { + test: { + globalSetup: [path.join(__dirname, "packages/platform/node/test/cluster-integration/globalSetup.ts")], + include: ["test/cluster-integration/**/*.test.ts"], + retry: 0, + sequence: { + concurrent: false + }, + testTimeout: 60_000 + } + }, + exclude.filter((path) => path !== "**/test/cluster-integration/**"), [ - "!packages/platform-node" - ] : - []) + "test/cluster-integration/**/*.test.ts" + ] + ), + ...project("@effect/platform-node-shared", "packages/platform/node-shared", !isDeno), + ...project("@effect/vitest", "packages/vitest"), + ...project("@effect/sql-clickhouse", "packages/sql/clickhouse"), + ...project("@effect/sql-d1", "packages/sql/d1", !isDeno), + ...project("@effect/sql-libsql", "packages/sql/libsql"), + ...project("@effect/sql-mssql", "packages/sql/mssql"), + // MySQL starts a fresh container per integration test suite, so avoid competing + // with the other container-backed projects on Deno CI runners. + ...project( + "@effect/sql-mysql2", + "packages/sql/mysql2", + true, + isDeno && integrationTestsEnabled + ? { + test: { + fileParallelism: false, + sequence: { + groupOrder: 1 + } + } + } + : {} + ), + ...project("@effect/sql-pg", "packages/sql/pg"), + ...project("@effect/sql-pglite", "packages/sql/pglite"), + ...project("@effect/sql-sqlite-bun", "packages/sql/sqlite-bun"), + ...project("@effect/sql-sqlite-do", "packages/sql/sqlite-do"), + ...project("@effect/sql-sqlite-node", "packages/sql/sqlite-node", !isDeno), + ...project("@effect/sql-sqlite-react-native", "packages/sql/sqlite-react-native"), + ...project("@effect/sql-sqlite-wasm", "packages/sql/sqlite-wasm"), + ...project("@effect/api-diff", "packages/tools/api-diff"), + ...project("@effect/bundle", "packages/tools/bundle"), + ...project("@effect/doctest", "packages/tools/doctest"), + ...project("@effect/docgen", "packages/tools/docgen"), + ...project("@effect/jsdocs", "packages/tools/jsdocs"), + ...project("@effect/openapi-generator", "packages/tools/openapi-generator"), + ...project("@effect/oxc", "packages/tools/oxc") ] } }) diff --git a/.context/effect/vitest.docs.ts b/.context/effect/vitest.docs.ts new file mode 100644 index 000000000..786b82aa2 --- /dev/null +++ b/.context/effect/vitest.docs.ts @@ -0,0 +1,18 @@ +import * as Doctest from "@effect/doctest/Plugin" +import { defineConfig } from "vitest/config" + +export default defineConfig({ + plugins: [Doctest.plugin()], + resolve: { + tsconfigPaths: true + }, + test: { + passWithNoTests: true, + testTimeout: 10_000, + include: [], + includeSource: [ + "packages/*/src/**/*.ts", + "packages/*/*/src/**/*.ts" + ] + } +}) diff --git a/.context/effect/vitest.shared.ts b/.context/effect/vitest.shared.ts deleted file mode 100644 index b040a6549..000000000 --- a/.context/effect/vitest.shared.ts +++ /dev/null @@ -1,61 +0,0 @@ -import path from "node:path" -import aliases from "vite-tsconfig-paths" -import type { ViteUserConfig } from "vitest/config" - -const config: ViteUserConfig = { - esbuild: { - target: "es2020" - }, - optimizeDeps: { - exclude: ["bun:sqlite"] - }, - plugins: [aliases()], - server: { - watch: { - ignored: [ - "**/.context/**", - "**/.direnv/**", - "**/.lalph/**", - "**/.repos/**" - ] - } - }, - test: { - exclude: [ - "**/.context/**", - "**/.direnv/**", - "**/.lalph/**", - "**/.repos/**", - "**/node_modules/**" - ], - setupFiles: [path.join(__dirname, "vitest.setup.ts")], - fakeTimers: { - toFake: undefined - }, - sequence: { - concurrent: true - }, - include: ["test/**/*.test.{ts,tsx}"], - coverage: { - provider: "v8", - reporter: ["html"], - reportsDirectory: "coverage", - exclude: [ - "node_modules/", - "dist/", - "benchmark/", - "bundle/", - "typetest/", - "build/", - "coverage/", - "test/utils/", - "**/*.d.ts", - "**/*.config.*", - "**/vitest.setup.*", - "**/vitest.shared.*" - ] - } - } -} - -export default config diff --git a/apps/alerting/package.json b/apps/alerting/package.json index 5197d5976..5a8bcb413 100644 --- a/apps/alerting/package.json +++ b/apps/alerting/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", diff --git a/apps/api/package.json b/apps/api/package.json index 42d9333f1..0c42e1cbe 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -29,8 +29,8 @@ "dependencies": { "@clerk/backend": "^2.30.1", "@clickhouse/client-web": "catalog:clickhouse", - "@distilled.cloud/cloudflare": "1.0.0-rc.2", - "@distilled.cloud/core": "1.0.0-rc.2", + "@distilled.cloud/cloudflare": "1.0.0-rc.4", + "@distilled.cloud/core": "1.0.0-rc.4", "@effect/platform-bun": "catalog:effect", "@maple-dev/clickhouse-builder": "workspace:*", "@maple-dev/effect-sdk": "workspace:*", diff --git a/apps/cli/package.json b/apps/cli/package.json index d7ed8f30b..59409a75f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", diff --git a/apps/electric-sync/package.json b/apps/electric-sync/package.json index be95d5036..114c79cf6 100644 --- a/apps/electric-sync/package.json +++ b/apps/electric-sync/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", diff --git a/apps/scraper/package.json b/apps/scraper/package.json index 91f16003c..966fa04a3 100644 --- a/apps/scraper/package.json +++ b/apps/scraper/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", diff --git a/bun.lock b/bun.lock index 538abed4a..164287487 100644 --- a/bun.lock +++ b/bun.lock @@ -6,13 +6,13 @@ "name": "maple", "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", - "@effect/platform-node": "4.0.0-beta.105", - "@effect/tsgo": "0.35.0", - "@effect/vitest": "4.0.0-beta.105", + "@effect/platform-node": "4.0.0-rc.108", + "@effect/tsgo": "0.36.4", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "@types/node": "catalog:tooling", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "knip": "^6.17.1", "oxfmt": "^0.55.0", @@ -34,7 +34,7 @@ "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", @@ -47,8 +47,8 @@ "dependencies": { "@clerk/backend": "^2.30.1", "@clickhouse/client-web": "catalog:clickhouse", - "@distilled.cloud/cloudflare": "1.0.0-rc.2", - "@distilled.cloud/core": "1.0.0-rc.2", + "@distilled.cloud/cloudflare": "1.0.0-rc.4", + "@distilled.cloud/core": "1.0.0-rc.4", "@effect/platform-bun": "catalog:effect", "@maple-dev/clickhouse-builder": "workspace:*", "@maple-dev/effect-sdk": "workspace:*", @@ -96,7 +96,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", @@ -115,7 +115,7 @@ "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", @@ -245,7 +245,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/bun": "^1.3.11", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", @@ -324,7 +324,7 @@ "@cloudflare/workers-types": "4.20260603.1", "@maple-dev/alchemy": "workspace:*", "@maple-dev/effect-sdk": "workspace:*", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "typescript": "catalog:tooling", }, @@ -477,7 +477,7 @@ "effect": "catalog:effect", }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/react": "catalog:react", "typescript": "catalog:tooling", "vitest": "^4.1.9", @@ -491,10 +491,10 @@ "version": "0.1.0", "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/domain": "workspace:*", "@types/node": "catalog:tooling", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "tsdown": "^0.22.14", "typescript": "catalog:tooling", @@ -514,7 +514,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", "vitest": "catalog:", @@ -607,7 +607,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/browser-session": "workspace:*", "@types/node": "catalog:tooling", "effect": "catalog:effect", @@ -707,12 +707,11 @@ }, }, "patchedDependencies": { - "@effect/vitest@4.0.0-beta.105": "patches/@effect%2Fvitest@4.0.0-beta.105.patch", - "alchemy@2.0.0-beta.70": "patches/alchemy@2.0.0-beta.70.patch", - "effect@4.0.0-beta.105": "patches/effect@4.0.0-beta.105.patch", + "effect@4.0.0-rc.108": "patches/effect@4.0.0-rc.108.patch", + "@effect/vitest@4.0.0-rc.108": "patches/@effect%2Fvitest@4.0.0-rc.108.patch", }, "overrides": { - "@effect/sql-d1": "4.0.0-beta.105", + "@effect/sql-d1": "4.0.0-rc.108", }, "catalog": { "vite": "^8.0.16", @@ -723,10 +722,10 @@ "@clickhouse/client-web": "^1.21.0", }, "effect": { - "@effect/atom-react": "4.0.0-beta.105", + "@effect/atom-react": "4.0.0-rc.108", "@effect/language-service": "^0.87.2", - "@effect/platform-bun": "4.0.0-beta.105", - "effect": "4.0.0-beta.105", + "@effect/platform-bun": "4.0.0-rc.108", + "effect": "4.0.0-rc.108", }, "react": { "@types/react": "^19.2.14", @@ -768,6 +767,8 @@ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="], + "@alchemy.run/cloudflare-runtime": ["@alchemy.run/cloudflare-runtime@2.0.0-beta.72", "", { "dependencies": { "@alchemy.run/node-utils": "0.0.5", "@cloudflare/unenv-preset": "^2.16.0", "@puppeteer/browsers": "^2.10.6", "capnp-es": "^0.0.14", "magic-string": "^0.30.21", "sharp": "^0.34.5", "unenv": "^2.0.0-rc.24", "workerd": "1.20260704.1" }, "peerDependencies": { "@distilled.cloud/cloudflare": "1.0.0-rc.4", "@effect/platform-bun": ">=4.0.0-beta.105 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.105 || >=4.0.0", "effect": ">=4.0.0-beta.105 || >=4.0.0", "rolldown": "1.1.5", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node", "rolldown", "vite"] }, "sha512-pIkyRfUvBDfI8Rx0+iUB/pcGngDlpEu7dHwljs4eJUXpUEsaAtWwDlqzcx4O0qLw+jLJDPJVgP1q3hlNGz7YHw=="], + "@alchemy.run/node-utils": ["@alchemy.run/node-utils@0.0.5", "", {}, "sha512-5agdhQxWBodxa5hDRyjnpx91RTU3g+qd5fxYB7uNDCaOzB0XC47UU/KHR6zf6jhz/NXf61gJS+vhYwn8NHeRoQ=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -1102,59 +1103,53 @@ "@date-fns/tz": ["@date-fns/tz@1.5.0", "", {}, "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg=="], - "@distilled.cloud/aws": ["@distilled.cloud/aws@1.0.0-rc.2", "", { "dependencies": { "@aws-crypto/crc32": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/credential-providers": "^3.994.0", "@aws-sdk/types": "^3.973.1", "@distilled.cloud/core": "1.0.0-rc.2", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "aws4fetch": "^1.0.20", "fast-xml-parser": "^5.3.2" }, "peerDependencies": { "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-hTWmdpfaKt6yLQ1YrJ17tp0DXINH7Wm5WQ05ap1FZW13Kwv+HzejXc4QPOYS6KT00Tlrfm6EEkTd3fDrY8dSGQ=="], - - "@distilled.cloud/axiom": ["@distilled.cloud/axiom@1.0.0-rc.2", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.2", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-okRnWBdEQ6vYanYtiKbNzNJCgccEa7ZgeytHaNRTvIpHD4PlwG5WYwBsLkhTYjjudOvD5yjJ4H2N8dQQsJlGrg=="], - - "@distilled.cloud/cloudflare": ["@distilled.cloud/cloudflare@1.0.0-rc.2", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.2", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-GGO5I8OxY/0iUT2bj+Ug+ivrjS6V1/nvdMRfL8dLOrpBkIhR8AFkLPgdatBwdIWeJWRE5wdU4wy+aeHJNp3oJA=="], + "@distilled.cloud/aws": ["@distilled.cloud/aws@1.0.0-rc.4", "", { "dependencies": { "@aws-crypto/crc32": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/credential-providers": "^3.994.0", "@aws-sdk/types": "^3.973.1", "@distilled.cloud/core": "1.0.0-rc.4", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "aws4fetch": "^1.0.20", "fast-xml-parser": "^5.3.2" }, "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-0ebeFe4d+h73hmei4L+0dfe0WXrFEDo79ZpSR4sk1Q3yB0R+y+osV2BnPDlzKuNvb6Kb7Ucu8dd0qqN/3ezwvg=="], - "@distilled.cloud/cloudflare-rolldown-plugin": ["@distilled.cloud/cloudflare-rolldown-plugin@0.16.1", "", { "dependencies": { "@cloudflare/unenv-preset": "^2.16.0", "magic-string": "^0.30.21", "unenv": "^2.0.0-rc.24" }, "peerDependencies": { "rolldown": "~1.1.5", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["rolldown", "vite"] }, "sha512-yj424SA0LMag1B6Ak/bhxEN3Wwg6j3Erv2xmRbcWgY1HbfZV5J1pUgRZ0RackjlSUAmCTRRxTrmZnQfqo5t2zQ=="], + "@distilled.cloud/axiom": ["@distilled.cloud/axiom@1.0.0-rc.4", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-kbZK5NjV+xSLwmgds+KihMt9RCYui43m03/XtNvkrAYEcPmUrkQa576W/8KaPEpyRkb/Jj31J+gMh17YMYe3FQ=="], - "@distilled.cloud/cloudflare-runtime": ["@distilled.cloud/cloudflare-runtime@0.16.1", "", { "dependencies": { "@alchemy.run/node-utils": "^0.0.5", "@puppeteer/browsers": "^2.10.6", "sharp": "^0.34.5", "workerd": "1.20260704.1" }, "peerDependencies": { "@distilled.cloud/cloudflare": "^0.30.3", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node"] }, "sha512-kubDEVDpkbXxNY56TnoXQGaAc0az2IA7SQsBkhgCnN538Vn9LGXTOJ1534AzFhKtfmrVxzj0rtcd5cPKBNTn4A=="], + "@distilled.cloud/cloudflare": ["@distilled.cloud/cloudflare@1.0.0-rc.4", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-BaFKlgSEIMj4zCl+2K940FmV5qPhjCzSq5LrkMjGRK28RV4tS3QmQwejW6ilKegdvPn4RWS0UxlwrCPE9zGerw=="], - "@distilled.cloud/cloudflare-vite-plugin": ["@distilled.cloud/cloudflare-vite-plugin@0.16.1", "", { "dependencies": { "@distilled.cloud/cloudflare-rolldown-plugin": "0.16.1" }, "peerDependencies": { "@distilled.cloud/cloudflare": "^0.30.3", "@distilled.cloud/cloudflare-runtime": "0.16.1", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0", "vite": "^7.0.0 || ^8.0.0" }, "optionalPeers": ["@effect/platform-bun", "@effect/platform-node"] }, "sha512-OzVZlegqYqt/bNdVHOVKp/vBnUU1+Cdfymu2yBJioAOjNIJEW2uKUKm6KwHZf29HlgGDh2jSruqOWui86kM6Gg=="], + "@distilled.cloud/core": ["@distilled.cloud/core@1.0.0-rc.4", "", { "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-g/5THnVZoBKO31hhdU1JJxqcI5aXAmMIJQpv2WUmutYeedbdX/kknnvwY5a75rc7MGSWsHnB4EAA/07FtdSSKg=="], - "@distilled.cloud/core": ["@distilled.cloud/core@1.0.0-rc.2", "", { "peerDependencies": { "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-cVswTWsqC15Z9EPAOdRpfTG4UU8JPXhQEeH2ThW7Yh2BRl4lhIIOdxbRqUsRXPDMn24Mn08h2C3A6MGKP/KwLA=="], + "@distilled.cloud/neon": ["@distilled.cloud/neon@1.0.0-rc.4", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-ps7McSoK5M+qDXRPZy8xDNJWtqw3RSXTu5cN1f3Fa/macUzzSPmkSmFHRIpScsnwgi38sQVXc2ZmV8qztWQQLQ=="], - "@distilled.cloud/neon": ["@distilled.cloud/neon@1.0.0-rc.2", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.2", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-j1SRh/ionzWDeS8nO3+zF8h2zHNOddmTfqeev5LBs1pKeQWBJ3ZVDK3GeBACZzaeW3rCcll8eJEZ6fRnrp3xbw=="], - - "@distilled.cloud/planetscale": ["@distilled.cloud/planetscale@1.0.0-rc.2", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.2", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "effect": ">=4.0.0-beta.102 || >=4.0.0" } }, "sha512-ucuQ0VM3bmovjzHoC/C7uo9634EQOJcI1gzx0jjdE2QIDsvy56UhjT3alilq9kADGz1V0/LUHOV3Xf1VqJf6Nw=="], + "@distilled.cloud/planetscale": ["@distilled.cloud/planetscale@1.0.0-rc.4", "", { "dependencies": { "@distilled.cloud/core": "1.0.0-rc.4" }, "peerDependencies": { "effect": ">=4.0.0-beta.104 || >=4.0.0" } }, "sha512-7EwQF+AGahkVgCKo5lgWXbewcJ1xGCjwctTk8FfmdtUAWwNdmXAysIMdfw4mOYADLB8HMw6XZ3TTV6H7xYyq0w=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@effect-router/core": ["@effect-router/core@workspace:lib/effect-router"], - "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.105", "", { "peerDependencies": { "effect": "^4.0.0-beta.105", "react": ">=19.2.7 <20.0.0", "scheduler": ">=0.27.0 <0.28.0" } }, "sha512-ldHOwlnrHMYCDb6ln6+uR+LwYV8Q2aQksaXQ92wAxP+YMvIpXP8PVfBdBoAooagmj9hGBgbyRnyE+wnjQRPkTQ=="], + "@effect/atom-react": ["@effect/atom-react@4.0.0-rc.108", "", { "peerDependencies": { "effect": "^4.0.0-rc.108", "react": ">=19.2.7 <20.0.0", "scheduler": ">=0.27.0 <0.28.0" } }, "sha512-GNHQ4ildpnI00AdyL3cKRBo387aEpoY6tIIfwPrrZoNbMGCVNupiAIReoD6dBwWIgOEIASd/YLwx1DR42Jqx7g=="], "@effect/language-service": ["@effect/language-service@0.87.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-CfiSoaVQO8pZgaMZGw5VvMvRGybsJ2LaSDoLYaqiVGvo/YYPYVR2GzUKY0h1NtIweq/+SQ5XLrvtzPIttWQ0GQ=="], - "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.105", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.105" }, "peerDependencies": { "effect": "^4.0.0-beta.105" } }, "sha512-5eCuMLxfkLbVK5If5zFybyKduQvhYofy0oPsbTH8UiIrYqyx5QqK9G9PZ1SfBsbqHfykwLEpMCdzQczFGERw2w=="], + "@effect/platform-bun": ["@effect/platform-bun@4.0.0-rc.108", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.108" }, "peerDependencies": { "effect": "^4.0.0-rc.108" } }, "sha512-27RoALzmzx6Qp4LrPIE8bYJfHe+8ZaAO3xLhJMEE6mVA8fxcPh4HAGwBv09Nk2qTFVh578SlSCY5ojUlqeSJ4A=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.105", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.105", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.105", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-gOHtgE9PqCT8aiiM0W7ZSbu4M2/jPGfJOKWuyBCxofNV5Np36fFY55FSa1MtyG+vjOjlD5btNY2i6i/SPtRqTA=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-rc.108", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.108", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-rc.108", "ioredis": ">=5.7.0 <6.0.0" } }, "sha512-Nof78154BaHGdSYr4TPQFZ5+Dg+HkpmbI3SQUdwsby5QNs6yahGJPu2AgdIVqdx7pKZ2w7j/bvdnqoMmkG0PbA=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.105", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.105" } }, "sha512-UfeC8cFm4sP+XW14OO66CcOGJ8PcD102hH0kLwj8xUVL6nYIDGE/SJvlfVmFx+9OWF6rnjfXjMV6vyl6mCdhCQ=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-rc.108", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-rc.108" } }, "sha512-g5dSRR+tHzFcWav5A6zbug78oV8WPJL5BJqgKlKtow0cQ2IpULcLZaP2xCYR3RJDvz5SETREmPCJ+BNONIsalw=="], - "@effect/sql-d1": ["@effect/sql-d1@4.0.0-beta.105", "", { "dependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "peerDependencies": { "effect": "^4.0.0-beta.105" } }, "sha512-69ShnmTbpxYEqJGVms/JGIiUAjzLsZZxAAIZv9rs5SBHlL0P2gZ+ZCHxtjL9hgnSlt5xePGRdK+vUiNMuRynKQ=="], + "@effect/sql-d1": ["@effect/sql-d1@4.0.0-rc.108", "", { "dependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "peerDependencies": { "effect": "^4.0.0-rc.108" } }, "sha512-2Pef1buqPhYuCHnfxT/N5Pny6lBcZbju13EDxqTBT6wiUc3R0yrICu8ZG45lm4vekqPAzFntUCbZt8yKOcenBg=="], "@effect/sql-sqlite-do": ["@effect/sql-sqlite-do@4.0.0-beta.105", "", { "peerDependencies": { "effect": "^4.0.0-beta.105" } }, "sha512-k48tPWVOtZds3x8PAxt8fvdoIM9+J07KHt1AdiQvHZgK4vRSLFpw+fs1yI8fSfLjDX2EIMxtycuHty0/6irmkQ=="], - "@effect/tsgo": ["@effect/tsgo@0.35.0", "", { "optionalDependencies": { "@effect/tsgo-darwin-arm64": "0.35.0", "@effect/tsgo-darwin-x64": "0.35.0", "@effect/tsgo-linux-arm": "0.35.0", "@effect/tsgo-linux-arm64": "0.35.0", "@effect/tsgo-linux-x64": "0.35.0", "@effect/tsgo-win32-arm64": "0.35.0", "@effect/tsgo-win32-x64": "0.35.0" }, "bin": { "effect-tsgo": "dist/effect-tsgo.cjs" } }, "sha512-10UWtAhbl/AlMSXKwo2xp9HkNtrzXpxgeWTH8vWKo7Ir6DAsOIYAJjTjDxud3sCb2vJtxi2zHg6Qq3/i/MKxzQ=="], + "@effect/tsgo": ["@effect/tsgo@0.36.4", "", { "optionalDependencies": { "@effect/tsgo-darwin-arm64": "0.36.4", "@effect/tsgo-darwin-x64": "0.36.4", "@effect/tsgo-linux-arm": "0.36.4", "@effect/tsgo-linux-arm64": "0.36.4", "@effect/tsgo-linux-x64": "0.36.4", "@effect/tsgo-win32-arm64": "0.36.4", "@effect/tsgo-win32-x64": "0.36.4" }, "bin": { "effect-tsgo": "dist/effect-tsgo.cjs" } }, "sha512-fNmdUV6FgXnvIcG18AVS1SRXt7z9jsyBh5CKuJYmTsE+DgbLaWF0CevKHx7hQPcidDWRVmdJYfU66Jvc/ZEt6w=="], - "@effect/tsgo-darwin-arm64": ["@effect/tsgo-darwin-arm64@0.35.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zp1nnKYPyl8Us+1igPePPEkbIElCu5yrr5NyQf/nxqxZkniohMtm4hJXA5BDwz0Tv7IvWPCsHuJ8ISg6i8LkxA=="], + "@effect/tsgo-darwin-arm64": ["@effect/tsgo-darwin-arm64@0.36.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oOcWdKQucNch6G4CvidHEzmfgeKEPMvFcz+DXuKGonBLwVHX+i1VYGbZwccEOoiitOg7eWVl4e8S0qKMXg3/gg=="], - "@effect/tsgo-darwin-x64": ["@effect/tsgo-darwin-x64@0.35.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6lNk1j0o/ReXihhiqQdhzvJll/ClrkEJwUtPpMBcUtlmH+8svSH50+S84GOzJMwp2ghw/hyJld6Lc8wzzry0Jw=="], + "@effect/tsgo-darwin-x64": ["@effect/tsgo-darwin-x64@0.36.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-4QZh0n/nUoTI7sp4o7gDEviJ/jXH+IFTVqIyQ2w2fqqPoOvaGl9bVoR/2f9j/67hvY4DHcXKaccpmyoRYhaYsw=="], - "@effect/tsgo-linux-arm": ["@effect/tsgo-linux-arm@0.35.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BmDz9FGnGLy8HIzWx2VPdRCKbFXpbCCJFzSnkMMYYHHkNP+6CRqETW7x4HFbxuIFxxiGRXfxiE5ui2lomCBWZA=="], + "@effect/tsgo-linux-arm": ["@effect/tsgo-linux-arm@0.36.4", "", { "os": "linux", "cpu": "arm" }, "sha512-10oTUnOK/RCfmTMFvMndNIfk61uviyYBRpbxhDd/SiU6DYCmWlnZ/xhGmgyj5uiysOiFNsp3NKxT0d9y4uAz8g=="], - "@effect/tsgo-linux-arm64": ["@effect/tsgo-linux-arm64@0.35.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-DsSBnE5LGWvxKUZrhvtQL0H9nDQcOyef0ct7c5Rws46tgBM9nssEj/Q45GAD+JTVwBKcEvvzPEgDSLY+fFYtvw=="], + "@effect/tsgo-linux-arm64": ["@effect/tsgo-linux-arm64@0.36.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-aOv+wVbZWgBt9PGXPA874qgw151GrzkWqaswe+4TXjhdw/M4i35PCUH5T+uaQj3FnQ1NWiPqHIGfNBRjf3xG6w=="], - "@effect/tsgo-linux-x64": ["@effect/tsgo-linux-x64@0.35.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mq++Fyn4T/0FSviRrGcOAE1FKnXHFeQLtfOEJgfAEZw4/yiE7JLV0akJgFw3wjaEQx8bmbRb20UBcAI84zQ5AA=="], + "@effect/tsgo-linux-x64": ["@effect/tsgo-linux-x64@0.36.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+BuwiG5d8dLNrrN1PB5GA3gRkBwvlUYmMkm/XkttaLnppp9jePFkB8CgAiNV8j290q7BVO2nQjao5aPkVwmluw=="], - "@effect/tsgo-win32-arm64": ["@effect/tsgo-win32-arm64@0.35.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-o76O+JMa1KAJnd3QVDLmeBGTt23VE5IBbuzUhYWHG6UQ0YQynSMo0vZXOmrrPH0KARD7uJAgEMaJu/SReYDObA=="], + "@effect/tsgo-win32-arm64": ["@effect/tsgo-win32-arm64@0.36.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-HsW90wbMWOeorUeNMdTqTcMZIDcCXdkeFGXTUkx9K1R21fXSw+QEdPtTmEyKsRMXEH4vqek/HQa0yMkUT0CJzA=="], - "@effect/tsgo-win32-x64": ["@effect/tsgo-win32-x64@0.35.0", "", { "os": "win32", "cpu": "x64" }, "sha512-IC4nRkMZBCwCdGsqjQQnw7vE6aQFP0jkhkD/rQwb40m6AQN8mqrgq6jXm2WHVxsslrqWu4DXPC3ZHZQjJO50lg=="], + "@effect/tsgo-win32-x64": ["@effect/tsgo-win32-x64@0.36.4", "", { "os": "win32", "cpu": "x64" }, "sha512-q4j1q0ESYxxookjA0ECz/84DEH2MsP51K0aZb3uXKfxagHDASPoWDcVu4BJTcwrK5kdZzvw9QrCPjAXe9WZqng=="], - "@effect/vitest": ["@effect/vitest@4.0.0-beta.105", "", { "peerDependencies": { "effect": "^4.0.0-beta.105", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-2t1J/H/+9hAKDC1S3iFyUVZJhdMpKvuggnOGNXPTmxZcVdxG/VkhjQjo7wzOHWs6SZcX8Cr/EsB+uiVAn9G0Ng=="], + "@effect/vitest": ["@effect/vitest@4.0.0-rc.108", "", { "peerDependencies": { "effect": "^4.0.0-rc.108", "vitest": ">=4.1.0 <5.0.0" } }, "sha512-XD2GP1JATN28wnIeFGsBqYMuQDCHIodKKgFulGyG91GvuHqgf2gz+WxR2LPdyNoKMtr7lsbRzVj07niSYtIKzA=="], "@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="], @@ -2792,7 +2787,7 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "alchemy": ["alchemy@2.0.0-beta.70", "", { "dependencies": { "@alchemy.run/node-utils": "0.0.5", "@aws-sdk/credential-providers": "^3.0.0", "@clack/prompts": "^1.7.0", "@distilled.cloud/aws": "1.0.0-rc.2", "@distilled.cloud/axiom": "1.0.0-rc.2", "@distilled.cloud/cloudflare": "1.0.0-rc.2", "@distilled.cloud/cloudflare-rolldown-plugin": "0.16.1", "@distilled.cloud/cloudflare-runtime": "0.16.1", "@distilled.cloud/cloudflare-vite-plugin": "0.16.1", "@distilled.cloud/core": "1.0.0-rc.2", "@distilled.cloud/neon": "1.0.0-rc.2", "@distilled.cloud/planetscale": "1.0.0-rc.2", "@effect/sql-d1": ">=4.0.0-beta.102 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-beta.102 || >=4.0.0", "@effect/vitest": ">=4.0.0-beta.102 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@smithy/node-config-provider": "^4.0.0", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.4", "ink": "^6.3.1", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.0", "rolldown": "1.1.5", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-beta.102 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.102 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-beta.102 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-beta.102 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.4", "drizzle-orm": "1.0.0-rc.4", "effect": ">=4.0.0-beta.102 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.15.3", "pg": "^8.13.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "bin/cli.js" } }, "sha512-ITaYMa061cYuXHGP4xdS0+HsP5smpNl58f+eIja8egvJwzpwGbPt2aK0y+vDMH+44iMAsfrH7yTOKm7H5DN4cg=="], + "alchemy": ["alchemy@2.0.0-beta.72", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.72", "@alchemy.run/node-utils": "0.0.5", "@aws-sdk/credential-providers": "^3.0.0", "@clack/prompts": "^1.7.0", "@distilled.cloud/aws": "1.0.0-rc.4", "@distilled.cloud/axiom": "1.0.0-rc.4", "@distilled.cloud/cloudflare": "1.0.0-rc.4", "@distilled.cloud/core": "1.0.0-rc.4", "@distilled.cloud/neon": "1.0.0-rc.4", "@distilled.cloud/planetscale": "1.0.0-rc.4", "@effect/sql-d1": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-beta.105 || >=4.0.0", "@effect/vitest": ">=4.0.0-beta.105 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@smithy/node-config-provider": "^4.0.0", "@smithy/shared-ini-file-loader": "^4.3.4", "@smithy/types": "^4.8.1", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "fast-xml-parser": "^5.3.4", "ink": "^6.3.1", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.0", "rolldown": "1.1.5", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-beta.105 || >=4.0.0", "@effect/platform-node": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-beta.105 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-beta.105 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": ">=4.0.0-beta.105 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.23.2", "pg": "^8.22.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "bin/cli.js" } }, "sha512-3nD1U4hWdGTqJp3eeEXumEYQL6WmoFL8blzWB0VnodM8JD8ginGr9vYsexsrpqyH78tU/r6EBLBkzbbKgAwIQg=="], "alien-signals": ["alien-signals@2.0.6", "", {}, "sha512-P3TxJSe31bUHBiblg59oU1PpaWPtmxF9GhJ/cB7OkgJ0qN/ifFSKUI25/v8ZhsT+lIG6ac8DpTOplXxORX6F3Q=="], @@ -2992,6 +2987,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + "capnp-es": ["capnp-es@0.0.14", "", { "peerDependencies": { "typescript": "^5.7.3" }, "optionalPeers": ["typescript"], "bin": { "capnp-es": "dist/compiler/capnpc-js.mjs", "capnpc-js": "dist/compiler/capnpc-js.mjs", "capnpc-ts": "dist/compiler/capnpc-ts.mjs", "capnpc-dts": "dist/compiler/capnpc-dts.mjs" } }, "sha512-8lWj4GJISiqRSlAJGkWpI4Azib7QY5UDIkqxeHcI7aAnXVk9SFuWxl1Fme+2HhzNANV0WTJqwUZvvXvloc3sBA=="], + "capnweb": ["capnweb@0.6.1", "", {}, "sha512-fmhV26QPd1ewf5R74h55oVZnGwIcSaRMzbfLQUy8+zOBjuTmT3KXoT8wxHvnp1m9Ht9BoUUS5ZwNLoVLfQTyBg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -3326,7 +3323,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.105", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-7U5OzWVNlpnZFvXiZfOeu8V+dCQUa3Om569HnYG2Dx8C8V6uoKS920kFJkIDEYVhlSGhzkRqaGKmYY9bPkBMMQ=="], + "effect": ["effect@4.0.0-rc.108", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "uuid": "^14.0.1" } }, "sha512-KmI3DlKZWPvCL4QQ2FMaPOuxMt/7DrKMENCY/gQ+MkDR5QYw25wgU5Zmh/wVLboNjIci1gNOgNCFe4xqgxli3A=="], "electron-to-chromium": ["electron-to-chromium@1.5.399", "", {}, "sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA=="], @@ -5330,6 +5327,8 @@ "@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@alchemy.run/cloudflare-runtime/workerd": ["workerd@1.20260704.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260704.1", "@cloudflare/workerd-darwin-arm64": "1.20260704.1", "@cloudflare/workerd-linux-64": "1.20260704.1", "@cloudflare/workerd-linux-arm64": "1.20260704.1", "@cloudflare/workerd-windows-64": "1.20260704.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-GDZ0jzIYDYfN7rCt/oFJv4BG3QJ+4IS2kfRvxEMY9VKIfPhzo63PH69Ir7ug8LfORCgCtmfkiQVXrqbot7pZTQ=="], + "@astrojs/markdown-remark/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.9.1", "", { "dependencies": { "picomatch": "^4.0.4" } }, "sha512-1pWuARqYom/TzuU3+0ZugsTrKlUydWKuULmDqSMTuonY+9IRDUEGKX/8PXQ1nBxRq3w85uGtd9q9SXfqEldMIQ=="], "@astrojs/react/@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], @@ -5386,8 +5385,6 @@ "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@distilled.cloud/cloudflare-runtime/workerd": ["workerd@1.20260704.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260704.1", "@cloudflare/workerd-darwin-arm64": "1.20260704.1", "@cloudflare/workerd-linux-64": "1.20260704.1", "@cloudflare/workerd-linux-arm64": "1.20260704.1", "@cloudflare/workerd-windows-64": "1.20260704.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-GDZ0jzIYDYfN7rCt/oFJv4BG3QJ+4IS2kfRvxEMY9VKIfPhzo63PH69Ir7ug8LfORCgCtmfkiQVXrqbot7pZTQ=="], - "@effect/platform-node-shared/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "@effect/sql-d1/@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260731.1", "", {}, "sha512-ly+eua642FCR2nbQGi/OxqlVaJI3fNdyqK7G4u6jWCAZ4WYhOBvZWyf0M7s5zurWbNQW3YjelOxuKqR0zYK90w=="], @@ -6310,6 +6307,16 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260704.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ=="], + + "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260704.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6iI7nbOOO8PzEQ6UZVBZB/hv95m8jl0yvyjMuWrF5cJbiLb5zPw3KnpqvGi+aeOs2ZmUkc81i1FWKfXwLXzPRA=="], + + "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260704.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3mT0YHtxT7eLjghu3hKSJDUQoz+AYv8FM43nLPYhM0YiHOxr8OlmvnCb2Lp7v/U3bwd3Gnx7WEqjSppR583mGg=="], + + "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260704.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Opo7cPTPg4x0WwK+eZSDszCyFLKQkOGNCWxF005HRTJ5SJH8h0K9KyrC1k4LBwx3haXSVi+E1eGqo1gZ1Hmhkg=="], + + "@alchemy.run/cloudflare-runtime/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260704.1", "", { "os": "win32", "cpu": "x64" }, "sha512-a97Ecnzhy04x3U052VKPCNp863F7Wf00WY7Ga5P0SbtYvaO1PDzylsHrvFMPKeiDFcOwqiredawmJ+v6bhIgeQ=="], + "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@astrojs/react/vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -6322,16 +6329,6 @@ "@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260704.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-XO+vvdhhTNZSsIWCkZ+JaE/JrFUmQAB0H0y/sVkAf32xa2TYBRvFUMwjSqf6WxtlxcKPQvmbLfpO/UQ0l/q4eQ=="], - - "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260704.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6iI7nbOOO8PzEQ6UZVBZB/hv95m8jl0yvyjMuWrF5cJbiLb5zPw3KnpqvGi+aeOs2ZmUkc81i1FWKfXwLXzPRA=="], - - "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260704.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3mT0YHtxT7eLjghu3hKSJDUQoz+AYv8FM43nLPYhM0YiHOxr8OlmvnCb2Lp7v/U3bwd3Gnx7WEqjSppR583mGg=="], - - "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260704.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Opo7cPTPg4x0WwK+eZSDszCyFLKQkOGNCWxF005HRTJ5SJH8h0K9KyrC1k4LBwx3haXSVi+E1eGqo1gZ1Hmhkg=="], - - "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260704.1", "", { "os": "win32", "cpu": "x64" }, "sha512-a97Ecnzhy04x3U052VKPCNp863F7Wf00WY7Ga5P0SbtYvaO1PDzylsHrvFMPKeiDFcOwqiredawmJ+v6bhIgeQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], diff --git a/examples/alchemy-maple/package.json b/examples/alchemy-maple/package.json index a68ce360e..11b0dcb55 100644 --- a/examples/alchemy-maple/package.json +++ b/examples/alchemy-maple/package.json @@ -10,7 +10,7 @@ "@cloudflare/workers-types": "4.20260603.1", "@maple-dev/alchemy": "workspace:*", "@maple-dev/effect-sdk": "workspace:*", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "typescript": "catalog:tooling" } diff --git a/lib/unitflow/README.md b/lib/unitflow/README.md index 7b72c2734..293615864 100644 --- a/lib/unitflow/README.md +++ b/lib/unitflow/README.md @@ -4,11 +4,11 @@ Vendored copy of [unitflow](https://github.com/timurrakhimzhan/unitflow) (`@unit - **Upstream commit:** `51729b480fd84ed64a121bf2180eae3c45a7c5c2` (2026-07-06) - **Upstream license:** MIT (declared in each upstream `package.json`; upstream ships no standalone LICENSE file) -- **Ported from:** effect `4.0.0-beta.88` → workspace catalog pin (`catalog:effect`, currently `4.0.0-beta.93`) +- **Ported from:** effect `4.0.0-beta.88` → workspace catalog pin (`catalog:effect`, currently `4.0.0-rc.108`) ## Why vendored -Upstream exact-pins `effect@4.0.0-beta.88` as a peer; this workspace pins a bun-patched `4.0.0-beta.93`. Effect v4 betas break between releases, so the package is vendored and ported rather than depended on. When bumping the effect catalog, re-typecheck and re-test this package as part of the upgrade procedure. +Upstream exact-pins `effect@4.0.0-beta.88` as a peer; this workspace pins a bun-patched `4.0.0-rc.108`. Effect v4 betas break between releases, so the package is vendored and ported rather than depended on. When bumping the effect catalog, re-typecheck and re-test this package as part of the upgrade procedure. ## Layout diff --git a/lib/unitflow/package.json b/lib/unitflow/package.json index f35d1e54a..a54a443e4 100644 --- a/lib/unitflow/package.json +++ b/lib/unitflow/package.json @@ -25,7 +25,7 @@ "effect": "catalog:effect" }, "devDependencies": { - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/react": "catalog:react", "typescript": "catalog:tooling", "vitest": "^4.1.9" diff --git a/package.json b/package.json index a211b4276..431007383 100644 --- a/package.json +++ b/package.json @@ -50,13 +50,13 @@ "dependencies": {}, "devDependencies": { "@cloudflare/workers-types": "4.20260603.1", - "@effect/platform-node": "4.0.0-beta.105", - "@effect/tsgo": "0.35.0", - "@effect/vitest": "4.0.0-beta.105", + "@effect/platform-node": "4.0.0-rc.108", + "@effect/tsgo": "0.36.4", + "@effect/vitest": "4.0.0-rc.108", "@maple/infra": "workspace:*", "@tinybirdco/sdk": "catalog:tinybird", "@types/node": "catalog:tooling", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "knip": "^6.17.1", "oxfmt": "^0.55.0", @@ -67,7 +67,7 @@ "typescript": "catalog:tooling" }, "overrides": { - "@effect/sql-d1": "4.0.0-beta.105" + "@effect/sql-d1": "4.0.0-rc.108" }, "packageManager": "bun@1.3.11", "catalog": { @@ -79,10 +79,10 @@ "@clickhouse/client-web": "^1.21.0" }, "effect": { - "@effect/atom-react": "4.0.0-beta.105", + "@effect/atom-react": "4.0.0-rc.108", "@effect/language-service": "^0.87.2", - "@effect/platform-bun": "4.0.0-beta.105", - "effect": "4.0.0-beta.105" + "@effect/platform-bun": "4.0.0-rc.108", + "effect": "4.0.0-rc.108" }, "react": { "@types/react": "^19.2.14", @@ -112,8 +112,7 @@ } }, "patchedDependencies": { - "@effect/vitest@4.0.0-beta.105": "patches/@effect%2Fvitest@4.0.0-beta.105.patch", - "alchemy@2.0.0-beta.70": "patches/alchemy@2.0.0-beta.70.patch", - "effect@4.0.0-beta.105": "patches/effect@4.0.0-beta.105.patch" + "@effect/vitest@4.0.0-rc.108": "patches/@effect%2Fvitest@4.0.0-rc.108.patch", + "effect@4.0.0-rc.108": "patches/effect@4.0.0-rc.108.patch" } } diff --git a/packages/alchemy-maple/README.md b/packages/alchemy-maple/README.md index 13daad175..1142aef9f 100644 --- a/packages/alchemy-maple/README.md +++ b/packages/alchemy-maple/README.md @@ -7,7 +7,7 @@ npm install @maple-dev/alchemy alchemy effect ``` `alchemy` and `effect` are peer dependencies — this release is built and tested against -`alchemy@2.0.0-beta.70` and `effect@4.0.0-beta.105`. +`alchemy@2.0.0-beta.72` and `effect@4.0.0-rc.108`. ## Usage diff --git a/packages/alchemy-maple/package.json b/packages/alchemy-maple/package.json index 44fc39d75..c2113f9e9 100644 --- a/packages/alchemy-maple/package.json +++ b/packages/alchemy-maple/package.json @@ -42,10 +42,10 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/domain": "workspace:*", "@types/node": "catalog:tooling", - "alchemy": "2.0.0-beta.70", + "alchemy": "2.0.0-beta.72", "effect": "catalog:effect", "tsdown": "^0.22.14", "typescript": "catalog:tooling", diff --git a/packages/auth/package.json b/packages/auth/package.json index c1ebdf8a6..82b045c83 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@types/node": "catalog:tooling", "typescript": "catalog:tooling", "vitest": "catalog:" diff --git a/packages/effect-sdk/package.json b/packages/effect-sdk/package.json index d6d880ecf..3253efce3 100644 --- a/packages/effect-sdk/package.json +++ b/packages/effect-sdk/package.json @@ -42,7 +42,7 @@ }, "devDependencies": { "@effect/language-service": "catalog:effect", - "@effect/vitest": "4.0.0-beta.105", + "@effect/vitest": "4.0.0-rc.108", "@maple/browser-session": "workspace:*", "@types/node": "catalog:tooling", "effect": "catalog:effect", diff --git a/patches/@effect%2Fvitest@4.0.0-beta.105.patch b/patches/@effect%2Fvitest@4.0.0-beta.105.patch deleted file mode 100644 index fcf6ede32..000000000 --- a/patches/@effect%2Fvitest@4.0.0-beta.105.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/dist/internal/internal.js b/dist/internal/internal.js ---- a/dist/internal/internal.js -+++ b/dist/internal/internal.js -@@ -1,3 +1,4 @@ - /** - * @since 4.0.0 - */ -+import { getCurrentSuite } from "@vitest/runner"; -@@ -17,4 +18,3 @@ import * as TestClock from "effect/testing/TestClock"; - import * as TestConsole from "effect/testing/TestConsole"; - import * as V from "vitest"; --const getCurrentSuite = V.TestRunner.getCurrentSuite; - const runPromise = /*#__PURE__*/Effect.fnUntraced(function* (effect, _ctx) { -diff --git a/src/internal/internal.ts b/src/internal/internal.ts ---- a/src/internal/internal.ts -+++ b/src/internal/internal.ts -@@ -4,0 +5 @@ -+import { getCurrentSuite } from "@vitest/runner" -@@ -22,2 +22,0 @@ --const getCurrentSuite = V.TestRunner.getCurrentSuite -- diff --git a/patches/@effect%2Fvitest@4.0.0-rc.108.patch b/patches/@effect%2Fvitest@4.0.0-rc.108.patch new file mode 100644 index 000000000..4428cc3f3 --- /dev/null +++ b/patches/@effect%2Fvitest@4.0.0-rc.108.patch @@ -0,0 +1,41 @@ +diff --git a/dist/internal/internal.js b/dist/internal/internal.js +index ddf18ceda305f5ea0c4669e4b2592b45d178f15b..c584b4388624a6ce2c7f5801f13c8008d613e2e8 100644 +--- a/dist/internal/internal.js ++++ b/dist/internal/internal.js +@@ -1,6 +1,7 @@ + /** + * @since 4.0.0 + */ ++import { getCurrentSuite } from "@vitest/runner"; + import * as Cause from "effect/Cause"; + import * as Duration from "effect/Duration"; + import * as Effect from "effect/Effect"; +@@ -16,7 +17,6 @@ import * as fc from "effect/testing/FastCheck"; + import * as TestClock from "effect/testing/TestClock"; + import * as TestConsole from "effect/testing/TestConsole"; + import * as V from "vitest"; +-const getCurrentSuite = V.TestRunner.getCurrentSuite; + const runPromise = /*#__PURE__*/Effect.fnUntraced(function* (effect, _ctx) { + const exit = yield* Effect.exit(effect); + if (Exit.isFailure(exit)) { +diff --git a/src/internal/internal.ts b/src/internal/internal.ts +index f7717b0a60f3b94384fba3d2f11d6895227fcb86..24d96f6644a9c4ea3292308cee4b636fcc569ccb 100644 +--- a/src/internal/internal.ts ++++ b/src/internal/internal.ts +@@ -2,6 +2,7 @@ + * @since 4.0.0 + */ + ++import { getCurrentSuite } from "@vitest/runner" + import * as Cause from "effect/Cause" + import * as Duration from "effect/Duration" + import * as Effect from "effect/Effect" +@@ -19,8 +20,6 @@ import * as TestConsole from "effect/testing/TestConsole" + import * as V from "vitest" + import type * as Vitest from "../index.ts" + +-const getCurrentSuite = V.TestRunner.getCurrentSuite +- + const runPromise: ( + _: Effect.Effect, + ctx?: V.TestContext | undefined diff --git a/patches/alchemy@2.0.0-beta.70.patch b/patches/alchemy@2.0.0-beta.70.patch deleted file mode 100644 index 1cb4ab8ed..000000000 --- a/patches/alchemy@2.0.0-beta.70.patch +++ /dev/null @@ -1,170 +0,0 @@ -diff --git a/bin/exec.js b/bin/exec.js -index 54ff600893844773c1362be6acd87fcc707dc876..cfd2eb15146e78cd885543b78642e99e74a8db43 100644 ---- a/bin/exec.js -+++ b/bin/exec.js -@@ -178,7 +178,7 @@ var init_AuthProvider$1 = __esm({ "lib/Auth/AuthProvider.js": (() => { - ]); - INTERACTIVE_METHODS = new Set(["login", "configure"]); - interactiveMutex = Semaphore.makeUnsafe(1); -- AuthError = class extends Schema.TaggedErrorClass()("AuthError", { -+ AuthError = class extends Schema.TaggedError()("AuthError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect()) - }) {}; -diff --git a/lib/Auth/AuthProvider.js b/lib/Auth/AuthProvider.js -index a3d5290478ff12dd06bf07af9eb05bd502f35d3d..4ee1a77799e161bbc16ff3b42352d1729a0a8ece 100644 ---- a/lib/Auth/AuthProvider.js -+++ b/lib/Auth/AuthProvider.js -@@ -36,7 +36,7 @@ const LOCKED_METHODS = new Set(["read", "login", "logout", "configure"]); - */ - const INTERACTIVE_METHODS = new Set(["login", "configure"]); - const interactiveMutex = Semaphore.makeUnsafe(1); --export class AuthError extends Schema.TaggedErrorClass()("AuthError", { -+export class AuthError extends Schema.TaggedError()("AuthError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect()), - }) { -diff --git a/lib/Bundle/Bundle.js b/lib/Bundle/Bundle.js -index e237b2b4a3901f52300d811a4ea226f40d32dac9..6b287a72c44b0140122d26cdce5cbfad67b4cce3 100644 ---- a/lib/Bundle/Bundle.js -+++ b/lib/Bundle/Bundle.js -@@ -15,7 +15,7 @@ import { rawPlugin } from "./RawPlugin.js"; - * nothing must not require the native bundler to be loadable (#562). - */ - const loadRolldown = () => import("rolldown"); --export class BundleError extends Schema.TaggedErrorClass()("BundleError", { -+export class BundleError extends Schema.TaggedError()("BundleError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect({ includeStack: true })), - }) { -diff --git a/lib/Cli/commands/cloudflare.js b/lib/Cli/commands/cloudflare.js -index d2948880e5cd1487d38d69bfdb7b065a720380fc..d71a1f2acb82ec6d27310637626bdb22192f499e 100644 ---- a/lib/Cli/commands/cloudflare.js -+++ b/lib/Cli/commands/cloudflare.js -@@ -73,7 +73,7 @@ const teardownCommand = Command.make("teardown", { - workerName, - profile, - }).pipe(Effect.provide(services)); --}))).pipe(Command.withHidden, Command.withDescription("Tear down the cloudflare state store")); -+}))).pipe(Command.unlisted, Command.withDescription("Tear down the cloudflare state store")); - /** - * Cloudflare scopes that `buildTokenPolicies` knows how to turn into a policy, - * mapped to a short human label shown as a hint in the selection prompt. -diff --git a/lib/Cli/commands/nuke.js b/lib/Cli/commands/nuke.js -index 8d4828e524de40e5f203cf46cf20bf6637e0db7a..46771fa55a3329ff39edac9f44fa5cc05e788bb0 100644 ---- a/lib/Cli/commands/nuke.js -+++ b/lib/Cli/commands/nuke.js -@@ -560,7 +560,7 @@ const nukeCommand = Command.make("nuke", { - } - }))).pipe( - // hide the command because it's dangerous and we don't want agents to discover and use it --Command.withHidden, Command.withDescription("Enumerate every live resource across the stack's providers and delete " + -+Command.unlisted, Command.withDescription("Enumerate every live resource across the stack's providers and delete " + - "them. DESTRUCTIVE — use --include/--exclude/--filter to scope it.")); - export const unsafeCommand = Command.make("unsafe", {}).pipe(Command.withDescription("Dangerous, irreversible operations."), Command.withSubcommands([nukeCommand])); - //# sourceMappingURL=nuke.js.map -\ No newline at end of file -diff --git a/lib/Cloudflare/Access.js b/lib/Cloudflare/Access.js -index 5aec9ac4ddca929a69dc8543cf6aaf00a478b417..90a777e12d637d60c57a03565c9eb56459cc367c 100644 ---- a/lib/Cloudflare/Access.js -+++ b/lib/Cloudflare/Access.js -@@ -7,7 +7,7 @@ import * as Stream from "effect/Stream"; - import * as ChildProcess from "effect/unstable/process/ChildProcess"; - import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; - import { cachedFunction } from "../Util/cached-function.js"; --export class AccessError extends Schema.TaggedErrorClass()("AccessError", { -+export class AccessError extends Schema.TaggedError()("AccessError", { - message: Schema.String, - cause: Schema.optional(Schema.Defect()), - }) { -diff --git a/lib/Cloudflare/Workers/RuntimeBindings.js b/lib/Cloudflare/Workers/RuntimeBindings.js -index c6c8745811318c01ad8b10a52a8d866c4027ccfb..64b83ee193cf7cfd864ea96f5311411df39263e6 100644 ---- a/lib/Cloudflare/Workers/RuntimeBindings.js -+++ b/lib/Cloudflare/Workers/RuntimeBindings.js -@@ -3,7 +3,7 @@ import * as Effect from "effect/Effect"; - import * as Redacted from "effect/Redacted"; - import * as Schema from "effect/Schema"; - import { isLocalId } from "../LocalRuntime.js"; --export class WorkerValidationError extends Schema.TaggedErrorClass()("WorkerValidationError", { -+export class WorkerValidationError extends Schema.TaggedError()("WorkerValidationError", { - message: Schema.String, - hint: Schema.optional(Schema.String), - value: Schema.Unknown, -diff --git a/src/Auth/AuthProvider.ts b/src/Auth/AuthProvider.ts -index c4d8394ebbf2a3754892730d2729619bc011ab82..9a146a99f1d31493e530b2dca56ee527999c85b5 100644 ---- a/src/Auth/AuthProvider.ts -+++ b/src/Auth/AuthProvider.ts -@@ -40,7 +40,7 @@ const LOCKED_METHODS = new Set(["read", "login", "logout", "configure"]); - const INTERACTIVE_METHODS = new Set(["login", "configure"]); - const interactiveMutex = Semaphore.makeUnsafe(1); - --export class AuthError extends Schema.TaggedErrorClass()( -+export class AuthError extends Schema.TaggedError()( - "AuthError", - { - message: Schema.String, -diff --git a/src/Bundle/Bundle.ts b/src/Bundle/Bundle.ts -index 4e8af036eafdcc2ebb6c096e47de571b8187865a..3aa996e0400bc6506819da53c9ebbfc73159943f 100644 ---- a/src/Bundle/Bundle.ts -+++ b/src/Bundle/Bundle.ts -@@ -95,7 +95,7 @@ export interface BundleFile { - readonly hash: string; - } - --export class BundleError extends Schema.TaggedErrorClass()( -+export class BundleError extends Schema.TaggedError()( - "BundleError", - { - message: Schema.String, -diff --git a/src/Cli/commands/cloudflare.ts b/src/Cli/commands/cloudflare.ts -index c0c25a9af3c99fb656ffd5cab5d5bc2f26e51b43..8eaab97a79f01fd74513816a2b81537946bde513 100644 ---- a/src/Cli/commands/cloudflare.ts -+++ b/src/Cli/commands/cloudflare.ts -@@ -142,7 +142,7 @@ const teardownCommand = Command.make( - }), - ), - ).pipe( -- Command.withHidden, -+ Command.unlisted, - Command.withDescription("Tear down the cloudflare state store"), - ); - -diff --git a/src/Cli/commands/nuke.ts b/src/Cli/commands/nuke.ts -index e3651c1094135bec0339b72225c04f6cc09d1180..df2e8494fe3fcbb5caf7a18402ae0808ea4bd1e6 100644 ---- a/src/Cli/commands/nuke.ts -+++ b/src/Cli/commands/nuke.ts -@@ -799,7 +799,7 @@ const nukeCommand = Command.make( - ), - ).pipe( - // hide the command because it's dangerous and we don't want agents to discover and use it -- Command.withHidden, -+ Command.unlisted, - Command.withDescription( - "Enumerate every live resource across the stack's providers and delete " + - "them. DESTRUCTIVE — use --include/--exclude/--filter to scope it.", -diff --git a/src/Cloudflare/Access.ts b/src/Cloudflare/Access.ts -index 0d8d68502dbbd0c31519d89b28f79e345215710b..94193074129747bf1e9c248485d7c9a5f84a3de2 100644 ---- a/src/Cloudflare/Access.ts -+++ b/src/Cloudflare/Access.ts -@@ -8,7 +8,7 @@ import * as ChildProcess from "effect/unstable/process/ChildProcess"; - import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; - import { cachedFunction } from "../Util/cached-function.ts"; - --export class AccessError extends Schema.TaggedErrorClass()( -+export class AccessError extends Schema.TaggedError()( - "AccessError", - { - message: Schema.String, -diff --git a/src/Cloudflare/Workers/RuntimeBindings.ts b/src/Cloudflare/Workers/RuntimeBindings.ts -index 4344de609722731789d061b0beead971d41fcaf5..9c96fe2e2de7bbe9136893efb0eab3d0cce4605b 100644 ---- a/src/Cloudflare/Workers/RuntimeBindings.ts -+++ b/src/Cloudflare/Workers/RuntimeBindings.ts -@@ -42,7 +42,7 @@ import * as Schema from "effect/Schema"; - import { isLocalId } from "../LocalRuntime.ts"; - import type { WorkerBinding } from "./WorkerBinding.ts"; - --export class WorkerValidationError extends Schema.TaggedErrorClass()( -+export class WorkerValidationError extends Schema.TaggedError()( - "WorkerValidationError", - { - message: Schema.String, diff --git a/patches/effect@4.0.0-beta.105.patch b/patches/effect@4.0.0-beta.105.patch deleted file mode 100644 index 40e4941fd..000000000 --- a/patches/effect@4.0.0-beta.105.patch +++ /dev/null @@ -1,170 +0,0 @@ -diff --git a/dist/internal/redacted.js b/dist/internal/redacted.js ---- a/dist/internal/redacted.js -+++ b/dist/internal/redacted.js -@@ -1,5 +1,11 @@ - /** @internal */ --export const redactedRegistry = /*#__PURE__*/new WeakMap(); -+// Patched (maple): share the registry across duplicate `effect` module -+// instances via a Symbol.for-keyed global (like effect v3's globalValue). -+// Bun's isolated linker gives the alchemy CLI and the user's alchemy.run.ts -+// two path-distinct copies of this module; a module-local WeakMap makes -+// `Redacted.value` throw "Unable to get redacted value" for a Redacted made -+// by the other copy. -+export const redactedRegistry = globalThis[Symbol.for("effect/Redacted/redactedRegistry")] ??= /*#__PURE__*/new WeakMap(); - /** @internal */ - export const value = self => { - if (redactedRegistry.has(self)) { -diff --git a/dist/Schema.d.ts b/dist/Schema.d.ts ---- a/dist/Schema.d.ts -+++ b/dist/Schema.d.ts -@@ -9856,6 +9856,8 @@ export declare const TaggedError: { - } & S["fields"]]>, Cause_.YieldableError & Brand>; - }; - }; -+/** @deprecated Compatibility alias for dependencies that still target Effect beta.102. */ -+export declare const TaggedErrorClass: typeof TaggedError; - /** - * A thunk that, given the `fast-check` module, returns an `Arbitrary`. - * Use this type when you need to defer instantiation of the arbitrary, -diff --git a/dist/Schema.js b/dist/Schema.js ---- a/dist/Schema.js -+++ b/dist/Schema.js -@@ -9211,6 +9211,8 @@ export const TaggedError = identifier => { - return Error(identifier ?? tagValue)(struct, annotations); - }; - }; -+/** @deprecated Compatibility alias for dependencies that still target Effect beta.102. */ -+export const TaggedErrorClass = TaggedError; - /** - * Derives a {@link LazyArbitrary} from a schema. The result is memoized so - * repeated calls with the same schema are cheap. -diff --git a/dist/unstable/ai/McpServer.d.ts b/dist/unstable/ai/McpServer.d.ts ---- a/dist/unstable/ai/McpServer.d.ts -+++ b/dist/unstable/ai/McpServer.d.ts -@@ -24,7 +24,7 @@ import * as RpcMessage from "../rpc/RpcMessage.ts"; - import * as RpcServer from "../rpc/RpcServer.ts"; - import type * as McpProtocol from "./McpProtocol.ts"; - import { CallToolResult, ElicitationDeclined, GetPromptResult, InternalError, InvalidParams, McpErrorBase, McpServerClient, Prompt, Resource, ResourceTemplate, ServerNotificationRpcs, Tool as McpTool } from "./McpSchema.ts"; --import type { CallTool, ClientCapabilities, Complete, CompleteResult, GetPrompt, Param, PromptMessage, ReadResourceResult } from "./McpSchema.ts"; -+import type { CallTool, ClientCapabilities, Complete, CompleteResult, GetPrompt, Initialize, Param, PromptMessage, ReadResourceResult } from "./McpSchema.ts"; - import * as Tool from "./Tool.ts"; - import type * as Toolkit from "./Toolkit.ts"; - type CompletionContext = typeof Complete.payloadSchema.Type["context"]; -@@ -167,6 +167,7 @@ export declare const run: (options: { - readonly version: string; - readonly protocols: Arr.NonEmptyReadonlyArray; - readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; -+ readonly clientSessions?: Map | undefined; - }) => Effect.Effect; - /** - * Creates a layer that starts an MCP server over an existing -@@ -202,6 +203,7 @@ export declare const layer: (options: { - readonly version: string; - readonly protocols: Arr.NonEmptyReadonlyArray; - readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; -+ readonly clientSessions?: Map | undefined; - }) => Layer.Layer; - /** - * Runs the McpServer, using stdio for input and output. -@@ -254,6 +256,7 @@ export declare const layerStdio: (options: { - readonly version: string; - readonly protocols: Arr.NonEmptyReadonlyArray; - readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; -+ readonly clientSessions?: Map | undefined; - }) => Layer.Layer; - /** - * Registers a Streamable HTTP MCP endpoint at `options.path`. -@@ -281,6 +284,7 @@ export declare const layerHttp: (options: { - readonly protocols: Arr.NonEmptyReadonlyArray; - readonly allowedOrigins?: ReadonlyArray | undefined; - readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; -+ readonly clientSessions?: Map | undefined; - }) => Layer.Layer; - /** - * Registers a `Toolkit` with the `McpServer`. -diff --git a/dist/unstable/ai/McpServer.js b/dist/unstable/ai/McpServer.js ---- a/dist/unstable/ai/McpServer.js -+++ b/dist/unstable/ai/McpServer.js -@@ -228,15 +228,56 @@ const decodeCancelledNotification = /*#__PURE__*/Schema.decodeUnknownEffect(/*# - const requestKey = requestId => `${typeof requestId}:${requestId}`; - class McpClientKey extends Data.Class {} - class McpProtocolState extends /*#__PURE__*/Context.Service()("effect/ai/McpServer/McpProtocolState") {} --const makeMcpProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (protocols) { -+class PersistedSessionMap extends Map { -+ constructor(clientSessions, protocolRegistry) { -+ super(); -+ this.clientSessions = clientSessions; -+ this.protocolRegistry = protocolRegistry; -+ } -+ get(sessionId) { -+ const current = super.get(sessionId); -+ if (current !== undefined) { -+ return current; -+ } -+ const initializePayload = this.clientSessions.get(sessionId); -+ if (initializePayload === undefined) { -+ return undefined; -+ } -+ const session = { -+ initializePayload, -+ protocol: this.protocolRegistry.select(getOfferedProtocolVersion(initializePayload)), -+ resourceSubscriptions: undefined, -+ logLevel: { -+ _tag: "Effect", -+ level: "Info" -+ } -+ }; -+ super.set(sessionId, session); -+ return session; -+ } -+ set(sessionId, session) { -+ this.clientSessions.set(sessionId, session.initializePayload); -+ return super.set(sessionId, session); -+ } -+ delete(sessionId) { -+ this.clientSessions.delete(sessionId); -+ return super.delete(sessionId); -+ } -+ clear() { -+ this.clientSessions.clear(); -+ super.clear(); -+ } -+} -+const makeMcpProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (protocols, clientSessions) { -+ const protocolRegistry = yield* McpProtocolRegistry.make(protocols); - return McpProtocolState.of({ - sessions: { -- bySessionId: new Map(), -+ bySessionId: clientSessions === undefined ? new Map() : new PersistedSessionMap(clientSessions, protocolRegistry), - byClientId: new Map() - }, -- protocolRegistry: yield* McpProtocolRegistry.make(protocols) -+ protocolRegistry - }); - }); --const layerMcpProtocolState = protocols => Layer.effect(McpProtocolState)(makeMcpProtocolState(protocols)); -+const layerMcpProtocolState = (protocols, clientSessions) => Layer.effect(McpProtocolState)(makeMcpProtocolState(protocols, clientSessions)); - /** - * Runs an MCP server over the current `RpcServer.Protocol`. -@@ -253,5 +294,5 @@ const layerMcpProtocolState = protocols => Layer.effect(McpProtocolState)(makeMc - export const run = /*#__PURE__*/Effect.fnUntraced(function* (options) { - const protocolStateOption = yield* Effect.serviceOption(McpProtocolState); -- const protocolState = Option.isSome(protocolStateOption) ? protocolStateOption.value : yield* makeMcpProtocolState(options.protocols); -+ const protocolState = Option.isSome(protocolStateOption) ? protocolStateOption.value : yield* makeMcpProtocolState(options.protocols, options.clientSessions); - return yield* runWithProtocolState(options, protocolState); - }); -@@ -568,5 +609,5 @@ const runWithProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (options, - * @since 4.0.0 - */ --export const layer = options => layerWithProtocolState(options).pipe(Layer.provide(layerMcpProtocolState(options.protocols))); -+export const layer = options => layerWithProtocolState(options).pipe(Layer.provide(layerMcpProtocolState(options.protocols, options.clientSessions))); - const layerWithProtocolState = options => Layer.effectDiscard(Effect.gen(function* () { - const protocolState = yield* McpProtocolState; -@@ -709,6 +750,6 @@ export const layerStdio = options => layer(options).pipe(Layer.provide(RpcServer - * @since 4.0.0 - */ - export const layerHttp = options => { -- const protocolState = layerMcpProtocolState(options.protocols); -+ const protocolState = layerMcpProtocolState(options.protocols, options.clientSessions); - const methodNotAllowedResponse = HttpServerResponse.empty({ - status: 405, diff --git a/patches/effect@4.0.0-rc.108.patch b/patches/effect@4.0.0-rc.108.patch new file mode 100644 index 000000000..53ffc241b --- /dev/null +++ b/patches/effect@4.0.0-rc.108.patch @@ -0,0 +1,155 @@ +diff --git a/dist/internal/redacted.js b/dist/internal/redacted.js +index b71c514f6c36983a6fb94a663e9ff878d58a3c1b..6192bdac6f761aef3c8e1ed2f3b4747ffd55692f 100644 +--- a/dist/internal/redacted.js ++++ b/dist/internal/redacted.js +@@ -1,5 +1,11 @@ + /** @internal */ +-export const redactedRegistry = /*#__PURE__*/new WeakMap(); ++// Patched (maple): share the registry across duplicate `effect` module ++// instances via a Symbol.for-keyed global (like effect v3's globalValue). ++// Bun's isolated linker gives the alchemy CLI and the user's alchemy.run.ts ++// two path-distinct copies of this module; a module-local WeakMap makes ++// `Redacted.value` throw "Unable to get redacted value" for a Redacted made ++// by the other copy. ++export const redactedRegistry = globalThis[Symbol.for("effect/Redacted/redactedRegistry")] ??= /*#__PURE__*/new WeakMap(); + /** @internal */ + export const value = self => { + if (redactedRegistry.has(self)) { +diff --git a/dist/unstable/ai/McpServer.d.ts b/dist/unstable/ai/McpServer.d.ts +index 58e3fd6cf9c4e4c3d3c970ebef5ae2e44a16da35..ccd5027e268bd4888d66b443c557a80dcb2569be 100644 +--- a/dist/unstable/ai/McpServer.d.ts ++++ b/dist/unstable/ai/McpServer.d.ts +@@ -25,7 +25,7 @@ import * as RpcMessage from "../rpc/RpcMessage.ts"; + import * as RpcServer from "../rpc/RpcServer.ts"; + import type * as McpProtocol from "./McpProtocol.ts"; + import { CallToolResult, ElicitationDeclined, GetPromptResult, InternalError, InvalidParams, McpErrorBase, McpServerClient, Prompt, Resource, ResourceTemplate, ServerNotificationRpcs, Tool as McpTool } from "./McpSchema.ts"; +-import type { CallTool, ClientCapabilities, Complete, CompleteResult, GetPrompt, Param, PromptMessage, ReadResourceResult } from "./McpSchema.ts"; ++import type { CallTool, ClientCapabilities, Complete, CompleteResult, GetPrompt, Initialize, Param, PromptMessage, ReadResourceResult } from "./McpSchema.ts"; + import * as Tool from "./Tool.ts"; + import type * as Toolkit from "./Toolkit.ts"; + type CompletionContext = typeof Complete.payloadSchema.Type["context"]; +@@ -167,6 +167,7 @@ export declare const run: (options: { + readonly version: string; + readonly protocols: Arr.NonEmptyReadonlyArray; + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; ++ readonly clientSessions?: Map | undefined; + }) => Effect.Effect; + /** + * Creates a layer that starts an MCP server over an existing +@@ -202,6 +203,7 @@ export declare const layer: (options: { + readonly version: string; + readonly protocols: Arr.NonEmptyReadonlyArray; + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; ++ readonly clientSessions?: Map | undefined; + }) => Layer.Layer; + /** + * Runs the McpServer, using stdio for input and output. +@@ -254,6 +256,7 @@ export declare const layerStdio: (options: { + readonly version: string; + readonly protocols: Arr.NonEmptyReadonlyArray; + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; ++ readonly clientSessions?: Map | undefined; + }) => Layer.Layer; + /** + * Registers a Streamable HTTP MCP endpoint at `options.path`. +@@ -281,6 +284,7 @@ export declare const layerHttp: (options: { + readonly protocols: Arr.NonEmptyReadonlyArray; + readonly allowedOrigins?: ReadonlyArray | undefined; + readonly extensions?: Record<`${string}/${string}`, unknown> | undefined; ++ readonly clientSessions?: Map | undefined; + }) => Layer.Layer; + /** + * Registers a `Toolkit` with the `McpServer`. +diff --git a/dist/unstable/ai/McpServer.js b/dist/unstable/ai/McpServer.js +index 073350187152036c2c8432428ea073645d8eb77d..51a027528d22a665b77e4bfef1a999a256efa475 100644 +--- a/dist/unstable/ai/McpServer.js ++++ b/dist/unstable/ai/McpServer.js +@@ -228,16 +228,57 @@ const decodeCancelledNotification = /*#__PURE__*/Schema.decodeUnknownEffect(/*#_ + const requestKey = requestId => `${typeof requestId}:${requestId}`; + class McpClientKey extends Data.Class {} + class McpProtocolState extends /*#__PURE__*/Context.Service()("effect/ai/McpServer/McpProtocolState") {} +-const makeMcpProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (protocols) { ++class PersistedSessionMap extends Map { ++ constructor(clientSessions, protocolRegistry) { ++ super(); ++ this.clientSessions = clientSessions; ++ this.protocolRegistry = protocolRegistry; ++ } ++ get(sessionId) { ++ const current = super.get(sessionId); ++ if (current !== undefined) { ++ return current; ++ } ++ const initializePayload = this.clientSessions.get(sessionId); ++ if (initializePayload === undefined) { ++ return undefined; ++ } ++ const session = { ++ initializePayload, ++ protocol: this.protocolRegistry.select(getOfferedProtocolVersion(initializePayload)), ++ resourceSubscriptions: undefined, ++ logLevel: { ++ _tag: "Effect", ++ level: "Info" ++ } ++ }; ++ super.set(sessionId, session); ++ return session; ++ } ++ set(sessionId, session) { ++ this.clientSessions.set(sessionId, session.initializePayload); ++ return super.set(sessionId, session); ++ } ++ delete(sessionId) { ++ this.clientSessions.delete(sessionId); ++ return super.delete(sessionId); ++ } ++ clear() { ++ this.clientSessions.clear(); ++ super.clear(); ++ } ++} ++const makeMcpProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (protocols, clientSessions) { ++ const protocolRegistry = yield* McpProtocolRegistry.make(protocols); + return McpProtocolState.of({ + sessions: { +- bySessionId: new Map(), ++ bySessionId: clientSessions === undefined ? new Map() : new PersistedSessionMap(clientSessions, protocolRegistry), + byClientId: new Map() + }, +- protocolRegistry: yield* McpProtocolRegistry.make(protocols) ++ protocolRegistry + }); + }); +-const layerMcpProtocolState = protocols => Layer.effect(McpProtocolState)(makeMcpProtocolState(protocols)); ++const layerMcpProtocolState = (protocols, clientSessions) => Layer.effect(McpProtocolState)(makeMcpProtocolState(protocols, clientSessions)); + /** + * Runs an MCP server over the current `RpcServer.Protocol`. + * +@@ -252,7 +293,7 @@ const layerMcpProtocolState = protocols => Layer.effect(McpProtocolState)(makeMc + */ + export const run = /*#__PURE__*/Effect.fnUntraced(function* (options) { + const protocolStateOption = yield* Effect.serviceOption(McpProtocolState); +- const protocolState = Option.isSome(protocolStateOption) ? protocolStateOption.value : yield* makeMcpProtocolState(options.protocols); ++ const protocolState = Option.isSome(protocolStateOption) ? protocolStateOption.value : yield* makeMcpProtocolState(options.protocols, options.clientSessions); + return yield* runWithProtocolState(options, protocolState); + }); + const runWithProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (options, protocolState) { +@@ -579,7 +620,7 @@ const runWithProtocolState = /*#__PURE__*/Effect.fnUntraced(function* (options, + * @category layers + * @since 4.0.0 + */ +-export const layer = options => layerWithProtocolState(options).pipe(Layer.provide(layerMcpProtocolState(options.protocols))); ++export const layer = options => layerWithProtocolState(options).pipe(Layer.provide(layerMcpProtocolState(options.protocols, options.clientSessions))); + const layerWithProtocolState = options => Layer.effectDiscard(Effect.gen(function* () { + const protocolState = yield* McpProtocolState; + yield* Effect.forkScoped(runWithProtocolState(options, protocolState)); +@@ -721,7 +762,7 @@ export const layerStdio = options => layer(options).pipe(Layer.provide(RpcServer + * @since 4.0.0 + */ + export const layerHttp = options => { +- const protocolState = layerMcpProtocolState(options.protocols); ++ const protocolState = layerMcpProtocolState(options.protocols, options.clientSessions); + const methodNotAllowedResponse = HttpServerResponse.empty({ + status: 405, + headers: {